Fix proxied DNS over HTTPS

This commit is contained in:
Daniil Gentili 2019-06-21 11:41:33 +02:00
parent d82914187c
commit b9e8c5a06f
5 changed files with 124 additions and 57 deletions

View File

@ -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<ClientSocket>
*/
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);

View File

@ -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;

View File

@ -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]);

View File

@ -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.
*
@ -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.
*

View File

@ -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);
}