Compare commits
No commits in common. "9c07b9fadc52c94009ea1be1cd05164aa4ad90a7" and "f274a7dd4bbd88d9a9cea878760658eb430095fa" have entirely different histories.
9c07b9fadc
...
f274a7dd4b
@ -8,7 +8,7 @@ return [
|
|||||||
'192.168.0.0/16',
|
'192.168.0.0/16',
|
||||||
'10.10.0.0/16',
|
'10.10.0.0/16',
|
||||||
],
|
],
|
||||||
'allowed_ports' => [22, 80, 443, '3000-60000', 3306, 5432],
|
'allowed_ports' => [22, 80, 443, 3306, 5432],
|
||||||
'enabled' => true,
|
'enabled' => true,
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|||||||
@ -5,7 +5,7 @@ return [
|
|||||||
'policy_id' => 'public-web-egress',
|
'policy_id' => 'public-web-egress',
|
||||||
'users' => ['normal-user', 'admin', 'devops'],
|
'users' => ['normal-user', 'admin', 'devops'],
|
||||||
'target_hosts' => ['*'],
|
'target_hosts' => ['*'],
|
||||||
'target_ports' => [80, 443, '3000-60000'],
|
'target_ports' => [80, 443],
|
||||||
'protocol' => 'tcp',
|
'protocol' => 'tcp',
|
||||||
'route_type' => 'direct',
|
'route_type' => 'direct',
|
||||||
'enabled' => true,
|
'enabled' => true,
|
||||||
|
|||||||
68
contract.md
68
contract.md
@ -151,15 +151,6 @@ Completed in this checkpoint:
|
|||||||
* `chacha20` currently uses libsodium XChaCha20 stream encryption with a random nonce per frame.
|
* `chacha20` currently uses libsodium XChaCha20 stream encryption with a random nonce per frame.
|
||||||
* Verified `none` and `chacha20` FrameCodec encode/decode round trips.
|
* Verified `none` and `chacha20` FrameCodec encode/decode round trips.
|
||||||
* Verified POP Server starts with `LAYLINK_FRAME_ENCRYPTION=chacha20`.
|
* Verified POP Server starts with `LAYLINK_FRAME_ENCRYPTION=chacha20`.
|
||||||
* Added port range matching for policy and agent allowlist ports:
|
|
||||||
* `target_ports` supports exact ports such as `80` and string ranges such as `'8080-10080'`.
|
|
||||||
* `allowed_ports` supports the same syntax.
|
|
||||||
* Allowed sample public TCP egress policy on port range `'8080-10080'` for HTTP-alt/speedtest style endpoints.
|
|
||||||
* Optimized TCP stream `DATA` frames:
|
|
||||||
* Control frames remain JSON.
|
|
||||||
* TCP `DATA` payloads now use binary frame encoding when both ends run the updated code.
|
|
||||||
* This removes base64 expansion and JSON string encoding from the hot TCP data path.
|
|
||||||
* Verified binary TCP `DATA` frame encode/decode under both `none` and `chacha20`.
|
|
||||||
|
|
||||||
Known MVP limitations:
|
Known MVP limitations:
|
||||||
|
|
||||||
@ -173,7 +164,6 @@ Known MVP limitations:
|
|||||||
* No TLS yet.
|
* No TLS yet.
|
||||||
* No production-grade client identity yet; `dev-token` is hardcoded for MVP development.
|
* No production-grade client identity yet; `dev-token` is hardcoded for MVP development.
|
||||||
* No automated integration test harness yet.
|
* No automated integration test harness yet.
|
||||||
* TCP stream forwarding is still single Agent-to-POP connection based; binary `DATA` frames reduce per-byte overhead, but KCP/multipath/parallel transport and flow-control tuning are still future performance work.
|
|
||||||
* No explicit idle timeout or connect timeout enforcement yet.
|
* No explicit idle timeout or connect timeout enforcement yet.
|
||||||
* UDP relay is datagram-oriented and currently creates short-lived POP-side UDP sockets per outbound datagram; pooling and stronger timeout accounting are still future work.
|
* UDP relay is datagram-oriented and currently creates short-lived POP-side UDP sockets per outbound datagram; pooling and stronger timeout accounting are still future work.
|
||||||
* HTTP proxy supports `CONNECT` and ordinary absolute URL HTTP requests; advanced proxy auth and full HTTP/2 proxying are not implemented.
|
* HTTP proxy supports `CONNECT` and ordinary absolute URL HTTP requests; advanced proxy auth and full HTTP/2 proxying are not implemented.
|
||||||
@ -185,12 +175,10 @@ Next recommended tasks:
|
|||||||
3. Add target connect timeout and session idle timeout.
|
3. Add target connect timeout and session idle timeout.
|
||||||
4. Add buffer full/drain handling with audit result `buffer_overflow`.
|
4. Add buffer full/drain handling with audit result `buffer_overflow`.
|
||||||
5. Add README quickstart with exact local commands.
|
5. Add README quickstart with exact local commands.
|
||||||
6. Add a reproducible throughput benchmark script for direct-vs-LayLink comparisons.
|
6. Optimize UDP relay with POP-side UDP socket pooling.
|
||||||
7. Add KCP or another UDP-based reliable transport behind the transport abstraction.
|
7. Add UDP association idle timeouts and cleanup.
|
||||||
8. Optimize UDP relay with POP-side UDP socket pooling.
|
8. Aggregate UDP audit records per association instead of per datagram.
|
||||||
9. Add UDP association idle timeouts and cleanup.
|
9. Add UDP and per-user rate limiting.
|
||||||
10. Aggregate UDP audit records per association instead of per datagram.
|
|
||||||
11. Add UDP and per-user rate limiting.
|
|
||||||
|
|
||||||
## 0. Project Name
|
## 0. Project Name
|
||||||
|
|
||||||
@ -433,7 +421,7 @@ Required frame types:
|
|||||||
| `OPEN` | Client Agent -> POP | Client Agent requests POP to authorize and open a target stream. |
|
| `OPEN` | Client Agent -> POP | Client Agent requests POP to authorize and open a target stream. |
|
||||||
| `OPEN_OK` | POP -> Client Agent | POP has connected the target and the stream can begin. |
|
| `OPEN_OK` | POP -> Client Agent | POP has connected the target and the stream can begin. |
|
||||||
| `OPEN_FAIL` | POP -> Client Agent | POP rejected or failed the requested target stream. |
|
| `OPEN_FAIL` | POP -> Client Agent | POP rejected or failed the requested target stream. |
|
||||||
| `DATA` | Bidirectional | Stream bytes for one `session_id`; TCP stream payloads use binary frame encoding when both ends are updated. |
|
| `DATA` | Bidirectional | Stream bytes for one `session_id`; MVP payload uses base64. |
|
||||||
| `UDP_DATA` | Bidirectional | UDP datagram bytes for one UDP association; MVP payload uses base64 and includes target metadata. |
|
| `UDP_DATA` | Bidirectional | UDP datagram bytes for one UDP association; MVP payload uses base64 and includes target metadata. |
|
||||||
| `CLOSE` | Bidirectional | Close one stream session. |
|
| `CLOSE` | Bidirectional | Close one stream session. |
|
||||||
| `ERROR` | Bidirectional | Explicit protocol or session error. |
|
| `ERROR` | Bidirectional | Explicit protocol or session error. |
|
||||||
@ -459,11 +447,13 @@ payload_length
|
|||||||
payload
|
payload
|
||||||
```
|
```
|
||||||
|
|
||||||
Control frames use JSON payloads. TCP stream `DATA` frames may use the binary DATA encoding below.
|
Suggested JSON payload for MVP is acceptable.
|
||||||
|
|
||||||
|
Binary optimization may be added later.
|
||||||
|
|
||||||
### 6.3 Frame Encoding
|
### 6.3 Frame Encoding
|
||||||
|
|
||||||
For control frames, use length-prefixed JSON frames.
|
For MVP, use length-prefixed JSON frames.
|
||||||
|
|
||||||
Format:
|
Format:
|
||||||
|
|
||||||
@ -472,33 +462,20 @@ uint32_be length
|
|||||||
json_payload
|
json_payload
|
||||||
```
|
```
|
||||||
|
|
||||||
Example decoded control frame:
|
Example decoded frame:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"type": "OPEN",
|
"type": "DATA",
|
||||||
"session_id": "018f6f4a-xxxx-xxxx",
|
"session_id": "018f6f4a-xxxx-xxxx",
|
||||||
"payload": {
|
"payload": "base64-encoded-binary"
|
||||||
"target_host": "example.com",
|
|
||||||
"target_port": 443,
|
|
||||||
"protocol": "tcp"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
TCP stream `DATA` frames use a binary body before optional encryption:
|
For `DATA` frames, binary stream data may be base64 encoded in MVP.
|
||||||
|
|
||||||
```text
|
A later version may replace this with binary headers plus raw binary body.
|
||||||
uint32_be encrypted_or_plain_body_length
|
|
||||||
"LLB1"
|
|
||||||
uint8 binary_type=1
|
|
||||||
uint16_be session_id_length
|
|
||||||
session_id bytes
|
|
||||||
raw TCP payload bytes
|
|
||||||
```
|
|
||||||
|
|
||||||
Legacy JSON/base64 `DATA` decoding remains accepted for compatibility, but updated senders should emit binary `DATA`.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -681,7 +658,7 @@ return [
|
|||||||
'policy_id' => 'public-web-egress',
|
'policy_id' => 'public-web-egress',
|
||||||
'users' => ['normal-user', 'admin'],
|
'users' => ['normal-user', 'admin'],
|
||||||
'target_hosts' => ['*'],
|
'target_hosts' => ['*'],
|
||||||
'target_ports' => [80, 443, '8080-10080'],
|
'target_ports' => [80, 443],
|
||||||
'route_type' => 'direct',
|
'route_type' => 'direct',
|
||||||
'enabled' => true,
|
'enabled' => true,
|
||||||
],
|
],
|
||||||
@ -719,7 +696,7 @@ return [
|
|||||||
'192.168.0.0/16',
|
'192.168.0.0/16',
|
||||||
'10.10.0.0/16',
|
'10.10.0.0/16',
|
||||||
],
|
],
|
||||||
'allowed_ports' => [22, 80, 443, '8080-10080', 3306, 5432],
|
'allowed_ports' => [22, 80, 443, 3306, 5432],
|
||||||
'enabled' => true,
|
'enabled' => true,
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
@ -796,7 +773,18 @@ On failure:
|
|||||||
|
|
||||||
After `OPEN_OK`, data is exchanged with `DATA` frames.
|
After `OPEN_OK`, data is exchanged with `DATA` frames.
|
||||||
|
|
||||||
Updated implementations encode TCP `DATA` as the binary frame described in section 6.3. Legacy JSON/base64 `DATA` frames may still be decoded during rolling upgrades.
|
Example:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"type": "DATA",
|
||||||
|
"session_id": "018f6f4a-xxxx",
|
||||||
|
"payload": {
|
||||||
|
"data": "base64-encoded-binary"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
Both POP Server and Agent must map `session_id` to the corresponding local TCP connection.
|
Both POP Server and Agent must map `session_id` to the corresponding local TCP connection.
|
||||||
|
|
||||||
|
|||||||
@ -1,9 +0,0 @@
|
|||||||
当前仍无法实现chacha20加密,在开启chacha20之后,
|
|
||||||
Using SOCKS5 proxy: socks5h://127.0.0.1:21080
|
|
||||||
[1/2] HTTPS connectivity: https://bing.com/
|
|
||||||
* Trying 127.0.0.1:21080...
|
|
||||||
* SOCKS5 connect to bing.com:443 (remotely resolved)
|
|
||||||
* Can't complete SOCKS5 connection to bing.com. (1)
|
|
||||||
* Closing connection 0
|
|
||||||
curl: (97) Can't complete SOCKS5 connection to bing.com. (1)
|
|
||||||
ERR bing_request_failed status=97
|
|
||||||
15
readme.md
15
readme.md
@ -181,7 +181,7 @@ Client Agent 的节点身份不是只写在 `.env` 中,POP Server 侧还必须
|
|||||||
'192.168.0.0/16',
|
'192.168.0.0/16',
|
||||||
'10.10.0.0/16',
|
'10.10.0.0/16',
|
||||||
],
|
],
|
||||||
'allowed_ports' => [22, 80, 443, '8080-10080', 3306, 5432],
|
'allowed_ports' => [22, 80, 443, 3306, 5432],
|
||||||
'enabled' => true,
|
'enabled' => true,
|
||||||
],
|
],
|
||||||
```
|
```
|
||||||
@ -262,7 +262,7 @@ CLIENT_AGENT_SOCKS5_PASSWORD=change-this-password
|
|||||||
| `auth_token` | 客户端认证 token。当前 MVP 固定为 `dev-token`。 | `dev-token` |
|
| `auth_token` | 客户端认证 token。当前 MVP 固定为 `dev-token`。 | `dev-token` |
|
||||||
| `user_id` | 用户身份。POP Server 会用它匹配 `config/policies.php`。 | `admin`、`devops`、`normal-user` |
|
| `user_id` | 用户身份。POP Server 会用它匹配 `config/policies.php`。 | `admin`、`devops`、`normal-user` |
|
||||||
| `target_host` | 目标主机。 | `192.168.10.20`、`example.com` |
|
| `target_host` | 目标主机。 | `192.168.10.20`、`example.com` |
|
||||||
| `target_port` | 目标端口。 | `22`、`80`、`443`、`8080`、`5432` |
|
| `target_port` | 目标端口。 | `22`、`80`、`443`、`5432` |
|
||||||
| `protocol` | 目标协议。当前只支持 TCP。 | `tcp` |
|
| `protocol` | 目标协议。当前只支持 TCP。 | `tcp` |
|
||||||
| `route_hint` | 预留字段。新的最小路径由 POP Server 直连公网目标,通常不需要填写。 | `null` |
|
| `route_hint` | 预留字段。新的最小路径由 POP Server 直连公网目标,通常不需要填写。 | `null` |
|
||||||
|
|
||||||
@ -277,7 +277,7 @@ CLIENT_AGENT_SOCKS5_PASSWORD=change-this-password
|
|||||||
'policy_id' => 'public-web-egress',
|
'policy_id' => 'public-web-egress',
|
||||||
'users' => ['normal-user', 'admin', 'devops'],
|
'users' => ['normal-user', 'admin', 'devops'],
|
||||||
'target_hosts' => ['*'],
|
'target_hosts' => ['*'],
|
||||||
'target_ports' => [80, 443, '8080-10080'],
|
'target_ports' => [80, 443],
|
||||||
'protocol' => 'tcp',
|
'protocol' => 'tcp',
|
||||||
'route_type' => 'direct',
|
'route_type' => 'direct',
|
||||||
'enabled' => true,
|
'enabled' => true,
|
||||||
@ -286,15 +286,10 @@ CLIENT_AGENT_SOCKS5_PASSWORD=change-this-password
|
|||||||
|
|
||||||
这条策略表示:
|
这条策略表示:
|
||||||
|
|
||||||
* `normal-user`、`admin` 和 `devops` 可以访问任意主机的 `80`、`443`,以及 `8080` 到 `10080` 端口。
|
* `normal-user`、`admin` 和 `devops` 可以访问任意主机的 `80`、`443` 端口。
|
||||||
* Client Agent 只负责把请求封装成 Frame 发到 POP Server。
|
* Client Agent 只负责把请求封装成 Frame 发到 POP Server。
|
||||||
* POP Server 校验策略后直接连接公网目标。
|
* POP Server 校验策略后直接连接公网目标。
|
||||||
|
|
||||||
`target_ports` 和 `allowed_ports` 都支持两种写法:
|
|
||||||
|
|
||||||
* 单端口:`80`
|
|
||||||
* 端口范围:`'8080-10080'`
|
|
||||||
|
|
||||||
路由类型:
|
路由类型:
|
||||||
|
|
||||||
| `route_type` | 作用 |
|
| `route_type` | 作用 |
|
||||||
@ -378,8 +373,6 @@ php bin/client-agent.php start
|
|||||||
|
|
||||||
然后把应用的代理设置为 SOCKS5 `127.0.0.1:1080`。Client Agent 会解析 SOCKS5 `CONNECT`,封装成 `OPEN` 帧发给 POP Server;POP Server 校验通过后直连公网目标,随后通过 `DATA` 帧转发原始 TCP 数据。
|
然后把应用的代理设置为 SOCKS5 `127.0.0.1:1080`。Client Agent 会解析 SOCKS5 `CONNECT`,封装成 `OPEN` 帧发给 POP Server;POP Server 校验通过后直连公网目标,随后通过 `DATA` 帧转发原始 TCP 数据。
|
||||||
|
|
||||||
TCP 大流量 `DATA` 帧使用二进制帧编码;`AUTH`、`OPEN`、`CLOSE`、`ERROR` 等控制帧仍使用 JSON 编码。启用 `chacha20` 时,二进制和 JSON Frame body 都会被加密。
|
|
||||||
|
|
||||||
验证 SOCKS5 HTTPS 联通性和出口 IP:
|
验证 SOCKS5 HTTPS 联通性和出口 IP:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
63
runtime/workerman.log
Normal file
63
runtime/workerman.log
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
2026-05-28 10:19:07 pid:478270 Workerman[pop-server.php] start in DEBUG mode
|
||||||
|
2026-05-28 10:19:09 pid:478270 Workerman[pop-server.php] received signal SIGTERM
|
||||||
|
2026-05-28 10:19:09 pid:478270 Workerman[pop-server.php] stopping
|
||||||
|
2026-05-28 10:19:09 pid:478270 Workerman[pop-server.php] has been stopped
|
||||||
|
2026-05-28 11:13:28 pid:481004 Workerman[client-agent.php] start in DEBUG mode
|
||||||
|
2026-05-28 11:13:30 pid:481004 Workerman[client-agent.php] received signal SIGTERM
|
||||||
|
2026-05-28 11:13:30 pid:481004 Workerman[client-agent.php] stopping
|
||||||
|
2026-05-28 11:13:30 pid:481004 Workerman[client-agent.php] has been stopped
|
||||||
|
2026-05-28 11:21:31 pid:482154 Workerman[client-agent.php] start in DEBUG mode
|
||||||
|
2026-05-28 11:21:32 pid:482187 Workerman[pop-server.php] start in DEBUG mode
|
||||||
|
2026-05-28 11:21:33 pid:482154 Workerman[client-agent.php] received signal SIGTERM
|
||||||
|
2026-05-28 11:21:33 pid:482154 Workerman[client-agent.php] stopping
|
||||||
|
2026-05-28 11:21:33 pid:482154 Workerman[client-agent.php] has been stopped
|
||||||
|
2026-05-28 11:21:34 pid:482187 Workerman[pop-server.php] received signal SIGTERM
|
||||||
|
2026-05-28 11:21:34 pid:482187 Workerman[pop-server.php] stopping
|
||||||
|
2026-05-28 11:21:34 pid:482187 Workerman[pop-server.php] has been stopped
|
||||||
|
2026-05-28 11:32:55 pid:484077 Workerman[client-agent.php] start in DEBUG mode
|
||||||
|
2026-05-28 11:32:57 pid:484077 Workerman[client-agent.php] received signal SIGTERM
|
||||||
|
2026-05-28 11:32:57 pid:484077 Workerman[client-agent.php] stopping
|
||||||
|
2026-05-28 11:32:57 pid:484077 Workerman[client-agent.php] has been stopped
|
||||||
|
2026-05-28 11:34:04 pid:484561 Workerman[client-agent.php] start in DEBUG mode
|
||||||
|
2026-05-28 11:34:06 pid:484561 Workerman[client-agent.php] received signal SIGTERM
|
||||||
|
2026-05-28 11:34:06 pid:484561 Workerman[client-agent.php] stopping
|
||||||
|
2026-05-28 11:34:06 pid:484561 Workerman[client-agent.php] has been stopped
|
||||||
|
2026-05-28 11:36:48 pid:485045 Workerman[client-agent.php] start in DEBUG mode
|
||||||
|
2026-05-28 11:36:50 pid:485045 Workerman[client-agent.php] received signal SIGTERM
|
||||||
|
2026-05-28 11:36:50 pid:485045 Workerman[client-agent.php] stopping
|
||||||
|
2026-05-28 11:36:50 pid:485045 Workerman[client-agent.php] has been stopped
|
||||||
|
2026-05-28 11:41:26 pid:486188 Workerman[client-agent.php] start in DEBUG mode
|
||||||
|
2026-05-28 11:41:28 pid:486188 Workerman[client-agent.php] received signal SIGTERM
|
||||||
|
2026-05-28 11:41:28 pid:486188 Workerman[client-agent.php] stopping
|
||||||
|
2026-05-28 11:41:28 pid:486188 Workerman[client-agent.php] has been stopped
|
||||||
|
2026-05-28 12:08:09 pid:489103 Workerman[pop-server.php] start in DEBUG mode
|
||||||
|
2026-05-28 12:08:11 pid:489103 Workerman[pop-server.php] received signal SIGTERM
|
||||||
|
2026-05-28 12:08:11 pid:489103 Workerman[pop-server.php] stopping
|
||||||
|
2026-05-28 12:08:11 pid:489103 Workerman[pop-server.php] has been stopped
|
||||||
|
2026-05-28 12:28:14 pid:489644 Workerman[client-agent.php] start in DEBUG mode
|
||||||
|
2026-05-28 12:34:47 pid:490774 Workerman[client-agent.php] restart
|
||||||
|
2026-05-28 12:34:47 pid:490774 Workerman[client-agent.php] is stopping ...
|
||||||
|
2026-05-28 12:34:47 pid:489644 Workerman[client-agent.php] received signal SIGINT
|
||||||
|
2026-05-28 12:34:47 pid:489644 Workerman[client-agent.php] stopping
|
||||||
|
2026-05-28 12:34:48 pid:489644 Workerman[client-agent.php] has been stopped
|
||||||
|
2026-05-28 12:34:48 pid:490774 Workerman[client-agent.php] stop success
|
||||||
|
2026-05-28 12:35:17 pid:490876 Workerman[client-agent.php] stop
|
||||||
|
2026-05-28 12:35:17 pid:490876 Workerman[client-agent.php] is stopping ...
|
||||||
|
2026-05-28 12:35:17 pid:490774 Workerman[client-agent.php] received signal SIGINT
|
||||||
|
2026-05-28 12:35:17 pid:490774 Workerman[client-agent.php] stopping
|
||||||
|
2026-05-28 12:35:19 pid:490774 Workerman[client-agent.php] has been stopped
|
||||||
|
2026-05-28 12:35:19 pid:490876 Workerman[client-agent.php] stop success
|
||||||
|
2026-05-28 12:35:19 pid:490845 Workerman[client-agent.php] start in DAEMON mode
|
||||||
|
2026-05-28 12:40:44 pid:491096 Workerman[client-agent.php] restart
|
||||||
|
2026-05-28 12:40:44 pid:491096 Workerman[client-agent.php] is stopping ...
|
||||||
|
2026-05-28 12:40:44 pid:490878 Workerman[client-agent.php] received signal SIGINT
|
||||||
|
2026-05-28 12:40:44 pid:490878 Workerman[client-agent.php] stopping
|
||||||
|
2026-05-28 12:40:44 pid:490878 Workerman[client-agent.php] has been stopped
|
||||||
|
2026-05-28 12:40:44 pid:491096 Workerman[client-agent.php] stop success
|
||||||
|
2026-05-28 13:04:58 pid:492360 Workerman[pop-server.php] start in DEBUG mode
|
||||||
|
2026-05-28 13:05:00 pid:492360 Workerman[pop-server.php] received signal SIGTERM
|
||||||
|
2026-05-28 13:05:00 pid:492360 Workerman[pop-server.php] stopping
|
||||||
|
2026-05-28 13:05:00 pid:492360 Workerman[pop-server.php] has been stopped
|
||||||
|
2026-05-28 13:07:21 pid:491096 Workerman[client-agent.php] received signal SIGINT
|
||||||
|
2026-05-28 13:07:21 pid:491096 Workerman[client-agent.php] stopping
|
||||||
|
2026-05-28 13:07:21 pid:491096 Workerman[client-agent.php] has been stopped
|
||||||
@ -24,7 +24,7 @@ echo "Using SOCKS5 proxy: ${proxy}"
|
|||||||
echo
|
echo
|
||||||
echo "[1/2] HTTPS connectivity: https://bing.com/"
|
echo "[1/2] HTTPS connectivity: https://bing.com/"
|
||||||
bing_code="$(
|
bing_code="$(
|
||||||
curl -vvvv \
|
curl \
|
||||||
--silent \
|
--silent \
|
||||||
--show-error \
|
--show-error \
|
||||||
--location \
|
--location \
|
||||||
|
|||||||
@ -162,7 +162,7 @@ final class AgentClient
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->send(new Frame(FrameType::DATA, $sessionId, ['data_raw' => $data]));
|
$this->send(new Frame(FrameType::DATA, $sessionId, ['data' => base64_encode($data)]));
|
||||||
}
|
}
|
||||||
|
|
||||||
private function handleInitialRequest(TcpConnection $connection, string $data): void
|
private function handleInitialRequest(TcpConnection $connection, string $data): void
|
||||||
@ -544,7 +544,7 @@ final class AgentClient
|
|||||||
$pending = $this->pendingData[$frame->sessionId] ?? '';
|
$pending = $this->pendingData[$frame->sessionId] ?? '';
|
||||||
unset($this->pendingData[$frame->sessionId]);
|
unset($this->pendingData[$frame->sessionId]);
|
||||||
if ($pending !== '') {
|
if ($pending !== '') {
|
||||||
$this->send(new Frame(FrameType::DATA, $frame->sessionId, ['data_raw' => $pending]));
|
$this->send(new Frame(FrameType::DATA, $frame->sessionId, ['data' => base64_encode($pending)]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -573,7 +573,7 @@ final class AgentClient
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$data = $this->frameData($frame);
|
$data = base64_decode((string)($frame->payload['data'] ?? ''), true);
|
||||||
if ($data === false) {
|
if ($data === false) {
|
||||||
$this->send(new Frame(FrameType::ERROR, $frame->sessionId, ['reason' => 'invalid_frame']));
|
$this->send(new Frame(FrameType::ERROR, $frame->sessionId, ['reason' => 'invalid_frame']));
|
||||||
return;
|
return;
|
||||||
@ -582,15 +582,6 @@ final class AgentClient
|
|||||||
$this->clients[$frame->sessionId]->send($data);
|
$this->clients[$frame->sessionId]->send($data);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function frameData(Frame $frame): string|false
|
|
||||||
{
|
|
||||||
if (isset($frame->payload['data_raw']) && is_string($frame->payload['data_raw'])) {
|
|
||||||
return $frame->payload['data_raw'];
|
|
||||||
}
|
|
||||||
|
|
||||||
return base64_decode((string)($frame->payload['data'] ?? ''), true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function onClientClose(TcpConnection $connection): void
|
private function onClientClose(TcpConnection $connection): void
|
||||||
{
|
{
|
||||||
unset($this->initialBuffers[$connection->id]);
|
unset($this->initialBuffers[$connection->id]);
|
||||||
|
|||||||
@ -5,7 +5,6 @@ declare(strict_types=1);
|
|||||||
namespace LayLink\Agent;
|
namespace LayLink\Agent;
|
||||||
|
|
||||||
use LayLink\Auth\PolicyChecker;
|
use LayLink\Auth\PolicyChecker;
|
||||||
use LayLink\Auth\PortMatcher;
|
|
||||||
|
|
||||||
final class TargetConnector
|
final class TargetConnector
|
||||||
{
|
{
|
||||||
@ -15,7 +14,7 @@ final class TargetConnector
|
|||||||
|
|
||||||
public function isAllowed(string $host, int $port): bool
|
public function isAllowed(string $host, int $port): bool
|
||||||
{
|
{
|
||||||
if (!PortMatcher::matches($port, $this->nodeConfig['allowed_ports'] ?? [])) {
|
if (!in_array($port, $this->nodeConfig['allowed_ports'] ?? [], true)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -22,7 +22,7 @@ final class PolicyChecker
|
|||||||
if (($policy['protocol'] ?? 'tcp') !== $protocol) {
|
if (($policy['protocol'] ?? 'tcp') !== $protocol) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!PortMatcher::matches($port, $policy['target_ports'] ?? [])) {
|
if (!in_array($port, $policy['target_ports'] ?? [], true)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!$this->hostMatches($host, $policy['target_hosts'] ?? [])) {
|
if (!$this->hostMatches($host, $policy['target_hosts'] ?? [])) {
|
||||||
|
|||||||
@ -1,47 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace LayLink\Auth;
|
|
||||||
|
|
||||||
final class PortMatcher
|
|
||||||
{
|
|
||||||
public static function matches(int $port, array $rules): bool
|
|
||||||
{
|
|
||||||
foreach ($rules as $rule) {
|
|
||||||
if (is_int($rule) && $rule === $port) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (is_string($rule) && self::stringRuleMatches($port, $rule)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static function stringRuleMatches(int $port, string $rule): bool
|
|
||||||
{
|
|
||||||
$rule = trim($rule);
|
|
||||||
if ($rule === '') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ctype_digit($rule)) {
|
|
||||||
return (int)$rule === $port;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!preg_match('/^(\d{1,5})\s*-\s*(\d{1,5})$/', $rule, $matches)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$start = (int)$matches[1];
|
|
||||||
$end = (int)$matches[2];
|
|
||||||
if ($start < 1 || $end > 65535 || $start > $end) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $port >= $start && $port <= $end;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -9,8 +9,6 @@ use InvalidArgumentException;
|
|||||||
final class FrameCodec
|
final class FrameCodec
|
||||||
{
|
{
|
||||||
public const MAX_FRAME_LENGTH = 16 * 1024 * 1024;
|
public const MAX_FRAME_LENGTH = 16 * 1024 * 1024;
|
||||||
private const BINARY_MAGIC = "LLB1";
|
|
||||||
private const BINARY_TYPE_DATA = 1;
|
|
||||||
private const ENCRYPTION_NONE = 'none';
|
private const ENCRYPTION_NONE = 'none';
|
||||||
private const ENCRYPTION_CHACHA20 = 'chacha20';
|
private const ENCRYPTION_CHACHA20 = 'chacha20';
|
||||||
|
|
||||||
@ -42,19 +40,20 @@ final class FrameCodec
|
|||||||
|
|
||||||
public static function encode(Frame $frame): string
|
public static function encode(Frame $frame): string
|
||||||
{
|
{
|
||||||
$body = self::encrypt(self::serializeFrame($frame));
|
$json = json_encode($frame->toArray(), JSON_UNESCAPED_SLASHES);
|
||||||
|
if ($json === false) {
|
||||||
|
throw new InvalidArgumentException('invalid_frame_payload');
|
||||||
|
}
|
||||||
|
|
||||||
|
$body = self::encrypt($json);
|
||||||
|
|
||||||
return pack('N', strlen($body)) . $body;
|
return pack('N', strlen($body)) . $body;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function decode(string $body): Frame
|
public static function decode(string $body): Frame
|
||||||
{
|
{
|
||||||
$plaintext = self::decrypt($body);
|
$json = self::decrypt($body);
|
||||||
if (str_starts_with($plaintext, self::BINARY_MAGIC)) {
|
$data = json_decode($json, true);
|
||||||
return self::decodeBinaryFrame($plaintext);
|
|
||||||
}
|
|
||||||
|
|
||||||
$data = json_decode($plaintext, true);
|
|
||||||
if (!is_array($data) || !isset($data['type']) || !is_string($data['type'])) {
|
if (!is_array($data) || !isset($data['type']) || !is_string($data['type'])) {
|
||||||
throw new InvalidArgumentException('invalid_frame');
|
throw new InvalidArgumentException('invalid_frame');
|
||||||
}
|
}
|
||||||
@ -77,49 +76,6 @@ final class FrameCodec
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function serializeFrame(Frame $frame): string
|
|
||||||
{
|
|
||||||
if ($frame->type === FrameType::DATA && isset($frame->payload['data_raw']) && is_string($frame->payload['data_raw'])) {
|
|
||||||
if ($frame->sessionId === null || strlen($frame->sessionId) > 65535) {
|
|
||||||
throw new InvalidArgumentException('invalid_session_id');
|
|
||||||
}
|
|
||||||
|
|
||||||
return self::BINARY_MAGIC
|
|
||||||
. chr(self::BINARY_TYPE_DATA)
|
|
||||||
. pack('n', strlen($frame->sessionId))
|
|
||||||
. $frame->sessionId
|
|
||||||
. $frame->payload['data_raw'];
|
|
||||||
}
|
|
||||||
|
|
||||||
$json = json_encode($frame->toArray(), JSON_UNESCAPED_SLASHES);
|
|
||||||
if ($json === false) {
|
|
||||||
throw new InvalidArgumentException('invalid_frame_payload');
|
|
||||||
}
|
|
||||||
|
|
||||||
return $json;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static function decodeBinaryFrame(string $plaintext): Frame
|
|
||||||
{
|
|
||||||
if (strlen($plaintext) < 7) {
|
|
||||||
throw new InvalidArgumentException('invalid_binary_frame');
|
|
||||||
}
|
|
||||||
|
|
||||||
$type = ord($plaintext[4]);
|
|
||||||
$sessionLength = unpack('n', substr($plaintext, 5, 2))[1];
|
|
||||||
if (strlen($plaintext) < 7 + $sessionLength) {
|
|
||||||
throw new InvalidArgumentException('invalid_binary_frame');
|
|
||||||
}
|
|
||||||
|
|
||||||
$sessionId = substr($plaintext, 7, $sessionLength);
|
|
||||||
$data = substr($plaintext, 7 + $sessionLength);
|
|
||||||
|
|
||||||
return match ($type) {
|
|
||||||
self::BINARY_TYPE_DATA => new Frame(FrameType::DATA, $sessionId, ['data_raw' => $data]),
|
|
||||||
default => throw new InvalidArgumentException('unsupported_binary_frame'),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private static function encrypt(string $plaintext): string
|
private static function encrypt(string $plaintext): string
|
||||||
{
|
{
|
||||||
if (self::$encryption === self::ENCRYPTION_NONE) {
|
if (self::$encryption === self::ENCRYPTION_NONE) {
|
||||||
|
|||||||
@ -34,7 +34,7 @@ final class FrameType
|
|||||||
self::OPEN => 'Client Agent asks POP Server to open an authorized target stream.',
|
self::OPEN => 'Client Agent asks POP Server to open an authorized target stream.',
|
||||||
self::OPEN_OK => 'POP Server confirms target stream is open.',
|
self::OPEN_OK => 'POP Server confirms target stream is open.',
|
||||||
self::OPEN_FAIL => 'POP Server rejects or fails target stream opening.',
|
self::OPEN_FAIL => 'POP Server rejects or fails target stream opening.',
|
||||||
self::DATA => 'Bidirectional stream bytes; TCP stream DATA uses binary frame encoding when both ends are updated.',
|
self::DATA => 'Bidirectional stream bytes, base64 encoded in MVP.',
|
||||||
self::UDP_DATA => 'Bidirectional UDP datagram relay, base64 encoded in MVP.',
|
self::UDP_DATA => 'Bidirectional UDP datagram relay, base64 encoded in MVP.',
|
||||||
self::CLOSE => 'Either side closes a stream session.',
|
self::CLOSE => 'Either side closes a stream session.',
|
||||||
self::ERROR => 'Explicit protocol or session error.',
|
self::ERROR => 'Explicit protocol or session error.',
|
||||||
|
|||||||
@ -123,7 +123,7 @@ final class AgentListener
|
|||||||
}
|
}
|
||||||
|
|
||||||
match ($frame->type) {
|
match ($frame->type) {
|
||||||
FrameType::DATA => $this->forwardDataToTarget($session, $frame),
|
FrameType::DATA => $this->forwardDataToTarget($session, (string)($frame->payload['data'] ?? '')),
|
||||||
FrameType::CLOSE => $this->closeSession($session, 'closed', null),
|
FrameType::CLOSE => $this->closeSession($session, 'closed', null),
|
||||||
default => null,
|
default => null,
|
||||||
};
|
};
|
||||||
@ -233,7 +233,7 @@ final class AgentListener
|
|||||||
$target->onMessage = function (AsyncTcpConnection $target, string $data) use ($session, $agentConnection): void {
|
$target->onMessage = function (AsyncTcpConnection $target, string $data) use ($session, $agentConnection): void {
|
||||||
$session->bytesTargetToClient += strlen($data);
|
$session->bytesTargetToClient += strlen($data);
|
||||||
$this->send($agentConnection, new Frame(FrameType::DATA, $session->sessionId, [
|
$this->send($agentConnection, new Frame(FrameType::DATA, $session->sessionId, [
|
||||||
'data_raw' => $data,
|
'data' => base64_encode($data),
|
||||||
]));
|
]));
|
||||||
};
|
};
|
||||||
$target->onClose = fn () => $this->closeSession($session, 'closed', null);
|
$target->onClose = fn () => $this->closeSession($session, 'closed', null);
|
||||||
@ -241,9 +241,9 @@ final class AgentListener
|
|||||||
$target->connect();
|
$target->connect();
|
||||||
}
|
}
|
||||||
|
|
||||||
private function forwardDataToTarget(TunnelSession $session, Frame $frame): void
|
private function forwardDataToTarget(TunnelSession $session, string $encoded): void
|
||||||
{
|
{
|
||||||
$data = $this->frameData($frame);
|
$data = base64_decode($encoded, true);
|
||||||
if ($data === false) {
|
if ($data === false) {
|
||||||
$this->closeSession($session, 'failed', 'invalid_frame');
|
$this->closeSession($session, 'failed', 'invalid_frame');
|
||||||
return;
|
return;
|
||||||
@ -259,15 +259,6 @@ final class AgentListener
|
|||||||
$this->closeSession($session, 'failed', $reason);
|
$this->closeSession($session, 'failed', $reason);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function frameData(Frame $frame): string|false
|
|
||||||
{
|
|
||||||
if (isset($frame->payload['data_raw']) && is_string($frame->payload['data_raw'])) {
|
|
||||||
return $frame->payload['data_raw'];
|
|
||||||
}
|
|
||||||
|
|
||||||
return base64_decode((string)($frame->payload['data'] ?? ''), true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function rejectOpen(TcpConnection $agentConnection, Frame $frame, string $reason, string $userId, string $nodeId, ?string $policyId = null): void
|
private function rejectOpen(TcpConnection $agentConnection, Frame $frame, string $reason, string $userId, string $nodeId, ?string $policyId = null): void
|
||||||
{
|
{
|
||||||
$this->send($agentConnection, new Frame(FrameType::OPEN_FAIL, $frame->sessionId, ['reason' => $reason]));
|
$this->send($agentConnection, new Frame(FrameType::OPEN_FAIL, $frame->sessionId, ['reason' => $reason]));
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user