Add hex2pcap
This commit is contained in:
@@ -0,0 +1,381 @@
|
||||
// Package inet recalculates length fields and checksums of IPv4/IPv6 packets
|
||||
// and their transport headers (TCP, UDP, ICMP, ICMPv6, SCTP).
|
||||
package inet
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash/crc32"
|
||||
)
|
||||
|
||||
// IP protocol numbers.
|
||||
const (
|
||||
protoHopByHop = 0
|
||||
protoICMP = 1
|
||||
protoTCP = 6
|
||||
protoUDP = 17
|
||||
protoRouting = 43
|
||||
protoFragment = 44
|
||||
protoAH = 51
|
||||
protoICMPv6 = 58
|
||||
protoDstOpts = 60
|
||||
protoSCTP = 132
|
||||
)
|
||||
|
||||
// Change records a field rewritten by Fix.
|
||||
type Change struct {
|
||||
Field string
|
||||
Old string
|
||||
New string
|
||||
}
|
||||
|
||||
// Result describes what Fix did.
|
||||
type Result struct {
|
||||
Changes []Change // fields that were rewritten (including ones whose value did not change)
|
||||
Notes []string // informational messages, e.g. why a checksum was skipped
|
||||
}
|
||||
|
||||
func (r *Result) change16(field string, b []byte, v uint16) {
|
||||
old := binary.BigEndian.Uint16(b)
|
||||
binary.BigEndian.PutUint16(b, v)
|
||||
r.Changes = append(r.Changes, Change{field, fmt.Sprintf("0x%04x (%d)", old, old), fmt.Sprintf("0x%04x (%d)", v, v)})
|
||||
}
|
||||
|
||||
func (r *Result) notef(format string, args ...any) {
|
||||
r.Notes = append(r.Notes, fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
// Fix rewrites, in place, the length fields and checksums of the IPv4 or IPv6
|
||||
// packet in pkt. pkt must start at the IP header and extend to the end of the
|
||||
// packet; everything after the IP header is taken as the IP payload.
|
||||
//
|
||||
// hdrLen is the number of leading bytes of pkt that are headers, i.e. where
|
||||
// the user's header input ends and the payload begins, or 0 if unknown. When
|
||||
// known it is used to set the TCP data offset and the length of the SCTP
|
||||
// chunk that the payload belongs to; when unknown those fields are left alone.
|
||||
// The IPv4 IHL and IPv6 extension header lengths are always taken as given.
|
||||
func Fix(pkt []byte, hdrLen int) (Result, error) {
|
||||
if len(pkt) == 0 {
|
||||
return Result{}, errors.New("no IP header data")
|
||||
}
|
||||
switch v := pkt[0] >> 4; v {
|
||||
case 4:
|
||||
return fixIPv4(pkt, hdrLen)
|
||||
case 6:
|
||||
return fixIPv6(pkt, hdrLen)
|
||||
default:
|
||||
return Result{}, fmt.Errorf("not an IPv4/IPv6 packet: IP version nibble is %d", v)
|
||||
}
|
||||
}
|
||||
|
||||
func fixIPv4(pkt []byte, hdrLen int) (Result, error) {
|
||||
var r Result
|
||||
if len(pkt) < 20 {
|
||||
return r, fmt.Errorf("IPv4 header truncated: %d bytes, need at least 20", len(pkt))
|
||||
}
|
||||
ihl := int(pkt[0]&0x0f) * 4
|
||||
if ihl < 20 {
|
||||
return r, fmt.Errorf("invalid IPv4 header length (IHL) %d bytes", ihl)
|
||||
}
|
||||
if ihl > len(pkt) {
|
||||
return r, fmt.Errorf("IPv4 header length (IHL) %d exceeds packet length %d", ihl, len(pkt))
|
||||
}
|
||||
if len(pkt) > 0xffff {
|
||||
return r, fmt.Errorf("IPv4 packet length %d exceeds 65535", len(pkt))
|
||||
}
|
||||
if off := badIPv4Option(pkt[20:ihl]); off >= 0 {
|
||||
r.notef("IPv4 option at offset %d does not fit in the header length (IHL %d bytes); check IHL", 20+off, ihl)
|
||||
}
|
||||
|
||||
r.change16("IPv4 total length", pkt[2:4], uint16(len(pkt)))
|
||||
binary.BigEndian.PutUint16(pkt[10:12], 0)
|
||||
r.change16("IPv4 header checksum", pkt[10:12], checksum(pkt[:ihl], 0))
|
||||
|
||||
flagsFrag := binary.BigEndian.Uint16(pkt[6:8])
|
||||
if flagsFrag&0x2000 != 0 || flagsFrag&0x1fff != 0 {
|
||||
r.notef("IPv4 packet is a fragment; transport header not changed")
|
||||
return r, nil
|
||||
}
|
||||
|
||||
proto := pkt[9]
|
||||
payload := pkt[ihl:]
|
||||
pseudo := sum(pkt[12:16], 0)
|
||||
pseudo = sum(pkt[16:20], pseudo)
|
||||
pseudo += uint32(proto) + uint32(len(payload))
|
||||
fixTransport(&r, proto, payload, transportHdrLen(&r, hdrLen, ihl), pseudo, false)
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// badIPv4Option walks IPv4 options and returns the offset of the first option
|
||||
// that runs past the end of opts, or -1 if they are well formed.
|
||||
func badIPv4Option(opts []byte) int {
|
||||
for i := 0; i < len(opts); {
|
||||
switch opts[i] {
|
||||
case 0: // end of option list, the rest is padding
|
||||
return -1
|
||||
case 1: // no-op
|
||||
i++
|
||||
continue
|
||||
}
|
||||
if i+1 >= len(opts) || opts[i+1] < 2 || i+int(opts[i+1]) > len(opts) {
|
||||
return i
|
||||
}
|
||||
i += int(opts[i+1])
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func fixIPv6(pkt []byte, hdrLen int) (Result, error) {
|
||||
var r Result
|
||||
if len(pkt) < 40 {
|
||||
return r, fmt.Errorf("IPv6 header truncated: %d bytes, need 40", len(pkt))
|
||||
}
|
||||
if len(pkt)-40 > 0xffff {
|
||||
return r, fmt.Errorf("IPv6 payload length %d exceeds 65535 (jumbograms not supported)", len(pkt)-40)
|
||||
}
|
||||
r.change16("IPv6 payload length", pkt[4:6], uint16(len(pkt)-40))
|
||||
|
||||
dst := pkt[24:40]
|
||||
next := pkt[6]
|
||||
off := 40
|
||||
for isIPv6ExtHeader(next) {
|
||||
if off+8 > len(pkt) {
|
||||
r.notef("IPv6 extension header truncated; transport header not changed")
|
||||
return r, nil
|
||||
}
|
||||
var hlen int
|
||||
switch next {
|
||||
case protoFragment:
|
||||
hlen = 8
|
||||
fragOffMore := binary.BigEndian.Uint16(pkt[off+2 : off+4])
|
||||
if fragOffMore&0xfff9 != 0 { // fragment offset != 0 or M flag set
|
||||
r.notef("IPv6 packet is a fragment; transport header not changed")
|
||||
return r, nil
|
||||
}
|
||||
case protoAH:
|
||||
hlen = (int(pkt[off+1]) + 2) * 4
|
||||
default:
|
||||
hlen = (int(pkt[off+1]) + 1) * 8
|
||||
}
|
||||
if off+hlen > len(pkt) {
|
||||
r.notef("IPv6 extension header truncated; transport header not changed")
|
||||
return r, nil
|
||||
}
|
||||
if next == protoRouting {
|
||||
if d := routingFinalDst(pkt[off : off+hlen]); d != nil {
|
||||
dst = d
|
||||
}
|
||||
}
|
||||
next = pkt[off]
|
||||
off += hlen
|
||||
}
|
||||
|
||||
payload := pkt[off:]
|
||||
pseudo := sum(pkt[8:24], 0)
|
||||
pseudo = sum(dst, pseudo)
|
||||
pseudo += uint32(len(payload)) + uint32(next)
|
||||
fixTransport(&r, next, payload, transportHdrLen(&r, hdrLen, off), pseudo, true)
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// transportHdrLen converts the header length given to Fix into the number of
|
||||
// transport-layer header bytes, or -1 if unknown.
|
||||
func transportHdrLen(r *Result, hdrLen, ipHdrLen int) int {
|
||||
if hdrLen <= 0 {
|
||||
return -1
|
||||
}
|
||||
if hdrLen < ipHdrLen {
|
||||
r.notef("IP header (%d bytes including options/extension headers) extends past the layer 3 field (%d bytes)", ipHdrLen, hdrLen)
|
||||
return 0
|
||||
}
|
||||
return hdrLen - ipHdrLen
|
||||
}
|
||||
|
||||
func isIPv6ExtHeader(proto byte) bool {
|
||||
switch proto {
|
||||
case protoHopByHop, protoRouting, protoFragment, protoAH, protoDstOpts:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// routingFinalDst returns the final destination address carried in an IPv6
|
||||
// routing header, which the upper-layer pseudo header must use (RFC 8200 8.1).
|
||||
// It returns nil when the IPv6 destination address field should be used.
|
||||
func routingFinalDst(rh []byte) []byte {
|
||||
routingType, segmentsLeft := rh[2], rh[3]
|
||||
if segmentsLeft == 0 {
|
||||
return nil
|
||||
}
|
||||
switch routingType {
|
||||
case 0: // deprecated source route: last address in the list
|
||||
n := int(rh[1]) / 2
|
||||
if n > 0 && 8+n*16 <= len(rh) {
|
||||
return rh[8+(n-1)*16 : 8+n*16]
|
||||
}
|
||||
case 2, 4: // Mobile IPv6 home address / Segment Routing: first address in the list
|
||||
if len(rh) >= 24 {
|
||||
return rh[8:24]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// fixTransport fixes the transport header at the start of payload. hdrLen is
|
||||
// the number of transport header bytes the user entered before the payload,
|
||||
// or -1 if unknown.
|
||||
func fixTransport(r *Result, proto byte, payload []byte, hdrLen int, pseudo uint32, ipv6 bool) {
|
||||
switch {
|
||||
case proto == protoTCP:
|
||||
if len(payload) < 20 {
|
||||
r.notef("TCP header truncated (%d bytes); TCP checksum not recalculated", len(payload))
|
||||
return
|
||||
}
|
||||
fixTCPDataOffset(r, payload, hdrLen)
|
||||
binary.BigEndian.PutUint16(payload[16:18], 0)
|
||||
r.change16("TCP checksum", payload[16:18], checksum(payload, pseudo))
|
||||
|
||||
case proto == protoUDP:
|
||||
if len(payload) < 8 {
|
||||
r.notef("UDP header truncated (%d bytes); UDP length and checksum not recalculated", len(payload))
|
||||
return
|
||||
}
|
||||
r.change16("UDP length", payload[4:6], uint16(len(payload)))
|
||||
binary.BigEndian.PutUint16(payload[6:8], 0)
|
||||
cs := checksum(payload, pseudo)
|
||||
if cs == 0 {
|
||||
cs = 0xffff // zero means "no checksum" in UDP
|
||||
}
|
||||
r.change16("UDP checksum", payload[6:8], cs)
|
||||
|
||||
case proto == protoICMP && !ipv6:
|
||||
if len(payload) < 4 {
|
||||
r.notef("ICMP header truncated (%d bytes); ICMP checksum not recalculated", len(payload))
|
||||
return
|
||||
}
|
||||
binary.BigEndian.PutUint16(payload[2:4], 0)
|
||||
r.change16("ICMP checksum", payload[2:4], checksum(payload, 0))
|
||||
|
||||
case proto == protoICMPv6 && ipv6:
|
||||
if len(payload) < 4 {
|
||||
r.notef("ICMPv6 header truncated (%d bytes); ICMPv6 checksum not recalculated", len(payload))
|
||||
return
|
||||
}
|
||||
binary.BigEndian.PutUint16(payload[2:4], 0)
|
||||
r.change16("ICMPv6 checksum", payload[2:4], checksum(payload, pseudo))
|
||||
|
||||
case proto == protoSCTP:
|
||||
if len(payload) < 12 {
|
||||
r.notef("SCTP common header truncated (%d bytes); SCTP checksum not recalculated", len(payload))
|
||||
return
|
||||
}
|
||||
fixSCTPChunkLength(r, payload, hdrLen)
|
||||
old := binary.BigEndian.Uint32(payload[8:12])
|
||||
binary.BigEndian.PutUint32(payload[8:12], 0)
|
||||
// CRC32c is transmitted least significant byte first (RFC 9260 appendix A).
|
||||
binary.LittleEndian.PutUint32(payload[8:12], crc32.Checksum(payload, castagnoli))
|
||||
v := binary.BigEndian.Uint32(payload[8:12])
|
||||
r.Changes = append(r.Changes, Change{"SCTP checksum (CRC32c)", fmt.Sprintf("0x%08x", old), fmt.Sprintf("0x%08x", v)})
|
||||
|
||||
default:
|
||||
r.notef("IP protocol %d: no transport checksum to recalculate", proto)
|
||||
}
|
||||
}
|
||||
|
||||
// fixTCPDataOffset sets the TCP data offset to the TCP header length (with
|
||||
// options) entered in the layer 3 field.
|
||||
func fixTCPDataOffset(r *Result, tcp []byte, hdrLen int) {
|
||||
switch {
|
||||
case hdrLen < 0:
|
||||
return
|
||||
case hdrLen == 0:
|
||||
r.notef("TCP header is in the payload field; TCP data offset not changed")
|
||||
return
|
||||
case hdrLen < 20 || hdrLen > 60 || hdrLen%4 != 0:
|
||||
r.notef("TCP header in the layer 3 field is %d bytes (must be 20-60 and a multiple of 4); TCP data offset not changed", hdrLen)
|
||||
return
|
||||
}
|
||||
old := int(tcp[12] >> 4)
|
||||
tcp[12] = byte(hdrLen/4)<<4 | tcp[12]&0x0f
|
||||
r.Changes = append(r.Changes, Change{"TCP data offset",
|
||||
fmt.Sprintf("%d (%d bytes)", old, old*4), fmt.Sprintf("%d (%d bytes)", hdrLen/4, hdrLen)})
|
||||
}
|
||||
|
||||
// fixSCTPChunkLength sets the length of the SCTP chunk that the payload
|
||||
// belongs to: the last chunk whose header starts in the layer 3 field. Chunks
|
||||
// before it are skipped using their own length fields. The chunk is taken to
|
||||
// extend to the end of the packet, so the payload must not include padding
|
||||
// unless the chunk length is already correct.
|
||||
func fixSCTPChunkLength(r *Result, sctp []byte, hdrLen int) {
|
||||
switch {
|
||||
case len(sctp) == 12: // no chunks
|
||||
return
|
||||
case hdrLen < 0:
|
||||
r.notef("SCTP chunk lengths are only recalculated when the layer 3 field is used; not changed")
|
||||
return
|
||||
case hdrLen < 16:
|
||||
r.notef("SCTP chunk header is not in the layer 3 field; chunk length not changed")
|
||||
return
|
||||
}
|
||||
|
||||
off := 12
|
||||
for {
|
||||
clen := int(binary.BigEndian.Uint16(sctp[off+2:]))
|
||||
next := off + (clen+3)&^3
|
||||
if clen >= 4 && next+4 <= hdrLen {
|
||||
off = next
|
||||
continue
|
||||
}
|
||||
|
||||
rem := len(sctp) - off
|
||||
newLen := rem
|
||||
if clen >= 4 && clen <= rem && (clen+3)&^3 == rem {
|
||||
newLen = clen // already correct; the rest is padding
|
||||
}
|
||||
if newLen > 0xffff {
|
||||
r.notef("SCTP chunk would be %d bytes, more than 65535; chunk length not changed", newLen)
|
||||
return
|
||||
}
|
||||
r.change16(fmt.Sprintf("SCTP %s chunk length", sctpChunkName(sctp[off])), sctp[off+2:off+4], uint16(newLen))
|
||||
if rem%4 != 0 {
|
||||
r.notef("SCTP %s chunk is %d bytes, not padded to a multiple of 4", sctpChunkName(sctp[off]), rem)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func sctpChunkName(t byte) string {
|
||||
names := map[byte]string{
|
||||
0: "DATA", 1: "INIT", 2: "INIT ACK", 3: "SACK", 4: "HEARTBEAT", 5: "HEARTBEAT ACK",
|
||||
6: "ABORT", 7: "SHUTDOWN", 8: "SHUTDOWN ACK", 9: "ERROR", 10: "COOKIE ECHO",
|
||||
11: "COOKIE ACK", 14: "SHUTDOWN COMPLETE", 64: "I-DATA",
|
||||
}
|
||||
if n, ok := names[t]; ok {
|
||||
return n
|
||||
}
|
||||
return fmt.Sprintf("type %d", t)
|
||||
}
|
||||
|
||||
var castagnoli = crc32.MakeTable(crc32.Castagnoli)
|
||||
|
||||
// sum adds data as big-endian 16-bit words to s (RFC 1071).
|
||||
func sum(data []byte, s uint32) uint32 {
|
||||
n := len(data) &^ 1
|
||||
for i := 0; i < n; i += 2 {
|
||||
s += uint32(data[i])<<8 | uint32(data[i+1])
|
||||
}
|
||||
if len(data)%2 == 1 {
|
||||
s += uint32(data[len(data)-1]) << 8
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// checksum returns the Internet checksum of data, starting from the partial sum s.
|
||||
func checksum(data []byte, s uint32) uint16 {
|
||||
s = sum(data, s)
|
||||
for s>>16 != 0 {
|
||||
s = s&0xffff + s>>16
|
||||
}
|
||||
return ^uint16(s)
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
package inet
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"pcap-web-tools/internal/pcap"
|
||||
)
|
||||
|
||||
func mustHex(t *testing.T, s string) []byte {
|
||||
t.Helper()
|
||||
b, err := hex.DecodeString(strings.ReplaceAll(s, " ", ""))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// The want packets were generated by Fix and validated with tshark's checksum checks.
|
||||
// Each test zeroes the length/checksum fields and checks Fix restores them.
|
||||
func TestFix(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
want string
|
||||
hdrLen int // header length passed to Fix (0 = unknown)
|
||||
zero [][2]int // byte ranges cleared before calling Fix
|
||||
changes int // expected number of changes, if not len(zero)
|
||||
notes []string // expected substrings of the notes, in order
|
||||
}{
|
||||
{
|
||||
name: "IPv4 UDP",
|
||||
want: "45000028000140004011b970c0a80001c0a80002303904d20014117668656c6c6f2c207063617021",
|
||||
zero: [][2]int{{2, 4}, {10, 12}, {24, 26}, {26, 28}},
|
||||
},
|
||||
{
|
||||
name: "IPv4 with options, TCP, odd length payload",
|
||||
want: "46000036123440004006118b0a0000010a00000201010100c350005000000001000000005018fffff9700000474554202f0d0a0d0a2e",
|
||||
hdrLen: 44,
|
||||
zero: [][2]int{{2, 4}, {10, 12}, {36, 37}, {40, 42}},
|
||||
},
|
||||
{
|
||||
name: "IPv4 ICMP",
|
||||
want: "45000021000100004001aa22c0a80001080808080800f54babcd00017071727374",
|
||||
zero: [][2]int{{2, 4}, {10, 12}, {22, 24}},
|
||||
},
|
||||
{
|
||||
name: "IPv4 SCTP",
|
||||
want: "45000034000040004084b8f2c0a80001c0a800020b5904d200000000a05c7b6a0100001400000001000100000001000100000001",
|
||||
zero: [][2]int{{2, 4}, {10, 12}, {28, 32}},
|
||||
notes: []string{"only recalculated when the layer 3 field is used"},
|
||||
},
|
||||
{
|
||||
name: "IPv6 hop-by-hop UDP",
|
||||
want: "600000000016000020010db800000000000000000000000120010db800000000000000000000000211000000000000001f900035000e1bfcdeadbeefcafe",
|
||||
zero: [][2]int{{4, 6}, {52, 54}, {54, 56}},
|
||||
},
|
||||
{
|
||||
name: "IPv6 ICMPv6",
|
||||
want: "60000000000d3a40fe800000000000000000000000000001fe800000000000000000000000000002800046b7123400016162636465",
|
||||
zero: [][2]int{{4, 6}, {42, 44}},
|
||||
},
|
||||
{
|
||||
name: "IPv6 segment routing header UDP uses final destination",
|
||||
want: "6000000000242b4020010db800000000000000000000000120010db80000000000000000000000aa110204010000000020010db80000000000000000000000ff1f900035000c7f9901020304",
|
||||
zero: [][2]int{{4, 6}, {68, 70}, {70, 72}},
|
||||
},
|
||||
{
|
||||
name: "IPv4 options, TCP SYN with options and no payload",
|
||||
want: "460000401234400040067f7d0a0000010a00000294040000c35000500000000100000000a002ffff705b0000020405b40402080a000000010000000001030307",
|
||||
hdrLen: 64,
|
||||
zero: [][2]int{{2, 4}, {10, 12}, {36, 37}, {40, 42}},
|
||||
},
|
||||
{
|
||||
name: "IPv4 options, TCP with options and payload",
|
||||
want: "4600003d1234400040067f800a0000010a00000294040000c350005000000001000000008018ffffd4a500000101080a0000000100000000474554202f",
|
||||
hdrLen: 56,
|
||||
zero: [][2]int{{2, 4}, {10, 12}, {36, 37}, {40, 42}},
|
||||
},
|
||||
{
|
||||
name: "IPv4 options, SCTP DATA without padding",
|
||||
want: "460000391234400040847f060a0000010a00000294040000c35004d200000001866854b30003001500000001000100000000000068656c6c6f",
|
||||
hdrLen: 52,
|
||||
zero: [][2]int{{2, 4}, {10, 12}, {32, 36}, {38, 40}},
|
||||
notes: []string{"not padded"},
|
||||
},
|
||||
{
|
||||
name: "IPv4 options, SCTP DATA with padding keeps correct chunk length",
|
||||
want: "4600003c1234400040847f030a0000010a00000294040000c35004d200000001ef89c5b10003001500000001000100000000000068656c6c6f000000",
|
||||
hdrLen: 52,
|
||||
zero: [][2]int{{2, 4}, {10, 12}, {32, 36}},
|
||||
changes: 4,
|
||||
},
|
||||
{
|
||||
name: "SCTP INIT entirely in layer 3",
|
||||
want: "460000381234400040847f070a0000010a00000294040000c35004d200000001c3d6cafe0100001400000001000100000001000100000001",
|
||||
hdrLen: 56,
|
||||
zero: [][2]int{{2, 4}, {10, 12}, {32, 36}, {38, 40}},
|
||||
},
|
||||
{
|
||||
name: "IPv6 SCTP SACK bundled before DATA",
|
||||
want: "600000000034844020010db800000000000000000000000120010db8000000000000000000000002c35004d200000001634380c903000010000000000000ffff00000000000300180000000100010000000000000102030405060708",
|
||||
hdrLen: 84,
|
||||
zero: [][2]int{{4, 6}, {48, 52}, {70, 72}},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
want := mustHex(t, tt.want)
|
||||
pkt := bytes.Clone(want)
|
||||
for _, z := range tt.zero {
|
||||
clear(pkt[z[0]:z[1]])
|
||||
}
|
||||
res, err := Fix(pkt, tt.hdrLen)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(pkt, want) {
|
||||
t.Errorf("got %x\nwant %x", pkt, want)
|
||||
}
|
||||
changes := tt.changes
|
||||
if changes == 0 {
|
||||
changes = len(tt.zero)
|
||||
}
|
||||
if len(res.Changes) != changes {
|
||||
t.Errorf("got %d changes, want %d: %+v", len(res.Changes), changes, res.Changes)
|
||||
}
|
||||
checkNotes(t, res.Notes, tt.notes)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func checkNotes(t *testing.T, got, want []string) {
|
||||
t.Helper()
|
||||
if len(got) != len(want) {
|
||||
t.Errorf("notes %q, want %q", got, want)
|
||||
return
|
||||
}
|
||||
for i := range want {
|
||||
if !strings.Contains(got[i], want[i]) {
|
||||
t.Errorf("note %q does not contain %q", got[i], want[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFixNotes(t *testing.T) {
|
||||
const (
|
||||
ip4TCP = "45000000000140004006 0000 c0a80001c0a80002"
|
||||
tcpHdr = "c35000500000000100000000 5002ffff 0000 0000"
|
||||
ip4SCTP = "45000000000140004084 0000 c0a80001c0a80002"
|
||||
sctpHdr = "c35004d2 00000001 00000000"
|
||||
)
|
||||
tests := []struct {
|
||||
name string
|
||||
pkt string
|
||||
hdrLen int
|
||||
notes []string
|
||||
}{
|
||||
{"TCP header in payload field", ip4TCP + tcpHdr, 20, []string{"TCP header is in the payload field"}},
|
||||
{"TCP header length not a multiple of 4", ip4TCP + tcpHdr + "0101", 42, []string{"must be 20-60"}},
|
||||
{"IP header extends past layer 3", ip4TCP + tcpHdr, 12, []string{"extends past the layer 3 field", "TCP header is in the payload field"}},
|
||||
{"IPv4 option overruns IHL", "46000000000140004006 0000 c0a80001c0a80002 01 44 08 00" + tcpHdr, 0, []string{"does not fit in the header length"}},
|
||||
{"SCTP chunk header in payload field", ip4SCTP + sctpHdr + "00030000 00000001 00010000 00000000", 32, []string{"chunk header is not in the layer 3 field"}},
|
||||
{"SCTP without chunks", ip4SCTP + sctpHdr, 32, nil},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
res, err := Fix(mustHex(t, tt.pkt), tt.hdrLen)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
checkNotes(t, res.Notes, tt.notes)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFixIPv4Fragment(t *testing.T) {
|
||||
// More-fragments flag set: header fixed, UDP left alone.
|
||||
pkt := mustHex(t, "45000000000120004011 0000 c0a80001c0a80002 303904d200000000 aabb")
|
||||
res, err := Fix(pkt, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(res.Changes) != 2 || len(res.Notes) != 1 || !strings.Contains(res.Notes[0], "fragment") {
|
||||
t.Errorf("changes %+v notes %v", res.Changes, res.Notes)
|
||||
}
|
||||
if !bytes.Equal(pkt[24:28], []byte{0, 0, 0, 0}) {
|
||||
t.Errorf("UDP header modified: %x", pkt[20:28])
|
||||
}
|
||||
}
|
||||
|
||||
func TestFixTruncatedTransport(t *testing.T) {
|
||||
pkt := mustHex(t, "45000000000140004006 0000 c0a80001c0a80002 c350")
|
||||
res, err := Fix(pkt, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(res.Notes) != 1 || !strings.Contains(res.Notes[0], "TCP header truncated") {
|
||||
t.Errorf("notes %v", res.Notes)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFixErrors(t *testing.T) {
|
||||
for name, in := range map[string]string{
|
||||
"empty": "",
|
||||
"not IP": "0011223344",
|
||||
"short IPv4": "4500001c",
|
||||
"IHL too small": "41000000000000004011000000000000000000000000",
|
||||
"IHL beyond end": "4f000000000000004011000000000000000000000000",
|
||||
"short IPv6": "6000000000001140",
|
||||
} {
|
||||
if _, err := Fix(mustHex(t, in), 0); err == nil {
|
||||
t.Errorf("%s: expected error", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindIP(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
frame string
|
||||
linkType uint32
|
||||
want int
|
||||
wantErr bool
|
||||
}{
|
||||
{"ethernet", "001122334455 66778899aabb 0800 45", pcap.LinkTypeEthernet, 14, false},
|
||||
{"ethernet IPv6", "001122334455 66778899aabb 86dd 60", pcap.LinkTypeEthernet, 14, false},
|
||||
{"vlan", "001122334455 66778899aabb 8100 0064 0800 45", pcap.LinkTypeEthernet, 18, false},
|
||||
{"qinq", "001122334455 66778899aabb 88a8 0001 8100 0064 86dd 60", pcap.LinkTypeEthernet, 22, false},
|
||||
{"arp", "ffffffffffff 66778899aabb 0806 0001", pcap.LinkTypeEthernet, 0, true},
|
||||
{"truncated", "001122334455", pcap.LinkTypeEthernet, 0, true},
|
||||
{"raw", "45", pcap.LinkTypeRaw, 0, false},
|
||||
{"sll", "0000 0001 0006 66778899aabb 0000 0800 45", pcap.LinkTypeLinuxSLL, 16, false},
|
||||
{"sll2", "86dd 0000 00000002 0001 00 06 66778899aabb0000 60", pcap.LinkTypeLinuxSLL2, 20, false},
|
||||
{"unsupported", "00", pcap.LinkTypeIEEE80211, 0, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
got, err := FindIP(mustHex(t, tt.frame), tt.linkType)
|
||||
if (err != nil) != tt.wantErr || got != tt.want {
|
||||
t.Errorf("%s: FindIP = %d, %v; want %d, err=%v", tt.name, got, err, tt.want, tt.wantErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package inet
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
|
||||
"pcap-web-tools/internal/pcap"
|
||||
)
|
||||
|
||||
const (
|
||||
etherTypeIPv4 = 0x0800
|
||||
etherTypeIPv6 = 0x86dd
|
||||
etherTypeVLAN = 0x8100
|
||||
etherTypeQinQ = 0x88a8
|
||||
)
|
||||
|
||||
// FindIP returns the offset of the IP header within a link-layer frame of the
|
||||
// given pcap link type, by parsing the link-layer header.
|
||||
func FindIP(frame []byte, linkType uint32) (int, error) {
|
||||
switch linkType {
|
||||
case pcap.LinkTypeRaw:
|
||||
return 0, nil
|
||||
|
||||
case pcap.LinkTypeEthernet:
|
||||
off := 12
|
||||
for {
|
||||
if off+2 > len(frame) {
|
||||
return 0, fmt.Errorf("Ethernet header truncated")
|
||||
}
|
||||
et := binary.BigEndian.Uint16(frame[off:])
|
||||
if et == etherTypeVLAN || et == etherTypeQinQ {
|
||||
off += 4
|
||||
continue
|
||||
}
|
||||
return etherIP(et, off+2)
|
||||
}
|
||||
|
||||
case pcap.LinkTypeLinuxSLL:
|
||||
if len(frame) < 16 {
|
||||
return 0, fmt.Errorf("Linux cooked (SLL) header truncated")
|
||||
}
|
||||
return etherIP(binary.BigEndian.Uint16(frame[14:]), 16)
|
||||
|
||||
case pcap.LinkTypeLinuxSLL2:
|
||||
if len(frame) < 20 {
|
||||
return 0, fmt.Errorf("Linux cooked v2 (SLL2) header truncated")
|
||||
}
|
||||
return etherIP(binary.BigEndian.Uint16(frame[0:]), 20)
|
||||
}
|
||||
return 0, fmt.Errorf("cannot locate the IP header for link type %s", pcap.LinkTypeName(linkType))
|
||||
}
|
||||
|
||||
func etherIP(etherType uint16, off int) (int, error) {
|
||||
if etherType != etherTypeIPv4 && etherType != etherTypeIPv6 {
|
||||
return 0, fmt.Errorf("link-layer payload is not IP (EtherType 0x%04x)", etherType)
|
||||
}
|
||||
return off, nil
|
||||
}
|
||||
Reference in New Issue
Block a user