fix bug in right aligned field extraction

This commit is contained in:
Patricio Whittingslow
2025-12-31 02:58:09 -03:00
parent 5f0edf62a9
commit 02ca576e14
2 changed files with 251 additions and 4 deletions
+8 -4
View File
@@ -590,10 +590,14 @@ func appendField(dst, pkt []byte, fieldBitStart, bitlen int, rightAligned bool)
dst = append(dst, pkt[octetsStart+1:octetsStart+octets]...)
return dst, nil
}
// Right aligned with trailing bits. i.e: ???
for i := 1; i < octets; i++ {
b := pkt[octetsStart+i] >> (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
+243
View File
@@ -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)
}
}