// Package gre 实现 GRE 隧道管理(Linux ip tunnel)。 // GRE 是简单的 IP-over-IP 封装,依赖 iproute2 的 ip 命令。 package gre import ( "fmt" "net/netip" "regexp" "strings" "vpn-platform/backend/internal/core" "vpn-platform/backend/internal/model" "vpn-platform/backend/internal/service" "vpn-platform/backend/internal/store" ) type Service struct{} var interfaceNamePattern = regexp.MustCompile(`^[a-zA-Z0-9_.-]{1,15}$`) 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 localEndpoint, err := netip.ParseAddr(service.Str(out, "local_endpoint")) if err != nil || !localEndpoint.Is4() { return nil, fmt.Errorf("local_endpoint 必须是有效的 IP 地址") } remoteEndpoint, err := netip.ParseAddr(service.Str(out, "remote_endpoint")) if err != nil || !remoteEndpoint.Is4() { return nil, fmt.Errorf("remote_endpoint 必须是有效的 IP 地址") } localTunnel, err := netip.ParsePrefix(service.Str(out, "local_tunnel_ip")) if err != nil || !localTunnel.Addr().Is4() { return nil, fmt.Errorf("local_tunnel_ip 必须是 CIDR 格式(如 10.10.0.1/30)") } remoteTunnel, err := netip.ParsePrefix(service.Str(out, "remote_tunnel_ip")) if err != nil || !remoteTunnel.Addr().Is4() { return nil, fmt.Errorf("remote_tunnel_ip 必须是 CIDR 格式(如 10.10.0.2/30)") } if localTunnel == remoteTunnel || localTunnel.Masked() != remoteTunnel.Masked() || localTunnel.Bits() != remoteTunnel.Bits() { return nil, fmt.Errorf("local_tunnel_ip 与 remote_tunnel_ip 必须位于同一隧道子网") } if remoteSubnet := service.Str(out, "remote_subnet"); remoteSubnet != "" { prefix, err := netip.ParsePrefix(remoteSubnet) if err != nil || !prefix.Addr().Is4() { return nil, fmt.Errorf("remote_subnet 必须是 IPv4 CIDR") } } if outbound := service.Str(out, "outbound_interface"); outbound != "" && !interfaceNamePattern.MatchString(outbound) { return nil, fmt.Errorf("outbound_interface 包含非法字符") } // Defaults if service.Num(out, "ttl", 0) == 0 { out["ttl"] = 255 } if service.Num(out, "mtu", 0) == 0 { out["mtu"] = 1476 } if ttl := int(service.Num(out, "ttl", 255)); ttl < 1 || ttl > 255 { return nil, fmt.Errorf("ttl 必须在 1-255") } if mtu := int(service.Num(out, "mtu", 1476)); mtu < 576 || mtu > 9000 { return nil, fmt.Errorf("mtu 必须在 576-9000") } 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 } } }