AegisNet 1.1.0
Android is verified on an Android 14 emulator: the tunnel establishes, a blocked
domain answers NXDOMAIN (ping doubleclick.net → unknown host), a normal domain
resolves through the DoH upstream (ping example.com → 172.66.147.243), and the
dashboard shows the engine's own counters. iOS is not verified — the
PacketTunnel target was only just added and none of its Swift has been compiled
on a Mac yet. Treat this release as Android-only.
🚨 Fixed — release blockers found by review
- The Android tunnel could not start at all. The IPv6 ULA was written
fd00:aegis::2, which is not a valid IPv6 literal (g,iandsare not
hex digits), soVpnService.Builder.addAddressthrew beforeestablish()was
ever reached. - DNS failed outright with the default upstream. Routing public resolver IPs
(1.1.1.1, 8.8.8.8, 9.9.9.9, …) into the TUN to stop apps bypassing the filter
also captured the engine's own DoH traffic tohttps://1.1.1.1/dns-query,
where the DNS-only filter dropped it. Every lookup ended in SERVFAIL after a
5s timeout. The routes are removed; doing this properly needs a protected
upstream socket and is tracked in ROADMAP.md. - Quick Settings tile crashed on Android 14+.
startActivityAndCollapse(Intent)
throwsUnsupportedOperationExceptionfor apps targeting API 34, and the app
targets 36. - Quick Settings tile crashed on Android 12+. Tapping it with the app closed
calledstartForegroundServicefrom the background, which is not an exempt
context for a tile click; the resultingForegroundServiceStartNotAllowedException
went uncaught. It now falls back to opening the app. - The service came back as a zombie after being killed.
START_STICKY
redelivers a null intent, which matched no branch, leaving the process alive
with no notification and no tunnel — the routine outcome on MIUI. It now
rebuilds the tunnel, using a bypass list persisted to storage so a process
kill does not silently route the user's excluded apps through the VPN. - The tile reported stale state. It kept showing "ON" after the tunnel went
down, including after MIUI revoked VPN consent.
🌲 Rust Core Engine — DomainTrie Optimization & Custom Hosts
-
DomainTrie prefix tree. Replaced
HashSet<String>domain matching with a
DomainTriethat stores one node per label, so a blocked zone costs one
terminal node instead of one entry per host. Note the behaviour change: the
user denylist now covers subdomains, where it used to match exact hosts only. -
DomainTrie memory fix. The first version gave each node a
HashMap<String, TrieNode>, which measured at 2.5x–7.7x the memory of the
HashSetit replaced — the opposite of the intended effect, because a
domain trie is mostly single-child chains and every one of them paid for a
hash table. Children are now a sortedVec<(Box<str>, TrieNode)>searched by
binary search. Measured over 300k rules with a counting allocator:Rule shape Before After HashSetbaseline2-label, hosts-style 44.4 MB 17.6 MB 17.5 MB 3-label, unique second level 142.2 MB 31.9 MB 18.4 MB Many subdomains under 500 zones 41.5 MB 15.7 MB 17.8 MB This is what made it a correctness issue rather than a tuning one: the iOS
PacketTunnel extension has a hard memory limit in the tens of MB, and the
default blocklists are large enough that the old layout got it killed.
Lookups are ~170 ns, so no speed claim is made either way — the trie's win is
that one rule covers a whole zone. -
Custom DNS Host Overrides (Local DNS Mapping). Added local DNS mapping
support (domain->IP, e.g.myrouter.local->192.168.1.1) directly in
the Rust engine, with C-FFI exportsaegis_add_custom_hostand
aegis_remove_custom_host. Overrides answer A and AAAA with a record of the
matching family and NOERROR/empty otherwise, and travel in the settings
snapshot so the iOS extension honours them too. -
Bounded top-domain statistics. The per-domain hit counters are capped at
2,000 names per direction, evicting the coldest half when full, and the top-5
is selected linearly instead of sorting and cloning the whole table on every
UI poll.
🧹 Removed — numbers the UI invented
Several screens filled empty state with realistic-looking sample data, which is
indistinguishable from a measurement once it is rendered. All of it is gone; the
screens now say they have no data yet.
- Dashboard opened at 1,420 queries / 385 blocked / 27.1% / 55.1 MB on a fresh
install, and the query log came pre-seeded with three fabricated entries. - Analytics fell back to a hand-written top-blocked list (
doubleclick.net
×142,api.github.com×320, …) whenever the engine had counted nothing. - "Hourly Query Distribution" drew seven hardcoded bars that never changed.
There is no hourly bucketing to plot, so the chart now shows the query-rate
history that does exist, retitled to match. - The dashboard's "Traffic & Latency" curve was seeded with
[15, 28, 42, 35, 50, 48, 62], drawing convincing traffic on a device that
had never resolved anything, next to a hardcoded "14 ms (Ultra Fast)" that
was never measured — the engine does not time its lookups. The chart starts
empty and the badge reports the sample count instead. - Settings had an "Export / Import Configuration" button that built a JSON
string, discarded it, and reported "Config exported successfully: N bytes".
Removed — the Backup & Restore section does the real thing.
🔢 Versioning
- The settings footer hardcoded
v1.0.0with nothing keeping it honest. It now
readskAppVersion, and a test asserts that constant matchespubspec.yaml
— the release workflow runs the test before it builds.
💻 Desktop Scaffolding & Desktop DNS Proxy
- Multi-Platform Desktop Shell. Added native desktop scaffolding
(windows/,macos/,linux/) so the app compiles and runs as a native
desktop application. - Desktop DNS Resolver Integration. Connected
DesktopDnsProxythrough
AegisBridgeandVpnProviderfor desktop platforms.
⏰ Quiet Hours Schedule Blocking & Custom Subscriptions
- Scheduled Parental Control. Added
setScheduleand quiet hours
evaluation (default 22:00 - 06:00) to automatically enforce Adult category
filters during quiet hours. - Custom Hosts UI Tab. Added a dedicated Local DNS Hosts tab in
RulesScreenfor managing local DNS host overrides with real-time UI mapping. - Status Filter Chips in Logs. Added
ALL LOGS,BLOCKED, andALLOWED
filter chips inLogsScreenfor fast real-time query log inspection. - Full Configuration Backup. Extended
ConfigSyncServiceJSON
export/import to backup custom hosts, schedule settings, and custom filter
sources. - Expanded Test Coverage. Rust tests: 16 → 33, Flutter unit & widget
tests: 7 → 28.
🔴 Android — Real DNS filtering (verified on device)
-
Native TUN → Rust → device pipeline wired end-to-end.
AegisVpnService
now reads each IPv4 packet off the TUN, hands it to the Rust engine
(aegis_process_ip_packetvia a JNI bridge), and writes synthesized DNS
replies back. Previously the read loop discarded every packet. -
DNS-only tunnel routing. Only the virtual DNS server (
10.0.0.3/32) is
routed into the TUN. Non-DNS traffic and the engine's own upstream lookups
stay on the real network, so nothing loops and noVpnService.protect()is
required. -
Graceful native-absent fallback / crash fix.
System.loadLibraryran in
the service's static initializer and crashed the whole app on VPN start when
libaegis_core.sowas not bundled. It is now loaded defensively behind a
nativeAvailableflag; the app falls back to simulation instead of crashing. -
Reproducible native build. A best-effort Gradle
preBuildtask compiles
the Rust engine withcargo-ndkintojniLibsfor all ABIs. It runs only
whencargo-ndkis onPATH, so toolchain-less machines still build.Verified on an Android 34 emulator:
doubleclick.netand
graph.facebook.comresolve to a null address (blocked) whilegithub.com
resolves to its real IP via DoH.
🟠 Rust core — Correctness fixes
- DNS cache correctness. The cache is now keyed by
(domain, qtype)and
stamps the current request's transaction id onto cached replies. Previously it
returned a stale transaction id and ignored the record type, so clients
rejected cached answers. - SafeSearch precision. Rewrites now match an exact allow-list of search
hostnames instead of a substring.mail.google.com/drive.google.comand
look-alikes such asgoogle.com.attacker.netare no longer hijacked. - Whitelist covers subdomains + removal wired. Whitelisting
facebook.com
now also allowsgraph.facebook.com. Newaegis_remove_whitelist/
aegis_remove_blacklistFFI exports are wired through Dart and the provider,
so removing an entry in the UI actually reaches the engine. - DNS-over-HTTPS upstream (RFC 8484). Cleartext UDP:53 forwarding was
replaced with a DoHPOST(application/dns-message). The endpoint is an
IP literal (https://1.1.1.1/dns-query) on purpose — resolving a hostname
here would recurse into our own captured resolver and deadlock. - New
packetmodule. Minimal IPv4/UDP parsing, reply reassembly and RFC
1071 checksum, fully unit-tested. - JNI bridge (
nativeProcessPacket) for the Android service. - Dependency cleanup. Removed unused
tokio,aho-corasick,regex,
parking_lot. - Rust tests: 6 → 16, no compiler warnings.
🟡 Flutter / Dart
- Fallback matching fixed. The pure-Dart fallback matched domains by
substring (adnxs.comblockedmyadnxs.com). It now matches a domain or its
subdomains only. - Removal wiring + single source of truth.
removeWhitelist/
removeBlacklistadded; the FFI stub kept in sync with the native bindings;
seed allow/deny lists are pushed into the engine on startup so the UI and the
engine agree. - Deprecation sweep.
withOpacity→withValues,activeColor→
activeThumbColor.flutter analyze: 20 issues → 0. - Removed the unused
flutter_rust_bridgedependency. - Dart tests: 3 → 5.
🍎 iOS — Partial (needs macOS/Xcode to finish)
- Fixed invalid
fnkeyword (Rust syntax) →func; the packet tunnel did not
compile before. - The
readPacketsloop now runs each packet throughaegis_process_ip_packet
and writes replies back instead of discarding them; the engine C ABI is
declared in the bridging header. - Full integration code prepared (assembled on a Mac — see
ios/IOS_SETUP.md): thePacketTunnelProvidermoved to a
dedicatedios/PacketTunnel/extension folder with DNS-only routing that
mirrors Android;VpnManager.swiftwiring thecom.aegisnet/vpnchannel to
NETunnelProviderManager; app + extension entitlements; the extension
Info.plist; andios/build_rust_ios.shto produceAegisCore.xcframework. - Rust core verified to cross-compile for iOS (device + simulator) on a
macOS CI runner (.github/workflows/ios.yml). - Still requires macOS + a paid Apple Developer account: creating the
Network Extension target, capabilities/provisioning, and linking the
framework. Runtime testing needs a real iPhone.
🧹 Housekeeping
- Confirmed the prebuilt
aegis_core.dllis git-ignored and untracked.
🩹 iOS shell fixes (static review — not yet compiled on a Mac)
- The VPN channel was never registered.
AppDelegatewired
com.aegisnet/vpnfromdidFinishLaunchingWithOptions:via
window?.rootViewController, but this project uses the UIScene lifecycle
(SceneDelegate+UIApplicationSceneManifest), where no scene has connected
at launch andwindowis still nil. Registration moved to
didInitializeImplicitFlutterEngine, which runs before any scene connects. - The UI no longer claims protection it does not have.
AegisBridge.startVpn
treatedMissingPluginExceptionas success, so the missing registration above
surfaced as a green "protected" dashboard with no tunnel running. Missing
handlers now count as failure on Android/iOS and remain a no-op only on
web/desktop, where there is no native side by design. Covered by a regression
test; Dart tests: 5 → 7. ios/Runner.xcworkspace/was gitignored, which broke every fresh iOS build.
The whole directory was excluded in.gitignore, so a clean checkout had no
workspace:flutter build iosaborted with "An error occurred when adding
Swift Package Manager integration: Exception: Xcode workspace not found", and
anyone cloning on a Mac had to openRunner.xcodeprojdirectly — where the
generated Swift package cannot be resolved, producing "Missing package product
'FlutterGeneratedPluginSwiftPackage'". Both reported errors came from this one
line. The workspace is part of the Flutter template and is now committed.- CI stops swallowing iOS build failures. The
flutter build iosstep is no
longercontinue-on-error(the job stays non-blocking), the pointless
--no-enable-swift-package-managerstep is gone —Runner.xcodeprojis a
Swift Package Manager project — and the generated
ios/Flutter/ephemeral/Packagesstate is dumped before the build to diagnose
"Missing package product 'FlutterGeneratedPluginSwiftPackage'".
Known follow-ups
- iOS: complete the Network Extension target, entitlements and static-lib
linkage listed above. - iOS: the extension gets its own copy of the engine. Runner and PacketTunnel
are separate processes and the Rust engine keeps state in process-local
globals, so rules pushed from Dart never reach the code doing the filtering.
Needs a shared-state design over the already-declared App Group — see
ios/IOS_SETUP.md. - iOS: link
AegisCore.xcframeworkinto Runner too, not just PacketTunnel;
DynamicLibrary.process()finds nothing otherwise and the app silently uses
placeholder rules and statistics. - Android: the upstream DoH call is synchronous on the tunnel thread; a
thread-pool/async path would improve throughput under load.
What's Changed
- Release gate: develop → main (mở sớm để CI chạy liên tục) by @vannt-dev in #12
- fix(ci): pin the release tag to the built commit by @vannt-dev in #13
- Feature/analytics and enhancements by @vannt-dev in #14
- Feature/custom hosts schedule desktop by @vannt-dev in #15
- fix: release blockers, invented metrics, and Android 12 / MIUI hardening (v1.1.0) by @vannt-dev in #17
- ci(release): send the CHANGELOG to testers instead of a merge commit subject by @vannt-dev in #18
- ci(release): check the CHANGELOG entry before spending 7 minutes building by @vannt-dev in #19
- release: v1.1.0 — custom hosts, schedule, desktop shell, and the fixes that make them work by @vannt-dev in #16
Full Changelog: v1.0.0...v1.1.0