501 lines
16 KiB
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)
|
|
}
|
|
}
|
|
}
|