arblant
firefox-vpn-client / core / internal / tunservice / service_linux.go
service_linux.go104 lines · 2.7 KB
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104
package tunservice

import (
	"errors"
	"fmt"
	"os"
	"os/exec"

	"github.com/armanlife/firefox-vpn-client/core/internal/tunapi"
)

// InstallDir is the installed service's folder.
const InstallDir = "/opt/firefox-vpn-tun"

const unitName = tunapi.ServiceName + ".service"

// unit is the service's systemd unit. Restart=always starts it again after
// a crash and after an update (which ends it); an uninstall stops it for
// good (stopForGood).
const unit = `[Unit]
Description=Firefox VPN TUN
Documentation=https://github.com/armanlife/firefox-vpn-client
After=network.target

[Service]
Type=simple
ExecStart=%s service
Restart=always
RestartSec=2

[Install]
WantedBy=multi-user.target
`

// initSystemPresent: the service needs systemd as the init system.
func initSystemPresent() error {
	if _, err := os.Stat("/run/systemd/system"); err != nil {
		return errors.New("the TUN mode needs systemd, which does not run this system")
	}
	if _, err := exec.LookPath("systemctl"); err != nil {
		return errors.New("the TUN mode needs systemd's systemctl, which is not found")
	}
	return nil
}

// register writes the unit for the executable exe, then enables and
// starts it.
func register(exe string) error {
	if err := replaceFile(tunapi.ServiceFile, []byte(fmt.Sprintf(unit, systemdQuote(exe))), 0o644); err != nil {
		return err
	}
	if err := run("systemctl", "daemon-reload"); err != nil {
		return err
	}
	if err := run("systemctl", "enable", unitName); err != nil {
		return err
	}
	return run("systemctl", "restart", unitName)
}

// unregister disables the unit and removes it; it does not stop the
// service, which may be the caller.
func unregister() error {
	if installed, _ := tunapi.Installed(); !installed {
		return nil
	}
	_ = run("systemctl", "disable", unitName)
	if err := os.Remove(tunapi.ServiceFile); err != nil && !errors.Is(err, os.ErrNotExist) {
		return err
	}
	return run("systemctl", "daemon-reload")
}

// stopInstalled stops the installed service and waits for it.
func stopInstalled() error {
	if installed, _ := tunapi.Installed(); !installed {
		return nil
	}
	return run("systemctl", "stop", unitName)
}

// stopForGood has systemd stop the service, whose unit is removed, so it is
// not started again when this process ends.
func stopForGood() {
	_ = run("systemctl", "--no-block", "stop", unitName)
}

// systemdQuote quotes a path for ExecStart: in double quotes, with
// backslash and double quote escaped, and the specifier and variable signs
// doubled.
func systemdQuote(path string) string {
	out := []byte{'"'}
	for i := 0; i < len(path); i++ {
		switch c := path[i]; c {
		case '\\', '"':
			out = append(out, '\\', c)
		case '%', '$':
			out = append(out, c, c)
		default:
			out = append(out, c)
		}
	}
	return string(append(out, '"'))
}