Previously, mirrors only worked when using the "normal" action. This commit performs mirroring even when mirroring is not used. It also adds some unit tests. --- NEWS | 1 + ofproto/ofproto-dpif.c | 122 +++++++++++++++---------- ofproto/ofproto-provider.h | 8 +- ofproto/ofproto.c | 5 +- tests/ofproto-dpif.at | 215 ++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 296 insertions(+), 55 deletions(-)
diff --git a/NEWS b/NEWS index aec514d..c718c56 100644 --- a/NEWS +++ b/NEWS @@ -7,6 +7,7 @@ post-v1.3.0 - Added ability to modify ECN bits in IPv4. - Added ability to modify TTL in IPv4. - ovs-vswitchd: + - Don't require the "normal" action to use mirrors. - Track packet and byte statistics sent on mirrors. - ovs-appctl: - New "fdb/flush" command to flush bridge's MAC learning table. diff --git a/ofproto/ofproto-dpif.c b/ofproto/ofproto-dpif.c index ab65fd5..7108213 100644 --- a/ofproto/ofproto-dpif.c +++ b/ofproto/ofproto-dpif.c @@ -136,8 +136,6 @@ static void update_mirror_stats(struct ofproto_dpif *ofproto, mirror_mask_t mirrors, uint64_t packets, uint64_t bytes); -/* A group of one or more OpenFlow ports. */ -#define OFBUNDLE_FLOOD ((struct ofbundle *) 1) struct ofbundle { struct ofproto_dpif *ofproto; /* Owning ofproto. */ struct hmap_node hmap_node; /* In struct ofproto's "bundles" hmap. */ @@ -169,6 +167,8 @@ static void bundle_destroy(struct ofbundle *); static void bundle_del_port(struct ofport_dpif *); static void bundle_run(struct ofbundle *); static void bundle_wait(struct ofbundle *); +static struct ofport_dpif *lookup_input_bundle(struct ofproto_dpif *, + uint16_t in_port, bool warn); static void stp_run(struct ofproto_dpif *ofproto); static void stp_wait(struct ofproto_dpif *ofproto); @@ -451,6 +451,8 @@ static int send_packet(struct ofproto_dpif *, uint32_t odp_port, static size_t compose_sflow_action(const struct ofproto_dpif *, struct ofpbuf *odp_actions, const struct flow *, uint32_t odp_port); +static void add_mirror_actions(struct action_xlate_ctx *ctx, + const struct flow *flow); /* Global variables. */ static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5); @@ -3925,7 +3927,6 @@ compose_output_action__(struct action_xlate_ctx *ctx, uint16_t ofp_port, ctx->sflow_n_outputs++; ctx->nf_output_iface = ofp_port; ctx->flow.nw_tos = flow_nw_tos; - ctx->mirrors = ofport->bundle->mirror_out; } static void @@ -4447,6 +4448,8 @@ static struct ofpbuf * xlate_actions(struct action_xlate_ctx *ctx, const union ofp_action *in, size_t n_in) { + struct flow orig_flow = ctx->flow; + COVERAGE_INC(ofproto_dpif_xlate); ctx->odp_actions = ofpbuf_new(512); @@ -4501,6 +4504,7 @@ xlate_actions(struct action_xlate_ctx *ctx, compose_output_action(ctx, OFPP_LOCAL); } } + add_mirror_actions(ctx, &orig_flow); fix_sflow_action(ctx); } @@ -4677,34 +4681,6 @@ ofbundle_get_a_port(const struct ofbundle *bundle) struct ofport_dpif, bundle_node); } -static mirror_mask_t -compose_dsts(struct action_xlate_ctx *ctx, uint16_t vlan, - const struct ofbundle *in_bundle, - const struct ofbundle *out_bundle) -{ - mirror_mask_t dst_mirrors = 0; - - if (out_bundle == OFBUNDLE_FLOOD) { - struct ofbundle *bundle; - - HMAP_FOR_EACH (bundle, hmap_node, &ctx->ofproto->bundles) { - if (bundle != in_bundle - && ofbundle_includes_vlan(bundle, vlan) - && bundle->floodable - && !bundle->mirror_out) { - output_normal(ctx, bundle, vlan); - dst_mirrors |= bundle->dst_mirrors; - } - } - ctx->nf_output_iface = NF_OUT_FLOOD; - } else if (out_bundle) { - output_normal(ctx, out_bundle, vlan); - dst_mirrors = out_bundle->dst_mirrors; - } - - return dst_mirrors; -} - static bool vlan_is_mirrored(const struct ofmirror *m, int vlan) { @@ -4753,18 +4729,69 @@ eth_dst_may_rspan(const uint8_t dst[ETH_ADDR_LEN]) } static void -output_mirrors(struct action_xlate_ctx *ctx, - uint16_t vlan, const struct ofbundle *in_bundle, - mirror_mask_t dst_mirrors) +add_mirror_actions(struct action_xlate_ctx *ctx, const struct flow *orig_flow) { struct ofproto_dpif *ofproto = ctx->ofproto; mirror_mask_t mirrors; + struct ofport_dpif *in_port; + struct ofbundle *in_bundle; + uint16_t vlan; + uint16_t vid; + const struct nlattr *a; + size_t left; + + /* Obtain in_port from orig_flow.in_port. + * + * lookup_input_bundle() also ensures that in_port belongs to a bundle. */ + in_port = lookup_input_bundle(ctx->ofproto, orig_flow->in_port, + ctx->packet != NULL); + if (!in_port) { + return; + } + in_bundle = in_port->bundle; + mirrors = in_bundle->src_mirrors; + + /* Drop frames on bundles reserved for mirroring. */ + if (in_bundle->mirror_out) { + if (ctx->packet != NULL) { + static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5); + VLOG_WARN_RL(&rl, "bridge %s: dropping packet received on port " + "%s, which is reserved exclusively for mirroring", + ctx->ofproto->up.name, in_bundle->name); + } + return; + } + + /* Check VLAN. */ + vid = vlan_tci_to_vid(orig_flow->vlan_tci); + if (!input_vid_is_valid(vid, in_bundle, ctx->packet != NULL)) { + return; + } + vlan = input_vid_to_vlan(in_bundle, vid); + + /* Look at the output ports to check for destination selections. */ + + NL_ATTR_FOR_EACH (a, left, ctx->odp_actions->data, + ctx->odp_actions->size) { + enum ovs_action_attr type = nl_attr_type(a); + struct ofport_dpif *ofport; + + if (type != OVS_ACTION_ATTR_OUTPUT) { + continue; + } + + ofport = get_odp_port(ofproto, nl_attr_get_u32(a)); + mirrors |= ofport->bundle->dst_mirrors; + } + ctx->mirrors = mirrors; - mirrors = in_bundle->src_mirrors | dst_mirrors; if (!mirrors) { return; } + /* Restore the original packet before adding the mirror actions. */ + ctx->flow = *orig_flow; + while (mirrors) { struct ofmirror *m; @@ -4778,7 +4805,7 @@ output_mirrors(struct action_xlate_ctx *ctx, mirrors &= ~m->dup_mirrors; if (m->out) { output_normal(ctx, m->out, vlan); - } else if (eth_dst_may_rspan(ctx->flow.dl_dst) + } else if (eth_dst_may_rspan(orig_flow->dl_dst) && vlan != m->out_vlan) { struct ofbundle *bundle; @@ -4954,10 +4981,8 @@ is_admissible(struct ofproto_dpif *ofproto, const struct flow *flow, static void xlate_normal(struct action_xlate_ctx *ctx) { - mirror_mask_t dst_mirrors = 0; struct ofport_dpif *in_port; struct ofbundle *in_bundle; - struct ofbundle *out_bundle; struct mac_entry *mac; uint16_t vlan; uint16_t vid; @@ -5006,7 +5031,6 @@ xlate_normal(struct action_xlate_ctx *ctx) /* Check other admissibility requirements. */ if (!is_admissible(ctx->ofproto, &ctx->flow, in_port, vlan, &ctx->tags)) { - output_mirrors(ctx, vlan, in_bundle, 0); return; } @@ -5018,8 +5042,8 @@ xlate_normal(struct action_xlate_ctx *ctx) /* Determine output bundle. */ mac = mac_learning_lookup(ctx->ofproto->ml, ctx->flow.dl_dst, vlan, &ctx->tags); - if (mac) { - out_bundle = mac->port.p; + if (mac && mac->port.p != in_bundle) { + output_normal(ctx, mac->port.p, vlan); } else if (!ctx->packet && !eth_addr_is_multicast(ctx->flow.dl_dst)) { /* If we are revalidating but don't have a learning entry then eject * the flow. Installing a flow that floods packets opens up a window @@ -5029,14 +5053,18 @@ xlate_normal(struct action_xlate_ctx *ctx) ctx->may_set_up_flow = false; return; } else { - out_bundle = OFBUNDLE_FLOOD; - } + struct ofbundle *bundle; - /* Don't send packets out their input bundles. */ - if (in_bundle != out_bundle) { - dst_mirrors = compose_dsts(ctx, vlan, in_bundle, out_bundle); + HMAP_FOR_EACH (bundle, hmap_node, &ctx->ofproto->bundles) { + if (bundle != in_bundle + && ofbundle_includes_vlan(bundle, vlan) + && bundle->floodable + && !bundle->mirror_out) { + output_normal(ctx, bundle, vlan); + } + } + ctx->nf_output_iface = NF_OUT_FLOOD; } - output_mirrors(ctx, vlan, in_bundle, dst_mirrors); } /* Optimized flow revalidation. diff --git a/ofproto/ofproto-provider.h b/ofproto/ofproto-provider.h index 34d18f2..dbb0101 100644 --- a/ofproto/ofproto-provider.h +++ b/ofproto/ofproto-provider.h @@ -1011,10 +1011,10 @@ struct ofproto_class { * 'ofproto' associated with client data pointer 'aux'. If no such mirror * has been registered, this has no effect. * - * This function affects only the behavior of the OFPP_NORMAL action. An - * implementation that does not support it at all may set it to NULL or - * return EOPNOTSUPP. An implementation that supports only a subset of the - * functionality should implement what it can and return 0. */ + * An implementation that does not support it at all may set it to + * NULL or return EOPNOTSUPP. An implementation that supports only + * a subset of the functionality should implement what it can and + * return 0. */ int (*mirror_set)(struct ofproto *ofproto, void *aux, const struct ofproto_mirror_settings *s); diff --git a/ofproto/ofproto.c b/ofproto/ofproto.c index 1f3a077..b9e3880 100644 --- a/ofproto/ofproto.c +++ b/ofproto/ofproto.c @@ -721,10 +721,7 @@ ofproto_bundle_unregister(struct ofproto *ofproto, void *aux) /* Registers a mirror associated with client data pointer 'aux' in 'ofproto'. * If 'aux' is already registered then this function updates its configuration - * to 's'. Otherwise, this function registers a new mirror. - * - * Mirrors affect only the treatment of packets output to the OFPP_NORMAL - * port. */ + * to 's'. Otherwise, this function registers a new mirror. */ int ofproto_mirror_register(struct ofproto *ofproto, void *aux, const struct ofproto_mirror_settings *s) diff --git a/tests/ofproto-dpif.at b/tests/ofproto-dpif.at index 9080e7e..b56d157 100644 --- a/tests/ofproto-dpif.at +++ b/tests/ofproto-dpif.at @@ -376,3 +376,218 @@ AT_CHECK([tail -1 stdout], [0], ]) OVS_VSWITCHD_STOP AT_CLEANUP + +AT_SETUP([ofproto-dpif - mirroring, select_all]) +dnl This test assumes that OpenFlow port numbers are allocated in order +dnl starting from one. Unlike the previous tests, it does require that +dnl they are allocated in the same order that they are named in the +dnl database. +OVS_VSWITCHD_START +AT_CHECK([ovs-vsctl \ + add-port br0 p1 -- set Interface p1 type=dummy --\ + add-port br0 p2 -- set Interface p2 type=dummy --\ + add-port br0 p3 -- set Interface p3 type=dummy --\ + set Bridge br0 mirrors=@m --\ + --id=@p3 get Port p3 --\ + --id=@m create Mirror name=mymirror \ + select_all=true output_port=@p3 ], [0], [stdout]) +AT_CHECK([perl $srcdir/uuidfilt.pl stdout], [0], [dnl +<0> +]) + +AT_DATA([flows.txt], [dnl +in_port=1 actions=output:2 +in_port=2 actions=output:1 +]) +AT_CHECK([ovs-ofctl add-flows br0 flows.txt]) +AT_CHECK([ovs-appctl ofproto/trace br0 'in_port(1),eth(src=50:54:00:00:00:05,dst=50:54:00:00:00:07),eth_type(0x0800),ipv4(src=192.168.0.1,dst=192.168.0.2,proto=1,tos=0,ttl=128,frag=no),icmp(type=8,code=0)'], [0], [stdout]) +AT_CHECK([tail -1 stdout], [0], + [Datapath actions: 2,3 +]) +AT_CHECK([ovs-appctl ofproto/trace br0 'in_port(2),eth(src=50:54:00:00:00:05,dst=50:54:00:00:00:07),eth_type(0x0800),ipv4(src=192.168.0.1,dst=192.168.0.2,proto=1,tos=0,ttl=128,frag=no),icmp(type=8,code=0)'], [0], [stdout]) +AT_CHECK([tail -1 stdout], [0], + [Datapath actions: 1,3 +]) +OVS_VSWITCHD_STOP +AT_CLEANUP + +AT_SETUP([ofproto-dpif - mirroring, select_src]) +dnl This test assumes that OpenFlow port numbers are allocated in order +dnl starting from one. Unlike the previous tests, it does require that +dnl they are allocated in the same order that they are named in the +dnl database. +OVS_VSWITCHD_START +AT_CHECK([ovs-vsctl \ + add-port br0 p1 -- set Interface p1 type=dummy --\ + add-port br0 p2 -- set Interface p2 type=dummy --\ + add-port br0 p3 -- set Interface p3 type=dummy --\ + set Bridge br0 mirrors=@m --\ + --id=@p1 get Port p1 -- --id=@p3 get Port p3 --\ + --id=@m create Mirror name=mymirror \ + select_src_port=@p1 output_port=@p3 ], [0], [stdout]) +AT_CHECK([perl $srcdir/uuidfilt.pl stdout], [0], [dnl +<0> +]) + +AT_DATA([flows.txt], [dnl +in_port=1 actions=output:2 +in_port=2 actions=output:1 +]) +AT_CHECK([ovs-ofctl add-flows br0 flows.txt]) +AT_CHECK([ovs-appctl ofproto/trace br0 'in_port(1),eth(src=50:54:00:00:00:05,dst=50:54:00:00:00:07),eth_type(0x0800),ipv4(src=192.168.0.1,dst=192.168.0.2,proto=1,tos=0,ttl=128,frag=no),icmp(type=8,code=0)'], [0], [stdout]) +AT_CHECK([tail -1 stdout], [0], + [Datapath actions: 2,3 +]) +AT_CHECK([ovs-appctl ofproto/trace br0 'in_port(2),eth(src=50:54:00:00:00:05,dst=50:54:00:00:00:07),eth_type(0x0800),ipv4(src=192.168.0.1,dst=192.168.0.2,proto=1,tos=0,ttl=128,frag=no),icmp(type=8,code=0)'], [0], [stdout]) +AT_CHECK([tail -1 stdout], [0], + [Datapath actions: 1 +]) +OVS_VSWITCHD_STOP +AT_CLEANUP + +AT_SETUP([ofproto-dpif - mirroring, select_dst]) +dnl This test assumes that OpenFlow port numbers are allocated in order +dnl starting from one. Unlike the previous tests, it does require that +dnl they are allocated in the same order that they are named in the +dnl database. +OVS_VSWITCHD_START +AT_CHECK([ovs-vsctl \ + add-port br0 p1 -- set Interface p1 type=dummy --\ + add-port br0 p2 -- set Interface p2 type=dummy --\ + add-port br0 p3 -- set Interface p3 type=dummy --\ + set Bridge br0 mirrors=@m --\ + --id=@p2 get Port p2 -- --id=@p3 get Port p3 --\ + --id=@m create Mirror name=mymirror \ + select_dst_port=@p2 output_port=@p3 ], [0], [stdout]) +AT_CHECK([perl $srcdir/uuidfilt.pl stdout], [0], [dnl +<0> +]) + +AT_DATA([flows.txt], [dnl +in_port=1 actions=output:2 +in_port=2 actions=output:1 +]) +AT_CHECK([ovs-ofctl add-flows br0 flows.txt]) +AT_CHECK([ovs-appctl ofproto/trace br0 'in_port(1),eth(src=50:54:00:00:00:05,dst=50:54:00:00:00:07),eth_type(0x0800),ipv4(src=192.168.0.1,dst=192.168.0.2,proto=1,tos=0,ttl=128,frag=no),icmp(type=8,code=0)'], [0], [stdout]) +AT_CHECK([tail -1 stdout], [0], + [Datapath actions: 2,3 +]) +AT_CHECK([ovs-appctl ofproto/trace br0 'in_port(2),eth(src=50:54:00:00:00:05,dst=50:54:00:00:00:07),eth_type(0x0800),ipv4(src=192.168.0.1,dst=192.168.0.2,proto=1,tos=0,ttl=128,frag=no),icmp(type=8,code=0)'], [0], [stdout]) +AT_CHECK([tail -1 stdout], [0], + [Datapath actions: 1 +]) +OVS_VSWITCHD_STOP +AT_CLEANUP + +AT_SETUP([ofproto-dpif - mirroring, select_vlan]) +dnl This test assumes that OpenFlow port numbers are allocated in order +dnl starting from one. Unlike the previous tests, it does require that +dnl they are allocated in the same order that they are named in the +dnl database. +OVS_VSWITCHD_START +AT_CHECK([ovs-vsctl \ + add-port br0 p1 -- set Interface p1 type=dummy --\ + add-port br0 p2 -- set Interface p2 type=dummy --\ + add-port br0 p3 -- set Interface p3 type=dummy --\ + set Bridge br0 mirrors=@m --\ + --id=@p2 get Port p2 -- --id=@p3 get Port p3 --\ + --id=@m create Mirror name=mymirror \ + select_all=true select_vlan=11 output_port=@p3 ], [0], [stdout]) +AT_CHECK([perl $srcdir/uuidfilt.pl stdout], [0], [dnl +<0> +]) + +AT_DATA([flows.txt], [dnl +in_port=1, actions=output:2 +]) +AT_CHECK([ovs-ofctl add-flows br0 flows.txt]) +AT_CHECK([ovs-appctl ofproto/trace br0 'in_port(1),eth(src=50:54:00:00:00:05,dst=50:54:00:00:00:07),eth_type(0x0800),ipv4(src=192.168.0.1,dst=192.168.0.2,proto=1,tos=0,ttl=128,frag=no),icmp(type=8,code=0)'], [0], [stdout]) +AT_CHECK([tail -1 stdout], [0], + [Datapath actions: 2 +]) +AT_CHECK([ovs-appctl ofproto/trace br0 'in_port(1),eth(src=50:54:00:00:00:05,dst=50:54:00:00:00:07),eth_type(0x8100),vlan(vid=10,pcp=0),encap(eth_type(0x0800),ipv4(src=192.168.0.1,dst=192.168.0.2,proto=1,tos=0,ttl=128,frag=no),icmp(type=8,code=0))'], [0], [stdout]) +AT_CHECK([tail -1 stdout], [0], + [Datapath actions: 2 +]) +AT_CHECK([ovs-appctl ofproto/trace br0 'in_port(1),eth(src=50:54:00:00:00:05,dst=50:54:00:00:00:07),eth_type(0x8100),vlan(vid=11,pcp=0),encap(eth_type(0x0800),ipv4(src=192.168.0.1,dst=192.168.0.2,proto=1,tos=0,ttl=128,frag=no),icmp(type=8,code=0))'], [0], [stdout]) +AT_CHECK([tail -1 stdout], [0], + [Datapath actions: 2,3 +]) +OVS_VSWITCHD_STOP +AT_CLEANUP + +AT_SETUP([ofproto-dpif - mirroring, output_port]) +dnl This test assumes that OpenFlow port numbers are allocated in order +dnl starting from one. Unlike the previous tests, it does require that +dnl they are allocated in the same order that they are named in the +dnl database. +OVS_VSWITCHD_START +AT_CHECK([ovs-vsctl \ + add-port br0 p1 -- set Interface p1 type=dummy --\ + add-port br0 p2 -- set Interface p2 type=dummy --\ + add-port br0 p3 -- set Interface p3 type=dummy --\ + set Bridge br0 mirrors=@m --\ + --id=@p3 get Port p3 --\ + --id=@m create Mirror name=mymirror \ + select_all=true output_port=@p3 ], [0], [stdout]) +AT_CHECK([perl $srcdir/uuidfilt.pl stdout], [0], [dnl +<0> +]) + +AT_DATA([flows.txt], [dnl +in_port=1 actions=mod_vlan_vid:17,output:2 +in_port=2 actions=output:1 +]) +AT_CHECK([ovs-ofctl add-flows br0 flows.txt]) +AT_CHECK([ovs-appctl ofproto/trace br0 'in_port(1),eth(src=50:54:00:00:00:05,dst=50:54:00:00:00:07),eth_type(0x0800),ipv4(src=192.168.0.1,dst=192.168.0.2,proto=1,tos=0,ttl=128,frag=no),icmp(type=8,code=0)'], [0], [stdout]) +AT_CHECK([tail -1 stdout], [0], + [Datapath actions: push_vlan(vid=17,pcp=0),2,pop_vlan,3 +]) +AT_CHECK([ovs-appctl ofproto/trace br0 'in_port(2),eth(src=50:54:00:00:00:05,dst=50:54:00:00:00:07),eth_type(0x0800),ipv4(src=192.168.0.1,dst=192.168.0.2,proto=1,tos=0,ttl=128,frag=no),icmp(type=8,code=0)'], [0], [stdout]) +AT_CHECK([tail -1 stdout], [0], + [Datapath actions: 1,3 +]) +OVS_VSWITCHD_STOP +AT_CLEANUP + +AT_SETUP([ofproto-dpif - mirroring, output_vlan]) +dnl This test assumes that OpenFlow port numbers are allocated in order +dnl starting from one. Unlike the previous tests, it does require that +dnl they are allocated in the same order that they are named in the +dnl database. +OVS_VSWITCHD_START +AT_CHECK([ovs-vsctl \ + add-port br0 p1 -- set Interface p1 type=dummy --\ + add-port br0 p2 -- set Interface p2 type=dummy --\ + set Bridge br0 mirrors=@m --\ + --id=@m create Mirror name=mymirror \ + select_all=true output_vlan=12 ], [0], [stdout]) +AT_CHECK([perl $srcdir/uuidfilt.pl stdout], [0], [dnl +<0> +]) + +AT_DATA([flows.txt], [dnl +in_port=1 actions=output:2 +in_port=2 actions=mod_vlan_vid:17,output:1 +]) +AT_CHECK([ovs-ofctl add-flows br0 flows.txt]) + +flow="in_port(1),eth(src=50:54:00:00:00:05,dst=50:54:00:00:00:07),eth_type(0x0800),ipv4(src=192.168.0.1,dst=192.168.0.2,proto=1,tos=0,ttl=128,frag=no),icmp(type=8,code=0)" + +AT_CHECK([ovs-appctl ofproto/trace br0 $flow], [0], [stdout]) +actual=`tail -1 stdout | sed 's/Datapath actions: //'` + +AT_CHECK([ovs-dpctl normalize-actions "$flow" "2,push_vlan(vid=12,pcp=0),0,1,2"], [0], [stdout]) +mv stdout expout +AT_CHECK([ovs-dpctl normalize-actions "$flow" "$actual"], [0], [expout]) + +flow="in_port(2),eth(src=50:54:00:00:00:05,dst=50:54:00:00:00:07),eth_type(0x0800),ipv4(src=192.168.0.1,dst=192.168.0.2,proto=1,tos=0,ttl=128,frag=no),icmp(type=8,code=0)" + +AT_CHECK([ovs-appctl ofproto/trace br0 $flow], [0], [stdout]) +actual=`tail -1 stdout | sed 's/Datapath actions: //'` + +AT_CHECK([ovs-dpctl normalize-actions "$flow" "push_vlan(vid=17,pcp=0),1,pop_vlan,push_vlan(vid=12,pcp=0),0,1,2"], [0], [stdout]) +mv stdout expout +AT_CHECK([ovs-dpctl normalize-actions "$flow" "$actual"], [0], [expout]) +OVS_VSWITCHD_STOP +AT_CLEANUP -- 1.7.4.1 _______________________________________________ dev mailing list dev@openvswitch.org http://openvswitch.org/mailman/listinfo/dev