mirror of
https://github.com/AdguardTeam/AdGuardHome.git
synced 2026-08-04 15:28:58 +00:00
Align DHCPv6 prefix tracking with code guidelines
This commit is contained in:
parent
2ebbeb2867
commit
f70bb2ba2a
12 changed files with 1224 additions and 680 deletions
|
|
@ -481,29 +481,32 @@ Response:
|
|||
200 OK
|
||||
|
||||
{
|
||||
"enabled":false,
|
||||
"interface_name":"...",
|
||||
"v4":{
|
||||
"gateway_ip":"...",
|
||||
"subnet_mask":"...",
|
||||
"range_start":"...", // if empty: DHCPv4 won't be enabled
|
||||
"range_end":"...",
|
||||
"lease_duration":60,
|
||||
},
|
||||
"v6":{
|
||||
"prefix_source":"static",
|
||||
"range_start":"...", // if empty: DHCPv6 won't be enabled
|
||||
"lease_duration":60,
|
||||
},
|
||||
"leases":[
|
||||
{"ip":"...","mac":"...","hostname":"...","expires":"..."}
|
||||
...
|
||||
],
|
||||
"static_leases":[
|
||||
{"ip":"...","mac":"...","hostname":"..."}
|
||||
...
|
||||
]
|
||||
}
|
||||
```none
|
||||
{
|
||||
"enabled":false,
|
||||
"interface_name":"...",
|
||||
"v4":{
|
||||
"gateway_ip":"...",
|
||||
"subnet_mask":"...",
|
||||
"range_start":"...", // if empty: DHCPv4 won't be enabled
|
||||
"range_end":"...",
|
||||
"lease_duration":60,
|
||||
},
|
||||
"v6":{
|
||||
"prefix_source":"static",
|
||||
"range_start":"...", // if empty: DHCPv6 won't be enabled
|
||||
"lease_duration":60,
|
||||
},
|
||||
"leases":[
|
||||
{"ip":"...","mac":"...","hostname":"...","expires":"..."}
|
||||
...
|
||||
],
|
||||
"static_leases":[
|
||||
{"ip":"...","mac":"...","hostname":"..."}
|
||||
...
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### API: Check DHCP
|
||||
|
|
@ -558,24 +561,26 @@ If `static_ip.static` is:
|
|||
|
||||
Request:
|
||||
|
||||
POST /control/dhcp/set_config
|
||||
```none
|
||||
POST /control/dhcp/set_config
|
||||
|
||||
{
|
||||
"enabled":true,
|
||||
"interface_name":"vboxnet0",
|
||||
"v4":{
|
||||
"gateway_ip":"192.169.56.1",
|
||||
"subnet_mask":"255.255.255.0",
|
||||
"range_start":"192.169.56.100",
|
||||
"range_end":"192.169.56.200", // Note: first 3 octets must match "range_start"
|
||||
"lease_duration":60,
|
||||
},
|
||||
"v6":{
|
||||
"prefix_source":"static",
|
||||
"range_start":"...",
|
||||
"lease_duration":60,
|
||||
}
|
||||
}
|
||||
{
|
||||
"enabled":true,
|
||||
"interface_name":"vboxnet0",
|
||||
"v4":{
|
||||
"gateway_ip":"192.169.56.1",
|
||||
"subnet_mask":"255.255.255.0",
|
||||
"range_start":"192.169.56.100",
|
||||
"range_end":"192.169.56.200", // Note: first 3 octets must match "range_start"
|
||||
"lease_duration":60,
|
||||
},
|
||||
"v6":{
|
||||
"prefix_source":"static",
|
||||
"range_start":"...",
|
||||
"lease_duration":60,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
|
|
@ -760,8 +765,11 @@ Configuration:
|
|||
* `ra_slaac_only:false; ra_allow_slaac:true`: use option #3.
|
||||
Periodically send `ICMPv6.RouterAdvertisement(Flags=(Managed=true,Other=true))` packets.
|
||||
|
||||
For IPv6 prefix tracking, `prefix_source:static` keeps the current legacy behavior.
|
||||
`prefix_source:interface` derives the advertised prefix from the interface, keeps `range_start` as a host template for the dynamic pool, and deprecates the previous prefix with preferred lifetime `0` and a bounded valid lifetime when renumbering is observed.
|
||||
For IPv6 prefix tracking, `prefix_source:static` keeps the current
|
||||
legacy behavior. `prefix_source:interface` derives the advertised
|
||||
prefix from the interface, keeps `range_start` as a host template for
|
||||
the dynamic pool, and deprecates the previous prefix with preferred
|
||||
lifetime `0` and a bounded valid lifetime when renumbering is observed.
|
||||
|
||||
ICMPv6.RouterAdvertisement packet description:
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,9 @@ NOTE: Add new changes BELOW THIS COMMENT.
|
|||
|
||||
### Added
|
||||
|
||||
- Opt-in IPv6 prefix tracking for DHCPv6 and Router Advertisements. When enabled, AdGuard Home derives the advertised prefix from the interface and deprecates the previous prefix during renumbering.
|
||||
- Opt-in IPv6 prefix tracking for DHCPv6 and Router Advertisements.
|
||||
When enabled, AdGuard Home derives the advertised prefix from the
|
||||
interface and deprecates the previous prefix during renumbering.
|
||||
|
||||
### Changed
|
||||
|
||||
|
|
|
|||
|
|
@ -52,41 +52,9 @@ func parseIfconfigIPv6Addr(fields []string) (state IPv6AddrState, err error) {
|
|||
prefixBits := -1
|
||||
|
||||
for i := 2; i < len(fields); i++ {
|
||||
switch strings.ToLower(fields[i]) {
|
||||
case "prefixlen":
|
||||
i++
|
||||
if i >= len(fields) {
|
||||
return IPv6AddrState{}, fmt.Errorf("missing prefixlen value in %q", strings.Join(fields, " "))
|
||||
}
|
||||
|
||||
prefixBits, err = strconv.Atoi(fields[i])
|
||||
if err != nil {
|
||||
return IPv6AddrState{}, fmt.Errorf("parsing prefixlen %q: %w", fields[i], err)
|
||||
}
|
||||
case "pltime":
|
||||
i++
|
||||
if i >= len(fields) {
|
||||
return IPv6AddrState{}, fmt.Errorf("missing pltime value in %q", strings.Join(fields, " "))
|
||||
}
|
||||
|
||||
preferred, err = parseIPv6Lifetime(fields[i])
|
||||
if err != nil {
|
||||
return IPv6AddrState{}, fmt.Errorf("parsing pltime %q: %w", fields[i], err)
|
||||
}
|
||||
case "vltime":
|
||||
i++
|
||||
if i >= len(fields) {
|
||||
return IPv6AddrState{}, fmt.Errorf("missing vltime value in %q", strings.Join(fields, " "))
|
||||
}
|
||||
|
||||
valid, err = parseIPv6Lifetime(fields[i])
|
||||
if err != nil {
|
||||
return IPv6AddrState{}, fmt.Errorf("parsing vltime %q: %w", fields[i], err)
|
||||
}
|
||||
case "temporary":
|
||||
state.Temporary = true
|
||||
case "tentative":
|
||||
state.Tentative = true
|
||||
i, err = parseIfconfigIPv6AddrField(fields, i, &state, &preferred, &valid, &prefixBits)
|
||||
if err != nil {
|
||||
return IPv6AddrState{}, err
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -104,15 +72,86 @@ func parseIfconfigIPv6Addr(fields []string) (state IPv6AddrState, err error) {
|
|||
}, nil
|
||||
}
|
||||
|
||||
// parseIfconfigIPv6AddrField parses one token from an ifconfig IPv6 address
|
||||
// line.
|
||||
func parseIfconfigIPv6AddrField(
|
||||
fields []string,
|
||||
i int,
|
||||
state *IPv6AddrState,
|
||||
preferred, valid *uint32,
|
||||
prefixBits *int,
|
||||
) (next int, err error) {
|
||||
switch strings.ToLower(fields[i]) {
|
||||
case "prefixlen":
|
||||
return parseIfconfigIPv6AddrInt(fields, i, "prefixlen", func(v int) {
|
||||
*prefixBits = v
|
||||
})
|
||||
case "pltime":
|
||||
return parseIfconfigIPv6AddrLifetime(fields, i, "pltime", preferred)
|
||||
case "vltime":
|
||||
return parseIfconfigIPv6AddrLifetime(fields, i, "vltime", valid)
|
||||
case "temporary":
|
||||
state.Temporary = true
|
||||
case "tentative":
|
||||
state.Tentative = true
|
||||
}
|
||||
|
||||
return i, nil
|
||||
}
|
||||
|
||||
// parseIfconfigIPv6AddrInt parses one int token from an ifconfig IPv6 address
|
||||
// line.
|
||||
func parseIfconfigIPv6AddrInt(
|
||||
fields []string,
|
||||
i int,
|
||||
name string,
|
||||
set func(int),
|
||||
) (next int, err error) {
|
||||
i++
|
||||
if i >= len(fields) {
|
||||
return i, fmt.Errorf("missing %s value in %q", name, strings.Join(fields, " "))
|
||||
}
|
||||
|
||||
v, err := strconv.Atoi(fields[i])
|
||||
if err != nil {
|
||||
return i, fmt.Errorf("parsing %s %q: %w", name, fields[i], err)
|
||||
}
|
||||
|
||||
set(v)
|
||||
|
||||
return i, nil
|
||||
}
|
||||
|
||||
// parseIfconfigIPv6AddrLifetime parses one IPv6 lifetime token from an
|
||||
// ifconfig IPv6 address line.
|
||||
func parseIfconfigIPv6AddrLifetime(
|
||||
fields []string,
|
||||
i int,
|
||||
name string,
|
||||
lifetime *uint32,
|
||||
) (next int, err error) {
|
||||
i++
|
||||
if i >= len(fields) {
|
||||
return i, fmt.Errorf("missing %s value in %q", name, strings.Join(fields, " "))
|
||||
}
|
||||
|
||||
*lifetime, err = parseIPv6Lifetime(fields[i])
|
||||
if err != nil {
|
||||
return i, fmt.Errorf("parsing %s %q: %w", name, fields[i], err)
|
||||
}
|
||||
|
||||
return i, nil
|
||||
}
|
||||
|
||||
// parseIPv6Lifetime parses an IPv6 lifetime token from command output.
|
||||
func parseIPv6Lifetime(s string) (sec uint32, err error) {
|
||||
switch strings.ToLower(s) {
|
||||
case "forever", "infinity", "infinite", "infty":
|
||||
return math.MaxUint32, nil
|
||||
default:
|
||||
v, err := strconv.ParseUint(s, 10, 32)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
v, parseErr := strconv.ParseUint(s, 10, 32)
|
||||
if parseErr != nil {
|
||||
return 0, parseErr
|
||||
}
|
||||
|
||||
return uint32(v), nil
|
||||
|
|
|
|||
|
|
@ -9,24 +9,19 @@ import (
|
|||
"log/slog"
|
||||
"net"
|
||||
"net/netip"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/osutil/executil"
|
||||
"github.com/mdlayher/netlink"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// ObserveIPv6Addrs returns IPv6 interface address state for ifaceName.
|
||||
//
|
||||
// ctx is accepted to match the BSD implementations (which run ifconfig under
|
||||
// an [executil.CommandConstructor] and can be cancelled) but is not honored
|
||||
// here: syscall.NetlinkRIB is synchronous and uncancellable from outside the
|
||||
// call. Wrapping it in a goroutine that selects on ctx.Done() would only
|
||||
// hide a stuck kernel from the caller while leaking the blocked goroutine on
|
||||
// every retry, which is strictly worse than failing fast on the caller side
|
||||
// and letting the operator notice a genuinely broken environment.
|
||||
// rtnetlink responds in microseconds under normal conditions, so the lack of
|
||||
// cancellation is acceptable in practice.
|
||||
// ctx is accepted to match the BSD implementations, which run ifconfig under
|
||||
// an [executil.CommandConstructor] and can be canceled. Linux uses a netlink
|
||||
// socket instead, so we still accept ctx for API compatibility but do not
|
||||
// consult it while receiving the dump reply.
|
||||
func ObserveIPv6Addrs(
|
||||
_ context.Context,
|
||||
_ *slog.Logger,
|
||||
|
|
@ -38,53 +33,40 @@ func ObserveIPv6Addrs(
|
|||
return nil, fmt.Errorf("finding interface %s: %w", ifaceName, err)
|
||||
}
|
||||
|
||||
rib, err := syscall.NetlinkRIB(syscall.RTM_GETADDR, syscall.AF_INET6)
|
||||
conn, err := netlink.Dial(unix.NETLINK_ROUTE, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("dialing rtnetlink: %w", err)
|
||||
}
|
||||
defer func() { err = errors.WithDeferred(err, conn.Close()) }()
|
||||
|
||||
msgs, err := conn.Execute(netlink.Message{
|
||||
Header: netlink.Header{
|
||||
Type: netlink.HeaderType(unix.RTM_GETADDR),
|
||||
Flags: netlink.Request | netlink.Dump,
|
||||
},
|
||||
Data: []byte{unix.AF_INET6},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("querying rtnetlink addrs: %w", err)
|
||||
}
|
||||
|
||||
msgs, err := syscall.ParseNetlinkMessage(rib)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing rtnetlink addrs: %w", err)
|
||||
}
|
||||
|
||||
return parseIPv6AddrStatesNetlink(msgs, iface.Index)
|
||||
}
|
||||
|
||||
// parseIPv6AddrStatesNetlink parses IPv6 address state from netlink messages.
|
||||
func parseIPv6AddrStatesNetlink(
|
||||
msgs []syscall.NetlinkMessage,
|
||||
msgs []netlink.Message,
|
||||
ifIndex int,
|
||||
) (states []IPv6AddrState, err error) {
|
||||
loop:
|
||||
for _, msg := range msgs {
|
||||
switch msg.Header.Type {
|
||||
case syscall.NLMSG_DONE:
|
||||
break loop
|
||||
case syscall.RTM_NEWADDR:
|
||||
// Go on.
|
||||
default:
|
||||
continue
|
||||
}
|
||||
|
||||
if len(msg.Data) < syscall.SizeofIfAddrmsg {
|
||||
return nil, fmt.Errorf("short ifaddrmsg payload")
|
||||
}
|
||||
|
||||
ifam := (*syscall.IfAddrmsg)(unsafe.Pointer(&msg.Data[0]))
|
||||
if ifam.Family != syscall.AF_INET6 || int(ifam.Index) != ifIndex {
|
||||
continue
|
||||
}
|
||||
|
||||
attrs, err := syscall.ParseNetlinkRouteAttr(&msg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing route attrs: %w", err)
|
||||
}
|
||||
|
||||
state, ok, err := parseIPv6AddrStateNetlink(ifam, attrs)
|
||||
state, done, ok, err := parseIPv6AddrStateMessage(msg, ifIndex)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if ok {
|
||||
}
|
||||
if done {
|
||||
return states, nil
|
||||
}
|
||||
if ok {
|
||||
states = append(states, state)
|
||||
}
|
||||
}
|
||||
|
|
@ -92,44 +74,53 @@ loop:
|
|||
return states, nil
|
||||
}
|
||||
|
||||
// parseIPv6AddrStateMessage parses one netlink message carrying IPv6 address
|
||||
// state.
|
||||
func parseIPv6AddrStateMessage(
|
||||
msg netlink.Message,
|
||||
ifIndex int,
|
||||
) (state IPv6AddrState, done, ok bool, err error) {
|
||||
switch msg.Header.Type {
|
||||
case netlink.Done:
|
||||
return IPv6AddrState{}, true, false, nil
|
||||
case netlink.HeaderType(unix.RTM_NEWADDR):
|
||||
// Go on.
|
||||
default:
|
||||
return IPv6AddrState{}, false, false, nil
|
||||
}
|
||||
|
||||
ifam, err := parseIfAddrmsg(msg.Data)
|
||||
if err != nil {
|
||||
return IPv6AddrState{}, false, false, err
|
||||
}
|
||||
if ifam.Family != unix.AF_INET6 || int(ifam.Index) != ifIndex {
|
||||
return IPv6AddrState{}, false, false, nil
|
||||
}
|
||||
|
||||
attrs, err := netlink.UnmarshalAttributes(msg.Data[unix.SizeofIfAddrmsg:])
|
||||
if err != nil {
|
||||
return IPv6AddrState{}, false, false, fmt.Errorf("parsing route attrs: %w", err)
|
||||
}
|
||||
|
||||
state, ok, err = parseIPv6AddrStateNetlink(ifam, attrs)
|
||||
|
||||
return state, false, ok, err
|
||||
}
|
||||
|
||||
// parseIPv6AddrStateNetlink parses one IPv6 address state from the netlink
|
||||
// message data.
|
||||
func parseIPv6AddrStateNetlink(
|
||||
ifam *syscall.IfAddrmsg,
|
||||
attrs []syscall.NetlinkRouteAttr,
|
||||
ifam unix.IfAddrmsg,
|
||||
attrs []netlink.Attribute,
|
||||
) (state IPv6AddrState, ok bool, err error) {
|
||||
var addr netip.Addr
|
||||
var cache *unix.IfaCacheinfo
|
||||
var cache *ipv6AddrCacheInfo
|
||||
flags := uint32(ifam.Flags)
|
||||
|
||||
for _, attr := range attrs {
|
||||
switch attr.Attr.Type {
|
||||
case unix.IFA_LOCAL:
|
||||
addr, err = parseIPv6AddrAttr(attr.Value)
|
||||
if err != nil {
|
||||
return IPv6AddrState{}, false, fmt.Errorf("parsing ifa_local: %w", err)
|
||||
}
|
||||
case unix.IFA_ADDRESS:
|
||||
if addr.IsValid() {
|
||||
continue
|
||||
}
|
||||
|
||||
addr, err = parseIPv6AddrAttr(attr.Value)
|
||||
if err != nil {
|
||||
return IPv6AddrState{}, false, fmt.Errorf("parsing ifa_address: %w", err)
|
||||
}
|
||||
case unix.IFA_FLAGS:
|
||||
if len(attr.Value) < 4 {
|
||||
return IPv6AddrState{}, false, fmt.Errorf("short ifa_flags attribute")
|
||||
}
|
||||
|
||||
flags = binary.NativeEndian.Uint32(attr.Value[:4])
|
||||
case unix.IFA_CACHEINFO:
|
||||
if len(attr.Value) < unix.SizeofIfaCacheinfo {
|
||||
return IPv6AddrState{}, false, fmt.Errorf("short ifa_cacheinfo attribute")
|
||||
}
|
||||
|
||||
cache = (*unix.IfaCacheinfo)(unsafe.Pointer(&attr.Value[0]))
|
||||
addr, flags, cache, err = parseIPv6AddrStateNetlinkAttr(attr, addr, flags, cache)
|
||||
if err != nil {
|
||||
return IPv6AddrState{}, false, err
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -139,8 +130,8 @@ func parseIPv6AddrStateNetlink(
|
|||
|
||||
preferred, valid := uint32(^uint32(0)), uint32(^uint32(0))
|
||||
if cache != nil {
|
||||
preferred = cache.Prefered
|
||||
valid = cache.Valid
|
||||
preferred = cache.preferredLifetimeSec
|
||||
valid = cache.validLifetimeSec
|
||||
}
|
||||
|
||||
return IPv6AddrState{
|
||||
|
|
@ -153,6 +144,122 @@ func parseIPv6AddrStateNetlink(
|
|||
}, true, nil
|
||||
}
|
||||
|
||||
// parseIPv6AddrStateNetlinkAttr parses one IPv6 address attribute from a
|
||||
// netlink message.
|
||||
func parseIPv6AddrStateNetlinkAttr(
|
||||
attr netlink.Attribute,
|
||||
addr netip.Addr,
|
||||
flags uint32,
|
||||
cache *ipv6AddrCacheInfo,
|
||||
) (nextAddr netip.Addr, nextFlags uint32, nextCache *ipv6AddrCacheInfo, err error) {
|
||||
switch attr.Type {
|
||||
case unix.IFA_LOCAL:
|
||||
return parseIPv6AddrStateLocalAttr(attr.Data, flags, cache)
|
||||
case unix.IFA_ADDRESS:
|
||||
return parseIPv6AddrStateAddressAttr(attr.Data, addr, flags, cache)
|
||||
case unix.IFA_FLAGS:
|
||||
return parseIPv6AddrStateFlagsAttr(attr.Data, addr, cache)
|
||||
case unix.IFA_CACHEINFO:
|
||||
return parseIPv6AddrStateCacheAttr(attr.Data, addr, flags)
|
||||
default:
|
||||
return addr, flags, cache, nil
|
||||
}
|
||||
}
|
||||
|
||||
// parseIPv6AddrStateLocalAttr parses an IFA_LOCAL attribute.
|
||||
func parseIPv6AddrStateLocalAttr(
|
||||
data []byte,
|
||||
flags uint32,
|
||||
cache *ipv6AddrCacheInfo,
|
||||
) (addr netip.Addr, nextFlags uint32, nextCache *ipv6AddrCacheInfo, err error) {
|
||||
addr, err = parseIPv6AddrAttr(data)
|
||||
if err != nil {
|
||||
return netip.Addr{}, 0, nil, fmt.Errorf("parsing ifa_local: %w", err)
|
||||
}
|
||||
|
||||
return addr, flags, cache, nil
|
||||
}
|
||||
|
||||
// parseIPv6AddrStateAddressAttr parses an IFA_ADDRESS attribute.
|
||||
func parseIPv6AddrStateAddressAttr(
|
||||
data []byte,
|
||||
addr netip.Addr,
|
||||
flags uint32,
|
||||
cache *ipv6AddrCacheInfo,
|
||||
) (nextAddr netip.Addr, nextFlags uint32, nextCache *ipv6AddrCacheInfo, err error) {
|
||||
if addr.IsValid() {
|
||||
return addr, flags, cache, nil
|
||||
}
|
||||
|
||||
nextAddr, err = parseIPv6AddrAttr(data)
|
||||
if err != nil {
|
||||
return netip.Addr{}, 0, nil, fmt.Errorf("parsing ifa_address: %w", err)
|
||||
}
|
||||
|
||||
return nextAddr, flags, cache, nil
|
||||
}
|
||||
|
||||
// parseIPv6AddrStateFlagsAttr parses an IFA_FLAGS attribute.
|
||||
func parseIPv6AddrStateFlagsAttr(
|
||||
data []byte,
|
||||
addr netip.Addr,
|
||||
cache *ipv6AddrCacheInfo,
|
||||
) (nextAddr netip.Addr, nextFlags uint32, nextCache *ipv6AddrCacheInfo, err error) {
|
||||
if len(data) < 4 {
|
||||
return netip.Addr{}, 0, nil, fmt.Errorf("short ifa_flags attribute")
|
||||
}
|
||||
|
||||
return addr, binary.NativeEndian.Uint32(data[:4]), cache, nil
|
||||
}
|
||||
|
||||
// parseIPv6AddrStateCacheAttr parses an IFA_CACHEINFO attribute.
|
||||
func parseIPv6AddrStateCacheAttr(
|
||||
data []byte,
|
||||
addr netip.Addr,
|
||||
flags uint32,
|
||||
) (nextAddr netip.Addr, nextFlags uint32, nextCache *ipv6AddrCacheInfo, err error) {
|
||||
ifaCacheInfo, err := parseIfaCacheinfo(data)
|
||||
if err != nil {
|
||||
return netip.Addr{}, 0, nil, err
|
||||
}
|
||||
|
||||
return addr, flags, &ifaCacheInfo, nil
|
||||
}
|
||||
|
||||
// ipv6AddrCacheInfo is the lifetime subset of Linux ifa_cacheinfo used by
|
||||
// IPv6 address observation.
|
||||
type ipv6AddrCacheInfo struct {
|
||||
preferredLifetimeSec uint32
|
||||
validLifetimeSec uint32
|
||||
}
|
||||
|
||||
// parseIfAddrmsg parses one Linux ifaddrmsg structure.
|
||||
func parseIfAddrmsg(b []byte) (ifam unix.IfAddrmsg, err error) {
|
||||
if len(b) < unix.SizeofIfAddrmsg {
|
||||
return unix.IfAddrmsg{}, fmt.Errorf("short ifaddrmsg payload")
|
||||
}
|
||||
|
||||
return unix.IfAddrmsg{
|
||||
Family: b[0],
|
||||
Prefixlen: b[1],
|
||||
Flags: b[2],
|
||||
Scope: b[3],
|
||||
Index: binary.NativeEndian.Uint32(b[4:8]),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// parseIfaCacheinfo parses one Linux ifa_cacheinfo structure.
|
||||
func parseIfaCacheinfo(b []byte) (cache ipv6AddrCacheInfo, err error) {
|
||||
if len(b) < unix.SizeofIfaCacheinfo {
|
||||
return ipv6AddrCacheInfo{}, fmt.Errorf("short ifa_cacheinfo attribute")
|
||||
}
|
||||
|
||||
return ipv6AddrCacheInfo{
|
||||
preferredLifetimeSec: binary.NativeEndian.Uint32(b[0:4]),
|
||||
validLifetimeSec: binary.NativeEndian.Uint32(b[4:8]),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// parseIPv6AddrAttr parses one IPv6 address attribute.
|
||||
func parseIPv6AddrAttr(b []byte) (addr netip.Addr, err error) {
|
||||
if len(b) < net.IPv6len {
|
||||
|
|
|
|||
|
|
@ -5,10 +5,9 @@ package aghnet
|
|||
import (
|
||||
"encoding/binary"
|
||||
"net/netip"
|
||||
"syscall"
|
||||
"testing"
|
||||
"unsafe"
|
||||
|
||||
"github.com/mdlayher/netlink"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/sys/unix"
|
||||
|
|
@ -19,24 +18,22 @@ func TestParseIPv6AddrStateNetlink(t *testing.T) {
|
|||
flags := make([]byte, 4)
|
||||
binary.NativeEndian.PutUint32(flags, unix.IFA_F_TEMPORARY|unix.IFA_F_TENTATIVE)
|
||||
|
||||
cache := unix.IfaCacheinfo{
|
||||
Prefered: 600,
|
||||
Valid: 1200,
|
||||
}
|
||||
cacheBytes := *(*[unix.SizeofIfaCacheinfo]byte)(unsafe.Pointer(&cache))
|
||||
cacheBytes := make([]byte, unix.SizeofIfaCacheinfo)
|
||||
binary.NativeEndian.PutUint32(cacheBytes[0:4], 600)
|
||||
binary.NativeEndian.PutUint32(cacheBytes[4:8], 1200)
|
||||
|
||||
state, ok, err := parseIPv6AddrStateNetlink(&syscall.IfAddrmsg{
|
||||
Family: syscall.AF_INET6,
|
||||
state, ok, err := parseIPv6AddrStateNetlink(unix.IfAddrmsg{
|
||||
Family: unix.AF_INET6,
|
||||
Prefixlen: 64,
|
||||
}, []syscall.NetlinkRouteAttr{{
|
||||
Attr: syscall.RtAttr{Type: unix.IFA_ADDRESS},
|
||||
Value: addr[:],
|
||||
}, []netlink.Attribute{{
|
||||
Type: unix.IFA_ADDRESS,
|
||||
Data: addr[:],
|
||||
}, {
|
||||
Attr: syscall.RtAttr{Type: unix.IFA_FLAGS},
|
||||
Value: flags,
|
||||
Type: unix.IFA_FLAGS,
|
||||
Data: flags,
|
||||
}, {
|
||||
Attr: syscall.RtAttr{Type: unix.IFA_CACHEINFO},
|
||||
Value: cacheBytes[:],
|
||||
Type: unix.IFA_CACHEINFO,
|
||||
Data: cacheBytes,
|
||||
}})
|
||||
require.NoError(t, err)
|
||||
require.True(t, ok)
|
||||
|
|
|
|||
|
|
@ -272,10 +272,14 @@ type V6ServerConf struct {
|
|||
// If it is empty, the configured static prefix semantics are used.
|
||||
PrefixSource V6PrefixSource `yaml:"prefix_source" json:"prefix_source"`
|
||||
|
||||
LeaseDuration uint32 `yaml:"lease_duration" json:"lease_duration"` // in seconds
|
||||
// LeaseDuration is the DHCPv6 lease duration, in seconds.
|
||||
LeaseDuration uint32 `yaml:"lease_duration" json:"lease_duration"`
|
||||
|
||||
RASLAACOnly bool `yaml:"ra_slaac_only" json:"ra_slaac_only"` // send ICMPv6.RA packets without MO flags
|
||||
RAAllowSLAAC bool `yaml:"ra_allow_slaac" json:"-"` // send ICMPv6.RA packets with MO flags
|
||||
// RASLAACOnly sends ICMPv6 Router Advertisements without M or O flags.
|
||||
RASLAACOnly bool `yaml:"ra_slaac_only" json:"ra_slaac_only"`
|
||||
|
||||
// RAAllowSLAAC sends ICMPv6 Router Advertisements with M and O flags.
|
||||
RAAllowSLAAC bool `yaml:"ra_allow_slaac" json:"-"`
|
||||
|
||||
ipStart net.IP // starting IP address for dynamic leases
|
||||
leaseTime time.Duration // the time during which a dynamic lease is considered valid
|
||||
|
|
|
|||
|
|
@ -51,6 +51,26 @@ type dbDeprecatedPrefix struct {
|
|||
ValidUntil string `json:"valid_until"`
|
||||
}
|
||||
|
||||
// v6MetaRestorer restores persisted DHCPv6 prefix metadata into the running
|
||||
// server implementation.
|
||||
type v6MetaRestorer interface {
|
||||
DHCPServer
|
||||
setRestoredPrefixMeta(
|
||||
renewable map[netip.Prefix]struct{},
|
||||
deprecated map[netip.Prefix]time.Time,
|
||||
)
|
||||
}
|
||||
|
||||
// v6Snapshotter returns the DHCPv6 leases and prefix metadata for persistence.
|
||||
type v6Snapshotter interface {
|
||||
DHCPServer
|
||||
dbSnapshot(now time.Time) (
|
||||
leases []*dhcpsvc.Lease,
|
||||
renewable map[netip.Prefix]struct{},
|
||||
deprecated map[netip.Prefix]time.Time,
|
||||
)
|
||||
}
|
||||
|
||||
// dbLease is the structure of stored lease.
|
||||
type dbLease struct {
|
||||
Expiry string `json:"expires"`
|
||||
|
|
@ -121,13 +141,36 @@ func (s *server) dbLoad() (err error) {
|
|||
return fmt.Errorf("decoding db: %w", err)
|
||||
}
|
||||
|
||||
leases := dl.Leases
|
||||
leases4 := []*dhcpsvc.Lease{}
|
||||
leases6 := []*dhcpsvc.Lease{}
|
||||
leases4, leases6 := splitStoredLeases(dl.Leases)
|
||||
|
||||
err = s.srv4.ResetLeases(leases4)
|
||||
if err != nil {
|
||||
return fmt.Errorf("resetting dhcpv4 leases: %w", err)
|
||||
}
|
||||
|
||||
if s.srv6 != nil {
|
||||
err = s.srv6.ResetLeases(leases6)
|
||||
if err != nil {
|
||||
return fmt.Errorf("resetting dhcpv6 leases: %w", err)
|
||||
}
|
||||
restoreLoadedV6Meta(s.srv6, dl.V6Meta)
|
||||
}
|
||||
|
||||
log.Info(
|
||||
"dhcp: loaded leases v4:%d v6:%d total-read:%d from DB",
|
||||
len(leases4),
|
||||
len(leases6),
|
||||
len(dl.Leases),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// splitStoredLeases converts stored database leases into DHCPv4 and DHCPv6
|
||||
// leases.
|
||||
func splitStoredLeases(leases []*dbLease) (leases4, leases6 []*dhcpsvc.Lease) {
|
||||
for _, l := range leases {
|
||||
var lease *dhcpsvc.Lease
|
||||
lease, err = l.toLease()
|
||||
lease, err := l.toLease()
|
||||
if err != nil {
|
||||
log.Info("dhcp: invalid lease: %s", err)
|
||||
|
||||
|
|
@ -141,103 +184,124 @@ func (s *server) dbLoad() (err error) {
|
|||
}
|
||||
}
|
||||
|
||||
err = s.srv4.ResetLeases(leases4)
|
||||
if err != nil {
|
||||
return fmt.Errorf("resetting dhcpv4 leases: %w", err)
|
||||
return leases4, leases6
|
||||
}
|
||||
|
||||
// restoreLoadedV6Meta restores the persisted IPv6 prefix metadata into srv6.
|
||||
func restoreLoadedV6Meta(srv6 DHCPServer, meta *dataLeasesV6Meta) {
|
||||
if meta == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if s.srv6 != nil {
|
||||
err = s.srv6.ResetLeases(leases6)
|
||||
v6srv, ok := srv6.(v6MetaRestorer)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
renewable, deprecated := splitStoredV6Meta(meta)
|
||||
v6srv.setRestoredPrefixMeta(renewable, deprecated)
|
||||
}
|
||||
|
||||
// splitStoredV6Meta converts stored IPv6 prefix metadata into the in-memory
|
||||
// structures used by the DHCPv6 server.
|
||||
func splitStoredV6Meta(meta *dataLeasesV6Meta) (
|
||||
renewable map[netip.Prefix]struct{},
|
||||
deprecated map[netip.Prefix]time.Time,
|
||||
) {
|
||||
renewable = map[netip.Prefix]struct{}{}
|
||||
for _, pref := range meta.Renewable {
|
||||
renewable[pref] = struct{}{}
|
||||
}
|
||||
|
||||
deprecated = map[netip.Prefix]time.Time{}
|
||||
for _, dp := range meta.Deprecated {
|
||||
if dp == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
until, err := time.Parse(time.RFC3339, dp.ValidUntil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("resetting dhcpv6 leases: %w", err)
|
||||
log.Info("dhcp: invalid v6 deprecated prefix %s: %s", dp.Prefix, err)
|
||||
|
||||
continue
|
||||
}
|
||||
if dl.V6Meta != nil {
|
||||
if srv6, ok := s.srv6.(*v6Server); ok {
|
||||
renewable := map[netip.Prefix]struct{}{}
|
||||
for _, pref := range dl.V6Meta.Renewable {
|
||||
renewable[pref] = struct{}{}
|
||||
}
|
||||
|
||||
deprecated := map[netip.Prefix]time.Time{}
|
||||
for _, dp := range dl.V6Meta.Deprecated {
|
||||
if dp == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
until, parseErr := time.Parse(time.RFC3339, dp.ValidUntil)
|
||||
if parseErr != nil {
|
||||
log.Info("dhcp: invalid v6 deprecated prefix %s: %s", dp.Prefix, parseErr)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
deprecated[dp.Prefix] = until
|
||||
}
|
||||
|
||||
srv6.setRestoredPrefixMeta(renewable, deprecated)
|
||||
}
|
||||
}
|
||||
deprecated[dp.Prefix] = until
|
||||
}
|
||||
|
||||
log.Info(
|
||||
"dhcp: loaded leases v4:%d v6:%d total-read:%d from DB",
|
||||
len(leases4),
|
||||
len(leases6),
|
||||
len(leases),
|
||||
)
|
||||
|
||||
return nil
|
||||
return renewable, deprecated
|
||||
}
|
||||
|
||||
// dbStore stores DHCP leases.
|
||||
func (s *server) dbStore() (err error) {
|
||||
// Use an empty slice here as opposed to nil so that it doesn't write
|
||||
// "null" into the database file if leases are empty.
|
||||
leases := []*dbLease{}
|
||||
leases := dbLeasesFromRef(s.srv4.getLeasesRef())
|
||||
var v6Meta *dataLeasesV6Meta
|
||||
|
||||
for _, l := range s.srv4.getLeasesRef() {
|
||||
leases = append(leases, fromLease(l))
|
||||
}
|
||||
|
||||
if s.srv6 != nil {
|
||||
if srv6, ok := s.srv6.(*v6Server); ok {
|
||||
leases6, renewable, deprecated := srv6.dbSnapshot(time.Now())
|
||||
for _, l := range leases6 {
|
||||
leases = append(leases, fromLease(l))
|
||||
}
|
||||
if len(renewable) > 0 || len(deprecated) > 0 {
|
||||
v6Meta = &dataLeasesV6Meta{
|
||||
Renewable: make([]netip.Prefix, 0, len(renewable)),
|
||||
}
|
||||
for pref := range renewable {
|
||||
v6Meta.Renewable = append(v6Meta.Renewable, pref)
|
||||
}
|
||||
slices.SortFunc(v6Meta.Renewable, prefixCompare)
|
||||
|
||||
if len(deprecated) > 0 {
|
||||
v6Meta.Deprecated = make([]*dbDeprecatedPrefix, 0, len(deprecated))
|
||||
for pref, until := range deprecated {
|
||||
v6Meta.Deprecated = append(v6Meta.Deprecated, &dbDeprecatedPrefix{
|
||||
Prefix: pref,
|
||||
ValidUntil: until.Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
slices.SortFunc(v6Meta.Deprecated, func(a, b *dbDeprecatedPrefix) int {
|
||||
return prefixCompare(a.Prefix, b.Prefix)
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for _, l := range s.srv6.getLeasesRef() {
|
||||
leases = append(leases, fromLease(l))
|
||||
}
|
||||
}
|
||||
leases, v6Meta = s.dbStoreV6(leases)
|
||||
}
|
||||
|
||||
return writeDB(s.conf.dbFilePath, leases, v6Meta)
|
||||
}
|
||||
|
||||
// dbLeasesFromRef converts DHCP leases to database leases.
|
||||
func dbLeasesFromRef(leases []*dhcpsvc.Lease) (dbLeases []*dbLease) {
|
||||
dbLeases = make([]*dbLease, 0, len(leases))
|
||||
for _, l := range leases {
|
||||
dbLeases = append(dbLeases, fromLease(l))
|
||||
}
|
||||
|
||||
return dbLeases
|
||||
}
|
||||
|
||||
// dbStoreV6 adds DHCPv6 leases and prefix metadata to the database snapshot.
|
||||
func (s *server) dbStoreV6(leases []*dbLease) (out []*dbLease, v6Meta *dataLeasesV6Meta) {
|
||||
if srv6, ok := s.srv6.(v6Snapshotter); ok {
|
||||
leases6, renewable, deprecated := srv6.dbSnapshot(time.Now())
|
||||
leases = append(leases, dbLeasesFromRef(leases6)...)
|
||||
return leases, buildStoredV6Meta(renewable, deprecated)
|
||||
}
|
||||
|
||||
return append(leases, dbLeasesFromRef(s.srv6.getLeasesRef())...), nil
|
||||
}
|
||||
|
||||
// buildStoredV6Meta converts snapshot metadata into the persisted form.
|
||||
func buildStoredV6Meta(
|
||||
renewable map[netip.Prefix]struct{},
|
||||
deprecated map[netip.Prefix]time.Time,
|
||||
) (v6Meta *dataLeasesV6Meta) {
|
||||
if len(renewable) == 0 && len(deprecated) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
v6Meta = &dataLeasesV6Meta{
|
||||
Renewable: make([]netip.Prefix, 0, len(renewable)),
|
||||
}
|
||||
for pref := range renewable {
|
||||
v6Meta.Renewable = append(v6Meta.Renewable, pref)
|
||||
}
|
||||
slices.SortFunc(v6Meta.Renewable, prefixCompare)
|
||||
|
||||
if len(deprecated) == 0 {
|
||||
return v6Meta
|
||||
}
|
||||
|
||||
v6Meta.Deprecated = make([]*dbDeprecatedPrefix, 0, len(deprecated))
|
||||
for pref, until := range deprecated {
|
||||
v6Meta.Deprecated = append(v6Meta.Deprecated, &dbDeprecatedPrefix{
|
||||
Prefix: pref,
|
||||
ValidUntil: until.Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
slices.SortFunc(v6Meta.Deprecated, func(a, b *dbDeprecatedPrefix) int {
|
||||
return prefixCompare(a.Prefix, b.Prefix)
|
||||
})
|
||||
|
||||
return v6Meta
|
||||
}
|
||||
|
||||
// writeDB writes leases to file at path.
|
||||
func writeDB(path string, leases []*dbLease, v6Meta *dataLeasesV6Meta) (err error) {
|
||||
defer func() { err = errors.Annotate(err, "writing db: %w") }()
|
||||
|
|
|
|||
|
|
@ -230,14 +230,7 @@ func (ra *raCtx) Init(initial raState) (err error) {
|
|||
// Advertisements.
|
||||
func (ra *raCtx) ensureConn(sourceAddr netip.Addr) (err error) {
|
||||
if !sourceAddr.IsValid() {
|
||||
if ra.conn != nil {
|
||||
err = ra.conn.Close()
|
||||
}
|
||||
|
||||
ra.conn = nil
|
||||
ra.connSourceAddr = netip.Addr{}
|
||||
|
||||
return err
|
||||
return ra.closeConn()
|
||||
}
|
||||
|
||||
if ra.conn != nil && ra.connSourceAddr == sourceAddr {
|
||||
|
|
@ -245,14 +238,29 @@ func (ra *raCtx) ensureConn(sourceAddr netip.Addr) (err error) {
|
|||
}
|
||||
|
||||
if ra.conn != nil {
|
||||
err = ra.conn.Close()
|
||||
ra.conn = nil
|
||||
ra.connSourceAddr = netip.Addr{}
|
||||
if err != nil {
|
||||
if err = ra.closeConn(); err != nil {
|
||||
return fmt.Errorf("closing previous icmp listener: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return ra.openConn(sourceAddr)
|
||||
}
|
||||
|
||||
// closeConn closes the current ICMPv6 socket and clears the tracked source
|
||||
// address.
|
||||
func (ra *raCtx) closeConn() (err error) {
|
||||
if ra.conn != nil {
|
||||
err = ra.conn.Close()
|
||||
}
|
||||
|
||||
ra.conn = nil
|
||||
ra.connSourceAddr = netip.Addr{}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// openConn opens a new ICMPv6 socket for the given source address.
|
||||
func (ra *raCtx) openConn(sourceAddr netip.Addr) (err error) {
|
||||
ipAndScope := sourceAddr.String() + "%" + ra.ifaceName
|
||||
newConn, err := icmp.ListenPacket("ip6:ipv6-icmp", ipAndScope)
|
||||
if err != nil {
|
||||
|
|
@ -334,17 +342,17 @@ func (ra *raCtx) refresh(ctx context.Context) {
|
|||
}
|
||||
|
||||
now := time.Now()
|
||||
_ = ra.state.merge(obs, now)
|
||||
change := ra.state.merge(obs, now)
|
||||
if ra.onStateRefresh != nil {
|
||||
ra.onStateRefresh(now, &ra.state)
|
||||
}
|
||||
ra.syncStateChange(now)
|
||||
ra.syncStateChange(now, &change)
|
||||
}
|
||||
|
||||
// sendPacket rebuilds and sends the current Router Advertisement packet.
|
||||
func (ra *raCtx) sendPacket() {
|
||||
now := time.Now()
|
||||
ra.syncStateChange(now)
|
||||
ra.syncStateChange(now, nil)
|
||||
|
||||
sourceAddr, rdnssAddr := ra.state.sourceAndRDNSS()
|
||||
err := ra.ensureConn(sourceAddr)
|
||||
|
|
@ -424,7 +432,7 @@ func tickerC(t *time.Ticker) (c <-chan time.Time) {
|
|||
// elapsed time. The comparison uses a deadline-based digest of the tracked
|
||||
// prefixes, so repeated polls that only observe the kernel counting lifetimes
|
||||
// down do not spuriously fire the callback.
|
||||
func (ra *raCtx) syncStateChange(now time.Time) {
|
||||
func (ra *raCtx) syncStateChange(now time.Time, change *raActiveChange) {
|
||||
digest := ra.state.digest(now)
|
||||
changed := !sameRAStateDigest(ra.lastDigest, digest)
|
||||
ra.lastDigest = digest
|
||||
|
|
@ -433,7 +441,12 @@ func (ra *raCtx) syncStateChange(now time.Time) {
|
|||
return
|
||||
}
|
||||
|
||||
active := ra.state.activeSnapshot(now)
|
||||
var active *raPrefixSnapshot
|
||||
if change != nil && change.Changed {
|
||||
active = change.Active
|
||||
} else {
|
||||
active = ra.state.activeSnapshot(now)
|
||||
}
|
||||
advertised := ra.state.pios(now)
|
||||
ra.onActivePrefixChange(active, advertised)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,7 +64,11 @@ func TestCreateICMPv6RAPacket(t *testing.T) {
|
|||
assert.Equal(t, byte(0xc0), opt.Data[1])
|
||||
assert.Equal(t, uint32(1800), binary.BigEndian.Uint32(opt.Data[2:6]))
|
||||
assert.Equal(t, uint32(0), binary.BigEndian.Uint32(opt.Data[6:10]))
|
||||
assert.Equal(t, netip.MustParsePrefix("2001:db8:abcd::/60").Masked().Addr().As16(), [16]byte(opt.Data[14:30]))
|
||||
assert.Equal(
|
||||
t,
|
||||
netip.MustParsePrefix("2001:db8:abcd::/60").Masked().Addr().As16(),
|
||||
[16]byte(opt.Data[14:30]),
|
||||
)
|
||||
|
||||
opt = raPkt.Options[2]
|
||||
require.Equal(t, layers.ICMPv6OptMTU, opt.Type)
|
||||
|
|
@ -105,7 +109,7 @@ func TestRACtxSyncStateChange_DeprecatedExpiry(t *testing.T) {
|
|||
}
|
||||
ra.lastDigest = ra.state.digest(now)
|
||||
|
||||
ra.syncStateChange(now.Add(2 * time.Second))
|
||||
ra.syncStateChange(now.Add(2*time.Second), nil)
|
||||
|
||||
require.Len(t, notifications, 1)
|
||||
require.Len(t, notifications[0], 1)
|
||||
|
|
@ -142,9 +146,9 @@ func TestRACtxSyncStateChange_StableStateDoesNotFireCallback(t *testing.T) {
|
|||
|
||||
// Three ticks without any new observation: only elapsed time changed,
|
||||
// so the digest must be unchanged and the callback must not fire.
|
||||
ra.syncStateChange(now.Add(1 * time.Second))
|
||||
ra.syncStateChange(now.Add(3 * time.Second))
|
||||
ra.syncStateChange(now.Add(5 * time.Second))
|
||||
ra.syncStateChange(now.Add(1*time.Second), nil)
|
||||
ra.syncStateChange(now.Add(3*time.Second), nil)
|
||||
ra.syncStateChange(now.Add(5*time.Second), nil)
|
||||
|
||||
assert.Zero(t, fired)
|
||||
}
|
||||
|
|
@ -187,7 +191,7 @@ func TestRACtxSyncStateChange_DeprecatingPrefixTriggersCallback(t *testing.T) {
|
|||
}},
|
||||
}, now.Add(time.Minute))
|
||||
|
||||
ra.syncStateChange(now.Add(time.Minute))
|
||||
ra.syncStateChange(now.Add(time.Minute), nil)
|
||||
|
||||
require.Len(t, notifications, 1)
|
||||
require.Len(t, notifications[0], 2)
|
||||
|
|
@ -227,12 +231,12 @@ func TestRACtxSyncStateChange_PreferredExpiryTriggersCallback(t *testing.T) {
|
|||
|
||||
// While the preferred countdown still has time remaining no callback
|
||||
// should fire.
|
||||
ra.syncStateChange(now.Add(299 * time.Second))
|
||||
ra.syncStateChange(now.Add(299*time.Second), nil)
|
||||
assert.Empty(t, notifications)
|
||||
|
||||
// At the moment the preferred lifetime hits zero the callback must
|
||||
// fire so v6Server can drop the prefix from its renewable set.
|
||||
ra.syncStateChange(now.Add(301 * time.Second))
|
||||
ra.syncStateChange(now.Add(301*time.Second), nil)
|
||||
require.Len(t, notifications, 1)
|
||||
require.Len(t, notifications[0], 1)
|
||||
assert.Equal(t, netip.MustParsePrefix("2001:db8::/64"), notifications[0][0].Prefix)
|
||||
|
|
@ -240,8 +244,8 @@ func TestRACtxSyncStateChange_PreferredExpiryTriggersCallback(t *testing.T) {
|
|||
|
||||
// But subsequent ticks past the expiry must not re-fire: the state
|
||||
// digest has settled at preferredExpired=true and stays that way.
|
||||
ra.syncStateChange(now.Add(305 * time.Second))
|
||||
ra.syncStateChange(now.Add(310 * time.Second))
|
||||
ra.syncStateChange(now.Add(305*time.Second), nil)
|
||||
ra.syncStateChange(now.Add(310*time.Second), nil)
|
||||
assert.Len(t, notifications, 1)
|
||||
}
|
||||
|
||||
|
|
@ -277,9 +281,9 @@ func TestRACtxSyncStateChange_PreferredExpiryOnDeprecatedEntryStillQuiescent(t *
|
|||
|
||||
// Several steady-state ticks across the valid countdown. No kernel
|
||||
// state changed, so nothing should fire.
|
||||
ra.syncStateChange(now.Add(1 * time.Second))
|
||||
ra.syncStateChange(now.Add(5 * time.Second))
|
||||
ra.syncStateChange(now.Add(30 * time.Second))
|
||||
ra.syncStateChange(now.Add(1*time.Second), nil)
|
||||
ra.syncStateChange(now.Add(5*time.Second), nil)
|
||||
ra.syncStateChange(now.Add(30*time.Second), nil)
|
||||
assert.Zero(t, fired)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -96,12 +96,12 @@ type raStateDigest struct {
|
|||
// trackedPrefixDigest is the deadline-based fingerprint of one trackedPrefix.
|
||||
//
|
||||
// preferredExpired is a time-derived boolean that flips from false to true
|
||||
// exactly once during a natural countdown — the moment the prefix's preferred
|
||||
// exactly once during a natural countdown, the moment the prefix's preferred
|
||||
// lifetime reaches zero. Without it, a prefix whose preferred lifetime
|
||||
// counts down while its absolute preferredUntil deadline stays put would
|
||||
// produce identical digests before and after it becomes non-renewable, so
|
||||
// downstream reconciliation (which rebuilds the v6Server renewablePrefixes
|
||||
// set) would never fire for that transition.
|
||||
// downstream reconciliation, which rebuilds the v6Server renewablePrefixes
|
||||
// set, would never fire for that transition.
|
||||
type trackedPrefixDigest struct {
|
||||
prefix netip.Prefix
|
||||
origin raPrefixOrigin
|
||||
|
|
@ -166,8 +166,8 @@ func sameRAStateDigest(a, b raStateDigest) (ok bool) {
|
|||
}
|
||||
|
||||
for pref, digest := range a.deprecated {
|
||||
other, ok := b.deprecated[pref]
|
||||
if !ok || other != digest {
|
||||
other, found := b.deprecated[pref]
|
||||
if !found || other != digest {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
@ -196,9 +196,19 @@ func newStaticRAState(obs raObservation) (st raState) {
|
|||
return st
|
||||
}
|
||||
|
||||
// capDeprecatedLifetime bounds a remaining valid lifetime by the standard
|
||||
// two-hour cap used for deprecated prefixes.
|
||||
func capDeprecatedLifetime(valid uint32) (capped uint32) {
|
||||
if valid > raDeprecatedLifetimeCapSecs || valid == math.MaxUint32 {
|
||||
return raDeprecatedLifetimeCapSecs
|
||||
}
|
||||
|
||||
return valid
|
||||
}
|
||||
|
||||
// merge merges a fresh interface observation into s and reports whether the
|
||||
// active prefix changed.
|
||||
func (s *raState) merge(obs raObservation, now time.Time) (change raActiveChange) {
|
||||
func (s *raState) merge(obs raObservation, now time.Time) raActiveChange {
|
||||
if s.deprecated == nil {
|
||||
s.deprecated = map[netip.Prefix]*trackedPrefix{}
|
||||
}
|
||||
|
|
@ -213,74 +223,83 @@ func (s *raState) merge(obs raObservation, now time.Time) (change raActiveChange
|
|||
s.sourceAddr = obs.SourceAddr
|
||||
s.rdnssAddr = obs.RDNSSAddr
|
||||
|
||||
switch {
|
||||
case obs.Active != nil && s.active != nil && s.active.prefix == obs.Active.Prefix:
|
||||
s.active = reconcileTrackedPrefix(s.active, *obs.Active, raPrefixOriginObservedActive, now)
|
||||
case obs.Active != nil:
|
||||
s.moveActiveToDeprecated(now)
|
||||
s.active = newTrackedPrefix(*obs.Active, raPrefixOriginObservedActive, now)
|
||||
activeChanged = prev != nil && prev.Prefix != obs.Active.Prefix
|
||||
case obs.Active == nil:
|
||||
s.moveActiveToDeprecated(now)
|
||||
s.active = nil
|
||||
activeChanged = prev != nil
|
||||
}
|
||||
activeChanged = s.mergeActiveObservation(obs.Active, prev, now)
|
||||
|
||||
if s.active != nil {
|
||||
delete(s.deprecated, s.active.prefix)
|
||||
}
|
||||
|
||||
observedInactive := map[netip.Prefix]struct{}{}
|
||||
observedInactive := s.mergeInactiveObservations(obs.Inactive, prevActivePrefix, activeChanged, now)
|
||||
|
||||
for _, dep := range obs.Inactive {
|
||||
if s.active != nil && dep.Prefix == s.active.prefix {
|
||||
continue
|
||||
}
|
||||
if activeChanged && dep.Prefix == prevActivePrefix && dep.PreferredSec == 0 {
|
||||
valid := dep.ValidSec
|
||||
if valid > raDeprecatedLifetimeCapSecs || valid == math.MaxUint32 {
|
||||
valid = raDeprecatedLifetimeCapSecs
|
||||
}
|
||||
if valid == 0 {
|
||||
delete(s.deprecated, dep.Prefix)
|
||||
s.deprecateMissingObservedInactivePrefixes(observedInactive, now)
|
||||
|
||||
continue
|
||||
}
|
||||
s.evictExpired(now)
|
||||
|
||||
s.deprecated[dep.Prefix] = reconcileTrackedPrefix(
|
||||
s.deprecated[dep.Prefix],
|
||||
raPrefixSnapshot{
|
||||
Prefix: dep.Prefix,
|
||||
PreferredSec: 0,
|
||||
ValidSec: valid,
|
||||
},
|
||||
raPrefixOriginDeprecated,
|
||||
now,
|
||||
)
|
||||
next := s.activeSnapshot(now)
|
||||
return raActiveChange{
|
||||
Changed: !sameActivePrefix(prev, next),
|
||||
Active: next,
|
||||
}
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
// mergeActiveObservation updates the active prefix from obs and reports
|
||||
// whether the active prefix changed.
|
||||
func (s *raState) mergeActiveObservation(
|
||||
obs, prev *raPrefixSnapshot,
|
||||
now time.Time,
|
||||
) (activeChanged bool) {
|
||||
switch {
|
||||
case obs != nil && s.active != nil && s.active.prefix == obs.Prefix:
|
||||
s.active = reconcileTrackedPrefix(s.active, *obs, raPrefixOriginObservedActive, now)
|
||||
case obs != nil:
|
||||
s.moveActiveToDeprecated(now)
|
||||
s.active = newTrackedPrefix(*obs, raPrefixOriginObservedActive, now)
|
||||
activeChanged = prev != nil && prev.Prefix != obs.Prefix
|
||||
default:
|
||||
s.moveActiveToDeprecated(now)
|
||||
s.active = nil
|
||||
activeChanged = prev != nil
|
||||
}
|
||||
|
||||
if dep.PreferredSec > 0 {
|
||||
observedInactive[dep.Prefix] = struct{}{}
|
||||
s.deprecated[dep.Prefix] = reconcileTrackedPrefix(
|
||||
s.deprecated[dep.Prefix],
|
||||
dep,
|
||||
raPrefixOriginObservedInactive,
|
||||
now,
|
||||
)
|
||||
return activeChanged
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
// mergeInactiveObservations reconciles inactive prefix observations with the
|
||||
// tracked deprecated prefixes and returns the set of prefixes that still
|
||||
// appear as observed inactive.
|
||||
func (s *raState) mergeInactiveObservations(
|
||||
inactive []raPrefixSnapshot,
|
||||
prevActivePrefix netip.Prefix,
|
||||
activeChanged bool,
|
||||
now time.Time,
|
||||
) (observedInactive map[netip.Prefix]struct{}) {
|
||||
observedInactive = map[netip.Prefix]struct{}{}
|
||||
|
||||
valid := dep.ValidSec
|
||||
if valid > raDeprecatedLifetimeCapSecs || valid == math.MaxUint32 {
|
||||
valid = raDeprecatedLifetimeCapSecs
|
||||
}
|
||||
for _, dep := range inactive {
|
||||
s.mergeInactiveObservation(dep, prevActivePrefix, activeChanged, now, observedInactive)
|
||||
}
|
||||
|
||||
return observedInactive
|
||||
}
|
||||
|
||||
// mergeInactiveObservation reconciles one inactive prefix observation.
|
||||
func (s *raState) mergeInactiveObservation(
|
||||
dep raPrefixSnapshot,
|
||||
prevActivePrefix netip.Prefix,
|
||||
activeChanged bool,
|
||||
now time.Time,
|
||||
observedInactive map[netip.Prefix]struct{},
|
||||
) {
|
||||
if s.active != nil && dep.Prefix == s.active.prefix {
|
||||
return
|
||||
}
|
||||
|
||||
if activeChanged && dep.Prefix == prevActivePrefix && dep.PreferredSec == 0 {
|
||||
valid := capDeprecatedLifetime(dep.ValidSec)
|
||||
if valid == 0 {
|
||||
delete(s.deprecated, dep.Prefix)
|
||||
|
||||
continue
|
||||
return
|
||||
}
|
||||
|
||||
s.deprecated[dep.Prefix] = reconcileTrackedPrefix(
|
||||
|
|
@ -293,8 +312,47 @@ func (s *raState) merge(obs raObservation, now time.Time) (change raActiveChange
|
|||
raPrefixOriginDeprecated,
|
||||
now,
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if dep.PreferredSec > 0 {
|
||||
observedInactive[dep.Prefix] = struct{}{}
|
||||
s.deprecated[dep.Prefix] = reconcileTrackedPrefix(
|
||||
s.deprecated[dep.Prefix],
|
||||
dep,
|
||||
raPrefixOriginObservedInactive,
|
||||
now,
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
valid := capDeprecatedLifetime(dep.ValidSec)
|
||||
if valid == 0 {
|
||||
delete(s.deprecated, dep.Prefix)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
s.deprecated[dep.Prefix] = reconcileTrackedPrefix(
|
||||
s.deprecated[dep.Prefix],
|
||||
raPrefixSnapshot{
|
||||
Prefix: dep.Prefix,
|
||||
PreferredSec: 0,
|
||||
ValidSec: valid,
|
||||
},
|
||||
raPrefixOriginDeprecated,
|
||||
now,
|
||||
)
|
||||
}
|
||||
|
||||
// deprecateMissingObservedInactivePrefixes converts any observed-inactive
|
||||
// prefixes that are no longer present into deprecated prefixes.
|
||||
func (s *raState) deprecateMissingObservedInactivePrefixes(
|
||||
observedInactive map[netip.Prefix]struct{},
|
||||
now time.Time,
|
||||
) {
|
||||
for pref, tracked := range s.deprecated {
|
||||
if tracked.origin != raPrefixOriginObservedInactive {
|
||||
continue
|
||||
|
|
@ -304,14 +362,6 @@ func (s *raState) merge(obs raObservation, now time.Time) (change raActiveChange
|
|||
s.deprecateTrackedPrefix(pref, tracked, now)
|
||||
}
|
||||
}
|
||||
|
||||
s.evictExpired(now)
|
||||
|
||||
next := s.activeSnapshot(now)
|
||||
change.Changed = !sameActivePrefix(prev, next)
|
||||
change.Active = next
|
||||
|
||||
return change
|
||||
}
|
||||
|
||||
// deprecateTrackedPrefix converts a tracked prefix into a deprecated one using
|
||||
|
|
@ -323,9 +373,7 @@ func (s *raState) deprecateTrackedPrefix(pref netip.Prefix, tracked *trackedPref
|
|||
|
||||
return
|
||||
}
|
||||
if valid > raDeprecatedLifetimeCapSecs || valid == math.MaxUint32 {
|
||||
valid = raDeprecatedLifetimeCapSecs
|
||||
}
|
||||
valid = capDeprecatedLifetime(valid)
|
||||
|
||||
s.deprecated[pref] = newTrackedPrefix(raPrefixSnapshot{
|
||||
Prefix: pref,
|
||||
|
|
@ -387,6 +435,15 @@ func buildInterfaceRAObservation(states []aghnet.IPv6AddrState) (obs raObservati
|
|||
obs.SourceAddr = pickRASourceAddr(states)
|
||||
obs.RDNSSAddr = obs.SourceAddr
|
||||
|
||||
prefixes := buildInterfaceRAPrefixSnapshots(states)
|
||||
obs.Active, obs.Inactive = splitInterfaceRAPrefixSnapshots(prefixes)
|
||||
|
||||
return obs
|
||||
}
|
||||
|
||||
// buildInterfaceRAPrefixSnapshots groups eligible interface states by prefix
|
||||
// and collapses each group into a single snapshot.
|
||||
func buildInterfaceRAPrefixSnapshots(states []aghnet.IPv6AddrState) (prefixes []raPrefixSnapshot) {
|
||||
grouped := map[netip.Prefix][]aghnet.IPv6AddrState{}
|
||||
for _, st := range states {
|
||||
if !isEligibleRAPrefixState(st) {
|
||||
|
|
@ -397,7 +454,7 @@ func buildInterfaceRAObservation(states []aghnet.IPv6AddrState) (obs raObservati
|
|||
grouped[pref] = append(grouped[pref], st)
|
||||
}
|
||||
|
||||
prefixes := make([]raPrefixSnapshot, 0, len(grouped))
|
||||
prefixes = make([]raPrefixSnapshot, 0, len(grouped))
|
||||
for pref, group := range grouped {
|
||||
snap, ok := collapsePrefixGroup(pref, group)
|
||||
if !ok {
|
||||
|
|
@ -407,7 +464,40 @@ func buildInterfaceRAObservation(states []aghnet.IPv6AddrState) (obs raObservati
|
|||
prefixes = append(prefixes, snap)
|
||||
}
|
||||
|
||||
activeIdx := -1
|
||||
return prefixes
|
||||
}
|
||||
|
||||
// splitInterfaceRAPrefixSnapshots selects the active snapshot and sorts the
|
||||
// inactive ones.
|
||||
func splitInterfaceRAPrefixSnapshots(
|
||||
prefixes []raPrefixSnapshot,
|
||||
) (active *raPrefixSnapshot, inactive []raPrefixSnapshot) {
|
||||
activeIdx := selectInterfaceRAActivePrefixIndex(prefixes)
|
||||
for i, pref := range prefixes {
|
||||
if i == activeIdx {
|
||||
active = &raPrefixSnapshot{
|
||||
Prefix: pref.Prefix,
|
||||
PreferredSec: pref.PreferredSec,
|
||||
ValidSec: pref.ValidSec,
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
inactive = append(inactive, pref)
|
||||
}
|
||||
|
||||
slices.SortFunc(inactive, func(a, b raPrefixSnapshot) int {
|
||||
return prefixCompare(a.Prefix, b.Prefix)
|
||||
})
|
||||
|
||||
return active, inactive
|
||||
}
|
||||
|
||||
// selectInterfaceRAActivePrefixIndex reports the best active prefix index in
|
||||
// prefixes, or -1 when none is suitable.
|
||||
func selectInterfaceRAActivePrefixIndex(prefixes []raPrefixSnapshot) (activeIdx int) {
|
||||
activeIdx = -1
|
||||
for i, pref := range prefixes {
|
||||
if pref.PreferredSec == 0 {
|
||||
continue
|
||||
|
|
@ -418,24 +508,7 @@ func buildInterfaceRAObservation(states []aghnet.IPv6AddrState) (obs raObservati
|
|||
}
|
||||
}
|
||||
|
||||
for i, pref := range prefixes {
|
||||
switch {
|
||||
case i == activeIdx:
|
||||
obs.Active = &raPrefixSnapshot{
|
||||
Prefix: pref.Prefix,
|
||||
PreferredSec: pref.PreferredSec,
|
||||
ValidSec: pref.ValidSec,
|
||||
}
|
||||
default:
|
||||
obs.Inactive = append(obs.Inactive, pref)
|
||||
}
|
||||
}
|
||||
|
||||
slices.SortFunc(obs.Inactive, func(a, b raPrefixSnapshot) int {
|
||||
return prefixCompare(a.Prefix, b.Prefix)
|
||||
})
|
||||
|
||||
return obs
|
||||
return activeIdx
|
||||
}
|
||||
|
||||
// buildStaticRAObservation returns the configured static prefix observation.
|
||||
|
|
@ -684,7 +684,7 @@ func sameDeadlineMap(a, b map[netip.Prefix]time.Time) (ok bool) {
|
|||
}
|
||||
|
||||
for pref, until := range a {
|
||||
if other, ok := b[pref]; !ok || !other.Equal(until) {
|
||||
if other, found := b[pref]; !found || !other.Equal(until) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
@ -756,6 +756,102 @@ func (s *v6Server) checkIA(msg *dhcpv6.Message, lease *dhcpsvc.Lease) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// leaseCommitSnapshot captures the prefix state used to compute lease
|
||||
// lifetimes while holding s.leasesLock.
|
||||
type leaseCommitSnapshot struct {
|
||||
leaseTime time.Duration
|
||||
renewable bool
|
||||
renewableLifetime time.Duration
|
||||
deprecatedLifetime time.Duration
|
||||
preferredUntil time.Time
|
||||
hasPreferredUntil bool
|
||||
}
|
||||
|
||||
// snapshotLeaseCommitState captures the prefix-tracking state that lease
|
||||
// lifetime calculations depend on.
|
||||
func (s *v6Server) snapshotLeaseCommitState(
|
||||
now time.Time,
|
||||
lease *dhcpsvc.Lease,
|
||||
) (snapshot leaseCommitSnapshot) {
|
||||
prefix := netip.PrefixFrom(lease.IP, raObservedPrefixBits).Masked()
|
||||
snapshot.leaseTime = s.conf.leaseTime
|
||||
snapshot.renewable = !lease.IsStatic && leasePrefixRenewable(s.renewablePrefixes, lease.IP)
|
||||
validUntil, hasValidUntil := s.validUntilByPrefix[prefix]
|
||||
snapshot.preferredUntil, snapshot.hasPreferredUntil = s.preferredUntilByPrefix[prefix]
|
||||
|
||||
snapshot.renewableLifetime = snapshot.leaseTime
|
||||
if hasValidUntil {
|
||||
capped := time.Duration(remainingUntil(now, validUntil)) * time.Second
|
||||
snapshot.renewableLifetime = min(snapshot.renewableLifetime, capped)
|
||||
}
|
||||
|
||||
if hasValidUntil {
|
||||
validForPrefix := time.Duration(remainingUntil(now, validUntil)) * time.Second
|
||||
validForLease := max(time.Until(lease.Expiry), 0)
|
||||
snapshot.deprecatedLifetime = min(validForLease, validForPrefix)
|
||||
}
|
||||
|
||||
return snapshot
|
||||
}
|
||||
|
||||
// commitLeaseLifetime returns the valid lifetime to use for msg.
|
||||
func commitLeaseLifetime(
|
||||
now time.Time,
|
||||
msgType dhcpv6.MessageType,
|
||||
lease *dhcpsvc.Lease,
|
||||
snapshot leaseCommitSnapshot,
|
||||
) (lifetime time.Duration, shouldNotify bool) {
|
||||
switch msgType {
|
||||
case dhcpv6.MessageTypeConfirm:
|
||||
switch {
|
||||
case lease.IsStatic:
|
||||
lifetime = snapshot.leaseTime
|
||||
case snapshot.renewable:
|
||||
lifetime = min(max(time.Until(lease.Expiry), 0), snapshot.renewableLifetime)
|
||||
default:
|
||||
lifetime = snapshot.deprecatedLifetime
|
||||
}
|
||||
case dhcpv6.MessageTypeRequest,
|
||||
dhcpv6.MessageTypeRenew,
|
||||
dhcpv6.MessageTypeRebind:
|
||||
switch {
|
||||
case lease.IsStatic:
|
||||
lifetime = snapshot.leaseTime
|
||||
case snapshot.renewable:
|
||||
lifetime = snapshot.renewableLifetime
|
||||
lease.Expiry = now.Add(lifetime)
|
||||
shouldNotify = true
|
||||
default:
|
||||
lifetime = snapshot.deprecatedLifetime
|
||||
}
|
||||
default:
|
||||
lifetime = snapshot.leaseTime
|
||||
}
|
||||
|
||||
return lifetime, shouldNotify
|
||||
}
|
||||
|
||||
// commitLeasePreferredLifetime returns the preferred lifetime to use for the
|
||||
// reply.
|
||||
func commitLeasePreferredLifetime(
|
||||
now time.Time,
|
||||
lifetime time.Duration,
|
||||
lease *dhcpsvc.Lease,
|
||||
snapshot leaseCommitSnapshot,
|
||||
) (preferredLifetime time.Duration) {
|
||||
switch {
|
||||
case lease.IsStatic:
|
||||
return lifetime
|
||||
case !snapshot.renewable:
|
||||
return 0
|
||||
case snapshot.hasPreferredUntil:
|
||||
preferredForPrefix := time.Duration(remainingUntil(now, snapshot.preferredUntil)) * time.Second
|
||||
return min(lifetime, preferredForPrefix)
|
||||
default:
|
||||
return lifetime
|
||||
}
|
||||
}
|
||||
|
||||
// commitLease computes the valid and preferred lifetimes to grant lease in a
|
||||
// reply to msg. For Request/Renew/Rebind on renewable dynamic leases it also
|
||||
// updates lease.Expiry and enqueues a lease-change notification. commitLease
|
||||
|
|
@ -787,76 +883,9 @@ func (s *v6Server) commitLeaseLocked(
|
|||
msg *dhcpv6.Message,
|
||||
lease *dhcpsvc.Lease,
|
||||
) (lifetime, preferredLifetime time.Duration, shouldNotify bool) {
|
||||
leaseTime := s.conf.leaseTime
|
||||
|
||||
// Snapshot the prefix-tracking state that the computations below depend
|
||||
// on so they all agree about which view of the prefix we are acting
|
||||
// under.
|
||||
prefix := netip.PrefixFrom(lease.IP, raObservedPrefixBits).Masked()
|
||||
renewable := !lease.IsStatic && leasePrefixRenewable(s.renewablePrefixes, lease.IP)
|
||||
validUntil, hasValidUntil := s.validUntilByPrefix[prefix]
|
||||
preferredUntil, hasPreferredUntil := s.preferredUntilByPrefix[prefix]
|
||||
|
||||
// renewableLifetime is the lifetime we would grant a fresh lease,
|
||||
// capped by the prefix's remaining valid lifetime when tracking data is
|
||||
// available.
|
||||
renewableLifetime := leaseTime
|
||||
if hasValidUntil {
|
||||
capped := time.Duration(remainingUntil(now, validUntil)) * time.Second
|
||||
renewableLifetime = min(renewableLifetime, capped)
|
||||
}
|
||||
|
||||
// deprecatedLifetime is the remaining lifetime we are willing to honor
|
||||
// for an existing lease whose prefix is no longer renewable.
|
||||
var deprecatedLifetime time.Duration
|
||||
if hasValidUntil {
|
||||
validForPrefix := time.Duration(remainingUntil(now, validUntil)) * time.Second
|
||||
validForLease := max(time.Until(lease.Expiry), 0)
|
||||
deprecatedLifetime = min(validForLease, validForPrefix)
|
||||
}
|
||||
|
||||
// Default valid lifetime used for Solicit and any non-special cases.
|
||||
lifetime = leaseTime
|
||||
|
||||
switch msg.Type() {
|
||||
case dhcpv6.MessageTypeConfirm:
|
||||
switch {
|
||||
case lease.IsStatic:
|
||||
lifetime = leaseTime
|
||||
case renewable:
|
||||
lifetime = min(max(time.Until(lease.Expiry), 0), renewableLifetime)
|
||||
default:
|
||||
lifetime = deprecatedLifetime
|
||||
}
|
||||
|
||||
case dhcpv6.MessageTypeRequest,
|
||||
dhcpv6.MessageTypeRenew,
|
||||
dhcpv6.MessageTypeRebind:
|
||||
|
||||
switch {
|
||||
case lease.IsStatic:
|
||||
lifetime = leaseTime
|
||||
case renewable:
|
||||
lifetime = renewableLifetime
|
||||
lease.Expiry = now.Add(lifetime)
|
||||
shouldNotify = true
|
||||
default:
|
||||
lifetime = deprecatedLifetime
|
||||
}
|
||||
}
|
||||
|
||||
// Derive preferred lifetime from the same snapshot.
|
||||
switch {
|
||||
case lease.IsStatic:
|
||||
preferredLifetime = lifetime
|
||||
case !renewable:
|
||||
preferredLifetime = 0
|
||||
case hasPreferredUntil:
|
||||
preferredForPrefix := time.Duration(remainingUntil(now, preferredUntil)) * time.Second
|
||||
preferredLifetime = min(lifetime, preferredForPrefix)
|
||||
default:
|
||||
preferredLifetime = lifetime
|
||||
}
|
||||
snapshot := s.snapshotLeaseCommitState(now, lease)
|
||||
lifetime, shouldNotify = commitLeaseLifetime(now, msg.Type(), lease, snapshot)
|
||||
preferredLifetime = commitLeasePreferredLifetime(now, lifetime, lease, snapshot)
|
||||
|
||||
return lifetime, preferredLifetime, shouldNotify
|
||||
}
|
||||
|
|
@ -881,6 +910,34 @@ func requestedIP(msg *dhcpv6.Message) (ip netip.Addr) {
|
|||
return addr
|
||||
}
|
||||
|
||||
// exactRequestedLease reports whether lease matches the requested IP.
|
||||
func exactRequestedLease(reqIP netip.Addr, lease *dhcpsvc.Lease) (ok bool) {
|
||||
return reqIP.IsValid() && lease.IP == reqIP
|
||||
}
|
||||
|
||||
// usableRequestedDynamicLease reports whether a dynamic lease may be served
|
||||
// for the exact requested IP.
|
||||
func usableRequestedDynamicLease(
|
||||
msgType dhcpv6.MessageType,
|
||||
reqIP netip.Addr,
|
||||
inCurrentPool bool,
|
||||
advertisedPrefixes map[netip.Prefix]struct{},
|
||||
lease *dhcpsvc.Lease,
|
||||
) (ok bool) {
|
||||
if !exactRequestedLease(reqIP, lease) {
|
||||
return false
|
||||
}
|
||||
|
||||
return (inCurrentPool || canServeDeprecatedLease(msgType, lease.IP, advertisedPrefixes)) &&
|
||||
leaseNotExpired(lease)
|
||||
}
|
||||
|
||||
// usableFallbackLease reports whether lease may be reused when no exact match
|
||||
// was found.
|
||||
func (s *v6Server) usableFallbackLease(lease *dhcpsvc.Lease) (ok bool) {
|
||||
return s.ipInCurrentPoolLocked(lease.IP) && leaseNotExpired(lease)
|
||||
}
|
||||
|
||||
// findUsableLease returns a lease that should be served to mac for msg.
|
||||
func (s *v6Server) findUsableLease(msg *dhcpv6.Message, mac net.HardwareAddr) (lease *dhcpsvc.Lease) {
|
||||
reqIP := requestedIP(msg)
|
||||
|
|
@ -891,29 +948,58 @@ func (s *v6Server) findUsableLease(msg *dhcpv6.Message, mac net.HardwareAddr) (l
|
|||
continue
|
||||
}
|
||||
|
||||
if l.IsStatic {
|
||||
if reqIP.IsValid() && l.IP == reqIP {
|
||||
return l
|
||||
} else if lease == nil {
|
||||
lease = l
|
||||
}
|
||||
|
||||
continue
|
||||
if usable := s.exactUsableLease(msgType, reqIP, l); usable != nil {
|
||||
return usable
|
||||
}
|
||||
|
||||
if reqIP.IsValid() && l.IP == reqIP {
|
||||
if (s.ipInCurrentPoolLocked(l.IP) && leaseNotExpired(l)) ||
|
||||
(canServeDeprecatedLease(msgType, l.IP, s.advertisedPrefixes) && leaseNotExpired(l)) {
|
||||
return l
|
||||
}
|
||||
} else if lease == nil && s.ipInCurrentPoolLocked(l.IP) && leaseNotExpired(l) {
|
||||
lease = l
|
||||
if lease == nil {
|
||||
lease = s.fallbackLease(l)
|
||||
}
|
||||
}
|
||||
|
||||
return lease
|
||||
}
|
||||
|
||||
// exactUsableLease returns lease when the request explicitly targets it and the
|
||||
// current server state still allows serving it.
|
||||
func (s *v6Server) exactUsableLease(
|
||||
msgType dhcpv6.MessageType,
|
||||
reqIP netip.Addr,
|
||||
lease *dhcpsvc.Lease,
|
||||
) (usable *dhcpsvc.Lease) {
|
||||
if lease.IsStatic {
|
||||
if exactRequestedLease(reqIP, lease) {
|
||||
return lease
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if usableRequestedDynamicLease(
|
||||
msgType,
|
||||
reqIP,
|
||||
s.ipInCurrentPoolLocked(lease.IP),
|
||||
s.advertisedPrefixes,
|
||||
lease,
|
||||
) {
|
||||
return lease
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// fallbackLease returns the best reusable lease candidate when no exact
|
||||
// request-target match was found.
|
||||
func (s *v6Server) fallbackLease(lease *dhcpsvc.Lease) (fallback *dhcpsvc.Lease) {
|
||||
switch {
|
||||
case lease.IsStatic:
|
||||
return lease
|
||||
case s.usableFallbackLease(lease):
|
||||
return lease
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// canServeDeprecatedLease reports whether a deprecated dynamic lease for ip may
|
||||
// still be served for msgType while its prefix remains advertised.
|
||||
func canServeDeprecatedLease(
|
||||
|
|
@ -1028,6 +1114,43 @@ func (s *v6Server) process(msg *dhcpv6.Message, req, resp dhcpv6.DHCPv6) bool {
|
|||
return true
|
||||
}
|
||||
|
||||
// newPacketResponse creates the base response for msg.
|
||||
func newPacketResponse(msg *dhcpv6.Message) (resp dhcpv6.DHCPv6, err error) {
|
||||
switch msg.Type() {
|
||||
case dhcpv6.MessageTypeSolicit:
|
||||
if msg.GetOneOption(dhcpv6.OptionRapidCommit) == nil {
|
||||
return dhcpv6.NewAdvertiseFromSolicit(msg)
|
||||
}
|
||||
|
||||
return dhcpv6.NewReplyFromMessage(msg)
|
||||
case dhcpv6.MessageTypeRequest,
|
||||
dhcpv6.MessageTypeConfirm,
|
||||
dhcpv6.MessageTypeRenew,
|
||||
dhcpv6.MessageTypeRebind,
|
||||
dhcpv6.MessageTypeRelease,
|
||||
dhcpv6.MessageTypeInformationRequest:
|
||||
return dhcpv6.NewReplyFromMessage(msg)
|
||||
default:
|
||||
return nil, fmt.Errorf("message type %d not supported", msg.Type())
|
||||
}
|
||||
}
|
||||
|
||||
// addProcessFailureStatus appends the recoverable status code for a failed
|
||||
// lease-processing path.
|
||||
func addProcessFailureStatus(msgType dhcpv6.MessageType, resp dhcpv6.DHCPv6) (ok bool) {
|
||||
code, text, ok := replyStatusForProcessFailure(msgType)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
resp.AddOption(&dhcpv6.OptStatusCode{
|
||||
StatusCode: code,
|
||||
StatusMessage: text,
|
||||
})
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// 1.
|
||||
// fe80::* (client) --(Solicit + ClientID+IANA())-> ff02::1:2
|
||||
// server -(Advertise + ClientID+ServerID+IANA(IAAddress)> fe80::*
|
||||
|
|
@ -1062,29 +1185,7 @@ func (s *v6Server) packetHandler(conn net.PacketConn, peer net.Addr, req dhcpv6.
|
|||
return
|
||||
}
|
||||
|
||||
var resp dhcpv6.DHCPv6
|
||||
|
||||
switch msg.Type() {
|
||||
case dhcpv6.MessageTypeSolicit:
|
||||
if msg.GetOneOption(dhcpv6.OptionRapidCommit) == nil {
|
||||
resp, err = dhcpv6.NewAdvertiseFromSolicit(msg)
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
resp, err = dhcpv6.NewReplyFromMessage(msg)
|
||||
case dhcpv6.MessageTypeRequest,
|
||||
dhcpv6.MessageTypeConfirm,
|
||||
dhcpv6.MessageTypeRenew,
|
||||
dhcpv6.MessageTypeRebind,
|
||||
dhcpv6.MessageTypeRelease,
|
||||
dhcpv6.MessageTypeInformationRequest:
|
||||
resp, err = dhcpv6.NewReplyFromMessage(msg)
|
||||
default:
|
||||
log.Error("dhcpv6: message type %d not supported", msg.Type())
|
||||
|
||||
return
|
||||
}
|
||||
resp, err := newPacketResponse(msg)
|
||||
if err != nil {
|
||||
log.Error("dhcpv6: %s", err)
|
||||
|
||||
|
|
@ -1094,12 +1195,7 @@ func (s *v6Server) packetHandler(conn net.PacketConn, peer net.Addr, req dhcpv6.
|
|||
resp.AddOption(dhcpv6.OptServerID(s.sid))
|
||||
|
||||
if !s.process(msg, req, resp) {
|
||||
if code, text, ok := replyStatusForProcessFailure(msg.Type()); ok {
|
||||
resp.AddOption(&dhcpv6.OptStatusCode{
|
||||
StatusCode: code,
|
||||
StatusMessage: text,
|
||||
})
|
||||
} else if requiresProcessSuccess(msg.Type()) {
|
||||
if !addProcessFailureStatus(msg.Type(), resp) && requiresProcessSuccess(msg.Type()) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
@ -1218,13 +1314,16 @@ func (s *v6Server) observeRAState(ctx context.Context) (obs raObservation, err e
|
|||
//
|
||||
// An active prefix whose preferred lifetime has already reached zero is
|
||||
// treated as unavailable for new leases. If another advertised prefix still
|
||||
// has a non-zero preferred lifetime, the pool is moved there immediately;
|
||||
// otherwise the pool is set to nil so new Solicit/Request pairs cannot reserve
|
||||
// addresses on a prefix we would then have to answer with a zero-lifetime
|
||||
// Reply. Existing leases on deprecated prefixes are still honored via the
|
||||
// deprecated-lease path in [v6Server.findUsableLease] and
|
||||
// has a non-zero preferred lifetime, the pool is moved there immediately.
|
||||
// Otherwise, the pool is set to nil so new Solicit/Request pairs cannot
|
||||
// reserve addresses on a prefix we would then have to answer with a
|
||||
// zero-lifetime Reply. Existing leases on deprecated prefixes are still
|
||||
// honored via the deprecated-lease path in [v6Server.findUsableLease] and
|
||||
// [v6Server.commitLease].
|
||||
func (s *v6Server) trackedPrefixChanged(active *raPrefixSnapshot, advertised []prefixPIO) (err error) {
|
||||
func (s *v6Server) trackedPrefixChanged(
|
||||
active *raPrefixSnapshot,
|
||||
advertised []prefixPIO,
|
||||
) (err error) {
|
||||
if !s.conf.NeedsDHCPv6Pool() {
|
||||
s.setTrackedRangeStart(nil, advertised)
|
||||
|
||||
|
|
@ -1290,46 +1389,7 @@ func (s *v6Server) setTrackedRangeStart(ipStart net.IP, advertised []prefixPIO)
|
|||
s.renewablePrefixes = renewable
|
||||
s.preferredUntilByPrefix = preferredUntil
|
||||
s.validUntilByPrefix = validUntil
|
||||
|
||||
activePrefix := netip.Prefix{}
|
||||
if len(ipStart) == net.IPv6len {
|
||||
if addr, ok := netip.AddrFromSlice(ipStart); ok {
|
||||
activePrefix = netip.PrefixFrom(addr, raObservedPrefixBits).Masked()
|
||||
}
|
||||
}
|
||||
|
||||
// Always clear and rebuild the occupancy bitmap from the surviving
|
||||
// leases. Callers such as ResetLeases replace s.leases wholesale
|
||||
// without touching s.ipAddrs, and an earlier version of this function
|
||||
// that skipped the rebuild when ipStart happened to be unchanged
|
||||
// left stale bits from dropped leases behind, which made the pool
|
||||
// appear exhausted long after those addresses had been released.
|
||||
s.ipAddrs = [256]byte{}
|
||||
|
||||
removed := 0
|
||||
updated := false
|
||||
leases := s.leases[:0]
|
||||
for _, l := range s.leases {
|
||||
if !l.IsStatic {
|
||||
pref := netip.PrefixFrom(l.IP, raObservedPrefixBits).Masked()
|
||||
if !leasePrefixAdvertised(keepPrefixes, l.IP) ||
|
||||
(activePrefix.IsValid() && pref == activePrefix && !ip6InRange(ipStart, net.IP(l.IP.AsSlice()))) {
|
||||
removed++
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if until, ok := validUntil[pref]; ok && (l.Expiry.IsZero() || l.Expiry.After(until)) {
|
||||
l.Expiry = until
|
||||
updated = true
|
||||
}
|
||||
}
|
||||
|
||||
leases = append(leases, l)
|
||||
s.markLeaseOccupied(l)
|
||||
}
|
||||
|
||||
s.leases = leases
|
||||
removed, updated := s.retainTrackedLeases(ipStart, keepPrefixes, validUntil)
|
||||
newDeprecated := deprecatedMetaFrom(now, renewable, keepPrefixes, validUntil)
|
||||
metadataChanged := (len(oldDeprecated) > 0 || len(newDeprecated) > 0) &&
|
||||
(!samePrefixSet(oldRenewable, renewable) || !sameDeadlineMap(oldDeprecated, newDeprecated))
|
||||
|
|
@ -1373,47 +1433,48 @@ func (s *v6Server) hasStaticV6Leases() (ok bool) {
|
|||
return false
|
||||
}
|
||||
|
||||
// restoreDeprecatedPrefixes seeds initial deprecated prefixes from persisted
|
||||
// metadata whose renewable prefixes still match the currently observed
|
||||
// interface state.
|
||||
func (s *v6Server) restoreDeprecatedPrefixes(now time.Time, st *raState) {
|
||||
s.leasesLock.Lock()
|
||||
defer s.leasesLock.Unlock()
|
||||
|
||||
s.persistRestoredMeta = false
|
||||
|
||||
if len(s.restoredDeprecated) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// restoredPrefixesMatchObserved reports whether the persisted renewable
|
||||
// prefixes still match the currently observed interface state.
|
||||
func restoredPrefixesMatchObserved(
|
||||
now time.Time,
|
||||
st *raState,
|
||||
restored map[netip.Prefix]struct{},
|
||||
) (ok bool) {
|
||||
observedRenewable := renewableLeasePrefixes(st.pios(now))
|
||||
if len(s.restoredRenewable) > 0 {
|
||||
if !prefixSetContainsAll(observedRenewable, s.restoredRenewable) {
|
||||
return
|
||||
}
|
||||
} else if len(observedRenewable) > 0 {
|
||||
return
|
||||
switch {
|
||||
case len(restored) > 0:
|
||||
return prefixSetContainsAll(observedRenewable, restored)
|
||||
case len(observedRenewable) > 0:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
advertised := advertisedLeasePrefixes(st.pios(now))
|
||||
if len(advertised) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if len(s.restoredRenewable) == 0 {
|
||||
overlap := false
|
||||
for pref := range advertised {
|
||||
if _, ok := s.restoredDeprecated[pref]; ok {
|
||||
overlap = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !overlap {
|
||||
return
|
||||
// restoredDeprecatedPrefixOverlap reports whether any persisted deprecated
|
||||
// prefix is still advertised.
|
||||
func restoredDeprecatedPrefixOverlap(
|
||||
advertised map[netip.Prefix]struct{},
|
||||
restored map[netip.Prefix]time.Time,
|
||||
) (ok bool) {
|
||||
for pref := range advertised {
|
||||
if _, found := restored[pref]; found {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
for pref, until := range s.restoredDeprecated {
|
||||
return false
|
||||
}
|
||||
|
||||
// restoreDeprecatedPrefixEntries seeds the tracked state with persisted
|
||||
// deprecated prefixes that are no longer advertised.
|
||||
func restoreDeprecatedPrefixEntries(
|
||||
st *raState,
|
||||
now time.Time,
|
||||
advertised map[netip.Prefix]struct{},
|
||||
restored map[netip.Prefix]time.Time,
|
||||
) {
|
||||
for pref, until := range restored {
|
||||
if _, ok := advertised[pref]; ok {
|
||||
continue
|
||||
}
|
||||
|
|
@ -1435,6 +1496,36 @@ func (s *v6Server) restoreDeprecatedPrefixes(now time.Time, st *raState) {
|
|||
}
|
||||
}
|
||||
|
||||
// restoreDeprecatedPrefixes seeds initial deprecated prefixes from persisted
|
||||
// metadata whose renewable prefixes still match the currently observed
|
||||
// interface state.
|
||||
func (s *v6Server) restoreDeprecatedPrefixes(now time.Time, st *raState) {
|
||||
s.leasesLock.Lock()
|
||||
defer s.leasesLock.Unlock()
|
||||
|
||||
s.persistRestoredMeta = false
|
||||
|
||||
if len(s.restoredDeprecated) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if !restoredPrefixesMatchObserved(now, st, s.restoredRenewable) {
|
||||
return
|
||||
}
|
||||
|
||||
advertised := advertisedLeasePrefixes(st.pios(now))
|
||||
if len(advertised) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if len(s.restoredRenewable) == 0 &&
|
||||
!restoredDeprecatedPrefixOverlap(advertised, s.restoredDeprecated) {
|
||||
return
|
||||
}
|
||||
|
||||
restoreDeprecatedPrefixEntries(st, now, advertised, s.restoredDeprecated)
|
||||
}
|
||||
|
||||
// setRestoredPrefixMeta stores deprecated-prefix metadata loaded from disk.
|
||||
func (s *v6Server) setRestoredPrefixMeta(
|
||||
renewable map[netip.Prefix]struct{},
|
||||
|
|
@ -1450,7 +1541,12 @@ func (s *v6Server) setRestoredPrefixMeta(
|
|||
|
||||
// deprecatedPrefixMeta returns persisted metadata for currently tracked
|
||||
// interface-derived prefixes.
|
||||
func (s *v6Server) deprecatedPrefixMeta(now time.Time) (renewable map[netip.Prefix]struct{}, deprecated map[netip.Prefix]time.Time) {
|
||||
func (s *v6Server) deprecatedPrefixMeta(
|
||||
now time.Time,
|
||||
) (
|
||||
renewable map[netip.Prefix]struct{},
|
||||
deprecated map[netip.Prefix]time.Time,
|
||||
) {
|
||||
s.leasesLock.Lock()
|
||||
defer s.leasesLock.Unlock()
|
||||
|
||||
|
|
@ -1486,98 +1582,165 @@ func (s *v6Server) deprecatedPrefixMetaLocked(now time.Time) (renewable map[neti
|
|||
return renewable, deprecated
|
||||
}
|
||||
|
||||
// Start starts the IPv6 DHCP server.
|
||||
func (s *v6Server) Start(ctx context.Context) (err error) {
|
||||
defer func() { err = errors.Annotate(err, "dhcpv6: %w") }()
|
||||
|
||||
if !s.conf.Enabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
ifaceName := s.conf.InterfaceName
|
||||
iface, err := net.InterfaceByName(ifaceName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("finding interface %s by name: %w", ifaceName, err)
|
||||
}
|
||||
|
||||
log.Debug("dhcpv6: starting...")
|
||||
|
||||
ok, err := s.configureDNSIPAddrs(ctx, iface)
|
||||
if err != nil {
|
||||
// Don't wrap the error, because it's informative enough as is.
|
||||
return err
|
||||
}
|
||||
|
||||
if !ok {
|
||||
if s.conf.NormalizedPrefixSource() != V6PrefixSourceInterface {
|
||||
// No available IP addresses which may appear later.
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
initial raState
|
||||
observe raObserver
|
||||
)
|
||||
|
||||
// startPrefixSourceState initializes the RA state and callbacks for the
|
||||
// configured prefix source.
|
||||
func (s *v6Server) startPrefixSourceState(
|
||||
ctx context.Context,
|
||||
) (initial raState, observe raObserver, err error) {
|
||||
switch s.conf.NormalizedPrefixSource() {
|
||||
case V6PrefixSourceStatic:
|
||||
initial = newStaticRAState(buildStaticRAObservation(s.dnsIPAddrs(), s.conf.ipStart))
|
||||
s.ra.onStateRefresh = nil
|
||||
s.ra.onActivePrefixChange = nil
|
||||
return initial, nil, nil
|
||||
case V6PrefixSourceInterface:
|
||||
if s.hasStaticV6Leases() {
|
||||
s.conf.Logger.WarnContext(
|
||||
ctx,
|
||||
"dhcpv6: interface-derived prefix tracking does not rewrite literal static IPv6 leases",
|
||||
)
|
||||
}
|
||||
|
||||
initial = newObservedRAState()
|
||||
|
||||
// Fail fast on initial-observation errors in interface mode.
|
||||
// The rest of the server depends on having at least one
|
||||
// observed prefix to bootstrap ipStart, advertisedPrefixes and
|
||||
// the deadline maps; without them reserveLease can't allocate
|
||||
// addresses and findUsableLease can't renew existing leases,
|
||||
// so swallowing the error here would bring DHCPv6 up as
|
||||
// "enabled" while silently refusing to hand out or renew
|
||||
// anything. Transient errors are rare compared to permanent
|
||||
// environment misconfigurations (missing ifconfig, netlink
|
||||
// denied, wrong interface, cancelled context) and surfacing
|
||||
// them at Start() is how an operator notices.
|
||||
obs, obsErr := s.observeRAState(ctx)
|
||||
if obsErr != nil {
|
||||
return fmt.Errorf("observing initial ipv6 prefix state: %w", obsErr)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
initial.merge(obs, now)
|
||||
s.restoreDeprecatedPrefixes(now, &initial)
|
||||
if pios := initial.pios(now); len(pios) > 0 {
|
||||
if err = s.trackedPrefixChanged(initial.activeSnapshot(now), pios); err != nil {
|
||||
return fmt.Errorf("updating tracked range start: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
observe = s.observeRAState
|
||||
s.ra.onStateRefresh = func(now time.Time, st *raState) {
|
||||
s.restoreDeprecatedPrefixes(now, st)
|
||||
}
|
||||
s.ra.onActivePrefixChange = func(active *raPrefixSnapshot, advertised []prefixPIO) {
|
||||
if activeErr := s.trackedPrefixChanged(active, advertised); activeErr != nil {
|
||||
log.Error("dhcpv6: updating tracked pool: %s", activeErr)
|
||||
}
|
||||
}
|
||||
return s.startInterfacePrefixTracking(ctx)
|
||||
default:
|
||||
return fmt.Errorf("unsupported prefix source %q", s.conf.PrefixSource)
|
||||
return raState{}, nil, fmt.Errorf("unsupported prefix source %q", s.conf.PrefixSource)
|
||||
}
|
||||
}
|
||||
|
||||
// startInterfacePrefixTracking initializes interface-derived prefix tracking.
|
||||
func (s *v6Server) startInterfacePrefixTracking(
|
||||
ctx context.Context,
|
||||
) (initial raState, observe raObserver, err error) {
|
||||
if s.hasStaticV6Leases() {
|
||||
s.conf.Logger.WarnContext(
|
||||
ctx,
|
||||
"dhcpv6: interface-derived prefix tracking does not rewrite literal static IPv6 leases",
|
||||
)
|
||||
}
|
||||
|
||||
err = s.initRA(iface, initial, observe)
|
||||
if err != nil {
|
||||
return err
|
||||
initial = newObservedRAState()
|
||||
|
||||
// Fail fast on initial-observation errors in interface mode. The rest of
|
||||
// the server depends on having at least one observed prefix to bootstrap
|
||||
// ipStart, advertisedPrefixes and the deadline maps; without them
|
||||
// reserveLease can't allocate addresses and findUsableLease can't renew
|
||||
// existing leases, so swallowing the error here would bring DHCPv6 up as
|
||||
// "enabled" while silently refusing to hand out or renew anything.
|
||||
obs, obsErr := s.observeRAState(ctx)
|
||||
if obsErr != nil {
|
||||
return raState{}, nil, fmt.Errorf("observing initial ipv6 prefix state: %w", obsErr)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
initial.merge(obs, now)
|
||||
s.restoreDeprecatedPrefixes(now, &initial)
|
||||
if pios := initial.pios(now); len(pios) > 0 {
|
||||
if err = s.trackedPrefixChanged(initial.activeSnapshot(now), pios); err != nil {
|
||||
return raState{}, nil, fmt.Errorf("updating tracked range start: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
observe = s.observeRAState
|
||||
s.ra.onStateRefresh = func(now time.Time, st *raState) {
|
||||
s.restoreDeprecatedPrefixes(now, st)
|
||||
}
|
||||
s.ra.onActivePrefixChange = func(active *raPrefixSnapshot, advertised []prefixPIO) {
|
||||
if activeErr := s.trackedPrefixChanged(active, advertised); activeErr != nil {
|
||||
log.Error("dhcpv6: updating tracked pool: %s", activeErr)
|
||||
}
|
||||
}
|
||||
|
||||
return initial, observe, nil
|
||||
}
|
||||
|
||||
// activeTrackedPrefix returns the prefix for the current tracked DHCPv6 pool.
|
||||
func activeTrackedPrefix(ipStart net.IP) (prefix netip.Prefix) {
|
||||
if len(ipStart) != net.IPv6len {
|
||||
return netip.Prefix{}
|
||||
}
|
||||
|
||||
if addr, ok := netip.AddrFromSlice(ipStart); ok {
|
||||
return netip.PrefixFrom(addr, raObservedPrefixBits).Masked()
|
||||
}
|
||||
|
||||
return netip.Prefix{}
|
||||
}
|
||||
|
||||
// shouldKeepTrackedLease reports whether l still belongs to the active tracked
|
||||
// pool after the prefix transition.
|
||||
func shouldKeepTrackedLease(
|
||||
ipStart net.IP,
|
||||
activePrefix netip.Prefix,
|
||||
keepPrefixes map[netip.Prefix]struct{},
|
||||
l *dhcpsvc.Lease,
|
||||
) (ok bool) {
|
||||
if !leasePrefixAdvertised(keepPrefixes, l.IP) {
|
||||
return false
|
||||
}
|
||||
|
||||
if activePrefix.IsValid() &&
|
||||
netip.PrefixFrom(l.IP, raObservedPrefixBits).Masked() == activePrefix &&
|
||||
!ip6InRange(ipStart, net.IP(l.IP.AsSlice())) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// updateTrackedLeaseExpiry clamps l to the tracked prefix deadline when
|
||||
// needed.
|
||||
func updateTrackedLeaseExpiry(
|
||||
validUntil map[netip.Prefix]time.Time,
|
||||
l *dhcpsvc.Lease,
|
||||
) (updated bool) {
|
||||
pref := netip.PrefixFrom(l.IP, raObservedPrefixBits).Masked()
|
||||
until, ok := validUntil[pref]
|
||||
if !ok || (!l.Expiry.IsZero() && !l.Expiry.After(until)) {
|
||||
return false
|
||||
}
|
||||
|
||||
l.Expiry = until
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// retainTrackedLeases rebuilds the in-memory lease slice for a tracked prefix
|
||||
// transition.
|
||||
func (s *v6Server) retainTrackedLeases(
|
||||
ipStart net.IP,
|
||||
keepPrefixes map[netip.Prefix]struct{},
|
||||
validUntil map[netip.Prefix]time.Time,
|
||||
) (removed int, updated bool) {
|
||||
activePrefix := activeTrackedPrefix(ipStart)
|
||||
|
||||
// Always clear and rebuild the occupancy bitmap from the surviving
|
||||
// leases.
|
||||
s.ipAddrs = [256]byte{}
|
||||
|
||||
leases := s.leases[:0]
|
||||
for _, l := range s.leases {
|
||||
if !l.IsStatic {
|
||||
if !shouldKeepTrackedLease(ipStart, activePrefix, keepPrefixes, l) {
|
||||
removed++
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if updateTrackedLeaseExpiry(validUntil, l) {
|
||||
updated = true
|
||||
}
|
||||
}
|
||||
|
||||
leases = append(leases, l)
|
||||
s.markLeaseOccupied(l)
|
||||
}
|
||||
|
||||
s.leases = leases
|
||||
|
||||
return removed, updated
|
||||
}
|
||||
|
||||
// skipStartAfterDNSConfig reports whether Start should return after the DNS
|
||||
// address lookup without initializing RA state yet.
|
||||
func skipStartAfterDNSConfig(ok bool, prefixSource V6PrefixSource) (skip bool) {
|
||||
return !ok && prefixSource != V6PrefixSourceInterface
|
||||
}
|
||||
|
||||
// startDHCPv6Server initializes the DHCPv6 listener after RA state is ready.
|
||||
func (s *v6Server) startDHCPv6Server(iface *net.Interface) (err error) {
|
||||
// Don't initialize DHCPv6 server if we must force the clients to use SLAAC.
|
||||
if !s.conf.NeedsDHCPv6Pool() {
|
||||
log.Debug("not starting dhcpv6 server due to ra_slaac_only=true")
|
||||
|
|
@ -1614,6 +1777,46 @@ func (s *v6Server) Start(ctx context.Context) (err error) {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Start starts the IPv6 DHCP server.
|
||||
func (s *v6Server) Start(ctx context.Context) (err error) {
|
||||
defer func() { err = errors.Annotate(err, "dhcpv6: %w") }()
|
||||
|
||||
if !s.conf.Enabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
ifaceName := s.conf.InterfaceName
|
||||
iface, err := net.InterfaceByName(ifaceName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("finding interface %s by name: %w", ifaceName, err)
|
||||
}
|
||||
|
||||
log.Debug("dhcpv6: starting...")
|
||||
|
||||
ok, err := s.configureDNSIPAddrs(ctx, iface)
|
||||
if err != nil {
|
||||
// Don't wrap the error, because it's informative enough as is.
|
||||
return err
|
||||
}
|
||||
|
||||
if skipStartAfterDNSConfig(ok, s.conf.NormalizedPrefixSource()) {
|
||||
// No available IP addresses which may appear later.
|
||||
return nil
|
||||
}
|
||||
|
||||
initial, observe, err := s.startPrefixSourceState(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = s.initRA(iface, initial, observe)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.startDHCPv6Server(iface)
|
||||
}
|
||||
|
||||
// Stop - stop server
|
||||
func (s *v6Server) Stop() (err error) {
|
||||
err = s.ra.Close()
|
||||
|
|
@ -1638,6 +1841,52 @@ func (s *v6Server) Stop() (err error) {
|
|||
return nil
|
||||
}
|
||||
|
||||
// validateV6CreateRangeStart checks whether conf has the range-start value the
|
||||
// server setup needs.
|
||||
func validateV6CreateRangeStart(conf V6ServerConf) (err error) {
|
||||
needsConfiguredRange := conf.NormalizedPrefixSource() == V6PrefixSourceStatic || conf.NeedsDHCPv6Pool()
|
||||
if needsConfiguredRange && (conf.RangeStart == nil || conf.RangeStart.To16() == nil) {
|
||||
return fmt.Errorf("invalid range-start IP: %s", conf.RangeStart)
|
||||
}
|
||||
|
||||
if len(conf.RangeStart) != 0 && conf.RangeStart.To16() == nil {
|
||||
return fmt.Errorf("invalid range-start IP: %s", conf.RangeStart)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// configureV6CreateRangeStart normalizes the configured range-start IP.
|
||||
func configureV6CreateRangeStart(conf *V6ServerConf) {
|
||||
if len(conf.RangeStart) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
conf.RangeStart = bytes.Clone(conf.RangeStart.To16())
|
||||
}
|
||||
|
||||
// configureV6CreateStaticPrefix seeds the tracked pool for static prefix
|
||||
// source mode.
|
||||
func (s *v6Server) configureV6CreateStaticPrefix() {
|
||||
s.conf.ipStart = bytes.Clone(s.conf.RangeStart)
|
||||
if addr, ok := netip.AddrFromSlice(s.conf.ipStart); ok {
|
||||
prefix := netip.PrefixFrom(addr, raObservedPrefixBits).Masked()
|
||||
s.advertisedPrefixes = map[netip.Prefix]struct{}{prefix: {}}
|
||||
s.renewablePrefixes = map[netip.Prefix]struct{}{prefix: {}}
|
||||
}
|
||||
}
|
||||
|
||||
// configureV6CreateLeaseDuration fills in the effective lease duration.
|
||||
func (s *v6Server) configureV6CreateLeaseDuration(conf V6ServerConf) {
|
||||
if conf.LeaseDuration == 0 {
|
||||
s.conf.leaseTime = timeutil.Day
|
||||
s.conf.LeaseDuration = uint32(s.conf.leaseTime.Seconds())
|
||||
return
|
||||
}
|
||||
|
||||
s.conf.leaseTime = time.Second * time.Duration(conf.LeaseDuration)
|
||||
}
|
||||
|
||||
// Create DHCPv6 server
|
||||
func v6Create(conf V6ServerConf) (DHCPServer, error) {
|
||||
s := &v6Server{}
|
||||
|
|
@ -1664,33 +1913,16 @@ func v6Create(conf V6ServerConf) (DHCPServer, error) {
|
|||
return s, nil
|
||||
}
|
||||
|
||||
needsConfiguredRange := conf.NormalizedPrefixSource() == V6PrefixSourceStatic || conf.NeedsDHCPv6Pool()
|
||||
if needsConfiguredRange && (conf.RangeStart == nil || conf.RangeStart.To16() == nil) {
|
||||
return s, fmt.Errorf("dhcpv6: invalid range-start IP: %s", conf.RangeStart)
|
||||
}
|
||||
if len(conf.RangeStart) != 0 {
|
||||
if conf.RangeStart.To16() == nil {
|
||||
return s, fmt.Errorf("dhcpv6: invalid range-start IP: %s", conf.RangeStart)
|
||||
}
|
||||
|
||||
s.conf.RangeStart = bytes.Clone(conf.RangeStart.To16())
|
||||
if err = validateV6CreateRangeStart(conf); err != nil {
|
||||
return s, fmt.Errorf("dhcpv6: %w", err)
|
||||
}
|
||||
configureV6CreateRangeStart(&s.conf)
|
||||
|
||||
if conf.NormalizedPrefixSource() == V6PrefixSourceStatic {
|
||||
s.conf.ipStart = bytes.Clone(s.conf.RangeStart)
|
||||
if addr, ok := netip.AddrFromSlice(s.conf.ipStart); ok {
|
||||
prefix := netip.PrefixFrom(addr, raObservedPrefixBits).Masked()
|
||||
s.advertisedPrefixes = map[netip.Prefix]struct{}{prefix: {}}
|
||||
s.renewablePrefixes = map[netip.Prefix]struct{}{prefix: {}}
|
||||
}
|
||||
s.configureV6CreateStaticPrefix()
|
||||
}
|
||||
|
||||
if conf.LeaseDuration == 0 {
|
||||
s.conf.leaseTime = timeutil.Day
|
||||
s.conf.LeaseDuration = uint32(s.conf.leaseTime.Seconds())
|
||||
} else {
|
||||
s.conf.leaseTime = time.Second * time.Duration(conf.LeaseDuration)
|
||||
}
|
||||
s.configureV6CreateLeaseDuration(conf)
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -726,8 +726,9 @@ func TestV6ResetLeases_PreservesAdvertisedInterfacePrefixes(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, s.leases, 2)
|
||||
assert.Contains(t, []netip.Addr{s.leases[0].IP, s.leases[1].IP}, netip.MustParseAddr("2001:db8::10"))
|
||||
assert.Contains(t, []netip.Addr{s.leases[0].IP, s.leases[1].IP}, netip.MustParseAddr("2001:db8:1::10"))
|
||||
gotLeases := []netip.Addr{s.leases[0].IP, s.leases[1].IP}
|
||||
assert.Contains(t, gotLeases, netip.MustParseAddr("2001:db8::10"))
|
||||
assert.Contains(t, gotLeases, netip.MustParseAddr("2001:db8:1::10"))
|
||||
}
|
||||
|
||||
func TestObservedDNSIPAddrs(t *testing.T) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue