Docs Collect and forward HEP
Collect and forward HEP
Receive HEP from OpenSIPS or Kamailio, require a shared secret, carry it over TCP or TLS, forward what sipnab captures to Homer, and read what a collector outage cost.
On this page
- Before you start
- Receive HEP from a SIP server
- See who feeds the listener
- Require a shared secret
- Carry HEP over TCP or TLS
- Forward what sipnab captures to Homer
- When the collector goes away
- Read a HEP mirror off the wire instead
- Take signaling from HEP and media from the wire
- When something does not work
HEP (the Homer Encapsulation Protocol) carries a copy of each SIP message, with its addresses and the time the sender saw it, from a SIP server to a collector. sipnab works on both ends of it:
- As a collector (
--hep-listen), sipnab receives the copies your SIP servers send and analyzes the calls. It needs no packet capture and no privileges. - As a capture agent (
--hep-send), sipnab captures SIP off an interface and forwards it, with the RTCP of each call, to Homer or to another sipnab.
This page sets up both ends, adds the shared secret, TCP and TLS that a feed across a network needs, and shows how to check each step.
Before you start
-
A build with HEP. Every release package and binary has it. Check the
features:list:sipnab --versionsipnab 0.5.232 (0d8eea19) features: native,tui,audio,tls,hep,api,mcp,mcp-http,metrics,plugins,vcon,opus,archive,g729 libpcap version 1.10.4 (with TPACKET_V3); alternate capture backends named: nonehepmust be in it. TCP and TLS also needtls. -
No privileges for a collector. A listener on a port above 1023 runs as an ordinary user. Only
--hep-sendfrom a live interface needs capture rights (Install). -
A firewall rule on the collector for the port and transport you choose, open only to your SIP servers.
-
On the SIP server, the lines in Have your SIP server send HEP. They are the only change another system needs.
What ran: every command and output on this page ran on 2026-11-03 with sipnab
0.5.232 built from source, OpenSIPS 4.0.2, Kamailio 6.1.4 and heplify-server
1.60.10, in containers on one Linux host. The SIP server was at
198.51.100.10 and sipnab at 198.51.100.1. SIPp placed one call through the
SIP server for each test.
Receive HEP from a SIP server
Start the listener
sipnab -N -L 198.51.100.1:9061 --hep-allow 198.51.100.10 \
--duration 15 --report --hep-senders --no-cli-print
-L(--hep-listen) binds the UDP port. It decodes HEP on its own.--hep-parseis not needed, and changes nothing on a listener.--hep-allownames the senders it accepts: an address, or a range such as198.51.100.0/24. Repeat it for each.- sipnab refuses to bind a listener to a routable address with neither
--hep-allownor--hep-auth-file, because an open HEP port accepts forged calls from anyone who reaches it. A loopback bind needs neither. --durationstops after that long, then--reportand--hep-sendersprint what arrived. Leave--durationout for a collector that runs until you stop it, or run it as a service (Run sipnab as a service).
Have your SIP server send HEP
OpenSIPS 4.0. proto_hep defines the destination and tracer sends every
message of each call to it. These are the lines that ran. Give OpenSIPS a
hep_udp socket to send from, and call trace() for each new call:
socket=hep_udp:198.51.100.10:6061
loadmodule "dialog.so"
loadmodule "proto_hep.so"
modparam("proto_hep", "hep_id", "[sipnab] 198.51.100.1:9061; transport=udp; version=3")
modparam("proto_hep", "hep_capture_id", 101)
loadmodule "tracer.so"
modparam("tracer", "trace_id", "[tid]uri=hep:sipnab")
route {
# Your checks and record_route() come first.
create_dialog();
trace("tid", "d");
# Then t_relay(), as before.
}
- Without
transport=udp, OpenSIPS uses HEP over TCP, its default, and sends nothing unless it also has ahep_tcpsocket. "d"traces the whole dialog, both legs, in both directions."m"traces only the message the script runs on: a run with"m"received one INVITE per call and nothing after it.- Use OpenSIPS as your voice stack’s SIP server and Add Homer to your voice stack show the complete script.
Kamailio 6.1. siptrace sends a copy of every message to one destination:
loadmodule "siptrace.so"
modparam("siptrace", "duplicate_uri", "sip:198.51.100.1:9061")
modparam("siptrace", "hep_mode_on", 1)
modparam("siptrace", "hep_version", 3)
modparam("siptrace", "hep_capture_id", 102)
modparam("siptrace", "trace_to_database", 0)
modparam("siptrace", "trace_on", 1)
modparam("siptrace", "trace_mode", 1)
A second duplicate_uri line replaces the first rather than adding a
destination. Connect sipnab to Homer
gives the two ways to run sipnab beside Homer with Kamailio.
Asterisk, FreeSWITCH and rtpengine have their own sections: Run sipnab beside Asterisk, Run sipnab beside FreeSWITCH and Let sipnab name rtpengine’s media.
Check that it works
The OpenSIPS run above ended with the call and the one sender:
Call-ID From To State Code Duration Msgs PDD Tags
-------------------------------------------------------------------------------------------------------------------------
1-hep@example.com sipp echo Completed 200 0s 13 0.0s -
HEP senders (trust unauthenticated, silent after 30s)
received 13 admitted 13 refused 0 from untracked senders 0
SOURCE PACKETS LAST SEEN IDLE STATE
hep:101@198.51.100.10 13 2026-11-03T14:59:20.783Z 5s sending
1 of 1 tracked sender(s) shown, 0 silent, limit 4096. Capture ids are claimed by the sender, not proven.
Refused sources (0 of 0 tracked shown, limit 256, 0 evicted)
hep:101is thehep_capture_idOpenSIPS stamps on each copy. Kamailio’s run showedhep:102. Give each sender its own id, because the roster keys on it.- The start of the run warns that the listener is
non-loopback and unauthenticated. Require a shared secret is the fix. - HEP carries no RTP, so
--reportshows no streams. Take signaling from HEP and media from the wire adds them.
See who feeds the listener
With twenty SIP servers sending, the listener answers three questions: which are sending, which stopped, and which send while the listener turns them away.
- At the end of a bounded run:
--hep-senders, as above. With--json, the same roster is one JSON object on the last line of stdout. - While sipnab runs as a server:
GET /v1/hep/senders, or the MCPhep_senderstool. - In the TUI:
hfor capture health, thens.
Find who feeds the collector and who it refuses
reads each column. A sender that stops while others keep sending gets a log
line of its own, and another when it comes back. --hep-silence-warn sets how
long it may stay quiet first (30 seconds).
Require a shared secret
--hep-allow trusts an address. A shared secret also proves the sender holds
the key. sipnab checks it in one of two modes:
| Mode | What travels | Who can send it |
|---|---|---|
plain (default) | The secret itself, in the HEP auth-key chunk (0x000e) | Kamailio siptrace, another sipnab. A sniffer on the path can read and replay it |
hmac | A per-packet HMAC-SHA256 token over the whole packet, with a timestamp and a nonce | Another sipnab only |
Create the key
# Run all of these, in order.
printf '%s\n' 'replace-with-a-long-random-string' | sudo tee /etc/sipnab/hep.key >/dev/null
sudo chmod 600 /etc/sipnab/hep.key
sipnab reads the file and trims the line ending. Give the same string to the sender.
From Kamailio (plain)
Add the key to the siptrace lines above:
modparam("siptrace", "auth_key", "replace-with-a-long-random-string")
Start the listener with the file. A listener with a key needs no
--hep-allow to bind a routable address, and can still have one:
sipnab -N -L 198.51.100.1:9061 --hep-auth-file /etc/sipnab/hep.key \
--duration 15 --report --hep-senders --no-cli-print
The Kamailio run admitted every packet, and the roster names the mode:
HEP senders (trust shared_secret_plain, silent after 30s)
received 13 admitted 13 refused 0 from untracked senders 0
SOURCE PACKETS LAST SEEN IDLE STATE
hep:102@198.51.100.10 13 2026-11-03T15:00:41.979Z 5s sending
The same run with a different key in the file refused all 13, and 30 seconds in the listener said why:
WARN sipnab::capture::hep: HEP listener on 198.51.100.1:9061: no packets admitted for 30s — 13 arrived and every one was refused, mostly auth_mismatch (the auth key does not match this listener's secret), the last of those from 198.51.100.10; the sender is reaching this port and being turned away; capture is still listening
OpenSIPS sends no key
proto_hep has no parameter for the auth-key chunk. Its documentation lists
hep_id keys uri, transport and version only
(proto_hep 4.0,
read 2026-11-03). A listener with a key therefore refuses every OpenSIPS copy:
Refused sources (1 of 1 tracked shown, limit 256, 0 evicted)
PEER PACKETS REASONS
198.51.100.10 13 auth_missing=13
Guard an OpenSIPS feed with --hep-allow and a firewall instead, and carry it
over TLS when it crosses a network you do not control.
Between two sipnabs (hmac)
Both ends take the same file and --hep-auth-mode hmac:
sipnab -N -L 127.0.0.1:9071 --hep-auth-file /etc/sipnab/hep.key --hep-auth-mode hmac \
--duration 4 --hep-senders --no-cli-print
sipnab -N -I sample-call.pcap --hep-send 127.0.0.1:9071 \
--hep-auth-file /etc/sipnab/hep.key --hep-auth-mode hmac --hep-id 2002 --no-cli-print
The listener admitted all 33 packets under trust shared_secret_hmac. The same
sender in plain mode met a refusal on every packet, as hmac_bad_format.
- The token’s timestamp must fall within
--hep-hmac-windowseconds of the listener’s clock (30 by default, at most 300). A sender with a wrong clock is refused ashmac_timestamp_out_of_window. Fix its NTP rather than widen the window: the window is how long a captured packet stays replayable. heplify-serverhas no option that checks a key, so Homer accepts a feed with or without one.
Carry HEP over TCP or TLS
UDP gives a sender no sign that the collector is down, and leaves the copies readable on the path. Each end of sipnab names its own transport, so a relay can take one in and send another out.
A TCP or TLS listener
sipnab -N -L 198.51.100.1:9062 --hep-listen-transport tcp --hep-allow 198.51.100.10 \
--duration 15 --report --hep-senders --no-cli-print
For TLS, give the listener a certificate and its key. sipnab refuses a key other users can read:
chmod 600 /etc/sipnab/collector.key
sipnab -N -L 198.51.100.1:9063 --hep-listen-transport tls \
--hep-tls-cert /etc/sipnab/collector.pem --hep-tls-key /etc/sipnab/collector.key \
--hep-allow 198.51.100.10 --duration 15 --report --hep-senders --no-cli-print
The start of the run names the transport, as in HEP listener started on 198.51.100.1:9063 (tls). A key with mode 0644 stops it instead:
the HEP TLS private key is world-readable (mode 0644); chmod 600 it.
- A TCP or TLS listener reads HEP v3 only. HEP v1 and v2 carry no length, so they work over UDP only.
- TLS proves the path is private, not who the sender is: sipnab asks senders
for no certificate. Keep
--hep-allow, a key, or both.
OpenSIPS over TCP and TLS
Each transport needs its own socket and transport= key. For TCP:
socket=hep_tcp:198.51.100.10:6062
modparam("proto_hep", "hep_id", "[sipnab] 198.51.100.1:9062; transport=tcp; version=3")
For TLS, add tls_mgm with a client domain that checks the collector’s
certificate against your CA:
socket=hep_tls:198.51.100.10:6063
loadmodule "tls_openssl.so"
loadmodule "tls_mgm.so"
modparam("tls_mgm", "client_domain", "hep")
modparam("tls_mgm", "match_ip_address", "[hep]198.51.100.1:9063")
modparam("tls_mgm", "ca_list", "[hep]/etc/opensips/hep-ca.pem")
modparam("tls_mgm", "verify_cert", "[hep]1")
modparam("tls_mgm", "require_cert", "[hep]1")
modparam("proto_hep", "hep_id", "[sipnab] 198.51.100.1:9063; transport=tls; version=3")
Both runs delivered all 13 messages of the call, as hep:101@198.51.100.10.
Kamailio over TCP and TLS
Put the transport on duplicate_uri, and give Kamailio a socket of that
transport:
listen=tcp:198.51.100.10:5060
modparam("siptrace", "duplicate_uri", "sip:198.51.100.1:9062;transport=tcp")
For TLS, load tls.so. Kamailio’s TLS module refuses to start without a server
certificate, even when it only connects out, so its configuration file needs
both halves:
listen=tls:198.51.100.10:5061
enable_tls=yes
loadmodule "tls.so"
modparam("tls", "config", "/etc/kamailio/tls.cfg")
modparam("siptrace", "duplicate_uri", "sip:198.51.100.1:9063;transport=tls")
[server:default]
method = TLSv1.2+
certificate = /etc/kamailio/kamailio.pem
private_key = /etc/kamailio/kamailio.key
[client:default]
method = TLSv1.2+
verify_certificate = yes
require_certificate = yes
ca_list = /etc/kamailio/hep-ca.pem
Both runs delivered all 13 messages, as hep:102@198.51.100.10. Without the
[server:default] certificate, Kamailio exited at startup with
init_mod_child(): error while initializing module tls.
Forward what sipnab captures to Homer
On a machine whose SIP server sends no HEP, sipnab captures the SIP and forwards it. Name the interface the SIP crosses:
sudo sipnab -N -d eth0 --hep-send 198.51.100.30:9060 --hep-id 2002
- SIP goes as HEP type 1. RTCP goes as type 5, as the JSON heplify-server stores, with the call’s Call-ID, and only for calls whose SDP sipnab saw. RTP is never forwarded.
--hep-idis the capture id Homer shows. Give each agent its own.--hep-send-transport tcportlspicks the transport. For TLS, the collector’s certificate must name the address or host name you send to.--hep-tls-canames the CA that signs it and replaces the host’s CA bundle.--hep-tls-extra-caadds a CA to the bundle instead.[hep]insipnab.tomltakes the same settings.
heplify-server 1.60.10 accepted all three transports, started with
-hepaddr 0.0.0.0:9060 -heptcpaddr 0.0.0.0:9061 -heptlsaddr 0.0.0.0:9062,
-tlscertfile and -tlskeyfile. The test read the sample capture rather than
an interface, one run per transport:
sipnab -N -I sample-call.pcap --hep-send 198.51.100.30:9062 --hep-send-transport tls \
--hep-tls-ca /etc/sipnab/collector-ca.pem --hep-id 2002 --no-cli-print
Each run ended with a line such as:
INFO sipnab::app::batch: HEP export to 198.51.100.30:9062 over tls: 33 packet(s) sent, 33 queued, 0 replayed after an outage, none dropped, 0 still queued, none failed, 0 reconnect(s); sent means written to the connection: a write that fails is counted, the packet stays queued, and the sender dials again with backoff
heplify-server’s own counter then read heplify_packets_total{node_id="2002",type="sip"} 99:
33 packets from each of the three runs. A run reading a capture file also warns
that the file’s signaling leaves the machine. Connect sipnab to
Homer finds the call in Homer
afterwards.
When the collector goes away
Over TCP or TLS, sending runs on its own thread behind a queue, so a collector that stops never slows the capture:
-
At startup, a collector that refuses the first connection stops the run with exit
2:Failed to create HEP sender: Failed to connect HEP sender to '127.0.0.1:9079' over TCP: Connection refused (os error 111) -
After that, packets queue, up to
--hep-send-queueMiB (64 by default), the oldest dropped first at the bound. The sender redials with backoff, each attempt bounded by--hep-send-timeoutseconds (3 by default), and replays the queue in capture order when the collector answers. -
Over UDP, sipnab cannot see an outage, so it holds nothing and replays nothing.
A live run that captured 25 calls showed it. sipnab sent over TCP to a second
sipnab on 127.0.0.1:9070. SIGTERM stopped the collector 12 seconds in, and
a new one started 20 seconds later:
sudo sipnab -N -d hepx --hep-send 127.0.0.1:9070 --hep-send-transport tcp \
--hep-id 2002 --duration 60 --no-cli-print 'udp port 5060'
WARN sipnab::capture::hep_delivery: HEP collector 127.0.0.1:9070 unreachable (write: the collector closed the connection); queueing up to 65536 KiB and retrying
INFO sipnab::capture::hep_delivery: HEP collector 127.0.0.1:9070 is reachable again; replaying 143 queued packet(s)
WARN sipnab::app::batch: HEP export to 127.0.0.1:9070 over tcp: 325 packet(s) sent, 325 queued, 143 replayed after an outage, none dropped, 0 still queued, 9 failed attempt(s) (connect 8, write 1), 1 reconnect(s); sent means written to the connection: a write that fails is counted, the packet stays queued, and the sender dials again with backoff
The first collector admitted 65 packets and the second 260: all 325. A failed
attempt keeps its packet queued, so 9 failed attempt(s) lost nothing. What
a run loses is the dropped count, by reason: queue_full, stopped (sipnab
itself stopped with packets still queued) and unsent_at_exit. The same
counts are in runtime_stats, GET /v1/runtime and the Prometheus series.
When the --hep-send collector goes away
gives every rule.
Read a HEP mirror off the wire instead
A SIP server may already send HEP to an address where nothing listens for it,
or to Homer on the same machine. sipnab can read those packets off the
interface rather than take the port, so Homer keeps its feed. -E
(--hep-parse) unwraps HEP inside ordinary captured UDP.
Put it in a configuration file, so that a plain sipnab does it. In this run
OpenSIPS sent to 198.51.100.1:9061, where nothing listened, and the file
named the interface the copies crossed and their port:
[capture]
device = "hepx"
hep_parse = true
bpf_filter = "udp port 9061"
For a SIP server that sends to Homer on the same machine, the interface is
lo and the port Homer’s.
sudo sipnab -N --config mirror.toml --duration 15 --report --no-cli-print
1-hep@example.com sipp echo Completed 200 0s 13 0.0s -
INFO sipnab::capture::live: Live capture on 'hepx' finished: 13 packets, no drops
INFO sipnab::app::batch: sipnab: 13 packets captured, 13 SIP messages, 0 RTP packets across 0 streams
- A sniffed copy reaches no socket, so
--hep-allowand a key do not apply to it. --no-hep-parseturns it off for one run when the file turns it on.- Use
--hep-parseon a relay capture covers rtpengine’s mirrored control plane.
Take signaling from HEP and media from the wire
HEP carries no RTP. One sipnab can take the SIP from the listener and the media from an interface, and bind each stream to its call by the SDP:
sudo sipnab -N -d hepx --hep-listen 198.51.100.1:9061 --hep-allow 198.51.100.10 \
--duration 15 --json-dialogs --no-cli-print 'udp port 5060'
That run’s filter also took the SIP off the bridge, to compare the two. To take
only media from the interface, give it your media ports instead, such as
"udp portrange 10000-20000". The run states both sources at startup:
Capturing from two sources in one process: device 'hepx' + HEP listener 198.51.100.1:9061. It also warns that two clocks stamp the two sources.
When the interface also sees the SIP, sipnab compares the two accounts of each
call. Agreement prints nothing. A disagreement is a finding in the call’s
diagnosis, under source_disagreement in --json-dialogs and in
--call-report. With OpenSIPS tracing "m" instead of "d", the mirror
reported only the INVITE, and the call said:
Capture sources disagree about this call: the wire carried 12 messages the mirror never reported (100 Giving it a try #2, INVITE (SDP) #3, 180 Ringing #4, and 9 more). 1 message matched on both.
Take signaling from HEP and media off the wire, in one process
explains each kind of finding, and the combinations sipnab refuses (-I,
--multi-device or -O with the pair).
When something does not work
| Symptom | Cause | Fix |
|---|---|---|
received 0 and no packets for 30s | Nothing reaches the port | The SIP server’s address and port, its transport, the firewall |
Refused, auth_missing | The listener has a key and the sender sends none (OpenSIPS never does) | Drop the key and use --hep-allow, or send from Kamailio with auth_key |
Refused, auth_mismatch | Different keys on the two ends | Compare the strings. sipnab trims the file’s line ending |
Refused, hmac_bad_format | The listener is in hmac mode and the sender in plain | Set --hep-auth-mode hmac on both |
Refused, allowlist | The sender’s address is outside --hep-allow | Add it, as an address or a range |
Failed to create HEP sender and exit 2 | The TCP or TLS collector refused the first connection | Start the collector first |
The TLS listener does not start: world-readable | The key file’s mode | chmod 600 the key |
| OpenSIPS sends nothing | hep_id without transport=udp and no hep_tcp socket | Add transport=udp, or the socket |
| Kamailio exits at startup with TLS | No [server:default] certificate | Add one, as above |
A HEP collector that receives nothing lists every refusal reason, and Error messages every startup refusal.