mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-11-15 09:04:23 +08:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 961c06655a | |||
| 8dc31383e0 | |||
| 184ac3c3e5 | |||
| 77dbd719ed | |||
| 04ce125416 |
@@ -7,6 +7,7 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -543,6 +544,28 @@ func (h *Handler) federationRemoteUsageList(w http.ResponseWriter, r *http.Reque
|
||||
response.WriteJSON(w, response.OK(items))
|
||||
}
|
||||
|
||||
func remoteNodePortRange(node *nodeRecord) (int, int) {
|
||||
if node == nil || node.IsRemote != 1 || node.RemoteConfig == "" {
|
||||
return 0, 0
|
||||
}
|
||||
_, _, _, _, portRangeStart, portRangeEnd := parseRemoteShareUsageConfig(node.RemoteConfig)
|
||||
return portRangeStart, portRangeEnd
|
||||
}
|
||||
|
||||
func validateRemoteNodePort(node *nodeRecord, port int) error {
|
||||
if node == nil || node.IsRemote != 1 || port <= 0 {
|
||||
return nil
|
||||
}
|
||||
start, end := remoteNodePortRange(node)
|
||||
if start <= 0 || end <= 0 {
|
||||
return nil
|
||||
}
|
||||
if port < start || port > end {
|
||||
return fmt.Errorf("远程节点端口 %d 超出允许范围 %d-%d", port, start, end)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseRemoteShareUsageConfig(raw string) (int64, int64, int64, int64, int, int) {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
@@ -779,9 +802,6 @@ func (h *Handler) federationTunnelCreate(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
tunnelType := 1
|
||||
if strings.ToLower(req.Protocol) == "udp" {
|
||||
tunnelType = 2
|
||||
}
|
||||
|
||||
tx, err := h.repo.DB().Begin()
|
||||
if err != nil {
|
||||
@@ -982,6 +1002,13 @@ func (h *Handler) federationRuntimeApplyRole(w http.ResponseWriter, r *http.Requ
|
||||
return
|
||||
}
|
||||
|
||||
if share.PortRangeStart > 0 && share.PortRangeEnd > 0 && runtime.Port > 0 {
|
||||
if runtime.Port < share.PortRangeStart || runtime.Port > share.PortRangeEnd {
|
||||
response.WriteJSON(w, response.Err(403, fmt.Sprintf("port %d out of allowed range %d-%d", runtime.Port, share.PortRangeStart, share.PortRangeEnd)))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
node, err := h.getNodeRecord(share.NodeID)
|
||||
if err != nil {
|
||||
response.WriteJSON(w, response.ErrDefault(err.Error()))
|
||||
@@ -1232,6 +1259,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 +1283,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
|
||||
}
|
||||
|
||||
if services, ok := dataMap["services"]; ok {
|
||||
serviceList, ok := services.([]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid services format")
|
||||
}
|
||||
for _, svc := range serviceList {
|
||||
svcMap, ok := svc.(map[string]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid service entry format")
|
||||
}
|
||||
addr, ok := svcMap["addr"].(string)
|
||||
if !ok || addr == "" {
|
||||
continue
|
||||
}
|
||||
_, portStr, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid service address: %s", addr)
|
||||
}
|
||||
port, err := strconv.Atoi(portStr)
|
||||
if err != nil || port <= 0 {
|
||||
return fmt.Errorf("invalid port in service address: %s", addr)
|
||||
}
|
||||
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")
|
||||
|
||||
@@ -545,6 +545,11 @@ func (h *Handler) tunnelCreate(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
if targetPort > 0 && targetAddr != "" {
|
||||
inNodeRec := runtimeState.Nodes[firstNodeID]
|
||||
if err := validateRemoteNodePort(inNodeRec, targetPort); err != nil {
|
||||
response.WriteJSON(w, response.ErrDefault(err.Error()))
|
||||
return
|
||||
}
|
||||
domainCfg, _ := h.repo.GetConfigByName("panel_domain")
|
||||
localDomain := ""
|
||||
if domainCfg != nil {
|
||||
@@ -569,12 +574,10 @@ func (h *Handler) tunnelCreate(w http.ResponseWriter, r *http.Request) {
|
||||
runtimeState.TunnelID = tunnelID
|
||||
var federationBindings []sqlite.FederationTunnelBinding
|
||||
var federationReleaseRefs []federationRuntimeReleaseRef
|
||||
if typeVal == 2 {
|
||||
federationBindings, federationReleaseRefs, err = h.applyFederationRuntime(runtimeState)
|
||||
if err != nil {
|
||||
response.WriteJSON(w, response.ErrDefault(err.Error()))
|
||||
return
|
||||
}
|
||||
federationBindings, federationReleaseRefs, err = h.applyFederationRuntime(runtimeState)
|
||||
if err != nil {
|
||||
response.WriteJSON(w, response.ErrDefault(err.Error()))
|
||||
return
|
||||
}
|
||||
applyTunnelPortsToRequest(req, runtimeState)
|
||||
if err := replaceTunnelChainsTx(tx, tunnelID, req); err != nil {
|
||||
@@ -693,12 +696,10 @@ func (h *Handler) tunnelUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
var federationBindings []sqlite.FederationTunnelBinding
|
||||
var federationReleaseRefs []federationRuntimeReleaseRef
|
||||
if typeVal == 2 {
|
||||
federationBindings, federationReleaseRefs, err = h.applyFederationRuntime(runtimeState)
|
||||
if err != nil {
|
||||
response.WriteJSON(w, response.ErrDefault(err.Error()))
|
||||
return
|
||||
}
|
||||
federationBindings, federationReleaseRefs, err = h.applyFederationRuntime(runtimeState)
|
||||
if err != nil {
|
||||
response.WriteJSON(w, response.ErrDefault(err.Error()))
|
||||
return
|
||||
}
|
||||
applyTunnelPortsToRequest(req, runtimeState)
|
||||
|
||||
@@ -1109,6 +1110,17 @@ func (h *Handler) forwardCreate(w http.ResponseWriter, r *http.Request) {
|
||||
if port <= 0 {
|
||||
port = 10000
|
||||
}
|
||||
entryNodes, _ := h.tunnelEntryNodeIDs(tunnelID)
|
||||
for _, nodeID := range entryNodes {
|
||||
node, nodeErr := h.getNodeRecord(nodeID)
|
||||
if nodeErr != nil {
|
||||
continue
|
||||
}
|
||||
if err := validateRemoteNodePort(node, port); err != nil {
|
||||
response.WriteJSON(w, response.ErrDefault(err.Error()))
|
||||
return
|
||||
}
|
||||
}
|
||||
now := time.Now().UnixMilli()
|
||||
inx := nextIndex(h.repo.DB(), "forward")
|
||||
var userName string
|
||||
@@ -1130,7 +1142,6 @@ func (h *Handler) forwardCreate(w http.ResponseWriter, r *http.Request) {
|
||||
response.WriteJSON(w, response.Err(-2, err.Error()))
|
||||
return
|
||||
}
|
||||
entryNodes, _ := h.tunnelEntryNodeIDs(tunnelID)
|
||||
for _, nodeID := range entryNodes {
|
||||
_, _ = tx.Exec(`INSERT INTO forward_port(forward_id, node_id, port) VALUES(?, ?, ?)`, forwardID, nodeID, port)
|
||||
}
|
||||
@@ -1220,6 +1231,17 @@ func (h *Handler) forwardUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
port = h.pickTunnelPort(tunnelID)
|
||||
}
|
||||
}
|
||||
fwdEntryNodes, _ := h.tunnelEntryNodeIDs(tunnelID)
|
||||
for _, nodeID := range fwdEntryNodes {
|
||||
node, nodeErr := h.getNodeRecord(nodeID)
|
||||
if nodeErr != nil {
|
||||
continue
|
||||
}
|
||||
if err := validateRemoteNodePort(node, port); err != nil {
|
||||
response.WriteJSON(w, response.ErrDefault(err.Error()))
|
||||
return
|
||||
}
|
||||
}
|
||||
now := time.Now().UnixMilli()
|
||||
_, err = h.repo.DB().Exec(`
|
||||
UPDATE forward SET name = ?, tunnel_id = ?, remote_addr = ?, strategy = ?, updated_time = ? WHERE id = ?
|
||||
@@ -1544,6 +1566,22 @@ func (h *Handler) forwardBatchChangeTunnel(w http.ResponseWriter, r *http.Reques
|
||||
if p <= 0 {
|
||||
p = h.pickTunnelPort(req.TargetTunnelID)
|
||||
}
|
||||
bctEntryNodes, _ := h.tunnelEntryNodeIDs(req.TargetTunnelID)
|
||||
portRangeOk := true
|
||||
for _, nid := range bctEntryNodes {
|
||||
nd, ndErr := h.getNodeRecord(nid)
|
||||
if ndErr != nil {
|
||||
continue
|
||||
}
|
||||
if validateRemoteNodePort(nd, p) != nil {
|
||||
portRangeOk = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if !portRangeOk {
|
||||
fail++
|
||||
continue
|
||||
}
|
||||
if err := h.replaceForwardPorts(id, req.TargetTunnelID, p); err != nil {
|
||||
h.rollbackForwardMutation(forward, oldPorts)
|
||||
fail++
|
||||
@@ -2239,6 +2277,19 @@ func (h *Handler) prepareTunnelCreateState(tx *store.Tx, req map[string]interfac
|
||||
state.Nodes[nodeID] = node
|
||||
}
|
||||
|
||||
for _, outNode := range state.OutNodes {
|
||||
if err := validateRemoteNodePort(state.Nodes[outNode.NodeID], outNode.Port); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
for _, hop := range state.ChainHops {
|
||||
for _, chainNode := range hop {
|
||||
if err := validateRemoteNodePort(state.Nodes[chainNode.NodeID], chainNode.Port); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return state, nil
|
||||
}
|
||||
|
||||
@@ -2340,7 +2391,7 @@ func (h *Handler) federationLocalDomain() string {
|
||||
func (h *Handler) applyFederationRuntime(state *tunnelCreateState) ([]sqlite.FederationTunnelBinding, []federationRuntimeReleaseRef, error) {
|
||||
bindings := make([]sqlite.FederationTunnelBinding, 0)
|
||||
releaseRefs := make([]federationRuntimeReleaseRef, 0)
|
||||
if h == nil || state == nil || state.Type != 2 {
|
||||
if h == nil || state == nil {
|
||||
return bindings, releaseRefs, nil
|
||||
}
|
||||
fc := client.NewFederationClient()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user