dpvs學習筆記: 12 TOA 實現(xiàn)原理

在 full-nat two-arm 模式下少漆,后端 real server 獲取到請求的來源都是 dpvs local ip, 如何獲取真實的 client ip 呢题禀?這就需要 toa 模塊,原理都說是修改了 rs 機器獲取 ip 的函數(shù)峭跳,具體如何初現(xiàn)呢蒂破?

tcp option 字段

關于 tcp header 可以參考 wiki, 我把截圖貼上來

tcp header

我們知道 ip header 里 src address 肯定是 dpvs local ip, 否則數(shù)據(jù)包無法發(fā)送。那么 client ip 放哪里呢?就是在 tcp header 的 option 字段中盖腿。

option 字段最長 40 bytes. 每填充一個選項由三部分構成:op-kind, op-length, op-data. 最常用的 mss 字段就是放在 option 里修壕。只要構建一個不沖突的 op-kind 就可以把 client ip 填充進去愈捅。ipv4 的度度是 4 bytes, ipv6 是 16 bytes. 看來整個 option 字段在不久就會不夠用。

dpvs 寫 tcp option address

DPVS fullnat 在調用 tcp_fnat_in_handler 時會調用 tcp_in_add_toa 寫到 mbuf.

static inline int tcp_in_add_toa(struct dp_vs_conn *conn, struct rte_mbuf *mbuf,
                          struct tcphdr *tcph)
{
    uint32_t mtu;
    struct tcpopt_addr *toa;
    uint32_t tcp_opt_len;

    uint8_t *p, *q, *tail;
    struct route_entry *rt;

    if (unlikely(conn->af != AF_INET && conn->af != AF_INET6))
        return EDPVS_NOTSUPP;

    tcp_opt_len = conn->af == AF_INET ? TCP_OLEN_IP4_ADDR : TCP_OLEN_IP6_ADDR;
    /*
     * check if we can add the new option
     */
    /* skb length and tcp option length checking */
    if ((rt = mbuf->userdata) != NULL) {
        mtu = rt->mtu;
    } else if (conn->in_dev) { /* no route for fast-xmit */
        mtu = conn->in_dev->mtu;
    } else {
        RTE_LOG(DEBUG, IPVS, "add toa: MTU unknown.\n");
        return EDPVS_NOROUTE;
    }

    if (unlikely(mbuf->pkt_len > (mtu - tcp_opt_len))) {
        RTE_LOG(DEBUG, IPVS, "add toa: need fragment, tcp opt len : %u.\n",
                tcp_opt_len);
        return EDPVS_FRAG;
    }

    /* maximum TCP header is 60, and 40 for options */
    if (unlikely((60 - (tcph->doff << 2)) < tcp_opt_len)) {
        RTE_LOG(DEBUG, IPVS, "add toa: no TCP header room, tcp opt len : %u.\n",
                tcp_opt_len);
        return EDPVS_NOROOM;
    }

    /* check tail room and expand mbuf.
     * have to pull all bits in segments for later operation. */
    if (unlikely(mbuf_may_pull(mbuf, mbuf->pkt_len) != 0))
        return EDPVS_INVPKT;
    tail = (uint8_t *)rte_pktmbuf_append(mbuf, tcp_opt_len);
    if (unlikely(!tail)) {
        RTE_LOG(DEBUG, IPVS, "add toa: no mbuf tail room, tcp opt len : %u.\n",
                tcp_opt_len);
        return EDPVS_NOROOM;
    }

    /*
     * now add address option
     */

    /* move data down, including existing tcp options
     * @p is last data byte,
     * @q is new position of last data byte */
    p = tail - 1;
    q = p + tcp_opt_len;
    while (p >= ((uint8_t *)tcph + sizeof(struct tcphdr))) {
        *q = *p;
        p--, q--;
    }

    /* insert toa right after TCP basic header */
    toa = (struct tcpopt_addr *)(tcph + 1);
    toa->opcode = TCP_OPT_ADDR;
    toa->opsize = tcp_opt_len;
    toa->port = conn->cport;

    if (conn->af == AF_INET) {
        struct tcpopt_ip4_addr *toa_ip4 = (struct tcpopt_ip4_addr *)(tcph + 1);
        toa_ip4->addr = conn->caddr.in;
    }
    else {
        struct tcpopt_ip6_addr *toa_ip6 = (struct tcpopt_ip6_addr *)(tcph + 1);
        toa_ip6->addr = conn->caddr.in6;
    }


    /* reset tcp header length */
    tcph->doff += tcp_opt_len >> 2;

    /* reset ip header total length */
    if (conn->af == AF_INET)
        ip4_hdr(mbuf)->total_length =
            htons(ntohs(ip4_hdr(mbuf)->total_length) + tcp_opt_len);
    else
        ip6_hdr(mbuf)->ip6_plen =
            htons(ntohs(ip6_hdr(mbuf)->ip6_plen) + tcp_opt_len);

    /* tcp csum will be recalc later, 
     * so as IP hdr csum since iph.tot_len has been chagned. */
    return EDPVS_OK;
}
  1. 根據(jù) ipv4 ipv6 來確定 toa 需要的長度叠殷,2 bytes op-kind, 2 bytes op-length 再加上地址長度改鲫。所以 ipv4 共需 8 bytes, ipv6 共需 20 bytes
  2. TCP header 最大長度 60,option 最大長度 40林束,確何不會超過
  3. rte_pktmbuf_append 將 mbuf 擴展空間像棘,能容納 toa
  4. 填充 tcpopt_addr 結構體,op-kind TCP_OPT_ADDR 是 254壶冒,非官方 tcp/ip 認可的值缕题。端口值是 conn->cport, 最后填充 conn->caddr.in 或 conn->caddr.in6 地址。

real server 安裝 toa

很簡單胖腾,make 編繹后生成 toa.ko 驅動烟零,然后 insmod toa.ko 即可。所有 real server 都需要安裝咸作。先看下 module_init 函數(shù) toa_init

static int __init
toa_init(void)
{

    TOA_INFO("TOA " TOA_VERSION " by pukong.wjm\n");

    /* alloc statistics array for toa */
    ext_stats = alloc_percpu(struct toa_stat_mib);
    if (NULL == ext_stats)
        return 1;
    proc_net_fops_create(&init_net, "toa_stats", 0, &toa_stats_fops);

    /* get the address of function sock_def_readable
     * so later we can know whether the sock is for rpc, tux or others
     */
    sk_data_ready_addr = kallsyms_lookup_name("sock_def_readable");
    TOA_INFO("CPU [%u] sk_data_ready_addr = "
        "kallsyms_lookup_name(sock_def_readable) = %lu\n",
         smp_processor_id(), sk_data_ready_addr);
    if (0 == sk_data_ready_addr) {
        TOA_INFO("cannot find sock_def_readable.\n");
        goto err;
    }

#ifdef TOA_IPV6_ENABLE
    if (0 != get_kernel_ipv6_symbol()) {
        TOA_INFO("get ipv6 struct from kernel fail.\n");
        goto err;
    }
#endif
    
    /* hook funcs for parse and get toa */
    hook_toa_functions();

    TOA_INFO("toa loaded\n");
    return 0;

err:
    proc_net_remove(&init_net, "toa_stats");
    if (NULL != ext_stats) {
        free_percpu(ext_stats);
        ext_stats = NULL;
    }

    return 1;
}
  1. proc_net_fops_create 在 /proc 文件系統(tǒng)下注冊 /proc/net/toa_stats 用于查看統(tǒng)計使用
  2. kallsyms_lookup_name 根據(jù)名稱來獲取 sock_def_readable 地址
  3. get_kernel_ipv6_symbol 如果支持 ipv6, 獲取相應的回調函數(shù)地址
  4. hook_toa_functions 將 toa 功能 hook 進內核
    proc_net_fops_create
/* replace the functions with our functions */
static inline int
hook_toa_functions(void)
{
    /* hook inet_getname for ipv4 */
    struct proto_ops *inet_stream_ops_p =
            (struct proto_ops *)&inet_stream_ops;
    /* hook tcp_v4_syn_recv_sock for ipv4 */
    struct inet_connection_sock_af_ops *ipv4_specific_p =
            (struct inet_connection_sock_af_ops *)&ipv4_specific;

    inet_stream_ops_p->getname = inet_getname_toa;
    TOA_INFO("CPU [%u] hooked inet_getname <%p> --> <%p>\n",
        smp_processor_id(), inet_getname, inet_stream_ops_p->getname);

    ipv4_specific_p->syn_recv_sock = tcp_v4_syn_recv_sock_toa;
    TOA_INFO("CPU [%u] hooked tcp_v4_syn_recv_sock <%p> --> <%p>\n",
        smp_processor_id(), tcp_v4_syn_recv_sock,
        ipv4_specific_p->syn_recv_sock);

#ifdef TOA_IPV6_ENABLE
    inet6_stream_ops_p->getname = inet6_getname_toa;
    TOA_INFO("CPU [%u] hooked inet6_getname <%p> --> <%p>\n",
        smp_processor_id(), inet6_getname, inet6_stream_ops_p->getname);

    ipv6_specific_p->syn_recv_sock = tcp_v6_syn_recv_sock_toa;
    TOA_INFO("CPU [%u] hooked tcp_v6_syn_recv_sock <%p> --> <%p>\n",
        smp_processor_id(), tcp_v6_syn_recv_sock_org_pt,
        ipv6_specific_p->syn_recv_sock);
#endif

    return 0;
}

仔細看看也不難锨阿,就是將 inet ops 回調函數(shù) getname 替換為 toa 的。但是我有問題记罚,如果請求不來自 dpvs墅诡,普通的請求會不會也受影響?

可以看到 hook 了兩個函數(shù) tcp_v4_syn_recv_sock_toa 和 inet_getname_toa

real server 獲取 client ip

當完成三次握手時調用 tcp_v4_syn_recv_sock_toa

static struct sock *
tcp_v4_syn_recv_sock_toa(struct sock *sk, struct sk_buff *skb,
            struct request_sock *req, struct dst_entry *dst)
{
    struct sock *newsock = NULL;

    TOA_DBG("tcp_v4_syn_recv_sock_toa called\n");

    /* call orginal one */
    newsock = tcp_v4_syn_recv_sock(sk, skb, req, dst);

    /* set our value if need */
    if (NULL != newsock && NULL == newsock->sk_user_data) {
        newsock->sk_user_data = get_toa_data(AF_INET, skb);
        if (NULL != newsock->sk_user_data)
            TOA_INC_STATS(ext_stats, SYN_RECV_SOCK_TOA_CNT);
        else
            TOA_INC_STATS(ext_stats, SYN_RECV_SOCK_NO_TOA_CNT);

        TOA_DBG("tcp_v4_syn_recv_sock_toa: set "
            "sk->sk_user_data to %p\n",
            newsock->sk_user_data);
    }
    return newsock;
}
  1. 調用原有函數(shù) tcp_v4_syn_recv_sock 處理桐智,也就是就里兼容了原有邏輯末早,普通非 toa 請求也會正常獲取到 ip
  2. 額外調用 get_toa_data 生成地址烟馅,可以看到地址放到了 sk->sk_user_data 字段。
static void *get_toa_data(int af, struct sk_buff *skb)
{
    struct tcphdr *th;
    int length;
    unsigned char *ptr;

    TOA_DBG("get_toa_data called\n");

    if (NULL != skb) {
        th = tcp_hdr(skb);
        length = (th->doff * 4) - sizeof(struct tcphdr);
        ptr = (unsigned char *) (th + 1);

        while (length > 0) {
            int opcode = *ptr++;
            int opsize;
            switch (opcode) {
            case TCPOPT_EOL:
                return NULL;
            case TCPOPT_NOP:    /* Ref: RFC 793 section 3.1 */
                length--;
                continue;
            default:
                opsize = *ptr++;
                if (opsize < 2) /* "silly options" */
                    return NULL;
                if (opsize > length)
                    /* don't parse partial options */
                    return NULL;
                if (TCPOPT_TOA == opcode &&
                    TCPOLEN_IP4_TOA == opsize) {

                    struct toa_ip4_data tdata;
                    void *ret_ptr = NULL;

                    memcpy(&tdata, ptr - 2, sizeof(tdata));
                    TOA_DBG("af = %d, find toa data: ip = "
                        TOA_NIPQUAD_FMT", port = %u\n",
                        af,
                        TOA_NIPQUAD(tdata.ip),
                        ntohs(tdata.port));
                    if (af == AF_INET) {
                        memcpy(&ret_ptr, &tdata,
                            sizeof(ret_ptr));
                        TOA_DBG("coded ip4 toa data: %p\n",
                            ret_ptr);
                        return ret_ptr;
                    }
#ifdef TOA_IPV6_ENABLE
                    else if (af == AF_INET6) {
                        struct toa_ip6_data *ptr_toa_ip6 =
                            kmalloc(sizeof(struct toa_ip6_data), GFP_ATOMIC);
                        if (!ptr_toa_ip6) {
                            return NULL;
                        }
                        ptr_toa_ip6->opcode = opcode;
                        ptr_toa_ip6->opsize = TCPOLEN_IP6_TOA;
                        ipv6_addr_set(&ptr_toa_ip6->in6_addr, 0, 0,
                            htonl(0x0000FFFF), tdata.ip);
                        TOA_DBG("coded ip6 toa data: %p\n",
                            ptr_toa_ip6);
                        TOA_INC_STATS(ext_stats, IP6_ADDR_ALLOC_CNT);
                        return ptr_toa_ip6;
                    }
#endif
                }

#ifdef TOA_IPV6_ENABLE
                if (TCPOPT_TOA == opcode &&
                    TCPOLEN_IP6_TOA == opsize &&
                    af == AF_INET6) {
                    struct toa_ip6_data *ptr_toa_ip6 =
                        kmalloc(sizeof(struct toa_ip6_data), GFP_ATOMIC);
                    if (!ptr_toa_ip6) {
                            return NULL;
                    }
                    memcpy(ptr_toa_ip6, ptr - 2, sizeof(struct toa_ip6_data));

                    TOA_DBG("find toa_v6 data : ip = "
                        TOA_NIP6_FMT", port = %u,"
                        " coded ip6 toa data: %p\n",
                        TOA_NIP6(ptr_toa_ip6->in6_addr),
                        ptr_toa_ip6->port,
                        ptr_toa_ip6);
                    TOA_INC_STATS(ext_stats, IP6_ADDR_ALLOC_CNT);
                    return ptr_toa_ip6;
                }
#endif
                ptr += opsize - 2;
                length -= opsize;
            }
        }
    }
    return NULL;
}
  1. 遍歷所有 option, 根據(jù) opcode 來處理 ipv4 或是 ipv6
  2. 將 toa struct 復制一份然磷,然后返回

然后當 real server 調用 getpeername 或是 getsocketname 時調用 inet_getname_toa 來獲取 ip郑趁,如果是 ipv6 則調用 inet6_getname_toa

inet_getname_toa(struct socket *sock, struct sockaddr *uaddr,
        int *uaddr_len, int peer)
{
    int retval = 0;
    struct sock *sk = sock->sk;
    struct sockaddr_in *sin = (struct sockaddr_in *) uaddr;
    struct toa_ip4_data tdata;

    TOA_DBG("inet_getname_toa called, sk->sk_user_data is %p\n",
        sk->sk_user_data);

    /* call orginal one */
    retval = inet_getname(sock, uaddr, uaddr_len, peer);

    /* set our value if need */
    if (retval == 0 && NULL != sk->sk_user_data && peer) {
        if (sk_data_ready_addr == (unsigned long) sk->sk_data_ready) {
            memcpy(&tdata, &sk->sk_user_data, sizeof(tdata));
            if (TCPOPT_TOA == tdata.opcode &&
                TCPOLEN_IP4_TOA == tdata.opsize) {
                TOA_INC_STATS(ext_stats, GETNAME_TOA_OK_CNT);
                TOA_DBG("inet_getname_toa: set new sockaddr, ip "
                    TOA_NIPQUAD_FMT" -> "TOA_NIPQUAD_FMT
                    ", port %u -> %u\n",
                    TOA_NIPQUAD(sin->sin_addr.s_addr),
                    TOA_NIPQUAD(tdata.ip), ntohs(sin->sin_port),
                    ntohs(tdata.port));
                sin->sin_port = tdata.port;
                sin->sin_addr.s_addr = tdata.ip;
            } else { /* sk_user_data doesn't belong to us */
                TOA_INC_STATS(ext_stats,
                        GETNAME_TOA_MISMATCH_CNT);
                TOA_DBG("inet_getname_toa: invalid toa data, "
                    "ip "TOA_NIPQUAD_FMT" port %u opcode %u "
                    "opsize %u\n",
                    TOA_NIPQUAD(tdata.ip), ntohs(tdata.port),
                    tdata.opcode, tdata.opsize);
            }
        } else {
            TOA_INC_STATS(ext_stats, GETNAME_TOA_BYPASS_CNT);
        }
    } else { /* no need to get client ip */
        TOA_INC_STATS(ext_stats, GETNAME_TOA_EMPTY_CNT);
    }

    return retval;
}
  1. 調用原有 inet_getname 函數(shù),獲取 ip姿搜,兼容原有內核邏輯
  2. 判斷 sk_user_data 不為空寡润,并且結構體 op-kind op-length 與 ipv4 toa 的相等,獲取 ip port ,并填充 sin

小結

實現(xiàn)原理還真簡單痪欲,只不過有兩個隱患悦穿。

  1. 如果 option 以后擴充其它內容,長度不夠咋辦业踢?資源本身就不多
  2. op-kind 254 現(xiàn)在不被 tcp/ip 官方認可栗柒,以后會不會被占用?
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末知举,一起剝皮案震驚了整個濱河市瞬沦,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌雇锡,老刑警劉巖逛钻,帶你破解...
    沈念sama閱讀 219,039評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異锰提,居然都是意外死亡曙痘,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評論 3 395
  • 文/潘曉璐 我一進店門立肘,熙熙樓的掌柜王于貴愁眉苦臉地迎上來边坤,“玉大人,你說我怎么就攤上這事谅年〖胙鳎” “怎么了?”我有些...
    開封第一講書人閱讀 165,417評論 0 356
  • 文/不壞的土叔 我叫張陵融蹂,是天一觀的道長旺订。 經常有香客問我,道長超燃,這世上最難降的妖魔是什么区拳? 我笑而不...
    開封第一講書人閱讀 58,868評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮意乓,結果婚禮上劳闹,老公的妹妹穿的比我還像新娘。我一直安慰自己洽瞬,他們只是感情好本涕,可當我...
    茶點故事閱讀 67,892評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著伙窃,像睡著了一般菩颖。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上为障,一...
    開封第一講書人閱讀 51,692評論 1 305
  • 那天晦闰,我揣著相機與錄音,去河邊找鬼鳍怨。 笑死呻右,一個胖子當著我的面吹牛,可吹牛的內容都是我干的鞋喇。 我是一名探鬼主播声滥,決...
    沈念sama閱讀 40,416評論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼侦香!你這毒婦竟也來了落塑?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,326評論 0 276
  • 序言:老撾萬榮一對情侶失蹤罐韩,失蹤者是張志新(化名)和其女友劉穎憾赁,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體散吵,經...
    沈念sama閱讀 45,782評論 1 316
  • 正文 獨居荒郊野嶺守林人離奇死亡龙考,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,957評論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了矾睦。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片晦款。...
    茶點故事閱讀 40,102評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖顷锰,靈堂內的尸體忽然破棺而出柬赐,到底是詐尸還是另有隱情,我是刑警寧澤官紫,帶...
    沈念sama閱讀 35,790評論 5 346
  • 正文 年R本政府宣布肛宋,位于F島的核電站,受9級特大地震影響束世,放射性物質發(fā)生泄漏酝陈。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,442評論 3 331
  • 文/蒙蒙 一毁涉、第九天 我趴在偏房一處隱蔽的房頂上張望沉帮。 院中可真熱鬧,春花似錦、人聲如沸穆壕。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,996評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽喇勋。三九已至缨该,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間川背,已是汗流浹背贰拿。 一陣腳步聲響...
    開封第一講書人閱讀 33,113評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留熄云,地道東北人膨更。 一個月前我還...
    沈念sama閱讀 48,332評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像缴允,于是被迫代替她去往敵國和親荚守。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,044評論 2 355

推薦閱讀更多精彩內容