tun-rs 2.8.3

Cross-platform TUN and TAP library
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
# iOS/tvOS Integration Guide


This guide provides detailed instructions for integrating tun-rs with iOS and tvOS applications using `NEPacketTunnelProvider`.

## Table of Contents


- [Overview]#overview
- [Getting the File Descriptor]#getting-the-file-descriptor
  - [iOS 16+ Recommended Method]#ios-16-recommended-method
  - [Legacy KVO Method]#legacy-kvo-method
- [Complete Integration Example]#complete-integration-example
- [WireGuardKit Method (Alternative)]#wireguardkit-method-alternative
- [Troubleshooting]#troubleshooting

## Overview


On iOS and tvOS, you cannot create TUN interfaces directly. Instead, you must:
1. Use `NEPacketTunnelProvider` to establish a VPN tunnel
2. Get the file descriptor from the packet flow
3. Pass the file descriptor to your Rust code via FFI
4. Use `tun_rs::SyncDevice::from_fd()` or `tun_rs::AsyncDevice::from_fd()` to manage the tunnel

## Getting the File Descriptor


### iOS 16+ Recommended Method


Starting with iOS 16, the Key-Value Observing (KVO) approach for accessing `socket.fileDescriptor` may return `nil`. The recommended approach is to search for the file descriptor by iterating through available file descriptors and matching against the utun control socket.

Here's the robust method adapted from [WireGuard](https://github.com/WireGuard/wireguard-apple):

```swift
import Foundation
import NetworkExtension

class PacketTunnelProvider: NEPacketTunnelProvider {
    
    /// Finds the tunnel file descriptor by searching through available file descriptors
    private func getTunnelFileDescriptor() -> Int32? {
        var ctlInfo = ctl_info()
        withUnsafeMutablePointer(to: &ctlInfo.ctl_name) {
            $0.withMemoryRebound(to: CChar.self, capacity: MemoryLayout.size(ofValue: $0.pointee)) {
                _ = strcpy($0, "com.apple.net.utun_control")
            }
        }
        
        // Search through file descriptors to find the utun socket
        // Note: Range 0...1024 is used to ensure we find the fd. In practice, the utun
        // socket is typically in the low range (< 100) and found quickly. This method
        // is from WireGuard's production implementation and only runs once at tunnel startup.
        for fd: Int32 in 0...1024 {
            var addr = sockaddr_ctl()
            var ret: Int32 = -1
            var len = socklen_t(MemoryLayout.size(ofValue: addr))
            
            withUnsafeMutablePointer(to: &addr) {
                $0.withMemoryRebound(to: sockaddr.self, capacity: 1) {
                    ret = getpeername(fd, $0, &len)
                }
            }
            
            if ret != 0 || addr.sc_family != AF_SYSTEM {
                continue
            }
            
            if ctlInfo.ctl_id == 0 {
                ret = ioctl(fd, CTLIOCGINFO, &ctlInfo)
                if ret != 0 {
                    continue
                }
            }
            
            if addr.sc_id == ctlInfo.ctl_id {
                return fd
            }
        }
        
        return nil
    }
    
    override func startTunnel(options: [String : NSObject]?, completionHandler: @escaping (Error?) -> Void) {
        // Configure tunnel settings
        let tunnelNetworkSettings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: "10.0.0.1")
        tunnelNetworkSettings.mtu = 1400
        
        let ipv4Settings = NEIPv4Settings(addresses: ["10.0.0.2"], subnetMasks: ["255.255.255.0"])
        ipv4Settings.includedRoutes = [NEIPv4Route.default()]
        tunnelNetworkSettings.ipv4Settings = ipv4Settings
        
        // Apply settings first
        setTunnelNetworkSettings(tunnelNetworkSettings) { [weak self] error in
            guard let self = self else {
                completionHandler(NSError(domain: "TunnelError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Self deallocated"]))
                return
            }
            
            if let error = error {
                completionHandler(error)
                return
            }
            
            // Get the file descriptor after settings are applied
            guard let tunFd = self.getTunnelFileDescriptor() else {
                completionHandler(NSError(domain: "TunnelError", code: 2, userInfo: [NSLocalizedDescriptionKey: "Cannot locate tunnel file descriptor"]))
                return
            }
            
            os_log(.default, "Starting tunnel with fd %{public}d", tunFd)
            
            // Start your Rust tunnel implementation on a background thread
            DispatchQueue.global(qos: .userInitiated).async {
                start_tun(tunFd)
            }
            
            completionHandler(nil)
        }
    }
    
    override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
        // Implement cleanup if needed
        os_log(.default, "Stopping tunnel, reason: %{public}@", String(describing: reason))
        completionHandler()
    }
}
```

### Legacy KVO Method


**⚠️ Warning:** This method is **deprecated** and may not work on iOS 16 and later.

```swift
// This may return nil on iOS 16+
let tunFd = self.packetFlow.value(forKeyPath: "socket.fileDescriptor") as? Int32
guard let unwrappedFd = tunFd else {
    os_log(.error, "Cannot start tunnel: file descriptor is nil")
    return
}
```

If you encounter issues with the file descriptor being `nil`, switch to the recommended method above.

## Complete Integration Example


Here's a complete example showing both Swift and Rust sides:

### Swift Side (PacketTunnelProvider)


```swift
import NetworkExtension
import os.log

class PacketTunnelProvider: NEPacketTunnelProvider {
    
    private var tunnelQueue: DispatchQueue?
    
    private func getTunnelFileDescriptor() -> Int32? {
        var ctlInfo = ctl_info()
        withUnsafeMutablePointer(to: &ctlInfo.ctl_name) {
            $0.withMemoryRebound(to: CChar.self, capacity: MemoryLayout.size(ofValue: $0.pointee)) {
                _ = strcpy($0, "com.apple.net.utun_control")
            }
        }
        
        // Search through file descriptors to find the utun socket
        // Note: Range 0...1024 ensures we find the fd. In practice, found quickly in low range.
        for fd: Int32 in 0...1024 {
            var addr = sockaddr_ctl()
            var ret: Int32 = -1
            var len = socklen_t(MemoryLayout.size(ofValue: addr))
            
            withUnsafeMutablePointer(to: &addr) {
                $0.withMemoryRebound(to: sockaddr.self, capacity: 1) {
                    ret = getpeername(fd, $0, &len)
                }
            }
            
            if ret != 0 || addr.sc_family != AF_SYSTEM {
                continue
            }
            
            if ctlInfo.ctl_id == 0 {
                ret = ioctl(fd, CTLIOCGINFO, &ctlInfo)
                if ret != 0 {
                    continue
                }
            }
            
            if addr.sc_id == ctlInfo.ctl_id {
                return fd
            }
        }
        
        return nil
    }
    
    override func startTunnel(options: [String : NSObject]?, completionHandler: @escaping (Error?) -> Void) {
        os_log(.info, "Starting tunnel...")
        
        // 1. Create tunnel network settings
        let settings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: "10.0.0.1")
        settings.mtu = 1400
        
        // 2. Configure IPv4
        let ipv4Settings = NEIPv4Settings(addresses: ["10.0.0.2"], subnetMasks: ["255.255.255.0"])
        ipv4Settings.includedRoutes = [NEIPv4Route.default()]
        settings.ipv4Settings = ipv4Settings
        
        // 3. Configure DNS (optional)
        let dnsSettings = NEDNSSettings(servers: ["8.8.8.8", "8.8.4.4"])
        settings.dnsSettings = dnsSettings
        
        // 4. Apply settings
        setTunnelNetworkSettings(settings) { [weak self] error in
            guard let self = self else {
                completionHandler(NSError(domain: "TunnelError", code: 1))
                return
            }
            
            if let error = error {
                os_log(.error, "Failed to set tunnel settings: %{public}@", error.localizedDescription)
                completionHandler(error)
                return
            }
            
            // 5. Get file descriptor
            guard let tunFd = self.getTunnelFileDescriptor() else {
                let error = NSError(
                    domain: "TunnelError",
                    code: 2,
                    userInfo: [NSLocalizedDescriptionKey: "Cannot locate tunnel file descriptor"]
                )
                os_log(.error, "Failed to get file descriptor")
                completionHandler(error)
                return
            }
            
            os_log(.info, "Tunnel file descriptor obtained: %{public}d", tunFd)
            
            // 6. Start Rust tunnel processing
            let queue = DispatchQueue(label: "com.yourapp.tunnel", qos: .userInitiated)
            self.tunnelQueue = queue
            
            queue.async {
                os_log(.info, "Starting Rust tunnel processing...")
                // This will block until the tunnel stops
                start_tun(tunFd)
                os_log(.info, "Rust tunnel processing ended")
            }
            
            completionHandler(nil)
        }
    }
    
    override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
        os_log(.info, "Stopping tunnel, reason: %{public}@", String(describing: reason))
        
        // Signal your Rust code to stop (you may need to implement a stop mechanism)
        stop_tun()
        
        completionHandler()
    }
    
    override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)?) {
        // Handle messages from the main app if needed
        if let handler = completionHandler {
            handler(nil)
        }
    }
}
```

### Rust Side (FFI + tun-rs)


```rust
use std::os::unix::io::RawFd;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use tun_rs::SyncDevice;

// Global flag to signal tunnel shutdown
static SHOULD_STOP: AtomicBool = AtomicBool::new(false);

#[no_mangle]

pub extern "C" fn start_tun(fd: std::os::raw::c_int) {
    // Relaxed ordering is sufficient for a simple flag with no dependent variables
    SHOULD_STOP.store(false, Ordering::Relaxed);
    
    // Create device from file descriptor
    let tun = unsafe {
        match SyncDevice::from_fd(fd as RawFd) {
            Ok(device) => device,
            Err(e) => {
                eprintln!("Failed to create device from fd {}: {:?}", fd, e);
                return;
            }
        }
    };
    
    println!("Tunnel started with fd {}", fd);
    
    let mut buf = vec![0u8; 4096];
    
    // Main packet processing loop
    loop {
        // Check if we should stop
        if SHOULD_STOP.load(Ordering::Relaxed) {
            println!("Stop signal received, exiting tunnel loop");
            break;
        }
        
        // Receive packet
        match tun.recv(&mut buf) {
            Ok(len) => {
                println!("Received packet: {} bytes", len);
                
                // Process packet here
                // For example, parse IP header, handle routing, etc.
                
                // Echo back (for testing)
                if let Err(e) = tun.send(&buf[..len]) {
                    eprintln!("Failed to send packet: {:?}", e);
                }
            }
            Err(e) => {
                eprintln!("Failed to receive packet: {:?}", e);
                // Consider whether to break or continue based on error type
                if e.kind() == std::io::ErrorKind::Interrupted {
                    continue;
                }
                break;
            }
        }
    }
    
    println!("Tunnel loop ended");
}

#[no_mangle]

pub extern "C" fn stop_tun() {
    println!("Stopping tunnel...");
    SHOULD_STOP.store(true, Ordering::Relaxed);
}
```

### Async Version (Tokio)


If you prefer async operations:

```rust
use std::os::unix::io::RawFd;
use std::sync::atomic::{AtomicBool, Ordering};
use tokio::runtime::Runtime;
use tun_rs::AsyncDevice;

static SHOULD_STOP: AtomicBool = AtomicBool::new(false);

#[no_mangle]

pub extern "C" fn start_tun(fd: std::os::raw::c_int) {
    // Relaxed ordering is sufficient for a simple flag with no dependent variables
    SHOULD_STOP.store(false, Ordering::Relaxed);
    
    // Create a Tokio runtime
    let rt = match Runtime::new() {
        Ok(rt) => rt,
        Err(e) => {
            eprintln!("Failed to create runtime: {:?}", e);
            return;
        }
    };
    
    rt.block_on(async move {
        // Create async device from file descriptor
        let tun = unsafe {
            match AsyncDevice::from_fd(fd as RawFd) {
                Ok(device) => device,
                Err(e) => {
                    eprintln!("Failed to create device from fd {}: {:?}", fd, e);
                    return;
                }
            }
        };
        
        println!("Async tunnel started with fd {}", fd);
        
        let mut buf = vec![0u8; 4096];
        
        loop {
            if SHOULD_STOP.load(Ordering::Relaxed) {
                println!("Stop signal received");
                break;
            }
            
            // Async receive
            match tun.recv(&mut buf).await {
                Ok(len) => {
                    println!("Received packet: {} bytes", len);
                    
                    // Process and echo back
                    if let Err(e) = tun.send(&buf[..len]).await {
                        eprintln!("Failed to send: {:?}", e);
                    }
                }
                Err(e) => {
                    eprintln!("Failed to receive: {:?}", e);
                    break;
                }
            }
        }
        
        println!("Async tunnel loop ended");
    });
}

#[no_mangle]

pub extern "C" fn stop_tun() {
    println!("Stopping tunnel...");
    SHOULD_STOP.store(true, Ordering::Relaxed);
}
```

### Cargo Configuration


Add tun-rs to your `Cargo.toml`:

```toml
[lib]
name = "your_tunnel_lib"
crate-type = ["staticlib", "cdylib"]

[dependencies]
tun-rs = "2"

# For async support

# tun-rs = { version = "2", features = ["async"] }

# tokio = { version = "1", features = ["rt", "rt-multi-thread"] }

```

## WireGuardKit Method (Alternative)


If you prefer to use the WireGuardKit approach directly, you can integrate it into your project:

### Setup


1. Add the `WireGuardKit` folder from [wireguard-apple]https://github.com/WireGuard/wireguard-apple to your Xcode project
2. Create a bridging header (e.g., `YourApp-Bridging-Header.h`) and include:

```objc
#import "WireGuardKitC/WireGuardKitC.h"

```

3. Configure the bridging header in your Xcode project build settings

### Using WireGuardKit's Adapter


```swift
import WireGuardKit

class PacketTunnelProvider: NEPacketTunnelProvider {
    private var adapter: WireGuardAdapter?
    
    override func startTunnel(options: [String : NSObject]?, completionHandler: @escaping (Error?) -> Void) {
        let adapter = WireGuardAdapter(with: self) { logLevel, message in
            print("[\(logLevel)]: \(message)")
        }
        self.adapter = adapter
        
        // Now you can use adapter.tunnelFileDescriptor
        if let fd = adapter.tunnelFileDescriptor {
            // Pass to your Rust code
            start_tun(fd)
        }
        
        // Continue with tunnel setup...
    }
}
```

## Troubleshooting


### File Descriptor is Nil


**Symptom:** `getTunnelFileDescriptor()` returns `nil`

**Solutions:**
1. Ensure `setTunnelNetworkSettings()` completes successfully before trying to get the FD
2. Check that your app has the proper VPN entitlements
3. Verify the Network Extension capability is enabled in your project
4. Make sure to call `getTunnelFileDescriptor()` **after** tunnel settings are applied

### Build Fails with Missing Symbols


**Symptom:** Linking errors when building the Rust library

**Solutions:**
1. Ensure your `Cargo.toml` specifies the correct crate-type:
   ```toml
   [lib]
   crate-type = ["staticlib"]
   ```
2. Build the Rust library for iOS targets:
   ```bash
   cargo build --target aarch64-apple-ios --release

   cargo build --target x86_64-apple-ios --release  # For simulator

   ```

### Tunnel Stops Unexpectedly


**Symptom:** Tunnel disconnects without clear error

**Solutions:**
1. Check system logs in Console.app
2. Implement proper error handling in your Rust code
3. Ensure the packet processing thread doesn't crash
4. Use `os_log` extensively for debugging

### Entitlements Issues


**Required Entitlements:**

In your Network Extension's entitlements file:

```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>com.apple.developer.networking.networkextension</key>
    <array>
        <string>packet-tunnel-provider</string>
    </array>
</dict>
</plist>
```

### Permissions and Capabilities

1. Enable **Network Extensions** capability in your Xcode project
2. Ensure your provisioning profile includes the Network Extensions entitlement
3. Test on a physical device (some features may not work in simulator)

## Additional Resources

- [Apple's Network Extension Documentation](https://developer.apple.com/documentation/networkextension)
- [NEPacketTunnelProvider Reference](https://developer.apple.com/documentation/networkextension/nepackettunnelprovider)
- [WireGuard iOS Implementation](https://github.com/WireGuard/wireguard-apple)
- [tun-rs Examples](https://github.com/tun-rs/tun-rs/tree/main/examples)

## Notes

- The file descriptor search method iterates through FDs 0-1024. This range is from WireGuard's production implementation and covers all realistic scenarios. In practice, the utun socket is typically found in the low range (< 100) very quickly, and this only runs once at tunnel startup, so performance impact is negligible.
- On iOS, you cannot create TUN devices directly using `DeviceBuilder`. You must use the file descriptor from `NEPacketTunnelProvider`.
- The tunnel processing should run on a background thread/queue to avoid blocking the main thread.
- Proper error handling and logging are crucial for debugging iOS Network Extensions, as they run in a sandboxed environment with limited debugging capabilities.