Direct USB Network connection RPi and Linux
After deciding to host my own Gitea server, I also wanted to be able to take my repositories on the road with me. And what better device to do this than a Raspberry Pi! I already had a spare Pi 4 with 4GB of RAM just sitting around and plenty of SD cards*. And I had previously tried connecting a Pi via USB to my laptop so I knew I could have it working with a direct connection rather than as a server sitting on the same network. But I quickly found out that things have changed, and previous methods to use the Pi as a USB-gadget wasn't going to work. Now I'm no Linux superuser so I turned to Claude for help. Maybe the solution isn't ideal, but it works and from what I can tell there isn't anything outright wrong with it.
*Don't start the whole 'you shouldn't use SD cards for this' debate. I know that the solution isn't ideal, but for now it's more a proof-of-concept.
Forget the old ways
We begin as most tutorials on the subject do, by editing the config.txt in /boot/firmware/
dtoverlay=dwc2,dr_mode=peripheralAdd the above snippet to the bottom of your file, in section marked [all]. There might be a section marked [cm5] right above which sets the mode to host. I commented out this line just to be sure. After this you might be tempted to edit the cmdline.txt file. Don't. Trying to load the g_ether (or g_cdc or g_ndc) module at boot is apperently what caused all my issues. This is what Claude told me:
The idea is thatg_etherloads at boot and presents the USB-C port as an Ethernet gadget, which the laptop picks up via thecdc_etherdriver. On older kernels, this works. On newer Raspberry Pi OS kernels (6.12+), it doesn't — and it fails silently in a frustrating way. The kernel logs show:
Unknown kernel command line parameters "modules-load=dwc2,g_ether", will be passed to user space.Themodules-loadparameter incmdline.txtis simply not being processed by the kernel. The modules never load. We tried every gadget module variant —g_ether,g_cdc,g_ncm— and none of them loaded viacmdline.txt. Some weren't even available in the kernel build at allg_ncm). Even when modules were loaded manually, a deeper problem emerged: the laptop'scdc_etherdriver would register the interface correctly, but frames would never cross the link.tcpdumpon the laptop showed ARP requests leaving the interface, whiletcpdumpon the Pi'susb0showed zero packets arriving. The kernel logs on the laptop told the real story:
cdc_ether 1-9:1.0 enxe25c996b0822: NETDEV WATCHDOG: CPU: 0: transmit queue 0 timed out 5042 msThe transmit queue was timing out repeatedly. The monolithic gadget modulesg_ether,g_cdc) were causing a mismatch between what the Pi advertised and what the laptop's driver expected, resulting in frames being queued but never acknowledged. The solution is to bypass the monolithic gadget modules entirely and use the kernel's configfs interface to build the gadget manually.
Since I had already started with the setup I had previously used for this, I had created a host of problems that would plague me going forward. If you are starting fresh some of these steps might not be necessary. We still need a way to load modules so that the Pi can work as a USB gadget. This is the solution I worked out with Claude.
Create /etc/modules-load.d/usb-gadget.conf in your favorite text editor and add these lines:
dwc2
libcompositeAnd because I had tried loading the gadget modules in cmdline.txt I ended up blacklisting these as they kept being bound to the USB Device Controller (UDC) giving me a 'Device or resource busy error when trying to load the above modules.
Create /etc/modprobe.d/blacklist-gadget.conf in your favorite text editor and and add these lines:
blacklist g_cdc
blacklist g_etherThe setup script
Now we still need to load the modules at boot and as I discovered there are issues with MAC addresses when you connect the Pi via USB so we needed to solve that as well. Here is the script Claude created and the explanation it provided for what it does.
Create /usr/local/bin/usb-gadget-setup.sh:
#!/bin/bash
GADGET=/sys/kernel/config/usb_gadget/pi4
# Unbind UDC if already bound
if [ -f "$GADGET/UDC" ] && [ -n "$(cat $GADGET/UDC)" ]; then
echo "" > "$GADGET/UDC" 2>/dev/null || true
fi
# Remove existing symlink if present
rm -f "$GADGET/configs/c.1/ecm.usb0" 2>/dev/null || true
# Create gadget structure
mkdir -p "$GADGET"
cd "$GADGET"
echo 0x1d6b > idVendor
echo 0x0104 > idProduct
mkdir -p strings/0x409
echo "fedcba9876543210" > strings/0x409/serialnumber
echo "Raspberry Pi" > strings/0x409/manufacturer
echo "Pi4 USB Ethernet" > strings/0x409/product
mkdir -p configs/c.1/strings/0x409
echo "CDC ECM" > configs/c.1/strings/0x409/configuration
echo 250 > configs/c.1/MaxPower
# Only create the function directory if it doesn't exist.
# This preserves the MAC addresses across script re-runs.
mkdir -p functions/ecm.usb0
echo "e2:5c:99:6b:08:22" > functions/ecm.usb0/host_addr
echo "fe:39:99:a8:44:61" > functions/ecm.usb0/dev_addr
ln -sf functions/ecm.usb0 configs/c.1/
ls /sys/class/udc > UDCWhat the Script Does
The script works by writing into/sys/kernel/config/usb_gadget/— a virtual filesystem exposed by thelibcompositemodule. Every directory you create and every value you write there translates directly into USB descriptor data that the Pi sends to the host when it enumerates on the USB bus. Vendor and product IDs (idVendor,idProduct) identify the device class to the host.0x1d6bis the Linux Foundation vendor ID, and0x0104is the Multifunction Composite Gadget product ID — a widely recognised combination that most Linux hosts handle correctly. The function directory (functions/ecm.usb0) is where we specify that this gadget presents a CDC ECM Ethernet interface. CDC ECM (Ethernet Control Model) is the standard USB networking protocol that Linux'scdc_etherdriver understands. By using configfs to create a pure ECM function rather than a composite gadget (whichg_cdccreates), we avoid the descriptor ambiguity that caused the transmit queue timeouts. The final line writes the UDC (USB Device Controller) name into theUDCfile, which binds the gadget configuration to the actual hardware and activates it.ls /sys/class/udcfinds whatever UDC is available (on a Pi 4 this will befe980000.usb) and writes it in.
Why We Lock the MAC Addresses
This is a detail that causes real pain if ignored. Whenecm.usb0is first created in configfs, the kernel generates random MAC addresses for both ends of the link — one for the Pi's side (dev_addr) and one for the host/laptop side (host_addr). On the laptop, Linux names network interfaces after their MAC address. A USB Ethernet adapter with host MACe2:5c:99:6b:08:22gets the interface nameenxe25c996b0822. If the MAC changes on the next boot, you get a completely different interface name — and any NetworkManager connections, IP configurations, or SSH config entries you set up for the old name stop working. Without locked MACs, every reboot produces a new random MAC, a new interface name, and a broken configuration on the laptop side. By writing explicit MAC addresses intofunctions/ecm.usb0/host_addrandfunctions/ecm.usb0/dev_addr, we ensure the laptop always sees the same interface name and the NetworkManager connection auto-applies correctly on every plug-in. The MACs just need to be locally unique and valid. The ones in the script are taken from the first successful boot — you can use any valid MAC addresses as long as you use them consistently.
A few notes on the script. Claude created this after a lot of troubleshooting trying to make things work. There might be things in the script that are overkill or unnecessary. Also, there are plenty of things in the script that I am uncertain what they do, but I was more concerned with making things work so I just let it be as it is. As with all random BASH scripts on the internet, use at your own risk! Oh, and don't forget to make it executable:
sudo chmod +x /usr/local/bin/usb-gadget-setup.shTie it all together
The script does the heavy lifting, setting up everything so that the host machine can talk to the Pi. But it still won't start at boot. Now you could probably get away with a simple line in your crontab, but we are not taking any chances. So a systemd service it is!
Create /etc/systemd/system/usb-gadget.service:
[Unit]
Description=USB Gadget ECM
After=network.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/usb-gadget-setup.sh
RemainAfterExit=yes
[Install]
WantedBy=multi-user.targetEnable the service:
sudo systemctl enable usb-gadgetAnd to make troubleshooting a little easier we can setup the usb0 interface with a static IP so the Pi and the computer always connect using the same setup. You should be able to resolve the Pi's IP via hostname, but just in case. On the Pi run :
nmcli con add type ethernet \
ifname usb0 \
con-name "USB Gadget" \
ipv4.method manual \
ipv4.addresses 10.55.0.1/29 \
connection.autoconnect yesWe are almost done with the Pi. Just a few more commands to run and we can leave the Pi alone:
# Make sure we our changes get executed at boot
sudo update-initramfs -u
# And reboot the Pi so all our changes take effect
sudo rebootHost machine setup
Since we established a static IP on the Pi for the usb0 interface we need to do likewise on the machine the Pi will connect to. When you plug it in, the host machine enumerates the Pi as a ethernet device and creates a network interface named after the host MAC address. In our case that should beenxe25c996b0822 since we hardcoded the MAC address in the setup script. You can confirm this by checking the name of the intereface:
ip link showThen we create a similar static IP for the host machine:
nmcli con add type ethernet \
ifname enxe25c996b0822 \
con-name "Pi USB" \
ipv4.method manual \
ipv4.addresses 10.55.0.2/29 \
connection.autoconnect yes
nmcli con up "Pi USB"Of course, make sure the interface name is correct before you copy/paste this. Just change it to whatever showed in ip link and you should be fine. Now you should be able to ping the Pi on 10.55.0.1 when you are connected via USB, or of course SSH into the Pi using this IP if necessary. For me the Pi resolved via hostname without any additional setup. If for some reason you cannot reach your Pi via hostname while connected over USB make sure the Avahi daemon is installed on both machines:
# Install if not present
sudo apt install avahi-daemon
# Enable daemon if not active
sudo systemctl enable --now avahi-daemon
# On the host machine install Avahi utils for testing
sudo apt install avahi-utilsNaturally, if your system is not Debian based use your package manager of choice. You can test the connection by using avahi-resolve -n hostname.local (from the utils package). This should resolve a IPv6 address. Assuming your router isn't setup to give out IPv6 addresses this should be the Pi responding over the USB Network that we have created.
And that's it! You should now be able to communicate with your Pi over a USB Network. Congrats!
One last thing to note. If the hostname works correctly there should be no difference between using a local network and the USB interface. The system should choose the faster of the two if both exist, and that should be USB. And if your Pi is not connected to the network then of course USB is the only option. This means that you can have your Pi plugged in via a powercord and running 24/7 on your home network, but also take it on the road and plug it into a laptop when necessary and still be able to reach whatever service you have running on the Pi.