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 四
This commit is contained in:
@@ -88,6 +88,15 @@ func (s *Service) CreateInstance(in model.Instance) (*model.Instance, error) {
|
||||
if in.Subnet == "" {
|
||||
in.Subnet = "10.8.0.0/24"
|
||||
}
|
||||
if in.AccessMode == "" {
|
||||
in.AccessMode = model.AccessOpen
|
||||
}
|
||||
// 校验白名单 CIDR
|
||||
for _, c := range in.AllowNetworks {
|
||||
if err := openvpn.ValidateCIDR(c); err != nil {
|
||||
return nil, fmt.Errorf("instance allow_networks: %w", err)
|
||||
}
|
||||
}
|
||||
// 名称查重
|
||||
if _, err := s.Store.GetInstanceByName(in.Name); err == nil {
|
||||
return nil, fmt.Errorf("instance %s already exists", in.Name)
|
||||
@@ -111,11 +120,16 @@ func (s *Service) CreateInstance(in model.Instance) (*model.Instance, error) {
|
||||
if err := s.Ovm.IssueServerCert(in.Name); err != nil {
|
||||
return nil, fmt.Errorf("issue server cert: %w", err)
|
||||
}
|
||||
// 写 server.conf
|
||||
// 写 server.conf + 白名单脚本
|
||||
ccdDir := filepath.Join(s.Cfg.InstanceDir(in.Name), "ccd")
|
||||
if err := s.Ovm.WriteServerConf(&in, ccdDir); err != nil {
|
||||
return nil, fmt.Errorf("write conf: %w", err)
|
||||
}
|
||||
if in.AccessMode == model.AccessWhitelist {
|
||||
if _, err := s.Ovm.AllowNetworksScript(in.Name, in.AllowNetworks); err != nil {
|
||||
return nil, fmt.Errorf("gen whitelist scripts: %w", err)
|
||||
}
|
||||
}
|
||||
if err := s.Store.UpsertInstance(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -128,6 +142,26 @@ func (s *Service) UpdateInstance(in model.Instance) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if in.AccessMode == "" {
|
||||
in.AccessMode = old.AccessMode
|
||||
}
|
||||
if in.Cipher == "" {
|
||||
in.Cipher = old.Cipher
|
||||
}
|
||||
if in.AuthDigest == "" {
|
||||
in.AuthDigest = old.AuthDigest
|
||||
}
|
||||
if in.Dev == "" {
|
||||
in.Dev = old.Dev
|
||||
}
|
||||
if in.Subnet == "" {
|
||||
in.Subnet = old.Subnet
|
||||
}
|
||||
for _, c := range in.AllowNetworks {
|
||||
if err := openvpn.ValidateCIDR(c); err != nil {
|
||||
return fmt.Errorf("instance allow_networks: %w", err)
|
||||
}
|
||||
}
|
||||
in.CreatedAt = old.CreatedAt
|
||||
in.UpdatedAt = time.Now()
|
||||
in.Status = old.Status
|
||||
@@ -136,7 +170,24 @@ func (s *Service) UpdateInstance(in model.Instance) error {
|
||||
if err := s.Ovm.WriteServerConf(&in, ccdDir); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.Store.UpsertInstance(in)
|
||||
if in.AccessMode == model.AccessWhitelist {
|
||||
if _, err := s.Ovm.AllowNetworksScript(in.Name, in.AllowNetworks); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := s.Store.UpsertInstance(in); err != nil {
|
||||
return err
|
||||
}
|
||||
// 实例级 allow_networks 变更后,刷新所有用户的 ccd
|
||||
inst := in
|
||||
for _, u := range s.Store.ListUsers(inst.ID) {
|
||||
u := u
|
||||
merged := openvpn.MergeAllowNetworks(inst.AllowNetworks, u.AllowNetworks)
|
||||
if err := s.writeUserCCD(&inst, &u, merged); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteInstance 移除实例及其 PKI/配置。
|
||||
@@ -238,6 +289,12 @@ func (s *Service) CreateUser(u model.VPNUser) (*model.VPNUser, error) {
|
||||
if _, err := s.Store.GetUserByCN(u.InstanceID, u.Username); err == nil {
|
||||
return nil, fmt.Errorf("user %s already exists", u.Username)
|
||||
}
|
||||
// 校验用户级 CIDR
|
||||
for _, c := range u.AllowNetworks {
|
||||
if err := openvpn.ValidateCIDR(c); err != nil {
|
||||
return nil, fmt.Errorf("user allow_networks: %w", err)
|
||||
}
|
||||
}
|
||||
u.ID = uuid.NewString()
|
||||
u.Enabled = true
|
||||
u.CreatedAt = time.Now()
|
||||
@@ -245,12 +302,10 @@ func (s *Service) CreateUser(u model.VPNUser) (*model.VPNUser, error) {
|
||||
if _, _, err := s.Ovm.IssueCert(in.Name, u.Username); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// CCD
|
||||
if u.StaticIP != "" {
|
||||
ccd := "ifconfig-push " + u.StaticIP + " 255.255.255.0\n"
|
||||
if err := s.Ovm.WriteCCD(in.Name, u.Username, ccd); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// CCD: 合并实例级与用户级白名单(白名单模式才生效)
|
||||
merged := openvpn.MergeAllowNetworks(in.AllowNetworks, u.AllowNetworks)
|
||||
if err := s.writeUserCCD(in, &u, merged); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 预生成 ovpn(以空 host 生成占位,用户在 UI 上下载)
|
||||
if _, err := s.Ovm.GenerateClientOVPNFor(&u, in, "vpn.example.com"); err != nil {
|
||||
@@ -312,6 +367,47 @@ func (s *Service) DeleteUser(userID string) error {
|
||||
return s.Store.DeleteUser(userID)
|
||||
}
|
||||
|
||||
// UpdateUser 修改用户(主要用于改 allow_networks / static_ip 等)。
|
||||
func (s *Service) UpdateUser(u model.VPNUser) error {
|
||||
old, err := s.Store.GetUser(u.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, c := range u.AllowNetworks {
|
||||
if err := openvpn.ValidateCIDR(c); err != nil {
|
||||
return fmt.Errorf("user allow_networks: %w", err)
|
||||
}
|
||||
}
|
||||
u.CreatedAt = old.CreatedAt
|
||||
u.Enabled = old.Enabled
|
||||
u.RevokedAt = old.RevokedAt
|
||||
in, err := s.Store.GetInstance(u.InstanceID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
merged := openvpn.MergeAllowNetworks(in.AllowNetworks, u.AllowNetworks)
|
||||
if err := s.writeUserCCD(in, &u, merged); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.Store.UpsertUser(u)
|
||||
}
|
||||
|
||||
// writeUserCCD 把允许网段写进 ccd/<cn>,client-connect 脚本读取后推送 route。
|
||||
// 文件内容:
|
||||
// - 第 1 行 ifconfig-push (固定 IP)
|
||||
// - 之后每行一个 CIDR (允许的网段)
|
||||
// 这样设计既兼容现有 static_ip 场景,又能让 client-connect.sh 简单 grep。
|
||||
func (s *Service) writeUserCCD(in *model.Instance, u *model.VPNUser, allowNets []string) error {
|
||||
var b strings.Builder
|
||||
if u.StaticIP != "" {
|
||||
b.WriteString("ifconfig-push " + u.StaticIP + " 255.255.255.0\n")
|
||||
}
|
||||
for _, c := range allowNets {
|
||||
b.WriteString(c + "\n")
|
||||
}
|
||||
return s.Ovm.WriteCCD(in.Name, u.Username, b.String())
|
||||
}
|
||||
|
||||
// CertInfos 汇总所有用户证书的过期时间。
|
||||
func (s *Service) CertInfos() ([]model.CertInfo, error) {
|
||||
var out []model.CertInfo
|
||||
|
||||
Reference in New Issue
Block a user