568 lines
15 KiB
Go
568 lines
15 KiB
Go
package netconfig
|
|
|
|
import (
|
|
"context"
|
|
"net"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
|
|
"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()
|
|
if err != nil {
|
|
t.Fatal("Failed to create new netns", err)
|
|
}
|
|
|
|
link, err := netlink.LinkByName("lo")
|
|
if err != nil {
|
|
t.Fatalf("Failed to find \"lo\" in new netns: %v", err)
|
|
}
|
|
err = netlink.LinkSetUp(link)
|
|
if err != nil {
|
|
t.Fatalf("Failed to bring up \"lo\" in new netns: %v", err)
|
|
}
|
|
|
|
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)
|
|
if err != nil {
|
|
t.Fatalf("AddrList: %v", err)
|
|
}
|
|
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()
|
|
if err != nil {
|
|
t.Fatal(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},
|
|
}})
|
|
if err = h.LinkAdd(link); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err = h.LinkSetUp(link); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// 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)}
|
|
if err = h.AddrAdd(link, &netlink.Addr{IPNet: primaryIP}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err = h.AddrAdd(link, &netlink.Addr{IPNet: secondaryIP}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Add a default route via the subnet so connectivity verification has a
|
|
// gateway to work with.
|
|
gw := net.ParseIP("1.2.3.1")
|
|
if err = 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,
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Sanity check the initial primary/secondary assignment.
|
|
if _, primary := isPrimary(t, h, link, primaryIP.IP); !primary {
|
|
t.Fatal("expected 1.2.3.4 to start as primary")
|
|
}
|
|
if _, primary := isPrimary(t, h, link, secondaryIP.IP); primary {
|
|
t.Fatal("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.
|
|
if err = c.SetPrimaryAddress(context.Background(), name, secondaryIP); err != nil {
|
|
t.Fatalf("SetPrimaryAddress: %v", err)
|
|
}
|
|
|
|
// 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.
|
|
if present, primary := isPrimary(t, h, link, secondaryIP.IP); !present || !primary {
|
|
t.Errorf("after promotion 1.2.3.5: present=%v primary=%v, want both true", present, primary)
|
|
}
|
|
if present, primary := isPrimary(t, h, link, primaryIP.IP); !present || primary {
|
|
t.Errorf("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)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
haveDefault := false
|
|
for _, r := range routes {
|
|
if ones, _ := r.Dst.Mask.Size(); ones == 0 && r.Gw.Equal(gw) {
|
|
haveDefault = true
|
|
}
|
|
}
|
|
if !haveDefault {
|
|
t.Error("default route missing after promotion")
|
|
}
|
|
|
|
// The persisted config must receive the reordered list, primary first.
|
|
if len(backend.lastAddrs) == 0 || !backend.lastAddrs[0].IP.Equal(secondaryIP.IP) {
|
|
t.Errorf("backend primary = %v, want 1.2.3.5 first", backend.lastAddrs)
|
|
}
|
|
|
|
// Promoting the already-primary address is a no-op that still succeeds.
|
|
if err = c.SetPrimaryAddress(context.Background(), name, secondaryIP); err != nil {
|
|
t.Errorf("SetPrimaryAddress (idempotent): %v", err)
|
|
}
|
|
if _, primary := isPrimary(t, h, link, secondaryIP.IP); !primary {
|
|
t.Error("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)
|
|
if err == nil || err.Error() != "address not found on interface" {
|
|
t.Errorf("absent address error = %v, want \"address not found on interface\"", err)
|
|
}
|
|
}
|
|
|
|
// 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()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer h.Close()
|
|
|
|
name := "test_enp1s0"
|
|
link := netlink.Link(&netlink.Dummy{LinkAttrs: netlink.LinkAttrs{Name: name}})
|
|
if err = h.LinkAdd(link); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err = h.LinkSetUp(link); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
primaryIP := &net.IPNet{IP: net.ParseIP("1.2.3.4"), Mask: net.CIDRMask(24, 32)}
|
|
if err = h.AddrAdd(link, &netlink.Addr{IPNet: primaryIP}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
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)
|
|
if err == nil || !contains(err.Error(), "refusing to remove primary address") {
|
|
t.Errorf("RemoveAddress primary = %v, want refusal error", err)
|
|
}
|
|
|
|
// With the override enabled, removal succeeds.
|
|
c.allowPrimaryRemoval = true
|
|
if err = c.RemoveAddress(context.Background(), name, primaryIP); err != nil {
|
|
t.Errorf("RemoveAddress with allowPrimaryRemoval: %v", err)
|
|
}
|
|
}
|
|
|
|
func contains(s, substr string) bool {
|
|
return strings.Contains(s, substr)
|
|
}
|
|
|
|
func TestLinuxConfigurator(t *testing.T) {
|
|
tearDown := setupNetlinkTest(t)
|
|
defer tearDown()
|
|
|
|
// Connect to netlink.
|
|
h, err := netlink.NewHandle()
|
|
if err != nil {
|
|
t.Fatal(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)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Set test link up.
|
|
err = h.LinkSetUp(eth0Link)
|
|
if err != nil {
|
|
t.Fatal(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})
|
|
if err != nil {
|
|
t.Fatal(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)
|
|
if err != nil {
|
|
t.Fatal(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)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Set test link up.
|
|
err = h.LinkSetUp(eth1Link)
|
|
if err != nil {
|
|
t.Fatal(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})
|
|
if err != nil {
|
|
t.Fatal(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)
|
|
if err != nil {
|
|
t.Fatal(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")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
resultsDir := filepath.Join(testDir, "results")
|
|
tmpDir, err := os.MkdirTemp("", "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
configPath := filepath.Join(tmpDir, "50-cloud-init.yaml")
|
|
err = fileCopy(filepath.Join(testDir, "50-cloud-init.yaml"), configPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Setup ifupdown and parse test file.
|
|
ci, err := newCloudInitWith(configPath, filepath.Join(tmpDir, "nothing.json"), filepath.Join(tmpDir, "cloud.cfg.d"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
c.ifaceBackends = append(c.ifaceBackends, namedIfaceBackend{"cloud-init", ci})
|
|
|
|
// Get list of interfaces.
|
|
interfaces, err := c.GetInterfaces(context.Background())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Verify interfaces read from file.
|
|
err = testVerifyInterfaces(interfaces, resultsDir, 1)
|
|
if err != nil {
|
|
t.Error(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)
|
|
if err != nil {
|
|
t.Error(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::"))
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
|
|
// Get list of interfaces.
|
|
interfaces, err = c.GetInterfaces(context.Background())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Verify interfaces read from file.
|
|
err = testVerifyInterfaces(interfaces, resultsDir, 2)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
|
|
// Read the current file and expected state.
|
|
err = testVerifyResults(resultsDir, tmpDir, 1)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
|
|
// Test remove addresses.
|
|
err = c.RemoveAddress(context.Background(), eth0Name, testIP1)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
err = c.RemoveAddress(context.Background(), eth1Name, testIP2)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
|
|
// Get list of interfaces.
|
|
interfaces, err = c.GetInterfaces(context.Background())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Verify interfaces read from file.
|
|
err = testVerifyInterfaces(interfaces, resultsDir, 3)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
|
|
// Read the current file and expected state.
|
|
err = testVerifyResults(resultsDir, tmpDir, 2)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
|
|
// Test rollback on failures.
|
|
c.testAddress = "test_fail"
|
|
err = c.AddAddress(context.Background(), eth0Name, testIP1, nil)
|
|
if err == nil || err.Error() != "aborted operation due to loss of internet" {
|
|
t.Errorf("Failed rollback test: %v", err)
|
|
}
|
|
|
|
// Get list of interfaces.
|
|
interfaces, err = c.GetInterfaces(context.Background())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Verify interfaces read from file.
|
|
err = testVerifyInterfaces(interfaces, resultsDir, 3)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
|
|
// Read the current file and expected state.
|
|
err = testVerifyResults(resultsDir, tmpDir, 2)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
|
|
// Test remove gateway.
|
|
err = c.AddAddress(context.Background(), eth0Name, testIP3, make(net.IP, 4))
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
|
|
// Get list of interfaces.
|
|
interfaces, err = c.GetInterfaces(context.Background())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Verify interfaces read from file.
|
|
err = testVerifyInterfaces(interfaces, resultsDir, 4)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
|
|
// Read the current file and expected state.
|
|
err = testVerifyResults(resultsDir, tmpDir, 3)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
|
|
// Test add gateway rollback.
|
|
err = c.AddAddress(context.Background(), eth0Name, testIP3, testGW1)
|
|
if err == nil || err.Error() != "aborted operation due to loss of internet" {
|
|
t.Errorf("Failed rollback test: %v", err)
|
|
}
|
|
|
|
// Get list of interfaces.
|
|
interfaces, err = c.GetInterfaces(context.Background())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Verify interfaces read from file.
|
|
err = testVerifyInterfaces(interfaces, resultsDir, 4)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
|
|
// Read the current file and expected state.
|
|
err = testVerifyResults(resultsDir, tmpDir, 3)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
|
|
// Test remove gateway.
|
|
c.testAddress = "test_success"
|
|
err = c.AddAddress(context.Background(), eth0Name, testIP3, testGW1)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
|
|
// Get list of interfaces.
|
|
interfaces, err = c.GetInterfaces(context.Background())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Verify interfaces read from file.
|
|
err = testVerifyInterfaces(interfaces, resultsDir, 3)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
|
|
// Read the current file and expected state.
|
|
err = testVerifyResults(resultsDir, tmpDir, 2)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
|
|
// Test removing route.
|
|
err = c.RemoveRoute(context.Background(), eth1Name, ipv6Dest, testGW2)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
|
|
// Get list of interfaces.
|
|
interfaces, err = c.GetInterfaces(context.Background())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Verify interfaces read from file.
|
|
err = testVerifyInterfaces(interfaces, resultsDir, 5)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
|
|
// Read the current file and expected state.
|
|
err = testVerifyResults(resultsDir, tmpDir, 4)
|
|
if err != nil {
|
|
t.Error(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)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
|
|
// Get list of interfaces.
|
|
interfaces, err = c.GetInterfaces(context.Background())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Verify interfaces read from file.
|
|
err = testVerifyInterfaces(interfaces, resultsDir, 6)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
|
|
// Read the current file and expected state.
|
|
err = testVerifyResults(resultsDir, tmpDir, 5)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
}
|