add radvd
This commit is contained in:
parent
fa626839b1
commit
d3884d9074
55
cmd/radvd/radvd.go
Normal file
55
cmd/radvd/radvd.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
}
|
73
integrationradvd_test.go
Normal file
73
integrationradvd_test.go
Normal file
@ -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))
|
||||||
|
|
||||||
|
}
|
224
internal/radvd/radvd.go
Normal file
224
internal/radvd/radvd.go
Normal file
@ -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
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user