10 Commits

Author SHA1 Message Date
cnbugs a5519c2f6c Feature: per-instance public_host for .ovpn
When creating/editing an instance, add '公网地址' (public_host) field.
This is the domain/IP clients will use to reach the OpenVPN server.
When downloading a .ovpn, this field is used first, falling back to:
  1. ?host= query parameter (one-time override)
  2. Request Host header
  3. 'vpn.example.com' default

This eliminates the need to manually edit .ovpn files after download
when the server's public address differs from its internal IP.

Backend changes:
  model.Instance: add PublicHost string field
  service.GenerateOVPN: priority PublicHost > remoteHost > default
  service.UpdateInstance: PublicHost persisted (already via JSON tag)

Frontend changes:
  Instances.vue: new '公网地址' input in create/edit dialog
  Instances.vue: new column in list table (shows '未配置' when empty)
  Users.vue: hint text updated to '可选: 覆盖实例公网地址'

E2E verified:
  public_host='vpn.yunwei.blog'  → .ovpn has 'remote vpn.yunwei.blog 1194'
  public_host='' + host=X        → .ovpn has 'remote X 1194'
  public_host set + no host      → public_host wins
2026-08-10 00:35:30 +08:00
cnbugs 1706552b33 ConnLogs: return object with _diag for troubleshooting + frontend compat
API now returns:
{
  "logs": [...],    // ConnectionLog array (same fields as before)
  "_diag": [...]    // status.log paths/existence/parse status per instance
}

Frontend updated to handle both array (legacy) and object format.

This makes it easy to diagnose:
- Which status.log paths the backend looked at
- Whether files exist and their sizes
- How many entries were parsed
- Any parse errors

User just needs to: git pull && rebuild && restart on production.
2026-08-10 00:12:00 +08:00
cnbugs 81544d2335 Fix connection logs: real-time status.log parsing
Root cause: listConnLogs only read from db.json which was never
populated (background sync silently failed due to Status check and
broken regex).

Fix approach: read status.log directly in API handlers — no DB
dependency, works immediately when OpenVPN is running.

Changes:
  api/router.go:
  - listConnLogs: parse status.log from all instances in real-time,
    merge with historical DB entries for disconnected sessions
  - dashboard: same real-time approach for recent_conn_logs

  pkg/openvpn/manager.go:
  - Fix regex for status-version 3 format (11 fields):
    CLIENT_LIST,CN,RealAddr,VPNAddr,IPv6,BytesRecv,BytesSent,ConnectedSince,...
    Old regex assumed wrong field order (CN,ConnectedSince,RealAddr)
  - Add parseConnTime() helper supporting multiple time formats

  internal/service/service.go:
  - Remove Status=='running' check (was blocking sync for all instances)
  - Add log.Printf debug output for sync operations
  - Add 'log' import

  internal/store/store.go:
  - Add ListActiveConns(instanceID) for background sync
  - Add UpdateActiveConnStats() for traffic updates without flush
2026-08-10 00:06:40 +08:00
cnbugs 19c0b190ea Fix connection logs: add background status.log sync task
ConnLogs were never populated because AppendConnLog was never called.
Added StartStatusSync() goroutine that runs every 10 seconds:

1. Iterates all running instances
2. Reads status.log via ParseStatus (status-version 3)
3. For each CLIENT_LIST entry:
   - If no active ConnLog exists → AppendConnLog (new connection)
   - If active ConnLog exists → UpdateActiveConnStats (traffic)
4. For active ConnLogs with no matching CLIENT_LIST → CloseActiveConn

Store additions:
  ListActiveConns(instanceID)  — returns all unclosed connections
  UpdateActiveConnStats()      — updates bytes in/out without flush

StartStatusSync is called from main.go right after Service creation.
UpdateActiveConnStats deliberately skips flush() to avoid writing
db.json every 10 seconds; stats are persisted on disconnect.
2026-08-09 23:52:17 +08:00
cnbugs 8be6add7bc Add cert+password dual-factor VPN authentication
OpenVPN now requires BOTH a valid client certificate AND a
username/password to establish a VPN connection.

Architecture:
  Client .ovpn has 'auth-user-pass' → prompts for credentials
  Server.conf has 'auth-user-pass-verify verify.sh via-env'
  verify.sh (bash+curl) calls POST /api/vpn/verify on the manager
  Manager verifies bcrypt hash via Go's golang.org/x/crypto/bcrypt
  Endpoint is localhost-only (127.0.0.1) for security

Model changes:
  Instance: new AuthMode field ('cert' | 'cert+password', default cert+password)
  VPNUser:  new PasswordHash (bcrypt) + Password (plaintext, transient)

Backend:
  model: AuthMode type, VPNUser.PasswordHash, VPNUser.Password (transient)
  store: ListUsers clears PasswordHash before returning
  service: CreateUser hashes password with bcrypt, enforces min 4 chars
  service: ResetVPNPassword for admin password reset
  service: UpdateUser preserves PasswordHash from old record
  service: CreateInstance defaults AuthMode=cert+password
  api: POST /api/vpn/verify (no JWT, localhost-only, bcrypt verify)
  api: POST /instances/:id/users/:uid/password (admin reset VPN pwd)
  openvpn: WriteVerifyScript generates bash+curl verify script
  openvpn: WriteServerConf adds script-security/auth-user-pass-verify
  openvpn: GenerateClientOVPN adds auth-user-pass directive

Frontend:
  Users.vue: password field on create form
  Users.vue: '重置密码' button in table + dialog
  Users.vue: '证书+密码' tag in auth column
  api: Inst.resetVPNPassword() method

Security:
  /api/vpn/verify rejects non-127.0.0.1 clients (403)
  PasswordHash never exposed via any API response
  verify.sh uses localhost curl (no external dependencies)
  bcrypt cost=10 (same as admin passwords)

Verified: correct pwd → 200, wrong pwd → 401, missing user → 401,
non-localhost → 403, .ovpn has auth-user-pass, server.conf has
script-security 2 + auth-user-pass-verify + verify-client-cert require.
2026-08-09 22:59:32 +08:00
cnbugs 7c0f8cf4d8 Fix TLS handshake: add X.509 keyUsage/EKU extensions to all certs
OpenVPN 2.7 + OpenSSL 3.6 strictly checks keyUsage extension.
Client log showed: 'Certificate does not have key usage extension'
→ 'VERIFY KU ERROR' → 'TLS handshake failed'.

Root cause: all certs generated via bare 'openssl req/x509 -req'
without any -extensions/-extfile, so no X.509 v3 extensions at all.

Fix: generate per-cert openssl ext config files:

CA cert (EnsureCA):
  basicConstraints = critical,CA:TRUE
  keyUsage = critical,keyCertSign,cRLSign
  subjectKeyIdentifier = hash
  authorityKeyIdentifier = keyid:always,issuer

Server cert (IssueCert, clientName=='server'):
  basicConstraints = critical,CA:FALSE
  keyUsage = critical,digitalSignature,keyEncipherment
  extendedKeyUsage = serverAuth

Client cert (IssueCert, otherwise):
  basicConstraints = critical,CA:FALSE
  keyUsage = critical,digitalSignature,keyEncipherment
  extendedKeyUsage = clientAuth

Also fix while here:
- .ovpn remote host: strip port from HTTP Host header with
  net.SplitHostPort; prefer ?host= query param from frontend
- Replace deprecated 'persist-key' (OpenVPN 2.7 warns) with just
  'persist-tun' in both server.conf and client .ovpn
- Replace 'cipher X' with 'data-ciphers X:AES-128-GCM' (OpenVPN 2.5+
  negotiation) in server.conf and client .ovpn

Verified: CA/Server/Client certs all show correct keyUsage + EKU via
openssl x509 -text; .ovpn remote shows correct host:port.
2026-08-09 22:39:51 +08:00
cnbugs ae02b60d67 Fix OpenVPN start: CIDR→netmask, dhcp-option DNS, ccd dir, dh none ECDHE
Four bugs prevented OpenVPN from actually listening on its UDP port:

1. server.conf line 'server 10.8.0.0/24' — OpenVPN 2.5 rejects CIDR.
   Fix: cidrToServerDirective() converts CIDR to 'NETWORK NETMASK'
   (e.g. '10.8.0.0 255.255.255.0') using net.ParseCIDR.

2. push_dns '1.1.1.1 8.8.8.8' was emitted as a single push directive
   instead of multiple 'push dhcp-option DNS x' lines.
   Fix: split space-separated IPs into individual push directives;
   pass through if already 'dhcp-option ...' form.

3. client-config-dir referenced a ccd/ directory that was never created.
   Fix: CreateInstance now MkdirAll(ccd); WriteServerConf also
   defensively MkdirAll(extraDir) before writing the directive.

4. dh.pem generated with 1024-bit DH params → OpenSSL 3.0 refuses
   with 'dh key too small'. Fix: use 'dh none' in server.conf so
   OpenVPN 2.4+ uses ECDHE key exchange — no DH params needed at all.
   Removed the slow openssl dhparam generation from EnsureCA.

Verified: instance starts, OpenVPN parses config successfully,
now fails only at TUNSETIFF (expected: requires root/CAP_NET_ADMIN).
2026-08-09 22:17:29 +08:00
cnbugs 09f6918aeb Add multi-admin account management
Schema:
- New AdminUser model with bcrypt-hashed password (cost 10)
- Roles: admin (full) / operator (read-only ops)
- Status: active / disabled
- MustChangePassword flag forces first-login password change

Backend:
- store: Add admins [] + CRUD methods (ListAdmins strips PasswordHash)
- service: SeedDefaultAdminIfEmpty (uses env credentials on first run),
  CreateAdmin, ChangePassword, ResetPassword, SetAdminStatus, DeleteAdmin
- middleware: JWT now carries user_id (UUID)
- api: login() uses bcrypt + updates last_login_at/ip, blocks disabled
- api: me() returns role + must_change_password
- api: new endpoints:
    POST /api/me/password       (self password change)
    GET  /api/admins
    POST /api/admins             (create)
    POST /api/admins/:id/password (reset by admin)
    POST /api/admins/:id/status   (enable/disable)
    DELETE /api/admins/:id        (with self/last-admin guard)

Frontend:
- Login: must_change_password=true triggers forced change-password dialog
- Layout: admin dropdown shows role tag + 修改密码 / 退出登录
- New /admins page (admin only) with table + create/reset/status/delete
- Router guard hides /admins from non-admin accounts
- API client: Auth.changePassword, Admins.{list,create,resetPassword,setStatus,delete}

Security:
- PasswordHash stored as bcrypt $2a$10$... in db.json
- ListAdmins always returns PasswordHash=''; never leaks via API
- Login returns 403 for disabled accounts

Verified: 21/21 API tests + browser E2E (first-login forced change,
restart persistence, admin list without hash, role-based menu)
2026-08-09 21:45:41 +08:00
cnbugs 4c8b7b5188 Add per-instance/user access whitelist
Features:
- New Instance.AccessMode: "open" (default) or "whitelist"
- New Instance.AllowNetworks + VPNUser.AllowNetworks: list of CIDRs
- Effective whitelist = instance allow_networks ∪ user allow_networks (dedup)
- Auto-generates client-connect.sh / client-disconnect.sh for OpenVPN:
    * Reads ccd/<cn> to extract CIDRs
    * Pushes "route <ip> <mask>" to client (client side)
    * Inserts iptables ACCEPT rules in FORWARD chain (server side, defense in depth)
    * Cleans up rules on disconnect
- server.conf auto-includes client-connect / client-disconnect directives
  and push "redirect-gateway def1 bypass-dhcp" in whitelist mode
- ccd/<cn> file format: first line ifconfig-push (static IP), then one CIDR per line
- Editing instance allow_networks refreshes all users' ccd automatically
- New PUT /api/instances/:id/users/:uid endpoint
- CIDR format validation; reject malformed inputs with friendly errors
- Dashboard shows whitelist_instances count and per-instance allow_networks table

Docs:
- README: new section "三、访问控制(白名单模式)" with usage, validation, pitfalls
- docs/API.md: updated Instance / VPNUser model + create/update payloads
- Renumbered client usage section as 四
2026-08-09 20:47:21 +08:00
cnbugs 77f8b59290 Initial commit: OpenVPN Manager v1.0
OpenVPN Web management console with multi-instance support, client cert
issuance, traffic/connection auditing, certificate expiry reminders,
auto backup/restore.

Stack:
- Backend: Go 1.21+ (Gin + JWT)
- Frontend: Vue 3 + Element Plus + ECharts + Vite
- Storage: JSON file (db.json) + filesystem (pki/, instances/, clients/, backups/)

Features:
- Multi-instance OpenVPN management (independent port/proto/subnet/PKI)
- One-click client certificate issuance with .ovpn (embedded certs)
- Certificate expiry reminders (30-day threshold)
- Connection log parsing (status-version 3)
- Auto backup/restore (tar.gz)
- Audit log for all write operations
- JWT auth (12h TTL)
- One-line install.sh for Ubuntu/Debian/RHEL/Fedora
2026-08-09 20:32:37 +08:00