Skip to content

Send vCons to a vCon Server

github-actions[bot] edited this page Oct 16, 2026 · 1 revision

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.

Keep recorded and observed vCons apart

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.

The recorder's vCons stay in vcons_recorded.

Tested on

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 an ingress list and a chain for sipnab

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 api

config.yml now has:

  • two keys under ingress_auth,
  • two links,
  • two entries under storages,
  • two chains.

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

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.
    • It uses the same language --filter uses (Filter language).
    • This one writes every call that ended with a BYE.
    • state == 'Failed' would write the failed ones instead.
  • --export-vcon-dir /var/spool/sipnab-vcon is where the files go.
  • --retain-audio keeps 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 sipnab

Notes 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 ExecStart in 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.

Install the forwarder

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 sipnab ingress list,
    • deletes the file once the server has taken it.
    • It needs only Python's standard library.
  • sipnab releases after 0.5.204 also include a forwarder, sipnab --vcon-forward.
  • 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-forward

The 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 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"

What you see:

  • The forwarder logs sent <file> for each vCon.
  • The directory is empty.
  • vcons_observed has 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

Or deliver them with sipnab's own forwarder

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 vcon feature,
    • the sipnab ingress list from the first step.

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/delivered

The auth file holds one line, x-conserver-api-token: <key>. The forwarder refuses it if other users can read it.

The forwarder:

  • logs one delivered line naming the file and the server's 204,
  • 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 sipnab list ["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, never stored.
  • 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.

The same delivery with the conserver kind

--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=sipnab to 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/delivered

With 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_AUTH

With 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 delivered line naming the file and the server's 204,
  • 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.

With Kamailio

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 sipnab

sipnab 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.

Run sipnab on a different machine

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 --url at the vCon server: --url http://192.0.2.10:8000.
  • Copy the sipnab key from the vCon server's config.yml into /etc/sipnab/vcon-forward.env by hand, as VCON_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.
    • To see its state, run sipnab -N -I on 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-audio is missing,
    • the capture filter does not admit the media ports.
  • The forwarder stops with answered 403. The key in /etc/sipnab/vcon-forward.env does not match the sipnab key in config.yml.

  • Files pile up in rejected/.

    • vcon-server refused them with a 4xx other than 401, 403 or 404.
    • 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.

Home

Getting started

Using the TUI

CLI & automation

Configuration

Integrations (API & MCP)

Your SIP server

Add to your voice stack

Development & internals

Clone this wiki locally