Add custom image handling and access policy management

- Implement tests for custom KVM and LXC image creation, ensuring invalid sources and architecture mismatches are rejected.
- Introduce access policy management in CLI, allowing configuration of allowed sources and trusted proxies.
- Add NAT network configuration with validation for RFC1918 compliance and subnet parsing.
- Create panel access policy management, including normalization and evaluation of access decisions based on client IPs and forwarded headers.
- Develop middleware for enforcing access policies in the server, returning appropriate responses for allowed and denied requests.
- Enhance custom image downloading and validation, ensuring integrity and security of downloaded root filesystem archives.
- Include comprehensive tests for all new functionalities to ensure reliability and correctness.
This commit is contained in:
MengMengCode
2026-07-26 04:04:45 +08:00
parent 8283b88ded
commit 38debab1aa
49 changed files with 4504 additions and 246 deletions
+189 -7
View File
@@ -794,6 +794,8 @@ type ClicdConfig struct {
NextSSHPort int `json:"next_ssh_port"`
NATPortStart int `json:"nat_port_start"`
NATPortEnd int `json:"nat_port_end"`
LXCNATSubnet string `json:"lxc_nat_subnet"`
KVMNATSubnet string `json:"kvm_nat_subnet"`
SetupComplete bool `json:"setup_complete"`
SubUsers []SubUser `json:"sub_users"`
ApiKeys []ApiKeyConfig `json:"api_keys"`
@@ -801,10 +803,13 @@ type ClicdConfig struct {
Tasks []SavedTask `json:"tasks"`
LoginLogs []SavedLoginLog `json:"login_logs"`
EnabledImages []string `json:"enabled_images"`
CustomKVMImages []CustomKVMImage `json:"custom_kvm_images"`
CustomLXCImages []CustomLXCImage `json:"custom_lxc_images"`
Snapshots []Snapshot `json:"snapshots"`
PublicIPv4Pool []PublicIPv4Assignment `json:"public_ipv4_pool"`
PublicIPv6Prefixes []PublicIPv6Prefix `json:"public_ipv6_prefixes"`
WebSSHAllowedOrigins []string `json:"webssh_allowed_origins"`
PanelAccessPolicy PanelAccessPolicy `json:"panel_access_policy"`
SecurityAutoShutdown bool `json:"security_auto_shutdown"`
TaskConcurrency int `json:"task_concurrency"`
Language string `json:"language"`
@@ -813,6 +818,39 @@ type ClicdConfig struct {
StoragePools []StoragePool `json:"storage_pools"`
}
const (
KVMProvisionerLinuxCloudInit = "linux-cloud-init"
KVMProvisionerWindows10 = "windows-10"
KVMProvisionerWindows11 = "windows-11"
)
// CustomKVMImage is an administrator-defined KVM image source.
type CustomKVMImage struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Distro string `json:"distro"`
Release string `json:"release"`
Arch string `json:"arch"`
URL string `json:"url"`
Provisioner string `json:"provisioner"`
SHA256 string `json:"sha256,omitempty"`
CreatedAt string `json:"created_at"`
}
// CustomLXCImage is an administrator-defined LXC rootfs archive source.
type CustomLXCImage struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Distro string `json:"distro"`
Release string `json:"release"`
Arch string `json:"arch"`
URL string `json:"url"`
SHA256 string `json:"sha256,omitempty"`
CreatedAt string `json:"created_at"`
}
var configPath string
var AppConfig *ClicdConfig
var allocationMu sync.Mutex
@@ -946,6 +984,8 @@ func InitConfig() (*ClicdConfig, error) {
NextSSHPort: 22000,
NATPortStart: DefaultNATPortStart,
NATPortEnd: DefaultNATPortEnd,
LXCNATSubnet: configuredSubnetValue("", "CLICD_LXC_SUBNET", DefaultLXCNATSubnet),
KVMNATSubnet: configuredSubnetValue("", "CLICD_KVM_SUBNET", DefaultKVMNATSubnet),
SetupComplete: false,
SubUsers: []SubUser{},
AuditLogs: []AuditLog{},
@@ -955,8 +995,12 @@ func InitConfig() (*ClicdConfig, error) {
PublicIPv4Pool: []PublicIPv4Assignment{},
PublicIPv6Prefixes: []PublicIPv6Prefix{},
WebSSHAllowedOrigins: []string{},
TaskConcurrency: DefaultTaskConcurrency,
StoragePools: []StoragePool{defaultPrimaryStoragePool()},
PanelAccessPolicy: PanelAccessPolicy{
AllowedSources: []string{},
TrustedProxies: []string{},
},
TaskConcurrency: DefaultTaskConcurrency,
StoragePools: []StoragePool{defaultPrimaryStoragePool()},
}
if err := SaveConfig(); err != nil {
@@ -994,6 +1038,9 @@ func normalizeConfigDefaults(dataDir string) bool {
if normalizeNATPortRangeDefaults() {
changed = true
}
if normalizeNATNetworkDefaults() {
changed = true
}
if AppConfig.NextContainerID == 0 {
AppConfig.NextContainerID = 1
changed = true
@@ -1029,6 +1076,18 @@ func normalizeConfigDefaults(dataDir string) bool {
AppConfig.WebSSHAllowedOrigins = normalized
changed = true
}
if normalized, err := NormalizePanelAccessPolicy(AppConfig.PanelAccessPolicy); err == nil {
if !panelAccessPoliciesEqual(AppConfig.PanelAccessPolicy, normalized) {
AppConfig.PanelAccessPolicy = normalized
changed = true
}
} else {
AppConfig.PanelAccessPolicy = PanelAccessPolicy{
AllowedSources: []string{},
TrustedProxies: []string{},
}
changed = true
}
if len(AppConfig.StoragePools) == 0 {
AppConfig.StoragePools = []StoragePool{defaultPrimaryStoragePool()}
changed = true
@@ -1067,6 +1126,14 @@ func normalizeConfigDefaults(dataDir string) bool {
AppConfig.EnabledImages = make([]string, 0)
changed = true
}
if AppConfig.CustomKVMImages == nil {
AppConfig.CustomKVMImages = make([]CustomKVMImage, 0)
changed = true
}
if AppConfig.CustomLXCImages == nil {
AppConfig.CustomLXCImages = make([]CustomLXCImage, 0)
changed = true
}
if AppConfig.Language == "" {
AppConfig.Language = "zh"
changed = true
@@ -1435,6 +1502,104 @@ func SaveConfig() error {
return saveConfigToDB()
}
func ListCustomKVMImages() []CustomKVMImage {
allocationMu.Lock()
defer allocationMu.Unlock()
if AppConfig == nil {
return nil
}
return append([]CustomKVMImage(nil), AppConfig.CustomKVMImages...)
}
func AddCustomKVMImage(image CustomKVMImage) error {
allocationMu.Lock()
defer allocationMu.Unlock()
for _, existing := range AppConfig.CustomKVMImages {
if existing.ID == image.ID {
return fmt.Errorf("custom KVM image %q already exists", image.ID)
}
}
AppConfig.CustomKVMImages = append(AppConfig.CustomKVMImages, image)
if err := SaveConfig(); err != nil {
AppConfig.CustomKVMImages = AppConfig.CustomKVMImages[:len(AppConfig.CustomKVMImages)-1]
return err
}
return nil
}
func RemoveCustomKVMImage(id string) (bool, error) {
allocationMu.Lock()
defer allocationMu.Unlock()
filtered := make([]CustomKVMImage, 0, len(AppConfig.CustomKVMImages))
found := false
for _, image := range AppConfig.CustomKVMImages {
if image.ID == id {
found = true
continue
}
filtered = append(filtered, image)
}
if !found {
return false, nil
}
previous := AppConfig.CustomKVMImages
AppConfig.CustomKVMImages = filtered
if err := SaveConfig(); err != nil {
AppConfig.CustomKVMImages = previous
return false, err
}
return true, nil
}
func ListCustomLXCImages() []CustomLXCImage {
allocationMu.Lock()
defer allocationMu.Unlock()
if AppConfig == nil {
return nil
}
return append([]CustomLXCImage(nil), AppConfig.CustomLXCImages...)
}
func AddCustomLXCImage(image CustomLXCImage) error {
allocationMu.Lock()
defer allocationMu.Unlock()
for _, existing := range AppConfig.CustomLXCImages {
if existing.ID == image.ID {
return fmt.Errorf("custom LXC image %q already exists", image.ID)
}
}
AppConfig.CustomLXCImages = append(AppConfig.CustomLXCImages, image)
if err := SaveConfig(); err != nil {
AppConfig.CustomLXCImages = AppConfig.CustomLXCImages[:len(AppConfig.CustomLXCImages)-1]
return err
}
return nil
}
func RemoveCustomLXCImage(id string) (bool, error) {
allocationMu.Lock()
defer allocationMu.Unlock()
filtered := make([]CustomLXCImage, 0, len(AppConfig.CustomLXCImages))
found := false
for _, image := range AppConfig.CustomLXCImages {
if image.ID == id {
found = true
continue
}
filtered = append(filtered, image)
}
if !found {
return false, nil
}
previous := AppConfig.CustomLXCImages
AppConfig.CustomLXCImages = filtered
if err := SaveConfig(); err != nil {
AppConfig.CustomLXCImages = previous
return false, err
}
return true, nil
}
// AddContainer adds a container to the config
func AddContainer(c Container) {
allocationMu.Lock()
@@ -1736,6 +1901,28 @@ func AllocateSSHPort() (int, error) {
func AllocateSSHPortExcluding(excluded []int) (int, error) {
allocationMu.Lock()
defer allocationMu.Unlock()
candidate, err := previewSSHPortExcluding(excluded)
if err != nil {
return 0, err
}
start, end := NATPortRange()
AppConfig.NextSSHPort = candidate + 1
if AppConfig.NextSSHPort > end {
AppConfig.NextSSHPort = start
}
SaveConfig()
return candidate, nil
}
// PreviewSSHPortExcluding returns the management port that the allocator would
// choose without advancing or persisting the allocation cursor.
func PreviewSSHPortExcluding(excluded []int) (int, error) {
allocationMu.Lock()
defer allocationMu.Unlock()
return previewSSHPortExcluding(excluded)
}
func previewSSHPortExcluding(excluded []int) (int, error) {
used := collectAllHostPorts()
for _, port := range excluded {
if port > 0 {
@@ -1753,11 +1940,6 @@ func AllocateSSHPortExcluding(excluded []int) (int, error) {
if used[candidate] {
continue
}
AppConfig.NextSSHPort = candidate + 1
if AppConfig.NextSSHPort > end {
AppConfig.NextSSHPort = start
}
SaveConfig()
return candidate, nil
}
return 0, fmt.Errorf("no free NAT4 host port in configured range %d-%d", start, end)