Files
prx/internal/engine/engine.go
T

144 lines
3.8 KiB
Go
Raw Permalink Normal View History

// 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
}