Skip to content

Add SSRC option to force a fixed egress SSRC - #2166

Open
dnygate wants to merge 2 commits into
sipwise:masterfrom
dnygate:ng-ssrc-egress
Open

Add SSRC option to force a fixed egress SSRC#2166
dnygate wants to merge 2 commits into
sipwise:masterfrom
dnygate:ng-ssrc-egress

Conversation

@dnygate

@dnygate dnygate commented Sep 4, 2026

Copy link
Copy Markdown

This adds a new SSRC dictionary to the NG control protocol with two keys,
egress-to-offerer and egress-to-answerer. Each takes an SSRC value (an
integer, or a decimal or 0x-prefixed hex string). rtpengine then rewrites
the SSRC field of every RTP packet it sends towards that side of the call
to the given value.

Motivation: when rtpengine feeds media into a downstream system such as a
WebRTC selective forwarding unit (SFU), that system needs to know the SSRC
of the incoming stream up front in order to bind a receiver to it. Today
the controlling application can only learn rtpengine's outgoing SSRC after
the fact, from RTCP or from a query. With this option the control plane
picks the SSRC itself and tells both sides.

Implementation notes:

  • The forced SSRC is stored against the call leg it applies to as soon as
    the offer or answer carrying it is processed. It can be set in either
    message and stays in effect for the rest of the call.
  • The rewrite happens at the single point all outgoing RTP passes through
    just before SRTP encryption (media_packet_encrypt()). It therefore
    covers media that is relayed unchanged, media that is transcoded, and
    media generated by rtpengine itself, for plain RTP as well as SRTP, and
    the SRTP authentication tag is computed over the rewritten header. RTCP
    is left untouched.
  • Because the rewrite only exists in userspace, streams towards a party
    with a forced SSRC are excluded from kernel packet forwarding. Streams
    in the other direction are unaffected.
  • If the SSRCs in RTCP sender and receiver reports should match the forced
    value as well, I am happy to look into feeding it through the existing
    per-stream output SSRC mapping that RTCP generation already uses, which
    would also allow the kernel module to apply it.

Tests: t/auto-daemon-tests-ssrc.pl covers integer, hex string and decimal
string values, setting the value in the offer and in the answer, the flat
string flag syntax used by the SIP proxy modules, and that a value of 0
is ignored.

Example in string syntax: SSRC=[egress-to-answerer=0x12345678]

Add a new `SSRC` dictionary to the NG control protocol with the keys
`egress-to-offerer` and `egress-to-answerer`. Each takes an SSRC value
which rtpengine then writes into the SSRC field of every RTP packet it
sends towards that side of the call.

The forced SSRC is stored against the call leg it applies to as soon as
the offer or answer carrying it is processed, so it can be set in either
message and stays in effect for the rest of the call. The rewrite itself
happens in media_packet_encrypt(), the single point all outgoing RTP
passes through just before SRTP encryption. It therefore covers media
that is relayed unchanged, transcoded media and media generated by
rtpengine itself, for plain RTP as well as SRTP, and the SRTP
authentication tag is computed over the rewritten header. RTCP is left
untouched.

Because the rewrite is only implemented in userspace, streams towards a
party with a forced SSRC are excluded from kernel packet forwarding.

This allows a controlling application to make a downstream receiver,
such as a WebRTC selective forwarding unit, expect a known SSRC instead
of having to discover it from the media or from RTCP.

@rfuchs rfuchs left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The obvious question of course is: what if two ingress SSRCs appear? They would be rewritten to the same egress SSRC, which may have undesirable side effects for sequence and timestamp tracking, or SRTP contexts.

Also the a=ssrc attribute comes to mind - if we have a fixed egress SSRC, that would lend itself to their usage, no?

Comment thread daemon/media_socket.c
Comment on lines +2012 to +2015
// forced egress SSRC is only implemented in userspace
if (sh->sink->media && sh->sink->media->monologue
&& sh->sink->media->monologue->force_egress_ssrc)
goto no_kernel;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do have support for SSRC substitution in the kernel module (see ssrc_out and ssrc_subst)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. ssrc_subst and ssrc_out[] are already wired up from media_socket.c:1863,1882
off ssrc_map_out, with the substitution itself at nft_rtpengine.c:6745, so once this
moves onto ssrc_map_out the bailout serves no purpose and forced-SSRC streams can stay in
kernel forwarding. Removing it.

Comment thread daemon/media_socket.c
Comment on lines +3129 to +3138
if (!mp->rtcp && out->media && out->media->monologue && out->media->monologue->force_egress_ssrc) {
uint32_t ssrc = htonl(out->media->monologue->force_egress_ssrc);
IQUEUE_FOREACH(&mp->packets_out, p) {
str payload;
struct rtp_header *rh = rtp_payload(&payload, &p->s, NULL);
if (rh)
rh->ssrc = ssrc;
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This whole thing should really be handled by the existing SSRC substitution mechanism (see handler_func_passthrough_ssrc in codec.c) instead of adding another loop over the already-processed output packets (and even parsing the RTP header again)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. I will rework this onto __stream_ssrc_out() and ssrc_map_out rather than adding a
second pass over the already-processed output packets, which also does away with parsing the
RTP header again.

Comment thread daemon/call_flags.c
Comment on lines +133 to +139
case CSH_LOOKUP("egress-to-offerer"):
case CSH_LOOKUP("egress to offerer"):
out->ssrc_force.egress_to_offerer = call_ng_parse_ssrc(parser, value);
break;
case CSH_LOOKUP("egress-to-answerer"):
case CSH_LOOKUP("egress to answerer"):
out->ssrc_force.egress_to_answerer = call_ng_parse_ssrc(parser, value);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This mechanism would also be relevant to methods which don't involve two separate parties for offerer and answerer (e.g. publish/subscribe), so a syntax that is less specific to offer/answer would be beneficial.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair point. source-tag and from-tags look like the established pattern, so keying the
value by tag rather than by offerer or answerer role would cover publish/subscribe alongside
offer/answer.

This has to move from the monologue to the media in any case to handle bundle, so the
addressing is going to change regardless of what the keys end up being called. Unless you
would prefer a different shape, I will use a tag-keyed dictionary and drop
egress-to-offerer and egress-to-answerer.

@dnygate

dnygate commented Sep 4, 2026

Copy link
Copy Markdown
Author

Two ingress SSRCs

That is the case the current patch does not handle, and it needs the option to be narrower
than I have written it.

The deployment this came from involves a sequential succession of SSRCs rather than
concurrent streams: a PSTN gateway that changes its SSRC mid-call, after a restart or a media
source switch, feeding an SFU that has already bound a receiver to the original value. At
present each new ingress SSRC gets a fresh entry with seq_diff at zero and a fresh random
ssrc_map_out (ssrc.c:38), and the direct egress path passes the ingress SSRC through
unchanged, so the far side sees the change.

Collapsing that succession onto one stable egress SSRC only works if seq_diff and the SRTP
index both carry across the changeover. Without that, the receiver is handed a stable SSRC
whose sequence numbers jump at the point of the change while RTCP still reports the old
value, and on an SRTP leg the per-SSRC replay window will begin discarding packets. All of
that comes for free from ssrc_map_out, which is the same conclusion as your other comment
and roughly what I offered at the end of the description.

For genuinely concurrent ingress SSRCs I do not think there is a defensible answer, so I
would rather restrict the option than invent one: force a single SSRC per media and pass any
concurrent additional ones through untouched. The two sources of concurrency I can find are
bundle, where each m= line demuxes to its own call_media (media_socket.c:2841) with its
own SSRC hashes, and injected media, where media_player.c:245 mints its own SSRC and calls
media_packet_encrypt() directly. Both are visible at the point the option would be applied,
so the restriction can be enforced rather than only documented.

That also means the value wants to be scoped per media rather than per monologue as it is at
the moment, which runs straight into your naming comment.

a=ssrc

It does follow from a fixed egress SSRC, but it is a larger change than it first appears.
There is no ATTR_SSRC in enum attr_id, and struct attribute_ssrc at sdp.c:178 is
unreferenced apart from its member in the union, so both the parsing and the printing side
would be new work. I would prefer to keep that as a separate PR rather than grow this one,
unless you consider a fixed egress SSRC incomplete without it, in which case I will fold it
in here.

@dnygate

dnygate commented Sep 4, 2026

Copy link
Copy Markdown
Author

Holding the rework here until you have had a chance to look at the two open points above,
since the addressing and whether a=ssrc comes in now both change the shape of it and I
would rather not build it twice. The parts you have already called unambiguously, dropping
the kernel bailout and moving onto ssrc_map_out, I can push separately in the meantime if
you would rather review those on their own.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants