Repository navigation
Send vCons to a vCon Server
sipnab can write a vCon for every call it sees.
- Unlike the recording in
Add a vCon server to an OpenSIPS voice stack, sipnab is
not in the call path.
- It reads a copy of the traffic.
- So it needs nothing from your SIP server.
- It changes nothing about how your SIP server handles calls.
- It writes each vCon to a directory.
- A small forwarder sends them from there to vcon-server.
- It deletes each one once the server has taken it.
This guide starts where the vCon server guide ends, with vcon-server running.
- The first part puts sipnab on the same machine.
- The last section says what changes when sipnab runs on another machine.
With Kamailio, this is how a vCon server gets vCons at all.
- Kamailio has no SIPREC module to record with, as the vCon server guide explains.
- With Kamailio says what changes.
The two kinds of vCon hold different things:
- A vCon from the recorder holds the audio a party to the call handed over.
- A vCon from sipnab holds what a passive observer saw on the wire.
- That can be less: a packet the capture missed is not in it.
Consumers should be able to tell the two apart. So this guide gives sipnab:
- its own ingress list,
sipnab, - its own chain. The chain:
- tags each vCon
vcon_role:observer, - stores it in a separate table,
vcons_observed.
- tags each vCon
The recorder's vCons stay in vcons_recorded.
Every block on this page ran as written, in order, on 2026-09-28. The exception is the blocks under Or deliver them with sipnab's own forwarder.
- sipnab: 0.5.194 from its release package.
- Machines: clean x86_64 virtual machines with 2 cores and 3 GB of memory.
- Systems: Debian 13 (kernel 6.12.63) and Ubuntu 24.04.5 (kernel 6.8.0).
- Stacks:
- OpenSIPS, from source and from the packages,
- Kamailio,
- both on one machine.
On Debian 13, causing each fault under When something does not work produced what it describes.
Add a key, a tag and a table for sipnab to /opt/vcon/config.yml. Leave the
recorder's settings as they are:
# Run all of these, in order.
cd /opt/vcon
. ./.env
python3 - "$(openssl rand -hex 32)" "$POSTGRES_PASSWORD" <<'EOF'
import sys
key, password = sys.argv[1], sys.argv[2]
path = "config.yml"
text = open(path).read()
text = text.replace("ingress_auth:\n", f'ingress_auth:\n sipnab: "{key}"\n', 1)
text = text.replace("links:\n", "links:\n mark_observer:\n module: links.tag\n"
" options:\n tags: [vcon_role:observer]\n", 1)
text = text.replace("storages:\n", "storages:\n postgres_observed:\n"
" module: storage.postgres\n options:\n host: postgres\n"
" port: 5432\n database: vcon\n user: vcon\n"
f' password: "{password}"\n table_name: vcons_observed\n', 1)
text += (" sipnab_observer:\n links: [mark_observer]\n ingress_lists: [sipnab]\n"
" storages: [postgres_observed]\n enabled: 1\n")
open(path, "w").write(text)
EOF
docker compose restart conserver apiconfig.yml now has:
- two keys under
ingress_auth, - two links,
- two entries under
storages, - two chains.
# Run all of these, in order.
V=$(curl -fsSL https://api.github.com/repos/NormB/sipnab/releases/latest \
| python3 -c 'import json,sys; print(json.load(sys.stdin)["tag_name"].lstrip("v"))')
curl -fsSLO https://github.com/NormB/sipnab/releases/download/v$V/sipnab_${V}_amd64.deb
curl -fsSL https://github.com/NormB/sipnab/releases/download/v$V/SHA256SUMS.txt \
| grep " sipnab_${V}_amd64.deb$" | sha256sum -c -
sudo apt-get install -y ./sipnab_${V}_amd64.debsipnab's package runs it as the sipnab user under systemd. A drop-in adds
three things to what it does:
-
--export-vcon-when "state == 'Completed'"picks the calls to write.- It uses the same language
--filteruses (Filter language). - This one writes every call that ended with a
BYE. -
state == 'Failed'would write the failed ones instead.
- It uses the same language
-
--export-vcon-dir /var/spool/sipnab-vconis where the files go. -
--retain-audiokeeps each call's audio so that the vCon can carry it.- sipnab keeps the whole call.
- It stays inside a budget of 1 GiB across all calls held at once (Audio retention budget).
- It releases a call's audio once it writes the call's vCon.
- Without it the vCon holds the signaling only.
For the audio to be there at all, sipnab has to capture it.
- Up to 0.5.192 it captured only the SIP ports by default.
- Later versions capture the media too.
- Either way, give it a capture filter that names the media ports.
- rtpengine relays media on 30000-39999 in the vCon server guide:
# Run all of these, in order.
sudo install -d -o sipnab -g sipnab -m 0750 /var/spool/sipnab-vcon
sudo mkdir -p /etc/systemd/system/sipnab.service.d
sudo tee /etc/systemd/system/sipnab.service.d/vcon.conf >/dev/null <<'EOF'
[Service]
ExecStart=
ExecStart=/usr/bin/sipnab -N -d any --no-cli-print --syslog --metrics 127.0.0.1:9090 \
--retain-audio \
--export-vcon-when "state == 'Completed'" \
--export-vcon-dir /var/spool/sipnab-vcon \
"port 5060 or port 5090 or udp portrange 30000-39999"
ReadWritePaths=/var/spool/sipnab-vcon
EOF
sudo systemctl daemon-reload
sudo systemctl enable sipnab
sudo systemctl restart sipnabNotes on this block:
- sipnab needs
ReadWritePaths. The unit makes the rest of the file system read-only to sipnab. - The last line restarts sipnab rather than using
enable --now.- A sipnab that another guide already started would keep running with its old flags.
Following Let sipnab see and control TFPS as well?
- Both guides replace sipnab's
ExecStartin a drop-in. - systemd uses the last
ExecStart=it reads, taking drop-ins in file-name order. - So one silently replaces the other.
- Put the flags from both into one drop-in instead.
When the file appears. On a live capture, sipnab writes a call's vCon within about ten seconds of the call ending.
- It checks every five seconds.
- It waits until the call has been quiet for five.
- If you stop sipnab, it writes nothing more.
- Calls that ended in the last few seconds before the stop are not written.
- Stopping means stopping, and sipnab keeps no call data behind.
The capture process never sends a vCon anywhere itself.
-
vcon_forward.py, from sipnab's repository, does that. It:- posts each file in the directory to vcon-server's
sipnabingress list, - deletes the file once the server has taken it.
- It needs only Python's standard library.
- posts each file in the directory to vcon-server's
- sipnab releases after 0.5.204 also include a forwarder,
sipnab --vcon-forward.- Or deliver them with sipnab's own forwarder runs it against the same ingress list.
- This guide's tested steps use the Python forwarder.
# Run all of these, in order.
V=$(curl -fsSL https://api.github.com/repos/NormB/sipnab/releases/latest \
| python3 -c 'import json,sys; print(json.load(sys.stdin)["tag_name"].lstrip("v"))')
sudo install -d /usr/local/lib/sipnab
sudo curl -fsSL -o /usr/local/lib/sipnab/vcon_forward.py \
https://raw.githubusercontent.com/NormB/sipnab/v$V/clients/python/vcon_forward.py
KEY=$(sed -n 's/^ sipnab: "\(.*\)"/\1/p' /opt/vcon/config.yml)
sudo sh -c "umask 077; printf 'VCON_INGRESS_TOKEN=%s\n' '$KEY' > /etc/sipnab/vcon-forward.env"
sudo tee /etc/systemd/system/sipnab-vcon-forward.service >/dev/null <<'EOF'
[Unit]
Description=Forward sipnab vCons to the vCon server
After=network-online.target sipnab.service
Wants=network-online.target
[Service]
User=sipnab
Group=sipnab
EnvironmentFile=/etc/sipnab/vcon-forward.env
ExecStart=/usr/bin/python3 /usr/local/lib/sipnab/vcon_forward.py \
--spool /var/spool/sipnab-vcon --url http://127.0.0.1:8000 --ingress-list sipnab
Restart=on-failure
RestartSec=10
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/spool/sipnab-vcon
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now sipnab-vcon-forwardThe key lives in a file only root can read. systemd reads it before starting the forwarder. So the key never appears on a command line.
What the forwarder does with each file:
| The server answers | The forwarder |
|---|---|
2xx |
deletes the file, unless sipnab replaced it during the send, in which case it sends the new one on the next pass |
401, 403 or 404
|
stops: the key or the list name is wrong, and retrying would not help |
another 4xx
|
moves the file to rejected/ in the directory, for a person to read |
5xx, or no answer |
keeps the file and tries again five seconds later |
Place the test call from the vCon server guide. After about ten seconds:
# Run all of these, in order.
sudo journalctl -u sipnab-vcon-forward -n 5
sudo ls /var/spool/sipnab-vcon
cd /opt/vcon
docker compose exec -T postgres psql -U vcon -d vcon -c \
"select id, vcon_json->'attachments'->-1->>'body' as tags,
jsonb_array_length(vcon_json->'dialog') as dialogs
from vcons_observed order by created_at desc limit 5"What you see:
- The forwarder logs
sent <file>for each vCon. - The directory is empty.
-
vcons_observedhas the vCons tagged["vcon_role:observer"].
The test call makes two vCons in vcons_observed, not one.
- sipnab sees the call.
- It also sees the SIPREC session OpenSIPS opens to the recorder. That session is a SIP call of its own.
- The SIPREC session's first message goes to the recorder's port, 5090.
- So this expression keeps the call and leaves the recording session out:
state == 'Completed' and dst.port != 5090
sipnab --vcon-forward does the forwarder's job from the step
"Install the forwarder" without Python.
- It is a second sipnab process. It:
- posts each file in the directory to vcon-server,
- moves the file out of the directory once the server answers.
- It captures nothing.
- It needs:
- a sipnab built with the
vconfeature, - the
sipnabingress list from the first step.
- a sipnab built with the
This section ran on 2026-10-07, not on the machines in Tested on. It ran:
- against a vcon-server already running on an aarch64 Linux host,
- with sipnab built from source,
- with that server's configuration file in place of
/opt/vcon/config.yml.
This walk-through uses a directory you own and a capture file. So it touches nothing else on the machine. The capture is sipnab's synthetic test call: example addresses, no real subscriber.
# Run all of these, in order.
mkdir -p ~/vcon-forward-demo && cd ~/vcon-forward-demo
curl -fsSLO https://raw.githubusercontent.com/NormB/sipnab/main/tests/fixtures/sip_call.pcap
KEY=$(sed -n 's/^ sipnab: "\(.*\)"/\1/p' /opt/vcon/config.yml)
(umask 077; printf 'x-conserver-api-token: %s\n' "$KEY" > vcon-forward.auth)
sipnab -N --no-cli-print -I sip_call.pcap --export-vcon-when "state == 'Completed'" --export-vcon-dir spool
sipnab --vcon-forward spool \
--vcon-forward-url 'http://127.0.0.1:8000/vcon/external-ingress?ingress_list=sipnab' \
--vcon-forward-auth-file vcon-forward.auth --vcon-forward-once
echo "exit $?"
ls spool/deliveredThe auth file holds one line, x-conserver-api-token: <key>. The forwarder
refuses it if other users can read it.
The forwarder:
- logs one
deliveredline naming the file and the server's204, - exits
0.
The file is now in spool/delivered. Look for it in PostgreSQL by the uuid
inside it:
# Run all of these, in order.
UUID=$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["uuid"])' ~/vcon-forward-demo/spool/delivered/*.vcon.json)
cd /opt/vcon
sleep 3
docker compose exec -T postgres psql -U vcon -d vcon -c \
"select id, vcon_json->'attachments'->-1->>'body' as tags from vcons_observed where id='$UUID'"The query returns one row.
- The chain from the first step tags it
["vcon_role:observer"]. - The server this section ran against tags its
sipnablist["vcon_source:sipnab", "vcon_role:observer"], and returned that.
As in
Prove it stores what it accepts,
a 204 means queued.
- The forwarder reports
delivered, neverstored. - This query is how you know.
To run it all the time, leave out --vcon-forward-once. The forwarder then:
- passes over the directory every five seconds until you stop it,
- holds a container it cannot deliver because the server is down. That container waits and goes again.
Deliver the spool to a store lists everything it does with each answer.
Remove the demo directory with rm -r ~/vcon-forward-demo when you finish.
The delivered copy is call data.
--vcon-forward-kind conserver supplies the two values this server needs. So
you give less. The kind:
- adds the ingest path
/vcon/external-ingress?ingress_list=sipnabto a base URL with no path, - sends a bare key as
x-conserver-api-token: <key>.
Run these in the demo directory, before you remove it.
These three runs ran on 2026-10-07:
- against the same vCon server as the section above,
- with sipnab 0.5.206 built from source,
- with that server's configuration file in place of
/opt/vcon/config.yml.
With the bare key in a file:
# Run all of these, in order.
cd ~/vcon-forward-demo
KEY=$(sed -n 's/^ sipnab: "\(.*\)"/\1/p' /opt/vcon/config.yml)
(umask 077; printf '%s\n' "$KEY" > conserver.key)
sipnab -N --no-cli-print -I sip_call.pcap --export-vcon-when "state == 'Completed'" --export-vcon-dir spool
sipnab --vcon-forward spool --vcon-forward-kind conserver \
--vcon-forward-url http://127.0.0.1:8000 \
--vcon-forward-auth-file conserver.key --vcon-forward-once
echo "exit $?"
ls spool/deliveredWith the bare key in the SIPNAB_VCON_FORWARD_AUTH environment variable
instead of a file. sipnab refuses the variable beside an auth file. So the last
line removes it from the shell:
# Run all of these, in order.
cd ~/vcon-forward-demo
export SIPNAB_VCON_FORWARD_AUTH="$(sed -n 's/^ sipnab: "\(.*\)"/\1/p' /opt/vcon/config.yml)"
sipnab -N --no-cli-print -I sip_call.pcap --export-vcon-when "state == 'Completed'" --export-vcon-dir spool
sipnab --vcon-forward spool --vcon-forward-kind conserver --vcon-forward-url http://127.0.0.1:8000 --vcon-forward-once
echo "exit $?"
unset SIPNAB_VCON_FORWARD_AUTHWith the settings in a config file.
- The
[vcon_forward]section holds every setting but the spool and--vcon-forward-once. - This one names the key file from the first block:
[vcon_forward]
kind = "conserver"
url = "http://127.0.0.1:8000"
auth_file = "/home/you/vcon-forward-demo/conserver.key"# Run all of these, in order.
cd ~/vcon-forward-demo
printf '[vcon_forward]\nkind = "conserver"\nurl = "http://127.0.0.1:8000"\nauth_file = "%s/vcon-forward-demo/conserver.key"\n' "$HOME" > forward.toml
sipnab -N --no-cli-print -I sip_call.pcap --export-vcon-when "state == 'Completed'" --export-vcon-dir spool
sipnab --vcon-forward spool --config forward.toml --vcon-forward-once
echo "exit $?"Each run:
- logs one
deliveredline naming the file and the server's204, - exits
0.
The conserver kind sends the container unchanged, as the first command in
this section does. Every forwarder setting and its key:
[vcon_forward] in the configuration reference.
Set up the vCon server with the vCon server guide's first two steps:
Then follow this guide's first four steps here as they are, from "Add an ingress list and a chain for sipnab" to "Install the forwarder". sipnab's capture filter from the step "Have sipnab write a vCon for every finished call" already admits Kamailio's port, 5060.
For a vCon with audio in it, the call's media has to pass where sipnab captures.
- Anchor it on rtpengine, as With Kamailio in the rtpengine guide sets it up.
- Place that guide's test call, as its section Place a test call shows.
- Then run the commands of this guide's step "Place a call and find its vCon".
The test call makes one vCon, not two. With no recorder, there is no SIPREC session for sipnab to see.
With OpenSIPS and Kamailio on one machine, Kamailio is on 5062, as OpenSIPS and Kamailio on one machine sets it up. sipnab then needs Kamailio's port twice:
- in the capture filter, so that the packets reach it,
- in
--portrange, so that it reads them as SIP.
By default it reads SIP on 5060-5061 only. With the filter alone it writes no vCon for Kamailio's calls. Add both to the drop-in from the step "Have sipnab write a vCon for every finished call":
# Run all of these, in order.
F=/etc/systemd/system/sipnab.service.d/vcon.conf
sudo sed -i 's|^ --retain-audio \\$| --retain-audio --portrange 5060-5062 \\|; s|"port 5060 or port 5090 or|"port 5060 or port 5062 or port 5090 or|' "$F"
grep -nE 'portrange|port 5062' "$F"
sudo systemctl daemon-reload
sudo systemctl restart sipnabsipnab then writes a vCon for:
- the calls through each proxy,
- OpenSIPS's SIPREC sessions to the recorder.
A call through Kamailio makes one vCon. A call through OpenSIPS makes two.
sipnab has to see the calls. So it runs on one of these:
- a machine the SIP and media traffic reaches,
- a machine that receives a copy of it (a mirror port, or a HEP feed).
The forwarder runs beside sipnab, because it reads sipnab's directory.
On that machine, do these steps as they are:
- "Install sipnab",
- "Have sipnab write a vCon for every finished call",
- "Install the forwarder".
Make two changes:
- In the forwarder's unit, point
--urlat the vCon server:--url http://192.0.2.10:8000. - Copy the
sipnabkey from the vCon server'sconfig.ymlinto/etc/sipnab/vcon-forward.envby hand, asVCON_INGRESS_TOKEN=<key>.
On the vCon server, port 8000 is now reachable from the network.
- Allow it only from the machines that post vCons.
- The key is for posting to one list.
- The
CONSERVER_API_TOKENin/opt/vcon/.envreads every vCon.- So it never leaves the vCon server.
-
The directory stays empty after a call.
- Check that the call matches the expression.
- To see its state, run
sipnab -N -Ion a capture of it, with--report. - On a stopped sipnab, see "When the file appears" above.
-
The vCon has no audio. One of these is the cause:
-
--retain-audiois missing, - the capture filter does not admit the media ports.
-
-
The forwarder stops with
answered 403. The key in/etc/sipnab/vcon-forward.envdoes not match thesipnabkey inconfig.yml. -
Files pile up in
rejected/.- vcon-server refused them with a
4xxother than401,403or404. - The forwarder's journal names each file it moved there.
- vcon-server's API logs only the status, such as
422. - To read the server's reason, post one of them by hand:
# Run all of these, in order. F=$(sudo ls /var/spool/sipnab-vcon/rejected | head -1) KEY=$(sudo sed -n 's/^VCON_INGRESS_TOKEN=//p' /etc/sipnab/vcon-forward.env) sudo cat "/var/spool/sipnab-vcon/rejected/$F" | curl -s -w '\n' -X POST \ "localhost:8000/vcon/external-ingress?ingress_list=sipnab" \ -H "x-conserver-api-token: $KEY" -H 'Content-Type: application/json' --data-binary @-
The answer names the field and what was wrong with it.
- vcon-server refused them with a
Website · Repository · Issues · Generated from docs/ — edit there, not here.
Getting started
- Installation
- First CLI Triage
- Glossary
- Cookbook
- Runnable Examples
- Real World Captures
- Troubleshooting
- Tuning Capture
- Encapsulations
- Rtpengine Relays
- Backers
Using the TUI
CLI & automation
- CLI Reference
- Error Messages
- Filter DSL
- Output Formats
- SIP Response Codes
- SIP Methods
- SIP Header Fields
- SIP Parameters
- SIP Lint Rules
- MOS and Codecs
Configuration
Integrations (API & MCP)
- REST API
- Prometheus Metrics
- Authentication
- MCP
- MCP Deployment
- MCP Across an Estate
- MCP Tools
- MCP Tools Core
- MCP Tools Signaling
- MCP Tools Captures
- MCP Tools Security
- MCP Tools Media
- MCP Tools Relay
- MCP Tools TFPS
- MCP Tools Server
- MCP Tools vCon
- MCP Tools TLS
- MCP Protocol
- Uprobe Walkthrough
- Capture SIP over TLS
- WASM Plugins
- vCon Export
- vCon Capture Stack
- G729 Decoding
Your SIP server
- Use OpenSIPS as the SIP Server
- Run sipnab beside OpenSIPS
- Use Kamailio as the SIP Server
- Run sipnab beside Kamailio
- Use Asterisk as the PBX
- Run sipnab beside Asterisk
- Use FreeSWITCH in the Voice Stack
- Run sipnab beside FreeSWITCH
Add to your voice stack
- Add a vCon Server
- Send vCons to a vCon Server
- Send vCons to vcon store
- Add TFPS
- Control TFPS from sipnab
- Ban SIP Scanners with fail2ban
- Feed fail2ban from sipnab
- Add rtpengine
- Name rtpengine Media with sipnab
- Add rtpproxy
- Name rtpproxy Media with sipnab
- Add Homer
- Connect sipnab to Homer
- Add Prometheus and Grafana
- Add sipnab Metrics to Prometheus
Development & internals
- Internals Index
- Internals Subsystem Guide
- Internals Invariants
- Internals Testing
- Internals Walkthroughs
- Internals Build CI Release
- Internals Profiling
- Internals Domain Primer
- Library API
- Benchmarks
- Fault Model
- Threat Model
- Assurance Case
- Architecture
- Internals Threading
- Internals TUI Testing
- Internals Zero Copy Payloads
- Internals Uprobe Capture
- Internals Rtpengine Control Plane
- Internals Relay Control Decoding
- Internals vCon Exporter