Files
x/handler/tun/handler.go
T
ginuerzh 37bc81576c fix(tun): prevent leaked goroutines in bidirectional copy on transport failure
transportClient and transportServer each launch two goroutines for
bidirectional copy between the TUN device and the remote connection.
Previously, when one goroutine errored, the function returned
immediately while the sibling goroutine continued running — reading
from and writing to connections that would soon be closed by the
caller's deferred cleanup. In the retry loop of handleClient and
handleServer, stale goroutines accumulated across iterations, racing
with new goroutines for the TUN device and writing to closed pipes.

This caused the "io: read/write on closed pipe" error reported in
go-gost/gost#345, and more importantly, prevented recovery because
subsequent retry iterations operated on a corrupted state.

Fix:
- Derive a per-transport context in transportClient/transportServer
  and check it at the top of each goroutine's loop to allow the
  first-failing goroutine to signal its sibling to exit
- Replace the single-error select with a collectFirstError helper
  that drains both goroutines and includes a timeout guard (5s) to
  prevent deadlock when a goroutine is stuck in a non-cancelable read
- Use a per-iteration context in handleClient for the keepalive
  goroutine so it is properly cancelled when an iteration ends
- Fix handleServer to use errors.Is(err, ErrTun) for consistency
- Fix transportServer's errc buffer from 1 to 2 to prevent a
  goroutine leak when both goroutines error simultaneously
2026-06-20 22:10:09 +08:00

147 lines
3.5 KiB
Go

package tun
import (
"context"
"errors"
"fmt"
"io"
"net"
"sync"
"time"
"github.com/go-gost/core/chain"
"github.com/go-gost/core/handler"
"github.com/go-gost/core/hop"
md "github.com/go-gost/core/metadata"
xctx "github.com/go-gost/x/ctx"
ictx "github.com/go-gost/x/internal/ctx"
tun_util "github.com/go-gost/x/internal/util/tun"
"github.com/go-gost/x/registry"
)
var (
ErrTun = errors.New("tun device error")
ErrInvalidNet = errors.New("invalid net IP")
)
func init() {
registry.HandlerRegistry().Register("tun", NewHandler)
}
type tunHandler struct {
hop hop.Hop
routes sync.Map
md metadata
options handler.Options
}
func NewHandler(opts ...handler.Option) handler.Handler {
options := handler.Options{}
for _, opt := range opts {
opt(&options)
}
return &tunHandler{
options: options,
}
}
func (h *tunHandler) Init(md md.Metadata) (err error) {
if err = h.parseMetadata(md); err != nil {
return
}
return
}
// Forward implements handler.Forwarder.
func (h *tunHandler) Forward(hop hop.Hop) {
h.hop = hop
}
func (h *tunHandler) Handle(ctx context.Context, conn net.Conn, opts ...handler.HandleOption) error {
defer conn.Close()
log := h.options.Logger
var config *tun_util.Config
if md := ictx.MetadataFromContext(ctx); md != nil {
config, _ = md.Get("config").(*tun_util.Config)
}
if config == nil {
err := errors.New("tun: wrong connection type")
log.Error(err)
return err
}
start := time.Now()
log = log.WithFields(map[string]any{
"remote": conn.RemoteAddr().String(),
"local": conn.LocalAddr().String(),
"sid": xctx.SidFromContext(ctx).String(),
})
log.Infof("%s <> %s", conn.RemoteAddr(), conn.LocalAddr())
defer func() {
log.WithFields(map[string]any{
"duration": time.Since(start),
}).Infof("%s >< %s", conn.RemoteAddr(), conn.LocalAddr())
}()
var target *chain.Node
if h.hop != nil {
target = h.hop.Select(ctx)
}
if target != nil {
network := "udp"
if _, _, err := net.SplitHostPort(target.Addr); err != nil {
network = "ip"
}
log = log.WithFields(map[string]any{
"dst": fmt.Sprintf("%s/%s", target.Addr, network),
})
log.Debugf("%s >> %s", conn.RemoteAddr(), target.Addr)
if err := h.handleClient(ctx, conn, network, target.Addr, config, log); err != nil {
log.Error(err)
}
return nil
}
return h.handleServer(ctx, conn, config, log)
}
// collectFirstError drains errc (whose capacity must equal the number of
// goroutines writing to it) and returns the first error that is not io.EOF,
// context.Canceled, or context.DeadlineExceeded. It cancels the derived
// context after the first error to signal sibling goroutines to exit.
// A timeout on the second read prevents deadlock when a goroutine is stuck
// in a blocking read that is not context-aware.
func collectFirstError(errc <-chan error, cancel context.CancelFunc) error {
var firstErr error
// Wait for the first goroutine to exit.
err := <-errc
cancel() // signal the sibling goroutine to exit
if err != nil && err != io.EOF && err != context.Canceled && err != context.DeadlineExceeded {
firstErr = err
}
// Wait for the sibling goroutine, but don't block forever — the
// goroutine may be stuck in a blocking read that is not context-aware.
select {
case err := <-errc:
if err != nil && firstErr == nil && err != io.EOF && err != context.Canceled && err != context.DeadlineExceeded {
firstErr = err
}
case <-time.After(5 * time.Second):
}
return firstErr
}
type tunRouteKey [16]byte
func ipToTunRouteKey(ip net.IP) (key tunRouteKey) {
copy(key[:], ip.To16())
return
}