// SPDX-License-Identifier: MIT // Package engine controls the proxy engine (sing-box) through systemd. package engine import ( "context" "encoding/json" "errors" "fmt" "os" "strings" "prx/internal/config" "prx/internal/execx" ) // IsActive reports whether the systemd unit is active. func IsActive(ctx context.Context, r execx.Runner, service string) (bool, error) { res, err := r.Run(ctx, nil, "systemctl", "is-active", service) state := strings.TrimSpace(string(res.Stdout)) if err != nil { var ee *execx.ExitError if errors.As(err, &ee) { // inactive/failed units exit non-zero; that is not an error here. return state == "active", nil } return false, err } return state == "active", nil } // Start starts the unit. func Start(ctx context.Context, r execx.Runner, service string) error { _, err := r.Run(ctx, nil, "systemctl", "start", service) if err != nil { return fmt.Errorf("start %s: %w", service, err) } return nil } // Stop stops the unit. func Stop(ctx context.Context, r execx.Runner, service string) error { _, err := r.Run(ctx, nil, "systemctl", "stop", service) if err != nil { return fmt.Errorf("stop %s: %w", service, err) } return nil } // Reload asks the unit to reload its configuration. func Reload(ctx context.Context, r execx.Runner, service string) error { _, err := r.Run(ctx, nil, "systemctl", "reload", service) if err != nil { return fmt.Errorf("reload %s: %w", service, err) } return nil } // ValidateConfig inspects a sing-box configuration for the settings prx relies // on, returning human readable warnings (never fatal). func ValidateConfig(path string) ([]string, error) { if path == "" { return nil, nil } raw, err := os.ReadFile(path) if err != nil { return nil, err } var parsed struct { Inbounds []struct { Type string `json:"type"` } `json:"inbounds"` Route struct { AutoRoute *bool `json:"auto_route"` } `json:"route"` } if err := json.Unmarshal(raw, &parsed); err != nil { return nil, fmt.Errorf("parse engine config %s: %w", path, err) } var warnings []string hasTun := false for _, in := range parsed.Inbounds { if in.Type == "tun" { hasTun = true } } if !hasTun { warnings = append(warnings, "engine config has no tun inbound; proxy routing expects TUN mode") } if parsed.Route.AutoRoute != nil && !*parsed.Route.AutoRoute { warnings = append(warnings, "engine route.auto_route is false; the tunnel may not capture traffic") } return warnings, nil } // Template renders a baseline sing-box configuration derived from cfg. func Template(cfg *config.Config) ([]byte, error) { directRule := map[string]any{ "action": "route", "outbound": "direct", } if len(cfg.Bypass.AlwaysCIDRs) > 0 || len(cfg.Bypass.ExtraCIDRs) > 0 { directRule["ip_cidr"] = append(append([]string{}, cfg.Bypass.AlwaysCIDRs...), cfg.Bypass.ExtraCIDRs...) } if len(cfg.Bypass.Domains) > 0 { directRule["domain"] = cfg.Bypass.Domains } if len(cfg.ProcessBypass.Names) > 0 { directRule["process_name"] = cfg.ProcessBypass.Names } doc := map[string]any{ "log": map[string]any{"level": "info"}, "inbounds": []any{ map[string]any{ "type": "tun", "tag": "tun-in", "interface_name": "tun0", "address": []string{"172.19.0.1/30", "fdfe:dcba:9876::1/126"}, "mtu": 9000, "auto_route": true, "strict_route": true, "stack": "system", "auto_redirect": false, }, }, "outbounds": []any{ // Replace "direct" with your proxy outbound (e.g. { "type": "shadowsocks", ... }). map[string]any{"type": "direct", "tag": "direct"}, }, "route": map[string]any{ "auto_detect_interface": true, "rules": []any{directRule}, "final": "direct", }, } out, err := json.MarshalIndent(doc, "", " ") if err != nil { return nil, err } return append(out, '\n'), nil }