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.
This commit is contained in:
世界
2026-06-10 09:23:58 +08:00
parent 7bf1b91564
commit 33d6f60d88
+127 -19
View File
@@ -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) {