diff --git a/README.md b/README.md index e813cb0..992e648 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,36 @@ A tunnel interface for HTTP and SOCKS proxies on Linux, Android, macOS, iOS and - SOCKS5 UDP support - Native support for proxying DNS over TCP - UdpGW (UDP gateway) support for UDP over TCP, see the [wiki](https://github.com/tun2proxy/tun2proxy/wiki/UDP-gateway-feature) for more information +- Session info embedding for per-app routing on Android (see below) +## Session Info for Per-App Routing (Android) + +To enable per-app traffic routing on Android 10+, you can embed session information (protocol, source IP, source port) in the SOCKS5 username field. This allows your proxy server to call `getConnectionOwnerUid()` to identify which app initiated the connection. + +To enable this feature, append `+info` to your username: + +```bash +# Without session info (normal mode) +tun2proxy --proxy "socks5://user:pass@127.0.0.1:1080" + +# With session info +tun2proxy --proxy "socks5://user+info:pass@127.0.0.1:1080" +``` + +The proxy server will receive the username in the format: +``` +original_username|protocol|src_ip|src_port +``` + +`|` is a literal field delimiter in this format. The client does not escape `|` characters in +`original_username`, so when using `+info`, the original username must not contain `|` (including +a `|` introduced via percent-encoding in the proxy URL), otherwise the value becomes ambiguous for +server-side parsers. + +For example, `user+info` becomes `user|tcp|10.0.0.5|54321`. + +Servers should parse this as exactly four `|`-separated fields and reject usernames that do not +meet that requirement. ## Build Clone the repository and `cd` into the project folder. Then run the following: ``` diff --git a/src/socks.rs b/src/socks.rs index 7198a68..6177a20 100644 --- a/src/socks.rs +++ b/src/socks.rs @@ -170,7 +170,21 @@ impl SocksProxyImpl { fn send_auth_data(&mut self) -> std::io::Result<()> { let tmp = UserKey::default(); let credentials = self.credentials.as_ref().unwrap_or(&tmp); - let request = password_method::Request::new(&credentials.username, &credentials.password); + + const SESSION_INFO_MARKER: &str = "+info"; + let username = if credentials.username.ends_with(SESSION_INFO_MARKER) { + let base_username = &credentials.username[..credentials.username.len() - SESSION_INFO_MARKER.len()]; + let proto = match self.command { + protocol::Command::Connect => "tcp", + protocol::Command::UdpAssociate => "udp", + _ => "unknown", + }; + format!("{}|{}|{}|{}", base_username, proto, self.info.src.ip(), self.info.src.port()) + } else { + credentials.username.clone() + }; + + let request = password_method::Request::new(&username, &credentials.password); request.write_to_stream(&mut self.server_outbuf)?; self.state = SocksState::ReceiveAuthResponse; Ok(())