English | 日本語
Warp is an outbound transparent SMTP proxy. It records every SMTP command and response of an outgoing session, measures how long each destination takes before it accepts a message, and can hand the message to a filter hook before relaying it. https://warp.linyo.ws
Operating email infrastructure at scale comes with significant challenges:
- No visibility — You can't see what's being sent, to whom, or how the remote server responds
- Deliverability issues — Throttling and blocklisting happen silently; you only find out when users complain
- No filtering layer — Compromised accounts or misconfigured applications send spam or phishing emails before anyone notices
- IP reputation management — A single bad sender can damage the reputation of your entire IP range
- Compliance gaps — Auditing outbound email requires bolting on external tools that don't integrate cleanly
Warp deploys as a transparent proxy using Linux's SO_ORIGINAL_DST — it intercepts SMTP connections at the network level, meaning zero changes to your existing mail clients or applications. That socket option reads the connection tracking entry created by NAT, so Warp has to run on the host where the iptables rule is applied; the MTA itself may run anywhere. Once in place, Warp provides:
Every SMTP command and response is captured in real time. You can log to files, MySQL, SQLite, or send notifications to Slack — all through a simple plugin system. Connection metadata (sender, recipient, HELO hostname, elapsed time) is automatically extracted and structured for analysis.
Warp can buffer the entire message during the DATA phase and pass it to a filter hook before relaying. Your filter logic decides what happens next:
| Action | Behavior |
|---|---|
| Relay | Pass the message through to the destination server as-is |
| Reject | Return an SMTP error to the sender — the message never reaches the destination |
| Add Header | Modify the message (e.g., add X-Spam-Score headers) and then relay |
Filtering is an extension point, not a feature you can switch on: none of the built-in plugins implement BeforeRelay, so the decision logic is a plugin you write yourself. Warp uses the first loaded plugin that implements FilterHook.
Warp records the time between connecting to the destination and its 354 response, the point at which the destination is ready to accept the message body. That elapsed time goes into the log and into the connection metadata passed to hooks, next to every SMTP status code of the session. Warp does not classify the result: concluding that a destination is throttling you is left to whatever consumes the logs.
The source address used for relayed connections is set with -outbound-ip and applies to the whole process. Sending through more than one address means running one instance per address and choosing between them in the iptables rules; Warp does not pick an address per message.
Transparent Proxy (iptables nat)
┌─────────────────────────────┐
SMTP Client ────▶ │ Warp │ ────▶ Destination MX
│ │
│ ┌─────────┐ ┌──────────┐ │
│ │Upstream │ │Downstream│ │
│ │Mediator │ │Mediator │ │
│ └────┬────┘ └────┬─────┘ │
│ │ │ │
│ ┌────▼────────────▼────┐ │
│ │ Hook System │ │
│ │ ┌─────┐ ┌────────┐ │ │
│ │ │ Log │ │ Filter │ │ │
│ │ └─────┘ └────────┘ │ │
│ └──────────────────────┘ │
└─────────────────────────────┘
Key components:
- Server — Listens for incoming TCP connections and extracts the original destination using
SO_ORIGINAL_DST - Pipe — Manages bidirectional data flow between client and server with upstream/downstream mediators
- Mediators — Inspect and transform SMTP traffic in each direction, extracting metadata (MAIL FROM, RCPT TO, HELO) and handling STARTTLS negotiation
- Hook System — Extensible plugin architecture for logging (
AfterComm,AfterConn) and filtering (BeforeRelay)
Warp transparently handles TLS negotiation: it strips STARTTLS from the server's EHLO response to the client, then initiates its own TLS connection to the destination server. This allows Warp to inspect SMTP traffic while maintaining encrypted delivery.
Every log line carries the connection id and a two-character marker telling which leg of the session the data belongs to. The markers separate traffic that Warp passed through from traffic it handled itself, which is what makes a STARTTLS session readable:
| Marker | Meaning |
|---|---|
-> |
Client to destination |
<- |
Destination to client |
>| |
Client to Warp |
|> |
Warp to destination |
|< |
Destination to Warp |
<| |
Warp to client |
-- |
An event inside Warp |
2023/11/27 08:14:54.947569 01HG7XGYYZCEB3WSNXPAVG4ZZN -> EHLO localhost\r\n
2023/11/27 08:14:54.947752 01HG7XGYYZCEB3WSNXPAVG4ZZN |< 250-example.local\r\n250-STARTTLS\r\n250 8BITMIME\r\n
2023/11/27 08:14:54.951257 01HG7XGYYZCEB3WSNXPAVG4ZZN <| 250-example.local\r\n250 8BITMIME\r\n
2023/11/27 08:14:54.951335 01HG7XGYYZCEB3WSNXPAVG4ZZN -- pipe locked for tls connection
2023/11/27 08:14:54.951341 01HG7XGYYZCEB3WSNXPAVG4ZZN |> STARTTLS\r\n
2023/11/27 08:14:54.957949 01HG7XGYYZCEB3WSNXPAVG4ZZN -- tls connected, to pipe unlocked
2023/11/27 08:14:54.959709 01HG7XGYYZCEB3WSNXPAVG4ZZN -- from:alice@example.test to:bob@example.local elapse:14 msec
- Linux with Netfilter — the transparent proxy relies on
SO_ORIGINAL_DST, and Warp must run on the host that performs the NAT - Go 1.23 or later to build from source
- cgo (
CGO_ENABLED=1) if you want to use plugins; Go's plugin system does not work in a pure-Go build
go install github.com/linyows/warp/cmd/warp@latestOr download a pre-built binary from Releases.
warp -ip 0.0.0.0 -port 10025 -verboseRedirect outgoing SMTP traffic to Warp. When the MTA runs on the same host as Warp, the connection is created locally, so the rule goes in the OUTPUT chain:
iptables -t nat -A OUTPUT -p tcp --dport 25 -j DNAT --to-destination 127.0.0.1:10025When the MTA runs on another host, route its outgoing SMTP traffic through the Warp host — for example by making it the default gateway of the network of the MTA — and redirect the traffic in the PREROUTING chain instead:
iptables -t nat -A PREROUTING -p tcp --dport 25 -j DNAT --to-destination 203.0.113.10:10025Either way the NAT, and therefore the connection tracking entry Warp reads, has to happen on the host running Warp.
| Flag | Default | Description |
|---|---|---|
-ip |
127.0.0.1 |
Listen IP address |
-port |
(ephemeral) | Listen port |
-outbound-ip |
0.0.0.0 |
Source IP for outgoing connections |
-plugins |
Comma-separated plugin names: mysql, sqlite, file, slack |
|
-message-size-limit |
10240000 |
Maximum message size in bytes (~10MB) |
-verbose |
false |
Enable detailed logging |
-pprof |
Expose net/http/pprof on host:port; bind it to a loopback address |
|
-version |
false |
Show version information |
# Start Warp with MySQL logging and Slack notifications
warp -ip 0.0.0.0 -port 10025 \
-outbound-ip 203.0.113.10 \
-plugins mysql,slack \
-message-size-limit 20480000 \
-verboseWarp uses Go's plugin system to load .so files from /opt/warp/plugins/ (or the path specified by the PLUGIN_PATH environment variable). Only the plugins named in -plugins are loaded; the rest of the directory is ignored. Each plugin implements the Hook interface:
type Hook interface {
Name() string
AfterInit()
AfterComm(*AfterCommData) // Called after each SMTP command/response
AfterConn(*AfterConnData) // Called when a connection closes
}For message filtering, implement the FilterHook interface:
type FilterHook interface {
Hook
BeforeRelay(*BeforeRelayData) *FilterResult
}| Plugin | Description | Environment Variables |
|---|---|---|
| file | Logs all SMTP communications to a JSON file | FILE_PATH |
| mysql | Stores communications and connections in MySQL | DSN |
| sqlite | Stores communications and connections in SQLite | DSN |
| slack | Sends connection notifications to a Slack channel | SLACK_TOKEN, SLACK_CHANNEL |
Go plugins require cgo, so Warp and the plugins have to be built with CGO_ENABLED=1 — the build-withcgo target does that:
make build-withcgo
make file-plugin mysql-plugin # builds into plugins/*.so
PLUGIN_PATH=./plugins ./warp -ip 0.0.0.0 -port 10025 -plugins file,mysqlInstall the .so files into /opt/warp/plugins/ to run without PLUGIN_PATH.
Use the provided schema to set up the database:
mysql < misc/setup.sqlGo plugins are version-locked: a plugin has to be built with the same Go toolchain and the same dependency versions as Warp itself, or plugin.Open refuses to load it. Build them together.
- Email Gateway — Centralized SMTP inspection point for your organization
- Spam & Phishing Prevention — Filter outbound messages before they damage your reputation, with a filter plugin of your own
- Compliance & Auditing — Log every outbound email with full SMTP-level detail
- Data Loss Prevention (DLP) — Inspect message content for sensitive data before delivery
- Deliverability Monitoring — Track MX response times and SMTP status codes over time
- IP Reputation Management — Separate outgoing traffic onto different source addresses by running an instance per address
- Development & Debugging — Capture and inspect SMTP traffic during development
make build # build the warp binary
make test # unit tests
make integration # run a sending client and a receiving server against Warp
make test-all # full suite with race detector and coveragemake integration starts an outgoing mail client and an incoming SMTP server in Go and sends a message through Warp, so the whole path can be exercised on a workstation without any iptables rule.
- Fork it
- Create your feature branch (
git checkout -b my-new-feature) - Commit your changes (
git commit -am 'Add some feature') - Push to the branch (
git push origin my-new-feature) - Create a new Pull Request
MIT
