/** * In-memory stand-in for the firegex backend, used only by the public demo build * (`bun run build:demo`, i.e. VITE_DEMO=true). * * A normal build never ships this: `utils.tsx` guards every reference behind IS_DEMO, * which vite inlines as a literal `false`, so rollup drops the module and its seed data. * Keep it free of top-level side effects, or that tree-shaking stops working. * * It covers the subset of the API the frontend actually calls, over mutable state, so a * visitor can create, edit, start, stop and delete things and watch the UI react exactly * as it would against a real instance. Nothing is persisted: a reload restores the seed. */ type Json = Record const uuid = () => (crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(16).slice(2)) const DEMO_CERT = "-----BEGIN CERTIFICATE-----\n(demo placeholder, no real key material is shipped)\n-----END CERTIFICATE-----\n" // Three functions in one file, because that is the normal case and it is what the // per-function switches on the filter card are for. Nothing here declares a protocol: // asking for an HttpRequest is what makes this an HTTP filter, and the RawPacket one // sits beside it because HTTP provides both. const SAMPLE_FILTER = `from firegex.pyfilters import pyfilter, ACCEPT, REJECT from firegex.pyfilters.models import HttpRequest, RawPacket @pyfilter def block_path_traversal(http_request: HttpRequest): """Drop any request trying to climb out of the web root.""" if ".." in http_request.url: return REJECT return ACCEPT @pyfilter def refuse_flag_leaks(packet: RawPacket): """Never let a flag leave, whatever the request looked like.""" if not packet.is_input and b"FLAG{" in packet.data: return REJECT return ACCEPT @pyfilter def log_client(packet: RawPacket): """Noisy while debugging, switched off once it has served its purpose.""" print("chunk from", packet.client_ip, packet.client_port) return ACCEPT ` const b64 = (text: string) => btoa(text) // A trimmed copy of what the backend introspects out of the library, so the demo's // editor offers the same completion. Short on purpose: the real one is generated. const DEMO_PYFILTER_API = { models: [ { name: "RawPacket", protocols: ["http", "tcp"], doc: "One chunk of a connection, with what is known about where it came from. " + "Everything below the application layer is metadata, and read-only.", members: [ { name: "client_ip", doc: "The client's address, whichever way this chunk is going", writable: false }, { name: "client_port", doc: "The client's port, whichever way this chunk is going", writable: false }, { name: "data", doc: "The application payload, as it arrived. Read-only: a filter answers with a verdict", writable: false }, { name: "data_size", doc: "How many bytes of application payload this chunk carries", writable: false }, { name: "dst_ip", doc: "Where this chunk is going", writable: false }, { name: "dst_port", doc: "The port this chunk is going to", writable: false }, { name: "is_input", doc: "True if the chunk is going from the client to the service", writable: false }, { name: "is_ipv6", doc: "True if the connection is IPv6, false if it is IPv4", writable: false }, { name: "is_tcp", doc: "True if the connection is TCP, false if it is UDP", writable: false }, { name: "server_ip", doc: "The protected service's address", writable: false }, { name: "server_port", doc: "The protected service's port", writable: false }, { name: "src_ip", doc: "Where this chunk came from", writable: false }, { name: "src_port", doc: "The port this chunk came from", writable: false }, ], }, { name: "HttpRequest", protocols: ["http"], doc: "The current HTTP request, once its headers have been parsed.", members: [ { name: "body", doc: "The request body, when it is complete", writable: false }, { name: "get_header", doc: "One header by name", writable: false, signature: "get_header(name)" }, { name: "headers", doc: "Every header of the request", writable: false }, { name: "method", doc: "The request method", writable: false }, { name: "url", doc: "The request target", writable: false }, ], }, { name: "TCPInputStream", protocols: ["http", "tcp"], doc: "The assembled client-to-service stream. A filter using it is only called for incoming data.", members: [ { name: "data", doc: "The entire input-direction stream assembled so far", writable: false }, { name: "is_ipv6", doc: "True if the connection is IPv6", writable: false }, { name: "total_stream_size", doc: "Size of that stream", writable: false }, ], }, ], verdicts: [ { name: "ACCEPT", value: 0, doc: "Forward this chunk. Returning None means the same thing.", values: [] }, { name: "REJECT", value: 2, doc: "Refuse the connection. Everything still in the stream goes with it.", values: [] }, { name: "DROP", value: 1, doc: "Stop the connection's traffic. On NFQUEUE this chunk and every one after it are dropped without closing anything; on the proxy layer, where a stream cannot skip bytes, the connection is closed as REJECT does.", values: [] }, ], settings: [ { name: "FGEX_STREAM_MAX_SIZE", doc: "Bytes of one stream a model may accumulate.", values: [] }, { name: "FGEX_FULL_STREAM_ACTION", doc: "What happens when a stream reaches that size.", values: ["FullStreamAction.FLUSH", "FullStreamAction.ACCEPT", "FullStreamAction.REJECT", "FullStreamAction.DROP"] }, { name: "FGEX_INVALID_ENCODING_ACTION", doc: "What happens when a parser cannot read the traffic.", values: ["ExceptionAction.ACCEPT", "ExceptionAction.DROP", "ExceptionAction.REJECT", "ExceptionAction.NOACTION"] }, ], } // ------------------------------------------------------------------ seed state const svcShop = uuid(), svcScore = uuid(), svcVault = uuid(), svcArena = uuid() const fltShopRegex = uuid(), fltShopPy = uuid(), fltScoreRegex = uuid(), fltVaultRegex = uuid() const fltArenaRegex = uuid() const svcWeb = uuid() const fltWebPy = uuid() const svcHijack = uuid() /** The demo's services have been up for a while; a fresh one starts filtering now. */ const demoStart = Math.floor(Date.now() / 1000) - 6 * 60 * 60 const state = { interfaces: [ { name: "lo", addr: "127.0.0.1" }, { name: "eth0", addr: "10.60.3.1" }, { name: "eth0", addr: "fd66:666:3::1" }, { name: "wg0", addr: "10.10.0.3" }, ], // One shape for every service: a network layer, and a chain of filters on it. services: [ { service_id: svcShop, name: "shop-api", status: "active", proto: "tcp", transport: "proxy", fail_open: true, max_connections: 512, over_limit_forwards: false, first_byte_timeout: 0, over_limit_hits: 1847, over_limit_first: demoStart, over_limit_last: demoStart + 900, filtering_since: demoStart }, { service_id: svcScore, name: "scoreboard", status: "active", proto: "tcp", transport: "nfqueue", fail_open: true, max_connections: 0, over_limit_forwards: false, first_byte_timeout: 0, over_limit_hits: 0, over_limit_first: null, over_limit_last: null, filtering_since: demoStart }, { service_id: svcVault, name: "vault", status: "stop", proto: "tls", transport: "proxy", fail_open: true, max_connections: 0, over_limit_forwards: false, first_byte_timeout: 0, over_limit_hits: 0, over_limit_first: null, over_limit_last: null, filtering_since: null }, // QUIC: UDP on the wire, terminated here because nothing else can see inside it. // Its filters are written against HTTP because its traffic is HTTP/3, which the // engine shows the chain as the HTTP/1.1 it would have been. { service_id: svcArena, name: "arena-h3", status: "active", proto: "quic", transport: "proxy", fail_open: true, max_connections: 0, over_limit_forwards: false, first_byte_timeout: 0, over_limit_hits: 0, over_limit_first: null, over_limit_last: null, filtering_since: demoStart }, // One service for every version of HTTP: HTTP/1.1 and HTTP/2 on the TCP // addresses, in the clear on one port and under TLS on the other, and HTTP/3 on // the UDP one beside them. One certificate and one chain — which is the whole // point, because covering the three used to take two or three services with the // same filter copied between them by hand. { service_id: svcWeb, name: "shop-web", status: "active", proto: "http", transport: "proxy", fail_open: true, max_connections: 0, over_limit_forwards: false, first_byte_timeout: 0, over_limit_hits: 0, over_limit_first: null, over_limit_last: null, filtering_since: demoStart }, // Handed to a proxy the operator wrote themselves: firegex steers, nothing here inspects. { service_id: svcHijack, name: "legacy-ftp", status: "active", proto: "tcp", transport: "external", fail_open: true, max_connections: 0, over_limit_forwards: false, first_byte_timeout: 0, over_limit_hits: 0, over_limit_first: null, over_limit_last: null, filtering_since: demoStart }, ] as Json[], // Where each one is reachable. shop-api answers on both stacks behind one chain, // which is the case that used to need two services kept in step by hand — and on // `wg0` by name, the other half of the same idea: an address somebody else hands // out is still one place this service answers. addresses: [ { address_id: uuid(), service_id: svcShop, ip_int: "10.60.3.1/32", port: 9000, proto: "tcp", edge: "tcp", target_port: null, upstream: "same", proxy_ip: null, proxy_port: null }, { address_id: uuid(), service_id: svcShop, ip_int: "fd66:666:3::1/128", port: 9000, proto: "tcp", edge: "tcp", target_port: null, upstream: "same", proxy_ip: null, proxy_port: null }, { address_id: uuid(), service_id: svcShop, ip_int: "wg0", port: 9000, proto: "tcp", edge: "tcp", target_port: null, upstream: "same", proxy_ip: null, proxy_port: null }, { address_id: uuid(), service_id: svcScore, ip_int: "10.60.0.0/16", port: 8080, proto: "tcp", edge: "tcp", target_port: null, upstream: "same", proxy_ip: null, proxy_port: null }, { address_id: uuid(), service_id: svcVault, ip_int: "10.60.3.1", port: 8443, proto: "tcp", edge: "tls", target_port: null, upstream: "same", proxy_ip: null, proxy_port: null }, // `udp`, because that is what the kernel matches for a QUIC service. { address_id: uuid(), service_id: svcArena, ip_int: "10.60.3.1", port: 4433, proto: "udp", edge: "quic", target_port: null, upstream: "same", proxy_ip: null, proxy_port: null }, // The three edges of one HTTP service, and what they say: in the clear on :80, // TLS on :443, HTTP/3 on :443/udp. The last two are *publications* — the service // itself only ever answered HTTP/1.1 on :80, and has not moved. { address_id: uuid(), service_id: svcWeb, ip_int: "10.60.3.1", port: 80, proto: "tcp", edge: "tcp", target_port: null, upstream: "same", proxy_ip: null, proxy_port: null }, { address_id: uuid(), service_id: svcWeb, ip_int: "10.60.3.1", port: 443, proto: "tcp", edge: "tls", target_port: 80, upstream: "tcp", proxy_ip: null, proxy_port: null }, { address_id: uuid(), service_id: svcWeb, ip_int: "10.60.3.1", port: 443, proto: "udp", edge: "quic", target_port: 80, upstream: "tcp", proxy_ip: null, proxy_port: null }, { address_id: uuid(), service_id: svcHijack, ip_int: "10.60.3.1", port: 21, proto: "tcp", edge: "tcp", target_port: null, upstream: "same", proxy_ip: "127.0.0.1", proxy_port: 12021 }, ] as Json[], filters: [ // shop-api shows what only the proxy layer can do: two kinds, in an order. { filter_id: fltShopRegex, service_id: svcShop, position: 0, kind: "regex", proto: "tcp", name: "patterns", active: true, blocked: 368 }, // `http` because its code asks for an HttpRequest, not because anybody said so. { filter_id: fltShopPy, service_id: svcShop, position: 1, kind: "pyfilter", proto: "http", name: "python", active: true, blocked: 641 }, { filter_id: fltScoreRegex, service_id: svcScore, position: 0, kind: "regex", proto: "tcp", name: "patterns", active: true, blocked: 1204 }, { filter_id: fltVaultRegex, service_id: svcVault, position: 0, kind: "regex", proto: "tcp", name: "patterns", active: true, blocked: 0 }, { filter_id: fltArenaRegex, service_id: svcArena, position: 0, kind: "regex", proto: "tcp", name: "patterns", active: true, blocked: 52 }, // The same file that would run on a plain HTTP/1.1 service, unchanged — which is // the point of an `http` service: HTTP/1.1, HTTP/2 and HTTP/3 are all shown to it // as the same HTTP/1.1, so one filter covers the three edges below. { filter_id: fltWebPy, service_id: svcWeb, position: 0, kind: "pyfilter", proto: "http", name: "python", active: true, blocked: 1190 }, ] as Json[], regexes: [ { regex_id: uuid(), filter_id: fltShopRegex, regex: b64("\\.\\./"), mode: "C", case_sensitive: true, active: true, blocked: 341 }, { regex_id: uuid(), filter_id: fltShopRegex, regex: b64("FLAG\\{[A-Za-z0-9_]+\\}"), mode: "S", case_sensitive: true, active: true, blocked: 27 }, // A rewriting rule, so the demo shows what only the proxy layer can do. { regex_id: uuid(), filter_id: fltScoreRegex, regex: b64("/etc/passwd"), mode: "C", case_sensitive: true, active: true, blocked: 914 }, { regex_id: uuid(), filter_id: fltScoreRegex, regex: b64("