mirror of
https://github.com/MengMengCode/CLICD.git
synced 2026-08-05 05:36:07 +08:00
增加了服务器防火墙功能
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -22,6 +22,17 @@ type PortMapping struct {
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
type FirewallRule struct {
|
||||
ID string `json:"id"`
|
||||
Direction string `json:"direction"` // "in" or "out"
|
||||
Protocol string `json:"protocol"` // "tcp", "udp", "icmp", "all"
|
||||
Port string `json:"port"` // "" = all, "22", "80,443", "8000-9000"
|
||||
SourceIP string `json:"source_ip"` // "" = any
|
||||
Action string `json:"action"` // "ACCEPT" or "DROP"
|
||||
Description string `json:"description"`
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
type PublicIPv4Assignment struct {
|
||||
Address string `json:"address"`
|
||||
Interface string `json:"interface,omitempty"`
|
||||
@@ -124,6 +135,8 @@ type Container struct {
|
||||
SSHHostKey string `json:"ssh_host_key,omitempty"`
|
||||
PortMappings []PortMapping `json:"port_mappings"`
|
||||
PortMappingLimit int `json:"port_mapping_limit"`
|
||||
FirewallEnabled bool `json:"firewall_enabled"`
|
||||
FirewallRules []FirewallRule `json:"firewall_rules"`
|
||||
SnapshotLimit int `json:"snapshot_limit"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
ExpiresAt string `json:"expires_at"`
|
||||
|
||||
@@ -364,6 +364,8 @@ func ensureSchemaMigrations() error {
|
||||
{"port_mappings", "host_ip", "TEXT"},
|
||||
{"container_public_ipv4s", "prefix_len", "INTEGER"},
|
||||
{"container_public_ipv4s", "gateway", "TEXT"},
|
||||
{"containers", "firewall_enabled", "INTEGER NOT NULL DEFAULT 0"},
|
||||
{"containers", "firewall_rules", "TEXT"},
|
||||
} {
|
||||
if err := ensureColumn(column.table, column.name, column.def); err != nil {
|
||||
return err
|
||||
@@ -583,8 +585,9 @@ func saveContainers(tx *sql.Tx) error {
|
||||
ssh_host_key, port_mapping_limit, snapshot_limit, created_at, expires_at,
|
||||
snapshot_schedule_enabled, snapshot_schedule_interval_hours, snapshot_schedule_time,
|
||||
snapshot_schedule_last_run, snapshot_schedule_next_run, snapshot_schedule_created_by,
|
||||
policy_blocked, policy_blocked_reason, policy_blocked_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
policy_blocked, policy_blocked_reason, policy_blocked_at,
|
||||
firewall_enabled, firewall_rules
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
c.ID, c.UUID, c.Name, c.Virtualization, c.LXCName, c.KVMName, c.DiskImage, c.MACAddress, c.Template,
|
||||
c.VCPU, c.RAMMB, c.DiskGB, c.NetworkBWMbps, c.MonthlyTrafficGB, c.TrafficMode, c.TrafficInGB,
|
||||
c.TrafficOutGB, c.TrafficUsedRX, c.TrafficUsedTX, c.TrafficResetDate, c.IOSpeedMBps,
|
||||
@@ -593,6 +596,7 @@ func saveContainers(tx *sql.Tx) error {
|
||||
boolInt(c.SnapshotScheduleEnabled), c.SnapshotScheduleIntervalHours, c.SnapshotScheduleTime,
|
||||
c.SnapshotScheduleLastRun, c.SnapshotScheduleNextRun, c.SnapshotScheduleCreatedBy,
|
||||
boolInt(c.PolicyBlocked), c.PolicyBlockedReason, c.PolicyBlockedAt,
|
||||
boolInt(c.FirewallEnabled), marshalFirewallRules(c.FirewallRules),
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -789,7 +793,8 @@ func loadContainers() ([]Container, error) {
|
||||
ssh_host_key, port_mapping_limit, snapshot_limit, created_at, expires_at,
|
||||
snapshot_schedule_enabled, snapshot_schedule_interval_hours, snapshot_schedule_time,
|
||||
snapshot_schedule_last_run, snapshot_schedule_next_run, snapshot_schedule_created_by,
|
||||
policy_blocked, policy_blocked_reason, policy_blocked_at
|
||||
policy_blocked, policy_blocked_reason, policy_blocked_at,
|
||||
firewall_enabled, firewall_rules
|
||||
FROM containers ORDER BY id`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -799,7 +804,8 @@ func loadContainers() ([]Container, error) {
|
||||
result := []Container{}
|
||||
for rows.Next() {
|
||||
var c Container
|
||||
var scheduleEnabled, policyBlocked int
|
||||
var scheduleEnabled, policyBlocked, firewallEnabled int
|
||||
var firewallRulesJSON sql.NullString
|
||||
if err := rows.Scan(
|
||||
&c.ID, &c.UUID, &c.Name, &c.Virtualization, &c.LXCName, &c.KVMName, &c.DiskImage, &c.MACAddress, &c.Template,
|
||||
&c.VCPU, &c.RAMMB, &c.DiskGB, &c.NetworkBWMbps, &c.MonthlyTrafficGB, &c.TrafficMode, &c.TrafficInGB,
|
||||
@@ -809,11 +815,16 @@ func loadContainers() ([]Container, error) {
|
||||
&scheduleEnabled, &c.SnapshotScheduleIntervalHours, &c.SnapshotScheduleTime,
|
||||
&c.SnapshotScheduleLastRun, &c.SnapshotScheduleNextRun, &c.SnapshotScheduleCreatedBy,
|
||||
&policyBlocked, &c.PolicyBlockedReason, &c.PolicyBlockedAt,
|
||||
&firewallEnabled, &firewallRulesJSON,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.SnapshotScheduleEnabled = scheduleEnabled != 0
|
||||
c.PolicyBlocked = policyBlocked != 0
|
||||
c.FirewallEnabled = firewallEnabled != 0
|
||||
if firewallRulesJSON.Valid && strings.TrimSpace(firewallRulesJSON.String) != "" {
|
||||
_ = json.Unmarshal([]byte(firewallRulesJSON.String), &c.FirewallRules)
|
||||
}
|
||||
result = append(result, c)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
@@ -1167,6 +1178,17 @@ func boolInt(value bool) int {
|
||||
return 0
|
||||
}
|
||||
|
||||
func marshalFirewallRules(rules []FirewallRule) interface{} {
|
||||
if len(rules) == 0 {
|
||||
return nil
|
||||
}
|
||||
data, err := json.Marshal(rules)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
|
||||
func boolPtrInt(value *bool) interface{} {
|
||||
if value == nil {
|
||||
return nil
|
||||
|
||||
@@ -626,6 +626,9 @@ func (m *Manager) StartContainer(id int) error {
|
||||
if err := lxc.NewManager().ApplyPortMappings(id); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := lxc.ApplyFirewallRules(id); err != nil {
|
||||
fmt.Printf("Warning: failed to apply firewall rules: %v\n", err)
|
||||
}
|
||||
}
|
||||
// Wait for cloud-init to finish and SSH to be reachable (password-only mode)
|
||||
if !isWindows && c.IP != "" {
|
||||
@@ -706,6 +709,7 @@ func (m *Manager) StopContainer(id int) error {
|
||||
return fmt.Errorf("container not found: %d", id)
|
||||
}
|
||||
_ = lxc.NewManager().CleanPortMappings(id)
|
||||
lxc.CleanFirewallRules(id)
|
||||
name := c.VirshName()
|
||||
status, _ := m.GetContainerStatus(name)
|
||||
if status != "running" {
|
||||
@@ -1183,6 +1187,7 @@ func (m *Manager) prepareVMForColdCopy(id int, name string) (bool, error) {
|
||||
time.Sleep(time.Second)
|
||||
} else {
|
||||
_ = lxc.NewManager().CleanPortMappings(id)
|
||||
lxc.CleanFirewallRules(id)
|
||||
}
|
||||
return wasRunning, nil
|
||||
}
|
||||
@@ -2486,6 +2491,9 @@ func (m *Manager) EnsureSSH(id int) error {
|
||||
if mapErr := lxc.NewManager().ApplyPortMappings(id); mapErr != nil {
|
||||
return mapErr
|
||||
}
|
||||
if err := lxc.ApplyFirewallRules(id); err != nil {
|
||||
fmt.Printf("Warning: failed to apply firewall rules: %v\n", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if lastErr == nil {
|
||||
|
||||
@@ -1324,6 +1324,9 @@ func (m *Manager) StartContainer(id int) error {
|
||||
if err := m.ApplyPortMappings(id); err != nil {
|
||||
fmt.Printf("Warning: failed to apply port mappings: %v\n", err)
|
||||
}
|
||||
if err := ApplyFirewallRules(id); err != nil {
|
||||
fmt.Printf("Warning: failed to apply firewall rules: %v\n", err)
|
||||
}
|
||||
if c.IPv6 != "" || len(c.IPv6Addresses) > 0 {
|
||||
if err := m.ApplyIPv6(id); err != nil {
|
||||
fmt.Printf("Warning: failed to apply IPv6 routing for %s: %v\n", lxcName, err)
|
||||
@@ -1472,11 +1475,13 @@ func (m *Manager) StopContainer(id int) error {
|
||||
if status != "running" {
|
||||
config.UpdateContainerStatus(id, "stopped")
|
||||
m.CleanPortMappings(id)
|
||||
CleanFirewallRules(id)
|
||||
m.cleanupBandwidthLimit(lxcName)
|
||||
return nil
|
||||
}
|
||||
|
||||
m.CleanPortMappings(id)
|
||||
CleanFirewallRules(id)
|
||||
m.cleanupBandwidthLimit(lxcName)
|
||||
|
||||
cmd := exec.Command("lxc-stop", "-n", lxcName)
|
||||
@@ -2585,6 +2590,7 @@ func (m *Manager) ReinstallContainer(id int, templateID string, authConfig ...Co
|
||||
|
||||
// Clean port mappings temporarily
|
||||
m.CleanPortMappings(id)
|
||||
CleanFirewallRules(id)
|
||||
|
||||
// Download the new OS into a temporary container, then replace only the
|
||||
// existing rootfs. The target container directory and config are preserved.
|
||||
|
||||
@@ -568,3 +568,176 @@ func hostPortKey(hostIP string, port int) int {
|
||||
}
|
||||
return port + (sum % 1000000 * 100000)
|
||||
}
|
||||
|
||||
// CleanFirewallRules removes all firewall rules for a container from the FORWARD chain.
|
||||
func CleanFirewallRules(id int) {
|
||||
tag := clicdTag(id)
|
||||
// Remove all rules with the firewall tag prefix
|
||||
cmd := exec.Command("bash", "-c",
|
||||
fmt.Sprintf("iptables -S FORWARD 2>/dev/null | grep 'clicd-%s-fw-' | sed 's/^-A /-D /' | while read rule; do iptables $rule; done", tag))
|
||||
cmd.CombinedOutput()
|
||||
|
||||
// Also remove legacy default policy rules (without specific rule ID)
|
||||
for _, suffix := range []string{"default-in", "default-out"} {
|
||||
for _, proto := range []string{"tcp", "udp"} {
|
||||
exec.Command("iptables", "-D", "FORWARD",
|
||||
"-m", "comment", "--comment", fmt.Sprintf("clicd-%s-fw-%s-%s", tag, suffix, proto),
|
||||
).CombinedOutput()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ApplyFirewallRules applies iptables FORWARD rules for a container's firewall configuration.
|
||||
func ApplyFirewallRules(id int) error {
|
||||
c := config.FindContainer(id)
|
||||
if c == nil {
|
||||
return fmt.Errorf("container not found: %d", id)
|
||||
}
|
||||
|
||||
// Always clean existing firewall rules first
|
||||
CleanFirewallRules(id)
|
||||
|
||||
// If firewall is disabled or no rules, nothing to apply
|
||||
if !c.FirewallEnabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
bridge := "lxcbr0"
|
||||
if c.IsKVM() {
|
||||
bridge = "virbr0"
|
||||
}
|
||||
containerIP := c.IP
|
||||
if containerIP == "" {
|
||||
return nil
|
||||
}
|
||||
tag := clicdTag(id)
|
||||
|
||||
// Apply default DROP policy first (inserted at position 1).
|
||||
// Then insert ACCEPT rules (also at position 1), which pushes the DROPs down.
|
||||
// Final order: ACCEPT rules on top, DROP defaults below, bridge ACCEPT rules at the bottom.
|
||||
applyDefaultFirewallPolicy(tag, bridge, containerIP)
|
||||
|
||||
for _, rule := range c.FirewallRules {
|
||||
if !rule.Enabled {
|
||||
continue
|
||||
}
|
||||
if err := applyOneFirewallRule(tag, bridge, containerIP, rule); err != nil {
|
||||
fmt.Printf("Warning: failed to apply firewall rule %s for container %d: %v\n", rule.ID, id, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func applyOneFirewallRule(tag, bridge, containerIP string, rule config.FirewallRule) error {
|
||||
commentTag := fmt.Sprintf("clicd-%s-fw-%s", tag, rule.ID)
|
||||
|
||||
// Build base iptables args
|
||||
args := []string{"-I", "FORWARD", "1"}
|
||||
|
||||
// Direction: in = traffic arriving at container (-i bridge -d containerIP)
|
||||
// out = traffic leaving container (-o bridge -s containerIP)
|
||||
switch rule.Direction {
|
||||
case "in":
|
||||
args = append(args, "-i", bridge, "-d", containerIP+"/32")
|
||||
case "out":
|
||||
args = append(args, "-o", bridge, "-s", containerIP+"/32")
|
||||
default:
|
||||
return fmt.Errorf("invalid direction: %s", rule.Direction)
|
||||
}
|
||||
|
||||
// Protocol
|
||||
switch rule.Protocol {
|
||||
case "tcp", "udp":
|
||||
args = append(args, "-p", rule.Protocol)
|
||||
case "icmp":
|
||||
args = append(args, "-p", "icmp")
|
||||
case "all":
|
||||
// no protocol filter
|
||||
default:
|
||||
return fmt.Errorf("invalid protocol: %s", rule.Protocol)
|
||||
}
|
||||
|
||||
// Port matching (only for tcp/udp)
|
||||
if rule.Port != "" && (rule.Protocol == "tcp" || rule.Protocol == "udp") {
|
||||
// For "in" direction, traffic going TO the container uses --dport
|
||||
// For "out" direction, traffic going FROM the container uses --dport (destination port on remote)
|
||||
args = append(args, "--dport", normalizePortSpec(rule.Port))
|
||||
}
|
||||
|
||||
// Source IP filter (for "out" direction, this matches the remote source; for "in", it matches the sender)
|
||||
if rule.SourceIP != "" {
|
||||
switch rule.Direction {
|
||||
case "in":
|
||||
args = append(args, "-s", rule.SourceIP)
|
||||
case "out":
|
||||
args = append(args, "-d", rule.SourceIP)
|
||||
}
|
||||
}
|
||||
|
||||
// Action
|
||||
action := "DROP"
|
||||
if rule.Action == "ACCEPT" {
|
||||
action = "ACCEPT"
|
||||
}
|
||||
args = append(args, "-j", action)
|
||||
|
||||
// Comment tag for cleanup
|
||||
args = append(args, "-m", "comment", "--comment", commentTag)
|
||||
|
||||
cmd := exec.Command("iptables", args...)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("iptables error: %s", string(output))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// normalizePortSpec converts user port input to iptables-compatible port spec.
|
||||
// "80,443" -> "80,443", "8000-9000" -> "8000:9000", "22" -> "22"
|
||||
func normalizePortSpec(port string) string {
|
||||
port = strings.TrimSpace(port)
|
||||
if port == "" {
|
||||
return ""
|
||||
}
|
||||
// Convert comma-separated to iptables format (already valid)
|
||||
// Convert dash range to colon range: "8000-9000" -> "8000:9000"
|
||||
if strings.Contains(port, "-") && !strings.Contains(port, ":") {
|
||||
parts := strings.SplitN(port, "-", 2)
|
||||
if len(parts) == 2 {
|
||||
return strings.TrimSpace(parts[0]) + ":" + strings.TrimSpace(parts[1])
|
||||
}
|
||||
}
|
||||
return port
|
||||
}
|
||||
|
||||
func applyDefaultFirewallPolicy(tag, bridge, containerIP string) {
|
||||
// Default DROP: inserted at position 1 so they sit above bridge ACCEPT rules.
|
||||
// The user-defined ACCEPT rules (also at position 1) were inserted first,
|
||||
// so they end up above these DROP defaults after the position-1 insertions.
|
||||
for _, proto := range []string{"tcp", "udp"} {
|
||||
args := []string{
|
||||
"-I", "FORWARD", "1",
|
||||
"-i", bridge,
|
||||
"-d", containerIP + "/32",
|
||||
"-p", proto,
|
||||
"-j", "DROP",
|
||||
"-m", "comment", "--comment", fmt.Sprintf("clicd-%s-fw-default-in-%s", tag, proto),
|
||||
}
|
||||
cmd := exec.Command("iptables", args...)
|
||||
cmd.CombinedOutput()
|
||||
}
|
||||
|
||||
for _, proto := range []string{"tcp", "udp"} {
|
||||
args := []string{
|
||||
"-I", "FORWARD", "1",
|
||||
"-o", bridge,
|
||||
"-s", containerIP + "/32",
|
||||
"-p", proto,
|
||||
"-j", "DROP",
|
||||
"-m", "comment", "--comment", fmt.Sprintf("clicd-%s-fw-default-out-%s", tag, proto),
|
||||
}
|
||||
cmd := exec.Command("iptables", args...)
|
||||
cmd.CombinedOutput()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user