Inspiration

Our project is heavily inspired by LuLu, a firewall built by Objective-See which automatically block DNS requests from newly installed apps. Our idea is, that instead of asking the user for approval in order to allow access to the said DNS, we build our own agentic AI framework to call tools — ultimately reaching a verdict on wether the link is safe or not.

What it does

Our script controls MacOS's built-in firewall, called pf to route all network traffic to a proxy made in Python using psutil. We cross-reference the network request with known outgoing connections to match the request with the PiD, then routes connections based on wether it passes our tests or not.

How we built it

We implemented two routes for determining wether the network request is safe or not — the FAST route and the SLOW route. The FAST route includes:

  • Entropy value calculations — We compute a composite randomness score for each domain to detect algorithmically generated domains (DGAs) commonly used by malware. The core uses Shannon entropy:

$$H = -\sum_{i} \frac{c_i}{n} \log_2 \frac{c_i}{n}$$

where $c_i$ is the count of each unique character and $n$ is the total character count. This raw entropy is then normalized against the theoretical maximum $\log_2(\min(36, n))$ and dampened by label length to avoid penalizing long but legitimate domains like CDN hostnames. A digit-ratio boost is applied when the label already appears high-entropy, catching hex-heavy C2 domains. The three signals are blended into a single 1.0–5.0 score:

$$S = 0.1 \cdot L_{\text{norm}} + 0.45 \cdot (E_{\text{norm}} \cdot D_{\text{len}}) + 0.45 \cdot R_{\text{digit}}$$

$$\text{entropyValue} = \min!\big(5,\; \max!\big(1,\; 1 + 4S\big)\big)$$

Any domain exceeding the configurable tolerance (default 4.0) is blocked instantly without further analysis.

  • Known domain blacklisting — We aggregate over 100,000 domains across curated blocklists covering malware, ads, gambling, fake news, pornography, and social media tracking. Incoming SNI hostnames are checked against this set in O(1) time using a hash set. Domains on the allowlist bypass all checks entirely.

The SLOW route includes:

  • Trustscore calculation — A weighted composite of seven normalized signals, each scaled to 0.0–1.0 (1.0 = full trust):

$$T = 100\Big(0.25 \cdot A_{\text{age}} + 0.20 \cdot E_{\text{entropy}} + 0.20 \cdot B_{\text{brand}} + 0.15 \cdot D_{\text{tld}} + 0.10 \cdot K_{\text{keywords}} + 0.05 \cdot S_{\text{subdomain}} + 0.05 \cdot L_{\text{length}}\Big)$$

  • Domain age ($A$): RDAP lookup for the registration date; younger domains score lower, capped at 5 years.
  • Entropy ($E$): Reuses the fast-route entropy score, inversely normalized.
  • Brand impersonation ($B$): Levenshtein distance against 16 top phishing targets (Microsoft, Google, PayPal, etc.). One edit away = instant zero. Substring embedding (e.g. paypal-update.tk) is also caught.
  • TLD trust tier ($D$): .gov/.edu/.mil get full trust; known-abuse TLDs like .tk, .xyz, .top get 0.1.
  • Suspicious keywords ($K$): Counts tokens like "tracking", "login", "verify", "billing" in domain labels.
  • Subdomain depth ($S$): Penalizes excessive nesting (e.g. secure.login.account.example.tk).
  • Domain length ($L$): Longer combined labels reduce trust.

Domains scoring below the tolerance threshold (default 65) are escalated to the AI judge.

  • Agentic maliciousness verdict using gpt-oss-20b — An agentic loop backed by a local LM Studio instance running gpt-oss-20b. The AI is given the application name and target domain, then autonomously calls tools across up to 12 rounds to gather evidence before returning allow, block, or unsure. The four tools available to the agent are:
    • WHOIS — registrar, creation date, and status via the system whois client
    • DNS_RESOLVE — A, AAAA, CNAME, MX, NS, and TXT records via Google Public DNS over HTTPS
    • VIRUSTOTAL — domain reputation report via the VirusTotal v3 API
    • FETCH — retrieves the HTTP body of the domain's landing page

Verdicts are cached per domain and protected by per-domain locks so parallel connections to the same host don't each trigger a full LM session. Domains the AI allows are automatically appended to the allowlist for future fast-path resolution.

We also built a real-time web dashboard using Flask and Server-Sent Events that visualizes every intercepted connection, blocked domain, and AI judgement as it happens — including a live terminal feed of the proxy's output, connection metrics, and full configuration management.

Challenges we ran into

In order to achieve high-speed, low-level network access we would need to go through the loophole of getting approved for Apple's ES or NE framework, which we do not have time to do as it involves complex code in SWIFT or C. Instead, we used pf to route all outgoing requests to a proxy, which optimally slows down your internet speed by half as it analyzes connections.

Because pf routes traffic at the kernel level, we had to craft a rule that excludes root-owned traffic from redirection (user != root), otherwise our own proxy's outbound connections would loop back into itself infinitely. Figuring out this one-line pf rule took significant debugging.

Extracting the target domain without decrypting TLS was another hurdle. We parse the raw TLS ClientHello byte-by-byte in Python to extract the SNI (Server Name Indication) extension, which gives us the hostname in plaintext before encryption begins — no MITM certificate or TLS termination required.

Matching a network connection back to the originating application required cross-referencing the source port from the intercepted socket against the OS's open TCP connection table via psutil. This is a race condition — by the time our proxy receives the connection, the originating process may have already closed its socket or been reassigned a new PID — so we handle graceful fallbacks for dead or inaccessible processes.

The agentic AI loop introduced its own class of challenges: local models frequently ignore instructions to reply with a single token, wrapping their answer in reasoning blocks, markdown formatting, or multi-paragraph explanations. We had to build a robust parser that strips <think> blocks, scans lines in reverse, and falls back to regex extraction to reliably pull a tool name or verdict out of arbitrarily verbose model output.

Accomplishments that we're proud of

  • Building a fully functional, transparent network firewall for macOS entirely in Python in under 24 hours — no kernel extensions, no code signing, no Apple developer entitlements.
  • The two-tier architecture (fast heuristic path + slow AI path) means the vast majority of connections are resolved in microseconds, and only genuinely ambiguous domains pay the cost of an LM inference round.
  • A working agentic AI loop that autonomously gathers WHOIS, DNS, VirusTotal, and HTTP data across multiple rounds before making a judgement — not a single-shot prompt, but a real tool-using agent.
  • An adaptive allowlist that learns from AI verdicts: once a domain is judged safe, it never needs to be analyzed again.
  • A real-time dashboard that makes the entire firewall's decision-making process visible and auditable.

What we learned

  • How macOS's pf packet filter works at the kernel level — writing rdr/pass rules, managing IP forwarding, and the subtleties of user != root to avoid routing loops.
  • How TLS ClientHello packets are structured at the byte level, and how SNI extraction lets you inspect encrypted traffic destinations without breaking encryption.
  • The gap between "LLM can answer questions" and "LLM can reliably act as an agent" — local models need extensive output parsing, retry logic, and structured prompting to behave as deterministic tool-callers.
  • Weighted scoring systems require careful normalization: raw Shannon entropy alone over-penalizes long legitimate CDN domains, so length dampening and per-component weighting were essential to reduce false positives.
  • Thread safety in Python is subtle when multiple network connections arrive simultaneously — we implemented per-domain locking and verdict caching to prevent redundant AI inference and race conditions.

What's next for Resolagent

  • Network Extension migration — Replace the pf + proxy architecture with a proper macOS Network Extension to operate as a first-class system firewall with App Store distribution potential.
  • On-device model — Replace the LM Studio dependency with a quantized on-device model so the AI judge works offline with zero setup.
  • Feedback loop — Let users override AI verdicts from the dashboard and use those corrections to fine-tune the trustscore weights and the model's system prompt over time.
  • Per-app policies — Use the PID-to-app resolution to enforce app-specific rulesets (e.g. allow Slack to reach its own CDN, but block unknown apps from contacting the same domain).
  • Cross-platform support — Port the proxy layer to Linux (iptables/nftables) and Windows (WFP) to extend Resolagent beyond macOS.
Share this project:

Updates