关于linux:toa-内核模块分析

15次阅读

共计 4615 个字符,预计需要花费 12 分钟才能阅读完成。

TOA 的由来

咱们晓得 LVS 之前有三种负载平衡模式:DR、NAT 和 Tunnel,但都有各自的缺点,比方 DR 和 NAT 要求 virtual server 与 real server 在同一子网下,而 Tunnel 运维起来比较复杂。因而,为了灵便部署,开发了第四种模式,即 FULLNAT。

FULLNAT 模式是 NAT 模式的一种扩大,不仅会替换目标 IP,也会替换源 IP。带来的益处是,使得 virtual server 和 real server 解脱后端网络的解放,不再要求它们位于同一子网下。

然而,这种模式也带来了一个问题,real server 无奈获取实在的客户端 IP 地址,而在很多业务场景下,咱们在对外提供服务时,须要查看服务申请方的 IP 地址,来针对 IP 地址做一些业务解决,最常见的一个例子就是:做白名单校验,只有在白名单列表中的 IP 地址,咱们才容许它拜访咱们的服务;还有一种利用场景,那就是基于客户端的申请 IP 来进行调度,譬如 CDN 服务,那么就须要依据客户端的申请 IP,来调度最近最适宜的资源提供服务。

为了解决上述问题,TOA 应运而生,它理论是一个 TCP option filed,应用了 8 字节(kind = 0xfe,Length = 0x08,Value = 4B client’s IP + 2B port),源码如下,

/* MUST be 4 bytes alignment */
struct toa_data {
    __u8 opcode;
    __u8 opsize;
    __u16 port;
    __u32 ip;
};

服务端机器打上 patch 后,在 lvs FULLNAT 模式下可能通过零碎调用 getsockopt 拿到实在的 client IP 地址。

TOA 的应用

为了反对 TOA,FULLNAT 间接批改了内核代码,如果要从新编译内核,那应用起来就很麻烦了,咱们能够以 .ko 文件的模式加载到内核,通过以下命令查看以后机器是否加载了 toa 模块,

lsmod | grep toa

toa 模块的编译能够参考文档 TOA 插件配置。

TOA 的实现原理

TOA 次要通过 hook 零碎函数,进而从 tcp option 解析出 toa data。

留神:以下阐明中用到的 linux 源码版本为 3.2.101。

toa_init 函数是 toa 模块的初始化函数,

/* module init */
static int __init
toa_init(void)
{
    ...
    /* hook funcs for parse and get toa */
    hook_toa_functions();
    ...
}

以上省略了一些解决细节,重点代码是 hook 的处理函数 hook_toa_functions,以 ipv4 协定为例进行阐明。

/* 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 源码中 ipv4 协定各处理函数有如下定义,

/* net/ipv4/tcp_ipv4.c */
const struct inet_connection_sock_af_ops ipv4_specific = {
    ..
    .send_check       = tcp_v4_send_check,
    .conn_request       = tcp_v4_conn_request,
    .syn_recv_sock       = tcp_v4_syn_recv_sock,
    .get_peer       = tcp_v4_get_peer,
};
EXPORT_SYMBOL(ipv4_specific);

stream 类型 socket 各处理函数有如下定义,

/* net/ipv4/af_inet.c */
const struct proto_ops inet_stream_ops = {
    .family           = PF_INET,
    .bind           = inet_bind,
    .connect       = inet_stream_connect,
    .accept           = inet_accept,
    .getname       = inet_getname,
    .listen           = inet_listen,
    .shutdown       = inet_shutdown,
    ...
};
EXPORT_SYMBOL(inet_stream_ops);

联合 linux 源码和 toa 代码,发现了两个要害 hook:

  • syn_recv_sock 函数指针 tcp_v4_syn_recv_sock -> tcp_v4_syn_recv_sock_toa
  • getname 函数指针 inet_getname -> inet_getname_toa

syn_recv_sock 调用

syn_recv_sock 函数在 server 收到 第三次握手 的 ack 包后触发调用逻辑,调用门路为 tcp_v4_do_rcv -> tcp_v4_hnd_req -> tcp_check_req -> syn_recv_sock

/* net/ipv4/tcp_minisocks.c */
struct sock *tcp_check_req(struct sock *sk, struct sk_buff *skb,
               struct request_sock *req,
               struct request_sock **prev)
{
    ...
    child = inet_csk(sk)->icsk_af_ops->syn_recv_sock(sk, skb, req, NULL);
    if (child == NULL)
        goto listen_overflow;
    ...
}

另外,在浏览这部分 linux 源码时发现,server socket 在收到第三次握手时状态仍为 TCP_LISTEN。

int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
{
    ...
    if (sk->sk_state == TCP_LISTEN) {struct sock *nsk = tcp_v4_hnd_req(sk, skb);
        if (!nsk)
            goto discard;

         /* 在第三次握手时产生了一个新的 socket,进入该逻辑 */
        if (nsk != sk) {sock_rps_save_rxhash(nsk, skb);
            if (tcp_child_process(sk, nsk, skb)) {
                rsk = nsk;
                goto reset;
            }
            return 0;
        }
    }
    ...
}

第三次握手会产生一个新 socket,初始状态为 TCP_SYN_RECV,随后转换成 TCP_ESTABLISHED。


上面来看一下代替函数 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;

    /* 先走原有的逻辑 */
    newsock = tcp_v4_syn_recv_sock(sk, skb, req, dst);

    /* 解析 toa data 放到 newsock->sk_user_data */
    if (NULL != newsock && NULL == newsock->sk_user_data) {newsock->sk_user_data = get_toa_data(skb);
        ..
    }
    return newsock;
}

解析 toa data 的函数为 get_toa_data,代码要害是找到 tcp option 的相应字段并解析到一个 toa_data 类型的变量 sk_user_data 里,这里不开展剖析。

inet_getname 调用

当咱们须要从 socket 里拿 client ip 时,就会调用到 inet_getname 函数。

一种应用形式是通过 accept 零碎调用。

#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *restrict addr,
           socklen_t *restrict addrlen);

如果传入 sockaddr 类型变量,就会触发 inet_getname 函数调用逻辑,

/* net/socket.c */
SYSCALL_DEFINE4(accept4, int, fd, struct sockaddr __user *, upeer_sockaddr,
        int __user *, upeer_addrlen, int, flags)
{
    ...
    if (upeer_sockaddr) {if (newsock->ops->getname(newsock, (struct sockaddr *)&address,
                      &len, 2) < 0) {
            err = -ECONNABORTED;
            goto out_fd;
        }
        ...
    }
    ...
}

另外,也能够通过 getpeernamegetsockopt 等零碎调用触发。

那么,上面来看一下代替函数 inet_getname_toa 的实现逻辑。

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;

    /* 调用原来的逻辑 */
    retval = inet_getname(sock, uaddr, uaddr_len, peer);
    
    /* sk_user_data 有数据会进行数据拷贝 */
    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;
            }
            ...
        }
        ...
    } 
    return retval;
}

sk_user_data 变量里有数据时,且未 toa 数据时,会替换相应的 ip 和 port,这样就能拿到失常的 client ip 和 port 了。

通过以上剖析能够看到,toa 模块的工作模式是,在第三次握手时,将 toa data 解析到 sk_user_data 变量里,而后,每次在须要的时候进行相应的替换。

正文完
 0