]> ruderich.org/simon Gitweb - linux-network-namespace-labs/linux-network-namespace-labs.git/commitdiff
First working version
authorSimon Ruderich <simon@ruderich.org>
Sun, 27 Oct 2024 20:33:26 +0000 (21:33 +0100)
committerSimon Ruderich <simon@ruderich.org>
Sun, 27 Oct 2024 20:33:26 +0000 (21:33 +0100)
.gitignore [new file with mode: 0644]
Makefile [new file with mode: 0644]
config.go [new file with mode: 0644]
go.mod [new file with mode: 0644]
main.go [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..7661a79
--- /dev/null
@@ -0,0 +1 @@
+/linux-network-namespace-labs
diff --git a/Makefile b/Makefile
new file mode 100644 (file)
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 (file)
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" <name> <prefix>...`
+                       err = parseConfigNet(&cfg, xs[1:])
+               case "node":
+                       usage = `"node" <name> [<loopback-net-name>...]`
+                       err = parseConfigNode(&cfg, xs[1:])
+               case "link":
+                       usage = `"link" <node> <node> <net-name>`
+                       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 (file)
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 (file)
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 <config>", 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: