commit 6b9ce5728a94efc5c56a70ab75aa025637757050 Author: Michael Stapelberg Date: Sun May 27 17:30:42 2018 +0200 Initial commit diff --git a/cmd/dhcp4/dhcp4.go b/cmd/dhcp4/dhcp4.go new file mode 100644 index 0000000..5158807 --- /dev/null +++ b/cmd/dhcp4/dhcp4.go @@ -0,0 +1,63 @@ +// Binary dhcp4 obtains a DHCPv4 lease, persists its contents to +// /perm/dhcp4/wire/lease.json and notifies netconfigd. +package main + +import ( + "encoding/json" + "flag" + "fmt" + "io/ioutil" + "log" + "net" + "os" + "path/filepath" + "syscall" + "time" + + "router7/internal/dhcp4" + "router7/internal/notify" +) + +func logic() error { + const configPath = "/perm/dhcp4/wire/lease.json" + if err := os.MkdirAll(filepath.Dir(configPath), 0755); err != nil { + return err + } + iface, err := net.InterfaceByName("uplink0") + if err != nil { + return err + } + c := dhcp4.Client{ + Interface: iface, + } + for c.ObtainOrRenew() { + if err := c.Err(); err != nil { + log.Printf("Temporary error: %v", err) + continue + } + // TODO: use a logger which writes to /dev/console + log.Printf("lease: %+v", c.Config()) + ioutil.WriteFile("/dev/console", []byte(fmt.Sprintf("lease: %+v\n", c.Config())), 0600) + b, err := json.Marshal(c.Config()) + if err != nil { + return err + } + if err := ioutil.WriteFile(configPath, b, 0644); err != nil { + return err + } + if err := notify.Process("/user/netconfi", syscall.SIGUSR1); err != nil { + log.Printf("notifying netconfig: %v", err) + ioutil.WriteFile("/dev/console", []byte(fmt.Sprintf("notifying netconfigd: %+v\n", err)), 0600) + } + time.Sleep(time.Until(c.Config().RenewAfter)) + } + return c.Err() // permanent error +} + +func main() { + // TODO: drop privileges, run as separate uid? + flag.Parse() + if err := logic(); err != nil { + log.Fatal(err) + } +} diff --git a/cmd/dhcp4d/dhcp4d.go b/cmd/dhcp4d/dhcp4d.go new file mode 100644 index 0000000..2a1388e --- /dev/null +++ b/cmd/dhcp4d/dhcp4d.go @@ -0,0 +1,57 @@ +// Binary dhcp4d hands out DHCPv4 leases to clients. +package main + +import ( + "encoding/json" + "flag" + "fmt" + "io/ioutil" + "log" + "os" + "syscall" + + "router7/internal/dhcp4d" + "router7/internal/notify" + + "github.com/krolaw/dhcp4" + "github.com/krolaw/dhcp4/conn" +) + +func logic() error { + if err := os.MkdirAll("/perm/dhcp4d", 0755); err != nil { + return err + } + errs := make(chan error) + handler := dhcp4d.NewHandler() + handler.Leases = func(leases []*dhcp4d.Lease) { + b, err := json.Marshal(leases) + if err != nil { + errs <- err + return + } + // TODO: write atomically + if err := ioutil.WriteFile("/perm/dhcp4d/leases.json", b, 0644); err != nil { + errs <- err + } + if err := notify.Process("/user/dnsd", syscall.SIGUSR1); err != nil { + log.Printf("notifying dnsd: %v", err) + ioutil.WriteFile("/dev/console", []byte(fmt.Sprintf("notifying dnsd: %+v\n", err)), 0600) + } + } + conn, err := conn.NewUDP4BoundListener("lan0", ":67") // TODO: customizeable + if err != nil { + return err + } + go func() { + errs <- dhcp4.Serve(conn, handler) + }() + return <-errs +} + +func main() { + // TODO: drop privileges, run as separate uid? + flag.Parse() + if err := logic(); err != nil { + log.Fatal(err) + } +} diff --git a/cmd/dhcp6/dhcp6.go b/cmd/dhcp6/dhcp6.go new file mode 100644 index 0000000..5e16cbd --- /dev/null +++ b/cmd/dhcp6/dhcp6.go @@ -0,0 +1,60 @@ +// Binary dhcp6 obtains a DHCPv6 lease, persists its contents to +// /perm/dhcp6/wire/lease.json and notifies netconfigd. +package main + +import ( + "encoding/json" + "flag" + "fmt" + "io/ioutil" + "log" + "os" + "path/filepath" + "router7/internal/dhcp6" + "router7/internal/notify" + "syscall" + "time" +) + +func logic() error { + const configPath = "/perm/dhcp6/wire/lease.json" + if err := os.MkdirAll(filepath.Dir(configPath), 0755); err != nil { + return err + } + + c, err := dhcp6.NewClient(dhcp6.ClientConfig{ + InterfaceName: "uplink0", + }) + if err != nil { + return err + } + for c.ObtainOrRenew() { + if err := c.Err(); err != nil { + log.Printf("Temporary error: %v", err) + continue + } + // TODO: use a logger which writes to /dev/console + log.Printf("lease: %+v", c.Config()) + ioutil.WriteFile("/dev/console", []byte(fmt.Sprintf("lease: %+v\n", c.Config())), 0600) + b, err := json.Marshal(c.Config()) + if err != nil { + return err + } + if err := ioutil.WriteFile(configPath, b, 0644); err != nil { + return err + } + if err := notify.Process("/user/netconfi", syscall.SIGUSR1); err != nil { + log.Printf("notifying netconfig: %v", err) + ioutil.WriteFile("/dev/console", []byte(fmt.Sprintf("notifying netconfigd: %+v\n", err)), 0600) + } + time.Sleep(time.Until(c.Config().RenewAfter)) + } + return c.Err() // permanent error +} + +func main() { + flag.Parse() + if err := logic(); err != nil { + log.Fatal(err) + } +} diff --git a/cmd/dhcp6/router7.test b/cmd/dhcp6/router7.test new file mode 100755 index 0000000..57d4bd3 Binary files /dev/null and b/cmd/dhcp6/router7.test differ diff --git a/cmd/dnsd/dnsd.go b/cmd/dnsd/dnsd.go new file mode 100644 index 0000000..82a44fe --- /dev/null +++ b/cmd/dnsd/dnsd.go @@ -0,0 +1,54 @@ +// Binary dnsd answers DNS requests by forwarding or consulting DHCP leases. +package main + +import ( + "encoding/json" + "flag" + "io/ioutil" + "log" + "os" + "os/signal" + "syscall" + + "router7/internal/dhcp4d" + "router7/internal/dns" +) + +func logic() error { + // TODO: serve on correct IP address + // TODO: set correct upstream DNS resolver(s) + srv := dns.NewServer("192.168.42.1:53", "lan") + readLeases := func() error { + b, err := ioutil.ReadFile("/perm/dhcp4d/leases.json") + if err != nil { + return err + } + var leases []dhcp4d.Lease + if err := json.Unmarshal(b, &leases); err != nil { + return err + } + srv.SetLeases(leases) + return nil + } + if err := readLeases(); err != nil { + log.Printf("readLeases: %v", err) + } + ch := make(chan os.Signal, 1) + signal.Notify(ch, syscall.SIGUSR1) + go func() { + for range ch { + if err := readLeases(); err != nil { + log.Printf("readLeases: %v", err) + } + } + }() + return srv.ListenAndServe() +} + +func main() { + // TODO: drop privileges, run as separate uid? + flag.Parse() + if err := logic(); err != nil { + log.Fatal(err) + } +} diff --git a/cmd/netconfigd/netconfigd.go b/cmd/netconfigd/netconfigd.go new file mode 100644 index 0000000..8fa09ad --- /dev/null +++ b/cmd/netconfigd/netconfigd.go @@ -0,0 +1,41 @@ +// Binary netconfigd reads state from dhcp4, dhcp6, … and applies it. +package main + +import ( + "flag" + "fmt" + "io/ioutil" + "log" + "os" + "os/signal" + "syscall" + + "router7/internal/netconfig" +) + +var ( + linger = flag.Bool("linger", true, "linger around after applying the configuration (until killed)") +) + +func logic() error { + ch := make(chan os.Signal, 1) + signal.Notify(ch, syscall.SIGUSR1) + for { + if err := netconfig.Apply("uplink0", "/perm/"); err != nil { + return err + } + if *linger { + <-ch + } + } + return nil +} + +func main() { + flag.Parse() + if err := logic(); err != nil { + // TODO: use a logger which writes to /dev/console + ioutil.WriteFile("/dev/console", []byte(fmt.Sprintf("netconfig: %v\n", err)), 0600) + log.Fatal(err) + } +} diff --git a/cmd/router7/main.go b/cmd/router7/main.go new file mode 100644 index 0000000..c538f67 --- /dev/null +++ b/cmd/router7/main.go @@ -0,0 +1,17 @@ +package main + +import ( + "flag" + "log" +) + +func logic() error { + return nil +} + +func main() { + flag.Parse() + if err := logic(); err != nil { + log.Fatal(err) + } +} diff --git a/integrationdhcpv4_test.go b/integrationdhcpv4_test.go new file mode 100644 index 0000000..e877096 --- /dev/null +++ b/integrationdhcpv4_test.go @@ -0,0 +1,238 @@ +package integration_test + +import ( + "bytes" + "fmt" + "io/ioutil" + "log" + "net" + "os" + "os/exec" + "strconv" + "strings" + "testing" + "time" + + "golang.org/x/sys/unix" + + "github.com/d2g/dhcp4" + "github.com/d2g/dhcp4client" + "github.com/google/gopacket" + "github.com/google/gopacket/layers" + "github.com/google/gopacket/pcap" + "github.com/google/gopacket/pcapgo" +) + +var ( + hardwareAddr net.HardwareAddr +) + +func addHostname(p *dhcp4.Packet) { + var utsname unix.Utsname + if err := unix.Uname(&utsname); err != nil { + log.Fatal(err) + } + nnb := utsname.Nodename[:bytes.IndexByte(utsname.Nodename[:], 0)] + p.AddOption(dhcp4.OptionHostName, nnb) +} + +func addClientId(p *dhcp4.Packet) { + id := make([]byte, len(hardwareAddr)+1) + id[0] = 1 // hardware type ethernet, https://tools.ietf.org/html/rfc1700 + copy(id[1:], hardwareAddr) + p.AddOption(dhcp4.OptionClientIdentifier, id) +} + +// dhcpRequest is a copy of (dhcp4client/Client).Request which +// includes the hostname. +func dhcpRequest(c *dhcp4client.Client) (bool, dhcp4.Packet, error) { + discoveryPacket := c.DiscoverPacket() + addHostname(&discoveryPacket) + addClientId(&discoveryPacket) + discoveryPacket.PadToMinSize() + + if err := c.SendPacket(discoveryPacket); err != nil { + return false, discoveryPacket, err + } + + offerPacket, err := c.GetOffer(&discoveryPacket) + if err != nil { + return false, offerPacket, err + } + + requestPacket := c.RequestPacket(&offerPacket) + addHostname(&requestPacket) + addClientId(&requestPacket) + requestPacket.PadToMinSize() + + if err := c.SendPacket(requestPacket); err != nil { + return false, requestPacket, err + } + + acknowledgement, err := c.GetAcknowledgement(&requestPacket) + if err != nil { + return false, acknowledgement, err + } + + acknowledgementOptions := acknowledgement.ParseOptions() + if dhcp4.MessageType(acknowledgementOptions[dhcp4.OptionDHCPMessageType][0]) != dhcp4.ACK { + return false, acknowledgement, nil + } + + return true, acknowledgement, nil +} + +type connection interface { + Close() error + Write(packet []byte) error + ReadFrom() ([]byte, net.IP, error) + SetReadTimeout(t time.Duration) error +} +type replayer struct { + underlying connection +} + +func (r *replayer) Close() error { return r.underlying.Close() } +func (r *replayer) Write(b []byte) error { return r.underlying.Write(b) } +func (r *replayer) SetReadTimeout(t time.Duration) error { return r.underlying.SetReadTimeout(t) } + +func (r *replayer) ReadFrom() ([]byte, net.IP, error) { + d, ip, err := r.underlying.ReadFrom() + log.Printf("d = %+v, ip = %v, err = %v", d, ip, err) + return d, ip, err +} + +func dhcp() error { + v0, err := net.InterfaceByName("veth0a") + if err != nil { + return err + } + + hardwareAddr = v0.HardwareAddr + + pktsock, err := dhcp4client.NewPacketSock(v0.Index) + if err != nil { + return err + } + dhcp, err := dhcp4client.New( + dhcp4client.HardwareAddr(v0.HardwareAddr), + dhcp4client.Timeout(5*time.Second), + dhcp4client.Broadcast(false), + dhcp4client.Connection(&replayer{underlying: pktsock}), + ) + if err != nil { + return err + } + + //ok, ack, err := dhcpRequest(dhcp) + fmt.Println(dhcpRequest(dhcp)) + return nil +} + +func TestDHCPv4(t *testing.T) { + const ns = "ns0" // name of the network namespace to use for this test + + if err := exec.Command("ip", "netns", "add", ns).Run(); err != nil { + t.Fatalf("ip netns add %s: %v", ns, err) + } + defer exec.Command("ip", "netns", "delete", ns).Run() + + nsSetup := []*exec.Cmd{ + exec.Command("ip", "link", "add", "veth0a", "type", "veth", "peer", "name", "veth0b", "netns", ns), + exec.Command("ip", "link", "set", "veth0a", "up"), + exec.Command("ip", "netns", "exec", ns, "ip", "addr", "add", "192.168.23.1/24", "dev", "veth0b"), + exec.Command("ip", "netns", "exec", ns, "ip", "link", "set", "veth0b", "up"), + exec.Command("ip", "netns", "exec", ns, "ip", "link", "set", "veth0b"), + } + + for _, cmd := range nsSetup { + if err := cmd.Run(); err != nil { + t.Fatalf("%v: %v", cmd.Args, err) + } + } + + ready, err := ioutil.TempFile("", "router7") + if err != nil { + t.Fatal(err) + } + ready.Close() // dnsmasq will re-create the file anyway + defer os.Remove(ready.Name()) // dnsmasq does not clean up its pid file + + dnsmasq := exec.Command("ip", "netns", "exec", ns, "dnsmasq", + "--keep-in-foreground", // cannot use --no-daemon because we need --pid-file + "--log-facility=-", // log to stderr + "--pid-file="+ready.Name(), + "--bind-interfaces", + "--interface=veth0b", + "--dhcp-range=192.168.23.2,192.168.23.10", + "--dhcp-range=::1,::10,constructor:veth0b", + "--dhcp-authoritative", // eliminate timeouts + "--no-ping", // disable ICMP confirmation of unused addresses to eliminate tedious timeout + "--leasefile-ro", // do not create a lease database + ) + dnsmasq.Stdout = os.Stdout + dnsmasq.Stderr = os.Stderr + if err := dnsmasq.Start(); err != nil { + t.Fatal(err) + } + done := false // TODO: fix data race + go func() { + err := dnsmasq.Wait() + if !done { + t.Fatalf("dnsmasq exited prematurely: %v", err) + } + }() + defer func() { + done = true + dnsmasq.Process.Kill() + }() + + // TODO(later): use inotify instead of polling + // Wait for dnsmasq to write its process id, at which point it is already + // listening for requests. + for { + b, err := ioutil.ReadFile(ready.Name()) + if err != nil { + t.Fatal(err) + } + if strings.TrimSpace(string(b)) == strconv.Itoa(dnsmasq.Process.Pid) { + break + } + time.Sleep(10 * time.Millisecond) + } + + f, err := os.Create("/tmp/pcap") + if err != nil { + t.Fatal(err) + } + defer f.Close() + pcapw := pcapgo.NewWriter(f) + if err := pcapw.WriteFileHeader(1600, layers.LinkTypeEthernet); err != nil { + t.Fatal(err) + } + handle, err := pcap.OpenLive("veth0a", 1600, true, pcap.BlockForever) + if err != nil { + t.Fatal(err) + } + pkgsrc := gopacket.NewPacketSource(handle, handle.LinkType()) + closed := make(chan struct{}) + go func() { + for packet := range pkgsrc.Packets() { + if packet.Layer(layers.LayerTypeDHCPv4) != nil { + log.Printf("packet: %+v", packet) + if err := pcapw.WritePacket(packet.Metadata().CaptureInfo, packet.Data()); err != nil { + t.Fatalf("pcap.WritePacket(): %v", err) + } + } + } + close(closed) + }() + // TODO: test the capture daemon + + if err := dhcp(); err != nil { + t.Fatal(err) + } + time.Sleep(1 * time.Second) + handle.Close() + <-closed +} diff --git a/integrationdhcpv6_test.go b/integrationdhcpv6_test.go new file mode 100644 index 0000000..4230a3e --- /dev/null +++ b/integrationdhcpv6_test.go @@ -0,0 +1,145 @@ +package integration_test + +import ( + "io/ioutil" + "os" + "os/exec" + "strconv" + "strings" + "testing" + "time" + + "router7/internal/dhcp6" + + "github.com/google/go-cmp/cmp" +) + +func TestDHCPv6(t *testing.T) { + const ns = "ns0" // name of the network namespace to use for this test + + if err := exec.Command("ip", "netns", "add", ns).Run(); err != nil { + t.Fatalf("ip netns add %s: %v", ns, err) + } + defer exec.Command("ip", "netns", "delete", ns).Run() + + nsSetup := []*exec.Cmd{ + exec.Command("ip", "link", "add", "veth0a", "type", "veth", "peer", "name", "veth0b", "netns", ns), + + // Disable Duplicate Address Detection: until DAD completes, the link-local + // address remains in state “tentative”, resulting in any attempts to + // bind(2) to the address to fail with -EADDRNOTAVAIL. + exec.Command("/bin/sh", "-c", "echo 0 > /proc/sys/net/ipv6/conf/veth0a/accept_dad"), + exec.Command("ip", "netns", "exec", ns, "/bin/sh", "-c", "echo 0 > /proc/sys/net/ipv6/conf/veth0b/accept_dad"), + + exec.Command("ip", "link", "set", "veth0a", "up"), + exec.Command("ip", "netns", "exec", ns, "ip", "addr", "add", "192.168.23.1/24", "dev", "veth0b"), + exec.Command("ip", "netns", "exec", ns, "ip", "addr", "add", "2001:db8::1/64", "dev", "veth0b"), + exec.Command("ip", "netns", "exec", ns, "ip", "link", "set", "veth0b", "up"), + } + + for _, cmd := range nsSetup { + if err := cmd.Run(); err != nil { + t.Fatalf("%v: %v", cmd.Args, err) + } + } + + ready, err := ioutil.TempFile("", "router7") + if err != nil { + t.Fatal(err) + } + ready.Close() // dnsmasq will re-create the file anyway + defer os.Remove(ready.Name()) // dnsmasq does not clean up its pid file + + dnsmasq := exec.Command("ip", "netns", "exec", ns, "dnsmasq", + "--keep-in-foreground", // cannot use --no-daemon because we need --pid-file + "--log-facility=-", // log to stderr + "--pid-file="+ready.Name(), + "--bind-interfaces", + "--interface=veth0b", + "--dhcp-range=192.168.23.2,192.168.23.10", + "--dhcp-range=::1,::10,constructor:veth0b", + "--dhcp-authoritative", // eliminate timeouts + "--no-ping", // disable ICMP confirmation of unused addresses to eliminate tedious timeout + "--leasefile-ro", // do not create a lease database + ) + dnsmasq.Stdout = os.Stdout + dnsmasq.Stderr = os.Stderr + if err := dnsmasq.Start(); err != nil { + t.Fatal(err) + } + done := false // TODO: fix data race + go func() { + err := dnsmasq.Wait() + if !done { + t.Fatalf("dnsmasq exited prematurely: %v", err) + } + }() + defer func() { + done = true + dnsmasq.Process.Kill() + }() + + // TODO(later): use inotify instead of polling + // Wait for dnsmasq to write its process id, at which point it is already + // listening for requests. + for { + b, err := ioutil.ReadFile(ready.Name()) + if err != nil { + t.Fatal(err) + } + if strings.TrimSpace(string(b)) == strconv.Itoa(dnsmasq.Process.Pid) { + break + } + time.Sleep(10 * time.Millisecond) + } + + // f, err := os.Create("/tmp/pcap") + // if err != nil { + // t.Fatal(err) + // } + // defer f.Close() + // pcapw := pcapgo.NewWriter(f) + // if err := pcapw.WriteFileHeader(1600, layers.LinkTypeEthernet); err != nil { + // t.Fatal(err) + // } + // handle, err := pcap.OpenLive("veth0a", 1600, true, pcap.BlockForever) + // if err != nil { + // t.Fatal(err) + // } + // pkgsrc := gopacket.NewPacketSource(handle, handle.LinkType()) + // closed := make(chan struct{}) + // go func() { + // for packet := range pkgsrc.Packets() { + // if packet.Layer(layers.LayerTypeDHCPv4) != nil { + // log.Printf("packet: %+v", packet) + // if err := pcapw.WritePacket(packet.Metadata().CaptureInfo, packet.Data()); err != nil { + // t.Fatalf("pcap.WritePacket(): %v", err) + // } + // } + // } + // close(closed) + // }() + // // TODO: test the capture daemon + + c, err := dhcp6.NewClient(dhcp6.ClientConfig{ + InterfaceName: "veth0a", + }) + if err != nil { + t.Fatal(err) + } + c.ObtainOrRenew() + if err := c.Err(); err != nil { + t.Fatal(err) + } + got := c.Config() + want := dhcp6.Config{ + DNS: []string{"2001:db8::1"}, + } + if diff := cmp.Diff(got, want); diff != "" { + t.Fatalf("unexpected config: diff (-got +want):\n%s", diff) + } + + // time.Sleep(1 * time.Second) + // handle.Close() + // <-closed +} diff --git a/integrationdns_test.go b/integrationdns_test.go new file mode 100644 index 0000000..48bff6e --- /dev/null +++ b/integrationdns_test.go @@ -0,0 +1,25 @@ +package integration_test + +import ( + "os" + "os/exec" + "strconv" + "strings" + "testing" + + "router7/internal/dns" +) + +func TestDNS(t *testing.T) { + go dns.NewServer("localhost:4453", "lan").ListenAndServe() + const port = 4453 + dig := exec.Command("dig", "-p", strconv.Itoa(port), "+timeout=1", "+short", "-x", "8.8.8.8", "@127.0.0.1") + dig.Stderr = os.Stderr + out, err := dig.Output() + if err != nil { + t.Fatal(err) + } + if got, want := strings.TrimSpace(string(out)), "google-public-dns-a.google.com."; got != want { + t.Fatalf("dig -x 8.8.8.8: unexpected reply: got %q, want %q", got, want) + } +} diff --git a/integrationnetconfig_test.go b/integrationnetconfig_test.go new file mode 100644 index 0000000..b448570 --- /dev/null +++ b/integrationnetconfig_test.go @@ -0,0 +1,153 @@ +package integration_test + +import ( + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "regexp" + "router7/internal/netconfig" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" +) + +const goldenInterfaces = ` +{ + "interfaces":[ + { + "hardware_addr": "02:73:53:00:ca:fe", + "name": "dummy23" + } + ] +} +` + +const goldenDhcp4 = ` +{ + "valid_until":"2018-05-18T23:46:04.429895261+02:00", + "client_ip":"85.195.207.62", + "subnet_mask":"255.255.255.128", + "router":"85.195.207.1", + "dns":[ + "77.109.128.2", + "213.144.129.20" + ] +} +` + +const goldenDhcp6 = ` +{ + "valid_until":"0001-01-01T00:00:00Z", + "prefixes":[ + {"IP":"2a02:168:4a00::","Mask":"////////AAAAAAAAAAAAAA=="} + ], + "dns":[ + "2001:1620:2777:1::10", + "2001:1620:2777:2::20" + ] +} +` + +func TestNetconfig(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 + }{ + {"dhcp4/wire/lease.json", goldenDhcp4}, + {"dhcp6/wire/lease.json", goldenDhcp6}, + {"interfaces.json", goldenInterfaces}, + } { + 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 := netconfig.Apply("dummy23", tmp); err != nil { + t.Fatalf("netconfig.Apply: %v", err) + } + + return + } + const ns = "ns1" // name of the network namespace to use for this test + + if err := exec.Command("ip", "netns", "add", ns).Run(); err != nil { + t.Fatalf("ip netns add %s: %v", ns, err) + } + defer exec.Command("ip", "netns", "delete", ns).Run() + + nsSetup := []*exec.Cmd{ + exec.Command("ip", "netns", "exec", ns, "ip", "link", "add", "dummy0", "type", "dummy"), + exec.Command("ip", "netns", "exec", ns, "ip", "link", "set", "dummy0", "address", "02:73:53:00:ca:fe"), + } + + 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=^TestNetconfig$") + 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) + } + + addrs, err := exec.Command("ip", "netns", "exec", ns, "ip", "address", "show", "dev", "dummy23").Output() + if err != nil { + t.Fatal(err) + } + + addrRe := regexp.MustCompile(`(?m)^\s*inet 85.195.207.62/25 brd 85.195.207.127 scope global dummy23$`) + if !addrRe.MatchString(string(addrs)) { + t.Fatalf("regexp %s does not match %s", addrRe, string(addrs)) + } + addr6Re := regexp.MustCompile(`(?m)^\s*inet6 2a02:168:4a00::1/48 scope global\s*$`) + if !addr6Re.MatchString(string(addrs)) { + t.Fatalf("regexp %s does not match %s", addr6Re, string(addrs)) + } + + wantRoutes := []string{ + "default via 85.195.207.1 proto dhcp src 85.195.207.62 ", + "85.195.207.0/25 proto kernel scope link src 85.195.207.62 ", + "85.195.207.1 proto dhcp scope link src 85.195.207.62", + } + + out, err := exec.Command("ip", "netns", "exec", ns, "ip", "route", "show", "dev", "dummy23").Output() + if err != nil { + t.Fatal(err) + } + routes := strings.Split(strings.TrimSpace(string(out)), "\n") + + if diff := cmp.Diff(routes, wantRoutes); diff != "" { + t.Fatalf("routes: diff (-got +want):\n%s", diff) + } + + out, err = exec.Command("ip", "netns", "exec", ns, "iptables", "-t", "nat", "-L", "POSTROUTING", "-v").Output() + if err != nil { + t.Fatal(err) + } + rules := strings.Split(strings.TrimSpace(string(out)), "\n") + for n, rule := range rules { + t.Logf("rule %d: %s", n, rule) + } + if len(rules) < 3 { + t.Fatalf("iptables rules in nat table POSTROUTING not found") + } + wantRule := " 0 0 MASQUERADE all -- any uplink0 anywhere anywhere" + if got, want := rules[2], wantRule; got != want { + t.Fatalf("unexpected iptables rule: got %q, want %q", got, want) + } +} diff --git a/internal/dhcp4/dhcp4.go b/internal/dhcp4/dhcp4.go new file mode 100644 index 0000000..a378241 --- /dev/null +++ b/internal/dhcp4/dhcp4.go @@ -0,0 +1,199 @@ +// Package dhcp4 implements a DHCPv4 client. +package dhcp4 + +import ( + "bytes" + "crypto/rand" + "encoding/binary" + "fmt" + "log" + "net" + "sync" + "time" + + "golang.org/x/sys/unix" + + "github.com/d2g/dhcp4" + "github.com/d2g/dhcp4client" +) + +type Config struct { + RenewAfter time.Time `json:"valid_until"` + ClientIP string `json:"client_ip"` // e.g. 85.195.207.62 + SubnetMask string `json:"subnet_mask"` // e.g. 255.255.255.128 + Router string `json:"router"` // e.g. 85.195.207.1 + DNS []string `json:"dns"` // e.g. 77.109.128.2, 213.144.129.20 +} + +type Client struct { + Interface *net.Interface // e.g. net.InterfaceByName("eth0") + + err error + once sync.Once + dhcp *dhcp4client.Client + connection dhcp4client.ConnectionInt + hardwareAddr net.HardwareAddr + cfg Config + timeNow func() time.Time + randRead func([]byte) (int, error) +} + +// ObtainOrRenew returns false when encountering a permanent error. +func (c *Client) ObtainOrRenew() bool { + c.once.Do(func() { + if c.timeNow == nil { + c.timeNow = time.Now + } + if c.randRead == nil { + c.randRead = rand.Read + } + if c.connection == nil && c.Interface != nil { + pktsock, err := dhcp4client.NewPacketSock(c.Interface.Index) + if err != nil { + c.err = err + return + } + c.connection = pktsock + } + if c.connection == nil && c.Interface == nil { + c.err = fmt.Errorf("Interface is nil") + return + } + if c.hardwareAddr == nil { + c.hardwareAddr = c.Interface.HardwareAddr + } + dhcp, err := dhcp4client.New( + dhcp4client.HardwareAddr(c.hardwareAddr), + dhcp4client.Timeout(5*time.Second), + dhcp4client.Broadcast(false), + dhcp4client.Connection(c.connection), + ) + if err != nil { + c.err = err + return + } + dhcp.RandRead = c.randRead + c.dhcp = dhcp + }) + if c.err != nil { + return false + } + ok, ack, err := c.dhcpRequest() + if err != nil { + c.err = err + return true // temporary error + } + if !ok { + c.err = fmt.Errorf("received DHCPNAK") + return true // temporary error + } + opts := ack.ParseOptions() + + // DHCPACK (described in RFC2131 4.3.1) + // - yiaddr: IP address assigned to client + c.cfg.ClientIP = ack.YIAddr().String() + + if b, ok := opts[dhcp4.OptionSubnetMask]; ok { + mask := net.IPMask(b) + c.cfg.SubnetMask = fmt.Sprintf("%d.%d.%d.%d", mask[0], mask[1], mask[2], mask[3]) + } + + // if b, ok := opts[dhcp4.OptionBroadcastAddress]; ok { + // if err := cs.SetBroadcast(net.IP(b)); err != nil { + // log.Fatalf("setBroadcast(%v): %v", net.IP(b), err) + // } + // } + + if b, ok := opts[dhcp4.OptionRouter]; ok { + c.cfg.Router = net.IP(b).String() + } + + if b, ok := opts[dhcp4.OptionDomainNameServer]; ok { + c.cfg.DNS = nil + for len(b) > 0 { + c.cfg.DNS = append(c.cfg.DNS, net.IP(b[:4]).String()) + b = b[4:] + } + } + + leaseTime := 10 * time.Minute // seems sensible as a fallback + if b, ok := opts[dhcp4.OptionIPAddressLeaseTime]; ok && len(b) == 4 { + leaseTime = parseDHCPDuration(b) + } + + // As per RFC 2131 section 4.4.5: + // renewal time defaults to 50% of the lease time + renewalTime := time.Duration(float64(leaseTime) * 0.5) + if b, ok := opts[dhcp4.OptionRenewalTimeValue]; ok && len(b) == 4 { + renewalTime = parseDHCPDuration(b) + } + c.cfg.RenewAfter = c.timeNow().Add(renewalTime) + return true +} + +func (c *Client) Err() error { + return c.err +} + +func (c *Client) Config() Config { + return c.cfg +} + +func parseDHCPDuration(b []byte) time.Duration { + return time.Duration(binary.BigEndian.Uint32(b)) * time.Second +} + +func (c *Client) addHostname(p *dhcp4.Packet) { + var utsname unix.Utsname + if err := unix.Uname(&utsname); err != nil { + log.Fatal(err) + } + nnb := utsname.Nodename[:bytes.IndexByte(utsname.Nodename[:], 0)] + p.AddOption(dhcp4.OptionHostName, nnb) +} + +func (c *Client) addClientId(p *dhcp4.Packet) { + id := make([]byte, len(c.hardwareAddr)+1) + id[0] = 1 // hardware type ethernet, https://tools.ietf.org/html/rfc1700 + copy(id[1:], c.hardwareAddr) + p.AddOption(dhcp4.OptionClientIdentifier, id) +} + +// dhcpRequest is a copy of (dhcp4client/Client).Request which +// includes the hostname. +func (c *Client) dhcpRequest() (bool, dhcp4.Packet, error) { + discoveryPacket := c.dhcp.DiscoverPacket() + c.addHostname(&discoveryPacket) + c.addClientId(&discoveryPacket) + discoveryPacket.PadToMinSize() + + if err := c.dhcp.SendPacket(discoveryPacket); err != nil { + return false, discoveryPacket, err + } + + offerPacket, err := c.dhcp.GetOffer(&discoveryPacket) + if err != nil { + return false, offerPacket, err + } + + requestPacket := c.dhcp.RequestPacket(&offerPacket) + c.addHostname(&requestPacket) + c.addClientId(&requestPacket) + requestPacket.PadToMinSize() + + if err := c.dhcp.SendPacket(requestPacket); err != nil { + return false, requestPacket, err + } + + acknowledgement, err := c.dhcp.GetAcknowledgement(&requestPacket) + if err != nil { + return false, acknowledgement, err + } + + acknowledgementOptions := acknowledgement.ParseOptions() + if dhcp4.MessageType(acknowledgementOptions[dhcp4.OptionDHCPMessageType][0]) != dhcp4.ACK { + return false, acknowledgement, nil + } + + return true, acknowledgement, nil +} diff --git a/internal/dhcp4/dhcp4_test.go b/internal/dhcp4/dhcp4_test.go new file mode 100644 index 0000000..be48738 --- /dev/null +++ b/internal/dhcp4/dhcp4_test.go @@ -0,0 +1,95 @@ +package dhcp4 + +import ( + "fmt" + "net" + "os" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/gopacket" + "github.com/google/gopacket/layers" + "github.com/google/gopacket/pcapgo" +) + +type packet struct { + data []byte + ip net.IP + err error +} + +type replayer struct { + pcapr *pcapgo.Reader +} + +func (r *replayer) Close() error { return nil } +func (r *replayer) Write(b []byte) error { /*log.Printf("-> %v", b); */ return nil } +func (r *replayer) SetReadTimeout(t time.Duration) error { return nil } + +func (r *replayer) ReadFrom() ([]byte, net.IP, error) { + data, _, err := r.pcapr.ReadPacketData() + if err != nil { + return nil, nil, err + } + pkt := gopacket.NewPacket(data, layers.LayerTypeEthernet, gopacket.DecodeOptions{}) + // TODO: get source IP + udp := pkt.Layer(layers.LayerTypeUDP) + if udp == nil { + return nil, nil, fmt.Errorf("pcap contained unexpected non-UDP packet") + } + + //log.Printf("ReadFrom(): %v, %v, pkt = %+v", udp.LayerPayload(), err, pkt) + return udp.LayerPayload(), net.ParseIP("192.168.23.1"), err +} + +func TestDHCP4(t *testing.T) { + f, err := os.Open("testdata/fiber7.pcap") + if err != nil { + t.Fatal(err) + } + defer f.Close() + pcapr, err := pcapgo.NewReader(f) + if err != nil { + t.Fatal(err) + } + + mac, err := net.ParseMAC("d8:58:d7:00:4e:df") + if err != nil { + t.Fatal(err) + } + + now := time.Now() + c := Client{ + hardwareAddr: mac, + timeNow: func() time.Time { return now }, + connection: &replayer{pcapr: pcapr}, + randRead: func(b []byte) (n int, err error) { + if got, want := len(b), 4; got != want { + return 0, fmt.Errorf("github.com/d2g/dhcp4client request unexpected amount of bytes: got %d, want %d", got, want) + } + // TODO: read the transaction ID from the pcap file + copy(b, []byte{0x77, 0x08, 0xd7, 0x24}) + return len(b), nil + }, + } + + c.ObtainOrRenew() + if err := c.Err(); err != nil { + t.Fatalf("unexpected error: %v", err) + } + got := c.Config() + want := Config{ + RenewAfter: now.Add(13*time.Minute + 24*time.Second), + ClientIP: "85.195.207.62", + SubnetMask: "255.255.255.128", + Router: "85.195.207.1", + DNS: []string{ + "77.109.128.2", + "213.144.129.20", + }, + } + if diff := cmp.Diff(got, want); diff != "" { + t.Fatalf("unexpected config: diff (-got +want):\n%s", diff) + } +} diff --git a/internal/dhcp4/serialize_test.go b/internal/dhcp4/serialize_test.go new file mode 100644 index 0000000..33c2d26 --- /dev/null +++ b/internal/dhcp4/serialize_test.go @@ -0,0 +1,36 @@ +package dhcp4 + +import ( + "encoding/json" + "fmt" + "testing" + "time" + + "github.com/google/go-cmp/cmp" +) + +func TestSerialize(t *testing.T) { + want := Config{ + RenewAfter: time.Now().Add(30 * time.Minute), + ClientIP: "85.195.207.62", + SubnetMask: "255.255.255.128", + Router: "85.195.207.1", + DNS: []string{ + "77.109.128.2", + "213.144.129.20", + }, + } + // Round-trip through JSON to verify serialization works + b, err := json.Marshal(want) + if err != nil { + t.Fatal(err) + } + fmt.Println(string(b)) + var got Config + if err := json.Unmarshal(b, &got); err != nil { + t.Fatal(err) + } + if diff := cmp.Diff(got, want); diff != "" { + t.Fatalf("unexpected config: diff (-got +want):\n%s", diff) + } +} diff --git a/internal/dhcp4/testdata/fiber7.pcap b/internal/dhcp4/testdata/fiber7.pcap new file mode 100644 index 0000000..db152fd Binary files /dev/null and b/internal/dhcp4/testdata/fiber7.pcap differ diff --git a/internal/dhcp4d/dhcp4d.go b/internal/dhcp4d/dhcp4d.go new file mode 100644 index 0000000..6cc4199 --- /dev/null +++ b/internal/dhcp4d/dhcp4d.go @@ -0,0 +1,142 @@ +// Package dhcp4d implements a DHCPv4 server. +package dhcp4d + +import ( + "log" + "math/rand" + "net" + "time" + + "github.com/krolaw/dhcp4" +) + +type Lease struct { + Addr net.IP + HardwareAddr string + Hostname string + Expiry time.Time +} + +type Handler struct { + serverIP net.IP + start net.IP // first IP address to hand out + leaseRange int // number of IP addresses to hand out + leasePeriod time.Duration + options dhcp4.Options + leasesHW map[string]*Lease + leasesIP map[int]*Lease + + // Leases is called whenever a new lease is handed out + Leases func([]*Lease) +} + +// TODO: restore leases from permanent storage +func NewHandler() *Handler { + serverIP := net.IP{192, 168, 42, 1} // TODO: customizeable + return &Handler{ + leasesHW: make(map[string]*Lease), + leasesIP: make(map[int]*Lease), + serverIP: serverIP, + start: net.IP{192, 168, 42, 2}, + leaseRange: 50, + leasePeriod: 2 * time.Hour, + options: dhcp4.Options{ + dhcp4.OptionSubnetMask: []byte{255, 255, 255, 0}, + dhcp4.OptionRouter: []byte(serverIP), + dhcp4.OptionDomainNameServer: []byte(serverIP), + dhcp4.OptionDomainName: []byte("lan"), + dhcp4.OptionDomainSearch: []byte{0x03, 'l', 'a', 'n', 0x00}, + }, + } +} + +func (h *Handler) findLease() int { + if len(h.leasesIP) < h.leaseRange { + // Hand out a free lease + i := rand.Intn(h.leaseRange) + if _, ok := h.leasesIP[i]; !ok { + return i + } + for i := 0; i < h.leaseRange; i++ { + if _, ok := h.leasesIP[i]; !ok { + return i + } + } + } + // Re-use the oldest lease + return -1 +} + +// TODO: is ServeDHCP always run from the same goroutine, or do we need locking? +func (h *Handler) ServeDHCP(p dhcp4.Packet, msgType dhcp4.MessageType, options dhcp4.Options) dhcp4.Packet { + log.Printf("got DHCP packet: %+v, msgType: %v, options: %v", p, msgType, options) + switch msgType { + case dhcp4.Discover: + // Find previous lease for this HardwareAddr, if any + // hwAddr := p.CHAddr().String() + // if lease, ok := h.leases[hwAddr]; ok { + + // } + free := h.findLease() + if free == -1 { + log.Printf("Cannot reply with DHCPOFFER: no more leases available") + return nil // no free leases + } + log.Printf("start = %v, free = %v", h.start, free) + return dhcp4.ReplyPacket(p, + dhcp4.Offer, + h.serverIP, + dhcp4.IPAdd(h.start, free), + h.leasePeriod, + h.options.SelectOrderOrAll(options[dhcp4.OptionParameterRequestList])) + + case dhcp4.Request: + if server, ok := options[dhcp4.OptionServerIdentifier]; ok && !net.IP(server).Equal(h.serverIP) { + return nil // message not for this dhcp server + } + nak := dhcp4.ReplyPacket(p, dhcp4.NAK, h.serverIP, nil, 0, nil) + reqIP := net.IP(options[dhcp4.OptionRequestedIPAddress]) + if reqIP == nil { + reqIP = net.IP(p.CIAddr()) + } + + if len(reqIP) != 4 || reqIP.Equal(net.IPv4zero) { + return nak + } + leaseNum := dhcp4.IPRange(h.start, reqIP) - 1 + if leaseNum < 0 || leaseNum >= h.leaseRange { + return nak + } + + if l, exists := h.leasesIP[leaseNum]; exists && l.HardwareAddr != p.CHAddr().String() { + return nak // lease already in use + } + + var hostname string + if b, ok := options[dhcp4.OptionHostName]; ok { + hostname = string(b) + } + + lease := &Lease{ + Addr: reqIP, + HardwareAddr: p.CHAddr().String(), + Expiry: time.Now().Add(h.leasePeriod), + Hostname: hostname, + } + h.leasesIP[leaseNum] = lease + h.leasesHW[lease.HardwareAddr] = lease + if h.Leases != nil { + var leases []*Lease + for _, l := range h.leasesIP { + leases = append(leases, l) + } + h.Leases(leases) + } + return dhcp4.ReplyPacket(p, dhcp4.ACK, h.serverIP, reqIP, h.leasePeriod, + h.options.SelectOrderOrAll(options[dhcp4.OptionParameterRequestList])) + + } + // 1970/01/01 01:00:04 got DHCP packet: [1 1 6 0 142 216 238 39 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 164 76 200 233 19 71 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 99 130 83 99 53 1 3 54 4 192 168 42 1 50 4 192 168 42 33 12 3 120 112 115 55 18 1 28 2 3 15 6 119 12 44 47 26 121 42 121 249 33 252 42 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0], msgType: Request, options: map[OptionDHCPMessageType:[3] OptionServerIdentifier:[192 168 42 1] OptionHostName:[120 112 115] OptionParameterRequestList:[1 28 2 3 15 6 119 12 44 47 26 121 42 121 249 33 252 42] OptionRequestedIPAddress:[192 168 42 33]] + + return nil +} diff --git a/internal/dhcp4d/dhcp4d_test.go b/internal/dhcp4d/dhcp4d_test.go new file mode 100644 index 0000000..bfc935f --- /dev/null +++ b/internal/dhcp4d/dhcp4d_test.go @@ -0,0 +1,52 @@ +package dhcp4d + +import ( + "bytes" + "net" + "testing" + + "github.com/krolaw/dhcp4" +) + +func TestLease(t *testing.T) { + var ( + addr = net.IP{192, 168, 42, 23} + hardwareAddr = net.HardwareAddr{0x11, 0x22, 0x33, 0x44, 0x55, 0x66} + hostname = "xps" + ) + handler := NewHandler() + leasedCalled := false + handler.Leases = func(leases []*Lease) { + if got, want := len(leases), 1; got != want { + t.Fatalf("unexpected number of leases: got %d, want %d", got, want) + } + l := leases[0] + if got, want := l.Addr, addr; !bytes.Equal(got, want) { + t.Fatalf("unexpected lease.Addr: got %v, want %v", got, want) + } + if got, want := l.HardwareAddr, hardwareAddr.String(); got != want { + t.Fatalf("unexpected lease.HardwareAddr: got %q, want %q", got, want) + } + if got, want := l.Hostname, hostname; got != want { + t.Fatalf("unexpected lease.Hostname: got %q, want %q", got, want) + } + leasedCalled = true + } + p := dhcp4.RequestPacket( + dhcp4.Request, + hardwareAddr, // MAC address + addr, // requested IP address + []byte{0xaa, 0xbb, 0xcc, 0xdd}, // transaction ID + false, // broadcast, + []dhcp4.Option{ + { + Code: dhcp4.OptionHostName, + Value: []byte(hostname), + }, + }, + ) + handler.ServeDHCP(p, dhcp4.Request, p.ParseOptions()) + if !leasedCalled { + t.Fatalf("leased callback not called") + } +} diff --git a/internal/dhcp6/dhcp6.go b/internal/dhcp6/dhcp6.go new file mode 100644 index 0000000..3449134 --- /dev/null +++ b/internal/dhcp6/dhcp6.go @@ -0,0 +1,257 @@ +// Package dhcp6 implements a DHCPv6 client. +package dhcp6 + +import ( + "fmt" + "log" + "net" + "time" + + "github.com/insomniacslk/dhcp/dhcpv6" +) + +type ClientConfig struct { + InterfaceName string // e.g. eth0 + + // LocalAddr allows overwriting the source address used for sending DHCPv6 + // packets. It defaults to the first link-local address of InterfaceName. + LocalAddr *net.UDPAddr + + // RemoteAddr allows addressing a specific DHCPv6 server. It defaults to + // the dhcpv6.AllDHCPRelayAgentsAndServers multicast address. + RemoteAddr *net.UDPAddr + + Conn net.PacketConn // for testing + TransactionIDs []uint32 // for testing +} + +// Config contains the obtained network configuration. +type Config struct { + RenewAfter time.Time `json:"valid_until"` + Prefixes []net.IPNet `json:"prefixes"` // e.g. 2a02:168:4a00::/48 + DNS []string `json:"dns"` // e.g. 2001:1620:2777:1::10, 2001:1620:2777:2::20 +} + +type Client struct { + interfaceName string + raddr *net.UDPAddr + timeNow func() time.Time + + cfg Config + err error + + Conn net.PacketConn // TODO: unexport + transactionIDs []uint32 + + ReadTimeout time.Duration + WriteTimeout time.Duration + + RemoteAddr net.Addr +} + +func NewClient(cfg ClientConfig) (*Client, error) { + // if no LocalAddr is specified, get the interface's link-local address + laddr := cfg.LocalAddr + if laddr == nil { + llAddr, err := dhcpv6.GetLinkLocalAddr(cfg.InterfaceName) + if err != nil { + return nil, err + } + laddr = &net.UDPAddr{ + IP: *llAddr, + Port: dhcpv6.DefaultClientPort, + Zone: cfg.InterfaceName, + } + } + + // if no RemoteAddr is specified, use AllDHCPRelayAgentsAndServers + raddr := cfg.RemoteAddr + if raddr == nil { + raddr = &net.UDPAddr{ + IP: dhcpv6.AllDHCPRelayAgentsAndServers, + Port: dhcpv6.DefaultServerPort, + } + } + + // prepare the socket to listen on for replies + conn := cfg.Conn + if conn == nil { + udpConn, err := net.ListenUDP("udp6", laddr) + if err != nil { + return nil, err + } + conn = udpConn + } + + return &Client{ + interfaceName: cfg.InterfaceName, + timeNow: time.Now, + raddr: raddr, + Conn: conn, + transactionIDs: cfg.TransactionIDs, + ReadTimeout: dhcpv6.DefaultReadTimeout, + WriteTimeout: dhcpv6.DefaultWriteTimeout, + }, nil +} + +func (c *Client) Close() error { + return c.Conn.Close() +} + +const maxUDPReceivedPacketSize = 8192 // arbitrary size. Theoretically could be up to 65kb + +func (c *Client) sendReceive(packet dhcpv6.DHCPv6, expectedType dhcpv6.MessageType) (dhcpv6.DHCPv6, error) { + if packet == nil { + return nil, fmt.Errorf("Packet to send cannot be nil") + } + if expectedType == dhcpv6.MSGTYPE_NONE { + // infer the expected type from the packet being sent + if packet.Type() == dhcpv6.SOLICIT { + expectedType = dhcpv6.ADVERTISE + } else if packet.Type() == dhcpv6.REQUEST { + expectedType = dhcpv6.REPLY + } else if packet.Type() == dhcpv6.RELAY_FORW { + expectedType = dhcpv6.RELAY_REPL + } else if packet.Type() == dhcpv6.LEASEQUERY { + expectedType = dhcpv6.LEASEQUERY_REPLY + } // and probably more + } + + // send the packet out + c.Conn.SetWriteDeadline(time.Now().Add(c.WriteTimeout)) + if _, err := c.Conn.WriteTo(packet.ToBytes(), c.raddr); err != nil { + return nil, err + } + + // wait for a reply + c.Conn.SetReadDeadline(time.Now().Add(c.ReadTimeout)) + var ( + adv dhcpv6.DHCPv6 + isMessage bool + ) + msg, ok := packet.(*dhcpv6.DHCPv6Message) + if ok { + isMessage = true + } + for { + buf := make([]byte, maxUDPReceivedPacketSize) + n, _, err := c.Conn.ReadFrom(buf) + if err != nil { + return nil, err + } + adv, err = dhcpv6.FromBytes(buf[:n]) + if err != nil { + log.Printf("non-DHCP: %v", err) + // skip non-DHCP packets + continue + } + if recvMsg, ok := adv.(*dhcpv6.DHCPv6Message); ok && isMessage { + // if a regular message, check the transaction ID first + // XXX should this unpack relay messages and check the XID of the + // inner packet too? + if msg.TransactionID() != recvMsg.TransactionID() { + log.Printf("different XID") + // different XID, we don't want this packet for sure + continue + } + } + if expectedType == dhcpv6.MSGTYPE_NONE { + // just take whatever arrived + break + } else if adv.Type() == expectedType { + break + } + } + return adv, nil +} + +func (c *Client) solicit(solicit dhcpv6.DHCPv6) (dhcpv6.DHCPv6, dhcpv6.DHCPv6, error) { + var err error + if solicit == nil { + solicit, err = dhcpv6.NewSolicitForInterface(c.interfaceName) + if err != nil { + return nil, nil, err + } + } + if len(c.transactionIDs) > 0 { + id := c.transactionIDs[0] + c.transactionIDs = c.transactionIDs[1:] + solicit.(*dhcpv6.DHCPv6Message).SetTransactionID(id) + } + advertise, err := c.sendReceive(solicit, dhcpv6.MSGTYPE_NONE) + return solicit, advertise, err +} + +func (c *Client) request(advertise, request dhcpv6.DHCPv6) (dhcpv6.DHCPv6, dhcpv6.DHCPv6, error) { + if request == nil { + var err error + request, err = dhcpv6.NewRequestFromAdvertise(advertise) + if err != nil { + return nil, nil, err + } + } + if len(c.transactionIDs) > 0 { + id := c.transactionIDs[0] + c.transactionIDs = c.transactionIDs[1:] + request.(*dhcpv6.DHCPv6Message).SetTransactionID(id) + } + reply, err := c.sendReceive(request, dhcpv6.MSGTYPE_NONE) + return request, reply, err +} + +func (c *Client) ObtainOrRenew() bool { + _, advertise, err := c.solicit(nil) + if err != nil { + c.err = err + return true + } + + _, reply, err := c.request(advertise, nil) + if err != nil { + c.err = err + return true + } + var newCfg Config + for _, opt := range reply.Options() { + switch o := opt.(type) { + case *dhcpv6.OptIAForPrefixDelegation: + t1 := c.timeNow().Add(time.Duration(o.T1()) * time.Second) + if t1.Before(newCfg.RenewAfter) || newCfg.RenewAfter.IsZero() { + newCfg.RenewAfter = t1 + } + for b := o.Options(); len(b) > 0; { + sopt, err := dhcpv6.ParseOption(b) + if err != nil { + c.err = err + return true + } + b = b[4+sopt.Length():] + + prefix, ok := sopt.(*dhcpv6.OptIAPrefix) + if !ok { + continue + } + + newCfg.Prefixes = append(newCfg.Prefixes, net.IPNet{ + IP: prefix.IPv6Prefix(), + Mask: net.CIDRMask(int(prefix.PrefixLength()), 128), + }) + } + + case *dhcpv6.OptDNSRecursiveNameServer: + for _, ns := range o.NameServers { + newCfg.DNS = append(newCfg.DNS, ns.String()) + } + } + } + c.cfg = newCfg + return true +} + +func (c *Client) Err() error { + return c.err +} + +func (c *Client) Config() Config { + return c.cfg +} diff --git a/internal/dhcp6/dhcp6_test.go b/internal/dhcp6/dhcp6_test.go new file mode 100644 index 0000000..58fac12 --- /dev/null +++ b/internal/dhcp6/dhcp6_test.go @@ -0,0 +1,111 @@ +package dhcp6 + +import ( + "fmt" + "net" + "os" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/gopacket" + "github.com/google/gopacket/layers" + "github.com/google/gopacket/pcapgo" +) + +type packet struct { + data []byte + ip net.IP + err error +} + +type replayer struct { + pcapr *pcapgo.Reader +} + +func (r *replayer) LocalAddr() net.Addr { return nil } +func (r *replayer) Close() error { return nil } +func (r *replayer) WriteTo(b []byte, addr net.Addr) (n int, err error) { + //log.Printf("-> %v", b) + return len(b), nil +} +func (r *replayer) SetDeadline(t time.Time) error { return nil } +func (r *replayer) SetReadDeadline(t time.Time) error { return nil } +func (r *replayer) SetWriteDeadline(t time.Time) error { return nil } + +func (r *replayer) ReadFrom(buf []byte) (int, net.Addr, error) { + data, _, err := r.pcapr.ReadPacketData() + if err != nil { + return 0, nil, err + } + pkt := gopacket.NewPacket(data, layers.LayerTypeEthernet, gopacket.DecodeOptions{}) + // TODO: get source IP + udp := pkt.Layer(layers.LayerTypeUDP) + if udp == nil { + return 0, nil, fmt.Errorf("pcap contained unexpected non-UDP packet") + } + + //log.Printf("ReadFrom(): %x, %v, pkt = %+v", udp.LayerPayload(), err, pkt) + copy(buf, udp.LayerPayload()) + return len(udp.LayerPayload()), &net.IPAddr{IP: net.ParseIP("192.168.23.1")}, err +} + +func TestDHCP6(t *testing.T) { + f, err := os.Open("testdata/fiber7.pcap") + if err != nil { + t.Fatal(err) + } + defer f.Close() + pcapr, err := pcapgo.NewReader(f) + if err != nil { + t.Fatal(err) + } + + laddr, err := net.ResolveUDPAddr("udp6", "[fe80::42:aff:fea5:966e]:546") + if err != nil { + t.Fatal(err) + } + now := time.Now() + c, err := NewClient(ClientConfig{ + // NOTE(stapelberg): dhcpv6.NewSolicitForInterface requires an interface + // name to get the MAC address. + InterfaceName: "lo", + LocalAddr: laddr, + Conn: &replayer{pcapr: pcapr}, + TransactionIDs: []uint32{ + 0x48e59e, // SOLICIT + 0x738c3b, // REQUEST + }, + }) + if err != nil { + t.Fatal(err) + } + c.timeNow = func() time.Time { return now } + + c.ObtainOrRenew() + if err := c.Err(); err != nil { + t.Fatalf("unexpected error: %v", err) + } + got := c.Config() + want := Config{ + RenewAfter: now.Add(20 * time.Minute), + Prefixes: []net.IPNet{ + mustParseCIDR("2a02:168:4a00::/48"), + }, + DNS: []string{ + "2001:1620:2777:1::10", + "2001:1620:2777:2::20", + }, + } + if diff := cmp.Diff(got, want); diff != "" { + t.Fatalf("unexpected config: diff (-got +want):\n%s", diff) + } +} + +func mustParseCIDR(s string) net.IPNet { + _, net, err := net.ParseCIDR(s) + if err != nil { + panic(err) + } + return *net +} diff --git a/internal/dhcp6/router7.test b/internal/dhcp6/router7.test new file mode 100755 index 0000000..57d4bd3 Binary files /dev/null and b/internal/dhcp6/router7.test differ diff --git a/internal/dhcp6/testdata/fiber7.pcap b/internal/dhcp6/testdata/fiber7.pcap new file mode 100644 index 0000000..f9430b3 Binary files /dev/null and b/internal/dhcp6/testdata/fiber7.pcap differ diff --git a/internal/dns/dns.go b/internal/dns/dns.go new file mode 100644 index 0000000..88a9a62 --- /dev/null +++ b/internal/dns/dns.go @@ -0,0 +1,96 @@ +package dns + +import ( + "log" + "strings" + "time" + + "router7/internal/dhcp4d" + + "golang.org/x/time/rate" + + "github.com/miekg/dns" +) + +type Server struct { + *dns.Server + client *dns.Client + domain string + upstream string + sometimes *rate.Limiter + hostsByName map[string]string + hostsByIP map[string]string +} + +func NewServer(addr, domain string) *Server { + server := &Server{ + Server: &dns.Server{Addr: addr, Net: "udp"}, + client: &dns.Client{}, + domain: domain, + upstream: "8.8.8.8:53", + sometimes: rate.NewLimiter(rate.Every(1*time.Second), 1), // at most once per second + hostsByName: make(map[string]string), + hostsByIP: make(map[string]string), + } + dns.HandleFunc(".", server.handleRequest) + return server +} + +func (s *Server) SetLeases(leases []dhcp4d.Lease) { + for _, l := range leases { + s.hostsByName[l.Hostname] = l.Addr.String() + if rev, err := dns.ReverseAddr(l.Addr.String()); err == nil { + s.hostsByIP[rev] = l.Hostname + } + } +} + +// TODO: is handleRequest called in more than one goroutine at a time? +// TODO: require search domains to be present, then use HandleFunc("lan.", internalName) +func (s *Server) handleRequest(w dns.ResponseWriter, r *dns.Msg) { + if len(r.Question) == 1 { // TODO: answer all questions we can answer + q := r.Question[0] + if q.Qtype == dns.TypeA && q.Qclass == dns.ClassINET { + name := strings.TrimSuffix(q.Name, ".") + name = strings.TrimSuffix(name, "."+s.domain) + + if !strings.Contains(name, ".") { + if host, ok := s.hostsByName[name]; ok { + rr, err := dns.NewRR(q.Name + " 3600 IN A " + host) + if err != nil { + log.Fatal(err) + } + m := new(dns.Msg) + m.SetReply(r) + m.Answer = append(m.Answer, rr) + w.WriteMsg(m) + return + } + } + } + if q.Qtype == dns.TypePTR && q.Qclass == dns.ClassINET { + if strings.HasSuffix(q.Name, "168.192.in-addr.arpa.") { + if host, ok := s.hostsByIP[q.Name]; ok { + rr, err := dns.NewRR(q.Name + " 3600 IN PTR " + host + "." + s.domain) + if err != nil { + log.Fatal(err) + } + m := new(dns.Msg) + m.SetReply(r) + m.Answer = append(m.Answer, rr) + w.WriteMsg(m) + return + } + } + } + } + + in, _, err := s.client.Exchange(r, s.upstream) + if err != nil { + if s.sometimes.Allow() { + log.Printf("resolving %v failed: %v", r.Question, err) + } + return // DNS has no reply for resolving errors + } + w.WriteMsg(in) +} diff --git a/internal/dns/dns_test.go b/internal/dns/dns_test.go new file mode 100644 index 0000000..a686ece --- /dev/null +++ b/internal/dns/dns_test.go @@ -0,0 +1,101 @@ +package dns + +import ( + "bytes" + "net" + "router7/internal/dhcp4d" + "testing" + + "github.com/miekg/dns" +) + +// TODO(later): upstream a dnstest.Recorder implementation +type recorder struct { + response *dns.Msg +} + +func (r *recorder) WriteMsg(m *dns.Msg) error { + r.response = m + return nil +} + +func (r *recorder) LocalAddr() net.Addr { return nil } +func (r *recorder) RemoteAddr() net.Addr { return nil } +func (r *recorder) Write([]byte) (int, error) { return 0, nil } +func (r *recorder) Close() error { return nil } +func (r *recorder) TsigStatus() error { return nil } +func (r *recorder) TsigTimersOnly(bool) {} +func (r *recorder) Hijack() {} + +func TestNXDOMAIN(t *testing.T) { + r := &recorder{} + s := NewServer("localhost:0", "lan") + m := new(dns.Msg) + m.SetQuestion("foo.invalid.", dns.TypeA) + s.handleRequest(r, m) + if got, want := r.response.MsgHdr.Rcode, dns.RcodeNameError; got != want { + t.Fatalf("unexpected rcode: got %v, want %v", got, want) + } +} + +func TestResolveError(t *testing.T) { + r := &recorder{} + s := NewServer("localhost:0", "lan") + s.upstream = "266.266.266.266:53" + m := new(dns.Msg) + m.SetQuestion("foo.invalid.", dns.TypeA) + s.handleRequest(r, m) + if r.response != nil { + t.Fatalf("r.response unexpectedly not nil: %v", r.response) + } +} + +func TestDHCP(t *testing.T) { + r := &recorder{} + s := NewServer("localhost:0", "lan") + s.SetLeases([]dhcp4d.Lease{ + { + Hostname: "xps", + Addr: net.IP{192, 168, 42, 23}, + }, + }) + m := new(dns.Msg) + m.SetQuestion("xps.lan.", dns.TypeA) + s.handleRequest(r, m) + if got, want := len(r.response.Answer), 1; got != want { + t.Fatalf("unexpected number of answers: got %d, want %d", got, want) + } + a := r.response.Answer[0] + if _, ok := a.(*dns.A); !ok { + t.Fatalf("unexpected response type: got %T, want dns.A", a) + } + if got, want := a.(*dns.A).A.To4(), (net.IP{192, 168, 42, 23}); !bytes.Equal(got, want) { + t.Fatalf("unexpected response IP: got %v, want %v", got, want) + } +} + +func TestDHCPReverse(t *testing.T) { + r := &recorder{} + s := NewServer("localhost:0", "lan") + s.SetLeases([]dhcp4d.Lease{ + { + Hostname: "xps", + Addr: net.IP{192, 168, 42, 23}, + }, + }) + m := new(dns.Msg) + m.SetQuestion("23.42.168.192.in-addr.arpa.", dns.TypePTR) + s.handleRequest(r, m) + if got, want := len(r.response.Answer), 1; got != want { + t.Fatalf("unexpected number of answers: got %d, want %d", got, want) + } + a := r.response.Answer[0] + if _, ok := a.(*dns.PTR); !ok { + t.Fatalf("unexpected response type: got %T, want dns.A", a) + } + if got, want := a.(*dns.PTR).Ptr, "xps.lan."; got != want { + t.Fatalf("unexpected response record: got %q, want %q", got, want) + } +} + +// TODO: multiple questions diff --git a/internal/netconfig/netconfig.go b/internal/netconfig/netconfig.go new file mode 100644 index 0000000..7e93b98 --- /dev/null +++ b/internal/netconfig/netconfig.go @@ -0,0 +1,256 @@ +package netconfig + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "log" + "net" + "os" + "path/filepath" + "strconv" + "strings" + "syscall" + + "github.com/vishvananda/netlink" + "golang.org/x/sys/unix" + + "router7/internal/dhcp4" + "router7/internal/dhcp6" +) + +func subnetMaskSize(mask string) (int, error) { + parts := strings.Split(mask, ".") + if got, want := len(parts), 4; got != want { + return 0, fmt.Errorf("unexpected number of parts in subnet mask %q: got %d, want %d", mask, got, want) + } + numeric := make([]byte, len(parts)) + for idx, part := range parts { + i, err := strconv.ParseUint(part, 0, 8) + if err != nil { + return 0, err + } + numeric[idx] = byte(i) + } + ones, _ := net.IPv4Mask(numeric[0], numeric[1], numeric[2], numeric[3]).Size() + return ones, nil +} + +func applyDhcp4(iface, dir string) error { + b, err := ioutil.ReadFile(filepath.Join(dir, "dhcp4/wire/lease.json")) + if err != nil { + return err + } + var got dhcp4.Config + if err := json.Unmarshal(b, &got); err != nil { + return err + } + + link, err := netlink.LinkByName(iface) + if err != nil { + return err + } + + subnetSize, err := subnetMaskSize(got.SubnetMask) + if err != nil { + return err + } + + addr, err := netlink.ParseAddr(fmt.Sprintf("%s/%d", got.ClientIP, subnetSize)) + if err != nil { + return err + } + + h, err := netlink.NewHandle() + if err != nil { + return fmt.Errorf("netlink.NewHandle: %v", err) + } + if err := h.AddrAdd(link, addr); err != nil { + return fmt.Errorf("AddrAdd(%v): %v", addr, err) + } + + // from include/uapi/linux/rtnetlink.h + const ( + RTPROT_STATIC = 4 + RTPROT_DHCP = 16 + ) + + if err := h.RouteAdd(&netlink.Route{ + LinkIndex: link.Attrs().Index, + Dst: &net.IPNet{ + IP: net.ParseIP(got.Router), + Mask: net.CIDRMask(32, 32), + }, + Src: net.ParseIP(got.ClientIP), + Scope: netlink.SCOPE_LINK, + Protocol: RTPROT_DHCP, + }); err != nil { + return fmt.Errorf("RouteAdd(router): %v", err) + } + + if err := h.RouteAdd(&netlink.Route{ + LinkIndex: link.Attrs().Index, + Dst: &net.IPNet{ + IP: net.ParseIP("0.0.0.0"), + Mask: net.CIDRMask(0, 32), + }, + Gw: net.ParseIP(got.Router), + Src: net.ParseIP(got.ClientIP), + Protocol: RTPROT_DHCP, + }); err != nil { + return fmt.Errorf("RouteAdd(default): %v", err) + } + + return nil +} + +func applyDhcp6(iface, dir string) error { + b, err := ioutil.ReadFile(filepath.Join(dir, "dhcp6/wire/lease.json")) + if err != nil { + return err + } + var got dhcp6.Config + if err := json.Unmarshal(b, &got); err != nil { + return err + } + + link, err := netlink.LinkByName(iface) + if err != nil { + return err + } + + for _, prefix := range got.Prefixes { + // pick the first address of the prefix, e.g. address 2a02:168:4a00::1 + // for prefix 2a02:168:4a00::/48 + prefix.IP[len(prefix.IP)-1] = 1 + addr, err := netlink.ParseAddr(prefix.String()) + if err != nil { + return err + } + + if err := netlink.AddrAdd(link, addr); err != nil { + return fmt.Errorf("AddrAdd(%v): %v", addr, err) + } + } + return nil +} + +type InterfaceDetails struct { + HardwareAddr string `json:"hardware_addr"` // e.g. dc:9b:9c:ee:72:fd + Name string `json:"name"` // e.g. uplink0, or lan0 + Addr string `json:"addr"` // e.g. 192.168.42.1/24 +} + +type InterfaceConfig struct { + Interfaces []InterfaceDetails `json:"interfaces"` +} + +func applyInterfaces(dir string) error { + b, err := ioutil.ReadFile(filepath.Join(dir, "interfaces.json")) + if err != nil { + return err + } + var cfg InterfaceConfig + if err := json.Unmarshal(b, &cfg); err != nil { + return err + } + byHardwareAddr := make(map[string]InterfaceDetails) + for _, details := range cfg.Interfaces { + byHardwareAddr[details.HardwareAddr] = details + } + links, err := netlink.LinkList() + for _, l := range links { + attr := l.Attrs() + details, ok := byHardwareAddr[attr.HardwareAddr.String()] + if !ok { + continue + } + log.Printf("apply details %+v", details) + ioutil.WriteFile("/dev/console", []byte(fmt.Sprintf("apply %+v\n", details)), 0600) + if attr.Name != details.Name { + if err := netlink.LinkSetName(l, details.Name); err != nil { + return fmt.Errorf("LinkSetName(%q): %v", details.Name, err) + } + attr.Name = details.Name + } + + if attr.OperState != netlink.OperUp { + // Set the interface to up, which is required by all other configuration. + if err := netlink.LinkSetUp(l); err != nil { + return fmt.Errorf("LinkSetUp(%s): %v", attr.Name, err) + } + } + + if details.Addr != "" { + addr, err := netlink.ParseAddr(details.Addr) + if err != nil { + return fmt.Errorf("ParseAddr(%q): %v", details.Addr, err) + } + + if err := netlink.AddrReplace(l, addr); err != nil { + return fmt.Errorf("AddrReplace(%s, %v): %v", attr.Name, addr, err) + } + } + } + return nil +} + +func applyFirewall() error { + // Fake it till you make it! + // Captured via: + // ./strace -xx -v -f -s 2048 ./xtables-multi iptables -t nat -A POSTROUTING -o uplink0 -j MASQUERADE + optRule := "\x6e\x61\x74\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1b\x00\x00\x00\x06\x00\x00\x00\xb8\x03\x00\x00\x00\x00\x00\x00\x98\x00\x00\x00\x00\x00\x00\x00\x30\x01\x00\x00\xc8\x01\x00\x00\x00\x00\x00\x00\x98\x00\x00\x00\x00\x00\x00\x00\x30\x01\x00\x00\x70\x02\x00\x00\x05\x00\x00\x00\x70\xed\xdb\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x70\x00\x98\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x28\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xfe\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x70\x00\x98\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x28\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xfe\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x70\x00\x98\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x28\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xfe\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x75\x70\x6c\x69\x6e\x6b\x30\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x70\x00\xa8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x38\x00\x4d\x41\x53\x51\x55\x45\x52\x41\x44\x45\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x70\x00\x98\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x28\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xfe\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x70\x00\xb0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x40\x00\x45\x52\x52\x4f\x52\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x45\x52\x52\x4f\x52\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + optCounters := "\x6e\x61\x74\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + + fd, err := unix.Socket(unix.AF_INET, unix.SOCK_RAW, unix.IPPROTO_RAW) + if err != nil { + return err + } + // TODO: close socket later + + if err := unix.SetsockoptString(fd, unix.SOL_IP, 0x40, optRule); err != nil { + return err + } + if err := unix.SetsockoptString(fd, unix.SOL_IP, 0x41, optCounters); err != nil { + return err + } + + return nil +} + +func applySysctl() error { + if err := ioutil.WriteFile("/proc/sys/net/ipv4/ip_forward", []byte("1"), 0644); err != nil { + return err + } + return nil +} + +func Apply(iface, dir string) error { + if err := applyInterfaces(dir); err != nil { + return err + } + + if err := applyDhcp4(iface, dir); err != nil { + return err + } + + if err := applyDhcp6(iface, dir); err != nil { + return err + } + + if err := applySysctl(); err != nil { + return err + } + + if err := applyFirewall(); err != nil { + return err + } + + // Notify gokrazy init of new addresses + p, _ := os.FindProcess(1) + if err := p.Signal(syscall.SIGHUP); err != nil { + log.Printf("send SIGHUP to init: %v", err) + } + + return nil +} diff --git a/internal/notify/notify.go b/internal/notify/notify.go new file mode 100644 index 0000000..ae6c537 --- /dev/null +++ b/internal/notify/notify.go @@ -0,0 +1,38 @@ +package notify + +import ( + "io/ioutil" + "os" + "path/filepath" + "regexp" + "strconv" + "strings" +) + +var numericRe = regexp.MustCompile(`^[0-9]+$`) + +func Process(name string, sig os.Signal) error { + fis, err := ioutil.ReadDir("/proc") + if err != nil { + return err + } + for _, fi := range fis { + if !fi.IsDir() { + continue + } + if !numericRe.MatchString(fi.Name()) { + continue + } + b, err := ioutil.ReadFile(filepath.Join("/proc", fi.Name(), "cmdline")) + if err != nil { + return err + } + if !strings.HasPrefix(string(b), name) { + continue + } + pid, _ := strconv.Atoi(fi.Name()) // already verified to be numeric + p, _ := os.FindProcess(pid) + return p.Signal(sig) + } + return nil +}