Files
openvpn-manager/backend/internal/model/model.go
T
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

136 lines
6.1 KiB
Go

package model
import "time"
// AccessMode 控制客户端的访问范围。
// - "open": 不限制,客户端可访问所有可达网段(默认)
// - "whitelist":白名单模式,仅允许访问 Instance 与 User 合并后的 allow_networks
// 列表中的网段,其他内网流量在服务端 FORWARD 链被丢弃
type AccessMode string
const (
AccessOpen AccessMode = "open"
AccessWhitelist AccessMode = "whitelist"
)
// AuthMode 控制客户端认证方式。
// - "cert": 仅证书认证(向后兼容)
// - "cert+password": 证书 + 用户名密码双因素认证(更安全)
type AuthMode string
const (
AuthCert AuthMode = "cert"
AuthCertPassword AuthMode = "cert+password"
)
// Instance 一个 OpenVPN 服务端实例。
// 每个实例使用独立端口与 PKI,运行在自己的 server.conf 下,
// 由 systemd 单元(或后台进程)托管,本服务通过 management 接口与之通信。
type Instance struct {
ID string `json:"id"`
Name string `json:"name"` // 唯一名,作为目录名
Port int `json:"port"` // openvpn 监听端口
Proto string `json:"proto"` // udp / tcp
Dev string `json:"dev"` // tun / tap
Subnet string `json:"subnet"` // 客户端子网,如 10.8.0.0/24
Cipher string `json:"cipher"` // 加密算法
AuthDigest string `json:"auth_digest"` // 摘要算法
PushDNS string `json:"push_dns"` // push "dhcp-option DNS x.x.x.x"
PushRoutes string `json:"push_routes"` // 多行
Extra string `json:"extra"` // 用户追加配置
AccessMode AccessMode `json:"access_mode"` // open | whitelist
AllowNetworks []string `json:"allow_networks"` // 实例级白名单 CIDR 列表
AuthMode AuthMode `json:"auth_mode"` // cert | cert+password
PublicHost string `json:"public_host"` // 公网域名/IP,生成 .ovpn 时优先用它;空则用下载时前端传入的 host
Status string `json:"status"` // running/stopped/error
PID int `json:"pid"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// VPNUser 一个 OpenVPN 客户端用户。
// 证书从对应实例的 PKI 中签发,可下载 .ovpn 客户端配置。
type VPNUser struct {
ID string `json:"id"`
InstanceID string `json:"instance_id"`
Username string `json:"username"` // 证书 CN, 同时也是 VPN 登录用户名
Password string `json:"password,omitempty"` // 明文密码(仅创建/重置时传入,不持久化)
PasswordHash string `json:"password_hash"` // bcrypt 哈希;store.ListUsers 返回时清空
RealName string `json:"real_name"` // 备注
Email string `json:"email"`
Enabled bool `json:"enabled"` // 是否启用, false 即吊销/禁用
StaticIP string `json:"static_ip"` // ccd 固定地址, 空表示动态
AllowNetworks []string `json:"allow_networks"` // 用户级白名单(在实例基础上叠加)
CreatedAt time.Time `json:"created_at"`
ExpiresAt *time.Time `json:"expires_at,omitempty"`
RevokedAt *time.Time `json:"revoked_at,omitempty"`
}
// AuditLog 操作审计日志
type AuditLog struct {
ID string `json:"id"`
Time time.Time `json:"time"`
User string `json:"user"` // 操作者(管理用户)
Action string `json:"action"` // create_instance, revoke_user, ...
Target string `json:"target"` // 目标对象
Result string `json:"result"` // ok / failed
Detail string `json:"detail"`
IP string `json:"ip"`
}
// AdminUser 管理控制台账号。
// 密码以 bcrypt 哈希存储 (cost=10)。
// - Role: "admin" = 全部权限 (含账号管理)
// "operator" = 仅运维操作(实例/用户/备份等),无账号管理
// - Status: "active" | "disabled"
// - MustChangePassword: 首次 seed 的默认账号(密码 = admin123)需要首次登录后改密
//
// 安全:
// * PasswordHash 字段在 db.json 中以 password_hash 持久化(必须!)
// * 但 store.ListAdmins 在返回前会清空 PasswordHash,
// 因此所有 API 响应里 hash 都是空字符串,绝不出网
type AdminUser struct {
ID string `json:"id"`
Username string `json:"username"`
PasswordHash string `json:"password_hash"` // 注意:store.ListAdmins 返回时会清空
Role string `json:"role"`
Status string `json:"status"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
LastLoginAt *time.Time `json:"last_login_at,omitempty"`
LastLoginIP string `json:"last_login_ip,omitempty"`
MustChangePassword bool `json:"must_change_password"` // 强制改密标志
}
// ConnectionLog 来自 OpenVPN status 的实时/历史连接记录。
// 周期由 OpenVPN 自身写入 status.log,本服务周期性读取解析后入库。
type ConnectionLog struct {
InstanceID string `json:"instance_id"`
CommonName string `json:"common_name"`
RealIP string `json:"real_ip"` // 客户端公网 IP
VPNIP string `json:"vpn_ip"` // 分配的 VPN 内网 IP
BytesIn int64 `json:"bytes_in"`
BytesOut int64 `json:"bytes_out"`
ConnectedAt time.Time `json:"connected_at"`
DisconnectedAt *time.Time `json:"disconnected_at,omitempty"`
}
// Backup 一份备份归档
type Backup struct {
ID string `json:"id"`
CreatedAt time.Time `json:"created_at"`
Size int64 `json:"size"`
Note string `json:"note"`
Filename string `json:"filename"`
Includes []string `json:"includes"`
}
// CertInfo 证书元数据(用于证书到期提醒)。
type CertInfo struct {
InstanceID string `json:"instance_id"`
Username string `json:"username"`
Subject string `json:"subject"`
NotBefore time.Time `json:"not_before"`
NotAfter time.Time `json:"not_after"`
DaysLeft int `json:"days_left"`
}