Skip to content

Architecture

WireBuddy system architecture and design decisions.

High-Level Architecture

graph TB
    subgraph "Client Layer"
        A[Web Browser]
        B[WireGuard Client]
        C[API Client]
    end

    subgraph "Application Layer"
        D[FastAPI App]
        E[Uvicorn ASGI Server]
    end

    subgraph "Business Logic"
        F[Auth & Sessions]
        G[WireGuard Manager]
        H[DNS Resolver]
        I[Metrics Collector]
    end

    subgraph "Data Layer"
        J[SQLite Database]
        K[TSDB]
        L[File Storage]
    end

    subgraph "System Layer"
        M[WireGuard Kernel Module]
        N[Unbound DNS]
        O[Conntrack]
    end

    A --> D
    C --> D
    B --> M
    D --> E
    D --> F
    D --> G
    D --> H
    D --> I
    F --> J
    G --> J
    G --> M
    H --> N
    I --> K
    I --> O
    G --> L

Technology Stack

Component Technology Purpose
Web Framework FastAPI REST API and web interface
ASGI Server Uvicorn High-performance async server
Database SQLite3 Configuration and user data
Time-Series DB Custom TSDB Metrics storage
Template Engine Jinja2 HTML rendering
VPN WireGuard VPN server
DNS Unbound DNS resolver
Frontend Bootstrap 5 Responsive UI
Charts Chart.js Traffic visualization
Icons Material Icons Icon set

Application Structure

FastAPI Application

# app/main.py
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates

app = FastAPI(title="WireBuddy")

# Mount static files
app.mount("/static", StaticFiles(directory="app/static"), name="static")

# Template engine
templates = Jinja2Templates(directory="app/templates")

# Include routers
app.include_router(auth_router, prefix="/api/auth", tags=["auth"])
app.include_router(wireguard_router, prefix="/api/wireguard", tags=["wireguard"])
# ...

Router Pattern

# app/api/wireguard.py
from fastapi import APIRouter, Depends
from app.models import PeerCreate, PeerResponse
from app.db import get_db

router = APIRouter()

@router.post("/peers", response_model=PeerResponse)
async def create_peer(
    peer: PeerCreate,
    db = Depends(get_db),
    user = Depends(get_current_user)
):
    # Validate
    # Create peer
    # Return response
    pass

Database Schema

Users Table

CREATE TABLE users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    username TEXT UNIQUE NOT NULL,
    email TEXT UNIQUE,
    password_hash TEXT NOT NULL,
    salt TEXT NOT NULL,
    role TEXT NOT NULL DEFAULT 'user',
    totp_secret TEXT,
    created_at INTEGER NOT NULL,
    updated_at INTEGER NOT NULL,
    disabled INTEGER DEFAULT 0
);

Interfaces Table

CREATE TABLE interfaces (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT UNIQUE NOT NULL,
    address TEXT NOT NULL,
    listen_port INTEGER NOT NULL,
    private_key TEXT NOT NULL,
    public_key TEXT NOT NULL,
    status TEXT DEFAULT 'inactive',
    created_at INTEGER NOT NULL
);

Peers Table

CREATE TABLE peers (
    id TEXT PRIMARY KEY,
    name TEXT NOT NULL,
    interface TEXT NOT NULL,
    ip TEXT NOT NULL,
    public_key TEXT NOT NULL,
    private_key TEXT NOT NULL,
    preshared_key TEXT,
    allowed_ips TEXT NOT NULL,
    persistent_keepalive INTEGER DEFAULT 0,
    enabled INTEGER DEFAULT 1,
    created_at INTEGER NOT NULL,
    FOREIGN KEY (interface) REFERENCES interfaces(name)
);

Sessions Table

CREATE TABLE sessions (
    id TEXT PRIMARY KEY,
    user_id INTEGER NOT NULL,
    token_hash TEXT UNIQUE NOT NULL,
    ip_address TEXT,
    user_agent TEXT,
    created_at INTEGER NOT NULL,
    expires_at INTEGER NOT NULL,
    FOREIGN KEY (user_id) REFERENCES users(id)
);

Authentication Flow

sequenceDiagram
    participant U as User
    participant B as Browser
    participant API as FastAPI
    participant DB as Database

    U->>B: Enter credentials
    B->>API: POST /api/auth/login
    API->>DB: Query user by username
    DB-->>API: User data + password hash
    API->>API: Verify password (PBKDF2)
    alt MFA Enabled
        API-->>B: Require MFA
        B->>U: Request TOTP code
        U->>B: Enter code
        B->>API: Send TOTP code
        API->>API: Verify TOTP
    end
    API->>API: Generate session token
    API->>DB: Store session (hashed)
    API-->>B: Set session cookie
    B-->>U: Logged in

WireGuard Management

Configuration Generation

def generate_interface_config(interface: Interface) -> str:
    config = f"""[Interface]
PrivateKey = {interface.private_key}
Address = {interface.address}
ListenPort = {interface.listen_port}
"""

    # Add peers
    for peer in interface.peers:
        config += f"""
[Peer]
PublicKey = {peer.public_key}
AllowedIPs = {peer.allowed_ips}
"""
        if peer.preshared_key:
            config += f"PresharedKey = {peer.preshared_key}\n"
        if peer.persistent_keepalive:
            config += f"PersistentKeepalive = {peer.persistent_keepalive}\n"

    return config

Interface Management

class WireGuardManager:
    def start_interface(self, name: str):
        # Generate config
        config = self.generate_config(name)

        # Write to file
        config_path = f"/etc/wireguard/{name}.conf"
        with open(config_path, 'w') as f:
            f.write(config)

        # Start with wg-quick
        subprocess.run(["wg-quick", "up", name], check=True)

    def stop_interface(self, name: str):
        subprocess.run(["wg-quick", "down", name], check=True)

DNS Integration

Unbound Configuration

def generate_unbound_config(settings: DNSSettings) -> str:
    config = """
server:
    verbosity: 1
    interface: 10.8.0.1
    port: 53
    do-ip4: yes
    do-ip6: yes
    do-udp: yes
    do-tcp: yes

    # Performance
    num-threads: 4
    msg-cache-size: 50m
    rrset-cache-size: 100m

    # Security
    hide-identity: yes
    hide-version: yes
    qname-minimisation: yes
"""

    # Add blocklists
    for domain in settings.blocked_domains:
        config += f'    local-zone: "{domain}" always_refuse\n'

    # DoT upstream
    if settings.dot_enabled:
        config += """
forward-zone:
    name: "."
    forward-tls-upstream: yes
    forward-addr: 1.1.1.1@853#cloudflare-dns.com
"""

    return config

Query Logging

class DNSQueryLogger:
    def __init__(self, log_path: str):
        self.log_path = log_path
        self.tailer = FileTailer(log_path)

    async def stream_queries(self):
        async for line in self.tailer:
            query = self.parse_query(line)
            yield query

    def parse_query(self, line: str) -> DNSQuery:
        # Parse Unbound log format
        # Return structured query object
        pass

Metrics Collection

Conntrack Monitoring

class ConntrackMonitor:
    def __init__(self):
        self.conntrack_path = "/proc/net/nf_conntrack"

    def collect_peer_traffic(self, peer_ip: str) -> dict:
        traffic = {"tx": 0, "rx": 0}

        with open(self.conntrack_path) as f:
            for line in f:
                # Parse conntrack entry
                if peer_ip in line:
                    entry = self.parse_entry(line)
                    traffic["tx"] += entry.bytes_orig
                    traffic["rx"] += entry.bytes_reply

        return traffic

Time-Series Database

class TSDB:
    def __init__(self, path: str):
        self.db = sqlite3.connect(path)
        self.init_schema()

    def record_metric(self, metric: str, value: float, tags: dict):
        timestamp = int(time.time())
        self.db.execute(
            "INSERT INTO metrics (timestamp, metric, value, tags) VALUES (?, ?, ?, ?)",
            (timestamp, metric, value, json.dumps(tags))
        )
        self.db.commit()

    def query(self, metric: str, start: int, end: int) -> list:
        cursor = self.db.execute(
            "SELECT timestamp, value FROM metrics WHERE metric = ? AND timestamp BETWEEN ? AND ?",
            (metric, start, end)
        )
        return cursor.fetchall()

Security Architecture

Password Hashing

def hash_password(password: str) -> tuple[bytes, bytes]:
    salt = os.urandom(32)
    kdf = PBKDF2HMAC(
        algorithm=hashes.SHA256(),
        length=32,
        salt=salt,
        iterations=600_000
    )
    key = kdf.derive(password.encode())
    return salt, key

Secret Encryption

class SecretVault:
    def __init__(self, master_key: str):
        self.master_key = master_key

    def encrypt(self, plaintext: str, salt: bytes) -> str:
        kdf = PBKDF2HMAC(
            algorithm=hashes.SHA256(),
            length=32,
            salt=salt,
            iterations=100_000
        )
        key = base64.urlsafe_b64encode(kdf.derive(self.master_key.encode()))
        f = Fernet(key)
        return f.encrypt(plaintext.encode()).decode()

    def decrypt(self, ciphertext: str, salt: bytes) -> str:
        kdf = PBKDF2HMAC(
            algorithm=hashes.SHA256(),
            length=32,
            salt=salt,
            iterations=100_000
        )
        key = base64.urlsafe_b64encode(kdf.derive(self.master_key.encode()))
        f = Fernet(key)
        return f.decrypt(ciphertext.encode()).decode()

Frontend Architecture

JavaScript Structure

// app/static/js/main.js
const WireBuddy = {
    init() {
        this.setupEventListeners();
        this.loadDashboard();
    },

    async loadDashboard() {
        const response = await fetch('/api/metrics/dashboard');
        const data = await response.json();
        this.updateDashboard(data);
    },

    updateDashboard(data) {
        document.getElementById('peer-count').textContent = data.peers.total;
        // ...
    }
};

document.addEventListener('DOMContentLoaded', () => WireBuddy.init());

Chart Integration

const TrafficChart = {
    chart: null,

    init(canvasId) {
        const ctx = document.getElementById(canvasId).getContext('2d');
        this.chart = new Chart(ctx, {
            type: 'line',
            data: { /* ... */ },
            options: { /* ... */ }
        });
    },

    update(data) {
        this.chart.data.datasets[0].data = data;
        this.chart.update();
    }
};

Deployment Architecture

Docker Container

FROM python:3.13-slim

# Install system dependencies
RUN apt-get update && apt-get install -y \
    wireguard-tools \
    unbound \
    conntrack \
    && rm -rf /var/lib/apt/lists/*

# Copy application
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .

# Non-root user
RUN useradd -m wirebuddy
USER wirebuddy

# Start application
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

Docker Compose

services:
  wirebuddy:
    image: giiibates/wirebuddy:latest
    network_mode: host
    cap_add:
      - NET_ADMIN
    volumes:
      - ./data:/app/data
    env_file:
      - settings.env

Performance Considerations

Database Optimization

  • SQLite WAL mode for concurrent reads
  • Indexes on frequently queried columns
  • Connection pooling (AsyncIO)

Caching

  • DNS query cache (Unbound)
  • Session cache (in-memory)
  • Metrics cache (Redis in future)

Async Operations

  • FastAPI async handlers
  • Async database queries (aiosqlite)
  • Background tasks (BackgroundTasks)

Scalability

Current Limitations

  • Single-server deployment
  • SQLite (not distributed)
  • No horizontal scaling

Future Enhancements

  • PostgreSQL support
  • Redis for caching
  • Distributed mode (multiple workers)
  • Metrics persistence (InfluxDB, Prometheus)

Monitoring & Observability

Logging

  • Structured logging (JSON)
  • Log levels (DEBUG, INFO, WARNING, ERROR)
  • Audit logs (security events)

Metrics (Future)

  • Prometheus exporter
  • Grafana dashboards
  • Custom alerts

Design Decisions

Why SQLite?

Pros: - Simple deployment (no separate DB server) - Fast for single-server workload - Embedded, no maintenance - ACID compliant

Cons: - No network access (must be local) - Limited concurrent writes - No replication

Why FastAPI?

Pros: - Modern async framework - Automatic OpenAPI docs - Type hints and validation - High performance

Why Unbound?

Pros: - Lightweight and fast - DNSSEC support - Easy configuration - Well-tested and secure

Next Steps