]> git.ipfire.org Git - thirdparty/ipxe.git/commitdiff
[rndis] Add generic RNDIS device abstraction
authorMichael Brown <mcb30@ipxe.org>
Thu, 11 Dec 2014 17:13:38 +0000 (17:13 +0000)
committerMichael Brown <mcb30@ipxe.org>
Thu, 18 Dec 2014 14:46:38 +0000 (14:46 +0000)
RNDIS provides an abstraction of a network device on top of a generic
packet transmission mechanism.

Signed-off-by: Michael Brown <mcb30@ipxe.org>
src/include/ipxe/errfile.h
src/include/ipxe/netdevice.h
src/include/ipxe/rndis.h [new file with mode: 0644]
src/net/rndis.c [new file with mode: 0644]

index f809337ff24c393f1f466d9ea47c995de7ab6bd5..fe20b516f0361aeb39fde88929cf2758973ae6b0 100644 (file)
@@ -227,6 +227,7 @@ FILE_LICENCE ( GPL2_OR_LATER );
 #define ERRFILE_ping                   ( ERRFILE_NET | 0x003a0000 )
 #define ERRFILE_dhcpv6                 ( ERRFILE_NET | 0x003b0000 )
 #define ERRFILE_nfs_uri                        ( ERRFILE_NET | 0x003c0000 )
+#define ERRFILE_rndis                  ( ERRFILE_NET | 0x003d0000 )
 
 #define ERRFILE_image                ( ERRFILE_IMAGE | 0x00000000 )
 #define ERRFILE_elf                  ( ERRFILE_IMAGE | 0x00010000 )
index 95ad1cf1bcb3683296da1e6b809f5b05258e192d..f471e2519031e9b4e422a8b172aa65b2a3c3fbe8 100644 (file)
@@ -36,13 +36,12 @@ struct device;
 
 /** Maximum length of a link-layer header
  *
- * The longest currently-supported link-layer header is for 802.11: a
- * 24-byte frame header plus an 8-byte 802.3 LLC/SNAP header, plus a
- * possible 4-byte VLAN header.  (The IPoIB link-layer pseudo-header
- * doesn't actually include link-layer addresses; see ipoib.c for
- * details.)
+ * The longest currently-supported link-layer header is for RNDIS: an
+ * 8-byte RNDIS header, a 32-byte RNDIS packet message header, a
+ * 14-byte Ethernet header and a possible 4-byte VLAN header.  Round
+ * up to 64 bytes.
  */
-#define MAX_LL_HEADER_LEN 36
+#define MAX_LL_HEADER_LEN 64
 
 /** Maximum length of a network-layer address */
 #define MAX_NET_ADDR_LEN 16
diff --git a/src/include/ipxe/rndis.h b/src/include/ipxe/rndis.h
new file mode 100644 (file)
index 0000000..60ffd10
--- /dev/null
@@ -0,0 +1,348 @@
+#ifndef _IPXE_RNDIS_H
+#define _IPXE_RNDIS_H
+
+/** @file
+ *
+ * Remote Network Driver Interface Specification
+ *
+ */
+
+FILE_LICENCE ( GPL2_OR_LATER );
+
+#include <stdint.h>
+#include <ipxe/netdevice.h>
+#include <ipxe/iobuf.h>
+
+/** Maximum time to wait for a transaction to complete
+ *
+ * This is a policy decision.
+ */
+#define RNDIS_MAX_WAIT_MS 1000
+
+/** RNDIS message header */
+struct rndis_header {
+       /** Message type */
+       uint32_t type;
+       /** Message length */
+       uint32_t len;
+} __attribute__ (( packed ));
+
+/** RNDIS initialise message */
+#define RNDIS_INITIALIZE_MSG 0x00000002UL
+
+/** RNDIS initialise message */
+struct rndis_initialize_message {
+       /** Request ID */
+       uint32_t id;
+       /** Major version */
+       uint32_t major;
+       /** Minor version */
+       uint32_t minor;
+       /** Maximum transfer size */
+       uint32_t mtu;
+} __attribute__ (( packed ));
+
+/** RNDIS initialise completion */
+#define RNDIS_INITIALISE_CMPLT 0x80000002UL
+
+/** RNDIS initialise completion */
+struct rndis_initialise_completion {
+       /** Request ID */
+       uint32_t id;
+       /** Status */
+       uint32_t status;
+       /** Major version */
+       uint32_t major;
+       /** Minor version */
+       uint32_t minor;
+       /** Device flags */
+       uint32_t flags;
+       /** Medium */
+       uint32_t medium;
+       /** Maximum packets per transfer */
+       uint32_t max_pkts;
+       /** Maximum transfer size */
+       uint32_t mtu;
+       /** Packet alignment factor */
+       uint32_t align;
+       /** Reserved */
+       uint32_t reserved;
+} __attribute__ (( packed ));
+
+/** RNDIS halt message */
+#define RNIS_HALT_MSG 0x00000003UL
+
+/** RNDIS halt message */
+struct rndis_halt_message {
+       /** Request ID */
+       uint32_t id;
+} __attribute__ (( packed ));
+
+/** RNDIS query OID message */
+#define RNDIS_QUERY_MSG 0x00000004UL
+
+/** RNDIS set OID message */
+#define RNDIS_SET_MSG 0x00000005UL
+
+/** RNDIS query or set OID message */
+struct rndis_oid_message {
+       /** Request ID */
+       uint32_t id;
+       /** Object ID */
+       uint32_t oid;
+       /** Information buffer length */
+       uint32_t len;
+       /** Information buffer offset */
+       uint32_t offset;
+       /** Reserved */
+       uint32_t reserved;
+} __attribute__ (( packed ));
+
+/** RNDIS query OID completion */
+#define RNDIS_QUERY_CMPLT 0x80000004UL
+
+/** RNDIS query OID completion */
+struct rndis_query_completion {
+       /** Request ID */
+       uint32_t id;
+       /** Status */
+       uint32_t status;
+       /** Information buffer length */
+       uint32_t len;
+       /** Information buffer offset */
+       uint32_t offset;
+} __attribute__ (( packed ));
+
+/** RNDIS set OID completion */
+#define RNDIS_SET_CMPLT 0x80000005UL
+
+/** RNDIS set OID completion */
+struct rndis_set_completion {
+       /** Request ID */
+       uint32_t id;
+       /** Status */
+       uint32_t status;
+} __attribute__ (( packed ));
+
+/** RNDIS reset message */
+#define RNDIS_RESET_MSG 0x00000006UL
+
+/** RNDIS reset message */
+struct rndis_reset_message {
+       /** Reserved */
+       uint32_t reserved;
+} __attribute__ (( packed ));
+
+/** RNDIS reset completion */
+#define RNDIS_RESET_CMPLT 0x80000006UL
+
+/** RNDIS reset completion */
+struct rndis_reset_completion {
+       /** Status */
+       uint32_t status;
+       /** Addressing reset */
+       uint32_t addr;
+} __attribute__ (( packed ));
+
+/** RNDIS indicate status message */
+#define RNDIS_INDICATE_STATUS_MSG 0x00000007UL
+
+/** RNDIS diagnostic information */
+struct rndis_diagnostic_info {
+       /** Status */
+       uint32_t status;
+       /** Error offset */
+       uint32_t offset;
+} __attribute__ (( packed ));
+
+/** RNDIS indicate status message */
+struct rndis_indicate_status_message {
+       /** Status */
+       uint32_t status;
+       /** Status buffer length */
+       uint32_t len;
+       /** Status buffer offset */
+       uint32_t offset;
+       /** Diagnostic information (optional) */
+       struct rndis_diagnostic_info diag[0];
+} __attribute__ (( packed ));
+
+/** RNDIS status codes */
+enum rndis_status {
+       /** Device is connected to a network medium */
+       RNDIS_STATUS_MEDIA_CONNECT = 0x4001000bUL,
+       /** Device is disconnected from the medium */
+       RNDIS_STATUS_MEDIA_DISCONNECT = 0x4001000cUL,
+};
+
+/** RNDIS keepalive message */
+#define RNDIS_KEEPALIVE_MSG 0x00000008UL
+
+/** RNDIS keepalive message */
+struct rndis_keepalive_message {
+       /** Request ID */
+       uint32_t id;
+} __attribute__ (( packed ));
+
+/** RNDIS keepalive completion */
+#define RNDIS_KEEPALIVE_CMPLT 0x80000008UL
+
+/** RNDIS keepalive completion */
+struct rndis_keepalive_completion {
+       /** Request ID */
+       uint32_t id;
+       /** Status */
+       uint32_t status;
+} __attribute__ (( packed ));
+
+/** RNDIS packet message */
+#define RNDIS_PACKET_MSG 0x00000001UL
+
+/** RNDIS packet field */
+struct rndis_packet_field {
+       /** Offset */
+       uint32_t offset;
+       /** Length */
+       uint32_t len;
+} __attribute__ (( packed ));
+
+/** RNDIS packet message */
+struct rndis_packet_message {
+       /** Data */
+       struct rndis_packet_field data;
+       /** Out-of-band data records */
+       struct rndis_packet_field oob;
+       /** Number of out-of-band data records */
+       uint32_t oob_count;
+       /** Per-packet information record */
+       struct rndis_packet_field ppi;
+       /** Reserved */
+       uint32_t reserved;
+} __attribute__ (( packed ));
+
+/** RNDIS packet record */
+struct rndis_packet_record {
+       /** Length */
+       uint32_t len;
+       /** Type */
+       uint32_t type;
+       /** Offset */
+       uint32_t offset;
+} __attribute__ (( packed ));
+
+/** OID for packet filter */
+#define RNDIS_OID_GEN_CURRENT_PACKET_FILTER 0x0001010eUL
+
+/** Packet filter bits */
+enum rndis_packet_filter {
+       /** Unicast packets */
+       RNDIS_FILTER_UNICAST = 0x00000001UL,
+       /** Multicast packets */
+       RNDIS_FILTER_MULTICAST = 0x00000002UL,
+       /** All multicast packets */
+       RNDIS_FILTER_ALL_MULTICAST = 0x00000004UL,
+       /** Broadcast packets */
+       RNDIS_FILTER_BROADCAST = 0x00000008UL,
+       /** All packets */
+       RNDIS_FILTER_PROMISCUOUS = 0x00000020UL
+};
+
+/** OID for media status */
+#define RNDIS_OID_GEN_MEDIA_CONNECT_STATUS 0x00010114UL
+
+/** OID for permanent MAC address */
+#define RNDIS_OID_802_3_PERMANENT_ADDRESS 0x01010101UL
+
+/** OID for current MAC address */
+#define RNDIS_OID_802_3_CURRENT_ADDRESS        0x01010102UL
+
+struct rndis_device;
+
+/** RNDIS device operations */
+struct rndis_operations {
+       /**
+        * Open RNDIS device
+        *
+        * @v rndis             RNDIS device
+        * @ret rc              Return status code
+        */
+       int ( * open ) ( struct rndis_device *rndis );
+       /**
+        * Close RNDIS device
+        *
+        * @v rndis             RNDIS device
+        */
+       void ( * close ) ( struct rndis_device *rndis );
+       /**
+        * Transmit packet
+        *
+        * @v rndis             RNDIS device
+        * @v iobuf             I/O buffer
+        * @ret rc              Return status code
+        *
+        * If this method returns success then the RNDIS device must
+        * eventually report completion via rndis_tx_complete().
+        */
+       int ( * transmit ) ( struct rndis_device *rndis,
+                            struct io_buffer *iobuf );
+       /**
+        * Poll for completed and received packets
+        *
+        * @v rndis             RNDIS device
+        */
+       void ( * poll ) ( struct rndis_device *rndis );
+};
+
+/** An RNDIS device */
+struct rndis_device {
+       /** Network device */
+       struct net_device *netdev;
+       /** Device name */
+       const char *name;
+       /** RNDIS operations */
+       struct rndis_operations *op;
+       /** Driver private data */
+       void *priv;
+
+       /** Request ID for current blocking request */
+       unsigned int wait_id;
+       /** Return status code for current blocking request */
+       int wait_rc;
+};
+
+/**
+ * Initialise an RNDIS device
+ *
+ * @v rndis            RNDIS device
+ * @v op               RNDIS device operations
+ */
+static inline void rndis_init ( struct rndis_device *rndis,
+                               struct rndis_operations *op ) {
+
+       rndis->op = op;
+}
+
+extern void rndis_tx_complete_err ( struct rndis_device *rndis,
+                                   struct io_buffer *iobuf, int rc );
+extern int rndis_tx_defer ( struct rndis_device *rndis,
+                           struct io_buffer *iobuf );
+extern void rndis_rx ( struct rndis_device *rndis, struct io_buffer *iobuf );
+
+extern struct rndis_device * alloc_rndis ( size_t priv_len );
+extern int register_rndis ( struct rndis_device *rndis );
+extern void unregister_rndis ( struct rndis_device *rndis );
+extern void free_rndis ( struct rndis_device *rndis );
+
+/**
+ * Complete message transmission
+ *
+ * @v rndis            RNDIS device
+ * @v iobuf            I/O buffer
+ */
+static inline void rndis_tx_complete ( struct rndis_device *rndis,
+                                      struct io_buffer *iobuf ) {
+
+       rndis_tx_complete_err ( rndis, iobuf, 0 );
+}
+
+#endif /* _IPXE_RNDIS_H */
diff --git a/src/net/rndis.c b/src/net/rndis.c
new file mode 100644 (file)
index 0000000..4ad7b81
--- /dev/null
@@ -0,0 +1,850 @@
+/*
+ * Copyright (C) 2014 Michael Brown <mbrown@fensystems.co.uk>.
+ *
+ * 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., 51 Franklin Street, Fifth Floor, Boston, MA
+ * 02110-1301, USA.
+ */
+
+FILE_LICENCE ( GPL2_OR_LATER );
+
+/** @file
+ *
+ * Remote Network Driver Interface Specification
+ *
+ */
+
+#include <unistd.h>
+#include <string.h>
+#include <errno.h>
+#include <byteswap.h>
+#include <ipxe/iobuf.h>
+#include <ipxe/netdevice.h>
+#include <ipxe/ethernet.h>
+#include <ipxe/device.h>
+#include <ipxe/rndis.h>
+
+/**
+ * Allocate I/O buffer
+ *
+ * @v len              Length
+ * @ret iobuf          I/O buffer, or NULL
+ */
+static struct io_buffer * rndis_alloc_iob ( size_t len ) {
+       struct rndis_header *header;
+       struct io_buffer *iobuf;
+
+       /* Allocate I/O buffer and reserve space */
+       iobuf = alloc_iob ( sizeof ( *header ) + len );
+       if ( iobuf )
+               iob_reserve ( iobuf, sizeof ( *header ) );
+
+       return iobuf;
+}
+
+/**
+ * Transmit message
+ *
+ * @v rndis            RNDIS device
+ * @v iobuf            I/O buffer
+ * @v type             Message type
+ * @ret rc             Return status code
+ */
+static int rndis_tx_message ( struct rndis_device *rndis,
+                             struct io_buffer *iobuf, unsigned int type ) {
+       struct rndis_header *header;
+       int rc;
+
+       /* Prepend RNDIS header */
+       header = iob_push ( iobuf, sizeof ( *header ) );
+       header->type = cpu_to_le32 ( type );
+       header->len = cpu_to_le32 ( iob_len ( iobuf ) );
+
+       /* Transmit message */
+       if ( ( rc = rndis->op->transmit ( rndis, iobuf ) ) != 0 ) {
+               DBGC ( rndis, "RNDIS %s could not transmit: %s\n",
+                      rndis->name, strerror ( rc ) );
+               return rc;
+       }
+
+       return 0;
+}
+
+/**
+ * Complete message transmission
+ *
+ * @v rndis            RNDIS device
+ * @v iobuf            I/O buffer
+ * @v rc               Packet status code
+ */
+void rndis_tx_complete_err ( struct rndis_device *rndis,
+                            struct io_buffer *iobuf, int rc ) {
+       struct net_device *netdev = rndis->netdev;
+       struct rndis_header *header;
+       size_t len = iob_len ( iobuf );
+
+       /* Sanity check */
+       if ( len < sizeof ( *header ) ) {
+               DBGC ( rndis, "RNDIS %s completed underlength transmission:\n",
+                      rndis->name );
+               DBGC_HDA ( rndis, 0, iobuf->data, len );
+               netdev_tx_err ( netdev, NULL, -EINVAL );
+               return;
+       }
+       header = iobuf->data;
+
+       /* Complete buffer */
+       if ( header->type == cpu_to_le32 ( RNDIS_PACKET_MSG ) ) {
+               netdev_tx_complete_err ( netdev, iobuf, rc );
+       } else {
+               free_iob ( iobuf );
+       }
+}
+
+/**
+ * Transmit data packet
+ *
+ * @v rndis            RNDIS device
+ * @v iobuf            I/O buffer
+ * @ret rc             Return status code
+ */
+static int rndis_tx_data ( struct rndis_device *rndis,
+                          struct io_buffer *iobuf ) {
+       struct rndis_packet_message *msg;
+       size_t len = iob_len ( iobuf );
+       int rc;
+
+       /* Prepend packet message header */
+       msg = iob_push ( iobuf, sizeof ( *msg ) );
+       memset ( msg, 0, sizeof ( *msg ) );
+       msg->data.offset = cpu_to_le32 ( sizeof ( *msg ) );
+       msg->data.len = cpu_to_le32 ( len );
+
+       /* Transmit message */
+       if ( ( rc = rndis_tx_message ( rndis, iobuf, RNDIS_PACKET_MSG ) ) != 0 )
+               return rc;
+
+       return 0;
+}
+
+/**
+ * Defer transmitted packet
+ *
+ * @v rndis            RNDIS device
+ * @v iobuf            I/O buffer
+ * @ret rc             Return status code
+ *
+ * As with netdev_tx_defer(), the caller must ensure that space in the
+ * transmit descriptor ring is freed up before calling
+ * rndis_tx_complete().
+ *
+ * Unlike netdev_tx_defer(), this call may fail.
+ */
+int rndis_tx_defer ( struct rndis_device *rndis, struct io_buffer *iobuf ) {
+       struct net_device *netdev = rndis->netdev;
+       struct rndis_header *header;
+       struct rndis_packet_message *msg;
+
+       /* Fail unless this was a packet message.  Only packet
+        * messages correspond to I/O buffers in the network device's
+        * TX queue; other messages cannot be deferred in this way.
+        */
+       assert ( iob_len ( iobuf ) >= sizeof ( *header ) );
+       header = iobuf->data;
+       if ( header->type != cpu_to_le32 ( RNDIS_PACKET_MSG ) )
+               return -ENOTSUP;
+
+       /* Strip RNDIS header and packet message header, to return
+        * this packet to the state in which we received it.
+        */
+       iob_pull ( iobuf, ( sizeof ( *header ) + sizeof ( *msg ) ) );
+
+       /* Defer packet */
+       netdev_tx_defer ( netdev, iobuf );
+
+       return 0;
+}
+
+/**
+ * Receive data packet
+ *
+ * @v rndis            RNDIS device
+ * @v iobuf            I/O buffer
+ */
+static void rndis_rx_data ( struct rndis_device *rndis,
+                           struct io_buffer *iobuf ) {
+       struct net_device *netdev = rndis->netdev;
+       struct rndis_packet_message *msg;
+       size_t len = iob_len ( iobuf );
+       size_t data_offset;
+       size_t data_len;
+       int rc;
+
+       /* Sanity check */
+       if ( len < sizeof ( *msg ) ) {
+               DBGC ( rndis, "RNDIS %s received underlength data packet:\n",
+                      rndis->name );
+               DBGC_HDA ( rndis, 0, iobuf->data, len );
+               rc = -EINVAL;
+               goto err_len;
+       }
+       msg = iobuf->data;
+
+       /* Locate and sanity check data buffer */
+       data_offset = le32_to_cpu ( msg->data.offset );
+       data_len = le32_to_cpu ( msg->data.len );
+       if ( ( data_offset > len ) || ( data_len > ( len - data_offset ) ) ) {
+               DBGC ( rndis, "RNDIS %s data packet data exceeds packet:\n",
+                      rndis->name );
+               DBGC_HDA ( rndis, 0, iobuf->data, len );
+               rc = -EINVAL;
+               goto err_data;
+       }
+
+       /* Strip non-data portions */
+       iob_pull ( iobuf, data_offset );
+       iob_unput ( iobuf, ( iob_len ( iobuf ) - data_len ) );
+
+       /* Hand off to network stack */
+       netdev_rx ( netdev, iob_disown ( iobuf ) );
+
+       return;
+
+ err_data:
+ err_len:
+       /* Report error to network stack */
+       netdev_rx_err ( netdev, iob_disown ( iobuf ), rc );
+}
+
+/**
+ * Transmit OID message
+ *
+ * @v rndis            RNDIS device
+ * @v oid              Object ID
+ * @v data             New OID value (or NULL to query current value)
+ * @v len              Length of new OID value
+ * @ret rc             Return status code
+ */
+static int rndis_tx_oid ( struct rndis_device *rndis, unsigned int oid,
+                         const void *data, size_t len ) {
+       struct io_buffer *iobuf;
+       struct rndis_oid_message *msg;
+       unsigned int type;
+       int rc;
+
+       /* Allocate I/O buffer */
+       iobuf = rndis_alloc_iob ( sizeof ( *msg ) + len );
+       if ( ! iobuf ) {
+               rc = -ENOMEM;
+               goto err_alloc;
+       }
+
+       /* Construct message.  We use the OID as the request ID. */
+       msg = iob_put ( iobuf, sizeof ( *msg ) );
+       memset ( msg, 0, sizeof ( *msg ) );
+       msg->id = oid; /* Non-endian */
+       msg->oid = cpu_to_le32 ( oid );
+       msg->offset = cpu_to_le32 ( sizeof ( *msg ) );
+       msg->len = cpu_to_le32 ( len );
+       memcpy ( iob_put ( iobuf, len ), data, len );
+
+       /* Transmit message */
+       type = ( data ? RNDIS_SET_MSG : RNDIS_QUERY_MSG );
+       if ( ( rc = rndis_tx_message ( rndis, iobuf, type ) ) != 0 )
+               goto err_tx;
+
+       return 0;
+
+ err_tx:
+       free_iob ( iobuf );
+ err_alloc:
+       return rc;
+}
+
+/**
+ * Receive query OID completion
+ *
+ * @v rndis            RNDIS device
+ * @v iobuf            I/O buffer
+ */
+static void rndis_rx_query_oid ( struct rndis_device *rndis,
+                                struct io_buffer *iobuf ) {
+       struct net_device *netdev = rndis->netdev;
+       struct rndis_query_completion *cmplt;
+       size_t len = iob_len ( iobuf );
+       size_t info_offset;
+       size_t info_len;
+       unsigned int id;
+       void *info;
+       uint32_t *link_status;
+       int rc;
+
+       /* Sanity check */
+       if ( len < sizeof ( *cmplt ) ) {
+               DBGC ( rndis, "RNDIS %s received underlength query "
+                      "completion:\n", rndis->name );
+               DBGC_HDA ( rndis, 0, iobuf->data, len );
+               rc = -EINVAL;
+               goto err_len;
+       }
+       cmplt = iobuf->data;
+
+       /* Extract request ID */
+       id = cmplt->id; /* Non-endian */
+
+       /* Check status */
+       if ( cmplt->status ) {
+               DBGC ( rndis, "RNDIS %s received query completion failure "
+                      "%#08x\n", rndis->name, le32_to_cpu ( cmplt->status ) );
+               DBGC_HDA ( rndis, 0, iobuf->data, len );
+               rc = -EIO;
+               goto err_status;
+       }
+
+       /* Locate and sanity check information buffer */
+       info_offset = le32_to_cpu ( cmplt->offset );
+       info_len = le32_to_cpu ( cmplt->len );
+       if ( ( info_offset > len ) || ( info_len > ( len - info_offset ) ) ) {
+               DBGC ( rndis, "RNDIS %s query completion information exceeds "
+                      "packet:\n", rndis->name );
+               DBGC_HDA ( rndis, 0, iobuf->data, len );
+               rc = -EINVAL;
+               goto err_info;
+       }
+       info = ( ( ( void * ) cmplt ) + info_offset );
+
+       /* Handle OID */
+       switch ( id ) {
+
+       case RNDIS_OID_802_3_PERMANENT_ADDRESS:
+               if ( info_len > sizeof ( netdev->hw_addr ) )
+                       info_len = sizeof ( netdev->hw_addr );
+               memcpy ( netdev->hw_addr, info, info_len );
+               break;
+
+       case RNDIS_OID_802_3_CURRENT_ADDRESS:
+               if ( info_len > sizeof ( netdev->ll_addr ) )
+                       info_len = sizeof ( netdev->ll_addr );
+               memcpy ( netdev->ll_addr, info, info_len );
+               break;
+
+       case RNDIS_OID_GEN_MEDIA_CONNECT_STATUS:
+               if ( info_len != sizeof ( *link_status ) ) {
+                       DBGC ( rndis, "RNDIS %s invalid link status:\n",
+                              rndis->name );
+                       DBGC_HDA ( rndis, 0, iobuf->data, len );
+                       rc = -EPROTO;
+                       goto err_link_status;
+               }
+               link_status = info;
+               if ( *link_status == 0 ) {
+                       DBGC ( rndis, "RNDIS %s link is up\n", rndis->name );
+                       netdev_link_up ( netdev );
+               } else {
+                       DBGC ( rndis, "RNDIS %s link is down: %#08x\n",
+                              rndis->name, le32_to_cpu ( *link_status ) );
+                       netdev_link_down ( netdev );
+               }
+               break;
+
+       default:
+               DBGC ( rndis, "RNDIS %s unexpected query completion ID %#08x\n",
+                      rndis->name, id );
+               DBGC_HDA ( rndis, 0, iobuf->data, len );
+               rc = -EPROTO;
+               goto err_id;
+       }
+
+       /* Success */
+       rc = 0;
+
+ err_id:
+ err_link_status:
+ err_info:
+ err_status:
+       /* Record completion result if applicable */
+       if ( id == rndis->wait_id ) {
+               rndis->wait_id = 0;
+               rndis->wait_rc = rc;
+       }
+ err_len:
+       /* Free I/O buffer */
+       free_iob ( iobuf );
+}
+
+/**
+ * Receive set OID completion
+ *
+ * @v rndis            RNDIS device
+ * @v iobuf            I/O buffer
+ */
+static void rndis_rx_set_oid ( struct rndis_device *rndis,
+                              struct io_buffer *iobuf ) {
+       struct rndis_set_completion *cmplt;
+       size_t len = iob_len ( iobuf );
+       unsigned int id;
+       int rc;
+
+       /* Sanity check */
+       if ( len < sizeof ( *cmplt ) ) {
+               DBGC ( rndis, "RNDIS %s received underlength set completion:\n",
+                      rndis->name );
+               DBGC_HDA ( rndis, 0, iobuf->data, len );
+               rc = -EINVAL;
+               goto err_len;
+       }
+       cmplt = iobuf->data;
+
+       /* Extract request ID */
+       id = cmplt->id; /* Non-endian */
+
+       /* Check status */
+       if ( cmplt->status ) {
+               DBGC ( rndis, "RNDIS %s received set completion failure "
+                      "%#08x\n", rndis->name, le32_to_cpu ( cmplt->status ) );
+               DBGC_HDA ( rndis, 0, iobuf->data, len );
+               rc = -EIO;
+               goto err_status;
+       }
+
+       /* Success */
+       rc = 0;
+
+ err_status:
+       /* Record completion result if applicable */
+       if ( id == rndis->wait_id ) {
+               rndis->wait_id = 0;
+               rndis->wait_rc = rc;
+       }
+ err_len:
+       /* Free I/O buffer */
+       free_iob ( iobuf );
+}
+
+/**
+ * Query or set OID
+ *
+ * @v rndis            RNDIS device
+ * @v oid              Object ID
+ * @v data             New OID value (or NULL to query current value)
+ * @v len              Length of new OID value
+ * @ret rc             Return status code
+ */
+static int rndis_oid ( struct rndis_device *rndis, unsigned int oid,
+                      const void *data, size_t len ) {
+       unsigned int i;
+       int rc;
+
+       /* Transmit query */
+       if ( ( rc = rndis_tx_oid ( rndis, oid, data, len ) ) != 0 )
+               return rc;
+
+       /* Record query ID */
+       rndis->wait_id = oid;
+
+       /* Wait for operation to complete */
+       for ( i = 0 ; i < RNDIS_MAX_WAIT_MS ; i++ ) {
+
+               /* Check for completion */
+               if ( ! rndis->wait_id )
+                       return rndis->wait_rc;
+
+               /* Poll RNDIS device */
+               rndis->op->poll ( rndis );
+
+               /* Delay for 1ms */
+               mdelay ( 1 );
+       }
+
+       DBGC ( rndis, "RNDIS %s timed out waiting for OID %#08x\n",
+              rndis->name, oid );
+       return -ETIMEDOUT;
+}
+
+/**
+ * Receive indicate status message
+ *
+ * @v rndis            RNDIS device
+ * @v iobuf            I/O buffer
+ */
+static void rndis_rx_status ( struct rndis_device *rndis,
+                             struct io_buffer *iobuf ) {
+       struct net_device *netdev = rndis->netdev;
+       struct rndis_indicate_status_message *msg;
+       size_t len = iob_len ( iobuf );
+       unsigned int status;
+       int rc;
+
+       /* Sanity check */
+       if ( len < sizeof ( *msg ) ) {
+               DBGC ( rndis, "RNDIS %s received underlength status message:\n",
+                      rndis->name );
+               DBGC_HDA ( rndis, 0, iobuf->data, len );
+               rc = -EINVAL;
+               goto err_len;
+       }
+       msg = iobuf->data;
+
+       /* Extract status */
+       status = le32_to_cpu ( msg->status );
+
+       /* Handle status */
+       switch ( msg->status ) {
+
+       case RNDIS_STATUS_MEDIA_CONNECT:
+               DBGC ( rndis, "RNDIS %s link is up\n", rndis->name );
+               netdev_link_up ( netdev );
+               break;
+
+       case RNDIS_STATUS_MEDIA_DISCONNECT:
+               DBGC ( rndis, "RNDIS %s link is down\n", rndis->name );
+               netdev_link_down ( netdev );
+               break;
+
+       default:
+               DBGC ( rndis, "RNDIS %s unexpected status %#08x:\n",
+                      rndis->name, status );
+               DBGC_HDA ( rndis, 0, iobuf->data, len );
+               rc = -ENOTSUP;
+               goto err_status;
+       }
+
+       /* Free I/O buffer */
+       free_iob ( iobuf );
+
+       return;
+
+ err_status:
+ err_len:
+       /* Report error via network device statistics */
+       netdev_rx_err ( netdev, iobuf, rc );
+}
+
+/**
+ * Receive RNDIS message
+ *
+ * @v rndis            RNDIS device
+ * @v iobuf            I/O buffer
+ * @v type             Message type
+ */
+static void rndis_rx_message ( struct rndis_device *rndis,
+                              struct io_buffer *iobuf, unsigned int type ) {
+       struct net_device *netdev = rndis->netdev;
+       int rc;
+
+       /* Handle packet */
+       switch ( type ) {
+
+       case RNDIS_PACKET_MSG:
+               rndis_rx_data ( rndis, iob_disown ( iobuf ) );
+               break;
+
+       case RNDIS_QUERY_CMPLT:
+               rndis_rx_query_oid ( rndis, iob_disown ( iobuf ) );
+               break;
+
+       case RNDIS_SET_CMPLT:
+               rndis_rx_set_oid ( rndis, iob_disown ( iobuf ) );
+               break;
+
+       case RNDIS_INDICATE_STATUS_MSG:
+               rndis_rx_status ( rndis, iob_disown ( iobuf ) );
+               break;
+
+       default:
+               DBGC ( rndis, "RNDIS %s received unexpected type %#08x\n",
+                      rndis->name, type );
+               DBGC_HDA ( rndis, 0, iobuf->data, iob_len ( iobuf ) );
+               rc = -EPROTO;
+               goto err_type;
+       }
+
+       return;
+
+ err_type:
+       /* Report error via network device statistics */
+       netdev_rx_err ( netdev, iobuf, rc );
+}
+
+/**
+ * Receive packet from underlying transport layer
+ *
+ * @v rndis            RNDIS device
+ * @v iobuf            I/O buffer, or NULL if allocation failed
+ */
+void rndis_rx ( struct rndis_device *rndis, struct io_buffer *iobuf ) {
+       struct net_device *netdev = rndis->netdev;
+       struct rndis_header *header;
+       struct io_buffer *msg;
+       size_t len;
+       unsigned int type;
+       int rc;
+
+       /* Record dropped packet if I/O buffer is missing */
+       if ( ! iobuf ) {
+               DBGC2 ( rndis, "RNDIS %s received dropped packet\n",
+                       rndis->name );
+               rc = -ENOBUFS;
+               goto drop;
+       }
+
+       /* Split packet into messages */
+       while ( iobuf ) {
+
+               /* Sanity check */
+               if ( iob_len ( iobuf ) < sizeof ( *header ) ) {
+                       DBGC ( rndis, "RNDIS %s received underlength packet:\n",
+                              rndis->name );
+                       DBGC_HDA ( rndis, 0, iobuf->data, iob_len ( iobuf ) );
+                       rc = -EINVAL;
+                       goto drop;
+               }
+               header = iobuf->data;
+
+               /* Parse and check header */
+               type = le32_to_cpu ( header->type );
+               len = le32_to_cpu ( header->len );
+               if ( len > iob_len ( iobuf ) ) {
+                       DBGC ( rndis, "RNDIS %s received underlength packet:\n",
+                              rndis->name );
+                       DBGC_HDA ( rndis, 0, iobuf->data, iob_len ( iobuf ) );
+                       rc = -EINVAL;
+                       goto drop;
+               }
+
+               /* Split buffer if required */
+               if ( len < iob_len ( iobuf ) ) {
+                       msg = iob_split ( iobuf, len );
+                       if ( ! msg ) {
+                               rc = -ENOMEM;
+                               goto drop;
+                       }
+               } else {
+                       msg = iobuf;
+                       iobuf = NULL;
+               }
+
+               /* Strip header */
+               iob_pull ( msg, sizeof ( *header ) );
+
+               /* Handle message */
+               rndis_rx_message ( rndis, iob_disown ( msg ), type );
+       }
+
+       return;
+
+ drop:
+       /* Record error */
+       netdev_rx_err ( netdev, iob_disown ( iobuf ), rc );
+}
+
+/**
+ * Open network device
+ *
+ * @v netdev           Network device
+ * @ret rc             Return status code
+ */
+static int rndis_open ( struct net_device *netdev ) {
+       struct rndis_device *rndis = netdev->priv;
+       uint32_t filter;
+       int rc;
+
+       /* Open RNDIS device */
+       if ( ( rc = rndis->op->open ( rndis ) ) != 0 ) {
+               DBGC ( rndis, "RNDIS %s could not open: %s\n",
+                      rndis->name, strerror ( rc ) );
+               goto err_open;
+       }
+
+       /* Set receive filter */
+       filter = cpu_to_le32 ( RNDIS_FILTER_UNICAST |
+                              RNDIS_FILTER_MULTICAST |
+                              RNDIS_FILTER_ALL_MULTICAST |
+                              RNDIS_FILTER_BROADCAST |
+                              RNDIS_FILTER_PROMISCUOUS );
+       if ( ( rc = rndis_oid ( rndis, RNDIS_OID_GEN_CURRENT_PACKET_FILTER,
+                               &filter, sizeof ( filter ) ) ) != 0 ) {
+               DBGC ( rndis, "RNDIS %s could not set receive filter: %s\n",
+                      rndis->name, strerror ( rc ) );
+               goto err_set_filter;
+       }
+
+       /* Update link status */
+       if ( ( rc = rndis_oid ( rndis, RNDIS_OID_GEN_MEDIA_CONNECT_STATUS,
+                               NULL, 0 ) ) != 0 )
+               goto err_query_link;
+
+       return 0;
+
+ err_query_link:
+ err_set_filter:
+       rndis->op->close ( rndis );
+ err_open:
+       return rc;
+}
+
+/**
+ * Close network device
+ *
+ * @v netdev           Network device
+ */
+static void rndis_close ( struct net_device *netdev ) {
+       struct rndis_device *rndis = netdev->priv;
+
+       /* Close RNDIS device */
+       rndis->op->close ( rndis );
+}
+
+/**
+ * Transmit packet
+ *
+ * @v netdev           Network device
+ * @v iobuf            I/O buffer
+ * @ret rc             Return status code
+ */
+static int rndis_transmit ( struct net_device *netdev,
+                           struct io_buffer *iobuf ) {
+       struct rndis_device *rndis = netdev->priv;
+
+       /* Transmit data packet */
+       return rndis_tx_data ( rndis, iobuf );
+}
+
+/**
+ * Poll for completed and received packets
+ *
+ * @v netdev           Network device
+ */
+static void rndis_poll ( struct net_device *netdev ) {
+       struct rndis_device *rndis = netdev->priv;
+
+       /* Poll RNDIS device */
+       rndis->op->poll ( rndis );
+}
+
+/** Network device operations */
+static struct net_device_operations rndis_operations = {
+       .open           = rndis_open,
+       .close          = rndis_close,
+       .transmit       = rndis_transmit,
+       .poll           = rndis_poll,
+};
+
+/**
+ * Allocate RNDIS device
+ *
+ * @v priv_len         Length of private data
+ * @ret rndis          RNDIS device, or NULL on allocation failure
+ */
+struct rndis_device * alloc_rndis ( size_t priv_len ) {
+       struct net_device *netdev;
+       struct rndis_device *rndis;
+
+       /* Allocate and initialise structure */
+       netdev = alloc_etherdev ( sizeof ( *rndis ) + priv_len );
+       if ( ! netdev )
+               return NULL;
+       netdev_init ( netdev, &rndis_operations );
+       rndis = netdev->priv;
+       rndis->netdev = netdev;
+       rndis->priv = ( ( ( void * ) rndis ) + sizeof ( *rndis ) );
+
+       return rndis;
+}
+
+/**
+ * Register RNDIS device
+ *
+ * @v rndis            RNDIS device
+ * @ret rc             Return status code
+ *
+ * Note that this routine will open and use the RNDIS device in order
+ * to query the MAC address.  The device must be immediately ready for
+ * use prior to registration.
+ */
+int register_rndis ( struct rndis_device *rndis ) {
+       struct net_device *netdev = rndis->netdev;
+       int rc;
+
+       /* Assign device name (for debugging) */
+       rndis->name = netdev->dev->name;
+
+       /* Register network device */
+       if ( ( rc = register_netdev ( netdev ) ) != 0 ) {
+               DBGC ( rndis, "RNDIS %s could not register: %s\n",
+                      rndis->name, strerror ( rc ) );
+               goto err_register;
+       }
+
+       /* Open RNDIS device to read MAC addresses */
+       if ( ( rc = rndis->op->open ( rndis ) ) != 0 ) {
+               DBGC ( rndis, "RNDIS %s could not open: %s\n",
+                      rndis->name, strerror ( rc ) );
+               goto err_open;
+       }
+
+       /* Query permanent MAC address */
+       if ( ( rc = rndis_oid ( rndis, RNDIS_OID_802_3_PERMANENT_ADDRESS,
+                               NULL, 0 ) ) != 0 )
+               goto err_query_permanent;
+
+       /* Query current MAC address */
+       if ( ( rc = rndis_oid ( rndis, RNDIS_OID_802_3_CURRENT_ADDRESS,
+                               NULL, 0 ) ) != 0 )
+               goto err_query_current;
+
+       /* Get link status */
+       if ( ( rc = rndis_oid ( rndis, RNDIS_OID_GEN_MEDIA_CONNECT_STATUS,
+                               NULL, 0 ) ) != 0 )
+               goto err_query_link;
+
+       /* Close RNDIS device */
+       rndis->op->close ( rndis );
+
+       return 0;
+
+ err_query_link:
+ err_query_current:
+ err_query_permanent:
+       rndis->op->close ( rndis );
+ err_open:
+       unregister_netdev ( netdev );
+ err_register:
+       return rc;
+}
+
+/**
+ * Unregister RNDIS device
+ *
+ * @v rndis            RNDIS device
+ */
+void unregister_rndis ( struct rndis_device *rndis ) {
+       struct net_device *netdev = rndis->netdev;
+
+       /* Unregister network device */
+       unregister_netdev ( netdev );
+}
+
+/**
+ * Free RNDIS device
+ *
+ * @v rndis            RNDIS device
+ */
+void free_rndis ( struct rndis_device *rndis ) {
+       struct net_device *netdev = rndis->netdev;
+
+       /* Free network device */
+       netdev_nullify ( netdev );
+       netdev_put ( netdev );
+}