N2N VPN client bindings for .NET MAUI Android applications. Provides a clean P/Invoke interface to the native n2n peer-to-peer VPN library. Features include async/await support, event-driven status updates, and full VpnService integration.
$ dotnet add package N2nBindingsVersion: 2.3.0 Last Updated: December 2024 .NET Version: 9.0
N2nBindings is a .NET MAUI Android library that provides a clean, managed interface to the n2n peer-to-peer VPN client. It enables Android applications to create virtual private networks without requiring root access.
graph TB
subgraph "C# MAUI Application"
App[Your MAUI App]
N2nEdge[N2nEdge Class]
VpnService[N2nVpnService]
end
subgraph "Native Layer"
PInvoke[P/Invoke Bindings]
LibN2n[libn2n_android.so]
end
subgraph "Android System"
AndroidVpn[Android VpnService API]
TunDevice[TUN Device]
end
subgraph "Network"
Supernode[Supernode]
Peers[Other Edge Nodes]
end
App --> N2nEdge
N2nEdge --> VpnService
VpnService --> PInvoke
PInvoke --> LibN2n
VpnService --> AndroidVpn
AndroidVpn --> TunDevice
LibN2n --> TunDevice
LibN2n --> Supernode
LibN2n --> Peers
flowchart TB
subgraph Application["Application Layer"]
direction LR
MAUI["MAUI Application"]
Config["N2nConfiguration"]
end
subgraph Bindings["N2nBindings Library"]
direction LR
Edge["N2nEdge"]
Native["N2nNative (P/Invoke)"]
Service["N2nVpnService"]
end
subgraph NativeLib["Native Library (libn2n_android.so)"]
direction LR
Android["n2n_android.c"]
TunTap["tuntap_android.c"]
Core["n2n Core"]
end
Application --> Bindings
Bindings --> NativeLib
sequenceDiagram
participant App as MAUI App
participant Edge as N2nEdge
participant VPN as N2nVpnService
participant Native as libn2n_android.so
participant Android as Android OS
App->>Edge: Start(config, tunFd)
Edge->>Native: n2n_android_start()
Note over VPN,Android: VPN Permission Flow
App->>Android: VpnService.Prepare()
Android-->>App: Permission Intent (if needed)
App->>VPN: StartService(config)
VPN->>Android: VpnBuilder.Establish()
Android-->>VPN: TUN File Descriptor
VPN->>Native: n2n_android_start(config, fd)
Native->>Native: Initialize edge
Native->>Native: Start edge thread
Native-->>Edge: Status: Connecting
Edge-->>App: StatusChanged event
Native->>Native: Register with supernode
Native-->>Edge: Status: Connected
Edge-->>App: StatusChanged event
| Component | Technology | Purpose |
|---|---|---|
| Runtime | .NET 9.0 | Application framework |
| UI Framework | MAUI | Cross-platform UI |
| Native Bindings | P/Invoke | C# to C interop |
| VPN Protocol | n2n v3 | Peer-to-peer VPN |
| Encryption | Twofish/AES/ChaCha20 | Data encryption |
| Android API | VpnService | TUN interface management |
| Architecture | ABI | Devices |
|---|---|---|
| ARM64 | arm64-v8a | Modern phones/tablets |
| ARM32 | armeabi-v7a | Older devices |
| x86_64 | x86_64 | Emulators |
| x86 | x86 | Emulators |
| Requirement | Version | Purpose |
|---|---|---|
| .NET SDK | 9.0+ | Build MAUI applications |
| Android SDK | API 21+ | Android development |
| Visual Studio / Rider | Latest | IDE |
| Requirement | Version | Purpose |
|---|---|---|
| Android NDK | r21+ | Cross-compilation |
| CMake | 3.10+ | Build system |
| Linux/macOS | Any | Build environment |
Add the N2nBindings project to your solution:
dotnet add reference ../N2nBindings/N2nBindings.csproj
Or copy the compiled library to your project.
In your AndroidManifest.xml:
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
The N2nVpnService is automatically registered via attributes, but ensure your app declares the service in the manifest.
Copy the compiled native libraries to your project:
YourApp/
├── lib/
│ ├── arm64-v8a/
│ │ └── libn2n_android.so
│ └── armeabi-v7a/
│ └── libn2n_android.so
The native library source is available at: https://github.com/NakanoMiku13/n2n-android
# Clone the n2n-android repository
git clone https://github.com/NakanoMiku13/n2n-android.git
cd n2n-android
# Checkout the stable version
git checkout v1.0.0
# Set NDK path (if not already set)
export ANDROID_NDK=$HOME/Android/Sdk/ndk/25.2.9519653
# Build all architectures
./build.sh
# Or build specific architecture
./build.sh arm64-v8a
git clone https://github.com/NakanoMiku13/n2n-android.git
cd n2n-android
mkdir build && cd build
# Configure for ARM64
cmake \
-DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK/build/cmake/android.toolchain.cmake \
-DANDROID_ABI=arm64-v8a \
-DANDROID_PLATFORM=android-21 \
-DCMAKE_BUILD_TYPE=Release \
..
# Build
cmake --build . --config Release
# Library will be at: build/libn2n_android.so
After building, copy the libraries:
# From n2n-android/output/lib/
cp -r output/lib/* /path/to/N2nBindings/lib/
| Property | Type | Default | Description |
|---|---|---|---|
Community | string | Required | Virtual network name (max 19 chars) |
SecretKey | string | "" | Encryption password |
Supernode | string | Required | Supernode address "host:port" |
Supernode2 | string? | null | Backup supernode |
IpAddress | string | Required* | Local VPN IP address (*not required for Auto IP mode) |
Netmask | string | "255.255.255.0" | Network mask |
MacAddress | string? | Auto | Virtual MAC address |
Mtu | int | 1400 | Maximum transmission unit |
LocalPort | int | 0 (auto) | Local UDP port |
Encryption | enum | Twofish | Encryption algorithm |
IpMode | enum | Static | IP assignment mode (Static or Auto) |
AllowP2P | bool | true | Enable peer-to-peer |
AllowRouting | bool | false | Accept routed packets |
HeaderEncryption | bool | false | Encrypt packet headers |
Compression | bool | false | Enable compression |
| Value | Description |
|---|---|
Static | Use the IpAddress property (user-defined). Default mode. |
Auto | Request IP address automatically from supernode. Use RequestIpAsync() before starting. |
| Value | Algorithm | Notes |
|---|---|---|
None | No encryption | Not recommended |
Twofish | Twofish | Default, good balance |
Aes | AES-CBC | Strong, widely supported |
ChaCha20 | ChaCha20 | Fast on mobile |
Speck | Speck | Lightweight |
The main interface for managing the n2n edge client.
// Current connection status
N2nNative.N2nStatus Status { get; }
// Whether edge is running
bool IsRunning { get; }
// Whether connected to supernode
bool IsConnected { get; }
// Last error message
string LastError { get; }
// Library version
static string Version { get; }
// Start the edge client
int Start(N2nConfiguration config, int tunFd);
// Start asynchronously (waits for connection)
Task<bool> StartAsync(N2nConfiguration config, int tunFd, CancellationToken ct = default);
// Stop the edge client
int Stop();
// Stop asynchronously
Task StopAsync(CancellationToken ct = default);
// Get runtime statistics
N2nStatistics GetStatistics();
// Set log level (0-4)
void SetLogLevel(int level);
// Request IP from supernode (auto IP mode)
Task<(string IpAddress, string Netmask)> RequestIpAsync(N2nConfiguration config, CancellationToken ct = default);
// Get last assigned IP (auto IP mode)
bool GetAssignedIp(out string? ipAddress, out string? netmask);
// Status changed
event EventHandler<StatusChangedEventArgs> StatusChanged;
// Error occurred
event EventHandler<ErrorEventArgs> ErrorOccurred;
// Log message received
event EventHandler<LogEventArgs> LogReceived;
// Socket created (protect with VpnService.Protect())
event EventHandler<SocketCreatedEventArgs> SocketCreated;
// IP assigned by supernode (auto IP mode)
event EventHandler<IpAssignedEventArgs> IpAssigned;
// Create intent to start VPN
static Intent CreateStartIntent(Context context, N2nConfiguration config);
// Create intent to stop VPN
static Intent CreateStopIntent(Context context);
// Check if VPN permission is granted
static bool HasVpnPermission(Context context);
// Get intent to request VPN permission
static Intent? GetVpnPermissionIntent(Context context);
// Current status
static N2nNative.N2nStatus CurrentStatus { get; }
// Status changed event
static event EventHandler<N2nNative.N2nStatus> StatusChanged;
using N2nBindings;
// Create configuration with static IP
var config = N2nConfiguration.Create(
community: "mynetwork",
supernode: "supernode.example.com:7654",
ipAddress: "10.0.0.100",
secretKey: "mysecretpassword"
);
// Validate
var error = config.Validate();
if (error != null)
{
Console.WriteLine($"Config error: {error}");
return;
}
// Start via VpnService
var intent = N2nVpnService.CreateStartIntent(context, config);
context.StartService(intent);
using N2nBindings;
// Create configuration for auto IP assignment
var config = N2nConfiguration.CreateAutoIp(
community: "mynetwork",
supernode: "supernode.example.com:7654",
secretKey: "mysecretpassword"
);
// Create edge instance
using var edge = new N2nEdge();
// Handle socket creation for VPN protection
edge.SocketCreated += (s, e) =>
{
// IMPORTANT: Protect the socket from VPN routing
vpnService.Protect(e.SocketFd);
};
// Request IP from supernode
var (assignedIp, assignedNetmask) = await edge.RequestIpAsync(config);
Console.WriteLine($"Assigned IP: {assignedIp}/{assignedNetmask}");
// Update config with assigned IP
config.IpAddress = assignedIp;
config.Netmask = assignedNetmask;
// Now create VPN interface with the assigned IP and start edge
// ... VpnService.Builder setup with assignedIp ...
await edge.StartAsync(config, tunFd);
using N2nBindings;
public class VpnManager
{
private N2nEdge? _edge;
public async Task<bool> ConnectAsync(N2nConfiguration config, int tunFd)
{
_edge = new N2nEdge();
_edge.StatusChanged += (s, e) =>
{
Console.WriteLine($"Status: {e.Status}");
};
_edge.ErrorOccurred += (s, e) =>
{
Console.WriteLine($"Error: {e.Message}");
};
return await _edge.StartAsync(config, tunFd);
}
public async Task DisconnectAsync()
{
if (_edge != null)
{
await _edge.StopAsync();
_edge.Dispose();
_edge = null;
}
}
public N2nStatistics? GetStats()
{
return _edge?.GetStatistics();
}
}
public partial class VpnPage : ContentPage
{
private const int VpnPermissionRequestCode = 100;
public VpnPage()
{
InitializeComponent();
N2nVpnService.StatusChanged += OnVpnStatusChanged;
}
private async void OnConnectClicked(object sender, EventArgs e)
{
var context = Platform.CurrentActivity;
// Check VPN permission
var permissionIntent = N2nVpnService.GetVpnPermissionIntent(context);
if (permissionIntent != null)
{
context.StartActivityForResult(permissionIntent, VpnPermissionRequestCode);
return;
}
// Start VPN
var config = new N2nConfiguration
{
Community = CommunityEntry.Text,
SecretKey = PasswordEntry.Text,
Supernode = SupernodeEntry.Text,
IpAddress = IpAddressEntry.Text
};
var intent = N2nVpnService.CreateStartIntent(context, config);
context.StartService(intent);
}
private void OnDisconnectClicked(object sender, EventArgs e)
{
var context = Platform.CurrentActivity;
var intent = N2nVpnService.CreateStopIntent(context);
context.StartService(intent);
}
private void OnVpnStatusChanged(object? sender, N2nNative.N2nStatus status)
{
MainThread.BeginInvokeOnMainThread(() =>
{
StatusLabel.Text = status.ToString();
ConnectButton.IsEnabled = status == N2nNative.N2nStatus.Stopped;
DisconnectButton.IsEnabled = status == N2nNative.N2nStatus.Connected;
});
}
}
flowchart TD
A[User taps Connect] --> B{Has VPN Permission?}
B -->|Yes| C[Start VpnService]
B -->|No| D[Request Permission]
D --> E{User Grants?}
E -->|Yes| C
E -->|No| F[Show Error]
C --> G[Establish TUN Interface]
G --> H[Start n2n Edge]
H --> I[Connect to Supernode]
// In your Activity
protected override void OnActivityResult(int requestCode, Result resultCode, Intent? data)
{
base.OnActivityResult(requestCode, resultCode, data);
if (requestCode == VpnPermissionRequestCode)
{
if (resultCode == Result.Ok)
{
// Permission granted, start VPN
StartVpn();
}
else
{
// Permission denied
ShowError("VPN permission required");
}
}
}
Problem: DllNotFoundException: n2n_android
Solutions:
lib/arm64-v8a/libn2n_android.solib/armeabi-v7a/libn2n_android.so<AndroidNativeLibrary Include="lib\arm64-v8a\libn2n_android.so" Abi="arm64-v8a" />
Problem: Status stays at "Connecting"
Solutions:
Problem: VpnService.Prepare() returns non-null Intent repeatedly
Solutions:
Problem: "Failed to establish VPN interface"
Solutions:
Problem: VPN connects successfully, pings work for a few seconds, then "Destination Host Unreachable"
Symptoms in logs:
[N2N-edge] TUN not ready 17 times in 5s, sock_ready=1, tun_fd=224
[N2N-edge] WARNING: select() error: Interrupted system call (errno=4)
Solution: Ensure the VPN interface uses blocking mode. In N2nVpnService.cs:
var builder = new Builder(this)
.SetSession("N2N VPN")
.SetMtu(config.Mtu)
.AddAddress(config.IpAddress, prefixLength)
.SetBlocking(true); // Must be true, not false!
Explanation: Android's VPN framework has issues with non-blocking TUN file descriptors. The n2n native code uses select() which doesn't work correctly with non-blocking TUN on Android.
Problem: VPN shows "Connected" but peers cannot communicate. Logs show:
WARNING: invalid transop ID: expected Twofish (2), got AES (3)
Solutions:
var config = new N2nConfiguration
{
// ... other settings ...
Encryption = N2nNative.N2nEncryption.Aes // Must match all peers
};
None = 1Twofish = 2 (default)AES = 3ChaCha20 = 4Speck = 5Enable verbose logging in C#:
var edge = new N2nEdge();
edge.SetLogLevel(4); // Debug level
edge.LogReceived += (s, e) =>
{
Debug.WriteLine($"[N2N] {e.Message}");
};
The native library outputs detailed logs to Android logcat. Use these commands to view them:
# View all n2n related logs
adb logcat -s N2N:* N2N-native:* N2N-edge:*
# Or filter by tags individually:
adb logcat -s N2N:V # C# VpnService logs
adb logcat -s N2N-native:V # Native API logs (n2n_android.c)
adb logcat -s N2N-edge:V # n2n core trace output (stdout redirect)
# Clear logcat and start fresh
adb logcat -c && adb logcat -s N2N:* N2N-native:* N2N-edge:*
Log tags explained:
N2N - High-level logs from C# N2nVpnService (VPN interface setup, configuration)N2N-native - Native library logs from n2n_android.c (init, start, stop, callbacks)N2N-edge - Core n2n trace output (supernode registration, peer discovery, packet flow)N2nBindings/
├── N2nNative.cs # P/Invoke declarations for n2n_android
├── N2nConfiguration.cs # Configuration class with validation
├── N2nEdge.cs # High-level edge wrapper with events
├── N2nVpnService.cs # Android VpnService implementation
├── N2nBindings.csproj # Project file
├── README.md # This file
└── lib/ # Native libraries (libn2n_android.so)
├── arm64-v8a/ # 64-bit ARM (modern devices)
├── armeabi-v7a/ # 32-bit ARM (older devices)
├── x86/ # x86 emulators
└── x86_64/ # x86_64 emulators
# Native source: https://github.com/NakanoMiku13/n2n-android
git clone https://github.com/NakanoMiku13/n2n-android.git
git checkout v1.0.0
cd n2n-android
./build.sh
cd N2nBindings
dotnet build
Test on both ARM64 and ARM32 devices/emulators:
New Features:
IpMode property in N2nConfiguration (Static or Auto)RequestIpAsync() method to request IP from supernodeGetAssignedIp() method to retrieve assigned IPIpAssigned event for async IP assignment notificationN2nConfiguration.CreateAutoIp() for quick setupAPI Additions:
N2nNative.N2nIpMode enum (Static=0, Auto=1)N2nNative.IpAssignedCallback delegateN2nNative.n2n_android_request_ip() P/InvokeN2nNative.n2n_android_set_ip_assigned_callback() P/InvokeN2nNative.n2n_android_get_assigned_ip() P/InvokeN2nEdge.IpAssigned eventN2nEdge.RequestIpAsync() methodN2nEdge.GetAssignedIp() methodUsage:
// Auto IP mode
var config = N2nConfiguration.CreateAutoIp("community", "supernode:7654", "secret");
var (ip, netmask) = await edge.RequestIpAsync(config);
config.IpAddress = ip;
await edge.StartAsync(config, tunFd);
Bug Fixes:
select() to incorrectly report TUN as "not ready"Technical Details:
The n2n native code uses select() to monitor both the UDP socket and TUN interface. With non-blocking mode (SetBlocking(false)), Android's VPN stack would intermittently report the TUN fd as not ready for reading, even when data was available. This caused:
Bug Fixes:
n2n_android_stop() - edge_term_conf() already frees encrypt_key internallyN2nEdge.Dispose() when callbacks were clearedImprovements:
N2N-edge)Documentation:
This project is licensed under the MIT License.
MIT License
Copyright (c) 2024 NakanoMiku13
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
The native libn2n_android.so library is built from a custom fork of n2n optimized for Android:
Repository: https://github.com/NakanoMiku13/n2n-android
Current Version: v1.0.0
This fork includes Android-specific modifications:
n2n_android.h)To build the native library from source, see the repository's build instructions.