最近部门有个需求,需要对一些客户端IP做白名单,在白名单范围内,才能做一些业务操作。按我们的部门的一贯做法,我们会封装一个client包,提供给业务方使用。(注: 我们的项目是运行在K8S上)本以为这是一个不是很难的功能,部门的小伙伴不到一天,就把功能实现了,他通过本地调试,可以获取到正确的客户端IP,但是发布到测试环境,发现获取到的客户端IP一直是节点的IP,后面那个小伙伴排查了很久,一直没头绪,就找到我帮忙一直排查一下。今天文章主要就是来复盘这个过程
public class IpUtils {
private static Logger logger = LoggerFactory.getLogger(IpUtils.class);
private static final String IP_UTILS_FLAG = ",";
private static final String UNKNOWN = "unknown";
private static final String LOCALHOST_IP = "0:0:0:0:0:0:0:1";
private static final String LOCALHOST_IP1 = "127.0.0.1";
/**
* 获取IP地址
*
*/
public static String getIpAddr(HttpServletRequest request) {
String ip = null;
try {
//以下两个获取在k8s中,将真实的客户端IP,放到了x-Original-Forwarded-For。而将WAF的回源地址放到了 x-Forwarded-For了。
ip = request.getHeader("X-Original-Forwarded-For");
System.out.println("X-Original-Forwarded-For:" + ip);
if (StringUtils.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Forwarded-For");
}
//获取nginx等代理的ip
if (StringUtils.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getHeader("x-forwarded-for");
System.out.println("x-forwarded-for:" + ip);
}
if (StringUtils.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (StringUtils.isEmpty(ip) || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (StringUtils.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
}
if (StringUtils.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
}
//兼容k8s集群获取ip
if (StringUtils.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
System.out.println("getRemoteAddr:" + ip);
if (LOCALHOST_IP1.equalsIgnoreCase(ip) || LOCALHOST_IP.equalsIgnoreCase(ip)) {
//根据网卡取本机配置的IP
InetAddress iNet = null;
try {
iNet = InetAddress.getLocalHost();
} catch (UnknownHostException e) {
logger.error("getClientIp error: {}", e);
}
ip = iNet.getHostAddress();
}
}
} catch (Exception e) {
logger.error("IPUtils ERROR ", e);
}
//使用代理,则获取第一个IP地址
if (!StringUtils.isEmpty(ip) && ip.indexOf(IP_UTILS_FLAG) > 0) {
ip = ip.substring(0, ip.indexOf(IP_UTILS_FLAG));
}
return ip;
}
}
这逻辑看着貌似没问题,因为本地调试可以获取到正确的客户端IP,而测试环境获取不到,大概率是环境有问题。于是就把方向转为定位环境的差异性
我们测试环境的访问流程为客户端–> k8s service nodeport—>pod
通过搜索在https://kubernetes.io/zh-cn/docs/tutorials/services/source-ip/
在这篇文章找到答案。
Kubernetes Service 转发场景下,无论使用 iptbales 或 ipvs 的负载均衡转发模式,转发时都会对数据包做 SNAT,即不会保留客户端真实源 IP
整体流程
上文的链接也贴了解法
1、步骤一:业务pod的配置调度到指定节点
示例
spec:
nodeName: node1 #指定pod节点配置
containers:
- name: pod-name
2、步骤二:将业务的service yaml 默认配置的externalTrafficPolicy: Cluster改为 externalTrafficPolicy: Local
示例
spec:
type: NodePort
externalTrafficPolicy: Local
3、步骤三:通过指定在pod上的node节点 + nodeport进行访问
示例
http://node1:nodeport
假设部署了node1和node2节点,只能通过node1:nodeport才能访问到具体业务,如果通过node2:nodeport,则请求的数据包会被抛弃
通过上述的方案,解决了在测试环境通过service nodeport获取不到正确客户端ip的问题
当测试环境没问题后,将项目发布到UAT环境,然后不出意外的话,又出意外了。
uat的访问流程为 客户端- -> nginx+keepalive --> ingress --> pod
因为访问方式不一样,因此解法又有差异。通过搜索了解到*用户ip的传递依靠的是X-Forwarded-参数。但是默认情况下,ingress是没有开启的 因此我们需要开启。开启需要如下参数
详细的介绍可以查看官网
https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/configmap/#use-forwarded-headers
我们在Ingress Nginx Controller 的 Configmap添加如下内容
apiVersion: v1
kind: ConfigMap
......
data:
compute-full-forwarded-for: "true"
use-forwarded-headers: "true"
forwarded-for-header:"X-Forwarded-For"
配置后,发现没鸟用,没有效果。后面查了很多资料,发现网上都是那么配的,后面就觉得是不是nginx - keepalive 这一环节出了啥问题,于是就问了一下运维,看他nginx那边是否有配置X-Forwarded-For,他说没有,那我就问他能否配置一下,他的回答是因为nginx那边启用了 ssl_preread 模块无法使用X-Forwarded-For
后面就问他能否改下,他回答说是后面公司要采用F5了,到时候在配置一下就好。而他目前事情比较多,没时间帮我弄这个。
由于业务比较赶,运维又没空搞,于是就和业务那边沟通,采取了折中方案,就是通过自定义请求头,我们在client包配置了一个属性,那个属性用来让业务将白名单ip填进去,示例
lybgeek:
whilte-ips: 192.168.1.1,192.168.2.1
在业务项目启动的时候,client包会自动将配置的白名单塞入请求头
header("x-custom-forwarded-for",whilteIps)
服务端那边获取客户端ip做如下改造
@Slf4j
public final class IPHelper {
private IPHelper(){}
private static final String IP_UTILS_FLAG = ",";
private static final String UNKNOWN = "unknown";
private static final String LOCALHOST_IP = "0:0:0:0:0:0:0:1";
private static final String LOCALHOST_IP1 = "127.0.0.1";
private static final String[] headersToTry = {
//在k8s中,将真实的客户端IP,放到了x-Original-Forwarded-For。而将WAF的回源地址放到了 x-Forwarded-For了。
"X-Original-Forwarded-For",
"X-Forwarded-For",
"Proxy-Client-IP",
"WL-Proxy-Client-IP",
"HTTP_X_FORWARDED_FOR",
"HTTP_X_FORWARDED",
"HTTP_X_CLUSTER_CLIENT_IP",
"HTTP_CLIENT_IP",
"HTTP_FORWARDED_FOR",
"HTTP_FORWARDED",
"HTTP_VIA",
"REMOTE_ADDR",
// 自定义请求头
"X-Custom-Forwarded-For",
};
/**
* 获取用户的真正IP地址
*
* @param request request对象
* @return 返回用户的IP地址
*/
@SneakyThrows
public static String getIpAddr(HttpServletRequest request) {
String ip = null;
for (String header : headersToTry) {
ip = request.getHeader(header);
if (StringUtils.hasText(ip) && !UNKNOWN.equalsIgnoreCase(ip)){
log.info("hit the target client ip -> 【{}】 by header --> 【{}】",ip,header);
return ip;
}
}
//兼容k8s集群获取ip
if (org.springframework.util.StringUtils.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
if (LOCALHOST_IP1.equalsIgnoreCase(ip) || LOCALHOST_IP.equalsIgnoreCase(ip)) {
//根据网卡取本机配置的IP
InetAddress iNet = null;
try {
iNet = InetAddress.getLocalHost();
} catch (UnknownHostException e) {
log.error("getIpAddr error: {}", e);
}
ip = iNet.getHostAddress();
}
log.info("hit the target client ip -> 【{}】 by method 【getRemoteAddr】 ",ip);
}
//使用代理,则获取第一个IP地址
if (!org.springframework.util.StringUtils.isEmpty(ip) && ip.indexOf(IP_UTILS_FLAG) > 0) {
ip = ip.substring(0, ip.indexOf(IP_UTILS_FLAG));
}
return ip;
}
}
其实做的事情,就将原来的工具类稍微重构了一下,并加入自定义请求头X-Custom-Forwarded-For
这次的复盘总结就是很多东西没那么想当然,有些简单的东西,里面可能也有有坑。当遇到跨部门合作时,如果遇到一些不可抗力因素,我们除了向上反馈之外,还要有兜底方案,不然会非常被动