OverviewFilesCommitsIssuesDocsReleasesPeers

hyperident-gateway / lib / utun-manager.js

370 lines · javascript ref:
/**
 * UTUNManager - Manages TUN virtual network interface
 *
 * Responsibilities:
 * - Create and configure TUN device
 * - Read IP packets from TUN interface
 * - Write IP packets to TUN interface
 * - Parse packets to determine routing decisions
 * - Integration with GatewayIdentity for service lookup
 */

import UTUN from 'utun'

// Largest packet utun 2.1.0 can read: rx_packet_t holds 2048 - sizeof(uint16_t)
const MAX_TUN_MTU = 2046

export class UTUNManager {
  constructor(config = {}) {
    this.config = {
      deviceName: config.deviceName || 'tun0',
      gatewayIP: config.gatewayIP || '10.22.0.1',
      subnet: config.subnet || '10.22.0.0/16',
      netmask: config.netmask || '255.255.0.0',
      mtu: config.mtu || 1500,
      ...config
    }

    this.gatewayIdentity = config.gatewayIdentity
    this.tun = null
    this.isRunning = false
    this.stats = {
      packetsReceived: 0,
      packetsSent: 0,
      bytesReceived: 0,
      bytesSent: 0,
      packetsDropped: 0,
      errors: 0
    }

    // Packet handlers
    this.onPacketReceived = config.onPacketReceived || (() => {})
    this.onError = config.onError || ((err) => console.error('UTUN Error:', err))
  }

  /**
   * Create and configure the TUN device
   */
  async open() {
    if (this.tun) {
      throw new Error('TUN device already open')
    }

    try {
      // Create TUN device
      this.tun = new UTUN({
        mtu: this.config.mtu
      })

      // Listen for incoming packets
      this.tun.on('data', (packet) => {
        this._handleIncomingPacket(packet)
      })

      // Configure the interface
      await this.configure()

      this.isRunning = true
      console.log(`✅ TUN device created: ${this.tun.info().name}`)
      console.log(`   IP: ${this.config.gatewayIP}`)
      console.log(`   Subnet: ${this.config.subnet}`)
      console.log(`   MTU: ${this.config.mtu}`)

      return this.tun
    } catch (error) {
      this.onError(error)
      throw new Error(`Failed to create TUN device: ${error.message}`)
    }
  }

  /**
   * Configure the TUN interface with IP and routing
   */
  async configure() {
    if (!this.tun) {
      throw new Error('TUN device not created')
    }

    // utun reads each packet into a 2046-byte slot (binding.c rx_packet_t);
    // anything larger would be truncated, so refuse such an MTU outright
    if (this.config.mtu > MAX_TUN_MTU) {
      throw new Error(`MTU ${this.config.mtu} exceeds ${MAX_TUN_MTU}, the largest packet utun can read`)
    }

    try {
      // Configure IP address, netmask and MTU
      // Note: On Linux, this creates an automatic route for the subnet
      await this.tun.configure({
        ip: this.config.gatewayIP,
        netmask: this.config.netmask,
        mtu: this.config.mtu
      })

      console.log(`✅ TUN device configured`)
    } catch (error) {
      throw new Error(`Failed to configure TUN device: ${error.message}`)
    }
  }

  /**
   * Handle incoming packet from TUN interface
   * @private
   */
  _handleIncomingPacket(packet) {
    try {
      this.stats.packetsReceived++
      this.stats.bytesReceived += packet.length

      // Parse IP packet
      const ipPacket = this._parseIPPacket(packet)

      if (!ipPacket) {
        this.stats.packetsDropped++
        return
      }

      // Check if destination is in our subnet
      if (!this._isInSubnet(ipPacket.dstIP)) {
        // Packet destined for outside our network - drop it
        this.stats.packetsDropped++
        return
      }

      // Pass ALL subnet packets to router for P2P forwarding
      // Router will check routing table to find correct peer
      this.onPacketReceived({
        packet,
        ipPacket,
        destIP: ipPacket.dstIP
      })

    } catch (error) {
      this.stats.errors++
      this.onError(error)
    }
  }

  /**
   * Parse IPv4 packet header
   * @private
   */
  _parseIPPacket(buffer) {
    try {
      // IPv4 header is at least 20 bytes
      if (buffer.length < 20) {
        return null
      }

      // Check for IPv4 (version = 4)
      const version = (buffer[0] >> 4) & 0x0F
      if (version !== 4) {
        return null
      }

      const ihl = buffer[0] & 0x0F // Header length in 32-bit words
      const headerLength = ihl * 4

      if (buffer.length < headerLength) {
        return null
      }

      // Extract fields
      const totalLength = (buffer[2] << 8) | buffer[3]
      const protocol = buffer[9]

      // Source IP (bytes 12-15)
      const srcIP = `${buffer[12]}.${buffer[13]}.${buffer[14]}.${buffer[15]}`

      // Destination IP (bytes 16-19)
      const dstIP = `${buffer[16]}.${buffer[17]}.${buffer[18]}.${buffer[19]}`

      // Extract payload
      const payload = buffer.subarray(headerLength)

      return {
        version,
        headerLength,
        totalLength,
        protocol,
        srcIP,
        dstIP,
        payload,
        raw: buffer
      }
    } catch (error) {
      this.onError(new Error(`Failed to parse IP packet: ${error.message}`))
      return null
    }
  }

  /**
   * Check if IP is in our subnet
   * @private
   */
  _isInSubnet(ip) {
    // Parse subnet config (e.g., "10.22.0.0/16")
    const [network, cidr] = this.config.subnet.split('/')
    const networkParts = network.split('.')
    const ipParts = ip.split('.')
    const maskBits = parseInt(cidr)

    // Check octets based on CIDR
    if (maskBits >= 8 && networkParts[0] !== ipParts[0]) return false
    if (maskBits >= 16 && networkParts[1] !== ipParts[1]) return false
    if (maskBits >= 24 && networkParts[2] !== ipParts[2]) return false

    return true
  }

  /**
   * Write packet to TUN interface
   * This is called when we receive a packet from P2P that needs to go to local apps
   */
  async writePacket(packet) {
    if (!this.tun || !this.isRunning) {
      throw new Error('TUN device not running')
    }

    try {
      const written = await this.tun.write(packet)

      if (written > 0) {
        this.stats.packetsSent++
        this.stats.bytesSent += packet.length
      }

      return written
    } catch (error) {
      this.stats.errors++
      this.onError(error)
      throw error
    }
  }

  /**
   * Try to write packet synchronously (may drop if queue full)
   */
  tryWritePacket(packet) {
    if (!this.tun || !this.isRunning) {
      return false
    }

    try {
      const written = this.tun.tryWrite(packet)

      if (written) {
        this.stats.packetsSent++
        this.stats.bytesSent += packet.length
        return true
      } else {
        this.stats.packetsDropped++
        return false
      }
    } catch (error) {
      this.stats.errors++
      this.onError(error)
      return false
    }
  }

  /**
   * Get TUN device statistics
   */
  getStats() {
    const tunInfo = this.tun ? this.tun.info() : null

    return {
      ...this.stats,
      tun: tunInfo,
      isRunning: this.isRunning
    }
  }

  /**
   * Close the TUN device
   */
  async close() {
    if (!this.tun) {
      return
    }

    this.isRunning = false

    return new Promise((resolve) => {
      this.tun.close(() => {
        console.log('✅ TUN device closed')
        this.tun = null
        resolve()
      })
    })
  }

  /**
   * Build an IPv4 packet
   * Utility method for testing and packet construction
   */
  static buildIPv4Packet(options) {
    const {
      srcIP,
      dstIP,
      protocol = 6, // TCP by default
      payload = Buffer.alloc(0)
    } = options

    // Parse IPs
    const src = srcIP.split('.').map(Number)
    const dst = dstIP.split('.').map(Number)

    // Build IP header (20 bytes, no options)
    const header = Buffer.alloc(20)

    header[0] = 0x45 // Version 4, IHL 5 (20 bytes)
    header[1] = 0x00 // DSCP/ECN

    const totalLength = 20 + payload.length
    header[2] = (totalLength >> 8) & 0xFF
    header[3] = totalLength & 0xFF

    header[4] = 0x00 // Identification
    header[5] = 0x00

    header[6] = 0x40 // Flags (Don't Fragment)
    header[7] = 0x00 // Fragment offset

    header[8] = 64 // TTL
    header[9] = protocol

    // Checksum (calculate later)
    header[10] = 0x00
    header[11] = 0x00

    // Source IP
    header[12] = src[0]
    header[13] = src[1]
    header[14] = src[2]
    header[15] = src[3]

    // Destination IP
    header[16] = dst[0]
    header[17] = dst[1]
    header[18] = dst[2]
    header[19] = dst[3]

    // Calculate checksum
    let checksum = 0
    for (let i = 0; i < 20; i += 2) {
      checksum += (header[i] << 8) | header[i + 1]
    }
    while (checksum >> 16) {
      checksum = (checksum & 0xFFFF) + (checksum >> 16)
    }
    checksum = ~checksum & 0xFFFF

    header[10] = (checksum >> 8) & 0xFF
    header[11] = checksum & 0xFF

    // Combine header and payload
    return Buffer.concat([header, payload])
  }
}