Files
kube-vip/pkg/vip/dhcpv6.go
Patryk Strusiewicz-Surmacki b4771c5319 Fixed DHCP concurrency exit issues
Signed-off-by: Patryk Strusiewicz-Surmacki <patryk.pawel.strusiewicz-surmacki@external.telekom.de>
2026-09-13 22:01:14 +02:00

439 lines
12 KiB
Go

package vip
import (
"context"
"fmt"
log "log/slog"
"net"
"sync"
"sync/atomic"
"time"
"github.com/insomniacslk/dhcp/dhcpv6"
"github.com/insomniacslk/dhcp/dhcpv6/nclient6"
"github.com/insomniacslk/dhcp/iana"
"github.com/jpillora/backoff"
"github.com/vishvananda/netlink"
)
var dhcpv6ClientManager *DHCPv6ClientManager
func init() {
dhcpv6ClientManager = NewDHCPv6ClientManager()
}
type DHCPv6ClientManager struct {
clients map[string]*DHCPv6InternalClient
}
func NewDHCPv6ClientManager() *DHCPv6ClientManager {
return &DHCPv6ClientManager{
clients: map[string]*DHCPv6InternalClient{},
}
}
func (m *DHCPv6ClientManager) Get(iface string) *DHCPv6InternalClient {
c, exists := m.clients[iface]
if !exists {
return nil
}
return c
}
func (m *DHCPv6ClientManager) Add(iface string) (*DHCPv6InternalClient, error) {
c := m.Get(iface)
if c != nil {
c.references.Add(1)
return c, nil
}
client, err := NewDHCPv6InternalClient(iface)
if err != nil {
return nil, err
}
m.clients[iface] = client
return client, nil
}
func (m *DHCPv6ClientManager) Delete(iface string) {
c := m.Get(iface)
if c != nil {
c.references.Add(-1)
ref := c.references.Load()
if ref < 1 {
c.client.Close()
delete(m.clients, iface)
}
}
}
type DHCPv6InternalClient struct {
client *nclient6.Client
references *atomic.Int32
}
func NewDHCPv6InternalClient(iface string) (*DHCPv6InternalClient, error) {
client, err := nclient6.New(iface)
if err != nil {
return nil, fmt.Errorf("failed to create DHCPv6 client for interface %q: %w", iface, err)
}
ref := &atomic.Int32{}
ref.Store(1)
return &DHCPv6InternalClient{
client: client,
references: ref,
}, nil
}
type DHCPv6Client struct {
iface *net.Interface
managerKey string
ddnsHostName string
initRebootFlag bool
requestedIP net.IP
stopChan chan struct{} // used as a signal to release the IP and stop the dhcp client daemon
errorChan chan error // indicates there was an error on the IP request
ipChan chan string
ic *DHCPv6InternalClient
addr *dhcpv6.OptIAAddress
backoffAttempts uint
stop sync.Once
mtx sync.RWMutex
}
func (c *DHCPv6Client) storeAddr(addr *dhcpv6.OptIAAddress) {
c.mtx.Lock()
defer c.mtx.Unlock()
c.addr = addr
}
func (c *DHCPv6Client) loadAddr() *dhcpv6.OptIAAddress {
c.mtx.RLock()
defer c.mtx.RUnlock()
return c.addr
}
// NewDHCPv6Client returns a new DHCP6 Client.
func NewDHCPv6Client(iface *net.Interface, parent netlink.Link, initRebootFlag bool, requestedIP string, backoffAttempts uint) (*DHCPv6Client, error) {
name := iface.Name
if parent != nil {
name = parent.Attrs().Name
}
client, err := dhcpv6ClientManager.Add(name)
if err != nil {
return nil, fmt.Errorf("failed to create DHCPv6 client: %w", err)
}
return &DHCPv6Client{
iface: iface,
managerKey: name,
stopChan: make(chan struct{}),
errorChan: make(chan error),
initRebootFlag: initRebootFlag,
requestedIP: net.ParseIP(requestedIP),
ipChan: make(chan string),
ic: client,
backoffAttempts: backoffAttempts,
}, nil
}
func (c *DHCPv6Client) WithHostName(hostname string) DHCPClient {
c.ddnsHostName = hostname
return c
}
// Stop state-transition process and close dhcp client
func (c *DHCPv6Client) Stop() {
c.close()
}
// Close dhcp client channels
func (c *DHCPv6Client) close() {
c.stop.Do(func() {
close(c.ipChan)
close(c.stopChan)
})
dhcpv6ClientManager.Delete(c.managerKey)
}
// Gets the IPChannel for consumption
func (c *DHCPv6Client) IPChannel() chan string {
return c.ipChan
}
// Gets the ErrorChannel for consumption
func (c *DHCPv6Client) ErrorChannel() chan error {
return c.errorChan
}
func (c *DHCPv6Client) Start(ctx context.Context) error {
addr, err := c.requestWithBackoff(ctx)
if err != nil {
return fmt.Errorf("DHCPv6 client failed: %w", err)
}
c.initRebootFlag = false
c.storeAddr(addr)
// Set up two ticker to renew/rebind regularly
t1Timeout, t2Timeout := getAddrTimeouts(addr)
t1, t2 := time.NewTimer(t1Timeout), time.NewTimer(t2Timeout)
for {
select {
case <-c.stopChan:
return c.killProcessing(t1, t2)
case <-ctx.Done():
c.close()
return c.killProcessing(t1, t2)
case <-t1.C:
// renew is a unicast request of the IP renewal
// A point on renew is: the library does not return the right message (NAK)
// on renew error due to IP Change, but instead it returns a different error
// This way there's not much to do other than log and continue, as the renew error
// may be an offline server, or may be an incorrect package match
addr, err := c.renew(ctx)
if err == nil {
c.storeAddr(addr)
log.Info("[DHCPv6] renew", "addr", addr.IPv6Addr.String())
t1Timeout, t2Timeout = getAddrTimeouts(addr)
t2.Reset(t2Timeout)
} else {
log.Error("[DHCPv6] renew failed", "err", err)
}
t1.Reset(t1Timeout)
case <-t2.C:
// rebind is just like a request, but forcing to provide a new IP address
addr, err := c.request(ctx, true)
if err == nil {
c.storeAddr(addr)
log.Info("[DHCPv6] rebind", "lease", addr)
t1Timeout, t2Timeout = getAddrTimeouts(addr)
} else {
addr = c.loadAddr()
log.Warn("[DHCPv6] ip may have changed", "ip", addr.IPv6Addr.String(), "err", err)
c.initRebootFlag = false
addr, backoffErr := c.requestWithBackoff(ctx)
if backoffErr != nil {
log.Error("[DHCPv6] failed to reacquire lease", "err", backoffErr)
continue
}
c.storeAddr(addr)
t1Timeout, t2Timeout = getAddrTimeouts(addr)
}
t1.Reset(t1Timeout)
t2.Reset(t2Timeout)
}
}
}
func getAddrTimeouts(addr *dhcpv6.OptIAAddress) (time.Duration, time.Duration) {
t1Timeout, t2Timeout := addr.PreferredLifetime/2, (addr.ValidLifetime/8)*7
log.Debug("[DHCPv6] timeouts", "address", addr.IPv6Addr.String(), "T1", t1Timeout, "T2", t2Timeout)
return t1Timeout, t2Timeout
}
func (c *DHCPv6Client) killProcessing(t1, t2 *time.Timer) error {
// create new context for DHCP cleanup (independent)
dhcpStopCtx, cancel := context.WithCancel(context.Background())
defer cancel()
// IP address release.
var err error
if c.loadAddr() != nil {
if err = c.release(dhcpStopCtx); err != nil {
log.Error("[DHCPv6] release failed", "err", err)
} else {
log.Info("[DHCPv6] released", "address", c.addr.String())
}
}
t1.Stop()
t2.Stop()
return err
}
func (c *DHCPv6Client) requestWithBackoff(ctx context.Context) (*dhcpv6.OptIAAddress, error) {
backoff := backoff.Backoff{
Factor: 2,
Jitter: true,
Min: 10 * time.Second,
Max: 1 * time.Minute,
}
var addr *dhcpv6.OptIAAddress
var err error
log.Debug("[DHCPv6]", "attempts", c.backoffAttempts)
RequestLoop:
for {
select {
case <-ctx.Done():
return nil, fmt.Errorf("[DHCPv6] context error: %w", ctx.Err())
default:
log.Debug("[DHCPv6] trying to get a new IP", "attempt", backoff.Attempt()+1)
addr, err = c.request(ctx, false)
if err != nil {
dur := backoff.Duration()
if c.backoffAttempts > 0 && backoff.Attempt() > float64(c.backoffAttempts)-1 {
errMsg := fmt.Errorf("failed to get an IPv4 address after %d attempt(s), giving up, error: %s", c.backoffAttempts, err.Error())
log.Error(fmt.Sprintf("[DHCPv6] %s", errMsg.Error()))
c.errorChan <- errMsg
return nil, fmt.Errorf("failed to get IPv6 address: %w", err)
}
log.Error("[DHCPv6] request failed", "attempt", backoff.Attempt(), "err", err.Error(), "waiting", dur)
t := time.NewTimer(dur)
select {
case <-t.C:
t.Stop()
case <-ctx.Done():
}
continue RequestLoop
}
backoff.Reset()
break RequestLoop
}
}
if c.ipChan != nil {
c.ipChan <- addr.IPv6Addr.String()
}
return addr, nil
}
func (c *DHCPv6Client) request(ctx context.Context, rebind bool) (*dhcpv6.OptIAAddress, error) {
modifiers := []dhcpv6.Modifier{}
modifiers = append(modifiers, dhcpv6.WithClientID(&dhcpv6.DUIDEN{EnterpriseNumber: 1, EnterpriseIdentifier: []byte(c.ddnsHostName)}))
modifiers = append(modifiers, dhcpv6.WithFQDN(4, c.ddnsHostName))
// if initRebootFlag is set, this means we have an IP already set on c.requestedIP that should be used
if c.initRebootFlag {
log.Debug("[DHCPv6] init-reboot", "ip", c.requestedIP)
addr := dhcpv6.OptIAAddress{
IPv6Addr: c.requestedIP,
}
modifiers = append(modifiers, dhcpv6.WithIANA(addr))
} else if rebind {
if c.addr == nil {
return nil, fmt.Errorf("unable to rebind - current IP unknown")
}
log.Debug("[DHCPv6] rebinding", "ip", c.addr.IPv6Addr)
modifiers = append(modifiers, dhcpv6.WithIANA(*c.addr))
}
var reply *dhcpv6.Message
if rebind || c.initRebootFlag {
request, err := dhcpv6.NewMessage(modifiers...)
if err != nil {
return nil, fmt.Errorf("failed to create rebind message: %w", err)
}
request.MessageType = dhcpv6.MessageTypeRebind
reply, err = c.ic.client.SendAndRead(ctx, c.ic.client.RemoteAddr(), request, nil)
if err != nil {
return nil, fmt.Errorf("rebind error: %w", err)
}
} else {
adv, err := c.ic.client.Solicit(ctx, modifiers...)
if err != nil {
return nil, fmt.Errorf("solicit error: %w", err)
}
request, err := dhcpv6.NewRequestFromAdvertise(adv, modifiers...)
if err != nil {
return nil, fmt.Errorf("unable to create request message: %w", err)
}
request.MessageType = dhcpv6.MessageTypeAdvertise
reply, err = c.ic.client.Request(ctx, request, modifiers...)
if err != nil {
return nil, fmt.Errorf("request error: %w", err)
}
}
if reply == nil {
return nil, fmt.Errorf("invalid request")
}
return getAddress(reply.Options.IANA())
}
func (c *DHCPv6Client) renew(ctx context.Context) (*dhcpv6.OptIAAddress, error) {
modifiers := []dhcpv6.Modifier{}
modifiers = append(modifiers, dhcpv6.WithClientID(&dhcpv6.DUIDEN{EnterpriseNumber: 1, EnterpriseIdentifier: []byte(c.ddnsHostName)}))
modifiers = append(modifiers, dhcpv6.WithFQDN(4, c.ddnsHostName))
modifiers = append(modifiers, dhcpv6.WithOption(&dhcpv6.OptionGeneric{OptionCode: dhcpv6.OptionUnicast}))
adv, err := c.ic.client.Solicit(ctx, modifiers...)
if err != nil {
return nil, fmt.Errorf("solicit error: %w", err)
}
request, err := dhcpv6.NewRequestFromAdvertise(adv)
if err != nil {
return nil, fmt.Errorf("failed to create request message: %w", err)
}
request.MessageType = dhcpv6.MessageTypeRenew
reply, err := c.ic.client.SendAndRead(ctx, c.ic.client.RemoteAddr(), request, nil)
if err != nil {
return nil, fmt.Errorf("failed to send renew: %w", err)
}
return getAddress(reply.Options.IANA())
}
func (c *DHCPv6Client) release(ctx context.Context) error {
modifiers := []dhcpv6.Modifier{}
modifiers = append(modifiers, dhcpv6.WithClientID(&dhcpv6.DUIDEN{EnterpriseNumber: 1, EnterpriseIdentifier: []byte(c.ddnsHostName)}))
modifiers = append(modifiers, dhcpv6.WithFQDN(4, c.ddnsHostName))
adv, err := c.ic.client.Solicit(ctx, modifiers...)
if err != nil {
return fmt.Errorf("solicit error: %w", err)
}
request, err := dhcpv6.NewRequestFromAdvertise(adv)
if err != nil {
return fmt.Errorf("failed to create release message: %w", err)
}
request.MessageType = dhcpv6.MessageTypeRelease
reply, err := c.ic.client.SendAndRead(ctx, c.ic.client.RemoteAddr(), request, nil)
if err != nil {
return fmt.Errorf("failed to send release: %w", err)
}
if reply.Options.Status().StatusCode != iana.StatusSuccess {
return fmt.Errorf("release failed with code %d: %s", reply.Options.Status().StatusCode, reply.Options.Status().StatusMessage)
}
return nil
}
func getAddress(iana []*dhcpv6.OptIANA) (*dhcpv6.OptIAAddress, error) {
if len(iana) < 1 {
return nil, fmt.Errorf("failed to get IANA")
}
addrs := iana[0].Options.Addresses()
if len(addrs) == 0 {
return nil, fmt.Errorf("IANA contained no addresses")
}
return addrs[0], nil
}