Compare commits

...
Author SHA1 Message Date
587b06b583
ims-pdn-up: connect the ims bearer by profile index, not by APN string
All checks were successful
package / package (push) Successful in 1m33s
On carriers whose attach PDN is granted IPv4-only (Odido, Swisscom, O2 UK
so far) the modem refuses every AP-side IPv6 bearer request made by APN
string with pdn-ipv6-call-disallowed before it reaches the air, so the
ims PDN never came up and imsd looped. A request made by 3GPP profile
index is treated as the IMS-class call the modem's own engine makes and
is exempt: it goes on the air as IPv6 and the network grants it. That is
the path 81voltd uses.

Look the ims profile up by APN in the modem's profile list and create the
bearer with profile-id=<index>,ip-type=<type>; fall back to the APN string
when no such profile exists. A profile-indexed bearer reports no APN, so
the reuse match accepts the profile id too. New knobs in /etc/imsd.env:
IMS_APN (default ims) and IMS_PROFILE_ID (default: looked up; "none"
forces the APN-string path).

Verified on KPN with the wall induced by a forced IPv4 attach: the
APN-string request is refused, the profile-indexed request connects, a
second run reuses the bearer, imsd registers over it; attach and mobile
data untouched.
2026-09-17 13:45:19 +02:00
dce941e082 imsd 0.3.4
0.3.3 plus: our requests are signed with the registered public identity
instead of the IMSI-derived temporary IMPU (the caller-ID leak a Telia
P-CSCF exposed), the identity survives a warm resume, and DUMP_SIP also
keeps the last outgoing INVITE.
2026-09-08 20:38:14 +02:00
7978b94502 messages: sign our requests with the public identity, not the IMSI IMPU
Every request we originated — INVITE, CANCEL, both ACKs, in-dialog BYE and
friends — put the IMSI-derived temporary IMPU in From. 3GPP allows that
identity in REGISTER only; the same lesson was learned for the reg-event
SUBSCRIBE (480) and never carried to calls. Most P-CSCFs overwrite From
and hid it; a Telia node did not, and a reporter's IMSI appeared on the
callee's screen (field report 2026-09-01). A strict P-CSCF may reject the
INVITE outright.

CallerId(): the registered sip: public identity (P-Associated-URI), else
the tel: one, and the temporary IMPU only before either is learned. One
helper feeds all five builders, so a dialog's From never drifts. As UAS the
dialog's local URI is the INVITE's To (RFC 3261 12.2.1.1), stored on the
Dialog, so an incoming call's BYE is signed the way the network addressed
us. The identity is persisted in the state file and restored on warm
resume, so a call placed before the refresh 200 re-learns it cannot fall
back. DUMP_SIP now also writes the last outgoing INVITE
(imsd-invite-out.raw): the one request a field log could never show.

The byte-pinned INVITE fixture moves to the tel: identity its test context
knows; new scenarios cover the sip: identity, the tel: fallback, the
pre-learning case and the UAS BYE.

Bench-verified on KPN 2026-09-08: outgoing INVITE From is the registered
sip: identity, call accepted and carried; caller ID at the far end
unchanged.
2026-09-08 20:37:47 +02:00
7 changed files with 135 additions and 20 deletions

View file

@ -164,7 +164,8 @@ PCSCF=2001:db8::105
| `EMERGENCY_NUMBERS` | *(empty)* | comma-separated additions to the builtin 112/911 emergency numbers (e.g. a private test core's short code). SIM `EF_ECC` is not read yet |
| `RTP_PORT` | `50004` | local RTP port advertised in SDP |
| `PRECOND` | `0` | `1` offers SDP QoS preconditions |
| `DUMP_SIP` | `0` | `1` writes raw REGISTER-200/SUBSCRIBE-200/INVITE dumps (mode 0600) for debugging — they contain your IMSI/MSISDN and addresses |
| `CODECS` | *(empty — defaults)* | comma-separated codec preference list over `AMR-WB`, `AMR` (or `AMR-NB`), `PCMA`, `PCMU`: restricts and orders both the codecs offered on an outgoing call and those accepted from an inbound offer (default: offer AMR-WB + AMR, accept all four in that order). A bench knob — a network whose gateway transcodes every caller up to AMR-WB otherwise never lets the narrowband path run |
| `DUMP_SIP` | `0` | `1` writes raw REGISTER-200/SUBSCRIBE-200 dumps, the last inbound INVITE (`imsd-invite-in.raw`) and the last outgoing INVITE (`imsd-invite-out.raw`), mode 0600, for debugging — they contain your IMSI/MSISDN and addresses |
| `DUMP_DIR`, `OUT_DIR` | state dir | where dumps / per-call media stats land |
| `IMSD_MEDIA` | *(auto)* | path to `imsd-media` (default: next to `imsd`, else `/usr/libexec/imsd-media`) |

View file

@ -41,7 +41,7 @@ import Imsd;
namespace {
// ---- static config (env-overridable, same knobs as imsd.py) ---------------
constexpr const char* Version = "0.3.3";
constexpr const char* Version = "0.3.4";
constexpr const char* BusName = "net.catcrafts.IMS1";
constexpr const char* ObjPath = "/net/catcrafts/IMS1";
constexpr const char* Iface = "net.catcrafts.IMS1";
@ -411,12 +411,12 @@ std::string StateDir() {
// tiny JSON reader/writer for the persisted registration context.
std::string StatePath() { return EnvOr("STATE_FILE", std::format("{}/imsreg.state", StateDir())); }
void PersistState(const RegState& r, const std::string& route, const std::string& ppi) {
void PersistState(const RegState& r, const std::string& route, const std::string& ppi, const std::string& aor) {
std::string j = std::format(
"{{\"callid\": \"{}\", \"ftag\": \"{}\", \"cseq\": {}, \"spi_uc\": {}, "
"\"spi_us\": {}, \"expiry\": {}, \"route\": \"{}\", \"ppi\": \"{}\", "
"\"contact_user\": \"{}\"}}",
r.callid, r.ftag, r.cseq, r.spiUc, r.spiUs, r.expiry, route, ppi,
"\"aor\": \"{}\", \"contact_user\": \"{}\"}}",
r.callid, r.ftag, r.cseq, r.spiUc, r.spiUs, r.expiry, route, ppi, aor,
r.contactUser);
std::string path = StatePath();
std::error_code ec;
@ -441,7 +441,7 @@ void DumpRaw(std::string_view name, std::string_view msg) {
}
struct PersistedState {
std::optional<std::string> callid, ftag, route, ppi, contactUser;
std::optional<std::string> callid, ftag, route, ppi, aor, contactUser;
std::optional<long> cseq, spiUc, spiUs, expiry;
};
std::optional<PersistedState> LoadState() {
@ -456,6 +456,7 @@ std::optional<PersistedState> LoadState() {
ps.ftag = QuotedAfter(s, "\"ftag\": \"");
ps.route = QuotedAfter(s, "\"route\": \"");
ps.ppi = QuotedAfter(s, "\"ppi\": \"");
ps.aor = QuotedAfter(s, "\"aor\": \"");
ps.contactUser = QuotedAfter(s, "\"contact_user\": \"");
ps.cseq = IntAfter(s, "\"cseq\": ");
ps.spiUc = IntAfter(s, "\"spi_uc\": ");
@ -809,6 +810,10 @@ private:
// is re-learned from the next 200's P-Associated-URI.
ppi_ = ps->ppi.value_or("");
ss_ = sa->securityServer;
// The public identity our requests are signed with (From) — restored
// here so a call placed before the refresh 200 re-learns it does not
// fall back to the barred temporary IMPU.
ctx_.aor = ps->aor.value_or("");
ctx_.route = route_; ctx_.ppi = ppi_; ctx_.securityServer = ss_;
ctx_.portUc = PortUc; ctx_.portUs = PortUs;
Log(std::format("RESUME via existing SA (cseq {})", ps->cseq.value_or(1)));
@ -834,7 +839,7 @@ private:
if (auto e = imsd::sip::GrantedExpires(okMsg)) reg_.expiry = *e;
UpdateRouteAndPpi(okMsg);
LogBindings(okMsg);
PersistState(reg_, route_, ppi_);
PersistState(reg_, route_, ppi_, ctx_.aor);
Log("REGISTERED (resumed + true-refreshed)");
return true;
}
@ -933,7 +938,7 @@ private:
LogBindings(ok);
if (route_.empty()) route_ = std::format("<sip:{};lr>", imsd::util::HostPort(pcscf_, portPs));
ctx_.route = route_; ctx_.ppi = ppi_;
PersistState(reg_, route_, ppi_);
PersistState(reg_, route_, ppi_, ctx_.aor);
Log("REGISTERED (fresh)");
}
@ -1051,7 +1056,7 @@ private:
auto [msg, used] = Reregister(reg_.callid, reg_.ftag, reg_.cseq + 1, reg_.spiUc, reg_.spiUs);
reg_.cseq = used;
if (auto e = imsd::sip::GrantedExpires(msg)) reg_.expiry = *e;
PersistState(reg_, route_, ppi_);
PersistState(reg_, route_, ppi_, ctx_.aor);
Log(std::format("keepalive re-REGISTER ok (cseq {})", used));
SubscribeRegEvent();
if (!registered_) { registered_ = true; EmitStatus(true); }
@ -1087,7 +1092,7 @@ private:
if (auto e = imsd::sip::GrantedExpires(msg)) reg_.expiry = *e;
UpdateRouteAndPpi(msg);
LogBindings(msg);
PersistState(reg_, route_, ppi_);
PersistState(reg_, route_, ppi_, ctx_.aor);
Log("reconnected + re-registered");
SubscribeRegEvent();
if (!registered_) { registered_ = true; EmitStatus(true); }
@ -1242,6 +1247,9 @@ private:
for (const auto& a : actions) {
switch (a.type) {
case T::SendClient:
// the outgoing INVITE is the one request a field log could
// never show (DUMP_SIP dumped inbound messages only)
if (a.text.starts_with("INVITE ")) DumpRaw("imsd-invite-out.raw", a.text);
if (!sip_.Send(a.text)) { sendOk = false; Log("client send failed"); }
break;
case T::SendResponse:

View file

@ -120,6 +120,7 @@ export namespace imsd::engine {
d_.itag = rng_.Token(10); // the To tag we mint = our dialog tag
d_.invBranch = rng_.Token(20); // unused as UAS; kept initialized
if (auto from = imsd::sip::Header(invite_, "From")) d_.dialogTo = std::string(*from);
if (auto to = imsd::sip::Header(invite_, "To")) d_.localUri = AngleUri(*to).value_or("");
if (auto ct = imsd::sip::Header(invite_, "Contact")) d_.remoteTarget = AngleUri(*ct);
d_.ruri = d_.remoteTarget.value_or("");
// The UAS route set is the Record-Route in RECEIVED order

View file

@ -290,8 +290,28 @@ export namespace imsd::msg {
std::optional<std::string> dialogTo; // remote To/From incl. tag, once known
std::vector<std::string> recordRoute; // route set, UAC order (see DialogRoute)
std::optional<std::string> remoteTarget; // remote Contact URI
std::string localUri; // our URI in this dialog: the UAS takes the
// INVITE's To URI; empty = CallerId(c) (UAC)
};
// The identity we sign our own requests with: the registered public
// identity (P-Associated-URI sip: entry), else the tel: one, and the
// IMSI-derived temporary IMPU only before either is learned. 3GPP allows
// the temporary IMPU in REGISTER alone; a P-CSCF that does not overwrite
// From shows it to the callee (Telia, 2026-09-01: the reporter's IMSI on
// the far phone) and a strict one may reject the request.
inline std::string_view CallerId(const Context& c) {
if (!c.aor.empty()) return c.aor;
if (!c.ppi.empty()) return c.ppi;
return c.id.impu;
}
// From URI of a request inside a dialog (RFC 3261 12.2.1.1: the dialog's
// local URI — as UAS that is the INVITE's To, as UAC our caller identity).
inline std::string_view FromUri(const Context& c, const Dialog& d) {
return d.localUri.empty() ? CallerId(c) : std::string_view(d.localUri);
}
// Dialog route set: the reversed Record-Route, or the registration Route
// when none was recorded (imsd._dialog_route).
inline std::string DialogRoute(const Context& c, const Dialog& d) {
@ -314,7 +334,7 @@ export namespace imsd::msg {
std::format("Via: SIP/2.0/TCP {};branch=z9hG4bK{};rport", imsd::util::HostPort(c.local, c.portUs), d.invBranch),
"Max-Forwards: 70",
std::format("Route: {}", c.route),
std::format("From: <{}>;tag={}", c.id.impu, d.itag),
std::format("From: <{}>;tag={}", FromUri(c, d), d.itag),
std::format("To: <{}>", d.ruri),
std::format("Call-ID: {}", d.callid),
"CSeq: 1 INVITE",
@ -347,7 +367,7 @@ export namespace imsd::msg {
ViaLine(c, "TCP", c.portUs, viaBranch),
"Max-Forwards: 70",
std::format("Route: {}", DialogRoute(c, d)),
std::format("From: <{}>;tag={}", c.id.impu, d.itag),
std::format("From: <{}>;tag={}", FromUri(c, d), d.itag),
std::format("To: {}", to),
std::format("Call-ID: {}", d.callid),
std::format("CSeq: {} {}", cseq, method),
@ -374,7 +394,7 @@ export namespace imsd::msg {
ViaLine(c, "TCP", c.portUs, viaBranch),
"Max-Forwards: 70",
std::format("Route: {}", DialogRoute(c, d)),
std::format("From: <{}>;tag={}", c.id.impu, d.itag),
std::format("From: <{}>;tag={}", FromUri(c, d), d.itag),
std::format("To: {}", to),
std::format("Call-ID: {}", d.callid),
"CSeq: 1 ACK",
@ -391,7 +411,7 @@ export namespace imsd::msg {
std::format("Via: SIP/2.0/TCP {};branch=z9hG4bK{};rport", imsd::util::HostPort(c.local, c.portUs), d.invBranch),
"Max-Forwards: 70",
std::format("Route: {}", c.route),
std::format("From: <{}>;tag={}", c.id.impu, d.itag),
std::format("From: <{}>;tag={}", FromUri(c, d), d.itag),
std::format("To: {}", toHeader),
std::format("Call-ID: {}", d.callid),
"CSeq: 1 ACK",
@ -407,7 +427,7 @@ export namespace imsd::msg {
std::format("Via: SIP/2.0/TCP {};branch=z9hG4bK{};rport", imsd::util::HostPort(c.local, c.portUs), d.invBranch),
"Max-Forwards: 70",
std::format("Route: {}", c.route),
std::format("From: <{}>;tag={}", c.id.impu, d.itag),
std::format("From: <{}>;tag={}", FromUri(c, d), d.itag),
std::format("To: <{}>", d.ruri),
std::format("Call-ID: {}", d.callid),
"CSeq: 1 CANCEL",

View file

@ -11,7 +11,14 @@
#
# Configuration, via the unit's EnvironmentFile /etc/imsd.env (also read
# directly so manual runs behave the same):
# IMS_APN ims APN name (default ims)
# IMS_IP_TYPE bearer ip-type (default ipv6)
# IMS_PROFILE_ID 3GPP profile index to connect through (default: looked up
# by APN in the modem's profile list; empty/none = connect by
# APN string). A profile-indexed call is exempt from the
# modem's attach-family forcing that refuses an APN-string
# IPv6 request when the attach PDN was granted IPv4-only
# (journal/ims 2026-09-16/17 bench: Odido, O2 UK, Swisscom).
# IMS_REG_TIMEOUT max seconds to wait for network registration before
# connect attempts start counting anyway (default 300)
@ -34,6 +41,9 @@ envval() { # $1 = key — for manual runs; under systemd the vars are inherited
}
IP_TYPE=${IMS_IP_TYPE:-$(envval IMS_IP_TYPE)}
IP_TYPE=${IP_TYPE:-ipv6}
IMS_APN=${IMS_APN:-$(envval IMS_APN)}
IMS_APN=${IMS_APN:-ims}
PROFILE_ID=${IMS_PROFILE_ID:-$(envval IMS_PROFILE_ID)}
REG_TIMEOUT=${IMS_REG_TIMEOUT:-$(envval IMS_REG_TIMEOUT)}
REG_TIMEOUT=${REG_TIMEOUT:-300}
@ -48,11 +58,30 @@ while :; do
done
log "modem $MODEM"
# ---- the ims profile index (WDS profile list), unless configured
ims_profile_index() { # $1 = apn
qmicli -d qrtr://0 --wds-get-profile-list=3gpp 2>/dev/null | awk -v apn="$1" '
/^[ \t]*\[[0-9]+\] 3gpp/ { idx = $1; gsub(/[^0-9]/, "", idx) }
/APN:/ { a = $0; sub(/.*APN: '"'"'/, "", a); sub(/'"'"'.*/, "", a);
if (tolower(a) == tolower(apn) && idx != "") { print idx; exit } }'
}
if [ -z "$PROFILE_ID" ]; then
PROFILE_ID=$(ims_profile_index "$IMS_APN")
[ -n "$PROFILE_ID" ] && log "ims profile: index $PROFILE_ID (apn $IMS_APN)" || log "ims profile: none for apn $IMS_APN, connecting by APN string"
elif [ "$PROFILE_ID" = none ]; then
PROFILE_ID=
fi
# ---- find a connected ims bearer; else find-or-create one and connect it
find_ims_bearer() { # $1 = required bearer.status.connected value
for B in $(bearer_paths); do
INFO=$(kv -b "$B") || continue
echo "$INFO" | grep -q '^bearer\.properties\.apn *: *ims$' || continue
if [ -n "$PROFILE_ID" ]; then
echo "$INFO" | grep -q "^bearer\.properties\.profile-id *: *$PROFILE_ID\$" ||
echo "$INFO" | grep -q "^bearer\.properties\.apn *: *$IMS_APN\$" || continue
else
echo "$INFO" | grep -q "^bearer\.properties\.apn *: *$IMS_APN\$" || continue
fi
echo "$INFO" | grep -q "^bearer\.properties\.ip-type *: *$IP_TYPE\$" || continue
echo "$INFO" | grep -q "^bearer\.status\.connected *: *$1\$" || continue
echo "$B"
@ -90,10 +119,15 @@ while [ -z "$BEARER" ]; do
[ "$n" -gt 10 ] && { log "bearer connect failed after 10 attempts"; exit 1; }
B=$(find_ims_bearer no) # reuse a stale disconnected ims bearer
if [ -z "$B" ]; then
OUT=$(mmcli -m "$MODEM" --create-bearer="apn=ims,ip-type=$IP_TYPE" 2>&1)
if [ -n "$PROFILE_ID" ]; then
SPEC="profile-id=$PROFILE_ID,ip-type=$IP_TYPE"
else
SPEC="apn=$IMS_APN,ip-type=$IP_TYPE"
fi
OUT=$(mmcli -m "$MODEM" --create-bearer="$SPEC" 2>&1)
B=$(printf '%s' "$OUT" | sed -n 's,.*\(/org/freedesktop/ModemManager1/Bearer/[0-9]*\).*,\1,p')
if [ -n "$B" ]; then
log "created bearer $B (ip-type=$IP_TYPE)"
log "created bearer $B ($SPEC)"
else
log "create-bearer attempt $n failed: $(squash "$OUT"); retrying in 10 s"
sleep 10

View file

@ -164,6 +164,8 @@ int main() {
CallMachine m(c, rng, "ims-call-1", "1233", 50004, false);
auto start = m.Start();
Check(Sent(start, "INVITE ") != nullptr, "Start sends INVITE");
Check(Sent(start, "INVITE ")->text.contains("\r\nFrom: <tel:+31611111111>;tag="), "MO INVITE From is our public (tel:) identity, not the IMSI IMPU");
Check(!Sent(start, "INVITE ")->text.contains("From: <sip:001010123456789@"), "MO INVITE From never the temporary IMPU once an identity is known");
Check(Has(start, Action::Type::SetDeadline), "Start arms ring timeout");
Check(m.State() == CallState::Dialing, "starts dialing");
@ -610,7 +612,8 @@ int main() {
Check(bye && bye->text.starts_with("BYE sip:origgw@[2001:db8::9]:5060 SIP/2.0"), "BYE targets the caller's Contact");
Check(bye && bye->text.contains("CSeq: 1 BYE\r\n"), "our UAS CSeq space starts at 1");
Check(bye && bye->text.contains("To: <sip:+31612345678@ims.mnc001.mcc001.3gppnetwork.org;user=phone>" ";tag=caller1\r\n"), "BYE To is the caller (their tag)");
Check(bye && bye->text.contains("From: <sip:001010123456789@ims.mnc001.mcc001." "3gppnetwork.org>;tag="), "BYE From is us (our tag)");
Check(bye && bye->text.contains("From: <sip:+31611111111@ims.mnc001.mcc001.3gppnetwork.org;user=phone>;tag="), "UAS BYE From is the INVITE's To URI (our tag), not the IMSI IMPU");
Check(bye && !bye->text.contains("001010123456789@ims"), "UAS BYE carries no IMSI-derived identity");
// UAS route set: Record-Route in RECEIVED order
Check(bye && bye->text.contains("Route: <sip:[2001:db8::105]:5100;lr>, " "<sip:[2001:db8::9]:5060;lr>\r\n"), "BYE routes via Record-Route in received order");
const Action* t = Find(ah, Action::Type::State);
@ -828,6 +831,22 @@ int main() {
Check(inv && inv->text.contains("a=rtpmap:100 telephone-event/8000\r\n") && !inv->text.contains("telephone-event/16000"), "CODECS=PCMA,PCMU: narrowband DTMF only");
}
// ---- MO with the registered sip: identity learned (the normal case on a
// live network): From, CANCEL and BYE all carry it, never the IMSI
{
Context ca = MakeCtx();
ca.aor = "sip:+31611111111@ims.mnc001.mcc001.3gppnetwork.org";
imsd::util::Rng rng(61);
CallMachine m(ca, rng, "ims-call-30", "1233", 50004, false);
auto start = m.Start();
const Action* inv = Sent(start, "INVITE ");
Check(inv && inv->text.contains("\r\nFrom: <sip:+31611111111@ims.mnc001.mcc001.3gppnetwork.org>;tag="), "MO INVITE From is the registered sip: identity");
auto cancel = m.OnHangup();
const Action* can = Sent(cancel, "CANCEL ");
Check(can && can->text.contains("\r\nFrom: <sip:+31611111111@ims.mnc001.mcc001.3gppnetwork.org>;tag="), "CANCEL From is the same identity");
Check(can && !can->text.contains("001010123456789@ims"), "CANCEL carries no IMSI");
}
if (Failures == 0) std::println("Engine: all tests passed");
return Failures;
}

View file

@ -117,7 +117,7 @@ namespace {
"Via: SIP/2.0/TCP [2001:db8::db43]:45062;branch=z9hG4bKINVBRANCH0000000000;rport\r\n"
"Max-Forwards: 70\r\n"
"Route: <sip:[2001:db8::105]:6000;lr>\r\n"
"From: <sip:001010123456789@ims.mnc001.mcc001.3gppnetwork.org>;tag=ITAG123456\r\n"
"From: <tel:+31611111111>;tag=ITAG123456\r\n"
"To: <sip:1233;phone-context=ims.mnc001.mcc001.3gppnetwork.org@ims.mnc001.mcc001.3gppnetwork.org;user=phone>\r\n"
"Call-ID: CALLID789@2001:db8::db43\r\n"
"CSeq: 1 INVITE\r\n"
@ -230,6 +230,38 @@ int main() {
Eq(BuildInvite(c, d, Sdp, /*precond=*/false), Invite, "INVITE + AMR-WB SDP offer");
}
// ---- From carries our PUBLIC identity, never the IMSI-derived temporary
// IMPU once anything better is known (ledger F1: a Telia P-CSCF showed
// the reporter's IMSI to the callee). aor > tel: ppi > temp IMPU; the
// same URI on every request of the dialog; a UAS signs with the
// INVITE's To.
{
Dialog d;
d.ruri = RuriFor("1233", c.id.domain);
d.callid = "CID@x"; d.itag = "it"; d.invBranch = "ib";
Context c2 = MakeCtx();
c2.aor = "sip:+31611111111@ims.mnc001.mcc001.3gppnetwork.org";
std::string inv = BuildInvite(c2, d, "v=0\r\n", false);
Check(inv.contains("\r\nFrom: <sip:+31611111111@ims.mnc001.mcc001.3gppnetwork.org>;tag=it\r\n"), "INVITE From is the registered sip: identity when known");
Check(!inv.contains("001010123456789@ims"), "INVITE never carries the IMSI IMPU once the aor is known");
Check(inv.contains("\r\nP-Preferred-Identity: <tel:+31611111111>\r\n"), "P-Preferred-Identity unchanged");
Check(BuildCancel(c2, d).contains("\r\nFrom: <sip:+31611111111@ims.mnc001.mcc001.3gppnetwork.org>;tag=it\r\n"), "CANCEL From follows");
Check(BuildAckNon2xx(c2, d, "<sip:x@y>;tag=r").contains("\r\nFrom: <sip:+31611111111@ims.mnc001.mcc001.3gppnetwork.org>;tag=it\r\n"), "non-2xx ACK From follows");
d.dialogTo = "<sip:x@y>;tag=r"; d.remoteTarget = "sip:x@[2001:db8::9]:5060";
Check(BuildAck2xx(c2, d, "ab", "<sip:x@y>;tag=r").contains("\r\nFrom: <sip:+31611111111@ims.mnc001.mcc001.3gppnetwork.org>;tag=it\r\n"), "2xx ACK From follows");
Check(BuildInDialog(c2, d, "BYE", 2, "bb").contains("\r\nFrom: <sip:+31611111111@ims.mnc001.mcc001.3gppnetwork.org>;tag=it\r\n"), "BYE From follows");
// only tel: known (pre-aor): the tel: identity, as the pinned INVITE shows
Check(BuildInvite(c, d, "v=0\r\n", false).contains("\r\nFrom: <tel:+31611111111>;tag=it\r\n"), "INVITE From falls back to the tel: identity");
// nothing learned yet: the temporary IMPU is all there is
Context c4 = MakeCtx();
c4.ppi.clear();
Check(BuildInvite(c4, d, "v=0\r\n", false).contains("\r\nFrom: <sip:001010123456789@ims.mnc001.mcc001.3gppnetwork.org>;tag=it\r\n"), "INVITE From is the temporary IMPU only before any identity is learned");
// UAS: the dialog's local URI is the INVITE's To, whatever we registered as
Dialog du = d;
du.localUri = "sip:+31611111111@ims.mnc001.mcc001.3gppnetwork.org;user=phone";
Check(BuildInDialog(c4, du, "BYE", 1, "cc").contains("\r\nFrom: <sip:+31611111111@ims.mnc001.mcc001.3gppnetwork.org;user=phone>;tag=it\r\n"), "UAS BYE From is the INVITE's To URI");
}
// ---- emergency INVITE: byte-identical to the plain INVITE except the
// Request-URI and To carry the service URN (RFC 5031 / TS 24.229 5.1.6)
{