From 9b90defbdba9fbab93659ef97001f06e40854dd0 Mon Sep 17 00:00:00 2001 From: GnomeZworc Date: Mon, 31 Aug 2026 19:48:50 +0200 Subject: [PATCH] f-46: dhcp: add the dhcp binary, its unit and its wrapper #46 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cmd/dhcp reçoit quatre paramètres en clair — interface, state, socket, conf — sur le modèle de dnsmasq. Il ne compose aucun chemin, ne découpe aucun nom composite et ignore le netns dans lequel il tourne : seul le wrapper en a besoin, pour y entrer. La boucle de lecture UDP est écrite à la main plutôt que confiée à server4.Serve, qui lance une goroutine par datagramme sans borne et ne pose aucun recover. Elle traite en ligne, réutilise un tampon de 1500 octets et pose un recover par datagramme. NewIPv4UDPConn est conservé pour le SO_BROADCAST et le bind à l'interface. Une réponse destinée à un client sans adresse part en broadcast. dhcp.run_dir n'est pas une clé de configuration : le wrapper code /run/two/dhcp en dur et le Go utilise dhcpapi.DefaultRunDir, comme dhcp.DefaultConfDir pour dnsmasq. Un test lit le script et vérifie que les deux s'accordent. Le défaut dhcp.backend reste dnsmasq : un agent.yml de 0.1.0 se comporte comme avant. deploy.sh et le pipeline publient le binaire, l'unit et le script. La boucle est testée sur une vraie socket UDP en loopback. Neuf mutations, dont une qui a révélé que le recover n'était couvert par rien. Signed-off-by: GnomeZworc --- .forgejo/workflows/release-pipeline.yml | 5 + cmd/dhcp/main.go | 78 +++++++++ conf/agent/config.exemple.yml | 10 ++ internal/api/dhcp/paths.go | 29 ++++ internal/api/dhcp/paths_test.go | 69 ++++++++ internal/config/agent/dhcp.go | 17 ++ internal/config/agent/dhcp_test.go | 56 +++++++ internal/config/agent/struct.go | 4 + internal/dhcpd/listener.go | 68 ++++++++ internal/dhcpd/listener_test.go | 206 ++++++++++++++++++++++++ scripts/deploy.sh | 4 +- scripts/run-dhcp-in-netns.sh | 25 +++ systemd/dhcp@.service | 10 ++ 13 files changed, 579 insertions(+), 2 deletions(-) create mode 100644 cmd/dhcp/main.go create mode 100644 internal/api/dhcp/paths.go create mode 100644 internal/api/dhcp/paths_test.go create mode 100644 internal/config/agent/dhcp.go create mode 100644 internal/config/agent/dhcp_test.go create mode 100644 internal/dhcpd/listener.go create mode 100644 internal/dhcpd/listener_test.go create mode 100644 scripts/run-dhcp-in-netns.sh create mode 100644 systemd/dhcp@.service diff --git a/.forgejo/workflows/release-pipeline.yml b/.forgejo/workflows/release-pipeline.yml index 53e2ad3..b4e018a 100644 --- a/.forgejo/workflows/release-pipeline.yml +++ b/.forgejo/workflows/release-pipeline.yml @@ -51,6 +51,7 @@ jobs: binaries: - metadata - agent + - dhcp uses: ./.forgejo/workflows/build.yml with: tag: ${{ needs.set-release-target.outputs.release_cible }} @@ -69,10 +70,14 @@ jobs: include: - path: scripts/run-dnsmasq-in-netns.sh name: run-dnsmasq-in-netns.sh + - path: scripts/run-dhcp-in-netns.sh + name: run-dhcp-in-netns.sh - path: systemd/agent.service name: agent.service - path: systemd/dnsmasq@.service name: dnsmasq@.service + - path: systemd/dhcp@.service + name: dhcp@.service - path: systemd/metadata@.service name: metadata@.service steps: diff --git a/cmd/dhcp/main.go b/cmd/dhcp/main.go new file mode 100644 index 0000000..ab5b7fb --- /dev/null +++ b/cmd/dhcp/main.go @@ -0,0 +1,78 @@ +package main + +import ( + "flag" + "fmt" + "net" + "os" + + dhcpapi "git.g3e.fr/syonad/two/internal/api/dhcp" + configuration "git.g3e.fr/syonad/two/internal/config/agent" + "git.g3e.fr/syonad/two/internal/dhcpd" + "git.g3e.fr/syonad/two/pkg/logger" + + "github.com/insomniacslk/dhcp/dhcpv4" + "github.com/insomniacslk/dhcp/dhcpv4/server4" +) + +var ( + confFile = flag.String("conf", "/etc/two/agent.yml", "configuration file") + iface = flag.String("interface", "", "bridge to serve, already present in the current network namespace") + statePath = flag.String("state", "", "state file owned by this process") + socketPath = flag.String("socket", "", "control socket the agent talks to") +) + +func main() { + flag.Parse() + + if err := run(); err != nil { + fmt.Fprintf(os.Stderr, "dhcp: %v\n", err) + os.Exit(1) + } +} + +func run() error { + for name, value := range map[string]string{ + "-interface": *iface, + "-state": *statePath, + "-socket": *socketPath, + } { + if value == "" { + return fmt.Errorf("%s is required", name) + } + } + + cfg, err := configuration.LoadConfig(*confFile) + if err != nil { + return fmt.Errorf("load config: %w", err) + } + + log := logger.New(cfg.Logger.Level, cfg.Logger.Debug).With("bridge", *iface) + + store := dhcpd.NewStore(*statePath) + if err := store.Load(); err != nil { + return fmt.Errorf("load state: %w", err) + } + + control, err := dhcpapi.Listen(store, *socketPath, log) + if err != nil { + return fmt.Errorf("listen on the control socket: %w", err) + } + defer control.Close() + + go func() { + if err := control.Serve(); err != nil { + log.Error("control socket stopped", "error", err) + } + }() + + conn, err := server4.NewIPv4UDPConn(*iface, &net.UDPAddr{Port: dhcpv4.ServerPort}) + if err != nil { + return fmt.Errorf("bind udp/%d on %s: %w", dhcpv4.ServerPort, *iface, err) + } + defer conn.Close() + + log.Info("dhcp server started", "state", store.Path(), "socket", control.Addr()) + + return store.Serve(conn, log) +} diff --git a/conf/agent/config.exemple.yml b/conf/agent/config.exemple.yml index 72e88f8..de0f6ae 100644 --- a/conf/agent/config.exemple.yml +++ b/conf/agent/config.exemple.yml @@ -39,6 +39,16 @@ interfaces: metadata: run_dir: "/run/two/metadata" +# DHCP backend used for the subnets created by this agent. +# dnsmasq : dnsmasq@ instances driven by generated config files +# two : the built-in dhcp binary, driven over a unix socket +# Switching backends is a manual operation: drain the hypervisor, change this +# value, restart the agent. There is no hot migration. +# The per-subnet control socket and state file live in /run/two/dhcp, which is +# not configurable: the wrapper script hardcodes it too. +dhcp: + backend: dnsmasq + # QEMU runtime paths qemu: # UEFI firmware (requires apt install ovmf on Debian/Ubuntu) diff --git a/internal/api/dhcp/paths.go b/internal/api/dhcp/paths.go new file mode 100644 index 0000000..fcd5b09 --- /dev/null +++ b/internal/api/dhcp/paths.go @@ -0,0 +1,29 @@ +package dhcpapi + +import ( + "path/filepath" +) + +const ( + DefaultRunDir = "/run/two/dhcp" + SocketExt = ".sock" + StateExt = ".state" + UnitExt = ".service" + UnitName = "dhcp@" +) + +func Instance(vpc, bridge string) string { + return vpc + "_" + bridge +} + +func Unit(instance string) string { + return UnitName + instance + UnitExt +} + +func SocketPath(runDir, instance string) string { + return filepath.Join(runDir, instance+SocketExt) +} + +func StatePath(runDir, instance string) string { + return filepath.Join(runDir, instance+StateExt) +} diff --git a/internal/api/dhcp/paths_test.go b/internal/api/dhcp/paths_test.go new file mode 100644 index 0000000..8ad3658 --- /dev/null +++ b/internal/api/dhcp/paths_test.go @@ -0,0 +1,69 @@ +package dhcpapi + +import ( + "os" + "strings" + "testing" +) + +func TestInstance_JoinsVPCAndBridge(t *testing.T) { + if got := Instance("vp-admin", "br-000001"); got != "vp-admin_br-000001" { + t.Errorf("Instance = %s, want vp-admin_br-000001", got) + } +} + +func TestUnit_NamesTheTemplatedService(t *testing.T) { + if got := Unit(Instance("vp-admin", "br-000001")); got != "dhcp@vp-admin_br-000001.service" { + t.Errorf("Unit = %s", got) + } +} + +func TestSocketPath_SitsUnderTheRunDir(t *testing.T) { + got := SocketPath(DefaultRunDir, Instance("vp-admin", "br-000001")) + if got != "/run/two/dhcp/vp-admin_br-000001.sock" { + t.Errorf("SocketPath = %s", got) + } +} + +func TestStatePath_SitsUnderTheRunDir(t *testing.T) { + got := StatePath(DefaultRunDir, Instance("vp-admin", "br-000001")) + if got != "/run/two/dhcp/vp-admin_br-000001.state" { + t.Errorf("StatePath = %s", got) + } +} + +func TestPaths_NameTheVPCSoAListingIsReadable(t *testing.T) { + got := SocketPath(DefaultRunDir, Instance("vp-admin", "br-000001")) + if !strings.Contains(got, "vp-admin") { + t.Errorf("path = %s, want the vpc visible when listing the run dir", got) + } +} + +func TestPaths_DistinguishTwoSubnetsOfTheSameVPC(t *testing.T) { + a := SocketPath(DefaultRunDir, Instance("vp-admin", "br-000001")) + b := SocketPath(DefaultRunDir, Instance("vp-admin", "br-000002")) + if a == b { + t.Error("two subnets must not share a control socket") + } +} + +func TestSocketPath_StaysUnderTheUnixPathLimit(t *testing.T) { + got := SocketPath(DefaultRunDir, Instance("vp-000000", "br-000000")) + if len(got) > 100 { + t.Errorf("socket path is %d bytes (%s): sun_path caps at 104 on darwin and 108 on linux", len(got), got) + } +} + +func TestDefaultRunDir_MatchesTheWrapperScript(t *testing.T) { + const script = "../../../scripts/run-dhcp-in-netns.sh" + + raw, err := os.ReadFile(script) + if err != nil { + t.Fatalf("read %s: %v", script, err) + } + + want := `RUN_DIR="` + DefaultRunDir + `"` + if !strings.Contains(string(raw), want) { + t.Errorf("%s does not set %s: the agent would talk to a socket the server never creates", script, want) + } +} diff --git a/internal/config/agent/dhcp.go b/internal/config/agent/dhcp.go new file mode 100644 index 0000000..f26f023 --- /dev/null +++ b/internal/config/agent/dhcp.go @@ -0,0 +1,17 @@ +package configuration + +import "fmt" + +const ( + BackendDnsmasq = "dnsmasq" + BackendTwo = "two" +) + +func ValidBackend(backend string) error { + switch backend { + case BackendDnsmasq, BackendTwo: + return nil + default: + return fmt.Errorf("unknown dhcp backend %q: expected %q or %q", backend, BackendDnsmasq, BackendTwo) + } +} diff --git a/internal/config/agent/dhcp_test.go b/internal/config/agent/dhcp_test.go new file mode 100644 index 0000000..6796e56 --- /dev/null +++ b/internal/config/agent/dhcp_test.go @@ -0,0 +1,56 @@ +package configuration + +import ( + "os" + "path/filepath" + "testing" +) + +func writeConfig(t *testing.T, body string) string { + t.Helper() + path := filepath.Join(t.TempDir(), "agent.yml") + if err := os.WriteFile(path, []byte(body), 0o600); err != nil { + t.Fatalf("WriteFile: %v", err) + } + return path +} + +func TestValidBackend_AcceptsTheTwoKnownBackends(t *testing.T) { + for _, backend := range []string{BackendDnsmasq, BackendTwo} { + if err := ValidBackend(backend); err != nil { + t.Errorf("ValidBackend(%q) = %v, want nil", backend, err) + } + } +} + +func TestValidBackend_RejectsAnythingElse(t *testing.T) { + for _, backend := range []string{"", "dhcpd", "DNSMASQ", "two "} { + if err := ValidBackend(backend); err == nil { + t.Errorf("ValidBackend(%q) = nil, want an error", backend) + } + } +} + +func TestLoadConfig_DefaultsToDnsmasq(t *testing.T) { + path := writeConfig(t, "database:\n path: /tmp/two\n") + + cfg, err := LoadConfig(path) + if err != nil { + t.Fatalf("LoadConfig: %v", err) + } + if cfg.DHCP.Backend != BackendDnsmasq { + t.Errorf("backend = %q, want %q: a 0.1.0 config must keep behaving as before", cfg.DHCP.Backend, BackendDnsmasq) + } +} + +func TestLoadConfig_ReadsTheTwoBackend(t *testing.T) { + path := writeConfig(t, "dhcp:\n backend: two\n") + + cfg, err := LoadConfig(path) + if err != nil { + t.Fatalf("LoadConfig: %v", err) + } + if cfg.DHCP.Backend != BackendTwo { + t.Errorf("backend = %q, want two", cfg.DHCP.Backend) + } +} diff --git a/internal/config/agent/struct.go b/internal/config/agent/struct.go index 92a8677..d4afea1 100644 --- a/internal/config/agent/struct.go +++ b/internal/config/agent/struct.go @@ -31,6 +31,9 @@ type Config struct { Metadata struct { RunDir string `mapstructure:"run_dir"` } `mapstructure:"metadata"` + DHCP struct { + Backend string `mapstructure:"backend"` + } `mapstructure:"dhcp"` Admin struct { Enabled bool `mapstructure:"enabled"` Address string `mapstructure:"address"` @@ -67,6 +70,7 @@ func LoadConfig(path string) (*Config, error) { v.SetDefault("dispatcher.timeout_seconds", 300) v.SetDefault("dispatcher.poll_seconds", 2) v.SetDefault("metadata.run_dir", "/run/two/metadata") + v.SetDefault("dhcp.backend", BackendDnsmasq) v.SetDefault("qemu.ovmf_code_path", "/usr/share/OVMF/OVMF_CODE.fd") v.SetDefault("qemu.ovmf_vars_template", "/usr/share/OVMF/OVMF_VARS.fd") v.SetDefault("qemu.uefi_vars_dir", "/run/two/vms/uefi") diff --git a/internal/dhcpd/listener.go b/internal/dhcpd/listener.go new file mode 100644 index 0000000..af7bde5 --- /dev/null +++ b/internal/dhcpd/listener.go @@ -0,0 +1,68 @@ +package dhcpd + +import ( + "log/slog" + "net" + + "github.com/insomniacslk/dhcp/dhcpv4" +) + +const MaxDatagramBytes = 1500 + +var clientBroadcast = net.IPv4bcast + +func replyTo(peer net.Addr) net.Addr { + udp, ok := peer.(*net.UDPAddr) + if !ok { + return peer + } + if udp.IP == nil || udp.IP.IsUnspecified() { + return &net.UDPAddr{IP: clientBroadcast, Port: udp.Port} + } + return udp +} + +func (s *Store) serveDatagram(conn net.PacketConn, raw []byte, peer net.Addr, logger *slog.Logger) { + defer func() { + if r := recover(); r != nil { + logger.Error("dhcp datagram handling panicked", "peer", peer, "panic", r) + } + }() + + req, err := dhcpv4.FromBytes(raw) + if err != nil { + logger.Debug("malformed dhcp datagram", "peer", peer, "error", err) + return + } + + reply, err := s.Handle(req) + if err != nil { + logger.Error("building dhcp reply failed", + "peer", peer, "mac", req.ClientHWAddr, "type", req.MessageType(), "error", err) + return + } + if reply == nil { + logger.Debug("no reply for datagram", "mac", req.ClientHWAddr, "type", req.MessageType()) + return + } + + target := replyTo(peer) + if _, err := conn.WriteTo(reply.ToBytes(), target); err != nil { + logger.Error("sending dhcp reply failed", "target", target, "mac", req.ClientHWAddr, "error", err) + return + } + logger.Info("dhcp reply sent", + "mac", req.ClientHWAddr, "type", reply.MessageType(), "ip", reply.YourIPAddr, "target", target) +} + +func (s *Store) Serve(conn net.PacketConn, logger *slog.Logger) error { + buf := make([]byte, MaxDatagramBytes) + + for { + n, peer, err := conn.ReadFrom(buf) + if err != nil { + return err + } + s.serveDatagram(conn, buf[:n], peer, logger) + } +} diff --git a/internal/dhcpd/listener_test.go b/internal/dhcpd/listener_test.go new file mode 100644 index 0000000..26000a7 --- /dev/null +++ b/internal/dhcpd/listener_test.go @@ -0,0 +1,206 @@ +package dhcpd + +import ( + "io" + "log/slog" + "net" + "sync" + "testing" + "time" + + "github.com/insomniacslk/dhcp/dhcpv4" +) + +func discard() *slog.Logger { + return slog.New(slog.NewTextHandler(io.Discard, nil)) +} + +func loopbackPair(t *testing.T) (*net.UDPConn, *net.UDPConn) { + t.Helper() + + server, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1)}) + if err != nil { + t.Fatalf("ListenUDP server: %v", err) + } + t.Cleanup(func() { server.Close() }) + + client, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1)}) + if err != nil { + t.Fatalf("ListenUDP client: %v", err) + } + t.Cleanup(func() { client.Close() }) + + return server, client +} + +func exchange(t *testing.T, s *Store, raw []byte) *dhcpv4.DHCPv4 { + t.Helper() + + server, client := loopbackPair(t) + go s.Serve(server, discard()) + + if _, err := client.WriteToUDP(raw, server.LocalAddr().(*net.UDPAddr)); err != nil { + t.Fatalf("WriteToUDP: %v", err) + } + + if err := client.SetReadDeadline(time.Now().Add(500 * time.Millisecond)); err != nil { + t.Fatalf("SetReadDeadline: %v", err) + } + buf := make([]byte, MaxDatagramBytes) + n, _, err := client.ReadFromUDP(buf) + if err != nil { + return nil + } + + reply, err := dhcpv4.FromBytes(buf[:n]) + if err != nil { + t.Fatalf("the reply must be a valid dhcp packet: %v", err) + } + return reply +} + +func TestServe_AnswersAKnownMAC(t *testing.T) { + s := configuredStore(t) + + reply := exchange(t, s, request(t, dhcpv4.MessageTypeDiscover, mac(t, "00:22:33:00:00:0a")).ToBytes()) + if reply == nil { + t.Fatal("a known mac must be answered on the wire") + } + if reply.MessageType() != dhcpv4.MessageTypeOffer { + t.Errorf("message type = %s, want OFFER", reply.MessageType()) + } + if !reply.YourIPAddr.Equal(net.ParseIP("10.0.5.10")) { + t.Errorf("yiaddr = %s, want 10.0.5.10", reply.YourIPAddr) + } +} + +func TestServe_StaysSilentForAnUnknownMAC(t *testing.T) { + s := configuredStore(t) + + if reply := exchange(t, s, request(t, dhcpv4.MessageTypeDiscover, mac(t, "00:22:33:ff:ff:ff")).ToBytes()); reply != nil { + t.Errorf("an unknown mac must get nothing on the wire, got %s", reply.MessageType()) + } +} + +func TestServe_StaysSilentOnARelease(t *testing.T) { + s := configuredStore(t) + + if reply := exchange(t, s, request(t, dhcpv4.MessageTypeRelease, mac(t, "00:22:33:00:00:0a")).ToBytes()); reply != nil { + t.Errorf("a RELEASE must get nothing on the wire, got %s", reply.MessageType()) + } +} + +func TestServe_SurvivesAMalformedDatagram(t *testing.T) { + s := configuredStore(t) + server, client := loopbackPair(t) + go s.Serve(server, discard()) + + target := server.LocalAddr().(*net.UDPAddr) + for _, garbage := range [][]byte{{}, {0x01}, make([]byte, 1200)} { + if _, err := client.WriteToUDP(garbage, target); err != nil { + t.Fatalf("WriteToUDP: %v", err) + } + } + + if _, err := client.WriteToUDP(request(t, dhcpv4.MessageTypeDiscover, mac(t, "00:22:33:00:00:0a")).ToBytes(), target); err != nil { + t.Fatalf("WriteToUDP: %v", err) + } + if err := client.SetReadDeadline(time.Now().Add(time.Second)); err != nil { + t.Fatalf("SetReadDeadline: %v", err) + } + buf := make([]byte, MaxDatagramBytes) + if _, _, err := client.ReadFromUDP(buf); err != nil { + t.Fatalf("the loop must survive garbage and keep serving: %v", err) + } +} + +func TestServe_ReturnsWhenTheConnectionCloses(t *testing.T) { + s := configuredStore(t) + server, _ := loopbackPair(t) + + done := make(chan error, 1) + go func() { done <- s.Serve(server, discard()) }() + + server.Close() + select { + case err := <-done: + if err == nil { + t.Error("Serve must report why it stopped") + } + case <-time.After(time.Second): + t.Fatal("Serve did not return after the connection closed") + } +} + +type explodingConn struct { + net.PacketConn + mu sync.Mutex + writes int +} + +func (c *explodingConn) WriteTo(b []byte, addr net.Addr) (int, error) { + c.mu.Lock() + first := c.writes == 0 + c.writes++ + c.mu.Unlock() + + if first { + panic("write exploded") + } + return c.PacketConn.WriteTo(b, addr) +} + +func TestServe_SurvivesAPanicWhileHandlingADatagram(t *testing.T) { + s := configuredStore(t) + server, client := loopbackPair(t) + go s.Serve(&explodingConn{PacketConn: server}, discard()) + + target := server.LocalAddr().(*net.UDPAddr) + raw := request(t, dhcpv4.MessageTypeDiscover, mac(t, "00:22:33:00:00:0a")).ToBytes() + + for range 2 { + if _, err := client.WriteToUDP(raw, target); err != nil { + t.Fatalf("WriteToUDP: %v", err) + } + time.Sleep(50 * time.Millisecond) + } + + if err := client.SetReadDeadline(time.Now().Add(time.Second)); err != nil { + t.Fatalf("SetReadDeadline: %v", err) + } + buf := make([]byte, MaxDatagramBytes) + if _, _, err := client.ReadFromUDP(buf); err != nil { + t.Fatalf("a panic on one datagram must not kill the serving loop: %v", err) + } +} + +func TestReplyTo_BroadcastsWhenTheClientHasNoAddress(t *testing.T) { + got := replyTo(&net.UDPAddr{IP: net.IPv4zero, Port: 68}) + + udp, ok := got.(*net.UDPAddr) + if !ok { + t.Fatalf("target = %T, want *net.UDPAddr", got) + } + if !udp.IP.Equal(net.IPv4bcast) { + t.Errorf("target = %s, want 255.255.255.255: the client cannot be reached by unicast yet", udp.IP) + } + if udp.Port != 68 { + t.Errorf("port = %d, want the client port to be kept", udp.Port) + } +} + +func TestReplyTo_KeepsTheUnicastPeerWhenItHasAnAddress(t *testing.T) { + got := replyTo(&net.UDPAddr{IP: net.ParseIP("10.0.5.10"), Port: 68}) + + udp := got.(*net.UDPAddr) + if !udp.IP.Equal(net.ParseIP("10.0.5.10")) { + t.Errorf("target = %s, want the renewing client itself", udp.IP) + } +} + +func TestReplyTo_BroadcastsWhenThePeerIPIsNil(t *testing.T) { + udp := replyTo(&net.UDPAddr{Port: 68}).(*net.UDPAddr) + if !udp.IP.Equal(net.IPv4bcast) { + t.Errorf("target = %s, want 255.255.255.255", udp.IP) + } +} diff --git a/scripts/deploy.sh b/scripts/deploy.sh index 07d7668..d6b1828 100755 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -116,7 +116,7 @@ start_services () { profile_units () { case "${1}" in - kvm) echo "agent.service dnsmasq@.service metadata@.service" ;; + kvm) echo "agent.service dnsmasq@.service dhcp@.service metadata@.service" ;; intel) echo "" ;; *) return 1 ;; esac @@ -124,7 +124,7 @@ profile_units () { profile_binaries () { case "${1}" in - kvm) echo "agent metadata run-dnsmasq-in-netns.sh" ;; + kvm) echo "agent metadata dhcp run-dnsmasq-in-netns.sh run-dhcp-in-netns.sh" ;; intel) echo "" ;; *) return 1 ;; esac diff --git a/scripts/run-dhcp-in-netns.sh b/scripts/run-dhcp-in-netns.sh new file mode 100644 index 0000000..c09050f --- /dev/null +++ b/scripts/run-dhcp-in-netns.sh @@ -0,0 +1,25 @@ +#!/bin/bash +set -e + +# Expects one argument: netns_bridge (e.g. vpc-00003_br-00002 or vpc1_br0) +# The netns is only needed here, to enter it. The server is handed its bridge +# and its two file paths, and knows nothing of the namespace it runs in. +arg="$1" +NETNS="${arg%%_*}" +BRIDGE="${arg#*_}" +RUN_DIR="/run/two/dhcp" + +if [[ "${NETNS}" == "${arg}" || -z "${NETNS}" || -z "${BRIDGE}" ]] +then + echo "instance ${arg} is not _" >&2 + exit 1 +fi + +echo "start dhcp ${arg}" + +exec ip netns exec "${NETNS}" \ + /opt/two/bin/dhcp \ + -conf /etc/two/agent.yml \ + -interface "${BRIDGE}" \ + -state "${RUN_DIR}/${arg}.state" \ + -socket "${RUN_DIR}/${arg}.sock" diff --git a/systemd/dhcp@.service b/systemd/dhcp@.service new file mode 100644 index 0000000..e29ff73 --- /dev/null +++ b/systemd/dhcp@.service @@ -0,0 +1,10 @@ +[Unit] +Description=two dhcp server in netns %i +After=network.target + +[Service] +Type=simple +ExecStart=/opt/two/bin/run-dhcp-in-netns.sh %i + +[Install] +WantedBy=multi-user.target