AI Excerpt

本文介绍了在内网穿透场景下基于NginX的TCP转发实现SSL转发的方法。在内网穿透过程中,引入SSL时可能会出现Mixed Content的问题,主要原因是服务端没有正确识别SSL。文章针对这种情况提供了一种解决方案:在服务端设置TCP转发,并使用NginX实现SSL转发。这种方法可以避免Mixed Content问题,保证网站的正常访问。

问题

上个月把主站Wordpress从香港服务器迁移到了自己搭的小型服务器上,然后用frp做内网穿透暴露到公网上。本来没什么问题,结果在配置完HTTPS后就遇到了Mixed Content的麻烦。

混合内容警告

简单来说就是页面上的HTTP和HTTPS内容混合加载,浏览器为了安全会阻止部分HTTP内容的加载,就导致例如样式的部分加载异常。

样式加载失败

原因

经查阅,出现这种问题的原因主要是Wordpress没能正确识别SSL。想了一下也有道理,之前部署SSL的方法是在服务端用Certbot直接部署在Wordpress的NginX配置中,类似于这样:

配置拓扑
## NginX配置
server {   
     server_name okkk.cc;
     root /path/to/wordpress;             
     index index.php index.html;              

	proxy_busy_buffers_size   512k;
	proxy_buffers   4 512k;
	proxy_buffer_size   256k;        
     
     access_log /path/to/okkk.access.log;             
     error_log /path/to/okkk.error.log;              

     location / {                            
          try_files $uri $uri/ /index.php?$args;
     }

     location ~ \.php$ {
     include snippets/fastcgi-php.conf;                            
          fastcgi_pass unix:/run/php/php8.1-fpm.sock;             
     }              

     location ~ /\.ht {                            
          deny all;             
     }             

     location = /favicon.ico {                            
          log_not_found off;                            
          access_log off;             
     }             

     location = /robots.txt {                            
          allow all;                            
          log_not_found off;                            
          access_log off;             
     }             

     location ~* \.(js|css|png|jpg|jpeg|gif|ico)$ {                            
          expires max;                            
          log_not_found off;             
     }

    listen 443 ssl; # managed by Certbot
    ssl_certificate /path/to/okkk.cc/fullchain.pem; # managed by Certbot
    ssl_certificate_key /path/to/okkk.cc/privkey.pem; # managed by Certbot
    include /path/to/options-ssl-nginx.conf; # managed by Certbot
    ssl_dhparam /path/to/ssl-dhparams.pem; # managed by Certbot

}

server {
     if ($host = okkk.cc) {
          return 301 https://$host$request_uri;
     } # managed by Certbot
     server_name okkk.cc;  
     listen 80;
     return 404; # managed by Certbot
}

现在问题就主要体现在内网穿透服务器上。我现在用nps做穿透,把内网的Wordpress网站转发到穿透服务器的指定端口上,然后在穿透服务器上写一个http的NginX配置把网站流量转发到这个指定端口上。一开始自然地我也就想到在穿透服务端部署SSL,因为这样要简单一些,而且安全性也没什么太大问题。

穿透配置
server {
     server_name okkk.cc;

     access_log /path/to/okkk.access.log;
     error_log /path/to/okkk.error.log;

     location / {
          proxy_pass #ADDRESS;
          proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
          proxy_set_header Host $http_host;
          proxy_set_header X-Forwarded-Proto $scheme;
          proxy_set_header X-NginX-Proxy true;
          proxy_http_version 1.1;
          proxy_set_header Upgrade $http_upgrade;
          proxy_set_header Connection "upgrade";
          proxy_max_temp_file_size 0;
          proxy_redirect off;
          proxy_read_timeout 40s;

     }
     listen 443 ssl; # managed by Certbot
     ssl_certificate /path/to/okkk.cc/fullchain.pem; # managed by Certbot
     ssl_certificate_key /path/to/okkk.cc/privkey.pem; # managed by Certbot
     include /path/to/options-ssl-nginx.conf; # managed by Certbot
     ssl_dhparam /path/to/ssl-dhparams.pem; # managed by Certbot
}
server {
     if ($host = okkk.cc) {
          return 301 https://$host$request_uri;
     } # managed by Certbot
     server_name okkk.cc;
     listen 80;
     return 404; # managed by Certbot
}

解决方案

结果就出现了混合内容的问题。经过一番搜索,找到了个可以解决这个问题的插件SSL Insecure Content Fixer

选择简单修复,识别HTTPS方式改为X-Forwarded-Proto(需要在转发端配置proxy_set_header),应用之后网页确实能正常加载样式了。

但是...

有些小问题,首先Wordpress内置的协议检测方法仍然不能正常识别HTTPS连接,而且部分站内需要调用外部链接的页面比如插件商店会出现搜索和加载异常的情况(后续发现这一点可能是因为别的活动插件bug导致的,有待确认),而且整体的页面加载时间会有很小幅度的上升。于是就想着穿透服务端直接把Wordpress的TCP流量转发到后端服务器上,然后后端部署HTTPS,这样就能保证Wordpress正常识别HTTPS了。

问题是一般TCP连接是不带hostname信息的。就算是http连接,stream模块也不能直接读取server_name,但是我的穿透服务器上还要转发一些其他web服务,不能全部转发80/443端口到Wordpress后端。

最终

一番搜索后发现了ngx_stream_ssl_preread module这个模块可以直接从请求中的client hello信息读取主机名,利用map映射可以实现将不同主机名的TCP请求转发到对应服务器上。

## 以下内容需要添加到stream中

upstream okkk {
	server 127.0.0.1:#端口1;
}

upstream okkkdrive{
	server 127.0.0.1:#端口2;
}

upstream https_default_backend {
	server 127.0.0.1:#端口3;
}

map $ssl_preread_server_name $name {
	okkk.cc okkk;
	drive.okkk.cc okkkdrive;
	default https_default_backend;
}

server {
	listen 443;
	ssl_preread on;
	proxy_pass $name;
}

需要注意:

1. 只有基于SSL/TLS的HTTPS请求才会包含client hello握手信息,也就是说这种方法只能转发HTTPS流量。这里我的解决方法是另写一个配置监听80端口,然后在配置中将所有运行的网站HTTP请求都重定向到HTTPS请求上;

2. 我使用的Certbot部署SSL证书,在后端部署的过程中一度出现了验证失败的情况,这是因为Certbot需要验证域名的所有权,此时需要确保你的域名指向的主机ip能通过80端口连通到Certbot运行主机上。

用其它域名做个示范

我的解决方法是先临时写一个http配置监听80端口然后转发到Wordpress后端,确保外界能通过HTTP访问主页,然后在部署完SSL证书后删掉临时的http配置,再在stream中添加443端口映射规则。

3. NginX配置中stream服务器和http服务器优先级是同级的,因此在stream监听了443端口(listen 443会监听本机所有的443端口)后就不能再在其他配置里监听443端口了,否则NginX会报错。在网上找到了一种解决443复用的方法是添加一块虚拟网卡,比如在stream中把监听改成127.0.0.1:443,然后把默认backend服务器指向127.0.0.2:443,再在其他配置里面监听127.0.0.2:443就行了。我就偷懒了一点,直接把默认backend指向444端口,再在其他http配置中监听444端口就行了,实测不影响SSL。

总结

内网穿透和SSL的坑还是挺多的,特别是内网穿透,以前用了好久的frp发现速度一直都比较慢,跑不满带宽。也用过nps但是可惜当时没有对比,上周试了一下远程桌面卡的一批,于是换了nps,带宽直接拉满,操作瞬间流畅了不少。过两天再水一篇frp和nps对比的文章。