--- /dev/null
+// 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:
--- /dev/null
+// 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: