|
|
@@ -1,27 +1,30 @@
|
|
|
package vpn
|
|
|
|
|
|
import (
|
|
|
- "bufio"
|
|
|
"crypto/rand"
|
|
|
+ "encoding/binary"
|
|
|
"encoding/json"
|
|
|
"fmt"
|
|
|
+ "log"
|
|
|
+ "net"
|
|
|
"os"
|
|
|
"os/exec"
|
|
|
"path/filepath"
|
|
|
"strconv"
|
|
|
- "strings"
|
|
|
"sync"
|
|
|
+ "sync/atomic"
|
|
|
+ "syscall"
|
|
|
"time"
|
|
|
)
|
|
|
|
|
|
-// EoIPInstance represents a MikroTik Ethernet-over-IP (GRETAP) tunnel
|
|
|
+// EoIPInstance represents a MikroTik Ethernet-over-IP tunnel instance
|
|
|
type EoIPInstance struct {
|
|
|
ID string `json:"id"`
|
|
|
- Name string `json:"name"` // e.g. "eoip-tunnel1"
|
|
|
+ Name string `json:"name"` // e.g. "Goldix" or "eoip-tunnel1"
|
|
|
TunnelID int `json:"tunnel_id"` // 1 - 65535
|
|
|
- RemoteAddress string `json:"remote_address"` // Remote router IPv4
|
|
|
+ RemoteAddress string `json:"remote_address"` // Remote MikroTik IPv4
|
|
|
LocalAddress string `json:"local_address"` // Local router IPv4 (optional)
|
|
|
- MACAddress string `json:"mac_address"` // e.g. "02:00:00:00:00:01"
|
|
|
+ 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"`
|
|
|
@@ -40,12 +43,14 @@ type EoIPManager struct {
|
|
|
mu sync.RWMutex
|
|
|
storePath string
|
|
|
tunnels map[string]*EoIPInstance
|
|
|
+ workers map[string]*eoipTunnelWorker
|
|
|
}
|
|
|
|
|
|
func NewEoIPManager() *EoIPManager {
|
|
|
mgr := &EoIPManager{
|
|
|
storePath: "/etc/proxmox-router/vpn/eoip.json",
|
|
|
tunnels: make(map[string]*EoIPInstance),
|
|
|
+ workers: make(map[string]*eoipTunnelWorker),
|
|
|
}
|
|
|
mgr.load()
|
|
|
mgr.syncAll()
|
|
|
@@ -80,7 +85,7 @@ func (m *EoIPManager) List() []*EoIPInstance {
|
|
|
m.mu.Lock()
|
|
|
defer m.mu.Unlock()
|
|
|
|
|
|
- list := make([]*EoIPInstance, 0)
|
|
|
+ list := make([]*EoIPInstance, 0, len(m.tunnels))
|
|
|
for _, t := range m.tunnels {
|
|
|
m.updateStats(t)
|
|
|
list = append(list, t)
|
|
|
@@ -122,15 +127,29 @@ func (m *EoIPManager) Save(inst *EoIPInstance) error {
|
|
|
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)
|
|
|
+ // 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()
|
|
|
|
|
|
- return m.applyTunnel(inst)
|
|
|
+ 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 {
|
|
|
@@ -144,23 +163,40 @@ func (m *EoIPManager) SetState(id string, disabled bool) error {
|
|
|
t.Disabled = disabled
|
|
|
_ = m.save()
|
|
|
|
|
|
+ if w, exists := m.workers[id]; exists {
|
|
|
+ w.stop()
|
|
|
+ delete(m.workers, id)
|
|
|
+ }
|
|
|
+
|
|
|
if disabled {
|
|
|
- m.destroyInterface(t.Name)
|
|
|
t.Running = false
|
|
|
return nil
|
|
|
}
|
|
|
- return m.applyTunnel(t)
|
|
|
+
|
|
|
+ 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()
|
|
|
|
|
|
- t, ok := m.tunnels[id]
|
|
|
- if !ok {
|
|
|
+ if _, ok := m.tunnels[id]; !ok {
|
|
|
return fmt.Errorf("tunnel not found")
|
|
|
}
|
|
|
- m.destroyInterface(t.Name)
|
|
|
+
|
|
|
+ if w, exists := m.workers[id]; exists {
|
|
|
+ w.stop()
|
|
|
+ delete(m.workers, id)
|
|
|
+ }
|
|
|
+
|
|
|
delete(m.tunnels, id)
|
|
|
return m.save()
|
|
|
}
|
|
|
@@ -169,111 +205,376 @@ func (m *EoIPManager) syncAll() {
|
|
|
m.mu.Lock()
|
|
|
defer m.mu.Unlock()
|
|
|
|
|
|
- for _, t := range m.tunnels {
|
|
|
+ for id, t := range m.tunnels {
|
|
|
if !t.Disabled {
|
|
|
- _ = m.applyTunnel(t)
|
|
|
+ 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 {
|
|
|
- m.destroyInterface(t.Name)
|
|
|
t.Running = false
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-func (m *EoIPManager) applyTunnel(t *EoIPInstance) error {
|
|
|
- m.destroyInterface(t.Name)
|
|
|
-
|
|
|
- if t.Disabled {
|
|
|
+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
|
|
|
- return nil
|
|
|
}
|
|
|
+ t.RxHuman = formatBytes(t.RxBytes)
|
|
|
+ t.TxHuman = formatBytes(t.TxBytes)
|
|
|
+}
|
|
|
+
|
|
|
+// -------------------------------------------------------------
|
|
|
+// 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
|
|
|
+}
|
|
|
|
|
|
- // Construct ip link add command:
|
|
|
- // ip link add <name> type gretap remote <remote_ip> [local <local_ip>] key <tunnel_id>
|
|
|
- args := []string{"link", "add", t.Name, "type", "gretap", "remote", t.RemoteAddress}
|
|
|
- if t.LocalAddress != "" {
|
|
|
- args = append(args, "local", t.LocalAddress)
|
|
|
+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)
|
|
|
}
|
|
|
- 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)
|
|
|
+ // Internal peer interface name (max 15 chars for Linux ifnames)
|
|
|
+ peerName := fmt.Sprintf("ep_%s", inst.Name)
|
|
|
+ if len(peerName) > 15 {
|
|
|
+ peerName = peerName[:15]
|
|
|
}
|
|
|
|
|
|
- // Set MAC address if provided
|
|
|
- if t.MACAddress != "" {
|
|
|
- _ = exec.Command("ip", "link", "set", t.Name, "address", t.MACAddress).Run()
|
|
|
+ // 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)
|
|
|
}
|
|
|
|
|
|
- // Set MTU
|
|
|
- if t.MTU > 0 {
|
|
|
- _ = exec.Command("ip", "link", "set", t.Name, "mtu", strconv.Itoa(t.MTU)).Run()
|
|
|
+ // 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()
|
|
|
|
|
|
- // 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)
|
|
|
+ // 4. Bring both interfaces UP
|
|
|
+ _ = exec.Command("ip", "link", "set", inst.Name, "up").Run()
|
|
|
+ _ = exec.Command("ip", "link", "set", peerName, "up").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()
|
|
|
}
|
|
|
|
|
|
- // 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()
|
|
|
+ // 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)
|
|
|
}
|
|
|
|
|
|
- t.Running = true
|
|
|
- t.LastError = ""
|
|
|
- return nil
|
|
|
+ // 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 (m *EoIPManager) destroyInterface(name string) {
|
|
|
- if name == "" {
|
|
|
- return
|
|
|
+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
|
|
|
}
|
|
|
- _ = 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()
|
|
|
+ return time.Since(time.Unix(0, last)) < 15*time.Second
|
|
|
}
|
|
|
|
|
|
-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
|
|
|
+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)
|
|
|
+}
|
|
|
|
|
|
- // 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)
|
|
|
+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
|
|
|
}
|
|
|
- break
|
|
|
}
|
|
|
+
|
|
|
+ // 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 {
|