diff -r -u ORIG-ipvs-0.8.2/ipvs/ip_vs_app.c ipvs-0.8.2/ipvs/ip_vs_app.c --- ORIG-ipvs-0.8.2/ipvs/ip_vs_app.c Thu Apr 12 03:59:31 2001 +++ ipvs-0.8.2/ipvs/ip_vs_app.c Fri Nov 16 16:10:16 2001 @@ -517,6 +517,10 @@ n_skb->protocol = skb->protocol; n_skb->ip_summed = skb->ip_summed; n_skb->dst = dst_clone(skb->dst); + +#ifdef CONFIG_NETFILTER_DEBUG + n_skb->nf_debug = skb->nf_debug; +#endif /* * Copy pkt in new buffer diff -r -u ORIG-ipvs-0.8.2/ipvs/ip_vs_conn.c ipvs-0.8.2/ipvs/ip_vs_conn.c --- ORIG-ipvs-0.8.2/ipvs/ip_vs_conn.c Mon Oct 22 14:54:20 2001 +++ ipvs-0.8.2/ipvs/ip_vs_conn.c Mon Jan 21 15:19:17 2002 @@ -44,8 +44,9 @@ #include #include "ip_vs.h" -//#include - +#include +#include +#include /* * Connection hash table: for input and output packets lookups of IPVS @@ -60,6 +61,8 @@ */ static atomic_t ip_vs_conn_no_cport_cnt = ATOMIC_INIT(0); +static void ip_vs_deal_with_conntrack(struct sk_buff *skb, + struct ip_vs_conn *cp); /* * Set ip_vs_conn expiration. @@ -711,9 +714,75 @@ int csum = 0; /* checksum */ int csum_ok = 0; /* csum_ok says if csum is valid */ int mtu; + struct ip_conntrack *ct; EnterFunction(10); + /* By the time we're sending the packet out the other + * side, there should be a confirmed Netfilter CT entry + * for this connection. This may not be the case, + * however, if it's a brand new connection, or if the NF + * entry has timed out before ours has. Either way, if + * the NF CT entry is unconfirmed, confirm it, and deal + * with reply tuple mangling at the same time. + */ + ct = (struct ip_conntrack *)(skb->nfct->master); + +#if defined(BN_ASSERTIONS) + { + int ok_p = 0; + struct ip_conntrack_tuple *orig_tup + = &ct->tuplehash[IP_CT_DIR_ORIGINAL].tuple; + struct ip_conntrack_tuple *repl_tup + = &ct->tuplehash[IP_CT_DIR_REPLY].tuple; + + if (cp->caddr == orig_tup->src.ip + && ((cp->cport == 0) || (cp->cport == orig_tup->src.u.tcp.port)) + && cp->protocol == orig_tup->dst.protonum + && cp->vaddr == orig_tup->dst.ip + && cp->vport == orig_tup->dst.u.tcp.port) + ok_p = 1; + + /* If that doesn't match, then the only other + * option should be an inside-out FTP active + * data connection, so check that possibility + * fairly thoroughly. + */ + if (!ok_p + && (cp->caddr == repl_tup->src.ip) + && (cp->cport == repl_tup->src.u.tcp.port) + && (cp->caddr == orig_tup->dst.ip) + && (cp->cport == orig_tup->dst.u.tcp.port) + && (cp->vaddr == repl_tup->dst.ip) + && (cp->vport == repl_tup->dst.u.tcp.port) + && (cp->daddr == orig_tup->src.ip) + && (cp->dport == orig_tup->src.u.tcp.port)) + ok_p = 1; + + if (!ok_p) { + printk(KERN_ALERT "ip_vs_nat_xmit(): " + "Mismatch of tuple: proto %d; " + "caddr %u.%u.%u.%u; cport %d; " + "vaddr %u.%u.%u.%u; vport %d; " + "SKB proto %d; " + "SKB src %u.%u.%u.%u; SKB sALL %d; " + "SKB dst %u.%u.%u.%u; SKB dALL %d\n", + cp->protocol, + NIPQUAD(cp->caddr), ntohs(cp->cport), + NIPQUAD(cp->vaddr), ntohs(cp->vport), + orig_tup->dst.protonum, + NIPQUAD(orig_tup->src.ip), + ntohs(orig_tup->src.u.all), + NIPQUAD(orig_tup->dst.ip), + ntohs(orig_tup->dst.u.all)); + return NF_DROP; + } + } +#endif + + if (!is_confirmed(ct)) + ip_vs_deal_with_conntrack(skb, cp); + h.raw = (char*) iph + iph->ihl * 4; size = ntohs(iph->tot_len) - (iph->ihl * 4); doff = ip_vs_proto_doff(iph->protocol, h.raw, size); @@ -1074,11 +1143,134 @@ } +static void ip_vs_deal_with_conntrack(struct sk_buff *skb, + struct ip_vs_conn *cp) +{ + struct ip_conntrack *ct; + struct ip_conntrack_tuple new_reply; + int alter_retval; + int confirm_retval; + + /* Something slightly unusual is going on --- the FTP + * IPVS modules triggers this, for instance. + */ + if (!skb) + return; + + /* We only deal with UDP or TCP services. + */ + if (cp->protocol != IPPROTO_UDP && cp->protocol != IPPROTO_TCP) + return; + + /* IPVS uses a "connection" with a source port of zero + * to record persistent connections, and similar + * "template" connections. Such connections do not + * exist within NF's conntrack, so we do nothing for + * those cases. + */ + if (cp->cport == 0) + return; + + ct = (struct ip_conntrack *)(skb->nfct->master); + +#if defined(BN_ASSERTIONS) + { + struct ip_conntrack_tuple *orig_tup + = &ct->tuplehash[IP_CT_DIR_ORIGINAL].tuple; + + if (cp->caddr != orig_tup->src.ip + || cp->cport != orig_tup->src.u.tcp.port + || cp->protocol != orig_tup->dst.protonum + || cp->vaddr != orig_tup->dst.ip + || cp->vport != orig_tup->dst.u.tcp.port) { + printk(KERN_ALERT "ip_vs_deal_with_conntrack(): " + "Mismatch of tuple: proto %d; " + "caddr %u.%u.%u.%u; cport %d; " + "vaddr %u.%u.%u.%u; vport %d; " + "SKB proto %d; " + "SKB src %u.%u.%u.%u; SKB sALL %d; " + "SKB dst %u.%u.%u.%u; SKB dALL %d\n", + cp->protocol, + NIPQUAD(cp->caddr), ntohs(cp->cport), + NIPQUAD(cp->vaddr), ntohs(cp->vport), + orig_tup->dst.protonum, + NIPQUAD(orig_tup->src.ip), + ntohs(orig_tup->src.u.all), + NIPQUAD(orig_tup->dst.ip), + ntohs(orig_tup->dst.u.all)); + return; + } + } +#endif + + new_reply.src.ip = cp->daddr; + new_reply.src.u.tcp.port = cp->dport; + new_reply.dst.ip = cp->caddr; + new_reply.dst.u.tcp.port = cp->cport; + new_reply.dst.protonum = cp->protocol; + +#if defined(BN_DEBUG_IPVS_CONN) + { + struct ip_conntrack_tuple *orig_tup + = &ct->tuplehash[IP_CT_DIR_ORIGINAL].tuple; + + printk(KERN_DEBUG "ip_vs_deal_with_conntrack(): " + "Tuple [%d] %u.%u.%u.%u.%u -> %u.%u.%u.%u.%u\n", + orig_tup->dst.protonum, + NIPQUAD(orig_tup->src.ip), + ntohs(orig_tup->src.u.all), + NIPQUAD(orig_tup->dst.ip), + ntohs(orig_tup->dst.u.all)); + printk(KERN_DEBUG "ip_vs_deal_with_conntrack(): " + "new_reply: %u.%u.%u.%u.%u -> %u.%u.%u.%u.%u\n", + NIPQUAD(new_reply.src.ip), ntohs(new_reply.src.u.all), + NIPQUAD(new_reply.dst.ip), ntohs(new_reply.dst.u.all)); + } +#endif + + alter_retval = ip_conntrack_alter_reply(ct, &new_reply); +#if defined(BN_ASSERTIONS) + if (alter_retval == 0) { + printk(KERN_ALERT "ip_vs_deal_with_conntrack(): " + "ip_conntrack_alter_reply() gave 0 for " + "reply tuple %u.%u.%u.%u.%d -> %u.%u.%u.%u.%d\n", + NIPQUAD(cp->daddr), ntohs(cp->dport), + NIPQUAD(cp->caddr), ntohs(cp->cport)); + return; + } +#endif + + ip_nat_replace_in_hashes(ct, &ct->nat.info); + + if (skb->nfct + && !is_confirmed((struct ip_conntrack *)(skb->nfct->master))) + confirm_retval = __ip_conntrack_confirm(skb->nfct); + else { +#if defined(BN_ASSERTIONS) + printk(KERN_ALERT "ip_vs_deal_with_conntrack(): " + "skipped confirmation; skb->nfct %p\n", + skb->nfct); +#endif + confirm_retval = NF_ACCEPT; + } + +#if defined(BN_ASSERTIONS) + if (confirm_retval != NF_ACCEPT) { + printk(KERN_ALERT "ip_vs_deal_with_conntrack(): " + "ip_conntrack_confirm() gave %d for skb %p\n", + confirm_retval, + skb); + } +#endif +} + + /* * Bind a connection entry with a virtual service destination * Called just after a new connection entry is created. */ -void ip_vs_bind_dest(struct ip_vs_conn *cp, struct ip_vs_dest *dest) +void ip_vs_bind_dest(struct ip_vs_conn *cp, + struct ip_vs_dest *dest) { /* * Increase the refcnt counter of the dest. diff -r -u ORIG-ipvs-0.8.2/ipvs/ip_vs_core.c ipvs-0.8.2/ipvs/ip_vs_core.c --- ORIG-ipvs-0.8.2/ipvs/ip_vs_core.c Mon Oct 22 14:54:20 2001 +++ ipvs-0.8.2/ipvs/ip_vs_core.c Mon Jan 21 15:20:33 2002 @@ -43,8 +43,17 @@ #include #include "ip_vs.h" -//#include - +#include +#include +#include + +/* Define this symbol to get the "old" behaviour whereby a + * packet traversing the FORWARD chain which comes from a Real + * Service's IP:port but isn't part of an active IPVS connection + * is dropped and an ICMP "unreachable" message sent back to the + * Real Server. Leave it undefined to NF_ACCEPT such packets. + * + * #define IPVS_SEND_UNREACH_FOR_NONIPVS_PKTS */ EXPORT_SYMBOL(register_ip_vs_scheduler); EXPORT_SYMBOL(unregister_ip_vs_scheduler); @@ -658,6 +668,43 @@ return NF_ACCEPT; } +/* This code stolen from ip_nat_standalone.c, as is the + * following comment: + * + * FIXME: change in oif may mean change in hh_len. Check and realloc + * --RR + */ +static int +route_me_harder(struct sk_buff *skb) +{ + struct iphdr *iph = skb->nh.iph; + struct rtable *rt; + struct rt_key key = { dst:iph->daddr, + src:iph->saddr, + oif:skb->sk ? skb->sk->bound_dev_if : 0, + tos:RT_TOS(iph->tos)|RTO_CONN, +#ifdef CONFIG_IP_ROUTE_FWMARK + fwmark:skb->nfmark +#endif + }; + + /* Note that ip_route_output_key() makes routing + * decisions assuming that the packet has originated + * from this machine itself. This is the correct + * behaviour for our case. + */ + if (ip_route_output_key(&rt, &key) != 0) { + printk("route_me_harder(): No more route.\n"); + return -EINVAL; + } + + /* Drop old route. */ + dst_release(skb->dst); + + skb->dst = &rt->u.dst; + return 0; +} + /* * It is hooked at the NF_IP_FORWARD chain, used only for VS/NAT. @@ -678,6 +725,7 @@ int doff; /* transport protocol data offset */ int csum; /* checksum */ int csum_ok; /* csum_ok says if csum is valid */ + int retval; EnterFunction(11); @@ -721,6 +769,7 @@ cp = ip_vs_conn_out_get(iph->protocol, iph->saddr, h.portp[0], iph->daddr, h.portp[1]); if (!cp) { +#ifdef IPVS_SEND_UNREACH_FOR_NONIPVS_PKTS if (ip_vs_lookup_real_service(iph->protocol, iph->saddr, h.portp[0])) { /* @@ -734,6 +783,7 @@ return NF_STOLEN; } } +#endif IP_VS_DBG(12, "packet for %s %d.%d.%d.%d:%d " "continue traversal as normal.\n", ip_vs_proto_name(iph->protocol), @@ -853,8 +903,20 @@ skb->nfcache |= NFC_IPVS_PROPERTY; + /* For policy routing, packets originating from this + * machine itself may be routed differently to packets + * passing through. We want this packet to be routed as + * if it came from this machine itself. So re-compute + * the routing information. + */ + if (route_me_harder(skb) == 0) + retval = NF_ACCEPT; + else + /* No route available; what can we do? */ + retval = NF_DROP; + LeaveFunction(11); - return NF_ACCEPT; + return retval; } @@ -1122,9 +1184,49 @@ } if (!cp - && (h.th->syn || (iph->protocol!=IPPROTO_TCP)) + && ((h.th->syn && !h.th->ack) || (iph->protocol != IPPROTO_TCP)) && (svc = ip_vs_service_get(skb->nfmark, iph->protocol, iph->daddr, h.portp[1]))) { + struct ip_conntrack *ct; + + /* The NF connection-tracking entry should be + * unconfirmed, i.e., not in any hash table yet. + * + * However, there does seem to be a race + * somewhere which results in the NF conntrack + * being confirmed when it shouldn't be. We + * will try to hack round this by dropping the + * packet causing the trouble, and destroying + * the NF-ct if we detect this going on. It's + * also rare enough to make it worth logging. + * + * TODO: Investigate this more thoroughly. + */ + ct = (struct ip_conntrack *)(skb->nfct->master); + if (ct->tuplehash[IP_CT_DIR_ORIGINAL].list.next != NULL) { + printk(KERN_DEBUG "ip_vs_in(): " + "NF/LVS hack for " + "\"[%d] %u.%u.%u.%u.%u->%u.%u.%u.%u.%u\"; svc ([%d]%u.%u.%u.%u.%u(%u)).\n", + ct->tuplehash[IP_CT_DIR_ORIGINAL].tuple.dst.protonum, + NIPQUAD(ct->tuplehash[IP_CT_DIR_ORIGINAL].tuple.src.ip), + ntohs(ct->tuplehash[IP_CT_DIR_ORIGINAL].tuple.src.u.all), + NIPQUAD(ct->tuplehash[IP_CT_DIR_ORIGINAL].tuple.dst.ip), + ntohs(ct->tuplehash[IP_CT_DIR_ORIGINAL].tuple.dst.u.all), + svc->protocol, + NIPQUAD(svc->addr), + ntohs(svc->port), + svc->fwmark); + + if (del_timer(&ct->timeout)) { + if (ct->timeout.function) + ct->timeout.function(ct->timeout.data); + } + + ip_vs_service_put(svc); + + return NF_DROP; + } + if (ip_vs_todrop()) { /* * It seems that we are very loaded. diff -r -u ORIG-ipvs-0.8.2/ipvs/ip_vs_ftp.c ipvs-0.8.2/ipvs/ip_vs_ftp.c --- ORIG-ipvs-0.8.2/ipvs/ip_vs_ftp.c Sun Mar 18 15:01:45 2001 +++ ipvs-0.8.2/ipvs/ip_vs_ftp.c Mon Jan 21 15:21:04 2002 @@ -36,12 +36,18 @@ #include #include +#include +#include +#include +#include + #include "ip_vs.h" #define SERVER_STRING "227 Entering Passive Mode (" #define CLIENT_STRING "PORT " +static int ip_vs_ftp_expect_callback(struct ip_conntrack *ct); /* * List of ports (up to IP_VS_APP_MAX_PORTS) to be handled by helper @@ -151,6 +157,9 @@ char buf[24]; /* xxx.xxx.xxx.xxx,ppp,ppp\000 */ unsigned buf_len; int diff; + struct ip_conntrack_tuple tup, mask; + struct ip_conntrack *ct; + int expect_retval; /* Only useful for established sessions */ if (cp->state != IP_VS_S_ESTABLISHED) @@ -196,6 +205,56 @@ atomic_inc(&cp->dest->inactconns); } + /* By this stage either we (by coincidence, as + * far as I can tell) have an old IPVS entry + * which matches the PASV response just seen, or + * we have made a new one. Now set up the + * details of the related connection, and the + * mask. In this case, we don't know which port + * the client will choose to make the connection + * from, so mask that part out. + */ + tup.src.ip = n_cp->caddr; + tup.src.u.tcp.port = 0; + tup.dst.ip = n_cp->vaddr; + tup.dst.u.tcp.port = n_cp->vport; + tup.dst.protonum = IPPROTO_TCP; + mask.src.ip = 0xffffffff; + mask.src.u.all = 0; + mask.dst.ip = 0xffffffff; + mask.dst.u.all = 0xffff; + mask.dst.protonum = 0xffff; + +#if defined(BN_DEBUG_FTP) + printk(KERN_DEBUG "ip_vs_ftp_out(): " + "tup = [%d] %u.%u.%u.%u.%u -> %u.%u.%u.%u.%u\n", + tup.dst.protonum, + NIPQUAD(tup.src.ip), ntohs(tup.src.u.tcp.port), + NIPQUAD(tup.dst.ip), ntohs(tup.dst.u.tcp.port)); +#endif + + /* Find the NF connection-tracking entry for the + * control connection, and tell it to expect the + * data connection we've just specified. + */ + ct = (struct ip_conntrack *)(skb->nfct->master); + if (!ct) { + IP_VS_ERR("ip_vs_ftp_out(): ct NULL.\n"); + expect_retval = 0; + } else + expect_retval + = ip_conntrack_expect_related( + ct, + &tup, + &mask, + &ip_vs_ftp_expect_callback); + + if (expect_retval) { + IP_VS_ERR("ip_vs_ftp_out(): " + "ip_conntrack_expect_related() " + "gave error.\n"); + } + /* * Replace the old passive address with the new one */ @@ -257,6 +316,9 @@ __u32 to; __u16 port; struct ip_vs_conn *n_cp; + struct ip_conntrack_tuple tup, mask; + struct ip_conntrack *ct; + int expect_retval; /* Only useful for established sessions */ if (cp->state != IP_VS_S_ESTABLISHED) @@ -306,7 +368,7 @@ NIPQUAD(to), ntohs(port)); /* - * Now update or create an masquerade entry for it + * Now update or create a connection entry for it */ IP_VS_DBG(1-debug, "protocol %s %u.%u.%u.%u:%d %u.%u.%u.%u:%d\n", ip_vs_proto_name(iph->protocol), @@ -332,6 +394,56 @@ atomic_inc(&cp->dest->inactconns); } + /* By this stage either we (by coincidence, as far as I + * can tell) have an old IPVS entry which matches the + * PORT command just sent, or we have made a new one. + * Now set up the details of the related connection, and + * the mask. In this case, we know exactly what the + * connection should be at both ends, so set the mask to + * check every single bit. + */ + tup.src.ip = n_cp->daddr; + tup.src.u.tcp.port = n_cp->dport; + tup.dst.ip = n_cp->caddr; + tup.dst.u.tcp.port = n_cp->cport; + tup.dst.protonum = IPPROTO_TCP; + mask.src.ip = 0xffffffff; + mask.src.u.all = 0xffff; + mask.dst.ip = 0xffffffff; + mask.dst.u.all = 0xffff; + mask.dst.protonum = 0xffff; + +#if defined(BN_DEBUG_FTP) + printk(KERN_DEBUG "ip_vs_ftp_in(): " + "tup = [%d] %u.%u.%u.%u.%u -> %u.%u.%u.%u.%u\n", + tup.dst.protonum, + NIPQUAD(tup.src.ip), ntohs(tup.src.u.tcp.port), + NIPQUAD(tup.dst.ip), ntohs(tup.dst.u.tcp.port)); +#endif + + /* Find the NF connection-tracking entry for the + * control connection, and tell it to expect the + * data connection we've just specified. + */ + ct = (struct ip_conntrack *)(skb->nfct->master); + if (!ct) { + IP_VS_ERR("ip_vs_ftp_in(): ct NULL.\n"); + expect_retval = 0; + } else + expect_retval + = ip_conntrack_expect_related( + ct, + &tup, + &mask, + &ip_vs_ftp_expect_callback); + + if (expect_retval) { + IP_VS_ERR("ip_vs_ftp_in(): " + "ip_conntrack_expect_related() " + "gave error.\n"); + } + + /* * Move tunnel to listen state */ @@ -342,6 +454,106 @@ return 0; } + +static int ip_vs_ftp_expect_callback(struct ip_conntrack *ct) +{ + struct ip_conntrack_tuple *orig, *repl; + struct ip_vs_conn *in_cp, *out_cp; + + orig = &ct->tuplehash[IP_CT_DIR_ORIGINAL].tuple; + repl = &ct->tuplehash[IP_CT_DIR_REPLY].tuple; + +#if defined(BN_DEBUG_FTP) + printk(KERN_DEBUG "ip_vs_ftp_expect_callback(): " + "ORIG: %u.%u.%u.%u.%u -> %u.%u.%u.%u.%u; " + "REPL: %u.%u.%u.%u.%u -> %u.%u.%u.%u.%u\n", + NIPQUAD(orig->src.ip), ntohs(orig->src.u.all), + NIPQUAD(orig->dst.ip), ntohs(orig->dst.u.all), + NIPQUAD(repl->src.ip), ntohs(repl->src.u.all), + NIPQUAD(repl->dst.ip), ntohs(repl->dst.u.all)); +#endif + + in_cp = ip_vs_conn_in_get(orig->dst.protonum, + orig->src.ip, orig->src.u.tcp.port, + orig->dst.ip, orig->dst.u.tcp.port); + out_cp = ip_vs_conn_out_get(orig->dst.protonum, + orig->src.ip, orig->src.u.tcp.port, + orig->dst.ip, orig->dst.u.tcp.port); + + if ((!in_cp && !out_cp) || (in_cp && out_cp)) { + printk(KERN_ALERT "ip_vs_ftp_expect_callback(): " + "%s in_cp %s out_cp non-NULL.\n", + in_cp ? "both" : "neither", + in_cp ? "and" : "nor"); + return 0; + } + + if (in_cp) { + /* Passive. */ + +#if defined(BN_DEBUG_FTP) + printk(KERN_DEBUG "ip_vs_ftp_expect_callback(): " + "in_cp: C %u.%u.%u.%u.%u V %u.%u.%u.%u.%u D %u.%u.%u.%u.%u\n", + NIPQUAD(in_cp->caddr), ntohs(in_cp->cport), + NIPQUAD(in_cp->vaddr), ntohs(in_cp->vport), + NIPQUAD(in_cp->daddr), ntohs(in_cp->dport)); +#endif + + /* Modify the reply tuple so that it matches the + * soon-to-be-arriving DIR_REPLY packets which + * will be addressed D->C. ("Dest" == "Real".) + */ + repl->src.ip = in_cp->daddr; + repl->src.u.tcp.port = in_cp->dport; + + /* And confirm the connection. Note that + * __ip_conntrack_confirm() needs a (struct + * nf_ct_info *), and we could probably use + * _ESTABLISHED or _NEW, but for tidiness we use + * the truthful one. + */ + __ip_conntrack_confirm(&ct->infos[IP_CT_RELATED]); + + ip_vs_conn_put(in_cp); + } +#if defined(BN_DEBUG_FTP) + else + printk(KERN_DEBUG "ip_vs_ftp_expect_callback(): " + "in_cp NULL\n"); +#endif + + if (out_cp) { + /* Active. */ + +#if defined(BN_DEBUG_FTP) + printk(KERN_DEBUG "ip_vs_ftp_expect_callback(): " + "out_cp: C %u.%u.%u.%u.%u V %u.%u.%u.%u.%u D %u.%u.%u.%u.%u\n", + NIPQUAD(out_cp->caddr), ntohs(out_cp->cport), + NIPQUAD(out_cp->vaddr), ntohs(out_cp->vport), + NIPQUAD(out_cp->daddr), ntohs(out_cp->dport)); +#endif + + /* Modify the reply tuple so that it matches the + * soon-to-be-arriving DIR_REPLY packets which will be + * addressed C->V. + */ + repl->dst.ip = out_cp->vaddr; + repl->dst.u.tcp.port = out_cp->vport; + + /* And confirm the connection. See above. + */ + __ip_conntrack_confirm(&ct->infos[IP_CT_RELATED]); + + ip_vs_conn_put(out_cp); + } +#if defined(BN_DEBUG_FTP) + else + printk(KERN_DEBUG "ip_vs_ftp_expect_callback(): " + "out_cp NULL\n"); +#endif + + return 0; +} static struct ip_vs_app ip_vs_ftp = { {0}, /* n_list */