I finally had time to look at this again, and FFI is in fact packing everything correctly.
Just the IPv4 header alone packs correctly, but when you prepend the ethernet header to it, it causes FFI to place 2 bytes of padding after the checksum field, to align the src_addr field on a 4 byte boundary. Run https://gist.github.com/8171ff430dd66f3812da and you'll see what I mean.
There are 2 ways to work around it:
1) Use struct packing
2) Use separate ethernet and ipv4 headers structs, and access the ipv4 header something like:
eth = ... # get pointer to ethernet packet and wrap in an ethernet struct
ipv4 = IPv4_header.new(eth.pointer + 14)