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.
--network host
to use the host's vcan
network(s)
candump | nc
on one side and nc | cansend
on the other side
cangw
as described by
https://www.systec-electronic.com/en/demo/blog/article/news-socketcan-docker-the-solution
to setup pairs of unidirectional IP tunnels
I have experience using cannelloni
and socketcand
. I recommend using
socketcand
for ease-of-use.
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 .
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.
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.
I'm sure you could use cannelloni
for this, but I think socketcand
was easier,
so I never tried.
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
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
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.
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.
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