@@ -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.
@@ -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
new file mode 100644
@@ -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 <tparkin@katalix.com>");
+MODULE_DESCRIPTION("L2TP AC PPPoE driver");
+MODULE_VERSION("1.0");
+MODULE_ALIAS_L2TP_PWTYPE(8);
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 <tparkin@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