Files
gost-x/handler/tun/server.go
T
ginuerzh 06197bd987 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.
2026-09-13 14:50:29 +08:00

322 lines
7.8 KiB
Go

package tun
import (
"bytes"
"context"
"errors"
"io"
"net"
"net/netip"
"time"
"github.com/go-gost/core/auth"
"github.com/go-gost/core/logger"
"github.com/go-gost/core/router"
xip "github.com/go-gost/x/internal/net/ip"
tun_util "github.com/go-gost/x/internal/util/tun"
"github.com/songgao/water/waterutil"
"golang.org/x/net/ipv4"
"golang.org/x/net/ipv6"
)
func (h *tunHandler) handleServer(ctx context.Context, conn net.Conn, config *tun_util.Config, log logger.Logger) error {
for {
err := func() error {
pc, err := net.ListenPacket(conn.LocalAddr().Network(), conn.LocalAddr().String())
if err != nil {
return err
}
defer pc.Close()
return h.transportServer(ctx, conn, pc, config, log)
}()
if errors.Is(err, ErrTun) {
return err
}
log.Error(err)
time.Sleep(time.Second)
}
}
func (h *tunHandler) transportServer(ctx context.Context, tun io.ReadWriter, conn net.PacketConn, config *tun_util.Config, log logger.Logger) error {
c, cancel := context.WithCancel(ctx)
defer cancel()
errc := make(chan error, 2)
go func() {
var b [MaxMessageSize]byte
for {
select {
case <-c.Done():
errc <- c.Err()
return
default:
}
err := func() error {
n, err := tun.Read(b[:])
if err != nil {
return ErrTun
}
if n == 0 {
return nil
}
var src, dst net.IP
if waterutil.IsIPv4(b[:n]) {
header, err := ipv4.ParseHeader(b[:n])
if err != nil {
log.Warnf("parse ipv4 packet header: %v", err)
return nil
}
src, dst = header.Src, header.Dst
if log.IsLevelEnabled(logger.TraceLevel) {
log.Tracef("%s >> %s %-4s %d/%-4d %-4x %d",
src, dst, xip.Protocol(waterutil.IPv4Protocol(b[:n])),
header.Len, header.TotalLen, header.ID, header.Flags)
}
} else if waterutil.IsIPv6(b[:n]) {
header, err := ipv6.ParseHeader(b[:n])
if err != nil {
log.Warnf("parse ipv6 packet header: %v", err)
return nil
}
src, dst = header.Src, header.Dst
if log.IsLevelEnabled(logger.TraceLevel) {
log.Tracef("%s >> %s %s %d %d",
src, dst,
xip.Protocol(waterutil.IPProtocol(header.NextHeader)),
header.PayloadLen, header.TrafficClass)
}
} else {
log.Warnf("unknown packet, discarded(%d)", n)
return nil
}
addr := h.findRouteFor(ctx, dst, config.Router, log)
if addr == nil {
log.Debugf("no route for %s -> %s, packet discarded", src, dst)
return nil
}
log.Debugf("find route: %s -> %s", dst, addr)
if _, err := conn.WriteTo(b[:n], addr); err != nil {
return err
}
return nil
}()
if err != nil {
errc <- err
return
}
}
}()
go func() {
var b [MaxMessageSize]byte
for {
select {
case <-c.Done():
errc <- c.Err()
return
default:
}
err := func() error {
n, addr, err := conn.ReadFrom(b[:])
if err != nil {
return err
}
if n == 0 {
return nil
}
if n > keepAliveHeaderLength && bytes.Equal(b[:4], magicHeader) {
var peerIPs []net.IP
data := b[keepAliveHeaderLength:n]
if len(data)%net.IPv6len == 0 {
for len(data) > 0 {
peerIPs = append(peerIPs, net.IP(data[:net.IPv6len]))
data = data[net.IPv6len:]
}
}
if len(peerIPs) == 0 {
return nil
}
for _, net := range config.Net {
for _, ip := range peerIPs {
if ip.Equal(net.IP.To16()) {
return nil
}
}
}
if auther := h.options.Auther; auther != nil {
ok := true
key := bytes.TrimRight(b[4:20], "\x00")
for _, ip := range peerIPs {
if _, ok = auther.Authenticate(ctx, ip.String(), string(key), auth.WithService(h.options.Service)); !ok {
break
}
}
if !ok {
log.Debugf("keepalive from %v => %v, auth FAILED", addr, peerIPs)
return nil
}
}
log.Debugf("keepalive from %v => %v", addr, peerIPs)
addrPort, err := netip.ParseAddrPort(addr.String())
if err != nil {
log.Warnf("keepalive from %v: %v", addr, err)
return nil
}
var keepAliveData [keepAliveHeaderLength]byte
copy(keepAliveData[:4], magicHeader) // magic header
a16 := addrPort.Addr().As16()
copy(keepAliveData[4:], a16[:])
if _, err := conn.WriteTo(keepAliveData[:], addr); err != nil {
log.Warnf("keepalive to %v: %v", addr, err)
return nil
}
for _, ip := range peerIPs {
h.updateRoute(ip, addr, log)
}
return nil
}
var src, dst net.IP
if waterutil.IsIPv4(b[:n]) {
header, err := ipv4.ParseHeader(b[:n])
if err != nil {
log.Warnf("parse ipv4 packet header: %v", err)
return nil
}
src, dst = header.Src, header.Dst
if log.IsLevelEnabled(logger.TraceLevel) {
log.Tracef("%s >> %s %-4s %d/%-4d %-4x %d",
src, dst, xip.Protocol(waterutil.IPv4Protocol(b[:n])),
header.Len, header.TotalLen, header.ID, header.Flags)
}
} else if waterutil.IsIPv6(b[:n]) {
header, err := ipv6.ParseHeader(b[:n])
if err != nil {
log.Warnf("parse ipv6 packet header: %v", err)
return nil
}
src, dst = header.Src, header.Dst
if log.IsLevelEnabled(logger.TraceLevel) {
log.Tracef("%s > %s %s %d %d",
src, dst,
xip.Protocol(waterutil.IPProtocol(header.NextHeader)),
header.PayloadLen, header.TrafficClass)
}
} else {
log.Warnf("unknown packet, discarded(%d): % x", n, b[:n])
return nil
}
if !h.md.p2p {
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)
return err
}
}
if _, err := tun.Write(b[:n]); err != nil {
return ErrTun
}
return nil
}()
if err != nil {
errc <- err
return
}
}
}()
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)
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)
}
}
// 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 addr, ok := h.liveRoute(ipToTunRouteKey(dst), log); ok {
return addr
}
if router == nil {
return nil
}
if route := router.GetRoute(ctx, dst.String()); route != nil {
if gw := net.ParseIP(route.Gateway); gw != nil {
if addr, ok := h.liveRoute(ipToTunRouteKey(gw), log); ok {
return addr
}
}
}
return nil
}