140 lines
4.1 KiB
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")
|
|
}
|
|
}
|