Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
31 changes: 31 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
version: 2
updates:
# Dart/Flutter dependencies (pubspec.yaml)
- package-ecosystem: "pub"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
labels:
- "dependencies"
- "dart"

# Gradle dependencies (Android build)
- package-ecosystem: "gradle"
directory: "/android"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
labels:
- "dependencies"
- "android"

# GitHub Actions workflow versions
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
labels:
- "dependencies"
- "ci"
61 changes: 61 additions & 0 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
name: CodeQL

on:
push:
branches:
- main
pull_request:
branches:
- main
- dev
schedule:
# Weekly scan for newly-disclosed vulnerability patterns, not just new code.
- cron: '19 4 * * 1'

jobs:
analyze:
name: Analyze (java-kotlin)
runs-on: ubuntu-latest
permissions:
security-events: write
packages: read

steps:
- name: Checkout with submodules
uses: actions/checkout@v4
with:
submodules: recursive

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

- name: Set up Flutter 3.44.0 stable
uses: subosito/flutter-action@v2
with:
flutter-version: '3.44.0'
channel: stable
cache: true

- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: java-kotlin
build-mode: manual

- name: Install dependencies
run: flutter pub get

# Compile only the JVM unit tests (android/app/src/test) rather than a
# full app assemble — CodeQL only needs to see the Kotlin source being
# built, and this avoids needing the native/CMake toolchain or a
# matching compileSdk platform image in the runner image.
- name: Compile Kotlin sources for analysis
run: cd android && ./gradlew compileDebugKotlin compileDebugUnitTestKotlin

- name: Perform CodeQL analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:java-kotlin"
7 changes: 5 additions & 2 deletions .github/workflows/pr-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,16 @@ jobs:
run: flutter pub get

- name: Analyze
run: flutter analyze --no-fatal-infos --no-fatal-warnings
run: flutter analyze --no-fatal-infos

- name: Run Dart unit tests
run: flutter test

- name: Run Kotlin unit tests
run: cd android && ./gradlew test
# Scoped to :app — an unscoped `test` cascades into every Flutter
# plugin dependency's own bundled unit tests too, which aren't ours
# to keep green and can fail CI for reasons unrelated to this app.
run: cd android && ./gradlew :app:testDebugUnitTest

- name: Build debug APK (verify compilation)
run: flutter build apk --debug --target-platform android-arm64
36 changes: 35 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,41 @@ jobs:
run: flutter pub get

- name: Analyze
run: flutter analyze --no-fatal-infos --no-fatal-warnings
run: flutter analyze --no-fatal-infos

- name: Reconstruct release keystore from secrets
env:
KEYSTORE_BASE64: ${{ secrets.KEYSTORE_BASE64 }}
run: |
if [ -z "$KEYSTORE_BASE64" ]; then
echo "::error::KEYSTORE_BASE64 secret is not set — release APKs would be signed with the debug key. Add the repo secrets described in android/key.properties.example before tagging a release."
exit 1
fi
echo "$KEYSTORE_BASE64" | base64 -d > android/app/release-keystore.jks
cat > android/key.properties <<EOF
storePassword=${{ secrets.KEYSTORE_PASSWORD }}
keyPassword=${{ secrets.KEY_PASSWORD }}
keyAlias=${{ secrets.KEY_ALIAS }}
storeFile=../app/release-keystore.jks
EOF

- name: Build release APK (arm64)
run: flutter build apk --release --split-per-abi --target-platform android-arm64

- name: Verify release APK is signed with our release key (not debug)
run: |
APKSIGNER="$ANDROID_HOME/build-tools/$(ls -1 $ANDROID_HOME/build-tools | sort -V | tail -1)/apksigner"
expected=$(keytool -list -v -keystore android/app/release-keystore.jks \
-storepass "${{ secrets.KEYSTORE_PASSWORD }}" 2>/dev/null | grep 'SHA256:' | head -1 | awk '{print $2}')
actual=$("$APKSIGNER" verify --print-certs build/app/outputs/flutter-apk/app-arm64-v8a-release.apk \
| grep 'Signer #1 certificate SHA-256 digest' | awk '{print $NF}')
echo "Expected (our release keystore): $expected"
echo "Actual (built APK signer): $actual"
if [ -z "$actual" ] || [ "$(echo "$expected" | tr -d ':' | tr 'A-F' 'a-f')" != "$(echo "$actual" | tr -d ':' | tr 'A-F' 'a-f')" ]; then
echo "::error::Built APK's signing certificate does not match android/app/release-keystore.jks — it was not signed with the intended release key."
exit 1
fi

- name: Rename APK with version and architecture
run: |
mv build/app/outputs/flutter-apk/app-arm64-v8a-release.apk \
Expand Down Expand Up @@ -74,3 +104,7 @@ jobs:
make_latest: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Clean up keystore secrets from the runner workspace
if: always()
run: rm -f android/app/release-keystore.jks android/key.properties
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,11 @@ scripts/*.log

# Release artifacts (iOS)
*.ipa

# Release signing — never commit the real keystore or its passwords.
# See android/key.properties.example for the template. Unanchored (not
# /android/key.properties) so a copy left at the repo root or anywhere else
# is still caught.
key.properties
*.jks
*.keystore
111 changes: 111 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,117 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

---

## [1.3.0] - 2026-08-06

A full pass across the codebase: dead code removal, real bug fixes, dependency/toolchain
modernization, security hardening, hot-path performance work, and new features. Verified with
`flutter analyze` (0 issues), `flutter test` (25/25), `./gradlew :app:testDebugUnitTest` (106/106),
and live on-device testing (VPN capture, IPv6, per-app attribution, dark mode).

### ✨ Added
- **Per-app traffic attribution** — resolves the installed app owning each TCP/UDP flow via
`ConnectivityManager.getConnectionOwnerUid` and shows it on every packet card. Something desktop
Wireshark has no equivalent of, since it doesn't run on the device whose traffic it inspects.
- **Dark mode** — system/light/dark, persisted across launches (Settings → Theme).
- **IPv6 support in VPN-mode capture** — `ZdtunVpnService` previously returned `null` for any
non-IPv4 packet, so IPv6 traffic was tunneled correctly but completely invisible to DPI, anomaly
detection, and PCAP logging. Now fully parsed (address, ports, TCP flags) and analyzed.
Confirmed live: IPv6 link-local multicast traffic now shows up correctly formatted
(`fe80::...` zero-compressed notation) in the packet stream.
- **Malformed-TCP-packet detection** — flags illegal flag combinations (SYN+FIN, SYN+RST), a
known firewall/IDS evasion and stack-fingerprinting technique. Fills in the one `AnomalyType`
that previously existed in the model but was never actually produced.
- **DNS-over-HTTPS (DoH) detection** — TLS connections whose SNI matches a known public DoH
resolver are now labeled `DoH` instead of generic `HTTPS`, surfacing a common technique for
bypassing on-path DNS monitoring.
- **Change PIN/Password/Pattern** and **configurable auto-lock duration** (1/5/15/30/60 min),
both previously backed by working service methods with no UI path to reach them.
- `.github/dependabot.yml` (pub, gradle, github-actions) and a CodeQL workflow for the
Kotlin/Java surface.

### 🐛 Fixed
- **TCP flags were never populated on the VPN-mode (unrooted, default) capture path at all** —
`ZdtunVpnService.parseIpv4Packet`/`parseIpv6Packet` never extracted them, meaning SYN-flood and
connection-flood detection silently never fired for anyone using the app's zero-setup default
mode, despite being fully implemented and README-advertised as working. Only rooted
libpcap-mode users ever got real detection for these two. Now extracted on both paths.
- **PCAP annotated-packet timestamps were corrupted** — `PcapWriter.nativeWriteAnnotatedPacket`
treated a millisecond value from Kotlin as if it were already nanoseconds with no conversion, so
every anomaly-flagged packet (exactly the ones an analyst cares about) got written with a
timestamp near the 1970 epoch. Fixed to mirror the working `nativeWritePacket` path.
- **A live, UI-reachable capture path silently dropped all packets** — "Enhanced" mode
(`CaptureService`) never wrote packets back to the TUN device, blackholing the device's internet
connection whenever selected, and never ran DPI/anomaly detection/PCAP logging at all. Retired;
enhanced mode now shares the same proven `ZdtunVpnService` pipeline as VPN mode.
- **`test/widget_test.dart` was broken** — asserted stale UI text and pumped the app without its
required `Provider` ancestor, so `flutter test` (and CI's Kotlin/Dart test steps) had been
failing on every push/PR since at least May 2026. CI is green again; PR checks now fail on
`flutter analyze` warnings, not just hard errors, so this can't silently regress.
- `PcapWriter.nativeWriteAnnotatedPacket`'s JNI signature mismatch (`Boolean` in Kotlin vs `void`
in C) — the returned success/failure value was previously meaningless.
- PCAP file-count rotation (`max_files`) was a no-op that only logged; old rotated captures now
actually get pruned.
- `PacketInfo.fromMap`'s numeric field parsing threw (rather than falling back) for non-null,
non-numeric values (e.g. a stringified port) because `as num?` throws instead of returning null
for a type mismatch — caught by a new unit test, fixed with a proper safe-coercion helper.
- `RuleEngine`'s DNS-tunneling rule's `DomainMatches` condition was defined and used by the
default rule set but never handled by the evaluator, silently always `false` — caught by a new
Kotlin unit test, fixed.
- Root detection was implemented three different, inconsistent ways across the codebase (including
a fragile unquoted `su -c 'id'` shell string); consolidated into one `RootChecker`.
- Settings dialog: "Auto-scroll" no longer closes the whole dialog on every toggle; "Anomaly
notifications" and "Max packets limit" are now real, working, persisted-for-the-session controls
instead of a hardcoded switch and a display-only label.

### 🔒 Security
- **Release APKs are now properly signed** instead of using the public Android debug key —
`android/key.properties` (gitignored) drives local/CI signing with a documented GitHub Actions
secrets flow (`KEYSTORE_BASE64`, `KEYSTORE_PASSWORD`, `KEY_ALIAS`, `KEY_PASSWORD`); `release.yml`
now cryptographically verifies the built APK's signing certificate before publishing.
- **PIN/password/pattern hashing switched from unsalted SHA-256 to salted PBKDF2-HMAC-SHA256**
(120,000 iterations) — the old scheme meant all 10,000 possible 4-digit PIN hashes could be
precomputed in microseconds, notable given the app explicitly targets rooted devices. Existing
installs upgrade transparently on next successful login, no forced re-setup.
- `generateTestAnomaly` (injects a fake anomaly into the live detection stream) is now compiled
out of release builds instead of always being a reachable MethodChannel endpoint.
- Removed sensitive per-packet debug logging (source/dest IPs, domains, full packet maps) that ran
unconditionally on every packet, including in release builds, on a tool whose entire purpose is
capturing potentially sensitive traffic metadata.

### ⚡ Performance
- `RuleEngine`'s per-rule packet history used `ArrayList.removeAt(0)` for eviction — an O(n) shift
on every packet, across up to 10 rules simultaneously, once the 1000-entry cap filled. Switched
to `ArrayDeque` for O(1) eviction.
- `SignatureDatabase` (18 signatures) and `RuleEngine` (10 rules) each independently re-decoded the
same packet payload bytes to a string per check — now decoded once per packet and shared.
`PayloadAnalyzer`'s file-carving/keyword scans are now skipped entirely for known-encrypted
traffic (TLS/QUIC/HTTPS), where they could only ever produce noise, not signal.
- Packet hex-dump formatting used to format the *entire* payload (up to tens of KB) before
truncating the display string — now sliced to the display bound first.
- `AnomalyDetector`'s entropy-consecutive-hit counters (`highEntropyPacketCount`,
`dnsHighEntropyCount`) were the only trackers never included in the periodic cleanup sweep,
growing by one entry per unique source IP for the life of a capture session — now bounded.

### 🧹 Removed
- ~5,700 lines of confirmed-dead Kotlin (12 files — six abandoned VPN-service implementations, plus
their now-orphaned helpers) and an entire second, never-built native C++ capture tree
(`android/app/src/main/cpp/`), verified dead by inspecting actual build output, not just
cross-referencing symbols.
- Stray, misleading `build.gradle.kts`/`settings.gradle.kts` scaffolding (declared a different,
wrong package name than the live Groovy build files).
- Duplicate `ProtocolStats`/`NetworkMetrics` class definitions in `main.dart` that silently shadowed
the real ones in `models.dart`, plus a dead module-level `MethodChannel` handler
(`initPacketListener`) that was unreachable the moment the main screen mounted, and several
`NativeBridge` methods with no caller.

### 📦 Dependencies & Tooling
- Gradle 8.10.2 → 8.14.3, AGP 8.7.3 → 8.11.2, Kotlin 2.1.0 → 2.2.21, compileSdk 36 → 37,
kotlinx-coroutines 1.7.3 → 1.11.0.
- `fl_chart` 0.70 → 1.2, `flutter_secure_storage` 9 → 11, `local_auth` 2 → 3,
`permission_handler` 11 → 13, `share_plus` 10 → 13 (`Share.shareXFiles` →
`SharePlus.instance.share`), `flutter_lints` 5 → 6 — all breaking API changes at each major
fixed at the call site.

## [1.2.0] - 2026-05-21

### 🔧 Build & Install Fixes
Expand Down
34 changes: 22 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,21 +111,25 @@ Entropy score is displayed as a live badge (`E:x.x`) on each packet in the captu

### PCAP Export

- Standard, Wireshark-compatible libpcap format
- Microsecond timestamp precision
- Standard pcapng format (RFC 7663) with nanosecond-resolution timestamps — natively readable by Wireshark, tcpdump, and tshark
- Saved to `/sdcard/Download/AndroNet/`
- Fully compatible with Wireshark, tcpdump, and tshark
- Size- and duration-based file rotation with automatic pruning of old captures

### Protocol Intelligence

Recognizes **65+ application protocols**, including HTTPS, DNS, SSH, FTP, SMTP, MySQL, PostgreSQL, MongoDB, Redis, SIP, RDP, and VNC.
Recognizes **65+ application protocols**, including HTTPS, DNS, SSH, FTP, SMTP, MySQL, PostgreSQL, MongoDB, Redis, SIP, RDP, and VNC. DNS-over-HTTPS is detected by TLS SNI against known public resolvers, surfacing a common technique for bypassing on-path DNS monitoring.

### Per-App Traffic Attribution

Every TCP/UDP flow is resolved to the installed app that owns it (via `ConnectivityManager.getConnectionOwnerUid`) and shown inline on each packet — visibility a desktop packet analyzer can't offer, since it isn't running on the device whose traffic it's inspecting.

### Interface

- **16 predefined, color-coded filters:** ALL, HTTP, HTTPS, DNS, TCP, UDP, TLS, QUIC, ICMP, DHCP, ARP, SSH, FTP, SMTP, POP3, IMAP
- Live packet counts per filter (e.g. `HTTP (25)`)
- Filters adapt to observed traffic and match on both transport and application layers
- Enriched DPI detail inline — HTTP URLs/methods/status, DNS queries, TLS SNI, DHCP message types
- Enriched DPI detail inline — HTTP URLs/methods/status, DNS queries, TLS SNI, DHCP message types, resolved owning app
- Light, dark, and system-follow themes (Settings → Theme)

---

Expand Down Expand Up @@ -221,19 +225,20 @@ scripts/tag-release.ps1 1.0.0
|---|---|
| Flutter | 3.44.0 stable |
| Dart SDK | 3.8.1+ |
| Android Gradle Plugin | 8.7.3 |
| Gradle Wrapper | 8.10.2 |
| Kotlin | 2.1.0 |
| NDK | 28.2.13433566 |
| compileSdk / targetSdk | 36 |
| Android Gradle Plugin | 8.11.2 |
| Gradle Wrapper | 8.14.3 |
| Kotlin | 2.2.21 |
| NDK | 28.2.13676358 |
| compileSdk | 37 |
| targetSdk | 36 |
| minSdk | 24 (Android 7.0) |

> **16KB page alignment:** Android 15+ (API 35+) devices using 16KB memory pages require native libraries compiled with `-Wl,-z,max-page-size=16384` and packaged with `useLegacyPackaging = false`. Both flags are already applied to all `.so` targets (`zdtun_vpn`, `pcap_writer`, `pcap_capture`).

### Prerequisites

- Flutter SDK 3.44.0+
- Android Studio with NDK 28.2.13433566
- Android Studio with NDK 28.2.13676358
- CMake 3.22.1+
- Kali NetHunter (optional — required only for libpcap mode)

Expand Down Expand Up @@ -359,7 +364,12 @@ Attach `bug-report.zip` when opening a GitHub issue.
- [ ] Kernel-level capture path for rooted devices, reducing capture overhead below current libpcap-mode figures
- [ ] Expanded anomaly-detection benchmarking (labeled traffic dataset, precision/recall reporting)
- [ ] Cross-device portability testing across the NetHunter-supported device matrix
- [ ] Historical session storage and diffing between captures
- [ ] Historical session storage and diffing between captures (sqflite is already a dependency, currently unused for this)
- [ ] In-app packet search across IP/domain/payload/app, beyond the existing protocol filters
- [ ] Custom detection rules authored from the UI (`RuleEngine.addRule`/`SignatureDatabase.addSignature` already support it programmatically; no UI path yet)
- [ ] Threat-intel IP/domain blocklist import, replacing the small hardcoded sample list
- [ ] CSV/JSON packet export alongside PCAP
- [ ] Real traffic-over-time and anomaly-frequency charts (fl_chart is already a dependency, currently underused)

---

Expand Down
Loading
Loading