diff --git a/examples/tap/main.go b/examples/tap/main.go index 8831190..c3e780a 100644 --- a/examples/tap/main.go +++ b/examples/tap/main.go @@ -63,22 +63,24 @@ func run() error { } defer sv.Close() var cap pcap.PacketBreakdown + pf := pcap.Formatter{ + FilterClasses: []pcap.FieldClass{pcap.FieldClassDst, pcap.FieldClassSrc, pcap.FieldClassSize, pcap.FieldClassFlags}, + } + var pfbuf []byte sv.OnTransfer(func(channel int, pkt []byte) { captime := time.Now() frames, err := cap.CaptureEthernet(nil, pkt, 0) if err == nil { - flags, src, dst := getTCPData(frames, pkt) - if src != 0 { - if flags != 0 { - fmt.Println(channel, captime.Format("15:04:05.000"), frames, flags.String(), src, "->", dst) - } else { - fmt.Println(channel, captime.Format("15:04:05.000"), frames, src, "->", dst) - } + pfbuf = append(pfbuf[:0], '[') + pfbuf, err = pf.FormatFrames(pfbuf, frames, pkt) + pfbuf = append(pfbuf, ']') + if err != nil { + fmt.Printf("%d %s !err:%s\n", channel, captime.Format("15:04:05.000"), err) } else { - fmt.Println(channel, captime.Format("15:04:05.000"), frames) + fmt.Printf("%d %s %s\n", channel, captime.Format("15:04:05.000"), pfbuf) } } else { - fmt.Println(channel, captime.Format("15:04:05.000"), "ERR", frames, err.Error()) + fmt.Println(channel, captime.Format("15:04:05.000"), "cap ERR", frames, err.Error()) } }) hwaddr, err := sv.HardwareAddress6() diff --git a/internet/pcap/capture.go b/internet/pcap/capture.go index 4c62c5f..759a3e8 100644 --- a/internet/pcap/capture.go +++ b/internet/pcap/capture.go @@ -3,9 +3,14 @@ package pcap //go:generate stringer -type=FieldClass -linecomment -output stringers.go . import ( "encoding/binary" + "encoding/hex" "errors" "fmt" "math" + "net/netip" + "slices" + "strconv" + "strings" "github.com/soypat/lneto" "github.com/soypat/lneto/arp" @@ -530,17 +535,7 @@ func (frm Frame) FieldAsUint(fieldIdx int, pkt []byte) (uint64, error) { return badUint64, errors.New("invalid field index") } field := frm.Fields[fieldIdx] - octets := (field.BitLength + 7) / 8 - if octets > 8 { - return badUint64, errors.New("field too long to be represented by uint64") - } - var buf [8]byte - _, err := frm.AppendField(buf[8-octets:8-octets], fieldIdx, pkt) - if err != nil { - return badUint64, err - } - v := binary.BigEndian.Uint64(buf[:]) - return v, nil + return fieldAsUint(pkt, frm.PacketBitOffset+field.FrameBitOffset, field.BitLength, field.RightAligned) } func (frm Frame) AppendField(dst []byte, fieldIdx int, pkt []byte) ([]byte, error) { @@ -548,9 +543,27 @@ func (frm Frame) AppendField(dst []byte, fieldIdx int, pkt []byte) ([]byte, erro return dst, errors.New("invalid field index") } field := frm.Fields[fieldIdx] - fieldBitStart := frm.PacketBitOffset + field.FrameBitOffset - fieldBitEnd := fieldBitStart + field.BitLength - octets := (field.BitLength + 7) / 8 // total octets needed to represent field. + return appendField(dst, pkt, frm.PacketBitOffset+field.FrameBitOffset, field.BitLength, field.RightAligned) +} + +func fieldAsUint(pkt []byte, fieldBitStart, bitlen int, rightAligned bool) (uint64, error) { + const badUint64 = math.MaxUint64 + octets := (bitlen + 7) / 8 + if octets > 8 { + return badUint64, errors.New("field too long to be represented by uint64") + } + var buf [8]byte + _, err := appendField(buf[8-octets:8-octets], pkt, fieldBitStart, bitlen, rightAligned) + if err != nil { + return badUint64, err + } + v := binary.BigEndian.Uint64(buf[:]) + return v, nil +} + +func appendField(dst, pkt []byte, fieldBitStart, bitlen int, rightAligned bool) ([]byte, error) { + fieldBitEnd := fieldBitStart + bitlen + octets := (bitlen + 7) / 8 // total octets needed to represent field. octetsStart := fieldBitStart / 8 if octets+octetsStart > len(pkt) { return dst, errors.New("buffer overflow") @@ -558,7 +571,7 @@ func (frm Frame) AppendField(dst []byte, fieldIdx int, pkt []byte) ([]byte, erro firstBitOffset := fieldBitStart % 8 lastOctetExcessBits := fieldBitEnd % 8 if firstBitOffset == 0 { - if field.RightAligned { + if rightAligned { return dst, errors.New("invalid right aligned set for fully aligned field") } // Optimized path: field starts at byte boundary. @@ -570,17 +583,21 @@ func (frm Frame) AppendField(dst []byte, fieldIdx int, pkt []byte) ([]byte, erro } mask := byte(1<> (8 - lastOctetExcessBits) - b |= pkt[octetsStart+i-1] & mask + // Right aligned with trailing bits. i.e: IPv6 Traffic Class. + // Field spans an extra byte, so need octets+1 bytes from packet. + if octets+octetsStart+1 > len(pkt) { + return dst, errors.New("buffer overflow") + } + for i := 0; i < octets; i++ { + b := (pkt[octetsStart+i] & mask) << (8 - firstBitOffset) + b |= pkt[octetsStart+i+1] >> firstBitOffset dst = append(dst, b) } return dst, nil @@ -599,6 +616,129 @@ func (frm Frame) AppendField(dst []byte, fieldIdx int, pkt []byte) ([]byte, erro return dst, nil } +type Formatter struct { + FieldSep string + FrameSep string + FilterClasses []FieldClass + buf []byte +} + +func (f *Formatter) FormatFrames(dst []byte, frms []Frame, pkt []byte) (_ []byte, err error) { + sep := f.FrameSep + if sep == "" { + sep = " | " + } + for ifrm := range frms { + if ifrm != 0 { + dst = append(dst, sep...) + } + dst, err = f.FormatFrame(dst, frms[ifrm], pkt) + if err != nil { + return dst, err + } + } + return dst, nil +} + +func (f *Formatter) FormatFrame(dst []byte, frm Frame, pkt []byte) (_ []byte, err error) { + sep := f.FieldSep + if sep == "" { + sep = "; " // default field separator + } + bitlen := frm.LenBits() + if bitlen%8 == 0 { + dst = fmt.Appendf(dst, "%s len=%d", frm.Protocol, bitlen/8) + } else { + dst = fmt.Appendf(dst, "%s bitlen=%d", frm.Protocol, bitlen) + } + + for ifield := range frm.Fields { + field := frm.Fields[ifield] + if f.filterField(field) { + continue + } + dst = append(dst, sep...) + if field.Class == FieldClassFlags && frm.Protocol == lneto.IPProtoTCP { + dst = append(dst, "flags="...) + v, err := fieldAsUint(pkt, frm.PacketBitOffset+field.FrameBitOffset, field.BitLength, field.RightAligned) + if err != nil { + return dst, err + } + dst = tcp.Flags(v).AppendFormat(dst) + continue + } + dst, err = f.formatField(dst, frm.PacketBitOffset, field, pkt) + if err != nil { + return dst, err + } + } + return dst, nil +} + +func (f *Formatter) filterField(field FrameField) bool { + return f.FilterClasses != nil && !slices.Contains(f.FilterClasses, field.Class) +} + +func (f *Formatter) FormatField(dst []byte, pktStartOff int, field FrameField, pkt []byte) (_ []byte, err error) { + return f.formatField(dst, pktStartOff, field, pkt) +} + +func (f *Formatter) formatField(dst []byte, pktStartOff int, field FrameField, pkt []byte) (_ []byte, err error) { + name := field.Name + if name == "" { + name = field.Class.String() + } + hasSpaces := strings.IndexByte(name, ' ') >= 0 + if hasSpaces { + dst = append(dst, '(') + } + dst = append(dst, name...) + if hasSpaces { + dst = append(dst, ')') + } + dst = append(dst, '=') + f.buf, err = appendField(f.buf[:0], pkt, field.FrameBitOffset+pktStartOff, field.BitLength, field.RightAligned) + if err != nil { + return dst, err + } + fieldBitStart := pktStartOff + field.FrameBitOffset + switch field.Class { + default: + fallthrough + case FieldClassChecksum, FieldClassID, FieldClassFlags, FieldClassOptions, FieldClassAddress: + // Binary data to be printed as hexadecimal. + dst = append(dst, "0x"...) + dst = hex.AppendEncode(dst, f.buf) + case FieldClassDst, FieldClassSrc, FieldClassSize: + // IP, MAC addresses and ports. + if field.BitLength <= 16 { + v, err := fieldAsUint(pkt, fieldBitStart, field.BitLength, field.RightAligned) + if err != nil { + return dst, err + } + dst = strconv.AppendUint(dst, v, 10) + } else if field.BitLength == 4*8 { + dst = netip.AddrFrom4([4]byte(f.buf)).AppendTo(dst) + } else if field.BitLength == 6*8 { + for i := range f.buf { + if i != 0 { + dst = append(dst, ':') + } + if f.buf[i] < 16 { + dst = append(dst, '0') + } + dst = strconv.AppendUint(dst, uint64(f.buf[i]), 16) + } + } else if field.BitLength == 16*8 { + dst = netip.AddrFrom16([16]byte(f.buf)).AppendTo(dst) + } else { + dst = append(dst, "0x"...) + dst = hex.AppendEncode(dst, f.buf) + } + } + return dst, err +} + func (frm Frame) String() string { return string(frm.AppendString(nil)) } diff --git a/internet/pcap/capture_test.go b/internet/pcap/capture_test.go index 4ab1386..173d7c2 100644 --- a/internet/pcap/capture_test.go +++ b/internet/pcap/capture_test.go @@ -1,6 +1,7 @@ package pcap import ( + "encoding/binary" "math" "math/rand" "testing" @@ -10,6 +11,7 @@ import ( "github.com/soypat/lneto/http/httpraw" "github.com/soypat/lneto/internal/ltesto" "github.com/soypat/lneto/ipv4" + "github.com/soypat/lneto/ipv6" "github.com/soypat/lneto/tcp" ) @@ -121,3 +123,244 @@ func TestCap(t *testing.T) { t.Errorf("want %q HTTP body, got %q", httpBody, gotBody) } } + +// TestRightAlignedFields tests extraction of fields that span byte boundaries +// with right-aligned output, such as IPv6 Traffic Class and Flow Label. +func TestRightAlignedFields(t *testing.T) { + // Build a minimal IPv6 packet with known Traffic Class and Flow Label values. + // IPv6 header: 40 bytes minimum. + // Byte 0-3: Version (4 bits) + Traffic Class (8 bits) + Flow Label (20 bits) + const ( + wantVersion = 6 + wantTrafficClass = 0xAB // 8 bits at bit offset 4 + wantFlowLabel = 0x000C_DEF0 // 20 bits at bit offset 12 (we'll use 0xCDEF0 masked to 20 bits = 0xDEF0) + ) + // Actually flow label is 20 bits, so max is 0xFFFFF. Use 0xDEF01 masked. + const wantFlow20 = 0xDEF01 & 0xFFFFF // 0xDEF01 + + var pkt [14 + 40 + 20]byte // Ethernet + IPv6 header + TCP header + // Set up Ethernet frame. + efrm, _ := ethernet.NewFrame(pkt[:]) + efrm.SetEtherType(ethernet.TypeIPv6) + + // Set up IPv6 header manually. + i6frm, _ := ipv6.NewFrame(efrm.Payload()) + i6frm.SetVersionTrafficAndFlow(wantVersion, ipv6.ToS(wantTrafficClass), wantFlow20) + i6frm.SetPayloadLength(20) // TCP header size + i6frm.SetNextHeader(lneto.IPProtoTCP) + i6frm.SetHopLimit(64) + + // Set up minimal TCP header. + tfrm, _ := tcp.NewFrame(i6frm.Payload()) + tfrm.SetOffsetAndFlags(5, 0) // 5 words = 20 bytes, no flags + + // Verify our setup is correct. + gotVer, gotToS, gotFlow := i6frm.VersionTrafficAndFlow() + if gotVer != wantVersion { + t.Fatalf("setup: version mismatch: got %d, want %d", gotVer, wantVersion) + } + if uint8(gotToS) != wantTrafficClass { + t.Fatalf("setup: traffic class mismatch: got 0x%02x, want 0x%02x", gotToS, wantTrafficClass) + } + if gotFlow != wantFlow20 { + t.Fatalf("setup: flow label mismatch: got 0x%05x, want 0x%05x", gotFlow, wantFlow20) + } + + // Capture the packet. + var pbreak PacketBreakdown + frames, err := pbreak.CaptureEthernet(nil, pkt[:], 0) + if err != nil { + t.Fatal(err) + } + if len(frames) < 2 { + t.Fatalf("expected at least 2 frames (Ethernet + IPv6), got %d", len(frames)) + } + + // Find IPv6 frame. + var ipv6Frame *Frame + for i := range frames { + if frames[i].Protocol == ethernet.TypeIPv6 { + ipv6Frame = &frames[i] + break + } + } + if ipv6Frame == nil { + t.Fatal("IPv6 frame not found") + } + + // Helper to get field by name. + getByName := func(name string) (uint64, error) { + for i, f := range ipv6Frame.Fields { + if f.Name == name { + return ipv6Frame.FieldAsUint(i, pkt[:]) + } + } + return 0, nil + } + + // Test Traffic Class (Type of Service) - 8 bits at bit offset 4, right-aligned. + gotTrafficClass, err := getByName("Type of Service") + if err != nil { + t.Fatalf("failed to get Traffic Class: %v", err) + } + if uint8(gotTrafficClass) != wantTrafficClass { + t.Errorf("Traffic Class: got 0x%02x, want 0x%02x", gotTrafficClass, wantTrafficClass) + } + + // Test Flow Label - 20 bits at bit offset 12, right-aligned. + gotFlowLabel, err := getByName("Flow Label") + if err != nil { + t.Fatalf("failed to get Flow Label: %v", err) + } + if uint32(gotFlowLabel) != wantFlow20 { + t.Errorf("Flow Label: got 0x%05x, want 0x%05x", gotFlowLabel, wantFlow20) + } + + // Test Version - 4 bits at bit offset 0, not right-aligned. + gotVersion, err := getByName("") + if err != nil { + t.Fatalf("failed to get Version: %v", err) + } + if len(ipv6Frame.Fields) > 0 && ipv6Frame.Fields[0].Class == FieldClassVersion { + gotVersion, _ = ipv6Frame.FieldAsUint(0, pkt[:]) + } + if uint8(gotVersion) != wantVersion { + t.Errorf("Version: got %d, want %d", gotVersion, wantVersion) + } +} + +// TestAppendFieldRightAligned directly tests the appendField function +// with right-aligned fields that have trailing bits. +func TestAppendFieldRightAligned(t *testing.T) { + testCases := []struct { + name string + pkt []byte + fieldBitStart int + bitlen int + rightAligned bool + wantData []byte + }{ + { + // IPv6 Traffic Class: bits 4-11 (8 bits spanning bytes 0-1) + name: "IPv6 Traffic Class 0xAB", + pkt: []byte{0x6A, 0xB0, 0x00, 0x00}, // Version=6, TC=0xAB, Flow=0 + fieldBitStart: 4, + bitlen: 8, + rightAligned: true, + wantData: []byte{0xAB}, + }, + { + // IPv6 Traffic Class with different value + name: "IPv6 Traffic Class 0xFF", + pkt: []byte{0x6F, 0xF0, 0x00, 0x00}, // Version=6, TC=0xFF, Flow=0 + fieldBitStart: 4, + bitlen: 8, + rightAligned: true, + wantData: []byte{0xFF}, + }, + { + // IPv6 Traffic Class at minimum + name: "IPv6 Traffic Class 0x00", + pkt: []byte{0x60, 0x00, 0x00, 0x00}, // Version=6, TC=0x00, Flow=0 + fieldBitStart: 4, + bitlen: 8, + rightAligned: true, + wantData: []byte{0x00}, + }, + { + // IPv6 Flow Label: bits 12-31 (20 bits spanning bytes 1-3) + name: "IPv6 Flow Label 0xDEF01", + pkt: []byte{0x60, 0x0D, 0xEF, 0x01}, // Version=6, TC=0, Flow=0xDEF01 + fieldBitStart: 12, + bitlen: 20, + rightAligned: true, + wantData: []byte{0x0D, 0xEF, 0x01}, // 20 bits right-aligned in 3 bytes + }, + { + // Flow Label max value + name: "IPv6 Flow Label 0xFFFFF", + pkt: []byte{0x60, 0xFF, 0xFF, 0xFF}, // Version=6, TC=0, Flow=0xFFFFF + fieldBitStart: 12, + bitlen: 20, + rightAligned: true, + wantData: []byte{0x0F, 0xFF, 0xFF}, // 20 bits right-aligned in 3 bytes + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := appendField(nil, tc.pkt, tc.fieldBitStart, tc.bitlen, tc.rightAligned) + if err != nil { + t.Fatalf("appendField error: %v", err) + } + if len(got) != len(tc.wantData) { + t.Fatalf("length mismatch: got %d bytes, want %d bytes", len(got), len(tc.wantData)) + } + for i := range got { + if got[i] != tc.wantData[i] { + t.Errorf("byte %d: got 0x%02x, want 0x%02x", i, got[i], tc.wantData[i]) + } + } + // Also verify as uint64 for single/double byte cases. + if len(tc.wantData) <= 8 { + gotVal, err := fieldAsUint(tc.pkt, tc.fieldBitStart, tc.bitlen, tc.rightAligned) + if err != nil { + t.Fatalf("fieldAsUint error: %v", err) + } + var wantVal uint64 + for _, b := range tc.wantData { + wantVal = wantVal<<8 | uint64(b) + } + if gotVal != wantVal { + t.Errorf("as uint: got 0x%x, want 0x%x", gotVal, wantVal) + } + } + }) + } +} + +// TestFieldAsUintRightAligned tests fieldAsUint with the same buffer +// used in the appendField fix, ensuring consistency. +func TestFieldAsUintRightAligned(t *testing.T) { + // Build IPv6 first 4 bytes with known values. + // Format: VVVV TTTT TTTT FFFF FFFF FFFF FFFF FFFF + // V=version (4 bits), T=traffic class (8 bits), F=flow label (20 bits) + var buf [4]byte + const version = 6 + const trafficClass = 0xAB + const flowLabel = 0xCDEF0 + + // Encode: version in bits 0-3, traffic class in bits 4-11, flow label in bits 12-31 + val := uint32(version)<<28 | uint32(trafficClass)<<20 | flowLabel + binary.BigEndian.PutUint32(buf[:], val) + + // Verify encoding. + t.Logf("Encoded bytes: %02x %02x %02x %02x", buf[0], buf[1], buf[2], buf[3]) + + // Test version extraction (bits 0-3, 4 bits, left-aligned) + gotVersion, err := fieldAsUint(buf[:], 0, 4, false) + if err != nil { + t.Fatalf("version extraction failed: %v", err) + } + if gotVersion != version { + t.Errorf("version: got %d, want %d", gotVersion, version) + } + + // Test traffic class extraction (bits 4-11, 8 bits, right-aligned) + gotTC, err := fieldAsUint(buf[:], 4, 8, true) + if err != nil { + t.Fatalf("traffic class extraction failed: %v", err) + } + if gotTC != trafficClass { + t.Errorf("traffic class: got 0x%02x, want 0x%02x", gotTC, trafficClass) + } + + // Test flow label extraction (bits 12-31, 20 bits, right-aligned) + gotFlow, err := fieldAsUint(buf[:], 12, 20, true) + if err != nil { + t.Fatalf("flow label extraction failed: %v", err) + } + if gotFlow != flowLabel { + t.Errorf("flow label: got 0x%05x, want 0x%05x", gotFlow, flowLabel) + } +}