Add an opt-in rollback message to the NG protocol. It lets a signalling
application undo an SDP offer that rtpengine has applied but the remote
endpoint subsequently rejects, without deleting the established call.
A client enables checkpointing with track-state on an offer. rtpengine
snapshots the affected dialogue before applying it; an answer commits the
exchange and discards the snapshot, and rollback restores and consumes it.
A dialogue holds at most one outstanding checkpoint, so offers arriving
before an exchange completes keep the existing snapshot and a rollback
returns to the last completed offer/answer.
A checkpoint holds a snapshot in the call record format the Redis encoder
already produces, so there is one definition of what a call's state looks
like and no second encoder to keep in step. Decoding a snapshot needs the
field readers the Redis restore path already had, so those are shared
rather than duplicated; json_restore_call() reads the same fields, in the
same order, with the same strict-return behaviour.
Applying a snapshot copies negotiated state back onto the live objects,
matched by unique id. The objects are never recreated, so local sockets
and ports survive. State a rejected offer introduced is removed as well as
overwritten: a field the encoder writes only when set is absent from a
snapshot taken before it existed, and that absence clears it.
ICE and DTLS reconverge rather than being rewound, since applying the
offer has already reset the agent and shut down the DTLS association.
Restoring the accepted credentials and candidates lets connectivity checks
rebuild ICE state, and restoring the fingerprint, TLS id and setup permits
a fresh handshake.
Outstanding checkpoints are stored in the Redis call record so they
survive takeover. Checkpoint data is auxiliary: one that cannot be read is
discarded in full while the call itself is restored without rollback
capability.
The preload shims no longer inherit the sanitizer options from CFLAGS.
They are loaded by the harness into the test scripts' own interpreter,
where a sanitizer runtime crashed the daemon tests on aarch64 during
library initialisation. Everything else in CFLAGS, the Debian hardening
flags included, still applies to them.
The Redis tests no longer assume the record length is four digits, and
read the record across as many reads as it takes.
Squashed:
commit ad49459152
Author: Daniel Donoghue <oneofsomany@hotmail.com>
Date: Fri Aug 28 13:33:17 2026 +0200
MT#55283 address review comments
Snapshots are now bencode, GLib macros are used throughout, and keys and
offsets are built with the existing helpers rather than by hand.
New tests cover the snapshot format and the state that exists only inside
a snapshot.
commit ff47944ad5
Author: Daniel Donoghue <oneofsomany@hotmail.com>
Date: Fri Aug 28 15:24:56 2026 +0200
MT#55283 scope snapshots to the dialogue
A snapshot now holds only the two monologues being checkpointed, and only
the entries the decoder reads: the socket and endpoint map pools, sinks,
subscriptions and aliases are left out.
The from-tag check in the rollback handler is dropped, since
call_get_monologue() is keyed on the tag.
commit 3bb19f2804
Author: Daniel Donoghue <oneofsomany@hotmail.com>
Date: Mon Aug 31 08:41:08 2026 +0200
MT#55283 give each monologue its own checkpoint
The checkpoint moves from a list on the call to a pointer on the monologue,
and its snapshot covers only that monologue. Checkpoints are stored in the
call record as checkpoint-<monologue id>; num_checkpoints and the offerer
and answerer ids are gone.
A monologue is shared between the branches of a forked call, so rolling one
branch back no longer reinstates what rolling another back had undone.
The call-level json dict is no longer written into snapshots, since nothing
reads it back.
commit 0c986a81e2
Author: Daniel Donoghue <oneofsomany@hotmail.com>
Date: Mon Aug 31 10:01:54 2026 +0200
MT#55283 do not report a rollback that did not happen
call_merge() renumbers every unique id, and a snapshot is keyed on them, so
a checkpoint taken before a merge no longer describes anything. It is
dropped with the ids it refers to, and rollback reports none outstanding
rather than a success that restored nothing.
Both sides of a dialogue are checkpointed together. A monologue is shared
between the branches of a forked call, so one side could already hold a
checkpoint while the other had never been tracked, leaving a rollback to
restore half a dialogue and still report success.
Each fix has a test that fails without it.
commit dfe7a72aff
Author: Daniel Donoghue <oneofsomany@hotmail.com>
Date: Mon Aug 31 10:41:50 2026 +0200
MT#55283 document that merging a call drops its checkpoints
Merging renumbers the state a snapshot refers to, so an outstanding
checkpoint is discarded and a later rollback reports none.
Closes#2159
Change-Id: Ie773e2cd4a39fc1c8f39fafe959784e269c0c80e
Forcing the DTLS role to passive only makes sense for the backwards
(torwards the offerer) direction anyway, as towards the answerer (in an
offer) it's not allowed by the RFC, and in an answer it's not possible
as the active DTLS connection would already be in progress.
Convert DTLS= processing into a list as multiple flags might be
required.
Change-Id: I54239280460e1bbce1966bc17a384489eb978618
Amends: 78ee0451 / Ib1d07831
Related: #2156
Related: #2145
Using the FILLED flag to decide how to resolve actpass is not reliable,
as during a reinvite FILLED would be set for both stream directions.
Instead, immediately clear the ACTIVE flag during the offer phase
(dtls_logic) and assume a strict passive role. Then if the answer
indicates that the connection needs to be active instead, a reset occurs
via dtls_connection_init.
Closes#2156
Change-Id: I0ded94620bcaa481eea920ae0c412c7bb86bff1f
DTLS connections created during an offer are passive by default. If the
answer requires a role change to active, don't reset the TLS ID, as we
have no way to communicate this change until a re-invite, which would
then lead to an unexpected reset of the connection.
Related: #2156
Change-Id: I49dcd93d63840a223c41a59c74e2f270d0a08f62
The primary pair selection already checks this, but a retransmit can
still trigger a check on a socket that has since been closed.
Closes#2154
Change-Id: I8308693e7269bfa9dcfcb24daf6863669c065139
Previously, once rtpengine detected that the remote endpoint of a session is a local endpoint, the
loop check would be permanently enabled for that session. A renegotiation that changes the remote
endpoint to be non-local, did not cause the check to be turned off again. This patch addresses that.
Closes#2152
Change-Id: I880428b09f5d1e5b4641c9de4563010e31d7a790
If a SSRC object is still in use (refcount is >1), keep it in the list,
even if the list is too large.
Otherwise we may end up recreating a new SSRC object for the same SSRC
that still has an object in use elsewhere, making locks unreliable.
Ref #2148
Change-Id: I2afd277696e05c356fc9cf5aea59f51e1fba0784
To not come across the sink's mutation (e.g. `endpoint.address.family`)
just lock way earlier, before to start processing and
translating endpoint to the kernel related structs.
Otherwise the other signaling path may mutate something
which will make the sink unactual and provide
to the `__re_address_translate_ep()` NULLed or garbage structs.
Change-Id: I88d87ae88abe4e37aaa5d3cdda76ed0ca82e4842
16de9688d6 resets the `socket.family` to NULL,
that makes the direct appeal to the corresponding
object unsafe.
E.g. `ps->selected_sfd->socket.family->name` when
filling the streams.
Add a simply guard and log `none` in case
the ip family is NULL.
Change-Id: I7ad7399e32e5d0ffb2fcc919822d05760fdc7421
If the dumper was not opened, then also take care
to clear the recording path, because later
the `response_pcap()` only checks the recording path
itself and not wehther the dumper was actually opened.
Hence NG would report a recording file, which indeed
was never even created.
For consistency reasons add the dumper's guard
in the `response_pcap()`.
Change-Id: Ifc1d5b73785633ffcc6293d64dbfbdea51b54582
The `finish_proc()` misses part of the clean-up
work if the kernel table isn't open.
Only `kernel_del_call()` really depends
on the `kernel.is_open`, local cleanup should still happen.
Change-Id: I8f999dbc48f7dd04b4ca1ba5edbc9218a3c49d29
When setting up streams, we currently check the pointer itself,
whether the kernel table isn't open, an existing stream index
and the `NO_RECORDING` flag.
Check additionally whether the recording call
was actually registered in the kernel.
Otherwise if the `proc_init()` fails to to add call
to the kernel recording interface, `setup_stream_proc()`
still tries to add the stream to the kernel.
Change-Id: Ibaf25d1f91f9818325e3e9f7be3316610f5c63be
From the cstdio docs, `snprintf()` - returns the number
of characters that would have been written
if n had been sufficiently large, not counting the terminating null character.
If a formatted unique id gets longer than 128 bytes,
or if the same happens later to the sequence of the formatted paramters,
then the according lenth becomes larger than the `sizeof(...)`,
and then the `append_meta_chunk()` may overflow the given buffer.
Just track the length by the fact after the printing is completed.
Also improve code formatting a little bit.
Change-Id: Ia3082623c4e59bd83c04af4c93851f8778bd73d2