iptables redirect 劫持跳转引起 Go 服务故障
😅 这是一个很有趣的事情。由于流量突增临时扩充多个node部署服务,但遇到一个问题全量接口调用失败总是返回无关的返回结果。简单说在服务里本调用其他服务接口,返回的结果莫名其妙。
由于出问题节点已经被修复,所以该问题是在虚拟机里重现的。
排查问题
首先确认其他接口方是否收到该请求,日志没有里没有请求,考虑到是自研封装的golang web框架可能会有问题,所以调用tcpdump抓包,结果看不到请求报文。
那么可以确认请求包没有来这台服务器上,通常来说这类请求大多出现在dns解析错误引起的。
在本机dig dns解析无异常,由于请求构建起来有些麻烦,所以没有单独拿到curl下测试。但通过 lsof 和 netstat 可以看到已建立连接是正常的解析ip,但是对端确实没有收到该请求。
重点,客户端可以看到已经建立的连接,但是服务端看不到。
// xiaorui.cc
pusher 10902 root 3u IPv4 314336 0t0 TCP 192.168.124.13:54504->123.56.223.52:80 (ESTABLISHED)
通过strace是可以看到服务请求过程中所涉及到的系统调用。
先是dns请求,当开启dns缓存服务nscd时,程序里的域名解析不是直接连接resolver.conf的nameserver地址,而是直接跟nscd socket通信,nscd作为缓存服务有个名为hosts的持久化db。当nscd无域名的缓存时会跟nameserver进行udp请求。如果无nscd服务,那么strace可以看到nameserver建连及解析过程。
总之ip的解析正确的。
// xiaori.cc
[pid 10007] connect(3, {sa_family=AF_LOCAL, sun_path="/var/run/nscd/socket"}, 110 <unfinished ...>
[pid 10007] sendto(3, "\2\0\0\0\r\0\0\0\6\0\0\0hosts\0", 18, MSG_NOSIGNAL, NULL, 0 <unfinished …>
[pid 10007] close(3 <unfinished ...>
再是http的数据请求,connect ip过程没问题,发送的请求体也是没问题的,但返回值有问题。
// xiaorui.cc
fcntl(3, F_SETFL, O_RDWR|O_NONBLOCK) = 0
connect(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("123.56.223.52")}, 16) = -1 EINPROGRESS (Operation now in progress)
poll([{fd=3, events=POLLOUT|POLLWRNORM}], 1, 0) = 1 ([{fd=3, revents=POLLOUT|POLLWRNORM}])
getsockopt(3, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
getpeername(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("123.56.223.52")}, [16]) = 0
getsockname(3, {sa_family=AF_INET, sin_port=htons(54508), sin_addr=inet_addr("192.168.124.13")}, [16]) = 0
sendto(3, "GET /6666 HTTP/1.1\r\nUser-Agent: curl/7.29.0\r\nHost: xiaorui.cc\r\nAccept: */*\r\n\r\n", 78, MSG_NOSIGNAL, NULL, 0) = 78
poll([{fd=3, events=POLLIN|POLLPRI|POLLRDNORM|POLLRDBAND}], 1, 0) = 0 (Timeout)
poll([{fd=3, events=POLLIN}], 1, 1000) = 1 ([{fd=3, revents=POLLIN}])
poll([{fd=3, events=POLLIN|POLLPRI|POLLRDNORM|POLLRDBAND}], 1, 0) = 1 ([{fd=3, revents=POLLIN|POLLRDNORM}])
recvfrom(3, "HTTP/1.1 404 Not Found\r\nServer: ...
...
大疑问?
奇怪了。。。
那么再尝试使用tcpdump来抓包。每次请求时都会跟127.0.0.1:80建连,请求体也会转到127.0.0.1:80上。这类情况很像是做了端口劫持跳转。
在iptables里发现了redirect跳转。所有output请求会转到sidecar_outbound自定义链,在sidecar自定义链中又把目标地址中80的请求转到本地的80端口上。
// xiaorui.cc
Chain OUTPUT (policy ACCEPT)
target prot opt source destination
OUTPUT_direct all -- anywhere anywhere
SIDECAR_OUTBOUND tcp -- anywhere anywhere
...
Chain SIDECAR_OUTBOUND (1 references)
target prot opt source destination
REDIRECT tcp -- anywhere 123.56.223.0 tcp dpt:http redir ports 80
...
本地为啥接收劫持的http请求?sidecar呗… 现在很火的istio service mesh就是使用iptables redirect方法劫持数据到envoy里,这样减少了用户对微服务的使用成本。把服务发现、负载均衡、熔断器/限频器、追踪等等都放在sidecar里,用户只需要关注自己的业务就可以了。
那为啥这回sidecar不能正确转发了?
因为该sidecar是半成品,开发这玩意的人跑路了,然后策略依旧存在。
如何测试?
iptables劫持脚本
// xiaorui.cc
iptables -t nat -N SIDECAR_OUTBOUND
iptables -t nat -A OUTPUT -p tcp -j SIDECAR_OUTBOUND
iptables -t nat -A SIDECAR_OUTBOUND -p tcp -d 123.56.0.0 --dport 80 -j REDIRECT --to-port 80
总结
通过strace和lsof都不好分析到问题,而tcpdump是可以的。现在想想其实通过netstat也是可以发现问题的,奈何在使用netstat时加入了pid过滤。
记得前段时间一个同事出现过域名拼写错误引起的问题,这哥们一出问题就怀疑是不是 go web 问题,再就是怀疑到 golang 本身,最后都怀疑到操作系统。😅 所以说要冷静,别瞎想….