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:
1 parent
c7f59f2cbf
commit
ef7eb378d2
28 files changed
+1363
-439
No files matched your search
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user