diff --git a/changelog.d/fixed-catch-tunnels-renamed-to-the-kernel-d126.md b/changelog.d/fixed-catch-tunnels-renamed-to-the-kernel-d126.md new file mode 100644 index 0000000..b380e34 --- /dev/null +++ b/changelog.d/fixed-catch-tunnels-renamed-to-the-kernel-d126.md @@ -0,0 +1,9 @@ +_2026-04-26_ + +## English + +Catch tunnels renamed to the kernel default `if` pattern (issue #86) — e.g. `tun0` renamed to `if33` is no longer visible to target apps. + +## Русский + +Прячем туннели, переименованные в дефолтный для ядра паттерн `if` (issue #86) — например `tun0` с именем `if33` теперь скрыт от целевых приложений. diff --git a/data/interfaces.toml b/data/interfaces.toml index 0577b52..dc183f2 100644 --- a/data/interfaces.toml +++ b/data/interfaces.toml @@ -63,6 +63,13 @@ note = "GRE tunnels" match = { contains = "vpn" } note = "catch-all for renamed clients (myvpn0, vpn-client, xvpn1, ...)" +[[vpn]] +match = { prefix = "if", suffix = "digits" } +note = """Anonymous netdev / renamed tunnel using the kernel's default \ +naming pattern (e.g. `ip link set tun0 name if33` from issue #86). \ +Does NOT match `ifb` — those are kernel intermediate-functional-block \ +traffic-shaping ifaces (different shape: `if` + letter, not + digit).""" + # ── Test vectors ────────────────────────────────────────────────────── # Array of {name, is_vpn} fixtures. Codegen renders these into per- @@ -198,12 +205,40 @@ is_vpn = false name = "rndis0" is_vpn = false -# The renamed-tun trick from issue #86 — name-only matching cannot -# catch this; here just confirms the matcher does not over-match on a -# generic prefix. +# The renamed-tun trick from issue #86 — caught by the +# `if` + digits rule above. [[test]] name = "if33" +is_vpn = true + +[[test]] +name = "if0" +is_vpn = true + +[[test]] +name = "if99" +is_vpn = true + +# `ifb` is the kernel's intermediate-functional-block (traffic +# shaping). Different shape (`if` + letter) — must NOT match. + +[[test]] +name = "ifb0" +is_vpn = false + +[[test]] +name = "ifb1" +is_vpn = false + +# `if` alone or with non-digit suffix — must NOT match. + +[[test]] +name = "if" +is_vpn = false + +[[test]] +name = "if_inet6" is_vpn = false # Edge cases diff --git a/kmod/generated/iface_lists.h b/kmod/generated/iface_lists.h index 52b38ad..7e57609 100644 --- a/kmod/generated/iface_lists.h +++ b/kmod/generated/iface_lists.h @@ -131,6 +131,9 @@ static inline bool vpnhide_iface_is_vpn(const char *name) /* catch-all for renamed clients (myvpn0, vpn-client, xvpn1, ...) */ if (vpnhide_iface_contains_ci(name, "vpn")) return true; + /* Anonymous netdev / renamed tunnel using the kernel's default naming pattern (e.g. `ip link set tun0 name if33` from issue #86). Does NOT match `ifb` — those are kernel intermediate-functional-block traffic-shaping ifaces (different shape: `if` + letter, not + digit). */ + if (vpnhide_iface_starts_with_then_digits_ci(name, "if")) + return true; return false; } diff --git a/kmod/test_iface_lists.c b/kmod/test_iface_lists.c index 0b60e4a..eb22be9 100644 --- a/kmod/test_iface_lists.c +++ b/kmod/test_iface_lists.c @@ -55,7 +55,13 @@ int main(void) check("dummy0", false); check("bnep0", false); check("rndis0", false); - check("if33", false); + check("if33", true); + check("if0", true); + check("if99", true); + check("ifb0", false); + check("ifb1", false); + check("if", false); + check("if_inet6", false); check("", false); check("tunl", true); check("atun0", false); @@ -65,6 +71,6 @@ int main(void) fprintf(stderr, "%d test(s) failed\n", failures); return 1; } - printf("OK: 36 vectors passed\n"); + printf("OK: 42 vectors passed\n"); return 0; } diff --git a/lsposed/app/src/main/kotlin/dev/okhsunrog/vpnhide/generated/IfaceLists.kt b/lsposed/app/src/main/kotlin/dev/okhsunrog/vpnhide/generated/IfaceLists.kt index 9466f46..64082b3 100644 --- a/lsposed/app/src/main/kotlin/dev/okhsunrog/vpnhide/generated/IfaceLists.kt +++ b/lsposed/app/src/main/kotlin/dev/okhsunrog/vpnhide/generated/IfaceLists.kt @@ -27,6 +27,8 @@ internal object IfaceLists { if (n.startsWith("gre")) return true // catch-all for renamed clients (myvpn0, vpn-client, xvpn1, ...) if (n.contains("vpn")) return true + // Anonymous netdev / renamed tunnel using the kernel's default naming pattern (e.g. `ip link set tun0 name if33` from issue #86). Does NOT match `ifb` — those are kernel intermediate-functional-block traffic-shaping ifaces (different shape: `if` + letter, not + digit). + if (n.startsWith("if") && n.length > 2 && n.substring(2).all { it.isDigit() }) return true return false } } diff --git a/lsposed/app/src/test/kotlin/dev/okhsunrog/vpnhide/generated/IfaceListsGeneratedTest.kt b/lsposed/app/src/test/kotlin/dev/okhsunrog/vpnhide/generated/IfaceListsGeneratedTest.kt index 39d49fb..f39bd93 100644 --- a/lsposed/app/src/test/kotlin/dev/okhsunrog/vpnhide/generated/IfaceListsGeneratedTest.kt +++ b/lsposed/app/src/test/kotlin/dev/okhsunrog/vpnhide/generated/IfaceListsGeneratedTest.kt @@ -39,7 +39,13 @@ class IfaceListsGeneratedTest { assertEquals("dummy0", false, IfaceLists.isVpnIface("dummy0")) assertEquals("bnep0", false, IfaceLists.isVpnIface("bnep0")) assertEquals("rndis0", false, IfaceLists.isVpnIface("rndis0")) - assertEquals("if33", false, IfaceLists.isVpnIface("if33")) + assertEquals("if33", true, IfaceLists.isVpnIface("if33")) + assertEquals("if0", true, IfaceLists.isVpnIface("if0")) + assertEquals("if99", true, IfaceLists.isVpnIface("if99")) + assertEquals("ifb0", false, IfaceLists.isVpnIface("ifb0")) + assertEquals("ifb1", false, IfaceLists.isVpnIface("ifb1")) + assertEquals("if", false, IfaceLists.isVpnIface("if")) + assertEquals("if_inet6", false, IfaceLists.isVpnIface("if_inet6")) assertEquals("", false, IfaceLists.isVpnIface("")) assertEquals("tunl", true, IfaceLists.isVpnIface("tunl")) assertEquals("atun0", false, IfaceLists.isVpnIface("atun0")) diff --git a/lsposed/native/src/generated/iface_lists.rs b/lsposed/native/src/generated/iface_lists.rs index 23319bd..ce62bbf 100644 --- a/lsposed/native/src/generated/iface_lists.rs +++ b/lsposed/native/src/generated/iface_lists.rs @@ -107,6 +107,10 @@ pub fn matches_vpn(name: &[u8]) -> bool { if contains_ci(name, b"vpn") { return true; } + // Anonymous netdev / renamed tunnel using the kernel's default naming pattern (e.g. `ip link set tun0 name if33` from issue #86). Does NOT match `ifb` — those are kernel intermediate-functional-block traffic-shaping ifaces (different shape: `if` + letter, not + digit). + if starts_with_then_digits_ci(name, b"if") { + return true; + } false } @@ -148,7 +152,13 @@ mod tests { assert_eq!(matches_vpn(b"dummy0"), false, "matches_vpn('dummy0')"); assert_eq!(matches_vpn(b"bnep0"), false, "matches_vpn('bnep0')"); assert_eq!(matches_vpn(b"rndis0"), false, "matches_vpn('rndis0')"); - assert_eq!(matches_vpn(b"if33"), false, "matches_vpn('if33')"); + assert_eq!(matches_vpn(b"if33"), true, "matches_vpn('if33')"); + assert_eq!(matches_vpn(b"if0"), true, "matches_vpn('if0')"); + assert_eq!(matches_vpn(b"if99"), true, "matches_vpn('if99')"); + assert_eq!(matches_vpn(b"ifb0"), false, "matches_vpn('ifb0')"); + assert_eq!(matches_vpn(b"ifb1"), false, "matches_vpn('ifb1')"); + assert_eq!(matches_vpn(b"if"), false, "matches_vpn('if')"); + assert_eq!(matches_vpn(b"if_inet6"), false, "matches_vpn('if_inet6')"); assert_eq!(matches_vpn(b""), false, "matches_vpn('')"); assert_eq!(matches_vpn(b"tunl"), true, "matches_vpn('tunl')"); assert_eq!(matches_vpn(b"atun0"), false, "matches_vpn('atun0')"); diff --git a/zygisk/src/generated/iface_lists.rs b/zygisk/src/generated/iface_lists.rs index 23319bd..ce62bbf 100644 --- a/zygisk/src/generated/iface_lists.rs +++ b/zygisk/src/generated/iface_lists.rs @@ -107,6 +107,10 @@ pub fn matches_vpn(name: &[u8]) -> bool { if contains_ci(name, b"vpn") { return true; } + // Anonymous netdev / renamed tunnel using the kernel's default naming pattern (e.g. `ip link set tun0 name if33` from issue #86). Does NOT match `ifb` — those are kernel intermediate-functional-block traffic-shaping ifaces (different shape: `if` + letter, not + digit). + if starts_with_then_digits_ci(name, b"if") { + return true; + } false } @@ -148,7 +152,13 @@ mod tests { assert_eq!(matches_vpn(b"dummy0"), false, "matches_vpn('dummy0')"); assert_eq!(matches_vpn(b"bnep0"), false, "matches_vpn('bnep0')"); assert_eq!(matches_vpn(b"rndis0"), false, "matches_vpn('rndis0')"); - assert_eq!(matches_vpn(b"if33"), false, "matches_vpn('if33')"); + assert_eq!(matches_vpn(b"if33"), true, "matches_vpn('if33')"); + assert_eq!(matches_vpn(b"if0"), true, "matches_vpn('if0')"); + assert_eq!(matches_vpn(b"if99"), true, "matches_vpn('if99')"); + assert_eq!(matches_vpn(b"ifb0"), false, "matches_vpn('ifb0')"); + assert_eq!(matches_vpn(b"ifb1"), false, "matches_vpn('ifb1')"); + assert_eq!(matches_vpn(b"if"), false, "matches_vpn('if')"); + assert_eq!(matches_vpn(b"if_inet6"), false, "matches_vpn('if_inet6')"); assert_eq!(matches_vpn(b""), false, "matches_vpn('')"); assert_eq!(matches_vpn(b"tunl"), true, "matches_vpn('tunl')"); assert_eq!(matches_vpn(b"atun0"), false, "matches_vpn('atun0')");