Docs Send sipnab's vCons to a vCon server
Send sipnab's vCons to a vCon server
Have sipnab write a vCon for every finished call and forward each one to vcon-server, on the same machine or another, kept apart from recorded calls.
On this page
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 OpenSIPS and changes nothing about how OpenSIPS handles calls. It writes each vCon to a directory. A small forwarder sends them from there to vcon-server and 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.
Keep recorded and observed vCons apart
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, which 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, and its
own chain, which tags each vCon vcon_role:observer and stores it in a separate
table, vcons_observed. The recorder’s vCons stay in vcons_recorded.
Tested on
Run on 2026-09-25 on the Debian 13 x86_64 machine from the vCon server guide,
with sipnab from its .deb.
1. Add an ingress list and a chain for sipnab
Add a key, a tag and a table for sipnab to /opt/vcon/config.yml, leaving 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 api
config.yml now has two keys under ingress_auth, two links, two entries under
storages and
two chains.
2. Install sipnab
# 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.deb
3. Have sipnab write a vCon for every finished call
sipnab’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, in the same language--filteruses (Filter language). This one writes every call that ended with aBYE;state == 'Failed'would write the failed ones instead.--export-vcon-dir /var/spool/sipnab-vconis where the files go.--retain-audiokeeps each call’s audio so that the vCon can carry it. Without it the vCon holds the signaling only.
For the audio to be there at all, sipnab has to capture it. By default it captures only the SIP ports, so give it a capture filter that also admits 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 --now sipnab
sipnab needs ReadWritePaths because the unit makes the rest of the file system
read-only to sipnab.
Following Let sipnab see and control TFPS as well? Both
guides replace sipnab’s ExecStart in a drop-in, and 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, and 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.
4. Install the forwarder
sipnab never sends a vCon anywhere itself. vcon_forward.py, from sipnab’s
repository, posts each file in the directory to vcon-server’s sipnab ingress
list and deletes it once the server has taken it. It needs only Python’s
standard library.
# 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-forward
The key lives in a file only root can read. systemd reads it before starting the forwarder, so it 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 |
5. Place a call and find its vCon
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"
The forwarder logs sent <file> for each vCon, the directory is empty, and
vcons_observed has the vCons tagged ["vcon_role:observer"].
The test call makes two vCons in vcons_observed, not one. sipnab sees the
call and also the SIPREC session OpenSIPS opens to the recorder, which 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
Run sipnab on a different machine
sipnab has to see the calls, so it runs on a machine the SIP and media traffic reaches, or on one 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 steps 2, 3 and 4 as they are, with 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_TOKEN in /opt/vcon/.env reads every vCon, so it never leaves
the vCon server.
When something does not work
- The directory stays empty after a call. Check that the call matches the
expression:
sipnab -N -Ion a capture of it, with--report, shows its state. On a stopped sipnab, see “When the file appears” above. - The vCon has no audio. Either
--retain-audiois missing, or 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. Its log says why:docker compose logs conserver.