[PATCH linux] usbip: tools: usbipd: implement authorization

Alyssa Ross
2021-10-18T12:33:15Z

Devices are filtered before all usbipd operations, so that to each
client, it's as if the only usbip-able devices are the ones it's been
authorized to see. Authorizing a client is as simple as e.g.:

ln -s /sys/bus/usb/devices/3-2.2 /var/run/usbipd/2001:db8::1/

Which will make the USB device 3-2.2 available to the client
connecting from 2001:db8::1. I selected this filesystem based
mechanism because of its simplicity to implement -- no need to parse
any files, and granting and revoking permissions is atomic.
Technically, it doesn't matter what's actually at that path, as long
as it exists, but I've chosen to require it to be a symlink into
sysfs, because I think that's the most clear, and actually checking
that minimizes valid states, which is generally good practice.

Currently, only IP addresses are supported (IPv4 and IPv6). It would
be possible to extend this to support hostnames as well, but that
would require various additional security considerations.

This shouldn't make a difference to any USB functionality when a
client _is_ authorized, but just to make sure, I've tested with
various USB 2 and USB 3 devices, including hubs.

Signed-off-by: Alyssa Ross <hi@alyssa.is>
---
tools/usb/usbip/src/usbipd.c | 112 +++++++++++++++++++++++++++++++----
1 file changed, 100 insertions(+), 12 deletions(-)

diff --git a/tools/usb/usbip/src/usbipd.c b/tools/usb/usbip/src/usbipd.c
index 48398a78e88a..c380e785106f 100644
--- a/tools/usb/usbip/src/usbipd.c
+++ b/tools/usb/usbip/src/usbipd.c
@@ -16,6 +16,7 @@
#include <unistd.h>
#include <netdb.h>
#include <string.h>
+#include <stdbool.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
@@ -45,6 +46,7 @@
#define MAIN_LOOP_TIMEOUT 10

#define DEFAULT_PID_FILE "/var/run/" PROGNAME ".pid"
+#define PROG_STATE_PATH "/var/run/" PROGNAME

static const char usbip_version_string[] = PACKAGE_STRING;

@@ -57,6 +59,14 @@ static const char usbipd_help_string[] =
" -6, --ipv6\n"
" Bind to IPv6. Default is both.\n"
"\n"
+ " -A, --authorization\n"
+ " Apply authorization rules to clients. Clients\n"
+ " will only be able to attach to devices, or see\n"
+ " devices in the device list, that they are\n"
+ " authorized to access. To authorize a client to\n"
+ " access a device:\n"
+ " ln -s /sys/bus/usb/devices/$USB_PATH " PROG_STATE_PATH "/auth/$CLIENT_IP/\n"
+ "\n"
" -e, --device\n"
" Run in device mode.\n"
" Rather than drive an attached device, create\n"
@@ -82,12 +92,78 @@ static const char usbipd_help_string[] =
" Show version.\n";

static struct usbip_host_driver *driver;
+static bool enable_authorization;

static void usbipd_help(void)
{
printf("%s\n", usbipd_help_string);
}

+static bool device_visible(struct usbip_exported_device *edev, int connfd)
+{
+ bool r = false;
+ int e;
+ struct sockaddr *addr = NULL;
+ socklen_t addr_len = 0;
+ char name[NI_MAXHOST];
+ char *path = NULL, *real_path = NULL;
+ char *expected_path = NULL, *expected_real_path = NULL;
+
+ if (!enable_authorization)
+ return true;
+
+ if (getpeername(connfd, NULL, &addr_len) == -1)
+ goto err;
+ if (!(addr = malloc(addr_len)))
+ goto err;
+ if (getpeername(connfd, addr, &addr_len) == -1)
+ goto err;
+
+ if ((e = getnameinfo(addr, addr_len,
+ name, sizeof name, NULL, 0, NI_NUMERICHOST))) {
+ err("getting name of peer: %s", gai_strerror(e));
+ goto out;
+ }
+
+ // Since we're mandating numeric hosts, we don't have to worry
+ // about what happens if name contains things like slashes.
+ // If we were to use names as well, we might have to be more
+ // careful, and use openat2, etc.
+ if (asprintf(&path, "%s/auth/%s/%s",
+ PROG_STATE_PATH, name, edev->udev.busid) == -1)
+ goto err;
+
+ if (!(real_path = realpath(path, NULL))) {
+ if (errno == ENOENT)
+ goto out;
+ goto err;
+ }
+
+ if (asprintf(&expected_path, "/sys/bus/usb/devices/%s",
+ edev->udev.busid) == -1)
+ goto err;
+
+ if (!(expected_real_path = realpath(expected_path, NULL)))
+ goto err;
+
+ r = !strcmp(real_path, expected_real_path);
+ goto out;
+
+err:
+ err("checking device visibility: %s", strerror(errno));
+out:
+ free(addr);
+ free(path);
+ free(real_path);
+ free(expected_path);
+ free(expected_real_path);
+ return r;
+}
+
+#define CHECK_DEVICE_VISIBILITY(edev, connfd) \
+ if (!device_visible(edev, connfd)) \
+ continue
+
static int recv_request_import(int sockfd)
{
struct op_import_request req;
@@ -109,6 +185,7 @@ static int recv_request_import(int sockfd)

list_for_each(i, &driver->edev_list) {
edev = list_entry(i, struct usbip_exported_device, node);
+ CHECK_DEVICE_VISIBILITY(edev, sockfd);
if (!strncmp(req.busid, edev->udev.busid, SYSFS_BUS_ID_SIZE)) {
info("found requested device: %s", req.busid);
found = 1;
@@ -176,6 +253,7 @@ static int send_reply_devlist(int connfd)
/* number of exported devices */
list_for_each(j, &driver->edev_list) {
edev = list_entry(j, struct usbip_exported_device, node);
+ CHECK_DEVICE_VISIBILITY(edev, connfd);
if (edev->status != SDEV_ST_USED)
reply.ndev += 1;
}
@@ -196,6 +274,7 @@ static int send_reply_devlist(int connfd)

list_for_each(j, &driver->edev_list) {
edev = list_entry(j, struct usbip_exported_device, node);
+ CHECK_DEVICE_VISIBILITY(edev, connfd);
if (edev->status == SDEV_ST_USED)
continue;

@@ -586,17 +665,18 @@ static int do_standalone_mode(int daemonize, int ipv4, int ipv6)
int main(int argc, char *argv[])
{
static const struct option longopts[] = {
- { "ipv4", no_argument, NULL, '4' },
- { "ipv6", no_argument, NULL, '6' },
- { "daemon", no_argument, NULL, 'D' },
- { "daemon", no_argument, NULL, 'D' },
- { "debug", no_argument, NULL, 'd' },
- { "device", no_argument, NULL, 'e' },
- { "pid", optional_argument, NULL, 'P' },
- { "tcp-port", required_argument, NULL, 't' },
- { "help", no_argument, NULL, 'h' },
- { "version", no_argument, NULL, 'v' },
- { NULL, 0, NULL, 0 }
+ { "ipv4", no_argument, NULL, '4' },
+ { "ipv6", no_argument, NULL, '6' },
+ { "authorization", no_argument, NULL, 'A' },
+ { "daemon", no_argument, NULL, 'D' },
+ { "daemon", no_argument, NULL, 'D' },
+ { "debug", no_argument, NULL, 'd' },
+ { "device", no_argument, NULL, 'e' },
+ { "pid", optional_argument, NULL, 'P' },
+ { "tcp-port", required_argument, NULL, 't' },
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, 'v' },
+ { NULL, 0, NULL, 0 }
};

enum {
@@ -620,7 +700,7 @@ int main(int argc, char *argv[])
cmd = cmd_standalone_mode;
driver = &host_driver;
for (;;) {
- opt = getopt_long(argc, argv, "46DdeP::t:hv", longopts, NULL);
+ opt = getopt_long(argc, argv, "46ADdeP::t:hv", longopts, NULL);

if (opt == -1)
break;
@@ -632,6 +712,9 @@ int main(int argc, char *argv[])
case '6':
ipv6 = 1;
break;
+ case 'A':
+ enable_authorization = true;
+ break;
case 'D':
daemonize = 1;
break;
@@ -663,6 +746,11 @@ int main(int argc, char *argv[])
if (!ipv4 && !ipv6)
ipv4 = ipv6 = 1;

+ if (enable_authorization && driver != &host_driver) {
+ err("authorization is only supported when using host driver");
+ goto err_out;
+ }
+
switch (cmd) {
case cmd_standalone_mode:
rc = do_standalone_mode(daemonize, ipv4, ipv6);

base-commit: 620b74d01b9d4393bef6742bf121908322c2fe0b
--
2.33.0