From 77dbd719edf8f3afc07408adf4bdba8da6d190b4 Mon Sep 17 00:00:00 2001 From: sagitchu Date: Sun, 15 Feb 2026 11:22:50 +0800 Subject: [PATCH] fix(backend): enforce port range in federation runtime commands The federationRuntimeCommand handler forwarded AddService/UpdateService commands from consumers to provider nodes without validating that the port in the service payload falls within the share's allowed port range. This allowed consumers to use any port on shared nodes, bypassing the provider's port_range_start/port_range_end restrictions. Add port extraction and validation in federationRuntimeCommand for service commands, rejecting requests with ports outside the allowed range with a 403 error. Co-Authored-By: Claude Opus 4.6 --- .../internal/http/handler/federation.go | 57 +++++++++ .../federation_dual_panel_contract_test.go | 109 ++++++++++++++++++ 2 files changed, 166 insertions(+) diff --git a/go-backend/internal/http/handler/federation.go b/go-backend/internal/http/handler/federation.go index 9a06513..402a00f 100644 --- a/go-backend/internal/http/handler/federation.go +++ b/go-backend/internal/http/handler/federation.go @@ -7,6 +7,7 @@ import ( "net" "net/http" "sort" + "strconv" "strings" "sync" "time" @@ -1232,6 +1233,13 @@ func (h *Handler) federationRuntimeCommand(w http.ResponseWriter, r *http.Reques return } + if isFederationServiceCommand(cmd) { + if err := validateFederationCommandPorts(share, req.Data); err != nil { + response.WriteJSON(w, response.Err(403, err.Error())) + return + } + } + res, err := h.sendNodeCommand(share.NodeID, cmd, req.Data, false, false) if err != nil { response.WriteJSON(w, response.ErrDefault(err.Error())) @@ -1249,6 +1257,55 @@ func isFederationRuntimeCommandAllowed(commandType string) bool { } } +func isFederationServiceCommand(commandType string) bool { + switch strings.ToLower(strings.TrimSpace(commandType)) { + case "addservice", "updateservice": + return true + default: + return false + } +} + +func validateFederationCommandPorts(share *sqlite.PeerShare, data interface{}) error { + if share == nil || (share.PortRangeStart <= 0 && share.PortRangeEnd <= 0) { + return nil + } + dataMap, ok := data.(map[string]interface{}) + if !ok { + return nil + } + services, ok := dataMap["services"] + if !ok { + return nil + } + serviceList, ok := services.([]interface{}) + if !ok { + return nil + } + for _, svc := range serviceList { + svcMap, ok := svc.(map[string]interface{}) + if !ok { + continue + } + addr, ok := svcMap["addr"].(string) + if !ok || addr == "" { + continue + } + _, portStr, err := net.SplitHostPort(addr) + if err != nil { + continue + } + port, err := strconv.Atoi(portStr) + if err != nil || port <= 0 { + continue + } + if port < share.PortRangeStart || port > share.PortRangeEnd { + return fmt.Errorf("port %d out of allowed range %d-%d", port, share.PortRangeStart, share.PortRangeEnd) + } + } + return nil +} + func (h *Handler) pickPeerSharePort(share *sqlite.PeerShare, requestedPort int) (int, error) { if share == nil { return 0, fmt.Errorf("share not found") diff --git a/go-backend/tests/contract/federation_dual_panel_contract_test.go b/go-backend/tests/contract/federation_dual_panel_contract_test.go index 784ad29..f5e3d49 100644 --- a/go-backend/tests/contract/federation_dual_panel_contract_test.go +++ b/go-backend/tests/contract/federation_dual_panel_contract_test.go @@ -688,3 +688,112 @@ func valueAsBool(v interface{}) bool { return false } } + +func TestFederationRuntimeCommandPortRangeEnforcement(t *testing.T) { + providerSecret := "provider-portrange-jwt" + providerRouter, providerRepo := setupContractRouter(t, providerSecret) + providerServer := httptest.NewServer(providerRouter) + defer providerServer.Close() + + now := time.Now().UnixMilli() + providerNodeID := insertContractNode(t, providerRepo, "provider-portrange-node", "198.51.100.50", "44000-44010", "provider-portrange-secret", 1) + + insertPeerShare(t, providerRepo, &sqlite.PeerShare{ + Name: "portrange-share", + NodeID: providerNodeID, + Token: "share-portrange-token", + PortRangeStart: 44000, + PortRangeEnd: 44010, + IsActive: 1, + CreatedTime: now, + UpdatedTime: now, + }) + + stopNode := startMockNodeSession(t, providerServer.URL, "provider-portrange-secret") + defer stopNode() + + sendCommand := func(token string, cmdType string, data interface{}) *httptest.ResponseRecorder { + payload := map[string]interface{}{ + "commandType": cmdType, + "data": data, + } + body, err := json.Marshal(payload) + if err != nil { + t.Fatalf("marshal command payload: %v", err) + } + req := httptest.NewRequest(http.MethodPost, "/api/v1/federation/runtime/command", bytes.NewReader(body)) + req.Header.Set("Authorization", "Bearer "+token) + req.Header.Set("Content-Type", "application/json") + res := httptest.NewRecorder() + providerRouter.ServeHTTP(res, req) + return res + } + + // Test: AddService with port OUTSIDE allowed range should be rejected + outOfRangeData := map[string]interface{}{ + "services": []map[string]interface{}{ + { + "name": "test_service_tcp", + "addr": "[::]:55555", + "handler": map[string]interface{}{ + "type": "tcp", + }, + "listener": map[string]interface{}{ + "type": "tcp", + }, + }, + }, + } + res := sendCommand("share-portrange-token", "AddService", outOfRangeData) + var out response.R + if err := json.NewDecoder(res.Body).Decode(&out); err != nil { + t.Fatalf("decode response: %v", err) + } + if out.Code != 403 { + t.Fatalf("expected code 403 for out-of-range port, got %d (msg: %s)", out.Code, out.Msg) + } + + // Test: UpdateService with port OUTSIDE allowed range should be rejected + res = sendCommand("share-portrange-token", "UpdateService", outOfRangeData) + out = response.R{} + if err := json.NewDecoder(res.Body).Decode(&out); err != nil { + t.Fatalf("decode response: %v", err) + } + if out.Code != 403 { + t.Fatalf("expected code 403 for out-of-range UpdateService, got %d (msg: %s)", out.Code, out.Msg) + } + + // Test: AddService with port INSIDE allowed range should succeed + inRangeData := map[string]interface{}{ + "services": []map[string]interface{}{ + { + "name": "test_service_ok_tcp", + "addr": "[::]:44005", + "handler": map[string]interface{}{ + "type": "tcp", + }, + "listener": map[string]interface{}{ + "type": "tcp", + }, + }, + }, + } + res = sendCommand("share-portrange-token", "AddService", inRangeData) + out = response.R{} + if err := json.NewDecoder(res.Body).Decode(&out); err != nil { + t.Fatalf("decode response: %v", err) + } + if out.Code != 0 { + t.Fatalf("expected code 0 for in-range port, got %d (msg: %s)", out.Code, out.Msg) + } + + // Test: Non-service commands should pass through without port validation + res = sendCommand("share-portrange-token", "reload", nil) + out = response.R{} + if err := json.NewDecoder(res.Body).Decode(&out); err != nil { + t.Fatalf("decode response: %v", err) + } + if out.Code != 0 { + t.Fatalf("expected code 0 for reload command, got %d (msg: %s)", out.Code, out.Msg) + } +}