add more
This commit is contained in:
parent
f274a7dd4b
commit
ad1327bebd
@ -8,7 +8,7 @@ return [
|
||||
'192.168.0.0/16',
|
||||
'10.10.0.0/16',
|
||||
],
|
||||
'allowed_ports' => [22, 80, 443, 3306, 5432],
|
||||
'allowed_ports' => [22, 80, 443, '3000-60000', 3306, 5432],
|
||||
'enabled' => true,
|
||||
],
|
||||
];
|
||||
|
||||
@ -5,7 +5,7 @@ return [
|
||||
'policy_id' => 'public-web-egress',
|
||||
'users' => ['normal-user', 'admin', 'devops'],
|
||||
'target_hosts' => ['*'],
|
||||
'target_ports' => [80, 443],
|
||||
'target_ports' => [80, 443, '3000-60000'],
|
||||
'protocol' => 'tcp',
|
||||
'route_type' => 'direct',
|
||||
'enabled' => true,
|
||||
|
||||
68
contract.md
68
contract.md
@ -151,6 +151,15 @@ Completed in this checkpoint:
|
||||
* `chacha20` currently uses libsodium XChaCha20 stream encryption with a random nonce per frame.
|
||||
* Verified `none` and `chacha20` FrameCodec encode/decode round trips.
|
||||
* 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:
|
||||
|
||||
@ -164,6 +173,7 @@ Known MVP limitations:
|
||||
* No TLS yet.
|
||||
* No production-grade client identity yet; `dev-token` is hardcoded for MVP development.
|
||||
* 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.
|
||||
* 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.
|
||||
@ -175,10 +185,12 @@ Next recommended tasks:
|
||||
3. Add target connect timeout and session idle timeout.
|
||||
4. Add buffer full/drain handling with audit result `buffer_overflow`.
|
||||
5. Add README quickstart with exact local commands.
|
||||
6. Optimize UDP relay with POP-side UDP socket pooling.
|
||||
7. Add UDP association idle timeouts and cleanup.
|
||||
8. Aggregate UDP audit records per association instead of per datagram.
|
||||
9. Add UDP and per-user rate limiting.
|
||||
6. Add a reproducible throughput benchmark script for direct-vs-LayLink comparisons.
|
||||
7. Add KCP or another UDP-based reliable transport behind the transport abstraction.
|
||||
8. Optimize UDP relay with POP-side UDP socket pooling.
|
||||
9. Add UDP association idle timeouts and cleanup.
|
||||
10. Aggregate UDP audit records per association instead of per datagram.
|
||||
11. Add UDP and per-user rate limiting.
|
||||
|
||||
## 0. Project Name
|
||||
|
||||
@ -421,7 +433,7 @@ Required frame types:
|
||||
| `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_FAIL` | POP -> Client Agent | POP rejected or failed the requested target stream. |
|
||||
| `DATA` | Bidirectional | Stream bytes for one `session_id`; MVP payload uses base64. |
|
||||
| `DATA` | Bidirectional | Stream bytes for one `session_id`; TCP stream payloads use binary frame encoding when both ends are updated. |
|
||||
| `UDP_DATA` | Bidirectional | UDP datagram bytes for one UDP association; MVP payload uses base64 and includes target metadata. |
|
||||
| `CLOSE` | Bidirectional | Close one stream session. |
|
||||
| `ERROR` | Bidirectional | Explicit protocol or session error. |
|
||||
@ -447,13 +459,11 @@ payload_length
|
||||
payload
|
||||
```
|
||||
|
||||
Suggested JSON payload for MVP is acceptable.
|
||||
|
||||
Binary optimization may be added later.
|
||||
Control frames use JSON payloads. TCP stream `DATA` frames may use the binary DATA encoding below.
|
||||
|
||||
### 6.3 Frame Encoding
|
||||
|
||||
For MVP, use length-prefixed JSON frames.
|
||||
For control frames, use length-prefixed JSON frames.
|
||||
|
||||
Format:
|
||||
|
||||
@ -462,20 +472,33 @@ uint32_be length
|
||||
json_payload
|
||||
```
|
||||
|
||||
Example decoded frame:
|
||||
Example decoded control frame:
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 1,
|
||||
"type": "DATA",
|
||||
"type": "OPEN",
|
||||
"session_id": "018f6f4a-xxxx-xxxx",
|
||||
"payload": "base64-encoded-binary"
|
||||
"payload": {
|
||||
"target_host": "example.com",
|
||||
"target_port": 443,
|
||||
"protocol": "tcp"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
For `DATA` frames, binary stream data may be base64 encoded in MVP.
|
||||
TCP stream `DATA` frames use a binary body before optional encryption:
|
||||
|
||||
A later version may replace this with binary headers plus raw binary body.
|
||||
```text
|
||||
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`.
|
||||
|
||||
---
|
||||
|
||||
@ -658,7 +681,7 @@ return [
|
||||
'policy_id' => 'public-web-egress',
|
||||
'users' => ['normal-user', 'admin'],
|
||||
'target_hosts' => ['*'],
|
||||
'target_ports' => [80, 443],
|
||||
'target_ports' => [80, 443, '8080-10080'],
|
||||
'route_type' => 'direct',
|
||||
'enabled' => true,
|
||||
],
|
||||
@ -696,7 +719,7 @@ return [
|
||||
'192.168.0.0/16',
|
||||
'10.10.0.0/16',
|
||||
],
|
||||
'allowed_ports' => [22, 80, 443, 3306, 5432],
|
||||
'allowed_ports' => [22, 80, 443, '8080-10080', 3306, 5432],
|
||||
'enabled' => true,
|
||||
],
|
||||
];
|
||||
@ -773,18 +796,7 @@ On failure:
|
||||
|
||||
After `OPEN_OK`, data is exchanged with `DATA` frames.
|
||||
|
||||
Example:
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 1,
|
||||
"type": "DATA",
|
||||
"session_id": "018f6f4a-xxxx",
|
||||
"payload": {
|
||||
"data": "base64-encoded-binary"
|
||||
}
|
||||
}
|
||||
```
|
||||
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.
|
||||
|
||||
Both POP Server and Agent must map `session_id` to the corresponding local TCP connection.
|
||||
|
||||
|
||||
9
problems.md
Normal file
9
problems.md
Normal file
@ -0,0 +1,9 @@
|
||||
当前仍无法实现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',
|
||||
'10.10.0.0/16',
|
||||
],
|
||||
'allowed_ports' => [22, 80, 443, 3306, 5432],
|
||||
'allowed_ports' => [22, 80, 443, '8080-10080', 3306, 5432],
|
||||
'enabled' => true,
|
||||
],
|
||||
```
|
||||
@ -262,7 +262,7 @@ CLIENT_AGENT_SOCKS5_PASSWORD=change-this-password
|
||||
| `auth_token` | 客户端认证 token。当前 MVP 固定为 `dev-token`。 | `dev-token` |
|
||||
| `user_id` | 用户身份。POP Server 会用它匹配 `config/policies.php`。 | `admin`、`devops`、`normal-user` |
|
||||
| `target_host` | 目标主机。 | `192.168.10.20`、`example.com` |
|
||||
| `target_port` | 目标端口。 | `22`、`80`、`443`、`5432` |
|
||||
| `target_port` | 目标端口。 | `22`、`80`、`443`、`8080`、`5432` |
|
||||
| `protocol` | 目标协议。当前只支持 TCP。 | `tcp` |
|
||||
| `route_hint` | 预留字段。新的最小路径由 POP Server 直连公网目标,通常不需要填写。 | `null` |
|
||||
|
||||
@ -277,7 +277,7 @@ CLIENT_AGENT_SOCKS5_PASSWORD=change-this-password
|
||||
'policy_id' => 'public-web-egress',
|
||||
'users' => ['normal-user', 'admin', 'devops'],
|
||||
'target_hosts' => ['*'],
|
||||
'target_ports' => [80, 443],
|
||||
'target_ports' => [80, 443, '8080-10080'],
|
||||
'protocol' => 'tcp',
|
||||
'route_type' => 'direct',
|
||||
'enabled' => true,
|
||||
@ -286,10 +286,15 @@ CLIENT_AGENT_SOCKS5_PASSWORD=change-this-password
|
||||
|
||||
这条策略表示:
|
||||
|
||||
* `normal-user`、`admin` 和 `devops` 可以访问任意主机的 `80`、`443` 端口。
|
||||
* `normal-user`、`admin` 和 `devops` 可以访问任意主机的 `80`、`443`,以及 `8080` 到 `10080` 端口。
|
||||
* Client Agent 只负责把请求封装成 Frame 发到 POP Server。
|
||||
* POP Server 校验策略后直接连接公网目标。
|
||||
|
||||
`target_ports` 和 `allowed_ports` 都支持两种写法:
|
||||
|
||||
* 单端口:`80`
|
||||
* 端口范围:`'8080-10080'`
|
||||
|
||||
路由类型:
|
||||
|
||||
| `route_type` | 作用 |
|
||||
@ -373,6 +378,8 @@ php bin/client-agent.php start
|
||||
|
||||
然后把应用的代理设置为 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:
|
||||
|
||||
```bash
|
||||
|
||||
@ -61,3 +61,16 @@
|
||||
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
|
||||
2026-05-28 13:27:02 pid:492815 Workerman[client-agent.php] restart
|
||||
2026-05-28 13:27:02 pid:492815 Workerman[client-agent.php] is stopping ...
|
||||
2026-05-28 13:27:02 pid:492815 Workerman[client-agent.php] stop success
|
||||
2026-05-28 15:16:23 pid:494454 Workerman[pop-server.php] start in DAEMON mode
|
||||
2026-05-28 15:22:27 pid:492815 Workerman[client-agent.php] received signal SIGINT
|
||||
2026-05-28 15:22:27 pid:492815 Workerman[client-agent.php] stopping
|
||||
2026-05-28 15:22:27 pid:492815 Workerman[client-agent.php] has been stopped
|
||||
2026-05-28 15:27:40 pid:495186 Workerman[pop-server.php] restart
|
||||
2026-05-28 15:27:40 pid:495186 Workerman[pop-server.php] is stopping ...
|
||||
2026-05-28 15:27:40 pid:494484 Workerman[pop-server.php] received signal SIGINT
|
||||
2026-05-28 15:27:40 pid:494484 Workerman[pop-server.php] stopping
|
||||
2026-05-28 15:27:40 pid:494484 Workerman[pop-server.php] has been stopped
|
||||
2026-05-28 15:27:40 pid:495186 Workerman[pop-server.php] stop success
|
||||
|
||||
@ -24,7 +24,7 @@ echo "Using SOCKS5 proxy: ${proxy}"
|
||||
echo
|
||||
echo "[1/2] HTTPS connectivity: https://bing.com/"
|
||||
bing_code="$(
|
||||
curl \
|
||||
curl -vvvv \
|
||||
--silent \
|
||||
--show-error \
|
||||
--location \
|
||||
|
||||
@ -162,7 +162,7 @@ final class AgentClient
|
||||
return;
|
||||
}
|
||||
|
||||
$this->send(new Frame(FrameType::DATA, $sessionId, ['data' => base64_encode($data)]));
|
||||
$this->send(new Frame(FrameType::DATA, $sessionId, ['data_raw' => $data]));
|
||||
}
|
||||
|
||||
private function handleInitialRequest(TcpConnection $connection, string $data): void
|
||||
@ -544,7 +544,7 @@ final class AgentClient
|
||||
$pending = $this->pendingData[$frame->sessionId] ?? '';
|
||||
unset($this->pendingData[$frame->sessionId]);
|
||||
if ($pending !== '') {
|
||||
$this->send(new Frame(FrameType::DATA, $frame->sessionId, ['data' => base64_encode($pending)]));
|
||||
$this->send(new Frame(FrameType::DATA, $frame->sessionId, ['data_raw' => $pending]));
|
||||
}
|
||||
}
|
||||
|
||||
@ -573,7 +573,7 @@ final class AgentClient
|
||||
return;
|
||||
}
|
||||
|
||||
$data = base64_decode((string)($frame->payload['data'] ?? ''), true);
|
||||
$data = $this->frameData($frame);
|
||||
if ($data === false) {
|
||||
$this->send(new Frame(FrameType::ERROR, $frame->sessionId, ['reason' => 'invalid_frame']));
|
||||
return;
|
||||
@ -582,6 +582,15 @@ final class AgentClient
|
||||
$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
|
||||
{
|
||||
unset($this->initialBuffers[$connection->id]);
|
||||
|
||||
@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace LayLink\Agent;
|
||||
|
||||
use LayLink\Auth\PolicyChecker;
|
||||
use LayLink\Auth\PortMatcher;
|
||||
|
||||
final class TargetConnector
|
||||
{
|
||||
@ -14,7 +15,7 @@ final class TargetConnector
|
||||
|
||||
public function isAllowed(string $host, int $port): bool
|
||||
{
|
||||
if (!in_array($port, $this->nodeConfig['allowed_ports'] ?? [], true)) {
|
||||
if (!PortMatcher::matches($port, $this->nodeConfig['allowed_ports'] ?? [])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@ -22,7 +22,7 @@ final class PolicyChecker
|
||||
if (($policy['protocol'] ?? 'tcp') !== $protocol) {
|
||||
continue;
|
||||
}
|
||||
if (!in_array($port, $policy['target_ports'] ?? [], true)) {
|
||||
if (!PortMatcher::matches($port, $policy['target_ports'] ?? [])) {
|
||||
continue;
|
||||
}
|
||||
if (!$this->hostMatches($host, $policy['target_hosts'] ?? [])) {
|
||||
|
||||
47
src/Auth/PortMatcher.php
Normal file
47
src/Auth/PortMatcher.php
Normal file
@ -0,0 +1,47 @@
|
||||
<?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,6 +9,8 @@ use InvalidArgumentException;
|
||||
final class FrameCodec
|
||||
{
|
||||
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_CHACHA20 = 'chacha20';
|
||||
|
||||
@ -40,20 +42,19 @@ final class FrameCodec
|
||||
|
||||
public static function encode(Frame $frame): string
|
||||
{
|
||||
$json = json_encode($frame->toArray(), JSON_UNESCAPED_SLASHES);
|
||||
if ($json === false) {
|
||||
throw new InvalidArgumentException('invalid_frame_payload');
|
||||
}
|
||||
|
||||
$body = self::encrypt($json);
|
||||
$body = self::encrypt(self::serializeFrame($frame));
|
||||
|
||||
return pack('N', strlen($body)) . $body;
|
||||
}
|
||||
|
||||
public static function decode(string $body): Frame
|
||||
{
|
||||
$json = self::decrypt($body);
|
||||
$data = json_decode($json, true);
|
||||
$plaintext = self::decrypt($body);
|
||||
if (str_starts_with($plaintext, self::BINARY_MAGIC)) {
|
||||
return self::decodeBinaryFrame($plaintext);
|
||||
}
|
||||
|
||||
$data = json_decode($plaintext, true);
|
||||
if (!is_array($data) || !isset($data['type']) || !is_string($data['type'])) {
|
||||
throw new InvalidArgumentException('invalid_frame');
|
||||
}
|
||||
@ -76,6 +77,49 @@ 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
|
||||
{
|
||||
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_OK => 'POP Server confirms target stream is open.',
|
||||
self::OPEN_FAIL => 'POP Server rejects or fails target stream opening.',
|
||||
self::DATA => 'Bidirectional stream bytes, base64 encoded in MVP.',
|
||||
self::DATA => 'Bidirectional stream bytes; TCP stream DATA uses binary frame encoding when both ends are updated.',
|
||||
self::UDP_DATA => 'Bidirectional UDP datagram relay, base64 encoded in MVP.',
|
||||
self::CLOSE => 'Either side closes a stream session.',
|
||||
self::ERROR => 'Explicit protocol or session error.',
|
||||
|
||||
@ -123,7 +123,7 @@ final class AgentListener
|
||||
}
|
||||
|
||||
match ($frame->type) {
|
||||
FrameType::DATA => $this->forwardDataToTarget($session, (string)($frame->payload['data'] ?? '')),
|
||||
FrameType::DATA => $this->forwardDataToTarget($session, $frame),
|
||||
FrameType::CLOSE => $this->closeSession($session, 'closed', null),
|
||||
default => null,
|
||||
};
|
||||
@ -233,7 +233,7 @@ final class AgentListener
|
||||
$target->onMessage = function (AsyncTcpConnection $target, string $data) use ($session, $agentConnection): void {
|
||||
$session->bytesTargetToClient += strlen($data);
|
||||
$this->send($agentConnection, new Frame(FrameType::DATA, $session->sessionId, [
|
||||
'data' => base64_encode($data),
|
||||
'data_raw' => $data,
|
||||
]));
|
||||
};
|
||||
$target->onClose = fn () => $this->closeSession($session, 'closed', null);
|
||||
@ -241,9 +241,9 @@ final class AgentListener
|
||||
$target->connect();
|
||||
}
|
||||
|
||||
private function forwardDataToTarget(TunnelSession $session, string $encoded): void
|
||||
private function forwardDataToTarget(TunnelSession $session, Frame $frame): void
|
||||
{
|
||||
$data = base64_decode($encoded, true);
|
||||
$data = $this->frameData($frame);
|
||||
if ($data === false) {
|
||||
$this->closeSession($session, 'failed', 'invalid_frame');
|
||||
return;
|
||||
@ -259,6 +259,15 @@ final class AgentListener
|
||||
$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
|
||||
{
|
||||
$this->send($agentConnection, new Frame(FrameType::OPEN_FAIL, $frame->sessionId, ['reason' => $reason]));
|
||||
|
||||
Loading…
Reference in New Issue
Block a user