fix
This commit is contained in:
+81
-30
@@ -8,6 +8,7 @@ import (
|
||||
"log"
|
||||
"net"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/iceBear67/redapricot/client/wire"
|
||||
@@ -64,17 +65,30 @@ func clampWindow(w int) int {
|
||||
return w
|
||||
}
|
||||
|
||||
// session is an established redapricot session: the frame transport plus what
|
||||
// was negotiated during establishment.
|
||||
type session struct {
|
||||
fc *wire.FramedConn
|
||||
peerWnd int // hub's advertised per-stream receive window
|
||||
heartbeat bool // hub accepted mux-level PING/PONG on worker conns
|
||||
}
|
||||
|
||||
// dialSession opens a TCP connection, performs the Intent-17 handshake, the
|
||||
// Phase-A rekey, and reads SessionReady, returning an established frame conn
|
||||
// and the hub's advertised per-stream receive window. Per-stream flow control
|
||||
// is mandatory: a hub that does not echo the STREAM_FC flag is rejected.
|
||||
func (c *Client) dialSession(magic byte) (fc *wire.FramedConn, peerWnd int, err error) {
|
||||
conn, err := net.DialTimeout("tcp", c.cfg.Server, 10*time.Second)
|
||||
// Phase-A rekey, and reads SessionReady. Per-stream flow control is mandatory:
|
||||
// a hub that does not echo the STREAM_FC flag is rejected.
|
||||
//
|
||||
// The whole exchange is bounded by HandshakeTimeout. A hub that accepts the
|
||||
// socket but never answers (wedged event loop, a load balancer accepting on its
|
||||
// behalf) must fail fast rather than park the caller forever.
|
||||
func (c *Client) dialSession(magic byte) (sess *session, err error) {
|
||||
conn, err := net.DialTimeout("tcp", c.cfg.Server, HandshakeTimeout)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
return nil, err
|
||||
}
|
||||
if tcp, ok := conn.(*net.TCPConn); ok {
|
||||
_ = tcp.SetNoDelay(true)
|
||||
_ = tcp.SetKeepAlive(true)
|
||||
_ = tcp.SetKeepAlivePeriod(TCPKeepAlivePeriod)
|
||||
}
|
||||
ok := false
|
||||
defer func() {
|
||||
@@ -82,15 +96,18 @@ func (c *Client) dialSession(magic byte) (fc *wire.FramedConn, peerWnd int, err
|
||||
_ = conn.Close()
|
||||
}
|
||||
}()
|
||||
if err := conn.SetDeadline(time.Now().Add(HandshakeTimeout)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 1. plaintext Minecraft Handshake, Intent 17, address = hex(SHA3-224(PSK)).
|
||||
hs := wire.BuildHandshake(ProtocolVersion, c.pskAddr, c.serverPort, IntentRedapricot)
|
||||
if _, err := conn.Write(hs); err != nil {
|
||||
return nil, 0, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 2. Phase-A ciphers derived from the PSK.
|
||||
fc = wire.NewFramedConn(conn,
|
||||
fc := wire.NewFramedConn(conn,
|
||||
wire.CipherFor(c.pskBytes, wire.DirS2C), // in: server -> client
|
||||
wire.CipherFor(c.pskBytes, wire.DirC2S), // out: client -> server
|
||||
)
|
||||
@@ -99,13 +116,14 @@ func (c *Client) dialSession(magic byte) (fc *wire.FramedConn, peerWnd int, err
|
||||
// per-stream receive window.
|
||||
rnd := make([]byte, 16)
|
||||
if _, err := crand.Read(rnd); err != nil {
|
||||
return nil, 0, err
|
||||
return nil, err
|
||||
}
|
||||
ts := time.Now().UnixMilli()
|
||||
offered := FlagStreamFC | FlagWorkerHeartbeat
|
||||
rekeyMsg := wire.NewWriter().U8(magic).VarInt(len(rnd)).Bytes(rnd).I64(ts).
|
||||
VarInt(FlagStreamFC).VarInt(c.streamWnd).Out()
|
||||
VarInt(offered).VarInt(c.streamWnd).Out()
|
||||
if err := fc.WriteFrame(rekeyMsg); err != nil {
|
||||
return nil, 0, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 4. Switch to Phase-B ciphers: REKEY = Rand || Timestamp(I64 BE).
|
||||
@@ -123,22 +141,28 @@ func (c *Client) dialSession(magic byte) (fc *wire.FramedConn, peerWnd int, err
|
||||
// its per-stream receive window. Both are required.
|
||||
payload, err := fc.ReadFrame()
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
return nil, err
|
||||
}
|
||||
if len(payload) < 1 || payload[0] != CtlSessionReady {
|
||||
return nil, 0, fmt.Errorf("expected SessionReady, got %v", payload)
|
||||
return nil, fmt.Errorf("expected SessionReady, got %v", payload)
|
||||
}
|
||||
r := wire.NewReader(payload[1:])
|
||||
flags, ferr := r.VarInt()
|
||||
hubWnd, werr := r.VarInt()
|
||||
if ferr != nil || werr != nil || flags&FlagStreamFC == 0 || hubWnd <= 0 {
|
||||
return nil, 0, fmt.Errorf("hub did not accept per-stream flow control (unsupported hub version?)")
|
||||
return nil, fmt.Errorf("hub did not accept per-stream flow control (unsupported hub version?)")
|
||||
}
|
||||
if hubWnd > MaxStreamWindow {
|
||||
hubWnd = MaxStreamWindow
|
||||
}
|
||||
|
||||
// The session is live: drop the establishment deadline. From here on
|
||||
// liveness is the heartbeat's job (and WriteFrame bounds each write).
|
||||
if err := conn.SetDeadline(time.Time{}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ok = true
|
||||
return fc, hubWnd, nil
|
||||
return &session{fc: fc, peerWnd: hubWnd, heartbeat: flags&FlagWorkerHeartbeat != 0}, nil
|
||||
}
|
||||
|
||||
// Start establishes the control session and registers all patterns. It returns
|
||||
@@ -149,20 +173,30 @@ func (c *Client) Start(ctx context.Context) error {
|
||||
}
|
||||
|
||||
func (c *Client) connectControl(ctx context.Context) error {
|
||||
fc, _, err := c.dialSession(MagicControl)
|
||||
sess, err := c.dialSession(MagicControl)
|
||||
if err != nil {
|
||||
return fmt.Errorf("control connect: %w", err)
|
||||
}
|
||||
c.registerAll(fc)
|
||||
ctrl := &ctrlSession{fc: sess.fc}
|
||||
ctrl.lastPong.Store(time.Now().UnixMilli())
|
||||
c.registerAll(sess.fc)
|
||||
c.mu.Lock()
|
||||
c.ctrl = fc
|
||||
c.ctrl = sess.fc
|
||||
c.mu.Unlock()
|
||||
log.Printf("control session established with %s", c.cfg.Server)
|
||||
go c.serveControl(ctx, fc)
|
||||
go c.pingLoop(ctx, fc)
|
||||
go c.serveControl(ctx, ctrl)
|
||||
go c.pingLoop(ctx, ctrl)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ctrlSession tracks liveness for one control connection. A control session
|
||||
// whose path dies silently must be detected, otherwise the hub keeps routing
|
||||
// players to a session the client will never read from and nobody can connect.
|
||||
type ctrlSession struct {
|
||||
fc *wire.FramedConn
|
||||
lastPong atomic.Int64 // unix ms of the most recent Pong
|
||||
}
|
||||
|
||||
func (c *Client) registerAll(fc *wire.FramedConn) {
|
||||
for pattern := range c.mappings {
|
||||
msg := wire.NewWriter().U8(CtlRegister).String(pattern).Out()
|
||||
@@ -174,15 +208,15 @@ func (c *Client) registerAll(fc *wire.FramedConn) {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) serveControl(ctx context.Context, fc *wire.FramedConn) {
|
||||
func (c *Client) serveControl(ctx context.Context, ctrl *ctrlSession) {
|
||||
for {
|
||||
payload, err := fc.ReadFrame()
|
||||
payload, err := ctrl.fc.ReadFrame()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
c.dispatchControl(payload)
|
||||
c.dispatchControl(ctrl, payload)
|
||||
}
|
||||
_ = fc.Close()
|
||||
_ = ctrl.fc.Close()
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
@@ -200,7 +234,7 @@ func (c *Client) serveControl(ctx context.Context, fc *wire.FramedConn) {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) dispatchControl(payload []byte) {
|
||||
func (c *Client) dispatchControl(ctrl *ctrlSession, payload []byte) {
|
||||
r := wire.NewReader(payload)
|
||||
t, err := r.U8()
|
||||
if err != nil {
|
||||
@@ -223,22 +257,33 @@ func (c *Client) dispatchControl(payload []byte) {
|
||||
port, _ := r.U16()
|
||||
go c.handleControlRequest(cid, pattern, ip, int(port))
|
||||
case CtlPong:
|
||||
// ignore
|
||||
ctrl.lastPong.Store(time.Now().UnixMilli())
|
||||
default:
|
||||
log.Printf("control: unknown message type %d", t)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) pingLoop(ctx context.Context, fc *wire.FramedConn) {
|
||||
ticker := time.NewTicker(time.Duration(c.cfg.PingIntervalMs) * time.Millisecond)
|
||||
// pingLoop keeps the control session alive and, crucially, verifies that the
|
||||
// hub is still answering. A path that dies silently (no FIN/RST) would
|
||||
// otherwise leave the read loop parked forever: the client would believe it is
|
||||
// still registered while the hub routes players into the void.
|
||||
func (c *Client) pingLoop(ctx context.Context, ctrl *ctrlSession) {
|
||||
ticker := time.NewTicker(c.cfg.pingInterval())
|
||||
defer ticker.Stop()
|
||||
timeout := c.cfg.heartbeatTimeout()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
last := time.UnixMilli(ctrl.lastPong.Load())
|
||||
if time.Since(last) > timeout {
|
||||
log.Printf("control session silent for %s; dropping it to force a reconnect", time.Since(last).Round(time.Second))
|
||||
_ = ctrl.fc.Close() // unblocks serveControl, which reconnects
|
||||
return
|
||||
}
|
||||
msg := wire.NewWriter().U8(CtlPing).I64(time.Now().UnixMilli()).Out()
|
||||
if err := fc.WriteFrame(msg); err != nil {
|
||||
if err := ctrl.fc.WriteFrame(msg); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -260,10 +305,16 @@ func (c *Client) handleControlRequest(cid []byte, pattern, ip string, port int)
|
||||
return
|
||||
}
|
||||
st := newStream(wc, sid, cid, mapping, ip, port)
|
||||
// Register before SYN so inbound DATA can never race ahead of the table,
|
||||
// and start the pumps before the (bounded) SYN write so a failed or slow
|
||||
// SYN cannot strand a stream that nothing would ever tear down.
|
||||
wc.registerStream(sid, st)
|
||||
wc.sendSyn(sid, cid)
|
||||
go st.writeLoop()
|
||||
go st.run()
|
||||
if err := wc.sendSyn(sid, cid); err != nil {
|
||||
log.Printf("stream %d: SYN failed: %v", sid, err)
|
||||
st.teardown(false)
|
||||
}
|
||||
}
|
||||
|
||||
// WorkerConnCount reports the current number of open worker connections
|
||||
|
||||
Reference in New Issue
Block a user