From a49931ed548700ec9c3debc6bf9341f617fcd3d7 Mon Sep 17 00:00:00 2001 From: Nikita Nemirovsky Date: Tue, 31 Mar 2026 17:56:06 +0800 Subject: [PATCH 01/25] feat(xdns): add resolver-based DNS tunneling with multi-resolver fan-out Add optional `resolvers` config field to XDNS finalmask. When set, the client sends DNS queries through public DNS resolvers instead of connecting directly to the server on port 53. - One UDP socket per resolver with independent receive goroutines - Round-robin query distribution across resolvers - Backward compatible: omitting resolvers preserves direct mode - Fix server sendLoop starvation under mKCP retransmission flood - Drain excess query records to skip stale queries - Reduce server response delay from 1s to 50ms - Increase server write queue from 512 to 4096 --- infra/conf/transport_internet.go | 6 +- transport/internet/finalmask/xdns/client.go | 151 ++++++-- .../internet/finalmask/xdns/config.pb.go | 15 +- .../internet/finalmask/xdns/config.proto | 1 + transport/internet/finalmask/xdns/dns_test.go | 329 +++++++++++++++++- transport/internet/finalmask/xdns/server.go | 162 ++++++--- 6 files changed, 576 insertions(+), 88 deletions(-) diff --git a/infra/conf/transport_internet.go b/infra/conf/transport_internet.go index 849f0e38ee1d..e3b60d162c6d 100644 --- a/infra/conf/transport_internet.go +++ b/infra/conf/transport_internet.go @@ -1660,7 +1660,8 @@ func (c *Sudoku) Build() (proto.Message, error) { } type Xdns struct { - Domain string `json:"domain"` + Domain string `json:"domain"` + Resolvers []string `json:"resolvers,omitempty"` } func (c *Xdns) Build() (proto.Message, error) { @@ -1669,7 +1670,8 @@ func (c *Xdns) Build() (proto.Message, error) { } return &xdns.Config{ - Domain: c.Domain, + Domain: c.Domain, + Resolvers: c.Resolvers, }, nil } diff --git a/transport/internet/finalmask/xdns/client.go b/transport/internet/finalmask/xdns/client.go index d6867b0a7473..2dc8f8a46114 100644 --- a/transport/internet/finalmask/xdns/client.go +++ b/transport/internet/finalmask/xdns/client.go @@ -10,6 +10,7 @@ import ( "io" "net" "sync" + "sync/atomic" "time" "github.com/xtls/xray-core/common" @@ -28,6 +29,23 @@ const ( var base32Encoding = base32.StdEncoding.WithPadding(base32.NoPadding) +type resolverConn struct { + conn net.PacketConn + addr *net.UDPAddr +} + +func parseResolverAddr(s string) (*net.UDPAddr, error) { + host, port, err := net.SplitHostPort(s) + if err != nil { + host = s + port = "53" + } + if host == "" { + return nil, go_errors.New("empty resolver address") + } + return net.ResolveUDPAddr("udp", net.JoinHostPort(host, port)) +} + type packet struct { p []byte addr net.Addr @@ -39,12 +57,20 @@ type xdnsConnClient struct { clientID []byte domain Name + resolverConns []*resolverConn + resolverIdx atomic.Uint32 + serverAddr atomic.Value // stores net.Addr; set by WriteTo, used by recvLoopFrom in resolver mode + recvWg sync.WaitGroup + sendWg sync.WaitGroup + pollChan chan struct{} readQueue chan *packet writeQueue chan *packet - closed bool - mutex sync.Mutex + closed atomic.Bool + closeOnce sync.Once + closeErr error + mutex sync.Mutex } func NewConnClient(c *Config, raw net.PacketConn) (net.PacketConn, error) { @@ -66,21 +92,70 @@ func NewConnClient(c *Config, raw net.PacketConn) (net.PacketConn, error) { common.Must2(rand.Read(conn.clientID)) - go conn.recvLoop() - go conn.sendLoop() + if len(c.Resolvers) > 0 { + for _, rs := range c.Resolvers { + addr, err := parseResolverAddr(rs) + if err != nil { + for _, rc := range conn.resolverConns { + rc.conn.Close() + } + return nil, errors.New("invalid resolver address: ", rs, ": ", err) + } + uc, err := net.ListenPacket("udp", ":0") + if err != nil { + for _, rc := range conn.resolverConns { + rc.conn.Close() + } + return nil, errors.New("failed to create resolver socket: ", err) + } + conn.resolverConns = append(conn.resolverConns, &resolverConn{conn: uc, addr: addr}) + } + for _, rc := range conn.resolverConns { + conn.recvWg.Add(1) + go func(pconn net.PacketConn) { + defer conn.recvWg.Done() + conn.recvLoopFrom(pconn) + }(rc.conn) + } + } else { + conn.recvWg.Add(1) + go func() { + defer conn.recvWg.Done() + conn.recvLoop() + }() + } + conn.sendWg.Add(1) + go func() { + defer conn.sendWg.Done() + conn.sendLoop() + }() return conn, nil } func (c *xdnsConnClient) recvLoop() { + c.recvLoopFrom(c.PacketConn) + + errors.LogDebug(context.Background(), "xdns closed") + + close(c.pollChan) + close(c.readQueue) + + c.closed.Store(true) + c.mutex.Lock() + defer c.mutex.Unlock() + close(c.writeQueue) +} + +func (c *xdnsConnClient) recvLoopFrom(conn net.PacketConn) { var buf [finalmask.UDPSize]byte for { - if c.closed { + if c.closed.Load() { break } - n, addr, err := c.PacketConn.ReadFrom(buf[:]) + n, addr, err := conn.ReadFrom(buf[:]) if err != nil || n == 0 { if go_errors.Is(err, net.ErrClosed) || go_errors.Is(err, io.EOF) { break @@ -95,6 +170,16 @@ func (c *xdnsConnClient) recvLoop() { } payload := dnsResponsePayload(&resp, c.domain) + if payload == nil { + continue + } + + pktAddr := net.Addr(addr) + if len(c.resolverConns) > 0 { + if sa := c.serverAddr.Load(); sa != nil { + pktAddr = sa.(net.Addr) + } + } r := bytes.NewReader(payload) anyPacket := false @@ -110,7 +195,7 @@ func (c *xdnsConnClient) recvLoop() { select { case c.readQueue <- &packet{ p: buf, - addr: addr, + addr: pktAddr, }: default: errors.LogDebug(context.Background(), addr, " mask read err queue full") @@ -124,17 +209,6 @@ func (c *xdnsConnClient) recvLoop() { } } } - - errors.LogDebug(context.Background(), "xdns closed") - - close(c.pollChan) - close(c.readQueue) - - c.mutex.Lock() - defer c.mutex.Unlock() - - c.closed = true - close(c.writeQueue) } func (c *xdnsConnClient) sendLoop() { @@ -179,20 +253,30 @@ func (c *xdnsConnClient) sendLoop() { } } else { if !pollTimer.Stop() { - <-pollTimer.C + select { + case <-pollTimer.C: + default: + } } pollDelay = initPollDelay } pollTimer.Reset(pollDelay) - if c.closed { + if c.closed.Load() { return } if p != nil { - _, err := c.PacketConn.WriteTo(p.p, p.addr) + var err error + if len(c.resolverConns) > 0 { + idx := c.resolverIdx.Add(1) + rc := c.resolverConns[idx%uint32(len(c.resolverConns))] + _, err = rc.conn.WriteTo(p.p, rc.addr) + } else { + _, err = c.PacketConn.WriteTo(p.p, p.addr) + } if go_errors.Is(err, net.ErrClosed) || go_errors.Is(err, io.ErrClosedPipe) { - c.closed = true + c.closed.Store(true) break } } @@ -213,10 +297,12 @@ func (c *xdnsConnClient) ReadFrom(p []byte) (n int, addr net.Addr, err error) { } func (c *xdnsConnClient) WriteTo(p []byte, addr net.Addr) (n int, err error) { + c.serverAddr.Store(addr) + c.mutex.Lock() defer c.mutex.Unlock() - if c.closed { + if c.closed.Load() { return 0, io.ErrClosedPipe } @@ -239,8 +325,23 @@ func (c *xdnsConnClient) WriteTo(p []byte, addr net.Addr) (n int, err error) { } func (c *xdnsConnClient) Close() error { - c.closed = true - return c.PacketConn.Close() + c.closeOnce.Do(func() { + c.closed.Store(true) + for _, rc := range c.resolverConns { + rc.conn.Close() + } + c.closeErr = c.PacketConn.Close() + c.recvWg.Wait() + if len(c.resolverConns) > 0 { + close(c.pollChan) + close(c.readQueue) + c.mutex.Lock() + close(c.writeQueue) + c.mutex.Unlock() + } + c.sendWg.Wait() + }) + return c.closeErr } func encode(p []byte, clientID []byte, domain Name) ([]byte, error) { diff --git a/transport/internet/finalmask/xdns/config.pb.go b/transport/internet/finalmask/xdns/config.pb.go index 279240510137..55b01b45cbe9 100644 --- a/transport/internet/finalmask/xdns/config.pb.go +++ b/transport/internet/finalmask/xdns/config.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.11 -// protoc v6.33.5 +// protoc v5.28.3 // source: transport/internet/finalmask/xdns/config.proto package xdns @@ -24,6 +24,7 @@ const ( type Config struct { state protoimpl.MessageState `protogen:"open.v1"` Domain string `protobuf:"bytes,1,opt,name=domain,proto3" json:"domain,omitempty"` + Resolvers []string `protobuf:"bytes,2,rep,name=resolvers,proto3" json:"resolvers,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -65,13 +66,21 @@ func (x *Config) GetDomain() string { return "" } +func (x *Config) GetResolvers() []string { + if x != nil { + return x.Resolvers + } + return nil +} + var File_transport_internet_finalmask_xdns_config_proto protoreflect.FileDescriptor const file_transport_internet_finalmask_xdns_config_proto_rawDesc = "" + "\n" + - ".transport/internet/finalmask/xdns/config.proto\x12&xray.transport.internet.finalmask.xdns\" \n" + + ".transport/internet/finalmask/xdns/config.proto\x12&xray.transport.internet.finalmask.xdns\">\n" + "\x06Config\x12\x16\n" + - "\x06domain\x18\x01 \x01(\tR\x06domainB\x94\x01\n" + + "\x06domain\x18\x01 \x01(\tR\x06domain\x12\x1c\n" + + "\tresolvers\x18\x02 \x03(\tR\tresolversB\x94\x01\n" + "*com.xray.transport.internet.finalmask.xdnsP\x01Z;github.com/xtls/xray-core/transport/internet/finalmask/xdns\xaa\x02&Xray.Transport.Internet.Finalmask.Xdnsb\x06proto3" var ( diff --git a/transport/internet/finalmask/xdns/config.proto b/transport/internet/finalmask/xdns/config.proto index e1c717709dea..64f1e14e377d 100644 --- a/transport/internet/finalmask/xdns/config.proto +++ b/transport/internet/finalmask/xdns/config.proto @@ -8,5 +8,6 @@ option java_multiple_files = true; message Config { string domain = 1; + repeated string resolvers = 2; } diff --git a/transport/internet/finalmask/xdns/dns_test.go b/transport/internet/finalmask/xdns/dns_test.go index aa163476d9f1..5fc5a85b58c6 100644 --- a/transport/internet/finalmask/xdns/dns_test.go +++ b/transport/internet/finalmask/xdns/dns_test.go @@ -4,9 +4,12 @@ import ( "bytes" "fmt" "io" + "net" "strconv" "strings" + "sync/atomic" "testing" + "time" ) func namesEqual(a, b Name) bool { @@ -547,8 +550,8 @@ func TestEncodeRDataTXT(t *testing.T) { // zero, not an empty slice. p := make([]byte, 0) encoded := EncodeRDataTXT(p) - if len(encoded) < 0 { - t.Errorf("EncodeRDataTXT(%v) returned %v", p, encoded) + if len(encoded) < 1 { + t.Errorf("EncodeRDataTXT(%v) returned %v, want at least 1 byte", p, encoded) } // 255 bytes should be able to be encoded into 256 bytes. @@ -558,7 +561,10 @@ func TestEncodeRDataTXT(t *testing.T) { t.Errorf("EncodeRDataTXT(%d bytes) returned %d bytes", len(p), len(encoded)) } - fmt.Println(EncodeRDataTXT(nil)) + nilEncoded := EncodeRDataTXT(nil) + if len(nilEncoded) < 1 { + t.Errorf("EncodeRDataTXT(nil) returned %v, want at least 1 byte", nilEncoded) + } } func TestRDataTXTRoundTrip(t *testing.T) { @@ -592,3 +598,320 @@ func TestRDataTXTRoundTrip(t *testing.T) { } } } + +func TestParseResolverAddr(t *testing.T) { + tests := []struct { + name string + input string + wantIP string + wantPort int + wantErr bool + }{ + {"bare_ipv4", "1.1.1.1", "1.1.1.1", 53, false}, + {"ipv4_with_port", "8.8.8.8:53", "8.8.8.8", 53, false}, + {"ipv4_custom_port", "8.8.8.8:5353", "8.8.8.8", 5353, false}, + {"ipv6_with_port", "[2606:4700:4700::1111]:53", "2606:4700:4700::1111", 53, false}, + {"empty_string", "", "", 0, true}, + {"empty_host_with_port", ":53", "", 0, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + addr, err := parseResolverAddr(tt.input) + if tt.wantErr { + if err == nil { + t.Fatalf("parseResolverAddr(%q): want error, got nil", tt.input) + } + return + } + if err != nil { + t.Fatalf("parseResolverAddr(%q): %v", tt.input, err) + } + if addr.IP.String() != tt.wantIP { + t.Errorf("parseResolverAddr(%q).IP = %v, want %v", tt.input, addr.IP, tt.wantIP) + } + if addr.Port != tt.wantPort { + t.Errorf("parseResolverAddr(%q).Port = %v, want %v", tt.input, addr.Port, tt.wantPort) + } + }) + } +} + +func TestResolverModeRoundTrip(t *testing.T) { + // Auth server: raw UDP socket that the XDNS server wraps + authServer, err := net.ListenPacket("udp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + defer authServer.Close() + + // Mock resolver: bidirectional UDP forwarder between client and auth server + resolver, err := net.ListenPacket("udp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + defer resolver.Close() + + go func() { + buf := make([]byte, 4096) + authAddr := authServer.LocalAddr().String() + var clientAddr net.Addr + for { + n, addr, err := resolver.ReadFrom(buf) + if err != nil { + return + } + if addr.String() == authAddr { + // Response from auth server -> forward to client + if clientAddr != nil { + resolver.WriteTo(buf[:n], clientAddr) + } + } else { + // Query from client -> forward to auth server + clientAddr = addr + resolver.WriteTo(buf[:n], authServer.LocalAddr()) + } + } + }() + + // XDNS server wrapping the auth server socket + serverConfig := &Config{Domain: "t.example.com"} + server, err := NewConnServer(serverConfig, authServer) + if err != nil { + t.Fatal(err) + } + defer server.Close() + + // XDNS client configured to use the mock resolver + config := &Config{ + Domain: "t.example.com", + Resolvers: []string{resolver.LocalAddr().String()}, + } + rawConn, err := net.ListenPacket("udp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + defer rawConn.Close() + + client, err := NewConnClient(config, rawConn) + if err != nil { + t.Fatal(err) + } + defer client.Close() + + // Send test payload through client + testPayload := []byte("hello xdns resolver") + _, err = client.WriteTo(testPayload, rawConn.LocalAddr()) + if err != nil { + t.Fatal(err) + } + + // Read from server with timeout (server.ReadFrom blocks on channel, + // so SetReadDeadline won't work; use a goroutine instead) + done := make(chan struct{}) + var serverBuf [256]byte + var readN int + var readErr error + go func() { + defer close(done) + readN, _, readErr = server.ReadFrom(serverBuf[:]) + }() + + select { + case <-done: + case <-time.After(5 * time.Second): + t.Fatal("timeout waiting for server ReadFrom") + } + + if readErr != nil { + t.Fatalf("server ReadFrom: %v", readErr) + } + if !bytes.Equal(serverBuf[:readN], testPayload) { + t.Errorf("server received %q, want %q", serverBuf[:readN], testPayload) + } +} + +func TestMultiResolverDistribution(t *testing.T) { + const numResolvers = 3 + + // Create mock resolvers that count received packets + resolvers := make([]net.PacketConn, numResolvers) + var counts [numResolvers]atomic.Int32 + for i := range resolvers { + r, err := net.ListenPacket("udp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + defer r.Close() + resolvers[i] = r + idx := i + go func() { + buf := make([]byte, 4096) + for { + _, _, err := resolvers[idx].ReadFrom(buf) + if err != nil { + return + } + counts[idx].Add(1) + } + }() + } + + resolverAddrs := make([]string, numResolvers) + for i, r := range resolvers { + resolverAddrs[i] = r.LocalAddr().String() + } + + config := &Config{ + Domain: "t.example.com", + Resolvers: resolverAddrs, + } + rawConn, err := net.ListenPacket("udp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + defer rawConn.Close() + + client, err := NewConnClient(config, rawConn) + if err != nil { + t.Fatal(err) + } + defer client.Close() + + // Send enough messages to hit all resolvers via round-robin + for i := 0; i < numResolvers*3; i++ { + payload := []byte(fmt.Sprintf("msg-%d", i)) + _, err = client.WriteTo(payload, rawConn.LocalAddr()) + if err != nil { + t.Fatal(err) + } + } + + // Allow sendLoop to process the queue + time.Sleep(500 * time.Millisecond) + + for i := 0; i < numResolvers; i++ { + c := counts[i].Load() + if c == 0 { + t.Errorf("resolver %d received no queries, want at least 1", i) + } + } +} + + +func TestResolverModeServerToClient(t *testing.T) { + // Auth server: raw UDP socket that the XDNS server wraps + authServer, err := net.ListenPacket("udp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + defer authServer.Close() + + // Mock resolver: bidirectional UDP forwarder + resolver, err := net.ListenPacket("udp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + defer resolver.Close() + + go func() { + buf := make([]byte, 4096) + authAddr := authServer.LocalAddr().String() + var clientAddr net.Addr + for { + n, addr, err := resolver.ReadFrom(buf) + if err != nil { + return + } + if addr.String() == authAddr { + if clientAddr != nil { + resolver.WriteTo(buf[:n], clientAddr) + } + } else { + clientAddr = addr + resolver.WriteTo(buf[:n], authServer.LocalAddr()) + } + } + }() + + // XDNS server + serverConfig := &Config{Domain: "t.example.com"} + server, err := NewConnServer(serverConfig, authServer) + if err != nil { + t.Fatal(err) + } + defer server.Close() + + // XDNS client with resolver + config := &Config{ + Domain: "t.example.com", + Resolvers: []string{resolver.LocalAddr().String()}, + } + rawConn, err := net.ListenPacket("udp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + defer rawConn.Close() + + client, err := NewConnClient(config, rawConn) + if err != nil { + t.Fatal(err) + } + defer client.Close() + + // Client sends a query to trigger the connection and set serverAddr + _, err = client.WriteTo([]byte("init"), rawConn.LocalAddr()) + if err != nil { + t.Fatal(err) + } + + // Wait for server to receive the client query + serverBuf := make([]byte, 256) + done := make(chan struct{}) + var serverReadAddr net.Addr + go func() { + defer close(done) + _, serverReadAddr, _ = server.ReadFrom(serverBuf) + }() + select { + case <-done: + case <-time.After(5 * time.Second): + t.Fatal("timeout waiting for server ReadFrom") + } + + // Server writes data back to the client + responsePayload := []byte("hello from server") + _, err = server.WriteTo(responsePayload, serverReadAddr) + if err != nil { + t.Fatalf("server WriteTo: %v", err) + } + + // Client sends another query to trigger the server to send the response + // (server data is delivered as DNS response payloads) + _, err = client.WriteTo([]byte("poll"), rawConn.LocalAddr()) + if err != nil { + t.Fatal(err) + } + + // Read from client with timeout + clientBuf := make([]byte, 256) + done2 := make(chan struct{}) + var clientReadN int + var clientReadErr error + go func() { + defer close(done2) + clientReadN, _, clientReadErr = client.ReadFrom(clientBuf) + }() + + select { + case <-done2: + case <-time.After(5 * time.Second): + t.Fatal("timeout waiting for client ReadFrom") + } + + if clientReadErr != nil { + t.Fatalf("client ReadFrom: %v", clientReadErr) + } + if !bytes.Equal(clientBuf[:clientReadN], responsePayload) { + t.Errorf("client received %q, want %q", clientBuf[:clientReadN], responsePayload) + } +} diff --git a/transport/internet/finalmask/xdns/server.go b/transport/internet/finalmask/xdns/server.go index ec2f18f9a8ac..4c511c2ef15b 100644 --- a/transport/internet/finalmask/xdns/server.go +++ b/transport/internet/finalmask/xdns/server.go @@ -8,6 +8,7 @@ import ( "io" "net" "sync" + "sync/atomic" "time" "github.com/xtls/xray-core/common/errors" @@ -15,9 +16,8 @@ import ( ) const ( - idleTimeout = 10 * time.Second - responseTTL = 60 - maxResponseDelay = 1 * time.Second + idleTimeout = 10 * time.Second + responseTTL = 60 ) var ( @@ -58,7 +58,7 @@ type xdnsConnServer struct { readQueue chan *packet writeQueueMap map[string]*queue - closed bool + closed atomic.Bool mutex sync.Mutex } @@ -90,7 +90,7 @@ func (c *xdnsConnServer) clean() { c.mutex.Lock() defer c.mutex.Unlock() - if c.closed { + if c.closed.Load() { return true } @@ -116,14 +116,14 @@ func (c *xdnsConnServer) clean() { } func (c *xdnsConnServer) ensureQueue(addr net.Addr) *queue { - if c.closed { + if c.closed.Load() { return nil } q, ok := c.writeQueueMap[addr.String()] if !ok { q = &queue{ - queue: make(chan []byte, 512), + queue: make(chan []byte, 4096), stash: make(chan []byte, 1), } c.writeQueueMap[addr.String()] = q @@ -137,7 +137,7 @@ func (c *xdnsConnServer) stash(queue *queue, p []byte) { c.mutex.Lock() defer c.mutex.Unlock() - if c.closed { + if c.closed.Load() { return } @@ -151,7 +151,7 @@ func (c *xdnsConnServer) recvLoop() { var buf [finalmask.UDPSize]byte for { - if c.closed { + if c.closed.Load() { break } @@ -216,7 +216,7 @@ func (c *xdnsConnServer) recvLoop() { c.mutex.Lock() defer c.mutex.Unlock() - c.closed = true + c.closed.Store(true) for key, q := range c.writeQueueMap { close(q.queue) close(q.stash) @@ -224,17 +224,57 @@ func (c *xdnsConnServer) recvLoop() { } } +func (c *xdnsConnServer) sendEmptyResponse(rec *record) { + if rec.Resp.Rcode() == RcodeNoError && len(rec.Resp.Question) == 1 { + rec.Resp.Answer = []RR{ + { + Name: rec.Resp.Question[0].Name, + Type: rec.Resp.Question[0].Type, + Class: rec.Resp.Question[0].Class, + TTL: responseTTL, + Data: EncodeRDataTXT(nil), + }, + } + } + buf, err := rec.Resp.WireFormat() + if err != nil { + return + } + if len(buf) > maxUDPPayload { + buf = buf[:maxUDPPayload] + buf[2] |= 0x02 + } + c.PacketConn.WriteTo(buf, rec.Addr) +} + func (c *xdnsConnServer) sendLoop() { - var nextRec *record for { - rec := nextRec - nextRec = nil + rec, ok := <-c.ch + if !ok { + break + } - if rec == nil { - var ok bool - rec, ok = <-c.ch - if !ok { - break + // Drain excess records, keeping the latest. mKCP floods retransmissions + // that fill c.ch with hundreds of queries. Process only the latest one. + // Send empty responses for discarded records so resolvers don't time out. + drain: + for { + select { + case newer, ok2 := <-c.ch: + if !ok2 { + break drain + } + // Refresh queue timestamp immediately so clean() cannot reap + // a queue with pending downlink data during the drain loop. + c.mutex.Lock() + if q, ok := c.writeQueueMap[rec.ClientAddr.String()]; ok { + q.last = time.Now() + } + c.mutex.Unlock() + c.sendEmptyResponse(rec) + rec = newer + default: + break drain } } @@ -251,56 +291,69 @@ func (c *xdnsConnServer) sendLoop() { var payload bytes.Buffer limit := maxEncodedPayload - timer := time.NewTimer(maxResponseDelay) - for { - c.mutex.Lock() - q := c.ensureQueue(rec.ClientAddr) - if q == nil { - c.mutex.Unlock() - return - } + c.mutex.Lock() + q := c.ensureQueue(rec.ClientAddr) + if q == nil { c.mutex.Unlock() + return + } + c.mutex.Unlock() - var p []byte - + // Try to get data immediately (non-blocking). If no data is + // available, wait briefly (50ms) for data to arrive. DNS tunneling + // needs fast turnaround because the client can only receive data in + // responses to its queries. + var p []byte + select { + case p = <-q.stash: + default: select { case p = <-q.stash: + case p = <-q.queue: default: + timer := time.NewTimer(50 * time.Millisecond) select { case p = <-q.stash: + timer.Stop() case p = <-q.queue: - default: - select { - case p = <-q.stash: - case p = <-q.queue: - case <-timer.C: - case nextRec = <-c.ch: - } + timer.Stop() + case <-timer.C: } } + } - timer.Reset(0) - - if len(p) == 0 { - break - } - + // Pack first packet + if len(p) > 0 { limit -= 2 + len(p) - if payload.Len() > 0 && limit < 0 { - c.stash(q, p) - break - } - - // if len(p) > 65535 { - // panic(len(p)) - // } - _ = binary.Write(&payload, binary.BigEndian, uint16(len(p))) payload.Write(p) + + // Try to batch more packets immediately (non-blocking) + for { + var p2 []byte + select { + case p2 = <-q.stash: + default: + select { + case p2 = <-q.stash: + case p2 = <-q.queue: + default: + } + } + if len(p2) == 0 { + break + } + limit -= 2 + len(p2) + if limit < 0 { + c.stash(q, p2) + break + } + _ = binary.Write(&payload, binary.BigEndian, uint16(len(p2))) + payload.Write(p2) + } } - timer.Stop() rec.Resp.Answer[0].Data = EncodeRDataTXT(payload.Bytes()) } @@ -316,13 +369,13 @@ func (c *xdnsConnServer) sendLoop() { buf[2] |= 0x02 } - if c.closed { + if c.closed.Load() { return } _, err = c.PacketConn.WriteTo(buf, rec.Addr) if go_errors.Is(err, net.ErrClosed) || go_errors.Is(err, io.ErrClosedPipe) { - c.closed = true + c.closed.Store(true) break } } @@ -362,13 +415,12 @@ func (c *xdnsConnServer) WriteTo(p []byte, addr net.Addr) (n int, err error) { case q.queue <- buf: return len(p), nil default: - // errors.LogDebug(context.Background(), addr, " mask write err queue full") return 0, nil } } func (c *xdnsConnServer) Close() error { - c.closed = true + c.closed.Store(true) return c.PacketConn.Close() } From d000c4d6d363b204a287478c5448eaffd0780062 Mon Sep 17 00:00:00 2001 From: Nikita Nemirovsky Date: Tue, 31 Mar 2026 18:00:43 +0800 Subject: [PATCH 02/25] fix(kcp): reduce aggressive retransmissions when congestion control is enabled The cwnd *= 20 multiplier allowed 20x more packets in flight than the congestion window, defeating the purpose of congestion control. When congestion is enabled, respect the actual cwnd without the multiplier. This prevents mKCP from flooding low-bandwidth transports like XDNS. Also increase connection timeout from 30s to 120s to accommodate high-latency transports like DNS tunneling. --- transport/internet/kcp/connection.go | 2 +- transport/internet/kcp/sending.go | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/transport/internet/kcp/connection.go b/transport/internet/kcp/connection.go index 90c4b7b807e3..50ce79fdd856 100644 --- a/transport/internet/kcp/connection.go +++ b/transport/internet/kcp/connection.go @@ -610,7 +610,7 @@ func (c *Connection) flush() { if c.State() == StateTerminated { return } - if c.State() == StateActive && current-atomic.LoadUint32(&c.lastIncomingTime) >= 30000 { + if c.State() == StateActive && current-atomic.LoadUint32(&c.lastIncomingTime) >= 120000 { c.Close() } if c.State() == StateReadyToClose && c.sendingWorker.IsEmpty() { diff --git a/transport/internet/kcp/sending.go b/transport/internet/kcp/sending.go index ac8e98c16c73..5ba0fe99c4be 100644 --- a/transport/internet/kcp/sending.go +++ b/transport/internet/kcp/sending.go @@ -321,7 +321,9 @@ func (w *SendingWorker) Flush(current uint32) { cwnd = w.controlWindow } - cwnd *= 20 // magic + if !w.conn.Config.Congestion { + cwnd *= 20 + } if !w.window.IsEmpty() { w.window.Flush(current, w.conn.roundTrip.Timeout(), cwnd) From 331c02ea614449ebd85a3ef7f54acf436d4bb8fc Mon Sep 17 00:00:00 2001 From: Nikita Nemirovsky Date: Tue, 31 Mar 2026 20:01:20 +0800 Subject: [PATCH 03/25] refactor(xdns): simplify resolver mode per review feedback Address LjhAUMEM review: masks should not create new connections. Use WriteTo addr on the existing PacketConn to send to different resolvers instead of creating separate sockets. Revert server changes (server already supports data from different DNS sources). Remove sockopt files, sync changes, and layer validation. --- transport/internet/finalmask/xdns/client.go | 136 +++++----------- transport/internet/finalmask/xdns/server.go | 162 +++++++------------- 2 files changed, 90 insertions(+), 208 deletions(-) diff --git a/transport/internet/finalmask/xdns/client.go b/transport/internet/finalmask/xdns/client.go index 2dc8f8a46114..eba916cad27b 100644 --- a/transport/internet/finalmask/xdns/client.go +++ b/transport/internet/finalmask/xdns/client.go @@ -29,11 +29,6 @@ const ( var base32Encoding = base32.StdEncoding.WithPadding(base32.NoPadding) -type resolverConn struct { - conn net.PacketConn - addr *net.UDPAddr -} - func parseResolverAddr(s string) (*net.UDPAddr, error) { host, port, err := net.SplitHostPort(s) if err != nil { @@ -57,20 +52,15 @@ type xdnsConnClient struct { clientID []byte domain Name - resolverConns []*resolverConn - resolverIdx atomic.Uint32 - serverAddr atomic.Value // stores net.Addr; set by WriteTo, used by recvLoopFrom in resolver mode - recvWg sync.WaitGroup - sendWg sync.WaitGroup + resolvers []*net.UDPAddr + resolverIdx atomic.Uint32 pollChan chan struct{} readQueue chan *packet writeQueue chan *packet - closed atomic.Bool - closeOnce sync.Once - closeErr error - mutex sync.Mutex + closed bool + mutex sync.Mutex } func NewConnClient(c *Config, raw net.PacketConn) (net.PacketConn, error) { @@ -92,70 +82,29 @@ func NewConnClient(c *Config, raw net.PacketConn) (net.PacketConn, error) { common.Must2(rand.Read(conn.clientID)) - if len(c.Resolvers) > 0 { - for _, rs := range c.Resolvers { - addr, err := parseResolverAddr(rs) - if err != nil { - for _, rc := range conn.resolverConns { - rc.conn.Close() - } - return nil, errors.New("invalid resolver address: ", rs, ": ", err) - } - uc, err := net.ListenPacket("udp", ":0") - if err != nil { - for _, rc := range conn.resolverConns { - rc.conn.Close() - } - return nil, errors.New("failed to create resolver socket: ", err) - } - conn.resolverConns = append(conn.resolverConns, &resolverConn{conn: uc, addr: addr}) - } - for _, rc := range conn.resolverConns { - conn.recvWg.Add(1) - go func(pconn net.PacketConn) { - defer conn.recvWg.Done() - conn.recvLoopFrom(pconn) - }(rc.conn) + for _, rs := range c.Resolvers { + addr, err := parseResolverAddr(rs) + if err != nil { + return nil, errors.New("invalid resolver address: ", rs, ": ", err) } - } else { - conn.recvWg.Add(1) - go func() { - defer conn.recvWg.Done() - conn.recvLoop() - }() + conn.resolvers = append(conn.resolvers, addr) } - conn.sendWg.Add(1) - go func() { - defer conn.sendWg.Done() - conn.sendLoop() - }() + + go conn.recvLoop() + go conn.sendLoop() return conn, nil } func (c *xdnsConnClient) recvLoop() { - c.recvLoopFrom(c.PacketConn) - - errors.LogDebug(context.Background(), "xdns closed") - - close(c.pollChan) - close(c.readQueue) - - c.closed.Store(true) - c.mutex.Lock() - defer c.mutex.Unlock() - close(c.writeQueue) -} - -func (c *xdnsConnClient) recvLoopFrom(conn net.PacketConn) { var buf [finalmask.UDPSize]byte for { - if c.closed.Load() { + if c.closed { break } - n, addr, err := conn.ReadFrom(buf[:]) + n, addr, err := c.PacketConn.ReadFrom(buf[:]) if err != nil || n == 0 { if go_errors.Is(err, net.ErrClosed) || go_errors.Is(err, io.EOF) { break @@ -174,13 +123,6 @@ func (c *xdnsConnClient) recvLoopFrom(conn net.PacketConn) { continue } - pktAddr := net.Addr(addr) - if len(c.resolverConns) > 0 { - if sa := c.serverAddr.Load(); sa != nil { - pktAddr = sa.(net.Addr) - } - } - r := bytes.NewReader(payload) anyPacket := false for { @@ -195,7 +137,7 @@ func (c *xdnsConnClient) recvLoopFrom(conn net.PacketConn) { select { case c.readQueue <- &packet{ p: buf, - addr: pktAddr, + addr: addr, }: default: errors.LogDebug(context.Background(), addr, " mask read err queue full") @@ -209,6 +151,17 @@ func (c *xdnsConnClient) recvLoopFrom(conn net.PacketConn) { } } } + + errors.LogDebug(context.Background(), "xdns closed") + + close(c.pollChan) + close(c.readQueue) + + c.mutex.Lock() + defer c.mutex.Unlock() + + c.closed = true + close(c.writeQueue) } func (c *xdnsConnClient) sendLoop() { @@ -262,21 +215,19 @@ func (c *xdnsConnClient) sendLoop() { } pollTimer.Reset(pollDelay) - if c.closed.Load() { + if c.closed { return } if p != nil { - var err error - if len(c.resolverConns) > 0 { + dest := p.addr + if len(c.resolvers) > 0 { idx := c.resolverIdx.Add(1) - rc := c.resolverConns[idx%uint32(len(c.resolverConns))] - _, err = rc.conn.WriteTo(p.p, rc.addr) - } else { - _, err = c.PacketConn.WriteTo(p.p, p.addr) + dest = c.resolvers[idx%uint32(len(c.resolvers))] } + _, err := c.PacketConn.WriteTo(p.p, dest) if go_errors.Is(err, net.ErrClosed) || go_errors.Is(err, io.ErrClosedPipe) { - c.closed.Store(true) + c.closed = true break } } @@ -297,12 +248,10 @@ func (c *xdnsConnClient) ReadFrom(p []byte) (n int, addr net.Addr, err error) { } func (c *xdnsConnClient) WriteTo(p []byte, addr net.Addr) (n int, err error) { - c.serverAddr.Store(addr) - c.mutex.Lock() defer c.mutex.Unlock() - if c.closed.Load() { + if c.closed { return 0, io.ErrClosedPipe } @@ -325,23 +274,8 @@ func (c *xdnsConnClient) WriteTo(p []byte, addr net.Addr) (n int, err error) { } func (c *xdnsConnClient) Close() error { - c.closeOnce.Do(func() { - c.closed.Store(true) - for _, rc := range c.resolverConns { - rc.conn.Close() - } - c.closeErr = c.PacketConn.Close() - c.recvWg.Wait() - if len(c.resolverConns) > 0 { - close(c.pollChan) - close(c.readQueue) - c.mutex.Lock() - close(c.writeQueue) - c.mutex.Unlock() - } - c.sendWg.Wait() - }) - return c.closeErr + c.closed = true + return c.PacketConn.Close() } func encode(p []byte, clientID []byte, domain Name) ([]byte, error) { diff --git a/transport/internet/finalmask/xdns/server.go b/transport/internet/finalmask/xdns/server.go index 4c511c2ef15b..ec2f18f9a8ac 100644 --- a/transport/internet/finalmask/xdns/server.go +++ b/transport/internet/finalmask/xdns/server.go @@ -8,7 +8,6 @@ import ( "io" "net" "sync" - "sync/atomic" "time" "github.com/xtls/xray-core/common/errors" @@ -16,8 +15,9 @@ import ( ) const ( - idleTimeout = 10 * time.Second - responseTTL = 60 + idleTimeout = 10 * time.Second + responseTTL = 60 + maxResponseDelay = 1 * time.Second ) var ( @@ -58,7 +58,7 @@ type xdnsConnServer struct { readQueue chan *packet writeQueueMap map[string]*queue - closed atomic.Bool + closed bool mutex sync.Mutex } @@ -90,7 +90,7 @@ func (c *xdnsConnServer) clean() { c.mutex.Lock() defer c.mutex.Unlock() - if c.closed.Load() { + if c.closed { return true } @@ -116,14 +116,14 @@ func (c *xdnsConnServer) clean() { } func (c *xdnsConnServer) ensureQueue(addr net.Addr) *queue { - if c.closed.Load() { + if c.closed { return nil } q, ok := c.writeQueueMap[addr.String()] if !ok { q = &queue{ - queue: make(chan []byte, 4096), + queue: make(chan []byte, 512), stash: make(chan []byte, 1), } c.writeQueueMap[addr.String()] = q @@ -137,7 +137,7 @@ func (c *xdnsConnServer) stash(queue *queue, p []byte) { c.mutex.Lock() defer c.mutex.Unlock() - if c.closed.Load() { + if c.closed { return } @@ -151,7 +151,7 @@ func (c *xdnsConnServer) recvLoop() { var buf [finalmask.UDPSize]byte for { - if c.closed.Load() { + if c.closed { break } @@ -216,7 +216,7 @@ func (c *xdnsConnServer) recvLoop() { c.mutex.Lock() defer c.mutex.Unlock() - c.closed.Store(true) + c.closed = true for key, q := range c.writeQueueMap { close(q.queue) close(q.stash) @@ -224,57 +224,17 @@ func (c *xdnsConnServer) recvLoop() { } } -func (c *xdnsConnServer) sendEmptyResponse(rec *record) { - if rec.Resp.Rcode() == RcodeNoError && len(rec.Resp.Question) == 1 { - rec.Resp.Answer = []RR{ - { - Name: rec.Resp.Question[0].Name, - Type: rec.Resp.Question[0].Type, - Class: rec.Resp.Question[0].Class, - TTL: responseTTL, - Data: EncodeRDataTXT(nil), - }, - } - } - buf, err := rec.Resp.WireFormat() - if err != nil { - return - } - if len(buf) > maxUDPPayload { - buf = buf[:maxUDPPayload] - buf[2] |= 0x02 - } - c.PacketConn.WriteTo(buf, rec.Addr) -} - func (c *xdnsConnServer) sendLoop() { + var nextRec *record for { - rec, ok := <-c.ch - if !ok { - break - } + rec := nextRec + nextRec = nil - // Drain excess records, keeping the latest. mKCP floods retransmissions - // that fill c.ch with hundreds of queries. Process only the latest one. - // Send empty responses for discarded records so resolvers don't time out. - drain: - for { - select { - case newer, ok2 := <-c.ch: - if !ok2 { - break drain - } - // Refresh queue timestamp immediately so clean() cannot reap - // a queue with pending downlink data during the drain loop. - c.mutex.Lock() - if q, ok := c.writeQueueMap[rec.ClientAddr.String()]; ok { - q.last = time.Now() - } - c.mutex.Unlock() - c.sendEmptyResponse(rec) - rec = newer - default: - break drain + if rec == nil { + var ok bool + rec, ok = <-c.ch + if !ok { + break } } @@ -291,69 +251,56 @@ func (c *xdnsConnServer) sendLoop() { var payload bytes.Buffer limit := maxEncodedPayload + timer := time.NewTimer(maxResponseDelay) - c.mutex.Lock() - q := c.ensureQueue(rec.ClientAddr) - if q == nil { + for { + c.mutex.Lock() + q := c.ensureQueue(rec.ClientAddr) + if q == nil { + c.mutex.Unlock() + return + } c.mutex.Unlock() - return - } - c.mutex.Unlock() - // Try to get data immediately (non-blocking). If no data is - // available, wait briefly (50ms) for data to arrive. DNS tunneling - // needs fast turnaround because the client can only receive data in - // responses to its queries. - var p []byte - select { - case p = <-q.stash: - default: + var p []byte + select { case p = <-q.stash: - case p = <-q.queue: default: - timer := time.NewTimer(50 * time.Millisecond) select { case p = <-q.stash: - timer.Stop() case p = <-q.queue: - timer.Stop() - case <-timer.C: + default: + select { + case p = <-q.stash: + case p = <-q.queue: + case <-timer.C: + case nextRec = <-c.ch: + } } } - } - // Pack first packet - if len(p) > 0 { + timer.Reset(0) + + if len(p) == 0 { + break + } + limit -= 2 + len(p) + if payload.Len() > 0 && limit < 0 { + c.stash(q, p) + break + } + + // if len(p) > 65535 { + // panic(len(p)) + // } + _ = binary.Write(&payload, binary.BigEndian, uint16(len(p))) payload.Write(p) - - // Try to batch more packets immediately (non-blocking) - for { - var p2 []byte - select { - case p2 = <-q.stash: - default: - select { - case p2 = <-q.stash: - case p2 = <-q.queue: - default: - } - } - if len(p2) == 0 { - break - } - limit -= 2 + len(p2) - if limit < 0 { - c.stash(q, p2) - break - } - _ = binary.Write(&payload, binary.BigEndian, uint16(len(p2))) - payload.Write(p2) - } } + timer.Stop() rec.Resp.Answer[0].Data = EncodeRDataTXT(payload.Bytes()) } @@ -369,13 +316,13 @@ func (c *xdnsConnServer) sendLoop() { buf[2] |= 0x02 } - if c.closed.Load() { + if c.closed { return } _, err = c.PacketConn.WriteTo(buf, rec.Addr) if go_errors.Is(err, net.ErrClosed) || go_errors.Is(err, io.ErrClosedPipe) { - c.closed.Store(true) + c.closed = true break } } @@ -415,12 +362,13 @@ func (c *xdnsConnServer) WriteTo(p []byte, addr net.Addr) (n int, err error) { case q.queue <- buf: return len(p), nil default: + // errors.LogDebug(context.Background(), addr, " mask write err queue full") return 0, nil } } func (c *xdnsConnServer) Close() error { - c.closed.Store(true) + c.closed = true return c.PacketConn.Close() } From b5de5501a41af22d290f9655a9fc6eb30f4ff15f Mon Sep 17 00:00:00 2001 From: Nikita Nemirovsky Date: Tue, 31 Mar 2026 20:39:23 +0800 Subject: [PATCH 04/25] refactor(xdns): use separate sockets per resolver Per review: separate UDP sockets per resolver avoids ISP source-port rate limiting. Each resolver has its own recvLoop goroutine. Client-only changes, server unchanged. --- transport/internet/finalmask/xdns/client.go | 67 +++++++++++++++------ 1 file changed, 47 insertions(+), 20 deletions(-) diff --git a/transport/internet/finalmask/xdns/client.go b/transport/internet/finalmask/xdns/client.go index eba916cad27b..b69d51d55299 100644 --- a/transport/internet/finalmask/xdns/client.go +++ b/transport/internet/finalmask/xdns/client.go @@ -52,8 +52,9 @@ type xdnsConnClient struct { clientID []byte domain Name - resolvers []*net.UDPAddr - resolverIdx atomic.Uint32 + resolverConns []net.PacketConn + resolverAddrs []*net.UDPAddr + resolverIdx atomic.Uint32 pollChan chan struct{} readQueue chan *packet @@ -85,18 +86,50 @@ func NewConnClient(c *Config, raw net.PacketConn) (net.PacketConn, error) { for _, rs := range c.Resolvers { addr, err := parseResolverAddr(rs) if err != nil { + for _, rc := range conn.resolverConns { + rc.Close() + } return nil, errors.New("invalid resolver address: ", rs, ": ", err) } - conn.resolvers = append(conn.resolvers, addr) + uc, err := net.ListenPacket("udp", ":0") + if err != nil { + for _, rc := range conn.resolverConns { + rc.Close() + } + return nil, errors.New("failed to create resolver socket: ", err) + } + conn.resolverConns = append(conn.resolverConns, uc) + conn.resolverAddrs = append(conn.resolverAddrs, addr) } - go conn.recvLoop() + if len(conn.resolverConns) > 0 { + for _, rc := range conn.resolverConns { + go conn.recvLoopFrom(rc) + } + } else { + go conn.recvLoop() + } go conn.sendLoop() return conn, nil } func (c *xdnsConnClient) recvLoop() { + c.recvLoopFrom(c.PacketConn) + + errors.LogDebug(context.Background(), "xdns closed") + + close(c.pollChan) + close(c.readQueue) + + c.mutex.Lock() + defer c.mutex.Unlock() + + c.closed = true + close(c.writeQueue) +} + +func (c *xdnsConnClient) recvLoopFrom(conn net.PacketConn) { var buf [finalmask.UDPSize]byte for { @@ -104,7 +137,7 @@ func (c *xdnsConnClient) recvLoop() { break } - n, addr, err := c.PacketConn.ReadFrom(buf[:]) + n, addr, err := conn.ReadFrom(buf[:]) if err != nil || n == 0 { if go_errors.Is(err, net.ErrClosed) || go_errors.Is(err, io.EOF) { break @@ -151,17 +184,6 @@ func (c *xdnsConnClient) recvLoop() { } } } - - errors.LogDebug(context.Background(), "xdns closed") - - close(c.pollChan) - close(c.readQueue) - - c.mutex.Lock() - defer c.mutex.Unlock() - - c.closed = true - close(c.writeQueue) } func (c *xdnsConnClient) sendLoop() { @@ -220,12 +242,14 @@ func (c *xdnsConnClient) sendLoop() { } if p != nil { - dest := p.addr - if len(c.resolvers) > 0 { + var err error + if len(c.resolverConns) > 0 { idx := c.resolverIdx.Add(1) - dest = c.resolvers[idx%uint32(len(c.resolvers))] + i := idx % uint32(len(c.resolverConns)) + _, err = c.resolverConns[i].WriteTo(p.p, c.resolverAddrs[i]) + } else { + _, err = c.PacketConn.WriteTo(p.p, p.addr) } - _, err := c.PacketConn.WriteTo(p.p, dest) if go_errors.Is(err, net.ErrClosed) || go_errors.Is(err, io.ErrClosedPipe) { c.closed = true break @@ -275,6 +299,9 @@ func (c *xdnsConnClient) WriteTo(p []byte, addr net.Addr) (n int, err error) { func (c *xdnsConnClient) Close() error { c.closed = true + for _, rc := range c.resolverConns { + rc.Close() + } return c.PacketConn.Close() } From c33fbc8f2a1a4a7d0df3369b5d6fa6ccbb386a35 Mon Sep 17 00:00:00 2001 From: null Date: Wed, 1 Apr 2026 10:37:40 +0800 Subject: [PATCH 05/25] refactor: xnds client --- transport/internet/finalmask/xdns/client.go | 205 +++++------ transport/internet/finalmask/xdns/config.go | 12 + transport/internet/finalmask/xdns/dns_test.go | 329 +----------------- transport/internet/finalmask/xicmp/client.go | 10 +- transport/internet/finalmask/xicmp/config.go | 16 +- transport/internet/finalmask/xicmp/server.go | 6 +- 6 files changed, 125 insertions(+), 453 deletions(-) diff --git a/transport/internet/finalmask/xdns/client.go b/transport/internet/finalmask/xdns/client.go index b69d51d55299..a3babbbab34c 100644 --- a/transport/internet/finalmask/xdns/client.go +++ b/transport/internet/finalmask/xdns/client.go @@ -9,8 +9,8 @@ import ( go_errors "errors" "io" "net" + "strconv" "sync" - "sync/atomic" "time" "github.com/xtls/xray-core/common" @@ -29,18 +29,6 @@ const ( var base32Encoding = base32.StdEncoding.WithPadding(base32.NoPadding) -func parseResolverAddr(s string) (*net.UDPAddr, error) { - host, port, err := net.SplitHostPort(s) - if err != nil { - host = s - port = "53" - } - if host == "" { - return nil, go_errors.New("empty resolver address") - } - return net.ResolveUDPAddr("udp", net.JoinHostPort(host, port)) -} - type packet struct { p []byte addr net.Addr @@ -48,14 +36,13 @@ type packet struct { type xdnsConnClient struct { net.PacketConn + resolverConns []net.PacketConn + resolverAddrs []*net.UDPAddr + resolverIdx uint32 clientID []byte domain Name - resolverConns []net.PacketConn - resolverAddrs []*net.UDPAddr - resolverIdx atomic.Uint32 - pollChan chan struct{} readQueue chan *packet writeQueue chan *packet @@ -65,6 +52,10 @@ type xdnsConnClient struct { } func NewConnClient(c *Config, raw net.PacketConn) (net.PacketConn, error) { + if len(c.Resolvers) == 0 { + return nil, errors.New("empty resolvers") + } + domain, err := ParseName(c.Domain) if err != nil { return nil, err @@ -84,14 +75,24 @@ func NewConnClient(c *Config, raw net.PacketConn) (net.PacketConn, error) { common.Must2(rand.Read(conn.clientID)) for _, rs := range c.Resolvers { - addr, err := parseResolverAddr(rs) + h, p, err := net.SplitHostPort(rs) if err != nil { - for _, rc := range conn.resolverConns { - rc.Close() - } - return nil, errors.New("invalid resolver address: ", rs, ": ", err) + return nil, err + } + ip := net.ParseIP(h) + if ip == nil { + return nil, errors.New("invalid ip address") + } + port, _ := strconv.Atoi(p) + if port == 0 { + return nil, errors.New("invalid port") + } + var uc net.PacketConn + if ip.To4() != nil { + uc, err = net.ListenPacket("udp4", ":0") + } else { + uc, err = net.ListenPacket("udp6", ":0") } - uc, err := net.ListenPacket("udp", ":0") if err != nil { for _, rc := range conn.resolverConns { rc.Close() @@ -99,96 +100,95 @@ func NewConnClient(c *Config, raw net.PacketConn) (net.PacketConn, error) { return nil, errors.New("failed to create resolver socket: ", err) } conn.resolverConns = append(conn.resolverConns, uc) - conn.resolverAddrs = append(conn.resolverAddrs, addr) + conn.resolverAddrs = append(conn.resolverAddrs, &net.UDPAddr{IP: ip, Port: port}) } - if len(conn.resolverConns) > 0 { - for _, rc := range conn.resolverConns { - go conn.recvLoopFrom(rc) - } - } else { - go conn.recvLoop() - } + go conn.recvLoop() go conn.sendLoop() return conn, nil } func (c *xdnsConnClient) recvLoop() { - c.recvLoopFrom(c.PacketConn) + var wg sync.WaitGroup - errors.LogDebug(context.Background(), "xdns closed") + for _, rc := range c.resolverConns { + wg.Add(1) + go func() { + defer wg.Done() - close(c.pollChan) - close(c.readQueue) + var buf [finalmask.UDPSize]byte - c.mutex.Lock() - defer c.mutex.Unlock() + for { + if c.closed { + break + } - c.closed = true - close(c.writeQueue) -} + n, addr, err := rc.ReadFrom(buf[:]) + if err != nil || n == 0 { + if go_errors.Is(err, net.ErrClosed) || go_errors.Is(err, io.EOF) { + break + } + continue + } -func (c *xdnsConnClient) recvLoopFrom(conn net.PacketConn) { - var buf [finalmask.UDPSize]byte + resp, err := MessageFromWireFormat(buf[:n]) + if err != nil { + errors.LogDebug(context.Background(), addr, " xdns from wireformat err ", err) + continue + } - for { - if c.closed { - break - } + payload := dnsResponsePayload(&resp, c.domain) + + r := bytes.NewReader(payload) + anyPacket := false + for { + p, err := nextPacket(r) + if err != nil { + break + } + anyPacket = true + + buf := make([]byte, len(p)) + copy(buf, p) + select { + case c.readQueue <- &packet{ + p: buf, + addr: addr, + }: + default: + errors.LogDebug(context.Background(), addr, " mask read err queue full") + } + } - n, addr, err := conn.ReadFrom(buf[:]) - if err != nil || n == 0 { - if go_errors.Is(err, net.ErrClosed) || go_errors.Is(err, io.EOF) { - break + if anyPacket { + select { + case c.pollChan <- struct{}{}: + default: + } + } } - continue - } + }() + } - resp, err := MessageFromWireFormat(buf[:n]) - if err != nil { - errors.LogDebug(context.Background(), addr, " xdns from wireformat err ", err) - continue - } + wg.Wait() - payload := dnsResponsePayload(&resp, c.domain) - if payload == nil { - continue - } + errors.LogDebug(context.Background(), "xdns closed") - r := bytes.NewReader(payload) - anyPacket := false - for { - p, err := nextPacket(r) - if err != nil { - break - } - anyPacket = true + close(c.pollChan) + close(c.readQueue) + for _, rc := range c.resolverConns { + rc.Close() + } - buf := make([]byte, len(p)) - copy(buf, p) - select { - case c.readQueue <- &packet{ - p: buf, - addr: addr, - }: - default: - errors.LogDebug(context.Background(), addr, " mask read err queue full") - } - } + c.mutex.Lock() + defer c.mutex.Unlock() - if anyPacket { - select { - case c.pollChan <- struct{}{}: - default: - } - } - } + c.closed = true + close(c.writeQueue) } func (c *xdnsConnClient) sendLoop() { - var addr net.Addr - pollDelay := initPollDelay pollTimer := time.NewTimer(pollDelay) for { @@ -207,17 +207,14 @@ func (c *xdnsConnClient) sendLoop() { } if p != nil { - addr = p.addr - select { case <-c.pollChan: default: } - } else if addr != nil { + } else { encoded, _ := encode(nil, c.clientID, c.domain) p = &packet{ - p: encoded, - addr: addr, + p: encoded, } } @@ -241,20 +238,9 @@ func (c *xdnsConnClient) sendLoop() { return } - if p != nil { - var err error - if len(c.resolverConns) > 0 { - idx := c.resolverIdx.Add(1) - i := idx % uint32(len(c.resolverConns)) - _, err = c.resolverConns[i].WriteTo(p.p, c.resolverAddrs[i]) - } else { - _, err = c.PacketConn.WriteTo(p.p, p.addr) - } - if go_errors.Is(err, net.ErrClosed) || go_errors.Is(err, io.ErrClosedPipe) { - c.closed = true - break - } - } + _, _ = c.resolverConns[c.resolverIdx].WriteTo(p.p, c.resolverAddrs[c.resolverIdx]) + c.resolverIdx += 1 + c.resolverIdx %= uint32(len(c.resolverConns)) } } @@ -299,9 +285,6 @@ func (c *xdnsConnClient) WriteTo(p []byte, addr net.Addr) (n int, err error) { func (c *xdnsConnClient) Close() error { c.closed = true - for _, rc := range c.resolverConns { - rc.Close() - } return c.PacketConn.Close() } diff --git a/transport/internet/finalmask/xdns/config.go b/transport/internet/finalmask/xdns/config.go index 157102dafa2b..834233ec2df6 100644 --- a/transport/internet/finalmask/xdns/config.go +++ b/transport/internet/finalmask/xdns/config.go @@ -2,15 +2,27 @@ package xdns import ( "net" + + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/transport/internet" + "github.com/xtls/xray-core/transport/internet/hysteria/udphop" ) func (c *Config) UDP() { } func (c *Config) WrapPacketConnClient(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) { + _, ok1 := raw.(*internet.FakePacketConn) + _, ok2 := raw.(*udphop.UdpHopPacketConn) + if level != 0 || ok1 || ok2 { + return nil, errors.New("xicmp requires being at the outermost level") + } return NewConnClient(c, raw) } func (c *Config) WrapPacketConnServer(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) { + if level != 0 { + return nil, errors.New("xdns requires being at the outermost level") + } return NewConnServer(c, raw) } diff --git a/transport/internet/finalmask/xdns/dns_test.go b/transport/internet/finalmask/xdns/dns_test.go index 5fc5a85b58c6..aa163476d9f1 100644 --- a/transport/internet/finalmask/xdns/dns_test.go +++ b/transport/internet/finalmask/xdns/dns_test.go @@ -4,12 +4,9 @@ import ( "bytes" "fmt" "io" - "net" "strconv" "strings" - "sync/atomic" "testing" - "time" ) func namesEqual(a, b Name) bool { @@ -550,8 +547,8 @@ func TestEncodeRDataTXT(t *testing.T) { // zero, not an empty slice. p := make([]byte, 0) encoded := EncodeRDataTXT(p) - if len(encoded) < 1 { - t.Errorf("EncodeRDataTXT(%v) returned %v, want at least 1 byte", p, encoded) + if len(encoded) < 0 { + t.Errorf("EncodeRDataTXT(%v) returned %v", p, encoded) } // 255 bytes should be able to be encoded into 256 bytes. @@ -561,10 +558,7 @@ func TestEncodeRDataTXT(t *testing.T) { t.Errorf("EncodeRDataTXT(%d bytes) returned %d bytes", len(p), len(encoded)) } - nilEncoded := EncodeRDataTXT(nil) - if len(nilEncoded) < 1 { - t.Errorf("EncodeRDataTXT(nil) returned %v, want at least 1 byte", nilEncoded) - } + fmt.Println(EncodeRDataTXT(nil)) } func TestRDataTXTRoundTrip(t *testing.T) { @@ -598,320 +592,3 @@ func TestRDataTXTRoundTrip(t *testing.T) { } } } - -func TestParseResolverAddr(t *testing.T) { - tests := []struct { - name string - input string - wantIP string - wantPort int - wantErr bool - }{ - {"bare_ipv4", "1.1.1.1", "1.1.1.1", 53, false}, - {"ipv4_with_port", "8.8.8.8:53", "8.8.8.8", 53, false}, - {"ipv4_custom_port", "8.8.8.8:5353", "8.8.8.8", 5353, false}, - {"ipv6_with_port", "[2606:4700:4700::1111]:53", "2606:4700:4700::1111", 53, false}, - {"empty_string", "", "", 0, true}, - {"empty_host_with_port", ":53", "", 0, true}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - addr, err := parseResolverAddr(tt.input) - if tt.wantErr { - if err == nil { - t.Fatalf("parseResolverAddr(%q): want error, got nil", tt.input) - } - return - } - if err != nil { - t.Fatalf("parseResolverAddr(%q): %v", tt.input, err) - } - if addr.IP.String() != tt.wantIP { - t.Errorf("parseResolverAddr(%q).IP = %v, want %v", tt.input, addr.IP, tt.wantIP) - } - if addr.Port != tt.wantPort { - t.Errorf("parseResolverAddr(%q).Port = %v, want %v", tt.input, addr.Port, tt.wantPort) - } - }) - } -} - -func TestResolverModeRoundTrip(t *testing.T) { - // Auth server: raw UDP socket that the XDNS server wraps - authServer, err := net.ListenPacket("udp", "127.0.0.1:0") - if err != nil { - t.Fatal(err) - } - defer authServer.Close() - - // Mock resolver: bidirectional UDP forwarder between client and auth server - resolver, err := net.ListenPacket("udp", "127.0.0.1:0") - if err != nil { - t.Fatal(err) - } - defer resolver.Close() - - go func() { - buf := make([]byte, 4096) - authAddr := authServer.LocalAddr().String() - var clientAddr net.Addr - for { - n, addr, err := resolver.ReadFrom(buf) - if err != nil { - return - } - if addr.String() == authAddr { - // Response from auth server -> forward to client - if clientAddr != nil { - resolver.WriteTo(buf[:n], clientAddr) - } - } else { - // Query from client -> forward to auth server - clientAddr = addr - resolver.WriteTo(buf[:n], authServer.LocalAddr()) - } - } - }() - - // XDNS server wrapping the auth server socket - serverConfig := &Config{Domain: "t.example.com"} - server, err := NewConnServer(serverConfig, authServer) - if err != nil { - t.Fatal(err) - } - defer server.Close() - - // XDNS client configured to use the mock resolver - config := &Config{ - Domain: "t.example.com", - Resolvers: []string{resolver.LocalAddr().String()}, - } - rawConn, err := net.ListenPacket("udp", "127.0.0.1:0") - if err != nil { - t.Fatal(err) - } - defer rawConn.Close() - - client, err := NewConnClient(config, rawConn) - if err != nil { - t.Fatal(err) - } - defer client.Close() - - // Send test payload through client - testPayload := []byte("hello xdns resolver") - _, err = client.WriteTo(testPayload, rawConn.LocalAddr()) - if err != nil { - t.Fatal(err) - } - - // Read from server with timeout (server.ReadFrom blocks on channel, - // so SetReadDeadline won't work; use a goroutine instead) - done := make(chan struct{}) - var serverBuf [256]byte - var readN int - var readErr error - go func() { - defer close(done) - readN, _, readErr = server.ReadFrom(serverBuf[:]) - }() - - select { - case <-done: - case <-time.After(5 * time.Second): - t.Fatal("timeout waiting for server ReadFrom") - } - - if readErr != nil { - t.Fatalf("server ReadFrom: %v", readErr) - } - if !bytes.Equal(serverBuf[:readN], testPayload) { - t.Errorf("server received %q, want %q", serverBuf[:readN], testPayload) - } -} - -func TestMultiResolverDistribution(t *testing.T) { - const numResolvers = 3 - - // Create mock resolvers that count received packets - resolvers := make([]net.PacketConn, numResolvers) - var counts [numResolvers]atomic.Int32 - for i := range resolvers { - r, err := net.ListenPacket("udp", "127.0.0.1:0") - if err != nil { - t.Fatal(err) - } - defer r.Close() - resolvers[i] = r - idx := i - go func() { - buf := make([]byte, 4096) - for { - _, _, err := resolvers[idx].ReadFrom(buf) - if err != nil { - return - } - counts[idx].Add(1) - } - }() - } - - resolverAddrs := make([]string, numResolvers) - for i, r := range resolvers { - resolverAddrs[i] = r.LocalAddr().String() - } - - config := &Config{ - Domain: "t.example.com", - Resolvers: resolverAddrs, - } - rawConn, err := net.ListenPacket("udp", "127.0.0.1:0") - if err != nil { - t.Fatal(err) - } - defer rawConn.Close() - - client, err := NewConnClient(config, rawConn) - if err != nil { - t.Fatal(err) - } - defer client.Close() - - // Send enough messages to hit all resolvers via round-robin - for i := 0; i < numResolvers*3; i++ { - payload := []byte(fmt.Sprintf("msg-%d", i)) - _, err = client.WriteTo(payload, rawConn.LocalAddr()) - if err != nil { - t.Fatal(err) - } - } - - // Allow sendLoop to process the queue - time.Sleep(500 * time.Millisecond) - - for i := 0; i < numResolvers; i++ { - c := counts[i].Load() - if c == 0 { - t.Errorf("resolver %d received no queries, want at least 1", i) - } - } -} - - -func TestResolverModeServerToClient(t *testing.T) { - // Auth server: raw UDP socket that the XDNS server wraps - authServer, err := net.ListenPacket("udp", "127.0.0.1:0") - if err != nil { - t.Fatal(err) - } - defer authServer.Close() - - // Mock resolver: bidirectional UDP forwarder - resolver, err := net.ListenPacket("udp", "127.0.0.1:0") - if err != nil { - t.Fatal(err) - } - defer resolver.Close() - - go func() { - buf := make([]byte, 4096) - authAddr := authServer.LocalAddr().String() - var clientAddr net.Addr - for { - n, addr, err := resolver.ReadFrom(buf) - if err != nil { - return - } - if addr.String() == authAddr { - if clientAddr != nil { - resolver.WriteTo(buf[:n], clientAddr) - } - } else { - clientAddr = addr - resolver.WriteTo(buf[:n], authServer.LocalAddr()) - } - } - }() - - // XDNS server - serverConfig := &Config{Domain: "t.example.com"} - server, err := NewConnServer(serverConfig, authServer) - if err != nil { - t.Fatal(err) - } - defer server.Close() - - // XDNS client with resolver - config := &Config{ - Domain: "t.example.com", - Resolvers: []string{resolver.LocalAddr().String()}, - } - rawConn, err := net.ListenPacket("udp", "127.0.0.1:0") - if err != nil { - t.Fatal(err) - } - defer rawConn.Close() - - client, err := NewConnClient(config, rawConn) - if err != nil { - t.Fatal(err) - } - defer client.Close() - - // Client sends a query to trigger the connection and set serverAddr - _, err = client.WriteTo([]byte("init"), rawConn.LocalAddr()) - if err != nil { - t.Fatal(err) - } - - // Wait for server to receive the client query - serverBuf := make([]byte, 256) - done := make(chan struct{}) - var serverReadAddr net.Addr - go func() { - defer close(done) - _, serverReadAddr, _ = server.ReadFrom(serverBuf) - }() - select { - case <-done: - case <-time.After(5 * time.Second): - t.Fatal("timeout waiting for server ReadFrom") - } - - // Server writes data back to the client - responsePayload := []byte("hello from server") - _, err = server.WriteTo(responsePayload, serverReadAddr) - if err != nil { - t.Fatalf("server WriteTo: %v", err) - } - - // Client sends another query to trigger the server to send the response - // (server data is delivered as DNS response payloads) - _, err = client.WriteTo([]byte("poll"), rawConn.LocalAddr()) - if err != nil { - t.Fatal(err) - } - - // Read from client with timeout - clientBuf := make([]byte, 256) - done2 := make(chan struct{}) - var clientReadN int - var clientReadErr error - go func() { - defer close(done2) - clientReadN, _, clientReadErr = client.ReadFrom(clientBuf) - }() - - select { - case <-done2: - case <-time.After(5 * time.Second): - t.Fatal("timeout waiting for client ReadFrom") - } - - if clientReadErr != nil { - t.Fatalf("client ReadFrom: %v", clientReadErr) - } - if !bytes.Equal(clientBuf[:clientReadN], responsePayload) { - t.Errorf("client received %q, want %q", clientBuf[:clientReadN], responsePayload) - } -} diff --git a/transport/internet/finalmask/xicmp/client.go b/transport/internet/finalmask/xicmp/client.go index 6ceaf2671643..d738b125098d 100644 --- a/transport/internet/finalmask/xicmp/client.go +++ b/transport/internet/finalmask/xicmp/client.go @@ -10,9 +10,7 @@ import ( "github.com/xtls/xray-core/common/crypto" "github.com/xtls/xray-core/common/errors" - "github.com/xtls/xray-core/transport/internet" "github.com/xtls/xray-core/transport/internet/finalmask" - "github.com/xtls/xray-core/transport/internet/hysteria/udphop" "golang.org/x/net/icmp" "golang.org/x/net/ipv4" "golang.org/x/net/ipv6" @@ -54,13 +52,7 @@ type xicmpConnClient struct { mutex sync.Mutex } -func NewConnClient(c *Config, raw net.PacketConn, level int) (net.PacketConn, error) { - _, ok1 := raw.(*internet.FakePacketConn) - _, ok2 := raw.(*udphop.UdpHopPacketConn) - if level != 0 || ok1 || ok2 { - return nil, errors.New("xicmp requires being at the outermost level") - } - +func NewConnClient(c *Config, raw net.PacketConn) (net.PacketConn, error) { network := "ip4:icmp" typ := icmp.Type(ipv4.ICMPTypeEcho) proto := 1 diff --git a/transport/internet/finalmask/xicmp/config.go b/transport/internet/finalmask/xicmp/config.go index c570ce96817e..fdcb02ae701f 100644 --- a/transport/internet/finalmask/xicmp/config.go +++ b/transport/internet/finalmask/xicmp/config.go @@ -2,15 +2,27 @@ package xicmp import ( "net" + + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/transport/internet" + "github.com/xtls/xray-core/transport/internet/hysteria/udphop" ) func (c *Config) UDP() { } func (c *Config) WrapPacketConnClient(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) { - return NewConnClient(c, raw, level) + _, ok1 := raw.(*internet.FakePacketConn) + _, ok2 := raw.(*udphop.UdpHopPacketConn) + if level != 0 || ok1 || ok2 { + return nil, errors.New("xicmp requires being at the outermost level") + } + return NewConnClient(c, raw) } func (c *Config) WrapPacketConnServer(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) { - return NewConnServer(c, raw, level) + if level != 0 { + return nil, errors.New("xicmp requires being at the outermost level") + } + return NewConnServer(c, raw) } diff --git a/transport/internet/finalmask/xicmp/server.go b/transport/internet/finalmask/xicmp/server.go index 79a5d010dd84..94012f019802 100644 --- a/transport/internet/finalmask/xicmp/server.go +++ b/transport/internet/finalmask/xicmp/server.go @@ -50,11 +50,7 @@ type xicmpConnServer struct { mutex sync.Mutex } -func NewConnServer(c *Config, raw net.PacketConn, level int) (net.PacketConn, error) { - if level != 0 { - return nil, errors.New("xicmp requires being at the outermost level") - } - +func NewConnServer(c *Config, raw net.PacketConn) (net.PacketConn, error) { network := "ip4:icmp" typ := icmp.Type(ipv4.ICMPTypeEchoReply) proto := 1 From 0cdf23b2a30455d52e7ac5f9d0f50f9c552eeae5 Mon Sep 17 00:00:00 2001 From: null Date: Wed, 1 Apr 2026 10:53:24 +0800 Subject: [PATCH 06/25] restore pollTimer --- transport/internet/finalmask/xdns/client.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/transport/internet/finalmask/xdns/client.go b/transport/internet/finalmask/xdns/client.go index a3babbbab34c..617460aeef97 100644 --- a/transport/internet/finalmask/xdns/client.go +++ b/transport/internet/finalmask/xdns/client.go @@ -225,10 +225,7 @@ func (c *xdnsConnClient) sendLoop() { } } else { if !pollTimer.Stop() { - select { - case <-pollTimer.C: - default: - } + <-pollTimer.C } pollDelay = initPollDelay } From 75f771fed884657f6d33fb9fac40c4364197dead Mon Sep 17 00:00:00 2001 From: null Date: Wed, 1 Apr 2026 10:58:09 +0800 Subject: [PATCH 07/25] typo --- transport/internet/finalmask/xdns/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/transport/internet/finalmask/xdns/config.go b/transport/internet/finalmask/xdns/config.go index 834233ec2df6..dbd78a28633d 100644 --- a/transport/internet/finalmask/xdns/config.go +++ b/transport/internet/finalmask/xdns/config.go @@ -15,7 +15,7 @@ func (c *Config) WrapPacketConnClient(raw net.PacketConn, level int, levelCount _, ok1 := raw.(*internet.FakePacketConn) _, ok2 := raw.(*udphop.UdpHopPacketConn) if level != 0 || ok1 || ok2 { - return nil, errors.New("xicmp requires being at the outermost level") + return nil, errors.New("xdns requires being at the outermost level") } return NewConnClient(c, raw) } From b934ca17be9d6f158b13a172740fd835b30813a5 Mon Sep 17 00:00:00 2001 From: null Date: Wed, 1 Apr 2026 11:32:33 +0800 Subject: [PATCH 08/25] config.pb.go --- transport/internet/finalmask/xdns/config.pb.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/transport/internet/finalmask/xdns/config.pb.go b/transport/internet/finalmask/xdns/config.pb.go index 55b01b45cbe9..c316ea757d0c 100644 --- a/transport/internet/finalmask/xdns/config.pb.go +++ b/transport/internet/finalmask/xdns/config.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.11 -// protoc v5.28.3 +// protoc v6.33.5 // source: transport/internet/finalmask/xdns/config.proto package xdns From 9f1066bd93ce70e680aa96c40baebccfcaf81ab2 Mon Sep 17 00:00:00 2001 From: null Date: Wed, 1 Apr 2026 12:24:20 +0800 Subject: [PATCH 09/25] fix bug --- transport/internet/finalmask/xdns/client.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/transport/internet/finalmask/xdns/client.go b/transport/internet/finalmask/xdns/client.go index 617460aeef97..369ef6963e45 100644 --- a/transport/internet/finalmask/xdns/client.go +++ b/transport/internet/finalmask/xdns/client.go @@ -177,9 +177,6 @@ func (c *xdnsConnClient) recvLoop() { close(c.pollChan) close(c.readQueue) - for _, rc := range c.resolverConns { - rc.Close() - } c.mutex.Lock() defer c.mutex.Unlock() @@ -282,6 +279,9 @@ func (c *xdnsConnClient) WriteTo(p []byte, addr net.Addr) (n int, err error) { func (c *xdnsConnClient) Close() error { c.closed = true + for _, rc := range c.resolverConns { + rc.Close() + } return c.PacketConn.Close() } From ceff1b6651654baa7e601147654b6d953c07e472 Mon Sep 17 00:00:00 2001 From: null Date: Wed, 1 Apr 2026 13:44:34 +0800 Subject: [PATCH 10/25] healthCheck --- transport/internet/finalmask/xdns/client.go | 59 ++++++++++++++++++--- 1 file changed, 53 insertions(+), 6 deletions(-) diff --git a/transport/internet/finalmask/xdns/client.go b/transport/internet/finalmask/xdns/client.go index 369ef6963e45..d2e6653e563e 100644 --- a/transport/internet/finalmask/xdns/client.go +++ b/transport/internet/finalmask/xdns/client.go @@ -36,9 +36,11 @@ type packet struct { type xdnsConnClient struct { net.PacketConn - resolverConns []net.PacketConn - resolverAddrs []*net.UDPAddr - resolverIdx uint32 + resolverConns []net.PacketConn + resolverAddrs []*net.UDPAddr + resolverIdx uint32 + resolverLast []time.Time + resolverClosed []bool clientID []byte domain Name @@ -102,17 +104,42 @@ func NewConnClient(c *Config, raw net.PacketConn) (net.PacketConn, error) { conn.resolverConns = append(conn.resolverConns, uc) conn.resolverAddrs = append(conn.resolverAddrs, &net.UDPAddr{IP: ip, Port: port}) } + conn.resolverLast = make([]time.Time, len(conn.resolverConns)) + conn.resolverClosed = make([]bool, len(conn.resolverConns)) + go conn.healthCheck() go conn.recvLoop() go conn.sendLoop() return conn, nil } +func (c *xdnsConnClient) healthCheck() { + for { + if c.closed { + return + } + + now := time.Now() + for i := range c.resolverLast { + if c.resolverLast[i].IsZero() { + continue + } + c.mutex.Lock() + if now.Sub(c.resolverLast[i]) > 3*time.Second { + c.resolverClosed[i] = true + } + c.mutex.Unlock() + } + + time.Sleep(1 * time.Second) + } +} + func (c *xdnsConnClient) recvLoop() { var wg sync.WaitGroup - for _, rc := range c.resolverConns { + for i, rc := range c.resolverConns { wg.Add(1) go func() { defer wg.Done() @@ -162,6 +189,11 @@ func (c *xdnsConnClient) recvLoop() { } if anyPacket { + c.mutex.Lock() + c.resolverLast[i] = time.Now() + c.resolverClosed[i] = false + c.mutex.Unlock() + select { case c.pollChan <- struct{}{}: default: @@ -233,8 +265,23 @@ func (c *xdnsConnClient) sendLoop() { } _, _ = c.resolverConns[c.resolverIdx].WriteTo(p.p, c.resolverAddrs[c.resolverIdx]) - c.resolverIdx += 1 - c.resolverIdx %= uint32(len(c.resolverConns)) + if c.resolverLast[c.resolverIdx].IsZero() { + c.resolverLast[c.resolverIdx] = time.Now() + } + cur := c.resolverIdx + for { + c.resolverIdx += 1 + c.resolverIdx %= uint32(len(c.resolverConns)) + if c.resolverIdx == cur { + break + } + c.mutex.Lock() + if !c.resolverClosed[c.resolverIdx] { + c.mutex.Unlock() + break + } + c.mutex.Unlock() + } } } From e064e0f12137eb00583885da09b65f4e84abe132 Mon Sep 17 00:00:00 2001 From: null Date: Wed, 1 Apr 2026 16:37:19 +0800 Subject: [PATCH 11/25] refactor: healthCheck --- transport/internet/finalmask/xdns/client.go | 37 ++++++++++----------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/transport/internet/finalmask/xdns/client.go b/transport/internet/finalmask/xdns/client.go index d2e6653e563e..db8ccd690a8b 100644 --- a/transport/internet/finalmask/xdns/client.go +++ b/transport/internet/finalmask/xdns/client.go @@ -36,11 +36,12 @@ type packet struct { type xdnsConnClient struct { net.PacketConn - resolverConns []net.PacketConn - resolverAddrs []*net.UDPAddr - resolverIdx uint32 - resolverLast []time.Time - resolverClosed []bool + resolverConns []net.PacketConn + resolverAddrs []*net.UDPAddr + resolverIdx uint32 + // resolverRecv []uint32 + resolverSend []uint32 + resolverDead []bool clientID []byte domain Name @@ -104,8 +105,8 @@ func NewConnClient(c *Config, raw net.PacketConn) (net.PacketConn, error) { conn.resolverConns = append(conn.resolverConns, uc) conn.resolverAddrs = append(conn.resolverAddrs, &net.UDPAddr{IP: ip, Port: port}) } - conn.resolverLast = make([]time.Time, len(conn.resolverConns)) - conn.resolverClosed = make([]bool, len(conn.resolverConns)) + conn.resolverSend = make([]uint32, len(conn.resolverConns)) + conn.resolverDead = make([]bool, len(conn.resolverConns)) go conn.healthCheck() go conn.recvLoop() @@ -120,14 +121,10 @@ func (c *xdnsConnClient) healthCheck() { return } - now := time.Now() - for i := range c.resolverLast { - if c.resolverLast[i].IsZero() { - continue - } + for i := range c.resolverSend { c.mutex.Lock() - if now.Sub(c.resolverLast[i]) > 3*time.Second { - c.resolverClosed[i] = true + if c.resolverSend[i] > 3 { + c.resolverDead[i] = true } c.mutex.Unlock() } @@ -190,8 +187,8 @@ func (c *xdnsConnClient) recvLoop() { if anyPacket { c.mutex.Lock() - c.resolverLast[i] = time.Now() - c.resolverClosed[i] = false + c.resolverSend[i] = 0 + c.resolverDead[i] = false c.mutex.Unlock() select { @@ -265,9 +262,6 @@ func (c *xdnsConnClient) sendLoop() { } _, _ = c.resolverConns[c.resolverIdx].WriteTo(p.p, c.resolverAddrs[c.resolverIdx]) - if c.resolverLast[c.resolverIdx].IsZero() { - c.resolverLast[c.resolverIdx] = time.Now() - } cur := c.resolverIdx for { c.resolverIdx += 1 @@ -276,12 +270,15 @@ func (c *xdnsConnClient) sendLoop() { break } c.mutex.Lock() - if !c.resolverClosed[c.resolverIdx] { + if !c.resolverDead[c.resolverIdx] { c.mutex.Unlock() break } c.mutex.Unlock() } + c.mutex.Lock() + c.resolverSend[cur] += 1 + c.mutex.Unlock() } } From 29e8170a53d53a025f73ea4e1387463e60bef752 Mon Sep 17 00:00:00 2001 From: null Date: Wed, 1 Apr 2026 18:13:15 +0800 Subject: [PATCH 12/25] refactor: healthCheck --- transport/internet/finalmask/xdns/client.go | 37 ++++----------------- 1 file changed, 7 insertions(+), 30 deletions(-) diff --git a/transport/internet/finalmask/xdns/client.go b/transport/internet/finalmask/xdns/client.go index db8ccd690a8b..a042a25158f5 100644 --- a/transport/internet/finalmask/xdns/client.go +++ b/transport/internet/finalmask/xdns/client.go @@ -39,9 +39,7 @@ type xdnsConnClient struct { resolverConns []net.PacketConn resolverAddrs []*net.UDPAddr resolverIdx uint32 - // resolverRecv []uint32 - resolverSend []uint32 - resolverDead []bool + resolverSend []uint32 clientID []byte domain Name @@ -106,33 +104,13 @@ func NewConnClient(c *Config, raw net.PacketConn) (net.PacketConn, error) { conn.resolverAddrs = append(conn.resolverAddrs, &net.UDPAddr{IP: ip, Port: port}) } conn.resolverSend = make([]uint32, len(conn.resolverConns)) - conn.resolverDead = make([]bool, len(conn.resolverConns)) - go conn.healthCheck() go conn.recvLoop() go conn.sendLoop() return conn, nil } -func (c *xdnsConnClient) healthCheck() { - for { - if c.closed { - return - } - - for i := range c.resolverSend { - c.mutex.Lock() - if c.resolverSend[i] > 3 { - c.resolverDead[i] = true - } - c.mutex.Unlock() - } - - time.Sleep(1 * time.Second) - } -} - func (c *xdnsConnClient) recvLoop() { var wg sync.WaitGroup @@ -188,7 +166,6 @@ func (c *xdnsConnClient) recvLoop() { if anyPacket { c.mutex.Lock() c.resolverSend[i] = 0 - c.resolverDead[i] = false c.mutex.Unlock() select { @@ -261,6 +238,9 @@ func (c *xdnsConnClient) sendLoop() { return } + c.mutex.Lock() + c.resolverSend[c.resolverIdx] += 1 + c.mutex.Unlock() _, _ = c.resolverConns[c.resolverIdx].WriteTo(p.p, c.resolverAddrs[c.resolverIdx]) cur := c.resolverIdx for { @@ -270,15 +250,12 @@ func (c *xdnsConnClient) sendLoop() { break } c.mutex.Lock() - if !c.resolverDead[c.resolverIdx] { - c.mutex.Unlock() + alive := c.resolverSend[c.resolverIdx] <= 3 + c.mutex.Unlock() + if alive { break } - c.mutex.Unlock() } - c.mutex.Lock() - c.resolverSend[cur] += 1 - c.mutex.Unlock() } } From d60ac6d3aca92f55fa13d52440036f401c048df4 Mon Sep 17 00:00:00 2001 From: null Date: Wed, 1 Apr 2026 20:01:41 +0800 Subject: [PATCH 13/25] err type --- transport/internet/finalmask/xdns/client.go | 4 ++-- transport/internet/finalmask/xdns/server.go | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/transport/internet/finalmask/xdns/client.go b/transport/internet/finalmask/xdns/client.go index a042a25158f5..c4acda4913a1 100644 --- a/transport/internet/finalmask/xdns/client.go +++ b/transport/internet/finalmask/xdns/client.go @@ -127,8 +127,8 @@ func (c *xdnsConnClient) recvLoop() { } n, addr, err := rc.ReadFrom(buf[:]) - if err != nil || n == 0 { - if go_errors.Is(err, net.ErrClosed) || go_errors.Is(err, io.EOF) { + if err != nil { + if go_errors.Is(err, net.ErrClosed) { break } continue diff --git a/transport/internet/finalmask/xdns/server.go b/transport/internet/finalmask/xdns/server.go index ec2f18f9a8ac..775e6e1600da 100644 --- a/transport/internet/finalmask/xdns/server.go +++ b/transport/internet/finalmask/xdns/server.go @@ -156,8 +156,8 @@ func (c *xdnsConnServer) recvLoop() { } n, addr, err := c.PacketConn.ReadFrom(buf[:]) - if err != nil || n == 0 { - if go_errors.Is(err, net.ErrClosed) || go_errors.Is(err, io.EOF) { + if err != nil { + if go_errors.Is(err, net.ErrClosed) { break } continue @@ -321,7 +321,7 @@ func (c *xdnsConnServer) sendLoop() { } _, err = c.PacketConn.WriteTo(buf, rec.Addr) - if go_errors.Is(err, net.ErrClosed) || go_errors.Is(err, io.ErrClosedPipe) { + if go_errors.Is(err, net.ErrClosed) { c.closed = true break } From e2c672bb1c7937470cdd8c7ce22e5f4ca9f39400 Mon Sep 17 00:00:00 2001 From: null Date: Wed, 1 Apr 2026 21:13:14 +0800 Subject: [PATCH 14/25] server sendLoop --- transport/internet/finalmask/xdns/server.go | 3 ++- transport/internet/finalmask/xicmp/server.go | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/transport/internet/finalmask/xdns/server.go b/transport/internet/finalmask/xdns/server.go index 775e6e1600da..13a07e26cc5f 100644 --- a/transport/internet/finalmask/xdns/server.go +++ b/transport/internet/finalmask/xdns/server.go @@ -322,10 +322,11 @@ func (c *xdnsConnServer) sendLoop() { _, err = c.PacketConn.WriteTo(buf, rec.Addr) if go_errors.Is(err, net.ErrClosed) { - c.closed = true break } } + + c.closed = true } func (c *xdnsConnServer) ReadFrom(p []byte) (n int, addr net.Addr, err error) { diff --git a/transport/internet/finalmask/xicmp/server.go b/transport/internet/finalmask/xicmp/server.go index 94012f019802..d293e75d92e1 100644 --- a/transport/internet/finalmask/xicmp/server.go +++ b/transport/internet/finalmask/xicmp/server.go @@ -311,6 +311,8 @@ func (c *xicmpConnServer) sendLoop() { errors.LogDebug(context.Background(), rec.addr, " ", rec.id, " ", rec.seq, " xicmp writeto err ", err) } } + + c.closed = true } func (c *xicmpConnServer) ReadFrom(p []byte) (n int, addr net.Addr, err error) { From 394649a63f5f5878c89ca33a8eca3acec75b94dd Mon Sep 17 00:00:00 2001 From: null Date: Thu, 2 Apr 2026 07:59:12 +0800 Subject: [PATCH 15/25] atomic --- transport/internet/finalmask/xdns/client.go | 18 ++++++------------ transport/internet/finalmask/xdns/server.go | 3 +-- transport/internet/finalmask/xicmp/server.go | 2 -- 3 files changed, 7 insertions(+), 16 deletions(-) diff --git a/transport/internet/finalmask/xdns/client.go b/transport/internet/finalmask/xdns/client.go index c4acda4913a1..3ea9ca88eb8a 100644 --- a/transport/internet/finalmask/xdns/client.go +++ b/transport/internet/finalmask/xdns/client.go @@ -11,6 +11,7 @@ import ( "net" "strconv" "sync" + "sync/atomic" "time" "github.com/xtls/xray-core/common" @@ -39,7 +40,7 @@ type xdnsConnClient struct { resolverConns []net.PacketConn resolverAddrs []*net.UDPAddr resolverIdx uint32 - resolverSend []uint32 + resolverSend []atomic.Uint32 clientID []byte domain Name @@ -103,7 +104,7 @@ func NewConnClient(c *Config, raw net.PacketConn) (net.PacketConn, error) { conn.resolverConns = append(conn.resolverConns, uc) conn.resolverAddrs = append(conn.resolverAddrs, &net.UDPAddr{IP: ip, Port: port}) } - conn.resolverSend = make([]uint32, len(conn.resolverConns)) + conn.resolverSend = make([]atomic.Uint32, len(conn.resolverConns)) go conn.recvLoop() go conn.sendLoop() @@ -164,9 +165,7 @@ func (c *xdnsConnClient) recvLoop() { } if anyPacket { - c.mutex.Lock() - c.resolverSend[i] = 0 - c.mutex.Unlock() + c.resolverSend[i].Swap(0) select { case c.pollChan <- struct{}{}: @@ -238,9 +237,7 @@ func (c *xdnsConnClient) sendLoop() { return } - c.mutex.Lock() - c.resolverSend[c.resolverIdx] += 1 - c.mutex.Unlock() + c.resolverSend[c.resolverIdx].Add(1) _, _ = c.resolverConns[c.resolverIdx].WriteTo(p.p, c.resolverAddrs[c.resolverIdx]) cur := c.resolverIdx for { @@ -249,10 +246,7 @@ func (c *xdnsConnClient) sendLoop() { if c.resolverIdx == cur { break } - c.mutex.Lock() - alive := c.resolverSend[c.resolverIdx] <= 3 - c.mutex.Unlock() - if alive { + if c.resolverSend[c.resolverIdx].Load() <= 3 { break } } diff --git a/transport/internet/finalmask/xdns/server.go b/transport/internet/finalmask/xdns/server.go index 13a07e26cc5f..775e6e1600da 100644 --- a/transport/internet/finalmask/xdns/server.go +++ b/transport/internet/finalmask/xdns/server.go @@ -322,11 +322,10 @@ func (c *xdnsConnServer) sendLoop() { _, err = c.PacketConn.WriteTo(buf, rec.Addr) if go_errors.Is(err, net.ErrClosed) { + c.closed = true break } } - - c.closed = true } func (c *xdnsConnServer) ReadFrom(p []byte) (n int, addr net.Addr, err error) { diff --git a/transport/internet/finalmask/xicmp/server.go b/transport/internet/finalmask/xicmp/server.go index d293e75d92e1..94012f019802 100644 --- a/transport/internet/finalmask/xicmp/server.go +++ b/transport/internet/finalmask/xicmp/server.go @@ -311,8 +311,6 @@ func (c *xicmpConnServer) sendLoop() { errors.LogDebug(context.Background(), rec.addr, " ", rec.id, " ", rec.seq, " xicmp writeto err ", err) } } - - c.closed = true } func (c *xicmpConnServer) ReadFrom(p []byte) (n int, addr net.Addr, err error) { From 6505b724910fb5a3f2f56b3ab5371d4480616dc5 Mon Sep 17 00:00:00 2001 From: null Date: Thu, 2 Apr 2026 12:01:36 +0800 Subject: [PATCH 16/25] SetDeadline --- transport/internet/finalmask/xdns/client.go | 31 +++++++++++++++++++-- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/transport/internet/finalmask/xdns/client.go b/transport/internet/finalmask/xdns/client.go index 3ea9ca88eb8a..91670f5b43de 100644 --- a/transport/internet/finalmask/xdns/client.go +++ b/transport/internet/finalmask/xdns/client.go @@ -36,7 +36,7 @@ type packet struct { } type xdnsConnClient struct { - net.PacketConn + conn net.PacketConn resolverConns []net.PacketConn resolverAddrs []*net.UDPAddr resolverIdx uint32 @@ -64,7 +64,7 @@ func NewConnClient(c *Config, raw net.PacketConn) (net.PacketConn, error) { } conn := &xdnsConnClient{ - PacketConn: raw, + conn: raw, clientID: make([]byte, 8), domain: domain, @@ -297,7 +297,32 @@ func (c *xdnsConnClient) Close() error { for _, rc := range c.resolverConns { rc.Close() } - return c.PacketConn.Close() + return c.conn.Close() +} + +func (c *xdnsConnClient) LocalAddr() net.Addr { + return c.conn.LocalAddr() +} + +func (c *xdnsConnClient) SetDeadline(t time.Time) error { + for _, rc := range c.resolverConns { + rc.SetDeadline(t) + } + return c.conn.SetDeadline(t) +} + +func (c *xdnsConnClient) SetReadDeadline(t time.Time) error { + for _, rc := range c.resolverConns { + rc.SetReadDeadline(t) + } + return c.conn.SetReadDeadline(t) +} + +func (c *xdnsConnClient) SetWriteDeadline(t time.Time) error { + for _, rc := range c.resolverConns { + rc.SetWriteDeadline(t) + } + return c.conn.SetWriteDeadline(t) } func encode(p []byte, clientID []byte, domain Name) ([]byte, error) { From cb657f85590f12fa536c21b37f8dcaa5f8f76cb8 Mon Sep 17 00:00:00 2001 From: null Date: Thu, 2 Apr 2026 17:24:05 +0800 Subject: [PATCH 17/25] valid response --- transport/internet/finalmask/xdns/client.go | 80 +++++++++++---------- 1 file changed, 44 insertions(+), 36 deletions(-) diff --git a/transport/internet/finalmask/xdns/client.go b/transport/internet/finalmask/xdns/client.go index 91670f5b43de..f1c0f6ae69e8 100644 --- a/transport/internet/finalmask/xdns/client.go +++ b/transport/internet/finalmask/xdns/client.go @@ -58,24 +58,9 @@ func NewConnClient(c *Config, raw net.PacketConn) (net.PacketConn, error) { return nil, errors.New("empty resolvers") } - domain, err := ParseName(c.Domain) - if err != nil { - return nil, err - } - - conn := &xdnsConnClient{ - conn: raw, - - clientID: make([]byte, 8), - domain: domain, - - pollChan: make(chan struct{}, pollLimit), - readQueue: make(chan *packet, 256), - writeQueue: make(chan *packet, 256), - } - - common.Must2(rand.Read(conn.clientID)) - + var resolverConns []net.PacketConn + var resolverAddrs []*net.UDPAddr + var resolverSend []atomic.Uint32 for _, rs := range c.Resolvers { h, p, err := net.SplitHostPort(rs) if err != nil { @@ -96,15 +81,36 @@ func NewConnClient(c *Config, raw net.PacketConn) (net.PacketConn, error) { uc, err = net.ListenPacket("udp6", ":0") } if err != nil { - for _, rc := range conn.resolverConns { + for _, rc := range resolverConns { rc.Close() } return nil, errors.New("failed to create resolver socket: ", err) } - conn.resolverConns = append(conn.resolverConns, uc) - conn.resolverAddrs = append(conn.resolverAddrs, &net.UDPAddr{IP: ip, Port: port}) + resolverConns = append(resolverConns, uc) + resolverAddrs = append(resolverAddrs, &net.UDPAddr{IP: ip, Port: port}) } - conn.resolverSend = make([]atomic.Uint32, len(conn.resolverConns)) + resolverSend = make([]atomic.Uint32, len(resolverConns)) + + domain, err := ParseName(c.Domain) + if err != nil { + return nil, err + } + + conn := &xdnsConnClient{ + conn: raw, + resolverConns: resolverConns, + resolverAddrs: resolverAddrs, + resolverSend: resolverSend, + + clientID: make([]byte, 8), + domain: domain, + + pollChan: make(chan struct{}, pollLimit), + readQueue: make(chan *packet, 256), + writeQueue: make(chan *packet, 256), + } + + common.Must2(rand.Read(conn.clientID)) go conn.recvLoop() go conn.sendLoop() @@ -141,7 +147,10 @@ func (c *xdnsConnClient) recvLoop() { continue } - payload := dnsResponsePayload(&resp, c.domain) + payload, valid := dnsResponsePayload(&resp, c.domain) + if valid { + c.resolverSend[i].Store(0) + } r := bytes.NewReader(payload) anyPacket := false @@ -165,8 +174,6 @@ func (c *xdnsConnClient) recvLoop() { } if anyPacket { - c.resolverSend[i].Swap(0) - select { case c.pollChan <- struct{}{}: default: @@ -414,31 +421,32 @@ func nextPacket(r *bytes.Reader) ([]byte, error) { return p, err } -func dnsResponsePayload(resp *Message, domain Name) []byte { +func dnsResponsePayload(resp *Message, domain Name) (payload []byte, valid bool) { + payload = nil + valid = false + if resp.Flags&0x8000 != 0x8000 { - return nil + return } if resp.Flags&0x000f != RcodeNoError { - return nil + return } if len(resp.Answer) != 1 { - return nil + return } answer := resp.Answer[0] _, ok := answer.Name.TrimSuffix(domain) if !ok { - return nil + return } if answer.Type != RRTypeTXT { - return nil - } - payload, err := DecodeRDataTXT(answer.Data) - if err != nil { - return nil + return } - return payload + valid = true + payload, _ = DecodeRDataTXT(answer.Data) + return } From 67d8adc04983b966476bf9e64fd053227d96e8d7 Mon Sep 17 00:00:00 2001 From: null Date: Sat, 4 Apr 2026 10:04:25 +0800 Subject: [PATCH 18/25] cwnd --- transport/internet/kcp/sending.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/transport/internet/kcp/sending.go b/transport/internet/kcp/sending.go index 5ba0fe99c4be..d2401ce2cd1d 100644 --- a/transport/internet/kcp/sending.go +++ b/transport/internet/kcp/sending.go @@ -321,7 +321,7 @@ func (w *SendingWorker) Flush(current uint32) { cwnd = w.controlWindow } - if !w.conn.Config.Congestion { + if w.conn.Config.Congestion { cwnd *= 20 } From 4b3465f6f64d938a47db3b7c46ef9f7fc922ec6c Mon Sep 17 00:00:00 2001 From: null Date: Mon, 6 Apr 2026 13:05:30 +0800 Subject: [PATCH 19/25] restore kcp --- transport/internet/kcp/connection.go | 2 +- transport/internet/kcp/sending.go | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/transport/internet/kcp/connection.go b/transport/internet/kcp/connection.go index 50ce79fdd856..90c4b7b807e3 100644 --- a/transport/internet/kcp/connection.go +++ b/transport/internet/kcp/connection.go @@ -610,7 +610,7 @@ func (c *Connection) flush() { if c.State() == StateTerminated { return } - if c.State() == StateActive && current-atomic.LoadUint32(&c.lastIncomingTime) >= 120000 { + if c.State() == StateActive && current-atomic.LoadUint32(&c.lastIncomingTime) >= 30000 { c.Close() } if c.State() == StateReadyToClose && c.sendingWorker.IsEmpty() { diff --git a/transport/internet/kcp/sending.go b/transport/internet/kcp/sending.go index d2401ce2cd1d..ac8e98c16c73 100644 --- a/transport/internet/kcp/sending.go +++ b/transport/internet/kcp/sending.go @@ -321,9 +321,7 @@ func (w *SendingWorker) Flush(current uint32) { cwnd = w.controlWindow } - if w.conn.Config.Congestion { - cwnd *= 20 - } + cwnd *= 20 // magic if !w.window.IsEmpty() { w.window.Flush(current, w.conn.roundTrip.Timeout(), cwnd) From 8142bb1ae9e267e7308e56e6688f1c9e2132a60a Mon Sep 17 00:00:00 2001 From: null Date: Mon, 6 Apr 2026 21:04:10 +0800 Subject: [PATCH 20/25] domains --- infra/conf/transport_internet.go | 6 ++- .../internet/finalmask/xdns/config.pb.go | 17 +++++++-- .../internet/finalmask/xdns/config.proto | 3 +- transport/internet/finalmask/xdns/dns_test.go | 1 + transport/internet/finalmask/xdns/server.go | 37 ++++++++++++++----- 5 files changed, 48 insertions(+), 16 deletions(-) diff --git a/infra/conf/transport_internet.go b/infra/conf/transport_internet.go index e3b60d162c6d..b3ec5c7ba4f9 100644 --- a/infra/conf/transport_internet.go +++ b/infra/conf/transport_internet.go @@ -1661,16 +1661,18 @@ func (c *Sudoku) Build() (proto.Message, error) { type Xdns struct { Domain string `json:"domain"` - Resolvers []string `json:"resolvers,omitempty"` + Domains []string `json:"domains"` + Resolvers []string `json:"resolvers"` } func (c *Xdns) Build() (proto.Message, error) { - if c.Domain == "" { + if c.Domain == "" && len(c.Domains) == 0 { return nil, errors.New("empty domain") } return &xdns.Config{ Domain: c.Domain, + Domains: c.Domains, Resolvers: c.Resolvers, }, nil } diff --git a/transport/internet/finalmask/xdns/config.pb.go b/transport/internet/finalmask/xdns/config.pb.go index c316ea757d0c..2446b8ecd61b 100644 --- a/transport/internet/finalmask/xdns/config.pb.go +++ b/transport/internet/finalmask/xdns/config.pb.go @@ -24,7 +24,8 @@ const ( type Config struct { state protoimpl.MessageState `protogen:"open.v1"` Domain string `protobuf:"bytes,1,opt,name=domain,proto3" json:"domain,omitempty"` - Resolvers []string `protobuf:"bytes,2,rep,name=resolvers,proto3" json:"resolvers,omitempty"` + Domains []string `protobuf:"bytes,2,rep,name=domains,proto3" json:"domains,omitempty"` + Resolvers []string `protobuf:"bytes,3,rep,name=resolvers,proto3" json:"resolvers,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -66,6 +67,13 @@ func (x *Config) GetDomain() string { return "" } +func (x *Config) GetDomains() []string { + if x != nil { + return x.Domains + } + return nil +} + func (x *Config) GetResolvers() []string { if x != nil { return x.Resolvers @@ -77,10 +85,11 @@ var File_transport_internet_finalmask_xdns_config_proto protoreflect.FileDescrip const file_transport_internet_finalmask_xdns_config_proto_rawDesc = "" + "\n" + - ".transport/internet/finalmask/xdns/config.proto\x12&xray.transport.internet.finalmask.xdns\">\n" + + ".transport/internet/finalmask/xdns/config.proto\x12&xray.transport.internet.finalmask.xdns\"X\n" + "\x06Config\x12\x16\n" + - "\x06domain\x18\x01 \x01(\tR\x06domain\x12\x1c\n" + - "\tresolvers\x18\x02 \x03(\tR\tresolversB\x94\x01\n" + + "\x06domain\x18\x01 \x01(\tR\x06domain\x12\x18\n" + + "\adomains\x18\x02 \x03(\tR\adomains\x12\x1c\n" + + "\tresolvers\x18\x03 \x03(\tR\tresolversB\x94\x01\n" + "*com.xray.transport.internet.finalmask.xdnsP\x01Z;github.com/xtls/xray-core/transport/internet/finalmask/xdns\xaa\x02&Xray.Transport.Internet.Finalmask.Xdnsb\x06proto3" var ( diff --git a/transport/internet/finalmask/xdns/config.proto b/transport/internet/finalmask/xdns/config.proto index 64f1e14e377d..2cbd08fc5538 100644 --- a/transport/internet/finalmask/xdns/config.proto +++ b/transport/internet/finalmask/xdns/config.proto @@ -8,6 +8,7 @@ option java_multiple_files = true; message Config { string domain = 1; - repeated string resolvers = 2; + repeated string domains = 2; + repeated string resolvers = 3; } diff --git a/transport/internet/finalmask/xdns/dns_test.go b/transport/internet/finalmask/xdns/dns_test.go index aa163476d9f1..2ddc9da50273 100644 --- a/transport/internet/finalmask/xdns/dns_test.go +++ b/transport/internet/finalmask/xdns/dns_test.go @@ -559,6 +559,7 @@ func TestEncodeRDataTXT(t *testing.T) { } fmt.Println(EncodeRDataTXT(nil)) + fmt.Println(computeMaxEncodedPayload(maxUDPPayload)) } func TestRDataTXTRoundTrip(t *testing.T) { diff --git a/transport/internet/finalmask/xdns/server.go b/transport/internet/finalmask/xdns/server.go index 775e6e1600da..d843735484cc 100644 --- a/transport/internet/finalmask/xdns/server.go +++ b/transport/internet/finalmask/xdns/server.go @@ -52,7 +52,7 @@ type queue struct { type xdnsConnServer struct { net.PacketConn - domain Name + domains []Name ch chan *record readQueue chan *packet @@ -63,15 +63,27 @@ type xdnsConnServer struct { } func NewConnServer(c *Config, raw net.PacketConn) (net.PacketConn, error) { - domain, err := ParseName(c.Domain) - if err != nil { - return nil, err + domains := make([]Name, 0, len(c.Domains)) + if len(c.Domains) == 0 { + domain, err := ParseName(c.Domain) + if err != nil { + return nil, err + } + domains = append(domains, domain) + } else { + for _, domain := range c.Domains { + domain, err := ParseName(domain) + if err != nil { + return nil, err + } + domains = append(domains, domain) + } } conn := &xdnsConnServer{ PacketConn: raw, - domain: domain, + domains: domains, ch: make(chan *record, 500), readQueue: make(chan *packet, 512), @@ -169,7 +181,7 @@ func (c *xdnsConnServer) recvLoop() { continue } - resp, payload := responseFor(&query, c.domain) + resp, payload := responseFor(&query, c.domains) var clientID [8]byte n = copy(clientID[:], payload) @@ -399,7 +411,7 @@ func nextPacketServer(r *bytes.Reader) ([]byte, error) { } } -func responseFor(query *Message, domain Name) (*Message, []byte) { +func responseFor(query *Message, domains []Name) (*Message, []byte) { resp := &Message{ ID: query.ID, Flags: 0x8000, @@ -447,7 +459,14 @@ func responseFor(query *Message, domain Name) (*Message, []byte) { } question := query.Question[0] - prefix, ok := question.Name.TrimSuffix(domain) + var prefix Name + var ok bool + for _, domain := range domains { + prefix, ok = question.Name.TrimSuffix(domain) + if ok { + break + } + } if !ok { resp.Flags |= RcodeNameError return resp, nil @@ -525,7 +544,7 @@ func computeMaxEncodedPayload(limit int) int { }, }, } - resp, _ := responseFor(query, [][]byte{}) + resp, _ := responseFor(query, []Name{[][]byte{}}) resp.Answer = []RR{ { From bb3304e3735d0137f6efa6b40e5a37a40ae30ed4 Mon Sep 17 00:00:00 2001 From: null Date: Tue, 7 Apr 2026 10:37:59 +0800 Subject: [PATCH 21/25] resolvers --- infra/conf/transport_internet.go | 6 --- transport/internet/finalmask/xdns/client.go | 43 +++++++++++++------ .../internet/finalmask/xdns/config.pb.go | 21 +++------ .../internet/finalmask/xdns/config.proto | 8 ++-- transport/internet/finalmask/xdns/server.go | 15 +++---- 5 files changed, 44 insertions(+), 49 deletions(-) diff --git a/infra/conf/transport_internet.go b/infra/conf/transport_internet.go index b3ec5c7ba4f9..6afbd00cb041 100644 --- a/infra/conf/transport_internet.go +++ b/infra/conf/transport_internet.go @@ -1660,18 +1660,12 @@ func (c *Sudoku) Build() (proto.Message, error) { } type Xdns struct { - Domain string `json:"domain"` Domains []string `json:"domains"` Resolvers []string `json:"resolvers"` } func (c *Xdns) Build() (proto.Message, error) { - if c.Domain == "" && len(c.Domains) == 0 { - return nil, errors.New("empty domain") - } - return &xdns.Config{ - Domain: c.Domain, Domains: c.Domains, Resolvers: c.Resolvers, }, nil diff --git a/transport/internet/finalmask/xdns/client.go b/transport/internet/finalmask/xdns/client.go index f1c0f6ae69e8..b4693fc6e1b5 100644 --- a/transport/internet/finalmask/xdns/client.go +++ b/transport/internet/finalmask/xdns/client.go @@ -10,6 +10,7 @@ import ( "io" "net" "strconv" + "strings" "sync" "sync/atomic" "time" @@ -43,7 +44,7 @@ type xdnsConnClient struct { resolverSend []atomic.Uint32 clientID []byte - domain Name + domains []Name pollChan chan struct{} readQueue chan *packet @@ -58,10 +59,25 @@ func NewConnClient(c *Config, raw net.PacketConn) (net.PacketConn, error) { return nil, errors.New("empty resolvers") } + var domains []Name + var servers []string + for _, rs := range c.Resolvers { + parts := strings.Split(rs, "+udp://") + if len(parts) != 2 { + return nil, errors.New("invalid resolvers") + } + domain, err := ParseName(parts[0]) + if err != nil { + return nil, err + } + domains = append(domains, domain) + servers = append(servers, parts[1]) + } + var resolverConns []net.PacketConn var resolverAddrs []*net.UDPAddr var resolverSend []atomic.Uint32 - for _, rs := range c.Resolvers { + for _, rs := range servers { h, p, err := net.SplitHostPort(rs) if err != nil { return nil, err @@ -91,11 +107,6 @@ func NewConnClient(c *Config, raw net.PacketConn) (net.PacketConn, error) { } resolverSend = make([]atomic.Uint32, len(resolverConns)) - domain, err := ParseName(c.Domain) - if err != nil { - return nil, err - } - conn := &xdnsConnClient{ conn: raw, resolverConns: resolverConns, @@ -103,7 +114,7 @@ func NewConnClient(c *Config, raw net.PacketConn) (net.PacketConn, error) { resolverSend: resolverSend, clientID: make([]byte, 8), - domain: domain, + domains: domains, pollChan: make(chan struct{}, pollLimit), readQueue: make(chan *packet, 256), @@ -147,7 +158,7 @@ func (c *xdnsConnClient) recvLoop() { continue } - payload, valid := dnsResponsePayload(&resp, c.domain) + payload, valid := dnsResponsePayload(&resp, c.domains) if valid { c.resolverSend[i].Store(0) } @@ -221,7 +232,7 @@ func (c *xdnsConnClient) sendLoop() { default: } } else { - encoded, _ := encode(nil, c.clientID, c.domain) + encoded, _ := encode(nil, c.clientID, c.domains[c.resolverIdx]) p = &packet{ p: encoded, } @@ -281,7 +292,7 @@ func (c *xdnsConnClient) WriteTo(p []byte, addr net.Addr) (n int, err error) { return 0, io.ErrClosedPipe } - encoded, err := encode(p, c.clientID, c.domain) + encoded, err := encode(p, c.clientID, c.domains[c.resolverIdx]) if err != nil { errors.LogDebug(context.Background(), addr, " xdns wireformat err ", err, " ", len(p)) return 0, nil @@ -421,7 +432,7 @@ func nextPacket(r *bytes.Reader) ([]byte, error) { return p, err } -func dnsResponsePayload(resp *Message, domain Name) (payload []byte, valid bool) { +func dnsResponsePayload(resp *Message, domains []Name) (payload []byte, valid bool) { payload = nil valid = false @@ -437,7 +448,13 @@ func dnsResponsePayload(resp *Message, domain Name) (payload []byte, valid bool) } answer := resp.Answer[0] - _, ok := answer.Name.TrimSuffix(domain) + var ok bool + for _, domain := range domains { + _, ok = answer.Name.TrimSuffix(domain) + if ok { + break + } + } if !ok { return } diff --git a/transport/internet/finalmask/xdns/config.pb.go b/transport/internet/finalmask/xdns/config.pb.go index 2446b8ecd61b..e1f06aa930d2 100644 --- a/transport/internet/finalmask/xdns/config.pb.go +++ b/transport/internet/finalmask/xdns/config.pb.go @@ -23,9 +23,8 @@ const ( type Config struct { state protoimpl.MessageState `protogen:"open.v1"` - Domain string `protobuf:"bytes,1,opt,name=domain,proto3" json:"domain,omitempty"` - Domains []string `protobuf:"bytes,2,rep,name=domains,proto3" json:"domains,omitempty"` - Resolvers []string `protobuf:"bytes,3,rep,name=resolvers,proto3" json:"resolvers,omitempty"` + Domains []string `protobuf:"bytes,1,rep,name=domains,proto3" json:"domains,omitempty"` + Resolvers []string `protobuf:"bytes,2,rep,name=resolvers,proto3" json:"resolvers,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -60,13 +59,6 @@ func (*Config) Descriptor() ([]byte, []int) { return file_transport_internet_finalmask_xdns_config_proto_rawDescGZIP(), []int{0} } -func (x *Config) GetDomain() string { - if x != nil { - return x.Domain - } - return "" -} - func (x *Config) GetDomains() []string { if x != nil { return x.Domains @@ -85,11 +77,10 @@ var File_transport_internet_finalmask_xdns_config_proto protoreflect.FileDescrip const file_transport_internet_finalmask_xdns_config_proto_rawDesc = "" + "\n" + - ".transport/internet/finalmask/xdns/config.proto\x12&xray.transport.internet.finalmask.xdns\"X\n" + - "\x06Config\x12\x16\n" + - "\x06domain\x18\x01 \x01(\tR\x06domain\x12\x18\n" + - "\adomains\x18\x02 \x03(\tR\adomains\x12\x1c\n" + - "\tresolvers\x18\x03 \x03(\tR\tresolversB\x94\x01\n" + + ".transport/internet/finalmask/xdns/config.proto\x12&xray.transport.internet.finalmask.xdns\"@\n" + + "\x06Config\x12\x18\n" + + "\adomains\x18\x01 \x03(\tR\adomains\x12\x1c\n" + + "\tresolvers\x18\x02 \x03(\tR\tresolversB\x94\x01\n" + "*com.xray.transport.internet.finalmask.xdnsP\x01Z;github.com/xtls/xray-core/transport/internet/finalmask/xdns\xaa\x02&Xray.Transport.Internet.Finalmask.Xdnsb\x06proto3" var ( diff --git a/transport/internet/finalmask/xdns/config.proto b/transport/internet/finalmask/xdns/config.proto index 2cbd08fc5538..b859b17aee11 100644 --- a/transport/internet/finalmask/xdns/config.proto +++ b/transport/internet/finalmask/xdns/config.proto @@ -7,8 +7,6 @@ option java_package = "com.xray.transport.internet.finalmask.xdns"; option java_multiple_files = true; message Config { - string domain = 1; - repeated string domains = 2; - repeated string resolvers = 3; -} - + repeated string domains = 1; + repeated string resolvers = 2; +} \ No newline at end of file diff --git a/transport/internet/finalmask/xdns/server.go b/transport/internet/finalmask/xdns/server.go index d843735484cc..c96149ad5c01 100644 --- a/transport/internet/finalmask/xdns/server.go +++ b/transport/internet/finalmask/xdns/server.go @@ -63,21 +63,16 @@ type xdnsConnServer struct { } func NewConnServer(c *Config, raw net.PacketConn) (net.PacketConn, error) { - domains := make([]Name, 0, len(c.Domains)) if len(c.Domains) == 0 { - domain, err := ParseName(c.Domain) + return nil, errors.New("empty domains") + } + domains := make([]Name, 0, len(c.Domains)) + for _, domain := range c.Domains { + domain, err := ParseName(domain) if err != nil { return nil, err } domains = append(domains, domain) - } else { - for _, domain := range c.Domains { - domain, err := ParseName(domain) - if err != nil { - return nil, err - } - domains = append(domains, domain) - } } conn := &xdnsConnServer{ From d5a8278f82db877cf839d0fd4f60166feefcd268 Mon Sep 17 00:00:00 2001 From: null Date: Tue, 7 Apr 2026 10:48:29 +0800 Subject: [PATCH 22/25] 1 --- transport/internet/finalmask/xdns/client.go | 31 ++++++++++----------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/transport/internet/finalmask/xdns/client.go b/transport/internet/finalmask/xdns/client.go index b4693fc6e1b5..5e22c35af370 100644 --- a/transport/internet/finalmask/xdns/client.go +++ b/transport/internet/finalmask/xdns/client.go @@ -158,10 +158,7 @@ func (c *xdnsConnClient) recvLoop() { continue } - payload, valid := dnsResponsePayload(&resp, c.domains) - if valid { - c.resolverSend[i].Store(0) - } + payload := dnsResponsePayload(&resp, c.domains) r := bytes.NewReader(payload) anyPacket := false @@ -185,6 +182,7 @@ func (c *xdnsConnClient) recvLoop() { } if anyPacket { + c.resolverSend[i].Store(0) select { case c.pollChan <- struct{}{}: default: @@ -292,7 +290,7 @@ func (c *xdnsConnClient) WriteTo(p []byte, addr net.Addr) (n int, err error) { return 0, io.ErrClosedPipe } - encoded, err := encode(p, c.clientID, c.domains[c.resolverIdx]) + encoded, err := encode(p, c.clientID, c.domains[c.resolverIdx%uint32(len(c.resolverConns))]) if err != nil { errors.LogDebug(context.Background(), addr, " xdns wireformat err ", err, " ", len(p)) return 0, nil @@ -432,19 +430,16 @@ func nextPacket(r *bytes.Reader) ([]byte, error) { return p, err } -func dnsResponsePayload(resp *Message, domains []Name) (payload []byte, valid bool) { - payload = nil - valid = false - +func dnsResponsePayload(resp *Message, domains []Name) []byte { if resp.Flags&0x8000 != 0x8000 { - return + return nil } if resp.Flags&0x000f != RcodeNoError { - return + return nil } if len(resp.Answer) != 1 { - return + return nil } answer := resp.Answer[0] @@ -456,14 +451,16 @@ func dnsResponsePayload(resp *Message, domains []Name) (payload []byte, valid bo } } if !ok { - return + return nil } if answer.Type != RRTypeTXT { - return + return nil + } + payload, err := DecodeRDataTXT(answer.Data) + if err != nil { + return nil } - valid = true - payload, _ = DecodeRDataTXT(answer.Data) - return + return payload } From 3f8df1bbf443a99b7a22b11a41958856576c0fec Mon Sep 17 00:00:00 2001 From: null Date: Tue, 7 Apr 2026 19:33:26 +0800 Subject: [PATCH 23/25] PrintRemovedFeatureError --- infra/conf/transport_internet.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/infra/conf/transport_internet.go b/infra/conf/transport_internet.go index 6afbd00cb041..2d4dc8562990 100644 --- a/infra/conf/transport_internet.go +++ b/infra/conf/transport_internet.go @@ -1660,11 +1660,17 @@ func (c *Sudoku) Build() (proto.Message, error) { } type Xdns struct { + Domain json.RawMessage `json:"domain"` + Domains []string `json:"domains"` Resolvers []string `json:"resolvers"` } func (c *Xdns) Build() (proto.Message, error) { + if c.Domain != nil { + return nil, errors.PrintRemovedFeatureError("domain", "domains(server) & resolvers(client)") + } + return &xdns.Config{ Domains: c.Domains, Resolvers: c.Resolvers, From db791b63919924d8b7c140935d63f87324cb619b Mon Sep 17 00:00:00 2001 From: null Date: Tue, 7 Apr 2026 19:52:27 +0800 Subject: [PATCH 24/25] infra --- infra/conf/transport_internet.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/infra/conf/transport_internet.go b/infra/conf/transport_internet.go index 2d4dc8562990..44eb448df631 100644 --- a/infra/conf/transport_internet.go +++ b/infra/conf/transport_internet.go @@ -1671,6 +1671,16 @@ func (c *Xdns) Build() (proto.Message, error) { return nil, errors.PrintRemovedFeatureError("domain", "domains(server) & resolvers(client)") } + if len(c.Domains) == 0 && len(c.Resolvers) == 0 { + return nil, errors.New("empty domains & empty resolvers") + } + + for _, r := range c.Resolvers { + if !strings.Contains(r, "+udp://") { + return nil, errors.New("invalid resolver ", r) + } + } + return &xdns.Config{ Domains: c.Domains, Resolvers: c.Resolvers, From a25961740586a53976bbfb2bb24f86a1a53459d9 Mon Sep 17 00:00:00 2001 From: null Date: Tue, 7 Apr 2026 20:35:04 +0800 Subject: [PATCH 25/25] healthCheck --- transport/internet/finalmask/xdns/client.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/transport/internet/finalmask/xdns/client.go b/transport/internet/finalmask/xdns/client.go index 5e22c35af370..6f8d97371937 100644 --- a/transport/internet/finalmask/xdns/client.go +++ b/transport/internet/finalmask/xdns/client.go @@ -253,16 +253,16 @@ func (c *xdnsConnClient) sendLoop() { return } - c.resolverSend[c.resolverIdx].Add(1) - _, _ = c.resolverConns[c.resolverIdx].WriteTo(p.p, c.resolverAddrs[c.resolverIdx]) cur := c.resolverIdx + curSend := c.resolverSend[c.resolverIdx].Add(1) + _, _ = c.resolverConns[c.resolverIdx].WriteTo(p.p, c.resolverAddrs[c.resolverIdx]) for { c.resolverIdx += 1 c.resolverIdx %= uint32(len(c.resolverConns)) if c.resolverIdx == cur { break } - if c.resolverSend[c.resolverIdx].Load() <= 3 { + if c.resolverSend[c.resolverIdx].Load() < curSend { break } }