-
Notifications
You must be signed in to change notification settings - Fork 1
Architecture
This page describes how Rosemary works internally, from the transport layer to kernel-level traffic interception.
Rosemary has two components:
- Server runs on the operator machine with root/admin privileges. It manages agent connections, installs interception rules, and exposes the dashboard and REST API.
- Agent runs on the remote host with no special privileges. It connects back to the server, registers its subnets, and forwards traffic on demand.
Once an agent registers, the server installs kernel rules so that any packet your machine sends to the agent's subnets is silently intercepted and tunneled through the agent. No application changes, no proxy settings, no TUN device.
Application traffic
|
v
iptables / WinDivert / pf (kernel intercept)
|
v
Transparent proxy listener (server, userspace)
|
v
QUIC/UDP agent transport (default :2048/udp)
|
v
RTN framed messages (control + data envelopes)
|
v
AES-256-GCM payloads (encrypted JSON + data)
|
v
Agent (dials target, bridges data)
In standard mode the agent connects outbound to the server over QUIC/UDP. The default agent transport port is 2048, configured with -agent-port on the server and -s <server>:2048 on the agent.
QUIC provides stream multiplexing and transport keepalive. Rosemary carries control messages and proxied data over transport streams.
In bind mode the agent listens on a TCP address, default 0.0.0.0:9001, and the server connects with connect <ip:port>. Bind mode uses length-prefixed AES-GCM frames over TCP and is intended for networks where the server can reach the agent but the agent cannot connect outbound to the server.
Rosemary message payloads use AES-256-GCM:
- Key: 32-byte random value shared between server and agent, base64url-encoded
- Nonce: random per encrypted message
- Bind framing: 4-byte big-endian length prefix followed by encrypted payload
- QUIC authentication: key-derived certificate material and HMAC token
When a data payload exceeds 256 bytes and DEFLATE compression reduces its size, the payload is compressed before encryption. The receiver detects and decompresses automatically.
After authentication the agent:
- Enumerates local network interfaces
- Runs
ip route show(Linux) orroute print -4(Windows) to discover subnets - Probes internet access by dialing an external address
- Reads hostname and current username
- Sends a
registermessage with all of this metadata
The server assigns the agent an ID and stores its subnet list.
The server uses iptables to intercept traffic destined for the agent subnets:
| Protocol | Mechanism |
|---|---|
| TCP |
OUTPUT REDIRECT to the TCP proxy port (1080) |
| UDP |
MARK + TPROXY to the UDP proxy port (1081) |
| ICMP |
MARK on echo-request packets, caught by raw socket |
| DNS |
OUTPUT REDIRECT port 53 to the DNS proxy port (5300) |
The TCP proxy reads the original destination using SO_ORIGINAL_DST. The UDP proxy uses IP_RECVORIGDSTADDR to recover the original destination from TPROXY-intercepted packets.
When a default egress agent is set, a catch-all rule routes all non-subnet traffic through that agent as well.
The server embeds WinDivert.dll and WinDivert64.sys and extracts them at startup. WinDivert intercepts packets at the network layer before they leave the machine, allowing the server to redirect them to the appropriate agent.
The server configures pf rules and loopback routing to redirect traffic for agent subnets.
The server runs four listeners that handle intercepted traffic:
- Accepts connections redirected by iptables
- Reads original destination via
SO_ORIGINAL_DST - Looks up which agent owns that destination subnet
- Sends a
connectmessage to the agent - Bridges data bidirectionally
- Binds with
IP_TRANSPARENTandIP_RECVORIGDSTADDR - Reads original destination from ancillary socket data
- Forwards UDP datagrams to the owning agent
- Spoofs UDP responses back using a raw socket bound to the original destination
- Intercepts all DNS queries redirected from port 53
- Fans the query out to all connected agents in parallel
- Returns the first successful answer
- Falls back to saved system DNS servers and public resolvers (8.8.8.8, 1.1.1.1)
- Opens a raw
ip4:icmpsocket - Intercepts echo-request packets marked by iptables
- Forwards them to the owning agent via
icmp_proxymessages - Returns echo replies to the original sender
The server maintains a routing table mapping each CIDR subnet to the agent that owns it. When a connection arrives at a proxy listener, the server does a longest-prefix match against this table to find the correct agent.
Messages include original_agent_id and target_agent_id fields, enabling the server to relay messages between agents for multi-hop pivoting scenarios.
After registration the agent enters a loop processing server commands:
| Message | Action |
|---|---|
connect |
Dial the target host and bridge data back |
data |
Write payload to an open connection |
icmp_proxy |
Send an ICMP echo request and return the result |
dns_request |
Resolve a hostname and return the answer |
ping-sweep-request |
Probe a subnet for live hosts via TCP |
port-scan-request |
Scan TCP/UDP ports on a target |
start-agent-listener |
Open a local port forward |
stop-agent-listener |
Close a port forward |
agent_fwd_open |
Initiate a reverse forward |
reconnect |
Close and reconnect to the server |
disconnect |
Exit |
The agent also sends a heartbeat message every 10 seconds. If the transport drops, the agent reconnects with exponential backoff.
In standard mode the agent connects outbound to the server. In bind mode the roles are reversed:
- Agent listens on a local TCP address
- Operator runs
connect <ip:port>from the server REPL - Server dials the agent and exchanges encrypted frames over the TCP connection
- Agent registers normally after the server receives a valid decrypted
registermessage
This is useful when the agent host cannot reach the server directly but the server can reach the agent.
Use only on systems you own or have explicit written permission to test. Unauthorized use is prohibited.