From cffd8723464acacc0477966f88ae9306bb7bd5e3 Mon Sep 17 00:00:00 2001 From: Michael Stapelberg Date: Sun, 6 Jun 2021 12:02:18 +0200 Subject: [PATCH] netconfig: implement bridge configuration fixes https://github.com/rtr7/router7/issues/65 --- integration/netconfig/netconfig_test.go | 125 ++++++++++++++++++++++++ internal/netconfig/netconfig.go | 59 +++++++++++ 2 files changed, 184 insertions(+) diff --git a/integration/netconfig/netconfig_test.go b/integration/netconfig/netconfig_test.go index 8546579..d8ab0ed 100644 --- a/integration/netconfig/netconfig_test.go +++ b/integration/netconfig/netconfig_test.go @@ -458,6 +458,131 @@ peer: AVU3LodtnFaFnJmMyNNW7cUk4462lqnVULTFkjWYvRo= }) } +const goldenInterfacesBridges = ` +{ + "bridges":[ + { + "name": "lan0", + "interface_hardware_addrs": ["02:73:53:00:b0:0c"] + } + ], + "interfaces":[ + { + "hardware_addr": "02:73:53:00:ca:fe", + "name": "uplink0" + }, + { + "spoof_hardware_addr": "02:73:53:00:b0:aa", + "name": "lan0", + "addr": "192.168.42.1/24" + } + ] +} +` + +func TestNetconfigBridges(t *testing.T) { + if os.Getenv("HELPER_PROCESS") == "1" { + tmp, err := ioutil.TempDir("", "router7") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmp) + + for _, golden := range []struct { + filename, content string + }{ + {"interfaces.json", goldenInterfacesBridges}, + } { + if err := os.MkdirAll(filepath.Join(tmp, filepath.Dir(golden.filename)), 0755); err != nil { + t.Fatal(err) + } + if err := ioutil.WriteFile(filepath.Join(tmp, golden.filename), []byte(golden.content), 0600); err != nil { + t.Fatal(err) + } + } + + if err := os.MkdirAll(filepath.Join(tmp, "root", "etc"), 0755); err != nil { + t.Fatal(err) + } + + if err := os.MkdirAll(filepath.Join(tmp, "root", "tmp"), 0755); err != nil { + t.Fatal(err) + } + + netconfig.DefaultCounterObj = &nftables.CounterObj{Packets: 23, Bytes: 42} + if err := netconfig.Apply(tmp, filepath.Join(tmp, "root")); err != nil { + t.Fatalf("netconfig.Apply: %v", err) + } + + // Apply twice to ensure the absence of errors when dealing with + // already-configured interfaces, addresses, routes, … (and ensure + // nftables rules are replaced, not appendend to). + netconfig.DefaultCounterObj = &nftables.CounterObj{Packets: 0, Bytes: 0} + if err := netconfig.Apply(tmp, filepath.Join(tmp, "root")); err != nil { + t.Fatalf("netconfig.Apply: %v", err) + } + + return + } + const ns = "ns6" // name of the network namespace to use for this test + + add := exec.Command("ip", "netns", "add", ns) + add.Stderr = os.Stderr + if err := add.Run(); err != nil { + t.Fatalf("%v: %v", add.Args, err) + } + defer exec.Command("ip", "netns", "delete", ns).Run() + + nsSetup := []*exec.Cmd{ + exec.Command("ip", "-netns", ns, "link", "add", "dummy0", "type", "dummy"), + exec.Command("ip", "-netns", ns, "link", "add", "eth0", "type", "dummy"), + exec.Command("ip", "-netns", ns, "link", "set", "dummy0", "address", "02:73:53:00:ca:fe"), + exec.Command("ip", "-netns", ns, "link", "set", "eth0", "address", "02:73:53:00:b0:0c"), + } + + for _, cmd := range nsSetup { + if err := cmd.Run(); err != nil { + t.Fatalf("%v: %v", cmd.Args, err) + } + } + + cmd := exec.Command("ip", "netns", "exec", ns, os.Args[0], "-test.run=^TestNetconfigBridges") + cmd.Env = append(os.Environ(), "HELPER_PROCESS=1") + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + t.Fatal(err) + } + + t.Run("VerifyAddresses", func(t *testing.T) { + link, err := exec.Command("ip", "-netns", ns, "link", "show", "dev", "lan0", "type", "bridge").Output() + if err != nil { + t.Fatal(err) + } + if !strings.Contains(string(link), "link/ether 02:73:53:00:b0:aa") { + t.Errorf("lan0 MAC address is not 02:73:53:00:b0:aa") + } + + addrs, err := exec.Command("ip", "-netns", ns, "address", "show", "dev", "lan0").Output() + if err != nil { + t.Fatal(err) + } + + addrRe := regexp.MustCompile(`(?m)^\s*inet 192.168.42.1/24 brd 192.168.42.255 scope global lan0`) + if !addrRe.MatchString(string(addrs)) { + t.Fatalf("regexp %s does not match %s", addrRe, string(addrs)) + } + + bridgeLinks, err := exec.Command("ip", "-netns", ns, "link", "show", "master", "lan0").Output() + if err != nil { + t.Fatal(err) + } + if !strings.Contains(string(bridgeLinks), ": eth0: ") { + t.Errorf("lan0 bridge does not contain eth0 interface") + } + }) +} + func ipLines(args ...string) ([]string, error) { cmd := exec.Command("ip", args...) out, err := cmd.Output() diff --git a/internal/netconfig/netconfig.go b/internal/netconfig/netconfig.go index 416d4be..c6350a6 100644 --- a/internal/netconfig/netconfig.go +++ b/internal/netconfig/netconfig.go @@ -198,8 +198,14 @@ type InterfaceDetails struct { Addr string `json:"addr"` // e.g. 192.168.42.1/24 } +type BridgeDetails struct { + Name string `json:"name"` // e.g. br0 or lan0 + InterfaceHardwareAddrs []string `json:"interface_hardware_addrs"` +} + type InterfaceConfig struct { Interfaces []InterfaceDetails `json:"interfaces"` + Bridges []BridgeDetails `json:"bridges"` } // Interface returns the InterfaceDetails configured for interface ifname in @@ -255,6 +261,56 @@ func applyInterfaces(dir, root string) error { } byName[details.Name] = details } + + for _, bridge := range cfg.Bridges { + if _, err := netlink.LinkByName(bridge.Name); err != nil { + link := &netlink.Bridge{LinkAttrs: netlink.LinkAttrs{Name: bridge.Name}} + if err := netlink.LinkAdd(link); err != nil { + return fmt.Errorf("netlink.LinkAdd: %v", err) + } + } + interfaces := make(map[string]bool) + for _, hwaddr := range bridge.InterfaceHardwareAddrs { + interfaces[hwaddr] = true + } + bridgeLink, err := netlink.LinkByName(bridge.Name) + if err != nil { + return fmt.Errorf("LinkByName(%s): %v", bridge.Name, err) + } + + links, err := netlink.LinkList() + if err != nil { + return err + } + for _, l := range links { + attr := l.Attrs() + addr := attr.HardwareAddr.String() + if addr == "" { + continue + } + if !interfaces[addr] { + continue + } + log.Printf("adding interface %s to bridge %s", attr.Name, bridge.Name) + if err := netlink.LinkSetMaster(l, bridgeLink); err != nil { + return fmt.Errorf("LinkSetMaster(%s): %v", attr.Name, err) + } + if attr.OperState != netlink.OperUp { + log.Printf("setting interface %s up", attr.Name) + if err := netlink.LinkSetUp(l); err != nil { + return fmt.Errorf("LinkSetUp(%s): %v", attr.Name, err) + } + } + + } + if attr := bridgeLink.Attrs(); attr.OperState != netlink.OperUp { + log.Printf("setting interface %s up", attr.Name) + if err := netlink.LinkSetUp(bridgeLink); err != nil { + return fmt.Errorf("LinkSetUp(%s): %v", attr.Name, err) + } + } + } + links, err := netlink.LinkList() if err != nil { return err @@ -276,6 +332,9 @@ func applyInterfaces(dir, root string) error { } } else { details, ok = byHardwareAddr[addr] + if !ok { + details, ok = byName[attr.Name] + } } if !ok { log.Printf("no config for interface %s/%s", attr.Name, addr)