protocol: add v2 wire protocol with binary framing and capability negotiation (#5294)

This commit is contained in:
fatedier
2026-04-27 00:17:00 +08:00
committed by GitHub
parent e8dfd6efcc
commit e9464919d1
40 changed files with 1861 additions and 223 deletions

View File

@@ -17,7 +17,6 @@ package server
import (
"context"
"fmt"
"net"
"runtime/debug"
"sync"
"sync/atomic"
@@ -32,9 +31,7 @@ import (
"github.com/fatedier/frp/pkg/msg"
plugin "github.com/fatedier/frp/pkg/plugin/server"
"github.com/fatedier/frp/pkg/transport"
netpkg "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/wait"
"github.com/fatedier/frp/pkg/util/xlog"
"github.com/fatedier/frp/server/controller"
@@ -108,9 +105,7 @@ type SessionContext struct {
// key used for connection encryption
EncryptionKey []byte
// control connection
Conn net.Conn
// indicates whether the connection is encrypted
ConnEncrypted bool
Conn *msg.Conn
// login message
LoginMsg *msg.Login
// server configuration
@@ -131,7 +126,7 @@ type Control struct {
msgDispatcher *msg.Dispatcher
// work connections
workConnCh chan net.Conn
workConnCh chan *proxy.WorkConn
// proxies in one client
proxies map[string]proxy.Proxy
@@ -161,7 +156,7 @@ func NewControl(ctx context.Context, sessionCtx *SessionContext) (*Control, erro
poolCount := min(sessionCtx.LoginMsg.PoolCount, int(sessionCtx.ServerCfg.Transport.MaxPoolCount))
ctl := &Control{
sessionCtx: sessionCtx,
workConnCh: make(chan net.Conn, poolCount+10),
workConnCh: make(chan *proxy.WorkConn, poolCount+10),
proxies: make(map[string]proxy.Proxy),
poolCount: poolCount,
portsUsedNum: 0,
@@ -172,29 +167,14 @@ func NewControl(ctx context.Context, sessionCtx *SessionContext) (*Control, erro
}
ctl.lastPing.Store(time.Now())
if sessionCtx.ConnEncrypted {
cryptoRW, err := netpkg.NewCryptoReadWriter(sessionCtx.Conn, sessionCtx.EncryptionKey)
if err != nil {
return nil, err
}
ctl.msgDispatcher = msg.NewDispatcher(cryptoRW)
} else {
ctl.msgDispatcher = msg.NewDispatcher(sessionCtx.Conn)
}
ctl.msgDispatcher = msg.NewDispatcher(sessionCtx.Conn)
ctl.registerMsgHandlers()
ctl.msgTransporter = transport.NewMessageTransporter(ctl.msgDispatcher)
return ctl, nil
}
// Start send a login success message to client and start working.
// Start starts the control session workers after login succeeds.
func (ctl *Control) Start() {
loginRespMsg := &msg.LoginResp{
Version: version.Full(),
RunID: ctl.runID,
Error: "",
}
_ = msg.WriteMsg(ctl.sessionCtx.Conn, loginRespMsg)
go func() {
for i := 0; i < ctl.poolCount; i++ {
// ignore error here, that means that this control is closed
@@ -216,7 +196,7 @@ func (ctl *Control) Replaced(newCtl *Control) {
ctl.sessionCtx.Conn.Close()
}
func (ctl *Control) RegisterWorkConn(conn net.Conn) error {
func (ctl *Control) RegisterWorkConn(conn *proxy.WorkConn) error {
xl := ctl.xl
defer func() {
if err := recover(); err != nil {
@@ -239,7 +219,7 @@ func (ctl *Control) RegisterWorkConn(conn net.Conn) error {
// If no workConn available in the pool, send message to frpc to get one or more
// and wait until it is available.
// return an error if wait timeout
func (ctl *Control) GetWorkConn() (workConn net.Conn, err error) {
func (ctl *Control) GetWorkConn() (workConn *proxy.WorkConn, err error) {
xl := ctl.xl
defer func() {
if err := recover(); err != nil {

View File

@@ -57,7 +57,7 @@ type HTTPGroup struct {
// CreateConnFuncs indexed by proxy name
createFuncs map[string]vhost.CreateConnFunc
pxyNames []string
index uint64
index atomic.Uint64
ctl *HTTPGroupController
mu sync.RWMutex
}
@@ -136,7 +136,7 @@ func (g *HTTPGroup) UnRegister(proxyName string) {
func (g *HTTPGroup) createConn(remoteAddr string) (net.Conn, error) {
var f vhost.CreateConnFunc
newIndex := atomic.AddUint64(&g.index, 1)
newIndex := g.index.Add(1)
g.mu.RLock()
group := g.group
@@ -158,7 +158,7 @@ func (g *HTTPGroup) createConn(remoteAddr string) (net.Conn, error) {
}
func (g *HTTPGroup) chooseEndpoint() (string, error) {
newIndex := atomic.AddUint64(&g.index, 1)
newIndex := g.index.Add(1)
name := ""
g.mu.RLock()

View File

@@ -287,6 +287,7 @@ func buildClientInfoResp(info registry.ClientInfo) model.ClientInfoResp {
ClientID: info.ClientID(),
RunID: info.RunID,
Version: info.Version,
WireProtocol: info.WireProtocol,
Hostname: info.Hostname,
ClientIP: info.IP,
FirstConnectedAt: toUnix(info.FirstConnectedAt),

View File

@@ -17,8 +17,11 @@ package http
import (
"encoding/json"
"testing"
"time"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/proto/wire"
"github.com/fatedier/frp/server/registry"
)
func TestGetConfFromConfigurerKeepsPluginFields(t *testing.T) {
@@ -69,3 +72,24 @@ func TestGetConfFromConfigurerKeepsPluginFields(t *testing.T) {
t.Fatalf("plugin httpPassword mismatch, want %q got %#v", "password", got)
}
}
func TestBuildClientInfoRespIncludesWireProtocol(t *testing.T) {
info := registry.ClientInfo{
Key: "user.client",
User: "user",
RawClientID: "client",
RunID: "run-id",
Version: "1.0.0",
WireProtocol: wire.ProtocolV2,
Hostname: "host",
IP: "127.0.0.1",
FirstConnectedAt: time.Unix(1, 0),
LastConnectedAt: time.Unix(2, 0),
Online: true,
}
resp := buildClientInfoResp(info)
if resp.WireProtocol != wire.ProtocolV2 {
t.Fatalf("wire protocol mismatch, want %q got %q", wire.ProtocolV2, resp.WireProtocol)
}
}

View File

@@ -46,6 +46,7 @@ type ClientInfoResp struct {
ClientID string `json:"clientID"`
RunID string `json:"runID"`
Version string `json:"version,omitempty"`
WireProtocol string `json:"wireProtocol,omitempty"`
Hostname string `json:"hostname"`
ClientIP string `json:"clientIP,omitempty"`
FirstConnectedAt int64 `json:"firstConnectedAt"`

View File

@@ -44,7 +44,26 @@ func RegisterProxyFactory(proxyConfType reflect.Type, factory func(*BaseProxy) P
proxyFactoryRegistry[proxyConfType] = factory
}
type GetWorkConnFn func() (net.Conn, error)
type WorkConn struct {
conn *msg.Conn
}
func NewWorkConn(conn *msg.Conn) *WorkConn {
return &WorkConn{conn: conn}
}
func (c *WorkConn) Start(m *msg.StartWorkConn) (net.Conn, error) {
if err := c.conn.WriteMsg(m); err != nil {
return nil, err
}
return c.conn, nil
}
func (c *WorkConn) Close() error {
return c.conn.Close()
}
type GetWorkConnFn func() (*WorkConn, error)
type Proxy interface {
Context() context.Context
@@ -125,13 +144,13 @@ func (pxy *BaseProxy) GetWorkConnFromPool(src, dst net.Addr) (workConn net.Conn,
xl := xlog.FromContextSafe(pxy.ctx)
// try all connections from the pool
for i := 0; i < pxy.poolCount+1; i++ {
if workConn, err = pxy.getWorkConnFn(); err != nil {
var pxyWorkConn *WorkConn
if pxyWorkConn, err = pxy.getWorkConnFn(); err != nil {
xl.Warnf("failed to get work connection: %v", err)
return
}
xl.Debugf("get a new work connection: [%s]", workConn.RemoteAddr().String())
xl.Debugf("get a new work connection: [%s]", pxyWorkConn.conn.RemoteAddr().String())
xl.Spawn().AppendPrefix(pxy.GetName())
workConn = netpkg.NewContextConn(pxy.ctx, workConn)
var (
srcAddr string
@@ -150,7 +169,7 @@ func (pxy *BaseProxy) GetWorkConnFromPool(src, dst net.Addr) (workConn net.Conn,
dstAddr, dstPortStr, _ = net.SplitHostPort(dst.String())
dstPort, _ = strconv.ParseUint(dstPortStr, 10, 16)
}
err = msg.WriteMsg(workConn, &msg.StartWorkConn{
workConn, err = pxyWorkConn.Start(&msg.StartWorkConn{
ProxyName: pxy.GetName(),
SrcAddr: srcAddr,
SrcPort: uint16(srcPort),
@@ -160,9 +179,10 @@ func (pxy *BaseProxy) GetWorkConnFromPool(src, dst net.Addr) (workConn net.Conn,
})
if err != nil {
xl.Warnf("failed to send message to work connection from pool: %v, times: %d", err, i)
workConn.Close()
pxyWorkConn.Close()
workConn = nil
} else {
workConn = netpkg.NewContextConn(pxy.ctx, workConn)
break
}
}

View File

@@ -0,0 +1,53 @@
// Copyright 2026 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 proxy
import (
"net"
"testing"
"github.com/stretchr/testify/require"
"github.com/fatedier/frp/pkg/msg"
)
func TestWorkConnStartWritesStartWorkConn(t *testing.T) {
client, server := net.Pipe()
defer client.Close()
defer server.Close()
serverMsgConn := msg.NewConn(server, msg.NewV2ReadWriter(server))
clientMsgConn := msg.NewConn(client, msg.NewV2ReadWriter(client))
workConn := NewWorkConn(serverMsgConn)
in := &msg.StartWorkConn{ProxyName: "tcp", SrcAddr: "127.0.0.1", SrcPort: 1234}
type startResult struct {
conn net.Conn
err error
}
resultCh := make(chan startResult, 1)
go func() {
conn, err := workConn.Start(in)
resultCh <- startResult{conn: conn, err: err}
}()
out, err := clientMsgConn.ReadMsg()
require.NoError(t, err)
require.Equal(t, in, out)
result := <-resultCh
require.NoError(t, result.err)
require.Same(t, serverMsgConn, result.conn)
}

View File

@@ -29,6 +29,7 @@ type ClientInfo struct {
Hostname string
IP string
Version string
WireProtocol string
FirstConnectedAt time.Time
LastConnectedAt time.Time
DisconnectedAt time.Time
@@ -51,7 +52,7 @@ func NewClientRegistry() *ClientRegistry {
}
// Register stores/updates metadata for a client and returns the registry key plus whether it conflicts with an online client.
func (cr *ClientRegistry) Register(user, rawClientID, runID, hostname, version, remoteAddr string) (key string, conflict bool) {
func (cr *ClientRegistry) Register(user, rawClientID, runID, hostname, version, remoteAddr, wireProtocol string) (key string, conflict bool) {
if runID == "" {
return "", false
}
@@ -88,6 +89,7 @@ func (cr *ClientRegistry) Register(user, rawClientID, runID, hostname, version,
info.Hostname = hostname
info.IP = remoteAddr
info.Version = version
info.WireProtocol = wireProtocol
if info.FirstConnectedAt.IsZero() {
info.FirstConnectedAt = now
}

View File

@@ -0,0 +1,37 @@
// Copyright 2026 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 registry
import (
"testing"
"github.com/fatedier/frp/pkg/proto/wire"
)
func TestClientRegistryRegisterStoresWireProtocol(t *testing.T) {
registry := NewClientRegistry()
key, conflict := registry.Register("user", "client-id", "run-id", "host", "1.0.0", "127.0.0.1", wire.ProtocolV2)
if conflict {
t.Fatal("unexpected client conflict")
}
info, ok := registry.GetByKey(key)
if !ok {
t.Fatalf("client %q not found", key)
}
if info.WireProtocol != wire.ProtocolV2 {
t.Fatalf("wire protocol mismatch, want %q got %q", wire.ProtocolV2, info.WireProtocol)
}
}

View File

@@ -19,6 +19,7 @@ import (
"context"
"crypto/tls"
"fmt"
"io"
"net"
"net/http"
"os"
@@ -37,6 +38,7 @@ import (
"github.com/fatedier/frp/pkg/msg"
"github.com/fatedier/frp/pkg/nathole"
plugin "github.com/fatedier/frp/pkg/plugin/server"
"github.com/fatedier/frp/pkg/proto/wire"
"github.com/fatedier/frp/pkg/ssh"
"github.com/fatedier/frp/pkg/transport"
httppkg "github.com/fatedier/frp/pkg/util/http"
@@ -432,20 +434,15 @@ func (svr *Service) Close() error {
func (svr *Service) handleConnection(ctx context.Context, conn net.Conn, internal bool) {
xl := xlog.FromContextSafe(ctx)
var (
rawMsg msg.Message
err error
)
_ = conn.SetReadDeadline(time.Now().Add(connReadTimeout))
if rawMsg, err = msg.ReadMsg(conn); err != nil {
log.Tracef("failed to read message: %v", err)
acceptedConn, err := svr.acceptConnection(ctx, conn)
if err != nil {
log.Tracef("failed to accept frp connection: %v", err)
conn.Close()
return
}
_ = conn.SetReadDeadline(time.Time{})
conn = acceptedConn.conn
switch m := rawMsg.(type) {
switch m := acceptedConn.firstMsg.(type) {
case *msg.Login:
// server plugin hook
content := &plugin.LoginContent{
@@ -453,35 +450,66 @@ func (svr *Service) handleConnection(ctx context.Context, conn net.Conn, interna
ClientAddress: conn.RemoteAddr().String(),
}
retContent, err := svr.pluginManager.Login(content)
var ctl *Control
if err == nil {
m = &retContent.Login
err = svr.RegisterControl(conn, m, internal)
controlConn := acceptedConn.conn
if !internal {
var controlRW io.ReadWriter
controlRW, err = netpkg.NewCryptoReadWriter(conn, svr.auth.EncryptionKey())
if err == nil {
controlConn = acceptedConn.messageConnFor(controlRW)
}
}
if err == nil {
ctl, err = svr.RegisterControl(controlConn, m, internal, acceptedConn.wireProtocol)
}
}
// If login failed, send error message there.
// Otherwise send success message in control's work goroutine.
if err != nil {
xl.Warnf("register control error: %v", err)
_ = msg.WriteMsg(conn, &msg.LoginResp{
_ = acceptedConn.conn.WriteMsg(&msg.LoginResp{
Version: version.Full(),
Error: util.GenerateResponseErrorString("register control error", err, lo.FromPtr(svr.cfg.DetailedErrorsToClient)),
})
conn.Close()
return
}
if err = acceptedConn.conn.WriteMsg(&msg.LoginResp{
Version: version.Full(),
RunID: ctl.runID,
Error: "",
}); err != nil {
xl.Warnf("write login response error: %v", err)
svr.ctlManager.Del(m.RunID, ctl)
svr.clientRegistry.MarkOfflineByRunID(m.RunID)
conn.Close()
return
}
ctl.Start()
metrics.Server.NewClient()
go func() {
// block until control closed
ctl.WaitClosed()
svr.ctlManager.Del(m.RunID, ctl)
}()
case *msg.NewWorkConn:
if err := svr.RegisterWorkConn(conn, m); err != nil {
if err := svr.RegisterWorkConn(acceptedConn.conn, m); err != nil {
_ = acceptedConn.conn.WriteMsg(&msg.StartWorkConn{
Error: util.GenerateResponseErrorString("invalid NewWorkConn", err, lo.FromPtr(svr.cfg.DetailedErrorsToClient)),
})
conn.Close()
}
case *msg.NewVisitorConn:
if err = svr.RegisterVisitorConn(conn, m); err != nil {
xl.Warnf("register visitor conn error: %v", err)
_ = msg.WriteMsg(conn, &msg.NewVisitorConnResp{
_ = acceptedConn.conn.WriteMsg(&msg.NewVisitorConnResp{
ProxyName: m.ProxyName,
Error: util.GenerateResponseErrorString("register visitor conn error", err, lo.FromPtr(svr.cfg.DetailedErrorsToClient)),
})
conn.Close()
} else {
_ = msg.WriteMsg(conn, &msg.NewVisitorConnResp{
_ = acceptedConn.conn.WriteMsg(&msg.NewVisitorConnResp{
ProxyName: m.ProxyName,
Error: "",
})
@@ -492,6 +520,87 @@ func (svr *Service) handleConnection(ctx context.Context, conn net.Conn, interna
}
}
type acceptedConnection struct {
conn *msg.Conn
wireProtocol string
firstMsg msg.Message
}
func (svr *Service) acceptConnection(ctx context.Context, conn net.Conn) (*acceptedConnection, error) {
_ = conn.SetReadDeadline(time.Now().Add(connReadTimeout))
checkedConn, isV2, err := wire.CheckMagic(conn)
if err != nil {
return nil, fmt.Errorf("read wire protocol magic: %w", err)
}
wireProtocol := wire.ProtocolV1
if isV2 {
wireProtocol = wire.ProtocolV2
}
conn = netpkg.NewContextConn(ctx, checkedConn)
acceptedConn := &acceptedConnection{wireProtocol: wireProtocol}
if isV2 {
wireConn := wire.NewConn(conn)
rw := msg.NewV2ReadWriterWithConn(wireConn)
acceptedConn.conn = msg.NewConn(conn, rw)
acceptedConn.firstMsg, err = acceptedConn.readFirstV2Msg(wireConn)
} else {
rw := msg.NewV1ReadWriter(conn)
acceptedConn.conn = msg.NewConn(conn, rw)
acceptedConn.firstMsg, err = acceptedConn.conn.ReadMsg()
}
if err != nil {
return nil, err
}
_ = conn.SetReadDeadline(time.Time{})
return acceptedConn, nil
}
func (ac *acceptedConnection) messageConnFor(rw io.ReadWriter) *msg.Conn {
return msg.NewConn(ac.conn, msg.NewReadWriter(rw, ac.wireProtocol))
}
func (ac *acceptedConnection) readFirstV2Msg(wireConn *wire.Conn) (msg.Message, error) {
frame, err := wireConn.ReadFrame()
if err != nil {
return nil, fmt.Errorf("read v2 frame: %w", err)
}
if frame.Type == wire.FrameTypeClientHello {
if err := ac.handleClientHello(wireConn, frame); err != nil {
return nil, err
}
frame, err = wireConn.ReadFrame()
if err != nil {
return nil, fmt.Errorf("read first v2 message frame: %w", err)
}
}
m, err := msg.DecodeV2MessageFrame(frame)
if err != nil {
return nil, fmt.Errorf("decode v2 message: %w", err)
}
return m, nil
}
func (ac *acceptedConnection) handleClientHello(wireConn *wire.Conn, frame *wire.Frame) error {
var hello wire.ClientHello
if err := wireConn.UnmarshalFrame(frame, &hello); err != nil {
return fmt.Errorf("decode ClientHello: %w", err)
}
serverHello := wire.DefaultServerHello()
if err := wire.ValidateClientHello(hello); err != nil {
serverHello.Error = err.Error()
_ = wireConn.WriteJSONFrame(wire.FrameTypeServerHello, serverHello)
return err
}
if err := wireConn.WriteJSONFrame(wire.FrameTypeServerHello, serverHello); err != nil {
return fmt.Errorf("write ServerHello: %w", err)
}
return nil
}
// HandleListener accepts connections from client and call handleConnection to handle them.
// If internal is true, it means that this listener is used for internal communication like ssh tunnel gateway.
// TODO(fatedier): Pass some parameters of listener/connection through context to avoid passing too many parameters.
@@ -577,14 +686,19 @@ func (svr *Service) HandleQUICListener(l *quic.Listener) {
}
}
func (svr *Service) RegisterControl(ctlConn net.Conn, loginMsg *msg.Login, internal bool) error {
func (svr *Service) RegisterControl(
ctlConn *msg.Conn,
loginMsg *msg.Login,
internal bool,
wireProtocol string,
) (*Control, error) {
// If client's RunID is empty, it's a new client, we just create a new controller.
// Otherwise, we check if there is one controller has the same run id. If so, we release previous controller and start new one.
var err error
if loginMsg.RunID == "" {
loginMsg.RunID, err = util.RandID()
if err != nil {
return err
return nil, err
}
}
@@ -601,7 +715,7 @@ func (svr *Service) RegisterControl(ctlConn net.Conn, loginMsg *msg.Login, inter
authVerifier = auth.AlwaysPassVerifier
}
if err := authVerifier.VerifyLogin(loginMsg); err != nil {
return err
return nil, err
}
ctl, err := NewControl(ctx, &SessionContext{
@@ -611,7 +725,6 @@ func (svr *Service) RegisterControl(ctlConn net.Conn, loginMsg *msg.Login, inter
AuthVerifier: authVerifier,
EncryptionKey: svr.auth.EncryptionKey(),
Conn: ctlConn,
ConnEncrypted: !internal,
LoginMsg: loginMsg,
ServerCfg: svr.cfg,
ClientRegistry: svr.clientRegistry,
@@ -619,7 +732,7 @@ func (svr *Service) RegisterControl(ctlConn net.Conn, loginMsg *msg.Login, inter
if err != nil {
xl.Warnf("create new controller error: %v", err)
// don't return detailed errors to client
return fmt.Errorf("unexpected error when creating new controller")
return nil, fmt.Errorf("unexpected error when creating new controller")
}
if oldCtl := svr.ctlManager.Add(loginMsg.RunID, ctl); oldCtl != nil {
@@ -630,34 +743,24 @@ func (svr *Service) RegisterControl(ctlConn net.Conn, loginMsg *msg.Login, inter
if host, _, err := net.SplitHostPort(remoteAddr); err == nil {
remoteAddr = host
}
_, conflict := svr.clientRegistry.Register(loginMsg.User, loginMsg.ClientID, loginMsg.RunID, loginMsg.Hostname, loginMsg.Version, remoteAddr)
_, conflict := svr.clientRegistry.Register(loginMsg.User, loginMsg.ClientID, loginMsg.RunID, loginMsg.Hostname, loginMsg.Version, remoteAddr, wireProtocol)
if conflict {
svr.ctlManager.Del(loginMsg.RunID, ctl)
ctl.Close()
return fmt.Errorf("client_id [%s] for user [%s] is already online", loginMsg.ClientID, loginMsg.User)
return nil, fmt.Errorf("client_id [%s] for user [%s] is already online", loginMsg.ClientID, loginMsg.User)
}
ctl.Start()
// for statistics
metrics.Server.NewClient()
go func() {
// block until control closed
ctl.WaitClosed()
svr.ctlManager.Del(loginMsg.RunID, ctl)
}()
return nil
return ctl, nil
}
// RegisterWorkConn register a new work connection to control and proxies need it.
func (svr *Service) RegisterWorkConn(workConn net.Conn, newMsg *msg.NewWorkConn) error {
func (svr *Service) RegisterWorkConn(workConn *msg.Conn, newMsg *msg.NewWorkConn) error {
xl := netpkg.NewLogFromConn(workConn)
ctl, exist := svr.ctlManager.GetByID(newMsg.RunID)
if !exist {
xl.Warnf("no client control found for run id [%s]", newMsg.RunID)
return fmt.Errorf("no client control found for run id [%s]", newMsg.RunID)
}
// server plugin hook
content := &plugin.NewWorkConnContent{
User: plugin.UserInfo{
@@ -675,12 +778,9 @@ func (svr *Service) RegisterWorkConn(workConn net.Conn, newMsg *msg.NewWorkConn)
}
if err != nil {
xl.Warnf("invalid NewWorkConn with run id [%s]", newMsg.RunID)
_ = msg.WriteMsg(workConn, &msg.StartWorkConn{
Error: util.GenerateResponseErrorString("invalid NewWorkConn", err, lo.FromPtr(svr.cfg.DetailedErrorsToClient)),
})
return fmt.Errorf("invalid NewWorkConn with run id [%s]", newMsg.RunID)
return err
}
return ctl.RegisterWorkConn(workConn)
return ctl.RegisterWorkConn(proxy.NewWorkConn(workConn))
}
func (svr *Service) RegisterVisitorConn(visitorConn net.Conn, newMsg *msg.NewVisitorConn) error {