diff --git a/AGHTechDoc.md b/AGHTechDoc.md index 4f0c5c8d..5e9fa0d5 100644 --- a/AGHTechDoc.md +++ b/AGHTechDoc.md @@ -33,6 +33,7 @@ Contents: * Static IP check/set * API: Add a static lease * API: Reset DHCP configuration + * RA+SLAAC * DNS general settings * API: Get DNS general settings * API: Set DNS general settings @@ -725,6 +726,53 @@ Response: 200 OK +### RA+SLAAC + +There are 3 options for a client to get IPv6 address: + +1. via DHCPv6. + Client doesn't receive any `ICMPv6.RouterAdvertisement` packets, so it tries to use DHCPv6. +2. via SLAAC. + Client receives a `ICMPv6.RouterAdvertisement` packet with `Managed=false` flag and IPv6 prefix. + Client then assigns to itself an IPv6 address using this prefix and its MAC address. + DHCPv6 server won't be started in this case. +3. via DHCPv6 or SLAAC. + Client receives a `ICMPv6.RouterAdvertisement` packet with `Managed=true` flag and IPv6 prefix. + Client may choose to use SLAAC or DHCPv6 to obtain an IPv6 address. + +Configuration: + + dhcp: + ... + dhcpv6: + ... + ra_slaac_only: false + ra_allow_slaac: false + +* `ra_slaac_only:false; ra_allow_slaac:false`: use option #1. + Don't send any `ICMPv6.RouterAdvertisement` packets. +* `ra_slaac_only:true; ra_allow_slaac:false`: use option #2. + Periodically send `ICMPv6.RouterAdvertisement(Flags=(Managed=false,Other=false))` packets. +* `ra_slaac_only:false; ra_allow_slaac:true`: use option #3. + Periodically send `ICMPv6.RouterAdvertisement(Flags=(Managed=true,Other=true))` packets. + +ICMPv6.RouterAdvertisement packet description: + + ICMPv6: + Type=RouterAdvertisement(134) + Flags + Managed= + Other= + Option=Prefix information(3) + + Option=MTU(5) + <...> + Option=Source link-layer address(1) + + Option=Recursive DNS Server(25) + + + ## TLS diff --git a/dhcpd/README.md b/dhcpd/README.md index 83e48183..fb2bdc8d 100644 --- a/dhcpd/README.md +++ b/dhcpd/README.md @@ -30,6 +30,10 @@ To set up a test environment for DHCP server you need: 3. Start your VM, install an OS. Configure your network interface to use DHCP and the OS should ask for a IP address from our DHCP server. +4. To see the current IP address on client OS you can use `ip a` command on Linux or `ipconfig` on Windows. + +5. To force the client OS to request an IP from DHCP server again, you can use `dhclient` on Linux or `ipconfig /release` on Windows. + ### Configure server 1. Edit server configuration file 'AdGuardHome.yaml', for example: @@ -37,12 +41,19 @@ To set up a test environment for DHCP server you need: dhcp: enabled: true interface_name: vboxnet0 - gateway_ip: 192.168.56.1 - subnet_mask: 255.255.255.0 - range_start: 192.168.56.2 - range_end: 192.168.56.2 - lease_duration: 86400 - icmp_timeout_msec: 1000 + dhcpv4: + gateway_ip: 192.168.56.1 + subnet_mask: 255.255.255.0 + range_start: 192.168.56.2 + range_end: 192.168.56.2 + lease_duration: 86400 + icmp_timeout_msec: 1000 + options: [] + dhcpv6: + range_start: 2001::1 + lease_duration: 86400 + ra_slaac_only: false + ra_allow_slaac: false 2. Start the server diff --git a/dhcpd/router_adv.go b/dhcpd/router_adv.go new file mode 100644 index 00000000..f4ecae19 --- /dev/null +++ b/dhcpd/router_adv.go @@ -0,0 +1,239 @@ +package dhcpd + +import ( + "encoding/binary" + "fmt" + "net" + "sync/atomic" + "time" + + "github.com/AdguardTeam/golibs/log" + "golang.org/x/net/icmp" + "golang.org/x/net/ipv6" +) + +type raCtx struct { + raAllowSlaac bool // send RA packets without MO flags + raSlaacOnly bool // send RA packets with MO flags + ipAddr net.IP // source IP address (link-local-unicast) + dnsIPAddr net.IP // IP address for DNS Server option + prefixIPAddr net.IP // IP address for Prefix option + ifaceName string + iface *net.Interface + packetSendPeriod time.Duration // how often RA packets are sent + + conn *icmp.PacketConn // ICMPv6 socket + stop atomic.Value // stop the packet sending loop +} + +type icmpv6RA struct { + managedAddressConfiguration bool + otherConfiguration bool + prefix net.IP + prefixLen int + sourceLinkLayerAddress net.HardwareAddr + recursiveDNSServer net.IP + mtu uint32 +} + +// Create an ICMPv6.RouterAdvertisement packet with all necessary options. +// +// ICMPv6: +// type[1] +// code[1] +// chksum[2] +// body (RouterAdvertisement): +// Cur Hop Limit[1] +// Flags[1]: MO...... +// Router Lifetime[2] +// Reachable Time[4] +// Retrans Timer[4] +// Option=Prefix Information(3): +// Type[1] +// Length * 8bytes[1] +// Prefix Length[1] +// Flags[1]: LA...... +// Valid Lifetime[4] +// Preferred Lifetime[4] +// Reserved[4] +// Prefix[16] +// Option=MTU(5): +// Type[1] +// Length * 8bytes[1] +// Reserved[2] +// MTU[4] +// Option=Source link-layer address(1): +// Link-Layer Address[6] +// Option=Recursive DNS Server(25): +// Type[1] +// Length * 8bytes[1] +// Reserved[2] +// Lifetime[4] +// Addresses of IPv6 Recursive DNS Servers[16] +func createICMPv6RAPacket(params icmpv6RA) []byte { + data := make([]byte, 88) + i := 0 + + // ICMPv6: + + data[i] = 134 // type + data[i+1] = 0 // code + data[i+2] = 0 // chksum + data[i+3] = 0 + i += 4 + + // RouterAdvertisement: + + data[i] = 64 // Cur Hop Limit[1] + i++ + + data[i] = 0 // Flags[1]: MO...... + if params.managedAddressConfiguration { + data[i] |= 0x80 + } + if params.otherConfiguration { + data[i] |= 0x40 + } + i++ + + binary.BigEndian.PutUint16(data[i:], 1800) // Router Lifetime[2] + i += 2 + binary.BigEndian.PutUint32(data[i:], 0) // Reachable Time[4] + i += 4 + binary.BigEndian.PutUint32(data[i:], 0) // Retrans Timer[4] + i += 4 + + // Option=Prefix Information: + + data[i] = 3 // Type + data[i+1] = 4 // Length + i += 2 + data[i] = byte(params.prefixLen) // Prefix Length[1] + i++ + data[i] = 0xc0 // Flags[1] + i++ + binary.BigEndian.PutUint32(data[i:], 3600) // Valid Lifetime[4] + i += 4 + binary.BigEndian.PutUint32(data[i:], 3600) // Preferred Lifetime[4] + i += 4 + binary.BigEndian.PutUint32(data[i:], 0) // Reserved[4] + i += 4 + copy(data[i:], params.prefix[:8]) // Prefix[16] + binary.BigEndian.PutUint32(data[i+8:], 0) + binary.BigEndian.PutUint32(data[i+12:], 0) + i += 16 + + // Option=MTU: + + data[i] = 5 // Type + data[i+1] = 1 // Length + i += 2 + binary.BigEndian.PutUint16(data[i:], 0) // Reserved[2] + i += 2 + binary.BigEndian.PutUint32(data[i:], params.mtu) // MTU[4] + i += 4 + + // Option=Source link-layer address: + + data[i] = 1 // Type + data[i+1] = 1 // Length + i += 2 + copy(data[i:], params.sourceLinkLayerAddress) // Link-Layer Address[6] + i += 6 + + // Option=Recursive DNS Server: + + data[i] = 25 // Type + data[i+1] = 3 // Length + i += 2 + binary.BigEndian.PutUint16(data[i:], 0) // Reserved[2] + i += 2 + binary.BigEndian.PutUint32(data[i:], 3600) // Lifetime[4] + i += 4 + copy(data[i:], params.recursiveDNSServer) // Addresses of IPv6 Recursive DNS Servers[16] + + return data +} + +// Init - initialize RA module +func (ra *raCtx) Init() error { + ra.stop.Store(0) + ra.conn = nil + if !(ra.raAllowSlaac || ra.raSlaacOnly) { + return nil + } + + log.Debug("DHCPv6 RA: source IP address: %s DNS IP address: %s", + ra.ipAddr, ra.dnsIPAddr) + + params := icmpv6RA{ + managedAddressConfiguration: !ra.raSlaacOnly, + otherConfiguration: !ra.raSlaacOnly, + mtu: uint32(ra.iface.MTU), + prefixLen: 64, + recursiveDNSServer: ra.dnsIPAddr, + sourceLinkLayerAddress: ra.iface.HardwareAddr, + } + params.prefix = make([]byte, 16) + copy(params.prefix, ra.prefixIPAddr[:8]) // /64 + + data := createICMPv6RAPacket(params) + + var err error + ipAndScope := ra.ipAddr.String() + "%" + ra.ifaceName + ra.conn, err = icmp.ListenPacket("ip6:ipv6-icmp", ipAndScope) + if err != nil { + return fmt.Errorf("DHCPv6 RA: icmp.ListenPacket: %s", err) + } + success := false + defer func() { + if !success { + ra.Close() + } + }() + + con6 := ra.conn.IPv6PacketConn() + + if err := con6.SetHopLimit(255); err != nil { + return fmt.Errorf("DHCPv6 RA: SetHopLimit: %s", err) + } + + if err := con6.SetMulticastHopLimit(255); err != nil { + return fmt.Errorf("DHCPv6 RA: SetMulticastHopLimit: %s", err) + } + + msg := &ipv6.ControlMessage{ + HopLimit: 255, + Src: ra.ipAddr, + IfIndex: ra.iface.Index, + } + addr := &net.UDPAddr{ + IP: net.ParseIP("ff02::1"), + } + + go func() { + log.Debug("DHCPv6 RA: starting to send periodic RouterAdvertisement packets") + for ra.stop.Load() == 0 { + _, err = con6.WriteTo(data, msg, addr) + if err != nil { + log.Error("DHCPv6 RA: WriteTo: %s", err) + } + time.Sleep(ra.packetSendPeriod) + } + log.Debug("DHCPv6 RA: loop exit") + }() + + success = true + return nil +} + +// Close - close module +func (ra *raCtx) Close() { + log.Debug("DHCPv6 RA: closing") + + ra.stop.Store(1) + + if ra.conn != nil { + ra.conn.Close() + } +} diff --git a/dhcpd/router_adv_test.go b/dhcpd/router_adv_test.go new file mode 100644 index 00000000..95f3d4fa --- /dev/null +++ b/dhcpd/router_adv_test.go @@ -0,0 +1,31 @@ +package dhcpd + +import ( + "bytes" + "net" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestRA(t *testing.T) { + ra := icmpv6RA{ + managedAddressConfiguration: false, + otherConfiguration: true, + mtu: 1500, + prefix: net.ParseIP("1234::"), + prefixLen: 64, + recursiveDNSServer: net.ParseIP("fe80::800:27ff:fe00:0"), + sourceLinkLayerAddress: []byte{0x0a, 0x00, 0x27, 0x00, 0x00, 0x00}, + } + data := createICMPv6RAPacket(ra) + dataCorrect := []byte{ + 0x86, 0x00, 0x00, 0x00, 0x40, 0x40, 0x07, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x03, 0x04, 0x40, 0xc0, 0x00, 0x00, 0x0e, 0x10, 0x00, 0x00, 0x0e, 0x10, 0x00, 0x00, 0x00, 0x00, + 0x12, 0x34, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x05, 0x01, 0x00, 0x00, 0x00, 0x00, 0x05, 0xdc, 0x01, 0x01, 0x0a, 0x00, 0x27, 0x00, 0x00, 0x00, + 0x19, 0x03, 0x00, 0x00, 0x00, 0x00, 0x0e, 0x10, 0xfe, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x08, 0x00, 0x27, 0xff, 0xfe, 0x00, 0x00, 0x00, + } + assert.True(t, bytes.Equal(data, dataCorrect)) +} diff --git a/dhcpd/server.go b/dhcpd/server.go index 1701c780..240715ca 100644 --- a/dhcpd/server.go +++ b/dhcpd/server.go @@ -83,6 +83,9 @@ type V6ServerConf struct { LeaseDuration uint32 `yaml:"lease_duration"` // in seconds + RaSlaacOnly bool `yaml:"ra_slaac_only"` // send ICMPv6.RA packets without MO flags + RaAllowSlaac bool `yaml:"ra_allow_slaac"` // send ICMPv6.RA packets with MO flags + ipStart net.IP // starting IP address for dynamic leases leaseTime time.Duration // the time during which a dynamic lease is considered valid dnsIPAddrs []net.IP // IPv6 addresses to return to DHCP clients as DNS server addresses diff --git a/dhcpd/v6.go b/dhcpd/v6.go index 5259777a..23c8b88b 100644 --- a/dhcpd/v6.go +++ b/dhcpd/v6.go @@ -25,6 +25,8 @@ type v6Server struct { ipAddrs [256]byte sid dhcpv6.Duid + ra raCtx // RA module + conf V6ServerConf } @@ -557,6 +559,27 @@ func getIfaceIPv6(iface net.Interface) []net.IP { return res } +// initialize RA module +func (s *v6Server) initRA(iface *net.Interface) error { + // choose the source IP address - should be link-local-unicast + s.ra.ipAddr = s.conf.dnsIPAddrs[0] + for _, ip := range s.conf.dnsIPAddrs { + if ip.IsLinkLocalUnicast() { + s.ra.ipAddr = ip + break + } + } + + s.ra.raAllowSlaac = s.conf.RaAllowSlaac + s.ra.raSlaacOnly = s.conf.RaSlaacOnly + s.ra.dnsIPAddr = s.ra.ipAddr + s.ra.prefixIPAddr = s.conf.ipStart + s.ra.ifaceName = s.conf.InterfaceName + s.ra.iface = iface + s.ra.packetSendPeriod = 1 * time.Second + return s.ra.Init() +} + // Start - start server func (s *v6Server) Start() error { if !s.conf.Enabled { @@ -568,13 +591,25 @@ func (s *v6Server) Start() error { return wrapErrPrint(err, "Couldn't find interface by name %s", s.conf.InterfaceName) } - log.Debug("DHCPv6: starting...") s.conf.dnsIPAddrs = getIfaceIPv6(*iface) if len(s.conf.dnsIPAddrs) == 0 { log.Debug("DHCPv6: no IPv6 address for interface %s", iface.Name) return nil } + err = s.initRA(iface) + if err != nil { + return err + } + + // don't initialize DHCPv6 server if we must force the clients to use SLAAC + if s.conf.RaSlaacOnly { + log.Debug("DHCPv6: not starting DHCPv6 server due to ra_slaac_only=true") + return nil + } + + log.Debug("DHCPv6: starting...") + if len(iface.HardwareAddr) != 6 { return fmt.Errorf("DHCPv6: invalid MAC %s", iface.HardwareAddr) } @@ -598,11 +633,15 @@ func (s *v6Server) Start() error { err = s.srv.Serve() log.Debug("DHCPv6: srv.Serve: %s", err) }() + return nil } // Stop - stop server func (s *v6Server) Stop() { + s.ra.Close() + + // DHCPv6 server may not be initialized if ra_slaac_only=true if s.srv == nil { return } @@ -626,7 +665,7 @@ func v6Create(conf V6ServerConf) (DHCPServer, error) { } s.conf.ipStart = net.ParseIP(conf.RangeStart) - if s.conf.ipStart == nil { + if s.conf.ipStart == nil || s.conf.ipStart.To16() == nil { return s, fmt.Errorf("DHCPv6: invalid range-start IP: %s", conf.RangeStart) }