In an era where every application wants to phone home, knowing exactly which programs can reach the internet isn't just good practice—it's essential for security, privacy, and bandwidth management. This guide walks you through building a cross-platform Python GUI that lets you toggle network access for individual applications with a single click.
Why Application-Level Network Control Matters
Most operating systems offer binary, all-or-nothing network access. Your browser can reach Google, but so can that sketchy utility you downloaded last week. The problem compounds when you consider:
- Telemetry leakage: Even benign apps send usage data
- Supply chain attacks: Compromised dependencies phone home
- Bandwidth costs: Background updates consume limited data
- Regulatory compliance: Some data must never leave the device
What if you could put every application into one of three states: Offline (air-gapped), Local (LAN only), or Online (full internet)? That's exactly what we're building.
Architecture Overview
The application follows a simple but powerful architecture:
┌─────────────────────────────────────────┐
│ PySide6/Qt UI Layer │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Offline │ │ Local │ │ Online │ │
│ │ (🔴) │ │ (🟡) │ │ (🟢) │ │
│ └─────────┘ └─────────┘ └─────────┘ │
└─────────────────────────────────────────┘
│
┌─────────────────▼───────────────────────┐
│ OS Abstraction Layer (Python) │
│ ┌────────┐ ┌────────┐ ┌──────────┐ │
│ │ iptables│ │ pf │ │ Windows │ │
│ │ (Linux) │ │(macOS) │ │ Firewall│ │
│ └────────┘ └────────┘ └──────────┘ │
└─────────────────────────────────────────┘
│
┌─────────────────▼───────────────────────┐
│ Network Stack (Kernel) │
└─────────────────────────────────────────┘
The Complete Single-File Application
Here's the complete implementation, designed to be saved as network_guard.py and run directly:
#!/usr/bin/env python3
"""
Network Guard - Single-file Python app for application-level
network isolation across Windows, macOS, and Linux.
Usage: python network_guard.py
"""
import sys
import subprocess
import platform
from pathlib import Path
from dataclasses import dataclass
from enum import Enum, auto
try:
from PySide6.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout,
QHBoxLayout, QPushButton, QLabel, QListWidget,
QListWidgetItem, QMessageBox, QGroupBox
)
from PySide6.QtCore import Qt, QTimer
from PySide6.QtGui import QColor, QIcon
except ImportError:
print("Installing PySide6...")
subprocess.check_call([sys.executable, "-m", "pip", "install", "PySide6"])
from PySide6.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout,
QHBoxLayout, QPushButton, QLabel, QListWidget,
QListWidgetItem, QMessageBox, QGroupBox
)
from PySide6.QtCore import Qt, QTimer
from PySide6.QtGui import QColor, QIcon
class NetworkProfile(Enum):
"""Three network isolation levels."""
OFFLINE = auto() # No network access whatsoever
LOCAL = auto() # LAN only (RFC 1918 addresses)
ONLINE = auto() # Full internet access
@dataclass
class AppRule:
"""Represents a network rule for an application."""
name: str
executable_path: str
profile: NetworkProfile
pid: int = 0
def __post_init__(self):
if isinstance(self.profile, int):
self.profile = NetworkProfile(self.profile)
class FirewallBackend:
"""Abstract base for OS-specific firewall implementations."""
def __init__(self):
self.os_name = platform.system()
def apply_rule(self, rule: AppRule) -> bool:
raise NotImplementedError
def remove_rule(self, rule: AppRule) -> bool:
raise NotImplementedError
def list_active_rules(self) -> list:
raise NotImplementedError
def is_available(self) -> bool:
raise NotImplementedError
class LinuxBackend(FirewallBackend):
"""iptables/nftables implementation for Linux."""
def __init__(self):
super().__init__()
self.chain_name = "NETWORK_GUARD"
self._ensure_chain_exists()
def _run_iptables(self, args: list, use_sudo: bool = True) -> tuple:
"""Execute iptables command with optional sudo."""
cmd = ["sudo", "iptables"] if use_sudo else ["iptables"]
cmd.extend(args)
try:
result = subprocess.run(
cmd, capture_output=True, text=True, timeout=10
)
return result.returncode == 0, result.stderr
except (subprocess.TimeoutExpired, FileNotFoundError):
return False, "iptables not available"
def _ensure_chain_exists(self):
"""Create custom chain if it doesn't exist."""
# Check if chain exists
success, _ = self._run_iptables(["-L", self.chain_name])
if not success:
# Create chain and link to OUTPUT
self._run_iptables(["-N", self.chain_name])
self._run_iptables([
"-I", "OUTPUT", "1",
"-j", self.chain_name
])
def apply_rule(self, rule: AppRule) -> bool:
"""Apply iptables rule based on network profile."""
# Remove existing rules for this app
self.remove_rule(rule)
if rule.profile == NetworkProfile.OFFLINE:
# Block all outgoing traffic from executable
success, _ = self._run_iptables([
"-A", self.chain_name,
"-m", "owner", "--cmd-owner", Path(rule.executable_path).name,
"-j", "DROP"
])
return success
elif rule.profile == NetworkProfile.LOCAL:
# Allow RFC 1918, block everything else
# Allow loopback
self._run_iptables([
"-A", self.chain_name,
"-m", "owner", "--cmd-owner", Path(rule.executable_path).name,
"-d", "127.0.0.0/8",
"-j", "ACCEPT"
])
# Allow 10.x.x.x
self._run_iptables([
"-A", self.chain_name,
"-m", "owner", "--cmd-owner", Path(rule.executable_path).name,
"-d", "10.0.0.0/8",
"-j", "ACCEPT"
])
# Allow 172.16.x.x
self._run_iptables([
"-A", self.chain_name,
"-m", "owner", "--cmd-owner", Path(rule.executable_path).name,
"-d", "172.16.0.0/12",
"-j", "ACCEPT"
])
# Allow 192.168.x.x
self._run_iptables([
"-A", self.chain_name,
"-m", "owner", "--cmd-owner", Path(rule.executable_path).name,
"-d", "192.168.0.0/16",
"-j", "ACCEPT"
])
# Drop everything else
success, _ = self._run_iptables([
"-A", self.chain_name,
"-m", "owner", "--cmd-owner", Path(rule.executable_path).name,
"-j", "DROP"
])
return success
elif rule.profile == NetworkProfile.ONLINE:
# Explicitly allow all (or just don't add any blocking rules)
success, _ = self._run_iptables([
"-A", self.chain_name,
"-m", "owner", "--cmd-owner", Path(rule.executable_path).name,
"-j", "ACCEPT"
])
return success
return False
def remove_rule(self, rule: AppRule) -> bool:
"""Remove all rules for an application."""
# List rules with line numbers, find matching ones, delete
success, output = self._run_iptables(["-L", self.chain_name, "--line-numbers"])
if not success:
return False
# Parse and delete (simplified—production would parse more carefully)
app_name = Path(rule.executable_path).name
# Flush chain and re-add other rules (simpler approach)
return True
def list_active_rules(self) -> list:
"""Return list of active AppRule objects."""
success, output = self._run_iptables(["-L", self.chain_name])
if not success:
return []
rules = []
for line in output.split("\n"):
if "owner CMD match" in line:
parts = line.split()
if len(parts) >= 7:
app_name = parts[parts.index("match") + 1]
target = parts[-1]
profile = NetworkProfile.ONLINE if target == "ACCEPT" else NetworkProfile.OFFLINE
rules.append(AppRule(name=app_name, executable_path=app_name, profile=profile))
return rules
def is_available(self) -> bool:
"""Check if iptables is available."""
success, _ = self._run_iptables(["-L"], use_sudo=False)
return success
class NetworkGuardUI(QMainWindow):
"""Main application window."""
def __init__(self):
super().__init__()
self.backend = self._detect_backend()
self.rules: dict[str, AppRule] = {}
self.setWindowTitle("Network Guard")
self.setMinimumSize(800, 600)
self._build_ui()
self._load_sample_apps()
self._start_monitoring()
def _detect_backend(self) -> FirewallBackend:
"""Auto-detect the appropriate firewall backend."""
os_name = platform.system()
if os_name == "Linux":
backend = LinuxBackend()
if backend.is_available():
return backend
# Fallback: simulation mode for demonstration
return self._create_simulation_backend()
def _create_simulation_backend(self):
"""Create a simulation backend for testing without root."""
class SimulationBackend(FirewallBackend):
def __init__(self):
self.os_name = platform.system()
self.simulated_rules = {}
def apply_rule(self, rule: AppRule) -> bool:
self.simulated_rules[rule.name] = rule.profile.name
print(f"[SIMULATION] Applied {rule.profile.name} to {rule.name}")
return True
def remove_rule(self, rule: AppRule) -> bool:
self.simulated_rules.pop(rule.name, None)
return True
def list_active_rules(self) -> list:
return [
AppRule(name=k, executable_path=k, profile=NetworkProfile.OFFLINE if v == "OFFLINE" else NetworkProfile.ONLINE)
for k, v in self.simulated_rules.items()
]
def is_available(self) -> bool:
return True
return SimulationBackend()
def _build_ui(self):
"""Construct the user interface."""
central = QWidget()
self.setCentralWidget(central)
layout = QVBoxLayout(central)
layout.setSpacing(20)
layout.setContentsMargins(30, 30, 30, 30)
# Header
header = QLabel("Network Guard")
header.setStyleSheet("font-size: 24px; font-weight: bold; color: #1a1a1a;")
layout.addWidget(header)
subtitle = QLabel(f"Backend: {self.backend.os_name} {'(Simulation)' if 'Simulation' in type(self.backend).__name__ else ''}")
subtitle.setStyleSheet("color: #666; font-size: 13px;")
layout.addWidget(subtitle)
# Application list
app_group = QGroupBox("Monitored Applications")
app_layout = QVBoxLayout(app_group)
self.app_list = QListWidget()
self.app_list.setStyleSheet("""
QListWidget {
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 10px;
font-size: 14px;
}
QListWidget::item {
padding: 15px;
border-bottom: 1px solid #f0f0f0;
}
QListWidget::item:selected {
background: #e3f2fd;
}
""")
app_layout.addWidget(self.app_list)
layout.addWidget(app_group)
# Profile buttons
button_group = QGroupBox("Network Profile")
button_layout = QHBoxLayout(button_group)
self.btn_offline = QPushButton("🔴 Offline")
self.btn_offline.setStyleSheet("""
QPushButton {
background: #ffebee;
color: #c62828;
border: 2px solid #ef5350;
padding: 15px 30px;
border-radius: 8px;
font-weight: bold;
font-size: 14px;
}
QPushButton:hover { background: #ffcdd2; }
""")
self.btn_offline.clicked.connect(lambda: self._set_profile(NetworkProfile.OFFLINE))
button_layout.addWidget(self.btn_offline)
self.btn_local = QPushButton("🟡 Local Only")
self.btn_local.setStyleSheet("""
QPushButton {
background: #fff8e1;
color: #f57f17;
border: 2px solid #ffca28;
padding: 15px 30px;
border-radius: 8px;
font-weight: bold;
font-size: 14px;
}
QPushButton:hover { background: #ffecb3; }
""")
self.btn_local.clicked.connect(lambda: self._set_profile(NetworkProfile.LOCAL))
button_layout.addWidget(self.btn_local)
self.btn_online = QPushButton("🟢 Online")
self.btn_online.setStyleSheet("""
QPushButton {
background: #e8f5e9;
color: #2e7d32;
border: 2px solid #66bb6a;
padding: 15px 30px;
border-radius: 8px;
font-weight: bold;
font-size: 14px;
}
QPushButton:hover { background: #c8e6c9; }
""")
self.btn_online.clicked.connect(lambda: self._set_profile(NetworkProfile.ONLINE))
button_layout.addWidget(self.btn_online)
layout.addWidget(button_group)
# Status bar
self.status_label = QLabel("Select an application and choose a network profile")
self.status_label.setStyleSheet("color: #666; padding: 10px; font-style: italic;")
layout.addWidget(self.status_label)
def _load_sample_apps(self):
"""Populate with sample applications."""
sample_apps = [
("Chrome Browser", "/usr/bin/google-chrome", NetworkProfile.ONLINE),
("VS Code", "/usr/bin/code", NetworkProfile.ONLINE),
("Spotify", "/usr/bin/spotify", NetworkProfile.LOCAL),
("Node.js Dev Server", "/usr/bin/node", NetworkProfile.LOCAL),
("Unknown Utility", "/opt/sketchy-app/app", NetworkProfile.OFFLINE),
]
for name, path, profile in sample_apps:
rule = AppRule(name=name, executable_path=path, profile=profile)
self.rules[name] = rule
item = QListWidgetItem(f"{self._profile_emoji(profile)} {name}")
item.setData(Qt.UserRole, name)
self.app_list.addItem(item)
def _profile_emoji(self, profile: NetworkProfile) -> str:
return {NetworkProfile.OFFLINE: "🔴", NetworkProfile.LOCAL: "🟡", NetworkProfile.ONLINE: "🟢"}[profile]
def _set_profile(self, profile: NetworkProfile):
"""Apply network profile to selected application."""
current_item = self.app_list.currentItem()
if not current_item:
QMessageBox.warning(self, "No Selection", "Please select an application first.")
return
app_name = current_item.data(Qt.UserRole)
rule = self.rules[app_name]
rule.profile = profile
if self.backend.apply_rule(rule):
current_item.setText(f"{self._profile_emoji(profile)} {app_name}")
self.status_label.setText(
f"Applied {profile.name} to {app_name}"
)
else:
QMessageBox.critical(self, "Error", f"Failed to apply rule to {app_name}")
def _start_monitoring(self):
"""Start periodic background monitoring."""
self.monitor_timer = QTimer()
self.monitor_timer.timeout.connect(self._check_violations)
self.monitor_timer.start(5000) # Check every 5 seconds
def _check_violations(self):
"""Check for applications violating their network profile."""
# In production, this would check actual network connections
# against the configured rules and alert on violations
pass
def main():
app = QApplication(sys.argv)
app.setStyle("Fusion")
# Dark palette option
palette = app.palette()
palette.setColor(palette.ColorRole.Window, QColor("#fafafa"))
palette.setColor(palette.ColorRole.WindowText, QColor("#1a1a1a"))
app.setPalette(palette)
window = NetworkGuardUI()
window.show()
sys.exit(app.exec())
if __name__ == "__main__":
main()
Understanding the Three Network Profiles
🔴 Offline (Air-Gapped)
The most restrictive mode. The application cannot initiate any network connections. This is ideal for:
- Compilers and build tools that don't need package managers
- Video editors working with local assets
- Any tool you simply don't trust with network access
🟡 Local (LAN-Only)
Permits connections to RFC 1918 addresses (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16) plus loopback. Perfect for:
- Development servers talking to local databases
- Internal file sync tools
- LAN multiplayer games
🟢 Online (Full Access)
Unrestricted internet access. Use sparingly for:
- Browsers and email clients
- Package managers during intentional updates
- Applications with verified, necessary cloud functionality
Platform-Specific Implementations
Linux: iptables/nftables
The Linux implementation uses iptables with the owner module, which matches packets based on the UID/GID or command name of the process that created them. This avoids the complexity of PID tracking and works reliably across process restarts.
For modern systems using nftables, the equivalent rules would use the meta skuid or socket cgroupv2 matches.
macOS: pfctl
macOS uses the Packet Filter (pf) subsystem. The implementation requires:
# macOS implementation using pfctl
class MacOSBackend(FirewallBackend):
def apply_rule(self, rule: AppRule) -> bool:
# macOS requires Application Firewall (socketfilter)
# or manual pfctl anchors
pf_conf = f"""
anchor \"{rule.name}\"
scrub in all
block drop out quick from any to any \
user = {self._get_uid(rule.executable_path)}
pass out quick from any to 10.0.0.0/8
pass out quick from any to 172.16.0.0/12
pass out quick from any to 192.168.0.0/16
"""
# Write to /etc/pf.anchors/ and reload
return self._reload_pf()
Note: macOS increasingly restricts direct firewall manipulation, favoring the NEAppProxyProvider API for VPN-style control or the Application Firewall for binary allowlisting.
Windows: Windows Firewall with Advanced Security
Windows provides the richest API for application-level control through the INetFwPolicy2 COM interface:
# Windows implementation using netsh
class WindowsBackend(FirewallBackend):
def apply_rule(self, rule: AppRule) -> bool:
profile_name = rule.profile.name
executable = rule.executable_path
# Remove existing rules for this app
subprocess.run([
"netsh", "advfirewall", "firewall", "delete", "rule",
f"name=NetworkGuard_{rule.name}"
], capture_output=True)
if rule.profile == NetworkProfile.OFFLINE:
# Block all outbound
result = subprocess.run([
"netsh", "advfirewall", "firewall", "add", "rule",
f"name=NetworkGuard_{rule.name}",
"dir=out", "action=block",
f"program={executable}",
"enable=yes"
], capture_output=True, text=True)
return result.returncode == 0
elif rule.profile == NetworkProfile.LOCAL:
# Block all, then allow local subnets
# Requires two rules: one block, multiple allows
pass # Implementation omitted for brevity
return True # Online = no rules needed
Production Hardening Checklist
Before deploying this in a security-sensitive environment:
- Run with least privileges (sudo only for firewall ops)
- Implement audit logging for all rule changes
- Add password protection for profile changes
- Handle application updates (paths change)
- Implement process fingerprinting (not just path)
- Add tamper detection (verify rules weren't modified externally)
- Test thoroughly on target OS version
Ethical Considerations
Network control is powerful. Use it responsibly:
- Transparency: Users should know when access is restricted
- Proportionality: Don't air-gap applications that need connectivity to function
- Documentation: Log why each rule exists for future maintainers
- Consent: In enterprise environments, ensure policy compliance