[<prev] [next>] [<thread-prev] [thread-next>] [day] [month] [year] [list]
Message-ID: <4B420464.3040301@trash.net>
Date: Mon, 04 Jan 2010 16:08:20 +0100
From: Patrick McHardy <kaber@...sh.net>
To: Samir Bellabes <sam@...ack.fr>
CC: linux-security-module@...r.kernel.org, jamal <hadi@...erus.ca>,
Evgeniy Polyakov <zbr@...emap.net>,
Neil Horman <nhorman@...driver.com>, netdev@...r.kernel.org,
netfilter-devel@...r.kernel.org
Subject: Re: [RFC 7/9] snet: introduce snet_netlink.c and snet_netlink.h
Samir Bellabes wrote:
> +++ b/security/snet/include/snet_netlink.h
> @@ -0,0 +1,201 @@
> +#ifndef _SNET_NETLINK_H
> +#define _SNET_NETLINK_H
> +
> +#include <linux/in6.h>
> +#include "snet_hooks.h"
> +
> +extern unsigned int snet_verdict_delay;
As this file defines the userspace interface, it probably shouldn't
contain declarations of kernel-internal variables (same for
snet_hooks.h). It would also be better placed in include/linux as
the other netlink API definitions.
> +
> +/* commands */
> +enum {
> + SNET_C_UNSPEC,
> + SNET_C_VERSION,
> + SNET_C_REGISTER,
> + SNET_C_UNREGISTER,
> + SNET_C_INSERT,
> + SNET_C_REMOVE,
> + SNET_C_FLUSH,
> + SNET_C_LIST,
> + SNET_C_VERDICT,
> + SNET_C_VERDICT_DELAY,
> + __SNET_C_MAX,
> +};
> +
> +#define SNET_C_MAX (__SNET_C_MAX - 1)
> +
> +/* attributes */
> +enum {
> + SNET_A_UNSPEC,
> + SNET_A_VERSION, /* (NLA_U32) the snet protocol version */
You're using this to check for a "compliant protocol version" below.
This shouldn't be needed as any protocol changes need to be done
in a compatible fashion.
> + SNET_A_SYSCALL, /* (NLA_U8) a syscall identifier */
We're already using 299 syscalls on x86, so perhaps a larger type
would be better suited.
> + SNET_A_PROTOCOL, /* (NLA_U8) a protocol identifier */
> + SNET_A_INSERTED,
> + SNET_A_REMOVED,
> + SNET_A_FLUSHED,
> + SNET_A_REGISTERED,
> + SNET_A_UNREGISTERED,
> + SNET_A_VERDICT_ID,
> + SNET_A_FAMILY,
> + SNET_A_UID,
> + SNET_A_PID,
> + SNET_A_VERDICT,
> + SNET_A_DATA_EXT,
> + SNET_A_VERDICT_DELAY,
> + SNET_A_VERDICT_DELAYED,
> + __SNET_A_MAX,
> +};
> +
> +#define SNET_A_MAX (__SNET_A_MAX - 1)
> +
> +#define SNET_GENL_NAME "SNET"
> +#define SNET_GENL_VERSION SNET_VERSION
> +
> +int snet_nl_send_event(const u32 verdict_id, const enum snet_syscall syscall,
> + const u8 protocol, const u8 family, void *data,
> + const unsigned int len);
> +
> +int snet_nl_list_fill_info(struct sk_buff *skb, u32 pid, u32 seq,
> + u32 flags, u8 protocol, enum snet_syscall syscall);
> +
> +void snet_netlink_exit(void);
> +
> +struct snet_sock_half {
> + struct {
> + union {
> + __be32 ip;
> + struct in6_addr ip6;
> + };
> + } u3;
> + struct {
> + __be16 port;
> + } u;
> +};
> +
> +struct snet_sock_info {
> + struct snet_sock_half src;
> + struct snet_sock_half dst;
> + int type;
> +};
How about using a struct sockaddr or encoding the values within
netlink attributes? That would provide a bit more flexibility in
case you want to support more protocols in the future.
> +
> +#endif /* _SNET_NETLINK_H */
> diff --git a/security/snet/snet_netlink.c b/security/snet/snet_netlink.c
> new file mode 100644
> index 0000000..cc21d6c
> --- /dev/null
> +++ b/security/snet/snet_netlink.c
> @@ -0,0 +1,541 @@
> +#include <linux/sched.h>
> +#include <net/genetlink.h>
> +#include <linux/in6.h>
> +
> +#include "snet.h"
> +#include "snet_netlink.h"
> +#include "snet_verdict.h"
> +#include "snet_event.h"
> +#include <snet_utils.h>
> +
> +atomic_t snet_nl_seq = ATOMIC_INIT(0);
> +static uint32_t snet_nl_pid;
> +static struct genl_family snet_genl_family;
> +atomic_t snet_num_listeners = ATOMIC_INIT(0);
The num_listeners seem to be redundant as you only support a
single listener anyways, whose presence is indicated by
snet_nl_pid != 0.
> +
> +/*
> + * snet genetlink
> + */
> +int snet_nl_send_event(const u32 verdict_id, const enum snet_syscall syscall,
> + const u8 protocol, const u8 family, void *data,
> + const unsigned int len)
> +{
> + struct sk_buff *skb_rsp;
> + void *msg_head;
> + int ret = -ENOMEM;
> +
> + skb_rsp = genlmsg_new(NLMSG_GOODSIZE, GFP_KERNEL);
> + if (skb_rsp == NULL)
> + return 0;
You could decrease the chance of rcvqueue overflow by using a smaller
allocation size.
> +
> + msg_head = genlmsg_put(skb_rsp, snet_nl_pid,
> + atomic_inc_return(&snet_nl_seq),
> + &snet_genl_family, 0, SNET_C_VERDICT);
> + if (msg_head == NULL)
> + goto send_event_failure;
> +
> + snet_dbg("verdict_id=0x%x syscall=%s protocol=%u "
> + "family=%u uid=%u pid=%u\n",
> + verdict_id, snet_syscall_name(syscall),
> + protocol, family, current_uid(), current->pid);
> +
> + if (verdict_id) {
> + ret = nla_put_u32(skb_rsp, SNET_A_VERDICT_ID, verdict_id);
> + if (ret != 0)
> + goto send_event_failure;
> + }
> + ret = nla_put_u8(skb_rsp, SNET_A_SYSCALL, syscall);
> + if (ret != 0)
> + goto send_event_failure;
> + ret = nla_put_u8(skb_rsp, SNET_A_PROTOCOL, protocol);
> + if (ret != 0)
> + goto send_event_failure;
> + ret = nla_put_u8(skb_rsp, SNET_A_FAMILY, family);
> + if (ret != 0)
> + goto send_event_failure;
> + ret = nla_put_u32(skb_rsp, SNET_A_UID, current_uid());
> + if (ret != 0)
> + goto send_event_failure;
> + ret = nla_put_u32(skb_rsp, SNET_A_PID, current->pid);
> + if (ret != 0)
> + goto send_event_failure;
> + ret = nla_put(skb_rsp, SNET_A_DATA_EXT, len, data);
> + if (ret != 0)
> + goto send_event_failure;
I guess its a matter of taste, but the NLA_PUT macros already take
care of exception handling and keep the code smaller.
> +
> + ret = genlmsg_end(skb_rsp, msg_head);
> + if (ret < 0)
> + goto send_event_failure;
> +
> + genlmsg_unicast(&init_net, skb_rsp, snet_nl_pid);
> + return 0;
> +
> +send_event_failure:
> + kfree_skb(skb_rsp);
> + return 0;
Shouldn't this error be returned to the caller to avoid waiting
for the timeout to occur (same question for the return value of
genlmsg_unicast, which can also fail).
> +}
> +
> +/*
> + * snet genetlink helper functions
> + */
> +static int snet_nl_response_flag(struct genl_info *info,
> + struct genl_family *family,
> + u8 cmd, int attrtype, u8 set_resp_flag)
> +{
> + int ret = -EINVAL;
> + struct sk_buff *skb_rsp = NULL;
> + void *msg_head;
> +
> + skb_rsp = genlmsg_new(NLMSG_GOODSIZE, GFP_KERNEL);
> + if (skb_rsp == NULL)
> + return -ENOMEM;
> + msg_head = genlmsg_put_reply(skb_rsp, info, family, 0, cmd);
> + if (msg_head == NULL)
> + goto response_failure;
> +
> + /* we put flag only if it is asked */
> + if (set_resp_flag) {
> + ret = nla_put_flag(skb_rsp, attrtype);
> + if (ret != 0)
> + goto response_failure;
> + }
> +
> + genlmsg_end(skb_rsp, msg_head);
> + ret = genlmsg_reply(skb_rsp, info);
> + if (ret != 0)
> + goto response_failure;
> + return 0;
> +
> +response_failure:
> + kfree_skb(skb_rsp);
> + return ret;
> +}
> +
> +/*
> + * snet genetlink functions
> + */
> +
> +static struct genl_family snet_genl_family = {
> + .id = GENL_ID_GENERATE,
> + .hdrsize = 0,
> + .name = SNET_GENL_NAME,
> + .version = SNET_GENL_VERSION,
> + .maxattr = SNET_A_MAX,
> +};
> +
> +static const struct nla_policy snet_genl_policy[SNET_A_MAX + 1]
> +__read_mostly = {
You don't need __read_mostly for const. If I recall correctly, it even
causes an error with certain compiler or linker versions.
> + [SNET_A_VERSION] = { .type = NLA_U32 },
> + [SNET_A_SYSCALL] = { .type = NLA_U8 },
> + [SNET_A_PROTOCOL] = { .type = NLA_U8 },
> + [SNET_A_INSERTED] = { .type = NLA_FLAG },
> + [SNET_A_REMOVED] = { .type = NLA_FLAG },
> + [SNET_A_FLUSHED] = { .type = NLA_FLAG },
> + [SNET_A_REGISTERED] = { .type = NLA_FLAG },
> + [SNET_A_UNREGISTERED] = { .type = NLA_FLAG },
> + [SNET_A_VERDICT_ID] = { .type = NLA_U32 },
> + [SNET_A_FAMILY] = { .type = NLA_U8 },
> + [SNET_A_UID] = { .type = NLA_U32 },
> + [SNET_A_PID] = { .type = NLA_U32 },
> + [SNET_A_VERDICT] = { .type = NLA_U8 },
> + [SNET_A_DATA_EXT] = { .type = NLA_BINARY,
> + .len = sizeof(struct snet_sock_info) },
> + [SNET_A_VERDICT_DELAY] = { .type = NLA_U32 },
> + [SNET_A_VERDICT_DELAYED] = { .type = NLA_FLAG },
> +};
> +
> +/**
> + * snet_nl_version - Handle a VERSION message
> + * @skb: the NETLINK buffer
> + * @info: the Generic NETLINK info block
> + *
> + * Description:
> + * Process a user generated VERSION message and respond accordingly.
> + * Returns zero on success, negative values on failure.
> + */
> +static int snet_nl_version(struct sk_buff *skb, struct genl_info *info)
> +{
> + int ret = -ENOMEM;
> + struct sk_buff *skb_rsp = NULL;
> + void *msg_head;
> +
> + atomic_set(&snet_nl_seq, info->snd_seq);
> +
> + if (atomic_read(&snet_num_listeners) <= 0)
> + return 0;
In all these checks for listeners, I think it would make sense to
provide an error to userspace if it hasn't registered properly,
perhaps ENOTCONN or something like that.
> +
> + skb_rsp = genlmsg_new(NLMSG_GOODSIZE, GFP_KERNEL);
> + if (skb_rsp == NULL)
> + return -ENOMEM;
> + msg_head = genlmsg_put_reply(skb_rsp, info, &snet_genl_family,
> + 0, SNET_C_VERSION);
> + if (msg_head == NULL)
> + goto version_failure;
> +
> + ret = nla_put_u32(skb_rsp, SNET_A_VERSION, SNET_VERSION);
> + if (ret != 0)
> + goto version_failure;
> +
> + genlmsg_end(skb_rsp, msg_head);
> +
> + ret = genlmsg_reply(skb_rsp, info);
> + if (ret != 0)
> + goto version_failure;
> + return 0;
> +
> +version_failure:
> + kfree_skb(skb_rsp);
> + return ret;
> +}
> +
> +/**
> + * snet_nl_register - Handle a REGISTER message
> + * @skb: the NETLINK buffer
> + * @info: the Generic NETLINK info block
> + *
> + * Description:
> + * Notify the kernel that an application is listening for events.
> + * Returns zero on success, negative values on failure.
> + */
> +static int snet_nl_register(struct sk_buff *skb, struct genl_info *info)
> +{
> + int ret = -EINVAL;
> + u32 version = 0;
> + u8 set_resp_flag = 0;
> +
> + atomic_set(&snet_nl_seq, info->snd_seq);
> +
> + if (!info->attrs[SNET_A_VERSION])
> + return -EINVAL;
> + version = nla_get_u32(info->attrs[SNET_A_VERSION]);
> +
> + if (version == SNET_VERSION) { /* version is compliant */
> + atomic_inc(&snet_num_listeners);
> + set_resp_flag = 1;
> + }
> +
> + ret = snet_nl_response_flag(info, &snet_genl_family,
> + SNET_C_REGISTER, SNET_A_REGISTERED,
> + set_resp_flag);
Is this really needed? A return value of 0 should already tell userspace
that the command was successful. If it really wants a seperate success
message, it can use NLM_F_ACK. This will also automatically take care
of using the proper sequence number, so the snet_nl_seq handling isn't
required anymore. Same for all similar cases below.
> +
> + snet_nl_pid = info->snd_pid;
> + snet_dbg("pid=%u num_listeners=%d\n",
> + snet_nl_pid, atomic_read(&snet_num_listeners));
> + return ret;
> +}
> +
> +/**
> + * snet_nl_unregister - Handle a UNREGISTER message
> + * @skb: the NETLINK buffer
> + * @info: the Generic NETLINK info block
> + *
> + * Description:
> + * Notify the kernel that the application is no more listening for events.
> + * Returns zero on success, negative values on failure.
> + */
> +static int snet_nl_unregister(struct sk_buff *skb, struct genl_info *info)
> +{
> + int ret = -EINVAL;
> +
> + atomic_set(&snet_nl_seq, info->snd_seq);
> +
> + if (atomic_read(&snet_num_listeners))
> + atomic_dec(&snet_num_listeners);
> + ret = snet_nl_response_flag(info, &snet_genl_family,
> + SNET_C_UNREGISTER, SNET_A_UNREGISTERED, 1);
> + snet_dbg("pid=%u num_listeners=%d\n",
> + snet_nl_pid, atomic_read(&snet_num_listeners));
> + return ret;
> +}
> +
> +/**
> + * snet_nl_insert - Handle a INSERT message
> + * @skb: the NETLINK buffer
> + * @info: the Generic NETLINK info block
> + *
> + * Description:
> + * Insert a new event to the events' hashtable. Returns zero on success,
> + * negative values on failure.
> + */
> +static int snet_nl_insert(struct sk_buff *skb, struct genl_info *info)
> +{
> + int ret_event = -EINVAL, ret = -EINVAL;
> + enum snet_syscall syscall;
> + u8 protocol;
> + u8 set_resp_flag = 0;
> +
> + atomic_set(&snet_nl_seq, info->snd_seq);
> +
> + if (atomic_read(&snet_num_listeners) <= 0)
> + return 0;
> +
> + if (!info->attrs[SNET_A_SYSCALL] || !info->attrs[SNET_A_PROTOCOL])
> + return -EINVAL;
> +
> + syscall = nla_get_u8(info->attrs[SNET_A_SYSCALL]);
> + protocol = nla_get_u8(info->attrs[SNET_A_PROTOCOL]);
> + ret_event = snet_event_insert(syscall, protocol);
> + snet_dbg("syscall=%s protocol=%u insert=%s\n",
> + snet_syscall_name(syscall), protocol,
> + (ret_event == 0) ? "success" : "failed");
> +
> + if (ret_event == 0)
> + set_resp_flag = 1;
> +
> + ret = snet_nl_response_flag(info, &snet_genl_family,
> + SNET_C_INSERT, SNET_A_INSERTED,
> + set_resp_flag);
> + return ret;
> +}
> +
> +/**
> + * snet_nl_remove - Handle a REMOVE message
> + * @skb: the NETLINK buffer
> + * @info: the Generic NETLINK info block
> + *
> + * Description:
> + * Remove a event from the events' hastable. Returns zero on success,
> + * negative values on failure.
> + */
> +static int snet_nl_remove(struct sk_buff *skb, struct genl_info *info)
> +{
> + int ret_event = -EINVAL, ret = -EINVAL;
> + enum snet_syscall syscall;
> + u8 protocol;
> + u8 set_resp_flag = 0;
> +
> + atomic_set(&snet_nl_seq, info->snd_seq);
> +
> + if (atomic_read(&snet_num_listeners) <= 0)
> + return 0;
> +
> + if (!info->attrs[SNET_A_SYSCALL] || !info->attrs[SNET_A_PROTOCOL])
> + return -EINVAL;
> +
> + syscall = nla_get_u8(info->attrs[SNET_A_SYSCALL]);
> + protocol = nla_get_u8(info->attrs[SNET_A_PROTOCOL]);
> + ret_event = snet_event_remove(syscall, protocol);
> + snet_dbg("syscall=%s protocol=%u remove=%s\n",
> + snet_syscall_name(syscall), protocol,
> + (ret_event == 0) ? "success" : "failed");
> +
> + if (ret_event == 0)
> + set_resp_flag = 1;
> +
> + ret = snet_nl_response_flag(info, &snet_genl_family,
> + SNET_C_REMOVE, SNET_A_REMOVED,
> + set_resp_flag);
> + return ret;
> +}
> +
> +/**
> + * snet_nl_flush - Handle a FLUSH message
> + * @skb: the NETLINK buffer
> + * @info: the Generic NETLINK info block
> + *
> + * Description:
> + * Remove all events from the hashtable. Returns zero on success,
> + * negative values on failure.
> + */
> +static int snet_nl_flush(struct sk_buff *skb, struct genl_info *info)
> +{
> + int ret = -EINVAL;
> + u8 set_resp_flag = 0;
> +
> + atomic_set(&snet_nl_seq, info->snd_seq);
> +
> + if (atomic_read(&snet_num_listeners) <= 0)
> + return 0;
> +
> + snet_event_flush();
> +
> + set_resp_flag = 1;
> +
> + ret = snet_nl_response_flag(info, &snet_genl_family,
> + SNET_C_FLUSH, SNET_A_FLUSHED,
> + set_resp_flag);
> + return ret;
> +}
> +
> +int snet_nl_list_fill_info(struct sk_buff *skb, u32 pid, u32 seq,
> + u32 flags, u8 protocol, enum snet_syscall syscall)
> +{
> + void *hdr;
> + int ret = -1;
> +
> + hdr = genlmsg_put(skb, pid, seq, &snet_genl_family, flags, SNET_C_LIST);
> + if (hdr == NULL)
> + return -1;
> +
> + ret = nla_put_u8(skb, SNET_A_SYSCALL, syscall);
> + if (ret != 0)
> + goto list_failure;
> +
> + ret = nla_put_u8(skb, SNET_A_PROTOCOL, protocol);
> + if (ret != 0)
> + goto list_failure;
> +
> + return genlmsg_end(skb, hdr);
> +
> +list_failure:
> + genlmsg_cancel(skb, hdr);
> + return 0;
> +}
> +/**
> + * snet_nl_list - Handle a LIST message
> + * @skb: the NETLINK buffer
> + * @cb:
> + *
> + * Description:
> + * Process a user LIST message and respond. Returns zero on success,
> + * and negative values on error.
> + */
> +static int snet_nl_list(struct sk_buff *skb, struct netlink_callback *cb)
> +{
> + unsigned int len = 0;
> +
> + atomic_set(&snet_nl_seq, cb->nlh->nlmsg_seq);
> + len = snet_event_fill_info(skb, cb);
> + return len;
> +}
> +
> +/**
> + * snet_nl_verdict - Handle a VERDICT message
> + * @skb: the NETLINK buffer
> + * @info the Generic NETLINK info block
> + *
> + * Description:
> + * Provides userspace with a VERDICT message, ie we are sending informations
> + * with this command. Userspace is sending the appropriate verdict for the
> + * event. Returns zero on success,and negative values on error.
> + */
> +static int snet_nl_verdict(struct sk_buff *skb,
> + struct genl_info *info)
> +{
> + u32 verdict_id;
> + enum snet_verdict verdict;
> +
> + atomic_set(&snet_nl_seq, info->snd_seq);
> +
> + if (atomic_read(&snet_num_listeners) <= 0)
> + return 0;
> +
> + if (!info->attrs[SNET_A_VERDICT_ID] || !info->attrs[SNET_A_VERDICT])
> + return -EINVAL;
> +
> + verdict_id = nla_get_u32(info->attrs[SNET_A_VERDICT_ID]);
> + verdict = nla_get_u8(info->attrs[SNET_A_VERDICT]);
> + snet_verdict_set(verdict_id, verdict);
> + return 0;
> +}
> +
> +/**
> + * snet_nl_verdict_delay - Handle a VERDICT_DELAY message
> + * @skb: the NETLINK buffer
> + * @info the Generic NETLINK info block
> + *
> + * Description:
> + * Provides userspace with a VERDICT_DELAY message, ie userspace application
> + * is able to set the value of the timeout for verdicts
> + * Returns zero on success, and negative values on error.
> + */
> +static int snet_nl_verdict_delay(struct sk_buff *skb,
> + struct genl_info *info)
> +{
> + int ret = -EINVAL;
> +
> + atomic_set(&snet_nl_seq, info->snd_seq);
> +
> + if (atomic_read(&snet_num_listeners) <= 0)
> + return 0;
> +
> + if (!info->attrs[SNET_A_VERDICT_DELAY])
> + return -EINVAL;
> +
> + snet_verdict_delay = nla_get_u32(info->attrs[SNET_A_VERDICT_DELAY]);
> + /* FIXME: do something */
> + ret = snet_nl_response_flag(info, &snet_genl_family,
> + SNET_C_VERDICT_DELAY, SNET_A_VERDICT_DELAYED,
> + 1);
> + return ret;
> +}
> +
> +static struct genl_ops snet_genl_ops[] = {
> + {
> + .cmd = SNET_C_VERSION,
> + .flags = GENL_ADMIN_PERM,
> + .policy = snet_genl_policy,
> + .doit = snet_nl_version,
> + .dumpit = NULL,
The NULL initializations aren't neccessary.
> + },
> + {
> + .cmd = SNET_C_REGISTER,
> + .flags = GENL_ADMIN_PERM,
> + .policy = snet_genl_policy,
> + .doit = snet_nl_register,
> + .dumpit = NULL,
> + },
> + {
> + .cmd = SNET_C_UNREGISTER,
> + .flags = GENL_ADMIN_PERM,
> + .policy = snet_genl_policy,
> + .doit = snet_nl_unregister,
> + .dumpit = NULL,
> + },
> + {
> + .cmd = SNET_C_INSERT,
> + .flags = GENL_ADMIN_PERM,
> + .policy = snet_genl_policy,
> + .doit = snet_nl_insert,
> + .dumpit = NULL,
> + },
> + {
> + .cmd = SNET_C_REMOVE,
> + .flags = GENL_ADMIN_PERM,
> + .policy = snet_genl_policy,
> + .doit = snet_nl_remove,
> + .dumpit = NULL,
> + },
> + {
> + .cmd = SNET_C_FLUSH,
> + .flags = GENL_ADMIN_PERM,
> + .policy = snet_genl_policy,
> + .doit = snet_nl_flush,
> + .dumpit = NULL,
> + },
> + {
> + .cmd = SNET_C_LIST,
> + .flags = GENL_ADMIN_PERM,
> + .policy = snet_genl_policy,
> + .doit = NULL,
> + .dumpit = snet_nl_list,
> + },
> + {
> + .cmd = SNET_C_VERDICT,
> + .flags = GENL_ADMIN_PERM,
> + .policy = snet_genl_policy,
> + .doit = snet_nl_verdict,
> + .dumpit = NULL,
> + },
> + {
> + .cmd = SNET_C_VERDICT_DELAY,
> + .flags = GENL_ADMIN_PERM,
> + .policy = snet_genl_policy,
> + .doit = snet_nl_verdict_delay,
> + .dumpit = NULL,
> + },
> +};
> +
> +static __init int snet_netlink_init(void)
> +{
> + return genl_register_family_with_ops(&snet_genl_family,
> + snet_genl_ops,
> + ARRAY_SIZE(snet_genl_ops));
> +}
> +
> +void snet_netlink_exit(void)
> +{
> + genl_unregister_family(&snet_genl_family);
> +}
> +
> +__initcall(snet_netlink_init);
--
To unsubscribe from this list: send the line "unsubscribe netdev" in
the body of a message to majordomo@...r.kernel.org
More majordomo info at http://vger.kernel.org/majordomo-info.html
Powered by blists - more mailing lists