haproxy透傳用戶ip-方法和原理

前言

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連接時富腊,做兩件事情:

  1. 創(chuàng)建和后端server通信的socket,并調(diào)用setsockopt函數(shù)域帐, 將socket設置為IP_TRANSPARENT或IP_FREEBIND
  2. 調(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ā)并返回到客戶端羊异,需要解決兩個問題:

  1. 后端server能夠?qū)⒎祷匕?發(fā)送到Haproxy所在機器(而不是根據(jù)根據(jù)源ip地址,直接返回到客戶端彤断。由于客戶端不存在對應的tcp狀態(tài)機野舶,直接返回亦將出錯)。

  2. 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簡介

  1. linux tproxy是linux內(nèi)核支持透明代理的一技術(shù),在linux內(nèi)核2.6.28版本后含懊, tproxy已經(jīng)成為linux內(nèi)核的一部分身冬。

  2. 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)絡包步脓?

  1. tproxy處理【目的地為非本地ip網(wǎng)絡包】的流程示意圖:
image.png

一些背景知識:
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的源和目的地址鸠真,找到對應的套接字悯仙。

  1. 將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進程進行處理,最終返回給客戶端霹粥。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末灭将,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子后控,更是在濱河造成了極大的恐慌庙曙,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,858評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件浩淘,死亡現(xiàn)場離奇詭異矾利,居然都是意外死亡,警方通過查閱死者的電腦和手機馋袜,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評論 3 395
  • 文/潘曉璐 我一進店門男旗,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人欣鳖,你說我怎么就攤上這事察皇。” “怎么了泽台?”我有些...
    開封第一講書人閱讀 165,282評論 0 356
  • 文/不壞的土叔 我叫張陵什荣,是天一觀的道長。 經(jīng)常有香客問我怀酷,道長稻爬,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,842評論 1 295
  • 正文 為了忘掉前任蜕依,我火速辦了婚禮桅锄,結(jié)果婚禮上琉雳,老公的妹妹穿的比我還像新娘。我一直安慰自己友瘤,他們只是感情好翠肘,可當我...
    茶點故事閱讀 67,857評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著辫秧,像睡著了一般束倍。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上盟戏,一...
    開封第一講書人閱讀 51,679評論 1 305
  • 那天绪妹,我揣著相機與錄音,去河邊找鬼柿究。 笑死喂急,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的笛求。 我是一名探鬼主播,決...
    沈念sama閱讀 40,406評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼糕簿,長吁一口氣:“原來是場噩夢啊……” “哼探入!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起懂诗,我...
    開封第一講書人閱讀 39,311評論 0 276
  • 序言:老撾萬榮一對情侶失蹤蜂嗽,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后殃恒,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體植旧,經(jīng)...
    沈念sama閱讀 45,767評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年离唐,在試婚紗的時候發(fā)現(xiàn)自己被綠了病附。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,090評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡亥鬓,死狀恐怖完沪,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情嵌戈,我是刑警寧澤覆积,帶...
    沈念sama閱讀 35,785評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站熟呛,受9級特大地震影響宽档,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜庵朝,卻給世界環(huán)境...
    茶點故事閱讀 41,420評論 3 331
  • 文/蒙蒙 一吗冤、第九天 我趴在偏房一處隱蔽的房頂上張望又厉。 院中可真熱鬧,春花似錦欣孤、人聲如沸馋没。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽篷朵。三九已至,卻和暖如春婆排,著一層夾襖步出監(jiān)牢的瞬間声旺,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評論 1 271
  • 我被黑心中介騙來泰國打工段只, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留腮猖,地道東北人。 一個月前我還...
    沈念sama閱讀 48,298評論 3 372
  • 正文 我出身青樓赞枕,卻偏偏與公主長得像澈缺,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子炕婶,可洞房花燭夜當晚...
    茶點故事閱讀 45,033評論 2 355