Compare commits

...

3 Commits

Author SHA1 Message Date
sagit e5e22baf43 fix(federation): cleanup tunnels when unsharing federation node (#126)
When unsharing a federation node, tunnels created via federationTunnelCreate
were not cleaned up, allowing clients to continue using them. Added
cleanupFederationTunnels() to delete these tunnels and reload the node agent.
2026-02-15 11:04:53 +00:00
sagit 961c06655a fix(backend): enforce port range restrictions in federation mode (#125)
Added dual-layer port range enforcement for federation sharing:

Server-side (Provider):
- federationRuntimeApplyRole: validate runtime.Port against share range
- validateFederationCommandPorts: hardened against malformed JSON bypass
- New helpers: validateRemoteNodePort, remoteNodePortRange

Client-side (Consumer):
- prepareTunnelCreateState: pre-check ports for remote nodes
- tunnelCreate type=1: validate targetPort for remote entry
- forwardCreate/Update/BatchChangeTunnel: port range validation

Prevents consumers from using arbitrary ports outside provider's allowed range.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-02-15 10:42:39 +00:00
sagit 8dc31383e0 fix(backend): correct federation port forwarding tunnel type handling (#123)
- Fix Type 1 (port forwarding) tunnels to call applyFederationRuntime
  Previously only Type 2 tunnels applied federation runtime, causing
  port forwarding tunnels to not be properly configured in federation mode

- Remove incorrect UDP tunnel type override in federationTunnelCreate
  UDP tunnels were being incorrectly set to Type 2, which conflicted with
  the federation runtime logic that expects Type 1 for port forwarding

These fixes ensure all tunnel types are properly handled in federation mode
with correct runtime configuration applied.
2026-02-15 12:12:54 +08:00
2 changed files with 150 additions and 42 deletions
+85 -28
View File
@@ -299,13 +299,20 @@ func (h *Handler) federationShareDelete(w http.ResponseWriter, r *http.Request)
return
}
share, _ := h.repo.GetPeerShare(req.ID)
h.cleanupPeerShareRuntimes(req.ID)
h.cleanupFederationTunnels(req.ID)
if err := h.repo.DeletePeerShare(req.ID); err != nil {
response.WriteJSON(w, response.Err(-2, err.Error()))
return
}
if share != nil && h.wsServer != nil {
h.wsServer.SendCommand(share.NodeID, "reload", nil, time.Second*5)
}
response.WriteJSON(w, response.OKEmpty())
}
@@ -544,6 +551,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 == "" {
@@ -780,9 +809,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 {
@@ -983,6 +1009,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()))
@@ -1274,35 +1307,35 @@ func validateFederationCommandPorts(share *sqlite.PeerShare, data interface{}) e
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 services, ok := dataMap["services"]; ok {
serviceList, ok := services.([]interface{})
if !ok {
continue
return fmt.Errorf("invalid services format")
}
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)
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
}
@@ -1616,3 +1649,27 @@ func (h *Handler) cleanupPeerShareRuntimes(shareID int64) {
_ = h.repo.MarkPeerShareRuntimeReleased(runtime.ID, now)
}
}
func (h *Handler) cleanupFederationTunnels(shareID int64) {
if h == nil || h.repo == nil || shareID <= 0 {
return
}
namePrefix := fmt.Sprintf("Share-%d-Port-", shareID)
rows, err := h.repo.DB().Query(`SELECT id FROM tunnel WHERE name LIKE ?`, namePrefix+"%")
if err != nil {
return
}
defer rows.Close()
var tunnelIDs []int64
for rows.Next() {
var id int64
if err := rows.Scan(&id); err == nil {
tunnelIDs = append(tunnelIDs, id)
}
}
for _, tid := range tunnelIDs {
_ = h.deleteTunnelByID(tid)
}
}
+65 -14
View File
@@ -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()