mirror of https://github.com/fatedier/frp.git
Compare commits
5 Commits
2f64d0a5c5
...
5b57440798
Author | SHA1 | Date |
---|---|---|
李林哲 | 5b57440798 | |
fatedier | e81b36c5ba | |
Weltolk | d0d396becb | |
fatedier | ee3892798d | |
lilinzhe | 5d87105619 |
|
@ -804,7 +804,7 @@ You can disable this feature by modify `frps.toml` and `frpc.toml`:
|
|||
|
||||
```toml
|
||||
# frps.toml and frpc.toml, must be same
|
||||
tcpMux = false
|
||||
transport.tcpMux = false
|
||||
```
|
||||
|
||||
### Support KCP Protocol
|
||||
|
@ -983,7 +983,7 @@ The HTTP request will have the `Host` header rewritten to `Host: dev.example.com
|
|||
|
||||
### Setting other HTTP Headers
|
||||
|
||||
Similar to `Host`, You can override other HTTP request headers with proxy type `http`.
|
||||
Similar to `Host`, You can override other HTTP request and response headers with proxy type `http`.
|
||||
|
||||
```toml
|
||||
# frpc.toml
|
||||
|
@ -995,9 +995,10 @@ localPort = 80
|
|||
customDomains = ["test.example.com"]
|
||||
hostHeaderRewrite = "dev.example.com"
|
||||
requestHeaders.set.x-from-where = "frp"
|
||||
responseHeaders.set.foo = "bar"
|
||||
```
|
||||
|
||||
In this example, it will set header `x-from-where: frp` in the HTTP request.
|
||||
In this example, it will set header `x-from-where: frp` in the HTTP request and `foo: bar` in the HTTP response.
|
||||
|
||||
### Get Real IP
|
||||
|
||||
|
|
|
@ -1,6 +1,13 @@
|
|||
### Notable Changes
|
||||
|
||||
We have optimized the heartbeat mechanism when tcpmux is enabled (enabled by default). The default value of `heartbeatInterval` has been adjusted to -1. This update ensures that when tcpmux is active, the client does not send additional heartbeats to the server. Since tcpmux incorporates its own heartbeat system, this change effectively reduces unnecessary data consumption, streamlining communication efficiency between client and server.
|
||||
|
||||
When connecting to frps versions older than v0.39.0 might encounter compatibility issues due to changes in the heartbeat mechanism. As a temporary workaround, setting the `heartbeatInterval` to 30 can help maintain stable connectivity with these older versions. We recommend updating to the latest frps version to leverage full functionality and improvements.
|
||||
|
||||
### Features
|
||||
|
||||
* Show tcpmux proxies on the frps dashboard.
|
||||
* `http` proxy can modify the response header. For example, `responseHeaders.set.foo = "bar"` will add a new header `foo: bar` to the response.
|
||||
|
||||
### Fixes
|
||||
|
||||
|
|
|
@ -20,8 +20,6 @@ import (
|
|||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/samber/lo"
|
||||
|
||||
"github.com/fatedier/frp/client/proxy"
|
||||
"github.com/fatedier/frp/client/visitor"
|
||||
"github.com/fatedier/frp/pkg/auth"
|
||||
|
@ -236,10 +234,8 @@ func (ctl *Control) registerMsgHandlers() {
|
|||
func (ctl *Control) heartbeatWorker() {
|
||||
xl := ctl.xl
|
||||
|
||||
// TODO(fatedier): Change default value of HeartbeatInterval to -1 if tcpmux is enabled.
|
||||
// Users can still enable heartbeat feature by setting HeartbeatInterval to a positive value.
|
||||
if ctl.sessionCtx.Common.Transport.HeartbeatInterval > 0 {
|
||||
// send heartbeat to server
|
||||
// Send heartbeat to server.
|
||||
sendHeartBeat := func() (bool, error) {
|
||||
xl.Debugf("send heartbeat to server")
|
||||
pingMsg := &msg.Ping{}
|
||||
|
@ -263,10 +259,8 @@ func (ctl *Control) heartbeatWorker() {
|
|||
)
|
||||
}
|
||||
|
||||
// Check heartbeat timeout only if TCPMux is not enabled and users don't disable heartbeat feature.
|
||||
if ctl.sessionCtx.Common.Transport.HeartbeatInterval > 0 && ctl.sessionCtx.Common.Transport.HeartbeatTimeout > 0 &&
|
||||
!lo.FromPtr(ctl.sessionCtx.Common.Transport.TCPMux) {
|
||||
|
||||
// Check heartbeat timeout.
|
||||
if ctl.sessionCtx.Common.Transport.HeartbeatInterval > 0 && ctl.sessionCtx.Common.Transport.HeartbeatTimeout > 0 {
|
||||
go wait.Until(func() {
|
||||
if time.Since(ctl.lastPong.Load().(time.Time)) > time.Duration(ctl.sessionCtx.Common.Transport.HeartbeatTimeout)*time.Second {
|
||||
xl.Warnf("heartbeat timeout")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -209,6 +209,7 @@ locations = ["/", "/pic"]
|
|||
# routeByHTTPUser = abc
|
||||
hostHeaderRewrite = "example.com"
|
||||
requestHeaders.set.x-from-where = "frp"
|
||||
responseHeaders.set.foo = "bar"
|
||||
healthCheck.type = "http"
|
||||
# frpc will send a GET http request '/status' to local http service
|
||||
# http service is alive when it return 2xx http response code
|
||||
|
@ -336,6 +337,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 +372,5 @@ maxRetriesAnHour = 8
|
|||
minRetryInterval = 90
|
||||
# fallbackTo = "stcp_visitor"
|
||||
# fallbackTimeoutMs = 500
|
||||
allowToUseUPNP = false
|
||||
upnpPortMappingDescription = "helper-port-mapping"
|
||||
|
|
3
go.mod
3
go.mod
|
@ -46,7 +46,9 @@ require (
|
|||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/google/go-cmp v0.6.0 // 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
|
||||
|
@ -60,6 +62,7 @@ require (
|
|||
github.com/prometheus/common v0.48.0 // indirect
|
||||
github.com/prometheus/procfs v0.12.0 // 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
|
||||
|
|
6
go.sum
6
go.sum
|
@ -66,9 +66,13 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
|||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
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=
|
||||
|
@ -127,6 +131,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/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
|
@ -194,6 +199,7 @@ golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2
|
|||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/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.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
|
||||
|
|
|
@ -136,8 +136,14 @@ func (c *ClientTransportConfig) Complete() {
|
|||
c.PoolCount = util.EmptyOr(c.PoolCount, 1)
|
||||
c.TCPMux = util.EmptyOr(c.TCPMux, lo.ToPtr(true))
|
||||
c.TCPMuxKeepaliveInterval = util.EmptyOr(c.TCPMuxKeepaliveInterval, 60)
|
||||
c.HeartbeatInterval = util.EmptyOr(c.HeartbeatInterval, 30)
|
||||
c.HeartbeatTimeout = util.EmptyOr(c.HeartbeatTimeout, 90)
|
||||
if lo.FromPtr(c.TCPMux) {
|
||||
// If TCPMux is enabled, heartbeat of application layer is unnecessary because we can rely on heartbeat in tcpmux.
|
||||
c.HeartbeatInterval = util.EmptyOr(c.HeartbeatInterval, -1)
|
||||
c.HeartbeatTimeout = util.EmptyOr(c.HeartbeatTimeout, -1)
|
||||
} else {
|
||||
c.HeartbeatInterval = util.EmptyOr(c.HeartbeatInterval, 30)
|
||||
c.HeartbeatTimeout = util.EmptyOr(c.HeartbeatTimeout, 90)
|
||||
}
|
||||
c.QUIC.Complete()
|
||||
c.TLS.Complete()
|
||||
}
|
||||
|
|
|
@ -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"`
|
||||
}
|
||||
|
|
|
@ -291,6 +291,7 @@ type HTTPProxyConfig struct {
|
|||
HTTPPassword string `json:"httpPassword,omitempty"`
|
||||
HostHeaderRewrite string `json:"hostHeaderRewrite,omitempty"`
|
||||
RequestHeaders HeaderOperations `json:"requestHeaders,omitempty"`
|
||||
ResponseHeaders HeaderOperations `json:"responseHeaders,omitempty"`
|
||||
RouteByHTTPUser string `json:"routeByHTTPUser,omitempty"`
|
||||
}
|
||||
|
||||
|
@ -304,6 +305,7 @@ func (c *HTTPProxyConfig) MarshalToMsg(m *msg.NewProxy) {
|
|||
m.HTTPUser = c.HTTPUser
|
||||
m.HTTPPwd = c.HTTPPassword
|
||||
m.Headers = c.RequestHeaders.Set
|
||||
m.ResponseHeaders = c.ResponseHeaders.Set
|
||||
m.RouteByHTTPUser = c.RouteByHTTPUser
|
||||
}
|
||||
|
||||
|
@ -317,6 +319,7 @@ func (c *HTTPProxyConfig) UnmarshalFromMsg(m *msg.NewProxy) {
|
|||
c.HTTPUser = m.HTTPUser
|
||||
c.HTTPPassword = m.HTTPPwd
|
||||
c.RequestHeaders.Set = m.Headers
|
||||
c.ResponseHeaders.Set = m.ResponseHeaders
|
||||
c.RouteByHTTPUser = m.RouteByHTTPUser
|
||||
}
|
||||
|
||||
|
@ -411,6 +414,8 @@ type XTCPProxyConfig struct {
|
|||
|
||||
Secretkey string `json:"secretKey,omitempty"`
|
||||
AllowUsers []string `json:"allowUsers,omitempty"`
|
||||
|
||||
XTCPConfigUPNPMixin
|
||||
}
|
||||
|
||||
func (c *XTCPProxyConfig) MarshalToMsg(m *msg.NewProxy) {
|
||||
|
|
|
@ -179,7 +179,12 @@ func (c *ServerTransportConfig) Complete() {
|
|||
c.TCPMuxKeepaliveInterval = util.EmptyOr(c.TCPMuxKeepaliveInterval, 60)
|
||||
c.TCPKeepAlive = util.EmptyOr(c.TCPKeepAlive, 7200)
|
||||
c.MaxPoolCount = util.EmptyOr(c.MaxPoolCount, 5)
|
||||
c.HeartbeatTimeout = util.EmptyOr(c.HeartbeatTimeout, 90)
|
||||
if lo.FromPtr(c.TCPMux) {
|
||||
// If TCPMux is enabled, heartbeat of application layer is unnecessary because we can rely on heartbeat in tcpmux.
|
||||
c.HeartbeatTimeout = util.EmptyOr(c.HeartbeatTimeout, -1)
|
||||
} else {
|
||||
c.HeartbeatTimeout = util.EmptyOr(c.HeartbeatTimeout, 90)
|
||||
}
|
||||
c.QUIC.Complete()
|
||||
if c.TLS.TrustedCaFile != "" {
|
||||
c.TLS.Force = true
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -121,6 +121,7 @@ type NewProxy struct {
|
|||
HTTPPwd string `json:"http_pwd,omitempty"`
|
||||
HostHeaderRewrite string `json:"host_header_rewrite,omitempty"`
|
||||
Headers map[string]string `json:"headers,omitempty"`
|
||||
ResponseHeaders map[string]string `json:"response_headers,omitempty"`
|
||||
RouteByHTTPUser string `json:"route_by_http_user,omitempty"`
|
||||
|
||||
// stcp, sudp, xtcp
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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")
|
||||
|
||||
}
|
|
@ -63,9 +63,9 @@ func NewHTTPReverseProxy(option HTTPReverseProxyOptions, vhostRouter *Routers) *
|
|||
req := r.Out
|
||||
req.URL.Scheme = "http"
|
||||
reqRouteInfo := req.Context().Value(RouteInfoKey).(*RequestRouteInfo)
|
||||
oldHost, _ := httppkg.CanonicalHost(reqRouteInfo.Host)
|
||||
originalHost, _ := httppkg.CanonicalHost(reqRouteInfo.Host)
|
||||
|
||||
rc := rp.GetRouteConfig(oldHost, reqRouteInfo.URL, reqRouteInfo.HTTPUser)
|
||||
rc := req.Context().Value(RouteConfigKey).(*RouteConfig)
|
||||
if rc != nil {
|
||||
if rc.RewriteHost != "" {
|
||||
req.Host = rc.RewriteHost
|
||||
|
@ -77,7 +77,7 @@ func NewHTTPReverseProxy(option HTTPReverseProxyOptions, vhostRouter *Routers) *
|
|||
endpoint, _ = rc.ChooseEndpointFn()
|
||||
reqRouteInfo.Endpoint = endpoint
|
||||
log.Tracef("choose endpoint name [%s] for http request host [%s] path [%s] httpuser [%s]",
|
||||
endpoint, oldHost, reqRouteInfo.URL, reqRouteInfo.HTTPUser)
|
||||
endpoint, originalHost, reqRouteInfo.URL, reqRouteInfo.HTTPUser)
|
||||
}
|
||||
// Set {domain}.{location}.{routeByHTTPUser}.{endpoint} as URL host here to let http transport reuse connections.
|
||||
req.URL.Host = rc.Domain + "." +
|
||||
|
@ -92,6 +92,15 @@ func NewHTTPReverseProxy(option HTTPReverseProxyOptions, vhostRouter *Routers) *
|
|||
req.URL.Host = req.Host
|
||||
}
|
||||
},
|
||||
ModifyResponse: func(r *http.Response) error {
|
||||
rc := r.Request.Context().Value(RouteConfigKey).(*RouteConfig)
|
||||
if rc != nil {
|
||||
for k, v := range rc.ResponseHeaders {
|
||||
r.Header.Set(k, v)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
// Create a connection to one proxy routed by route policy.
|
||||
Transport: &http.Transport{
|
||||
ResponseHeaderTimeout: rp.responseHeaderTimeout,
|
||||
|
@ -157,14 +166,6 @@ func (rp *HTTPReverseProxy) GetRouteConfig(domain, location, routeByHTTPUser str
|
|||
return nil
|
||||
}
|
||||
|
||||
func (rp *HTTPReverseProxy) GetHeaders(domain, location, routeByHTTPUser string) (headers map[string]string) {
|
||||
vr, ok := rp.getVhost(domain, location, routeByHTTPUser)
|
||||
if ok {
|
||||
headers = vr.payload.(*RouteConfig).Headers
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// CreateConnection create a new connection by route config
|
||||
func (rp *HTTPReverseProxy) CreateConnection(reqRouteInfo *RequestRouteInfo, byEndpoint bool) (net.Conn, error) {
|
||||
host, _ := httppkg.CanonicalHost(reqRouteInfo.Host)
|
||||
|
@ -305,8 +306,13 @@ func (rp *HTTPReverseProxy) injectRequestInfoToCtx(req *http.Request) *http.Requ
|
|||
RemoteAddr: req.RemoteAddr,
|
||||
URLHost: req.URL.Host,
|
||||
}
|
||||
|
||||
originalHost, _ := httppkg.CanonicalHost(reqRouteInfo.Host)
|
||||
rc := rp.GetRouteConfig(originalHost, reqRouteInfo.URL, reqRouteInfo.HTTPUser)
|
||||
|
||||
newctx := req.Context()
|
||||
newctx = context.WithValue(newctx, RouteInfoKey, reqRouteInfo)
|
||||
newctx = context.WithValue(newctx, RouteConfigKey, rc)
|
||||
return req.Clone(newctx)
|
||||
}
|
||||
|
||||
|
|
|
@ -29,7 +29,8 @@ import (
|
|||
type RouteInfo string
|
||||
|
||||
const (
|
||||
RouteInfoKey RouteInfo = "routeInfo"
|
||||
RouteInfoKey RouteInfo = "routeInfo"
|
||||
RouteConfigKey RouteInfo = "routeConfig"
|
||||
)
|
||||
|
||||
type RequestRouteInfo struct {
|
||||
|
@ -113,6 +114,7 @@ type RouteConfig struct {
|
|||
Username string
|
||||
Password string
|
||||
Headers map[string]string
|
||||
ResponseHeaders map[string]string
|
||||
RouteByHTTPUser string
|
||||
|
||||
CreateConnFn CreateConnFunc
|
||||
|
|
|
@ -297,20 +297,18 @@ func (ctl *Control) GetWorkConn() (workConn net.Conn, err error) {
|
|||
}
|
||||
|
||||
func (ctl *Control) heartbeatWorker() {
|
||||
xl := ctl.xl
|
||||
|
||||
// Don't need application heartbeat if TCPMux is enabled,
|
||||
// yamux will do same thing.
|
||||
// TODO(fatedier): let default HeartbeatTimeout to -1 if TCPMux is enabled. Users can still set it to positive value to enable it.
|
||||
if !lo.FromPtr(ctl.serverCfg.Transport.TCPMux) && ctl.serverCfg.Transport.HeartbeatTimeout > 0 {
|
||||
go wait.Until(func() {
|
||||
if time.Since(ctl.lastPing.Load().(time.Time)) > time.Duration(ctl.serverCfg.Transport.HeartbeatTimeout)*time.Second {
|
||||
xl.Warnf("heartbeat timeout")
|
||||
ctl.conn.Close()
|
||||
return
|
||||
}
|
||||
}, time.Second, ctl.doneCh)
|
||||
if ctl.serverCfg.Transport.HeartbeatTimeout <= 0 {
|
||||
return
|
||||
}
|
||||
|
||||
xl := ctl.xl
|
||||
go wait.Until(func() {
|
||||
if time.Since(ctl.lastPing.Load().(time.Time)) > time.Duration(ctl.serverCfg.Transport.HeartbeatTimeout)*time.Second {
|
||||
xl.Warnf("heartbeat timeout")
|
||||
ctl.conn.Close()
|
||||
return
|
||||
}
|
||||
}, time.Second, ctl.doneCh)
|
||||
}
|
||||
|
||||
// block until Control closed
|
||||
|
|
|
@ -58,6 +58,7 @@ func (pxy *HTTPProxy) Run() (remoteAddr string, err error) {
|
|||
RewriteHost: pxy.cfg.HostHeaderRewrite,
|
||||
RouteByHTTPUser: pxy.cfg.RouteByHTTPUser,
|
||||
Headers: pxy.cfg.RequestHeaders.Set,
|
||||
ResponseHeaders: pxy.cfg.ResponseHeaders.Set,
|
||||
Username: pxy.cfg.HTTPUser,
|
||||
Password: pxy.cfg.HTTPPassword,
|
||||
CreateConnFn: pxy.GetRealConn,
|
||||
|
|
|
@ -267,7 +267,7 @@ var _ = ginkgo.Describe("[Feature: HTTP]", func() {
|
|||
Ensure()
|
||||
})
|
||||
|
||||
ginkgo.It("Modify headers", func() {
|
||||
ginkgo.It("Modify request headers", func() {
|
||||
vhostHTTPPort := f.AllocPort()
|
||||
serverConf := getDefaultServerConf(vhostHTTPPort)
|
||||
|
||||
|
@ -292,7 +292,6 @@ var _ = ginkgo.Describe("[Feature: HTTP]", func() {
|
|||
|
||||
f.RunProcesses([]string{serverConf}, []string{clientConf})
|
||||
|
||||
// not set auth header
|
||||
framework.NewRequestExpect(f).Port(vhostHTTPPort).
|
||||
RequestModify(func(r *request.Request) {
|
||||
r.HTTP().HTTPHost("normal.example.com")
|
||||
|
@ -301,6 +300,40 @@ var _ = ginkgo.Describe("[Feature: HTTP]", func() {
|
|||
Ensure()
|
||||
})
|
||||
|
||||
ginkgo.It("Modify response headers", func() {
|
||||
vhostHTTPPort := f.AllocPort()
|
||||
serverConf := getDefaultServerConf(vhostHTTPPort)
|
||||
|
||||
localPort := f.AllocPort()
|
||||
localServer := httpserver.New(
|
||||
httpserver.WithBindPort(localPort),
|
||||
httpserver.WithHandler(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
w.WriteHeader(200)
|
||||
})),
|
||||
)
|
||||
f.RunServer("", localServer)
|
||||
|
||||
clientConf := consts.DefaultClientConfig
|
||||
clientConf += fmt.Sprintf(`
|
||||
[[proxies]]
|
||||
name = "test"
|
||||
type = "http"
|
||||
localPort = %d
|
||||
customDomains = ["normal.example.com"]
|
||||
responseHeaders.set.x-from-where = "frp"
|
||||
`, localPort)
|
||||
|
||||
f.RunProcesses([]string{serverConf}, []string{clientConf})
|
||||
|
||||
framework.NewRequestExpect(f).Port(vhostHTTPPort).
|
||||
RequestModify(func(r *request.Request) {
|
||||
r.HTTP().HTTPHost("normal.example.com")
|
||||
}).
|
||||
Ensure(func(res *request.Response) bool {
|
||||
return res.Header.Get("X-From-Where") == "frp"
|
||||
})
|
||||
})
|
||||
|
||||
ginkgo.It("Host Header Rewrite", func() {
|
||||
vhostHTTPPort := f.AllocPort()
|
||||
serverConf := getDefaultServerConf(vhostHTTPPort)
|
||||
|
|
Loading…
Reference in New Issue