Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

nathole: upnp support #4110

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion client/proxy/xtcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand All @@ -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()
Expand All @@ -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
Expand Down
20 changes: 19 additions & 1 deletion client/visitor/xtcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
5 changes: 5 additions & 0 deletions conf/frpc_full_example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]]
Expand Down Expand Up @@ -368,3 +371,5 @@ maxRetriesAnHour = 8
minRetryInterval = 90
# fallbackTo = "stcp_visitor"
# fallbackTimeoutMs = 500
allowToUseUPNP = false
upnpPortMappingDescription = "helper-port-mapping"
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down
5 changes: 5 additions & 0 deletions pkg/config/v1/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}
2 changes: 2 additions & 0 deletions pkg/config/v1/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
2 changes: 2 additions & 0 deletions pkg/config/v1/visitor.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
11 changes: 10 additions & 1 deletion pkg/nathole/nathole.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
Expand Down
173 changes: 173 additions & 0 deletions pkg/nathole/upnp/upnp.go
Original file line number Diff line number Diff line change
@@ -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")

}