first commit
This commit is contained in:
690
README.md
Normal file
690
README.md
Normal file
@@ -0,0 +1,690 @@
|
||||
# portspoof-py
|
||||
|
||||
A Python asyncio rewrite of [portspoof](https://github.com/drk1wi/portspoof) — a TCP honeypot that makes every port on a machine appear open and running a real service.
|
||||
|
||||
When a port scanner connects to any TCP port, it receives a plausible fake service banner (SSH, HTTP, FTP, etc.). The banners are randomised from a signature library at startup, so the scanner cannot fingerprint the deception from banner patterns alone.
|
||||
|
||||
---
|
||||
|
||||
## Table of contents
|
||||
|
||||
1. [How it works](#how-it-works)
|
||||
2. [Requirements](#requirements)
|
||||
3. [Installation](#installation)
|
||||
4. [Quick start](#quick-start)
|
||||
5. [CLI reference](#cli-reference)
|
||||
6. [Signature file](#signature-file)
|
||||
7. [Config file](#config-file)
|
||||
8. [iptables rules](#iptables-rules)
|
||||
9. [JSON log](#json-log)
|
||||
10. [Web admin interface](#web-admin-interface)
|
||||
11. [Email alerts](#email-alerts)
|
||||
12. [systemd service](#systemd-service)
|
||||
13. [Recipes](#recipes)
|
||||
14. [Troubleshooting](#troubleshooting)
|
||||
|
||||
---
|
||||
|
||||
## How it works
|
||||
|
||||
```
|
||||
attacker kernel portspoof-py
|
||||
│ │ │
|
||||
│─── TCP SYN → port 443 ──►│ │
|
||||
│ │ iptables REDIRECT │
|
||||
│ │──► port 4444 ────────────►│
|
||||
│◄── TCP SYN-ACK ──────────│◄──────────────────────────│
|
||||
│─── data ────────────────►│──────────────────────────►│
|
||||
│ │ SO_ORIGINAL_DST │
|
||||
│ │ recovers port 443 │
|
||||
│◄── fake HTTPS banner ────│◄──────────────────────────│
|
||||
│─── FIN ─────────────────►│──────────────────────────►│
|
||||
```
|
||||
|
||||
1. **iptables PREROUTING REDIRECT** — on startup, three rules are added to the `nat` table. All incoming TCP traffic (except port 22 and the listener port itself) is transparently redirected to the single listener port.
|
||||
2. **Single asyncio listener** — one coroutine handles every connection. There is no thread pool and no polling loop.
|
||||
3. **SO_ORIGINAL_DST** — the Linux kernel preserves the originally-intended destination port in the socket. The handler reads it with `getsockopt(SOL_IP, SO_ORIGINAL_DST)`.
|
||||
4. **Pre-computed banner map** — at startup, each of the 65,536 ports is assigned a banner generated by processing a randomly-chosen regex pattern from the signature file. Banner generation is done once; serving is instant.
|
||||
5. **Graceful shutdown** — SIGTERM/SIGINT triggers the stop event, the server drains, the log queue is flushed, and the iptables rules are removed.
|
||||
|
||||
---
|
||||
|
||||
## Requirements
|
||||
|
||||
| Requirement | Notes |
|
||||
|---|---|
|
||||
| Python 3.11+ | Uses `asyncio`, `struct`, `socket`, `pathlib`, stdlib only |
|
||||
| Linux | `SO_ORIGINAL_DST` and `iptables` are Linux-specific |
|
||||
| Root | Required for iptables management; `--no-iptables` bypasses this |
|
||||
| iptables | Must be installed (`apt install iptables` or equivalent) |
|
||||
|
||||
No PyPI packages are needed at runtime.
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
**Run directly from the source tree (recommended for a lab machine):**
|
||||
|
||||
```bash
|
||||
git clone <repo>
|
||||
cd portspoof_py
|
||||
sudo python3 -m portspoof_py -s tools/portspoof_signatures
|
||||
```
|
||||
|
||||
**Install as a package:**
|
||||
|
||||
```bash
|
||||
pip install . # installs the portspoof-py entry point
|
||||
portspoof-py --help
|
||||
```
|
||||
|
||||
**Install build tool only if needed:**
|
||||
|
||||
```bash
|
||||
pip install flit
|
||||
flit build # produces a wheel in dist/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick start
|
||||
|
||||
### Test mode (no iptables, no root)
|
||||
|
||||
Useful for verifying the installation or developing.
|
||||
|
||||
```bash
|
||||
python3 -m portspoof_py \
|
||||
--no-iptables \
|
||||
-p 4444 \
|
||||
-s tools/portspoof_signatures \
|
||||
-v
|
||||
|
||||
# In another terminal — should receive a random service banner
|
||||
nc localhost 4444
|
||||
```
|
||||
|
||||
### Full honeypot mode (as root)
|
||||
|
||||
```bash
|
||||
sudo python3 -m portspoof_py \
|
||||
-p 4444 \
|
||||
-s /etc/portspoof/portspoof_signatures \
|
||||
-c /etc/portspoof/portspoof.conf \
|
||||
-l /var/log/portspoof/portspoof.jsonl
|
||||
```
|
||||
|
||||
After startup you can verify with:
|
||||
|
||||
```bash
|
||||
sudo iptables -t nat -L PREROUTING --line-numbers
|
||||
# Should show three portspoof rules
|
||||
|
||||
nmap -p 1-1000 <your-ip>
|
||||
# All ports should appear open
|
||||
```
|
||||
|
||||
Stop with Ctrl-C or `kill <pid>`. Rules are removed automatically.
|
||||
|
||||
### With the web admin interface
|
||||
|
||||
```bash
|
||||
# Create a credentials file first
|
||||
echo "admin:changeme" > admin.passwd
|
||||
|
||||
sudo python3 -m portspoof_py \
|
||||
-p 4444 \
|
||||
-s /etc/portspoof/portspoof_signatures \
|
||||
-l /var/log/portspoof/portspoof.jsonl \
|
||||
--admin-port 8080
|
||||
|
||||
# Open http://127.0.0.1:8080 in a browser (HTTP Basic Auth prompt will appear)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## CLI reference
|
||||
|
||||
```
|
||||
python3 -m portspoof_py [OPTIONS]
|
||||
portspoof-py [OPTIONS]
|
||||
```
|
||||
|
||||
### Listener
|
||||
|
||||
| Flag | Default | Description |
|
||||
|---|---|---|
|
||||
| `-p PORT` / `--port PORT` | `4444` | TCP port the honeypot listens on. This port is excluded from the iptables REDIRECT rule so it does not redirect to itself. |
|
||||
| `-i IP` / `--bind-ip IP` | all interfaces | Bind the listener to a specific IP address. |
|
||||
|
||||
### Signatures and configuration
|
||||
|
||||
| Flag | Default | Description |
|
||||
|---|---|---|
|
||||
| `-s FILE` / `--signatures FILE` | — | Path to the signature file. Each line is a regex-like pattern used to generate a banner. At startup, one pattern is chosen randomly for each of the 65,536 ports. Without this flag every port returns an empty banner (the port appears open but silent). |
|
||||
| `-c FILE` / `--config FILE` | — | Path to the config file. Overrides specific ports or port ranges with a fixed payload after the signature map is built. |
|
||||
|
||||
### Logging
|
||||
|
||||
| Flag | Default | Description |
|
||||
|---|---|---|
|
||||
| `-l FILE` / `--log-file FILE` | — | Append JSON connection events to this file. One line per connection. |
|
||||
| `-v` / `--verbose` | off | Print each connection event as JSON to stdout. |
|
||||
|
||||
### iptables
|
||||
|
||||
| Flag | Default | Description |
|
||||
|---|---|---|
|
||||
| `--iface IFACE` | all interfaces | Restrict the iptables REDIRECT rule to a specific network interface (e.g. `eth0`). Useful when one interface is internal and should not be intercepted. |
|
||||
| `--no-iptables` | off | Skip iptables setup and teardown entirely. The listener still runs on `-p PORT`. Use for local testing or when you manage iptables externally. |
|
||||
|
||||
### Web admin
|
||||
|
||||
| Flag | Default | Description |
|
||||
|---|---|---|
|
||||
| `--admin-port PORT` | disabled | Start the web admin interface on this port. |
|
||||
| `--admin-host HOST` | `127.0.0.1` | Address the admin interface binds to. Set to `0.0.0.0` to expose it on all interfaces (protect with a firewall). |
|
||||
| `--admin-passwd FILE` | `admin.passwd` | File containing `username:password` on a single line. Required when `--admin-port` is used. |
|
||||
| `--email-config FILE` | `email.json` | JSON file where email alert settings are stored. Created automatically when you first save settings from the admin UI. |
|
||||
|
||||
---
|
||||
|
||||
## Signature file
|
||||
|
||||
The signature file contains one pattern per line. Blank lines are ignored. The bundled file `tools/portspoof_signatures` contains 8,962 patterns derived from the nmap service detection database.
|
||||
|
||||
Each pattern is passed through a three-pass regex materialiser at startup to produce a concrete banner string:
|
||||
|
||||
**Pass 1** — group and bracket expansion
|
||||
- `(...)` groups are stripped, their content kept.
|
||||
- `[abc]`, `[a-z]`, `[0-9]`, `[^\0]` etc. are expanded: a random character (or random-length sequence for `*`/`+` quantifiers) is chosen from the defined set.
|
||||
|
||||
**Pass 2** — special sequence substitution
|
||||
- `\w` → a random lowercase letter `a`–`z`
|
||||
- `\d` → a random digit `0`–`9`
|
||||
- `.` → a random lowercase letter
|
||||
- `\n`, `\r`, `\t` → literal newline, carriage return, tab
|
||||
|
||||
**Pass 3** — hex escape decoding
|
||||
- `\xNN` → the byte with hex value `NN`
|
||||
|
||||
**Example patterns and what they can produce:**
|
||||
|
||||
```
|
||||
# Input pattern → Example output
|
||||
SSH-2.0-OpenSSH_([\w._-]+)\r\n → SSH-2g0-OpenSSH_QpKz\r\n
|
||||
220 ([-.\w]+) ESMTP\r\n → 220 mxprod ESMTP\r\n
|
||||
HTTP/1.\d \d\d\d [\w ]+\r\n → HTTP/1.4 200 OK\r\n
|
||||
\x80\x01\x00\x80 → (four literal bytes)
|
||||
AMServer → AMServer
|
||||
```
|
||||
|
||||
The file is read with latin-1 encoding so binary-like content in patterns does not cause decode errors.
|
||||
|
||||
---
|
||||
|
||||
## Config file
|
||||
|
||||
The config file lets you pin specific ports or port ranges to a fixed payload, overriding the random assignment from the signature file. It is applied after the signature map is built.
|
||||
|
||||
### Format
|
||||
|
||||
Lines beginning with `#` are comments and are ignored. Each active line has the form:
|
||||
|
||||
```
|
||||
PORT "payload"
|
||||
START-END "payload"
|
||||
```
|
||||
|
||||
The payload is everything between the **first** and **last** double-quote on the line. It is processed through the same three-pass materialiser as signature patterns, so hex escapes and regex metacharacters work.
|
||||
|
||||
### Example
|
||||
|
||||
```
|
||||
# Single port — port 22 returns a specific SSH banner
|
||||
22 "SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.6\r\n"
|
||||
|
||||
# Port range — ports 80 through 443 return an HTTP 200
|
||||
80-443 "HTTP/1.1 200 OK\r\nServer: nginx/1.24.0\r\nContent-Length: 0\r\n\r\n"
|
||||
|
||||
# Hex payload — raw bytes
|
||||
9200 "\x7b\x22\x6e\x61\x6d\x65\x22\x3a\x22\x65\x6c\x61\x73\x74\x69\x63\x22\x7d"
|
||||
```
|
||||
|
||||
The bundled `tools/portspoof.conf` shows the full comment syntax and more examples.
|
||||
|
||||
---
|
||||
|
||||
## iptables rules
|
||||
|
||||
When running without `--no-iptables`, three rules are appended to the `nat` PREROUTING chain at startup and removed at shutdown.
|
||||
|
||||
### Rules added (example with `-p 4444 --admin-port 8080`)
|
||||
|
||||
```
|
||||
iptables -t nat -A PREROUTING -p tcp --dport 22 -j RETURN
|
||||
iptables -t nat -A PREROUTING -p tcp --dport 8080 -j RETURN
|
||||
iptables -t nat -A PREROUTING -p tcp --dport 4444 -j RETURN
|
||||
iptables -t nat -A PREROUTING -p tcp -j REDIRECT --to-port 4444
|
||||
```
|
||||
|
||||
The exempt ports are always added **before** the REDIRECT rule in this order:
|
||||
|
||||
1. Port 22 (SSH) — always first, so you cannot lock yourself out.
|
||||
2. `--admin-port` — exempted automatically if specified, so the admin interface remains reachable.
|
||||
3. The listener port (`-p`) — prevents a redirect loop.
|
||||
|
||||
Without `--admin-port`, the middle rule is omitted.
|
||||
|
||||
With `--iface eth0`, each rule gains `--in-interface eth0`.
|
||||
|
||||
### Verifying the rules
|
||||
|
||||
```bash
|
||||
sudo iptables -t nat -L PREROUTING -v --line-numbers
|
||||
```
|
||||
|
||||
### Manual cleanup
|
||||
|
||||
If the process is killed with SIGKILL (skipping the shutdown handler), the systemd `ExecStopPost` directives remove the rules. You can also remove them manually:
|
||||
|
||||
```bash
|
||||
sudo iptables -t nat -D PREROUTING -p tcp --dport 22 -j RETURN
|
||||
sudo iptables -t nat -D PREROUTING -p tcp --dport 4444 -j RETURN
|
||||
sudo iptables -t nat -D PREROUTING -p tcp -j REDIRECT --to-port 4444
|
||||
```
|
||||
|
||||
### Persistence across reboots
|
||||
|
||||
iptables rules are not persistent by default. The systemd service re-adds them on start. If you want them to survive without the service running, use `iptables-save` / `iptables-restore` or `netfilter-persistent`.
|
||||
|
||||
---
|
||||
|
||||
## JSON log
|
||||
|
||||
With `-l FILE`, each accepted connection appends one JSON object followed by a newline to the log file.
|
||||
|
||||
### Fields
|
||||
|
||||
| Field | Type | Description |
|
||||
|---|---|---|
|
||||
| `timestamp` | string | ISO 8601 UTC timestamp with microseconds |
|
||||
| `src_ip` | string | Source IP address of the scanner |
|
||||
| `src_port` | integer | Source (ephemeral) port of the scanner |
|
||||
| `dst_port` | integer | Port the scanner originally connected to (recovered via `SO_ORIGINAL_DST`) |
|
||||
| `banner_hex` | string | Hex-encoded bytes of the banner that was sent |
|
||||
| `banner_len` | integer | Length of the banner in bytes |
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{"timestamp": "2025-06-01T14:32:07.841203+00:00", "src_ip": "198.51.100.42", "src_port": 54312, "dst_port": 443, "banner_hex": "485454502f312e31203230300d0a", "banner_len": 14}
|
||||
```
|
||||
|
||||
### Querying the log
|
||||
|
||||
```bash
|
||||
# Show all source IPs sorted by frequency
|
||||
jq -r .src_ip portspoof.jsonl | sort | uniq -c | sort -rn | head -20
|
||||
|
||||
# Show all targeted ports sorted by frequency
|
||||
jq -r .dst_port portspoof.jsonl | sort | uniq -c | sort -rn | head -20
|
||||
|
||||
# Show connections from a specific IP
|
||||
jq 'select(.src_ip == "198.51.100.42")' portspoof.jsonl
|
||||
|
||||
# Decode a banner from hex
|
||||
jq -r .banner_hex portspoof.jsonl | head -1 | xxd -r -p
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Web admin interface
|
||||
|
||||
Enable with `--admin-port PORT`. The interface binds to `127.0.0.1` by default; use `--admin-host 0.0.0.0` to expose it externally (ensure it is firewalled).
|
||||
|
||||
### Authentication
|
||||
|
||||
Every request requires HTTP Basic Auth. Create a credentials file before starting:
|
||||
|
||||
```bash
|
||||
echo "admin:changeme" > admin.passwd
|
||||
```
|
||||
|
||||
The file must contain a single line in `username:password` format. Pass a custom path with `--admin-passwd FILE`.
|
||||
|
||||
### Dashboard — `GET /`
|
||||
|
||||
A dark-themed HTML page that auto-refreshes every 5 seconds. Sections:
|
||||
|
||||
- **Stat cards** — total connections, connections in the last 60 seconds, uptime, ports mapped, and last connection time.
|
||||
- **Top source IPs** — the 10 most active scanner addresses since startup.
|
||||
- **Top targeted ports** — the 10 most-probed ports since startup.
|
||||
- **Banner lookup** — enter any port number (0–65535) and see the hex and text preview of the banner that port will send. The result persists across auto-refreshes when using the `?port=N` query parameter.
|
||||
- **Recent connections** — the 50 most-recent connections, newest first, with timestamp, source, destination port, banner hex excerpt, and banner length.
|
||||
|
||||
### Banner lookup shortcut
|
||||
|
||||
Append `?port=N` to the dashboard URL to pre-populate the lookup:
|
||||
|
||||
```
|
||||
http://127.0.0.1:8080/?port=443
|
||||
```
|
||||
|
||||
### JSON API
|
||||
|
||||
All endpoints require Basic Auth and return `Content-Type: application/json`.
|
||||
|
||||
#### `GET /api/stats`
|
||||
|
||||
Current statistics snapshot.
|
||||
|
||||
```json
|
||||
{
|
||||
"uptime": "2h 14m 37s",
|
||||
"total_connections": 18432,
|
||||
"last_connection": "2025-06-01T14:32:07.841203+00:00",
|
||||
"connections_per_min": 312,
|
||||
"ports_mapped": 65536,
|
||||
"top_ips": [
|
||||
{"ip": "198.51.100.42", "count": 9821},
|
||||
{"ip": "203.0.113.7", "count": 4102}
|
||||
],
|
||||
"top_ports": [
|
||||
{"port": 80, "count": 3201},
|
||||
{"port": 443, "count": 2987},
|
||||
{"port": 22, "count": 2104}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### `GET /api/connections?limit=N`
|
||||
|
||||
The N most-recent connection events, newest first. `limit` defaults to 50, maximum 500.
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"timestamp": "2025-06-01T14:32:07.841203+00:00",
|
||||
"src_ip": "198.51.100.42",
|
||||
"src_port": 54312,
|
||||
"dst_port": 443,
|
||||
"banner_hex": "485454502f312e31203230300d0a",
|
||||
"banner_len": 14
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
#### `GET /api/banner?port=N`
|
||||
|
||||
The pre-computed banner for port N.
|
||||
|
||||
```json
|
||||
{
|
||||
"port": 443,
|
||||
"banner_hex": "485454502f312e31203230300d0a",
|
||||
"banner_len": 14,
|
||||
"banner_text": "HTTP/1.1 200\r\n"
|
||||
}
|
||||
```
|
||||
|
||||
`banner_text` is the banner decoded as latin-1. Non-printable bytes will appear as replacement characters in some terminals.
|
||||
|
||||
#### `GET /api/email/config`
|
||||
|
||||
Current email alert configuration (password is masked).
|
||||
|
||||
---
|
||||
|
||||
## Email alerts
|
||||
|
||||
portspoof can send email digest alerts when connections are detected. Configuration is done entirely through the admin UI — no restart required.
|
||||
|
||||
### Setup
|
||||
|
||||
1. Start with `--admin-port` (the email config page is always available).
|
||||
2. Open `http://127.0.0.1:8080/config` (or click **email alerts** in the dashboard header).
|
||||
3. Fill in your SMTP details and click **Save**.
|
||||
4. Click **Send test email** to verify delivery.
|
||||
|
||||
### How batching works
|
||||
|
||||
Rather than sending one email per connection, portspoof collects events and sends a single digest:
|
||||
|
||||
1. When a matching connection arrives, it is added to a pending list and a flush timer starts.
|
||||
2. Any further matching connections within the **batch delay** window are added to the same list.
|
||||
3. After the batch delay elapses, one email is sent summarising all collected events.
|
||||
4. The **cooldown** prevents a new batch from being sent until that many seconds have passed since the last one, avoiding email floods during sustained scans.
|
||||
|
||||
### Configuration options
|
||||
|
||||
| Option | Default | Description |
|
||||
|---|---|---|
|
||||
| Enabled | off | Master switch — no emails are sent when disabled. |
|
||||
| SMTP host | — | Hostname of your SMTP server (e.g. `smtp.gmail.com`). |
|
||||
| SMTP port | `587` | Port to connect to. |
|
||||
| Use STARTTLS | on | Upgrade the connection with STARTTLS. Disable for plain SMTP or SSL-only servers. |
|
||||
| SMTP username | — | Login username for SMTP authentication. |
|
||||
| SMTP password | — | Login password. Stored in the config file; masked in the UI. |
|
||||
| From address | — | Sender address. Defaults to the SMTP username if blank. |
|
||||
| To address | — | Recipient address for alert emails. |
|
||||
| Trigger ports | (all) | Comma-separated list of ports that trigger alerts. Leave blank to alert on every port. |
|
||||
| Batch delay | `60` s | How long to wait and collect events before sending one digest email. |
|
||||
| Cooldown | `300` s | Minimum time between batch emails. |
|
||||
|
||||
### Config file
|
||||
|
||||
Settings are saved to `email.json` (or the path given by `--email-config`) as JSON. The file is created on first save. You can edit it directly; changes take effect at the next batch send.
|
||||
|
||||
```json
|
||||
{
|
||||
"enabled": true,
|
||||
"smtp_host": "smtp.gmail.com",
|
||||
"smtp_port": 587,
|
||||
"smtp_starttls": true,
|
||||
"smtp_user": "you@gmail.com",
|
||||
"smtp_password": "app-password-here",
|
||||
"from_addr": "",
|
||||
"to_addr": "alerts@example.com",
|
||||
"trigger_ports": [22, 3389],
|
||||
"batch_delay_seconds": 60,
|
||||
"cooldown_seconds": 300
|
||||
}
|
||||
```
|
||||
|
||||
### Example digest email
|
||||
|
||||
```
|
||||
Subject: [portspoof] 3 connections detected
|
||||
|
||||
portspoof alert — 3 connections
|
||||
|
||||
2025-06-01 14:32:07 198.51.100.42:54312 → port 22 (14 B 485353482d322e302d4f70656e5353…)
|
||||
2025-06-01 14:32:09 198.51.100.42:54398 → port 23 (6 B deadbeef0102)
|
||||
2025-06-01 14:32:11 203.0.113.7:61204 → port 3389 (0 B )
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## systemd service
|
||||
|
||||
The included `portspoof.service` file runs portspoof as a system service that starts after the network is up, restarts on failure, and removes iptables rules even if it is killed with SIGKILL.
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
# Copy files
|
||||
sudo cp portspoof.service /etc/systemd/system/
|
||||
sudo mkdir -p /etc/portspoof /var/log/portspoof
|
||||
sudo cp tools/portspoof_signatures /etc/portspoof/
|
||||
sudo cp tools/portspoof.conf /etc/portspoof/
|
||||
|
||||
# Enable and start
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable portspoof
|
||||
sudo systemctl start portspoof
|
||||
|
||||
# Check status
|
||||
sudo systemctl status portspoof
|
||||
sudo journalctl -u portspoof -f
|
||||
```
|
||||
|
||||
### Adding the admin interface to the service
|
||||
|
||||
```bash
|
||||
# Create credentials file
|
||||
echo "admin:changeme" | sudo tee /etc/portspoof/admin.passwd
|
||||
sudo chmod 600 /etc/portspoof/admin.passwd
|
||||
```
|
||||
|
||||
Edit `/etc/systemd/system/portspoof.service` and append the flags to `ExecStart`:
|
||||
|
||||
```ini
|
||||
ExecStart=/usr/bin/python3 -m portspoof_py \
|
||||
-s /etc/portspoof/portspoof_signatures \
|
||||
-c /etc/portspoof/portspoof.conf \
|
||||
-l /var/log/portspoof/portspoof.jsonl \
|
||||
--admin-port 8080 \
|
||||
--admin-passwd /etc/portspoof/admin.passwd \
|
||||
--email-config /etc/portspoof/email.json
|
||||
```
|
||||
|
||||
Then reload:
|
||||
|
||||
```bash
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl restart portspoof
|
||||
```
|
||||
|
||||
### Stopping
|
||||
|
||||
```bash
|
||||
sudo systemctl stop portspoof
|
||||
# iptables rules are removed during graceful shutdown
|
||||
# ExecStopPost directives handle the SIGKILL case
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Recipes
|
||||
|
||||
### Honeypot on a dedicated interface only
|
||||
|
||||
```bash
|
||||
sudo python3 -m portspoof_py \
|
||||
-p 4444 \
|
||||
--iface eth1 \
|
||||
-s /etc/portspoof/portspoof_signatures \
|
||||
-l /var/log/portspoof/portspoof.jsonl
|
||||
```
|
||||
|
||||
Only traffic arriving on `eth1` is redirected. Traffic on other interfaces (e.g. a management interface) is unaffected.
|
||||
|
||||
### Monitor in real time with the admin API
|
||||
|
||||
```bash
|
||||
# Poll stats every second
|
||||
watch -n1 'curl -su admin:changeme http://127.0.0.1:8080/api/stats | python3 -m json.tool'
|
||||
|
||||
# Tail recent connections
|
||||
watch -n2 'curl -su admin:changeme "http://127.0.0.1:8080/api/connections?limit=10" | python3 -m json.tool'
|
||||
```
|
||||
|
||||
### Inspect what a specific port would send
|
||||
|
||||
```bash
|
||||
curl -su admin:changeme "http://127.0.0.1:8080/api/banner?port=22" | python3 -m json.tool
|
||||
|
||||
# Decode banner bytes directly
|
||||
curl -su admin:changeme "http://127.0.0.1:8080/api/banner?port=22" \
|
||||
| python3 -c "import sys,json; print(json.load(sys.stdin)['banner_text'])"
|
||||
```
|
||||
|
||||
### Custom banners for a port range
|
||||
|
||||
Create `/etc/portspoof/portspoof.conf`:
|
||||
|
||||
```
|
||||
# Make ports 8080-8090 look like nginx
|
||||
8080-8090 "HTTP/1.1 200 OK\r\nServer: nginx/1.24.0\r\nContent-Length: 612\r\n\r\n"
|
||||
|
||||
# Make port 3306 look like MySQL
|
||||
3306 "\x4a\x00\x00\x00\x0a\x38\x2e\x30\x2e\x33\x36\x00"
|
||||
```
|
||||
|
||||
### Analyse a scan after the fact
|
||||
|
||||
```bash
|
||||
# Top 10 IPs
|
||||
jq -r .src_ip /var/log/portspoof/portspoof.jsonl \
|
||||
| sort | uniq -c | sort -rn | head -10
|
||||
|
||||
# All ports targeted in the last hour
|
||||
jq -r 'select(.timestamp > "2025-06-01T13:00:00") | .dst_port' \
|
||||
/var/log/portspoof/portspoof.jsonl | sort -n | uniq -c | sort -rn | head -20
|
||||
|
||||
# Check if a specific IP hit you
|
||||
grep '"src_ip": "198.51.100.42"' /var/log/portspoof/portspoof.jsonl | wc -l
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### `ERROR: root required for iptables management`
|
||||
|
||||
Run with `sudo`, or pass `--no-iptables` if you are testing locally.
|
||||
|
||||
### `ERROR: iptables setup failed`
|
||||
|
||||
Check that iptables is installed and the `nat` table is available:
|
||||
|
||||
```bash
|
||||
sudo iptables -t nat -L
|
||||
```
|
||||
|
||||
On some minimal systems you may need:
|
||||
|
||||
```bash
|
||||
sudo apt install iptables # Debian/Ubuntu
|
||||
sudo modprobe ip_tables
|
||||
sudo modprobe iptable_nat
|
||||
```
|
||||
|
||||
### Banners contain `000` instead of null bytes
|
||||
|
||||
This is expected. The signature file uses `\0` to represent null bytes in the nmap probe format, but the banner materialiser does not interpret `\0` as a null byte — it outputs the ASCII character `0` (0x30). Use `\x00` in signature patterns or config payloads when you need an actual null byte.
|
||||
|
||||
### Port 22 is redirected and SSH breaks
|
||||
|
||||
The iptables setup adds a `RETURN` rule for port 22 before the `REDIRECT` rule, so SSH should always be exempt. If you are still locked out, verify the rule order:
|
||||
|
||||
```bash
|
||||
sudo iptables -t nat -L PREROUTING --line-numbers
|
||||
```
|
||||
|
||||
The `RETURN` rules for port 22 and the listener port must appear **before** the `REDIRECT` rule. If they are in the wrong order, remove all portspoof rules and restart the service.
|
||||
|
||||
### `SO_ORIGINAL_DST` returns the listener port instead of the original port
|
||||
|
||||
This happens when there is no `REDIRECT` rule in effect (e.g. `--no-iptables` mode, or the rule is missing). The server falls back to reporting the listener port as the destination. Banners are still sent; the logged `dst_port` will be the listener port rather than the port the scanner connected to.
|
||||
|
||||
### Admin interface is not accessible from a remote machine
|
||||
|
||||
By default `--admin-host` is `127.0.0.1` (localhost only). To expose it remotely:
|
||||
|
||||
```bash
|
||||
--admin-host 0.0.0.0 --admin-port 8080
|
||||
```
|
||||
|
||||
Then allow the port in your firewall and restrict access to trusted IPs:
|
||||
|
||||
```bash
|
||||
sudo iptables -A INPUT -p tcp --dport 8080 -s <your-ip> -j ACCEPT
|
||||
sudo iptables -A INPUT -p tcp --dport 8080 -j DROP
|
||||
```
|
||||
|
||||
### Startup is slow (~4–5 seconds)
|
||||
|
||||
The banner map is built synchronously at startup: 65,536 ports × one signature materialisation each. This is expected and is a one-time cost. After startup, every connection is served from the pre-computed map with no per-connection processing.
|
||||
Reference in New Issue
Block a user