Merge remote-tracking branch 'upstream/dev' into dev

This commit is contained in:
2026-02-08 00:57:08 +08:00
125 changed files with 20623 additions and 7709 deletions

View File

@@ -167,6 +167,7 @@ func RegisterClientCommonConfigFlags(cmd *cobra.Command, c *v1.ClientCommonConfi
c.Transport.TLS.Enable = cmd.PersistentFlags().BoolP("tls_enable", "", true, "enable frpc tls")
}
cmd.PersistentFlags().StringVarP(&c.User, "user", "u", "", "user")
cmd.PersistentFlags().StringVar(&c.ClientID, "client-id", "", "unique identifier for this frpc instance")
cmd.PersistentFlags().StringVarP(&c.Auth.Token, "token", "t", "", "auth token")
}

View File

@@ -37,6 +37,8 @@ type ClientCommonConfig struct {
// clients. If this value is not "", proxy names will automatically be
// changed to "{user}.{proxy_name}".
User string `json:"user,omitempty"`
// ClientID uniquely identifies this frpc instance.
ClientID string `json:"clientID,omitempty"`
// ServerAddr specifies the address of the server to connect to. By
// default, this value is "0.0.0.0".

View File

@@ -56,9 +56,9 @@ func (m *serverMetrics) CloseClient() {
}
}
func (m *serverMetrics) NewProxy(name string, proxyType string) {
func (m *serverMetrics) NewProxy(name string, proxyType string, user string, clientID string) {
for _, v := range m.ms {
v.NewProxy(name, proxyType)
v.NewProxy(name, proxyType, user, clientID)
}
}

View File

@@ -98,7 +98,7 @@ func (m *serverMetrics) CloseClient() {
m.info.ClientCounts.Dec(1)
}
func (m *serverMetrics) NewProxy(name string, proxyType string) {
func (m *serverMetrics) NewProxy(name string, proxyType string, user string, clientID string) {
m.mu.Lock()
defer m.mu.Unlock()
counter, ok := m.info.ProxyTypeCounts[proxyType]
@@ -119,6 +119,8 @@ func (m *serverMetrics) NewProxy(name string, proxyType string) {
}
m.info.ProxyStatistics[name] = proxyStats
}
proxyStats.User = user
proxyStats.ClientID = clientID
proxyStats.LastStartTime = time.Now()
}
@@ -215,6 +217,8 @@ func (m *serverMetrics) GetProxiesByType(proxyType string) []*ProxyStats {
ps := &ProxyStats{
Name: name,
Type: proxyStats.ProxyType,
User: proxyStats.User,
ClientID: proxyStats.ClientID,
TodayTrafficIn: proxyStats.TrafficIn.TodayCount(),
TodayTrafficOut: proxyStats.TrafficOut.TodayCount(),
CurConns: int64(proxyStats.CurConns.Count()),
@@ -247,6 +251,8 @@ func (m *serverMetrics) GetProxiesByTypeAndName(proxyType string, proxyName stri
res = &ProxyStats{
Name: name,
Type: proxyStats.ProxyType,
User: proxyStats.User,
ClientID: proxyStats.ClientID,
TodayTrafficIn: proxyStats.TrafficIn.TodayCount(),
TodayTrafficOut: proxyStats.TrafficOut.TodayCount(),
CurConns: int64(proxyStats.CurConns.Count()),
@@ -262,6 +268,31 @@ func (m *serverMetrics) GetProxiesByTypeAndName(proxyType string, proxyName stri
return
}
func (m *serverMetrics) GetProxyByName(proxyName string) (res *ProxyStats) {
m.mu.Lock()
defer m.mu.Unlock()
proxyStats, ok := m.info.ProxyStatistics[proxyName]
if ok {
res = &ProxyStats{
Name: proxyName,
Type: proxyStats.ProxyType,
User: proxyStats.User,
ClientID: proxyStats.ClientID,
TodayTrafficIn: proxyStats.TrafficIn.TodayCount(),
TodayTrafficOut: proxyStats.TrafficOut.TodayCount(),
CurConns: int64(proxyStats.CurConns.Count()),
}
if !proxyStats.LastStartTime.IsZero() {
res.LastStartTime = proxyStats.LastStartTime.Format("01-02 15:04:05")
}
if !proxyStats.LastCloseTime.IsZero() {
res.LastCloseTime = proxyStats.LastCloseTime.Format("01-02 15:04:05")
}
}
return
}
func (m *serverMetrics) GetProxyTraffic(name string) (res *ProxyTrafficInfo) {
m.mu.Lock()
defer m.mu.Unlock()

View File

@@ -35,6 +35,8 @@ type ServerStats struct {
type ProxyStats struct {
Name string
Type string
User string
ClientID string
TodayTrafficIn int64
TodayTrafficOut int64
LastStartTime string
@@ -51,6 +53,8 @@ type ProxyTrafficInfo struct {
type ProxyStatistics struct {
Name string
ProxyType string
User string
ClientID string
TrafficIn metric.DateCounter
TrafficOut metric.DateCounter
CurConns metric.Counter
@@ -78,6 +82,7 @@ type Collector interface {
GetServer() *ServerStats
GetProxiesByType(proxyType string) []*ProxyStats
GetProxiesByTypeAndName(proxyType string, proxyName string) *ProxyStats
GetProxyByName(proxyName string) *ProxyStats
GetProxyTraffic(name string) *ProxyTrafficInfo
ClearOfflineProxies() (int, int)
}

View File

@@ -30,7 +30,7 @@ func (m *serverMetrics) CloseClient() {
m.clientCount.Dec()
}
func (m *serverMetrics) NewProxy(name string, proxyType string) {
func (m *serverMetrics) NewProxy(name string, proxyType string, _ string, _ string) {
m.proxyCount.WithLabelValues(proxyType).Inc()
m.proxyCountDetailed.WithLabelValues(proxyType, name).Inc()
}

View File

@@ -82,6 +82,7 @@ type Login struct {
PrivilegeKey string `json:"privilege_key,omitempty"`
Timestamp int64 `json:"timestamp,omitempty"`
RunID string `json:"run_id,omitempty"`
ClientID string `json:"client_id,omitempty"`
Metas map[string]string `json:"metas,omitempty"`
// Currently only effective for VirtualClient.

View File

@@ -124,8 +124,8 @@ func Forwarder(dstAddr *net.UDPAddr, readCh <-chan *msg.UDPPacket, sendCh chan<-
}
mu.Unlock()
// Add proxy protocol header if configured
if proxyProtocolVersion != "" && udpMsg.RemoteAddr != nil {
// Add proxy protocol header if configured (only for the first packet of a new connection)
if !ok && proxyProtocolVersion != "" && udpMsg.RemoteAddr != nil {
ppBuf, err := netpkg.BuildProxyProtocolHeader(udpMsg.RemoteAddr, dstAddr, proxyProtocolVersion)
if err == nil {
// Prepend proxy protocol header to the UDP payload

View File

@@ -11,7 +11,7 @@ import (
"strconv"
"strings"
"github.com/fatedier/frp/client"
"github.com/fatedier/frp/client/api"
httppkg "github.com/fatedier/frp/pkg/util/http"
)
@@ -32,7 +32,7 @@ func (c *Client) SetAuth(user, pwd string) {
c.authPwd = pwd
}
func (c *Client) GetProxyStatus(ctx context.Context, name string) (*client.ProxyStatusResp, error) {
func (c *Client) GetProxyStatus(ctx context.Context, name string) (*api.ProxyStatusResp, error) {
req, err := http.NewRequestWithContext(ctx, "GET", "http://"+c.address+"/api/status", nil)
if err != nil {
return nil, err
@@ -41,7 +41,7 @@ func (c *Client) GetProxyStatus(ctx context.Context, name string) (*client.Proxy
if err != nil {
return nil, err
}
allStatus := make(client.StatusResp)
allStatus := make(api.StatusResp)
if err = json.Unmarshal([]byte(content), &allStatus); err != nil {
return nil, fmt.Errorf("unmarshal http response error: %s", strings.TrimSpace(content))
}
@@ -55,7 +55,7 @@ func (c *Client) GetProxyStatus(ctx context.Context, name string) (*client.Proxy
return nil, fmt.Errorf("no proxy status found")
}
func (c *Client) GetAllProxyStatus(ctx context.Context) (client.StatusResp, error) {
func (c *Client) GetAllProxyStatus(ctx context.Context) (api.StatusResp, error) {
req, err := http.NewRequestWithContext(ctx, "GET", "http://"+c.address+"/api/status", nil)
if err != nil {
return nil, err
@@ -64,7 +64,7 @@ func (c *Client) GetAllProxyStatus(ctx context.Context) (client.StatusResp, erro
if err != nil {
return nil, err
}
allStatus := make(client.StatusResp)
allStatus := make(api.StatusResp)
if err = json.Unmarshal([]byte(content), &allStatus); err != nil {
return nil, fmt.Errorf("unmarshal http response error: %s", strings.TrimSpace(content))
}

57
pkg/util/http/context.go Normal file
View File

@@ -0,0 +1,57 @@
// Copyright 2025 The frp Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package http
import (
"encoding/json"
"io"
"net/http"
"github.com/gorilla/mux"
)
type Context struct {
Req *http.Request
Resp http.ResponseWriter
vars map[string]string
}
func NewContext(w http.ResponseWriter, r *http.Request) *Context {
return &Context{
Req: r,
Resp: w,
vars: mux.Vars(r),
}
}
func (c *Context) Param(key string) string {
return c.vars[key]
}
func (c *Context) Query(key string) string {
return c.Req.URL.Query().Get(key)
}
func (c *Context) BindJSON(obj any) error {
body, err := io.ReadAll(c.Req.Body)
if err != nil {
return err
}
return json.Unmarshal(body, obj)
}
func (c *Context) Body() ([]byte, error) {
return io.ReadAll(c.Req.Body)
}

33
pkg/util/http/error.go Normal file
View File

@@ -0,0 +1,33 @@
// Copyright 2025 The frp Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package http
import "fmt"
type Error struct {
Code int
Err error
}
func (e *Error) Error() string {
return e.Err.Error()
}
func NewError(code int, msg string) *Error {
return &Error{
Code: code,
Err: fmt.Errorf("%s", msg),
}
}

66
pkg/util/http/handler.go Normal file
View File

@@ -0,0 +1,66 @@
// Copyright 2025 The frp Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package http
import (
"encoding/json"
"net/http"
"github.com/fatedier/frp/pkg/util/log"
)
type GeneralResponse struct {
Code int
Msg string
}
// APIHandler is a handler function that returns a response object or an error.
type APIHandler func(ctx *Context) (any, error)
// MakeHTTPHandlerFunc turns a normal APIHandler into a http.HandlerFunc.
func MakeHTTPHandlerFunc(handler APIHandler) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := NewContext(w, r)
res, err := handler(ctx)
if err != nil {
log.Warnf("http response [%s]: error: %v", r.URL.Path, err)
code := http.StatusInternalServerError
if e, ok := err.(*Error); ok {
code = e.Code
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
_ = json.NewEncoder(w).Encode(GeneralResponse{Code: code, Msg: err.Error()})
return
}
if res == nil {
w.WriteHeader(http.StatusOK)
return
}
switch v := res.(type) {
case []byte:
_, _ = w.Write(v)
case string:
_, _ = w.Write([]byte(v))
default:
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode(v)
}
}
}

View File

@@ -0,0 +1,40 @@
// Copyright 2025 The frp Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package http
import (
"net/http"
"github.com/fatedier/frp/pkg/util/log"
)
type responseWriter struct {
http.ResponseWriter
code int
}
func (rw *responseWriter) WriteHeader(code int) {
rw.code = code
rw.ResponseWriter.WriteHeader(code)
}
func NewRequestLogger(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Infof("http request: [%s]", r.URL.Path)
rw := &responseWriter{ResponseWriter: w, code: http.StatusOK}
next.ServeHTTP(rw, r)
log.Infof("http response [%s]: code [%d]", r.URL.Path, rw.code)
})
}

View File

@@ -14,7 +14,7 @@
package version
var version = "LoliaFRP-CLI 0.66.3"
var version = "LoliaFRP-CLI 0.67.0"
func Full() string {
return version