├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── README.md └── src └── main.rs /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | name: Continuous integration 4 | 5 | jobs: 6 | check: 7 | name: Check 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: actions-rs/toolchain@v1 12 | with: 13 | profile: minimal 14 | toolchain: stable 15 | override: true 16 | - uses: actions-rs/cargo@v1 17 | with: 18 | command: check 19 | 20 | test: 21 | name: Test Suite 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v2 25 | - uses: actions-rs/toolchain@v1 26 | with: 27 | profile: minimal 28 | toolchain: stable 29 | override: true 30 | - uses: actions-rs/cargo@v1 31 | with: 32 | command: test 33 | 34 | fmt: 35 | name: Rustfmt 36 | runs-on: ubuntu-latest 37 | steps: 38 | - uses: actions/checkout@v2 39 | - uses: actions-rs/toolchain@v1 40 | with: 41 | profile: minimal 42 | toolchain: stable 43 | override: true 44 | - run: rustup component add rustfmt 45 | - uses: actions-rs/cargo@v1 46 | with: 47 | command: fmt 48 | args: --all -- --check 49 | 50 | clippy: 51 | name: Clippy 52 | runs-on: ubuntu-latest 53 | steps: 54 | - uses: actions/checkout@v2 55 | - uses: actions-rs/toolchain@v1 56 | with: 57 | profile: minimal 58 | toolchain: stable 59 | override: true 60 | - run: rustup component add clippy 61 | - uses: actions-rs/cargo@v1 62 | with: 63 | command: clippy 64 | args: -- -D warnings 65 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Upload to release 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | build_x86: 9 | name: Upload x86_64 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions-rs/toolchain@v1 14 | with: 15 | toolchain: stable 16 | - uses: actions-rs/cargo@v1 17 | with: 18 | use-cross: true 19 | command: build 20 | args: --release --target x86_64-unknown-linux-musl 21 | - uses: actions/upload-release-asset@v1 22 | env: 23 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 24 | with: 25 | upload_url: ${{ github.event.release.upload_url }} 26 | asset_path: target/x86_64-unknown-linux-musl/release/proxycat 27 | asset_name: proxycat_x86_64 28 | asset_content_type: application/octet-stream 29 | 30 | build_arm: 31 | name: Upload ARM 32 | runs-on: ubuntu-latest 33 | steps: 34 | - uses: actions/checkout@v2 35 | - uses: actions-rs/toolchain@v1 36 | with: 37 | toolchain: stable 38 | - uses: actions-rs/cargo@v1 39 | with: 40 | use-cross: true 41 | command: build 42 | args: --release --target arm-unknown-linux-musleabi 43 | - uses: actions/upload-release-asset@v1 44 | env: 45 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 46 | with: 47 | upload_url: ${{ github.event.release.upload_url }} 48 | asset_path: target/arm-unknown-linux-musleabi/release/proxycat 49 | asset_name: proxycat_armv6 50 | asset_content_type: application/octet-stream 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | [[package]] 4 | name = "ansi_term" 5 | version = "0.11.0" 6 | source = "registry+https://github.com/rust-lang/crates.io-index" 7 | checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" 8 | dependencies = [ 9 | "winapi", 10 | ] 11 | 12 | [[package]] 13 | name = "anyhow" 14 | version = "1.0.28" 15 | source = "registry+https://github.com/rust-lang/crates.io-index" 16 | checksum = "d9a60d744a80c30fcb657dfe2c1b22bcb3e814c1a1e3674f32bf5820b570fbff" 17 | 18 | [[package]] 19 | name = "atty" 20 | version = "0.2.14" 21 | source = "registry+https://github.com/rust-lang/crates.io-index" 22 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 23 | dependencies = [ 24 | "hermit-abi", 25 | "libc", 26 | "winapi", 27 | ] 28 | 29 | [[package]] 30 | name = "bitflags" 31 | version = "1.2.1" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" 34 | 35 | [[package]] 36 | name = "clap" 37 | version = "2.33.0" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "5067f5bb2d80ef5d68b4c87db81601f0b75bca627bc2ef76b141d7b846a3c6d9" 40 | dependencies = [ 41 | "ansi_term", 42 | "atty", 43 | "bitflags", 44 | "strsim", 45 | "textwrap", 46 | "unicode-width", 47 | "vec_map", 48 | ] 49 | 50 | [[package]] 51 | name = "hermit-abi" 52 | version = "0.1.10" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "725cf19794cf90aa94e65050cb4191ff5d8fa87a498383774c47b332e3af952e" 55 | dependencies = [ 56 | "libc", 57 | ] 58 | 59 | [[package]] 60 | name = "libc" 61 | version = "0.2.68" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "dea0c0405123bba743ee3f91f49b1c7cfb684eef0da0a50110f758ccf24cdff0" 64 | 65 | [[package]] 66 | name = "proxycat" 67 | version = "0.2.0" 68 | dependencies = [ 69 | "anyhow", 70 | "clap", 71 | ] 72 | 73 | [[package]] 74 | name = "strsim" 75 | version = "0.8.0" 76 | source = "registry+https://github.com/rust-lang/crates.io-index" 77 | checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" 78 | 79 | [[package]] 80 | name = "textwrap" 81 | version = "0.11.0" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" 84 | dependencies = [ 85 | "unicode-width", 86 | ] 87 | 88 | [[package]] 89 | name = "unicode-width" 90 | version = "0.1.7" 91 | source = "registry+https://github.com/rust-lang/crates.io-index" 92 | checksum = "caaa9d531767d1ff2150b9332433f32a24622147e5ebb1f26409d5da67afd479" 93 | 94 | [[package]] 95 | name = "vec_map" 96 | version = "0.8.1" 97 | source = "registry+https://github.com/rust-lang/crates.io-index" 98 | checksum = "05c78687fb1a80548ae3250346c3db86a80a7cdd77bda190189f2d0a0987c81a" 99 | 100 | [[package]] 101 | name = "winapi" 102 | version = "0.3.8" 103 | source = "registry+https://github.com/rust-lang/crates.io-index" 104 | checksum = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6" 105 | dependencies = [ 106 | "winapi-i686-pc-windows-gnu", 107 | "winapi-x86_64-pc-windows-gnu", 108 | ] 109 | 110 | [[package]] 111 | name = "winapi-i686-pc-windows-gnu" 112 | version = "0.4.0" 113 | source = "registry+https://github.com/rust-lang/crates.io-index" 114 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 115 | 116 | [[package]] 117 | name = "winapi-x86_64-pc-windows-gnu" 118 | version = "0.4.0" 119 | source = "registry+https://github.com/rust-lang/crates.io-index" 120 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 121 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "proxycat" 3 | version = "0.2.0" 4 | authors = ["Terry Chia "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | clap = "2.33.0" 11 | anyhow = "1.0.28" 12 | 13 | [profile.release] 14 | opt-level = 'z' # Optimize for size. 15 | lto = true 16 | codegen-units = 1 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # proxycat 2 | 3 | `proxycat` makes it easy to transparently proxy a specific Android 4 | application's TCP traffic. 5 | 6 | `proxycat` is a largely a wrapper around `iptables` and has to be run on an 7 | Android device with root privileges. `proxycat` does not take into 8 | consideration existing iptables rules on the device and might conflict with or 9 | clobber existing rules. Use with caution. 10 | 11 | `iptables` rules are inserted into the `PROXYCAT` chain in the `nat` table. 12 | Run `iptables -t nat -L` on the Android device to view the inserted rules. 13 | 14 | ## Usage 15 | 16 | ``` 17 | ➜ ./proxycat 18 | proxycat 0.1.0 19 | Terry Chia 20 | 21 | 22 | USAGE: 23 | proxycat [SUBCOMMAND] 24 | 25 | FLAGS: 26 | -h, --help Prints help information 27 | -V, --version Prints version information 28 | 29 | SUBCOMMANDS: 30 | add Add proxy rule. 31 | clean Remove iptable NAT rules. 32 | help Prints this message or the help of the given subcommand(s) 33 | ``` 34 | 35 | ### `proxycat add` 36 | 37 | This subcommand is used to add a new `iptables` rule and requires the package 38 | name and proxy address as arguments. 39 | 40 | ``` 41 | ➜ ./proxycat add --help 42 | proxycat-add 43 | Add proxy rule. 44 | 45 | USAGE: 46 | proxycat add 47 | 48 | FLAGS: 49 | -h, --help Prints help information 50 | -V, --version Prints version information 51 | 52 | ARGS: 53 | Android app to proxy. 54 | Proxy address to use. 55 | ``` 56 | 57 | ### `proxycat clean` 58 | 59 | The `clean` subcommand removes all `nat` rules from the device. 60 | 61 | ## How It Works 62 | 63 | `iptables` rules can be configured to apply to packets created by a specific 64 | user with the `--uid-owner` option. 65 | 66 | This can be used to create `iptables` rules that apply to a specific 67 | application as every Android app is assigned a unique UID at install time. This 68 | UID is stored in the `/data/system/packages.list` file which is parsed by 69 | `proxycat`. 70 | 71 | `nat` rules are then added to transparently proxy traffic to the specified 72 | proxy address. 73 | 74 | ## Building 75 | 76 | `proxycat` can be compiled into a static binary with the following command: 77 | 78 | ``` 79 | ➜ cargo build --target x86_64-unknown-linux-musl --release 80 | ``` 81 | 82 | If a binary for another architecture is required, `proxycat` can be 83 | cross-compiled using [cross][cross]. 84 | 85 | ``` 86 | ➜ cross build --target arm-unknown-linux-musleabi --release 87 | ``` 88 | 89 | [cross]: https://github.com/rust-embedded/cross 90 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{bail, Result}; 2 | use clap::{App, AppSettings, Arg, SubCommand}; 3 | use std::collections::HashMap; 4 | use std::fs::File; 5 | use std::io::{BufRead, BufReader}; 6 | use std::process::{Command, Stdio}; 7 | 8 | fn main() -> Result<()> { 9 | let matches = App::new(clap::crate_name!()) 10 | .author(clap::crate_authors!()) 11 | .version(clap::crate_version!()) 12 | .about(clap::crate_description!()) 13 | .setting(AppSettings::ArgRequiredElseHelp) 14 | .subcommand( 15 | SubCommand::with_name("add") 16 | .about("Add proxy rule.") 17 | .arg( 18 | Arg::with_name("PACKAGE") 19 | .help("Android app to proxy.") 20 | .required(true) 21 | .index(1), 22 | ) 23 | .arg( 24 | Arg::with_name("PROXY") 25 | .help("Proxy address to use.") 26 | .required(true) 27 | .index(2), 28 | ), 29 | ) 30 | .subcommand(SubCommand::with_name("clean").about("Remove iptable NAT rules.")) 31 | .get_matches(); 32 | 33 | if let Some(matches) = matches.subcommand_matches("add") { 34 | add(matches)?; 35 | } 36 | 37 | if matches.subcommand_matches("clean").is_some() { 38 | clean()?; 39 | } 40 | 41 | Ok(()) 42 | } 43 | 44 | fn add(matches: &clap::ArgMatches) -> Result<()> { 45 | let package_name = matches.value_of("PACKAGE").unwrap(); 46 | let proxy = matches.value_of("PROXY").unwrap(); 47 | 48 | let packages = parse_packages_list()?; 49 | 50 | if let Some(v) = packages.get(package_name) { 51 | setup_proxycat_chain()?; 52 | insert_iptable_rule(v, proxy)?; 53 | } else { 54 | bail!("Package {} not installed on device.", package_name); 55 | } 56 | 57 | Ok(()) 58 | } 59 | 60 | fn clean() -> Result<()> { 61 | let status = Command::new("iptables") 62 | .stdout(Stdio::null()) 63 | .stderr(Stdio::null()) 64 | .args(&["-t", "nat", "-n", "-L", "PROXYCAT"]) 65 | .status()?; 66 | 67 | if status.success() { 68 | Command::new("iptables") 69 | .args(&["-t", "nat", "-F", "PROXYCAT"]) 70 | .status()?; 71 | 72 | Command::new("iptables") 73 | .args(&["-t", "nat", "-D", "OUTPUT", "-j", "PROXYCAT"]) 74 | .status()?; 75 | 76 | Command::new("iptables") 77 | .args(&["-t", "nat", "-X", "PROXYCAT"]) 78 | .status()?; 79 | } 80 | 81 | Ok(()) 82 | } 83 | 84 | fn parse_packages_list() -> Result> { 85 | let file = File::open("/data/system/packages.list")?; 86 | 87 | let mut map = HashMap::new(); 88 | 89 | for line in BufReader::new(file).lines() { 90 | let line = line?; 91 | let mut l = line.split_ascii_whitespace(); 92 | 93 | let package_name = l.next().unwrap().to_string(); 94 | let package_uid = l.next().unwrap().to_string(); 95 | 96 | map.insert(package_name, package_uid); 97 | } 98 | 99 | Ok(map) 100 | } 101 | 102 | fn setup_proxycat_chain() -> Result<()> { 103 | let status = Command::new("iptables") 104 | .stdout(Stdio::null()) 105 | .stderr(Stdio::null()) 106 | .args(&["-t", "nat", "-n", "-L", "PROXYCAT"]) 107 | .status()?; 108 | 109 | if !status.success() { 110 | Command::new("iptables") 111 | .args(&["-t", "nat", "-N", "PROXYCAT"]) 112 | .status()?; 113 | 114 | Command::new("iptables") 115 | .args(&["-t", "nat", "-I", "OUTPUT", "-j", "PROXYCAT"]) 116 | .status()?; 117 | } 118 | 119 | Ok(()) 120 | } 121 | 122 | fn insert_iptable_rule(uid: &str, proxy: &str) -> Result<()> { 123 | Command::new("iptables") 124 | .args(&[ 125 | "-t", 126 | "nat", 127 | "-A", 128 | "PROXYCAT", 129 | "-m", 130 | "owner", 131 | "--uid-owner", 132 | uid, 133 | "-p", 134 | "tcp", 135 | "-j", 136 | "DNAT", 137 | "--to-destination", 138 | proxy, 139 | ]) 140 | .status()?; 141 | 142 | Ok(()) 143 | } 144 | --------------------------------------------------------------------------------