Files
opsx-vpn/backend/internal/service/gre/gre.go
T
opsx-tools a14d9e62e6 feat: 日志中心 + GRE协议支持 + 流量监控 + 部署脚本
- 新增日志中心功能 (api/logapi.go + Logs.vue)
- 新增GRE协议服务支持 (service/gre/)
- 新增流量监控组件 (TrafficChart.vue)
- 新增L2TP/IPSec测试用例
- 新增日志轮转服务 (logrotate.go)
- 新增部署脚本 (scripts/install.sh, uninstall.sh, service文件)
- 更新i18n国际化文件
- 优化后端API和前端路由
2026-07-25 08:50:34 +08:00

304 lines
9.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package gre 实现 GRE 隧道管理(Linux ip tunnel)。
// GRE 是简单的 IP-over-IP 封装,依赖 iproute2 的 ip 命令。
package gre
import (
"fmt"
"net/netip"
"strings"
"vpn-platform/backend/internal/core"
"vpn-platform/backend/internal/model"
"vpn-platform/backend/internal/service"
"vpn-platform/backend/internal/store"
)
type Service struct{}
func New() *Service { return &Service{} }
func (s *Service) Name() string { return "gre" }
func (s *Service) DisplayName() string { return "GRE 隧道" }
func (s *Service) Available() (bool, string) {
if p, ok := service.LookBin("ip"); ok {
return true, p
}
return false, "未检测到 iproute2(ip),GRE 隧道需要 ip 命令"
}
func ifaceName(t *model.Tunnel) string {
return fmt.Sprintf("gre%d", t.ID)
}
func (s *Service) Validate(cfg map[string]interface{}) (map[string]interface{}, error) {
if err := service.MustValidate(cfg, "local_endpoint", "remote_endpoint", "local_tunnel_ip", "remote_tunnel_ip"); err != nil {
return nil, err
}
out := service.CloneConfig(cfg)
// Validate IPs
if _, err := netip.ParseAddr(service.Str(out, "local_endpoint")); err != nil {
return nil, fmt.Errorf("local_endpoint 必须是有效的 IP 地址")
}
if _, err := netip.ParseAddr(service.Str(out, "remote_endpoint")); err != nil {
return nil, fmt.Errorf("remote_endpoint 必须是有效的 IP 地址")
}
if _, err := netip.ParsePrefix(service.Str(out, "local_tunnel_ip")); err != nil {
return nil, fmt.Errorf("local_tunnel_ip 必须是 CIDR 格式(如 10.10.0.1/30)")
}
if _, err := netip.ParsePrefix(service.Str(out, "remote_tunnel_ip")); err != nil {
return nil, fmt.Errorf("remote_tunnel_ip 必须是 CIDR 格式(如 10.10.0.2/30)")
}
// Defaults
if service.Num(out, "ttl", 0) == 0 {
out["ttl"] = 255
}
if service.Num(out, "mtu", 0) == 0 {
out["mtu"] = 1476
}
return out, nil
}
func (s *Service) Create(t *model.Tunnel) error {
cfg, err := store.TunnelConfig(t)
if err != nil {
return err
}
cfg, err = s.Validate(cfg)
if err != nil {
return err
}
// Save canonicalised config
public, sec := service.MergeSecrets(cfg)
return service.SaveTunnelConfig(t, public, sec)
}
func (s *Service) Start(t *model.Tunnel) error {
cfg, err := store.TunnelConfig(t)
if err != nil {
return err
}
ip, ok := service.LookBin("ip")
if !ok {
return fmt.Errorf("无法启动 GRE:未检测到 ip 命令")
}
if err := service.RequireRoot("GRE"); err != nil {
return err
}
ifName := ifaceName(t)
localEP := service.Str(cfg, "local_endpoint")
remoteEP := service.Str(cfg, "remote_endpoint")
ttl := int(service.Num(cfg, "ttl", 255))
mtu := int(service.Num(cfg, "mtu", 1476))
localTunnelIP := service.Str(cfg, "local_tunnel_ip")
// Clean up any stale interface first
_, _ = service.Run(ip, "tunnel", "del", ifName)
// ip tunnel add greN mode gre local X remote Y ttl T
if _, err := service.Run(ip, "tunnel", "add", ifName,
"mode", "gre",
"local", localEP,
"remote", remoteEP,
"ttl", fmt.Sprintf("%d", ttl),
); err != nil {
return fmt.Errorf("创建 GRE 隧道接口失败: %w", err)
}
// ip link set greN mtu M
if _, err := service.Run(ip, "link", "set", ifName, "mtu", fmt.Sprintf("%d", mtu)); err != nil {
_, _ = service.Run(ip, "tunnel", "del", ifName)
return fmt.Errorf("设置 GRE MTU 失败: %w", err)
}
// ip addr add local_tunnel_ip dev greN
if _, err := service.Run(ip, "addr", "add", localTunnelIP, "dev", ifName); err != nil {
_, _ = service.Run(ip, "tunnel", "del", ifName)
return fmt.Errorf("分配 GRE 隧道 IP 失败: %w", err)
}
// ip link set greN up
if _, err := service.Run(ip, "link", "set", ifName, "up"); err != nil {
_, _ = service.Run(ip, "tunnel", "del", ifName)
return fmt.Errorf("启用 GRE 接口失败: %w", err)
}
// Optional: route remote subnet via remote tunnel IP
if remoteSubnet := service.Str(cfg, "remote_subnet"); remoteSubnet != "" {
remoteTunnelIP := service.Str(cfg, "remote_tunnel_ip")
// Extract just the IP (without prefix) for the route gateway
if addr, err := netip.ParsePrefix(remoteTunnelIP); err == nil {
gw := addr.Addr().String()
_, _ = service.Run(ip, "route", "add", remoteSubnet, "via", gw, "dev", ifName)
}
}
// Optional: NAT / MASQUERADE
if service.Bool(cfg, "enable_nat", false) {
if err := s.installFirewall(t, cfg); err != nil {
_, _ = service.Run(ip, "tunnel", "del", ifName)
return err
}
}
return nil
}
func (s *Service) Stop(t *model.Tunnel) error {
ip, ok := service.LookBin("ip")
if !ok {
return fmt.Errorf("无法停止 GRE:未检测到 ip 命令")
}
ifName := ifaceName(t)
_, _ = service.Run(ip, "tunnel", "del", ifName)
s.removeFirewall(t)
return nil
}
func (s *Service) Status(t *model.Tunnel) (core.Status, error) {
ip, ok := service.LookBin("ip")
if !ok {
return service.NowStatus(false, "undeployed", "未检测到 ip 命令,GRE 隧道未部署"), nil
}
ifName := ifaceName(t)
// Check tunnel exists
if _, err := service.Run(ip, "tunnel", "show", ifName); err != nil {
return service.NowStatus(false, "stopped", "GRE 隧道接口不存在"), nil
}
// Check link is UP
out, err := service.Run(ip, "link", "show", ifName)
if err != nil {
return service.NowStatus(false, "stopped", "无法读取 GRE 接口状态"), nil
}
if strings.Contains(out, "UP") {
cfg, _ := store.TunnelConfig(t)
detail := fmt.Sprintf("GRE 隧道 %s 运行中", ifName)
if le := service.Str(cfg, "local_endpoint"); le != "" {
detail += fmt.Sprintf(",本端 %s → 对端 %s", le, service.Str(cfg, "remote_endpoint"))
}
return service.NowStatus(true, "running", detail), nil
}
return service.NowStatus(false, "stopped", "GRE 隧道接口存在但未 UP"), nil
}
func (s *Service) Delete(t *model.Tunnel) error {
_ = s.Stop(t)
return service.RemoveInstance(t)
}
func (s *Service) GenerateClientConfig(t *model.Tunnel) (string, error) {
cfg, err := store.TunnelConfig(t)
if err != nil {
return "", err
}
ttl := int(service.Num(cfg, "ttl", 255))
mtu := int(service.Num(cfg, "mtu", 1476))
localEP := service.Str(cfg, "local_endpoint")
remoteEP := service.Str(cfg, "remote_endpoint")
localTunnelIP := service.Str(cfg, "local_tunnel_ip")
remoteTunnelIP := service.Str(cfg, "remote_tunnel_ip")
// Extract just the address portion for the route gateway
gw := remoteTunnelIP
if prefix, err := netip.ParsePrefix(remoteTunnelIP); err == nil {
gw = prefix.Addr().String()
}
return fmt.Sprintf(`======== GRE 隧道对端参考配置 ========
本端公网地址(local_endpoint) : %s
对端公网地址(remote_endpoint) : %s
本端隧道 IP(local_tunnel_ip) : %s
对端隧道 IP(remote_tunnel_ip) : %s
TTL : %d
MTU : %d
----- 对端配置指引 -----
ip tunnel add greN mode gre local %s remote %s ttl %d
ip addr add %s dev greN
ip link set greN up
注意:
- 双端 local_endpoint / remote_endpoint 需交叉对应
- 隧道 IP 建议使用同一 /30 子网
- 如需 NAT 请在各自防火墙中添加 MASQUERADE 规则
- remote_subnet 路由示例: ip route add <子网> via %s dev greN
`,
localEP,
remoteEP,
localTunnelIP,
remoteTunnelIP,
ttl,
mtu,
remoteEP,
localEP,
ttl,
remoteTunnelIP,
gw,
), nil
}
func (s *Service) installFirewall(t *model.Tunnel, cfg map[string]interface{}) error {
outbound := service.Str(cfg, "outbound_interface")
if outbound == "" {
var err error
outbound, err = service.DefaultRouteInterface()
if err != nil {
return fmt.Errorf("无法确定 NAT 出口接口,请设置 outbound_interface: %w", err)
}
}
comment := fmt.Sprintf("opsx-vpn-gre-%d", t.ID)
remoteSubnet := service.Str(cfg, "remote_subnet")
if remoteSubnet == "" {
// Use the tunnel IP prefix as the source for MASQUERADE
if prefix, err := netip.ParsePrefix(service.Str(cfg, "remote_tunnel_ip")); err == nil {
remoteSubnet = prefix.Masked().String()
}
}
if remoteSubnet != "" {
rules := []service.FirewallRule{
{Table: "filter", Chain: "FORWARD", Args: []string{"-i", ifaceName(t), "-m", "comment", "--comment", comment, "-j", "ACCEPT"}},
{Table: "filter", Chain: "FORWARD", Args: []string{"-o", ifaceName(t), "-m", "conntrack", "--ctstate", "RELATED,ESTABLISHED", "-m", "comment", "--comment", comment, "-j", "ACCEPT"}},
{Table: "nat", Chain: "POSTROUTING", Args: []string{"-s", remoteSubnet, "-o", outbound, "-m", "comment", "--comment", comment, "-j", "MASQUERADE"}},
}
return service.ApplyFirewallRules(rules)
}
return nil
}
func (s *Service) removeFirewall(t *model.Tunnel) {
comment := fmt.Sprintf("opsx-vpn-gre-%d", t.ID)
iptables, ok := service.LookBin("iptables")
if !ok {
return
}
// Remove rules matching our comment in FORWARD and POSTROUTING chains
for _, chain := range []struct{ table, name string }{
{"filter", "FORWARD"},
{"nat", "POSTROUTING"},
} {
for {
out, err := service.Run(iptables, "-t", chain.table, "-L", chain.name, "-n", "--line-numbers")
if err != nil || !strings.Contains(out, comment) {
break
}
for _, line := range strings.Split(out, "\n") {
if !strings.Contains(line, comment) {
continue
}
fields := strings.Fields(line)
if len(fields) > 0 {
_, _ = service.Run(iptables, "-t", chain.table, "-D", chain.name, fields[0])
}
}
break
}
}
}