16 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
openvpn-manager_0.0.3
2026-08-10 00:35:30 +08:00
cnbugs 2b0f144abc Docs: add IP forwarding/NAT section for client-to-LAN access
The most common OpenVPN deployment scenario is letting remote users
access the server's LAN (office/home network). This requires:
1. net.ipv4.ip_forward=1
2. iptables MASQUERADE on the LAN-facing interface
3. push 'route <LAN-net>' so clients know to send LAN traffic through VPN

Previously this was only mentioned in the FAQ row 'ping不通服务端',
with no actionable instructions. Users (including the project's own
first deployment) hit this exact issue and had to figure it out from
forum posts.

Changes:
  README.md:
  - New section 'IP 转发与内网访问' before 防火墙与公网暴露
  - Covers: enabling ip_forward (immediate + persistent via sysctl.d),
    MASQUERADE rules for one/multiple LAN interfaces, push routes,
    verification commands, troubleshooting table, full checklist
  - FAQ table: add explicit row for 'gateway reachable, LAN not'
  - Features table: add '内网转发' row
  - TOC: link new section

  scripts/install.sh:
  - Auto-enable ip_forward on install (idempotent, persistent via
    /etc/sysctl.d/99-openvpn-manager.conf)
  - Skip silently in containerized environments (no /proc/sys write)
  - Print reminder about manual MASQUERADE rule with link to docs
openvpn-manager_0.0.2
2026-08-10 00:26:34 +08:00
cnbugs 5725149546 Fix v3 parser: scan for numeric fields (IPv6 field position varies)
The user's OpenVPN 2.5.11 outputs CLIENT_LIST with 13 tab-separated
fields:
  CLIENT_LIST CN RealAddr VPNAddr IPv6(empty) (empty) BytesRecv
              BytesSent ConnectedSince (time_t) Username ClientID
              PeerID Cipher

But OpenVPN 2.6+ drops the IPv6 and Cipher fields (11 columns), and
some versions include 'Connected Since (time_t)' which moves all
positions.

Solution: parse by content type, not position:
  - parts[0] = 'CLIENT_LIST'
  - parts[1] = CN (string, may be empty)
  - parts[2] = RealAddr (string with ':')
  - parts[3] = VPNAddr (IP, may be empty)
  - parts[4..] = scan for first pure-numeric field → BytesRecv
  - BytesRecv+1 = BytesSent
  - BytesRecv+2 = ConnectedSince

Verified against the user's real status.log:
  CN=test RealAddr=123.118.73.196:17564 VPNAddr=10.8.0.2
  bytesIn=629025 bytesOut=539217
2026-08-10 00:22:02 +08:00
cnbugs 9054da954a Add diag-connlogs.sh diagnostic script 2026-08-10 00:19:20 +08:00
cnbugs 9f24c74527 Fix status parser: support status-version 1 (default OpenVPN format)
Root cause: user's status.log uses default OpenVPN format (status-version 1)
with human-readable column names. Our parser only handled status-version 3
(CSV with 11 fields per CLIENT_LIST line).

v1 format observed on production:
  Common Name,Real Address,Bytes Received,Bytes Sent,Connected Since
  test,123.118.73.196:18257,423700,455528,2026-08-09 16:14:10

Changes:
  pkg/openvpn/manager.go:
  - Detect format by scanning first line: TITLE, → v3, OpenVPN CLIENT LIST → v1
  - Add reCli1 regex for v1 5-column data rows
  - v1 mode: read full file, parse CLIENT_LIST then parse ROUTING_TABLE
    separately to associate CN→VPNAddress (v1 doesn't include VPN IP
    in the client data row)
  - Add bytes import

  Both formats now work side-by-side.
2026-08-10 00:16:28 +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 f3cda67d9f Fix VPN verify: via-env→via-file, add pass_len debug logging
OpenVPN 2.7 Windows DCO passes username but NOT password as
environment variable with via-env mode. Switched to via-file:
  server.conf: auth-user-pass-verify script via-file
  verify.sh reads credentials from temp file ():
    line 1 = username, line 2 = password

Also added pass_len to debug log so we can immediately see
if the password was actually received by the script.
0.0.1
2026-08-09 23:34:05 +08:00
cnbugs 70341c78e5 Fix VPN verify script: add debug logging, remove set -e
The verify.sh had 'set -e' which could cause premature exit in
edge cases. Replaced with explicit logging to /tmp/openvpn-verify.log
so we can diagnose AUTH_FAILED on production servers.

Log shows: timestamp, username, HTTP code, response body, ALLOW/DENY.
Also redirected curl stderr to log file instead of /dev/null so
connection errors are visible for debugging.
2026-08-09 23:21:05 +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