On-device phishing and malware protection for iOS. Sentinel intercepts DNS queries through a local VPN tunnel, checks every domain against curated threat intelligence feeds (400K+ domains), and blocks malicious connections instantly — all without sending your browsing data to the cloud.
iOS app makes DNS query
→ Local VPN tunnel intercepts it
→ Rust engine checks domain against bloom filter (400K+ threat domains)
→ Match? → Sinkhole response (0.0.0.0) sent back, connection blocked
→ No match? → Query forwarded to real DNS server normally
All processing happens on-device. Zero browsing data leaves your phone.
The app runs as two processes:
- Network Extension — intercepts DNS traffic, runs the Rust threat matching engine, blocks malicious domains with synthesized sinkhole responses
- Main app — security dashboard with VPN control, threat log, domain browser, and feed management
- DNS-level threat blocking — 400K+ known phishing, malware, and C2 domains blocked via bloom filter (~700 KB memory)
- DNS sinkhole — blocked domains get an instant
A 0.0.0.0response instead of a 5-second timeout - Unified security dashboard — VPN toggle, live metrics (domains/packets/queries), threat stats, and recent threats in one view
- Push notifications — local alerts when threats are blocked (rate-limited)
- Per-domain allowlist — one-tap false positive recovery via UserDefaults IPC
- Domain transparency — browse every domain your device connects to, grouped by site, searchable and sortable
- Activity stats — 7-day activity chart, top sites, category breakdown
- Alert dedup — one alert per domain per session, capped at 10K to prevent memory growth
- Feed validation — rejects oversized feeds (>50 MB), HTML error pages, and caps parsing at 1M domains
- ECH downgrade — strips Encrypted Client Hello configs for security visibility (opt-in)
- 100% on-device — no browsing data transmitted to any server, ever
| Feed | Coverage | Update Frequency | License |
|---|---|---|---|
| HaGeZi Pro | ~402K domains | Daily | MIT |
| URLhaus (abuse.ch) | ~20-40K active malware URLs | Daily | CC0 |
Feeds are downloaded by the app on launch, cached in the App Group container, and loaded into the Network Extension's Rust engine on tunnel startup.
sentinel/ iOS app (Xcode project)
sentinel/ Main app target (SwiftUI)
Views/
Threats/ Security dashboard, threat detail, allowlist
Connection/ Connect button, connection view model
Domains/ Domain history browser
Stats/ Activity charts and rankings
Settings/ App configuration, diagnostics
Onboarding/ First-launch permission flow
Services/
ThreatFeedService Downloads and caches threat feeds
ThreatAlertService Local push notifications for blocks
DatabaseReader SQLite queries (domains, alerts, allowlist)
VPNManager NEPacketTunnelProvider lifecycle
Models/
ThreatRecord Threat alert data model
DomainRecord Domain observation data model
SiteRecord Grouped site data model
UserSettings User preferences
PacketTunnelExtension/ Network Extension target
PacketTunnelProvider DNS interception + forwarding + sinkhole blocking
RustBridge Swift wrapper around Rust C FFI
SentinelWidgets/ Lock screen Live Activity widget
Shared/
AppGroupConfig App Group IDs, Darwin notification names
rust/packet_engine/ Rust packet engine (185 tests)
src/
engine.rs Packet processing pipeline + threat integration
threat_feed.rs Bloom filter feed loading (1M domain cap)
threat_matcher.rs Multi-feed matching with allowlist bypass
dns.rs DNS query/response parser
tls.rs TLS ClientHello / SNI extractor
dns_filter.rs ECH downgrade (HTTPS RR stripping)
storage.rs SQLite storage (domains, alerts, allowlist)
lib.rs C FFI entry points (11 functions)
scripts/
build-universal.sh Build Rust for device + simulator
build-rust.sh Build Rust for a specific target
- macOS 14+
- Xcode 15+
- Rust 1.75+ with iOS targets
- Apple Developer account (Organization enrollment required for VPN apps)
- Physical iPhone for testing (Network Extension doesn't work on simulator)
rustup target add aarch64-apple-ios aarch64-apple-ios-sim./scripts/build-universal.shCompiles libpacket_engine.a for both device and simulator, generates the C header via cbindgen, and copies artifacts into the Xcode project.
open sentinel/sentinel.xcodeprojSelect your development team in Signing & Capabilities for all targets:
sentinel(main app)PacketTunnelExtensionSentinelWidgetsExtension
Configure the App Group group.com.jimmykim.sentinel in your Apple Developer portal.
Select your iPhone and hit Cmd+R. The app will:
- Show a 3-step onboarding flow explaining threat protection
- Request VPN permission (local tunnel, not a remote proxy)
- Download threat feeds on first launch
- Start blocking malicious domains when you browse
cd rust/packet_engine
cargo test185 tests covering DNS parsing, TLS SNI extraction, TCP reassembly, ECH downgrade, threat feed loading, bloom filter matching, alert dedup, allowlist bypass, sinkhole response generation, and the full packet processing pipeline.
┌─────────────────────────────────┐
│ Network Extension Process │
│ (50 MB jetsam limit) │
│ │
│ Rust Engine (~3 MB) │
│ + Bloom Filter (~700 KB) │
│ = ~4 MB total │
│ │
│ DNS query → bloom check │
│ → match → sinkhole 0.0.0.0 │
│ → alert to SQLite │
│ → Darwin notify main app │
└──────────────┬──────────────────┘
│ SQLite (WAL) + Darwin Notifications + UserDefaults
┌──────────────▼──────────────────┐
│ Main App Process │
│ │
│ Reads alerts from SQLite │
│ Security dashboard UI │
│ Feed download + caching │
│ Allowlist via UserDefaults │
└─────────────────────────────────┘
Why Rust? The Network Extension has a 50 MB memory limit (15 MB on some devices). Rust's ownership model prevents leaks, and the engine runs at ~4 MB total.
Why bloom filter? A HashSet of 400K domains costs ~29 MB. The bloom filter at 0.1% FP rate costs ~700 KB with zero false negatives. False positives are handled via allowlist.
Why DNS sinkhole? Dropping packets causes a 5-second timeout. Returning A 0.0.0.0 makes the connection fail instantly.
Why UserDefaults for allowlist? Writing to shared SQLite from the main app risks 0xDEAD10CC crashes on iOS suspension. UserDefaults is atomic and cross-process safe.
Why on-device? Cloud DNS resolvers require sending every query to a remote server. Sentinel's local approach means zero browsing data leaves the device.
- Detection lag: 2-7 days (feeds update daily, phishing domains live <24 hours)
- Bloom filter false positive rate: ~0.1% (~5-10 false alerts/day for typical usage)
- AAAA (IPv6) queries don't receive sinkhole responses (connection times out instead)
- iCloud Private Relay traffic is invisible to the tunnel
- No URL-path-level blocking (DNS layer only)
- All packet analysis runs locally in the Network Extension process
- Domain history and threat alerts are stored in a local SQLite database
- Threat feeds are cached locally — no per-query lookups to external services
- The VPN tunnel is local — traffic is observed and forwarded, not proxied
- The only network requests are two anonymous GET requests to download public threat feed files from CDN (jsDelivr) and abuse.ch
- No user data, device identifiers, or browsing history is ever transmitted
- Privacy manifests (
PrivacyInfo.xcprivacy) included for both app and extension targets
MIT