Context
Deferred from CIRISVerify#56 (closed by v4.8.1) — that fix shipped two of the three asks:
- ✅ probe semantics —
mobile_http::probe_reachability treats any HTTP response (including 401) as reachable
- ✅ decouple local checks from network race —
self_verification always populates with the locally-computed binary hash, even on a network-unavailable fast-fail
- 🟡 bind native sockets to the active Android Network — this issue
Real-device testing on the Galaxy S21U (the agent team's pin to v4.8.1) is reportedly green for #1 + #2 today, which is consistent with the #56 trace evidence (ureq reached the registry, returned 401). But the #56 root-cause analysis flagged a second-order defect: on Android multi-network devices (WiFi + Cellular + VPN simultaneously), native sockets created without an explicit Network binding may pick the wrong network OR fail with ENETUNREACH. The Java/OkHttp layer and JVM-level resolver know about the multi-network state and route correctly; the native Rust + ureq paths do not.
v4.8.1 doesn't fix this structural gap — it's still possible for a future Android scenario (mid-call network switch, captive portal, VPN flap) to surface it. v4.8.2 closes it.
Proposed approach
FFI helper: add ciris_verify_bind_to_active_network() -> bool to the FFI surface. On Android, the implementation calls (via JNI):
ConnectivityManager.getActiveNetwork() → returns a Network handle (or null if no active network).
- If non-null:
ConnectivityManager.bindProcessToNetwork(network) (or the deprecated setProcessDefaultNetwork, both equivalent at the syscall level — they call android_setprocnetwork(net_handle) from the NDK).
- Return true on success, false on no-active-network or JNI failure.
On non-Android targets (Linux, macOS, iOS, Windows), the function is a no-op returning true. iOS doesn't have the multi-network-via-API model — system-level routing handles it.
After this call, EVERY subsequent native socket created in the process — by ureq, reqwest, hickory-dns, anything — goes through the bound Network. No per-call binding required.
Caller responsibility
CIRISAgent (and any other consumer of the verify wheel on Android) calls ciris_verify_bind_to_active_network() ONCE at startup, before the first attestation. The wheel's Python __init__ can call it automatically on Android.
Why FFI helper, not Java layer
The Java layer COULD do this — Android's recommended pattern is "Java binds the process, then native code doesn't care." But putting the binding in the verify FFI means:
- Every wheel consumer (Python agents, future C consumers, mobile SDKs) gets the binding for free
- The Network is bound on the same thread that loads the verify .so, removing race windows
- The bound-Network state is testable from Rust unit tests (mocked on desktop, real on Android instrumentation tests)
Testing requirement
This MUST be tested on a real Android device with multi-network state (WiFi + Cellular simultaneously) before merge. Suggested QA scenario:
- Boot device with WiFi + Cellular both up
- Run attestation → expect green
- Disconnect WiFi mid-attestation → expect either green (race wins on Cellular path) or
network_unavailable partial result with self_verification populated (the v4.8.1 fix), NOT a hang or ENETUNREACH cascade
- Reconnect WiFi → next attestation should be green
Out of scope
- Network-change callbacks (re-binding when active Network changes mid-process). For v4.8.2 the binding is set at init time only; if the active Network changes, the next process restart re-binds. Continuous re-binding is a v4.9 candidate if needed.
- VPN-only / WiFi-Network-only forced routing. The binding picks whatever ConnectivityManager calls "active" — operator preference (force Cellular, force WiFi) is out of scope.
Cross-refs
- CIRISVerify#56 — original report (closed by v4.8.1, fixes 1+2 of 3)
- CIRISAgent#862 — agent classifier consumes the structured
network_unavailable status (independent fix on agent side)
- CIRISAgent#843 — startup attestation budget bump kept at 20s as defense-in-depth
Context
Deferred from CIRISVerify#56 (closed by v4.8.1) — that fix shipped two of the three asks:
mobile_http::probe_reachabilitytreats any HTTP response (including 401) as reachableself_verificationalways populates with the locally-computed binary hash, even on a network-unavailable fast-failReal-device testing on the Galaxy S21U (the agent team's pin to v4.8.1) is reportedly green for #1 + #2 today, which is consistent with the #56 trace evidence (ureq reached the registry, returned 401). But the #56 root-cause analysis flagged a second-order defect: on Android multi-network devices (WiFi + Cellular + VPN simultaneously), native sockets created without an explicit
Networkbinding may pick the wrong network OR fail withENETUNREACH. The Java/OkHttp layer and JVM-level resolver know about the multi-network state and route correctly; the native Rust + ureq paths do not.v4.8.1 doesn't fix this structural gap — it's still possible for a future Android scenario (mid-call network switch, captive portal, VPN flap) to surface it. v4.8.2 closes it.
Proposed approach
FFI helper: add
ciris_verify_bind_to_active_network() -> boolto the FFI surface. On Android, the implementation calls (via JNI):ConnectivityManager.getActiveNetwork()→ returns aNetworkhandle (or null if no active network).ConnectivityManager.bindProcessToNetwork(network)(or the deprecatedsetProcessDefaultNetwork, both equivalent at the syscall level — they callandroid_setprocnetwork(net_handle)from the NDK).On non-Android targets (Linux, macOS, iOS, Windows), the function is a no-op returning
true. iOS doesn't have the multi-network-via-API model — system-level routing handles it.After this call, EVERY subsequent native socket created in the process — by ureq, reqwest, hickory-dns, anything — goes through the bound
Network. No per-call binding required.Caller responsibility
CIRISAgent (and any other consumer of the verify wheel on Android) calls
ciris_verify_bind_to_active_network()ONCE at startup, before the first attestation. The wheel's Python__init__can call it automatically on Android.Why FFI helper, not Java layer
The Java layer COULD do this — Android's recommended pattern is "Java binds the process, then native code doesn't care." But putting the binding in the verify FFI means:
Testing requirement
This MUST be tested on a real Android device with multi-network state (WiFi + Cellular simultaneously) before merge. Suggested QA scenario:
network_unavailablepartial result withself_verificationpopulated (the v4.8.1 fix), NOT a hang or ENETUNREACH cascadeOut of scope
Cross-refs
network_unavailablestatus (independent fix on agent side)