From 2f5132a93bf7862310b11732e70450c0b4631e45 Mon Sep 17 00:00:00 2001 From: user Date: Thu, 17 Sep 2026 10:41:38 +0300 Subject: [PATCH] fix(rndis): canonical packet framing and filter re-apply for vivo/MediaTek Frames were sent as REMOTE_NDIS_PACKET_MSG with a 48-byte header and DataOffset 40. The vivo X200 Pro mini (MediaTek) rejects those frames: they are counted as receive errors and dropped, so the relay never sees a DHCP offer and the tunnel never comes up. The in-tree Linux host driver (rndis_host.c) uses a 44-byte header with DataOffset 36, and the same device accepts it immediately. Switch to the canonical layout and add a unit test that pins it. Additionally: - drain stale encapsulated control responses before the handshake; the device can otherwise hand back completions from a previous session and desync INIT/QUERY/SET; - select the data interface explicitly (SET_INTERFACE alt 0) and clear any stale endpoint halt before first use: some gadgets keep their bulk endpoints disabled until SET_INTERFACE, and transfers to a halted pipe NAK until they time out; - re-apply OID_GEN_CURRENT_PACKET_FILTER after the data interface has been opened. SET_INTERFACE resets the filter to zero on vivo, which makes the phone drop its own broadcast traffic (DHCP offers) silently. Tested against vivo X200 Pro mini on macOS 26 (Apple Silicon): DHCP OFFER/ACK complete and traffic flows over the USB link. --- internal/daemon/relay.go | 6 ++++ internal/rndis/messages.go | 37 ++++++++++++--------- internal/rndis/messages_test.go | 59 +++++++++++++++++++++++++++++++++ internal/rndis/rndis.go | 44 +++++++++++++++++++++++- internal/usb/device.go | 25 ++++++++++++-- 5 files changed, 152 insertions(+), 19 deletions(-) create mode 100644 internal/rndis/messages_test.go diff --git a/internal/daemon/relay.go b/internal/daemon/relay.go index af78fe6..0991b8c 100644 --- a/internal/daemon/relay.go +++ b/internal/daemon/relay.go @@ -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, diff --git a/internal/rndis/messages.go b/internal/rndis/messages.go index fe8fa78..c120ace 100644 --- a/internal/rndis/messages.go +++ b/internal/rndis/messages.go @@ -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 { @@ -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 } @@ -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 } @@ -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 } diff --git a/internal/rndis/messages_test.go b/internal/rndis/messages_test.go new file mode 100644 index 0000000..6bc157c --- /dev/null +++ b/internal/rndis/messages_test.go @@ -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") + } +} diff --git a/internal/rndis/rndis.go b/internal/rndis/rndis.go index 6eb9c6f..4275912 100644 --- a/internal/rndis/rndis.go +++ b/internal/rndis/rndis.go @@ -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{ @@ -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++ @@ -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) @@ -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) + } +} diff --git a/internal/usb/device.go b/internal/usb/device.go index 2b796fc..04457c5 100644 --- a/internal/usb/device.go +++ b/internal/usb/device.go @@ -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. @@ -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 @@ -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