Tutorial  on  NetworkingContainers

Cars, Containers and CAN Networking

This tutorial shows different ways to work with CAN

CAN Basics

Cars use different internal networks to exchange information between the electronic control units. The communication runs over a common medium, but in a bus topoglogy in constrast to the star topology in common Ethernet networks in IT.

simplified vehicle network

The CAN bus communicates using the arbitration ID (CAN ID for short) and a data payload.

Modern vehicle networks feature High Performance Computers (HCP) and containers. Some containers need to be connected to the vehicle network to receive information such as vehicle speed, position or send out information.

Virtual CAN Networking

The CAN bus is integrated into the Linux networking and provides physical drivers, but also virtual networking (similar to veth). A CAN network interface can be added and activated (set up), which behaves like a physical CAN network, but without the bitrate and frequency configuration.

In the following we create a virtual CAN network interface vcan0 using the vcan driver.

sudo modprobe vcan
sudo ip link add vcan0 type vcan
sudo ip link set up dev vcan0

The commands above create a new CAN networking interface

ip link show dev vcan0
5: vcan0: <NOARP,UP,LOWER_UP> mtu 72 qdisc noqueue state UNKNOWN group default qlen 1000
    link/can

At this stage a program can open a socket to this interface and start communicating. The SocketCAN implementation allows to create a socket of type PF_CAN for sending and receiving data.

s = socket(PF_CAN, SOCK_RAW, CAN_RAW);

The can-utils implement general functions to send and receive data from a CAN bus. In this example we start candump to listen to the previously created vcan0 interface. After we send a CAN frame via cansend on the same interface and therefore on the same CAN bus, candump displays the frame.

candump vcan0 &
cansend vcan0 123#abcd
  vcan0  123   [2]  AB CD

We can observe that on arbitration ID hex 123 the hex bytes AB CD are transmitted in the data field.

CAN and Containers

Since the CAN network is a bus topology, a vcan interface behaves like a bus system. We can send information in the same bus and multiple processes can open a socket on that interface to exchange information. This extends to the use inside of a container: as we move the interface into the container, all processes inside the container can use it.

CONTAINER=$(docker run -d --rm --name carly -ti alpine)
PID=$(docker inspect -f '{{.State.Pid}}' "$CONTAINER")
docker ps

CONTAINER ID   IMAGE     COMMAND     CREATED                  STATUS                  PORTS     NAMES
9f05bc214940   alpine    "/bin/sh"   Less than a second ago   Up Less than a second             carly

sudo ip link set vcan0 name vcan0 netns $PID

docker exec -ti carly ip link

This approach works with physical interfaces as well. The downside is, that only processes inside the container can use this interface. Processes outside the container share a different network namespace and therefore can't connect to the CAN network.

Thanks to the work of Oliver Hartkopp on the Linux Kernel, we can use CAN also in container setups using the vxcan driver (see Design & separation of CAN applications). This provides a pair of interfaces (similar to veth) that share the same CAN bus and can be used in two different network namespaces (containers).

container setup with vxcan

In the following we create a vxcan pair named vxcanA and vxcanB.

sudo modprobe vxcan
sudo ip link add vxcanA type vxcan peer name vxcanB 
ip link show

5: vxcanB@vxcanA: <NOARP,M-DOWN> mtu 72 qdisc noop state DOWN group default qlen 1000
    link/can 
6: vxcanA@vxcanB: <NOARP,M-DOWN> mtu 72 qdisc noop state DOWN group default qlen 1000
    link/can

The newly created vxcan interface pair needs to be moved into the container and set "up". This can be done using the ip tool in conjunction with nsneter:

  • the vxcanB interface is moved in the container (identified by process ID)
  • the network interface in the container vxcanB is renamed to can0
  • can0 in the container is set "up"
  • the host interface vxcanA is set up
sudo ip link set up dev vxcanB 
sudo ip link set vxcanB name can0 netns $PID
sudo nsenter -t $PID -n ip link set vxcanB name can0
sudo nsenter -t $PID -n ip link set can0 up
sudo ip link set up dev vxcanA

The result is a container running cangen and a host interface. At this point the host should receive the CAN data from the container.

candump vxcanA

Docker Compose

Install the Docker plugin.

docker plugin install wsovalle/vxcan
Plugin "wsovalle/vxcan" is requesting the following privileges:
 - network: [host]
 - capabilities: [CAP_NET_ADMIN]
Do you grant the above permissions? [y/N] y
latest: Pulling from wsovalle/vxcan
Digest: sha256:9a0346041673fcc28764352859d0f6e04f0ec15755b131266925ceae399d7ebb
8d673625b8e2: Complete 
Installed plugin wsovalle/vxcan

Load the CAN-GW module!

sudo modprobe can-gw

Write a docker-compose.yml into your current directory.

services:
  udsserver:
    image: rkugler/carly
    command: ["cangen", "can0"]
    tty: true
    networks: 
      - canbus0
networks:
  canbus0:
    driver: wsovalle/vxcan:latest 
    driver_opts:
      vxcan.dev: can_hostA
      vxcan.peer: can
      vxcan.id: 0

eBPF Programming

sudo apt-get update -y && sudo apt-get install bpfcc-tools libbpfcc-dev python3-bpfcc xz-utils gcc rsync libbpf-dev bpftool flex autoconf -y
wget -O linux-6.1.141.tar.xz https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.1.141.tar.xz
tar -C /usr/src/ -xf linux-6.1.141.tar.xz
sudo ln -sfn linux-6.1.141 /usr/src/linux

sudo apt update -y && sudo apt install -y git build-essential clang llvm libelf-dev libz-dev libcap-dev binutils-dev libbpf-dev libssl-dev
git clone --recurse-submodules https://github.com/libbpf/bpftool.git
cd bpftool/src
sudo make install

In this first step we just use the XDP hook to print the data.

SEC("xdp")
int xdp_filter_can(struct xdp_md *ctx)
{
    void *data_end = (void *)(long)ctx->data_end;
    void *data = (void *)(long)ctx->data;
    struct can_frame *frame = data;

    if(data + sizeof(struct can_frame) > data_end)
    {
        return XDP_DROP;
    }

    bpf_printk("can/xdp id=0x%x dlc=%d data=0x%02x.%02x.%02x.%02x.%02x.%02x.%02x.%02x\n", 
        frame->can_id, 
        frame->can_dlc, 
        frame->data[0],
        frame->data[1],
        frame->data[2],
        frame->data[3],
        frame->data[4],
        frame->data[5],
        frame->data[6],
        frame->data[7]); // 8 data bytes

    return XDP_ALLOW; // catch all
}

Clone the edgecase1/ebpf-can repo.

git clone https://github.com/edgecase1/ebpf-can
cd ebpf-can/can-xdp-starter/
sudo bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h
make

ls -la can-xdp.bpf.o
-rw-rw-r-- 1 laborant laborant 8576 Jun 29 11:05 can-xdp.bpf.o

Identify the CAN interface using ip link. The interface should be up and of type can.

8: vcanf78c1b1f@if7: <NOARP,UP,LOWER_UP> mtu 72 qdisc noqueue state UP mode DEFAULT group default qlen 1000    link/can  link-netnsid 0

sudo bash run.sh xdp_start vcanf78c1b1f
use xdp_unload <dev> to load this program

[ ] loading 'can-xdp.bpf.o' and attaching it to vcanf78c1b1f (XDP)
8: vcanf78c1b1f@if7: <NOARP,UP,LOWER_UP> mtu 72 xdpgeneric qdisc noqueue state UP mode DEFAULT group default qlen 1000
    link/can  link-netnsid 0
    prog/xdp id 71 name xdp_can_drop_id tag 4124a3daf685e437 jited 

------------------------------------------------------------------------
eBPF program has been loaded and attached to device vcanf78c1b1f.
After this promt is closed the eBPF program is detached and unloaded.
Press ENTER or CTRL-D to close this promt.
------------------------------------------------------------------------

In a new Terminal you can verify what's talked on the CAN bus:

candump vcanf78c1b1f
  vcanf78c1b1f  143   [8]  4A 83 F5 5E 0A E8 41 66
  vcanf78c1b1f  4E1   [0] 
  vcanf78c1b1f  475   [5]  2D 69 64 22 2A
  vcanf78c1b1f  681   [8]  86 6C 5D 05 64 C2 B9 24
  vcanf78c1b1f  4CB   [8]  CD 42 46 04 23 0F BC 33
  vcanf78c1b1f  7A3   [8]  44 E3 EF 2E CF 42 2E 20
  vcanf78c1b1f  209   [8]  82 69 62 78 3A F2 90 7E
  vcanf78c1b1f  534   [5]  7F 3A F4 0D 77

And the same conversation should be visible in the trace-pipe.

sudo cat /sys/kernel/tracing/trace_pipe
  cangen-3231    [000] ..s21  1047.140230: bpf_trace_printk: can/xdp id=0x65 dlc=8 data=0xba.2c.28.7c.93.dc.69.59
  cangen-3231    [000] ..s21  1047.340373: bpf_trace_printk: can/xdp id=0x4b9 dlc=8 data=0x68.e4.25.1f.1c.eb.e2.5e
  cangen-3231    [000] ..s21  1047.540489: bpf_trace_printk: can/xdp id=0x39e dlc=5 data=0xcc.d0.41.3b.08.00.00.00

To actually filter a CAN ID, for a simple demo purpose, we need to adapt the code can-xdp.bpf.c. At this point we could remove the debug bpf_printk statement as well.

if(frame->can_id >= 0x123) // drop CAN frames with CAN ID 0x123 packets
{                    
    bpf_printk("dropping CAN frame with ID %x", frame->can_id);
    return XDP_DROP;
}

After we finished editing, we need to rebuild the object file and load it.

make
clang -g -O2 -target bpf -D__TARGET_ARCH_x86_64 -I . -c can-xdp.bpf.c -o can-xdp.bpf.o 
sudo bash run.sh xdp_start vcanf78c1b1f```

After the new program is loaded you shoud see only packets with CAN ID up to 0x122 and the dropped CAN frames are shown in the trace-pipe.

  cangen-3231    [002] ..s21  1482.786976: bpf_trace_printk: dropping CAN frame with ID 788
  cangen-3231    [002] ..s21  1482.987073: bpf_trace_printk: dropping CAN frame with ID 77f
  cangen-3231    [002] ..s21  1483.187178: bpf_trace_printk: dropping CAN frame with ID 41b
  cangen-3231    [002] ..s21  1483.387305: bpf_trace_printk: dropping CAN frame with ID 783

About the Author

Reinhard Kugler

Reinhard Kugler

Find this author online