ZapretKVN-android/audit/xray_module_integration_test.go
loop-uh fedc3d3351
Some checks failed
Android CI / build (push) Has been cancelled
xray-мост: соединение переживает отмену dial-контекста — DNS через VLESS снова работает
sing-box 1.14 (пул DNS-соединений) отменяет dial-ctx сразу после dial(),
а Xray подключается к серверу асинхронно на этом же ctx. Итог на VLESS:
5 мгновенных «operation was canceled», затем «closed pipe» через ~2 с;
DNS через туннель падал/тормозил, YouTube не грузился.

Линк Xray теперь живёт на WithoutCancel(ctx) и закрывается вместе с
соединением или владельцем-outbound. Регрессионный тест
TestZapretXrayConnectionsOutliveDialContext (падал до фикса).
2026-09-28 17:07:39 +03:00

239 lines
6.8 KiB
Go

package xraycore_test
import (
"context"
"fmt"
"io"
"net"
"testing"
"time"
box "github.com/sagernet/sing-box"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/include"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common/json"
M "github.com/sagernet/sing/common/metadata"
)
func startBox(t *testing.T, text string) *box.Box {
t.Helper()
ctx := include.Context(context.Background())
var options option.Options
if err := json.UnmarshalContext(ctx, []byte(text), &options); err != nil {
t.Fatal(err)
}
instance, err := box.New(box.Options{Options: options, Context: ctx})
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = instance.Close() })
if err = instance.Start(); err != nil {
t.Fatal(err)
}
return instance
}
// startEchoFixture starts TCP/UDP echo servers and a VLESS server in front of
// them; it returns both echo endpoints and a client config whose "proxy"
// outbound is owned by the Xray module.
func startEchoFixture(t *testing.T) (net.Listener, net.PacketConn, string) {
t.Helper()
listener, err := net.Listen("tcp4", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = listener.Close() })
go func() {
for {
conn, err := listener.Accept()
if err != nil {
return
}
go func() { defer conn.Close(); _, _ = io.Copy(conn, conn) }()
}
}()
udp, err := net.ListenPacket("udp4", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = udp.Close() })
go func() {
buffer := make([]byte, 2048)
for {
n, peer, err := udp.ReadFrom(buffer)
if err != nil {
return
}
_, _ = udp.WriteTo(buffer[:n], peer)
}
}()
reserve, err := net.Listen("tcp4", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
port := reserve.Addr().(*net.TCPAddr).Port
reserve.Close()
const uuid = "11111111-1111-1111-1111-111111111111"
startBox(t, fmt.Sprintf(`{
"log":{"disabled":true},
"inbounds":[{"type":"vless","listen":"127.0.0.1","listen_port":%d,
"users":[{"uuid":"%s"}]}],
"outbounds":[{"type":"direct","tag":"direct"}]
}`, port, uuid))
config := fmt.Sprintf(`{
"log":{"disabled":true},
"dns":{"servers":[{"type":"local","tag":"local"}]},
"outbounds":[{"type":"vless","tag":"proxy","server":"127.0.0.1","server_port":%d,
"uri":"vless://%s@127.0.0.1:%d?security=none&encryption=none&type=tcp"}]
}`, port, uuid, port)
return listener, udp, config
}
func proxyOutbound(t *testing.T, client *box.Box) adapter.Outbound {
t.Helper()
outbound, loaded := client.Outbound().Outbound("proxy")
if !loaded {
t.Fatal("outbound missing")
}
if fmt.Sprintf("%T", outbound) != "*xraycore.Outbound" {
t.Fatalf("wrong protocol owner: %T", outbound)
}
return outbound
}
// sing-box 1.14 DNS connection pools cancel the dial context as soon as dial()
// returns and keep the connection. Xray dispatches the upstream dial
// asynchronously, so the connection must not inherit the dial context's
// cancellation, or every pooled DNS exchange through the proxy fails.
func TestZapretXrayConnectionsOutliveDialContext(t *testing.T) {
listener, udp, config := startEchoFixture(t)
outbound := proxyOutbound(t, startBox(t, config))
echo := func(t *testing.T, conn net.Conn, payload string) {
t.Helper()
defer conn.Close()
_ = conn.SetDeadline(time.Now().Add(3 * time.Second))
if _, err := conn.Write([]byte(payload)); err != nil {
t.Fatal(err)
}
reply := make([]byte, len(payload))
if _, err := io.ReadFull(conn, reply); err != nil {
t.Fatal(err)
}
if string(reply) != payload {
t.Fatal("payload changed")
}
}
for _, network := range []string{"tcp", "udp"} {
t.Run(network, func(t *testing.T) {
target := listener.Addr().String()
if network == "udp" {
target = udp.LocalAddr().String()
}
dialCtx, cancel := context.WithCancel(context.Background())
conn, err := outbound.DialContext(dialCtx, network, M.ParseSocksaddr(target))
cancel()
if err != nil {
t.Fatal(err)
}
echo(t, conn, network+"-after-cancel")
})
}
t.Run("packet", func(t *testing.T) {
dialCtx, cancel := context.WithCancel(context.Background())
packet, err := outbound.ListenPacket(dialCtx, M.ParseSocksaddr(udp.LocalAddr().String()))
cancel()
if err != nil {
t.Fatal(err)
}
defer packet.Close()
_ = packet.SetDeadline(time.Now().Add(3 * time.Second))
if _, err = packet.WriteTo([]byte("packet-after-cancel"), udp.LocalAddr()); err != nil {
t.Fatal(err)
}
data := make([]byte, 64)
n, _, err := packet.ReadFrom(data)
if err != nil {
t.Fatal(err)
}
if string(data[:n]) != "packet-after-cancel" {
t.Fatal("payload changed")
}
})
}
func TestZapretXrayModuleCarriesTCPAndUDPAndIsolatesOwners(t *testing.T) {
listener, udp, config := startEchoFixture(t)
clients := []*box.Box{startBox(t, config), startBox(t, config)}
for index, client := range clients {
outbound := proxyOutbound(t, client)
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
conn, err := outbound.DialContext(ctx, "tcp", M.ParseSocksaddr(listener.Addr().String()))
if err != nil {
t.Fatal(err)
}
_ = conn.SetDeadline(time.Now().Add(3 * time.Second))
if _, err = conn.Write([]byte("tcp-echo")); err != nil {
t.Fatal(err)
}
reply := make([]byte, 8)
if _, err = io.ReadFull(conn, reply); err != nil {
t.Fatal(err)
}
conn.Close()
if string(reply) != "tcp-echo" {
t.Fatal("TCP payload changed")
}
packet, err := outbound.ListenPacket(ctx, M.ParseSocksaddr(udp.LocalAddr().String()))
if err != nil {
t.Fatal(err)
}
if _, err = packet.WriteTo([]byte("udp-echo"), udp.LocalAddr()); err != nil {
t.Fatal(err)
}
result := make(chan error, 1)
go func() {
data := make([]byte, 32)
n, peer, err := packet.ReadFrom(data)
if err == nil && (string(data[:n]) != "udp-echo" || peer.String() != udp.LocalAddr().String()) {
err = fmt.Errorf("UDP payload or peer changed")
}
result <- err
}()
select {
case err = <-result:
if err != nil {
t.Fatal(err)
}
case <-ctx.Done():
packet.Close()
t.Fatal("UDP response timed out")
}
packet.Close()
if index == 0 {
idlePacket, err := outbound.ListenPacket(ctx, M.ParseSocksaddr(udp.LocalAddr().String()))
if err != nil {
t.Fatal(err)
}
defer idlePacket.Close()
if err = client.Close(); err != nil {
t.Fatal(err)
}
closed := make(chan error, 1)
go func() { _, _, err := idlePacket.ReadFrom(make([]byte, 32)); closed <- err }()
select {
case err = <-closed:
if err == nil {
t.Fatal("idle packet connection survived owner close")
}
case <-time.After(time.Second):
t.Fatal("idle packet read blocked after owner close")
}
if _, err = outbound.DialContext(ctx, "tcp", M.ParseSocksaddr(listener.Addr().String())); err == nil {
t.Fatal("closed owner accepted new traffic")
}
}
}
}