Scert/backend/internal/servicehost/linux.go

239 lines
6.2 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//go:build linux
package servicehost
import (
"context"
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
)
func Install(ctx context.Context, options Options) error {
o, err := options.normalized()
if err != nil {
return err
}
if err := writeUnitFile(o); err != nil {
return commandError("写入", err)
}
if err := runSystemctl(ctx, "daemon-reload"); err != nil {
return commandError("重新加载", err)
}
if err := runSystemctl(ctx, "enable", o.Name); err != nil {
return commandError("注册", err)
}
return nil
}
func Uninstall(ctx context.Context, name string) error {
name, err := normalizeName(name)
if err != nil {
return err
}
if err := runSystemctl(ctx, "disable", name); err != nil {
return commandError("取消注册", err)
}
if err := removeUnitFile(name); err != nil {
return commandError("删除", err)
}
if err := runSystemctl(ctx, "daemon-reload"); err != nil {
return commandError("重新加载", err)
}
return nil
}
func Start(ctx context.Context, name string) error {
name, err := normalizeName(name)
if err != nil {
return err
}
if err := runSystemctl(ctx, "start", name); err != nil {
return commandError("启动", err)
}
return nil
}
func Stop(ctx context.Context, name string) error {
name, err := normalizeName(name)
if err != nil {
return err
}
if err := runSystemctl(ctx, "stop", name); err != nil {
return commandError("停止", err)
}
return nil
}
func runSystemctl(ctx context.Context, args ...string) error {
command := exec.CommandContext(ctx, "systemctl", args...)
output, err := command.CombinedOutput()
if err != nil {
return &commandFailure{output: string(output), err: err}
}
return nil
}
const systemUnitDirectory = "/etc/systemd/system"
func unitFilePath(name string) (string, error) {
name, err := normalizeName(name)
if err != nil {
return "", err
}
return filepath.Join(systemUnitDirectory, name+".service"), nil
}
func writeUnitFile(options Options) error {
path, err := unitFilePath(options.Name)
if err != nil {
return err
}
content, err := renderUnitFile(options)
if err != nil {
return err
}
if info, statErr := os.Lstat(path); statErr == nil && info.Mode()&os.ModeSymlink != 0 {
return errors.New("服务 unit 文件不能是符号链接")
} else if statErr != nil && !errors.Is(statErr, os.ErrNotExist) {
return fmt.Errorf("检查服务 unit 文件失败:%w", statErr)
}
temporary, err := os.CreateTemp(systemUnitDirectory, ".scert-unit-")
if err != nil {
return fmt.Errorf("创建临时 unit 文件失败:%w", err)
}
temporaryPath := temporary.Name()
defer os.Remove(temporaryPath)
if err := temporary.Chmod(0o644); err != nil {
_ = temporary.Close()
return fmt.Errorf("设置 unit 文件权限失败:%w", err)
}
if _, err := temporary.WriteString(content); err != nil {
_ = temporary.Close()
return fmt.Errorf("写入 unit 文件失败:%w", err)
}
if err := temporary.Sync(); err != nil {
_ = temporary.Close()
return fmt.Errorf("同步 unit 文件失败:%w", err)
}
if err := temporary.Close(); err != nil {
return fmt.Errorf("关闭 unit 文件失败:%w", err)
}
if err := os.Rename(temporaryPath, path); err != nil {
return fmt.Errorf("替换 unit 文件失败:%w", err)
}
return nil
}
func removeUnitFile(name string) error {
path, err := unitFilePath(name)
if err != nil {
return err
}
info, err := os.Lstat(path)
if errors.Is(err, os.ErrNotExist) {
return nil
}
if err != nil {
return fmt.Errorf("检查服务 unit 文件失败:%w", err)
}
if info.Mode()&os.ModeSymlink != 0 {
return errors.New("服务 unit 文件不能是符号链接")
}
if err := os.Remove(path); err != nil {
return fmt.Errorf("删除服务 unit 文件失败:%w", err)
}
return nil
}
func renderUnitFile(options Options) (string, error) {
normalized, err := options.normalized()
if err != nil {
return "", err
}
for _, path := range normalized.ReadWritePaths {
if !filepath.IsAbs(path) {
return "", fmt.Errorf("服务可写路径必须是绝对路径:%s", path)
}
}
lines := []string{
"[Unit]",
"Description=" + normalized.Description,
"After=network-online.target",
"Wants=network-online.target",
"",
"[Service]",
"Type=notify",
"NotifyAccess=main",
"TimeoutStartSec=90",
}
if normalized.User != "" {
lines = append(lines, "User="+normalized.User, "Group="+normalized.Group)
}
lines = append(lines,
"WorkingDirectory="+quoteUnitValue(normalized.WorkDir),
"ExecStart="+quoteUnitValue(normalized.Executable)+" --config "+quoteUnitValue(normalized.ConfigPath)+" serve",
"Environment=SCERT_BOOTSTRAP_FIREWALL_MODE=external",
"Environment=SCERT_SERVER_HOST=127.0.0.1",
"KillSignal=SIGTERM",
"TimeoutStopSec=180",
"Restart=on-failure",
"UMask=0077",
"RuntimeDirectory=scert",
"RuntimeDirectoryMode=0700",
"NoNewPrivileges=yes",
"CapabilityBoundingSet=",
"AmbientCapabilities=",
"PrivateTmp=yes",
"PrivateDevices=yes",
"ProtectHome=yes",
"ProtectSystem=strict",
"ProtectHostname=yes",
"ProtectClock=yes",
"ProtectKernelTunables=yes",
"ProtectKernelModules=yes",
"ProtectControlGroups=yes",
"ProtectKernelLogs=yes",
"LockPersonality=yes",
"MemoryDenyWriteExecute=yes",
"RestrictNamespaces=yes",
"RestrictRealtime=yes",
"RestrictSUIDSGID=yes",
"RemoveIPC=yes",
"SystemCallArchitectures=native",
// 初始化证书枚举本机网卡需要 NETLINK_ROUTE,空能力集仍禁止网络管理操作。
"RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK",
)
if len(normalized.ReadWritePaths) > 0 {
quotedPaths := make([]string, 0, len(normalized.ReadWritePaths))
for _, path := range normalized.ReadWritePaths {
quotedPaths = append(quotedPaths, quoteUnitValue(path))
}
lines = append(lines, "ReadWritePaths="+strings.Join(quotedPaths, " "))
}
lines = append(lines, "", "[Install]", "WantedBy=multi-user.target", "")
return strings.Join(lines, "\n"), nil
}
func quoteUnitValue(value string) string {
return strconv.Quote(value)
}
type commandFailure struct {
output string
err error
}
func (e *commandFailure) Error() string {
if e.err != nil {
return e.output + ": " + e.err.Error()
}
return e.output
}
func (e *commandFailure) Unwrap() error { return e.err }