乐趣区

关于kubernetes:聊聊部署在K8S的项目如何获取客户端真实IP

前言

最近部门有个需要,须要对一些客户端 IP 做白名单,在白名单范畴内,能力做一些业务操作。按咱们的部门的一贯做法,咱们会封装一个 client 包,提供给业务方应用。(注: 咱们的我的项目是运行在 K8S 上)本认为这是一个不是很难的性能,部门的小伙伴不到一天,就把性能实现了,他通过本地调试,能够获取到正确的客户端 IP,然而公布到测试环境,发现获取到的客户端 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 环境,而后不出意外的话,又出意外了。

uat 的拜访流程为 客户端 - -> nginx+keepalive –> ingress –> pod

因为拜访形式不一样,因而解法又有差别。通过搜寻理解到 用户 ip 的传递依附的是 X -Forwarded-* 参数。然而默认状况下,ingress 是没有开启的 因而咱们须要开启。开启须要如下参数

  • use-forwarded-headers: 如果设置为 True 时,则将设定的 X -Forwarded-* Header 传递给后端,
    当 Ingress 在 L7 代理 / 负载均衡器之后应用此选项。如果设置为 false 时,则会疏忽传入的 X -Forwarded-*Header,
    当 Ingress 间接裸露在互联网或者 L3/ 数据包的负载均衡器前面, 并且不会更改数据包中的源 IP 请应用此选项。
  • forwarded-for-header: 设置用于标识客户端的原始 IP 地址的 Header 字段。默认值 X -Forwarded-For。如果想批改为自定义的字段名,则能够在 configmap 的 data 配置块下增加:forwarded-for-header: “THE_NAME_YOU_WANT”
  • compute-full-forwarded-for: 将 remote address 附加到 X-Forwarded-For
    Header 而不是替换它。当启用此选项后端应用程序负责依据本人的受信赖代理列表排除并提取客户端 IP。

具体的介绍能够查看官网

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

总结

这次的复盘总结就是很多货色没那么想当然,有些简略的货色,外面可能也有有坑。当遇到跨部门单干时,如果遇到一些不可抗力因素,咱们除了向上反馈之外,还要有兜底计划,不然会十分被动

退出移动版