From 8ea944061f7c8959e836e1f721c94d259ad9d2d6 Mon Sep 17 00:00:00 2001 From: thediveo Date: Sat, 14 May 2022 16:49:27 +0000 Subject: [PATCH] add typed xtables information un/marshalling more tests and fixes more info support; refactoring --- expr/match.go | 20 ++- expr/match_test.go | 4 +- expr/target.go | 20 ++- expr/target_test.go | 4 +- xt/info.go | 94 ++++++++++++ xt/match_addrtype.go | 89 ++++++++++++ xt/match_addrtype_test.go | 71 +++++++++ xt/match_conntrack.go | 250 ++++++++++++++++++++++++++++++++ xt/match_conntrack_test.go | 213 +++++++++++++++++++++++++++ xt/match_tcp.go | 74 ++++++++++ xt/match_tcp_test.go | 44 ++++++ xt/match_udp.go | 57 ++++++++ xt/match_udp_test.go | 41 ++++++ xt/target_dnat.go | 106 ++++++++++++++ xt/target_dnat_test.go | 97 +++++++++++++ xt/target_masquerade_ip.go | 86 +++++++++++ xt/target_masquerade_ip_test.go | 54 +++++++ xt/unknown.go | 17 +++ xt/unknown_test.go | 38 +++++ xt/util.go | 64 ++++++++ xt/xt.go | 50 +++++++ 21 files changed, 1483 insertions(+), 10 deletions(-) create mode 100644 xt/info.go create mode 100644 xt/match_addrtype.go create mode 100644 xt/match_addrtype_test.go create mode 100644 xt/match_conntrack.go create mode 100644 xt/match_conntrack_test.go create mode 100644 xt/match_tcp.go create mode 100644 xt/match_tcp_test.go create mode 100644 xt/match_udp.go create mode 100644 xt/match_udp_test.go create mode 100644 xt/target_dnat.go create mode 100644 xt/target_dnat_test.go create mode 100644 xt/target_masquerade_ip.go create mode 100644 xt/target_masquerade_ip_test.go create mode 100644 xt/unknown.go create mode 100644 xt/unknown_test.go create mode 100644 xt/util.go create mode 100644 xt/xt.go diff --git a/expr/match.go b/expr/match.go index b568f4a..123c6f9 100644 --- a/expr/match.go +++ b/expr/match.go @@ -5,6 +5,7 @@ import ( "encoding/binary" "github.com/google/nftables/binaryutil" + "github.com/google/nftables/xt" "github.com/mdlayher/netlink" "golang.org/x/sys/unix" ) @@ -13,7 +14,7 @@ import ( type Match struct { Name string Rev uint32 - Info []byte + Info xt.InfoAny } func (e *Match) marshal(fam byte) ([]byte, error) { @@ -24,10 +25,16 @@ func (e *Match) marshal(fam byte) ([]byte, error) { if len(name) >= /* sic! */ XTablesExtensionNameMaxLen { name = name[:XTablesExtensionNameMaxLen-1] // leave room for trailing \x00. } + // Marshalling assumes that the correct Info type for the particular table + // family and Match revision has been set. + info, err := xt.Marshal(xt.TableFamily(fam), e.Rev, e.Info) + if err != nil { + return nil, err + } attrs := []netlink.Attribute{ {Type: unix.NFTA_MATCH_NAME, Data: []byte(name + "\x00")}, {Type: unix.NFTA_MATCH_REV, Data: binaryutil.BigEndian.PutUint32(e.Rev)}, - {Type: unix.NFTA_MATCH_INFO, Data: e.Info}, + {Type: unix.NFTA_MATCH_INFO, Data: info}, } data, err := netlink.MarshalAttributes(attrs) if err != nil { @@ -47,6 +54,7 @@ func (e *Match) unmarshal(fam byte, data []byte) error { return err } + var info []byte ad.ByteOrder = binary.BigEndian for ad.Next() { switch ad.Type() { @@ -56,8 +64,12 @@ func (e *Match) unmarshal(fam byte, data []byte) error { case unix.NFTA_MATCH_REV: e.Rev = ad.Uint32() case unix.NFTA_MATCH_INFO: - e.Info = ad.Bytes() + info = ad.Bytes() } } - return ad.Err() + if err = ad.Err(); err != nil { + return err + } + e.Info, err = xt.Unmarshal(e.Name, xt.TableFamily(fam), e.Rev, info) + return err } diff --git a/expr/match_test.go b/expr/match_test.go index 2e12d9c..0923c9f 100644 --- a/expr/match_test.go +++ b/expr/match_test.go @@ -5,12 +5,14 @@ import ( "reflect" "testing" + "github.com/google/nftables/xt" "github.com/mdlayher/netlink" "golang.org/x/sys/unix" ) func TestMatch(t *testing.T) { t.Parallel() + payload := xt.Unknown([]byte{0xb0, 0x1d, 0xca, 0xfe, 0x00}) tests := []struct { name string mtch Match @@ -20,7 +22,7 @@ func TestMatch(t *testing.T) { mtch: Match{ Name: "foobar", Rev: 1234567890, - Info: []byte{0xb0, 0x1d, 0xca, 0xfe, 0x00}, + Info: &payload, }, }, } diff --git a/expr/target.go b/expr/target.go index c22d30c..e531a9f 100644 --- a/expr/target.go +++ b/expr/target.go @@ -5,6 +5,7 @@ import ( "encoding/binary" "github.com/google/nftables/binaryutil" + "github.com/google/nftables/xt" "github.com/mdlayher/netlink" "golang.org/x/sys/unix" ) @@ -16,7 +17,7 @@ const XTablesExtensionNameMaxLen = 29 type Target struct { Name string Rev uint32 - Info []byte + Info xt.InfoAny } func (e *Target) marshal(fam byte) ([]byte, error) { @@ -27,10 +28,16 @@ func (e *Target) marshal(fam byte) ([]byte, error) { if len(name) >= /* sic! */ XTablesExtensionNameMaxLen { name = name[:XTablesExtensionNameMaxLen-1] // leave room for trailing \x00. } + // Marshalling assumes that the correct Info type for the particular table + // family and Match revision has been set. + info, err := xt.Marshal(xt.TableFamily(fam), e.Rev, e.Info) + if err != nil { + return nil, err + } attrs := []netlink.Attribute{ {Type: unix.NFTA_TARGET_NAME, Data: []byte(name + "\x00")}, {Type: unix.NFTA_TARGET_REV, Data: binaryutil.BigEndian.PutUint32(e.Rev)}, - {Type: unix.NFTA_TARGET_INFO, Data: e.Info}, + {Type: unix.NFTA_TARGET_INFO, Data: info}, } data, err := netlink.MarshalAttributes(attrs) @@ -51,6 +58,7 @@ func (e *Target) unmarshal(fam byte, data []byte) error { return err } + var info []byte ad.ByteOrder = binary.BigEndian for ad.Next() { switch ad.Type() { @@ -60,8 +68,12 @@ func (e *Target) unmarshal(fam byte, data []byte) error { case unix.NFTA_TARGET_REV: e.Rev = ad.Uint32() case unix.NFTA_TARGET_INFO: - e.Info = ad.Bytes() + info = ad.Bytes() } } - return ad.Err() + if err = ad.Err(); err != nil { + return err + } + e.Info, err = xt.Unmarshal(e.Name, xt.TableFamily(fam), e.Rev, info) + return err } diff --git a/expr/target_test.go b/expr/target_test.go index 87783b4..e630e86 100644 --- a/expr/target_test.go +++ b/expr/target_test.go @@ -5,12 +5,14 @@ import ( "reflect" "testing" + "github.com/google/nftables/xt" "github.com/mdlayher/netlink" "golang.org/x/sys/unix" ) func TestTarget(t *testing.T) { t.Parallel() + payload := xt.Unknown([]byte{0xb0, 0x1d, 0xca, 0xfe, 0x00}) tests := []struct { name string tgt Target @@ -20,7 +22,7 @@ func TestTarget(t *testing.T) { tgt: Target{ Name: "foobar", Rev: 1234567890, - Info: []byte{0xb0, 0x1d, 0xca, 0xfe, 0x00}, + Info: &payload, }, }, } diff --git a/xt/info.go b/xt/info.go new file mode 100644 index 0000000..0cf9ab9 --- /dev/null +++ b/xt/info.go @@ -0,0 +1,94 @@ +package xt + +import ( + "golang.org/x/sys/unix" +) + +// TableFamily specifies the address family of the table Match or Target Info +// data is contained in. On purpose, we don't import the expr package here in +// order to keep the option open to import this package instead into expr. +type TableFamily byte + +// InfoAny is a (un)marshaling implemented by any info type. +type InfoAny interface { + marshal(fam TableFamily, rev uint32) ([]byte, error) + unmarshal(fam TableFamily, rev uint32, data []byte) error +} + +// Marshal a Match or Target Info type into its binary representation. +func Marshal(fam TableFamily, rev uint32, info InfoAny) ([]byte, error) { + return info.marshal(fam, rev) +} + +// Unmarshal Info binary payload into its corresponding dedicated type as +// indicated by the name argument. In several cases, unmarshalling depends on +// the specific table family the Target or Match expression with the info +// payload belongs to, as well as the specific info structure revision. +func Unmarshal(name string, fam TableFamily, rev uint32, data []byte) (InfoAny, error) { + var i InfoAny + switch name { + case "addrtype": + switch rev { + case 0: + i = &AddrType{} + case 1: + i = &AddrTypeV1{} + } + case "conntrack": + switch rev { + case 1: + i = &ConntrackMtinfo1{} + case 2: + i = &ConntrackMtinfo2{} + case 3: + i = &ConntrackMtinfo3{} + } + case "tcp": + i = &Tcp{} + case "udp": + i = &Udp{} + case "SNAT": + if fam == unix.NFPROTO_IPV4 { + i = &NatIPv4MultiRangeCompat{} + } + case "DNAT": + switch fam { + case unix.NFPROTO_IPV4: + if rev == 0 { + i = &NatIPv4MultiRangeCompat{} + break + } + fallthrough + case unix.NFPROTO_IPV6: + switch rev { + case 1: + i = &NatRange{} + case 2: + i = &NatRange2{} + } + } + case "MASQUERADE": + switch fam { + case unix.NFPROTO_IPV4: + i = &NatIPv4MultiRangeCompat{} + } + case "REDIRECT": + switch fam { + case unix.NFPROTO_IPV4: + if rev == 0 { + i = &NatIPv4MultiRangeCompat{} + break + } + fallthrough + case unix.NFPROTO_IPV6: + i = &NatRange{} + } + } + if i == nil { + i = &Unknown{} + } + if err := i.unmarshal(fam, rev, data); err != nil { + return nil, err + } + return i, nil +} diff --git a/xt/match_addrtype.go b/xt/match_addrtype.go new file mode 100644 index 0000000..d822796 --- /dev/null +++ b/xt/match_addrtype.go @@ -0,0 +1,89 @@ +package xt + +import ( + "github.com/google/nftables/alignedbuff" +) + +// Rev. 0, see https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/netfilter/xt_addrtype.h#L38 +type AddrType struct { + Source uint16 + Dest uint16 + InvertSource bool + InvertDest bool +} + +type AddrTypeFlags uint32 + +const ( + AddrTypeUnspec AddrTypeFlags = 1 << iota + AddrTypeUnicast + AddrTypeLocal + AddrTypeBroadcast + AddrTypeAnycast + AddrTypeMulticast + AddrTypeBlackhole + AddrTypeUnreachable + AddrTypeProhibit + AddrTypeThrow + AddrTypeNat + AddrTypeXresolve +) + +// See https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/netfilter/xt_addrtype.h#L31 +type AddrTypeV1 struct { + Source uint16 + Dest uint16 + Flags AddrTypeFlags +} + +func (x *AddrType) marshal(fam TableFamily, rev uint32) ([]byte, error) { + ab := alignedbuff.New() + ab.PutUint16(x.Source) + ab.PutUint16(x.Dest) + putBool32(&ab, x.InvertSource) + putBool32(&ab, x.InvertDest) + return ab.Data(), nil +} + +func (x *AddrType) unmarshal(fam TableFamily, rev uint32, data []byte) error { + ab := alignedbuff.NewWithData(data) + var err error + if x.Source, err = ab.Uint16(); err != nil { + return nil + } + if x.Dest, err = ab.Uint16(); err != nil { + return nil + } + if x.InvertSource, err = bool32(&ab); err != nil { + return nil + } + if x.InvertDest, err = bool32(&ab); err != nil { + return nil + } + return nil +} + +func (x *AddrTypeV1) marshal(fam TableFamily, rev uint32) ([]byte, error) { + ab := alignedbuff.New() + ab.PutUint16(x.Source) + ab.PutUint16(x.Dest) + ab.PutUint32(uint32(x.Flags)) + return ab.Data(), nil +} + +func (x *AddrTypeV1) unmarshal(fam TableFamily, rev uint32, data []byte) error { + ab := alignedbuff.NewWithData(data) + var err error + if x.Source, err = ab.Uint16(); err != nil { + return nil + } + if x.Dest, err = ab.Uint16(); err != nil { + return nil + } + var flags uint32 + if flags, err = ab.Uint32(); err != nil { + return nil + } + x.Flags = AddrTypeFlags(flags) + return nil +} diff --git a/xt/match_addrtype_test.go b/xt/match_addrtype_test.go new file mode 100644 index 0000000..76bc750 --- /dev/null +++ b/xt/match_addrtype_test.go @@ -0,0 +1,71 @@ +package xt + +import ( + "reflect" + "testing" +) + +func TestTargetAddrType(t *testing.T) { + t.Parallel() + tests := []struct { + name string + fam byte + rev uint32 + info InfoAny + empty InfoAny + }{ + { + name: "un/marshal AddrType Rev 0 round-trip", + fam: 0, + rev: 0, + info: &AddrType{ + Source: 0x1234, + Dest: 0x5678, + InvertSource: true, + InvertDest: false, + }, + empty: &AddrType{}, + }, + { + name: "un/marshal AddrType Rev 0 round-trip", + fam: 0, + rev: 0, + info: &AddrType{ + Source: 0x1234, + Dest: 0x5678, + InvertSource: false, + InvertDest: true, + }, + empty: &AddrType{}, + }, + { + name: "un/marshal AddrType Rev 1 round-trip", + fam: 0, + rev: 0, + info: &AddrTypeV1{ + Source: 0x1234, + Dest: 0x5678, + Flags: 0xb00f, + }, + empty: &AddrTypeV1{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + data, err := tt.info.marshal(TableFamily(tt.fam), tt.rev) + if err != nil { + t.Fatalf("marshal error: %+v", err) + + } + var recoveredInfo InfoAny = tt.empty + err = recoveredInfo.unmarshal(TableFamily(tt.fam), tt.rev, data) + if err != nil { + t.Fatalf("unmarshal error: %+v", err) + } + if !reflect.DeepEqual(tt.info, recoveredInfo) { + t.Fatalf("original %+v and recovered %+v are different", tt.info, recoveredInfo) + } + }) + } +} diff --git a/xt/match_conntrack.go b/xt/match_conntrack.go new file mode 100644 index 0000000..ff35728 --- /dev/null +++ b/xt/match_conntrack.go @@ -0,0 +1,250 @@ +package xt + +import ( + "net" + + "github.com/google/nftables/alignedbuff" +) + +type ConntrackFlags uint16 + +const ( + ConntrackState ConntrackFlags = 1 << iota + ConntrackProto + ConntrackOrigSrc + ConntrackOrigDst + ConntrackReplSrc + ConntrackReplDst + ConntrackStatus + ConntrackExpires + ConntrackOrigSrcPort + ConntrackOrigDstPort + ConntrackReplSrcPort + ConntrackReplDstPrt + ConntrackDirection + ConntrackStateAlias +) + +type ConntrackMtinfoBase struct { + OrigSrcAddr net.IP + OrigSrcMask net.IPMask + OrigDstAddr net.IP + OrigDstMask net.IPMask + ReplSrcAddr net.IP + ReplSrcMask net.IPMask + ReplDstAddr net.IP + ReplDstMask net.IPMask + ExpiresMin uint32 + ExpiresMax uint32 + L4Proto uint16 + OrigSrcPort uint16 + OrigDstPort uint16 + ReplSrcPort uint16 + ReplDstPort uint16 +} + +// See https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/netfilter/xt_conntrack.h#L38 +type ConntrackMtinfo1 struct { + ConntrackMtinfoBase + StateMask uint8 + StatusMask uint8 +} + +// See https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/netfilter/xt_conntrack.h#L51 +type ConntrackMtinfo2 struct { + ConntrackMtinfoBase + StateMask uint16 + StatusMask uint16 +} + +// See https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/netfilter/xt_conntrack.h#L64 +type ConntrackMtinfo3 struct { + ConntrackMtinfo2 + OrigSrcPortHigh uint16 + OrigDstPortHigh uint16 + ReplSrcPortHigh uint16 + ReplDstPortHigh uint16 +} + +func (x *ConntrackMtinfoBase) marshalAB(fam TableFamily, rev uint32, ab *alignedbuff.AlignedBuff) error { + if err := putIPv46(ab, fam, x.OrigSrcAddr); err != nil { + return err + } + if err := putIPv46Mask(ab, fam, x.OrigSrcMask); err != nil { + return err + } + if err := putIPv46(ab, fam, x.OrigDstAddr); err != nil { + return err + } + if err := putIPv46Mask(ab, fam, x.OrigDstMask); err != nil { + return err + } + if err := putIPv46(ab, fam, x.ReplSrcAddr); err != nil { + return err + } + if err := putIPv46Mask(ab, fam, x.ReplSrcMask); err != nil { + return err + } + if err := putIPv46(ab, fam, x.ReplDstAddr); err != nil { + return err + } + if err := putIPv46Mask(ab, fam, x.ReplDstMask); err != nil { + return err + } + ab.PutUint32(x.ExpiresMin) + ab.PutUint32(x.ExpiresMax) + ab.PutUint16(x.L4Proto) + ab.PutUint16(x.OrigSrcPort) + ab.PutUint16(x.OrigDstPort) + ab.PutUint16(x.ReplSrcPort) + ab.PutUint16(x.ReplDstPort) + return nil +} + +func (x *ConntrackMtinfoBase) unmarshalAB(fam TableFamily, rev uint32, ab *alignedbuff.AlignedBuff) error { + var err error + if x.OrigSrcAddr, err = iPv46(ab, fam); err != nil { + return err + } + if x.OrigSrcMask, err = iPv46Mask(ab, fam); err != nil { + return err + } + if x.OrigDstAddr, err = iPv46(ab, fam); err != nil { + return err + } + if x.OrigDstMask, err = iPv46Mask(ab, fam); err != nil { + return err + } + if x.ReplSrcAddr, err = iPv46(ab, fam); err != nil { + return err + } + if x.ReplSrcMask, err = iPv46Mask(ab, fam); err != nil { + return err + } + if x.ReplDstAddr, err = iPv46(ab, fam); err != nil { + return err + } + if x.ReplDstMask, err = iPv46Mask(ab, fam); err != nil { + return err + } + if x.ExpiresMin, err = ab.Uint32(); err != nil { + return err + } + if x.ExpiresMax, err = ab.Uint32(); err != nil { + return err + } + if x.L4Proto, err = ab.Uint16(); err != nil { + return err + } + if x.OrigSrcPort, err = ab.Uint16(); err != nil { + return err + } + if x.OrigDstPort, err = ab.Uint16(); err != nil { + return err + } + if x.ReplSrcPort, err = ab.Uint16(); err != nil { + return err + } + if x.ReplDstPort, err = ab.Uint16(); err != nil { + return err + } + return nil +} + +func (x *ConntrackMtinfo1) marshal(fam TableFamily, rev uint32) ([]byte, error) { + ab := alignedbuff.New() + if err := x.ConntrackMtinfoBase.marshalAB(fam, rev, &ab); err != nil { + return nil, err + } + ab.PutUint8(x.StateMask) + ab.PutUint8(x.StatusMask) + return ab.Data(), nil +} + +func (x *ConntrackMtinfo1) unmarshal(fam TableFamily, rev uint32, data []byte) error { + ab := alignedbuff.NewWithData(data) + var err error + if err = x.ConntrackMtinfoBase.unmarshalAB(fam, rev, &ab); err != nil { + return err + } + if x.StateMask, err = ab.Uint8(); err != nil { + return err + } + if x.StatusMask, err = ab.Uint8(); err != nil { + return err + } + return nil +} + +func (x *ConntrackMtinfo2) marshalAB(fam TableFamily, rev uint32, ab *alignedbuff.AlignedBuff) error { + if err := x.ConntrackMtinfoBase.marshalAB(fam, rev, ab); err != nil { + return err + } + ab.PutUint16(x.StateMask) + ab.PutUint16(x.StatusMask) + return nil +} + +func (x *ConntrackMtinfo2) marshal(fam TableFamily, rev uint32) ([]byte, error) { + ab := alignedbuff.New() + if err := x.marshalAB(fam, rev, &ab); err != nil { + return nil, err + } + return ab.Data(), nil +} + +func (x *ConntrackMtinfo2) unmarshalAB(fam TableFamily, rev uint32, ab *alignedbuff.AlignedBuff) error { + var err error + if err = x.ConntrackMtinfoBase.unmarshalAB(fam, rev, ab); err != nil { + return err + } + if x.StateMask, err = ab.Uint16(); err != nil { + return err + } + if x.StatusMask, err = ab.Uint16(); err != nil { + return err + } + return nil +} + +func (x *ConntrackMtinfo2) unmarshal(fam TableFamily, rev uint32, data []byte) error { + ab := alignedbuff.NewWithData(data) + var err error + if err = x.unmarshalAB(fam, rev, &ab); err != nil { + return err + } + return nil +} + +func (x *ConntrackMtinfo3) marshal(fam TableFamily, rev uint32) ([]byte, error) { + ab := alignedbuff.New() + if err := x.ConntrackMtinfo2.marshalAB(fam, rev, &ab); err != nil { + return nil, err + } + ab.PutUint16(x.OrigSrcPortHigh) + ab.PutUint16(x.OrigDstPortHigh) + ab.PutUint16(x.ReplSrcPortHigh) + ab.PutUint16(x.ReplDstPortHigh) + return ab.Data(), nil +} + +func (x *ConntrackMtinfo3) unmarshal(fam TableFamily, rev uint32, data []byte) error { + ab := alignedbuff.NewWithData(data) + var err error + if err = x.ConntrackMtinfo2.unmarshalAB(fam, rev, &ab); err != nil { + return err + } + if x.OrigSrcPortHigh, err = ab.Uint16(); err != nil { + return err + } + if x.OrigDstPortHigh, err = ab.Uint16(); err != nil { + return err + } + if x.ReplSrcPortHigh, err = ab.Uint16(); err != nil { + return err + } + if x.ReplDstPortHigh, err = ab.Uint16(); err != nil { + return err + } + return nil +} diff --git a/xt/match_conntrack_test.go b/xt/match_conntrack_test.go new file mode 100644 index 0000000..6b69b2d --- /dev/null +++ b/xt/match_conntrack_test.go @@ -0,0 +1,213 @@ +package xt + +import ( + "net" + "reflect" + "testing" + + "golang.org/x/sys/unix" +) + +func TestMatchConntrack(t *testing.T) { + t.Parallel() + tests := []struct { + name string + fam byte + rev uint32 + info InfoAny + empty InfoAny + }{ + { + name: "un/marshal ConntrackMtinfo1 IPv4 round-trip", + fam: unix.NFPROTO_IPV4, + rev: 0, + info: &ConntrackMtinfo1{ + ConntrackMtinfoBase: ConntrackMtinfoBase{ + OrigSrcAddr: net.ParseIP("1.2.3.4").To4(), + OrigSrcMask: net.IPv4Mask(0x12, 0x23, 0x34, 0x45), // only for test ;) + OrigDstAddr: net.ParseIP("2.3.4.5").To4(), + OrigDstMask: net.IPv4Mask(0x23, 0x34, 0x45, 0x56), // only for test ;) + ReplSrcAddr: net.ParseIP("10.20.30.40").To4(), + ReplSrcMask: net.IPv4Mask(0xf2, 0xe3, 0xd4, 0xc5), // only for test ;) + ReplDstAddr: net.ParseIP("2.3.4.5").To4(), + ReplDstMask: net.IPv4Mask(0xe3, 0xd4, 0xc5, 0xb6), // only for test ;) + ExpiresMin: 0x1234, + ExpiresMax: 0x2345, + L4Proto: 0xaa55, + OrigSrcPort: 123, + OrigDstPort: 321, + ReplSrcPort: 789, + ReplDstPort: 987, + }, + StateMask: 0x55, + StatusMask: 0xaa, + }, + empty: &ConntrackMtinfo1{}, + }, + { + name: "un/marshal ConntrackMtinfo1 IPv6 round-trip", + fam: unix.NFPROTO_IPV6, + rev: 0, + info: &ConntrackMtinfo1{ + ConntrackMtinfoBase: ConntrackMtinfoBase{ + OrigSrcAddr: net.ParseIP("fe80::dead:f001"), + OrigSrcMask: net.IPMask{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10}, // only for test ;) + OrigDstAddr: net.ParseIP("fd00::dead:f001"), + OrigDstMask: net.IPMask{0x11, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10}, // only for test ;) + ReplSrcAddr: net.ParseIP("fe80::c01d:cafe"), + ReplSrcMask: net.IPMask{0x21, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10}, // only for test ;) + ReplDstAddr: net.ParseIP("fd00::c01d:cafe"), + ReplDstMask: net.IPMask{0x31, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10}, // only for test ;) + ExpiresMin: 0x1234, + ExpiresMax: 0x2345, + L4Proto: 0xaa55, + OrigSrcPort: 123, + OrigDstPort: 321, + ReplSrcPort: 789, + ReplDstPort: 987, + }, + StateMask: 0x55, + StatusMask: 0xaa, + }, + empty: &ConntrackMtinfo1{}, + }, + { + name: "un/marshal ConntrackMtinfo2 IPv4 round-trip", + fam: unix.NFPROTO_IPV4, + rev: 0, + info: &ConntrackMtinfo2{ + ConntrackMtinfoBase: ConntrackMtinfoBase{ + OrigSrcAddr: net.ParseIP("1.2.3.4").To4(), + OrigSrcMask: net.IPv4Mask(0x12, 0x23, 0x34, 0x45), // only for test ;) + OrigDstAddr: net.ParseIP("2.3.4.5").To4(), + OrigDstMask: net.IPv4Mask(0x23, 0x34, 0x45, 0x56), // only for test ;) + ReplSrcAddr: net.ParseIP("10.20.30.40").To4(), + ReplSrcMask: net.IPv4Mask(0xf2, 0xe3, 0xd4, 0xc5), // only for test ;) + ReplDstAddr: net.ParseIP("2.3.4.5").To4(), + ReplDstMask: net.IPv4Mask(0xe3, 0xd4, 0xc5, 0xb6), // only for test ;) + ExpiresMin: 0x1234, + ExpiresMax: 0x2345, + L4Proto: 0xaa55, + OrigSrcPort: 123, + OrigDstPort: 321, + ReplSrcPort: 789, + ReplDstPort: 987, + }, + StateMask: 0x55aa, + StatusMask: 0xaa55, + }, + empty: &ConntrackMtinfo2{}, + }, + { + name: "un/marshal ConntrackMtinfo1 IPv6 round-trip", + fam: unix.NFPROTO_IPV6, + rev: 0, + info: &ConntrackMtinfo2{ + ConntrackMtinfoBase: ConntrackMtinfoBase{ + OrigSrcAddr: net.ParseIP("fe80::dead:f001"), + OrigSrcMask: net.IPMask{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10}, // only for test ;) + OrigDstAddr: net.ParseIP("fd00::dead:f001"), + OrigDstMask: net.IPMask{0x11, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10}, // only for test ;) + ReplSrcAddr: net.ParseIP("fe80::c01d:cafe"), + ReplSrcMask: net.IPMask{0x21, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10}, // only for test ;) + ReplDstAddr: net.ParseIP("fd00::c01d:cafe"), + ReplDstMask: net.IPMask{0x31, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10}, // only for test ;) + ExpiresMin: 0x1234, + ExpiresMax: 0x2345, + L4Proto: 0xaa55, + OrigSrcPort: 123, + OrigDstPort: 321, + ReplSrcPort: 789, + ReplDstPort: 987, + }, + StateMask: 0x55aa, + StatusMask: 0xaa55, + }, + empty: &ConntrackMtinfo2{}, + }, + { + name: "un/marshal ConntrackMtinfo3 IPv4 round-trip", + fam: unix.NFPROTO_IPV4, + rev: 0, + info: &ConntrackMtinfo3{ + ConntrackMtinfo2: ConntrackMtinfo2{ + ConntrackMtinfoBase: ConntrackMtinfoBase{ + OrigSrcAddr: net.ParseIP("1.2.3.4").To4(), + OrigSrcMask: net.IPv4Mask(0x12, 0x23, 0x34, 0x45), // only for test ;) + OrigDstAddr: net.ParseIP("2.3.4.5").To4(), + OrigDstMask: net.IPv4Mask(0x23, 0x34, 0x45, 0x56), // only for test ;) + ReplSrcAddr: net.ParseIP("10.20.30.40").To4(), + ReplSrcMask: net.IPv4Mask(0xf2, 0xe3, 0xd4, 0xc5), // only for test ;) + ReplDstAddr: net.ParseIP("2.3.4.5").To4(), + ReplDstMask: net.IPv4Mask(0xe3, 0xd4, 0xc5, 0xb6), // only for test ;) + ExpiresMin: 0x1234, + ExpiresMax: 0x2345, + L4Proto: 0xaa55, + OrigSrcPort: 123, + OrigDstPort: 321, + ReplSrcPort: 789, + ReplDstPort: 987, + }, + StateMask: 0x55aa, + StatusMask: 0xaa55, + }, + OrigSrcPortHigh: 0xabcd, + OrigDstPortHigh: 0xcdba, + ReplSrcPortHigh: 0x1234, + ReplDstPortHigh: 0x4321, + }, + empty: &ConntrackMtinfo3{}, + }, + { + name: "un/marshal ConntrackMtinfo1 IPv6 round-trip", + fam: unix.NFPROTO_IPV6, + rev: 0, + info: &ConntrackMtinfo3{ + ConntrackMtinfo2: ConntrackMtinfo2{ + ConntrackMtinfoBase: ConntrackMtinfoBase{ + OrigSrcAddr: net.ParseIP("fe80::dead:f001"), + OrigSrcMask: net.IPMask{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10}, // only for test ;) + OrigDstAddr: net.ParseIP("fd00::dead:f001"), + OrigDstMask: net.IPMask{0x11, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10}, // only for test ;) + ReplSrcAddr: net.ParseIP("fe80::c01d:cafe"), + ReplSrcMask: net.IPMask{0x21, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10}, // only for test ;) + ReplDstAddr: net.ParseIP("fd00::c01d:cafe"), + ReplDstMask: net.IPMask{0x31, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10}, // only for test ;) + ExpiresMin: 0x1234, + ExpiresMax: 0x2345, + L4Proto: 0xaa55, + OrigSrcPort: 123, + OrigDstPort: 321, + ReplSrcPort: 789, + ReplDstPort: 987, + }, + StateMask: 0x55aa, + StatusMask: 0xaa55, + }, + OrigSrcPortHigh: 0xabcd, + OrigDstPortHigh: 0xcdba, + ReplSrcPortHigh: 0x1234, + ReplDstPortHigh: 0x4321, + }, + empty: &ConntrackMtinfo3{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + data, err := tt.info.marshal(TableFamily(tt.fam), tt.rev) + if err != nil { + t.Fatalf("marshal error: %+v", err) + + } + var recoveredInfo InfoAny = tt.empty + err = recoveredInfo.unmarshal(TableFamily(tt.fam), tt.rev, data) + if err != nil { + t.Fatalf("unmarshal error: %+v", err) + } + if !reflect.DeepEqual(tt.info, recoveredInfo) { + t.Fatalf("original %+v and recovered %+v are different", tt.info, recoveredInfo) + } + }) + } +} diff --git a/xt/match_tcp.go b/xt/match_tcp.go new file mode 100644 index 0000000..ddf5bf3 --- /dev/null +++ b/xt/match_tcp.go @@ -0,0 +1,74 @@ +package xt + +import ( + "github.com/google/nftables/alignedbuff" +) + +// Tcp is the Match.Info payload for the tcp xtables extension +// (https://wiki.nftables.org/wiki-nftables/index.php/Supported_features_compared_to_xtables#tcp). +// +// See +// https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/netfilter/xt_tcpudp.h#L8 +type Tcp struct { + SrcPorts [2]uint16 // min, max source port range + DstPorts [2]uint16 // min, max destination port range + Option uint8 // TCP option if non-zero + FlagsMask uint8 // TCP flags mask + FlagsCmp uint8 // TCP flags compare + InvFlags TcpInvFlagset // Inverse flags +} + +type TcpInvFlagset uint8 + +const ( + TcpInvSrcPorts TcpInvFlagset = 1 << iota + TcpInvDestPorts + TcpInvFlags + TcpInvOption + TcpInvMask TcpInvFlagset = (1 << iota) - 1 +) + +func (x *Tcp) marshal(fam TableFamily, rev uint32) ([]byte, error) { + ab := alignedbuff.New() + ab.PutUint16(x.SrcPorts[0]) + ab.PutUint16(x.SrcPorts[1]) + ab.PutUint16(x.DstPorts[0]) + ab.PutUint16(x.DstPorts[1]) + ab.PutUint8(x.Option) + ab.PutUint8(x.FlagsMask) + ab.PutUint8(x.FlagsCmp) + ab.PutUint8(byte(x.InvFlags)) + return ab.Data(), nil +} + +func (x *Tcp) unmarshal(fam TableFamily, rev uint32, data []byte) error { + ab := alignedbuff.NewWithData(data) + var err error + if x.SrcPorts[0], err = ab.Uint16(); err != nil { + return err + } + if x.SrcPorts[1], err = ab.Uint16(); err != nil { + return err + } + if x.DstPorts[0], err = ab.Uint16(); err != nil { + return err + } + if x.DstPorts[1], err = ab.Uint16(); err != nil { + return err + } + if x.Option, err = ab.Uint8(); err != nil { + return err + } + if x.FlagsMask, err = ab.Uint8(); err != nil { + return err + } + if x.FlagsCmp, err = ab.Uint8(); err != nil { + return err + } + var invFlags uint8 + if invFlags, err = ab.Uint8(); err != nil { + return err + } + x.InvFlags = TcpInvFlagset(invFlags) + return nil +} diff --git a/xt/match_tcp_test.go b/xt/match_tcp_test.go new file mode 100644 index 0000000..1e84f46 --- /dev/null +++ b/xt/match_tcp_test.go @@ -0,0 +1,44 @@ +package xt + +import ( + "reflect" + "testing" +) + +func TestMatchTcp(t *testing.T) { + t.Parallel() + tests := []struct { + name string + info InfoAny + }{ + { + name: "un/marshal Tcp round-trip", + info: &Tcp{ + SrcPorts: [2]uint16{0x1234, 0x5678}, + DstPorts: [2]uint16{0x2345, 0x6789}, + Option: 0x12, + FlagsMask: 0x34, + FlagsCmp: 0x56, + InvFlags: 0x78, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + data, err := tt.info.marshal(0, 0) + if err != nil { + t.Fatalf("marshal error: %+v", err) + + } + var recoveredInfo InfoAny = &Tcp{} + err = recoveredInfo.unmarshal(0, 0, data) + if err != nil { + t.Fatalf("unmarshal error: %+v", err) + } + if !reflect.DeepEqual(tt.info, recoveredInfo) { + t.Fatalf("original %+v and recovered %+v are different", tt.info, recoveredInfo) + } + }) + } +} diff --git a/xt/match_udp.go b/xt/match_udp.go new file mode 100644 index 0000000..2f10aa0 --- /dev/null +++ b/xt/match_udp.go @@ -0,0 +1,57 @@ +package xt + +import ( + "github.com/google/nftables/alignedbuff" +) + +// Tcp is the Match.Info payload for the tcp xtables extension +// (https://wiki.nftables.org/wiki-nftables/index.php/Supported_features_compared_to_xtables#tcp). +// +// See +// https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/netfilter/xt_tcpudp.h#L8 +type Udp struct { + SrcPorts [2]uint16 // min, max source port range + DstPorts [2]uint16 // min, max destination port range + InvFlags UdpInvFlagset // Inverse flags +} + +type UdpInvFlagset uint8 + +const ( + UdpInvSrcPorts UdpInvFlagset = 1 << iota + UdpInvDestPorts + UdpInvMask UdpInvFlagset = (1 << iota) - 1 +) + +func (x *Udp) marshal(fam TableFamily, rev uint32) ([]byte, error) { + ab := alignedbuff.New() + ab.PutUint16(x.SrcPorts[0]) + ab.PutUint16(x.SrcPorts[1]) + ab.PutUint16(x.DstPorts[0]) + ab.PutUint16(x.DstPorts[1]) + ab.PutUint8(byte(x.InvFlags)) + return ab.Data(), nil +} + +func (x *Udp) unmarshal(fam TableFamily, rev uint32, data []byte) error { + ab := alignedbuff.NewWithData(data) + var err error + if x.SrcPorts[0], err = ab.Uint16(); err != nil { + return err + } + if x.SrcPorts[1], err = ab.Uint16(); err != nil { + return err + } + if x.DstPorts[0], err = ab.Uint16(); err != nil { + return err + } + if x.DstPorts[1], err = ab.Uint16(); err != nil { + return err + } + var invFlags uint8 + if invFlags, err = ab.Uint8(); err != nil { + return err + } + x.InvFlags = UdpInvFlagset(invFlags) + return nil +} diff --git a/xt/match_udp_test.go b/xt/match_udp_test.go new file mode 100644 index 0000000..b4116d5 --- /dev/null +++ b/xt/match_udp_test.go @@ -0,0 +1,41 @@ +package xt + +import ( + "reflect" + "testing" +) + +func TestMatchUdp(t *testing.T) { + t.Parallel() + tests := []struct { + name string + info InfoAny + }{ + { + name: "un/marshal Udp round-trip", + info: &Udp{ + SrcPorts: [2]uint16{0x1234, 0x5678}, + DstPorts: [2]uint16{0x2345, 0x6789}, + InvFlags: 0x78, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + data, err := tt.info.marshal(0, 0) + if err != nil { + t.Fatalf("marshal error: %+v", err) + + } + var recoveredInfo InfoAny = &Udp{} + err = recoveredInfo.unmarshal(0, 0, data) + if err != nil { + t.Fatalf("unmarshal error: %+v", err) + } + if !reflect.DeepEqual(tt.info, recoveredInfo) { + t.Fatalf("original %+v and recovered %+v are different", tt.info, recoveredInfo) + } + }) + } +} diff --git a/xt/target_dnat.go b/xt/target_dnat.go new file mode 100644 index 0000000..6aaa7f7 --- /dev/null +++ b/xt/target_dnat.go @@ -0,0 +1,106 @@ +package xt + +import ( + "net" + + "github.com/google/nftables/alignedbuff" +) + +type NatRangeFlags uint + +// See: https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/netfilter/nf_nat.h#L8 +const ( + NatRangeMapIPs NatRangeFlags = (1 << iota) + NatRangeProtoSpecified + NatRangeProtoRandom + NatRangePersistent + NatRangeProtoRandomFully + NatRangeProtoOffset + NatRangeNetmap + + NatRangeMask NatRangeFlags = (1 << iota) - 1 + + NatRangeProtoRandomAll = NatRangeProtoRandom | NatRangeProtoRandomFully +) + +// see: https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/netfilter/nf_nat.h#L38 +type NatRange struct { + Flags uint // sic! platform/arch/compiler-dependent uint size + MinIP net.IP // always taking up space for an IPv6 address + MaxIP net.IP // dito + MinPort uint16 + MaxPort uint16 +} + +// see: https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/netfilter/nf_nat.h#L46 +type NatRange2 struct { + NatRange + BasePort uint16 +} + +func (x *NatRange) marshal(fam TableFamily, rev uint32) ([]byte, error) { + ab := alignedbuff.New() + if err := x.marshalAB(fam, rev, &ab); err != nil { + return nil, err + } + return ab.Data(), nil +} + +func (x *NatRange) marshalAB(fam TableFamily, rev uint32, ab *alignedbuff.AlignedBuff) error { + ab.PutUint(x.Flags) + if err := putIPv46(ab, fam, x.MinIP); err != nil { + return err + } + if err := putIPv46(ab, fam, x.MaxIP); err != nil { + return err + } + ab.PutUint16BE(x.MinPort) + ab.PutUint16BE(x.MaxPort) + return nil +} + +func (x *NatRange) unmarshal(fam TableFamily, rev uint32, data []byte) error { + ab := alignedbuff.NewWithData(data) + return x.unmarshalAB(fam, rev, &ab) +} + +func (x *NatRange) unmarshalAB(fam TableFamily, rev uint32, ab *alignedbuff.AlignedBuff) error { + var err error + if x.Flags, err = ab.Uint(); err != nil { + return err + } + if x.MinIP, err = iPv46(ab, fam); err != nil { + return err + } + if x.MaxIP, err = iPv46(ab, fam); err != nil { + return err + } + if x.MinPort, err = ab.Uint16BE(); err != nil { + return err + } + if x.MaxPort, err = ab.Uint16BE(); err != nil { + return err + } + return nil +} + +func (x *NatRange2) marshal(fam TableFamily, rev uint32) ([]byte, error) { + ab := alignedbuff.New() + if err := x.NatRange.marshalAB(fam, rev, &ab); err != nil { + return nil, err + } + ab.PutUint16BE(x.BasePort) + return ab.Data(), nil +} + +func (x *NatRange2) unmarshal(fam TableFamily, rev uint32, data []byte) error { + ab := alignedbuff.NewWithData(data) + var err error + if err = x.NatRange.unmarshalAB(fam, rev, &ab); err != nil { + return err + } + if x.BasePort, err = ab.Uint16BE(); err != nil { + return err + } + return nil +} diff --git a/xt/target_dnat_test.go b/xt/target_dnat_test.go new file mode 100644 index 0000000..0e43762 --- /dev/null +++ b/xt/target_dnat_test.go @@ -0,0 +1,97 @@ +package xt + +import ( + "net" + "reflect" + "testing" + + "golang.org/x/sys/unix" +) + +func TestTargetDNAT(t *testing.T) { + t.Parallel() + tests := []struct { + name string + fam byte + rev uint32 + info InfoAny + empty InfoAny + }{ + { + name: "un/marshal NatRange IPv4 round-trip", + fam: unix.NFPROTO_IPV4, + rev: 0, + info: &NatRange{ + Flags: 0x1234, + MinIP: net.ParseIP("12.23.34.45").To4(), + MaxIP: net.ParseIP("21.32.43.54").To4(), + MinPort: 0x5678, + MaxPort: 0xabcd, + }, + empty: &NatRange{}, + }, + { + name: "un/marshal NatRange IPv6 round-trip", + fam: unix.NFPROTO_IPV6, + rev: 0, + info: &NatRange{ + Flags: 0x1234, + MinIP: net.ParseIP("fe80::dead:beef"), + MaxIP: net.ParseIP("fe80::c001:cafe"), + MinPort: 0x5678, + MaxPort: 0xabcd, + }, + empty: &NatRange{}, + }, + { + name: "un/marshal NatRange2 IPv4 round-trip", + fam: unix.NFPROTO_IPV4, + rev: 0, + info: &NatRange2{ + NatRange: NatRange{ + Flags: 0x1234, + MinIP: net.ParseIP("12.23.34.45").To4(), + MaxIP: net.ParseIP("21.32.43.54").To4(), + MinPort: 0x5678, + MaxPort: 0xabcd, + }, + BasePort: 0xfedc, + }, + empty: &NatRange2{}, + }, + { + name: "un/marshal NatRange2 IPv6 round-trip", + fam: unix.NFPROTO_IPV6, + rev: 0, + info: &NatRange2{ + NatRange: NatRange{ + Flags: 0x1234, + MinIP: net.ParseIP("fe80::dead:beef"), + MaxIP: net.ParseIP("fe80::c001:cafe"), + MinPort: 0x5678, + MaxPort: 0xabcd, + }, + BasePort: 0xfedc, + }, + empty: &NatRange2{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + data, err := tt.info.marshal(TableFamily(tt.fam), tt.rev) + if err != nil { + t.Fatalf("marshal error: %+v", err) + + } + var recoveredInfo InfoAny = tt.empty + err = recoveredInfo.unmarshal(TableFamily(tt.fam), tt.rev, data) + if err != nil { + t.Fatalf("unmarshal error: %+v", err) + } + if !reflect.DeepEqual(tt.info, recoveredInfo) { + t.Fatalf("original %+v and recovered %+v are different", tt.info, recoveredInfo) + } + }) + } +} diff --git a/xt/target_masquerade_ip.go b/xt/target_masquerade_ip.go new file mode 100644 index 0000000..2c12b70 --- /dev/null +++ b/xt/target_masquerade_ip.go @@ -0,0 +1,86 @@ +package xt + +import ( + "errors" + "net" + + "github.com/google/nftables/alignedbuff" +) + +// See https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/netfilter/nf_nat.h#L25 +type NatIPv4Range struct { + Flags uint // sic! + MinIP net.IP + MaxIP net.IP + MinPort uint16 + MaxPort uint16 +} + +// NatIPv4MultiRangeCompat despite being a slice of NAT IPv4 ranges is currently allowed to +// only hold exactly one element. +// +// See https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/netfilter/nf_nat.h#L33 +type NatIPv4MultiRangeCompat []NatIPv4Range + +func (x *NatIPv4MultiRangeCompat) marshal(fam TableFamily, rev uint32) ([]byte, error) { + ab := alignedbuff.New() + if len(*x) != 1 { + return nil, errors.New("MasqueradeIp must contain exactly one NatIPv4Range") + } + ab.PutUint(uint(len(*x))) + for _, nat := range *x { + if err := nat.marshalAB(fam, rev, &ab); err != nil { + return nil, err + } + } + return ab.Data(), nil +} + +func (x *NatIPv4MultiRangeCompat) unmarshal(fam TableFamily, rev uint32, data []byte) error { + ab := alignedbuff.NewWithData(data) + l, err := ab.Uint() + if err != nil { + return err + } + nats := make(NatIPv4MultiRangeCompat, l) + for l > 0 { + l-- + if err := nats[l].unmarshalAB(fam, rev, &ab); err != nil { + return err + } + } + *x = nats + return nil +} + +func (x *NatIPv4Range) marshalAB(fam TableFamily, rev uint32, ab *alignedbuff.AlignedBuff) error { + ab.PutUint(x.Flags) + ab.PutBytesAligned32(x.MinIP.To4(), 4) + ab.PutBytesAligned32(x.MaxIP.To4(), 4) + ab.PutUint16BE(x.MinPort) + ab.PutUint16BE(x.MaxPort) + return nil +} + +func (x *NatIPv4Range) unmarshalAB(fam TableFamily, rev uint32, ab *alignedbuff.AlignedBuff) error { + var err error + if x.Flags, err = ab.Uint(); err != nil { + return err + } + var ip []byte + if ip, err = ab.BytesAligned32(4); err != nil { + return err + } + x.MinIP = net.IP(ip) + if ip, err = ab.BytesAligned32(4); err != nil { + return err + } + x.MaxIP = net.IP(ip) + if x.MinPort, err = ab.Uint16BE(); err != nil { + return err + } + if x.MaxPort, err = ab.Uint16BE(); err != nil { + return err + } + return nil +} diff --git a/xt/target_masquerade_ip_test.go b/xt/target_masquerade_ip_test.go new file mode 100644 index 0000000..b447c32 --- /dev/null +++ b/xt/target_masquerade_ip_test.go @@ -0,0 +1,54 @@ +package xt + +import ( + "net" + "reflect" + "testing" + + "golang.org/x/sys/unix" +) + +func TestTargetMasqueradeIP(t *testing.T) { + t.Parallel() + tests := []struct { + name string + fam byte + rev uint32 + info InfoAny + empty InfoAny + }{ + { + name: "un/marshal NatIPv4Range round-trip", + fam: unix.NFPROTO_IPV4, + rev: 0, + info: &NatIPv4MultiRangeCompat{ + NatIPv4Range{ + Flags: 0x1234, + MinIP: net.ParseIP("12.23.34.45").To4(), + MaxIP: net.ParseIP("21.32.43.54").To4(), + MinPort: 0x5678, + MaxPort: 0xabcd, + }, + }, + empty: new(NatIPv4MultiRangeCompat), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + data, err := tt.info.marshal(TableFamily(tt.fam), tt.rev) + if err != nil { + t.Fatalf("marshal error: %+v", err) + + } + var recoveredInfo InfoAny = tt.empty + err = recoveredInfo.unmarshal(TableFamily(tt.fam), tt.rev, data) + if err != nil { + t.Fatalf("unmarshal error: %+v", err) + } + if !reflect.DeepEqual(tt.info, recoveredInfo) { + t.Fatalf("original %+v and recovered %+v are different", tt.info, recoveredInfo) + } + }) + } +} diff --git a/xt/unknown.go b/xt/unknown.go new file mode 100644 index 0000000..c648307 --- /dev/null +++ b/xt/unknown.go @@ -0,0 +1,17 @@ +package xt + +// Unknown represents the bytes Info payload for unknown Info types where no +// dedicated match/target info type has (yet) been defined. +type Unknown []byte + +func (x *Unknown) marshal(fam TableFamily, rev uint32) ([]byte, error) { + // In case of unknown payload we assume its creator knows what she/he does + // and thus we don't do any alignment padding. Just take the payload "as + // is". + return *x, nil +} + +func (x *Unknown) unmarshal(fam TableFamily, rev uint32, data []byte) error { + *x = data + return nil +} diff --git a/xt/unknown_test.go b/xt/unknown_test.go new file mode 100644 index 0000000..e777ae7 --- /dev/null +++ b/xt/unknown_test.go @@ -0,0 +1,38 @@ +package xt + +import ( + "reflect" + "testing" +) + +func TestUnknown(t *testing.T) { + t.Parallel() + payload := Unknown([]byte{0xb0, 0x1d, 0xca, 0xfe, 0x00}) + tests := []struct { + name string + info InfoAny + }{ + { + name: "un/marshal Unknown round-trip", + info: &payload, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + data, err := tt.info.marshal(0, 0) + if err != nil { + t.Fatalf("marshal error: %+v", err) + + } + var recoveredInfo InfoAny = &Unknown{} + err = recoveredInfo.unmarshal(0, 0, data) + if err != nil { + t.Fatalf("unmarshal error: %+v", err) + } + if !reflect.DeepEqual(tt.info, recoveredInfo) { + t.Fatalf("original %+v and recovered %+v are different", tt.info, recoveredInfo) + } + }) + } +} diff --git a/xt/util.go b/xt/util.go new file mode 100644 index 0000000..673ac54 --- /dev/null +++ b/xt/util.go @@ -0,0 +1,64 @@ +package xt + +import ( + "fmt" + "net" + + "github.com/google/nftables/alignedbuff" + "golang.org/x/sys/unix" +) + +func bool32(ab *alignedbuff.AlignedBuff) (bool, error) { + v, err := ab.Uint32() + if err != nil { + return false, err + } + if v != 0 { + return true, nil + } + return false, nil +} + +func putBool32(ab *alignedbuff.AlignedBuff, b bool) { + if b { + ab.PutUint32(1) + return + } + ab.PutUint32(0) +} + +func iPv46(ab *alignedbuff.AlignedBuff, fam TableFamily) (net.IP, error) { + ip, err := ab.BytesAligned32(16) + if err != nil { + return nil, err + } + switch fam { + case unix.NFPROTO_IPV4: + return net.IP(ip[:4]), nil + case unix.NFPROTO_IPV6: + return net.IP(ip), nil + default: + return nil, fmt.Errorf("unmarshal IP: unsupported table family %d", fam) + } +} + +func iPv46Mask(ab *alignedbuff.AlignedBuff, fam TableFamily) (net.IPMask, error) { + v, err := iPv46(ab, fam) + return net.IPMask(v), err +} + +func putIPv46(ab *alignedbuff.AlignedBuff, fam TableFamily, ip net.IP) error { + switch fam { + case unix.NFPROTO_IPV4: + ab.PutBytesAligned32(ip.To4(), 16) + case unix.NFPROTO_IPV6: + ab.PutBytesAligned32(ip.To16(), 16) + default: + return fmt.Errorf("marshal IP: unsupported table family %d", fam) + } + return nil +} + +func putIPv46Mask(ab *alignedbuff.AlignedBuff, fam TableFamily, mask net.IPMask) error { + return putIPv46(ab, fam, net.IP(mask)) +} diff --git a/xt/xt.go b/xt/xt.go new file mode 100644 index 0000000..b0d0dd7 --- /dev/null +++ b/xt/xt.go @@ -0,0 +1,50 @@ +/* + +Package xt implements dedicated types for (some) of the "Info" payload in Match +and Target expressions that bridge between the nftables and xtables worlds. + +Bridging between the more unified world of nftables and the slightly +heterogenous world of xtables comes with some caveats. Unmarshalling the +extension/translation information in Match and Target expressions requires +information about the table family the information belongs to, as well as type +and type revision information. In consequence, unmarshalling the Match and +Target Info field payloads often (but not necessarily always) require the table +family and revision information, so it gets passed to the type-specific +unmarshallers. + +To complicate things more, even marshalling requires knowledge about the +enclosing table family. The NatRange/NatRange2 types are an example, where it is +necessary to differentiate between IPv4 and IPv6 address marshalling. Due to +Go's net.IP habit to normally store IPv4 addresses as IPv4-compatible IPv6 +addresses (see also RFC 4291, section 2.5.5.1) marshalling must be handled +differently in the context of an IPv6 table compared to an IPv4 table. In an +IPv4 table, an IPv4-compatible IPv6 address must be marshalled as a 32bit +address, whereas in an IPv6 table the IPv4 address must be marshalled as an +128bit IPv4-compatible IPv6 address. Not relying on heuristics here we avoid +behavior unexpected and most probably unknown to our API users. The net.IP habit +of storing IPv4 addresses in two different storage formats is already a source +for trouble, especially when comparing net.IPs from different Go module sources. +We won't add to this confusion. (...or maybe we can, because of it?) + +An important property of all types of Info extension/translation payloads is +that their marshalling and unmarshalling doesn't follow netlink's TLV +(tag-length-value) architecture. Instead, Info payloads a basically plain binary +blobs of their respective type-specific data structures, so host +platform/architecture alignment and data type sizes apply. The alignedbuff +package implements the different required data types alignments. + +Please note that Info payloads are always padded at their end to the next uint64 +alignment. Kernel code is checking for the padded payload size and will reject +payloads not correctly padded at their ends. + +Most of the time, we find explifcitly sized (unsigned integer) data types. +However, there are notable exceptions where "unsigned int" is used: on 64bit +platforms this mostly translates into 32bit(!). This differs from Go mapping +uint to uint64 instead. This package currently clamps its mapping of C's +"unsigned int" to Go's uint32 for marshalling and unmarshalling. If in the +future 128bit platforms with a differently sized C unsigned int should come into +production, then the alignedbuff package will need to be adapted accordingly, as +it abstracts away this data type handling. + +*/ +package xt