diff --git a/app/db.py b/app/db.py
index c1879ce..896ef86 100644
--- a/app/db.py
+++ b/app/db.py
@@ -25,6 +25,7 @@ def get_db():
PEER_MIGRATIONS = {
"note": "ALTER TABLE peers ADD COLUMN note VARCHAR(256) NOT NULL DEFAULT ''",
+ "dns": "ALTER TABLE peers ADD COLUMN dns VARCHAR(128) NOT NULL DEFAULT ''",
"quota_bytes": "ALTER TABLE peers ADD COLUMN quota_bytes INTEGER NOT NULL DEFAULT 0",
"cum_rx": "ALTER TABLE peers ADD COLUMN cum_rx INTEGER NOT NULL DEFAULT 0",
"cum_tx": "ALTER TABLE peers ADD COLUMN cum_tx INTEGER NOT NULL DEFAULT 0",
diff --git a/app/main.py b/app/main.py
index 6fd8fd9..c14d596 100644
--- a/app/main.py
+++ b/app/main.py
@@ -2,6 +2,7 @@ import asyncio
import io
from contextlib import asynccontextmanager
from datetime import datetime
+from urllib.parse import quote
import qrcode
from fastapi import Depends, FastAPI, Form, HTTPException, Request
@@ -108,11 +109,13 @@ def dashboard(request: Request, db: Session = Depends(get_db)):
@app.get("/peers", response_class=HTMLResponse, dependencies=[logged_in])
-def peers_page(request: Request, db: Session = Depends(get_db)):
+def peers_page(request: Request, db: Session = Depends(get_db), error: str = ""):
status = wireguard.get_status()
peers = service.list_peers(db)
return templates.TemplateResponse(
- request, "peers.html", {"peers": peers, "status": status}
+ request,
+ "peers.html",
+ {"peers": peers, "status": status, "error": error, "settings": settings},
)
@@ -129,13 +132,22 @@ def create_peer(
note: str = Form(""),
quota_gib: str = Form(""),
count: int = Form(1),
+ address: str = Form(""),
+ dns: str = Form(""),
):
expiry = datetime.fromisoformat(expires_at) if expires_at else None
quota = _parse_quota_gib(quota_gib)
- if count > 1:
- service.create_peers_batch(db, name.strip(), min(count, 50), expiry, note.strip(), quota)
- return RedirectResponse("/peers", status_code=303)
- peer = service.create_peer(db, name.strip(), expiry, note.strip(), quota)
+ try:
+ if count > 1:
+ service.create_peers_batch(
+ db, name.strip(), min(count, 50), expiry, note.strip(), quota
+ )
+ return RedirectResponse("/peers", status_code=303)
+ peer = service.create_peer(
+ db, name.strip(), expiry, note.strip(), quota, address.strip(), dns.strip()
+ )
+ except ValueError as exc:
+ return RedirectResponse(f"/peers?error={quote(str(exc))}", status_code=303)
return RedirectResponse(f"/peers/{peer.id}", status_code=303)
@@ -150,10 +162,16 @@ def _get_peer_or_404(db: Session, peer_id: int):
def peer_detail(request: Request, peer_id: int, db: Session = Depends(get_db)):
peer = _get_peer_or_404(db, peer_id)
status = wireguard.get_status()
+ _, server_public = wireguard.ensure_server_keys()
+ client_config = wireguard.render_client_config(peer, server_public)
return templates.TemplateResponse(
request,
"peer_detail.html",
- {"peer": peer, "peer_status": status.peers.get(peer.public_key)},
+ {
+ "peer": peer,
+ "peer_status": status.peers.get(peer.public_key),
+ "client_config": client_config,
+ },
)
@@ -163,9 +181,10 @@ def update_peer(
db: Session = Depends(get_db),
note: str = Form(""),
quota_gib: str = Form(""),
+ dns: str = Form(""),
):
peer = _get_peer_or_404(db, peer_id)
- service.update_peer(db, peer, note.strip(), _parse_quota_gib(quota_gib))
+ service.update_peer(db, peer, note.strip(), _parse_quota_gib(quota_gib), dns.strip())
return RedirectResponse(f"/peers/{peer_id}", status_code=303)
diff --git a/app/models.py b/app/models.py
index 0a61d37..4933abf 100644
--- a/app/models.py
+++ b/app/models.py
@@ -23,6 +23,7 @@ class Peer(Base):
expires_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow)
note: Mapped[str] = mapped_column(String(256), default="")
+ dns: Mapped[str] = mapped_column(String(128), default="")
quota_bytes: Mapped[int] = mapped_column(Integer, default=0)
cum_rx: Mapped[int] = mapped_column(Integer, default=0)
cum_tx: Mapped[int] = mapped_column(Integer, default=0)
diff --git a/app/service.py b/app/service.py
index fcab3b8..a82a8d1 100644
--- a/app/service.py
+++ b/app/service.py
@@ -22,10 +22,16 @@ def create_peer(
expires_at: datetime | None = None,
note: str = "",
quota_bytes: int = 0,
+ address: str = "",
+ dns: str = "",
) -> Peer:
private, public = wireguard.generate_keypair()
psk = wireguard.genpsk()
- address = wireguard.next_free_address([p.address for p in list_peers(db)])
+ taken = [p.address for p in list_peers(db)]
+ if address:
+ address = wireguard.validate_address(address, taken)
+ else:
+ address = wireguard.next_free_address(taken)
peer = Peer(
name=name,
public_key=public,
@@ -35,6 +41,7 @@ def create_peer(
expires_at=expires_at,
note=note,
quota_bytes=quota_bytes,
+ dns=dns,
)
db.add(peer)
db.commit()
@@ -62,9 +69,10 @@ def create_peers_batch(
return peers
-def update_peer(db: Session, peer: Peer, note: str, quota_bytes: int) -> Peer:
+def update_peer(db: Session, peer: Peer, note: str, quota_bytes: int, dns: str = "") -> Peer:
peer.note = note
peer.quota_bytes = quota_bytes
+ peer.dns = dns
db.commit()
if not peer.over_quota:
apply_config(db)
diff --git a/app/static/style.css b/app/static/style.css
index fad9a30..c469266 100644
--- a/app/static/style.css
+++ b/app/static/style.css
@@ -71,6 +71,19 @@ input {
}
.add-peer { display: flex; gap: 0.5rem; align-items: center; margin-bottom: 0.25rem; flex-wrap: wrap; }
input.narrow { width: 80px; }
+input.mid { width: 160px; }
+.breadcrumbs { margin-bottom: 1rem; font-size: 0.85rem; color: #6b7280; }
+.breadcrumbs a { color: #2563eb; text-decoration: none; }
+.breadcrumbs a:hover { text-decoration: underline; }
+.config-card { margin-top: 1rem; }
+.config-card pre {
+ background: #1f2430;
+ color: #d6e2ff;
+ padding: 1rem;
+ border-radius: 6px;
+ overflow-x: auto;
+ font-size: 0.85rem;
+}
form.stack label { display: block; margin-bottom: 0.5rem; }
form.stack { margin-top: 0.75rem; }
diff --git a/app/templates/base.html b/app/templates/base.html
index 9ff4e81..916c6e5 100644
--- a/app/templates/base.html
+++ b/app/templates/base.html
@@ -17,6 +17,10 @@
{{ client_config }}
+Count > 1 creates a batch named name-1, name-2, ... Quota 0 = unlimited.
+Count > 1 creates a batch named name-1, name-2, ... (custom IP ignored). Quota 0 = unlimited.