From b9e8c5a06fa5e58878c629dd6ec247efe9d1c122 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Fri, 21 Jun 2019 11:41:33 +0200 Subject: [PATCH] Fix proxied DNS over HTTPS --- src/danog/MadelineProto/DataCenter.php | 121 ++++++++++++------ src/danog/MadelineProto/MTProto.php | 2 +- src/danog/MadelineProto/ProxySocketPool.php | 8 +- .../Stream/ConnectionContext.php | 48 +++++-- .../MTProtoTransport/AbridgedStream.php | 2 - 5 files changed, 124 insertions(+), 57 deletions(-) diff --git a/src/danog/MadelineProto/DataCenter.php b/src/danog/MadelineProto/DataCenter.php index 238f4436..5b58472b 100644 --- a/src/danog/MadelineProto/DataCenter.php +++ b/src/danog/MadelineProto/DataCenter.php @@ -24,10 +24,21 @@ use Amp\Artax\Cookie\ArrayCookieJar; use Amp\Artax\DefaultClient; use Amp\Artax\HttpSocketPool; use Amp\CancellationToken; +use Amp\Deferred; +use Amp\Dns\Record; use Amp\Dns\Resolver; +use Amp\Dns\Rfc1035StubResolver; use Amp\DoH\DoHConfig; +use Amp\DoH\Nameserver; use Amp\DoH\Rfc8484StubResolver; +use Amp\Loop; +use Amp\NullCancellationToken; +use Amp\Promise; use Amp\Socket\ClientConnectContext; +use Amp\Socket\ClientSocket; +use Amp\Socket\ClientTlsContext; +use Amp\Socket\ConnectException; +use Amp\TimeoutException; use danog\MadelineProto\Stream\Common\BufferedRawStream; use danog\MadelineProto\Stream\ConnectionContext; use danog\MadelineProto\Stream\MTProtoTransport\AbridgedStream; @@ -42,19 +53,8 @@ use danog\MadelineProto\Stream\Proxy\SocksProxy; use danog\MadelineProto\Stream\Transport\DefaultStream; use danog\MadelineProto\Stream\Transport\WssStream; use danog\MadelineProto\Stream\Transport\WsStream; -use Amp\DoH\Nameserver; use function Amp\call; -use Amp\Promise; -use Amp\Socket\ClientTlsContext; -use Amp\Deferred; -use Amp\NullCancellationToken; use function Amp\Socket\Internal\parseUri; -use Amp\Dns\Record; -use Amp\Socket\ConnectException; -use Amp\Loop; -use Amp\TimeoutException; -use Amp\Socket\ClientSocket; -use Amp\Dns\Rfc1035StubResolver; /** * Manages datacenters. @@ -70,6 +70,7 @@ class DataCenter private $settings = []; private $HTTPClient; private $DoHClient; + private $NonProxiedDoHClient; public function __sleep() { @@ -91,16 +92,33 @@ class DataCenter unset($this->sockets[$key]); } } - $this->HTTPClient = new DefaultClient(new ArrayCookieJar(), new HttpSocketPool(new ProxySocketPool($this))); + $this->HTTPClient = new DefaultClient(new ArrayCookieJar(), new HttpSocketPool(new ProxySocketPool([$this, 'rawConnectAsync']))); + $DoHHTTPClient = new DefaultClient( + new ArrayCookieJar(), + new HttpSocketPool( + new ProxySocketPool( + function (string $uri, CancellationToken $token = null, ClientConnectContext $ctx = null) { + return $this->rawConnectAsync($uri, $token, $ctx, true); + } + ) + ) + ); $DoHConfig = new DoHConfig( [ new Nameserver('https://mozilla.cloudflare-dns.com/dns-query'), new Nameserver('https://google.com/resolve', Nameserver::GOOGLE_JSON, ["Host" => "dns.google.com"]), ], - $this->HTTPClient + $DoHHTTPClient + ); + $NonProxiedDoHConfig = new DoHConfig( + [ + new Nameserver('https://mozilla.cloudflare-dns.com/dns-query'), + new Nameserver('https://google.com/resolve', Nameserver::GOOGLE_JSON, ["Host" => "dns.google.com"]), + ], ); $this->DoHClient = Magic::$altervista || Magic::$zerowebhost ? new Rfc1035StubResolver() : new Rfc8484StubResolver($DoHConfig); + $this->NonProxiedDoHClient = Magic::$altervista || Magic::$zerowebhost ? new Rfc1035StubResolver() : new Rfc8484StubResolver($NonProxiedDoHConfig); } /** @@ -108,7 +126,7 @@ class DataCenter * * Note: Once resolved the socket stream will already be set to non-blocking mode. * - * @param bool $ipv6 + * @param ConnectionContext $ctx * @param string $uricall * @param ClientConnectContext $socketContext * @param ClientTlsContext $tlsContext @@ -117,13 +135,13 @@ class DataCenter * @return Promise */ public function cryptoConnect( - bool $ipv6, + ConnectionContext $ctx, string $uri, ClientConnectContext $socketContext = null, ClientTlsContext $tlsContext = null, CancellationToken $token = null ): Promise { - return call(function () use ($ipv6, $uri, $socketContext, $tlsContext, $token) { + return call(function () use ($ctx, $uri, $socketContext, $tlsContext, $token) { $tlsContext = $tlsContext ?? new ClientTlsContext; if ($tlsContext->getPeerName() === null) { @@ -131,7 +149,7 @@ class DataCenter } /** @var ClientSocket $socket */ - $socket = yield $this->socketConnect($ipv6, $uri, $socketContext, $token); + $socket = yield $this->socketConnect($ctx, $uri, $socketContext, $token); $promise = $socket->enableCrypto($tlsContext); @@ -170,16 +188,16 @@ class DataCenter /** * Asynchronously establish a socket connection to the specified URI. * - * @param bool $ipv6 Whether to use IPv6 + * @param ConnectionContext $ctx Connection context * @param string $uri URI in scheme://host:port format. TCP is assumed if no scheme is present. * @param ClientConnectContext $socketContext Socket connect context to use when connecting. * @param CancellationToken|null $token * * @return Promise<\Amp\Socket\ClientSocket> */ - public function socketConnect(bool $ipv6, string $uri, ClientConnectContext $socketContext = null, CancellationToken $token = null): Promise + public function socketConnect(ConnectionContext $ctx, string $uri, ClientConnectContext $socketContext = null, CancellationToken $token = null): Promise { - return call(function () use ($ipv6, $uri, $socketContext, $token) { + return call(function () use ($ctx, $uri, $socketContext, $token) { $socketContext = $socketContext ?? new ClientConnectContext; $token = $token ?? new NullCancellationToken; $attempt = 0; @@ -197,12 +215,37 @@ class DataCenter $uris = [$uri]; } else { // Host is not an IP address, so resolve the domain name. - $records = yield $this->DoHClient->resolve($host, $socketContext->getDnsTypeRestriction()); - + // When we're connecting to a host, we may need to resolve the domain name, first. + // The resolution is usually done using DNS over HTTPS. + // + // The DNS over HTTPS resolver needs to resolve the domain name of the DOH server: + // this is handled internally by the DNS over HTTPS client, + // by redirecting the resolution request to the plain DNS client. + // + // However, if the DoH connection is proxied with a proxy that has a domain name itself, + // we cannot resolve it with the DoH resolver, since this will cause an infinite loop + // + // resolve host.com => (DoH resolver) => resolve dohserver.com => (simple resolver) => OK + // + // |> resolve dohserver.com => (simple resolver) => OK + // resolve host.com => (DoH resolver) =| + // |> resolve proxy.com => (non-proxied resolver) => OK + // + // + // This means that we must detect if the domain name we're trying to resolve is a proxy domain name. + // + // Here, we simply check if the connection URI has changed since we first set it: + // this would indicate that a proxy class has changed the connection URI to the proxy URI. + // + if ($ctx->isDns()) { + $records = yield $this->NonProxiedDoHClient->resolve($host, $socketContext->getDnsTypeRestriction()); + } else { + $records = yield $this->DoHClient->resolve($host, $socketContext->getDnsTypeRestriction()); + } \usort($records, function (Record $a, Record $b) { return $a->getType() - $b->getType(); }); - if ($ipv6) { + if ($ctx->getIpv6()) { $records = array_reverse($records); } @@ -290,7 +333,7 @@ class DataCenter }); } - public function rawConnectAsync(string $uri, CancellationToken $token = null, ClientConnectContext $ctx = null): \Generator + public function rawConnectAsync(string $uri, CancellationToken $token = null, ClientConnectContext $ctx = null, $fromDns = false): \Generator { $ctxs = $this->generateContexts(0, $uri, $ctx); if (empty($ctxs)) { @@ -299,6 +342,7 @@ class DataCenter foreach ($ctxs as $ctx) { /* @var $ctx \danog\MadelineProto\Stream\ConnectionContext */ try { + $ctx->setIsDns($fromDns); $ctx->setCancellationToken($token); $result = yield $ctx->getStream(); $this->API->logger->logger('OK!', \danog\MadelineProto\Logger::WARNING); @@ -308,9 +352,9 @@ class DataCenter if (defined('MADELINEPROTO_TEST') && MADELINEPROTO_TEST === 'pony') { throw $e; } - $this->API->logger->logger('Connection failed: '.$e->getMessage(), \danog\MadelineProto\Logger::ERROR); + $this->API->logger->logger('Connection failed: '.$e, \danog\MadelineProto\Logger::ERROR); } catch (\Exception $e) { - $this->API->logger->logger('Connection failed: '.$e->getMessage(), \danog\MadelineProto\Logger::ERROR); + $this->API->logger->logger('Connection failed: '.$e, \danog\MadelineProto\Logger::ERROR); } } @@ -498,6 +542,7 @@ class DataCenter $ipv6 = [$this->settings[$dc_config_number]['ipv6'] ? 'ipv6' : 'ipv4', $this->settings[$dc_config_number]['ipv6'] ? 'ipv4' : 'ipv6']; foreach ($ipv6 as $ipv6) { + // This is only for non-MTProto connections if (!$dc_number) { /** @var $ctx \danog\MadelineProto\Stream\ConnectionContext */ $ctx = (new ConnectionContext()) @@ -507,23 +552,22 @@ class DataCenter foreach ($combo as $stream) { if ($stream[0] === DefaultStream::getName() && $stream[1] === []) { - $isIpv6 = $ipv6 === 'ipv6'; $stream[1] = [ function ( string $uri, ClientConnectContext $socketContext = null, CancellationToken $token = null - ) use ($isIpv6): Promise { - return $this->socketConnect($isIpv6, $uri, $socketContext, $token); + ) use ($ctx): Promise { + return $this->socketConnect($ctx, $uri, $socketContext, $token); }, function ( string $uri, ClientConnectContext $socketContext = null, ClientTlsContext $tlsContext = null, CancellationToken $token = null - ) use ($isIpv6): Promise { - return $this->cryptoConnect($isIpv6, $uri, $socketContext, $tlsContext, $token); - } + ) use ($ctx): Promise { + return $this->cryptoConnect($ctx, $uri, $socketContext, $tlsContext, $token); + }, ]; } $ctx->addStream(...$stream); @@ -531,6 +575,8 @@ class DataCenter $ctxs[] = $ctx; continue; } + + // This is only for MTProto connections if (!isset($this->dclist[$test][$ipv6][$dc_number]['ip_address'])) { continue; } @@ -584,23 +630,22 @@ class DataCenter foreach ($combo as $stream) { if ($stream[0] === DefaultStream::getName() && $stream[1] === []) { - $isIpv6 = $ipv6 === 'ipv6'; $stream[1] = [ function ( string $uri, ClientConnectContext $socketContext = null, CancellationToken $token = null - ) use ($isIpv6): Promise { - return $this->socketConnect($isIpv6, $uri, $socketContext, $token); + ) use ($ctx): Promise { + return $this->socketConnect($ctx, $uri, $socketContext, $token); }, function ( string $uri, ClientConnectContext $socketContext = null, ClientTlsContext $tlsContext = null, CancellationToken $token = null - ) use ($isIpv6): Promise { - return $this->cryptoConnect($isIpv6, $uri, $socketContext, $tlsContext, $token); - } + ) use ($ctx): Promise { + return $this->cryptoConnect($ctx, $uri, $socketContext, $tlsContext, $token); + }, ]; } $ctx->addStream(...$stream); diff --git a/src/danog/MadelineProto/MTProto.php b/src/danog/MadelineProto/MTProto.php index 6c9c4809..d14b2833 100644 --- a/src/danog/MadelineProto/MTProto.php +++ b/src/danog/MadelineProto/MTProto.php @@ -826,7 +826,7 @@ class MTProto extends AsyncConstruct implements TLCallback public function parse_settings($settings) { $settings = self::getSettings($settings, $this->settings); - if ($this->settings['app_info'] === null) { + if ($settings['app_info'] === null) { throw new \danog\MadelineProto\Exception(\danog\MadelineProto\Lang::$current_lang['api_not_set'], 0, null, 'MadelineProto', 1); } $this->settings = $settings; diff --git a/src/danog/MadelineProto/ProxySocketPool.php b/src/danog/MadelineProto/ProxySocketPool.php index 42c12e74..02a02067 100644 --- a/src/danog/MadelineProto/ProxySocketPool.php +++ b/src/danog/MadelineProto/ProxySocketPool.php @@ -31,13 +31,13 @@ class ProxySocketPool implements SocketPool private $pendingCount = []; private $idleTimeout; private $socketContext; - private $dataCenter; + private $connectCallback; - public function __construct(DataCenter $dataCenter, int $idleTimeout = 10000, ClientConnectContext $socketContext = null) + public function __construct(callable $connectCallback, int $idleTimeout = 10000, ClientConnectContext $socketContext = null) { $this->idleTimeout = $idleTimeout; $this->socketContext = $socketContext ?? new ClientConnectContext(); - $this->dataCenter = $dataCenter; + $this->connectCallback = $connectCallback; } /** @@ -129,7 +129,7 @@ class ProxySocketPool implements SocketPool try { /** @var ClientSocket $rawSocket */ - $rawSocket = yield $this->call($this->dataCenter->rawConnectAsync($uri, $token, $this->socketContext)); + $rawSocket = yield $this->call(($this->connectCallback)($uri, $token, $this->socketContext)); } finally { if (--$this->pendingCount[$uri] === 0) { unset($this->pendingCount[$uri]); diff --git a/src/danog/MadelineProto/Stream/ConnectionContext.php b/src/danog/MadelineProto/Stream/ConnectionContext.php index b284e6af..9dcd9fbb 100644 --- a/src/danog/MadelineProto/Stream/ConnectionContext.php +++ b/src/danog/MadelineProto/Stream/ConnectionContext.php @@ -51,6 +51,12 @@ class ConnectionContext * @var \Amp\Uri\Uri */ private $uri; + /** + * Whether this connection context will be used by the DNS client + * + * @var bool + */ + private $isDns = false; /** * Socket context. * @@ -122,7 +128,7 @@ class ConnectionContext public function setUri($uri): self { $this->uri = $uri instanceof Uri ? $uri : new Uri($uri); - + return $this; } @@ -169,7 +175,15 @@ class ConnectionContext { return $this->cancellationToken; } - + /** + * Return a clone of the current connection context + * + * @return self + */ + public function getCtx(): self + { + return clone $this; + } /** * Set the secure boolean. * @@ -194,6 +208,26 @@ class ConnectionContext return $this->test; } + /** + * Whether this connection context will only be used by the DNS client + * + * @return bool + */ + public function isDns(): bool + { + return $this->isDns; + } + /** + * Whether this connection context will only be used by the DNS client + * + * @param boolean $isDns + * @return self + */ + public function setIsDns(bool $isDns): self + { + $this->isDns = $isDns; + return $this; + } /** * Set the secure boolean. * @@ -284,16 +318,6 @@ class ConnectionContext return $this->ipv6; } - /** - * Set the ipv6 boolean. - * - * @return self - */ - public function getCtx(): self - { - return clone $this; - } - /** * Add a stream to the stream chain. * diff --git a/src/danog/MadelineProto/Stream/MTProtoTransport/AbridgedStream.php b/src/danog/MadelineProto/Stream/MTProtoTransport/AbridgedStream.php index 870bedb9..f59ad9d6 100644 --- a/src/danog/MadelineProto/Stream/MTProtoTransport/AbridgedStream.php +++ b/src/danog/MadelineProto/Stream/MTProtoTransport/AbridgedStream.php @@ -34,7 +34,6 @@ class AbridgedStream implements BufferedStreamInterface, MTProtoBufferInterface use BufferedStream; private $stream; - private $ctx; /** * Connect to stream. @@ -45,7 +44,6 @@ class AbridgedStream implements BufferedStreamInterface, MTProtoBufferInterface */ public function connectAsync(ConnectionContext $ctx, string $header = ''): \Generator { - $this->ctx = $ctx->getCtx(); $this->stream = yield $ctx->getStream(chr(239).$header); }