adr/interfaces/adr-007-vpn-gateway.md
Rama main · réplica de lectura del repositorio original.
# VPN Gateway Interfac# VPN Gateway Interface
**Status:** Accepted
## Context and Problem Statement
Download clients (qBittorrent, SABnzbd) need to route their traffic through a VPN gateway (gluetun-k8s) via VXLAN overlay networking. Both the gateway charm and consumer charms need to be patched with pod-gateway containers at runtime. This interface must enable clean integration while following Juju's relation-based orchestration model and allowing future extensibility for alternative routing methods.
## Considered Options
* Active provider + active requirer pattern (both sides react)
* Passive provider + active requirer pattern (provider publishes, requirer reacts)
* Hardcoded routing method (pod-gateway only)
* Configurable routing method via relation data (extensible)
## Decision Outcome
Chosen option: **"Passive provider + active requirer with configurable routing method"**, because:
1. **Passive provider pattern**: The VPN gateway publishes configuration and doesn't need to track individual consumers. This matches the pattern used for other Charmarr interfaces.
2. **Active requirer pattern**: Download clients react when gateway config changes to patch their StatefulSets and create NetworkPolicy.
3. **Configurable routing method**: The provider specifies `routing_method` in relation data (defaulting to `pod-gateway`). The requirer reads this and applies the appropriate patching strategy. This allows future routing methods without charm code changes - only library updates needed.
### Library Structure
The `vpn-gateway` interface is implemented in **charmarr-vpn**, a package within the charmarr-lib monorepo. This design:
* Provides consistent packaging with other Charmarr libraries (charmarr-core, charmarr-testing)
* Allows gluetun-k8s charm (or future alternatives) to implement the provider side
* Enables any charm needing VPN egress to use the requirer side
* Maintains clean separation of VPN functionality from core interfaces
* Still allows reuse beyond the Charmarr ecosystem
**charmarr-vpn provides:**
* `VPNGatewayProvider` - for VPN gateway charms
- Patches gateway charm's StatefulSet with pod-gateway gateway containers
- Publishes routing config and method to relation data
- Handles gateway-side VXLAN setup
* `VPNGatewayRequirer` - for consumer charms
- Reads `routing_method` from relation data
- Patches consumer's StatefulSet with appropriate client containers
- Creates NetworkPolicy for kill switch
- Provides `reconcile()` method that handles everything
* Data models (`VPNGatewayProviderData`, `VPNGatewayRequirerData`)
* Constants (pod-gateway image version, VXLAN defaults)
### Extensibility Model
```python
# Current usage (gluetun-k8s)
self.gateway = VPNGatewayProvider(self, "vpn-gateway")
# Defaults to routing_method="pod-gateway"
# Future hypothetical usage
self.gateway = VPNGatewayProvider(self, "vpn-gateway", routing_method="ebpf-routing")
```
Consumer charms don't specify routing method - they just call `reconcile()` and the library handles it based on what the provider advertised:
```python
# Consumer charm (unchanged regardless of routing method)
self.vpn = VPNGatewayRequirer(self, "vpn-gateway")
def _reconcile(self, event):
if self.vpn.is_ready():
self.vpn.reconcile() # Library checks routing_method internally
```
To add a new routing method:
1. Update charmarr-vpn with new patching logic
2. Consumer charms automatically support it after lib update
3. Optionally update provider charms to use new method
### Consequences
* Good, because follows established Charmarr interface patterns (passive provider)
* Good, because routing method communicated via relation data (extensible)
* Good, because consumer charms don't need to know routing details
* Good, because adding new routing methods doesn't require consumer charm changes
* Good, because VPN gateway complexity isolated from download clients
* Good, because single `changed` event enables clean reconciler pattern
* Good, because `external_ip` enables user verification of VPN functionality
* Good, because topology visible via `juju status --relations`
* Good, because charmarr-vpn is reusable beyond Charmarr
* Bad, because download clients need lightkube for StatefulSet patching (via charmarr-vpn)
* Bad, because download clients need RBAC permissions to patch their own StatefulSets
## Interface Data Models
### Provider Data (Published by gluetun-k8s)
```python
from typing import Optional
from pydantic import BaseModel, Field
class VPNGatewayProviderData(BaseModel):
"""Data published by VPN gateway provider."""
# Routing method - tells requirer how to configure itself
routing_method: str = Field(
default="pod-gateway",
description="Routing method: 'pod-gateway' (VXLAN overlay)"
)
# VXLAN routing configuration (used when routing_method="pod-gateway")
vxlan_id: int = Field(
description="VXLAN ID for overlay network, e.g., 42"
)
vxlan_gateway_ip: str = Field(
description="Gateway IP address in VXLAN subnet, e.g., '172.16.0.1'"
)
cluster_cidrs: str = Field(
description="Comma-separated CIDRs to NOT route through VPN, e.g., '10.42.0.0/16,10.96.0.0/12'"
)
# Gateway health
vpn_connected: bool = Field(
description="Whether VPN tunnel is established and healthy"
)
# Verification/debugging
external_ip: Optional[str] = Field(
default=None,
description="Current VPN exit IP address for user verification"
)
# Identity
instance_name: str = Field(
description="Juju application name, e.g., 'gluetun'"
)
```
### Requirer Data (Published by Download Clients)
```python
from charmarr_lib.interfaces.download_client import DownloadClient
class VPNGatewayRequirerData(BaseModel):
"""Data published by VPN gateway requirers (download clients)."""
client: DownloadClient = Field(
description="Download client type (qbittorrent, sabnzbd, etc.)"
)
instance_name: str = Field(
description="Unique identifier, e.g., 'qbittorrent-vpn'"
)
```
## Relation Schema (metadata.yaml)
**gluetun-k8s:**
```yaml
provides:
vpn-gateway:
interface: vpn-gateway
```
**qbittorrent-k8s, sabnzbd-k8s:**
```yaml
requires:
vpn-gateway:
interface: vpn-gateway
limit: 1 # Only one VPN gateway per download client
```
## Provider-Side Patching
The `VPNGatewayProvider` class patches the gateway charm's own StatefulSet to add pod-gateway containers:
```python
class VPNGatewayProvider:
def __init__(self, charm, relation_name, routing_method="pod-gateway"):
self._routing_method = routing_method
# ...
def reconcile(self):
"""Patch gateway StatefulSet and publish relation data."""
if self._routing_method == "pod-gateway":
self._patch_gateway_containers()
self._publish_provider_data()
```
This means gluetun-k8s charm only declares the gluetun container in charmcraft.yaml. The pod-gateway gateway containers are added at runtime by the library.
## User Workflow
```bash
# Deploy VPN gateway
juju deploy gluetun-k8s gluetun --trust \
--config vpn-provider=nordvpn \
--config wireguard-private-key=secret:vpn-creds
# Deploy download client
juju deploy qbittorrent-k8s qbittorrent --trust
# Relate to enable VPN routing
juju relate qbittorrent:vpn-gateway gluetun:vpn-gateway
# Check status
juju status
# gluetun/0: active - VPN connected: Switzerland (185.112.34.56)
# qbittorrent/0: active - Routing via gluetun (185.112.34.56)
# Verify VPN routing is working
kubectl exec -it qbittorrent-0 -- curl ifconfig.me
# Should show VPN exit IP
```
## charmarr-vpn Implementation Notes (Validated 2025-12-11)
### Provider-Side Patching (VPNGatewayProvider)
The library's `VPNGatewayProvider.reconcile()` method must:
1. **Patch gateway StatefulSet** with pod-gateway gateway containers:
- Init container with **privileged: true** and **iptables fix**
- Sidecar container (only needs NET_ADMIN capability)
2. **Discover cluster CIDRs** automatically:
```python
# Query cluster-info dump for pod and service CIDRs
pod_cidr = self._get_pod_cidr()
service_cidr = self._get_service_cidr()
```
3. **Publish provider data** including:
- `cluster_cidrs`: **Comma-separated string** in relation data (e.g., `"10.1.0.0/16,10.152.183.0/24"`)
- `vpn_connected`: Query gluetun's /v1/publicip/ip endpoint
- `external_ip`: For user verification
### Requirer-Side Patching (VPNGatewayRequirer)
The library's `VPNGatewayRequirer.reconcile()` method must:
1. **Check gateway readiness** before patching:
```python
if not provider_data.vpn_connected:
# Set WaitingStatus, don't patch yet
return
```
2. **Create ConfigMap** with cluster CIDRs and DNS IP:
```python
dns_ip = self._get_dns_service_ip() # Query kube-dns/coredns service
# CRITICAL: Convert comma-separated (relation data) to space-separated (ConfigMap)
# Client scripts use shell word splitting on spaces
cluster_cidrs_spaces = provider_data.cluster_cidrs.replace(',', ' ')
configmap_data = {
"settings.sh": f"""K8S_DNS_IPS="{dns_ip}"
NOT_ROUTED_TO_GATEWAY_CIDRS="{cluster_cidrs_spaces}"
"""
}
```
3. **Patch consumer StatefulSet** with:
- Init container (NET_ADMIN capability)
- Sidecar container (NET_ADMIN capability)
- ConfigMap volume mount
4. **Create NetworkPolicy** kill switch
5. **Monitor relation changes** and unpatch when relation breaks
### Validation Results
**Architecture validated on MicroK8s with Calico CNI:**
- ✅ Cross-namespace VXLAN routing (gateway and client in different namespaces)
- ✅ Kill switch blocks traffic when VPN down
- ✅ Auto-recovery after gateway restart (~10 seconds)
- ✅ Cluster traffic preserved (DNS, pod-to-pod, services)
- ✅ Resource overhead: ~3MB RAM per client sidecar
## Related ADRs
- **ADR-002: VPN Gateway Solution** - Defines gluetun + pod-gateway architecture
- **ADR-003: Download Client VPN Integration** - Defines consumer self-patching approach
- **ADR-004: VPN Kill Switch** - Defines two-layer kill switch with consumer-owned NetworkPolicy
- **ADR-005: Istio Mesh Integration** - Download clients IN mesh, gateway OUT