Open-source project
AceDroidX/frp-Android avatar
AceDroidX/frp-Android

frp-Android: running an frp tunnel from a phone

A frp client for Android. 一个Android的frp客户端

632 stars147 forksKotlinApache-2.0

At a glance

What is it?
frp-Android is a Kotlin application that ships the frp client as a cross-compiled native executable inside the APK and drives it over a shell, with configuration handled by TOML files and control exposed through adb broadcasts and a ContentProvider. It suits people who need a reverse tunnel from an Android device on networks where port forwarding is impossible, and it comes with the Android-specific friction that implies.
Who is it for?
frp-Android earns its place for a specific and common problem: getting an inbound tunnel to a device that sits behind a carrier NAT with no port forwarding, especially one that reboots on its own schedule and needs to reconnect without supervision. It is the wrong tool if you need to swap the frp core at runtime, since Android 10 removed the execute permission that would make that possible, and it will not survive aggressive OEM background policies without manual settings.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 152 days ago.
What is it written in?
Mainly Kotlin, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

What frp-Android is: the frp kernel bundled as an executable

frp-Android is described simply as an frp client for Android, and the README's most useful paragraph is the one explaining where the native library comes from.

The libfrpc.so shipped in the app is built by cross-compiling the frp source from the fatedier/frp repository into an Android native library, using the Clang toolchain of the Android NDK with GOOS=android, and it supports three architectures: arm64-v8a, armeabi-v7a and x86_64.

The next sentence is the design decision that defines the whole application: the project does not call methods inside the .so, it treats the .so as an executable file and runs the corresponding commands through a shell. That is why the app can use a stock frp configuration file unchanged, and also why the frp version is fixed at build time. The binary is a program with a command line, not a library with an API, so the app's job is to manage a process, write a configuration file, feed it commands and read its output.

The application itself is written in Kotlin, in the conventional Android project layout with app/, build.gradle.kts, gradle/ and gradle.properties, plus a scripts/ directory holding the build helpers, an AGENTS.md, a devcontainer configuration and a keystore.example.properties for signing. The licence is Apache-2.0, and the project ships both a Chinese and an English README.

The releases are v1.3.2 from 2026-05-24, v1.3.1 from 2026-05-06 and v1.3.0 from 2025-12-20, with the last push to the repository on 2026-05-24 and the repository not archived.

Building through GitHub Actions, with signing secrets if you want them

The build documentation offers two routes, GitHub Actions and Android Studio, and the GitHub Actions one is marked as the recommended path.

The first step is to fork the project. The signing steps are optional, and the README explains what happens if you skip them: the package is signed with Android's public default debug key, which is fine for a personal build and not what you want for anything you distribute.

If you do want your own signature, the key file has to be made available to the build. The README gives a Linux example for encoding the keystore so it can be stored as a repository secret:

shell
base64 -w 0 keystore.jks > keystore.jks.base64

You then add four secrets in the GitHub settings under Secrets and variables, Actions, Repository secrets: KEY_ALIAS, KEY_PASSWORD, STORE_FILE and STORE_PASSWORD. The content of STORE_FILE is the base64 output from the previous step, and the other three you fill in from your own key file.

The build is then triggered either by pushing a commit or by running the Android CI workflow manually from the Actions page, and the manual trigger accepts a tag naming the frp core version to download, with v0.65.0 given as an example. Leaving that field empty, and any automatic trigger, downloads the latest version.

That parameter is the practical reason to build the app yourself at all: the version of the frp core inside the APK is chosen at build time, and the workflow lets you pin it.

Building locally: Android Studio, the NDK and the frp cross-compilation scripts

The Android Studio route assumes you are comfortable with a native toolchain, and the scripts directory carries the instructions.

The steps are: optionally create a keystore.properties file in the project root, using the neighbouring keystore.example.properties as the reference. Then either run the build_frp_binaries script to compile the kernel from the frp source, which requires Go and the Android NDK environment, or run the update_frp_binaries script to download a prebuilt core instead of compiling it. Finally you build and package with Android Studio.

The distinction between those two scripts is the interesting part. Compiling from source means you can patch the frp core or pin any version of it, at the cost of a Go toolchain and an NDK on your machine. Downloading a prebuilt binary is quick and reproducible, but it means trusting an artefact you did not build.

The scripts directory has its own README with the script documentation, and the repository also carries a .devcontainer directory, which suggests the development environment is meant to be reproducible in a container rather than assembled by hand on each machine.

For a first evaluation of the app, the prebuilt route is the faster one, and the GitHub Actions route requires no local native toolchain at all beyond the fork.

Configuration details that decide whether the tunnel actually stays up

The FAQ section documents the three configuration problems that come up in practice on a phone, and each has a concrete answer tied to frp's own options.

The first is the boot-time race. If the app starts with the device, frpc begins connecting before the network is ready and the first login attempt fails, at which point frpc exits and the tunnel never comes up. Setting loginFailExit = false in the frpc configuration makes the first failed login non-fatal, so the client retries instead of quitting. That single line is the difference between a tunnel that survives a reboot and one that does not.

The second is DNS resolution. The README states that all architectures now use a kernel compiled with GOOS=android, which resolves the DNS failures that earlier builds had, and that if DNS problems persist you should set dnsServer in the configuration to name a resolver explicitly.

The third is the frp configuration file itself, which is a TOML file managed by the app. The ContentProvider and broadcast examples in the README refer to configurations by type and name, such as a frpc configuration called example.toml, so the naming convention for those files is what the automation interfaces address.

The README also notes that the app follows native Android conventions, but that some domestic OEM systems apply stricter background controls. The concrete example given is ColorOS 16, where the app disconnects when moved to the background, and the fix is in the application settings under power management, where full background behaviour has to be allowed.

Android 17 local network permission: a new gate on LAN traffic

The most recent compatibility item in the README is also the one most likely to surprise a user. Android 17, API level 37, introduces an ACCESS_LOCAL_NETWORK permission requirement, and accessing local network resources now needs explicit user consent.

The app handles this in the onboarding flow. The first-launch guide page prompts for the permission, and it can also be enabled manually in the system settings. The consequence if it is not granted is direct: frp will not be able to communicate with devices on the local network.

That is a bigger constraint than it first appears for this particular application. An frp tunnel from a phone usually means the phone is the thing that talks to a machine on the same network, for instance a local Home Assistant instance or a development server, and that traffic is exactly what the new permission governs. On older Android versions the app keeps working, but on Android 17 and later the permission becomes a precondition rather than a nicety.

Combined with the OEM background behaviour, an Android 17 phone on a Chinese ROM needs two separate user actions before a tunnel is dependable: granting the local network permission, and allowing background activity in the battery management settings. Neither is automatable from the app.

Automation interfaces: broadcasts and a ContentProvider for configuration

The app exposes two interfaces that let other tooling drive it, and both are documented with adb commands.

The first is a BroadcastReceiver. In the settings there is a switch for starting and stopping on receipt of a broadcast, and once it is enabled the tunnel configurations can be started and stopped from a shell:

shell
adb shell am broadcast -a io.github.acedroidx.frp.START io.github.acedroidx.frp
adb shell am broadcast -a io.github.acedroidx.frp.STOP io.github.acedroidx.frp
adb shell am broadcast -a io.github.acedroidx.frp.START -e TYPE frpc -e NAME example.toml io.github.acedroidx.frp

The first two act on every configuration that has auto-start enabled, and the third targets a single configuration by type and name. The STOP action takes the same two extras.

The second interface is a ContentProvider for reading and writing the configuration files, and it is guarded by read and write switches in the settings, with the README warning about the security risk of configuration passwords leaking through it. One useful behaviour is documented: a configuration written through the provider is automatically switched to auto-start, so there is no separate step to enable it.

shell
adb shell content query --uri content://io.github.acedroidx.frp.config
adb shell content read --uri content://io.github.acedroidx.frp.config/frpc/example.toml
adb shell content write --uri content://io.github.acedroidx.frp.config/frpc/example.toml < example.toml
adb shell content delete --uri content://io.github.acedroidx.frp.config/frpc/example.toml

Those four commands list all configurations, read one, write one and delete one, all against the content://io.github.acedroidx.frp.config URI. The write command pipes a local TOML file in, and the README notes that on some devices a delete has to be issued first before a write succeeds. Inside the app, long-pressing the edit button on the home screen list opens the configuration through the same provider with a third-party application.

Why the core cannot be swapped at runtime, and where this app stops being useful

The README answers the question of whether the frp core can be changed inside the app, and the answer is no, with a reason rather than a preference. Android 10 and later removed the execute permission in the application's home directory, so a newly downloaded core file cannot be executed. Only the versions bundled in the installed package can run, which means the kernel is fixed at install time.

The author's own conclusion is that users should compile the app themselves with the core version they need, and the README acknowledges that other projects solve this differently: NekoBoxForAndroid has a plugin system that separates the binary into an installable APK plugin, and termux achieves execution of binaries in restricted environments through a set of tricks. The README is candid that both of those are complex and that the author did not implement them.

That limitation shapes the whole usage model. You are not running an app that updates itself; you are running an app that was built with a particular frp version, and you rebuild it when you want a different one.

The second limitation is the Android-specific environment itself. Background execution on modern Android is a negotiation rather than a given, and the app needs the user to intervene in the OEM settings, as the ColorOS 16 example shows.

The third is the scope of the tool. This is a client for the frp reverse proxy protocol, and it does the client side well: it embeds the kernel, manages configurations and exposes hooks for automation. If what you need is a general-purpose Android VPN client, or a proxy with a graphical rule editor and subscription support, a dedicated app of that kind is the better fit. The realistic alternative is running frpc on the phone from Termux or another command line environment, which gives you a newer core and full control of the configuration at the cost of losing the app's background keepalive handling and its automation interfaces.

Editorial conclusion

frp-Android earns its place for a specific and common problem: getting an inbound tunnel to a device that sits behind a carrier NAT with no port forwarding, especially one that reboots on its own schedule and needs to reconnect without supervision. It is the wrong tool if you need to swap the frp core at runtime, since Android 10 removed the execute permission that would make that possible, and it will not survive aggressive OEM background policies without manual settings. Verify first that your frpc configuration uses loginFailExit = false so a boot-time start retries instead of exiting, that ACCESS_LOCAL_NETWORK is granted on Android 17 and later, and that your frp server accepts the tunnel configuration you intend to ship in the TOML files.

Frequently asked questions

How do I build frp-Android?

Either fork the project and run the Android CI workflow on GitHub Actions, optionally adding the KEY_ALIAS, KEY_PASSWORD, STORE_FILE and STORE_PASSWORD secrets so the package is signed with your own key instead of the default debug key, or build locally in Android Studio after running the build_frp_binaries script, which needs Go and the Android NDK, or the update_frp_binaries script, which downloads a prebuilt kernel.

Where does the libfrpc.so in the app come from?

It is cross-compiled from the frp source with the Clang toolchain of the Android NDK using GOOS=android, and it is built for arm64-v8a, armeabi-v7a and x86_64. The app does not call functions inside the library, it treats it as an executable and runs its commands through a shell.

Why does my frp client exit immediately after a reboot?

Because the network is not ready when the app starts, so the first login fails and frpc exits. Setting loginFailExit = false in the frp configuration makes the first failed login non-fatal and lets the client retry.

Can I change the frp core version inside the app?

No. Android 10 and later removed the execute permission in the app home directory, so a downloaded core file cannot be run, and only the version bundled in the APK is usable. The README recommends compiling the app yourself with the core version you need.

What is the ACCESS_LOCAL_NETWORK permission on Android 17?

Android 17, API level 37, requires explicit user consent for access to local network resources. The app asks for it on the first-launch guide page and it can also be enabled in the system settings. Without it, frp cannot communicate with devices on the local network.

How can I start a tunnel from adb?

Enable the start and stop on broadcast switch in the settings, then send an intent with adb shell am broadcast using the action io.github.acedroidx.frp.START or io.github.acedroidx.frp.STOP, adding -e TYPE frpc -e NAME example.toml to target a single configuration.

Official sources

  1. AceDroidX/frp-Android on GitHub
  2. Issues
  3. License: Apache-2.0
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/acedroidx-frp-android.svg)](https://hysenlabs.com/projects/acedroidx-frp-android)

Was this useful?