Documentation

Network stacks: end-to-end capture

One section per Android HTTP client / network stack, each a complete walkthrough: which layer reads the plaintext (system proxy, native in-process tap, or an app-layer hook), the exact rutken capture --embed-trust or rutken run --bespoke command, the pinning-bypass flag if that stack pins, which libssl the native tap targets, the output you should see, and the gotchas. For the mechanism behind each pin, this page cross-links the TLS pinning bypass companion; for the start-to-finish runbook of a single capture, see Dynamic & network capture.

How to read this page

Every HTTPS connection can be read at one of four layers; the first that fits the app wins. Which layer applies is decided by the stack, so each stack section below names its layer up front.

  • Proxy MITM — the app honours the system HTTP proxy and uses a Java TLS stack, so after a pinning-bypass hook the traffic decrypts through Rutken's proxy. This is the path for every Java stack that respects http_proxy (HttpURLConnection, OkHttp, Volley, Apache, Retrofit, and the OkHttp wrappers).
  • Native in-process tap — SSL_read / SSL_write inside the process's BoringSSL. It sees cleartext before encryption and after decryption, so it is immune to pinning and needs no proxy at all. This is the path for the system-proxy-blind native stacks (Cronet, Flutter), and the universal fallback for anything the Java unpin misses. Add --bespoke-raw-bytes to surface the actual request / response bytes (the session otherwise digests them to {len, sha256}).
  • App-layer hook — the app's own decrypt / secret methods, located by static analysis.
  • TLS keylog — a CLIENT_RANDOM keyfile for offline decryption.

Two engines deliver the hooks. unpin (Community) drives a bundled Frida gadget — turnkey, broad Java coverage, ideal for a first look. capture / run --bespoke (paid editions) run Rutken's own in-house agent — no Frida fingerprint, and it is the only engine with the native in-process tap. Reach for unpin first; switch to the bespoke agent when the target detects Frida or when the stack is native (Cronet, Flutter) and the proxy never sees the bytes.

Only capture traffic from an app you own or are explicitly authorized to test. The behaviour-changing hooks take their consent acknowledgements verbatim and fail closed without them.

Prerequisites (all stacks)

These are common to every walkthrough below; each stack section only adds what is specific to it.

  • Device connected: adb devices shows it as device. Note the serial (e.g. emulator-5554) — it is the --device / --serial value everywhere.
  • The target APK on disk. For a third-party app, adb pull its installed path first (pm path <pkg> finds it).
  • A signing keystore to resign the repacked APK on the embed route: keytool -genkeypair -keystore signing.p12 -storetype PKCS12 -storepass <pw> -alias k -keyalg RSA -keysize 2048 -validity 3650 -dname "CN=Test".
  • An owned-allowlist file for the bespoke capture route: echo "<pkg>" > owned-allowlist.txt.
  • A current arm64 agent for run --bespoke / capture --agent-so — build it fresh, never reuse a fixture .so (stale ones are rejected with a version mismatch): cargo build -p agent --features bespoke-agent --target aarch64-linux-android --release. Or pass --bespoke-attach (no path) to let Rutken auto-resolve its own bundled agent for the device ABI.
  • The consent phrases, supplied verbatim: --i-am-authorized-bespoke for observation and --i-am-authorized-bespoke-active for any behaviour change (pinning bypass is a return-override, so it needs the active ack). Under capture --embed-trust the active ack auto-arms debug.rutken.return_overrides; under run --bespoke set it by hand first (adb shell setprop debug.rutken.return_overrides 1).

Replace <sha256> everywhere with the SHA-256 of the exact target APK, and <pool-resident-name> with a string already in the target's DEX string pool (the embed step is resolve-only).

Coverage at a glance

The read layer and pin-bypass flag for each stack. "Proxy" means the stack honours the system proxy and decrypts there after the pin is defeated; "Native tap" means the in-process SSL_read / SSL_write reads it regardless of proxy or pinning.

Stack Read layer Honours system proxy? If it pins
HttpURLConnection / javax.net.ssl Proxy MITM (native tap fallback) yes Conscrypt / NSC force-void
OkHttp3 / Retrofit Proxy MITM (native tap fallback) yes CertificatePinner return-override
OkHttp2 Proxy MITM (native tap fallback) yes CertificatePinner return-override
Volley Proxy MITM yes rides HttpURLConnection → Conscrypt force-void
Apache HttpClient Proxy MITM (native tap fallback) yes Conscrypt force-void (native-trust path)
Cronet Native tap (proxy-blind) no addPublicKeyPins no-op
WebView Proxy MITM + native tap yes native tap reads plaintext around an onReceivedSslError pin
Flutter (dart:io) Native tap (proxy-blind) no native force-accept on CertificateCallback
Xamarin / .NET MAUI Native tap (rides Conscrypt) yes (via Conscrypt) native tap reads plaintext around a managed pin
Ktor / gRPC / React Native collapse to OkHttp3 — see that section

Per network stack

Each card gives the read layer, the exact end-to-end command, the pinning-bypass flag, the libssl the native tap targets, the expected output, and the gotchas. Where two stacks have an identical capture procedure, they share one heading.

HttpURLConnection / javax.net.ssl (platform Conscrypt)

Read layer: Proxy MITM · native tap fallback

The baseline Java stack. It honours the system HTTP proxy and its TLS runs through the platform Conscrypt libssl at /apex/com.android.conscrypt/lib64/libssl.so, so after Rutken installs its CA the proxy decrypts directly. Most apps do not pin here; when they do it is a custom X509TrustManager or a Network Security Config pin-set, both of which funnel through TrustManagerImpl.checkServerTrusted — the single force-void path (see the Conscrypt flavour).

# Decrypt over the proxy with Rutken's in-house agent (paid)
RUTKEN_KEYSTORE_PASS=<pw> rutken <app>.apk capture \
  --device <serial> --owned-allowlist owned-allowlist.txt \
  --embed-trust \
  --agent-so target/aarch64-linux-android/release/libagent.so \
  --library-name <pool-resident-name> \
  --keystore signing.p12 --key-alias k \
  --i-am-authorized-repack "I am authorized to repack and reinstall this target on this device" \
  --i-am-authorized-bespoke "I am authorized to observe this target via the bespoke agent" \
  --i-am-authorized-bespoke-active "I am authorized to MODIFY this target's behaviour via the bespoke agent" \
  --duration 90 --format json

# Community first look (Frida gadget, proxy only -- no native tap)
rutken app-gadget.apk unpin --gadget --gadget-port 27042 --ca-store none \
  --serial <serial> --duration 120 --record out.rcap \
  --authorize <sha256> \
  --i-am-authorized "I am authorized to intercept traffic for this target" \
  --i-am-authorized-subvert "I am authorized to subvert this target's defenses"

Native tap target: platform /apex/.../libssl (shared by every Java stack). Expected: the closing summary shows captured N flows, 0 undecrypted/opaque — "opaque" is a connection the proxy could not decrypt, so you want it at 0. Gotcha: trigger traffic after the proxy is live (adb shell settings get global http_proxy shows 127.0.0.1:<port>); launch-time pings race the setup.

OkHttp3 / Retrofit

Read layer: Proxy MITM · native tap fallback

Retrofit is identical to OkHttp3 — it is a typed wrapper that builds and calls an OkHttpClient, so there is nothing stack-specific to do beyond the OkHttp procedure. OkHttp honours the system proxy and uses the platform Conscrypt libssl, so the proxy decrypts after the pin check is neutralised. Its pin is okhttp3.CertificatePinner.check; defeat it by-name for an un-obfuscated build, or with the structural name-independent detector when R8 has renamed okhttp3.CertificatePinner to something like p0.f (full detail on the OkHttp pinning flavour).

# By-name: force CertificatePinner.check -> Void (un-obfuscated)
rutken <app>.apk run --bespoke --bespoke-attach-agent <arm64 libagent.so> \
  --bespoke-pack java.tls.certificatepinner_check \
  --bespoke-authorize <sha256> \
  --i-am-authorized-bespoke "I am authorized to observe this target via the bespoke agent" \
  --i-am-authorized-bespoke-active "I am authorized to MODIFY this target's behaviour via the bespoke agent"

# Structural (name-independent) route for an R8-renamed okhttp
rutken <app>.apk run --bespoke --bespoke-attach-agent <agent> \
  --bespoke-derived --bespoke-force-void-cert-pinner \
  --bespoke-authorize <sha256> \
  --i-am-authorized-bespoke "I am authorized to observe this target via the bespoke agent" \
  --i-am-authorized-bespoke-active "I am authorized to MODIFY this target's behaviour via the bespoke agent"

# Or decrypt over the proxy end-to-end with the embed route
rutken <app>.apk capture --device <serial> --embed-trust \
  --owned-allowlist owned-allowlist.txt \
  --agent-so target/aarch64-linux-android/release/libagent.so \
  --library-name <pool-resident-name> \
  --keystore signing.p12 --key-alias k \
  --i-am-authorized-repack "I am authorized to repack and reinstall this target on this device" --i-am-authorized-bespoke "I am authorized to observe this target via the bespoke agent" \
  --i-am-authorized-bespoke-active "I am authorized to MODIFY this target's behaviour via the bespoke agent" --duration 90

Native tap target: platform /apex/.../libssl (OkHttp uses the platform SSLSocket / Conscrypt). Expected: logcat shows okhttp3.CertificatePinner.check outcome=JavaBound, the pinned call stops raising SSLPeerUnverifiedException, and the x-api-key-class header decrypts on the wire. Gotcha: the by-name pack misses on R8 builds — use --bespoke-force-void-cert-pinner, or fall back to the native tap, which reads okhttp plaintext regardless of the rename.

OkHttp2

Read layer: Proxy MITM · native tap fallback

Same procedure as OkHttp3, only the namespace differs: the pin lives on com.squareup.okhttp.CertificatePinner and is defeated by the same by-name return-override. It honours the system proxy and rides the platform Conscrypt libssl exactly like OkHttp3.

# OkHttp2 CertificatePinner -- same by-name return-override as okhttp3
rutken <app>.apk run --bespoke --bespoke-attach-agent <agent> \
  --bespoke-pack java.tls.certificatepinner_check \
  --bespoke-authorize <sha256> \
  --i-am-authorized-bespoke "I am authorized to observe this target via the bespoke agent" \
  --i-am-authorized-bespoke-active "I am authorized to MODIFY this target's behaviour via the bespoke agent"

Native tap target: platform /apex/.../libssl. Expected: identical flip to OkHttp3 — the pinned call completes and the proxy decrypts. Gotcha: if the build is obfuscated and the by-name pack misses, the native tap still reads the plaintext.

Volley

Read layer: Proxy MITM

Volley honours the system proxy and, in its default HurlStack, is built on HttpURLConnection — so it is read exactly like the HttpURLConnection section through the platform Conscrypt libssl. It rarely pins; when it does it is usually a custom HostnameVerifier / SSLSocketFactory, defeated by the Conscrypt force-void or the derived force-true-hostname pass (see custom HostnameVerifier).

# Volley rides HttpURLConnection -> decrypt over the proxy (embed route)
rutken <app>.apk capture --device <serial> --embed-trust \
  --owned-allowlist owned-allowlist.txt \
  --agent-so target/aarch64-linux-android/release/libagent.so \
  --library-name <pool-resident-name> \
  --keystore signing.p12 --key-alias k \
  --i-am-authorized-repack "I am authorized to repack and reinstall this target on this device" --i-am-authorized-bespoke "I am authorized to observe this target via the bespoke agent" \
  --i-am-authorized-bespoke-active "I am authorized to MODIFY this target's behaviour via the bespoke agent" --duration 90

Native tap target: platform /apex/.../libssl. Expected: flows decrypt on the proxy with no extra pack for the common (unpinned) case. Gotcha: an app that swaps in a custom HurlStack SSL factory needs the matching force-void / force-true-hostname pass; the HttpURLConnection proxy path is otherwise unchanged.

Apache HttpClient

Read layer: Proxy MITM · native tap fallback

The legacy org.apache.http client honours the system proxy and runs its TLS through the platform Conscrypt libssl, so the proxy decrypts after the trust check is neutralised. It decrypts via the native-trust (TrustManagerImpl.checkServerTrusted) path — the same force-void as Conscrypt, and it works on obfuscated release builds.

# Apache rides Conscrypt -- force the platform trust check void, then decrypt
rutken <app>.apk run --bespoke --bespoke-attach-agent <agent> \
  --bespoke-pack java.tls.trustmanagerimpl_check_server_trusted \
  --bespoke-authorize <sha256> \
  --i-am-authorized-bespoke "I am authorized to observe this target via the bespoke agent" \
  --i-am-authorized-bespoke-active "I am authorized to MODIFY this target's behaviour via the bespoke agent"

# Non-debuggable release: embed the agent + CA (Route 1)
rutken <app>.apk capture --device <serial> --embed-trust \
  --owned-allowlist owned-allowlist.txt \
  --agent-so target/aarch64-linux-android/release/libagent.so \
  --library-name <pool-resident-name> --keystore signing.p12 --key-alias k \
  --i-am-authorized-repack "I am authorized to repack and reinstall this target on this device" --i-am-authorized-bespoke "I am authorized to observe this target via the bespoke agent" \
  --i-am-authorized-bespoke-active "I am authorized to MODIFY this target's behaviour via the bespoke agent" --duration 90

Native tap target: platform /apex/.../libssl. Expected: the pinned endpoint stops throwing and the Apache client returns its payload; the per-flavour secret reads off the wire. Gotcha: same force-void path as every other Conscrypt-backed Java stack — one hook covers Apache, Volley, HttpURLConnection, and OkHttp's trust layer at once.

Cronet

Read layer: Native in-process tap — system-proxy-blind

Cronet is the Chromium network stack and does its TLS in its own statically-linked BoringSSL inside the bundled libcronet.*.so (or Play Services' libmonochrome) — not the platform Conscrypt libssl. It is system-proxy-blind: it does not route through the Android HTTP proxy, so a proxy MITM sees nothing. The read path is the native in-process tap (SSL_read / SSL_write), which reads cleartext regardless of pinning. If the app pins via CronetEngine.Builder.addPublicKeyPins, Rutken neutralises it with an addPublicKeyPins no-op (see the Cronet pinning flavour).

# Native plaintext tap -- no proxy, immune to pinning
rutken <app>.apk run --bespoke \
  --bespoke-attach-agent <arm64 libagent.so> \
  --bespoke-hooks native-tls.json \        # SSL_read / SSL_write / keylog symbols
  --bespoke-raw-bytes \                     # surface the actual bytes, not a digest
  --bespoke-authorize <sha256> \
  --i-am-authorized-bespoke "I am authorized to observe this target via the bespoke agent" \
  --i-am-authorized-bespoke-active "I am authorized to MODIFY this target's behaviour via the bespoke agent"

# Also neutralise addPublicKeyPins on the real CronetEngine.Builder
rutken <app>.apk run --bespoke --bespoke-attach-agent <agent> \
  --bespoke-pack java.tls.cronet_add_public_key_pins_noop \
  --i-am-authorized-bespoke "I am authorized to observe this target via the bespoke agent" --i-am-authorized-bespoke-active "I am authorized to MODIFY this target's behaviour via the bespoke agent"

Native tap target: the app-bundled Cronet BoringSSL (libcronet.*.so / libmonochrome), not /apex Conscrypt. Expected: with --bespoke-raw-bytes the real request and response print in cleartext, secret headers included; the pack logs the no-op firing. Gotcha: do not expect proxy flows for Cronet — it is proxy-blind, so the native tap is the read path, not a fallback. The native tier is R8-immune.

WebView

Read layer: Proxy MITM · native tap

WebView runs the system WebView's Chromium net stack, which generally honours the system proxy, so unpinned WebView traffic decrypts through the proxy. For a WebView that pins via a WebViewClient.onReceivedSslError override, read the plaintext directly with the native in-process tap instead (see the WebView flavour). When the app's sensitive traffic also traverses a Java TLS stack or BoringSSL, the native way-2 tap still reads that plaintext.

# Unpinned WebView: decrypt through the proxy (embed route)
rutken <app>.apk capture --device <serial> --embed-trust \
  --owned-allowlist owned-allowlist.txt \
  --agent-so target/aarch64-linux-android/release/libagent.so \
  --library-name <pool-resident-name> --keystore signing.p12 --key-alias k \
  --i-am-authorized-repack "I am authorized to repack and reinstall this target on this device" --i-am-authorized-bespoke "I am authorized to observe this target via the bespoke agent" \
  --i-am-authorized-bespoke-active "I am authorized to MODIFY this target's behaviour via the bespoke agent" --duration 90

# Pinned via onReceivedSslError: read the BoringSSL
# plaintext in-process instead
rutken <app>.apk run --bespoke --bespoke-attach-agent <agent> \
  --bespoke-hooks native-tls.json --bespoke-raw-bytes \
  --bespoke-authorize <sha256> \
  --i-am-authorized-bespoke "I am authorized to observe this target via the bespoke agent" --i-am-authorized-bespoke-active "I am authorized to MODIFY this target's behaviour via the bespoke agent"

Native tap target: the system WebView's own BoringSSL (in the WebView APK's native libs). Expected: unpinned pages decrypt on the proxy; the onReceivedSslError override itself does not flip. Gotcha: when a WebView pins, treat the native tap as the dependable path for WebView plaintext.

Flutter (dart:io)

Read layer: Native in-process tap — system-proxy-blind

Flutter's dart:io HTTP client does its TLS in a BoringSSL statically linked into libflutter.so, and it does not honour the system proxy — so, like Cronet, the read path is the native in-process tap. The pin lives on SSLCertContext::CertificateCallback, found by native pattern-scan and forced to accept. The extractNativeLibs=false case — where libflutter.so is mmap'd straight from base.apk and has no name in /proc/maps — is handled by a dl_iterate_phdr module locate (full detail on the Flutter flavour).

# Native force-accept on the dart:io certificate callback + plaintext tap
rutken <app>.apk run --bespoke --bespoke-attach-agent <arm64 libagent.so> \
  --bespoke-hooks flutter-tls.json \
  --bespoke-raw-bytes \
  --bespoke-authorize <sha256> \
  --i-am-authorized-bespoke "I am authorized to observe this target via the bespoke agent" \
  --i-am-authorized-bespoke-active "I am authorized to MODIFY this target's behaviour via the bespoke agent"

Native tap target: the app-bundled libflutter.so BoringSSL (mmap'd from base.apk when extractNativeLibs=false), not /apex Conscrypt. Expected: a Flutter app that pins via SecurityContext has its untrusted-cert probe flip to SUCCESS 200; with --bespoke-raw-bytes the request / response cleartext prints. Gotcha: proxy-blind, so there is no proxy flow to wait on — the native tier is the read path and it is R8-immune.

Xamarin / .NET MAUI

Read layer: Native in-process tap (rides Conscrypt)

Xamarin / .NET MAUI's HttpClient (via the Android AndroidMessageHandler) rides the platform Conscrypt libssl — the same TLS engine as HttpURLConnection. So the native way-2 tap reads its request / response plaintext off /apex/.../libssl with no Java-layer hook at all. Actively defeating a managed (C#) pin is a separate problem that needs a Mono-method bypass (see the Xamarin flavour); passive plaintext capture does not need it.

# Passive native tap -- reads plaintext regardless of a managed pin
rutken <app>.apk run --bespoke \
  --bespoke-attach-agent <arm64 libagent.so> \
  --bespoke-hooks native-tls.json \
  --bespoke-raw-bytes \
  --bespoke-authorize <sha256> \
  --i-am-authorized-bespoke "I am authorized to observe this target via the bespoke agent" \
  --i-am-authorized-bespoke-active "I am authorized to MODIFY this target's behaviour via the bespoke agent"

Native tap target: platform /apex/.../libssl (shared with HttpURLConnection / Conscrypt). Expected: the request and response cleartext (secret header included) appear in the recorded session — no logcat "flip", because nothing is being return-overridden for a passive read. Gotcha: the managed C# pin is not defeated by this run; the native tap simply reads the bytes around it. For an unpinned Xamarin app you can also decrypt through the proxy exactly as in the HttpURLConnection section.

Ktor / gRPC / React Native (they ride OkHttp)

Read layer: collapses to OkHttp3

These are not separate TLS stacks — each delegates to OkHttp under the hood, so capture them exactly as OkHttp3:

  • Ktor on Android (the OkHttp engine) → OkHttp3. Proxy MITM; CertificatePinner return-override if it pins.
  • gRPC-Java with the OkHttp transport → OkHttp3. Proxy MITM over its Conscrypt TLS. (A gRPC build configured on the Cronet transport instead collapses to Cronet — native tap, proxy-blind.)
  • React Native (the default networking module) → OkHttp3. Proxy MITM; CertificatePinner / NSC bypass as for OkHttp.
# Ktor / gRPC-OkHttp / React Native all ride OkHttp -> same as OkHttp3
rutken <app>.apk run --bespoke --bespoke-attach-agent <agent> \
  --bespoke-pack java.tls.certificatepinner_check \
  --bespoke-authorize <sha256> \
  --i-am-authorized-bespoke "I am authorized to observe this target via the bespoke agent" \
  --i-am-authorized-bespoke-active "I am authorized to MODIFY this target's behaviour via the bespoke agent"

Native tap target: platform /apex/.../libssl (except the Cronet-transport gRPC case, which targets the bundled Cronet BoringSSL). Gotcha: identify the real transport first — rutken sca <app>.apk fingerprints the bundled libraries so you know whether a "gRPC" app is on the OkHttp or the Cronet transport before you pick the read layer.

Command cheat-sheet

The three shapes every stack above reduces to. Replace <sha256> with the SHA-256 of the exact target APK and supply the consent phrases verbatim.

# 1. Proxy MITM, end-to-end, non-debuggable release (Route 1 embed)
#    -> HttpURLConnection, OkHttp3/Retrofit, OkHttp2, Volley, Apache, unpinned WebView
RUTKEN_KEYSTORE_PASS=<pw> rutken <app>.apk capture \
  --device <serial> --owned-allowlist owned-allowlist.txt --embed-trust \
  --agent-so target/aarch64-linux-android/release/libagent.so \
  --library-name <pool-resident-name> --keystore signing.p12 --key-alias k \
  --i-am-authorized-repack "I am authorized to repack and reinstall this target on this device" --i-am-authorized-bespoke "I am authorized to observe this target via the bespoke agent" \
  --i-am-authorized-bespoke-active "I am authorized to MODIFY this target's behaviour via the bespoke agent" --duration 90 --format json

# 2. Native in-process tap, no proxy, immune to pinning
#    -> Cronet, Flutter, Xamarin, WebView fallback, any obfuscated miss
rutken <app>.apk run --bespoke --bespoke-attach-agent <agent> \
  --bespoke-hooks native-tls.json --bespoke-raw-bytes \
  --bespoke-authorize <sha256> \
  --i-am-authorized-bespoke "I am authorized to observe this target via the bespoke agent" --i-am-authorized-bespoke-active "I am authorized to MODIFY this target's behaviour via the bespoke agent"

# 3. Defeat a specific pin (by-name pack or derived/structural)
rutken <app>.apk run --bespoke --bespoke-attach-agent <agent> \
  --bespoke-pack java.tls.certificatepinner_check \        # okhttp
  # --bespoke-pack java.tls.cronet_add_public_key_pins_noop  # cronet
  # --bespoke-derived --bespoke-force-void-cert-pinner        # R8 okhttp
  # --bespoke-derived --bespoke-force-true-hostname           # hostname verifier
  --bespoke-authorize <sha256> \
  --i-am-authorized-bespoke "I am authorized to observe this target via the bespoke agent" --i-am-authorized-bespoke-active "I am authorized to MODIFY this target's behaviour via the bespoke agent"

The pinning-bypass flag for every flavour lives on the TLS pinning bypass page; the full flag surface is always current via rutken capture --help / rutken run --help.

Troubleshooting

  • "flows: 0" but pinning says bypassed, or opaque flows only. You triggered traffic before the proxy / gate was live, or the app's launch pings raced the setup. Wait until http_proxy is set, then exercise the app — new connections pick up the proxy, launch-time ones may not.
  • Nothing on the proxy for Cronet or Flutter. Expected — both are system-proxy-blind. Use the native tap (shape 2 above) with --bespoke-raw-bytes.
  • Bespoke hooks all JavaRefused(ReturnOverrideDisabled). The return-override gate was not armed. The active ack auto-arms it under capture --embed-trust; under run --bespoke, adb shell setprop debug.rutken.return_overrides 1 before launch (only "1" enables it).
  • unsupported protocol version on bespoke attach. You used a stale / fixture agent .so. Rebuild the agent from current source.
  • The by-name okhttp pack fires 0× on a release build. R8 renamed okhttp3.CertificatePinner. Use --bespoke-derived --bespoke-force-void-cert-pinner, or read the plaintext off the native tap, which is name-independent.
  • Agent-lib name collision. Route 1 embeds the agent as lib<library-name>.so; if that name collides with a real app lib (e.g. one of the app's own bundled .so files), the agent shadows it and the app's own native function breaks (UnsatisfiedLinkError). Pick a non-colliding --library-name.

Only capture traffic from an app you own or are explicitly authorized to test. The behaviour-changing hooks require authorization acknowledgements and fail closed without them.