diff --git a/cmd/softether-go/session.go b/cmd/softether-go/session.go index 5cf3d18..8cf8f41 100644 --- a/cmd/softether-go/session.go +++ b/cmd/softether-go/session.go @@ -61,6 +61,13 @@ func runSession(cfg client.Config, dev *tap.Device, mac net.HardwareAddr, opts n if opts.PolicyRouteTable > 0 && lease.Gateway != nil { 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 { @@ -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) { log.Println("starting DHCP exchange...") diff --git a/pkg/dhcp/dhcp.go b/pkg/dhcp/dhcp.go index 7eb8064..a5be160 100644 --- a/pkg/dhcp/dhcp.go +++ b/pkg/dhcp/dhcp.go @@ -150,6 +150,49 @@ func (c *Client) Run(sendFrame func([]byte) error, timeout time.Duration) (*Leas 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) { deadline := time.After(timeout) for { diff --git a/pkg/netcfg/netcfg.go b/pkg/netcfg/netcfg.go index df5bfcd..2c46837 100644 --- a/pkg/netcfg/netcfg.go +++ b/pkg/netcfg/netcfg.go @@ -80,6 +80,17 @@ func ConfigureTAP(ifname string, lease *dhcp.Lease, acceptDefaultGW, acceptStati 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 // 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.