Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions internal/daemon/relay.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ func NewRelay(ctx context.Context, dev *usb.Device, tun tun.Interface, phoneMAC
return nil, err
}

// Opening the data interface clears the RNDIS packet filter on some
// devices, so it has to be enabled again before data can flow.
if err := rndis.NewSession(dev).ReapplyFilter(); err != nil {
return nil, err
}

ctx, cancel := context.WithCancel(ctx)
return &Relay{
dev: dev,
Expand Down
37 changes: 21 additions & 16 deletions internal/rndis/messages.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ type CommonHeader struct {

// RemoteNdisInitializeMsg is sent by Host -> Device to start the handshake.
type RemoteNdisInitializeMsg struct {
MessageType uint32 // MsgInit (0x02)
MessageLength uint32 // 24 bytes
RequestID uint32
MajorVersion uint32 // 1
MinorVersion uint32 // 0
MaxTransferSz uint32 // Recommended: 16384 (16KB)
MessageType uint32 // MsgInit (0x02)
MessageLength uint32 // 24 bytes
RequestID uint32
MajorVersion uint32 // 1
MinorVersion uint32 // 0
MaxTransferSz uint32 // Recommended: 16384 (16KB)
}

func (m *RemoteNdisInitializeMsg) Marshal() []byte {
Expand Down Expand Up @@ -66,8 +66,8 @@ func UnmarshalInitializeCmplt(data []byte) (*RemoteNdisInitializeCmplt, error) {

// RemoteNdisQueryMsg is sent by Host -> Device to query OIDs (e.g., getting MAC address).
type RemoteNdisQueryMsg struct {
RequestID uint32
OID uint32
RequestID uint32
OID uint32
InformationBufferLength uint32 // for simple queries, usually 0
}

Expand All @@ -77,9 +77,9 @@ func (m *RemoteNdisQueryMsg) Marshal() []byte {
binary.LittleEndian.PutUint32(b[4:8], 28)
binary.LittleEndian.PutUint32(b[8:12], m.RequestID)
binary.LittleEndian.PutUint32(b[12:16], m.OID)
binary.LittleEndian.PutUint32(b[16:20], 0) // Info Buffer Length
binary.LittleEndian.PutUint32(b[16:20], 0) // Info Buffer Length
binary.LittleEndian.PutUint32(b[20:24], 20) // Info Buffer Offset (offset from RequestID field, which is 8 index + 12 = 20)
binary.LittleEndian.PutUint32(b[24:28], 0) // Device Context
binary.LittleEndian.PutUint32(b[24:28], 0) // Device Context
return b
}

Expand Down Expand Up @@ -148,19 +148,24 @@ func UnmarshalSetCmplt(data []byte) (uint32, uint32, error) { // returns Request
}

// EncapsulatePacket wraps a raw Ethernet packet with an RNDIS header.
//
// The header is the standard 44-byte REMOTE_NDIS_PACKET_MSG with a DataOffset
// of 36, matching what the in-tree Linux host driver (rndis_host.c) sends.
// Some Android RNDIS gadgets (e.g. vivo/MediaTek) drop frames that pad the
// header beyond those 44 bytes and surface them as receive errors.
func EncapsulatePacket(packet []byte) []byte {
headerLen := 48 // Use 48 bytes for 8-byte alignment (44 + 4 padding)
headerLen := 44 // Full REMOTE_NDIS_PACKET_MSG header; data starts right after it.
totalLen := headerLen + len(packet)

b := make([]byte, totalLen)
binary.LittleEndian.PutUint32(b[0:4], MsgPacket)
binary.LittleEndian.PutUint32(b[4:8], uint32(totalLen))
binary.LittleEndian.PutUint32(b[8:12], 40) // DataOffset (relative to byte 8: 8 + 40 = 48)
binary.LittleEndian.PutUint32(b[8:12], 36) // DataOffset (relative to byte 8: 8 + 36 = 44)
binary.LittleEndian.PutUint32(b[12:16], uint32(len(packet)))
// Bytes 16-47 are reserved/optional fields (offset/length for OOB data, info, etc.)
// Bytes 16-43 are reserved/optional fields (offset/length for OOB data, info, etc.)
// We leave them zeroed.
copy(b[48:], packet)

copy(b[44:], packet)
return b
}

Expand Down
59 changes: 59 additions & 0 deletions internal/rndis/messages_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package rndis

import (
"bytes"
"encoding/binary"
"testing"
)

// TestEncapsulatePacketLayout locks the wire format to the canonical
// REMOTE_NDIS_PACKET_MSG layout used by the Linux host driver: a 44-byte
// header, DataOffset 36 (relative to the start of RequestId) and the
// Ethernet frame immediately after the header.
func TestEncapsulatePacketLayout(t *testing.T) {
frame := []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x02, 0x00, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, 0x00}
pkt := EncapsulatePacket(frame)

if len(pkt) != 44+len(frame) {
t.Fatalf("packet length = %d, want %d", len(pkt), 44+len(frame))
}
if got := binary.LittleEndian.Uint32(pkt[0:4]); got != MsgPacket {
t.Errorf("MessageType = 0x%08X, want 0x%08X", got, MsgPacket)
}
if got := binary.LittleEndian.Uint32(pkt[4:8]); got != uint32(len(pkt)) {
t.Errorf("MessageLength = %d, want %d", got, len(pkt))
}
if got := binary.LittleEndian.Uint32(pkt[8:12]); got != 36 {
t.Errorf("DataOffset = %d, want 36", got)
}
if got := binary.LittleEndian.Uint32(pkt[12:16]); got != uint32(len(frame)) {
t.Errorf("DataLength = %d, want %d", got, len(frame))
}
if !bytes.Equal(pkt[44:], frame) {
t.Errorf("payload = %x, want %x", pkt[44:], frame)
}
for i := 16; i < 44; i++ {
if pkt[i] != 0 {
t.Errorf("reserved byte %d = 0x%02X, want 0", i, pkt[i])
}
}
}

// TestDecapsulatePacket checks that the canonical layout round-trips through
// the decoder and that truncated input is rejected.
func TestDecapsulatePacket(t *testing.T) {
frame := []byte{0x02, 0x00, 0x00, 0x00, 0x00, 0x01, 0x42, 0x86, 0x6b, 0x49, 0x67, 0x24, 0x08, 0x06, 0x01, 0x02}
pkt := EncapsulatePacket(frame)

got, err := DecapsulatePacket(pkt)
if err != nil {
t.Fatalf("DecapsulatePacket: %v", err)
}
if !bytes.Equal(got, frame) {
t.Errorf("round-trip payload = %x, want %x", got, frame)
}

if _, err := DecapsulatePacket(pkt[:20]); err == nil {
t.Error("expected error for truncated packet")
}
}
44 changes: 43 additions & 1 deletion internal/rndis/rndis.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ func NewSession(dev *usb.Device) *Session {
func (s *Session) Handshake() ([]byte, error) {
log.Info().Str("component", "rndis").Msg("Starting RNDIS handshake...")

// 0. Drain any stale encapsulated responses left on the control pipe by a
// previous host session. Without this the handshake can desync: the first
// read would consume an old completion and every subsequent read would be
// off by one message.
s.flushResponses()

// 1. Send INIT
s.requestID++
initMsg := &RemoteNdisInitializeMsg{
Expand Down Expand Up @@ -109,6 +115,29 @@ func (s *Session) Handshake() ([]byte, error) {
return macCmplt.Payload, nil
}

// ReapplyFilter re-sends OID_GEN_CURRENT_PACKET_FILTER to enable data flow.
//
// Some RNDIS gadgets (e.g. vivo/MediaTek) reset the packet filter to zero when
// the data interface is re-selected with SET_INTERFACE, which makes the phone
// silently drop all of its own broadcast traffic — DHCP offers included.
// Re-applying the filter after the data interface has been opened restores it.
func (s *Session) ReapplyFilter() error {
s.requestID++
setFilter := &RemoteNdisSetMsg{
RequestID: s.requestID,
OID: OID_GEN_CURRENT_PACKET_FILTER,
Value: PacketTypeDirected | PacketTypeBroadcast | PacketTypeAllMulticast,
}
if err := s.sendControl(setFilter.Marshal()); err != nil {
return fmt.Errorf("rndis: failed to re-apply packet filter: %w", err)
}
if _, err := s.receiveControl(); err != nil {
log.Warn().Str("component", "rndis").Err(err).Msg("No SET_CMPLT after re-applying packet filter")
}
log.Info().Str("component", "rndis").Msg("Packet filter re-applied after opening data interface")
return nil
}

// KeepAlive sends a REMOTE_NDIS_KEEPALIVE_MSG to keep the connection active.
func (s *Session) KeepAlive() error {
s.requestID++
Expand Down Expand Up @@ -144,7 +173,7 @@ func (s *Session) sendControl(data []byte) error {
// receiveControl retrieves an encapsulated RNDIS response.
func (s *Session) receiveControl() ([]byte, error) {
buf := make([]byte, 2048)

// Direct polling with generous retries for macOS (50 * 20ms = 1s total)
for i := 0; i < 50; i++ {
time.Sleep(20 * time.Millisecond)
Expand All @@ -155,3 +184,16 @@ func (s *Session) receiveControl() ([]byte, error) {
}
return nil, fmt.Errorf("rndis: timeout waiting for control response")
}

// flushResponses discards any encapsulated responses still queued on the
// device, so the next handshake starts from a clean slate.
func (s *Session) flushResponses() {
buf := make([]byte, 2048)
for i := 0; i < 16; i++ {
n, err := s.dev.Control(0xA1, 0x01, 0, uint16(s.dev.InterfaceNum), buf)
if err != nil || n == 0 {
return
}
time.Sleep(5 * time.Millisecond)
}
}
25 changes: 23 additions & 2 deletions internal/usb/device.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ package usb

import (
"fmt"
"time"
"github.com/google/gousb"
"time"
)

// Device wraps a gousb.Device with RNDIS specific fields and capabilities.
Expand Down Expand Up @@ -152,9 +152,10 @@ func (d *Device) OpenBulkEndpoints() (in *gousb.InEndpoint, out *gousb.OutEndpoi
continue
}
}

in, out, _ = d.findEndpoints(intf)
if in != nil && out != nil {
d.prepareDataInterface(intf)
fmt.Printf("USB DEBUG: Found bulk endpoints on intf %d alt %d\n", intfDesc.Number, alt.Number)
d.dataIntf = intf
return in, out, nil
Expand All @@ -166,12 +167,32 @@ func (d *Device) OpenBulkEndpoints() (in *gousb.InEndpoint, out *gousb.OutEndpoi
}
}
} else {
d.prepareDataInterface(d.dataIntf)
return d.findEndpoints(d.dataIntf)
}

return nil, nil, fmt.Errorf("could not find bulk endpoint pair")
}

// prepareDataInterface makes a claimed RNDIS data interface ready for traffic:
// select alt 0 explicitly and clear any stale endpoint halt left behind by an
// earlier session.
//
// Some gadgets (e.g. vivo/MediaTek) keep their bulk endpoints disabled until
// the host issues SET_INTERFACE, and transfers to a halted endpoint NAK until
// they time out. Both requests are standard and harmless on other devices.
func (d *Device) prepareDataInterface(intf *gousb.Interface) {
num := intf.Setting.Number
if _, err := d.Control(0x01, 0x0B, 0, uint16(num), nil); err != nil {
fmt.Printf("USB DEBUG: SET_INTERFACE(%d, 0) failed: %v\n", num, err)
}
for _, ep := range intf.Setting.Endpoints {
if _, err := d.Control(0x02, 0x01, 0, uint16(ep.Address), nil); err != nil {
fmt.Printf("USB DEBUG: CLEAR_FEATURE(halt) ep 0x%02x failed: %v\n", ep.Address, err)
}
}
}

func (d *Device) findEndpoints(intf *gousb.Interface) (*gousb.InEndpoint, *gousb.OutEndpoint, error) {
var in *gousb.InEndpoint
var out *gousb.OutEndpoint
Expand Down