From d3884d90740742a0c383b2f1a17d06b88d0a6d1a Mon Sep 17 00:00:00 2001 From: Michael Stapelberg Date: Fri, 1 Jun 2018 09:53:44 +0200 Subject: [PATCH] add radvd --- cmd/radvd/radvd.go | 55 ++++++++++ integrationradvd_test.go | 73 +++++++++++++ internal/radvd/radvd.go | 224 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 352 insertions(+) create mode 100644 cmd/radvd/radvd.go create mode 100644 integrationradvd_test.go create mode 100644 internal/radvd/radvd.go diff --git a/cmd/radvd/radvd.go b/cmd/radvd/radvd.go new file mode 100644 index 0000000..4d124c9 --- /dev/null +++ b/cmd/radvd/radvd.go @@ -0,0 +1,55 @@ +// Binary radvd sends IPv6 router advertisments. +package main + +import ( + "encoding/json" + "flag" + "io/ioutil" + "log" + "os" + "os/signal" + "syscall" + + "router7/internal/dhcp6" + "router7/internal/radvd" +) + +func logic() error { + srv, err := radvd.NewServer() + if err != nil { + return err + } + readConfig := func() error { + b, err := ioutil.ReadFile("/perm/dhcp6/wire/lease.json") + if err != nil { + return err + } + var cfg dhcp6.Config + if err := json.Unmarshal(b, &cfg); err != nil { + return err + } + srv.SetPrefixes(cfg.Prefixes) + return nil + } + if err := readConfig(); err != nil { + log.Printf("readConfig: %v", err) + } + ch := make(chan os.Signal, 1) + signal.Notify(ch, syscall.SIGUSR1) + go func() { + for range ch { + if err := readConfig(); err != nil { + log.Printf("readConfig: %v", err) + } + } + }() + return srv.ListenAndServe("lan0") +} + +func main() { + // TODO: drop privileges, run as separate uid? + flag.Parse() + if err := logic(); err != nil { + log.Fatal(err) + } +} diff --git a/integrationradvd_test.go b/integrationradvd_test.go new file mode 100644 index 0000000..c75eed7 --- /dev/null +++ b/integrationradvd_test.go @@ -0,0 +1,73 @@ +package integration_test + +import ( + "io/ioutil" + "net" + "os" + "os/exec" + "router7/internal/radvd" + "testing" +) + +func TestRouterAdvertisement(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", "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 + + srv, err := radvd.NewServer() + if err != nil { + t.Fatal(err) + } + srv.SetPrefixes([]net.IPNet{ + net.IPNet{IP: net.ParseIP("2a02:168:4a00::"), Mask: net.CIDRMask(64, 128)}, + }) + go func() { + if err := srv.ListenAndServe("veth0a"); err != nil { + t.Fatal(err) + } + }() + //time.Sleep(5 * time.Second) + rdisc6 := exec.Command("ip", "netns", "exec", ns, "rdisc6", + "--single", // exit after first router advertisement + "--retry", "1", // retry only once + "--wait", "1000", // wait 1s + "veth0b") + rdisc6.Stderr = os.Stderr + b, err := rdisc6.Output() + if err != nil { + t.Fatal(err) + } + t.Logf("b = %s", string(b)) + +} diff --git a/internal/radvd/radvd.go b/internal/radvd/radvd.go new file mode 100644 index 0000000..bd51e8b --- /dev/null +++ b/internal/radvd/radvd.go @@ -0,0 +1,224 @@ +// Package radvd implements IPv6 router advertisments. +package radvd + +import ( + "encoding/binary" + "log" + "net" + "sync" + "time" + + "github.com/google/gopacket" + "github.com/google/gopacket/layers" + + "golang.org/x/net/icmp" + "golang.org/x/net/ipv6" +) + +type Server struct { + pc *ipv6.PacketConn + + mu sync.Mutex + prefixes []net.IPNet + iface *net.Interface +} + +func NewServer() (*Server, error) { + return &Server{}, nil +} + +func (s *Server) SetPrefixes(prefixes []net.IPNet) { + s.mu.Lock() + s.prefixes = prefixes + s.mu.Unlock() + if s.iface != nil { + s.sendAdvertisement(nil) + } +} + +func (s *Server) ListenAndServe(ifname string) error { + var err error + s.iface, err = net.InterfaceByName(ifname) + if err != nil { + return err + } + + // TODO(correctness): would it be better to listen on + // net.IPv6linklocalallrouters? Just specifying that results in an error, + // though. + conn, err := net.ListenIP("ip6:ipv6-icmp", &net.IPAddr{net.IPv6unspecified, ""}) + if err != nil { + return err + } + defer conn.Close() + s.pc = ipv6.NewPacketConn(conn) + s.pc.SetHopLimit(255) // as per RFC 4861, section 4.1 + + var filter ipv6.ICMPFilter + filter.SetAll(true) + filter.Accept(ipv6.ICMPTypeRouterSolicitation) + if err := s.pc.SetICMPFilter(&filter); err != nil { + return err + } + + go func() { + for { + s.sendAdvertisement(nil) // TODO: handle error + time.Sleep(1 * time.Minute) + } + }() + + // A 512 bytes buffer is sufficient for router solicitation packets, which + // are basically empty. + buf := make([]byte, 512) + for { + n, _, addr, err := s.pc.ReadFrom(buf) + if err != nil { + return err + } + // TODO: isn’t this guaranteed by the filter above? + if n == 0 || + ipv6.ICMPType(buf[0]) != ipv6.ICMPTypeRouterSolicitation { + continue + } + if err := s.sendAdvertisement(addr); err != nil { + return err + } + } + + return nil +} + +type sourceLinkLayerAddress struct { + address net.HardwareAddr +} + +func (o sourceLinkLayerAddress) Marshal() layers.ICMPv6Option { + return layers.ICMPv6Option{ + Type: layers.ICMPv6OptSourceAddress, + Data: o.address, + } +} + +type mtu struct { + mtu uint32 +} + +func (o mtu) Marshal() layers.ICMPv6Option { + buf := make([]byte, 6) + // First 2 bytes are reserved + binary.BigEndian.PutUint32(buf[2:], o.mtu) + return layers.ICMPv6Option{ + Type: layers.ICMPv6OptMTU, + Data: buf, + } +} + +type prefixInfo struct { + prefixLength byte + flags byte // TODO: enum for values + validLifetime uint32 // seconds + preferredLifetime uint32 // seconds + prefix [16]byte +} + +func (o prefixInfo) Marshal() layers.ICMPv6Option { + buf := make([]byte, 30) + buf[0] = o.prefixLength + buf[1] = o.flags + binary.BigEndian.PutUint32(buf[2:], o.validLifetime) + binary.BigEndian.PutUint32(buf[6:], o.preferredLifetime) + // 4 bytes reserved + copy(buf[14:], o.prefix[:]) + return layers.ICMPv6Option{ + Type: layers.ICMPv6OptPrefixInfo, + Data: buf, + } +} + +type rdnss struct { + lifetime uint32 // seconds + server [16]byte +} + +func (o rdnss) Marshal() layers.ICMPv6Option { + buf := make([]byte, 22) + // 2 bytes reserved + binary.BigEndian.PutUint32(buf[2:], o.lifetime) + copy(buf[6:], o.server[:]) + return layers.ICMPv6Option{ + Type: 25, // TODO: Recursive DNS Server + Data: buf, + } +} + +func (s *Server) sendAdvertisement(addr net.Addr) error { + if addr == nil { + addr = &net.IPAddr{net.IPv6linklocalallnodes, s.iface.Name} + } + // TODO: cache the packet + msgbody := []byte{ + 0x40, // hop limit: 64 + 0x80, // flags: managed address configuration + 0x07, 0x08, // router lifetime (s): 1800 + 0x00, 0x00, 0x00, 0x00, // reachable time (ms): 0 + 0x00, 0x00, 0x00, 0x00, // retrans time (ms): 0 + } + + options := layers.ICMPv6Options{ + (sourceLinkLayerAddress{address: s.iface.HardwareAddr}).Marshal(), + (mtu{mtu: uint32(s.iface.MTU)}).Marshal(), + } + s.mu.Lock() + for _, prefix := range s.prefixes { + ones, _ := prefix.Mask.Size() + // Use the first /64 subnet within larger prefixes + if ones < 64 { + ones = 64 + } + + var net [16]byte + copy(net[:], prefix.IP) + options = append(options, (prefixInfo{ + prefixLength: byte(ones), + flags: 0xc0, // TODO + validLifetime: 7200, // seconds + preferredLifetime: 1800, // seconds + prefix: net, + }).Marshal()) + } + if len(s.prefixes) > 0 { + prefix := s.prefixes[0] + var server [16]byte + copy(server[:], prefix.IP) + // pick the first address of the prefix, e.g. address 2a02:168:4a00::1 + // for prefix 2a02:168:4a00::/48 + server[len(server)-1] = 1 + options = append(options, (rdnss{ + lifetime: 1800, // seconds + server: server, + }).Marshal()) + } + s.mu.Unlock() + + buf := gopacket.NewSerializeBuffer() + if err := options.SerializeTo(buf, gopacket.SerializeOptions{FixLengths: true}); err != nil { + return err + } + msgbody = append(msgbody, buf.Bytes()...) + + msg := &icmp.Message{ + Type: ipv6.ICMPTypeRouterAdvertisement, + Code: 0, + Checksum: 0, // calculated by the kernel + Body: &icmp.DefaultMessageBody{msgbody}} + mb, err := msg.Marshal(nil) + if err != nil { + return err + } + log.Printf("sending to %s (%#v)", addr, addr) + if _, err := s.pc.WriteTo(mb, nil, addr); err != nil { + return err + } + return nil +}