2
Plugin Development
Loren Eteval edited this page 2026-09-19 14:39:58 +08:00
This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.

Capability indexing is atomic per plugin. If initialization fails, the registry calls the plugins shutdown hook and removes its partially indexed capabilities. The plugin must still clean up resources or other side effects acquired before that failure.

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 prepared runtimes and readiness policies
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:

from typing import Optional

from Furious.Plugins import SubscriptionResult


def decode(self, data: bytes) -> Optional[SubscriptionResult]:
    ...

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. When a caller explicitly chooses a decoder ID, decoding is restricted to that decoder rather than silently falling back to another format.

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

Worker-safe parsing

Capabilities default to workerSafe = False. Set it to True only when the capability can process copied input concurrently without touching widgets, live repositories, or other thread-affine state. The host checks both the subscription decoder and the relevant protocol handlers before using the worker path; unclassified capabilities retain the GUI-thread compatibility path.

This flag does not make a Python or native call interruptible. Keep work bounded and apply finite I/O timeouts where needed; cancellation cannot forcibly stop third-party code already executing.

Adding a runtime

A CoreRuntimeFactory declares:

  • factoryId;
  • owned configurationTypes;
  • owned runtimeTypes;
  • fromMapping(mapping) when it recognizes a full backend document;
  • create(CoreRuntimeRequest), returning PreparedRuntime or None when it cannot create a runtime.

The request supplies the configuration, routing value, exit callback, message callback, proxy-only flag, logging flag, and host options. Prepare an owned runtime whose zero-argument start() acquires execution resources. Return it in PreparedRuntime, optionally with a separate CoreRuntimeStartup readiness policy:

from Furious.Plugins import CoreRuntimeStartup, PreparedRuntime


def prepared_proxy(runtime, http_endpoint):
    return PreparedRuntime(
        runtime=runtime,
        readiness=CoreRuntimeStartup(endpoint=http_endpoint),
    )

This helper illustrates the return shape; the factory must construct and configure the runtime first. The readiness endpoint is probed asynchronously for TCP acceptance, with a default 2,500 ms deadline and 50 ms retry interval. Readiness is distinct from process liveness and the later network connectivity test.

The request and prepared-result dataclasses are frozen envelopes, not deep copies of their contents. Prepare mutable connection, routing, probe, and TUN data on explicit copies; never mutate persisted profile data as a side effect of preparing a connection.

Optional factory hooks include:

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

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

The runtime implements the CoreRuntime contract in Furious.Interface.Runtime. Bind its exit callback before starting and keep that event sink stable. Publish a typed RuntimeExit exactly once for each execution; interpret raw backend exit codes at the runtime boundary. isRunning() must passively report liveness without consuming an exit or emitting callbacks. Exit callbacks may arrive on an implementation worker thread, so UI owners must hand them off to the Qt thread.

A failed start() raises RuntimeStartError. The factory owns resources acquired before returning; after a PreparedRuntime is returned, the caller retains the runtime for cleanup even when starting fails. Give processes, threads, timers, files, sockets, and callbacks explicit owners. Startup failure must unwind partial resources; stop/dispose must be bounded and idempotent. A timeout or terminal state alone is not proof that native resources were released.

prepareTUN() reports native-TUN ownership. Raise TUNPreparationError when required preparation cannot be completed safely; do not silently switch to a different TUN implementation. Preserve explicit user TUN configuration during normal preparation, and strip TUN only on an explicit proxy-only preparation path such as a download test.

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.

Return a TrafficStatsMonitor containing a query callable and its target. Its background query(target) returns cumulative TrafficCounters or None when unavailable. Give every query a finite I/O timeout: cancellation cannot interrupt a plugin or native call already blocked. The frozen monitor descriptor does not make mutable targets thread-safe. 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

Read the current public capability contracts and runtime interface alongside the official implementations:

  • 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