Compare commits

...

29 Commits

Author SHA1 Message Date
fatedier
fe5fb0326b Merge pull request #2955 from fatedier/dev
bump version to v0.43.0
2022-05-27 16:27:19 +08:00
fatedier
0711295b0a release note for v0.43.0 (#2954) 2022-05-27 16:02:36 +08:00
fatedier
4af85da0c2 type http/tcpmux proxy support route_by_http_user, tcpmux support passthourgh mode (#2932) 2022-05-26 23:57:30 +08:00
fatedier
bd89eaba2f remove systemd files 2022-04-29 21:31:48 +08:00
fatedier
a72259c604 docker build&push: some adjustments 2022-04-29 01:15:42 +08:00
蓝云Reyes
44eb513f05 Update docker image build file (#2892)
* update docker image building
2022-04-29 01:12:07 +08:00
fatedier
eb1e19a821 Merge pull request #2906 from fatedier/dev
bump version
2022-04-22 11:32:27 +08:00
fatedier
6c658586f6 bump version to v0.42.0 2022-04-22 11:15:23 +08:00
fatedier
888ed25314 dependency: update github.com/pires/go-proxyproto to v0.6.2 (#2894) 2022-04-15 11:36:00 +08:00
fatedier
21240ed962 some improvements 2022-04-14 11:24:36 +08:00
Colin Adler
6481870d03 fix: data races when accessing github.com/fatedier/frp/client.(*Service).ctl (#2891)
* fix: data race in client/service.go

* review fixes
2022-04-14 11:14:19 +08:00
fatedier
a7a4ba270d fix error parsing env values (#2886) 2022-04-05 12:48:57 +08:00
cui fliter
915d9f4c09 fix some typos (#2882)
Signed-off-by: cuishuang <imcusg@gmail.com>
2022-04-02 17:35:51 +08:00
fatedier
18a2af4703 frpc: support multiple confs (#2873) 2022-03-28 12:12:35 +08:00
fatedier
305e40fa8a update .goreleaser.yml 2022-03-23 21:47:43 +08:00
fatedier
10f2620131 Merge pull request #2869 from fatedier/dev
bump version to v0.41.0
2022-03-23 21:19:59 +08:00
fatedier
4acae540c8 support go1.18 and remove go1.16 (#2868) 2022-03-23 21:15:01 +08:00
fatedier
11b13533a0 add release note (#2867) 2022-03-23 20:14:55 +08:00
fatedier
100d556336 support tcp keepalive params (#2863) 2022-03-22 19:29:30 +08:00
Blizard
452fe25cc6 feat: SUDP alway reconnect and print too much log when no data ready (#2844)
* feat: random sleep duration before reconnecting

* fix: bug
2022-03-17 12:03:20 +08:00
fatedier
63efa6b776 support pprof (#2849) 2022-03-17 11:42:59 +08:00
fatedier
37c27169ac workflows: update stale action (#2846) 2022-03-15 11:53:14 +08:00
fatedier
ce677820c6 Merge pull request #2834 from fatedier/dev
bump version
2022-03-11 19:51:32 +08:00
fatedier
1f88a7a0b8 bump version to v0.40.0 (#2833) 2022-03-11 19:45:34 +08:00
Johan Hernefeldt
eeea7602d9 bugfix: Issue #2831 - Cant connect to frps behind ingress with tls (#2832)
Co-authored-by: Johan Hernefeldt <johan.hernefeldt@moralis.io>
2022-03-11 14:51:47 +08:00
Harry Cheng
bf635c0e90 Notify server plugins when a proxy is closed (#2823)
* add close proxy op

* Move to actual closing routine

* Fix e2e tests for CloseProxy

* Add warning on resource exhaustion

* Add CloseProxy to manual close

* retuen errors to `CloseProxy` callers
2022-03-08 15:08:09 +08:00
Blizard
cd31359a27 feat: support add additional params for OIDC (#2814)
* feat: support add additional params and test access by auth0

* fix: config name

Co-authored-by: blizard863 <760076784@qq.com>
2022-03-07 14:23:49 +08:00
fatedier
19739ed31a random sleep duration before reconnecting (#2816) 2022-02-24 11:59:36 +08:00
fatedier
10100c28d9 client: add dial_server_timeout (#2805) 2022-02-19 16:49:21 +08:00
58 changed files with 1066 additions and 523 deletions

View File

@@ -2,14 +2,14 @@ version: 2
jobs:
go-version-latest:
docker:
- image: cimg/go:1.17-node
- image: cimg/go:1.18-node
steps:
- checkout
- run: make
- run: make alltest
go-version-last:
docker:
- image: cimg/go:1.16-node
- image: cimg/go:1.17-node
steps:
- checkout
- run: make

View File

@@ -10,43 +10,9 @@ on:
required: true
default: 'test'
jobs:
binary:
name: Build Golang project
runs-on: ubuntu-latest
steps:
- name: Set up Go 1.x
uses: actions/setup-go@v2
with:
go-version: 1.17
- run: |
# https://github.com/actions/setup-go/issues/107
cp -f `which go` /usr/bin/go
- run: go version
- name: Check out code into the Go module directory
uses: actions/checkout@v2
- name: Build
run: make build
- name: Archive artifacts for frpc
uses: actions/upload-artifact@v1
with:
name: frpc
path: bin/frpc
- name: Archive artifacts for frps
uses: actions/upload-artifact@v1
with:
name: frps
path: bin/frps
image:
name: Build Image from Dockerfile and binaries
runs-on: ubuntu-latest
needs: binary
steps:
# environment
- name: Checkout
@@ -60,19 +26,6 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
# download binaries of frpc and frps
- name: Download binary of frpc
uses: actions/download-artifact@v2
with:
name: frpc
path: bin/frpc
- name: Download binary of frps
uses: actions/download-artifact@v2
with:
name: frps
path: bin/frps
# get image tag name
- name: Get Image Tag Name
run: |
@@ -81,6 +34,18 @@ jobs:
else
echo "TAG_NAME=${{ github.event.inputs.tag }}" >> $GITHUB_ENV
fi
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
- name: Login to the GPR
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GPR_TOKEN }}
# prepare image tags
- name: Prepare Image Tags
@@ -92,26 +57,24 @@ jobs:
echo "TAG_FRPC_GPR=ghcr.io/fatedier/frpc:${{ env.TAG_NAME }}" >> $GITHUB_ENV
echo "TAG_FRPS_GPR=ghcr.io/fatedier/frps:${{ env.TAG_NAME }}" >> $GITHUB_ENV
# build images
- name: Build Images
run: |
# for Docker hub
docker build --file ${{ env.DOCKERFILE_FRPC_PATH }} --tag ${{ env.TAG_FRPC }} .
docker build --file ${{ env.DOCKERFILE_FRPS_PATH }} --tag ${{ env.TAG_FRPS }} .
# for GPR
docker build --file ${{ env.DOCKERFILE_FRPC_PATH }} --tag ${{ env.TAG_FRPC_GPR }} .
docker build --file ${{ env.DOCKERFILE_FRPS_PATH }} --tag ${{ env.TAG_FRPS_GPR }} .
- name: Build and push frpc
uses: docker/build-push-action@v2
with:
context: .
file: ./dockerfiles/Dockerfile-for-frpc
platforms: linux/amd64,linux/arm/v7,linux/arm64,linux/ppc64le,linux/s390x
push: true
tags: |
${{ env.TAG_FRPC }}
${{ env.TAG_FRPC_GPR }}
# push to dockerhub
- name: Publish to Dockerhub
run: |
echo ${{ secrets.DOCKERHUB_PASSWORD }} | docker login --username ${{ secrets.DOCKERHUB_USERNAME }} --password-stdin
docker push ${{ env.TAG_FRPC }}
docker push ${{ env.TAG_FRPS }}
# push to gpr
- name: Publish to GPR
run: |
echo ${{ secrets.GPR_TOKEN }} | docker login ghcr.io --username ${{ github.repository_owner }} --password-stdin
docker push ${{ env.TAG_FRPC_GPR }}
docker push ${{ env.TAG_FRPS_GPR }}
- name: Build and push frps
uses: docker/build-push-action@v2
with:
context: .
file: ./dockerfiles/Dockerfile-for-frps
platforms: linux/amd64,linux/arm/v7,linux/arm64,linux/ppc64le,linux/s390x
push: true
tags: |
${{ env.TAG_FRPS }}
${{ env.TAG_FRPS_GPR }}

View File

@@ -15,7 +15,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.17
go-version: 1.18
- run: |
# https://github.com/actions/setup-go/issues/107

View File

@@ -12,11 +12,11 @@ jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v3
- uses: actions/stale@v5
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
stale-issue-message: 'Issues go stale after 30d of inactivity. Stale issues rot after an additional 7d of inactivity and eventually close.'
stale-pr-message: 'Issues go stale after 30d of inactivity. Stale issues rot after an additional 7d of inactivity and eventually close.'
stale-pr-message: "PRs go stale after 30d of inactivity. Stale PRs rot after an additional 7d of inactivity and eventually close."
stale-issue-label: 'lifecycle/stale'
exempt-issue-labels: 'bug,doc,enhancement,future,proposal,question,testing,todo,easy,help wanted,assigned'
stale-pr-label: 'lifecycle/stale'
@@ -24,3 +24,5 @@ jobs:
days-before-stale: 30
days-before-close: 7
debug-only: ${{ github.event.inputs.debug-only }}
exempt-all-pr-milestones: true
exempt-all-pr-assignees: true

View File

@@ -1,7 +1,10 @@
builds:
- skip: true
checksum:
name_template: 'checksums.txt'
name_template: '{{ .ProjectName }}_sha256_checksums.txt'
algorithm: sha256
extra_files:
- glob: ./release/packages/*
release:
# Same as for github
# Note: it can only be one: either github, gitlab or gitea

View File

@@ -16,6 +16,9 @@ file:
fmt:
go fmt ./...
vet:
go vet ./...
frps:
env CGO_ENABLED=0 go build -trimpath -ldflags "$(LDFLAGS)" -o bin/frps ./cmd/frps
@@ -37,7 +40,7 @@ e2e:
e2e-trace:
DEBUG=true LOG_LEVEL=trace ./hack/run-e2e.sh
alltest: gotest e2e
alltest: vet gotest e2e
clean:
rm -f ./bin/frpc

View File

@@ -1,3 +1,5 @@
### Fix
### New
* Fixed IPv6 address parse issue.
* Added `route_by_http_user` in `http` and `tcpmux` proxy to support route to different clients by HTTP basic auth user.
* `CONNECT` method can be forwarded in `http` type proxy.
* Added `tcpmux_passthrough` in `tcpmux` proxy. If true, `CONNECT` request will be forwarded to frpc.

View File

@@ -17,6 +17,7 @@ package client
import (
"net"
"net/http"
"net/http/pprof"
"time"
"github.com/fatedier/frp/assets"
@@ -26,8 +27,8 @@ import (
)
var (
httpServerReadTimeout = 10 * time.Second
httpServerWriteTimeout = 10 * time.Second
httpServerReadTimeout = 60 * time.Second
httpServerWriteTimeout = 60 * time.Second
)
func (svr *Service) RunAdminServer(address string) (err error) {
@@ -36,6 +37,15 @@ func (svr *Service) RunAdminServer(address string) (err error) {
router.HandleFunc("/healthz", svr.healthz)
// debug
if svr.cfg.PprofEnable {
router.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
router.HandleFunc("/debug/pprof/profile", pprof.Profile)
router.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
router.HandleFunc("/debug/pprof/trace", pprof.Trace)
router.PathPrefix("/debug/pprof/").HandlerFunc(pprof.Index)
}
subRouter := router.NewRoute().Subrouter()
user, passwd := svr.cfg.AdminUser, svr.cfg.AdminPwd
subRouter.Use(frpNet.NewHTTPAuthMiddleware(user, passwd).Middleware)

View File

@@ -251,6 +251,8 @@ func (ctl *Control) connectServer() (conn net.Conn, err error) {
}
dialOptions = append(dialOptions,
libdial.WithProtocol(protocol),
libdial.WithTimeout(time.Duration(ctl.clientCfg.DialServerTimeout)*time.Second),
libdial.WithKeepAlive(time.Duration(ctl.clientCfg.DialServerKeepAlive)*time.Second),
libdial.WithProxy(proxyType, addr),
libdial.WithProxyAuth(auth),
libdial.WithTLSConfig(tlsConfig),

View File

@@ -366,7 +366,7 @@ func (pxy *XTCPProxy) InWorkConn(conn net.Conn, m *msg.StartWorkConn) {
// Listen for clientConn's address and wait for visitor connection
lConn, err := net.ListenUDP("udp", laddr)
if err != nil {
xl.Error("listen on visitorConn's local adress error: %v", err)
xl.Error("listen on visitorConn's local address error: %v", err)
return
}
defer lConn.Close()
@@ -787,7 +787,10 @@ func HandleTCPWorkConnection(ctx context.Context, localInfo *config.LocalSvrConf
return
}
localConn, err := libdial.Dial(net.JoinHostPort(localInfo.LocalIP, strconv.Itoa(localInfo.LocalPort)))
localConn, err := libdial.Dial(
net.JoinHostPort(localInfo.LocalIP, strconv.Itoa(localInfo.LocalPort)),
libdial.WithTimeout(10*time.Second),
)
if err != nil {
workConn.Close()
xl.Error("connect to local service [%s:%d] error: %v", localInfo.LocalIP, localInfo.LocalPort, err)

View File

@@ -17,12 +17,13 @@ package client
import (
"context"
"crypto/tls"
"errors"
"fmt"
"io"
"math/rand"
"net"
"runtime"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
@@ -34,13 +35,20 @@ import (
"github.com/fatedier/frp/pkg/transport"
"github.com/fatedier/frp/pkg/util/log"
frpNet "github.com/fatedier/frp/pkg/util/net"
"github.com/fatedier/frp/pkg/util/util"
"github.com/fatedier/frp/pkg/util/version"
"github.com/fatedier/frp/pkg/util/xlog"
"github.com/fatedier/golib/crypto"
libdial "github.com/fatedier/golib/net/dial"
fmux "github.com/hashicorp/yamux"
)
func init() {
crypto.DefaultSalt = "frp"
rand.Seed(time.Now().UnixNano())
}
// Service is a client service.
type Service struct {
// uniq id got from frps, attach it in loginMsg
@@ -98,6 +106,21 @@ func (svr *Service) GetController() *Control {
func (svr *Service) Run() error {
xl := xlog.FromContextSafe(svr.ctx)
// set custom DNSServer
if svr.cfg.DNSServer != "" {
dnsAddr := svr.cfg.DNSServer
if !strings.Contains(dnsAddr, ":") {
dnsAddr += ":53"
}
// Change default dns server for frpc
net.DefaultResolver = &net.Resolver{
PreferGo: true,
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
return net.Dial("udp", dnsAddr)
},
}
}
// login to frps
for {
conn, session, err := svr.login()
@@ -109,7 +132,7 @@ func (svr *Service) Run() error {
if svr.cfg.LoginFailExit {
return err
}
time.Sleep(10 * time.Second)
util.RandomSleep(10*time.Second, 0.9, 1.1)
} else {
// login success
ctl := NewControl(svr.ctx, svr.runID, conn, session, svr.cfg, svr.pxyCfgs, svr.visitorCfgs, svr.serverUDPPort, svr.authSetter)
@@ -158,8 +181,11 @@ func (svr *Service) keepControllerWorking() {
// the first three retry with no delay
if reconnectCounts > 3 {
time.Sleep(reconnectDelay)
util.RandomSleep(reconnectDelay, 0.9, 1.1)
xl.Info("wait %v to reconnect", reconnectDelay)
reconnectDelay *= 2
} else {
util.RandomSleep(time.Second, 0, 0.5)
}
reconnectCounts++
@@ -175,18 +201,12 @@ func (svr *Service) keepControllerWorking() {
xl.Info("try to reconnect to server...")
conn, session, err := svr.login()
if err != nil {
xl.Warn("reconnect to server error: %v", err)
time.Sleep(delayTime)
xl.Warn("reconnect to server error: %v, wait %v for another retry", err, delayTime)
util.RandomSleep(delayTime, 0.9, 1.1)
opErr := &net.OpError{}
// quick retry for dial error
if errors.As(err, &opErr) && opErr.Op == "dial" {
delayTime = 2 * time.Second
} else {
delayTime = delayTime * 2
if delayTime > maxDelayTime {
delayTime = maxDelayTime
}
delayTime = delayTime * 2
if delayTime > maxDelayTime {
delayTime = maxDelayTime
}
continue
}
@@ -245,6 +265,8 @@ func (svr *Service) login() (conn net.Conn, session *fmux.Session, err error) {
}
dialOptions = append(dialOptions,
libdial.WithProtocol(protocol),
libdial.WithTimeout(time.Duration(svr.cfg.DialServerTimeout)*time.Second),
libdial.WithKeepAlive(time.Duration(svr.cfg.DialServerKeepAlive)*time.Second),
libdial.WithProxy(proxyType, addr),
libdial.WithProxyAuth(auth),
libdial.WithTLSConfig(tlsConfig),
@@ -334,7 +356,14 @@ func (svr *Service) ReloadConf(pxyCfgs map[string]config.ProxyConf, visitorCfgs
svr.visitorCfgs = visitorCfgs
svr.cfgMu.Unlock()
return svr.ctl.ReloadConf(pxyCfgs, visitorCfgs)
svr.ctlMu.RLock()
ctl := svr.ctl
svr.ctlMu.RUnlock()
if ctl != nil {
return svr.ctl.ReloadConf(pxyCfgs, visitorCfgs)
}
return nil
}
func (svr *Service) Close() {
@@ -343,8 +372,12 @@ func (svr *Service) Close() {
func (svr *Service) GracefulClose(d time.Duration) {
atomic.StoreUint32(&svr.exit, 1)
svr.ctlMu.RLock()
if svr.ctl != nil {
svr.ctl.GracefulClose(d)
}
svr.ctlMu.RUnlock()
svr.cancel()
}

View File

@@ -377,29 +377,33 @@ func (sv *SUDPVisitor) Run() (err error) {
func (sv *SUDPVisitor) dispatcher() {
xl := xlog.FromContextSafe(sv.ctx)
var (
visitorConn net.Conn
err error
firstPacket *msg.UDPPacket
)
for {
// loop for get frpc to frps tcp conn
// setup worker
// wait worker to finished
// retry or exit
visitorConn, err := sv.getNewVisitorConn()
if err != nil {
// check if proxy is closed
// if checkCloseCh is close, we will return, other case we will continue to reconnect
select {
case <-sv.checkCloseCh:
select {
case firstPacket = <-sv.sendCh:
if firstPacket == nil {
xl.Info("frpc sudp visitor proxy is closed")
return
default:
}
case <-sv.checkCloseCh:
xl.Info("frpc sudp visitor proxy is closed")
return
}
time.Sleep(3 * time.Second)
visitorConn, err = sv.getNewVisitorConn()
if err != nil {
xl.Warn("newVisitorConn to frps error: %v, try to reconnect", err)
continue
}
sv.worker(visitorConn)
// visitorConn always be closed when worker done.
sv.worker(visitorConn, firstPacket)
select {
case <-sv.checkCloseCh:
@@ -407,9 +411,10 @@ func (sv *SUDPVisitor) dispatcher() {
default:
}
}
}
func (sv *SUDPVisitor) worker(workConn net.Conn) {
func (sv *SUDPVisitor) worker(workConn net.Conn, firstPacket *msg.UDPPacket) {
xl := xlog.FromContextSafe(sv.ctx)
xl.Debug("starting sudp proxy worker")
@@ -463,6 +468,14 @@ func (sv *SUDPVisitor) worker(workConn net.Conn) {
}()
var errRet error
if firstPacket != nil {
if errRet = msg.WriteMsg(conn, firstPacket); errRet != nil {
xl.Warn("sender goroutine for udp work connection closed: %v", errRet)
return
}
xl.Trace("send udp package to workConn: %s", firstPacket.Content)
}
for {
select {
case udpMsg, ok := <-sv.sendCh:

View File

@@ -15,18 +15,10 @@
package main
import (
"math/rand"
"time"
_ "github.com/fatedier/frp/assets/frpc"
"github.com/fatedier/frp/cmd/frpc/sub"
"github.com/fatedier/golib/crypto"
)
func main() {
crypto.DefaultSalt = "frp"
rand.Seed(time.Now().UnixNano())
sub.Execute()
}

View File

@@ -15,13 +15,14 @@
package sub
import (
"context"
"fmt"
"io/fs"
"net"
"os"
"os/signal"
"path/filepath"
"strconv"
"strings"
"sync"
"syscall"
"time"
@@ -41,6 +42,7 @@ const (
var (
cfgFile string
cfgDir string
showVersion bool
serverAddr string
@@ -72,15 +74,12 @@ var (
bindPort int
tlsEnable bool
kcpDoneCh chan struct{}
)
func init() {
rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "./frpc.ini", "config file of frpc")
rootCmd.PersistentFlags().StringVarP(&cfgDir, "config_dir", "", "", "config directory, run one frpc service for each file in config directory")
rootCmd.PersistentFlags().BoolVarP(&showVersion, "version", "v", false, "version of frpc")
kcpDoneCh = make(chan struct{})
}
func RegisterCommonFlags(cmd *cobra.Command) {
@@ -104,6 +103,32 @@ var rootCmd = &cobra.Command{
return nil
}
// If cfgDir is not empty, run multiple frpc service for each config file in cfgDir.
// Note that it's only designed for testing. It's not guaranteed to be stable.
if cfgDir != "" {
var wg sync.WaitGroup
filepath.WalkDir(cfgDir, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return nil
}
if d.IsDir() {
return nil
}
wg.Add(1)
time.Sleep(time.Millisecond)
go func() {
defer wg.Done()
err := runClient(path)
if err != nil {
fmt.Printf("frpc service error for config file [%s]\n", path)
}
}()
return nil
})
wg.Wait()
return nil
}
// Do not show command usage here.
err := runClient(cfgFile)
if err != nil {
@@ -120,12 +145,12 @@ func Execute() {
}
}
func handleSignal(svr *client.Service) {
ch := make(chan os.Signal)
func handleSignal(svr *client.Service, doneCh chan struct{}) {
ch := make(chan os.Signal, 1)
signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
<-ch
svr.GracefulClose(500 * time.Millisecond)
close(kcpDoneCh)
close(doneCh)
}
func parseClientCommonCfgFromCmd() (cfg config.ClientCommonConf, err error) {
@@ -182,18 +207,9 @@ func startService(
log.InitLog(cfg.LogWay, cfg.LogFile, cfg.LogLevel,
cfg.LogMaxDays, cfg.DisableLogColor)
if cfg.DNSServer != "" {
s := cfg.DNSServer
if !strings.Contains(s, ":") {
s += ":53"
}
// Change default dns server for frpc
net.DefaultResolver = &net.Resolver{
PreferGo: true,
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
return net.Dial("udp", s)
},
}
if cfgFile != "" {
log.Trace("start frpc service for config file [%s]", cfgFile)
defer log.Trace("frpc service for config file [%s] stopped", cfgFile)
}
svr, errRet := client.NewService(cfg, pxyCfgs, visitorCfgs, cfgFile)
if errRet != nil {
@@ -201,9 +217,10 @@ func startService(
return
}
kcpDoneCh := make(chan struct{})
// Capture the exit signal if we use kcp.
if cfg.Protocol == "kcp" {
go handleSignal(svr)
go handleSignal(svr, kcpDoneCh)
}
err = svr.Run()

View File

@@ -6,6 +6,13 @@
server_addr = 0.0.0.0
server_port = 7000
# The maximum amount of time a dial to server will wait for a connect to complete. Default value is 10 seconds.
# dial_server_timeout = 10
# dial_server_keepalive specifies the interval between keep-alive probes for an active network connection between frpc and frps.
# If negative, keep-alive probes are disabled.
# dial_server_keepalive = 7200
# if you want to connect frps by http proxy or socks5 proxy or ntlm proxy, you can set http_proxy here or in global environment variables
# it only works when protocol is tcp
# http_proxy = http://user:passwd@192.168.1.128:8080
@@ -48,6 +55,12 @@ oidc_audience =
# It will be used to get an OIDC token if AuthenticationMethod == "oidc". By default, this value is "".
oidc_token_endpoint_url =
# oidc_additional_xxx specifies additional parameters to be sent to the OIDC Token Endpoint.
# For example, if you want to specify the "audience" parameter, you can set as follow.
# frp will add "audience=<value>" "var1=<value>" to the additional parameters.
# oidc_additional_audience = https://dev.auth.com/api/v2/
# oidc_additional_var1 = foobar
# set admin address for control frpc's action by http api such as reload
admin_addr = 127.0.0.1
admin_port = 7400
@@ -60,7 +73,8 @@ admin_pwd = admin
pool_count = 5
# if tcp stream multiplexing is used, default is true, it must be same with frps
tcp_mux = true
# tcp_mux = true
# specify keep alive interval for tcp mux.
# only valid if tcp_mux is true.
# tcp_mux_keepalive_interval = 60
@@ -91,7 +105,7 @@ tls_enable = true
# specify a dns server, so frpc will use this instead of default one
# dns_server = 8.8.8.8
# proxy names you want to start seperated by ','
# proxy names you want to start separated by ','
# default is empty, means all proxies
# start = ssh,dns
@@ -117,6 +131,10 @@ udp_packet_size = 1500
# If DisableCustomTLSFirstByte is true, frpc will not send that custom byte.
disable_custom_tls_first_byte = false
# Enable golang pprof handlers in admin listener.
# Admin port must be set first.
pprof_enable = false
# 'ssh' is the unique proxy name
# if user in [common] section is not empty, it will be changed to {user}.{proxy} such as 'your_name.ssh'
[ssh]
@@ -198,6 +216,8 @@ subdomain = web01
custom_domains = web01.yourdomain.com
# locations is only available for http type
locations = /,/pic
# route requests to this service if http basic auto user is abc
# route_by_http_user = abc
host_header_rewrite = example.com
# params with prefix "header_" will be used to update http request headers
header_X-From-Where = frp
@@ -330,3 +350,4 @@ multiplexer = httpconnect
local_ip = 127.0.0.1
local_port = 10701
custom_domains = tunnel1
# route_by_http_user = user1

View File

@@ -30,6 +30,9 @@ vhost_https_port = 443
# HTTP CONNECT requests. By default, this value is 0.
# tcpmux_httpconnect_port = 1337
# If tcpmux_passthrough is true, frps won't do any update on traffic.
# tcpmux_passthrough = false
# set dashboard_addr and dashboard_port to view dashboard of frps
# dashboard_addr's default value is same with bind_addr
# dashboard is available only if dashboard_port is set
@@ -86,7 +89,6 @@ oidc_audience =
# By default, this value is false.
oidc_skip_expiry_check = false
# oidc_skip_issuer_check specifies whether to skip checking if the OIDC token's issuer claim matches the issuer specified in OidcIssuer.
# By default, this value is false.
oidc_skip_issuer_check = false
@@ -120,11 +122,16 @@ tls_only = false
subdomain_host = frps.com
# if tcp stream multiplexing is used, default is true
tcp_mux = true
# tcp_mux = true
# specify keep alive interval for tcp mux.
# only valid if tcp_mux is true.
# tcp_mux_keepalive_interval = 60
# tcp_keepalive specifies the interval between keep-alive probes for an active network connection between frpc and frps.
# If negative, keep-alive probes are disabled.
# tcp_keepalive = 7200
# custom 404 page for HTTP requests
# custom_404_page = /path/to/404.html
@@ -133,6 +140,10 @@ tcp_mux = true
# It affects the udp and sudp proxy.
udp_packet_size = 1500
# Enable golang pprof handlers in dashboard listener.
# Dashboard port must be set first
pprof_enable = false
[plugin.user-manager]
addr = 127.0.0.1:9000
path = /handler

View File

@@ -1,15 +0,0 @@
[Unit]
Description=Frp Client Service
After=network.target
[Service]
Type=simple
User=nobody
Restart=on-failure
RestartSec=5s
ExecStart=/usr/bin/frpc -c /etc/frp/frpc.ini
ExecReload=/usr/bin/frpc reload -c /etc/frp/frpc.ini
LimitNOFILE=1048576
[Install]
WantedBy=multi-user.target

View File

@@ -1,15 +0,0 @@
[Unit]
Description=Frp Client Service
After=network.target
[Service]
Type=simple
User=nobody
Restart=on-failure
RestartSec=5s
ExecStart=/usr/bin/frpc -c /etc/frp/%i.ini
ExecReload=/usr/bin/frpc reload -c /etc/frp/%i.ini
LimitNOFILE=1048576
[Install]
WantedBy=multi-user.target

View File

@@ -1,14 +0,0 @@
[Unit]
Description=Frp Server Service
After=network.target
[Service]
Type=simple
User=nobody
Restart=on-failure
RestartSec=5s
ExecStart=/usr/bin/frps -c /etc/frp/frps.ini
LimitNOFILE=1048576
[Install]
WantedBy=multi-user.target

View File

@@ -1,14 +0,0 @@
[Unit]
Description=Frp Server Service
After=network.target
[Service]
Type=simple
User=nobody
Restart=on-failure
RestartSec=5s
ExecStart=/usr/bin/frps -c /etc/frp/%i.ini
LimitNOFILE=1048576
[Install]
WantedBy=multi-user.target

View File

@@ -70,7 +70,7 @@ The response can look like any of the following:
### Operation
Currently `Login`, `NewProxy`, `Ping`, `NewWorkConn` and `NewUserConn` operations are supported.
Currently `Login`, `NewProxy`, `CloseProxy`, `Ping`, `NewWorkConn` and `NewUserConn` operations are supported.
#### Login
@@ -136,6 +136,26 @@ Create new proxy
}
```
#### CloseProxy
A previously created proxy is closed.
Please note that one request will be sent for every proxy that is closed, do **NOT** use this
if you have too many proxies bound to a single client, as this may exhaust the server's resources.
```
{
"content": {
"user": {
"user": <string>,
"metas": map<string>string
"run_id": <string>
},
"proxy_name": <string>
}
}
```
#### Ping
Heartbeat from frpc

View File

@@ -1,14 +1,12 @@
FROM alpine:3 AS temp
FROM golang:1.18 AS building
COPY bin/frpc /tmp
RUN chmod -R 777 /tmp/frpc
COPY . /building
WORKDIR /building
RUN make frpc
FROM alpine:3
WORKDIR /app
COPY --from=temp /tmp/frpc /usr/bin
COPY --from=building /building/bin/frpc /usr/bin/frpc
ENTRYPOINT ["/usr/bin/frpc"]

View File

@@ -1,14 +1,12 @@
FROM alpine:3 AS temp
FROM golang:1.18 AS building
COPY bin/frps /tmp
RUN chmod -R 777 /tmp/frps
COPY . /building
WORKDIR /building
RUN make frps
FROM alpine:3
WORKDIR /app
COPY --from=temp /tmp/frps /usr/bin
COPY --from=building /building/bin/frps /usr/bin/frps
ENTRYPOINT ["/usr/bin/frps"]

4
go.mod
View File

@@ -6,7 +6,7 @@ require (
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
github.com/coreos/go-oidc v2.2.1+incompatible
github.com/fatedier/beego v0.0.0-20171024143340-6c6a4f5bd5eb
github.com/fatedier/golib v0.1.1-0.20220119075718-78e5cf8c00ee
github.com/fatedier/golib v0.1.1-0.20220321042308-c306138b83ac
github.com/fatedier/kcp-go v2.0.4-0.20190803094908-fe8645b0a904+incompatible
github.com/go-playground/validator/v10 v10.6.1
github.com/google/uuid v1.2.0
@@ -16,7 +16,7 @@ require (
github.com/leodido/go-urn v1.2.1 // indirect
github.com/onsi/ginkgo v1.16.4
github.com/onsi/gomega v1.13.0
github.com/pires/go-proxyproto v0.5.0
github.com/pires/go-proxyproto v0.6.2
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect
github.com/prometheus/client_golang v1.11.0
github.com/rodaine/table v1.0.1

8
go.sum
View File

@@ -88,8 +88,8 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7
github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/fatedier/beego v0.0.0-20171024143340-6c6a4f5bd5eb h1:wCrNShQidLmvVWn/0PikGmpdP0vtQmnvyRg3ZBEhczw=
github.com/fatedier/beego v0.0.0-20171024143340-6c6a4f5bd5eb/go.mod h1:wx3gB6dbIfBRcucp94PI9Bt3I0F2c/MyNEWuhzpWiwk=
github.com/fatedier/golib v0.1.1-0.20220119075718-78e5cf8c00ee h1:iS0wlj2uZPxh3pciAf/HTzi88Kqu7DPh1jNKgJaFhtI=
github.com/fatedier/golib v0.1.1-0.20220119075718-78e5cf8c00ee/go.mod h1:fLV0TLwHqrnB/L3jbNl67Gn6PCLggDGHniX1wLrA2Qo=
github.com/fatedier/golib v0.1.1-0.20220321042308-c306138b83ac h1:td1FJwN/oz8+9GldeEm3YdBX0Husc0FSPywLesZxi4w=
github.com/fatedier/golib v0.1.1-0.20220321042308-c306138b83ac/go.mod h1:fLV0TLwHqrnB/L3jbNl67Gn6PCLggDGHniX1wLrA2Qo=
github.com/fatedier/kcp-go v2.0.4-0.20190803094908-fe8645b0a904+incompatible h1:ssXat9YXFvigNge/IkkZvFMn8yeYKFX+uI6wn2mLJ74=
github.com/fatedier/kcp-go v2.0.4-0.20190803094908-fe8645b0a904+incompatible/go.mod h1:YpCOaxj7vvMThhIQ9AfTOPW2sfztQR5WDfs7AflSy4s=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
@@ -300,8 +300,8 @@ github.com/onsi/gomega v1.13.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je4
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
github.com/pires/go-proxyproto v0.5.0 h1:A4Jv4ZCaV3AFJeGh5mGwkz4iuWUYMlQ7IoO/GTuSuLo=
github.com/pires/go-proxyproto v0.5.0/go.mod h1:Odh9VFOZJCf9G8cLW5o435Xf1J95Jw9Gw5rnCjcwzAY=
github.com/pires/go-proxyproto v0.6.2 h1:KAZ7UteSOt6urjme6ZldyFm4wDe/z0ZUP0Yv0Dos0d8=
github.com/pires/go-proxyproto v0.6.2/go.mod h1:Odh9VFOZJCf9G8cLW5o435Xf1J95Jw9Gw5rnCjcwzAY=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=

View File

@@ -40,14 +40,20 @@ type OidcClientConfig struct {
// It will be used to get an OIDC token if AuthenticationMethod == "oidc".
// By default, this value is "".
OidcTokenEndpointURL string `ini:"oidc_token_endpoint_url" json:"oidc_token_endpoint_url"`
// OidcAdditionalEndpointParams specifies additional parameters to be sent
// this field will be transfer to map[string][]string in OIDC token generator
// The field will be set by prefix "oidc_additional_"
OidcAdditionalEndpointParams map[string]string `ini:"-" json:"oidc_additional_endpoint_params"`
}
func getDefaultOidcClientConf() OidcClientConfig {
return OidcClientConfig{
OidcClientID: "",
OidcClientSecret: "",
OidcAudience: "",
OidcTokenEndpointURL: "",
OidcClientID: "",
OidcClientSecret: "",
OidcAudience: "",
OidcTokenEndpointURL: "",
OidcAdditionalEndpointParams: make(map[string]string),
}
}
@@ -88,11 +94,17 @@ type OidcAuthProvider struct {
}
func NewOidcAuthSetter(baseCfg BaseConfig, cfg OidcClientConfig) *OidcAuthProvider {
eps := make(map[string][]string)
for k, v := range cfg.OidcAdditionalEndpointParams {
eps[k] = []string{v}
}
tokenGenerator := &clientcredentials.Config{
ClientID: cfg.OidcClientID,
ClientSecret: cfg.OidcClientSecret,
Scopes: []string{cfg.OidcAudience},
TokenURL: cfg.OidcTokenEndpointURL,
ClientID: cfg.OidcClientID,
ClientSecret: cfg.OidcClientSecret,
Scopes: []string{cfg.OidcAudience},
TokenURL: cfg.OidcTokenEndpointURL,
EndpointParams: eps,
}
return &OidcAuthProvider{

View File

@@ -38,6 +38,11 @@ type ClientCommonConf struct {
// ServerPort specifies the port to connect to the server on. By default,
// this value is 7000.
ServerPort int `ini:"server_port" json:"server_port"`
// The maximum amount of time a dial to server will wait for a connect to complete.
DialServerTimeout int64 `ini:"dial_server_timeout" json:"dial_server_timeout"`
// DialServerKeepAlive specifies the interval between keep-alive probes for an active network connection between frpc and frps.
// If negative, keep-alive probes are disabled.
DialServerKeepAlive int64 `ini:"dial_server_keepalive" json:"dial_server_keepalive"`
// ConnectServerLocalIP specifies the address of the client bind when it connect to server.
// By default, this value is empty.
// this value only use in TCP/Websocket protocol. Not support in KCP protocol.
@@ -128,7 +133,7 @@ type ClientCommonConf struct {
// It only works when "tls_enable" is valid and tls configuration of server
// has been specified.
TLSTrustedCaFile string `ini:"tls_trusted_ca_file" json:"tls_trusted_ca_file"`
// TLSServerName specifices the custom server name of tls certificate. By
// TLSServerName specifies the custom server name of tls certificate. By
// default, server name if same to ServerAddr.
TLSServerName string `ini:"tls_server_name" json:"tls_server_name"`
// By default, frpc will connect frps with first custom byte if tls is enabled.
@@ -149,6 +154,9 @@ type ClientCommonConf struct {
UDPPacketSize int64 `ini:"udp_packet_size" json:"udp_packet_size"`
// Include other config files for proxies.
IncludeConfigFiles []string `ini:"includes" json:"includes"`
// Enable golang pprof handlers in admin listener.
// Admin port must be set first.
PprofEnable bool `ini:"pprof_enable" json:"pprof_enable"`
}
// GetDefaultClientConf returns a client configuration with default values.
@@ -157,6 +165,8 @@ func GetDefaultClientConf() ClientCommonConf {
ClientConfig: auth.GetDefaultClientConf(),
ServerAddr: "0.0.0.0",
ServerPort: 7000,
DialServerTimeout: 10,
DialServerKeepAlive: 7200,
HTTPProxy: os.Getenv("http_proxy"),
LogFile: "console",
LogWay: "console",
@@ -185,6 +195,7 @@ func GetDefaultClientConf() ClientCommonConf {
Metas: make(map[string]string),
UDPPacketSize: 1500,
IncludeConfigFiles: make([]string, 0),
PprofEnable: false,
}
}
@@ -258,6 +269,8 @@ func UnmarshalClientConfFromIni(source interface{}) (ClientCommonConf, error) {
}
common.Metas = GetMapWithoutPrefix(s.KeysHash(), "meta_")
common.ClientConfig.OidcAdditionalEndpointParams = GetMapWithoutPrefix(s.KeysHash(), "oidc_additional_")
return common, nil
}

View File

@@ -261,6 +261,8 @@ func Test_LoadClientCommonConf(t *testing.T) {
},
ServerAddr: "0.0.0.9",
ServerPort: 7009,
DialServerTimeout: 10,
DialServerKeepAlive: 7200,
HTTPProxy: "http://user:passwd@192.168.1.128:8080",
LogFile: "./frpc.log9",
LogWay: "file",

View File

@@ -162,6 +162,7 @@ type HTTPProxyConf struct {
HTTPPwd string `ini:"http_pwd" json:"http_pwd"`
HostHeaderRewrite string `ini:"host_header_rewrite" json:"host_header_rewrite"`
Headers map[string]string `ini:"-" json:"headers"`
RouteByHTTPUser string `ini:"route_by_http_user" json:"route_by_http_user"`
}
// HTTPS
@@ -178,8 +179,9 @@ type TCPProxyConf struct {
// TCPMux
type TCPMuxProxyConf struct {
BaseProxyConf `ini:",extends"`
DomainConf `ini:",extends"`
BaseProxyConf `ini:",extends"`
DomainConf `ini:",extends"`
RouteByHTTPUser string `ini:"route_by_http_user" json:"route_by_http_user"`
Multiplexer string `ini:"multiplexer"`
}
@@ -576,7 +578,7 @@ func (cfg *TCPMuxProxyConf) Compare(cmp ProxyConf) bool {
return false
}
if cfg.Multiplexer != cmpConf.Multiplexer {
if cfg.Multiplexer != cmpConf.Multiplexer || cfg.RouteByHTTPUser != cmpConf.RouteByHTTPUser {
return false
}
@@ -601,6 +603,7 @@ func (cfg *TCPMuxProxyConf) UnmarshalFromMsg(pMsg *msg.NewProxy) {
cfg.CustomDomains = pMsg.CustomDomains
cfg.SubDomain = pMsg.SubDomain
cfg.Multiplexer = pMsg.Multiplexer
cfg.RouteByHTTPUser = pMsg.RouteByHTTPUser
}
func (cfg *TCPMuxProxyConf) MarshalToMsg(pMsg *msg.NewProxy) {
@@ -610,6 +613,7 @@ func (cfg *TCPMuxProxyConf) MarshalToMsg(pMsg *msg.NewProxy) {
pMsg.CustomDomains = cfg.CustomDomains
pMsg.SubDomain = cfg.SubDomain
pMsg.Multiplexer = cfg.Multiplexer
pMsg.RouteByHTTPUser = cfg.RouteByHTTPUser
}
func (cfg *TCPMuxProxyConf) CheckForCli() (err error) {
@@ -724,6 +728,7 @@ func (cfg *HTTPProxyConf) Compare(cmp ProxyConf) bool {
cfg.HTTPUser != cmpConf.HTTPUser ||
cfg.HTTPPwd != cmpConf.HTTPPwd ||
cfg.HostHeaderRewrite != cmpConf.HostHeaderRewrite ||
cfg.RouteByHTTPUser != cmpConf.RouteByHTTPUser ||
!reflect.DeepEqual(cfg.Headers, cmpConf.Headers) {
return false
}
@@ -754,6 +759,7 @@ func (cfg *HTTPProxyConf) UnmarshalFromMsg(pMsg *msg.NewProxy) {
cfg.HTTPUser = pMsg.HTTPUser
cfg.HTTPPwd = pMsg.HTTPPwd
cfg.Headers = pMsg.Headers
cfg.RouteByHTTPUser = pMsg.RouteByHTTPUser
}
func (cfg *HTTPProxyConf) MarshalToMsg(pMsg *msg.NewProxy) {
@@ -767,6 +773,7 @@ func (cfg *HTTPProxyConf) MarshalToMsg(pMsg *msg.NewProxy) {
pMsg.HTTPUser = cfg.HTTPUser
pMsg.HTTPPwd = cfg.HTTPPwd
pMsg.Headers = cfg.Headers
pMsg.RouteByHTTPUser = cfg.RouteByHTTPUser
}
func (cfg *HTTPProxyConf) CheckForCli() (err error) {

View File

@@ -62,6 +62,8 @@ type ServerCommonConf struct {
// requests on one single port. If it's not - it will listen on this value for
// HTTP CONNECT requests. By default, this value is 0.
TCPMuxHTTPConnectPort int `ini:"tcpmux_httpconnect_port" json:"tcpmux_httpconnect_port" validate:"gte=0,lte=65535"`
// If TCPMuxPassthrough is true, frps won't do any update on traffic.
TCPMuxPassthrough bool `ini:"tcpmux_passthrough" json:"tcpmux_passthrough"`
// VhostHTTPTimeout specifies the response header timeout for the Vhost
// HTTP server, in seconds. By default, this value is 60.
VhostHTTPTimeout int64 `ini:"vhost_http_timeout" json:"vhost_http_timeout"`
@@ -121,6 +123,9 @@ type ServerCommonConf struct {
// TCPMuxKeepaliveInterval specifies the keep alive interval for TCP stream multipler.
// If TCPMux is true, heartbeat of application layer is unnecessary because it can only rely on heartbeat in TCPMux.
TCPMuxKeepaliveInterval int64 `ini:"tcp_mux_keepalive_interval" json:"tcp_mux_keepalive_interval"`
// TCPKeepAlive specifies the interval between keep-alive probes for an active network connection between frpc and frps.
// If negative, keep-alive probes are disabled.
TCPKeepAlive int64 `ini:"tcp_keepalive" json:"tcp_keepalive"`
// Custom404Page specifies a path to a custom 404 page to display. If this
// value is "", a default page will be displayed. By default, this value is
// "".
@@ -167,6 +172,9 @@ type ServerCommonConf struct {
// UDPPacketSize specifies the UDP packet size
// By default, this value is 1500
UDPPacketSize int64 `ini:"udp_packet_size" json:"udp_packet_size"`
// Enable golang pprof handlers in dashboard listener.
// Dashboard port must be set first.
PprofEnable bool `ini:"pprof_enable" json:"pprof_enable"`
}
// GetDefaultServerConf returns a server configuration with reasonable
@@ -182,6 +190,7 @@ func GetDefaultServerConf() ServerCommonConf {
VhostHTTPPort: 0,
VhostHTTPSPort: 0,
TCPMuxHTTPConnectPort: 0,
TCPMuxPassthrough: false,
VhostHTTPTimeout: 60,
DashboardAddr: "0.0.0.0",
DashboardPort: 0,
@@ -198,6 +207,7 @@ func GetDefaultServerConf() ServerCommonConf {
SubDomainHost: "",
TCPMux: true,
TCPMuxKeepaliveInterval: 60,
TCPKeepAlive: 7200,
AllowPorts: make(map[int]struct{}),
MaxPoolCount: 5,
MaxPortsPerClient: 0,
@@ -210,6 +220,7 @@ func GetDefaultServerConf() ServerCommonConf {
Custom404Page: "",
HTTPPlugins: make(map[string]plugin.HTTPPluginOptions),
UDPPacketSize: 1500,
PprofEnable: false,
}
}

View File

@@ -140,6 +140,7 @@ func Test_LoadServerCommonConf(t *testing.T) {
SubDomainHost: "frps.com",
TCPMux: true,
TCPMuxKeepaliveInterval: 60,
TCPKeepAlive: 7200,
UDPPacketSize: 1509,
HTTPPlugins: map[string]plugin.HTTPPluginOptions{
@@ -191,6 +192,7 @@ func Test_LoadServerCommonConf(t *testing.T) {
DetailedErrorsToClient: true,
TCPMux: true,
TCPMuxKeepaliveInterval: 60,
TCPKeepAlive: 7200,
AllowPorts: make(map[int]struct{}),
MaxPoolCount: 5,
HeartbeatTimeout: 90,

View File

@@ -29,11 +29,11 @@ func init() {
glbEnvs = make(map[string]string)
envs := os.Environ()
for _, env := range envs {
kv := strings.Split(env, "=")
if len(kv) != 2 {
pair := strings.SplitN(env, "=", 2)
if len(pair) != 2 {
continue
}
glbEnvs[kv[0]] = kv[1]
glbEnvs[pair[0]] = pair[1]
}
}

View File

@@ -62,133 +62,134 @@ var (
// When frpc start, client send this message to login to server.
type Login struct {
Version string `json:"version"`
Hostname string `json:"hostname"`
Os string `json:"os"`
Arch string `json:"arch"`
User string `json:"user"`
PrivilegeKey string `json:"privilege_key"`
Timestamp int64 `json:"timestamp"`
RunID string `json:"run_id"`
Metas map[string]string `json:"metas"`
Version string `json:"version,omitempty"`
Hostname string `json:"hostname,omitempty"`
Os string `json:"os,omitempty"`
Arch string `json:"arch,omitempty"`
User string `json:"user,omitempty"`
PrivilegeKey string `json:"privilege_key,omitempty"`
Timestamp int64 `json:"timestamp,omitempty"`
RunID string `json:"run_id,omitempty"`
Metas map[string]string `json:"metas,omitempty"`
// Some global configures.
PoolCount int `json:"pool_count"`
PoolCount int `json:"pool_count,omitempty"`
}
type LoginResp struct {
Version string `json:"version"`
RunID string `json:"run_id"`
ServerUDPPort int `json:"server_udp_port"`
Error string `json:"error"`
Version string `json:"version,omitempty"`
RunID string `json:"run_id,omitempty"`
ServerUDPPort int `json:"server_udp_port,omitempty"`
Error string `json:"error,omitempty"`
}
// When frpc login success, send this message to frps for running a new proxy.
type NewProxy struct {
ProxyName string `json:"proxy_name"`
ProxyType string `json:"proxy_type"`
UseEncryption bool `json:"use_encryption"`
UseCompression bool `json:"use_compression"`
Group string `json:"group"`
GroupKey string `json:"group_key"`
Metas map[string]string `json:"metas"`
ProxyName string `json:"proxy_name,omitempty"`
ProxyType string `json:"proxy_type,omitempty"`
UseEncryption bool `json:"use_encryption,omitempty"`
UseCompression bool `json:"use_compression,omitempty"`
Group string `json:"group,omitempty"`
GroupKey string `json:"group_key,omitempty"`
Metas map[string]string `json:"metas,omitempty"`
// tcp and udp only
RemotePort int `json:"remote_port"`
RemotePort int `json:"remote_port,omitempty"`
// http and https only
CustomDomains []string `json:"custom_domains"`
SubDomain string `json:"subdomain"`
Locations []string `json:"locations"`
HTTPUser string `json:"http_user"`
HTTPPwd string `json:"http_pwd"`
HostHeaderRewrite string `json:"host_header_rewrite"`
Headers map[string]string `json:"headers"`
CustomDomains []string `json:"custom_domains,omitempty"`
SubDomain string `json:"subdomain,omitempty"`
Locations []string `json:"locations,omitempty"`
HTTPUser string `json:"http_user,omitempty"`
HTTPPwd string `json:"http_pwd,omitempty"`
HostHeaderRewrite string `json:"host_header_rewrite,omitempty"`
Headers map[string]string `json:"headers,omitempty"`
RouteByHTTPUser string `json:"route_by_http_user,omitempty"`
// stcp
Sk string `json:"sk"`
Sk string `json:"sk,omitempty"`
// tcpmux
Multiplexer string `json:"multiplexer"`
Multiplexer string `json:"multiplexer,omitempty"`
}
type NewProxyResp struct {
ProxyName string `json:"proxy_name"`
RemoteAddr string `json:"remote_addr"`
Error string `json:"error"`
ProxyName string `json:"proxy_name,omitempty"`
RemoteAddr string `json:"remote_addr,omitempty"`
Error string `json:"error,omitempty"`
}
type CloseProxy struct {
ProxyName string `json:"proxy_name"`
ProxyName string `json:"proxy_name,omitempty"`
}
type NewWorkConn struct {
RunID string `json:"run_id"`
PrivilegeKey string `json:"privilege_key"`
Timestamp int64 `json:"timestamp"`
RunID string `json:"run_id,omitempty"`
PrivilegeKey string `json:"privilege_key,omitempty"`
Timestamp int64 `json:"timestamp,omitempty"`
}
type ReqWorkConn struct {
}
type StartWorkConn struct {
ProxyName string `json:"proxy_name"`
SrcAddr string `json:"src_addr"`
DstAddr string `json:"dst_addr"`
SrcPort uint16 `json:"src_port"`
DstPort uint16 `json:"dst_port"`
Error string `json:"error"`
ProxyName string `json:"proxy_name,omitempty"`
SrcAddr string `json:"src_addr,omitempty"`
DstAddr string `json:"dst_addr,omitempty"`
SrcPort uint16 `json:"src_port,omitempty"`
DstPort uint16 `json:"dst_port,omitempty"`
Error string `json:"error,omitempty"`
}
type NewVisitorConn struct {
ProxyName string `json:"proxy_name"`
SignKey string `json:"sign_key"`
Timestamp int64 `json:"timestamp"`
UseEncryption bool `json:"use_encryption"`
UseCompression bool `json:"use_compression"`
ProxyName string `json:"proxy_name,omitempty"`
SignKey string `json:"sign_key,omitempty"`
Timestamp int64 `json:"timestamp,omitempty"`
UseEncryption bool `json:"use_encryption,omitempty"`
UseCompression bool `json:"use_compression,omitempty"`
}
type NewVisitorConnResp struct {
ProxyName string `json:"proxy_name"`
Error string `json:"error"`
ProxyName string `json:"proxy_name,omitempty"`
Error string `json:"error,omitempty"`
}
type Ping struct {
PrivilegeKey string `json:"privilege_key"`
Timestamp int64 `json:"timestamp"`
PrivilegeKey string `json:"privilege_key,omitempty"`
Timestamp int64 `json:"timestamp,omitempty"`
}
type Pong struct {
Error string `json:"error"`
Error string `json:"error,omitempty"`
}
type UDPPacket struct {
Content string `json:"c"`
LocalAddr *net.UDPAddr `json:"l"`
RemoteAddr *net.UDPAddr `json:"r"`
Content string `json:"c,omitempty"`
LocalAddr *net.UDPAddr `json:"l,omitempty"`
RemoteAddr *net.UDPAddr `json:"r,omitempty"`
}
type NatHoleVisitor struct {
ProxyName string `json:"proxy_name"`
SignKey string `json:"sign_key"`
Timestamp int64 `json:"timestamp"`
ProxyName string `json:"proxy_name,omitempty"`
SignKey string `json:"sign_key,omitempty"`
Timestamp int64 `json:"timestamp,omitempty"`
}
type NatHoleClient struct {
ProxyName string `json:"proxy_name"`
Sid string `json:"sid"`
ProxyName string `json:"proxy_name,omitempty"`
Sid string `json:"sid,omitempty"`
}
type NatHoleResp struct {
Sid string `json:"sid"`
VisitorAddr string `json:"visitor_addr"`
ClientAddr string `json:"client_addr"`
Error string `json:"error"`
Sid string `json:"sid,omitempty"`
VisitorAddr string `json:"visitor_addr,omitempty"`
ClientAddr string `json:"client_addr,omitempty"`
Error string `json:"error,omitempty"`
}
type NatHoleClientDetectOK struct {
}
type NatHoleSid struct {
Sid string `json:"sid"`
Sid string `json:"sid,omitempty"`
}

View File

@@ -18,6 +18,7 @@ import (
"context"
"errors"
"fmt"
"strings"
"github.com/fatedier/frp/pkg/util/util"
"github.com/fatedier/frp/pkg/util/xlog"
@@ -26,6 +27,7 @@ import (
type Manager struct {
loginPlugins []Plugin
newProxyPlugins []Plugin
closeProxyPlugins []Plugin
pingPlugins []Plugin
newWorkConnPlugins []Plugin
newUserConnPlugins []Plugin
@@ -35,6 +37,7 @@ func NewManager() *Manager {
return &Manager{
loginPlugins: make([]Plugin, 0),
newProxyPlugins: make([]Plugin, 0),
closeProxyPlugins: make([]Plugin, 0),
pingPlugins: make([]Plugin, 0),
newWorkConnPlugins: make([]Plugin, 0),
newUserConnPlugins: make([]Plugin, 0),
@@ -48,6 +51,9 @@ func (m *Manager) Register(p Plugin) {
if p.IsSupport(OpNewProxy) {
m.newProxyPlugins = append(m.newProxyPlugins, p)
}
if p.IsSupport(OpCloseProxy) {
m.closeProxyPlugins = append(m.closeProxyPlugins, p)
}
if p.IsSupport(OpPing) {
m.pingPlugins = append(m.pingPlugins, p)
}
@@ -127,6 +133,32 @@ func (m *Manager) NewProxy(content *NewProxyContent) (*NewProxyContent, error) {
return content, nil
}
func (m *Manager) CloseProxy(content *CloseProxyContent) error {
if len(m.closeProxyPlugins) == 0 {
return nil
}
errs := make([]string, 0)
reqid, _ := util.RandID()
xl := xlog.New().AppendPrefix("reqid: " + reqid)
ctx := xlog.NewContext(context.Background(), xl)
ctx = NewReqidContext(ctx, reqid)
for _, p := range m.closeProxyPlugins {
_, _, err := p.Handle(ctx, OpCloseProxy, *content)
if err != nil {
xl.Warn("send CloseProxy request to plugin [%s] error: %v", p.Name(), err)
errs = append(errs, fmt.Sprintf("[%s]: %v", p.Name(), err))
}
}
if len(errs) > 0 {
return fmt.Errorf("send CloseProxy request to plugin errors: %s", strings.Join(errs, "; "))
} else {
return nil
}
}
func (m *Manager) Ping(content *PingContent) (*PingContent, error) {
if len(m.pingPlugins) == 0 {
return content, nil

View File

@@ -23,6 +23,7 @@ const (
OpLogin = "Login"
OpNewProxy = "NewProxy"
OpCloseProxy = "CloseProxy"
OpPing = "Ping"
OpNewWorkConn = "NewWorkConn"
OpNewUserConn = "NewUserConn"

View File

@@ -48,6 +48,11 @@ type NewProxyContent struct {
msg.NewProxy
}
type CloseProxyContent struct {
User UserInfo `json:"user"`
msg.CloseProxy
}
type PingContent struct {
User UserInfo `json:"user"`
msg.Ping

View File

@@ -100,6 +100,8 @@ func NewClientTLSConfig(certPath, keyPath, caPath, serverName string) (*tls.Conf
base.Certificates = []tls.Certificate{*cert}
}
base.ServerName = serverName
if caPath != "" {
pool, err := newCertPool(caPath)
if err != nil {
@@ -107,7 +109,6 @@ func NewClientTLSConfig(certPath, keyPath, caPath, serverName string) (*tls.Conf
}
base.RootCAs = pool
base.ServerName = serverName
base.InsecureSkipVerify = false
} else {
base.InsecureSkipVerify = true

View File

@@ -24,18 +24,24 @@ import (
"github.com/fatedier/frp/pkg/util/util"
"github.com/fatedier/frp/pkg/util/vhost"
gnet "github.com/fatedier/golib/net"
)
type HTTPConnectTCPMuxer struct {
*vhost.Muxer
passthrough bool
authRequired bool // Not supported until we really need this.
}
func NewHTTPConnectTCPMuxer(listener net.Listener, timeout time.Duration) (*HTTPConnectTCPMuxer, error) {
mux, err := vhost.NewMuxer(listener, getHostFromHTTPConnect, nil, sendHTTPOk, nil, timeout)
return &HTTPConnectTCPMuxer{mux}, err
func NewHTTPConnectTCPMuxer(listener net.Listener, passthrough bool, timeout time.Duration) (*HTTPConnectTCPMuxer, error) {
ret := &HTTPConnectTCPMuxer{passthrough: passthrough, authRequired: false}
mux, err := vhost.NewMuxer(listener, ret.getHostFromHTTPConnect, nil, ret.sendConnectResponse, nil, timeout)
ret.Muxer = mux
return ret, err
}
func readHTTPConnectRequest(rd io.Reader) (host string, err error) {
func (muxer *HTTPConnectTCPMuxer) readHTTPConnectRequest(rd io.Reader) (host string, httpUser string, err error) {
bufioReader := bufio.NewReader(rd)
req, err := http.ReadRequest(bufioReader)
@@ -49,20 +55,40 @@ func readHTTPConnectRequest(rd io.Reader) (host string, err error) {
}
host, _ = util.CanonicalHost(req.Host)
proxyAuth := req.Header.Get("Proxy-Authorization")
if proxyAuth != "" {
httpUser, _, _ = util.ParseBasicAuth(proxyAuth)
}
return
}
func sendHTTPOk(c net.Conn) error {
func (muxer *HTTPConnectTCPMuxer) sendConnectResponse(c net.Conn, reqInfo map[string]string) error {
if muxer.passthrough {
return nil
}
return util.OkResponse().Write(c)
}
func getHostFromHTTPConnect(c net.Conn) (_ net.Conn, _ map[string]string, err error) {
func (muxer *HTTPConnectTCPMuxer) getHostFromHTTPConnect(c net.Conn) (net.Conn, map[string]string, error) {
reqInfoMap := make(map[string]string, 0)
host, err := readHTTPConnectRequest(c)
sc, rd := gnet.NewSharedConn(c)
host, httpUser, err := muxer.readHTTPConnectRequest(rd)
if err != nil {
return nil, reqInfoMap, err
}
reqInfoMap["Host"] = host
reqInfoMap["Scheme"] = "tcp"
return c, reqInfoMap, nil
reqInfoMap["HTTPUser"] = httpUser
var outConn net.Conn = c
if muxer.passthrough {
outConn = sc
if muxer.authRequired && httpUser == "" {
util.ProxyUnauthorizedResponse().Write(c)
outConn = c
}
}
return outConn, reqInfoMap, nil
}

View File

@@ -15,6 +15,7 @@
package util
import (
"encoding/base64"
"net"
"net/http"
"strings"
@@ -34,6 +35,20 @@ func OkResponse() *http.Response {
return res
}
func ProxyUnauthorizedResponse() *http.Response {
header := make(http.Header)
header.Set("Proxy-Authenticate", `Basic realm="Restricted"`)
res := &http.Response{
Status: "Proxy Authentication Required",
StatusCode: 407,
Proto: "HTTP/1.1",
ProtoMajor: 1,
ProtoMinor: 1,
Header: header,
}
return res
}
// canonicalHost strips port from host if present and returns the canonicalized
// host name.
func CanonicalHost(host string) (string, error) {
@@ -64,3 +79,21 @@ func hasPort(host string) bool {
}
return host[0] == '[' && strings.Contains(host, "]:")
}
func ParseBasicAuth(auth string) (username, password string, ok bool) {
const prefix = "Basic "
// Case insensitive prefix match. See Issue 22736.
if len(auth) < len(prefix) || !strings.EqualFold(auth[:len(prefix)], prefix) {
return
}
c, err := base64.StdEncoding.DecodeString(auth[len(prefix):])
if err != nil {
return
}
cs := string(c)
s := strings.IndexByte(cs, ':')
if s < 0 {
return
}
return cs[:s], cs[s+1:], true
}

View File

@@ -19,9 +19,11 @@ import (
"crypto/rand"
"encoding/hex"
"fmt"
mathrand "math/rand"
"net"
"strconv"
"strings"
"time"
)
// RandID return a rand string used in frp.
@@ -109,3 +111,17 @@ func GenerateResponseErrorString(summary string, err error, detailed bool) strin
}
return summary
}
func RandomSleep(duration time.Duration, minRatio, maxRatio float64) time.Duration {
min := int64(minRatio * 1000.0)
max := int64(maxRatio * 1000.0)
var n int64
if max <= min {
n = min
} else {
n = mathrand.Int63n(max-min) + min
}
d := duration * time.Duration(n) / time.Duration(1000)
time.Sleep(d)
return d
}

View File

@@ -19,7 +19,7 @@ import (
"strings"
)
var version string = "0.39.1"
var version string = "0.43.0"
func Full() string {
return version

View File

@@ -23,17 +23,19 @@ import (
"log"
"net"
"net/http"
"net/url"
"strings"
"time"
frpLog "github.com/fatedier/frp/pkg/util/log"
"github.com/fatedier/frp/pkg/util/util"
frpIo "github.com/fatedier/golib/io"
"github.com/fatedier/golib/pool"
)
var (
ErrNoDomain = errors.New("no such domain")
ErrNoRouteFound = errors.New("no route found")
)
type HTTPReverseProxyOptions struct {
@@ -56,17 +58,22 @@ func NewHTTPReverseProxy(option HTTPReverseProxyOptions, vhostRouter *Routers) *
vhostRouter: vhostRouter,
}
proxy := &ReverseProxy{
// Modify incoming requests by route policies.
Director: func(req *http.Request) {
req.URL.Scheme = "http"
url := req.Context().Value(RouteInfoURL).(string)
routeByHTTPUser := req.Context().Value(RouteInfoHTTPUser).(string)
oldHost, _ := util.CanonicalHost(req.Context().Value(RouteInfoHost).(string))
rc := rp.GetRouteConfig(oldHost, url)
rc := rp.GetRouteConfig(oldHost, url, routeByHTTPUser)
if rc != nil {
if rc.RewriteHost != "" {
req.Host = rc.RewriteHost
}
// Set {domain}.{location} as URL host here to let http transport reuse connections.
req.URL.Host = rc.Domain + "." + base64.StdEncoding.EncodeToString([]byte(rc.Location))
// Set {domain}.{location}.{routeByHTTPUser} as URL host here to let http transport reuse connections.
// TODO(fatedier): use proxy name instead?
req.URL.Host = rc.Domain + "." +
base64.StdEncoding.EncodeToString([]byte(rc.Location)) + "." +
base64.StdEncoding.EncodeToString([]byte(rc.RouteByHTTPUser))
for k, v := range rc.Headers {
req.Header.Set(k, v)
@@ -76,20 +83,36 @@ func NewHTTPReverseProxy(option HTTPReverseProxyOptions, vhostRouter *Routers) *
}
},
// Create a connection to one proxy routed by route policy.
Transport: &http.Transport{
ResponseHeaderTimeout: rp.responseHeaderTimeout,
IdleConnTimeout: 60 * time.Second,
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
url := ctx.Value(RouteInfoURL).(string)
host, _ := util.CanonicalHost(ctx.Value(RouteInfoHost).(string))
routerByHTTPUser := ctx.Value(RouteInfoHTTPUser).(string)
remote := ctx.Value(RouteInfoRemote).(string)
return rp.CreateConnection(host, url, remote)
return rp.CreateConnection(host, url, routerByHTTPUser, remote)
},
Proxy: func(req *http.Request) (*url.URL, error) {
// Use proxy mode if there is host in HTTP first request line.
// GET http://example.com/ HTTP/1.1
// Host: example.com
//
// Normal:
// GET / HTTP/1.1
// Host: example.com
urlHost := req.Context().Value(RouteInfoURLHost).(string)
if urlHost != "" {
return req.URL, nil
}
return nil, nil
},
},
BufferPool: newWrapPool(),
ErrorLog: log.New(newWrapLogger(), "", 0),
ErrorHandler: func(rw http.ResponseWriter, req *http.Request, err error) {
frpLog.Warn("do http proxy request error: %v", err)
frpLog.Warn("do http proxy request [host: %s] error: %v", req.Host, err)
rw.WriteHeader(http.StatusNotFound)
rw.Write(getNotFoundPageContent())
},
@@ -101,7 +124,7 @@ func NewHTTPReverseProxy(option HTTPReverseProxyOptions, vhostRouter *Routers) *
// Register register the route config to reverse proxy
// reverse proxy will use CreateConnFn from routeCfg to create a connection to the remote service
func (rp *HTTPReverseProxy) Register(routeCfg RouteConfig) error {
err := rp.vhostRouter.Add(routeCfg.Domain, routeCfg.Location, &routeCfg)
err := rp.vhostRouter.Add(routeCfg.Domain, routeCfg.Location, routeCfg.RouteByHTTPUser, &routeCfg)
if err != nil {
return err
}
@@ -109,28 +132,29 @@ func (rp *HTTPReverseProxy) Register(routeCfg RouteConfig) error {
}
// UnRegister unregister route config by domain and location
func (rp *HTTPReverseProxy) UnRegister(domain string, location string) {
rp.vhostRouter.Del(domain, location)
func (rp *HTTPReverseProxy) UnRegister(routeCfg RouteConfig) {
rp.vhostRouter.Del(routeCfg.Domain, routeCfg.Location, routeCfg.RouteByHTTPUser)
}
func (rp *HTTPReverseProxy) GetRouteConfig(domain string, location string) *RouteConfig {
vr, ok := rp.getVhost(domain, location)
func (rp *HTTPReverseProxy) GetRouteConfig(domain, location, routeByHTTPUser string) *RouteConfig {
vr, ok := rp.getVhost(domain, location, routeByHTTPUser)
if ok {
frpLog.Debug("get new HTTP request host [%s] path [%s] httpuser [%s]", domain, location, routeByHTTPUser)
return vr.payload.(*RouteConfig)
}
return nil
}
func (rp *HTTPReverseProxy) GetRealHost(domain string, location string) (host string) {
vr, ok := rp.getVhost(domain, location)
func (rp *HTTPReverseProxy) GetRealHost(domain, location, routeByHTTPUser string) (host string) {
vr, ok := rp.getVhost(domain, location, routeByHTTPUser)
if ok {
host = vr.payload.(*RouteConfig).RewriteHost
}
return
}
func (rp *HTTPReverseProxy) GetHeaders(domain string, location string) (headers map[string]string) {
vr, ok := rp.getVhost(domain, location)
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
}
@@ -138,19 +162,19 @@ func (rp *HTTPReverseProxy) GetHeaders(domain string, location string) (headers
}
// CreateConnection create a new connection by route config
func (rp *HTTPReverseProxy) CreateConnection(domain string, location string, remoteAddr string) (net.Conn, error) {
vr, ok := rp.getVhost(domain, location)
func (rp *HTTPReverseProxy) CreateConnection(domain, location, routeByHTTPUser string, remoteAddr string) (net.Conn, error) {
vr, ok := rp.getVhost(domain, location, routeByHTTPUser)
if ok {
fn := vr.payload.(*RouteConfig).CreateConnFn
if fn != nil {
return fn(remoteAddr)
}
}
return nil, fmt.Errorf("%v: %s %s", ErrNoDomain, domain, location)
return nil, fmt.Errorf("%v: %s %s %s", ErrNoRouteFound, domain, location, routeByHTTPUser)
}
func (rp *HTTPReverseProxy) CheckAuth(domain, location, user, passwd string) bool {
vr, ok := rp.getVhost(domain, location)
func (rp *HTTPReverseProxy) CheckAuth(domain, location, routeByHTTPUser, user, passwd string) bool {
vr, ok := rp.getVhost(domain, location, routeByHTTPUser)
if ok {
checkUser := vr.payload.(*RouteConfig).Username
checkPasswd := vr.payload.(*RouteConfig).Password
@@ -161,45 +185,120 @@ func (rp *HTTPReverseProxy) CheckAuth(domain, location, user, passwd string) boo
return true
}
// getVhost get vhost router by domain and location
func (rp *HTTPReverseProxy) getVhost(domain string, location string) (vr *Router, ok bool) {
// first we check the full hostname
// if not exist, then check the wildcard_domain such as *.example.com
vr, ok = rp.vhostRouter.Get(domain, location)
if ok {
return
}
domainSplit := strings.Split(domain, ".")
if len(domainSplit) < 3 {
// getVhost trys to get vhost router by route policy.
func (rp *HTTPReverseProxy) getVhost(domain, location, routeByHTTPUser string) (*Router, bool) {
findRouter := func(inDomain, inLocation, inRouteByHTTPUser string) (*Router, bool) {
vr, ok := rp.vhostRouter.Get(inDomain, inLocation, inRouteByHTTPUser)
if ok {
return vr, ok
}
// Try to check if there is one proxy that doesn't specify routerByHTTPUser, it means match all.
vr, ok = rp.vhostRouter.Get(inDomain, inLocation, "")
if ok {
return vr, ok
}
return nil, false
}
// First we check the full hostname
// if not exist, then check the wildcard_domain such as *.example.com
vr, ok := findRouter(domain, location, routeByHTTPUser)
if ok {
return vr, ok
}
// e.g. domain = test.example.com, try to match wildcard domains.
// *.example.com
// *.com
domainSplit := strings.Split(domain, ".")
for {
if len(domainSplit) < 3 {
return nil, false
break
}
domainSplit[0] = "*"
domain = strings.Join(domainSplit, ".")
vr, ok = rp.vhostRouter.Get(domain, location)
vr, ok = findRouter(domain, location, routeByHTTPUser)
if ok {
return vr, true
}
domainSplit = domainSplit[1:]
}
// Finally, try to check if there is one proxy that domain is "*" means match all domains.
vr, ok = findRouter("*", location, routeByHTTPUser)
if ok {
return vr, true
}
return nil, false
}
func (rp *HTTPReverseProxy) connectHandler(rw http.ResponseWriter, req *http.Request) {
hj, ok := rw.(http.Hijacker)
if !ok {
rw.WriteHeader(http.StatusInternalServerError)
return
}
client, _, err := hj.Hijack()
if err != nil {
rw.WriteHeader(http.StatusInternalServerError)
return
}
url := req.Context().Value(RouteInfoURL).(string)
routeByHTTPUser := req.Context().Value(RouteInfoHTTPUser).(string)
domain, _ := util.CanonicalHost(req.Context().Value(RouteInfoHost).(string))
remoteAddr := req.Context().Value(RouteInfoRemote).(string)
remote, err := rp.CreateConnection(domain, url, routeByHTTPUser, remoteAddr)
if err != nil {
http.Error(rw, "Failed", http.StatusBadRequest)
client.Close()
return
}
req.Write(remote)
go frpIo.Join(remote, client)
}
func (rp *HTTPReverseProxy) injectRequestInfoToCtx(req *http.Request) *http.Request {
newctx := req.Context()
newctx = context.WithValue(newctx, RouteInfoURL, req.URL.Path)
newctx = context.WithValue(newctx, RouteInfoHost, req.Host)
newctx = context.WithValue(newctx, RouteInfoURLHost, req.URL.Host)
user := ""
// If url host isn't empty, it's a proxy request. Get http user from Proxy-Authorization header.
if req.URL.Host != "" {
proxyAuth := req.Header.Get("Proxy-Authorization")
if proxyAuth != "" {
user, _, _ = parseBasicAuth(proxyAuth)
}
}
if user == "" {
user, _, _ = req.BasicAuth()
}
newctx = context.WithValue(newctx, RouteInfoHTTPUser, user)
newctx = context.WithValue(newctx, RouteInfoRemote, req.RemoteAddr)
return req.Clone(newctx)
}
func (rp *HTTPReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
domain, _ := util.CanonicalHost(req.Host)
location := req.URL.Path
user, passwd, _ := req.BasicAuth()
if !rp.CheckAuth(domain, location, user, passwd) {
if !rp.CheckAuth(domain, location, user, user, passwd) {
rw.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return
}
rp.proxy.ServeHTTP(rw, req)
newreq := rp.injectRequestInfoToCtx(req)
if req.Method == http.MethodConnect {
rp.connectHandler(rw, newreq)
} else {
rp.proxy.ServeHTTP(rw, newreq)
}
}
type wrapPool struct{}

View File

@@ -8,6 +8,7 @@ package vhost
import (
"context"
"encoding/base64"
"fmt"
"io"
"log"
@@ -209,6 +210,24 @@ func (p *ReverseProxy) modifyResponse(rw http.ResponseWriter, res *http.Response
return true
}
func parseBasicAuth(auth string) (username, password string, ok bool) {
const prefix = "Basic "
// Case insensitive prefix match. See Issue 22736.
if len(auth) < len(prefix) || !strings.EqualFold(auth[:len(prefix)], prefix) {
return
}
c, err := base64.StdEncoding.DecodeString(auth[len(prefix):])
if err != nil {
return
}
cs := string(c)
s := strings.IndexByte(cs, ':')
if s < 0 {
return
}
return cs[:s], cs[s+1:], true
}
func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
transport := p.Transport
if transport == nil {
@@ -238,13 +257,6 @@ func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
outreq.Header = make(http.Header) // Issue 33142: historical behavior was to always allocate
}
// =============================
// Modified for frp
outreq = outreq.Clone(context.WithValue(outreq.Context(), RouteInfoURL, req.URL.Path))
outreq = outreq.Clone(context.WithValue(outreq.Context(), RouteInfoHost, req.Host))
outreq = outreq.Clone(context.WithValue(outreq.Context(), RouteInfoRemote, req.RemoteAddr))
// =============================
p.Director(outreq)
outreq.Close = false

View File

@@ -11,33 +11,42 @@ var (
ErrRouterConfigConflict = errors.New("router config conflict")
)
type routerByHTTPUser map[string][]*Router
type Routers struct {
RouterByDomain map[string][]*Router
mutex sync.RWMutex
indexByDomain map[string]routerByHTTPUser
mutex sync.RWMutex
}
type Router struct {
domain string
location string
httpUser string
// store any object here
payload interface{}
}
func NewRouters() *Routers {
return &Routers{
RouterByDomain: make(map[string][]*Router),
indexByDomain: make(map[string]routerByHTTPUser),
}
}
func (r *Routers) Add(domain, location string, payload interface{}) error {
func (r *Routers) Add(domain, location, httpUser string, payload interface{}) error {
r.mutex.Lock()
defer r.mutex.Unlock()
if _, exist := r.exist(domain, location); exist {
if _, exist := r.exist(domain, location, httpUser); exist {
return ErrRouterConfigConflict
}
vrs, found := r.RouterByDomain[domain]
routersByHTTPUser, found := r.indexByDomain[domain]
if !found {
routersByHTTPUser = make(map[string][]*Router)
}
vrs, found := routersByHTTPUser[httpUser]
if !found {
vrs = make([]*Router, 0, 1)
}
@@ -45,20 +54,27 @@ func (r *Routers) Add(domain, location string, payload interface{}) error {
vr := &Router{
domain: domain,
location: location,
httpUser: httpUser,
payload: payload,
}
vrs = append(vrs, vr)
sort.Sort(sort.Reverse(ByLocation(vrs)))
r.RouterByDomain[domain] = vrs
routersByHTTPUser[httpUser] = vrs
r.indexByDomain[domain] = routersByHTTPUser
return nil
}
func (r *Routers) Del(domain, location string) {
func (r *Routers) Del(domain, location, httpUser string) {
r.mutex.Lock()
defer r.mutex.Unlock()
vrs, found := r.RouterByDomain[domain]
routersByHTTPUser, found := r.indexByDomain[domain]
if !found {
return
}
vrs, found := routersByHTTPUser[httpUser]
if !found {
return
}
@@ -68,40 +84,46 @@ func (r *Routers) Del(domain, location string) {
newVrs = append(newVrs, vr)
}
}
r.RouterByDomain[domain] = newVrs
routersByHTTPUser[httpUser] = newVrs
}
func (r *Routers) Get(host, path string) (vr *Router, exist bool) {
func (r *Routers) Get(host, path, httpUser string) (vr *Router, exist bool) {
r.mutex.RLock()
defer r.mutex.RUnlock()
vrs, found := r.RouterByDomain[host]
routersByHTTPUser, found := r.indexByDomain[host]
if !found {
return
}
vrs, found := routersByHTTPUser[httpUser]
if !found {
return
}
// can't support load balance, will to do
for _, vr = range vrs {
if strings.HasPrefix(path, vr.location) {
return vr, true
}
}
return
}
func (r *Routers) exist(host, path string) (vr *Router, exist bool) {
vrs, found := r.RouterByDomain[host]
func (r *Routers) exist(host, path, httpUser string) (route *Router, exist bool) {
routersByHTTPUser, found := r.indexByDomain[host]
if !found {
return
}
routers, found := routersByHTTPUser[httpUser]
if !found {
return
}
for _, vr = range vrs {
if path == vr.location {
return vr, true
for _, route = range routers {
if path == route.location {
return route, true
}
}
return
}

View File

@@ -29,16 +29,19 @@ import (
type RouteInfo string
const (
RouteInfoURL RouteInfo = "url"
RouteInfoHost RouteInfo = "host"
RouteInfoRemote RouteInfo = "remote"
RouteInfoURL RouteInfo = "url"
RouteInfoHost RouteInfo = "host"
RouteInfoHTTPUser RouteInfo = "httpUser"
RouteInfoRemote RouteInfo = "remote"
RouteInfoURLHost RouteInfo = "urlHost"
)
type muxFunc func(net.Conn) (net.Conn, map[string]string, error)
type httpAuthFunc func(net.Conn, string, string, string) (bool, error)
type hostRewriteFunc func(net.Conn, string) (net.Conn, error)
type successFunc func(net.Conn) error
type successFunc func(net.Conn, map[string]string) error
// Muxer is only used for https and tcpmux proxy.
type Muxer struct {
listener net.Listener
timeout time.Duration
@@ -49,7 +52,15 @@ type Muxer struct {
registryRouter *Routers
}
func NewMuxer(listener net.Listener, vhostFunc muxFunc, authFunc httpAuthFunc, successFunc successFunc, rewriteFunc hostRewriteFunc, timeout time.Duration) (mux *Muxer, err error) {
func NewMuxer(
listener net.Listener,
vhostFunc muxFunc,
authFunc httpAuthFunc,
successFunc successFunc,
rewriteFunc hostRewriteFunc,
timeout time.Duration,
) (mux *Muxer, err error) {
mux = &Muxer{
listener: listener,
timeout: timeout,
@@ -67,12 +78,13 @@ type CreateConnFunc func(remoteAddr string) (net.Conn, error)
// RouteConfig is the params used to match HTTP requests
type RouteConfig struct {
Domain string
Location string
RewriteHost string
Username string
Password string
Headers map[string]string
Domain string
Location string
RewriteHost string
Username string
Password string
Headers map[string]string
RouteByHTTPUser string
CreateConnFn CreateConnFunc
}
@@ -81,49 +93,66 @@ type RouteConfig struct {
// then rewrite the host header to rewriteHost
func (v *Muxer) Listen(ctx context.Context, cfg *RouteConfig) (l *Listener, err error) {
l = &Listener{
name: cfg.Domain,
location: cfg.Location,
rewriteHost: cfg.RewriteHost,
userName: cfg.Username,
passWord: cfg.Password,
mux: v,
accept: make(chan net.Conn),
ctx: ctx,
name: cfg.Domain,
location: cfg.Location,
routeByHTTPUser: cfg.RouteByHTTPUser,
rewriteHost: cfg.RewriteHost,
userName: cfg.Username,
passWord: cfg.Password,
mux: v,
accept: make(chan net.Conn),
ctx: ctx,
}
err = v.registryRouter.Add(cfg.Domain, cfg.Location, l)
err = v.registryRouter.Add(cfg.Domain, cfg.Location, cfg.RouteByHTTPUser, l)
if err != nil {
return
}
return l, nil
}
func (v *Muxer) getListener(name, path string) (l *Listener, exist bool) {
func (v *Muxer) getListener(name, path, httpUser string) (*Listener, bool) {
findRouter := func(inName, inPath, inHTTPUser string) (*Listener, bool) {
vr, ok := v.registryRouter.Get(inName, inPath, httpUser)
if ok {
return vr.payload.(*Listener), true
}
// Try to check if there is one proxy that doesn't specify routerByHTTPUser, it means match all.
vr, ok = v.registryRouter.Get(inName, inPath, "")
if ok {
return vr.payload.(*Listener), true
}
return nil, false
}
// first we check the full hostname
// if not exist, then check the wildcard_domain such as *.example.com
vr, found := v.registryRouter.Get(name, path)
if found {
return vr.payload.(*Listener), true
l, ok := findRouter(name, path, httpUser)
if ok {
return l, true
}
domainSplit := strings.Split(name, ".")
if len(domainSplit) < 3 {
return
}
for {
if len(domainSplit) < 3 {
return
break
}
domainSplit[0] = "*"
name = strings.Join(domainSplit, ".")
vr, found = v.registryRouter.Get(name, path)
if found {
return vr.payload.(*Listener), true
l, ok = findRouter(name, path, httpUser)
if ok {
return l, true
}
domainSplit = domainSplit[1:]
}
// Finally, try to check if there is one proxy that domain is "*" means match all domains.
l, ok = findRouter("*", path, httpUser)
if ok {
return l, true
}
return nil, false
}
func (v *Muxer) run() {
@@ -151,25 +180,26 @@ func (v *Muxer) handle(c net.Conn) {
name := strings.ToLower(reqInfoMap["Host"])
path := strings.ToLower(reqInfoMap["Path"])
l, ok := v.getListener(name, path)
httpUser := reqInfoMap["HTTPUser"]
l, ok := v.getListener(name, path, httpUser)
if !ok {
res := notFoundResponse()
res.Write(c)
log.Debug("http request for host [%s] path [%s] not found", name, path)
log.Debug("http request for host [%s] path [%s] httpUser [%s] not found", name, path, httpUser)
c.Close()
return
}
xl := xlog.FromContextSafe(l.ctx)
if v.successFunc != nil {
if err := v.successFunc(c); err != nil {
if err := v.successFunc(c, reqInfoMap); err != nil {
xl.Info("success func failure on vhost connection: %v", err)
c.Close()
return
}
}
// if authFunc is exist and userName/password is set
// if authFunc is exist and username/password is set
// then verify user access
if l.mux.authFunc != nil && l.userName != "" && l.passWord != "" {
bAccess, err := l.mux.authFunc(c, l.userName, l.passWord, reqInfoMap["Authorization"])
@@ -188,7 +218,7 @@ func (v *Muxer) handle(c net.Conn) {
}
c = sConn
xl.Debug("get new http request host [%s] path [%s]", name, path)
xl.Debug("new request host [%s] path [%s] httpUser [%s]", name, path, httpUser)
err = errors.PanicToError(func() {
l.accept <- c
})
@@ -198,14 +228,15 @@ func (v *Muxer) handle(c net.Conn) {
}
type Listener struct {
name string
location string
rewriteHost string
userName string
passWord string
mux *Muxer // for closing Muxer
accept chan net.Conn
ctx context.Context
name string
location string
routeByHTTPUser string
rewriteHost string
userName string
passWord string
mux *Muxer // for closing Muxer
accept chan net.Conn
ctx context.Context
}
func (l *Listener) Accept() (net.Conn, error) {
@@ -231,7 +262,7 @@ func (l *Listener) Accept() (net.Conn, error) {
}
func (l *Listener) Close() error {
l.mux.registryRouter.Del(l.name, l.location)
l.mux.registryRouter.Del(l.name, l.location, l.routeByHTTPUser)
close(l.accept)
return nil
}

View File

@@ -258,7 +258,7 @@ func (ctl *Control) GetWorkConn() (workConn net.Conn, err error) {
case workConn, ok = <-ctl.workConnCh:
if !ok {
err = frpErr.ErrCtlClosed
xl.Warn("no work connections avaiable, %v", err)
xl.Warn("no work connections available, %v", err)
return
}
@@ -376,6 +376,20 @@ func (ctl *Control) stoper() {
pxy.Close()
ctl.pxyManager.Del(pxy.GetName())
metrics.Server.CloseProxy(pxy.GetName(), pxy.GetConf().GetBaseInfo().ProxyType)
notifyContent := &plugin.CloseProxyContent{
User: plugin.UserInfo{
User: ctl.loginMsg.User,
Metas: ctl.loginMsg.Metas,
RunID: ctl.loginMsg.RunID,
},
CloseProxy: msg.CloseProxy{
ProxyName: pxy.GetName(),
},
}
go func() {
ctl.pluginManager.CloseProxy(notifyContent)
}()
}
ctl.allShutdown.Done()
@@ -444,11 +458,11 @@ func (ctl *Control) manager() {
ProxyName: m.ProxyName,
}
if err != nil {
xl.Warn("new proxy [%s] error: %v", m.ProxyName, err)
xl.Warn("new proxy [%s] type [%s] error: %v", m.ProxyName, m.ProxyType, err)
resp.Error = util.GenerateResponseErrorString(fmt.Sprintf("new proxy [%s] error", m.ProxyName), err, ctl.serverCfg.DetailedErrorsToClient)
} else {
resp.RemoteAddr = remoteAddr
xl.Info("new proxy [%s] success", m.ProxyName)
xl.Info("new proxy [%s] type [%s] success", m.ProxyName, m.ProxyType)
metrics.Server.NewProxy(m.ProxyName, m.ProxyType)
}
ctl.sendCh <- resp
@@ -564,5 +578,20 @@ func (ctl *Control) CloseProxy(closeMsg *msg.CloseProxy) (err error) {
ctl.mu.Unlock()
metrics.Server.CloseProxy(pxy.GetName(), pxy.GetConf().GetBaseInfo().ProxyType)
notifyContent := &plugin.CloseProxyContent{
User: plugin.UserInfo{
User: ctl.loginMsg.User,
Metas: ctl.loginMsg.Metas,
RunID: ctl.loginMsg.RunID,
},
CloseProxy: msg.CloseProxy{
ProxyName: pxy.GetName(),
},
}
go func() {
ctl.pluginManager.CloseProxy(notifyContent)
}()
return
}

View File

@@ -17,6 +17,7 @@ package server
import (
"net"
"net/http"
"net/http/pprof"
"time"
"github.com/fatedier/frp/assets"
@@ -27,8 +28,8 @@ import (
)
var (
httpServerReadTimeout = 10 * time.Second
httpServerWriteTimeout = 10 * time.Second
httpServerReadTimeout = 60 * time.Second
httpServerWriteTimeout = 60 * time.Second
)
func (svr *Service) RunDashboardServer(address string) (err error) {
@@ -36,6 +37,15 @@ func (svr *Service) RunDashboardServer(address string) (err error) {
router := mux.NewRouter()
router.HandleFunc("/healthz", svr.Healthz)
// debug
if svr.cfg.PprofEnable {
router.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
router.HandleFunc("/debug/pprof/profile", pprof.Profile)
router.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
router.HandleFunc("/debug/pprof/trace", pprof.Trace)
router.PathPrefix("/debug/pprof/").HandlerFunc(pprof.Index)
}
subRouter := router.NewRoute().Subrouter()
user, passwd := svr.cfg.DashboardUser, svr.cfg.DashboardPwd

View File

@@ -10,8 +10,11 @@ import (
)
type HTTPGroupController struct {
// groups by indexKey
groups map[string]*HTTPGroup
// register createConn for each group to vhostRouter.
// createConn will get a connection from one proxy of the group
vhostRouter *vhost.Routers
mu sync.Mutex
@@ -24,10 +27,12 @@ func NewHTTPGroupController(vhostRouter *vhost.Routers) *HTTPGroupController {
}
}
func (ctl *HTTPGroupController) Register(proxyName, group, groupKey string,
routeConfig vhost.RouteConfig) (err error) {
func (ctl *HTTPGroupController) Register(
proxyName, group, groupKey string,
routeConfig vhost.RouteConfig,
) (err error) {
indexKey := httpGroupIndex(group, routeConfig.Domain, routeConfig.Location)
indexKey := group
ctl.mu.Lock()
g, ok := ctl.groups[indexKey]
if !ok {
@@ -39,8 +44,8 @@ func (ctl *HTTPGroupController) Register(proxyName, group, groupKey string,
return g.Register(proxyName, group, groupKey, routeConfig)
}
func (ctl *HTTPGroupController) UnRegister(proxyName, group, domain, location string) {
indexKey := httpGroupIndex(group, domain, location)
func (ctl *HTTPGroupController) UnRegister(proxyName, group string, routeConfig vhost.RouteConfig) {
indexKey := group
ctl.mu.Lock()
defer ctl.mu.Unlock()
g, ok := ctl.groups[indexKey]
@@ -55,11 +60,13 @@ func (ctl *HTTPGroupController) UnRegister(proxyName, group, domain, location st
}
type HTTPGroup struct {
group string
groupKey string
domain string
location string
group string
groupKey string
domain string
location string
routeByHTTPUser string
// CreateConnFuncs indexed by echo proxy name
createFuncs map[string]vhost.CreateConnFunc
pxyNames []string
index uint64
@@ -75,8 +82,10 @@ func NewHTTPGroup(ctl *HTTPGroupController) *HTTPGroup {
}
}
func (g *HTTPGroup) Register(proxyName, group, groupKey string,
routeConfig vhost.RouteConfig) (err error) {
func (g *HTTPGroup) Register(
proxyName, group, groupKey string,
routeConfig vhost.RouteConfig,
) (err error) {
g.mu.Lock()
defer g.mu.Unlock()
@@ -84,7 +93,7 @@ func (g *HTTPGroup) Register(proxyName, group, groupKey string,
// the first proxy in this group
tmp := routeConfig // copy object
tmp.CreateConnFn = g.createConn
err = g.ctl.vhostRouter.Add(routeConfig.Domain, routeConfig.Location, &tmp)
err = g.ctl.vhostRouter.Add(routeConfig.Domain, routeConfig.Location, routeConfig.RouteByHTTPUser, &tmp)
if err != nil {
return
}
@@ -93,8 +102,10 @@ func (g *HTTPGroup) Register(proxyName, group, groupKey string,
g.groupKey = groupKey
g.domain = routeConfig.Domain
g.location = routeConfig.Location
g.routeByHTTPUser = routeConfig.RouteByHTTPUser
} else {
if g.group != group || g.domain != routeConfig.Domain || g.location != routeConfig.Location {
if g.group != group || g.domain != routeConfig.Domain ||
g.location != routeConfig.Location || g.routeByHTTPUser != routeConfig.RouteByHTTPUser {
err = ErrGroupParamsInvalid
return
}
@@ -125,7 +136,7 @@ func (g *HTTPGroup) UnRegister(proxyName string) (isEmpty bool) {
if len(g.createFuncs) == 0 {
isEmpty = true
g.ctl.vhostRouter.Del(g.domain, g.location)
g.ctl.vhostRouter.Del(g.domain, g.location, g.routeByHTTPUser)
}
return
}
@@ -138,6 +149,7 @@ func (g *HTTPGroup) createConn(remoteAddr string) (net.Conn, error) {
group := g.group
domain := g.domain
location := g.location
routeByHTTPUser := g.routeByHTTPUser
if len(g.pxyNames) > 0 {
name := g.pxyNames[int(newIndex)%len(g.pxyNames)]
f, _ = g.createFuncs[name]
@@ -145,12 +157,9 @@ func (g *HTTPGroup) createConn(remoteAddr string) (net.Conn, error) {
g.mu.RUnlock()
if f == nil {
return nil, fmt.Errorf("no CreateConnFunc for http group [%s], domain [%s], location [%s]", group, domain, location)
return nil, fmt.Errorf("no CreateConnFunc for http group [%s], domain [%s], location [%s], routeByHTTPUser [%s]",
group, domain, location, routeByHTTPUser)
}
return f(remoteAddr)
}
func httpGroupIndex(group, domain, location string) string {
return fmt.Sprintf("%s_%s_%s", group, domain, location)
}

View File

@@ -46,8 +46,11 @@ func NewTCPMuxGroupCtl(tcpMuxHTTPConnectMuxer *tcpmux.HTTPConnectTCPMuxer) *TCPM
// Listen is the wrapper for TCPMuxGroup's Listen
// If there are no group, we will create one here
func (tmgc *TCPMuxGroupCtl) Listen(ctx context.Context, multiplexer string, group string, groupKey string,
domain string) (l net.Listener, err error) {
func (tmgc *TCPMuxGroupCtl) Listen(
ctx context.Context,
multiplexer, group, groupKey string,
routeConfig vhost.RouteConfig,
) (l net.Listener, err error) {
tmgc.mu.Lock()
tcpMuxGroup, ok := tmgc.groups[group]
@@ -59,7 +62,7 @@ func (tmgc *TCPMuxGroupCtl) Listen(ctx context.Context, multiplexer string, grou
switch multiplexer {
case consts.HTTPConnectTCPMultiplexer:
return tcpMuxGroup.HTTPConnectListen(ctx, group, groupKey, domain)
return tcpMuxGroup.HTTPConnectListen(ctx, group, groupKey, routeConfig)
default:
err = fmt.Errorf("unknown multiplexer [%s]", multiplexer)
return
@@ -75,9 +78,10 @@ func (tmgc *TCPMuxGroupCtl) RemoveGroup(group string) {
// TCPMuxGroup route connections to different proxies
type TCPMuxGroup struct {
group string
groupKey string
domain string
group string
groupKey string
domain string
routeByHTTPUser string
acceptCh chan net.Conn
index uint64
@@ -99,15 +103,17 @@ func NewTCPMuxGroup(ctl *TCPMuxGroupCtl) *TCPMuxGroup {
// Listen will return a new TCPMuxGroupListener
// if TCPMuxGroup already has a listener, just add a new TCPMuxGroupListener to the queues
// otherwise, listen on the real address
func (tmg *TCPMuxGroup) HTTPConnectListen(ctx context.Context, group string, groupKey string, domain string) (ln *TCPMuxGroupListener, err error) {
func (tmg *TCPMuxGroup) HTTPConnectListen(
ctx context.Context,
group, groupKey string,
routeConfig vhost.RouteConfig,
) (ln *TCPMuxGroupListener, err error) {
tmg.mu.Lock()
defer tmg.mu.Unlock()
if len(tmg.lns) == 0 {
// the first listener, listen on the real address
routeConfig := &vhost.RouteConfig{
Domain: domain,
}
tcpMuxLn, errRet := tmg.ctl.tcpMuxHTTPConnectMuxer.Listen(ctx, routeConfig)
tcpMuxLn, errRet := tmg.ctl.tcpMuxHTTPConnectMuxer.Listen(ctx, &routeConfig)
if errRet != nil {
return nil, errRet
}
@@ -115,7 +121,8 @@ func (tmg *TCPMuxGroup) HTTPConnectListen(ctx context.Context, group string, gro
tmg.group = group
tmg.groupKey = groupKey
tmg.domain = domain
tmg.domain = routeConfig.Domain
tmg.routeByHTTPUser = routeConfig.RouteByHTTPUser
tmg.tcpMuxLn = tcpMuxLn
tmg.lns = append(tmg.lns, ln)
if tmg.acceptCh == nil {
@@ -123,8 +130,8 @@ func (tmg *TCPMuxGroup) HTTPConnectListen(ctx context.Context, group string, gro
}
go tmg.worker()
} else {
// domain in the same group must be equal
if tmg.group != group || tmg.domain != domain {
// route config in the same group must be equal
if tmg.group != group || tmg.domain != routeConfig.Domain || tmg.routeByHTTPUser != routeConfig.RouteByHTTPUser {
return nil, ErrGroupParamsInvalid
}
if tmg.groupKey != groupKey {

View File

@@ -38,11 +38,12 @@ type HTTPProxy struct {
func (pxy *HTTPProxy) Run() (remoteAddr string, err error) {
xl := pxy.xl
routeConfig := vhost.RouteConfig{
RewriteHost: pxy.cfg.HostHeaderRewrite,
Headers: pxy.cfg.Headers,
Username: pxy.cfg.HTTPUser,
Password: pxy.cfg.HTTPPwd,
CreateConnFn: pxy.GetRealConn,
RewriteHost: pxy.cfg.HostHeaderRewrite,
RouteByHTTPUser: pxy.cfg.RouteByHTTPUser,
Headers: pxy.cfg.Headers,
Username: pxy.cfg.HTTPUser,
Password: pxy.cfg.HTTPPwd,
CreateConnFn: pxy.GetRealConn,
}
locations := pxy.cfg.Locations
@@ -65,8 +66,8 @@ func (pxy *HTTPProxy) Run() (remoteAddr string, err error) {
routeConfig.Domain = domain
for _, location := range locations {
routeConfig.Location = location
tmpDomain := routeConfig.Domain
tmpLocation := routeConfig.Location
tmpRouteConfig := routeConfig
// handle group
if pxy.cfg.Group != "" {
@@ -76,7 +77,7 @@ func (pxy *HTTPProxy) Run() (remoteAddr string, err error) {
}
pxy.closeFuncs = append(pxy.closeFuncs, func() {
pxy.rc.HTTPGroupCtl.UnRegister(pxy.name, pxy.cfg.Group, tmpDomain, tmpLocation)
pxy.rc.HTTPGroupCtl.UnRegister(pxy.name, pxy.cfg.Group, tmpRouteConfig)
})
} else {
// no group
@@ -85,11 +86,12 @@ func (pxy *HTTPProxy) Run() (remoteAddr string, err error) {
return
}
pxy.closeFuncs = append(pxy.closeFuncs, func() {
pxy.rc.HTTPReverseProxy.UnRegister(tmpDomain, tmpLocation)
pxy.rc.HTTPReverseProxy.UnRegister(tmpRouteConfig)
})
}
addrs = append(addrs, util.CanonicalAddr(routeConfig.Domain, int(pxy.serverCfg.VhostHTTPPort)))
xl.Info("http proxy listen for host [%s] location [%s] group [%s]", routeConfig.Domain, routeConfig.Location, pxy.cfg.Group)
xl.Info("http proxy listen for host [%s] location [%s] group [%s], routeByHTTPUser [%s]",
routeConfig.Domain, routeConfig.Location, pxy.cfg.Group, pxy.cfg.RouteByHTTPUser)
}
}
@@ -97,8 +99,8 @@ func (pxy *HTTPProxy) Run() (remoteAddr string, err error) {
routeConfig.Domain = pxy.cfg.SubDomain + "." + pxy.serverCfg.SubDomainHost
for _, location := range locations {
routeConfig.Location = location
tmpDomain := routeConfig.Domain
tmpLocation := routeConfig.Location
tmpRouteConfig := routeConfig
// handle group
if pxy.cfg.Group != "" {
@@ -108,7 +110,7 @@ func (pxy *HTTPProxy) Run() (remoteAddr string, err error) {
}
pxy.closeFuncs = append(pxy.closeFuncs, func() {
pxy.rc.HTTPGroupCtl.UnRegister(pxy.name, pxy.cfg.Group, tmpDomain, tmpLocation)
pxy.rc.HTTPGroupCtl.UnRegister(pxy.name, pxy.cfg.Group, tmpRouteConfig)
})
} else {
err = pxy.rc.HTTPReverseProxy.Register(routeConfig)
@@ -116,12 +118,13 @@ func (pxy *HTTPProxy) Run() (remoteAddr string, err error) {
return
}
pxy.closeFuncs = append(pxy.closeFuncs, func() {
pxy.rc.HTTPReverseProxy.UnRegister(tmpDomain, tmpLocation)
pxy.rc.HTTPReverseProxy.UnRegister(tmpRouteConfig)
})
}
addrs = append(addrs, util.CanonicalAddr(tmpDomain, pxy.serverCfg.VhostHTTPPort))
addrs = append(addrs, util.CanonicalAddr(tmpRouteConfig.Domain, pxy.serverCfg.VhostHTTPPort))
xl.Info("http proxy listen for host [%s] location [%s] group [%s]", routeConfig.Domain, routeConfig.Location, pxy.cfg.Group)
xl.Info("http proxy listen for host [%s] location [%s] group [%s], routeByHTTPUser [%s]",
routeConfig.Domain, routeConfig.Location, pxy.cfg.Group, pxy.cfg.RouteByHTTPUser)
}
}
remoteAddr = strings.Join(addrs, ",")

View File

@@ -30,20 +30,23 @@ type TCPMuxProxy struct {
cfg *config.TCPMuxProxyConf
}
func (pxy *TCPMuxProxy) httpConnectListen(domain string, addrs []string) (_ []string, err error) {
func (pxy *TCPMuxProxy) httpConnectListen(domain, routeByHTTPUser string, addrs []string) ([]string, error) {
var l net.Listener
var err error
routeConfig := &vhost.RouteConfig{
Domain: domain,
RouteByHTTPUser: routeByHTTPUser,
}
if pxy.cfg.Group != "" {
l, err = pxy.rc.TCPMuxGroupCtl.Listen(pxy.ctx, pxy.cfg.Multiplexer, pxy.cfg.Group, pxy.cfg.GroupKey, domain)
l, err = pxy.rc.TCPMuxGroupCtl.Listen(pxy.ctx, pxy.cfg.Multiplexer, pxy.cfg.Group, pxy.cfg.GroupKey, *routeConfig)
} else {
routeConfig := &vhost.RouteConfig{
Domain: domain,
}
l, err = pxy.rc.TCPMuxHTTPConnectMuxer.Listen(pxy.ctx, routeConfig)
}
if err != nil {
return nil, err
}
pxy.xl.Info("tcpmux httpconnect multiplexer listens for host [%s]", domain)
pxy.xl.Info("tcpmux httpconnect multiplexer listens for host [%s], group [%s] routeByHTTPUser [%s]",
domain, pxy.cfg.Group, pxy.cfg.RouteByHTTPUser)
pxy.listeners = append(pxy.listeners, l)
return append(addrs, util.CanonicalAddr(domain, pxy.serverCfg.TCPMuxHTTPConnectPort)), nil
}
@@ -55,14 +58,14 @@ func (pxy *TCPMuxProxy) httpConnectRun() (remoteAddr string, err error) {
continue
}
addrs, err = pxy.httpConnectListen(domain, addrs)
addrs, err = pxy.httpConnectListen(domain, pxy.cfg.RouteByHTTPUser, addrs)
if err != nil {
return "", err
}
}
if pxy.cfg.SubDomain != "" {
addrs, err = pxy.httpConnectListen(pxy.cfg.SubDomain+"."+pxy.serverCfg.SubDomainHost, addrs)
addrs, err = pxy.httpConnectListen(pxy.cfg.SubDomain+"."+pxy.serverCfg.SubDomainHost, pxy.cfg.RouteByHTTPUser, addrs)
if err != nil {
return "", err
}

View File

@@ -131,12 +131,12 @@ func NewService(cfg config.ServerCommonConf) (svr *Service, err error) {
return
}
svr.rc.TCPMuxHTTPConnectMuxer, err = tcpmux.NewHTTPConnectTCPMuxer(l, vhostReadWriteTimeout)
svr.rc.TCPMuxHTTPConnectMuxer, err = tcpmux.NewHTTPConnectTCPMuxer(l, cfg.TCPMuxPassthrough, vhostReadWriteTimeout)
if err != nil {
err = fmt.Errorf("Create vhost tcpMuxer error, %v", err)
return
}
log.Info("tcpmux httpconnect multiplexer listen on %s", address)
log.Info("tcpmux httpconnect multiplexer listen on %s, passthough: %v", address, cfg.TCPMuxPassthrough)
}
// Init all plugins
@@ -186,6 +186,7 @@ func NewService(cfg config.ServerCommonConf) (svr *Service, err error) {
}
svr.muxer = mux.NewMux(ln)
svr.muxer.SetKeepAlive(time.Duration(cfg.TCPKeepAlive) * time.Second)
go svr.muxer.Serve()
ln = svr.muxer.DefaultListener()

View File

@@ -268,5 +268,4 @@ var _ = Describe("[Feature: Client-Server]", func() {
})
}
})
})

View File

@@ -10,7 +10,6 @@ import (
"github.com/fatedier/frp/test/e2e/framework/consts"
"github.com/fatedier/frp/test/e2e/mock/server/httpserver"
"github.com/fatedier/frp/test/e2e/pkg/request"
"github.com/fatedier/frp/test/e2e/pkg/utils"
"github.com/gorilla/websocket"
. "github.com/onsi/ginkgo"
@@ -59,28 +58,83 @@ var _ = Describe("[Feature: HTTP]", func() {
f.RunProcesses([]string{serverConf}, []string{clientConf})
// foo path
framework.NewRequestExpect(f).Explain("foo path").Port(vhostHTTPPort).
tests := []struct {
path string
expectResp string
desc string
}{
{path: "/foo", expectResp: "foo", desc: "foo path"},
{path: "/bar", expectResp: "bar", desc: "bar path"},
{path: "/other", expectResp: "foo", desc: "other path"},
}
for _, test := range tests {
framework.NewRequestExpect(f).Explain(test.desc).Port(vhostHTTPPort).
RequestModify(func(r *request.Request) {
r.HTTP().HTTPHost("normal.example.com").HTTPPath(test.path)
}).
ExpectResp([]byte(test.expectResp)).
Ensure()
}
})
It("HTTP route by HTTP user", func() {
vhostHTTPPort := f.AllocPort()
serverConf := getDefaultServerConf(vhostHTTPPort)
fooPort := f.AllocPort()
f.RunServer("", newHTTPServer(fooPort, "foo"))
barPort := f.AllocPort()
f.RunServer("", newHTTPServer(barPort, "bar"))
otherPort := f.AllocPort()
f.RunServer("", newHTTPServer(otherPort, "other"))
clientConf := consts.DefaultClientConfig
clientConf += fmt.Sprintf(`
[foo]
type = http
local_port = %d
custom_domains = normal.example.com
route_by_http_user = user1
[bar]
type = http
local_port = %d
custom_domains = normal.example.com
route_by_http_user = user2
[catchAll]
type = http
local_port = %d
custom_domains = normal.example.com
`, fooPort, barPort, otherPort)
f.RunProcesses([]string{serverConf}, []string{clientConf})
// user1
framework.NewRequestExpect(f).Explain("user1").Port(vhostHTTPPort).
RequestModify(func(r *request.Request) {
r.HTTP().HTTPHost("normal.example.com").HTTPPath("/foo")
r.HTTP().HTTPHost("normal.example.com").HTTPAuth("user1", "")
}).
ExpectResp([]byte("foo")).
Ensure()
// bar path
framework.NewRequestExpect(f).Explain("bar path").Port(vhostHTTPPort).
// user2
framework.NewRequestExpect(f).Explain("user2").Port(vhostHTTPPort).
RequestModify(func(r *request.Request) {
r.HTTP().HTTPHost("normal.example.com").HTTPPath("/bar")
r.HTTP().HTTPHost("normal.example.com").HTTPAuth("user2", "")
}).
ExpectResp([]byte("bar")).
Ensure()
// other path
framework.NewRequestExpect(f).Explain("other path").Port(vhostHTTPPort).
// other user
framework.NewRequestExpect(f).Explain("other user").Port(vhostHTTPPort).
RequestModify(func(r *request.Request) {
r.HTTP().HTTPHost("normal.example.com").HTTPPath("/other")
r.HTTP().HTTPHost("normal.example.com").HTTPAuth("user3", "")
}).
ExpectResp([]byte("foo")).
ExpectResp([]byte("other")).
Ensure()
})
@@ -110,18 +164,14 @@ var _ = Describe("[Feature: HTTP]", func() {
// set incorrect auth header
framework.NewRequestExpect(f).Port(vhostHTTPPort).
RequestModify(func(r *request.Request) {
r.HTTP().HTTPHost("normal.example.com").HTTPHeaders(map[string]string{
"Authorization": utils.BasicAuth("test", "invalid"),
})
r.HTTP().HTTPHost("normal.example.com").HTTPAuth("test", "invalid")
}).
Ensure(framework.ExpectResponseCode(401))
// set correct auth header
framework.NewRequestExpect(f).Port(vhostHTTPPort).
RequestModify(func(r *request.Request) {
r.HTTP().HTTPHost("normal.example.com").HTTPHeaders(map[string]string{
"Authorization": utils.BasicAuth("test", "test"),
})
r.HTTP().HTTPHost("normal.example.com").HTTPAuth("test", "test")
}).
Ensure()
})

View File

@@ -12,7 +12,7 @@ import (
// RunProcesses run multiple processes from templates.
// The first template should always be frps.
func (f *Framework) RunProcesses(serverTemplates []string, clientTemplates []string) {
func (f *Framework) RunProcesses(serverTemplates []string, clientTemplates []string) ([]*process.Process, []*process.Process) {
templates := make([]string, 0, len(serverTemplates)+len(clientTemplates))
for _, t := range serverTemplates {
templates = append(templates, t)
@@ -28,6 +28,7 @@ func (f *Framework) RunProcesses(serverTemplates []string, clientTemplates []str
f.usedPorts[name] = port
}
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]), 0666)
@@ -37,11 +38,13 @@ func (f *Framework) RunProcesses(serverTemplates []string, clientTemplates []str
p := process.NewWithEnvs(TestContext.FRPServerPath, []string{"-c", path}, f.osEnvs)
f.serverConfPaths = append(f.serverConfPaths, path)
f.serverProcesses = append(f.serverProcesses, p)
currentServerProcesses = append(currentServerProcesses, p)
err = p.Start()
ExpectNoError(err)
}
time.Sleep(time.Second)
currentClientProcesses := make([]*process.Process, 0, len(clientTemplates))
for i := range clientTemplates {
index := i + len(serverTemplates)
path := filepath.Join(f.TempDirectory, fmt.Sprintf("frp-e2e-client-%d", i))
@@ -52,11 +55,14 @@ func (f *Framework) RunProcesses(serverTemplates []string, clientTemplates []str
p := process.NewWithEnvs(TestContext.FRPClientPath, []string{"-c", path}, f.osEnvs)
f.clientConfPaths = append(f.clientConfPaths, path)
f.clientProcesses = append(f.clientProcesses, p)
currentClientProcesses = append(currentClientProcesses, p)
err = p.Start()
ExpectNoError(err)
time.Sleep(500 * time.Millisecond)
}
time.Sleep(500 * time.Millisecond)
time.Sleep(time.Second)
return currentServerProcesses, currentClientProcesses
}
func (f *Framework) RunFrps(args ...string) (*process.Process, string, error) {

View File

@@ -13,6 +13,7 @@ import (
"time"
"github.com/fatedier/frp/test/e2e/pkg/rpc"
"github.com/fatedier/frp/test/e2e/pkg/utils"
libdial "github.com/fatedier/golib/net/dial"
)
@@ -20,10 +21,11 @@ type Request struct {
protocol string
// for all protocol
addr string
port int
body []byte
timeout time.Duration
addr string
port int
body []byte
timeout time.Duration
resolver *net.Resolver
// for http or https
method string
@@ -32,6 +34,8 @@ type Request struct {
headers map[string]string
tlsConfig *tls.Config
authValue string
proxyURL string
}
@@ -40,8 +44,9 @@ func New() *Request {
protocol: "tcp",
addr: "127.0.0.1",
method: "GET",
path: "/",
method: "GET",
path: "/",
headers: map[string]string{},
}
}
@@ -108,6 +113,11 @@ func (r *Request) HTTPHeaders(headers map[string]string) *Request {
return r
}
func (r *Request) HTTPAuth(user, password string) *Request {
r.authValue = utils.BasicAuth(user, password)
return r
}
func (r *Request) TLSConfig(tlsConfig *tls.Config) *Request {
r.tlsConfig = tlsConfig
return r
@@ -123,6 +133,11 @@ func (r *Request) Body(content []byte) *Request {
return r
}
func (r *Request) Resolver(resolver *net.Resolver) *Request {
r.resolver = resolver
return r
}
func (r *Request) Do() (*Response, error) {
var (
conn net.Conn
@@ -150,11 +165,12 @@ func (r *Request) Do() (*Response, error) {
return nil, err
}
} else {
dialer := &net.Dialer{Resolver: r.resolver}
switch r.protocol {
case "tcp":
conn, err = net.Dial("tcp", addr)
conn, err = dialer.Dial("tcp", addr)
case "udp":
conn, err = net.Dial("udp", addr)
conn, err = dialer.Dial("udp", addr)
default:
return nil, fmt.Errorf("invalid protocol")
}
@@ -198,11 +214,15 @@ func (r *Request) sendHTTPRequest(method, urlstr string, host string, headers ma
for k, v := range headers {
req.Header.Set(k, v)
}
if r.authValue != "" {
req.Header.Set("Authorization", r.authValue)
}
tr := &http.Transport{
DialContext: (&net.Dialer{
Timeout: time.Second,
KeepAlive: 30 * time.Second,
DualStack: true,
Resolver: r.resolver,
}).DialContext,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,

View File

@@ -12,7 +12,6 @@ import (
"github.com/fatedier/frp/test/e2e/pkg/cert"
"github.com/fatedier/frp/test/e2e/pkg/port"
"github.com/fatedier/frp/test/e2e/pkg/request"
"github.com/fatedier/frp/test/e2e/pkg/utils"
. "github.com/onsi/ginkgo"
)
@@ -181,9 +180,7 @@ var _ = Describe("[Feature: Client-Plugins]", func() {
// from http proxy with auth
framework.NewRequestExpect(f).Request(
framework.NewHTTPRequest().HTTPHost("other.example.com").HTTPPath("/test_static_file").Port(vhostPort).HTTPHeaders(map[string]string{
"Authorization": utils.BasicAuth("abc", "123"),
}),
framework.NewHTTPRequest().HTTPHost("other.example.com").HTTPPath("/test_static_file").Port(vhostPort).HTTPAuth("abc", "123"),
).ExpectResp([]byte("foo")).Ensure()
})

View File

@@ -150,7 +150,7 @@ var _ = Describe("[Feature: Server-Plugins]", func() {
type = tcp
local_port = {{ .%s }}
remote_port = 0
`, framework.TCPEchoServerPort, remotePort)
`, framework.TCPEchoServerPort)
f.RunProcesses([]string{serverConf}, []string{clientConf})
@@ -158,6 +158,56 @@ var _ = Describe("[Feature: Server-Plugins]", func() {
})
})
Describe("CloseProxy", func() {
newFunc := func() *plugin.Request {
var r plugin.Request
r.Content = &plugin.CloseProxyContent{}
return &r
}
It("Validate Info", func() {
localPort := f.AllocPort()
var recordProxyName string
handler := func(req *plugin.Request) *plugin.Response {
var ret plugin.Response
content := req.Content.(*plugin.CloseProxyContent)
recordProxyName = content.ProxyName
return &ret
}
pluginServer := NewHTTPPluginServer(localPort, newFunc, handler, nil)
f.RunServer("", pluginServer)
serverConf := consts.DefaultServerConfig + fmt.Sprintf(`
[plugin.test]
addr = 127.0.0.1:%d
path = /handler
ops = CloseProxy
`, localPort)
clientConf := consts.DefaultClientConfig
remotePort := f.AllocPort()
clientConf += fmt.Sprintf(`
[tcp]
type = tcp
local_port = {{ .%s }}
remote_port = %d
`, framework.TCPEchoServerPort, remotePort)
_, clients := f.RunProcesses([]string{serverConf}, []string{clientConf})
framework.NewRequestExpect(f).Port(remotePort).Ensure()
for _, c := range clients {
c.Stop()
}
time.Sleep(1 * time.Second)
framework.ExpectEqual(recordProxyName, "tcp")
})
})
Describe("Ping", func() {
newFunc := func() *plugin.Request {
var r plugin.Request