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