Skip to content

Raw Ethernet on macOS through BPF - #31

Merged
jannikbecher merged 4 commits into
sid2baker:mainfrom
tallakt:main
Oct 15, 2026
Merged

jannikbecher merged 4 commits into
sid2baker:mainfrom
tallakt:main

Conversation

@tallakt

@tallakt tallakt commented Oct 13, 2026

Copy link
Copy Markdown
Contributor

What this adds

{:raw, %{interface: "en7"}} now works on macOS, so a rack on a desk can be scanned and run from a Mac. Linux is unchanged.

macOS has no AF_PACKET sockets. Whole Ethernet frames go through a BPF device (/dev/bpfN) instead, with ioctl calls the BEAM cannot make. So this adds:

  • priv/bpf/ethercat_bpf.c, a small C program run as an Erlang port (a process of its own, no NIF). It opens a BPF device on the interface, lets only EtherType 0x88A4 through (with or without one VLAN tag), never receives what the machine itself sent, and passes frames to and from the BEAM over stdio in {:packet, 2}.
  • EtherCAT.Bus.Transport.Bpf, the EtherCAT.Bus.Transport behaviour on top of that port.
  • EtherCAT.Bus.Transport.raw/0, which gives Bpf on macOS and RawSocket everywhere else. Link.Single and Link.Redundant use it where they used RawSocket by default.

Choices you may want different

  • The port program is compiled on first use, with the system's cc, beside its source or in the temporary directory. That keeps the build as it is for everyone on Linux: no elixir_make, no compiler step. It does mean a Mac needs the Xcode command line tools at run time. If you would rather have a build-time compiler, I can change it.
  • Receive timestamps are taken in the BEAM when the frame is taken from the port, not by the kernel as RawSocket does with SO_TIMESTAMPING. Every round trip includes the time through the port, some tens of microseconds. Fine for a desk, not for timing that matters.
  • Arming is a no-op. The link only looks at transport messages while it awaits a reply, so the port delivers every frame as it comes; set_active_once/1 and rearm/1 have nothing to do, and drain/1 empties the mailbox of waiting frames.

Not covered

  • The raw simulator endpoint (EtherCAT.Simulator.Transport.Raw.Endpoint) still uses AF_PACKET, so the raw simulator and redundant-ring tests remain Linux only.
  • EtherCAT.Bus.InterfaceInfo still reads sysfs; the BPF transport gets the hardware address from the port program and does not use it.

How it was tested

On hardware, from a Mac (macOS 15, Apple Silicon) through a Realtek RTL8153 USB adapter, against an EK1100 with EL7037 ×3, EL1809 ×2, EL1088, EL2535 and EL2809:

  • EtherCAT.Scan.scan/1 finds all nine devices in about 175 ms, the same on repeated scans.
  • A session with a 10 ms domain reaches operational; the 40 digital inputs read, and the 16 outputs of the EL2809 switch one after another, seen on its LEDs. The EL7037 and EL2535 were left in PREOP, unconfigured.

In the suite, test/ethercat/bus/transport/bpf_test.exs (7 tests, tagged :bpf). They need no second interface and no root: a second port program is built from the same source with -DETHERCAT_BPF_SEE_SENT=1, so it receives what the first one sends on the same interface. They check the frame on the wire (destination, source, EtherType, payload, padding), the filter, VLAN tags, that the transport does not hear itself, draining, and a port program that dies.

They run only on macOS with ETHERCAT_BPF_INTERFACE set to an interface that is not on a network, such as a USB adapter with no cable in it, since they send frames on it; elsewhere they are excluded:

ETHERCAT_BPF_INTERFACE=en17 mix test test/ethercat/bus/transport/bpf_test.exs

On macOS the rest of the suite has 83 failures before and after this change, all from the simulator binding 127.0.0.2, which macOS only has after sudo ifconfig lo0 alias 127.0.0.2. I have not run the suite on Linux; CI will.

Access on macOS

Opening /dev/bpf* needs root, or membership of the access_bpf group that Wireshark's ChmodBPF sets up.

🤖 Generated with Claude Code

@jannikbecher

Copy link
Copy Markdown
Contributor

Thanks for the PR. I will add Mac to the CI and then merge.
Since this will be used for testing I think it is ok when it is compiled on runtime.

jannikbecher and others added 3 commits October 15, 2026 14:40
Preserve the macOS transport addition alongside the mailbox deadline fix
and shared Linux/macOS CI timing policy. Resolve the changelog conflict
by retaining both user-facing entries.

Validation: mix compile --warnings-as-errors; mix test (478 passed,
30 excluded; BPF wire tests require macOS fixtures).

Co-authored-by: Codex <codex@openai.com>
Compile into atomically created owner-only temporary directories and
reuse only builds published by this VM. Ignore pre-existing executables,
serialize concurrent builds, and rebuild on source-content changes.
Report exited helper ports as closed before explicit transport cleanup.

Exercise the production BPF receive path over an isolated macOS feth
pair in the shared mix ci workflow. Configured BPF setup failures now
fail instead of silently excluding the tests. Document the fixture and
remove the test-only see-sent helper variant.

Validation: mix ci passed locally (484 tests; 30 hardware/platform tests
excluded), including six portable build-security/lifecycle regressions.
macOS wire validation follows on the PR runner before merge.

Co-authored-by: Codex <codex@openai.com>
Replace the pending reference with the validated implementation commit.
No code changes; validation remains the preceding successful mix ci run.

Co-authored-by: Codex <codex@openai.com>
@jannikbecher
jannikbecher merged commit fa8b132 into sid2baker:main Oct 15, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants