mirror of
https://github.com/fatedier/frp.git
synced 2026-07-17 01:39:18 +08:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe79598ee4 | ||
|
|
269a26c5d5 | ||
|
|
2886393f5b |
4
.github/workflows/build-and-push-image.yml
vendored
4
.github/workflows/build-and-push-image.yml
vendored
@@ -65,7 +65,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./dockerfiles/Dockerfile-for-frpc
|
file: ./dockerfiles/Dockerfile-for-frpc
|
||||||
platforms: linux/amd64,linux/arm/v7,linux/arm64,linux/ppc64le,linux/s390x
|
platforms: linux/amd64,linux/arm/v7,linux/arm64,linux/ppc64le
|
||||||
push: true
|
push: true
|
||||||
tags: |
|
tags: |
|
||||||
${{ env.TAG_FRPC }}
|
${{ env.TAG_FRPC }}
|
||||||
@@ -76,7 +76,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./dockerfiles/Dockerfile-for-frps
|
file: ./dockerfiles/Dockerfile-for-frps
|
||||||
platforms: linux/amd64,linux/arm/v7,linux/arm64,linux/ppc64le,linux/s390x
|
platforms: linux/amd64,linux/arm/v7,linux/arm64,linux/ppc64le
|
||||||
push: true
|
push: true
|
||||||
tags: |
|
tags: |
|
||||||
${{ env.TAG_FRPS }}
|
${{ env.TAG_FRPS }}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ all: build
|
|||||||
build: app
|
build: app
|
||||||
|
|
||||||
app:
|
app:
|
||||||
@$(foreach n, $(os-archs), \
|
@set -e; $(foreach n, $(os-archs), \
|
||||||
os=$(shell echo "$(n)" | cut -d : -f 1); \
|
os=$(shell echo "$(n)" | cut -d : -f 1); \
|
||||||
arch=$(shell echo "$(n)" | cut -d : -f 2); \
|
arch=$(shell echo "$(n)" | cut -d : -f 2); \
|
||||||
extra=$(shell echo "$(n)" | cut -d : -f 3); \
|
extra=$(shell echo "$(n)" | cut -d : -f 3); \
|
||||||
|
|||||||
7
go.mod
7
go.mod
@@ -5,7 +5,7 @@ go 1.25.0
|
|||||||
require (
|
require (
|
||||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
|
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
|
||||||
github.com/coreos/go-oidc/v3 v3.14.1
|
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/google/uuid v1.6.0
|
||||||
github.com/gorilla/mux v1.8.1
|
github.com/gorilla/mux v1.8.1
|
||||||
github.com/gorilla/websocket v1.5.0
|
github.com/gorilla/websocket v1.5.0
|
||||||
@@ -13,7 +13,6 @@ require (
|
|||||||
github.com/onsi/ginkgo/v2 v2.23.4
|
github.com/onsi/ginkgo/v2 v2.23.4
|
||||||
github.com/onsi/gomega v1.36.3
|
github.com/onsi/gomega v1.36.3
|
||||||
github.com/pelletier/go-toml/v2 v2.2.0
|
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/pires/go-proxyproto v0.7.0
|
||||||
github.com/prometheus/client_golang v1.19.1
|
github.com/prometheus/client_golang v1.19.1
|
||||||
github.com/quic-go/quic-go v0.55.0
|
github.com/quic-go/quic-go v0.55.0
|
||||||
@@ -53,9 +52,6 @@ require (
|
|||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.2.6 // indirect
|
github.com/klauspost/cpuid/v2 v2.2.6 // indirect
|
||||||
github.com/klauspost/reedsolomon v1.12.0 // 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/pkg/errors v0.9.1 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/prometheus/client_model v0.5.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/tidwall/pretty v1.2.0 // indirect
|
||||||
github.com/tjfoc/gmsm v1.4.1 // indirect
|
github.com/tjfoc/gmsm v1.4.1 // indirect
|
||||||
github.com/vishvananda/netns v0.0.4 // 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
|
go.uber.org/automaxprocs v1.6.0 // indirect
|
||||||
golang.org/x/mod v0.33.0 // indirect
|
golang.org/x/mod v0.33.0 // indirect
|
||||||
golang.org/x/text v0.35.0 // indirect
|
golang.org/x/text v0.35.0 // indirect
|
||||||
|
|||||||
14
go.sum
14
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.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
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/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.2-0.20260715094513-2780ae1d8cdb h1:6gcaE769mFb1G/cNV6lgzmwIYkNXmhj4pmcddllfmB4=
|
||||||
github.com/fatedier/golib v0.7.0/go.mod h1:ArUGvPg2cOw/py2RAuBt46nNZH2VQ5Z70p109MAZpJw=
|
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 h1:u92UUy6FURPmNsMBUuongRWC0rBqN6gd01Dzu+D21NE=
|
||||||
github.com/fatedier/yamux v0.0.0-20250825093530-d0154be01cd6/go.mod h1:c5/tk6G0dSpXGzJN7Wk1OEie8grdSJAmeawId9Zvd34=
|
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=
|
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/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 h1:QLgLl2yMN7N+ruc31VynXs1vhMZa7CeHHejIeBAsoHo=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.0/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
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 h1:IukmRewDQFWC7kfnb66CSomk2q/seBuilHBYFwyq0Hs=
|
||||||
github.com/pires/go-proxyproto v0.7.0/go.mod h1:Vz/1JPY/OACxWGQNIRY2BeyDmpoaWmEP40O9LbuiFR4=
|
github.com/pires/go-proxyproto v0.7.0/go.mod h1:Vz/1JPY/OACxWGQNIRY2BeyDmpoaWmEP40O9LbuiFR4=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
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/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 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
|
||||||
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
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 h1:FEjtz9+D4p8t2x4WjciGt/jsIuhlWjjgPCCWjrVR4Hk=
|
||||||
github.com/xtaci/kcp-go/v5 v5.6.13/go.mod h1:75S1AKYYzNUSXIv30h+jPKJYZUwqpfvLshu63nCNSOM=
|
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=
|
github.com/xtaci/lossyconn v0.0.0-20200209145036-adba10fffc37 h1:EWU6Pktpas0n8lLQwDsRyZfmkPeRbdgPtW609es+/9E=
|
||||||
|
|||||||
@@ -15,31 +15,24 @@
|
|||||||
package nathole
|
package nathole
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pion/stun/v3"
|
"github.com/fatedier/golib/net/stun"
|
||||||
)
|
)
|
||||||
|
|
||||||
var responseTimeout = 3 * time.Second
|
var responseTimeout = 3 * time.Second
|
||||||
|
|
||||||
type Message struct {
|
|
||||||
Body []byte
|
|
||||||
Addr string
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the localAddr is empty, it will listen on a random port.
|
// If the localAddr is empty, it will listen on a random port.
|
||||||
func Discover(stunServers []string, localAddr string) ([]string, net.Addr, error) {
|
func Discover(stunServers []string, localAddr string) ([]string, net.Addr, error) {
|
||||||
// create a discoverConn and get response from messageChan
|
|
||||||
discoverConn, err := listen(localAddr)
|
discoverConn, err := listen(localAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
defer discoverConn.Close()
|
defer discoverConn.Close()
|
||||||
|
|
||||||
go discoverConn.readLoop()
|
|
||||||
|
|
||||||
addresses := make([]string, 0, len(stunServers))
|
addresses := make([]string, 0, len(stunServers))
|
||||||
for _, addr := range stunServers {
|
for _, addr := range stunServers {
|
||||||
// get external address from stun server
|
// get external address from stun server
|
||||||
@@ -58,10 +51,9 @@ type stunResponse struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type discoverConn struct {
|
type discoverConn struct {
|
||||||
conn *net.UDPConn
|
conn *net.UDPConn
|
||||||
|
client *stun.Client
|
||||||
localAddr net.Addr
|
localAddr net.Addr
|
||||||
messageChan chan *Message
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func listen(localAddr string) (*discoverConn, error) {
|
func listen(localAddr string) (*discoverConn, error) {
|
||||||
@@ -77,82 +69,50 @@ func listen(localAddr string) (*discoverConn, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
client, err := stun.NewClient(conn)
|
||||||
|
if err != nil {
|
||||||
|
_ = conn.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
return &discoverConn{
|
return &discoverConn{
|
||||||
conn: conn,
|
conn: conn,
|
||||||
localAddr: conn.LocalAddr(),
|
client: client,
|
||||||
messageChan: make(chan *Message, 10),
|
localAddr: conn.LocalAddr(),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *discoverConn) Close() error {
|
func (c *discoverConn) Close() error {
|
||||||
if c.messageChan != nil {
|
|
||||||
close(c.messageChan)
|
|
||||||
c.messageChan = nil
|
|
||||||
}
|
|
||||||
return c.conn.Close()
|
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) {
|
func (c *discoverConn) doSTUNRequest(addr string) (*stunResponse, error) {
|
||||||
serverAddr, err := net.ResolveUDPAddr("udp4", addr)
|
serverAddr, err := net.ResolveUDPAddr("udp4", addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
request, err := stun.Build(stun.TransactionID, stun.BindingRequest)
|
transaction, err := stun.NewBindingTransaction(serverAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if err := c.conn.SetReadDeadline(time.Now().Add(responseTimeout)); err != nil {
|
||||||
if err = request.NewTransactionID(); err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if _, err := c.conn.WriteTo(request.Raw, serverAddr); err != nil {
|
response, err := c.client.Do(transaction)
|
||||||
return nil, err
|
if err != nil {
|
||||||
}
|
var netErr net.Error
|
||||||
|
if errors.As(err, &netErr) && netErr.Timeout() {
|
||||||
var m stun.Message
|
return nil, fmt.Errorf("wait response from stun server timeout")
|
||||||
select {
|
|
||||||
case msg := <-c.messageChan:
|
|
||||||
m.Raw = msg.Body
|
|
||||||
if err := m.Decode(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
case <-time.After(responseTimeout):
|
return nil, err
|
||||||
return nil, fmt.Errorf("wait response from stun server timeout")
|
|
||||||
}
|
}
|
||||||
xorAddrGetter := &stun.XORMappedAddress{}
|
|
||||||
mappedAddrGetter := &stun.MappedAddress{}
|
|
||||||
changedAddrGetter := ChangedAddress{}
|
|
||||||
otherAddrGetter := &stun.OtherAddress{}
|
|
||||||
|
|
||||||
resp := &stunResponse{}
|
resp := &stunResponse{}
|
||||||
if err := mappedAddrGetter.GetFrom(&m); err == nil {
|
if response.MappedAddr != nil {
|
||||||
resp.externalAddr = mappedAddrGetter.String()
|
resp.externalAddr = response.MappedAddr.String()
|
||||||
}
|
}
|
||||||
if err := xorAddrGetter.GetFrom(&m); err == nil {
|
if response.OtherAddr != nil {
|
||||||
resp.externalAddr = xorAddrGetter.String()
|
resp.otherAddr = response.OtherAddr.String()
|
||||||
}
|
|
||||||
if err := changedAddrGetter.GetFrom(&m); err == nil {
|
|
||||||
resp.otherAddr = changedAddrGetter.String()
|
|
||||||
}
|
|
||||||
if err := otherAddrGetter.GetFrom(&m); err == nil {
|
|
||||||
resp.otherAddr = otherAddrGetter.String()
|
|
||||||
}
|
}
|
||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|||||||
382
pkg/nathole/discovery_test.go
Normal file
382
pkg/nathole/discovery_test.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
@@ -18,10 +18,8 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/fatedier/golib/crypto"
|
"github.com/fatedier/golib/crypto"
|
||||||
"github.com/pion/stun/v3"
|
|
||||||
|
|
||||||
"github.com/fatedier/frp/pkg/msg"
|
"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)
|
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) {
|
func ListAllLocalIPs() ([]net.IP, error) {
|
||||||
addrs, err := net.InterfaceAddrs()
|
addrs, err := net.InterfaceAddrs()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user