Repository navigation
Add a vCon Server
A vCon is a JSON container that holds one conversation:
- who took part,
- the recording,
- anything a later tool adds, such as a transcript.
This guide stands up a vCon server beside an OpenSIPS proxy. OpenSIPS then records every call into it.
- This guide does not use sipnab.
- When you have this working, Send sipnab's vCons to a vCon server adds sipnab as a second source.
Kamailio cannot record calls this way. It has no SIPREC module. With Kamailio says what that means. It also says what a Kamailio stack can send the vCon server instead.
The parts, and what each one does:
| Part | Role |
|---|---|
| vcon-server | Receives vCons over HTTP, runs them through a processing chain, and stores them. Its worker process is the conserver. |
| Valkey | The conserver's working store: queues and cached vCons. Valkey is the open-source fork of Redis and speaks the same protocol. |
| PostgreSQL | Keeps the finished vCons. |
| OpenSIPS | Your SIP proxy. Its siprec module starts a recording for each call. |
| rtpengine | Relays each call's media and sends a copy of it to the recorder. |
| vcon-siprec-adapter | The recorder. It receives the copy over SIPREC, turns each finished call into a vCon, and posts it to vcon-server. |
SIPREC (RFC 7866) is the standard way for a SIP server to hand a copy of a call to a recorder.
- The proxy opens a second SIP session to the recorder.
- It sends the recorder both directions of the audio.
Everything below runs on one machine. That keeps the example short. The last section says what changes when the parts live on different machines.
Every block on this page ran as written, in order, on 2026-09-28.
- 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).
On each, it ran:
- With OpenSIPS built from source on a machine with nothing installed.
- With the OpenSIPS 4.0 packages.
- On different machines, with the vCon server on its own machine.
More:
- With Kamailio ran with the sipnab page.
- On Debian 13, causing each fault under When something does not work produced what it describes.
- The commands pin the components to the versions below.
- Newer commits may behave differently.
- Pinning them keeps the guide describing what you get.
| Software | Version or commit |
|---|---|
| Docker Engine / Compose | 29.8.1 / v5.5.1, from the Docker apt repository |
| vcon-server |
d441470b, main on 2026-09-20 |
| Valkey / PostgreSQL |
valkey/valkey:9.1.2-alpine / postgres:17.11-alpine
|
| OpenSIPS, built here |
f46ef9337b, master, 4.1.0-dev |
| OpenSIPS, from packages | 4.0.2, installed as the OpenSIPS guide installs it |
| rtpengine |
8da4be3355, master, packaged as 26.3.0.0 |
| vcon-siprec-adapter |
fa09b939d3, main |
| SIPp (for the test call) | the distribution's sip-tester: 3.7.3 on Debian, 3.7.2 on Ubuntu |
The whole vCon stack used about 165 MiB of memory while idle.
The examples use 192.0.2.10 as the machine's address. Replace it with yours
everywhere it appears.
Find your case, and follow the steps it names:
-
No SIP server yet. Follow every step.
- The step "Install OpenSIPS's recording module" builds OpenSIPS for you.
- You may rather run the OpenSIPS 4.0 packages. Then install them with Install OpenSIPS in the OpenSIPS guide.
- Then take the packages' route in "Install OpenSIPS's recording module".
-
OpenSIPS already runs, from the packages or built from source.
- The step "Install OpenSIPS's recording module" adds the
siprecmodule. - The step "Configure OpenSIPS to record every call" finds your configuration file and replaces it.
- On a machine whose script you want to keep, add the lines that step marks to your own script instead.
- rtpengine may already relay your calls, as Add rtpengine to your voice stack sets it up. Then skip the step "Build rtpengine".
- The step "Install OpenSIPS's recording module" adds the
- Kamailio. Kamailio cannot record over SIPREC. Read With Kamailio first.
-
Both OpenSIPS and Kamailio on one machine.
- OpenSIPS records the calls that pass through it.
- Kamailio's calls go unrecorded.
- With Kamailio says how to get vCons for both.
vcon-server runs as containers. Install Docker Engine and the Compose plugin from the Docker repository:
# Run all of these, in order.
sudo apt-get update
sudo apt-get install -y ca-certificates curl git
sudo install -m 0755 -d /etc/apt/keyrings
. /etc/os-release
sudo curl -fsSL "https://download.docker.com/linux/$ID/gpg" -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/$ID $VERSION_CODENAME stable" \
| sudo tee /etc/apt/sources.list.d/docker.list >/dev/null
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo usermod -aG docker "$USER"Log out and back in. Your account's new docker group then takes effect.
Then check:
docker compose versionCheck out vcon-server at the tested commit:
# Run all of these, in order.
sudo mkdir -p /opt/vcon && sudo chown "$USER": /opt/vcon
cd /opt/vcon
git clone https://github.com/vcon-dev/vcon-server.git
git -C vcon-server checkout d441470bThe docker-compose.yml in the checkout runs Redis and development tooling.
Write a smaller one beside the checkout. It runs Valkey and PostgreSQL instead:
cat > /opt/vcon/compose.yml <<'EOF'
name: vcon
services:
valkey:
image: valkey/valkey:9.1.2-alpine
command: ["valkey-server", "--save", "20", "1", "--notify-keyspace-events", "Ex",
"--appendonly", "yes", "--dir", "/data"]
volumes: [valkey_data:/data]
healthcheck:
test: ["CMD-SHELL", "valkey-cli ping | grep -q PONG"]
interval: 10s
retries: 12
restart: unless-stopped
postgres:
image: postgres:17.11-alpine
environment:
POSTGRES_DB: vcon
POSTGRES_USER: vcon
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}
volumes: [postgres_data:/var/lib/postgresql/data]
healthcheck:
test: ["CMD-SHELL", "pg_isready -U vcon -d vcon"]
interval: 10s
retries: 12
restart: unless-stopped
conserver:
build: {context: ./vcon-server, dockerfile: ./docker/Dockerfile}
command: ["python", "/app/conserver/main.py"]
volumes:
- ./vcon-server:/app
- ./config.yml:/app/config.yml:ro
env_file: [.env]
depends_on:
valkey: {condition: service_healthy}
postgres: {condition: service_healthy}
restart: unless-stopped
api:
build: {context: ./vcon-server, dockerfile: ./docker/Dockerfile}
command: ["uvicorn", "api:app", "--host", "0.0.0.0", "--port", "8000"]
volumes:
- ./vcon-server:/app
- ./config.yml:/app/config.yml:ro
ports: ["8000:8000"]
env_file: [.env]
depends_on:
valkey: {condition: service_healthy}
restart: unless-stopped
volumes:
valkey_data: {}
postgres_data: {}
EOFCreate the two files that hold secrets.
-
umask 077makes both readable by you alone. - Each secret is a fresh random value:
# Run all of these, in order.
cd /opt/vcon
umask 077
cat > .env <<EOF
REDIS_URL=redis://valkey
CONSERVER_CONFIG_FILE=/app/config.yml
CONSERVER_API_TOKEN=$(openssl rand -hex 32)
POSTGRES_PASSWORD=$(openssl rand -hex 24)
EOF
. ./.env
cat > config.yml <<EOF
ingress_auth:
siprec: "$(openssl rand -hex 32)"
links:
mark_recorder:
module: links.tag
options:
tags: [vcon_role:recorder]
storages:
postgres_recorded:
module: storage.postgres
options:
host: postgres
port: 5432
database: vcon
user: vcon
password: "$POSTGRES_PASSWORD"
table_name: vcons_recorded
chains:
siprec_recorder:
links: [mark_recorder]
ingress_lists: [siprec]
storages: [postgres_recorded]
enabled: 1
EOF
umask 022What that configuration says:
-
ingress_authgives the ingress listsiprecits own key.- A producer that holds this key can post to
siprecand nothing else. - The full
CONSERVER_API_TOKENin.envis for reading vCons back. - Keep it off the producers.
- A producer that holds this key can post to
-
chainsroutes everything that arrives onsiprec:- through the
mark_recorderlink, which tags each vConvcon_role:recorder, - into the
vcons_recordedtable. - PostgreSQL creates the table on the first write.
- through the
Start it and check that the API answers:
# Run all of these, in order.
cd /opt/vcon
docker compose up -d --build
until curl -sf localhost:8000/health; do sleep 2; done; echo
until docker compose logs conserver | grep -q 'Worker-1 started'; do sleep 2; done
until docker compose exec -T postgres pg_isready -q -h 127.0.0.1 -U vcon -d vcon; do sleep 2; done
docker compose psWhat those wait lines are for:
- The API takes a few seconds to start. So the first
untilline waits for/healthto answer{"status":"healthy",...}. - The conserver stores what the API queues. It starts later.
- On a fresh machine its worker came up about 15 seconds after the API answered.
- The second
untilline waits for it.
- On its first start PostgreSQL creates its database with a temporary server.
Then it stops that server and starts the real one.
- So the third
untilline waits for the real one to answer on the network. - The temporary server never listens there.
- So the third
All four services then show running. valkey and postgres also show
healthy.
The ingress route answers 204 when it has queued a vCon. It does not wait
until it has stored it. Post a small one and look for it in PostgreSQL:
# Run all of these, in order.
cd /opt/vcon
KEY=$(sed -n 's/^ siprec: "\(.*\)"/\1/p' config.yml)
UUID=$(cat /proc/sys/kernel/random/uuid)
printf '{"vcon":"0.0.1","uuid":"%s","created_at":"%s","parties":[{"tel":"+15551230001"},{"tel":"+15551230002"}],"dialog":[],"analysis":[],"attachments":[]}' \
"$UUID" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > /tmp/test.vcon.json
curl -s -o /dev/null -w '%{http_code}\n' -X POST \
"localhost:8000/vcon/external-ingress?ingress_list=siprec" \
-H "x-conserver-api-token: $KEY" -H "Content-Type: application/json" \
--data-binary @/tmp/test.vcon.json
rm /tmp/test.vcon.json
sleep 3
docker compose exec -T postgres psql -U vcon -d vcon -c \
"select id, vcon_json->'attachments' from vcons_recorded where id='$UUID'"The curl prints 204. The query returns one row. Its attachment is the
["vcon_role:recorder"] tag. A wrong key gets 403 instead.
rtpengine's own documentation recommends building Debian packages from the source tree. That gives you:
- a systemd unit,
- a configuration file,
- a clean uninstall.
The build:
- installs its dependencies from the tree's
debian/control, - then runs rtpengine's test suite, which takes a while:
# Run all of these, in order.
sudo apt-get install -y --no-install-recommends build-essential devscripts equivs fakeroot
sudo mkdir -p /usr/local/src/voice && sudo chown "$USER": /usr/local/src/voice
cd /usr/local/src/voice
git clone https://github.com/sipwise/rtpengine.git
cd rtpengine
git checkout 8da4be3355
sudo mk-build-deps -i -r -t "apt-get -y --no-install-recommends" debian/control
dpkg-buildpackage -us -uc -b -j2
cd ..
sudo apt-get install -y ./ngcp-rtpengine-daemon_26.3.0.0+0~mr26.3.0.0_amd64.deb \
./ngcp-rtpengine-utils_26.3.0.0+0~mr26.3.0.0_all.debThe package's configuration asks for rtpengine's kernel forwarding module. This guide does not install that module. Tell rtpengine to forward in userspace, and restart it:
# Run all of these, in order.
sudo sed -i '0,/^table = 0/s//table = -1/' /etc/rtpengine/rtpengine.conf
sudo systemctl restart ngcp-rtpengine-daemon
systemctl is-active ngcp-rtpengine-daemonIt listens for OpenSIPS on 127.0.0.1:2223. It relays media on ports
30000-39999.
OpenSIPS records with its siprec module.
With the OpenSIPS packages, it comes in a package of its own. The
uac_auth module comes in another. The step "Configure OpenSIPS to record
every call" loads that module:
sudo apt-get install -y opensips-siprec-module opensips-auth-modulesOr build OpenSIPS with it. Skip this if you installed the packages.
- OpenSIPS master builds with compiler optimizations turned off.
- That is right for OpenSIPS's own developers.
- It is wrong for a proxy carrying calls.
- Turn them back on before you build.
- Add the
siprecmodule. The default build leaves it out. - You may already have built OpenSIPS from source by one of these guides.
- Then the block reuses that tree,
/usr/local/src/voice/opensips. - It rebuilds it with
siprec.
- Then the block reuses that tree,
- With a tree of your own, change
/usr/local/src/voicein the block to the directory that holds it:
# Run all of these, in order.
sudo apt-get install -y --no-install-recommends bison flex uuid-dev pkg-config libncurses-dev
cd /usr/local/src/voice
[ -d opensips ] || git clone https://github.com/OpenSIPS/opensips.git
cd opensips
git checkout f46ef9337b
make Makefile.conf
sed -i 's/^DEFS+= -DCC_O0/#DEFS+= -DCC_O0/' Makefile.conf
make -j2 all include_modules="siprec"
sudo make install include_modules="siprec"
/usr/local/sbin/opensips -V | head -2The second line of opensips -V lists the build flags. CC_O0 is no longer
among them.
Leave DBG_MALLOC as it is.
- With both it and
CC_O0switched off, this commit of master does not compile. -
net/tcp_conn_defs.hcallsget_ticks()without including the header that declares it. - Only
DBG_MALLOC's headers happen to supply it.
The build installs under /usr/local:
- the binary is
/usr/local/sbin/opensips, - modules are in
/usr/local/lib64/opensips/modules/, - OpenSIPS reads its configuration from
/usr/local/etc/opensips/.
The packages install under /usr, with the configuration in /etc/opensips/.
This configuration is a minimal proxy with recording added.
- Your own script does much more (registration, authentication, routing to carriers).
- The recording part is the block marked below, and the
loadmodulelines it needs.
The packages and the source build keep their configuration and their modules in different places. The first two lines find them:
-
Cis your configuration file. -
Mis the directory your install loads modules from. The script'smpathnames it.
# Run all of these, in order.
for f in /etc/opensips/opensips.cfg /usr/local/etc/opensips/opensips.cfg; do sudo test -f "$f" && C=$f && break; done
for d in /usr/lib/*/opensips/modules /usr/local/lib64/opensips/modules; do [ -f "$d/tm.so" ] && M=$d && break; done
echo "configuration: $C modules: $M"
sudo tee "$C" >/dev/null <<'EOF'
# OpenSIPS as a SIP proxy that records every call over SIPREC.
log_level=3
stderror_enabled=no
syslog_enabled=yes
syslog_facility=LOG_LOCAL0
udp_workers=2
open_files_limit=4096
socket=udp:192.0.2.10:5060 # the address your phones and carriers reach
mpath="MODULES/"
loadmodule "proto_udp.so" # built into the core, but still loaded by name
loadmodule "signaling.so"
loadmodule "sl.so"
loadmodule "tm.so"
loadmodule "rr.so"
loadmodule "maxfwd.so"
loadmodule "sipmsgops.so"
loadmodule "mi_fifo.so"
modparam("mi_fifo", "fifo_name", "/run/opensips/opensips_fifo")
# Recording: siprec needs dialog, b2b_entities and rtp_relay, loaded first.
loadmodule "dialog.so"
loadmodule "uac_auth.so"
loadmodule "b2b_entities.so"
loadmodule "siprec.so"
loadmodule "rtp_relay.so"
loadmodule "rtpengine.so"
modparam("rtpengine", "rtpengine_sock", "udp:127.0.0.1:2223")
route {
if (!mf_process_maxfwd_header(10)) {
send_reply(483, "Too Many Hops");
exit;
}
if (has_totag()) {
if (is_method("ACK") && t_check_trans()) {
t_relay();
exit;
}
if (!loose_route()) {
send_reply(404, "Not here");
exit;
}
t_relay();
exit;
}
if (is_method("CANCEL")) {
if (t_check_trans())
t_relay();
exit;
}
t_check_trans();
if (!is_method("INVITE")) {
send_reply(405, "Method Not Allowed");
exit;
}
record_route();
# --- recording starts here ---
create_dialog();
rtp_relay_engage("rtpengine");
siprec_start_recording("sip:127.0.0.1:5090");
# --- recording ends here ---
# Where the call goes. Here, a test callee on this machine; in your
# stack, lookup("location"), dispatcher or a carrier.
$du = "sip:127.0.0.1:5070";
t_relay();
}
EOF
sudo sed -i "s|^mpath=\"MODULES/\"|mpath=\"$M/\"|" "$C"
sudo grep '^mpath=' "$C"What the recording block does, line by line:
-
create_dialog()makes OpenSIPS track the call.- So it knows when the call ends.
- It can end the recording with it.
-
rtp_relay_engage("rtpengine")puts rtpengine in the media path.- The recorder can only receive a copy of audio that passes through rtpengine.
-
siprec_start_recording("sip:127.0.0.1:5090")opens the SIPREC session to the recorder.- It starts only when the callee's SDP arrives, usually in the
200 OK.
- It starts only when the callee's SDP arrives, usually in the
loadmodule "uac_auth.so" is there because b2b_entities asks for it at
startup. Leaving it out works. But it then logs a warning on every start.
The packages come with an opensips user and a systemd unit.
- Did you install them? Or does your source build already run as a service? Then skip to Check the configuration and start OpenSIPS.
- A fresh build has neither. Run OpenSIPS as its own user, under systemd:
# Run all of these, in order.
sudo useradd --system --home-dir /run/opensips --shell /usr/sbin/nologin opensips
sudo chown root:opensips /usr/local/etc/opensips
sudo chmod 750 /usr/local/etc/opensips
sudo tee /etc/systemd/system/opensips.service >/dev/null <<'EOF'
[Unit]
Description=OpenSIPS SIP server
After=network.target ngcp-rtpengine-daemon.service
[Service]
Type=forking
User=opensips
Group=opensips
RuntimeDirectory=opensips
RuntimeDirectoryMode=775
PIDFile=/run/opensips/opensips.pid
ExecStart=/usr/local/sbin/opensips -P /run/opensips/opensips.pid -f /usr/local/etc/opensips/opensips.cfg -m 64 -M 8
Restart=always
TimeoutStopSec=30s
LimitNOFILE=262144
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload# Run all of these, in order.
for f in /etc/opensips/opensips.cfg /usr/local/etc/opensips/opensips.cfg; do sudo test -f "$f" && C=$f && break; done
sudo chown root:opensips "$C"
sudo chmod 640 "$C"
sudo opensips -C -f "$C"
sudo systemctl enable opensips
sudo systemctl restart opensips
systemctl is-active opensipsopensips -C checks the configuration before you start anything. It prints
config file ok.
vcon-siprec-adapter runs as a container.
- It uses the host's network directly.
- A call's audio arrives on a range of UDP ports.
- Publishing a range through Docker is slow.
- Its ports stay clear of everything else on this machine:
- SIP on
127.0.0.1:5090, - audio on 40000-40999,
- its health check on
127.0.0.1:8081.
- SIP on
# Run all of these, in order.
sudo mkdir -p /opt/siprec && sudo chown "$USER": /opt/siprec
cd /opt/siprec
git clone https://github.com/vcon-dev/vcon-siprec-adapter.git
git -C vcon-siprec-adapter checkout fa09b939d3
mkdir -p vcons dlq logs
KEY=$(sed -n 's/^ siprec: "\(.*\)"/\1/p' /opt/vcon/config.yml)
umask 077
cat > config.yaml <<EOF
server:
listen_address: "127.0.0.1"
sip_port_udp: 5090
sip_port_tcp: 5090
sip_port_tls: 5091
tls_cert: null
tls_key: null
user_agent: "SIPREC-SRS/1.0"
max_sessions: 100
session_timeout: 3600
storage:
local_path: "/app/vcons"
filename_pattern: "{timestamp}_{call_id}.vcon.json"
create_directories: true
cleanup_temp_files: true
webhooks:
enabled: true
dlq_path: "/app/dlq"
endpoints:
- url: "http://127.0.0.1:8000/vcon/external-ingress?ingress_list=siprec"
headers:
x-conserver-api-token: "$KEY"
retry_attempts: 3
timeout: 30
backoff_factor: 2.0
hmac_secret: null
media:
mode: "inline"
publisher: "none"
signing:
enabled: false
health:
enabled: true
host: "127.0.0.1"
port: 8081
rtp:
buffer_size: 65536
supported_codecs: ["PCMU/8000", "PCMA/8000", "G722/8000", "opus/48000"]
audio_format: "wav"
sample_rate: 8000
channels: 1
port_range_start: 40000
port_range_end: 40999
lawful_basis:
enabled: true
lawful_basis: "legitimate_interests"
purposes: ["recording"]
expiration: null
justification: "Recorded by the operator of this SIP service."
logging:
level: "INFO"
file: "/app/logs/siprec-srs.log"
max_size: "10MB"
backup_count: 5
format: "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
EOF
umask 022
cat > compose.yml <<'EOF'
name: siprec
services:
srs:
build: ./vcon-siprec-adapter
network_mode: host
volumes:
- ./config.yaml:/app/config.yaml:ro
- ./vcons:/app/vcons
- ./dlq:/app/dlq
- ./logs:/app/logs
environment:
SIPREC_PUBLIC_IP: 127.0.0.1
restart: unless-stopped
EOF
docker compose up -d --build
until curl -fs 127.0.0.1:8081/healthz; do sleep 2; done; echoThe recorder takes a few seconds to start after its container does. So the
last line waits until the health check answers {"status": "ok", ...}.
Four settings deserve a word:
-
sip_port_tls: 5091with no certificate. TLS stays off.- The adapter's own
config.yamlpoints at certificate files that do not exist. - It does not start with them.
- Its validator also refuses a TLS port of
null.
- The adapter's own
-
webhooksposts each finished vCon to thesiprecingress list with that list's key.- If every attempt fails, the vCon goes to
dlq/(dead-letter queue) instead. - So it is not lost.
- If every attempt fails, the vCon goes to
-
storage.local_pathkeeps a copy of every vCon, audio included, in/opt/siprec/vcons.- That copy is call data.
- The operating section says how to clear it.
-
signing: enabled: false. A signed (JWS) vCon does not match the shape vcon-server's ingress route accepts.
SIPp plays both ends:
- a callee that echoes audio back,
- a caller that dials through OpenSIPS and plays a recorded G.711 sample.
SIPp's built-in caller ignores the Record-Route header OpenSIPS adds.
- So its
BYEwould miss the proxy and draw404 Not here. - The three
sedlines make it honor the route set, the way a real phone does. - They also drop the DTMF clip the recorder cannot store:
# Run all of these, in order.
sudo apt-get install -y sip-tester
mkdir -p ~/sipp/pcap && cd ~/sipp
ln -sf /usr/share/sip-tester/*.pcap pcap/
sipp -sd uac_pcap > uac_rr.xml
sed -i 's|<recv response="200" rtd="true" crlf="true">|<recv response="200" rtd="true" crlf="true" rrs="true">|' uac_rr.xml
sed -i -E 's#^( *)(ACK|BYE) sip:\[service\]@\[remote_ip\]:\[remote_port\] SIP/2.0#\1\2 [next_url] SIP/2.0\n\1[routes]#' uac_rr.xml
sed -i '/dtmf_2833_1.pcap/d' uac_rr.xml
sipp -sn uas -i 127.0.0.1 -p 5070 -rtp_echo -m 1 -bg
sudo sipp -sf uac_rr.xml 192.0.2.10:5060 -i 192.0.2.10 -p 5080 -s echo -m 1 -timeout 90sThe caller needs sudo because it plays the audio sample through a raw socket.
At the end SIPp's statistics screen shows Successful call at 1.
When the call ends, the recorder builds the vCon and posts it. Its log names the vCon's UUID:
# Run all of these, in order.
cd /opt/siprec
docker compose logs --since 5m srs | grep -E "Vcon object initialized|delivered vCon"Look it up in PostgreSQL, with the UUID from that log line:
# Run all of these, in order.
cd /opt/vcon
docker compose exec -T postgres psql -U vcon -d vcon -c \
"select id, jsonb_array_length(vcon_json->'parties') as parties,
jsonb_array_length(vcon_json->'dialog') as recordings
from vcons_recorded order by created_at desc limit 1"The test call produced one vCon with:
- 2 parties,
- 2 recordings, one for each direction of the audio.
- Each is an
audio/wavof about 150 KB.
- Each is an
Check health.
# Run all of these, in order.
curl -s -w '\n' localhost:8000/health # vcon-server API
curl -s -w '\n' 127.0.0.1:8081/healthz # recorder
systemctl is-active opensips ngcp-rtpengine-daemon
cd /opt/vcon && docker compose psRead the logs.
# Run all of these, in order.
cd /opt/vcon && docker compose logs --tail 50 conserver
cd /opt/siprec && docker compose logs --tail 50 srs
sudo journalctl -u opensips -n 50
sudo journalctl -u ngcp-rtpengine-daemon -n 50The conserver logs one Completed processing vCon <uuid> ... Chain: siprec_recorder line for every vCon it stores.
Restart after a configuration change. vcon-server reads config.yml on
each request. But restart it anyway, so that both processes agree:
# Run all of these, in order.
cd /opt/vcon && docker compose restart conserver api
sudo systemctl restart opensipsClear the recorder's local copies.
- Once a vCon is in PostgreSQL, the copy in
/opt/siprec/vconsis a second copy of the call. - The container writes it as root. So remove it with
sudo. - This deletes copies more than a day old:
sudo find /opt/siprec/vcons -name '*.vcon.json' -mtime +1 -deleteCheck /opt/siprec/dlq as well. Anything there is a vCon that never reached
vcon-server.
Back up the store. Everything worth keeping is in PostgreSQL:
# Run all of these, in order.
cd /opt/vcon
docker compose exec -T postgres pg_dump -U vcon vcon | gzip > vcon-$(date +%F).sql.gzUninstall.
-
docker compose down -vremoves the containers and their volumes.- The volumes hold every stored vCon.
- The rtpengine build installed its build dependencies through one package,
ngcp-rtpengine-build-deps.- Purging it and running
autoremoveremoves them.
- Purging it and running
-
autoremovealso removes any other package that nothing depends on any more.- So on a machine that runs other software, drop
-yand read its list first.
- So on a machine that runs other software, drop
# Run all of these, in order.
cd /opt/siprec && docker compose down
cd /opt/vcon && docker compose down -v
sudo systemctl disable --now opensips ngcp-rtpengine-daemon
sudo apt-get purge -y ngcp-rtpengine-daemon ngcp-rtpengine-utils ngcp-rtpengine-build-deps
sudo apt-get autoremove -yKamailio has no SIPREC module.
- So it cannot hand a copy of a call to a recorder the way the step "Configure OpenSIPS to record every call" has OpenSIPS do.
- Neither Kamailio 6.1 nor its development branch has one.
- None of the 256 modules in Kamailio 6.1's source tree implements SIPREC.
- None of the 258 on its
masterbranch does either.
- The recorder in this guide, vcon-siprec-adapter, takes SIPREC only.
- So with Kamailio it receives nothing.
What a Kamailio stack can send the vCon server is sipnab's own vCons.
- sipnab reads the calls off the wire, beside Kamailio.
- It writes a vCon for each. The vCon holds:
- the parties,
- the signaling,
- with the media in its capture, the audio.
- It is an observer's record, not a party's.
- A packet the capture missed is not in it.
-
Send sipnab's vCons to a vCon server sets it up.
- It keeps those vCons apart from recorded ones.
With Kamailio:
- Follow the first two steps here, "Install Docker" and "Install the vCon server". Skip the rest of the steps, from "Build rtpengine" to "Find the call's vCon": the recorder and its test.
- Follow Send sipnab's vCons to a vCon server. Its With Kamailio section places the test call.
With OpenSIPS and Kamailio on one machine:
- Follow every step here for OpenSIPS's calls.
- Then follow the sipnab guide. Its same section widens sipnab's capture to Kamailio's port.
The vCon server on its own machine.
- The vCon server, Valkey and PostgreSQL can have a machine to themselves,
here
192.0.2.20. - The recorder stays beside the SIP proxy and rtpengine. They send it the audio.
The steps:
- Follow "Install Docker" and "Install the vCon server" on that machine.
- On the proxy's machine, do the steps from "Build rtpengine" to "Configure OpenSIPS to record every call".
- Then fetch the recorder's key before "Install the recorder".
- "Install the recorder" reads it from
/opt/vcon/config.yml. - So this writes that file with the key and nothing else, not the database password:
- "Install the recorder" reads it from
# Run all of these, in order.
sudo mkdir -p /opt/vcon && sudo chown "$USER": /opt/vcon
ssh 192.0.2.20 "grep '^ siprec: ' /opt/vcon/config.yml" > /opt/vcon/config.yml
chmod 600 /opt/vcon/config.yml
grep -c '^ siprec: "' /opt/vcon/config.ymlIt prints 1. Then do "Install the recorder". Send the recorder's vCons to the vCon server's address in place of the loopback one:
# Run all of these, in order.
cd /opt/siprec
sed -i 's|url: "http://127.0.0.1:8000/|url: "http://192.0.2.20:8000/|' config.yaml
grep -n 'url:' config.yaml
docker compose up -d --force-recreate"Place a test call" and "Find the call's vCon" then work as written.
- Run the database query from "Find the call's vCon" on the vCon server's machine.
- Port 8000 on it is now reachable from the network. So allow it only from the machines that post vCons.
This guide covers that split only.
- The recorder receives the audio from rtpengine.
- So moving either one to another machine changes the addresses both of them and OpenSIPS use.
- This guide has not run that.
-
ERROR:core:main: no transport protocol loaded.-
loadmodule "proto_udp.so"is missing. - The OpenSIPS binary contains the module.
- But the script must still name it.
-
-
loading config file ... Permission denied.- OpenSIPS runs as
opensipsand cannot read its configuration. -
make installcreates/usr/local/etc/opensipsreadable by root alone. - So give the group both the directory and the file, as in the step "Configure OpenSIPS to record every call".
- The packages set this up themselves.
- But a file you wrote as root keeps root's group until that step changes it.
- OpenSIPS runs as
-
The test call's
BYEgets404 Not here.- The caller ignored the route set.
- Use the edited
uac_rr.xml, not SIPp's built-inuac_pcap.
-
/healthanswers but nothing reaches PostgreSQL.- A
204from the ingress route means queued. - Read
docker compose logs conserver. The conserver logs a storage failure there.
- A
-
rtpengine logs
FAILED TO OPEN KERNEL TABLE 0. One of these happened:-
table = -1was not set, - the service was not restarted after setting it.
-
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