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
+44 -2
View File
@@ -199,6 +199,8 @@ const endpointGroups: Array<{ title: string; endpoints: EndpointTuple[] }> = [
['GET', '/api/v1/templates', '模板列表'],
['GET', '/api/v1/images', '镜像管理列表'],
['GET', '/api/v1/images/enabled?type=lxc&container={id}', '可用于创建或重装的已启用镜像'],
['POST', '/api/v1/images/custom', '添加第三方 LXC/KVM 镜像源'],
['DELETE', '/api/v1/images/custom', '移除第三方 LXC/KVM 镜像源'],
['POST', '/api/v1/images/download', '下载镜像'],
['POST', '/api/v1/images/cancel', '取消镜像下载'],
['DELETE', '/api/v1/images/delete', '删除镜像缓存'],
@@ -228,6 +230,8 @@ const endpointGroups: Array<{ title: string; endpoints: EndpointTuple[] }> = [
['PUT', '/api/v1/ssl', '更新 SSL 配置'],
['GET', '/api/v1/webssh-origins', 'WebSSH/VNC Origin 白名单'],
['PUT', '/api/v1/webssh-origins', '更新 WebSSH/VNC Origin 白名单'],
['GET', '/api/v1/access-policy', '面板访问来源策略'],
['PUT', '/api/v1/access-policy', '更新面板访问来源策略'],
['GET', '/api/v1/language', '面板语言'],
['PUT', '/api/v1/language', '更新面板语言'],
],
@@ -845,6 +849,18 @@ const requestBodySamples: Record<string, Record<string, unknown>> = {
time: '03:00',
},
'PUT /api/v1/containers/{id}/snapshots/quota': { snapshot_limit: 2 },
'POST /api/v1/images/custom': {
type: 'kvm',
name: 'Custom Ubuntu Cloud',
description: 'Private mirror image',
distro: 'ubuntu',
release: 'noble',
arch: 'amd64',
url: 'https://images.example.com/ubuntu-noble.qcow2',
provisioner: 'linux-cloud-init',
sha256: '',
},
'DELETE /api/v1/images/custom': { id: 'custom-kvm-a1b2c3d4e5' },
'POST /api/v1/images/download': { template_id: 'debian-bookworm' },
'POST /api/v1/images/cancel': { template_id: 'debian-bookworm' },
'DELETE /api/v1/images/delete': { template_id: 'debian-bookworm' },
@@ -873,6 +889,11 @@ const requestBodySamples: Record<string, Record<string, unknown>> = {
'PUT /api/v1/webssh-origins': {
origins: ['https://panel.example.com'],
},
'PUT /api/v1/access-policy': {
enabled: true,
allowed_sources: ['203.0.113.10', '192.168.1.0/24', '2001:db8::/32'],
trusted_proxies: ['127.0.0.1'],
},
'PUT /api/v1/language': { language: 'zh' },
'PUT /api/v1/routing': {
items: [
@@ -1027,6 +1048,11 @@ const responseSamples: Record<string, unknown> = {
data: {
nat4: { used: 62, remaining: '45474', total: '45536' },
nat4_port_range: { start: 20000, end: 65535 },
nat4_next_port: 22005,
nat4_networks: {
lxc: { subnet: '10.0.3.0/24', gateway: '10.0.3.1', netmask: '255.255.255.0', dhcp_start: '10.0.3.2', dhcp_end: '10.0.3.254', dhcp_max: 253, prefix_bits: 24 },
kvm: { subnet: '192.168.122.0/24', gateway: '192.168.122.1', netmask: '255.255.255.0', dhcp_start: '192.168.122.2', dhcp_end: '192.168.122.254', dhcp_max: 253, prefix_bits: 24 },
},
ipv4: { used: 1, remaining: '3', total: '4' },
ipv6: { used: 31, remaining: 'large', total: 'large' },
public_ipv4_addresses: [{ address: '203.0.113.10', interface: 'eth0', prefix_len: 32, gateway: '203.0.113.1' }],
@@ -1042,6 +1068,11 @@ const responseSamples: Record<string, unknown> = {
data: {
nat4: { used: 62, remaining: '45474', total: '45536' },
nat4_port_range: { start: 20000, end: 65535 },
nat4_next_port: 22005,
nat4_networks: {
lxc: { subnet: '10.0.3.0/24', gateway: '10.0.3.1' },
kvm: { subnet: '192.168.122.0/24', gateway: '192.168.122.1' },
},
ipv4: { used: 1, remaining: '3', total: '4' },
public_ipv4_addresses: [{ address: '203.0.113.10', interface: 'eth0', prefix_len: 32, gateway: '203.0.113.1' }],
ipv6_prefixes: [{ interface: 'eth0', address: '2001:db8:100::2', prefix: '2001:db8:100::/64', prefix_len: 64, gateway: '2001:db8:100::1' }],
@@ -1234,6 +1265,12 @@ const responseSamples: Record<string, unknown> = {
{ id: 'debian-bookworm', name: 'Debian 12', distro: 'debian', release: 'bookworm', arch: 'amd64', type: 'lxc', downloaded: true, enabled: true },
],
},
'POST /api/v1/images/custom': {
success: true,
message: 'Custom image added',
data: { id: 'custom-kvm-a1b2c3d4e5', name: 'Custom Ubuntu Cloud' },
},
'DELETE /api/v1/images/custom': { success: true, message: 'Custom image removed' },
'POST /api/v1/images/download': { success: true, message: 'Already downloaded' },
'POST /api/v1/images/cancel': { success: true, message: 'Cancel requested' },
'DELETE /api/v1/images/delete': { success: true, message: 'Deleted' },
@@ -1277,6 +1314,8 @@ const responseSamples: Record<string, unknown> = {
'PUT /api/v1/ssl': { success: true, message: 'SSL settings saved', data: { enabled: true, mode: 'letsencrypt', target: 'panel.example.com', needs_restart: true } },
'GET /api/v1/webssh-origins': { success: true, data: { origins: ['https://panel.example.com'], current_origin: 'https://panel.example.com' } },
'PUT /api/v1/webssh-origins': { success: true, message: 'Origin allowlist saved', data: { origins: ['https://panel.example.com'], current_origin: 'https://panel.example.com' } },
'GET /api/v1/access-policy': { success: true, data: { enabled: true, allowed_sources: ['203.0.113.10', '192.168.1.0/24'], trusted_proxies: ['127.0.0.1'], current_source: '203.0.113.10', direct_source: '127.0.0.1', using_forwarded: true } },
'PUT /api/v1/access-policy': { success: true, message: 'Panel access policy saved', data: { enabled: true, allowed_sources: ['203.0.113.10', '192.168.1.0/24'], trusted_proxies: ['127.0.0.1'], current_source: '203.0.113.10', direct_source: '127.0.0.1', using_forwarded: true } },
'GET /api/v1/language': { success: true, data: { language: 'zh' } },
'PUT /api/v1/language': { success: true, data: { language: 'zh' } },
'GET /api/v1/security/alerts': { success: true, data: [] },
@@ -1367,7 +1406,7 @@ function endpointNoteFor(key: string) {
}
if (key === 'POST /api/v1/batch-create') {
notes.push('Each containers[] item in batch creation supports the same storage, network, image allowlist, and SSH authentication fields as POST /api/v1/containers.')
notes.push('Custom management_port and NAT host_port values must be unique across the batch. The panel shifts each source-port group for later containers while keeping target ports unchanged; direct API clients should submit the expanded values explicitly.')
notes.push('Custom management_port and NAT host_port values must be unique across the batch. The panel places each later source-port group after the previous container\'s highest public port while keeping every target container_port unchanged; direct API clients should submit the expanded values explicitly.')
}
if (key === 'PUT /api/v1/containers/{id}/resource-limit') {
notes.push('Supports independent download/upload bandwidth limits and read/write I/O limits. Omitted fields keep their current values, and explicitly passing 0 makes that direction unlimited. network_bw_mbps and io_speed_mbps are deprecated symmetric compatibility aliases.')
@@ -1399,9 +1438,12 @@ function endpointNoteFor(key: string) {
if (key === 'PUT /api/v1/storage') {
notes.push('Start from GET /api/v1/storage and submit mounted disks returned by the server. Paths and mount points are server-managed and custom paths are rejected. content_types enables a disk for each workload; only one pool may be the default for each type.')
}
if (key.includes('/api/v1/storage') || key.includes('/task-queue/settings') || key.includes('/api/v1/ssl') || key.includes('/webssh-origins')) {
if (key.includes('/api/v1/storage') || key.includes('/task-queue/settings') || key.includes('/api/v1/ssl') || key.includes('/webssh-origins') || key.includes('/access-policy')) {
notes.push('This endpoint requires an API key with admin:access.')
}
if (key === 'PUT /api/v1/access-policy') {
notes.push('allowed_sources and trusted_proxies accept IPv4, IPv6, or CIDR values. Forwarded client headers are ignored unless the direct peer matches trusted_proxies. The server rejects an enabled policy that excludes the current source.')
}
if (key === 'PUT /api/v1/task-queue/settings') {
notes.push('concurrency must be between 1 and 16. Tasks targeting the same container are still serialized.')
}