use serde::Deserialize;
#[derive(Deserialize)]
struct SingboxConfig {
#[serde(default)]
inbounds: Vec<SingboxInbound>,
}
#[derive(Deserialize)]
struct SingboxInbound {
#[serde(rename = "type", default)]
kind: Option<String>,
#[serde(default = "default_enabled")]
enabled: bool,
}
fn default_enabled() -> bool {
true
}
fn parse_mihomo(config: &str) -> Result<serde_mihomo::Config, serde_mihomo::Error> {
serde_mihomo::Config::from_yaml_str(config)
}
pub fn mihomo_tun_enabled(config: &str) -> Result<bool, serde_mihomo::Error> {
Ok(parse_mihomo(config)?.tun.is_some_and(|tun| tun.enable))
}
pub fn has_tun_yaml(config: &str) -> bool {
parse_mihomo(config).is_ok_and(|config| config.tun.is_some())
}
pub fn singbox_tun_enabled(config: &str) -> Result<bool, serde_json::Error> {
let parsed: SingboxConfig = serde_json::from_str(config)?;
Ok(parsed
.inbounds
.iter()
.any(|inbound| inbound.kind.as_deref() == Some("tun") && inbound.enabled))
}
pub fn ensure_tun_device() {
#[cfg(target_os = "linux")]
linux::ensure_tun_device();
#[cfg(not(target_os = "linux"))]
crate::log_info!("Skipping TUN device setup: only supported on Linux");
}
#[cfg(target_os = "linux")]
mod linux {
use crate::utils::command::execute;
use crate::{log_info, log_warn};
use std::ffi::CString;
use std::io::ErrorKind;
use std::os::unix::fs::PermissionsExt;
use std::path::Path;
const TUN_MODULE: &str = "tun";
const TUN_DEVICE: &str = "/dev/net/tun";
const TUN_DEVICE_DIR: &str = "/dev/net";
const TUN_MAJOR: libc::c_uint = 10;
const TUN_MINOR: libc::c_uint = 200;
const TUN_MODE: libc::mode_t = 0o666;
pub(super) fn ensure_tun_device() {
log_info!(
"Preparing TUN device (admin: {})",
if is_admin::is_admin() { "yes" } else { "no" }
);
ensure_tun_module();
ensure_tun_device_node();
}
fn ensure_tun_module() {
if tun_module_loaded() {
log_info!("Kernel module '{}' already loaded", TUN_MODULE);
return;
}
log_info!(
"Kernel module '{}' not loaded, running modprobe",
TUN_MODULE
);
match execute("modprobe", &[TUN_MODULE]) {
Ok(_) => log_info!("Kernel module '{}' loaded", TUN_MODULE),
Err(e) => log_warn!(
"Failed to load kernel module '{}': {} (continuing)",
TUN_MODULE,
e
),
}
}
fn tun_module_loaded() -> bool {
match std::fs::read_to_string("/proc/modules") {
Ok(modules) => modules
.lines()
.any(|line| line.split_whitespace().next() == Some(TUN_MODULE)),
Err(e) => {
log_warn!(
"Failed to read /proc/modules: {} (will try modprobe anyway)",
e
);
false
}
}
}
fn ensure_tun_device_node() {
let path = Path::new(TUN_DEVICE);
if !path.exists() {
log_info!("TUN device {} missing, creating it", TUN_DEVICE);
match create_tun_device(path) {
Ok(()) => log_info!("Created TUN device {}", TUN_DEVICE),
Err(e) => {
log_warn!(
"Failed to create TUN device {}: {} (continuing)",
TUN_DEVICE,
e
);
return;
}
}
}
ensure_tun_permissions(path);
}
fn create_tun_device(path: &Path) -> std::io::Result<()> {
std::fs::create_dir_all(TUN_DEVICE_DIR)?;
let c_path = CString::new(path.as_os_str().as_encoded_bytes())
.map_err(|e| std::io::Error::new(ErrorKind::InvalidInput, e))?;
let rc = unsafe {
libc::mknod(
c_path.as_ptr(),
libc::S_IFCHR | TUN_MODE,
libc::makedev(TUN_MAJOR, TUN_MINOR),
)
};
if rc != 0 {
return Err(std::io::Error::last_os_error());
}
Ok(())
}
fn ensure_tun_permissions(path: &Path) {
let Ok(metadata) = std::fs::metadata(path) else {
log_warn!("Failed to read metadata of {} (continuing)", TUN_DEVICE);
return;
};
let mode = metadata.permissions().mode() & 0o777;
if mode == TUN_MODE {
log_info!("TUN device {} already has mode {:o}", TUN_DEVICE, mode);
return;
}
log_info!(
"TUN device {} has mode {:o}, setting to {:o}",
TUN_DEVICE,
mode,
TUN_MODE
);
let Ok(c_path) = CString::new(path.as_os_str().as_encoded_bytes()) else {
log_warn!("TUN device path contains a NUL byte (continuing)");
return;
};
let rc = unsafe { libc::chmod(c_path.as_ptr(), TUN_MODE) };
if rc != 0 {
log_warn!(
"Failed to chmod {} to {:o}: {} (continuing)",
TUN_DEVICE,
TUN_MODE,
std::io::Error::last_os_error()
);
} else {
log_info!("Set TUN device {} mode to {:o}", TUN_DEVICE, TUN_MODE);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn yaml_enabled() {
assert!(mihomo_tun_enabled("tun:\n enable: true\n stack: gVisor\n").unwrap());
assert!(mihomo_tun_enabled("port: 7890\ntun:\n enable: true\n").unwrap());
}
#[test]
fn yaml_disabled_or_absent() {
assert!(!mihomo_tun_enabled("port: 7890\n").unwrap());
assert!(!mihomo_tun_enabled("tun:\n enable: false\n").unwrap());
assert!(!mihomo_tun_enabled("other:\n enable: true\n").unwrap());
}
#[test]
fn yaml_has_tun_reports_presence() {
assert!(has_tun_yaml("tun:\n enable: false\n"));
assert!(has_tun_yaml("tun:\n stack: gVisor\n"));
assert!(has_tun_yaml("port: 7890\ntun: {}\n"));
assert!(!has_tun_yaml("port: 7890\n"));
assert!(!has_tun_yaml("tun-proxy: 7890\n"));
assert!(!has_tun_yaml("proxy:\n tun:\n enable: true\n"));
assert!(!has_tun_yaml("tun: [unclosed\n"));
}
#[test]
fn yaml_handles_comments_and_spacing() {
assert!(mihomo_tun_enabled("tun: # enable tun\n enable: true # yes\n").unwrap());
assert!(!mihomo_tun_enabled("proxy:\n tun:\n enable: true\n").unwrap());
}
#[test]
fn yaml_surfaces_parse_errors() {
assert!(mihomo_tun_enabled("tun: [unclosed\n").is_err());
assert!(mihomo_tun_enabled("proxies:\n - name: broken\n").is_err());
}
#[test]
fn json_enabled() {
assert!(singbox_tun_enabled(r#"{"inbounds":[{"type":"tun","enabled":true}]}"#).unwrap());
assert!(singbox_tun_enabled(r#"{"inbounds":[{"type":"tun"}]}"#).unwrap());
}
#[test]
fn json_disabled_or_absent() {
assert!(!singbox_tun_enabled(r#"{"inbounds":[{"type":"tun","enabled":false}]}"#).unwrap());
assert!(!singbox_tun_enabled(r#"{"inbounds":[{"type":"mixed"}]}"#).unwrap());
}
#[test]
fn json_surfaces_parse_errors() {
assert!(singbox_tun_enabled("not json").is_err());
}
#[test]
fn shipped_config_assets_are_valid() {
assert!(!mihomo_tun_enabled(include_str!("../assets/mihomo.yaml")).unwrap());
assert!(!has_tun_yaml(include_str!("../assets/mihomo.yaml")));
let tun_block = include_str!("../assets/mihomo_tun.yaml");
assert!(mihomo_tun_enabled(tun_block).unwrap());
assert!(has_tun_yaml(tun_block));
}
}