diff --git a/Mofang/README.md b/Mofang/README.md index 23012d1..902b873 100644 --- a/Mofang/README.md +++ b/Mofang/README.md @@ -10,6 +10,7 @@ README.md handlers/ webssh.php templates/ + firewall.html info.html nat.html ``` @@ -93,11 +94,12 @@ Content-Type: application/json ## 客户区页面 -模块提供两个客户区选项卡: +模块提供三个客户区选项卡: ```text 实例信息 NAT转发 +防火墙 ``` 客户区按钮提供: @@ -197,6 +199,65 @@ DELETE /api/v1/containers/{id}/port-mappings/{index} } ``` +## 防火墙 + +防火墙是独立客户区页面,支持: + +- 查看防火墙启用状态、默认动作和规则列表 +- 启用 / 停用防火墙 +- 设置默认动作:未匹配拒绝或未匹配放行 +- 添加规则 +- 编辑规则 +- 删除规则 +- 单独启用 / 停用某条规则 + +页面会先在前端修改规则列表和开关状态,点击“保存设置”后才统一同步到 CLICD。这样可以避免每次切换开关、修改默认动作或编辑规则时都立即请求后端,减少客户区卡顿。 + +注意:防火墙关闭时也可以保存规则;关闭只表示暂时不接管该容器流量,不代表规则必须清空。 + +使用的 CLICD API: + +```text +GET /api/v1/containers/{id}/firewall +PUT /api/v1/containers/{id}/firewall +``` + +更新防火墙时必须使用 JSON 请求体,例如: + +```json +{ + "enabled": true, + "default_action": "ACCEPT", + "rules": [ + { + "id": "", + "network": "ipv4", + "direction": "in", + "protocol": "tcp", + "port": "22", + "source_ip": "", + "action": "ACCEPT", + "description": "Allow SSH", + "enabled": true + } + ] +} +``` + +规则字段说明: + +| 字段 | 说明 | +| --- | --- | +| `network` | 网络范围,常用 `ipv4`,也支持 `ipv6` / `all` | +| `direction` | 方向,`in` 入站,`out` 出站 | +| `protocol` | 协议,`tcp` 或 `udp` | +| `port` | 端口,可填写单端口、逗号分隔端口或端口段,例如 `22`、`80,443`、`8000-9000` | +| `source_ip` | 来源 IP / CIDR,留空表示任意来源 | +| `action` | 动作,`ACCEPT` 放行,`DROP` 拒绝 | +| `description` | 规则描述 | +| `enabled` | 是否启用该规则 | + +IPv4 NAT 入站规则的端口按容器内部端口匹配,不是宿主机公网端口。例如公网 `22023 -> 容器 22`,防火墙规则端口应填写 `22`。 ## WebSSH WebSSH 按钮会调用: @@ -252,6 +313,8 @@ https://www.example.com | 变更资源 | `PUT /api/v1/containers/{name}/resource-limit` | | 变更流量 | `PUT /api/v1/containers/{name}/traffic-limit` | | 同步到期 | `PUT /api/v1/containers/{name}/expiry` | +| 查询防火墙 | `GET /api/v1/containers/{id}/firewall` | +| 更新防火墙 | `PUT /api/v1/containers/{id}/firewall` | | WebSSH | `POST /api/v1/ssh-ticket` | ## 建议 API 权限 @@ -269,6 +332,7 @@ container:password container:traffic container:resize container:port +container:firewall task:read ssh-ticket:create ``` @@ -316,6 +380,22 @@ curl --location --request PUT \ --data-raw '{"container_port":8081,"host_port":61320,"protocol":"tcp","description":"HTTP"}' ``` +查询防火墙: + +```bash +curl -H "X-API-Key: clicd_sk_xxxx" \ + https://0.0.0.0:8999/api/v1/containers/10/firewall +``` + +更新防火墙: + +```bash +curl --location --request PUT \ + "https://0.0.0.0:8999/api/v1/containers/10/firewall" \ + --header "X-API-Key: clicd_sk_xxxx" \ + --header "Content-Type: application/json" \ + --data-raw '{"enabled":true,"default_action":"ACCEPT","rules":[{"id":"","network":"ipv4","direction":"in","protocol":"tcp","port":"22","source_ip":"","action":"ACCEPT","description":"Allow SSH","enabled":true}]}' +``` 创建 WebSSH 票据: ```bash @@ -336,6 +416,41 @@ curl --location --request POST \ Content-Type: application/json ``` +### 防火墙获取提示“不支持的方法” + +请确认模块版本已经包含防火墙页签修复。客户区防火墙列表应通过模块公开的 `firewallList` 调用,再由模块向 CLICD 发起: + +```text +GET /api/v1/containers/{id}/firewall +``` + +如果页面或二开代码直接把读取请求改成 `POST /api/v1/containers/{id}/firewall`,CLICD 会返回“不支持的方法”。 + +### 防火墙保存后规则为空 + +请确认更新接口最终发往 CLICD 的请求体是 JSON,并且包含 `rules` 数组。防火墙关闭时也可以保存规则,`enabled: false` 不应自动清空 `rules`。 + +正确请求体示例: + +```json +{ + "enabled": false, + "default_action": "ACCEPT", + "rules": [ + { + "id": "", + "network": "ipv4", + "direction": "in", + "protocol": "tcp", + "port": "22", + "source_ip": "", + "action": "ACCEPT", + "description": "Allow SSH", + "enabled": true + } + ] +} +``` ### 图表刚打开只有一条横线 CLICD 当前用量接口返回的是实时值,不是历史序列。页面刚打开时只有一个采样点,所以会显示当前值横线。选择 `10 秒` 自动刷新或点击“立即刷新”多采样几次后,会逐步形成折线。 @@ -344,7 +459,66 @@ CLICD 当前用量接口返回的是实时值,不是历史序列。页面刚 旧版本只显示 GB,小流量换算后会被四舍五入成 `0 GB`。当前版本已改为智能单位,会显示 B / KB / MB / GB。 -### WebSSH 打不开或提示不安全 WebSocket +### 防火墙 + +防火墙是独立客户区页面,支持: + +- 查看防火墙启用状态、默认动作和规则列表 +- 启用 / 停用防火墙 +- 设置默认动作:未匹配拒绝或未匹配放行 +- 添加规则 +- 编辑规则 +- 删除规则 +- 单独启用 / 停用某条规则 + +页面会先在前端修改规则列表和开关状态,点击“保存设置”后才统一同步到 CLICD。这样可以避免每次切换开关、修改默认动作或编辑规则时都立即请求后端,减少客户区卡顿。 + +注意:防火墙关闭时也可以保存规则;关闭只表示暂时不接管该容器流量,不代表规则必须清空。 + +使用的 CLICD API: + +```text +GET /api/v1/containers/{id}/firewall +PUT /api/v1/containers/{id}/firewall +``` + +更新防火墙时必须使用 JSON 请求体,例如: + +```json +{ + "enabled": true, + "default_action": "ACCEPT", + "rules": [ + { + "id": "", + "network": "ipv4", + "direction": "in", + "protocol": "tcp", + "port": "22", + "source_ip": "", + "action": "ACCEPT", + "description": "Allow SSH", + "enabled": true + } + ] +} +``` + +规则字段说明: + +| 字段 | 说明 | +| --- | --- | +| `network` | 网络范围,常用 `ipv4`,也支持 `ipv6` / `all` | +| `direction` | 方向,`in` 入站,`out` 出站 | +| `protocol` | 协议,`tcp` 或 `udp` | +| `port` | 端口,可填写单端口、逗号分隔端口或端口段,例如 `22`、`80,443`、`8000-9000` | +| `source_ip` | 来源 IP / CIDR,留空表示任意来源 | +| `action` | 动作,`ACCEPT` 放行,`DROP` 拒绝 | +| `description` | 规则描述 | +| `enabled` | 是否启用该规则 | + +IPv4 NAT 入站规则的端口按容器内部端口匹配,不是宿主机公网端口。例如公网 `22023 -> 容器 22`,防火墙规则端口应填写 `22`。 +## WebSSH 打不开或提示不安全 WebSocket 请确认 CLICD 面板已经启用 HTTPS/WSS,并且魔方服务器配置使用 HTTPS: diff --git a/Mofang/clicd.php b/Mofang/clicd.php index 7c0282a..353f912 100644 --- a/Mofang/clicd.php +++ b/Mofang/clicd.php @@ -40,7 +40,7 @@ function clicd_MetaData() 'DisplayName' => 'CLICD 对接模块 by 欢-Huan and ChatGPT 5.5 and DeepSeek V4', 'APIVersion' => '1.1', 'HelpDoc' => 'https://github.com/MengMengCode/CLICD', - 'version' => '1.0.5', + 'version' => '1.0.11', ]; } @@ -365,7 +365,10 @@ function clicd_webssh_url($params, $ticket, $containerName) $host = parse_url($baseUrl, PHP_URL_HOST); $port = parse_url($baseUrl, PHP_URL_PORT); $wsBase = $scheme . '://' . $host . ($port ? ':' . $port : ''); - $wsUrl = $wsBase . '/api/ssh?container=' . rawurlencode((string)$containerName); + $wsUrl = $wsBase + . '/api/ssh?container=' . rawurlencode((string)$containerName) + . '&container_name=' . rawurlencode((string)$containerName) + . '&ticket=' . rawurlencode((string)$ticket); $siteScheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http'; $siteHost = $_SERVER['HTTP_HOST'] ?? ''; @@ -374,6 +377,7 @@ function clicd_webssh_url($params, $ticket, $containerName) return $handler . '?ws=' . rawurlencode($wsUrl) . '&protocol=' . rawurlencode('clicd-ticket.' . (string)$ticket) + . '&ticket=' . rawurlencode((string)$ticket) . '&container=' . rawurlencode((string)$containerName); } @@ -626,9 +630,24 @@ function clicd_request_value($key, $default = '') function clicd_json_input() { + $input = []; + if (!empty($_POST) && is_array($_POST)) { + $input = $_POST; + } + $raw = file_get_contents('php://input'); $data = json_decode((string)$raw, true); - return is_array($data) ? $data : []; + if (is_array($data)) { + return array_merge($input, $data); + } + + $form = []; + parse_str((string)$raw, $form); + if (!empty($form) && is_array($form)) { + return array_merge($input, $form); + } + + return $input; } function clicd_param_value($data, $key, $default = '') @@ -1404,7 +1423,13 @@ function clicd_ClientButton($params) function clicd_webssh($params) { + $container = []; $containerName = clicd_container_name($params); + clicd_container_api_id($params, $container); + if (!empty($container['name'])) { + $containerName = (string)$container['name']; + } + $res = clicd_request($params, '/api/v1/ssh-ticket', ['container_name' => $containerName], 'POST', 30); if (!clicd_success($res)) { return ['status' => 'error', 'msg' => clicd_message($res, 'WebSSH ticket create failed')]; @@ -1689,7 +1714,3 @@ function clicd_ClientAreaOutput($params, $key) ], ]; } - - - - diff --git a/Mofang/handlers/webssh.php b/Mofang/handlers/webssh.php index 3be35c7..510308a 100644 --- a/Mofang/handlers/webssh.php +++ b/Mofang/handlers/webssh.php @@ -2,8 +2,13 @@ $ws = isset($_GET['ws']) ? (string)$_GET['ws'] : (isset($_GET['amp;ws']) ? (string)$_GET['amp;ws'] : ''); $protocol = isset($_GET['protocol']) ? (string)$_GET['protocol'] : (isset($_GET['amp;protocol']) ? (string)$_GET['amp;protocol'] : ''); $container = isset($_GET['container']) ? (string)$_GET['container'] : (isset($_GET['amp;container']) ? (string)$_GET['amp;container'] : ''); +$ticket = isset($_GET['ticket']) ? (string)$_GET['ticket'] : (isset($_GET['amp;ticket']) ? (string)$_GET['amp;ticket'] : ''); -if ($ws === '' || $protocol === '') { +if ($protocol === '' && $ticket !== '') { + $protocol = 'clicd-ticket.' . $ticket; +} + +if ($ws === '') { http_response_code(400); header('Content-Type: text/plain; charset=utf-8'); echo "Missing WebSSH parameters\n"; @@ -64,6 +69,7 @@ if ($ws === '' || $protocol === '') { (function(){ var wsUrl = ; var protocol = ; + var ticket = ; var term = document.getElementById('term'); var state = document.getElementById('state'); var modeSelect = document.getElementById('send-mode'); @@ -189,8 +195,17 @@ if ($ws === '' || $protocol === '') { iostat.textContent = 'S' + sentCount + ' R' + recvCount + ' ' + stateText; } + function websocketProtocolValue(value) { + value = String(value || ''); + return /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/.test(value) ? value : ''; + } + try { - socket = new WebSocket(wsUrl, protocol); + var protocolValue = websocketProtocolValue(protocol); + if (!protocolValue && ticket) { + append('[WebSSH] 票据已通过 URL 参数传递,当前浏览器不会发送子协议。\n'); + } + socket = protocolValue ? new WebSocket(wsUrl, protocolValue) : new WebSocket(wsUrl); socket.binaryType = 'arraybuffer'; } catch (e) { setState('err', '\nWebSocket 创建失败:' + e.message + '\n'); diff --git a/Mofang/templates/firewall.html b/Mofang/templates/firewall.html index 3450595..1375ade 100644 --- a/Mofang/templates/firewall.html +++ b/Mofang/templates/firewall.html @@ -1,134 +1,660 @@