Tunneling CAN over IP

Table of Contents

This post will focus on sharing a CAN network between Docker containers. I'm sure there are other use-cases for tunneling CAN over IP, but this is the one that I've run into.

I have experience using cannelloni and socketcand. I recommend using socketcand for ease-of-use.

Sharing the same CAN bus between two Docker containers

socketcand

Docker image

Take the following Docker image

FROM ubuntu:22.04

RUN apt update \
    && DEBIAN_FRONTEND=noninteractive apt install -y --no-install-recommends \
        build-essential \
        ca-certificates \
        can-utils \
        dh-autoreconf \
        git \
        iproute2 \
        kmod \
    && apt clean \
    && rm -rf /var/lib/apt/lists/* \
    ;

SHELL ["/bin/bash", "-c"]
RUN git clone https://github.com/linux-can/socketcand.git /tmp/socketcand/ \
    && pushd /tmp/socketcand/ \
    && ./autogen.sh \
    && ./configure --prefix /usr/local/ \
    && make \
    && make install \
    && popd \
    && rm -rf /tmp/socketcand/ \
    ;

If you really care about container size, you could use a multi-stage build to cut out the build dependencies from the resulting socketcand and kmod binaries.

Build with

docker build -t socketcand .

Server

On the host, run

docker network create socketcand-shared
docker run -it --rm --network socketcand-shared --name socketcand-server --cap-add NET_ADMIN socketcand

In the container, run

ip link add dev can1 type vcan
ip link set up can1
socketcand --verbose --interface can1 &
candump -L can1

Then again on the host, run

$ docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' socketcand-server
172.19.0.2

This is how we'll tell what IP address to connect the socketcandcl client to.

Client

The socketcand protocol defines a UDP service discovery mechanism for clients to discover the socketcand server.

However, the socketcandcl client does not use this discovery mechanism, so you need to connect it to the socketcand server by IP address directly.

On the host, run

export SOCKETCAND_SERVER="$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' socketcand-server)"
docker run -it --rm --network socketcand-shared --name socketcand-client --cap-add NET_ADMIN --env SOCKETCAND_SERVER socketcand

In the container, run

ip link add dev can1 type vcan
ip link set up can1
socketcandcl --verbose --server $SOCKETCAND_SERVER --interfaces can1,can1 &
candump -L can1 &
cansend can1 123#4455

You should see

(1687220675.698707) can1 123#4455

in both the server and the client's console output.

cannelloni

I'm sure you could use cannelloni for this, but I think socketcand was easier, so I never tried.

Sharing a CAN bus between a Docker container and your host

socketcand

You can run either the server or the client in a container. I'll provide instructions for running the server on the host.

On the host, create a can1 virtual CAN network to share with the container.

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

This doesn't have to be a vcan network, but I don't currently have access to a physical CAN network for demonstration.

On the host, run

socketcand --verbose --interface can1 &
candump -L can1 &

This will bind to your default interface's public IP, which we'll need to use to connect the client to the server.

# There's probably a better way
export SOCKETCAND_SERVER=$(ip -4 -br address show eth0 | tr -s ' ' | cut -d ' ' -f 3 | sed 's|\(.*\)/.*|\1|')

In a separate terminal session, start a container with

docker run -it --rm --name socketcand-client --cap-add NET_ADMIN --env SOCKETCAND_SERVER socketcand

and in the resulting shell, run

ip link add dev can1 type vcan
ip link set up can1
socketcandcl --verbose --server $SOCKETCAND_SERVER --interfaces can1,can1 &
candump -L can1 &
cansend can1 123#4455

cannelloni

It's been several years since I've last used cannelloni, and the thing that made it less desirable was that it used SCTP rather than TCP. But SCTP wasn't supported by the Linux kernel for the hardware device I was interacting with.

But nowadays it appears it supports TCP just fine.

Build and install cannelloni on the host

sudo apt install libsctp-dev
git clone https://github.com/mguentner/cannelloni.git
cd cannelloni/
cmake -B ./build/ -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=$HOME/.local/ .
cmake --build ./build/ --parallel --target install

Then do the same inside a Docker image with the following Dockerfile:

FROM ubuntu:22.04

RUN apt update \
    && DEBIAN_FRONTEND=noninteractive apt install -y --no-install-recommends \
        build-essential \
        ca-certificates \
        can-utils \
        cmake \
        git \
        iproute2 \
        libsctp-dev \
    && apt clean \
    && rm -rf /var/lib/apt/lists/* \
    ;

SHELL ["/bin/bash", "-c"]
RUN git clone https://github.com/mguentner/cannelloni.git /tmp/cannelloni/ \
    && pushd /tmp/cannelloni/ \
    && cmake -B ./build/ -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr/ . \
    && cmake --build ./build/ --parallel --target install \
    && popd \
    && rm -rf /tmp/cannelloni/ \
    ;

built with

docker build -t cannelloni .

Then, on the host, create a can1 network to share between the host and a Docker container

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

Option 1 - UDP

Run the Docker container

docker run  --interactive --name cannelloni-udp --tty --rm --publish 2000:2000/udp --cap-add=NET_ADMIN cannelloni:latest
ip link add dev can1 type vcan
ip link set up can1
cannelloni -I can1 -R 127.0.0.1 -p -l 2000 -r 3000 &
candump -L can1 &

Then run cannelloni on the host

export CANNELLONI_IP="$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' cannelloni-udp)"
cannelloni -I can1 -R $CANNELLONI_IP -r 2000 -l 3000 &
candump -L can1 &

Notice that the -l and -r arguments flipped!

Now send a CAN message in the Docker container!

docker exec -it cannelloni-udp cansend can1 123#001122

You should see it in the host's candump -L can1 output. Likewise, you should be able to send a CAN message from the host to the container.

Option 2 - SCTP

Run cannelloni in the container

docker run  --interactive --tty --rm --publish 2000:2000/sctp --cap-add=NET_ADMIN --name cannelloni-sctp cannelloni:latest
ip link add dev can1 type vcan
ip link set can1 up
cannelloni -I can1 -S s &
candump -L can1 &

and on the host, run

export CANNELLONI_IP="$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' cannelloni-sctp)"
cannelloni -I can1 -R $CANNELLONI_IP -S c &
candump -L can1 &
docker exec -it cannelloni-sctp cansend can1 123#334455
cansend can1 456#7788

The docs suggest that just switching -S s/-S c to -C s/-C c is all that's necessary to use TCP rather than SCTP.

Bonus - using Docker compose

Take the following docker-compose.yml file.

version: "3.9"
services:
    socketcand-server:
        image: socketcand
        networks:
            - socketcand-shared
        cap_add:
            - NET_ADMIN
        entrypoint:
            - bash
            - -c
            - "ip link add dev can1 type vcan && ip link set up can1 && socketcand --verbose --interfaces can1"

    socketcand-client:
        image: socketcand
        networks:
            - socketcand-shared
        cap_add:
            - NET_ADMIN
        entrypoint:
            - bash
            - -c
            - "ip link add dev can1 type vcan && ip link set up can1 && socketcandcl --verbose --server socketcand-server --interfaces can1,can1"

networks:
    socketcand-shared:

Notice that we did not have to specify any IP addresses, instead using --server to pass the hostname of the socketcand-server container to socketcandcl.

After running docker compose up, we can start a shell in the running containers to run candump and cansend to prove to ourselves that the two services are sharing a CAN network.

docker exec -it can-tunneling-socketcand-client-1 candump -L can1
docker exec -it can-tunneling-socketcand-server-1 cansend can1 123#4455