/**
* 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])
}
}