From 33d6f60d88eeb63f3bdccd63cc813998d39ce1b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 10 Jun 2026 09:23:58 +0800 Subject: [PATCH] usbip: authenticate the VHCI loopback relay peer The one-shot loopback listener accepted the first connection with no validation, and the handshake discarded the busid the peer sent. Any local process racing the driver's WSK connect could receive the OP_REP_IMPORT device info and speak raw USB/IP to the remote device while denying the real attach. Accepted peers must now be kernel-owned sockets (GetExtendedTcpTable owner is the System process), complete the handshake within a deadline, and request exactly this session's busid; rejected peers are dropped without ending the session. --- service/usbip/client_windows.go | 146 +++++++++++++++++++++++++++----- 1 file changed, 127 insertions(+), 19 deletions(-) diff --git a/service/usbip/client_windows.go b/service/usbip/client_windows.go index d90dea06e..d83b4a401 100644 --- a/service/usbip/client_windows.go +++ b/service/usbip/client_windows.go @@ -4,18 +4,31 @@ package usbip import ( "context" + "encoding/binary" "fmt" "io" "net" "strconv" "sync" + "time" + "unsafe" "github.com/sagernet/sing-box/common/usbipvhci" "github.com/sagernet/sing-box/log" E "github.com/sagernet/sing/common/exceptions" + + "golang.org/x/sys/windows" ) -const loopbackHost = "127.0.0.1" +const ( + loopbackHost = "127.0.0.1" + + // The VHCI driver connects from kernel WSK and sends OP_REQ_IMPORT + // immediately; a peer that stalls the handshake is not the driver. + relayHandshakeTimeout = 5 * time.Second + + systemProcessID = 4 +) // windowsImportHost imports remote devices through the usbip-win2 UDE // driver. The driver connects and speaks USB/IP itself, in-kernel, so it @@ -129,17 +142,38 @@ func (s *windowsClientSession) start(ctx context.Context) error { return nil } +// acceptAndRelay accepts loopback connections until one proves to be +// the VHCI driver, then splices it to the server connection. The +// listener address is observable by any local process between Listen +// and the driver's in-kernel connect, so each accepted peer must be +// authenticated (kernel-owned socket + correct import handshake) +// before it sees device data; rejected peers do not end the session. func (s *windowsClientSession) acceptAndRelay() { defer s.markDone() - driverConn, err := s.listener.Accept() - _ = s.listener.Close() // one-shot: only the driver should connect - if err != nil { - if s.ctx.Err() == nil { - s.logger.Debug("usbip windows: accept vhci driver: ", err) + var driverConn net.Conn + for { + conn, err := s.listener.Accept() + if err != nil { + if s.ctx.Err() == nil { + s.logger.Debug("usbip windows: accept vhci driver: ", err) + } + return } - return + err = s.verifyDriverConn(conn) + if err != nil { + _ = conn.Close() + if s.ctx.Err() != nil { + return + } + s.logger.Warn("usbip windows: rejected loopback peer: ", err) + continue + } + driverConn = conn + break } + _ = s.listener.Close() // one-shot: the driver has connected + s.connAccess.Lock() s.driverConn = driverConn s.connAccess.Unlock() @@ -148,20 +182,30 @@ func (s *windowsClientSession) acceptAndRelay() { return } - err = s.respondImport(driverConn) - if err != nil { - s.setErr(err) - if s.ctx.Err() == nil { - s.logger.Debug("usbip windows: import handshake: ", err) - } - _ = driverConn.Close() - _ = s.remote.Close() - return - } - relay(driverConn, s.remote) } +// verifyDriverConn authenticates an accepted loopback connection: the +// peer socket must be owned by the kernel (the driver connects via +// WSK, attributed to the System process) and must complete the import +// handshake for exactly the device this session carries. +func (s *windowsClientSession) verifyDriverConn(conn net.Conn) error { + pid, err := loopbackPeerPID(conn) + if err != nil { + return E.Cause(err, "resolve loopback peer") + } + if pid != systemProcessID && pid != 0 { + return E.New("peer is process ", pid, ", not the kernel") + } + _ = conn.SetDeadline(time.Now().Add(relayHandshakeTimeout)) + err = s.respondImport(conn) + if err != nil { + return err + } + _ = conn.SetDeadline(time.Time{}) + return nil +} + // respondImport answers the driver's in-kernel OP_REQ_IMPORT from the // cached device info, leaving both sides positioned at the data phase. // The driver verifies the bus id in our reply equals the one it sent @@ -174,10 +218,13 @@ func (s *windowsClientSession) respondImport(driverConn net.Conn) error { if header.Code != OpReqImport { return E.New("unexpected driver op code ", fmt.Sprintf("0x%04x", header.Code)) } - _, err = ReadOpReqImportBody(driverConn) + busid, err := ReadOpReqImportBody(driverConn) if err != nil { return E.Cause(err, "read driver OP_REQ_IMPORT body") } + if busid != s.info.BusIDString() { + return E.New("import handshake for ", busid, ", session carries ", s.info.BusIDString()) + } info := s.info err = WriteOpRepImport(driverConn, OpRepImport, OpStatusOK, &info) if err != nil { @@ -186,6 +233,67 @@ func (s *windowsClientSession) respondImport(driverConn net.Conn) error { return nil } +// loopbackPeerPID resolves the owning process of the peer side of an +// accepted loopback TCP connection via GetExtendedTcpTable: the row +// whose local endpoint is our remote endpoint (and vice versa). +func loopbackPeerPID(conn net.Conn) (uint32, error) { + remote, remoteOK := conn.RemoteAddr().(*net.TCPAddr) + local, localOK := conn.LocalAddr().(*net.TCPAddr) + if !remoteOK || !localOK { + return 0, E.New("unexpected address type") + } + const tcpTableOwnerPIDAll = 5 + const rowSize = 24 // MIB_TCPROW_OWNER_PID + var size uint32 + var table []byte + for { + var tablePtr *byte + if len(table) > 0 { + tablePtr = &table[0] + } + ret, _, _ := procGetExtendedTcpTable.Call( + uintptr(unsafe.Pointer(tablePtr)), + uintptr(unsafe.Pointer(&size)), + 0, + uintptr(windows.AF_INET), + tcpTableOwnerPIDAll, + 0, + ) + if ret == uintptr(windows.ERROR_INSUFFICIENT_BUFFER) { + table = make([]byte, size) + continue + } + if ret != 0 { + return 0, E.New("GetExtendedTcpTable error ", ret) + } + break + } + if len(table) < 4 { + return 0, E.New("GetExtendedTcpTable returned no table") + } + count := int(binary.LittleEndian.Uint32(table[0:4])) + for i := 0; i < count; i++ { + row := table[4+i*rowSize:] + if len(row) < rowSize { + break + } + rowLocalIP := net.IP(row[4:8]) + rowLocalPort := int(binary.BigEndian.Uint16(row[8:10])) + rowRemoteIP := net.IP(row[12:16]) + rowRemotePort := int(binary.BigEndian.Uint16(row[16:18])) + if rowLocalPort == remote.Port && rowRemotePort == local.Port && + rowLocalIP.Equal(remote.IP) && rowRemoteIP.Equal(local.IP) { + return binary.LittleEndian.Uint32(row[20:24]), nil + } + } + return 0, E.New("peer connection not found in tcp table") +} + +var ( + modIPHelper = windows.NewLazySystemDLL("iphlpapi.dll") + procGetExtendedTcpTable = modIPHelper.NewProc("GetExtendedTcpTable") +) + // relay splices the loopback driver stream to the proxied server stream // until either direction ends, then closes both. func relay(driverConn, remote net.Conn) {