- 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(构建备份二进制)
329 lines
10 KiB
Go
329 lines
10 KiB
Go
// 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
|
||
}
|
||
}
|
||
}
|