add more
This commit is contained in:
@@ -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'] ?? [])) {
|
||||
|
||||
@@ -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]));
|
||||
|
||||
Reference in New Issue
Block a user