如何让nginx服务和ocserv共用443端口

前言与需求背景

在许多应用中,我们可能需要让多个服务共享同一端口。例如,你可能有一个Web服务器(如Nginx)和一个VPN服务器(如OpenConnect VPN,简称ocserv),你希望它们都使用443端口。这样做的好处是可以提高网络性能,因为操作系统通常会为443端口提供更高的处理优先级,从而得到更快的响应。此外,这种设置也可以简化防火墙的配置,因为只需要开放一个端口。

然而,实现这种配置并不简单。首先,Nginx和ocserv都需要独占443端口,因此不能直接将它们绑定到同一端口。其次,尽管Nginx可以使用其stream模块来实现TCP级别的反向代理,但这需要额外的配置,并且可能导致一些问题。例如,ocserv需要了解客户端的真实IP,但在Nginx的反向代理后,ocserv只能看到Nginx服务器的IP。

为什么选择HAProxy

为了解决这个问题,我们可以使用HAProxy作为负载均衡器,将流量分发到Nginx和ocserv。HAProxy是一个高性能的负载均衡器和反向代理服务器,可以处理HTTP和TCP协议。相比Nginx,HAProxy提供了更强大和灵活的负载均衡功能,包括基于内容的路由,健康检查,连接持久化等。

更重要的是,HAProxy支持SNI(Server Name Indication),这是一个TLS扩展,允许客户端在握手过程中指定请求的主机名。我们可以利用SNI,将基于不同域名的请求分发到不同的服务器,从而实现共享443端口。

实践方案

首先,我们需要修改ocserv的配置,开启代理协议支持,将监听地址设为本地地址。编辑ocserv.conf文件,添加以下两行:

listen-proxy-proto = true
listen-host = 127.0.0.1

然后,我们需要配置HAProxy,将基于不同域名的请求转发到不同的服务器。以下是一个示例配置,假设你的服务器的IP地址为172.19.17.203

frontend https
   bind 172.19.17.203:443
   mode tcp
   tcp-request inspect-delay 5s
   tcp-request content accept if { req_ssl_hello_type 1 }

   use_backend ocserv if { req_ssl_sni -i vpn.domain.com }
   use_backend nginx if { req_ssl_sni -i your.servicedomain.com }
   default_backend ocserv

这个配置的含义是:如果客户端的SNI匹配vpn.domain.com,则将请求转发到ocserv;如果SNI匹配your.servicedomain.com,则将请求转发到Nginx;否则,默认将请求转发到ocserv。

接下来,我们配置HAProxy的后端。对于ocserv,我们将其设置为监听在127.0.0.1:443,并启用代理协议:

backend ocserv
   mode tcp
   option ssl-hello-chk
   server ocserv 127.0.0.1:443 send-proxy-v2

对于Nginx,我们将其设置为监听在127.0.0.2:443:

backend nginx
   mode tcp
   option ssl-hello-chk
   server nginx 127.0.0.2:443 check

最后,我们需要修改Nginx的配置,将其监听地址改为127.0.0.2。以下是一个示例配置:

server {
    listen 127.0.0.2:443 ssl;
    server_name your.servicedomain.com;

    # 其他配置项...
}

这样,当客户端发送请求时,HAProxy会根据SNI将请求路由到正确的服务器,而不管它们是否共享443端口。

注意事项

在实施此方案时,需要注意以下几点:

  • 确保你的客户端支持SNI。大多数现代浏览器和操作系统都支持SNI,但一些较旧的系统可能不支持。
  • 在使用HAProxy时,需要确保它可以正确识别并处理TLS流量。在上述配置中,我们使用了mode tcp和tcp-request content accept if { req_ssl_hello_type 1 }来实现这一点。
  • HAProxy的配置可以根据你的具体需求进行调整。例如,你可以添加更多的后端服务器,或者根据其他条件(如IP地址或请求路径)进行路由。

总的来说,通过使用HAProxy,我们可以实现在同一服务器上共享443端口的需求,同时避免了Nginx的配置复杂性和ocserv的限制。

开箱即用

最后给出开箱即用的配置

haproxy.cfg 配置

global
    log 127.0.0.1 local2
    chroot /var/lib/haproxy
    pidfile /var/run/haproxy.pid
    maxconn 4000
    user haproxy
    group haproxy
    daemon

defaults
    mode http
    log global
    option dontlognull
    timeout connect         10s
    timeout client          1m
    timeout server          1m

frontend https
   # ip addr show eth0 | grep inet | awk '{ print $2; }' | sed 's/\/.*$//' 
   bind 172.19.17.203:443
# bind fe80::250:56ff:fe0d:ae9e:443
   mode tcp
   tcp-request inspect-delay 5s
   tcp-request content accept if { req_ssl_hello_type 1 }

   use_backend ocserv if { req_ssl_sni -i vpn.domain.com }
   use_backend nginx if { req_ssl_sni -i your.servicedomain.com }

   default_backend ocserv

backend ocserv
   mode tcp
   option ssl-hello-chk
   # pass requests to 127.0.0.1:443. Proxy protocol (v2) header is required by ocserv.
   server ocserv 127.0.0.1:443 send-proxy-v2
# server ocserv6 [::1]:443 send-proxy-v2

backend nginx
   mode tcp
   option ssl-hello-chk
   server nginx 127.0.0.2:443 check

your.servicedomain.com.conf 配置

server {
    listen 80;
    server_name your.servicedomain.com;
    return 301 https://$host$request_uri;
}

server {
    listen 127.0.0.2:443 ssl;
    server_name your.servicedomain.com;

    ssl_certificate /etc/nginx/ssl/your.servicedomain.com_ecc/your.servicedomain.com.cer;
    ssl_certificate_key /etc/nginx/ssl/your.servicedomain.com_ecc/your.servicedomain.com.key;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384';   
    ssl_prefer_server_ciphers on;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 1d;
    ssl_session_tickets off;

    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
    add_header X-Content-Type-Options nosniff;
    add_header X-XSS-Protection "1; mode=block";
    add_header Content-Security-Policy "frame-ancestors 'self' *.vanjay.cn";

    location / {
        proxy_pass http://127.0.0.1:5230;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

ocserv.conf 配置

#auth = "plain[passwd=/etc/ocserv/ocpasswd]"
auth = "certificate"
tcp-port = 443
run-as-user = ocserv
run-as-group = ocserv
socket-file = ocserv.sock
chroot-dir = /var/lib/ocserv
isolate-workers = false
max-clients = 32
max-same-clients = 8
keepalive = 32400
dpd = 30
mobile-dpd = 90
try-mtu-discovery = false
server-cert = /etc/ocserv/pki/vpn.domain.com_ecc/vpn.domain.com.cer
server-key = /etc/ocserv/pki/vpn.domain.com_ecc/vpn.domain.com.key
crl = /etc/ocserv/pem/crl.pem
ca-cert = /etc/ocserv/pem/ca-cert.pem

listen-proxy-proto = true
listen-host = 127.0.0.1

cert-user-oid = 2.5.4.3
compression = true
no-compress-limit = 256
tls-priorities = "NORMAL:%SERVER_PRECEDENCE:%COMPAT:-VERS-SSL3.0"
auth-timeout = 240
min-reauth-time = 300
max-ban-score = 50
ban-reset-time = 300
cookie-timeout = 300
deny-roaming = false
rekey-time = 172800
rekey-method = ssl
use-utmp = true
use-occtl = true
pid-file = /var/run/ocserv.pid
device = vpns
predictable-ips = true
default-domain = vpn.domain.com
ipv4-network = 192.168.101.0/24
dns = 1.1.1.1
dns = 8.8.8.8
ping-leases = false

cisco-client-compat = true
dtls-legacy = true
# user-profile= profile.xml
connect-script = /etc/ocserv/connect-script
disconnect-script = /etc/ocserv/connect-script

config-per-user = /etc/ocserv/config-per-user/
config-per-group = /etc/ocserv/config-per-group/
default-group-config = /etc/ocserv/config-per-group/others
default-select-group = others
auto-select-group = false

# config-per-user = /etc/ocserv/config-per-user/
# config-per-group = /etc/ocserv/config-per-group/

# default-user-config = /etc/ocserv/defaults/user.conf
# default-group-config = /etc/ocserv/defaults/group.conf

# (x*1024)*1024/8
rx-data-per-sec = 4194304
tx-data-per-sec = 4194304

no-route = 192.168.0.0/255.255.0.0
no-route = 47.242.201.43/255.255.255.255

后话,Nginx与HAProxy的比较

看起来 haproxy 和 nginx 都能实现反向代理,那他们各自有什么优劣以及适用什么场景呢?这里做个简单总结。
Nginx和HAProxy都是非常强大且广泛使用的反向代理服务器。然而,它们各自的优势和更适用的场景略有不同。

Nginx

Nginx是一个强大的Web服务器,也可以作为邮件代理服务器,以及通用的TCP/UDP代理服务器。它被设计为处理非常高的并发连接,这得益于其基于事件的架构。

Nginx的主要优点包括:

  • 静态文件服务:Nginx是一个出色的静态文件服务器,能够快速、高效地为大量用户提供服务。
  • 灵活的配置:Nginx的配置文件非常强大且灵活,可以支持各种复杂的路由和负载均衡策略。
  • HTTPS和HTTP/2支持:Nginx支持最新的Web协议,包括HTTPS和HTTP/2。

然而,Nginx的某些功能,如动态负载均衡和高级健康检查,在社区版中是不可用的。这些功能只在Nginx Plus(商业版)中可用。

HAProxy

HAProxy是一个专注于高可用性、负载均衡和代理的开源软件。它可以处理非常大量的并发连接,并提供丰富的负载均衡算法和健康检查功能。

HAProxy的主要优点包括:

  • 高性能:HAProxy被设计为处理非常高的并发连接,且拥有优秀的性能。
  • 灵活的负载均衡:HAProxy提供了多种负载均衡算法,可以根据需要进行选择和配置。
  • 先进的健康检查:HAProxy可以进行非常详细的健康检查,包括对HTTP、TCP和其他协议的支持。

但是,HAProxy在处理静态内容或者作为全功能的Web服务器方面,可能不如Nginx强大。

结论

选择Nginx还是HAProxy,取决于你的具体需求。如果你主要是为了提供静态内容或需要一个全功能的Web服务器,Nginx可能是更好的选择。如果你需要一个专注于高可用性、负载均衡和代理的解决方案,HAProxy可能更适合你。

总的来说,Nginx和HAProxy都是非常优秀的工具,它们在很多情况下可以互补使用,而不是相互替代。在本文的情境中,我们使用了HAProxy来处理基于SNI的路由,同时利用Nginx的Web服务能力,共同实现了一种强大且灵活的解决方案。这样的组合充分利用了这两个工具的各自优势,为我们的服务提供了可靠的网络连接。

最后,不管你选择使用哪种工具,都应当确保理解其工作原理和配置细节,这样才能最大化地发挥其作用。同时,始终保持对工具的更新和安全性的关注,是维持服务稳定运行的关键。

参考

openconnect-vpn-server-ocserv-ubuntu-20-04-lets-encrypt

ocserv官方使用haproxy的SNI解决方案

ocserv443端口复用