diff --git a/app/log/command/config_grpc.pb.go b/app/log/command/config_grpc.pb.go index eb1c75c690a..3ce9850e6a6 100644 --- a/app/log/command/config_grpc.pb.go +++ b/app/log/command/config_grpc.pb.go @@ -81,10 +81,10 @@ type LoggerServiceServer interface { type UnimplementedLoggerServiceServer struct{} func (UnimplementedLoggerServiceServer) RestartLogger(context.Context, *RestartLoggerRequest) (*RestartLoggerResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method RestartLogger not implemented") + return nil, status.Error(codes.Unimplemented, "method RestartLogger not implemented") } func (UnimplementedLoggerServiceServer) FollowLog(*FollowLogRequest, grpc.ServerStreamingServer[FollowLogResponse]) error { - return status.Errorf(codes.Unimplemented, "method FollowLog not implemented") + return status.Error(codes.Unimplemented, "method FollowLog not implemented") } func (UnimplementedLoggerServiceServer) mustEmbedUnimplementedLoggerServiceServer() {} func (UnimplementedLoggerServiceServer) testEmbeddedByValue() {} @@ -97,7 +97,7 @@ type UnsafeLoggerServiceServer interface { } func RegisterLoggerServiceServer(s grpc.ServiceRegistrar, srv LoggerServiceServer) { - // If the following call pancis, it indicates UnimplementedLoggerServiceServer was + // If the following call panics, it indicates UnimplementedLoggerServiceServer was // embedded by pointer and is nil. This will cause panics if an // unimplemented method is ever invoked, so we test this at initialization // time to prevent it from happening at runtime later due to I/O. diff --git a/common/dualStack/fusedPacketConn/fusedPacketSocket.go b/common/dualStack/fusedPacketConn/fusedPacketSocket.go new file mode 100644 index 00000000000..e89f5d072af --- /dev/null +++ b/common/dualStack/fusedPacketConn/fusedPacketSocket.go @@ -0,0 +1,130 @@ +package fusedPacketConn + +import ( + "errors" + "time" + + "github.com/v2fly/v2ray-core/v5/common/net" +) + +var errClosed = errors.New("fused packet conn is closed") + +// FusedPacketConn combines two PacketConn socket to create a dual stack PacketConn +// When sending packet, the correct PacketConn for that destination address will be chosen +// When receiving packet, will receive packet from either socket +// Other operations will be done on both conn +type FusedPacketConn struct { + ipv6 net.PacketConn + ipv4 net.PacketConn + + readCh chan readResult + done chan struct{} + + localAddrPreferIPv6 bool +} + +type readResult struct { + data []byte + addr net.Addr + err error +} + +func NewFusedPacketConn(ipv4, ipv6 net.PacketConn, readBufSize int, localAddrPreferIPv6 bool) *FusedPacketConn { + f := &FusedPacketConn{ + ipv4: ipv4, + ipv6: ipv6, + readCh: make(chan readResult, 2), + done: make(chan struct{}), + localAddrPreferIPv6: localAddrPreferIPv6, + } + go f.readLoop(ipv4, readBufSize) + go f.readLoop(ipv6, readBufSize) + return f +} + +func (f *FusedPacketConn) readLoop(conn net.PacketConn, bufSize int) { + for { + buf := make([]byte, bufSize) + n, addr, err := conn.ReadFrom(buf) + select { + case <-f.done: + return + case f.readCh <- readResult{data: buf[:n], addr: addr, err: err}: + } + if err != nil { + return + } + } +} + +func (f *FusedPacketConn) ReadFrom(p []byte) (int, net.Addr, error) { + select { + case <-f.done: + return 0, nil, errClosed + case r := <-f.readCh: + if r.err != nil { + return 0, r.addr, r.err + } + n := copy(p, r.data) + return n, r.addr, nil + } +} + +func isIPv4Addr(addr net.Addr) bool { + udpAddr, ok := addr.(*net.UDPAddr) + if !ok { + return false + } + return udpAddr.IP.To4() != nil +} + +func (f *FusedPacketConn) WriteTo(p []byte, addr net.Addr) (int, error) { + if isIPv4Addr(addr) { + return f.ipv4.WriteTo(p, addr) + } + return f.ipv6.WriteTo(p, addr) +} + +func (f *FusedPacketConn) Close() error { + close(f.done) + err4 := f.ipv4.Close() + err6 := f.ipv6.Close() + if err4 != nil { + return err4 + } + return err6 +} + +func (f *FusedPacketConn) LocalAddr() net.Addr { + if f.localAddrPreferIPv6 { + return f.ipv6.LocalAddr() + } + return f.ipv4.LocalAddr() +} + +func (f *FusedPacketConn) SetDeadline(t time.Time) error { + err4 := f.ipv4.SetDeadline(t) + err6 := f.ipv6.SetDeadline(t) + if err4 != nil { + return err4 + } + return err6 +} + +func (f *FusedPacketConn) SetReadDeadline(t time.Time) error { + err4 := f.ipv4.SetReadDeadline(t) + err6 := f.ipv6.SetReadDeadline(t) + if err4 != nil { + return err4 + } + return err6 +} + +func (f *FusedPacketConn) SetWriteDeadline(t time.Time) error { + err4 := f.ipv4.SetWriteDeadline(t) + err6 := f.ipv6.SetWriteDeadline(t) + if err4 != nil { + return err4 + } + return err6 +} diff --git a/common/natTraversal/stun/filteredStunConnection.go b/common/natTraversal/stun/filteredStunConnection.go new file mode 100644 index 00000000000..ddd6dcedb27 --- /dev/null +++ b/common/natTraversal/stun/filteredStunConnection.go @@ -0,0 +1,29 @@ +package stun + +import ( + "github.com/pion/stun/v3" + + "github.com/v2fly/v2ray-core/v5/common/net" +) + +type STUNMessageCallback func(b []byte, addr net.Addr) + +func NewFilteredConnection(inner net.PacketConn, callback STUNMessageCallback) (*FilteredConnection, error) { + return &FilteredConnection{ + PacketConn: inner, + stunMsgCallback: callback, + }, nil +} + +type FilteredConnection struct { + net.PacketConn + stunMsgCallback STUNMessageCallback +} + +func (f *FilteredConnection) ReadFrom(b []byte) (int, net.Addr, error) { + n, addr, err := f.PacketConn.ReadFrom(b) + if stun.IsMessage(b[:n]) { + f.stunMsgCallback(b[:n], addr) + } + return n, addr, err +} diff --git a/common/natTraversal/stun/natTypeTest.go b/common/natTraversal/stun/natTypeTest.go new file mode 100644 index 00000000000..5a01c8d5fec --- /dev/null +++ b/common/natTraversal/stun/natTypeTest.go @@ -0,0 +1,656 @@ +package stun + +// Mostly Machine Generated Code +import ( + "context" + "encoding/binary" + "errors" + "net" + "sync" + "time" + + "github.com/pion/stun/v3" + + "github.com/v2fly/v2ray-core/v5/common/task" +) + +type NATDependantType int + +const ( + Unknown NATDependantType = iota + Independent + EndpointDependent + EndpointPortDependent + EndpointPortDependentPinned +) + +type NATYesOrNoUnknownType int + +const ( + NATYesOrNoUnknownType_Unknown NATYesOrNoUnknownType = iota + NATYesOrNoUnknownType_Yes + NATYesOrNoUnknownType_No +) + +type NATTypeTest struct { + newStunConn func() (net.PacketConn, error) + testsTranscript []TestConducted + transcriptMux sync.Mutex + + Timeout time.Duration + Attempts int + + FilterBehaviour NATDependantType + MappingBehaviour NATDependantType + HairpinBehaviour NATYesOrNoUnknownType + + StableMappingOnSecondaryServer NATYesOrNoUnknownType + + // Calculated values from testsTranscript + PreserveSourcePortWhenSourceNATMapping NATYesOrNoUnknownType + SingleSourceIPSourceNATMapping NATYesOrNoUnknownType + // PreserveSourceIPPortWhenDestNATMapping + // means when receiving packets, + // whether the real source address is preserved in the reply message + // some time a bad proxy would fill in a default value rather the real remote address + // this can be detected when asking remote server to reply from a different ip or port + PreserveSourceIPPortWhenDestNATMapping NATYesOrNoUnknownType + + TestServer net.Addr + TestServerSecondary net.Addr + + SourceIPs []net.IP +} + +func NewNATTypeTest(newStunConn func() (net.PacketConn, error), testServer net.Addr, testServerSecondary net.Addr, timeout time.Duration, attempts int) *NATTypeTest { + return &NATTypeTest{ + newStunConn: newStunConn, + Timeout: timeout, + Attempts: attempts, + TestServer: testServer, + TestServerSecondary: testServerSecondary, + } +} + +type TestConducted struct { + Req stun.Message + ReqSentTo net.Addr + ReqSentFrom net.Addr + Resp *stun.Message + RespFrom net.Addr +} + +func changeRequestSetter(changeIP, changePort bool) stun.RawAttribute { + val := make([]byte, 4) + var flags uint32 + if changeIP { + flags |= 0x04 + } + if changePort { + flags |= 0x02 + } + binary.BigEndian.PutUint32(val, flags) + return stun.RawAttribute{ + Type: stun.AttrChangeRequest, + Value: val, + } +} + +func startBackgroundReader(conn *StunClientConn) { + go func() { + buf := make([]byte, 1500) + for { + _, _, err := conn.ReadFrom(buf) + if err != nil { + return + } + } + }() +} + +func (t *NATTypeTest) recordTransaction(tc TestConducted) { + t.transcriptMux.Lock() + defer t.transcriptMux.Unlock() + t.testsTranscript = append(t.testsTranscript, tc) +} + +// doTransactionWithRetry sends multiple STUN requests at once (each with a fresh +// transaction ID) and waits for the first response within a single timeout window. +// This avoids sequential retry delays caused by UDP packet loss. +// Non-timeout errors from sending are returned immediately. +func (t *NATTypeTest) doTransactionWithRetry(conn *StunClientConn, localAddr net.Addr, dest net.Addr, attempts int, setters ...stun.Setter) (*stun.Message, net.Addr, error) { //nolint:unparam + type result struct { + msg stun.Message + addr net.Addr + } + ch := make(chan result, attempts) + + var txIDs []stunTransactionID + var firstMsg *stun.Message + + for i := 0; i < attempts; i++ { + msg := stun.MustBuild(setters...) + if i == 0 { + firstMsg = msg + } + + _, _, err := conn.ExecuteSTUNMessageAsync(*msg, dest, func(_ [stun.TransactionIDSize]byte, respMsg stun.Message, respAddr net.Addr) { + ch <- result{msg: respMsg, addr: respAddr} + }) + if err != nil { + for _, id := range txIDs { + conn.processor.CancelTransaction(id) + } + return nil, nil, err + } + txIDs = append(txIDs, msg.TransactionID) + } + + // Wait for first response or timeout + var resp *result + select { + case r := <-ch: + resp = &r + case <-time.After(t.Timeout): + } + + // Cancel all remaining pending transactions + for _, id := range txIDs { + conn.processor.CancelTransaction(id) + } + + // Record result + if resp != nil { + respMsg := resp.msg + t.recordTransaction(TestConducted{ + Req: *firstMsg, + ReqSentTo: dest, + ReqSentFrom: localAddr, + Resp: &respMsg, + RespFrom: resp.addr, + }) + return &respMsg, resp.addr, nil + } + + t.recordTransaction(TestConducted{ + Req: *firstMsg, + ReqSentTo: dest, + ReqSentFrom: localAddr, + }) + return nil, nil, ErrTimeout +} + +// TestFilterBehaviour determines NAT filtering behavior per RFC 5780 Section 4.4. +func (t *NATTypeTest) TestFilterBehaviour() error { + rawConn, err := t.newStunConn() + if err != nil { + return err + } + + conn, err := NewStunClientConn(rawConn) + if err != nil { + rawConn.Close() + return err + } + defer conn.Close() + localAddr := rawConn.LocalAddr() + startBackgroundReader(conn) + + // Test I: Regular binding to confirm connectivity and get OTHER-ADDRESS + resp1, _, err := t.doTransactionWithRetry(conn, localAddr, t.TestServer, t.Attempts, + stun.TransactionID, stun.BindingRequest) + if err != nil { + return err + } + + // Check if server supports RFC 5780 (OTHER-ADDRESS). + // Without it, CHANGE-REQUEST results are unreliable. + var filterOtherAddr stun.OtherAddress + if err := filterOtherAddr.GetFrom(resp1); err != nil { + t.FilterBehaviour = Unknown + return nil + } + + // Test II: Request server to respond from different IP and port + _, _, err = t.doTransactionWithRetry(conn, localAddr, t.TestServer, t.Attempts, + stun.TransactionID, stun.BindingRequest, changeRequestSetter(true, true)) + if err == nil { + t.FilterBehaviour = Independent + return nil + } + if !errors.Is(err, ErrTimeout) { + return err + } + + // Test III: Request server to respond from different port only + _, _, err = t.doTransactionWithRetry(conn, localAddr, t.TestServer, t.Attempts, + stun.TransactionID, stun.BindingRequest, changeRequestSetter(false, true)) + if err == nil { + t.FilterBehaviour = EndpointDependent + return nil + } + if !errors.Is(err, ErrTimeout) { + return err + } + + // Test IV: Check if sending outbound UDP can open the filter for a new endpoint. + // Send a binding to the alternative address to create a NAT filter entry, + // then ask the original server to reply from that alternative address. + // If the response arrives, the filter can be opened by outbound packets. + altAddr := &net.UDPAddr{IP: filterOtherAddr.IP, Port: filterOtherAddr.Port} + + // Send binding to alt address to open the NAT filter for that endpoint + _, _, err = t.doTransactionWithRetry(conn, localAddr, altAddr, t.Attempts, + stun.TransactionID, stun.BindingRequest) + if err != nil && !errors.Is(err, ErrTimeout) { + return err + } + + // Now ask original server to reply from the alternative address + _, _, err = t.doTransactionWithRetry(conn, localAddr, t.TestServer, t.Attempts, + stun.TransactionID, stun.BindingRequest, changeRequestSetter(true, true)) + if err == nil { + t.FilterBehaviour = EndpointPortDependent + return nil + } + if !errors.Is(err, ErrTimeout) { + return err + } + + t.FilterBehaviour = EndpointPortDependentPinned + return nil +} + +// TestMappingBehaviour determines NAT mapping behavior per RFC 5780 Section 4.3. +func (t *NATTypeTest) TestMappingBehaviour() error { + rawConn, err := t.newStunConn() + if err != nil { + return err + } + + conn, err := NewStunClientConn(rawConn) + if err != nil { + rawConn.Close() + return err + } + defer conn.Close() + localAddr := rawConn.LocalAddr() + startBackgroundReader(conn) + + // Test I: Regular binding to primary server + resp1, _, err := t.doTransactionWithRetry(conn, localAddr, t.TestServer, t.Attempts, + stun.TransactionID, stun.BindingRequest) + if err != nil { + return err + } + + var mappedAddr1 stun.XORMappedAddress + if err := mappedAddr1.GetFrom(resp1); err != nil { + return err + } + + var otherAddr stun.OtherAddress + if err := otherAddr.GetFrom(resp1); err != nil { + // Server does not support RFC 5780 (no OTHER-ADDRESS), cannot test mapping + t.MappingBehaviour = Unknown + return nil + } + + // Test II: From same socket, binding to OTHER-ADDRESS (different IP and port) + altAddr := &net.UDPAddr{IP: otherAddr.IP, Port: otherAddr.Port} + resp2, _, err := t.doTransactionWithRetry(conn, localAddr, altAddr, t.Attempts, + stun.TransactionID, stun.BindingRequest) + if err != nil { + return err + } + + var mappedAddr2 stun.XORMappedAddress + if err := mappedAddr2.GetFrom(resp2); err != nil { + return err + } + + if mappedAddr1.String() == mappedAddr2.String() { + t.MappingBehaviour = Independent + return nil + } + + // Test III: From same socket, binding to (other IP, original port) + serverUDP, ok := t.TestServer.(*net.UDPAddr) + if !ok { + return errors.New("TestServer is not a UDP address") + } + altAddr2 := &net.UDPAddr{IP: otherAddr.IP, Port: serverUDP.Port} + resp3, _, err := t.doTransactionWithRetry(conn, localAddr, altAddr2, t.Attempts, + stun.TransactionID, stun.BindingRequest) + if err != nil { + return err + } + + var mappedAddr3 stun.XORMappedAddress + if err := mappedAddr3.GetFrom(resp3); err != nil { + return err + } + + if mappedAddr2.String() == mappedAddr3.String() { + t.MappingBehaviour = EndpointDependent + } else { + t.MappingBehaviour = EndpointPortDependent + } + return nil +} + +func (t *NATTypeTest) TestMappingBehaviourWithSecondaryServer() error { + if t.TestServerSecondary == nil { + t.StableMappingOnSecondaryServer = NATYesOrNoUnknownType_Unknown + return nil + } + + rawConn, err := t.newStunConn() + if err != nil { + return err + } + + conn, err := NewStunClientConn(rawConn) + if err != nil { + rawConn.Close() + return err + } + defer conn.Close() + localAddr := rawConn.LocalAddr() + startBackgroundReader(conn) + + // Binding to primary server + resp1, _, err := t.doTransactionWithRetry(conn, localAddr, t.TestServer, t.Attempts, + stun.TransactionID, stun.BindingRequest) + if err != nil { + return err + } + + var mappedAddr1 stun.XORMappedAddress + if err := mappedAddr1.GetFrom(resp1); err != nil { + return err + } + + // Binding to secondary server from the same socket + resp2, _, err := t.doTransactionWithRetry(conn, localAddr, t.TestServerSecondary, t.Attempts, + stun.TransactionID, stun.BindingRequest) + if err != nil { + return err + } + + var mappedAddr2 stun.XORMappedAddress + if err := mappedAddr2.GetFrom(resp2); err != nil { + return err + } + + if mappedAddr1.String() == mappedAddr2.String() { + t.StableMappingOnSecondaryServer = NATYesOrNoUnknownType_Yes + } else { + t.StableMappingOnSecondaryServer = NATYesOrNoUnknownType_No + } + + return nil +} + +// TestHairpinBehaviour determines if the NAT supports hairpinning per RFC 5780 Section 4.5. +// Both sockets must first get their mapped addresses via STUN, then send to each other's +// mapped address. This ensures the NAT filter is opened for the peer's mapped address +// before the hairpin test packet arrives, avoiding false negatives from filtering. +func (t *NATTypeTest) TestHairpinBehaviour() error { + // Socket 1: get mapped address + rawConn1, err := t.newStunConn() + if err != nil { + return err + } + conn1, err := NewStunClientConn(rawConn1) + if err != nil { + rawConn1.Close() + return err + } + defer conn1.Close() + localAddr1 := rawConn1.LocalAddr() + startBackgroundReader(conn1) + + resp1, _, err := t.doTransactionWithRetry(conn1, localAddr1, t.TestServer, t.Attempts, + stun.TransactionID, stun.BindingRequest) + if err != nil { + return err + } + var mappedAddr1 stun.XORMappedAddress + if err := mappedAddr1.GetFrom(resp1); err != nil { + return err + } + selfAddr1 := &net.UDPAddr{IP: mappedAddr1.IP, Port: mappedAddr1.Port} + + // Socket 2: get mapped address + rawConn2, err := t.newStunConn() + if err != nil { + return err + } + conn2, err := NewStunClientConn(rawConn2) + if err != nil { + rawConn2.Close() + return err + } + defer conn2.Close() + localAddr2 := rawConn2.LocalAddr() + startBackgroundReader(conn2) + + resp2, _, err := t.doTransactionWithRetry(conn2, localAddr2, t.TestServer, t.Attempts, + stun.TransactionID, stun.BindingRequest) + if err != nil { + return err + } + var mappedAddr2 stun.XORMappedAddress + if err := mappedAddr2.GetFrom(resp2); err != nil { + return err + } + selfAddr2 := &net.UDPAddr{IP: mappedAddr2.IP, Port: mappedAddr2.Port} + + // Socket 1 sends to MA2 to open the NAT filter for MA2 on socket 1's side. + // Without this, a hairpinned packet from socket 2 (appearing as MA2) would be + // filtered by endpoint-dependent filtering on socket 1. + openMsg := stun.MustBuild(stun.TransactionID, stun.BindingRequest) + openMsg.Encode() + conn1.WriteTo(openMsg.Raw, selfAddr2) + + // Build hairpin test messages: register on conn1's processor, send from conn2. + // Hairpinned packets arrive at conn1 from MA2 (now allowed by filter). + type result struct { + msg stun.Message + addr net.Addr + } + ch := make(chan result, t.Attempts) + + var txIDs []stunTransactionID + var firstMsg *stun.Message + for i := 0; i < t.Attempts; i++ { + msg := stun.MustBuild(stun.TransactionID, stun.BindingRequest) + if i == 0 { + firstMsg = msg + } + msg.Encode() + + // Register on conn1's processor (hairpinned packet arrives at conn1) + conn1.processor.AddPendingTransactionListener(msg.TransactionID, func(_ [stun.TransactionIDSize]byte, respMsg stun.Message, respAddr net.Addr) { + ch <- result{msg: respMsg, addr: respAddr} + }) + txIDs = append(txIDs, msg.TransactionID) + + // Send from conn2 to MA1 (socket 1's mapped address) + if _, err := conn2.WriteTo(msg.Raw, selfAddr1); err != nil { + for _, id := range txIDs { + conn1.processor.CancelTransaction(id) + } + return err + } + } + + // Wait for hairpinned packet on conn1 + var respResult *result + select { + case r := <-ch: + respResult = &r + case <-time.After(t.Timeout): + } + + // Cancel all remaining pending transactions + for _, id := range txIDs { + conn1.processor.CancelTransaction(id) + } + + t.HairpinBehaviour = NATYesOrNoUnknownType_No + + if respResult != nil { + respMsg := respResult.msg + t.recordTransaction(TestConducted{ + Req: *firstMsg, + ReqSentTo: selfAddr1, + ReqSentFrom: localAddr2, + Resp: &respMsg, + RespFrom: respResult.addr, + }) + if respMsg.Type == stun.BindingRequest { + t.HairpinBehaviour = NATYesOrNoUnknownType_Yes + } + return nil + } + + t.recordTransaction(TestConducted{ + Req: *firstMsg, + ReqSentTo: selfAddr1, + ReqSentFrom: localAddr2, + }) + return nil +} + +// TestAll runs all NAT behavior tests in parallel, then calculates derived values. +func (t *NATTypeTest) TestAll() error { + err := task.Run(context.Background(), + t.TestFilterBehaviour, + t.TestMappingBehaviour, + t.TestHairpinBehaviour, + t.TestMappingBehaviourWithSecondaryServer, + ) + if err != nil { + return err + } + return t.CalcReminderValues() +} + +// CalcReminderValues derives additional NAT properties from the collected test transcripts. +func (t *NATTypeTest) CalcReminderValues() error { + t.transcriptMux.Lock() + transcripts := make([]TestConducted, len(t.testsTranscript)) + copy(transcripts, t.testsTranscript) + t.transcriptMux.Unlock() + + type addrKey struct { + ip string + port int + } + + var mappedAddrs []addrKey + for _, tc := range transcripts { + if tc.Resp == nil { + continue + } + var addr stun.XORMappedAddress + if err := addr.GetFrom(tc.Resp); err != nil { + continue + } + mappedAddrs = append(mappedAddrs, addrKey{ip: addr.IP.String(), port: addr.Port}) + } + + // Collect unique mapped source IPs + seenIPs := make(map[string]struct{}) + t.SourceIPs = nil + for _, m := range mappedAddrs { + if _, ok := seenIPs[m.ip]; !ok { + seenIPs[m.ip] = struct{}{} + t.SourceIPs = append(t.SourceIPs, net.ParseIP(m.ip)) + } + } + + // Need at least 2 mapped addresses to draw any meaningful comparison + if len(mappedAddrs) < 2 { + t.SingleSourceIPSourceNATMapping = NATYesOrNoUnknownType_Unknown + t.PreserveSourceIPPortWhenDestNATMapping = NATYesOrNoUnknownType_Unknown + t.PreserveSourcePortWhenSourceNATMapping = NATYesOrNoUnknownType_Unknown + return nil + } + + // SingleSourceIPSourceNATMapping: check if all mapped IPs are the same + allSameIP := true + for _, m := range mappedAddrs[1:] { + if m.ip != mappedAddrs[0].ip { + allSameIP = false + break + } + } + if allSameIP { + t.SingleSourceIPSourceNATMapping = NATYesOrNoUnknownType_Yes + } else { + t.SingleSourceIPSourceNATMapping = NATYesOrNoUnknownType_No + } + + allSendToMatchRespFrom := true + validPairCount := 0 + for _, tc := range transcripts { + if tc.Resp == nil || tc.RespFrom == nil || tc.ReqSentTo == nil { + continue + } + if value, ok := tc.Req.Attributes.Get(stun.AttrChangeRequest); ok { + if len(value.Value) != 4 || (value.Value[0] == 0 && value.Value[1] == 0 && value.Value[2] == 0 && value.Value[3] == 0) { + continue + } + } else { + continue + } + validPairCount++ + if tc.RespFrom.String() != tc.ReqSentTo.String() { + allSendToMatchRespFrom = false + break + } + } + switch { + case validPairCount < 1: + t.PreserveSourceIPPortWhenDestNATMapping = NATYesOrNoUnknownType_Unknown + case allSendToMatchRespFrom: + t.PreserveSourceIPPortWhenDestNATMapping = NATYesOrNoUnknownType_No + default: + t.PreserveSourceIPPortWhenDestNATMapping = NATYesOrNoUnknownType_Yes + } + + // PreserveSourcePortWhenSourceNATMapping: check if mapped port matches local source port + preserves := true + validCount := 0 + for _, tc := range transcripts { + if tc.Resp == nil || tc.ReqSentFrom == nil { + continue + } + localUDP, ok := tc.ReqSentFrom.(*net.UDPAddr) + if !ok || localUDP.Port == 0 { + continue + } + var addr stun.XORMappedAddress + if err := addr.GetFrom(tc.Resp); err != nil { + continue + } + validCount++ + if addr.Port != localUDP.Port { + preserves = false + } + } + if validCount >= 2 { + if preserves { + t.PreserveSourcePortWhenSourceNATMapping = NATYesOrNoUnknownType_Yes + } else { + t.PreserveSourcePortWhenSourceNATMapping = NATYesOrNoUnknownType_No + } + } else { + t.PreserveSourcePortWhenSourceNATMapping = NATYesOrNoUnknownType_Unknown + } + + return nil +} diff --git a/common/natTraversal/stun/processor.go b/common/natTraversal/stun/processor.go new file mode 100644 index 00000000000..b00280c7ba9 --- /dev/null +++ b/common/natTraversal/stun/processor.go @@ -0,0 +1,77 @@ +package stun + +import ( + "sync" + "time" + + "github.com/pion/stun/v3" + + "github.com/v2fly/v2ray-core/v5/common/net" +) + +type stunTransactionID = [stun.TransactionIDSize]byte + +type pendingTransaction struct { + handler PendingTransactionHandler + createdAt time.Time +} + +type Processor struct { + pendingStunRequest map[stunTransactionID]pendingTransaction + closed bool + mux sync.Mutex +} + +func NewProcessor() *Processor { + return &Processor{ + pendingStunRequest: make(map[stunTransactionID]pendingTransaction), + } +} + +func (p *Processor) HandleStunPacket(b []byte, addr net.Addr) { + var msg stun.Message + if err := stun.Decode(b, &msg); err != nil { + return + } + + p.mux.Lock() + pt, ok := p.pendingStunRequest[msg.TransactionID] + if ok { + delete(p.pendingStunRequest, msg.TransactionID) + } + p.mux.Unlock() + + if ok { + pt.handler(msg.TransactionID, msg, addr) + } +} + +type PendingTransactionHandler func(transactionID [stun.TransactionIDSize]byte, msg stun.Message, addr net.Addr) + +func (p *Processor) AddPendingTransactionListener(transactionID [stun.TransactionIDSize]byte, handler PendingTransactionHandler) { + p.mux.Lock() + defer p.mux.Unlock() + p.pendingStunRequest[transactionID] = pendingTransaction{ + handler: handler, + createdAt: time.Now(), + } +} + +func (p *Processor) CancelTransaction(transactionID [stun.TransactionIDSize]byte) { + p.mux.Lock() + defer p.mux.Unlock() + delete(p.pendingStunRequest, transactionID) +} + +func (p *Processor) ExpiredTransaction(newerThanThisTimeOrExpire time.Time) int { + p.mux.Lock() + defer p.mux.Unlock() + expired := 0 + for id, pt := range p.pendingStunRequest { + if pt.createdAt.Before(newerThanThisTimeOrExpire) { + delete(p.pendingStunRequest, id) + expired++ + } + } + return expired +} diff --git a/common/natTraversal/stun/stunClientConn.go b/common/natTraversal/stun/stunClientConn.go new file mode 100644 index 00000000000..bb1ed8d7aa6 --- /dev/null +++ b/common/natTraversal/stun/stunClientConn.go @@ -0,0 +1,64 @@ +package stun + +import ( + "errors" + "time" + + "github.com/pion/stun/v3" + + "github.com/v2fly/v2ray-core/v5/common/net" +) + +var ErrTimeout = errors.New("STUN transaction timed out") + +func NewStunClientConn(conn net.PacketConn) (*StunClientConn, error) { + processor := NewProcessor() + filtered, err := NewFilteredConnection(conn, processor.HandleStunPacket) + if err != nil { + return nil, err + } + return &StunClientConn{ + PacketConn: filtered, + processor: processor, + }, nil +} + +type StunClientConn struct { + net.PacketConn + processor *Processor +} + +func (conn *StunClientConn) ExecuteSTUNMessage(msg stun.Message, dest net.Addr, timeout time.Duration) (resp stun.Message, addr net.Addr, err error) { + type result struct { + msg stun.Message + addr net.Addr + } + ch := make(chan result, 1) + + _, _, err = conn.ExecuteSTUNMessageAsync(msg, dest, func(_ [stun.TransactionIDSize]byte, respMsg stun.Message, respAddr net.Addr) { + ch <- result{msg: respMsg, addr: respAddr} + }) + if err != nil { + return resp, nil, err + } + + select { + case r := <-ch: + return r.msg, r.addr, nil + case <-time.After(timeout): + conn.processor.CancelTransaction(msg.TransactionID) + return resp, nil, ErrTimeout + } +} + +func (conn *StunClientConn) ExecuteSTUNMessageAsync(msg stun.Message, dest net.Addr, callback PendingTransactionHandler) (resp stun.Message, addr net.Addr, err error) { + msg.Encode() + conn.processor.AddPendingTransactionListener(msg.TransactionID, callback) + + if _, err = conn.WriteTo(msg.Raw, dest); err != nil { + conn.processor.CancelTransaction(msg.TransactionID) + return resp, nil, err + } + + return resp, nil, nil +} diff --git a/common/natTraversal/stun/stuncli/stuncli.go b/common/natTraversal/stun/stuncli/stuncli.go new file mode 100644 index 00000000000..ae4532e25ad --- /dev/null +++ b/common/natTraversal/stun/stuncli/stuncli.go @@ -0,0 +1,264 @@ +package stuncli + +// Mostly machine generated code +import ( + "flag" + "fmt" + "net" + "time" + + "github.com/v2fly/v2ray-core/v5/common/buf" + stunlib "github.com/v2fly/v2ray-core/v5/common/natTraversal/stun" + vnet "github.com/v2fly/v2ray-core/v5/common/net" + "github.com/v2fly/v2ray-core/v5/main/commands/all/engineering" + "github.com/v2fly/v2ray-core/v5/main/commands/base" + "github.com/v2fly/v2ray-core/v5/proxy/socks" +) + +var ( + server *string + server2 *string + timeout *int + attempts *int + socks5udp *string +) + +var cmdStunTest = &base.Command{ + UsageLine: "{{.Exec}} engineering stun-nat-type-discovery", + Short: "run STUN NAT type tests", + Long: ` +Run STUN NAT behavior discovery tests (RFC 5780) against a STUN server. + +Tests NAT filtering, mapping, and hairpin behavior, then reports results. + +The STUN server must support RFC 5780 (OTHER-ADDRESS and CHANGE-REQUEST) +for full test coverage. + +Usage: + {{.Exec}} engineering stun-test -server [-server2 ] [-timeout ] [-attempts ] [-socks5udp ] + +Options: + -server + The STUN server address (required) + -server2 + A secondary STUN server address for cross-server mapping stability test + -timeout + Timeout per test in milliseconds (default: 3000) + -attempts + Number of parallel requests per test for UDP loss resilience (default: 3) + -socks5udp + SOCKS5 UDP relay address (skips TCP handshake, sends UDP directly) + +Example: + {{.Exec}} engineering stun-test -server stun.example.com:3478 + {{.Exec}} engineering stun-test -server stun.example.com:3478 -server2 stun2.example.com:3478 + {{.Exec}} engineering stun-test -server stun.example.com:3478 -socks5udp 127.0.0.1:1080 +`, + Flag: func() flag.FlagSet { + fs := flag.NewFlagSet("", flag.ExitOnError) + server = fs.String("server", "", "STUN server address (host:port)") + server2 = fs.String("server2", "", "secondary STUN server address (host:port)") + timeout = fs.Int("timeout", 3000, "timeout per test in milliseconds") + attempts = fs.Int("attempts", 3, "number of parallel requests per test") + socks5udp = fs.String("socks5udp", "", "SOCKS5 UDP relay address (host:port)") + return *fs + }(), + Run: executeStunTest, +} + +func init() { + engineering.AddCommand(cmdStunTest) +} + +// socks5UDPConn wraps a PacketConn to encapsulate/decapsulate SOCKS5 UDP packets. +// All outgoing packets are wrapped in a SOCKS5 UDP header and sent to the relay. +// All incoming packets are unwrapped, with the real source address extracted from the header. +type socks5UDPConn struct { + net.PacketConn + relayAddr net.Addr +} + +func (c *socks5UDPConn) WriteTo(p []byte, addr net.Addr) (int, error) { + udpAddr := addr.(*net.UDPAddr) + dest := vnet.UDPDestination(vnet.IPAddress(udpAddr.IP), vnet.Port(udpAddr.Port)) + packet, err := socks.EncodeUDPPacketFromAddress(dest, p) + if err != nil { + return 0, err + } + defer packet.Release() + _, err = c.PacketConn.WriteTo(packet.Bytes(), c.relayAddr) + if err != nil { + return 0, err + } + return len(p), nil +} + +func (c *socks5UDPConn) ReadFrom(p []byte) (int, net.Addr, error) { + // Allocate enough space for SOCKS5 header + payload + rawBuf := make([]byte, len(p)+256) + n, _, err := c.PacketConn.ReadFrom(rawBuf) + if err != nil { + return 0, nil, err + } + packet := buf.FromBytes(rawBuf[:n]) + req, err := socks.DecodeUDPPacket(packet) + if err != nil { + return 0, nil, err + } + // After DecodeUDPPacket, packet.Bytes() contains the payload + dataN := copy(p, packet.Bytes()) + srcAddr := &net.UDPAddr{ + IP: req.Address.IP(), + Port: int(req.Port), + } + return dataN, srcAddr, nil +} + +func natDependantTypeString(t stunlib.NATDependantType) string { + switch t { + case stunlib.Unknown: + return "Unknown" + case stunlib.Independent: + return "Independent" + case stunlib.EndpointDependent: + return "Endpoint Dependent" + case stunlib.EndpointPortDependent: + return "Endpoint+Port Dependent" + case stunlib.EndpointPortDependentPinned: + return "Endpoint+Port Dependent (Pinned)" + default: + return fmt.Sprintf("Unknown(%d)", t) + } +} + +func natYesOrNoString(t stunlib.NATYesOrNoUnknownType) string { + switch t { + case stunlib.NATYesOrNoUnknownType_Unknown: + return "Unknown" + case stunlib.NATYesOrNoUnknownType_Yes: + return "Yes" + case stunlib.NATYesOrNoUnknownType_No: + return "No" + default: + return fmt.Sprintf("Unknown(%d)", t) + } +} + +func executeStunTest(cmd *base.Command, args []string) { + err := cmd.Flag.Parse(args) + if err != nil { + base.Fatalf("failed to parse flags: %v", err) + } + + if *server == "" { + base.Fatalf("-server is required") + } + + host, portStr, err := net.SplitHostPort(*server) + if err != nil { + base.Fatalf("invalid server address %q: %v", *server, err) + } + + ips, err := net.ResolveIPAddr("ip", host) + if err != nil { + base.Fatalf("failed to resolve %q: %v", host, err) + } + + port, err := net.LookupPort("udp", portStr) + if err != nil { + base.Fatalf("invalid port %q: %v", portStr, err) + } + + serverAddr := &net.UDPAddr{IP: ips.IP, Port: port} + + // Resolve secondary STUN server address if provided + var server2Addr *net.UDPAddr + if *server2 != "" { + host2, portStr2, err := net.SplitHostPort(*server2) + if err != nil { + base.Fatalf("invalid server2 address %q: %v", *server2, err) + } + ips2, err := net.ResolveIPAddr("ip", host2) + if err != nil { + base.Fatalf("failed to resolve server2 host %q: %v", host2, err) + } + port2, err := net.LookupPort("udp", portStr2) + if err != nil { + base.Fatalf("invalid server2 port %q: %v", portStr2, err) + } + server2Addr = &net.UDPAddr{IP: ips2.IP, Port: port2} + } + + // Resolve SOCKS5 UDP relay address if provided + var relayAddr *net.UDPAddr + if *socks5udp != "" { + rHost, rPortStr, err := net.SplitHostPort(*socks5udp) + if err != nil { + base.Fatalf("invalid socks5udp address %q: %v", *socks5udp, err) + } + rIPs, err := net.ResolveIPAddr("ip", rHost) + if err != nil { + base.Fatalf("failed to resolve socks5udp host %q: %v", rHost, err) + } + rPort, err := net.LookupPort("udp", rPortStr) + if err != nil { + base.Fatalf("invalid socks5udp port %q: %v", rPortStr, err) + } + relayAddr = &net.UDPAddr{IP: rIPs.IP, Port: rPort} + } + + fmt.Printf("STUN server: %s\n", serverAddr) + if server2Addr != nil { + fmt.Printf("STUN server 2: %s\n", server2Addr) + } + if relayAddr != nil { + fmt.Printf("SOCKS5 UDP relay: %s\n", relayAddr) + } + fmt.Printf("Timeout: %dms, Attempts: %d\n\n", *timeout, *attempts) + + newConn := func() (net.PacketConn, error) { + conn, err := net.ListenPacket("udp", ":0") + if err != nil { + return nil, err + } + if relayAddr != nil { + return &socks5UDPConn{PacketConn: conn, relayAddr: relayAddr}, nil + } + return conn, nil + } + + var secondaryServer net.Addr + if server2Addr != nil { + secondaryServer = server2Addr + } + + test := stunlib.NewNATTypeTest( + newConn, + serverAddr, + secondaryServer, + time.Duration(*timeout)*time.Millisecond, + *attempts, + ) + + fmt.Println("Running tests...") + if err := test.TestAll(); err != nil { + base.Fatalf("test failed: %v", err) + } + + fmt.Println() + fmt.Println("=== NAT Behavior Test Results ===") + fmt.Printf(" Filter Behaviour: %s\n", natDependantTypeString(test.FilterBehaviour)) + fmt.Printf(" Mapping Behaviour: %s\n", natDependantTypeString(test.MappingBehaviour)) + fmt.Printf(" Hairpin Behaviour: %s\n", natYesOrNoString(test.HairpinBehaviour)) + fmt.Printf(" Stable Mapping on Secondary Server: %s\n", natYesOrNoString(test.StableMappingOnSecondaryServer)) + fmt.Println() + fmt.Println("=== Derived Properties ===") + fmt.Printf(" Preserve Source Port (Source NAT): %s\n", natYesOrNoString(test.PreserveSourcePortWhenSourceNATMapping)) + fmt.Printf(" Single Source IP (Source NAT): %s\n", natYesOrNoString(test.SingleSourceIPSourceNATMapping)) + fmt.Printf(" Preserve Source Addr (Dest NAT Reply): %s\n", natYesOrNoString(test.PreserveSourceIPPortWhenDestNATMapping)) + fmt.Println() + fmt.Println("=== Source IPs ===") + for _, ip := range test.SourceIPs { + fmt.Printf(" %s\n", ip) + } +} diff --git a/common/packetswitch/gvisorstack/config.pb.go b/common/packetswitch/gvisorstack/config.pb.go index 9563b88665d..70b8176aa2e 100644 --- a/common/packetswitch/gvisorstack/config.pb.go +++ b/common/packetswitch/gvisorstack/config.pb.go @@ -28,6 +28,7 @@ type Config struct { EnableSpoofing bool `protobuf:"varint,9,opt,name=enable_spoofing,json=enableSpoofing,proto3" json:"enable_spoofing,omitempty"` SocketSettings *internet.SocketConfig `protobuf:"bytes,10,opt,name=socket_settings,json=socketSettings,proto3" json:"socket_settings,omitempty"` PreferIpv6ForUdp bool `protobuf:"varint,11,opt,name=prefer_ipv6_for_udp,json=preferIpv6ForUdp,proto3" json:"prefer_ipv6_for_udp,omitempty"` + DualStackUdp bool `protobuf:"varint,12,opt,name=dual_stack_udp,json=dualStackUdp,proto3" json:"dual_stack_udp,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -118,11 +119,18 @@ func (x *Config) GetPreferIpv6ForUdp() bool { return false } +func (x *Config) GetDualStackUdp() bool { + if x != nil { + return x.DualStackUdp + } + return false +} + var File_common_packetswitch_gvisorstack_config_proto protoreflect.FileDescriptor const file_common_packetswitch_gvisorstack_config_proto_rawDesc = "" + "\n" + - ",common/packetswitch/gvisorstack/config.proto\x12*v2ray.core.common.packetswitch.gvisorstack\x1a$app/router/routercommon/common.proto\x1a\x1ftransport/internet/config.proto\x1a common/protoext/extensions.proto\"\x9d\x03\n" + + ",common/packetswitch/gvisorstack/config.proto\x12*v2ray.core.common.packetswitch.gvisorstack\x1a$app/router/routercommon/common.proto\x1a\x1ftransport/internet/config.proto\x1a common/protoext/extensions.proto\"\xc3\x03\n" + "\x06Config\x12\x10\n" + "\x03mtu\x18\x02 \x01(\rR\x03mtu\x12\x1d\n" + "\n" + @@ -133,7 +141,8 @@ const file_common_packetswitch_gvisorstack_config_proto_rawDesc = "" + "\x0fenable_spoofing\x18\t \x01(\bR\x0eenableSpoofing\x12T\n" + "\x0fsocket_settings\x18\n" + " \x01(\v2+.v2ray.core.transport.internet.SocketConfigR\x0esocketSettings\x12-\n" + - "\x13prefer_ipv6_for_udp\x18\v \x01(\bR\x10preferIpv6ForUdpB\x9f\x01\n" + + "\x13prefer_ipv6_for_udp\x18\v \x01(\bR\x10preferIpv6ForUdp\x12$\n" + + "\x0edual_stack_udp\x18\f \x01(\bR\fdualStackUdpB\x9f\x01\n" + ".com.v2ray.core.common.packetswitch.gvisorstackP\x01Z>github.com/v2fly/v2ray-core/v5/common/packetswitch/gvisorstack\xaa\x02*V2Ray.Core.Common.Packetswitch.Gvisorstackb\x06proto3" var ( diff --git a/common/packetswitch/gvisorstack/config.proto b/common/packetswitch/gvisorstack/config.proto index cd154b5d6ca..c53659faae8 100644 --- a/common/packetswitch/gvisorstack/config.proto +++ b/common/packetswitch/gvisorstack/config.proto @@ -19,4 +19,5 @@ message Config { bool enable_spoofing = 9; v2ray.core.transport.internet.SocketConfig socket_settings = 10; bool prefer_ipv6_for_udp = 11; + bool dual_stack_udp = 12; } \ No newline at end of file diff --git a/common/packetswitch/gvisorstack/dialer.go b/common/packetswitch/gvisorstack/dialer.go index 022a39bde51..f58b0306eaa 100644 --- a/common/packetswitch/gvisorstack/dialer.go +++ b/common/packetswitch/gvisorstack/dialer.go @@ -9,6 +9,7 @@ import ( "gvisor.dev/gvisor/pkg/tcpip/network/ipv4" "gvisor.dev/gvisor/pkg/tcpip/network/ipv6" + "github.com/v2fly/v2ray-core/v5/common/dualStack/fusedPacketConn" "github.com/v2fly/v2ray-core/v5/common/net" ) @@ -116,6 +117,20 @@ func (w *WrappedStack) ListenUDP(ctx context.Context, localAddress net.Destinati return udpConn, nil } + if w.config.DualStackUdp { + udpConn4, err := gonet.DialUDP(w.stack, nil, nil, ipv4.ProtocolNumber) + if err != nil { + return nil, fmt.Errorf("failed to create IPv4 UDP conn for dual stack: %w", err) + } + udpConn6, err := gonet.DialUDP(w.stack, nil, nil, ipv6.ProtocolNumber) + if err != nil { + udpConn4.Close() + return nil, fmt.Errorf("failed to create IPv6 UDP conn for dual stack: %w", err) + } + preferIPv6 := w.config.GetPreferIpv6ForUdp() + return fusedPacketConn.NewFusedPacketConn(udpConn4, udpConn6, int(w.config.Mtu), preferIPv6), nil + } + // If not specified, let the stack choose the local address (pass nil laddr). // Default network selection honors PreferIpv6ForUdp if configured. defaultNet := ipv4.ProtocolNumber diff --git a/go.mod b/go.mod index fac78407108..d57dbaa80e1 100644 --- a/go.mod +++ b/go.mod @@ -22,6 +22,7 @@ require ( github.com/mustafaturan/bus v1.0.2 github.com/pelletier/go-toml v1.9.5 github.com/pion/dtls/v2 v2.2.12 + github.com/pion/stun/v3 v3.1.1 github.com/pion/transport/v2 v2.2.10 github.com/pires/go-proxyproto v0.11.0 github.com/quic-go/quic-go v0.59.0 @@ -73,16 +74,19 @@ require ( github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 // indirect github.com/mustafaturan/monoton v1.0.0 // indirect github.com/patrickmn/go-cache v2.1.0+incompatible // indirect - github.com/pion/logging v0.2.2 // indirect + github.com/pion/dtls/v3 v3.0.10 // indirect + github.com/pion/logging v0.2.4 // indirect github.com/pion/randutil v0.1.0 // indirect github.com/pion/sctp v1.8.7 // indirect github.com/pion/transport/v3 v3.0.7 // indirect + github.com/pion/transport/v4 v4.0.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/quic-go/qpack v0.6.0 // indirect github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect github.com/rs/cors v1.7.0 // indirect github.com/secure-io/siv-go v0.0.0-20180922214919-5ff40651e2c4 // indirect github.com/stretchr/objx v0.5.2 // indirect + github.com/wlynxg/anet v0.0.5 // indirect github.com/xtaci/smux v1.5.24 // indirect golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect golang.org/x/mod v0.32.0 // indirect diff --git a/go.sum b/go.sum index 33138c02148..39bb0fbd951 100644 --- a/go.sum +++ b/go.sum @@ -387,13 +387,18 @@ github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi github.com/pion/dtls/v2 v2.0.0-rc.7/go.mod h1:U199DvHpRBN0muE9+tVN4TMy1jvEhZIZ63lk4xkvVSk= github.com/pion/dtls/v2 v2.2.12 h1:KP7H5/c1EiVAAKUmXyCzPiQe5+bCJrpOeKg/L05dunk= github.com/pion/dtls/v2 v2.2.12/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE= -github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= +github.com/pion/dtls/v3 v3.0.10 h1:k9ekkq1kaZoxnNEbyLKI8DI37j/Nbk1HWmMuywpQJgg= +github.com/pion/dtls/v3 v3.0.10/go.mod h1:YEmmBYIoBsY3jmG56dsziTv/Lca9y4Om83370CXfqJ8= github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= +github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8= +github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so= github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= github.com/pion/sctp v1.7.6/go.mod h1:ichkYQ5tlgCQwEwvgfdcAolqx1nHbYCxo4D7zK/K0X8= github.com/pion/sctp v1.8.7 h1:JnABvFakZueGAn4KU/4PSKg+GWbF6QWbKTWZOSGJjXw= github.com/pion/sctp v1.8.7/go.mod h1:g1Ul+ARqZq5JEmoFy87Q/4CePtKnTJ1QCL9dBBdN6AU= +github.com/pion/stun/v3 v3.1.1 h1:CkQxveJ4xGQjulGSROXbXq94TAWu8gIX2dT+ePhUkqw= +github.com/pion/stun/v3 v3.1.1/go.mod h1:qC1DfmcCTQjl9PBaMa5wSn3x9IPmKxSdcCsxBcDBndM= github.com/pion/transport v0.8.10/go.mod h1:tBmha/UCjpum5hqTWhfAEs3CO4/tHSg0MYRhSzR+CZ8= github.com/pion/transport/v2 v2.1.0/go.mod h1:AdSw4YBZVDkZm8fpoz+fclXyQwANWmZAlDuQdctTThQ= github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0= @@ -401,6 +406,8 @@ github.com/pion/transport/v2 v2.2.10 h1:ucLBLE8nuxiHfvkFKnkDQRYWYfp8ejf4YBOPfaQp github.com/pion/transport/v2 v2.2.10/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E= github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0= github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo= +github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o= +github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM= github.com/pires/go-proxyproto v0.11.0 h1:gUQpS85X/VJMdUsYyEgyn59uLJvGqPhJV5YvG68wXH4= github.com/pires/go-proxyproto v0.11.0/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -529,6 +536,8 @@ github.com/v2fly/struc v0.0.0-20241227015403-8e8fa1badfd6/go.mod h1:a/FYYQz8bW7w github.com/vincent-petithory/dataurl v1.0.0 h1:cXw+kPto8NLuJtlMsI152irrVw9fRDX8AbShPRpg2CI= github.com/vincent-petithory/dataurl v1.0.0/go.mod h1:FHafX5vmDzyP+1CQATJn7WFKc9CvnvxyvZy6I1MrG/U= github.com/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= +github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= +github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xiaokangwang/VLite v0.0.0-20220418190619-cff95160a432 h1:I/ATawgO2RerCq9ACwL0wBB8xNXZdE3J+93MCEHReRs= github.com/xiaokangwang/VLite v0.0.0-20220418190619-cff95160a432/go.mod h1:QN7Go2ftTVfx0aCTh9RXHV8pkpi0FtmbwQw40dy61wQ= diff --git a/main/distro/all/pion.go b/main/distro/all/pion.go new file mode 100644 index 00000000000..56b53a00f85 --- /dev/null +++ b/main/distro/all/pion.go @@ -0,0 +1,5 @@ +//go:build !android + +package all + +import _ "github.com/v2fly/v2ray-core/v5/common/natTraversal/stun/stuncli" diff --git a/proxy/wireguard/outbound/outbound.go b/proxy/wireguard/outbound/outbound.go index 3d170fc8f99..08d5eed9d30 100644 --- a/proxy/wireguard/outbound/outbound.go +++ b/proxy/wireguard/outbound/outbound.go @@ -252,7 +252,7 @@ func (w *WireguardOutbound) Process(ctx context.Context, link *transport.Link, d if packetConn, err := packetaddr.ToPacketAddrConn(link, destination); err == nil { defer func() { _ = packetConn.Close() }() - pc, err := sess.stack.ListenUDP(ctx, cnet.UDPDestination(cnet.AnyIP, 0)) + pc, err := sess.stack.ListenUDP(ctx, cnet.UDPDestination(nil, 0)) if err != nil { return newError("failed to create udp session in stack").Base(err) }