Repository navigation
send() and sendp() not working on PPPoE interface #4964
Description
Brief description
send() and sendp() sets invalid protocol when using AF_PACKET on PPPoE interfaces, causing packets to be dropped by PPP driver (skb->protocol = 0).
Scapy version
2.7.0.dev46
Python version
Python 3.13.5, using IPython 8.35.0
Operating system
Linux 6.12.74+deb13+1-amd64
Additional environment information
When using send(with ScopedIP) and sendp() on PPPoE (ppp0) interface, Scapy sends packets via AF_PACKET without correctly setting the L3 protocol (sll_protocol). As a result, packet_snd assigns skb->protocol = 0 and causes packets to be dropped by PPP driver that rely on skb->protocol (in this case, ppp_start_xmit).
How to reproduce
Create a PPPoE link interface. Make sure it's type 512.
cat /sys/class/net/ppp0/type
512Send an IPv6 ICMPv6 echo request by send() and sendp().
>>> pkt = IPv6(dst="ff02::1%ppp0")/ICMPv6EchoRequest()
>>> pkt.show()
###[ IPv6 ]###
version = 6
tc = 0
fl = 0
plen = None
nh = ICMPv6
hlim = 64
src = fe80::be24:11c6:ebd1:9865
dst = ScopedIP('ff02::1', scope='ppp0')
###[ ICMPv6 Echo Request ]###
type = Echo Request
code = 0
cksum = None
id = 0x0
seq = 0x0
data = b''
>>> sendp(pkt, iface="ppp0")
.
Sent 1 packets.
>>> send(pkt)
.
Sent 1 packets.
>>>
I also tried pkt = CookedLinux(proto=0x86DD) / IPv6(src="fe80::1", dst="ff02::2") / ICMPv6ND_RS() and pkt = PPP() / IPv6(src="fe80::1", dst="ff02::2") / ICMPv6ND_RS(), none of them worked.
Actual result
Look at tcpdump output, just nothing there.
~# tcpdump -n -i ppp0 'icmp6'
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on ppp0, link-type LINUX_SLL (Linux cooked v1), snapshot length 262144 bytes
Look at ifconfig ppp0, there 2 more dropped TX packets.
Before:
~# ifconfig ppp0
ppp0: flags=4305<UP,POINTOPOINT,RUNNING,NOARP,MULTICAST> mtu 1492
inet 100.64.10.201 netmask 255.255.255.255 destination 100.64.0.1
inet6 fe80::be24:11c6:ebd1:9865 prefixlen 128 scopeid 0x20<link>
ppp txqueuelen 3 (Point-to-Point Protocol)
RX packets 4752172 bytes 3692861302 (3.4 GiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 4389765 bytes 935034757 (891.7 MiB)
TX errors 0 dropped 388 overruns 0 carrier 0 collisions 0
After:
~# ifconfig ppp0
ppp0: flags=4305<UP,POINTOPOINT,RUNNING,NOARP,MULTICAST> mtu 1492
inet 100.64.10.201 netmask 255.255.255.255 destination 100.64.0.1
inet6 fe80::be24:11c6:ebd1:9865 prefixlen 128 scopeid 0x20<link>
ppp txqueuelen 3 (Point-to-Point Protocol)
RX packets 4752398 bytes 3692927445 (3.4 GiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 4390064 bytes 935094677 (891.7 MiB)
TX errors 0 dropped 390 overruns 0 carrier 0 collisions 0
Run dropwatch -l kas start + scapy while True:sendp(pkt, iface="ppp0") and it shows the following:
...
1 drops at tcp_validate_incoming+1a3 (0xffffffffa21125e3) [software]
1 drops at __netif_receive_skb_core.constprop.0+144 (0xffffffffa2034584) [software]
1 drops at __netif_receive_skb_core.constprop.0+144 (0xffffffffa2034584) [software]
1 drops at __init_scratch_end+1c80a73c (0xffffffffc100a73c) [software]
12 drops at __init_scratch_end+1c80c9b0 (0xffffffffc100c9b0) [software]
10 drops at __init_scratch_end+1c80c9b0 (0xffffffffc100c9b0) [software]
...
And trace it:
bpftrace -e 'tracepoint:skb:kfree_skb /args->location == 0xffffffffc100c9b0/ { print(kstack); }'
Attaching 1 probe...
sk_skb_reason_drop+148
ppp_start_xmit+112
dev_hard_start_xmit+100
sch_direct_xmit+159
__dev_queue_xmit+2540
packet_sendmsg+4285
__sys_sendto+479
__x64_sys_sendto+36
do_syscall_64+130
entry_SYSCALL_64_after_hwframe+118
sk_skb_reason_drop+148
ppp_start_xmit+112
dev_hard_start_xmit+100
sch_direct_xmit+159
__dev_queue_xmit+2540
packet_sendmsg+4285
__sys_sendto+479
__x64_sys_sendto+36
do_syscall_64+130
entry_SYSCALL_64_after_hwframe+118
It seems that ppp_start_xmit+112 is dropping the TX packet.
Trace skb->protocol:
# bpftrace -e '
tracepoint:skb:kfree_skb
{
printf("protocol=0x%x\n", args->protocol);
}'
protocol=0x800
protocol=0x800
protocol=0x800
protocol=0x800
protocol=0x800
protocol=0x800
protocol=0x0
protocol=0x0
protocol=0x800
protocol=0x0
protocol=0x0
protocol=0x800
protocol=0x0
There are lots of protocol=0x0 when I run scapy while True:sendp(pkt, iface="ppp0")
Packets sent via sendp() on ppp0:
- Are transmitted via
AF_PACKET - End up with
skb->protocol = 0 - Are dropped in kernel:
ppp_start_xmit()
-> ethertype_to_npindex(ntohs(skb->protocol)) -> -1
-> drop
No packets are visible on the wire.
Expected result
The ping request should at lease appear on tcpdump output, and the ifconfig TX drop counter should not change when I send()/sendp().
Expected something like this (with my simple AF_PACKET test program):
# tcpdump -n -i ppp0 'icmp6'
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on ppp0, link-type LINUX_SLL (Linux cooked v1), snapshot length 262144 bytes
13:14:02.186611 IP6 fe80::be24:11c6:ebd1:9865 > ff02::1: ICMP6, echo request, id 16494, seq 1, length 16
13:14:02.188890 IP6 fe80::[REDACTED] > fe80::be24:11c6:ebd1:9865: ICMP6, echo reply, id 16494, seq 1, length 16
^C
2 packets captured
2 packets received by filter
0 packets dropped by kernel
Related resources
packet_snd: https://elixir.bootlin.com/linux/v6.19.11/source/net/packet/af_packet.c#L2939
packet_snd is where skb->protocol assigned.
ppp_start_xmit: https://elixir.bootlin.com/linux/v6.19.11/source/drivers/net/ppp/ppp_generic.c#L1451
ppp_start_xmit is where packets are droped.
My test AF_PACKET program (written by GPT for test purpose only, compiles on debian 13 x86_64 GCC):
// ppp_ping_demo.c
#define _GNU_SOURCE
#include <arpa/inet.h>
#include <errno.h>
#include <linux/if_packet.h>
#include <net/if.h>
#include <netinet/icmp6.h>
#include <netinet/ip6.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <time.h>
#include <unistd.h>
#include <linux/if_ether.h>
static uint16_t csum16(const void *data, size_t len) {
const uint8_t *p = (const uint8_t *)data;
uint32_t sum = 0;
while (len > 1) {
sum += (p[0] << 8) | p[1];
p += 2;
len -= 2;
}
if (len) sum += p[0] << 8;
while (sum >> 16) sum = (sum & 0xFFFF) + (sum >> 16);
return (uint16_t)(~sum);
}
static uint16_t icmp6_checksum(const struct in6_addr *src,
const struct in6_addr *dst,
const void *icmp, size_t icmp_len) {
// Pseudo-header
struct {
struct in6_addr src;
struct in6_addr dst;
uint32_t len;
uint8_t zero[3];
uint8_t next;
} ph;
memset(&ph, 0, sizeof(ph));
ph.src = *src;
ph.dst = *dst;
ph.len = htonl((uint32_t)icmp_len);
ph.next = IPPROTO_ICMPV6;
// sum pseudo + icmp
uint32_t sum = 0;
sum += (~csum16(&ph, sizeof(ph)) & 0xFFFF);
sum += (~csum16(icmp, icmp_len) & 0xFFFF);
while (sum >> 16) sum = (sum & 0xFFFF) + (sum >> 16);
return (uint16_t)(~sum);
}
static void usage(const char *p) {
fprintf(stderr, "Usage: %s <ifname> <src-ll> <dst-addr>\n", p);
fprintf(stderr, "Example: %s ppp0 fe80::1 ff02::1\n", p);
}
int main(int argc, char **argv) {
if (argc != 4) {
usage(argv[0]);
return 1;
}
const char *ifname = argv[1];
const char *src_str = argv[2];
const char *dst_str = argv[3];
int ifindex = if_nametoindex(ifname);
if (!ifindex) {
perror("if_nametoindex");
return 1;
}
// fill in address (supports link local)
struct in6_addr src, dst;
if (inet_pton(AF_INET6, src_str, &src) != 1) {
fprintf(stderr, "invalid src\n");
return 1;
}
char dst_buf[256];
memset(dst_buf, 0, sizeof(dst_buf));
// for link-local, add %iface
if (strchr(dst_str, '%') == NULL &&
strncmp(dst_str, "fe80:", 5) == 0) {
snprintf(dst_buf, sizeof(dst_buf), "%s%%%s", dst_str, ifname);
} else {
snprintf(dst_buf, sizeof(dst_buf), "%s", dst_str);
}
struct sockaddr_in6 dst_in6;
memset(&dst_in6, 0, sizeof(dst_in6));
dst_in6.sin6_family = AF_INET6;
if (inet_pton(AF_INET6, dst_buf, &dst) != 1) {
// inet_pton not supports %scope
char tmp[256];
strncpy(tmp, dst_buf, sizeof(tmp)-1);
char *pct = strchr(tmp, '%');
if (pct) *pct = '\0';
if (inet_pton(AF_INET6, tmp, &dst) != 1) {
fprintf(stderr, "invalid dst\n");
return 1;
}
}
// PF_PACKET + SOCK_DGRAM (set skb->protocol here)
int fd = socket(AF_PACKET, SOCK_DGRAM, htons(ETH_P_IPV6));
if (fd < 0) {
perror("socket(AF_PACKET)");
return 1;
}
// set dest ll address
struct sockaddr_ll sll;
memset(&sll, 0, sizeof(sll));
sll.sll_family = AF_PACKET;
sll.sll_ifindex = ifindex;
sll.sll_protocol = htons(ETH_P_IPV6);
// PPP has no L2 addr, so no sll_addr
// construct IPv6 + ICMPv6 Echo Request
uint8_t buf[1500];
memset(buf, 0, sizeof(buf));
struct ip6_hdr *ip6 = (struct ip6_hdr *)buf;
struct icmp6_hdr *icmp = (struct icmp6_hdr *)(buf + sizeof(struct ip6_hdr));
const char payload[] = "ppp-demo";
size_t payload_len = sizeof(payload) - 1;
// IPv6 header
ip6->ip6_flow = htonl((6 << 28) | 0); // version + tc/flow
ip6->ip6_plen = htons(sizeof(struct icmp6_hdr) + payload_len);
ip6->ip6_nxt = IPPROTO_ICMPV6;
ip6->ip6_hlim = 255; // for ND/LL. hlim should be 255
ip6->ip6_src = src;
ip6->ip6_dst = dst;
// ICMPv6 Echo
icmp->icmp6_type = ICMP6_ECHO_REQUEST;
icmp->icmp6_code = 0;
icmp->icmp6_id = htons((uint16_t)getpid());
icmp->icmp6_seq = htons(1);
uint8_t *data = (uint8_t *)(icmp + 1);
memcpy(data, payload, payload_len);
// checksum
icmp->icmp6_cksum = 0;
icmp->icmp6_cksum = htons(icmp6_checksum(&src, &dst, icmp,
sizeof(struct icmp6_hdr) + payload_len));
size_t pkt_len = sizeof(struct ip6_hdr) + sizeof(struct icmp6_hdr) + payload_len;
// sendp
ssize_t n = sendto(fd, buf, pkt_len, 0,
(struct sockaddr *)&sll, sizeof(sll));
if (n < 0) {
perror("sendto");
close(fd);
return 1;
}
printf("sent %zd bytes on %s (proto=0x86dd)\n", n, ifname);
close(fd);
return 0;
}
Activity
cinit commentedon Apr 11, 2026
Here's a workaround.
socket.socket(socket.AF_PACKET, socket.SOCK_DGRAM)+s.bind((iface, socket.ETHERTYPE_IPV6))+s.send(raw(pkt[IPv6]))can be used to send IPv6 packets on PPP link.mohamedsolaiman commentedon May 1, 2026
I ran into a similar issue with PPPoE interfaces. The problem is that when Scapy sends via AF_PACKET on PPPoE, it does not set sll_protocol correctly for the PPP driver. A workaround that worked for me was to use conf.L2socket = L2ListenTcpdump and set the protocol field manually. Not a perfect fix but it gets packets through. Would love to see proper PPPoE support in Scapy!
Fix AF_PACKET protocol on raw L3 sends
Fix AF_PACKET protocol on raw L3 sends
DresslerFrank commentedon Oct 11, 2026
I have done some research on this issue, looking at the kernel source and debugging the kernel. See the description below. My private notes are more comprehensive. I am not describing everything below but I hope it is sufficient to help you fix the problem. If you want, I can actually work more on it and fix it. At least I want to share my findings here because they might actually help others fix the issue properly.
Quick workaround
Explicitly pass the protocol to
send(). This allows you to send packets via PPPoE and similar devices:send(IP()/..., type=ETH_P_IP)send(IPv6()/..., type=ETH_P_IPV6)Explicitly specifying the protocol helps because it makes Scapy create the socket with that type/protocol instead of
ETH_P_ALL. TheETH_P_ALLonly works with some netdev types, e.g. Ethernet ones, but not others, e.g. PPP ones. See below for details.Issue is more general
The issue is not PPPoE specific. It applies to all kinds of PPP netdevs and also to other kinds of netdevs such as GRE ones. In general, it applies to all netdevs that require you to pass the protocol to the kernel via e.g.
socket(),bind(), orsendto(). Details below.Importance of the Protocol in AF_PACKET, SOCK_RAW sockets
When you have a
(AF_PACKET, SOCK_RAW)socket, what you need to pass to the kernel depends on the kind of netdev. Independent of the netdev the kernel needs to know the protocol (same numbers as ethertype) of the packet you are sending in. The kernel takes that information from eithersocket(),bind(), orsendto(). If that protocol isETH_P_ALLor0, the kernel tries to help you out by taking a look at the packet but this depends on the specific netdev type.For Ethernet devices, the kernel expects you to pass
Ether()/...to the kernel. And because the Ether() contains the ether type, the kernel will just extract the protocol from the packet, if none was passed explicitly viasocket()/bind()/sendto().For PPP devices, the kernel expects you to pass
IP()/...,IPv6()/...to it, i.e. no header from which it could extract the protocol. Because of that, the kernel cannot help you out. So, if your socket was created withETH_P_ALLorbind()toETH_P_ALL, and you do not specify a protocol explicitly insendto(), the kernel has no way to know the protocol. It'll rewrite the protocol fromETH_P_ALL(3) to0. The PPP code, however, needs to know the protocol because the protocol ID must be translated to another kind of protocol ID and placed in the PPP header the kernel adds to the packet before sending it. Since0is not a valid protocol, the kernel rejects the packet. The reason for why the kenrel cannot help you detect the protocol automatically is that PPP can carry not just IP and IPv6, but also IPX and other protocols.For GRE devices, it is similar to PPP devices but a bit less restrictive. GRE devices also expect you to pass
IP()/...,IPv6()and because of that cannot extract the protocol ID from the packet. But for GRE, the protocol ID is simply copied 1:1 to a 16 bit field in the GRE header with no translation. This means, if you do not pass a protocol to the kernel, the kernel will turn theETH_P_ALLto0and, while that0is wrong, the kernel does not care and just puts this0into the GRE header. It should beETH_P_IPorETH_P_IPV6instead but the kernel does not care.0is a valid value for GRE. The kernel would not drop the packet, it'd be sent.For SIT devices, we have a case similar to PPP and GRE, where again you pass
IP()/...,IPV6()/...or whatever to the kernel, i.e. no protocol as part of what you pass to it. However, SIT is limited to carry IPv4 or IPv6. Because of that, the kernel can actually help you autoamtically detect the protocol. Both, IPv4 and IPv6, have the protocol version in the top nibble of their first byte. So what the kernel does is look at that nibble and if it is4, it has detected an IPv4 packet, and if it is6, it has detected anIPv6packet.Device Type Property Overview
Ether()/...IP()/...,IPV6()/..., ...ETH_P_IP,ETH_P_IPV6,ETH_P_IPX,ETH_P_PPPTALK,ETH_P_ATALK,ETH_P_MPLS_UC,ETH_P_MPLS_MCIP()/...,IPV6()/..., ...IP()/...,IPV6()/...The ones not working properly with Scapy are the ones that do not have automatic protocol detection. GRE seems to work because packets are sent and received, but they will be sent with the wrong protocol (
0).Scapy
Currently, usually, Scapy does not pass the protocol to the kernel explicitly but instead uses
ETH_P_ALLandsend(). Because of that, only netdevs for which the kernel supports automatic protocol detections work, i.e. regular Ethernet devices, but also SIT or Wireguard would work, for example. See table above.However, when you specify the protocol explicitly, i.e.
send(..., type=PROTOCOL), Scapy doesbind(..., PROTOCOL)which is why it then works: The kernel does not need to automatically detect the protocol and so it works for netdevs that do not support automatic protocol detection.Fixing the Issue
bind()ing the socket to the desired protocol would be one way of solving the issue. However, that'd restrict the socket a bit too much. It'd then be restricted to just that protocol, e.g. IPv4 or IPv6. You could not use one socket for multiple protocols anymore. Limiting a socket to a specific protocol would be unnecessarily restrictive and it is unnecessary as the following paragraph shows.What you can do instead is to use
sendto()instead ofsend().sendto()allows you to pass thestruct sockaddr_llto the kernel with the packet data. Thisstructcontains the desitnation address, the device to use, but also the protocol and some other fields. For the(AF_PACKET, SOCK_RAW)sockets Scapy uses, the kernel actually ignores all fields except for the interface and protocol. And Python already supports this by allowing you to pass just these two instead of forcing you to make up values for all the other fields. You can just do as.sendto(bytes(pkt), ("ppp0", ETH_P_IP))ors.sendto(bytes(pkt), ("ppp0", ETH_P_IPV6))instead of as.send()that Scapy currently does. To help the user, we can extract the protocol ID from thepkt.So you'd get the following change in code.
Before:
After:
This would basically move the protocol detection to Scapy. The kernel would not need to do it anymore. But, I wonder, if that might be slow and sometimes undesired. So maybe it should be restricted to when the socket
sockis bound toETH_P_ALLor0which is when the kernel would do the auto-detection and potentially further restricted to only those kinds of sockets which are bound to device types that do not support automatic protocol detection. The solution above is just one that probably always works for as long asethertype_from_pkt(pkt)works which requirespktto be aPacket, e.g.IP()/...or similar, i.e.it could not be abytes()thing already. In that situation, you could haveethertype_from_pktimplemented as follows:Since that function could fail and restricts the
pktto actualPackets, excludingbytes(), it might make sense to really restrict it to netdevs for which the kernel has no protocol auto detection. That'd then allow users to still passbytes(). Forsend()specifically, the user currently cannot passbytes(), so the given code would not be restrictive.Sample Patch
The following diff/patch shows a potential change that makes
send()work for PPP and similar devices that require you to pass down the protocol explicitly. It is still flawed because it would not work with the auto-fragmentation code just below. Use it only as an example to prove the concept. Bascially, allsock.send()s need to be replaced by a conditionalsock.sendto()where necessary (netdev does not support protocol auto detection and user did not explicitly specify a protocol) with a fallback tosock.send()where the kernel can detect the protocol explicitly.DresslerFrank commentedon Oct 11, 2026
Here are two helper scripts that allow you to create a test environment for the issue.
The first script creates two network name spaces,
scapy_L(L for left) andscapy_R(R for right) with different kinds of netdev types of the ones described above between them. Enter the network name spaces viaip netns exec scapy_L bashandip netns exec scapy_R bash, then use the devices, e.g. viasend(IP(dst="10.102.0.2")/...)fromscapy_Lwithtcpdumprunning inscapy_R.The second script injects packets in various ways to demonstrate the effects of
bind()vssendto()and the different netdev types (eth, ppp, gre, ...). Run it fromspectral_Llike./inject.py veth1,./inject.py gre1, ... and havetcpdump -lXnni gre1 udp,tcpdump -lXnni veth1 udp, ... running inspectral_R. Thetcpdumpoutput will prove which ones work.Of course, with that test env, you can test whether the approach in my shared patch above works. Do a
send(IP(src="10.103.0.1", dst="10.103.0.2")/UDP()/(128*b"x"))from the scapy CLI running inspectral_Lwith and without the patch. With the patch it'll work. Without it it wont. But if you addtype=ETH_P_IPto thesend(...)it'll work even without the patch.SetupTest Env
Run as root, then enter the name spaces via
ip netns exec scapy_L bashandip netns exec scapy_R bashInject packets
Run from
scapy_Lnetns via./inject.py veth1,./inject.py gre1, ... and runtcpdumpfromscapy_Rwhile doing so.gpotter2 commentedon Oct 12, 2026
@DresslerFrank I encourage you to read the CONTRIBUTING.md guidelines. You may NOT comment using AI on the Scapy repo. Comments are for talking to humans, please don't be rude.
DresslerFrank commentedon Oct 12, 2026
[Conslidated my previous comments into one.]
@gpotter2 I did not use AI. I have done that research myself. Let me share my notes (research_notes.md). It is really not nice of you to just accuse me of it. I have spent many hours, over 2 days doing the research.
I also started working on solving the problem. But with your accusation I am now really demotivated. Here is the test I have written so far (scapy_gre_test.py) that I was still working on, to test that GRE works. Because if that works, PPP also works.
What makes you believe that I created my comments using AI? I can share my screen and show you how I kernel debugged it. AI won't be able to do that. Let me know where you want me to share my screen. Would you take back the accusation then?
You can contact me on LinkedIn (https://www.linkedin.com/in/frank-dressler-5a0b391a2) or via email frank@dressler.pro I'll find a way to share my screen then and prove to you I actually did the research.
I have sent you an email using the email address I found in the source code of Scapy. Jitsi seems to be a working way I can use to share my screen.
gpotter2 commentedon Oct 12, 2026
Hi @DresslerFrank, apologies if I misattributed your comments to AI. We almost never get answers this long and with heavy formatting that aren't AI-slop. (and trust me right now we're flooded with it..)
Thank you, of course, for contributing to the understanding of this issue and sorry for the confusion