nginx 转发代理 tls tcp 并且使用 sni 复用 443 端口

您所在的位置:网站首页 nginx443端口 nginx 转发代理 tls tcp 并且使用 sni 复用 443 端口

nginx 转发代理 tls tcp 并且使用 sni 复用 443 端口

2023-03-04 12:17| 来源: 网络整理| 查看: 265

前言

之前有处理过 nginx 转发代理 wss 和 https (目标程序是 ws 和 http), 后面发现我们的服务还有一些是 tcp 长连接,而且还是支持 tls 的 tcp 长连接。 这个也是非 80 和 443 端口的。 后面也需要用 nginx 转发代理一下。

nginx 支持 tcp 层的转发

nginx 1.9 开始支持 tcp 层的转发,通过 stream 实现的,而 socket 也是基于 tcp 通信,跟 ws 和 wss 不一样, 本质上 ws 虽然也是长连接, 但是他是基于 http 的协议上去进行协议升级的,所以可以写在 http 指令串里面。

但是 tcp 不支持,而且依赖的模块也不一样,他依赖的是 ngx_stream_core_module, 这个模块也是要单独安装的。而且这个 stream 块, 只能放到 nginx.conf 文件中,不能放到 site-avaliable 目录中, 不然就会变成 http 的。

安装 stream 和 ssl_stream 模块

安装 nginx,stream 模块默认不安装的, 通过 /usr/local/nginx/sbin/nginx -V 查找123456[root@VM-0-13-centos ~]# /usr/local/nginx/sbin/nginx -Vnginx version: nginx/1.18.0built by gcc 4.8.5 20150623 (Red Hat 4.8.5-44) (GCC)built with OpenSSL 1.0.2k-fips 26 Jan 2017TLS SNI support enabledconfigure arguments: --prefix=/usr/local/nginx --with-http_ssl_module

目前只安装 http 的 ssl 的模块, 并没有包含 ngx_stream_core_module 这个模块, 所以我们只需要重新编译一下,覆盖一下:1[root@VM-0-13-centos nginx-1.18.0]# ./configure --prefix=/usr/local/nginx --with-http_ssl_module --with-stream --with-stream_ssl_module

具体步骤可以看: CentOS 7 安装 Nginx 的 现有的 nginx 程序添加 http_ssl_module 模块 小节,一样的流程。而且这次不仅仅要添加 stream 模块, ssl_stream 模块也要用到,因为我们也有用到 tls 的 tcp 连接。所以两个一起加进去。

最后添加完之后是这样子:123456[root@VM-0-13-centos nginx-1.18.0]# /usr/local/nginx/sbin/nginx -Vnginx version: nginx/1.18.0built by gcc 4.8.5 20150623 (Red Hat 4.8.5-44) (GCC)built with OpenSSL 1.0.2k-fips 26 Jan 2017TLS SNI support enabledconfigure arguments: --prefix=/usr/local/nginx --with-http_ssl_module --with-stream --with-stream_ssl_module

简单的转发 demo

我们写个简单的 demo

客户端 demo123456789101112131415161718192021222324252627282930313233343536package mainimport ( "bufio" "fmt" "net" "os" "strings")func main() { conn, err := net.Dial("tcp", "127.0.0.1:8006") if err != nil { fmt.Println("client err=", err) return } defer conn.Close() // 关闭连接 //客户端可以发送单行数据 reader := bufio.NewReader(os.Stdin) for { input, err := reader.ReadString('\n') if err != nil { fmt.Println("readstring err=", err) } inputInfo := strings.Trim(input, "\r\n") //fmt.Println(inputInfo) if strings.ToUpper(inputInfo) == "Q" { // 如果输入q就退出 return } //将line发送给服务器 _, err = conn.Write([]byte(inputInfo)) if err != nil { fmt.Println("conn.write err=", err) } }} 服务端 demo12345678910111213141516171819202122232425262728293031323334353637383940package mainimport ( "fmt" "net")func process(conn net.Conn) { defer conn.Close() for { buf := make([]byte, 1024) fmt.Printf("服务器在等待客户端%s发送信息\n", conn.RemoteAddr().String()) n, err := conn.Read(buf) if err != nil { fmt.Println("客户端退出 err=", err) return } fmt.Println("客户端发送的数据:", string(buf[:n])) }}func main() { fmt.Println("服务器开始监听端口") listen, err := net.Listen("tcp", "0.0.0.0:8008") fmt.Println(listen) if err != nil { fmt.Println("listen err=", err) return } defer listen.Close() for { fmt.Println("等待客户端连接") conn, err := listen.Accept() if err != nil { fmt.Println("Accept err=", err) } else { fmt.Printf("suc conn=%v,客户端ip=%v\n ", conn, conn.RemoteAddr().String()) } go process(conn) }}

逻辑很简单, 服务端监听的端口是 8008, 但是 客户端连接的端口是 8006。 说明 nginx 会代理这个 tcp 请求

nginx 配置:

这个要配在 nginx.conf 文件中, 原先的都可以不变,再最下面加上

1234567891011stream { upstream tcpend { server 127.0.0.1:8008; } server { listen 8006; proxy_pass tcpend; }}

逻辑很简单,如果是 tcp 请求,那么就将 8006 端口代理到上游服务的 8008 端口中。

先启动 demo 的 server, 再启动 demo 的 client,然后两者进行交互, server log 如下:123456789101112[root@VM-0-13-centos new-demo]# ./server 服务器开始监听端口&{0xc0000cc000 { 0}}等待客户端连接suc conn=&{{0xc0000cc080}},客户端ip=127.0.0.1:27071 等待客户端连接服务器在等待客户端127.0.0.1:27071发送信息客户端发送的数据: hello服务器在等待客户端127.0.0.1:27071发送信息客户端发送的数据: this is test服务器在等待客户端127.0.0.1:27071发送信息客户端退出 err= EOF

client log:1234[root@VM-0-13-centos new-demo]# ./client hellothis is testq

可以看到转发 tcp 是没有问题的。

tls 版本 demo

接下来试一下 tls tcp 的版本, 之前已经装了 stream_ssl_module, 所以这边直接配置。 证书用自制的就行了

客户端 demo123456789101112131415161718192021222324252627282930313233343536package mainimport ( "bufio" "fmt" "os" "strings" "crypto/tls")func main() { conn, err := tls.Dial("tcp", "127.0.0.1:443", &tls.Config{InsecureSkipVerify: true}) if err != nil { fmt.Println("client err=", err) return } defer conn.Close() // 关闭连接 //客户端可以发送单行数据 reader := bufio.NewReader(os.Stdin) for { input, err := reader.ReadString('\n') if err != nil { fmt.Println("readstring err=", err) } inputInfo := strings.Trim(input, "\r\n") //fmt.Println(inputInfo) if strings.ToUpper(inputInfo) == "Q" { // 如果输入q就退出 return } //将line发送给服务器 _, err = conn.Write([]byte(inputInfo)) if err != nil { fmt.Println("conn.write err=", err) } }}

这次不需要服务端修改, 因为是走 tcp tls 代理的, 上游程序还是一样是 8008 的非 tls 的 tcp 连接。 而且因为是自制证书, 所以客户端连接的时候,要指定 insecure 选项

nginx 配置123456789101112131415161718stream { upstream tcpend { server 127.0.0.1:8008; } server { #listen 8006; listen 443 ssl; proxy_pass tcpend; ssl_certificate ssl/server.crt; ssl_certificate_key ssl/server.key; ssl_ciphers HIGH:!aNULL:!MD5; ssl_prefer_server_ciphers on; } }

server log:123456789101112[root@VM-0-13-centos new-demo]# ./server 服务器开始监听端口&{0xc0000c4000 { 0}}等待客户端连接suc conn=&{{0xc0000c4080}},客户端ip=127.0.0.1:33291 等待客户端连接服务器在等待客户端127.0.0.1:33291发送信息客户端发送的数据: this is tc服务器在等待客户端127.0.0.1:33291发送信息客户端发送的数据: this is tls hello服务器在等待客户端127.0.0.1:33291发送信息客户端退出 err= EOF

client log:1234[root@VM-0-13-centos new-demo]# ./client-tlsthis is tcthis is tls helloq

可以看到转发没有问题。 而且也不一定要合法证书,用自建证书,然后 ip 地址连接,然后再配置跳过 ssl 校验的话,那么是可以的。

这样子就可以将 入口的 tls tcp 转发到非 tls 的 tcp 后端程序。 跟之前用 nginx 代理 wss 一样, 后端程序不用改成 tls, 走 nginx 即可。

使用 SNI 来使得 https 和 tls tcp 复用 443 端口

正常情况下, 在 同一个 nginx 配置中, http 块 和 stream 是没办法同时监听 443 端口的。 How to combine nginx “stream” and “http” for the same servername?

那么如果我一定要这么配呢,会出现什么情况,会报错吗? 客户端和服务端还是不变。 然后 nginx.conf 配置变一下, http 和 stream 都监听 tls 443123456789101112131415161718192021222324252627282930313233343536373839404142[root@VM-0-13-centos conf]# cat nginx.confworker_processes 1;events { worker_connections 1024;}http { # HTTPS server server { listen 443 ssl; server_name localhost; ssl_certificate ssl/server.crt; ssl_certificate_key ssl/server.key; ssl_ciphers HIGH:!aNULL:!MD5; ssl_prefer_server_ciphers on; location / { root html; index index.html index.htm; } }}stream { upstream tcpend { server 127.0.0.1:8008; } server { #listen 8006; listen 443 ssl; proxy_pass tcpend; ssl_certificate ssl/server.crt; ssl_certificate_key ssl/server.key; ssl_protocols SSLv3 TLSv1 TLSv1.1 TLSv1.2; ssl_ciphers HIGH:!aNULL:!MD5; ssl_prefer_server_ciphers on; } }

然后用 tcp 客户端连一下, 发现是可以连接上的:

12345678suc conn=&{{0xc0000c4100}},客户端ip=127.0.0.1:35825 等待客户端连接服务器在等待客户端127.0.0.1:35825发送信息客户端发送的数据: hello服务器在等待客户端127.0.0.1:35825发送信息客户端发送的数据: yes服务器在等待客户端127.0.0.1:35825发送信息客户端退出 err= EOF

可以连上去,说明没问题。

然后接下来用 curl 请求一下 https 请求:1[root@VM-0-13-centos sbin]# curl https://127.0.0.1:443 --insecure

发现没有走 http 块,还是走到 stream 块,所以直接转发到 上游服务 8008 端口那边:1234567891011suc conn=&{{0xc0000e4000}},客户端ip=127.0.0.1:36171 等待客户端连接服务器在等待客户端127.0.0.1:36171发送信息客户端发送的数据: GET / HTTP/1.1User-Agent: curl/7.29.0Host: 127.0.0.1Accept: */*服务器在等待客户端127.0.0.1:36171发送信息客户端退出 err= EOF

说明虽然 http 和 stream 都可以监听 443 端口, 但是其实流量只会走 stream 块。 http 块不会走。

那么跟 nginx.conf 的 stream 块和 http 块的顺序是否有关呢, 上面的配置文件是 http 上面, stream 下面, 我将其调换一下。 发现结果还是一样。 流量都走 stream 了。

所以其实就可以理解 443 端口其实是被 stream 块使用了, http 块不行。因为 http 协议(网络层第七层应用层)本质上也是基于 tcp 协议(网络层第四层传输层), 所以如果在传输层就将流量劫持了,那么就没有上层的事情了。 我如果将 http 块的监听,换成其他端口,比如 8001 而不是 443,那么是可以正常走到 https 那一边。

12345678910111213[root@VM-0-13-centos new-demo]# curl https://127.0.0.1:8001 --insecureWelcome to nginx! body { width: 35em; margin: 0 auto; font-family: Tahoma, Verdana, Arial, sans-serif; }...

所以至少从上面的配置来说, stream 和 http 是无法复用 443 端口。 但是其实还有一种方式可以让 https/wss 和 tls tcp 复用 443 端口,那就是 SNI(Server Name Indication)

SNI 概念

传输层安全性协议(即大名鼎鼎的 TLS)是一个工作在传输层上的重要安全协议,它可以为互联网通信提供安全及数据完整性保障,像HTTPS等安全传输都是基于TLS所进行的。

服务器名称指示(SNI)是TLS的一个扩展协议,在该协议下,在握手过程开始时客户端告诉它正在连接的服务器要连接的主机名称。Nginx 就可以利用stream模块,基于SNI,对进入同一端口、不同主机名的TLS流量进行分流。如果你有一个基于TLS的应用,想要运行在443端口;而443端口已经被Nginx监听用作Web运行网站,你就可以使用Nginx的SNI分流,将443端口复用,把使用不同的域名(主机名)的TLS流量分开,互不干扰,完美共存。

nginx 的版本至少要 1.15.9

前置准备工作 1: 安装模块

如果要开启 SNI, 除了之前我们安装的 ngx_stream_core_module 模块(stream模块), 那么就还需要开启 ngx_stream_ssl_preread_module, 所以我们要再重新编译加载 --with-stream_ssl_preread_module。123456[root@VM-0-13-centos conf]# nginx -Vnginx version: nginx/1.18.0built by gcc 4.8.5 20150623 (Red Hat 4.8.5-44) (GCC) built with OpenSSL 1.0.2n 7 Dec 2017TLS SNI support enabledconfigure arguments: --prefix=/usr/local/nginx --with-http_ssl_module --with-stream --with-stream_ssl_module --with-stream_ssl_preread_module

前置准备工作 2: 设置域名和有效证书

因为 SNI 是基于域名的,所以我们要准备两个域名,一个用于 tcp 转发,一个用于 http 转发

test-tcp-proxy.example.com 这个是 tcp test-tcp-http.example.com 这个是 https 前置准备工作 3: 上游程序要支持 tls tcp 连接

因为 SNI 是基于 tls 传输的,不仅客户端入口连接的时候,要走 tls 加密, 连上游服务器 upstream 在转发的时候也要走 tls 监听

这一点跟上述的 nginx 代理转发 非 tls tcp 的 upstream 是不一样的。 SNI 的 upstream 一定要走 tls 才行。 这时候当初实践的时候,踩了一个大坑

如果 upstream 没有走 tls 监听的话, 是可以连上,但是握手的时候会报一堆的乱码:123456789suc conn=&{{0xc0000c2180}},客户端ip=127.0.0.1:29852等待客户端连接服务器在等待客户端127.0.0.1:29852发送信息客户端发送的数据: ¸#△Έ©ԿL



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3