Compare commits

..
18 Commits
Author SHA1 Message Date
CityFun 6cb65e2290 fix: Fixed with ollama page not work (#13855) 2026-09-18 11:35:46 +08:00
ssongliu 0bad1b471f feat(core): add runtime diagnostics and pprof capture (#13852) 2026-09-17 15:46:06 +08:00
ssongliu 3814525edd fix(core): avoid enumerating authorized IP subnets (#13851)
Use net.IPNet.Contains to check CIDR membership directly and remove the address increment loop. Large authorized subnets no longer cause per-request address enumeration and excessive CPU usage.
2026-09-17 15:45:57 +08:00
ssongliu 8162dd1856 fix(container): improve inspect panel theme and text layout (#13849) 2026-09-17 15:45:48 +08:00
ssongliu e833787020 fix(firewall): preserve whitelist priority and rule ordering (#13845) 2026-09-17 13:02:08 +08:00
ssongliu 673ffac516 refactor(firewall): simplify whitelist configuration and rule protection (#13838) 2026-09-16 22:09:49 +08:00
ssongliu e864610015 fix(firewall): validate whitelist ports and sources in form (#13835) 2026-09-16 21:31:06 +08:00
ssongliu 78402e1b7d refactor(firewall): consolidate utilities and flatten packages (#13833) 2026-09-16 16:48:48 +08:00
ssongliu 782bc1e67c fix(firewall): manage SSH access and queue stop operations (#13831)
* fix(firewall): manage SSH access and queue stop operations

* fix(firewall): reconcile whitelist rules and sync differences
2026-09-16 16:45:33 +08:00
ssongliu 86e4ed6f64 perf(firewall): optimize large rule sets and queue deletions (#13829) 2026-09-16 15:34:10 +08:00
CityFun ee8bac39af style: Optimize the website configuration UI (#13828) 2026-09-16 14:48:57 +08:00
ssongliu fe742b9f41 fix(firewall): improve whitelist management and rule lifecycle (#13826) 2026-09-15 23:55:01 +08:00
CityFun 9a5bd9bcba fix: Fix the issue where Brotli settings cannot be saved when enabled in OpenResty (#13822) 2026-09-15 18:36:51 +08:00
ssongliu b9c8e39560 fix: validate Docker IPv4 forwarding (#13820) 2026-09-15 18:36:36 +08:00
CityFun 6b20ff0b13 feat: OpenClaw supports configuring model Max Tokens (#13818) 2026-09-15 18:36:24 +08:00
ssongliu 89bd32b6d4 fix(terminal): isolate persistent shortcut sessions (#13810) 2026-09-15 10:05:16 +08:00
ssongliu 005f240fb7 fix: improve firewall lifecycle and sync (#13809) 2026-09-15 10:04:53 +08:00
蘭 75da53e374 feat: Remote download supports server file name options and improves error handling (#13808)
* feat: Remote download supports server file name options and improves error handling

* feat: Remote download supports server file name options and improves error handling
2026-09-15 10:04:44 +08:00
141 changed files with 5545 additions and 2746 deletions
+57 -12
View File
@@ -60,7 +60,7 @@ func (b *BaseApi) LoadFirewallBaseInfo(c *gin.Context) {
// @Summary Operate firewall
// @Accept json
// @Param request body dto.FirewallLifecycleOperation true "request"
// @Success 200
// @Success 200 {object} dto.FirewallLifecycleOperationResponse
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/operate [post]
@@ -71,12 +71,13 @@ func (b *BaseApi) OperateFirewall(c *gin.Context) {
return
}
if err := firewallService.OperateFirewall(request); err != nil {
result, err := firewallService.QueueFirewallOperation(request)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
helper.SuccessWithData(c, result)
}
// @Tags Firewall
@@ -367,7 +368,8 @@ func (b *BaseApi) SyncFirewallRules(c *gin.Context) {
}
// @Tags Firewall
// @Summary Delete managed unified firewall v2 rules
// @Summary Queue firewall rule deletion
// @Description Deletes managed rules by UUID or unprotected before-chain rules by instance key. Returns a taskID immediately; results are written to the task log.
// @Accept json
// @Param request body dto.FirewallRuleDelete true "request"
// @Success 200 {object} dto.FirewallRuleDeleteResponse
@@ -491,26 +493,69 @@ func (b *BaseApi) LoadFirewallSettings(c *gin.Context) {
}
// @Tags Firewall
// @Summary Queue firewall port whitelist update
// @Description Returns a taskID; configuration save and per-rule results are recorded in the task log.
// @Summary Create firewall port whitelist rules
// @Description Saves whitelist configuration only. Missing rules are added on startup, restart, initialization, or synchronization; existing rules are not removed.
// @Accept json
// @Param request body dto.FirewallPortWhitelistUpdate true "request"
// @Success 200 {object} dto.FilterChainOperationResponse
// @Param request body dto.FirewallPortWhitelistCreate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/settings/whitelist [post]
// @x-panel-log {"bodyKeys":["value"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新防火墙端口白名单 [value]","formatEN":"update firewall port whitelist [value]"}
// @x-panel-log {"bodyKeys":["rule"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建防火墙端口白名单","formatEN":"create firewall port whitelist"}
func (b *BaseApi) CreateFirewallPortWhitelist(c *gin.Context) {
var request dto.FirewallPortWhitelistCreate
if err := helper.CheckBindAndValidate(&request, c); err != nil {
return
}
if err := firewallSettingService.CreatePortWhitelist(c.Request.Context(), request); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags Firewall
// @Summary Update firewall port whitelist rules
// @Description Saves whitelist configuration only. Missing rules are added on startup, restart, initialization, or synchronization; existing rules are not removed.
// @Accept json
// @Param request body dto.FirewallPortWhitelistUpdate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/settings/whitelist/update [post]
// @x-panel-log {"bodyKeys":["oldRule","rule"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"编辑防火墙端口白名单","formatEN":"update firewall port whitelist"}
func (b *BaseApi) UpdateFirewallPortWhitelist(c *gin.Context) {
var request dto.FirewallPortWhitelistUpdate
if err := helper.CheckBindAndValidate(&request, c); err != nil {
return
}
result, err := firewallSettingService.QueuePortWhitelist(request.Value)
if err != nil {
if err := firewallSettingService.UpdatePortWhitelist(c.Request.Context(), request); err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, result)
helper.Success(c)
}
// @Tags Firewall
// @Summary Delete firewall port whitelist rules
// @Description Saves whitelist configuration only. Missing rules are added on startup, restart, initialization, or synchronization; existing rules are not removed.
// @Accept json
// @Param request body dto.FirewallPortWhitelistDelete true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/settings/whitelist/delete [post]
// @x-panel-log {"bodyKeys":["rules"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除防火墙端口白名单","formatEN":"delete firewall port whitelist"}
func (b *BaseApi) DeleteFirewallPortWhitelist(c *gin.Context) {
var request dto.FirewallPortWhitelistDelete
if err := helper.CheckBindAndValidate(&request, c); err != nil {
return
}
if err := firewallSettingService.DeletePortWhitelist(c.Request.Context(), request); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags Firewall
+10 -7
View File
@@ -29,6 +29,7 @@ import (
// @Summary Ws local terminal
// @Param command query string false "command"
// @Param session query string false "session id to reattach"
// @Param terminalPersistent query boolean false "allow recovery after an unexpected disconnect"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
@@ -43,6 +44,7 @@ func (b *BaseApi) WsLocalTerminal(c *gin.Context) {
// @Param command query string false "command"
// @Param session query string false "session id to reattach"
// @Param title query string false "session title shown in the session list"
// @Param terminalPersistent query boolean false "allow recovery after an unexpected disconnect"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
@@ -146,13 +148,14 @@ func (b *BaseApi) runSSHSession(c *gin.Context, kind string, connect func() (*ss
hostID, _ = strconv.Atoi(c.DefaultQuery("id", "0"))
}
opts := terminal.SessionOptions{
Identity: identity,
Kind: kind,
Title: sanitizeTerminalTitle(c.Query("title")),
HostID: uint(max(hostID, 0)),
Cols: cols,
Rows: rows,
InitCmd: command,
Identity: identity,
Kind: kind,
Title: sanitizeTerminalTitle(c.Query("title")),
Persistent: c.Query("terminalPersistent") == "true",
HostID: uint(max(hostID, 0)),
Cols: cols,
Rows: rows,
InitCmd: command,
}
err := terminal.Serve(wsConn, strings.TrimSpace(c.Query("session")), opts, func() (*gossh.Client, error) {
client, err := connect()
+16 -7
View File
@@ -162,16 +162,25 @@ type AgentWebsiteBindReq struct {
}
type AgentModelConfigUpdateReq struct {
AgentID uint `json:"agentId" validate:"required"`
AccountID uint `json:"accountId" validate:"required"`
Model string `json:"model" validate:"required"`
Fallbacks []string `json:"fallbacks"`
AgentID uint `json:"agentId" validate:"required"`
AccountID uint `json:"accountId" validate:"required"`
Model string `json:"model" validate:"required"`
Fallbacks []string `json:"fallbacks"`
Metadata []AgentModelMetadata `json:"metadata" validate:"dive"`
}
type AgentModelConfig struct {
AccountID uint `json:"accountId"`
Model string `json:"model"`
Fallbacks []string `json:"fallbacks"`
AccountID uint `json:"accountId"`
Model string `json:"model"`
Fallbacks []string `json:"fallbacks"`
Metadata []AgentModelMetadata `json:"metadata"`
}
type AgentModelMetadata struct {
Model string `json:"model" validate:"required"`
InputMode string `json:"inputMode" validate:"required,oneof=auto text image"`
ContextWindow int `json:"contextWindow" validate:"min=0"`
MaxTokens int `json:"maxTokens" validate:"min=0"`
}
type AgentHermesChatSessionItem struct {
+35 -12
View File
@@ -1,6 +1,7 @@
package dto
import (
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
firewallsync "github.com/1Panel-dev/1Panel/agent/utils/firewall/sync"
)
@@ -18,6 +19,7 @@ type FirewallSubsystemStatus struct {
Message string `json:"message,omitempty"`
Reason string `json:"reason,omitempty"`
SyncError string `json:"syncError,omitempty"`
LifecycleTaskID string `json:"lifecycleTaskID,omitempty"`
IPv4 FirewallBackendFamilyStatus `json:"ipv4"`
IPv6 FirewallBackendFamilyStatus `json:"ipv6"`
}
@@ -27,6 +29,11 @@ type FirewallLifecycleOperation struct {
WithDockerRestart bool `json:"withDockerRestart"`
}
type FirewallLifecycleOperationResponse struct {
TaskID string `json:"taskID,omitempty"`
Queued bool `json:"queued"`
}
type FirewallBackendOption struct {
Name string `json:"name"`
Installed bool `json:"installed"`
@@ -55,15 +62,26 @@ type FirewallBackendGroup struct {
}
type FirewallSettings struct {
System FirewallBackendGroup `json:"system"`
Forwarding FirewallBackendGroup `json:"forwarding"`
Docker FirewallBackendGroup `json:"docker"`
PingStatus string `json:"pingStatus"`
PortWhitelist string `json:"portWhiteList"`
System FirewallBackendGroup `json:"system"`
Forwarding FirewallBackendGroup `json:"forwarding"`
Docker FirewallBackendGroup `json:"docker"`
PingStatus string `json:"pingStatus"`
PortWhitelist []filter.PortWhitelist `json:"portWhiteList"`
PanelPort string `json:"panelPort"`
SSHPort string `json:"sshPort"`
}
type FirewallPortWhitelistCreate struct {
Rule filter.PortWhitelist `json:"rule" validate:"required"`
}
type FirewallPortWhitelistUpdate struct {
Value string `json:"value" validate:"required"`
OldRule filter.PortWhitelist `json:"oldRule" validate:"required"`
Rule filter.PortWhitelist `json:"rule" validate:"required"`
}
type FirewallPortWhitelistDelete struct {
Rule *filter.PortWhitelist `json:"rule" validate:"required"`
}
type FirewallBackendOperation struct {
@@ -87,11 +105,7 @@ type FirewallInitializationTask struct {
TaskID string `json:"taskID,omitempty" validate:"omitempty,max=64"`
}
type FirewallSystemPort struct {
Family string
Port string
Protocol string
}
type FirewallSystemPort = firewall.SystemPort
type FirewallRuleInventoryResponse struct {
IPv4Range filter.PositionRange `json:"ipv4Range"`
@@ -114,6 +128,7 @@ type FirewallRuleReset struct {
}
type FirewallRuleInventory struct {
Refresh bool `json:"refresh,omitempty"`
PageInfo
Scope filter.Scope `json:"scope,omitempty"`
Scopes []filter.Scope `json:"scopes,omitempty" validate:"max=16"`
@@ -313,10 +328,18 @@ type FirewallRuleSyncFailure struct {
}
type FirewallRuleDelete struct {
UUIDs []string `json:"uuids" validate:"required,min=1,dive,required,max=64"`
UUIDs []string `json:"uuids" validate:"omitempty,dive,required,max=64"`
BeforeRules []FirewallRuleDeleteTarget `json:"beforeRules,omitempty" validate:"omitempty,dive"`
}
type FirewallRuleDeleteTarget struct {
Scope filter.Scope `json:"scope" validate:"required"`
InstanceKey string `json:"instanceKey" validate:"required,max=128"`
}
type FirewallRuleDeleteResponse struct {
TaskID string `json:"taskID,omitempty"`
Queued bool `json:"queued,omitempty"`
Succeeded int `json:"succeeded"`
Failed int `json:"failed"`
Errors []FirewallRuleDeleteFailure `json:"errors,omitempty"`
+1
View File
@@ -122,6 +122,7 @@ type FileWget struct {
Name string `json:"name" validate:"required"`
IgnoreCertificate bool `json:"ignoreCertificate"`
UseProxy bool `json:"useProxy"`
UseServerFilename bool `json:"useServerFilename"`
}
type FileMove struct {
-6
View File
@@ -29,12 +29,6 @@ type FirewallRuleRepo struct {
db *gorm.DB
}
func WithFirewallRuleSource(kind, id string) DBOption {
return func(db *gorm.DB) *gorm.DB {
return db.Where("owner = ?", model.FirewallRuleOwner(kind, id))
}
}
func NewIFirewallRuleRepo() IFirewallRuleRepo {
return &FirewallRuleRepo{}
}
+3 -2
View File
@@ -936,6 +936,7 @@ func (a AgentService) GetModelConfig(req dto.AgentIDReq) (*dto.AgentModelConfig,
AccountID: agent.AccountID,
Model: model,
Fallbacks: extractOpenclawFallbackModelIDs(conf, account, models, model),
Metadata: extractOpenclawModelMetadata(conf, account, models),
}, nil
}
@@ -967,7 +968,7 @@ func (a AgentService) UpdateModelConfig(req dto.AgentModelConfigUpdateReq) error
if agent.AgentType != constant.AppOpenclaw {
return fmt.Errorf("%s does not support", agent.AgentType)
}
if err := writeOpenclawConfig(confDir, account, modelName, agent.Token, nil, req.Fallbacks); err != nil {
if err := writeOpenclawConfig(confDir, account, modelName, agent.Token, nil, req.Fallbacks, req.Metadata); err != nil {
return err
}
}
@@ -1684,7 +1685,7 @@ func (a AgentService) syncAgentsByAccount(account *model.AgentAccount) error {
return err
}
fallbacks := extractOpenclawFallbackModelIDs(conf, account, accountModels, selectedAccountModel.ID)
if err := writeOpenclawConfig(confDir, account, modelName, agent.Token, nil, fallbacks); err != nil {
if err := writeOpenclawConfig(confDir, account, modelName, agent.Token, nil, fallbacks, nil); err != nil {
return err
}
case constant.AppHermesAgent:
+4
View File
@@ -1354,6 +1354,10 @@ func installOpenclawPlugin(mgr *cmd.CommandHelper, containerName, spec, pluginID
} else if slices.Contains(options, "--dangerously-force-unsafe-install") {
args = append(args, "--dangerously-force-unsafe-install")
}
// Source confirmation does not grant the selected channel plugin's capabilities.
if slices.Contains(options, "--accept-capabilities") {
args = append(args, "--accept-capabilities")
}
return mgr.Run("docker", args...)
}
+150 -5
View File
@@ -10,6 +10,7 @@ import (
"net/url"
"path"
"regexp"
"slices"
"strconv"
"strings"
"time"
@@ -737,9 +738,11 @@ type modelProvider struct {
}
type modelEntry struct {
ID string `json:"id"`
Name string `json:"name"`
Input []string `json:"input,omitempty"`
ID string `json:"id"`
Name string `json:"name"`
Input []string `json:"input,omitempty"`
ContextWindow int `json:"contextWindow,omitempty"`
MaxTokens int `json:"maxTokens,omitempty"`
}
func requiresOpenclawProviderModels(provider string) bool {
@@ -767,7 +770,7 @@ type browserConfig struct {
DefaultProfile string `json:"defaultProfile"`
}
func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName, token string, allowedOrigins []string, fallbacks []string) error {
func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName, token string, allowedOrigins []string, fallbacks []string, metadata []dto.AgentModelMetadata) error {
if strings.TrimSpace(confDir) == "" {
return fmt.Errorf("config dir is required")
}
@@ -852,6 +855,7 @@ func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName,
}
conf = initial
} else {
preserveOpenclawModelMetadata(conf, cfg.Models)
if err := applyOpenclawModelsConfig(conf, cfg.Models); err != nil {
return err
}
@@ -906,6 +910,9 @@ func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName,
if allowedOrigins != nil {
setSecurityConfig(conf, dto.AgentSecurityConfig{AllowedOrigins: allowedOrigins})
}
if err := applyOpenclawModelMetadata(conf, account, metadata); err != nil {
return err
}
if err := writeOpenclawConfigRaw(configPath, conf); err != nil {
return err
}
@@ -920,6 +927,144 @@ func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName,
return writeAgentEnvMap(path.Join(confDir, ".env"), envMap, order)
}
func readOpenclawModelsConfig(conf map[string]interface{}) *modelsConfig {
raw, ok := conf["models"]
if !ok {
return nil
}
payload, err := json.Marshal(raw)
if err != nil {
return nil
}
var models modelsConfig
if err := json.Unmarshal(payload, &models); err != nil {
return nil
}
return &models
}
func preserveOpenclawModelMetadata(conf map[string]interface{}, next *modelsConfig) {
current := readOpenclawModelsConfig(conf)
if current == nil || next == nil {
return
}
for providerID, nextProvider := range next.Providers {
currentProvider, ok := current.Providers[providerID]
if !ok {
continue
}
byID := make(map[string]modelEntry, len(currentProvider.Models))
for _, entry := range currentProvider.Models {
byID[entry.ID] = entry
}
for index := range nextProvider.Models {
currentEntry, ok := byID[nextProvider.Models[index].ID]
if !ok {
continue
}
nextProvider.Models[index].Input = currentEntry.Input
nextProvider.Models[index].ContextWindow = currentEntry.ContextWindow
nextProvider.Models[index].MaxTokens = currentEntry.MaxTokens
}
next.Providers[providerID] = nextProvider
}
}
func extractOpenclawModelMetadata(conf map[string]interface{}, account *model.AgentAccount, accountModels []dto.AgentAccountModel) []dto.AgentModelMetadata {
result := make([]dto.AgentModelMetadata, 0, len(accountModels))
configured := readOpenclawModelsConfig(conf)
for _, item := range accountModels {
_, inferred, providerID, _, err := buildOpenclawAccountModelConfig(account, item)
if err != nil {
continue
}
metadata := dto.AgentModelMetadata{Model: item.ID, InputMode: "auto"}
if configured != nil {
for _, entry := range configured.Providers[providerID].Models {
if entry.ID != inferred.ID {
continue
}
metadata.ContextWindow = entry.ContextWindow
metadata.MaxTokens = entry.MaxTokens
if len(entry.Input) > 0 && !slices.Equal(entry.Input, inferred.Input) {
if slices.Contains(entry.Input, "image") {
metadata.InputMode = "image"
} else {
metadata.InputMode = "text"
}
}
break
}
}
result = append(result, metadata)
}
return result
}
func applyOpenclawModelMetadata(conf map[string]interface{}, account *model.AgentAccount, requested []dto.AgentModelMetadata) error {
if len(requested) == 0 {
return nil
}
configured := readOpenclawModelsConfig(conf)
if configured == nil {
return fmt.Errorf("model metadata is not supported for provider %s", account.Provider)
}
accountModels, err := loadAgentAccountModels(account)
if err != nil {
return err
}
available := make(map[string]dto.AgentAccountModel, len(accountModels))
for _, item := range accountModels {
available[item.ID] = item
}
seen := make(map[string]struct{}, len(requested))
for _, metadata := range requested {
item, ok := available[metadata.Model]
if !ok {
return buserr.New("ErrAgentModelNotInAccount")
}
if _, ok := seen[metadata.Model]; ok {
return fmt.Errorf("duplicate model metadata: %s", metadata.Model)
}
seen[metadata.Model] = struct{}{}
_, inferred, providerID, _, err := buildOpenclawAccountModelConfig(account, item)
if err != nil {
return err
}
provider := configured.Providers[providerID]
found := false
for index := range provider.Models {
if provider.Models[index].ID != inferred.ID {
continue
}
found = true
provider.Models[index].ContextWindow = metadata.ContextWindow
provider.Models[index].MaxTokens = metadata.MaxTokens
switch metadata.InputMode {
case "auto":
provider.Models[index].Input = inferred.Input
case "text":
provider.Models[index].Input = []string{"text"}
case "image":
provider.Models[index].Input = []string{"text", "image"}
default:
return fmt.Errorf("unsupported model input mode: %s", metadata.InputMode)
}
break
}
if !found {
return buserr.New("ErrAgentModelNotInAccount")
}
configured.Providers[providerID] = provider
}
modelsMap, err := structToMap(configured)
if err != nil {
return err
}
conf["models"] = modelsMap
return nil
}
func resolveOpenclawFallbackModels(account *model.AgentAccount, primaryModel string, fallbackIDs []string) ([]string, error) {
accountModels, err := loadAgentAccountModels(account)
if err != nil {
@@ -1041,7 +1186,7 @@ func prepareOpenclawInstallFiles(appInstall *model.AppInstall, account *model.Ag
return fmt.Errorf("app install is required")
}
confDir := path.Join(appInstall.GetPath(), "data", "conf")
if err := writeOpenclawConfig(confDir, account, modelName, token, allowedOrigins, nil); err != nil {
if err := writeOpenclawConfig(confDir, account, modelName, token, allowedOrigins, nil, nil); err != nil {
return err
}
dataDir := path.Join(appInstall.GetPath(), "data")
+6
View File
@@ -18,6 +18,7 @@ import (
"github.com/1Panel-dev/1Panel/agent/utils/common"
"github.com/1Panel-dev/1Panel/agent/utils/controller"
"github.com/1Panel-dev/1Panel/agent/utils/docker"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/docker_guard"
)
const dockerNftablesMinVersion = "29.0.0"
@@ -82,6 +83,11 @@ func (u *DockerService) UpdateFirewallBackend(backend string) error {
if backend == constant.FirewallProviderNftables && !dockerNftablesSupported(version) {
return fmt.Errorf("Docker Engine %s or later is required for the nftables firewall backend", dockerNftablesMinVersion)
}
if backend == constant.FirewallProviderNftables {
if err := docker_guard.CheckIPv4Forwarding(); err != nil {
return err
}
}
original, readErr := os.ReadFile(constant.DaemonJsonPath)
existed := readErr == nil
+1
View File
@@ -896,6 +896,7 @@ func (f *FileService) Wget(w request.FileWget) (string, error) {
key := "file-wget-" + common.GetUuid()
options := files.DownloadOptions{
IgnoreCertificate: w.IgnoreCertificate,
UseServerFilename: w.UseServerFilename,
}
if w.UseProxy {
systemProxy, err := NewISettingService().GetSystemProxy()
File diff suppressed because it is too large Load Diff
-144
View File
@@ -1,144 +0,0 @@
package service
import (
"context"
"errors"
"fmt"
"strconv"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle"
)
type panelPortWhitelistKey struct{}
func (s *FirewallService) UpdatePanelPort(ctx context.Context, oldPort, port uint) error {
if oldPort == 0 || oldPort > 65535 || port == 0 || port > 65535 {
return fmt.Errorf("invalid panel port transition %d -> %d", oldPort, port)
}
if LoadPanelPort() != strconv.Itoa(int(oldPort)) {
return fmt.Errorf("panel port changed before firewall update")
}
if oldPort == port {
return nil
}
client, err := s.baseClient()
if err != nil {
if configuredSystemFirewallBackend() == "" && len(lifecycle.InstalledProviders()) == 0 {
return nil
}
return err
}
provider := client.Name()
active, err := client.Status()
if err != nil {
return err
}
if !supportsManagedFilterChains(provider) && !active {
return nil
}
required, err := loadRequiredFirewallPorts(strconv.Itoa(int(port)))
if err != nil {
return err
}
managedChains := supportsManagedFilterChains(provider)
if managedChains {
firewallRuleMutationMu.Lock()
defer firewallRuleMutationMu.Unlock()
}
configured, err := loadConfiguredFirewallPortWhiteList()
if err != nil {
return err
}
protected := firewall.NormalizePortWhitelist(append(configured, required...))
if managedChains {
prepared := append([]firewall.PortWhitelist{{Port: strconv.Itoa(int(oldPort)), Protocol: "tcp"}}, required...)
if err := syncPanelRequiredPorts(provider, prepared); err != nil {
return err
}
if err := syncPanelRequiredPorts(provider, required); err != nil {
warnPanelPortCleanupFailure(oldPort, err)
return nil
}
warnPanelPortCleanupFailure(oldPort, s.cleanupPanelPortLocked(ctx, provider, oldPort, protected))
return nil
}
ports := systemPorts([]firewall.PortWhitelist{{Port: strconv.Itoa(int(port)), Protocol: "tcp"}})
for _, port := range ports {
if err := s.ensureSystemPort(ctx, port); err != nil {
return err
}
}
firewallRuleMutationMu.Lock()
defer firewallRuleMutationMu.Unlock()
warnPanelPortCleanupFailure(oldPort, s.cleanupPanelPortLocked(ctx, provider, oldPort, protected))
return nil
}
// cleanupPanelPortLocked removes the old system-owned policy as well as any
// remaining managed runtime rule. The caller must hold firewallRuleMutationMu.
func (s *FirewallService) cleanupPanelPortLocked(ctx context.Context, provider string, oldPort uint, protected []firewall.PortWhitelist) error {
ctx = context.WithValue(ctx, panelPortWhitelistKey{}, protected)
ports := systemPorts([]firewall.PortWhitelist{{Port: strconv.Itoa(int(oldPort)), Protocol: "tcp"}})
if provider == constant.FirewallProviderUFW {
for _, port := range ports {
port.Protocol = "all"
ports = append(ports, port)
}
}
// Include family-neutral records left by firewalld or older versions, even
// when the selected backend has since changed.
ports = append(ports, dto.FirewallSystemPort{Port: strconv.Itoa(int(oldPort)), Protocol: "tcp"})
var cleanupErrors []error
for _, port := range ports {
if panelPortStillRequired(port, protected) {
continue
}
records, err := s.systemPortRecords(ctx, port)
if err != nil {
cleanupErrors = append(cleanupErrors, err)
continue
}
for _, record := range records {
if err := s.deleteRule(ctx, record.UUID, true); err != nil && !errors.Is(err, filter.ErrProtectedRule) {
cleanupErrors = append(cleanupErrors, err)
}
}
}
return errors.Join(cleanupErrors...)
}
func warnPanelPortCleanupFailure(port uint, err error) {
if err != nil && global.LOG != nil {
global.LOG.Warnf("clean up old panel firewall port %d failed: %v", port, err)
}
}
func panelPortStillRequired(port dto.FirewallSystemPort, protected []firewall.PortWhitelist) bool {
rule := systemPortRule(filter.ProviderUFW, port)
for _, required := range protected {
if port.Family != "" && required.Family != "" && port.Family != required.Family {
continue
}
other := rule
other.Protocol, other.DestinationPort = required.Protocol, required.Port
if filter.RulesOverlap(rule, other) {
return true
}
}
return false
}
func panelRuleStillRequired(rule filter.FirewallRule, protected []firewall.PortWhitelist) bool {
family := string(rule.Scope.Family)
if rule.Scope.Family == filter.FamilyInet {
family = ""
}
return panelPortStillRequired(dto.FirewallSystemPort{
Family: family, Port: rule.DestinationPort, Protocol: rule.Protocol,
}, protected)
}
+175 -164
View File
@@ -5,35 +5,37 @@ import (
"encoding/json"
"errors"
"fmt"
"os"
"reflect"
"slices"
"sync"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/app/repo"
"github.com/1Panel-dev/1Panel/agent/app/task"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/i18n"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/docker_guard"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
filterruntime "github.com/1Panel-dev/1Panel/agent/utils/firewall/filter/runtime"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/iptables_helper"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/nftables_helper"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/ping"
"gorm.io/gorm"
)
type IFirewallSettingService interface {
QueuePortWhitelist(value string) (dto.FilterChainOperationResponse, error)
CreatePortWhitelist(context.Context, dto.FirewallPortWhitelistCreate) error
UpdatePortWhitelist(context.Context, dto.FirewallPortWhitelistUpdate) error
DeletePortWhitelist(context.Context, dto.FirewallPortWhitelistDelete) error
Load(context.Context) (dto.FirewallSettings, error)
Operate(context.Context, dto.FirewallBackendOperation) error
}
type FirewallSettingService struct{}
var firewallWhitelistTaskMu sync.Mutex
var firewallWhitelistMu sync.Mutex
var ErrFirewallBackendCleanupRequired = errors.New("firewall backend cleanup required")
@@ -50,187 +52,182 @@ func NewIFirewallSettingService() IFirewallSettingService {
return &FirewallSettingService{}
}
func (s *FirewallSettingService) QueuePortWhitelist(value string) (dto.FilterChainOperationResponse, error) {
return s.queuePortWhitelist(value, newFirewallService())
func (s *FirewallSettingService) CreatePortWhitelist(ctx context.Context, request dto.FirewallPortWhitelistCreate) error {
return savePortWhitelist(ctx, func(current []firewall.PortWhitelist) ([]firewall.PortWhitelist, error) {
return append(current, request.Rule), nil
})
}
func (s *FirewallSettingService) queuePortWhitelist(value string, firewallService *FirewallService) (dto.FilterChainOperationResponse, error) {
firewallWhitelistTaskMu.Lock()
defer firewallWhitelistTaskMu.Unlock()
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeFirewall, 0); err != nil {
return dto.FilterChainOperationResponse{}, err
func (s *FirewallSettingService) UpdatePortWhitelist(ctx context.Context, request dto.FirewallPortWhitelistUpdate) error {
return savePortWhitelist(ctx, func(current []firewall.PortWhitelist) ([]firewall.PortWhitelist, error) {
index, err := findPortWhitelistRule(current, request.OldRule)
if err != nil {
return nil, err
}
current[index] = request.Rule
return current, nil
})
}
func (s *FirewallSettingService) DeletePortWhitelist(ctx context.Context, request dto.FirewallPortWhitelistDelete) error {
if request.Rule == nil {
return fmt.Errorf("select one firewall port whitelist rule to delete")
}
taskItem, err := task.NewTask(i18n.GetMsgByKey("FirewallWhitelistTask"), task.TaskUpdate, task.TaskScopeFirewall, "", 0)
return savePortWhitelist(ctx, func(current []firewall.PortWhitelist) ([]firewall.PortWhitelist, error) {
index, err := findPortWhitelistRule(current, *request.Rule)
if err != nil {
return nil, err
}
return slices.Delete(current, index, index+1), nil
})
}
func findPortWhitelistRule(rules []firewall.PortWhitelist, target firewall.PortWhitelist) (int, error) {
index := slices.IndexFunc(rules, func(rule firewall.PortWhitelist) bool {
return samePortWhitelistRule(rule, target)
})
if index < 0 {
return -1, fmt.Errorf("firewall port whitelist rule has changed or no longer exists; refresh and retry")
}
return index, nil
}
func samePortWhitelistRule(left, right firewall.PortWhitelist) bool {
if reflect.DeepEqual(left, right) {
return true
}
normalizedLeft, err := firewall.ValidatePortWhitelist([]firewall.PortWhitelist{left})
if err != nil {
return dto.FilterChainOperationResponse{}, err
return false
}
taskItem.AddSubTaskWithOps(taskItem.Name, func(t *task.Task) error {
succeeded, failed := 0, 0
err := s.applyPortWhitelist(t.TaskCtx, value, firewallService, func(status, label string, err error) {
switch status {
case "applied":
succeeded++
t.LogSuccess(label)
case "failed":
failed++
t.LogFailedWithErr(label, err)
default:
t.Log(i18n.GetWithName(status, label))
}
})
t.Log(i18n.GetMsgWithMap("FirewallRuleOperationResult", map[string]interface{}{
"succeeded": succeeded, "failed": failed,
}))
return err
}, nil, 0, 0)
if err := repo.NewITaskRepo().Save(context.Background(), taskItem.Task); err != nil {
closeUnstartedFirewallTask(taskItem)
return dto.FilterChainOperationResponse{}, fmt.Errorf("save firewall whitelist task: %w", err)
normalizedRight, err := firewall.ValidatePortWhitelist([]firewall.PortWhitelist{right})
if err != nil {
return false
}
go func() { _ = taskItem.Execute() }()
return dto.FilterChainOperationResponse{TaskID: taskItem.TaskID, Queued: true}, nil
slices.Sort(normalizedLeft[0].Sources)
slices.Sort(normalizedRight[0].Sources)
return reflect.DeepEqual(normalizedLeft[0], normalizedRight[0])
}
type whitelistReporter func(status, label string, err error)
func (s *FirewallSettingService) applyPortWhitelist(ctx context.Context, value string, firewallService *FirewallService, report whitelistReporter) error {
func savePortWhitelist(ctx context.Context, change func([]firewall.PortWhitelist) ([]firewall.PortWhitelist, error)) error {
firewallWhitelistMu.Lock()
defer firewallWhitelistMu.Unlock()
firewallRuleMutationMu.Lock()
defer firewallRuleMutationMu.Unlock()
if err := ctx.Err(); err != nil {
return err
}
ports, err := firewall.ParsePortWhitelist(value)
if err != nil {
return err
}
required, err := firewallService.requiredPorts()
if err != nil {
return err
}
removed, err := s.savePortWhitelist(ctx, ports, required, firewallService)
if err != nil {
return err
}
report("FirewallWhitelistSaved", "", nil)
for _, port := range systemPorts(removed) {
report("FirewallWhitelistReleased", whitelistPortLabel(port), nil)
}
ctx = context.WithValue(ctx, panelPortWhitelistKey{}, required)
provider, providerErr := firewallService.selectedProvider(ctx)
ready := s.portWhitelistReadiness(provider, providerErr, firewallService)
return syncPortWhitelist(ctx, ports, required, ready, firewallService.ensureSystemPortLocked, report)
}
func (s *FirewallSettingService) savePortWhitelist(ctx context.Context, ports, required []firewall.PortWhitelist, firewallService *FirewallService) ([]firewall.PortWhitelist, error) {
var removed []firewall.PortWhitelist
err := global.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var setting model.Setting
err := tx.Where("key = ?", constant.FirewallPortWhiteList).First(&setting).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
setting.Value = constant.FirewallPortWhiteListValue
} else if err != nil {
return err
}
previous, err := firewall.ParsePortWhitelist(setting.Value)
defer filterruntime.InvalidateInventory()
return global.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
current, err := loadPortWhitelistSetting(tx)
if err != nil {
return err
}
removed = excludeFirewallPorts(excludeFirewallPorts(previous, ports), required)
txCtx := context.WithValue(ctx, constant.DB, tx)
if err := firewallService.releaseSystemPorts(txCtx, systemPorts(removed)); err != nil {
return err
}
value, err := json.Marshal(ports)
desired, err := change(current)
if err != nil {
return err
}
return tx.Where("key = ?", constant.FirewallPortWhiteList).
Assign(map[string]interface{}{"value": string(value)}).
desired, err = firewall.ValidatePortWhitelist(desired)
if err != nil {
return err
}
value, err := json.Marshal(desired)
if err != nil {
return err
}
return tx.Where("key = ?", constant.FirewallPortWhiteList).Assign(map[string]interface{}{"value": string(value)}).
FirstOrCreate(&model.Setting{Key: constant.FirewallPortWhiteList}).Error
})
return removed, err
}
func (s *FirewallSettingService) portWhitelistReadiness(provider filter.Provider, providerErr error, firewallService *FirewallService) func(dto.FirewallSystemPort) (bool, error) {
type state struct {
ready bool
err error
func checkFirewallRuleWhitelistProtection(provider filter.Provider, record model.FirewallRule) error {
ports, err := loadFirewallPortWhiteList()
if err != nil {
return err
}
states := make(map[string]state)
return func(port dto.FirewallSystemPort) (bool, error) {
if providerErr != nil {
return false, providerErr
rules, err := record.RulesForProvider(provider)
if err != nil {
return err
}
for _, rule := range rules {
if filter.RuleMatchesPortWhitelist(rule, ports) {
return filter.ErrProtectedRule
}
key := "service"
if isDirectFirewallProvider(provider) {
key = port.Family
}
return nil
}
func loadPortWhitelistSetting(db *gorm.DB) ([]firewall.PortWhitelist, error) {
var setting model.Setting
if err := db.Where("key = ?", constant.FirewallPortWhiteList).First(&setting).Error; errors.Is(err, gorm.ErrRecordNotFound) {
setting.Value = constant.FirewallPortWhiteListValue
} else if err != nil {
return nil, err
}
var rules []firewall.PortWhitelist
err := json.Unmarshal([]byte(setting.Value), &rules)
return rules, err
}
func loadSSHWhitelistPortFrom(path string) (string, error) {
directives, _, err := parseSSHConfigTree(path)
if errors.Is(err, os.ErrNotExist) {
return defaultSSHPort, nil
}
if err != nil {
return "", err
}
return loadSSHPortValues(directives)[0], nil
}
func customWhitelist(entries []firewall.PortWhitelist) []firewall.PortWhitelist {
result := make([]firewall.PortWhitelist, 0, len(entries))
for _, entry := range entries {
if entry.Type == "" {
result = append(result, entry)
}
if cached, ok := states[key]; ok {
return cached.ready, cached.err
}
return result
}
func InitializeFirewallWhitelistPorts(entries []firewall.PortWhitelist) ([]firewall.PortWhitelist, error) {
entries = slices.Clone(entries)
var sshPort string
for i := range entries {
entry := &entries[i]
if entry.Type == "" || entry.Port != "" {
continue
}
var result state
if isDirectFirewallProvider(provider) {
initialized, bound, err := loadSystemFirewallFamilyStatus(string(provider), port.Family)
result = state{ready: initialized && bound, err: err}
} else {
client, err := firewallService.baseClient()
result.err = err
if err == nil {
result.ready, result.err = client.Status()
switch entry.Type {
case firewall.PortWhitelistTypePanel:
entry.Port = LoadPanelPort()
case firewall.PortWhitelistTypeSSH:
if sshPort == "" {
var err error
sshPort, err = loadSSHWhitelistPortFrom(sshPath)
if err != nil {
return nil, err
}
}
entry.Port = sshPort
}
}
return firewall.ValidatePortWhitelist(entries)
}
func updateSystemAccessPortWhitelist(ctx context.Context, serviceType string, ports []string) error {
return savePortWhitelist(ctx, func(entries []firewall.PortWhitelist) ([]firewall.PortWhitelist, error) {
for i := range entries {
if entries[i].Type == serviceType {
if len(ports) == 0 {
return nil, fmt.Errorf("firewall whitelist %s requires a port", serviceType)
}
entries[i].Port = ports[0]
}
}
states[key] = result
return result.ready, result.err
}
}
func syncPortWhitelist(
ctx context.Context,
ports, required []firewall.PortWhitelist,
ready func(dto.FirewallSystemPort) (bool, error),
ensure func(context.Context, dto.FirewallSystemPort) error,
report whitelistReporter,
) error {
var failures []error
for _, port := range systemPorts(ports) {
if err := ctx.Err(); err != nil {
return errors.Join(append(failures, err)...)
}
label := whitelistPortLabel(port)
if containsFirewallPort(required, firewall.PortWhitelist{Family: port.Family, Port: port.Port, Protocol: port.Protocol}) {
report("FirewallWhitelistRequired", label, nil)
continue
}
active, err := ready(port)
if err == nil && !active {
report("FirewallWhitelistDeferred", label, nil)
continue
}
if err == nil {
err = ensure(ctx, port)
}
if err != nil {
report("failed", label, err)
failures = append(failures, fmt.Errorf("%s: %w", label, err))
continue
}
report("applied", label, nil)
}
return errors.Join(failures...)
}
func whitelistPortLabel(port dto.FirewallSystemPort) string {
return fmt.Sprintf("%s %s/%s", port.Family, port.Port, port.Protocol)
return entries, nil
})
}
func (s *FirewallSettingService) Load(ctx context.Context) (dto.FirewallSettings, error) {
result := dto.FirewallSettings{PingStatus: ping.LoadStatus()}
if ports, err := settingRepo.GetValueByKey(constant.FirewallPortWhiteList); err == nil {
result.PortWhitelist = ports
} else {
result.PortWhitelist = constant.FirewallPortWhiteListValue
}
result := dto.FirewallSettings{PingStatus: firewall.LoadPingStatus()}
installed := make(map[string]bool)
for _, name := range lifecycle.InstalledProviders() {
@@ -341,8 +338,19 @@ func (s *FirewallSettingService) Load(ctx context.Context) (dto.FirewallSettings
}
result.Docker.Options = append(result.Docker.Options, option)
}
return result, nil
var err error
result.PortWhitelist, err = loadPortWhitelistSetting(global.DB.WithContext(ctx))
if err != nil {
return result, err
}
result.PanelPort = LoadPanelPort()
sshPort, sshErr := loadSSHWhitelistPortFrom(sshPath)
if sshErr != nil {
global.LOG.Warnf("load SSH port for firewall settings: %v", sshErr)
} else {
result.SSHPort = sshPort
}
return result, err
}
func loadSystemFirewallFamilyStatus(provider, family string) (bool, bool, error) {
@@ -372,6 +380,10 @@ func loadSystemFirewallFamilyInfo(provider, family string) dto.FirewallBackendFa
}
func (s *FirewallSettingService) Operate(ctx context.Context, request dto.FirewallBackendOperation) error {
if err := lockFirewallLifecycleIdle(); err != nil {
return err
}
defer firewallLifecycleTaskMu.Unlock()
if request.Subsystem != "system" && request.Backend != constant.FirewallProviderIptables && request.Backend != constant.FirewallProviderNftables {
return fmt.Errorf("%s only supports iptables or nftables", request.Subsystem)
}
@@ -385,10 +397,9 @@ func (s *FirewallSettingService) Operate(ctx context.Context, request dto.Firewa
}
if request.Operation == "initialize" {
service := newFirewallService()
if err := service.restoreStoredFirewallRules(ctx, filter.Provider(request.Backend)); err != nil {
return err
}
return service.syncConfiguredFirewallPorts(ctx)
rulesErr := service.restoreStoredFirewallRules(ctx, filter.Provider(request.Backend), nil)
whitelistErr := service.SyncPortWhitelist(ctx)
return errors.Join(rulesErr, whitelistErr)
}
return nil
case "forwarding":
+403 -93
View File
@@ -20,8 +20,8 @@ import (
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
filterruntime "github.com/1Panel-dev/1Panel/agent/utils/firewall/filter/runtime"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/forwarding"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/nftables_helper"
firewallsync "github.com/1Panel-dev/1Panel/agent/utils/firewall/sync"
"github.com/google/uuid"
"gorm.io/gorm"
)
@@ -82,6 +82,15 @@ func (s *FirewallService) loadFirewallSyncRules(ctx context.Context, request dto
return nil, nil, nil, err
}
model.SortFirewallRules(stored, request.TargetProvider)
ports, err := loadFirewallPortWhiteList()
if err != nil {
return nil, nil, nil, err
}
required, err := firewall.RequiredPortWhitelist(ports)
if err != nil {
return nil, nil, nil, err
}
whitelistDesired := whitelistRules(request.TargetProvider, firewall.ExpandPortWhitelist(customWhitelist(ports)), firewall.ExpandPortWhitelist(required))
rules := make([]*firewallSyncRule, 0, len(stored))
preservedMarkers := make(map[string]bool)
compileFailed := false
@@ -97,9 +106,38 @@ func (s *FirewallService) loadFirewallSyncRules(ctx context.Context, request dto
}
for _, native := range desired {
rule := native.Rule
native.Protected = filter.RuleMatchesPortWhitelist(rule, ports)
rules = append(rules, &firewallSyncRule{desired: native, FirewallRuleSyncItem: dto.FirewallRuleSyncItem{SourceUUID: record.UUID, Rule: &rule}})
}
}
for _, candidate := range whitelistDesired {
prepared, err := s.prepareCreate(ctx, request.TargetProvider, dto.FirewallRuleCreateItem{Rule: candidate})
if err != nil {
return nil, nil, nil, err
}
rule := prepared.request.Rule
duplicate := false
for _, existing := range rules {
if existing.Rule != nil {
if same, err := filter.SameRuleContent(*existing.Rule, rule); err == nil && same {
duplicate = true
break
}
}
}
if duplicate {
continue
}
key, err := filter.RuleKey(rule)
if err != nil {
return nil, nil, nil, err
}
rule.UUID = uuid.NewSHA1(uuid.NameSpaceOID, []byte(key)).String()
rules = append(rules, &firewallSyncRule{
desired: filter.DesiredRule{UUID: rule.UUID, Rule: rule, RuleKey: key, Origin: filter.RuleOriginCreated, Protected: true},
FirewallRuleSyncItem: dto.FirewallRuleSyncItem{SourceUUID: rule.UUID, Rule: &rule},
})
}
snapshots := make([]filter.Snapshot, 0)
for _, scope := range filter.ManagedInputScopes(request.TargetProvider) {
desired := make([]filter.DesiredRule, 0)
@@ -137,6 +175,9 @@ func (s *FirewallService) loadFirewallSyncRules(ctx context.Context, request dto
continue
}
observed := item.Observed
if observed.Rule.Scope.Chain == filter.BasicBeforeChain {
continue
}
rule := &firewallSyncRule{observed: observed, FirewallRuleSyncItem: dto.FirewallRuleSyncItem{
SourceUUID: strings.TrimPrefix(observed.Marker, "1panel-rule:"), Rule: &observed.Rule, Status: firewallsync.StatusRemove,
ReasonCode: firewallsync.ReasonManagedOnlyInTarget, Reason: firewallsync.ReasonMessage(firewallsync.ReasonManagedOnlyInTarget),
@@ -170,7 +211,7 @@ func (s *FirewallService) loadFirewallSyncRules(ctx context.Context, request dto
}
drifted := firewallsync.RuleOrder(snapshot, ordered)
for _, rule := range scoped {
if !drifted[rule.desired.Marker] {
if rule.desired.Protected || !drifted[rule.desired.Marker] {
continue
}
if rule.Status == firewallsync.StatusExisting {
@@ -182,6 +223,19 @@ func (s *FirewallService) loadFirewallSyncRules(ctx context.Context, request dto
}
func (s *FirewallService) syncRules(ctx context.Context, _ string, request dto.FirewallRuleSyncRequest, t *task.Task) (result dto.FirewallRuleSyncResult, err error) {
if request.SourceProvider != "" || request.ResetSource {
return result, fmt.Errorf("%w: system synchronization reads rules from the database", filter.ErrInvalidRule)
}
if err := s.checkSelectedProvider(ctx, request.TargetProvider); err != nil {
return result, err
}
whitelistErr := s.SyncPortWhitelist(ctx)
if t != nil {
t.LogWithStatus(i18n.GetMsgByKey("FirewallSyncWhitelistStep"), whitelistErr)
}
if whitelistErr != nil {
return result, whitelistErr
}
firewallRuleMutationMu.Lock()
defer firewallRuleMutationMu.Unlock()
result = dto.FirewallRuleSyncResult{Subsystem: "system", TargetProvider: request.TargetProvider}
@@ -192,9 +246,11 @@ func (s *FirewallService) syncRules(ctx context.Context, _ string, request dto.F
if err != nil {
return result, err
}
created, removed, unexecuted := 0, 0, 0
stopped := make(map[string]error)
failedRemovals := make(map[string]error)
var firewalldFinalSnapshot *filter.Snapshot
record := func(operation string, rule *firewallSyncRule, cause error, skipped bool) {
item := rule.FirewallRuleSyncItem
if operation == "TaskDelete" && rule.observed != nil {
@@ -234,13 +290,15 @@ func (s *FirewallService) syncRules(ctx context.Context, _ string, request dto.F
}
if skipped {
t.Logf("%s %s: %v", label, i18n.GetMsgByKey("FirewallCreateRuleSkipped"), cause)
} else if operation == task.TaskSync && cause == nil {
t.Logf("%s %s", label, i18n.GetMsgByKey("FirewallSyncRuleUnchanged"))
} else {
t.LogWithStatus(label, cause)
}
}
defer func() {
if t != nil {
t.Log(i18n.GetMsgWithMap("FirewallSyncOperationsResult", map[string]interface{}{"created": created, "removed": removed, "failed": result.Failed, "skipped": unexecuted}))
t.Log(i18n.GetMsgWithMap("FirewallSyncOperationsResult", map[string]interface{}{"created": created, "removed": removed, "failed": result.Failed, "skipped": unexecuted, "unchanged": result.Skipped - unexecuted}))
}
}()
blocked := false
@@ -287,6 +345,16 @@ func (s *FirewallService) syncRules(ctx context.Context, _ string, request dto.F
return syncObservedPosition(queue[i].observed) > syncObservedPosition(queue[j].observed)
})
}
if scope.Provider == filter.ProviderFirewalld && operation == filter.ChangeCreate && len(failedRemovals) == 0 && stopped[scope.Key()] == nil {
firewalldFinalSnapshot = syncFirewalldCreates(ctx, runtime, initial, removed > 0, queue, t, record)
continue
}
markers := make([]string, 0)
for _, candidate := range rules {
if candidate.Rule != nil && candidate.Rule.Scope.Key() == scope.Key() && candidate.desired.Marker != "" {
markers = append(markers, candidate.desired.Marker)
}
}
for start := 0; start < len(queue); {
rule := queue[start]
cause := stopped[scope.Key()]
@@ -325,21 +393,15 @@ func (s *FirewallService) syncRules(ctx context.Context, _ string, request dto.F
changes = append(changes, change)
} else {
after := *entry.Rule
markers := make([]string, 0)
for _, candidate := range rules {
if candidate.Rule != nil && candidate.Rule.Scope.Key() == scope.Key() && candidate.desired.Marker != "" {
markers = append(markers, candidate.desired.Marker)
}
}
after.OrderIndex = nil
if len(batch) == 1 {
if len(batch) == 1 && scope.Provider != filter.ProviderFirewalld {
after.OrderIndex = firewallsync.InsertionPosition(snapshot, markers, entry.desired.Marker)
}
changes = append(changes, filter.DesiredChange{Operation: operation, After: &after, Append: scope.Provider == filter.ProviderUFW && after.OrderIndex == nil})
}
}
if err == nil {
err = runtime.ExecuteSync(ctx, snapshot, changes)
_, err = runtime.ExecuteSync(ctx, snapshot, changes)
}
for _, entry := range batch {
record(name, entry, err, false)
@@ -351,9 +413,129 @@ func (s *FirewallService) syncRules(ctx context.Context, _ string, request dto.F
}
}
}
if firewalldFinalSnapshot != nil && result.Failed == 0 && created > 0 {
verifyErr := verifyFirewalldSyncSnapshot(*firewalldFinalSnapshot, rules)
if t != nil {
t.LogWithStatus(i18n.GetWithName("FirewallSyncStep", string(request.TargetProvider)), verifyErr)
}
if verifyErr != nil {
return result, verifyErr
}
}
return result, nil
}
func syncFirewalldCreates(
ctx context.Context,
runtime *filterruntime.Engine,
initial filter.Snapshot,
refresh bool,
queue []*firewallSyncRule,
t *task.Task,
record func(string, *firewallSyncRule, error, bool),
) *filter.Snapshot {
if len(queue) == 0 {
return nil
}
planner, readErr := runtime.NewCreatePlanner(initial)
if readErr != nil {
for _, entry := range queue {
record(task.TaskCreate, entry, readErr, false)
}
return nil
}
pending := make([]*firewallSyncRule, 0, len(queue))
for index, entry := range queue {
if refresh {
var snapshot filter.Snapshot
snapshot, readErr = runtime.ObserveMutation(ctx, initial.Scope)
if readErr == nil {
planner, readErr = runtime.NewCreatePlanner(snapshot)
}
if readErr != nil {
record(task.TaskCreate, entry, readErr, false)
for _, remaining := range queue[index+1:] {
record(task.TaskCreate, remaining, readErr, true)
}
break
}
refresh = false
}
if t != nil {
t.Logf("[%d/%d] %s %s", index+1, len(queue), i18n.GetMsgByKey(task.TaskCreate), entry.SourceUUID)
}
after := *entry.Rule
after.OrderIndex = nil
_, err := runtime.ExecutePlannedCreate(ctx, planner, filter.DesiredChange{
Operation: filter.ChangeCreate, After: &after, CommandOnly: true,
})
if err != nil {
record(task.TaskCreate, entry, err, false)
if firewallCreateUnavailable(err) {
for _, remaining := range queue[index+1:] {
record(task.TaskCreate, remaining, err, true)
}
break
}
refresh = true
continue
}
pending = append(pending, entry)
}
var actual filter.Snapshot
if readErr == nil {
actual, readErr = runtime.ObserveMutation(ctx, initial.Scope)
}
if readErr != nil {
for _, entry := range pending {
record(task.TaskCreate, entry, readErr, false)
}
return nil
}
states := firewalldRuleStates(actual)
for _, entry := range pending {
key, err := filter.RuleKey(*entry.Rule)
if err == nil && states[key] != 1 {
err = filter.ErrVerificationFailed
}
record(task.TaskCreate, entry, err, false)
}
return &actual
}
func firewalldRuleStates(snapshot filter.Snapshot) map[string]int {
states := make(map[string]int, len(snapshot.Rules))
for _, observed := range snapshot.Rules {
if observed.ParseStatus != filter.ParseStatusSupported || observed.Persistence != filter.PersistenceStatusConverged {
continue
}
if key, err := filter.RuleKey(observed.Rule); err == nil {
states[key]++
}
}
return states
}
func verifyFirewalldSyncSnapshot(snapshot filter.Snapshot, rules []*firewallSyncRule) error {
states := firewalldRuleStates(snapshot)
for _, entry := range rules {
if entry.Status == firewallsync.StatusRemove {
continue
}
if entry.Rule == nil {
return filter.ErrVerificationFailed
}
key, err := filter.RuleKey(*entry.Rule)
if err != nil {
return err
}
if states[key] != 1 {
return fmt.Errorf("%w: %s", filter.ErrVerificationFailed, entry.SourceUUID)
}
}
return nil
}
func syncObservedPosition(rule *filter.ObservedRule) int {
if rule.Locator.Position != nil {
return *rule.Locator.Position
@@ -361,8 +543,8 @@ func syncObservedPosition(rule *filter.ObservedRule) int {
return 0
}
func (s *FirewallService) restoreStoredFirewallRules(ctx context.Context, provider filter.Provider) error {
result, err := s.syncRules(ctx, "", dto.FirewallRuleSyncRequest{TargetProvider: provider}, nil)
func (s *FirewallService) restoreStoredFirewallRules(ctx context.Context, provider filter.Provider, t *task.Task) error {
result, err := s.syncRules(ctx, "", dto.FirewallRuleSyncRequest{TargetProvider: provider}, t)
if err != nil {
return fmt.Errorf("restore database firewall rules: %w", err)
}
@@ -393,6 +575,10 @@ func (s *FirewallService) syncSystemRules(
clientIP string,
request dto.FirewallRuleSyncRequest,
) (dto.FirewallRuleSyncResult, error) {
if err := lockFirewallLifecycleIdle(); err != nil {
return dto.FirewallRuleSyncResult{}, err
}
defer firewallLifecycleTaskMu.Unlock()
firewallRuleSyncTaskMu.Lock()
defer firewallRuleSyncTaskMu.Unlock()
@@ -401,7 +587,12 @@ func (s *FirewallService) syncSystemRules(
return dto.FirewallRuleSyncResult{}, err
}
if running.Executing {
return runningFirewallRuleSyncResult(request, running.TaskID), nil
return dto.FirewallRuleSyncResult{
Subsystem: firewallSyncSubsystem(request.Subsystem),
TargetProvider: request.TargetProvider,
TaskID: running.TaskID,
Queued: true,
}, nil
}
if firewallSyncSubsystem(request.Subsystem) != "system" {
return dto.FirewallRuleSyncResult{}, fmt.Errorf("%w: firewall synchronization tasks are only available for the system firewall", filter.ErrInvalidRule)
@@ -475,120 +666,239 @@ func currentFirewallRuleSyncTaskLocked() (dto.FirewallRuleSyncTask, error) {
return dto.FirewallRuleSyncTask{TaskID: record.ID, Executing: true}, nil
}
func runningFirewallRuleSyncResult(request dto.FirewallRuleSyncRequest, taskID string) dto.FirewallRuleSyncResult {
return dto.FirewallRuleSyncResult{
Subsystem: firewallSyncSubsystem(request.Subsystem),
TargetProvider: request.TargetProvider,
TaskID: taskID,
Queued: true,
func whitelistRules(provider filter.Provider, ports, required []firewall.SystemPort) []filter.FirewallRule {
rules := make([]filter.FirewallRule, 0, len(ports)+len(required))
for _, port := range required {
rule := systemPortRule(provider, port)
if isDirectFirewallProvider(provider) {
rule.Scope.Chain = filter.BasicBeforeChain
}
rules = append(rules, rule)
}
for _, port := range ports {
rules = append(rules, systemPortRule(provider, port))
}
return rules
}
func (s *FirewallService) syncConfiguredFirewallPorts(ctx context.Context) error {
configured, err := loadConfiguredFirewallPortWhiteList()
func (s *FirewallService) SyncPortWhitelist(ctx context.Context) error {
firewallWhitelistMu.Lock()
defer firewallWhitelistMu.Unlock()
filterruntime.InvalidateInventory()
defer filterruntime.InvalidateInventory()
ports, err := loadFirewallPortWhiteList()
if err != nil {
return err
}
required, err := LoadRequiredFirewallPortWhiteList()
required, err := firewall.RequiredPortWhitelist(ports)
if err != nil {
return err
}
ports := excludeFirewallPorts(configured, required)
return s.SyncSystemPorts(ctx, nil, systemPorts(ports))
}
func (s *FirewallService) SyncSystemPorts(ctx context.Context, previous, current []dto.FirewallSystemPort) error {
previousSet, err := normalizeSystemPorts(previous)
if err != nil {
return err
}
currentSet, err := normalizeSystemPorts(current)
if err != nil {
return err
}
provider, err := s.selectedProvider(ctx)
if err != nil {
return err
}
if !supportsNativeRuleBatch(provider) {
var syncErrors []error
for _, key := range sortedSystemPortKeys(currentSet) {
if _, exists := previousSet[key]; exists {
rules := whitelistRules(provider, firewall.ExpandPortWhitelist(customWhitelist(ports)), firewall.ExpandPortWhitelist(required))
activeFamilies := make(map[filter.Family]bool)
if isDirectFirewallProvider(provider) {
for _, rule := range rules {
family := rule.Scope.Family
if _, checked := activeFamilies[family]; checked {
continue
}
if err := s.ensureSystemPort(ctx, currentSet[key]); err != nil {
wrapped := fmt.Errorf("restore accepted firewall port %s: %w", key, err)
syncErrors = append(syncErrors, wrapped)
if global.LOG != nil {
global.LOG.Errorf("%v", wrapped)
}
initialized, bound, err := loadSystemFirewallFamilyStatus(string(provider), string(family))
if err != nil {
return err
}
activeFamilies[family] = initialized && bound
}
for _, key := range sortedSystemPortKeys(previousSet) {
if _, exists := currentSet[key]; exists {
continue
}
if err := s.deleteSystemPort(ctx, previousSet[key]); err != nil {
wrapped := fmt.Errorf("release accepted firewall port %s: %w", key, err)
syncErrors = append(syncErrors, wrapped)
if global.LOG != nil {
global.LOG.Errorf("%v", wrapped)
}
}
} else if len(rules) > 0 {
client, err := s.baseClient()
if err != nil {
return err
}
return errors.Join(syncErrors...)
}
for _, key := range sortedSystemPortKeys(currentSet) {
if _, exists := previousSet[key]; exists {
continue
}
if err := s.ensureSystemPort(ctx, currentSet[key]); err != nil {
active, err := client.Status()
if err != nil || !active {
return err
}
}
for _, key := range sortedSystemPortKeys(previousSet) {
if _, exists := currentSet[key]; exists {
prepared := make([]preparedFirewallRuleCreate, 0, len(rules))
var failures []error
for _, rule := range rules {
if err := ctx.Err(); err != nil {
return err
}
if isDirectFirewallProvider(provider) && !activeFamilies[rule.Scope.Family] {
continue
}
if err := s.deleteSystemPort(ctx, previousSet[key]); err != nil {
port := firewall.SystemPort{Family: string(rule.Scope.Family), Port: rule.DestinationPort, Protocol: rule.Protocol, SourceAddress: rule.SourceAddress}
item, err := s.prepareCreate(ctx, provider, dto.FirewallRuleCreateItem{
Rule: rule, SourceKind: constant.FirewallRuleSourceSecurity, SourceID: systemPortSourceID(port),
})
if err != nil {
failures = append(failures, fmt.Errorf("prepare whitelist rule %s: %w", firewall.SystemPortKey(port), err))
continue
}
prepared = append(prepared, item)
}
if len(failures) > 0 {
return errors.Join(failures...)
}
if provider == filter.ProviderUFW && len(prepared) > 0 {
return s.syncUFWPortWhitelist(ctx, prepared)
}
for _, item := range prepared {
if err := ctx.Err(); err != nil {
return err
}
if err := s.addWhitelistRule(ctx, item); err != nil {
rule := item.request.Rule
failures = append(failures, fmt.Errorf("add whitelist rule %s %s/%s [%s]: %w", rule.Scope.Family, rule.DestinationPort, rule.Protocol, rule.SourceAddress, err))
}
}
return errors.Join(failures...)
}
func (s *FirewallService) syncUFWPortWhitelist(ctx context.Context, prepared []preparedFirewallRuleCreate) error {
firewallRuleMutationMu.Lock()
defer firewallRuleMutationMu.Unlock()
runtime := prepared[0].runtime
snapshots, err := runtime.ObserveScopes(ctx, filter.ManagedInputScopes(filter.ProviderUFW))
if err != nil {
return err
}
for itemIndex, item := range prepared {
if err := ctx.Err(); err != nil {
return err
}
rule := item.request.Rule
var snapshot *filter.Snapshot
for index := range snapshots {
if snapshots[index].Scope.Key() == rule.Scope.Key() {
snapshot = &snapshots[index]
break
}
}
if snapshot == nil {
return fmt.Errorf("%w: missing UFW whitelist scope %s", filter.ErrInventoryUnavailable, rule.Scope.Key())
}
for _, notice := range snapshot.Notices {
if notice.Code == filter.ScopeNoticeManagedScopeInactive || notice.Code == filter.ScopeNoticeManagedScopeMissing {
return filter.ErrProviderUnavailable
}
}
added, err := s.addWhitelistRuleFromSnapshot(ctx, item, *snapshot)
if err != nil {
return fmt.Errorf("add whitelist rule %s %s/%s [%s]: %w", rule.Scope.Family, rule.DestinationPort, rule.Protocol, rule.SourceAddress, err)
}
if !added {
continue
}
if itemIndex+1 < len(prepared) {
if err := ctx.Err(); err != nil {
return err
}
snapshots, err = runtime.ObserveScopes(ctx, filter.ManagedInputScopes(filter.ProviderUFW))
if err != nil {
return err
}
}
}
return nil
}
func syncManagedAcceptedPorts(previous, current []firewall.PortWhitelist) error {
return newFirewallService().
SyncSystemPorts(context.Background(), systemPorts(previous), systemPorts(current))
func (s *FirewallService) addWhitelistRule(ctx context.Context, prepared preparedFirewallRuleCreate) error {
firewallRuleMutationMu.Lock()
defer firewallRuleMutationMu.Unlock()
if err := ctx.Err(); err != nil {
return err
}
rule, runtime := prepared.request.Rule, prepared.runtime
snapshot, err := runtime.ObserveMutation(ctx, rule.Scope)
if err != nil {
return err
}
_, err = s.addWhitelistRuleFromSnapshot(ctx, prepared, snapshot)
return err
}
func syncPanelRequiredPorts(provider string, ports []firewall.PortWhitelist) error {
loadPorts := func() ([]firewall.PortWhitelist, error) { return ports, nil }
if provider == constant.FirewallProviderIptables {
manager := newIptablesHelperManager()
manager.LoadRequiredPorts = loadPorts
return manager.SyncRequiredPorts(true)
}
initialized := false
for _, family := range []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6} {
familyInitialized, _, err := nftables_helper.LoadFamilyInitStatus(family, "base")
if family == filter.FamilyIPv6 && errors.Is(err, filter.ErrFamilyUnavailable) {
continue
func (s *FirewallService) addWhitelistRuleFromSnapshot(ctx context.Context, prepared preparedFirewallRuleCreate, snapshot filter.Snapshot) (bool, error) {
rule, runtime := prepared.request.Rule, prepared.runtime
for _, observed := range snapshot.Rules {
if observed.ParseStatus == filter.ParseStatusSupported {
if same, err := filter.SameRuleContent(rule, observed.Rule); err == nil && same {
return false, nil
}
}
}
if err := filter.CheckObservedRuleCollisions(snapshot, rule, nil); err != nil {
return false, err
}
if rule.Scope.Chain == filter.BasicBeforeChain {
position := int64(1)
rule.OrderIndex = &position
rule.UUID = uuid.NewString()
return true, runtime.ExecuteCreate(ctx, snapshot, []filter.DesiredChange{{Operation: filter.ChangeCreate, After: &rule}})
}
stored, err := s.rules.List(ctx)
if err != nil {
return false, err
}
model.SortFirewallRules(stored, rule.Scope.Provider)
markers := make([]string, 0, len(stored))
var existing *filter.DesiredRule
var firstSequence *int64
for _, record := range stored {
compiled, err := s.compileStoredFirewallRules(ctx, record, rule.Scope.Provider)
if err != nil {
return err
if isFirewallPolicyIncompatible(err) {
continue
}
return false, err
}
for _, candidate := range compiled {
if candidate.Rule.Scope.Key() == rule.Scope.Key() && candidate.Marker != "" {
markers = append(markers, candidate.Marker)
if firstSequence == nil && record.Sequence != nil {
firstSequence = record.Sequence
}
}
if err := filter.CheckRuleCollision(rule, candidate.Rule); errors.Is(err, filter.ErrRuleOperation) {
copy := candidate
existing = &copy
} else if err != nil {
return false, err
}
}
initialized = initialized || familyInitialized
}
if !initialized {
return nil
if existing != nil {
existing.Rule.OrderIndex = firewallsync.InsertionPosition(snapshot, markers, existing.Marker)
return true, runtime.ExecuteCreate(ctx, snapshot, []filter.DesiredChange{{
Operation: filter.ChangeCreate, After: &existing.Rule,
Append: rule.Scope.Provider == filter.ProviderUFW && existing.Rule.OrderIndex == nil,
}})
}
manager := newNftablesHelperManager()
manager.LoadRequiredPorts = loadPorts
return manager.SyncRequiredPorts()
if rule.Scope.Provider != filter.ProviderFirewalld {
position := int64(1)
rule.OrderIndex = &position
record, err := firewallRuleModelForCreate(rule, prepared.request, constant.FirewallRuleOriginCreated)
if err != nil {
return false, err
}
sequence := model.FirewallRuleSequenceStep
if firstSequence != nil {
sequence = *firstSequence - model.FirewallRuleSequenceStep
}
record.UUID, record.Sequence = uuid.NewString(), &sequence
rule.UUID = record.UUID
if err := runtime.ExecuteCreate(ctx, snapshot, []filter.DesiredChange{{Operation: filter.ChangeCreate, After: &rule}}); err != nil {
return false, firewallCreateExecutionError(err)
}
return true, s.saveFirewallRule(ctx, &record)
}
return true, s.applyCreateRules(ctx, runtime, snapshot, stored, []preparedFirewallRuleCreate{prepared})[0]
}
type forwardingRuleSyncCandidate struct {
@@ -600,7 +910,7 @@ func (s *ForwardingService) loadRuleSyncCandidates(
ctx context.Context,
targetProvider filter.Provider,
) (*forwarding.Manager, []forwardingRuleSyncCandidate, []forwarding.Rule, bool, error) {
target, err := s.manager()
target, err := s.managerFactory()
if err != nil {
return nil, nil, nil, false, err
}
+9 -14
View File
@@ -16,10 +16,9 @@ import (
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/i18n"
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/forwarding"
forwardingproviders "github.com/1Panel-dev/1Panel/agent/utils/firewall/forwarding/providers"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/ping"
)
type IForwardingService interface {
@@ -76,7 +75,7 @@ func (s *ForwardingService) LoadBaseInfo() (dto.FirewallSubsystemStatus, error)
baseInfo := dto.FirewallSubsystemStatus{
Version: "-", Name: forwardingDisplayName(selected), Backend: selected, SyncError: lastForwardingSyncError(),
}
manager, err := s.manager()
manager, err := s.managerFactory()
if err != nil {
if errors.Is(err, errForwardingBackendUnavailable) {
baseInfo.Reason = constant.FirewallBackendNotInstalled
@@ -91,7 +90,7 @@ func (s *ForwardingService) LoadBaseInfo() (dto.FirewallSubsystemStatus, error)
baseInfo.IsExist = true
baseInfo.Name, baseInfo.Backend = forwardingDisplayName(status.Name), status.Name
baseInfo.Version = status.Version
baseInfo.PingStatus = ping.LoadStatus()
baseInfo.PingStatus = firewall.LoadPingStatus()
baseInfo.IsInit, baseInfo.IsBind = status.IsInit, status.IsBind
baseInfo.IPv4 = loadForwardingFamilyInfo(manager, status.Name, constant.FirewallFamilyIPv4)
baseInfo.IPv6 = loadForwardingFamilyInfo(manager, status.Name, constant.FirewallFamilyIPv6)
@@ -125,7 +124,7 @@ func (s *ForwardingService) SearchRules(request dto.ForwardRuleSearch) (int64, [
if err != nil {
return 0, nil, err
}
manager, err := s.manager()
manager, err := s.managerFactory()
if err != nil {
return 0, nil, err
}
@@ -245,7 +244,7 @@ func (s *ForwardingService) operateRules(ctx context.Context, request dto.Forwar
func (s *ForwardingService) Enable() error {
forwardingMutationMu.Lock()
defer forwardingMutationMu.Unlock()
manager, err := s.manager()
manager, err := s.managerFactory()
if err != nil {
recordForwardingSyncError(err)
return err
@@ -284,7 +283,7 @@ func (s *ForwardingService) QueueInitialization(
forwardingMutationMu.Lock()
defer forwardingMutationMu.Unlock()
var err error
manager, err = s.manager()
manager, err = s.managerFactory()
if err != nil {
recordForwardingSyncError(err)
return err
@@ -330,7 +329,7 @@ func (s *ForwardingService) Restore(ctx context.Context) error {
}
return err
}
manager, err := s.manager()
manager, err := s.managerFactory()
if err != nil {
recordForwardingSyncError(err)
return err
@@ -350,7 +349,7 @@ func (s *ForwardingService) Restore(ctx context.Context) error {
}
func (s *ForwardingService) reconcile(rules []forwarding.Rule) error {
manager, err := s.manager()
manager, err := s.managerFactory()
if err != nil {
return err
}
@@ -549,10 +548,6 @@ func forwardingOperationsOnlyRemove(operations []dto.ForwardRuleOperation) bool
return true
}
func (s *ForwardingService) manager() (*forwarding.Manager, error) {
return s.managerFactory()
}
func newForwardingManager() (*forwarding.Manager, error) {
return newForwardingManagerFor(configuredForwardingBackend())
}
@@ -574,7 +569,7 @@ func newForwardingManagerFor(backend string) (*forwarding.Manager, error) {
errForwardingBackendUnavailable, backend, err,
)
}
adapter, err := forwardingproviders.New(client.Name())
adapter, err := forwarding.New(client.Name())
if err != nil {
return nil, err
}
+31 -24
View File
@@ -4,6 +4,7 @@ import (
"bufio"
"bytes"
"compress/gzip"
"context"
"encoding/base64"
"encoding/json"
"fmt"
@@ -22,6 +23,7 @@ import (
"github.com/1Panel-dev/1Panel/agent/utils/copier"
csvexport "github.com/1Panel-dev/1Panel/agent/utils/csv_export"
"github.com/1Panel-dev/1Panel/agent/utils/encrypt"
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
"github.com/1Panel-dev/1Panel/agent/utils/geo"
"github.com/gin-gonic/gin"
@@ -217,10 +219,21 @@ func (u *SSHService) Update(req dto.SSHUpdate) error {
return err
}
oldPortValue := strings.Join(loadSSHPortValues(directives), ",")
if req.Key == "Port" {
if err := checkSSHPortAvailability(splitSSHPorts(oldPortValue), splitSSHPorts(req.NewValue)); err != nil {
return err
}
}
if err := updateSSHDirectiveValue(req.Key, req.NewValue, directives); err != nil {
return err
}
if req.Key == "Port" {
if err := updateSystemAccessPortWhitelist(context.Background(), firewall.PortWhitelistTypeSSH, splitSSHPorts(req.NewValue)); err != nil {
if restoreErr := rewriteSSHManagedDirectives(sshPath, "Port", buildSSHDirectiveLines("Port", oldPortValue)); restoreErr != nil {
return fmt.Errorf("save SSH whitelist: %w; restore SSH configuration: %v", err, restoreErr)
}
return err
}
handleSSHPortUpdate(oldPortValue, req.NewValue)
}
@@ -320,18 +333,6 @@ func handleSSHPortUpdate(oldValue, newValue string) {
}
}
removedPorts, err := parseSSHPortsToInts(diffSSHPorts(oldPorts, newPorts))
if err != nil {
global.LOG.Errorf("parse removed ssh ports failed, err: %v", err)
} else {
addedPorts, err := parseSSHPortsToInts(diffSSHPorts(newPorts, oldPorts))
if err != nil {
global.LOG.Errorf("parse added ssh ports failed, err: %v", err)
} else if err := OperateFirewallPort(removedPorts, addedPorts); err != nil {
global.LOG.Errorf("reset firewall rules %s -> %s failed, err: %v", oldValue, newValue, err)
}
}
primaryPort, err := loadPrimarySSHPort(newValue)
if err != nil {
global.LOG.Errorf("load primary ssh port from %s failed, err: %v", newValue, err)
@@ -371,6 +372,24 @@ func diffSSHPorts(left, right []string) []string {
return diff
}
func checkSSHPortAvailability(oldPorts, newPorts []string) error {
for _, port := range diffSSHPorts(newPorts, oldPorts) {
value, err := strconv.Atoi(port)
if err != nil || value < 1 || value > 65535 {
return fmt.Errorf("invalid SSH port %q", port)
}
if common.ScanPort(value) {
return buserr.WithDetail("ErrPortInUsed", value, nil)
}
listener, err := net.Listen("tcp4", ":"+strconv.Itoa(value))
if err != nil {
return buserr.WithDetail("ErrPortInUsed", value, nil)
}
_ = listener.Close()
}
return nil
}
func loadPrimarySSHPort(value string) (int, error) {
ports := splitSSHPorts(value)
if len(ports) == 0 {
@@ -379,18 +398,6 @@ func loadPrimarySSHPort(value string) (int, error) {
return strconv.Atoi(ports[0])
}
func parseSSHPortsToInts(ports []string) ([]int, error) {
var values []int
for _, port := range ports {
value, err := strconv.Atoi(port)
if err != nil {
return nil, err
}
values = append(values, value)
}
return values, nil
}
func runWithOptionalSudo(sudo, name string, args ...string) (string, error) {
cmdMgr := cmd.NewCommandMgr(cmd.WithTimeout(20 * time.Second))
if sudo != "" {
+1 -1
View File
@@ -32,7 +32,7 @@ func (w WebsiteService) CreateWebsiteDomain(create request.WebsiteDomainCreate)
return nil, err
}
go func() {
_ = OperateFirewallPort(nil, addPorts)
_ = ensureFirewallPorts(addPorts)
}()
nginxInstall, err := getAppInstallByKey(constant.AppOpenresty)
+33 -5
View File
@@ -2470,10 +2470,19 @@
},
"/core/xpack/vms/del": {
"bodyKeys": [
"name"
"id"
],
"paramKeys": [],
"beforeFunctions": [],
"beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "virtual_machines",
"output_column": "name",
"output_value": "name"
}
],
"formatZH": "删除虚拟机 [name]",
"formatEN": "delete VM [name]"
},
@@ -3770,12 +3779,31 @@
},
"/hosts/firewall/settings/whitelist": {
"bodyKeys": [
"value"
"rule"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "更新防火墙端口白名单 [value]",
"formatEN": "update firewall port whitelist [value]"
"formatZH": "创建防火墙端口白名单",
"formatEN": "create firewall port whitelist"
},
"/hosts/firewall/settings/whitelist/delete": {
"bodyKeys": [
"rules"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "删除防火墙端口白名单",
"formatEN": "delete firewall port whitelist"
},
"/hosts/firewall/settings/whitelist/update": {
"bodyKeys": [
"oldRule",
"rule"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "编辑防火墙端口白名单",
"formatEN": "update firewall port whitelist"
},
"/hosts/monitor/clean": {
"bodyKeys": [],
+1 -1
View File
@@ -27,7 +27,7 @@ const (
FirewallPingStatusKey = "BanPing"
FirewallPortWhiteList = "FirewallPortWhiteList"
FirewallPortWhiteListValue = "80/tcp,443/tcp,443/udp"
FirewallPortWhiteListValue = `[{"port":"80","protocol":"tcp","sources":["0.0.0.0/0","::/0"]},{"port":"443","protocol":"tcp","sources":["0.0.0.0/0","::/0"]},{"port":"443","protocol":"udp","sources":["0.0.0.0/0","::/0"]}]`
)
const (
+2 -4
View File
@@ -702,7 +702,8 @@ FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{
FirewallCreateRuleExecutionFailed: 'Rule creation failed; no database record was saved and executed commands were not rolled back'
FirewallCreateRulePersistenceFailed: 'The rule was created, but its management record could not be saved'
FirewallAdoptRulePersistenceFailed: 'The rule was adopted, but its management record could not be saved'
FirewallSyncOperationsResult: 'Synchronization operations: {{ .removed }} deleted, {{ .created }} created, {{ .failed }} failed, {{ .skipped }} not executed'
FirewallSyncOperationsResult: 'Synchronization operations: {{ .removed }} deleted, {{ .created }} created, {{ .failed }} failed, {{ .skipped }} not executed, {{ .unchanged }} already matching (no changes needed)'
FirewallSyncRuleUnchanged: 'Already matching; no changes needed'
FirewallSyncStep: 'Synchronize rules to {{ .name }}'
FirewallSyncFailed: '{{ .failed }} firewall rules failed to synchronize'
FirewallResetSourceStep: 'Reset and disable source firewall {{ .name }}'
@@ -716,8 +717,5 @@ FirewallInspectDockerGuardStep: 'Inspect Docker firewall backend and policies'
FirewallInitializeDockerGuardStep: 'Initialize and bind {{ .name }} port guard chains'
FirewallPersistDockerGuardStep: 'Persist Docker port guard status'
ErrFirewallRuleScopeChange: "The current firewall does not support changing a rule's scope (such as its IPv4/IPv6 address family). Please create a new rule."
FirewallWhitelistTask: "Update firewall port whitelist"
FirewallWhitelistSaved: "Whitelist configuration saved"
FirewallWhitelistDeferred: "{{ .name }}: configuration saved; pending firewall activation"
FirewallWhitelistReleased: "{{ .name }}: whitelist protection released; allow rule retained. To close the port, delete the rule manually from the rule list"
FirewallWhitelistRequired: "{{ .name }}: protected by mandatory system port rules"
+2 -4
View File
@@ -702,7 +702,8 @@ FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{
FirewallCreateRuleExecutionFailed: 'Error al crear la regla. No se guardó ningún registro en la base de datos ni se revirtieron los comandos ejecutados'
FirewallCreateRulePersistenceFailed: 'La regla se creó, pero no se pudo guardar su registro de gestión'
FirewallAdoptRulePersistenceFailed: 'Se ejecutó la adopción de la regla, pero no se pudo guardar su registro de gestión'
FirewallSyncOperationsResult: 'Operaciones de sincronización: {{ .removed }} eliminadas, {{ .created }} creadas, {{ .failed }} fallidas, {{ .skipped }} sin ejecutar'
FirewallSyncOperationsResult: 'Operaciones de sincronización: {{ .removed }} eliminadas, {{ .created }} creadas, {{ .failed }} fallidas, {{ .skipped }} sin ejecutar, {{ .unchanged }} ya coinciden (sin cambios necesarios)'
FirewallSyncRuleUnchanged: 'Ya coincide; no requiere cambios'
FirewallSyncStep: 'Sincronizar reglas con {{ .name }}'
FirewallSyncFailed: 'No se pudieron sincronizar {{ .failed }} reglas del firewall'
FirewallResetSourceStep: 'Restablecer y desactivar el firewall de origen {{ .name }}'
@@ -716,8 +717,5 @@ FirewallInspectDockerGuardStep: 'Inspeccionar el backend del firewall de Docker
FirewallInitializeDockerGuardStep: 'Inicializar y vincular las cadenas de protección de puertos de {{ .name }}'
FirewallPersistDockerGuardStep: 'Guardar el estado de protección de puertos de Docker'
ErrFirewallRuleScopeChange: "El cortafuegos actual no permite cambiar el ámbito de una regla (como su familia de direcciones IPv4/IPv6). Cree una regla nueva."
FirewallWhitelistTask: "Actualizar la lista de puertos permitidos del cortafuegos"
FirewallWhitelistSaved: "Configuración de la lista de permitidos guardada"
FirewallWhitelistDeferred: "{{ .name }}: configuración guardada; pendiente de activar el cortafuegos"
FirewallWhitelistReleased: "{{ .name }}: protección de la lista de permitidos retirada; se conserva la regla de permiso. Para cerrar el puerto, elimine la regla manualmente de la lista"
FirewallWhitelistRequired: "{{ .name }}: protegido por las reglas de puertos obligatorios del sistema"
+2 -4
View File
@@ -702,7 +702,8 @@ FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{
FirewallCreateRuleExecutionFailed: 'ایجاد قانون ناموفق بود. هیچ رکوردی در پایگاه داده ذخیره نشد و دستورات اجراشده بازگردانی نشدند'
FirewallCreateRulePersistenceFailed: 'قانون ایجاد شد، اما ذخیره رکورد مدیریتی آن ناموفق بود'
FirewallAdoptRulePersistenceFailed: 'دستور پذیرش قانون برای مدیریت اجرا شد، اما اطلاعات مدیریت ذخیره نشد'
FirewallSyncOperationsResult: 'عملیات همگام‌سازی: {{ .removed }} حذف‌شده، {{ .created }} ایجادشده، {{ .failed }} ناموفق، {{ .skipped }} اجرا‌نشده'
FirewallSyncOperationsResult: 'عملیات همگام‌سازی: {{ .removed }} حذف‌شده، {{ .created }} ایجادشده، {{ .failed }} ناموفق، {{ .skipped }} اجرا‌نشده، {{ .unchanged }} از قبل مطابق (بدون نیاز به تغییر)'
FirewallSyncRuleUnchanged: 'از قبل مطابق است؛ نیازی به تغییر نیست'
FirewallSyncStep: 'همگام‌سازی قوانین با {{ .name }}'
FirewallSyncFailed: 'همگام‌سازی {{ .failed }} قانون فایروال ناموفق بود'
FirewallResetSourceStep: 'بازنشانی و غیرفعال‌کردن فایروال مبدأ {{ .name }}'
@@ -716,8 +717,5 @@ FirewallInspectDockerGuardStep: 'بررسی پشتیبان فایروال Docker
FirewallInitializeDockerGuardStep: 'راه‌اندازی و اتصال زنجیره‌های محافظت پورت {{ .name }}'
FirewallPersistDockerGuardStep: 'ذخیره وضعیت محافظت پورت Docker'
ErrFirewallRuleScopeChange: "فایروال فعلی از تغییر محدودهٔ قانون (مانند خانوادهٔ آدرس IPv4/IPv6) پشتیبانی نمی‌کند. لطفاً یک قانون جدید ایجاد کنید."
FirewallWhitelistTask: "به‌روزرسانی فهرست پورت‌های مجاز فایروال"
FirewallWhitelistSaved: "پیکربندی فهرست مجاز ذخیره شد"
FirewallWhitelistDeferred: "{{ .name }}: پیکربندی ذخیره شد؛ در انتظار فعال‌سازی فایروال"
FirewallWhitelistReleased: "{{ .name }}: حفاظت فهرست مجاز برداشته شد؛ قانون اجازه حفظ می‌شود. برای بستن پورت، قانون را به‌صورت دستی از فهرست قوانین حذف کنید"
FirewallWhitelistRequired: "{{ .name }}: توسط قوانین پورت‌های ضروری سیستم محافظت می‌شود"
+2 -4
View File
@@ -702,7 +702,8 @@ FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{
FirewallCreateRuleExecutionFailed: 'ルールの作成に失敗しました。データベースには保存せず、実行済みのコマンドはロールバックしません'
FirewallCreateRulePersistenceFailed: 'ルールは作成されましたが、管理情報の保存に失敗しました'
FirewallAdoptRulePersistenceFailed: 'ルールの管理対象への取り込みコマンドは実行されましたが、管理情報の保存に失敗しました'
FirewallSyncOperationsResult: '同期操作:削除成功 {{ .removed }} 件、作成成功 {{ .created }} 件、失敗 {{ .failed }} 件、未実行 {{ .skipped }} 件'
FirewallSyncOperationsResult: '同期操作:削除成功 {{ .removed }} 件、作成成功 {{ .created }} 件、失敗 {{ .failed }} 件、未実行 {{ .skipped }} 件、一致済みで変更不要 {{ .unchanged }} 件'
FirewallSyncRuleUnchanged: '一致済み、変更不要'
FirewallSyncStep: '{{ .name }} にルールを同期'
FirewallSyncFailed: '{{ .failed }} 件のファイアウォールルールを同期できませんでした'
FirewallResetSourceStep: '移行元ファイアウォール {{ .name }} をリセットして無効化'
@@ -716,8 +717,5 @@ FirewallInspectDockerGuardStep: 'Docker ファイアウォールバックエン
FirewallInitializeDockerGuardStep: '{{ .name }} のポート保護チェーンを初期化してバインド'
FirewallPersistDockerGuardStep: 'Docker ポート保護状態を保存'
ErrFirewallRuleScopeChange: "現在のファイアウォールでは、ルールの適用範囲(IPv4/IPv6 アドレスファミリーなど)を変更できません。新しいルールを作成してください。"
FirewallWhitelistTask: "ファイアウォールのポート許可リストを更新"
FirewallWhitelistSaved: "許可リストの設定を保存しました"
FirewallWhitelistDeferred: "{{ .name }}:設定を保存しました。ファイアウォールの有効化後に適用します"
FirewallWhitelistReleased: "{{ .name }}:許可リストの保護を解除しました。許可ルールは保持されます。ポートを閉じるには、ルール一覧から手動で削除してください"
FirewallWhitelistRequired: "{{ .name }}:システム必須ポートのルールで保護されています"
+2 -4
View File
@@ -702,7 +702,8 @@ FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{
FirewallCreateRuleExecutionFailed: '규칙 생성에 실패했습니다. 데이터베이스에 저장하지 않았으며 실행된 명령은 롤백하지 않습니다'
FirewallCreateRulePersistenceFailed: '규칙은 생성되었지만 관리 정보를 저장하지 못했습니다'
FirewallAdoptRulePersistenceFailed: '규칙 관리 등록 명령은 실행되었지만 관리 정보를 저장하지 못했습니다'
FirewallSyncOperationsResult: '동기화 작업: 삭제 성공 {{ .removed }}개, 생성 성공 {{ .created }}개, 실패 {{ .failed }}개, 미실행 {{ .skipped }}개'
FirewallSyncOperationsResult: '동기화 작업: 삭제 성공 {{ .removed }}개, 생성 성공 {{ .created }}개, 실패 {{ .failed }}개, 미실행 {{ .skipped }}개, 이미 일치하여 변경 불필요 {{ .unchanged }}개'
FirewallSyncRuleUnchanged: '이미 일치하여 변경이 필요하지 않음'
FirewallSyncStep: '{{ .name }}에 규칙 동기화'
FirewallSyncFailed: '방화벽 규칙 {{ .failed }}개를 동기화하지 못했습니다'
FirewallResetSourceStep: '원본 방화벽 {{ .name }} 초기화 및 비활성화'
@@ -716,8 +717,5 @@ FirewallInspectDockerGuardStep: 'Docker 방화벽 백엔드 및 보호 정책
FirewallInitializeDockerGuardStep: '{{ .name }} 포트 보호 체인 초기화 및 바인딩'
FirewallPersistDockerGuardStep: 'Docker 포트 보호 상태 저장'
ErrFirewallRuleScopeChange: "현재 방화벽에서는 규칙의 적용 범위(예: IPv4/IPv6 주소 패밀리)를 변경할 수 없습니다. 새 규칙을 생성하세요."
FirewallWhitelistTask: "방화벽 포트 허용 목록 업데이트"
FirewallWhitelistSaved: "허용 목록 설정이 저장되었습니다"
FirewallWhitelistDeferred: "{{ .name }}: 설정이 저장되었으며 방화벽 활성화 후 적용됩니다"
FirewallWhitelistReleased: "{{ .name }}: 허용 목록 보호가 해제되었으며 허용 규칙은 유지됩니다. 포트를 닫으려면 규칙 목록에서 수동으로 삭제하세요"
FirewallWhitelistRequired: "{{ .name }}: 시스템 필수 포트 규칙으로 보호됩니다"
+2 -4
View File
@@ -693,7 +693,8 @@ FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{
FirewallCreateRuleExecutionFailed: 'ການສ້າງກົດລົ້ມເຫຼວ. ບໍ່ໄດ້ບັນທຶກໃນຖານຂໍ້ມູນ ແລະ ບໍ່ໄດ້ຍ້ອນກັບຄຳສັ່ງທີ່ດຳເນີນການແລ້ວ'
FirewallCreateRulePersistenceFailed: 'ສ້າງກົດແລ້ວ ແຕ່ບັນທຶກຂໍ້ມູນການຈັດການບໍ່ສຳເລັດ'
FirewallAdoptRulePersistenceFailed: 'ຄຳສັ່ງນຳກົດເຂົ້າການຈັດການໄດ້ດຳເນີນການແລ້ວ ແຕ່ບໍ່ສາມາດບັນທຶກຂໍ້ມູນການຈັດການໄດ້'
FirewallSyncOperationsResult: 'ການດຳເນີນການຊິງຄ໌: ລຶບ {{ .removed }}, ສ້າງ {{ .created }}, ລົ້ມເຫຼວ {{ .failed }}, ບໍ່ໄດ້ດຳເນີນການ {{ .skipped }}'
FirewallSyncOperationsResult: 'ການດຳເນີນການຊິງຄ໌: ລຶບ {{ .removed }}, ສ້າງ {{ .created }}, ລົ້ມເຫຼວ {{ .failed }}, ບໍ່ໄດ້ດຳເນີນການ {{ .skipped }}, ກົງກັນແລ້ວ {{ .unchanged }} (ບໍ່ຕ້ອງປ່ຽນແປງ)'
FirewallSyncRuleUnchanged: 'ກົງກັນແລ້ວ; ບໍ່ຕ້ອງປ່ຽນແປງ'
FirewallSyncStep: 'ຊິງຄ໌ກົດໄປຫາ {{ .name }}'
FirewallSyncFailed: 'ຊິງຄ໌ກົດໄຟວອລ {{ .failed }} ລາຍການບໍ່ສຳເລັດ'
FirewallResetSourceStep: 'ຣີເຊັດ ແລະ ປິດໃຊ້ໄຟວອລຕົ້ນທາງ {{ .name }}'
@@ -707,8 +708,5 @@ FirewallInspectDockerGuardStep: 'ກວດສອບ backend firewall Docker ແ
FirewallInitializeDockerGuardStep: 'ເລີ່ມຕົ້ນ ແລະ ຜູກ chain ປ້ອງກັນພອດ {{ .name }}'
FirewallPersistDockerGuardStep: 'ບັນທຶກສະຖານະປ້ອງກັນພອດ Docker'
ErrFirewallRuleScopeChange: "ໄຟວໍປັດຈຸບັນບໍ່ຮອງຮັບການປ່ຽນຂອບເຂດຂອງກົດ (ເຊັ່ນ ຕະກູນທີ່ຢູ່ IPv4/IPv6). ກະລຸນາສ້າງກົດໃໝ່."
FirewallWhitelistTask: "ອັບເດດລາຍຊື່ພອດທີ່ອະນຸຍາດຂອງໄຟວໍ"
FirewallWhitelistSaved: "ບັນທຶກການຕັ້ງຄ່າລາຍຊື່ທີ່ອະນຸຍາດແລ້ວ"
FirewallWhitelistDeferred: "{{ .name }}: ບັນທຶກການຕັ້ງຄ່າແລ້ວ; ລໍຖ້າເປີດໃຊ້ໄຟວໍ"
FirewallWhitelistReleased: "{{ .name }}: ຍົກເລີກການປ້ອງກັນລາຍຊື່ທີ່ອະນຸຍາດແລ້ວ; ຍັງຄົງກົດອະນຸຍາດໄວ້. ຫາກຕ້ອງການປິດພອດ ໃຫ້ລຶບກົດດ້ວຍຕົນເອງຈາກລາຍການກົດ"
FirewallWhitelistRequired: "{{ .name }}: ປ້ອງກັນໂດຍກົດພອດທີ່ຈຳເປັນຂອງລະບົບ"
+2 -4
View File
@@ -702,7 +702,8 @@ FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{
FirewallCreateRuleExecutionFailed: 'Penciptaan peraturan gagal. Tiada rekod disimpan dalam pangkalan data dan arahan yang dilaksanakan tidak dibatalkan'
FirewallCreateRulePersistenceFailed: 'Peraturan telah dicipta, tetapi rekod pengurusannya tidak dapat disimpan'
FirewallAdoptRulePersistenceFailed: 'Arahan pengambilalihan peraturan telah dilaksanakan, tetapi rekod pengurusannya tidak dapat disimpan'
FirewallSyncOperationsResult: 'Operasi penyegerakan: {{ .removed }} dipadam, {{ .created }} dicipta, {{ .failed }} gagal, {{ .skipped }} tidak dilaksanakan'
FirewallSyncOperationsResult: 'Operasi penyegerakan: {{ .removed }} dipadam, {{ .created }} dicipta, {{ .failed }} gagal, {{ .skipped }} tidak dilaksanakan, {{ .unchanged }} sudah sepadan (tiada perubahan diperlukan)'
FirewallSyncRuleUnchanged: 'Sudah sepadan; tiada perubahan diperlukan'
FirewallSyncStep: 'Segerakkan peraturan ke {{ .name }}'
FirewallSyncFailed: '{{ .failed }} peraturan firewall gagal disegerakkan'
FirewallResetSourceStep: 'Tetapkan semula dan nyahdayakan firewall sumber {{ .name }}'
@@ -716,8 +717,5 @@ FirewallInspectDockerGuardStep: 'Periksa backend firewall Docker dan polisi'
FirewallInitializeDockerGuardStep: 'Mulakan dan ikat rantai perlindungan port {{ .name }}'
FirewallPersistDockerGuardStep: 'Simpan status perlindungan port Docker'
ErrFirewallRuleScopeChange: "Tembok api semasa tidak menyokong perubahan skop peraturan (seperti keluarga alamat IPv4/IPv6). Sila cipta peraturan baharu."
FirewallWhitelistTask: "Kemas kini senarai port dibenarkan tembok api"
FirewallWhitelistSaved: "Konfigurasi senarai dibenarkan telah disimpan"
FirewallWhitelistDeferred: "{{ .name }}: konfigurasi disimpan; menunggu pengaktifan tembok api"
FirewallWhitelistReleased: "{{ .name }}: perlindungan senarai dibenarkan telah dilepaskan; peraturan izin dikekalkan. Untuk menutup port, padamkan peraturan secara manual daripada senarai peraturan"
FirewallWhitelistRequired: "{{ .name }}: dilindungi oleh peraturan port wajib sistem"
+2 -4
View File
@@ -702,7 +702,8 @@ FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{
FirewallCreateRuleExecutionFailed: 'Falha ao criar a regra. Nenhum registro foi salvo no banco de dados e os comandos executados não foram revertidos'
FirewallCreateRulePersistenceFailed: 'A regra foi criada, mas não foi possível salvar seu registro de gerenciamento'
FirewallAdoptRulePersistenceFailed: 'A regra foi adotada, mas não foi possível salvar seu registro de gerenciamento'
FirewallSyncOperationsResult: 'Operações de sincronização: {{ .removed }} excluídas, {{ .created }} criadas, {{ .failed }} falhas, {{ .skipped }} não executadas'
FirewallSyncOperationsResult: 'Operações de sincronização: {{ .removed }} excluídas, {{ .created }} criadas, {{ .failed }} falhas, {{ .skipped }} não executadas, {{ .unchanged }} já correspondem (sem alterações necessárias)'
FirewallSyncRuleUnchanged: 'Já corresponde; nenhuma alteração necessária'
FirewallSyncStep: 'Sincronizar regras com {{ .name }}'
FirewallSyncFailed: '{{ .failed }} regras de firewall falharam na sincronização'
FirewallResetSourceStep: 'Redefinir e desativar o firewall de origem {{ .name }}'
@@ -716,8 +717,5 @@ FirewallInspectDockerGuardStep: 'Inspecionar o backend do firewall Docker e as p
FirewallInitializeDockerGuardStep: 'Inicializar e vincular as cadeias de proteção de portas do {{ .name }}'
FirewallPersistDockerGuardStep: 'Salvar o status da proteção de portas do Docker'
ErrFirewallRuleScopeChange: "O firewall atual não permite alterar o escopo de uma regra (como a família de endereços IPv4/IPv6). Crie uma nova regra."
FirewallWhitelistTask: "Atualizar a lista de portas permitidas do firewall"
FirewallWhitelistSaved: "Configuração da lista de permissões salva"
FirewallWhitelistDeferred: "{{ .name }}: configuração salva; aguardando a ativação do firewall"
FirewallWhitelistReleased: "{{ .name }}: proteção da lista de permissões removida; regra de permissão mantida. Para fechar a porta, exclua a regra manualmente da lista"
FirewallWhitelistRequired: "{{ .name }}: protegido pelas regras de portas obrigatórias do sistema"
+2 -4
View File
@@ -702,7 +702,8 @@ FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{
FirewallCreateRuleExecutionFailed: 'Не удалось создать правило. Запись в базе данных не сохранена, выполненные команды не отменены'
FirewallCreateRulePersistenceFailed: 'Правило создано, но не удалось сохранить запись управления'
FirewallAdoptRulePersistenceFailed: 'Команды принятия правила под управление выполнены, но сохранить запись управления не удалось'
FirewallSyncOperationsResult: 'Операции синхронизации: удалено {{ .removed }}, создано {{ .created }}, ошибок {{ .failed }}, не выполнено {{ .skipped }}'
FirewallSyncOperationsResult: 'Операции синхронизации: удалено {{ .removed }}, создано {{ .created }}, ошибок {{ .failed }}, не выполнено {{ .skipped }}, уже совпадают (изменения не нужны): {{ .unchanged }}'
FirewallSyncRuleUnchanged: 'Уже совпадает; изменения не нужны'
FirewallSyncStep: 'Синхронизировать правила с {{ .name }}'
FirewallSyncFailed: 'Не удалось синхронизировать правил межсетевого экрана: {{ .failed }}'
FirewallResetSourceStep: 'Сбросить и отключить исходный межсетевой экран {{ .name }}'
@@ -716,8 +717,5 @@ FirewallInspectDockerGuardStep: 'Проверить бэкенд межсете
FirewallInitializeDockerGuardStep: 'Инициализировать и привязать цепочки защиты портов {{ .name }}'
FirewallPersistDockerGuardStep: 'Сохранить состояние защиты портов Docker'
ErrFirewallRuleScopeChange: "Текущий межсетевой экран не поддерживает изменение области действия правила (например, семейства адресов IPv4/IPv6). Создайте новое правило."
FirewallWhitelistTask: "Обновить список разрешённых портов межсетевого экрана"
FirewallWhitelistSaved: "Настройки списка разрешённых портов сохранены"
FirewallWhitelistDeferred: "{{ .name }}: настройки сохранены; ожидается включение межсетевого экрана"
FirewallWhitelistReleased: "{{ .name }}: защита списка разрешённых портов снята; разрешающее правило сохранено. Чтобы закрыть порт, удалите правило вручную из списка правил"
FirewallWhitelistRequired: "{{ .name }}: защищён обязательными правилами системных портов"
+2 -4
View File
@@ -702,7 +702,8 @@ FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{
FirewallCreateRuleExecutionFailed: 'Kural oluşturma başarısız. Veritabanına kayıt yazılmadı ve yürütülen komutlar geri alınmadı'
FirewallCreateRulePersistenceFailed: 'Kural oluşturuldu ancak yönetim kaydı kaydedilemedi'
FirewallAdoptRulePersistenceFailed: 'Kuralı yönetime alma komutu yürütüldü, ancak yönetim kaydı kaydedilemedi'
FirewallSyncOperationsResult: 'Eşitleme işlemleri: {{ .removed }} silindi, {{ .created }} oluşturuldu, {{ .failed }} başarısız, {{ .skipped }} yürütülmedi'
FirewallSyncOperationsResult: 'Eşitleme işlemleri: {{ .removed }} silindi, {{ .created }} oluşturuldu, {{ .failed }} başarısız, {{ .skipped }} yürütülmedi, {{ .unchanged }} zaten eşleşiyor (değişiklik gerekmiyor)'
FirewallSyncRuleUnchanged: 'Zaten eşleşiyor; değişiklik gerekmiyor'
FirewallSyncStep: 'Kuralları {{ .name }} ile eşitle'
FirewallSyncFailed: '{{ .failed }} güvenlik duvarı kuralı eşitlenemedi'
FirewallResetSourceStep: 'Kaynak güvenlik duvarı {{ .name }} sıfırla ve devre dışı bırak'
@@ -716,8 +717,5 @@ FirewallInspectDockerGuardStep: 'Docker güvenlik duvarı arka ucunu ve ilkeleri
FirewallInitializeDockerGuardStep: '{{ .name }} bağlantı noktası koruma zincirlerini başlat ve bağla'
FirewallPersistDockerGuardStep: 'Docker bağlantı noktası koruma durumunu kaydet'
ErrFirewallRuleScopeChange: "Mevcut güvenlik duvarı, kuralın kapsamını (IPv4/IPv6 adres ailesi gibi) değiştirmeyi desteklemiyor. Lütfen yeni bir kural oluşturun."
FirewallWhitelistTask: "Güvenlik duvarı izin verilen port listesini güncelle"
FirewallWhitelistSaved: "İzin verilenler listesi yapılandırması kaydedildi"
FirewallWhitelistDeferred: "{{ .name }}: yapılandırma kaydedildi; güvenlik duvarının etkinleştirilmesi bekleniyor"
FirewallWhitelistReleased: "{{ .name }}: izin listesi koruması kaldırıldı; izin kuralı korundu. Portu kapatmak için kuralı kural listesinden elle silin"
FirewallWhitelistRequired: "{{ .name }}: zorunlu sistem portu kuralları tarafından korunuyor"
+2 -4
View File
@@ -702,7 +702,8 @@ FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{
FirewallCreateRuleExecutionFailed: '規則建立失敗,未寫入資料庫,已執行的命令不回復'
FirewallCreateRulePersistenceFailed: '規則已建立,但納管資訊儲存失敗'
FirewallAdoptRulePersistenceFailed: '規則納管命令已執行,但納管資訊儲存失敗'
FirewallSyncOperationsResult: '同步操作彙總:刪除成功 {{ .removed }} 條,建立成功 {{ .created }} 條,失敗 {{ .failed }} 條,未執行 {{ .skipped }} 條'
FirewallSyncOperationsResult: '同步操作彙總:刪除成功 {{ .removed }} 條,建立成功 {{ .created }} 條,失敗 {{ .failed }} 條,未執行 {{ .skipped }} 條,已一致無需變更 {{ .unchanged }} 條'
FirewallSyncRuleUnchanged: '已一致,無需變更'
FirewallSyncStep: '同步規則到 {{ .name }}'
FirewallSyncFailed: '{{ .failed }} 條防火牆規則同步失敗'
FirewallResetSourceStep: '重設並停用來源防火牆 {{ .name }}'
@@ -716,8 +717,5 @@ FirewallInspectDockerGuardStep: '檢查 Docker 防火牆後端與防護策略'
FirewallInitializeDockerGuardStep: '初始化並綁定 {{ .name }} 連接埠防護鏈'
FirewallPersistDockerGuardStep: '儲存 Docker 連接埠防護狀態'
ErrFirewallRuleScopeChange: "目前的防火牆不支援修改規則的作用範圍(如 IPv4/IPv6 位址族),請建立新規則。"
FirewallWhitelistTask: "更新防火牆連接埠白名單"
FirewallWhitelistSaved: "白名單設定已儲存"
FirewallWhitelistDeferred: "{{ .name }}:設定已儲存,待防火牆啟用後套用"
FirewallWhitelistReleased: "{{ .name }}:已解除白名單保護,放行規則保留;如需關閉連接埠,請在規則清單手動刪除"
FirewallWhitelistRequired: "{{ .name }}:由系統必要連接埠規則保護"
+2 -4
View File
@@ -702,7 +702,8 @@ FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{
FirewallCreateRuleExecutionFailed: '规则创建失败,未入库,已执行的命令不回滚'
FirewallCreateRulePersistenceFailed: '规则已创建,但纳管信息保存失败'
FirewallAdoptRulePersistenceFailed: '规则纳管命令已执行,但纳管信息保存失败'
FirewallSyncOperationsResult: '同步操作汇总:删除成功 {{ .removed }} 条,创建成功 {{ .created }} 条,失败 {{ .failed }} 条,未执行 {{ .skipped }} 条'
FirewallSyncOperationsResult: '同步操作汇总:删除成功 {{ .removed }} 条,创建成功 {{ .created }} 条,失败 {{ .failed }} 条,未执行 {{ .skipped }} 条,已一致无需变更 {{ .unchanged }} 条'
FirewallSyncRuleUnchanged: '已一致,无需变更'
FirewallSyncStep: "同步规则到 {{ .name }}"
FirewallSyncFailed: "{{ .failed }} 条防火墙规则同步失败"
FirewallResetSourceStep: "重置并停用源防火墙 {{ .name }}"
@@ -716,8 +717,5 @@ FirewallInspectDockerGuardStep: "检查 Docker 防火墙后端和防护策略"
FirewallInitializeDockerGuardStep: "初始化并绑定 {{ .name }} 端口防护链"
FirewallPersistDockerGuardStep: "保存 Docker 端口防护状态"
ErrFirewallRuleScopeChange: "当前防火墙不支持修改规则的作用范围(如 IPv4/IPv6 地址族),请新建规则。"
FirewallWhitelistTask: "更新防火墙端口白名单"
FirewallWhitelistSaved: "白名单配置已保存"
FirewallWhitelistDeferred: "{{ .name }}:配置已保存,待防火墙启用后下发"
FirewallWhitelistReleased: "{{ .name }}:已解除白名单保护,放行规则保留;如需关闭端口,请在规则列表手动删除"
FirewallWhitelistRequired: "{{ .name }}:由系统必需端口规则保护"
+15 -12
View File
@@ -11,11 +11,11 @@ import (
"github.com/1Panel-dev/1Panel/agent/app/service"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/init/migration/migrations"
migrationutils "github.com/1Panel-dev/1Panel/agent/init/migration/migrations/utils"
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/iptables_helper"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/nftables_helper"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/ping"
)
func Init() {
@@ -26,6 +26,15 @@ func Init() {
return
}
clientName := client.Name()
initialize := false
defer func() {
if err := migrations.TransferFirewalldSSHService(ctx, client, service.NewIFirewallService().SyncPortWhitelist); err != nil {
global.LOG.Warnf("synchronize firewall whitelist on startup failed, err: %v", err)
}
if initialize {
initDockerPortGuard(ctx)
}
}()
if err := migrationutils.TransferHostFirewall(ctx, clientName); err != nil {
global.LOG.Errorf("transfer legacy host firewall records failed, err: %v", err)
return
@@ -40,11 +49,11 @@ func Init() {
if err := initForwardingRules(ctx); err != nil {
global.LOG.Warnf("restore forwarding rules failed, manual synchronization is available, err: %v", err)
}
if !needInit() {
initialize = needInit()
if !initialize {
repairIptablesBaseChains(clientName)
return
}
defer initDockerPortGuard(ctx)
InitPingStatus()
global.LOG.Info("initializing firewall settings...")
if clientName == "nftables" {
@@ -64,17 +73,12 @@ func Init() {
return
}
settingRepo := repo.NewISettingRepo()
panelPort := service.LoadPanelPort()
if len(panelPort) == 0 {
global.LOG.Errorf("find 1panel service port failed")
return
}
requiredPorts, err := service.LoadRequiredFirewallPortWhiteList()
if err != nil {
global.LOG.Errorf("load required firewall ports failed, err: %v", err)
return
}
if err := iptables_helper.RestoreBaseChains(panelPort, requiredPorts); err != nil {
if err := iptables_helper.RestoreBaseChains(requiredPorts); err != nil {
global.LOG.Errorf("restore iptables base chains failed, err: %v", err)
return
}
@@ -100,7 +104,6 @@ func repairIptablesBaseChains(clientName string) {
return
}
manager := iptables_helper.Manager{
PanelPort: service.LoadPanelPort,
LoadRequiredPorts: service.LoadRequiredFirewallPortWhiteList,
}
if err := manager.RepairBaseChains(); err != nil {
@@ -150,7 +153,7 @@ func needInit() bool {
func InitPingStatus() {
global.LOG.Info("initializing ban ping status from settings...")
status := ping.LoadStatus()
status := firewall.LoadPingStatus()
statusInDB, _ := repo.NewISettingRepo().GetValueByKey("BanPing")
if statusInDB == status {
return
@@ -160,7 +163,7 @@ func InitPingStatus() {
if statusInDB == constant.StatusDisable {
enable = "0"
}
if err := ping.UpdateStatus(enable); err != nil {
if err := firewall.UpdatePingStatus(enable); err != nil {
global.LOG.Errorf("initialize ping status failed: %v", err)
}
}
+1
View File
@@ -110,6 +110,7 @@ func agentDBMigrations() []*gormigrate.Migration {
migrations.NormalizeFirewallBackendSelections,
migrations.SimplifyFirewallRulePolicy,
migrations.AddDockerPortGuardReadOnly,
migrations.MigrateFirewallPortWhitelistSources,
}
}
@@ -0,0 +1,215 @@
package migrations
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"unicode"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/app/service"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle/providers"
"github.com/go-gormigrate/gormigrate/v2"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
const firewalldSSHServiceMigrationID = "20260916-remove-firewalld-ssh-service"
func TransferFirewalldSSHService(ctx context.Context, client lifecycle.Client, syncWhitelist func(context.Context) error) error {
return transferFirewalldSSHService(ctx, global.DB, client, syncWhitelist)
}
func transferFirewalldSSHService(ctx context.Context, db *gorm.DB, client lifecycle.Client, syncWhitelist func(context.Context) error) error {
if err := syncWhitelist(ctx); err != nil {
return err
}
if client.Name() != lifecycle.ProviderFirewalld {
return nil
}
var count int64
if err := db.WithContext(ctx).Table("migrations").Where("id = ?", firewalldSSHServiceMigrationID).Count(&count).Error; err != nil {
return fmt.Errorf("check firewalld SSH service migration: %w", err)
}
if count > 0 {
return nil
}
active, err := client.Status()
if err != nil || !active {
return err
}
if err := ctx.Err(); err != nil {
return err
}
if err := providers.RemoveFirewalldSSHService(); err != nil {
return fmt.Errorf("transfer firewalld SSH access to whitelist: %w", err)
}
if err := db.WithContext(ctx).Table("migrations").Clauses(clause.OnConflict{DoNothing: true}).
Create(map[string]interface{}{"id": firewalldSSHServiceMigrationID}).Error; err != nil {
return fmt.Errorf("record firewalld SSH service migration: %w", err)
}
return nil
}
var MigrateFirewallPortWhitelistSources = &gormigrate.Migration{
ID: "20260915-migrate-firewall-port-whitelist-sources",
Migrate: func(tx *gorm.DB) error {
var setting model.Setting
err := tx.Where("key = ?", constant.FirewallPortWhiteList).First(&setting).Error
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
rules, err := migrateFirewallPortWhitelist(setting.Value)
if err != nil {
return fmt.Errorf("migrate firewall port whitelist: %w", err)
}
value, err := json.Marshal(rules)
if err != nil {
return err
}
if setting.ID == 0 {
err = tx.Create(&model.Setting{Key: constant.FirewallPortWhiteList, Value: string(value)}).Error
} else {
err = tx.Model(&setting).Update("value", string(value)).Error
}
if err != nil {
return err
}
return tx.Where("key = ?", "FirewallPortWhiteListPending").Delete(&model.Setting{}).Error
},
}
type legacyPortWhitelist struct {
Ports []string `json:"ports"`
Family string `json:"family"`
Port string `json:"port"`
Protocol string `json:"protocol"`
Type string `json:"type"`
Sources []string `json:"sources"`
}
func (entry legacyPortWhitelist) singlePortRule() firewall.PortWhitelist {
rule := firewall.PortWhitelist{Port: entry.Port, Protocol: entry.Protocol, Type: entry.Type, Sources: entry.Sources}
if strings.TrimSpace(rule.Type) != "" && rule.Port == "" && len(entry.Ports) > 0 {
rule.Port = entry.Ports[0]
}
return rule
}
func migrateFirewallPortWhitelist(value string) ([]firewall.PortWhitelist, error) {
legacy, err := parseLegacyPortWhitelist(value)
if err != nil {
return nil, err
}
rules := make([]firewall.PortWhitelist, 0, len(legacy)+5)
indexes := make(map[string]int)
key := func(rule firewall.PortWhitelist) string {
if rule.Type != "" {
return rule.Type + "/" + rule.Protocol
}
return rule.Type + "/" + rule.Protocol + "/" + rule.Port
}
for index, entry := range legacy {
family := strings.ToLower(strings.TrimSpace(entry.Family))
if family != "" && family != constant.FirewallFamilyIPv4 && family != constant.FirewallFamilyIPv6 {
return nil, fmt.Errorf("entry #%d: invalid address family %q", index+1, entry.Family)
}
rule := entry.singlePortRule()
if strings.TrimSpace(rule.Protocol) == "" {
rule.Protocol = "tcp"
}
if len(rule.Sources) == 0 {
rule.Sources = []string{"0.0.0.0/0"}
if family == constant.FirewallFamilyIPv6 {
rule.Sources = []string{"::/0"}
} else if family == "" && strings.TrimSpace(rule.Type) != "" {
rule.Sources = append(rule.Sources, "::/0")
}
}
rule.Sources, err = firewall.NormalizeWhitelistSources(family, rule.Sources)
if err != nil {
return nil, fmt.Errorf("entry #%d: %w", index+1, err)
}
normalized, err := service.InitializeFirewallWhitelistPorts([]firewall.PortWhitelist{rule})
if err != nil {
return nil, fmt.Errorf("entry #%d: %w", index+1, err)
}
rule = normalized[0]
if existing, found := indexes[key(rule)]; found {
rules[existing].Sources, err = firewall.NormalizeWhitelistSources("", append(rules[existing].Sources, rule.Sources...))
if err != nil {
return nil, err
}
continue
}
indexes[key(rule)] = len(rules)
rules = append(rules, rule)
}
defaults := []firewall.PortWhitelist{
{Type: firewall.PortWhitelistTypePanel, Protocol: "tcp"},
{Type: firewall.PortWhitelistTypeSSH, Protocol: "tcp"},
{Port: "443", Protocol: "tcp"},
{Port: "443", Protocol: "udp"},
{Port: "80", Protocol: "tcp"},
}
for _, rule := range defaults {
index, found := indexes[key(rule)]
if !found {
index = len(rules)
indexes[key(rule)] = index
rules = append(rules, rule)
}
var ipv4, ipv6 bool
for _, source := range rules[index].Sources {
if strings.Contains(source, ":") {
ipv6 = true
} else {
ipv4 = true
}
}
if !ipv4 {
rules[index].Sources = append(rules[index].Sources, "0.0.0.0/0")
}
if !ipv6 {
rules[index].Sources = append(rules[index].Sources, "::/0")
}
}
return service.InitializeFirewallWhitelistPorts(rules)
}
func parseLegacyPortWhitelist(value string) ([]legacyPortWhitelist, error) {
value = strings.TrimSpace(value)
if value == "" || value == "null" {
return nil, nil
}
if strings.HasPrefix(value, "[") {
var rules []legacyPortWhitelist
err := json.Unmarshal([]byte(value), &rules)
return rules, err
}
items := strings.FieldsFunc(value, func(r rune) bool { return r == ',' || r == ';' || unicode.IsSpace(r) })
rules := make([]legacyPortWhitelist, 0, len(items))
for _, item := range items {
parts := strings.Split(item, "/")
rule := legacyPortWhitelist{}
switch len(parts) {
case 1:
rule.Port = parts[0]
case 2:
rule.Port, rule.Protocol = parts[0], parts[1]
case 3:
rule.Family, rule.Port, rule.Protocol = parts[0], parts[1], parts[2]
default:
return nil, fmt.Errorf("invalid legacy whitelist entry %q", item)
}
rules = append(rules, rule)
}
return rules, nil
}
+2 -2
View File
@@ -23,7 +23,7 @@ import (
"github.com/1Panel-dev/1Panel/agent/utils/common"
"github.com/1Panel-dev/1Panel/agent/utils/copier"
"github.com/1Panel-dev/1Panel/agent/utils/encrypt"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/ping"
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
"github.com/1Panel-dev/1Panel/agent/utils/ssh"
"github.com/1Panel-dev/1Panel/agent/utils/xpack"
@@ -1131,7 +1131,7 @@ var AddisIPtoWebsiteSSL = &gormigrate.Migration{
var InitPingStatus = &gormigrate.Migration{
ID: "20251201-init-ping-status",
Migrate: func(tx *gorm.DB) error {
status := ping.LoadStatus()
status := firewall.LoadPingStatus()
if err := tx.Create(&model.Setting{Key: "BanPing", Value: status}).Error; err != nil {
return err
}
+7
View File
@@ -4,6 +4,7 @@ import (
"bytes"
"fmt"
"path"
"strconv"
"github.com/1Panel-dev/1Panel/agent/cmd/server/conf"
"github.com/1Panel-dev/1Panel/agent/global"
@@ -61,4 +62,10 @@ func initBaseInfo() {
panic(err)
}
global.CONF.Base.InstallDir = nodeInfo.BaseDir
if !global.IsMaster {
global.CONF.Base.Port = strconv.FormatUint(uint64(nodeInfo.NodePort), 10)
if nodeInfo.NodePort == 0 {
global.CONF.Base.Port = "9999"
}
}
}
+3 -1
View File
@@ -27,7 +27,9 @@ func (s *HostRouter) InitRouter(Router *gin.RouterGroup) {
hostRouter.POST("/firewall/port", baseApi.UpdatePanelFirewallPort)
hostRouter.GET("/firewall/settings", baseApi.LoadFirewallSettings)
hostRouter.POST("/firewall/settings/operate", baseApi.OperateFirewallBackend)
hostRouter.POST("/firewall/settings/whitelist", baseApi.UpdateFirewallPortWhitelist)
hostRouter.POST("/firewall/settings/whitelist", baseApi.CreateFirewallPortWhitelist)
hostRouter.POST("/firewall/settings/whitelist/update", baseApi.UpdateFirewallPortWhitelist)
hostRouter.POST("/firewall/settings/whitelist/delete", baseApi.DeleteFirewallPortWhitelist)
hostRouter.POST("/firewall/forward/base", baseApi.LoadForwardingBaseInfo)
hostRouter.POST("/firewall/forward/search", baseApi.SearchForwardingRules)
hostRouter.POST("/firewall/forward/operate", baseApi.OperateForwardingRules)
+16 -1
View File
@@ -2,6 +2,7 @@ package manager
import (
"errors"
"strings"
"time"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
@@ -18,5 +19,19 @@ func handlerErr(out string, err error) error {
}
func run(name string, args ...string) (string, error) {
return cmd.NewCommandMgr(cmd.WithTimeout(10*time.Second), cmd.WithEnv("LANGUAGE=en_US:en")).RunWithStdout(name, args...)
return runWithTimeout(10*time.Second, name, args...)
}
func runWithTimeout(timeout time.Duration, name string, args ...string) (string, error) {
return cmd.NewCommandMgr(cmd.WithTimeout(timeout), cmd.WithEnv("LANGUAGE=en_US:en")).RunWithStdout(name, args...)
}
func serviceOperationTimeout(operation, serviceName string) time.Duration {
if operation == "restart" {
switch strings.TrimSuffix(serviceName, ".service") {
case "docker", "dockerd", "docker.dockerd", "snap.docker.dockerd":
return 2 * time.Minute
}
}
return 10 * time.Second
}
+1 -1
View File
@@ -46,7 +46,7 @@ func (s *Openrc) Operate(operate, serviceName string) error {
case "disable":
return handlerErr(run("rc-update", "del", serviceName, "default"))
default:
return handlerErr(run(s.toolCmd, serviceName, operate))
return handlerErr(runWithTimeout(serviceOperationTimeout(operate, serviceName), s.toolCmd, serviceName, operate))
}
}
+3 -2
View File
@@ -1,6 +1,7 @@
package manager
import (
"fmt"
"strings"
)
@@ -48,7 +49,7 @@ func (s *Snap) IsEnable(serviceName string) bool {
func (s *Snap) Operate(operate, serviceName string) error {
if s.IsExist(serviceName) {
return handlerErr(run(s.toolCmd, operate, serviceName))
return handlerErr(runWithTimeout(serviceOperationTimeout(operate, serviceName), s.toolCmd, operate, serviceName))
}
return nil
return fmt.Errorf("snap service %q does not exist", serviceName)
}
+2 -2
View File
@@ -57,7 +57,7 @@ func (s *Systemd) Status(serviceName string) (string, error) {
return run(s.toolCmd, "status", serviceName)
}
func (s *Systemd) Operate(operate, serviceName string) error {
out, err := run(s.toolCmd, operate, serviceName)
out, err := runWithTimeout(serviceOperationTimeout(operate, serviceName), s.toolCmd, operate, serviceName)
if err != nil {
if fallbackName := systemdAliasFallbackName(serviceName); fallbackName != "" && strings.Contains(out, "alias name or linked unit file") {
return s.Operate(operate, fallbackName)
@@ -65,7 +65,7 @@ func (s *Systemd) Operate(operate, serviceName string) error {
if err := NewSnap().Operate(operate, serviceName); err == nil {
return nil
}
return handlerErr(run(s.toolCmd, operate, serviceName))
return handlerErr(out, err)
}
return nil
}
+1 -1
View File
@@ -39,7 +39,7 @@ func (s *Sysvinit) Status(serviceName string) (string, error) {
}
func (s *Sysvinit) Operate(operate, serviceName string) error {
return handlerErr(run(s.toolCmd, serviceName, operate))
return handlerErr(runWithTimeout(serviceOperationTimeout(operate, serviceName), s.toolCmd, serviceName, operate))
}
func (s *Sysvinit) Reload() error {
+71 -20
View File
@@ -16,6 +16,7 @@ import (
"io"
"io/fs"
"math"
"mime"
"net"
"net/http"
"net/url"
@@ -28,6 +29,8 @@ import (
"sync"
"syscall"
"time"
"unicode"
"unicode/utf8"
"github.com/1Panel-dev/1Panel/agent/buserr"
@@ -386,9 +389,23 @@ type DownloadProxyConfig struct {
type DownloadOptions struct {
IgnoreCertificate bool
UseServerFilename bool
Proxy *DownloadProxyConfig
}
func downloadResponseFilename(header string) string {
_, params, err := mime.ParseMediaType(header)
if err != nil {
return ""
}
name := strings.TrimSpace(params["filename"])
if name == "" || name == "." || name == ".." || len(name) > 255 || !utf8.ValidString(name) ||
strings.ContainsAny(name, "/\\:") || strings.IndexFunc(name, unicode.IsControl) >= 0 {
return ""
}
return name
}
func buildDownloadProxyURL(proxy DownloadProxyConfig) (*url.URL, error) {
proxyType := strings.TrimSpace(proxy.Type)
proxyHost := strings.TrimSpace(proxy.URL)
@@ -455,7 +472,7 @@ type downloadPolicy struct {
idleTimeout time.Duration
}
var remoteDownloadPolicy = downloadPolicy{retries: 3, retryDelay: 2 * time.Second, idleTimeout: 90 * time.Second}
var remoteDownloadPolicy = downloadPolicy{retries: 3, retryDelay: 5 * time.Second, idleTimeout: 90 * time.Second}
func saveDownloadProcess(process Process) {
if process.Total > 0 {
@@ -492,20 +509,27 @@ func (f FileOp) DownloadFileWithProcess(rawURL, dst, key string, options Downloa
client.CloseIdleConnections()
return err
}
original, err := os.Lstat(dst)
if err != nil && !os.IsNotExist(err) {
client.CloseIdleConnections()
return err
}
if original != nil && !original.Mode().IsRegular() {
client.CloseIdleConnections()
return fmt.Errorf("download target must be a regular file")
parent = filepath.Dir(dst)
var original os.FileInfo
if !options.UseServerFilename {
original, err = os.Lstat(dst)
if err != nil && !os.IsNotExist(err) {
client.CloseIdleConnections()
return err
}
if original != nil && !original.Mode().IsRegular() {
client.CloseIdleConnections()
return fmt.Errorf("download target must be a regular file")
}
}
ctx, cancel := context.WithCancel(context.Background())
task := &downloadTask{cancel: cancel, done: make(chan struct{}), dst: dst}
if options.UseServerFilename {
task.dst = ""
}
downloadMu.Lock()
for _, active := range downloadTasks {
if active.dst == dst {
if task.dst != "" && active.dst == task.dst {
downloadMu.Unlock()
cancel()
client.CloseIdleConnections()
@@ -532,6 +556,34 @@ func (f FileOp) DownloadFileWithProcess(rawURL, dst, key string, options Downloa
close(task.done)
}()
process := Process{Key: key, Name: filepath.Base(dst), Status: "Downloading"}
nameResolved := !options.UseServerFilename
resolveName := func(resp *http.Response) (string, error) {
if nameResolved {
return dst, nil
}
name := downloadResponseFilename(resp.Header.Get("Content-Disposition"))
if name == "" {
name = filepath.Base(dst)
}
resolved := filepath.Join(parent, name)
process.Name = name
downloadMu.Lock()
defer downloadMu.Unlock()
for otherKey, active := range downloadTasks {
if otherKey != key && active.dst == resolved {
return "", buserr.New("TaskIsExecuting")
}
}
if _, statErr := os.Lstat(resolved); statErr == nil {
return "", fmt.Errorf("download target already exists: %s", name)
} else if !os.IsNotExist(statErr) {
return "", statErr
}
task.dst = resolved
dst = resolved
nameResolved = true
return dst, nil
}
update := func(state downloadState, status string, attempt int) {
process.Written = uint64(state.written)
process.Total = uint64(max(0, state.total))
@@ -553,7 +605,7 @@ func (f FileOp) DownloadFileWithProcess(rawURL, dst, key string, options Downloa
record, runErr = recordDownloadPart(out.Name(), partInfo)
}
if runErr == nil {
runErr = runRemoteDownload(ctx, client, rawURL, dst, out, remoteDownloadPolicy, update)
runErr = runRemoteDownload(ctx, client, rawURL, dst, out, remoteDownloadPolicy, update, resolveName)
}
task.mu.Lock()
if ctx.Err() != nil {
@@ -685,7 +737,7 @@ func retryDownloadError(err error) bool {
}
func runRemoteDownload(ctx context.Context, client *http.Client, rawURL, dst string, out *os.File,
policy downloadPolicy, update func(downloadState, string, int)) error {
policy downloadPolicy, update func(downloadState, string, int), resolveName ...func(*http.Response) (string, error)) error {
state := downloadState{total: -1}
for attempt := 0; ; attempt++ {
if err := ctx.Err(); err != nil {
@@ -693,7 +745,7 @@ func runRemoteDownload(ctx context.Context, client *http.Client, rawURL, dst str
}
update(state, "Downloading", attempt)
retry, retryAfter, err := downloadAttempt(ctx, client, rawURL, dst, out, &state, policy.idleTimeout,
func() { update(state, "Downloading", attempt) })
func() { update(state, "Downloading", attempt) }, resolveName...)
if err == nil {
return nil
}
@@ -719,7 +771,7 @@ func runRemoteDownload(ctx context.Context, client *http.Client, rawURL, dst str
}
func downloadAttempt(ctx context.Context, client *http.Client, rawURL, dst string, out *os.File,
state *downloadState, idleTimeout time.Duration, progress func()) (bool, time.Duration, error) {
state *downloadState, idleTimeout time.Duration, progress func(), resolveName ...func(*http.Response) (string, error)) (bool, time.Duration, error) {
attemptCtx, cancel := context.WithCancel(ctx)
defer cancel()
request, err := http.NewRequestWithContext(attemptCtx, http.MethodGet, rawURL, nil)
@@ -755,12 +807,6 @@ func downloadAttempt(ctx context.Context, client *http.Client, rawURL, dst strin
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusPartialContent {
return false, 0, fmt.Errorf("remote download returned HTTP %d", resp.StatusCode)
}
ct := strings.ToLower(resp.Header.Get("Content-Type"))
ext := strings.ToLower(filepath.Ext(dst))
if (strings.Contains(ct, "text/html") || strings.Contains(ct, "text/xml")) &&
ext != ".html" && ext != ".htm" && ext != ".xml" && ext != ".svg" {
return false, 0, fmt.Errorf("unexpected download Content-Type: %s", ct)
}
if encoding := resp.Header.Get("Content-Encoding"); encoding != "" && !strings.EqualFold(encoding, "identity") {
return false, 0, fmt.Errorf("unexpected download Content-Encoding: %s", encoding)
}
@@ -793,6 +839,11 @@ func downloadAttempt(ctx context.Context, client *http.Client, rawURL, dst strin
state.etag = etag
}
}
if len(resolveName) > 0 {
if _, err := resolveName[0](resp); err != nil {
return false, 0, err
}
}
progress()
timer := time.AfterFunc(idleTimeout, cancel)
defer timer.Stop()
@@ -1,42 +0,0 @@
package docker_guard
import (
"errors"
"fmt"
"strings"
)
var ErrDockerForwardPolicyDrop = errors.New("iptables FORWARD default policy is DROP")
func (m *NftablesManager) checkForwardPolicy() error {
for _, family := range []struct{ command, name string }{
{"iptables", FamilyIPv4},
{"ip6tables", FamilyIPv6},
} {
if !m.runner.Exists(family.command) {
continue
}
output, err := m.runner.Run(family.command, "-t", "filter", "-w", "-S", "FORWARD")
if err != nil {
return &FamilyError{Family: family.name, Err: fmt.Errorf("inspect iptables FORWARD policy: %w", err)}
}
found := false
for _, line := range strings.Split(output, "\n") {
fields := strings.Fields(line)
if len(fields) != 3 || fields[0] != "-P" || fields[1] != "FORWARD" {
continue
}
found = true
if fields[2] == "DROP" {
return &FamilyError{Family: family.name, Err: ErrDockerForwardPolicyDrop}
}
if fields[2] != "ACCEPT" {
return &FamilyError{Family: family.name, Err: fmt.Errorf("unexpected iptables FORWARD policy: %s", fields[2])}
}
}
if !found {
return &FamilyError{Family: family.name, Err: errors.New("iptables FORWARD default policy was not found")}
}
}
return nil
}
@@ -133,6 +133,9 @@ func NewManager() *Manager { return &Manager{runner: commandRunner{}} }
func (m *Manager) Initialize(policies []Policy) error {
mutationMu.Lock()
defer mutationMu.Unlock()
if err := CheckIPv4Forwarding(); err != nil {
return err
}
inventory, err := m.ListPolicies()
if err != nil {
return err
@@ -30,6 +30,9 @@ func (m *NftablesManager) Initialize(policies []Policy) error {
if !m.runner.Exists("nft") {
return errors.New("nft is not installed")
}
if err := CheckIPv4Forwarding(); err != nil {
return err
}
if err := m.checkForwardPolicy(); err != nil {
return err
}
@@ -493,3 +496,38 @@ func nftHasFirstUniqueJump(output string) bool {
}
return false
}
var ErrDockerForwardPolicyDrop = errors.New("iptables FORWARD default policy is DROP")
func (m *NftablesManager) checkForwardPolicy() error {
for _, family := range []struct{ command, name string }{
{"iptables", FamilyIPv4},
{"ip6tables", FamilyIPv6},
} {
if !m.runner.Exists(family.command) {
continue
}
output, err := m.runner.Run(family.command, "-t", "filter", "-w", "-S", "FORWARD")
if err != nil {
return &FamilyError{Family: family.name, Err: fmt.Errorf("inspect iptables FORWARD policy: %w", err)}
}
found := false
for _, line := range strings.Split(output, "\n") {
fields := strings.Fields(line)
if len(fields) != 3 || fields[0] != "-P" || fields[1] != "FORWARD" {
continue
}
found = true
if fields[2] == "DROP" {
return &FamilyError{Family: family.name, Err: ErrDockerForwardPolicyDrop}
}
if fields[2] != "ACCEPT" {
return &FamilyError{Family: family.name, Err: fmt.Errorf("unexpected iptables FORWARD policy: %s", fields[2])}
}
}
if !found {
return &FamilyError{Family: family.name, Err: errors.New("iptables FORWARD default policy was not found")}
}
}
return nil
}
+249
View File
@@ -8,6 +8,8 @@ import (
"sort"
"strconv"
"strings"
"github.com/mattn/go-shellwords"
)
var ErrInvalidPolicy = errors.New("invalid Docker port guard request")
@@ -144,3 +146,250 @@ func DecodeSources(value string) []string {
_ = json.Unmarshal([]byte(value), &result)
return result
}
type observedPolicy struct {
policy Policy
sequence int64
nativeRules []NativeRule
managedOrders []int64
dropAll bool
droppedSource []string
allowedSource []string
acceptedSource []string
acceptAll bool
}
func parseDockerGuardPolicies(output, family string) (PolicyInventory, error) {
groups := make(map[string]*observedPolicy)
order := make([]string, 0)
sequence := int64(0)
for _, line := range strings.Split(output, "\n") {
line = strings.TrimSpace(line)
if line == "" {
continue
}
tokens, err := shellwords.Parse(line)
if err != nil {
return PolicyInventory{}, fmt.Errorf("parse Docker guard rule: %w", err)
}
managed := strings.Contains(line, "1panel-docker:")
if !managed && !hasAcceptAction(tokens) {
continue
}
sequence++
fragment, source, action, err := parseDockerGuardRuleTokens(tokens, family)
if err != nil {
return PolicyInventory{}, err
}
identity := fragment.UUID
if action == "accept" {
identity = action
}
key := strings.Join([]string{identity, fragment.Family, fragment.HostIP, strconv.Itoa(int(fragment.HostPort)), fragment.Protocol}, "|")
group, exists := groups[key]
if !exists {
group = &observedPolicy{policy: fragment, sequence: sequence}
groups[key] = group
order = append(order, key)
}
switch {
case action == "accept" && source != "":
group.acceptedSource = append(group.acceptedSource, source)
group.nativeRules = append(group.nativeRules, NativeRule{Family: family, Order: sequence, Tokens: nativeRuleTokens(tokens)})
case action == "accept":
group.acceptAll = true
group.nativeRules = append(group.nativeRules, NativeRule{Family: family, Order: sequence, Tokens: nativeRuleTokens(tokens)})
case action == "return" && source != "":
group.allowedSource = append(group.allowedSource, source)
case action == "drop" && source != "":
group.droppedSource = append(group.droppedSource, source)
case action == "drop":
group.dropAll = true
default:
return PolicyInventory{}, fmt.Errorf("unsupported Docker guard rule action %q", action)
}
if action != "accept" {
group.managedOrders = append(group.managedOrders, sequence)
}
}
inventory := PolicyInventory{Policies: make([]Policy, 0, len(order)), ManagedRuleOrders: make(map[string][]int64)}
for _, key := range order {
group := groups[key]
if group.acceptAll || len(group.acceptedSource) > 0 {
group.policy.Sources = uniqueSortedStrings(group.acceptedSource)
inventory.ReadOnly = append(inventory.ReadOnly, ReadOnlyPolicy{
Policy: group.policy, Action: "accept", Sequence: group.sequence, NativeRules: group.nativeRules,
})
continue
}
switch {
case len(group.allowedSource) > 0:
group.policy.Mode = ModeAllow
group.policy.Sources = uniqueSortedStrings(group.allowedSource)
case len(group.droppedSource) > 0:
group.policy.Mode = ModeSources
group.policy.Sources = uniqueSortedStrings(group.droppedSource)
case group.dropAll:
group.policy.Mode = ModeAll
default:
return PolicyInventory{}, fmt.Errorf("Docker guard policy %s has no effective rules", group.policy.UUID)
}
inventory.Policies = append(inventory.Policies, group.policy)
inventory.ManagedRuleOrders[managedOrderKey(group.policy.Family, group.policy.UUID)] = append([]int64(nil), group.managedOrders...)
}
return inventory, nil
}
func nativeRuleTokens(tokens []string) []string {
result := make([]string, 0, len(tokens))
for index, token := range tokens {
if token == "#" {
tokens = tokens[:index]
break
}
}
if len(tokens) >= 2 && tokens[len(tokens)-2] == "handle" {
tokens = tokens[:len(tokens)-2]
}
for index := 0; index < len(tokens); index++ {
result = append(result, tokens[index])
if tokens[index] == "counter" && index+4 < len(tokens) && tokens[index+1] == "packets" && tokens[index+3] == "bytes" {
index += 4
}
}
return result
}
func managedOrderKey(family, policyUUID string) string {
return family + "\x00" + policyUUID
}
func parseDockerGuardRuleTokens(tokens []string, family string) (Policy, string, string, error) {
policy := Policy{Family: family, HostIP: wildcardHost(family)}
source, action := "", ""
for index := 0; index < len(tokens); index++ {
switch tokens[index] {
case "-p":
policy.Protocol = nextPolicyToken(tokens, index)
case "--ctorigdst":
policy.HostIP = normalizeObservedHost(nextPolicyToken(tokens, index))
case "-d":
policy.HostIP = normalizeObservedHost(nextPolicyToken(tokens, index))
case "--ctorigdstport":
policy.HostPort = parsePolicyPort(nextPolicyToken(tokens, index))
case "--dport":
policy.HostPort = parsePolicyPort(nextPolicyToken(tokens, index))
case "-s":
source = nextPolicyToken(tokens, index)
case "--comment", "comment":
marker := nextPolicyToken(tokens, index)
if strings.HasPrefix(marker, "1panel-docker:") {
policy.UUID = strings.TrimPrefix(marker, "1panel-docker:")
}
case "-j":
action = strings.ToLower(nextPolicyToken(tokens, index))
case "meta":
if nextPolicyToken(tokens, index) == "l4proto" {
policy.Protocol = nextPolicyToken(tokens, index+1)
}
case "ct":
if nextPolicyToken(tokens, index) != "original" {
continue
}
switch nextPolicyToken(tokens, index+1) {
case "proto-dst":
policy.HostPort = parsePolicyPort(nextPolicyToken(tokens, index+2))
case "ip", "ip6":
if nextPolicyToken(tokens, index+2) == "daddr" {
policy.HostIP = normalizeObservedHost(nextPolicyToken(tokens, index+3))
}
}
case "ip", "ip6":
switch nextPolicyToken(tokens, index) {
case "saddr":
source = nextPolicyToken(tokens, index+1)
case "daddr":
policy.HostIP = normalizeObservedHost(nextPolicyToken(tokens, index+1))
}
case "tcp", "udp":
if nextPolicyToken(tokens, index) == "dport" {
policy.Protocol = tokens[index]
policy.HostPort = parsePolicyPort(nextPolicyToken(tokens, index+1))
}
case "accept", "drop", "return":
if isCommentValue(tokens, index) {
continue
}
action = tokens[index]
}
}
if action == "" || (action != "accept" && (policy.UUID == "" || policy.Protocol == "" || policy.HostPort == 0)) {
return Policy{}, "", "", fmt.Errorf("incomplete 1Panel Docker guard rule")
}
if action == "accept" && policy.Protocol == "" {
policy.Protocol = "all"
}
return policy, source, action, nil
}
func hasAcceptAction(tokens []string) bool {
for index, token := range tokens {
if token == "-j" && strings.EqualFold(nextPolicyToken(tokens, index), "accept") {
return true
}
if strings.EqualFold(token, "accept") && !isCommentValue(tokens, index) {
return true
}
}
return false
}
func isCommentValue(tokens []string, index int) bool {
if index == 0 {
return false
}
return tokens[index-1] == "comment" || tokens[index-1] == "--comment"
}
func normalizeObservedHost(value string) string {
if prefix, err := netip.ParsePrefix(value); err == nil && prefix.Bits() == prefix.Addr().BitLen() {
return prefix.Addr().String()
}
return value
}
func nextPolicyToken(tokens []string, index int) string {
if index+1 >= len(tokens) {
return ""
}
return tokens[index+1]
}
func parsePolicyPort(value string) uint16 {
port, err := strconv.ParseUint(value, 10, 16)
if err != nil || port == 0 {
return 0
}
return uint16(port)
}
func wildcardHost(family string) string {
if family == FamilyIPv6 {
return "::"
}
return "0.0.0.0"
}
func uniqueSortedStrings(values []string) []string {
seen := make(map[string]struct{}, len(values))
result := make([]string, 0, len(values))
for _, value := range values {
if _, exists := seen[value]; exists {
continue
}
seen[value] = struct{}{}
result = append(result, value)
}
sort.Strings(result)
return result
}
@@ -1,258 +0,0 @@
package docker_guard
import (
"fmt"
"net/netip"
"sort"
"strconv"
"strings"
"github.com/mattn/go-shellwords"
)
type observedPolicy struct {
policy Policy
sequence int64
nativeRules []NativeRule
managedOrders []int64
dropAll bool
droppedSource []string
allowedSource []string
acceptedSource []string
acceptAll bool
}
func parseDockerGuardPolicies(output, family string) (PolicyInventory, error) {
groups := make(map[string]*observedPolicy)
order := make([]string, 0)
sequence := int64(0)
for _, line := range strings.Split(output, "\n") {
line = strings.TrimSpace(line)
if line == "" {
continue
}
tokens, err := shellwords.Parse(line)
if err != nil {
return PolicyInventory{}, fmt.Errorf("parse Docker guard rule: %w", err)
}
managed := strings.Contains(line, "1panel-docker:")
if !managed && !hasAcceptAction(tokens) {
continue
}
sequence++
fragment, source, action, err := parseDockerGuardRuleTokens(tokens, family)
if err != nil {
return PolicyInventory{}, err
}
identity := fragment.UUID
if action == "accept" {
identity = action
}
key := strings.Join([]string{identity, fragment.Family, fragment.HostIP, strconv.Itoa(int(fragment.HostPort)), fragment.Protocol}, "|")
group, exists := groups[key]
if !exists {
group = &observedPolicy{policy: fragment, sequence: sequence}
groups[key] = group
order = append(order, key)
}
switch {
case action == "accept" && source != "":
group.acceptedSource = append(group.acceptedSource, source)
group.nativeRules = append(group.nativeRules, NativeRule{Family: family, Order: sequence, Tokens: nativeRuleTokens(tokens)})
case action == "accept":
group.acceptAll = true
group.nativeRules = append(group.nativeRules, NativeRule{Family: family, Order: sequence, Tokens: nativeRuleTokens(tokens)})
case action == "return" && source != "":
group.allowedSource = append(group.allowedSource, source)
case action == "drop" && source != "":
group.droppedSource = append(group.droppedSource, source)
case action == "drop":
group.dropAll = true
default:
return PolicyInventory{}, fmt.Errorf("unsupported Docker guard rule action %q", action)
}
if action != "accept" {
group.managedOrders = append(group.managedOrders, sequence)
}
}
inventory := PolicyInventory{Policies: make([]Policy, 0, len(order)), ManagedRuleOrders: make(map[string][]int64)}
for _, key := range order {
group := groups[key]
if group.acceptAll || len(group.acceptedSource) > 0 {
group.policy.Sources = uniqueSortedStrings(group.acceptedSource)
inventory.ReadOnly = append(inventory.ReadOnly, ReadOnlyPolicy{
Policy: group.policy, Action: "accept", Sequence: group.sequence, NativeRules: group.nativeRules,
})
continue
}
switch {
case len(group.allowedSource) > 0:
group.policy.Mode = ModeAllow
group.policy.Sources = uniqueSortedStrings(group.allowedSource)
case len(group.droppedSource) > 0:
group.policy.Mode = ModeSources
group.policy.Sources = uniqueSortedStrings(group.droppedSource)
case group.dropAll:
group.policy.Mode = ModeAll
default:
return PolicyInventory{}, fmt.Errorf("Docker guard policy %s has no effective rules", group.policy.UUID)
}
inventory.Policies = append(inventory.Policies, group.policy)
inventory.ManagedRuleOrders[managedOrderKey(group.policy.Family, group.policy.UUID)] = append([]int64(nil), group.managedOrders...)
}
return inventory, nil
}
func nativeRuleTokens(tokens []string) []string {
result := make([]string, 0, len(tokens))
for index, token := range tokens {
if token == "#" {
tokens = tokens[:index]
break
}
}
if len(tokens) >= 2 && tokens[len(tokens)-2] == "handle" {
tokens = tokens[:len(tokens)-2]
}
for index := 0; index < len(tokens); index++ {
result = append(result, tokens[index])
if tokens[index] == "counter" && index+4 < len(tokens) && tokens[index+1] == "packets" && tokens[index+3] == "bytes" {
index += 4
}
}
return result
}
func managedOrderKey(family, policyUUID string) string {
return family + "\x00" + policyUUID
}
func parseDockerGuardRuleTokens(tokens []string, family string) (Policy, string, string, error) {
policy := Policy{Family: family, HostIP: wildcardHost(family)}
source, action := "", ""
for index := 0; index < len(tokens); index++ {
switch tokens[index] {
case "-p":
policy.Protocol = nextPolicyToken(tokens, index)
case "--ctorigdst":
policy.HostIP = normalizeObservedHost(nextPolicyToken(tokens, index))
case "-d":
policy.HostIP = normalizeObservedHost(nextPolicyToken(tokens, index))
case "--ctorigdstport":
policy.HostPort = parsePolicyPort(nextPolicyToken(tokens, index))
case "--dport":
policy.HostPort = parsePolicyPort(nextPolicyToken(tokens, index))
case "-s":
source = nextPolicyToken(tokens, index)
case "--comment", "comment":
marker := nextPolicyToken(tokens, index)
if strings.HasPrefix(marker, "1panel-docker:") {
policy.UUID = strings.TrimPrefix(marker, "1panel-docker:")
}
case "-j":
action = strings.ToLower(nextPolicyToken(tokens, index))
case "meta":
if nextPolicyToken(tokens, index) == "l4proto" {
policy.Protocol = nextPolicyToken(tokens, index+1)
}
case "ct":
if nextPolicyToken(tokens, index) != "original" {
continue
}
switch nextPolicyToken(tokens, index+1) {
case "proto-dst":
policy.HostPort = parsePolicyPort(nextPolicyToken(tokens, index+2))
case "ip", "ip6":
if nextPolicyToken(tokens, index+2) == "daddr" {
policy.HostIP = normalizeObservedHost(nextPolicyToken(tokens, index+3))
}
}
case "ip", "ip6":
switch nextPolicyToken(tokens, index) {
case "saddr":
source = nextPolicyToken(tokens, index+1)
case "daddr":
policy.HostIP = normalizeObservedHost(nextPolicyToken(tokens, index+1))
}
case "tcp", "udp":
if nextPolicyToken(tokens, index) == "dport" {
policy.Protocol = tokens[index]
policy.HostPort = parsePolicyPort(nextPolicyToken(tokens, index+1))
}
case "accept", "drop", "return":
if isCommentValue(tokens, index) {
continue
}
action = tokens[index]
}
}
if action == "" || (action != "accept" && (policy.UUID == "" || policy.Protocol == "" || policy.HostPort == 0)) {
return Policy{}, "", "", fmt.Errorf("incomplete 1Panel Docker guard rule")
}
if action == "accept" && policy.Protocol == "" {
policy.Protocol = "all"
}
return policy, source, action, nil
}
func hasAcceptAction(tokens []string) bool {
for index, token := range tokens {
if token == "-j" && strings.EqualFold(nextPolicyToken(tokens, index), "accept") {
return true
}
if strings.EqualFold(token, "accept") && !isCommentValue(tokens, index) {
return true
}
}
return false
}
func isCommentValue(tokens []string, index int) bool {
if index == 0 {
return false
}
return tokens[index-1] == "comment" || tokens[index-1] == "--comment"
}
func normalizeObservedHost(value string) string {
if prefix, err := netip.ParsePrefix(value); err == nil && prefix.Bits() == prefix.Addr().BitLen() {
return prefix.Addr().String()
}
return value
}
func nextPolicyToken(tokens []string, index int) string {
if index+1 >= len(tokens) {
return ""
}
return tokens[index+1]
}
func parsePolicyPort(value string) uint16 {
port, err := strconv.ParseUint(value, 10, 16)
if err != nil || port == 0 {
return 0
}
return uint16(port)
}
func wildcardHost(family string) string {
if family == FamilyIPv6 {
return "::"
}
return "0.0.0.0"
}
func uniqueSortedStrings(values []string) []string {
seen := make(map[string]struct{}, len(values))
result := make([]string, 0, len(values))
for _, value := range values {
if _, exists := seen[value]; exists {
continue
}
seen[value] = struct{}{}
result = append(result, value)
}
sort.Strings(result)
return result
}
@@ -1,8 +1,11 @@
package docker_guard
import (
"errors"
"fmt"
"os"
"slices"
"strings"
"github.com/1Panel-dev/1Panel/agent/constant"
)
@@ -144,3 +147,22 @@ func ReconcileTarget(backend string, policies []Policy, runtime Runtime) error {
}
return nil
}
const ipv4ForwardingPath = "/proc/sys/net/ipv4/ip_forward"
var ErrIPv4ForwardingDisabled = errors.New("IPv4 forwarding is disabled; set net.ipv4.ip_forward=1 before using Docker's firewall backend")
func CheckIPv4Forwarding() error {
return checkIPv4Forwarding(os.ReadFile)
}
func checkIPv4Forwarding(readFile func(string) ([]byte, error)) error {
value, err := readFile(ipv4ForwardingPath)
if err != nil {
return fmt.Errorf("inspect IPv4 forwarding: %w", err)
}
if strings.TrimSpace(string(value)) != "1" {
return ErrIPv4ForwardingDisabled
}
return nil
}
+13
View File
@@ -91,6 +91,19 @@ type MultiScopeObserver interface {
ObserveScopes(context.Context, []Scope) ([]Snapshot, error)
}
type ObservationSessionFactory interface {
NewObservationSession() Adapter
}
type CreatePlanner interface {
Compile(DesiredChange) (BackendPlan, error)
Applied(ObservedRule)
}
type CreatePlannerFactory interface {
NewCreatePlanner(Snapshot) CreatePlanner
}
type RulePreparer interface {
PrepareRule(FirewallRule) (FirewallRule, error)
}
-25
View File
@@ -1,25 +0,0 @@
package filter
import "fmt"
var ErrDuplicateAdoption = fmt.Errorf("%w: duplicate firewall rules prevent adoption; manually delete duplicate rules and retry", ErrRuleOperation)
func CheckAdoptDuplicates(snapshot Snapshot, requested FirewallRule) error {
count := 0
for _, observed := range snapshot.Rules {
if observed.ParseStatus != ParseStatusSupported {
continue
}
same, err := SameRuleContent(observed.Rule, requested)
if err != nil {
return err
}
if same {
count++
if count > 1 {
return ErrDuplicateAdoption
}
}
}
return nil
}
+37
View File
@@ -150,6 +150,19 @@ func CheckObservedRuleCollisions(snapshot Snapshot, requested FirewallRule, excl
return nil
}
func ObservedRuleCollisionIndex(snapshot Snapshot) (RuleCollisionIndex, error) {
index := make(RuleCollisionIndex, len(snapshot.Rules))
for _, observed := range snapshot.Rules {
if observed.ParseStatus != ParseStatusSupported {
continue
}
if err := index.Add(observed.Rule); err != nil {
return nil, err
}
}
return index, nil
}
func normalizedRuleKey(normalized FirewallRule) (string, error) {
identity := ruleIdentity{
Scope: normalized.Scope.Key(),
@@ -306,3 +319,27 @@ func FindCandidate(candidates []ObservedRule, selected string) (ObservedRule, er
}
return matched[0], nil
}
const ObservedFieldProtocol = "protocol"
// ObservedRuleMatchesExpected compares the semantic fields that the backend
// could actually observe. Partial rules name fields omitted by their native
// listing; those fields are supplied from the expected rule before comparing
// normalized identities.
func ObservedRuleMatchesExpected(observed ObservedRule, expected FirewallRule) bool {
if observed.ParseStatus == ParseStatusOpaque {
return false
}
hydrated := observed.Rule
for _, field := range observed.UncertainFields {
switch field {
case ObservedFieldProtocol:
hydrated.Protocol = expected.Protocol
default:
return false
}
}
gotKey, gotErr := RuleKey(hydrated)
wantKey, wantErr := RuleKey(expected)
return gotErr == nil && wantErr == nil && gotKey == wantKey
}
+37
View File
@@ -304,3 +304,40 @@ func inventoryStateForDesired(desired DesiredRule, match InventoryMatch) Invento
return InventoryStateManaged
}
}
func InventoryPositionRanges(provider Provider, items []InventoryItem) (ipv4, ipv6 PositionRange) {
if provider == ProviderFirewalld {
return PositionRange{Min: -32768, Max: 32767}, PositionRange{Min: -32768, Max: 32767}
}
for _, item := range items {
if item.Observed == nil || item.Observed.Locator.Position == nil {
continue
}
scope := item.Observed.Rule.Scope
if scope.Provider != provider || scope.Direction != DirectionInput {
continue
}
if (provider == ProviderIptables || provider == ProviderNftables) &&
(scope.Table != "filter" || scope.Chain != IptablesInputChain) {
continue
}
bounds := &ipv4
if scope.Family == FamilyIPv6 {
bounds = &ipv6
} else if scope.Family != FamilyIPv4 {
continue
}
position := *item.Observed.Locator.Position
if position < 1 {
continue
}
if bounds.Min == 0 || position < bounds.Min {
bounds.Min = position
}
bounds.Max = max(bounds.Max, position)
if provider != ProviderUFW {
bounds.Min = 1
}
}
return
}
@@ -1,38 +0,0 @@
package filter
func InventoryPositionRanges(provider Provider, items []InventoryItem) (ipv4, ipv6 PositionRange) {
if provider == ProviderFirewalld {
return PositionRange{Min: -32768, Max: 32767}, PositionRange{Min: -32768, Max: 32767}
}
for _, item := range items {
if item.Observed == nil || item.Observed.Locator.Position == nil {
continue
}
scope := item.Observed.Rule.Scope
if scope.Provider != provider || scope.Direction != DirectionInput {
continue
}
if (provider == ProviderIptables || provider == ProviderNftables) &&
(scope.Table != "filter" || scope.Chain != IptablesInputChain) {
continue
}
bounds := &ipv4
if scope.Family == FamilyIPv6 {
bounds = &ipv6
} else if scope.Family != FamilyIPv4 {
continue
}
position := *item.Observed.Locator.Position
if position < 1 {
continue
}
if bounds.Min == 0 || position < bounds.Min {
bounds.Min = position
}
bounds.Max = max(bounds.Max, position)
if provider != ProviderUFW {
bounds.Min = 1
}
}
return
}
+5 -3
View File
@@ -28,9 +28,11 @@ const (
)
type PortWhitelist struct {
Family string `json:"family"`
Port string `json:"port"`
Protocol string `json:"protocol"`
Family string `json:"-"`
Port string `json:"port,omitempty"`
Protocol string `json:"protocol,omitempty"`
Type string `json:"type,omitempty"`
Sources []string `json:"sources"`
}
type Direction string
-25
View File
@@ -1,25 +0,0 @@
package filter
const ObservedFieldProtocol = "protocol"
// ObservedRuleMatchesExpected compares the semantic fields that the backend
// could actually observe. Partial rules name fields omitted by their native
// listing; those fields are supplied from the expected rule before comparing
// normalized identities.
func ObservedRuleMatchesExpected(observed ObservedRule, expected FirewallRule) bool {
if observed.ParseStatus == ParseStatusOpaque {
return false
}
hydrated := observed.Rule
for _, field := range observed.UncertainFields {
switch field {
case ObservedFieldProtocol:
hydrated.Protocol = expected.Protocol
default:
return false
}
}
gotKey, gotErr := RuleKey(hydrated)
wantKey, wantErr := RuleKey(expected)
return gotErr == nil && wantErr == nil && gotKey == wantKey
}
@@ -409,13 +409,17 @@ func nativeCanonical(rule filter.FirewallRule) string {
func missingRuleCommands(snapshot filter.Snapshot, rule filter.FirewallRule) ([]filter.NativeCommand, []filter.NativeCommand) {
commands, rollback := pairedCommands(rule, "add", "remove")
canonical := nativeCanonical(rule)
var runtimeExists, permanentExists bool
for _, observed := range snapshot.Rules {
if observed.Locator.Canonical != nativeCanonical(rule) {
if observed.Locator.Canonical != canonical {
continue
}
runtimeExists = runtimeExists || observed.Persistence == filter.PersistenceStatusConverged || observed.Persistence == filter.PersistenceStatusRuntimeOnly
permanentExists = permanentExists || observed.Persistence == filter.PersistenceStatusConverged || observed.Persistence == filter.PersistenceStatusPermanentOnly
if runtimeExists && permanentExists {
break
}
}
var changes, inverses []filter.NativeCommand
for index, exists := range []bool{runtimeExists, permanentExists} {
@@ -1089,3 +1093,35 @@ func validateSystemCommand(command filter.NativeCommand) error {
}
return fmt.Errorf("%w: firewalld command must target the managed input zone", filter.ErrUnsupportedScope)
}
type createPlanner struct {
adapter *Adapter
snapshot filter.Snapshot
byCanonical map[string][]filter.ObservedRule
}
func (a *Adapter) NewCreatePlanner(snapshot filter.Snapshot) filter.CreatePlanner {
byCanonical := make(map[string][]filter.ObservedRule, len(snapshot.Rules))
for _, observed := range snapshot.Rules {
byCanonical[observed.Locator.Canonical] = append(byCanonical[observed.Locator.Canonical], observed)
}
snapshot.Rules = nil
return &createPlanner{adapter: a, snapshot: snapshot, byCanonical: byCanonical}
}
func (p *createPlanner) Compile(change filter.DesiredChange) (filter.BackendPlan, error) {
if change.Operation != filter.ChangeCreate || change.After == nil {
return filter.BackendPlan{}, filter.ErrInvalidRule
}
rule, err := p.adapter.PrepareRule(*change.After)
if err != nil {
return filter.BackendPlan{}, err
}
snapshot := p.snapshot
snapshot.Rules = p.byCanonical[nativeCanonical(rule)]
return p.adapter.Compile(snapshot, []filter.DesiredChange{change})
}
func (p *createPlanner) Applied(rule filter.ObservedRule) {
p.byCanonical[rule.Locator.Canonical] = []filter.ObservedRule{rule}
}
@@ -185,6 +185,8 @@ func compileBatch(snapshot filter.Snapshot, changes []filter.DesiredChange) (fil
Rules: make([]filter.NativeRulePlan, 0, len(changes)),
}
current := snapshot
current.Rules = make([]filter.ObservedRule, len(snapshot.Rules), len(snapshot.Rules)+len(changes))
copy(current.Rules, snapshot.Rules)
for _, change := range changes {
rulePlan, err := compileChange(current, change)
if err != nil {
@@ -199,6 +201,9 @@ func compileBatch(snapshot filter.Snapshot, changes []filter.DesiredChange) (fil
}
}
if _, err := filter.NewSnapshot(snapshot.Scope, current.Rules); err != nil {
return filter.BackendPlan{}, err
}
applyScript, err := buildRestoreScript(snapshot.Scope, current.Rules)
if err != nil {
return filter.BackendPlan{}, err
@@ -225,7 +230,8 @@ func applyRestoreRulePlan(snapshot filter.Snapshot, plan filter.NativeRulePlan)
return filter.Snapshot{}, fmt.Errorf("%w: batch rule has no target position", filter.ErrInvalidRule)
}
nativePosition := *position
rules := append([]filter.ObservedRule(nil), snapshot.Rules...)
rules := snapshot.Rules
firstChanged := nativePosition - 1
switch plan.Operation {
case filter.ChangeCreate:
if nativePosition < 1 || nativePosition > len(rules)+1 {
@@ -246,6 +252,7 @@ func applyRestoreRulePlan(snapshot filter.Snapshot, plan filter.NativeRulePlan)
return filter.Snapshot{}, fmt.Errorf("%w: mutation has no previous position", filter.ErrInvalidRule)
}
previousPosition := *plan.Previous.Locator.Position
firstChanged = min(firstChanged, previousPosition-1)
if previousPosition < 1 || previousPosition > len(rules) || nativePosition < 1 || nativePosition > len(rules) {
return filter.Snapshot{}, fmt.Errorf("%w: mutation position is out of range", filter.ErrRuleStale)
}
@@ -262,11 +269,12 @@ func applyRestoreRulePlan(snapshot filter.Snapshot, plan filter.NativeRulePlan)
default:
return filter.Snapshot{}, fmt.Errorf("%w: unsupported batch operation %s", filter.ErrInvalidRule, plan.Operation)
}
for index := range rules {
for index := firstChanged; index < len(rules); index++ {
position := index + 1
rules[index].Locator.Position = &position
}
return filter.NewSnapshot(snapshot.Scope, rules)
snapshot.Rules = rules
return snapshot, nil
}
func buildRestoreScript(scope filter.Scope, rules []filter.ObservedRule) (string, error) {
@@ -317,10 +325,15 @@ func (a *Adapter) Verify(ctx context.Context, plan filter.BackendPlan) (filter.V
if err != nil {
return filter.VerifyResult{}, err
}
byMarker := make(map[string][]int, len(snapshot.Rules))
for index, observed := range snapshot.Rules {
byMarker[observed.Marker] = append(byMarker[observed.Marker], index)
}
for _, expected := range plan.Rules {
markerMatches := 0
semanticMatches := 0
for _, observed := range snapshot.Rules {
for _, index := range byMarker[expected.Expected.Marker] {
observed := snapshot.Rules[index]
if observed.Marker != "" && observed.Marker == expected.Expected.Marker {
markerMatches++
want, wantErr := filter.RuleKey(expected.Expected.Rule)
@@ -335,7 +348,8 @@ func (a *Adapter) Verify(ctx context.Context, plan filter.BackendPlan) (filter.V
positionMatches := true
if requiresPositionMatch {
positionMatches = false
for _, observed := range snapshot.Rules {
for _, index := range byMarker[expected.Expected.Marker] {
observed := snapshot.Rules[index]
if observed.Marker == expected.Expected.Marker && observed.Locator.Position != nil &&
expected.Expected.Locator.Position != nil && *observed.Locator.Position == *expected.Expected.Locator.Position {
positionMatches = true
@@ -686,14 +700,8 @@ func (systemBackend) CheckMultiport(ctx context.Context, family filter.Family) e
}
func (systemBackend) ListChain(ctx context.Context, scope filter.Scope) (string, error) {
output, err := native.ReadTable(ctx, scope.Table, scope.Family == filter.FamilyIPv6)
if err != nil {
return "", err
}
if !containsChainDeclaration(output, scope.Chain) {
return "", fmt.Errorf("%w: iptables %s chain %s is not initialized", filter.ErrProviderUnavailable, scope.Family, scope.Chain)
}
return output, nil
output, err := (systemBackend{}).ListTable(ctx, scope)
return chainOutput(scope, output, err)
}
func containsChainDeclaration(output, chain string) bool {
@@ -909,7 +917,7 @@ func parseRule(scope filter.Scope, raw string, position int) filter.ObservedRule
}
return filter.ObservedRule{
Rule: normalized, Locator: locator, Marker: marker, ParseStatus: filter.ParseStatusSupported, Raw: raw,
Protected: isProtectedRule(scope, normalized, comment),
Protected: filter.IsBuiltinProtectedRule(normalized),
}
}
@@ -920,29 +928,6 @@ func isDefaultRejectWith(family filter.Family, value string) bool {
return value == "icmp-port-unreachable"
}
func isProtectedRule(scope filter.Scope, rule filter.FirewallRule, comment string) bool {
if scope.Chain == native.BasicBeforeChain || scope.Chain == native.BasicAfterChain {
return true
}
if rule.Action == filter.ActionAccept && rule.Interface == "lo" {
return true
}
if rule.Action == filter.ActionAccept {
states := make(map[string]struct{}, len(rule.ConnectionStates))
for _, state := range rule.ConnectionStates {
states[state] = struct{}{}
}
if _, established := states["established"]; established {
return true
}
}
if scope.Chain == native.BasicAfterChain && rule.Action == filter.ActionDrop &&
rule.SourceAddress == "" && rule.DestinationAddress == "" && rule.SourcePort == "" && rule.DestinationPort == "" {
return true
}
return strings.Contains(strings.ToLower(comment), "whitelist")
}
func takeValue(args []string, index *int, target *string) bool {
if *index+1 >= len(args) {
return false
@@ -951,3 +936,55 @@ func takeValue(args []string, index *int, target *string) bool {
*target = args[*index]
return true
}
type tableReader interface {
ListTable(context.Context, filter.Scope) (string, error)
}
type tableRead struct {
output string
err error
}
type tableObservationReader struct {
reader tableReader
tables map[string]tableRead
}
func (r *tableObservationReader) ListChain(ctx context.Context, scope filter.Scope) (string, error) {
if err := ctx.Err(); err != nil {
return "", err
}
key := string(scope.Family) + ":" + scope.Table
read, exists := r.tables[key]
if !exists {
read.output, read.err = r.reader.ListTable(ctx, scope)
r.tables[key] = read
}
return chainOutput(scope, read.output, read.err)
}
func (a *Adapter) NewObservationSession() filter.Adapter {
reader, ok := a.reader.(tableReader)
if !ok {
return a
}
return &Adapter{
reader: &tableObservationReader{reader: reader, tables: make(map[string]tableRead)},
writer: a.writer, checker: a.checker,
}
}
func (systemBackend) ListTable(ctx context.Context, scope filter.Scope) (string, error) {
return native.ReadTable(ctx, scope.Table, scope.Family == filter.FamilyIPv6)
}
func chainOutput(scope filter.Scope, output string, err error) (string, error) {
if err != nil {
return "", err
}
if !containsChainDeclaration(output, scope.Chain) {
return "", fmt.Errorf("%w: iptables %s chain %s is not initialized", filter.ErrProviderUnavailable, scope.Family, scope.Chain)
}
return output, nil
}
@@ -75,6 +75,8 @@ func (a *Adapter) Compile(snapshot filter.Snapshot, changes []filter.DesiredChan
operation := changes[0].Operation
current := snapshot
current.Rules = make([]filter.ObservedRule, len(snapshot.Rules), len(snapshot.Rules)+len(changes))
copy(current.Rules, snapshot.Rules)
plan := filter.BackendPlan{
Provider: filter.ProviderNftables, Scope: snapshot.Scope, SnapshotRevision: snapshot.Revision,
Rules: make([]filter.NativeRulePlan, 0, len(changes)),
@@ -90,10 +92,10 @@ func (a *Adapter) Compile(snapshot filter.Snapshot, changes []filter.DesiredChan
plan.Rules = append(plan.Rules, filter.NativeRulePlan{
RuleUUID: ruleUUID(change), Operation: change.Operation, Previous: previous, Expected: expected,
})
current, err = filter.NewSnapshot(snapshot.Scope, rules)
if err != nil {
return filter.BackendPlan{}, err
}
current.Rules = rules
}
if _, err := filter.NewSnapshot(snapshot.Scope, current.Rules); err != nil {
return filter.BackendPlan{}, err
}
applyCommand, err := rebuildCommand(snapshot.Scope, current.Rules)
if err != nil {
@@ -150,9 +152,14 @@ func (a *Adapter) Verify(ctx context.Context, plan filter.BackendPlan) (filter.V
if err != nil {
return filter.VerifyResult{}, err
}
byMarker := make(map[string][]int, len(snapshot.Rules))
for index, observed := range snapshot.Rules {
byMarker[observed.Marker] = append(byMarker[observed.Marker], index)
}
for _, expected := range plan.Rules {
matches := 0
for _, observed := range snapshot.Rules {
for _, index := range byMarker[expected.Expected.Marker] {
observed := snapshot.Rules[index]
if observed.Marker == expected.Expected.Marker {
if expected.Operation == filter.ChangeDelete {
matches++
@@ -245,7 +252,7 @@ func validateNativeCommand(command filter.NativeCommand) error {
}
func applyChange(snapshot filter.Snapshot, change filter.DesiredChange) ([]filter.ObservedRule, filter.ObservedRule, *filter.ObservedRule, error) {
rules := append([]filter.ObservedRule(nil), snapshot.Rules...)
rules := snapshot.Rules
rule := change.After
if change.Operation == filter.ChangeDelete {
rule = change.Before
@@ -290,6 +297,10 @@ func applyChange(snapshot filter.Snapshot, change filter.DesiredChange) ([]filte
}
if change.Operation == filter.ChangeDelete {
expected := observedRule(normalized, marker, position, "")
for index := position - 1; index < len(rules); index++ {
position := index + 1
rules[index].Locator.Position = &position
}
return rules, expected, previous, nil
}
if target < 1 || target > len(rules)+1 {
@@ -299,7 +310,7 @@ func applyChange(snapshot filter.Snapshot, change filter.DesiredChange) ([]filte
rules = append(rules, filter.ObservedRule{})
copy(rules[target:], rules[target-1:])
rules[target-1] = expected
for index := range rules {
for index := min(position, target) - 1; index < len(rules); index++ {
position := index + 1
rules[index].Locator.Position = &position
}
@@ -416,7 +427,7 @@ func parseRule(scope filter.Scope, raw, handle string, position int) filter.Obse
return filter.ObservedRule{
Rule: filter.FirewallRule{Scope: scope, NativeKind: filter.NativeKindOpaque},
Locator: locator, ParseStatus: filter.ParseStatusOpaque, Raw: raw,
Protected: scope.Chain != filter.IptablesInputChain,
Protected: scope.Chain == filter.BasicAfterChain,
}
}
tokens, err := shellwords.Parse(raw)
@@ -533,7 +544,7 @@ func parseRule(scope filter.Scope, raw, handle string, position int) filter.Obse
if err != nil {
return opaque()
}
return filter.ObservedRule{Rule: normalized, Locator: locator, Marker: marker, ParseStatus: filter.ParseStatusSupported, Raw: raw, Protected: scope.Chain != filter.IptablesInputChain}
return filter.ObservedRule{Rule: normalized, Locator: locator, Marker: marker, ParseStatus: filter.ParseStatusSupported, Raw: raw, Protected: filter.IsBuiltinProtectedRule(normalized)}
}
func parseProtocol(value string) string {
@@ -336,8 +336,12 @@ func compileChange(snapshot filter.Snapshot, change filter.DesiredChange) (filte
return filter.NativeRulePlan{}, fmt.Errorf("%w: create target is out of range", filter.ErrInvalidRule)
}
command := insertCommand(position, normalized, marker)
if change.Append || position == maximumObservedPosition(snapshot)+1 {
if !change.Append && normalized.OrderIndex != nil && position == 1 {
command = filter.NativeCommand{Executable: "ufw", Args: append([]string{"prepend"}, compileRuleArgs(normalized, marker)...)}
} else if change.Append || position == maximumObservedPosition(snapshot)+1 {
command = commentCommand(normalized, marker)
}
if command.Args[0] != "insert" {
plan.Expected.Locator.NativeID = ""
plan.Expected.Locator.Position = nil
}
@@ -366,7 +370,14 @@ func compileChange(snapshot filter.Snapshot, change filter.DesiredChange) (filte
deleteRuleCommand(normalized, marker),
}
case filter.ChangeUpdate, filter.ChangeReorder:
target, targetErr := validateMutationTarget(snapshot, change, normalized, marker, true)
if change.Before == nil {
return filter.NativeRulePlan{}, fmt.Errorf("%w: previous ufw rule is required", filter.ErrInvalidRule)
}
before, err := filter.NormalizeRule(*change.Before)
if err != nil {
return filter.NativeRulePlan{}, err
}
target, targetErr := validateMutationTarget(snapshot, change, before, marker, true)
if targetErr != nil {
return filter.NativeRulePlan{}, targetErr
}
@@ -733,7 +744,7 @@ func validateCommand(command filter.NativeCommand) error {
return nil
}
switch first {
case "insert", "allow", "deny", "reject":
case "insert", "prepend", "allow", "deny", "reject":
return nil
default:
return fmt.Errorf("%w: unsupported ufw command %q", filter.ErrInvalidRule, first)
@@ -0,0 +1,123 @@
package runtime
import (
"context"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
)
const inventoryTTL = 2 * time.Second
var inventoryGeneration atomic.Uint64
func InvalidateInventory() { inventoryGeneration.Add(1) }
type inventoryEntry struct {
snapshots []filter.Snapshot
expires time.Time
generation uint64
}
type inventoryCache struct {
mu sync.Mutex
entries map[string]inventoryEntry
reading map[string]chan struct{}
}
func (e *Engine) ObserveInventory(ctx context.Context, scope filter.Scope, refresh bool) (filter.Snapshot, error) {
snapshots, err := e.ObserveInventoryScopes(ctx, []filter.Scope{scope}, refresh)
if err != nil {
return filter.Snapshot{}, err
}
return snapshots[0], nil
}
func (e *Engine) ObserveInventoryScopes(ctx context.Context, scopes []filter.Scope, refresh bool) ([]filter.Snapshot, error) {
read := func() ([]filter.Snapshot, error) {
if len(scopes) == 1 {
snapshot, err := e.adapter.Observe(ctx, scopes[0])
return []filter.Snapshot{snapshot}, err
}
observer, ok := e.adapter.(filter.MultiScopeObserver)
if !ok {
return nil, filter.ErrAdapterUnavailable
}
return observer.ObserveScopes(ctx, scopes)
}
var snapshots []filter.Snapshot
var err error
if e.Provider() == filter.ProviderFirewalld || e.Provider() == filter.ProviderUFW {
keys := make([]string, len(scopes))
for i, scope := range scopes {
keys[i] = scope.Normalize().Key()
}
snapshots, err = e.inventory.load(ctx, strings.Join(keys, "\n"), refresh, read)
} else {
snapshots, err = read()
}
if err != nil {
return nil, err
}
result := append([]filter.Snapshot(nil), snapshots...)
for i := range result {
result[i].Rules = append([]filter.ObservedRule(nil), snapshots[i].Rules...)
result[i].Notices = append([]filter.ScopeNotice(nil), snapshots[i].Notices...)
if e.policy != nil {
result[i], err = e.policy(ctx, result[i])
if err != nil {
return nil, err
}
}
}
return result, nil
}
func (c *inventoryCache) load(ctx context.Context, key string, refresh bool, read func() ([]filter.Snapshot, error)) ([]filter.Snapshot, error) {
for {
if err := ctx.Err(); err != nil {
return nil, err
}
c.mu.Lock()
generation := inventoryGeneration.Load()
if entry, ok := c.entries[key]; !refresh && ok && entry.generation == generation && time.Now().Before(entry.expires) {
c.mu.Unlock()
return entry.snapshots, nil
}
if done := c.reading[key]; done != nil {
c.mu.Unlock()
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-done:
refresh = false
continue
}
}
if c.reading == nil {
c.reading = make(map[string]chan struct{})
}
done := make(chan struct{})
c.reading[key] = done
delete(c.entries, key)
c.mu.Unlock()
snapshots, err := read()
c.mu.Lock()
if err == nil && generation == inventoryGeneration.Load() {
if len(c.entries) >= 32 {
clear(c.entries)
}
if c.entries == nil {
c.entries = make(map[string]inventoryEntry)
}
c.entries[key] = inventoryEntry{snapshots, time.Now().Add(inventoryTTL), generation}
}
delete(c.reading, key)
close(done)
c.mu.Unlock()
return snapshots, err
}
}
+57 -7
View File
@@ -16,8 +16,9 @@ import (
type SnapshotPolicy func(context.Context, filter.Snapshot) (filter.Snapshot, error)
type Engine struct {
adapter filter.Adapter
policy SnapshotPolicy
adapter filter.Adapter
policy SnapshotPolicy
inventory inventoryCache
}
type Registry map[filter.Provider]*Engine
@@ -69,6 +70,13 @@ func (e *Engine) Observe(ctx context.Context, scope filter.Scope) (filter.Snapsh
return e.policy(ctx, snapshot)
}
func (e *Engine) NewObservationSession() *Engine {
if factory, ok := e.adapter.(filter.ObservationSessionFactory); ok {
return New(factory.NewObservationSession(), e.policy)
}
return e
}
func (e *Engine) ObserveScopes(ctx context.Context, scopes []filter.Scope) ([]filter.Snapshot, error) {
observer, ok := e.adapter.(filter.MultiScopeObserver)
if !ok {
@@ -120,6 +128,8 @@ func (e *Engine) CheckRule(ctx context.Context, rule filter.FirewallRule) error
}
func (e *Engine) AppendUnverified(ctx context.Context, rule filter.FirewallRule, comment string) error {
InvalidateInventory()
defer InvalidateInventory()
appender, ok := e.adapter.(filter.UnverifiedRuleAppender)
if !ok {
return fmt.Errorf("%w: %s does not support unverified rule appends", filter.ErrAdapterUnavailable, e.Provider())
@@ -238,6 +248,8 @@ func (e *Engine) Capabilities(ctx context.Context) (filter.Capabilities, error)
}
func (e *Engine) ExecuteCreate(ctx context.Context, snapshot filter.Snapshot, changes []filter.DesiredChange) error {
InvalidateInventory()
defer InvalidateInventory()
plan, err := e.adapter.Compile(snapshot, changes)
if err != nil {
return err
@@ -249,9 +261,11 @@ func (e *Engine) ExecuteCreate(ctx context.Context, snapshot filter.Snapshot, ch
return err
}
func (e *Engine) ExecuteSync(ctx context.Context, snapshot filter.Snapshot, changes []filter.DesiredChange) error {
func (e *Engine) ExecuteSync(ctx context.Context, snapshot filter.Snapshot, changes []filter.DesiredChange) (filter.ApplyResult, error) {
InvalidateInventory()
defer InvalidateInventory()
if err := ctx.Err(); err != nil {
return err
return filter.ApplyResult{}, err
}
changes = append([]filter.DesiredChange(nil), changes...)
for index := range changes {
@@ -259,14 +273,15 @@ func (e *Engine) ExecuteSync(ctx context.Context, snapshot filter.Snapshot, chan
}
plan, err := e.adapter.Compile(snapshot, changes)
if err != nil {
return err
return filter.ApplyResult{}, err
}
plan.CommandOnly = true
_, err = e.adapter.Apply(ctx, plan)
return err
return e.adapter.Apply(ctx, plan)
}
func (e *Engine) Execute(ctx context.Context, snapshot filter.Snapshot, changes []filter.DesiredChange) (filter.BackendPlan, filter.VerifyResult, error) {
InvalidateInventory()
defer InvalidateInventory()
plan, err := e.adapter.Compile(snapshot, changes)
if err != nil {
return filter.BackendPlan{}, filter.VerifyResult{}, err
@@ -299,6 +314,8 @@ func (e *Engine) Execute(ctx context.Context, snapshot filter.Snapshot, changes
}
func (e *Engine) Rollback(ctx context.Context, plan filter.BackendPlan) error {
InvalidateInventory()
defer InvalidateInventory()
rollbacker, ok := e.adapter.(filter.PlanRollbacker)
if !ok {
return fmt.Errorf("%w: provider %s does not support applied-plan rollback", filter.ErrAdapterUnavailable, e.adapter.Provider())
@@ -352,3 +369,36 @@ func compiledRuleUUID(policyUUID, ruleKey string, scopeOrdinal int) string {
}
return fmt.Sprintf("%s-%d-%s", policyUUID, scopeOrdinal+1, ruleKey)
}
func (e *Engine) NewCreatePlanner(snapshot filter.Snapshot) (filter.CreatePlanner, error) {
factory, ok := e.adapter.(filter.CreatePlannerFactory)
if !ok {
return nil, filter.ErrAdapterUnavailable
}
return factory.NewCreatePlanner(snapshot), nil
}
func (e *Engine) ExecutePlannedCreate(ctx context.Context, planner filter.CreatePlanner, change filter.DesiredChange) (filter.ObservedRule, error) {
if err := ctx.Err(); err != nil {
return filter.ObservedRule{}, err
}
plan, err := planner.Compile(change)
if err != nil {
return filter.ObservedRule{}, err
}
if !plan.CreatesOnly() {
return filter.ObservedRule{}, filter.ErrInvalidRule
}
InvalidateInventory()
defer InvalidateInventory()
plan.CommandOnly = change.CommandOnly
result, err := e.adapter.Apply(ctx, plan)
if err != nil {
return filter.ObservedRule{}, err
}
if len(result.Applied) != 1 {
return filter.ObservedRule{}, filter.ErrVerificationFailed
}
planner.Applied(result.Applied[0])
return result.Applied[0], nil
}
+96 -50
View File
@@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"net/netip"
"slices"
"strconv"
"strings"
)
@@ -19,26 +20,7 @@ var ErrVerificationFailed = errors.New("firewall rule verification failed")
func ProtectSnapshot(snapshot Snapshot, ports []PortWhitelist) (Snapshot, error) {
rules := append([]ObservedRule(nil), snapshot.Rules...)
for index := range rules {
rule := rules[index].Rule
if rules[index].ParseStatus != ParseStatusSupported || rule.Action != ActionAccept ||
rule.SourceAddress != "" || rule.SourcePort != "" || rule.DestinationAddress != "" || rule.Interface != "" {
continue
}
protected := false
for _, protectedPort := range ports {
family := strings.ToLower(strings.TrimSpace(protectedPort.Family))
if family != "" && rule.Scope.Family != FamilyInet && string(rule.Scope.Family) != family {
continue
}
if rule.Protocol != "all" && rule.Protocol != protectedPort.Protocol {
continue
}
if portCovers(rule.DestinationPort, protectedPort.Port) {
protected = true
break
}
}
if protected {
if rules[index].ParseStatus == ParseStatusSupported && RuleMatchesPortWhitelist(rules[index].Rule, ports) {
rules[index].Protected = true
}
}
@@ -55,6 +37,78 @@ func ProtectSnapshot(snapshot Snapshot, ports []PortWhitelist) (Snapshot, error)
return protected, nil
}
func RuleMatchesPortWhitelist(rule FirewallRule, ports []PortWhitelist) bool {
rule, err := NormalizeRule(rule)
if err != nil || rule.Action != ActionAccept || rule.SourcePort != "" || rule.DestinationAddress != "" || rule.Interface != "" || len(rule.ConnectionStates) != 0 {
return false
}
if rule.Scope.Provider == ProviderFirewalld && (rule.NativeKind == NativeKindZonePort ||
(rule.NativeKind == NativeKindRule && rule.Scope.Family == FamilyInet && rule.Priority == nil)) {
return false
}
families := []Family{rule.Scope.Family}
if rule.Scope.Family == FamilyInet {
families = []Family{FamilyIPv4, FamilyIPv6}
}
for _, family := range families {
matched := false
for _, port := range ports {
portFamily := Family(strings.ToLower(strings.TrimSpace(port.Family)))
if portFamily != "" && !familiesOverlap(family, portFamily) {
continue
}
protocol, err := normalizeProtocol(port.Protocol)
if err != nil || rule.Protocol != protocol {
continue
}
portRange, err := normalizePort(port.Port)
if err != nil || rule.DestinationPort != portRange {
continue
}
sources := port.Sources
if len(sources) == 0 {
sources = []string{""}
}
for _, source := range sources {
normalized, err := normalizeAddress(source, family)
if err == nil && normalized == rule.SourceAddress {
matched = true
break
}
}
if matched {
break
}
}
if !matched {
return false
}
}
return true
}
func IsBuiltinProtectedRule(rule FirewallRule) bool {
if rule.Scope.Provider != ProviderIptables && rule.Scope.Provider != ProviderNftables {
return false
}
rule, err := NormalizeRule(rule)
if err != nil || rule.SourceAddress != "" || rule.DestinationAddress != "" || rule.SourcePort != "" || rule.DestinationPort != "" {
return false
}
switch rule.Scope.Chain {
case BasicBeforeChain:
if rule.Action != ActionAccept || rule.Protocol != "all" {
return false
}
return rule.Interface == "lo" && len(rule.ConnectionStates) == 0 ||
rule.Interface == "" && slices.Equal(rule.ConnectionStates, []string{"established", "related"})
case BasicAfterChain:
return rule.Action == ActionDrop && (rule.Protocol == "tcp" || rule.Protocol == "udp") &&
rule.Interface == "" && len(rule.ConnectionStates) == 0
}
return false
}
func GuardMutation(target ObservedRule) error {
if target.Protected {
return ErrProtectedRule
@@ -176,36 +230,6 @@ func addressesOverlap(left, right string) bool {
return leftPrefix.Contains(rightPrefix.Addr()) || rightPrefix.Contains(leftPrefix.Addr())
}
func portCovers(existing, requested string) bool {
if existing == "" {
return true
}
if requested == "" {
return false
}
existingIntervals, err := portIntervals(existing)
if err != nil {
return false
}
requestedIntervals, err := portIntervals(requested)
if err != nil {
return false
}
for _, requestedInterval := range requestedIntervals {
covered := false
for _, existingInterval := range existingIntervals {
if existingInterval[0] <= requestedInterval[0] && existingInterval[1] >= requestedInterval[1] {
covered = true
break
}
}
if !covered {
return false
}
}
return true
}
func portsOverlap(left, right string) bool {
if left == "" || right == "" {
return true
@@ -254,3 +278,25 @@ func portInterval(value string) (int, int, error) {
end, err := strconv.Atoi(parts[1])
return start, end, err
}
var ErrDuplicateAdoption = fmt.Errorf("%w: duplicate firewall rules prevent adoption; manually delete duplicate rules and retry", ErrRuleOperation)
func CheckAdoptDuplicates(snapshot Snapshot, requested FirewallRule) error {
count := 0
for _, observed := range snapshot.Rules {
if observed.ParseStatus != ParseStatusSupported {
continue
}
same, err := SameRuleContent(observed.Rule, requested)
if err != nil {
return err
}
if same {
count++
if count > 1 {
return ErrDuplicateAdoption
}
}
}
return nil
}
@@ -75,6 +75,17 @@ type Manager struct {
runtime RuntimeClient
}
func New(provider string) (Adapter, error) {
switch provider {
case "iptables":
return newIptablesNATAdapter(provider), nil
case "nftables":
return newNftablesAdapter(), nil
default:
return nil, errors.New("unsupported forwarding provider: " + provider)
}
}
func NewManager(adapter Adapter, runtime RuntimeClient) *Manager {
return &Manager{adapter: adapter, runtime: runtime}
}
@@ -1,4 +1,4 @@
package providers
package forwarding
import (
"context"
@@ -13,7 +13,6 @@ import (
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
firewallutil "github.com/1Panel-dev/1Panel/agent/utils/firewall"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/forwarding"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/iptables_helper"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle"
"github.com/mattn/go-shellwords"
@@ -65,7 +64,7 @@ func (systemIptablesBackend) Restore(family, input string) error {
return err
}
executable := commands.Restore4
if family == forwarding.FamilyIPv6 {
if family == FamilyIPv6 {
executable = commands.Restore6
if executable == "" {
return fmt.Errorf("ip6tables-restore command family is unavailable")
@@ -122,12 +121,12 @@ func (l *iptablesNATAdapter) Name() string {
return l.provider
}
func (l *iptablesNATAdapter) List() ([]forwarding.Rule, error) {
func (l *iptablesNATAdapter) List() ([]Rule, error) {
stdout, err := l.backend.RunWithStd(iptables_helper.NatTab, "-S")
if err != nil {
return nil, fmt.Errorf("failed to list NAT rules: %w", err)
}
rules := parseIptablesRules(stdout, forwarding.FamilyIPv4)
rules := parseIptablesRules(stdout, FamilyIPv4)
if !l.backend.IPv6Available() {
return rules, nil
}
@@ -135,26 +134,26 @@ func (l *iptablesNATAdapter) List() ([]forwarding.Rule, error) {
if err != nil {
return nil, fmt.Errorf("failed to list IPv6 NAT rules: %w", err)
}
return append(rules, parseIptablesRules(stdout, forwarding.FamilyIPv6)...), nil
return append(rules, parseIptablesRules(stdout, FamilyIPv6)...), nil
}
func (l *iptablesNATAdapter) Reconcile(rules []forwarding.Rule) error {
byFamily := map[string][]forwarding.Rule{
forwarding.FamilyIPv4: nil,
forwarding.FamilyIPv6: nil,
func (l *iptablesNATAdapter) Reconcile(rules []Rule) error {
byFamily := map[string][]Rule{
FamilyIPv4: nil,
FamilyIPv6: nil,
}
for _, rule := range rules {
normalized, err := forwarding.NormalizeRule(rule)
normalized, err := NormalizeRule(rule)
if err != nil {
return err
}
if normalized.Family == forwarding.FamilyIPv6 && !l.backend.IPv6Available() {
if normalized.Family == FamilyIPv6 && !l.backend.IPv6Available() {
return fmt.Errorf("ip6tables command family is unavailable")
}
byFamily[normalized.Family] = append(byFamily[normalized.Family], normalized)
}
for _, family := range []string{forwarding.FamilyIPv4, forwarding.FamilyIPv6} {
if family == forwarding.FamilyIPv6 && !l.backend.IPv6Available() {
for _, family := range []string{FamilyIPv4, FamilyIPv6} {
if family == FamilyIPv6 && !l.backend.IPv6Available() {
continue
}
if err := l.batchEnsureChains(family); err != nil {
@@ -171,13 +170,13 @@ func (l *iptablesNATAdapter) Reconcile(rules []forwarding.Rule) error {
return nil
}
func buildIptablesForwardRestoreScript(rules []forwarding.Rule) (string, error) {
natRules := [][]string{{"-F", forwarding.ChainPreRouting}, {"-F", forwarding.ChainPostRouting}}
filterRules := [][]string{{"-F", forwarding.ChainForward}}
func buildIptablesForwardRestoreScript(rules []Rule) (string, error) {
natRules := [][]string{{"-F", ChainPreRouting}, {"-F", ChainPostRouting}}
filterRules := [][]string{{"-F", ChainForward}}
for _, rule := range rules {
sourcePort := strings.ReplaceAll(rule.Port, "-", ":")
targetPort := strings.ReplaceAll(rule.TargetPort, "-", ":")
preRouting := []string{"-A", forwarding.ChainPreRouting}
preRouting := []string{"-A", ChainPreRouting}
if rule.Interface != "" {
preRouting = append(preRouting, "-i", rule.Interface)
}
@@ -188,11 +187,11 @@ func buildIptablesForwardRestoreScript(rules []forwarding.Rule) (string, error)
}
natRules = append(natRules,
append(preRouting, "-j", "DNAT", "--to-destination", forwardingTarget(rule)),
[]string{"-A", forwarding.ChainPostRouting, "-d", rule.TargetIP, "-p", rule.Protocol, "--dport", targetPort, "-j", "MASQUERADE"},
[]string{"-A", ChainPostRouting, "-d", rule.TargetIP, "-p", rule.Protocol, "--dport", targetPort, "-j", "MASQUERADE"},
)
filterRules = append(filterRules,
[]string{"-A", forwarding.ChainForward, "-d", rule.TargetIP, "-p", rule.Protocol, "--dport", targetPort, "-j", "ACCEPT"},
[]string{"-A", forwarding.ChainForward, "-s", rule.TargetIP, "-p", rule.Protocol, "--sport", targetPort, "-j", "ACCEPT"},
[]string{"-A", ChainForward, "-d", rule.TargetIP, "-p", rule.Protocol, "--dport", targetPort, "-j", "ACCEPT"},
[]string{"-A", ChainForward, "-s", rule.TargetIP, "-p", rule.Protocol, "--sport", targetPort, "-j", "ACCEPT"},
)
}
var script strings.Builder
@@ -220,15 +219,15 @@ func buildIptablesForwardRestoreScript(rules []forwarding.Rule) (string, error)
return script.String(), nil
}
func forwardingTarget(rule forwarding.Rule) string {
if rule.Family == forwarding.FamilyIPv6 {
func forwardingTarget(rule Rule) string {
if rule.Family == FamilyIPv6 {
return "[" + rule.TargetIP + "]:" + rule.TargetPort
}
return rule.TargetIP + ":" + rule.TargetPort
}
func isRemoteTarget(family, target string) bool {
if family == forwarding.FamilyIPv6 {
if family == FamilyIPv6 {
return target != "" && target != "::1" && target != "localhost"
}
return target != "" && target != "127.0.0.1" && target != "localhost"
@@ -239,8 +238,8 @@ func (l *iptablesNATAdapter) Enable() error {
return err
}
for _, family := range []string{forwarding.FamilyIPv4, forwarding.FamilyIPv6} {
if family == forwarding.FamilyIPv6 && !l.backend.IPv6Available() {
for _, family := range []string{FamilyIPv4, FamilyIPv6} {
if family == FamilyIPv6 && !l.backend.IPv6Available() {
continue
}
if err := l.batchEnsureChains(family); err != nil {
@@ -252,7 +251,7 @@ func (l *iptablesNATAdapter) Enable() error {
func (l *iptablesNATAdapter) batchEnsureChains(family string) error {
list := l.backend.RunWithStd
if family == forwarding.FamilyIPv6 {
if family == FamilyIPv6 {
list = l.backend.RunIPv6WithStd
}
outputs := make(map[string]string, 2)
@@ -274,12 +273,12 @@ func (l *iptablesNATAdapter) batchEnsureChains(family string) error {
}
func (l *iptablesNATAdapter) Cleanup() error {
for _, family := range []string{forwarding.FamilyIPv4, forwarding.FamilyIPv6} {
if family == forwarding.FamilyIPv6 && !l.backend.IPv6Available() {
for _, family := range []string{FamilyIPv4, FamilyIPv6} {
if family == FamilyIPv6 && !l.backend.IPv6Available() {
continue
}
list := l.backend.RunWithStd
if family == forwarding.FamilyIPv6 {
if family == FamilyIPv6 {
list = l.backend.RunIPv6WithStd
}
outputs := make(map[string]string, 2)
@@ -297,8 +296,8 @@ func (l *iptablesNATAdapter) Cleanup() error {
}
}
}
for _, file := range []string{forwarding.ForwardFile, forwarding.PreRoutingFile, forwarding.PostRoutingFile,
iptables_helper.IPv6FileName(forwarding.ForwardFile), iptables_helper.IPv6FileName(forwarding.PreRoutingFile), iptables_helper.IPv6FileName(forwarding.PostRoutingFile)} {
for _, file := range []string{ForwardFile, PreRoutingFile, PostRoutingFile,
iptables_helper.IPv6FileName(ForwardFile), iptables_helper.IPv6FileName(PreRoutingFile), iptables_helper.IPv6FileName(PostRoutingFile)} {
if err := os.Remove(filepath.Join(global.Dir.FirewallDir, file)); err != nil && !errors.Is(err, os.ErrNotExist) {
return err
}
@@ -308,8 +307,8 @@ func (l *iptablesNATAdapter) Cleanup() error {
func buildIptablesForwardLifecycleScript(outputs map[string]string, create bool) string {
items := []struct{ table, parent, chain string }{
{iptables_helper.NatTab, "PREROUTING", forwarding.ChainPreRouting},
{iptables_helper.NatTab, "POSTROUTING", forwarding.ChainPostRouting},
{iptables_helper.NatTab, "PREROUTING", ChainPreRouting},
{iptables_helper.NatTab, "POSTROUTING", ChainPostRouting},
}
byTable := make(map[string][]string, 2)
for _, item := range items {
@@ -335,25 +334,25 @@ func buildIptablesForwardLifecycleScript(outputs map[string]string, create bool)
}
filterOutput := outputs[iptables_helper.FilterTab]
filterChainExists := containsExactLine(filterOutput, "-N "+forwarding.ChainForward)
filterBinding := "-A FORWARD -j " + forwarding.ChainForward
filterChainExists := containsExactLine(filterOutput, "-N "+ChainForward)
filterBinding := "-A FORWARD -j " + ChainForward
filterBindingCount := countExactLines(filterOutput, filterBinding)
if create {
if !filterChainExists {
byTable[iptables_helper.FilterTab] = append(byTable[iptables_helper.FilterTab], "-N "+forwarding.ChainForward)
byTable[iptables_helper.FilterTab] = append(byTable[iptables_helper.FilterTab], "-N "+ChainForward)
}
if !forwardBindingEffective(filterOutput) {
for range filterBindingCount {
byTable[iptables_helper.FilterTab] = append(byTable[iptables_helper.FilterTab], "-D FORWARD -j "+forwarding.ChainForward)
byTable[iptables_helper.FilterTab] = append(byTable[iptables_helper.FilterTab], "-D FORWARD -j "+ChainForward)
}
byTable[iptables_helper.FilterTab] = append(byTable[iptables_helper.FilterTab], canonicalForwardBindingRule(filterOutput))
}
} else {
for range filterBindingCount {
byTable[iptables_helper.FilterTab] = append(byTable[iptables_helper.FilterTab], "-D FORWARD -j "+forwarding.ChainForward)
byTable[iptables_helper.FilterTab] = append(byTable[iptables_helper.FilterTab], "-D FORWARD -j "+ChainForward)
}
if filterChainExists {
byTable[iptables_helper.FilterTab] = append(byTable[iptables_helper.FilterTab], "-F "+forwarding.ChainForward, "-X "+forwarding.ChainForward)
byTable[iptables_helper.FilterTab] = append(byTable[iptables_helper.FilterTab], "-F "+ChainForward, "-X "+ChainForward)
}
}
var script strings.Builder
@@ -382,7 +381,7 @@ func countExactLines(output, want string) int {
}
func forwardBindingEffective(output string) bool {
binding := "-A FORWARD -j " + forwarding.ChainForward
binding := "-A FORWARD -j " + ChainForward
bindingPosition := 0
terminalPosition := 0
position := 0
@@ -405,7 +404,7 @@ func forwardBindingEffective(output string) bool {
}
func canonicalForwardBindingRule(output string) string {
binding := "-A FORWARD -j " + forwarding.ChainForward
binding := "-A FORWARD -j " + ChainForward
position := 1
for _, line := range strings.Split(output, "\n") {
line = strings.TrimSpace(line)
@@ -413,11 +412,11 @@ func canonicalForwardBindingRule(output string) string {
continue
}
if isUnconditionalForwardTerminal(line) {
return fmt.Sprintf("-I FORWARD %d -j %s", position, forwarding.ChainForward)
return fmt.Sprintf("-I FORWARD %d -j %s", position, ChainForward)
}
position++
}
return "-A FORWARD -j " + forwarding.ChainForward
return "-A FORWARD -j " + ChainForward
}
func isUnconditionalForwardTerminal(line string) bool {
@@ -443,14 +442,14 @@ func containsExactLine(output, want string) bool {
}
func (l *iptablesNATAdapter) InitStatus() (bool, bool, error) {
ipv4Init, ipv4Bind, err := l.familyInitStatus(forwarding.FamilyIPv4)
ipv4Init, ipv4Bind, err := l.familyInitStatus(FamilyIPv4)
if err != nil {
return false, false, err
}
if !l.backend.IPv6Available() {
return ipv4Init, ipv4Bind, nil
}
ipv6Init, ipv6Bind, err := l.familyInitStatus(forwarding.FamilyIPv6)
ipv6Init, ipv6Bind, err := l.familyInitStatus(FamilyIPv6)
if err != nil {
return false, false, err
}
@@ -461,7 +460,7 @@ func (l *iptablesNATAdapter) familyInitStatus(family string) (bool, bool, error)
sysctlPath := "/proc/sys/net/ipv4/ip_forward"
label := "IPv4"
list := l.backend.RunWithStd
if family == forwarding.FamilyIPv6 {
if family == FamilyIPv6 {
sysctlPath = "/proc/sys/net/ipv6/conf/all/forwarding"
label = "IPv6"
list = l.backend.RunIPv6WithStd
@@ -476,8 +475,8 @@ func (l *iptablesNATAdapter) familyInitStatus(family string) (bool, bool, error)
return false, false, fmt.Errorf("list %s NAT initialization rules: %w", label, err)
}
natInit, natBind := checkInitAndBind(
[]string{"-N " + forwarding.ChainPreRouting, "-N " + forwarding.ChainPostRouting},
[]string{"-A PREROUTING -j " + forwarding.ChainPreRouting, "-A POSTROUTING -j " + forwarding.ChainPostRouting},
[]string{"-N " + ChainPreRouting, "-N " + ChainPostRouting},
[]string{"-A PREROUTING -j " + ChainPreRouting, "-A POSTROUTING -j " + ChainPostRouting},
strings.Split(natRules, "\n"),
)
if !natInit {
@@ -488,7 +487,7 @@ func (l *iptablesNATAdapter) familyInitStatus(family string) (bool, bool, error)
return false, false, fmt.Errorf("list %s filter initialization rules: %w", label, err)
}
filterInit, _ := checkInitAndBind(
[]string{"-N " + forwarding.ChainForward},
[]string{"-N " + ChainForward},
nil,
strings.Split(filterRules, "\n"),
)
@@ -497,7 +496,7 @@ func (l *iptablesNATAdapter) familyInitStatus(family string) (bool, bool, error)
}
func (l *iptablesNATAdapter) FamilyStatus(family string) (bool, bool, error) {
if family == forwarding.FamilyIPv6 && !l.backend.IPv6Available() {
if family == FamilyIPv6 && !l.backend.IPv6Available() {
return false, false, nil
}
return l.familyInitStatus(family)
@@ -527,8 +526,8 @@ func containsExactRule(lines []string, rule string) bool {
}
func (l *iptablesNATAdapter) Replay() error {
for _, family := range []string{forwarding.FamilyIPv4, forwarding.FamilyIPv6} {
if family == forwarding.FamilyIPv6 && !l.backend.IPv6Available() {
for _, family := range []string{FamilyIPv4, FamilyIPv6} {
if family == FamilyIPv6 && !l.backend.IPv6Available() {
continue
}
if err := l.batchEnsureChains(family); err != nil {
@@ -540,9 +539,9 @@ func (l *iptablesNATAdapter) Replay() error {
chain string
file string
}{
{iptables_helper.FilterTab, forwarding.ChainForward, forwarding.ForwardFile},
{iptables_helper.NatTab, forwarding.ChainPreRouting, forwarding.PreRoutingFile},
{iptables_helper.NatTab, forwarding.ChainPostRouting, forwarding.PostRoutingFile},
{iptables_helper.FilterTab, ChainForward, ForwardFile},
{iptables_helper.NatTab, ChainPreRouting, PreRoutingFile},
{iptables_helper.NatTab, ChainPostRouting, PostRoutingFile},
} {
if err := l.backend.LoadRulesFromFile(item.table, item.chain, item.file); err != nil {
return err
@@ -556,17 +555,17 @@ func (l *iptablesNATAdapter) Replay() error {
return nil
}
func parseIptablesRules(stdout, family string) []forwarding.Rule {
var rules []forwarding.Rule
func parseIptablesRules(stdout, family string) []Rule {
var rules []Rule
num := 0
lines:
for _, line := range strings.Split(stdout, "\n") {
fields, err := shellwords.Parse(line)
if err != nil || len(fields) < 2 || fields[0] != "-A" || fields[1] != forwarding.ChainPreRouting {
if err != nil || len(fields) < 2 || fields[0] != "-A" || fields[1] != ChainPreRouting {
continue
}
num++
rule := forwarding.Rule{Num: strconv.Itoa(num), Family: family}
rule := Rule{Num: strconv.Itoa(num), Family: family}
target := ""
for index := 2; index < len(fields); index++ {
var value *string
@@ -609,7 +608,7 @@ lines:
switch target {
case "REDIRECT":
rule.TargetIP = "127.0.0.1"
if family == forwarding.FamilyIPv6 {
if family == FamilyIPv6 {
rule.TargetIP = "::1"
}
case "DNAT":
@@ -1,4 +1,4 @@
package providers
package forwarding
import (
"encoding/base64"
@@ -12,7 +12,6 @@ import (
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/forwarding"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/nftables_helper"
)
@@ -31,10 +30,10 @@ func newNftablesAdapter() *nftablesAdapter {
func (n *nftablesAdapter) Name() string { return "nftables" }
func (n *nftablesAdapter) List() ([]forwarding.Rule, error) {
rules := make([]forwarding.Rule, 0)
for _, family := range []string{forwarding.FamilyIPv4, forwarding.FamilyIPv6} {
stdout, err := nftables_helper.ReadChain(nftRun, nftTableFamily(family), nftForwardTable, nftForwardChain(forwarding.ChainPreRouting))
func (n *nftablesAdapter) List() ([]Rule, error) {
rules := make([]Rule, 0)
for _, family := range []string{FamilyIPv4, FamilyIPv6} {
stdout, err := nftables_helper.ReadChain(nftRun, nftTableFamily(family), nftForwardTable, nftForwardChain(ChainPreRouting))
if errors.Is(err, nftables_helper.ErrChainNotFound) {
continue
}
@@ -46,7 +45,7 @@ func (n *nftablesAdapter) List() ([]forwarding.Rule, error) {
return rules, nil
}
func (n *nftablesAdapter) Reconcile(rules []forwarding.Rule) error {
func (n *nftablesAdapter) Reconcile(rules []Rule) error {
if err := ensureNftForwardTables(); err != nil {
return fmt.Errorf("initialize nftables forwarding table: %w", err)
}
@@ -69,7 +68,7 @@ func (n *nftablesAdapter) Enable() error {
func (n *nftablesAdapter) Cleanup() error {
commands := make([][]string, 0, 2)
for _, family := range []string{forwarding.FamilyIPv4, forwarding.FamilyIPv6} {
for _, family := range []string{FamilyIPv4, FamilyIPv6} {
tableFamily := nftTableFamily(family)
if _, err := nftRun("list", "table", tableFamily, nftForwardTable); err != nil {
continue
@@ -89,7 +88,7 @@ func (n *nftablesAdapter) Cleanup() error {
}
func (n *nftablesAdapter) InitStatus() (bool, bool, error) {
for _, family := range []string{forwarding.FamilyIPv4, forwarding.FamilyIPv6} {
for _, family := range []string{FamilyIPv4, FamilyIPv6} {
initialized, bound, err := n.FamilyStatus(family)
if err != nil || !initialized || !bound {
return initialized, bound, err
@@ -100,14 +99,14 @@ func (n *nftablesAdapter) InitStatus() (bool, bool, error) {
func (n *nftablesAdapter) FamilyStatus(family string) (bool, bool, error) {
sysctlPath := "/proc/sys/net/ipv4/ip_forward"
if family == forwarding.FamilyIPv6 {
if family == FamilyIPv6 {
sysctlPath = "/proc/sys/net/ipv6/conf/all/forwarding"
}
data, err := n.system.ReadFile(sysctlPath)
if err != nil {
return false, false, fmt.Errorf("read %s forwarding status: %w", family, err)
}
for _, chain := range []string{forwarding.ChainPreRouting, forwarding.ChainPostRouting, forwarding.ChainForward} {
for _, chain := range []string{ChainPreRouting, ChainPostRouting, ChainForward} {
if _, err := nftRun("list", "chain", nftTableFamily(family), nftForwardTable, nftForwardChain(chain)); err != nil {
return false, false, nil
}
@@ -123,7 +122,7 @@ func (n *nftablesAdapter) Replay() error {
return err
}
allPresent := true
for _, family := range []string{forwarding.FamilyIPv4, forwarding.FamilyIPv6} {
for _, family := range []string{FamilyIPv4, FamilyIPv6} {
if _, err := nftRun("list", "table", nftTableFamily(family), nftForwardTable); err != nil {
allPresent = false
}
@@ -136,7 +135,7 @@ func (n *nftablesAdapter) Replay() error {
func ensureNftForwardTables() error {
commands := make([][]string, 0, 8)
for _, family := range []string{forwarding.FamilyIPv4, forwarding.FamilyIPv6} {
for _, family := range []string{FamilyIPv4, FamilyIPv6} {
tableFamily := nftTableFamily(family)
tableExists := true
if _, err := nftRun("list", "table", tableFamily, nftForwardTable); err != nil {
@@ -146,9 +145,9 @@ func ensureNftForwardTables() error {
chains := []struct {
name, chainType, hook, priority string
}{
{nftForwardChain(forwarding.ChainPreRouting), "nat", "prerouting", "-100"},
{nftForwardChain(forwarding.ChainPostRouting), "nat", "postrouting", "100"},
{nftForwardChain(forwarding.ChainForward), "filter", "forward", "0"},
{nftForwardChain(ChainPreRouting), "nat", "prerouting", "-100"},
{nftForwardChain(ChainPostRouting), "nat", "postrouting", "100"},
{nftForwardChain(ChainForward), "filter", "forward", "0"},
}
for _, chain := range chains {
if tableExists {
@@ -168,15 +167,15 @@ func ensureNftForwardTables() error {
return nftRunCommands(commands)
}
func rebuildNftForwardCommands(rules []forwarding.Rule) ([][]string, error) {
func rebuildNftForwardCommands(rules []Rule) ([][]string, error) {
commands := make([][]string, 0, 6+len(rules)*4)
for _, family := range []string{forwarding.FamilyIPv4, forwarding.FamilyIPv6} {
for _, chain := range []string{forwarding.ChainPreRouting, forwarding.ChainPostRouting, forwarding.ChainForward} {
for _, family := range []string{FamilyIPv4, FamilyIPv6} {
for _, chain := range []string{ChainPreRouting, ChainPostRouting, ChainForward} {
commands = append(commands, []string{"flush", "chain", nftTableFamily(family), nftForwardTable, nftForwardChain(chain)})
}
}
for _, rule := range rules {
normalized, err := forwarding.NormalizeRule(rule)
normalized, err := NormalizeRule(rule)
if err != nil {
return nil, err
}
@@ -189,18 +188,18 @@ func rebuildNftForwardCommands(rules []forwarding.Rule) ([][]string, error) {
interfaceMatch = append(interfaceMatch, "iifname", strconv.Quote(rule.Interface))
}
if isRemoteTarget(rule.Family, rule.TargetIP) {
preRouting := []string{"add", "rule", tableFamily, nftForwardTable, nftForwardChain(forwarding.ChainPreRouting)}
preRouting := []string{"add", "rule", tableFamily, nftForwardTable, nftForwardChain(ChainPreRouting)}
preRouting = append(preRouting, interfaceMatch...)
preRouting = append(preRouting, "meta", "l4proto", rule.Protocol, rule.Protocol, "dport", rule.Port, "dnat", "to", forwardingTarget(rule), "comment", comment)
commands = append(commands,
preRouting,
[]string{"add", "rule", tableFamily, nftForwardTable, nftForwardChain(forwarding.ChainPostRouting), addressKeyword, "daddr", rule.TargetIP, "meta", "l4proto", rule.Protocol, rule.Protocol, "dport", rule.TargetPort, "masquerade", "comment", comment},
[]string{"add", "rule", tableFamily, nftForwardTable, nftForwardChain(forwarding.ChainForward), addressKeyword, "daddr", rule.TargetIP, "meta", "l4proto", rule.Protocol, rule.Protocol, "dport", rule.TargetPort, "accept", "comment", comment},
[]string{"add", "rule", tableFamily, nftForwardTable, nftForwardChain(forwarding.ChainForward), addressKeyword, "saddr", rule.TargetIP, "meta", "l4proto", rule.Protocol, rule.Protocol, "sport", rule.TargetPort, "accept", "comment", comment},
[]string{"add", "rule", tableFamily, nftForwardTable, nftForwardChain(ChainPostRouting), addressKeyword, "daddr", rule.TargetIP, "meta", "l4proto", rule.Protocol, rule.Protocol, "dport", rule.TargetPort, "masquerade", "comment", comment},
[]string{"add", "rule", tableFamily, nftForwardTable, nftForwardChain(ChainForward), addressKeyword, "daddr", rule.TargetIP, "meta", "l4proto", rule.Protocol, rule.Protocol, "dport", rule.TargetPort, "accept", "comment", comment},
[]string{"add", "rule", tableFamily, nftForwardTable, nftForwardChain(ChainForward), addressKeyword, "saddr", rule.TargetIP, "meta", "l4proto", rule.Protocol, rule.Protocol, "sport", rule.TargetPort, "accept", "comment", comment},
)
continue
}
preRouting := []string{"add", "rule", tableFamily, nftForwardTable, nftForwardChain(forwarding.ChainPreRouting)}
preRouting := []string{"add", "rule", tableFamily, nftForwardTable, nftForwardChain(ChainPreRouting)}
preRouting = append(preRouting, interfaceMatch...)
preRouting = append(preRouting, "meta", "l4proto", rule.Protocol, rule.Protocol, "dport", rule.Port, "redirect", "to", ":"+rule.TargetPort, "comment", comment)
commands = append(commands, preRouting)
@@ -209,22 +208,22 @@ func rebuildNftForwardCommands(rules []forwarding.Rule) ([][]string, error) {
}
func nftTableFamily(family string) string {
if family == forwarding.FamilyIPv6 {
if family == FamilyIPv6 {
return "ip6"
}
return nftForwardFamily
}
func nftAddressKeyword(family string) string {
if family == forwarding.FamilyIPv6 {
if family == FamilyIPv6 {
return "ip6"
}
return "ip"
}
func encodeNftForwardRule(rule forwarding.Rule) string {
func encodeNftForwardRule(rule Rule) string {
family, protocol := "4", "t"
if rule.Family == forwarding.FamilyIPv6 {
if rule.Family == FamilyIPv6 {
family = "6"
}
if rule.Protocol == "udp" {
@@ -236,9 +235,9 @@ func encodeNftForwardRule(rule forwarding.Rule) string {
)
}
func decodeNftForwardRule(value string) (forwarding.Rule, bool) {
func decodeNftForwardRule(value string) (Rule, bool) {
if !strings.HasPrefix(value, nftForwardMarker) {
return forwarding.Rule{}, false
return Rule{}, false
}
value = strings.TrimPrefix(value, nftForwardMarker)
if strings.HasPrefix(value, "v2|") {
@@ -247,19 +246,19 @@ func decodeNftForwardRule(value string) (forwarding.Rule, bool) {
return decodeLegacyNftForwardRule(value)
}
func decodeCompactNftForwardRule(value string) (forwarding.Rule, bool) {
func decodeCompactNftForwardRule(value string) (Rule, bool) {
parts := strings.Split(value, "|")
if len(parts) != 7 || parts[0] != "v2" {
return forwarding.Rule{}, false
return Rule{}, false
}
family, protocol := "", ""
switch parts[1] {
case "4":
family = forwarding.FamilyIPv4
family = FamilyIPv4
case "6":
family = forwarding.FamilyIPv6
family = FamilyIPv6
default:
return forwarding.Rule{}, false
return Rule{}, false
}
switch parts[2] {
case "t":
@@ -267,31 +266,31 @@ func decodeCompactNftForwardRule(value string) (forwarding.Rule, bool) {
case "u":
protocol = "udp"
default:
return forwarding.Rule{}, false
return Rule{}, false
}
return forwarding.Rule{
return Rule{
Family: family, Protocol: protocol, Port: parts[3], TargetIP: parts[4], TargetPort: parts[5], Interface: parts[6],
}, true
}
func decodeLegacyNftForwardRule(value string) (forwarding.Rule, bool) {
func decodeLegacyNftForwardRule(value string) (Rule, bool) {
parts := strings.Split(value, ".")
if len(parts) != 6 {
return forwarding.Rule{}, false
return Rule{}, false
}
decoded := make([]string, len(parts))
for index, part := range parts {
data, err := base64.RawURLEncoding.DecodeString(part)
if err != nil {
return forwarding.Rule{}, false
return Rule{}, false
}
decoded[index] = string(data)
}
return forwarding.Rule{Family: decoded[0], Protocol: decoded[1], Port: decoded[2], TargetIP: decoded[3], TargetPort: decoded[4], Interface: decoded[5]}, true
return Rule{Family: decoded[0], Protocol: decoded[1], Port: decoded[2], TargetIP: decoded[3], TargetPort: decoded[4], Interface: decoded[5]}, true
}
func parseNftForwardRules(stdout string) []forwarding.Rule {
result := make([]forwarding.Rule, 0)
func parseNftForwardRules(stdout string) []Rule {
result := make([]Rule, 0)
for _, line := range strings.Split(stdout, "\n") {
commentStart := strings.Index(line, `comment "`+nftForwardMarker)
handleStart := strings.LastIndex(line, "# handle ")
@@ -1,18 +0,0 @@
package providers
import (
"errors"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/forwarding"
)
func New(provider string) (forwarding.Adapter, error) {
switch provider {
case "iptables":
return newIptablesNATAdapter(provider), nil
case "nftables":
return newNftablesAdapter(), nil
default:
return nil, errors.New("unsupported forwarding provider: " + provider)
}
}
@@ -1,4 +1,4 @@
package providers
package forwarding
import (
"errors"
@@ -145,3 +145,22 @@ func CheckChainExist(tab, chain string) (bool, error) {
}
return false, nil
}
func ReadTable(ctx context.Context, table string, ipv6 bool) (string, error) {
commands, err := lifecycle.ResolveIptablesCommands()
if err != nil {
return "", err
}
executable := commands.IPv4
if ipv6 {
if !commands.IPv6Available() {
return "", fmt.Errorf("%w: ip6tables/ip6tables-restore are not installed", filter.ErrFamilyUnavailable)
}
executable = commands.IPv6
}
output, err := runTables(ctx, executable, table, false, true, "-S")
if err != nil && ipv6 && (strings.Contains(err.Error(), "Address family not supported") || strings.Contains(err.Error(), "Protocol not supported")) {
return output, fmt.Errorf("%w: %v", filter.ErrFamilyUnavailable, err)
}
return output, err
}
@@ -4,62 +4,10 @@ import (
"fmt"
"strings"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
)
type FilterRules struct {
ID uint `json:"id"`
Chain string `json:"chain"`
Protocol string `json:"protocol"`
SrcPort string `json:"srcPort"`
DstPort string `json:"dstPort"`
SrcIP string `json:"srcIP"`
DstIP string `json:"dstIP"`
Strategy string `json:"strategy"`
Description string `json:"description"`
}
func readFilterRulesByChain(chain string, run func(string, ...string) (string, error)) ([]FilterRules, error) {
var rules []FilterRules
if cmd.CheckIllegal(chain) {
return rules, buserr.New("ErrCmdIllegal")
}
stdout, err := run(FilterTab, "-nL", chain)
if err != nil {
return rules, fmt.Errorf("load filter fules by chain %s failed, %v", chain, err)
}
lines := strings.Split(stdout, "\n")
for i := 0; i < len(lines); i++ {
fields := strings.Fields(lines[i])
if len(fields) > 2 && strings.Contains(fields[2], ":") {
fields = append(fields, "")
copy(fields[3:], fields[2:])
fields[2] = "--"
}
if len(fields) < 5 {
continue
}
strategy := strings.ToLower(fields[0])
if strategy != "accept" && strategy != "drop" && strategy != "reject" {
continue
}
itemRule := FilterRules{
Chain: chain,
Protocol: loadProtocol(fields[1]),
SrcPort: loadPort("src", fields),
DstPort: loadPort("dst", fields),
SrcIP: loadIP(fields[3]),
DstIP: loadIP(fields[4]),
Strategy: strategy,
}
rules = append(rules, itemRule)
}
return rules, nil
}
func LoadInitStatus(tab string) (bool, bool, error) {
return loadInitStatus(tab, RunWithStd, true)
}
@@ -172,47 +120,3 @@ func checkWithInitAndBind(initRules, bindRules []string, lines []string) (bool,
}
return true, true
}
func loadPort(position string, portStr []string) string {
if len(portStr) < 7 {
return ""
}
var portItem string
if strings.Contains(portStr[6], "spt:") && position == "src" {
portItem = strings.ReplaceAll(portStr[6], "spt:", "")
}
if strings.Contains(portStr[6], "dpt:") && position == "dst" {
portItem = strings.ReplaceAll(portStr[6], "dpt:", "")
}
if strings.Contains(portStr[6], "spts:") && position == "src" {
portItem = strings.ReplaceAll(portStr[6], "spts:", "")
}
if strings.Contains(portStr[6], "dpts:") && position == "dst" {
portItem = strings.ReplaceAll(portStr[6], "dpts:", "")
}
portItem = strings.ReplaceAll(portItem, ":", "-")
return portItem
}
func loadIP(ipStr string) string {
if ipStr == ANYWHERE || ipStr == "0.0.0.0/0" || ipStr == "::/0" {
return ""
}
return ipStr
}
func loadProtocol(protocol string) string {
switch protocol {
case "0":
return "all"
case "1":
return "icmp"
case "6":
return "tcp"
case "17":
return "udp"
default:
return protocol
}
}
+11 -14
View File
@@ -15,17 +15,14 @@ func (m *Manager) EnsureIPv6BaseChains() error {
if err != nil {
return err
}
return EnsureIPv6BaseChains(m.panelPort(), ports)
return EnsureIPv6BaseChains(ports)
}
func EnsureIPv6BaseChains(panelPort string, ports []firewall.PortWhitelist) error {
func EnsureIPv6BaseChains(ports []firewall.PortWhitelist) error {
commands, err := lifecycle.ResolveIptablesCommands()
if err != nil || !commands.IPv6Available() {
return fmt.Errorf("ip6tables and ip6tables-restore are required")
}
if panelPort == "" {
return fmt.Errorf("panel port is required")
}
output, err := RunIPv6WithStd(FilterTab, "-S")
if err != nil {
return err
@@ -33,7 +30,7 @@ func EnsureIPv6BaseChains(panelPort string, ports []firewall.PortWhitelist) erro
if err := ensureBaseChainsFamily(true); err != nil {
return err
}
script, err := buildIPv6BaseInitializationScript(global.Dir.FirewallDir, panelPort, ports, output)
script, err := buildIPv6BaseInitializationScript(global.Dir.FirewallDir, ports, output)
if err != nil {
return err
}
@@ -71,13 +68,13 @@ func BindIPv6BaseChains() error {
return setBaseChainBindings(true, true)
}
func buildIPv6BaseInitializationScript(dir, panelPort string, ports []firewall.PortWhitelist, output string) (string, error) {
func buildIPv6BaseInitializationScript(dir string, ports []firewall.PortWhitelist, output string) (string, error) {
for _, chain := range BasicChains() {
if !containsIptablesRule(output, "-N "+chain) {
return buildBaseChainsRestoreScript(dir, panelPort, true, ports...)
return buildBaseChainsRestoreScript(dir, true, ports...)
}
}
defaults, err := baseDefaultRules(panelPort, ports, constant.FirewallFamilyIPv6)
defaults, err := baseDefaultRules(ports, constant.FirewallFamilyIPv6)
if err != nil {
return "", err
}
@@ -92,15 +89,15 @@ func buildIPv6BaseInitializationScript(dir, panelPort string, ports []firewall.P
return script.String(), nil
}
func baseDefaultRules(panelPort string, ports []firewall.PortWhitelist, family string) ([]string, error) {
ports, err := firewall.NormalizeRequiredPorts(append([]firewall.PortWhitelist{{Port: panelPort, Protocol: "tcp"}}, ports...))
func baseDefaultRules(ports []firewall.PortWhitelist, family string) ([]string, error) {
ports, err := firewall.NormalizeRequiredPorts(ports)
if err != nil {
return nil, err
}
rules := []string{"-A " + BasicBeforeChain + " " + IoRuleIn, "-A " + BasicBeforeChain + " " + EstablishedRule}
for _, port := range ports {
if port.Family == "" || port.Family == family {
rules = append(rules, iptablesPortRuleLine("-A", BasicBeforeChain, port.Protocol, port.Port))
for _, port := range firewall.ExpandPortWhitelist(ports) {
if port.Family == family {
rules = append(rules, iptablesSystemPortRuleLine(port))
}
}
return append(rules, "-A "+BasicAfterChain+" "+DropAllTcp, "-A "+BasicAfterChain+" "+DropAllUdp), nil
+80 -114
View File
@@ -3,9 +3,10 @@ package iptables_helper
import (
"errors"
"fmt"
"net/netip"
"os"
"path/filepath"
"strconv"
"sort"
"strings"
"github.com/1Panel-dev/1Panel/agent/constant"
@@ -13,11 +14,11 @@ import (
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle"
"github.com/mattn/go-shellwords"
)
type Manager struct {
UpdateSetting func(key, value string) error
PanelPort func() string
LoadRequiredPorts func() ([]firewall.PortWhitelist, error)
}
@@ -60,7 +61,7 @@ func (m *Manager) Operate(operation firewall.BaseOperation) error {
func (m *Manager) enableBase(prepare bool) error {
if prepare {
if err := ensureBaseChains(); err != nil {
if err := ensureBaseChainsFamily(false); err != nil {
return err
}
if err := m.initPreRules(); err != nil {
@@ -70,13 +71,16 @@ func (m *Manager) enableBase(prepare bool) error {
return err
}
}
if err := bindBaseChains(); err != nil {
if err := setBaseChainBindings(false, true); err != nil {
return err
}
if prepare {
if err := m.ensureIPv6BaseChains(); err != nil {
return err
}
if err := m.SyncRequiredPorts(true); err != nil {
return err
}
} else if err := BindIPv6BaseChains(); err != nil {
return err
}
@@ -93,10 +97,6 @@ func (m *Manager) disableBase() error {
return m.updateSetting("IptablesStatus", constant.StatusDisable)
}
func ensureBaseChains() error {
return ensureBaseChainsFamily(false)
}
func ensureBaseChainsFamily(ipv6 bool) error {
commands, err := lifecycle.ResolveIptablesCommands()
if err != nil {
@@ -165,10 +165,6 @@ func cleanupBaseChains(ipv6 bool) error {
return nil
}
func bindBaseChains() error {
return setBaseChainBindings(false, true)
}
func setBaseChainBindings(ipv6, bind bool) error {
commands, err := lifecycle.ResolveIptablesCommands()
if err != nil {
@@ -241,19 +237,15 @@ func saveBaseChains() error {
return nil
}
func RestoreBaseChains(panelPort string, requiredPorts []firewall.PortWhitelist) error {
port, err := strconv.Atoi(panelPort)
if err != nil || port < 1 || port > 65535 {
return fmt.Errorf("invalid panel port %q", panelPort)
}
func RestoreBaseChains(requiredPorts []firewall.PortWhitelist) error {
commands, err := lifecycle.ResolveIptablesCommands()
if err != nil {
return err
}
if err := ensureBaseChains(); err != nil {
if err := ensureBaseChainsFamily(false); err != nil {
return err
}
input, err := buildBaseChainsRestoreScript(global.Dir.FirewallDir, panelPort, false)
input, err := buildBaseChainsRestoreScript(global.Dir.FirewallDir, false, requiredPorts...)
if err != nil {
return err
}
@@ -266,7 +258,7 @@ func RestoreBaseChains(panelPort string, requiredPorts []firewall.PortWhitelist)
if err := ensureBaseChainsFamily(true); err != nil {
return err
}
input, err = buildBaseChainsRestoreScript(global.Dir.FirewallDir, panelPort, true, requiredPorts...)
input, err = buildBaseChainsRestoreScript(global.Dir.FirewallDir, true, requiredPorts...)
if err != nil {
return err
}
@@ -276,7 +268,7 @@ func RestoreBaseChains(panelPort string, requiredPorts []firewall.PortWhitelist)
return nil
}
func buildBaseChainsRestoreScript(firewallDir, panelPort string, ipv6 bool, requiredPorts ...firewall.PortWhitelist) (string, error) {
func buildBaseChainsRestoreScript(firewallDir string, ipv6 bool, requiredPorts ...firewall.PortWhitelist) (string, error) {
var script strings.Builder
script.WriteString("*filter\n")
for _, chain := range BasicChains() {
@@ -284,8 +276,6 @@ func buildBaseChainsRestoreScript(firewallDir, panelPort string, ipv6 bool, requ
script.WriteString(chain)
script.WriteByte('\n')
}
panelRule := "-A " + BasicBeforeChain + " -p tcp -m tcp --dport " + panelPort + " -j ACCEPT"
panelRuleFound := false
for _, item := range []struct{ chain, file string }{
{BasicBeforeChain, BasicBeforeFileName},
{BasicChain, BasicFileName},
@@ -308,26 +298,24 @@ func buildBaseChainsRestoreScript(firewallDir, panelPort string, ipv6 bool, requ
if !strings.HasPrefix(line, prefix) || strings.ContainsAny(line, "\r\n") {
continue
}
if line == panelRule {
panelRuleFound = true
}
script.WriteString(line)
script.WriteByte('\n')
}
}
if !panelRuleFound {
script.WriteString(panelRule)
script.WriteByte('\n')
}
family := constant.FirewallFamilyIPv4
if ipv6 {
defaults, err := baseDefaultRules(panelPort, requiredPorts, constant.FirewallFamilyIPv6)
if err != nil {
return "", err
}
for _, rule := range defaults {
if !containsIptablesRule(script.String(), rule) {
script.WriteString(rule + "\n")
family = constant.FirewallFamilyIPv6
}
defaults, err := baseDefaultRules(requiredPorts, family)
if err != nil {
return "", err
}
for _, rule := range defaults {
if !containsIptablesRule(script.String(), rule) {
if strings.HasPrefix(rule, "-A "+BasicBeforeChain+" ") && strings.Contains(rule, " --dport ") {
rule = strings.Replace(rule, "-A "+BasicBeforeChain+" ", "-I "+BasicBeforeChain+" 1 ", 1)
}
script.WriteString(rule + "\n")
}
}
script.WriteString("COMMIT\n")
@@ -386,23 +374,22 @@ func (m *Manager) SyncRequiredPorts(withSave bool) error {
}
func applyRequiredFirewallPortWhiteListRules(portWhiteList []firewall.PortWhitelist, withSave, includeDefaults, ipv6 bool) error {
portWhiteList, err := firewall.NormalizeRequiredPorts(portWhiteList)
ports, err := firewall.NormalizeRequiredPorts(portWhiteList)
if err != nil {
return err
}
run, save := RunWithStd, SaveRulesToFile
rules := firewall.ExpandPortWhitelist(ports)
run := RunWithStd
save := SaveRulesToFile
beforeFile, afterFile := BasicBeforeFileName, BasicAfterFileName
if ipv6 {
run, save = RunIPv6WithStd, SaveIPv6RulesToFile
run = RunIPv6WithStd
save = SaveIPv6RulesToFile
beforeFile, afterFile = IPv6FileName(beforeFile), IPv6FileName(afterFile)
}
beforeRules, err := readFilterRulesByChain(BasicBeforeChain, run)
if err != nil {
return err
}
afterRules, err := readFilterRulesByChain(BasicAfterChain, run)
if err != nil {
return err
family := constant.FirewallFamilyIPv4
if ipv6 {
family = constant.FirewallFamilyIPv6
}
beforeRaw, err := run(FilterTab, "-S", BasicBeforeChain)
if err != nil {
@@ -412,16 +399,16 @@ func applyRequiredFirewallPortWhiteListRules(portWhiteList []firewall.PortWhitel
if err != nil {
return err
}
script := buildRequiredPortsRestoreScript(portWhiteList, beforeRules, afterRules, beforeRaw, afterRaw, includeDefaults)
commands, err := lifecycle.ResolveIptablesCommands()
if err != nil {
return err
}
restore := commands.Restore4
if ipv6 {
restore = commands.Restore6
}
script := buildRequiredPortsRestoreScript(rules, family, beforeRaw, afterRaw, includeDefaults)
if script != "" {
commands, resolveErr := lifecycle.ResolveIptablesCommands()
if resolveErr != nil {
return resolveErr
}
restore := commands.Restore4
if ipv6 {
restore = commands.Restore6
}
if err := restoreRules(restore, script); err != nil {
return fmt.Errorf("batch sync required firewall ports with %s: %w", restore, err)
}
@@ -436,53 +423,24 @@ func applyRequiredFirewallPortWhiteListRules(portWhiteList []firewall.PortWhitel
}
func buildRequiredPortsRestoreScript(
desired []firewall.PortWhitelist,
beforeRules, afterRules []FilterRules,
desired []firewall.SystemPort,
family string,
beforeRaw, afterRaw string,
includeDefaults bool,
) string {
desiredKeys := firewall.PortWhitelistMap(desired)
kept := make(map[string]struct{}, len(desired))
commands := make([]string, 0)
for _, rule := range beforeRules {
if !simpleAcceptedPortRule(rule) {
continue
}
key := firewall.PortWhitelistKey(firewall.PortWhitelist{Protocol: rule.Protocol, Port: rule.DstPort})
if _, wanted := desiredKeys[key]; wanted {
if _, alreadyKept := kept[key]; !alreadyKept {
kept[key] = struct{}{}
continue
}
}
commands = append(commands, iptablesPortRuleLine("-D", BasicBeforeChain, rule.Protocol, rule.DstPort))
}
for _, rule := range afterRules {
if simpleAcceptedPortRule(rule) && rule.Protocol == "udp" {
commands = append(commands, iptablesPortRuleLine("-D", BasicAfterChain, rule.Protocol, rule.DstPort))
var commands []string
for _, line := range []string{"-A " + BasicBeforeChain + " " + IoRuleIn, "-A " + BasicBeforeChain + " " + EstablishedRule} {
if !containsIptablesRule(beforeRaw, line) {
commands = append(commands, line)
}
}
if includeDefaults {
for _, rule := range []string{
"-A " + BasicBeforeChain + " " + IoRuleIn,
"-A " + BasicBeforeChain + " " + EstablishedRule,
} {
count := countIptablesRule(beforeRaw, rule)
for duplicate := 1; duplicate < count; duplicate++ {
commands = append(commands, strings.Replace(rule, "-A ", "-D ", 1))
}
if count == 0 {
commands = append(commands, rule)
}
for _, rule := range desired {
line := iptablesSystemPortRuleLine(rule)
if rule.Family == family && !containsIptablesRule(beforeRaw, line) {
commands = append(commands, strings.Replace(line, "-A "+BasicBeforeChain+" ", "-I "+BasicBeforeChain+" 1 ", 1))
beforeRaw += "\n" + line
}
}
for _, port := range desired {
if _, exists := kept[firewall.PortWhitelistKey(port)]; exists {
continue
}
commands = append(commands, iptablesPortRuleLine("-A", BasicBeforeChain, port.Protocol, port.Port))
}
if includeDefaults {
for _, rule := range []string{DropAllTcp, DropAllUdp} {
line := "-A " + BasicAfterChain + " " + rule
@@ -497,13 +455,12 @@ func buildRequiredPortsRestoreScript(
return "*filter\n" + strings.Join(commands, "\n") + "\nCOMMIT\n"
}
func simpleAcceptedPortRule(rule FilterRules) bool {
return rule.Strategy == "accept" && (rule.Protocol == "tcp" || rule.Protocol == "udp") && rule.DstPort != "" &&
rule.SrcIP == "" && rule.DstIP == "" && rule.SrcPort == ""
}
func iptablesPortRuleLine(operation, chain, protocol, port string) string {
return strings.Join([]string{operation, chain, "-p", protocol, "-m", protocol, "--dport", port, "-j", "ACCEPT"}, " ")
func iptablesSystemPortRuleLine(rule firewall.SystemPort) string {
parts := []string{"-A", BasicBeforeChain}
if rule.SourceAddress != "" {
parts = append(parts, "-s", rule.SourceAddress)
}
return strings.Join(append(parts, "-p", rule.Protocol, "-m", rule.Protocol, "--dport", rule.Port, "-j", "ACCEPT"), " ")
}
func containsIptablesRule(output, rule string) bool {
@@ -512,11 +469,27 @@ func containsIptablesRule(output, rule string) bool {
func countIptablesRule(output, rule string) int {
canonical := func(value string) string {
value = strings.TrimSpace(value)
if strings.Contains(value, " -j ACCEPT") {
return strings.Replace(value, " -j ACCEPT", "", 1) + " -j ACCEPT"
fields, err := shellwords.Parse(value)
if err != nil || len(fields)%2 != 0 {
return strings.TrimSpace(value)
}
return value
var options []string
for index := 0; index < len(fields); index += 2 {
key, value := fields[index], fields[index+1]
if key == "--comment" || key == "-m" && (value == "comment" || value == "tcp" || value == "udp") {
continue
}
if prefix, err := netip.ParsePrefix(value); err == nil {
prefix = prefix.Masked()
value = prefix.String()
if prefix.Bits() == prefix.Addr().BitLen() {
value = prefix.Addr().String()
}
}
options = append(options, key+" "+value)
}
sort.Strings(options)
return strings.Join(options, " ")
}
rule = canonical(rule)
count := 0
@@ -535,13 +508,6 @@ func (m *Manager) updateSetting(key, value string) error {
return nil
}
func (m *Manager) panelPort() string {
if m != nil && m.PanelPort != nil {
return m.PanelPort()
}
return ""
}
func (m *Manager) loadRequiredPorts() ([]firewall.PortWhitelist, error) {
if m != nil && m.LoadRequiredPorts != nil {
return m.LoadRequiredPorts()
@@ -1,29 +0,0 @@
package iptables_helper
import (
"context"
"fmt"
"strings"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle"
)
func ReadTable(ctx context.Context, table string, ipv6 bool) (string, error) {
commands, err := lifecycle.ResolveIptablesCommands()
if err != nil {
return "", err
}
executable := commands.IPv4
if ipv6 {
if !commands.IPv6Available() {
return "", fmt.Errorf("%w: ip6tables/ip6tables-restore are not installed", filter.ErrFamilyUnavailable)
}
executable = commands.IPv6
}
output, err := runTables(ctx, executable, table, false, true, "-S")
if err != nil && ipv6 && (strings.Contains(err.Error(), "Address family not supported") || strings.Contains(err.Error(), "Protocol not supported")) {
return output, fmt.Errorf("%w: %v", filter.ErrFamilyUnavailable, err)
}
return output, err
}
@@ -39,7 +39,7 @@ func (m *Manager) RepairBaseChains() error {
if err != nil {
return nil, err
}
return baseDefaultRules(m.panelPort(), ports, family)
return baseDefaultRules(ports, family)
})
if err != nil {
return fmt.Errorf("prepare %s base chain repair: %w", family, err)
+28 -9
View File
@@ -7,6 +7,7 @@ import (
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/controller"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle/providers"
)
const fail2BanRestoreWithFirewallMarker = "/run/1panel_fail2ban_restore_with_firewall"
@@ -20,7 +21,8 @@ const (
)
type Operator struct {
client Client
client Client
RunAction func(operation, name string, action func() error) error
}
// DockerRestartError reports that the requested firewall operation completed,
@@ -54,26 +56,33 @@ func NewOperator(client Client) *Operator {
return &Operator{client: client}
}
func (o *Operator) runAction(operation, name string, action func() error) error {
if o.RunAction != nil {
return o.RunAction(operation, name, action)
}
return action()
}
func (o *Operator) Operate(operation Operation, withDockerRestart bool, prepareStart func(Client) error) error {
var recoveryErrors []error
switch operation {
case OperationStart:
if err := o.client.Start(); err != nil {
if err := o.runAction("Start", o.client.Name(), o.client.Start); err != nil {
return err
}
if prepareStart != nil {
if err := prepareStart(o.client); err != nil {
if err := o.prepareAfterStart(prepareStart); err != nil {
recoveryErrors = append(recoveryErrors, fmt.Errorf("prepare firewall after start: %w", err))
}
}
case OperationStop:
return o.StopWithPrepare(withDockerRestart, nil)
case OperationRestart:
if err := o.client.Restart(); err != nil {
if err := o.runAction("TaskRestart", o.client.Name(), o.client.Restart); err != nil {
return err
}
if prepareStart != nil {
if err := prepareStart(o.client); err != nil {
if err := o.prepareAfterStart(prepareStart); err != nil {
recoveryErrors = append(recoveryErrors, fmt.Errorf("prepare firewall after restart: %w", err))
}
}
@@ -82,12 +91,12 @@ func (o *Operator) Operate(operation Operation, withDockerRestart bool, prepareS
}
if withDockerRestart {
if err := controller.HandleRestart("docker"); err != nil {
if err := o.runAction("TaskRestart", "Docker", func() error { return controller.HandleRestart("docker") }); err != nil {
recoveryErrors = append(recoveryErrors, &DockerRestartError{Err: err})
}
}
if o.client.Name() == ProviderFirewalld && operation == OperationStart {
if err := restoreFail2BanAfterFirewallStart(); err != nil {
if err := o.runAction("TaskRecover", "Fail2Ban", restoreFail2BanAfterFirewallStart); err != nil {
recoveryErrors = append(recoveryErrors, err)
}
}
@@ -97,6 +106,16 @@ func (o *Operator) Operate(operation Operation, withDockerRestart bool, prepareS
return nil
}
func (o *Operator) prepareAfterStart(prepare func(Client) error) error {
if err := prepare(o.client); err != nil {
return err
}
if o.client.Name() == ProviderFirewalld {
return providers.RemoveFirewalldSSHService()
}
return nil
}
// StopWithPrepare records dependent service state, runs preparation, stops the
// firewall, and optionally restarts Docker in that order.
func (o *Operator) StopWithPrepare(withDockerRestart bool, prepareStop func() error) error {
@@ -110,11 +129,11 @@ func (o *Operator) StopWithPrepare(withDockerRestart bool, prepareStop func() er
return err
}
}
if err := o.client.Stop(); err != nil {
if err := o.runAction("Stop", o.client.Name(), o.client.Stop); err != nil {
return err
}
if withDockerRestart {
if err := controller.HandleRestart("docker"); err != nil {
if err := o.runAction("TaskRestart", "Docker", func() error { return controller.HandleRestart("docker") }); err != nil {
return &DockerRestartError{Err: err}
}
}
@@ -11,6 +11,7 @@ import (
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
"github.com/1Panel-dev/1Panel/agent/utils/controller"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
)
type Firewalld struct{}
@@ -68,6 +69,21 @@ func (f *Firewalld) Start() error {
return nil
}
func RemoveFirewalldSSHService() error {
for _, permanent := range []bool{true, false} {
args := []string{"--zone=" + filter.FirewalldInputZone, "--remove-service=ssh"}
configuration := "runtime"
if permanent {
args = append(args, "--permanent")
configuration = "permanent"
}
if _, err := cmd.NewCommandMgr(cmd.WithEnv("LANGUAGE=en_US:en")).RunWithStdout("firewall-cmd", args...); err != nil {
return fmt.Errorf("remove firewalld SSH service from %s configuration: %w", configuration, err)
}
}
return nil
}
func (f *Firewalld) Stop() error {
if err := controller.HandleStop("firewalld"); err != nil {
return fmt.Errorf("stop the firewall failed, err: %v", err)
+74 -118
View File
@@ -4,8 +4,10 @@ import (
"context"
"errors"
"fmt"
"net/netip"
"os"
"path/filepath"
"slices"
"strconv"
"strings"
@@ -111,12 +113,15 @@ func (m *Manager) ensureBaseChains() error {
return nil
}
func requiredPortCommand(tableFamily string, port firewall.PortWhitelist) []string {
return []string{
"add", "rule", tableFamily, TableName, BasicBeforeChain,
"meta", "l4proto", port.Protocol, port.Protocol, "dport", port.Port,
"accept", "comment", `"` + requiredPortComment + `"`,
func requiredPortCommand(tableFamily string, rule firewall.SystemPort) []string {
command := []string{
"insert", "rule", tableFamily, TableName, BasicBeforeChain,
}
if rule.SourceAddress != "" {
command = append(command, tableFamily, "saddr", rule.SourceAddress)
}
return append(command, "meta", "l4proto", rule.Protocol, rule.Protocol, "dport", rule.Port,
"accept", "comment", `"`+requiredPortComment+`"`)
}
func (m *Manager) initPreRules() error {
@@ -128,16 +133,29 @@ func (m *Manager) initPreRules() error {
if err != nil {
return err
}
commands := make([][]string, 0, 12+len(ports)*2)
rules := firewall.ExpandPortWhitelist(ports)
var commands [][]string
for _, family := range []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6} {
tableFamily := TableFamily(family)
commands = append(commands,
[]string{"flush", "chain", tableFamily, TableName, BasicBeforeChain},
[]string{"add", "rule", tableFamily, TableName, BasicBeforeChain, "iifname", `"lo"`, "accept", "comment", `"Loopback Whitelist"`},
[]string{"add", "rule", tableFamily, TableName, BasicBeforeChain, "ct", "state", "{", "established,related", "}", "accept", "comment", `"ESTABLISHED Whitelist"`},
)
for _, port := range ports {
commands = append(commands, requiredPortCommand(tableFamily, port))
output, _, err := readNftObject(run, "-n", "list", "chain", tableFamily, TableName, BasicBeforeChain)
if err != nil {
return err
}
candidates := [][]string{
{"add", "rule", tableFamily, TableName, BasicBeforeChain, "iifname", `"lo"`, "accept", "comment", `"Loopback Whitelist"`},
{"add", "rule", tableFamily, TableName, BasicBeforeChain, "ct", "state", "{", "established,related", "}", "accept", "comment", `"ESTABLISHED Whitelist"`},
}
for _, rule := range rules {
if rule.Family == string(family) {
candidates = append(candidates, requiredPortCommand(tableFamily, rule))
}
}
for _, command := range candidates {
expression := strings.Join(command[5:], " ")
if !containsRequiredPortRule(output, expression) {
commands = append(commands, command)
output += "\n" + expression
}
}
commands = append(commands,
[]string{"flush", "chain", tableFamily, TableName, BasicAfterChain},
@@ -148,118 +166,56 @@ func (m *Manager) initPreRules() error {
return runBatch(commands...)
}
func (m *Manager) SyncRequiredPorts() error {
ports, err := m.loadRequiredPorts()
if err != nil {
return err
}
ports, err = firewall.NormalizeRequiredPorts(ports)
if err != nil {
return err
}
commands, err := requiredPortSyncCommands(run, ports)
if err != nil {
return err
}
if err := runBatch(commands...); err != nil {
return err
}
return PersistRuleset(context.Background())
}
func requiredPortSyncCommands(run func(...string) (string, error), ports []firewall.PortWhitelist) ([][]string, error) {
commands := make([][]string, 0)
for _, family := range []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6} {
tableFamily := TableFamily(family)
_, exists, err := readNftObject(run, "list", "table", tableFamily, TableName)
if family == filter.FamilyIPv6 && errors.Is(err, filter.ErrFamilyUnavailable) {
continue
func containsRequiredPortRule(output, expression string) bool {
canonical := func(line string) string {
line, _, _ = strings.Cut(line, " comment ")
line, _, _ = strings.Cut(line, " # handle ")
for _, protocol := range []string{"tcp", "udp"} {
line = strings.ReplaceAll(line, "meta l4proto "+protocol+" ", "")
}
if err != nil {
return nil, err
}
if !exists {
continue
}
stdout, err := run("-n", "-a", "list", "chain", tableFamily, TableName, BasicBeforeChain)
if err != nil {
return nil, err
}
existing := requiredPortRules(stdout)
missing, staleHandles := requiredPortChanges(existing, ports)
for _, port := range missing {
commands = append(commands, requiredPortCommand(tableFamily, port))
}
for _, handle := range staleHandles {
commands = append(commands, []string{"delete", "rule", tableFamily, TableName, BasicBeforeChain, "handle", handle})
}
}
return commands, nil
}
type requiredPortRule struct {
Key string
Handle string
}
func requiredPortChanges(existing []requiredPortRule, desiredPorts []firewall.PortWhitelist) ([]firewall.PortWhitelist, []string) {
desired := firewall.PortWhitelistMap(desiredPorts)
existingKeys := make(map[string]struct{}, len(existing))
for _, rule := range existing {
existingKeys[rule.Key] = struct{}{}
}
missing := make([]firewall.PortWhitelist, 0)
for _, port := range desiredPorts {
if _, exists := existingKeys[firewall.PortWhitelistKey(port)]; !exists {
missing = append(missing, port)
}
}
kept := make(map[string]struct{}, len(existing))
staleHandles := make([]string, 0)
for _, rule := range existing {
if _, wanted := desired[rule.Key]; wanted {
if _, alreadyKept := kept[rule.Key]; !alreadyKept {
kept[rule.Key] = struct{}{}
continue
line = strings.NewReplacer("{", "", "}", "", ", ", ",", " ,", ",").Replace(line)
fields := strings.Fields(line)
for index, field := range fields {
if index >= 2 && fields[index-2] == "ct" && fields[index-1] == "state" {
states := strings.Split(field, ",")
for i, state := range states {
value, err := strconv.ParseUint(state, 0, 64)
if err != nil {
continue
}
switch value {
case 1:
states[i] = "invalid"
case 2:
states[i] = "established"
case 4:
states[i] = "related"
case 8:
states[i] = "new"
case 64:
states[i] = "untracked"
}
}
slices.Sort(states)
fields[index] = strings.Join(states, ",")
}
if prefix, err := netip.ParsePrefix(field); err == nil {
prefix = prefix.Masked()
fields[index] = prefix.String()
if prefix.Bits() == prefix.Addr().BitLen() {
fields[index] = prefix.Addr().String()
}
}
}
staleHandles = append(staleHandles, rule.Handle)
return strings.Join(fields, " ")
}
return missing, staleHandles
}
func requiredPortRules(output string) []requiredPortRule {
rules := make([]requiredPortRule, 0)
marker := `comment "` + requiredPortComment + `"`
wanted := canonical(expression)
for _, line := range strings.Split(output, "\n") {
if !strings.Contains(line, marker) {
continue
}
handleIndex := strings.LastIndex(line, "# handle ")
if handleIndex < 0 {
continue
}
handle := strings.TrimSpace(line[handleIndex+len("# handle "):])
if _, err := strconv.ParseUint(handle, 10, 64); err != nil {
continue
}
fields := strings.Fields(line[:handleIndex])
for index := 0; index+2 < len(fields); index++ {
protocol := fields[index]
if (protocol != "tcp" && protocol != "udp") || fields[index+1] != "dport" {
continue
}
port, err := strconv.Atoi(fields[index+2])
if err != nil || port < 1 || port > 65535 {
break
}
rules = append(rules, requiredPortRule{
Key: firewall.PortWhitelistKey(firewall.PortWhitelist{Protocol: protocol, Port: strconv.Itoa(port)}), Handle: handle,
})
break
if canonical(line) == wanted {
return true
}
}
return rules
return false
}
func (m *Manager) updateSetting(key, value string) error {
@@ -1,16 +0,0 @@
package nftables_helper
import "errors"
var ErrChainNotFound = errors.New("nftables chain is not initialized")
func ReadChain(run func(...string) (string, error), family, table, chain string) (string, error) {
output, exists, err := readNftObject(run, "-a", "list", "chain", family, table, chain)
if err != nil {
return "", err
}
if !exists {
return "", ErrChainNotFound
}
return output, nil
}
@@ -2,6 +2,7 @@ package nftables_helper
import (
"context"
"errors"
"fmt"
"os"
"slices"
@@ -184,3 +185,16 @@ func loadFamilyInitStatus(family filter.Family) (bool, bool, error) {
}
return true, true, nil
}
var ErrChainNotFound = errors.New("nftables chain is not initialized")
func ReadChain(run func(...string) (string, error), family, table, chain string) (string, error) {
output, exists, err := readNftObject(run, "-a", "list", "chain", family, table, chain)
if err != nil {
return "", err
}
if !exists {
return "", ErrChainNotFound
}
return output, nil
}
@@ -1,4 +1,4 @@
package ping
package firewall
import (
"fmt"
@@ -10,7 +10,7 @@ import (
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
)
func LoadStatus() string {
func LoadPingStatus() string {
data, err := os.ReadFile("/proc/sys/net/ipv4/icmp_echo_ignore_all")
if err != nil {
return constant.StatusNone
@@ -28,7 +28,7 @@ func LoadStatus() string {
return constant.StatusDisable
}
func UpdateStatus(enable string) error {
func UpdatePingStatus(enable string) error {
const confPath = "/etc/sysctl.conf"
const panelSysctlPath = "/etc/sysctl.d/98-onepanel.conf"
+141 -96
View File
@@ -1,8 +1,8 @@
package firewall
import (
"encoding/json"
"fmt"
"net/netip"
"sort"
"strconv"
"strings"
@@ -13,73 +13,59 @@ import (
type PortWhitelist = filter.PortWhitelist
func ParsePortWhitelist(value string) ([]PortWhitelist, error) {
value = strings.TrimSpace(value)
if value == "" {
return []PortWhitelist{}, nil
}
if strings.HasPrefix(value, "[") {
var rules []PortWhitelist
if err := json.Unmarshal([]byte(value), &rules); err != nil {
return nil, fmt.Errorf("invalid firewall port whitelist JSON: %w", err)
}
return validatePortWhitelist(rules)
}
const (
PortWhitelistTypePanel = "panel"
PortWhitelistTypeSSH = "ssh"
)
items := strings.FieldsFunc(value, func(r rune) bool {
return r == ',' || r == '\n' || r == ';' || r == ' '
})
rules := make([]PortWhitelist, 0, len(items))
for _, item := range items {
item = strings.TrimSpace(item)
if item == "" {
continue
}
parts := strings.Split(item, "/")
rule := PortWhitelist{Family: constant.FirewallFamilyIPv4, Protocol: "tcp"}
switch len(parts) {
case 1:
rule.Port = parts[0]
case 2:
rule.Port, rule.Protocol = parts[0], parts[1]
case 3:
rule.Family, rule.Port, rule.Protocol = parts[0], parts[1], parts[2]
default:
return nil, fmt.Errorf("invalid firewall port whitelist: %s", item)
}
rules = append(rules, rule)
func ValidatePortWhitelist(rules []PortWhitelist) ([]PortWhitelist, error) {
if rules == nil {
return nil, fmt.Errorf("firewall port whitelist must be an array")
}
return validatePortWhitelist(rules)
}
func validatePortWhitelist(rules []PortWhitelist) ([]PortWhitelist, error) {
result := make([]PortWhitelist, 0, len(rules))
exists := make(map[string]struct{}, len(rules))
seen := make(map[string]bool, len(rules))
for _, rule := range rules {
rule.Family = strings.ToLower(strings.TrimSpace(rule.Family))
if rule.Family == "" {
rule.Family = constant.FirewallFamilyIPv4
}
if rule.Family != constant.FirewallFamilyIPv4 && rule.Family != constant.FirewallFamilyIPv6 {
return nil, fmt.Errorf("invalid firewall port whitelist family: %s", rule.Family)
}
rule.Type = strings.ToLower(strings.TrimSpace(rule.Type))
rule.Protocol = strings.ToLower(strings.TrimSpace(rule.Protocol))
if rule.Protocol == "" {
if rule.Type != "" && rule.Protocol == "" {
rule.Protocol = "tcp"
}
if rule.Protocol != "tcp" && rule.Protocol != "udp" {
return nil, fmt.Errorf("invalid firewall port whitelist protocol: %s", rule.Protocol)
}
port, err := normalizeWhitelistPort(rule.Port)
if rule.Type != "" {
if rule.Type != PortWhitelistTypePanel && rule.Type != PortWhitelistTypeSSH {
return nil, fmt.Errorf("invalid firewall port whitelist type: %s", rule.Type)
}
port, err := parseWhitelistPort(rule.Port)
if err != nil {
return nil, err
}
rule.Port = strconv.Itoa(port)
} else {
var err error
rule.Port, err = normalizeWhitelistPort(rule.Port)
if err != nil {
return nil, err
}
}
if len(rule.Sources) == 0 {
return nil, fmt.Errorf("firewall port whitelist requires at least one source")
}
var err error
rule.Sources, err = NormalizeWhitelistSources("", rule.Sources)
if err != nil {
return nil, err
}
rule.Port = port
key := PortWhitelistKey(rule)
if _, ok := exists[key]; ok {
continue
rule.Family = ""
key := rule.Type + "/" + rule.Protocol + "/" + rule.Port
if rule.Type != "" {
key = rule.Type + "/" + rule.Protocol
}
exists[key] = struct{}{}
if seen[key] {
return nil, fmt.Errorf("duplicate firewall port whitelist: %s", key)
}
seen[key] = true
result = append(result, rule)
}
return result, nil
@@ -132,10 +118,10 @@ func NormalizePortWhitelist(items []PortWhitelist) []PortWhitelist {
if item.Port == "" {
continue
}
baseKey := item.Port + "/" + strings.ToLower(strings.TrimSpace(item.Protocol))
baseKey := strings.Join([]string{item.Port, item.Protocol, strings.Join(item.Sources, ",")}, "/")
duplicate := false
for _, current := range ports {
currentBaseKey := current.Port + "/" + strings.ToLower(strings.TrimSpace(current.Protocol))
currentBaseKey := strings.Join([]string{current.Port, current.Protocol, strings.Join(current.Sources, ",")}, "/")
if currentBaseKey == baseKey && (current.Family == "" || current.Family == item.Family) {
duplicate = true
break
@@ -147,7 +133,7 @@ func NormalizePortWhitelist(items []PortWhitelist) []PortWhitelist {
if item.Family == "" {
filtered := ports[:0]
for _, current := range ports {
currentBaseKey := current.Port + "/" + strings.ToLower(strings.TrimSpace(current.Protocol))
currentBaseKey := strings.Join([]string{current.Port, current.Protocol, strings.Join(current.Sources, ",")}, "/")
if currentBaseKey != baseKey {
filtered = append(filtered, current)
}
@@ -159,26 +145,75 @@ func NormalizePortWhitelist(items []PortWhitelist) []PortWhitelist {
return ports
}
func PortWhitelistMap(items []PortWhitelist) map[string]struct{} {
ports := make(map[string]struct{}, len(items))
for _, item := range items {
ports[PortWhitelistKey(item)] = struct{}{}
func NormalizeWhitelistSources(family string, sources []string) ([]string, error) {
result := make([]string, 0, len(sources))
seen := make(map[string]struct{}, len(sources))
for _, source := range sources {
source = strings.TrimSpace(source)
prefix, err := netip.ParsePrefix(source)
if err != nil {
address, err := netip.ParseAddr(source)
if err != nil {
return nil, err
}
prefix = netip.PrefixFrom(address, address.BitLen())
}
sourceFamily := family
if sourceFamily == "" {
sourceFamily = constant.FirewallFamilyIPv6
if prefix.Addr().Unmap().Is4() {
sourceFamily = constant.FirewallFamilyIPv4
}
}
rule, err := filter.NormalizeRule(RuleForSystemPort(filter.ProviderIptables, SystemPort{
Family: sourceFamily, Port: "1", Protocol: "tcp", SourceAddress: source,
}))
if err != nil {
return nil, err
}
if rule.SourceAddress == "" {
rule.SourceAddress = "0.0.0.0/0"
if sourceFamily == constant.FirewallFamilyIPv6 {
rule.SourceAddress = "::/0"
}
}
if _, exists := seen[rule.SourceAddress]; !exists {
seen[rule.SourceAddress] = struct{}{}
result = append(result, rule.SourceAddress)
}
}
return ports
}
func PortWhitelistKey(item PortWhitelist) string {
key := item.Port + "/" + strings.ToLower(strings.TrimSpace(item.Protocol))
if family := strings.ToLower(strings.TrimSpace(item.Family)); family != "" {
return family + "/" + key
}
return key
return result, nil
}
type SystemPort struct {
Family string
Port string
Protocol string
Family string
Port string
Protocol string
SourceAddress string
}
func ExpandPortWhitelist(ports []PortWhitelist) []SystemPort {
result := make([]SystemPort, 0, len(ports))
for _, port := range ports {
sources := port.Sources
if len(sources) == 0 {
sources = []string{"0.0.0.0/0", "::/0"}
}
for _, source := range sources {
family := constant.FirewallFamilyIPv4
if strings.Contains(source, ":") {
family = constant.FirewallFamilyIPv6
}
if port.Family != "" && port.Family != family {
continue
}
if source == "0.0.0.0/0" || source == "::/0" {
source = ""
}
result = append(result, SystemPort{Family: family, Port: port.Port, Protocol: port.Protocol, SourceAddress: source})
}
}
return result
}
func RuleForSystemPort(provider filter.Provider, port SystemPort) filter.FirewallRule {
@@ -203,7 +238,7 @@ func RuleForSystemPort(provider filter.Provider, port SystemPort) filter.Firewal
}
return filter.FirewallRule{
Scope: scope, Protocol: port.Protocol, DestinationPort: port.Port,
Action: filter.ActionAccept,
SourceAddress: port.SourceAddress, Action: filter.ActionAccept,
}
}
@@ -218,7 +253,10 @@ func NormalizeSystemPorts(ports []SystemPort) (map[string]SystemPort, error) {
if family != "" {
family = string(normalized.Scope.Family)
}
item := SystemPort{Family: family, Port: normalized.DestinationPort, Protocol: normalized.Protocol}
item := SystemPort{
Family: family, Port: normalized.DestinationPort,
Protocol: normalized.Protocol, SourceAddress: normalized.SourceAddress,
}
result[SystemPortKey(item)] = item
}
return result, nil
@@ -227,7 +265,10 @@ func NormalizeSystemPorts(ports []SystemPort) (map[string]SystemPort, error) {
func SystemPortKey(port SystemPort) string {
key := LegacySystemPortKey(port)
if family := strings.ToLower(strings.TrimSpace(port.Family)); family != "" {
return family + "/" + key
key = family + "/" + key
}
if source := strings.TrimSpace(port.SourceAddress); source != "" {
key += "/" + source
}
return key
}
@@ -245,26 +286,6 @@ func SortedSystemPortKeys(ports map[string]SystemPort) []string {
return keys
}
func ContainsPort(ports []PortWhitelist, target PortWhitelist) bool {
for _, port := range ports {
familyMatches := port.Family == "" || target.Family == "" || port.Family == target.Family
if familyMatches && port.Port == target.Port && port.Protocol == target.Protocol {
return true
}
}
return false
}
func ExcludePorts(ports, excluded []PortWhitelist) []PortWhitelist {
result := make([]PortWhitelist, 0, len(ports))
for _, port := range ports {
if !ContainsPort(excluded, port) {
result = append(result, port)
}
}
return result
}
func NormalizeRequiredPorts(ports []PortWhitelist) ([]PortWhitelist, error) {
result := make([]PortWhitelist, 0, len(ports))
for _, port := range ports {
@@ -277,7 +298,31 @@ func NormalizeRequiredPorts(ports []PortWhitelist) ([]PortWhitelist, error) {
return nil, fmt.Errorf("invalid required firewall port %q", port.Port)
}
port.Port = strconv.Itoa(portNumber)
if len(port.Sources) > 0 {
port.Sources, err = NormalizeWhitelistSources(port.Family, port.Sources)
if err != nil {
return nil, err
}
}
result = append(result, port)
}
return NormalizePortWhitelist(result), nil
}
func RequiredPortWhitelist(entries []PortWhitelist) ([]PortWhitelist, error) {
result := make([]PortWhitelist, 0, len(entries))
for _, entry := range entries {
if entry.Type == "" {
continue
}
if entry.Port == "" {
return nil, fmt.Errorf("firewall whitelist %s has no stored port", entry.Type)
}
protocol := strings.ToLower(strings.TrimSpace(entry.Protocol))
if protocol == "" {
protocol = "tcp"
}
result = append(result, PortWhitelist{Family: entry.Family, Port: entry.Port, Protocol: protocol, Sources: entry.Sources})
}
return NormalizeRequiredPorts(result)
}
+29 -12
View File
@@ -2,6 +2,7 @@ package sync
import (
"slices"
"strconv"
"strings"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
@@ -14,13 +15,20 @@ func RuleOrder(snapshot filter.Snapshot, ordered []filter.InventoryItem) map[str
}
markers := make([]string, 0, len(ordered))
projected := append([]filter.ObservedRule(nil), snapshot.Rules...)
markerSet := make(map[string]bool, len(ordered))
locations := make(map[string][]int, len(projected))
for index, rule := range projected {
key := locatorBucket(rule.Locator)
locations[key] = append(locations[key], index)
}
for _, item := range ordered {
if item.Desired == nil || item.Desired.Marker == "" {
continue
}
markers = append(markers, item.Desired.Marker)
markerSet[item.Desired.Marker] = true
if item.Observed != nil {
for index := range projected {
for _, index := range locations[locatorBucket(item.Observed.Locator)] {
if filter.SameLocator(projected[index].Locator, item.Observed.Locator) {
projected[index].Marker = item.Desired.Marker
break
@@ -31,7 +39,7 @@ func RuleOrder(snapshot filter.Snapshot, ordered []filter.InventoryItem) map[str
actual := make([]string, 0, len(markers))
presentMarkers := make(map[string]bool, len(markers))
for _, rule := range projected {
if slices.Contains(markers, rule.Marker) {
if markerSet[rule.Marker] {
actual = append(actual, rule.Marker)
presentMarkers[rule.Marker] = true
}
@@ -55,22 +63,24 @@ func InsertionPosition(snapshot filter.Snapshot, markers []string, target string
if snapshot.Scope.Provider == filter.ProviderFirewalld {
return nil
}
positions := make(map[string]int, len(snapshot.Rules))
for _, rule := range snapshot.Rules {
if _, exists := positions[rule.Marker]; !exists && rule.Locator.Position != nil {
positions[rule.Marker] = *rule.Locator.Position
}
}
targetIndex := slices.Index(markers, target)
for index := targetIndex - 1; index >= 0; index-- {
for _, rule := range snapshot.Rules {
if rule.Marker == markers[index] && rule.Locator.Position != nil {
position := int64(*rule.Locator.Position + 1)
return &position
}
if previous, ok := positions[markers[index]]; ok {
position := int64(previous + 1)
return &position
}
}
if targetIndex >= 0 {
for _, marker := range markers[targetIndex+1:] {
for _, rule := range snapshot.Rules {
if rule.Marker == marker && rule.Locator.Position != nil {
position := int64(*rule.Locator.Position)
return &position
}
if next, ok := positions[marker]; ok {
position := int64(next)
return &position
}
}
}
@@ -117,3 +127,10 @@ func ObservedRule(observed filter.ObservedRule) filter.FirewallRule {
}
return rule
}
func locatorBucket(locator filter.Locator) string {
if locator.Position != nil {
return locator.ScopeKey + "\x00position:" + strconv.Itoa(*locator.Position)
}
return locator.ScopeKey + "\x00canonical:" + locator.Canonical
}
-12
View File
@@ -34,18 +34,6 @@ func (s *lexer) scan() flag.Flag {
return s.Latest
}
//func (s *lexer) all() flag.Flags {
// tokens := make([]flag.Flag, 0)
// for {
// v := s.scan()
// if v.Type == flag.EOF || v.Type == -1 {
// break
// }
// tokens = append(tokens, v)
// }
// return tokens
//}
func (s *lexer) getNextFlag() flag.Flag {
if s.inLuaBlock {
s.inLuaBlock = false
+18 -42
View File
@@ -16,12 +16,6 @@ import (
gossh "golang.org/x/crypto/ssh"
)
// Lifetime rules. A session outlives its websocket:
// - the client closes its websocket with 1000 -> the shell is closed at once
// - the websocket drops any other way (browser tab closed, network) -> the
// shell waits graceTimeout for a reattach, then is closed
//
// ponytail: all fixed; promote to settings only if someone asks.
const (
graceTimeout = 30 * time.Minute
revalidateInterval = 60 * time.Second
@@ -31,41 +25,37 @@ const (
pumpInterval = 60 * time.Millisecond
)
// Websocket close codes of the session protocol; the frontend switches on them.
const (
// CloseCodeSessionNotFound: the session is gone or not the caller's; do not retry.
CloseCodeSessionNotFound = 4404
// CloseCodeAttachedElsewhere: a newer websocket took over the session.
CloseCodeSessionNotFound = 4404
CloseCodeAttachedElsewhere = 4409
CloseCodeRevalidate = 4410
)
var errSessionClosed = errors.New("terminal session is closed")
// SessionOptions describes a session that is about to be created.
type SessionOptions struct {
Identity Identity
Kind string
Target string
Title string
HostID uint // 0 = local shell
Cols int
Rows int
InitCmd string
Identity Identity
Kind string
Target string
Title string
Persistent bool
HostID uint // 0 = local shell
Cols int
Rows int
InitCmd string
}
// Info is the client visible snapshot of a session.
type Info struct {
ID string `json:"id"`
Kind string `json:"kind"`
Title string `json:"title"`
Persistent bool `json:"persistent"`
HostID uint `json:"hostId"`
Attached bool `json:"attached"`
CreatedAt time.Time `json:"createdAt"`
DetachedAt time.Time `json:"detachedAt"` // zero while attached
}
// Session owns one shell; a websocket is only a detachable attachment.
type Session struct {
ID string
UserID string
@@ -73,6 +63,7 @@ type Session struct {
Kind string
Target string
Title string
Persistent bool
HostID uint
CreatedAt time.Time
@@ -104,9 +95,6 @@ type sessionBackend interface {
Close() error
}
// Serve drives ws until it ends: it reattaches to sessionID when given, and
// otherwise opens a fresh shell on the client that connect returns. A returned
// error has not been reported to the client yet.
func Serve(ws *websocket.Conn, sessionID string, opts SessionOptions, connect func() (*gossh.Client, error)) error {
return serve(ws, sessionID, opts, func() (*Session, error) {
client, err := connect()
@@ -138,7 +126,7 @@ func ServeCommand(ws *websocket.Conn, sessionID string, opts SessionOptions, con
func serve(ws *websocket.Conn, sessionID string, opts SessionOptions, open func() (*Session, error)) error {
if sessionID != "" {
sess, ok := Lookup(sessionID, opts.Identity)
if ok && sess.Kind == opts.Kind && sess.Target == opts.Target && sess.HostID == opts.HostID {
if ok && sess.Kind == opts.Kind && sess.Target == opts.Target && sess.Persistent == opts.Persistent && sess.HostID == opts.HostID {
att, err := sess.Attach(ws, opts.Cols, opts.Rows)
if err == nil {
att.Run()
@@ -154,7 +142,6 @@ func serve(ws *websocket.Conn, sessionID string, opts SessionOptions, open func(
if err != nil {
return err
}
// no sess.Close() on return: a dirty disconnect leaves the shell alive for a reattach
att, err := sess.Attach(ws, opts.Cols, opts.Rows)
if err != nil {
sess.Close()
@@ -164,7 +151,6 @@ func serve(ws *websocket.Conn, sessionID string, opts SessionOptions, open func(
return nil
}
// Open starts a shell on client and registers the session.
func Open(client *gossh.Client, opts SessionOptions) (*Session, error) {
if err := validateSessionOptions(opts); err != nil {
return nil, err
@@ -197,6 +183,7 @@ func openBackend(backend sessionBackend, ring *ringBuffer, opts SessionOptions)
Kind: opts.Kind,
Target: opts.Target,
Title: opts.Title,
Persistent: opts.Persistent,
HostID: opts.HostID,
CreatedAt: time.Now(),
cols: opts.Cols,
@@ -229,8 +216,6 @@ func validateSessionOptions(opts SessionOptions) error {
return nil
}
// Attach binds ws to the session, kicking any previous attachment, and replays
// the retained output tail before any live output.
func (s *Session) Attach(ws *websocket.Conn, cols, rows int) (*attachment, error) {
if ws == nil {
return nil, errors.New("nil websocket connection")
@@ -299,7 +284,6 @@ func (s *Session) Attach(ws *websocket.Conn, cols, rows int) (*attachment, error
return att, nil
}
// detach unbinds a. A clean detach closes the shell; a dirty one arms the grace timer.
func (s *Session) detach(a *attachment, clean, revalidate bool, cursor uint64) {
s.mu.Lock()
if s.attached != a {
@@ -312,7 +296,8 @@ func (s *Session) detach(a *attachment, clean, revalidate bool, cursor uint64) {
if revalidate {
s.revalidateCursor = cursor
}
if !clean {
shouldClose := clean || (!s.Persistent && !revalidate)
if !shouldClose {
timeout := graceTimeout
if revalidate {
timeout = revalidateGrace
@@ -320,7 +305,7 @@ func (s *Session) detach(a *attachment, clean, revalidate bool, cursor uint64) {
s.grace = time.AfterFunc(timeout, s.Close)
}
s.mu.Unlock()
if clean {
if shouldClose {
s.Close()
}
}
@@ -356,7 +341,6 @@ func (s *Session) doClose() {
}
}
// Info snapshots the session for listing.
func (s *Session) Info() Info {
s.mu.Lock()
defer s.mu.Unlock()
@@ -364,6 +348,7 @@ func (s *Session) Info() Info {
ID: s.ID,
Kind: s.Kind,
Title: s.Title,
Persistent: s.Persistent,
HostID: s.HostID,
Attached: s.attached != nil,
CreatedAt: s.CreatedAt,
@@ -371,7 +356,6 @@ func (s *Session) Info() Info {
}
}
// resize forwards a window size change to the shell.
func (s *Session) resize(cols, rows int) {
s.mu.Lock()
s.cols, s.rows = cols, rows
@@ -381,14 +365,12 @@ func (s *Session) resize(cols, rows int) {
}
}
// writeInput forwards client input to the shell stdin.
func (s *Session) writeInput(data []byte) {
if _, err := s.backend.Write(data); err != nil {
global.LOG.Errorf("ws cmd bytes write to ssh.stdin pipe failed, err: %v", err)
}
}
// ensureAIInterceptor rebuilds the interceptor when AI runtime settings change.
func (s *Session) ensureAIInterceptor() *aiInputInterceptor {
s.mu.Lock()
defer s.mu.Unlock()
@@ -399,7 +381,6 @@ func (s *Session) ensureAIInterceptor() *aiInputInterceptor {
return s.aiInterceptor
}
// pump forwards new ring output to the current attachment.
func (s *Session) pump() {
defer func() {
if r := recover(); r != nil {
@@ -418,8 +399,6 @@ func (s *Session) pump() {
}
}
// flush sends everything the attachment has not seen yet. A client that fell
// behind the ring skips ahead and is told so; output is never queued unbounded.
func (s *Session) flush() {
s.mu.Lock()
att := s.attached
@@ -448,7 +427,6 @@ func (s *Session) flush() {
att.cursor = next
}
// keepaliveLoop probes the shell connection; a failed or stuck probe closes the session.
func (s *Session) keepaliveLoop() {
tick := time.NewTicker(keepaliveInterval)
defer tick.Stop()
@@ -477,7 +455,6 @@ func (s *Session) keepaliveLoop() {
}
}
// waitBackend closes the session once the shell exits, after a last flush.
func (s *Session) waitBackend() {
_ = s.backend.Wait()
s.flush()
@@ -489,7 +466,6 @@ func cmdMessage(data []byte) []byte {
return msg
}
// sendClose writes a close frame with code and reason, best effort.
func sendClose(ws *websocket.Conn, code int, reason string) {
_ = ws.WriteControl(websocket.CloseMessage, websocket.FormatCloseMessage(code, reason), time.Now().Add(time.Second))
}
+9 -8
View File
@@ -9,12 +9,13 @@ type ApiGroup struct {
var ApiGroupApp = new(ApiGroup)
var (
authService = service.NewIAuthService()
backupService = service.NewIBackupService()
settingService = service.NewISettingService()
logService = service.NewILogService()
upgradeService = service.NewIUpgradeService()
groupService = service.NewIGroupService()
commandService = service.NewICommandService()
scriptService = service.NewIScriptService()
runtimeDiagnosticsService = service.NewIRuntimeDiagnosticsService()
authService = service.NewIAuthService()
backupService = service.NewIBackupService()
settingService = service.NewISettingService()
logService = service.NewILogService()
upgradeService = service.NewIUpgradeService()
groupService = service.NewIGroupService()
commandService = service.NewICommandService()
scriptService = service.NewIScriptService()
)
+63
View File
@@ -0,0 +1,63 @@
package v2
import (
"os"
"github.com/1Panel-dev/1Panel/core/app/api/v2/helper"
"github.com/1Panel-dev/1Panel/core/app/dto"
"github.com/gin-gonic/gin"
)
// @Tags RuntimeDiagnostics
// @Summary Load Core runtime diagnostics summary
// @Success 200 {object} dto.RuntimeDiagnosticsSummary
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /core/hosts/diagnostics/summary [get]
func (b *BaseApi) LoadRuntimeDiagnosticsSummary(c *gin.Context) {
data, err := runtimeDiagnosticsService.Summary()
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, data)
}
// @Tags RuntimeDiagnostics
// @Summary Load Core grouped goroutine snapshot
// @Success 200 {object} dto.RuntimeGoroutineSnapshot
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /core/hosts/diagnostics/goroutines [get]
func (b *BaseApi) LoadRuntimeGoroutines(c *gin.Context) {
data, err := runtimeDiagnosticsService.Goroutines()
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, data)
}
// @Tags RuntimeDiagnostics
// @Summary Capture Core runtime profile
// @Param request body dto.RuntimeProfileCreate true "request"
// @Success 200 {file} file
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /core/hosts/diagnostics/profiles [post]
func (b *BaseApi) CreateRuntimeProfile(c *gin.Context) {
var req dto.RuntimeProfileCreate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
profile, err := runtimeDiagnosticsService.CreateProfile(req)
if err != nil {
helper.BadRequest(c, err)
return
}
defer os.Remove(profile.Path)
c.Header("Content-Disposition", `attachment; filename="`+profile.Name+`"`)
c.Header("Content-Type", "application/octet-stream")
c.File(profile.Path)
c.Abort()
}
+50
View File
@@ -17,10 +17,60 @@ import (
"github.com/1Panel-dev/1Panel/core/buserr"
"github.com/1Panel-dev/1Panel/core/constant"
"github.com/1Panel-dev/1Panel/core/global"
"github.com/1Panel-dev/1Panel/core/init/session/psession"
"github.com/1Panel-dev/1Panel/core/utils/common"
"github.com/gin-gonic/gin"
)
// @Tags System Setting
// @Summary Load current user's file download preference
// @Success 200 {object} dto.FileDownloadPreference
// @Router /core/settings/file/download [get]
func (b *BaseApi) GetFileDownloadPreference(c *gin.Context) {
user, ok := fileDownloadPreferenceUser(c)
if !ok {
return
}
preference, err := settingService.GetFileDownloadPreference(user.ID)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, preference)
}
// @Tags System Setting
// @Summary Update current user's file download preference
// @Accept json
// @Param request body dto.FileDownloadPreference true "request"
// @Success 200
// @Router /core/settings/file/download [post]
func (b *BaseApi) UpdateFileDownloadPreference(c *gin.Context) {
user, ok := fileDownloadPreferenceUser(c)
if !ok {
return
}
var req dto.FileDownloadPreference
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := settingService.UpdateFileDownloadPreference(user.ID, req); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
func fileDownloadPreferenceUser(c *gin.Context) (psession.SessionUser, bool) {
// Preferences always belong to the authenticated session, never a request-supplied user ID.
user, err := global.SESSION.Get(c)
if err != nil || user.ID == "" {
helper.BadAuth(c, "ErrNotLogin", buserr.New("ErrNotLogin"))
return psession.SessionUser{}, false
}
return user, true
}
// @Tags System Setting
// @Summary Load system setting info
// @Success 200 {object} dto.SettingInfo
+30
View File
@@ -0,0 +1,30 @@
package dto
import "time"
type RuntimeDiagnosticsSummary struct {
RSS uint64 `json:"rss"`
HeapAlloc uint64 `json:"heapAlloc"`
HeapObjects uint64 `json:"heapObjects"`
Goroutines int `json:"goroutines"`
}
type RuntimeGoroutineGroup struct {
State string `json:"state"`
Top string `json:"top"`
Count int `json:"count"`
Stack []string `json:"stack"`
}
type RuntimeGoroutineSnapshot struct {
Total int `json:"total"`
GroupCount int `json:"groupCount"`
Truncated bool `json:"truncated"`
CapturedAt time.Time `json:"capturedAt"`
Goroutines []RuntimeGoroutineGroup `json:"goroutines"`
}
type RuntimeProfileCreate struct {
Type string `json:"type" validate:"required,oneof=cpu heap goroutine mutex block"`
Duration int `json:"duration" validate:"omitempty,min=5,max=30"`
}
+4
View File
@@ -78,6 +78,10 @@ type SettingBaseInfo struct {
DashboardSimpleNodeVisible string `json:"dashboardSimpleNodeVisible"`
}
type FileDownloadPreference struct {
UseServerFilename bool `json:"useServerFilename"`
}
type SettingUpdate struct {
Key string `json:"key" validate:"required,base_setting_key"`
Value string `json:"value"`
+411
View File
@@ -0,0 +1,411 @@
package service
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"os"
"regexp"
"runtime"
stdpprof "runtime/pprof"
"sort"
"strings"
"sync"
"time"
"github.com/1Panel-dev/1Panel/core/app/dto"
profile "github.com/google/pprof/profile"
"github.com/shirou/gopsutil/v4/process"
)
const (
diagnosticsDefaultDuration = 15
diagnosticsMaxProfileSize = 64 * 1024 * 1024
diagnosticsMaxSnapshotSize = 16 * 1024 * 1024
diagnosticsDetailedGoroutines = 10_000
diagnosticsBlockProfileRate = 1_000_000
diagnosticsMaxGroups = 200
)
var (
runtimeDiagnosticsInstance = &RuntimeDiagnosticsService{}
goroutineHeaderPattern = regexp.MustCompile(`^goroutine \d+ \[([^]]+)\]:$`)
goroutineArgPattern = regexp.MustCompile(`0x[0-9a-fA-F]+`)
errProfileSizeLimit = errors.New("runtime profile exceeds the 64 MiB size limit")
)
type cappedWriter struct {
writer io.Writer
remaining int64
exceeded bool
}
func (w *cappedWriter) Write(data []byte) (int, error) {
if int64(len(data)) <= w.remaining {
n, err := w.writer.Write(data)
w.remaining -= int64(n)
return n, err
}
w.exceeded = true
if w.remaining <= 0 {
return 0, errProfileSizeLimit
}
allowed := int(w.remaining)
n, err := w.writer.Write(data[:allowed])
w.remaining -= int64(n)
if err != nil {
return n, err
}
return n, errProfileSizeLimit
}
type IRuntimeDiagnosticsService interface {
Summary() (dto.RuntimeDiagnosticsSummary, error)
Goroutines() (dto.RuntimeGoroutineSnapshot, error)
CreateProfile(req dto.RuntimeProfileCreate) (RuntimeProfileResult, error)
}
type RuntimeProfileResult struct {
Path string
Name string
}
type RuntimeDiagnosticsService struct {
captureMu sync.Mutex
processMu sync.Mutex
process *process.Process
}
func NewIRuntimeDiagnosticsService() IRuntimeDiagnosticsService {
return runtimeDiagnosticsInstance
}
func (s *RuntimeDiagnosticsService) Summary() (dto.RuntimeDiagnosticsSummary, error) {
rss, err := s.processRSS()
if err != nil {
return dto.RuntimeDiagnosticsSummary{}, err
}
var stats runtime.MemStats
runtime.ReadMemStats(&stats)
return dto.RuntimeDiagnosticsSummary{
RSS: rss,
HeapAlloc: stats.HeapAlloc,
HeapObjects: stats.HeapObjects,
Goroutines: runtime.NumGoroutine(),
}, nil
}
func (s *RuntimeDiagnosticsService) Goroutines() (dto.RuntimeGoroutineSnapshot, error) {
total := runtime.NumGoroutine()
if total > diagnosticsDetailedGoroutines {
groups, truncated := compactGoroutineSnapshot(total)
return dto.RuntimeGoroutineSnapshot{
Total: total, GroupCount: len(groups), Truncated: truncated, CapturedAt: time.Now(), Goroutines: groups,
}, nil
}
var data bytes.Buffer
writer := &cappedWriter{writer: &data, remaining: diagnosticsMaxSnapshotSize}
if err := stdpprof.Lookup("goroutine").WriteTo(writer, 2); err != nil {
groups, truncated := compactGoroutineSnapshot(total)
return dto.RuntimeGoroutineSnapshot{
Total: total, GroupCount: len(groups), Truncated: truncated, CapturedAt: time.Now(), Goroutines: groups,
}, nil
}
groups, truncated := parseGoroutineDump(&data, diagnosticsMaxGroups)
result := dto.RuntimeGoroutineSnapshot{
Total: total,
GroupCount: len(groups),
Truncated: truncated,
CapturedAt: time.Now(),
}
result.Goroutines = groups
return result, nil
}
func (s *RuntimeDiagnosticsService) CreateProfile(req dto.RuntimeProfileCreate) (RuntimeProfileResult, error) {
if !s.captureMu.TryLock() {
return RuntimeProfileResult{}, errors.New("another runtime profile is being captured")
}
defer s.captureMu.Unlock()
return captureRuntimeProfile(req)
}
func captureRuntimeProfile(req dto.RuntimeProfileCreate) (result RuntimeProfileResult, err error) {
duration := req.Duration
if duration == 0 {
duration = diagnosticsDefaultDuration
}
if duration < 5 || duration > 30 {
return result, errors.New("profile duration must be between 5 and 30 seconds")
}
if req.Type == "heap" || req.Type == "goroutine" {
duration = 0
}
name := fmt.Sprintf("core-%s-%s-%ds.pb.gz", req.Type, newRuntimeEventID(), duration)
file, err := os.CreateTemp("", "1panel-core-runtime-profile-*.tmp")
if err != nil {
return result, err
}
writer := &cappedWriter{writer: file, remaining: diagnosticsMaxProfileSize}
removeOnError := true
defer func() {
_ = file.Close()
if removeOnError {
_ = os.Remove(file.Name())
}
}()
switch req.Type {
case "cpu":
if err = stdpprof.StartCPUProfile(writer); err != nil {
return result, err
}
time.Sleep(time.Duration(duration) * time.Second)
stdpprof.StopCPUProfile()
case "heap":
err = stdpprof.Lookup("heap").WriteTo(writer, 0)
case "goroutine":
err = stdpprof.Lookup("goroutine").WriteTo(writer, 0)
case "mutex":
err = captureWindowedRuntimeProfile("mutex", duration, writer, func() func() {
previous := runtime.SetMutexProfileFraction(5)
return func() { runtime.SetMutexProfileFraction(previous) }
})
case "block":
err = captureWindowedRuntimeProfile("block", duration, writer, func() func() {
runtime.SetBlockProfileRate(diagnosticsBlockProfileRate)
return func() { runtime.SetBlockProfileRate(0) }
})
default:
return result, errors.New("unsupported runtime profile type")
}
if err != nil {
return result, err
}
if writer.exceeded {
return result, errProfileSizeLimit
}
if err = file.Close(); err != nil {
return result, err
}
removeOnError = false
return RuntimeProfileResult{Path: file.Name(), Name: name}, nil
}
func captureWindowedRuntimeProfile(name string, duration int, writer io.Writer, enable func() func()) error {
restore := enable()
sampling := true
defer func() {
if sampling {
restore()
}
}()
before, err := readRuntimeProfile(name)
if err != nil {
return err
}
startedAt := time.Now()
time.Sleep(time.Duration(duration) * time.Second)
restore()
sampling = false
after, err := readRuntimeProfile(name)
if err != nil {
return err
}
delta, err := diffRuntimeProfiles(before, after, startedAt, time.Duration(duration)*time.Second)
if err != nil {
return err
}
return delta.Write(writer)
}
func readRuntimeProfile(name string) (*profile.Profile, error) {
var data bytes.Buffer
writer := &cappedWriter{writer: &data, remaining: diagnosticsMaxProfileSize}
if err := stdpprof.Lookup(name).WriteTo(writer, 0); err != nil {
return nil, err
}
if writer.exceeded {
return nil, errProfileSizeLimit
}
return profile.Parse(&data)
}
func diffRuntimeProfiles(before, after *profile.Profile, startedAt time.Time, duration time.Duration) (*profile.Profile, error) {
baseline := before.Copy()
baseline.Scale(-1)
delta, err := profile.Merge([]*profile.Profile{after, baseline})
if err != nil {
return nil, err
}
delta.TimeNanos = startedAt.UnixNano()
delta.DurationNanos = duration.Nanoseconds()
return delta, nil
}
func (s *RuntimeDiagnosticsService) processRSS() (uint64, error) {
s.processMu.Lock()
defer s.processMu.Unlock()
if s.process == nil {
proc, err := process.NewProcess(int32(os.Getpid()))
if err != nil {
return 0, err
}
s.process = proc
}
memoryInfo, err := s.process.MemoryInfo()
if err != nil {
return 0, err
}
if memoryInfo == nil {
return 0, errors.New("process memory information is unavailable")
}
return memoryInfo.RSS, nil
}
func newRuntimeEventID() string {
return time.Now().Format("20060102-150405.000000000")
}
func parseGoroutineDump(reader io.Reader, maxGroups int) ([]dto.RuntimeGoroutineGroup, bool) {
type groupValue struct {
state string
top string
stack []string
count int
}
groups := make(map[string]*groupValue)
truncated := false
scanner := bufio.NewScanner(reader)
scanner.Buffer(make([]byte, 64*1024), 4*1024*1024)
var block []string
flush := func() {
if len(block) == 0 {
return
}
matches := goroutineHeaderPattern.FindStringSubmatch(block[0])
state := "unknown"
if len(matches) == 2 {
state = matches[1]
}
stack := append([]string(nil), block[1:]...)
if len(stack) > 40 {
stack = stack[:40]
}
top := "runtime"
functionLines := make([]string, 0, len(stack)/2)
for _, line := range stack {
trimmed := strings.TrimSpace(line)
if trimmed == "" || strings.HasPrefix(trimmed, "/") || strings.HasPrefix(trimmed, "created by ") {
continue
}
normalized := goroutineArgPattern.ReplaceAllString(trimmed, "0x…")
functionLines = append(functionLines, normalized)
if top == "runtime" {
top = strings.Split(normalized, "(")[0]
}
}
signature := state + "\n" + strings.Join(functionLines, "\n")
if existing, ok := groups[signature]; ok {
existing.count++
return
}
if len(groups) >= maxGroups {
truncated = true
return
}
groups[signature] = &groupValue{state: state, top: top, stack: stack, count: 1}
}
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, "goroutine ") && strings.HasSuffix(line, "]:") {
flush()
block = []string{line}
continue
}
if len(block) > 0 {
block = append(block, line)
}
}
if scanner.Err() != nil {
truncated = true
}
flush()
result := make([]dto.RuntimeGoroutineGroup, 0, len(groups))
for _, group := range groups {
result = append(result, dto.RuntimeGoroutineGroup{State: group.state, Top: group.top, Count: group.count, Stack: group.stack})
}
sort.Slice(result, func(i, j int) bool {
if result[i].Count == result[j].Count {
return result[i].Top < result[j].Top
}
return result[i].Count > result[j].Count
})
return result, truncated
}
func compactGoroutineSnapshot(initialSize int) ([]dto.RuntimeGoroutineGroup, bool) {
records := make([]runtime.StackRecord, initialSize+32)
count, ok := runtime.GoroutineProfile(records)
if !ok {
records = make([]runtime.StackRecord, count+32)
count, ok = runtime.GoroutineProfile(records)
}
truncated := !ok
if count > len(records) {
count = len(records)
truncated = true
}
records = records[:count]
type compactGroup struct {
top string
stack []string
count int
}
groups := make(map[string]*compactGroup)
for _, record := range records {
frames := runtime.CallersFrames(record.Stack())
stack := make([]string, 0, 16)
functions := make([]string, 0, 8)
top := "runtime"
for {
frame, more := frames.Next()
if frame.Function != "" {
if top == "runtime" {
top = frame.Function
}
functions = append(functions, frame.Function)
stack = append(stack, frame.Function, fmt.Sprintf("\t%s:%d", frame.File, frame.Line))
}
if !more || len(functions) >= 20 {
break
}
}
signature := strings.Join(functions, "\n")
if existing, exists := groups[signature]; exists {
existing.count++
continue
}
if len(groups) >= diagnosticsMaxGroups {
truncated = true
continue
}
groups[signature] = &compactGroup{top: top, stack: stack, count: 1}
}
result := make([]dto.RuntimeGoroutineGroup, 0, len(groups))
for _, group := range groups {
result = append(result, dto.RuntimeGoroutineGroup{State: "profiled", Top: group.top, Count: group.count, Stack: group.stack})
}
sort.Slice(result, func(i, j int) bool {
if result[i].Count == result[j].Count {
return result[i].Top < result[j].Top
}
return result[i].Count > result[j].Count
})
return result, truncated
}
+36
View File
@@ -9,6 +9,7 @@ import (
"crypto/x509"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"io"
"net"
@@ -38,13 +39,17 @@ import (
"github.com/1Panel-dev/1Panel/core/utils/xpack"
"github.com/gin-gonic/gin"
"golang.org/x/net/proxy"
"gorm.io/gorm"
)
type SettingService struct{}
var panelPortChangeMu sync.Mutex
var fileDownloadPreferenceMu sync.Mutex
type ISettingService interface {
GetFileDownloadPreference(userID string) (dto.FileDownloadPreference, error)
UpdateFileDownloadPreference(userID string, req dto.FileDownloadPreference) error
GetSettingInfo() (*dto.SettingInfo, error)
GetSettingBaseInfo() (*dto.SettingBaseInfo, error)
LoadInterfaceAddr() ([]string, error)
@@ -74,6 +79,37 @@ func NewISettingService() ISettingService {
return &SettingService{}
}
func (u *SettingService) GetFileDownloadPreference(userID string) (dto.FileDownloadPreference, error) {
var preference dto.FileDownloadPreference
if userID == "" {
return preference, buserr.New("ErrNotLogin")
}
fileDownloadPreferenceMu.Lock()
defer fileDownloadPreferenceMu.Unlock()
value, err := settingRepo.GetValueByKey("FileDownloadPreference:" + userID)
if errors.Is(err, gorm.ErrRecordNotFound) {
return preference, nil
}
if err != nil {
return preference, err
}
err = json.Unmarshal([]byte(value), &preference)
return preference, err
}
func (u *SettingService) UpdateFileDownloadPreference(userID string, req dto.FileDownloadPreference) error {
if userID == "" {
return buserr.New("ErrNotLogin")
}
value, err := json.Marshal(req)
if err != nil {
return err
}
fileDownloadPreferenceMu.Lock()
defer fileDownloadPreferenceMu.Unlock()
return settingRepo.UpdateOrCreate("FileDownloadPreference:"+userID, string(value))
}
func (u *SettingService) GetSettingInfo() (*dto.SettingInfo, error) {
setting, err := settingRepo.List()
if err != nil {
+275 -23
View File
@@ -10701,6 +10701,89 @@ const docTemplate = `{
}
}
},
"/core/hosts/diagnostics/goroutines": {
"get": {
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.RuntimeGoroutineSnapshot"
}
}
},
"security": [
{
"ApiKeyAuth": []
},
{
"Timestamp": []
}
],
"summary": "Load Core grouped goroutine snapshot",
"tags": [
"RuntimeDiagnostics"
]
}
},
"/core/hosts/diagnostics/profiles": {
"post": {
"parameters": [
{
"description": "request",
"in": "body",
"name": "request",
"required": true,
"schema": {
"$ref": "#/definitions/dto.RuntimeProfileCreate"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "file"
}
}
},
"security": [
{
"ApiKeyAuth": []
},
{
"Timestamp": []
}
],
"summary": "Capture Core runtime profile",
"tags": [
"RuntimeDiagnostics"
]
}
},
"/core/hosts/diagnostics/summary": {
"get": {
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.RuntimeDiagnosticsSummary"
}
}
},
"security": [
{
"ApiKeyAuth": []
},
{
"Timestamp": []
}
],
"summary": "Load Core runtime diagnostics summary",
"tags": [
"RuntimeDiagnostics"
]
}
},
"/core/logs/clean": {
"post": {
"consumes": [
@@ -19106,6 +19189,7 @@ const docTemplate = `{
},
"/hosts/firewall/rules/delete": {
"post": {
"description": "Deletes managed rules by UUID or unprotected before-chain rules by instance key. Returns a taskID immediately; results are written to the task log.",
"consumes": [
"application/json"
],
@@ -19142,7 +19226,7 @@ const docTemplate = `{
"Timestamp": []
}
],
"summary": "Delete managed unified firewall v2 rules",
"summary": "Queue firewall rule deletion",
"tags": [
"Firewall"
],
@@ -19418,7 +19502,97 @@ const docTemplate = `{
"consumes": [
"application/json"
],
"description": "Returns a taskID; configuration save and per-rule results are recorded in the task log.",
"description": "Saves whitelist configuration only. Missing rules are added on startup, restart, initialization, or synchronization; existing rules are not removed.",
"parameters": [
{
"description": "request",
"in": "body",
"name": "request",
"required": true,
"schema": {
"$ref": "#/definitions/dto.FirewallPortWhitelistCreate"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"security": [
{
"ApiKeyAuth": []
},
{
"Timestamp": []
}
],
"summary": "Create firewall port whitelist rules",
"tags": [
"Firewall"
],
"x-panel-log": {
"BeforeFunctions": [],
"bodyKeys": [
"rule"
],
"formatEN": "create firewall port whitelist",
"formatZH": "创建防火墙端口白名单",
"paramKeys": []
}
}
},
"/hosts/firewall/settings/whitelist/delete": {
"post": {
"consumes": [
"application/json"
],
"description": "Saves whitelist configuration only. Missing rules are added on startup, restart, initialization, or synchronization; existing rules are not removed.",
"parameters": [
{
"description": "request",
"in": "body",
"name": "request",
"required": true,
"schema": {
"$ref": "#/definitions/dto.FirewallPortWhitelistDelete"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"security": [
{
"ApiKeyAuth": []
},
{
"Timestamp": []
}
],
"summary": "Delete firewall port whitelist rules",
"tags": [
"Firewall"
],
"x-panel-log": {
"BeforeFunctions": [],
"bodyKeys": [
"rules"
],
"formatEN": "delete firewall port whitelist",
"formatZH": "删除防火墙端口白名单",
"paramKeys": []
}
}
},
"/hosts/firewall/settings/whitelist/update": {
"post": {
"consumes": [
"application/json"
],
"description": "Saves whitelist configuration only. Missing rules are added on startup, restart, initialization, or synchronization; existing rules are not removed.",
"parameters": [
{
"description": "request",
@@ -19432,10 +19606,7 @@ const docTemplate = `{
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.FilterChainOperationResponse"
}
"description": "OK"
}
},
"security": [
@@ -19446,17 +19617,18 @@ const docTemplate = `{
"Timestamp": []
}
],
"summary": "Queue firewall port whitelist update",
"summary": "Update firewall port whitelist rules",
"tags": [
"Firewall"
],
"x-panel-log": {
"BeforeFunctions": [],
"bodyKeys": [
"value"
"oldRule",
"rule"
],
"formatEN": "update firewall port whitelist [value]",
"formatZH": "更新防火墙端口白名单 [value]",
"formatEN": "update firewall port whitelist",
"formatZH": "编辑防火墙端口白名单",
"paramKeys": []
}
}
@@ -36182,6 +36354,43 @@ const docTemplate = `{
],
"type": "object"
},
"dto.FirewallPortWhitelistCreate": {
"properties": {
"rule": {
"$ref": "#/definitions/filter.PortWhitelist"
}
},
"required": [
"rule"
],
"type": "object"
},
"dto.FirewallPortWhitelistDelete": {
"properties": {
"rule": {
"$ref": "#/definitions/filter.PortWhitelist"
}
},
"required": [
"rule"
],
"type": "object"
},
"dto.FirewallPortWhitelistUpdate": {
"properties": {
"oldRule": {
"$ref": "#/definitions/filter.PortWhitelist"
},
"rule": {
"$ref": "#/definitions/filter.PortWhitelist"
}
},
"required": [
"oldRule",
"rule"
],
"type": "object"
},
"dto.FirewallRuleAdopt": {
"type": "object",
"required": [
@@ -36278,17 +36487,36 @@ const docTemplate = `{
"type": "object"
},
"dto.FirewallRuleDelete": {
"description": "Provide managed rule UUIDs, before-chain rule targets, or both.",
"properties": {
"beforeRules": {
"items": {
"$ref": "#/definitions/dto.FirewallRuleDeleteTarget"
},
"type": "array"
},
"uuids": {
"items": {
"type": "string"
},
"minItems": 1,
"type": "array"
}
},
"type": "object"
},
"dto.FirewallRuleDeleteTarget": {
"properties": {
"instanceKey": {
"maxLength": 128,
"type": "string"
},
"scope": {
"$ref": "#/definitions/filter.Scope"
}
},
"required": [
"uuids"
"scope",
"instanceKey"
],
"type": "object"
},
@@ -36317,8 +36545,14 @@ const docTemplate = `{
"failed": {
"type": "integer"
},
"queued": {
"type": "boolean"
},
"succeeded": {
"type": "integer"
},
"taskID": {
"type": "string"
}
},
"type": "object"
@@ -36452,17 +36686,6 @@ const docTemplate = `{
],
"type": "object"
},
"dto.FirewallPortWhitelistUpdate": {
"properties": {
"value": {
"type": "string"
}
},
"required": [
"value"
],
"type": "object"
},
"dto.FirewallSettings": {
"properties": {
"docker": {
@@ -36471,10 +36694,19 @@ const docTemplate = `{
"forwarding": {
"$ref": "#/definitions/dto.FirewallBackendGroup"
},
"panelPort": {
"type": "string"
},
"pingStatus": {
"type": "string"
},
"portWhiteList": {
"type": "array",
"items": {
"$ref": "#/definitions/filter.PortWhitelist"
}
},
"sshPort": {
"type": "string"
},
"system": {
@@ -41473,6 +41705,26 @@ const docTemplate = `{
"PersistenceStatusPermanentOnly"
]
},
"filter.PortWhitelist": {
"properties": {
"port": {
"type": "string"
},
"protocol": {
"type": "string"
},
"sources": {
"type": "array",
"items": {
"type": "string"
}
},
"type": {
"type": "string"
}
},
"type": "object"
},
"filter.RuntimeUsage": {
"properties": {
"reason": {
+276 -24
View File
@@ -10697,6 +10697,89 @@
}
}
},
"/core/hosts/diagnostics/goroutines": {
"get": {
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.RuntimeGoroutineSnapshot"
}
}
},
"security": [
{
"ApiKeyAuth": []
},
{
"Timestamp": []
}
],
"summary": "Load Core grouped goroutine snapshot",
"tags": [
"RuntimeDiagnostics"
]
}
},
"/core/hosts/diagnostics/profiles": {
"post": {
"parameters": [
{
"description": "request",
"in": "body",
"name": "request",
"required": true,
"schema": {
"$ref": "#/definitions/dto.RuntimeProfileCreate"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "file"
}
}
},
"security": [
{
"ApiKeyAuth": []
},
{
"Timestamp": []
}
],
"summary": "Capture Core runtime profile",
"tags": [
"RuntimeDiagnostics"
]
}
},
"/core/hosts/diagnostics/summary": {
"get": {
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.RuntimeDiagnosticsSummary"
}
}
},
"security": [
{
"ApiKeyAuth": []
},
{
"Timestamp": []
}
],
"summary": "Load Core runtime diagnostics summary",
"tags": [
"RuntimeDiagnostics"
]
}
},
"/core/logs/clean": {
"post": {
"consumes": [
@@ -19102,6 +19185,7 @@
},
"/hosts/firewall/rules/delete": {
"post": {
"description": "Deletes managed rules by UUID or unprotected before-chain rules by instance key. Returns a taskID immediately; results are written to the task log.",
"consumes": [
"application/json"
],
@@ -19138,7 +19222,7 @@
"Timestamp": []
}
],
"summary": "Delete managed unified firewall v2 rules",
"summary": "Queue firewall rule deletion",
"tags": [
"Firewall"
],
@@ -19414,7 +19498,97 @@
"consumes": [
"application/json"
],
"description": "Returns a taskID; configuration save and per-rule results are recorded in the task log.",
"description": "Saves whitelist configuration only. Missing rules are added on startup, restart, initialization, or synchronization; existing rules are not removed.",
"parameters": [
{
"description": "request",
"in": "body",
"name": "request",
"required": true,
"schema": {
"$ref": "#/definitions/dto.FirewallPortWhitelistCreate"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"security": [
{
"ApiKeyAuth": []
},
{
"Timestamp": []
}
],
"summary": "Create firewall port whitelist rules",
"tags": [
"Firewall"
],
"x-panel-log": {
"BeforeFunctions": [],
"bodyKeys": [
"rule"
],
"formatEN": "create firewall port whitelist",
"formatZH": "创建防火墙端口白名单",
"paramKeys": []
}
}
},
"/hosts/firewall/settings/whitelist/delete": {
"post": {
"consumes": [
"application/json"
],
"description": "Saves whitelist configuration only. Missing rules are added on startup, restart, initialization, or synchronization; existing rules are not removed.",
"parameters": [
{
"description": "request",
"in": "body",
"name": "request",
"required": true,
"schema": {
"$ref": "#/definitions/dto.FirewallPortWhitelistDelete"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"security": [
{
"ApiKeyAuth": []
},
{
"Timestamp": []
}
],
"summary": "Delete firewall port whitelist rules",
"tags": [
"Firewall"
],
"x-panel-log": {
"BeforeFunctions": [],
"bodyKeys": [
"rules"
],
"formatEN": "delete firewall port whitelist",
"formatZH": "删除防火墙端口白名单",
"paramKeys": []
}
}
},
"/hosts/firewall/settings/whitelist/update": {
"post": {
"consumes": [
"application/json"
],
"description": "Saves whitelist configuration only. Missing rules are added on startup, restart, initialization, or synchronization; existing rules are not removed.",
"parameters": [
{
"description": "request",
@@ -19428,10 +19602,7 @@
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.FilterChainOperationResponse"
}
"description": "OK"
}
},
"security": [
@@ -19442,17 +19613,18 @@
"Timestamp": []
}
],
"summary": "Queue firewall port whitelist update",
"summary": "Update firewall port whitelist rules",
"tags": [
"Firewall"
],
"x-panel-log": {
"BeforeFunctions": [],
"bodyKeys": [
"value"
"oldRule",
"rule"
],
"formatEN": "update firewall port whitelist [value]",
"formatZH": "更新防火墙端口白名单 [value]",
"formatEN": "update firewall port whitelist",
"formatZH": "编辑防火墙端口白名单",
"paramKeys": []
}
}
@@ -36178,6 +36350,43 @@
],
"type": "object"
},
"dto.FirewallPortWhitelistCreate": {
"properties": {
"rule": {
"$ref": "#/definitions/filter.PortWhitelist"
}
},
"required": [
"rule"
],
"type": "object"
},
"dto.FirewallPortWhitelistDelete": {
"properties": {
"rule": {
"$ref": "#/definitions/filter.PortWhitelist"
}
},
"required": [
"rule"
],
"type": "object"
},
"dto.FirewallPortWhitelistUpdate": {
"properties": {
"oldRule": {
"$ref": "#/definitions/filter.PortWhitelist"
},
"rule": {
"$ref": "#/definitions/filter.PortWhitelist"
}
},
"required": [
"oldRule",
"rule"
],
"type": "object"
},
"dto.FirewallRuleAdopt": {
"type": "object",
"required": [
@@ -36274,17 +36483,36 @@
"type": "object"
},
"dto.FirewallRuleDelete": {
"description": "Provide managed rule UUIDs, before-chain rule targets, or both.",
"properties": {
"beforeRules": {
"items": {
"$ref": "#/definitions/dto.FirewallRuleDeleteTarget"
},
"type": "array"
},
"uuids": {
"items": {
"type": "string"
},
"minItems": 1,
"type": "array"
}
},
"type": "object"
},
"dto.FirewallRuleDeleteTarget": {
"properties": {
"instanceKey": {
"maxLength": 128,
"type": "string"
},
"scope": {
"$ref": "#/definitions/filter.Scope"
}
},
"required": [
"uuids"
"scope",
"instanceKey"
],
"type": "object"
},
@@ -36313,8 +36541,14 @@
"failed": {
"type": "integer"
},
"queued": {
"type": "boolean"
},
"succeeded": {
"type": "integer"
},
"taskID": {
"type": "string"
}
},
"type": "object"
@@ -36448,17 +36682,6 @@
],
"type": "object"
},
"dto.FirewallPortWhitelistUpdate": {
"properties": {
"value": {
"type": "string"
}
},
"required": [
"value"
],
"type": "object"
},
"dto.FirewallSettings": {
"properties": {
"docker": {
@@ -36467,10 +36690,19 @@
"forwarding": {
"$ref": "#/definitions/dto.FirewallBackendGroup"
},
"panelPort": {
"type": "string"
},
"pingStatus": {
"type": "string"
},
"portWhiteList": {
"type": "array",
"items": {
"$ref": "#/definitions/filter.PortWhitelist"
}
},
"sshPort": {
"type": "string"
},
"system": {
@@ -41469,6 +41701,26 @@
"PersistenceStatusPermanentOnly"
]
},
"filter.PortWhitelist": {
"properties": {
"port": {
"type": "string"
},
"protocol": {
"type": "string"
},
"sources": {
"type": "array",
"items": {
"type": "string"
}
},
"type": {
"type": "string"
}
},
"type": "object"
},
"filter.RuntimeUsage": {
"properties": {
"reason": {
@@ -49043,4 +49295,4 @@
"type": "object"
}
}
}
}
+33 -5
View File
@@ -2470,10 +2470,19 @@
},
"/core/xpack/vms/del": {
"bodyKeys": [
"name"
"id"
],
"paramKeys": [],
"beforeFunctions": [],
"beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "virtual_machines",
"output_column": "name",
"output_value": "name"
}
],
"formatZH": "删除虚拟机 [name]",
"formatEN": "delete VM [name]"
},
@@ -3770,12 +3779,31 @@
},
"/hosts/firewall/settings/whitelist": {
"bodyKeys": [
"value"
"rule"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "更新防火墙端口白名单 [value]",
"formatEN": "update firewall port whitelist [value]"
"formatZH": "创建防火墙端口白名单",
"formatEN": "create firewall port whitelist"
},
"/hosts/firewall/settings/whitelist/delete": {
"bodyKeys": [
"rules"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "删除防火墙端口白名单",
"formatEN": "delete firewall port whitelist"
},
"/hosts/firewall/settings/whitelist/update": {
"bodyKeys": [
"oldRule",
"rule"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "编辑防火墙端口白名单",
"formatEN": "update firewall port whitelist"
},
"/hosts/monitor/clean": {
"bodyKeys": [],
+1
View File
@@ -17,6 +17,7 @@ require (
github.com/go-playground/validator/v10 v10.30.3
github.com/go-resty/resty/v2 v2.17.2
github.com/go-webauthn/webauthn v0.17.4
github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6
github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.3
github.com/jinzhu/copier v0.4.0
+2
View File
@@ -126,6 +126,8 @@ github.com/google/go-tpm-tools v0.3.13-0.20230620182252-4639ecce2aba/go.mod h1:E
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6 h1:EEHtgt9IwisQ2AZ4pIsMjahcegHh6rmhqxzIRQIyepY=
github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
+1
View File
@@ -9,5 +9,6 @@ func commonGroups() []CommonRouter {
&CommandRouter{},
&GroupRouter{},
&ScriptRouter{},
&RuntimeDiagnosticsRouter{},
}
}
+21
View File
@@ -0,0 +1,21 @@
package router
import (
v2 "github.com/1Panel-dev/1Panel/core/app/api/v2"
"github.com/1Panel-dev/1Panel/core/middleware"
"github.com/gin-gonic/gin"
)
type RuntimeDiagnosticsRouter struct{}
func (s *RuntimeDiagnosticsRouter) InitRouter(Router *gin.RouterGroup) {
diagnosticsRouter := Router.Group("hosts/diagnostics").
Use(middleware.SessionAuth()).
Use(middleware.PasswordExpired())
baseApi := v2.ApiGroupApp.BaseApi
{
diagnosticsRouter.GET("/summary", baseApi.LoadRuntimeDiagnosticsSummary)
diagnosticsRouter.GET("/goroutines", baseApi.LoadRuntimeGoroutines)
diagnosticsRouter.POST("/profiles", baseApi.CreateRuntimeProfile)
}
}
+2
View File
@@ -22,6 +22,8 @@ func (s *SettingRouter) InitRouter(Router *gin.RouterGroup) {
Use(middleware.PasswordExpired())
{
settingRouter.POST("/search", baseApi.GetSettingInfo)
settingRouter.GET("/file/download", baseApi.GetFileDownloadPreference)
settingRouter.POST("/file/download", baseApi.UpdateFileDownloadPreference)
settingRouter.POST("/terminal/search", baseApi.GetTerminalSettingInfo)
settingRouter.GET("/search/available", baseApi.GetSystemAvailable)
settingRouter.POST("/update", baseApi.UpdateSetting)

Some files were not shown because too many files have changed in this diff Show More