fix(rpc): cap concurrent IO streams per user and per server

GHSA-jg62-j5h6-8mpq: the terminal and file-manager endpoints created unbounded
IO streams; an authenticated member could open thousands, each spawning
goroutines, a 1MiB buffer and an agent-side PTY, exhausting dashboard and agent
resources. CreateStream now enforces a per-user (20) and per-server (40) cap in
the existing ioStreamMutex critical section, using the stream map as the single
source of truth. Dashboard-internal streams (uid==0: NAT, server transfer, MCP
transfer) skip the per-user cap but still count per-server. Adds caps,
exemption, slot-release and no-leak regression tests.
This commit is contained in:
naiba
2026-06-05 02:42:38 +00:00
parent fb5c37e983
commit 36240f5888
7 changed files with 241 additions and 9 deletions
+3 -1
View File
@@ -49,7 +49,9 @@ func createFM(c *gin.Context) (*model.CreateFMResponse, error) {
return nil, err
}
rpc.NezhaHandlerSingleton.CreateStream(streamId, getUid(c), server.ID)
if err := rpc.NezhaHandlerSingleton.CreateStream(streamId, getUid(c), server.ID); err != nil {
return nil, err
}
fmData, _ := json.Marshal(&model.TaskFM{
StreamID: streamId,
+3 -1
View File
@@ -962,7 +962,9 @@ func openFsTransferStream(ctx context.Context, serverID uint64, req *model.FsTra
}
req.StreamID = streamId
rpc.NezhaHandlerSingleton.CreateStreamWithPurpose(streamId, 0, serverID, rpc.PurposeMCPTransfer)
if err := rpc.NezhaHandlerSingleton.CreateStreamWithPurpose(streamId, 0, serverID, rpc.PurposeMCPTransfer); err != nil {
return nil, func() {}, err
}
cleanup := func() { _ = rpc.NezhaHandlerSingleton.CloseStream(streamId) }
body, err := json.Marshal(req)
+3 -1
View File
@@ -47,7 +47,9 @@ func createTerminal(c *gin.Context) (*model.CreateTerminalResponse, error) {
return nil, err
}
rpc.NezhaHandlerSingleton.CreateStream(streamId, getUid(c), server.ID)
if err := rpc.NezhaHandlerSingleton.CreateStream(streamId, getUid(c), server.ID); err != nil {
return nil, err
}
terminalData, _ := json.Marshal(&model.TerminalTask{
StreamID: streamId,
+5 -1
View File
@@ -217,7 +217,11 @@ func ServeNAT(w http.ResponseWriter, r *http.Request, natConfig *model.NAT) {
// IS required though — the receiving agent must prove it is the server the
// NAT config addressed, otherwise any agent that snoops the streamId can
// answer NAT traffic on behalf of an unrelated host.
rpcService.NezhaHandlerSingleton.CreateStream(streamId, 0, server.ID)
if err := rpcService.NezhaHandlerSingleton.CreateStream(streamId, 0, server.ID); err != nil {
w.WriteHeader(http.StatusTooManyRequests)
w.Write(fmt.Appendf(nil, "stream limit: %v", err))
return
}
defer rpcService.NezhaHandlerSingleton.CloseStream(streamId)
taskData, err := json.Marshal(model.TaskNAT{