Skip to content

High-performance C11 library for L2↔L3 packet translation with ARP virtualization, DHCP learning, IPv6/NDP support, and DNS caching

Notifications You must be signed in to change notification settings

SoftEtherUnofficial/VirtualTap

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

16 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

VirtualTap

CI License Platform Language

Layer 2/Layer 3 bridge for mobile VPN clients

VirtualTap is a high-performance, zero-dependency C11 library that enables SoftEther VPN (Layer 2) to work seamlessly on iOS and Android (Layer 3-only platforms). It provides bidirectional Ethernet ↔ IP packet translation with intelligent ARP handling, DHCP learning, IPv6 support with NDP, and advanced features like DNS caching and fragment reassembly.


🎯 Why VirtualTap?

The Problem: SoftEther VPN operates at Layer 2 (Ethernet frames with MAC addresses), but iOS and Android only expose Layer 3 interfaces (raw IP packets). This fundamental mismatch prevents direct integration.

The Solution: VirtualTap bridges this gap by:

Feature Description Benefit
🔄 L2↔L3 Translation Bidirectional Ethernet ↔ IP conversion Seamless protocol bridging
🎭 ARP Virtualization Internal ARP table + request/reply handling No platform ARP support needed
🧠 Smart Learning Auto-learns IP, MAC, gateway from traffic Zero manual configuration
📦 Fragment Handling IPv4/IPv6 reassembly (up to 64KB) Supports large MTUs
🌐 Full IPv6 Support NDP (NS/NA/RA) + address learning Modern network compatibility
DNS Caching LRU cache (256 entries, 5min TTL) Reduced latency
🔒 Thread-Safe No global state, instance isolation Production-grade reliability
📦 Zero Dependencies Pure C11, stdlib only Easy integration

📊 Quick Stats

Lines of Code:     ~3,200 (production-tested)
Memory Footprint:  ~2.3MB per instance
Packet Latency:    <5µs (IP↔Ethernet), <50µs (fragment reassembly)
Test Coverage:     14 comprehensive unit tests
Platforms:         iOS 15+, Android 5+, macOS, Linux
Status:            ✅ Production-ready (November 2025)

🚀 Quick Start

Installation

Option 1: Build from source

git clone https://github.com/SoftEtherUnofficial/VirtualTap.git
cd VirtualTap
make

Option 2: Include in Xcode project (iOS)

  1. Add VirtualTap/ directory to your project
  2. Add include/ to Header Search Paths
  3. Link libvirtualtap_ios.a in Build Phases

Option 3: Include in Android NDK project

  1. Copy VirtualTap/ to app/src/main/cpp/
  2. Add to CMakeLists.txt:
add_library(virtualtap STATIC
    VirtualTap/src/virtual_tap.c
    VirtualTap/src/arp_handler.c
    VirtualTap/src/translator.c
    VirtualTap/src/dhcp_parser.c
    VirtualTap/src/ip_utils.c
    VirtualTap/src/icmpv6_handler.c
    VirtualTap/src/dns_handler.c
    VirtualTap/src/fragment_handler.c
    VirtualTap/src/icmp_handler.c
)
target_include_directories(virtualtap PUBLIC VirtualTap/include)

Build Targets

make           # Build libvirtualtap.a (native platform)
make ios       # Build libvirtualtap_ios.a (iOS arm64 cross-compile)
make test      # Build and run 14 unit tests
make clean     # Remove build artifacts

Basic Usage

#include "virtual_tap.h"

// 1. Create instance
VirtualTapConfig config = {
    .our_mac = {0x02, 0x00, 0x5E, 0x10, 0x20, 0x30},  // Virtual MAC
    .our_ip = 0,                // Auto-learned from DHCP
    .gateway_ip = 0,            // Auto-learned from traffic
    .handle_arp = true,         // Enable ARP virtualization
    .learn_ip = true,           // Enable DHCP learning
    .learn_gateway_mac = true,  // Learn gateway from traffic
    .verbose = false            // Disable debug logging
};
VirtualTap* tap = virtual_tap_create(&config);

// 2. Outgoing: IP packet → Ethernet frame (for VPN server)
uint8_t eth_frame[2048];
int32_t eth_len = virtual_tap_ip_to_ethernet(
    tap, ip_packet, ip_len, eth_frame, sizeof(eth_frame)
);
if (eth_len > 0) {
    send_to_vpn_server(eth_frame, eth_len);
}

// 3. Incoming: Ethernet frame → IP packet (for mobile OS)
uint8_t ip_packet[2048];
int32_t ip_len = virtual_tap_ethernet_to_ip(
    tap, eth_frame, eth_len, ip_packet, sizeof(ip_packet)
);
if (ip_len > 0) {
    send_to_mobile_os(ip_packet, ip_len);
} else if (ip_len == 0) {
    // ARP handled internally, check for pending replies
}

// 4. Handle ARP replies (send to VPN server)
while (virtual_tap_has_pending_arp_reply(tap)) {
    uint8_t arp_reply[42];
    int32_t arp_len = virtual_tap_pop_arp_reply(tap, arp_reply, sizeof(arp_reply));
    if (arp_len > 0) {
        send_to_vpn_server(arp_reply, arp_len);
    }
}

// 5. Query learned configuration
uint32_t our_ip = virtual_tap_get_learned_ip(tap);  // From DHCP
uint8_t gateway_mac[6];
bool has_gateway = virtual_tap_get_gateway_mac(tap, gateway_mac);

// 6. Get statistics
VirtualTapStats stats;
virtual_tap_get_stats(tap, &stats);
printf("Packets: %llu IP→Eth, %llu Eth→IP\n", 
       stats.ip_to_eth_packets, stats.eth_to_ip_packets);

// 7. Cleanup
virtual_tap_destroy(tap);

🏗️ Architecture

System Overview

┌──────────────────────────────────────────────────────────────┐
│                      Mobile VPN App                          │
│  ┌────────────────────────────────────────────────────────┐  │
│  │           NEPacketTunnelProvider (iOS)                 │  │
│  │              VpnService (Android)                      │  │
│  └────────────────┬───────────────────┬───────────────────┘  │
│                   │ IP packets        │ IP packets           │
│                   ↓                   ↑                      │
│  ┌────────────────────────────────────────────────────────┐  │
│  │                    VirtualTap                          │  │
│  │  ┌──────────┐  ┌────────────┐  ┌──────────┐  ┌──────┐  │  │
│  │  │   ARP    │  │    L2↔L3   │  │  DHCP    │  │ DNS  │  │  │
│  │  │  Handler │  │ Translator │  │  Parser  │  │Cache │  │  │
│  │  │          │  │            │  │          │  │      │  │  │
│  │  │ • Table  │  │ • Add hdr  │  │ • Learn  │  │ • LRU│  │  │
│  │  │ • Lookup │  │ • Strip    │  │   IP     │  │ • 256│  │  │
│  │  │ • Reply  │  │ • MAC      │  │ • Gateway│  │  ents│  │  │
│  │  └──────────┘  └────────────┘  └──────────┘  └──────┘  │  │
│  │                                                        │  │
│  │  ┌──────────┐  ┌────────────┐  ┌──────────┐  ┌──────┐  │  │
│  │  │  ICMPv6  │  │ Fragment   │  │   ICMP   │  │ IPv6 │  │  │
│  │  │   NDP    │  │ Reassembly │  │  Errors  │  │Learn │  │  │
│  │  │          │  │            │  │          │  │      │  │  │
│  │  │ • NS/NA  │  │ • IPv4/v6  │  │ • MTU    │  │ • RA │  │  │
│  │  │ • RA     │  │ • 32 chains│  │ • Unreach│  │ • GW │  │  │
│  │  └──────────┘  └────────────┘  └──────────┘  └──────┘  │  │
│  └────────────────┬───────────────────┬───────────────────┘  │
│                   │ Ethernet frames   │ Ethernet frames      │
│                   ↓                   ↑                      │
│  ┌────────────────────────────────────────────────────────┐  │
│  │              SoftEther VPN Client                      │  │
│  └────────────────────────────────────────────────────────┘  │
└──────────────────────────────────────────────────────────────┘
                             ↕
                    Internet (VPN tunnel)
                             ↕
                   SoftEther VPN Server

Data Flow

Outgoing: Device → Server

Mobile App (NEPacketTunnelProvider)
    ↓
[IP packet: 192.168.1.100 → 8.8.8.8]
    ↓
virtual_tap_ip_to_ethernet()
    ├─ Extract IP src/dst
    ├─ Lookup gateway MAC in ARP table
    ├─ Add 14-byte Ethernet header
    └─ Learn src IP if DHCP
    ↓
[Ethernet frame: MAC_us → MAC_gateway, EtherType=0x0800, IP payload]
    ↓
SoftEther VPN Client
    ↓
VPN Tunnel → Server

Incoming: Server → Device

VPN Server
    ↓
SoftEther VPN Client
    ↓
[Ethernet frame: MAC_gateway → MAC_us, EtherType=0x0800]
    ↓
virtual_tap_ethernet_to_ip()
    ├─ Parse Ethernet header
    ├─ Check EtherType:
    │   ├─ 0x0800 (IPv4) → Strip header, return IP packet
    │   ├─ 0x86DD (IPv6) → Strip header, return IP packet
    │   └─ 0x0806 (ARP) → Handle internally:
    │       ├─ Parse ARP request
    │       ├─ Build ARP reply
    │       ├─ Queue reply for sending
    │       └─ Return 0 (handled)
    ├─ Learn gateway MAC from src
    └─ Update statistics
    ↓
[IP packet: 8.8.8.8 → 192.168.1.100]
    ↓
Mobile App (write to TUN interface)

ARP Cycle

VPN Server sends ARP request:
    "Who has 192.168.1.100? Tell 192.168.1.1"
    ↓
VirtualTap.ethernet_to_ip()
    ├─ Detect EtherType 0x0806 (ARP)
    ├─ Parse: target_ip = 192.168.1.100
    ├─ Check if target_ip matches our_ip
    ├─ Build ARP reply:
    │   "192.168.1.100 is at 02:00:5E:10:20:30"
    ├─ Queue reply in arp_reply_queue
    └─ Return 0 (handled internally)
    ↓
App checks: virtual_tap_has_pending_arp_reply() → true
    ↓
App calls: virtual_tap_pop_arp_reply()
    ↓
[ARP reply frame: 42 bytes]
    ↓
Send to VPN Server

✨ Features

Protocol Support

Protocol Status Features
IPv4 ✅ Full Header parsing, address learning, fragmentation
IPv6 ✅ Full NDP (NS/NA/RA), address learning, fragmentation
ARP ✅ Full Request/reply handling, table with timeout (5min)
DHCP ✅ Parse Extract IP, gateway, subnet, DNS from OFFER/ACK
ICMPv6 ✅ Full Neighbor Discovery (NS/NA), Router Advertisement
DNS ✅ Cache LRU cache (256 entries, 5min TTL)
ICMP ✅ Parse Error messages (MTU, unreachable, time exceeded)

Advanced Features

🧩 Fragment Reassembly

  • IPv4 & IPv6 fragmentation handling
  • 32 concurrent chains (16 IPv4 + 16 IPv6)
  • Up to 64KB payload per chain
  • 30-second timeout for incomplete chains
  • Automatic cleanup of expired fragments

🌐 IPv6 Support

  • Address Learning: Learns global IPv6 from outgoing packets (skips link-local)
  • NDP Handling:
    • Responds to Neighbor Solicitation (NS) with Neighbor Advertisement (NA)
    • Parses Router Advertisement (RA) for prefix/gateway/DNS
  • Gateway Learning: Learns gateway MAC from IPv6 traffic
  • Dual-Stack: Simultaneous IPv4/IPv6 operation

🎯 DNS Caching

  • LRU eviction (Least Recently Used)
  • 256 entries (configurable)
  • 5-minute TTL (Time To Live)
  • Query parsing: Extracts domain names from DNS queries
  • ~2µs lookup time

🔍 ICMP Error Handling

  • Path MTU Discovery: Parse ICMP "Fragmentation Needed" messages
  • Unreachable Detection: Track destination/port unreachable errors
  • Troubleshooting: Detailed error statistics for debugging

📚 API Reference

Core Functions

Instance Management

/**
 * Create a VirtualTap instance
 * @param config Configuration (MAC, IP settings, flags)
 * @return VirtualTap pointer or NULL on failure
 */
VirtualTap* virtual_tap_create(const VirtualTapConfig* config);

/**
 * Destroy a VirtualTap instance and free resources
 * @param tap VirtualTap instance
 */
void virtual_tap_destroy(VirtualTap* tap);

Packet Translation

/**
 * Convert IP packet to Ethernet frame (add 14-byte header)
 * @param tap VirtualTap instance
 * @param ip_packet Input IP packet buffer
 * @param ip_len IP packet length
 * @param eth_frame_out Output Ethernet frame buffer
 * @param out_capacity Output buffer capacity
 * @return Ethernet frame length (>0), or error code (<0)
 */
int32_t virtual_tap_ip_to_ethernet(
    VirtualTap* tap,
    const uint8_t* ip_packet,
    uint32_t ip_len,
    uint8_t* eth_frame_out,
    uint32_t out_capacity
);

/**
 * Convert Ethernet frame to IP packet (strip header, handle ARP)
 * @param tap VirtualTap instance
 * @param eth_frame Input Ethernet frame buffer
 * @param eth_len Ethernet frame length
 * @param ip_packet_out Output IP packet buffer
 * @param out_capacity Output buffer capacity
 * @return IP packet length (>0), 0 if handled internally (ARP), 
 *         or error code (<0)
 */
int32_t virtual_tap_ethernet_to_ip(
    VirtualTap* tap,
    const uint8_t* eth_frame,
    uint32_t eth_len,
    uint8_t* ip_packet_out,
    uint32_t out_capacity
);

ARP Reply Management

/**
 * Check if there are pending ARP replies
 * @param tap VirtualTap instance
 * @return true if replies are queued
 */
bool virtual_tap_has_pending_arp_reply(VirtualTap* tap);

/**
 * Pop the next ARP reply from the queue
 * @param tap VirtualTap instance
 * @param arp_reply_out Output buffer for ARP reply (42 bytes)
 * @param out_capacity Output buffer capacity
 * @return ARP reply length (42), or 0 if queue empty
 */
int32_t virtual_tap_pop_arp_reply(
    VirtualTap* tap,
    uint8_t* arp_reply_out,
    uint32_t out_capacity
);

Configuration Queries

/**
 * Get learned IP address (from DHCP or outgoing packets)
 * @param tap VirtualTap instance
 * @return IP address in host byte order, or 0 if not learned
 */
uint32_t virtual_tap_get_learned_ip(VirtualTap* tap);

/**
 * Get learned gateway MAC address
 * @param tap VirtualTap instance
 * @param mac_out Output buffer for MAC (6 bytes)
 * @return true if gateway MAC is known
 */
bool virtual_tap_get_gateway_mac(VirtualTap* tap, uint8_t mac_out[6]);

/**
 * Get statistics (packet counts, cache hits, etc.)
 * @param tap VirtualTap instance
 * @param stats Output statistics structure
 */
void virtual_tap_get_stats(VirtualTap* tap, VirtualTapStats* stats);

Configuration Structure

typedef struct {
    uint8_t our_mac[6];         // Virtual MAC address (default: random)
    uint32_t our_ip;            // Our IP (0 = learn from DHCP)
    uint32_t gateway_ip;        // Gateway IP (0 = learn from traffic)
    bool handle_arp;            // Enable ARP virtualization
    bool learn_ip;              // Learn IP from DHCP
    bool learn_gateway_mac;     // Learn gateway MAC from traffic
    bool verbose;               // Enable debug logging
} VirtualTapConfig;

Statistics Structure

typedef struct {
    uint64_t ip_to_eth_packets;       // IP → Ethernet conversions
    uint64_t eth_to_ip_packets;       // Ethernet → IP conversions
    uint64_t arp_requests_handled;    // ARP requests answered
    uint64_t arp_replies_sent;        // ARP replies sent to server
    uint64_t ipv4_packets;            // IPv4 packets processed
    uint64_t ipv6_packets;            // IPv6 packets processed
    uint64_t icmpv6_packets;          // ICMPv6 NDP packets (NS/NA/RA)
    uint64_t arp_packets;             // ARP packets processed
    uint64_t dhcp_packets;            // DHCP packets parsed
    uint64_t dns_queries;             // DNS queries intercepted
    uint64_t dns_cache_hits;          // DNS cache hits
    uint64_t dns_cache_misses;        // DNS cache misses
    uint64_t ipv4_fragments;          // IPv4 fragments received
    uint64_t ipv6_fragments;          // IPv6 fragments received
    uint64_t fragments_reassembled;   // Complete fragment chains
    uint64_t icmp_errors_received;    // ICMP error messages
    uint64_t icmpv6_errors_received;  // ICMPv6 error messages
    uint64_t arp_table_entries;       // Current ARP table size
    uint64_t other_packets;           // Unknown protocol packets
} VirtualTapStats;

Error Codes

#define VTAP_ERROR_INVALID_PARAMS    -1  // NULL pointer or invalid parameters
#define VTAP_ERROR_PARSE_FAILED      -2  // Packet parsing failed
#define VTAP_ERROR_BUFFER_TOO_SMALL  -3  // Output buffer too small
#define VTAP_ERROR_ALLOC_FAILED      -4  // Memory allocation failed

⚡ Performance

Memory Footprint

Component Size Details
VirtualTap instance ~8 KB Core state machine
ARP table ~4 KB 64 entries × 64 bytes
DNS cache ~16 KB 256 entries × 64 bytes
Fragment handlers ~2.2 MB 32 chains × 65KB buffers
ARP reply queue ~500 B Typical usage
Total per instance ~2.3 MB Production footprint

CPU Performance

Operation Latency Notes
IP → Ethernet <5 µs Add header, ARP lookup
Ethernet → IP <5 µs Strip header, learn MAC
ARP lookup <1 µs Linear search (64 entries)
ARP reply build ~10 µs Generate 42-byte frame
DNS cache lookup <2 µs LRU with 256 entries
Fragment check <1 µs Hash table lookup
Fragment reassembly ~50 µs When complete
ICMPv6 RA parse ~80 µs Extract prefix/gateway
ICMPv6 NA response ~55 µs Build response

Benchmark Environment: Apple M1 Pro, macOS 14.6, Clang 15.0, -O2

Throughput

  • Sustained rate: 1M+ packets/sec on modern hardware
  • Bottleneck: Typically VPN tunnel latency, not VirtualTap
  • CPU usage: <1% on mobile devices at typical VPN speeds (10-100 Mbps)

🧪 Testing

Unit Test Suite

$ make test
=== VirtualTap C Implementation Tests ===

✅ Test 1: Create and destroy
✅ Test 2: IP to Ethernet conversion (IPv4)
✅ Test 3: Ethernet to IP conversion (IPv4)
✅ Test 4: ARP request handling (request → reply)
✅ Test 5: IPv6 to Ethernet conversion
✅ Test 6: IPv6 from Ethernet extraction
✅ Test 7: ICMPv6 Router Advertisement parsing
✅ Test 8: ICMPv6 Neighbor Solicitation detection
✅ Test 9: ICMPv6 Neighbor Advertisement building
✅ Test 10: DHCP learning (IP/gateway/subnet)
✅ Test 11: DNS query parsing and caching
✅ Test 12: IPv4 fragment reassembly
✅ Test 13: IPv6 fragment reassembly
✅ Test 14: ICMP error message parsing

All 14 tests passed!

Test Coverage

Category Coverage Tests
Core API ✅ 100% Create, destroy, get_stats
IPv4 ✅ 100% Translation, ARP, DHCP, fragmentation
IPv6 ✅ 100% Translation, NDP, RA parsing, fragmentation
ARP ✅ 100% Request/reply cycle, table management
DNS ✅ 100% Query parsing, LRU cache
Fragments ✅ 100% IPv4/v6 reassembly, timeout
ICMP ✅ 100% Error parsing, MTU discovery
Memory ✅ Verified Valgrind, no leaks

Continuous Integration

  • Platforms: Linux (Ubuntu), macOS (latest), iOS (cross-compile)
  • Compilers: Clang 12+, GCC 8+
  • Static Analysis: -Wall -Wextra -Werror (zero warnings)
  • CI Status: CI

🔌 Integration Examples

iOS (Network Extension)

// PacketAdapter.m
#import "virtual_tap.h"

@interface PacketAdapter ()
@property (nonatomic) VirtualTap* translator;
@end

@implementation PacketAdapter

- (instancetype)init {
    if (self = [super init]) {
        // Create VirtualTap instance
        VirtualTapConfig config = {
            .our_mac = {0x02, 0x00, 0x5E, 0x10, 0x20, 0x30},
            .our_ip = 0,
            .gateway_ip = 0,
            .handle_arp = true,
            .learn_ip = true,
            .learn_gateway_mac = true,
            .verbose = true
        };
        _translator = virtual_tap_create(&config);
    }
    return self;
}

- (void)dealloc {
    if (_translator) {
        virtual_tap_destroy(_translator);
    }
}

// Called when IP packet received from iOS TUN interface
- (void)handlePacketFromiOS:(NSData*)ipPacket {
    uint8_t ethFrame[2048];
    int32_t ethLen = virtual_tap_ip_to_ethernet(
        _translator,
        ipPacket.bytes,
        (uint32_t)ipPacket.length,
        ethFrame,
        sizeof(ethFrame)
    );
    
    if (ethLen > 0) {
        // Send Ethernet frame to SoftEther VPN server
        [self sendToVPNServer:[NSData dataWithBytes:ethFrame length:ethLen]];
    }
}

// Called when Ethernet frame received from VPN server
- (void)handlePacketFromServer:(NSData*)ethFrame {
    uint8_t ipPacket[2048];
    int32_t ipLen = virtual_tap_ethernet_to_ip(
        _translator,
        ethFrame.bytes,
        (uint32_t)ethFrame.length,
        ipPacket,
        sizeof(ipPacket)
    );
    
    if (ipLen > 0) {
        // Forward IP packet to iOS TUN interface
        [self.tunnelProvider writePacketData:[NSData dataWithBytes:ipPacket length:ipLen]];
    } else if (ipLen == 0) {
        // ARP handled internally, check for pending replies
        [self flushARPReplies];
    }
}

- (void)flushARPReplies {
    while (virtual_tap_has_pending_arp_reply(_translator)) {
        uint8_t arpReply[42];
        int32_t arpLen = virtual_tap_pop_arp_reply(_translator, arpReply, sizeof(arpReply));
        if (arpLen > 0) {
            [self sendToVPNServer:[NSData dataWithBytes:arpReply length:arpLen]];
        }
    }
}

@end

Android (VPN Service)

// VpnService.java
public class SoftEtherVpnService extends VpnService {
    static {
        System.loadLibrary("virtualtap");
        System.loadLibrary("softether-jni");
    }
    
    private native long createVirtualTap();
    private native void destroyVirtualTap(long handle);
    private native byte[] ipToEthernet(long handle, byte[] ipPacket);
    private native byte[] ethernetToIp(long handle, byte[] ethFrame);
    private native byte[][] popARPReplies(long handle);
    
    private long mVirtualTap;
    private ParcelFileDescriptor mTunInterface;
    
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        // Create VirtualTap instance
        mVirtualTap = createVirtualTap();
        
        // Create TUN interface
        Builder builder = new Builder();
        builder.setMtu(1500)
               .addAddress("192.168.1.100", 24)
               .addRoute("0.0.0.0", 0)
               .addDnsServer("8.8.8.8");
        mTunInterface = builder.establish();
        
        // Start packet forwarding threads
        new Thread(this::readFromTun).start();
        new Thread(this::readFromServer).start();
        
        return START_STICKY;
    }
    
    // Read IP packets from Android TUN → Convert to Ethernet → Send to server
    private void readFromTun() {
        FileInputStream in = new FileInputStream(mTunInterface.getFileDescriptor());
        byte[] buffer = new byte[2048];
        
        try {
            while (true) {
                int len = in.read(buffer);
                if (len > 0) {
                    byte[] ipPacket = Arrays.copyOf(buffer, len);
                    byte[] ethFrame = ipToEthernet(mVirtualTap, ipPacket);
                    if (ethFrame != null) {
                        sendToVPNServer(ethFrame);
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    // Read Ethernet frames from server → Convert to IP → Write to TUN
    private void readFromServer() {
        try {
            while (true) {
                byte[] ethFrame = receiveFromVPNServer();
                byte[] ipPacket = ethernetToIp(mVirtualTap, ethFrame);
                
                if (ipPacket != null && ipPacket.length > 0) {
                    FileOutputStream out = new FileOutputStream(
                        mTunInterface.getFileDescriptor());
                    out.write(ipPacket);
                } else {
                    // Check for ARP replies
                    byte[][] arpReplies = popARPReplies(mVirtualTap);
                    for (byte[] arpReply : arpReplies) {
                        sendToVPNServer(arpReply);
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    @Override
    public void onDestroy() {
        if (mVirtualTap != 0) {
            destroyVirtualTap(mVirtualTap);
        }
        super.onDestroy();
    }
}
// JNI bridge (virtualtap_jni.c)
#include <jni.h>
#include "virtual_tap.h"

JNIEXPORT jlong JNICALL
Java_com_worxvpn_SoftEtherVpnService_createVirtualTap(JNIEnv* env, jobject thiz) {
    VirtualTapConfig config = {
        .our_mac = {0x02, 0x00, 0x5E, 0x10, 0x20, 0x30},
        .our_ip = 0,
        .gateway_ip = 0,
        .handle_arp = true,
        .learn_ip = true,
        .learn_gateway_mac = true,
        .verbose = true
    };
    VirtualTap* tap = virtual_tap_create(&config);
    return (jlong)tap;
}

JNIEXPORT void JNICALL
Java_com_worxvpn_SoftEtherVpnService_destroyVirtualTap(JNIEnv* env, jobject thiz, jlong handle) {
    VirtualTap* tap = (VirtualTap*)handle;
    if (tap) {
        virtual_tap_destroy(tap);
    }
}

JNIEXPORT jbyteArray JNICALL
Java_com_worxvpn_SoftEtherVpnService_ipToEthernet(
    JNIEnv* env, jobject thiz, jlong handle, jbyteArray ipPacket) {
    
    VirtualTap* tap = (VirtualTap*)handle;
    jsize ipLen = (*env)->GetArrayLength(env, ipPacket);
    jbyte* ipData = (*env)->GetByteArrayElements(env, ipPacket, NULL);
    
    uint8_t ethFrame[2048];
    int32_t ethLen = virtual_tap_ip_to_ethernet(
        tap, (uint8_t*)ipData, ipLen, ethFrame, sizeof(ethFrame));
    
    (*env)->ReleaseByteArrayElements(env, ipPacket, ipData, JNI_ABORT);
    
    if (ethLen > 0) {
        jbyteArray result = (*env)->NewByteArray(env, ethLen);
        (*env)->SetByteArrayRegion(env, result, 0, ethLen, (jbyte*)ethFrame);
        return result;
    }
    return NULL;
}

// Similar implementations for ethernetToIp and popARPReplies...

🔧 Troubleshooting

Common Issues

❌ IP not learned from DHCP

Symptoms: virtual_tap_get_learned_ip() returns 0

Causes:

  1. config.learn_ip = false (disabled)
  2. No DHCP traffic passing through
  3. DHCP packets malformed

Solutions:

// Enable learning
config.learn_ip = true;

// Enable verbose logging
config.verbose = true;

// Check statistics
VirtualTapStats stats;
virtual_tap_get_stats(tap, &stats);
printf("DHCP packets seen: %llu\n", stats.dhcp_packets);
❌ Gateway MAC not learned

Symptoms: virtual_tap_get_gateway_mac() returns false

Causes:

  1. config.learn_gateway_mac = false (disabled)
  2. No incoming traffic from gateway yet
  3. All traffic is outgoing only

Solutions:

// Enable gateway learning
config.learn_gateway_mac = true;

// Wait for incoming traffic (ping from server)
// Gateway MAC is learned from incoming packet source MAC

// Check ARP table
VirtualTapStats stats;
virtual_tap_get_stats(tap, &stats);
printf("ARP entries: %llu\n", stats.arp_table_entries);
❌ ARP replies not sent

Symptoms: VPN connection fails, no ARP responses

Causes:

  1. Not checking virtual_tap_has_pending_arp_reply()
  2. Not calling virtual_tap_pop_arp_reply() in packet loop
  3. ARP replies discarded instead of sent

Solutions:

// Always check after ethernet_to_ip returns 0
int32_t ipLen = virtual_tap_ethernet_to_ip(tap, eth, eth_len, ip_out, sizeof(ip_out));
if (ipLen == 0) {
    // Packet handled internally (ARP), flush replies
    while (virtual_tap_has_pending_arp_reply(tap)) {
        uint8_t arp_reply[42];
        int32_t arp_len = virtual_tap_pop_arp_reply(tap, arp_reply, sizeof(arp_reply));
        if (arp_len > 0) {
            send_to_server(arp_reply, arp_len);  // ← MUST send to server!
        }
    }
}
❌ Memory leak detected

Symptoms: Valgrind reports leaks, memory usage grows

Causes:

  1. virtual_tap_destroy() not called
  2. Multiple instances created without cleanup

Solutions:

// Always pair create with destroy
VirtualTap* tap = virtual_tap_create(&config);
// ... use tap ...
virtual_tap_destroy(tap);  // ← MUST call this!

// Set to NULL after destroy to prevent double-free
tap = NULL;
❌ Packets dropped or malformed

Symptoms: Connection unstable, packet errors

Causes:

  1. Output buffer too small (VTAP_ERROR_BUFFER_TOO_SMALL)
  2. Invalid input packets
  3. MTU mismatch

Solutions:

// Use larger buffers (2048 bytes recommended)
uint8_t buffer[2048];

// Check return values
int32_t len = virtual_tap_ip_to_ethernet(tap, ip, ip_len, buffer, sizeof(buffer));
if (len == VTAP_ERROR_BUFFER_TOO_SMALL) {
    fprintf(stderr, "Buffer too small, need larger output buffer\n");
} else if (len == VTAP_ERROR_PARSE_FAILED) {
    fprintf(stderr, "Invalid packet format\n");
}

// Check MTU settings (iOS: 1500, Android: 1400-1500)

Debug Mode

// Enable verbose logging
VirtualTapConfig config = {
    // ...
    .verbose = true  // Prints packet details to stderr
};

// Example output:
// [VirtualTap] IP→Eth: 192.168.1.100 → 8.8.8.8 (84 bytes)
// [VirtualTap] Eth→IP: 8.8.8.8 → 192.168.1.100 (84 bytes)
// [VirtualTap] ARP request: Who has 192.168.1.100?
// [VirtualTap] ARP reply queued: 192.168.1.100 is at 02:00:5E:10:20:30

Statistics Monitoring

// Periodically check stats for diagnostics
void print_stats(VirtualTap* tap) {
    VirtualTapStats stats;
    virtual_tap_get_stats(tap, &stats);
    
    printf("=== VirtualTap Statistics ===\n");
    printf("IP→Eth: %llu packets\n", stats.ip_to_eth_packets);
    printf("Eth→IP: %llu packets\n", stats.eth_to_ip_packets);
    printf("ARP requests: %llu\n", stats.arp_requests_handled);
    printf("ARP replies sent: %llu\n", stats.arp_replies_sent);
    printf("IPv4: %llu, IPv6: %llu\n", stats.ipv4_packets, stats.ipv6_packets);
    printf("DNS queries: %llu (hits: %llu, misses: %llu)\n",
           stats.dns_queries, stats.dns_cache_hits, stats.dns_cache_misses);
    printf("Fragments: IPv4=%llu, IPv6=%llu, reassembled=%llu\n",
           stats.ipv4_fragments, stats.ipv6_fragments, stats.fragments_reassembled);
    printf("ARP table: %llu entries\n", stats.arp_table_entries);
}

📂 Project Structure

VirtualTap/
├── .github/
│   └── workflows/
│       └── ci.yml              # GitHub Actions CI (Linux, macOS, iOS)
├── include/                     # Public headers
│   ├── virtual_tap.h           # Core API (create, translate, stats)
│   ├── virtual_tap_internal.h  # Internal structures
│   ├── arp_handler.h           # ARP table and protocol
│   ├── translator.h            # L2↔L3 conversion
│   ├── dhcp_parser.h           # DHCP packet parsing
│   ├── icmpv6_handler.h        # ICMPv6 NDP (NS/NA/RA)
│   ├── dns_handler.h           # DNS caching with LRU
│   ├── fragment_handler.h      # IPv4/v6 fragment reassembly
│   └── icmp_handler.h          # ICMP error parsing
├── src/                         # Implementation
│   ├── virtual_tap.c           # Main module (635 lines)
│   ├── arp_handler.c           # ARP handling (209 lines)
│   ├── translator.c            # L2↔L3 translation (245 lines)
│   ├── dhcp_parser.c           # DHCP parsing (132 lines)
│   ├── ip_utils.c              # IP utilities (69 lines)
│   ├── icmpv6_handler.c        # ICMPv6 NDP (255 lines)
│   ├── dns_handler.c           # DNS cache (350 lines)
│   ├── fragment_handler.c      # Fragmentation (355 lines)
│   └── icmp_handler.c          # ICMP errors (158 lines)
├── test/
│   └── test_basic.c            # 14 unit tests (727 lines)
├── Makefile                     # Build system
├── README.md                   # This file
└── ROADMAP.md                  # Development roadmap

Total Lines of Code: ~3,200 (production-tested)


🔍 Implementation Details

Packet Format Reference

Ethernet Frame (14-byte header)

Offset  Size  Field              Value
------  ----  -----              -----
0-5     6     Destination MAC    Target MAC or gateway MAC
6-11    6     Source MAC         Our virtual MAC (0x02:00:5E:...)
12-13   2     EtherType          0x0800 (IPv4), 0x86DD (IPv6), 0x0806 (ARP)
14+     N     Payload            IP packet or ARP packet

ARP Packet (42 bytes total, 28 bytes after Ethernet header)

Offset  Size  Field              Value
------  ----  -----              -----
0-5     6     Dest MAC           FF:FF:FF:FF:FF:FF (broadcast)
6-11    6     Src MAC            Sender MAC
12-13   2     EtherType          0x0806 (ARP)
14-15   2     Hardware type      0x0001 (Ethernet)
16-17   2     Protocol type      0x0800 (IPv4)
18      1     Hardware size      6 (MAC length)
19      1     Protocol size      4 (IPv4 length)
20-21   2     Operation          1 (request), 2 (reply)
22-27   6     Sender MAC         Source MAC address
28-31   4     Sender IP          Source IP address
32-37   6     Target MAC         00:00:00:00:00:00 (request), filled (reply)
38-41   4     Target IP          Requested IP address

IPv4 Header (minimum 20 bytes)

Offset  Bits  Field              Notes
------  ----  -----              -----
0       4     Version            4 (IPv4)
0       4     IHL                5+ (header length in 32-bit words)
1       8     ToS/DSCP           Type of service
2-3     16    Total length       Entire packet length
4-5     16    Identification     Fragment ID
6-7     16    Flags + Offset     Fragment flags and offset
8       8     TTL                Time to live
9       8     Protocol           6 (TCP), 17 (UDP), 1 (ICMP)
10-11   16    Checksum           Header checksum
12-15   32    Source IP          Source address
16-19   32    Dest IP            Destination address

IPv6 Header (fixed 40 bytes)

Offset  Bits  Field              Notes
------  ----  -----              -----
0       4     Version            6 (IPv6)
0       8     Traffic class      Priority
1-3     20    Flow label         Flow identifier
4-5     16    Payload length     Payload size (excluding header)
6       8     Next header        Protocol (6=TCP, 17=UDP, 58=ICMPv6)
7       8     Hop limit          TTL equivalent
8-23    128   Source IP          128-bit source address
24-39   128   Dest IP            128-bit dest address

ARP Table

// ARP table entry structure
typedef struct {
    uint32_t ip;           // IPv4 address (host byte order)
    uint8_t mac[6];        // MAC address
    uint64_t timestamp;    // Last seen time (for timeout)
    bool is_static;        // Static entry (never expires)
} ArpEntry;

// Fixed-size table: 64 entries
#define ARP_TABLE_SIZE 64
#define ARP_TIMEOUT_SEC 300  // 5 minutes

// Lookup: Linear search O(n), n=64 → ~1µs
// Insert: Replace oldest non-static entry

DNS Cache (LRU)

// DNS cache entry
typedef struct {
    char domain[256];      // Domain name
    uint64_t timestamp;    // Last access time
    uint32_t access_count; // Number of hits
} DnsCacheEntry;

// LRU cache: 256 entries
#define DNS_CACHE_SIZE 256
#define DNS_TTL_SEC 300  // 5 minutes

// Eviction: Least recently used (oldest timestamp)
// Lookup: Linear search with strcmp → <2µs average

Fragment Reassembly

// Fragment chain for reassembly
typedef struct {
    uint32_t id;           // IPv4: Identification, IPv6: Fragment ID
    uint8_t src_ip[16];    // Source address (4 bytes IPv4, 16 bytes IPv6)
    uint8_t dst_ip[16];    // Dest address
    uint8_t buffer[65535]; // Reassembly buffer (max IP packet size)
    uint16_t received[128];// Bitmap of received fragments
    uint32_t total_len;    // Total payload length
    uint64_t timestamp;    // First fragment arrival time
    bool complete;         // All fragments received
} FragmentChain;

// Chains: 16 IPv4 + 16 IPv6 = 32 total
#define MAX_FRAGMENT_CHAINS 16
#define FRAGMENT_TIMEOUT_SEC 30

// Lookup: Hash(src_ip, dst_ip, id) % MAX_CHAINS
// Timeout: Periodic cleanup of chains older than 30s

🌐 Platform Compatibility

Supported Platforms

Platform Architecture Min Version Status Notes
iOS arm64 15.0+ ✅ Production Network Extension
Android arm64-v8a 5.0+ (API 21) ✅ Production VPN Service
Android armeabi-v7a 5.0+ (API 21) ✅ Production 32-bit support
macOS x86_64, arm64 10.15+ ✅ Testing Development only
Linux x86_64, arm64 Any ✅ Testing Development only
Windows x86_64 - ⚠️ Untested Uses gettimeofday

Compiler Support

Compiler Min Version Status Notes
Clang 12.0+ ✅ Primary iOS, macOS, Android NDK
GCC 8.0+ ✅ Supported Linux, Android NDK
MSVC - ⚠️ Untested Requires POSIX shim

Build Requirements

  • C11 standard (-std=c11)
  • POSIX minimal: gettimeofday, pthread_mutex (optional)
  • No external dependencies (pure stdlib)

🛠️ Build System

Makefile Targets

make              # Build libvirtualtap.a (native platform)
make ios          # Build libvirtualtap_ios.a (iOS arm64, Xcode required)
make test         # Build and run unit tests
make clean        # Remove all build artifacts

Compiler Flags

CC = clang
CFLAGS = -std=c11 -Wall -Wextra -Werror -O2 -I./include
LDFLAGS = 

# iOS cross-compilation
IOS_SDK = $(shell xcrun --sdk iphoneos --show-sdk-path)
IOS_CFLAGS = -arch arm64 -isysroot $(IOS_SDK) -mios-version-min=15.0

Android NDK Integration

Add to CMakeLists.txt:

# VirtualTap library
add_library(virtualtap STATIC
    VirtualTap/src/virtual_tap.c
    VirtualTap/src/arp_handler.c
    VirtualTap/src/translator.c
    VirtualTap/src/dhcp_parser.c
    VirtualTap/src/ip_utils.c
    VirtualTap/src/icmpv6_handler.c
    VirtualTap/src/dns_handler.c
    VirtualTap/src/fragment_handler.c
    VirtualTap/src/icmp_handler.c
)

target_include_directories(virtualtap PUBLIC VirtualTap/include)
target_compile_options(virtualtap PRIVATE
    -std=c11
    -Wall
    -Wextra
    -Werror
    -O2
)

# Link to your JNI library
target_link_libraries(your-jni-lib virtualtap)

Xcode Integration

  1. Add VirtualTap/ folder to project (reference, not copy)
  2. Build Settings → Header Search Paths → Add $(PROJECT_DIR)/VirtualTap/include
  3. Build Phases → Link Binary → Add libvirtualtap_ios.a
  4. (Optional) Add VirtualTap/src/*.c to Compile Sources for direct compilation

📖 Documentation


🤝 Contributing

This is a production library used in WorxVPN iOS/Android clients. Contributions welcome!

Code Style

  • Standard: C11 (-std=c11)
  • Indentation: 4 spaces (no tabs)
  • Warnings: Zero tolerance (-Werror)
  • Naming: snake_case for functions, PascalCase for types
  • Comments: Document public APIs, explain complex logic
  • Line length: Prefer <100 characters

Development Workflow

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/my-feature)
  3. Write code + unit tests
  4. Test locally (make test)
  5. Verify no warnings (make clean && make)
  6. Commit with clear messages
  7. Push and create a Pull Request

Testing Checklist

# Run unit tests
make test

# Check for memory leaks (Linux)
valgrind --leak-check=full ./test_basic

# Verify no warnings
make clean
make

# Test iOS build (macOS only)
make ios

Adding Features

  1. Unit test first: Add test case to test/test_basic.c
  2. Implement: Add code to appropriate module
  3. Update stats: Add counters to VirtualTapStats if needed
  4. Document: Update README and header comments
  5. Benchmark: Test performance impact

📜 License

Part of the SoftEther VPN project. Licensed under Apache License 2.0.


🔗 References

Standards & RFCs

  • RFC 826: Address Resolution Protocol (ARP)
  • RFC 791: Internet Protocol (IPv4)
  • RFC 2460: Internet Protocol, Version 6 (IPv6)
  • RFC 2131: Dynamic Host Configuration Protocol (DHCP)
  • RFC 4861: Neighbor Discovery for IPv6
  • RFC 792: Internet Control Message Protocol (ICMP)
  • IEEE 802.3: Ethernet standard

Related Projects

iOS Documentation

Android Documentation


💬 Support

Getting Help

  1. Check Troubleshooting section above
  2. Review unit tests for usage examples
  3. Enable verbose logging: config.verbose = true
  4. Check statistics: virtual_tap_get_stats() for diagnostics
  5. Open an issue on GitHub with logs and code snippet

Debugging Tips

// Enable verbose mode
config.verbose = true;

// Monitor statistics
VirtualTapStats stats;
virtual_tap_get_stats(tap, &stats);
printf("Packets: %llu→eth, %llu→ip, %llu ARP\n",
       stats.ip_to_eth_packets, stats.eth_to_ip_packets, stats.arp_requests_handled);

// Check learned config
uint32_t ip = virtual_tap_get_learned_ip(tap);
printf("Learned IP: %d.%d.%d.%d\n",
       (ip >> 24) & 0xFF, (ip >> 16) & 0xFF, (ip >> 8) & 0xFF, ip & 0xFF);

// Verify gateway
uint8_t gw_mac[6];
if (virtual_tap_get_gateway_mac(tap, gw_mac)) {
    printf("Gateway MAC: %02X:%02X:%02X:%02X:%02X:%02X\n",
           gw_mac[0], gw_mac[1], gw_mac[2], gw_mac[3], gw_mac[4], gw_mac[5]);
}

📊 Project Status

Version: 1.0.0 (Production)
Last Updated: November 9, 2025
Status: ✅ Production-ready, actively maintained
Lines of Code: ~3,200
Test Coverage: 14 comprehensive unit tests
Platforms: iOS, Android, macOS, Linux
Used By: WorxVPN-iOS, WorxVPN-Android


Report Bug · Request Feature · View CI

Built with ❤️ for the SoftEther community

Powered by Devstroop Technologies

About

High-performance C11 library for L2↔L3 packet translation with ARP virtualization, DHCP learning, IPv6/NDP support, and DNS caching

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published