背景:
家里的 荣耀路由器 Pro 2 的 NAT 配置限制非常多,不能手动指定 IP 地址,只能选择设备。但是,这个设备还不能是路由器!
所以,我不能将请求路由给 LAN 口上的子路由器,导致子路由器上连接的设备无法从外部访问。
因此,我将内网中的树莓派4配置为 DMZ 主机,打算通过 iptables, nginx 等手段作为路由器的补充。
实现方案
- 背景
- 预期结果
- Nginx 实现(简单)
- 安装 Nginx (--with-stream)
- 加载动态模块
- 配置转发
- 关于 UDP 转发配置 `proxy_responses`
- 验证连接
- 验证方式:`nc` 命令
- 验证 TCP 连接
- 验证 UDP 连接
背景
网络拓扑图如下:
网络结构非常简单,Server 作为 R1 的 DMZ 主机,如果要实现外网访问 Server,完全就是 R0 一条 NAT 的事情。
但是! R0 这类消费级的路由器,UI 缺少很多路由器本身能实现的功能,包括 静态路由表 等。更奇葩的是:
这个路由器的 NAT、DMZ 等功能不能选择路由器作为目标!且不能手动输入NAT、DMZ 的目标 IP 地址!
当前在线的终端如下:
DMZ 可选设备完整列表如下(NAT可选设备列表相同):
DMZ 和 NAT 都不可以选连接在 LAN 口上的路由器!
因此,本人尝试使用运行 Linux 系统的树莓派 RPi 实现 NAT 到子路由器的功能。
预期结果
RPi 作为 R0 的 DMZ 主机,只要 PC 能通过 RPi 连接 Server,外网就可以访问到 Server。
Nginx 实现(简单)
通过 Nginx 实现 TCP、UDP 等协议的转发比较简单
安装 Nginx (–with-stream)
可以通过 apt、yum 等直接安装功能比较全的版本 nginx-extras,该版本包括了 stream 等模块
sudo apt install -y nginx-extras
通过以下命令,确认 nginx 带有 --with-stream
编译参数
nginx -V
输出结果,可以看到 --with-stream=dynamic
nginx version: nginx/1.14.2
built with OpenSSL 1.1.1c 28 May 2019 (running with OpenSSL 1.1.1d 10 Sep 2019)
TLS SNI support enabled
configure arguments: --with-cc-opt='-g -O2 -fdebug-prefix-map=/build/nginx-v2a0Oa/nginx-1.14.2=. -fstack-protector-strong -Wformat -Werror=format-security -fPIC -Wdate-time -D_FORTIFY_SOURCE=2' --with-ld-opt='-Wl,-z,relro -Wl,-z,now -fPIC' --prefix=/usr/share/nginx --conf-path=/etc/nginx/nginx.conf --http-log-path=/var/log/nginx/access.log --error-log-path=/var/log/nginx/error.log --lock-path=/var/lock/nginx.lock --pid-path=/run/nginx.pid --modules-path=/usr/lib/nginx/modules --http-client-body-temp-path=/var/lib/nginx/body --http-fastcgi-temp-path=/var/lib/nginx/fastcgi --http-proxy-temp-path=/var/lib/nginx/proxy --http-scgi-temp-path=/var/lib/nginx/scgi --http-uwsgi-temp-path=/var/lib/nginx/uwsgi --with-debug --with-pcre-jit --with-http_ssl_module --with-http_stub_status_module --with-http_realip_module --with-http_auth_request_module --with-http_v2_module --with-http_dav_module --with-http_slice_module --with-threads --with-http_addition_module --with-http_flv_module --with-http_geoip_module=dynamic --with-http_gunzip_module --with-http_gzip_static_module --with-http_image_filter_module=dynamic --with-http_mp4_module --with-http_perl_module=dynamic --with-http_random_index_module --with-http_secure_link_module --with-http_sub_module --with-http_xslt_module=dynamic --with-mail=dynamic --with-mail_ssl_module --with-stream=dynamic --with-stream_ssl_module --with-stream_ssl_preread_module --add-dynamic-module=/build/nginx-v2a0Oa/nginx-1.14.2/debian/modules/http-headers-more-filter --add-dynamic-module=/build/nginx-v2a0Oa/nginx-1.14.2/debian/modules/http-auth-pam --add-dynamic-module=/build/nginx-v2a0Oa/nginx-1.14.2/debian/modules/http-cache-purge --add-dynamic-module=/build/nginx-v2a0Oa/nginx-1.14.2/debian/modules/http-dav-ext --add-dynamic-module=/build/nginx-v2a0Oa/nginx-1.14.2/debian/modules/http-ndk --add-dynamic-module=/build/nginx-v2a0Oa/nginx-1.14.2/debian/modules/http-echo --add-dynamic-module=/build/nginx-v2a0Oa/nginx-1.14.2/debian/modules/http-fancyindex --add-dynamic-module=/build/nginx-v2a0Oa/nginx-1.14.2/debian/modules/nchan --add-dynamic-module=/build/nginx-v2a0Oa/nginx-1.14.2/debian/modules/http-lua --add-dynamic-module=/build/nginx-v2a0Oa/nginx-1.14.2/debian/modules/rtmp --add-dynamic-module=/build/nginx-v2a0Oa/nginx-1.14.2/debian/modules/http-uploadprogress --add-dynamic-module=/build/nginx-v2a0Oa/nginx-1.14.2/debian/modules/http-upstream-fair --add-dynamic-module=/build/nginx-v2a0Oa/nginx-1.14.2/debian/modules/http-subs-filter
加载动态模块
一般通过 apt 等安装 nginx-extras,模块已默认启用,可以跳过本章节。
--with-stream=dynamic
与 --with-stream
的区别在于,前者可以在 nginx 的配置文件配置动态加载模块,不需要使用该模块时可以不加载。
模块的加载在 nginx 的配置文件中可以配置
在配置文件中使用 load_module
加载模块:
load_module modules/ngx_stream_module.so;
模块加载可以单独放在一个文件中,然后通过 include
引入到 nginx.conf
中/etc/nginx/modules-enabled/50-mod-stream.conf
本人的 /etc/nginx/nginx.conf
文件节选:
第 4 行引入了模块相关配置
user www-data;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;
events {
worker_connections 768;
# multi_accept on;
}
include /etc/nginx/conf.d/*.conf;
http {
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
include /etc/nginx/mime.types;
default_type application/octet-stream;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # Dropping SSLv3, ref: POODLE
ssl_prefer_server_ciphers on;
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
include /etc/nginx/sites-enabled/*;
}
配置转发
新建一个配置文件 /etc/nginx/conf.d/forward.conf
stream {
server {
listen 65000;
proxy_pass 192.168.3.99:65000;
}
server {
listen 65000 udp;
proxy_pass 192.168.3.99:65000;
# proxy_responses 1;
}
}
通过 include
的方式引入 nginx.conf
中。
第 11 行引入了 /etc/nginx/conf.d/
目录下所有配置文件,包括了刚才创建的 forward.conf
user www-data;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;
events {
worker_connections 768;
# multi_accept on;
}
include /etc/nginx/conf.d/*.conf;
http {
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
include /etc/nginx/mime.types;
default_type application/octet-stream;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # Dropping SSLv3, ref: POODLE
ssl_prefer_server_ciphers on;
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
include /etc/nginx/sites-enabled/*;
}
配置修改完成后,nginx 重新加载配置
sudo nginx -s reload
关于 UDP 转发配置 proxy_responses
使用 UDP 的时候,有一项参数 proxy_responses
参考官方文档 ngx_stream_proxy_module # proxy_responses
Syntax: proxy_responses number;
Default: —
Context: stream, server
This directive appeared in version 1.9.13.
Sets the number of datagrams expected from the proxied server in response to a client datagram if the UDP protocol is used. The number serves as a hint for session termination. By default, the number of datagrams is not limited.
If zero value is specified, no response is expected. However, if a response is received and the session is still not finished, the response will be handled.
个人理解:
- 如果客户端仅单向发送 UDP 数据包到服务端,不需要接收服务端数据,则可以配置该项为 0,即不保持 UDP 会话;
- 如果使用类似于 RDP(远程桌面) 等工具,客户端与服务端之间有大量的双向 UDP 通讯,则这一项可以不配置,这样会话将持续保持一段时间,经过
proxy_timeout
时间后结束会话。
proxy_timeout
参考官方文档 ngx_stream_proxy_module # proxy_timeout
Syntax: proxy_timeout timeout;
Default:
proxy_timeout 10m;
Context: stream, server
Sets the timeout between two successive read or write operations on client or proxied server connections. If no data is transmitted within this time, the connection is closed.
验证连接
验证方式:nc
命令
PC 与 Server 之间的连接,采用 nc
命令进行验证
nc
命令基本使用方式:
OpenBSD netcat (Debian patchlevel 1.195-2)
usage: nc [-46CDdFhklNnrStUuvZz] [-I length] [-i interval] [-M ttl]
[-m minttl] [-O length] [-P proxy_username] [-p source_port]
[-q seconds] [-s source] [-T keyword] [-V rtable] [-W recvlimit] [-w timeout]
[-X proxy_protocol] [-x proxy_address[:port]] [destination] [port]
Command Summary:
-4 Use IPv4
-6 Use IPv6
-b Allow broadcast
-C Send CRLF as line-ending
-D Enable the debug socket option
-d Detach from stdin
-F Pass socket fd
-h This help text
-I length TCP receive buffer length
-i interval Delay interval for lines sent, ports scanned
-k Keep inbound sockets open for multiple connects
-l Listen mode, for inbound connects
-M ttl Outgoing TTL / Hop Limit
-m minttl Minimum incoming TTL / Hop Limit
-N Shutdown the network socket after EOF on stdin
-n Suppress name/port resolutions
-O length TCP send buffer length
-P proxyuser Username for proxy authentication
-p port Specify local port for remote connects
-q secs quit after EOF on stdin and delay of secs
-r Randomize remote ports
-S Enable the TCP MD5 signature option
-s source Local source address
-T keyword TOS value
-t Answer TELNET negotiation
-U Use UNIX domain socket
-u UDP mode
-V rtable Specify alternate routing table
-v Verbose
-W recvlimit Terminate after receiving a number of packets
-w timeout Timeout for connects and final net reads
-X proto Proxy protocol: "4", "5" (SOCKS) or "connect"
-x addr[:port] Specify proxy address and port
-Z DCCP mode
-z Zero-I/O mode [used for scanning]
Port numbers can be individual or ranges: lo-hi [inclusive]
验证 TCP 连接
Server 端监听 TCP 65000 端口
nc -vlp 65000
PC 端使用 TCP 连接 RPi 65000 端口
nc -v 192.168.3.254 65000
PC 与 Server 各发一些数据:
Server 端输出:
Listening on [0.0.0.0] (family 2, port 65000)
Connection from rpi4 17150 received!
from server: hi, pc!
from PC: hi, server!
PC 端输出:
Connection to 192.168.3.254 65000 port [tcp/*] succeeded!
from server: hi, pc!
from PC: hi, server!
由此可见,TCP 转发成功
验证 UDP 连接
Server 端监听 UDP 65000 端口
nc -uvlp 65000
PC 端使用 UDP 连接 RPi 65000 端口
nc -uv 192.168.3.254 65000
PC 与 Server 各发一些数据:
Server 端输出:
Listening on [0.0.0.0] (family 2, port 65000)
Connection from rpi4 52981 received!
XXXXXhi, PC!
hi, Server!
PC 端输出:
Connection to 192.168.3.254 65000 port [udp/*] succeeded!
hi, PC!
hi, Server!
验证完成,达到预期效果。