diff --git a/expr/expr.go b/expr/expr.go index 3735fbd..5cb004e 100644 --- a/expr/expr.go +++ b/expr/expr.go @@ -17,7 +17,6 @@ package expr import ( "encoding/binary" - "fmt" "github.com/google/nftables/binaryutil" "github.com/mdlayher/netlink" @@ -135,17 +134,88 @@ func (e *Meta) unmarshal(data []byte) error { // Masq (Masquerade) is a special case of SNAT, where the source address is // automagically set to the address of the output interface. See also // https://wiki.nftables.org/wiki-nftables/index.php/Performing_Network_Address_Translation_(NAT)#Masquerading -type Masq struct{} +type Masq struct { + Random bool + FullyRandom bool + Persistent bool + ToPorts bool + RegProtoMin uint32 + RegProtoMax uint32 +} + +// TODO, Once the constants below are available in golang.org/x/sys/unix, switch to use those. +const ( + // NF_NAT_RANGE_PROTO_RANDOM defines flag for a random masquerade + NF_NAT_RANGE_PROTO_RANDOM = 0x4 + // NF_NAT_RANGE_PROTO_RANDOM_FULLY defines flag for a fully random masquerade + NF_NAT_RANGE_PROTO_RANDOM_FULLY = 0x10 + // NF_NAT_RANGE_PERSISTENT defines flag for a persistent masquerade + NF_NAT_RANGE_PERSISTENT = 0x8 +) func (e *Masq) marshal() ([]byte, error) { + msgData := []byte{} + if !e.ToPorts { + flags := uint32(0) + if e.Random { + flags |= NF_NAT_RANGE_PROTO_RANDOM + } + if e.FullyRandom { + flags |= NF_NAT_RANGE_PROTO_RANDOM_FULLY + } + if e.Persistent { + flags |= NF_NAT_RANGE_PERSISTENT + } + if flags != 0 { + flagsData, err := netlink.MarshalAttributes([]netlink.Attribute{ + {Type: unix.NFTA_MASQ_FLAGS, Data: binaryutil.BigEndian.PutUint32(flags)}}) + if err != nil { + return nil, err + } + msgData = append(msgData, flagsData...) + } + } else { + regsData, err := netlink.MarshalAttributes([]netlink.Attribute{ + {Type: unix.NFTA_MASQ_REG_PROTO_MIN, Data: binaryutil.BigEndian.PutUint32(e.RegProtoMin)}}) + if err != nil { + return nil, err + } + msgData = append(msgData, regsData...) + if e.RegProtoMax != 0 { + regsData, err := netlink.MarshalAttributes([]netlink.Attribute{ + {Type: unix.NFTA_MASQ_REG_PROTO_MAX, Data: binaryutil.BigEndian.PutUint32(e.RegProtoMax)}}) + if err != nil { + return nil, err + } + msgData = append(msgData, regsData...) + } + } return netlink.MarshalAttributes([]netlink.Attribute{ {Type: unix.NFTA_EXPR_NAME, Data: []byte("masq\x00")}, - {Type: unix.NLA_F_NESTED | unix.NFTA_EXPR_DATA, Data: nil}, + {Type: unix.NLA_F_NESTED | unix.NFTA_EXPR_DATA, Data: msgData}, }) } func (e *Masq) unmarshal(data []byte) error { - return fmt.Errorf("not yet implemented") + ad, err := netlink.NewAttributeDecoder(data) + if err != nil { + return err + } + ad.ByteOrder = binary.BigEndian + for ad.Next() { + switch ad.Type() { + case unix.NFTA_MASQ_REG_PROTO_MIN: + e.RegProtoMin = ad.Uint32() + case unix.NFTA_MASQ_REG_PROTO_MAX: + e.RegProtoMax = ad.Uint32() + case unix.NFTA_MASQ_FLAGS: + flags := ad.Uint32() + e.Persistent = (flags & NF_NAT_RANGE_PERSISTENT) != 0 + e.Random = (flags & NF_NAT_RANGE_PROTO_RANDOM) != 0 + e.FullyRandom = (flags & NF_NAT_RANGE_PROTO_RANDOM_FULLY) != 0 + } + } + return ad.Err() } // CmpOp specifies which type of comparison should be performed. diff --git a/nftables_test.go b/nftables_test.go index a5b7740..3d1d383 100644 --- a/nftables_test.go +++ b/nftables_test.go @@ -2386,3 +2386,158 @@ func TestSet4(t *testing.T) { t.Fatal(err) } } + +func TestMasq(t *testing.T) { + tests := []struct { + name string + chain *nftables.Chain + want [][]byte + masqExprs []expr.Any + }{ + { + name: "Masquerada", + chain: &nftables.Chain{ + Name: "base-chain", + }, + want: [][]byte{ + // batch begin + []byte("\x00\x00\x00\x0a"), + // nft add table ip filter + []byte("\x02\x00\x00\x00\x0b\x00\x01\x00\x66\x69\x6c\x74\x65\x72\x00\x00\x08\x00\x02\x00\x00\x00\x00\x00"), + // nft add chain ip filter base-chain + []byte("\x02\x00\x00\x00\x0b\x00\x01\x00\x66\x69\x6c\x74\x65\x72\x00\x00\x0f\x00\x03\x00\x62\x61\x73\x65\x2d\x63\x68\x61\x69\x6e\x00\x00"), + // nft add rule ip filter base-chain ip protocol tcp masquerade + []byte("\x02\x00\x00\x00\x0b\x00\x01\x00\x66\x69\x6c\x74\x65\x72\x00\x00\x0f\x00\x02\x00\x62\x61\x73\x65\x2d\x63\x68\x61\x69\x6e\x00\x00\x78\x00\x04\x80\x34\x00\x01\x80\x0c\x00\x01\x00\x70\x61\x79\x6c\x6f\x61\x64\x00\x24\x00\x02\x80\x08\x00\x01\x00\x00\x00\x00\x01\x08\x00\x02\x00\x00\x00\x00\x01\x08\x00\x03\x00\x00\x00\x00\x09\x08\x00\x04\x00\x00\x00\x00\x01\x2c\x00\x01\x80\x08\x00\x01\x00\x63\x6d\x70\x00\x20\x00\x02\x80\x08\x00\x01\x00\x00\x00\x00\x01\x08\x00\x02\x00\x00\x00\x00\x00\x0c\x00\x03\x80\x05\x00\x01\x00\x06\x00\x00\x00\x14\x00\x01\x80\x09\x00\x01\x00\x6d\x61\x73\x71\x00\x00\x00\x00\x04\x00\x02\x80"), + // batch end + []byte("\x00\x00\x00\x0a"), + }, + masqExprs: []expr.Any{ + &expr.Masq{}, + }, + }, + { + name: "Masquerada with flags", + chain: &nftables.Chain{ + Name: "base-chain", + }, + want: [][]byte{ + // batch begin + []byte("\x00\x00\x00\x0a"), + // nft add table ip filter + []byte("\x02\x00\x00\x00\x0b\x00\x01\x00\x66\x69\x6c\x74\x65\x72\x00\x00\x08\x00\x02\x00\x00\x00\x00\x00"), + // nft add chain ip filter base-chain + []byte("\x02\x00\x00\x00\x0b\x00\x01\x00\x66\x69\x6c\x74\x65\x72\x00\x00\x0f\x00\x03\x00\x62\x61\x73\x65\x2d\x63\x68\x61\x69\x6e\x00\x00"), + // nft add rule ip filter base-chain ip protocol tcp masquerade random,fully-random,persistent + []byte("\x02\x00\x00\x00\x0b\x00\x01\x00\x66\x69\x6c\x74\x65\x72\x00\x00\x0f\x00\x02\x00\x62\x61\x73\x65\x2d\x63\x68\x61\x69\x6e\x00\x00\x80\x00\x04\x80\x34\x00\x01\x80\x0c\x00\x01\x00\x70\x61\x79\x6c\x6f\x61\x64\x00\x24\x00\x02\x80\x08\x00\x01\x00\x00\x00\x00\x01\x08\x00\x02\x00\x00\x00\x00\x01\x08\x00\x03\x00\x00\x00\x00\x09\x08\x00\x04\x00\x00\x00\x00\x01\x2c\x00\x01\x80\x08\x00\x01\x00\x63\x6d\x70\x00\x20\x00\x02\x80\x08\x00\x01\x00\x00\x00\x00\x01\x08\x00\x02\x00\x00\x00\x00\x00\x0c\x00\x03\x80\x05\x00\x01\x00\x06\x00\x00\x00\x1c\x00\x01\x80\x09\x00\x01\x00\x6d\x61\x73\x71\x00\x00\x00\x00\x0c\x00\x02\x80\x08\x00\x01\x00\x00\x00\x00\x1c"), + // batch end + []byte("\x00\x00\x00\x0a"), + }, + masqExprs: []expr.Any{ + &expr.Masq{Random: true, FullyRandom: true, Persistent: true, ToPorts: false}, + }, + }, + { + name: "Masquerada with 1 port", + chain: &nftables.Chain{ + Name: "base-chain", + }, + want: [][]byte{ + // batch begin + []byte("\x00\x00\x00\x0a"), + // nft add table ip filter + []byte("\x02\x00\x00\x00\x0b\x00\x01\x00\x66\x69\x6c\x74\x65\x72\x00\x00\x08\x00\x02\x00\x00\x00\x00\x00"), + // nft add chain ip filter base-chain + []byte("\x02\x00\x00\x00\x0b\x00\x01\x00\x66\x69\x6c\x74\x65\x72\x00\x00\x0f\x00\x03\x00\x62\x61\x73\x65\x2d\x63\x68\x61\x69\x6e\x00\x00"), + // nft add rule ip filter base-chain ip protocol tcp masquerade to :1024 + []byte("\x02\x00\x00\x00\x0b\x00\x01\x00\x66\x69\x6c\x74\x65\x72\x00\x00\x0f\x00\x02\x00\x62\x61\x73\x65\x2d\x63\x68\x61\x69\x6e\x00\x00\xac\x00\x04\x80\x34\x00\x01\x80\x0c\x00\x01\x00\x70\x61\x79\x6c\x6f\x61\x64\x00\x24\x00\x02\x80\x08\x00\x01\x00\x00\x00\x00\x01\x08\x00\x02\x00\x00\x00\x00\x01\x08\x00\x03\x00\x00\x00\x00\x09\x08\x00\x04\x00\x00\x00\x00\x01\x2c\x00\x01\x80\x08\x00\x01\x00\x63\x6d\x70\x00\x20\x00\x02\x80\x08\x00\x01\x00\x00\x00\x00\x01\x08\x00\x02\x00\x00\x00\x00\x00\x0c\x00\x03\x80\x05\x00\x01\x00\x06\x00\x00\x00\x2c\x00\x01\x80\x0e\x00\x01\x00\x69\x6d\x6d\x65\x64\x69\x61\x74\x65\x00\x00\x00\x18\x00\x02\x80\x08\x00\x01\x00\x00\x00\x00\x01\x0c\x00\x02\x80\x08\x00\x01\x00\x04\x00\x00\x00\x1c\x00\x01\x80\x09\x00\x01\x00\x6d\x61\x73\x71\x00\x00\x00\x00\x0c\x00\x02\x80\x08\x00\x02\x00\x00\x00\x00\x01"), + // batch end + []byte("\x00\x00\x00\x0a"), + }, + masqExprs: []expr.Any{ + &expr.Immediate{Register: 1, Data: binaryutil.BigEndian.PutUint32(uint32(1024) << 16)}, + &expr.Masq{ToPorts: true, RegProtoMin: 1}, + }, + }, + { + name: "Masquerada with port range", + chain: &nftables.Chain{ + Name: "base-chain", + }, + want: [][]byte{ + // batch begin + []byte("\x00\x00\x00\x0a"), + // nft add table ip filter + []byte("\x02\x00\x00\x00\x0b\x00\x01\x00\x66\x69\x6c\x74\x65\x72\x00\x00\x08\x00\x02\x00\x00\x00\x00\x00"), + // nft add chain ip filter base-chain + []byte("\x02\x00\x00\x00\x0b\x00\x01\x00\x66\x69\x6c\x74\x65\x72\x00\x00\x0f\x00\x03\x00\x62\x61\x73\x65\x2d\x63\x68\x61\x69\x6e\x00\x00"), + // nft add rule ip filter base-chain ip protocol tcp masquerade to :1024-2044 + []byte("\x02\x00\x00\x00\x0b\x00\x01\x00\x66\x69\x6c\x74\x65\x72\x00\x00\x0f\x00\x02\x00\x62\x61\x73\x65\x2d\x63\x68\x61\x69\x6e\x00\x00\xe0\x00\x04\x80\x34\x00\x01\x80\x0c\x00\x01\x00\x70\x61\x79\x6c\x6f\x61\x64\x00\x24\x00\x02\x80\x08\x00\x01\x00\x00\x00\x00\x01\x08\x00\x02\x00\x00\x00\x00\x01\x08\x00\x03\x00\x00\x00\x00\x09\x08\x00\x04\x00\x00\x00\x00\x01\x2c\x00\x01\x80\x08\x00\x01\x00\x63\x6d\x70\x00\x20\x00\x02\x80\x08\x00\x01\x00\x00\x00\x00\x01\x08\x00\x02\x00\x00\x00\x00\x00\x0c\x00\x03\x80\x05\x00\x01\x00\x06\x00\x00\x00\x2c\x00\x01\x80\x0e\x00\x01\x00\x69\x6d\x6d\x65\x64\x69\x61\x74\x65\x00\x00\x00\x18\x00\x02\x80\x08\x00\x01\x00\x00\x00\x00\x01\x0c\x00\x02\x80\x08\x00\x01\x00\x04\x00\x00\x00\x2c\x00\x01\x80\x0e\x00\x01\x00\x69\x6d\x6d\x65\x64\x69\x61\x74\x65\x00\x00\x00\x18\x00\x02\x80\x08\x00\x01\x00\x00\x00\x00\x02\x0c\x00\x02\x80\x08\x00\x01\x00\x07\xfc\x00\x00\x24\x00\x01\x80\x09\x00\x01\x00\x6d\x61\x73\x71\x00\x00\x00\x00\x14\x00\x02\x80\x08\x00\x02\x00\x00\x00\x00\x01\x08\x00\x03\x00\x00\x00\x00\x02"), + // batch end + []byte("\x00\x00\x00\x0a"), + }, + masqExprs: []expr.Any{ + &expr.Immediate{Register: 1, Data: binaryutil.BigEndian.PutUint32(uint32(1024) << 16)}, + &expr.Immediate{Register: 2, Data: binaryutil.BigEndian.PutUint32(uint32(2044) << 16)}, + &expr.Masq{ToPorts: true, RegProtoMin: 1, RegProtoMax: 2}, + }, + }, + } + + for _, tt := range tests { + c := &nftables.Conn{ + TestDial: func(req []netlink.Message) ([]netlink.Message, error) { + for idx, msg := range req { + b, err := msg.MarshalBinary() + if err != nil { + t.Fatal(err) + } + if len(b) < 16 { + continue + } + b = b[16:] + if len(tt.want[idx]) == 0 { + t.Errorf("no want entry for message %d: %x", idx, b) + continue + } + got := b + if !bytes.Equal(got, tt.want[idx]) { + t.Errorf("message %d: %s", idx, linediff(nfdump(got), nfdump(tt.want[idx]))) + } + } + return req, nil + }, + } + + filter := c.AddTable(&nftables.Table{ + Family: nftables.TableFamilyIPv4, + Name: "filter", + }) + + tt.chain.Table = filter + chain := c.AddChain(tt.chain) + exprs := []expr.Any{ + // [ payload load 1b @ network header + 9 => reg 1 ] + &expr.Payload{ + DestRegister: 1, + Base: expr.PayloadBaseNetworkHeader, + Offset: 9, + Len: 1, + }, + // [ cmp eq reg 1 0x00000006 ] + &expr.Cmp{ + Op: expr.CmpOpEq, + Register: 1, + Data: []byte{unix.IPPROTO_TCP}, + }, + } + exprs = append(exprs, tt.masqExprs...) + c.AddRule(&nftables.Rule{ + Table: filter, + Chain: chain, + Exprs: exprs, + }) + if err := c.Flush(); err != nil { + t.Fatalf("Test \"%s\" failed with error: %+v", tt.name, err) + } + } +}