mirror of https://github.com/jetkvm/kvm.git
408 lines
8.5 KiB
Go
408 lines
8.5 KiB
Go
package jetdhcpc
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net"
|
|
"slices"
|
|
|
|
"time"
|
|
|
|
"github.com/jetkvm/kvm/internal/sync"
|
|
"github.com/jetkvm/kvm/pkg/nmlite/link"
|
|
|
|
"github.com/insomniacslk/dhcp/dhcpv4"
|
|
"github.com/insomniacslk/dhcp/dhcpv6"
|
|
"github.com/jetkvm/kvm/internal/network/types"
|
|
"github.com/rs/zerolog"
|
|
)
|
|
|
|
const (
|
|
VendorIdentifier = "jetkvm"
|
|
)
|
|
|
|
var (
|
|
ErrIPv6LinkTimeout = errors.New("timeout after waiting for a non-tentative IPv6 address")
|
|
ErrIPv6RouteTimeout = errors.New("timeout after waiting for an IPv6 route")
|
|
ErrInterfaceUpTimeout = errors.New("timeout after waiting for an interface to come up")
|
|
ErrInterfaceUpCanceled = errors.New("context canceled while waiting for an interface to come up")
|
|
)
|
|
|
|
type LeaseChangeHandler func(lease *types.DHCPLease)
|
|
|
|
// Config is a DHCP client configuration.
|
|
type Config struct {
|
|
LinkUpTimeout time.Duration
|
|
|
|
// Timeout is the timeout for one DHCP request attempt.
|
|
Timeout time.Duration
|
|
|
|
// Retries is how many times to retry DHCP attempts.
|
|
Retries int
|
|
|
|
// IPv4 is whether to request an IPv4 lease.
|
|
IPv4 bool
|
|
|
|
// IPv6 is whether to request an IPv6 lease.
|
|
IPv6 bool
|
|
|
|
// Modifiers4 allows modifications to the IPv4 DHCP request.
|
|
Modifiers4 []dhcpv4.Modifier
|
|
|
|
// Modifiers6 allows modifications to the IPv6 DHCP request.
|
|
Modifiers6 []dhcpv6.Modifier
|
|
|
|
// V6ServerAddr can be a unicast or broadcast destination for DHCPv6
|
|
// messages.
|
|
//
|
|
// If not set, it will default to nclient6's default (all servers &
|
|
// relay agents).
|
|
V6ServerAddr *net.UDPAddr
|
|
|
|
// V6ClientPort is the port that is used to send and receive DHCPv6
|
|
// messages.
|
|
//
|
|
// If not set, it will default to dhcpv6's default (546).
|
|
V6ClientPort *int
|
|
|
|
// V4ServerAddr can be a unicast or broadcast destination for IPv4 DHCP
|
|
// messages.
|
|
//
|
|
// If not set, it will default to nclient4's default (DHCP broadcast
|
|
// address).
|
|
V4ServerAddr *net.UDPAddr
|
|
|
|
// If true, add Client Identifier (61) option to the IPv4 request.
|
|
V4ClientIdentifier bool
|
|
|
|
Hostname string
|
|
|
|
OnLease4Change LeaseChangeHandler
|
|
OnLease6Change LeaseChangeHandler
|
|
|
|
UpdateResolvConf func([]string) error
|
|
}
|
|
|
|
// Client is a DHCP client.
|
|
type Client struct {
|
|
types.DHCPClient
|
|
|
|
ifaces []string
|
|
cfg Config
|
|
l *zerolog.Logger
|
|
|
|
ctx context.Context
|
|
|
|
// TODO: support multiple interfaces
|
|
currentLease4 *Lease
|
|
currentLease6 *Lease
|
|
|
|
mu sync.Mutex
|
|
cfgMu sync.Mutex
|
|
|
|
lease4Mu sync.Mutex
|
|
lease6Mu sync.Mutex
|
|
|
|
timer4 *time.Timer
|
|
timer6 *time.Timer
|
|
stateDir string
|
|
}
|
|
|
|
var (
|
|
defaultTimerDuration = 1 * time.Second
|
|
defaultLinkUpTimeout = 30 * time.Second
|
|
maxRenewalAttemptDuration = 2 * time.Hour
|
|
)
|
|
|
|
// NewClient creates a new DHCP client for the given interface.
|
|
func NewClient(ctx context.Context, ifaces []string, c *Config, l *zerolog.Logger) (*Client, error) {
|
|
timer4 := time.NewTimer(defaultTimerDuration)
|
|
timer6 := time.NewTimer(defaultTimerDuration)
|
|
|
|
cfg := *c
|
|
if cfg.LinkUpTimeout == 0 {
|
|
cfg.LinkUpTimeout = defaultLinkUpTimeout
|
|
}
|
|
|
|
if cfg.Timeout == 0 {
|
|
cfg.Timeout = defaultLinkUpTimeout
|
|
}
|
|
|
|
if cfg.Retries == 0 {
|
|
cfg.Retries = 3
|
|
}
|
|
|
|
return &Client{
|
|
ctx: ctx,
|
|
ifaces: ifaces,
|
|
cfg: cfg,
|
|
l: l,
|
|
stateDir: "/run/jetkvm-dhcp",
|
|
|
|
currentLease4: nil,
|
|
currentLease6: nil,
|
|
|
|
lease4Mu: sync.Mutex{},
|
|
lease6Mu: sync.Mutex{},
|
|
|
|
mu: sync.Mutex{},
|
|
cfgMu: sync.Mutex{},
|
|
|
|
timer4: timer4,
|
|
timer6: timer6,
|
|
}, nil
|
|
}
|
|
|
|
func resetTimer(t *time.Timer, l *zerolog.Logger) {
|
|
l.Debug().Dur("delay", defaultTimerDuration).Msg("will retry later")
|
|
t.Reset(defaultTimerDuration)
|
|
}
|
|
|
|
func getRenewalTime(lease *Lease) time.Duration {
|
|
if lease.RenewalTime <= 0 || lease.LeaseTime > maxRenewalAttemptDuration/2 {
|
|
return maxRenewalAttemptDuration
|
|
}
|
|
|
|
return lease.RenewalTime
|
|
}
|
|
|
|
func (c *Client) requestLoop(t *time.Timer, family int, ifname string) {
|
|
l := c.l.With().Str("interface", ifname).Int("family", family).Logger()
|
|
for range t.C {
|
|
l.Info().Msg("requesting lease")
|
|
|
|
if _, err := c.ensureInterfaceUp(ifname); err != nil {
|
|
l.Error().Err(err).Msg("failed to ensure interface up")
|
|
resetTimer(t, c.l)
|
|
continue
|
|
}
|
|
|
|
var (
|
|
lease *Lease
|
|
err error
|
|
)
|
|
switch family {
|
|
case link.AfInet:
|
|
lease, err = c.requestLease4(ifname)
|
|
case link.AfInet6:
|
|
lease, err = c.requestLease6(ifname)
|
|
}
|
|
if err != nil {
|
|
l.Error().Err(err).Msg("failed to request lease")
|
|
resetTimer(t, c.l)
|
|
continue
|
|
}
|
|
|
|
c.handleLeaseChange(lease)
|
|
|
|
nextRenewal := getRenewalTime(lease)
|
|
|
|
l.Info().
|
|
Dur("nextRenewal", nextRenewal).
|
|
Dur("leaseTime", lease.LeaseTime).
|
|
Dur("rebindingTime", lease.RebindingTime).
|
|
Msg("sleeping until next renewal")
|
|
|
|
t.Reset(nextRenewal)
|
|
}
|
|
}
|
|
|
|
func (c *Client) ensureInterfaceUp(ifname string) (*link.Link, error) {
|
|
nlm := link.GetNetlinkManager()
|
|
iface, err := nlm.GetLinkByName(ifname)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return nlm.EnsureInterfaceUpWithTimeout(c.ctx, iface, c.cfg.LinkUpTimeout)
|
|
}
|
|
|
|
// Lease4 returns the current IPv4 lease
|
|
func (c *Client) Lease4() *types.DHCPLease {
|
|
c.lease4Mu.Lock()
|
|
defer c.lease4Mu.Unlock()
|
|
|
|
if c.currentLease4 == nil {
|
|
return nil
|
|
}
|
|
|
|
return c.currentLease4.ToDHCPLease()
|
|
}
|
|
|
|
// Lease6 returns the current IPv6 lease
|
|
func (c *Client) Lease6() *types.DHCPLease {
|
|
c.lease6Mu.Lock()
|
|
defer c.lease6Mu.Unlock()
|
|
|
|
if c.currentLease6 == nil {
|
|
return nil
|
|
}
|
|
|
|
return c.currentLease6.ToDHCPLease()
|
|
}
|
|
|
|
// Domain returns the current domain
|
|
func (c *Client) Domain() string {
|
|
c.lease4Mu.Lock()
|
|
defer c.lease4Mu.Unlock()
|
|
|
|
if c.currentLease4 != nil {
|
|
return c.currentLease4.Domain
|
|
}
|
|
|
|
c.lease6Mu.Lock()
|
|
defer c.lease6Mu.Unlock()
|
|
|
|
if c.currentLease6 != nil {
|
|
return c.currentLease6.Domain
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
// handleLeaseChange handles lease changes
|
|
func (c *Client) handleLeaseChange(lease *Lease) {
|
|
// do not use defer here, because we need to unlock the mutex before returning
|
|
ipv4 := lease.p4 != nil
|
|
|
|
if ipv4 {
|
|
c.lease4Mu.Lock()
|
|
c.currentLease4 = lease
|
|
c.lease4Mu.Unlock()
|
|
} else {
|
|
c.lease6Mu.Lock()
|
|
c.currentLease6 = lease
|
|
c.lease6Mu.Unlock()
|
|
}
|
|
|
|
c.apply()
|
|
|
|
// TODO: handle lease expiration
|
|
if c.cfg.OnLease4Change != nil && ipv4 {
|
|
c.cfg.OnLease4Change(lease.ToDHCPLease())
|
|
}
|
|
|
|
if c.cfg.OnLease6Change != nil && !ipv4 {
|
|
c.cfg.OnLease6Change(lease.ToDHCPLease())
|
|
}
|
|
}
|
|
|
|
func (c *Client) Renew() error {
|
|
c.timer4.Reset(defaultTimerDuration)
|
|
c.timer6.Reset(defaultTimerDuration)
|
|
return nil
|
|
}
|
|
|
|
func (c *Client) Release() error {
|
|
// TODO: implement
|
|
return nil
|
|
}
|
|
|
|
func (c *Client) SetIPv4(ipv4 bool) {
|
|
c.cfgMu.Lock()
|
|
defer c.cfgMu.Unlock()
|
|
|
|
currentIPv4 := c.cfg.IPv4
|
|
c.cfg.IPv4 = ipv4
|
|
|
|
if currentIPv4 == ipv4 {
|
|
return
|
|
}
|
|
|
|
if !ipv4 {
|
|
c.lease4Mu.Lock()
|
|
c.currentLease4 = nil
|
|
c.lease4Mu.Unlock()
|
|
|
|
c.timer4.Stop()
|
|
}
|
|
|
|
c.timer4.Reset(defaultTimerDuration)
|
|
}
|
|
|
|
func (c *Client) SetIPv6(ipv6 bool) {
|
|
c.cfgMu.Lock()
|
|
defer c.cfgMu.Unlock()
|
|
|
|
currentIPv6 := c.cfg.IPv6
|
|
c.cfg.IPv6 = ipv6
|
|
|
|
if currentIPv6 == ipv6 {
|
|
return
|
|
}
|
|
|
|
if !ipv6 {
|
|
c.lease6Mu.Lock()
|
|
c.currentLease6 = nil
|
|
c.lease6Mu.Unlock()
|
|
|
|
c.timer6.Stop()
|
|
}
|
|
|
|
c.timer6.Reset(defaultTimerDuration)
|
|
}
|
|
|
|
func (c *Client) Start() error {
|
|
if err := c.killUdhcpc(); err != nil {
|
|
c.l.Warn().Err(err).Msg("failed to kill udhcpc processes, continuing anyway")
|
|
}
|
|
|
|
for _, iface := range c.ifaces {
|
|
if c.cfg.IPv4 {
|
|
go c.requestLoop(c.timer4, link.AfInet, iface)
|
|
}
|
|
if c.cfg.IPv6 {
|
|
go c.requestLoop(c.timer6, link.AfInet6, iface)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *Client) apply() {
|
|
var (
|
|
iface string
|
|
nameservers []net.IP
|
|
searchList []string
|
|
domain string
|
|
)
|
|
|
|
if c.currentLease4 != nil {
|
|
iface = c.currentLease4.InterfaceName
|
|
nameservers = c.currentLease4.DNS
|
|
searchList = c.currentLease4.SearchList
|
|
domain = c.currentLease4.Domain
|
|
}
|
|
|
|
if c.currentLease6 != nil {
|
|
iface = c.currentLease6.InterfaceName
|
|
nameservers = append(nameservers, c.currentLease6.DNS...)
|
|
searchList = append(searchList, c.currentLease6.SearchList...)
|
|
domain = c.currentLease6.Domain
|
|
}
|
|
|
|
// deduplicate searchList
|
|
searchList = slices.Compact(searchList)
|
|
|
|
if c.cfg.UpdateResolvConf == nil {
|
|
c.l.Warn().Msg("no UpdateResolvConf function set, skipping resolv.conf update")
|
|
return
|
|
}
|
|
|
|
c.l.Info().
|
|
Str("interface", iface).
|
|
Interface("nameservers", nameservers).
|
|
Interface("searchList", searchList).
|
|
Str("domain", domain).
|
|
Msg("updating resolv.conf")
|
|
|
|
// Convert net.IP to string slice
|
|
var nameserverStrings []string
|
|
for _, ns := range nameservers {
|
|
nameserverStrings = append(nameserverStrings, ns.String())
|
|
}
|
|
|
|
if err := c.cfg.UpdateResolvConf(nameserverStrings); err != nil {
|
|
c.l.Error().Err(err).Msg("failed to update resolv.conf")
|
|
}
|
|
}
|