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
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
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.
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.
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).