原文:知乎文章
K3s + Traefik 获取真实客户端公网 IP:一套可复用的配置
把服务迁到 K3s 后,我遇到过一个很常见的问题:
业务代码里拿到的请求 IP 总是 10.42.x.x、10.43.x.x 或 100.64.x.x,而不是客户端真正的公网 IP。
我最初把问题归因于 Flannel,并尝试改用 Calico。
后来重新梳理完整流量链路后发现,这个判断并不准确。
真实客户端 IP 是否能传到应用,主要取决于公网入口如何进入 Traefik,
以及应用是否正确读取反向代理头;
它和使用 Flannel 还是 Calico 没有直接关系。
本文记录的方案基于下面这套网络结构:
- K3s
v1.34.3+k3s3 - Flannel VXLAN,节点间流量通过
tailscale0 - Traefik
v3.6.7 - 一个或多个 K3s 节点拥有公网入口
- Traefik 以
DaemonSet + hostNetwork方式直接监听所在节点的80/443 - 业务 Pod 通过普通
ClusterIP Service + Ingress接入
这套方案不需要替换 Flannel,也不依赖 externalTrafficPolicy: Local。
先理解:应用看到的 IP 为什么会变
当前流量链路如下:
公网客户端
│
│ 访问任一入口节点的公网 IP:80/443
▼
云厂商公网 NAT / 安全组
│
│ 原始 TCP 源地址仍是客户端公网 IP
▼
K3s 入口节点:80/443
Traefik(DaemonSet + hostNetwork)
│
│ HTTP 反向代理,并写入:
│ X-Forwarded-For: <客户端公网 IP>
│ X-Real-Ip: <客户端公网 IP>
▼
ClusterIP Service
│
│ Flannel VXLAN over tailscale0
▼
业务 Pod这里要区分两种 IP:
remoteAddr、request.getRemoteAddr()或req.socket.remoteAddress是当前 TCP 连接对端的地址。业务 Pod 的直接对端是 Traefik,所以这里出现 Pod IP 或节点 IP 很正常。- 原始客户端 IP 由 Traefik 放在
X-Forwarded-For和X-Real-Ip中。业务框架需要启用“信任反向代理”后再读取。
Traefik 默认就会为代理请求添加这些头,不需要额外写一个 Header Middleware。
一、入口节点不需要硬编码
获取真实 IP 并不依赖某个固定节点。只需要满足一个条件:
公网流量到达某个节点时,这个节点本地必须有 Traefik 在监听 80/443。本文把 Traefik 配置成 DaemonSet。默认不写 nodeSelector 时,它会运行在所有可调度且满足污点容忍条件的节点上。公网 IP、DNS 或外部负载均衡器可以指向其中任意一个 Traefik 正常运行的节点。
如果只想让部分节点承担公网入口,再给这些节点统一加一个通用标签:
kubectl label node <node-name> ingress-ready=true --overwrite随后在本文的 Helm values 中按需增加:
nodeSelector:
ingress-ready: "true"这个标签只负责控制 Traefik 调度范围,不是保留真实 IP 的必要配置。
增加多个入口节点时,给它们加同一个标签即可,不需要在配置中写任何具体节点名称。
二、用 HelmChartConfig 持久化 Traefik 配置
不要直接修改:
/var/lib/rancher/k3s/server/manifests/traefik.yaml这是 K3s 自带的打包清单,K3s 重启或升级时可能重新生成。正确方式是创建同名的 HelmChartConfig。
执行下面的配置:
kubectl apply -f - <<'EOF'
apiVersion: helm.cattle.io/v1
kind: HelmChartConfig
metadata:
name: traefik
namespace: kube-system
spec:
failurePolicy: reinstall
valuesContent: |-
deployment:
kind: DaemonSet
dnsPolicy: ClusterFirstWithHostNet
# Traefik 直接使用节点网络,在所在节点监听 80/443。
# 公网请求不再经过 NodePort、ServiceLB 或跨节点转发。
hostNetwork: true
ports:
web:
port: 80
websecure:
port: 443
# Service 只用于集群内服务发现和 publishedService 状态。
# 公网流量实际走上面的 hostNetwork 端口。
service:
type: ClusterIP
podSecurityContext:
runAsUser: 0
runAsGroup: 0
runAsNonRoot: false
seccompProfile:
type: RuntimeDefault
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
add:
- NET_BIND_SERVICE
updateStrategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1
maxSurge: 0
EOF
等待配置生效:
kubectl rollout status daemonset/traefik -n kube-system –timeout=180s
kubectl get pods -n kube-system
-l app.kubernetes.io/name=traefik
-o wide
kubectl get svc traefik -n kube-system正确结果应该满足:
- Traefik 是
DaemonSet。 - Traefik Pod 运行在预期节点上;如果配置了可选的
nodeSelector,就只会运行在带相应标签的节点上。 - Pod IP 和所在节点的 Tailscale IP 相同,说明
hostNetwork已生效。 traefikService 类型是ClusterIP。- 所有被公网流量选中的节点,其
80/443都已由 Traefik 监听。
可以在任意入口节点上继续检查:
sudo ss -lntp | grep -E ‘:(80|443)\b’最后确认云安全组和主机防火墙允许公网访问 TCP 80/443,
域名的 A 记录指向任一正常运行 Traefik 的节点公网 IP。
三、用 whoami 验证整条链路
不要一上来就从业务日志猜问题。部署一个临时回显服务,直接观察 Traefik 传给后端的请求头:
kubectl apply -f - <<‘EOF’
apiVersion: v1
kind: Namespace
metadata:
name: ip-check
apiVersion: apps/v1
kind: Deployment
metadata:
name: whoami
namespace: ip-check
spec:
replicas: 1
selector:
matchLabels:
app: whoami
template:
metadata:
labels:
app: whoami
spec:
containers:
- name: whoami
image: traefik/whoami:v1.11.0
ports:
- name: http
containerPort: 80
apiVersion: v1
kind: Service
metadata:
name: whoami
namespace: ip-check
spec:
selector:
app: whoami
ports:
- name: http
port: 80
targetPort: http
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: whoami
namespace: ip-check
spec:
ingressClassName: traefik
rules:
- host: ip-check.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: whoami
port:
number: 80
EOF
把 ip-check.example.com 换成自己的测试域名。
如果暂时不想配置 DNS,可以从集群外的电脑直接指定 Host:
curl -sS
-H ‘Host: ip-check.example.com’
http://<ingress-node-public-ip>/一定要从集群外测试,例如家里宽带、手机 5G 或另一台公网服务器。从集群节点内部请求,只能验证内网路径,不能证明公网客户端 IP 是否保留。
响应中应看到类似内容:
RemoteAddr: <Traefik 连接后端时使用的源 IP>:xxxxx
X-Forwarded-For: <发起 curl 的公网 IP>
X-Real-Ip: <发起 curl 的公网 IP>
X-Forwarded-Proto: httpRemoteAddr 可能是 10.42.x.x、100.64.x.x 或其他入口节点地址。它是 Traefik 建立后端连接时使用的源地址,并不代表配置失败。判断是否成功,要看 X-Forwarded-For 或 X-Real-Ip 是否包含测试客户端的公网 IP。
验证完成后删除临时资源:
kubectl delete namespace ip-check四、业务代码必须信任 Traefik 代理头
入口配置正确后,如果业务代码仍然读取 TCP 对端地址,
拿到的依然会是 Traefik 或节点 IP。需要按所用框架启用代理支持。
Spring Boot
在 application.yml 中启用容器对转发头的原生支持:
server:
forward-headers-strategy: native然后继续使用:
request.getRemoteAddr();如果项目已有自定义 IP 工具,也可以读取 X-Forwarded-For,
但不要简单地无条件取客户端传来的第一个值;应该只信任来自 Traefik 的代理链。
Express
显式信任集群内的 Traefik 来源,再读取 req.ip:
app.set(’trust proxy’, [
’loopback’,
‘10.42.0.0/16’,
‘100.64.0.0/10’,
]);
app.get(’/ip’, (req, res) => {
res.json({ ip: req.ip, chain: req.ips });
});
不要为了省事直接使用 app.set(’trust proxy’, true)。如果入口没有清洗伪造的 X-Forwarded-For,客户端可能伪造任意 IP。
后端还有一层 Nginx
如果链路是 Traefik -> Nginx -> 应用,Nginx 也需要恢复真实地址:
set_real_ip_from 10.42.0.0/16;
set_real_ip_from 100.64.0.0/10;
real_ip_header X-Forwarded-For;
real_ip_recursive on;这里的 10.42.0.0/16 是本文集群的 Pod CIDR,100.64.0.0/10 是 Tailscale 地址段。生产环境可以根据 whoami 显示的 RemoteAddr,进一步收窄为实际 Traefik 节点或代理地址,不必无条件信任整个网段。
应用再把 Nginx 视为可信代理。原则始终一样:从最靠近应用的一侧开始,只跳过明确可信的代理地址,遇到的第一个非可信地址才是客户端地址。
五、为什么当前配置不需要 externalTrafficPolicy: Local
externalTrafficPolicy 只影响 NodePort、LoadBalancer 和 ExternalIP 这类 Service 的外部流量。
当前 Traefik Service 是 ClusterIP,公网请求通过 hostNetwork 直接到达入口节点上的 Traefik,根本不经过 Traefik Service。因此下面这种修改对当前入口链路没有作用:
kubectl patch svc traefik -n kube-system
-p ‘{“spec”:{“externalTrafficPolicy”:“Local”}}’这条命令甚至可能因为 Service 类型是 ClusterIP 而被拒绝。
只有改回 K3s 默认的 LoadBalancer Service + ServiceLB,或使用 NodePort、云负载均衡器时,才需要重点考虑:
service:
type: LoadBalancer
spec:
externalTrafficPolicy: LocalLocal 会避免 kube-proxy 把请求再转发到其他节点并做 SNAT,从而保留源 IP;代价是流量打到没有本地 Traefik 端点的节点时会被丢弃。所以外部负载均衡器必须只选择真正运行 Traefik 的节点。
六、前面还有 CDN 或负载均衡器怎么办
本文主方案假设公网客户端直接访问 K3s 入口节点。如果前面还有 Cloudflare、CDN、Nginx 或云负载均衡器,Traefik 在 TCP 层看到的就会是上游代理 IP,而不是客户端 IP。
这时要在 Traefik 的 web 和 websecure 入口上配置 forwardedHeaders.trustedIPs,并且只填写上游代理官方公布的出口网段。例如:
ports:
web:
forwardedHeaders:
trustedIPs:
- “<upstream-proxy-cidr>”
websecure:
forwardedHeaders:
trustedIPs:
- “<upstream-proxy-cidr>"不要在生产环境使用:
forwardedHeaders:
insecure: true这等于信任任何客户端自己提交的 X-Forwarded-For,会让 IP 封禁、审计日志和限流规则都可以被绕过。
如果上游使用的是 PROXY Protocol,则必须让上游和 Traefik 两端同时启用,并在 Traefik 中通过 proxyProtocol.trustedIPs 限定可信来源。不能把普通 HTTP 转发头和 PROXY Protocol 混为一谈。
最终结论
这套 K3s 集群要拿到正确的客户端公网 IP,关键只有三层:
- 公网
80/443只能进入实际运行 Traefik 的节点;节点名称和标签都可以自由选择。 - Traefik 使用
DaemonSet + hostNetwork直接接收公网连接,并自动写入X-Forwarded-For、X-Real-Ip。 - 业务应用启用可信代理支持,从代理链中读取第一个非可信地址。
Flannel 继续负责 Pod 网络,tailscale0 继续负责跨云节点互联,不需要为了真实客户端 IP 把 CNI 换成 Calico。
参考资料: