Skip to content

Commit

Permalink
Infer subnet for node /128 IPv6 addresses
Browse files Browse the repository at this point in the history
When node addresses are allocated through DHCPv6 IA_NA, they might get
a /128 prefix address. These node addresses are set on the gateway
router port as well. OVN routers infers on-link routes from subnet
information on the address itself. Since /128 addresses are not on a
subnet, cluster traffic is routed through the node gatway instead of
directly.

Note that OVN does not support learning routes from ICMPv6 route
advertisement and does not support adding on-link static routes.

This patch infers the subnet of the interface looking at the node
routing information and replaces the /128 prefix with the subnet
prefix.

Signed-off-by: Jaime Caamaño Ruiz <jcaamano@redhat.com>
  • Loading branch information
jcaamano committed Jul 14, 2021
1 parent 279a237 commit 46b3780
Show file tree
Hide file tree
Showing 3 changed files with 152 additions and 1 deletion.
8 changes: 7 additions & 1 deletion go-controller/pkg/node/gateway_init.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,13 @@ func getNetworkInterfaceIPAddresses(iface string) ([]*net.IPNet, error) {
for _, ip := range allIPs {
if utilnet.IsIPv6CIDR(ip) {
if config.IPv6Mode && !foundIPv6 {
ips = append(ips, ip)
// For IPv6 addresses with 128 prefix, let's try to find an appropriate subnet
// in the routing table
subnetIP, err := util.GetIPv6OnSubnet(iface, ip)
if err != nil {
return nil, fmt.Errorf("could not find IPv6 address on subnet: %v", err)
}
ips = append(ips, subnetIP)
foundIPv6 = true
}
} else if config.IPv4Mode && !foundIPv4 {
Expand Down
33 changes: 33 additions & 0 deletions go-controller/pkg/util/net_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -378,3 +378,36 @@ func GetNetworkInterfaceIPs(iface string) ([]*net.IPNet, error) {
}
return ips, nil
}

// GetIPv6OnSubnet when given an IPv6 address with a 128 prefix for an interface,
// looks for possible broadest subnet on-link routes and returns the same address
// with the found subnet prefix. Otherwise it returns the provided address unchanged.
func GetIPv6OnSubnet(iface string, ip *net.IPNet) (*net.IPNet, error) {
if s, _ := ip.Mask.Size(); s != 128 {
return ip, nil
}

link, err := netLinkOps.LinkByName(iface)
if err != nil {
return nil, fmt.Errorf("failed to lookup link %s: %v", iface, err)
}

routeFilter := &netlink.Route{
LinkIndex: link.Attrs().Index,
Scope: netlink.SCOPE_LINK,
}
filterMask := netlink.RT_FILTER_SCOPE | netlink.RT_FILTER_OIF
routes, err := netLinkOps.RouteListFiltered(getFamily(ip.IP), routeFilter, filterMask)
if err != nil {
return nil, fmt.Errorf("failed to get on-link routes for ip %s and iface %s", ip.String(), iface)
}

dst := *ip
for _, route := range routes {
if route.Dst.Contains(dst.IP) && !dst.Contains(route.Dst.IP) {
dst.Mask = route.Dst.Mask
}
}

return &dst, err
}
112 changes: 112 additions & 0 deletions go-controller/pkg/util/net_linux_unit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -858,3 +858,115 @@ func TestDeleteConntrack(t *testing.T) {
})
}
}

func TestGetIPv6OnSubnet(t *testing.T) {
mockNetLinkOps := new(mocks.NetLinkOps)
mockLink := new(netlink_mocks.Link)
// below is defined in net_linux.go
netLinkOps = mockNetLinkOps

tests := []struct {
desc string
errExp bool
inputIface string
inputIP *net.IPNet
expectedIP *net.IPNet
onRetArgsNetLinkLibOpers []ovntest.TestifyMockHelper
onRetArgsLinkIfaceOpers []ovntest.TestifyMockHelper
}{
{
desc: "tests code path when LinkByName() returns error",
inputIface: "testIfaceName",
inputIP: ovntest.MustParseIPNet("2620:52:0:11c::20/128"),
expectedIP: nil,
errExp: true,
onRetArgsNetLinkLibOpers: []ovntest.TestifyMockHelper{
{OnCallMethodName: "LinkByName", OnCallMethodArgType: []string{"string"}, RetArgList: []interface{}{nil, fmt.Errorf("mock error")}},
},
},
{
desc: "tests code path when RouteListFiltered() returns error",
inputIface: "testIfaceName",
inputIP: ovntest.MustParseIPNet("2620:52:0:11c::20/128"),
expectedIP: nil,
errExp: true,
onRetArgsNetLinkLibOpers: []ovntest.TestifyMockHelper{
{OnCallMethodName: "LinkByName", OnCallMethodArgType: []string{"string"}, RetArgList: []interface{}{mockLink, nil}},
{OnCallMethodName: "RouteListFiltered", OnCallMethodArgType: []string{"int", "*netlink.Route", "uint64"}, RetArgList: []interface{}{nil, fmt.Errorf("mock error")}},
},
onRetArgsLinkIfaceOpers: []ovntest.TestifyMockHelper{
{OnCallMethodName: "Attrs", OnCallMethodArgType: []string{}, RetArgList: []interface{}{&netlink.LinkAttrs{Name: "testIfaceName", Index: 1}}},
},
},
{
desc: "provided address does not have 128 prefix",
inputIface: "testIfaceName",
inputIP: ovntest.MustParseIPNet("2620:52:0:11c::20/64"),
expectedIP: ovntest.MustParseIPNet("2620:52:0:11c::20/64"),
errExp: false,
onRetArgsNetLinkLibOpers: []ovntest.TestifyMockHelper{},
},
{
desc: "provided address is not ipv6",
inputIface: "testIfaceName",
inputIP: ovntest.MustParseIPNet("192.169.1.12/32"),
expectedIP: ovntest.MustParseIPNet("192.169.1.12/32"),
errExp: false,
onRetArgsNetLinkLibOpers: []ovntest.TestifyMockHelper{},
},
{
desc: "returns address on broadest subnet found",
inputIface: "testIfaceName",
inputIP: ovntest.MustParseIPNet("2620:52:0:11c::20/128"),
expectedIP: ovntest.MustParseIPNet("2620:52:0:11c::20/32"),
errExp: false,
onRetArgsNetLinkLibOpers: []ovntest.TestifyMockHelper{
{OnCallMethodName: "LinkByName", OnCallMethodArgType: []string{"string"}, RetArgList: []interface{}{mockLink, nil}},
{OnCallMethodName: "RouteListFiltered", OnCallMethodArgType: []string{"int", "*netlink.Route", "uint64"}, RetArgList: []interface{}{[]netlink.Route{
{Dst: ovntest.MustParseIPNet("2620:52:0:11c::/64")},
{Dst: ovntest.MustParseIPNet("2620:52::/32")},
}, nil}},
},
onRetArgsLinkIfaceOpers: []ovntest.TestifyMockHelper{
{OnCallMethodName: "Attrs", OnCallMethodArgType: []string{}, RetArgList: []interface{}{&netlink.LinkAttrs{Name: "testIfaceName", Index: 1}}},
},
},
{
desc: "returns address on broadest subnet found irrespective of order",
inputIface: "testIfaceName",
inputIP: ovntest.MustParseIPNet("2620:52:0:11c::20/128"),
expectedIP: ovntest.MustParseIPNet("2620:52:0:11c::20/32"),
errExp: false,
onRetArgsNetLinkLibOpers: []ovntest.TestifyMockHelper{
{OnCallMethodName: "LinkByName", OnCallMethodArgType: []string{"string"}, RetArgList: []interface{}{mockLink, nil}},
{OnCallMethodName: "RouteListFiltered", OnCallMethodArgType: []string{"int", "*netlink.Route", "uint64"}, RetArgList: []interface{}{[]netlink.Route{
{Dst: ovntest.MustParseIPNet("2620:52::/32")},
{Dst: ovntest.MustParseIPNet("2620:52:0:11c::/64")},
}, nil}},
},
onRetArgsLinkIfaceOpers: []ovntest.TestifyMockHelper{
{OnCallMethodName: "Attrs", OnCallMethodArgType: []string{}, RetArgList: []interface{}{&netlink.LinkAttrs{Name: "testIfaceName", Index: 1}}},
},
},
}
for i, tc := range tests {
t.Run(fmt.Sprintf("%d:%s", i, tc.desc), func(t *testing.T) {

ovntest.ProcessMockFnList(&mockNetLinkOps.Mock, tc.onRetArgsNetLinkLibOpers)
ovntest.ProcessMockFnList(&mockLink.Mock, tc.onRetArgsLinkIfaceOpers)

ip, err := GetIPv6OnSubnet(tc.inputIface, tc.inputIP)
t.Log(ip, err)
if tc.errExp {
assert.Error(t, err)
} else {
assert.Nil(t, err)
}
if tc.expectedIP != nil {
assert.Equal(t, ip, tc.expectedIP)
}
mockNetLinkOps.AssertExpectations(t)
mockLink.AssertExpectations(t)
})
}
}

0 comments on commit 46b3780

Please sign in to comment.