feat: 新增 GRE 隧道协议,管理端口调整为 18080

- backend: 新增 GRE 隧道实现(ip tunnel 点对点互联),binmgr 支持 iproute2
- backend: api/identity 调整,各协议 service(gre/ikev2/ipsec/l2tp/openvpn)统一改动
- backend: OpenVPN 认证辅助与用户口令同步逻辑完善
- frontend: 协议图标/向导/编辑器支持 GRE,VPN 用户页重构(234 行)
- frontend: 新增 RouteListInput 路由列表输入组件,Dashboard/Layout 优化
- i18n: 中英文文案同步更新
- test: 新增 binmgr/common/gre 单测,openvpn 测试补充
- docs: README 补充 GRE 说明,默认监听端口 8080 → 18080
- chore: .gitignore 增加 opsx-vpn.previous(构建备份二进制)
This commit is contained in:
opsx committed 2026-10-20 20:58:58 +08:00
1 parent c7f59f2cbf
commit ef7eb378d2
28 files changed
+1363 -439

No files matched your search

+113 -1
View File
@@ -44,6 +44,15 @@ func LookBin(names ...string) (string, bool) {
if p, err := exec.LookPath(n); err == nil {
return p, true
}
// systemd services often omit /usr/local/{s,}bin from PATH. Locally
// compiled dependencies such as xl2tpd are conventionally installed
// there, so discover them without relying on the caller's environment.
for _, dir := range []string{"/usr/local/sbin", "/usr/local/bin", "/usr/sbin", "/usr/bin"} {
p := filepath.Join(dir, n)
if info, err := os.Stat(p); err == nil && !info.IsDir() && info.Mode()&0o111 != 0 {
return p, true
}
}
}
return "", false
}
@@ -78,12 +87,52 @@ func RequireIPv4Forwarding(component string) error {
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 nil
}
// A VPN service needs host forwarding to route traffic from tun/xfrm
// interfaces. Enable it at start instead of requiring a manual pre-step.
if _, err := Run(sysctl, "-w", "net.ipv4.ip_forward=1"); err != nil {
return fmt.Errorf("无法启动 %s:自动启用 net.ipv4.ip_forward 失败: %w", component, err)
}
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 fmt.Errorf("无法启动 %s:net.ipv4.ip_forward 未成功启用", component)
}
return nil
}
// EnsureStrongSwanVICI makes sure swanctl can reach charon's VICI socket.
// RPM-based distributions commonly install the binary without enabling the
// strongswan.service unit, which otherwise produces a misleading load error.
func EnsureStrongSwanVICI(swanctl string) error {
if _, err := Run(swanctl, "--list-conns"); err == nil {
return nil
}
systemctl, ok := LookBin("systemctl")
if !ok {
return fmt.Errorf("strongSwan 守护进程未运行,且未检测到 systemctl 以启动 charon")
}
var lastErr error
for _, unit := range []string{"strongswan.service", "strongswan-swanctl.service"} {
if _, err := Run(systemctl, "enable", "--now", unit); err != nil {
lastErr = err
continue
}
// Give systemd a short interval to create charon's VICI socket.
for range 10 {
if _, err := Run(swanctl, "--list-conns"); err == nil {
return nil
}
time.Sleep(100 * time.Millisecond)
}
}
if lastErr != nil {
return fmt.Errorf("无法启动 strongSwan 守护进程: %w", lastErr)
}
return fmt.Errorf("strongSwan 守护进程已启动但 VICI 接口不可用")
}
// 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.
@@ -230,6 +279,69 @@ func RemoveInstance(t *model.Tunnel) error {
return os.RemoveAll(InstanceDir(t))
}
// SnapshotInstance captures one tunnel's generated files before a live
// configuration update. It intentionally excludes no files: protocol-specific
// keys, PID metadata and auxiliary scripts must be restored as one unit.
func SnapshotInstance(t *model.Tunnel) (string, error) {
source := InstanceDir(t)
parent := filepath.Dir(source)
if err := os.MkdirAll(parent, 0o700); err != nil {
return "", err
}
backup, err := os.MkdirTemp(parent, ".opsx-backup-")
if err != nil {
return "", err
}
if _, err := os.Stat(source); os.IsNotExist(err) {
return backup, nil
} else if err != nil {
_ = os.RemoveAll(backup)
return "", err
}
if err := copyTree(source, backup); err != nil {
_ = os.RemoveAll(backup)
return "", err
}
return backup, nil
}
// RestoreInstanceSnapshot replaces generated files with a previous snapshot.
func RestoreInstanceSnapshot(t *model.Tunnel, backup string) error {
if err := os.RemoveAll(InstanceDir(t)); err != nil {
return err
}
if err := os.MkdirAll(InstanceDir(t), 0o700); err != nil {
return err
}
return copyTree(backup, InstanceDir(t))
}
func CleanupInstanceSnapshot(backup string) { _ = os.RemoveAll(backup) }
func copyTree(source, destination string) error {
return filepath.Walk(source, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
rel, err := filepath.Rel(source, path)
if err != nil {
return err
}
target := filepath.Join(destination, rel)
if info.IsDir() {
return os.MkdirAll(target, info.Mode().Perm())
}
if !info.Mode().IsRegular() {
return nil
}
content, err := os.ReadFile(path)
if err != nil {
return err
}
return os.WriteFile(target, content, info.Mode().Perm())
})
}
// Str / StrOr / Num / Bool 从配置 map 中安全取值
func Str(cfg map[string]interface{}, key string) string {
if v, ok := cfg[key]; ok {