Files
xboard-node/internal/model/custom_outbound_validate.go

140 lines
4.1 KiB
Go

package model
import (
"fmt"
"strconv"
"strings"
)
func ValidateCustomOutbounds(outbounds []OutboundConfig) error {
return ValidateCustomOutboundsForKernel(outbounds, "", nil)
}
func ValidateCustomOutboundsForKernel(outbounds []OutboundConfig, kernelType string, additionalTags []string) error {
if len(outbounds) == 0 {
return nil
}
kernelType = strings.ToLower(strings.TrimSpace(kernelType))
allowedProtocols := map[string]struct{}{}
if kernelType != "" {
support, ok := OutboundSupportMatrix()[kernelType]
if !ok {
return fmt.Errorf("unsupported kernel type %q", kernelType)
}
for _, protocol := range support.Protocols {
allowedProtocols[strings.ToLower(protocol)] = struct{}{}
}
}
seen := make(map[string]struct{}, len(outbounds))
availableTags := make(map[string]struct{}, len(outbounds)+len(additionalTags)+2)
availableTags["direct"] = struct{}{}
availableTags["block"] = struct{}{}
for _, tag := range additionalTags {
tag = strings.ToLower(strings.TrimSpace(tag))
if tag != "" {
availableTags[tag] = struct{}{}
}
}
for i, outbound := range outbounds {
tag := strings.TrimSpace(outbound.Tag)
protocol := strings.ToLower(strings.TrimSpace(outbound.Protocol))
if tag == "" {
return fmt.Errorf("custom_outbounds[%d].tag is required", i)
}
if protocol == "" {
return fmt.Errorf("custom_outbounds[%d].protocol is required", i)
}
if kernelType != "" {
if _, ok := allowedProtocols[protocol]; !ok {
return fmt.Errorf("custom_outbounds[%d].protocol %q is not supported by kernel %q", i, outbound.Protocol, kernelType)
}
}
if err := validateOutboundSettings(i, outbound.Settings); err != nil {
return err
}
key := strings.ToLower(tag)
if _, exists := seen[key]; exists {
return fmt.Errorf("custom_outbounds[%d].tag duplicates %q", i, tag)
}
seen[key] = struct{}{}
availableTags[key] = struct{}{}
}
for i, outbound := range outbounds {
if outbound.ProxyTag == "" {
continue
}
proxyTag := strings.ToLower(strings.TrimSpace(outbound.ProxyTag))
if proxyTag == "" {
return fmt.Errorf("custom_outbounds[%d].proxy_tag must not be blank", i)
}
if proxyTag == strings.ToLower(strings.TrimSpace(outbound.Tag)) {
return fmt.Errorf("custom_outbounds[%d].proxy_tag must not reference itself", i)
}
if _, exists := availableTags[proxyTag]; !exists {
return fmt.Errorf("custom_outbounds[%d].proxy_tag references unknown outbound %q", i, outbound.ProxyTag)
}
}
return nil
}
func validateOutboundSettings(index int, settings map[string]any) error {
if len(settings) == 0 {
return fmt.Errorf("custom_outbounds[%d].settings is required", index)
}
for _, reservedKey := range []string{"tag", "protocol", "proxy_tag", "proxyTag"} {
if _, exists := settings[reservedKey]; exists {
return fmt.Errorf("custom_outbounds[%d].settings.%s is reserved", index, reservedKey)
}
}
for _, field := range []string{"server", "uuid", "password", "method", "cipher", "private_key", "secret_key", "privateKey", "secretKey"} {
if value, ok := settings[field]; ok {
if strings.TrimSpace(fmt.Sprintf("%v", value)) == "" {
return fmt.Errorf("custom_outbounds[%d].settings.%s must not be blank", index, field)
}
}
}
if value, ok := settings["server_port"]; ok {
if _, err := asPort(value); err != nil {
return fmt.Errorf("custom_outbounds[%d].settings.server_port %w", index, err)
}
}
if value, ok := settings["serverPort"]; ok {
if _, err := asPort(value); err != nil {
return fmt.Errorf("custom_outbounds[%d].settings.serverPort %w", index, err)
}
}
return nil
}
func asPort(value any) (int, error) {
switch v := value.(type) {
case int:
if v < 1 || v > 65535 {
return 0, fmt.Errorf("must be between 1 and 65535")
}
return v, nil
case int32:
return asPort(int(v))
case int64:
return asPort(int(v))
case float64:
if v != float64(int(v)) {
return 0, fmt.Errorf("must be an integer")
}
return asPort(int(v))
case string:
parsed, err := strconv.Atoi(strings.TrimSpace(v))
if err != nil {
return 0, fmt.Errorf("must be an integer")
}
return asPort(parsed)
default:
return 0, fmt.Errorf("must be an integer")
}
}