来自于 frockee 的备份,以防源站数据丢失
haproxy透传用户ip-方法和原理
前言
haproxy透传用户ip到服务器端, 已经有非常成熟的技术,网上有非常多的资料,很多是可以work的。但是如果你踩得坑足够多,你就会知道,将网上的方案应用于生成环境,一定要慎之又慎;而作为一个严肃的IAAS基础设施研发人员(UCloud云数据库团队),搞清楚这些配置和方法背后的道理,是很有必要的。为了解决haproxy透传用户Ip的问题,我花了几天时间,了解了其中最主流的一种技术(linux tproxy)的原理和实现,并总结出了一个可能是最精简的的配置方法。下面是详细的内容。
1. 方法
主要参考的一些资料:
http://www.360doc.com/content/17/0204/11/36792569_626412598.shtml http://www.360doc.com/content/13/0821/17/13047933_308812287.shtml http://kb.snapt-ui.com/wp-content/uploads/2012/03/Snapt-HAProxy-TPROXY.pdf http://forlinux.blog.51cto.com/8001278/1415350 https://www.nginx.com/blog/ip-transparency-direct-server-return-nginx-plus-transparent-proxy/
1.1 重新编译haproxy
wget http://haproxy.1wt.eu/download/2.1/src/haproxy-2.1.3.tar.gz tar zxvf haproxy-2.1.3.tar.gz cd haproxy-2.1.3 yum install gcc gcc-c++ autoconf automake openssl-devel systemd-devel -y yum -y groupinstall "Development Tools" #make TARGET=linux2628 arch=x86_64 USE_LINUX_TPROXY=1 make -j12 ARCH=x86_64 TARGET=linux-glibc USE_PCRE=1 USE_OPENSSL=1 USE_ZLIB=1 USE_SYSTEMD=1 USE_CPU_AFFINITY=1 USE_LINUX_TPROXY=1 make install
这一步的关键在于make时,指定TARGET=linux2628 arch=x86_64 USE_LINUX_TPROXY=1
选项,来打开透传用户IP的代码。
1.2 配置haproxy.cfg
在haproxy.cfg配置文件的代理配置分节中, 增加配置项:source 0.0.0.0 usesrc clientip
。来指定对所有的用户ip做ip透传。更详细的配置示例如下:
listen ICE01 10.10.46.198:3306 mode tcp maxconn 2000 balance roundrobin source 0.0.0.0 usesrc clientip server ice-10.10.1.109 10.10.1.109:3306 check inter 5000 fall 1 rise 2
1.3 配置返回包路由
通过1.1和1.2两步配置,即可对用户ip进行透传,但这样还不够: 由于后端server拿到的源ip,是客户端ip而非proxyip, 如此后端server在回包时,则无法走正常路径。此时,需要利用Linux的tproxy补丁,结合iptables/netfileter 来对回包进行处理。
1.3.1 后端server的路由配置
route add -net 10.10.0.0/16 gw 10.10.46.198
通过添加这条路由,让后端server,将返回包路由到proxy节点,10.10.46.198
为proxy的Ip。
1.3.2 Proxy路由配置
/sbin/iptables -F /sbin/iptables -t mangle -N DIVERT /sbin/iptables -t mangle -A PREROUTING -p tcp -m socket -j DIVERT /sbin/iptables -t mangle -A DIVERT -j MARK --set-mark 1 /sbin/iptables -t mangle -A DIVERT -j ACCEPT /sbin/ip rule add fwmark 1 lookup 100 /sbin/ip route add local 0.0.0.0/0 dev lo table 100
通过以上配置,将所有发往Proxy的tcp包,重定向到本地环路(lo)上。然后由TProxy内核补丁来对这些网络包进行处理,进而成功将后端server返回包路由回源客户端。
注: 1.3.2 步,在将用户Ip透传环节,亦起到作用。将这一节放到最后,是为了方便大家由易到难,更好地理解和操作。
2. 原理
结合haproxy的代码,和tproxy作者的一个slice,以及网上对tproxy代码的分析:
http://people.netfilter.org/hidden/nfws/nfws-2008-tproxy_slides.pdf http://www.360doc.com/content/17/0204/11/36792569_626412598.shtml
大致搞懂了基于tproxy进行用户Ip透传的原理。总结如下:
这一步其实非常简单。haproxy进程只需要拿到用户ip,然后在创建到后端server的tcp连接时,做两件事情:
1. 创建和后端server通信的socket,并调用setsockopt函数, 将socket设置为IP_TRANSPARENT或IP_FREEBIND
2. 调用bind函数,将用户ip绑定到该socket,绑定后后端server看到的该tcp连接ip,即为用户源ip。
haproxy相关代码:
int tcpv4_bind_socket(int fd, int flags, struct sockaddr_in *local, struct sockaddr_in *remote) { struct sockaddr_in bind_addr; int foreign_ok = 0; int ret; #ifdef CONFIG_HAP_LINUX_TPROXY static int ip_transp_working = 1; if (flags && ip_transp_working) { if (setsockopt(fd, SOL_IP, IP_TRANSPARENT, (char *) &one, sizeof(one)) == 0 || setsockopt(fd, SOL_IP, IP_FREEBIND, (char *) &one, sizeof(one)) == 0) foreign_ok = 1; else ip_transp_working = 0; } #endif if (flags) { memset(&bind_addr, 0, sizeof(bind_addr)); bind_addr.sin_family = AF_INET; if (flags & 1) bind_addr.sin_addr = remote->sin_addr; if (flags & 2) bind_addr.sin_port = remote->sin_port; } setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (char *) &one, sizeof(one)); if (foreign_ok) { ret = bind(fd, (struct sockaddr *)&bind_addr, sizeof(bind_addr)); if (ret < 0) return 2; } else { ret = bind(fd, (struct sockaddr *)local, sizeof(*local)); if (ret < 0)
2.2 haproxy如何正确处理后端server返回包?
后端server的返回包, 要通过haproxy正确转发并返回到客户端,需要解决两个问题:
-
后端server能够将返回包, 发送到Haproxy所在机器(而不是根据根据源ip地址,直接返回到客户端。由于客户端不存在对应的tcp状态机,直接返回亦将出错)。
-
haproxy所在机器,在收到后端server返回包后,能够将该返回包正确路由给haproxy进程进行处理。最终由Haproxy返回到客户端。
对于问题1, 解决方法很简单,只需要在后端server中配置一个路由:
route add -net 10.10.0.0/16 gw 10.10.46.198
配置该路由后, 后端server所在机器,将把所有目的地为10.10.0.0/16的包,路由到10.10.46.198这台机器。
而问题2的解决,则需要linux tproxy出场了。
2.2.1 linux tproxy简介
-
linux tproxy是linux内核支持透明代理的一技术,在linux内核2.6.28版本后, tproxy已经成为linux内核的一部分。
-
tproxy的核心原理, 是承接netfilter(通过iptables配置)路由过来的网络包, 然后对网络包进行处理。处理的结果之一,是将一个目的地为非本地ip(用户ip)的网络包, 递交给本地进程(haproxy)进行处理,最终由haproxy将该返回包返回到客户端。
在下面一节,将结合tproxy的源码,分析tproxy如何将目的地为用户ip的网络包, 递交到haproxy。
2.2.2 tproxy如何处理目的地为非本地ip(用户ip)的网络包?
-
tproxy处理【目的地为非本地ip网络包】的流程示意图:
一些背景知识:
-
linux内核分层处理后端server返回的网络包,网络包经由链路层(网卡驱动)、Ip层、Tcp层,最终递交到应用层的Haproxy;
-
数据包在各层传递过程中, 在linux内核中,统一表示为一个结构:struct sk_buff,或称之为socket buffer,简写为skb;
-
skb在递交给tcp层时, 由skb->sk 标明该网络包,对应的应用层socket套接字是哪个,tcp层将根据skb->sk这个信息,将网络包放入到某个应用进程创建的套接字中,供应用层处理该网络包
-
为此,处理【目的地为非本地ip网络包】的关键, 在于在ip层,将skb中的sk,指定为haproxy进程创建的socket套接字
在网络层,tproxy结合netfilter/iptables, 是这么干的:
1.通过iptables配置ip层的路由规则, 将所有基于tcp的网络包(skb),打上标记(–set-mark 1),并将这些打了标记的包, 重定向到本地环路:
/sbin/iptables -F /sbin/iptables -t mangle -N DIVERT /sbin/iptables -t mangle -A PREROUTING -p tcp -m socket -j DIVERT /sbin/iptables -t mangle -A DIVERT -j MARK --set-mark 1 /sbin/iptables -t mangle -A DIVERT -j ACCEPT /sbin/ip rule add fwmark 1 lookup 100 /sbin/ip route add local 0.0.0.0/0 dev lo table 100
2.tproxy从本地环路上抓取网络包(skb),然后提取出网络包中的源ip/port,目的ip/port,根据这些信息,从内核中查找出对应的套接字句柄sk,然后进行赋值: skb->sk = sk(勘误:网上资料都是skb->sock=sk 但从linux内核的skb的定义来看,应该是sk而不是sock)。 赋值后将该包递交给上一层:tcp层。tcp层将根据skb->sk这个句柄,决定将该网络包递交给haproxy进程创建的socket套接字进行处理。
tproxy该处理逻辑相关的代码如下:
static unsigned int tproxy_tg4(struct sk_buff *skb, __be32 laddr, __be16 lport, u_int32_t mark_mask, u_int32_t mark_value) { struct udphdr _hdr, *hp; struct sock *sk; hp = skb_header_pointer(skb, ip_hdrlen(skb), sizeof(_hdr), &_hdr);//获得传输头 if (hp == NULL) { pr_debug("TPROXY: packet is too short to contain a transport header, dropping\n"); return NF_DROP; } //根据数据包的内容,向tcp已建立的队列查找skb属于的struct sock //如果客户端与代理服务器已经建立连接,该数据包属于的sock将存在 sk = nf_tproxy_get_sock_v4(dev_net(skb->dev), iph->protocol, iph->saddr, iph->daddr, hp->source, hp->dest, skb->dev, NFT_LOOKUP_ESTABLISHED); ... more code see: http://web.mit.edu/kolya/.f/root/net.mit.edu/sipb.mit.edu/contrib/linux/net/netfilter/xt_TPROXY.c ... if (sk && nf_tproxy_assign_sock(skb, sk)) { //运行至此,说明客户端已经与服务器端建立了三次握手,即sk存在; //则通过nf_tproxy_assign_sock函数,将当前数据包的skb与代理服务器的监听socket建立联系,即skb->sk = sk //最后,将数据包打上比较,待策略路由转发到loobackshang /* This should be in a separate target, but we don't do multiple targets on the same rule yet */ skb->mark = (skb->mark & ~mark_mask) ^ mark_value; pr_debug("TPROXY: redirecting: proto %u %08x:%u -> %08x:%u, mark: %x\n", iph->protocol, ntohl(iph->daddr), ntohs(hp->dest), ntohl(laddr), ntohs(lport), skb->mark); return NF_ACCEPT; } } ... more code see: http://web.mit.edu/kolya/.f/root/net.mit.edu/sipb.mit.edu/contrib/linux/net/netfilter/xt_TPROXY.c ...
代码中核心的操作其实就三步:
1.从网络包(skb)中提取出源和目的地址:
const struct iphdr *iph = ip_hdr(skb);
2.调用nf_tproxy_get_sock_v4函数,
sk = nf_tproxy_get_sock_v4(dev_net(skb->dev), iph->protocol, iph->saddr, iph->daddr, hp->source, hp->dest, skb->dev, NFT_LOOKUP_ESTABLISHED);
去内核中,获得该网络包对应的socket套接字。该套接字为什么能够获得到呢? 因为haproxy进程,在发送请求到后端server时,就已经在和后端的套接字中,通过bind,绑定了用户ip和port作为源地址,后端server作为目标地址。 这就保证了,在处理后端server所回的这个skb时,linux能够根据skb的源和目的地址,找到对应的套接字。
-
将sk复制给skb的sk指针:
if (sk && nf_tproxy_assign_sock(skb, sk))
3. 总结
为了透传用户ip到后端server, proxy机器需要解决两个问题:
-
在创建到后端server的套接字时, 将用户ip作为套接字的源ip,从而让后端server看到;
-
后端server在回包时, 能够将目的地为用户ip的回包,返回给proxy机器,而proxy机器能够将该包,从网卡驱动(链路层)收下来,并正确递交给应用层的haproxy进程
为了解决这两个问题,haproxy进程和所在机器需要做三个事情:
-
haproxy进程在创建到后端server的tcp套接字时,开启IP_TRANSPARENT选项, 并绑定用户ip为源ip;
-
后端server修改路由规则,将目的地为用户ip的回包,路由给proxy机器;
-
proxy机器在处理回包时, 在ip层, 由TProxy通过结合netfilter/iptables, 对该回包做一些小动作,将该回包的skb->sk = sk(sk为haproxy进程创建的对应套接字),从而让tcp层能够根据skb->sk, 将该回包递交给haroxy进程进行处理,最终返回给客户端。
————————————————
版权声明:本文为CSDN博主「frockee」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
文章评论(0)