From 44be5d20696b8653e7d80e84c050a7ba0ffed1d3 Mon Sep 17 00:00:00 2001 From: sudoup Date: Wed, 16 Sep 2026 22:01:48 +0300 Subject: [PATCH 1/3] support disable-icmp-forwarding in tun options Add a "Disable ICMP Forwarding" VpnService option and pass it through to the core tun configuration. When enabled (default), ICMP echo requests are answered locally instead of being forwarded directly, avoiding ICMP leaks and the 'receive ICMP echo reply: i/o timeout' error. --- core/src/main/cpp/main.c | 3 ++- core/src/main/golang/native/tun.go | 4 ++-- core/src/main/golang/native/tun/tun.go | 23 ++++++++++--------- .../java/com/github/kr328/clash/core/Clash.kt | 3 ++- .../github/kr328/clash/core/bridge/Bridge.kt | 10 +++++++- .../clash/design/NetworkSettingsDesign.kt | 7 ++++++ design/src/main/res/values/strings.xml | 2 ++ .../github/kr328/clash/service/TunService.kt | 1 + .../clash/service/clash/module/TunModule.kt | 2 ++ .../kr328/clash/service/store/ServiceStore.kt | 5 ++++ 10 files changed, 44 insertions(+), 16 deletions(-) diff --git a/core/src/main/cpp/main.c b/core/src/main/cpp/main.c index 61e431c8a1..4b7aa825b3 100644 --- a/core/src/main/cpp/main.c +++ b/core/src/main/cpp/main.c @@ -116,6 +116,7 @@ Java_com_github_kr328_clash_core_bridge_Bridge_nativeStartTun(JNIEnv *env, jobje jstring gateway, jstring portal, jstring dns, + jboolean disable_icmp_forwarding, jobject cb) { TRACE_METHOD(); @@ -125,7 +126,7 @@ Java_com_github_kr328_clash_core_bridge_Bridge_nativeStartTun(JNIEnv *env, jobje scoped_string _dns = get_string(dns); jobject _interface = new_global(cb); - startTun(fd, _stack, _gateway, _portal, _dns, _interface); + startTun(fd, _stack, _gateway, _portal, _dns, (int) disable_icmp_forwarding, _interface); } JNIEXPORT void JNICALL diff --git a/core/src/main/golang/native/tun.go b/core/src/main/golang/native/tun.go index 860a1fdc21..2525ff10f6 100644 --- a/core/src/main/golang/native/tun.go +++ b/core/src/main/golang/native/tun.go @@ -64,7 +64,7 @@ func (t *remoteTun) close() { } //export startTun -func startTun(fd C.int, stack, gateway, portal, dns C.c_string, callback unsafe.Pointer) C.int { +func startTun(fd C.int, stack, gateway, portal, dns C.c_string, disableICMPForwarding C.int, callback unsafe.Pointer) C.int { rTunLock.Lock() defer rTunLock.Unlock() @@ -83,7 +83,7 @@ func startTun(fd C.int, stack, gateway, portal, dns C.c_string, callback unsafe. app.ApplyTunContext(remote.markSocket, remote.querySocketUid) - closer, err := tun.Start(f, s, g, p, d) + closer, err := tun.Start(f, s, g, p, d, disableICMPForwarding != 0) if err != nil { remote.close() diff --git a/core/src/main/golang/native/tun/tun.go b/core/src/main/golang/native/tun/tun.go index ee2c2a9124..87991ad72c 100644 --- a/core/src/main/golang/native/tun/tun.go +++ b/core/src/main/golang/native/tun/tun.go @@ -14,7 +14,7 @@ import ( "github.com/metacubex/mihomo/tunnel" ) -func Start(fd int, stack, gateway, portal, dns string) (io.Closer, error) { +func Start(fd int, stack, gateway, portal, dns string, disableICMPForwarding bool) (io.Closer, error) { log.Debugln("TUN: fd = %d, stack = %s, gateway = %s, portal = %s, dns = %s", fd, stack, gateway, portal, dns) tunStack, ok := C.StackTypeMapping[strings.ToLower(stack)] @@ -52,16 +52,17 @@ func Start(fd int, stack, gateway, portal, dns string) (io.Closer, error) { } options := LC.Tun{ - Enable: true, - Device: sing_tun.InterfaceName, - Stack: tunStack, - DNSHijack: dnsHijack, - AutoRoute: false, // had set route in TunService.kt - AutoDetectInterface: false, // implements by VpnService::protect - Inet4Address: prefix4, - Inet6Address: prefix6, - MTU: 9000, // private const val TUN_MTU = 9000 in TunService.kt - FileDescriptor: fd, + Enable: true, + Device: sing_tun.InterfaceName, + Stack: tunStack, + DNSHijack: dnsHijack, + AutoRoute: false, // had set route in TunService.kt + AutoDetectInterface: false, // implements by VpnService::protect + Inet4Address: prefix4, + Inet6Address: prefix6, + MTU: 9000, // private const val TUN_MTU = 9000 in TunService.kt + FileDescriptor: fd, + DisableICMPForwarding: disableICMPForwarding, } tunOptions, _ := json.Marshal(options) diff --git a/core/src/main/java/com/github/kr328/clash/core/Clash.kt b/core/src/main/java/com/github/kr328/clash/core/Clash.kt index edba62bed0..6038b57e1b 100644 --- a/core/src/main/java/com/github/kr328/clash/core/Clash.kt +++ b/core/src/main/java/com/github/kr328/clash/core/Clash.kt @@ -70,10 +70,11 @@ object Clash { gateway: String, portal: String, dns: String, + disableIcmpForwarding: Boolean, markSocket: (Int) -> Boolean, querySocketUid: (protocol: Int, source: InetSocketAddress, target: InetSocketAddress) -> Int ) { - Bridge.nativeStartTun(fd, stack, gateway, portal, dns, object : TunInterface { + Bridge.nativeStartTun(fd, stack, gateway, portal, dns, disableIcmpForwarding, object : TunInterface { override fun markSocket(fd: Int) { markSocket(fd) } diff --git a/core/src/main/java/com/github/kr328/clash/core/bridge/Bridge.kt b/core/src/main/java/com/github/kr328/clash/core/bridge/Bridge.kt index fa3b9247ef..1dd12ce901 100644 --- a/core/src/main/java/com/github/kr328/clash/core/bridge/Bridge.kt +++ b/core/src/main/java/com/github/kr328/clash/core/bridge/Bridge.kt @@ -19,7 +19,15 @@ object Bridge { external fun nativeNotifyDnsChanged(dnsList: String) external fun nativeNotifyTimeZoneChanged(name: String, offset: Int) external fun nativeNotifyInstalledAppChanged(uidList: String) - external fun nativeStartTun(fd: Int, stack: String, gateway: String, portal: String, dns: String, cb: TunInterface) + external fun nativeStartTun( + fd: Int, + stack: String, + gateway: String, + portal: String, + dns: String, + disableIcmpForwarding: Boolean, + cb: TunInterface + ) external fun nativeStopTun() external fun nativeStartHttp(listenAt: String): String? external fun nativeStopHttp() diff --git a/design/src/main/java/com/github/kr328/clash/design/NetworkSettingsDesign.kt b/design/src/main/java/com/github/kr328/clash/design/NetworkSettingsDesign.kt index 93e1ee3d07..2f5178838d 100644 --- a/design/src/main/java/com/github/kr328/clash/design/NetworkSettingsDesign.kt +++ b/design/src/main/java/com/github/kr328/clash/design/NetworkSettingsDesign.kt @@ -70,6 +70,13 @@ class NetworkSettingsDesign( configure = vpnDependencies::add, ) + switch( + value = srvStore::disableIcmpForwarding, + title = R.string.disable_icmp_forwarding, + summary = R.string.disable_icmp_forwarding_summary, + configure = vpnDependencies::add, + ) + switch( value = srvStore::allowBypass, title = R.string.allow_bypass, diff --git a/design/src/main/res/values/strings.xml b/design/src/main/res/values/strings.xml index 4e43d3fd26..1aabf02a20 100644 --- a/design/src/main/res/values/strings.xml +++ b/design/src/main/res/values/strings.xml @@ -135,6 +135,8 @@ Bypass private network addresses DNS Hijacking Handle all dns packet + Disable ICMP Forwarding + Reply ICMP echo requests locally instead of forwarding them Block Loopback Block loopback connections Allow Bypass diff --git a/service/src/main/java/com/github/kr328/clash/service/TunService.kt b/service/src/main/java/com/github/kr328/clash/service/TunService.kt index 8550f8ab81..9cf78729fa 100644 --- a/service/src/main/java/com/github/kr328/clash/service/TunService.kt +++ b/service/src/main/java/com/github/kr328/clash/service/TunService.kt @@ -222,6 +222,7 @@ class TunService : VpnService(), CoroutineScope by CoroutineScope(Dispatchers.De gateway = "$TUN_GATEWAY/$TUN_SUBNET_PREFIX" + if (store.allowIpv6) ",$TUN_GATEWAY6/$TUN_SUBNET_PREFIX6" else "", portal = TUN_PORTAL + if (store.allowIpv6) ",$TUN_PORTAL6" else "", dns = if (store.dnsHijacking) NET_ANY else (TUN_DNS + if (store.allowIpv6) ",$TUN_DNS6" else ""), + disableIcmpForwarding = store.disableIcmpForwarding, ) } diff --git a/service/src/main/java/com/github/kr328/clash/service/clash/module/TunModule.kt b/service/src/main/java/com/github/kr328/clash/service/clash/module/TunModule.kt index 84197d325f..c9237d3563 100644 --- a/service/src/main/java/com/github/kr328/clash/service/clash/module/TunModule.kt +++ b/service/src/main/java/com/github/kr328/clash/service/clash/module/TunModule.kt @@ -19,6 +19,7 @@ class TunModule(private val vpn: VpnService) : Module(vpn) { val gateway: String, val portal: String, val dns: String, + val disableIcmpForwarding: Boolean, ) private val connectivity = service.getSystemService()!! @@ -61,6 +62,7 @@ class TunModule(private val vpn: VpnService) : Module(vpn) { gateway = device.gateway, portal = device.portal, dns = device.dns, + disableIcmpForwarding = device.disableIcmpForwarding, markSocket = vpn::protect, querySocketUid = this::queryUid ) diff --git a/service/src/main/java/com/github/kr328/clash/service/store/ServiceStore.kt b/service/src/main/java/com/github/kr328/clash/service/store/ServiceStore.kt index d361848ff5..562123b250 100644 --- a/service/src/main/java/com/github/kr328/clash/service/store/ServiceStore.kt +++ b/service/src/main/java/com/github/kr328/clash/service/store/ServiceStore.kt @@ -61,6 +61,11 @@ class ServiceStore(context: Context) { defaultValue = "system" ) + var disableIcmpForwarding by store.boolean( + key = "disable_icmp_forwarding", + defaultValue = true + ) + var dynamicNotification by store.boolean( key = "dynamic_notification", defaultValue = true From a512ddfe7de53c72c5740e2f7121f8e05ce15b5d Mon Sep 17 00:00:00 2001 From: sudoup Date: Wed, 16 Sep 2026 22:13:16 +0300 Subject: [PATCH 2/3] support icmp-timeout in tun options Add an ICMP Timeout VpnService option and pass it through to the core tun configuration, so a custom icmp-timeout is no longer ignored. The default value 0 keeps the core default (10s). --- core/src/main/cpp/main.c | 3 ++- core/src/main/golang/native/tun.go | 4 ++-- core/src/main/golang/native/tun/tun.go | 3 ++- .../src/main/java/com/github/kr328/clash/core/Clash.kt | 3 ++- .../java/com/github/kr328/clash/core/bridge/Bridge.kt | 1 + .../github/kr328/clash/design/NetworkSettingsDesign.kt | 8 ++++++++ .../com/github/kr328/clash/design/preference/Value.kt | 10 ++++++++++ design/src/main/res/values/strings.xml | 2 ++ .../java/com/github/kr328/clash/service/TunService.kt | 1 + .../kr328/clash/service/clash/module/TunModule.kt | 2 ++ .../github/kr328/clash/service/store/ServiceStore.kt | 5 +++++ 11 files changed, 37 insertions(+), 5 deletions(-) diff --git a/core/src/main/cpp/main.c b/core/src/main/cpp/main.c index 4b7aa825b3..f1e4d85c81 100644 --- a/core/src/main/cpp/main.c +++ b/core/src/main/cpp/main.c @@ -117,6 +117,7 @@ Java_com_github_kr328_clash_core_bridge_Bridge_nativeStartTun(JNIEnv *env, jobje jstring portal, jstring dns, jboolean disable_icmp_forwarding, + jint icmp_timeout, jobject cb) { TRACE_METHOD(); @@ -126,7 +127,7 @@ Java_com_github_kr328_clash_core_bridge_Bridge_nativeStartTun(JNIEnv *env, jobje scoped_string _dns = get_string(dns); jobject _interface = new_global(cb); - startTun(fd, _stack, _gateway, _portal, _dns, (int) disable_icmp_forwarding, _interface); + startTun(fd, _stack, _gateway, _portal, _dns, (int) disable_icmp_forwarding, (int) icmp_timeout, _interface); } JNIEXPORT void JNICALL diff --git a/core/src/main/golang/native/tun.go b/core/src/main/golang/native/tun.go index 2525ff10f6..36f9863170 100644 --- a/core/src/main/golang/native/tun.go +++ b/core/src/main/golang/native/tun.go @@ -64,7 +64,7 @@ func (t *remoteTun) close() { } //export startTun -func startTun(fd C.int, stack, gateway, portal, dns C.c_string, disableICMPForwarding C.int, callback unsafe.Pointer) C.int { +func startTun(fd C.int, stack, gateway, portal, dns C.c_string, disableICMPForwarding C.int, icmpTimeout C.int, callback unsafe.Pointer) C.int { rTunLock.Lock() defer rTunLock.Unlock() @@ -83,7 +83,7 @@ func startTun(fd C.int, stack, gateway, portal, dns C.c_string, disableICMPForwa app.ApplyTunContext(remote.markSocket, remote.querySocketUid) - closer, err := tun.Start(f, s, g, p, d, disableICMPForwarding != 0) + closer, err := tun.Start(f, s, g, p, d, disableICMPForwarding != 0, int64(icmpTimeout)) if err != nil { remote.close() diff --git a/core/src/main/golang/native/tun/tun.go b/core/src/main/golang/native/tun/tun.go index 87991ad72c..aaa52f4d69 100644 --- a/core/src/main/golang/native/tun/tun.go +++ b/core/src/main/golang/native/tun/tun.go @@ -14,7 +14,7 @@ import ( "github.com/metacubex/mihomo/tunnel" ) -func Start(fd int, stack, gateway, portal, dns string, disableICMPForwarding bool) (io.Closer, error) { +func Start(fd int, stack, gateway, portal, dns string, disableICMPForwarding bool, icmpTimeout int64) (io.Closer, error) { log.Debugln("TUN: fd = %d, stack = %s, gateway = %s, portal = %s, dns = %s", fd, stack, gateway, portal, dns) tunStack, ok := C.StackTypeMapping[strings.ToLower(stack)] @@ -63,6 +63,7 @@ func Start(fd int, stack, gateway, portal, dns string, disableICMPForwarding boo MTU: 9000, // private const val TUN_MTU = 9000 in TunService.kt FileDescriptor: fd, DisableICMPForwarding: disableICMPForwarding, + ICMPTimeout: icmpTimeout, } tunOptions, _ := json.Marshal(options) diff --git a/core/src/main/java/com/github/kr328/clash/core/Clash.kt b/core/src/main/java/com/github/kr328/clash/core/Clash.kt index 6038b57e1b..a0753d1917 100644 --- a/core/src/main/java/com/github/kr328/clash/core/Clash.kt +++ b/core/src/main/java/com/github/kr328/clash/core/Clash.kt @@ -71,10 +71,11 @@ object Clash { portal: String, dns: String, disableIcmpForwarding: Boolean, + icmpTimeout: Int, markSocket: (Int) -> Boolean, querySocketUid: (protocol: Int, source: InetSocketAddress, target: InetSocketAddress) -> Int ) { - Bridge.nativeStartTun(fd, stack, gateway, portal, dns, disableIcmpForwarding, object : TunInterface { + Bridge.nativeStartTun(fd, stack, gateway, portal, dns, disableIcmpForwarding, icmpTimeout, object : TunInterface { override fun markSocket(fd: Int) { markSocket(fd) } diff --git a/core/src/main/java/com/github/kr328/clash/core/bridge/Bridge.kt b/core/src/main/java/com/github/kr328/clash/core/bridge/Bridge.kt index 1dd12ce901..1e8231849f 100644 --- a/core/src/main/java/com/github/kr328/clash/core/bridge/Bridge.kt +++ b/core/src/main/java/com/github/kr328/clash/core/bridge/Bridge.kt @@ -26,6 +26,7 @@ object Bridge { portal: String, dns: String, disableIcmpForwarding: Boolean, + icmpTimeout: Int, cb: TunInterface ) external fun nativeStopTun() diff --git a/design/src/main/java/com/github/kr328/clash/design/NetworkSettingsDesign.kt b/design/src/main/java/com/github/kr328/clash/design/NetworkSettingsDesign.kt index 2f5178838d..432d31927e 100644 --- a/design/src/main/java/com/github/kr328/clash/design/NetworkSettingsDesign.kt +++ b/design/src/main/java/com/github/kr328/clash/design/NetworkSettingsDesign.kt @@ -77,6 +77,14 @@ class NetworkSettingsDesign( configure = vpnDependencies::add, ) + editableText( + value = srvStore::icmpTimeout, + adapter = NullableTextAdapter.Int, + title = R.string.icmp_timeout, + empty = R.string.icmp_timeout_default, + configure = vpnDependencies::add, + ) + switch( value = srvStore::allowBypass, title = R.string.allow_bypass, diff --git a/design/src/main/java/com/github/kr328/clash/design/preference/Value.kt b/design/src/main/java/com/github/kr328/clash/design/preference/Value.kt index cf2dcb79fc..084d5cfaf5 100644 --- a/design/src/main/java/com/github/kr328/clash/design/preference/Value.kt +++ b/design/src/main/java/com/github/kr328/clash/design/preference/Value.kt @@ -19,6 +19,16 @@ interface NullableTextAdapter { } } + val Int = object : NullableTextAdapter { + override fun from(value: Int): String? { + return if (value > 0) value.toString() else "" + } + + override fun to(text: String?): Int { + return text?.toIntOrNull() ?: 0 + } + } + val String = object : NullableTextAdapter { override fun from(value: String?): String? { return value diff --git a/design/src/main/res/values/strings.xml b/design/src/main/res/values/strings.xml index 1aabf02a20..893dcacb81 100644 --- a/design/src/main/res/values/strings.xml +++ b/design/src/main/res/values/strings.xml @@ -137,6 +137,8 @@ Handle all dns packet Disable ICMP Forwarding Reply ICMP echo requests locally instead of forwarding them + ICMP Timeout + Default Block Loopback Block loopback connections Allow Bypass diff --git a/service/src/main/java/com/github/kr328/clash/service/TunService.kt b/service/src/main/java/com/github/kr328/clash/service/TunService.kt index 9cf78729fa..eb3a88036a 100644 --- a/service/src/main/java/com/github/kr328/clash/service/TunService.kt +++ b/service/src/main/java/com/github/kr328/clash/service/TunService.kt @@ -223,6 +223,7 @@ class TunService : VpnService(), CoroutineScope by CoroutineScope(Dispatchers.De portal = TUN_PORTAL + if (store.allowIpv6) ",$TUN_PORTAL6" else "", dns = if (store.dnsHijacking) NET_ANY else (TUN_DNS + if (store.allowIpv6) ",$TUN_DNS6" else ""), disableIcmpForwarding = store.disableIcmpForwarding, + icmpTimeout = store.icmpTimeout, ) } diff --git a/service/src/main/java/com/github/kr328/clash/service/clash/module/TunModule.kt b/service/src/main/java/com/github/kr328/clash/service/clash/module/TunModule.kt index c9237d3563..ddf794143e 100644 --- a/service/src/main/java/com/github/kr328/clash/service/clash/module/TunModule.kt +++ b/service/src/main/java/com/github/kr328/clash/service/clash/module/TunModule.kt @@ -20,6 +20,7 @@ class TunModule(private val vpn: VpnService) : Module(vpn) { val portal: String, val dns: String, val disableIcmpForwarding: Boolean, + val icmpTimeout: Int, ) private val connectivity = service.getSystemService()!! @@ -63,6 +64,7 @@ class TunModule(private val vpn: VpnService) : Module(vpn) { portal = device.portal, dns = device.dns, disableIcmpForwarding = device.disableIcmpForwarding, + icmpTimeout = device.icmpTimeout, markSocket = vpn::protect, querySocketUid = this::queryUid ) diff --git a/service/src/main/java/com/github/kr328/clash/service/store/ServiceStore.kt b/service/src/main/java/com/github/kr328/clash/service/store/ServiceStore.kt index 562123b250..59a5914d41 100644 --- a/service/src/main/java/com/github/kr328/clash/service/store/ServiceStore.kt +++ b/service/src/main/java/com/github/kr328/clash/service/store/ServiceStore.kt @@ -66,6 +66,11 @@ class ServiceStore(context: Context) { defaultValue = true ) + var icmpTimeout by store.int( + key = "icmp_timeout", + defaultValue = 0 + ) + var dynamicNotification by store.boolean( key = "dynamic_notification", defaultValue = true From 51479b1d8bf96b8c61641f7c0c4f10b45ca24bbb Mon Sep 17 00:00:00 2001 From: sudoup Date: Wed, 16 Sep 2026 22:19:33 +0300 Subject: [PATCH 3/3] clamp icmp-timeout to non-negative and show its unit Negative input produced a negative ICMPTimeout, which sets a past read deadline and breaks ICMP forwarding with an immediate 'i/o timeout'. Coerce the parsed value to at least zero and trim whitespace, and label the option with the seconds unit. --- .../main/java/com/github/kr328/clash/design/preference/Value.kt | 2 +- design/src/main/res/values/strings.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/design/src/main/java/com/github/kr328/clash/design/preference/Value.kt b/design/src/main/java/com/github/kr328/clash/design/preference/Value.kt index 084d5cfaf5..7e6ce7a708 100644 --- a/design/src/main/java/com/github/kr328/clash/design/preference/Value.kt +++ b/design/src/main/java/com/github/kr328/clash/design/preference/Value.kt @@ -25,7 +25,7 @@ interface NullableTextAdapter { } override fun to(text: String?): Int { - return text?.toIntOrNull() ?: 0 + return text?.trim()?.toIntOrNull()?.coerceAtLeast(0) ?: 0 } } diff --git a/design/src/main/res/values/strings.xml b/design/src/main/res/values/strings.xml index 893dcacb81..d6cc8d03d4 100644 --- a/design/src/main/res/values/strings.xml +++ b/design/src/main/res/values/strings.xml @@ -137,7 +137,7 @@ Handle all dns packet Disable ICMP Forwarding Reply ICMP echo requests locally instead of forwarding them - ICMP Timeout + ICMP Timeout (seconds) Default Block Loopback Block loopback connections