Cybersecurity-Projects/PROJECTS/advanced/rveng/learn/02-ARCHITECTURE.md

7.0 KiB

Architecture

rveng is one analysis engine with three faces. The engine is framework-free Python that knows nothing about HTTP or React. A thin FastAPI layer adapts it to the web. A React app consumes that API. Progress is persisted behind a small interface so it can be swapped. This shape exists so the engine and the lesson content can be lifted out of this project and mounted inside a larger application without dragging a web framework along.

One engine, three faces

                         +--------------------------+
                         |   rveng/engine/  (pure)  |
                         |  elf disasm plt xref cfg  |
                         |  hex strings patch discover|
                         |  challenge (grading)      |
                         +------------+--------------+
                                      |
             +------------------------+------------------------+
             |                        |                        |
        +----+-----+           +------+------+          +------+------+
        | HTTP API |           |   library   |          |     CLI     |
        | FastAPI  |           |  (import it)|          | (potential) |
        +----+-----+           +-------------+          +-------------+
             |
        +----+-----+
        | React app|
        +----------+

The engine is the product. The API is an adapter. The React app is a face. The same engine that answers /api/challenges/05-find-the-gate/disasm could be imported directly by CertGames and driven with no HTTP at all. Nothing in rveng/engine/ imports FastAPI, and nothing renders HTML. That separation is the whole point.

The engine modules

Each module is small and does one thing against raw bytes. None of them execute anything.

engine/elf.py       parse the ELF64 header, sections, and symbols from bytes
engine/hex.py       render a canonical offset/hex/ascii dump
engine/strings.py   scan bytes for printable runs
engine/disasm.py    decode x86-64 with capstone, annotate the interesting ops
engine/plt.py       resolve a PLT stub address to an imported symbol name
engine/xref.py      collect cross-references from decoded instructions
engine/cfg.py       split a function into basic blocks and connect them
engine/discover.py  find functions in a stripped binary by prologue scan
engine/patch.py     apply and diff byte edits, verify a patch statically
engine/challenge.py the challenge model and the solve-then-reveal grader

The dependency direction is strict: everything depends on elf.py and its byte buffer, disasm.py feeds xref.py and cfg.py, and challenge.py depends on patch.py for patch grading. Nothing points back up toward the API.

The API layer

rveng/api/ is the adapter. It loads the curated challenges once at startup, exposes read-only analysis of them, and grades submissions.

api/app.py        builds the FastAPI app and defines every route
api/store.py      loads challenges from disk; the progress store (Protocol + impls)
api/schemas.py    the Pydantic response and request shapes
api/limits.py     size and length caps for untrusted input
api/middleware.py a body-size cap enforced before the body is parsed
api/server.py     the ASGI entry point (create_app())

The routes are deliberately boring. Given a challenge id they return a hex view, an ELF summary, a disassembly, a control-flow graph, cross-references, or the string list. One route accepts a submission and grades it. No route accepts a binary to analyze; the only binaries in the system are the curated challenge assets.

How a request flows

Take a learner disassembling the check function of the sample:

browser
  GET /api/challenges/05-find-the-gate/disasm?symbol=check
    |
  nginx  (serves the React app, proxies /api to the API container)
    |
  FastAPI route in app.py
    |  look up the challenge by id  (store.py)
    |  parse its binary            (elf.ElfImage)
    |  find the symbol "check"     (image.symbol)
    |  disassemble the symbol      (disasm.disassemble_symbol)
    |  resolve any call names      (plt.plt_map)
    |  serialize to schemas.DisasmView
    v
  JSON back to the browser, rendered by the disasm pane

The engine did the analysis; the API only translated ids to calls and objects to JSON. Crucially, the gate-highlight annotation on the disassembly is withheld until the challenge is solved, so a learner sees honest raw disassembly first and the "this is the gate" hint only after they have found it themselves.

Progress behind an interface

Progress (which challenges a session has solved) is the one piece of state. It lives behind a ProgressStore protocol with two methods:

mark_solved(session, challenge_id) -> None
solved(session) -> set[str]

Two implementations satisfy it: InMemoryProgress (a dict, used in tests) and SqliteProgress (a file-backed table, the default the server runs). The routes only ever see the protocol, so swapping the backing store is a one-line change in create_app and touches no route. When rveng is extracted into a host application, that application points the same protocol at its own database, and nothing else moves.

routes ---> ProgressStore (protocol)
                 |
        +--------+---------+
        |                  |
  InMemoryProgress    SqliteProgress    (host app plugs its own here)

The deployment topology

rveng self-hosts on localhost with Docker. There is no cloud, no TLS, no domain, no account. Two containers in production:

PROD (compose.yml, project "rveng")        DEV (dev.compose.yml, project "rveng-dev")

  browser :8790                              browser :8791
      |                                          |
  [ nginx ]  serves built React dist         [ nginx ]  proxies everything
      |  /api                                    |  /        |  /api
  [ api ]  uvicorn (FastAPI engine)          [ vite HMR ] [ api ] uvicorn --reload
      |                                                        |
  rveng_data volume (sqlite progress)                    rveng_data_dev volume

nginx serves the compiled frontend and proxies /api to the API container. The API is a pure engine adapter that never learns to serve a SPA, which keeps it extractable. In development, nginx fronts the Vite dev server for hot reload instead of serving a static build. The two projects use structural-literal names (rveng and rveng-dev), so dev and prod are namespace-isolated by construction and cannot collide. Everything runs on built-in defaults, so a clean clone comes up with no configuration.

Extraction into a host application

The end state this architecture buys: mounting rveng as a feature elsewhere means importing rveng.engine, reusing the challenge assets as-is, pointing the ProgressStore protocol at the host's store, and mounting the self-contained React components. The engine and content come along unchanged. That is the difference between building a throwaway project and building a feature that happens to also stand alone.