Skip to content

Commit f81da6b

Browse files
Merge pull request #142 from loss-and-quick/feat/tun-engine-hev
feat(tun): add hev-socks5-tunnel as a selectable TUN engine
2 parents 0e4555a + 11767e3 commit f81da6b

42 files changed

Lines changed: 696 additions & 128 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -309,11 +309,13 @@ jobs:
309309
shell: bash
310310
run: |
311311
# tauri-build validates declared bundle.resources at compile time. The
312-
# real wintun.dll / libcronet.dll are fetched only for the nightly/release
313-
# bundle; placeholders satisfy the path check for this compile-only job.
312+
# real wintun.dll / libcronet.dll / msys-2.0.dll are fetched only for the
313+
# nightly/release bundle; placeholders satisfy the path check for this
314+
# compile-only job.
314315
mkdir -p src-tauri/binaries
315316
: > src-tauri/binaries/wintun.dll
316317
: > src-tauri/binaries/libcronet.dll
318+
: > src-tauri/binaries/msys-2.0.dll
317319
318320
- name: Compile the Tauri app (no bundle)
319321
run: cargo build -p kasumi-desktop

.github/workflows/nightly.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -242,12 +242,14 @@ jobs:
242242
mkdir -p "$out"
243243
cp "target/$t/release/kasumi-desktop.exe" "$out/"
244244
cp "target/$t/release/kasumi-helper.exe" "$out/"
245-
for c in xray sing-box tun2socks; do
245+
for c in xray sing-box tun2socks hev-socks5-tunnel; do
246246
cp "src-tauri/binaries/$c-$t.exe" "$out/$c.exe"
247247
done
248248
cp src-tauri/binaries/wintun.dll "$out/"
249249
# libcronet.dll must sit next to sing-box for its naive outbound to load.
250250
cp src-tauri/binaries/libcronet.dll "$out/"
251+
# msys-2.0.dll must sit next to hev-socks5-tunnel (msys2 build) to load.
252+
cp src-tauri/binaries/msys-2.0.dll "$out/"
251253
: > "$out/portable.dat"
252254
name=$(scripts/artifact-name.sh windows-portable)
253255
( cd dist/portable && 7z a -tzip "../$name" Kasumi-Proxy >/dev/null )
@@ -265,7 +267,7 @@ jobs:
265267
mkdir -p "$out"
266268
cp "target/$t/release/kasumi-desktop" "$out/"
267269
cp "target/$t/release/kasumi-helper" "$out/"
268-
for c in xray sing-box tun2socks; do
270+
for c in xray sing-box tun2socks hev-socks5-tunnel; do
269271
cp "src-tauri/binaries/$c-$t" "$out/$c"
270272
done
271273
# libcronet.so must sit next to sing-box for its naive outbound to load.

.github/workflows/release.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -443,12 +443,14 @@ jobs:
443443
mkdir -p "$out"
444444
cp "target/$t/release/kasumi-desktop.exe" "$out/"
445445
cp "target/$t/release/kasumi-helper.exe" "$out/"
446-
for c in xray sing-box tun2socks; do
446+
for c in xray sing-box tun2socks hev-socks5-tunnel; do
447447
cp "src-tauri/binaries/$c-$t.exe" "$out/$c.exe"
448448
done
449449
cp src-tauri/binaries/wintun.dll "$out/"
450450
# libcronet.dll must sit next to sing-box for its naive outbound to load.
451451
cp src-tauri/binaries/libcronet.dll "$out/"
452+
# msys-2.0.dll must sit next to hev-socks5-tunnel (msys2 build) to load.
453+
cp src-tauri/binaries/msys-2.0.dll "$out/"
452454
: > "$out/portable.dat"
453455
name=$(scripts/artifact-name.sh windows-portable)
454456
( cd dist/portable && 7z a -tzip "../$name" Kasumi-Proxy >/dev/null )
@@ -466,7 +468,7 @@ jobs:
466468
mkdir -p "$out"
467469
cp "target/$t/release/kasumi-desktop" "$out/"
468470
cp "target/$t/release/kasumi-helper" "$out/"
469-
for c in xray sing-box tun2socks; do
471+
for c in xray sing-box tun2socks hev-socks5-tunnel; do
470472
cp "src-tauri/binaries/$c-$t" "$out/$c"
471473
done
472474
# libcronet.so must sit next to sing-box for its naive outbound to load.

Cargo.lock

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/kasumi-backend/src/lifecycle.rs

Lines changed: 56 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ use regex::Regex;
1212
use tokio::process::Child;
1313

1414
use kasumi_core::enums::{CoreEngine, TunEngine};
15+
use kasumi_core::hev_config::build_hev_config;
1516
use kasumi_core::state::{AppState, DEFAULT_LOCAL_SOCKS_PORT};
17+
use kasumi_core::tun::TunOptions;
1618

1719
use crate::commands::{CommandError, build_profile_config};
1820
use crate::fs::{exists, read_text, remove_file, write_text};
@@ -51,11 +53,12 @@ pub fn random_tun_iface() -> String {
5153
}
5254

5355
/// Build the config for `profile_id` (else the active profile), write it and the
54-
/// engine marker, and return the engine + resolved TUN engine + local SOCKS port.
56+
/// engine marker, and return the engine + resolved TUN engine + external-engine
57+
/// tuning + local SOCKS port.
5558
pub async fn resolve_and_write_config(
5659
platform: &dyn Platform,
5760
profile_id: Option<&str>,
58-
) -> Result<(Engine, TunEngine, u16), CommandError> {
61+
) -> Result<(Engine, TunEngine, TunOptions, u16), CommandError> {
5962
let paths = platform.paths();
6063
let state = read_json::<AppState>(&paths.app_state).await;
6164
let id = profile_id
@@ -78,10 +81,12 @@ pub async fn resolve_and_write_config(
7881
write_text(&paths.engine_file, engine_label(engine))
7982
.await
8083
.map_err(|e| CommandError(e.to_string()))?;
81-
let socks_port = state
82-
.and_then(|s| s.settings.local_socks_port)
84+
let settings = state.map(|s| s.settings).unwrap_or_default();
85+
let socks_port = settings
86+
.local_socks_port
8387
.unwrap_or(DEFAULT_LOCAL_SOCKS_PORT);
84-
Ok((engine, tun, socks_port))
88+
let tun_opts = settings.tun_options();
89+
Ok((engine, tun, tun_opts, socks_port))
8590
}
8691

8792
fn engine_label(engine: CoreEngine) -> &'static str {
@@ -326,62 +331,68 @@ pub async fn spawn_core(
326331
.await
327332
}
328333

329-
/// Spawn tun2socks bridging `iface` to the SOCKS port, logging to `log_path`.
330-
/// `fwmark`, when set, marks tun2socks' own upstream socket so an `ip rule` can
331-
/// keep it out of the tunnel — that's a Linux SO_MARK feature. Windows has no
332-
/// fwmark (its server bypass is a host route), so it passes `None`.
333-
pub async fn spawn_tun2socks(
334-
bin: &str,
335-
iface: &str,
336-
socks_port: u16,
337-
log_path: &Path,
338-
fwmark: Option<u32>,
339-
) -> std::io::Result<Child> {
340-
spawn_logged(
341-
&tun2socks_argv(bin, iface, socks_port, fwmark),
342-
&HashMap::new(),
343-
log_path,
344-
false,
345-
)
346-
.await
347-
}
348-
349-
/// tun2socks' argv. Split out (like [`core_argv`]) so a caller that supervises the
350-
/// spawn itself can reuse the exact command. tun2socks needs no special env.
351-
pub fn tun2socks_argv(bin: &str, iface: &str, socks_port: u16, fwmark: Option<u32>) -> Vec<String> {
334+
/// Spawn tun2socks bridging the tun to the local SOCKS port. `fwmark`, when set,
335+
/// marks tun2socks' own upstream socket so an `ip rule` can keep it out of the
336+
/// tunnel — a Linux SO_MARK feature. Windows has no fwmark (its server bypass is a
337+
/// host route), so it passes `None`.
338+
async fn spawn_tun2socks(s: &TunSpawn<'_>) -> std::io::Result<Child> {
352339
let mut argv = vec![
353-
bin.to_owned(),
340+
s.bin.to_owned(),
354341
"-device".into(),
355-
format!("tun://{iface}"),
342+
format!("tun://{}", s.iface),
356343
"-proxy".into(),
357-
format!("socks5://127.0.0.1:{socks_port}"),
344+
format!("socks5://127.0.0.1:{}", s.socks_port),
345+
// The TUN MTU setting applies to every external engine; tun2socks creates
346+
// its own tun, so it must be told the MTU here (hev takes it in its YAML).
347+
"-mtu".into(),
348+
s.opts.mtu.to_string(),
358349
];
359-
if let Some(mark) = fwmark {
350+
if let Some(mark) = s.fwmark {
360351
argv.push("-fwmark".into());
361352
argv.push(mark.to_string());
362353
}
363-
argv
354+
spawn_logged(&argv, &std::collections::HashMap::new(), s.log_path, false).await
355+
}
356+
357+
/// Everything needed to bring up one external TUN engine, gathered so adding an
358+
/// engine is a single match arm. `bin` is the engine binary (resolved per-platform);
359+
/// `cfg_path` is where a config-file engine (hev) writes its YAML; `ipv4`/`ipv6` are
360+
/// the addresses such an engine assigns to the tun it creates itself; `opts` carries
361+
/// the resolved tuning.
362+
pub struct TunSpawn<'a> {
363+
pub bin: &'a str,
364+
pub iface: &'a str,
365+
pub ipv4: &'a str,
366+
pub ipv6: Option<&'a str>,
367+
pub socks_port: u16,
368+
pub log_path: &'a Path,
369+
pub fwmark: Option<u32>,
370+
pub cfg_path: &'a Path,
371+
pub opts: &'a TunOptions,
364372
}
365373

366374
/// The single place that knows how to launch an external TUN engine. Every shell
367375
/// (desktop, root daemon) routes its bring-up through here, so adding a new engine
368376
/// is one more arm — nothing else in the orchestration learns engine specifics.
369-
/// `bin` is the engine's binary (resolved per-platform). `SingboxTun` has no
370-
/// external helper (the sing-box core owns the tun) and must not reach this.
371-
pub async fn spawn_tun_engine(
372-
tun: TunEngine,
373-
bin: &str,
374-
iface: &str,
375-
socks_port: u16,
376-
log_path: &Path,
377-
fwmark: Option<u32>,
378-
) -> std::io::Result<Child> {
377+
/// `SingboxTun` has no external helper (the sing-box core owns the tun) and must
378+
/// not reach this.
379+
pub async fn spawn_tun_engine(tun: TunEngine, s: &TunSpawn<'_>) -> std::io::Result<Child> {
379380
match tun {
380-
TunEngine::Tun2socks => spawn_tun2socks(bin, iface, socks_port, log_path, fwmark).await,
381+
TunEngine::Tun2socks => spawn_tun2socks(s).await,
382+
TunEngine::Hev => spawn_hev(s).await,
381383
TunEngine::SingboxTun => unreachable!("SingboxTun has no external helper to spawn"),
382384
}
383385
}
384386

387+
/// hev creates and addresses its own tun from a YAML config: render it, write it
388+
/// next to the runtime state, then run `<hev_bin> <cfg>`.
389+
async fn spawn_hev(s: &TunSpawn<'_>) -> std::io::Result<Child> {
390+
let yaml = build_hev_config(s.iface, s.ipv4, s.ipv6, s.socks_port, s.fwmark, s.opts);
391+
write_text(s.cfg_path, &yaml).await?;
392+
let argv = [s.bin.to_owned(), s.cfg_path.to_string_lossy().into_owned()];
393+
spawn_logged(&argv, &std::collections::HashMap::new(), s.log_path, false).await
394+
}
395+
385396
/// Confirm the core stayed up: a bad config makes it exit within ~1s.
386397
pub async fn verify_core_alive(pid: i32, bin: &str, attempts: u32, delay: Duration) -> bool {
387398
for _ in 0..attempts {
@@ -448,7 +459,7 @@ mod tests {
448459
.unwrap();
449460

450461
// No explicit id → uses active_id.
451-
let (engine, tun, socks) = resolve_and_write_config(&p, None).await.unwrap();
462+
let (engine, tun, _tun_opts, socks) = resolve_and_write_config(&p, None).await.unwrap();
452463
assert_eq!(engine, CoreEngine::Xray);
453464
assert_eq!(tun, TunEngine::Tun2socks);
454465
assert_eq!(socks, 11080);

crates/kasumi-backend/src/platform.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use tokio::sync::mpsc;
1010

1111
use kasumi_core::contract::{LogTarget, ServiceState};
1212
use kasumi_core::enums::{CoreEngine, TunEngine};
13+
use kasumi_core::tun::TunOptions;
1314

1415
use crate::lifecycle::spawn_core;
1516
use crate::net::ProxyStatus;
@@ -125,8 +126,12 @@ pub struct AppInfo {
125126
pub struct StartDataPath {
126127
pub engine: Engine,
127128
/// The resolved TUN engine. `SingboxTun` uses the core's own tun (native for
128-
/// sing-box); `Tun2socks` fronts a socks-only core with an external tun.
129+
/// sing-box); `Tun2socks`/`Hev` front a socks-only core with an external tun.
129130
pub tun: TunEngine,
131+
/// External-engine tuning (mtu, buffers, timeouts, …), resolved once from the
132+
/// settings so the data-path owner (incl. the desktop root helper, across the
133+
/// privilege boundary) needn't re-read the settings schema.
134+
pub tun_opts: TunOptions,
130135
pub socks_port: u16,
131136
}
132137

crates/kasumi-backend/src/service.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -149,14 +149,15 @@ impl Service {
149149
})
150150
.await
151151
.map_err(|e| e.to_string())?;
152-
let (engine, tun, socks_port) =
152+
let (engine, tun, tun_opts, socks_port) =
153153
resolve_and_write_config(&*self.platform, id.as_deref())
154154
.await
155155
.map_err(|e| e.0)?;
156156
self.platform
157157
.start_data_path(StartDataPath {
158158
engine,
159159
tun,
160+
tun_opts,
160161
socks_port,
161162
})
162163
.await
@@ -181,13 +182,15 @@ impl Service {
181182
})
182183
.await
183184
.map_err(|e| e.to_string())?;
184-
let (engine, tun, socks_port) = resolve_and_write_config(&*self.platform, None)
185-
.await
186-
.map_err(|e| e.0)?;
185+
let (engine, tun, tun_opts, socks_port) =
186+
resolve_and_write_config(&*self.platform, None)
187+
.await
188+
.map_err(|e| e.0)?;
187189
self.platform
188190
.start_data_path(StartDataPath {
189191
engine,
190192
tun,
193+
tun_opts,
191194
socks_port,
192195
})
193196
.await

crates/kasumi-core/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ percent-encoding = "2"
1616
base64 = "0.22"
1717
uuid = { version = "1", features = ["v4"] }
1818
strum = { version = "0.26", features = ["derive"] }
19+
yaml_serde = "0.10.4"
1920

2021
[dev-dependencies]
2122
tempfile = "3"

crates/kasumi-core/src/enums.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,10 @@ pub enum CoreEngine {
2424
}
2525

2626
/// Which engine bridges the TUN device to the proxy core. `SingboxTun` means
27-
/// "use sing-box's own native TUN stack" (sing-box core only); `Tun2socks` is an
28-
/// external userspace tun→socks process in front of a socks-only core. Further
29-
/// engines plug in as new variants. Wire values: `"singbox-tun"`, `"tun2socks"`.
27+
/// "use sing-box's own native TUN stack" (sing-box core only); `Tun2socks` and
28+
/// `Hev` are external userspace tun→socks processes in front of a socks-only core.
29+
/// Further engines plug in as new variants. Wire values: `"singbox-tun"`,
30+
/// `"tun2socks"`, `"hev"`.
3031
#[derive(
3132
Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, strum::EnumIter, specta::Type,
3233
)]
@@ -35,6 +36,8 @@ pub enum TunEngine {
3536
SingboxTun,
3637
#[serde(rename = "tun2socks")]
3738
Tun2socks,
39+
#[serde(rename = "hev")]
40+
Hev,
3841
}
3942

4043
/// The wire label of a TUN engine — its serde value, the single source. Used as
@@ -347,6 +350,7 @@ mod tests {
347350
fn tun_engine_values() {
348351
assert_eq!(wire(&TunEngine::SingboxTun), "\"singbox-tun\"");
349352
assert_eq!(wire(&TunEngine::Tun2socks), "\"tun2socks\"");
353+
assert_eq!(wire(&TunEngine::Hev), "\"hev\"");
350354
}
351355

352356
#[test]

0 commit comments

Comments
 (0)