diff --git a/nftables_test.go b/nftables_test.go index 160d12d..f972188 100644 --- a/nftables_test.go +++ b/nftables_test.go @@ -253,6 +253,8 @@ func TestConfigureNAT(t *testing.T) { []byte("\x02\x00\x00\x00\x08\x00\x01\x00\x6e\x61\x74\x00\x0f\x00\x02\x00\x70\x72\x65\x72\x6f\x75\x74\x69\x6e\x67\x00\x00\x98\x01\x04\x80\x24\x00\x01\x80\x09\x00\x01\x00\x6d\x65\x74\x61\x00\x00\x00\x00\x14\x00\x02\x80\x08\x00\x02\x00\x00\x00\x00\x06\x08\x00\x01\x00\x00\x00\x00\x01\x38\x00\x01\x80\x08\x00\x01\x00\x63\x6d\x70\x00\x2c\x00\x02\x80\x08\x00\x01\x00\x00\x00\x00\x01\x08\x00\x02\x00\x00\x00\x00\x00\x18\x00\x03\x80\x14\x00\x01\x00\x75\x70\x6c\x69\x6e\x6b\x30\x00\x00\x00\x00\x00\x00\x00\x00\x00\x24\x00\x01\x80\x09\x00\x01\x00\x6d\x65\x74\x61\x00\x00\x00\x00\x14\x00\x02\x80\x08\x00\x02\x00\x00\x00\x00\x10\x08\x00\x01\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\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\x02\x08\x00\x03\x00\x00\x00\x00\x02\x08\x00\x04\x00\x00\x00\x00\x02\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\x06\x00\x01\x00\x0f\xe6\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\xc0\xa8\x17\x02\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\x06\x00\x01\x00\x0f\xf0\x00\x00\x30\x00\x01\x80\x08\x00\x01\x00\x6e\x61\x74\x00\x24\x00\x02\x80\x08\x00\x01\x00\x00\x00\x00\x01\x08\x00\x02\x00\x00\x00\x00\x02\x08\x00\x03\x00\x00\x00\x00\x01\x08\x00\x05\x00\x00\x00\x00\x02"), // nft add rule nat prerouting iifname uplink0 udp dport 4070-4090 dnat 192.168.23.2:4070-4090 []byte("\x02\x00\x00\x00\x08\x00\x01\x00\x6e\x61\x74\x00\x0f\x00\x02\x00\x70\x72\x65\x72\x6f\x75\x74\x69\x6e\x67\x00\x00\xf8\x01\x04\x80\x24\x00\x01\x80\x09\x00\x01\x00\x6d\x65\x74\x61\x00\x00\x00\x00\x14\x00\x02\x80\x08\x00\x02\x00\x00\x00\x00\x06\x08\x00\x01\x00\x00\x00\x00\x01\x38\x00\x01\x80\x08\x00\x01\x00\x63\x6d\x70\x00\x2c\x00\x02\x80\x08\x00\x01\x00\x00\x00\x00\x01\x08\x00\x02\x00\x00\x00\x00\x00\x18\x00\x03\x80\x14\x00\x01\x00\x75\x70\x6c\x69\x6e\x6b\x30\x00\x00\x00\x00\x00\x00\x00\x00\x00\x24\x00\x01\x80\x09\x00\x01\x00\x6d\x65\x74\x61\x00\x00\x00\x00\x14\x00\x02\x80\x08\x00\x02\x00\x00\x00\x00\x10\x08\x00\x01\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\x11\x00\x00\x00\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\x02\x08\x00\x03\x00\x00\x00\x00\x02\x08\x00\x04\x00\x00\x00\x00\x02\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\x05\x0c\x00\x03\x80\x06\x00\x01\x00\x0f\xe6\x00\x00\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\x03\x0c\x00\x03\x80\x06\x00\x01\x00\x0f\xfa\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\xc0\xa8\x17\x02\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\x06\x00\x01\x00\x0f\xe6\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\x03\x0c\x00\x02\x80\x06\x00\x01\x00\x0f\xfa\x00\x00\x38\x00\x01\x80\x08\x00\x01\x00\x6e\x61\x74\x00\x2c\x00\x02\x80\x08\x00\x01\x00\x00\x00\x00\x01\x08\x00\x02\x00\x00\x00\x00\x02\x08\x00\x03\x00\x00\x00\x00\x01\x08\x00\x05\x00\x00\x00\x00\x02\x08\x00\x06\x00\x00\x00\x00\x03"), + // nft add rule nat prerouting ip daddr 10.0.0.0/24 dnat prefix to 20.0.0.0/24 + []byte("\x02\x00\x00\x00\x08\x00\x01\x00\x6e\x61\x74\x00\x0f\x00\x02\x00\x70\x72\x65\x72\x6f\x75\x74\x69\x6e\x67\x00\x00\x38\x01\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\x10\x08\x00\x04\x00\x00\x00\x00\x04\x44\x00\x01\x80\x0c\x00\x01\x00\x62\x69\x74\x77\x69\x73\x65\x00\x34\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\x04\x0c\x00\x04\x80\x08\x00\x01\x00\xff\xff\xff\x00\x0c\x00\x05\x80\x08\x00\x01\x00\x00\x00\x00\x00\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\x08\x00\x01\x00\x0a\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\x14\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\x14\x00\x00\xff\x38\x00\x01\x80\x08\x00\x01\x00\x6e\x61\x74\x00\x2c\x00\x02\x80\x08\x00\x01\x00\x00\x00\x00\x01\x08\x00\x02\x00\x00\x00\x00\x02\x08\x00\x03\x00\x00\x00\x00\x01\x08\x00\x04\x00\x00\x00\x00\x02\x08\x00\x07\x00\x00\x00\x00\x40"), // batch end []byte("\x00\x00\x00\x0a"), } @@ -447,6 +449,58 @@ func TestConfigureNAT(t *testing.T) { }, }) + dstipmatch, dstcidrmatch, err := net.ParseCIDR("10.0.0.0/24") + if err != nil { + t.Fatal(err) + } + + dnatfirstip, dnatlastip, err := nftables.NetFirstAndLastIP("20.0.0.0/24") + if err != nil { + t.Fatal(err) + } + + c.AddRule(&nftables.Rule{ + Table: nat, + Chain: prerouting, + Exprs: []expr.Any{ + &expr.Payload{ + DestRegister: 1, + Base: expr.PayloadBaseNetworkHeader, + Offset: 16, // destination addr offset + Len: 4, + }, + &expr.Bitwise{ + SourceRegister: 1, + DestRegister: 1, + Len: 4, + // By specifying Xor to 0x0,0x0,0x0,0x0 and Mask to the CIDR mask, + // the rule will match the CIDR of the IP (e.g in this case 10.0.0.0/24). + Xor: []byte{0x0, 0x0, 0x0, 0x0}, + Mask: dstcidrmatch.Mask, + }, + &expr.Cmp{ + Op: expr.CmpOpEq, + Register: 1, + Data: dstipmatch.To4(), + }, + &expr.Immediate{ + Register: 1, + Data: dnatfirstip, + }, + &expr.Immediate{ + Register: 2, + Data: dnatlastip, + }, + &expr.NAT{ + Type: expr.NATTypeDestNAT, + RegAddrMin: 1, + RegAddrMax: 2, + Prefix: true, + Family: uint32(nftables.TableFamilyIPv4), + }, + }, + }) + if err := c.Flush(); err != nil { t.Fatal(err) } diff --git a/util.go b/util.go index b0576e7..4f03dc0 100644 --- a/util.go +++ b/util.go @@ -16,6 +16,7 @@ package nftables import ( "encoding/binary" + "net" "github.com/google/nftables/binaryutil" "golang.org/x/sys/unix" @@ -44,3 +45,45 @@ func (genmsg *NFGenMsg) Decode(b []byte) { genmsg.Version = b[1] genmsg.ResourceID = binary.BigEndian.Uint16(b[2:]) } + +// NetFirstAndLastIP takes the beginning address of an entire network in CIDR +// notation (e.g. 192.168.1.0/24) and returns the first and last IP addresses +// within the network (e.g. first 192.168.1.0, last 192.168.1.255). +// +// Note that these are the first and last IP addresses, not the first and last +// *usable* IP addresses (which would be 192.168.1.1 and 192.168.1.254, +// respectively, for 192.168.1.0/24). +func NetFirstAndLastIP(networkCIDR string) (first, last net.IP, err error) { + _, subnet, err := net.ParseCIDR(networkCIDR) + if err != nil { + return nil, nil, err + } + + first = make(net.IP, len(subnet.IP)) + last = make(net.IP, len(subnet.IP)) + + switch len(subnet.IP) { + case net.IPv4len: + mask := binary.BigEndian.Uint32(subnet.Mask) + ip := binary.BigEndian.Uint32(subnet.IP) + // To achieve the first IP address, we need to AND the IP with the mask. + // The AND operation will set all bits in the host part to 0. + binary.BigEndian.PutUint32(first, ip&mask) + // To achieve the last IP address, we need to OR the IP network with the inverted mask. + // The AND between the IP and the mask will set all bits in the host part to 0, keeping the network part. + // The XOR between the mask and 0xffffffff will set all bits in the host part to 1, and the network part to 0. + // The OR operation will keep the host part unchanged, and sets the host part to all 1. + binary.BigEndian.PutUint32(last, (ip&mask)|(mask^0xffffffff)) + case net.IPv6len: + mask1 := binary.BigEndian.Uint64(subnet.Mask[:8]) + mask2 := binary.BigEndian.Uint64(subnet.Mask[8:]) + ip1 := binary.BigEndian.Uint64(subnet.IP[:8]) + ip2 := binary.BigEndian.Uint64(subnet.IP[8:]) + binary.BigEndian.PutUint64(first[:8], ip1&mask1) + binary.BigEndian.PutUint64(first[8:], ip2&mask2) + binary.BigEndian.PutUint64(last[:8], (ip1&mask1)|(mask1^0xffffffffffffffff)) + binary.BigEndian.PutUint64(last[8:], (ip2&mask2)|(mask2^0xffffffffffffffff)) + } + + return first, last, nil +} diff --git a/util_test.go b/util_test.go new file mode 100644 index 0000000..9deee50 --- /dev/null +++ b/util_test.go @@ -0,0 +1,78 @@ +package nftables + +import ( + "net" + "reflect" + "testing" +) + +func TestNetFirstAndLastIP(t *testing.T) { + type args struct { + cidr string + } + tests := []struct { + name string + args args + wantFirstIP net.IP + wantLastIP net.IP + wantErr bool + }{ + { + name: "Test Fake", + args: args{cidr: "fakecidr"}, + wantFirstIP: nil, + wantLastIP: nil, + wantErr: true, + }, + { + name: "Test IPV4 1", + args: args{cidr: "10.0.0.0/24"}, + wantFirstIP: net.IP{10, 0, 0, 0}, + wantLastIP: net.IP{10, 0, 0, 255}, + wantErr: false, + }, + { + name: "Test IPV4 2", + args: args{cidr: "10.0.0.20/24"}, + wantFirstIP: net.IP{10, 0, 0, 0}, + wantLastIP: net.IP{10, 0, 0, 255}, + wantErr: false, + }, + { + name: "Test IPV4 2", + args: args{cidr: "10.0.0.0/19"}, + wantFirstIP: net.IP{10, 0, 0, 0}, + wantLastIP: net.IP{10, 0, 31, 255}, + wantErr: false, + }, + { + name: "Test IPV6 1", + args: args{cidr: "ff00::/16"}, + wantFirstIP: net.ParseIP("ff00::"), + wantLastIP: net.ParseIP("ff00:ffff:ffff:ffff:ffff:ffff:ffff:ffff"), + wantErr: false, + }, + { + name: "Test IPV6 2", + args: args{cidr: "2001:db8::/62"}, + wantFirstIP: net.ParseIP("2001:db8::"), + wantLastIP: net.ParseIP("2001:db8:0000:0003:ffff:ffff:ffff:ffff"), + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotFirstIP, gotLastIP, err := NetFirstAndLastIP(tt.args.cidr) + if (err != nil) != tt.wantErr { + t.Errorf("GetFirstAndLastIPFromCIDR() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(gotFirstIP, tt.wantFirstIP) { + t.Errorf("GetFirstAndLastIPFromCIDR() gotFirstIP = %v, want %v", gotFirstIP, tt.wantFirstIP) + } + if !reflect.DeepEqual(gotLastIP, tt.wantLastIP) { + t.Errorf("GetFirstAndLastIPFromCIDR() gotLastIP = %v, want %v", gotLastIP, tt.wantLastIP) + } + }) + } +}