- LaunchedEffect and test button both use refreshAllStreaming now
- onResult callback dispatches to Main thread for Compose state update
- Removed broken testAll() path (mini-sing doesn't populate proxy history)
- Bottom sheet test latency also uses streaming approach
- Remove libsingbox.so and libforkexec.so entirely
- Add MiniSing.startHelper/stopHelper/isHelperRunning JNI functions
- Add helperBox in ffi_android.go — second Box instance for testing
- NodeTester now uses MiniSing.startHelper() instead of ForkExec
- Single native library (libminising.so) handles both VPN and testing
- switchNode: rollback to previous config on failure instead of full disconnect
- NodeTester: synchronized start/stop to prevent race conditions
- CommandReceiver: set exported=false to prevent external broadcast abuse
- GeneralSettingsScreen: remove redundant onDispose save (each control saves immediately)
- Log level changed from trace to warn (configurable in Settings)
- Helper process log level also set to warn
- All UI polling (traffic stats, connections, logs) now uses
repeatOnLifecycle(STARTED) — pauses when screen not visible
- New RulesScreen with two tabs: Custom Rules (structured CRUD with
type/pattern/outbound, enable/disable, filter chips) and Rule Sets
(28 built-in + remote URL support, per-service toggle)
- Rule sets now loaded natively by engine via rule_set declarations
instead of Kotlin-side inline expansion — simpler and more efficient
- Remote rule sets downloaded through proxy (download_detour: "proxy")
- ProxyProtocol enum as single source of truth for supported protocols,
fixes NodeTester excluding hysteria2/tuic/wireguard/socks5
- Quick Rule auto-detects type (IP_CIDR, DOMAIN_SUFFIX, DOMAIN_KEYWORD)
- Logs screen: add open/share button via FileProvider
- Build time shown in settings
Long-pressing a connection in the Active Connections screen shows a
bottom sheet to quickly add the domain to direct/proxy/block rules.
Extracts the registrable domain from the host (e.g. cdn.example.com
→ example.com) and appends it to the corresponding rule list in
SharedPreferences. Duplicate detection prevents re-adding.
- CommandReceiver: exported broadcast receiver for adb CLI control
- start/stop/status/set_config commands
- Usage: adb shell am broadcast -a com.sing.vpn.CMD -n com.sing.vpn/.CommandReceiver --es cmd start
- SingConfig: add controlplane.tailscale.com → direct route rule
to prevent tailnet control traffic from going through proxy
Shell-based test framework using adb + uiautomator dump.
Avoids instrumented tests which crash due to Go c-shared
native lib init in test runner process.
Tests:
- test01: App launch, UI state verification
- test02: VPN connect/disconnect lifecycle
- test03: Clash API connectivity after connect
- test04: Tailscale peer discovery (soft-pass on emulator)
- test05: Reconnect after disconnect
Usage: ./test-e2e.sh [all|test01|test02|...]
- Remove ts-dns UDP upstream (100.100.100.100) — .ts.net DNS is handled
by resolver.Exchange() via LookupTailscale, never reaches mini-dns.
- Write accept_routes into Tailscale outbound JSON config so the
Settings toggle actually takes effect.
- NodeStore: remove default nodes with real server credentials,
start with empty list for user to add their own.
- Extract duplicate formatBytes() from HomeFragment and
ConnectionsFragment into shared Util.kt.
- addDnsServer: 172.19.0.1 → 198.18.0.1 (TUN local address can't be
DNS target — gvisor doesn't route packets to self through the stack)
- duplicateTunFdForEngine: dup() fd for Go so Java and Go each own
their fd independently, no double-close on stop/switch
- SingConfig: add auto_route/strict_route/exclude_outbound, rule set
coverage dedup, ads block opt-in toggle, PreferIPv4
- NodeTester: filter unsupported outbound types, fix interval type
Documents the full system design including:
- Process model (app + active sing-box + helper sing-box)
- file_descriptor patch (2 lines in sing-box source)
- Why SIGKILL+restart over SIGHUP hot-reload (fd lifecycle analysis)
- Helper process + Clash API for node speed testing
- DNS split routing (DoT remote + UDP local)
- fork_exec.c fd safety guards
- Build instructions and version history
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SIGHUP hot-reload won't work with binary mode because:
- Java-side dup'd fd exists only in parent process, not in child
- sing-box child process can't see fds created after fork
SIGKILL+restart approach is reliable and fast (~5ms gap):
- SIGKILL skips graceful shutdown, so fd is never closed by child
- TUN fd owned by VpnService (Java), survives child restarts
- Kernel TUN queue buffers packets during the gap
Removed: Os.dup() complexity, reflection for fd int extraction.
Simplified SingVpnService to clean SIGKILL+restart pattern.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SIGHUP hot-reload doesn't work with file_descriptor mode because
sing-box closes the TUN fd when tearing down the old tun inbound,
making it unavailable for the new instance.
Switch to fast SIGKILL+restart approach:
- SIGKILL old process (instant, no graceful shutdown delay)
- Start new process with same TUN fd (owned by VpnService, stays open)
- Total switch time ~5ms, packets buffered in kernel TUN queue
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Helper process (NodeTester):
- sing-box helper runs at app launch with mixed inbound + Clash API (:9090)
- All nodes configured as urltest group for auto speed testing
- Delay results shown in Nodes tab (green <300ms / orange <600ms / red)
- Queries via Clash REST API GET /proxies
A/B node swap:
- ACTION_SWITCH in SingVpnService: kill old sing-box, start new with same TUN fd
- NodesFragment triggers A/B swap when selecting node while VPN is connected
- ~200ms gap, packets buffered in kernel TUN queue
DNS hijack-dns fix:
- Add route rule {"action": "sniff"} before {"protocol": "dns", "action": "hijack-dns"}
- Required for sing-box 1.13+ where inbound sniff field is deprecated
- Fallback port 53 hijack rule for safety
Other (from previous worker):
- Splash screen, Reality/uTLS support, dual DNS (DoT remote + UDP local)
- 3 default nodes (HK01 VLESS, JMS SS, SOCKS5)
- network_security_config.xml for localhost cleartext HTTP (Clash API)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Patch sing-box source to add `file_descriptor` field to tun inbound config
(2 lines changed in option/tun.go + protocol/tun/inbound.go)
- Rebuild sing-box binary with fd:// support (android/arm64, full tags)
- SingVpnService: single-process architecture (sing-box only, no tun2socks)
- Create TUN first, pass fd to sing-box config, start sing-box with keepFd=tunFd
- SingBoxConfig: switch from mixed inbound to tun inbound with file_descriptor
- Startup time improved: 0.166s vs 0.88s+tun2socks
Architecture: TUN fd → sing-box (tun inbound fd://N → outbound) → upstream
Tested: HTTPS works, exit IP = 156.225.28.220, single process confirmed.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>