mirror of
https://github.com/go-gost/x.git
synced 2026-10-19 16:16:32 +00:00
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.
204 lines
6.0 KiB
Go
204 lines
6.0 KiB
Go
package tun
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/binary"
|
|
"errors"
|
|
"net"
|
|
"testing"
|
|
"time"
|
|
|
|
xlogger "github.com/go-gost/x/logger"
|
|
)
|
|
|
|
// a registration handshake frame, as sent by the other side of a tun link
|
|
func registrationFrame() []byte {
|
|
frame := make([]byte, 0, keepAliveHeaderLength+net.IPv6len)
|
|
frame = append(frame, magicHeader...)
|
|
frame = append(frame, []byte("0123456789abcdef")...) // passphrase
|
|
frame = append(frame, net.IPv4(10, 20, 0, 2).To16()...)
|
|
return frame
|
|
}
|
|
|
|
func bareEcho() []byte {
|
|
return append(append([]byte{}, magicHeader...), []byte("0123456789abcdef")...)
|
|
}
|
|
|
|
func realIPv4Packet() []byte {
|
|
pkt := make([]byte, 20)
|
|
pkt[0] = 0x45 // v4, IHL 5
|
|
binary.BigEndian.PutUint16(pkt[2:4], 20)
|
|
pkt[9] = 1 // ICMP
|
|
return pkt
|
|
}
|
|
|
|
// In a tun link the far side's registration handshake, and the keepalive
|
|
// echo, are protocol chatter: they must be swallowed by the client, never
|
|
// forwarded into the local device. Only real IP packets may pass.
|
|
func TestTransportClientDropsKeepaliveFrames(t *testing.T) {
|
|
device, kernelSide := net.Pipe()
|
|
ccA, ccB := net.Pipe()
|
|
|
|
h := newTestHandler(nil, nil)
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
defer device.Close()
|
|
defer ccA.Close()
|
|
|
|
// drain everything the device would receive
|
|
deviceGot := make(chan []byte, 1)
|
|
go func() {
|
|
var acc []byte
|
|
buf := make([]byte, 64)
|
|
kernelSide.SetReadDeadline(time.Now().Add(500 * time.Millisecond))
|
|
for {
|
|
n, err := kernelSide.Read(buf[:])
|
|
if n > 0 {
|
|
acc = append(acc, buf[:n]...)
|
|
}
|
|
if err != nil {
|
|
break
|
|
}
|
|
}
|
|
deviceGot <- acc
|
|
}()
|
|
|
|
errCh := make(chan error, 1)
|
|
go func() { errCh <- h.transportClient(ctx, device, ccA, false, xlogger.Nop()) }()
|
|
|
|
// Protocol frames must be swallowed...
|
|
if _, err := ccB.Write(registrationFrame()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := ccB.Write(bareEcho()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// ...and a real IP packet must still reach the device.
|
|
pkt := realIPv4Packet()
|
|
if _, err := ccB.Write(pkt); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ccB.Close()
|
|
|
|
got := <-deviceGot
|
|
if !bytes.Equal(got, pkt) {
|
|
t.Fatalf("device got %v bytes %x, want only the IP packet %x", len(got), got, pkt)
|
|
}
|
|
|
|
<-errCh // client loop exits on the closed link
|
|
}
|
|
|
|
// Without a keepalive period the client sets no read deadline at all, so a
|
|
// hub that never answers — not started, or silently refusing — leaves the
|
|
// spoke "running" forever on a link that will never deliver. The transport
|
|
// must give up waiting for the first inbound byte so the caller redials and
|
|
// re-registers; any inbound byte clears the one-shot.
|
|
func TestNoKeepaliveClientTimesOutWaitingForFirstInbound(t *testing.T) {
|
|
old := firstInboundTimeout
|
|
firstInboundTimeout = 200 * time.Millisecond
|
|
defer func() { firstInboundTimeout = old }()
|
|
|
|
device, kernelSide := net.Pipe()
|
|
defer device.Close()
|
|
defer kernelSide.Close()
|
|
ccA, ccB := net.Pipe()
|
|
defer ccB.Close() // the link stays open: silence, not closure, must end it
|
|
|
|
h := newTestHandler(nil, nil)
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
errCh := make(chan error, 1)
|
|
go func() { errCh <- h.transportClient(ctx, device, ccA, false, xlogger.Nop()) }()
|
|
|
|
select {
|
|
case err := <-errCh:
|
|
var nerr net.Error
|
|
if !errors.As(err, &nerr) || !nerr.Timeout() {
|
|
t.Fatalf("transportClient = %v, want a read timeout", err)
|
|
}
|
|
case <-time.After(15 * time.Second):
|
|
t.Fatal("transportClient waited forever on a silent link")
|
|
}
|
|
// Note: transportClient itself takes ~5s more to return — collectFirstError
|
|
// waits out the device reader, which blocks on an idle device with no
|
|
// context-aware read. That wait is pre-existing; the timeout above is what
|
|
// proves the silent link no longer waits forever.
|
|
}
|
|
|
|
// The mirror: one inbound byte — even protocol chatter, which never reaches
|
|
// the device — proves the link alive and clears the one-shot, so a healthy
|
|
// but quiet tunnel is never redialed. This is the socket arm; a p2p dial
|
|
// re-arms the bound after the first byte instead of clearing it, which
|
|
// TestP2PDialRedialsWhenEchoesStop covers.
|
|
func TestNoKeepaliveClientFirstInboundClearsTimeout(t *testing.T) {
|
|
old := firstInboundTimeout
|
|
firstInboundTimeout = 200 * time.Millisecond
|
|
defer func() { firstInboundTimeout = old }()
|
|
|
|
device, kernelSide := net.Pipe()
|
|
defer device.Close()
|
|
defer kernelSide.Close()
|
|
ccA, ccB := net.Pipe()
|
|
|
|
h := newTestHandler(nil, nil)
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
errCh := make(chan error, 1)
|
|
go func() { errCh <- h.transportClient(ctx, device, ccA, false, xlogger.Nop()) }()
|
|
|
|
if _, err := ccB.Write(bareEcho()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Three windows of silence past the one-shot: a cleared deadline stays
|
|
// cleared, and the transport must not report anything.
|
|
time.Sleep(3 * firstInboundTimeout)
|
|
select {
|
|
case err := <-errCh:
|
|
t.Fatalf("transportClient reported %v on a healthy quiet link", err)
|
|
default:
|
|
}
|
|
|
|
ccB.Close() // unblock the reader; the test proved what it had to
|
|
}
|
|
|
|
// The p2p arm of the bound: after the first inbound byte the rolling
|
|
// deadline is armed, and silence past p2pInboundTimeout must report a read
|
|
// timeout — which the dial loop turns into a redial.
|
|
func TestP2PDialTimesOutAfterSilenceOnceLinkProven(t *testing.T) {
|
|
old := p2pInboundTimeout
|
|
p2pInboundTimeout = 100 * time.Millisecond
|
|
defer func() { p2pInboundTimeout = old }()
|
|
|
|
device, kernelSide := net.Pipe()
|
|
defer device.Close()
|
|
defer kernelSide.Close()
|
|
ccA, ccB := net.Pipe()
|
|
defer ccA.Close()
|
|
defer ccB.Close()
|
|
|
|
h := newTestHandler(nil, nil)
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
errCh := make(chan error, 1)
|
|
go func() { errCh <- h.transportClient(ctx, device, ccA, true, xlogger.Nop()) }()
|
|
|
|
if _, err := ccB.Write(bareEcho()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
select {
|
|
case err := <-errCh:
|
|
var nerr net.Error
|
|
if !errors.As(err, &nerr) || !nerr.Timeout() {
|
|
t.Fatalf("transportClient = %v, want a read timeout", err)
|
|
}
|
|
case <-time.After(15 * time.Second):
|
|
t.Fatal("a p2p dial survived silence after the link proved itself")
|
|
}
|
|
}
|