diff --git a/go.mod b/go.mod index d9d53c84..f81b214b 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.25.0 require ( github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 github.com/coreos/go-oidc/v3 v3.14.1 - github.com/fatedier/golib v0.7.0 + github.com/fatedier/golib v0.7.2-0.20260715094513-2780ae1d8cdb github.com/google/uuid v1.6.0 github.com/gorilla/mux v1.8.1 github.com/gorilla/websocket v1.5.0 @@ -13,7 +13,6 @@ require ( github.com/onsi/ginkgo/v2 v2.23.4 github.com/onsi/gomega v1.36.3 github.com/pelletier/go-toml/v2 v2.2.0 - github.com/pion/stun/v3 v3.1.1 github.com/pires/go-proxyproto v0.7.0 github.com/prometheus/client_golang v1.19.1 github.com/quic-go/quic-go v0.55.0 @@ -53,9 +52,6 @@ require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/klauspost/cpuid/v2 v2.2.6 // indirect github.com/klauspost/reedsolomon v1.12.0 // indirect - github.com/pion/dtls/v3 v3.0.10 // indirect - github.com/pion/logging v0.2.4 // indirect - github.com/pion/transport/v4 v4.0.1 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_model v0.5.0 // indirect @@ -67,7 +63,6 @@ require ( github.com/tidwall/pretty v1.2.0 // indirect github.com/tjfoc/gmsm v1.4.1 // indirect github.com/vishvananda/netns v0.0.4 // indirect - github.com/wlynxg/anet v0.0.5 // indirect go.uber.org/automaxprocs v1.6.0 // indirect golang.org/x/mod v0.33.0 // indirect golang.org/x/text v0.35.0 // indirect diff --git a/go.sum b/go.sum index 2fad6ea2..efffcbc1 100644 --- a/go.sum +++ b/go.sum @@ -20,8 +20,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/fatedier/golib v0.7.0 h1:tMDF9ObcwVt59VUHroJOzHQjVFPLymZVMpGm9WAVwhY= -github.com/fatedier/golib v0.7.0/go.mod h1:ArUGvPg2cOw/py2RAuBt46nNZH2VQ5Z70p109MAZpJw= +github.com/fatedier/golib v0.7.2-0.20260715094513-2780ae1d8cdb h1:6gcaE769mFb1G/cNV6lgzmwIYkNXmhj4pmcddllfmB4= +github.com/fatedier/golib v0.7.2-0.20260715094513-2780ae1d8cdb/go.mod h1:ArUGvPg2cOw/py2RAuBt46nNZH2VQ5Z70p109MAZpJw= github.com/fatedier/yamux v0.0.0-20250825093530-d0154be01cd6 h1:u92UUy6FURPmNsMBUuongRWC0rBqN6gd01Dzu+D21NE= github.com/fatedier/yamux v0.0.0-20250825093530-d0154be01cd6/go.mod h1:c5/tk6G0dSpXGzJN7Wk1OEie8grdSJAmeawId9Zvd34= github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= @@ -78,14 +78,6 @@ github.com/onsi/gomega v1.36.3 h1:hID7cr8t3Wp26+cYnfcjR6HpJ00fdogN6dqZ1t6IylU= github.com/onsi/gomega v1.36.3/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= github.com/pelletier/go-toml/v2 v2.2.0 h1:QLgLl2yMN7N+ruc31VynXs1vhMZa7CeHHejIeBAsoHo= github.com/pelletier/go-toml/v2 v2.2.0/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= -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.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8= -github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so= -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/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.7.0 h1:IukmRewDQFWC7kfnb66CSomk2q/seBuilHBYFwyq0Hs= github.com/pires/go-proxyproto v0.7.0/go.mod h1:Vz/1JPY/OACxWGQNIRY2BeyDmpoaWmEP40O9LbuiFR4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -146,8 +138,6 @@ github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQ github.com/vishvananda/netlink v1.3.0/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs= github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8= github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= -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/xtaci/kcp-go/v5 v5.6.13 h1:FEjtz9+D4p8t2x4WjciGt/jsIuhlWjjgPCCWjrVR4Hk= github.com/xtaci/kcp-go/v5 v5.6.13/go.mod h1:75S1AKYYzNUSXIv30h+jPKJYZUwqpfvLshu63nCNSOM= github.com/xtaci/lossyconn v0.0.0-20200209145036-adba10fffc37 h1:EWU6Pktpas0n8lLQwDsRyZfmkPeRbdgPtW609es+/9E= diff --git a/pkg/nathole/discovery.go b/pkg/nathole/discovery.go index 6fa0140f..241b0f6c 100644 --- a/pkg/nathole/discovery.go +++ b/pkg/nathole/discovery.go @@ -15,31 +15,24 @@ package nathole import ( + "errors" "fmt" "net" "time" - "github.com/pion/stun/v3" + "github.com/fatedier/golib/net/stun" ) var responseTimeout = 3 * time.Second -type Message struct { - Body []byte - Addr string -} - // If the localAddr is empty, it will listen on a random port. func Discover(stunServers []string, localAddr string) ([]string, net.Addr, error) { - // create a discoverConn and get response from messageChan discoverConn, err := listen(localAddr) if err != nil { return nil, nil, err } defer discoverConn.Close() - go discoverConn.readLoop() - addresses := make([]string, 0, len(stunServers)) for _, addr := range stunServers { // get external address from stun server @@ -58,10 +51,9 @@ type stunResponse struct { } type discoverConn struct { - conn *net.UDPConn - - localAddr net.Addr - messageChan chan *Message + conn *net.UDPConn + client *stun.Client + localAddr net.Addr } func listen(localAddr string) (*discoverConn, error) { @@ -77,82 +69,50 @@ func listen(localAddr string) (*discoverConn, error) { if err != nil { return nil, err } + client, err := stun.NewClient(conn) + if err != nil { + _ = conn.Close() + return nil, err + } return &discoverConn{ - conn: conn, - localAddr: conn.LocalAddr(), - messageChan: make(chan *Message, 10), + conn: conn, + client: client, + localAddr: conn.LocalAddr(), }, nil } func (c *discoverConn) Close() error { - if c.messageChan != nil { - close(c.messageChan) - c.messageChan = nil - } return c.conn.Close() } -func (c *discoverConn) readLoop() { - for { - buf := make([]byte, 1024) - n, addr, err := c.conn.ReadFromUDP(buf) - if err != nil { - return - } - buf = buf[:n] - - c.messageChan <- &Message{ - Body: buf, - Addr: addr.String(), - } - } -} - func (c *discoverConn) doSTUNRequest(addr string) (*stunResponse, error) { serverAddr, err := net.ResolveUDPAddr("udp4", addr) if err != nil { return nil, err } - request, err := stun.Build(stun.TransactionID, stun.BindingRequest) + transaction, err := stun.NewBindingTransaction(serverAddr) if err != nil { return nil, err } - - if err = request.NewTransactionID(); err != nil { + if err := c.conn.SetReadDeadline(time.Now().Add(responseTimeout)); err != nil { return nil, err } - if _, err := c.conn.WriteTo(request.Raw, serverAddr); err != nil { - return nil, err - } - - var m stun.Message - select { - case msg := <-c.messageChan: - m.Raw = msg.Body - if err := m.Decode(); err != nil { - return nil, err + response, err := c.client.Do(transaction) + if err != nil { + var netErr net.Error + if errors.As(err, &netErr) && netErr.Timeout() { + return nil, fmt.Errorf("wait response from stun server timeout") } - case <-time.After(responseTimeout): - return nil, fmt.Errorf("wait response from stun server timeout") + return nil, err } - xorAddrGetter := &stun.XORMappedAddress{} - mappedAddrGetter := &stun.MappedAddress{} - changedAddrGetter := ChangedAddress{} - otherAddrGetter := &stun.OtherAddress{} resp := &stunResponse{} - if err := mappedAddrGetter.GetFrom(&m); err == nil { - resp.externalAddr = mappedAddrGetter.String() + if response.MappedAddr != nil { + resp.externalAddr = response.MappedAddr.String() } - if err := xorAddrGetter.GetFrom(&m); err == nil { - resp.externalAddr = xorAddrGetter.String() - } - if err := changedAddrGetter.GetFrom(&m); err == nil { - resp.otherAddr = changedAddrGetter.String() - } - if err := otherAddrGetter.GetFrom(&m); err == nil { - resp.otherAddr = otherAddrGetter.String() + if response.OtherAddr != nil { + resp.otherAddr = response.OtherAddr.String() } return resp, nil } diff --git a/pkg/nathole/discovery_test.go b/pkg/nathole/discovery_test.go new file mode 100644 index 00000000..6596314d --- /dev/null +++ b/pkg/nathole/discovery_test.go @@ -0,0 +1,382 @@ +package nathole + +import ( + "encoding/binary" + "errors" + "fmt" + "net" + "testing" + "time" + + "github.com/fatedier/golib/net/stun" + "github.com/stretchr/testify/require" +) + +const ( + testBindingRequest = 0x0001 + testBindingSuccess = 0x0101 + testBindingError = 0x0111 + testMagicCookie = 0x2112a442 + testAttrMapped = 0x0001 + testAttrChanged = 0x0005 + testAttrErrorCode = 0x0009 + testAttrXORMapped = 0x0020 + testAttrOther = 0x802c + testSTUNHeaderSize = 20 + testSTUNServerLimit = time.Second +) + +type testSTUNAttribute struct { + typ uint16 + value []byte +} + +type testSTUNExchange struct { + source *net.UDPAddr + err error +} + +func listenTestUDP4(t *testing.T) *net.UDPConn { + t.Helper() + + conn, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1)}) + require.NoError(t, err) + t.Cleanup(func() { _ = conn.Close() }) + return conn +} + +func serveOneSTUNRequest( + server *net.UDPConn, + buildResponse func([]byte, *net.UDPAddr) ([]byte, error), +) <-chan testSTUNExchange { + done := make(chan testSTUNExchange, 1) + go func() { + if err := server.SetDeadline(time.Now().Add(testSTUNServerLimit)); err != nil { + done <- testSTUNExchange{err: err} + return + } + buffer := make([]byte, 1024) + n, source, err := server.ReadFromUDP(buffer) + if err == nil && buildResponse != nil { + var response []byte + response, err = buildResponse(buffer[:n], source) + if err == nil && response != nil { + _, err = server.WriteToUDP(response, source) + } + } + done <- testSTUNExchange{source: source, err: err} + }() + return done +} + +func waitSTUNExchange(t *testing.T, done <-chan testSTUNExchange) *net.UDPAddr { + t.Helper() + + select { + case exchange := <-done: + require.NoError(t, exchange.err) + return exchange.source + case <-time.After(testSTUNServerLimit): + t.Fatal("timed out waiting for local STUN server") + return nil + } +} + +func makeTestSTUNResponse(request []byte, typ uint16, attributes ...testSTUNAttribute) ([]byte, error) { + if len(request) != testSTUNHeaderSize || binary.BigEndian.Uint16(request[0:2]) != testBindingRequest || + binary.BigEndian.Uint32(request[4:8]) != testMagicCookie { + return nil, fmt.Errorf("invalid Binding request") + } + + length := 0 + for _, attribute := range attributes { + length += 4 + (len(attribute.value)+3)&^3 + } + response := make([]byte, testSTUNHeaderSize, testSTUNHeaderSize+length) + binary.BigEndian.PutUint16(response[0:2], typ) + binary.BigEndian.PutUint16(response[2:4], uint16(length)) + binary.BigEndian.PutUint32(response[4:8], testMagicCookie) + copy(response[8:20], request[8:20]) + + for _, attribute := range attributes { + start := len(response) + paddedLength := (len(attribute.value) + 3) &^ 3 + response = append(response, make([]byte, 4+paddedLength)...) + binary.BigEndian.PutUint16(response[start:start+2], attribute.typ) + binary.BigEndian.PutUint16(response[start+2:start+4], uint16(len(attribute.value))) + copy(response[start+4:], attribute.value) + } + return response, nil +} + +func testIPv4AddressValue(ip net.IP, port int, xor bool) []byte { + value := make([]byte, 8) + value[1] = 0x01 + binary.BigEndian.PutUint16(value[2:4], uint16(port)) + copy(value[4:], ip.To4()) + if xor { + binary.BigEndian.PutUint16(value[2:4], binary.BigEndian.Uint16(value[2:4])^uint16(testMagicCookie>>16)) + for i := range 4 { + value[4+i] ^= byte(uint32(testMagicCookie) >> uint(24-8*i)) + } + } + return value +} + +func TestDiscoverReusesLocalPortAndPreservesNATClassification(t *testing.T) { + tests := []struct { + name string + secondMapped string + secondMappedPort int + wantNATType string + wantBehavior string + }{ + { + name: "same mapped address", + secondMapped: "198.51.100.10:40000", + secondMappedPort: 40000, + wantNATType: EasyNAT, + wantBehavior: BehaviorNoChange, + }, + { + name: "different mapped port", + secondMapped: "198.51.100.10:40001", + secondMappedPort: 40001, + wantNATType: HardNAT, + wantBehavior: BehaviorPortChanged, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + primary := listenTestUDP4(t) + alternate := listenTestUDP4(t) + alternateAddr := alternate.LocalAddr().(*net.UDPAddr) + + primaryDone := serveOneSTUNRequest(primary, func(request []byte, _ *net.UDPAddr) ([]byte, error) { + return makeTestSTUNResponse(request, testBindingSuccess, + testSTUNAttribute{typ: testAttrXORMapped, value: testIPv4AddressValue(net.ParseIP("198.51.100.10"), 40000, true)}, + testSTUNAttribute{typ: testAttrOther, value: testIPv4AddressValue(alternateAddr.IP, alternateAddr.Port, false)}, + ) + }) + alternateDone := serveOneSTUNRequest(alternate, func(request []byte, _ *net.UDPAddr) ([]byte, error) { + return makeTestSTUNResponse(request, testBindingSuccess, + testSTUNAttribute{typ: testAttrXORMapped, value: testIPv4AddressValue(net.ParseIP("198.51.100.10"), tt.secondMappedPort, true)}, + ) + }) + + addresses, localAddr, err := Discover([]string{primary.LocalAddr().String()}, "") + require.NoError(t, err) + require.Equal(t, []string{"198.51.100.10:40000", tt.secondMapped}, addresses) + + primarySource := waitSTUNExchange(t, primaryDone) + alternateSource := waitSTUNExchange(t, alternateDone) + require.Equal(t, primarySource.Port, alternateSource.Port) + require.Equal(t, localAddr.(*net.UDPAddr).Port, primarySource.Port) + + feature, err := ClassifyNATFeature(addresses, nil) + require.NoError(t, err) + require.Equal(t, tt.wantNATType, feature.NatType) + require.Equal(t, tt.wantBehavior, feature.Behavior) + }) + } +} + +func TestDoSTUNRequestMapsLegacyAndModernAddresses(t *testing.T) { + tests := []struct { + name string + attributes []testSTUNAttribute + wantExternal string + wantOther string + }{ + { + name: "legacy", + attributes: []testSTUNAttribute{ + {typ: testAttrMapped, value: testIPv4AddressValue(net.ParseIP("192.0.2.1"), 1000, false)}, + {typ: testAttrChanged, value: testIPv4AddressValue(net.ParseIP("192.0.2.2"), 2000, false)}, + }, + wantExternal: "192.0.2.1:1000", + wantOther: "192.0.2.2:2000", + }, + { + name: "modern takes precedence", + attributes: []testSTUNAttribute{ + {typ: testAttrMapped, value: testIPv4AddressValue(net.ParseIP("192.0.2.1"), 1000, false)}, + {typ: testAttrXORMapped, value: testIPv4AddressValue(net.ParseIP("198.51.100.1"), 3000, true)}, + {typ: testAttrChanged, value: testIPv4AddressValue(net.ParseIP("192.0.2.2"), 2000, false)}, + {typ: testAttrOther, value: testIPv4AddressValue(net.ParseIP("198.51.100.2"), 4000, false)}, + }, + wantExternal: "198.51.100.1:3000", + wantOther: "198.51.100.2:4000", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + server := listenTestUDP4(t) + done := serveOneSTUNRequest(server, func(request []byte, _ *net.UDPAddr) ([]byte, error) { + return makeTestSTUNResponse(request, testBindingSuccess, tt.attributes...) + }) + conn, err := listen("") + require.NoError(t, err) + t.Cleanup(func() { _ = conn.Close() }) + + response, err := conn.doSTUNRequest(server.LocalAddr().String()) + require.NoError(t, err) + require.Equal(t, tt.wantExternal, response.externalAddr) + require.Equal(t, tt.wantOther, response.otherAddr) + waitSTUNExchange(t, done) + }) + } +} + +func TestSTUNResponseErrorsAndMissingAddresses(t *testing.T) { + tests := []struct { + name string + buildResponse func([]byte, *net.UDPAddr) ([]byte, error) + request func(*discoverConn, string) error + checkError func(*testing.T, error) + }{ + { + name: "correlated malformed response", + buildResponse: func(request []byte, _ *net.UDPAddr) ([]byte, error) { + response, err := makeTestSTUNResponse(request, testBindingSuccess) + if err == nil { + binary.BigEndian.PutUint16(response[2:4], 4) + } + return response, err + }, + request: func(conn *discoverConn, server string) error { + _, err := conn.doSTUNRequest(server) + return err + }, + checkError: func(t *testing.T, err error) { + require.ErrorIs(t, err, stun.ErrMalformedResponse) + }, + }, + { + name: "Binding error response", + buildResponse: func(request []byte, _ *net.UDPAddr) ([]byte, error) { + return makeTestSTUNResponse(request, testBindingError, testSTUNAttribute{ + typ: testAttrErrorCode, + value: []byte{0, 0, 4, 20, 'U', 'n', 'k', 'n', 'o', 'w', 'n'}, + }) + }, + request: func(conn *discoverConn, server string) error { + _, err := conn.doSTUNRequest(server) + return err + }, + checkError: func(t *testing.T, err error) { + var responseErr *stun.ResponseError + require.ErrorAs(t, err, &responseErr) + require.Equal(t, 420, responseErr.Code) + }, + }, + { + name: "missing mapped address", + buildResponse: func(request []byte, _ *net.UDPAddr) ([]byte, error) { + return makeTestSTUNResponse(request, testBindingSuccess, + testSTUNAttribute{typ: testAttrOther, value: testIPv4AddressValue(net.ParseIP("192.0.2.2"), 2000, false)}, + ) + }, + request: func(conn *discoverConn, server string) error { + _, err := conn.discoverFromStunServer(server) + return err + }, + checkError: func(t *testing.T, err error) { + require.EqualError(t, err, "no external address found") + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + server := listenTestUDP4(t) + done := serveOneSTUNRequest(server, tt.buildResponse) + conn, err := listen("") + require.NoError(t, err) + t.Cleanup(func() { _ = conn.Close() }) + + err = tt.request(conn, server.LocalAddr().String()) + tt.checkError(t, err) + waitSTUNExchange(t, done) + }) + } + + t.Run("missing other address", func(t *testing.T) { + server := listenTestUDP4(t) + done := serveOneSTUNRequest(server, func(request []byte, _ *net.UDPAddr) ([]byte, error) { + return makeTestSTUNResponse(request, testBindingSuccess, + testSTUNAttribute{typ: testAttrXORMapped, value: testIPv4AddressValue(net.ParseIP("198.51.100.1"), 3000, true)}, + ) + }) + + _, err := Prepare([]string{server.LocalAddr().String()}, PrepareOptions{}) + require.EqualError(t, err, "discover error: not enough addresses") + waitSTUNExchange(t, done) + }) +} + +func TestSTUNTimeoutUsesCallerDeadlineWithoutRetry(t *testing.T) { + originalTimeout := responseTimeout + responseTimeout = 50 * time.Millisecond + t.Cleanup(func() { responseTimeout = originalTimeout }) + + server := listenTestUDP4(t) + done := serveOneSTUNRequest(server, nil) + conn, err := listen("") + require.NoError(t, err) + t.Cleanup(func() { _ = conn.Close() }) + + _, err = conn.doSTUNRequest(server.LocalAddr().String()) + require.EqualError(t, err, "wait response from stun server timeout") + waitSTUNExchange(t, done) + + require.NoError(t, server.SetReadDeadline(time.Now().Add(50*time.Millisecond))) + _, _, err = server.ReadFromUDP(make([]byte, 1)) + var netErr net.Error + require.ErrorAs(t, err, &netErr) + require.True(t, netErr.Timeout()) +} + +func TestSTUNClientLeavesSocketAndDeadlineWithCaller(t *testing.T) { + originalTimeout := responseTimeout + responseTimeout = 100 * time.Millisecond + t.Cleanup(func() { responseTimeout = originalTimeout }) + + server := listenTestUDP4(t) + unrelated := listenTestUDP4(t) + done := serveOneSTUNRequest(server, func(request []byte, source *net.UDPAddr) ([]byte, error) { + response, err := makeTestSTUNResponse(request, testBindingSuccess, + testSTUNAttribute{typ: testAttrXORMapped, value: testIPv4AddressValue(net.ParseIP("198.51.100.5"), 5000, true)}, + ) + if err != nil { + return nil, err + } + if _, err := unrelated.WriteToUDP(response, source); err != nil { + return nil, err + } + return response, nil + }) + conn, err := listen("") + require.NoError(t, err) + t.Cleanup(func() { _ = conn.Close() }) + + response, err := conn.doSTUNRequest(server.LocalAddr().String()) + require.NoError(t, err) + require.Equal(t, "198.51.100.5:5000", response.externalAddr) + waitSTUNExchange(t, done) + + _, _, err = conn.conn.ReadFromUDP(make([]byte, 1)) + var netErr net.Error + require.True(t, errors.As(err, &netErr)) + require.True(t, netErr.Timeout()) + + require.NoError(t, conn.conn.SetDeadline(time.Time{})) + require.NoError(t, server.SetReadDeadline(time.Now().Add(testSTUNServerLimit))) + _, err = conn.conn.WriteToUDP([]byte{1}, server.LocalAddr().(*net.UDPAddr)) + require.NoError(t, err) + _, source, err := server.ReadFromUDP(make([]byte, 1)) + require.NoError(t, err) + require.Equal(t, conn.localAddr.(*net.UDPAddr).Port, source.Port) +} diff --git a/pkg/nathole/utils.go b/pkg/nathole/utils.go index 2a882830..0d71692f 100644 --- a/pkg/nathole/utils.go +++ b/pkg/nathole/utils.go @@ -18,10 +18,8 @@ import ( "bytes" "fmt" "net" - "strconv" "github.com/fatedier/golib/crypto" - "github.com/pion/stun/v3" "github.com/fatedier/frp/pkg/msg" ) @@ -48,20 +46,6 @@ func DecodeMessageInto(data, key []byte, m msg.Message) error { return msg.ReadMsgInto(bytes.NewReader(buf), m) } -type ChangedAddress struct { - IP net.IP - Port int -} - -func (s *ChangedAddress) GetFrom(m *stun.Message) error { - a := (*stun.MappedAddress)(s) - return a.GetFromAs(m, stun.AttrChangedAddress) -} - -func (s *ChangedAddress) String() string { - return net.JoinHostPort(s.IP.String(), strconv.Itoa(s.Port)) -} - func ListAllLocalIPs() ([]net.IP, error) { addrs, err := net.InterfaceAddrs() if err != nil {