//go:build linux // Package bridge accepts USB devices handed in from another process. // // It exists for hosts where this process cannot open USB devices itself. // Android is the case that motivated it: apps there have no access to // /dev/bus/usb, and must ask the framework, which shows a permission dialog // and returns an already-open file descriptor. A small app-side shim obtains // that descriptor plus the device's raw descriptors and passes both here over // a Unix socket, using SCM_RIGHTS to transfer the descriptor itself. // // Nothing about this is Android-specific though: any supervising process can // use it to hand devices to an unprivileged client. package bridge import ( "encoding/json" "fmt" "log" "net" "os" "path/filepath" "sync" "github.com/duffy/usb-server/internal/usb" "golang.org/x/sys/unix" ) // maxRequestSize caps one request. Descriptor blobs are a few hundred bytes; // this leaves plenty of room while bounding what a caller can make us buffer. const maxRequestSize = 64 * 1024 // Request is one device handover, sent as a single JSON message with the // device's file descriptor attached as SCM_RIGHTS ancillary data. type Request struct { // Action is "add" or "remove". Action string `json:"action"` // BusID identifies the device within this client, e.g. "1-2". It must be // stable for as long as the device is shared: it is what peers request. BusID string `json:"bus_id"` // Descriptors is the raw descriptor blob, base64 encoded by encoding/json: // the device descriptor followed by all configuration descriptors. On // Android this is UsbDeviceConnection.getRawDescriptors(). Descriptors []byte `json:"descriptors,omitempty"` BusNum uint32 `json:"bus_num,omitempty"` DevNum uint32 `json:"dev_num,omitempty"` Speed uint32 `json:"speed,omitempty"` ConfigValue uint8 `json:"config_value,omitempty"` Manufacturer string `json:"manufacturer,omitempty"` Product string `json:"product,omitempty"` Serial string `json:"serial,omitempty"` } // Response reports the outcome of a request. type Response struct { OK bool `json:"ok"` Error string `json:"error,omitempty"` } // Server listens for device handovers on a Unix socket. type Server struct { listener net.Listener path string // OnChange fires after a device is added or removed, so the share manager // can refresh and announce its list without waiting for the next poll. OnChange func() mu sync.Mutex closed bool } // Listen starts a bridge server on the given Unix socket path. // // The socket is created with 0600 permissions: whoever can write to it can // make this client share arbitrary USB devices. func Listen(path string) (*Server, error) { if path == "" { return nil, fmt.Errorf("socket path is required") } if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil { return nil, fmt.Errorf("creating socket directory: %w", err) } // A leftover socket from a previous run would make Listen fail. if info, err := os.Stat(path); err == nil && info.Mode()&os.ModeSocket != 0 { os.Remove(path) } ln, err := net.Listen("unix", path) if err != nil { return nil, fmt.Errorf("listening on %s: %w", path, err) } if err := os.Chmod(path, 0600); err != nil { ln.Close() return nil, fmt.Errorf("securing socket: %w", err) } s := &Server{listener: ln, path: path} go s.acceptLoop() log.Printf("[bridge] listening on %s for device handovers", path) return s, nil } // Close stops the server and removes the socket. func (s *Server) Close() error { s.mu.Lock() s.closed = true s.mu.Unlock() err := s.listener.Close() os.Remove(s.path) usb.ReleaseAdoptedFDs() return err } func (s *Server) acceptLoop() { for { conn, err := s.listener.Accept() if err != nil { s.mu.Lock() closed := s.closed s.mu.Unlock() if closed { return } log.Printf("[bridge] accept error: %v", err) return } go s.handleConn(conn.(*net.UnixConn)) } } // handleConn processes requests on one connection until it closes. func (s *Server) handleConn(conn *net.UnixConn) { defer conn.Close() for { req, fd, err := readRequest(conn) if err != nil { // A clean disconnect is the normal way a session ends. return } resp := s.apply(req, fd) if err := writeResponse(conn, resp); err != nil { return } } } // apply carries out one request, taking ownership of fd. func (s *Server) apply(req *Request, fd int) Response { closeFD := func() { if fd >= 0 { unix.Close(fd) } } switch req.Action { case "add": if req.BusID == "" { closeFD() return Response{Error: "bus_id is required"} } if fd < 0 { return Response{Error: "no file descriptor was attached; " + "send the open device descriptor as SCM_RIGHTS ancillary data"} } if len(req.Descriptors) == 0 { closeFD() return Response{Error: "descriptors are required: this process cannot read them itself"} } meta := usb.ExternalDeviceMeta{ BusNum: req.BusNum, DevNum: req.DevNum, Speed: req.Speed, ConfigValue: req.ConfigValue, Manufacturer: req.Manufacturer, Product: req.Product, Serial: req.Serial, } if err := usb.RegisterExternalDevice(req.BusID, req.Descriptors, meta); err != nil { closeFD() return Response{Error: err.Error()} } // Register the descriptor only after the device parsed cleanly, so a // rejected request leaves nothing behind. if err := usb.AdoptDeviceFD(req.BusID, fd); err != nil { usb.UnregisterExternalDevice(req.BusID) closeFD() return Response{Error: err.Error()} } log.Printf("[bridge] device %s registered from outside (%s %s)", req.BusID, req.Manufacturer, req.Product) s.notify() return Response{OK: true} case "remove": closeFD() if req.BusID == "" { return Response{Error: "bus_id is required"} } usb.UnregisterExternalDevice(req.BusID) log.Printf("[bridge] device %s withdrawn", req.BusID) s.notify() return Response{OK: true} default: closeFD() return Response{Error: fmt.Sprintf("unknown action %q (expected add or remove)", req.Action)} } } func (s *Server) notify() { if s.OnChange != nil { s.OnChange() } } // readRequest reads one JSON message plus an optional attached descriptor. // It returns fd = -1 when no descriptor was sent. func readRequest(conn *net.UnixConn) (*Request, int, error) { buf := make([]byte, maxRequestSize) oob := make([]byte, unix.CmsgSpace(4)) // room for exactly one descriptor n, oobn, _, _, err := conn.ReadMsgUnix(buf, oob) if err != nil { return nil, -1, err } if n == 0 { return nil, -1, fmt.Errorf("empty request") } fd := extractFD(oob[:oobn]) var req Request if err := json.Unmarshal(buf[:n], &req); err != nil { if fd >= 0 { unix.Close(fd) } return nil, -1, fmt.Errorf("parsing request: %w", err) } return &req, fd, nil } // extractFD pulls a single descriptor out of ancillary data. // Any extra descriptors are closed rather than leaked. func extractFD(oob []byte) int { if len(oob) == 0 { return -1 } msgs, err := unix.ParseSocketControlMessage(oob) if err != nil { return -1 } result := -1 for _, msg := range msgs { fds, err := unix.ParseUnixRights(&msg) if err != nil { continue } for _, fd := range fds { if result == -1 { result = fd } else { unix.Close(fd) } } } return result } func writeResponse(conn *net.UnixConn, resp Response) error { data, err := json.Marshal(resp) if err != nil { return err } _, err = conn.Write(data) return err }