Multi-interface management, topology view, performance tuning, precompiled CSS
- Interface model with per-interface subnet/port/keys; import/adopt existing wg-quick configs (key-less imported peers, optional key rotation), cascade delete - Split wireguard.py into a package (keys via cryptography X25519, status, addressing, conf parse/render, sync, host tuning) - ECharts horizontal topology view (interface -> peers -> site subnets) - Advanced options: MTU, MSS clamping, FwMark/Table, custom PostUp/PostDown, per-peer keepalive override - Runtime settings (sample interval/retention, online threshold, UI refresh) with traffic sample pruning; host tuning (UDP buffers, backlog, GRO forwarding) - Precompiled Tailwind CSS replacing Play CDN runtime (fixes FOUC); stable table layout and diffed polling renders - Host network mode in compose; NAT/isolation iptables moved into app sync Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
parent
192960ad3e
commit
1b227c2470
+433
-46
@@ -1,15 +1,376 @@
|
||||
from datetime import datetime, timezone
|
||||
import ipaddress
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from . import wireguard
|
||||
from .config import settings
|
||||
from .crypto import encrypt
|
||||
from .models import Peer, TrafficSample
|
||||
from .models import AppSetting, Interface, Peer, TrafficSample
|
||||
|
||||
RUNTIME_DEFAULTS = {
|
||||
"traffic_sample_interval": "60",
|
||||
"traffic_retention_days": "30",
|
||||
"online_threshold": "180",
|
||||
"ui_refresh_seconds": "5",
|
||||
}
|
||||
|
||||
|
||||
def list_peers(db: Session) -> list[Peer]:
|
||||
return list(db.scalars(select(Peer).order_by(Peer.id)))
|
||||
def get_runtime_settings(db: Session) -> dict[str, int]:
|
||||
stored = {s.key: s.value for s in db.scalars(select(AppSetting))}
|
||||
result = {}
|
||||
for key, default in RUNTIME_DEFAULTS.items():
|
||||
try:
|
||||
result[key] = int(stored.get(key, default))
|
||||
except ValueError:
|
||||
result[key] = int(default)
|
||||
return result
|
||||
|
||||
|
||||
def update_runtime_settings(db: Session, values: dict[str, int]) -> None:
|
||||
bounds = {
|
||||
"traffic_sample_interval": (10, 3600),
|
||||
"traffic_retention_days": (0, 3650),
|
||||
"online_threshold": (30, 3600),
|
||||
"ui_refresh_seconds": (2, 300),
|
||||
}
|
||||
for key, (low, high) in bounds.items():
|
||||
if key not in values:
|
||||
continue
|
||||
value = values[key]
|
||||
if not low <= value <= high:
|
||||
raise ValueError(f"{key} must be between {low} and {high}")
|
||||
setting = db.get(AppSetting, key)
|
||||
if setting is None:
|
||||
db.add(AppSetting(key=key, value=str(value)))
|
||||
else:
|
||||
setting.value = str(value)
|
||||
db.commit()
|
||||
wireguard.status_module.online_threshold_seconds = get_runtime_settings(db)[
|
||||
"online_threshold"
|
||||
]
|
||||
|
||||
|
||||
def prune_traffic_samples(db: Session) -> int:
|
||||
retention_days = get_runtime_settings(db)["traffic_retention_days"]
|
||||
if retention_days <= 0:
|
||||
return 0
|
||||
cutoff = datetime.now(timezone.utc) - timedelta(days=retention_days)
|
||||
result = db.execute(
|
||||
TrafficSample.__table__.delete().where(TrafficSample.sampled_at < cutoff)
|
||||
)
|
||||
db.commit()
|
||||
return result.rowcount or 0
|
||||
|
||||
|
||||
def list_interfaces(db: Session) -> list[Interface]:
|
||||
return list(db.scalars(select(Interface).order_by(Interface.id)))
|
||||
|
||||
|
||||
def get_interface(db: Session, interface_id: int) -> Interface | None:
|
||||
return db.get(Interface, interface_id)
|
||||
|
||||
|
||||
def get_interface_by_name(db: Session, name: str) -> Interface | None:
|
||||
return db.scalar(select(Interface).where(Interface.name == name))
|
||||
|
||||
|
||||
def _check_interface_conflicts(
|
||||
db: Session, name: str, subnet: str, listen_port: int, exclude_id: int | None = None
|
||||
) -> None:
|
||||
for other in list_interfaces(db):
|
||||
if other.id == exclude_id:
|
||||
continue
|
||||
if other.name == name:
|
||||
raise ValueError(f"Interface {name} already exists")
|
||||
if other.listen_port == listen_port:
|
||||
raise ValueError(f"Port {listen_port} is already used by {other.name}")
|
||||
if ipaddress.ip_network(subnet).overlaps(ipaddress.ip_network(other.subnet)):
|
||||
raise ValueError(f"Subnet {subnet} overlaps {other.name} ({other.subnet})")
|
||||
|
||||
|
||||
def create_interface(
|
||||
db: Session,
|
||||
name: str,
|
||||
subnet: str,
|
||||
listen_port: int,
|
||||
host: str,
|
||||
dns: str = "",
|
||||
allowed_ips: str = "",
|
||||
persistent_keepalive: int = 25,
|
||||
peer_isolation: bool = False,
|
||||
) -> Interface:
|
||||
name = wireguard.validate_interface_name(name)
|
||||
subnet = wireguard.validate_subnet(subnet)
|
||||
if not 1 <= listen_port <= 65535:
|
||||
raise ValueError(f"Invalid port: {listen_port}")
|
||||
host = host.strip()
|
||||
if not host:
|
||||
raise ValueError("Endpoint host is required")
|
||||
_check_interface_conflicts(db, name, subnet, listen_port)
|
||||
if name in wireguard.system_interfaces():
|
||||
raise ValueError(
|
||||
f"Interface {name} is already running on this host. "
|
||||
"Import it instead of creating a new one."
|
||||
)
|
||||
private, public = wireguard.generate_keypair()
|
||||
iface = Interface(
|
||||
name=name,
|
||||
subnet=subnet,
|
||||
listen_port=listen_port,
|
||||
host=host,
|
||||
dns=dns.strip() or settings.wg_dns,
|
||||
allowed_ips=wireguard.validate_cidr_list(allowed_ips) or settings.wg_allowed_ips,
|
||||
persistent_keepalive=persistent_keepalive,
|
||||
peer_isolation=peer_isolation,
|
||||
private_key_enc=encrypt(private),
|
||||
public_key=public,
|
||||
)
|
||||
db.add(iface)
|
||||
db.commit()
|
||||
apply_config(db, iface)
|
||||
try:
|
||||
wireguard.interface_up(iface)
|
||||
except Exception:
|
||||
pass
|
||||
return iface
|
||||
|
||||
|
||||
def _validate_mtu(mtu: int) -> int:
|
||||
if mtu and not 1280 <= mtu <= 1500:
|
||||
raise ValueError("MTU must be between 1280 and 1500 (0 = default)")
|
||||
return mtu
|
||||
|
||||
|
||||
def _validate_mark(value: str, label: str) -> str:
|
||||
value = value.strip()
|
||||
if value and not value.replace("x", "").replace("X", "").isalnum():
|
||||
raise ValueError(f"Invalid {label}: {value}")
|
||||
return value
|
||||
|
||||
|
||||
def update_interface(
|
||||
db: Session,
|
||||
iface: Interface,
|
||||
host: str,
|
||||
dns: str,
|
||||
allowed_ips: str,
|
||||
persistent_keepalive: int,
|
||||
peer_isolation: bool,
|
||||
mtu: int = 0,
|
||||
mss_clamp: bool = False,
|
||||
fwmark: str = "",
|
||||
route_table: str = "",
|
||||
post_up: str = "",
|
||||
post_down: str = "",
|
||||
) -> Interface:
|
||||
host = host.strip()
|
||||
if not host:
|
||||
raise ValueError("Endpoint host is required")
|
||||
iface.host = host
|
||||
iface.dns = dns.strip() or settings.wg_dns
|
||||
iface.allowed_ips = (
|
||||
wireguard.validate_cidr_list(allowed_ips) or settings.wg_allowed_ips
|
||||
)
|
||||
iface.persistent_keepalive = persistent_keepalive
|
||||
iface.peer_isolation = peer_isolation
|
||||
iface.mtu = _validate_mtu(mtu)
|
||||
iface.mss_clamp = mss_clamp
|
||||
iface.fwmark = _validate_mark(fwmark, "FwMark")
|
||||
iface.route_table = _validate_mark(route_table, "Table")
|
||||
iface.post_up = post_up.strip()
|
||||
iface.post_down = post_down.strip()
|
||||
db.commit()
|
||||
apply_config(db, iface)
|
||||
return iface
|
||||
|
||||
|
||||
def toggle_interface(db: Session, iface: Interface) -> Interface:
|
||||
if iface.enabled:
|
||||
wireguard.interface_down(iface)
|
||||
iface.enabled = False
|
||||
else:
|
||||
iface.enabled = True
|
||||
apply_config(db, iface)
|
||||
wireguard.interface_up(iface)
|
||||
db.commit()
|
||||
return iface
|
||||
|
||||
|
||||
def delete_interface(db: Session, iface: Interface, cascade: bool = False) -> None:
|
||||
if iface.peers and not cascade:
|
||||
raise ValueError(
|
||||
f"Interface {iface.name} still has {len(iface.peers)} peers. "
|
||||
"Delete them first or use cascade delete."
|
||||
)
|
||||
try:
|
||||
wireguard.interface_down(iface)
|
||||
except Exception:
|
||||
pass
|
||||
wireguard.backup_config(iface.name)
|
||||
path = wireguard.config_path(iface.name)
|
||||
if path.exists():
|
||||
path.unlink()
|
||||
db.delete(iface)
|
||||
db.commit()
|
||||
|
||||
|
||||
@dataclass
|
||||
class ImportCandidate:
|
||||
name: str
|
||||
has_config: bool
|
||||
running: bool
|
||||
peer_count: int
|
||||
listen_port: int | None
|
||||
address: str
|
||||
|
||||
|
||||
def import_candidates(db: Session) -> list[ImportCandidate]:
|
||||
known = {iface.name for iface in list_interfaces(db)}
|
||||
names = set(wireguard.discover_configs()) | set(wireguard.system_interfaces())
|
||||
candidates = []
|
||||
for name in sorted(names - known):
|
||||
parsed = None
|
||||
path = wireguard.config_path(name)
|
||||
if path.exists():
|
||||
try:
|
||||
parsed = wireguard.parse_config(path.read_text())
|
||||
except Exception:
|
||||
parsed = None
|
||||
status = wireguard.get_status(name)
|
||||
candidates.append(
|
||||
ImportCandidate(
|
||||
name=name,
|
||||
has_config=parsed is not None and bool(parsed.private_key),
|
||||
running=status.up,
|
||||
peer_count=len(parsed.peers) if parsed else len(status.peers),
|
||||
listen_port=(parsed.listen_port if parsed else None) or status.listen_port,
|
||||
address=parsed.address if parsed else "",
|
||||
)
|
||||
)
|
||||
return candidates
|
||||
|
||||
|
||||
def import_interface(db: Session, name: str, host: str) -> Interface:
|
||||
name = wireguard.validate_interface_name(name)
|
||||
if get_interface_by_name(db, name) is not None:
|
||||
raise ValueError(f"Interface {name} is already managed")
|
||||
path = wireguard.config_path(name)
|
||||
if not path.exists():
|
||||
raise ValueError(
|
||||
f"No config file at {path}. Only wg-quick configs can be imported."
|
||||
)
|
||||
parsed = wireguard.parse_config(path.read_text())
|
||||
if not parsed.private_key:
|
||||
raise ValueError(f"{path} has no PrivateKey")
|
||||
if not parsed.address:
|
||||
raise ValueError(f"{path} has no Address")
|
||||
if not parsed.listen_port:
|
||||
raise ValueError(f"{path} has no ListenPort")
|
||||
host = host.strip()
|
||||
if not host:
|
||||
raise ValueError("Endpoint host is required")
|
||||
|
||||
network = ipaddress.ip_interface(parsed.address).network
|
||||
subnet = str(network)
|
||||
_check_interface_conflicts(db, name, subnet, parsed.listen_port)
|
||||
|
||||
wireguard.backup_config(name)
|
||||
iface = Interface(
|
||||
name=name,
|
||||
subnet=subnet,
|
||||
listen_port=parsed.listen_port,
|
||||
host=host,
|
||||
dns=settings.wg_dns,
|
||||
allowed_ips=settings.wg_allowed_ips,
|
||||
persistent_keepalive=settings.wg_persistent_keepalive,
|
||||
private_key_enc=encrypt(parsed.private_key),
|
||||
public_key=wireguard.pubkey(parsed.private_key),
|
||||
imported=True,
|
||||
)
|
||||
db.add(iface)
|
||||
db.flush()
|
||||
|
||||
used_names: set[str] = set()
|
||||
index = 1
|
||||
for parsed_peer in parsed.peers:
|
||||
address = ""
|
||||
extra: list[str] = []
|
||||
for cidr in parsed_peer.allowed_ips:
|
||||
try:
|
||||
net = ipaddress.ip_network(cidr, strict=False)
|
||||
except ValueError:
|
||||
continue
|
||||
if not address and net.prefixlen == net.max_prefixlen and net.network_address in network:
|
||||
address = f"{net.network_address}/{net.max_prefixlen}"
|
||||
else:
|
||||
extra.append(str(net))
|
||||
if not address:
|
||||
address = wireguard.next_free_address(
|
||||
subnet, [p.address for p in iface.peers]
|
||||
)
|
||||
peer_name = parsed_peer.name.strip() or f"imported-{index}"
|
||||
while peer_name in used_names:
|
||||
index += 1
|
||||
peer_name = f"imported-{index}"
|
||||
used_names.add(peer_name)
|
||||
index += 1
|
||||
iface.peers.append(
|
||||
Peer(
|
||||
name=peer_name,
|
||||
public_key=parsed_peer.public_key,
|
||||
private_key_enc="",
|
||||
preshared_key_enc=(
|
||||
encrypt(parsed_peer.preshared_key)
|
||||
if parsed_peer.preshared_key
|
||||
else ""
|
||||
),
|
||||
address=address,
|
||||
extra_allowed_ips=", ".join(extra),
|
||||
)
|
||||
)
|
||||
db.commit()
|
||||
apply_config(db, iface)
|
||||
return iface
|
||||
|
||||
|
||||
def bootstrap_default_interface(db: Session) -> None:
|
||||
if list_interfaces(db):
|
||||
return
|
||||
private_path = settings.data_dir / "server" / "privatekey"
|
||||
if private_path.exists():
|
||||
private = private_path.read_text().strip()
|
||||
else:
|
||||
private = wireguard.genkey()
|
||||
iface = Interface(
|
||||
name=settings.wg_interface,
|
||||
subnet=settings.wg_subnet,
|
||||
listen_port=settings.wg_port,
|
||||
host=settings.wg_host,
|
||||
dns=settings.wg_dns,
|
||||
allowed_ips=settings.wg_allowed_ips,
|
||||
persistent_keepalive=settings.wg_persistent_keepalive,
|
||||
peer_isolation=settings.wg_peer_isolation,
|
||||
private_key_enc=encrypt(private),
|
||||
public_key=wireguard.pubkey(private),
|
||||
)
|
||||
db.add(iface)
|
||||
db.commit()
|
||||
db.execute(
|
||||
Peer.__table__.update()
|
||||
.where(Peer.interface_id.notin_(select(Interface.id)))
|
||||
.values(interface_id=iface.id)
|
||||
)
|
||||
db.commit()
|
||||
|
||||
|
||||
def list_peers(db: Session, iface: Interface | None = None) -> list[Peer]:
|
||||
query = select(Peer).order_by(Peer.id)
|
||||
if iface is not None:
|
||||
query = query.where(Peer.interface_id == iface.id)
|
||||
return list(db.scalars(query))
|
||||
|
||||
|
||||
def get_peer(db: Session, peer_id: int) -> Peer | None:
|
||||
@@ -18,6 +379,7 @@ def get_peer(db: Session, peer_id: int) -> Peer | None:
|
||||
|
||||
def create_peer(
|
||||
db: Session,
|
||||
iface: Interface,
|
||||
name: str,
|
||||
expires_at: datetime | None = None,
|
||||
note: str = "",
|
||||
@@ -27,14 +389,17 @@ def create_peer(
|
||||
extra_allowed_ips: str = "",
|
||||
client_allowed_ips: str = "",
|
||||
) -> Peer:
|
||||
if any(p.name == name for p in list_peers(db, iface)):
|
||||
raise ValueError(f"Peer {name} already exists on {iface.name}")
|
||||
private, public = wireguard.generate_keypair()
|
||||
psk = wireguard.genpsk()
|
||||
taken = [p.address for p in list_peers(db)]
|
||||
taken = [p.address for p in list_peers(db, iface)]
|
||||
if address:
|
||||
address = wireguard.validate_address(address, taken)
|
||||
address = wireguard.validate_address(address, iface.subnet, taken)
|
||||
else:
|
||||
address = wireguard.next_free_address(taken)
|
||||
address = wireguard.next_free_address(iface.subnet, taken)
|
||||
peer = Peer(
|
||||
interface_id=iface.id,
|
||||
name=name,
|
||||
public_key=public,
|
||||
private_key_enc=encrypt(private),
|
||||
@@ -49,19 +414,20 @@ def create_peer(
|
||||
)
|
||||
db.add(peer)
|
||||
db.commit()
|
||||
apply_config(db)
|
||||
apply_config(db, iface)
|
||||
return peer
|
||||
|
||||
|
||||
def create_peers_batch(
|
||||
db: Session,
|
||||
iface: Interface,
|
||||
base_name: str,
|
||||
count: int,
|
||||
expires_at: datetime | None = None,
|
||||
note: str = "",
|
||||
quota_bytes: int = 0,
|
||||
) -> list[Peer]:
|
||||
existing = {p.name for p in list_peers(db)}
|
||||
existing = {p.name for p in list_peers(db, iface)}
|
||||
peers = []
|
||||
index = 1
|
||||
for _ in range(count):
|
||||
@@ -69,7 +435,7 @@ def create_peers_batch(
|
||||
index += 1
|
||||
name = f"{base_name}-{index}"
|
||||
existing.add(name)
|
||||
peers.append(create_peer(db, name, expires_at, note, quota_bytes))
|
||||
peers.append(create_peer(db, iface, name, expires_at, note, quota_bytes))
|
||||
return peers
|
||||
|
||||
|
||||
@@ -81,15 +447,19 @@ def update_peer(
|
||||
dns: str = "",
|
||||
extra_allowed_ips: str = "",
|
||||
client_allowed_ips: str = "",
|
||||
persistent_keepalive: int | None = None,
|
||||
) -> Peer:
|
||||
peer.note = note
|
||||
peer.quota_bytes = quota_bytes
|
||||
peer.dns = dns
|
||||
peer.extra_allowed_ips = wireguard.validate_cidr_list(extra_allowed_ips)
|
||||
peer.client_allowed_ips = wireguard.validate_cidr_list(client_allowed_ips)
|
||||
if persistent_keepalive is not None and not 0 <= persistent_keepalive <= 3600:
|
||||
raise ValueError("Keepalive must be between 0 and 3600")
|
||||
peer.persistent_keepalive = persistent_keepalive
|
||||
db.commit()
|
||||
if not peer.over_quota:
|
||||
apply_config(db)
|
||||
apply_config(db, peer.interface)
|
||||
return peer
|
||||
|
||||
|
||||
@@ -97,7 +467,7 @@ def reset_peer_usage(db: Session, peer: Peer) -> Peer:
|
||||
peer.cum_rx = 0
|
||||
peer.cum_tx = 0
|
||||
db.commit()
|
||||
apply_config(db)
|
||||
apply_config(db, peer.interface)
|
||||
return peer
|
||||
|
||||
|
||||
@@ -109,26 +479,27 @@ def rotate_peer_keys(db: Session, peer: Peer) -> Peer:
|
||||
peer.private_key_enc = encrypt(private)
|
||||
peer.preshared_key_enc = encrypt(wireguard.genpsk())
|
||||
db.commit()
|
||||
apply_config(db)
|
||||
apply_config(db, peer.interface)
|
||||
return peer
|
||||
|
||||
|
||||
def toggle_peer(db: Session, peer: Peer) -> Peer:
|
||||
peer.enabled = not peer.enabled
|
||||
db.commit()
|
||||
apply_config(db)
|
||||
apply_config(db, peer.interface)
|
||||
return peer
|
||||
|
||||
|
||||
def delete_peer(db: Session, peer: Peer) -> None:
|
||||
iface = peer.interface
|
||||
db.delete(peer)
|
||||
db.commit()
|
||||
apply_config(db)
|
||||
apply_config(db, iface)
|
||||
|
||||
|
||||
def disable_expired_peers(db: Session) -> bool:
|
||||
now = datetime.now(timezone.utc)
|
||||
changed = False
|
||||
changed_ifaces = []
|
||||
for peer in list_peers(db):
|
||||
if not peer.enabled:
|
||||
continue
|
||||
@@ -137,47 +508,63 @@ def disable_expired_peers(db: Session) -> bool:
|
||||
expires = expires.replace(tzinfo=timezone.utc)
|
||||
if (expires is not None and expires <= now) or peer.over_quota:
|
||||
peer.enabled = False
|
||||
changed = True
|
||||
if changed:
|
||||
if peer.interface not in changed_ifaces:
|
||||
changed_ifaces.append(peer.interface)
|
||||
if changed_ifaces:
|
||||
db.commit()
|
||||
apply_config(db)
|
||||
return changed
|
||||
for iface in changed_ifaces:
|
||||
apply_config(db, iface)
|
||||
return bool(changed_ifaces)
|
||||
|
||||
|
||||
def accumulate_usage(db: Session) -> None:
|
||||
status = wireguard.get_status()
|
||||
changed = False
|
||||
for peer in list_peers(db):
|
||||
peer_status = status.peers.get(peer.public_key)
|
||||
if peer_status is None:
|
||||
continue
|
||||
rx, tx = peer_status.rx_bytes, peer_status.tx_bytes
|
||||
# wg counters reset on interface restart or peer re-add
|
||||
delta_rx = rx - peer.last_rx if rx >= peer.last_rx else rx
|
||||
delta_tx = tx - peer.last_tx if tx >= peer.last_tx else tx
|
||||
if delta_rx or delta_tx or rx != peer.last_rx or tx != peer.last_tx:
|
||||
peer.cum_rx += delta_rx
|
||||
peer.cum_tx += delta_tx
|
||||
peer.last_rx = rx
|
||||
peer.last_tx = tx
|
||||
changed = True
|
||||
for iface in list_interfaces(db):
|
||||
status = wireguard.get_status(iface.name)
|
||||
for peer in list_peers(db, iface):
|
||||
peer_status = status.peers.get(peer.public_key)
|
||||
if peer_status is None:
|
||||
continue
|
||||
rx, tx = peer_status.rx_bytes, peer_status.tx_bytes
|
||||
# wg counters reset on interface restart or peer re-add
|
||||
delta_rx = rx - peer.last_rx if rx >= peer.last_rx else rx
|
||||
delta_tx = tx - peer.last_tx if tx >= peer.last_tx else tx
|
||||
if delta_rx or delta_tx or rx != peer.last_rx or tx != peer.last_tx:
|
||||
peer.cum_rx += delta_rx
|
||||
peer.cum_tx += delta_tx
|
||||
peer.last_rx = rx
|
||||
peer.last_tx = tx
|
||||
changed = True
|
||||
if changed:
|
||||
db.commit()
|
||||
|
||||
|
||||
def apply_config(db: Session) -> None:
|
||||
private, _ = wireguard.ensure_server_keys()
|
||||
wireguard.sync_peers(private, list_peers(db))
|
||||
def apply_config(db: Session, iface: Interface) -> None:
|
||||
if not iface.enabled:
|
||||
return
|
||||
wireguard.sync_peers(iface, list_peers(db, iface))
|
||||
|
||||
|
||||
def apply_all_configs(db: Session) -> None:
|
||||
for iface in list_interfaces(db):
|
||||
try:
|
||||
apply_config(db, iface)
|
||||
if iface.enabled:
|
||||
wireguard.interface_up(iface)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def sample_traffic(db: Session) -> None:
|
||||
status = wireguard.get_status()
|
||||
for peer_status in status.peers.values():
|
||||
db.add(
|
||||
TrafficSample(
|
||||
peer_public_key=peer_status.public_key,
|
||||
rx_bytes=peer_status.rx_bytes,
|
||||
tx_bytes=peer_status.tx_bytes,
|
||||
for iface in list_interfaces(db):
|
||||
status = wireguard.get_status(iface.name)
|
||||
for peer_status in status.peers.values():
|
||||
db.add(
|
||||
TrafficSample(
|
||||
interface_name=iface.name,
|
||||
peer_public_key=peer_status.public_key,
|
||||
rx_bytes=peer_status.rx_bytes,
|
||||
tx_bytes=peer_status.tx_bytes,
|
||||
)
|
||||
)
|
||||
)
|
||||
db.commit()
|
||||
|
||||
Reference in New Issue
Block a user