first commit

This commit is contained in:
Stefan Hacker
2026-02-02 09:46:35 +01:00
commit 6901dc369b
98 changed files with 13030 additions and 0 deletions
+36
View File
@@ -0,0 +1,36 @@
"""SQLAlchemy models for mGuard VPN Endpoint Server."""
from .tenant import Tenant
from .user import User
from .gateway import Gateway, RouterType
from .endpoint import Endpoint, ApplicationTemplate
from .access import UserGatewayAccess, UserEndpointAccess, ConnectionLog
from .certificate_authority import CertificateAuthority, CAStatus, CAAlgorithm
from .vpn_server import VPNServer, VPNProtocol, VPNCipher, VPNAuth, VPNCompression, VPNServerStatus
from .vpn_profile import VPNProfile, VPNProfileStatus
from .vpn_connection_log import VPNConnectionLog
__all__ = [
"Tenant",
"User",
"Gateway",
"RouterType",
"Endpoint",
"ApplicationTemplate",
"UserGatewayAccess",
"UserEndpointAccess",
"ConnectionLog",
# PKI & VPN
"CertificateAuthority",
"CAStatus",
"CAAlgorithm",
"VPNServer",
"VPNProtocol",
"VPNCipher",
"VPNAuth",
"VPNCompression",
"VPNServerStatus",
"VPNProfile",
"VPNProfileStatus",
"VPNConnectionLog",
]
+78
View File
@@ -0,0 +1,78 @@
"""Access control and logging models."""
from datetime import datetime
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey
from sqlalchemy.orm import relationship
from ..database import Base
class UserGatewayAccess(Base):
"""User access to gateways."""
__tablename__ = "user_gateway_access"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
gateway_id = Column(Integer, ForeignKey("gateways.id"), nullable=False)
granted_at = Column(DateTime, default=datetime.utcnow, nullable=False)
granted_by_id = Column(Integer, ForeignKey("users.id"), nullable=True)
# Relationships
user = relationship("User", foreign_keys=[user_id], back_populates="gateway_access")
gateway = relationship("Gateway", back_populates="user_access")
granted_by = relationship("User", foreign_keys=[granted_by_id])
def __repr__(self):
return f"<UserGatewayAccess(user_id={self.user_id}, gateway_id={self.gateway_id})>"
class UserEndpointAccess(Base):
"""User access to specific endpoints (optional fine-grained control)."""
__tablename__ = "user_endpoint_access"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
endpoint_id = Column(Integer, ForeignKey("endpoints.id"), nullable=False)
granted_at = Column(DateTime, default=datetime.utcnow, nullable=False)
granted_by_id = Column(Integer, ForeignKey("users.id"), nullable=True)
# Relationships
user = relationship("User", foreign_keys=[user_id], back_populates="endpoint_access")
endpoint = relationship("Endpoint", back_populates="user_access")
granted_by = relationship("User", foreign_keys=[granted_by_id])
def __repr__(self):
return f"<UserEndpointAccess(user_id={self.user_id}, endpoint_id={self.endpoint_id})>"
class ConnectionLog(Base):
"""Log of VPN connections for auditing."""
__tablename__ = "connection_logs"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
gateway_id = Column(Integer, ForeignKey("gateways.id"), nullable=False)
endpoint_id = Column(Integer, ForeignKey("endpoints.id"), nullable=True)
# Connection details
client_ip = Column(String(45), nullable=True) # Client's real IP
vpn_ip = Column(String(45), nullable=True) # Assigned VPN IP
connected_at = Column(DateTime, default=datetime.utcnow, nullable=False)
disconnected_at = Column(DateTime, nullable=True)
# Relationships
user = relationship("User", back_populates="connection_logs")
gateway = relationship("Gateway", back_populates="connection_logs")
endpoint = relationship("Endpoint", back_populates="connection_logs")
def __repr__(self):
return f"<ConnectionLog(user={self.user_id}, gateway={self.gateway_id}, connected={self.connected_at})>"
@property
def duration_seconds(self) -> int | None:
"""Get connection duration in seconds."""
if self.disconnected_at:
return int((self.disconnected_at - self.connected_at).total_seconds())
return None
@@ -0,0 +1,99 @@
"""Certificate Authority model for PKI management."""
from datetime import datetime
from enum import Enum as PyEnum
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Enum, Text
from sqlalchemy.orm import relationship
from ..database import Base
class CAStatus(str, PyEnum):
"""Certificate Authority status."""
PENDING = "pending" # DH parameters being generated
ACTIVE = "active"
EXPIRED = "expired"
REVOKED = "revoked"
class CAAlgorithm(str, PyEnum):
"""Key algorithm for CA."""
RSA = "rsa"
ECDSA = "ecdsa"
class CertificateAuthority(Base):
"""Certificate Authority for issuing VPN certificates."""
__tablename__ = "certificate_authorities"
id = Column(Integer, primary_key=True, index=True)
tenant_id = Column(Integer, ForeignKey("tenants.id"), nullable=True) # NULL for global CA
name = Column(String(255), nullable=False)
description = Column(Text, nullable=True)
# Certificate data (PEM encoded)
ca_cert = Column(Text, nullable=True) # CA certificate
ca_key = Column(Text, nullable=True) # CA private key (encrypted)
# DH parameters (shared across servers using this CA)
dh_params = Column(Text, nullable=True) # Pre-generated DH parameters
dh_generating = Column(Boolean, default=False) # DH generation in progress
# Key configuration
key_size = Column(Integer, default=4096)
algorithm = Column(Enum(CAAlgorithm), default=CAAlgorithm.RSA)
# Validity
valid_from = Column(DateTime, nullable=True)
valid_until = Column(DateTime, nullable=True)
# Status
is_default = Column(Boolean, default=False) # Default CA for new certificates
status = Column(Enum(CAStatus), default=CAStatus.PENDING)
# CRL (Certificate Revocation List)
crl = Column(Text, nullable=True)
crl_updated_at = Column(DateTime, nullable=True)
# Serial number tracking
next_serial = Column(Integer, default=1)
# Audit
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
created_by_id = Column(Integer, ForeignKey("users.id"), nullable=True)
# Relationships
tenant = relationship("Tenant", back_populates="certificate_authorities")
created_by = relationship("User", foreign_keys=[created_by_id])
vpn_servers = relationship("VPNServer", back_populates="certificate_authority")
vpn_profiles = relationship("VPNProfile", back_populates="certificate_authority")
def __repr__(self):
return f"<CertificateAuthority(id={self.id}, name='{self.name}', status='{self.status}')>"
@property
def is_ready(self) -> bool:
"""Check if CA is ready for issuing certificates."""
return (
self.status == CAStatus.ACTIVE and
self.ca_cert is not None and
self.ca_key is not None and
self.dh_params is not None
)
@property
def is_expired(self) -> bool:
"""Check if CA certificate is expired."""
if self.valid_until:
return datetime.utcnow() > self.valid_until
return False
@property
def days_until_expiry(self) -> int | None:
"""Days until CA expires."""
if self.valid_until:
delta = self.valid_until - datetime.utcnow()
return max(0, delta.days)
return None
+87
View File
@@ -0,0 +1,87 @@
"""Endpoint model for devices behind gateways."""
from datetime import datetime
from enum import Enum as PyEnum
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Enum, Text
from sqlalchemy.orm import relationship
from ..database import Base
class Protocol(str, PyEnum):
"""Network protocol for endpoint access."""
TCP = "tcp"
UDP = "udp"
class Endpoint(Base):
"""Endpoint model representing a device/service behind a gateway."""
__tablename__ = "endpoints"
id = Column(Integer, primary_key=True, index=True)
gateway_id = Column(Integer, ForeignKey("gateways.id"), nullable=False)
# Endpoint info
name = Column(String(255), nullable=False)
description = Column(Text, nullable=True)
# Network configuration
internal_ip = Column(String(45), nullable=False) # IP in customer network
port = Column(Integer, nullable=False)
protocol = Column(Enum(Protocol), default=Protocol.TCP, nullable=False)
# Application info
application_name = Column(String(100), nullable=True) # e.g., "CoDeSys", "SSH", "HTTP"
application_template_id = Column(Integer, ForeignKey("application_templates.id"), nullable=True)
# Timestamps
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Relationships
gateway = relationship("Gateway", back_populates="endpoints")
user_access = relationship("UserEndpointAccess", back_populates="endpoint", cascade="all, delete-orphan")
application_template = relationship("ApplicationTemplate")
connection_logs = relationship("ConnectionLog", back_populates="endpoint")
def __repr__(self):
return f"<Endpoint(id={self.id}, name='{self.name}', ip='{self.internal_ip}:{self.port}')>"
@property
def address(self) -> str:
"""Get full address string."""
return f"{self.internal_ip}:{self.port}"
class ApplicationTemplate(Base):
"""Pre-defined application templates with default ports."""
__tablename__ = "application_templates"
id = Column(Integer, primary_key=True, index=True)
name = Column(String(100), nullable=False, unique=True)
description = Column(Text, nullable=True)
default_port = Column(Integer, nullable=False)
protocol = Column(Enum(Protocol), default=Protocol.TCP, nullable=False)
icon = Column(String(100), nullable=True) # Icon name for client UI
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
def __repr__(self):
return f"<ApplicationTemplate(name='{self.name}', port={self.default_port})>"
# Default application templates to be seeded
DEFAULT_APPLICATION_TEMPLATES = [
{"name": "CoDeSys", "description": "CoDeSys Runtime/Gateway", "default_port": 11740, "protocol": "tcp"},
{"name": "CoDeSys Gateway", "description": "CoDeSys Gateway Service", "default_port": 1217, "protocol": "tcp"},
{"name": "SSH", "description": "Secure Shell", "default_port": 22, "protocol": "tcp"},
{"name": "HTTP", "description": "Web Interface", "default_port": 80, "protocol": "tcp"},
{"name": "HTTPS", "description": "Secure Web Interface", "default_port": 443, "protocol": "tcp"},
{"name": "VNC", "description": "Virtual Network Computing", "default_port": 5900, "protocol": "tcp"},
{"name": "RDP", "description": "Remote Desktop Protocol", "default_port": 3389, "protocol": "tcp"},
{"name": "Modbus TCP", "description": "Modbus over TCP/IP", "default_port": 502, "protocol": "tcp"},
{"name": "OPC UA", "description": "OPC Unified Architecture", "default_port": 4840, "protocol": "tcp"},
{"name": "MQTT", "description": "Message Queue Telemetry Transport", "default_port": 1883, "protocol": "tcp"},
{"name": "S7 Communication", "description": "Siemens S7 Protocol", "default_port": 102, "protocol": "tcp"},
]
+91
View File
@@ -0,0 +1,91 @@
"""Gateway model for mGuard routers."""
from datetime import datetime
from enum import Enum as PyEnum
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Enum, Text
from sqlalchemy.orm import relationship
from ..database import Base
class RouterType(str, PyEnum):
"""Supported mGuard router types."""
FL_MGUARD_2000 = "FL_MGUARD_2000"
FL_MGUARD_4000 = "FL_MGUARD_4000"
FL_MGUARD_RS4000 = "FL_MGUARD_RS4000"
FL_MGUARD_1000 = "FL_MGUARD_1000"
class ProvisioningMethod(str, PyEnum):
"""Method used to provision the gateway."""
REST_API = "rest_api" # For firmware 10.5.x+
SSH = "ssh" # For older firmware
ATV_FILE = "atv_file" # Offline via config file
class Gateway(Base):
"""Gateway model representing an mGuard router."""
__tablename__ = "gateways"
id = Column(Integer, primary_key=True, index=True)
tenant_id = Column(Integer, ForeignKey("tenants.id"), nullable=False)
# Basic info
name = Column(String(255), nullable=False)
description = Column(Text, nullable=True)
location = Column(String(255), nullable=True) # Physical location
# Router details
serial_number = Column(String(100), nullable=True, unique=True)
router_type = Column(Enum(RouterType), nullable=False)
firmware_version = Column(String(50), nullable=True)
provisioning_method = Column(Enum(ProvisioningMethod), default=ProvisioningMethod.ATV_FILE)
# VPN configuration
vpn_ip = Column(String(45), nullable=True, unique=True) # IPv4/IPv6
vpn_cert_cn = Column(String(255), nullable=True, unique=True) # Certificate Common Name
vpn_subnet = Column(String(50), nullable=True) # Network behind gateway, e.g., "10.0.0.0/24"
# Status
is_online = Column(Boolean, default=False, nullable=False)
is_provisioned = Column(Boolean, default=False, nullable=False)
last_seen = Column(DateTime, nullable=True)
last_config_update = Column(DateTime, nullable=True)
# Timestamps
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Relationships
tenant = relationship("Tenant", back_populates="gateways")
endpoints = relationship("Endpoint", back_populates="gateway", cascade="all, delete-orphan")
user_access = relationship("UserGatewayAccess", back_populates="gateway", cascade="all, delete-orphan")
connection_logs = relationship("ConnectionLog", back_populates="gateway")
vpn_profiles = relationship("VPNProfile", back_populates="gateway", cascade="all, delete-orphan", order_by="VPNProfile.priority")
def __repr__(self):
return f"<Gateway(id={self.id}, name='{self.name}', type='{self.router_type}')>"
@property
def supports_rest_api(self) -> bool:
"""Check if gateway supports REST API (firmware 10.x+)."""
if not self.firmware_version:
return False
try:
major_version = int(self.firmware_version.split('.')[0])
return major_version >= 10
except (ValueError, IndexError):
return False
@property
def primary_profile(self):
"""Get the primary VPN profile (highest priority)."""
active_profiles = [p for p in self.vpn_profiles if p.is_active]
if active_profiles:
return min(active_profiles, key=lambda p: p.priority)
return None
@property
def has_vpn_profiles(self) -> bool:
"""Check if gateway has any VPN profiles."""
return len(self.vpn_profiles) > 0
+28
View File
@@ -0,0 +1,28 @@
"""Tenant model for multi-tenant support."""
from datetime import datetime
from sqlalchemy import Column, Integer, String, Boolean, DateTime, Text
from sqlalchemy.orm import relationship
from ..database import Base
class Tenant(Base):
"""Tenant/Customer model for multi-tenant separation."""
__tablename__ = "tenants"
id = Column(Integer, primary_key=True, index=True)
name = Column(String(255), nullable=False, unique=True)
description = Column(Text, nullable=True)
is_active = Column(Boolean, default=True, nullable=False)
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Relationships
users = relationship("User", back_populates="tenant", cascade="all, delete-orphan")
gateways = relationship("Gateway", back_populates="tenant", cascade="all, delete-orphan")
certificate_authorities = relationship("CertificateAuthority", back_populates="tenant", cascade="all, delete-orphan")
vpn_servers = relationship("VPNServer", back_populates="tenant", cascade="all, delete-orphan")
def __repr__(self):
return f"<Tenant(id={self.id}, name='{self.name}')>"
+62
View File
@@ -0,0 +1,62 @@
"""User model with role-based access control."""
from datetime import datetime
from enum import Enum as PyEnum
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Enum
from sqlalchemy.orm import relationship
from ..database import Base
class UserRole(str, PyEnum):
"""User roles for access control."""
SUPER_ADMIN = "super_admin" # Can manage all tenants
ADMIN = "admin" # Can manage own tenant
TECHNICIAN = "technician" # Can connect to assigned gateways
VIEWER = "viewer" # Read-only access
class User(Base):
"""User model with multi-tenant support."""
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
tenant_id = Column(Integer, ForeignKey("tenants.id"), nullable=True) # NULL for super_admin
username = Column(String(255), nullable=False, unique=True, index=True)
password_hash = Column(String(255), nullable=False)
email = Column(String(255), nullable=False, unique=True)
full_name = Column(String(255), nullable=True)
role = Column(Enum(UserRole), default=UserRole.TECHNICIAN, nullable=False)
is_active = Column(Boolean, default=True, nullable=False)
last_login = Column(DateTime, nullable=True)
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Relationships
tenant = relationship("Tenant", back_populates="users")
gateway_access = relationship(
"UserGatewayAccess",
back_populates="user",
cascade="all, delete-orphan",
primaryjoin="User.id == UserGatewayAccess.user_id"
)
endpoint_access = relationship(
"UserEndpointAccess",
back_populates="user",
cascade="all, delete-orphan",
primaryjoin="User.id == UserEndpointAccess.user_id"
)
connection_logs = relationship("ConnectionLog", back_populates="user")
def __repr__(self):
return f"<User(id={self.id}, username='{self.username}', role='{self.role}')>"
@property
def is_super_admin(self) -> bool:
"""Check if user is super admin."""
return self.role == UserRole.SUPER_ADMIN
@property
def is_admin(self) -> bool:
"""Check if user has admin privileges."""
return self.role in (UserRole.SUPER_ADMIN, UserRole.ADMIN)
+50
View File
@@ -0,0 +1,50 @@
"""VPN Connection Log model for tracking profile connection history."""
from datetime import datetime
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, BigInteger
from sqlalchemy.orm import relationship
from ..database import Base
class VPNConnectionLog(Base):
"""Log of VPN profile connections."""
__tablename__ = "vpn_connection_logs"
id = Column(Integer, primary_key=True, index=True)
vpn_profile_id = Column(Integer, ForeignKey("vpn_profiles.id"), nullable=False)
vpn_server_id = Column(Integer, ForeignKey("vpn_servers.id"), nullable=False)
gateway_id = Column(Integer, ForeignKey("gateways.id"), nullable=False)
# Connection info
common_name = Column(String(255), nullable=False)
real_address = Column(String(255), nullable=True) # IP:Port
vpn_ip = Column(String(15), nullable=True) # Assigned VPN IP
# Timestamps
connected_at = Column(DateTime, default=datetime.utcnow, nullable=False)
disconnected_at = Column(DateTime, nullable=True)
# Traffic stats (updated on disconnect)
bytes_received = Column(BigInteger, default=0)
bytes_sent = Column(BigInteger, default=0)
# Relationships
vpn_profile = relationship("VPNProfile")
vpn_server = relationship("VPNServer")
gateway = relationship("Gateway")
def __repr__(self):
return f"<VPNConnectionLog(id={self.id}, cn='{self.common_name}', connected={self.connected_at})>"
@property
def duration_seconds(self) -> int | None:
"""Connection duration in seconds."""
if self.disconnected_at:
return int((self.disconnected_at - self.connected_at).total_seconds())
return None
@property
def is_active(self) -> bool:
"""Check if connection is still active."""
return self.disconnected_at is None
+90
View File
@@ -0,0 +1,90 @@
"""VPN Profile model for gateway VPN configurations."""
from datetime import datetime
from enum import Enum as PyEnum
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Enum, Text
from sqlalchemy.orm import relationship
from ..database import Base
class VPNProfileStatus(str, PyEnum):
"""VPN Profile status."""
PENDING = "pending" # Certificate being generated
ACTIVE = "active" # Ready to use
PROVISIONED = "provisioned" # Downloaded/deployed to gateway
EXPIRED = "expired" # Certificate expired
REVOKED = "revoked" # Certificate revoked
class VPNProfile(Base):
"""VPN Profile for a gateway - links gateway to VPN server."""
__tablename__ = "vpn_profiles"
id = Column(Integer, primary_key=True, index=True)
gateway_id = Column(Integer, ForeignKey("gateways.id"), nullable=False)
vpn_server_id = Column(Integer, ForeignKey("vpn_servers.id"), nullable=False)
ca_id = Column(Integer, ForeignKey("certificate_authorities.id"), nullable=False)
# Profile info
name = Column(String(255), nullable=False) # e.g., "Produktion", "Fallback"
description = Column(Text, nullable=True)
# Certificate data
cert_cn = Column(String(255), nullable=False) # Common Name
client_cert = Column(Text, nullable=True) # Client certificate PEM
client_key = Column(Text, nullable=True) # Client private key PEM
# Priority for failover (1 = highest priority)
priority = Column(Integer, default=1)
# Status
status = Column(Enum(VPNProfileStatus), default=VPNProfileStatus.PENDING)
is_active = Column(Boolean, default=True)
# Validity
valid_from = Column(DateTime, nullable=True)
valid_until = Column(DateTime, nullable=True)
# Provisioning tracking
provisioned_at = Column(DateTime, nullable=True)
last_connection = Column(DateTime, nullable=True)
# VPN IP assigned to this profile (if static)
vpn_ip = Column(String(15), nullable=True)
# Audit
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Relationships
gateway = relationship("Gateway", back_populates="vpn_profiles")
vpn_server = relationship("VPNServer", back_populates="vpn_profiles")
certificate_authority = relationship("CertificateAuthority", back_populates="vpn_profiles")
def __repr__(self):
return f"<VPNProfile(id={self.id}, name='{self.name}', gateway_id={self.gateway_id}, priority={self.priority})>"
@property
def is_ready(self) -> bool:
"""Check if profile is ready for provisioning."""
return (
self.status in (VPNProfileStatus.ACTIVE, VPNProfileStatus.PROVISIONED) and
self.client_cert is not None and
self.client_key is not None
)
@property
def is_expired(self) -> bool:
"""Check if certificate is expired."""
if self.valid_until:
return datetime.utcnow() > self.valid_until
return False
@property
def days_until_expiry(self) -> int | None:
"""Days until certificate expires."""
if self.valid_until:
delta = self.valid_until - datetime.utcnow()
return max(0, delta.days)
return None
+130
View File
@@ -0,0 +1,130 @@
"""VPN Server model for managing multiple OpenVPN instances."""
from datetime import datetime
from enum import Enum as PyEnum
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Enum, Text
from sqlalchemy.orm import relationship
from ..database import Base
class VPNProtocol(str, PyEnum):
"""VPN protocol type."""
UDP = "udp"
TCP = "tcp"
class VPNCipher(str, PyEnum):
"""VPN cipher options."""
AES_256_GCM = "AES-256-GCM"
AES_128_GCM = "AES-128-GCM"
AES_256_CBC = "AES-256-CBC"
CHACHA20_POLY1305 = "CHACHA20-POLY1305"
class VPNAuth(str, PyEnum):
"""VPN auth digest options."""
SHA256 = "SHA256"
SHA384 = "SHA384"
SHA512 = "SHA512"
class VPNCompression(str, PyEnum):
"""VPN compression options."""
NONE = "none"
LZ4 = "lz4"
LZ4_V2 = "lz4-v2"
LZO = "lzo"
class VPNServerStatus(str, PyEnum):
"""VPN Server status."""
PENDING = "pending" # Server created but not started
STARTING = "starting" # Container starting
RUNNING = "running" # Server running
STOPPED = "stopped" # Server stopped
ERROR = "error" # Error state
class VPNServer(Base):
"""VPN Server instance configuration."""
__tablename__ = "vpn_servers"
id = Column(Integer, primary_key=True, index=True)
tenant_id = Column(Integer, ForeignKey("tenants.id"), nullable=True) # NULL for global
ca_id = Column(Integer, ForeignKey("certificate_authorities.id"), nullable=False)
# Basic info
name = Column(String(255), nullable=False)
description = Column(Text, nullable=True)
# Network configuration
hostname = Column(String(255), nullable=False) # External hostname/IP
port = Column(Integer, default=1194, nullable=False)
protocol = Column(Enum(VPNProtocol), default=VPNProtocol.UDP, nullable=False)
# VPN network
vpn_network = Column(String(18), default="10.8.0.0", nullable=False) # CIDR or IP
vpn_netmask = Column(String(15), default="255.255.255.0", nullable=False)
# Server certificate (PEM encoded)
server_cert = Column(Text, nullable=True)
server_key = Column(Text, nullable=True)
ta_key = Column(Text, nullable=True) # TLS-Auth key
# Security settings
cipher = Column(Enum(VPNCipher), default=VPNCipher.AES_256_GCM)
auth = Column(Enum(VPNAuth), default=VPNAuth.SHA256)
tls_version_min = Column(String(10), default="1.2")
tls_auth_enabled = Column(Boolean, default=True)
# Performance settings
compression = Column(Enum(VPNCompression), default=VPNCompression.NONE)
max_clients = Column(Integer, default=100)
keepalive_interval = Column(Integer, default=10) # seconds
keepalive_timeout = Column(Integer, default=60) # seconds
# Docker settings
docker_container_name = Column(String(255), nullable=True)
management_port = Column(Integer, default=7505)
# Status
status = Column(Enum(VPNServerStatus), default=VPNServerStatus.PENDING)
is_active = Column(Boolean, default=True)
is_primary = Column(Boolean, default=False) # Primary server for tenant
auto_start = Column(Boolean, default=True) # Start on system boot
# Statistics (updated periodically)
connected_clients = Column(Integer, default=0)
last_status_check = Column(DateTime, nullable=True)
# Audit
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Relationships
tenant = relationship("Tenant", back_populates="vpn_servers")
certificate_authority = relationship("CertificateAuthority", back_populates="vpn_servers")
vpn_profiles = relationship("VPNProfile", back_populates="vpn_server")
def __repr__(self):
return f"<VPNServer(id={self.id}, name='{self.name}', {self.hostname}:{self.port}/{self.protocol.value})>"
@property
def is_ready(self) -> bool:
"""Check if server is ready to accept connections."""
return (
self.server_cert is not None and
self.server_key is not None and
self.certificate_authority is not None and
self.certificate_authority.is_ready
)
@property
def connection_string(self) -> str:
"""Get connection string for display."""
return f"{self.hostname}:{self.port}/{self.protocol.value.upper()}"
def get_docker_port_mapping(self) -> dict:
"""Get Docker port mapping for this server."""
return {f"{self.port}/{self.protocol.value}": self.port}