按照我自己的理解,对于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;
}