Build an AI Website Firewall on macOS

Use machine learning to detect and block unapproved AI sites with macOS pf.

Introduction

30 Second Summary

Companies around the world are grappling with how to manage employee access to the growing number of AI tools appearing online. From chatbots to image generators, each new service raises questions about data privacy and acceptable use.

In this project, you will build a local AI-powered website firewall that uses machine learning to detect AI-related domains and automatically block unapproved ones using your Mac's built-in packet filter.

What You'll Build

An enterprise-style AI website policy enforcement system that combines a trained domain classifier with macOS's pf (Packet Filter) to control access to AI websites.

By the end of this project, you'll have:

  • A trained scikit-learn classifier that identifies AI-related domains by analyzing URL structure features like keyword presence, domain length, and entropy.
  • A JSON-based whitelist of approved AI websites paired with a policy engine that produces allow, block, or non-AI verdicts for any domain.
  • A Python CLI tool that resolves unapproved AI domains to IP addresses and generates real macOS pf firewall rules to block network traffic.
  • Secret Mission: A real-time DNS monitor that watches for new AI domain access attempts and auto-classifies them live.

Are there any prerequisites?

No prior experience with firewalls or machine learning is needed. This guide includes setup instructions for all required tools, and you only need a Mac to get started.

Before We Start

Before We Start...

Set Up Your Development Environment

Before you can train an AI model or write firewall rules, you need the right tools installed on your Mac.

In this step, you will install VS Code for editing your Python files, verify that Python 3 is ready, and create an isolated project environment with all the packages your firewall tool needs.

In this step, get ready to:
  • Install VS Code and open it.
  • Verify Python 3 and create a virtual environment.
  • Install the required Python packages.
Install and open VS Code

VS Code is the code editor you will use throughout this project to write and manage your Python files.

✔️ I already have VS Code installed

Great, you are all set.

  • Press Cmd+Space to open Spotlight.
  • Type Visual Studio Code and press Enter to open it.

ⓧ I need to install VS Code

  • Open Safari and go to https://code.visualstudio.com.
  • Click Download for macOS.
  • Open the downloaded .dmg file.
  • Drag Visual Studio Code.app to the Applications folder.
  • Press Cmd+Space to open Spotlight.
  • Type Visual Studio Code and press Enter to open it.

macOS privacy prompt

macOS may ask if VS Code can access certain folders. Click Allow to let it access your project files.

Verify Python 3 and create your project

macOS includes Python 3 through its Command Line Tools. You need to confirm it is available before creating your project folder.

  • Open Terminal by pressing Cmd+Space, typing Terminal, and pressing Enter.
  • Check your Python 3 version by running this command:
python3 --version

✔️ I see a version number

You should see output like Python 3.x.x. Any Python 3 version works for this project.

ⓧ Command not found or a prompt appears

If macOS prompts you to install Command Line Tools, click Install in the dialog and wait for it to finish. Then run the command again.

If no dialog appears and you still see an error, install the Command Line Tools manually by running:

xcode-select --install

Follow the on-screen prompts, wait for the installation to complete, then run python3 --version again to confirm.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir ai-firewall
cd ai-firewall
  • Confirm the folder exists by running:
ls ~/Desktop

You should see ai-firewall listed in the output.

Now you will create a virtual environment to keep this project's packages separate from the rest of your system.

  • Create and activate a virtual environment by running these commands:
python3 -m venv venv
source venv/bin/activate

What is a virtual environment?

A virtual environment is an isolated space where Python packages are installed without affecting the rest of your system. It keeps your project's dependencies contained in one folder.

When the virtual environment is active, you will see (venv) at the start of your terminal prompt.

You should now see (venv) at the beginning of your terminal prompt. This confirms the virtual environment is active.

Don't see (venv) in your prompt?

Make sure you ran source venv/bin/activate (not just the python3 -m venv venv part). The first command creates the environment. The second command activates it.

If you closed your terminal and reopened it, you need to navigate back to the project folder and rerun the activate command.

Install the required Python packages

Your AI firewall needs three key libraries: scikit-learn for machine learning, tldextract for parsing domain names, and pandas for handling data.

  • Install the three packages by running this command:
pip install scikit-learn==1.9.0 tldextract==5.3.1 pandas

What do these packages do?

scikit-learn (1.9.0) provides the machine learning algorithms you will use to classify domains. tldextract (5.3.1) breaks apart URLs into their component parts like subdomain, domain, and suffix.

pandas handles the structured data (rows and columns) that your training dataset needs.

  • Verify all packages installed correctly by running:
python3 -c "import sklearn; import tldextract; print('Ready!')"

You should see Ready! printed with no errors.

Seeing an ImportError?

Make sure (venv) still appears in your prompt. If it does not, reactivate the environment with source venv/bin/activate and run the pip install command again.

If you see a network error, check your internet connection and retry the install command.

  • Open the ai-firewall folder in VS Code by running:
code .

If the code command is not found

In VS Code, press Cmd+Shift+P to open the Command Palette. Type shell command and select Shell Command: Install 'code' command in PATH. Then restart your terminal and run code . again.

Your development environment is ready. Next up, you will build the AI domain classifier that powers the firewall's intelligence.

Build the AI Domain Classifier

Your development environment is ready. Now it's time to build the brain of your firewall: a machine learning model that can look at any domain name and decide whether it belongs to an AI service.

The classifier works by analyzing structural features of a URL. Things like keyword presence, top-level domain type, and character patterns give strong signals about whether a domain is AI-related.

In this step, get ready to:
  • Write a feature extraction function that derives signals from domain URLs.
  • Build a labeled training dataset of AI and non-AI domains.
  • Train and evaluate a RandomForest model that classifies domains.
Set up the classifier file with feature extraction

Before the model can learn anything, it needs a way to turn raw domain names into numbers. This process is called feature extraction. You will create a Python file that imports your scikit-learn tools, defines what keywords to look for, and extracts numeric features from any domain.

  • In VS Code, create a new file called train_classifier.py inside your ai-firewall folder.
  • Add the imports and keyword list by pasting this code:
import json
import math
import joblib
import pandas as pd
import tldextract
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report

AI_KEYWORDS = [
    "ai", "gpt", "llm", "chat", "model", "neural", "ml",
    "deep", "learn", "copilot", "gemini", "anthropic", "openai",
    "midjourney", "diffusion", "transformer", "bert", "claude"
]

What does this code do?

The imports bring in your machine learning toolkit. joblib saves trained models to disk. pandas structures data into tables. tldextract parses domain names into their component parts.

AI_KEYWORDS is the list of terms associated with AI services. The classifier uses this to count how many AI-related keywords appear in a domain name.

  • Save train_classifier.py.
  • Confirm the file appears in the VS Code Explorer sidebar under ai-firewall.

Now add the two functions that turn a raw domain string into a set of numeric features. The first calculates the randomness (entropy) of the domain's characters. The second extracts all the features the model will use.

  • Below the AI_KEYWORDS list, add the entropy helper and the main feature extraction function:


def shannon_entropy(text):
    if not text:
        return 0.0
    freq = {}
    for ch in text:
        freq[ch] = freq.get(ch, 0) + 1
    length = len(text)
    return -sum((count / length) * math.log2(count / length) for count in freq.values())


def extract_features(domain):
    ext = tldextract.extract(domain)
    registered_domain = ext.domain
    subdomain = ext.subdomain
    suffix = ext.suffix
    full = f"{subdomain}.{registered_domain}.{suffix}" if subdomain else f"{registered_domain}.{suffix}"

    keyword_count = sum(1 for kw in AI_KEYWORDS if kw in full.lower())
    has_ai_tld = 1 if suffix in ["ai", "ml"] else 0

    return {
        "domain_length": len(full),
        "registered_domain_length": len(registered_domain),
        "subdomain_depth": subdomain.count(".") + 1 if subdomain else 0,
        "has_ai_tld": has_ai_tld,
        "keyword_count": keyword_count,
        "num_hyphens": full.count("-"),
        "num_dots": full.count("."),
        "digit_ratio": sum(c.isdigit() for c in registered_domain) / max(len(registered_domain), 1),
        "entropy": shannon_entropy(registered_domain),
    }

What does this code do?

shannon_entropy measures the randomness of characters in the domain. Random-looking domains (like auto-generated ones) have higher entropy than natural words.

extract_features uses tldextract to split a domain into its parts (subdomain, registered domain, suffix), then calculates nine numeric features. These include how many AI keywords appear, whether the TLD is .ai or .ml, and structural patterns like hyphens and dots.

  • Save train_classifier.py.
  • You won't see output yet. The next chunk builds the training data that uses these functions.

Seeing a red underline in VS Code?

If VS Code shows import errors, make sure your Python interpreter is set to the virtual environment. Press Cmd+Shift+P (macOS), type Python: Select Interpreter, and choose the one inside venv.

help me fix Python import errors in VS Code

Build the training dataset

A machine learning model learns patterns from labeled examples. You will give it a list of known AI domains and known non-AI domains so it can learn what distinguishes them.

  • Below the extract_features function, add the training data builder:


def build_training_data():
    ai_domains = [
        "claude.ai", "chatgpt.com", "openai.com", "huggingface.co",
        "midjourney.com", "stability.ai", "anthropic.com", "perplexity.ai",
        "copilot.microsoft.com", "gemini.google.com", "deepmind.google",
        "cohere.ai", "replicate.com", "together.ai", "mistral.ai",
        "deepseek.com", "ai21.com", "jasper.ai", "writesonic.com",
        "copy.ai", "runway.ml", "synthesia.io", "descript.com",
        "notion.ai", "grammarly.com", "otter.ai", "fireflies.ai",
        "lmsys.org", "ollama.com", "groq.com",
        "newai-assistant.ai", "gpt-helper.com", "smart-llm-tool.io",
        "deeplearn-platform.ai", "neural-chat.co", "ml-service.ai",
        "ai-copilot-pro.com", "transformer-hub.ai", "chat-model.io",
        "diffusion-art.ai",
    ]

    non_ai_domains = [
        "google.com", "amazon.com", "reddit.com", "wikipedia.org",
        "youtube.com", "twitter.com", "facebook.com", "instagram.com",
        "netflix.com", "spotify.com", "apple.com", "microsoft.com",
        "github.com", "stackoverflow.com", "nytimes.com", "bbc.com",
        "weather.com", "espn.com", "walmart.com", "target.com",
        "linkedin.com", "pinterest.com", "tumblr.com", "twitch.tv",
        "zoom.us", "slack.com", "dropbox.com", "adobe.com",
        "salesforce.com", "oracle.com", "ibm.com", "cisco.com",
        "shopify.com", "stripe.com", "paypal.com", "ebay.com",
        "airbnb.com", "uber.com", "lyft.com", "doordash.com",
    ]

    data = []
    for domain in ai_domains:
        features = extract_features(domain)
        features["label"] = 1
        data.append(features)
    for domain in non_ai_domains:
        features = extract_features(domain)
        features["label"] = 0
        data.append(features)

    return pd.DataFrame(data)

What does this code do?

build_training_data creates labeled examples for the model. It runs extract_features on 40 known AI domains (labeled 1) and 40 known non-AI domains (labeled 0), then returns everything as a pandas DataFrame.

The dataset includes both real services (like claude.ai, chatgpt.com) and synthetic domains (like gpt-helper.com) to give the classifier enough variety to generalize.

  • Save train_classifier.py.
  • You won't see output yet. The next function uses this dataset to train the model.
Train and evaluate the model

With the dataset and features ready, you can now train the classifier. It will split the data into training and test sets, learn the patterns, and report how accurately it distinguishes AI from non-AI domains.

  • Below the build_training_data function, add the training function and entry point:


def train_model():
    print("Building training data...")
    df = build_training_data()

    feature_cols = [c for c in df.columns if c != "label"]
    X = df[feature_cols]
    y = df["label"]

    X_train, X_test, y_train, y_test = train_test_split(
        X, y, test_size=0.2, random_state=42, stratify=y
    )

    print("Training RandomForest classifier...")
    clf = RandomForestClassifier(
        n_estimators=100, max_depth=10, class_weight="balanced", random_state=42
    )
    clf.fit(X_train, y_train)

    y_pred = clf.predict(X_test)
    print("\nClassification Report:")
    print(classification_report(y_test, y_pred, target_names=["non-ai", "ai"]))

    print("Feature importances:")
    for name, importance in sorted(
        zip(feature_cols, clf.feature_importances_), key=lambda x: x[1], reverse=True
    ):
        print(f"  {name}: {importance:.3f}")

    joblib.dump(clf, "ai_classifier.joblib")
    joblib.dump(feature_cols, "feature_columns.joblib")
    print("\nModel saved to ai_classifier.joblib")


if __name__ == "__main__":
    train_model()

What does this code do?

train_model orchestrates the full training pipeline. It builds the dataset, splits it 80/20 for training and testing, then trains a RandomForestClassifier with 100 decision trees.

After training, it prints a classification report showing precision, recall, and F1 scores for both classes. It also ranks which features mattered most. Finally, it saves the trained model and feature column names using joblib so other scripts can load the model without retraining.

  • Save train_classifier.py.
  • Run the training script in your terminal by running this command:
python3 train_classifier.py

You should see output showing the classification report with precision, recall, and F1 scores for both "non-ai" and "ai" classes, followed by a ranked list of feature importances, and a final confirmation that the model was saved.

Seeing a ModuleNotFoundError?

Make sure your virtual environment is active. Your terminal prompt should show (venv) at the beginning. If it does not, activate it by running:

source venv/bin/activate

help me fix ModuleNotFoundError when running train_classifier.py

  • Confirm that ai_classifier.joblib and feature_columns.joblib now exist in your project folder by running:
ls *.joblib

You should see ai_classifier.joblib and feature_columns.joblib listed in the output.

Why RandomForest for domain classification?

Domain names are short strings with limited text. Traditional text-classification approaches (like TF-IDF) struggle because there is not enough text to find meaningful word-frequency patterns. A RandomForest trained on hand-crafted structural features performs well because it can learn simple decision rules like "if the TLD is .ai and the keyword count is above 1, it is probably an AI domain."

✔️ Awesome, I've got everything!

Great. Double check you have saved train_classifier.py and that both .joblib files exist in your project folder.

ⓧ I'd like to double check the full code

Here is the complete train_classifier.py file. Compare yours against it:

import json
import math
import joblib
import pandas as pd
import tldextract
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report

AI_KEYWORDS = [
    "ai", "gpt", "llm", "chat", "model", "neural", "ml",
    "deep", "learn", "copilot", "gemini", "anthropic", "openai",
    "midjourney", "diffusion", "transformer", "bert", "claude"
]


def shannon_entropy(text):
    if not text:
        return 0.0
    freq = {}
    for ch in text:
        freq[ch] = freq.get(ch, 0) + 1
    length = len(text)
    return -sum((count / length) * math.log2(count / length) for count in freq.values())


def extract_features(domain):
    ext = tldextract.extract(domain)
    registered_domain = ext.domain
    subdomain = ext.subdomain
    suffix = ext.suffix
    full = f"{subdomain}.{registered_domain}.{suffix}" if subdomain else f"{registered_domain}.{suffix}"

    keyword_count = sum(1 for kw in AI_KEYWORDS if kw in full.lower())
    has_ai_tld = 1 if suffix in ["ai", "ml"] else 0

    return {
        "domain_length": len(full),
        "registered_domain_length": len(registered_domain),
        "subdomain_depth": subdomain.count(".") + 1 if subdomain else 0,
        "has_ai_tld": has_ai_tld,
        "keyword_count": keyword_count,
        "num_hyphens": full.count("-"),
        "num_dots": full.count("."),
        "digit_ratio": sum(c.isdigit() for c in registered_domain) / max(len(registered_domain), 1),
        "entropy": shannon_entropy(registered_domain),
    }


def build_training_data():
    ai_domains = [
        "claude.ai", "chatgpt.com", "openai.com", "huggingface.co",
        "midjourney.com", "stability.ai", "anthropic.com", "perplexity.ai",
        "copilot.microsoft.com", "gemini.google.com", "deepmind.google",
        "cohere.ai", "replicate.com", "together.ai", "mistral.ai",
        "deepseek.com", "ai21.com", "jasper.ai", "writesonic.com",
        "copy.ai", "runway.ml", "synthesia.io", "descript.com",
        "notion.ai", "grammarly.com", "otter.ai", "fireflies.ai",
        "lmsys.org", "ollama.com", "groq.com",
        "newai-assistant.ai", "gpt-helper.com", "smart-llm-tool.io",
        "deeplearn-platform.ai", "neural-chat.co", "ml-service.ai",
        "ai-copilot-pro.com", "transformer-hub.ai", "chat-model.io",
        "diffusion-art.ai",
    ]

    non_ai_domains = [
        "google.com", "amazon.com", "reddit.com", "wikipedia.org",
        "youtube.com", "twitter.com", "facebook.com", "instagram.com",
        "netflix.com", "spotify.com", "apple.com", "microsoft.com",
        "github.com", "stackoverflow.com", "nytimes.com", "bbc.com",
        "weather.com", "espn.com", "walmart.com", "target.com",
        "linkedin.com", "pinterest.com", "tumblr.com", "twitch.tv",
        "zoom.us", "slack.com", "dropbox.com", "adobe.com",
        "salesforce.com", "oracle.com", "ibm.com", "cisco.com",
        "shopify.com", "stripe.com", "paypal.com", "ebay.com",
        "airbnb.com", "uber.com", "lyft.com", "doordash.com",
    ]

    data = []
    for domain in ai_domains:
        features = extract_features(domain)
        features["label"] = 1
        data.append(features)
    for domain in non_ai_domains:
        features = extract_features(domain)
        features["label"] = 0
        data.append(features)

    return pd.DataFrame(data)


def train_model():
    print("Building training data...")
    df = build_training_data()

    feature_cols = [c for c in df.columns if c != "label"]
    X = df[feature_cols]
    y = df["label"]

    X_train, X_test, y_train, y_test = train_test_split(
        X, y, test_size=0.2, random_state=42, stratify=y
    )

    print("Training RandomForest classifier...")
    clf = RandomForestClassifier(
        n_estimators=100, max_depth=10, class_weight="balanced", random_state=42
    )
    clf.fit(X_train, y_train)

    y_pred = clf.predict(X_test)
    print("\nClassification Report:")
    print(classification_report(y_test, y_pred, target_names=["non-ai", "ai"]))

    print("Feature importances:")
    for name, importance in sorted(
        zip(feature_cols, clf.feature_importances_), key=lambda x: x[1], reverse=True
    ):
        print(f"  {name}: {importance:.3f}")

    joblib.dump(clf, "ai_classifier.joblib")
    joblib.dump(feature_cols, "feature_columns.joblib")
    print("\nModel saved to ai_classifier.joblib")


if __name__ == "__main__":
    train_model()

Your AI classifier is trained and saved. Next up, you will build the whitelist and policy engine that decides which AI domains are approved and which should be blocked.

Create the Whitelist and Policy Engine

Your classifier can now tell the difference between AI-related domains and regular websites. The next question is: what do you actually do with that information?

In this step, you will build a policy engine that combines a whitelist of approved AI sites with your classifier's predictions. Domains on the whitelist pass through freely. Unknown domains get classified, and any that look like unapproved AI services get flagged for blocking.

In this step, get ready to:
  • Create a JSON whitelist of approved AI domains.
  • Write a policy engine that returns allow, block, or skip verdicts.
  • Add DNS resolution to translate blocked domains into IP addresses.
Create the whitelist configuration file

Enterprise AI policies work by maintaining a list of approved services. Any AI tool not on the list gets blocked automatically. This whitelisting approach means only explicitly trusted tools can operate on the network.

  • In VS Code, create a new file called config.json inside your ai-firewall folder.
  • Add the following whitelist configuration:
{
  "whitelist": [
    {
      "domain": "claude.ai",
      "approved_date": "2026-06-09",
      "category": "AI Assistant"
    },
    {
      "domain": "chatgpt.com",
      "approved_date": "2026-06-09",
      "category": "AI Assistant"
    },
    {
      "domain": "platform.openai.com",
      "approved_date": "2026-06-09",
      "category": "AI Platform"
    },
    {
      "domain": "huggingface.co",
      "approved_date": "2026-06-09",
      "category": "AI/ML Models"
    },
    {
      "domain": "github.com",
      "approved_date": "2026-06-09",
      "category": "Development"
    }
  ],
  "blocked": [],
  "settings": {
    "interface": "en0",
    "auto_enforce": false
  }
}

What does this configuration do?

The whitelist array lists every AI domain your organization has approved. Each entry includes the domain name, the date it was approved, and a category for reporting.

The blocked array starts empty and will fill up as the policy engine discovers unapproved AI sites. The settings section tells the firewall which network interface to monitor (en0 is macOS's default Wi-Fi adapter).

  • Save config.json.

Why whitelisting instead of blacklisting?

A blacklist tries to block known-bad domains. The problem is that new AI tools launch constantly. A whitelist flips the model: only explicitly approved tools are allowed, and everything else is blocked by default.

This is the same zero-trust philosophy used by enterprise security teams. If it is not on the list, it does not get access.

Write the policy engine infrastructure

The policy engine lives in a new file that will grow into your full firewall tool. It loads the whitelist, loads the trained classifier, and provides functions to check whether a domain is approved.

  • Create a new file called ai_firewall.py inside your ai-firewall folder.
  • Add the imports and configuration constants by pasting this code:
import argparse
import json
import os
import socket
import subprocess
import sys
import joblib
from train_classifier import extract_features

# File paths and firewall anchor name
CONFIG_FILE = "config.json"
ANCHOR_NAME = "ai-firewall"
ANCHOR_FILE = f"/etc/pf.anchors/{ANCHOR_NAME}"

# Terminal color codes for readable output
GREEN = "\033[92m"
RED = "\033[91m"
YELLOW = "\033[93m"
GREY = "\033[90m"
RESET = "\033[0m"
BOLD = "\033[1m"

What does this code do?

  • The imports bring in everything the firewall tool will need: JSON for config, socket for DNS lookups, subprocess for running firewall commands, and joblib for loading the trained model.
  • ANCHOR_FILE points to where macOS stores pf firewall rule files. The color codes make terminal output easier to scan at a glance.
  • Add the configuration and model loading functions below the constants:

def load_config():
    with open(CONFIG_FILE, "r") as f:
        return json.load(f)


def save_config(config):
    with open(CONFIG_FILE, "w") as f:
        json.dump(config, f, indent=2)


def load_classifier():
    clf = joblib.load("ai_classifier.joblib")
    feature_cols = joblib.load("feature_columns.joblib")
    return clf, feature_cols

What do these functions do?

  • load_config() reads the whitelist and settings from config.json into a Python dictionary.
  • save_config() writes changes back to the file (for example, when a domain gets blocked).
  • load_classifier() loads both the trained model and the feature column names you saved in the previous step.
  • Add the whitelist check and classification functions below load_classifier():

def is_whitelisted(domain, config):
    whitelist_domains = [entry["domain"] for entry in config["whitelist"]]
    for wd in whitelist_domains:
        # Check exact match or subdomain match
        if domain == wd or domain.endswith("." + wd):
            return True
    return False


def classify_domain(domain, clf, feature_cols):
    features = extract_features(domain)
    feature_values = [features[col] for col in feature_cols]
    # Get both the prediction and the confidence probability
    prediction = clf.predict([feature_values])[0]
    probability = clf.predict_proba([feature_values])[0]
    return prediction, probability

What does this code do?

  • is_whitelisted() checks if a domain (or any of its parent domains) appears in your approved list. This means api.claude.ai would still match claude.ai in the whitelist.
  • classify_domain() extracts features from the domain (using the function from train_classifier.py) and runs them through the trained model. It returns both a prediction (1 = AI, 0 = non-AI) and a confidence score.
  • Save ai_firewall.py.

Seeing an import error?

If VS Code shows a red underline on from train_classifier import extract_features, make sure both ai_firewall.py and train_classifier.py are in the same ai-firewall folder.

help me fix an import error in my ai_firewall.py file

Add DNS resolution and the policy decision function

Knowing that a domain is an unapproved AI site is only half the picture. To actually block traffic at the firewall level, you need the domain's IP address. Domain names are human-readable labels, but firewalls operate on IP addresses.

  • In ai_firewall.py, add the DNS resolution function below classify_domain():

def resolve_domain(domain):
    try:
        # Look up IPv4 addresses for this domain
        results = socket.getaddrinfo(domain, None, socket.AF_INET)
        ips = list(set(r[4][0] for r in results))
        return ips
    except socket.gaierror:
        return []

What does this code do?

socket.getaddrinfo() performs a DNS lookup to translate a domain name into IP addresses. The socket.AF_INET argument restricts results to IPv4 addresses only.

If the domain cannot be resolved (for example, it does not exist), the function catches the socket.gaierror exception and returns an empty list instead of crashing.

  • Add the main policy decision function below resolve_domain():

def check_domain(domain, clf, feature_cols, config):
    # First check: is this domain on the approved whitelist?
    if is_whitelisted(domain, config):
        return "allowed", 1.0, "Whitelisted"

    # Second check: run the ML classifier on unknown domains
    prediction, probability = classify_domain(domain, clf, feature_cols)

    if prediction == 1:
        # Classifier says this is an AI site, but it's not approved
        ai_prob = probability[1]
        return "blocked", ai_prob, "AI site not in whitelist"
    else:
        # Not an AI site at all - no action needed
        return "non-ai", probability[0], "Not an AI website"

What does check_domain do?

This is the heart of your policy engine. It follows a two-stage decision process:

  • Stage 1: Check the whitelist. If the domain is approved, allow it immediately with full confidence.
  • Stage 2: If not whitelisted, run the classifier. If it detects an AI site, return "blocked". If it is a regular website, return "non-ai" (no action needed).
  • Save ai_firewall.py.
  • Test the policy engine by running this command in your terminal:
python3 -c "
from ai_firewall import load_config, load_classifier, check_domain
config = load_config()
clf, cols = load_classifier()
print(check_domain('claude.ai', clf, cols, config))
print(check_domain('some-ai-tool.com', clf, cols, config))
print(check_domain('weather.com', clf, cols, config))
"

You should see three tuples printed. claude.ai returns allowed (it is on the whitelist). some-ai-tool.com returns blocked (the classifier detects it as AI). weather.com returns non-ai (not an AI website).

Getting unexpected classifications?

If some-ai-tool.com shows as non-ai, your classifier may have trained differently. Try testing with perplexity.ai instead. Domains with .ai TLDs and AI keywords in them will produce the strongest "blocked" signals.

help me debug my policy engine classifications

✔️ Awesome, I've got everything!

Great. Double check you have saved both config.json and ai_firewall.py.

ⓧ I'd like to double check the full code

Here is the complete ai_firewall.py so far:

import argparse
import json
import os
import socket
import subprocess
import sys
import joblib
from train_classifier import extract_features

CONFIG_FILE = "config.json"
ANCHOR_NAME = "ai-firewall"
ANCHOR_FILE = f"/etc/pf.anchors/{ANCHOR_NAME}"

GREEN = "\033[92m"
RED = "\033[91m"
YELLOW = "\033[93m"
GREY = "\033[90m"
RESET = "\033[0m"
BOLD = "\033[1m"


def load_config():
    with open(CONFIG_FILE, "r") as f:
        return json.load(f)


def save_config(config):
    with open(CONFIG_FILE, "w") as f:
        json.dump(config, f, indent=2)


def load_classifier():
    clf = joblib.load("ai_classifier.joblib")
    feature_cols = joblib.load("feature_columns.joblib")
    return clf, feature_cols


def is_whitelisted(domain, config):
    whitelist_domains = [entry["domain"] for entry in config["whitelist"]]
    for wd in whitelist_domains:
        if domain == wd or domain.endswith("." + wd):
            return True
    return False


def classify_domain(domain, clf, feature_cols):
    features = extract_features(domain)
    feature_values = [features[col] for col in feature_cols]
    prediction = clf.predict([feature_values])[0]
    probability = clf.predict_proba([feature_values])[0]
    return prediction, probability


def resolve_domain(domain):
    try:
        results = socket.getaddrinfo(domain, None, socket.AF_INET)
        ips = list(set(r[4][0] for r in results))
        return ips
    except socket.gaierror:
        return []


def check_domain(domain, clf, feature_cols, config):
    if is_whitelisted(domain, config):
        return "allowed", 1.0, "Whitelisted"

    prediction, probability = classify_domain(domain, clf, feature_cols)

    if prediction == 1:
        ai_prob = probability[1]
        return "blocked", ai_prob, "AI site not in whitelist"
    else:
        return "non-ai", probability[0], "Not an AI website"

And your complete config.json:

{
  "whitelist": [
    {
      "domain": "claude.ai",
      "approved_date": "2026-06-09",
      "category": "AI Assistant"
    },
    {
      "domain": "chatgpt.com",
      "approved_date": "2026-06-09",
      "category": "AI Assistant"
    },
    {
      "domain": "platform.openai.com",
      "approved_date": "2026-06-09",
      "category": "AI Platform"
    },
    {
      "domain": "huggingface.co",
      "approved_date": "2026-06-09",
      "category": "AI/ML Models"
    },
    {
      "domain": "github.com",
      "approved_date": "2026-06-09",
      "category": "Development"
    }
  ],
  "blocked": [],
  "settings": {
    "interface": "en0",
    "auto_enforce": false
  }
}

Your policy engine can now classify any domain and decide whether to allow, block, or ignore it. Next up, you will turn those "blocked" verdicts into actual macOS firewall rules that stop traffic at the network level.

Generate and Apply macOS pf Firewall Rules

Your policy engine can now classify domains and resolve their IP addresses. The missing piece is enforcement.

In this step, you will write the functions that translate blocked IPs into real macOS pf (Packet Filter) rules and load them into the kernel. You will also build a disable command to flush those rules when you are done testing.

In this step, get ready to:
  • Write a function that generates pf table and block rules from a list of IPs.
  • Write a function that writes rules to an anchor file and loads them with pfctl.
  • Build a disable command that flushes all rules from the anchor.
Write the pf rule generation function

macOS includes a built-in packet filter called pf. It uses plain-text rule files to decide which network traffic to allow or block.

Your function will take a list of blocked IP addresses and produce a rule string that tells pf to silently drop all HTTP and HTTPS traffic to those IPs.

  • In ai_firewall.py, find the check_domain function that ends with return "non-ai", probability[0], "Not an AI website".
  • Add the following function below it:
def generate_pf_rules(blocked_ips, interface):
    if not blocked_ips:
        return "# No blocked IPs\n"

    ip_list = " ".join(blocked_ips)
    rules = f"table <blocked_ai> persist {{ {ip_list} }}\n"
    rules += f"block drop quick on {interface} proto tcp from any to <blocked_ai> port {{80, 443}}\n"
    return rules

What does this code do?

This function builds a pf rule string from a list of IP addresses. It has two parts:

  • table <blocked_ai> persist creates a named IP table that survives rule reloads. The blocked IPs are listed inside the curly braces.
  • block drop quick silently drops matching packets without sending a response. The quick keyword makes pf stop evaluating further rules on a match. Traffic to ports 80 (HTTP) and 443 (HTTPS) is targeted.

Why use an anchor instead of editing the main pf.conf?

An anchor is an isolated rule container within pf. Your rules live in the ai-firewall anchor, completely separate from any system rules.

This means flushing your anchor never touches other firewall rules on your Mac. It is the safest approach for a learning project.

  • Save ai_firewall.py.

You will not see a visible change yet. The next function uses this output to actually write the rules to disk.

Write the firewall application function

Generating rule text is only half the job. The rules need to be written to an anchor file on disk and then loaded into the kernel using pfctl.

  • In ai_firewall.py, add the following function below generate_pf_rules:
def apply_firewall_rules(rules):
    try:
        with open(ANCHOR_FILE, "w") as f:
            f.write(rules)
        subprocess.run(
            ["pfctl", "-a", ANCHOR_NAME, "-f", ANCHOR_FILE],
            check=True, capture_output=True
        )
        subprocess.run(["pfctl", "-e"], capture_output=True)
        return True
    except (PermissionError, subprocess.CalledProcessError) as e:
        print(f"{RED}Error applying rules (need sudo): {e}{RESET}")
        return False

What does this code do?

  • It writes the rule text to /etc/pf.anchors/ai-firewall. This is the standard directory for anchor files on macOS.
  • pfctl -a ai-firewall -f /etc/pf.anchors/ai-firewall loads those rules into the ai-firewall anchor.
  • pfctl -e enables the packet filter if it is not already running.
  • The whole operation requires sudo because writing to /etc/ and modifying kernel rules are privileged actions.
  • Save ai_firewall.py.

You will not see a change yet. The next function gives you a way to undo these rules.

Build the disable command

You need a safe way to remove all firewall rules your tool has applied. This command flushes everything from the ai-firewall anchor without affecting the rest of the system.

  • In ai_firewall.py, add the following function below apply_firewall_rules:
def cmd_disable(args):
    result = subprocess.run(
        ["pfctl", "-a", ANCHOR_NAME, "-F", "all"],
        capture_output=True, text=True
    )
    if result.returncode == 0:
        print(f"{GREEN}Firewall rules flushed.{RESET}")
    else:
        print(f"{RED}Error flushing rules (need sudo): {result.stderr}{RESET}")

What does this code do?

pfctl -a ai-firewall -F all flushes all rules loaded in the ai-firewall anchor. This removes every block rule your tool has applied while leaving all other system firewall rules untouched.

The function prints a green confirmation on success or a red error message reminding you to use sudo if permission is denied.

  • Save ai_firewall.py.
  • Test your rule generation by running this command in your terminal:
python3 -c "from ai_firewall import generate_pf_rules; print(generate_pf_rules(['1.2.3.4', '5.6.7.8'], 'en0'))"

You should see output that includes a table <blocked_ai> line listing the two test IPs and a block drop quick rule targeting ports 80 and 443.

Seeing a ModuleNotFoundError?

Make sure your virtual environment is still active. You should see (venv) at the beginning of your terminal prompt. If not, re-activate it by running source venv/bin/activate from inside your ai-firewall folder.

Also check that you saved ai_firewall.py after adding all three functions.

Help me debug a ModuleNotFoundError when importing from ai_firewall.py.

  • Now test the full flow by applying and then disabling rules. Run this command:
sudo python3 -c "
from ai_firewall import generate_pf_rules, apply_firewall_rules
rules = generate_pf_rules(['1.2.3.4'], 'en0')
print(rules)
apply_firewall_rules(rules)
"

Why sudo?

Writing to /etc/pf.anchors/ and loading kernel firewall rules both require root privileges. You will be prompted for your Mac password.

  • Verify the rule was loaded by running:
sudo pfctl -a ai-firewall -s rules

You should see output showing a block rule targeting port 80 and 443 for the test IP.

  • Clean up by flushing the test rules:
sudo pfctl -a ai-firewall -F all

You should see no output (the rules are silently flushed). Run sudo pfctl -a ai-firewall -s rules again to confirm it returns nothing.

Getting a permission denied error?

Make sure you are using sudo before each command. Without it, macOS blocks access to pf configuration.

If your Mac asks for a password, type your login password. The characters will not appear as you type.

Help me fix a permission denied error when running pfctl commands on macOS.

✔️ Awesome, I've got everything!

Great. Double check you have saved ai_firewall.py with all three new functions.

ⓧ I'd like to double check the full code

Here is the full ai_firewall.py file as it should look at the end of this step:

import argparse
import json
import os
import socket
import subprocess
import sys
import joblib
from train_classifier import extract_features

CONFIG_FILE = "config.json"
ANCHOR_NAME = "ai-firewall"
ANCHOR_FILE = f"/etc/pf.anchors/{ANCHOR_NAME}"

GREEN = "\033[92m"
RED = "\033[91m"
YELLOW = "\033[93m"
GREY = "\033[90m"
RESET = "\033[0m"
BOLD = "\033[1m"


def load_config():
    with open(CONFIG_FILE, "r") as f:
        return json.load(f)


def save_config(config):
    with open(CONFIG_FILE, "w") as f:
        json.dump(config, f, indent=2)


def load_classifier():
    clf = joblib.load("ai_classifier.joblib")
    feature_cols = joblib.load("feature_columns.joblib")
    return clf, feature_cols


def is_whitelisted(domain, config):
    whitelist_domains = [entry["domain"] for entry in config["whitelist"]]
    for wd in whitelist_domains:
        if domain == wd or domain.endswith("." + wd):
            return True
    return False


def classify_domain(domain, clf, feature_cols):
    features = extract_features(domain)
    feature_values = [features[col] for col in feature_cols]
    prediction = clf.predict([feature_values])[0]
    probability = clf.predict_proba([feature_values])[0]
    return prediction, probability


def resolve_domain(domain):
    try:
        results = socket.getaddrinfo(domain, None, socket.AF_INET)
        ips = list(set(r[4][0] for r in results))
        return ips
    except socket.gaierror:
        return []


def check_domain(domain, clf, feature_cols, config):
    if is_whitelisted(domain, config):
        return "allowed", 1.0, "Whitelisted"

    prediction, probability = classify_domain(domain, clf, feature_cols)

    if prediction == 1:
        ai_prob = probability[1]
        return "blocked", ai_prob, "AI site not in whitelist"
    else:
        return "non-ai", probability[0], "Not an AI website"


def generate_pf_rules(blocked_ips, interface):
    if not blocked_ips:
        return "# No blocked IPs\n"

    ip_list = " ".join(blocked_ips)
    rules = f"table <blocked_ai> persist {{ {ip_list} }}\n"
    rules += f"block drop quick on {interface} proto tcp from any to <blocked_ai> port {{80, 443}}\n"
    return rules


def apply_firewall_rules(rules):
    try:
        with open(ANCHOR_FILE, "w") as f:
            f.write(rules)
        subprocess.run(
            ["pfctl", "-a", ANCHOR_NAME, "-f", ANCHOR_FILE],
            check=True, capture_output=True
        )
        subprocess.run(["pfctl", "-e"], capture_output=True)
        return True
    except (PermissionError, subprocess.CalledProcessError) as e:
        print(f"{RED}Error applying rules (need sudo): {e}{RESET}")
        return False


def cmd_disable(args):
    result = subprocess.run(
        ["pfctl", "-a", ANCHOR_NAME, "-F", "all"],
        capture_output=True, text=True
    )
    if result.returncode == 0:
        print(f"{GREEN}Firewall rules flushed.{RESET}")
    else:
        print(f"{RED}Error flushing rules (need sudo): {result.stderr}{RESET}")

Your firewall functions are working. Next up, you will build the CLI dashboard that ties the classifier, policy engine, and firewall together into a single command-line tool.

Build the CLI Dashboard

You have a classifier that detects AI domains, a whitelist that defines approved sites, and a policy engine that produces verdicts. The missing piece is a way for a human to interact with all of this from the terminal.

In this step, you will build a CLI dashboard using Python's argparse module. The dashboard exposes commands to check individual domains, view firewall status, manage the whitelist, and trigger enforcement scans with colored output.

In this step, get ready to:
  • Create command handlers for check, status, and whitelist commands.
  • Build an enforce command that scans domains and generates a summary table.
  • Wire up argparse so the tool runs as a complete CLI application.
Create the check command handler

The check command is the most common interaction point. It takes a single domain, runs it through the policy engine, and prints a color-coded verdict with confidence score and resolved IPs.

  • In ai_firewall.py, find the cmd_disable function.
  • Add the following cmd_check function directly above cmd_disable:
def cmd_check(args):
    config = load_config()
    clf, feature_cols = load_classifier()
    domain = args.domain.lower().strip()

    verdict, confidence, reason = check_domain(domain, clf, feature_cols, config)

    if verdict == "allowed":
        color = GREEN
        icon = "[PASS]"
    elif verdict == "blocked":
        color = RED
        icon = "[BLOCK]"
    else:
        color = GREY
        icon = "[SKIP]"

    print(f"\n{color}{BOLD}{icon}{RESET} {domain}")
    print(f"  Verdict:    {color}{verdict.upper()}{RESET}")
    print(f"  Confidence: {confidence:.1%}")
    print(f"  Reason:     {reason}")

    if verdict == "blocked":
        ips = resolve_domain(domain)
        if ips:
            print(f"  Resolved:   {', '.join(ips)}")
        print(f"\n  {YELLOW}This domain would be blocked by the firewall.{RESET}")

What does this code do?

This handler loads the classifier and config on each invocation, then calls the policy engine's check_domain() function you built in the previous step.

  • The color-coded icons ([PASS], [BLOCK], [SKIP]) give an instant visual signal in the terminal.
  • For blocked domains, it resolves the IP addresses so you can see exactly what would be firewalled.
  • Save ai_firewall.py.

You will not see a change yet. The next chunks add the remaining commands and the argparse entry point that makes this function callable from the terminal.

Add the status and whitelist commands

The status command gives a bird's-eye view of the firewall state. The whitelist command lets you approve new domains on the fly without editing JSON manually.

  • In ai_firewall.py, add the following cmd_status function directly below cmd_check:
def cmd_status(args):
    config = load_config()

    print(f"\n{BOLD}AI Firewall Status{RESET}")
    print("=" * 40)

    print(f"\n{GREEN}Whitelisted ({len(config['whitelist'])}):{RESET}")
    for entry in config["whitelist"]:
        print(f"  + {entry['domain']} ({entry['category']})")

    print(f"\n{RED}Blocked ({len(config['blocked'])}):{RESET}")
    for entry in config["blocked"]:
        print(f"  x {entry['domain']} -> {', '.join(entry.get('ips', []))}")

    print(f"\n{BOLD}Firewall Rules:{RESET}")
    result = subprocess.run(
        ["pfctl", "-a", ANCHOR_NAME, "-s", "rules"],
        capture_output=True, text=True
    )
    if result.stdout.strip():
        print(f"  {result.stdout.strip()}")
    else:
        print(f"  {GREY}No active rules{RESET}")

What does this code do?

  • It prints all whitelisted domains (green) and blocked domains (red) from the config file.
  • It queries the live pf anchor with pfctl -a ai-firewall -s rules to show which rules are actually loaded in the kernel.
  • Now add the cmd_whitelist_add function directly below cmd_status:
def cmd_whitelist_add(args):
    config = load_config()
    domain = args.domain.lower().strip()

    for entry in config["whitelist"]:
        if entry["domain"] == domain:
            print(f"{YELLOW}{domain} is already whitelisted.{RESET}")
            return

    config["whitelist"].append({
        "domain": domain,
        "approved_date": "2026-06-09",
        "category": "User Approved"
    })

    config["blocked"] = [b for b in config["blocked"] if b["domain"] != domain]

    save_config(config)
    print(f"{GREEN}Added {domain} to whitelist.{RESET}")

What does this code do?

  • It checks for duplicates first. If the domain is already approved, it warns and exits early.
  • When adding a new domain, it also removes that domain from the blocked list. This ensures a domain cannot be simultaneously blocked and whitelisted.
  • Save ai_firewall.py.

You will not see a change yet. The next chunk adds the enforce command and the entry point that makes everything runnable.

Build the enforce command and wire up the CLI

The enforce command is where everything converges. It scans a list of known AI domains, classifies each one, resolves blocked domains to IPs, generates pf rules, and prints a summary table.

  • In ai_firewall.py, add the following cmd_enforce function directly below cmd_whitelist_add (and above cmd_disable):
def cmd_enforce(args):
    config = load_config()
    clf, feature_cols = load_classifier()

    test_domains = [
        "perplexity.ai", "copy.ai", "jasper.ai", "writesonic.com",
        "runway.ml", "synthesia.io", "midjourney.com", "stability.ai",
    ]

    blocked_ips = []
    new_blocked = []

    print(f"\n{BOLD}Scanning domains...{RESET}\n")
    for domain in test_domains:
        verdict, confidence, reason = check_domain(domain, clf, feature_cols, config)
        if verdict == "blocked":
            ips = resolve_domain(domain)
            if ips:
                blocked_ips.extend(ips)
                new_blocked.append({"domain": domain, "ips": ips})
            print(f"  {RED}[BLOCK]{RESET} {domain} -> {', '.join(ips) if ips else 'unresolved'}")
        elif verdict == "allowed":
            print(f"  {GREEN}[ALLOW]{RESET} {domain}")
        else:
            print(f"  {GREY}[SKIP]{RESET}  {domain}")

    config["blocked"] = new_blocked
    save_config(config)

    if blocked_ips:
        interface = config["settings"]["interface"]
        rules = generate_pf_rules(list(set(blocked_ips)), interface)
        print(f"\n{BOLD}Generated pf rules:{RESET}")
        print(f"  {rules.strip()}")

        if os.geteuid() == 0:
            success = apply_firewall_rules(rules)
            if success:
                print(f"\n{GREEN}Firewall rules applied successfully.{RESET}")
        else:
            print(f"\n{YELLOW}Run with sudo to apply firewall rules:{RESET}")
            print(f"  sudo python3 ai_firewall.py enforce")
    else:
        print(f"\n{GREEN}No domains to block.{RESET}")

    print(f"\n{BOLD}Summary:{RESET}")
    print(f"  Scanned:  {len(test_domains)}")
    print(f"  Blocked:  {len(new_blocked)}")
    print(f"  Allowed:  {sum(1 for d in test_domains if is_whitelisted(d, config))}")

What does this code do?

  • It loops through a hardcoded list of known AI domains and runs each through the policy engine.
  • Blocked domains get their IPs resolved and collected. The function then calls generate_pf_rules() to build the firewall rules.
  • The os.geteuid() == 0 check detects whether the script is running as root. If not, it prints the sudo command the user needs instead of failing silently.
  • The summary table at the end shows total scanned, blocked, and allowed counts.
  • Save ai_firewall.py.

You will not see a change yet. The final piece is the main() function that registers all commands with argparse.

  • Add the following main() function and entry point at the very bottom of ai_firewall.py, below cmd_disable:
def main():
    parser = argparse.ArgumentParser(
        description="AI Website Firewall - Block unapproved AI sites"
    )
    subparsers = parser.add_subparsers(dest="command", help="Available commands")

    # Register the check command
    check_parser = subparsers.add_parser("check", help="Check a domain")
    check_parser.add_argument("domain", help="Domain to classify")
    check_parser.set_defaults(func=cmd_check)

    # Register the status command
    status_parser = subparsers.add_parser("status", help="Show firewall status")
    status_parser.set_defaults(func=cmd_status)

    # Register the whitelist command with a nested subcommand
    whitelist_parser = subparsers.add_parser("whitelist", help="Manage whitelist")
    whitelist_sub = whitelist_parser.add_subparsers(dest="whitelist_action")
    add_parser = whitelist_sub.add_parser("add", help="Add domain to whitelist")
    add_parser.add_argument("domain", help="Domain to whitelist")
    add_parser.set_defaults(func=cmd_whitelist_add)

    # Register the enforce command
    enforce_parser = subparsers.add_parser("enforce", help="Scan and apply blocks")
    enforce_parser.set_defaults(func=cmd_enforce)

    # Register the disable command
    disable_parser = subparsers.add_parser("disable", help="Disable firewall rules")
    disable_parser.set_defaults(func=cmd_disable)

    args = parser.parse_args()
    if hasattr(args, "func"):
        args.func(args)
    else:
        parser.print_help()


if __name__ == "__main__":
    main()

What does this code do?

  • argparse parses the command-line arguments and routes each subcommand to its handler function via set_defaults(func=...).
  • The whitelist command uses nested subparsers so you can run whitelist add <domain> as a two-level command.
  • If no subcommand is given, parser.print_help() shows usage instructions.
  • Save ai_firewall.py.
  • Test the check command by running:
python3 ai_firewall.py check perplexity.ai

You should see a red [BLOCK] verdict with a confidence percentage, resolved IPs, and a yellow warning that the domain would be blocked.

  • Now test the status command by running:
python3 ai_firewall.py status

You should see a list of whitelisted domains in green, an empty blocked list, and a "No active rules" message under Firewall Rules.

  • Test adding a domain to the whitelist by running:
python3 ai_firewall.py whitelist add perplexity.ai
  • Verify the whitelist change took effect by checking the domain again:
python3 ai_firewall.py check perplexity.ai

This time you should see a green [PASS] verdict with "Whitelisted" as the reason. The domain is now approved.

Seeing a ModuleNotFoundError?

  • Make sure your virtual environment is active. You should see (venv) at the start of your terminal prompt.
  • Confirm you are running the command from inside the ai-firewall folder where train_classifier.py and ai_classifier.joblib live.
  • If you see an error about ai_classifier.joblib not found, re-run python3 train_classifier.py to regenerate it.

Help me debug my AI firewall CLI

✔️ Awesome, I've got everything!

Great. Double check you have saved ai_firewall.py and all four commands are working.

ⓧ I'd like to double check the full code

import argparse
import json
import os
import socket
import subprocess
import sys
import joblib
from train_classifier import extract_features

CONFIG_FILE = "config.json"
ANCHOR_NAME = "ai-firewall"
ANCHOR_FILE = f"/etc/pf.anchors/{ANCHOR_NAME}"

GREEN = "\033[92m"
RED = "\033[91m"
YELLOW = "\033[93m"
GREY = "\033[90m"
RESET = "\033[0m"
BOLD = "\033[1m"


def load_config():
    with open(CONFIG_FILE, "r") as f:
        return json.load(f)


def save_config(config):
    with open(CONFIG_FILE, "w") as f:
        json.dump(config, f, indent=2)


def load_classifier():
    clf = joblib.load("ai_classifier.joblib")
    feature_cols = joblib.load("feature_columns.joblib")
    return clf, feature_cols


def is_whitelisted(domain, config):
    whitelist_domains = [entry["domain"] for entry in config["whitelist"]]
    for wd in whitelist_domains:
        if domain == wd or domain.endswith("." + wd):
            return True
    return False


def classify_domain(domain, clf, feature_cols):
    features = extract_features(domain)
    feature_values = [features[col] for col in feature_cols]
    prediction = clf.predict([feature_values])[0]
    probability = clf.predict_proba([feature_values])[0]
    return prediction, probability


def resolve_domain(domain):
    try:
        results = socket.getaddrinfo(domain, None, socket.AF_INET)
        ips = list(set(r[4][0] for r in results))
        return ips
    except socket.gaierror:
        return []


def check_domain(domain, clf, feature_cols, config):
    if is_whitelisted(domain, config):
        return "allowed", 1.0, "Whitelisted"

    prediction, probability = classify_domain(domain, clf, feature_cols)

    if prediction == 1:
        ai_prob = probability[1]
        return "blocked", ai_prob, "AI site not in whitelist"
    else:
        return "non-ai", probability[0], "Not an AI website"


def generate_pf_rules(blocked_ips, interface):
    if not blocked_ips:
        return "# No blocked IPs\n"

    ip_list = " ".join(blocked_ips)
    rules = f"table <blocked_ai> persist {{ {ip_list} }}\n"
    rules += f"block drop quick on {interface} proto tcp from any to <blocked_ai> port {{80, 443}}\n"
    return rules


def apply_firewall_rules(rules):
    try:
        with open(ANCHOR_FILE, "w") as f:
            f.write(rules)
        subprocess.run(
            ["pfctl", "-a", ANCHOR_NAME, "-f", ANCHOR_FILE],
            check=True, capture_output=True
        )
        subprocess.run(["pfctl", "-e"], capture_output=True)
        return True
    except (PermissionError, subprocess.CalledProcessError) as e:
        print(f"{RED}Error applying rules (need sudo): {e}{RESET}")
        return False


def cmd_check(args):
    config = load_config()
    clf, feature_cols = load_classifier()
    domain = args.domain.lower().strip()

    verdict, confidence, reason = check_domain(domain, clf, feature_cols, config)

    if verdict == "allowed":
        color = GREEN
        icon = "[PASS]"
    elif verdict == "blocked":
        color = RED
        icon = "[BLOCK]"
    else:
        color = GREY
        icon = "[SKIP]"

    print(f"\n{color}{BOLD}{icon}{RESET} {domain}")
    print(f"  Verdict:    {color}{verdict.upper()}{RESET}")
    print(f"  Confidence: {confidence:.1%}")
    print(f"  Reason:     {reason}")

    if verdict == "blocked":
        ips = resolve_domain(domain)
        if ips:
            print(f"  Resolved:   {', '.join(ips)}")
        print(f"\n  {YELLOW}This domain would be blocked by the firewall.{RESET}")


def cmd_status(args):
    config = load_config()

    print(f"\n{BOLD}AI Firewall Status{RESET}")
    print("=" * 40)

    print(f"\n{GREEN}Whitelisted ({len(config['whitelist'])}):{RESET}")
    for entry in config["whitelist"]:
        print(f"  + {entry['domain']} ({entry['category']})")

    print(f"\n{RED}Blocked ({len(config['blocked'])}):{RESET}")
    for entry in config["blocked"]:
        print(f"  x {entry['domain']} -> {', '.join(entry.get('ips', []))}")

    print(f"\n{BOLD}Firewall Rules:{RESET}")
    result = subprocess.run(
        ["pfctl", "-a", ANCHOR_NAME, "-s", "rules"],
        capture_output=True, text=True
    )
    if result.stdout.strip():
        print(f"  {result.stdout.strip()}")
    else:
        print(f"  {GREY}No active rules{RESET}")


def cmd_whitelist_add(args):
    config = load_config()
    domain = args.domain.lower().strip()

    for entry in config["whitelist"]:
        if entry["domain"] == domain:
            print(f"{YELLOW}{domain} is already whitelisted.{RESET}")
            return

    config["whitelist"].append({
        "domain": domain,
        "approved_date": "2026-06-09",
        "category": "User Approved"
    })

    config["blocked"] = [b for b in config["blocked"] if b["domain"] != domain]

    save_config(config)
    print(f"{GREEN}Added {domain} to whitelist.{RESET}")


def cmd_enforce(args):
    config = load_config()
    clf, feature_cols = load_classifier()

    test_domains = [
        "perplexity.ai", "copy.ai", "jasper.ai", "writesonic.com",
        "runway.ml", "synthesia.io", "midjourney.com", "stability.ai",
    ]

    blocked_ips = []
    new_blocked = []

    print(f"\n{BOLD}Scanning domains...{RESET}\n")
    for domain in test_domains:
        verdict, confidence, reason = check_domain(domain, clf, feature_cols, config)
        if verdict == "blocked":
            ips = resolve_domain(domain)
            if ips:
                blocked_ips.extend(ips)
                new_blocked.append({"domain": domain, "ips": ips})
            print(f"  {RED}[BLOCK]{RESET} {domain} -> {', '.join(ips) if ips else 'unresolved'}")
        elif verdict == "allowed":
            print(f"  {GREEN}[ALLOW]{RESET} {domain}")
        else:
            print(f"  {GREY}[SKIP]{RESET}  {domain}")

    config["blocked"] = new_blocked
    save_config(config)

    if blocked_ips:
        interface = config["settings"]["interface"]
        rules = generate_pf_rules(list(set(blocked_ips)), interface)
        print(f"\n{BOLD}Generated pf rules:{RESET}")
        print(f"  {rules.strip()}")

        if os.geteuid() == 0:
            success = apply_firewall_rules(rules)
            if success:
                print(f"\n{GREEN}Firewall rules applied successfully.{RESET}")
        else:
            print(f"\n{YELLOW}Run with sudo to apply firewall rules:{RESET}")
            print(f"  sudo python3 ai_firewall.py enforce")
    else:
        print(f"\n{GREEN}No domains to block.{RESET}")

    print(f"\n{BOLD}Summary:{RESET}")
    print(f"  Scanned:  {len(test_domains)}")
    print(f"  Blocked:  {len(new_blocked)}")
    print(f"  Allowed:  {sum(1 for d in test_domains if is_whitelisted(d, config))}")


def cmd_disable(args):
    result = subprocess.run(
        ["pfctl", "-a", ANCHOR_NAME, "-F", "all"],
        capture_output=True, text=True
    )
    if result.returncode == 0:
        print(f"{GREEN}Firewall rules flushed.{RESET}")
    else:
        print(f"{RED}Error flushing rules (need sudo): {result.stderr}{RESET}")


def main():
    parser = argparse.ArgumentParser(
        description="AI Website Firewall - Block unapproved AI sites"
    )
    subparsers = parser.add_subparsers(dest="command", help="Available commands")

    check_parser = subparsers.add_parser("check", help="Check a domain")
    check_parser.add_argument("domain", help="Domain to classify")
    check_parser.set_defaults(func=cmd_check)

    status_parser = subparsers.add_parser("status", help="Show firewall status")
    status_parser.set_defaults(func=cmd_status)

    whitelist_parser = subparsers.add_parser("whitelist", help="Manage whitelist")
    whitelist_sub = whitelist_parser.add_subparsers(dest="whitelist_action")
    add_parser = whitelist_sub.add_parser("add", help="Add domain to whitelist")
    add_parser.add_argument("domain", help="Domain to whitelist")
    add_parser.set_defaults(func=cmd_whitelist_add)

    enforce_parser = subparsers.add_parser("enforce", help="Scan and apply blocks")
    enforce_parser.set_defaults(func=cmd_enforce)

    disable_parser = subparsers.add_parser("disable", help="Disable firewall rules")
    disable_parser.set_defaults(func=cmd_disable)

    args = parser.parse_args()
    if hasattr(args, "func"):
        args.func(args)
    else:
        parser.print_help()


if __name__ == "__main__":
    main()

Your AI firewall CLI is fully operational. You can classify any domain, inspect the firewall state, approve sites on the fly, and trigger a full enforcement scan. Next up, take on the Secret Mission to add real-time DNS monitoring that watches for new AI domain access attempts live.

Secret mission

Real-Time DNS Monitor

Your firewall blocks domains you already know about. But what about new AI services your team discovers while browsing? Build a live monitoring mode that watches DNS queries as they happen, classifies each new domain in real time, and optionally auto-enforces blocks the moment an unapproved AI site is detected.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • pf firewall rules loaded into the macOS kernel (no cost, local config).
  • pf anchor file at /etc/pf.anchors/ai-firewall (no cost, local file).
  • The ai-firewall project folder on your Desktop (includes virtual environment, scripts, and model files).

Keep everything running

No action needed. Choose this if you're still actively building or want to keep your firewall rules enforcing blocks on unapproved AI sites.

  • Your pf anchor rules remain loaded in the kernel and continue blocking traffic.
  • Your project folder, virtual environment, and trained model stay on your Desktop.
  • You can run python3 ai_firewall.py status any time to check which domains are blocked.

Pause - I'll come back to this later

Flush the active firewall rules so blocked sites become accessible again, but keep all your project files and config intact for later.

  • Flush the pf anchor rules by running:
sudo pfctl -a ai-firewall -F all
  • Your project folder, config, and trained model remain on your Desktop. When you come back, activate the virtual environment and run python3 ai_firewall.py enforce to re-apply blocks.

Delete - I don't want to use this again

Remove all project resources completely. This flushes the firewall rules, deletes the anchor file, and removes your project folder.

  • Flush the pf anchor rules and remove the anchor file by running:
sudo pfctl -a ai-firewall -F all && sudo rm /etc/pf.anchors/ai-firewall
  • Deactivate the virtual environment by running:
deactivate
  • Delete the project folder from your Desktop by running:
cd ~/Desktop && rm -rf ai-firewall
  • Confirm the folder is gone by running:
ls ~/Desktop

You should no longer see ai-firewall in the output.

Nice Work!

Nice Work!

You just built an enterprise-style AI website policy enforcement system from scratch. Your tool uses machine learning to detect AI-related domains, enforces a whitelist of approved services, and blocks unapproved ones at the network level using real macOS firewall rules.

You've learned how to:

  • Train a scikit-learn RandomForestClassifier to identify AI-related websites by extracting structural features from domain URLs like keyword presence, TLD type, and character entropy.
  • Build a policy engine that combines JSON whitelist lookup with ML classification to produce allow/block verdicts, then resolves blocked domains to IP addresses for firewall rule generation.
  • Generate and apply real macOS pf (Packet Filter) firewall rules using anchors, and wire everything together into a CLI tool with check, status, whitelist, enforce, and disable commands.
  • Secret Mission: Added real-time DNS monitoring that watches for new AI domain access attempts and auto-classifies them live using macOS unified logging.

Ready to quiz yourself?