Skip to content

pjsua: send a configured +sip.instance without SIP outbound too - #25

Closed
laconicman wants to merge 5 commits into
masterfrom
feat/instance-id-without-outbound-r2
Closed

laconicman wants to merge 5 commits into
masterfrom
feat/instance-id-without-outbound-r2

Conversation

@laconicman

@laconicman laconicman commented Oct 8, 2026 •

Copy link
Copy Markdown
Owner

Implements the proposal in pjsip#5292: an instance ID the application configured is sent in every REGISTER, also without SIP outbound.

pjsua appends +sip.instance to the registration Contact only together with reg-id, while SIP outbound is wanted, and drops both when the registrar does not confirm outbound (the 2010 design, which pjsip#5281 made take effect on the next refresh). RFC 5626 §6 allows an instance ID without reg-id, and registrars such as Kamailio and Flexisip key a binding on it alone, as does IMS (3GPP TS 24.229 §5.1.1.2.1).

  • update_regc_contact(): if rfc5626_instance_id is set, appends rfc5626_instprm whether or not outbound is wanted, and rfc5626_regprm only while it is. That covers UDP, use_rfc5626 disabled, a 2xx without Require: outbound, and the retry after a 439. The generated instance ID is sent as before, only with outbound.
  • init_outbound_setting() also runs for an account that sets an instance ID with use_rfc5626 disabled, and marks outbound as wanted only if use_rfc5626 is set, so the status of such an account never says outbound is wanted (defence in depth: pjsua_regc_init() resets it anyway). It logs a configured instance ID once (Acc N: instance ID … is sent in every REGISTER): when that line is missing, the application's setting did not arrive.
  • No new field. Setting rfc5626_instance_id (pjsua2: sipOutboundInstanceId) is the opt-in; with it empty nothing changes. The docs of both now say that a configured ID goes into every REGISTER, that Kamailio and Flexisip treat contacts with one instance ID as one device and replace the older binding, so the ID must be unique to the device and persistent (RFC 5626 §4.1), and why the generated one stays outbound-only.
  • pjsua app: --instance-id=URN, applied to every account that has no instance ID of its own, including one added at runtime with +a, and --outb-disable, which turns SIP outbound off for every account (the app had no way to). Both reach +a accounts, which is how the TCP scenarios avoid a start-up REGISTER racing SIPp, as uas-register-outbound-norequire does.

Why only a configured instance ID

An earlier version added an account switch that also sent the generated instance ID without outbound. The generated value is a 32-bit hash of the host name, so it is the same for every pjsua instance on one host, and on every iPhone (gethostname() returns localhost). Sent everywhere, two processes or two phones on one AOR would advertise one instance ID, and a registrar that keys bindings on it would let one replace the other. Only an ID the application chose is now sent beyond outbound.

The default itself is left alone. Changing how it is generated would change the instance ID every existing deployment sends, so registrars would see a new device after an upgrade. pjlib has no persistent store to keep a random one in, and the stable identifiers on phones are not reachable from C, so the application has to supply the ID.

Contact on the wire

Case instance ID not set (unchanged) instance ID set
TCP, 200 without Require: outbound: first / refresh ;reg-id=1;+sip.instance=<generated> / neither ;reg-id=1;+sip.instance=… / ;+sip.instance=…
TCP, Require: outbound on the first 200 only: REGISTER after the 200 without it neither ;+sip.instance=…
TCP, 439 to outbound: retry neither, Supported: path ;+sip.instance=…, Supported: path
UDP neither ;+sip.instance=…
TCP, use_rfc5626 disabled neither, no Supported: outbound ;+sip.instance=…, no reg-id, no Supported: outbound

Verification

Check Result
uas-register-instance-norequire, uas-register-instance-udp pass, 6/6 runs each across the builds of this branch
uas-register-instance-nooutbound passes, 3/3
The first two with --instance-id removed, the third with --outb-disable removed (scratch copies) fail: SIPp returned error 1: At least one call failed
uas-register-instance-nooutbound against a build without the init_outbound_setting() guard passes too: pjsua_regc_init() starts with destroy_regc(), which clears acc->contact and resets the status, so update_regc_contact() always runs before Supported is built. The scenario covers that wire row; the guard is defence in depth
uas-register-outbound-norequire (generated ID still dropped with reg-id) passes, unchanged
uas-norefersub, uas-auth pass
Scripted TCP registrar, all rows above, set and not set, and a build of a67b8e8 as in the table; not set is identical to a67b8e8
make no new warnings

Run locally on macOS with SIPp 3.7.7 (/usr/bin/python3 run.py -e <pjsua> mod_sipp.py scripts-sipp/<scenario>.xml).

Not verified here: Linux and Windows, real registrars.

This touches update_regc_contact(), as pjsip#5311 does; the two merge cleanly in either order.

Patch size: +41 code, +15 comment lines in source (contrib loc), plus three SIPp scenarios.


Devin Review

pjsua appends +sip.instance to the registration Contact only together
with reg-id, while SIP outbound is wanted, and drops both when the
registrar does not confirm outbound. RFC 5626 section 6 allows an
instance ID without reg-id, and registrars use it on its own to
recognize a device across registrations.

If the application set rfc5626_instance_id, update_regc_contact() now
appends it whether or not outbound is wanted: over UDP, with use_rfc5626
disabled, when a 2xx lacks "Require: outbound", and on the retry after a
439. reg-id is still sent only while outbound is wanted. The generated
instance ID is sent as before, only with outbound: it is a hash of the
host name, the same for all instances on one host and on iOS.

init_outbound_setting() now also runs for an account that sets an
instance ID with use_rfc5626 disabled, marks outbound as wanted only if
use_rfc5626 is set, and logs a configured instance ID.

The pjsua app gets --instance-id, applied to every account including
those added at runtime.
Two SIPp scenarios next to uas-register-outbound-norequire, which keeps
asserting that the generated instance ID is dropped with reg-id.

- uas-register-instance-norequire: TCP registrar answering 200 without
  "Require: outbound". The first REGISTER must carry reg-id; the refresh
  must carry the configured +sip.instance and no reg-id.
- uas-register-instance-udp: over UDP, where outbound is never offered,
  the REGISTER must carry the configured +sip.instance and no reg-id.
The startup loop assigned --instance-id to every account, also when the
option was not given, wiping a value an application set per account in
on_config_init(). Apply it only when given, and only to accounts that
have no instance ID of their own.
Registrars that key bindings on +sip.instance, such as Kamailio and
Flexisip, treat contacts with one instance ID as one device and replace
the older binding. Say so next to rfc5626_instance_id and
sipOutboundInstanceId, and that the ID must be unique to the device and
persistent (RFC 5626 section 4.1).

Also fix the comment in update_regc_contact()'s outbound branch, which
now appends only reg-id.
uas-register-instance-nooutbound: TCP, outbound disabled, instance ID
configured. The REGISTER must carry +sip.instance, no reg-id and no
"outbound" option tag.

The pjsua app gets --outb-disable, applied to every account including
those added at runtime, since it had no way to turn SIP outbound off.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Devin Review found 1 potential issue.

Devin Review

Comment on lines +2415 to +2418
if (config->instance_id.slen) {
cfg_addf(&cfg, max, "--instance-id %.*s\n",
(int)config->instance_id.slen, config->instance_id.ptr);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Account-specific instance IDs lost on reload

When an account overrides rfc5626_instance_id, write_settings saves only config->instance_id. Reloading assigns the global ID instead, so the account registers under a different device identity.

Learn more

An application can set a separate instance ID on an account through on_config_init; the startup loop preserves that override. The settings writer serializes account options before global options, but write_account_settings never writes the account's instance ID. Reloading therefore leaves the override empty, and the global ID fills it during account creation.

Example: A callback sets account A's ID to <urn:uuid:aaaa> and the global ID to <urn:uuid:bbbb>. Saving then reloading retains only bbbb, so A registers as bbbb, not aaaa.

Recommended fix: Serialize per-account instance IDs where account options are written, and parse them as account-scoped values. Preserve the global fallback separately so saving and reloading an overridden account produces the same registration identity.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Declined. on_config_init() runs in app_init() on every start, after the settings file is read (pjsua_app.c, just before the accounts are added), so a per-account instance ID it sets is applied again each time; saving and reloading cannot lose it. The settings file has no per-account instance-ID option to write it to, and adding one would be a new feature of its own; values that only code sets are not written today either (rfc5626_reg_id set through --outb-rid is not written back, for example).

@laconicman

Copy link
Copy Markdown
Owner Author

Landed upstream as pjsip/pjproject#5313 (70ee4eb2a), identical changed lines. This fork review has served its purpose.

@laconicman laconicman closed this Oct 10, 2026
@laconicman
laconicman deleted the feat/instance-id-without-outbound-r2 branch October 10, 2026 15:47
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.

1 participant