mirror of
https://hubproxy.babadafafafafa.cn/https://github.com/kube-vip/kube-vip.git
synced 2026-09-20 08:03:47 +08:00
447 lines
12 KiB
Go
447 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 {
|
|
// mu guards clients and the reference counts of its entries together, so a
|
|
// concurrent Add cannot join a client that Delete is already retiring.
|
|
mu sync.Mutex
|
|
clients map[string]*DHCPv6InternalClient
|
|
}
|
|
|
|
func NewDHCPv6ClientManager() *DHCPv6ClientManager {
|
|
return &DHCPv6ClientManager{
|
|
clients: map[string]*DHCPv6InternalClient{},
|
|
}
|
|
}
|
|
|
|
func (m *DHCPv6ClientManager) Get(iface string) *DHCPv6InternalClient {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
return m.clients[iface]
|
|
}
|
|
|
|
func (m *DHCPv6ClientManager) Add(iface string) (*DHCPv6InternalClient, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
if c := m.clients[iface]; 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) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
c := m.clients[iface]
|
|
if c == nil {
|
|
return
|
|
}
|
|
if c.references.Add(-1) < 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.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 IPv6 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 {
|
|
// Nothing closes ipChan, so never block on a consumer that already stopped.
|
|
select {
|
|
case c.ipChan <- addr.IPv6Addr.String():
|
|
case <-c.stopChan:
|
|
case <-ctx.Done():
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|