Initial commit

This commit is contained in:
Michael Stapelberg 2018-05-27 17:30:42 +02:00
commit 6b9ce5728a
25 changed files with 2236 additions and 0 deletions

63
cmd/dhcp4/dhcp4.go Normal file
View File

@ -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)
}
}

57
cmd/dhcp4d/dhcp4d.go Normal file
View File

@ -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)
}
}

60
cmd/dhcp6/dhcp6.go Normal file
View File

@ -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)
}
}

BIN
cmd/dhcp6/router7.test Executable file

Binary file not shown.

54
cmd/dnsd/dnsd.go Normal file
View File

@ -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)
}
}

View File

@ -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)
}
}

17
cmd/router7/main.go Normal file
View File

@ -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)
}
}

238
integrationdhcpv4_test.go Normal file
View File

@ -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
}

145
integrationdhcpv6_test.go Normal file
View File

@ -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
}

25
integrationdns_test.go Normal file
View File

@ -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)
}
}

View File

@ -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)
}
}

199
internal/dhcp4/dhcp4.go Normal file
View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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)
}
}

BIN
internal/dhcp4/testdata/fiber7.pcap vendored Normal file

Binary file not shown.

142
internal/dhcp4d/dhcp4d.go Normal file
View File

@ -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
}

View File

@ -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")
}
}

257
internal/dhcp6/dhcp6.go Normal file
View File

@ -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
}

View File

@ -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
}

BIN
internal/dhcp6/router7.test Executable file

Binary file not shown.

BIN
internal/dhcp6/testdata/fiber7.pcap vendored Normal file

Binary file not shown.

96
internal/dns/dns.go Normal file
View File

@ -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)
}

101
internal/dns/dns_test.go Normal file
View File

@ -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

View File

@ -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
}

38
internal/notify/notify.go Normal file
View File

@ -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
}