The goal of this guide is to make a Z-Wave USB Stick that is connected to a Proxmox 8.2.2 host available to an lxc container. To accomplish this, we'll passthrough the serial device from the host to the container.

First, we need to identify which serial device to passthrough:

$ ls -l /dev/serial/by-id/

total 0
lrwxrwxrwx 1 root root 13 May 27 09:51 usb-Silicon_Labs_Zooz_ZST10_700_Z-Wave_Stick_0001-if00-port0 -> ../../ttyUSB0

The device we're looking for is a Zooz ZST10, which we can easily identify here among the many serial devices.

Now we need the major/minor numbers for the device. Follow the symlink to /dev/ttyUSB0 and run stat:

$ stat -c '%Hr:%Lr' /dev/ttyUSB0

188:0

This gives us the major number of 188 and the minor number of 0. The major number should always remain the same, but the minor number may change if another USB serial device is attached.

Now we open the configuration of our container, in this case at /dev/pve/lxc/103.conf and add the following lines (replacing major/minor numbers and device path as needed):

lxc.cgroup.devices.allow: c 188:0 rwm

lxc.mount.entry: /dev/serial/by-id/usb-Silicon_Labs_Zooz_ZST10_700_Z-Wave_Stick_0001-if00-port0 dev/ttyUSB0 none bind,optional,create=file

Now the device is successfully passed through. However, if you're using an unprivileged container, then nothing in the container can access it! You'll need both read and write access to the device to use it. We'll look at two ways to accomplish this: the easy way, and the hard way.

The Easy Way

Simply give everyone read/write permissions on the device: chmod o+rw /dev/ttyUSB0. Quick and easy, but it does mean that every user has full access to the Z-Wave stick. Good for troubleshooting, but not recommended for long-term use.

The Hard Way

For The Hard Way™, we'll map a group into the container that can use the Z-Wave stick. First, we'll create a user on the host, and get the new uid and gid.

$ useradd zwave
$ id zwave

uid=1002(zwave) gid=1002(zwave) groups=1002(zwave)

In the container, create a group with the same gid (same gid isn't strictly necessary but makes things much easier).

groupadd -g 1002 zwave

Any user that is a member of the zwave group will be able to access the Z-Wave stick.

Now we have to map both the gid and uid into the container. The exact values will differ depending on the gid and uid that you end up with.

# Map 1002 ids from 0-1001 (container) to 100000-101001 (host)
lxc.idmap: u 0 100000 1002
lxc.idmap: g 0 100000 1002

# Map 1 id from 1002-1002 (container) to 1002-1002 (host)
lxc.idmap: u 1002 1002 1
lxc.idmap: g 1002 1002 1

# Map 64533 ids from 1003-65535 (container) to 101003-165535 (host)
lxc.idmap: u 1003 101003 64533
lxc.idmap: g 1003 101003 64533

More information on ID mapping can be found on the Proxmox wiki.

We need to allow the uid and gid to be remapped. In both /etc/subgid and /etc/subuid add the line root:1002:1. They should look something like

root:1002:1
root:100000:65536

Lastly, group of the device needs to be changed to zwave:

chgrp zwave /dev/ttyUSB0

Now we can restart the container, and the device's group will be zwave with 660 permissions:

$ ls -l /dev/ttyUSB0 
crw-rw---- 1 nobody zwave 188, 0 May 26 12:00 /dev/ttyUSB0

Reboot Persistence

Right now, once the host reboots, the new permissions on the device will disappear, and will need to be re-applied. To avoid this, we can create a udev rule to match the Z-Wave stick and change the group or file mode.

Identify the vendor ID and product ID using lsusb -v. Find the device, and note the idVendor and idProduct values.

Bus 003 Device 003: ID 10c4:ea60 Silicon Labs CP210x UART Bridge
Device Descriptor:
<snip />
  idVendor           0x10c4 Silicon Labs
  idProduct          0xea60 CP210x UART Bridge
  bcdDevice            1.00
  iManufacturer           1 Silicon Labs
  iProduct                2 Zooz_ZST10_700_Z-Wave_Stick
<snip />

In this case, the vendor id is 10c4 and the product id is ea60.

Create a new file under /etc/udev/rules.d/ called 90-zwave.rules, and add

SUBSYSTEMS=="usb", ATTRS{idVendor}=="10c4", ATTRS{idProduct}=="ea60", GROUP="zwave", MODE="0660"

Now any device that rule will be assigned to the group zwave and set to the file mode 0660.

If you're doing The Easy Way, you'll instead want to ignore the group, and use the mode 0666.

SUBSYSTEMS=="usb", ATTRS{idVendor}=="10c4", ATTRS{idProduct}=="ea60", MODE="0666"

Below is the full lxc configuration file for my container:

# Passthrough configuration
lxc.cgroup.devices.allow: c 188:0 rwm
lxc.mount.entry: /dev/serial/by-id/usb-Silicon_Labs_Zooz_ZST10_700_Z-Wave_Stick_0001-if00-port0 dev/ttyUSB0 none bind,optional,create=file
lxc.idmap: u 0 100000 1002
lxc.idmap: g 0 100000 1002
lxc.idmap: u 1002 1002 1
lxc.idmap: g 1002 1002 1
lxc.idmap: u 1003 101003 64533
lxc.idmap: g 1003 101003 64533

# Not relevant for passthrough but included for completeness
arch: amd64
cores: 1
features: nesting=1
hostname: zwavejs
memory: 512
net0: name=eth0,bridge=vmbr0,firewall=1,hwaddr=BC:24:11:D6:81:29,ip=dhcp,type=veth
ostype: debian
rootfs: vmstore:subvol-103-disk-0,size=8G
swap: 512
unprivileged: 1
lxc.cap.drop: sys_rawio