Things that are not obvious from the RNDIS specification, and cost real time to
find. Worth reading before changing rndis.c, dhcp.c or the transmit path.
drivers/net/usb/rndis_host.c in the Linux tree is a better reference than
Microsoft's specification — it encodes the device quirks that the spec does not.
- The control channel is not an endpoint. Commands go out as
SEND_ENCAPSULATED_COMMAND(bmRequestType 0x21,bRequest 0x00) on the communications interface's default control pipe; replies come back viaGET_ENCAPSULATED_RESPONSE(0xA1,0x01). - All
offsetfields in QUERY / SET / PACKET messages are measured from byte 8 of the message, not from its start. A packet header is 44 bytes, so itsdata_offsetis 36. - Nothing flows until
OID_GEN_CURRENT_PACKET_FILTERis set. This is the most common place a from-scratch implementation stalls: the link negotiates cleanly and then stays completely silent. - The host adopts the MAC from
OID_802_3_PERMANENT_ADDRESS. Android's gadget filters on destination MAC, so using the address it hands you is what Linux does too. - One bulk transfer may carry several chained packet messages. Walk the whole buffer rather than assuming one frame per transfer.
USB says a transfer that is an exact multiple of the endpoint's maximum packet size needs an explicit end-of-transfer marker, and a zero-length packet is the obvious choice. It breaks Android's gadget, with a symptom that points nowhere near the cause:
- the first 12–14 frames transmit normally
- every
WritePipeTOafterwards returnskIOUSBTransactionTimeout, forever - the phone never sends a single frame
- the control channel stays perfectly healthy throughout, and the phone
continues to report
media CONNECTED
The cause is on the gadget side. u_ether re-arms its receive ring from
process_rx_w(), which is only queued when rx_complete() sees a successful
unwrap. A zero-length packet arrives as its own empty transfer, fails
rndis_rm_hdr(), and the request returns to the free list without being
re-submitted. The ring drains to empty — roughly a dozen buffers, which is the
frame count — and the endpoint NAKs from then on. Nothing recovers it.
Linux avoids this: rndis_info omits FLAG_SEND_ZLP, so usbnet appends a single
padding byte instead. msg_len already carries the true length, so the extra
byte is ignored by the receiver.
From the same driver_info: FLAG_NO_SETINT.
Issuing SET_INTERFACE on the data interface looks like the natural way to make
the gadget re-run gether_connect() and refill that receive ring. It is not. It
wedges Android's gadget badly enough that the control endpoint stops answering
encapsulated commands entirely, and only re-enumeration clears it.
usb_reset_device() (-R) exists because of this.
RNDIS_MSG_HALT drops the gadget to RNDIS_UNINITIALIZED and calls
netif_carrier_off() plus netif_stop_queue(). Since the receive ring can only
be refilled from a completion, and completions need queued buffers, an emptied
ring stays dead until the netdev is bounced — so the next run wedges after a
frame or two. Linux sends HALT on unbind, but it re-binds cleanly afterwards;
this daemon does not, so HALT is opt-in (-H) rather than default.
ipconfig set <if> DHCP will not work here. It only manages interfaces that
are configured network services. Point it at a bare feth and IPConfiguration
claims the interface, fails, and deconfigures it — leaving it administratively
down after you just brought it up, with no error worth reading:
feth9: flags=8802<BROADCAST,SIMPLEX,MULTICAST> <- down
feth10: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST>
force_up() in net.c reads the flags back via SIOCGIFFLAGS after every
ifconfig up rather than trusting the exit code.
IP_BOUND_IF does not override destination routing for 255.255.255.255. A
DHCP DISCOVER sent through a socket bound to the tether interface departs via the
default route instead and never reaches the phone — silently, reporting a false
negative. This is why dhcp.c builds the Ethernet frame by hand and pushes it
out the bulk endpoint: layer 2 has no routing to lose, and it works before the
interface has an address, which is the point.
feth does not implement the IPv6 interface ioctls. SIOCGIFINFO_IN6
returns EINVAL, so there is no link-local address to test against.
Android usually serves 192.168.42.0/24 with the phone on .129. It is not a
guarantee — a Samsung on One UI was observed serving 10.253.124.0/24 with the
phone on .142.
Assuming the subnet is the single easiest way to lose an afternoon, because every symptom mimics a driver fault: ARP goes unanswered, DHCP gets no reply, and the phone appears mute — when in fact the host is simply on a different network. Hence DHCP-over-USB at startup, with a fallback that watches for the phone's own IPv4 traffic and takes an address beside it.
OID_GEN_MEDIA_CONNECT_STATUS reflects whether the gadget's netdev is up. It
says nothing about whether Android configured an address on it, started dnsmasq,
or installed NAT rules. A phone with mobile data off reports CONNECTED and
serves nothing.