feat(federation): add import API IP whitelist controls

This commit is contained in:
sagit
2026-02-10 06:40:46 +00:00
parent 00079ac7af
commit b205b47414
6 changed files with 347 additions and 12 deletions
@@ -4,6 +4,7 @@ import (
"database/sql"
"encoding/json"
"fmt"
"net"
"net/http"
"strings"
"time"
@@ -27,6 +28,7 @@ type createPeerShareRequest struct {
PortRangeStart int `json:"portRangeStart"`
PortRangeEnd int `json:"portRangeEnd"`
AllowedDomains string `json:"allowedDomains"`
AllowedIPs string `json:"allowedIps"`
}
type deletePeerShareRequest struct {
@@ -123,6 +125,12 @@ func (h *Handler) federationShareCreate(w http.ResponseWriter, r *http.Request)
return
}
allowedIPs, err := normalizePeerShareAllowedIPs(req.AllowedIPs)
if err != nil {
response.WriteJSON(w, response.ErrDefault(err.Error()))
return
}
node, err := h.repo.GetNodeByID(req.NodeID)
if err != nil {
response.WriteJSON(w, response.Err(-2, err.Error()))
@@ -152,6 +160,7 @@ func (h *Handler) federationShareCreate(w http.ResponseWriter, r *http.Request)
CreatedTime: now,
UpdatedTime: now,
AllowedDomains: req.AllowedDomains,
AllowedIPs: allowedIPs,
}
if err := h.repo.CreatePeerShare(share); err != nil {
@@ -294,6 +303,18 @@ func (h *Handler) authPeer(next http.HandlerFunc) http.HandlerFunc {
return
}
if strings.TrimSpace(share.AllowedIPs) != "" {
clientIP := resolvePeerClientIP(r)
if clientIP == nil {
response.WriteJSON(w, response.Err(403, "Unable to determine client IP"))
return
}
if !isPeerIPAllowed(clientIP, share.AllowedIPs) {
response.WriteJSON(w, response.Err(403, "IP not allowed"))
return
}
}
if share.AllowedDomains != "" {
clientDomain := r.Header.Get("X-Panel-Domain")
if clientDomain == "" {
@@ -863,3 +884,130 @@ func extractBearerToken(r *http.Request) string {
}
return ""
}
func normalizePeerShareAllowedIPs(raw string) (string, error) {
raw = strings.TrimSpace(raw)
if raw == "" {
return "", nil
}
parts := strings.Split(raw, ",")
normalized := make([]string, 0, len(parts))
seen := make(map[string]struct{}, len(parts))
for _, part := range parts {
item := strings.TrimSpace(part)
if item == "" {
continue
}
if strings.Contains(item, "/") {
_, network, err := net.ParseCIDR(item)
if err != nil {
return "", fmt.Errorf("Invalid allowed IP or CIDR: %s", item)
}
item = network.String()
} else {
ip := parseIPLiteral(item)
if ip == nil {
return "", fmt.Errorf("Invalid allowed IP or CIDR: %s", item)
}
item = ip.String()
}
if _, exists := seen[item]; exists {
continue
}
seen[item] = struct{}{}
normalized = append(normalized, item)
}
return strings.Join(normalized, ","), nil
}
func resolvePeerClientIP(r *http.Request) net.IP {
if r == nil {
return nil
}
remoteIP := parseIPLiteral(r.RemoteAddr)
if isTrustedProxyIP(remoteIP) {
if ip := parseForwardedFor(r.Header.Get("X-Forwarded-For")); ip != nil {
return ip
}
if ip := parseIPLiteral(r.Header.Get("X-Real-IP")); ip != nil {
return ip
}
}
return remoteIP
}
func parseForwardedFor(raw string) net.IP {
for _, part := range strings.Split(raw, ",") {
if ip := parseIPLiteral(part); ip != nil {
return ip
}
}
return nil
}
func parseIPLiteral(raw string) net.IP {
value := strings.Trim(strings.TrimSpace(raw), "\"")
if value == "" {
return nil
}
if ip := net.ParseIP(value); ip != nil {
return ip
}
host, _, err := net.SplitHostPort(value)
if err != nil {
return nil
}
host = strings.Trim(strings.TrimSpace(host), "[]")
if host == "" {
return nil
}
return net.ParseIP(host)
}
func isTrustedProxyIP(ip net.IP) bool {
if ip == nil {
return false
}
return ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast()
}
func isPeerIPAllowed(clientIP net.IP, whitelist string) bool {
if clientIP == nil {
return false
}
for _, part := range strings.Split(whitelist, ",") {
entry := strings.TrimSpace(part)
if entry == "" {
continue
}
if strings.Contains(entry, "/") {
_, network, err := net.ParseCIDR(entry)
if err != nil {
continue
}
if network.Contains(clientIP) {
return true
}
continue
}
allowedIP := parseIPLiteral(entry)
if allowedIP != nil && allowedIP.Equal(clientIP) {
return true
}
}
return false
}