Skip to content

Plugin Development

Loren Eteval edited this page Aug 28, 2026 · 1 revision

Plugin Development

Furious uses a capability-based Python plugin system. A plugin is a process-lifetime object that groups one or more independent capabilities; it is not assumed to be a proxy core.

This page documents the current plugin API (version 3) as implemented by the project. The API is still evolving, so pin and test against the Furious versions you support.

Architecture

The public contracts live in Furious.Plugins and Furious.Plugins.API. The process-wide registry:

  1. registers host-provided official plugins;
  2. discovers installed Python entry points in the furious.plugins group;
  3. validates plugin metadata and every declared capability;
  4. indexes capabilities by kind and stable identifier;
  5. calls initialize(context);
  6. calls shutdown() in reverse initialization order during application cleanup.

Registration is atomic. If initialization fails, the registry calls the plugin's shutdown hook and removes its partially indexed capabilities.

Capability types

Capability Purpose
ActionProvider Add actions to plugin-management UI
ProtocolHandler Own protocol identity, URI schemes, parsing, mapping, validation, and export
ProtocolEditorProvider Create a fresh Qt editor for one or more protocol IDs
SubscriptionDecoder Decode a subscription representation into independent items
CoreRuntimeFactory Recognize backend configurations and create managed runtime launches
TrafficStatsProvider Create traffic-statistics monitors for owned runtime types
PluginSettingsProvider Contribute host-rendered Settings sections
NavigationPageProvider Contribute persistent pages to the left navigation rail
PluginCapability with Utility kind Publish another independently queryable utility capability

A plugin may provide any useful combination. For example, a subscription decoder need not provide a runtime, and a navigation extension need not represent a proxy protocol.

Minimal package

A small distribution can use this layout:

furious-example/
├── pyproject.toml
└── src/
    └── furious_example/
        ├── __init__.py
        └── plugin.py

Declare the entry point in pyproject.toml:

[project.entry-points."furious.plugins"]
example = "furious_example.plugin:ExamplePlugin"

A minimal plugin class:

from Furious.Plugins import (
    PLUGIN_API_VERSION,
    FuriousPlugin,
    PluginMetadata,
)


class ExamplePlugin(FuriousPlugin):
    apiVersion = PLUGIN_API_VERSION
    metadata = PluginMetadata(
        id="example",
        displayName="Example",
        version="1.0.0",
        description="Example Furious extension",
        provider="Example Author",
    )
    capabilities = ()

Install the distribution into the same Python environment as Furious and restart the application. Discovery happens during process initialization; there is no hot reload.

Use stable, case-insensitively unique plugin and capability IDs. The registry rejects empty IDs, duplicate protocol IDs, duplicate URI schemes, overlapping runtime/configuration ownership, and unsupported API versions.

Adding a protocol

A ProtocolHandler owns one protocol's domain behavior:

  • descriptor — a ProtocolDescriptor containing ID, display name, add-action text, editor title, menu order, schema, and subscription eligibility;
  • schemes — the URI schemes accepted by the protocol;
  • supports(configuration);
  • parse(uri);
  • fromMapping(mapping);
  • blank();
  • export(configuration, remark) or exportProfile(...);
  • validate(configuration).

Parsing should return a ProtocolParseResult only for input the handler owns. Use a real URI parser, validate required fields, and avoid accepting malformed data that should be reported to the user.

Keep the configuration model independent from Qt. The handler, editor provider, and runtime factory may be separate capabilities from the same plugin.

Set subscriptionImportable=False for machine-local or executable configurations such as External Core.

Adding an editor

A ProtocolEditorProvider declares:

  • a unique editorId;
  • one or more protocolIds;
  • createEditor(protocolId, parent=None, **kwargs).

Create a new editor for each request. Do not store transient editors in the plugin or registry.

Follow Furious's Qt lifetime rules:

  • give widgets intentional Qt parents;
  • use the project's reusable window/dialog base classes;
  • avoid persistent direct bound-method connections from long-lived objects to transient receivers;
  • disconnect or weakly dispatch callbacks where ownership differs;
  • let the host own and release a returned transient editor.

A navigation-page provider is different: the host creates each descriptor's page once and retains it for the application lifetime.

Adding a subscription decoder

A SubscriptionDecoder declares decoderId, displayName, and integer priority, then implements:

def decode(self, data: bytes) -> SubscriptionResult | None:
    ...

Return None when the format does not match. Return a validated SubscriptionResult when it does. Decoding only identifies items; protocol handlers remain responsible for converting each URI/mapping into a connection configuration.

This separation prevents a Base64, YAML, JSON, or custom transport format from duplicating VMess/VLESS/Trojan parsers. Higher priority decoders are tried first.

Treat subscription bytes as untrusted and bound decompression, parsing, and collection sizes.

Adding a runtime

A CoreRuntimeFactory declares:

  • factoryId;
  • owned configurationTypes;
  • owned runtimeTypes;
  • fromMapping(mapping) when it recognizes a full backend document;
  • create(CoreRuntimeRequest), returning CoreRuntimeLaunch.

The request supplies the prepared configuration, routing value, exit callback, message callback, proxy-only flag, logging flag, and host options. The launch binds an owned runtime to the configuration and arguments used by its start() method.

Optional factory hooks include:

  • prepareTUN(config);
  • usesApplicationTun2socks(config);
  • routingOptions(config);
  • prepareDownloadTest(config, port);
  • configureEnvironment();
  • coreVersions();
  • logTimestampPatterns();
  • coreExitMessage(core, exitcode);
  • afterConnected(httpProxy).

Do not add backend-name conditionals to shared connection services. Express variation through these capabilities.

A runtime must have explicit ownership of processes, threads, timers, files, sockets, and callbacks. Startup failure must unwind partial resources; stop/cleanup must be bounded and idempotent.

Traffic statistics

A TrafficStatsProvider declares the runtime types it supports and implements monitorForRuntime(runtime). Return a monitor only when the active runtime has enough configuration to query cumulative upload/download counters.

Queries must not block the GUI thread. Report unavailable or malformed statistics as unavailable data rather than crashing the connection.

Settings, actions, and pages

  • PluginSettingsProvider.createSections(...) returns host-rendered settings descriptors. Prefer these for ordinary preferences rather than building an unrelated settings window.
  • ActionProvider.createActions(...) returns actions for plugin management.
  • NavigationPageProvider.pageDescriptors() returns NavigationPageDescriptor values with an ID, title, icon, order, and factory.

Navigation pages are sorted by descriptor order and receive IDs namespaced as plugin:<plugin-id>:<page-id>.

For translated bundled UI, follow Furious's static translation-key conventions. Third-party plugins should normally supply their own localized/literal text and mark it non-translatable rather than assuming the host catalog contains their strings.

Configuration and persistence

Prefer typed dataclasses/models for domain configuration. Preserve unknown fields when forward/backward compatibility is required, and separate user metadata from connection configuration.

Do not create an unrelated settings file when the host's configuration model or plugin settings capability is appropriate. Never execute code merely because a configuration was parsed, displayed, imported from a subscription, or opened in an editor.

Development checklist

  • Use a unique plugin ID and unique capability IDs.
  • Set apiVersion = PLUGIN_API_VERSION.
  • Keep imports side-effect-free; do not construct QApplication or widgets at module import time.
  • Keep models independent from Qt.
  • Keep protocol parsing/export centralized and round-trip tested.
  • Return fresh editors and runtime objects with clear owners.
  • Validate user/plugin/network input at boundaries.
  • Do not log credentials, full subscription payloads, or secret-bearing configurations.
  • Test initialization failure and reverse-order shutdown.
  • Test repeated editor open/close and runtime connect/disconnect cycles.
  • Test source execution and an installed wheel, not only an editable checkout.
  • Declare runtime data as package data so Nuitka/wheel installations can discover it.

Reference implementations

The official implementations are the best current examples:

  • Furious/Backends/Xray — several protocols, structured editors, routing, native TUN, statistics, settings, and actions;
  • Furious/Backends/Hysteria1 — protocol, editor, routing, and runtime;
  • Furious/Backends/Hysteria2 — protocol, editor, native TUN, settings, and traffic statistics;
  • Furious/Backends/ExternalCore — a machine-local non-shareable protocol plus a managed subprocess runtime;
  • Furious/Extensions/StandardSubscriptions.py — subscription decoders without a proxy core.

Also read the repository's scoped AGENTS.md files before contributing changes.

See also

Clone this wiki locally