fix(handler/tun): run keepalive on p2p links and expire stale routes

Three fixes found while wiring the p2p hub topology:

- The client keepalive was gated on network == "udp", but on a p2p link
  raddr is the peer's base64 key (no port), so SplitHostPort fails and
  network is "ip": the keepalive never ran, a tun server never learned the
  client's route, and the read deadline was never armed. Gate on the udp
  network OR a configured keepalive period; the udp branch keeps the one-shot
  registration handshake it always sent.
- Liveness was refreshed only by the keepalive echo; a transient channel
  hiccup can drop that single packet and redial a healthy tunnel after
  3xperiod. Refresh the read deadline after any successful read instead.
- Routes were never removed: a departed client's IP -> addr stayed forever
  and the device kept blackholing to a dead address. Expire a route once its
  last keepalive is older than 3x the configured period, checked lazily on
  the write path (no sweeper). The TTL is opt-in: without a server-side
  keepalive/ttl, routes keep their current never-expiring behavior.
This commit is contained in:
ginuerzh
2026-09-13 14:50:29 +08:00
parent a5605b935c
commit 06197bd987
2 changed files with 54 additions and 19 deletions
+14 -7
View File
@@ -44,7 +44,11 @@ func (h *tunHandler) handleClient(ctx context.Context, conn net.Conn, network st
}
defer cc.Close()
if network == "udp" {
// A datagram link needs the keepalive. On the udp network it is the
// registration handshake (and runs even without keepalive:true, so
// existing configs keep their one-shot registration); on a p2p link
// ("ip") it is what registers the route with the peer's tun server.
if network == "udp" || h.md.keepAlivePeriod > 0 {
iterCtx, iterCancel := context.WithCancel(ctx)
defer iterCancel()
@@ -178,13 +182,16 @@ func (h *tunHandler) transportClient(ctx context.Context, tun io.ReadWriter, con
return err
}
if n == keepAliveHeaderLength && bytes.Equal(b[:4], magicHeader) {
ip := net.IP(b[4:20])
log.Debugf("keepalive received at %v", ip)
// Any inbound byte proves the tunnel is alive: refresh the liveness
// deadline before interpreting the packet. Relying on the keepalive
// echo alone (which a transient channel hiccup can drop) would redial
// a healthy tunnel after 3×period.
if h.md.keepAlivePeriod > 0 {
conn.SetReadDeadline(time.Now().Add(h.md.keepAlivePeriod * 3))
}
if h.md.keepAlivePeriod > 0 {
conn.SetReadDeadline(time.Now().Add(h.md.keepAlivePeriod * 3))
}
if n == keepAliveHeaderLength && bytes.Equal(b[:4], magicHeader) {
log.Debugf("keepalive received at %v", net.IP(b[4:20]))
return nil
}
+40 -12
View File
@@ -97,7 +97,7 @@ func (h *tunHandler) transportServer(ctx context.Context, tun io.ReadWriter, con
return nil
}
addr := h.findRouteFor(ctx, dst, config.Router)
addr := h.findRouteFor(ctx, dst, config.Router, log)
if addr == nil {
log.Debugf("no route for %s -> %s, packet discarded", src, dst)
return nil
@@ -228,7 +228,7 @@ func (h *tunHandler) transportServer(ctx context.Context, tun io.ReadWriter, con
}
if !h.md.p2p {
if addr := h.findRouteFor(ctx, dst, config.Router); addr != nil {
if addr := h.findRouteFor(ctx, dst, config.Router, log); addr != nil {
log.Debugf("find route: %s -> %s", dst, addr)
_, err := conn.WriteTo(b[:n], addr)
@@ -252,30 +252,58 @@ func (h *tunHandler) transportServer(ctx context.Context, tun io.ReadWriter, con
return collectFirstError(errc, cancel)
}
// tunRoute is a registered route: the client's UDP address and when a keepalive
// last refreshed it, so an absent client's route can expire.
type tunRoute struct {
addr net.Addr
lastSeen time.Time
}
func (h *tunHandler) updateRoute(ip net.IP, addr net.Addr, log logger.Logger) {
if h.md.p2p {
ip = net.IPv6zero
}
rkey := ipToTunRouteKey(ip)
if actual, loaded := h.routes.LoadOrStore(rkey, addr); loaded {
if actual.(net.Addr).String() != addr.String() {
h.routes.Store(rkey, addr)
log.Debugf("update route: %s -> %s (old %s)",
ip, addr, actual.(net.Addr))
now := time.Now()
if actual, loaded := h.routes.LoadOrStore(rkey, tunRoute{addr: addr, lastSeen: now}); loaded {
old := actual.(tunRoute)
h.routes.Store(rkey, tunRoute{addr: addr, lastSeen: now}) // refresh lastSeen
if old.addr.String() != addr.String() {
log.Debugf("update route: %s -> %s (old %s)", ip, addr, old.addr)
}
} else {
log.Debugf("new route: %s -> %s", ip, addr)
}
}
func (h *tunHandler) findRouteFor(ctx context.Context, dst net.IP, router router.Router) net.Addr {
// liveRoute returns the route for rkey, lazily expiring it once the opt-in TTL
// has passed since its last keepalive. The TTL applies only when a keepalive
// period is configured (server-side keepalive/ttl), so an unconfigured
// deployment keeps today's never-expiring routes. No sweeper: every write path
// consults this, which is enough.
func (h *tunHandler) liveRoute(rkey tunRouteKey, log logger.Logger) (net.Addr, bool) {
v, ok := h.routes.Load(rkey)
if !ok {
return nil, false
}
r := v.(tunRoute)
if ttl := h.md.keepAlivePeriod * 3; ttl > 0 && time.Since(r.lastSeen) > ttl {
if h.routes.CompareAndDelete(rkey, r) {
log.Infof("route expired: %s -> %s", net.IP(rkey[:]), r.addr)
}
return nil, false
}
return r.addr, true
}
func (h *tunHandler) findRouteFor(ctx context.Context, dst net.IP, router router.Router, log logger.Logger) net.Addr {
if h.md.p2p {
dst = net.IPv6zero
router = nil
}
if v, ok := h.routes.Load(ipToTunRouteKey(dst)); ok {
return v.(net.Addr)
if addr, ok := h.liveRoute(ipToTunRouteKey(dst), log); ok {
return addr
}
if router == nil {
@@ -284,8 +312,8 @@ func (h *tunHandler) findRouteFor(ctx context.Context, dst net.IP, router router
if route := router.GetRoute(ctx, dst.String()); route != nil {
if gw := net.ParseIP(route.Gateway); gw != nil {
if v, ok := h.routes.Load(ipToTunRouteKey(gw)); ok {
return v.(net.Addr)
if addr, ok := h.liveRoute(ipToTunRouteKey(gw), log); ok {
return addr
}
}
}