Skip to content

Commit 9a14db1

Browse files
xaionaro@dx.centerxaionaro@dx.center
authored andcommitted
feat: remote binder proxy — invoke Android services from host via TCP+gadb
Two components: - Device-side daemon (cmd/binder-proxyd): opens /dev/binder, listens on TCP, resolves AIDL descriptors to services via INTERFACE_TRANSACTION, and forwards transactions from remote clients. - Host-side proxy (interop/gadb/proxy): RemoteTransport sends transactions over TCP; Session orchestrates cross-compilation, push, daemon startup (with SIGHUP trap for gadb shell survival), ADB port forwarding, and connection lifecycle. Wire protocol uses big-endian length-prefixed frames carrying descriptor, transaction code, flags, and raw parcel bytes. Verified on device 41041JEKB08092 (API 36): INTERFACE_TRANSACTION returns correct ServiceManager descriptor, ListServices returns 373 services.
1 parent e43700c commit 9a14db1

8 files changed

Lines changed: 1172 additions & 0 deletions

File tree

cmd/binder-proxyd/main.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
//go:build linux
2+
3+
// Binder proxy daemon — runs on an Android device, listens on TCP,
4+
// and forwards binder transactions from host-side clients.
5+
//
6+
// Build:
7+
//
8+
// GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -o build/binder-proxyd ./cmd/binder-proxyd/
9+
// adb push build/binder-proxyd /data/local/tmp/ && adb shell /data/local/tmp/binder-proxyd
10+
package main
11+
12+
import (
13+
"context"
14+
"flag"
15+
"fmt"
16+
"os"
17+
"os/signal"
18+
"syscall"
19+
20+
"github.com/AndroidGoLab/binder/interop/gadb/proxy"
21+
)
22+
23+
func main() {
24+
listenAddr := flag.String("listen", ":7100", "TCP address to listen on")
25+
flag.Parse()
26+
27+
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
28+
defer cancel()
29+
30+
if err := run(ctx, *listenAddr); err != nil {
31+
fmt.Fprintf(os.Stderr, "binder-proxyd: %v\n", err)
32+
os.Exit(1)
33+
}
34+
}
35+
36+
func run(ctx context.Context, listenAddr string) error {
37+
daemon, err := proxy.NewDaemon(ctx, proxy.DaemonOptionListenAddr(listenAddr))
38+
if err != nil {
39+
return fmt.Errorf("creating daemon: %w", err)
40+
}
41+
defer daemon.Close(ctx)
42+
43+
return daemon.Serve(ctx)
44+
}

interop/gadb/proxy/daemon.go

Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
//go:build linux
2+
3+
package proxy
4+
5+
import (
6+
"context"
7+
"fmt"
8+
"net"
9+
"sync"
10+
11+
"github.com/facebookincubator/go-belt/tool/logger"
12+
13+
"github.com/AndroidGoLab/binder/binder"
14+
"github.com/AndroidGoLab/binder/binder/versionaware"
15+
"github.com/AndroidGoLab/binder/kernelbinder"
16+
"github.com/AndroidGoLab/binder/parcel"
17+
"github.com/AndroidGoLab/binder/servicemanager"
18+
)
19+
20+
// Daemon is a device-side TCP server that forwards binder transactions
21+
// from a host-side proxy to the Android binder driver.
22+
type Daemon struct {
23+
listenAddr string
24+
driver *kernelbinder.Driver
25+
transport *versionaware.Transport
26+
sm *servicemanager.ServiceManager
27+
28+
// descriptorCache maps AIDL interface descriptors to their IBinder handles.
29+
descriptorCache map[string]binder.IBinder
30+
descriptorCacheMu sync.RWMutex
31+
}
32+
33+
// NewDaemon opens the binder driver and creates a ServiceManager.
34+
func NewDaemon(
35+
ctx context.Context,
36+
opts ...DaemonOption,
37+
) (_ *Daemon, _err error) {
38+
logger.Tracef(ctx, "NewDaemon")
39+
defer func() { logger.Tracef(ctx, "/NewDaemon: %v", _err) }()
40+
41+
cfg := DaemonOptions(opts).config()
42+
43+
driver, err := kernelbinder.Open(ctx, binder.WithMapSize(cfg.MapSize))
44+
if err != nil {
45+
return nil, fmt.Errorf("opening binder driver: %w", err)
46+
}
47+
defer func() {
48+
if _err != nil {
49+
_ = driver.Close(ctx)
50+
}
51+
}()
52+
53+
transport, err := versionaware.NewTransport(ctx, driver, 0)
54+
if err != nil {
55+
return nil, fmt.Errorf("creating version-aware transport: %w", err)
56+
}
57+
58+
sm := servicemanager.New(transport)
59+
60+
return &Daemon{
61+
listenAddr: cfg.ListenAddr,
62+
driver: driver,
63+
transport: transport,
64+
sm: sm,
65+
descriptorCache: make(map[string]binder.IBinder),
66+
}, nil
67+
}
68+
69+
// Serve accepts TCP connections and handles binder transactions.
70+
// Blocks until ctx is cancelled.
71+
func (d *Daemon) Serve(
72+
ctx context.Context,
73+
) (_err error) {
74+
logger.Tracef(ctx, "Serve")
75+
defer func() { logger.Tracef(ctx, "/Serve: %v", _err) }()
76+
77+
ln, err := net.Listen("tcp", d.listenAddr)
78+
if err != nil {
79+
return fmt.Errorf("listening on %s: %w", d.listenAddr, err)
80+
}
81+
defer ln.Close()
82+
83+
logger.Debugf(ctx, "daemon listening on %s", ln.Addr())
84+
85+
// Close the listener when the context is cancelled so Accept unblocks.
86+
go func() {
87+
<-ctx.Done()
88+
ln.Close()
89+
}()
90+
91+
for {
92+
conn, err := ln.Accept()
93+
if err != nil {
94+
select {
95+
case <-ctx.Done():
96+
return ctx.Err()
97+
default:
98+
return fmt.Errorf("accepting connection: %w", err)
99+
}
100+
}
101+
102+
logger.Debugf(ctx, "accepted connection from %s", conn.RemoteAddr())
103+
go d.handleConnection(ctx, conn)
104+
}
105+
}
106+
107+
// Close releases the binder driver.
108+
func (d *Daemon) Close(
109+
ctx context.Context,
110+
) error {
111+
return d.driver.Close(ctx)
112+
}
113+
114+
// handleConnection processes requests on a single TCP connection.
115+
func (d *Daemon) handleConnection(
116+
ctx context.Context,
117+
conn net.Conn,
118+
) {
119+
defer conn.Close()
120+
121+
for {
122+
select {
123+
case <-ctx.Done():
124+
return
125+
default:
126+
}
127+
128+
descriptor, code, flags, data, err := ReadRequest(conn)
129+
if err != nil {
130+
logger.Debugf(ctx, "reading request: %v", err)
131+
return
132+
}
133+
134+
logger.Debugf(ctx, "request: descriptor=%s code=%d flags=%d data_len=%d", descriptor, code, flags, len(data))
135+
136+
replyData, statusCode := d.executeTransaction(ctx, descriptor, code, flags, data)
137+
138+
if err := WriteResponse(conn, statusCode, replyData); err != nil {
139+
logger.Debugf(ctx, "writing response: %v", err)
140+
return
141+
}
142+
}
143+
}
144+
145+
// statusTransactFailed is the status code returned when the transaction
146+
// cannot be executed (service not found, transact error, etc.).
147+
const statusTransactFailed = 1
148+
149+
// executeTransaction resolves the service for the descriptor and
150+
// executes the binder transaction.
151+
func (d *Daemon) executeTransaction(
152+
ctx context.Context,
153+
descriptor string,
154+
code uint32,
155+
flags uint32,
156+
data []byte,
157+
) (replyData []byte, statusCode uint32) {
158+
svc, err := d.resolveService(ctx, descriptor)
159+
if err != nil {
160+
logger.Warnf(ctx, "resolving service for %q: %v", descriptor, err)
161+
return nil, statusTransactFailed
162+
}
163+
164+
p := parcel.FromBytes(data)
165+
reply, err := svc.Transact(
166+
ctx,
167+
binder.TransactionCode(code),
168+
binder.TransactionFlags(flags),
169+
p,
170+
)
171+
if err != nil {
172+
logger.Warnf(ctx, "transact %s code=%d: %v", descriptor, code, err)
173+
return nil, statusTransactFailed
174+
}
175+
defer reply.Recycle()
176+
177+
return reply.Data(), 0
178+
}
179+
180+
// resolveService finds the IBinder for a given AIDL interface descriptor.
181+
// Uses a cache to avoid repeated lookups.
182+
func (d *Daemon) resolveService(
183+
ctx context.Context,
184+
descriptor string,
185+
) (binder.IBinder, error) {
186+
// Fast path: cached.
187+
d.descriptorCacheMu.RLock()
188+
svc, ok := d.descriptorCache[descriptor]
189+
d.descriptorCacheMu.RUnlock()
190+
if ok {
191+
return svc, nil
192+
}
193+
194+
// Slow path: scan all services to find the one with this descriptor.
195+
d.descriptorCacheMu.Lock()
196+
defer d.descriptorCacheMu.Unlock()
197+
198+
// Double-check after acquiring write lock.
199+
if svc, ok := d.descriptorCache[descriptor]; ok {
200+
return svc, nil
201+
}
202+
203+
// Special case: the ServiceManager descriptor is always handle 0.
204+
if descriptor == "android.os.IServiceManager" {
205+
smBinder := binder.NewProxyBinder(d.transport, binder.DefaultCallerIdentity(), 0)
206+
d.descriptorCache[descriptor] = smBinder
207+
return smBinder, nil
208+
}
209+
210+
services, err := d.sm.ListServices(ctx)
211+
if err != nil {
212+
return nil, fmt.Errorf("listing services: %w", err)
213+
}
214+
215+
for _, name := range services {
216+
// Skip services we have already resolved.
217+
svcBinder, err := d.sm.GetService(ctx, name)
218+
if err != nil {
219+
logger.Debugf(ctx, "getting service %q: %v", name, err)
220+
continue
221+
}
222+
223+
svcDescriptor, err := queryDescriptor(ctx, svcBinder)
224+
if err != nil {
225+
logger.Debugf(ctx, "querying descriptor for %q: %v", name, err)
226+
continue
227+
}
228+
229+
d.descriptorCache[svcDescriptor] = svcBinder
230+
231+
if svcDescriptor == descriptor {
232+
return svcBinder, nil
233+
}
234+
}
235+
236+
return nil, fmt.Errorf("no service found for descriptor %q", descriptor)
237+
}
238+
239+
// queryDescriptor sends INTERFACE_TRANSACTION to an IBinder to learn
240+
// its AIDL interface descriptor string.
241+
func queryDescriptor(
242+
ctx context.Context,
243+
svc binder.IBinder,
244+
) (_ string, _err error) {
245+
data := parcel.New()
246+
defer data.Recycle()
247+
248+
reply, err := svc.Transact(ctx, binder.InterfaceTransaction, 0, data)
249+
if err != nil {
250+
return "", fmt.Errorf("INTERFACE_TRANSACTION: %w", err)
251+
}
252+
defer reply.Recycle()
253+
254+
desc, err := reply.ReadString16()
255+
if err != nil {
256+
return "", fmt.Errorf("reading descriptor: %w", err)
257+
}
258+
259+
return desc, nil
260+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package proxy
2+
3+
// DaemonOption configures a Daemon.
4+
type DaemonOption interface {
5+
applyDaemon(*daemonConfig)
6+
}
7+
8+
// DaemonOptions is a slice of DaemonOption.
9+
type DaemonOptions []DaemonOption
10+
11+
func (opts DaemonOptions) config() daemonConfig {
12+
cfg := defaultDaemonConfig()
13+
for _, o := range opts {
14+
o.applyDaemon(&cfg)
15+
}
16+
return cfg
17+
}
18+
19+
type daemonConfig struct {
20+
ListenAddr string
21+
MapSize uint32
22+
}
23+
24+
func defaultDaemonConfig() daemonConfig {
25+
return daemonConfig{
26+
ListenAddr: ":7100",
27+
MapSize: 4 * 1024 * 1024,
28+
}
29+
}
30+
31+
type daemonOptionListenAddr string
32+
33+
func (o daemonOptionListenAddr) applyDaemon(c *daemonConfig) { c.ListenAddr = string(o) }
34+
35+
// DaemonOptionListenAddr sets the TCP address the daemon listens on.
36+
func DaemonOptionListenAddr(addr string) DaemonOption { return daemonOptionListenAddr(addr) }
37+
38+
type daemonOptionMapSize uint32
39+
40+
func (o daemonOptionMapSize) applyDaemon(c *daemonConfig) { c.MapSize = uint32(o) }
41+
42+
// DaemonOptionMapSize sets the mmap size for the binder driver.
43+
func DaemonOptionMapSize(size uint32) DaemonOption { return daemonOptionMapSize(size) }

0 commit comments

Comments
 (0)