fix(filter): catch tunnels renamed to if<N> (issue #86)

Add a single TOML rule `prefix = "if", suffix = "digits"` to the shared
matcher. Renames using the kernel's default anonymous-netdev naming
(`ip link set tun0 name if33`) — the exact attack from issue #86 — now
get hidden by every component (kmod, zygisk, lsposed, lsposed-native).

The shape is intentionally narrow: `if` + 1+ ASCII digits only. `ifb<N>`
(intermediate-functional-block traffic shaping) has a letter after `if`
and is not matched.
This commit is contained in:
okhsunrog 2026-04-26 03:53:28 +03:00
parent 15d806a885
commit 54242b1140
8 changed files with 89 additions and 8 deletions

View file

@ -0,0 +1,9 @@
_2026-04-26_
## English
Catch tunnels renamed to the kernel default `if<N>` pattern (issue #86) — e.g. `tun0` renamed to `if33` is no longer visible to target apps.
## Русский
Прячем туннели, переименованные в дефолтный для ядра паттерн `if<N>` (issue #86) — например `tun0` с именем `if33` теперь скрыт от целевых приложений.

View file

@ -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<N>` 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<N>` 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

View file

@ -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<N>` — 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;
}

View file

@ -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;
}

View file

@ -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<N>` — 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
}
}

View file

@ -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"))

View file

@ -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<N>` — 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')");

View file

@ -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<N>` — 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')");