[PR #104] feat: JNI bridge for the split tunneling UID filter #101

Open
opened 2026-10-13 21:06:06 +00:00 by bsakaguchi · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/amnezia-vpn/amneziawg-android/pull/104
Author: @Fast-and-Pythonic
Created: 9/22/2026
Status: 🔄 Open

Base: master ← Head: pr/strict-split-tunneling


📝 Commits (2)

  • 15c9ea1 feat: JNI bridge for the split tunneling UID filter
  • 94c30d7 fix(libwg-go): do not hold the UID filter lock across the Java call

📊 Changes

3 files changed (+165 additions, -0 deletions)

View changed files

📝 tunnel/src/main/java/org/amnezia/awg/GoBackend.java (+15 -0)
📝 tunnel/tools/libwg-go/api-android.go (+28 -0)
📝 tunnel/tools/libwg-go/jni.c (+122 -0)

📄 Description

The second of three PRs fixing amnezia-vpn/amnezia-client#2457 on Android: an app excluded from the VPN can bind a socket to tun0 and reach the VPN server, which reveals the server's address.

amnezia-vpn/amneziawg-go#199 adds a uidfilter package. It drops outbound packets whose owning app the split-tunnel policy does not allow. The policy has to live on the Java side, because only the app that owns the active VpnService can ask ConnectivityManager.getConnectionOwnerUid who owns a connection. This PR connects the two.

What this adds

  • GoBackend.awgSetUidFilter(UidFilter) installs a filter, and awgSetUidFilter(null) removes it. It returns 0 on success. UidFilter is one method: boolean allow(String network, String srcIp, int srcPort, String dstIp, int dstPort).
  • In api-android.go, a Go PacketFilter that calls into C. In jni.c, the upcall into Java.

The call is an upcall from one of uidfilter's worker threads, which the JVM does not know, and jni.c handles that as follows:

  • the callback method is resolved at registration, on a Java thread where the app's classes are visible, never with FindClass from the Go thread;
  • a Go thread is attached once, as a daemon, and detached by a pthread_key destructor when it exits;
  • every call runs inside PushLocalFrame/PopLocalFrame, so no local references accumulate;
  • a mutex guards the registration only: under it a call takes a local reference to the filter, then calls Java without the lock. Replacing or removing the filter cannot race a call in flight, and several lookups can wait on the platform at once;
  • a missing filter, a failed attach or a Java exception (cleared) all deny the packet. The feature is fail-closed.

With no filter installed, nothing crosses the bridge: the Go side does not call into C at all.

Dependency

The bridge imports github.com/amnezia-vpn/amneziawg-go/v3/uidfilter, which does not exist until amnezia-vpn/amneziawg-go#199 is merged and released. Until then this branch does not build. Once it is released, this PR gets a go.mod bump to that version, and I will push it here. For review, it builds with -Werror against the amneziawg-go PR branch through a temporary replace, and both entry points are exported from libwg-go.so.

Testing

Built into the client with the rest of the series and run on a Poco F7 (Android 16) against an AmneziaWG server (protocol 3.1). With the filter on, 0 of 6 bypass attempts from an app outside the VPN reached the server, and ICMP echo through tun0 timed out. With it off, 6 of 6 did and ping answered, as before. There were no JNI errors or native crashes in logcat across several sessions with reconnects and mode switches. Re-tested on 2026-09-25 with the current amneziawg-go#199, where 4 worker threads call the filter concurrently. The logs showed denials from all four threads, no JNI errors, and connect latency under 5000 new flows/s matched the filter being off (numbers in amneziawg-go#199).

Notes and probes: https://github.com/amnezia-tun0-fix/strict-split-tunneling-notes

Series and merge order

  1. amnezia-vpn/amneziawg-go#199, the filter
  2. this PR, the bridge
  3. amnezia-vpn/amnezia-client#3199, the setting and the wiring; bumps awg-android once this is released

🤖 Generated with Claude Code


🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/amnezia-vpn/amneziawg-android/pull/104 **Author:** [@Fast-and-Pythonic](https://github.com/Fast-and-Pythonic) **Created:** 9/22/2026 **Status:** 🔄 Open **Base:** `master` ← **Head:** `pr/strict-split-tunneling` --- ### 📝 Commits (2) - [`15c9ea1`](https://github.com/amnezia-vpn/amneziawg-android/commit/15c9ea1ad3a6167b0319e65b0d748c92c76933cf) feat: JNI bridge for the split tunneling UID filter - [`94c30d7`](https://github.com/amnezia-vpn/amneziawg-android/commit/94c30d75a3a157de478692d80ee07420493a5191) fix(libwg-go): do not hold the UID filter lock across the Java call ### 📊 Changes **3 files changed** (+165 additions, -0 deletions) <details> <summary>View changed files</summary> 📝 `tunnel/src/main/java/org/amnezia/awg/GoBackend.java` (+15 -0) 📝 `tunnel/tools/libwg-go/api-android.go` (+28 -0) 📝 `tunnel/tools/libwg-go/jni.c` (+122 -0) </details> ### 📄 Description The second of three PRs fixing amnezia-vpn/amnezia-client#2457 on Android: an app excluded from the VPN can bind a socket to `tun0` and reach the VPN server, which reveals the server's address. amnezia-vpn/amneziawg-go#199 adds a `uidfilter` package. It drops outbound packets whose owning app the split-tunnel policy does not allow. The policy has to live on the Java side, because only the app that owns the active `VpnService` can ask `ConnectivityManager.getConnectionOwnerUid` who owns a connection. This PR connects the two. ## What this adds - `GoBackend.awgSetUidFilter(UidFilter)` installs a filter, and `awgSetUidFilter(null)` removes it. It returns 0 on success. `UidFilter` is one method: `boolean allow(String network, String srcIp, int srcPort, String dstIp, int dstPort)`. - In `api-android.go`, a Go `PacketFilter` that calls into C. In `jni.c`, the upcall into Java. The call is an upcall from one of `uidfilter`'s worker threads, which the JVM does not know, and `jni.c` handles that as follows: - the callback method is resolved at registration, on a Java thread where the app's classes are visible, never with `FindClass` from the Go thread; - a Go thread is attached once, as a daemon, and detached by a `pthread_key` destructor when it exits; - every call runs inside `PushLocalFrame`/`PopLocalFrame`, so no local references accumulate; - a mutex guards the registration only: under it a call takes a local reference to the filter, then calls Java without the lock. Replacing or removing the filter cannot race a call in flight, and several lookups can wait on the platform at once; - a missing filter, a failed attach or a Java exception (cleared) all deny the packet. The feature is fail-closed. With no filter installed, nothing crosses the bridge: the Go side does not call into C at all. ## Dependency The bridge imports `github.com/amnezia-vpn/amneziawg-go/v3/uidfilter`, which does not exist until amnezia-vpn/amneziawg-go#199 is merged and released. Until then this branch does not build. Once it is released, this PR gets a `go.mod` bump to that version, and I will push it here. For review, it builds with `-Werror` against the amneziawg-go PR branch through a temporary `replace`, and both entry points are exported from `libwg-go.so`. ## Testing Built into the client with the rest of the series and run on a Poco F7 (Android 16) against an AmneziaWG server (protocol 3.1). With the filter on, 0 of 6 bypass attempts from an app outside the VPN reached the server, and ICMP echo through `tun0` timed out. With it off, 6 of 6 did and ping answered, as before. There were no JNI errors or native crashes in logcat across several sessions with reconnects and mode switches. Re-tested on 2026-09-25 with the current amneziawg-go#199, where 4 worker threads call the filter concurrently. The logs showed denials from all four threads, no JNI errors, and connect latency under 5000 new flows/s matched the filter being off (numbers in amneziawg-go#199). Notes and probes: https://github.com/amnezia-tun0-fix/strict-split-tunneling-notes ## Series and merge order 1. amnezia-vpn/amneziawg-go#199, the filter 2. **this PR**, the bridge 3. amnezia-vpn/amnezia-client#3199, the setting and the wiring; bumps `awg-android` once this is released 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
Sign in to join this conversation.
No labels
pull-request
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
amnezia-vpn/amneziawg-android#101
No description provided.