From 5d8710561958ecbbbf4d0c07c1e34b40781d6976 Mon Sep 17 00:00:00 2001 From: lilinzhe Date: Thu, 28 Mar 2024 11:24:21 +0800 Subject: [PATCH] nathole: upnp support see also: https://github.com/fatedier/frp/issues/1823 https://github.com/fatedier/frp/issues/3703 --- client/proxy/xtcp.go | 20 ++++- client/visitor/xtcp.go | 20 ++++- conf/frpc_full_example.toml | 5 ++ go.mod | 3 + go.sum | 6 ++ pkg/config/v1/common.go | 5 ++ pkg/config/v1/proxy.go | 2 + pkg/config/v1/visitor.go | 2 + pkg/nathole/nathole.go | 11 ++- pkg/nathole/upnp/upnp.go | 173 ++++++++++++++++++++++++++++++++++++ 10 files changed, 244 insertions(+), 3 deletions(-) create mode 100644 pkg/nathole/upnp/upnp.go diff --git a/client/proxy/xtcp.go b/client/proxy/xtcp.go index 31f9ac89734..7945701791c 100644 --- a/client/proxy/xtcp.go +++ b/client/proxy/xtcp.go @@ -28,6 +28,7 @@ import ( v1 "github.com/fatedier/frp/pkg/config/v1" "github.com/fatedier/frp/pkg/msg" "github.com/fatedier/frp/pkg/nathole" + "github.com/fatedier/frp/pkg/nathole/upnp" "github.com/fatedier/frp/pkg/transport" netpkg "github.com/fatedier/frp/pkg/util/net" ) @@ -53,6 +54,23 @@ func NewXTCPProxy(baseProxy *BaseProxy, cfg v1.ProxyConfigurer) Proxy { } } +func (pxy *XTCPProxy) makeRouterToNatThisHole(remoteGetAddrs []string, localIps []string, localAddr net.Addr) { + + xl := pxy.xl + if !pxy.cfg.AllowToUseUPNP { + xl.Tracef("makeRouterToNatThisHole: upnp disabled") + return + } + + description := pxy.cfg.UPNPPortMappingDescription + if description == "" { + description = upnp.DEFAULT_UPNP_PROGRAM_DESCRIPTION + } + + upnp.AskForMapping(xl, remoteGetAddrs, localIps, localAddr, description) + +} + func (pxy *XTCPProxy) InWorkConn(conn net.Conn, startWorkConnMsg *msg.StartWorkConn) { xl := pxy.xl defer conn.Close() @@ -64,7 +82,7 @@ func (pxy *XTCPProxy) InWorkConn(conn net.Conn, startWorkConnMsg *msg.StartWorkC } xl.Tracef("nathole prepare start") - prepareResult, err := nathole.Prepare([]string{pxy.clientCfg.NatHoleSTUNServer}) + prepareResult, err := nathole.Prepare([]string{pxy.clientCfg.NatHoleSTUNServer}, pxy.makeRouterToNatThisHole) if err != nil { xl.Warnf("nathole prepare error: %v", err) return diff --git a/client/visitor/xtcp.go b/client/visitor/xtcp.go index a1efd72b62b..654e71e818b 100644 --- a/client/visitor/xtcp.go +++ b/client/visitor/xtcp.go @@ -32,6 +32,7 @@ import ( v1 "github.com/fatedier/frp/pkg/config/v1" "github.com/fatedier/frp/pkg/msg" "github.com/fatedier/frp/pkg/nathole" + "github.com/fatedier/frp/pkg/nathole/upnp" "github.com/fatedier/frp/pkg/transport" netpkg "github.com/fatedier/frp/pkg/util/net" "github.com/fatedier/frp/pkg/util/util" @@ -261,6 +262,23 @@ func (sv *XTCPVisitor) getTunnelConn() (net.Conn, error) { return nil, err } +func (sv *XTCPVisitor) makeRouterToNatThisHole(remoteGetAddrs []string, localIps []string, localAddr net.Addr) { + + xl := xlog.FromContextSafe(sv.ctx) + if !sv.cfg.AllowToUseUPNP { + xl.Tracef("makeRouterToNatThisHole: upnp disabled") + return + } + + description := sv.cfg.UPNPPortMappingDescription + if description == "" { + description = upnp.DEFAULT_UPNP_PROGRAM_DESCRIPTION + } + + upnp.AskForMapping(xl, remoteGetAddrs, localIps, localAddr, description) + +} + // 0. PreCheck // 1. Prepare // 2. ExchangeInfo @@ -275,7 +293,7 @@ func (sv *XTCPVisitor) makeNatHole() { } xl.Tracef("nathole prepare start") - prepareResult, err := nathole.Prepare([]string{sv.clientCfg.NatHoleSTUNServer}) + prepareResult, err := nathole.Prepare([]string{sv.clientCfg.NatHoleSTUNServer}, sv.makeRouterToNatThisHole) if err != nil { xl.Warnf("nathole prepare error: %v", err) return diff --git a/conf/frpc_full_example.toml b/conf/frpc_full_example.toml index 0528ddeaf37..b7926b5946f 100644 --- a/conf/frpc_full_example.toml +++ b/conf/frpc_full_example.toml @@ -336,6 +336,9 @@ localPort = 22 # If not empty, only visitors from specified users can connect. # Otherwise, visitors from same user can connect. '*' means allow all users. allowUsers = ["user1", "user2"] +# allow to use upnp to map this port +allowToUseUPNP = false +upnpPortMappingDescription = "helper-port-mapping" # frpc role visitor -> frps -> frpc role server [[visitors]] @@ -368,3 +371,5 @@ maxRetriesAnHour = 8 minRetryInterval = 90 # fallbackTo = "stcp_visitor" # fallbackTimeoutMs = 500 +allowToUseUPNP = false +upnpPortMappingDescription = "helper-port-mapping" diff --git a/go.mod b/go.mod index d820b96cdd2..1151e1d7a19 100644 --- a/go.mod +++ b/go.mod @@ -46,7 +46,9 @@ require ( github.com/golang/snappy v0.0.4 // indirect github.com/google/go-cmp v0.5.9 // indirect github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect + github.com/huin/goupnp v1.3.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jackpal/gateway v1.0.14 // indirect github.com/klauspost/cpuid/v2 v2.2.6 // indirect github.com/klauspost/reedsolomon v1.12.0 // indirect github.com/kr/text v0.2.0 // indirect @@ -61,6 +63,7 @@ require ( github.com/prometheus/common v0.42.0 // indirect github.com/prometheus/procfs v0.10.1 // indirect github.com/rogpeppe/go-internal v1.11.0 // indirect + github.com/stretchr/objx v0.5.0 // indirect github.com/templexxx/cpu v0.1.0 // indirect github.com/templexxx/xorsimd v0.4.2 // indirect github.com/tidwall/match v1.1.1 // indirect diff --git a/go.sum b/go.sum index b00cb78fafd..fbf8a36a6d2 100644 --- a/go.sum +++ b/go.sum @@ -68,9 +68,13 @@ github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= +github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jackpal/gateway v1.0.14 h1:6ZfIuFvnvWrS59hHbvZGR/R33ojV2LASBODomt7zlJU= +github.com/jackpal/gateway v1.0.14/go.mod h1:6c8LjW+FVESFmwxaXySkt7fU98Yv806ADS3OY6Cvh2U= github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc= github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/klauspost/reedsolomon v1.12.0 h1:I5FEp3xSwVCcEh3F5A7dofEfhXdF/bWhQWPH+XwBFno= @@ -129,6 +133,7 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -197,6 +202,7 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= diff --git a/pkg/config/v1/common.go b/pkg/config/v1/common.go index ddb23356823..ab89b628454 100644 --- a/pkg/config/v1/common.go +++ b/pkg/config/v1/common.go @@ -134,3 +134,8 @@ type HTTPHeader struct { Name string `json:"name"` Value string `json:"value"` } + +type XTCPConfigUPNPMixin struct { + AllowToUseUPNP bool `json:"allowToUseUPNP,omitempty"` + UPNPPortMappingDescription string `json:"upnpPortMappingDescription,omitempty"` +} diff --git a/pkg/config/v1/proxy.go b/pkg/config/v1/proxy.go index 1949cfd34d1..2dde809275d 100644 --- a/pkg/config/v1/proxy.go +++ b/pkg/config/v1/proxy.go @@ -411,6 +411,8 @@ type XTCPProxyConfig struct { Secretkey string `json:"secretKey,omitempty"` AllowUsers []string `json:"allowUsers,omitempty"` + + XTCPConfigUPNPMixin } func (c *XTCPProxyConfig) MarshalToMsg(m *msg.NewProxy) { diff --git a/pkg/config/v1/visitor.go b/pkg/config/v1/visitor.go index e9fa166ea57..f9da28dce7a 100644 --- a/pkg/config/v1/visitor.go +++ b/pkg/config/v1/visitor.go @@ -153,6 +153,8 @@ type XTCPVisitorConfig struct { MinRetryInterval int `json:"minRetryInterval,omitempty"` FallbackTo string `json:"fallbackTo,omitempty"` FallbackTimeoutMs int `json:"fallbackTimeoutMs,omitempty"` + + XTCPConfigUPNPMixin } func (c *XTCPVisitorConfig) Complete(g *ClientCommonConfig) { diff --git a/pkg/nathole/nathole.go b/pkg/nathole/nathole.go index bdd0ee83cb2..7eb53ef8083 100644 --- a/pkg/nathole/nathole.go +++ b/pkg/nathole/nathole.go @@ -107,8 +107,12 @@ func PreCheck( return nil } +type OnGetMyRemoteAddress func(remoteGetAddrs []string, localIps []string, localAddr net.Addr) + // Prepare is used to do some preparation work before penetration. -func Prepare(stunServers []string) (*PrepareResult, error) { +func Prepare(stunServers []string, + callback OnGetMyRemoteAddress, +) (*PrepareResult, error) { // discover for Nat type addrs, localAddr, err := Discover(stunServers, "") if err != nil { @@ -119,6 +123,11 @@ func Prepare(stunServers []string) (*PrepareResult, error) { } localIPs, _ := ListLocalIPsForNatHole(10) + + if callback != nil { + callback(addrs, localIPs, localAddr) + } + natFeature, err := ClassifyNATFeature(addrs, localIPs) if err != nil { return nil, fmt.Errorf("classify nat feature error: %v", err) diff --git a/pkg/nathole/upnp/upnp.go b/pkg/nathole/upnp/upnp.go new file mode 100644 index 00000000000..535bcd4b05e --- /dev/null +++ b/pkg/nathole/upnp/upnp.go @@ -0,0 +1,173 @@ +package upnp + +import ( + "context" + + "errors" + "github.com/fatedier/frp/pkg/util/xlog" + "golang.org/x/sync/errgroup" + "net" + "net/netip" + "time" + + "github.com/huin/goupnp/dcps/internetgateway2" + "github.com/jackpal/gateway" +) + +const DEFAULT_UPNP_PROGRAM_DESCRIPTION = "helper-port-mapping" + +type RouterClient interface { + AddPortMapping( + NewRemoteHost string, + NewExternalPort uint16, + NewProtocol string, + NewInternalPort uint16, + NewInternalClient string, + NewEnabled bool, + NewPortMappingDescription string, + NewLeaseDuration uint32, + ) (err error) + + GetExternalIPAddress() ( + NewExternalIPAddress string, + err error, + ) +} + +func PickRouterClient(ctx context.Context) (RouterClient, error) { + tasks, _ := errgroup.WithContext(ctx) + // Request each type of client in parallel, and return what is found. + var ip1Clients []*internetgateway2.WANIPConnection1 + tasks.Go(func() error { + var err error + ip1Clients, _, err = internetgateway2.NewWANIPConnection1Clients() + return err + }) + var ip2Clients []*internetgateway2.WANIPConnection2 + tasks.Go(func() error { + var err error + ip2Clients, _, err = internetgateway2.NewWANIPConnection2Clients() + return err + }) + var ppp1Clients []*internetgateway2.WANPPPConnection1 + tasks.Go(func() error { + var err error + ppp1Clients, _, err = internetgateway2.NewWANPPPConnection1Clients() + return err + }) + + if err := tasks.Wait(); err != nil { + return nil, err + } + + // Trivial handling for where we find exactly one device to talk to, you + // might want to provide more flexible handling than this if multiple + // devices are found. + switch { + case len(ip2Clients) == 1: + return ip2Clients[0], nil + case len(ip1Clients) == 1: + return ip1Clients[0], nil + case len(ppp1Clients) == 1: + return ppp1Clients[0], nil + default: + return nil, errors.New("multiple or no services found") + } +} + +func UPNP_ForwardPort(ctx context.Context, + NewRemoteHost string, + NewExternalPort uint16, + NewProtocol string, + NewInternalPort uint16, + NewInternalClient string, + NewPortMappingDescription string, + NewLeaseDuration uint32, +) error { + client, err := PickRouterClient(ctx) + if err != nil { + return err + } + + return client.AddPortMapping( + NewRemoteHost, + // External port number to expose to Internet: + NewExternalPort, + // Forward TCP (this could be "UDP" if we wanted that instead). + NewProtocol, + // Internal port number on the LAN to forward to. + // Some routers might not support this being different to the external + // port number. + NewInternalPort, + // Internal address on the LAN we want to forward to. + NewInternalClient, + // Enabled: + true, + // Informational description for the client requesting the port forwarding. + NewPortMappingDescription, + // How long should the port forward last for in seconds. + // If you want to keep it open for longer and potentially across router + // resets, you might want to periodically request before this elapses. + NewLeaseDuration, + ) +} + +func AskForMapping(xl *xlog.Logger, remoteGetAddrs []string, localIps []string, localAddr net.Addr, description string) { + + xl.Tracef("makeRouterToNatThisHole: %v, localIps %v, localAddr=%v", remoteGetAddrs, localIps, localAddr.String()) + + targetAddr := remoteGetAddrs[0] + remoteAddrPort, err := netip.ParseAddrPort(targetAddr) + if err != nil { + xl.Errorf("netip.ParseAddrPort error: %v. parse: %v", err, targetAddr) + return + } + + localAddrStr := localAddr.String() + localAddrPort, err := netip.ParseAddrPort(localAddrStr) + if err != nil { + xl.Errorf("netip.ParseAddrPort local error: %v. parse: %v", err, localAddrStr) + + return + } + + targetForwardTo := "" + if len(localIps) == 1 { + targetForwardTo = localIps[0] + } else { + targetForwardToIp, err := gateway.DiscoverInterface() + if err != nil { + xl.Warnf("load Default interface error:%v", err) + } else { + targetForwardTo = targetForwardToIp.String() + } + } + + if targetForwardTo == "" && len(localIps) > 1 { + targetForwardTo = localIps[0] + } + + ctx, _ := context.WithTimeout(context.Background(), 50*time.Millisecond) + + xl.Infof("UPNP_ForwardPort: remoteAddrPort=%v, localAddrPort=%v, targetForwardToLocal=%v", remoteAddrPort, localAddrPort, targetForwardTo) + err = UPNP_ForwardPort( + ctx, + /*NewRemoteHost*/ remoteAddrPort.Addr().String(), + /*NewExternalPort*/ remoteAddrPort.Port(), + /*NewProtocol*/ "UDP", + + /*NewInternalPort*/ + localAddrPort.Port(), + /*NewInternalClient*/ targetForwardTo, + /*NewPortMappingDescription*/ description, + /*NewLeaseDuration*/ 360, + ) + if err != nil { + xl.Warnf("UPNP_ForwardPort error: %v.", err) + + return + } + + xl.Tracef("UPNP_ForwardPort done") + +}