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:
Git Sagar 2026-06-06 22:05:34 +05:30
parent 61237283f5
commit 6416159164
3 changed files with 105 additions and 0 deletions

View file

@ -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...")

View file

@ -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 {

View file

@ -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.