feat: implement VPN platform management and real protocol services
This commit is contained in:
commit
58c036c34c
45 files changed
+9244
No files matched your search
@@ -0,0 +1,305 @@
|
||||
// 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
|
||||
}
|
||||
Reference in new issue
Block a user