package vpn import ( "bufio" "crypto/rand" "encoding/json" "fmt" "os" "os/exec" "path/filepath" "strconv" "strings" "sync" "time" ) // EoIPInstance represents a MikroTik Ethernet-over-IP (GRETAP) tunnel type EoIPInstance struct { ID string `json:"id"` Name string `json:"name"` // e.g. "eoip-tunnel1" TunnelID int `json:"tunnel_id"` // 1 - 65535 RemoteAddress string `json:"remote_address"` // Remote router IPv4 LocalAddress string `json:"local_address"` // Local router IPv4 (optional) MACAddress string `json:"mac_address"` // e.g. "02:00:00:00:00:01" MTU int `json:"mtu"` // default 1500 ClampMSS bool `json:"clamp_mss"` // Clamp TCP MSS for PMTUD Comment string `json:"comment,omitempty"` Disabled bool `json:"disabled"` Running bool `json:"running"` // MikroTik 'R' flag RxPackets uint64 `json:"rx_packets"` TxPackets uint64 `json:"tx_packets"` RxBytes uint64 `json:"rx_bytes"` TxBytes uint64 `json:"tx_bytes"` RxHuman string `json:"rx_human,omitempty"` TxHuman string `json:"tx_human,omitempty"` LastError string `json:"last_error,omitempty"` } type EoIPManager struct { mu sync.RWMutex storePath string tunnels map[string]*EoIPInstance } func NewEoIPManager() *EoIPManager { mgr := &EoIPManager{ storePath: "/etc/proxmox-router/vpn/eoip.json", tunnels: make(map[string]*EoIPInstance), } mgr.load() mgr.syncAll() return mgr } func (m *EoIPManager) load() { if data, err := os.ReadFile(m.storePath); err == nil { var list []*EoIPInstance if err := json.Unmarshal(data, &list); err == nil { for _, t := range list { m.tunnels[t.ID] = t } } } } func (m *EoIPManager) save() error { _ = os.MkdirAll(filepath.Dir(m.storePath), 0755) var list []*EoIPInstance for _, t := range m.tunnels { list = append(list, t) } data, err := json.MarshalIndent(list, "", " ") if err != nil { return err } return os.WriteFile(m.storePath, data, 0644) } func (m *EoIPManager) List() []*EoIPInstance { m.mu.Lock() defer m.mu.Unlock() list := make([]*EoIPInstance, 0) for _, t := range m.tunnels { m.updateStats(t) list = append(list, t) } return list } func (m *EoIPManager) Get(id string) (*EoIPInstance, bool) { m.mu.Lock() defer m.mu.Unlock() t, ok := m.tunnels[id] if ok { m.updateStats(t) } return t, ok } func (m *EoIPManager) Save(inst *EoIPInstance) error { m.mu.Lock() defer m.mu.Unlock() if inst.Name == "" { inst.Name = fmt.Sprintf("eoip-tunnel%d", inst.TunnelID) } if inst.TunnelID <= 0 || inst.TunnelID > 65535 { return fmt.Errorf("tunnel_id must be between 1 and 65535") } if inst.RemoteAddress == "" { return fmt.Errorf("remote_address is required") } if inst.MTU <= 0 { inst.MTU = 1500 } if inst.MACAddress == "" { inst.MACAddress = generateMAC() } if inst.ID == "" { inst.ID = fmt.Sprintf("eoip-%d", time.Now().UnixNano()) } // Stop previous interface if name changed or existing if prev, exists := m.tunnels[inst.ID]; exists && prev.Name != inst.Name { m.destroyInterface(prev.Name) } m.tunnels[inst.ID] = inst _ = m.save() return m.applyTunnel(inst) } func (m *EoIPManager) SetState(id string, disabled bool) error { m.mu.Lock() defer m.mu.Unlock() t, ok := m.tunnels[id] if !ok { return fmt.Errorf("tunnel not found") } t.Disabled = disabled _ = m.save() if disabled { m.destroyInterface(t.Name) t.Running = false return nil } return m.applyTunnel(t) } func (m *EoIPManager) Delete(id string) error { m.mu.Lock() defer m.mu.Unlock() t, ok := m.tunnels[id] if !ok { return fmt.Errorf("tunnel not found") } m.destroyInterface(t.Name) delete(m.tunnels, id) return m.save() } func (m *EoIPManager) syncAll() { m.mu.Lock() defer m.mu.Unlock() for _, t := range m.tunnels { if !t.Disabled { _ = m.applyTunnel(t) } else { m.destroyInterface(t.Name) t.Running = false } } } func (m *EoIPManager) applyTunnel(t *EoIPInstance) error { m.destroyInterface(t.Name) if t.Disabled { t.Running = false return nil } // Construct ip link add command: // ip link add type gretap remote [local ] key args := []string{"link", "add", t.Name, "type", "gretap", "remote", t.RemoteAddress} if t.LocalAddress != "" { args = append(args, "local", t.LocalAddress) } args = append(args, "key", strconv.Itoa(t.TunnelID)) if out, err := exec.Command("ip", args...).CombinedOutput(); err != nil { t.LastError = fmt.Sprintf("create failed: %s - %v", strings.TrimSpace(string(out)), err) t.Running = false return fmt.Errorf("%s", t.LastError) } // Set MAC address if provided if t.MACAddress != "" { _ = exec.Command("ip", "link", "set", t.Name, "address", t.MACAddress).Run() } // Set MTU if t.MTU > 0 { _ = exec.Command("ip", "link", "set", t.Name, "mtu", strconv.Itoa(t.MTU)).Run() } // Bring UP if out, err := exec.Command("ip", "link", "set", t.Name, "up").CombinedOutput(); err != nil { t.LastError = fmt.Sprintf("up failed: %s - %v", strings.TrimSpace(string(out)), err) t.Running = false return fmt.Errorf("%s", t.LastError) } // TCP MSS Clamping if enabled if t.ClampMSS { _ = exec.Command("iptables", "-t", "mangle", "-D", "FORWARD", "-o", t.Name, "-p", "tcp", "--tcp-flags", "SYN,RST", "SYN", "-j", "TCPMSS", "--clamp-mss-to-pmtu").Run() _ = exec.Command("iptables", "-t", "mangle", "-A", "FORWARD", "-o", t.Name, "-p", "tcp", "--tcp-flags", "SYN,RST", "SYN", "-j", "TCPMSS", "--clamp-mss-to-pmtu").Run() } t.Running = true t.LastError = "" return nil } func (m *EoIPManager) destroyInterface(name string) { if name == "" { return } _ = exec.Command("iptables", "-t", "mangle", "-D", "FORWARD", "-o", name, "-p", "tcp", "--tcp-flags", "SYN,RST", "SYN", "-j", "TCPMSS", "--clamp-mss-to-pmtu").Run() _ = exec.Command("ip", "link", "del", name).Run() } func (m *EoIPManager) updateStats(t *EoIPInstance) { // Check if interface is up out, err := exec.Command("ip", "link", "show", t.Name).Output() if err != nil || !strings.Contains(string(out), "UP") { t.Running = false } else { t.Running = true } // Read /proc/net/dev for RX/TX bytes file, err := os.Open("/proc/net/dev") if err != nil { return } defer file.Close() scanner := bufio.NewScanner(file) for scanner.Scan() { line := scanner.Text() if strings.Contains(line, t.Name+":") { parts := strings.Fields(line) if len(parts) >= 10 { rxBytes, _ := strconv.ParseUint(parts[1], 10, 64) rxPackets, _ := strconv.ParseUint(parts[2], 10, 64) txBytes, _ := strconv.ParseUint(parts[9], 10, 64) txPackets, _ := strconv.ParseUint(parts[10], 10, 64) t.RxBytes = rxBytes t.RxPackets = rxPackets t.TxBytes = txBytes t.TxPackets = txPackets t.RxHuman = formatBytes(rxBytes) t.TxHuman = formatBytes(txBytes) } break } } } func generateMAC() string { buf := make([]byte, 3) _, _ = rand.Read(buf) return fmt.Sprintf("02:64:%02x:%02x:%02x:%02x", buf[0], buf[1], buf[2], time.Now().UnixNano()%256) } func formatBytes(b uint64) string { const unit = 1024 if b < unit { return fmt.Sprintf("%d B", b) } div, exp := uint64(unit), 0 for n := b / unit; n >= unit; n /= unit { div *= unit exp++ } return fmt.Sprintf("%.1f %cB", float64(b)/float64(div), "KMGTPE"[exp]) }