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:
@@ -57,6 +57,7 @@ func (s *Server) Router(distDir string) *gin.Engine {
|
||||
|
||||
auth.GET("/instances/:id/users", s.listUsers)
|
||||
auth.POST("/instances/:id/users", s.createUser)
|
||||
auth.PUT("/instances/:id/users/:uid", s.updateUser)
|
||||
auth.POST("/instances/:id/users/:uid/revoke", s.revokeUser)
|
||||
auth.DELETE("/instances/:id/users/:uid", s.deleteUser)
|
||||
auth.GET("/instances/:id/users/:uid/ovpn", s.downloadOVPN)
|
||||
@@ -140,15 +141,29 @@ func (s *Server) dashboard(c *gin.Context) {
|
||||
cl, _ := s.Svc.ListOnline(in.ID)
|
||||
online += len(cl)
|
||||
}
|
||||
whitelistN := 0
|
||||
overview := []map[string]any{}
|
||||
for _, in := range instances {
|
||||
if in.AccessMode == model.AccessWhitelist {
|
||||
whitelistN++
|
||||
}
|
||||
overview = append(overview, map[string]any{
|
||||
"name": in.Name,
|
||||
"access_mode": in.AccessMode,
|
||||
"allow_networks": in.AllowNetworks,
|
||||
})
|
||||
}
|
||||
c.JSON(200, gin.H{
|
||||
"instances": len(instances),
|
||||
"running": countByStatus(instances, "running"),
|
||||
"users": len(users),
|
||||
"active_users": countEnabled(users),
|
||||
"online": online,
|
||||
"expiring_certs": expiring,
|
||||
"recent_audits": s.Svc.Store.ListAudits(20),
|
||||
"recent_conn_logs": s.Svc.Store.ListConnLogs("", 20),
|
||||
"instances": len(instances),
|
||||
"running": countByStatus(instances, "running"),
|
||||
"whitelist_instances": whitelistN,
|
||||
"users": len(users),
|
||||
"active_users": countEnabled(users),
|
||||
"online": online,
|
||||
"expiring_certs": expiring,
|
||||
"recent_audits": s.Svc.Store.ListAudits(20),
|
||||
"recent_conn_logs": s.Svc.Store.ListConnLogs("", 20),
|
||||
"access_overview": overview,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -261,6 +276,23 @@ func (s *Server) createUser(c *gin.Context) {
|
||||
c.JSON(200, out)
|
||||
}
|
||||
|
||||
func (s *Server) updateUser(c *gin.Context) {
|
||||
var u model.VPNUser
|
||||
if err := c.ShouldBindJSON(&u); err != nil {
|
||||
c.JSON(400, gin.H{"error": "bad request"})
|
||||
return
|
||||
}
|
||||
u.ID = c.Param("uid")
|
||||
u.InstanceID = c.Param("id")
|
||||
if err := s.Svc.UpdateUser(u); err != nil {
|
||||
s.Svc.AuditForAPI(c, "update_user", u.Username, err.Error(), "failed")
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
s.Svc.AuditForAPI(c, "update_user", u.Username, "", "ok")
|
||||
c.JSON(200, u)
|
||||
}
|
||||
|
||||
func (s *Server) revokeUser(c *gin.Context) {
|
||||
uid := c.Param("uid")
|
||||
if err := s.Svc.RevokeUser(uid); err != nil {
|
||||
|
||||
Reference in New Issue
Block a user