525 lines
14 KiB
C
525 lines
14 KiB
C
/* USB Gadget simulating a USB hub over FunctionFS
|
|
* - this was used to play with limitations of the Linux kernel hub driver
|
|
*
|
|
* (C) 2019 by Harald Welte <laforge@gnumonks.org>
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 2 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program; if not, write to the Free Software
|
|
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA
|
|
*/
|
|
|
|
#include <errno.h>
|
|
#include <stdint.h>
|
|
#include <endian.h>
|
|
#include <signal.h>
|
|
#include <sys/types.h>
|
|
#include <linux/usb/functionfs.h>
|
|
#include <linux/usb/ch11.h>
|
|
|
|
#if __BYTE_ORDER == __LITTLE_ENDIAN
|
|
#define cpu_to_le16(x) (x)
|
|
#define cpu_to_le32(x) (x)
|
|
#else
|
|
#define cpu_to_le16(x) ((((x) >> 8) & 0xffu) | (((x) & 0xffu) << 8))
|
|
#define cpu_to_le32(x) \
|
|
((((x) & 0xff000000u) >> 24) | (((x) & 0x00ff0000u) >> 8) | \
|
|
(((x) & 0x0000ff00u) << 8) | (((x) & 0x000000ffu) << 24))
|
|
#endif
|
|
|
|
#define le32_to_cpu(x) le32toh(x)
|
|
#define le16_to_cpu(x) le16toh(x)
|
|
|
|
enum {
|
|
DUSB,
|
|
};
|
|
|
|
/***********************************************************************
|
|
* Actual USB Descriptors
|
|
***********************************************************************/
|
|
|
|
static const struct {
|
|
struct usb_functionfs_descs_head_v2 header;
|
|
__le32 fs_count;
|
|
struct {
|
|
struct usb_interface_descriptor intf;
|
|
struct usb_endpoint_descriptor_no_audio ep_int;
|
|
} __attribute__ ((packed)) fs_descs;
|
|
} __attribute__ ((packed)) descriptors = {
|
|
.header = {
|
|
.magic = cpu_to_le32(FUNCTIONFS_DESCRIPTORS_MAGIC_V2),
|
|
.flags = cpu_to_le32(FUNCTIONFS_HAS_FS_DESC | FUNCTIONFS_ALL_CTRL_RECIP),
|
|
.length = cpu_to_le32(sizeof(descriptors)),
|
|
},
|
|
.fs_count = cpu_to_le32(2),
|
|
.fs_descs = {
|
|
.intf = {
|
|
.bLength = sizeof(descriptors.fs_descs.intf),
|
|
.bDescriptorType = USB_DT_INTERFACE,
|
|
.bNumEndpoints = 1,
|
|
.bInterfaceClass = USB_CLASS_HUB,
|
|
.iInterface = 1,
|
|
},
|
|
.ep_int = {
|
|
.bLength = sizeof(descriptors.fs_descs.ep_int),
|
|
.bDescriptorType = USB_DT_ENDPOINT,
|
|
.bEndpointAddress = 1 | USB_DIR_IN,
|
|
.bmAttributes = USB_ENDPOINT_XFER_INT,
|
|
.wMaxPacketSize = 64,
|
|
.bInterval = 12,
|
|
},
|
|
},
|
|
};
|
|
|
|
#define STR_INTERFACE_ "Osmocom USB Hub"
|
|
|
|
static const struct {
|
|
struct usb_functionfs_strings_head header;
|
|
struct {
|
|
__le16 code;
|
|
const char str1[sizeof(STR_INTERFACE_)];
|
|
} __attribute__((packed)) lang0;
|
|
} __attribute__((packed)) strings = {
|
|
.header = {
|
|
.magic = cpu_to_le32(FUNCTIONFS_STRINGS_MAGIC),
|
|
.length = cpu_to_le32(sizeof(strings)),
|
|
.str_count = cpu_to_le32(1),
|
|
.lang_count = cpu_to_le32(1),
|
|
},
|
|
.lang0 = {
|
|
cpu_to_le16(0x0409), /* en-us */
|
|
STR_INTERFACE_,
|
|
},
|
|
};
|
|
|
|
|
|
struct usb2_hub_desc_header {
|
|
__u8 bDescLength;
|
|
__u8 bDescriptorType;
|
|
__u8 bNbrPorts;
|
|
__le16 wHubCharacteristics;
|
|
__u8 bPwrOn2PwrGood;
|
|
__u8 bHubContrCurrent;
|
|
__u8 data[0];
|
|
};
|
|
|
|
#define NUM_PORTS 31
|
|
#define HDESC_ARR_BYTES ((NUM_PORTS + 1 + 7) / 8)
|
|
static const struct {
|
|
struct usb2_hub_desc_header hdr;
|
|
uint8_t DeviceRemovable[HDESC_ARR_BYTES];
|
|
uint8_t PortPwrCtrlMask[HDESC_ARR_BYTES];
|
|
} __attribute__ ((packed)) hub_desc = {
|
|
.hdr = {
|
|
.bDescLength = sizeof(hub_desc),
|
|
.bDescriptorType = USB_DT_HUB,
|
|
.bNbrPorts = NUM_PORTS,
|
|
.wHubCharacteristics = cpu_to_le16(0x0001),
|
|
.bPwrOn2PwrGood = 100,
|
|
.bHubContrCurrent = 0,
|
|
},
|
|
};
|
|
|
|
|
|
/***********************************************************************
|
|
* USB FunctionFS interface
|
|
***********************************************************************/
|
|
|
|
#include <stdlib.h>
|
|
#include <stdio.h>
|
|
#include <unistd.h>
|
|
#include <string.h>
|
|
#include <assert.h>
|
|
#include <fcntl.h>
|
|
#include <sys/stat.h>
|
|
#include <osmocom/core/select.h>
|
|
#include <osmocom/core/utils.h>
|
|
#include <osmocom/core/msgb.h>
|
|
#include <osmocom/core/utils.h>
|
|
#include <osmocom/core/application.h>
|
|
#include <osmocom/core/logging.h>
|
|
|
|
#ifndef FUNCTIONFS_SUPPORTS_POLL
|
|
#include <libaio.h>
|
|
struct aio_help {
|
|
struct msgb *msg;
|
|
struct iocb *iocb;
|
|
};
|
|
#endif
|
|
|
|
/* usb function handle */
|
|
struct ufunc_handle {
|
|
struct osmo_fd ep0;
|
|
struct osmo_fd ep_int;
|
|
struct llist_head ep_int_queue;
|
|
#ifndef FUNCTIONFS_SUPPORTS_POLL
|
|
struct osmo_fd aio_evfd;
|
|
io_context_t aio_ctx;
|
|
struct aio_help aio_int;
|
|
#endif
|
|
};
|
|
|
|
static int ep_int_cb(struct osmo_fd *ofd, unsigned int what)
|
|
{
|
|
LOGP(DUSB, LOGL_DEBUG, "%s\n", __func__);
|
|
return 0;
|
|
}
|
|
|
|
const struct value_string ffs_evt_type_names[] = {
|
|
{ FUNCTIONFS_BIND, "BIND" },
|
|
{ FUNCTIONFS_UNBIND, "UNBIND" },
|
|
{ FUNCTIONFS_ENABLE, "ENABLE" },
|
|
{ FUNCTIONFS_DISABLE, "DISABLE" },
|
|
{ FUNCTIONFS_SETUP, "SETUP" },
|
|
{ FUNCTIONFS_SUSPEND, "SUSPEND" },
|
|
{ FUNCTIONFS_RESUME, "RESUME" },
|
|
{ 0, NULL }
|
|
};
|
|
|
|
/* local, stand-alone definition of a USB control request */
|
|
struct _usb_ctrl_req {
|
|
uint8_t bRequestType;
|
|
uint8_t bRequest;
|
|
uint16_t wValue;
|
|
uint16_t wIndex;
|
|
uint16_t wLength;
|
|
} __attribute__ ((packed));;
|
|
|
|
/* class requests from the USB 2.0 hub spec, table 11-15 */
|
|
#define HUB_CLASS_REQ(dir, type, request) ((((dir) | (type)) << 8) | (request))
|
|
/* GetBusState and SetHubDescriptor are optional, omitted */
|
|
#define ClearHubFeature HUB_CLASS_REQ(USB_DIR_OUT, USB_RT_HUB, USB_REQ_CLEAR_FEATURE)
|
|
#define ClearPortFeature HUB_CLASS_REQ(USB_DIR_OUT, USB_RT_PORT, USB_REQ_CLEAR_FEATURE)
|
|
#define GetHubDescriptor HUB_CLASS_REQ(USB_DIR_IN, USB_RT_HUB, USB_REQ_GET_DESCRIPTOR)
|
|
#define GetHubStatus HUB_CLASS_REQ(USB_DIR_IN, USB_RT_HUB, USB_REQ_GET_STATUS)
|
|
#define GetPortStatus HUB_CLASS_REQ(USB_DIR_IN, USB_RT_PORT, USB_REQ_GET_STATUS)
|
|
#define SetHubFeature HUB_CLASS_REQ(USB_DIR_OUT, USB_RT_HUB, USB_REQ_SET_FEATURE)
|
|
#define SetPortFeature HUB_CLASS_REQ(USB_DIR_OUT, USB_RT_PORT, USB_REQ_SET_FEATURE)
|
|
|
|
static const struct value_string hub_class_spec_req_vals[] = {
|
|
OSMO_VALUE_STRING(USB_REQ_CLEAR_FEATURE),
|
|
OSMO_VALUE_STRING(USB_REQ_GET_DESCRIPTOR),
|
|
OSMO_VALUE_STRING(USB_REQ_GET_STATUS),
|
|
OSMO_VALUE_STRING(USB_REQ_SET_FEATURE),
|
|
{ 0, NULL }
|
|
};
|
|
|
|
#define CCID_CTRL_RET_INVALID -1
|
|
#define CCID_CTRL_RET_UNKNOWN -2
|
|
#define CCID_CTRL_RET_OK 0
|
|
|
|
/*! Handle [class specific] CTRL request. We assume the caller has already verified that the
|
|
* request was made to the correct interface as well as it is a class-specific request.
|
|
* \param[in] ci CCID Instance for which CTRL request was received
|
|
* \param[in] ctrl_req buffer holding the 8 bytes CTRL transfer header
|
|
* \param[out] data_in data to be returned to the host in the IN transaction (if any)
|
|
* \returns CCID_CTRL_RET_OK, CCID_CTRL_RET_INVALID or CCID_CTRL_RET_UNKNOWN
|
|
*/
|
|
int hub_handle_ctrl(void *ci, const uint8_t *ctrl_req, const uint8_t **data_in)
|
|
{
|
|
const struct _usb_ctrl_req *req = (const struct _usb_ctrl_req *) ctrl_req;
|
|
static uint16_t status[2];
|
|
int rc;
|
|
|
|
LOGP(DUSB, LOGL_NOTICE, "CTRL bmReqT=0x%02X bRequest=%s, wValue=0x%04X, wIndex=0x%04X, wLength=%d\n",
|
|
req->bRequestType, get_value_string(hub_class_spec_req_vals, req->bRequest),
|
|
req->wValue, req->wIndex, req->wLength);
|
|
|
|
switch (req->bRequestType & 0x7f) {
|
|
case USB_RT_HUB:
|
|
switch (req->bRequest) {
|
|
case USB_REQ_GET_DESCRIPTOR:
|
|
if (req->wIndex != 0) {
|
|
LOGP(DUSB, LOGL_ERROR, "GET_DESC wIndex invalid\n");
|
|
return CCID_CTRL_RET_INVALID;
|
|
}
|
|
if (0) // ctrl_req->wValue != FIXME
|
|
return CCID_CTRL_RET_INVALID;
|
|
*data_in = (const uint8_t *) &hub_desc;
|
|
return sizeof(hub_desc);
|
|
case USB_REQ_CLEAR_FEATURE:
|
|
switch (req->wValue) {
|
|
case C_HUB_LOCAL_POWER:
|
|
case C_HUB_OVER_CURRENT:
|
|
return CCID_CTRL_RET_OK;
|
|
}
|
|
break;
|
|
case USB_REQ_GET_STATUS:
|
|
status[0] = cpu_to_le16(HUB_STATUS_LOCAL_POWER);
|
|
status[1] = cpu_to_le16(0);
|
|
*data_in = (const uint8_t *) status;
|
|
return sizeof(status);
|
|
case USB_REQ_SET_FEATURE:
|
|
if (req->wValue > 1)
|
|
return CCID_CTRL_RET_INVALID;
|
|
return CCID_CTRL_RET_OK;
|
|
}
|
|
break;
|
|
case USB_RT_PORT:
|
|
switch (req->bRequest) {
|
|
case USB_REQ_CLEAR_FEATURE:
|
|
switch (req->wValue) {
|
|
case USB_PORT_FEAT_CONNECTION:
|
|
case USB_PORT_FEAT_ENABLE:
|
|
case USB_PORT_FEAT_SUSPEND:
|
|
case USB_PORT_FEAT_OVER_CURRENT:
|
|
case USB_PORT_FEAT_RESET:
|
|
case USB_PORT_FEAT_L1:
|
|
case USB_PORT_FEAT_POWER:
|
|
case USB_PORT_FEAT_LOWSPEED:
|
|
case USB_PORT_FEAT_C_CONNECTION:
|
|
case USB_PORT_FEAT_C_ENABLE:
|
|
case USB_PORT_FEAT_C_SUSPEND:
|
|
case USB_PORT_FEAT_C_OVER_CURRENT:
|
|
case USB_PORT_FEAT_C_RESET:
|
|
case USB_PORT_FEAT_TEST:
|
|
case USB_PORT_FEAT_C_PORT_L1:
|
|
return CCID_CTRL_RET_OK;
|
|
}
|
|
break;
|
|
case USB_REQ_GET_STATUS:
|
|
status[0] = cpu_to_le16(0);
|
|
status[1] = cpu_to_le16(0);
|
|
*data_in = (const uint8_t *) status;
|
|
return sizeof(status);
|
|
case USB_REQ_SET_FEATURE:
|
|
//selector = wIndex >> 8
|
|
//port = wIndex & 0xff
|
|
return CCID_CTRL_RET_OK;
|
|
}
|
|
}
|
|
return CCID_CTRL_RET_UNKNOWN;
|
|
}
|
|
|
|
static void handle_setup(int fd, const struct usb_ctrlrequest *setup)
|
|
{
|
|
const uint8_t *data_in = NULL;
|
|
int rc;
|
|
|
|
LOGP(DUSB, LOGL_NOTICE, "EP0 SETUP bRequestType=0x%02x, bRequest=0x%02x wValue=0x%04x, "
|
|
"wIndex=0x%04x, wLength=%u\n", setup->bRequestType, setup->bRequest,
|
|
le16_to_cpu(setup->wValue), le16_to_cpu(setup->wIndex), le16_to_cpu(setup->wLength));
|
|
|
|
rc = hub_handle_ctrl(NULL, (const uint8_t *) setup, &data_in);
|
|
switch (rc) {
|
|
case CCID_CTRL_RET_INVALID:
|
|
if (setup->bRequestType & USB_DIR_IN)
|
|
read(fd, NULL, 0); /* cause stall */
|
|
else
|
|
write(fd, NULL, 0); /* cause stall */
|
|
break;
|
|
case CCID_CTRL_RET_UNKNOWN:
|
|
/* FIXME: is this correct behavior? */
|
|
if (setup->bRequestType & USB_DIR_IN)
|
|
write(fd, NULL, 0); /* send ZLP */
|
|
else
|
|
read(fd, NULL, 0);
|
|
break;
|
|
default:
|
|
if (setup->bRequestType & USB_DIR_IN) {
|
|
uint16_t len = rc;
|
|
if (setup->wLength < len)
|
|
len = setup->wLength;
|
|
LOGP(DUSB, LOGL_NOTICE, "Writing %u: %s\n", len, osmo_hexdump_nospc(data_in, len));
|
|
write(fd, data_in, le16_to_cpu(len));
|
|
} else
|
|
read(fd, NULL, 0); /* FIXME: control OUT? */
|
|
break;
|
|
}
|
|
}
|
|
|
|
static int ep_0_cb(struct osmo_fd *ofd, unsigned int what)
|
|
{
|
|
struct ufunc_handle *uh = (struct ufunc_handle *) ofd->data;
|
|
int rc;
|
|
|
|
if (what & OSMO_FD_READ) {
|
|
struct usb_functionfs_event evt;
|
|
rc = read(ofd->fd, (uint8_t *)&evt, sizeof(evt));
|
|
if (rc < sizeof(evt))
|
|
return -23;
|
|
LOGP(DUSB, LOGL_NOTICE, "EP0 %s\n", get_value_string(ffs_evt_type_names, evt.type));
|
|
switch (evt.type) {
|
|
case FUNCTIONFS_ENABLE:
|
|
//aio_refill_out(uh);
|
|
break;
|
|
case FUNCTIONFS_SETUP:
|
|
handle_setup(ofd->fd, &evt.u.setup);
|
|
break;
|
|
}
|
|
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
#ifndef FUNCTIONFS_SUPPORTS_POLL
|
|
|
|
/* dequeue the next msgb from ep_int_queue and set up AIO for it */
|
|
static void dequeue_aio_write_int(struct ufunc_handle *uh)
|
|
{
|
|
struct aio_help *ah = &uh->aio_int;
|
|
struct msgb *d;
|
|
int rc;
|
|
|
|
if (ah->msg)
|
|
return;
|
|
|
|
d = msgb_dequeue(&uh->ep_int_queue);
|
|
if (!d)
|
|
return;
|
|
|
|
OSMO_ASSERT(ah->iocb);
|
|
ah->msg = d;
|
|
io_prep_pwrite(ah->iocb, uh->ep_int.fd, msgb_data(d), msgb_length(d), 0);
|
|
io_set_eventfd(ah->iocb, uh->aio_evfd.fd);
|
|
rc = io_submit(uh->aio_ctx, 1, &ah->iocb);
|
|
OSMO_ASSERT(rc >= 0);
|
|
}
|
|
|
|
static int evfd_cb(struct osmo_fd *ofd, unsigned int what)
|
|
{
|
|
struct ufunc_handle *uh = (struct ufunc_handle *) ofd->data;
|
|
struct io_event evt[1];
|
|
struct msgb *msg;
|
|
uint64_t ev_cnt;
|
|
int i, rc;
|
|
|
|
rc = read(ofd->fd, &ev_cnt, sizeof(ev_cnt));
|
|
assert(rc == sizeof(ev_cnt));
|
|
|
|
rc = io_getevents(uh->aio_ctx, 1, 1, evt, NULL);
|
|
if (rc <= 0) {
|
|
LOGP(DUSB, LOGL_ERROR, "error in io_getevents(): %d\n", rc);
|
|
return rc;
|
|
}
|
|
|
|
for (i = 0; i < rc; i++) {
|
|
int fd = evt[i].obj->aio_fildes;
|
|
if (fd == uh->ep_int.fd) {
|
|
/* interrupt endpoint AIO has completed. This means the IRQ transfer
|
|
* which we generated has reached the host */
|
|
LOGP(DUSB, LOGL_DEBUG, "IRQ AIO completed, free()ing msgb\n");
|
|
msgb_free(uh->aio_int.msg);
|
|
uh->aio_int.msg = NULL;
|
|
dequeue_aio_write_int(uh);
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
#endif
|
|
|
|
|
|
static int ep0_init(struct ufunc_handle *uh)
|
|
{
|
|
int rc;
|
|
|
|
/* open control endpoint and write descriptors to it */
|
|
rc = open("ep0", O_RDWR);
|
|
assert(rc >= 0);
|
|
osmo_fd_setup(&uh->ep0, rc, OSMO_FD_READ, &ep_0_cb, uh, 0);
|
|
osmo_fd_register(&uh->ep0);
|
|
rc = write(uh->ep0.fd, &descriptors, sizeof(descriptors));
|
|
if (rc != sizeof(descriptors)) {
|
|
LOGP(DUSB, LOGL_ERROR, "Cannot write descriptors: %s\n", strerror(errno));
|
|
return -1;
|
|
}
|
|
rc = write(uh->ep0.fd, &strings, sizeof(strings));
|
|
if (rc != sizeof(strings)) {
|
|
LOGP(DUSB, LOGL_ERROR, "Cannot write strings: %s\n", strerror(errno));
|
|
return -1;
|
|
}
|
|
|
|
/* open other endpoint file descriptors */
|
|
INIT_LLIST_HEAD(&uh->ep_int_queue);
|
|
rc = open("ep1", O_RDWR);
|
|
assert(rc >= 0);
|
|
osmo_fd_setup(&uh->ep_int, rc, 0, &ep_int_cb, uh, 1);
|
|
#ifdef FUNCTIONFS_SUPPORTS_POLL
|
|
osmo_fd_register(&uh->ep_int);
|
|
#endif
|
|
|
|
#ifndef FUNCTIONFS_SUPPORTS_POLL
|
|
#include <sys/eventfd.h>
|
|
/* for some absolutely weird reason, gadgetfs+functionfs don't support
|
|
* the standard methods of non-blocking I/o (select/poll). We need to
|
|
* work around using Linux AIO, which is not to be confused with POSIX AIO! */
|
|
|
|
memset(&uh->aio_ctx, 0, sizeof(uh->aio_ctx));
|
|
rc = io_setup(1, &uh->aio_ctx);
|
|
OSMO_ASSERT(rc >= 0);
|
|
|
|
/* create an eventfd, which will be marked readable once some AIO completes */
|
|
rc = eventfd(0, 0);
|
|
OSMO_ASSERT(rc >= 0);
|
|
osmo_fd_setup(&uh->aio_evfd, rc, OSMO_FD_READ, &evfd_cb, uh, 0);
|
|
osmo_fd_register(&uh->aio_evfd);
|
|
|
|
uh->aio_int.iocb = malloc(sizeof(struct iocb));
|
|
#endif
|
|
|
|
return 0;
|
|
}
|
|
|
|
static const struct log_info_cat log_info_cat[] = {
|
|
[DUSB] = {
|
|
.name = "USB",
|
|
.description = "USB Transport",
|
|
.enabled = 1,
|
|
.loglevel = LOGL_NOTICE,
|
|
},
|
|
};
|
|
|
|
static const struct log_info log_info = {
|
|
.cat = log_info_cat,
|
|
.num_cat = ARRAY_SIZE(log_info_cat),
|
|
};
|
|
|
|
static void *g_tall_ctx;
|
|
|
|
static void signal_handler(int signal)
|
|
{
|
|
switch (signal) {
|
|
case SIGUSR1:
|
|
talloc_report_full(g_tall_ctx, stderr);
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
int main(int argc, char **argv)
|
|
{
|
|
struct ufunc_handle ufh = (struct ufunc_handle) { 0, };
|
|
int rc;
|
|
|
|
g_tall_ctx = talloc_named_const(NULL, 0, "hub_main_functionfs");
|
|
msgb_talloc_ctx_init(g_tall_ctx, 0);
|
|
osmo_init_logging2(g_tall_ctx, &log_info);
|
|
|
|
signal(SIGUSR1, &signal_handler);
|
|
|
|
if (argc < 2) {
|
|
fprintf(stderr, "You have to specify the mount-path of the functionfs\n");
|
|
exit(2);
|
|
}
|
|
|
|
chdir(argv[1]);
|
|
rc = ep0_init(&ufh);
|
|
if (rc < 0) {
|
|
fprintf(stderr, "Error %d\n", rc);
|
|
exit(1);
|
|
}
|
|
|
|
while (1) {
|
|
osmo_select_main(0);
|
|
}
|
|
}
|