Skip to main content
  1. Documentation/
  2. Plugins/

Agent Packages

Table of Contents
An agent package defines the cryptographic protocol, wire format, capability declarations, and build pipeline for a class of agents.

Components
#

Each agent package provides four components:

  1. CryptoProvider — key exchange, session key derivation, encryption/decryption
  2. ProtocolCodec — encoding/decoding between internal format and wire format
  3. AgentPackage — ties them together with magic bytes, build templates, and capability declarations
  4. Capability declarations — module formats accepted, built-in commands, and agent capabilities

CryptoProvider
#

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
from tantoc2.server.crypto_provider import CryptoProviderBase, CryptoSession

class MyCryptoProvider(CryptoProviderBase):
    @classmethod
    def plugin_name(cls) -> str:
        return "my_crypto"

    def generate_keypair(self) -> tuple[bytes, bytes]:
        """Generate a server-side keypair. Returns (public_key, private_key)."""
        ...

    def create_session(
        self, registration_data: bytes, server_private_key: bytes
    ) -> tuple[CryptoSession, bytes]:
        """Process a registration request.

        Called when magic bytes match and session_token is all-zeros.

        Args:
            registration_data: Raw registration payload from the agent.
            server_private_key: The server's private key for this agent package.

        Returns:
            A tuple of (new CryptoSession, response bytes to send back).
        """
        ...

    def decrypt(self, session: CryptoSession, data: bytes) -> bytes:
        """Decrypt inbound agent data using session keys."""
        ...

    def encrypt(self, session: CryptoSession, data: bytes) -> bytes:
        """Encrypt outbound data for the agent."""
        ...

    def complete_handshake(self, session: CryptoSession, data: bytes) -> CryptoSession:
        """Finalize a multi-step key exchange (e.g., ECDH).
        Returns an updated session with state "established"."""
        ...

    def rotate_session_key(self, session: CryptoSession) -> tuple[CryptoSession, bytes]:
        """Rotate the session key.
        Returns (updated CryptoSession, rotation message bytes for the agent)."""
        ...

CryptoSession is a dataclass holding per-agent session state:

1
2
3
4
5
@dataclass
class CryptoSession:
    session_token: bytes            # 16-byte token assigned at registration
    state: str = "handshake"        # "handshake" or "established"
    session_data: dict[str, Any] = field(default_factory=dict)  # provider-specific data

ProtocolCodec
#

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
from tantoc2.server.protocol_codec import ProtocolCodecBase
from tantoc2.server.messages import InternalMessage

class MyProtocolCodec(ProtocolCodecBase):
    @classmethod
    def plugin_name(cls) -> str:
        return "my_codec"

    def decode(self, data: bytes) -> InternalMessage:
        """Decode decrypted bytes into an InternalMessage."""
        ...

    def encode(self, message: InternalMessage) -> bytes:
        """Encode an InternalMessage into bytes for encryption."""
        ...

InternalMessage is the canonical internal message representation:

1
2
3
4
5
6
7
8
@dataclass
class InternalMessage:
    msg_type: MessageType
    agent_id: str | None = None
    engagement_id: str | None = None
    payload: dict[str, Any] = field(default_factory=dict)
    timestamp: datetime = field(default_factory=lambda: datetime.now(UTC))
    metadata: dict[str, Any] = field(default_factory=dict)

AgentPackage
#

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
from tantoc2.server.agent_package import AgentPackageBase, AgentCapabilities

class MyAgentPackage(AgentPackageBase):
    @classmethod
    def plugin_name(cls) -> str:
        return "my_agent"

    @classmethod
    def magic_bytes(cls) -> bytes:
        """4-byte magic identifier. Must be unique across all packages."""
        return b"\xca\xfe\xba\xbe"

    @classmethod
    def crypto_provider_name(cls) -> str:
        """Return the name of the CryptoProvider plugin this agent uses."""
        return "my_crypto"

    @classmethod
    def protocol_codec_name(cls) -> str:
        """Return the name of the ProtocolCodec plugin this agent uses."""
        return "my_codec"

    @classmethod
    def supported_module_formats(cls) -> list[str]:
        """Module formats this agent can load (e.g., ['bof', 'shellcode']).
        Return empty list if agent does not support module loading."""
        return ["bof", "shellcode"]

    @classmethod
    def built_in_commands(cls) -> list[str]:
        """Commands the agent natively supports."""
        return ["survey", "upload", "download", "persist", "unpersist",
                "beacon_config", "kill", "load_module", "unload_module"]

    @classmethod
    def supports_daemonize(cls) -> bool:
        """Whether this agent supports daemonized module loading."""
        return True

    @classmethod
    def supports_relay(cls) -> bool:
        """Whether this agent supports acting as a P2P relay."""
        return False

    @classmethod
    def capabilities(cls) -> AgentCapabilities:
        """Capability metadata for UI/CLI presentation and filtering.
        Default implementation aggregates the other capability methods."""
        return AgentCapabilities(
            module_formats=cls.supported_module_formats(),
            built_in_commands=cls.built_in_commands(),
            supports_daemonize=cls.supports_daemonize(),
            supports_relay=cls.supports_relay(),
        )

Note that crypto_provider_name() and protocol_codec_name() are @classmethod methods returning the string name of the corresponding plugin, not instances. The teamserver looks up the actual CryptoProvider and ProtocolCodec from the plugin registry using these names.

Build Interface
#

Buildable agent packages additionally override the build methods:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
from tantoc2.server.agent_package import (
    AgentPackageBase,
    AgentTemplate,
    BuildConfig,
    CryptoMaterial,
)

class MyBuildablePackage(AgentPackageBase):
    # ... magic_bytes, plugin_name, crypto/codec names ...

    @classmethod
    def is_buildable(cls) -> bool:
        return True

    @classmethod
    def get_templates(cls) -> list[AgentTemplate]:
        return [
            AgentTemplate(
                name="my_beacon",
                platform="linux",
                arch="x86_64",
                format="elf",
                description="Linux ELF beacon agent",
            ),
        ]

    @classmethod
    def get_config_schema(cls) -> dict[str, OptionSchema]:
        """Return config schema for build options."""
        return { ... }

    @classmethod
    def stamp(
        cls,
        template_name: str,
        config: BuildConfig,
        crypto_material: CryptoMaterial,
    ) -> bytes:
        """Stamp configuration and crypto material into an agent binary.
        Returns the built agent binary as bytes."""
        ...

Supporting dataclasses:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@dataclass
class AgentTemplate:
    name: str         # e.g. "dev_beacon"
    platform: str     # "linux", "windows", "python"
    arch: str         # "x86_64", "any"
    format: str       # "py", "exe", "dll", "elf", "shellcode"
    description: str = ""

@dataclass
class BuildConfig:
    callbacks: list[CallbackAddress]
    kill_date: datetime
    beacon_interval: int = 60
    beacon_jitter: int = 10
    extra: dict[str, Any] = field(default_factory=dict)

@dataclass
class CryptoMaterial:
    public_key: bytes
    private_key: bytes
    server_public_key: bytes

@dataclass
class CallbackAddress:
    host: str
    port: int
    protocol: str = "https"

Capability Declarations
#

Agent packages declare their capabilities so the teamserver can:

  • Filter agent modules — only offer modules whose format matches supported_module_formats()
  • Present available commands — show operators which commands the agent supports
  • Validate module loading — reject load_module requests for incompatible formats
  • Gate daemonize requests — reject daemonize=true if the agent doesn’t support it

Agents with no module loading capability return an empty list from supported_module_formats(). The teamserver will not offer any agent modules for those agents.

Wire Protocol
#

All agent packages use this header structure:

1
2
3
4
5
+--------+--------+------------------+
| Magic  | Session| Payload          |
| 4 bytes| Token  | (variable)       |
|        | 16 bytes|                  |
+--------+--------+------------------+
  • Magic bytes (4 bytes): Identifies the agent package. The pipeline routes to the correct CryptoProvider and ProtocolCodec.
  • Session token (16 bytes): Assigned during registration. All zeros for registration messages.
  • Payload: CryptoProvider-encrypted, ProtocolCodec-encoded data.

Registration Flow
#

sequenceDiagram
    participant Agent
    participant Server

    Agent->>Server: magic + null_token + agent_public_key + codec(register_msg)
    Server->>Server: Generate keypair, derive session key, assign token
    Server->>Agent: magic + session_token + server_public_key + codec(register_response)
    Note over Agent,Server: Both sides now have the session key
    Agent->>Server: magic + session_token + encrypt(codec(checkin_msg))
    Server->>Agent: magic + session_token + encrypt(codec(tasks))

Message Types
#

TypeDirectionDescription
registerAgent -> ServerInitial registration with metadata and capabilities
checkinAgent -> ServerPeriodic check-in, receives queued tasks
task_resultAgent -> ServerCompleted task result
task_assignmentServer -> AgentTask assignments (including module payloads)
beacon_configServer -> AgentBeacon configuration update
killServer -> AgentKill the agent
survey_resultAgent -> ServerSystem metadata survey data
key_rotation_requestServer -> AgentRequest session key rotation
key_rotation_responseAgent -> ServerAcknowledge key rotation
relay_forwardBothRelay traffic through a P2P agent
plugin_messageBothPlugin-defined message type

Dev Agent Capabilities
#

The built-in dev agent package declares:

CapabilityValue
Plugin namedev_agent
Magic bytes\xde\xad\xc2\x01
CryptoProviderdev_crypto (ECDH + AES-256-GCM)
ProtocolCodecdev_codec (JSON + zlib)
Module formats["py"]
Built-in commandsls, cat, pwd, cd, whoami, env, ps, netstat, upload, download, load_module, unload_module
Supports daemonizeYes
Supports relayYes
Templatesdev_beacon (Python beacon for testing), dev_session (Python session mode for testing)

Dev Agent Crypto
#

The built-in dev agent package uses:

ComponentImplementation
Key exchangeECDH with P-256 (secp256r1)
Key derivationHKDF-SHA256 with info b"tantoc2-dev-agent-session"
EncryptionAES-256-GCM with 12-byte random nonces
Wire format[4B counter][12B nonce][ciphertext + GCM tag]
CodecJSON + zlib compression
Anti-replayMonotonic counter in encrypted payload

Deployment
#

Place the package in plugins/agent_packages/my_agent.py. It is discovered on the next server start. Alternatively, install the package as a Python package with an entry point in the tantoc2.agent_packages group.