Initial commit
This commit is contained in:
commit
6b9ce5728a
63
cmd/dhcp4/dhcp4.go
Normal file
63
cmd/dhcp4/dhcp4.go
Normal 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
57
cmd/dhcp4d/dhcp4d.go
Normal 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
60
cmd/dhcp6/dhcp6.go
Normal 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
BIN
cmd/dhcp6/router7.test
Executable file
Binary file not shown.
54
cmd/dnsd/dnsd.go
Normal file
54
cmd/dnsd/dnsd.go
Normal 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)
|
||||
}
|
||||
}
|
41
cmd/netconfigd/netconfigd.go
Normal file
41
cmd/netconfigd/netconfigd.go
Normal 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
17
cmd/router7/main.go
Normal 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
238
integrationdhcpv4_test.go
Normal 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
145
integrationdhcpv6_test.go
Normal 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
25
integrationdns_test.go
Normal 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)
|
||||
}
|
||||
}
|
153
integrationnetconfig_test.go
Normal file
153
integrationnetconfig_test.go
Normal 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
199
internal/dhcp4/dhcp4.go
Normal 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
|
||||
}
|
95
internal/dhcp4/dhcp4_test.go
Normal file
95
internal/dhcp4/dhcp4_test.go
Normal 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)
|
||||
}
|
||||
}
|
36
internal/dhcp4/serialize_test.go
Normal file
36
internal/dhcp4/serialize_test.go
Normal 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
BIN
internal/dhcp4/testdata/fiber7.pcap
vendored
Normal file
Binary file not shown.
142
internal/dhcp4d/dhcp4d.go
Normal file
142
internal/dhcp4d/dhcp4d.go
Normal 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
|
||||
}
|
52
internal/dhcp4d/dhcp4d_test.go
Normal file
52
internal/dhcp4d/dhcp4d_test.go
Normal 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
257
internal/dhcp6/dhcp6.go
Normal 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
|
||||
}
|
111
internal/dhcp6/dhcp6_test.go
Normal file
111
internal/dhcp6/dhcp6_test.go
Normal 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
BIN
internal/dhcp6/router7.test
Executable file
Binary file not shown.
BIN
internal/dhcp6/testdata/fiber7.pcap
vendored
Normal file
BIN
internal/dhcp6/testdata/fiber7.pcap
vendored
Normal file
Binary file not shown.
96
internal/dns/dns.go
Normal file
96
internal/dns/dns.go
Normal 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
101
internal/dns/dns_test.go
Normal 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
|
256
internal/netconfig/netconfig.go
Normal file
256
internal/netconfig/netconfig.go
Normal 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
38
internal/notify/notify.go
Normal 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
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user