IPVS源代码分析---app helper的实现

按照我自己的理解,对于NAT这种方式,IPVS实现了一个APP层(其他tunnel和dr方式,不会经过app层的处理)。也就是说提供了一个app层的接口,对于一些特殊的应用,可以进行特殊的处理。app层是一个通用的接口,我们可以基于app的接口,实现自己的应用,在内核中已实现的应用只有FTP。也就是说对于FTP来讲,可以利用ftp app(ftp app也叫ftp helper),实现多连接的处理,不需要persistent+fwmark的支持。当然,也可以使用 persistent+fwmark实现的方式。对于FTP app这种方式,其实比较低效,因为它对数据包的内容(而不是包头)进行了过滤,从而查找特定的应用协议的一些关键数据包,从而对应用协议进行处理。因此,我觉得ftp app这种方式应该是早期的处理FTP的方式。当然 app层主要是提供了一个接口,使我们可以对应用协议进行自己的处理和扩展。
 
对于每一个应用协议,会定义一个静态的struct ip_vs_app结构作为模板,以后登记该协议时,对应的应用指针并不是直接指向这个静态结构,而是新分配一个struct ip_vs_app结构,结构中的struct ip_vs_app指针指向这个静态结构,然后把新分配的这个结构分别挂接到静态struct ip_vs_app结构的具体实现链表和 IP协议的应用HASH链表中进行使用,这种实现方法和netfilter完全不同。

IPVS应用一些共享的处理函数在net/ipv4/ipvs/ip_vs_app.c中定义,其他各协议相关处理分别由各自文件处理,如net/ipv4/ipvs/ip_vs_ftp.c.

/*
 *	The application module object (a.k.a. app incarnation)
 */
struct ip_vs_app
{
	struct list_head	a_list;		/* member in app list */
	int			type;		/* IP_VS_APP_TYPE_xxx */
	char			*name;		/* application module name */
	__u16			protocol;
	struct module		*module;	/* THIS_MODULE/NULL */
	struct list_head	incs_list;	/* list of incarnations */

	/* members for application incarnations */
	struct list_head	p_list;		/* member in proto app list */
	struct ip_vs_app	*app;		/* its real application */
	__be16			port;		/* port number in net order */
	atomic_t		usecnt;		/* usage counter */

	/* output hook: return false if can't linearize. diff set for TCP.  */
	int (*pkt_out)(struct ip_vs_app *, struct ip_vs_conn *,
		       struct sk_buff *, int *diff);
	/* input hook: return false if can't linearize. diff set for TCP. */
	int (*pkt_in)(struct ip_vs_app *, struct ip_vs_conn *,
		      struct sk_buff *, int *diff);
	/* ip_vs_app initializer */
	int (*init_conn)(struct ip_vs_app *, struct ip_vs_conn *);
	/* ip_vs_app finish */
	int (*done_conn)(struct ip_vs_app *, struct ip_vs_conn *);

	int *			timeout_table;
	int *			timeouts;
	int			timeouts_size;

	int (*conn_schedule)(struct sk_buff *skb, struct ip_vs_app *app,
			     int *verdict, struct ip_vs_conn **cpp);
	struct ip_vs_conn *
	(*conn_in_get)(const struct sk_buff *skb, struct ip_vs_app *app,
		       const struct iphdr *iph, unsigned int proto_off,
		       int inverse);
	struct ip_vs_conn *
	(*conn_out_get)(const struct sk_buff *skb, struct ip_vs_app *app,
			const struct iphdr *iph, unsigned int proto_off,
			int inverse);
	int (*state_transition)(struct ip_vs_conn *cp, int direction,
				const struct sk_buff *skb,
				struct ip_vs_app *app);
	void (*timeout_change)(struct ip_vs_app *app, int flags);
};

 
FTP有两种模式 主动和被动,主动和被动是针对服务器说的,指的是对于数据连接,服务器主动连接到客户端,还是服务器被动接收客户端的连接。
主动模式是 client主动通告服务器它所开启的数据端口,服务器用20端口来连接client的数据端口。client通告服务器端口的消息有一个固定的格式,IPVS在ip_vs_ftp_in中,截获  "PORT xxx,xxx,xxx,xxx,ppp,ppp\n" 这个数据包,并建立一个新的connection entry。这样下次 客户端到服务器的这个连接在查找时就可以查找到。
被动模式是 服务器 告诉客户端它监听的数据端口,客户端主动连接到服务器。这样,IPVS在ip_vs_ftp_out中,截获"227 Entering Passive Mode (xxx,xxx,xxx,xxx,ppp,ppp)".xxx,xxx,xxx,xxx is the server address, ppp,ppp is the server port number. 这样也就可以建立一个connection entry。 因为ip_vs_ftp_out是在ip_vs_out里调用的,也就是挂在FORWARD这个hook上。
 

FTP协议应用结构模板
static struct ip_vs_app ip_vs_ftp = {
        .name =         "ftp",
        .type =         IP_VS_APP_TYPE_FTP,
        .protocol =     IPPROTO_TCP,
        .module =       THIS_MODULE,
        .incs_list =    LIST_HEAD_INIT(ip_vs_ftp.incs_list),
        .init_conn =    ip_vs_ftp_init_conn,
        .done_conn =    ip_vs_ftp_done_conn,
        .bind_conn =    NULL,
        .unbind_conn =  NULL,
        .pkt_out =      ip_vs_ftp_out,
        .pkt_in =       ip_vs_ftp_in,
};

进入方向的数据是FTP客户端发出的, 和子连接相关的命令为PORT命令,建立一个主动模式的子连接
static int ip_vs_ftp_in(struct ip_vs_app *app, struct ip_vs_conn *cp, struct sk_buff **pskb, int *diff)
{
       struct iphdr *iph;
       struct tcphdr *th;
       char *data, *data_start, *data_limit;
       char *start, *end;
       __u32 to;
       __u16 port;
       struct ip_vs_conn *n_cp;

      *diff = 0;
      //发子连接信息数据时主连接必然是TCP连接建立好状态,否则就出错
      if (cp->state != IP_VS_TCP_S_ESTABLISHED)
            return 1;
      //让数据包可写
      if (!ip_vs_make_skb_writable(pskb, (*pskb)->len))
            return 0;
      //协议头指针定位
      iph = (*pskb)->nh.iph;
      th = (struct tcphdr *)&(((char *)iph)[iph->ihl*4]);

      /* Since there may be OPTIONS in the TCP packet and the HLEN is                            
         the length of the header in 32-bit multiples, it is accurate                            
         to calculate data address by th+HLEN*4 */
      //数据定位
      data = data_start = (char *)th + (th->doff << 2);
      data_limit = (*pskb)->tail;

      //防止数据越界
      while (data <= data_limit - 6) {
            //PASV命令,表示要进入被动模式
            if (strnicmp(data, "PASV\r\n", 6) == 0) {
                  /* Passive mode on */
                  cp->app_data = &ip_vs_ftp_pasv;
                  return 1;
            }
            data++;
       }
       //#define CLIENT_STRING "PORT "
       // 查找FTP数据是否是PORT命令,提取出地址端口信息及其位置
       if (ip_vs_ftp_get_addrport(data_start, data_limit, CLIENT_STRING, sizeof(CLIENT_STRING)-1, '\r', &to, &port, &start, &end) != 1)
             return 1;
       cp->app_data = NULL;

       //用找到的地址端口和服务器虚地址虚端口找连接
       n_cp = ip_vs_conn_in_get(iph->protocol, to, port, cp->vaddr, htons(ntohs(cp->vport)-1));
       if (!n_cp) {//找不到连接,这是大部分的情况
              //新建连接作为子连接
              n_cp = ip_vs_conn_new(IPPROTO_TCP, to, port, cp->vaddr, htons(ntohs(cp->vport)-1),
                                      cp->daddr, htons(ntohs(cp->dport)-1), 0, cp->dest);
              if (!n_cp)
                    return 0;

              //子连接和主连接相连
              //不需要修改数据内容
              ip_vs_control_add(n_cp, cp);
       }
       //将子连接状态设置为监听状态
       ip_vs_tcp_conn_listen(n_cp);
       ip_vs_conn_put(n_cp);
       return 1;
}

发出方向的数据是FTP服务器发出的, 和子连接相关的回应为227类型,建立一个被动模式的子连接
static int ip_vs_ftp_out(struct ip_vs_app *app, struct ip_vs_conn *cp, struct sk_buff **pskb, int *diff)
{
      struct iphdr *iph;
      struct tcphdr *th;
      char *data, *data_limit;
      char *start, *end;
      __u32 from;
      __u16 port;
      struct ip_vs_conn *n_cp;
      char buf[24];           /* xxx.xxx.xxx.xxx,ppp,ppp\000 */
      unsigned buf_len;
      int ret;

      *diff = 0;
      //发子连接信息数据时主连接必然是TCP连接建立好状态,否则就出错
      if (cp->state != IP_VS_TCP_S_ESTABLISHED)
            return 1;
      //让数据包可写
      if (!ip_vs_make_skb_writable(pskb, (*pskb)->len))
            return 0;
      //子连接必须是被动模式的
      if (cp->app_data == &ip_vs_ftp_pasv) {
            //数据定位
            iph = (*pskb)->nh.iph;
            th = (struct tcphdr *)&(((char *)iph)[iph->ihl*4]);
            data = (char *)th + (th->doff << 2);
            data_limit = (*pskb)->tail;

            //#define SERVER_STRING "227 Entering Passive Mode ("
            //查找"227 "回应中的地址端口信息,from和port返回的是网路字节序
            if (ip_vs_ftp_get_addrport(data, data_limit, SERVER_STRING, sizeof(SERVER_STRING)-1, ')', 
                                                                    &from, &port, &start, &end) != 1)
                  return 1;

            //查找发出方向的连接
            n_cp = ip_vs_conn_out_get(iph->protocol, from, port, cp->caddr, 0);
            if (!n_cp) {//正常情况下是找不到的
                  //新建子连接, 注意各地址端口参数的位置
                  n_cp = ip_vs_conn_new(IPPROTO_TCP, cp->caddr, 0, cp->vaddr, port, from, port, IP_VS_CONN_F_NO_CPORT, cp->dest);
                  if (!n_cp)
                        return 0;
                  //将子连接和主连接联系起来
                  ip_vs_control_add(n_cp, cp);
            }
            //新地址端口用连接的虚拟地址和端口
            //需要修改数据包中的数据
            from = n_cp->vaddr;
            port = n_cp->vport;
            //修改后的地址端口信息
            sprintf(buf,"%d,%d,%d,%d,%d,%d", NIPQUAD(from), port&255, (port>>8)&255);
            buf_len = strlen(buf);

            //检查数据长度差异
            *diff = buf_len - (end-start);

            if (*diff == 0) {//长度相同的话直接覆盖就行了
                  memcpy(start, buf, buf_len);
                  ret = 1;
            } else {//修改数据
                  ret = !ip_vs_skb_replace(*pskb, GFP_ATOMIC, start, end-start, buf, buf_len);
            }
            cp->app_data = NULL;
            //连接状态设为监听
            ip_vs_tcp_conn_listen(n_cp);
            ip_vs_conn_put(n_cp);
            return ret;
       }
       return 1;
}
将skb包中某段数据更改为新的数据,是一个通用函数,可供应用协议修改协议数据的函数调用
int ip_vs_skb_replace(struct sk_buff *skb, gfp_t pri, char *o_buf, int o_len, char *n_buf, int n_len)
{
       struct iphdr *iph;
       int diff;
       int o_offset;
       int o_left;

       //新数据和老数据的长度差,这影响序列号和确认号
       diff = n_len - o_len;
       //老数据在数据包中的偏移
       o_offset = o_buf - (char *)skb->data;
       /* The length of left data after o_buf+o_len in the skb data */
       o_left = skb->len - (o_offset + o_len);//老数据左边的第一个数据

       if (diff <= 0) {//新长度不大于老长度,把原来老数据右边的数据移过来
             memmove(o_buf + n_len, o_buf + o_len, o_left);
             memcpy(o_buf, n_buf, n_len);
             skb_trim(skb, skb->len + diff);//减少数据包的长度
       } else if (diff <= skb_tailroom(skb)) {
             //新长度大于老长度,但skb包后面的空闲区可以容纳下新数据
             //扩展数据包长
             skb_put(skb, diff);
             memmove(o_buf + n_len, o_buf + o_len, o_left);
             memcpy(o_buf, n_buf, n_len);
       } else {
             //新长度大于老长度,但skb包后面的空闲区也容纳不下新数据
             //需要重新扩展skb大小
             if (pskb_expand_head(skb, skb_headroom(skb), diff, pri))
                   return -ENOMEM;

             skb_put(skb, diff);
             memmove(skb->data + o_offset + n_len, skb->data + o_offset + o_len, o_left);
             memcpy(skb->data + o_offset, n_buf, n_len);
       }
       iph = skb->nh.iph;
       iph->tot_len = htons(skb->len);
       return 0;
}
void ip_vs_tcp_conn_listen(struct ip_vs_conn *cp)
{...
}
static int ip_vs_ftp_get_addrport(char *data, char *data_limit, const char *pattern, size_t plen, char term,
                                  __u32 *addr, __u16 *port, char **start, char **end)
{...
}

初始化
#define IP_VS_APP_MAX_PORTS  8
static int ports[IP_VS_APP_MAX_PORTS] = {21, 0};
static int __init ip_vs_ftp_init(void)
{
       int i, ret;
       struct ip_vs_app *app = &ip_vs_ftp;
       //注册FTP应用模板
       ret = register_ip_vs_app(app); //实现list_add(&app->a_list, &ip_vs_app_list); ip_vs_app_list全局连表
       if (ret)
             return ret;
       //可从模块插入时,输入端口参数,指定在哪些端口上进行FTP应用绑定
       for (i=0; i<IP_VS_APP_MAX_PORTS; i++) {
             if (!ports[i])
                   continue;
             if (ports[i] < 0 || ports[i] > 0xffff) {
                   IP_VS_WARNING("ip_vs_ftp: Ignoring invalid configuration port[%d] = %d\n", i, ports[i]);
                   continue;
             }
             //新建应用实例
             ret = register_ip_vs_app_inc(app, app->protocol, ports[i]); //实现result = ip_vs_app_inc_new(app, proto, port);
             if (ret)
                   break;

             IP_VS_INFO("%s: loaded support on port[%d] = %d\n", app->name, i, ports[i]);
       }
       if (ret)
             unregister_ip_vs_app(app);
       return ret;
}
//新建一个应用实例,注意输入参数除了协议端口外,还需要提供一个应用模板的指针
//而且函数并不直接返回应用结构本身,而是在函数中新建的应用实例直接挂接到链表中
//只返回建立成功(0)或失败(<0)
static int ip_vs_app_inc_new(struct ip_vs_app *app, __u16 proto, __u16 port)
{
      struct ip_vs_protocol *pp;
      struct ip_vs_app *inc;
      int ret;

      //查找IPVS协议结构
      if (!(pp = ip_vs_proto_get(proto)))
            return -EPROTONOSUPPORT;
      //协议中不能只有应用登记函数而没有拆除函数
      if (!pp->unregister_app)
            return -EOPNOTSUPP;

      //分配应用结构内存
      inc = kmalloc(sizeof(struct ip_vs_app), GFP_KERNEL);
      if (!inc)
            return -ENOMEM;
      //将应用模板中的内容全部拷贝到新应用结构中
      memcpy(inc, app, sizeof(*inc));
      INIT_LIST_HEAD(&inc->p_list);
      INIT_LIST_HEAD(&inc->incs_list);
      inc->app = app;//应用实例中指向模板本身的指针
      inc->port = htons(port);//应用协议的端口
      atomic_set(&inc->usecnt, 0);//实例的使用计数

      if (app->timeouts) { //建立应用协议状态超时数组
            inc->timeout_table = ip_vs_create_timeout_table(app->timeouts, app->timeouts_size);
            if (!inc->timeout_table) {
                  ret = -ENOMEM;
                  goto out;
            }
      }
      //将应用实例向IP协议结构登记
      ret = pp->register_app(inc);
      if (ret)
            goto out;

      //将应用实例添加到应用模板的实例链表
      list_add(&inc->a_list, &app->incs_list);
      IP_VS_DBG(9, "%s application %s:%u registered\n", pp->name, inc->name, inc->port);
      return 0;
out:
      kfree(inc->timeout_table);
      kfree(inc);
      return ret;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值