go-network-configurator/configurator_linux_test.go

418 lines
14 KiB
Go

package netconfig
import (
"context"
"net"
"os"
"path/filepath"
"runtime"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/vishvananda/netlink"
"github.com/vishvananda/netns"
"golang.org/x/sys/unix"
)
// Function to setup a test namespace for test network interfaces.
func setupNetlinkTest(t testing.TB) func() {
t.Helper()
if os.Getuid() != 0 {
t.Skip("Test requires root privileges.")
}
runtime.LockOSThread()
ns, err := netns.New()
require.NoError(t, err, "Failed to create new netns")
link, err := netlink.LinkByName("lo")
require.NoError(t, err, "Failed to find \"lo\" in new netns")
err = netlink.LinkSetUp(link)
require.NoError(t, err, "Failed to bring up \"lo\" in new netns")
return func() {
ns.Close()
runtime.UnlockOSThread()
}
}
// isPrimary reports whether the given IP is currently the primary address of
// its subnet on the link, i.e. the kernel has not flagged it IFA_F_SECONDARY.
// It also confirms the address is present at all.
func isPrimary(t testing.TB, h *netlink.Handle, link netlink.Link, ip net.IP) (present, primary bool) {
t.Helper()
addrs, err := h.AddrList(link, netlink.FAMILY_ALL)
require.NoError(t, err, "AddrList")
for _, a := range addrs {
if a.IPNet.IP.Equal(ip) {
return true, a.Flags&unix.IFA_F_SECONDARY == 0
}
}
return false, false
}
// TestLinuxSetPrimaryAddress exercises the netlink reordering performed by
// SetPrimaryAddress: promoting a secondary same-subnet address to primary,
// idempotency when already primary, and the error when the address is absent.
func TestLinuxSetPrimaryAddress(t *testing.T) {
tearDown := setupNetlinkTest(t)
defer tearDown()
h, err := netlink.NewHandle()
require.NoError(t, err)
defer h.Close()
// Setup test link.
name := "test_enp1s0"
link := netlink.Link(&netlink.Dummy{LinkAttrs: netlink.LinkAttrs{
Name: name,
HardwareAddr: net.HardwareAddr{0x52, 0x54, 0x00, 0x8b, 0x0d, 0x93},
}})
require.NoError(t, h.LinkAdd(link))
require.NoError(t, h.LinkSetUp(link))
// Add two addresses in the same subnet. The first added is the primary.
primaryIP := &net.IPNet{IP: net.ParseIP("1.2.3.4"), Mask: net.CIDRMask(24, 32)}
secondaryIP := &net.IPNet{IP: net.ParseIP("1.2.3.5"), Mask: net.CIDRMask(24, 32)}
require.NoError(t, h.AddrAdd(link, &netlink.Addr{IPNet: primaryIP}))
require.NoError(t, h.AddrAdd(link, &netlink.Addr{IPNet: secondaryIP}))
// Add a default route via the subnet so connectivity verification has a
// gateway to work with.
gw := net.ParseIP("1.2.3.1")
require.NoError(t, h.RouteAdd(&netlink.Route{
Family: netlink.FAMILY_V4,
LinkIndex: link.Attrs().Index,
Dst: &net.IPNet{IP: net.IPv4zero, Mask: net.CIDRMask(0, 32)},
Gw: gw,
}))
// Sanity check the initial primary/secondary assignment.
_, primary := isPrimary(t, h, link, primaryIP.IP)
require.True(t, primary, "expected 1.2.3.4 to start as primary")
_, primary = isPrimary(t, h, link, secondaryIP.IP)
require.False(t, primary, "expected 1.2.3.5 to start as secondary")
// Configurator with the success sentinel and a capturing backend.
backend := &fakeIfaceBackend{}
c := &linuxConfigurator{configOptions: &configOptions{testAddress: "test_success"}}
c.ifaceBackends = append(c.ifaceBackends, namedIfaceBackend{"fake", backend})
// Promote the secondary to primary.
require.NoError(t, c.SetPrimaryAddress(context.Background(), name, secondaryIP), "SetPrimaryAddress")
// The kernel should now treat 1.2.3.5 as primary and 1.2.3.4 as secondary,
// with both addresses still present and the default route intact.
present, primary := isPrimary(t, h, link, secondaryIP.IP)
assert.True(t, present && primary, "after promotion 1.2.3.5: present=%v primary=%v, want both true", present, primary)
present, primary = isPrimary(t, h, link, primaryIP.IP)
assert.True(t, present && !primary, "after promotion 1.2.3.4: present=%v primary=%v, want present true primary false", present, primary)
routes, err := h.RouteList(link, netlink.FAMILY_V4)
require.NoError(t, err)
haveDefault := false
for _, r := range routes {
if ones, _ := r.Dst.Mask.Size(); ones == 0 && r.Gw.Equal(gw) {
haveDefault = true
}
}
assert.True(t, haveDefault, "default route missing after promotion")
// The persisted config must receive the reordered list, primary first.
assert.True(t, len(backend.lastAddrs) != 0 && backend.lastAddrs[0].IP.Equal(secondaryIP.IP), "backend primary = %v, want 1.2.3.5 first", backend.lastAddrs)
// Promoting the already-primary address is a no-op that still succeeds.
assert.NoError(t, c.SetPrimaryAddress(context.Background(), name, secondaryIP), "SetPrimaryAddress (idempotent)")
_, primary = isPrimary(t, h, link, secondaryIP.IP)
assert.True(t, primary, "1.2.3.5 should remain primary after idempotent call")
// Promoting an address not on the interface must error before any change.
absent := &net.IPNet{IP: net.ParseIP("1.2.3.99"), Mask: net.CIDRMask(24, 32)}
err = c.SetPrimaryAddress(context.Background(), name, absent)
assert.EqualError(t, err, "address not found on interface")
}
// TestLinuxRemovePrimaryRefused verifies that RemoveAddress refuses to remove
// the primary address of its family by default, and that WithAllowPrimaryRemoval
// overrides the refusal.
func TestLinuxRemovePrimaryRefused(t *testing.T) {
tearDown := setupNetlinkTest(t)
defer tearDown()
h, err := netlink.NewHandle()
require.NoError(t, err)
defer h.Close()
name := "test_enp1s0"
link := netlink.Link(&netlink.Dummy{LinkAttrs: netlink.LinkAttrs{Name: name}})
require.NoError(t, h.LinkAdd(link))
require.NoError(t, h.LinkSetUp(link))
primaryIP := &net.IPNet{IP: net.ParseIP("1.2.3.4"), Mask: net.CIDRMask(24, 32)}
require.NoError(t, h.AddrAdd(link, &netlink.Addr{IPNet: primaryIP}))
c := &linuxConfigurator{configOptions: &configOptions{testAddress: "test_success"}}
c.ifaceBackends = append(c.ifaceBackends, namedIfaceBackend{"fake", &fakeIfaceBackend{}})
// Default: removing the primary is refused.
err = c.RemoveAddress(context.Background(), name, primaryIP)
assert.ErrorContains(t, err, "refusing to remove primary address")
// With the override enabled, removal succeeds.
c.allowPrimaryRemoval = true
assert.NoError(t, c.RemoveAddress(context.Background(), name, primaryIP), "RemoveAddress with allowPrimaryRemoval")
}
func TestLinuxConfigurator(t *testing.T) {
tearDown := setupNetlinkTest(t)
defer tearDown()
// Connect to netlink.
h, err := netlink.NewHandle()
require.NoError(t, err)
defer h.Close()
// Setup test link.
eth0Name := "test_enp1s0"
eth0Link := netlink.Link(&netlink.Dummy{LinkAttrs: netlink.LinkAttrs{
Name: eth0Name,
HardwareAddr: net.HardwareAddr{0x52, 0x54, 0x00, 0x8b, 0x0d, 0x93},
}})
err = h.LinkAdd(eth0Link)
require.NoError(t, err)
// Set test link up.
err = h.LinkSetUp(eth0Link)
require.NoError(t, err)
// Add test IP.
testIP1 := &net.IPNet{
IP: net.ParseIP("1.2.3.4"),
Mask: net.CIDRMask(24, 32),
}
err = h.AddrAdd(eth0Link, &netlink.Addr{IPNet: testIP1})
require.NoError(t, err)
// Add default route.
testGW1 := net.ParseIP("1.2.3.1")
defaultRoute := &netlink.Route{
Family: netlink.FAMILY_V4,
LinkIndex: eth0Link.Attrs().Index,
Dst: &net.IPNet{
IP: net.IPv4zero,
Mask: net.CIDRMask(0, 32),
},
Gw: testGW1,
}
err = h.RouteAdd(defaultRoute)
require.NoError(t, err)
// Setup test link.
eth1Name := "test_enp2s0"
eth1Link := &netlink.Dummy{LinkAttrs: netlink.LinkAttrs{
Name: eth1Name,
HardwareAddr: net.HardwareAddr{0x52, 0x54, 0x00, 0x8b, 0xad, 0x93},
}}
err = h.LinkAdd(eth1Link)
require.NoError(t, err)
// Set test link up.
err = h.LinkSetUp(eth1Link)
require.NoError(t, err)
// Add test IP.
testIP2 := &net.IPNet{
IP: net.ParseIP("fc00:5aa8:7160:d9eb:1:0:1:3"),
Mask: net.CIDRMask(64, 128),
}
err = h.AddrAdd(eth1Link, &netlink.Addr{IPNet: testIP2})
require.NoError(t, err)
// Add default route.
testGW2 := net.ParseIP("fe80::1")
ipv6Dest := &net.IPNet{
IP: net.ParseIP("fc00:5aa8:7160:d9eb::1"),
Mask: net.CIDRMask(128, 128),
}
ipv6Route := &netlink.Route{
Family: netlink.FAMILY_V6,
LinkIndex: eth1Link.Attrs().Index,
Dst: ipv6Dest,
Gw: testGW2,
Priority: 200,
}
err = h.RouteAdd(ipv6Route)
require.NoError(t, err)
// Setup configurator with test URL to test server.
c := &linuxConfigurator{configOptions: &configOptions{}}
c.testAddress = "test_success"
// The test intentionally removes addresses that are primary at runtime;
// enable the override so the removal calls succeed.
c.allowPrimaryRemoval = true
// Setup test results path.
testDir, err := filepath.Abs("./tests/configurator_linux")
require.NoError(t, err)
resultsDir := filepath.Join(testDir, "results")
tmpDir, err := os.MkdirTemp("", "")
require.NoError(t, err)
configPath := filepath.Join(tmpDir, "50-cloud-init.yaml")
err = fileCopy(filepath.Join(testDir, "50-cloud-init.yaml"), configPath)
require.NoError(t, err)
// Setup ifupdown and parse test file.
ci, err := newCloudInitWith(configPath, filepath.Join(tmpDir, "nothing.json"), filepath.Join(tmpDir, "cloud.cfg.d"))
require.NoError(t, err)
c.ifaceBackends = append(c.ifaceBackends, namedIfaceBackend{"cloud-init", ci})
// Get list of interfaces.
interfaces, err := c.GetInterfaces(context.Background())
require.NoError(t, err)
// Verify interfaces read from file.
err = testVerifyInterfaces(interfaces, resultsDir, 1)
require.NoError(t, err)
// Test add addresses.
testIP3 := &net.IPNet{IP: net.ParseIP("1.2.3.5"), Mask: net.CIDRMask(24, 32)}
err = c.AddAddress(context.Background(), eth0Name, testIP3, nil)
assert.NoError(t, err)
testIP4 := &net.IPNet{IP: net.ParseIP("fc00:5aa8:7160:d9eb:1:0:1:5"), Mask: net.CIDRMask(64, 128)}
err = c.AddAddress(context.Background(), eth1Name, testIP4, net.ParseIP("fc00:5aa8:7160:d9eb::"))
assert.NoError(t, err)
// Get list of interfaces.
interfaces, err = c.GetInterfaces(context.Background())
require.NoError(t, err)
// Verify interfaces read from file.
err = testVerifyInterfaces(interfaces, resultsDir, 2)
require.NoError(t, err)
// Read the current file and expected state.
err = testVerifyResults(resultsDir, tmpDir, 1)
require.NoError(t, err)
// Test remove addresses.
err = c.RemoveAddress(context.Background(), eth0Name, testIP1)
assert.NoError(t, err)
err = c.RemoveAddress(context.Background(), eth1Name, testIP2)
assert.NoError(t, err)
// Get list of interfaces.
interfaces, err = c.GetInterfaces(context.Background())
require.NoError(t, err)
// Verify interfaces read from file.
err = testVerifyInterfaces(interfaces, resultsDir, 3)
require.NoError(t, err)
// Read the current file and expected state.
err = testVerifyResults(resultsDir, tmpDir, 2)
require.NoError(t, err)
// Test rollback on failures.
c.testAddress = "test_fail"
err = c.AddAddress(context.Background(), eth0Name, testIP1, nil)
assert.EqualError(t, err, "aborted operation due to loss of internet", "Failed rollback test")
// Get list of interfaces.
interfaces, err = c.GetInterfaces(context.Background())
require.NoError(t, err)
// Verify interfaces read from file.
err = testVerifyInterfaces(interfaces, resultsDir, 3)
require.NoError(t, err)
// Read the current file and expected state.
err = testVerifyResults(resultsDir, tmpDir, 2)
require.NoError(t, err)
// Test remove gateway.
err = c.AddAddress(context.Background(), eth0Name, testIP3, make(net.IP, 4))
assert.NoError(t, err)
// Get list of interfaces.
interfaces, err = c.GetInterfaces(context.Background())
require.NoError(t, err)
// Verify interfaces read from file.
err = testVerifyInterfaces(interfaces, resultsDir, 4)
require.NoError(t, err)
// Read the current file and expected state.
err = testVerifyResults(resultsDir, tmpDir, 3)
require.NoError(t, err)
// Test add gateway rollback.
err = c.AddAddress(context.Background(), eth0Name, testIP3, testGW1)
assert.EqualError(t, err, "aborted operation due to loss of internet", "Failed rollback test")
// Get list of interfaces.
interfaces, err = c.GetInterfaces(context.Background())
require.NoError(t, err)
// Verify interfaces read from file.
err = testVerifyInterfaces(interfaces, resultsDir, 4)
require.NoError(t, err)
// Read the current file and expected state.
err = testVerifyResults(resultsDir, tmpDir, 3)
require.NoError(t, err)
// Test remove gateway.
c.testAddress = "test_success"
err = c.AddAddress(context.Background(), eth0Name, testIP3, testGW1)
assert.NoError(t, err)
// Get list of interfaces.
interfaces, err = c.GetInterfaces(context.Background())
require.NoError(t, err)
// Verify interfaces read from file.
err = testVerifyInterfaces(interfaces, resultsDir, 3)
require.NoError(t, err)
// Read the current file and expected state.
err = testVerifyResults(resultsDir, tmpDir, 2)
require.NoError(t, err)
// Test removing route.
err = c.RemoveRoute(context.Background(), eth1Name, ipv6Dest, testGW2)
assert.NoError(t, err)
// Get list of interfaces.
interfaces, err = c.GetInterfaces(context.Background())
require.NoError(t, err)
// Verify interfaces read from file.
err = testVerifyInterfaces(interfaces, resultsDir, 5)
require.NoError(t, err)
// Read the current file and expected state.
err = testVerifyResults(resultsDir, tmpDir, 4)
require.NoError(t, err)
// Test add route.
ipv4Dest := &net.IPNet{
IP: net.ParseIP("10.0.10.0"),
Mask: net.CIDRMask(24, 32),
}
testGW3 := net.ParseIP("1.2.3.254")
err = c.AddRoute(context.Background(), eth0Name, ipv4Dest, testGW3, 100)
assert.NoError(t, err)
// Get list of interfaces.
interfaces, err = c.GetInterfaces(context.Background())
require.NoError(t, err)
// Verify interfaces read from file.
err = testVerifyInterfaces(interfaces, resultsDir, 6)
require.NoError(t, err)
// Read the current file and expected state.
err = testVerifyResults(resultsDir, tmpDir, 5)
require.NoError(t, err)
}