Skip to content

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

Merged
nanangizz merged 6 commits into
pjsip:masterfrom
laconicman:feat/instance-id-without-outbound
Oct 9, 2026
Merged

nanangizz merged 6 commits into
pjsip:masterfrom
laconicman:feat/instance-id-without-outbound

Conversation

@laconicman

@laconicman laconicman commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

Implements the proposal in #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 #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
After the rebase onto 98f3145 (#5311): the six scenarios above, the three negative controls, regc_test, pjsua_acc_test, and the scripted registrar rows all as before
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.

Rebased onto master after #5311, which reshaped update_regc_contact(); the change is the same, applied inside the new shape.

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

laconicman added a commit to laconicman/offhook that referenced this pull request Oct 4, 2026
* docs: instance ID on iOS, and which SIP server to run

- Instance-ID: why +sip.instance matters for a client that reconnects
  after every wake-up, why pjsua's host-name default cannot be used on
  iOS, what went wrong in a production client (identifierForVendor and
  UserDefaults before first unlock, a racing config writer, a random
  fallback, an SBC refusing the canonical form), and a proposed design:
  a Keychain-stored device ID written once, hashed per app and account.
  Links the upstream change, pjsip/pjproject#5313.
- Own-SIP-Server: Flexisip first (built-in APNs sender, bindings keyed
  on the instance ID; certificate-only, no Feature-Caps, AGPL),
  OpenSIPS second, Kamailio and Asterisk as lab subjects.
- Tech-Debt OH-11 (no instance ID of our own) and two Roadmap pointers.

* docs: mark pre-unlock launch by a VoIP push as unverified (U4)

@nanangizz nanangizz 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.

Thanks for the detailed write-up and the SIPp scenarios. The code looks correct to me: the init_outbound_setting() guard is needed (pjsua_regc_init() skips update_regc_contact() when acc->contact is set, so OUTBOUND_WANTED would otherwise leak into Supported: outbound, path), and the len accounting is right. My concern is the default behaviour.

Standards. RFC 5626 §6 does allow an instance ID without reg-id, but only as the GRUU case: "this combination will result in the creation of a GRUU, as described in the GRUU specification [RFC5627]". In RFC 5627 the UA asks for a GRUU with Supported: gruu, and the registrar returns pub-gruu/temp-gruu only then. PJSIP doesn't implement GRUU, so pjsua would send +sip.instance with neither Supported: gruu nor outbound, a combination neither RFC defines. It's valid on the wire, but what it does is up to the registrar.

Registrars. From their current sources:

  • Kamailio always matches bindings by instance ID (get_ucontact_by_instance()), without outbound or GRUU: the same ID updates the existing binding even from a new address, and a new Call-ID removes the other bindings with that ID.
  • Flexisip's default unique-id-parameters is +sip.instance pn-tok line, so the same ID replaces the old contact.
  • OpenSIPS stores the ID but matches bindings by Contact URI and Call-ID; the ID is only used for GRUU and remove().
  • Asterisk and the FreeSWITCH registrar don't use it.

So the benefit is real for Kamailio and Flexisip deployments, but on those same registrars it also changes binding semantics: devices sharing an ID replace each other's binding.

Request: make it opt-in. As it is, any application that already sets rfc5626_instance_id / sipOutboundInstanceId for outbound would start sending +sip.instance in every REGISTER after an upgrade, including over UDP (where pjsua never sends it today) and with outbound disabled. With use_rfc5626 on by default, that's a wide change for something the RFCs don't define. Could you add a dedicated account flag, default off, with its pjsua2 counterpart (e.g. pj_bool_t reg_send_instance_id / AccountRegConfig::sendInstanceId, the name is your call)? With it set, a configured instance ID is sent in every REGISTER as in this PR; without it, the current behaviour stays. The pjsua.h/account.hpp docs could then mention the Kamailio/Flexisip binding semantics and the need for a per-device ID (RFC 5626 §4.1). In the pjsua app, --instance-id could set the flag too, so the two scenarios keep working.

Minor:

  • update_regc_contact(): the comment in the need_outbound branch still says "append the contact with +sip.instance and reg-id parameters", while +sip.instance is now appended separately.
  • Optional: a scenario with use_rfc5626 disabled and an instance ID set, checking for +sip.instance and no Supported: outbound, the case the new init_outbound_setting() guard protects.

@laconicman

Copy link
Copy Markdown
Contributor Author

Thanks for the review, and for checking the registrars yourself.

Standards. I read the §6 parenthetical as describing what a registrar that implements RFC 5627 does with that combination, not as a condition on sending it. The instance ID is defined independently of GRUU: §4.1 requires every UA to have one and to convey it as a UA characteristic, leaving it out only for privacy, and its registration in §11.7 describes it as the identifier of the UA instance registering the Contact, typically used to route a call to a specific device. RFC 5627 §5.1 has the registrar process any Contact that carries +sip.instance; Supported: gruu only decides whether the GRUUs are returned (§5.2), so a UA without GRUU support simply does not learn them. 3GPP TS 24.229 (V19.7.0, §5.1.1.2.1) requires a UE with an IMEI to send +sip.instance even when it supports neither GRUU nor multiple registrations, with reg-id only for the latter, and notes that this puts no additional requirement on the network. A registrar that does not know the parameter ignores it (RFC 3840 §6).

Registrars. Agreed on all four, and the replacement is the point of the change rather than a side effect. An iOS client cannot keep a connection open in the background: it registers when a VoIP push wakes it, from a new connection each time, often in a new process with a new Call-ID. Kamailio and Flexisip recognise that REGISTER as the same phone only by its instance ID; without one, every wake-up adds a binding and the next INVITE forks to dead legs until they expire. Devices that share an ID replacing each other is an application error, which is why only an ID the application configured goes beyond outbound, and why the docs now say it must be per device.

Opt-in. Setting rfc5626_instance_id is the opt-in: an application that leaves it empty sees no change. That is how the struct's other wire-facing strings work too: reg_contact_params, contact_params, reg_hdr_list are sent when set, and rfc5626_reg_id replaces the default the same way. For one that sets it, the change on upgrade is smaller than it looks. On TCP/TLS, the usual transport for mobile clients, every release up to 2.17 kept sending ;reg-id=1;+sip.instance=… on refreshes even when the registrar never confirmed outbound, because update_rfc5626_status() never pushed the new Contact to the regc (#5159). #5281 drops both on the first refresh, and has not shipped yet. With this PR such an application keeps sending what 2.17 sent, minus reg-id. What is new is UDP and use_rfc5626 disabled, where the parameter is either ignored or used as above. A separate flag would leave every application that already sets an instance ID with the #5281 drop, the change noted in that PR's review, until it finds a second switch.

Docs. Done in 5f17bff: next to rfc5626_instance_id and sipOutboundInstanceId, 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).

Minor.

  • The need_outbound comment now says it appends reg-id (5f17bff).
  • Added uas-register-instance-nooutbound (d08ee19): TCP, outbound disabled, instance ID set; the REGISTER must carry +sip.instance, and neither reg-id nor outbound in Supported. The pjsua app had no way to turn outbound off, so it gets --outb-disable. It covers that row on the wire, not the guard: it also passes with the guard removed. Looking into why, pjsua_regc_init() starts with destroy_regc(), which clears acc->contact and resets rfc5626_status, so update_regc_contact() always runs before Supported is built and the path you describe is not reachable today. I kept the guard as defence in depth: it keeps the status honest for such an account before its first registration, and stops relying on that ordering.

@nanangizz nanangizz 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.

Thanks for the thorough reply. You're right on the guard: pjsua_regc_init() calls destroy_regc() first, so the Contact rebuild always runs before Supported is built. My "needed" was wrong; keeping it as defence in depth is fine.

On the standards point, I agree the §6 parenthetical isn't a condition on sending, and RFC 5627 §5.1 processes any Contact with +sip.instance regardless of Supported: gruu. One nuance: the §4.1 MUST is scoped to UAs implementing outbound, but the tag is a general RFC 3840 UA characteristic reused by GRUU and RFC 7255, so a configured ID without reg-id is well-defined.

I also take the compatibility point: 2.17 sent reg-id plus +sip.instance on every TCP/TLS refresh (#5159), so for those deployments this PR is closer to the last release than master is. Agreed on no extra flag; the configured ID is the opt-in, and the docs now state the binding semantics.

Verified here on Linux, merged with current master: the three new scenarios plus uas-register-outbound-norequire and uas-auth pass 3/3, regc_test and pjsua_acc_test pass, and scratch copies of the new scenarios with the feature removed fail as expected.

@nanangizz
nanangizz requested a review from sauwming October 8, 2026 10:41
@sauwming

sauwming commented Oct 8, 2026

Copy link
Copy Markdown
Member

It still has conflicts though

@sauwming sauwming 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.

Okay, just need to resolve the conflicts

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.
@laconicman
laconicman force-pushed the feat/instance-id-without-outbound branch from d08ee19 to 40e1a1a Compare October 8, 2026 17:24
@laconicman

Copy link
Copy Markdown
Contributor Author

Rebased onto master to resolve the conflict with #5311, which reshaped update_regc_contact(). The change is the same, re-applied inside the new shape: only the context of two commits moved, the other three are identical. One commit added: the Teluu copyright year in the five modified pjsua app files, per #5309. On the rebased build the six scenarios, the three negative controls, regc_test and pjsua_acc_test pass, and the Contact on the wire is unchanged for every case in the description.

Thanks also for the §4.1 nuance: agreed, that MUST is about UAs doing outbound; what makes a configured ID without reg-id well defined is the tag itself.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants