Compare commits

...

27 Commits

Author SHA1 Message Date
fatedier
f1b59940da added a 30s timeout for frpc subcommands to avoid long delays 2024-07-30 18:06:53 +08:00
Yurun
e8045194cd Fix loginFailExit = false bug (#4354)
* Fixed the issue that when loginFailExit = false, the frpc stop command cannot be stopped correctly if the server is not successfully connected after startup

* Update Release.md
2024-07-30 11:19:26 +08:00
fatedier
69cc422edf client plugin: added plugin tls2raw (#4341) 2024-07-25 14:28:17 +08:00
fatedier
b4d5d8c756 plugin https2http&https2https: return 421 if host not match sni (#4323) 2024-07-09 10:50:16 +08:00
fatedier
c6f9d8d403 update sponsors (#4303) 2024-06-26 14:51:34 +08:00
fatedier
939c490768 Add http2http client plugin with hostHeaderRewrite and requestHeaders support (#4275) 2024-06-12 17:30:10 +08:00
fatedier
f390e4a401 add sponsor (#4265) 2024-06-05 16:53:29 +08:00
fatedier
77990c31ef fix ini configuration default values (#4250) 2024-05-30 10:36:30 +08:00
fatedier
e680acf42d android: only use google dns server when the default dns server cannot be obtained (#4236) 2024-05-23 16:09:58 +08:00
fatedier
522e2c94c1 config: return error if plugin type is empty (#4235) 2024-05-23 14:52:12 +08:00
fatedier
301515d2e8 update the default value of transport.tcpMuxKeepaliveInterval (#4231) 2024-05-21 12:00:35 +08:00
fatedier
f0442d0cd5 plugin: fix http2 not enabled for https2http and https2https plugin (#4230) 2024-05-21 11:26:52 +08:00
fatedier
9ced717d69 update build-and-push-image.yml (#4206) 2024-05-07 19:14:09 +08:00
fatedier
92cb0b30c2 update version (#4204) 2024-05-07 18:05:36 +08:00
fatedier
e81b36c5ba support responseHeaders.set for proxy type http (#4192) 2024-04-29 15:53:45 +08:00
Weltolk
d0d396becb Update README.md (#4190) 2024-04-29 11:50:56 +08:00
fatedier
ee3892798d change default value of heartbeat interval and timeout when tcpmux enabled (#4186) 2024-04-28 20:48:44 +08:00
fatedier
405969085f client: add StatusExporter in service (#4182) 2024-04-25 20:20:39 +08:00
fatedier
c1893ee1b4 adjust arm compilation configuration (#4181) 2024-04-25 13:08:41 +08:00
ddscentral
eaae212d2d Makefile.cross-compiles: Fix softfloat flag not being honored for mipsle. (#4176) 2024-04-22 19:50:58 +08:00
fatedier
885278c045 README add releated projects (#4167) 2024-04-17 20:41:14 +08:00
fatedier
2626d6ed92 support linux/arm v6 (#4154) 2024-04-12 21:21:28 +08:00
fatedier
f3a71bc08f show tcpmux proxies on the frps dashboard (#4152) 2024-04-11 22:40:42 +08:00
fatedier
dd7e2e8473 return 504 instead of 404 for proxy type http request timeout (#4151) 2024-04-11 20:19:08 +08:00
gopherfarm
07946e9752 fix: revert gorilla/websocket from 1.5.1 to 1.5.0 (#4149) 2024-04-11 11:01:02 +08:00
fatedier
e52727e01c update golib (#4142) 2024-04-10 12:05:22 +08:00
fatedier
ba937e9fbf update README (#4139) 2024-04-09 15:03:32 +08:00
75 changed files with 1057 additions and 416 deletions

View File

@@ -2,7 +2,7 @@ name: Build Image and Publish to Dockerhub & GPR
on:
release:
types: [ created ]
types: [ published ]
workflow_dispatch:
inputs:
tag:
@@ -61,7 +61,7 @@ jobs:
echo "TAG_FRPS_GPR=ghcr.io/fatedier/frps:${{ env.TAG_NAME }}" >> $GITHUB_ENV
- name: Build and push frpc
uses: docker/build-push-action@v4
uses: docker/build-push-action@v5
with:
context: .
file: ./dockerfiles/Dockerfile-for-frpc

View File

@@ -1,6 +1,6 @@
service:
golangci-lint-version: 1.57.x # use the fixed version to not introduce new linters unexpectedly
run:
concurrency: 4
# timeout for analysis, e.g. 30s, 5m, default is 1m
@@ -86,9 +86,6 @@ linters-settings:
severity: "low"
confidence: "low"
excludes:
- G102
- G112
- G306
- G401
- G402
- G404

View File

@@ -2,22 +2,34 @@ export PATH := $(PATH):`go env GOPATH`/bin
export GO111MODULE=on
LDFLAGS := -s -w
os-archs=darwin:amd64 darwin:arm64 freebsd:amd64 linux:amd64 linux:arm linux:arm64 windows:amd64 windows:arm64 linux:mips64 linux:mips64le linux:mips:softfloat linux:mipsle:softfloat linux:riscv64 android:arm64
os-archs=darwin:amd64 darwin:arm64 freebsd:amd64 linux:amd64 linux:arm:7 linux:arm:5 linux:arm64 windows:amd64 windows:arm64 linux:mips64 linux:mips64le linux:mips:softfloat linux:mipsle:softfloat linux:riscv64 android:arm64
all: build
build: app
app:
@$(foreach n, $(os-archs),\
os=$(shell echo "$(n)" | cut -d : -f 1);\
arch=$(shell echo "$(n)" | cut -d : -f 2);\
gomips=$(shell echo "$(n)" | cut -d : -f 3);\
target_suffix=$${os}_$${arch};\
echo "Build $${os}-$${arch}...";\
env CGO_ENABLED=0 GOOS=$${os} GOARCH=$${arch} GOMIPS=$${gomips} go build -trimpath -ldflags "$(LDFLAGS)" -tags frpc -o ./release/frpc_$${target_suffix} ./cmd/frpc;\
env CGO_ENABLED=0 GOOS=$${os} GOARCH=$${arch} GOMIPS=$${gomips} go build -trimpath -ldflags "$(LDFLAGS)" -tags frps -o ./release/frps_$${target_suffix} ./cmd/frps;\
echo "Build $${os}-$${arch} done";\
@$(foreach n, $(os-archs), \
os=$(shell echo "$(n)" | cut -d : -f 1); \
arch=$(shell echo "$(n)" | cut -d : -f 2); \
extra=$(shell echo "$(n)" | cut -d : -f 3); \
flags=''; \
target_suffix=$${os}_$${arch}; \
if [ "$${os}" = "linux" ] && [ "$${arch}" = "arm" ] && [ "$${extra}" != "" ] ; then \
if [ "$${extra}" = "7" ]; then \
flags=GOARM=7; \
target_suffix=$${os}_arm_hf; \
elif [ "$${extra}" = "5" ]; then \
flags=GOARM=5; \
target_suffix=$${os}_arm; \
fi; \
elif [ "$${os}" = "linux" ] && ([ "$${arch}" = "mips" ] || [ "$${arch}" = "mipsle" ]) && [ "$${extra}" != "" ] ; then \
flags=GOMIPS=$${extra}; \
fi; \
echo "Build $${os}-$${arch}$${extra:+ ($${extra})}..."; \
env CGO_ENABLED=0 GOOS=$${os} GOARCH=$${arch} $${flags} go build -trimpath -ldflags "$(LDFLAGS)" -tags frpc -o ./release/frpc_$${target_suffix} ./cmd/frpc; \
env CGO_ENABLED=0 GOOS=$${os} GOARCH=$${arch} $${flags} go build -trimpath -ldflags "$(LDFLAGS)" -tags frps -o ./release/frps_$${target_suffix} ./cmd/frps; \
echo "Build $${os}-$${arch}$${extra:+ ($${extra})} done"; \
)
@mv ./release/frpc_windows_amd64 ./release/frpc_windows_amd64.exe
@mv ./release/frps_windows_amd64 ./release/frps_windows_amd64.exe

View File

@@ -11,11 +11,17 @@
<!--gold sponsors start-->
<p align="center">
<a href="https://workos.com/?utm_campaign=github_repo&utm_medium=referral&utm_content=frp&utm_source=github" target="_blank">
<img width="350px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_workos.png">
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_workos.png">
</a>
<a>&nbsp</a>
</p>
<p align="center">
<a href="https://github.com/daytonaio/daytona" target="_blank">
<img width="360px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_daytona.png">
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_daytona.png">
</a>
</p>
<p align="center">
<a href="https://github.com/beclab/terminus" target="_blank">
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_terminusos.jpeg">
</a>
</p>
<!--gold sponsors end-->
@@ -82,6 +88,7 @@ frp also offers a P2P connect mode.
* [Client Plugins](#client-plugins)
* [Server Manage Plugins](#server-manage-plugins)
* [SSH Tunnel Gateway](#ssh-tunnel-gateway)
* [Releated Projects](#releated-projects)
* [Contributing](#contributing)
* [Donation](#donation)
* [GitHub Sponsors](#github-sponsors)
@@ -351,7 +358,6 @@ You may substitute `https2https` for the plugin, and point the `localAddr` to a
# frpc.toml
serverAddr = "x.x.x.x"
serverPort = 7000
vhostHTTPSPort = 443
[[proxies]]
name = "test_https2http"
@@ -804,7 +810,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 +989,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,15 +1001,16 @@ 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
#### HTTP X-Forwarded-For
This feature is for http proxy only.
This feature is for `http` proxies or proxies with the `https2http` and `https2https` plugins enabled.
You can get user's real IP from HTTP request headers `X-Forwarded-For`.
@@ -1244,6 +1251,11 @@ frpc tcp --proxy_name "test-tcp" --local_ip 127.0.0.1 --local_port 8080 --remote
Please refer to this [document](/doc/ssh_tunnel_gateway.md) for more information.
## Releated Projects
* [gofrp/plugin](https://github.com/gofrp/plugin) - A repository for frp plugins that contains a variety of plugins implemented based on the frp extension mechanism, meeting the customization needs of different scenarios.
* [gofrp/tiny-frpc](https://github.com/gofrp/tiny-frpc) - A lightweight version of the frp client (around 3.5MB at minimum) implemented using the ssh protocol, supporting some of the most commonly used features, suitable for devices with limited resources.
## Contributing
Interested in getting involved? We would like to help you!

View File

@@ -13,11 +13,17 @@ frp 是一个专注于内网穿透的高性能的反向代理应用,支持 TCP
<!--gold sponsors start-->
<p align="center">
<a href="https://workos.com/?utm_campaign=github_repo&utm_medium=referral&utm_content=frp&utm_source=github" target="_blank">
<img width="350px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_workos.png">
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_workos.png">
</a>
<a>&nbsp</a>
</p>
<p align="center">
<a href="https://github.com/daytonaio/daytona" target="_blank">
<img width="360px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_daytona.png">
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_daytona.png">
</a>
</p>
<p align="center">
<a href="https://github.com/beclab/terminus" target="_blank">
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_terminusos.jpeg">
</a>
</p>
<!--gold sponsors end-->
@@ -72,7 +78,12 @@ frp 是一个免费且开源的项目,我们欢迎任何人为其开发和进
* 贡献代码请提交 PR 至 dev 分支master 分支仅用于发布稳定可用版本。
* 如果你有任何其他方面的问题或合作,欢迎发送邮件至 fatedier@gmail.com 。
**提醒:和项目相关的问题最好在 [issues](https://github.com/fatedier/frp/issues) 中反馈,这样方便其他有类似问题的人可以快速查找解决方法,并且也避免了我们重复回答一些问题。**
**提醒:和项目相关的问题在 [issues](https://github.com/fatedier/frp/issues) 中反馈,这样方便其他有类似问题的人可以快速查找解决方法,并且也避免了我们重复回答一些问题。**
## 关联项目
* [gofrp/plugin](https://github.com/gofrp/plugin) - frp 插件仓库,收录了基于 frp 扩展机制实现的各种插件,满足各种场景下的定制化需求。
* [gofrp/tiny-frpc](https://github.com/gofrp/tiny-frpc) - 基于 ssh 协议实现的 frp 客户端的精简版本(最低约 3.5MB 左右),支持常用的部分功能,适用于资源有限的设备。
## 赞助
@@ -93,7 +104,3 @@ frp 是一个免费且开源的项目,我们欢迎任何人为其开发和进
如果您想了解更多 frp 相关技术以及更新详解,或者寻求任何 frp 使用方面的帮助,都可以通过微信扫描下方的二维码付费加入知识星球的官方社群:
![zsxq](/doc/pic/zsxq.jpg)
### 微信支付捐赠
![donate-wechatpay](/doc/pic/donate-wechatpay.png)

View File

@@ -1,7 +1,8 @@
### Features
* `https2http` and `https2https` plugin now supports `X-Forwared-For` header.
* Added a new plugin `tls2raw`: Enables TLS termination and forwarding of decrypted raw traffic to local service.
* Added a default timeout of 30 seconds for the frpc subcommands to prevent commands from being stuck for a long time due to network issues.
### Fixes
* `X-Forwared-For` header is now correctly set in the request to the backend server for proxy type http.
* Fixed the issue that when `loginFailExit = false`, the frpc stop command cannot be stopped correctly if the server is not successfully connected after startup.

File diff suppressed because one or more lines are too long

View File

@@ -4,7 +4,7 @@
<head>
<meta charset="utf-8">
<title>frps dashboard</title>
<script type="module" crossorigin src="./index-Q42Pu2_S.js"></script>
<script type="module" crossorigin src="./index-82-40HIG.js"></script>
<link rel="stylesheet" crossorigin href="./index-rzPDshRD.css">
</head>

View File

@@ -253,7 +253,7 @@ func (svr *Service) apiPutConfig(w http.ResponseWriter, r *http.Request) {
return
}
if err := os.WriteFile(svr.configFilePath, body, 0o644); err != nil {
if err := os.WriteFile(svr.configFilePath, body, 0o600); err != nil {
res.Code = 500
res.Msg = fmt.Sprintf("write content to frpc config file error: %v", err)
log.Warnf("%s", res.Msg)

View File

@@ -24,7 +24,7 @@ import (
"sync"
"time"
libdial "github.com/fatedier/golib/net/dial"
libnet "github.com/fatedier/golib/net"
fmux "github.com/hashicorp/yamux"
quic "github.com/quic-go/quic-go"
"github.com/samber/lo"
@@ -169,44 +169,44 @@ func (c *defaultConnectorImpl) realConnect() (net.Conn, error) {
}
}
proxyType, addr, auth, err := libdial.ParseProxyURL(c.cfg.Transport.ProxyURL)
proxyType, addr, auth, err := libnet.ParseProxyURL(c.cfg.Transport.ProxyURL)
if err != nil {
xl.Errorf("fail to parse proxy url")
return nil, err
}
dialOptions := []libdial.DialOption{}
dialOptions := []libnet.DialOption{}
protocol := c.cfg.Transport.Protocol
switch protocol {
case "websocket":
protocol = "tcp"
dialOptions = append(dialOptions, libdial.WithAfterHook(libdial.AfterHook{Hook: netpkg.DialHookWebsocket(protocol, "")}))
dialOptions = append(dialOptions, libdial.WithAfterHook(libdial.AfterHook{
dialOptions = append(dialOptions, libnet.WithAfterHook(libnet.AfterHook{Hook: netpkg.DialHookWebsocket(protocol, "")}))
dialOptions = append(dialOptions, libnet.WithAfterHook(libnet.AfterHook{
Hook: netpkg.DialHookCustomTLSHeadByte(tlsConfig != nil, lo.FromPtr(c.cfg.Transport.TLS.DisableCustomTLSFirstByte)),
}))
dialOptions = append(dialOptions, libdial.WithTLSConfig(tlsConfig))
dialOptions = append(dialOptions, libnet.WithTLSConfig(tlsConfig))
case "wss":
protocol = "tcp"
dialOptions = append(dialOptions, libdial.WithTLSConfigAndPriority(100, tlsConfig))
dialOptions = append(dialOptions, libnet.WithTLSConfigAndPriority(100, tlsConfig))
// Make sure that if it is wss, the websocket hook is executed after the tls hook.
dialOptions = append(dialOptions, libdial.WithAfterHook(libdial.AfterHook{Hook: netpkg.DialHookWebsocket(protocol, tlsConfig.ServerName), Priority: 110}))
dialOptions = append(dialOptions, libnet.WithAfterHook(libnet.AfterHook{Hook: netpkg.DialHookWebsocket(protocol, tlsConfig.ServerName), Priority: 110}))
default:
dialOptions = append(dialOptions, libdial.WithAfterHook(libdial.AfterHook{
dialOptions = append(dialOptions, libnet.WithAfterHook(libnet.AfterHook{
Hook: netpkg.DialHookCustomTLSHeadByte(tlsConfig != nil, lo.FromPtr(c.cfg.Transport.TLS.DisableCustomTLSFirstByte)),
}))
dialOptions = append(dialOptions, libdial.WithTLSConfig(tlsConfig))
dialOptions = append(dialOptions, libnet.WithTLSConfig(tlsConfig))
}
if c.cfg.Transport.ConnectServerLocalIP != "" {
dialOptions = append(dialOptions, libdial.WithLocalAddr(c.cfg.Transport.ConnectServerLocalIP))
dialOptions = append(dialOptions, libnet.WithLocalAddr(c.cfg.Transport.ConnectServerLocalIP))
}
dialOptions = append(dialOptions,
libdial.WithProtocol(protocol),
libdial.WithTimeout(time.Duration(c.cfg.Transport.DialServerTimeout)*time.Second),
libdial.WithKeepAlive(time.Duration(c.cfg.Transport.DialServerKeepAlive)*time.Second),
libdial.WithProxy(proxyType, addr),
libdial.WithProxyAuth(auth),
libnet.WithProtocol(protocol),
libnet.WithTimeout(time.Duration(c.cfg.Transport.DialServerTimeout)*time.Second),
libnet.WithKeepAlive(time.Duration(c.cfg.Transport.DialServerKeepAlive)*time.Second),
libnet.WithProxy(proxyType, addr),
libnet.WithProxyAuth(auth),
)
conn, err := libdial.DialContext(
conn, err := libnet.DialContext(
c.ctx,
net.JoinHostPort(c.cfg.ServerAddr, strconv.Itoa(c.cfg.ServerPort)),
dialOptions...,

View File

@@ -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")

View File

@@ -25,7 +25,7 @@ import (
"time"
libio "github.com/fatedier/golib/io"
libdial "github.com/fatedier/golib/net/dial"
libnet "github.com/fatedier/golib/net"
pp "github.com/pires/go-proxyproto"
"golang.org/x/time/rate"
@@ -192,14 +192,14 @@ func (pxy *BaseProxy) HandleTCPWorkConnection(workConn net.Conn, m *msg.StartWor
if pxy.proxyPlugin != nil {
// if plugin is set, let plugin handle connection first
xl.Debugf("handle by plugin: %s", pxy.proxyPlugin.Name())
pxy.proxyPlugin.Handle(remote, workConn, &extraInfo)
pxy.proxyPlugin.Handle(pxy.ctx, remote, workConn, &extraInfo)
xl.Debugf("handle by plugin finished")
return
}
localConn, err := libdial.Dial(
localConn, err := libnet.Dial(
net.JoinHostPort(baseCfg.LocalIP, strconv.Itoa(baseCfg.LocalPort)),
libdial.WithTimeout(10*time.Second),
libnet.WithTimeout(10*time.Second),
)
if err != nil {
workConn.Close()

View File

@@ -169,6 +169,15 @@ func (svr *Service) Run(ctx context.Context) error {
netpkg.SetDefaultDNSAddress(svr.common.DNSServer)
}
if svr.webServer != nil {
go func() {
log.Infof("admin server listen on %s", svr.webServer.Address())
if err := svr.webServer.Run(); err != nil {
log.Warnf("admin server exit with error: %v", err)
}
}()
}
// first login to frps
svr.loopLoginUntilSuccess(10*time.Second, lo.FromPtr(svr.common.LoginFailExit))
if svr.ctl == nil {
@@ -179,14 +188,6 @@ func (svr *Service) Run(ctx context.Context) error {
go svr.keepControllerWorking()
if svr.webServer != nil {
go func() {
log.Infof("admin server listen on %s", svr.webServer.Address())
if err := svr.webServer.Run(); err != nil {
log.Warnf("admin server exit with error: %v", err)
}
}()
}
<-svr.ctx.Done()
svr.stop()
return nil
@@ -380,18 +381,31 @@ func (svr *Service) stop() {
}
}
// TODO(fatedier): Use StatusExporter to provide query interfaces instead of directly using methods from the Service.
func (svr *Service) GetProxyStatus(name string) (*proxy.WorkingStatus, error) {
func (svr *Service) getProxyStatus(name string) (*proxy.WorkingStatus, bool) {
svr.ctlMu.RLock()
ctl := svr.ctl
svr.ctlMu.RUnlock()
if ctl == nil {
return nil, fmt.Errorf("control is not running")
return nil, false
}
ws, ok := ctl.pm.GetProxyStatus(name)
if !ok {
return nil, fmt.Errorf("proxy [%s] is not found", name)
}
return ws, nil
return ctl.pm.GetProxyStatus(name)
}
func (svr *Service) StatusExporter() StatusExporter {
return &statusExporterImpl{
getProxyStatusFunc: svr.getProxyStatus,
}
}
type StatusExporter interface {
GetProxyStatus(name string) (*proxy.WorkingStatus, bool)
}
type statusExporterImpl struct {
getProxyStatusFunc func(name string) (*proxy.WorkingStatus, bool)
}
func (s *statusExporterImpl) GetProxyStatus(name string) (*proxy.WorkingStatus, bool) {
return s.getProxyStatusFunc(name)
}

View File

@@ -15,9 +15,11 @@
package sub
import (
"context"
"fmt"
"os"
"strings"
"time"
"github.com/rodaine/table"
"github.com/spf13/cobra"
@@ -27,24 +29,24 @@ import (
clientsdk "github.com/fatedier/frp/pkg/sdk/client"
)
var adminAPITimeout = 30 * time.Second
func init() {
rootCmd.AddCommand(NewAdminCommand(
"reload",
"Hot-Reload frpc configuration",
ReloadHandler,
))
commands := []struct {
name string
description string
handler func(*v1.ClientCommonConfig) error
}{
{"reload", "Hot-Reload frpc configuration", ReloadHandler},
{"status", "Overview of all proxies status", StatusHandler},
{"stop", "Stop the running frpc", StopHandler},
}
rootCmd.AddCommand(NewAdminCommand(
"status",
"Overview of all proxies status",
StatusHandler,
))
rootCmd.AddCommand(NewAdminCommand(
"stop",
"Stop the running frpc",
StopHandler,
))
for _, cmdConfig := range commands {
cmd := NewAdminCommand(cmdConfig.name, cmdConfig.description, cmdConfig.handler)
cmd.Flags().DurationVar(&adminAPITimeout, "api-timeout", adminAPITimeout, "Timeout for admin API calls")
rootCmd.AddCommand(cmd)
}
}
func NewAdminCommand(name, short string, handler func(*v1.ClientCommonConfig) error) *cobra.Command {
@@ -73,7 +75,9 @@ func NewAdminCommand(name, short string, handler func(*v1.ClientCommonConfig) er
func ReloadHandler(clientCfg *v1.ClientCommonConfig) error {
client := clientsdk.New(clientCfg.WebServer.Addr, clientCfg.WebServer.Port)
client.SetAuth(clientCfg.WebServer.User, clientCfg.WebServer.Password)
if err := client.Reload(strictConfigMode); err != nil {
ctx, cancel := context.WithTimeout(context.Background(), adminAPITimeout)
defer cancel()
if err := client.Reload(ctx, strictConfigMode); err != nil {
return err
}
fmt.Println("reload success")
@@ -83,7 +87,9 @@ func ReloadHandler(clientCfg *v1.ClientCommonConfig) error {
func StatusHandler(clientCfg *v1.ClientCommonConfig) error {
client := clientsdk.New(clientCfg.WebServer.Addr, clientCfg.WebServer.Port)
client.SetAuth(clientCfg.WebServer.User, clientCfg.WebServer.Password)
res, err := client.GetAllProxyStatus()
ctx, cancel := context.WithTimeout(context.Background(), adminAPITimeout)
defer cancel()
res, err := client.GetAllProxyStatus(ctx)
if err != nil {
return err
}
@@ -109,7 +115,9 @@ func StatusHandler(clientCfg *v1.ClientCommonConfig) error {
func StopHandler(clientCfg *v1.ClientCommonConfig) error {
client := clientsdk.New(clientCfg.WebServer.Addr, clientCfg.WebServer.Port)
client.SetAuth(clientCfg.WebServer.User, clientCfg.WebServer.Password)
if err := client.Stop(); err != nil {
ctx, cancel := context.WithTimeout(context.Background(), adminAPITimeout)
defer cancel()
if err := client.Stop(ctx); err != nil {
return err
}
fmt.Println("stop success")

View File

@@ -76,7 +76,7 @@ transport.poolCount = 5
# Specify keep alive interval for tcp mux.
# only valid if tcpMux is enabled.
# transport.tcpMuxKeepaliveInterval = 60
# transport.tcpMuxKeepaliveInterval = 30
# Communication protocol used to connect to server
# supports tcp, kcp, quic, websocket and wss now, default is tcp
@@ -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
@@ -314,6 +315,26 @@ localAddr = "127.0.0.1:443"
hostHeaderRewrite = "127.0.0.1"
requestHeaders.set.x-from-where = "frp"
[[proxies]]
name = "plugin_http2http"
type = "tcp"
remotePort = 6007
[proxies.plugin]
type = "http2http"
localAddr = "127.0.0.1:80"
hostHeaderRewrite = "127.0.0.1"
requestHeaders.set.x-from-where = "frp"
[[proxies]]
name = "plugin_tls2raw"
type = "https"
remotePort = 6008
[proxies.plugin]
type = "tls2raw"
localAddr = "127.0.0.1:80"
crtPath = "./server.crt"
keyPath = "./server.key"
[[proxies]]
name = "secret_tcp"
# If the type is secret tcp, remotePort is useless

View File

@@ -34,7 +34,7 @@ transport.maxPoolCount = 5
# Specify keep alive interval for tcp mux.
# only valid if tcpMux is true.
# transport.tcpMuxKeepaliveInterval = 60
# transport.tcpMuxKeepaliveInterval = 30
# tcpKeepalive specifies the interval between keep-alive probes for an active network connection between frpc and frps.
# If negative, keep-alive probes are disabled.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

BIN
doc/pic/sponsor_lokal.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

6
go.mod
View File

@@ -5,10 +5,10 @@ go 1.22
require (
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
github.com/coreos/go-oidc/v3 v3.10.0
github.com/fatedier/golib v0.4.2
github.com/fatedier/golib v0.5.0
github.com/google/uuid v1.6.0
github.com/gorilla/mux v1.8.1
github.com/gorilla/websocket v1.5.1
github.com/gorilla/websocket v1.5.0
github.com/hashicorp/yamux v0.1.1
github.com/onsi/ginkgo/v2 v2.17.1
github.com/onsi/gomega v1.32.0
@@ -35,7 +35,7 @@ require (
)
require (
github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c // indirect
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect

12
go.sum
View File

@@ -1,6 +1,6 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c h1:/IBSNwUN8+eKzUzbJPqhK839ygXJ82sde8x3ogr6R28=
github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8=
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
@@ -24,8 +24,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatedier/golib v0.4.2 h1:k+ZBdUFTTipnP1RHfEhGbzyShRdz/rZtFGnjpXG9D9c=
github.com/fatedier/golib v0.4.2/go.mod h1:gpu+1vXxtJ072NYaNsn/YWgojDL8Ap2kFZQtbzT2qkg=
github.com/fatedier/golib v0.5.0 h1:hNcH7hgfIFqVWbP+YojCCAj4eO94pPf4dEF8lmq2jWs=
github.com/fatedier/golib v0.5.0/go.mod h1:W6kIYkIFxHsTzbgqg5piCxIiDo4LzwgTY6R5W8l9NFQ=
github.com/fatedier/yamux v0.0.0-20230628132301-7aca4898904d h1:ynk1ra0RUqDWQfvFi5KtMiSobkVQ3cNc0ODb8CfIETo=
github.com/fatedier/yamux v0.0.0-20230628132301-7aca4898904d/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
github.com/go-jose/go-jose/v4 v4.0.1 h1:QVEPDE3OluqXBQZDcnNvQrInro2h0e4eqNbnZSWqS6U=
@@ -64,8 +64,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
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.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
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/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=

View File

@@ -19,48 +19,55 @@ mkdir -p ./release/packages
os_all='linux windows darwin freebsd android'
arch_all='386 amd64 arm arm64 mips64 mips64le mips mipsle riscv64'
extra_all='_ hf'
cd ./release
for os in $os_all; do
for arch in $arch_all; do
frp_dir_name="frp_${frp_version}_${os}_${arch}"
frp_path="./packages/frp_${frp_version}_${os}_${arch}"
for extra in $extra_all; do
suffix="${os}_${arch}"
if [ "x${extra}" != x"_" ]; then
suffix="${os}_${arch}_${extra}"
fi
frp_dir_name="frp_${frp_version}_${suffix}"
frp_path="./packages/frp_${frp_version}_${suffix}"
if [ "x${os}" = x"windows" ]; then
if [ ! -f "./frpc_${os}_${arch}.exe" ]; then
continue
fi
if [ ! -f "./frps_${os}_${arch}.exe" ]; then
continue
fi
mkdir ${frp_path}
mv ./frpc_${os}_${arch}.exe ${frp_path}/frpc.exe
mv ./frps_${os}_${arch}.exe ${frp_path}/frps.exe
else
if [ ! -f "./frpc_${os}_${arch}" ]; then
continue
fi
if [ ! -f "./frps_${os}_${arch}" ]; then
continue
fi
mkdir ${frp_path}
mv ./frpc_${os}_${arch} ${frp_path}/frpc
mv ./frps_${os}_${arch} ${frp_path}/frps
fi
cp ../LICENSE ${frp_path}
cp -f ../conf/frpc.toml ${frp_path}
cp -f ../conf/frps.toml ${frp_path}
if [ "x${os}" = x"windows" ]; then
if [ ! -f "./frpc_${os}_${arch}.exe" ]; then
continue
fi
if [ ! -f "./frps_${os}_${arch}.exe" ]; then
continue
fi
mkdir ${frp_path}
mv ./frpc_${os}_${arch}.exe ${frp_path}/frpc.exe
mv ./frps_${os}_${arch}.exe ${frp_path}/frps.exe
else
if [ ! -f "./frpc_${suffix}" ]; then
continue
fi
if [ ! -f "./frps_${suffix}" ]; then
continue
fi
mkdir ${frp_path}
mv ./frpc_${suffix} ${frp_path}/frpc
mv ./frps_${suffix} ${frp_path}/frps
fi
cp ../LICENSE ${frp_path}
cp -f ../conf/frpc.toml ${frp_path}
cp -f ../conf/frps.toml ${frp_path}
# packages
cd ./packages
if [ "x${os}" = x"windows" ]; then
zip -rq ${frp_dir_name}.zip ${frp_dir_name}
else
tar -zcf ${frp_dir_name}.tar.gz ${frp_dir_name}
fi
cd ..
rm -rf ${frp_path}
# packages
cd ./packages
if [ "x${os}" = x"windows" ]; then
zip -rq ${frp_dir_name}.zip ${frp_dir_name}
else
tar -zcf ${frp_dir_name}.tar.gz ${frp_dir_name}
fi
cd ..
rm -rf ${frp_path}
done
done
done

View File

@@ -345,35 +345,19 @@ func copySection(source, target *ini.Section) {
}
// GetDefaultClientConf returns a client configuration with default values.
// Note: Some default values here will be set to empty and will be converted to them
// new configuration through the 'Complete' function to set them as the default
// values of the new configuration.
func GetDefaultClientConf() ClientCommonConf {
return ClientCommonConf{
ClientConfig: legacyauth.GetDefaultClientConf(),
ServerAddr: "0.0.0.0",
ServerPort: 7000,
NatHoleSTUNServer: "stun.easyvoip.com:3478",
DialServerTimeout: 10,
DialServerKeepAlive: 7200,
HTTPProxy: os.Getenv("http_proxy"),
LogFile: "console",
LogWay: "console",
LogLevel: "info",
LogMaxDays: 3,
AdminAddr: "127.0.0.1",
PoolCount: 1,
TCPMux: true,
TCPMuxKeepaliveInterval: 60,
LoginFailExit: true,
Start: make([]string, 0),
Protocol: "tcp",
QUICKeepalivePeriod: 10,
QUICMaxIdleTimeout: 30,
QUICMaxIncomingStreams: 100000,
Start: make([]string, 0),
TLSEnable: true,
DisableCustomTLSFirstByte: true,
HeartbeatInterval: 30,
HeartbeatTimeout: 90,
Metas: make(map[string]string),
UDPPacketSize: 1500,
IncludeConfigFiles: make([]string, 0),
}
}

View File

@@ -200,34 +200,20 @@ type ServerCommonConf struct {
NatHoleAnalysisDataReserveHours int64 `ini:"nat_hole_analysis_data_reserve_hours" json:"nat_hole_analysis_data_reserve_hours"`
}
// GetDefaultServerConf returns a server configuration with reasonable
// defaults.
// GetDefaultServerConf returns a server configuration with reasonable defaults.
// Note: Some default values here will be set to empty and will be converted to them
// new configuration through the 'Complete' function to set them as the default
// values of the new configuration.
func GetDefaultServerConf() ServerCommonConf {
return ServerCommonConf{
ServerConfig: legacyauth.GetDefaultServerConf(),
BindAddr: "0.0.0.0",
BindPort: 7000,
QUICKeepalivePeriod: 10,
QUICMaxIdleTimeout: 30,
QUICMaxIncomingStreams: 100000,
VhostHTTPTimeout: 60,
DashboardAddr: "0.0.0.0",
LogFile: "console",
LogWay: "console",
LogLevel: "info",
LogMaxDays: 3,
DetailedErrorsToClient: true,
TCPMux: true,
TCPMuxKeepaliveInterval: 60,
TCPKeepAlive: 7200,
AllowPorts: make(map[int]struct{}),
MaxPoolCount: 5,
MaxPortsPerClient: 0,
HeartbeatTimeout: 90,
UserConnTimeout: 10,
HTTPPlugins: make(map[string]HTTPPluginOptions),
UDPPacketSize: 1500,
NatHoleAnalysisDataReserveHours: 7 * 24,
ServerConfig: legacyauth.GetDefaultServerConf(),
DashboardAddr: "0.0.0.0",
LogFile: "console",
LogWay: "console",
DetailedErrorsToClient: true,
TCPMux: true,
AllowPorts: make(map[int]struct{}),
HTTPPlugins: make(map[string]HTTPPluginOptions),
}
}

View File

@@ -135,9 +135,15 @@ func (c *ClientTransportConfig) Complete() {
c.ProxyURL = util.EmptyOr(c.ProxyURL, os.Getenv("http_proxy"))
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)
c.TCPMuxKeepaliveInterval = util.EmptyOr(c.TCPMuxKeepaliveInterval, 30)
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()
}

View File

@@ -17,11 +17,18 @@ package v1
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"reflect"
"github.com/samber/lo"
"github.com/fatedier/frp/pkg/util/util"
)
type ClientPluginOptions interface{}
type ClientPluginOptions interface {
Complete()
}
type TypedClientPluginOptions struct {
Type string `json:"type"`
@@ -42,7 +49,7 @@ func (c *TypedClientPluginOptions) UnmarshalJSON(b []byte) error {
c.Type = typeStruct.Type
if c.Type == "" {
return nil
return errors.New("plugin type is empty")
}
v, ok := clientPluginOptionsTypeMap[typeStruct.Type]
@@ -63,14 +70,20 @@ func (c *TypedClientPluginOptions) UnmarshalJSON(b []byte) error {
return nil
}
func (c *TypedClientPluginOptions) MarshalJSON() ([]byte, error) {
return json.Marshal(c.ClientPluginOptions)
}
const (
PluginHTTP2HTTPS = "http2https"
PluginHTTPProxy = "http_proxy"
PluginHTTPS2HTTP = "https2http"
PluginHTTPS2HTTPS = "https2https"
PluginHTTP2HTTP = "http2http"
PluginSocks5 = "socks5"
PluginStaticFile = "static_file"
PluginUnixDomainSocket = "unix_domain_socket"
PluginTLS2Raw = "tls2raw"
)
var clientPluginOptionsTypeMap = map[string]reflect.Type{
@@ -78,9 +91,11 @@ var clientPluginOptionsTypeMap = map[string]reflect.Type{
PluginHTTPProxy: reflect.TypeOf(HTTPProxyPluginOptions{}),
PluginHTTPS2HTTP: reflect.TypeOf(HTTPS2HTTPPluginOptions{}),
PluginHTTPS2HTTPS: reflect.TypeOf(HTTPS2HTTPSPluginOptions{}),
PluginHTTP2HTTP: reflect.TypeOf(HTTP2HTTPPluginOptions{}),
PluginSocks5: reflect.TypeOf(Socks5PluginOptions{}),
PluginStaticFile: reflect.TypeOf(StaticFilePluginOptions{}),
PluginUnixDomainSocket: reflect.TypeOf(UnixDomainSocketPluginOptions{}),
PluginTLS2Raw: reflect.TypeOf(TLS2RawPluginOptions{}),
}
type HTTP2HTTPSPluginOptions struct {
@@ -90,36 +105,61 @@ type HTTP2HTTPSPluginOptions struct {
RequestHeaders HeaderOperations `json:"requestHeaders,omitempty"`
}
func (o *HTTP2HTTPSPluginOptions) Complete() {}
type HTTPProxyPluginOptions struct {
Type string `json:"type,omitempty"`
HTTPUser string `json:"httpUser,omitempty"`
HTTPPassword string `json:"httpPassword,omitempty"`
}
func (o *HTTPProxyPluginOptions) Complete() {}
type HTTPS2HTTPPluginOptions struct {
Type string `json:"type,omitempty"`
LocalAddr string `json:"localAddr,omitempty"`
HostHeaderRewrite string `json:"hostHeaderRewrite,omitempty"`
RequestHeaders HeaderOperations `json:"requestHeaders,omitempty"`
EnableHTTP2 *bool `json:"enableHTTP2,omitempty"`
CrtPath string `json:"crtPath,omitempty"`
KeyPath string `json:"keyPath,omitempty"`
}
func (o *HTTPS2HTTPPluginOptions) Complete() {
o.EnableHTTP2 = util.EmptyOr(o.EnableHTTP2, lo.ToPtr(true))
}
type HTTPS2HTTPSPluginOptions struct {
Type string `json:"type,omitempty"`
LocalAddr string `json:"localAddr,omitempty"`
HostHeaderRewrite string `json:"hostHeaderRewrite,omitempty"`
RequestHeaders HeaderOperations `json:"requestHeaders,omitempty"`
EnableHTTP2 *bool `json:"enableHTTP2,omitempty"`
CrtPath string `json:"crtPath,omitempty"`
KeyPath string `json:"keyPath,omitempty"`
}
func (o *HTTPS2HTTPSPluginOptions) Complete() {
o.EnableHTTP2 = util.EmptyOr(o.EnableHTTP2, lo.ToPtr(true))
}
type HTTP2HTTPPluginOptions struct {
Type string `json:"type,omitempty"`
LocalAddr string `json:"localAddr,omitempty"`
HostHeaderRewrite string `json:"hostHeaderRewrite,omitempty"`
RequestHeaders HeaderOperations `json:"requestHeaders,omitempty"`
}
func (o *HTTP2HTTPPluginOptions) Complete() {}
type Socks5PluginOptions struct {
Type string `json:"type,omitempty"`
Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"`
}
func (o *Socks5PluginOptions) Complete() {}
type StaticFilePluginOptions struct {
Type string `json:"type,omitempty"`
LocalPath string `json:"localPath,omitempty"`
@@ -128,7 +168,20 @@ type StaticFilePluginOptions struct {
HTTPPassword string `json:"httpPassword,omitempty"`
}
func (o *StaticFilePluginOptions) Complete() {}
type UnixDomainSocketPluginOptions struct {
Type string `json:"type,omitempty"`
UnixPath string `json:"unixPath,omitempty"`
}
func (o *UnixDomainSocketPluginOptions) Complete() {}
type TLS2RawPluginOptions struct {
Type string `json:"type,omitempty"`
LocalAddr string `json:"localAddr,omitempty"`
CrtPath string `json:"crtPath,omitempty"`
KeyPath string `json:"keyPath,omitempty"`
}
func (o *TLS2RawPluginOptions) Complete() {}

View File

@@ -127,6 +127,10 @@ func (c *ProxyBaseConfig) Complete(namePrefix string) {
c.Name = lo.Ternary(namePrefix == "", "", namePrefix+".") + c.Name
c.LocalIP = util.EmptyOr(c.LocalIP, "127.0.0.1")
c.Transport.BandwidthLimitMode = util.EmptyOr(c.Transport.BandwidthLimitMode, types.BandwidthLimitModeClient)
if c.Plugin.ClientPluginOptions != nil {
c.Plugin.ClientPluginOptions.Complete()
}
}
func (c *ProxyBaseConfig) MarshalToMsg(m *msg.NewProxy) {
@@ -195,6 +199,10 @@ func (c *TypedProxyConfig) UnmarshalJSON(b []byte) error {
return nil
}
func (c *TypedProxyConfig) MarshalJSON() ([]byte, error) {
return json.Marshal(c.ProxyConfigurer)
}
type ProxyConfigurer interface {
Complete(namePrefix string)
GetBaseConfig() *ProxyBaseConfig
@@ -291,6 +299,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 +313,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 +327,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
}

View File

@@ -176,10 +176,15 @@ type ServerTransportConfig struct {
func (c *ServerTransportConfig) Complete() {
c.TCPMux = util.EmptyOr(c.TCPMux, lo.ToPtr(true))
c.TCPMuxKeepaliveInterval = util.EmptyOr(c.TCPMuxKeepaliveInterval, 60)
c.TCPMuxKeepaliveInterval = util.EmptyOr(c.TCPMuxKeepaliveInterval, 30)
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

View File

@@ -32,6 +32,8 @@ func ValidateClientPluginOptions(c v1.ClientPluginOptions) error {
return validateStaticFilePluginOptions(v)
case *v1.UnixDomainSocketPluginOptions:
return validateUnixDomainSocketPluginOptions(v)
case *v1.TLS2RawPluginOptions:
return validateTLS2RawPluginOptions(v)
}
return nil
}
@@ -70,3 +72,10 @@ func validateUnixDomainSocketPluginOptions(c *v1.UnixDomainSocketPluginOptions)
}
return nil
}
func validateTLS2RawPluginOptions(c *v1.TLS2RawPluginOptions) error {
if c.LocalAddr == "" {
return errors.New("localAddr is required")
}
return nil
}

View File

@@ -120,6 +120,10 @@ func (c *TypedVisitorConfig) UnmarshalJSON(b []byte) error {
return nil
}
func (c *TypedVisitorConfig) MarshalJSON() ([]byte, error) {
return json.Marshal(c.VisitorConfigurer)
}
func NewVisitorConfigurerByType(t VisitorType) VisitorConfigurer {
v, ok := visitorConfigTypeMap[t]
if !ok {

View File

@@ -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

View File

@@ -0,0 +1,94 @@
// Copyright 2024 The frp Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build !frps
package plugin
import (
"context"
"io"
stdlog "log"
"net"
"net/http"
"net/http/httputil"
"github.com/fatedier/golib/pool"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/util/log"
netpkg "github.com/fatedier/frp/pkg/util/net"
)
func init() {
Register(v1.PluginHTTP2HTTP, NewHTTP2HTTPPlugin)
}
type HTTP2HTTPPlugin struct {
opts *v1.HTTP2HTTPPluginOptions
l *Listener
s *http.Server
}
func NewHTTP2HTTPPlugin(options v1.ClientPluginOptions) (Plugin, error) {
opts := options.(*v1.HTTP2HTTPPluginOptions)
listener := NewProxyListener()
p := &HTTP2HTTPPlugin{
opts: opts,
l: listener,
}
rp := &httputil.ReverseProxy{
Rewrite: func(r *httputil.ProxyRequest) {
req := r.Out
req.URL.Scheme = "http"
req.URL.Host = p.opts.LocalAddr
if p.opts.HostHeaderRewrite != "" {
req.Host = p.opts.HostHeaderRewrite
}
for k, v := range p.opts.RequestHeaders.Set {
req.Header.Set(k, v)
}
},
BufferPool: pool.NewBuffer(32 * 1024),
ErrorLog: stdlog.New(log.NewWriteLogger(log.WarnLevel, 2), "", 0),
}
p.s = &http.Server{
Handler: rp,
ReadHeaderTimeout: 0,
}
go func() {
_ = p.s.Serve(listener)
}()
return p, nil
}
func (p *HTTP2HTTPPlugin) Handle(_ context.Context, conn io.ReadWriteCloser, realConn net.Conn, _ *ExtraInfo) {
wrapConn := netpkg.WrapReadWriteCloserToConn(conn, realConn)
_ = p.l.PutConn(wrapConn)
}
func (p *HTTP2HTTPPlugin) Name() string {
return v1.PluginHTTP2HTTP
}
func (p *HTTP2HTTPPlugin) Close() error {
return p.s.Close()
}

View File

@@ -17,13 +17,18 @@
package plugin
import (
"context"
"crypto/tls"
"io"
stdlog "log"
"net"
"net/http"
"net/http/httputil"
"github.com/fatedier/golib/pool"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/util/log"
netpkg "github.com/fatedier/frp/pkg/util/net"
)
@@ -67,7 +72,9 @@ func NewHTTP2HTTPSPlugin(options v1.ClientPluginOptions) (Plugin, error) {
req.Header.Set(k, v)
}
},
Transport: tr,
Transport: tr,
BufferPool: pool.NewBuffer(32 * 1024),
ErrorLog: stdlog.New(log.NewWriteLogger(log.WarnLevel, 2), "", 0),
}
p.s = &http.Server{
@@ -82,7 +89,7 @@ func NewHTTP2HTTPSPlugin(options v1.ClientPluginOptions) (Plugin, error) {
return p, nil
}
func (p *HTTP2HTTPSPlugin) Handle(conn io.ReadWriteCloser, realConn net.Conn, _ *ExtraInfo) {
func (p *HTTP2HTTPSPlugin) Handle(_ context.Context, conn io.ReadWriteCloser, realConn net.Conn, _ *ExtraInfo) {
wrapConn := netpkg.WrapReadWriteCloserToConn(conn, realConn)
_ = p.l.PutConn(wrapConn)
}

View File

@@ -18,6 +18,7 @@ package plugin
import (
"bufio"
"context"
"encoding/base64"
"io"
"net"
@@ -54,7 +55,8 @@ func NewHTTPProxyPlugin(options v1.ClientPluginOptions) (Plugin, error) {
}
hp.s = &http.Server{
Handler: hp,
Handler: hp,
ReadHeaderTimeout: 60 * time.Second,
}
go func() {
@@ -67,7 +69,7 @@ func (hp *HTTPProxy) Name() string {
return v1.PluginHTTPProxy
}
func (hp *HTTPProxy) Handle(conn io.ReadWriteCloser, realConn net.Conn, _ *ExtraInfo) {
func (hp *HTTPProxy) Handle(_ context.Context, conn io.ReadWriteCloser, realConn net.Conn, _ *ExtraInfo) {
wrapConn := netpkg.WrapReadWriteCloserToConn(conn, realConn)
sc, rd := libnet.NewSharedConn(wrapConn)

View File

@@ -17,15 +17,23 @@
package plugin
import (
"context"
"crypto/tls"
"fmt"
"io"
stdlog "log"
"net"
"net/http"
"net/http/httputil"
"time"
"github.com/fatedier/golib/pool"
"github.com/samber/lo"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/transport"
httppkg "github.com/fatedier/frp/pkg/util/http"
"github.com/fatedier/frp/pkg/util/log"
netpkg "github.com/fatedier/frp/pkg/util/net"
)
@@ -63,44 +71,42 @@ func NewHTTPS2HTTPPlugin(options v1.ClientPluginOptions) (Plugin, error) {
req.Header.Set(k, v)
}
},
BufferPool: pool.NewBuffer(32 * 1024),
ErrorLog: stdlog.New(log.NewWriteLogger(log.WarnLevel, 2), "", 0),
}
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.TLS != nil {
tlsServerName, _ := httppkg.CanonicalHost(r.TLS.ServerName)
host, _ := httppkg.CanonicalHost(r.Host)
if tlsServerName != "" && tlsServerName != host {
w.WriteHeader(http.StatusMisdirectedRequest)
return
}
}
rp.ServeHTTP(w, r)
})
p.s = &http.Server{
Handler: rp,
}
var (
tlsConfig *tls.Config
err error
)
if opts.CrtPath != "" || opts.KeyPath != "" {
tlsConfig, err = p.genTLSConfig()
} else {
tlsConfig, err = transport.NewServerTLSConfig("", "", "")
tlsConfig.InsecureSkipVerify = true
}
tlsConfig, err := transport.NewServerTLSConfig(p.opts.CrtPath, p.opts.KeyPath, "")
if err != nil {
return nil, fmt.Errorf("gen TLS config error: %v", err)
}
ln := tls.NewListener(listener, tlsConfig)
p.s = &http.Server{
Handler: handler,
ReadHeaderTimeout: 60 * time.Second,
TLSConfig: tlsConfig,
}
if !lo.FromPtr(opts.EnableHTTP2) {
p.s.TLSNextProto = make(map[string]func(*http.Server, *tls.Conn, http.Handler))
}
go func() {
_ = p.s.Serve(ln)
_ = p.s.ServeTLS(listener, "", "")
}()
return p, nil
}
func (p *HTTPS2HTTPPlugin) genTLSConfig() (*tls.Config, error) {
cert, err := tls.LoadX509KeyPair(p.opts.CrtPath, p.opts.KeyPath)
if err != nil {
return nil, err
}
config := &tls.Config{Certificates: []tls.Certificate{cert}}
return config, nil
}
func (p *HTTPS2HTTPPlugin) Handle(conn io.ReadWriteCloser, realConn net.Conn, extra *ExtraInfo) {
func (p *HTTPS2HTTPPlugin) Handle(_ context.Context, conn io.ReadWriteCloser, realConn net.Conn, extra *ExtraInfo) {
wrapConn := netpkg.WrapReadWriteCloserToConn(conn, realConn)
if extra.SrcAddr != nil {
wrapConn.SetRemoteAddr(extra.SrcAddr)

View File

@@ -17,15 +17,23 @@
package plugin
import (
"context"
"crypto/tls"
"fmt"
"io"
stdlog "log"
"net"
"net/http"
"net/http/httputil"
"time"
"github.com/fatedier/golib/pool"
"github.com/samber/lo"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/transport"
httppkg "github.com/fatedier/frp/pkg/util/http"
"github.com/fatedier/frp/pkg/util/log"
netpkg "github.com/fatedier/frp/pkg/util/net"
)
@@ -68,45 +76,43 @@ func NewHTTPS2HTTPSPlugin(options v1.ClientPluginOptions) (Plugin, error) {
req.Header.Set(k, v)
}
},
Transport: tr,
Transport: tr,
BufferPool: pool.NewBuffer(32 * 1024),
ErrorLog: stdlog.New(log.NewWriteLogger(log.WarnLevel, 2), "", 0),
}
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.TLS != nil {
tlsServerName, _ := httppkg.CanonicalHost(r.TLS.ServerName)
host, _ := httppkg.CanonicalHost(r.Host)
if tlsServerName != "" && tlsServerName != host {
w.WriteHeader(http.StatusMisdirectedRequest)
return
}
}
rp.ServeHTTP(w, r)
})
p.s = &http.Server{
Handler: rp,
}
var (
tlsConfig *tls.Config
err error
)
if opts.CrtPath != "" || opts.KeyPath != "" {
tlsConfig, err = p.genTLSConfig()
} else {
tlsConfig, err = transport.NewServerTLSConfig("", "", "")
tlsConfig.InsecureSkipVerify = true
}
tlsConfig, err := transport.NewServerTLSConfig(p.opts.CrtPath, p.opts.KeyPath, "")
if err != nil {
return nil, fmt.Errorf("gen TLS config error: %v", err)
}
ln := tls.NewListener(listener, tlsConfig)
p.s = &http.Server{
Handler: handler,
ReadHeaderTimeout: 60 * time.Second,
TLSConfig: tlsConfig,
}
if !lo.FromPtr(opts.EnableHTTP2) {
p.s.TLSNextProto = make(map[string]func(*http.Server, *tls.Conn, http.Handler))
}
go func() {
_ = p.s.Serve(ln)
_ = p.s.ServeTLS(listener, "", "")
}()
return p, nil
}
func (p *HTTPS2HTTPSPlugin) genTLSConfig() (*tls.Config, error) {
cert, err := tls.LoadX509KeyPair(p.opts.CrtPath, p.opts.KeyPath)
if err != nil {
return nil, err
}
config := &tls.Config{Certificates: []tls.Certificate{cert}}
return config, nil
}
func (p *HTTPS2HTTPSPlugin) Handle(conn io.ReadWriteCloser, realConn net.Conn, extra *ExtraInfo) {
func (p *HTTPS2HTTPSPlugin) Handle(_ context.Context, conn io.ReadWriteCloser, realConn net.Conn, extra *ExtraInfo) {
wrapConn := netpkg.WrapReadWriteCloserToConn(conn, realConn)
if extra.SrcAddr != nil {
wrapConn.SetRemoteAddr(extra.SrcAddr)

View File

@@ -15,6 +15,7 @@
package plugin
import (
"context"
"fmt"
"io"
"net"
@@ -57,7 +58,7 @@ type ExtraInfo struct {
type Plugin interface {
Name() string
Handle(conn io.ReadWriteCloser, realConn net.Conn, extra *ExtraInfo)
Handle(ctx context.Context, conn io.ReadWriteCloser, realConn net.Conn, extra *ExtraInfo)
Close() error
}

View File

@@ -17,6 +17,7 @@
package plugin
import (
"context"
"io"
"log"
"net"
@@ -50,7 +51,7 @@ func NewSocks5Plugin(options v1.ClientPluginOptions) (p Plugin, err error) {
return
}
func (sp *Socks5Plugin) Handle(conn io.ReadWriteCloser, realConn net.Conn, _ *ExtraInfo) {
func (sp *Socks5Plugin) Handle(_ context.Context, conn io.ReadWriteCloser, realConn net.Conn, _ *ExtraInfo) {
defer conn.Close()
wrapConn := netpkg.WrapReadWriteCloserToConn(conn, realConn)
_ = sp.Server.ServeConn(wrapConn)

View File

@@ -17,6 +17,7 @@
package plugin
import (
"context"
"io"
"net"
"net/http"
@@ -60,7 +61,8 @@ func NewStaticFilePlugin(options v1.ClientPluginOptions) (Plugin, error) {
router.Use(netpkg.NewHTTPAuthMiddleware(opts.HTTPUser, opts.HTTPPassword).SetAuthFailDelay(200 * time.Millisecond).Middleware)
router.PathPrefix(prefix).Handler(netpkg.MakeHTTPGzipHandler(http.StripPrefix(prefix, http.FileServer(http.Dir(opts.LocalPath))))).Methods("GET")
sp.s = &http.Server{
Handler: router,
Handler: router,
ReadHeaderTimeout: 60 * time.Second,
}
go func() {
_ = sp.s.Serve(listener)
@@ -68,7 +70,7 @@ func NewStaticFilePlugin(options v1.ClientPluginOptions) (Plugin, error) {
return sp, nil
}
func (sp *StaticFilePlugin) Handle(conn io.ReadWriteCloser, realConn net.Conn, _ *ExtraInfo) {
func (sp *StaticFilePlugin) Handle(_ context.Context, conn io.ReadWriteCloser, realConn net.Conn, _ *ExtraInfo) {
wrapConn := netpkg.WrapReadWriteCloserToConn(conn, realConn)
_ = sp.l.PutConn(wrapConn)
}

View File

@@ -0,0 +1,83 @@
// Copyright 2024 The frp Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build !frps
package plugin
import (
"context"
"crypto/tls"
"io"
"net"
libio "github.com/fatedier/golib/io"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/transport"
netpkg "github.com/fatedier/frp/pkg/util/net"
"github.com/fatedier/frp/pkg/util/xlog"
)
func init() {
Register(v1.PluginTLS2Raw, NewTLS2RawPlugin)
}
type TLS2RawPlugin struct {
opts *v1.TLS2RawPluginOptions
tlsConfig *tls.Config
}
func NewTLS2RawPlugin(options v1.ClientPluginOptions) (Plugin, error) {
opts := options.(*v1.TLS2RawPluginOptions)
p := &TLS2RawPlugin{
opts: opts,
}
tlsConfig, err := transport.NewServerTLSConfig(p.opts.CrtPath, p.opts.KeyPath, "")
if err != nil {
return nil, err
}
p.tlsConfig = tlsConfig
return p, nil
}
func (p *TLS2RawPlugin) Handle(ctx context.Context, conn io.ReadWriteCloser, realConn net.Conn, _ *ExtraInfo) {
xl := xlog.FromContextSafe(ctx)
wrapConn := netpkg.WrapReadWriteCloserToConn(conn, realConn)
tlsConn := tls.Server(wrapConn, p.tlsConfig)
if err := tlsConn.Handshake(); err != nil {
xl.Warnf("tls handshake error: %v", err)
return
}
rawConn, err := net.Dial("tcp", p.opts.LocalAddr)
if err != nil {
xl.Warnf("dial to local addr error: %v", err)
return
}
libio.Join(tlsConn, rawConn)
}
func (p *TLS2RawPlugin) Name() string {
return v1.PluginTLS2Raw
}
func (p *TLS2RawPlugin) Close() error {
return nil
}

View File

@@ -17,12 +17,14 @@
package plugin
import (
"context"
"io"
"net"
libio "github.com/fatedier/golib/io"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/util/xlog"
)
func init() {
@@ -48,9 +50,11 @@ func NewUnixDomainSocketPlugin(options v1.ClientPluginOptions) (p Plugin, err er
return
}
func (uds *UnixDomainSocketPlugin) Handle(conn io.ReadWriteCloser, _ net.Conn, extra *ExtraInfo) {
func (uds *UnixDomainSocketPlugin) Handle(ctx context.Context, conn io.ReadWriteCloser, _ net.Conn, extra *ExtraInfo) {
xl := xlog.FromContextSafe(ctx)
localConn, err := net.DialUnix("unix", nil, uds.UnixAddr)
if err != nil {
xl.Warnf("dial to uds %s error: %v", uds.UnixAddr, err)
return
}
if extra.ProxyProtocolHeader != nil {

View File

@@ -1,6 +1,7 @@
package client
import (
"context"
"encoding/json"
"fmt"
"io"
@@ -31,8 +32,8 @@ func (c *Client) SetAuth(user, pwd string) {
c.authPwd = pwd
}
func (c *Client) GetProxyStatus(name string) (*client.ProxyStatusResp, error) {
req, err := http.NewRequest("GET", "http://"+c.address+"/api/status", nil)
func (c *Client) GetProxyStatus(ctx context.Context, name string) (*client.ProxyStatusResp, error) {
req, err := http.NewRequestWithContext(ctx, "GET", "http://"+c.address+"/api/status", nil)
if err != nil {
return nil, err
}
@@ -54,8 +55,8 @@ func (c *Client) GetProxyStatus(name string) (*client.ProxyStatusResp, error) {
return nil, fmt.Errorf("no proxy status found")
}
func (c *Client) GetAllProxyStatus() (client.StatusResp, error) {
req, err := http.NewRequest("GET", "http://"+c.address+"/api/status", nil)
func (c *Client) GetAllProxyStatus(ctx context.Context) (client.StatusResp, error) {
req, err := http.NewRequestWithContext(ctx, "GET", "http://"+c.address+"/api/status", nil)
if err != nil {
return nil, err
}
@@ -70,7 +71,7 @@ func (c *Client) GetAllProxyStatus() (client.StatusResp, error) {
return allStatus, nil
}
func (c *Client) Reload(strictMode bool) error {
func (c *Client) Reload(ctx context.Context, strictMode bool) error {
v := url.Values{}
if strictMode {
v.Set("strictConfig", "true")
@@ -79,7 +80,7 @@ func (c *Client) Reload(strictMode bool) error {
if len(v) > 0 {
queryStr = "?" + v.Encode()
}
req, err := http.NewRequest("GET", "http://"+c.address+"/api/reload"+queryStr, nil)
req, err := http.NewRequestWithContext(ctx, "GET", "http://"+c.address+"/api/reload"+queryStr, nil)
if err != nil {
return err
}
@@ -87,8 +88,8 @@ func (c *Client) Reload(strictMode bool) error {
return err
}
func (c *Client) Stop() error {
req, err := http.NewRequest("POST", "http://"+c.address+"/api/stop", nil)
func (c *Client) Stop(ctx context.Context) error {
req, err := http.NewRequestWithContext(ctx, "POST", "http://"+c.address+"/api/stop", nil)
if err != nil {
return err
}
@@ -96,16 +97,16 @@ func (c *Client) Stop() error {
return err
}
func (c *Client) GetConfig() (string, error) {
req, err := http.NewRequest("GET", "http://"+c.address+"/api/config", nil)
func (c *Client) GetConfig(ctx context.Context) (string, error) {
req, err := http.NewRequestWithContext(ctx, "GET", "http://"+c.address+"/api/config", nil)
if err != nil {
return "", err
}
return c.do(req)
}
func (c *Client) UpdateConfig(content string) error {
req, err := http.NewRequest("PUT", "http://"+c.address+"/api/config", strings.NewReader(content))
func (c *Client) UpdateConfig(ctx context.Context, content string) error {
req, err := http.NewRequestWithContext(ctx, "PUT", "http://"+c.address+"/api/config", strings.NewReader(content))
if err != nil {
return err
}

View File

@@ -363,11 +363,13 @@ func (s *TunnelServer) waitProxyStatusReady(name string, timeout time.Duration)
timer := time.NewTimer(timeout)
defer timer.Stop()
statusExporter := s.vc.Service().StatusExporter()
for {
select {
case <-ticker.C:
ps, err := s.vc.Service().GetProxyStatus(name)
if err != nil {
ps, ok := statusExporter.GetProxyStatus(name)
if !ok {
continue
}
switch ps.Phase {

View File

@@ -15,11 +15,20 @@
package log
import (
"bytes"
"os"
"github.com/fatedier/golib/log"
)
var (
TraceLevel = log.TraceLevel
DebugLevel = log.DebugLevel
InfoLevel = log.InfoLevel
WarnLevel = log.WarnLevel
ErrorLevel = log.ErrorLevel
)
var Logger *log.Logger
func init() {
@@ -77,3 +86,24 @@ func Debugf(format string, v ...interface{}) {
func Tracef(format string, v ...interface{}) {
Logger.Tracef(format, v...)
}
func Logf(level log.Level, offset int, format string, v ...interface{}) {
Logger.Logf(level, offset, format, v...)
}
type WriteLogger struct {
level log.Level
offset int
}
func NewWriteLogger(level log.Level, offset int) *WriteLogger {
return &WriteLogger{
level: level,
offset: offset,
}
}
func (w *WriteLogger) Write(p []byte) (n int, err error) {
Logger.Log(w.level, w.offset, string(bytes.TrimRight(p, "\n")))
return len(p), nil
}

View File

@@ -5,11 +5,11 @@ import (
"net"
"net/url"
libdial "github.com/fatedier/golib/net/dial"
libnet "github.com/fatedier/golib/net"
"golang.org/x/net/websocket"
)
func DialHookCustomTLSHeadByte(enableTLS bool, disableCustomTLSHeadByte bool) libdial.AfterHookFunc {
func DialHookCustomTLSHeadByte(enableTLS bool, disableCustomTLSHeadByte bool) libnet.AfterHookFunc {
return func(ctx context.Context, c net.Conn, addr string) (context.Context, net.Conn, error) {
if enableTLS && !disableCustomTLSHeadByte {
_, err := c.Write([]byte{byte(FRPTLSHeadByte)})
@@ -21,7 +21,7 @@ func DialHookCustomTLSHeadByte(enableTLS bool, disableCustomTLSHeadByte bool) li
}
}
func DialHookWebsocket(protocol string, host string) libdial.AfterHookFunc {
func DialHookWebsocket(protocol string, host string) libnet.AfterHookFunc {
return func(ctx context.Context, c net.Conn, addr string) (context.Context, net.Conn, error) {
if protocol != "wss" {
protocol = "ws"

View File

@@ -4,6 +4,7 @@ import (
"errors"
"net"
"net/http"
"time"
"golang.org/x/net/websocket"
)
@@ -39,8 +40,9 @@ func NewWebsocketListener(ln net.Listener) (wl *WebsocketListener) {
}))
wl.server = &http.Server{
Addr: ln.Addr().String(),
Handler: muxer,
Addr: ln.Addr().String(),
Handler: muxer,
ReadHeaderTimeout: 60 * time.Second,
}
go func() {

View File

@@ -59,8 +59,12 @@ func fixDNSResolver() {
// Note: If there are other methods to obtain the default DNS servers, the default DNS servers should be used preferentially.
net.DefaultResolver = &net.Resolver{
PreferGo: true,
Dial: func(ctx context.Context, network, _ string) (net.Conn, error) {
return net.Dial(network, "8.8.8.8:53")
Dial: func(ctx context.Context, network, addr string) (net.Conn, error) {
if addr == "127.0.0.1:53" || addr == "[::1]:53" {
addr = "8.8.8.8:53"
}
var d net.Dialer
return d.DialContext(ctx, network, addr)
},
}
}

View File

@@ -14,7 +14,7 @@
package version
var version = "0.57.0"
var version = "0.60.0"
func Full() string {
return version

View File

@@ -15,7 +15,6 @@
package vhost
import (
"bytes"
"context"
"encoding/base64"
"errors"
@@ -64,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
@@ -78,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 + "." +
@@ -93,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,
@@ -116,10 +124,16 @@ func NewHTTPReverseProxy(option HTTPReverseProxyOptions, vhostRouter *Routers) *
return nil, nil
},
},
BufferPool: newWrapPool(),
ErrorLog: stdlog.New(newWrapLogger(), "", 0),
BufferPool: pool.NewBuffer(32 * 1024),
ErrorLog: stdlog.New(log.NewWriteLogger(log.WarnLevel, 2), "", 0),
ErrorHandler: func(rw http.ResponseWriter, req *http.Request, err error) {
log.Warnf("do http proxy request [host: %s] error: %v", req.Host, err)
log.Logf(log.WarnLevel, 1, "do http proxy request [host: %s] error: %v", req.Host, err)
if err != nil {
if e, ok := err.(net.Error); ok && e.Timeout() {
rw.WriteHeader(http.StatusGatewayTimeout)
return
}
}
rw.WriteHeader(http.StatusNotFound)
_, _ = rw.Write(getNotFoundPageContent())
},
@@ -152,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)
@@ -300,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)
}
@@ -322,20 +333,3 @@ func (rp *HTTPReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request)
rp.proxy.ServeHTTP(rw, newreq)
}
}
type wrapPool struct{}
func newWrapPool() *wrapPool { return &wrapPool{} }
func (p *wrapPool) Get() []byte { return pool.GetBuf(32 * 1024) }
func (p *wrapPool) Put(buf []byte) { pool.PutBuf(buf) }
type wrapLogger struct{}
func newWrapLogger() *wrapLogger { return &wrapLogger{} }
func (l *wrapLogger) Write(p []byte) (n int, err error) {
log.Warnf("%s", string(bytes.TrimRight(p, "\n")))
return len(p), nil
}

View File

@@ -12,7 +12,7 @@ import (
func TestGetHTTPSHostname(t *testing.T) {
require := require.New(t)
l, err := net.Listen("tcp", ":")
l, err := net.Listen("tcp", "127.0.0.1:")
require.NoError(err)
defer l.Close()

View File

@@ -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

View File

@@ -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

View File

@@ -32,8 +32,6 @@ import (
"github.com/fatedier/frp/pkg/util/version"
)
// TODO(fatedier): add an API to clean status of all offline proxies.
type GeneralResponse struct {
Code int
Msg string
@@ -146,7 +144,8 @@ type TCPOutConf struct {
type TCPMuxOutConf struct {
BaseOutConf
v1.DomainConfig
Multiplexer string `json:"multiplexer"`
Multiplexer string `json:"multiplexer"`
RouteByHTTPUser string `json:"routeByHTTPUser"`
}
type UDPOutConf struct {

View File

@@ -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,

View File

@@ -286,12 +286,13 @@ func NewService(cfg *v1.ServerConfig) (*Service, error) {
address := net.JoinHostPort(cfg.ProxyBindAddr, strconv.Itoa(cfg.VhostHTTPPort))
server := &http.Server{
Addr: address,
Handler: rp,
Addr: address,
Handler: rp,
ReadHeaderTimeout: 60 * time.Second,
}
var l net.Listener
if httpMuxOn {
l = svr.muxer.ListenHttp(1)
l = svr.muxer.ListenHTTP(1)
} else {
l, err = net.Listen("tcp", address)
if err != nil {
@@ -308,7 +309,7 @@ func NewService(cfg *v1.ServerConfig) (*Service, error) {
if cfg.VhostHTTPSPort > 0 {
var l net.Listener
if httpsMuxOn {
l = svr.muxer.ListenHttps(1)
l = svr.muxer.ListenHTTPS(1)
} else {
address := net.JoinHostPort(cfg.ProxyBindAddr, strconv.Itoa(cfg.VhostHTTPSPort))
l, err = net.Listen("tcp", address)

View File

@@ -260,7 +260,7 @@ func (f *Framework) SetEnvs(envs []string) {
func (f *Framework) WriteTempFile(name string, content string) string {
filePath := filepath.Join(f.TempDirectory, name)
err := os.WriteFile(filePath, []byte(content), 0o766)
err := os.WriteFile(filePath, []byte(content), 0o600)
ExpectNoError(err)
return filePath
}

View File

@@ -27,7 +27,7 @@ func (f *Framework) RunProcesses(serverTemplates []string, clientTemplates []str
currentServerProcesses := make([]*process.Process, 0, len(serverTemplates))
for i := range serverTemplates {
path := filepath.Join(f.TempDirectory, fmt.Sprintf("frp-e2e-server-%d", i))
err = os.WriteFile(path, []byte(outs[i]), 0o666)
err = os.WriteFile(path, []byte(outs[i]), 0o600)
ExpectNoError(err)
if TestContext.Debug {
@@ -48,7 +48,7 @@ func (f *Framework) RunProcesses(serverTemplates []string, clientTemplates []str
for i := range clientTemplates {
index := i + len(serverTemplates)
path := filepath.Join(f.TempDirectory, fmt.Sprintf("frp-e2e-client-%d", i))
err = os.WriteFile(path, []byte(outs[index]), 0o666)
err = os.WriteFile(path, []byte(outs[index]), 0o600)
ExpectNoError(err)
if TestContext.Debug {
@@ -94,7 +94,7 @@ func (f *Framework) RunFrpc(args ...string) (*process.Process, string, error) {
func (f *Framework) GenerateConfigFile(content string) string {
f.configFileIndex++
path := filepath.Join(f.TempDirectory, fmt.Sprintf("frp-e2e-config-%d", f.configFileIndex))
err := os.WriteFile(path, []byte(content), 0o666)
err := os.WriteFile(path, []byte(content), 0o600)
ExpectNoError(err)
return path
}

View File

@@ -1,6 +1,7 @@
package basic
import (
"context"
"fmt"
"strconv"
"strings"
@@ -54,7 +55,7 @@ var _ = ginkgo.Describe("[Feature: ClientManage]", func() {
framework.NewRequestExpect(f).Port(p3Port).Ensure()
client := f.APIClientForFrpc(adminPort)
conf, err := client.GetConfig()
conf, err := client.GetConfig(context.Background())
framework.ExpectNoError(err)
newP2Port := f.AllocPort()
@@ -65,10 +66,10 @@ var _ = ginkgo.Describe("[Feature: ClientManage]", func() {
newClientConf = newClientConf[:p3Index]
}
err = client.UpdateConfig(newClientConf)
err = client.UpdateConfig(context.Background(), newClientConf)
framework.ExpectNoError(err)
err = client.Reload(true)
err = client.Reload(context.Background(), true)
framework.ExpectNoError(err)
time.Sleep(time.Second)
@@ -120,7 +121,7 @@ var _ = ginkgo.Describe("[Feature: ClientManage]", func() {
framework.NewRequestExpect(f).Port(testPort).Ensure()
client := f.APIClientForFrpc(adminPort)
err := client.Stop()
err := client.Stop(context.Background())
framework.ExpectNoError(err)
time.Sleep(3 * time.Second)

View File

@@ -1,6 +1,7 @@
package basic
import (
"context"
"fmt"
"net"
"strconv"
@@ -101,7 +102,7 @@ var _ = ginkgo.Describe("[Feature: Server Manager]", func() {
client := f.APIClientForFrpc(adminPort)
// tcp random port
status, err := client.GetProxyStatus("tcp")
status, err := client.GetProxyStatus(context.Background(), "tcp")
framework.ExpectNoError(err)
_, portStr, err := net.SplitHostPort(status.RemoteAddr)
@@ -112,7 +113,7 @@ var _ = ginkgo.Describe("[Feature: Server Manager]", func() {
framework.NewRequestExpect(f).Port(port).Ensure()
// udp random port
status, err = client.GetProxyStatus("udp")
status, err = client.GetProxyStatus(context.Background(), "udp")
framework.ExpectNoError(err)
_, portStr, err = net.SplitHostPort(status.RemoteAddr)

View File

@@ -12,7 +12,7 @@ import (
"strconv"
"time"
libdial "github.com/fatedier/golib/net/dial"
libnet "github.com/fatedier/golib/net"
httppkg "github.com/fatedier/frp/pkg/util/http"
"github.com/fatedier/frp/test/e2e/pkg/rpc"
@@ -160,11 +160,11 @@ func (r *Request) Do() (*Response, error) {
if r.protocol != "tcp" {
return nil, fmt.Errorf("only tcp protocol is allowed for proxy")
}
proxyType, proxyAddress, auth, err := libdial.ParseProxyURL(r.proxyURL)
proxyType, proxyAddress, auth, err := libnet.ParseProxyURL(r.proxyURL)
if err != nil {
return nil, fmt.Errorf("parse ProxyURL error: %v", err)
}
conn, err = libdial.Dial(addr, libdial.WithProxy(proxyType, proxyAddress), libdial.WithProxyAuth(auth))
conn, err = libnet.Dial(addr, libnet.WithProxy(proxyType, proxyAddress), libnet.WithProxyAuth(auth))
if err != nil {
return nil, err
}

View File

@@ -1,6 +1,7 @@
package basic
import (
"context"
"fmt"
"strconv"
"strings"
@@ -57,7 +58,7 @@ var _ = ginkgo.Describe("[Feature: ClientManage]", func() {
framework.NewRequestExpect(f).Port(p3Port).Ensure()
client := f.APIClientForFrpc(adminPort)
conf, err := client.GetConfig()
conf, err := client.GetConfig(context.Background())
framework.ExpectNoError(err)
newP2Port := f.AllocPort()
@@ -68,10 +69,10 @@ var _ = ginkgo.Describe("[Feature: ClientManage]", func() {
newClientConf = newClientConf[:p3Index]
}
err = client.UpdateConfig(newClientConf)
err = client.UpdateConfig(context.Background(), newClientConf)
framework.ExpectNoError(err)
err = client.Reload(true)
err = client.Reload(context.Background(), true)
framework.ExpectNoError(err)
time.Sleep(time.Second)
@@ -124,7 +125,7 @@ var _ = ginkgo.Describe("[Feature: ClientManage]", func() {
framework.NewRequestExpect(f).Port(testPort).Ensure()
client := f.APIClientForFrpc(adminPort)
err := client.Stop()
err := client.Stop(context.Background())
framework.ExpectNoError(err)
time.Sleep(3 * time.Second)

View File

@@ -1,6 +1,7 @@
package basic
import (
"context"
"fmt"
"github.com/onsi/ginkgo/v2"
@@ -72,7 +73,7 @@ var _ = ginkgo.Describe("[Feature: Config]", func() {
client := f.APIClientForFrpc(adminPort)
checkProxyFn := func(name string, localPort, remotePort int) {
status, err := client.GetProxyStatus(name)
status, err := client.GetProxyStatus(context.Background(), name)
framework.ExpectNoError(err)
framework.ExpectContainSubstring(status.LocalAddr, fmt.Sprintf(":%d", localPort))

View File

@@ -5,6 +5,7 @@ import (
"net/http"
"net/url"
"strconv"
"time"
"github.com/gorilla/websocket"
"github.com/onsi/ginkgo/v2"
@@ -266,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)
@@ -291,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")
@@ -300,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)
@@ -385,4 +419,48 @@ var _ = ginkgo.Describe("[Feature: HTTP]", func() {
framework.ExpectNoError(err)
framework.ExpectEqualValues(consts.TestString, string(msg))
})
ginkgo.It("vhostHTTPTimeout", func() {
vhostHTTPPort := f.AllocPort()
serverConf := getDefaultServerConf(vhostHTTPPort)
serverConf += `
vhostHTTPTimeout = 2
`
delayDuration := 0 * time.Second
localPort := f.AllocPort()
localServer := httpserver.New(
httpserver.WithBindPort(localPort),
httpserver.WithHandler(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
time.Sleep(delayDuration)
_, _ = w.Write([]byte(req.Host))
})),
)
f.RunServer("", localServer)
clientConf := consts.DefaultClientConfig
clientConf += fmt.Sprintf(`
[[proxies]]
name = "test"
type = "http"
localPort = %d
customDomains = ["normal.example.com"]
`, localPort)
f.RunProcesses([]string{serverConf}, []string{clientConf})
framework.NewRequestExpect(f).Port(vhostHTTPPort).
RequestModify(func(r *request.Request) {
r.HTTP().HTTPHost("normal.example.com").HTTP().Timeout(time.Second)
}).
ExpectResp([]byte("normal.example.com")).
Ensure()
delayDuration = 3 * time.Second
framework.NewRequestExpect(f).Port(vhostHTTPPort).
RequestModify(func(r *request.Request) {
r.HTTP().HTTPHost("normal.example.com").HTTP().Timeout(5 * time.Second)
}).
Ensure(framework.ExpectResponseCode(504))
})
})

View File

@@ -1,6 +1,7 @@
package basic
import (
"context"
"fmt"
"net"
"strconv"
@@ -112,7 +113,7 @@ var _ = ginkgo.Describe("[Feature: Server Manager]", func() {
client := f.APIClientForFrpc(adminPort)
// tcp random port
status, err := client.GetProxyStatus("tcp")
status, err := client.GetProxyStatus(context.Background(), "tcp")
framework.ExpectNoError(err)
_, portStr, err := net.SplitHostPort(status.RemoteAddr)
@@ -123,7 +124,7 @@ var _ = ginkgo.Describe("[Feature: Server Manager]", func() {
framework.NewRequestExpect(f).Port(port).Ensure()
// udp random port
status, err = client.GetProxyStatus("udp")
status, err = client.GetProxyStatus(context.Background(), "udp")
framework.ExpectNoError(err)
_, portStr, err = net.SplitHostPort(status.RemoteAddr)

View File

@@ -3,6 +3,7 @@ package plugin
import (
"crypto/tls"
"fmt"
"net/http"
"strconv"
"github.com/onsi/ginkgo/v2"
@@ -329,4 +330,122 @@ var _ = ginkgo.Describe("[Feature: Client-Plugins]", func() {
ExpectResp([]byte("test")).
Ensure()
})
ginkgo.Describe("http2http", func() {
ginkgo.It("host header rewrite", func() {
serverConf := consts.DefaultServerConfig
localPort := f.AllocPort()
remotePort := f.AllocPort()
clientConf := consts.DefaultClientConfig + fmt.Sprintf(`
[[proxies]]
name = "http2http"
type = "tcp"
remotePort = %d
[proxies.plugin]
type = "http2http"
localAddr = "127.0.0.1:%d"
hostHeaderRewrite = "rewrite.test.com"
`, remotePort, localPort)
f.RunProcesses([]string{serverConf}, []string{clientConf})
localServer := httpserver.New(
httpserver.WithBindPort(localPort),
httpserver.WithHandler(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
_, _ = w.Write([]byte(req.Host))
})),
)
f.RunServer("", localServer)
framework.NewRequestExpect(f).
Port(remotePort).
RequestModify(func(r *request.Request) {
r.HTTP().HTTPHost("example.com")
}).
ExpectResp([]byte("rewrite.test.com")).
Ensure()
})
ginkgo.It("set request header", func() {
serverConf := consts.DefaultServerConfig
localPort := f.AllocPort()
remotePort := f.AllocPort()
clientConf := consts.DefaultClientConfig + fmt.Sprintf(`
[[proxies]]
name = "http2http"
type = "tcp"
remotePort = %d
[proxies.plugin]
type = "http2http"
localAddr = "127.0.0.1:%d"
requestHeaders.set.x-from-where = "frp"
`, remotePort, localPort)
f.RunProcesses([]string{serverConf}, []string{clientConf})
localServer := httpserver.New(
httpserver.WithBindPort(localPort),
httpserver.WithHandler(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
_, _ = w.Write([]byte(req.Header.Get("x-from-where")))
})),
)
f.RunServer("", localServer)
framework.NewRequestExpect(f).
Port(remotePort).
RequestModify(func(r *request.Request) {
r.HTTP().HTTPHost("example.com")
}).
ExpectResp([]byte("frp")).
Ensure()
})
})
ginkgo.It("tls2raw", func() {
generator := &cert.SelfSignedCertGenerator{}
artifacts, err := generator.Generate("example.com")
framework.ExpectNoError(err)
crtPath := f.WriteTempFile("tls2raw_server.crt", string(artifacts.Cert))
keyPath := f.WriteTempFile("tls2raw_server.key", string(artifacts.Key))
serverConf := consts.DefaultServerConfig
vhostHTTPSPort := f.AllocPort()
serverConf += fmt.Sprintf(`
vhostHTTPSPort = %d
`, vhostHTTPSPort)
localPort := f.AllocPort()
clientConf := consts.DefaultClientConfig + fmt.Sprintf(`
[[proxies]]
name = "tls2raw-test"
type = "https"
customDomains = ["example.com"]
[proxies.plugin]
type = "tls2raw"
localAddr = "127.0.0.1:%d"
crtPath = "%s"
keyPath = "%s"
`, localPort, crtPath, keyPath)
f.RunProcesses([]string{serverConf}, []string{clientConf})
localServer := httpserver.New(
httpserver.WithBindPort(localPort),
httpserver.WithResponse([]byte("test")),
)
f.RunServer("", localServer)
framework.NewRequestExpect(f).
Port(vhostHTTPSPort).
RequestModify(func(r *request.Request) {
r.HTTPS().HTTPHost("example.com").TLSConfig(&tls.Config{
ServerName: "example.com",
InsecureSkipVerify: true,
})
}).
ExpectResp([]byte("test")).
Ensure()
})
})

View File

@@ -31,6 +31,7 @@ declare module 'vue' {
ProxiesSTCP: typeof import('./src/components/ProxiesSTCP.vue')['default']
ProxiesSUDP: typeof import('./src/components/ProxiesSUDP.vue')['default']
ProxiesTCP: typeof import('./src/components/ProxiesTCP.vue')['default']
ProxiesTCPMux: typeof import('./src/components/ProxiesTCPMux.vue')['default']
ProxiesUDP: typeof import('./src/components/ProxiesUDP.vue')['default']
ProxyView: typeof import('./src/components/ProxyView.vue')['default']
ProxyViewExpand: typeof import('./src/components/ProxyViewExpand.vue')['default']

View File

@@ -39,6 +39,7 @@
<el-menu-item index="/proxies/udp">UDP</el-menu-item>
<el-menu-item index="/proxies/http">HTTP</el-menu-item>
<el-menu-item index="/proxies/https">HTTPS</el-menu-item>
<el-menu-item index="/proxies/tcpmux">TCPMUX</el-menu-item>
<el-menu-item index="/proxies/stcp">STCP</el-menu-item>
<el-menu-item index="/proxies/sudp">SUDP</el-menu-item>
</el-sub-menu>

View File

@@ -0,0 +1,38 @@
<template>
<ProxyView :proxies="proxies" proxyType="tcpmux" @refresh="fetchData" />
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { TCPMuxProxy } from '../utils/proxy.js'
import ProxyView from './ProxyView.vue'
let proxies = ref<TCPMuxProxy[]>([])
const fetchData = () => {
let tcpmuxHTTPConnectPort: number
let subdomainHost: string
fetch('../api/serverinfo', { credentials: 'include' })
.then((res) => {
return res.json()
})
.then((json) => {
tcpmuxHTTPConnectPort = json.tcpmuxHTTPConnectPort
subdomainHost = json.subdomainHost
fetch('../api/proxy/tcpmux', { credentials: 'include' })
.then((res) => {
return res.json()
})
.then((json) => {
proxies.value = []
for (let proxyStats of json.proxies) {
proxies.value.push(new TCPMuxProxy(proxyStats, tcpmuxHTTPConnectPort, subdomainHost))
}
})
})
}
fetchData()
</script>
<style></style>

View File

@@ -1,9 +1,9 @@
<template>
<el-form
label-position="left"
label-width="auto"
inline
class="proxy-table-expand"
v-if="proxyType === 'http' || proxyType === 'https'"
>
<el-form-item label="Name">
<span>{{ row.name }}</span>
@@ -11,18 +11,6 @@
<el-form-item label="Type">
<span>{{ row.type }}</span>
</el-form-item>
<el-form-item label="Domains">
<span>{{ row.customDomains }}</span>
</el-form-item>
<el-form-item label="SubDomain">
<span>{{ row.subdomain }}</span>
</el-form-item>
<el-form-item label="locations">
<span>{{ row.locations }}</span>
</el-form-item>
<el-form-item label="HostRewrite">
<span>{{ row.hostHeaderRewrite }}</span>
</el-form-item>
<el-form-item label="Encryption">
<span>{{ row.encryption }}</span>
</el-form-item>
@@ -35,30 +23,40 @@
<el-form-item label="Last Close">
<span>{{ row.lastCloseTime }}</span>
</el-form-item>
</el-form>
<el-form label-position="left" inline class="proxy-table-expand" v-else>
<el-form-item label="Name">
<span>{{ row.name }}</span>
</el-form-item>
<el-form-item label="Type">
<span>{{ row.type }}</span>
</el-form-item>
<el-form-item label="Addr">
<span>{{ row.addr }}</span>
</el-form-item>
<el-form-item label="Encryption">
<span>{{ row.encryption }}</span>
</el-form-item>
<el-form-item label="Compression">
<span>{{ row.compression }}</span>
</el-form-item>
<el-form-item label="Last Start">
<span>{{ row.lastStartTime }}</span>
</el-form-item>
<el-form-item label="Last Close">
<span>{{ row.lastCloseTime }}</span>
</el-form-item>
<div v-if="proxyType === 'http' || proxyType === 'https'">
<el-form-item label="Domains">
<span>{{ row.customDomains }}</span>
</el-form-item>
<el-form-item label="SubDomain">
<span>{{ row.subdomain }}</span>
</el-form-item>
<el-form-item label="locations">
<span>{{ row.locations }}</span>
</el-form-item>
<el-form-item label="HostRewrite">
<span>{{ row.hostHeaderRewrite }}</span>
</el-form-item>
</div>
<div v-else-if="proxyType === 'tcpmux'">
<el-form-item label="Multiplexer">
<span>{{ row.multiplexer }}</span>
</el-form-item>
<el-form-item label="RouteByHTTPUser">
<span>{{ row.routeByHTTPUser }}</span>
</el-form-item>
<el-form-item label="Domains">
<span>{{ row.customDomains }}</span>
</el-form-item>
<el-form-item label="SubDomain">
<span>{{ row.subdomain }}</span>
</el-form-item>
</div>
<div v-else>
<el-form-item label="Addr">
<span>{{ row.addr }}</span>
</el-form-item>
</div>
</el-form>
<div v-if="row.annotations && row.annotations.size > 0">

View File

@@ -20,10 +20,10 @@
<el-form-item label="QUIC Bind Port" v-if="data.quicBindPort != 0">
<span>{{ data.quicBindPort }}</span>
</el-form-item>
<el-form-item label="Http Port" v-if="data.vhostHTTPPort != 0">
<el-form-item label="HTTP Port" v-if="data.vhostHTTPPort != 0">
<span>{{ data.vhostHTTPPort }}</span>
</el-form-item>
<el-form-item label="Https Port" v-if="data.vhostHTTPSPort != 0">
<el-form-item label="HTTPS Port" v-if="data.vhostHTTPSPort != 0">
<span>{{ data.vhostHTTPSPort }}</span>
</el-form-item>
<el-form-item

View File

@@ -4,6 +4,7 @@ import ProxiesTCP from '../components/ProxiesTCP.vue'
import ProxiesUDP from '../components/ProxiesUDP.vue'
import ProxiesHTTP from '../components/ProxiesHTTP.vue'
import ProxiesHTTPS from '../components/ProxiesHTTPS.vue'
import ProxiesTCPMux from '../components/ProxiesTCPMux.vue'
import ProxiesSTCP from '../components/ProxiesSTCP.vue'
import ProxiesSUDP from '../components/ProxiesSUDP.vue'
@@ -35,6 +36,11 @@ const router = createRouter({
name: 'ProxiesHTTPS',
component: ProxiesHTTPS,
},
{
path: '/proxies/tcpmux',
name: 'ProxiesTCPMux',
component: ProxiesTCPMux,
},
{
path: '/proxies/stcp',
name: 'ProxiesSTCP',

View File

@@ -110,6 +110,28 @@ class HTTPSProxy extends BaseProxy {
}
}
class TCPMuxProxy extends BaseProxy {
multiplexer: string
routeByHTTPUser: string
constructor(proxyStats: any, port: number, subdomainHost: string) {
super(proxyStats)
this.type = 'tcpmux'
this.port = port
this.multiplexer = ''
this.routeByHTTPUser = ''
if (proxyStats.conf) {
this.customDomains = proxyStats.conf.customDomains || this.customDomains
this.multiplexer = proxyStats.conf.multiplexer
this.routeByHTTPUser = proxyStats.conf.routeByHTTPUser
if (proxyStats.conf.subdomain) {
this.subdomain = `${proxyStats.conf.subdomain}.${subdomainHost}`
}
}
}
}
class STCPProxy extends BaseProxy {
constructor(proxyStats: any) {
super(proxyStats)
@@ -128,6 +150,7 @@ export {
BaseProxy,
TCPProxy,
UDPProxy,
TCPMuxProxy,
HTTPProxy,
HTTPSProxy,
STCPProxy,