diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index b3e2cb43..80585928 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -17,13 +17,13 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: - go-version: '1.22' + go-version: '1.23' cache: false - name: golangci-lint uses: golangci/golangci-lint-action@v4 with: # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version - version: v1.57 + version: v1.61 # Optional: golangci-lint command line arguments. # args: --issues-exit-code=0 diff --git a/.github/workflows/goreleaser.yml b/.github/workflows/goreleaser.yml index 7c01f376..d55913fd 100644 --- a/.github/workflows/goreleaser.yml +++ b/.github/workflows/goreleaser.yml @@ -15,7 +15,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: '1.22' + go-version: '1.23' - name: Make All run: | diff --git a/.golangci.yml b/.golangci.yml index fb625c9b..f7456d65 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,5 +1,5 @@ service: - golangci-lint-version: 1.57.x # use the fixed version to not introduce new linters unexpectedly + golangci-lint-version: 1.61.x # use the fixed version to not introduce new linters unexpectedly run: concurrency: 4 @@ -14,7 +14,7 @@ linters: enable: - unused - errcheck - - exportloopref + - copyloopvar - gocritic - gofumpt - goimports @@ -90,6 +90,7 @@ linters-settings: - G402 - G404 - G501 + - G115 # integer overflow conversion issues: # List of regexps of issue texts to exclude, empty list by default. diff --git a/README_zh.md b/README_zh.md index a07e840c..63a49325 100644 --- a/README_zh.md +++ b/README_zh.md @@ -95,7 +95,7 @@ frp 是一个免费且开源的项目,我们欢迎任何人为其开发和进 您可以通过 [GitHub Sponsors](https://github.com/sponsors/fatedier) 赞助我们。 -国内用户可以通过 [爱发电](https://afdian.net/a/fatedier) 赞助我们。 +国内用户可以通过 [爱发电](https://afdian.com/a/fatedier) 赞助我们。 企业赞助者可以将贵公司的 Logo 以及链接放置在项目 README 文件中。 diff --git a/Release.md b/Release.md index d0414efe..ee388be1 100644 --- a/Release.md +++ b/Release.md @@ -1,8 +1,4 @@ ### Features -* Added a new plugin `tls2raw`: Enables TLS termination and forwarding of decrypted raw traffic to local service. -* Added a default timeout of 30 seconds for the frpc subcommands to prevent commands from being stuck for a long time due to network issues. - -### Fixes - -* Fixed the issue that when `loginFailExit = false`, the frpc stop command cannot be stopped correctly if the server is not successfully connected after startup. +* The frpc visitor command-line parameter adds the `--server-user` option to specify the username of the server-side proxy to connect to. +* Support multiple frpc instances with different subjects when using oidc authentication. diff --git a/client/control.go b/client/control.go index eeea1285..3e20c312 100644 --- a/client/control.go +++ b/client/control.go @@ -230,7 +230,7 @@ func (ctl *Control) registerMsgHandlers() { ctl.msgDispatcher.RegisterHandler(&msg.Pong{}, ctl.handlePong) } -// headerWorker sends heartbeat to server and check heartbeat timeout. +// heartbeatWorker sends heartbeat to server and check heartbeat timeout. func (ctl *Control) heartbeatWorker() { xl := ctl.xl diff --git a/client/proxy/proxy_wrapper.go b/client/proxy/proxy_wrapper.go index 487e3702..95048f29 100644 --- a/client/proxy/proxy_wrapper.go +++ b/client/proxy/proxy_wrapper.go @@ -137,7 +137,7 @@ func (pw *Wrapper) SetRunningStatus(remoteAddr string, respErr string) error { pw.Phase = ProxyPhaseStartErr pw.Err = respErr pw.lastStartErr = time.Now() - return fmt.Errorf(pw.Err) + return fmt.Errorf("%s", pw.Err) } if err := pw.pxy.Run(); err != nil { diff --git a/conf/frpc_full_example.toml b/conf/frpc_full_example.toml index 3bf86866..eb447392 100644 --- a/conf/frpc_full_example.toml +++ b/conf/frpc_full_example.toml @@ -327,7 +327,7 @@ requestHeaders.set.x-from-where = "frp" [[proxies]] name = "plugin_tls2raw" -type = "https" +type = "tcp" remotePort = 6008 [proxies.plugin] type = "tls2raw" diff --git a/dockerfiles/Dockerfile-for-frpc b/dockerfiles/Dockerfile-for-frpc index 99b31207..326c75b9 100644 --- a/dockerfiles/Dockerfile-for-frpc +++ b/dockerfiles/Dockerfile-for-frpc @@ -1,4 +1,4 @@ -FROM golang:1.22 AS building +FROM golang:1.23 AS building COPY . /building WORKDIR /building diff --git a/dockerfiles/Dockerfile-for-frps b/dockerfiles/Dockerfile-for-frps index 3d2c387a..0772686c 100644 --- a/dockerfiles/Dockerfile-for-frps +++ b/dockerfiles/Dockerfile-for-frps @@ -1,4 +1,4 @@ -FROM golang:1.22 AS building +FROM golang:1.23 AS building COPY . /building WORKDIR /building diff --git a/pkg/auth/auth.go b/pkg/auth/auth.go index 9d8db7b6..ae706986 100644 --- a/pkg/auth/auth.go +++ b/pkg/auth/auth.go @@ -50,7 +50,8 @@ func NewAuthVerifier(cfg v1.AuthServerConfig) (authVerifier Verifier) { case v1.AuthMethodToken: authVerifier = NewTokenAuth(cfg.AdditionalScopes, cfg.Token) case v1.AuthMethodOIDC: - authVerifier = NewOidcAuthVerifier(cfg.AdditionalScopes, cfg.OIDC) + tokenVerifier := NewTokenVerifier(cfg.OIDC) + authVerifier = NewOidcAuthVerifier(cfg.AdditionalScopes, tokenVerifier) } return authVerifier } diff --git a/pkg/auth/oidc.go b/pkg/auth/oidc.go index d87420ff..40ce060f 100644 --- a/pkg/auth/oidc.go +++ b/pkg/auth/oidc.go @@ -87,14 +87,18 @@ func (auth *OidcAuthProvider) SetNewWorkConn(newWorkConnMsg *msg.NewWorkConn) (e return err } +type TokenVerifier interface { + Verify(context.Context, string) (*oidc.IDToken, error) +} + type OidcAuthConsumer struct { additionalAuthScopes []v1.AuthScope - verifier *oidc.IDTokenVerifier - subjectFromLogin string + verifier TokenVerifier + subjectsFromLogin []string } -func NewOidcAuthVerifier(additionalAuthScopes []v1.AuthScope, cfg v1.AuthOIDCServerConfig) *OidcAuthConsumer { +func NewTokenVerifier(cfg v1.AuthOIDCServerConfig) TokenVerifier { provider, err := oidc.NewProvider(context.Background(), cfg.Issuer) if err != nil { panic(err) @@ -105,9 +109,14 @@ func NewOidcAuthVerifier(additionalAuthScopes []v1.AuthScope, cfg v1.AuthOIDCSer SkipExpiryCheck: cfg.SkipExpiryCheck, SkipIssuerCheck: cfg.SkipIssuerCheck, } + return provider.Verifier(&verifierConf) +} + +func NewOidcAuthVerifier(additionalAuthScopes []v1.AuthScope, verifier TokenVerifier) *OidcAuthConsumer { return &OidcAuthConsumer{ additionalAuthScopes: additionalAuthScopes, - verifier: provider.Verifier(&verifierConf), + verifier: verifier, + subjectsFromLogin: []string{}, } } @@ -116,7 +125,9 @@ func (auth *OidcAuthConsumer) VerifyLogin(loginMsg *msg.Login) (err error) { if err != nil { return fmt.Errorf("invalid OIDC token in login: %v", err) } - auth.subjectFromLogin = token.Subject + if !slices.Contains(auth.subjectsFromLogin, token.Subject) { + auth.subjectsFromLogin = append(auth.subjectsFromLogin, token.Subject) + } return nil } @@ -125,11 +136,11 @@ func (auth *OidcAuthConsumer) verifyPostLoginToken(privilegeKey string) (err err if err != nil { return fmt.Errorf("invalid OIDC token in ping: %v", err) } - if token.Subject != auth.subjectFromLogin { + if !slices.Contains(auth.subjectsFromLogin, token.Subject) { return fmt.Errorf("received different OIDC subject in login and ping. "+ - "original subject: %s, "+ + "original subjects: %s, "+ "new subject: %s", - auth.subjectFromLogin, token.Subject) + auth.subjectsFromLogin, token.Subject) } return nil } diff --git a/pkg/auth/oidc_test.go b/pkg/auth/oidc_test.go new file mode 100644 index 00000000..58054186 --- /dev/null +++ b/pkg/auth/oidc_test.go @@ -0,0 +1,64 @@ +package auth_test + +import ( + "context" + "testing" + "time" + + "github.com/coreos/go-oidc/v3/oidc" + "github.com/stretchr/testify/require" + + "github.com/fatedier/frp/pkg/auth" + v1 "github.com/fatedier/frp/pkg/config/v1" + "github.com/fatedier/frp/pkg/msg" +) + +type mockTokenVerifier struct{} + +func (m *mockTokenVerifier) Verify(ctx context.Context, subject string) (*oidc.IDToken, error) { + return &oidc.IDToken{ + Subject: subject, + }, nil +} + +func TestPingWithEmptySubjectFromLoginFails(t *testing.T) { + r := require.New(t) + consumer := auth.NewOidcAuthVerifier([]v1.AuthScope{v1.AuthScopeHeartBeats}, &mockTokenVerifier{}) + err := consumer.VerifyPing(&msg.Ping{ + PrivilegeKey: "ping-without-login", + Timestamp: time.Now().UnixMilli(), + }) + r.Error(err) + r.Contains(err.Error(), "received different OIDC subject in login and ping") +} + +func TestPingAfterLoginWithNewSubjectSucceeds(t *testing.T) { + r := require.New(t) + consumer := auth.NewOidcAuthVerifier([]v1.AuthScope{v1.AuthScopeHeartBeats}, &mockTokenVerifier{}) + err := consumer.VerifyLogin(&msg.Login{ + PrivilegeKey: "ping-after-login", + }) + r.NoError(err) + + err = consumer.VerifyPing(&msg.Ping{ + PrivilegeKey: "ping-after-login", + Timestamp: time.Now().UnixMilli(), + }) + r.NoError(err) +} + +func TestPingAfterLoginWithDifferentSubjectFails(t *testing.T) { + r := require.New(t) + consumer := auth.NewOidcAuthVerifier([]v1.AuthScope{v1.AuthScopeHeartBeats}, &mockTokenVerifier{}) + err := consumer.VerifyLogin(&msg.Login{ + PrivilegeKey: "login-with-first-subject", + }) + r.NoError(err) + + err = consumer.VerifyPing(&msg.Ping{ + PrivilegeKey: "ping-with-different-subject", + Timestamp: time.Now().UnixMilli(), + }) + r.Error(err) + r.Contains(err.Error(), "received different OIDC subject in login and ping") +} diff --git a/pkg/config/flags.go b/pkg/config/flags.go index 98f617be..ce2582dd 100644 --- a/pkg/config/flags.go +++ b/pkg/config/flags.go @@ -140,6 +140,7 @@ func registerVisitorBaseConfigFlags(cmd *cobra.Command, c *v1.VisitorBaseConfig, cmd.Flags().BoolVarP(&c.Transport.UseCompression, "uc", "", false, "use compression") cmd.Flags().StringVarP(&c.SecretKey, "sk", "", "", "secret key") cmd.Flags().StringVarP(&c.ServerName, "server_name", "", "", "server name") + cmd.Flags().StringVarP(&c.ServerUser, "server-user", "", "", "server user") cmd.Flags().StringVarP(&c.BindAddr, "bind_addr", "", "", "bind addr") cmd.Flags().IntVarP(&c.BindPort, "bind_port", "", 0, "bind port") } diff --git a/pkg/config/types/types.go b/pkg/config/types/types.go index a6cd2e71..8fa3105a 100644 --- a/pkg/config/types/types.go +++ b/pkg/config/types/types.go @@ -159,18 +159,18 @@ func NewPortsRangeSliceFromString(str string) ([]PortsRange, error) { out = append(out, PortsRange{Single: int(singleNum)}) case 2: // range numbers - min, err := strconv.ParseInt(strings.TrimSpace(numArray[0]), 10, 64) + minNum, err := strconv.ParseInt(strings.TrimSpace(numArray[0]), 10, 64) if err != nil { return nil, fmt.Errorf("range number is invalid, %v", err) } - max, err := strconv.ParseInt(strings.TrimSpace(numArray[1]), 10, 64) + maxNum, err := strconv.ParseInt(strings.TrimSpace(numArray[1]), 10, 64) if err != nil { return nil, fmt.Errorf("range number is invalid, %v", err) } - if max < min { + if maxNum < minNum { return nil, fmt.Errorf("range number is invalid") } - out = append(out, PortsRange{Start: int(min), End: int(max)}) + out = append(out, PortsRange{Start: int(minNum), End: int(maxNum)}) default: return nil, fmt.Errorf("range number is invalid") } diff --git a/pkg/nathole/utils.go b/pkg/nathole/utils.go index 3896a215..5f32142e 100644 --- a/pkg/nathole/utils.go +++ b/pkg/nathole/utils.go @@ -78,9 +78,9 @@ func ListAllLocalIPs() ([]net.IP, error) { return ips, nil } -func ListLocalIPsForNatHole(max int) ([]string, error) { - if max <= 0 { - return nil, fmt.Errorf("max must be greater than 0") +func ListLocalIPsForNatHole(maxItems int) ([]string, error) { + if maxItems <= 0 { + return nil, fmt.Errorf("maxItems must be greater than 0") } ips, err := ListAllLocalIPs() @@ -88,9 +88,9 @@ func ListLocalIPsForNatHole(max int) ([]string, error) { return nil, err } - filtered := make([]string, 0, max) + filtered := make([]string, 0, maxItems) for _, ip := range ips { - if len(filtered) >= max { + if len(filtered) >= maxItems { break } diff --git a/pkg/util/util/util.go b/pkg/util/util/util.go index 7758054d..774af2cf 100644 --- a/pkg/util/util/util.go +++ b/pkg/util/util/util.go @@ -85,21 +85,21 @@ func ParseRangeNumbers(rangeStr string) (numbers []int64, err error) { numbers = append(numbers, singleNum) case 2: // range numbers - min, errRet := strconv.ParseInt(strings.TrimSpace(numArray[0]), 10, 64) + minValue, errRet := strconv.ParseInt(strings.TrimSpace(numArray[0]), 10, 64) if errRet != nil { err = fmt.Errorf("range number is invalid, %v", errRet) return } - max, errRet := strconv.ParseInt(strings.TrimSpace(numArray[1]), 10, 64) + maxValue, errRet := strconv.ParseInt(strings.TrimSpace(numArray[1]), 10, 64) if errRet != nil { err = fmt.Errorf("range number is invalid, %v", errRet) return } - if max < min { + if maxValue < minValue { err = fmt.Errorf("range number is invalid") return } - for i := min; i <= max; i++ { + for i := minValue; i <= maxValue; i++ { numbers = append(numbers, i) } default: @@ -118,13 +118,13 @@ func GenerateResponseErrorString(summary string, err error, detailed bool) strin } func RandomSleep(duration time.Duration, minRatio, maxRatio float64) time.Duration { - min := int64(minRatio * 1000.0) - max := int64(maxRatio * 1000.0) + minValue := int64(minRatio * 1000.0) + maxValue := int64(maxRatio * 1000.0) var n int64 - if max <= min { - n = min + if maxValue <= minValue { + n = minValue } else { - n = mathrand.Int64N(max-min) + min + n = mathrand.Int64N(maxValue-minValue) + minValue } d := duration * time.Duration(n) / time.Duration(1000) time.Sleep(d) diff --git a/pkg/util/version/version.go b/pkg/util/version/version.go index a60d71d2..91b2ed98 100644 --- a/pkg/util/version/version.go +++ b/pkg/util/version/version.go @@ -14,7 +14,7 @@ package version -var version = "0.60.0" +var version = "0.61.0" func Full() string { return version diff --git a/server/proxy/proxy.go b/server/proxy/proxy.go index d5ab0f13..c7c18c32 100644 --- a/server/proxy/proxy.go +++ b/server/proxy/proxy.go @@ -137,17 +137,17 @@ func (pxy *BaseProxy) GetWorkConnFromPool(src, dst net.Addr) (workConn net.Conn, dstAddr string srcPortStr string dstPortStr string - srcPort int - dstPort int + srcPort uint64 + dstPort uint64 ) if src != nil { srcAddr, srcPortStr, _ = net.SplitHostPort(src.String()) - srcPort, _ = strconv.Atoi(srcPortStr) + srcPort, _ = strconv.ParseUint(srcPortStr, 10, 16) } if dst != nil { dstAddr, dstPortStr, _ = net.SplitHostPort(dst.String()) - dstPort, _ = strconv.Atoi(dstPortStr) + dstPort, _ = strconv.ParseUint(dstPortStr, 10, 16) } err := msg.WriteMsg(workConn, &msg.StartWorkConn{ ProxyName: pxy.GetName(), @@ -190,8 +190,8 @@ func (pxy *BaseProxy) startCommonTCPListenersHandler() { } else { tempDelay *= 2 } - if max := 1 * time.Second; tempDelay > max { - tempDelay = max + if maxTime := 1 * time.Second; tempDelay > maxTime { + tempDelay = maxTime } xl.Infof("met temporary error: %s, sleep for %s ...", err, tempDelay) time.Sleep(tempDelay)