增加了服务器防火墙功能

This commit is contained in:
MengMengCode
2026-06-12 01:35:04 +08:00
parent 6194b6e364
commit 875cd4716b
14 changed files with 711 additions and 22 deletions
+1 -1
View File
@@ -90,7 +90,7 @@ func hasScope(r *http.Request, scope string) bool {
func subUserScopeAllowed(scope string) bool {
switch scope {
case "container:read", "container:power", "container:reinstall", "container:network",
case "container:read", "container:power", "container:reinstall", "container:password", "container:network",
"dashboard:read", "image:read", "task:read", "snapshot:read", "snapshot:create", "snapshot:delete", "snapshot:restore", "snapshot:schedule",
"terminal:ssh", "terminal:vnc":
return true
+154
View File
@@ -0,0 +1,154 @@
package api
import (
"encoding/json"
"math/rand"
"net/http"
"strconv"
"strings"
"clicd/internal/config"
"clicd/internal/lxc"
)
func generateFirewallRuleID() string {
const chars = "abcdefghijklmnopqrstuvwxyz0123456789"
b := make([]byte, 8)
for i := range b {
b[i] = chars[rand.Intn(len(chars))]
}
return string(b)
}
func getFirewall(w http.ResponseWriter, r *http.Request, id int) {
c := config.FindContainer(id)
if c == nil {
jsonResponse(w, http.StatusNotFound, APIResponse{Success: false, Message: "Container not found"})
return
}
jsonResponse(w, http.StatusOK, APIResponse{
Success: true,
Data: map[string]interface{}{
"enabled": c.FirewallEnabled,
"rules": c.FirewallRules,
},
})
}
func updateFirewall(w http.ResponseWriter, r *http.Request, id int) {
c := config.FindContainer(id)
if c == nil {
jsonResponse(w, http.StatusNotFound, APIResponse{Success: false, Message: "Container not found"})
return
}
var req struct {
Enabled *bool `json:"enabled"`
Rules *[]config.FirewallRule `json:"rules"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
jsonResponse(w, http.StatusBadRequest, APIResponse{Success: false, Message: "Invalid request body"})
return
}
if req.Enabled != nil {
c.FirewallEnabled = *req.Enabled
}
if req.Rules != nil {
// Validate and assign IDs to new rules
rules := *req.Rules
for i := range rules {
rules[i].Direction = strings.ToLower(strings.TrimSpace(rules[i].Direction))
rules[i].Protocol = strings.ToLower(strings.TrimSpace(rules[i].Protocol))
rules[i].Action = strings.ToUpper(strings.TrimSpace(rules[i].Action))
rules[i].SourceIP = strings.TrimSpace(rules[i].SourceIP)
rules[i].Port = strings.TrimSpace(rules[i].Port)
if rules[i].Direction != "in" && rules[i].Direction != "out" {
jsonResponse(w, http.StatusBadRequest, APIResponse{Success: false, Message: "Invalid direction: " + rules[i].Direction})
return
}
if rules[i].Protocol != "tcp" && rules[i].Protocol != "udp" && rules[i].Protocol != "icmp" && rules[i].Protocol != "all" {
jsonResponse(w, http.StatusBadRequest, APIResponse{Success: false, Message: "Invalid protocol: " + rules[i].Protocol})
return
}
if rules[i].Action != "ACCEPT" && rules[i].Action != "DROP" {
jsonResponse(w, http.StatusBadRequest, APIResponse{Success: false, Message: "Invalid action: " + rules[i].Action})
return
}
if rules[i].ID == "" {
rules[i].ID = generateFirewallRuleID()
}
// Validate port spec
if rules[i].Port != "" {
if err := validatePortSpec(rules[i].Port); err != nil {
jsonResponse(w, http.StatusBadRequest, APIResponse{Success: false, Message: "Invalid port: " + err.Error()})
return
}
}
}
c.FirewallRules = rules
}
config.SaveConfig()
// Apply firewall rules to iptables if container is running
if c.Status == "running" {
if err := lxc.ApplyFirewallRules(id); err != nil {
jsonResponse(w, http.StatusInternalServerError, APIResponse{Success: false, Message: "Failed to apply firewall rules: " + err.Error()})
return
}
} else if !c.FirewallEnabled {
// If disabled and not running, clean any lingering rules
lxc.CleanFirewallRules(id)
}
jsonResponse(w, http.StatusOK, APIResponse{
Success: true,
Message: "Firewall updated",
Data: map[string]interface{}{
"enabled": c.FirewallEnabled,
"rules": c.FirewallRules,
},
})
}
func validatePortSpec(port string) error {
port = strings.TrimSpace(port)
if port == "" {
return nil
}
// Support: "22", "80,443", "8000-9000", "80,443,8000-9000"
for _, part := range strings.Split(port, ",") {
part = strings.TrimSpace(part)
if part == "" {
continue
}
if strings.Contains(part, "-") {
// Range
bounds := strings.SplitN(part, "-", 2)
lo, err := strconv.Atoi(strings.TrimSpace(bounds[0]))
if err != nil || lo < 1 || lo > 65535 {
return &portValidationError{part}
}
hi, err := strconv.Atoi(strings.TrimSpace(bounds[1]))
if err != nil || hi < 1 || hi > 65535 {
return &portValidationError{part}
}
} else {
p, err := strconv.Atoi(part)
if err != nil || p < 1 || p > 65535 {
return &portValidationError{part}
}
}
}
return nil
}
type portValidationError struct {
port string
}
func (e *portValidationError) Error() string {
return "invalid port value: " + e.port
}
+10
View File
@@ -182,6 +182,16 @@ func HandleSingleContainer(w http.ResponseWriter, r *http.Request) {
return
}
deletePortMapping(w, r, id, strings.TrimPrefix(action, "port-mappings/"))
case action == "firewall" && r.Method == http.MethodGet:
if !requireScope(w, r, "container:network") {
return
}
getFirewall(w, r, id)
case action == "firewall" && r.Method == http.MethodPut:
if !requireScope(w, r, "container:network") {
return
}
updateFirewall(w, r, id)
case r.Method == http.MethodGet:
if !requireScope(w, r, "container:read") {
return
+10 -1
View File
@@ -373,6 +373,15 @@ func SubUserMiddleware(next http.HandlerFunc) http.HandlerFunc {
return
}
imagesEnabledPath := "/api/images/enabled"
if strings.HasPrefix(path, "/api/v1/") {
imagesEnabledPath = "/api/v1/images/enabled"
}
if path == imagesEnabledPath && r.Method == http.MethodGet {
next(w, r)
return
}
if path == containerListPath {
if r.Method != http.MethodGet {
jsonResponse(w, http.StatusForbidden, APIResponse{Success: false, Message: "Sub-users cannot create containers"})
@@ -503,7 +512,7 @@ func isSubUserContainerActionAllowed(action string, method string) bool {
return method == http.MethodPost
case strings.HasPrefix(action, "snapshots/"):
return method == http.MethodDelete || method == http.MethodPost
case action == "start" || action == "stop" || action == "restart" || action == "reinstall":
case action == "start" || action == "stop" || action == "restart" || action == "reinstall" || action == "reset-password":
return method == http.MethodPost
case strings.HasPrefix(action, "port-mappings/"):
return method == http.MethodPut