The AC/PPPoE driver implements pseudowire type L2TP_PWTYPE_PPP_AC, for use in a PPPoE Access Concentrator configuration. Rather than terminating the PPP session locally, the AC/PPPoE driver forwards PPP packets over an L2TP tunnel for termination at the LNS.
l2tp_ac_pppoe provides a data path for PPPoE session packets, and should be instantiated once a userspace process has completed the PPPoE discovery process. To create an instance of an L2TP_PWTYPE_PPP_AC pseudowire, userspace must use the L2TP_CMD_SESSION_CREATE netlink command, and pass the following attributes: * L2TP_ATTR_IFNAME, to specify the name of the interface associated with the PPPoE session; * L2TP_ATTR_PPPOE_SESSION_ID, to specify the PPPoE session ID assigned to the session; * L2TP_ATTR_PPPOE_PEER_MAC_ADDR, to specify the MAC address of the PPPoE peer Signed-off-by: Tom Parkin <tpar...@katalix.com> --- net/l2tp/Kconfig | 7 + net/l2tp/Makefile | 1 + net/l2tp/l2tp_ac_pppoe.c | 446 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 454 insertions(+) create mode 100644 net/l2tp/l2tp_ac_pppoe.c diff --git a/net/l2tp/Kconfig b/net/l2tp/Kconfig index b7856748e960..f34d72070a6f 100644 --- a/net/l2tp/Kconfig +++ b/net/l2tp/Kconfig @@ -108,3 +108,10 @@ config L2TP_ETH To compile this driver as a module, choose M here. The module will be called l2tp_eth. + +config L2TP_AC_PPPOE + tristate "L2TP PPP Access Concentrator support" + depends on L2TP + help + Support for tunneling PPP frames from PPPoE sessions in an L2TP + session. diff --git a/net/l2tp/Makefile b/net/l2tp/Makefile index cf8f27071d3f..5bd66ae45eb6 100644 --- a/net/l2tp/Makefile +++ b/net/l2tp/Makefile @@ -16,3 +16,4 @@ obj-$(subst y,$(CONFIG_L2TP),$(CONFIG_L2TP_DEBUGFS)) += l2tp_debugfs.o ifneq ($(CONFIG_IPV6),) obj-$(subst y,$(CONFIG_L2TP),$(CONFIG_L2TP_IP)) += l2tp_ip6.o endif +obj-$(subst y,$(CONFIG_L2TP),$(CONFIG_L2TP_AC_PPPOE)) += l2tp_ac_pppoe.o diff --git a/net/l2tp/l2tp_ac_pppoe.c b/net/l2tp/l2tp_ac_pppoe.c new file mode 100644 index 000000000000..59dce046c813 --- /dev/null +++ b/net/l2tp/l2tp_ac_pppoe.c @@ -0,0 +1,446 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* L2TP PPPoE access concentrator driver + * + * Copyright (c) 2020 Katalix Systems Ltd + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include <linux/module.h> +#include <linux/if_pppox.h> +#include <linux/l2tp.h> +#include <linux/ppp_defs.h> +#include <linux/etherdevice.h> + +#include <net/net_namespace.h> +#include <net/netns/generic.h> + +#include "l2tp_core.h" + +#define L2TP_AC_PPPOE_SESSION_HASH_BITS 5 +#define L2TP_AC_PPPOE_SESSION_HASH_SIZE BIT(L2TP_AC_PPPOE_SESSION_HASH_BITS) + +/* Global hash list of PPPoE sessions. + * We hash on the PPPoE session ID, and scope session lookups to the + * associated netdev instance. + * Because the lookup is scoped to the netdev, it is practically + * scoped to the network namespace the netdev exists in. + */ +static struct hlist_head pppoe_session_hlist[L2TP_AC_PPPOE_SESSION_HASH_SIZE]; +static spinlock_t pppoe_session_hlist_lock; + +/* An AC PPPoE session instance */ +struct l2tp_ac_pppoe_session { + /* "Dead" flag used to prevent races between l2tp_core session delete + * and session removal via. a netdev event. + */ + unsigned long dead; + /* Device associated with the PPPoE session */ + struct net_device __rcu *dev; + /* PPPoE session ID */ + u16 id; + /* Destination MAC address for PPPoE frames */ + unsigned char h_dest[ETH_ALEN]; + /* L2TP session for this PPPoE session */ + struct l2tp_session *ls; + /* Entry on global hashlist */ + struct hlist_node hlist; +}; + +static struct hlist_head *l2tp_ac_pppoe_id_hash(u16 id) +{ + return &pppoe_session_hlist[hash_32(id, L2TP_AC_PPPOE_SESSION_HASH_BITS)]; +} + +/* Look up a PPPoE session instance by its ID. + * Must be called inside an rcu read lock. + */ +static struct l2tp_ac_pppoe_session *l2tp_ac_pppoe_find_by_id(struct net_device *dev, u16 id) +{ + struct l2tp_ac_pppoe_session *ps; + struct hlist_head *head; + + head = l2tp_ac_pppoe_id_hash(id); + + hlist_for_each_entry_rcu(ps, head, hlist) + if (ps->id == id) + if (rcu_dereference(ps->dev) == dev) + return ps; + + return NULL; +} + +static void l2tp_ac_pppoe_unhash_session(struct l2tp_ac_pppoe_session *ps) +{ + spin_lock_bh(&pppoe_session_hlist_lock); + hlist_del_init_rcu(&ps->hlist); + spin_unlock_bh(&pppoe_session_hlist_lock); + synchronize_rcu(); +} + +static void l2tp_ac_pppoe_kill_session(struct l2tp_ac_pppoe_session *ps) +{ + struct net_device *dev; + + if (test_and_set_bit(0, &ps->dead)) + return; + + rcu_read_lock(); + dev = rcu_dereference(ps->dev); + rcu_assign_pointer(ps->dev, NULL); + rcu_read_unlock(); + + /* This shouldn't occur, ref: l2tp_ac_pppoe_create_session + * which holds a session reference around assigning the dev + * pointer. + */ + if (WARN_ON(!dev)) + return; + + l2tp_ac_pppoe_unhash_session(ps); + + /* Drop the references taken by the session */ + dev_put(dev); + module_put(THIS_MODULE); +} + +/* struct l2tp_session pseudowire close callback */ +static void l2tp_ac_pppoe_session_close(struct l2tp_session *ls) +{ + l2tp_ac_pppoe_kill_session(l2tp_session_priv(ls)); +} + +/* struct l2tp_session pseudowire recv callback */ +static void l2tp_ac_pppoe_recv_skb(struct l2tp_session *ls, struct sk_buff *skb, int l2tp_data_len) +{ + struct l2tp_ac_pppoe_session *ps = l2tp_session_priv(ls); + int data_len = skb->len; + struct net_device *dev; + struct pppoe_hdr *ph; + + rcu_read_lock(); + + dev = rcu_dereference(ps->dev); + if (!dev) + goto drop; + + if (skb_cow_head(skb, sizeof(*ph) + dev->hard_header_len)) + goto drop; + + /* If the user data has PPP Address and Control fields, strip them out. + * This follows the approach of l2tp_ppp.c, which notes that although + * use of these fields should in theory be negotiated and handled at + * the PPP layer, the L2TP subsystem has always detected and removed + * them. + */ + if (skb->data[0] == PPP_ALLSTATIONS && skb->data[1] == PPP_UI) { + if (pskb_may_pull(skb, 2)) { + skb_pull(skb, 2); + data_len -= 2; + } + } + + /* Add PPPoE header */ + __skb_push(skb, sizeof(*ph)); + skb_reset_network_header(skb); + + ph = pppoe_hdr(skb); + ph->ver = 0x1; + ph->type = 0x1; + ph->code = 0; + ph->sid = htons(ps->id); + ph->length = htons(data_len); + + /* SKB settings */ + skb->dev = dev; + skb->protocol = htons(ETH_P_PPP_SES); + skb->ip_summed = CHECKSUM_UNNECESSARY; + + /* Add Ethernet header */ + dev_hard_header(skb, dev, ETH_P_PPP_SES, ps->h_dest, NULL, data_len); + + rcu_read_unlock(); + + dev_queue_xmit(skb); + + return; + +drop: + rcu_read_unlock(); + kfree_skb(skb); +} + +/* struct l2tp_session pseudowire show callback */ +static void l2tp_ac_pppoe_show(struct seq_file *m, void *arg) +{ + struct l2tp_ac_pppoe_session *ps = l2tp_session_priv(arg); + struct net_device *dev; + + rcu_read_lock(); + dev = rcu_dereference(ps->dev); + if (!dev) { + rcu_read_unlock(); + return; + } + rcu_read_unlock(); + + seq_printf(m, " interface %s\n", dev->name); + seq_printf(m, " PPPoE session %d\n", ps->id); + seq_printf(m, " client hwaddr %02X:%02X:%02X:%02X:%02X:%02X\n", + ps->h_dest[0], ps->h_dest[1], ps->h_dest[2], + ps->h_dest[3], ps->h_dest[4], ps->h_dest[5]); +} + +static int l2tp_ac_pppoe_create_session(struct net_device *dev, u16 id, + unsigned char *peer_mac, + struct l2tp_tunnel *tunnel, u32 sid, + u32 psid, struct l2tp_session_cfg *cfg, + struct l2tp_ac_pppoe_session **out) +{ + struct l2tp_ac_pppoe_session *ps; + struct l2tp_session *ls; + int ret = 0; + + ls = l2tp_session_create(sizeof(*ps), tunnel, sid, psid, cfg); + if (IS_ERR(ls)) { + ret = PTR_ERR(ls); + goto out; + } + + ps = l2tp_session_priv(ls); + memcpy(ps->h_dest, peer_mac, ETH_ALEN); + ps->id = id; + ps->ls = ls; + INIT_HLIST_NODE(&ps->hlist); + + ls->session_close = l2tp_ac_pppoe_session_close; + ls->recv_skb = l2tp_ac_pppoe_recv_skb; + if (IS_ENABLED(CONFIG_L2TP_DEBUGFS)) + ls->show = l2tp_ac_pppoe_show; + + /* Hold session refcount to ensure it can't go away until we have + * assigned the dev pointer in struct l2tp_ac_pppoe_session and + * taken a reference on the device. + */ + l2tp_session_inc_refcount(ls); + + ret = l2tp_session_register(ls, tunnel); + if (ret < 0) { + l2tp_session_dec_refcount(ls); + goto out; + } + + rcu_assign_pointer(ps->dev, dev); + dev_hold(ps->dev); + + l2tp_session_dec_refcount(ls); + + __module_get(THIS_MODULE); + + *out = ps; + +out: + return ret; +} + +/* Pass PPPoE packet into the associated L2TP session */ +static int l2tp_ac_pppoe_l2tp_xmit(struct net_device *dev, struct sk_buff *skb) +{ + struct l2tp_ac_pppoe_session *ps; + struct pppoe_hdr *ph; + int ret; + + if (!pskb_may_pull(skb, sizeof(*ph))) + goto drop; + + ph = pppoe_hdr(skb); + + skb_pull(skb, sizeof(*ph)); + + rcu_read_lock(); + + ps = l2tp_ac_pppoe_find_by_id(dev, ntohs(ph->sid)); + if (!ps) + goto unlock_drop; + + ret = l2tp_xmit_skb(ps->ls, skb); + rcu_read_unlock(); + return ret; + +unlock_drop: + rcu_read_unlock(); +drop: + kfree_skb(skb); + return NET_RX_DROP; +} + +/* PPPoE session packet rx handler */ +static int l2tp_ac_pppoe_recv_pppoe(struct sk_buff *skb, struct net_device *dev, + struct packet_type *pt, + struct net_device *orig_dev) +{ + skb = skb_share_check(skb, GFP_ATOMIC); + if (!skb) + return NET_RX_DROP; + return l2tp_ac_pppoe_l2tp_xmit(dev, skb); +} + +/* L2TP/netlink pseudowire create callback */ +static int l2tp_ac_pppoe_nl_create(struct net *net, struct l2tp_tunnel *tunnel, + u32 sid, u32 psid, + struct l2tp_session_cfg *cfg, + struct genl_info *info) +{ + unsigned char peer_mac[ETH_ALEN]; + struct l2tp_ac_pppoe_session *ps; + struct net_device *dev = NULL; + u16 pppoe_id; + int ret; + + /* We must have PPPoE session ID, the PPPoE peer's MAC address. + * and the name of the interface. The latter has already been + * unpacked into the session config structure by l2tp_netlink.c. + */ + if (!info->attrs[L2TP_ATTR_PPPOE_SESSION_ID]) { + ret = -EINVAL; + goto out; + } + + pppoe_id = nla_get_u16(info->attrs[L2TP_ATTR_PPPOE_SESSION_ID]); + if (!pppoe_id) { + ret = -EINVAL; + goto out; + } + + if (!info->attrs[L2TP_ATTR_PPPOE_PEER_MAC_ADDR]) { + ret = -EINVAL; + goto out; + } + + /* l2tp_netlink policy for mandates that PEER_MAC_ADDR must be of ETH_ALEN bytes */ + memcpy(peer_mac, nla_data(info->attrs[L2TP_ATTR_PPPOE_PEER_MAC_ADDR]), ETH_ALEN); + if (!is_valid_ether_addr(peer_mac)) { + ret = -EINVAL; + goto out; + } + + if (!cfg->ifname) { + ret = -EINVAL; + goto out; + } + + /* Look up the netdev of the specified name */ + dev = dev_get_by_name(net, cfg->ifname); + if (!dev) { + ret = -ENODEV; + goto out; + } + + /* Prevent ID clashes. + * Note the genl lock prevents any race due to the gap between checking + * for a clash and adding this session to the hash list, since: + * - ac_pppoe sessions can only be created by netlink command, and + * - l2tp_netlink doesn't enable parallel genl ops. + */ + rcu_read_lock(); + if (l2tp_ac_pppoe_find_by_id(dev, pppoe_id)) { + ret = -EALREADY; + rcu_read_unlock(); + goto out; + } + rcu_read_unlock(); + + ret = l2tp_ac_pppoe_create_session(dev, pppoe_id, peer_mac, tunnel, sid, psid, cfg, &ps); + if (ret != 0) + goto out; + + /* Add session to global hash */ + spin_lock_bh(&pppoe_session_hlist_lock); + hlist_add_head_rcu(&ps->hlist, l2tp_ac_pppoe_id_hash(pppoe_id)); + spin_unlock_bh(&pppoe_session_hlist_lock); + +out: + /* Drop dev reference if we have it: the session takes its own reference */ + if (dev) + dev_put(dev); + return ret; +} + +static int l2tp_ac_pppoe_netdevice_event(struct notifier_block *unused, + unsigned long event, void *ptr) +{ + struct net_device *dev = netdev_notifier_info_to_dev(ptr); + struct l2tp_ac_pppoe_session *ps; + int hash; + + if (event == NETDEV_UNREGISTER) { + rcu_read_lock(); + for (hash = 0; hash < L2TP_AC_PPPOE_SESSION_HASH_SIZE; hash++) + hlist_for_each_entry_rcu(ps, &pppoe_session_hlist[hash], hlist) + if (ps->dev == dev) + l2tp_ac_pppoe_kill_session(ps); + rcu_read_unlock(); + return NOTIFY_OK; + } + return NOTIFY_DONE; +} + +/****************************************************************************** + * Init and cleanup + */ + +static const struct l2tp_nl_cmd_ops l2tp_ac_pppoe_nl_cmd_ops = { + .session_create = l2tp_ac_pppoe_nl_create, + /* Our cleanup is handled via. the session_close callback, called by l2tp_session_delete */ + .session_delete = l2tp_session_delete, +}; + +static struct packet_type pppoes_ptype = { + .type = htons(ETH_P_PPP_SES), + .func = l2tp_ac_pppoe_recv_pppoe, +}; + +static struct notifier_block l2tp_ac_pppoe_notifier_block = { + .notifier_call = l2tp_ac_pppoe_netdevice_event, +}; + +static int __init l2tp_ac_pppoe_init(void) +{ + int err, hash; + + spin_lock_init(&pppoe_session_hlist_lock); + for (hash = 0; hash < L2TP_AC_PPPOE_SESSION_HASH_SIZE; hash++) + INIT_HLIST_HEAD(&pppoe_session_hlist[hash]); + + err = l2tp_nl_register_ops(L2TP_PWTYPE_PPP_AC, &l2tp_ac_pppoe_nl_cmd_ops); + if (err) + return err; + + err = register_netdevice_notifier(&l2tp_ac_pppoe_notifier_block); + if (err) { + l2tp_nl_unregister_ops(L2TP_PWTYPE_PPP_AC); + return err; + } + + dev_add_pack(&pppoes_ptype); + + pr_info("L2TP AC PPPoE support\n"); + + return err; +} + +static void __exit l2tp_ac_pppoe_exit(void) +{ + l2tp_nl_unregister_ops(L2TP_PWTYPE_PPP_AC); + unregister_netdevice_notifier(&l2tp_ac_pppoe_notifier_block); + dev_remove_pack(&pppoes_ptype); +} + +module_init(l2tp_ac_pppoe_init); +module_exit(l2tp_ac_pppoe_exit); + +MODULE_LICENSE("GPL"); +MODULE_AUTHOR("Tom Parkin <tpar...@katalix.com>"); +MODULE_DESCRIPTION("L2TP AC PPPoE driver"); +MODULE_VERSION("1.0"); +MODULE_ALIAS_L2TP_PWTYPE(8); -- 2.17.1