306 lines
8.0 KiB
Go
306 lines
8.0 KiB
Go
// Package service 提供协议实现共用的运行时工具函数
|
||
package service
|
||
|
||
import (
|
||
"bytes"
|
||
"crypto/rand"
|
||
"encoding/base64"
|
||
"encoding/json"
|
||
"fmt"
|
||
"net"
|
||
"os"
|
||
"os/exec"
|
||
"path/filepath"
|
||
"strconv"
|
||
"strings"
|
||
"syscall"
|
||
"time"
|
||
|
||
"vpn-platform/backend/internal/core"
|
||
"vpn-platform/backend/internal/model"
|
||
"vpn-platform/backend/internal/store"
|
||
)
|
||
|
||
// BaseDir 实例配置与运行时根目录(可通过 VPN_DATA_DIR 覆盖)
|
||
func BaseDir() string {
|
||
if v := os.Getenv("VPN_DATA_DIR"); v != "" {
|
||
return v
|
||
}
|
||
exe, _ := os.Executable()
|
||
if exe != "" {
|
||
return filepath.Join(filepath.Dir(exe), "data")
|
||
}
|
||
return "data"
|
||
}
|
||
|
||
// InstanceDir 专线实例目录
|
||
func InstanceDir(t *model.Tunnel) string {
|
||
return filepath.Join(BaseDir(), "instances", fmt.Sprintf("%d-%s", t.ID, t.Protocol))
|
||
}
|
||
|
||
// LookBin 检测系统依赖二进制
|
||
func LookBin(names ...string) (string, bool) {
|
||
for _, n := range names {
|
||
if p, err := exec.LookPath(n); err == nil {
|
||
return p, true
|
||
}
|
||
}
|
||
return "", false
|
||
}
|
||
|
||
// Run 执行命令并返回输出
|
||
func Run(name string, args ...string) (string, error) {
|
||
cmd := exec.Command(name, args...)
|
||
var out, errOut bytes.Buffer
|
||
cmd.Stdout = &out
|
||
cmd.Stderr = &errOut
|
||
if err := cmd.Run(); err != nil {
|
||
return out.String(), fmt.Errorf("%s %s 执行失败: %v %s", name, strings.Join(args, " "), err, errOut.String())
|
||
}
|
||
return out.String(), nil
|
||
}
|
||
|
||
// RequireRoot rejects service operations that need kernel networking privileges.
|
||
// Starting the web process as an unprivileged account must never be reported as
|
||
// a running VPN service merely because configuration generation succeeded.
|
||
func RequireRoot(component string) error {
|
||
if os.Geteuid() != 0 {
|
||
return fmt.Errorf("无法启动 %s:需要 root 权限以管理内核网络、IPSec SA 和防火墙规则", component)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// RequireIPv4Forwarding verifies the kernel prerequisite for forwarding traffic
|
||
// between a VPN address pool and the LAN/Internet.
|
||
func RequireIPv4Forwarding(component string) error {
|
||
sysctl, ok := LookBin("sysctl")
|
||
if !ok {
|
||
return fmt.Errorf("无法启动 %s:未检测到 sysctl,无法确认 net.ipv4.ip_forward", component)
|
||
}
|
||
out, err := Run(sysctl, "-n", "net.ipv4.ip_forward")
|
||
if err != nil || strings.TrimSpace(out) != "1" {
|
||
return fmt.Errorf("无法启动 %s:请先执行 sysctl -w net.ipv4.ip_forward=1", component)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// RequireUDPPortAvailable checks a port before handing it to a daemon. This is
|
||
// deliberately a real bind check instead of parsing process output, so it also
|
||
// catches listeners started outside this platform.
|
||
func RequireUDPPortAvailable(port int, component string) error {
|
||
conn, err := net.ListenUDP("udp4", &net.UDPAddr{Port: port})
|
||
if err != nil {
|
||
return fmt.Errorf("无法启动 %s:UDP %d 已被其他服务占用: %w", component, port, err)
|
||
}
|
||
return conn.Close()
|
||
}
|
||
|
||
// DefaultRouteInterface returns the interface used for IPv4 Internet egress.
|
||
// It is used only when a VPN configuration does not explicitly set one.
|
||
func DefaultRouteInterface() (string, error) {
|
||
ip, ok := LookBin("ip")
|
||
if !ok {
|
||
return "", fmt.Errorf("未检测到 iproute2 的 ip 命令,无法确定 NAT 出口接口")
|
||
}
|
||
out, err := Run(ip, "-4", "route", "show", "default")
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
fields := strings.Fields(out)
|
||
for i := 0; i+1 < len(fields); i++ {
|
||
if fields[i] == "dev" {
|
||
return fields[i+1], nil
|
||
}
|
||
}
|
||
return "", fmt.Errorf("未找到 IPv4 默认路由,无法确定 NAT 出口接口")
|
||
}
|
||
|
||
// StartDaemon 以守护方式启动进程并记录 PID
|
||
func StartDaemon(t *model.Tunnel, name string, args ...string) error {
|
||
if err := os.MkdirAll(InstanceDir(t), 0o700); err != nil {
|
||
return err
|
||
}
|
||
logFile, err := os.OpenFile(filepath.Join(InstanceDir(t), "instance.log"), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o600)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
cmd := exec.Command(name, args...)
|
||
cmd.Stdout = logFile
|
||
cmd.Stderr = logFile
|
||
cmd.Dir = InstanceDir(t)
|
||
if err := cmd.Start(); err != nil {
|
||
logFile.Close()
|
||
return err
|
||
}
|
||
pid := cmd.Process.Pid
|
||
_ = os.WriteFile(PidFile(t), []byte(strconv.Itoa(pid)), 0o600)
|
||
go func() { _ = cmd.Wait(); logFile.Close() }()
|
||
return nil
|
||
}
|
||
|
||
// PidFile 实例 PID 文件
|
||
func PidFile(t *model.Tunnel) string {
|
||
return filepath.Join(InstanceDir(t), "instance.pid")
|
||
}
|
||
|
||
// ReadPid 读取实例 PID 并校验存活
|
||
func ReadPid(t *model.Tunnel) (int, bool) {
|
||
raw, err := os.ReadFile(PidFile(t))
|
||
if err != nil {
|
||
return 0, false
|
||
}
|
||
pid, err := strconv.Atoi(strings.TrimSpace(string(raw)))
|
||
if err != nil || pid <= 0 {
|
||
return 0, false
|
||
}
|
||
proc, err := os.FindProcess(pid)
|
||
if err != nil {
|
||
return 0, false
|
||
}
|
||
// signal 0 检测进程存活
|
||
if err := proc.Signal(syscall.Signal(0)); err != nil {
|
||
return pid, false
|
||
}
|
||
return pid, true
|
||
}
|
||
|
||
// KillPid 停止实例进程
|
||
func KillPid(t *model.Tunnel) error {
|
||
pid, alive := ReadPid(t)
|
||
if !alive {
|
||
return nil
|
||
}
|
||
proc, err := os.FindProcess(pid)
|
||
if err != nil {
|
||
return nil
|
||
}
|
||
_ = proc.Kill()
|
||
_, _ = proc.Wait()
|
||
_ = os.Remove(PidFile(t))
|
||
return nil
|
||
}
|
||
|
||
// WriteConfig 写入实例配置文件(0600 权限)
|
||
func WriteConfig(t *model.Tunnel, name, content string) (string, error) {
|
||
if err := os.MkdirAll(InstanceDir(t), 0o700); err != nil {
|
||
return "", err
|
||
}
|
||
p := filepath.Join(InstanceDir(t), name)
|
||
if err := os.WriteFile(p, []byte(content), 0o600); err != nil {
|
||
return "", err
|
||
}
|
||
return p, nil
|
||
}
|
||
|
||
// RandomSecret 生成适合配置文件与 API 凭据使用的不可预测随机值。
|
||
func RandomSecret(bytesLen int) (string, error) {
|
||
raw := make([]byte, bytesLen)
|
||
if _, err := rand.Read(raw); err != nil {
|
||
return "", err
|
||
}
|
||
return base64.RawURLEncoding.EncodeToString(raw), nil
|
||
}
|
||
|
||
// RemoveInstance 清理隧道的专属配置、日志与 PID 文件。
|
||
func RemoveInstance(t *model.Tunnel) error {
|
||
return os.RemoveAll(InstanceDir(t))
|
||
}
|
||
|
||
// Str / StrOr / Num / Bool 从配置 map 中安全取值
|
||
func Str(cfg map[string]interface{}, key string) string {
|
||
if v, ok := cfg[key]; ok {
|
||
if s, ok := v.(string); ok {
|
||
return s
|
||
}
|
||
}
|
||
return ""
|
||
}
|
||
|
||
func StrOr(cfg map[string]interface{}, key, def string) string {
|
||
if s := Str(cfg, key); s != "" {
|
||
return s
|
||
}
|
||
return def
|
||
}
|
||
|
||
func Num(cfg map[string]interface{}, key string, def float64) float64 {
|
||
if v, ok := cfg[key]; ok {
|
||
switch n := v.(type) {
|
||
case float64:
|
||
return n
|
||
case int:
|
||
return float64(n)
|
||
case string:
|
||
if f, err := strconv.ParseFloat(n, 64); err == nil {
|
||
return f
|
||
}
|
||
}
|
||
}
|
||
return def
|
||
}
|
||
|
||
func Bool(cfg map[string]interface{}, key string, def bool) bool {
|
||
if v, ok := cfg[key]; ok {
|
||
if b, ok := v.(bool); ok {
|
||
return b
|
||
}
|
||
}
|
||
return def
|
||
}
|
||
|
||
// MustValidate 公共校验:配置必须是合法 JSON 对象
|
||
func MustValidate(cfg map[string]interface{}, required ...string) error {
|
||
for _, k := range required {
|
||
if Str(cfg, k) == "" && Num(cfg, k, 0) == 0 {
|
||
return fmt.Errorf("缺少必填参数: %s", k)
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// ConfigJSON 序列化配置
|
||
func ConfigJSON(cfg map[string]interface{}) string {
|
||
raw, _ := json.Marshal(cfg)
|
||
return string(raw)
|
||
}
|
||
|
||
// NowStatus 构造状态对象
|
||
func NowStatus(running bool, state, detail string) core.Status {
|
||
return core.Status{Running: running, State: state, Detail: detail, UpdatedAt: time.Now().Unix()}
|
||
}
|
||
|
||
// MergeSecrets 从 cfg 中提取敏感字段,返回 (脱敏后的公开配置, 敏感配置)
|
||
func MergeSecrets(cfg map[string]interface{}, secretKeys ...string) (public map[string]interface{}, secrets map[string]interface{}) {
|
||
public = map[string]interface{}{}
|
||
secrets = map[string]interface{}{}
|
||
for k, v := range cfg {
|
||
isSecret := false
|
||
for _, sk := range secretKeys {
|
||
if k == sk {
|
||
isSecret = true
|
||
break
|
||
}
|
||
}
|
||
if isSecret {
|
||
secrets[k] = v
|
||
} else {
|
||
public[k] = v
|
||
}
|
||
}
|
||
return
|
||
}
|
||
|
||
// SaveTunnelConfig 将公开配置与加密密钥写回数据库
|
||
func SaveTunnelConfig(t *model.Tunnel, public, secrets map[string]interface{}) error {
|
||
t.Config = ConfigJSON(public)
|
||
if len(secrets) > 0 {
|
||
raw, _ := json.Marshal(secrets)
|
||
enc, err := store.EncryptSecret(string(raw))
|
||
if err != nil {
|
||
return err
|
||
}
|
||
t.Secrets = enc
|
||
}
|
||
return store.DB.Save(t).Error
|
||
}
|