RPC Design

May 8, 2026 · View on GitHub

Overview

igf uses a 3-layer communication architecture:

UI (React)  ←──Socket.IO / REST──→  Server (Hono)  ←──Frida RPC──→  Agent (in-process)
  • UI ↔ Server: Socket.IO for real-time RPC and events, REST for data queries and file transfers
  • Server ↔ Agent: Frida's script.exports RPC mechanism over USB/network

Agent RPC Exports

Every platform agent (fruity/droid) exports four methods via rpc.exports:

invoke(namespace, method, args)

The primary dispatch function. Routes calls through the module registry:

// Example: list files in root directory
script.exports.invoke("fs", "ls", ["/"])

// Example: dump keychain items (iOS)
script.exports.invoke("keychain", "list", [])

interfaces()

Returns an array of all available namespace.method strings:

script.exports.interfaces()
// → ["checksec.all", "checksec.single", "classdump.hierarchy", "fs.ls", "fs.read", ...]

restore(rules)

Restores previously saved hook rules (pins). Called automatically on session start to re-establish hooks from a prior session:

script.exports.restore([
  { category: "objc", symbol: "-[NSURLSession dataTaskWithRequest:]", ... }
])

snapshot()

Captures the current set of active hook rules for persistence:

const rules = script.exports.snapshot()
// → [{ category: "objc", symbol: "...", ... }, ...]

Registry Pattern

The registry is defined in agent/src/common/registry.ts:

function createRegistry(route) {
  function invoke(ns, fn, args) {
    const iface = route[ns]
    if (!iface) throw new Error(`${ns} not found`)
    const method = iface[fn]
    if (!method) throw new Error(`${ns}.${fn} not found`)
    return method(...args)
  }

  function interfaces() {
    // yields all "namespace.method" strings
  }

  return { invoke, interfaces }
}

Each platform defines a router mapping namespaces to module objects:

// agent/src/fruity/router.ts
export default {
  checksec,    // binary security checks
  classdump,   // runtime class extraction
  cookies,     // HTTP cookie access
  crypto,      // crypto API interception control
  fs,          // filesystem operations
  info,        // app info (bundle, version, paths)
  keychain,    // keychain dumping
  // ...
}

Types flow end-to-end: RemoteRPC<T> converts synchronous module methods to async promises, matching the RPC boundary.

Socket.IO Events

Namespaces

NamespacePurpose
/devicesDevice list change notifications
/sessionIndividual instrumentation session

Server → Client Events

EventPayloadDescription
ready(pid: number)Session initialized, agent loaded
denied()Session attach denied by access rules
log(level: string, text: string)Agent log output
syslog(text: string)System log entry
hook(msg: BaseHookMessage)Hook interception event
crypto(msg: BaseHookMessage, data?: ArrayBuffer)Crypto API call captured
nsurl(event: NSURLEvent)Network request event (iOS)
droidHttp(event: HttpEvent)HTTP/WebSocket request event (Android)
jni(event: JNIEvent)JNI call event (Android)
flutter(event: Record<string, unknown>)Flutter method channel event
xpc(event: XPCSocketEvent)XPC/NSXPC message event (iOS)
hermes(event: { url, hash, size })Hermes bytecode capture event
privacy(msg: PrivacyMessage)Sensitive API access event
memoryScan(event: MemoryScanEvent, data?: ArrayBuffer)Memory scan progress/result event
lifecycle(event: string)App lifecycle change (active/inactive/foreground/background)
detached(reason: string)Session disconnected
fatal(detail: unknown)Fatal error
change()Device list changed (on /devices namespace)
invalid()Invalid session parameters

Client → Server Events

EventPayloadDescription
rpc(namespace, method, args, ack)Invoke agent RPC method
eval(source, name, ack)Evaluate JavaScript in agent
clearLog(type: "syslog" | "agent", ack)Clear log file

Connection Parameters

The /session namespace expects these query params on connect:

ParamValuesDescription
devicedevice IDTarget Frida device
platformfruity | droidiOS or Android
modeapp | daemonAttach to app or daemon process
bundlebundle IDApp identifier (for app mode)
pidnumberProcess ID (for daemon mode)
namestringDisplay name

REST API Endpoints

All endpoints are prefixed with /api.

Device Management

MethodPathDescription
GET/versionGet Frida and igf versions
GET/d.ts/packGet agent TypeScript type definitions
GET/devicesList connected devices
GET/device/:device/appsList apps on device
GET/device/:device/processesList processes on device
GET/device/:device/icon/:bundleGet app icon (PNG)
GET/device/:device/infoGet device system parameters
POST/device/:device/kill/:pidKill a process
PUT/devices/remote/:hostnameAdd remote Frida device
DELETE/devices/remote/:hostnameRemove remote Frida device

File Transfer

MethodPathDescription
GET/download/:device/:pid?path=...Download file from device
HEAD / GET/dump/:device/:pid?path=...Dump a decrypted app binary from device
GET/resource/:device/:pid?type=...&name=...Download Android resource
GET/apk-entry/:device/:pid?apk=...&entry=...Download an APK zip entry
POST/upload/:device/:pidUpload file to device

Data & History

MethodPathDescription
GET/logs/:device/:identifier/:typeStream log file (syslog/agent)
DELETE/logs/:device/:identifierDelete all logs
GET/hooks/:device/:identifierQuery hook logs (paginated)
DELETE/hooks/:device/:identifierClear hook logs
GET/history/crypto/:device/:identifierQuery crypto logs
DELETE/history/crypto/:device/:identifierClear crypto logs
GET/history/jni/:device/:identifierQuery JNI traces
DELETE/history/jni/:device/:identifierClear JNI traces
GET/history/flutter/:device/:identifierQuery Flutter logs
DELETE/history/flutter/:device/:identifierClear Flutter logs
GET/history/nsurl/:device/:identifierQuery network request logs
DELETE/history/nsurl/:device/:identifierClear network request logs
GET/history/nsurl/:device/:identifier/harExport iOS network logs as HAR
GET/history/nsurl/:device/:identifier/attachment/:requestIdDownload request body
GET/history/http/:device/:identifierQuery Android HTTP/WebSocket logs
DELETE/history/http/:device/:identifierClear Android HTTP/WebSocket logs
GET/history/http/:device/:identifier/harExport Android HTTP logs as HAR
GET/history/http/:device/:identifier/attachment/:requestIdDownload Android request/response body
GET/history/xpc/:device/:identifierQuery XPC/NSXPC logs
DELETE/history/xpc/:device/:identifierClear XPC/NSXPC logs
GET/history/privacy/:device/:identifierQuery privacy API access logs
DELETE/history/privacy/:device/:identifierClear privacy API access logs
GET/hermes/:device/:identifierQuery captured Hermes bytecode blobs
GET/hermes/:device/:identifier/download/:idDownload captured Hermes bytecode
GET/hermes/:device/:identifier/analyze/:idAnalyze Hermes bytecode metadata
GET/hermes/:device/:identifier/decompile/:idDecompile Hermes bytecode
GET/hermes/:device/:identifier/disassemble/:idDisassemble Hermes bytecode
GET/hermes/:device/:identifier/xrefs/:idQuery Hermes xrefs
DELETE/hermes/:device/:identifier/:idDelete one Hermes capture
DELETE/hermes/:device/:identifierClear Hermes captures
GET/pins/:device/:identifierLoad pin snapshot
DELETE/pins/:device/:identifierClear pin snapshot

Query parameters for history endpoints: limit, offset, since, category (hooks/privacy), method (JNI), protocol (XPC), and severity (privacy).

LLM

MethodPathDescription
POST/llmSend text to configured LLM, returns text response
POST/llm/streamSend text to configured LLM, streams text response

See LLM Setup for configuration.

End-to-End Data Flow

Example: Hooking an Objective-C Method

  1. User clicks "Add Hook" in the Hooks panel, selects -[NSURLSession dataTaskWithRequest:]

  2. Frontend emits Socket.IO RPC:

    socket.emit("rpc", "pins", "add", [{ category: "objc", symbol: "..." }], callback)
    
  3. Server (ws.ts) receives the rpc event and calls:

    script.exports.invoke("pins", "add", [{ category: "objc", symbol: "..." }])
    
  4. Agent (common/pins.ts) installs the Interceptor hook in the target process

  5. Agent sends hook events back via send() when the hooked method is called

  6. Server receives script messages, persists to HookStore (SQLite), and emits:

    socket.emit("hook", { symbol, direction, timestamp, ... })
    
  7. Frontend receives the hook event, updates TanStack Query cache, and renders the log entry

  8. Server auto-saves the pin snapshot after changes via script.exports.snapshot(), persisted to disk for session recovery

Example: Browsing the Filesystem

  1. Frontend calls rpc.fs.ls("/var/mobile") via the proxy
  2. Proxy emits socket.emit("rpc", "fs", "ls", ["/var/mobile"], callback)
  3. Server calls script.exports.invoke("fs", "ls", ["/var/mobile"])
  4. Agent's fs module calls Frida's ObjC.classes.NSFileManager APIs
  5. Result propagates back through ack callback to the frontend promise

Type Sharing Across Layers

Types flow from agent to frontend:

  1. Agent modules define method signatures in TypeScript
  2. tsgo generates type definitions to agent/types/
  3. Vite aliases @agent to ../agent/types so the GUI can import them
  4. RemoteRPC<T> type helper wraps all methods as async (reflecting the RPC boundary)
  5. createProxy() returns a typed proxy matching the agent interface

This ensures that calling rpc.fs.ls("/") in the frontend is fully type-checked against the agent's actual fs.ls implementation.