package transportcfg import ( "fmt" "strings" ) const ( KindSingBox = "singbox" KindDNSTT = "dnstt" KindPhoenix = "phoenix" defaultSingBoxUnitTemplate = "singbox@.service" ) type Client struct { ID string Kind string Config map[string]any } func BackendUnit(client Client) string { kind := strings.ToLower(strings.TrimSpace(client.Kind)) unit := strings.TrimSpace(ConfigString(client.Config, "unit")) if kind == KindSingBox { return resolveSingBoxBackendUnit(client.ID, unit) } if unit != "" { return unit } return DefaultBackendUnit(kind) } func DefaultBackendUnit(kind string) string { switch strings.ToLower(strings.TrimSpace(kind)) { case KindSingBox: return defaultSingBoxUnitTemplate case KindDNSTT: return "dnstt-client.service" case KindPhoenix: return "phoenix.service" default: return "" } } func resolveSingBoxBackendUnit(clientID, configuredUnit string) string { unit := strings.TrimSpace(configuredUnit) if unit == "" { unit = defaultSingBoxUnitTemplate } if strings.HasSuffix(unit, "@.service") { instance := sanitizeSystemdInstanceID(clientID) if instance == "" { instance = "client" } return strings.TrimSuffix(unit, "@.service") + "@" + instance + ".service" } return unit } func sanitizeSystemdInstanceID(in string) string { s := strings.ToLower(strings.TrimSpace(in)) if s == "" { return "" } var b strings.Builder b.Grow(len(s)) lastDash := false for i := 0; i < len(s); i++ { ch := s[i] if (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9') || ch == '_' || ch == '.' { b.WriteByte(ch) lastDash = false continue } if ch == '-' { if lastDash { continue } b.WriteByte('-') lastDash = true continue } if !lastDash { b.WriteByte('-') lastDash = true } } return strings.Trim(b.String(), "-") } func DNSTTSSHTunnelEnabled(client Client) bool { if strings.ToLower(strings.TrimSpace(client.Kind)) != KindDNSTT { return false } return ConfigBool(client.Config, "ssh_tunnel") || ConfigBool(client.Config, "ssh_overlay") } func DNSTTSSHUnit(client Client) string { unit := strings.TrimSpace(ConfigString(client.Config, "ssh_unit")) if unit != "" { return unit } return "dnstt-ssh-tunnel.service" } func RuntimeMode(cfg map[string]any) string { mode := strings.ToLower(strings.TrimSpace(ConfigString(cfg, "runtime_mode"))) switch mode { case "", "exec", "external", "companion": return "exec" case "embedded": return "embedded" case "sidecar": return "sidecar" default: return mode } } func SystemdActionUnits(client Client, action string) ([]string, string, string) { unit := BackendUnit(client) if unit == "" { return nil, "TRANSPORT_BACKEND_UNIT_REQUIRED", "systemd unit is required" } if !DNSTTSSHTunnelEnabled(client) { return []string{unit}, "", "" } sshUnit := DNSTTSSHUnit(client) if strings.TrimSpace(sshUnit) == "" { return nil, "TRANSPORT_BACKEND_UNIT_REQUIRED", "dnstt ssh tunnel unit is required" } switch action { case "stop": return []string{unit, sshUnit}, "", "" default: return []string{sshUnit, unit}, "", "" } } func SystemdHealthUnits(client Client) ([]string, string, string) { unit := BackendUnit(client) if unit == "" { return nil, "TRANSPORT_BACKEND_UNIT_REQUIRED", "systemd unit is required" } units := []string{unit} if DNSTTSSHTunnelEnabled(client) { sshUnit := DNSTTSSHUnit(client) if strings.TrimSpace(sshUnit) == "" { return nil, "TRANSPORT_BACKEND_UNIT_REQUIRED", "dnstt ssh tunnel unit is required" } units = append(units, sshUnit) } return units, "", "" } func ConfigString(cfg map[string]any, key string) string { if cfg == nil { return "" } raw, ok := cfg[key] if !ok || raw == nil { return "" } switch v := raw.(type) { case string: return strings.TrimSpace(v) default: return strings.TrimSpace(fmt.Sprint(v)) } } func ConfigBool(cfg map[string]any, key string) bool { if cfg == nil { return false } raw, ok := cfg[key] if !ok || raw == nil { return false } switch v := raw.(type) { case bool: return v case string: s := strings.ToLower(strings.TrimSpace(v)) return s == "1" || s == "true" || s == "yes" || s == "on" case float64: return v != 0 case int: return v != 0 default: s := strings.ToLower(strings.TrimSpace(fmt.Sprint(v))) return s == "1" || s == "true" || s == "yes" || s == "on" } } func ConfigHasKey(cfg map[string]any, key string) bool { if cfg == nil { return false } raw, ok := cfg[key] if !ok || raw == nil { return false } if s, ok := raw.(string); ok { return strings.TrimSpace(s) != "" } return true }