I have three computers on my desk: a Lenovo M710q Tiny, a Rock Pro 64, and a Dell XPS laptop. They all run Linux (of course). But I only have a single keyboard and mouse to share between them, so I plug the peripherals into whichever machine I’m using at the time. I’d really like to have a button to push that switches which machine they are wired to. The common solution to this is a USB switch or KVM.

But I look at the ethernet cables connecting each computer to a passive, 5-port switch and see a perfectly fine way to send arbitrary data between them. Why not keyboard and mouse events in addition to the normal IP traffic? It may not be the best design, but I know software and a decent bit of Linux.

libevdev

In Linux, user space programs can listen for inputs from a keyboard or mouse, which are USB HID class devices, by reading from a file. Go figure. The files for these devices are in /dev/input/. But it’s a little more complicated than just open() and read() - there are a bunch of ioctls needed to discover which event codes1 are relevant to each device, and then there’s parsing the input events from a byte array.

This is where libevdev2 comes in. The example code on the home page demonstrates just how easy it is to read evdev events such as key presses. In Zig, it looks something like

const c = @cImport({
    @cInclude("libevdev/libevdev.h");
});

const fd = try os.openZ(path, os.O.NONBLOCK, os.O.RDONLY);
defer os.close(fd);

const dev = c.libevdev_new() orelse return error.OutOfMemory;
defer c.libevdev_free(inner);

_ = c.libevdev_set_fd(dev, fd);

var rc: c_int = 0;
var ev = mem.zeroes(c.input_event);
while (rc != -@as(c_int, @intFromEnum(linux.E.AGAIN))) {
  rc = c.libevdev_next_event(dev, c.LIBEVDEV_READ_FLAG_NORMAL, &ev);
  log.debug("{s:<6} {s:<12} {d:<7}\n", .{ c.libevdev_event_type_get_name(ev.type), c.libevdev_event_code_get_name(ev.type, ev.code), ev.value });
}

Wrap that up in an actual program, and you’ll get output that looks like

EV_MSC MSC_SCAN     +458756
EV_KEY KEY_A             +1
EV_SYN SYN_REPORT        +0
EV_MSC MSC_SCAN     +458756
EV_KEY KEY_A             +0
EV_SYN SYN_REPORT        +0

This is what the kernel sends to user space when the “A” key is pressed and released. The device file descriptor can be opened with the NONBLOCK flag and monitored for read readiness with epoll.

Each event is the combination of a type, code, and value. EV_KEY-type events use a value of 0 for released, 1 for pressed, and sometimes 2 for held down. There are plenty of types; the other one I am primarily focused on is the EV_REL events emitted by my mouse.

uinput

The code above is great for reading events, but what about writing them? The machine that receives an input event needs to submit it to the OS. Linux supports that via the uinput module3. And sure enough, libevdev has an API for that as well.

const dev = c.libevdev_new();
if (dev == null) return error.OutOfMemory;
defer c.libevdev_free(dev);

_ = c.libevdev_enable_event_type(dev, c.EV_KEY);
_ = c.libevdev_enable_event_code(dev, c.EV_KEY, c.KEY_A, null);

var uidev: ?*c.libevdev_uinput = null;
_ = c.libevdev_uinput_create_from_device(dev, c.LIBEVDEV_UINPUT_OPEN_MANAGED, &uidev);

_ = c.libevdev_uinput_write_event(uidev, c.EV_KEY, c.KEY_A, 1);
_ = c.libevdev_uinput_write_event(uidev, c.EV_SYN, c.SYN_REPORT, 0);
_ = c.libevdev_uinput_write_event(uidev, c.EV_KEY, c.KEY_A, 0);
_ = c.libevdev_uinput_write_event(uidev, c.EV_SYN, c.SYN_REPORT, 0);

The program simply feeds the event type, code, and value it received from the real device back into the virtual uinput device. With the keyboard and mouse handling out of the way, I’m back into familiar territory - sending stuff over the network.

ETH_P_SPROUT

At first thought, TCP seems like a good protocol to use. This is a client + server model with strong need for order and accuracy. I don’t want key events being applied in the wrong order, and I don’t want a key press repeated just because the release event was lost in the network.

At the same time, I can’t help but bemoan the overhead. Each event is only 8 bytes since I don’t care about the timestamp:

// /usr/include/linux/input.h
struct input_event {
	struct timeval time;
	__u16 type;
	__u16 code;
	__s32 value;

Sending a pair of key and syn events would yield a 3x overhead, and that’s just for the PSH packets! I sincerely hope there is very little data loss between two machines sitting a couple feet apart from one another.

I don’t even really need IP for this - MAC addressing will work just fine. To send and receive raw Ethernet frames, Linux offers an AF_PACKET socket. And while you can bind() to an address just like a TCP or UDP socket, packet sockets make special use of the protocol field. Quoting man packet.7:

By default, all packets of the specified protocol type are passed to a packet socket. To get packets only from a specific interface use bind(2) specifying an address in a struct sockaddr_ll to bind the packet socket to an interface.

This is great, because I can tell the OS to dispatch only my special input event packets to this program rather than sift through the multitude of other Ethernet frames.

pub const ETH_P_SPROUT: u32 = 0x1b00;

const sk = try os.socket(linux.AF.PACKET, linux.SOCK.DGRAM | linux.SOCK.NONBLOCK, mem.nativeToBig(u16, msg.ETH_P_SPROUT));
const addr = linux.sockaddr.ll{
    .family = linux.AF.PACKET,
    .ifindex = ifindex,
    .halen = 0,
    .protocol = mem.nativeToBig(u16, msg.ETH_P_SPROUT),
    .hatype = 0,
    .pkttype = 0,
    .addr = [_]u8{0} ** 8,
};
errdefer os.close(sk);
try os.bind(sk, @ptrCast(&addr), @sizeOf(@TypeOf(addr)));

It doesn’t matter too much what protocol I pick, but I ought to avoid anything defined in /usr/include/linux/if_ether.h.

LLDP

At this point, I’m just going for style. The client needs to initiate a connection to the server, so it needs to know the server’s MAC address. I don’t expect to swap out machines any time soon, meaning I could hard-code the value and be done with it.

But I also know about this protocol called Link Layer Discovery Protocol4, which in short is a way for devices to advertize themselves over a wired LAN. Systemd makes it trivial to set up:

[Network]
LLDP=yes
EmitLLDP=yes

With these two options added to whatever systemd-networkd file is managing the interface, networkd will broadcast an LLDP message on a regular interval. It also records any recieved messages and stores them.

$ networkctl lldp
LINK            CHASSIS-ID          SYSTEM-NAME CAPS        PORT-ID   PORT-DESCRIPTION
enx4865ee151536 3bec90b8621047c1... barsoom     .......a... enp0s31f6 n/a               

This doesn’t look immediately helpful, but inspection of the networkd source shows that this data is stored on the filesystem at /run/systemd/netif/lldp/<ifindex>. The contents of the file are simply the LLDP frames received on that interface, which, because they are Ethernet packets, contains Ethernet headers. The source of the broadcast is in bytes 6-12.

Equiped with this discovery, my client program can now refer to the server by its hostname. It makes the CLI semantics a bit nicer, but mostly it just scratched an itch.

Grab

The last piece is finding a way to prevent other programs from receiving key events while the server is forwarding them to the client. Otherwise, programs like sway (my window manager) would keep responding to the keyboard. I’d have created a splitter rather than a demux.

c.libevdev_grab(device, c.LIBEVDEV_GRAB);

It is clearly dangerous, because this is the point of no return. If my program has a bug, the only recover is to reboot the machine (I don’t have a second keyboard to come temporarily rescue). There’s no way to Ctrl+C or systemctl stop when the rest of user space stops seeing the keyboard. How exciting!

Passing c.LIBEVDEV_UNGRAB reverses the operation. I coded the server to toggle the grab state when it sees KEY_PAUSE - a key that I have never used before and is in a convenient spot at the corner of the board.

Results

The programs work well. I had no idea how they would perform and half-expected the system to be laggy. But after working through the bugs, it pretty much just worked.

The source code is up at https://git.0x1b.me/sprout. I’m quite proud of this project, not so much because the code is impressive, but because the result is so useful to me. I spent zero dollars and didn’t add more cables to an already cluttered desk.

And I’m a sucker for anything that reduces impedance to getting stuff done. It’s very human, but admittedly there are times I won’t jot down psuedo-code of an idea or draft a quick post because I didn’t want to bother moving around my USB cables. Now I’m just one key away.