Just For Coding

Keep learning, keep living …

LVS FULLNAT模式下客户端真实地址的传递

在LVS的FULLNAT转发模式下, LVS对数据包同时做SNAT和DNAT,将数据包的源IP、源端口更换为LVS本地的IP和端口,将数据包的目的IP和目的端口修改为RS的IP和端口,从而不再依赖特定网络拓朴转发数据包。

这种方式存在一个问题: RealServer中接收到数据包中源IP和源端口为LVS机器的IP和端口,这样应用层程序获取到的TCP连接的客户端地址为LVS的IP地址,很多依赖客户端地址的功能就不能正常工作了。

为了解决这问题,FULLNAT模式在转发包的时候,在TCP包中添加一个OPTION,来传递客户端的真实地址。RealServer中通过内核模块toa令应用层程序获取真实的客户端地址。

TOA OPTION的OPCODE为254(0xfe), 长度为8字节,结构为:

1
2
3
4
5
6
7
struct toa_data
{
    __u8   opcode;
    __u8   opsize;
    __u16  port;
    __u32  ip;
}

比如,TOA的OPTION为:

1
fe 08 91 cd 0a 05 0c 46

0xfe为opcode, 08为option长度,8字节,Port和IP都为网络字节序,端口号为0x91cd(37325), IP为: 0x0a050c46, “10.5.12.70”。

来看toa模块具体实现:

模块的初始化函数为toa_init:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* module init */
static int __init
toa_init(void)
{
    ...

    /* hook funcs for parse and get toa */
    hook_toa_functions();

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

err:
    ...

    return 1;
}

函数调用hook_toa_functions函数HOOK两个函数:

  • inet_getname
  • tcp_v4_syn_recv_sock
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* 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;
    ...
    ipv4_specific_p->syn_recv_sock = tcp_v4_syn_recv_sock_toa;

    ...

    return 0;
}

Linux内核在监听套接字收到三次握手的ACK包之后,会从SYN_REVC状态进入到TCP_ESTABLISHED状态。这时内核会调用tcp_v4_syn_recv_sock函数。Hook函数tcp_v4_syn_recv_sock_toa首先调用原有的tcp_v4_syn_recv_sock函数,然后调用get_toa_data函数从TCP OPTION中提取出TOA OPTION,并存储在sk_user_data字段中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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;

    /* 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(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);
        }
    }
    return newsock;
}

get_toa_data函数的返回值处理比较特殊,并没有给返回结果分配内存空间,而是直接将TOA OPTION做为指针值返回并保存在sk_user_data这一指针变量中。这在64位服务器上没有问题,因为指针变量的大小为8字节,返回的TOA结构大小也为8字节。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
static void * get_toa_data(struct sk_buff *skb)
{
    struct tcphdr *th;
    int length;
    unsigned char *ptr;

    struct toa_data tdata;

    void *ret_ptr = NULL;

    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)
                    return NULL;    /* don't parse partial options */
                if (TCPOPT_TOA == opcode && TCPOLEN_TOA == opsize) {
                    memcpy(&tdata, ptr - 2, sizeof (tdata));
                    memcpy(&ret_ptr, &tdata, sizeof (ret_ptr));
                    return ret_ptr;
                }
                ptr += opsize - 2;
                length -= opsize;
            }
        }
    }
    return NULL;
}

用户在使用套接字中的accept函数时, 会调用inet_getname将sock结构体中存储的源IP地址和端口返回。Hook函数inet_getname_toa首先调用原有函数inet_getname, 然后用tcp_v4_syn_recv_sock_toa函数保存在sk_user_data中数据提取真实IP和Port,对返回结果进行替换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
static int
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_data tdata;

    ...

    /* 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_TOA == tdata.opsize) {
                ...
                sin->sin_port = tdata.port;
                sin->sin_addr.s_addr = tdata.ip;
            } else { /* sk_user_data doesn't belong to us */
                ...
            }
        } 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;
}

后续应用层程序调用getpeername()时就可以获取到真实的客户端地址了。