nginx传递真实客户端ip给后端

于 2023-05-05 发布

由于DOH服务器端需要客户端IP来更好地向upstream请求距离客户端比较近的IP地址——亦即所谓的edns,或者edns client subnet——所以需要让nginx能够在反向代理的时候传递客户端IP给后端。由于特殊需要,需要保持SNI分流即第4层分流。之前一直搞不了,直到今天看到stackoverflow的回答,在第4层分流的情况下,nginx是无法获知客户端IP的,所以给后端的IP永远都是nginx自己的IP;但是如果使用proxy protocol,那就可以知道客户端IP甚至Port。总而言之,现在需要实现的是:1. 打开proxy protocol的同时兼容不支持proxy protocol的后端;2. 在proxy protocol下传递客户端真实IP。

首先是第一方面,需要在打开proxy protocol的情况下,再由nginx本身去掉这一层protocol,然后传给后端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
stream {
	log_format stream_basic '$remote_addr [$time_local] '
	'$protocol $status $bytes_sent $bytes_received '
	'$session_time';

	# 这里就是 SNI 识别,将域名映射成一个配置名
	map $ssl_preread_server_name $backend_name {
    # 为不支持proxy protocol的后端设置一个匹配规则
		some.domain.com unix:/run/nginx-remove-proxy-stream.sock;
		# 域名都不匹配情况下的默认值,亦即nginx提供的ssl网站们
		default 127.0.0.1:48443;
	}
  # 去掉proxy protocol后转发给后端
	server {
    # 注意末端的proxy_protocol,每一个都需要有这个才能正常提供服务
    # 否则虽然nginx -t通过了,但是运行起来就会显示fail to read proxy xxx的错误
		listen unix:/run/nginx-remove-proxy-stream.sock proxy_protocol;
		proxy_pass 127.0.0.1:8772;
	}
  # 监听 443 并开启 ssl_preread
	server {
		listen 443 reuseport;
		proxy_pass $backend_name;
		ssl_preread on;
		proxy_protocol on;
	}
}

http {
	log_format proxy_main '$proxy_protocol_addr [$time_local] '
	'"$request" $status $body_bytes_sent '
	'"$http_referer" "$http_user_agent"';
  access_log /var/log/nginx/access.log proxy_main;
  include /etc/nginx/sites-enabled/*;
}

需要注意的有两点:1. http部分中,log_format不再写$remote_addr了,而是$proxy_protocol_addr,这就是客户端IP;2. 注释中“注意末端的proxy_protocol,每一个都需要有这个才能正常提供服务”,在上面的配置示例中,直接用一个server块来去掉了这个proxy_protocol,从而完成了本文需要解决的第一个问题;而对于由nginx提供服务的其他网站,每一个都需要在listen那一行里有一个proxy_protocol指令才能正常工作。如果不明白,看看下面这个虚拟网站的conf就知道了——同时,这个配置也体现了怎么传递真实IP给后端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
server {
  listen 48443 ssl http2 proxy_protocol;
  # 域名,多个以空格分开
  server_name some2.domain.com;
  # 这里写其他配置
  
  # 这里是转发给DOH的示例
  location /dns-query {
    proxy_redirect off;
    proxy_http_version 1.1;
    proxy_set_header Host $http_host;
    # show real IP
    # 把$proxy_protocol_addr所代表的真实IP用X-Real-IP传递给后端
    proxy_set_header X-Real-IP $proxy_protocol_addr;
    proxy_set_header X-Forwarded-For $proxy_protocol_addr;
    proxy_pass http://127.0.0.1:8080/dns-query;
  }
}

这样配置好之后,DOH服务器的日志里就显示出了客户端的IP,就能够利用起edns的好处了。

目录