From 3f9890ee96a5f52b5627fd433de68ba3ec277136 Mon Sep 17 00:00:00 2001 From: wenqiq Date: Tue, 18 May 2021 20:52:25 +0800 Subject: [PATCH] Send NDP NA message upon assigning egress IP Implement Neighbor Discovery Protocol(NDP) neighbor advertisement for Egress IPv6 support.Once an IPv6 IP address has been assigned to Node, an unsolicited Neighbor Advertisement ICMPv6 multicast packet will be sent, announcing the IP to all IPv6 nodes as per RFC4861. Signed-off-by: Wenqi Qiu --- .../egress/ipassigner/ip_assigner_linux.go | 22 ++-- pkg/agent/util/ndp/doc.go | 16 +++ pkg/agent/util/ndp/ndp_linux.go | 105 ++++++++++++++++++ pkg/agent/util/ndp/ndp_linux_test.go | 75 +++++++++++++ pkg/util/ip/ip.go | 10 ++ pkg/util/ip/ip_test.go | 24 ++++ .../agent/ip_assigner_linux_test.go | 22 +++- 7 files changed, 257 insertions(+), 17 deletions(-) create mode 100644 pkg/agent/util/ndp/doc.go create mode 100644 pkg/agent/util/ndp/ndp_linux.go create mode 100644 pkg/agent/util/ndp/ndp_linux_test.go diff --git a/pkg/agent/controller/egress/ipassigner/ip_assigner_linux.go b/pkg/agent/controller/egress/ipassigner/ip_assigner_linux.go index 0870e13f1da..9ac69130861 100644 --- a/pkg/agent/controller/egress/ipassigner/ip_assigner_linux.go +++ b/pkg/agent/controller/egress/ipassigner/ip_assigner_linux.go @@ -15,7 +15,6 @@ package ipassigner import ( - "errors" "fmt" "net" "sync" @@ -26,10 +25,9 @@ import ( "antrea.io/antrea/pkg/agent/util" "antrea.io/antrea/pkg/agent/util/arping" + "antrea.io/antrea/pkg/agent/util/ndp" ) -var ipv6NotSupportErr = errors.New("IPv6 not supported") - // ipAssigner creates a dummy device and assigns IPs to it. // It's supposed to be used in the cases that external IPs should be configured on the system so that they can be used // for SNAT (egress scenario) or DNAT (ingress scenario). A dummy device is used because the IPs just need to be present @@ -107,6 +105,7 @@ func (a *ipAssigner) AssignIP(ip string) error { if parsedIP == nil { return fmt.Errorf("invalid IP %s", ip) } + addr := netlink.NewIPNet(parsedIP) if err := func() error { a.mutex.Lock() @@ -117,8 +116,7 @@ func (a *ipAssigner) AssignIP(ip string) error { return nil } - addr := netlink.Addr{IPNet: &net.IPNet{IP: parsedIP, Mask: net.CIDRMask(32, 32)}} - if err := netlink.AddrAdd(a.dummyDevice, &addr); err != nil { + if err := netlink.AddrAdd(a.dummyDevice, &netlink.Addr{IPNet: addr}); err != nil { return fmt.Errorf("failed to add IP %v to interface %s: %v", ip, a.dummyDevice.Attrs().Name, err) } klog.InfoS("Assigned IP to interface", "ip", parsedIP, "interface", a.dummyDevice.Attrs().Name) @@ -129,14 +127,16 @@ func (a *ipAssigner) AssignIP(ip string) error { return err } - isIPv4 := parsedIP.To4() - if isIPv4 != nil { - if err := arping.GratuitousARPOverIface(isIPv4, a.externalInterface); err != nil { + if addr.IP.To4() != nil { + if err := arping.GratuitousARPOverIface(addr.IP.To4(), a.externalInterface); err != nil { return fmt.Errorf("failed to send gratuitous ARP: %v", err) } klog.V(2).InfoS("Sent gratuitous ARP", "ip", parsedIP) } else { - klog.ErrorS(ipv6NotSupportErr, "Failed to send Advertisement", "ip", parsedIP) + if err := ndp.NeighborAdvertisement(addr.IP.To16(), a.externalInterface); err != nil { + return fmt.Errorf("failed to send neighbor advertisement: %v", err) + } + klog.V(2).InfoS("Sent NDP neighbor advertisement", "ip", parsedIP) } return nil } @@ -156,8 +156,8 @@ func (a *ipAssigner) UnassignIP(ip string) error { return nil } - addr := netlink.Addr{IPNet: &net.IPNet{IP: parsedIP, Mask: net.CIDRMask(32, 32)}} - if err := netlink.AddrDel(a.dummyDevice, &addr); err != nil { + addr := netlink.NewIPNet(parsedIP) + if err := netlink.AddrDel(a.dummyDevice, &netlink.Addr{IPNet: addr}); err != nil { return fmt.Errorf("failed to delete IP %v from interface %s: %v", ip, a.dummyDevice.Attrs().Name, err) } klog.InfoS("Deleted IP from interface", "ip", ip, "interface", a.dummyDevice.Attrs().Name) diff --git a/pkg/agent/util/ndp/doc.go b/pkg/agent/util/ndp/doc.go new file mode 100644 index 00000000000..96177bca93e --- /dev/null +++ b/pkg/agent/util/ndp/doc.go @@ -0,0 +1,16 @@ +// Copyright 2021 Antrea Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package ndp contains functions to send NDP advertisement on Linux. +package ndp diff --git a/pkg/agent/util/ndp/ndp_linux.go b/pkg/agent/util/ndp/ndp_linux.go new file mode 100644 index 00000000000..2c432fab53a --- /dev/null +++ b/pkg/agent/util/ndp/ndp_linux.go @@ -0,0 +1,105 @@ +// Copyright 2021 Antrea Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ndp + +import ( + "fmt" + "net" + "syscall" + + "golang.org/x/net/icmp" + "golang.org/x/net/ipv6" + utilnet "k8s.io/utils/net" +) + +const ( + // Option Length, 8-bit unsigned integer. The length of the option (including the type and length fields) in units of 8 octets. + // The value 0 is invalid. Nodes MUST silently discard a ND packet that contains an option with length zero. + // https://datatracker.ietf.org/doc/html/rfc4861 + ndpOptionLen = 1 + + // ndpOptionType + // Option Name Type + // + // Source Link-Layer Address 1 + // Target Link-Layer Address 2 + // Prefix Information 3 + // Redirected Header 4 + // MTU 5 + ndpOptionType = 2 + + // Minimum byte length values for each type of valid Message. + naLen = 20 + + // Hop limit is always 255, refer RFC 4861. + hopLimit = 255 +) + +// NeighborAdvertisement sends an unsolicited Neighbor Advertisement ICMPv6 multicast packet, +// over interface 'iface' from 'srcIP', announcing a given IPv6 address('srcIP') to all IPv6 nodes as per RFC4861. +func NeighborAdvertisement(srcIP net.IP, iface *net.Interface) error { + if !utilnet.IsIPv6(srcIP) { + return fmt.Errorf("invalid IPv6 address: %v", srcIP) + } + + mb, err := newNDPNeighborAdvertisementMessage(srcIP, iface.HardwareAddr) + if err != nil { + return fmt.Errorf("new NDP Neighbor Advertisement Message error: %v", err) + } + + sockInet6, err := syscall.Socket(syscall.AF_INET6, syscall.SOCK_RAW, syscall.IPPROTO_ICMPV6) + if err != nil { + return err + } + defer syscall.Close(sockInet6) + + syscall.SetsockoptInt(sockInet6, syscall.IPPROTO_IPV6, syscall.IPV6_MULTICAST_HOPS, hopLimit) + + var r [16]byte + copy(r[:], net.IPv6linklocalallnodes.To16()) + toSockAddrInet6 := syscall.SockaddrInet6{Addr: r} + if err := syscall.Sendto(sockInet6, mb, 0, &toSockAddrInet6); err != nil { + return err + } + return nil +} + +func newNDPNeighborAdvertisementMessage(targetAddress net.IP, hwa net.HardwareAddr) ([]byte, error) { + naMsgBytes := make([]byte, naLen) + naMsgBytes[0] |= 1 << 5 + copy(naMsgBytes[4:], targetAddress) + + if 1+1+len(hwa) != int(ndpOptionLen*8) { + return nil, fmt.Errorf("hardwareAddr length error: %s", hwa) + } + optionsBytes := make([]byte, ndpOptionLen*8) + optionsBytes[0] = ndpOptionType + optionsBytes[1] = ndpOptionLen + copy(optionsBytes[2:], hwa) + naMsgBytes = append(naMsgBytes, optionsBytes...) + + im := icmp.Message{ + // ICMPType = 136, Neighbor Advertisement + Type: ipv6.ICMPTypeNeighborAdvertisement, + // Always zero. + Code: 0, + // The ICMP checksum. Calculated by caller or OS. + Checksum: 0, + Body: &icmp.RawBody{ + Data: naMsgBytes, + }, + } + return im.Marshal(nil) +} diff --git a/pkg/agent/util/ndp/ndp_linux_test.go b/pkg/agent/util/ndp/ndp_linux_test.go new file mode 100644 index 00000000000..27011f78cc1 --- /dev/null +++ b/pkg/agent/util/ndp/ndp_linux_test.go @@ -0,0 +1,75 @@ +// Copyright 2021 Antrea Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ndp + +import ( + "net" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "antrea.io/antrea/pkg/util/ip" +) + +func TestAdvertiserMarshalMessage(t *testing.T) { + tests := []struct { + name string + ipv6Addr string + hardwareAddr net.HardwareAddr + want []byte + }{ + { + name: "NDP neighbor advertise marshalMessage", + ipv6Addr: "fe80::250:56ff:fea7:e29d", + hardwareAddr: net.HardwareAddr{0x00, 0x50, 0x56, 0xa7, 0xe2, 0x9d}, + // Neighbor Advertisement Message Format + // 0 1 2 3 + // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + // | Type | Code | Checksum | + // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + // |R|S|O| Reserved | + // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + // | | + // + + + // | | + // + Target Address + + // | | + // + + + // | | + // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + // | Options ... + // +-+-+-+-+-+-+-+-+-+-+-+- + want: []byte{ + 0x88, 0x0, 0x0, 0x0, + 0x20, 0x0, 0x0, 0x0, + 0xfe, 0x80, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, + 0x2, 0x50, 0x56, 0xff, + 0xfe, 0xa7, 0xe2, 0x9d, + 0x2, 0x1, 0x0, 0x50, + 0x56, 0xa7, 0xe2, 0x9d, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := newNDPNeighborAdvertisementMessage(ip.MustIPv6(tt.ipv6Addr), tt.hardwareAddr) + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/util/ip/ip.go b/pkg/util/ip/ip.go index ff0926eda23..c876e59e6e3 100644 --- a/pkg/util/ip/ip.go +++ b/pkg/util/ip/ip.go @@ -20,6 +20,8 @@ import ( "net" "sort" + utilnet "k8s.io/utils/net" + "antrea.io/antrea/pkg/apis/controlplane/v1beta2" ) @@ -179,3 +181,11 @@ func MustParseCIDR(cidr string) *net.IPNet { } return ipNet } + +func MustIPv6(s string) net.IP { + ip := net.ParseIP(s) + if !utilnet.IsIPv6(ip) { + panic(fmt.Errorf("invalid IPv6 address: %s", s)) + } + return ip +} diff --git a/pkg/util/ip/ip_test.go b/pkg/util/ip/ip_test.go index 67d6aecddd6..a5d7ecd07cd 100644 --- a/pkg/util/ip/ip_test.go +++ b/pkg/util/ip/ip_test.go @@ -162,3 +162,27 @@ func TestIPProtocolNumberToString(t *testing.T) { assert.Equal(t, "IPv6-ICMP", IPProtocolNumberToString(ICMPv6Protocol, defaultValue)) assert.Equal(t, defaultValue, IPProtocolNumberToString(44, defaultValue)) } + +func TestMustIPv6(t *testing.T) { + tests := []struct { + name string + ipv6Str string + wantIP net.IP + }{ + { + name: "valid IPv6 local", + ipv6Str: "::1", + wantIP: net.IP{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x1}, + }, + { + name: "valid IPv6", + ipv6Str: "2021:4860:0000:2001:0000:0000:0000:0068", + wantIP: net.IP{0x20, 0x21, 0x48, 0x60, 0, 0, 0x20, 0x01, 0, 0, 0, 0, 0, 0, 0x00, 0x68}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.wantIP, MustIPv6(tt.ipv6Str)) + }) + } +} diff --git a/test/integration/agent/ip_assigner_linux_test.go b/test/integration/agent/ip_assigner_linux_test.go index cbf7a6563be..de785a256e8 100644 --- a/test/integration/agent/ip_assigner_linux_test.go +++ b/test/integration/agent/ip_assigner_linux_test.go @@ -15,7 +15,8 @@ package agent import ( - "net" + "fmt" + "os/exec" "testing" "github.com/stretchr/testify/assert" @@ -29,7 +30,10 @@ import ( const dummyDeviceName = "antrea-dummy0" func TestIPAssigner(t *testing.T) { - ipAssigner, err := ipassigner.NewIPAssigner(net.ParseIP("127.0.0.1"), dummyDeviceName) + nodeIPAddr := nodeIP.IP + require.NotNil(t, nodeIPAddr, "Get Node IP failed") + + ipAssigner, err := ipassigner.NewIPAssigner(nodeIPAddr, dummyDeviceName) require.NoError(t, err, "Initializing IP assigner failed") dummyDevice, err := netlink.LinkByName(dummyDeviceName) @@ -41,11 +45,17 @@ func TestIPAssigner(t *testing.T) { ip1 := "10.10.10.10" ip2 := "10.10.10.11" - desiredIPs := sets.NewString(ip1, ip2) + ip3 := "2021:124:6020:1006:250:56ff:fea7:36c2" + desiredIPs := sets.NewString(ip1, ip2, ip3) for ip := range desiredIPs { - err = ipAssigner.AssignIP(ip) - assert.NoError(t, err, "Failed to assign a valid IP") + errAssign := ipAssigner.AssignIP(ip) + cmd := exec.Command("ip", "addr") + out, err := cmd.CombinedOutput() + if err != nil { + t.Logf("List ip addr error: %v", err) + } + assert.NoError(t, errAssign, fmt.Sprintf("Failed to assign a valid IP, ip addrs: %s", string(out))) } assert.Equal(t, desiredIPs, ipAssigner.AssignedIPs(), "Assigned IPs don't match") @@ -55,7 +65,7 @@ func TestIPAssigner(t *testing.T) { assert.Equal(t, desiredIPs, actualIPs, "Actual IPs don't match") // NewIPAssigner should load existing IPs correctly. - newIPAssigner, err := ipassigner.NewIPAssigner(net.ParseIP("127.0.0.1"), dummyDeviceName) + newIPAssigner, err := ipassigner.NewIPAssigner(nodeIPAddr, dummyDeviceName) require.NoError(t, err, "Initializing new IP assigner failed") assert.Equal(t, desiredIPs, newIPAssigner.AssignedIPs(), "Assigned IPs don't match")