ipvsadm配置命令解析
以下为ipvsadm命令,第一行配置Linux虚拟服务:地址为207.175.44.110,端口号为80(-A选项),-t表明此服务为TCP,-s选项指定调度算法为Round-Robin。随后的几行为添加实际的服务器,如192.168.10.1:80(-r选项),-a选项表明为添加实际服务器,-t选项表明是TCP协议,-m选项表明转发方式采用NAT masquerading。
ipvsadm -A -t 207.175.44.110:80 -s rr
ipvsadm -a -t 207.175.44.110:80 -r 192.168.10.1:80 -m
ipvsadm -a -t 207.175.44.110:80 -r 192.168.10.2:80 -m
ipvsadm -a -t 207.175.44.110:80 -r 192.168.10.3:80 -m
ipvsadm向内核下发配置的方式由两种:netlink和raw套接口。由ipvsadm-1.29代码中的文件libipvs/Makefile可知,除非显示的将HAVE_NL定义为0,否则将使用netlink接口方式下发配置。即使不定义HAVE_NL,其为空,也是使用netlink。
ifneq (0,$(HAVE_NL))
CFLAGS += -DLIBIPVS_USE_NL
CFLAGS += $(shell \if which pkg-config > /dev/null 2>&1; then \if pkg-config --cflags libnl-3.0 2> /dev/null; then :; \elif pkg-config --cflags libnl-2.0 2> /dev/null; then :; \elif pkg-config --cflags libnl-1 2> /dev/null; then :; \fi; \fi)
endif
参见初始化函数ipvs_init,如果通用netlink接口初始化失败,还是会初始化raw套接口,实现ipvs配置下发。
int ipvs_init(void)
{
#ifdef LIBIPVS_USE_NLtry_nl = 1;if (ipvs_nl_send_message(NULL, NULL, NULL) == 0) {try_nl = 1;return ipvs_getinfo();}try_nl = 0;
#endiflen = sizeof(ipvs_info);if ((sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_RAW)) == -1)return -1;if (getsockopt(sockfd, IPPROTO_IP, IP_VS_SO_GET_INFO, (char *)&ipvs_info, &len))return -1;
}
添加虚拟服务
ipvsadm的参数解析由函数parse_options完成。本文开头的第一行配置命令的相关解析代码如下。解析过程中将初始化ipvs_command_entry类型的结构变量,主要为配置命令,如CMD_ADD对应-A选项;-t选项对应服务的协议变量ce->svc.protocol;-s选项指定的调度算法名称保存在变量ce->svc.sched_name中。
static int parse_options(int argc, char **argv, struct ipvs_command_entry *ce, unsigned int *options, unsigned int *format)
{switch (c) {case 'A':set_command(&ce->cmd, CMD_ADD);break;}while ((c=poptGetNextOpt(context)) >= 0){switch (c) {case 't':case 'u':case TAG_SCTP_SERVICE:set_option(options, OPT_SERVICE);ce->svc.protocol = option_to_protocol(c);parse = parse_service(optarg, &ce->svc);break;case 's':set_option(options, OPT_SCHEDULER);strncpy(ce->svc.sched_name, optarg, IP_VS_SCHEDNAME_MAXLEN);break;
}
函数process_options根据配置命令调用相应的处理函数,如CMD_ADD命令,由函数ipvs_add_service处理。
static int process_options(int argc, char **argv, int reading_stdin)
{struct ipvs_command_entry ce;switch (ce.cmd) {case CMD_ADD:result = ipvs_add_service(&ce.svc);break;
}
如果general netlink成功初始化,即try_nl为真,使用netlink下发配置;否则,使用raw套接口的setsockopt系统调用下发。对于netlink使用命令字IPVS_CMD_NEW_SERVICE;对于raw套接口,使用IP_VS_SO_SET_ADD套接口选项。
int ipvs_add_service(ipvs_service_t *svc)
{
#ifdef LIBIPVS_USE_NLif (try_nl) {struct nl_msg *msg = ipvs_nl_message(IPVS_CMD_NEW_SERVICE, 0);if (!msg) return -1;if (ipvs_nl_fill_service_attr(msg, svc)) {nlmsg_free(msg);return -1;}return ipvs_nl_send_message(msg, ipvs_nl_noop_cb, NULL);}
#endifCHECK_COMPAT_SVC(svc, -1);return setsockopt(sockfd, IPPROTO_IP, IP_VS_SO_SET_ADD, (char *)svc, sizeof(struct ip_vs_service_kern));
}
函数ipvs_nl_fill_service_attr负责填充netlink消息体的属性。以上命令使用到的属性分别为IPVS_SVC_ATTR_PROTOCOL(TCP)、IPVS_SVC_ATTR_ADDR(207.175.44.110),和IPVS_SVC_ATTR_PORT(80)。属性IPVS_SVC_ATTR_SCHED_NAME指定调度算法名称,此处为rr。
static int ipvs_nl_fill_service_attr(struct nl_msg *msg, ipvs_service_t *svc)
{struct nlattr *nl_service;struct ip_vs_flags flags = { .flags = svc->flags, .mask = ~0 };nl_service = nla_nest_start(msg, IPVS_CMD_ATTR_SERVICE);NLA_PUT_U16(msg, IPVS_SVC_ATTR_AF, svc->af);if (svc->fwmark) {NLA_PUT_U32(msg, IPVS_SVC_ATTR_FWMARK, svc->fwmark);} else {NLA_PUT_U16(msg, IPVS_SVC_ATTR_PROTOCOL, svc->protocol);NLA_PUT(msg, IPVS_SVC_ATTR_ADDR, sizeof(svc->addr), &(svc->addr));NLA_PUT_U16(msg, IPVS_SVC_ATTR_PORT, svc->port);}NLA_PUT_STRING(msg, IPVS_SVC_ATTR_SCHED_NAME, svc->sched_name);if (svc->pe_name[0])NLA_PUT_STRING(msg, IPVS_SVC_ATTR_PE_NAME, svc->pe_name);NLA_PUT(msg, IPVS_SVC_ATTR_FLAGS, sizeof(flags), &flags);NLA_PUT_U32(msg, IPVS_SVC_ATTR_TIMEOUT, svc->timeout);NLA_PUT_U32(msg, IPVS_SVC_ATTR_NETMASK, svc->netmask);nla_nest_end(msg, nl_service);
}
ipvsadm还支持使用iptables的mark值添加服务,如下。此种情况下的配置下发,使用netlink属性IPVS_SVC_ATTR_FWMARK。
iptables -A PREROUTING -t mangle -d 207.175.44.110/31 -j MARK --set-mark 1
ipvsadm -A -f 1 -s rr
添加真实服务器
如本文开头部分的命令:ipvsadm -a -t 207.175.44.110:80 -r 192.168.10.1:80 -m。添加真实服务器,我们使用到了参数-a,-t,-r和-m。其中-t选项与上节介绍的相同,用于指定使用TCP协议。参见如下函数parse_options,小写a选项对应的命令为CMD_ADDDEST;-r指定真实服务器的地址和端口等信息;-m选项设置转发方式标志IP_VS_CONN_F_MASQ。
static int parse_options(int argc, char **argv, struct ipvs_command_entry *ce, unsigned int *options, unsigned int *format)
{
switch © {
case ‘a’:
set_command(&ce->cmd, CMD_ADDDEST);
break;
while ((c=poptGetNextOpt(context)) >= 0){switch (c) {case 't':case 'u':case TAG_SCTP_SERVICE:set_option(options, OPT_SERVICE);ce->svc.protocol = option_to_protocol(c);parse = parse_service(optarg, &ce->svc);break;case 'r':set_option(options, OPT_SERVER);ipvs_service_t t_dest = ce->svc;parse = parse_service(optarg, &t_dest);ce->dest.af = t_dest.af;ce->dest.addr = t_dest.addr;ce->dest.port = t_dest.port;if (parse == 1)ce->dest.port = ce->svc.port;break;case 'm':set_option(options, OPT_FORWARD);ce->dest.conn_flags = IP_VS_CONN_F_MASQ;break;
}
在添加或者编辑真实服务器时,如果使用的是隧道转发方式(IP_VS_CONN_F_TUNNEL)或者直接路由方式(IP_VS_CONN_F_DROUTE),要求设置的真实服务器的端口号必须等于虚拟服务的端口号。另外,由于使用防火墙mark标志定义服务时,并不指定虚拟端口号,一定满足此要求。最后,只有在使用隧道转发方式时,才允许配置的真实服务器的地址族与虚拟服务的地址族不相同。
以上检查都通过的情况下,调用函数ipvs_add_dest执行添加操作。
static int process_options(int argc, char **argv, int reading_stdin)
{struct ipvs_command_entry ce;if (ce.cmd == CMD_ADDDEST || ce.cmd == CMD_EDITDEST) {if (!ce.svc.fwmark &&(ce.dest.conn_flags == IP_VS_CONN_F_TUNNEL || ce.dest.conn_flags == IP_VS_CONN_F_DROUTE))ce.dest.port = ce.svc.port;/* Tunneling allows different address family */if (ce.dest.af != ce.svc.af && ce.dest.conn_flags != IP_VS_CONN_F_TUNNEL)fail(2, "Different address family is allowed only for tunneling servers");}switch (ce.cmd) {case CMD_ADDDEST:result = ipvs_add_dest(&ce.svc, &ce.dest);break;
}
类似与以上的添加服务函数ipvs_add_service,ipvs_add_dest也是优先使用netlink下发接口。对于netlink使用命令字IPVS_CMD_NEW_DEST标识此命令;对于raw套接口使用套接口选项IP_VS_SO_SET_ADDDEST。
int ipvs_add_dest(ipvs_service_t *svc, ipvs_dest_t *dest)
{
#ifdef LIBIPVS_USE_NLipvs_func = ipvs_add_dest;if (try_nl) {struct nl_msg *msg = ipvs_nl_message(IPVS_CMD_NEW_DEST, 0);if (!msg) return -1;if (ipvs_nl_fill_service_attr(msg, svc))goto nla_put_failure;if (ipvs_nl_fill_dest_attr(msg, dest))goto nla_put_failure;return ipvs_nl_send_message(msg, ipvs_nl_noop_cb, NULL);nla_put_failure:nlmsg_free(msg);return -1;}
#endifCHECK_COMPAT_SVC(svc, -1);CHECK_COMPAT_DEST(dest, -1);memcpy(&svcdest.svc, svc, sizeof(svcdest.svc));memcpy(&svcdest.dest, dest, sizeof(svcdest.dest));return setsockopt(sockfd, IPPROTO_IP, IP_VS_SO_SET_ADDDEST, (char *)&svcdest, sizeof(svcdest));
}
ipvs_add_dest函数的特定下发内容由函数ipvs_nl_fill_dest_attr完成,如下,真实服务器的信息保存在以下属性中:IPVS_DEST_ATTR_ADDR(192.168.10.1),IPVS_DEST_ATTR_PORT(80)和IPVS_DEST_ATTR_FWD_METHOD(-m)中。本例配置并没有用到属性IPVS_DEST_ATTR_WEIGHT,IPVS_DEST_ATTR_U_THRESH和IPVS_DEST_ATTR_L_THRESH,这三个是对应于其它调度算法的属性,如算法weighted round robin, least-connection, 和weighted least-connection等。
static int ipvs_nl_fill_dest_attr(struct nl_msg *msg, ipvs_dest_t *dst)
{struct nlattr *nl_dest;nl_dest = nla_nest_start(msg, IPVS_CMD_ATTR_DEST);NLA_PUT_U16(msg, IPVS_DEST_ATTR_ADDR_FAMILY, dst->af);NLA_PUT(msg, IPVS_DEST_ATTR_ADDR, sizeof(dst->addr), &(dst->addr));NLA_PUT_U16(msg, IPVS_DEST_ATTR_PORT, dst->port);NLA_PUT_U32(msg, IPVS_DEST_ATTR_FWD_METHOD, dst->conn_flags & IP_VS_CONN_F_FWD_MASK);NLA_PUT_U32(msg, IPVS_DEST_ATTR_WEIGHT, dst->weight);NLA_PUT_U32(msg, IPVS_DEST_ATTR_U_THRESH, dst->u_threshold);NLA_PUT_U32(msg, IPVS_DEST_ATTR_L_THRESH, dst->l_threshold);nla_nest_end(msg, nl_dest);
}