add DHCP lease renewal at T/2
- Add Renew() to dhcp.Client: sends REQUEST with ciaddr (RENEWING state) - Start renewal goroutine in session at lease_time/2 - On IP change: flush TAP, reconfigure address/routes/DNS/policy routes - On renewal failure: retry at T/4 (min 60s) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
61237283f5
commit
6416159164
3 changed files with 105 additions and 0 deletions
|
|
@ -61,6 +61,13 @@ func runSession(cfg client.Config, dev *tap.Device, mac net.HardwareAddr, opts n
|
||||||
if opts.PolicyRouteTable > 0 && lease.Gateway != nil {
|
if opts.PolicyRouteTable > 0 && lease.Gateway != nil {
|
||||||
defer netcfg.ConfigurePolicyRoute(dev.Name, lease, opts.PolicyRouteTable)()
|
defer netcfg.ConfigurePolicyRoute(dev.Name, lease, opts.PolicyRouteTable)()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Start DHCP renewal at T/2
|
||||||
|
if lease.LeaseTime > 0 {
|
||||||
|
stopRenew := make(chan struct{})
|
||||||
|
defer close(stopRenew)
|
||||||
|
go renewLoop(dhcpClient, tunnel, dev.Name, lease, opts, stopRenew)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
select {
|
select {
|
||||||
|
|
@ -71,6 +78,50 @@ func runSession(cfg client.Config, dev *tap.Device, mac net.HardwareAddr, opts n
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func renewLoop(dc *dhcp.Client, tunnel *client.Tunnel, ifname string, lease *dhcp.Lease, opts netcfg.Options, stop chan struct{}) {
|
||||||
|
currentIP := lease.ClientIP
|
||||||
|
renewAt := lease.LeaseTime / 2
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-stop:
|
||||||
|
return
|
||||||
|
case <-time.After(renewAt):
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("dhcp: renewing lease for %s...", currentIP)
|
||||||
|
newLease, err := dc.Renew(currentIP, func(frame []byte) error {
|
||||||
|
return tunnel.WriteFrames([][]byte{frame})
|
||||||
|
}, 10*time.Second)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("dhcp: renewal failed: %v", err)
|
||||||
|
// Try again at T/4 of remaining time, or 60s minimum
|
||||||
|
renewAt = lease.LeaseTime / 4
|
||||||
|
if renewAt < 60*time.Second {
|
||||||
|
renewAt = 60 * time.Second
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
ones, _ := newLease.SubnetMask.Size()
|
||||||
|
log.Printf("dhcp: renewed lease: ip=%s/%d gw=%s ttl=%v",
|
||||||
|
newLease.ClientIP, ones, newLease.Gateway, newLease.LeaseTime)
|
||||||
|
|
||||||
|
if !newLease.ClientIP.Equal(currentIP) {
|
||||||
|
log.Printf("dhcp: IP changed %s → %s, reconfiguring", currentIP, newLease.ClientIP)
|
||||||
|
netcfg.ReconfigureTAP(ifname, newLease, opts.AcceptDefaultGW, opts.AcceptStaticRoutes, opts.AcceptDNS)
|
||||||
|
if opts.PolicyRouteTable > 0 && newLease.Gateway != nil {
|
||||||
|
netcfg.ConfigurePolicyRoute(ifname, newLease, opts.PolicyRouteTable)
|
||||||
|
}
|
||||||
|
currentIP = newLease.ClientIP
|
||||||
|
}
|
||||||
|
|
||||||
|
if newLease.LeaseTime > 0 {
|
||||||
|
renewAt = newLease.LeaseTime / 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func runDHCP(dc *dhcp.Client, tunnel *client.Tunnel) (*dhcp.Lease, error) {
|
func runDHCP(dc *dhcp.Client, tunnel *client.Tunnel) (*dhcp.Lease, error) {
|
||||||
log.Println("starting DHCP exchange...")
|
log.Println("starting DHCP exchange...")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -150,6 +150,49 @@ func (c *Client) Run(sendFrame func([]byte) error, timeout time.Duration) (*Leas
|
||||||
return lease, nil
|
return lease, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Renew sends a DHCP REQUEST to renew the current lease.
|
||||||
|
// In RENEWING state: ciaddr = current IP, no requested-IP or server-ID options.
|
||||||
|
// Returns the renewed lease on ACK, or error on NAK/timeout.
|
||||||
|
func (c *Client) Renew(currentIP net.IP, sendFrame func([]byte) error, timeout time.Duration) (*Lease, error) {
|
||||||
|
var ciaddr [4]byte
|
||||||
|
copy(ciaddr[:], currentIP.To4())
|
||||||
|
|
||||||
|
opts := []dhcpOption{
|
||||||
|
{optMessageType, []byte{dhcpRequest}},
|
||||||
|
{optParamRequest, []byte{optSubnetMask, optRouter, optDNS, optLeaseTime, optClasslessRoutes, optMSClasslessRoutes}},
|
||||||
|
}
|
||||||
|
frame := c.buildFrame(opts, ciaddr, nil)
|
||||||
|
if err := sendFrame(frame); err != nil {
|
||||||
|
return nil, fmt.Errorf("send renew: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ack, err := c.waitForType(dhcpAck, timeout)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("waiting for renew ack: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
lease := &Lease{
|
||||||
|
ClientIP: net.IP(ack.yiaddr[:]).To4(),
|
||||||
|
ServerIP: ack.getOptionIP(optServerID),
|
||||||
|
SubnetMask: net.IPMask(ack.getOptionRaw(optSubnetMask)),
|
||||||
|
Gateway: ack.getOptionIP(optRouter),
|
||||||
|
DNS: ack.getOptionIPs(optDNS),
|
||||||
|
}
|
||||||
|
if lt := ack.getOptionUint32(optLeaseTime); lt > 0 {
|
||||||
|
lease.LeaseTime = time.Duration(lt) * time.Second
|
||||||
|
}
|
||||||
|
if lease.SubnetMask == nil {
|
||||||
|
lease.SubnetMask = net.CIDRMask(24, 32)
|
||||||
|
}
|
||||||
|
if routes := parseClasslessRoutes(ack.getOptionRaw(optClasslessRoutes)); len(routes) > 0 {
|
||||||
|
lease.Routes = routes
|
||||||
|
} else if routes := parseClasslessRoutes(ack.getOptionRaw(optMSClasslessRoutes)); len(routes) > 0 {
|
||||||
|
lease.Routes = routes
|
||||||
|
}
|
||||||
|
|
||||||
|
return lease, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Client) waitForType(msgType byte, timeout time.Duration) (*dhcpMsg, error) {
|
func (c *Client) waitForType(msgType byte, timeout time.Duration) (*dhcpMsg, error) {
|
||||||
deadline := time.After(timeout)
|
deadline := time.After(timeout)
|
||||||
for {
|
for {
|
||||||
|
|
|
||||||
|
|
@ -80,6 +80,17 @@ func ConfigureTAP(ifname string, lease *dhcp.Lease, acceptDefaultGW, acceptStati
|
||||||
return cleanup, nil
|
return cleanup, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ReconfigureTAP flushes the current TAP config and applies a new lease.
|
||||||
|
// Used when DHCP renewal returns a different IP address.
|
||||||
|
func ReconfigureTAP(ifname string, lease *dhcp.Lease, acceptDefaultGW, acceptStaticRoutes, acceptDNS bool) {
|
||||||
|
log.Printf("tap %s: reconfiguring for new IP", ifname)
|
||||||
|
run("ip", "addr", "flush", "dev", ifname)
|
||||||
|
// Ignore errors — best effort reconfiguration
|
||||||
|
if _, err := ConfigureTAP(ifname, lease, acceptDefaultGW, acceptStaticRoutes, acceptDNS); err != nil {
|
||||||
|
log.Printf("warning: reconfigure tap: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ConfigurePolicyRoute sets up policy routing so packets from the VPN IP are routed
|
// ConfigurePolicyRoute sets up policy routing so packets from the VPN IP are routed
|
||||||
// back through the VPN gateway. Needed when the VPN server forwards ports to the
|
// back through the VPN gateway. Needed when the VPN server forwards ports to the
|
||||||
// client — without it, reply packets use the default route instead of the VPN tunnel.
|
// client — without it, reply packets use the default route instead of the VPN tunnel.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue