protocol: add AEAD encryption negotiation to v2 wire control channel (#5304)
Some checks failed
golangci-lint / lint (push) Has been cancelled

This commit is contained in:
fatedier 2026-05-06 10:43:47 +08:00 committed by GitHub
parent 57bb9e80fe
commit 8666e3643f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 866 additions and 86 deletions

View file

@ -74,17 +74,18 @@ func (d *controlSessionDialer) Dial(previousRunID string) (*SessionContext, erro
return nil, err
}
loginRespMsg, err := d.exchangeLogin(conn, loginMsg)
loginResult, err := d.exchangeLogin(conn, loginMsg)
if err != nil {
return nil, err
}
loginRespMsg := loginResult.resp
if loginRespMsg.Error != "" {
return nil, errors.New(loginRespMsg.Error)
}
var controlRW io.ReadWriter = conn
if d.clientSpec == nil || d.clientSpec.Type != "ssh-tunnel" {
controlRW, err = netpkg.NewCryptoReadWriter(conn, d.auth.EncryptionKey())
controlRW, err = d.newControlReadWriter(conn, loginResult.crypto)
if err != nil {
return nil, fmt.Errorf("create control crypto read writer: %w", err)
}
@ -125,9 +126,16 @@ func (d *controlSessionDialer) buildLoginMsg(previousRunID string) (*msg.Login,
return loginMsg, nil
}
func (d *controlSessionDialer) exchangeLogin(conn net.Conn, loginMsg *msg.Login) (*msg.LoginResp, error) {
type loginExchangeResult struct {
resp *msg.LoginResp
crypto *wire.CryptoContext
}
func (d *controlSessionDialer) exchangeLogin(conn net.Conn, loginMsg *msg.Login) (*loginExchangeResult, error) {
rw := msg.NewV1ReadWriter(conn)
var wireConn *wire.Conn
var clientHello wire.ClientHello
var clientHelloPayload []byte
if d.common.Transport.WireProtocol == wire.ProtocolV2 {
if err := wire.WriteMagic(conn); err != nil {
@ -136,14 +144,23 @@ func (d *controlSessionDialer) exchangeLogin(conn net.Conn, loginMsg *msg.Login)
wireConn = wire.NewConn(conn)
rw = msg.NewV2ReadWriterWithConn(wireConn)
hello := wire.DefaultClientHello(wire.BootstrapInfo{
var err error
clientHello, err = wire.NewClientHello(wire.BootstrapInfo{
Transport: d.common.Transport.Protocol,
TLS: lo.FromPtr(d.common.Transport.TLS.Enable) || d.common.Transport.Protocol == "wss" || d.common.Transport.Protocol == "quic",
TCPMux: lo.FromPtr(d.common.Transport.TCPMux),
})
if err := wireConn.WriteJSONFrame(wire.FrameTypeClientHello, hello); err != nil {
if err != nil {
return nil, err
}
clientHelloFrame, err := wire.NewJSONFrame(wire.FrameTypeClientHello, clientHello)
if err != nil {
return nil, err
}
if err := wireConn.WriteFrame(clientHelloFrame); err != nil {
return nil, err
}
clientHelloPayload = clientHelloFrame.Payload
}
if err := rw.WriteMsg(loginMsg); err != nil {
return nil, err
@ -154,19 +171,50 @@ func (d *controlSessionDialer) exchangeLogin(conn net.Conn, loginMsg *msg.Login)
_ = conn.SetReadDeadline(time.Time{})
}()
var cryptoContext *wire.CryptoContext
if wireConn != nil {
serverHelloFrame, err := wireConn.ReadFrame()
if err != nil {
return nil, err
}
if serverHelloFrame.Type != wire.FrameTypeServerHello {
return nil, fmt.Errorf("unexpected frame type %d, want %d", serverHelloFrame.Type, wire.FrameTypeServerHello)
}
var serverHello wire.ServerHello
if err := wireConn.ReadJSONFrame(wire.FrameTypeServerHello, &serverHello); err != nil {
if err := wireConn.UnmarshalFrame(serverHelloFrame, &serverHello); err != nil {
return nil, err
}
if serverHello.Error != "" {
return nil, errors.New(serverHello.Error)
}
cryptoContext, err = wire.NewClientCryptoContext(clientHelloPayload, serverHelloFrame.Payload)
if err != nil {
return nil, err
}
}
var loginRespMsg msg.LoginResp
if err := rw.ReadMsgInto(&loginRespMsg); err != nil {
return nil, err
}
return &loginRespMsg, nil
return &loginExchangeResult{
resp: &loginRespMsg,
crypto: cryptoContext,
}, nil
}
func (d *controlSessionDialer) newControlReadWriter(conn net.Conn, cryptoContext *wire.CryptoContext) (io.ReadWriter, error) {
if d.common.Transport.WireProtocol == wire.ProtocolV2 {
if cryptoContext == nil {
return nil, errors.New("missing v2 crypto negotiation")
}
return netpkg.NewAEADCryptoReadWriter(
conn,
d.auth.EncryptionKey(),
netpkg.AEADCryptoRoleClient,
cryptoContext.Algorithm,
cryptoContext.TranscriptHash,
)
}
return netpkg.NewCryptoReadWriter(conn, d.auth.EncryptionKey())
}

View file

@ -29,6 +29,7 @@ import (
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/msg"
"github.com/fatedier/frp/pkg/proto/wire"
netpkg "github.com/fatedier/frp/pkg/util/net"
)
type testConnector struct {
@ -140,8 +141,17 @@ func TestControlSessionDialerDialV2(t *testing.T) {
}
wireConn := wire.NewConn(serverRaw)
clientHelloFrame, err := wireConn.ReadFrame()
if err != nil {
serverErrCh <- err
return
}
if clientHelloFrame.Type != wire.FrameTypeClientHello {
serverErrCh <- fmt.Errorf("unexpected frame type %d, want %d", clientHelloFrame.Type, wire.FrameTypeClientHello)
return
}
var hello wire.ClientHello
if err := wireConn.ReadJSONFrame(wire.FrameTypeClientHello, &hello); err != nil {
if err := wireConn.UnmarshalFrame(clientHelloFrame, &hello); err != nil {
serverErrCh <- err
return
}
@ -160,11 +170,52 @@ func TestControlSessionDialerDialV2(t *testing.T) {
serverErrCh <- fmt.Errorf("unexpected user: %s", loginMsg.User)
return
}
if err := wireConn.WriteJSONFrame(wire.FrameTypeServerHello, wire.DefaultServerHello()); err != nil {
serverHello, err := wire.NewServerHello(hello)
if err != nil {
serverErrCh <- err
return
}
serverErrCh <- rw.WriteMsg(&msg.LoginResp{RunID: "run-v2"})
serverHelloFrame, err := wire.NewJSONFrame(wire.FrameTypeServerHello, serverHello)
if err != nil {
serverErrCh <- err
return
}
cryptoContext := wire.NewCryptoContext(
serverHello.Selected.Crypto.Algorithm,
clientHelloFrame.Payload,
serverHelloFrame.Payload,
)
if err := wireConn.WriteFrame(serverHelloFrame); err != nil {
serverErrCh <- err
return
}
if err := rw.WriteMsg(&msg.LoginResp{RunID: "run-v2"}); err != nil {
serverErrCh <- err
return
}
controlRW, err := netpkg.NewAEADCryptoReadWriter(
serverRaw,
[]byte("token"),
netpkg.AEADCryptoRoleServer,
cryptoContext.Algorithm,
cryptoContext.TranscriptHash,
)
if err != nil {
serverErrCh <- err
return
}
controlMsgRW := msg.NewReadWriter(controlRW, wire.ProtocolV2)
var ping msg.Ping
if err := controlMsgRW.ReadMsgInto(&ping); err != nil {
serverErrCh <- err
return
}
if ping.PrivilegeKey != "v2-ping" || ping.Timestamp != 12345 {
serverErrCh <- fmt.Errorf("unexpected ping: %+v", ping)
return
}
serverErrCh <- nil
}()
dialer := newTestControlSessionDialer(t, wire.ProtocolV2, connector, nil)
@ -177,6 +228,7 @@ func TestControlSessionDialerDialV2(t *testing.T) {
require.NotNil(t, sessionCtx.Conn)
require.NotNil(t, sessionCtx.Connector)
require.False(t, connector.closed.Load())
require.NoError(t, sessionCtx.Conn.WriteMsg(&msg.Ping{PrivilegeKey: "v2-ping", Timestamp: 12345}))
require.NoError(t, <-serverErrCh)
}