Swoole 在 v4.6.0 版本中对 SNI 进行了反对,这篇文章就对这个新个性进行一些演示和阐明。

先来理解一下什么是 SNI 协定?

Server Name Identification 简称 SNI,是一个扩大的 TLS 计算机联网协定,用来解决一个服务器领有多个域名的状况。

在该协定下,在握手过程开始时通过客户端通知它正在连接的服务器的主机名称。这容许服务器在雷同的 IP 地址和 TCP 端口号上出现多个证书,并且因而容许在雷同的 IP 地址上提供多个平安 HTTPS 网站(或其余任何基于 TLS 的服务),而不须要所有这些站点应用雷同的证书。

那么如果一台服务器有多个虚拟主机,而且每个主机的域名不一样,应用了不一样的证书,该和哪个主机进行通信?

和 HTTP 协定用来解决服务器多域名的计划相似,HTTP 在申请头中应用 Host 字段来指定要拜访的域名。

而 TLS 的做法也是增加 Host,在客户端收回 SSL 申请中的 Client Hello 阶段进行增加,这样就能够让服务器可能切换到正确的域并返回相应的证书。

在 Swoole 的 GitHub 中也有一个 Issue (#4031),想让 Swoole 的 HTTP Server 反对通过 Hostname 来配置 SSL 信息。

实际上是 Swoole 在 #3908 中曾经进行了反对,不过英文网站的文档还没来及更新,所以没有找到相干阐明。

上面就来演示 Swoole 如何设置 SNI:

首先先下载证书,这里间接应用 Swoole 测试的证书

wget -r -np -nd -P ./ssl_certs https://cdn.jsdelivr.net/gh/swoole/swoole-src@4.6.2/tests/include/ssl_certs/

下载实现后会寄存在当前目录下的ssl_certs目录中

再来创立一个 HTTP Server

use Swoole\Http\Request;use Swoole\Http\Response;use Swoole\Http\Server;$http = new Server('0.0.0.0', 9501);$http->on('request', function (Request $request, Response $response) {    $response->end('Hello Swoole');});$http->start();

这里就须要用到一个新增的 ssl_sni_certs 选项

ssl_sni_certs的参数是一个二维数组,key 为 Hostname,value 是对应的证书配置

$http->set([   'ssl_sni_certs' => [       'cs.php.net' => [           'ssl_cert_file' => SSL_FILE_DIR . '/sni_server_cs_cert.pem',           'ssl_key_file' => SSL_FILE_DIR . '/sni_server_cs_key.pem'       ],       'uk.php.net' => [           'ssl_cert_file' => SSL_FILE_DIR . '/sni_server_uk_cert.pem',           'ssl_key_file' => SSL_FILE_DIR . '/sni_server_uk_key.pem'       ],       'us.php.net' => [           'ssl_cert_file' => SSL_FILE_DIR . '/sni_server_us_cert.pem',           'ssl_key_file' => SSL_FILE_DIR . '/sni_server_us_key.pem',       ],   ]]);

配置一下 Server 的 sock_type 来反对 SSL,以及对应的证书配置,就有了如下代码

use Swoole\Http\Request;use Swoole\Http\Response;use Swoole\Http\Server;define('SSL_FILE_DIR', __DIR__ . '/ssl_certs');$http = new Server('127.0.0.1', 9501, SWOOLE_BASE, SWOOLE_SOCK_TCP | SWOOLE_SSL);$http->set([   'log_file' => '/dev/null',   'ssl_cert_file' => SSL_FILE_DIR . '/server.crt',   'ssl_key_file' => SSL_FILE_DIR . '/server.key',   'ssl_protocols' => SWOOLE_SSL_TLSv1_2 | SWOOLE_SSL_TLSv1_3 | SWOOLE_SSL_TLSv1_1 | SWOOLE_SSL_SSLv2,   'ssl_sni_certs' => [       'cs.php.net' => [           'ssl_cert_file' => SSL_FILE_DIR . '/sni_server_cs_cert.pem',           'ssl_key_file' => SSL_FILE_DIR . '/sni_server_cs_key.pem'       ],       'uk.php.net' => [           'ssl_cert_file' => SSL_FILE_DIR . '/sni_server_uk_cert.pem',           'ssl_key_file' => SSL_FILE_DIR . '/sni_server_uk_key.pem'       ],       'us.php.net' => [           'ssl_cert_file' => SSL_FILE_DIR . '/sni_server_us_cert.pem',           'ssl_key_file' => SSL_FILE_DIR . '/sni_server_us_key.pem',       ],   ]]);$http->on('request', function (Request $request, Response $response) {    $response->end('Hello Swoole');});$http->start();

搞个客户端来测试一下

$flags = STREAM_CLIENT_CONNECT;$port = 9501;$ctxArr = [    'cafile' => SSL_FILE_DIR . '/sni_server_ca.pem',    'capture_peer_cert' => true,    'verify_peer' => false,];$ctxArr['peer_name'] = 'cs.php.net';$ctx = stream_context_create(['ssl' => $ctxArr]);$client = stream_socket_client("tls://127.0.0.1:$port", $errno, $errstr, 1, $flags, $ctx);$cert = stream_context_get_options($ctx)['ssl']['peer_certificate'];var_dump(openssl_x509_parse($cert)['subject']['CN']);$ctxArr['peer_name'] = 'uk.php.net';$ctx = stream_context_create(['ssl' => $ctxArr]);$client = stream_socket_client("tls://127.0.0.1:$port", $errno, $errstr, 1, $flags, $ctx);$cert = stream_context_get_options($ctx)['ssl']['peer_certificate'];var_dump(openssl_x509_parse($cert)['subject']['CN']);$ctxArr['peer_name'] = 'us.php.net';$ctx = stream_context_create(['ssl' => $ctxArr]);$client = stream_socket_client("tls://127.0.0.1:$port", $errno, $errstr, 1, $flags, $ctx);$cert = stream_context_get_options($ctx)['ssl']['peer_certificate'];var_dump(openssl_x509_parse($cert)['subject']['CN']);

在测试的同时,应用 tcpdump 进行抓包

tcpdump -i lo0 port 9501 -w sni.pcap

申请胜利后,客户端会输入对应的三个 Hostname

$ php swoole.phpstring(10) "cs.php.net"string(10) "uk.php.net"string(10) "us.php.net"

而后应用 Wireshark 来剖析一下抓到的包,通过ssl.handshake来过滤出想要的报文

剖析报文发现 server_name 的扩大字段只存在于 Client Hello 这个过程中

接着应用ssl.handshake.extensions_server_name进行过滤,提取出蕴含 SNI 协定的 Client Hello 报文

就能够看到 SNI 扩大字段:

Extension: server_name (len=15)Type: server_name (0)Length: 15Server Name Indication extensionServer Name list length: 13Server Name Type: host_name (0)Server Name length: 10Server Name: cs.php.net

这里指定了该 TLS 握手的指标域名为 cs.php.net

通过 SNI,领有多域名的服务器就能够失常建设 TLS 连贯了。

上面是残缺的测试代码:

<?phpuse Swoole\Http\Request;use Swoole\Http\Response;use Swoole\Http\Server;use Swoole\Process\Manager;use Swoole\Process\Pool;define('SSL_FILE_DIR', __DIR__ . '/ssl_certs');$pm = new Manager();$pm->add(function (Pool $pool, int $workerId) {    $http = new Server('127.0.0.1', 9501, SWOOLE_BASE, SWOOLE_SOCK_TCP | SWOOLE_SSL);    $http->set([       'log_file' => '/dev/null',       'ssl_cert_file' => SSL_FILE_DIR . '/server.crt',       'ssl_key_file' => SSL_FILE_DIR . '/server.key',       'ssl_protocols' => SWOOLE_SSL_TLSv1_2 | SWOOLE_SSL_TLSv1_3 | SWOOLE_SSL_TLSv1_1 | SWOOLE_SSL_SSLv2,       'ssl_sni_certs' => [           'cs.php.net' => [               'ssl_cert_file' => SSL_FILE_DIR . '/sni_server_cs_cert.pem',               'ssl_key_file' => SSL_FILE_DIR . '/sni_server_cs_key.pem'           ],           'uk.php.net' => [               'ssl_cert_file' => SSL_FILE_DIR . '/sni_server_uk_cert.pem',               'ssl_key_file' => SSL_FILE_DIR . '/sni_server_uk_key.pem'           ],           'us.php.net' => [               'ssl_cert_file' => SSL_FILE_DIR . '/sni_server_us_cert.pem',               'ssl_key_file' => SSL_FILE_DIR . '/sni_server_us_key.pem',           ],       ]    ]);    $http->on('request', function (Request $request, Response $response) {        $response->end('Hello Swoole');    });    $http->start();});$pm->add(function (Pool $pool, int $workerId) {    $flags = STREAM_CLIENT_CONNECT;    $port = 9501;    $ctxArr = [        'cafile' => SSL_FILE_DIR . '/sni_server_ca.pem',        'capture_peer_cert' => true,        'verify_peer' => false,    ];    $ctxArr['peer_name'] = 'cs.php.net';    $ctx = stream_context_create(['ssl' => $ctxArr]);    $client = stream_socket_client("tls://127.0.0.1:$port", $errno, $errstr, 1, $flags, $ctx);    $cert = stream_context_get_options($ctx)['ssl']['peer_certificate'];    var_dump(openssl_x509_parse($cert)['subject']['CN']);    $ctxArr['peer_name'] = 'uk.php.net';    $ctx = stream_context_create(['ssl' => $ctxArr]);    $client = stream_socket_client("tls://127.0.0.1:$port", $errno, $errstr, 1, $flags, $ctx);    $cert = stream_context_get_options($ctx)['ssl']['peer_certificate'];    var_dump(openssl_x509_parse($cert)['subject']['CN']);    $ctxArr['peer_name'] = 'us.php.net';    $ctx = stream_context_create(['ssl' => $ctxArr]);    $client = stream_socket_client("tls://127.0.0.1:$port", $errno, $errstr, 1, $flags, $ctx);    $cert = stream_context_get_options($ctx)['ssl']['peer_certificate'];    var_dump(openssl_x509_parse($cert)['subject']['CN']);    $pool->shutdown();});$pm->start();