Skip to main content
  1. Documentation/
  2. Developer Guide/

Extension Points

Table of Contents
TantoC2 is designed to be extended. Every major subsystem is pluggable — from listeners to cryptographic protocols. This page maps out every extension point and how the plugin system works under the hood.

What Can You Build?
#

Extension PointWhat It IsEffortStart Here
Transport PluginA listener implementation (HTTP, DNS, named pipes, etc.)MediumYou need a new C2 channel
Tool (Agentless) PluginA Python plugin for direct network interaction (SSH, SMB, LDAP, etc.)Small–MediumYou want to interact with a new protocol without deploying an agent
Agent ModuleA compiled payload (BOF, shellcode, DLL, etc.) loaded into agents at runtimeMediumYou have compiled tradecraft to deploy through agents
Agent PackageA complete deployable agent with its own crypto, wire protocol, build pipeline, and capabilitiesLargeYou want to create a new agent in Go, Rust, C, etc.

Plugin Architecture
#

Plugin Architecture

All plugin types share the same underlying mechanism:

  1. Every plugin extends PluginBase with a unique plugin_name() and a plugin_type()
  2. Discovery is automatic — drop a .py file in the right directory, or install a wheel with entry points
  3. Hot-reload without restart — the plugin watcher detects changes and reloads
  4. No core changes needed — the framework discovers and wires everything at runtime
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
from tantoc2.server.plugins import PluginBase
from abc import abstractmethod

class PluginBase(ABC):
    @classmethod
    @abstractmethod
    def plugin_name(cls) -> str:
        """Unique name for this plugin."""

    @classmethod
    @abstractmethod
    def plugin_type(cls) -> str:
        """Type: 'transport', 'agentless_module', 'agent_package',
        'crypto_provider', or 'protocol_codec'."""

PluginRegistry is the generic container. There is one registry per plugin type. The server instantiates five registries at startup and passes them to the MessagePipeline, ListenerManager, AgentlessManager, and BuildManager.

Plugin Discovery
#

Plugins are discovered through three mechanisms, all feeding into PluginRegistry:

1. Python Entry Points (Recommended)
#

Package your plugin as a wheel and declare entry points in pyproject.toml. This is how all official plugins are distributed:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# Transport
[project.entry-points."tantoc2.transports"]
my_transport = "my_package.transport:MyTransport"

# Tool (agentless module)
[project.entry-points."tantoc2.agentless_modules"]
my_tool = "my_package.tool:MyToolModule"

# Agent package + its crypto and codec components
[project.entry-points."tantoc2.agent_packages"]
my_agent = "my_package.package:MyAgentPackage"

[project.entry-points."tantoc2.crypto_providers"]
my_crypto = "my_package.crypto:MyCryptoProvider"

[project.entry-points."tantoc2.protocol_codecs"]
my_codec = "my_package.codec:MyProtocolCodec"

Entry point names (left side of =) must match plugin_name() return values. The registry calls discover_entry_points(group) at startup and on each refresh().

2. Directory Scanning
#

PluginRegistry scans configured directories for .py files. Files beginning with _ are skipped. Any non-abstract subclass of the base class found in the file is registered.

1
2
3
plugins/transports/my_transport.py      → Transport plugin
plugins/agentless/my_tool.py            → Agentless tool
plugins/agent_packages/my_agent.py      → Agent package

On refresh(), the registry checks file modification times (mtime) and reloads changed files, deregistering old classes and loading new ones.

3. Plugin Inbox (Hot-Drop)
#

Drop files into the inbox directory (configured as plugin_inbox_dir). The PluginInboxWatcher polls the inbox on a timer:

  • .whl files: pip install-ed into the current venv, then all registries refresh() entry points. Failed installs are moved to inbox/failed/.
  • After install, MessagePipeline.rebuild_maps() is called so new agent packages are immediately routable.

The PluginRegistry in Detail
#

1
2
3
4
5
6
7
8
9
class PluginRegistry(Generic[T]):
    def add_search_directory(self, directory: str | Path) -> None: ...
    def discover(self) -> int:              # Initial scan of all search dirs
    def discover_entry_points(self, group: str) -> int:  # Scan installed packages
    def refresh(self) -> int:              # Re-scan dirs + entry points
    def get(self, name: str) -> type[T] | None:
    def list_plugins(self) -> list[dict[str, str]]:
    def register(self, plugin_class: type[T]) -> None:   # Manual registration
    def unregister(self, name: str) -> bool:

refresh() is additive for entry points but will remove plugins from deleted files. It never clears active MessagePipeline sessions.

Development Setup
#

Install the server in dev mode
#

1
2
3
4
cd tantoc2/
hatch env create dev
hatch run dev:pip install -e .
hatch run dev:pip install -e dev_agent/

Working with a local plugin
#

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# File-drop method (simplest for iteration)
cp my_transport.py tantoc2/plugins/transports/

# Or install in dev mode from its directory
hatch run dev:pip install -e transports/my-transport/

# Trigger a refresh in the running server
# CLI:
tantoc2> plugins refresh
# API:
curl -X POST http://localhost:8443/api/v1/plugins/refresh \
  -H "Authorization: Bearer <token>"

Running tests
#

1
2
cd my-plugin/
hatch run dev:pytest

Plugin Type Quick Reference
#

Base ClassModuleplugin_type()Entry Point Group
TransportBasetantoc2.server.transports_moduletransporttantoc2.transports
AgentlessModuleBasetantoc2.server.agentless_baseagentless_moduletantoc2.agentless_modules
AgentPackageBasetantoc2.server.agent_packageagent_packagetantoc2.agent_packages
CryptoProviderBasetantoc2.server.crypto_providercrypto_providertantoc2.crypto_providers
ProtocolCodecBasetantoc2.server.protocol_codecprotocol_codectantoc2.protocol_codecs

Agent modules are not Python plugins — they are compiled payloads discovered via YAML manifests in agent_modules/. See Building Agent Modules.

Dependency Auto-Installation
#

Python plugins (server modules and tools) can declare pip dependencies in their metadata. The manager auto-installs them at discovery time:

1
2
3
4
5
AgentlessMetadata(
    name="my_tool",
    ...,
    dependencies=["ldap3>=2.9", "impacket>=0.11"],
)

If installation fails, the module is tracked as unavailable. It will not appear in list_modules() results but its failure reason is accessible via unavailable_modules on the manager. Check the server log for details.

Next Steps
#

Pick the extension point that matches your goal: