# SNI
Date: 2023-12-11


**SNI**是TLS协议扩展， 在握手的开始标识其尝试连接的主机名， 当多个HTTPS服务部署在同一IP地址上，客户端就可以通过这个标识指定它将使用哪一个服务， 同时服务端也无需使用相同的证书，它在概念上相当于HTTP/1.1基于名称的虚拟主机。SNI扩展最早在2003年的RFC 3546中出现。

HTTP服务通过 Http header "Host", 来选择指定服务， HTTPS服务就是通过这个SNI来区分。通过可以通过nginx来验证一下

```
#user  nobody;
worker_processes  1;

#error_log  logs/error.log;
error_log  logs/error.log  debug;
#error_log  logs/error.log  info;

#pid        logs/nginx.pid;

events {
    worker_connections  1024;
}

http {
    access_log logs/access.log ;

    server {
        listen 8443 ssl ;
        server_name test;

        ssl_certificate     certs/cert.crt;
        ssl_certificate_key certs/cert.key;

        location / {
            return 200 "https-test\r\n";
        }
    }

    server {
        listen 8443 ssl ;
        server_name garlic;

        ssl_certificate     certs/cert2.crt;
        ssl_certificate_key certs/cert.key;

        location / {
            return 200 "https-garlic\r\n";
        }
    }

    server {
        listen 8443 ssl ;
        server_name 10.10.10.10;

        ssl_certificate     certs/cert3.crt;
        ssl_certificate_key certs/cert.key;

        location / {
            root   html;
            index  index.html index.htm;
        }
    }

    server {
        listen 9443 ssl ;
        server_name snitest;

        ssl_certificate     certs/cert4.crt;
        ssl_certificate_key certs/cert.key;

        location / {
            root   html;
            index  index.html index.htm;
        }
    }

    server {
        listen 8080 ;
        server_name http-test;

        location / {
            return 200 "http-test\r\n";
        }
    }

    server {
        listen 8080 ;
        server_name http-garlic;

        location / {
            return 200 "http-garlic\r\n";
        }
    }
}

```

 

http使用curl 通过后-H 增加header可以选择不同的主机

```
[garlic@dev nginx-quic]$ curl -H "Host: http-test" http://127.0.0.1:8080/
http-test
[garlic@dev nginx-quic]$ curl -H "Host: http-garlic" http://127.0.0.1:8080/
http-garlic

```

 

https服务可以使用openssl sclient来验证

```
[garlic@dev nginx-quic]$ openssl s_client -connect 127.0.0.1:8443 -servername test
CONNECTED(00000003)
depth=0 C = CN, ST = BEIJING, L = beijing, OU = organizationalUnitName, CN = test
verify error:num=18:self-signed certificate
verify return:1
depth=0 C = CN, ST = BEIJING, L = beijing, OU = organizationalUnitName, CN = test
verify return:1
---
Certificate chain
 0 s:C = CN, ST = BEIJING, L = beijing, OU = organizationalUnitName, CN = test
   i:C = CN, ST = BEIJING, L = beijing, OU = organizationalUnitName, CN = test
。。。。

[garlic@dev nginx-quic]$ openssl s_client -connect 127.0.0.1:8443 -servername garlic
CONNECTED(00000003)
depth=0 C = CN, ST = SHENZHEN, L = ShenZhen, OU = organizationalUnitName, CN = garlic
verify error:num=18:self-signed certificate
verify return:1
depth=0 C = CN, ST = SHENZHEN, L = ShenZhen, OU = organizationalUnitName, CN = garlic
verify return:1
---
Certificate chain
 0 s:C = CN, ST = SHENZHEN, L = ShenZhen, OU = organizationalUnitName, CN = garlic
   i:C = CN, ST = SHENZHEN, L = ShenZhen, OU = organizationalUnitName, CN = garlic

[garlic@dev nginx-quic]$ openssl s_client -connect 127.0.0.1:8443 -servername 10.10.10.10
CONNECTED(00000003)
depth=0 C = CN, ST = CN, L = BEIJING, OU = TEST, CN = 10.10.10.10
verify error:num=18:self-signed certificate
verify return:1
depth=0 C = CN, ST = CN, L = BEIJING, OU = TEST, CN = 10.10.10.10
verify return:1
---
Certificate chain
 0 s:C = CN, ST = CN, L = BEIJING, OU = TEST, CN = 10.10.10.10
   i:C = CN, ST = CN, L = BEIJING, OU = TEST, CN = 10.10.10.10
```

可以看到servername传送的不同， 选择的ssl服务也不同。特殊的如果servername 是一个ip， 处理也是一样的，创

> 这里servername与证书中的common name设置为一样，如果要支持多个域名或ip可以通过SAN配置。

 

对应nginx中sni的判断 `ngx_http_find_virtual_server`

如果是https服务 被调用两次 针对servername的回调函数， ngx\_http\_ssl\_servername， 第二次调用是ngx\_http\_set\_virtual\_server。

所以可以设置SNI来选择证书， 然后通过后Header来重新选择服务。

```
curl -ksv --resolve garlic:8443:127.0.0.1 https://garlic:8443 -H "Host: test"
。。。。
* Server certificate:
*  subject: C=CN; ST=SHENZHEN; L=ShenZhen; OU=organizationalUnitName; CN=garlic
*  start date: Dec  9 01:45:35 2023 GMT
*  expire date: Dec  6 01:45:35 2033 GMT
*  issuer: C=CN; ST=SHENZHEN; L=ShenZhen; OU=organizationalUnitName; CN=garlic
。。。。
https-test

```

 

在wiki SNI此词条里还讲到到安全部分， SNI是明文存放的， 所以虽然tls是加密的，其实还是查询出来访问的网站的名称。**Encrypted Client Hello** (**ECH**)则是为了解决这个问题。

 

**SNI Passthrough** 

如果需要将客户端上送SNI传送到下游服务上，对应如下配置：

```
proxy_ssl_name          $host;
proxy_ssl_server_name   on;
```

也就是把客户端上送的SNI再发送下游服务器时再设置一下。

详细的可以参考这篇blog https://blog.martdj.nl/2023/11/09/nginx-as-reverse-proxy-and-sni/

作者描述主要问题是针对一个ip托管多个tls服务网站，未开启sni passthrough 无法通过SNI获取的server\_name进行路由，无法区分是从哪个站点发起的。

 

**SNI Routing**

当然获取了SNI后，可以通过他进行选择下游服务器。通过map映射需要转发后端服务， 当然也可以根据需要设置默认使用的证书与key。

```
http {

    ssl_password_file password.txt;

    map $ssl_server_name $targetBackend {
        test1.example.com  127.0.0.1:8080;
        test2.example.com 127.0.0.1:8081;
    }

    map $ssl_server_name $targetCert {
        test1.example.com cert1/server.cert.pem;
        test2.example.com cert2/server.cert.pem;
    }

    map $ssl_server_name $targetCertKey {
        test1.example.com cert1/server.key.pem;
        test2.example.com cert2/server.key.pem;
    }

    server
    {
        listen 0.0.0.0:443 ssl;
        ssl_protocols TLSv1 TLSv1.1 TLSv1.2;

        ssl_certificate     $targetCert;
        ssl_certificate_key $targetCertKey;

        location / {
            proxy_pass http://$targetBackend;
        }

    }

}
stream {

    ssl_password_file password.txt;

    map $ssl_server_name $targetBackend {
        test1.example.com  127.0.0.1:8080;
        test2.example.com 127.0.0.1:8081;
    }

    map $ssl_server_name $targetCert {
        test1.example.com cert1/server.cert.pem;
        test2.example.com cert2/server.cert.pem;
    }

    map $ssl_server_name $targetCertKey {
        test1.example.com cert1/server.key.pem;
        test2.example.com cert2/server.key.pem;
    }

    server {
      listen 8443 ssl;
      ssl_protocols       TLSv1.2;
      ssl_certificate     $targetCert;
      ssl_certificate_key $targetCertKey;

      proxy_pass $targetBackend;
    }
}

```

可以参考下面的链接。[https://gist.github.com/kekru/c09dbab5e78bf76402966b13fa72b9d2](https://gist.github.com/kekru/c09dbab5e78bf76402966b13fa72b9d2)

nginx plus 可以配置lazy load， 配置更方便一些。

```
server {
   listen 443 ssl;

   ssl_certificate      /etc/ssl/$ssl_server_name.crt; # Lazy load from SNI   
   ssl_certificate_key  /etc/ssl/$ssl_server_name.key; # ditto                    
   ssl_protocols        TLSv1.3 TLSv1.2 TLSv1.1;
   ssl_prefer_server_ciphers on;

   location / {
       proxy_set_header Host $host;
       proxy_pass http://my_backend;
   }
}
```

https://www.infoq.com/news/2019/04/nginx-plus-release-18/

openresty 可以通过 ssl\_certificate\_by\_lua\_block 进行配置， 可以参考下面的链接。

https://github.com/openresty/lua-resty-core/blob/master/lib/ngx/ssl.md#raw\_client\_addr

 

#### **参考及引用**

https://jvns.ca/blog/2016/07/14/whats-sni/

https://en.wikipedia.org/wiki/Server\_Name\_Indication#Security\_implications

 

图片from[李華欽](https://www.facebook.com/groups/474930279315210/user/100063550502415/?__cft__[0]=AZWLDunxyRtHhxutX5mfXqIZSBnpe_NtzgwX8BsPFqlv5q7m_Yfbk3ZGlpVW2AHwE_Vm6atoJOTMi6WBauW58QqpOGZhIjC7Sn97TB8X9iq_L64gqE4j9GWG3rQ6_k2E7ZPo6YrVIA9JG8DC43js9i9I1VbdSPBfZOzyjLDfrcU7iva8Vy2qdgWThKVL8Ujx4cI&__tn__=-UC*F)

