+ 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:
parent
6222d17d86
commit
990f531f54
@ -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=<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
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
239
dhcpd/router_adv.go
Normal file
239
dhcpd/router_adv.go
Normal 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
31
dhcpd/router_adv_test.go
Normal 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))
|
||||
}
|
@ -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
|
||||
|
43
dhcpd/v6.go
43
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)
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user