前言
haproxy透傳用戶ip到服務器端, 已經(jīng)有非常成熟的技術(shù)魂拦,網(wǎng)上有非常多的資料毛仪,很多是可以work的。但是如果你踩得坑足夠多芯勘,你就會知道箱靴,將網(wǎng)上的方案應用于生成環(huán)境,一定要慎之又慎荷愕;而作為一個嚴肅的IAAS基礎設施研發(fā)人員(UCloud云數(shù)據(jù)庫團隊)衡怀,搞清楚這些配置和方法背后的道理,是很有必要的路翻。為了解決haproxy透傳用戶Ip的問題狈癞,我花了幾天時間,了解了其中最主流的一種技術(shù)(linux tproxy)的原理和實現(xiàn)茂契,并總結(jié)出了一個可能是最精簡的配置方法蝶桶。下面是詳細的內(nèi)容。
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/1.4/src/haproxy-1.4.25.tar.gz
tar zxvf haproxy-1.4.25.tar.gz
cd haproxy-1.4.25
yum install gcc gcc-c++ autoconf automake -y
make TARGET=linux2628 arch=x86_64 USE_LINUX_TPROXY=1
make install
這一步的關(guān)鍵在于make時掉冶,指定TARGET=linux2628 arch=x86_64 USE_LINUX_TPROXY=1
選項真竖,來打開透傳用戶IP的代碼。
1.2 配置haproxy.cfg
在haproxy.cfg配置文件的代理配置分節(jié)中厌小, 增加配置項: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補丁濒生,結(jié)合iptables/netfileter 來對回包進行處理。
1.3.1 后端server的路由配置
route add -net 10.10.0.0/16 gw 10.10.46.198
通過添加這條路由幔欧,讓后端server罪治,將返回包路由到proxy節(jié)點,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
通過以上配置觉义,將所有發(fā)往Proxy的tcp包,重定向到本地環(huán)路(lo)上浴井。然后由TProxy內(nèi)核補丁來對這些網(wǎng)絡包進行處理晒骇,進而成功將后端server返回包路由回源客戶端。
注: 1.3.2 步,在將用戶Ip透傳環(huán)節(jié)厉碟,亦起到作用。將這一節(jié)放到最后屠缭,是為了方便大家由易到難箍鼓,更好地理解和操作。
2. 原理
結(jié)合haproxy的代碼呵曹,和tproxy作者的一個slice款咖,以及網(wǎng)上對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透傳的原理⊙傥梗總結(jié)如下:
2.1 haproxy如何透傳用戶Ip铐殃?
這一步其實非常簡單。haproxy進程只需要拿到用戶ip跨新,然后在創(chuàng)建到后端server的tcp連接時富腊,做兩件事情:
- 創(chuàng)建和后端server通信的socket,并調(diào)用setsockopt函數(shù)域帐, 將socket設置為IP_TRANSPARENT或IP_FREEBIND
- 調(diào)用bind函數(shù)赘被,將用戶ip綁定到該socket,綁定后后端server看到的該tcp連接ip肖揣,即為用戶源ip民假。
haproxy相關(guān)代碼:
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正確轉(zhuǎn)發(fā)并返回到客戶端羊异,需要解決兩個問題:
后端server能夠?qū)⒎祷匕?發(fā)送到Haproxy所在機器(而不是根據(jù)根據(jù)源ip地址,直接返回到客戶端彤断。由于客戶端不存在對應的tcp狀態(tài)機野舶,直接返回亦將出錯)。
haproxy所在機器瓦糟,在收到后端server返回包后筒愚,能夠?qū)⒃摲祷匕_路由給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內(nèi)核支持透明代理的一技術(shù),在linux內(nèi)核2.6.28版本后含懊, tproxy已經(jīng)成為linux內(nèi)核的一部分身冬。
tproxy的核心原理, 是承接netfilter(通過iptables配置)路由過來的網(wǎng)絡包岔乔, 然后對網(wǎng)絡包進行處理酥筝。處理的結(jié)果之一,是將一個目的地為非本地ip(用戶ip)的網(wǎng)絡包雏门, 遞交給本地進程(haproxy)進行處理嘿歌,最終由haproxy將該返回包返回到客戶端。
在下面一節(jié)茁影,將結(jié)合tproxy的源碼宙帝,分析tproxy如何將目的地為用戶ip的網(wǎng)絡包, 遞交到haproxy募闲。
2.2.2 tproxy如何處理目的地為非本地ip(用戶ip)的網(wǎng)絡包步脓?
- tproxy處理【目的地為非本地ip網(wǎng)絡包】的流程示意圖:
一些背景知識:
a. linux內(nèi)核分層處理后端server返回的網(wǎng)絡包,網(wǎng)絡包經(jīng)由鏈路層(網(wǎng)卡驅(qū)動)蝇更、Ip層沪编、Tcp層,最終遞交到應用層的Haproxy年扩;
b.數(shù)據(jù)包在各層傳遞過程中蚁廓, 在linux內(nèi)核中,統(tǒng)一表示為一個結(jié)構(gòu):struct sk_buff厨幻,或稱之為socket buffer相嵌,簡寫為skb;
c.skb在遞交給tcp層時况脆, 由skb->sk 標明該網(wǎng)絡包饭宾,對應的應用層socket套接字是哪個,tcp層將根據(jù)skb->sk這個信息格了,將網(wǎng)絡包放入到某個應用進程創(chuàng)建的套接字中看铆,供應用層處理該網(wǎng)絡包
d.為此,處理【目的地為非本地ip網(wǎng)絡包】的關(guān)鍵盛末, 在于在ip層姑子,將skb中的sk账胧,指定為haproxy進程創(chuàng)建的socket套接字
在網(wǎng)絡層审残,tproxy結(jié)合netfilter/iptables母廷, 是這么干的:
1.通過iptables配置ip層的路由規(guī)則, 將所有基于tcp的網(wǎng)絡包(skb)檐嚣,打上標記(--set-mark 1)助泽,并將這些打了標記的包, 重定向到本地環(huán)路:
/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從本地環(huán)路上抓取網(wǎng)絡包(skb),然后提取出網(wǎng)絡包中的源ip/port嗡贺,目的ip/port隐解,根據(jù)這些信息,從內(nèi)核中查找出對應的套接字句柄sk诫睬,然后進行賦值: skb->sk = sk(勘誤:網(wǎng)上資料都是skb->sock=sk 但從linux內(nèi)核的skb的定義來看厢漩,應該是sk而不是sock)。 賦值后將該包遞交給上一層:tcp層岩臣。tcp層將根據(jù)skb->sk這個句柄,決定將該網(wǎng)絡包遞交給haproxy進程創(chuàng)建的socket套接字進行處理宵膨。
tproxy該處理邏輯相關(guān)的代碼如下:
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;
}
//根據(jù)數(shù)據(jù)包的內(nèi)容架谎,向tcp已建立的隊列查找skb屬于的struct sock
//如果客戶端與代理服務器已經(jīng)建立連接,該數(shù)據(jù)包屬于的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)) {
//運行至此辟躏,說明客戶端已經(jīng)與服務器端建立了三次握手谷扣,即sk存在;
//則通過nf_tproxy_assign_sock函數(shù),將當前數(shù)據(jù)包的skb與代理服務器的監(jiān)聽socket建立聯(lián)系捎琐,即skb->sk = sk
//最后会涎,將數(shù)據(jù)包打上比較,待策略路由轉(zhuǎn)發(fā)到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.從網(wǎng)絡包(skb)中提取出源和目的地址:
const struct iphdr *iph = ip_hdr(skb);
2.調(diào)用nf_tproxy_get_sock_v4函數(shù)瑞凑,
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);
去內(nèi)核中末秃,獲得該網(wǎng)絡包對應的socket套接字。該套接字為什么能夠獲得到呢籽御? 因為haproxy進程练慕,在發(fā)送請求到后端server時,就已經(jīng)在和后端的套接字中技掏,通過bind铃将,綁定了用戶ip和port作為源地址,后端server作為目標地址哑梳。 這就保證了劲阎,在處理后端server所回的這個skb時,linux能夠根據(jù)skb的源和目的地址鸠真,找到對應的套接字悯仙。
- 將sk復制給skb的sk指針:
if (sk && nf_tproxy_assign_sock(skb, sk))
3. 總結(jié)
為了透傳用戶ip到后端server, proxy機器需要解決兩個問題:
1.在創(chuàng)建到后端server的套接字時弧哎, 將用戶ip作為套接字的源ip雁比,從而讓后端server看到;
2.后端server在回包時撤嫩, 能夠?qū)⒛康牡貫橛脩鬷p的回包偎捎,返回給proxy機器,而proxy機器能夠?qū)⒃摪瑥木W(wǎng)卡驅(qū)動(鏈路層)收下來茴她,并正確遞交給應用層的haproxy進程
為了解決這兩個問題寻拂,haproxy進程和所在機器需要做三個事情:
1.haproxy進程在創(chuàng)建到后端server的tcp套接字時,開啟IP_TRANSPARENT選項丈牢, 并綁定用戶ip為源ip祭钉;
2.后端server修改路由規(guī)則,將目的地為用戶ip的回包己沛,路由給proxy機器慌核;
3.proxy機器在處理回包時, 在ip層申尼, 由TProxy通過結(jié)合netfilter/iptables垮卓, 對該回包做一些小動作,將該回包的skb->sk = sk(sk為haproxy進程創(chuàng)建的對應套接字)师幕,從而讓tcp層能夠根據(jù)skb->sk粟按, 將該回包遞交給haroxy進程進行處理,最終返回給客戶端霹粥。