| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633 |
- package vpn
- import (
- "crypto/rand"
- "encoding/binary"
- "encoding/json"
- "fmt"
- "log"
- "net"
- "os"
- "os/exec"
- "path/filepath"
- "sort"
- "strconv"
- "strings"
- "sync"
- "sync/atomic"
- "syscall"
- "time"
- )
- // EoIPInstance represents a MikroTik Ethernet-over-IP tunnel instance
- type EoIPInstance struct {
- ID string `json:"id"`
- Name string `json:"name"` // e.g. "Goldix" or "eoip-tunnel1"
- TunnelID int `json:"tunnel_id"` // 1 - 65535
- RemoteAddress string `json:"remote_address"` // Remote MikroTik IPv4
- LocalAddress string `json:"local_address"` // Local router IPv4 (optional)
- IPAddress string `json:"ip_address,omitempty"` // Overlay IP/CIDR (e.g. "172.16.33.254/30")
- MACAddress string `json:"mac_address"` // e.g. "02:64:83:6b:46:44"
- 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
- workers map[string]*eoipTunnelWorker
- }
- func NewEoIPManager() *EoIPManager {
- mgr := &EoIPManager{
- storePath: "/etc/debtik/vpn/eoip.json",
- tunnels: make(map[string]*EoIPInstance),
- workers: make(map[string]*eoipTunnelWorker),
- }
- 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, len(m.tunnels))
- for _, t := range m.tunnels {
- m.updateStats(t)
- list = append(list, t)
- }
- sort.Slice(list, func(i, j int) bool {
- if list[i].TunnelID != list[j].TunnelID {
- return list[i].TunnelID < list[j].TunnelID
- }
- return list[i].Name < list[j].Name
- })
- 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())
- }
- // Smart detection: if user put CIDR into LocalAddress by mistake, move it to IPAddress
- if strings.Contains(inst.LocalAddress, "/") && inst.IPAddress == "" {
- inst.IPAddress = inst.LocalAddress
- inst.LocalAddress = ""
- }
- // Stop previous worker if exists
- if w, exists := m.workers[inst.ID]; exists {
- w.stop()
- delete(m.workers, inst.ID)
- }
- m.tunnels[inst.ID] = inst
- _ = m.save()
- if !inst.Disabled {
- worker, err := newEoIPTunnelWorker(inst)
- if err != nil {
- inst.LastError = err.Error()
- inst.Running = false
- return err
- }
- m.workers[inst.ID] = worker
- go worker.start()
- } else {
- inst.Running = false
- }
- return nil
- }
- 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 w, exists := m.workers[id]; exists {
- w.stop()
- delete(m.workers, id)
- }
- if disabled {
- t.Running = false
- return nil
- }
- worker, err := newEoIPTunnelWorker(t)
- if err != nil {
- t.LastError = err.Error()
- t.Running = false
- return err
- }
- m.workers[id] = worker
- go worker.start()
- return nil
- }
- func (m *EoIPManager) Delete(id string) error {
- m.mu.Lock()
- defer m.mu.Unlock()
- if _, ok := m.tunnels[id]; !ok {
- return fmt.Errorf("tunnel not found")
- }
- if w, exists := m.workers[id]; exists {
- w.stop()
- delete(m.workers, id)
- }
- delete(m.tunnels, id)
- return m.save()
- }
- func (m *EoIPManager) syncAll() {
- m.mu.Lock()
- defer m.mu.Unlock()
- for id, t := range m.tunnels {
- if !t.Disabled {
- if w, exists := m.workers[id]; exists {
- w.stop()
- delete(m.workers, id)
- }
- worker, err := newEoIPTunnelWorker(t)
- if err != nil {
- t.LastError = err.Error()
- t.Running = false
- continue
- }
- m.workers[id] = worker
- go worker.start()
- } else {
- t.Running = false
- }
- }
- }
- func (m *EoIPManager) updateStats(t *EoIPInstance) {
- if w, exists := m.workers[t.ID]; exists {
- t.Running = w.isRunning()
- t.RxBytes = atomic.LoadUint64(&w.rxBytes)
- t.RxPackets = atomic.LoadUint64(&w.rxPackets)
- t.TxBytes = atomic.LoadUint64(&w.txBytes)
- t.TxPackets = atomic.LoadUint64(&w.txPackets)
- } else {
- t.Running = false
- }
- t.RxHuman = formatBytes(t.RxBytes)
- t.TxHuman = formatBytes(t.TxBytes)
- // Detect any bound IPv4 if not explicitly saved in JSON
- if t.IPAddress == "" {
- if ifi, err := net.InterfaceByName(t.Name); err == nil {
- if addrs, err := ifi.Addrs(); err == nil {
- for _, a := range addrs {
- if ipNet, ok := a.(*net.IPNet); ok && ipNet.IP.To4() != nil {
- t.IPAddress = ipNet.String()
- break
- }
- }
- }
- }
- }
- }
- // -------------------------------------------------------------
- // Native MikroTik EoIP Engine Worker
- // -------------------------------------------------------------
- type eoipTunnelWorker struct {
- inst *EoIPInstance
- peerIfName string
- peerIfIndex int
- greFd int
- pktFd int
- stopChan chan struct{}
- lastRx int64 // unix nano
- rxBytes uint64
- rxPackets uint64
- txBytes uint64
- txPackets uint64
- remoteAddrInet syscall.SockaddrInet4
- }
- func newEoIPTunnelWorker(inst *EoIPInstance) (*eoipTunnelWorker, error) {
- // Parse Remote IPv4
- remoteIP := net.ParseIP(inst.RemoteAddress).To4()
- if remoteIP == nil {
- return nil, fmt.Errorf("invalid remote address: %s", inst.RemoteAddress)
- }
- // Internal peer interface name (max 15 chars for Linux ifnames)
- peerName := fmt.Sprintf("ep_%s", inst.Name)
- if len(peerName) > 15 {
- peerName = peerName[:15]
- }
- // 1. Clean up existing links if present
- _ = exec.Command("ip", "link", "del", inst.Name).Run()
- _ = exec.Command("ip", "link", "del", peerName).Run()
- // 2. Create veth pair: inst.Name <--> peerName
- cmdAdd := exec.Command("ip", "link", "add", inst.Name, "type", "veth", "peer", "name", peerName)
- if out, err := cmdAdd.CombinedOutput(); err != nil {
- return nil, fmt.Errorf("failed to create veth pair: %s - %w", string(out), err)
- }
- // 3. Set MAC address and MTU
- if inst.MACAddress != "" {
- _ = exec.Command("ip", "link", "set", inst.Name, "address", inst.MACAddress).Run()
- }
- mtuStr := strconv.Itoa(inst.MTU)
- _ = exec.Command("ip", "link", "set", inst.Name, "mtu", mtuStr).Run()
- _ = exec.Command("ip", "link", "set", peerName, "mtu", mtuStr).Run()
- // 4. Bring both interfaces UP
- _ = exec.Command("ip", "link", "set", inst.Name, "up").Run()
- _ = exec.Command("ip", "link", "set", peerName, "up").Run()
- // 4b. Assign Overlay IP address if specified
- if inst.IPAddress != "" {
- _ = exec.Command("ip", "addr", "add", inst.IPAddress, "dev", inst.Name).Run()
- }
- // 5. TCP MSS Clamping
- if inst.ClampMSS {
- _ = exec.Command("iptables", "-t", "mangle", "-A", "FORWARD", "-o", inst.Name, "-p", "tcp", "--tcp-flags", "SYN,RST", "SYN", "-j", "TCPMSS", "--clamp-mss-to-pmtu").Run()
- }
- // 6. Find interface index of peerName
- peerIf, err := net.InterfaceByName(peerName)
- if err != nil {
- _ = exec.Command("ip", "link", "del", inst.Name).Run()
- return nil, fmt.Errorf("cannot find peer interface %s: %w", peerName, err)
- }
- // 7. Open AF_PACKET raw socket on peer interface
- pktFd, err := syscall.Socket(syscall.AF_PACKET, syscall.SOCK_RAW, int(htons(syscall.ETH_P_ALL)))
- if err != nil {
- _ = exec.Command("ip", "link", "del", inst.Name).Run()
- return nil, fmt.Errorf("cannot create AF_PACKET socket: %w", err)
- }
- pktAddr := syscall.SockaddrLinklayer{
- Protocol: htons(syscall.ETH_P_ALL),
- Ifindex: peerIf.Index,
- }
- if err := syscall.Bind(pktFd, &pktAddr); err != nil {
- syscall.Close(pktFd)
- _ = exec.Command("ip", "link", "del", inst.Name).Run()
- return nil, fmt.Errorf("cannot bind AF_PACKET socket to %s: %w", peerName, err)
- }
- // 8. Open raw GRE socket (IPPROTO_GRE = 47)
- greFd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_RAW, syscall.IPPROTO_GRE)
- if err != nil {
- syscall.Close(pktFd)
- _ = exec.Command("ip", "link", "del", inst.Name).Run()
- return nil, fmt.Errorf("cannot create raw GRE socket: %w", err)
- }
- // Bind GRE socket to LocalAddress if specified
- if inst.LocalAddress != "" {
- localIP := net.ParseIP(inst.LocalAddress).To4()
- if localIP != nil {
- localAddr := &syscall.SockaddrInet4{
- Addr: [4]byte{localIP[0], localIP[1], localIP[2], localIP[3]},
- }
- _ = syscall.Bind(greFd, localAddr)
- }
- }
- w := &eoipTunnelWorker{
- inst: inst,
- peerIfName: peerName,
- peerIfIndex: peerIf.Index,
- greFd: greFd,
- pktFd: pktFd,
- stopChan: make(chan struct{}),
- remoteAddrInet: syscall.SockaddrInet4{
- Addr: [4]byte{remoteIP[0], remoteIP[1], remoteIP[2], remoteIP[3]},
- },
- rxBytes: inst.RxBytes,
- rxPackets: inst.RxPackets,
- txBytes: inst.TxBytes,
- txPackets: inst.TxPackets,
- }
- return w, nil
- }
- func (w *eoipTunnelWorker) start() {
- log.Printf("[EoIP] Started tunnel '%s' (ID: %d, Remote: %s)", w.inst.Name, w.inst.TunnelID, w.inst.RemoteAddress)
- // Goroutine 1: Forward packets from Local veth -> Remote MikroTik via GRE
- go w.txLoop()
- // Goroutine 2: Receive packets from Remote MikroTik via GRE -> Inject to Local veth
- go w.rxLoop()
- // Goroutine 3: Periodic MikroTik EoIP Keepalive
- go w.keepaliveLoop()
- }
- func (w *eoipTunnelWorker) isRunning() bool {
- last := atomic.LoadInt64(&w.lastRx)
- if last == 0 {
- return false
- }
- return time.Since(time.Unix(0, last)) < 15*time.Second
- }
- func (w *eoipTunnelWorker) sendKeepalive() {
- // MikroTik EoIP Keepalive packet:
- // GRE Flags: 0x2001
- // Protocol: 0x6400
- // Length: 0x0000
- // Tunnel ID: Little-Endian
- tid := uint16(w.inst.TunnelID)
- pkt := []byte{
- 0x20, 0x01,
- 0x64, 0x00,
- 0x00, 0x00,
- byte(tid & 0xff), byte(tid >> 8),
- }
- _ = syscall.Sendto(w.greFd, pkt, 0, &w.remoteAddrInet)
- }
- func (w *eoipTunnelWorker) keepaliveLoop() {
- ticker := time.NewTicker(5 * time.Second)
- defer ticker.Stop()
- // Send immediate initial keepalive
- w.sendKeepalive()
- for {
- select {
- case <-w.stopChan:
- return
- case <-ticker.C:
- w.sendKeepalive()
- }
- }
- }
- func (w *eoipTunnelWorker) txLoop() {
- buf := make([]byte, 2048)
- tid := uint16(w.inst.TunnelID)
- for {
- select {
- case <-w.stopChan:
- return
- default:
- }
- n, from, err := syscall.Recvfrom(w.pktFd, buf, 0)
- if err != nil {
- select {
- case <-w.stopChan:
- return
- default:
- time.Sleep(10 * time.Millisecond)
- continue
- }
- }
- // Filter out outgoing packets to prevent reflection
- if fromLink, ok := from.(*syscall.SockaddrLinklayer); ok {
- if fromLink.Pkttype == syscall.PACKET_OUTGOING {
- continue
- }
- }
- // Must have at least minimum Ethernet header length (14 bytes)
- if n < 14 {
- continue
- }
- // Construct MikroTik EoIP Header (8 bytes) + Ethernet Frame
- // [0..1] = 0x2001 (Flags)
- // [2..3] = 0x6400 (Protocol)
- // [4..5] = payload length (Big-Endian)
- // [6..7] = tunnel ID (Little-Endian)
- grePkt := make([]byte, 8+n)
- grePkt[0] = 0x20
- grePkt[1] = 0x01
- grePkt[2] = 0x64
- grePkt[3] = 0x00
- binary.BigEndian.PutUint16(grePkt[4:6], uint16(n))
- binary.LittleEndian.PutUint16(grePkt[6:8], tid)
- copy(grePkt[8:], buf[:n])
- // Send over raw GRE socket
- if err := syscall.Sendto(w.greFd, grePkt, 0, &w.remoteAddrInet); err == nil {
- atomic.AddUint64(&w.txPackets, 1)
- atomic.AddUint64(&w.txBytes, uint64(n))
- }
- }
- }
- func (w *eoipTunnelWorker) rxLoop() {
- buf := make([]byte, 4096)
- expectedTid := uint16(w.inst.TunnelID)
- remoteIP := net.ParseIP(w.inst.RemoteAddress).To4()
- injectAddr := syscall.SockaddrLinklayer{
- Protocol: htons(syscall.ETH_P_ALL),
- Ifindex: w.peerIfIndex,
- Halen: 6,
- }
- for {
- select {
- case <-w.stopChan:
- return
- default:
- }
- n, from, err := syscall.Recvfrom(w.greFd, buf, 0)
- if err != nil {
- select {
- case <-w.stopChan:
- return
- default:
- time.Sleep(10 * time.Millisecond)
- continue
- }
- }
- // RAW IPv4 socket Recvfrom returns [IPv4 Header (20+ bytes)] + [GRE Header (8 bytes)] + [Payload]
- if n < 28 {
- continue
- }
- // Verify remote sender IP
- if fromIP, ok := from.(*syscall.SockaddrInet4); ok {
- if fromIP.Addr[0] != remoteIP[0] || fromIP.Addr[1] != remoteIP[1] ||
- fromIP.Addr[2] != remoteIP[2] || fromIP.Addr[3] != remoteIP[3] {
- continue
- }
- }
- // Calculate IPv4 Header Length
- ipHdrLen := int(buf[0]&0x0f) * 4
- if n < ipHdrLen+8 {
- continue
- }
- gre := buf[ipHdrLen:n]
- // Verify MikroTik EoIP GRE Header
- // Flags: 0x2001 (or 0x2000)
- // Proto: 0x6400
- if gre[0] != 0x20 || (gre[1] != 0x01 && gre[1] != 0x00) {
- continue
- }
- if gre[2] != 0x64 || gre[3] != 0x00 {
- continue
- }
- frameLen := binary.BigEndian.Uint16(gre[4:6])
- tunnelID := binary.LittleEndian.Uint16(gre[6:8])
- if tunnelID != expectedTid {
- continue
- }
- // Record successful packet reception
- now := time.Now().UnixNano()
- atomic.StoreInt64(&w.lastRx, now)
- if frameLen == 0 {
- // Keepalive probe from MikroTik: reply with keepalive
- w.sendKeepalive()
- continue
- }
- // Valid Ethernet frame received from MikroTik EoIP tunnel
- ethFrame := gre[8:]
- if len(ethFrame) < int(frameLen) {
- continue
- }
- payload := ethFrame[:frameLen]
- // Inject Ethernet frame into veth peer -> kernel delivers it to user interface (w.inst.Name)
- if err := syscall.Sendto(w.pktFd, payload, 0, &injectAddr); err == nil {
- atomic.AddUint64(&w.rxPackets, 1)
- atomic.AddUint64(&w.rxBytes, uint64(frameLen))
- }
- }
- }
- func (w *eoipTunnelWorker) stop() {
- close(w.stopChan)
- _ = syscall.Close(w.pktFd)
- _ = syscall.Close(w.greFd)
- // Clean up interfaces & iptables
- if w.inst.ClampMSS {
- _ = exec.Command("iptables", "-t", "mangle", "-D", "FORWARD", "-o", w.inst.Name, "-p", "tcp", "--tcp-flags", "SYN,RST", "SYN", "-j", "TCPMSS", "--clamp-mss-to-pmtu").Run()
- }
- _ = exec.Command("ip", "link", "del", w.inst.Name).Run()
- log.Printf("[EoIP] Stopped tunnel '%s'", w.inst.Name)
- }
- func htons(n uint16) uint16 {
- return (n<<8)&0xff00 | (n>>8)&0x00ff
- }
- 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])
- }
|