Files
ClusterCanvas/service/internal/auth/network.go
T
Squid 2605c3b346 Add users and network settings tabs with admin protections.
List and delete accounts with last-admin and Administrators-group guards, and expose editable network settings via the configuration UI.
2026-07-18 15:18:01 +02:00

136 lines
3.6 KiB
Go

package auth
import (
"fmt"
"net"
"regexp"
"strings"
"codeberg.org/SquidSE/ClusterCanvas/service/internal/settings"
)
var hostnamePattern = regexp.MustCompile(`^(?i)([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}$|^localhost$`)
// ValidateHostname checks a public hostname (empty is allowed for local/dev).
func ValidateHostname(hostname string) error {
trimmed := strings.TrimSpace(hostname)
if trimmed == "" {
return nil
}
if strings.Contains(trimmed, "/") || strings.Contains(trimmed, ":") || strings.Contains(trimmed, " ") {
return fmt.Errorf("public_hostname must be a bare DNS name without scheme, port, or path")
}
if !hostnamePattern.MatchString(trimmed) {
return fmt.Errorf("public_hostname is not a valid DNS hostname")
}
return nil
}
// HostMatches reports whether requestHost matches the configured public hostname.
// An empty / whitespace public hostname means any host is allowed (local/dev).
func HostMatches(requestHost string, publicHostname string) bool {
wanted := strings.TrimSpace(publicHostname)
if wanted == "" {
return true
}
host := strings.TrimSpace(requestHost)
if parsedHost, _, err := net.SplitHostPort(host); err == nil {
host = parsedHost
}
return strings.EqualFold(host, wanted)
}
// IPAllowed evaluates whitelist/blacklist rules against clientIP.
func IPAllowed(clientIP string, network settings.NetworkSettings) (bool, string) {
network = settings.EffectiveNetwork(network)
parsedIP := net.ParseIP(clientIP)
if parsedIP == nil {
return false, "unable to parse client IP"
}
switch network.AccessMode {
case settings.AccessModeOpen, "":
return true, ""
case settings.AccessModeWhitelist:
if matchesAnyRule(parsedIP, network.Rules) {
return true, ""
}
return false, "client IP is not on the whitelist"
case settings.AccessModeBlacklist:
if matchesAnyRule(parsedIP, network.Rules) {
return false, "client IP matches a blacklist rule"
}
return true, ""
default:
return false, "invalid access mode"
}
}
// WouldLockOut estimates whether rules would block clientIP.
func WouldLockOut(clientIP string, network settings.NetworkSettings) (bool, string) {
allowed, reason := IPAllowed(clientIP, network)
if allowed {
return false, ""
}
return true, reason
}
func matchesAnyRule(ip net.IP, rules []string) bool {
for _, rule := range rules {
trimmed := strings.TrimSpace(rule)
if trimmed == "" {
continue
}
if strings.Contains(trimmed, "/") {
_, network, err := net.ParseCIDR(trimmed)
if err != nil {
continue
}
if network.Contains(ip) {
return true
}
continue
}
ruleIP := net.ParseIP(trimmed)
if ruleIP != nil && ruleIP.Equal(ip) {
return true
}
}
return false
}
// ValidateListenAddress checks a bind address (IP or hostname).
func ValidateListenAddress(address string) error {
trimmed := strings.TrimSpace(address)
if trimmed == "" {
return fmt.Errorf("listen_address is required")
}
if trimmed == "0.0.0.0" || trimmed == "::" || trimmed == "localhost" {
return nil
}
if net.ParseIP(trimmed) != nil {
return nil
}
return fmt.Errorf("listen_address must be an IP address or 0.0.0.0")
}
// ValidateAccessRules validates CIDR/IP entries.
func ValidateAccessRules(rules []string) error {
for _, rule := range rules {
trimmed := strings.TrimSpace(rule)
if trimmed == "" {
continue
}
if strings.Contains(trimmed, "/") {
if _, _, err := net.ParseCIDR(trimmed); err != nil {
return fmt.Errorf("invalid CIDR rule %q", trimmed)
}
continue
}
if net.ParseIP(trimmed) == nil {
return fmt.Errorf("invalid IP rule %q", trimmed)
}
}
return nil
}