| .. | ||
| app | ||
| build.gradle.kts | ||
| ci-debug.keystore | ||
| gradle.properties | ||
| README.md | ||
| settings.gradle.kts | ||
True IMAP Tunnel Shadowsocks Android plugin
This directory contains the Android wrapper that exposes the true-imap-tunnel
Go binary as a SIP003 Shadowsocks native plugin.
The plugin id is:
true-imap-tunnel
The plugin reads normal SIP003 environment variables, especially
SS_LOCAL_HOST, SS_LOCAL_PORT, SS_REMOTE_HOST, SS_REMOTE_PORT, and
SS_PLUGIN_OPTIONS.
Android client hints
In Shadowsocks Android, it is strongly recommended to enable "Apps VPN Mode" / per-app VPN and route only the apps that really need this tunnel. IMAP is not a good bulk transport, so do not invite every background service on the phone to the party.
Also enable "Metered Hint" in the profile settings. It helps Android and apps treat the path as expensive and avoid unnecessary background traffic.
Set profile "Remote DNS" to an IP literal such as 1.1.1.1 or 8.8.8.8, not
dns.google. Using the IP avoids one DNS lookup before DNS-over-proxy starts.
If plugin DNS itself fails, set dns_server=1.1.1.1 in plugin options or put
dns_servers: ["1.1.1.1"] in YAML. If no DNS is configured and the local
resolver fails at startup, the plugin falls back to 1.1.1.1:53.
In Shadowsocks Android VPN mode, the app injects the SIP003 option
__android_vpn. The plugin detects it and protects outbound IMAP/DNS sockets
through Shadowsocks' protect_path socket so its own transport traffic does not
loop back into the VPN.
The APK also has a tiny launcher app. Open it while the Shadowsocks
profile is running to see current tunnel/account status and recent logs. The
plugin exposes this only on the device loopback address
http://127.123.45.67:17680; set status_addr=off in plugin options if you
need to disable it.
Plugin config formats
There are two supported SS_PLUGIN_OPTIONS styles.
Base64 YAML
config=<base64url-yaml>
This embeds the normal YAML config. When generated by the CLI, comments are stripped first so the URL stays smaller. This is the best format for full configs and multipath.
The decoder also accepts:
config_b64=<base64url-yaml>
yaml_b64=<base64url-yaml>
tits://<base64url-yaml>
Query/inline options
imap_host=imap.example.com:993;
imap_username=user@example.com;
imap_password=app-password;
folder_send=TunnelC2S;
folder_recv=TunnelS2C
Useful optional keys:
mode=client
imap_tls=implicit
zero_rtt_open=false
async_data_send=true
batch_delay_ms=2
dns_server=1.1.1.1
message_format=attachment
multipath_mode=stream_affinity
client_version=custom-build-label
Multiple accounts use suffixes: imap_host_2, imap_username_2,
folder_send_2, and so on. The first account may use either unsuffixed keys or
_1.
client_id may be supplied explicitly. On clients such as Shadowsocks Android,
SS_REMOTE_PORT is also exposed to the plugin; we map that port to a
client id byte with port % 256, with 0 mapped to 255. This lets multiple
users share the same YAML profile while still stamping distinct stream IDs.
zero_rtt_open defaults to off for SIP003 profiles. You can turn it on, but
test the profile with your actual app traffic first.
If you are tunneling Shadowsocks, you can usually leave
encryption_passphrase empty. Shadowsocks traffic is already encrypted, and the
extra tunnel encryption layer only adds CPU work and a few bytes of overhead per
frame.
Generate Shadowsocks URLs
Generate an importable ss:// URL from a YAML client config:
.\bin\true-imap-tunnel.exe -config .\client.yaml -show-ss-url `
-ss-method aes-128-gcm `
-ss-password "shadowsocks-profile-password" `
-ss-url-format base64 `
-ss-url-skip-defaults
Generate query/inline options instead:
.\bin\true-imap-tunnel.exe -config .\client.yaml -show-ss-url `
-ss-method aes-128-gcm `
-ss-password "shadowsocks-profile-password" `
-ss-url-format query `
-ss-url-skip-defaults
The Shadowsocks profile host is a placeholder for this plugin and defaults to
tits.local. The profile port defaults to 443; on Shadowsocks Android it also
becomes the client_id override described above.
Use a method accepted by the Android Shadowsocks importer, such as
aes-128-gcm, aes-256-gcm, or chacha20-ietf-poly1305.
Getting logs
If the Android plugin profile fails to connect, first add this to plugin options or YAML for one short test run:
log_level=debug
Use log_level=trace only when you need very noisy protocol details.
The Go plugin writes logs to stdout/stderr, and Shadowsocks Android usually captures those lines in logcat together with its own service logs. Clear logcat, start the profile, reproduce the problem, then dump the interesting lines:
$adb = "$env:LOCALAPPDATA\Android\Sdk\platform-tools\adb.exe"
& $adb logcat -c
# Start the Shadowsocks profile now, reproduce the issue, then:
& $adb logcat -d -v time |
Select-String -Pattern "true-imap-tunnel|trueimaptunnel|libtrueimaptunnel|tits|Shadowsocks|sslocal|plugin"
For a live view:
& $adb logcat -v time |
Select-String -Pattern "true-imap-tunnel|trueimaptunnel|libtrueimaptunnel|tits|Shadowsocks|sslocal|plugin"
On Linux/macOS:
adb logcat -c
adb logcat -v time | grep -Ei 'true-imap-tunnel|trueimaptunnel|libtrueimaptunnel|tits|shadowsocks|sslocal|plugin'
To focus on the Shadowsocks Android app process, find its process id:
& $adb shell pidof com.github.shadowsocks
Then use that PID with logcat if your Android version supports it:
& $adb logcat -v time --pid <pid>
Also check the Shadowsocks Android UI log screen if your version has one. When
sharing logs, remove IMAP usernames, tokens, passwords, and generated ss://
URLs.
Build
Requirements:
- Android SDK installed and
ANDROID_HOMEorANDROID_SDK_ROOTpointing to it. - Gradle 8.x available on PATH, or an installed Gradle distribution.
- Go installed.
Build debug APK:
$env:ANDROID_HOME = "$env:LOCALAPPDATA\Android\Sdk"
gradle -p android-plugin assembleDebug --no-daemon --console=plain
Build release APK:
$env:ANDROID_HOME = "$env:LOCALAPPDATA\Android\Sdk"
gradle -p android-plugin assembleRelease --no-daemon --console=plain
Release APKs are unsigned unless you configure Android signing. CI publishes the
signed debug APK (TITS-shadowsocks-plugin-android-debug.apk) so downloaded
artifacts can be sideloaded directly. Debug builds use the checked-in public
ci-debug.keystore, which keeps CI artifact signatures stable across builds.
If you previously installed a locally signed or older CI-signed plugin, fully
uninstall com.trueimaptunnel.plugin from every Android user/profile before
installing the new artifact.
To clear all profiles with ADB, list users and uninstall the package for each listed ID:
$adb = "$env:LOCALAPPDATA\Android\Sdk\platform-tools\adb.exe"
& $adb shell pm list users
& $adb shell pm uninstall --user 0 com.trueimaptunnel.plugin
& $adb shell pm uninstall --user 10 com.trueimaptunnel.plugin
Failure [not installed for N] is fine for profiles where the plugin is already
absent. After the per-profile uninstall, this check should print no matching
package:
& $adb shell pm list packages -u | Select-String 'trueimap|true-imap|tits'
The Gradle build runs scripts/build-android-plugin-binary.ps1 on Windows or
scripts/build-android-plugin-binary.sh elsewhere to compile the Android arm64
Go binary and stage it as:
android-plugin/app/src/main/jniLibs/arm64-v8a/libtrueimaptunnel.so
Debug APK output:
android-plugin/app/build/outputs/apk/debug/app-debug.apk
Install to a connected device:
& "$env:LOCALAPPDATA\Android\Sdk\platform-tools\adb.exe" install -r `
".\android-plugin\app\build\outputs\apk\debug\app-debug.apk"
The Android APK build stamps its versionName, git hash, and build date into
the Go binary. CI passes versionName from the release tag, stripping a leading
v, or from the workflow-dispatch build version override. For local builds that
should report a specific version, pass -PbuildVersion=0.4.0 to Gradle. Client
Ping frames include that build string after the timestamp, and servers log it as
client_version=.... Set client_version manually only for custom wrappers or
manual builds that need a different label.