package app import ( "fmt" "os" "path/filepath" "strings" transportcfg "selective-vpn-api/app/transportcfg" ) const ( transportSingBoxTemplateUnitName = "singbox@.service" transportSingBoxInstanceDropIn = "10-selective-vpn.conf" transportSingBoxTemplateMarker = "# SVPN_TEMPLATE=singbox-instance" transportSingBoxInstanceDropInTag = "# SVPN_INSTANCE_DROPIN=singbox" ) func transportSystemdSingBoxUsesTemplateInstance(client TransportClient, unit string) bool { if client.Kind != TransportClientSingBox { return false } u := strings.TrimSpace(unit) if u == "" || !strings.HasSuffix(strings.ToLower(u), ".service") { return false } at := strings.IndexByte(u, '@') return at > 0 } func transportSystemdTemplateUnitForInstance(unit string) string { u := strings.TrimSpace(unit) if !strings.HasSuffix(strings.ToLower(u), ".service") { return "" } at := strings.IndexByte(u, '@') if at <= 0 { return "" } return u[:at] + "@.service" } func transportSystemdUnitDropInDir(unit string) string { path := transportSystemdUnitPath(strings.TrimSpace(unit)) return path + ".d" } func transportSystemdUnitDropInPath(unit, fileName string) string { name := strings.TrimSpace(fileName) if name == "" { name = transportSingBoxInstanceDropIn } return filepath.Join(transportSystemdUnitDropInDir(unit), name) } func writeTransportSystemdDropInFile(unit, fileName, content string) error { path := transportSystemdUnitDropInPath(unit, fileName) if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { return err } tmp := path + ".tmp" if err := os.WriteFile(tmp, []byte(content), 0o644); err != nil { return err } return os.Rename(tmp, path) } func writeTransportSingBoxTemplateArtifacts( client TransportClient, in transportSystemdProvisionInputs, ) error { templateUnit := transportSystemdTemplateUnitForInstance(in.unit) if templateUnit == "" { templateUnit = transportSingBoxTemplateUnitName } if !validSystemdUnitName(templateUnit) { return fmt.Errorf("invalid template unit: %s", templateUnit) } templateTuning := transportSystemdServiceTuningFromConfig(nil, "") templateHardening := transportSystemdHardeningFromConfig(nil, "") templateContent := renderTransportSystemdUnit( TransportClient{ID: "%i", Kind: TransportClientSingBox}, templateUnit, "/bin/true", false, "", templateTuning, templateHardening, ) templateContent = transportSingBoxTemplateMarker + "\n" + templateContent if err := writeTransportSystemdUnitFile(templateUnit, templateContent); err != nil { return err } netnsEnabled := transportNetnsEnabled(client) netnsName := "" if netnsEnabled { netnsName = transportNetnsName(client) } configPath := transportSingBoxConfigPath(client) dropIn := renderTransportSingBoxInstanceDropIn(client, in, configPath, netnsEnabled, netnsName) return writeTransportSystemdDropInFile(in.unit, transportSingBoxInstanceDropIn, dropIn) } func renderTransportSingBoxInstanceDropIn( client TransportClient, in transportSystemdProvisionInputs, configPath string, netnsEnabled bool, netnsName string, ) string { b := strings.Builder{} b.WriteString(transportSingBoxInstanceDropInTag + "\n") b.WriteString("[Unit]\n") b.WriteString("StartLimitIntervalSec=" + fmt.Sprintf("%d", in.primaryTuning.StartLimitIntervalSec) + "\n") b.WriteString("StartLimitBurst=" + fmt.Sprintf("%d", in.primaryTuning.StartLimitBurst) + "\n") b.WriteString("\n[Service]\n") b.WriteString("WorkingDirectory=" + stateDir + "\n") b.WriteString("Restart=" + in.primaryTuning.RestartPolicy + "\n") b.WriteString("RestartSec=" + fmt.Sprintf("%d", in.primaryTuning.RestartSec) + "\n") b.WriteString("Environment=SVPN_TRANSPORT_ID=" + strings.TrimSpace(client.ID) + "\n") b.WriteString("Environment=SVPN_TRANSPORT_KIND=" + strings.TrimSpace(string(client.Kind)) + "\n") if strings.TrimSpace(configPath) != "" { b.WriteString("Environment=SVPN_CONFIG_PATH=" + strings.TrimSpace(configPath) + "\n") } if netnsEnabled { b.WriteString("Environment=SVPN_NETNS_ENABLED=1\n") if strings.TrimSpace(netnsName) != "" { b.WriteString("Environment=SVPN_NETNS_NAME=" + strings.TrimSpace(netnsName) + "\n") } } else { b.WriteString("Environment=SVPN_NETNS_ENABLED=0\n") } b.WriteString("ExecStart=\n") b.WriteString("ExecStart=" + transportcfg.SystemdShellExec(in.primaryCmd, shellQuoteArg) + "\n") b.WriteString("ExecStop=\n") b.WriteString("ExecStop=/bin/kill -TERM $MAINPID\n") b.WriteString("TimeoutStartSec=" + fmt.Sprintf("%d", in.primaryTuning.TimeoutStartSec) + "\n") b.WriteString("TimeoutStopSec=" + fmt.Sprintf("%d", in.primaryTuning.TimeoutStopSec) + "\n") if in.primaryTuning.WatchdogSec > 0 { b.WriteString("WatchdogSec=" + fmt.Sprintf("%d", in.primaryTuning.WatchdogSec) + "\n") b.WriteString("NotifyAccess=main\n") } if in.primaryHardening.Enabled { b.WriteString("NoNewPrivileges=" + transportSystemdBool(in.primaryHardening.NoNewPrivileges) + "\n") b.WriteString("PrivateTmp=" + transportSystemdBool(in.primaryHardening.PrivateTmp) + "\n") b.WriteString("ProtectSystem=" + in.primaryHardening.ProtectSystem + "\n") b.WriteString("ProtectHome=" + in.primaryHardening.ProtectHome + "\n") b.WriteString("ProtectControlGroups=" + transportSystemdBool(in.primaryHardening.ProtectControlGroups) + "\n") b.WriteString("ProtectKernelModules=" + transportSystemdBool(in.primaryHardening.ProtectKernelModules) + "\n") b.WriteString("ProtectKernelTunables=" + transportSystemdBool(in.primaryHardening.ProtectKernelTunables) + "\n") b.WriteString("RestrictSUIDSGID=" + transportSystemdBool(in.primaryHardening.RestrictSUIDSGID) + "\n") b.WriteString("LockPersonality=" + transportSystemdBool(in.primaryHardening.LockPersonality) + "\n") if in.primaryHardening.PrivateDevices { b.WriteString("PrivateDevices=yes\n") } b.WriteString("UMask=" + in.primaryHardening.UMask + "\n") } return b.String() } func transportSystemdBool(v bool) string { if v { return "yes" } return "no" }