Auto-select TAP adapters and persist edge settings

This commit is contained in:
2026-09-01 21:39:58 +08:00
parent 3694d157d1
commit 57e6ba8e1e
5 changed files with 187 additions and 14 deletions
+9
View File
@@ -49,6 +49,15 @@ The portable build creates `release/super-n2n-portable.zip`. It contains
`n2n/edge.exe` and `n2n/supernode.exe`; on Windows Super N2N starts them with
the relative paths `./n2n/edge.exe` and `./n2n/supernode.exe`.
### Windows TAP requirement
N2N uses an installed TAP-Windows virtual adapter; it does not create a new
adapter itself. Super N2N automatically selects an available TAP-Windows
adapter before starting Edge and reports a clear error when none is installed.
The optional TAP device field is only needed to select a specific existing
adapter. Connection settings, including advanced settings, are restored on the
next application launch.
## Gitea Actions
`.gitea/workflows/build-windows.yml` runs when a Release is published and uploads
+5
View File
@@ -8,4 +8,9 @@ them as .\n2n\edge.exe and .\n2n\supernode.exe, so no PATH configuration is requ
Windows WebView2 is required. It is included by default on current Windows 10 and
Windows 11 installations.
N2N requires a TAP-Windows virtual network adapter installed on the computer.
Super N2N automatically selects an available adapter when connecting. It does
not install or create a TAP adapter itself; install the TAP-Windows driver once
if the application reports that no TAP adapter is available.
The bundled n2n programs are distributed under n2n\LICENSE-n2n.txt.
+123 -1
View File
@@ -75,6 +75,20 @@ struct TapTraffic {
sent_bytes: u64,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct TapAdapterRequest {
tap_device: String,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct TapAdapter {
name: String,
description: String,
guid: String,
}
fn now_epoch() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
@@ -183,6 +197,65 @@ fn windows_tap_traffic(tap_device: &str) -> Result<TapTraffic, String> {
traffic
}
#[cfg(target_os = "windows")]
fn windows_tap_adapters() -> Result<Vec<TapAdapter>, String> {
use std::{ptr, slice};
use windows_sys::Win32::{
Foundation::NO_ERROR,
NetworkManagement::IpHelper::{FreeMibTable, GetIfTable2, MIB_IF_TABLE2},
};
fn wide_string(value: &[u16]) -> String {
let end = value
.iter()
.position(|unit| *unit == 0)
.unwrap_or(value.len());
String::from_utf16_lossy(&value[..end])
}
fn guid_string(guid: &windows_sys::core::GUID) -> String {
format!(
"{{{:08x}-{:04x}-{:04x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}}}",
guid.data1,
guid.data2,
guid.data3,
guid.data4[0],
guid.data4[1],
guid.data4[2],
guid.data4[3],
guid.data4[4],
guid.data4[5],
guid.data4[6],
guid.data4[7]
)
}
let mut table: *mut MIB_IF_TABLE2 = ptr::null_mut();
let result = unsafe { GetIfTable2(&mut table) };
if result != NO_ERROR || table.is_null() {
return Err(format!("unable to read network adapter table: {result}"));
}
let adapters = unsafe {
let rows = slice::from_raw_parts((*table).Table.as_ptr(), (*table).NumEntries as usize);
rows.iter()
.filter_map(|row| {
let description = wide_string(&row.Description);
description
.to_ascii_lowercase()
.contains("tap-windows")
.then_some(TapAdapter {
name: wide_string(&row.Alias),
description,
guid: guid_string(&row.InterfaceGuid),
})
})
.collect()
};
unsafe { FreeMibTable(table.cast()) };
Ok(adapters)
}
#[tauri::command]
fn start_service(request: StartRequest, state: State<'_, AppState>) -> Result<ServiceStatus, String> {
let mut processes = state.processes.lock().map_err(|_| "process state is unavailable".to_string())?;
@@ -209,7 +282,7 @@ fn start_service(request: StartRequest, state: State<'_, AppState>) -> Result<Se
return Err(format!("required n2n binary is missing: {}", binary.display()));
}
let child = Command::new(relative_binary)
let mut child = Command::new(relative_binary)
.current_dir(&working_directory)
.args(&request.args)
.stdout(Stdio::null())
@@ -217,6 +290,13 @@ fn start_service(request: StartRequest, state: State<'_, AppState>) -> Result<Se
.spawn()
.map_err(|e| format!("unable to start {}: {}", binary.display(), e))?;
let pid = child.id();
std::thread::sleep(Duration::from_millis(250));
if let Some(status) = child.try_wait().map_err(|error| error.to_string())? {
return Err(format!(
"{} exited immediately after startup with status {}",
request.service, status
));
}
processes.insert(request.service.clone(), child);
Ok(ServiceStatus {
@@ -393,6 +473,47 @@ fn tap_traffic(request: TapTrafficRequest) -> Result<TapTraffic, String> {
Err("TAP traffic monitoring is not supported on this operating system".to_string())
}
#[tauri::command]
fn resolve_tap_adapter(request: TapAdapterRequest) -> Result<TapAdapter, String> {
let requested = request.tap_device.trim();
if requested.contains(['/', '\\']) || requested == "." || requested == ".." {
return Err("invalid TAP device name".to_string());
}
#[cfg(target_os = "windows")]
{
let adapters = windows_tap_adapters()?;
if adapters.is_empty() {
return Err(
"未找到 TAP-Windows 网卡。请先安装 TAP-Windows 驱动,然后重新连接。".to_string(),
);
}
if requested.is_empty() {
return Ok(adapters.into_iter().next().expect("non-empty adapter list"));
}
return adapters
.into_iter()
.find(|adapter| {
adapter.name.eq_ignore_ascii_case(requested)
|| adapter.description.eq_ignore_ascii_case(requested)
|| adapter.guid.eq_ignore_ascii_case(requested)
})
.ok_or_else(|| format!("找不到指定的 TAP 设备:{requested}"));
}
#[cfg(not(target_os = "windows"))]
{
if requested.is_empty() {
return Err("请输入 TAP 设备名称".to_string());
}
Ok(TapAdapter {
name: requested.to_string(),
description: requested.to_string(),
guid: String::new(),
})
}
}
#[tauri::command]
fn firewall_status() -> Result<ToolResult, String> {
#[cfg(target_os = "windows")]
@@ -497,6 +618,7 @@ pub fn run() {
run_ping,
run_tcping,
tap_traffic,
resolve_tap_adapter,
firewall_status,
export_log
])
+3
View File
@@ -36,4 +36,7 @@ export const checkFirewall = (fallback) => nativeCall('firewall_status', {}, fal
export const tapTraffic = (tapDevice) => nativeCall('tap_traffic', { request: { tapDevice } }, () => {
throw new Error('浏览器预览无法读取 TAP 流量')
})
export const resolveTapAdapter = (tapDevice) => nativeCall('resolve_tap_adapter', { request: { tapDevice } }, () => {
throw new Error('浏览器预览无法识别 TAP 网卡')
})
export const exportLog = (contents) => nativeCall('export_log', { contents }, () => null)
+47 -13
View File
@@ -4,7 +4,7 @@ import {
ArrowDown, ArrowUp, CheckCircle2, ChevronDown, CircleAlert, Gauge, Globe2, Network, Play,
Download, Router, ScrollText, Server, Settings2, Square, Wifi,
} from 'lucide-react'
import { exportLog, isTauri, serviceStatus, startService, stopService, tapTraffic } from './lib/tauri'
import { exportLog, isTauri, resolveTapAdapter, serviceStatus, startService, stopService, tapTraffic } from './lib/tauri'
import './styles.css'
const initialEdge = {
@@ -15,13 +15,39 @@ const initialEdge = {
ip: '',
encryptionKey: '',
managementPort: '5644',
tapDevice: 'n2n0',
tapDevice: '',
}
const edgeConfigStorageKey = 'super-n2n.edge-config.v1'
function loadEdgeConfig() {
const fallback = { ...initialEdge }
if (typeof window === 'undefined') return fallback
try {
const saved = JSON.parse(window.localStorage.getItem(edgeConfigStorageKey) || 'null')
if (!saved || typeof saved !== 'object' || Array.isArray(saved)) return fallback
for (const [field, defaultValue] of Object.entries(initialEdge)) {
if (typeof saved[field] === typeof defaultValue) fallback[field] = saved[field]
}
} catch {
// A damaged or unavailable browser store must not block the connection form.
}
return fallback
}
function saveEdgeConfig(config) {
try {
window.localStorage.setItem(edgeConfigStorageKey, JSON.stringify(config))
} catch {
// The form stays usable when the platform denies local storage access.
}
}
function App() {
const [home, setHome] = useState('edge')
const [edge, setEdge] = useState(initialEdge)
const [edge, setEdge] = useState(loadEdgeConfig)
const [edgeRunning, setEdgeRunning] = useState(false)
const [activeTapDevice, setActiveTapDevice] = useState('')
const [advancedOpen, setAdvancedOpen] = useState(false)
const [logOpen, setLogOpen] = useState(false)
const [logs, setLogs] = useState([])
@@ -34,6 +60,10 @@ function App() {
setLogs((current) => [{ time, level, text }, ...current].slice(0, 100))
}
useEffect(() => {
saveEdgeConfig(edge)
}, [edge])
useEffect(() => {
let active = true
serviceStatus('edge')
@@ -47,7 +77,7 @@ function App() {
}, [])
useEffect(() => {
if (!edgeRunning) {
if (!edgeRunning || !activeTapDevice) {
lastTapSample.current = null
setTraffic(null)
return undefined
@@ -57,7 +87,7 @@ function App() {
let timer
const poll = async () => {
try {
const sample = await tapTraffic(edge.tapDevice)
const sample = await tapTraffic(activeTapDevice)
const sampledAt = Date.now()
const previous = lastTapSample.current
const elapsedSeconds = previous ? Math.max((sampledAt - previous.sampledAt) / 1000, .25) : 1
@@ -76,7 +106,7 @@ function App() {
active = false
window.clearTimeout(timer)
}
}, [edgeRunning, edge.tapDevice])
}, [activeTapDevice, edgeRunning])
const updateEdge = (field, value) => setEdge((current) => ({ ...current, [field]: value }))
const notify = (text, tone = 'error') => {
@@ -89,6 +119,7 @@ function App() {
try {
const status = await stopService('edge')
setEdgeRunning(Boolean(status.running))
setActiveTapDevice('')
appendLog('已断开 N2N 网络')
notify('已断开 N2N 网络', 'neutral')
} catch (error) {
@@ -115,12 +146,15 @@ function App() {
}
try {
const tapAdapter = await resolveTapAdapter(edge.tapDevice)
appendLog('已选择 TAP 设备:' + tapAdapter.name)
const status = await startService({
service: 'edge',
binaryPath: './n2n/edge.exe',
args: buildEdgeArgs(edge),
args: buildEdgeArgs(edge, tapAdapter.name),
})
setEdgeRunning(Boolean(status.running))
setActiveTapDevice(tapAdapter.name)
appendLog('已连接到群组 ' + edge.community, 'success')
notify('已连接到 ' + edge.community, 'success')
} catch (error) {
@@ -146,7 +180,7 @@ function App() {
</div>
{home === 'edge'
? <EdgeHome edge={edge} updateEdge={updateEdge} running={edgeRunning} traffic={traffic} advancedOpen={advancedOpen} setAdvancedOpen={setAdvancedOpen} logOpen={logOpen} setLogOpen={setLogOpen} logs={logs} onToggle={toggleEdge} onExportLog={() => exportLogs(logs, notify)} />
? <EdgeHome edge={edge} updateEdge={updateEdge} running={edgeRunning} traffic={traffic} activeTapDevice={activeTapDevice} advancedOpen={advancedOpen} setAdvancedOpen={setAdvancedOpen} logOpen={logOpen} setLogOpen={setLogOpen} logs={logs} onToggle={toggleEdge} onExportLog={() => exportLogs(logs, notify)} />
: <SupernodeHome />}
</section>
@@ -155,7 +189,7 @@ function App() {
</main>
}
function EdgeHome({ edge, updateEdge, running, traffic, advancedOpen, setAdvancedOpen, logOpen, setLogOpen, logs, onToggle, onExportLog }) {
function EdgeHome({ edge, updateEdge, running, traffic, activeTapDevice, advancedOpen, setAdvancedOpen, logOpen, setLogOpen, logs, onToggle, onExportLog }) {
const toggleAdvanced = () => {
setLogOpen(false)
setAdvancedOpen((current) => !current)
@@ -203,7 +237,7 @@ function EdgeHome({ edge, updateEdge, running, traffic, advancedOpen, setAdvance
{running ? '断开连接' : '连接网络'}
</button>
{running && <TrafficStrip traffic={traffic} tapDevice={edge.tapDevice} />}
{running && <TrafficStrip traffic={traffic} tapDevice={activeTapDevice} />}
</section>
<div className="secondary-actions">
@@ -214,7 +248,7 @@ function EdgeHome({ edge, updateEdge, running, traffic, advancedOpen, setAdvance
{advancedOpen && <div className="advanced-fields">
<label><span>加密密钥</span><input type="password" value={edge.encryptionKey} onChange={(event) => updateEdge('encryptionKey', event.target.value)} placeholder="可选" disabled={running} /></label>
<label><span>管理端口</span><input inputMode="numeric" value={edge.managementPort} onChange={(event) => updateEdge('managementPort', event.target.value)} disabled={running} /></label>
<label><span>TAP 设备名称</span><input value={edge.tapDevice} onChange={(event) => updateEdge('tapDevice', event.target.value)} disabled={running} /></label>
<label><span>TAP 设备可选</span><input value={edge.tapDevice} onChange={(event) => updateEdge('tapDevice', event.target.value)} placeholder="留空自动选择" disabled={running} /></label>
</div>}
</section>
@@ -323,12 +357,12 @@ function addOption(args, flag, value) {
if (String(value ?? '').trim()) args.push(flag, String(value).trim())
}
function buildEdgeArgs(config) {
function buildEdgeArgs(config, tapDevice) {
const args = []
addOption(args, '-c', config.community)
addOption(args, '-l', config.serverHost + ':' + (config.serverPort || '7777'))
addOption(args, '-a', config.dhcp ? 'dhcp:0.0.0.0' : config.ip)
addOption(args, '-d', config.tapDevice)
addOption(args, '-d', tapDevice)
addOption(args, '-t', config.managementPort)
if (config.encryptionKey) addOption(args, '-k', config.encryptionKey)
args.push('-f', '-v', '-v')