Files
gost-x/handler/tun/handler_test.go
ginuerzh c962805818 fix(handler/tun): end a p2p dial when the echoes stop
A p2p dial's stream survives the peer session under it being rebuilt,
so the link can go silently dead: the far side re-creates its session
while this side's stream becomes a zombie — writes succeed into the
void and nothing reports an error. firstInboundTimeout was a one-shot,
cleared by the first inbound byte and never re-armed, so such a dial
stayed "running" forever: on 2026-10-17 an Android spoke sat in a
two-way blackhole for 17 minutes after the hub killed and rebuilt its
relay session alone (derp peer-gone kill), while both ends reported
the path up; only a manual entrypoint restart healed it.

The registration refresh is echoed by the hub while the route to this
device is real, so silence is measurable. A p2p dial now re-arms the
read deadline to p2pInboundTimeout on every inbound byte; when it
expires the dial ends and the caller redials, which re-registers and
heals a hub-side rebuild — the same recovery a manual restart performs
(observed as "peer relay session up … client=true"). Socket dials
keep the one-shot semantics: their transport reports the far side gone
by itself.
2026-10-17 11:09:25 +00:00

121 lines
3.2 KiB
Go

package tun
import (
"context"
"errors"
"net"
"testing"
"github.com/go-gost/core/chain"
"github.com/go-gost/core/handler"
"github.com/go-gost/core/hop"
xlogger "github.com/go-gost/x/logger"
)
// fakeRouter only needs Options: the mode decision reads Options().Chain and
// never dials.
type fakeRouter struct {
options *chain.RouterOptions
// dialConn, when set, is what Dial hands back — the test holds the far
// end of it, which is how a dial is driven without a network.
dialConn net.Conn
// dialFunc, when set, takes precedence over dialConn: it lets a test
// hand out a fresh link per dial, which one static conn cannot — the
// shape a redial loop needs.
dialFunc func(ctx context.Context, network, address string) (net.Conn, error)
}
func (r *fakeRouter) Options() *chain.RouterOptions { return r.options }
func (r *fakeRouter) Dial(ctx context.Context, network, address string, opts ...chain.DialOption) (net.Conn, error) {
if r.dialFunc != nil {
return r.dialFunc(ctx, network, address)
}
if r.dialConn != nil {
return r.dialConn, nil
}
return nil, errors.New("unexpected dial")
}
func (r *fakeRouter) Bind(ctx context.Context, network, address string, opts ...chain.BindOption) (net.Listener, error) {
return nil, errors.New("unexpected bind")
}
type fakeChainer struct{}
func (fakeChainer) Route(ctx context.Context, network, address string, opts ...chain.RouteOption) chain.Route {
return nil
}
type fakeHop struct{ node *chain.Node }
func (h fakeHop) Select(ctx context.Context, opts ...hop.SelectOption) *chain.Node { return h.node }
func newTestHandler(hp hop.Hop, router chain.Router) *tunHandler {
opts := []handler.Option{handler.LoggerOption(xlogger.Nop())}
if router != nil {
opts = append(opts, handler.RouterOption(router))
}
h := NewHandler(opts...).(*tunHandler)
if hp != nil {
h.Forward(hp)
}
return h
}
func TestSelectTarget(t *testing.T) {
chained := &fakeRouter{options: &chain.RouterOptions{Chain: fakeChainer{}}}
plain := &fakeRouter{options: &chain.RouterOptions{}}
tests := []struct {
name string
hop hop.Hop
router chain.Router
wantClient bool
wantAddr string
}{
{
name: "forwarder node",
hop: fakeHop{node: chain.NewNode("n0", "1.2.3.4:8421")},
router: chained,
wantClient: true,
wantAddr: "1.2.3.4:8421",
},
{
name: "forwarder with no node falls back to server",
hop: fakeHop{},
router: chained,
wantClient: false,
},
{
name: "chain without forwarder is client mode with no address",
router: chained,
wantClient: true,
wantAddr: "",
},
{
name: "no forwarder, no chain is server mode",
router: plain,
wantClient: false,
},
{
name: "nil router is server mode",
router: nil,
wantClient: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
h := newTestHandler(tt.hop, tt.router)
addr, client := h.selectTarget(context.Background())
if client != tt.wantClient {
t.Fatalf("client mode = %v, want %v", client, tt.wantClient)
}
if addr != tt.wantAddr {
t.Fatalf("dial addr = %q, want %q", addr, tt.wantAddr)
}
})
}
}