DEV Community

Cover image for Building a High-Performance eBPF/XDP Policy Engine in Rust
Alex Root
Alex Root

Posted on

Building a High-Performance eBPF/XDP Policy Engine in Rust

A Bit of Background

The origins of eBPF (extended Berkeley Packet Filter) can be traced back to the original Berkeley Packet Filter (BPF), which was introduced in the early 1990s as a mechanism for filtering network packets. Over time, BPF evolved from a packet filtering framework into a general-purpose execution environment within the Linux kernel, ultimately becoming what we now know as eBPF.
Today, eBPF is far more than a networking technology. It is widely used for observability, performance monitoring, security enforcement, networking, load balancing, and many other kernel-level tasks. Instead of requiring developers to write and maintain custom kernel modules, eBPF allows sandboxed programs to run safely inside the kernel.
Before an eBPF program can be loaded, it must pass the kernel verifier. The verifier performs a static analysis to ensure that the program always terminates, accesses memory safely, and complies with the kernel's security constraints. This verification process enables eBPF to extend kernel functionality without compromising system stability or security.
One of eBPF's greatest strengths is its flexibility. Programs can be attached to multiple execution points—known as hooks—throughout the kernel. Each hook exposes different information about the packet or system state and is designed for a particular class of workloads. Choosing the right hook depends on the required performance, the amount of available context, and the functionality that needs to be implemented.
To better understand where these hooks fit into the networking stack, let's first look at the path a network packet takes through the Linux kernel.
Linux network packet processing path
The diagram above shows the packet processing path between the network interface and the Linux networking stack. XDP operates exclusively on the receive (RX) path, where packets are processed before they reach the kernel networking stack. TC (Traffic Control), on the other hand, provides both ingress and egress hook points, making it possible to process traffic in both directions.
The earliest packet processing point is XDP (eXpress Data Path). An XDP program runs between the network device driver and the Linux networking stack, before the kernel allocates the sk_buff (socket buffer) structure. sk_buff is the primary kernel data structure used to represent a network packet as it moves through the Linux networking stack.
Because XDP operates at such an early stage, it provides extremely low processing latency and is well suited for high-performance packet filtering, ACLs, DDoS protection, and packet redirection.
Once an sk_buff has been created, the packet enters the Linux networking stack, where eBPF programs can be attached through the Traffic Control (TC) subsystem. Unlike XDP, TC operates on the sk_buff, giving the program access to additional packet context that is not available at the XDP layer. TC is also closely integrated with routing, QoS, conntrack, tunneling, and other kernel networking subsystems.
This makes TC a convenient layer for implementing NAT, load balancing, packet modification, and more complex network processing.
XDP and TC are not the only eBPF attachment points. Cgroup hooks can be used to enforce policies for processes and containers, while socket filters can be used to inspect traffic associated with individual sockets. Tracepoints, kprobes, and uprobes are widely used for tracing and performance monitoring.
Although eBPF provides a wide range of attachment points, they are not interchangeable. The earlier a program runs in the networking stack, the lower the processing overhead, but the less context is available to it. As the packet moves further through the kernel, more context and functionality become available, but processing becomes more expensive.
Therefore, choosing an appropriate hook is fundamentally a trade-off between performance and functionality.
In Pulsar, XDP was chosen as the primary dataplane because it allows packet handling decisions to be made as early as possible, before the packet passes through most of the Linux networking stack. This makes XDP particularly well suited for high-performance ACLs and other early packet filtering mechanisms.
At the same time, the architecture is not limited to a single processing layer. Features that require richer packet context or deeper integration with kernel networking subsystems are planned to be implemented at the TC layer.

The Main Part

Over the years, the Linux networking stack has evolved to include a wide range of mechanisms for managing network policy. In addition to iptables and nftables, there are tools and systems such as firewalld, ufw, Docker, Kubernetes, and others that provide their own policy management interfaces or automatically modify existing filtering and routing rules.
As a result, administrators have to deal not only with the network policy itself, but also with the way that policy is implemented by a particular system. The same policy can be expressed in very different ways depending on the tool being used.
When moving between different platforms or introducing additional components such as Docker or Kubernetes, the changes go beyond policy syntax. The points at which policies are applied, the order in which traffic is processed, and the interactions between different networking mechanisms can all change.
A similar situation exists outside Linux as well. Many BSD systems and network devices use their own policy models and configuration approaches.
For example, consider the following simple filtering policy:

Deny TCP traffic from 192.168.0.3 to 192.168.0.202:80.

The same policy can look completely different depending on the system being used.

With iptables:

iptables -A INPUT -s 192.168.0.3 -d 192.168.0.202 -p tcp --dport 80 -j DROP    
Enter fullscreen mode Exit fullscreen mode

With nftables:

ip saddr 192.168.0.3 ip daddr 192.168.0.202 tcp dport 80 drop.   
Enter fullscreen mode Exit fullscreen mode

With pf:

block in proto tcp from 192.168.0.3 to 192.168.0.202 port 80.    
Enter fullscreen mode Exit fullscreen mode

All of the examples above describe the same network policy, but its representation depends on the underlying enforcement mechanism.
In the dataplane I am developing, the same policy is expressed through a single, unified interface:

./ebpf-ctl acl add drop  src 192.168.0.3:any  dst 192.168.0.202:80  proto=tcp 
Enter fullscreen mode Exit fullscreen mode

Or configuration file:

acl:
  drop:
    - src:
        addresses:
          - 192.168.0.3
        dst:
          addresses:
            - 192.168.0.202
        ports:
          number: 80
          proto: tcp
Enter fullscreen mode Exit fullscreen mode

The goal is not to introduce yet another policy syntax, but to abstract policy definition away from its enforcement mechanism.

The user works with a single declarative network policy model that is independent of the underlying enforcement mechanism. The control plane translates this policy definition into an internal representation consumed by the dataplane, while the CLI and YAML are simply different ways of expressing the same policy.

Pulsar Architecture
The diagram below illustrates the lifecycle of a policy, from its definition to its enforcement.<br>
The system follows the classic control plane / dataplane architecture: the control plane is responsible for building and distributing policy state, while the dataplane is responsible for processing individual packets.<br>

The diagram below illustrates the lifecycle of a policy, from its definition to its enforcement.
The system follows the classic control plane / dataplane architecture: the control plane is responsible for building and distributing policy state, while the dataplane is responsible for processing individual packets.
Control Plane and Dataplane
The primary mechanism for communication between the control plane and dataplane is BPF maps. The control plane uses them to pass prepared policy state to the dataplane: the control plane writes the policy state to the maps, while the dataplane reads it during packet processing.
Management Interfaces
Policy definitions enter the system through either a declarative configuration file (config.yml) or a command-line client. Both interfaces are intentionally kept thin: they pass the policy definition to the daemon and contain no policy interpretation logic of their own.
Daemon
The daemon is the core of the control plane. It converts the incoming policy definition into an internal policy model, performs validation and normalization, and then compiles the policy into a representation suitable for the dataplane.
At the final stage, the resulting structures are written to BPF maps.
Dataplane
The XDP program attached to the network interface accesses the BPF maps for every packet and makes the enforcement decision: whether to allow or drop the packet. It does not communicate directly with the control plane.
The configuration flow shown in the diagram goes from top to bottom. At runtime, however, data flows differently: the XDP program reads the policy state directly from the BPF maps.
Unified Policy Semantics
The same policy should have a consistent semantic representation regardless of whether it comes from the CLI, YAML, or another management interface.
To achieve this, all semantic processing—parsing, validation, normalization, and compilation—is centralized in the daemon, while management interfaces remain simple adapters to the internal policy model.
This separation allows management interfaces to evolve independently of the dataplane and makes it possible to introduce additional enforcement mechanisms without changing the underlying policy model.
BPF Maps as the Contract Between the Planes
In this architecture, BPF maps are more than just data structures. They serve as a contract between the control plane and dataplane.
The dataplane does not know where the policy came from, and the control plane does not need to know exactly how a packet will be processed. As long as both sides adhere to the same contract, the enforcement layer can be replaced—for example, with a TC/eBPF implementation—while the management interfaces can be extended without affecting the other plane.
This also provides an important operational property: a failure of the control plane does not affect the dataplane. If the daemon terminates, the XDP program continues to enforce the last policy state loaded into the BPF maps. Packet filtering therefore continues to operate even when the control plane is unavailable.
Current Capabilities
At the moment, Pulsar implements L3/L4 ACLs, IPv4 and IPv6 filtering, VLAN/QinQ ACLs, and rate limiting. Policy management is handled by a userspace control plane that supports declarative policy definitions.
Regardless of the management interface used, the dataplane receives the same internal representation of the policy.
Event Handling
Special attention is also given to event handling. The eBPF dataplane can send selected events to userspace through a Ring Buffer, where they are converted into loggable events.
The set of events to be logged is configurable, allowing the event generation mechanism to remain independent from how those events are ultimately represented or stored.
What Makes Pulsar Different
The main distinction of Pulsar is not the number of networking features it currently implements, but the architectural separation between the control plane and dataplane.
The control plane is responsible for defining, validating, and transforming policy, while the dataplane operates on the resulting prepared state to process packets.

The diagram shows the main flows between the control plane, data plane and event processing system.

Control Plane and Dataplane in Practice
The control plane runs in userspace. The CLI client sends commands to the daemon over a Unix socket, and the daemon writes the resulting rules to BPF maps: IP ACL for address-based filtering, VLAN ACL, and Rate Limit for controlling traffic intensity.
Because the policy state is stored in BPF maps, rules can be updated at runtime without reloading the XDP program.
The daemon also controls event logging through the Select Event configuration map. It specifies which events should be logged, and the XDP module sends only the selected events to the Ring Buffer, from which the daemon reads and processes them.

Dataplane
The dataplane operates directly on the driver's receive path. A packet arriving from the NIC reaches XDP before sk_buff is allocated and before the packet enters the Linux networking stack.
The XDP program matches the packet against the rules stored in the BPF maps and makes the corresponding decision.
If the packet is allowed, it continues through the normal Linux receive path and is processed by the networking stack. Depending on the configuration, it may subsequently pass through additional processing points, including TC ingress.
If the packet is denied, it is dropped as early as possible. This early-drop capability reduces the amount of work performed by subsequent layers of the networking stack, which is particularly important under high incoming traffic rates.
Traffic Processing in XDP
At the current stage, the main packet-processing logic in Pulsar is implemented in the XDP module.
For each packet, the XDP program accesses the BPF maps prepared by the control plane and uses the stored policy state to perform packet classification and enforcement.
Three basic mechanisms are currently implemented:

  • IP ACL
  • VLAN ACL
  • Rate Limiting

IP ACL
The IP ACL is responsible for filtering traffic based on IPv4/IPv6 and L4 parameters. It implements the basic allow/deny policies between sources and destinations.
When processing a packet, the XDP program extracts the required fields and constructs a lookup key for the ACL map. Depending on the policy, these fields may include:

  • source IP address;
  • destination IP address;
  • transport protocol, such as TCP or UDP;
  • source port;
  • destination port.

Based on the matching rule, the dataplane can allow the packet or drop it. If logging is enabled for that rule, the drop event is also sent through the Ring Buffer.
IPv4 and IPv6 are handled through a unified policy model. The control plane converts this model into the corresponding BPF map entries, so the dataplane does not need to parse configuration files or interpret policy definitions. It operates directly on the normalized policy state.
Importantly, the IP ACL in Pulsar is stateless. Each packet is evaluated based on its current fields and the loaded policy rules, without requiring connection-state tracking. This keeps packet processing lightweight and well suited to the XDP layer.
VLAN ACL
The VLAN ACL is designed to filter traffic based on 802.1Q and QinQ tags. Unlike the IP ACL, it operates on VLAN tags present in the Ethernet frame rather than IP addresses and transport ports.
This type of filtering is useful for several practical scenarios.
Network segment isolation. If an interface receives traffic from multiple VLANs, unwanted VLANs can be rejected at the host before the packets enter the Linux networking stack.
Protection against L2 configuration errors. For example, if a switch is misconfigured and traffic from one segment unexpectedly appears on another, a VLAN ACL can drop that traffic at an early stage.
Reducing the risk of VLAN hopping and handling malformed QinQ encapsulation. Unexpected or explicitly forbidden VLAN tags can be rejected before the packet is passed further into the Linux networking stack.
Unified declarative policy. In a traditional Linux environment, L2 and L3 filtering is often handled by different tools with different policy semantics. In Pulsar, VLAN policies are described using the same policy model as IP policies and loaded into the dataplane through the same control plane.
VLAN ACLs are particularly useful on bare-metal servers, hypervisors, edge nodes, and other infrastructure where a single physical or logical interface may receive traffic from multiple isolated segments.
There is, however, an important implementation detail to consider: network drivers can handle VLAN tags differently. In some configurations, the NIC may strip the VLAN tag in hardware and expose the VLAN information through packet metadata instead. Therefore, a correct VLAN ACL implementation needs to account for both VLAN tags present directly in the packet and VLAN information exposed through the XDP context.
Rate Limiting
Rate limiting in Pulsar is designed to control the amount of traffic allowed over a given period of time.
Each rate-limiting rule has its own state stored in a BPF map. This state contains the information required to calculate the current limit, such as the number of available tokens and the timestamp of the last update.
Comparison with Existing Solutions
There are several different approaches to implementing network filtering and programmable dataplanes.
Traditional solutions include iptables and nftables, while among modern eBPF-based platforms, Cilium and Calico are some of the closest architectural examples. There are also specialized eBPF dataplanes such as Katran, which focuses primarily on high-performance L4 load balancing.
Traditional Linux firewalls such as iptables and nftables are deeply integrated with the Linux networking stack and use the Netfilter subsystem to process packets. Configuration is performed through userspace tools that translate the specified rules into Netfilter state.
This approach is mature and well integrated into Linux, but the policy model and enforcement mechanism are closely tied to the particular firewall implementation.
Cilium is perhaps the closest example of using eBPF to build a programmable network dataplane. Cilium uses eBPF at different stages of packet processing, including XDP and TC, depending on the feature and configuration.
In addition to network policy, Cilium provides NAT, load balancing, routing, service networking, and many other capabilities. Therefore, in terms of the number of implemented features, Pulsar is currently far behind Cilium.
Calico provides another example of an eBPF-based dataplane. Its control-plane components translate network policies into state consumed by the dataplane. eBPF is used for filtering, routing, and other networking functions.
Like Cilium, Calico is a significantly more mature platform and is primarily focused on cloud-native and Kubernetes environments.
Katran is another interesting example. Unlike Cilium and Calico, it is not a general-purpose network security platform. Instead, it is a high-performance L4 load-balancer dataplane built on eBPF/XDP.
Its architecture demonstrates the performance benefits of moving packet processing to the earliest possible stages of the packet path. However, Katran's primary goal is fundamentally different from that of Pulsar.
Against this background, Pulsar is still at an early stage of development and does not attempt to compete with existing platforms in terms of feature count.
The current implementation focuses on building a programmable security dataplane in which security policy is separated from the underlying enforcement mechanism.
The key architectural idea is that policy is defined independently of the dataplane, transformed by the control plane into a formalized state, and then enforced directly by an eBPF program.
This separation allows the semantics of the policy and the packet-processing mechanism to evolve independently.
Test Environment
The test environment consisted of two single-board computers, an Orange Pi RV2 and an Orange Pi R2S, along with a MikroTik router.

Both single-board computers use a SoC from the SpacemiT K1 (Ky X1) family and the RISC-V 64-bit architecture.
The test setup consisted of:

  1. Orange Pi RV2 (192.168.0.202) — the test machine running Pulsar.
  2. Orange Pi R2S (192.168.0.3) — the test machine without Pulsar.

  3. Verify That No Rules Are Currently Configured

root@orangepirv2:/home/orangepi/ebpf# ./target/debug/ebpf-ctl acl list
(l4_acl map is empty)
root@orangepirv2:/home/orangepi/ebpf#
Enter fullscreen mode Exit fullscreen mode
  1. Make sure that logging has been disabled:
cat ./test.log 
2026-08-13T17:15:32.884095Z  WARN ebpf_daemon: Failed to load initial config config2.yml: io error: No such file or directory (os error 2)
2026-08-13T17:15:32.884379Z  WARN ebpf_daemon: Starting without configuration. Use 'ebpf-ctl reload <config>' to load later.
2026-08-13T17:15:32.995098Z  INFO ebpf_daemon::monitoring: Trying XDP program id: 237
2026-08-13T17:15:33.049185Z  INFO ebpf_daemon::monitoring: bpftool prog show id 237: 237: xdp  name xdp_dataplane  tag fa484001870657d5
    loaded_at 2026-08-12T07:52:31+0300  uid 0
    xlated 19776B  jited 8972B  memlock 20480B  map_ids 470,471,472,474,480,479
    btf_id 351
2026-08-13T17:15:33.103932Z  WARN ebpf_daemon::monitoring: Skipping map id 470: Map id 470 is not events ringbuf (name=counters, type=percpu_array)
2026-08-13T17:15:33.158030Z  WARN ebpf_daemon::monitoring: Skipping map id 471: Map id 471 is not events ringbuf (name=event_mask, type=array)
2026-08-13T17:15:33.212209Z  INFO ebpf_daemon::monitoring: Opened events map
2026-08-13T17:15:33.212382Z  INFO ebpf_daemon::monitoring: Events ringbuf size: 16777216 bytes
2026-08-13T17:15:33.219468Z  INFO ebpf_daemon::monitoring: Starting ringbuf event loop
root@orangepirv2:/home/orangepi/ebpf# 
Enter fullscreen mode Exit fullscreen mode
./target/debug/ebpf-ctl event-log list
event-log mask: 0x0
enabled events:
  [ ] PacketDrop
  [ ] RateLimited
  [ ] ConntrackMiss
  [ ] BackendSelected
  [ ] SlowPath
  [ ] ServiceMatched
  [ ] VlanDetected
  [ ] PacketAllow
Enter fullscreen mode Exit fullscreen mode
./target/debug/ebpf-ctl event-log enable  PacketDrop
enabled event-log: PacketDrop
root@orangepirv2:/home/orangepi/ebpf# ./target/debug/ebpf-ctl event-log list
event-log mask: 0x2
enabled events:
  [x] PacketDrop
  [ ] RateLimited
  [ ] ConntrackMiss
  [ ] BackendSelected
  [ ] SlowPath
  [ ] ServiceMatched
  [ ] VlanDetected
  [ ] PacketAllow
Enter fullscreen mode Exit fullscreen mode
  1. Check the Deny IP ACL rules ICMP:
root@orangepirv2:/home/orangepi/ebpf# ./target/debug/ebpf-ctl acl add drop src 192.168.0.0/24:any dst 192.168.0.202:any proto icmp
ACL rule added
added rule Drop: icmp 192.168.0.0:any -> 192.168.0.202:any (prefix_len=304)
root@orangepirv2:~sudo tcpdump -ni end0 icmp
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on end0, link-type EN10MB (Ethernet), snapshot length 262144 bytes
^C
0 packets captured
0 packets received by filter
0 packets dropped by kernel
Enter fullscreen mode Exit fullscreen mode

read log file:

root@orangepirv2:/home/orangepi/ebpf# cat ./test.log 
…
2026-08-13T17:21:35.257433Z  INFO ebpf_daemon::monitoring: eBPF event timestamp_ns=545048648274746 event_kind=PacketDrop ifindex=2 src=192.168.0.3 dst=192.168.0.202 src_port=0 dst_port=0 protocol=ICMP aux0=0 aux1=0
2026-08-13T17:21:36.285353Z  INFO ebpf_daemon::monitoring: eBPF event timestamp_ns=545049676208365 event_kind=PacketDrop ifindex=2 src=192.168.0.3 dst=192.168.0.202 src_port=0 dst_port=0 protocol=ICMP aux0=0 aux1=0
2026-08-13T17:21:37.313289Z  INFO ebpf_daemon::monitoring: eBPF event timestamp_ns=545050704135651 event_kind=PacketDrop ifindex=2 src=192.168.0.3 dst=192.168.0.202 src_port=0 dst_port=0 protocol=ICMP aux0=0 aux1=0
2026-08-13T17:21:38.333321Z  INFO ebpf_daemon::monitoring: eBPF event timestamp_ns=545051724172595 event_kind=PacketDrop ifindex=2 src=192.168.0.3 dst=192.168.0.202 src_port=0 dst_port=0 protocol=ICMP aux0=0 aux1=0
2026-08-13T17:21:39.357436Z  INFO ebpf_daemon::monitoring: eBPF event timestamp_ns=545052748286770 event_kind=PacketDrop ifindex=2 src=192.168.0.3 dst=192.168.0.202 src_port=0 dst_port=0 protocol=ICMP aux0=0 aux1=0
2026-08-13T17:21:40.381334Z  INFO ebpf_daemon::monitoring: eBPF event timestamp_ns=545053772177407 event_kind=PacketDrop ifindex=2 src=192.168.0.3 dst=192.168.0.202 src_port=0 dst_port=0 protocol=ICMP aux0=0 aux1=0
2026-08-13T17:21:41.405289Z  INFO ebpf_daemon::monitoring: eBPF event timestamp_ns=545054796140251 event_kind=PacketDrop ifindex=2 src=192.168.0.3 dst=192.168.0.202 src_port=0 dst_port=0 protocol=ICMP aux0=0 aux1=0
2026-08-13T17:21:42.429270Z  INFO ebpf_daemon::monitoring: eBPF event timestamp_ns=545055820124679 event_kind=PacketDrop ifindex=2 src=192.168.0.3 dst=192.168.0.202 src_port=0 dst_port=0 protocol=ICMP aux0=0 aux1=0
2026-08-13T17:21:43.453263Z  INFO ebpf_daemon::monitoring: eBPF event timestamp_ns=545056844116064 event_kind=PacketDrop ifindex=2 src=192.168.0.3 dst=192.168.0.202 src_port=0 dst_port=0 protocol=ICMP aux0=0 aux1=0
Enter fullscreen mode Exit fullscreen mode

On the Orange pi R2S itself:

$ ping 192.168.0.202
PING 192.168.0.202 (192.168.0.202) 56(84) bytes of data.
From 192.168.0.1: icmp_seq=2 Redirect Host(New nexthop: 192.168.0.202)
^C
--- 192.168.0.202 ping statistics ---
9 packets transmitted, 0 received, 100% packet loss, time 8196ms
Enter fullscreen mode Exit fullscreen mode

Check TCP using iperf3 (for example, port 80):
On OrangePi RV2:

root@orangepirv2:/home/orangepi/ebpf# ./target/debug/ebpf-ctl acl add drop src 192.168.0.0/24:any dst 192.168.0.202:80 proto tcp
ACL rule added

./target/debug/ebpf-ctl acl list
id  plen  fam  src                                dst                                proto sport  dport  rate  action
0   304   4    192.168.0.0                        192.168.0.202                      icmp any    any    -     Drop
1   320   4    192.168.0.0                        192.168.0.202                      tcp  any    80     -     Drop

root@orangepirv2:~# iperf3 -s -p 80
-----------------------------------------------------------
Server listening on 80 (test #1)
-----------------------------------------------------------
^Ciperf3: interrupt - the server has terminated
Enter fullscreen mode Exit fullscreen mode

root@orangepirv2:/home/orangepi/ebpf# cat ./test.log 
…
src=192.168.0.3 dst=192.168.0.202 src_port=47284 dst_port=80 protocol=TCP aux0=0 aux1=0
2026-08-13T17:36:23.293385Z  INFO ebpf_daemon::monitoring: eBPF event timestamp_ns=545936684167882 event_kind=PacketDrop ifindex=2 src=192.168.0.3 dst=192.168.0.202 src_port=47284 dst_port=80 protocol=TCP aux0=0 aux1=0
2026-08-13T17:36:24.317258Z  INFO ebpf_daemon::monitoring: eBPF event timestamp_ns=545937708103844 event_kind=PacketDrop ifindex=2 src=192.168.0.3 dst=192.168.0.202 src_port=47284 dst_port=80 protocol=TCP aux0=0 aux1=0
2026-08-13T17:36:25.341274Z  INFO ebpf_daemon::monitoring: eBPF event timestamp_ns=545938732107054 event_kind=PacketDrop ifindex=2 src=192.168.0.3 dst=192.168.0.202 src_port=47284 dst_port=80 protocol=TCP aux0=0 aux1=0
2026-08-13T17:36:26.365267Z  INFO ebpf_daemon::monitoring: eBPF event timestamp_ns=545939756097723 event_kind=PacketDrop ifindex=2 src=192.168.0.3 dst=192.168.0.202 src_port=47284 dst_port=80 protocol=TCP aux0=0 aux1=0
2026-08-13T17:36:27.389307Z  INFO ebpf_daemon::monitoring: eBPF event timestamp_ns=545940780144350 event_kind=PacketDrop ifindex=2 src=192.168.0.3 dst=192.168.0.202 src_port=47284 dst_port=80 protocol=TCP aux0=0 aux1=0
2026-08-13T17:36:29.405287Z  INFO ebpf_daemon::monitoring: eBPF event timestamp_ns=545942796125120 event_kind=PacketDrop ifindex=2 src=192.168.0.3 dst=192.168.0.202 src_port=47284 dst_port=80 protocol=TCP aux0=0 aux1=0
root@orangepirv2:/home/orangepi/ebpf# 
Enter fullscreen mode Exit fullscreen mode

Naturally, Orange Pi r2s:

root@orangepir2s:/home/orangepi# iperf3 -c 192.168.0.202 -p80
^C- - - - - - - - - - - - - - - - - - - - - - - - -
[ ID] Interval           Transfer     Bitrate         Retr
iperf3: interrupt - the client has terminated
Enter fullscreen mode Exit fullscreen mode

UDP:

root@orangepirv2:/home/orangepi/ebpf# ./target/debug/ebpf-ctl acl add drop src 192.168.0.0/24:any dst 192.168.0.202:80 proto udp
ACL rule added
added rule Drop: udp 192.168.0.0:any -> 192.168.0.202:80 (prefix_len=320)
root@orangepirv2:~# iperf3 -s -p 80 
-----------------------------------------------------------
Server listening on 80 (test #1)
-----------------------------------------------------------
^Ciperf3: interrupt - the server has terminated
Enter fullscreen mode Exit fullscreen mode
root@orangepirv2:/home/orangepi/ebpf# cat ./test.log 
…
2026-08-13T17:54:34.709969Z  WARN ebpf_daemon::monitoring: Skipping map id 470: Map id 470 is not events ringbuf (name=counters, type=percpu_array)
2026-08-13T17:54:34.763748Z  WARN ebpf_daemon::monitoring: Skipping map id 471: Map id 471 is not events ringbuf (name=event_mask, type=array)
2026-08-13T17:54:34.817658Z  INFO ebpf_daemon::monitoring: Opened events map
2026-08-13T17:54:34.817834Z  INFO ebpf_daemon::monitoring: Events ringbuf size: 16777216 bytes
2026-08-13T17:54:34.825118Z  INFO ebpf_daemon::monitoring: Starting ringbuf event loop
2026-08-13T17:54:50.010419Z  INFO ebpf_daemon::monitoring: eBPF event timestamp_ns=547043401254143 event_kind=PacketDrop ifindex=2 src=192.168.0.3 dst=192.168.0.202 src_port=59158 dst_port=80 protocol=UDP aux0=0 aux1=0
root@orangepirv2:/home/orangepi/ebpf# 
Enter fullscreen mode Exit fullscreen mode

OrangePI R2S:

iperf3 -c 192.168.0.202 -p80 -u
Connecting to host 192.168.0.202, port 80
iperf3: error - unable to read from stream socket: Resource temporarily unavailable
root@orangepir2s:/home/orangepi# 
Enter fullscreen mode Exit fullscreen mode
  1. Check the Allow IP ACL. Here we create an allow rule, but for a specific host (with or without a rate limit): Enable logging:
root@orangepirv2:/home/orangepi/ebpf# ./target/debug/ebpf-ctl event-log enable PacketAllow
enabled event-log: PacketAllow
root@orangepirv2:/home/orangepi/ebpf# ./target/debug/ebpf-ctl event-log list
event-log mask: 0x102
enabled events:
  [x] PacketDrop
  [ ] RateLimited
  [ ] ConntrackMiss
  [ ] BackendSelected
  [ ] SlowPath
  [ ] ServiceMatched
  [ ] VlanDetected
  [x] PacketAllow
Enter fullscreen mode Exit fullscreen mode

UDP traffic was not tested separately with iperf3 for the allow scenario because an iperf3 UDP test requires an additional TCP connection to control the session.

However, UDP processing at the ACL level was directly verified using a Drop rule. The eBPF logs recorded a packet with protocol=UDP and dst_port=80, after which the packet was dropped.

./target/debug/ebpf-ctl acl add allow src 192.168.0.3:any dst 192.168.0.202:80 proto tcp
ACL rule added
added rule Allow: tcp 192.168.0.3:any -> 192.168.0.202:80 (prefix_len=320)
root@orangepirv2:/home/orangepi/ebpf# ./target/debug/ebpf-ctl acl list
id  plen  fam  src                                dst                                proto sport  dport  rate  action
0   304   4    192.168.0.0                        192.168.0.202                      icmp any    any    -     Drop
1   320   4    192.168.0.0                        192.168.0.202                      tcp  any    80     -     Drop
2   320   4    192.168.0.0                        192.168.0.202                      udp  any    80     -     Drop
3   320   4    192.168.0.3                        192.168.0.202                      tcp  any    80     -     Allow
Enter fullscreen mode Exit fullscreen mode
root@orangepirv2:/home/orangepi/ebpf# iperf3 -s -p 80  
-----------------------------------------------------------
Server listening on 80 (test #1)
-----------------------------------------------------------
Accepted connection from 192.168.0.3, port 48720
[  5] local 192.168.0.202 port 80 connected to 192.168.0.3 port 48730
[ ID] Interval           Transfer     Bitrate
[  5]   0.00-1.00   sec  58.0 MBytes   486 Mbits/sec                  
[  5]   1.00-2.00   sec  59.9 MBytes   502 Mbits/sec                  
[  5]   2.00-3.00   sec  60.1 MBytes   504 Mbits/sec                  
[  5]   3.00-4.00   sec  59.5 MBytes   499 Mbits/sec                  
[  5]   4.00-5.00   sec  58.6 MBytes   492 Mbits/sec                  
[  5]   5.00-6.00   sec  58.5 MBytes   491 Mbits/sec                  
[  5]   6.00-7.00   sec  59.2 MBytes   497 Mbits/sec                  
[  5]   7.00-8.00   sec  60.1 MBytes   504 Mbits/sec                  
[  5]   8.00-9.00   sec  60.2 MBytes   505 Mbits/sec                  
[  5]   9.00-10.00  sec  59.4 MBytes   498 Mbits/sec                  
[  5]  10.00-10.02  sec  1.25 MBytes   510 Mbits/sec                  
- - - - - - - - - - - - - - - - - - - - - - - - -
[ ID] Interval           Transfer     Bitrate
[  5]   0.00-10.02  sec   595 MBytes   498 Mbits/sec                  receiver
-----------------------------------------------------------
Server listening on 80 (test #2)
-----------------------------------------------------------
^Ciperf3: interrupt - the server has terminated
Enter fullscreen mode Exit fullscreen mode
root@orangepirv2:/home/orangepi/ebpf# cat ./test.log | grep "192.168.0.3"
…
2026-08-13T18:24:00.442171Z  INFO ebpf_daemon::monitoring: eBPF event timestamp_ns=548786152157138 event_kind=PacketAllow ifindex=2 src=192.168.0.3 dst=192.168.0.202 src_port=32988 dst_port=80 protocol=TCP aux0=0 aux1=0
2026-08-13T18:24:00.442250Z  INFO ebpf_daemon::monitoring: eBPF event timestamp_ns=548786152166221 event_kind=PacketAllow ifindex=2 src=192.168.0.3 dst=192.168.0.202 src_port=32988 dst_port=80 protocol=TCP aux0=0 aux1=0
2026-08-13T18:24:00.442329Z  INFO ebpf_daemon::monitoring: eBPF event timestamp_ns=548786152174721 event_kind=PacketAllow ifindex=2 src=192.168.0.3 dst=192.168.0.202 src_port=32988 dst_port=80 protocol=TCP aux0=0 aux1=0
2026-08-13T18:24:00.442407Z  INFO ebpf_daemon::monitoring: eBPF event timestamp_ns=548786152184263 event_kind=PacketAllow ifindex=2 src=192.168.0.3 dst=192.168.0.202 src_port=32988 dst_port=80 protocol=TCP aux0=0 aux1=0
2026-08-13T18:24:00.442486Z  INFO ebpf_daemon::monitoring: eBPF event timestamp_ns=548786152192096 event_kind=PacketAllow ifindex=2 src=192.168.0.3 dst=192.168.0.202 src_port=32988 dst_port=80 protocol=TCP aux0=0 aux1=0
2026-08-13T18:24:00.442565Z  INFO ebpf_daemon::monitoring: eBPF event timestamp_ns=548786152200096 event_kind=PacketAllow ifindex=2 src=192.168.0.3 dst=192.168.0.202 src_port=32988 dst_port=80 protocol=TCP aux0=0 aux1=0
Enter fullscreen mode Exit fullscreen mode

Regarding the rate limit (this will simply be checked via iperf3):

./target/debug/ebpf-ctl acl list
id  plen  fam  src                                dst                                proto sport  dport  rate  action
0   304   4    192.168.0.0                        192.168.0.202                      icmp any    any    -     Drop
1   320   4    192.168.0.0                        192.168.0.202                      tcp  any    80     -     Drop
2   320   4    192.168.0.0                        192.168.0.202                      udp  any    80     -     Drop
3   320   4    192.168.0.3                        192.168.0.202                      tcp  any    80     -     Allow
Enter fullscreen mode Exit fullscreen mode
root@orangepirv2:/home/orangepi/ebpf# ./target/debug/ebpf-ctl acl del --id 3
Deleted ACL rule 3
deleted rule id=3
Enter fullscreen mode Exit fullscreen mode
root@orangepirv2:/home/orangepi/ebpf# ./target/debug/ebpf-ctl acl add allow src 192.168.0.3:any dst 192.168.0.202:80 proto tcp rate 1
ACL rule added
added rule Allow: tcp 192.168.0.3:any -> 192.168.0.202:80 (prefix_len=320 rate=1)
root@orangepirv2:/home/orangepi/ebpf# ./target/debug/ebpf-ctl acl list
id  plen  fam  src                                dst                                proto sport  dport  rate  action
0   304   4    192.168.0.0                        192.168.0.202                      icmp any    any    -     Drop
1   320   4    192.168.0.0                        192.168.0.202                      tcp  any    80     -     Drop
2   320   4    192.168.0.0                        192.168.0.202                      udp  any    80     -     Drop
3   320   4    192.168.0.3                        192.168.0.202                      tcp  any    80     1     Allow
root@orangepirv2:/home/orangepi/ebpf# 
Enter fullscreen mode Exit fullscreen mode

According to the tests it will be:

root@orangepirv2:~# iperf3 -s -p 80  
-----------------------------------------------------------
Server listening on 80 (test #1)
-----------------------------------------------------------
Accepted connection from 192.168.0.3, port 45004
[  5] local 192.168.0.202 port 80 connected to 192.168.0.3 port 45010
[ ID] Interval           Transfer     Bitrate
[  5]   0.00-1.00   sec   256 KBytes  2.09 Mbits/sec                  
[  5]   1.00-2.00   sec  0.00 Bytes  0.00 bits/sec                  
[  5]   2.00-3.00   sec  0.00 Bytes  0.00 bits/sec                  
[  5]   3.00-4.00   sec  0.00 Bytes  0.00 bits/sec                  
[  5]   4.00-5.00   sec  0.00 Bytes  0.00 bits/sec                  
[  5]   5.00-6.00   sec  0.00 Bytes  0.00 bits/sec                  
[  5]   6.00-7.00   sec  0.00 Bytes  0.00 bits/sec                  
[  5]   7.00-8.00   sec  0.00 Bytes  0.00 bits/sec                  
[  5]   8.00-9.00   sec  0.00 Bytes  0.00 bits/sec                  
[  5]   9.00-10.00  sec  0.00 Bytes  0.00 bits/sec                  
[  5]  10.00-10.21  sec   100 KBytes  3.90 Mbits/sec                  
- - - - - - - - - - - - - - - - - - - - - - - - -
[ ID] Interval           Transfer     Bitrate
[  5]   0.00-10.21  sec   356 KBytes   286 Kbits/sec                  receiver
-----------------------------------------------------------
Server listening on 80 (test #2)
-----------------------------------------------------------
Enter fullscreen mode Exit fullscreen mode

Now let's compare this with the results from the same type of test performed earlier.

4. Testing Deny and Allow with VLAN ACL

For a functional VLAN ACL test, a small isolated setup with two or three nodes is sufficient. A larger test environment with many VLANs, hosts, and policies would be necessary for evaluating dataplane performance and scalability as the number of rules and network segments increases.

The VLAN test was performed on the physical end0 interface. The end0.300 and end0.100.200 VLAN subinterfaces were used for IP configuration and generating test traffic, while the actual VLAN header processing was performed by the XDP program on incoming traffic received through the physical interface.

MikroTik VLAN Configuration

Here is how I configured the VLANs on the MikroTik:

/interface vlan
add interface=bridge0 name=vlan300 vlan-id=300
/ip address
add address=192.168.100.1/24 interface=vlan300
Enter fullscreen mode Exit fullscreen mode

How I configured VLAN on the OrangePi RV2 (I configured it similarly on the R2S):
Standard VLAN:

ip link add link end0 name end0.300 type vlan id 300 ip addr add 192.168.100.2/24 dev end0.300 ip link set end0.300 up
Enter fullscreen mode Exit fullscreen mode

QinQ on Mikrotik:
External tag:

/interface vlan
add interface=bridge0 name=qinq100 vlan-id=100 
Enter fullscreen mode Exit fullscreen mode

Internal tag:

interface vlan
add interface=qinq100 name=vlan200 vlan-id=200
/ip address
add address=192.168.200.1/24 interface=vlan200
Enter fullscreen mode Exit fullscreen mode

On orangepi:

ip link add link end0 name end0.100 type vlan id 100 ip link add link end0.100 name end0.100.200 type vlan id 200 ip addr add 192.168.200.2/24 dev end0.100.200
и получаем на orangepi :
6: end0.100@end0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether c0:74:2b:fa:72:0c brd ff:ff:ff:ff:ff:ff
    inet6 fe80::c274:2bff:fefa:720c/64 scope link 
       valid_lft forever preferred_lft forever
7: end0.100.200@end0.100: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether c0:74:2b:fa:72:0c brd ff:ff:ff:ff:ff:ff
    inet 192.168.200.2/24 scope global end0.100.200
       valid_lft forever preferred_lft forever
    inet6 fe80::c274:2bff:fefa:720c/64 scope link 
       valid_lft forever preferred_lft forever
9: end0.300@end0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether c0:74:2b:fa:72:0c brd ff:ff:ff:ff:ff:ff
    inet 192.168.100.2/24 scope global end0.300
       valid_lft forever preferred_lft forever
    inet6 fe80::c274:2bff:fefa:720c/64 scope link 
       valid_lft forever preferred_lft forever
Enter fullscreen mode Exit fullscreen mode

4.1 Deny VLAN ACL:

For a Clean Test:

root@orangepirv2:/home/orangepi/ebpf# ./target/debug/ebpf-ctl acl del --id 0
error: Failed to delete rule: Failed to delete ACL rule
root@orangepirv2:/home/orangepi/ebpf# ./target/debug/ebpf-ctl acl list
(l4_acl map is empty)
root@orangepirv2:/home/orangepi/ebpf# ./target/debug/ebpf-ctl acl vlan list
(vlan_acl map is empty)
Enter fullscreen mode Exit fullscreen mode
root@orangepirv2:/home/orangepi/ebpf# ./target/debug/ebpf-ctl event-log list
event-log mask: 0x2
enabled events:
  [x] PacketDrop
  [ ] RateLimited
  [ ] ConntrackMiss
  [ ] BackendSelected
  [ ] SlowPath
  [ ] ServiceMatched
  [ ] VlanDetected
  [ ] PacketAllow
Enter fullscreen mode Exit fullscreen mode
root@orangepirv2:/home/orangepi/ebpf# ./target/debug/ebpf-ctl event-log enable VlanDetected
enabled event-log: VlanDetected
root@orangepirv2:/home/orangepi/ebpf# ./target/debug/ebpf-ctl acl vlan add 300 drop
Added VLAN rule: outer_vlan=300
added vlan rule (outer=300): drop
root@orangepirv2:/home/orangepi/ebpf#
Enter fullscreen mode Exit fullscreen mode
cat ./test.log 
…
src=192.168.100.1 dst=192.168.100.2 src_port=0 dst_port=0 protocol=ICMP aux0=300 aux1=1
2026-08-13T19:19:27.799742Z  INFO ebpf_daemon::monitoring: eBPF event timestamp_ns=552121190282679 event_kind=PacketDrop ifindex=2 src=192.168.100.1 dst=192.168.100.2 src_port=0 dst_port=0 protocol=ICMP aux0=0 aux1=0
2026-08-13T19:19:28.813405Z  INFO ebpf_daemon::monitoring: eBPF event timestamp_ns=552122204262459 event_kind=VlanDetected ifindex=2 src=192.168.100.1 dst=192.168.100.2 src_port=0 dst_port=0 protocol=ICMP aux0=300 aux1=1
2026-08-13T19:19:28.813658Z  INFO ebpf_daemon::monitoring: eBPF event timestamp_ns=552122204286126 event_kind=PacketDrop ifindex=2 src=192.168.100.1 dst=192.168.100.2 src_port=0 dst_port=0 protocol=ICMP aux0=0 aux1=0
2026-08-13T19:19:29.837427Z  INFO ebpf_daemon::monitoring: eBPF event timestamp_ns=552123228281300 event_kind=VlanDetected ifindex=2 src=192.168.100.1 dst=192.168.100.2 src_port=0 dst_port=0 protocol=ICMP aux0=300 aux1=1
2026-08-13T19:19:29.837677Z  INFO ebpf_daemon::monitoring: eBPF event timestamp_ns=552123228302716 event_kind=PacketDrop ifindex=2 src=192.168.100.1 dst=192.168.100.2 src_port=0 dst_port=0 protocol=ICMP aux0=0 aux1=0
2026-08-13T19:19:30.861526Z  INFO ebpf_daemon::monitoring: eBPF event timestamp_ns=552124252362097 event_kind=VlanDetected ifindex=2 src=192.168.100.1 dst=192.168.100.2 src_port=0 dst_port=0 protocol=ICMP aux0=300 aux1=1
2026-08-13T19:19:30.861864Z  INFO ebpf_daemon::monitoring: eBPF event timestamp_ns=552124252401680 event_kind=PacketDrop ifindex=2 src=192.168.100.1 dst=192.168.100.2 src_port=0 dst_port=0 protocol=ICMP aux0=0 aux1=0
2026-08-13T19:19:31.885517Z  INFO ebpf_daemon::monitoring: eBPF event timestamp_ns=552125276358813 event_kind=VlanDetected ifindex=2 src=192.168.100.1 dst=192.168.100.2 src_port=0 dst_port=0 protocol=ICMP aux0=300 aux1=1
2026-08-13T19:19:31.885760Z  INFO ebpf_daemon::monitoring: eBPF event timestamp_ns=552125276389521 event_kind=PacketDrop ifindex=2 src=192.168.100.1 dst=192.168.100.2 src_port=0 dst_port=0 protocol=ICMP aux0=0 aux1=0
2026-08-13T19:19:32.909459Z  INFO ebpf_daemon::monitoring: eBPF event timestamp_ns=552126300305529 event_kind=VlanDetected ifindex=2 src=192.168.100.1 dst=192.168.100.2 src_port=0 dst_port=0 protocol=ICMP aux0=300 aux1=1
2026-08-13T19:19:32.909704Z  INFO ebpf_daemon::monitoring: eBPF event timestamp_ns=552126300335362 event_kind=PacketDrop ifindex=2 src=192.168.100.1 dst=192.168.100.2 src_port=0 dst_port=0 protocol=ICMP aux0=0 aux1=0
root@orangepirv2:/home/orangepi/ebpf# 
Enter fullscreen mode Exit fullscreen mode
root@orangepirv2:/home/orangepi/ebpf# ./target/debug/ebpf-ctl acl vlan add 100 drop inner 200

2026-08-13T19:21:11.197702Z  INFO ebpf_daemon::monitoring: eBPF event timestamp_ns=552224588524734 event_kind=VlanDetected ifindex=2 src=192.168.200.1 dst=192.168.200.2 src_port=0 dst_port=0 protocol=ICMP aux0=100 aux1=2
2026-08-13T19:21:11.198016Z  INFO ebpf_daemon::monitoring: eBPF event timestamp_ns=552224588554442 event_kind=PacketDrop ifindex=2 src=192.168.200.1 dst=192.168.200.2 src_port=0 dst_port=0 protocol=ICMP aux0=0 aux1=0
2026-08-13T19:21:12.209419Z  INFO ebpf_daemon::monitoring: eBPF event timestamp_ns=552225600272714 event_kind=VlanDetected ifindex=2 src=192.168.200.1 dst=192.168.200.2 src_port=0 dst_port=0 protocol=ICMP aux0=100 aux1=2
2026-08-13T19:21:12.209665Z  INFO ebpf_daemon::monitoring: eBPF event timestamp_ns=552225600299630 event_kind=PacketDrop ifindex=2 src=192.168.200.1 dst=192.168.200.2 src_port=0 dst_port=0 protocol=ICMP aux0=0 aux1=0
2026-08-13T19:21:13.233435Z  INFO ebpf_daemon::monitoring: eBPF event timestamp_ns=552226624286596 event_kind=VlanDetected ifindex=2 src=192.168.200.1 dst=192.168.200.2 src_port=0 dst_port=0 protocol=ICMP aux0=100 aux1=2
2026-08-13T19:21:13.233753Z  INFO ebpf_daemon::monitoring: eBPF event timestamp_ns=552226624313137 event_kind=PacketDrop ifindex=2 src=192.168.200.1 dst=192.168.200.2 src_port=0 dst_port=0 protocol=ICMP aux0=0 aux1=0
2026-08-13T19:21:14.257513Z  INFO ebpf_daemon::monitoring: eBPF event timestamp_ns=552227648361060 event_kind=VlanDetected ifindex=2 src=192.168.200.1 dst=192.168.200.2 src_port=0 dst_port=0 protocol=ICMP aux0=100 aux1=2
2026-08-13T19:21:14.257769Z  INFO ebpf_daemon::monitoring: eBPF event timestamp_ns=552227648388352 event_kind=PacketDrop ifindex=2 src=192.168.200.1 dst=192.168.200.2 src_port=0 dst_port=0 protocol=ICMP aux0=0 aux1=0
2026-08-13T19:21:15.277840Z  INFO ebpf_daemon::monitoring: eBPF event timestamp_ns=552228668675580 event_kind=VlanDetected ifindex=2 src=192.168.200.1 dst=192.168.200.2 src_port=0 dst_port=0 protocol=ICMP aux0=100 aux1=2
2026-08-13T19:21:15.278091Z  INFO ebpf_daemon::monitoring: eBPF event timestamp_ns=552228668710455 event_kind=PacketDrop ifindex=2 src=192.168.200.1 dst=192.168.200.2 src_port=0 dst_port=0 protocol=ICMP aux0=0 aux1=0
2026-08-13T19:21:16.301488Z  INFO ebpf_daemon::monitoring: eBPF event timestamp_ns=552229692338134 event_kind=VlanDetected ifindex=2 src=192.168.200.1 dst=192.168.200.2 src_port=0 dst_port=0 protocol=ICMP aux0=100 aux1=2
2026-08-13T19:21:16.301757Z  INFO ebpf_daemon::monitoring: eBPF event timestamp_ns=552229692367675 event_kind=PacketDrop ifindex=2 src=192.168.200.1 dst=192.168.200.2 src_port=0 dst_port=0 protocol=ICMP aux0=0 aux1=0
2026-08-13T19:21:17.325564Z  INFO ebpf_daemon::monitoring: eBPF event timestamp_ns=552230716399223 event_kind=VlanDetected ifindex=2 src=192.168.200.1 dst=192.168.200.2 src_port=0 dst_port=0 protocol=ICMP aux0=100 aux1=2
2026-08-13T19:21:17.325825Z  INFO ebpf_daemon::monitoring: eBPF event timestamp_ns=552230716434556 event_kind=PacketDrop ifindex=2 src=192.168.200.1 dst=192.168.200.2 src_port=0 dst_port=0 protocol=ICMP aux0=0 aux1=0
Enter fullscreen mode Exit fullscreen mode

Checking the rate limit on the VLAN:

./target/debug/ebpf-ctl acl vlan add 100 allow inner 200 rate 1
Added VLAN rule: outer_vlan=100
added vlan rule (outer=100 inner=200 rate=1): allow

iperf3 -s -p 80 
-----------------------------------------------------------
Server listening on 80 (test #1)
-----------------------------------------------------------
Accepted connection from 192.168.200.3, port 39500
[  5] local 192.168.200.2 port 80 connected to 192.168.200.3 port 39508
[ ID] Interval           Transfer     Bitrate
[  5]   0.00-1.00   sec  0.00 Bytes  0.00 bits/sec                  
[  5]   1.00-2.00   sec  0.00 Bytes  0.00 bits/sec                  
[  5]   2.00-3.00   sec  0.00 Bytes  0.00 bits/sec                  
[  5]   3.00-4.00   sec  0.00 Bytes  0.00 bits/sec                  
[  5]   4.00-5.00   sec  0.00 Bytes  0.00 bits/sec                  
[  5]   5.00-6.00   sec  0.00 Bytes  0.00 bits/sec                  
[  5]   6.00-7.00   sec  0.00 Bytes  0.00 bits/sec                  
[  5]   7.00-8.00   sec  0.00 Bytes  0.00 bits/sec                  
[  5]   8.00-9.00   sec  0.00 Bytes  0.00 bits/sec                  
[  5]   9.00-10.00  sec  0.00 Bytes  0.00 bits/sec                  
- - - - - - - - - - - - - - - - - - - - - - - - -
[ ID] Interval           Transfer     Bitrate
[  5]   0.00-10.00  sec  0.00 Bytes  0.00 bits/sec                  receiver
-----------------------------------------------------------
Server listening on 80 (test #2)
-----------------------------------------------------------
Enter fullscreen mode Exit fullscreen mode
  1. Check the application of rules from the configuration file:
cat ./config2.yml 
Enter fullscreen mode Exit fullscreen mode
acl:
  allow:
    - vlan:
        - 300
        - outer: 200
          inner:
            - 100
            - 101
      rate: 1
    - src:
        addresses:
          - 192.168.0.2/32
          - 192.168.100.0/24
      dst:
        addresses: 192.168.0.202
        ports:
          number: 80
          proto: tcp
      rate: 1

  drop:
    - vlan:
        - 300
        - outer: 130
          inner:
            - 100
    - src:
        addresses:
          - fe80::/64
      dst:
        addresses:
          - fe80::c274:2bff:fefa:720c
        ports:
          number: any
          proto: icmp
      rate: 1
    - src:
        addresses:
          - 10.0.0.0/8
          - 192.168.200.1/24
      dst:
        addresses:
          - 192.168.0.2
        ports:
          number: any
          proto: icmp
      rate: 1

logging:
  PacketDrop: true
  RateLimited: false
  ConntrackMiss: false
  BackendSelected: false
  SlowPath: false
  ServiceMatched: true
  VlanDetected: true
  PacketAllow: false
Enter fullscreen mode Exit fullscreen mode

And this is how Pulsar itself reads it:

cat ./test.log 
2026-08-13T20:11:00.641182Z  INFO ebpf_daemon::manager::maps: rate_limit: global max_tokens=65536, refill_per_sec=64000, per_rule_multiplier=256
2026-08-13T20:11:00.641442Z  INFO ebpf_daemon::manager::maps: monitoring: ringbuf_bytes=16777216, poll_interval_ms=100
2026-08-13T20:11:00.641509Z  INFO ebpf_daemon::manager::maps: acl.default_action: allow
2026-08-13T20:11:00.760323Z  INFO ebpf_daemon::monitoring: Trying XDP program id: 245
2026-08-13T20:11:00.814464Z  INFO ebpf_daemon::monitoring: bpftool prog show id 245: 245: xdp  name xdp_dataplane  tag fa484001870657d5
    loaded_at 2026-08-13T23:09:05+0300  uid 0
    xlated 19776B  jited 9000B  memlock 20480B  map_ids 483,484,485,487,493,492
    btf_id 360
2026-08-13T20:11:00.868271Z  WARN ebpf_daemon::monitoring: Skipping map id 483: Map id 483 is not events ringbuf (name=counters, type=percpu_array)
2026-08-13T20:11:00.922074Z  WARN ebpf_daemon::monitoring: Skipping map id 484: Map id 484 is not events ringbuf (name=event_mask, type=array)
2026-08-13T20:11:00.975972Z  INFO ebpf_daemon::monitoring: Opened events map
2026-08-13T20:11:00.976150Z  INFO ebpf_daemon::monitoring: Events ringbuf size: 16777216 bytes
2026-08-13T20:11:00.983324Z  INFO ebpf_daemon::monitoring: Starting ringbuf event loop
Enter fullscreen mode Exit fullscreen mode
root@orangepirv2:/home/orangepi/ebpf# ./target/debug/ebpf-ctl acl list
id  plen  fam  src                                dst                                proto sport  dport  rate  action
0   304   4    10.0.0.0                           192.168.0.2                        icmp any    any    -     Drop
1   320   4    192.168.0.2                        192.168.0.202                      tcp  any    80     1     Allow
2   320   4    192.168.100.0                      192.168.0.202                      tcp  any    80     1     Allow
3   304   4    192.168.200.0                      192.168.0.2                        icmp any    any    -     Drop
4   304   6    fe80:0:0:0:0:0:0:0                 fe80:0:0:0:c274:2bff:fefa:720c     icmp any    any    -     Drop
root@orangepirv2:/home/orangepi/ebpf# ./target/debug/ebpf-ctl acl vlan list
id    outer_vlan inner_vlan action   rate  
0     100        101        allow    1     
1     130        100        drop     0     
2     100        200        allow    1     
3     250        -          drop     0     
4     300        -          allow    1 
Enter fullscreen mode Exit fullscreen mode
root@orangepirv2:/home/orangepi/ebpf# ./target/debug/ebpf-ctl event-log list
event-log mask: 0xc2
enabled events:
  [x] PacketDrop
  [ ] RateLimited
  [ ] ConntrackMiss
  [ ] BackendSelected
  [ ] SlowPath
  [x] ServiceMatched
  [x] VlanDetected
  [ ] PacketAllow
root@orangepirv2:/home/orangepi/ebpf#

Enter fullscreen mode Exit fullscreen mode

What Still Needs to Be Done
Current Implementation Limitations

At the current stage, the implementation of several events and features related to stateful packet processing is not yet complete:

  • ConntrackMiss
  • BackendSelected
  • SlowPath
  • ServiceMatched

These features are related to connection state tracking, backend selection, and service traffic processing. They are planned to be implemented at the Traffic Control (TC) layer, where stateful processing will be handled, including conntrack, load balancing, and NAT.
For now, the events listed above are used as placeholders and do not represent fully implemented functionality.
The project is available under an open-source license on GitHub:
https://github.com/AlexRoot00/Pulsar/tree/master

Top comments (0)