Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
name: CI

# End-to-end and unit tests run on GitHub Actions (a free CI provider for
# public repositories). The whole suite runs on the JVM via Robolectric, so no
# Android emulator/device is required.

on:
push:
branches: [ master, update-to-current-toolchain ]
pull_request:
workflow_dispatch:

jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '17'

- name: Set up Android SDK
uses: android-actions/setup-android@v3

- name: Install required SDK packages
run: sdkmanager "platform-tools" "platforms;android-35" "build-tools;35.0.0"

- name: Cache Gradle
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle', 'gradle/wrapper/gradle-wrapper.properties') }}
restore-keys: gradle-${{ runner.os }}-

- name: Run unit and end-to-end tests
run: ./gradlew testDebugUnitTest --console=plain --stacktrace

- name: Upload test report
if: always()
uses: actions/upload-artifact@v4
with:
name: test-report
path: build/reports/tests/
if-no-files-found: ignore
8 changes: 7 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,10 @@
build/
**.orig
**.rej
**.patch
**.patch
release/
debug/
# Machine-specific / IDE files that should not be in VCS
local.properties
.idea/
.envrc.local
53 changes: 34 additions & 19 deletions AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,40 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="hu.blint.ssldroid" android:versionCode="9"
android:versionName="1.2.6">
<uses-sdk android:minSdkVersion="9" android:targetSdkVersion="17" />
xmlns:tools="http://schemas.android.com/tools"
android:versionCode="10"
android:versionName="1.3"
tools:ignore="GoogleAppIndexingWarning">

<permission android:name="hu.blint.ssldroid.permission.startstop" android:protectionLevel="normal" android:label="Start and stop the SSLDroid service" />
<uses-permission android:name="hu.blint.ssldroid.permission.startstop" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.READ_LOGS" />
<application android:label="@string/app_name" android:icon="@drawable/icon" android:allowBackup="true">
<activity android:name="SSLDroidGui" android:label="@string/app_name">
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application
android:allowBackup="true"
android:fullBackupContent="@xml/ssldroid_backup_rules"
android:icon="@drawable/icon"
android:label="@string/app_name">
<activity
android:name="SSLDroidGui"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".SSLDroidTunnelDetails"
android:windowSoftInputMode="stateVisible|adjustResize" />
<activity android:name=".SSLDroidReadLogs" />
<activity android:name=".SSLDroidProvisioning" />
<service android:enabled="true" android:name="SSLDroid" android:permission="hu.blint.ssldroid.permission.startstop">
android:windowSoftInputMode="stateVisible|adjustResize" android:exported="true"/>
<activity android:name=".SSLDroidReadLogs" android:exported="true" />
<activity android:name=".SSLDroidProvisioning" android:exported="true" />
<service
android:enabled="true"
android:name="SSLDroid"
android:exported="true"
android:permission="hu.blint.ssldroid.permission.startstop">
<intent-filter>
<action android:name="hu.blint.ssldroid.SSLDroid" />
</intent-filter>
</service>
<receiver android:name="BootStartupReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<category android:name="android.intent.category.HOME" />
</intent-filter>
</receiver>
<receiver android:name="NetworkChangeReceiver">
<service
android:name=".NetworkChangeService"
android:permission="android.permission.BIND_JOB_SERVICE"
android:exported="true"/>
<receiver
android:enabled="true"
android:name=".BootStartupReceiver"
android:exported="true"
android:permission="android.permission.RECEIVE_BOOT_COMPLETED">
<intent-filter>
<action android:name="android.net.conn.CONNECTIVITY_CHANGE"/>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</receiver>
</application>
Expand Down
1 change: 1 addition & 0 deletions assets/ssldroid_logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
69 changes: 58 additions & 11 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
buildscript {
repositories {
mavenCentral()
google()
}
dependencies {
classpath 'com.android.tools.build:gradle:1.1.0'
classpath 'com.android.tools.build:gradle:8.9.1'
}
}

allprojects {
repositories {
mavenCentral()
google()
}
}
apply plugin: 'com.android.application'

android {
compileSdkVersion 17
buildToolsVersion '22.0.1'

namespace 'hu.blint.ssldroid'

sourceSets {
main {
Expand All @@ -24,16 +31,56 @@ android {
assets.srcDirs = ['assets']
}

// Move the tests to tests/java, tests/res, etc...
instrumentTest.setRoot('tests')
// Local JVM unit/e2e tests live outside 'src' so they are not swept up
// into the main source set (which uses the whole 'src' tree).
test {
java.srcDirs = ['tests/java']
resources.srcDirs = ['tests/resources']
}

// Move the build types to build-types/<type>
// For instance, build-types/debug/java, build-types/debug/AndroidManifest.xml, ...
// This moves them out of them default location under src/<type>/... which would
// conflict with src/ being used by the main source set.
// Adding new build types or product flavors should be accompanied
// by a similar customization.
debug.setRoot('build-types/debug')
release.setRoot('build-types/release')
}
defaultConfig {
minSdkVersion 21
targetSdkVersion 33
}

compileSdk 35

buildFeatures {
renderScript true
aidl true
}

testOptions {
unitTests {
includeAndroidResources = true
returnDefaultValues = true
all {
// Surface the proxy's android.util.Log output in test logs,
// which makes CI failures in the e2e tunnel tests diagnosable.
systemProperty 'robolectric.logging', 'stdout'

// Robolectric instruments core classes, so on JDK 17 the test
// JVM needs reflective access to these java.base packages.
// Without java.net opened, real TLS handshakes in the e2e tests
// fail with an InaccessibleObjectException on InetAddress.
jvmArgs(
'--add-opens=java.base/java.lang=ALL-UNNAMED',
'--add-opens=java.base/java.util=ALL-UNNAMED',
'--add-opens=java.base/java.io=ALL-UNNAMED',
'--add-opens=java.base/java.net=ALL-UNNAMED',
'--add-opens=java.base/java.security=ALL-UNNAMED',
'--add-opens=java.base/javax.net.ssl=ALL-UNNAMED'
)
}
}
}
}

dependencies {
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.robolectric:robolectric:4.14.1'
testImplementation 'androidx.test:core:1.6.1'
}
75 changes: 75 additions & 0 deletions docs/branch-review.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Review: `update-to-current-toolchain`

Scope: the 18 commits on `update-to-current-toolchain` vs `master`. The branch
modernises the build (AGP 8.9.1, Gradle 8.11.1, `compileSdk 35`, `targetSdk 33`,
namespace, foreground service, notification channel, boot/network receivers) and
reorganises the `db` package back into the main package.

Overall the toolchain migration is sound and the app compiles against the new
SDK. The findings below are the outstanding functional issues; the first is a
user-facing regression introduced by this branch.

## Findings

### 1. (Blocker, regression) `SSLDroidDbAdapter.createContentValues` drops `cacertfile` and `usesni`

`createContentValues(...)` takes `cacertfile` and `usesni` parameters but never
writes them into the `ContentValues`:

- `master` persisted `cacertfile` (`values.put(KEY_CACERTFILE, cacertfile)`).
This branch's refactor removed that line, so **CA-pinning configuration is
silently lost** on save.
- The branch added a new `usesni` column (`usesni integer not null`) and a
`usesni` parameter, but never persists it. So **the SNI checkbox is not saved**.
- Worse: on a **fresh** install the `tunnels.usesni` column is `NOT NULL` with no
default (only upgraded DBs get `default 1` via `onUpgrade`). Inserting a row
without `usesni` violates the constraint, `insert` returns `-1`, and
`saveState()` treats that as failure — **tunnels cannot be saved at all on a
clean install.**

There is also a duplicated `values.put(KEY_REMOTEPORT, remoteport)` line.

Fix: put `KEY_CACERTFILE` and `KEY_USE_SNI`, drop the duplicate. Covered by the
new regression test `SSLDroidDbAdapterTest.persistsCaCertFileAndSniFlag`.

### 2. (Minor) Handshake failure leaks the accepted client socket

In `TcpProxyServerThread.run()`, when the upstream TLS `createSocket` /
`startHandshake` throws `IOException`, the handler logs and `return`s without
closing the already-accepted client socket `sc`. The sibling `catch (Exception)`
does close `sc`. The client is then left with a half-open connection that only
resolves on its own timeout (this is what the negative e2e test observes).
Non-blocking for this task; noting for a follow-up.

### 3. (Minor) A single bad connection tears down the whole tunnel

Several error paths in the `accept()` loop `return` from `run()` instead of
`continue`-ing, so one failed upstream connection stops the listener for that
tunnel until the service restarts. Pre-existing behaviour, not introduced here.

### 4. (Cosmetic) Dead byte-scrubbing loop in `Relay.run()`

The `for` loop that rewrites byte `0x07` to `'#'` runs *after* the buffer has
already been written to `out`, so it has no effect — leftover from the original
`TcpTunnelGui`. Harmless; could be deleted.

### 5. (Housekeeping) IDE and local files committed

`.idea/` (incl. `workspace.xml`) and `local.properties` are tracked on this
branch. `local.properties` is machine-specific (`sdk.dir=/home/blint/...`) and
should not be in VCS. Recommend adding both to `.gitignore`.

## Testing added by this change

- `tests/java/hu/blint/ssldroid/TcpProxyE2ETest.java` — true end-to-end tunnel
tests (cleartext client → real `TcpProxy` → real TLS backend), covering the
no-pinning, correct-CA, and wrong-CA cases.
- `tests/java/hu/blint/ssldroid/SSLDroidDbAdapterTest.java` — CRUD + the
regression test for finding #1.
- `.github/workflows/ci.yml` — runs the suite on GitHub Actions (free provider),
entirely on the JVM via Robolectric (no emulator needed).

Note: because Robolectric instruments core classes, the unit-test JVM is started
with `--add-opens` for several `java.base` packages (configured in
`build.gradle`); without `java.base/java.net` opened, JDK 17 fails the real TLS
handshake in the e2e tests with an `InaccessibleObjectException`.
4 changes: 4 additions & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
android.nonFinalResIds=false
android.nonTransitiveRClass=false
# Robolectric's test dependencies pull in AndroidX transitively.
android.useAndroidX=true
4 changes: 2 additions & 2 deletions gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#Sat Jul 25 01:04:55 EDT 2015
#Thu Jul 04 10:15:24 CEST 2019
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-2.5-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip
13 changes: 11 additions & 2 deletions res/layout/tunnel_details.xml
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@

<EditText android:id="@+id/pkcspass" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_weight="1"
android:password="true" android:hint="Not mandatory" />
android:inputType="textPassword" android:hint="Not mandatory" />
</TableRow>
<TableRow>
<TextView android:text="@string/cacert_file"
Expand All @@ -70,8 +70,17 @@
android:layout_weight="1" android:inputType="textEmailAddress" />
<Button android:text="..." android:layout_width="wrap_content" android:id="@+id/pickCaFile" android:layout_height="wrap_content"></Button>
</TableRow>
<TableRow>
<TextView android:text="@string/usesni"
android:paddingRight="10dip" android:gravity="right|center_vertical"
android:textAppearance="?android:attr/textAppearanceMedium" />

<CheckBox android:id="@+id/usesni" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_weight="1"
android:checked="true" />
</TableRow>
<TableRow android:gravity="center">
<Button android:layout_width="fill_parent" android:layout_height="fill_parent" android:gravity="center" android:textAppearance="?android:attr/textAppearanceMedium" android:text="@string/apply" android:id="@+id/tunnel_apply_button"></Button>
</TableRow>
</TableLayout>
</ScrollView>
</ScrollView>
4 changes: 3 additions & 1 deletion res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,16 @@
<string name="empty"></string>
<string name="apply">Apply</string>
<string name="pkcspass">PKCS12 pass</string>
<string name="usesni">Use SNI</string>
<string name="connection_name">Tunnel name</string>
<string name="add_tunnel">Add tunnel</string>
<string name="stop_service">Stop service</string>
<string name="stop_service_for_good">Stop until explicit start</string>
<string name="start_service">Start service</string>
<string name="no_tunnels">No tunnels configured yet</string>
<string name="menu_delete">Delete tunnel</string>
<string name="file_pick">Pick a PKCS12 file from SD card</string>
<string name="key_file_pick">Pick a PKCS12 file from SD card</string>
<string name="ca_file_pick">Pick a CA cert file from SD card</string>
<string name="alert_sdcard_absent">No SD card present, please insert one to continue</string>
<string name="menu_readlogs">Read logs</string>
<string name="reading_logs">Reading log messages&#8230;</string>
Expand Down
6 changes: 6 additions & 0 deletions res/xml/ssldroid_backup_rules.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<full-backup-content>
<include domain="file" path="." requireFlags="clientSideEncryption" />
<include domain="database" path="." requireFlags="clientSideEncryption" />
<include domain="sharedpref" path="." requireFlags="clientSideEncryption" />
</full-backup-content>
Loading
Loading