Skip to content

Commit

Permalink
Send NDP NA message upon assigning egress IP
Browse files Browse the repository at this point in the history
Implement Neighbor Discovery Protocol(NDP) neighbor advertisement
for Egress IPv6 support, once an IPv6 IP address has 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 <[email protected]>
  • Loading branch information
wenqiq committed Aug 23, 2021
1 parent 4077f80 commit e3d1081
Show file tree
Hide file tree
Showing 7 changed files with 257 additions and 17 deletions.
22 changes: 11 additions & 11 deletions pkg/agent/controller/egress/ipassigner/ip_assigner_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
package ipassigner

import (
"errors"
"fmt"
"net"
"sync"
Expand All @@ -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
Expand Down Expand Up @@ -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()
Expand All @@ -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)
Expand All @@ -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
}
Expand All @@ -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)
Expand Down
16 changes: 16 additions & 0 deletions pkg/agent/util/ndp/doc.go
Original file line number Diff line number Diff line change
@@ -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
105 changes: 105 additions & 0 deletions pkg/agent/util/ndp/ndp_linux.go
Original file line number Diff line number Diff line change
@@ -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)
}
75 changes: 75 additions & 0 deletions pkg/agent/util/ndp/ndp_linux_test.go
Original file line number Diff line number Diff line change
@@ -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)
})
}
}
10 changes: 10 additions & 0 deletions pkg/util/ip/ip.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import (
"net"
"sort"

utilnet "k8s.io/utils/net"

"antrea.io/antrea/pkg/apis/controlplane/v1beta2"
)

Expand Down Expand Up @@ -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
}
24 changes: 24 additions & 0 deletions pkg/util/ip/ip_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
})
}
}
22 changes: 16 additions & 6 deletions test/integration/agent/ip_assigner_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
package agent

import (
"net"
"fmt"
"os/exec"
"testing"

"github.com/stretchr/testify/assert"
Expand All @@ -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)
Expand All @@ -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")
Expand All @@ -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")

Expand Down

0 comments on commit e3d1081

Please sign in to comment.