From 5b47e4553c5af7cbdb2ae752776a99a81cea5826 Mon Sep 17 00:00:00 2001 From: Simon Ruderich Date: Sun, 27 Oct 2024 21:33:26 +0100 Subject: [PATCH] First working version --- .gitignore | 1 + Makefile | 6 ++ config.go | 221 +++++++++++++++++++++++++++++++++++++++++++++++++++++ go.mod | 3 + main.go | 202 ++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 433 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 config.go create mode 100644 go.mod create mode 100644 main.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7661a79 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/linux-network-namespace-labs diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..77f4efe --- /dev/null +++ b/Makefile @@ -0,0 +1,6 @@ +all: + go build + go fmt + go vet + +.PHONY: all diff --git a/config.go b/config.go new file mode 100644 index 0000000..3e69cad --- /dev/null +++ b/config.go @@ -0,0 +1,221 @@ +// Config parser + +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright (C) 2024 Simon Ruderich + +package main + +import ( + "bufio" + "fmt" + "net/netip" + "os" + "strings" +) + +type Config struct { + Nets map[string]*Net + Nodes map[string]*Node + Links []*Link +} + +type Net struct { + Name string + Prefixes []netip.Prefix + + next []netip.Addr // next free address of Prefixes +} + +// Next returns the next free address. +func (n *Net) Next() ([]netip.Addr, error) { + var res []netip.Addr + for i, prefix := range n.Prefixes { + next := n.next[i] + if !prefix.Contains(next) { + return nil, fmt.Errorf("no free addresses in net %q", n.Name) + } + res = append(res, next) + n.next[i] = next.Next() + } + return res, nil +} + +type Node struct { + Name string + Loopbacks []netip.Addr +} + +type Link struct { + A struct { + Node *Node + Addrs []netip.Prefix + } + B struct { + *Node + Addrs []netip.Prefix + } +} + +func LoadConfig(path string) (*Config, error) { + f, err := os.Open(path) + if err != nil { + return nil, err + } + defer f.Close() + + cfg := Config{ + Nets: make(map[string]*Net), + Nodes: make(map[string]*Node), + } + + n := 0 // line number + s := bufio.NewScanner(f) + for s.Scan() { + l := s.Text() + n++ + + if l == "" || l[0] == '#' { + continue + } + + var err error + var usage string + xs := strings.Split(l, " ") + switch xs[0] { + case "net": + usage = `"net" ...` + err = parseConfigNet(&cfg, xs[1:]) + case "node": + usage = `"node" [...]` + err = parseConfigNode(&cfg, xs[1:]) + case "link": + usage = `"link" ` + err = parseConfigLink(&cfg, xs[1:]) + default: + usage = `"net" | "node" | "link"` + err = fmt.Errorf("unknown option %q", xs[0]) + } + if err != nil { + return nil, fmt.Errorf("error in line %d %q: %v (expected %s)", + n, l, err, usage) + } + } + err = s.Err() + if err != nil { + return nil, err + } + + return &cfg, nil +} + +func parseConfigNet(cfg *Config, args []string) error { + if len(args) < 2 { + return fmt.Errorf("not enough arguments") + } + + res := Net{ + Name: args[0], + } + if cfg.Nets[res.Name] != nil { + return fmt.Errorf("%q already exists", res.Name) + } + + for _, arg := range args[1:] { + x, err := netip.ParsePrefix(arg) + if err != nil { + return fmt.Errorf("invalid prefix: %v", err) + } + if x != x.Masked() { + // User should not assume that addresses can be used here + return fmt.Errorf("prefix not canonical") + } + res.Prefixes = append(res.Prefixes, x) + res.next = append(res.next, x.Addr()) + } + + cfg.Nets[res.Name] = &res + return nil +} + +func parseConfigNode(cfg *Config, args []string) error { + if len(args) < 1 { + return fmt.Errorf("not enough arguments") + } + + res := Node{ + Name: args[0], + } + if cfg.Nodes[res.Name] != nil { + return fmt.Errorf("%q already exists", res.Name) + } + + for _, arg := range args[1:] { + n := cfg.Nets[arg] + if n == nil { + return fmt.Errorf("net %q does not exist", arg) + } + ls, err := n.Next() + if err != nil { + return err + } + res.Loopbacks = append(res.Loopbacks, ls...) + } + + cfg.Nodes[res.Name] = &res + return nil +} + +func parseConfigLink(cfg *Config, args []string) error { + if len(args) != 3 { + return fmt.Errorf("invalid arguments") + } + + n1 := cfg.Nodes[args[0]] + if n1 == nil { + return fmt.Errorf("node %q does not exist", args[0]) + } + n2 := cfg.Nodes[args[1]] + if n1 == nil { + return fmt.Errorf("node %q does not exist", args[1]) + } + + n := cfg.Nets[args[2]] + if n == nil { + return fmt.Errorf("net %q does not exist", args[2]) + } + a1, err := n.Next() + if err != nil { + return err + } + a2, err := n.Next() + if err != nil { + return err + } + + var l Link + l.A.Node = n1 + l.B.Node = n2 + + // Allocate addresses for the link, only /31 and /127 prefixes are used + for i := range a1 { + var bits int + if a1[i].Is4() { + bits = 31 + } else { + bits = 127 + } + p1 := netip.PrefixFrom(a1[i], bits) + p2 := netip.PrefixFrom(a2[i], bits) + if !p1.Contains(p2.Addr()) || !p2.Contains(p1.Addr()) { + return fmt.Errorf( + "prefix mismatch for %q and %q (loopback network?)", p1, p2) + } + l.A.Addrs = append(l.A.Addrs, p1) + l.B.Addrs = append(l.B.Addrs, p2) + } + + cfg.Links = append(cfg.Links, &l) + return nil +} + +// vi: set noet ts=4 sw=4 sts=4: diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..58a34ef --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module linux-network-namespace-labs + +go 1.22 diff --git a/main.go b/main.go new file mode 100644 index 0000000..a935b5c --- /dev/null +++ b/main.go @@ -0,0 +1,202 @@ +// Small helper program to create network lab setups using Linux network +// namespaces. A simple text configuration file is used to describe the nets +// (addresses), nodes and links. + +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright (C) 2024 Simon Ruderich + +package main + +import ( + "bufio" + "encoding/json" + "fmt" + "log" + "os" + "os/exec" + "path/filepath" +) + +func main() { + if len(os.Args) != 2 { + log.SetFlags(0) + log.Fatalf("usage: %s ", os.Args[0]) + } + + cfg, err := LoadConfig(os.Args[1]) + if err != nil { + log.Fatalf("config %q: %v", os.Args[1], err) + } + + for _, node := range cfg.Nodes { + log.Printf("Setting up node %q ...", node.Name) + + // One namespace per node, named as the node's name + ns := node.Name + if netnsExists(ns) { + // Prevent any conflicts with existing data + ip("netns", "del", ns) + // Don't remove anything in /etc/netns/ as the user might store + // configuration there! + } + ip("netns", "add", ns) + + // Write /etc/netns/$netns/hosts with all known hosts + nsCfgPath := filepath.Join("/etc/netns", ns) + nsHostsPath := filepath.Join(nsCfgPath, "hosts") + log.Printf(" Writing %q", nsHostsPath) + err := os.MkdirAll(nsCfgPath, 0755) + if err != nil { + log.Fatal(err) + } + err = writeHosts(cfg, nsHostsPath) + if err != nil { + log.Fatal(err) + } + + ip("-n", ns, "link", "set", "lo", "up") + // Extra interface for our loopback addresses; keeping them separate + // can make things easier (e.g. using the interface for protocols). + lo := "lo2" + ip("-n", ns, "link", "add", lo, "type", "dummy") + ip("-n", ns, "link", "set", lo, "up") + for _, x := range node.Loopbacks { + ip("-n", ns, "addr", "add", x.String(), "dev", lo) + } + } + + log.Printf("Setting up links ...") + for _, link := range cfg.Links { + nsa := link.A.Node.Name + nsb := link.B.Node.Name + + log.Printf(" Link between %q and %q ...", nsa, nsb) + + // Use name of other node for the interface + la := nsb + lb := nsa + // Support multiple links between nodes + if ifaceExists(nsa, la) { + la = nextFreeIface(nsa, la) + } + if ifaceExists(nsb, lb) { + lb = nextFreeIface(nsb, lb) + } + + ip("link", "add", "tmpa", "type", "veth", "peer", "name", "tmpb") + ip("link", "set", "tmpa", "netns", nsa) + ip("link", "set", "tmpb", "netns", nsb) + ip("-n", nsa, "link", "set", "tmpa", "name", la) + ip("-n", nsb, "link", "set", "tmpb", "name", lb) + for _, x := range link.A.Addrs { + ip("-n", nsa, "addr", "add", x.String(), "dev", la) + } + for _, x := range link.B.Addrs { + ip("-n", nsb, "addr", "add", x.String(), "dev", lb) + } + ip("-n", nsa, "link", "set", la, "up") + ip("-n", nsb, "link", "set", lb, "up") + } +} + +func ip(args ...string) { + xargs := append([]string{"ip"}, args...) + log.Printf(" Running %q", xargs) + + cmd := exec.Command("ip", args...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + err := cmd.Run() + if err != nil { + log.Fatalf("failed to run %q: %v", xargs, err) + } +} + +func netnsExists(name string) bool { + _, err := os.Stat(filepath.Join("/run/netns", name)) + if err != nil { + if os.IsNotExist(err) { + return false + } + log.Fatal(err) + } + return true +} + +func ifaceExists(netns, name string) bool { + args := []string{"-n", netns, "-json", "link"} + xargs := append([]string{"ip"}, args...) + + cmd := exec.Command("ip", args...) + out, err := cmd.Output() + if err != nil { + log.Fatalf("failed to run %q: %v", xargs, err) + } + + var ifaces []struct { + Ifname string `json:"ifname"` + } + err = json.Unmarshal(out, &ifaces) + if err != nil { + log.Fatalf("failed to parse output from ip (%q): %v", out, err) + } + + for _, x := range ifaces { + if x.Ifname == name { + return true + } + } + return false +} + +func nextFreeIface(netns, name string) string { + i := 2 + x := name + for ifaceExists(netns, x) { + x = fmt.Sprintf("%s_%d", name, i) + i++ + } + return x +} + +// writeHosts writes a hosts-file (i.e. /etc/hosts) with all known addresses +// and their corresponding node names. +func writeHosts(cfg *Config, path string) error { + f, err := os.Create(path) + if err != nil { + return err + } + defer f.Close() + w := bufio.NewWriter(f) + + // Standard entries + fmt.Fprintf(w, "127.0.0.1 localhost\n") + fmt.Fprintf(w, "::1 localhost ip6-localhost ip6-loopback\n") + + for _, node := range cfg.Nodes { + for _, x := range node.Loopbacks { + fmt.Fprintf(w, "%s %s-loop\n", x.String(), node.Name) + } + } + + for _, link := range cfg.Links { + for _, x := range link.A.Addrs { + fmt.Fprintf(w, "%s %s\n", x.Addr().String(), link.A.Node.Name) + } + for _, x := range link.B.Addrs { + fmt.Fprintf(w, "%s %s\n", x.Addr().String(), link.B.Node.Name) + } + } + + err = w.Flush() + if err != nil { + return err + } + err = f.Sync() + if err != nil { + return err + } + return nil +} + +// vi: set noet ts=4 sw=4 sts=4: -- 2.45.2