diff --git a/README_agi7.md b/README_agi7.md new file mode 100644 index 00000000..d165da17 --- /dev/null +++ b/README_agi7.md @@ -0,0 +1,9 @@ +## build nvr frpc +```shell +env GOOS=linux GOARCH=arm GOARM=7 go build -v -o frpc ./cmd/frpc +``` + +## build frps for linux +```shell +env GOOS=linux GOARCH=amd64 go build -v -o frps ./cmd/frps +``` diff --git a/go.mod b/go.mod index 2a5003d7..84f8bae0 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 github.com/coreos/go-oidc/v3 v3.10.0 github.com/fatedier/golib v0.4.2 + github.com/golang-jwt/jwt/v5 v5.2.1 github.com/google/uuid v1.6.0 github.com/gorilla/mux v1.8.1 github.com/gorilla/websocket v1.5.1 diff --git a/go.sum b/go.sum index 8f1610f3..6a94df6d 100644 --- a/go.sum +++ b/go.sum @@ -34,6 +34,8 @@ github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= diff --git a/pkg/auth/auth.go b/pkg/auth/auth.go index 9d8db7b6..47cb76f4 100644 --- a/pkg/auth/auth.go +++ b/pkg/auth/auth.go @@ -31,6 +31,8 @@ func NewAuthSetter(cfg v1.AuthClientConfig) (authProvider Setter) { switch cfg.Method { case v1.AuthMethodToken: authProvider = NewTokenAuth(cfg.AdditionalScopes, cfg.Token) + case v1.AuthMethodJWT: + authProvider = NewJWTAuth(cfg.AdditionalScopes, cfg.Token, cfg.Secret) case v1.AuthMethodOIDC: authProvider = NewOidcAuthSetter(cfg.AdditionalScopes, cfg.OIDC) default: @@ -49,6 +51,8 @@ func NewAuthVerifier(cfg v1.AuthServerConfig) (authVerifier Verifier) { switch cfg.Method { case v1.AuthMethodToken: authVerifier = NewTokenAuth(cfg.AdditionalScopes, cfg.Token) + case v1.AuthMethodJWT: + authVerifier = NewJWTAuth(cfg.AdditionalScopes, cfg.Token, cfg.Secret) case v1.AuthMethodOIDC: authVerifier = NewOidcAuthVerifier(cfg.AdditionalScopes, cfg.OIDC) } diff --git a/pkg/auth/jwt.go b/pkg/auth/jwt.go new file mode 100644 index 00000000..9d179b7f --- /dev/null +++ b/pkg/auth/jwt.go @@ -0,0 +1,119 @@ +package auth + +import ( + "errors" + "fmt" + "slices" + "time" + + "github.com/golang-jwt/jwt/v5" + + v1 "github.com/fatedier/frp/pkg/config/v1" + "github.com/fatedier/frp/pkg/msg" +) + +type JWTAuthSetterVerifier struct { + additionalAuthScopes []v1.AuthScope + token string + secret string +} + +func NewJWTAuth(additionalAuthScopes []v1.AuthScope, token, secret string) *JWTAuthSetterVerifier { + return &JWTAuthSetterVerifier{ + additionalAuthScopes: additionalAuthScopes, + token: token, + secret: secret, + } +} + +func (auth *JWTAuthSetterVerifier) SetLogin(loginMsg *msg.Login) error { + loginMsg.PrivilegeKey = auth.token + return nil +} + +func (auth *JWTAuthSetterVerifier) SetPing(pingMsg *msg.Ping) error { + if !slices.Contains(auth.additionalAuthScopes, v1.AuthScopeHeartBeats) { + return nil + } + + pingMsg.Timestamp = time.Now().Unix() + pingMsg.PrivilegeKey = auth.token + return nil +} + +func (auth *JWTAuthSetterVerifier) SetNewWorkConn(newWorkConnMsg *msg.NewWorkConn) error { + if !slices.Contains(auth.additionalAuthScopes, v1.AuthScopeNewWorkConns) { + return nil + } + + newWorkConnMsg.Timestamp = time.Now().Unix() + newWorkConnMsg.PrivilegeKey = auth.token + return nil +} + +func (auth *JWTAuthSetterVerifier) VerifyLogin(m *msg.Login) error { + if m.User == "" { + return errors.New("user is empty") + } + token := m.PrivilegeKey + return auth.VerifyToken(m.User, token) +} + +func (auth *JWTAuthSetterVerifier) VerifyPing(m *msg.Ping) error { + if !slices.Contains(auth.additionalAuthScopes, v1.AuthScopeHeartBeats) { + return nil + } + + token := m.PrivilegeKey + return auth.VerifyToken("", token) +} + +func (auth *JWTAuthSetterVerifier) VerifyNewWorkConn(m *msg.NewWorkConn) error { + if !slices.Contains(auth.additionalAuthScopes, v1.AuthScopeNewWorkConns) { + return nil + } + + token := m.PrivilegeKey + return auth.VerifyToken("", token) +} + +func (auth *JWTAuthSetterVerifier) VerifyToken(user, token string) error { + methodKey := map[string]string{jwt.SigningMethodHS256.Alg(): auth.secret} + parser := jwt.NewParser(jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Name})) + parsedToken, err := parser.Parse(token, func(t *jwt.Token) (any, error) { + key, ok := methodKey[t.Method.Alg()] + if !ok { + return nil, fmt.Errorf("method %s is not supported", t.Method) + } + return []byte(key), nil + }) + + if err != nil { + if errors.Is(err, jwt.ErrTokenExpired) { + return errors.New("token is expired") + } + return err + } + + if !parsedToken.Valid { + return fmt.Errorf("token %s is invalid", token) + } + + claims, ok := parsedToken.Claims.(jwt.MapClaims) + if !ok { + return fmt.Errorf("claims %v is invalid", parsedToken.Claims) + } + + sub := claims["sub"] + if sub != "remote_ssh" { + return fmt.Errorf("token sub is invalid") + } + if len(user) > 0 { + id := claims["aud"] + if id != user { + return fmt.Errorf("token %s is not for user %s", token, user) + } + } + + return nil +} diff --git a/pkg/config/v1/client.go b/pkg/config/v1/client.go index 52b87690..7041e4ed 100644 --- a/pkg/config/v1/client.go +++ b/pkg/config/v1/client.go @@ -175,6 +175,8 @@ type AuthClientConfig struct { // to succeed. By default, this value is "". Token string `json:"token,omitempty"` OIDC AuthOIDCClientConfig `json:"oidc,omitempty"` + + Secret string `json:"secret"` } func (c *AuthClientConfig) Complete() { diff --git a/pkg/config/v1/common.go b/pkg/config/v1/common.go index ddb23356..2579415f 100644 --- a/pkg/config/v1/common.go +++ b/pkg/config/v1/common.go @@ -44,6 +44,7 @@ type AuthMethod string const ( AuthMethodToken AuthMethod = "token" AuthMethodOIDC AuthMethod = "oidc" + AuthMethodJWT AuthMethod = "jwt" ) // QUIC protocol options diff --git a/pkg/config/v1/server.go b/pkg/config/v1/server.go index 03b05d9d..b2cfabf1 100644 --- a/pkg/config/v1/server.go +++ b/pkg/config/v1/server.go @@ -127,6 +127,7 @@ type AuthServerConfig struct { AdditionalScopes []AuthScope `json:"additionalScopes,omitempty"` Token string `json:"token,omitempty"` OIDC AuthOIDCServerConfig `json:"oidc,omitempty"` + Secret string `json:"secret,omitempty"` } func (c *AuthServerConfig) Complete() { diff --git a/pkg/config/v1/validation/validation.go b/pkg/config/v1/validation/validation.go index 4ca6b67f..cbe7a397 100644 --- a/pkg/config/v1/validation/validation.go +++ b/pkg/config/v1/validation/validation.go @@ -33,6 +33,7 @@ var ( SupportedAuthMethods = []v1.AuthMethod{ "token", "oidc", + "jwt", } SupportedAuthAdditionalScopes = []v1.AuthScope{ diff --git a/server/control.go b/server/control.go index ea8a34c1..0227549b 100644 --- a/server/control.go +++ b/server/control.go @@ -186,7 +186,11 @@ func NewControl( ctl.lastPing.Store(time.Now()) if ctlConnEncrypted { - cryptoRW, err := netpkg.NewCryptoReadWriter(ctl.conn, []byte(ctl.serverCfg.Auth.Token)) + key := []byte(ctl.serverCfg.Auth.Token) + if ctl.serverCfg.Auth.Method == v1.AuthMethodJWT { + key = []byte(loginMsg.PrivilegeKey) + } + cryptoRW, err := netpkg.NewCryptoReadWriter(ctl.conn, key) if err != nil { return nil, err } diff --git a/server/proxy/http.go b/server/proxy/http.go index cd4c4b96..3e0d5239 100644 --- a/server/proxy/http.go +++ b/server/proxy/http.go @@ -164,7 +164,12 @@ func (pxy *HTTPProxy) GetRealConn(remoteAddr string) (workConn net.Conn, err err var rwc io.ReadWriteCloser = tmpConn if pxy.cfg.Transport.UseEncryption { - rwc, err = libio.WithEncryption(rwc, []byte(pxy.serverCfg.Auth.Token)) + key := []byte(pxy.serverCfg.Auth.Token) + if pxy.serverCfg.Auth.Method == v1.AuthMethodJWT { + key = []byte(pxy.loginMsg.PrivilegeKey) + } + + rwc, err = libio.WithEncryption(rwc, key) if err != nil { xl.Errorf("create encryption stream error: %v", err) return diff --git a/server/proxy/proxy.go b/server/proxy/proxy.go index d5ab0f13..b73fa9ee 100644 --- a/server/proxy/proxy.go +++ b/server/proxy/proxy.go @@ -240,7 +240,11 @@ func (pxy *BaseProxy) handleUserTCPConnection(userConn net.Conn) { xl.Tracef("handler user tcp connection, use_encryption: %t, use_compression: %t", cfg.Transport.UseEncryption, cfg.Transport.UseCompression) if cfg.Transport.UseEncryption { - local, err = libio.WithEncryption(local, []byte(serverCfg.Auth.Token)) + key := []byte(serverCfg.Auth.Token) + if serverCfg.Auth.Method == v1.AuthMethodJWT { + key = []byte(pxy.loginMsg.PrivilegeKey) + } + local, err = libio.WithEncryption(local, key) if err != nil { xl.Errorf("create encryption stream error: %v", err) return diff --git a/server/proxy/udp.go b/server/proxy/udp.go index 53a07d52..28784f01 100644 --- a/server/proxy/udp.go +++ b/server/proxy/udp.go @@ -205,7 +205,11 @@ func (pxy *UDPProxy) Run() (remoteAddr string, err error) { var rwc io.ReadWriteCloser = workConn if pxy.cfg.Transport.UseEncryption { - rwc, err = libio.WithEncryption(rwc, []byte(pxy.serverCfg.Auth.Token)) + key := []byte(pxy.serverCfg.Auth.Token) + if pxy.serverCfg.Auth.Method == v1.AuthMethodJWT { + key = []byte(pxy.loginMsg.PrivilegeKey) + } + rwc, err = libio.WithEncryption(rwc, key) if err != nil { xl.Errorf("create encryption stream error: %v", err) workConn.Close()