Files
elmprodvpn/selective-vpn-api/app/transport_systemd_singbox_template_test.go

501 lines
16 KiB
Go

package app
import (
"fmt"
"os"
"path/filepath"
"strings"
"testing"
"time"
)
func TestTransportSystemdProvisionWritesSingBoxTemplateAndDropIn(t *testing.T) {
origRunner := transportRunCommand
origUnitsDir := transportSystemdUnitsDir
defer func() {
transportRunCommand = origRunner
transportSystemdUnitsDir = origUnitsDir
}()
tmpDir := t.TempDir()
transportSystemdUnitsDir = tmpDir
transportRunCommand = func(_ time.Duration, name string, args ...string) (string, string, int, error) {
cmd := name + " " + strings.Join(args, " ")
switch cmd {
case "systemctl daemon-reload":
return "", "", 0, nil
default:
return "", "", 0, nil
}
}
client := TransportClient{
ID: "sg-template",
Kind: TransportClientSingBox,
Config: map[string]any{
"runner": "systemd",
"unit": "singbox@sg-template.service",
"exec_start": "/usr/bin/sing-box run -c /tmp/sg-template.json",
},
}
backend := selectTransportBackend(client)
res := backend.Provision(client)
if !res.OK {
t.Fatalf("expected provision success, got %#v", res)
}
templatePath := filepath.Join(tmpDir, "singbox@.service")
templateData, err := os.ReadFile(templatePath)
if err != nil {
t.Fatalf("failed to read template unit: %v", err)
}
templateText := string(templateData)
if !strings.Contains(templateText, transportSingBoxTemplateMarker) {
t.Fatalf("template marker missing: %s", templateText)
}
dropInPath := filepath.Join(tmpDir, "singbox@sg-template.service.d", transportSingBoxInstanceDropIn)
dropInData, err := os.ReadFile(dropInPath)
if err != nil {
t.Fatalf("failed to read drop-in unit: %v", err)
}
dropInText := string(dropInData)
if !strings.Contains(dropInText, "Environment=SVPN_TRANSPORT_ID=sg-template") {
t.Fatalf("drop-in ownership marker missing: %s", dropInText)
}
if !strings.Contains(dropInText, "ExecStart=/bin/sh -lc") {
t.Fatalf("drop-in exec start missing: %s", dropInText)
}
}
func TestTransportSystemdBackendStartAutoProvisionOnMissingTemplateInstance(t *testing.T) {
origRunner := transportRunCommand
origUnitsDir := transportSystemdUnitsDir
defer func() {
transportRunCommand = origRunner
transportSystemdUnitsDir = origUnitsDir
}()
tmpDir := t.TempDir()
transportSystemdUnitsDir = tmpDir
startCalls := 0
transportRunCommand = func(_ time.Duration, name string, args ...string) (string, string, int, error) {
cmd := name + " " + strings.Join(args, " ")
switch cmd {
case "systemctl reset-failed singbox@sg-auto.service":
return "", "", 0, nil
case "systemctl daemon-reload":
return "", "", 0, nil
case "systemctl start singbox@sg-auto.service":
startCalls++
if startCalls == 1 {
return "", "Failed to start singbox@sg-auto.service: Unit singbox@sg-auto.service not found.", 5, fmt.Errorf("exit status 5")
}
return "", "", 0, nil
default:
return "", "", 0, nil
}
}
client := TransportClient{
ID: "sg-auto",
Kind: TransportClientSingBox,
Config: map[string]any{
"runner": "systemd",
"unit": "singbox@sg-auto.service",
"exec_start": "/usr/bin/sing-box run -c /tmp/sg-auto.json",
},
}
backend := selectTransportBackend(client)
res := backend.Action(client, "start")
if !res.OK {
t.Fatalf("expected start success after auto-provision, got %#v", res)
}
if startCalls != 2 {
t.Fatalf("expected start retried after auto-provision, got calls=%d", startCalls)
}
if _, err := os.Stat(filepath.Join(tmpDir, "singbox@.service")); err != nil {
t.Fatalf("expected template unit file, stat error: %v", err)
}
if _, err := os.Stat(filepath.Join(tmpDir, "singbox@sg-auto.service.d", transportSingBoxInstanceDropIn)); err != nil {
t.Fatalf("expected instance drop-in file, stat error: %v", err)
}
}
func TestTransportSystemdCleanupRemovesSingBoxDropInKeepsTemplate(t *testing.T) {
origRunner := transportRunCommand
origUnitsDir := transportSystemdUnitsDir
defer func() {
transportRunCommand = origRunner
transportSystemdUnitsDir = origUnitsDir
}()
tmpDir := t.TempDir()
transportSystemdUnitsDir = tmpDir
templatePath := filepath.Join(tmpDir, "singbox@.service")
if err := os.WriteFile(templatePath, []byte(transportSingBoxTemplateMarker+"\n[Service]\nExecStart=/bin/true\n"), 0o644); err != nil {
t.Fatalf("write template unit file: %v", err)
}
dropInDir := filepath.Join(tmpDir, "singbox@sg-clean.service.d")
if err := os.MkdirAll(dropInDir, 0o755); err != nil {
t.Fatalf("mkdir drop-in dir: %v", err)
}
dropInPath := filepath.Join(dropInDir, transportSingBoxInstanceDropIn)
if err := os.WriteFile(dropInPath, []byte("[Service]\nEnvironment=SVPN_TRANSPORT_ID=sg-clean\n"), 0o644); err != nil {
t.Fatalf("write drop-in file: %v", err)
}
calls := make([]string, 0, 8)
transportRunCommand = func(_ time.Duration, name string, args ...string) (string, string, int, error) {
cmd := name + " " + strings.Join(args, " ")
calls = append(calls, cmd)
return "", "", 0, nil
}
client := TransportClient{
ID: "sg-clean",
Kind: TransportClientSingBox,
Config: map[string]any{
"runner": "systemd",
"unit": "singbox@sg-clean.service",
},
}
backend := selectTransportBackend(client)
res := backend.Cleanup(client)
if !res.OK {
t.Fatalf("expected cleanup success, got %#v", res)
}
if _, err := os.Stat(templatePath); err != nil {
t.Fatalf("template should stay intact, stat error: %v", err)
}
if _, err := os.Stat(dropInPath); !os.IsNotExist(err) {
t.Fatalf("drop-in file was not removed: %v", err)
}
expected := []string{
"systemctl stop singbox@sg-clean.service",
"systemctl disable singbox@sg-clean.service",
"systemctl daemon-reload",
"systemctl reset-failed singbox@sg-clean.service",
}
for _, want := range expected {
found := false
for _, got := range calls {
if got == want {
found = true
break
}
}
if !found {
t.Fatalf("missing cleanup call %q in %#v", want, calls)
}
}
}
func TestTransportSystemdProvisionSingBoxTemplateIncludesNetnsEnv(t *testing.T) {
origRunner := transportRunCommand
origUnitsDir := transportSystemdUnitsDir
defer func() {
transportRunCommand = origRunner
transportSystemdUnitsDir = origUnitsDir
}()
tmpDir := t.TempDir()
transportSystemdUnitsDir = tmpDir
transportRunCommand = func(_ time.Duration, name string, args ...string) (string, string, int, error) {
cmd := name + " " + strings.Join(args, " ")
switch cmd {
case "systemctl daemon-reload":
return "", "", 0, nil
default:
return "", "", 0, nil
}
}
client := TransportClient{
ID: "sg-netns",
Kind: TransportClientSingBox,
Config: map[string]any{
"runner": "systemd",
"unit": "singbox@sg-netns.service",
"exec_start": "/usr/bin/sing-box run -c /tmp/sg-netns.json",
"netns_enabled": true,
"netns_name": "svpn-sg-netns",
},
}
backend := selectTransportBackend(client)
res := backend.Provision(client)
if !res.OK {
t.Fatalf("expected provision success, got %#v", res)
}
dropInPath := filepath.Join(tmpDir, "singbox@sg-netns.service.d", transportSingBoxInstanceDropIn)
dropInData, err := os.ReadFile(dropInPath)
if err != nil {
t.Fatalf("failed to read drop-in unit: %v", err)
}
dropInText := string(dropInData)
for _, want := range []string{
"Environment=SVPN_NETNS_ENABLED=1",
"Environment=SVPN_NETNS_NAME=svpn-sg-netns",
} {
if !strings.Contains(dropInText, want) {
t.Fatalf("drop-in is missing %q: %s", want, dropInText)
}
}
}
func TestTransportSystemdPreActionMigratesOwnedLegacyUnitToTemplateInstance(t *testing.T) {
origRunner := transportRunCommand
origUnitsDir := transportSystemdUnitsDir
defer func() {
transportRunCommand = origRunner
transportSystemdUnitsDir = origUnitsDir
}()
tmpDir := t.TempDir()
transportSystemdUnitsDir = tmpDir
legacyUnit := "singbox-sg-migrate.service"
legacyPath := filepath.Join(tmpDir, legacyUnit)
legacyBody := "[Service]\nEnvironment=SVPN_TRANSPORT_ID=sg-migrate\nExecStart=/bin/true\n"
if err := os.WriteFile(legacyPath, []byte(legacyBody), 0o644); err != nil {
t.Fatalf("write legacy unit: %v", err)
}
calls := make([]string, 0, 10)
transportRunCommand = func(_ time.Duration, name string, args ...string) (string, string, int, error) {
cmd := name + " " + strings.Join(args, " ")
calls = append(calls, cmd)
return "", "", 0, nil
}
client := TransportClient{
ID: "sg-migrate",
Kind: TransportClientSingBox,
Config: map[string]any{
"runner": "systemd",
"unit": "singbox@.service",
"bootstrap_bypass": false,
},
}
backend := selectTransportBackend(client)
res := backend.Action(client, "start")
if !res.OK {
t.Fatalf("expected start success, got %#v", res)
}
if !strings.Contains(res.Stdout, "legacy-migrate: singbox-sg-migrate.service -> singbox@sg-migrate.service") {
t.Fatalf("expected migrate message in stdout, got: %s", res.Stdout)
}
if _, err := os.Stat(legacyPath); !os.IsNotExist(err) {
t.Fatalf("legacy unit must be removed after migration, stat=%v", err)
}
mustContain := []string{
"systemctl stop singbox-sg-migrate.service",
"systemctl disable singbox-sg-migrate.service",
"systemctl daemon-reload",
"systemctl reset-failed singbox-sg-migrate.service",
"systemctl reset-failed singbox@sg-migrate.service",
"systemctl start singbox@sg-migrate.service",
}
got := strings.Join(calls, " | ")
for _, want := range mustContain {
if !strings.Contains(got, want) {
t.Fatalf("missing call %q in %s", want, got)
}
}
}
func TestTransportSystemdPreActionLegacyMigrationDryRun(t *testing.T) {
origRunner := transportRunCommand
origUnitsDir := transportSystemdUnitsDir
defer func() {
transportRunCommand = origRunner
transportSystemdUnitsDir = origUnitsDir
}()
tmpDir := t.TempDir()
transportSystemdUnitsDir = tmpDir
legacyUnit := "singbox-sg-dry.service"
legacyPath := filepath.Join(tmpDir, legacyUnit)
legacyBody := "[Service]\nEnvironment=SVPN_TRANSPORT_ID=sg-dry\nExecStart=/bin/true\n"
if err := os.WriteFile(legacyPath, []byte(legacyBody), 0o644); err != nil {
t.Fatalf("write legacy unit: %v", err)
}
calls := make([]string, 0, 10)
transportRunCommand = func(_ time.Duration, name string, args ...string) (string, string, int, error) {
cmd := name + " " + strings.Join(args, " ")
calls = append(calls, cmd)
return "", "", 0, nil
}
client := TransportClient{
ID: "sg-dry",
Kind: TransportClientSingBox,
Config: map[string]any{
"runner": "systemd",
"unit": "singbox@.service",
"bootstrap_bypass": false,
transportSingBoxLegacyMigrateDryRunConfigKey: true,
},
}
backend := selectTransportBackend(client)
res := backend.Action(client, "start")
if !res.OK {
t.Fatalf("expected start success, got %#v", res)
}
if !strings.Contains(res.Stdout, "legacy-migrate dry-run: singbox-sg-dry.service -> singbox@sg-dry.service") {
t.Fatalf("expected dry-run message in stdout, got: %s", res.Stdout)
}
if _, err := os.Stat(legacyPath); err != nil {
t.Fatalf("legacy unit must stay on dry-run, stat=%v", err)
}
got := strings.Join(calls, " | ")
if strings.Contains(got, "systemctl stop singbox-sg-dry.service") {
t.Fatalf("dry-run must not stop legacy unit, calls=%s", got)
}
if strings.Contains(got, "systemctl disable singbox-sg-dry.service") {
t.Fatalf("dry-run must not disable legacy unit, calls=%s", got)
}
if !strings.Contains(got, "systemctl start singbox@sg-dry.service") {
t.Fatalf("expected template start call, got=%s", got)
}
}
func TestTransportSystemdPreActionLegacyMigrationSkipsForeignOwnership(t *testing.T) {
origRunner := transportRunCommand
origUnitsDir := transportSystemdUnitsDir
defer func() {
transportRunCommand = origRunner
transportSystemdUnitsDir = origUnitsDir
}()
tmpDir := t.TempDir()
transportSystemdUnitsDir = tmpDir
legacyUnit := "singbox-sg-foreign.service"
legacyPath := filepath.Join(tmpDir, legacyUnit)
legacyBody := "[Service]\nEnvironment=SVPN_TRANSPORT_ID=other-client\nExecStart=/bin/true\n"
if err := os.WriteFile(legacyPath, []byte(legacyBody), 0o644); err != nil {
t.Fatalf("write legacy unit: %v", err)
}
calls := make([]string, 0, 10)
transportRunCommand = func(_ time.Duration, name string, args ...string) (string, string, int, error) {
cmd := name + " " + strings.Join(args, " ")
calls = append(calls, cmd)
return "", "", 0, nil
}
client := TransportClient{
ID: "sg-foreign",
Kind: TransportClientSingBox,
Config: map[string]any{
"runner": "systemd",
"unit": "singbox@.service",
"bootstrap_bypass": false,
},
}
backend := selectTransportBackend(client)
res := backend.Action(client, "start")
if !res.OK {
t.Fatalf("expected start success, got %#v", res)
}
if _, err := os.Stat(legacyPath); err != nil {
t.Fatalf("foreign legacy unit must stay untouched, stat=%v", err)
}
got := strings.Join(calls, " | ")
if strings.Contains(got, "systemctl stop singbox-sg-foreign.service") {
t.Fatalf("must not stop foreign legacy unit, calls=%s", got)
}
if strings.Contains(got, "systemctl disable singbox-sg-foreign.service") {
t.Fatalf("must not disable foreign legacy unit, calls=%s", got)
}
if !strings.Contains(got, "systemctl start singbox@sg-foreign.service") {
t.Fatalf("expected template start call, got=%s", got)
}
}
func TestTransportSystemdCleanupTemplateRemovesOwnedLegacyUnits(t *testing.T) {
origRunner := transportRunCommand
origUnitsDir := transportSystemdUnitsDir
defer func() {
transportRunCommand = origRunner
transportSystemdUnitsDir = origUnitsDir
}()
tmpDir := t.TempDir()
transportSystemdUnitsDir = tmpDir
templatePath := filepath.Join(tmpDir, "singbox@.service")
if err := os.WriteFile(templatePath, []byte(transportSingBoxTemplateMarker+"\n[Service]\nExecStart=/bin/true\n"), 0o644); err != nil {
t.Fatalf("write template unit: %v", err)
}
dropInDir := filepath.Join(tmpDir, "singbox@sg-clean-mixed.service.d")
if err := os.MkdirAll(dropInDir, 0o755); err != nil {
t.Fatalf("mkdir drop-in dir: %v", err)
}
dropInPath := filepath.Join(dropInDir, transportSingBoxInstanceDropIn)
if err := os.WriteFile(dropInPath, []byte("[Service]\nEnvironment=SVPN_TRANSPORT_ID=sg-clean-mixed\n"), 0o644); err != nil {
t.Fatalf("write drop-in: %v", err)
}
legacyPath := filepath.Join(tmpDir, "singbox-sg-clean-mixed.service")
if err := os.WriteFile(legacyPath, []byte("[Service]\nEnvironment=SVPN_TRANSPORT_ID=sg-clean-mixed\nExecStart=/bin/true\n"), 0o644); err != nil {
t.Fatalf("write legacy unit: %v", err)
}
calls := make([]string, 0, 16)
transportRunCommand = func(_ time.Duration, name string, args ...string) (string, string, int, error) {
cmd := name + " " + strings.Join(args, " ")
calls = append(calls, cmd)
return "", "", 0, nil
}
client := TransportClient{
ID: "sg-clean-mixed",
Kind: TransportClientSingBox,
Config: map[string]any{
"runner": "systemd",
"unit": "singbox@.service",
},
}
backend := selectTransportBackend(client)
res := backend.Cleanup(client)
if !res.OK {
t.Fatalf("expected cleanup success, got %#v", res)
}
if _, err := os.Stat(templatePath); err != nil {
t.Fatalf("template must stay intact, stat err=%v", err)
}
if _, err := os.Stat(dropInPath); !os.IsNotExist(err) {
t.Fatalf("drop-in must be removed, stat=%v", err)
}
if _, err := os.Stat(legacyPath); !os.IsNotExist(err) {
t.Fatalf("legacy unit must be removed, stat=%v", err)
}
got := strings.Join(calls, " | ")
mustContain := []string{
"systemctl stop singbox@sg-clean-mixed.service",
"systemctl disable singbox@sg-clean-mixed.service",
"systemctl stop singbox-sg-clean-mixed.service",
"systemctl disable singbox-sg-clean-mixed.service",
"systemctl daemon-reload",
"systemctl reset-failed singbox@sg-clean-mixed.service",
"systemctl reset-failed singbox-sg-clean-mixed.service",
}
for _, want := range mustContain {
if !strings.Contains(got, want) {
t.Fatalf("missing cleanup call %q in %s", want, got)
}
}
}