Looking for help to create a Raspberry Pi with USB ethernet

I’m currently trying to setup a Raspberry Pi with USB ethernet to connect to it from an iPad over USB-C.
There are various articles about this but I got a bit stuck.

My config currently lives here:

And it’s mostly inspired by the config from @maxhbr

I currently have two issues:

In addition I’m unsure about the following script, most of the articles implement this in some form and I don’t really know how to implement it in NixOS.
Using it 1:1 didn’t work.

#!/usr/bin/env bash
cd /sys/kernel/config/usb_gadget/
mkdir -p pi4
cd pi4
echo 0x1d6b > idVendor # Linux Foundation
echo 0x0104 > idProduct # Multifunction Composite Gadget
echo 0x0100 > bcdDevice # v1.0.0
echo 0x0200 > bcdUSB # USB2
echo 0xEF > bDeviceClass
echo 0x02 > bDeviceSubClass
echo 0x01 > bDeviceProtocol
mkdir -p strings/0x409
echo "fedcba9876543211" > strings/0x409/serialnumber
echo "TheWifiNinja" > strings/0x409/manufacturer
echo "PI4 USB Device" > strings/0x409/product
mkdir -p configs/c.1/strings/0x409
echo "Config 1: ECM network" > configs/c.1/strings/0x409/configuration
echo 250 > configs/c.1/MaxPower
# Add functions here
# see gadget configurations below
# End functions
mkdir -p functions/ecm.usb0
HOST="00:dc:c8:f7:75:14" # "HostPC"
SELF="00:dd:dc:eb:6d:a1" # "BadUSB"
echo $HOST > functions/ecm.usb0/host_addr
echo $SELF > functions/ecm.usb0/dev_addr
ln -s functions/ecm.usb0 configs/c.1/
udevadm settle -t 5 || :
ls /sys/class/udc > UDC
ifup usb0

Articles I found on the topic:

I have no experience with usb gadget mode, but I am curious, so I started digging into this a bit.

My first question is how do you “enable the libcomposite driver” (/etc/modules/libcomposite) as described in sausheong’s article?

I do not know how that works, but I expect it is related to the nixos dwc2 option. The option’s help text suggests how to verify gadget mode is working. Possibly the kernel module needed for an ethernet gadget is not loaded.

I’d have to dig deeper into kernel docs to figure out more details – maybe you can if you do not already know enough. I would like to learn how to make this work.

That script is mostly creating a directory containing a bunch of files with specific content. Then it tells the OS to use the dir. You could create a nix package to “build” the directory tree as a nix store entry, and tell the OS to use it in the “install” phase. It sure would be nice if someone has already done this for nix, but maybe it has to be you. :wink:

Good point I haven’t seen this yet, might be a piece of the puzzle.

As for the script, I don’t mind trying to package it, but shouldn’t it be working manually as well if a package is possible?
If not I’m not sure what is missing besides correcting the shebang.

It depends. The script is using the filesystem to tell the OS how to configure a particular feature. But NixOS typically feeds custom paths to OS tools. I’d try to understand how the script is supposed to work and then verify that is compatible with how nix configures the relevant OS bits (or adjust the script accordingly).

Ideally the tools involved in the script would report useful diagnostic info if the inputs from the script do not work. Do you see any relevant console output when running the script? Any relevant output in the journal?

In this case, the relevant tool is udev. The script creates the directory /sys/kernel/config/usb_gadget/pi4 and populates it with config parameters. Then it calls udevadm settle. Then adds another file, UDC, to pi4, and finally activates the new network interface with ifup.

I do not know linux well enough to know how /sys/kernel/config/ works without reading kernel docs. I don’t know NixOS well enough to know how it uses or supports /sys/kernel/config/. This directory is empty on my system.

Also, /sys/class/udc does not exist on my system. If udc is generally about USB or network devices, I’d expect it to be present if NixOS supports it. That’s another bit to find in the kernel docs.

I hope these thoughts help.

1 Like

Absolutely, I don’t know if they help me solve it but at least I have now a direction I can look into.

Running the script already fails at the first step and creating the directory doesn’t work.

mkdir: cannot create directory ‘/sys/kernel/config/usb_gadget/’: Operation not permitted

Does the directory exist? What are its permissions? (I presume you ran the script as root.)

I found this article on usb gadget configfs informative. It describes what your script is doing. Notice the instructions begin with modprobe libcomposite which implies boot.kernelModules = [ "libcomposite" ]; for NixOS. But maybe that module is included by default and explicitly listing it does nothing.

I am not sure how linux configfs fits with NixOS, or even how to use it, so I read a bit of the configfs kernel docs. Using findmnt I see that NixOS mounts configfs at /sys/kernel/config/. Mine is empty until I run sudo modprobe libcomposite, which creates the usb_gadgets dir. Now cd /sys/kernel/config/usb_gadget/ works. Maybe that NixOS boot.kernelModules entry actually is necessary. :slight_smile:

1 Like

You’re absolutely fantastic!
I got it working now. You’re right the module libcomposite was the missing piece and makes the usb_gadget directory appear.

If adapted the script slightly (full paths and restarting the relevant services at the end).
Wrapping everything into a systemd service should be easy now.

#!/usr/bin/env bash
mkdir -p /sys/kernel/config/usb_gadget/pi4
cd /sys/kernel/config/usb_gadget/pi4
echo 0x1d6b > idVendor # Linux Foundation
echo 0x0104 > idProduct # Multifunction Composite Gadget
echo 0x0100 > bcdDevice # v1.0.0
echo 0x0200 > bcdUSB # USB2
echo 0xEF > bDeviceClass
echo 0x02 > bDeviceSubClass
echo 0x01 > bDeviceProtocol
mkdir -p /sys/kernel/config/usb_gadget/pi4/strings/0x409
echo "fedcba9876543211" > strings/0x409/serialnumber
echo "TheWifiNinja" > strings/0x409/manufacturer
echo "PI4 USB Device" > strings/0x409/product
mkdir -p /sys/kernel/config/usb_gadget/pi4/configs/c.1/strings/0x409
echo "Config 1: ECM network" > configs/c.1/strings/0x409/configuration
echo 250 > configs/c.1/MaxPower
# Add functions here
# see gadget configurations below
# End functions
mkdir -p /sys/kernel/config/usb_gadget/pi4/functions/ecm.usb0
HOST="00:dc:c8:f7:75:14" # "HostPC"
SELF="00:dd:dc:eb:6d:a1" # "BadUSB"
echo $HOST > functions/ecm.usb0/host_addr
echo $SELF > functions/ecm.usb0/dev_addr
ln -s functions/ecm.usb0 configs/c.1/
udevadm settle -t 5 || :
ls /sys/class/udc > UDC
systemctl restart network-addresses-usb0.service
systemctl restart dhcpd4.service

Here’s the config for the pi as a minimal working example:
For anyone copying this, please be aware that the { custom, hostname }: part is very much non standard.

{ custom, hostname }: { lib, pkgs, ... }:
{
  imports = [
    custom.inputs.nixos-hardware.nixosModules.raspberry-pi-4
  ];

  boot = {
    supportedFilesystems = lib.mkForce [ "f2fs" "ntfs" "cifs" "ext4" "vfat" "nfs" "nfs4" ];
    kernelModules = [ "libcomposite" ];
  };
  fileSystems = {
    "/" = {
      device = "/dev/disk/by-label/NIXOS_SD";
      fsType = "ext4";
      options = [ "noatime" ];
    };
  };

  hardware.raspberry-pi."4".dwc2.enable = true;

  networking = {
    hostName = hostname;
    hosts = {
      "127.0.0.1" = [ "mobile.domain.local" ];
      "10.213.0.1" = [ "mobile.domain.local" ];
    };
    interfaces.usb0.ipv4.addresses = [
      {
        address = "10.213.0.1";
        prefixLength = 24;
      }
    ];
  };

  boot.loader.raspberryPi.firmwareConfig = "dtoverlay=dwc2";
  networking.dhcpcd.denyInterfaces = [ "usb0" ];

  services.dhcpd4 = {
    enable = true;
    interfaces = [ "usb0" ];
    extraConfig = ''
      option domain-name "domain.mobile";
      option subnet-mask 255.255.255.0;
      option broadcast-address 10.213.0.255;
      option domain-name-servers 9.9.9.9, 1.1.1.1;
      option routers 10.213.0.1;
      subnet 10.213.0.0 netmask 255.255.255.0 {
        range 10.213.0.100 10.213.0.200;
      }
    '';
  };
  systemd.services."usb-otg" = {
    serviceConfig = {
      Type = "oneshot";
      RemainAfterExit = true;
    };
    wantedBy = [ "default.target" ];
    script = ''
      mkdir -p /sys/kernel/config/usb_gadget/pi4
      cd /sys/kernel/config/usb_gadget/pi4
      echo 0x1d6b > idVendor # Linux Foundation
      echo 0x0104 > idProduct # Multifunction Composite Gadget
      echo 0x0100 > bcdDevice # v1.0.0
      echo 0x0200 > bcdUSB # USB2
      echo 0xEF > bDeviceClass
      echo 0x02 > bDeviceSubClass
      echo 0x01 > bDeviceProtocol
      mkdir -p /sys/kernel/config/usb_gadget/pi4/strings/0x409
      echo "fedcba9876543211" > strings/0x409/serialnumber
      echo "TheWifiNinja" > strings/0x409/manufacturer
      echo "PI4 USB Device" > strings/0x409/product
      mkdir -p /sys/kernel/config/usb_gadget/pi4/configs/c.1/strings/0x409
      echo "Config 1: ECM network" > configs/c.1/strings/0x409/configuration
      echo 250 > configs/c.1/MaxPower
      # Add functions here
      # see gadget configurations below
      # End functions
      mkdir -p /sys/kernel/config/usb_gadget/pi4/functions/ecm.usb0
      HOST="00:dc:c8:f7:75:14" # "HostPC"
      SELF="00:dd:dc:eb:6d:a1" # "BadUSB"
      echo $HOST > functions/ecm.usb0/host_addr
      echo $SELF > functions/ecm.usb0/dev_addr
      ln -s functions/ecm.usb0 configs/c.1/
      udevadm settle -t 5 || :
      ls /sys/class/udc > UDC
    '';
  };
  systemd.services.dhcpd4.after = [ "usb-otg.service" ];
  systemd.services."network-addresses-usb0".after = [ "usb-otg.service" ];
}
1 Like

I’ve extended the config in the previous example with the service.
The full config lives here:

That looks great! Thanks for sharing it.

I notice that you include networks.psk in your config. Isn’t that supposed to be secret? (I keep mine in local files outside the nix store.)

Also, your network uses top-level-domain .local. .local is reserved for use by mdns (such as with avahi). Its resolution by regular DNS depends on how the resolver is configured. I’ve encountered problems with such a configuration when mDNS is not running, particularly in recently upgraded Microsoft networks. The Wikipedia article on .local has more details.

You can pick or makeup any other TLD you want. .lan has been commonly used for local networks for decades. Just know that whatever you choose may shadow Internet access to that TLD – useful if you want to avoid resolving hosts in particular TLDs.

You’re absolutely right, however since it’s the hotspot from my phone I don’t really care too much.
Ideally I would use age to encrypt the password or file but I don’t had more time to extend it.

And right again, I just have used .local for years and so far have been to lazy to change it.
Thank you for the suggestion of .lan, I wasn’t sure what I should pick instead.

Hi @Nebucatnetzer @ericgundrum I was just looking for a similar solution and glad it worked for you, I’ll have a look into this in depth but it seems complex. I think OS images built with “nixos-hardware” on raspberry pis should have this behavior/usb gadget mode on by default.

Could you upstream a clean solution/create a PR to GitHub - NixOS/nixos-hardware: A collection of NixOS modules covering hardware quirks. , so people don’t need to configure this their own and have this behavior by default? Thanks!

The whole hardware stuff in general is complex.
I personally stopped using nixos-hardware because it broke my setup with 23.05 and I still continue understand the reason.
It could be working again but since I can achieve my configuration without it, I won’t readd it for the moment.