Skip to content

Commit 07c7268

Browse files
committed
sip: support TLS listeners and transport telemetry
1 parent 81154ff commit 07c7268

4 files changed

Lines changed: 73 additions & 16 deletions

File tree

.env.example

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ COMPOSE_FILE=docker-compose.host.yml
7272
# Run only specific honeypots (default: all)
7373
# Protocols: SSH, TNET, FTP, RDP, SMB, SIP, HTTP, SMTP
7474
# Use PROTO:PORT syntax to run on multiple ports, e.g. HTTP:80,HTTP:443
75+
# Declared protocol options can follow the port, e.g. SIP:5061:TLS.
76+
# SIP on 5061 auto-enables TLS by convention; SIP:5061:TLS is equivalent.
7577
# ENABLED_PROTOCOLS=SSH,TNET,FTP,RDP,SMB,SIP,HTTP,SMTP
7678

7779
# Number of uvicorn worker processes (systemd only; Docker uses 2 via CLI)

CLAUDE.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ Port 80 is open to all — it's a honeypot port. Port 443 can also be mapped to
230230
| `REDIS_HOST` | `localhost` | Redis server hostname (set to `redis` in Docker) |
231231
| `REDIS_DB` | `0` | Redis database index |
232232
| `DB_DIR` | `data` | Directory for SQLite databases and caches |
233-
| `ENABLED_PROTOCOLS` | all protocols | Comma-separated list of active protocols with optional port overrides, e.g. `SSH,SMTP:25,SMTP:587,HTTP:80,HTTP:443`. Empty string = ingest-only mode (no local honeypots). |
233+
| `ENABLED_PROTOCOLS` | all protocols | Comma-separated list of active protocols with optional port overrides and declared protocol options, e.g. `SSH,SMTP:25,SMTP:587,HTTP:80,HTTP:443,SIP:5061:TLS`. Empty string = ingest-only mode (no local honeypots). |
234234
| `DEFAULT_HOSTNAME` | unset | Canonical hostname all protocols advertise by default (an FQDN, e.g. `mail.corp.example`); each renders its own form (SMTP uses it as-is, SMB derives the short NetBIOS name). Per-protocol vars (`SMTP_HOSTNAME`, `SMB_SERVER_NAME`) override it; `PROTOCOL_VAR=auto` forces that protocol's built-in default instead. Also added to the self-redaction identity so it's scrubbed from captured fields. **Unset = each protocol's prior default** (SMTP reverse-DNS, SMB `WIN-SRV####`). **Docker (bridge networking only):** set this — bridge in-container discovery can't recover the advertised hostname — and also set `REDACT_SELF_IPS` (it can't see the public IP either). Under **host networking** (`docker-compose.host.yml`, the default in `.env.example`) the container sees the host's real IP/PTR, so discovery works like a bare-metal install and both are optional. |
235235

236236
### Web Server (`main.py`)
@@ -321,7 +321,9 @@ Port 80 is open to all — it's a honeypot port. Port 443 can also be mapped to
321321

322322
| Variable | Default | Purpose |
323323
|----------|---------|---------|
324-
| `SIP_PORT` | `5060` | Listening port (UDP + TCP) |
324+
| `SIP_PORT` | `5060` | Listening port (UDP + TCP for cleartext SIP; TCP/TLS only when SSL mode is enabled). `ENABLED_PROTOCOLS=SIP:5061` auto-enables TLS by convention; `SIP:5061:TLS` is also accepted. |
325+
| `SIP_TLS_CERT_PATH` | `data/sip_tls.crt` | TLS certificate path for SIP/TLS; auto-generated if missing. |
326+
| `SIP_TLS_KEY_PATH` | `data/sip_tls.key` | TLS key path for SIP/TLS; auto-generated if missing. |
325327
| `SIP_OK_DIALPLAN` | `+,bare,00,011,9` | Which dialed forms the fake PBX answers (200) vs rejects (404). `all` (any resolvable E.164), `none`, or a comma list of dial-out prefixes prepended to the bare E.164 digits: `bare` (no prefix), `+`, or digit prefixes like `00`/`011`/`9`. A dial is accepted iff its canonicalized digits equal `prefix + computed-E.164`; numbers that resolve to no E.164 are always rejected. Knocks are recorded regardless. |
326328
| `SIP_REALM` | `asterisk` | SIP realm in authentication challenge |
327329
| `SIP_AUTH_CHALLENGE_MODE` | `mixed` | `always`, `never`, or `mixed` |
@@ -410,7 +412,8 @@ knocks_tnet(... username, password)
410412
knocks_ftp(... username, password)
411413
knocks_smtp(... username, password, smtp_stage, smtp_mail_from, smtp_rcpt_to, subject, body_id) -- body_id → smtp_body_intel.id (full body stored deduped there, not inline)
412414
knocks_sip(... sip_method, sip_dial_string, sip_dial_number, sip_call_id, sip_cseq,
413-
sip_extension, sip_dial_country, sip_dial_country_name, sip_dial_lat, sip_dial_lng)
415+
sip_port, sip_transport, sip_extension, sip_dial_country, sip_dial_country_name,
416+
sip_dial_lat, sip_dial_lng)
414417
knocks_smb(... username, smb_action, smb_share, smb_file, smb_version, smb_domain, smb_host)
415418
knocks_rdp(... username, rdp_source, domain)
416419
knocks_http(... http_method, http_path, http_user_agent, http_body)

honeypots/sip_honeypot.py

Lines changed: 60 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
#!/usr/bin/env python3
2+
import argparse
23
import base64
34
import json
45
import os
56
import random
67
import re
78
import socket
89
import sqlite3
10+
import ssl
911
import string
1012
import sys
1113
import threading
@@ -36,11 +38,14 @@
3638
PerIpTokenBucket,
3739
create_dualstack_tcp_listener,
3840
create_dualstack_udp_listener,
41+
ensure_self_signed_server_cert,
3942
is_blocked,
4043
normalize_ip,
4144
)
4245

4346
SIP_PORT = int(os.environ.get('SIP_PORT', '5060'))
47+
SIP_TLS_CERT_PATH = os.environ.get('SIP_TLS_CERT_PATH', 'data/sip_tls.crt')
48+
SIP_TLS_KEY_PATH = os.environ.get('SIP_TLS_KEY_PATH', 'data/sip_tls.key')
4449
SIP_REALM = os.environ.get('SIP_REALM', 'asterisk')
4550
SIP_SERVER_HEADER = os.environ.get('SIP_SERVER_HEADER', 'Asterisk PBX 18.0.0')
4651
SIP_SDP_SESSION_NAME = os.environ.get('SIP_SDP_SESSION_NAME', 'Asterisk')
@@ -960,7 +965,7 @@ def emit_knock(client_ip, extra=None, dedup_key=None):
960965
print(json.dumps(knock), flush=True)
961966

962967

963-
def process_sip_request(req, client_ip, allow_b2bua=False):
968+
def process_sip_request(req, client_ip, client_port=None, transport=None, allow_b2bua=False):
964969
headers = req.get('headers', {})
965970
method = req.get('method', 'UNKNOWN')
966971
uri = req.get('uri', '')
@@ -974,6 +979,8 @@ def process_sip_request(req, client_ip, allow_b2bua=False):
974979
dial_string = re.sub(r'^sips?:', '', uri).split('@')[0] if uri else ''
975980
common = {
976981
'sip_method': method,
982+
'sip_port': SIP_PORT,
983+
'sip_transport': transport or '',
977984
'sip_dial_string': dial_string,
978985
'sip_call_id': _header_first(headers, 'call-id') or '',
979986
'sip_cseq': _header_first(headers, 'cseq') or '',
@@ -1159,7 +1166,7 @@ def udp_send(resp, _addr=addr): sock.sendto(resp, _addr)
11591166
if HAVE_B2BUA and sip_b2bua.handle_in_dialog(req, client_ip, udp_send):
11601167
trace(session_id, client_ip, 'udp_b2bua_in_dialog', method=req.get('method'))
11611168
continue
1162-
result = process_sip_request(req, client_ip, allow_b2bua=True)
1169+
result = process_sip_request(req, client_ip, client_port=addr[1], transport='udp', allow_b2bua=True)
11631170
if result[0] == 'INVITE_B2BUA':
11641171
threading.Thread(
11651172
target=_start_b2bua_or_fake,
@@ -1221,7 +1228,7 @@ def recv_one_sip_message(sock, timeout):
12211228
return None, 'incomplete_timeout', len(buf)
12221229

12231230

1224-
def handle_tcp_client(client_sock, client_ip):
1231+
def handle_tcp_client(client_sock, client_ip, transport='tcp'):
12251232
session_id = uuid.uuid4().hex[:8]
12261233
started_at = time.time()
12271234
message_count = 0
@@ -1251,7 +1258,7 @@ def handle_tcp_client(client_sock, client_ip):
12511258
trace(session_id, client_ip, 'tcp_parse_invalid', index=message_count)
12521259
break
12531260
trace(session_id, client_ip, 'tcp_parsed', index=message_count, method=req.get('method'), uri=req.get('uri'))
1254-
result = process_sip_request(req, client_ip)
1261+
result = process_sip_request(req, client_ip, client_port=client_sock.getpeername()[1], transport=transport)
12551262
if result[0] == 'INVITE_FAKE':
12561263
try:
12571264
_send_invite_sequence(result[1], client_sock.sendall)
@@ -1293,7 +1300,7 @@ def handle_tcp_client(client_sock, client_ip):
12931300
)
12941301

12951302

1296-
def tcp_loop(sock):
1303+
def tcp_loop(sock, ssl_context=None):
12971304
while True:
12981305
client, addr = sock.accept()
12991306
client_ip = normalize_ip(addr[0])
@@ -1304,22 +1311,62 @@ def tcp_loop(sock):
13041311
except Exception:
13051312
pass
13061313
continue
1307-
threading.Thread(target=handle_tcp_client, args=(client, client_ip), daemon=True).start()
1314+
if ssl_context:
1315+
try:
1316+
client = ssl_context.wrap_socket(client, server_side=True)
1317+
except (ssl.SSLError, OSError):
1318+
trace(f"t{uuid.uuid4().hex[:8]}", client_ip, 'tls_handshake_failed')
1319+
try:
1320+
client.close()
1321+
except Exception:
1322+
pass
1323+
continue
1324+
transport = 'tls' if ssl_context else 'tcp'
1325+
threading.Thread(target=handle_tcp_client, args=(client, client_ip, transport), daemon=True).start()
13081326

13091327

1310-
def start_honeypot():
1328+
def start_honeypot(use_ssl=False, cert_path=None, key_path=None):
13111329
_seed_dial_cache_from_db()
13121330
if HAVE_B2BUA and sip_b2bua.capturing():
13131331
sip_dial_profile.ensure_table()
1314-
udp_sock = create_dualstack_udp_listener(SIP_PORT)
1315-
13161332
tcp_sock = create_dualstack_tcp_listener(SIP_PORT, backlog=200)
13171333

1318-
print(f'🚀 SIP Honeypot Active on Port {SIP_PORT} (UDP+TCP IPv4+IPv6). Collecting knocks...', flush=True)
1334+
ssl_context = None
1335+
if use_ssl:
1336+
cert_path = cert_path or SIP_TLS_CERT_PATH
1337+
key_path = key_path or SIP_TLS_KEY_PATH
1338+
ensure_self_signed_server_cert(
1339+
cert_path=cert_path,
1340+
key_path=key_path,
1341+
subject='/CN=localhost/O=Asterisk/C=US',
1342+
days=825,
1343+
)
1344+
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
1345+
ssl_context.load_cert_chain(certfile=cert_path, keyfile=key_path)
1346+
1347+
if use_ssl:
1348+
print(f'🚀 SIP/TLS Honeypot Active on Port {SIP_PORT} (TCP/TLS IPv4+IPv6). Collecting knocks...', flush=True)
1349+
else:
1350+
udp_sock = create_dualstack_udp_listener(SIP_PORT)
1351+
print(f'🚀 SIP Honeypot Active on Port {SIP_PORT} (UDP+TCP IPv4+IPv6). Collecting knocks...', flush=True)
1352+
threading.Thread(target=udp_loop, args=(udp_sock,), daemon=True).start()
1353+
tcp_loop(tcp_sock, ssl_context=ssl_context)
1354+
1355+
1356+
def main():
1357+
global SIP_PORT
1358+
parser = argparse.ArgumentParser()
1359+
parser.add_argument('--port', type=int, default=SIP_PORT)
1360+
parser.add_argument('--ssl', dest='ssl', action='store_true', default=None)
1361+
parser.add_argument('--no-ssl', dest='ssl', action='store_false')
1362+
parser.add_argument('--ssl-cert', default=SIP_TLS_CERT_PATH)
1363+
parser.add_argument('--ssl-key', default=SIP_TLS_KEY_PATH)
1364+
args = parser.parse_args()
1365+
SIP_PORT = args.port
13191366

1320-
threading.Thread(target=udp_loop, args=(udp_sock,), daemon=True).start()
1321-
tcp_loop(tcp_sock)
1367+
use_ssl = args.ssl if args.ssl is not None else (SIP_PORT == 5061)
1368+
start_honeypot(use_ssl=use_ssl, cert_path=args.ssl_cert, key_path=args.ssl_key)
13221369

13231370

13241371
if __name__ == '__main__':
1325-
start_honeypot()
1372+
main()

protocols/sip.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,17 @@ def db_update(data, cur, ctx):
3333
badge_color="#ff7a00",
3434
ui_order=60,
3535
honeypot_script="honeypots/sip_honeypot.py",
36+
option_args={
37+
"TLS": ["--ssl"],
38+
},
3639
description="SIP allows humans and bots to make phone calls.",
3740
default_enabled_entries=["SIP"],
3841
supports_user_panel=False,
3942
supports_pass_panel=False,
4043
knock_table="knocks_sip",
4144
columns=[
45+
Column("sip_port", "INTEGER"),
46+
Column("sip_transport", "TEXT"),
4247
Column("sip_method", "TEXT"),
4348
Column("sip_dial_string", "TEXT"),
4449
Column("sip_dial_number", "TEXT"),

0 commit comments

Comments
 (0)