+ DHCP: add ra_slaac_only, ra_allow_slaac config settings

Close # 2076

Squashed commit of the following:

commit 7a938fc6ffa0adc1e705f59778d7e7442b83d4b9
Merge: 3db2605e 6222d17d
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Thu Sep 24 16:34:23 2020 +0300

    Merge remote-tracking branch 'origin/master' into 2076-dhcp-ra-slaac

commit 3db2605efda79e8d361fb46c32bb9ec753de0e02
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Thu Sep 24 16:21:36 2020 +0300

    minor

commit de31c6aeebd78dce54ca54fbff010f3dd50ce974
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Thu Sep 24 12:23:09 2020 +0300

    don't initialize DHCPv6 server if we must force the clients to use SLAAC

commit 7c4239899c85880641dc5b4826ae34386bc5ee94
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Tue Sep 22 19:34:27 2020 +0300

    minor

commit 2b57688ce4c31d45d2236bd397e0b3041dac68c1
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Mon Sep 21 19:50:11 2020 +0300

    + DHCP: add ra_slaac_only, ra_allow_slaac config settings
This commit is contained in:
Simon Zolin 2020-09-24 18:33:11 +03:00
parent 6222d17d86
commit 990f531f54
6 changed files with 379 additions and 8 deletions

View File

@ -33,6 +33,7 @@ Contents:
* Static IP check/set * Static IP check/set
* API: Add a static lease * API: Add a static lease
* API: Reset DHCP configuration * API: Reset DHCP configuration
* RA+SLAAC
* DNS general settings * DNS general settings
* API: Get DNS general settings * API: Get DNS general settings
* API: Set DNS general settings * API: Set DNS general settings
@ -725,6 +726,53 @@ Response:
200 OK 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=<BOOL>
Other=<BOOL>
Option=Prefix information(3)
<IPv6 address prefix (/64) of the network interface>
Option=MTU(5)
<...>
Option=Source link-layer address(1)
<MAC address>
Option=Recursive DNS Server(25)
<IPv6 address of DNS server>
## TLS ## TLS

View File

@ -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. 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 ### Configure server
1. Edit server configuration file 'AdGuardHome.yaml', for example: 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: dhcp:
enabled: true enabled: true
interface_name: vboxnet0 interface_name: vboxnet0
dhcpv4:
gateway_ip: 192.168.56.1 gateway_ip: 192.168.56.1
subnet_mask: 255.255.255.0 subnet_mask: 255.255.255.0
range_start: 192.168.56.2 range_start: 192.168.56.2
range_end: 192.168.56.2 range_end: 192.168.56.2
lease_duration: 86400 lease_duration: 86400
icmp_timeout_msec: 1000 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 2. Start the server

239
dhcpd/router_adv.go Normal file
View File

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

31
dhcpd/router_adv_test.go Normal file
View File

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

View File

@ -83,6 +83,9 @@ type V6ServerConf struct {
LeaseDuration uint32 `yaml:"lease_duration"` // in seconds 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 ipStart net.IP // starting IP address for dynamic leases
leaseTime time.Duration // the time during which a dynamic lease is considered valid 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 dnsIPAddrs []net.IP // IPv6 addresses to return to DHCP clients as DNS server addresses

View File

@ -25,6 +25,8 @@ type v6Server struct {
ipAddrs [256]byte ipAddrs [256]byte
sid dhcpv6.Duid sid dhcpv6.Duid
ra raCtx // RA module
conf V6ServerConf conf V6ServerConf
} }
@ -557,6 +559,27 @@ func getIfaceIPv6(iface net.Interface) []net.IP {
return res 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 // Start - start server
func (s *v6Server) Start() error { func (s *v6Server) Start() error {
if !s.conf.Enabled { 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) return wrapErrPrint(err, "Couldn't find interface by name %s", s.conf.InterfaceName)
} }
log.Debug("DHCPv6: starting...")
s.conf.dnsIPAddrs = getIfaceIPv6(*iface) s.conf.dnsIPAddrs = getIfaceIPv6(*iface)
if len(s.conf.dnsIPAddrs) == 0 { if len(s.conf.dnsIPAddrs) == 0 {
log.Debug("DHCPv6: no IPv6 address for interface %s", iface.Name) log.Debug("DHCPv6: no IPv6 address for interface %s", iface.Name)
return nil 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 { if len(iface.HardwareAddr) != 6 {
return fmt.Errorf("DHCPv6: invalid MAC %s", iface.HardwareAddr) return fmt.Errorf("DHCPv6: invalid MAC %s", iface.HardwareAddr)
} }
@ -598,11 +633,15 @@ func (s *v6Server) Start() error {
err = s.srv.Serve() err = s.srv.Serve()
log.Debug("DHCPv6: srv.Serve: %s", err) log.Debug("DHCPv6: srv.Serve: %s", err)
}() }()
return nil return nil
} }
// Stop - stop server // Stop - stop server
func (s *v6Server) Stop() { func (s *v6Server) Stop() {
s.ra.Close()
// DHCPv6 server may not be initialized if ra_slaac_only=true
if s.srv == nil { if s.srv == nil {
return return
} }
@ -626,7 +665,7 @@ func v6Create(conf V6ServerConf) (DHCPServer, error) {
} }
s.conf.ipStart = net.ParseIP(conf.RangeStart) 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) return s, fmt.Errorf("DHCPv6: invalid range-start IP: %s", conf.RangeStart)
} }