Skip to content

Commit 8666e36

Browse files
authored
protocol: add AEAD encryption negotiation to v2 wire control channel (#5304)
1 parent 57bb9e8 commit 8666e36

15 files changed

Lines changed: 865 additions & 85 deletions

File tree

README.md

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,14 @@ frp is an open source project with its ongoing development made possible entirel
1313

1414
<h3 align="center">Gold Sponsors</h3>
1515
<!--gold sponsors start-->
16+
<p align="center">
17+
<a href="https://jb.gg/frp" target="_blank">
18+
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_jetbrains.jpg">
19+
<br>
20+
<b>The complete IDE crafted for professional Go developers</b>
21+
</a>
22+
</p>
23+
1624
<p align="center">
1725
<a href="https://github.com/beclab/Olares" target="_blank">
1826
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_olares.jpeg">
@@ -32,24 +40,6 @@ If you're looking for a meeting recording API, consider checking out [Recall.ai]
3240
an API that records Zoom, Google Meet, Microsoft Teams, in-person meetings, and more.
3341

3442
</div>
35-
36-
<p align="center">
37-
<a href="https://requestly.com/?utm_source=github&utm_medium=partnered&utm_campaign=frp" target="_blank">
38-
<img width="480px" src="https://github.com/user-attachments/assets/24670320-997d-4d62-9bca-955c59fe883d">
39-
<br>
40-
<b>Requestly - Free & Open-Source alternative to Postman</b>
41-
<br>
42-
<sub>All-in-one platform to Test, Mock and Intercept APIs.</sub>
43-
</a>
44-
</p>
45-
46-
<p align="center">
47-
<a href="https://jb.gg/frp" target="_blank">
48-
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_jetbrains.jpg">
49-
<br>
50-
<b>The complete IDE crafted for professional Go developers</b>
51-
</a>
52-
</p>
5343
<!--gold sponsors end-->
5444

5545
## What is frp?

README_zh.md

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,14 @@ frp 是一个完全开源的项目,我们的开发工作完全依靠赞助者
1515

1616
<h3 align="center">Gold Sponsors</h3>
1717
<!--gold sponsors start-->
18+
<p align="center">
19+
<a href="https://jb.gg/frp" target="_blank">
20+
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_jetbrains.jpg">
21+
<br>
22+
<b>The complete IDE crafted for professional Go developers</b>
23+
</a>
24+
</p>
25+
1826
<p align="center">
1927
<a href="https://github.com/beclab/Olares" target="_blank">
2028
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_olares.jpeg">
@@ -34,24 +42,6 @@ If you're looking for a meeting recording API, consider checking out [Recall.ai]
3442
an API that records Zoom, Google Meet, Microsoft Teams, in-person meetings, and more.
3543

3644
</div>
37-
38-
<p align="center">
39-
<a href="https://requestly.com/?utm_source=github&utm_medium=partnered&utm_campaign=frp" target="_blank">
40-
<img width="480px" src="https://github.com/user-attachments/assets/24670320-997d-4d62-9bca-955c59fe883d">
41-
<br>
42-
<b>Requestly - Free & Open-Source alternative to Postman</b>
43-
<br>
44-
<sub>All-in-one platform to Test, Mock and Intercept APIs.</sub>
45-
</a>
46-
</p>
47-
48-
<p align="center">
49-
<a href="https://jb.gg/frp" target="_blank">
50-
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_jetbrains.jpg">
51-
<br>
52-
<b>The complete IDE crafted for professional Go developers</b>
53-
</a>
54-
</p>
5545
<!--gold sponsors end-->
5646

5747
## 为什么使用 frp ?

Release.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,12 @@ This release introduces wire protocol v2 as a transition path for future frpc/fr
1010

1111
**The default value of `transport.wireProtocol` remains `v1` in this release.** Users can keep the default for now. To test v2 early, upgrade both frpc and frps to versions that support it, then set `transport.wireProtocol = "v2"` in frpc. A v2-enabled frpc cannot connect to an older frps.
1212

13+
When `transport.wireProtocol = "v2"` is enabled, the control channel uses negotiated AEAD encryption after the login handshake. Both frpc and frps must be upgraded to this release to use v2.
14+
1315
v1 will be deprecated when v2 becomes the default in a future release. It will continue to be supported until v0.78.0 is released, and may be removed in v0.78.0 or later.
1416

1517
## Features
1618

1719
* Added `transport.wireProtocol` for frpc to select the internal message protocol used between frpc and frps. Supported values are `v1` and `v2`.
1820
* Added client protocol visibility in the frps dashboard and `/api/clients` API. Online clients now report their negotiated protocol as `v1` or `v2`.
21+
* Wire protocol v2 now negotiates AEAD control-channel encryption. Supported algorithms are `xchacha20-poly1305` and `aes-256-gcm`; frpc advertises its preferred order based on local AES-GCM hardware support, and frps selects the first supported algorithm from that list.

client/control_session.go

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -74,17 +74,18 @@ func (d *controlSessionDialer) Dial(previousRunID string) (*SessionContext, erro
7474
return nil, err
7575
}
7676

77-
loginRespMsg, err := d.exchangeLogin(conn, loginMsg)
77+
loginResult, err := d.exchangeLogin(conn, loginMsg)
7878
if err != nil {
7979
return nil, err
8080
}
81+
loginRespMsg := loginResult.resp
8182
if loginRespMsg.Error != "" {
8283
return nil, errors.New(loginRespMsg.Error)
8384
}
8485

8586
var controlRW io.ReadWriter = conn
8687
if d.clientSpec == nil || d.clientSpec.Type != "ssh-tunnel" {
87-
controlRW, err = netpkg.NewCryptoReadWriter(conn, d.auth.EncryptionKey())
88+
controlRW, err = d.newControlReadWriter(conn, loginResult.crypto)
8889
if err != nil {
8990
return nil, fmt.Errorf("create control crypto read writer: %w", err)
9091
}
@@ -125,9 +126,16 @@ func (d *controlSessionDialer) buildLoginMsg(previousRunID string) (*msg.Login,
125126
return loginMsg, nil
126127
}
127128

128-
func (d *controlSessionDialer) exchangeLogin(conn net.Conn, loginMsg *msg.Login) (*msg.LoginResp, error) {
129+
type loginExchangeResult struct {
130+
resp *msg.LoginResp
131+
crypto *wire.CryptoContext
132+
}
133+
134+
func (d *controlSessionDialer) exchangeLogin(conn net.Conn, loginMsg *msg.Login) (*loginExchangeResult, error) {
129135
rw := msg.NewV1ReadWriter(conn)
130136
var wireConn *wire.Conn
137+
var clientHello wire.ClientHello
138+
var clientHelloPayload []byte
131139

132140
if d.common.Transport.WireProtocol == wire.ProtocolV2 {
133141
if err := wire.WriteMagic(conn); err != nil {
@@ -136,14 +144,23 @@ func (d *controlSessionDialer) exchangeLogin(conn net.Conn, loginMsg *msg.Login)
136144

137145
wireConn = wire.NewConn(conn)
138146
rw = msg.NewV2ReadWriterWithConn(wireConn)
139-
hello := wire.DefaultClientHello(wire.BootstrapInfo{
147+
var err error
148+
clientHello, err = wire.NewClientHello(wire.BootstrapInfo{
140149
Transport: d.common.Transport.Protocol,
141150
TLS: lo.FromPtr(d.common.Transport.TLS.Enable) || d.common.Transport.Protocol == "wss" || d.common.Transport.Protocol == "quic",
142151
TCPMux: lo.FromPtr(d.common.Transport.TCPMux),
143152
})
144-
if err := wireConn.WriteJSONFrame(wire.FrameTypeClientHello, hello); err != nil {
153+
if err != nil {
154+
return nil, err
155+
}
156+
clientHelloFrame, err := wire.NewJSONFrame(wire.FrameTypeClientHello, clientHello)
157+
if err != nil {
145158
return nil, err
146159
}
160+
if err := wireConn.WriteFrame(clientHelloFrame); err != nil {
161+
return nil, err
162+
}
163+
clientHelloPayload = clientHelloFrame.Payload
147164
}
148165
if err := rw.WriteMsg(loginMsg); err != nil {
149166
return nil, err
@@ -154,19 +171,50 @@ func (d *controlSessionDialer) exchangeLogin(conn net.Conn, loginMsg *msg.Login)
154171
_ = conn.SetReadDeadline(time.Time{})
155172
}()
156173

174+
var cryptoContext *wire.CryptoContext
157175
if wireConn != nil {
176+
serverHelloFrame, err := wireConn.ReadFrame()
177+
if err != nil {
178+
return nil, err
179+
}
180+
if serverHelloFrame.Type != wire.FrameTypeServerHello {
181+
return nil, fmt.Errorf("unexpected frame type %d, want %d", serverHelloFrame.Type, wire.FrameTypeServerHello)
182+
}
158183
var serverHello wire.ServerHello
159-
if err := wireConn.ReadJSONFrame(wire.FrameTypeServerHello, &serverHello); err != nil {
184+
if err := wireConn.UnmarshalFrame(serverHelloFrame, &serverHello); err != nil {
160185
return nil, err
161186
}
162187
if serverHello.Error != "" {
163188
return nil, errors.New(serverHello.Error)
164189
}
190+
cryptoContext, err = wire.NewClientCryptoContext(clientHelloPayload, serverHelloFrame.Payload)
191+
if err != nil {
192+
return nil, err
193+
}
165194
}
166195

167196
var loginRespMsg msg.LoginResp
168197
if err := rw.ReadMsgInto(&loginRespMsg); err != nil {
169198
return nil, err
170199
}
171-
return &loginRespMsg, nil
200+
return &loginExchangeResult{
201+
resp: &loginRespMsg,
202+
crypto: cryptoContext,
203+
}, nil
204+
}
205+
206+
func (d *controlSessionDialer) newControlReadWriter(conn net.Conn, cryptoContext *wire.CryptoContext) (io.ReadWriter, error) {
207+
if d.common.Transport.WireProtocol == wire.ProtocolV2 {
208+
if cryptoContext == nil {
209+
return nil, errors.New("missing v2 crypto negotiation")
210+
}
211+
return netpkg.NewAEADCryptoReadWriter(
212+
conn,
213+
d.auth.EncryptionKey(),
214+
netpkg.AEADCryptoRoleClient,
215+
cryptoContext.Algorithm,
216+
cryptoContext.TranscriptHash,
217+
)
218+
}
219+
return netpkg.NewCryptoReadWriter(conn, d.auth.EncryptionKey())
172220
}

client/control_session_test.go

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
v1 "github.com/fatedier/frp/pkg/config/v1"
3030
"github.com/fatedier/frp/pkg/msg"
3131
"github.com/fatedier/frp/pkg/proto/wire"
32+
netpkg "github.com/fatedier/frp/pkg/util/net"
3233
)
3334

3435
type testConnector struct {
@@ -140,8 +141,17 @@ func TestControlSessionDialerDialV2(t *testing.T) {
140141
}
141142

142143
wireConn := wire.NewConn(serverRaw)
144+
clientHelloFrame, err := wireConn.ReadFrame()
145+
if err != nil {
146+
serverErrCh <- err
147+
return
148+
}
149+
if clientHelloFrame.Type != wire.FrameTypeClientHello {
150+
serverErrCh <- fmt.Errorf("unexpected frame type %d, want %d", clientHelloFrame.Type, wire.FrameTypeClientHello)
151+
return
152+
}
143153
var hello wire.ClientHello
144-
if err := wireConn.ReadJSONFrame(wire.FrameTypeClientHello, &hello); err != nil {
154+
if err := wireConn.UnmarshalFrame(clientHelloFrame, &hello); err != nil {
145155
serverErrCh <- err
146156
return
147157
}
@@ -160,11 +170,52 @@ func TestControlSessionDialerDialV2(t *testing.T) {
160170
serverErrCh <- fmt.Errorf("unexpected user: %s", loginMsg.User)
161171
return
162172
}
163-
if err := wireConn.WriteJSONFrame(wire.FrameTypeServerHello, wire.DefaultServerHello()); err != nil {
173+
serverHello, err := wire.NewServerHello(hello)
174+
if err != nil {
164175
serverErrCh <- err
165176
return
166177
}
167-
serverErrCh <- rw.WriteMsg(&msg.LoginResp{RunID: "run-v2"})
178+
serverHelloFrame, err := wire.NewJSONFrame(wire.FrameTypeServerHello, serverHello)
179+
if err != nil {
180+
serverErrCh <- err
181+
return
182+
}
183+
cryptoContext := wire.NewCryptoContext(
184+
serverHello.Selected.Crypto.Algorithm,
185+
clientHelloFrame.Payload,
186+
serverHelloFrame.Payload,
187+
)
188+
if err := wireConn.WriteFrame(serverHelloFrame); err != nil {
189+
serverErrCh <- err
190+
return
191+
}
192+
if err := rw.WriteMsg(&msg.LoginResp{RunID: "run-v2"}); err != nil {
193+
serverErrCh <- err
194+
return
195+
}
196+
197+
controlRW, err := netpkg.NewAEADCryptoReadWriter(
198+
serverRaw,
199+
[]byte("token"),
200+
netpkg.AEADCryptoRoleServer,
201+
cryptoContext.Algorithm,
202+
cryptoContext.TranscriptHash,
203+
)
204+
if err != nil {
205+
serverErrCh <- err
206+
return
207+
}
208+
controlMsgRW := msg.NewReadWriter(controlRW, wire.ProtocolV2)
209+
var ping msg.Ping
210+
if err := controlMsgRW.ReadMsgInto(&ping); err != nil {
211+
serverErrCh <- err
212+
return
213+
}
214+
if ping.PrivilegeKey != "v2-ping" || ping.Timestamp != 12345 {
215+
serverErrCh <- fmt.Errorf("unexpected ping: %+v", ping)
216+
return
217+
}
218+
serverErrCh <- nil
168219
}()
169220

170221
dialer := newTestControlSessionDialer(t, wire.ProtocolV2, connector, nil)
@@ -177,6 +228,7 @@ func TestControlSessionDialerDialV2(t *testing.T) {
177228
require.NotNil(t, sessionCtx.Conn)
178229
require.NotNil(t, sessionCtx.Connector)
179230
require.False(t, connector.closed.Load())
231+
require.NoError(t, sessionCtx.Conn.WriteMsg(&msg.Ping{PrivilegeKey: "v2-ping", Timestamp: 12345}))
180232
require.NoError(t, <-serverErrCh)
181233
}
182234

doc/agents/release.md

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,51 @@ git commit -m "bump version to vX.Y.Z"
3333
git push origin dev
3434
```
3535

36-
## 3. Merge dev → master
36+
## 3. Pre-release Validation
37+
38+
Run the standard e2e suite locally:
39+
40+
```bash
41+
make e2e
42+
```
43+
44+
For releases that touch compatibility-sensitive areas such as login, control
45+
connections, work connections, visitors, transport, or wire protocol handling,
46+
also run the manual compatibility e2e suite:
47+
48+
```bash
49+
make e2e-compatibility
50+
make e2e-compatibility-floor
51+
```
52+
53+
`make e2e-compatibility` builds the current `frps` and `frpc`, resolves the
54+
recent stable release baselines from GitHub, downloads or reuses their binaries,
55+
and tests current binaries against those historical releases. The default number
56+
of recent baselines is controlled by `FRP_COMPAT_BASELINE_COUNT` in the
57+
`Makefile`.
58+
59+
Downloaded release binaries are cached under:
60+
61+
```text
62+
.cache/e2e-compat/<version>/<os>_<arch>/
63+
```
64+
65+
For a release validation run that must be exactly reproducible, pass an explicit
66+
baseline matrix instead of using the floating recent-release list:
67+
68+
```bash
69+
FRP_COMPAT_BASELINE_VERSIONS="0.X.0 0.Y.0" make e2e-compatibility
70+
```
71+
72+
Use `make e2e-compatibility-smoke` for a quick single-baseline check while
73+
iterating locally. If GitHub release metadata requests are rate-limited, set
74+
`GITHUB_TOKEN` or use `FRP_COMPAT_BASELINE_VERSIONS`.
75+
76+
The compatibility floor is a support-policy decision, not a value that should
77+
change every release. Update `FRP_COMPAT_FLOOR_VERSION` only when the declared
78+
compatibility window changes.
79+
80+
## 4. Merge dev → master
3781

3882
Create a PR from `dev` to `master`:
3983

@@ -43,7 +87,7 @@ gh pr create --base master --head dev --title "bump version"
4387

4488
Wait for CI to pass, then merge using **merge commit** (not squash).
4589

46-
## 4. Tag the Release
90+
## 5. Tag the Release
4791

4892
```bash
4993
git checkout master
@@ -52,7 +96,7 @@ git tag -a vX.Y.Z -m "bump version"
5296
git push origin vX.Y.Z
5397
```
5498

55-
## 5. Trigger GoReleaser
99+
## 6. Trigger GoReleaser
56100

57101
Manually trigger the `goreleaser` workflow in GitHub Actions:
58102

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ go 1.25.0
55
require (
66
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
77
github.com/coreos/go-oidc/v3 v3.14.1
8-
github.com/fatedier/golib v0.6.0
8+
github.com/fatedier/golib v0.7.0
99
github.com/google/uuid v1.6.0
1010
github.com/gorilla/mux v1.8.1
1111
github.com/gorilla/websocket v1.5.0
@@ -30,6 +30,7 @@ require (
3030
golang.org/x/net v0.52.0
3131
golang.org/x/oauth2 v0.28.0
3232
golang.org/x/sync v0.20.0
33+
golang.org/x/sys v0.42.0
3334
golang.org/x/time v0.10.0
3435
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
3536
gopkg.in/ini.v1 v1.67.0
@@ -68,7 +69,6 @@ require (
6869
github.com/wlynxg/anet v0.0.5 // indirect
6970
go.uber.org/automaxprocs v1.6.0 // indirect
7071
golang.org/x/mod v0.33.0 // indirect
71-
golang.org/x/sys v0.42.0 // indirect
7272
golang.org/x/text v0.35.0 // indirect
7373
golang.org/x/tools v0.42.0 // indirect
7474
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect

0 commit comments

Comments
 (0)