diff --git a/alignedbuff/alignedbuff.go b/alignedbuff/alignedbuff.go new file mode 100644 index 0000000..605c0b1 --- /dev/null +++ b/alignedbuff/alignedbuff.go @@ -0,0 +1,240 @@ +// Package alignedbuff implements encoding and decoding aligned data elements +// to/from buffers in native endianess. +package alignedbuff + +import ( + "bytes" + "errors" + "fmt" + "unsafe" + + "github.com/google/nftables/binaryutil" +) + +// ErrEOF signals trying to read beyond the available payload information. +var ErrEOF = errors.New("not enough data left") + +// AlignedBuff implements marshalling and unmarshalling information in +// platform/architecture native endianess and data type alignment. It +// additionally covers some of the nftables-xtables translation-specific +// idiosyncracies to the extend needed in order to properly marshal and +// unmarshal Match and Target expressions, and their Info payload in particular. +type AlignedBuff struct { + data []byte + pos int +} + +// New returns a new AlignedBuff for marshalling aligned data in native +// endianess. +func New() AlignedBuff { + return AlignedBuff{} +} + +// NewWithData returns a new AlignedBuff for unmarshalling the passed data in +// native endianess. +func NewWithData(data []byte) AlignedBuff { + return AlignedBuff{data: data} +} + +// Data returns the properly padded info payload data written before by calling +// the various Uint8, Uint16, ... marshalling functions. +func (a *AlignedBuff) Data() []byte { + // The Linux kernel expects payloads to be padded to the next uint64 + // alignment. + a.alignWrite(uint64AlignMask) + return a.data +} + +// BytesAligned32 unmarshals the given amount of bytes starting with the native +// alignment for uint32 data types. It returns ErrEOF when trying to read beyond +// the payload. +// +// BytesAligned32 is used to unmarshal IP addresses for different IP versions, +// which are always aligned the same way as the native alignment for uint32. +func (a *AlignedBuff) BytesAligned32(size int) ([]byte, error) { + if err := a.alignCheckedRead(uint32AlignMask); err != nil { + return nil, err + } + if a.pos > len(a.data)-size { + return nil, ErrEOF + } + data := a.data[a.pos : a.pos+size] + a.pos += size + return data, nil +} + +// Uint8 unmarshals an uint8 in native endianess and alignment. It returns +// ErrEOF when trying to read beyond the payload. +func (a *AlignedBuff) Uint8() (uint8, error) { + if a.pos >= len(a.data) { + return 0, ErrEOF + } + v := a.data[a.pos] + a.pos++ + return v, nil +} + +// Uint16 unmarshals an uint16 in native endianess and alignment. It returns +// ErrEOF when trying to read beyond the payload. +func (a *AlignedBuff) Uint16() (uint16, error) { + if err := a.alignCheckedRead(uint16AlignMask); err != nil { + return 0, err + } + v := binaryutil.NativeEndian.Uint16(a.data[a.pos : a.pos+2]) + a.pos += 2 + return v, nil +} + +// Uint16BE unmarshals an uint16 in "network" (=big endian) endianess and native +// uint16 alignment. It returns ErrEOF when trying to read beyond the payload. +func (a *AlignedBuff) Uint16BE() (uint16, error) { + if err := a.alignCheckedRead(uint16AlignMask); err != nil { + return 0, err + } + v := binaryutil.BigEndian.Uint16(a.data[a.pos : a.pos+2]) + a.pos += 2 + return v, nil +} + +// Uint32 unmarshals an uint32 in native endianess and alignment. It returns +// ErrEOF when trying to read beyond the payload. +func (a *AlignedBuff) Uint32() (uint32, error) { + if err := a.alignCheckedRead(uint32AlignMask); err != nil { + return 0, err + } + v := binaryutil.NativeEndian.Uint32(a.data[a.pos : a.pos+4]) + a.pos += 4 + return v, nil +} + +// Uint64 unmarshals an uint64 in native endianess and alignment. It returns +// ErrEOF when trying to read beyond the payload. +func (a *AlignedBuff) Uint64() (uint64, error) { + if err := a.alignCheckedRead(uint64AlignMask); err != nil { + return 0, err + } + v := binaryutil.NativeEndian.Uint64(a.data[a.pos : a.pos+8]) + a.pos += 8 + return v, nil +} + +// Uint unmarshals an uint in native endianess and alignment for the C "unsigned +// int" type. It returns ErrEOF when trying to read beyond the payload. Please +// note that on 64bit platforms, the size and alignment of C's and Go's unsigned +// integer data types differ, so we encapsulate this difference here. +func (a *AlignedBuff) Uint() (uint, error) { + switch uintSize { + case 2: + v, err := a.Uint16() + return uint(v), err + case 4: + v, err := a.Uint32() + return uint(v), err + case 8: + v, err := a.Uint64() + return uint(v), err + default: + panic(fmt.Sprintf("unsupported uint size %d", uintSize)) + } +} + +// PutBytesAligned32 marshals the given bytes starting with the native alignment +// for uint32 data types. It additionaly adds padding to reach the specified +// size. +// +// PutBytesAligned32 is used to marshal IP addresses for different IP versions, +// which are always aligned the same way as the native alignment for uint32. +func (a *AlignedBuff) PutBytesAligned32(data []byte, size int) { + a.alignWrite(uint32AlignMask) + a.data = append(a.data, data...) + a.pos += len(data) + if len(data) < size { + padding := size - len(data) + a.data = append(a.data, bytes.Repeat([]byte{0}, padding)...) + a.pos += padding + } +} + +// PutUint8 marshals an uint8 in native endianess and alignment. +func (a *AlignedBuff) PutUint8(v uint8) { + a.data = append(a.data, v) + a.pos++ +} + +// PutUint16 marshals an uint16 in native endianess and alignment. +func (a *AlignedBuff) PutUint16(v uint16) { + a.alignWrite(uint16AlignMask) + a.data = append(a.data, binaryutil.NativeEndian.PutUint16(v)...) + a.pos += 2 +} + +// PutUint16BE marshals an uint16 in "network" (=big endian) endianess and +// native uint16 alignment. +func (a *AlignedBuff) PutUint16BE(v uint16) { + a.alignWrite(uint16AlignMask) + a.data = append(a.data, binaryutil.BigEndian.PutUint16(v)...) + a.pos += 2 +} + +// PutUint32 marshals an uint32 in native endianess and alignment. +func (a *AlignedBuff) PutUint32(v uint32) { + a.alignWrite(uint32AlignMask) + a.data = append(a.data, binaryutil.NativeEndian.PutUint32(v)...) + a.pos += 4 +} + +// PutUint64 marshals an uint64 in native endianess and alignment. +func (a *AlignedBuff) PutUint64(v uint64) { + a.alignWrite(uint64AlignMask) + a.data = append(a.data, binaryutil.NativeEndian.PutUint64(v)...) + a.pos += 8 +} + +// PutUint marshals an uint in native endianess and alignment for the C +// "unsigned int" type. Please note that on 64bit platforms, the size and +// alignment of C's and Go's unsigned integer data types differ, so we +// encapsulate this difference here. +func (a *AlignedBuff) PutUint(v uint) { + switch uintSize { + case 2: + a.PutUint16(uint16(v)) + case 4: + a.PutUint32(uint32(v)) + case 8: + a.PutUint64(uint64(v)) + default: + panic(fmt.Sprintf("unsupported uint size %d", uintSize)) + } +} + +// alignCheckedRead aligns the (read) position if necessary and suitable +// according to the specified alignment mask. alignCheckedRead returns an error +// if after any necessary alignment there isn't enough data left to be read into +// a value of the size corresponding to the specified alignment mask. +func (a *AlignedBuff) alignCheckedRead(m int) error { + a.pos = (a.pos + m) & ^m + if a.pos > len(a.data)-(m+1) { + return ErrEOF + } + return nil +} + +// alignWrite aligns the (write) position if necessary and suitable according to +// the specified alignment mask. It doubles as final payload padding helpmate in +// order to keep the kernel happy. +func (a *AlignedBuff) alignWrite(m int) { + pos := (a.pos + m) & ^m + if pos != a.pos { + a.data = append(a.data, padding[:pos-a.pos]...) + a.pos = pos + } +} + +// This is ... ugly. +var uint16AlignMask = int(unsafe.Alignof(uint16(0)) - 1) +var uint32AlignMask = int(unsafe.Alignof(uint32(0)) - 1) +var uint64AlignMask = int(unsafe.Alignof(uint64(0)) - 1) +var padding = bytes.Repeat([]byte{0}, uint64AlignMask) + +// And this even worse. +var uintSize = unsafe.Sizeof(uint32(0)) diff --git a/alignedbuff/alignedbuff_test.go b/alignedbuff/alignedbuff_test.go new file mode 100644 index 0000000..dd14a9f --- /dev/null +++ b/alignedbuff/alignedbuff_test.go @@ -0,0 +1,204 @@ +package alignedbuff + +import ( + "testing" +) + +func TestAlignmentData(t *testing.T) { + if uint16AlignMask == 0 { + t.Fatal("zero uint16 alignment mask") + } + if uint32AlignMask == 0 { + t.Fatal("zero uint32 alignment mask") + } + if uint64AlignMask == 0 { + t.Fatal("zero uint64 alignment mask") + } + if len(padding) == 0 { + t.Fatal("zero alignment padding sequence") + } + if uintSize == 0 { + t.Fatal("zero uint size") + } +} + +func TestAlignedBuff8(t *testing.T) { + b := NewWithData([]byte{0x42}) + tests := []struct { + name string + v uint8 + err error + }{ + { + name: "first read", + v: 0x42, + err: nil, + }, + { + name: "end of buffer", + v: 0, + err: ErrEOF, + }, + } + + for _, tt := range tests { + v, err := b.Uint8() + if v != tt.v || err != tt.err { + t.Errorf("expected: %#v %#v, got: %#v, %#v", + tt.v, tt.err, v, err) + } + } +} + +func TestAlignedBuff16(t *testing.T) { + b0 := New() + b0.PutUint8(0x42) + b0.PutUint16(0x1234) + b0.PutUint16(0x5678) + + b := NewWithData(b0.data) + v, err := b.Uint8() + if v != 0x42 || err != nil { + t.Fatalf("unaligment read failed") + } + tests := []struct { + name string + v uint16 + err error + }{ + { + name: "first read", + v: 0x1234, + err: nil, + }, + { + name: "second read", + v: 0x5678, + err: nil, + }, + { + name: "end of buffer", + v: 0, + err: ErrEOF, + }, + } + + for _, tt := range tests { + v, err := b.Uint16() + if v != tt.v || err != tt.err { + t.Errorf("%s failed, expected: %#v %#v, got: %#v, %#v", + tt.name, tt.v, tt.err, v, err) + } + } +} + +func TestAlignedBuff32(t *testing.T) { + b0 := New() + b0.PutUint8(0x42) + b0.PutUint32(0x12345678) + b0.PutUint32(0x01cecafe) + + b := NewWithData(b0.data) + + if len(b0.Data()) != 4*4 { + t.Fatalf("alignment padding failed") + } + + v, err := b.Uint8() + if v != 0x42 || err != nil { + t.Fatalf("unaligment read failed") + } + tests := []struct { + name string + v uint32 + err error + }{ + { + name: "first read", + v: 0x12345678, + err: nil, + }, + { + name: "second read", + v: 0x01cecafe, + err: nil, + }, + { + name: "end of buffer", + v: 0, + err: ErrEOF, + }, + } + + for _, tt := range tests { + v, err := b.Uint32() + if v != tt.v || err != tt.err { + t.Errorf("expected: %#v %#v, got: %#v, %#v", + tt.v, tt.err, v, err) + } + } +} + +func TestAlignedBuff64(t *testing.T) { + b0 := New() + b0.PutUint8(0x42) + b0.PutUint64(0x1234567823456789) + b0.PutUint64(0x01cecafec001beef) + + b := NewWithData(b0.data) + v, err := b.Uint8() + if v != 0x42 || err != nil { + t.Fatalf("unaligment read failed") + } + tests := []struct { + name string + v uint64 + err error + }{ + { + name: "first read", + v: 0x1234567823456789, + err: nil, + }, + { + name: "second read", + v: 0x01cecafec001beef, + err: nil, + }, + { + name: "end of buffer", + v: 0, + err: ErrEOF, + }, + } + + for _, tt := range tests { + v, err := b.Uint64() + if v != tt.v || err != tt.err { + t.Errorf("expected: %#v %#v, got: %#v, %#v", + tt.v, tt.err, v, err) + } + } +} + +func TestAlignedUint(t *testing.T) { + expectedv := uint(^uint32(0) - 1) + b0 := New() + b0.PutUint8(0x55) + b0.PutUint(expectedv) + b0.PutUint8(0xAA) + + b := NewWithData(b0.data) + v, err := b.Uint8() + if v != 0x55 || err != nil { + t.Fatalf("sentinel read failed") + } + uiv, err := b.Uint() + if uiv != expectedv || err != nil { + t.Fatalf("uint read failed, expected: %d, got: %d", expectedv, uiv) + } + v, err = b.Uint8() + if v != 0xAA || err != nil { + t.Fatalf("sentinel read failed") + } +}