packaging, ims-pdn-up, imsd, README: imsd runs without /etc/imsd.env and idles without a SIM
With the P-CSCF coming from the PDN's PCO, the configuration file has no required line left, so the apk's ConditionPathExists=/etc/imsd.env drop-in goes: the unit starts on every phone with the package, and VoLTE is on by default. Installing the package and inserting a SIM is the opt-in; the file holds overrides only, and its existence changes nothing. By the versioning rule (AGENTS §11, the first commit of this series) that is what moves the major number: the release is 1.0.0, and §11 now records it. What used to be the file's job — keeping a phone nobody configured out of a failing unit — is now a rule on facts, the same for every phone: a modem in the failed state with a SIM reason makes the script write IMSD_IDLE=no-sim to /run/imsd.env and exit 0; a profile-indexed connect series the network refuses ten times in a row — each a persistent 3GPP cause in ModemManager's typed error (not subscribed, unknown APN, barred, not authorised in the PLMN, authentication failed, the service option not supported, the wrong PDN type) with packet service at the time; counted only while the modem reads registered, connecting or connected with packet service not detached — stricter than the daemon's PacketServiceUp; any other read, a timeout or a transient cause starts the count again — writes IMSD_IDLE=no-ims-pdn; and a bring-up that ends with no P-CSCF — none configured, none discovered: no IPv6 P-CSCF read from the PCO (PCSCF_DISCOVERY=none in the hand-off, the reader's exit 2: the list absent, empty, unreadable or IPv4/FQDN only), discovery skipped (no rmnet mux id) or no hand-off at all (a manual run) — and no registration of imsd's own to resume is the daemon's own idle, decided before it takes the bus name (SaPeer and the holds' old_gone say what can be resumed; a pair the holds wrote off is taken out of the kernel there) and at the no-target bring-up, where a pair nothing can resume is written off and taken out of the kernel first. imsd reads the key, logs idle: … and exits 0, which Restart=on-failure leaves alone, so the unit stays inactive until the next boot, a systemctl start or a start of imsd-ofonod where that runs (its Wants starts an idle imsd again). What is not a fact keeps the retry: no coverage, a SIM still waiting for its PIN, a modem without an ims profile (the APN-string connect is tried), an APN-string series (the modem's own wall), a resumable pair whose attempt failed (the fatal), and a P-CSCF reader that did not answer (PCSCF_DISCOVERY=failed: the daemon exits 1 and the restart runs discovery again). A phone that never had the file keeps its stopped unit through the upgrade and starts at its next boot, registering where the rule above lets it — at the upgrade itself where imsd-ofonod runs, whose restart pulls imsd in through its Wants (systemd queues the marked unit's try-restart, collapses it to a restart and starts what the unit Wants in the same transaction: job_type_collapse, transaction_add_job_and_dependencies, 262~rc3); the opt-out is systemctl mask --now imsd, since a plain disable is undone by that Wants. imsd-ofonod.service loses its After=imsd.service: with imsd starting on every phone, GNOME Calls' backend would otherwise wait behind the ims PDN bring-up at every boot and at its marked restart at an upgrade, for the whole bring-up (up to its TimeoutStartSec=900); it starts at once and republishes when imsd's name appears (OnImsdAppeared). The configuration file's path is the IMSD_ENV seam the mock bench uses. AGENTS §6's lint figures are this tree's (109 findings in 16 of 20 files, 2026-10-13; the 72 of 2026-10-06 predate the Qmi files). Tested: crafter-build test, 8 passed; on the host build the daemon's idle exit for the script's keys and, with no pair, for every discovery verdict but failed (none, skipped, no hand-off) — exit 0 — and the exit 1 for failed; the script's arms on the mock bench; on the dev phone (KPN), in builds of this series: the daemon's idle at the no-target bring-up with the dead pair removed and before the bus name for a pair the holds had written off, the discovery-failed exit 1 and its restart, the start without the file, the pull-in by an imsd-ofonod restart and the mask holding against it and a reboot, and imsd-ofonod starting at once with imsd stopped and republishing when imsd's name appeared.
This commit is contained in:
parent
1d28063700
commit
7a3dcc8951
7 changed files with 476 additions and 149 deletions
351
AGENTS.md
351
AGENTS.md
|
|
@ -103,14 +103,23 @@ refusals and network silence, and the catch sites that sort a failed refresh
|
|||
(`FreshRecover`, `DeferBringUp`) into Refused, Silent or Throttled with the
|
||||
status, the Retry-After and `saReplaced_`, the bring-up resume's catch (a
|
||||
refusal's Retry-After fed as a hold, the old registration written off after
|
||||
a failed resume that a fresh registration follows — with no fresh target it
|
||||
is kept for the restart to resume, unless the engine's rule ends it — a 401, a refusal with a Retry-After, the
|
||||
second refusal across the restart (the count travels in the holds), a 403
|
||||
once fresh is due, a NOTIFY-terminated binding: the pair is taken out of the
|
||||
kernel like one nothing can resume), the `OnLink` read that writes the old registration off
|
||||
a failed resume that a fresh registration follows; with no fresh target only
|
||||
a pair nothing can resume is written off — a 403 once fresh is due or a
|
||||
binding a NOTIFY terminated under the attempt counts as one by the engine's
|
||||
rule, and so does a resume it gives up: a 401, a refusal with a Retry-After,
|
||||
the second refusal, the count travelling in the holds (`Reconnectable()` and
|
||||
`Fresh()`, decided once after both catch arms) — and taken out of the kernel,
|
||||
§7.2 step 1), the `OnLink` read that writes the old registration off
|
||||
when its address has left the netdev (`FreshRecover` after a failed move,
|
||||
`AddressGone`), and `PacketServiceUp`'s read of
|
||||
ModemManager before a silent cycle is counted;
|
||||
ModemManager before a silent cycle is counted; the no-target reads and
|
||||
reactions (1.0.0): the kernel text behind `SaPeer`, the holds' `old_gone`
|
||||
and the script's `PCSCF_DISCOVERY=` — whether a pair is resumable (`SaPeer`
|
||||
finds one and the holds' `old_gone` does not write it off: `!peer ||
|
||||
writtenOff` in `main()`) and the choice of the idle or the fatal are shell
|
||||
code — at `main()`'s idle before the bus name (a pair the holds wrote off
|
||||
taken out of the kernel on the way) and at the no-target bring-up's write-off
|
||||
of a dead pair, its removal and the idle or the fatal that follows (§7.3);
|
||||
and the xfrm deletes the SA setup and the dead pair's teardown run —
|
||||
`OwnXfrmDeleteCommands` builds them from the kernel text, with a test; the
|
||||
shell reads that text and runs them (§12);
|
||||
|
|
@ -373,37 +382,52 @@ Rules:
|
|||
(§10.1) ran through a fake `ip` (2026-10-05: the phone's listing of the
|
||||
netdev and two cut-down ones). The 1.0.0 arms no bench has reached: a
|
||||
fresh registration moving from the resumed SA peer to `PCSCF=` and back
|
||||
when the attempt fails before the SA setup (`FreshRecover`); the fresh-target tail of a state file another attempt left behind (its no-target tail ran on the tip's host build, bench 23f, with a file naming another SA pair; the check itself, `FileNamesAnotherPair`, is pinned in `tests/Ipsec`; the window itself is not staged on the phone); another transport-mode reqid 1 + 2 pair beside ours on other ports, and two
|
||||
addresses each carrying imsd's fingerprint (`tests/Ipsec` only — a VPN's
|
||||
tunnel policy and a transport reqid-1 policy beside the live pair, resumed
|
||||
and kept, and a lone one with
|
||||
when the attempt fails before the SA setup (`FreshRecover`); the
|
||||
fresh-target tail of a state file another attempt left behind (its no-target
|
||||
tail ran on the tip's host build, bench 23f, with a file naming another SA
|
||||
pair; the check itself, `FileNamesAnotherPair`, is pinned in `tests/Ipsec`;
|
||||
the window itself is not staged on the phone); another transport-mode reqid 1 + 2 pair
|
||||
beside ours on other ports, and two addresses each carrying imsd's
|
||||
fingerprint (`tests/Ipsec` only — a VPN's tunnel policy and a transport
|
||||
reqid-1 policy beside the live pair, resumed and kept, and a lone one with
|
||||
no pair of ours, none found, ran on the dev phone, 2026-10-11/12); the SA
|
||||
setup's and the dead pair's deletes with another transport-mode user's pair
|
||||
in the kernel (`tests/Ipsec` only — with a VPN's tunnel policy and tunnel
|
||||
state there, both left in place through a fresh registration and a dead
|
||||
pair's removal, they ran on the dev phone, 2026-10-12); a no-target resume
|
||||
the network refuses (the reducer's side of the count's travel, `Held` →
|
||||
`Restore`, is pinned in `tests/Engine`; the holds file's `refusals` key —
|
||||
`PersistHolds`/`LoadHolds`, shell code — and the shell's giving the pair up
|
||||
on the engine's verdict are unbenched); `FreshRecover`'s restart for want of
|
||||
a fresh target (a resumed no-target registration whose recovery needs a
|
||||
fresh one); an empty `Service-Route` value in the 200, ignored (KPN's 200
|
||||
carries none); a no-target resume whose refresh goes
|
||||
unanswered (the connect case ran on the tip's host build, bench 23f: the
|
||||
pair kept for the restart, `the restart tries the resume again`); `RESUME=0` at a no-target start (BringUp's removal of the untried
|
||||
pair); `RemoveDeadPair`'s
|
||||
no-peer line (`N xfrm entries of imsd's own with no pair to resume:
|
||||
removing them`); `RunOwnXfrmDeletes`' line for a delete that failed (`… -> rc
|
||||
N`: an entry gone between the read and the delete); the holds file's `pdn` compare in canonical form (no capture
|
||||
stages a holds file whose `pdn` is spelled unlike `LOCAL`); the discovery
|
||||
arms beyond the plain found one — the v4/FQDN `also gave` line, the
|
||||
eight-address cap, the IPv4-only and no-mux-id skips, the comma-list
|
||||
setup's and the dead pair's deletes with
|
||||
another transport-mode user's pair in the kernel (`tests/Ipsec` only — with
|
||||
a VPN's tunnel policy and tunnel state there, both left in place through a
|
||||
fresh registration and a dead pair's removal, they ran on the dev phone,
|
||||
2026-10-12); a no-target resume the network refuses (the reducer's side of
|
||||
the count's travel, `Held` → `Restore`, is pinned in `tests/Engine`; the
|
||||
holds file's `refusals` key — `PersistHolds`/`LoadHolds`, shell code — and
|
||||
the shell's giving the pair up on the engine's verdict are unbenched);
|
||||
`FreshRecover`'s restart for want of a fresh target (a resumed no-target
|
||||
registration whose recovery needs a fresh one); an empty `Service-Route`
|
||||
value in the 200, ignored (KPN's 200 carries none); a no-target resume
|
||||
whose refresh goes unanswered (the connect case ran on the tip's host
|
||||
build, bench 23f: the pair kept for the restart, `the restart tries the
|
||||
resume again`); `RESUME=0` at a no-target start
|
||||
(BringUp's removal of the untried pair — the same deletes ran before the
|
||||
bus name on the host's kernel, a holds write-off in front of imsd's pair,
|
||||
2026-10-13); `RemoveDeadPair`'s no-peer line (`N xfrm entries of imsd's own
|
||||
with no pair to resume: removing them`); `RunOwnXfrmDeletes`'
|
||||
line for a delete that failed (`… -> rc N`: an entry gone between the read
|
||||
and the delete); the holds file's `pdn` compare in canonical form (no
|
||||
capture stages a holds file whose `pdn` is spelled unlike `LOCAL`); the
|
||||
discovery arms beyond the plain found one — the v4/FQDN `also gave` line,
|
||||
the eight-address cap, the IPv4-only and no-mux-id skips, the comma-list
|
||||
`PCSCF=` refusal, the reader's rc-0 answer without an address — on the mock
|
||||
bench only (S13, S19–S22, S55): KPN's PCO carries
|
||||
two IPv6 addresses and nothing else, so on the real PCO the dev phone
|
||||
reached the found arm alone; the configured skip, the bracketed refusal
|
||||
and, through the `IMSD_PCSCF` seam, the reader's failure and its exit 2 ran
|
||||
on the dev phone too (2026-10-11/12).
|
||||
bench only (S13, S19–S22, S55): KPN's PCO carries two IPv6 addresses and nothing else, so on the
|
||||
real PCO the dev phone reached the found arm alone; the configured skip, the
|
||||
bracketed refusal and, through the `IMSD_PCSCF` seam, the reader's failure
|
||||
and its exit 2 ran on the dev phone too (2026-10-11/12); the script's idle arms (no SIM, a refused ims PDN) with the
|
||||
refusal count's reset by a coverage gap, a timeout and a transient cause,
|
||||
the no-coverage and locked-SIM retries and the APN-string connect of a
|
||||
modem without the profile, on the mock bench only (a SIM-out boot would
|
||||
reach the no-SIM idle alone; the others need a SIM-in run against a refusing
|
||||
network, an outage, a locked SIM, a modem without the profile) — the daemon's `idle:` exit for
|
||||
the script's keys ran on the host build (2026-10-12, exit 0), its own idle
|
||||
(nothing to register to), the discovery-failed exit 1, `main()`'s removal
|
||||
of a pair the holds wrote off and `imsd-ofonod` starting without waiting
|
||||
for imsd on the dev phone (2026-10-12).
|
||||
- `imsd-pcscf` ran on the dev phone (KPN) on 2026-10-11 against the live ims
|
||||
bearer: the `--mux 1 --profile 2 -v` sequence, a plain run, and `OutOfCall`
|
||||
on an unbound mux (the fp6 journal's `2026-10-11-pcscf-pco-probe` capture,
|
||||
|
|
@ -438,7 +462,10 @@ shared rule wants camelCase), then `format-concat`, `const-local`,
|
|||
`wrap-join`, `single-declaration` and `no-char-pointer` (`const-local` and
|
||||
`single-declaration` the copy did not report) — and the count moves (72 on
|
||||
2026-10-06, before the Qmi files; 108 in 16 of 20 files on the tree that
|
||||
added them, 2026-10-13). CI does not run lint.
|
||||
added them, 2026-10-13; 109 on the 1.0.0 tree, 2026-10-13: `naming` 69,
|
||||
`format-concat` 23, `const-local` 10, `wrap-join` 3, `single-declaration` 2,
|
||||
`no-char-pointer` 1 and `if-single-line` 1, the last new since). CI does not
|
||||
run lint.
|
||||
|
||||
Beyond the rules:
|
||||
|
||||
|
|
@ -495,8 +522,11 @@ through `/etc/skel/.config/autostart/org.kde.modem.daemon.desktop`);
|
|||
reads the P-CSCF list the network put in the PDN's PCO (`imsd-pcscf`;
|
||||
skipped when `PCSCF=` is set), gives the first a host route (§7.3) and
|
||||
writes `/run/imsd.env` with `DEV=` (the connected netdev), `LOCAL=` (the
|
||||
PDN address ModemManager assigned) and `PCSCF_DISCOVERED=` (the IPv6 addresses of that list, at most eight,
|
||||
comma-separated in the modem's order; IPv4 entries and names dropped).
|
||||
PDN address ModemManager assigned), `PCSCF_DISCOVERED=` (the IPv6 addresses of
|
||||
that list, at most eight, comma-separated in the modem's order; IPv4 entries
|
||||
and names dropped) and `PCSCF_DISCOVERY=` (how it came to be:
|
||||
found, none, failed or skipped, §7.3) — or `IMSD_IDLE=<reason>` alone when
|
||||
the bring-up found nothing to do (§7.3).
|
||||
2. `/usr/libexec/imsd-handover.sh` (`ExecStartPre=-`, failure-tolerant) hands
|
||||
a pre-0.3.10 daemon's throttle wait over (§7.5).
|
||||
|
||||
|
|
@ -515,20 +545,26 @@ The daemon's own bring-up (`BringUp()` in `main.cpp`):
|
|||
first address of `PCSCF_DISCOVERED=` (the bring-up's reading of the PDN's
|
||||
PCO, §7.3); the journal says which (`pcscf: configured …` / `pcscf:
|
||||
discovered … (using …)`). A resume needs neither: the kernel's SAs name
|
||||
its P-CSCF (step 3). With neither, a bring-up that did not resume fails
|
||||
with `no P-CSCF for a fresh registration: …`, and since no fresh one
|
||||
follows: a resumable pair whose attempt failed (a silent
|
||||
its P-CSCF (step 3). With neither, a bring-up that did not resume ends in
|
||||
the fatal `no P-CSCF for a fresh registration: …` and the restart's retry
|
||||
when the resume merely failed or the discovery did (`PCSCF_DISCOVERY=failed`,
|
||||
§7.3), else in the clean `idle: no P-CSCF …` exit (§7.3) — and since no
|
||||
fresh one follows, a resumable pair whose attempt failed (a silent
|
||||
connect, an unanswered refresh, a first refusal without a Retry-After —
|
||||
the refusal count goes to the holds) stays for the restart to resume, unless the engine's rule
|
||||
ends it — a 401, a refusal with a Retry-After or the second refusal
|
||||
(`RefusalsBeforeFresh`; the count travels in the holds, so the restart's
|
||||
refusal is the second) give the resume up, a 403 once a fresh registration
|
||||
is due or a binding a NOTIFY terminated under the attempt write the
|
||||
registration off (`old_gone`) — so the pair counts as one nothing can
|
||||
resume; a pair nothing can resume (its UE address left the link, no state file, a file another attempt left behind, written off before this start or by the engine during the attempt, `RESUME=0`) is taken out of the kernel first (`the SA pair to …
|
||||
is nothing to resume: removing it` — by exact key, what two rules select — the fingerprinted policies and the reqid-1/2 transport-mode ESP states in their direction or on the UE's ims-PDN address, §12), so the next bring-up finds nothing to resume; the holds
|
||||
keep the Retry-After of a refusal and, for a registration the engine wrote
|
||||
off, the write-off — a removed pair needs none.
|
||||
the refusal count goes to the holds) is not written off and stays for the
|
||||
restart to resume, unless the
|
||||
engine's rule ends it: a 401, a refusal with a Retry-After or the second
|
||||
refusal (`RefusalsBeforeFresh`; the count travels in the holds, so the
|
||||
restart's refusal is the second) give the resume up, a 403 once a fresh
|
||||
registration is due or a binding a NOTIFY terminated under the attempt
|
||||
write the registration off — either way the pair counts as one nothing
|
||||
can resume. A pair nothing can resume (its UE address left the link, no
|
||||
state file, a file another attempt left behind, written off before this start or
|
||||
given up by the engine during the attempt) is written off (`old_gone` in the holds — written as
|
||||
well for a pair `RESUME=0` leaves untried) and taken
|
||||
out of the kernel first (`the SA pair to … is nothing to resume: removing
|
||||
it` — by exact key, what two rules select — the fingerprinted policies and the reqid-1/2 transport-mode ESP states in their direction or on the UE's ims-PDN address, §12), so the next bring-up
|
||||
finds nothing to resume.
|
||||
2. `qmicli -d qrtr://0 --uim-get-card-status` finds the ready USIM (slot, AID);
|
||||
`mmcli -m any` gives the primary SIM path and the IMEI (for the
|
||||
`+sip.instance` URN); `mmcli -i <sim>` gives IMSI and MCC/MNC, from which
|
||||
|
|
@ -547,7 +583,7 @@ The daemon's own bring-up (`BringUp()` in `main.cpp`):
|
|||
terminated under the resume; a fresh hold kept from before this start);
|
||||
with no target every refusal reaches the reducer,
|
||||
§7.4; with none it is the fatal and the
|
||||
restart (step 1). The SA reader, the holds file's `pdn`
|
||||
restart, or the idle (step 1). The SA reader, the holds file's `pdn`
|
||||
key, `LOCAL` and `PCSCF` compare addresses in RFC 5952 §4 form
|
||||
(`imsd::util::CanonicalAddr`: lower-case hex, leading zeros dropped, the
|
||||
longest zero run compressed — what `ip xfrm` prints for a plain IPv6
|
||||
|
|
@ -578,10 +614,12 @@ The daemon's own bring-up (`BringUp()` in `main.cpp`):
|
|||
`spi_uc`/`spi_us` against the pair's (`FileNamesAnotherPair`), a 0.3.x
|
||||
file's too: an attempt that got through the SA setup and did not live to
|
||||
record, to this or another P-CSCF — is not used; the
|
||||
bring-up registers fresh (with no fresh target: the fatal, step 1). Every fresh registration goes to `PCSCF=`, else the first
|
||||
entry of `PCSCF_DISCOVERED=` that `CanonicalIp6` accepts; with neither, a bring-up that cannot resume fails
|
||||
with `no P-CSCF for a fresh registration: …` and a recovery step that
|
||||
needs one restarts the daemon through systemd.
|
||||
bring-up registers fresh (with no fresh target: the fatal or the idle, step 1). Every fresh registration goes to `PCSCF=`, else the first
|
||||
entry of `PCSCF_DISCOVERED=` that `CanonicalIp6` accepts; with neither, a bring-up that cannot resume ends as
|
||||
step 1 says (the fatal after a resumable pair's failed attempt or a failed
|
||||
discovery, else the clean `idle: no P-CSCF …` exit — a resume the engine
|
||||
gave up or wrote off included) and a recovery step that needs one
|
||||
restarts the daemon through systemd.
|
||||
4. **Fresh registration** otherwise (`FRESH registration`): the target's host
|
||||
route (imsd's own `ip -6 route replace`; step 3 says which target), an
|
||||
unprotected REGISTER on `PCSCF_PORT` (UDP, always), the 401 with the AKA challenge and
|
||||
|
|
@ -614,7 +652,7 @@ The daemon's own bring-up (`BringUp()` in `main.cpp`):
|
|||
(`Imsd:Ipsec`, which makes the choice): KPN's one-entry answer to the
|
||||
default offer in the same bytes, not a list such as O2's six, which RFC 3329 §2.3.1 has the server answer with a
|
||||
494 when it does not mirror its own; a refused resume registers fresh (with
|
||||
no fresh target: the fatal and the restart, step 1).
|
||||
no fresh target: the fatal and the restart, or the idle, step 1).
|
||||
A 401 to a
|
||||
reregistration asks for re-authentication over temporary SAs on the
|
||||
proposed ports, which imsd does not implement: it is a refusal and the
|
||||
|
|
@ -662,6 +700,59 @@ configures the muxed netdev. Idempotent; a non-zero exit fails the unit.
|
|||
- Waits up to 120 s for a modem, then gates the connect attempts on network
|
||||
registration (`IMS_REG_TIMEOUT`, default 300 s): some carriers attach ~2 min
|
||||
after boot.
|
||||
- Idles instead of retrying when there is nothing to do: a modem in the
|
||||
`failed` state with a SIM reason (`sim-missing`, `esim-without-profiles`,
|
||||
`sim-error`), checked once the modem appears and again while waiting for
|
||||
registration, writes `IMSD_IDLE=no-sim` to `/run/imsd.env` and exits 0; a
|
||||
profile-indexed connect series refused ten times in a row — each a
|
||||
persistent 3GPP cause in ModemManager's typed error
|
||||
(`ServiceOptionNotSubscribed`, `MissingOrUnknownApn`,
|
||||
`OperatorDeterminedBarring`, `ServiceOptionNotAuthorizedInPlmn`,
|
||||
`UserAuthenticationFailed`, `ServiceOptionNotSupported`, `Ipv4OnlyAllowed`,
|
||||
`Ipv6OnlyAllowed`) with packet service at the time — writes
|
||||
`IMSD_IDLE=no-ims-pdn` (the line carrying the last error). The script's own
|
||||
rule, stricter than the daemon's `PacketServiceUp`: a refusal counts only
|
||||
while the modem reads `registered`, `connecting` or `connected` with packet
|
||||
service not `detached`; any other read (`searching`, `disconnecting`,
|
||||
`unknown`, an unanswered `mmcli`), a D-Bus timeout, `esm-proc-timeout`, a
|
||||
transient cause (`InsufficientResources`, `ServiceOptionOutOfOrder`,
|
||||
`NetworkFailureActivation`) or any other error starts the count again, as a
|
||||
coverage gap does — an outage must not idle VoLTE until the next boot. Installing the package and inserting a
|
||||
SIM is the opt-in: `/etc/imsd.env` holds overrides only and its existence
|
||||
changes nothing here (before 1.0.0 the apk's drop-in made it the switch for
|
||||
the whole unit, §9.2). imsd reads the key, logs `idle: …` and exits 0;
|
||||
`Restart=on-failure` leaves a clean exit alone, so the unit stays inactive
|
||||
until the next boot, a `systemctl start imsd` or a start of `imsd-ofonod`,
|
||||
whose `Wants=imsd.service` starts an idle imsd again (the Phosh image: an
|
||||
`imsd-ofono` upgrade, its own restart after a crash) — one attempt per
|
||||
boot, or per such start. What is not a fact keeps the retry: ten failures
|
||||
without coverage — the network found only at the end or lost on the way —
|
||||
exit 1 like a disabled radio, as do a SIM still waiting for its PIN (the
|
||||
modem `locked`), a modem whose final profile list has no ims APN (the
|
||||
APN-string connect is tried; a connected APN-string bearer — a run whose
|
||||
lookup failed at boot — is reused and its registration resumed) and an
|
||||
APN-string series, which a walled carrier's modem refuses locally.
|
||||
The dead end after the PDN is up is the daemon's own, decided in `main()`
|
||||
before it takes the bus name with the same clean exit: nothing to register
|
||||
to — no `PCSCF=` and nothing discovered: `PCSCF_DISCOVERY=none` (no IPv6
|
||||
P-CSCF read from the PCO — the reader's exit 2: the list absent, empty,
|
||||
unreadable, or IPv4/FQDN entries only), `skipped` (a netdev without an rmnet
|
||||
mux id) or no `PCSCF_DISCOVERY=` at all (a manual run) —
|
||||
and no registration of its own to resume (`SaPeer` finds no pair, or the
|
||||
holds for this PDN and boot say `old_gone`):
|
||||
`idle: no P-CSCF — none configured (PCSCF=), none discovered (see the
|
||||
ims-pdn-up lines) — and nothing to resume; set PCSCF= in /etc/imsd.env to
|
||||
name one`; a pair the holds wrote off is taken out of the
|
||||
kernel by exact key on the way (`the SA pair to … is nothing to resume:
|
||||
removing it`, §12), so no start of this boot finds it. The no-target
|
||||
bring-up decides the same after its resume attempt: a resumable pair whose attempt failed keeps the fatal
|
||||
and the restart; a dead pair is written off, taken out of the kernel (§12)
|
||||
and the daemon idles (`RequestIdle`). A reader that did not answer
|
||||
(`PCSCF_DISCOVERY=failed`) is no fact: the daemon exits 1 instead — before
|
||||
the bus name with `no P-CSCF: discovery failed … — exiting for systemd
|
||||
restart`, from the no-target bring-up with `bring-up FAILED: no P-CSCF for a
|
||||
fresh registration: …` and `engine fatal: … — exiting for systemd restart` —
|
||||
and the unit's restart runs discovery again.
|
||||
- Never creates or connects a bearer while the modem is `disabled` — its
|
||||
radio is off (airplane mode): a modem still disabled after the wait, or
|
||||
disabled when an attempt is due, makes the script exit non-zero (`modem …
|
||||
|
|
@ -698,11 +789,18 @@ configures the muxed netdev. Idempotent; a non-zero exit fails the unit.
|
|||
routes the P-CSCF it uses itself as well — the fresh target before a fresh
|
||||
registration, the resumed SA peer once its SAs are confirmed (§7.2 steps 3 and
|
||||
4) —, as each call's media peer is. Nothing usable is logged with
|
||||
its cause (`nothing usable (…)`) and is not the script's failure: imsd
|
||||
resumes a registration of its own when it can and stays up on it (a fresh
|
||||
registration later restarts the daemon for want of a target); with nothing
|
||||
to resume it says so and exits, and the unit's restart runs discovery
|
||||
again. Anything else on
|
||||
its cause (`nothing usable (…)`), is not the script's failure, and is told
|
||||
apart in the hand-off: `PCSCF_DISCOVERY=none` when the reader found no IPv6
|
||||
P-CSCF in the PCO (its exit 2: the list absent, empty or unreadable —
|
||||
`tests/Qmi` pins the tolerant read) — a fact of this PDN: a registration of imsd's own that
|
||||
can be resumed is resumed and stays up (a fresh registration later restarts
|
||||
the daemon for want of a target, and that start runs discovery again), a
|
||||
bring-up that did not resume ends in the daemon's clean idle (`idle: no
|
||||
P-CSCF …`, the idle rule above) or, after a resumable pair's failed
|
||||
attempt, in the fatal and the restart; `PCSCF_DISCOVERY=failed` when the
|
||||
reader did not answer (an error, a timeout, nothing printed) — no fact: a
|
||||
daemon with nothing to resume exits 1 and the unit's restart runs
|
||||
discovery again. Anything else on
|
||||
the IMS network (its DNS servers, a traceroute target) needs its own host
|
||||
route (`ip -6 route add <addr> dev qmapmux0.0`). imsd installs a host route
|
||||
per media peer itself, and one to the P-CSCF it registers to or resumes on
|
||||
|
|
@ -713,7 +811,10 @@ configures the muxed netdev. Idempotent; a non-zero exit fails the unit.
|
|||
request after ESP decapsulation). Best-effort.
|
||||
- Writes `/run/imsd.env`: `DEV=<netdev>`, `LOCAL=<PDN address>`,
|
||||
`PCSCF_DISCOVERED=<list>` (comma-separated in the modem's order; empty when
|
||||
nothing was discovered or `PCSCF` is set), and logs the line it wrote. imsd
|
||||
nothing was discovered or `PCSCF` is set) and `PCSCF_DISCOVERY=found|none|failed|skipped`
|
||||
(how the list came to be: the PCO's answer, the reader's failure, or
|
||||
discovery not run — `PCSCF=` set, `IMS_IP_TYPE=ipv4`, no rmnet mux id), and
|
||||
logs the line it wrote. imsd
|
||||
registers on the address the network assigned, not on the kernel's
|
||||
temporary SLAAC address from the RA, which the netdev lists first and
|
||||
retires after about two days (the SAs and the Contact would go with it).
|
||||
|
|
@ -721,12 +822,13 @@ configures the muxed netdev. Idempotent; a non-zero exit fails the unit.
|
|||
hands it the previous run's file, and a stale discovered value must not
|
||||
pass for the admin's `PCSCF=`.
|
||||
|
||||
Its own settings (read from the environment, else from `/etc/imsd.env` for
|
||||
manual runs): `IMS_APN` (default `ims`), `IMS_IP_TYPE` (`ipv6`),
|
||||
`IMS_PROFILE_ID` (empty = look up; `none` = connect by APN string),
|
||||
`IMS_REG_TIMEOUT` (300) and `PCSCF`; from the environment only, `IMSD_PCSCF`
|
||||
(the reader's path, default `/usr/libexec/imsd-pcscf` — the mock bench's
|
||||
seam).
|
||||
Its own settings (read from the environment, else from the file `IMSD_ENV`
|
||||
names, `/etc/imsd.env` by default, for manual runs): `IMS_APN` (default
|
||||
`ims`), `IMS_IP_TYPE` (`ipv6`), `IMS_PROFILE_ID` (empty = look up; `none` =
|
||||
connect by APN string), `IMS_REG_TIMEOUT` (300) and `PCSCF`; from the
|
||||
environment only, `IMSD_PCSCF` (the reader's path, default
|
||||
`/usr/libexec/imsd-pcscf` — the mock bench's seam) and `IMSD_ENV` (the
|
||||
configuration file's path — the bench's seam too).
|
||||
|
||||
### 7.4 Registration recovery (`RegRecovery`, `Imsd-Engine.cppm`)
|
||||
|
||||
|
|
@ -754,7 +856,7 @@ The policy, with its constants:
|
|||
| A silent fresh attempt | Backs off 30 s → 10 min (`FreshDelayMin`/`Max`). |
|
||||
| `Retry-After` | Always wins over a shorter wait, honoured up to an hour (`RetryAfterMax`, the schedule's longest wait); a longer one is cut to an hour and the refusal heard again then. On a refused fresh attempt it replaces the schedule's wait, shorter or longer, never below 30 s (`FreshDelayMin`). |
|
||||
| A 403 | A refusal like the others: never hammered (the schedule), never abandoned (a SIM that is unbarred registers within the hour). |
|
||||
| The old registration is gone | A 403 once a fresh registration is due, a fresh attempt that got past the SA setup, a binding the network terminated (reg-event), an older address that left the netdev, or a bring-up whose resume failed while a fresh registration follows (with no fresh target a resumable pair whose attempt failed is kept for the restart; a pair nothing can resume — given up or written off by the engine, its address off the link, no state file, a file another attempt left behind, left untried by `RESUME=0` — is taken out of the kernel, §7.2 step 1): no reconnect over the old SA from then on. Otherwise the existing SA is still retried on the reconnect back-off while a fresh registration is deferred. |
|
||||
| The old registration is gone | A 403 once a fresh registration is due, a fresh attempt that got past the SA setup, a binding the network terminated (reg-event), an older address that left the netdev, or a bring-up whose resume failed while a fresh registration follows (with no fresh target a resumable pair whose attempt failed is kept for the restart; a pair nothing can resume — given up or written off by the engine, its address off the link, no state file, a file another attempt left behind, left untried by `RESUME=0` — is written off and taken out of the kernel, §7.2 step 1): no reconnect over the old SA from then on. Otherwise the existing SA is still retried on the reconnect back-off while a fresh registration is deferred. |
|
||||
| A `Dial` while unregistered | Brings the next recovery step forward (the reconnect, or the fresh attempt once it is due) — never a wait the network imposed (a `Retry-After`, or the schedule after a refused or throttled fresh attempt). imsd-ofonod refuses a `Dial` then and imsd-dialerd lists no modem. |
|
||||
| Success | Resets everything. |
|
||||
|
||||
|
|
@ -767,10 +869,10 @@ registration is gone: a 403 once a fresh registration is due, an attempt past
|
|||
the SA setup, a binding the network terminated, an older address that left
|
||||
the netdev, and every bring-up that goes on to register fresh — its resume
|
||||
failed or there was nothing to resume; a bring-up with no fresh target
|
||||
writes `old_gone` when the engine's rule writes the registration off — a
|
||||
403 once a fresh registration is due (one with a Retry-After, or the second
|
||||
refusal with the count restored from the holds), a binding a NOTIFY
|
||||
terminated under the attempt — and keeps one the holds already carried,
|
||||
writes `old_gone` when nothing can be resumed — a pair nothing can resume: its UE
|
||||
address left the link, no state file, a file another attempt left behind, written
|
||||
off before this start, given up or written off by the engine during the
|
||||
attempt, left untried by `RESUME=0` — never for a pair kept for the restart,
|
||||
§7.2 step 1); the reducer's refusal count travels too (`refusals`): a resume
|
||||
refused without a Retry-After at a start with no fresh target is one
|
||||
refusal, the restart's is the second and `RefusalsBeforeFresh` gives the
|
||||
|
|
@ -812,9 +914,10 @@ for a reason that allows renewal (RFC 6665: `deactivated`, `timeout`,
|
|||
earlier subscription's NOTIFY is still read for the binding's state, but its
|
||||
end is not ours to renew (KPN closes a registration's previous subscription
|
||||
with `noresource` when a new one replaces it). A NOTIFY that **terminates the
|
||||
binding** triggers a fresh registration where there is a fresh target (with
|
||||
none the daemon restarts instead, §7.2 step 3) — at once, or after the
|
||||
network's `retry-after` (up to an hour); after the call, when one is up (one exception:
|
||||
binding** triggers a fresh registration where there is a fresh target (with none a
|
||||
running daemon restarts, and a bring-up whose resume the NOTIFY ended idles
|
||||
— exit 1 instead when `PCSCF_DISCOVERY=failed` — §7.2 steps 1 and 3) — at
|
||||
once, or after the network's `retry-after` (up to an hour); after the call, when one is up (one exception:
|
||||
a REGISTER wait that still gets its 200 after such a NOTIFY, a call having
|
||||
kept the flow, leaves that registration standing). After
|
||||
`rejected` and `unregistered` it waits on the refusal schedule, a wait no
|
||||
|
|
@ -842,7 +945,21 @@ or goes on waiting out a hold the network had set (§7.4) — or, with an
|
|||
upper-case, zero-padded or uncompressed `PCSCF=` (accepted until 0.3.12),
|
||||
stops at bring-up with the spelling to use (§7.2 step 3). A stopped unit
|
||||
keeps no marker and `enqueue-marked` leaves it stopped (dev phone, systemd
|
||||
262~rc3, 2026-10-03).
|
||||
262~rc3, 2026-10-03) — so a phone that never had `/etc/imsd.env` (the unit
|
||||
never started before 1.0.0) keeps its stopped unit through the upgrade and
|
||||
starts at its next boot, registering where §7.3's rule lets it — unless `imsd-ofonod`
|
||||
runs there (the Phosh image, `imsd-ofono`): the upgrade restarts that daemon,
|
||||
and its `Wants=imsd.service` starts imsd at once (`enqueue-marked` queues the
|
||||
marked unit's try-restart, systemd collapses it to a restart for a running
|
||||
unit, and a restart job pulls in a start job for each `Wants=` — systemd
|
||||
262~rc3, `job_type_collapse` and `transaction_add_job_and_dependencies`; dev
|
||||
phone 2026-10-11: a stopped imsd was started in the same second an
|
||||
`imsd-ofonod` restart began, two seconds after it had been stopped, and
|
||||
resumed) — without waiting for it:
|
||||
since 1.0.0 `imsd-ofonod.service` has no `After=imsd.service` (that restart
|
||||
would otherwise wait behind the ims PDN bring-up — the whole `ExecStartPre`, up to `TimeoutStartSec=900`), so GNOME
|
||||
Calls' backend starts at once and republishes when imsd's name appears (`OnImsdAppeared`; dev
|
||||
phone 2026-10-12, bench 30).
|
||||
|
||||
A daemon older than 0.3.10 kept its bring-up throttle wait in memory only, so
|
||||
the restart registered fresh at once instead of waiting the schedule out.
|
||||
|
|
@ -936,8 +1053,8 @@ account `user`, its old default.
|
|||
|
||||
| Path | Written by | Content |
|
||||
|---|---|---|
|
||||
| `/etc/imsd.env` | the admin | configuration (§10), none required where discovery finds a P-CSCF (§7.3): `PCSCF=` overrides what discovery found, and names the P-CSCF where discovery is skipped or finds none. The apk adds a drop-in with `ConditionPathExists=/etc/imsd.env` (§9.2), so a phone without the file does not start the unit |
|
||||
| `/run/imsd.env` | `ims-pdn-up.sh` | `DEV=`, `LOCAL=` of the connected PDN, `PCSCF_DISCOVERED=` the IPv6 addresses of the PCO's P-CSCF list in the modem's order, at most eight (IPv4 entries and names are dropped; empty when the PCO gave no IPv6 P-CSCF, when discovery did not run or failed, or when `PCSCF=` is set) |
|
||||
| `/etc/imsd.env` | the admin | configuration (§10), none required since 1.0.0 where discovery finds a P-CSCF (§7.3): `PCSCF=` overrides what discovery found, and names the P-CSCF where discovery is skipped or finds none; the file's existence changes nothing. Before 1.0.0 the apk's drop-in `ConditionPathExists=/etc/imsd.env` made it the switch for the whole unit (§9.2) |
|
||||
| `/run/imsd.env` | `ims-pdn-up.sh` | `DEV=`, `LOCAL=` of the connected PDN, `PCSCF_DISCOVERED=` the IPv6 addresses of the PCO's P-CSCF list in the modem's order, at most eight (IPv4 entries and names are dropped; empty when the PCO gave no IPv6 P-CSCF, when discovery did not run or failed, or when `PCSCF=` is set), `PCSCF_DISCOVERY=found|none|failed|skipped` (how the list came to be: the PCO's answer, the reader's failure, or discovery not run); or `IMSD_IDLE=no-sim` / `no-ims-pdn` alone when the bring-up found nothing to do (§7.3; the daemon decides the dead end after the PDN is up itself) |
|
||||
| `/run/imsd.holds` (`HOLDS_FILE`) | imsd, `imsd-handover.sh` | the network's holds, the schedule's place and the reducer's refusal count (§7.4) |
|
||||
| `/run/imsd.handover` | `imsd.post-upgrade` | the InvocationID of the daemon an upgrade replaces, written when no holds file exists (§7.5); consumed by `imsd-handover.sh` |
|
||||
| `/run/imsd-ofonod.started` | `imsd-ofono-restart-calls.sh` | marker: imsd-ofonod has started once since boot |
|
||||
|
|
@ -1026,11 +1143,26 @@ Lines the operator (and the scripts) look for, all prefixed `imsd: `:
|
|||
- `connect: no answer from the P-CSCF N times (…)` when the protected
|
||||
connect series ends unanswered.
|
||||
|
||||
From the scripts: `ims-pdn-up: …` — among them `P-CSCF discovered: a, b
|
||||
- `idle: no SIM — nothing to do; systemctl start imsd when that changes`
|
||||
(or `idle: no ims PDN from the network — …` from `IMSD_IDLE`; `idle: no
|
||||
P-CSCF — none configured (PCSCF=), none discovered (see the ims-pdn-up
|
||||
lines) — and nothing to resume; set PCSCF= in /etc/imsd.env to name one —
|
||||
…` the daemon's own, before the bus name or
|
||||
from the no-target bring-up), then exit 0; `no P-CSCF: discovery failed
|
||||
(see the ims-pdn-up lines) and nothing to resume — exiting for systemd
|
||||
restart`, exit 1, when the reader did not answer (`PCSCF_DISCOVERY=failed`).
|
||||
|
||||
From the scripts: `ims-pdn-up: …` — among them `no SIM (sim-missing): not
|
||||
bringing the ims PDN up — systemctl start imsd once one is in`, `bearer
|
||||
connect refused 10 times in a row by profile index with packet service and a
|
||||
persistent cause (last: …): no ims PDN for this SIM here — systemctl start
|
||||
imsd to try again`, `wrote
|
||||
/run/imsd.env: IMSD_IDLE=…`, `P-CSCF discovered: a, b
|
||||
(pco-requested 1, profile-pcscf-via-pco 1)`, `P-CSCF discovery also gave v4 …
|
||||
(not usable on the IPv6-only ims PDN)`, `P-CSCF discovery: nothing usable
|
||||
(…)`, `P-CSCF X configured: discovery skipped`, `host route to P-CSCF X dev
|
||||
Y` (the configured address or the first discovered), `wrote /run/imsd.env: DEV=… LOCAL=… PCSCF_DISCOVERED=…` —,
|
||||
Y` (the configured address or the first discovered), `wrote
|
||||
/run/imsd.env: DEV=… LOCAL=… PCSCF_DISCOVERED=… PCSCF_DISCOVERY=…` —,
|
||||
`imsd-handover: …`, `imsd-ofono-restart-calls: …`.
|
||||
|
||||
## 8. D-Bus ABI and desktop integration
|
||||
|
|
@ -1176,11 +1308,31 @@ unit's sequencing is only tested under systemd.
|
|||
### 9.2 What the package installs and enables
|
||||
|
||||
`imsd.service` is enabled by preset (`80-imsd.preset`, plus the
|
||||
`multi-user.target.wants` link) and gated by a drop-in
|
||||
`ConditionPathExists=/etc/imsd.env`, so VoLTE survives reboots without a
|
||||
manual `systemctl enable` and an unconfigured phone does not boot into a
|
||||
failing unit. `imsd-ofonod.service` is enabled the same way by `imsd-ofono`.
|
||||
All of that is the apk's doing (`APKBUILD.binary`'s `package()`).
|
||||
`multi-user.target.wants` link), so VoLTE survives reboots without a manual
|
||||
`systemctl enable`. Since 1.0.0 there is no condition on `/etc/imsd.env`:
|
||||
the P-CSCF comes from the PDN's PCO, so the unit starts on every phone with
|
||||
the package — installing it and inserting a SIM is the opt-in; the file holds
|
||||
overrides only — and a phone with nothing to do (no SIM, no ims PDN from the
|
||||
network, no P-CSCF in the PCO and nothing to resume) exits cleanly and stays
|
||||
inactive (§7.3)
|
||||
rather than failing and restarting. `systemctl mask --now imsd` opts out: a
|
||||
plain `disable` removes only the [Install] links, and `imsd-ofonod`'s static
|
||||
`Wants=imsd.service` (the Phosh image) pulls a disabled imsd back in at every
|
||||
boot and at its every restart (dev phone 2026-10-11, the fp6 journal's
|
||||
`2026-10-11-0400-bench-kpn` capture: an `imsd-ofonod` restart started a
|
||||
stopped and, in the control run, a disabled imsd); the mask
|
||||
held through an `imsd-ofonod` restart and a reboot on the dev phone
|
||||
(2026-10-11). `systemctl unmask imsd` opts back in. `imsd-ofonod` does not
|
||||
wait for imsd (no `After=imsd.service` since 1.0.0, §7.5).
|
||||
Until 0.3.12 the apk shipped a drop-in `ConditionPathExists=/etc/imsd.env`;
|
||||
the upgrade removes it, and an upgraded phone that never had the file starts
|
||||
imsd at its next boot — or during the upgrade itself where `imsd-ofonod`
|
||||
runs (§7.5) — and registers where §7.3's rule lets it. `imsd-ofonod.service` is enabled the same way by
|
||||
`imsd-ofono`.
|
||||
The preset, the `multi-user.target.wants` link and the drop-in no longer
|
||||
shipped are the apk's doing (`APKBUILD.binary`'s `package()`); the idle, the
|
||||
`Wants=` pull-in and the opt-out are the bring-up script's and the daemon's,
|
||||
systemd's and the admin's.
|
||||
The main package hides kde-telephony's `modem-daemon` autostart for accounts
|
||||
created after it is installed (`/etc/skel/.config/autostart/org.kde.modem.daemon.desktop`)
|
||||
because `imsd-dialerd` owns those names.
|
||||
|
|
@ -1222,8 +1374,8 @@ bump commit titled `imsd X.Y.Z` that changes only the `Version` constant.
|
|||
|
||||
Everything is environment variables. The packaged unit reads
|
||||
`/run/imsd.env` (from `ims-pdn-up.sh`) and then `/etc/imsd.env` (the user's,
|
||||
wins on conflict). The variables are user documentation: the tables and the
|
||||
minimum `/etc/imsd.env` are in the README.
|
||||
wins on conflict; optional since 1.0.0). The variables are user
|
||||
documentation: the tables are in the README.
|
||||
|
||||
### 10.1 imsd
|
||||
|
||||
|
|
@ -1234,13 +1386,14 @@ requires updating the firewall rules `ims-pdn-up.sh` installs. `PCSCF` and
|
|||
and again by the engine): the first is one address in RFC 5952 form (§7.2 step 3: another spelling is
|
||||
refused at bring-up), and wins, the second a comma-separated IPv6 list whose
|
||||
first entry `CanonicalIp6` accepts is the fresh target otherwise (an IPv4
|
||||
address, a name, a literal with a zone or a dotted-quad tail is logged and
|
||||
skipped). A resume goes to the kernel's SA
|
||||
address, a name, a literal with a zone or a dotted-quad tail is skipped — logged by the
|
||||
engine when the daemon goes on to register, not by the idle before the bus
|
||||
name). A resume goes to the kernel's SA
|
||||
peer whatever they say (and routes it itself); beyond the resume's `pcscf:`
|
||||
line, having a fresh target decides what a refused or failed resume
|
||||
leads to — the fresh registration (a refusal without a Retry-After is not
|
||||
counted) or, with none, the pair kept for the restart or removed (§7.2
|
||||
step 1) — and a comma-separated `PCSCF=` stops the bring-up before the
|
||||
counted) or, with none, the pair kept for the restart, removed, or the idle
|
||||
(§7.2 step 1) — and a comma-separated `PCSCF=` stops the bring-up before the
|
||||
resume.
|
||||
|
||||
### 10.2 imsd-media (per call, set through imsd's environment)
|
||||
|
|
@ -1255,8 +1408,10 @@ there:
|
|||
|
||||
### 10.3 ims-pdn-up.sh and imsd-ofonod
|
||||
|
||||
`IMS_APN`, `IMS_IP_TYPE`, `IMS_PROFILE_ID`, `IMS_REG_TIMEOUT`, `PCSCF` and
|
||||
`IMSD_PCSCF` for the bring-up script (§7.3). `imsd-ofonod` reads `EMERGENCY_NUMBERS` (from
|
||||
`IMS_APN`, `IMS_IP_TYPE`, `IMS_PROFILE_ID`, `IMS_REG_TIMEOUT`, `PCSCF`,
|
||||
`IMSD_PCSCF` and `IMSD_ENV` (the configuration file's path, a bench seam) for
|
||||
the bring-up script (§7.3); `IMSD_IDLE` and `PCSCF_DISCOVERY` are the
|
||||
script's word to the daemon, not settings. `imsd-ofonod` reads `EMERGENCY_NUMBERS` (from
|
||||
`/etc/imsd.env` through its unit) and `IMSD_BUS=session|system` to override
|
||||
only the imsd side of its bus choice.
|
||||
|
||||
|
|
@ -1275,7 +1430,11 @@ only the imsd side of its bus choice.
|
|||
what a user can rely on the daemon for (0.3 added emergency calling), not
|
||||
with the size of a release; the major number moves when a release changes
|
||||
what an installed phone does without being asked, and it is not an ABI
|
||||
change (§8.1: the ABI is frozen).
|
||||
change (§8.1: the ABI is frozen). 1.0.0 is the first to move it: the unit
|
||||
starts on every phone with the package, with no file to write, and
|
||||
registers where `PCSCF=` names the P-CSCF or, on the FP6's `qmapmux0.0`
|
||||
(KPN, the one device and network it ran on), where the reader gets the
|
||||
PCO's IPv6 list.
|
||||
- **No AI attribution trailers.** Commits carry no `Assisted-by:`,
|
||||
`Co-Authored-By:` or similar lines, whoever or whatever drafted them.
|
||||
- **Signed commits.** Every commit is GPG-signed with the repository's
|
||||
|
|
|
|||
16
README.md
16
README.md
|
|
@ -48,17 +48,6 @@ Both can be found in the packages tab:
|
|||
https://forgejo.catcrafts.net/Catcrafts/-/packages/alpine/imsd
|
||||
```
|
||||
|
||||
The apk's `imsd.service` is enabled by preset and starts at boot once
|
||||
`/etc/imsd.env` exists (`touch /etc/imsd.env` is enough; the condition is checked when
|
||||
the unit starts, so after creating the file run `systemctl start imsd`). It first brings the `ims` PDN up through ModemManager, then registers:
|
||||
resuming an existing registration when it can, registering fresh otherwise,
|
||||
and waiting out whatever the network tells it to wait out. Check on it with:
|
||||
|
||||
```sh
|
||||
journalctl -u imsd -f
|
||||
busctl --system call net.catcrafts.IMS1 /net/catcrafts/IMS1 net.catcrafts.IMS1 GetStatus
|
||||
```
|
||||
|
||||
## Dialers
|
||||
|
||||
imsd itself is desktop-agnostic: everything a dialer needs is on the system
|
||||
|
|
@ -91,8 +80,8 @@ Configuration is environment variables in `/etc/imsd.env`, none are required.
|
|||
|
||||
| Variable | Default | Meaning |
|
||||
|---|---|---|
|
||||
| `PCSCF` | *(the first of `PCSCF_DISCOVERED`)* | P-CSCF address (IPv6), exactly one — not a comma-separated list — in RFC 5952 §4 form (lowercase, leading zeros dropped, the longest zero run compressed, no brackets — what `ip` prints for a plain IPv6 address; an IPv4-mapped address only as hex, `::ffff:a00:1` — the dotted spelling RFC 5952 §5 and `ip` use for it is refused); upper case, leading zeros, an uncompressed run or brackets are refused at start with the spelling to use (in the packaged unit the bring-up script refuses brackets first, with the rule), a dotted-quad tail with the rule; before 1.0.0 an upper-case, zero-padded or uncompressed `PCSCF=` matched no SA in `ip xfrm`'s text and registered fresh at every restart — an upgraded phone with such a setting stops at start with the spelling to use. Set, it wins over `PCSCF_DISCOVERED`. It is where a fresh registration goes; a registration resumed from the kernel's SAs stays on the P-CSCF it was made to until its next fresh registration. With neither this nor `PCSCF_DISCOVERED`, a fresh registration fails with a clear error |
|
||||
| `PCSCF_DISCOVERED` | *(from the PDN's PCO)* | the IPv6 P-CSCF list the network put in the ims PDN's PCO, comma-separated in the order the modem reports it; the first entry `CanonicalIp6` accepts is used when `PCSCF` is unset. The packaged service gets it via `/run/imsd.env` from `ims-pdn-up.sh`, which reads it from the modem with `imsd-pcscf` (at most eight; it routes the first; imsd routes the P-CSCF it uses — the fresh target, the resumed SA peer — itself as well); entries `CanonicalIp6` does not read — an IPv4 address, a name, an IPv6 literal with a zone or a dotted-quad tail (an IPv4-mapped entry in hex only) — are logged and ignored. In `/etc/imsd.env` it is a bench seam |
|
||||
| `PCSCF` | *(the first of `PCSCF_DISCOVERED`)* | P-CSCF address (IPv6), exactly one — not a comma-separated list — in RFC 5952 §4 form (lowercase, leading zeros dropped, the longest zero run compressed, no brackets — what `ip` prints for a plain IPv6 address; an IPv4-mapped address only as hex, `::ffff:a00:1` — the dotted spelling RFC 5952 §5 and `ip` use for it is refused); upper case, leading zeros, an uncompressed run or brackets are refused at start with the spelling to use (in the packaged unit the bring-up script refuses brackets first, with the rule), a dotted-quad tail with the rule; before 1.0.0 an upper-case, zero-padded or uncompressed `PCSCF=` matched no SA in `ip xfrm`'s text and registered fresh at every restart — an upgraded phone with such a setting stops at start with the spelling to use. Set, it wins over `PCSCF_DISCOVERED`. It is where a fresh registration goes; a registration resumed from the kernel's SAs stays on the P-CSCF it was made to until its next fresh registration. With neither this nor `PCSCF_DISCOVERED` there is no fresh registration: a phone with nothing to resume idles (`idle: no P-CSCF …`), unless a resume failed with its pair still resumable, or the reader did not answer (`PCSCF_DISCOVERY=failed`): those exit non-zero for systemd's restart |
|
||||
| `PCSCF_DISCOVERED` | *(from the PDN's PCO)* | the IPv6 P-CSCF list the network put in the ims PDN's PCO, comma-separated in the order the modem reports it; the first entry `CanonicalIp6` accepts is used when `PCSCF` is unset. The packaged service gets it via `/run/imsd.env` from `ims-pdn-up.sh`, which reads it from the modem with `imsd-pcscf` (at most eight; it routes the first; imsd routes the P-CSCF it uses — the fresh target, the resumed SA peer — itself as well); entries `CanonicalIp6` does not read — an IPv4 address, a name, an IPv6 literal with a zone or a dotted-quad tail (an IPv4-mapped entry in hex only) — are ignored (logged by the engine when the daemon goes on to register); `PCSCF_DISCOVERY=found|none|failed|skipped` beside it says how the list came to be — `failed` (the reader did not answer) makes a daemon with nothing to resume exit 1 for the restart instead of idling. In `/etc/imsd.env` it is a bench seam |
|
||||
| `IMSD_PCSCF` | `/usr/libexec/imsd-pcscf` | the P-CSCF reader `ims-pdn-up.sh` runs — a bench seam (the mock bench's fake reader; a stub through a unit drop-in on the dev phone), from the environment |
|
||||
| `PCSCF_PORT` | `5060` | unprotected SIP port for the initial REGISTER |
|
||||
| `DEV` | `qmapmux0.0` | ims-PDN netdev; the packaged service passes the real one via `/run/imsd.env` |
|
||||
|
|
@ -111,6 +100,7 @@ Configuration is environment variables in `/etc/imsd.env`, none are required.
|
|||
| `DUMP_SIP` | `0` | `1` writes raw REGISTER-200/SUBSCRIBE-200 dumps, the last refused REGISTER or SUBSCRIBE final (`imsd-register-fail.raw`, `imsd-subscribe-fail.raw`), 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. A refused REGISTER or SUBSCRIBE is logged with its status line and any Retry-After / Warning / Reason / Server headers |
|
||||
| `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`) |
|
||||
| `IMSD_ENV` | `/etc/imsd.env` | the configuration file's path — a bench seam for manual runs of `ims-pdn-up.sh` (the packaged unit reads `/etc/imsd.env` through `EnvironmentFile=`). The file's existence changes nothing |
|
||||
|
||||
The protected client/server ports are fixed at 45061/45062
|
||||
(`imsd::util::PortUc`/`PortUs`) — changing them in source also requires
|
||||
|
|
|
|||
|
|
@ -118,7 +118,8 @@ std::string RedactKeys(const std::vector<std::string>& argv) {
|
|||
// OwnXfrmDeleteCommands list — never `ip xfrm … flush`: another IPsec user's
|
||||
// entries, a policy-based VPN's, stay, §12); a delete that fails (an entry
|
||||
// gone between the read and the delete) is logged and the rest go on. At
|
||||
// file scope, as LoadHolds and PersistHolds are.
|
||||
// file scope, as LoadHolds and PersistHolds are: the Engine and main() both
|
||||
// run it (main() through RemoveDeadPair).
|
||||
void RunOwnXfrmDeletes(const std::vector<std::vector<std::string>>& cmds) {
|
||||
for (auto& cmd : cmds) {
|
||||
int rc = RunCmd(cmd);
|
||||
|
|
@ -131,7 +132,7 @@ void RunOwnXfrmDeletes(const std::vector<std::vector<std::string>>& cmds) {
|
|||
// on the UE's address): taken out by exact key, as the SA setup takes imsd's
|
||||
// previous pair out (RunOwnXfrmDeletes — nothing else in the kernel is
|
||||
// touched, §12), so the next bring-up finds nothing to resume. Run by the
|
||||
// no-target bring-up.
|
||||
// no-target bring-up and by main()'s idle before the bus name.
|
||||
void RemoveDeadPair(std::string_view local) {
|
||||
auto state = RunCapture({"ip", "xfrm", "state"});
|
||||
auto policy = RunCapture({"ip", "xfrm", "policy"});
|
||||
|
|
@ -309,6 +310,17 @@ std::string FreshPcscfFromEnv(std::vector<std::string>* discovered = nullptr, st
|
|||
return list.empty() ? std::string() : list.front();
|
||||
}
|
||||
|
||||
// The no-target bring-up's two outcomes, told apart by the script's word in
|
||||
// /run/imsd.env (§7.3). The PCO naming no P-CSCF is a fact
|
||||
// (PCSCF_DISCOVERY=none): a daemon with nothing to resume idles on it until
|
||||
// a boot or a start. A reader that did not answer is not
|
||||
// (PCSCF_DISCOVERY=failed): exit 1, and the unit's restart runs discovery
|
||||
// again. Nothing here depends on a configuration file existing — installing
|
||||
// the package and inserting a SIM is the opt-in.
|
||||
bool DiscoveryFailed() { return EnvOr("PCSCF_DISCOVERY", "") == "failed"; }
|
||||
constexpr std::string_view NoPcscfIdleText = "no P-CSCF — none configured (PCSCF=), none discovered (see the ims-pdn-up lines) — and nothing to resume; set PCSCF= in /etc/imsd.env to name one";
|
||||
constexpr std::string_view NoPcscfDiscoveryFailed = "no P-CSCF: discovery failed (see the ims-pdn-up lines) and nothing to resume";
|
||||
|
||||
// ---- sockaddr helper ------------------------------------------------------
|
||||
socklen_t MakeAddr(std::string_view ip, int port, sockaddr_storage& ss) {
|
||||
std::memset(&ss, 0, sizeof ss);
|
||||
|
|
@ -792,11 +804,11 @@ struct StatusSnapshot {
|
|||
|
||||
// engine -> main-loop event
|
||||
struct Event {
|
||||
enum class Type { Added, State, Deleted, Registration, Status, Fatal } type;
|
||||
enum class Type { Added, State, Deleted, Registration, Status, Fatal, Idle } type;
|
||||
CallInfo info; // Added
|
||||
std::string uni, state, reason; // State/Deleted
|
||||
StatusSnapshot status; // Registration/Status
|
||||
std::string text; // Fatal
|
||||
std::string text; // Fatal, Idle
|
||||
};
|
||||
|
||||
GDBusConnection* DbusConn = nullptr;
|
||||
|
|
@ -944,6 +956,12 @@ gboolean OnIdleEvent(gpointer data) {
|
|||
FatalExit = true; // non-zero exit, or Restart=on-failure never fires
|
||||
g_main_loop_quit(MainLoop);
|
||||
break;
|
||||
case Event::Type::Idle:
|
||||
// nothing to do (§7.3): a clean exit, which Restart=on-failure
|
||||
// leaves alone
|
||||
Log(std::format("idle: {} — nothing to do; systemctl start imsd when that changes", ev->text));
|
||||
g_main_loop_quit(MainLoop);
|
||||
break;
|
||||
}
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
|
@ -1357,22 +1375,27 @@ private:
|
|||
if (outcome == ResumeOutcome::Failed && (!recovery_.Reconnectable() || (pcscfTarget_.empty() && recovery_.Fresh()))) outcome = ResumeOutcome::Dead;
|
||||
}
|
||||
if (pcscfTarget_.empty()) {
|
||||
// No fresh registration follows, so this is no write-off: a
|
||||
// resumable pair whose attempt failed (a silent connect, an
|
||||
// No fresh registration follows, so a resumable pair is not written
|
||||
// off: one whose attempt failed (a silent connect, an
|
||||
// unanswered or refused refresh — its Retry-After is in the
|
||||
// holds) stays for the restart to resume — unless the engine's
|
||||
// rule wrote the registration off or gave the resume up (a 403 once
|
||||
// fresh is due, a NOTIFY-terminated binding; a 401, a refusal with a
|
||||
// Retry-After, the second refusal — the count travels in the holds;
|
||||
// Dead above); a pair nothing can
|
||||
// resume (its UE address left the link, no state file, the file
|
||||
// another attempt's, written off before this start) is taken out
|
||||
// of the kernel, by exact key — the fresh registration that would
|
||||
// have replaced it does not come — so the next bring-up finds
|
||||
// nothing to resume.
|
||||
if (outcome == ResumeOutcome::Dead) RemoveDeadPair(pdnLocal_);
|
||||
// holds) stays for the restart to resume (the fatal and the
|
||||
// retry) — unless the engine's rule wrote the registration off or
|
||||
// gave the resume up (a 403 once fresh is due, a NOTIFY-terminated
|
||||
// binding; a 401, a refusal with a Retry-After, the second refusal —
|
||||
// the count travels in the holds; Dead above); a pair nothing can
|
||||
// resume (its UE address left the link, no
|
||||
// state file, the file another attempt's, written off before this
|
||||
// start) is written off and taken out of the kernel by exact key
|
||||
// — the fresh registration that would have replaced it does not
|
||||
// come — so the next bring-up finds nothing to resume, and the
|
||||
// daemon idles (RequestIdle: a clean exit, §7.3): the PCO named no
|
||||
// P-CSCF and nothing is configured, a fact until a boot or a
|
||||
// start. A discovery that FAILED (the reader did not answer) is
|
||||
// no fact: the fatal, and the restart runs it again.
|
||||
if (outcome == ResumeOutcome::Dead) { recovery_.OldGone(); RemoveDeadPair(pdnLocal_); }
|
||||
KeepHolds();
|
||||
throw std::runtime_error(std::string(NoFreshPcscf));
|
||||
if (outcome == ResumeOutcome::Failed || DiscoveryFailed()) throw std::runtime_error(std::string(NoFreshPcscf));
|
||||
RequestIdle(std::string(NoPcscfIdleText)); throw Stopping{};
|
||||
}
|
||||
// a resume that failed found the stored SAs unusable (and after a
|
||||
// reboot there are none): nothing to reconnect over while a deferred
|
||||
|
|
@ -2169,6 +2192,16 @@ private:
|
|||
PostEvent(std::move(ev));
|
||||
}
|
||||
|
||||
// Leave cleanly: nothing to do (§7.3); the
|
||||
// unit stays inactive until the next boot, a `systemctl start imsd` or a
|
||||
// start of imsd-ofonod (its Wants=imsd.service, the Phosh image).
|
||||
void RequestIdle(const std::string& why) {
|
||||
quit_.store(true);
|
||||
auto ev = std::make_unique<Event>();
|
||||
ev->type = Event::Type::Idle; ev->text = why;
|
||||
PostEvent(std::move(ev));
|
||||
}
|
||||
|
||||
// After a registration succeeds — bring-up, a keepalive refresh, a
|
||||
// reconnect + refresh, or a fresh recovery: the status (a signal only
|
||||
// when it changes), the timers, the recovery policy, the reg-event
|
||||
|
|
@ -2708,6 +2741,17 @@ int main(int argc, char** argv) {
|
|||
return 1;
|
||||
}
|
||||
|
||||
// The bring-up found nothing to do (IMSD_IDLE= in /run/imsd.env — the
|
||||
// script's arms: no SIM, a refused ims PDN): say so and
|
||||
// leave cleanly. Restart=on-failure leaves a clean exit alone, so the
|
||||
// unit stays inactive until the next boot, a manual start or a start of
|
||||
// imsd-ofonod (its Wants=imsd.service).
|
||||
if (std::string idle = EnvOr("IMSD_IDLE", ""); !idle.empty()) {
|
||||
Log(std::format("idle: {} — nothing to do; systemctl start imsd when that changes",
|
||||
idle == "no-sim" ? "no SIM" : idle == "no-ims-pdn" ? "no ims PDN from the network" : idle));
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::string dev = EnvOr("DEV", "qmapmux0.0");
|
||||
std::string local = EnvOr("LOCAL", "");
|
||||
if (local.empty()) {
|
||||
|
|
@ -2720,6 +2764,28 @@ int main(int argc, char** argv) {
|
|||
local = imsd::util::CanonicalAddr(local);
|
||||
std::string freshPcscf = FreshPcscfFromEnv();
|
||||
Log(std::format("LOCAL={} P-CSCF=[{}]:{}", local, freshPcscf.empty() ? "(none)" : freshPcscf, EnvOr("PCSCF_PORT", "5060")));
|
||||
// The daemon's own dead end, decided here, before the bus name, so the
|
||||
// exit is clean and the unit stays inactive (§7.3): nothing to register
|
||||
// to — no PCSCF=, nothing discovered — and no registration of its own to
|
||||
// resume (SaPeer finds no pair, or the holds' write-off for this PDN and
|
||||
// boot says it cannot). The script decides the dead ends only it can see
|
||||
// (no SIM, a refused ims PDN). A discovery that failed is no dead end:
|
||||
// exit 1, and the unit's restart runs it again.
|
||||
if (!session && freshPcscf.empty()) {
|
||||
auto policy = RunCapture({"ip", "xfrm", "policy"});
|
||||
auto holds = LoadHolds(local);
|
||||
auto peer = imsd::ipsec::SaPeer(policy.out);
|
||||
const bool writtenOff = holds && holds->oldGone;
|
||||
if (!peer || writtenOff) {
|
||||
// a pair the holds wrote off before this start is nothing to
|
||||
// resume: taken out of the kernel here, as the no-target bring-up
|
||||
// takes a dead pair out (§7.2 step 1), so no start of this boot
|
||||
// finds it — the holds keep the write-off
|
||||
if (peer && writtenOff) RemoveDeadPair(local);
|
||||
if (DiscoveryFailed()) { Log(std::format("{} — exiting for systemd restart", NoPcscfDiscoveryFailed)); return 1; }
|
||||
Log(std::format("idle: {} — nothing to do; systemctl start imsd when that changes", NoPcscfIdleText)); return 0;
|
||||
}
|
||||
}
|
||||
if (!session) LogAudioSession();
|
||||
imsd::bus::AuthorizeCalls = !session;
|
||||
|
||||
|
|
|
|||
|
|
@ -60,18 +60,17 @@ package() {
|
|||
# account created at install — imsd-dialerd owns those session bus names
|
||||
install -Dm644 "$srcdir"/org.kde.modem.daemon.desktop \
|
||||
"$pkgdir"/etc/skel/.config/autostart/org.kde.modem.daemon.desktop
|
||||
# enabled by preset: the unit is a no-op until /etc/imsd.env exists, and
|
||||
# VoLTE surviving reboots must not depend on a manual systemctl enable
|
||||
# enabled by preset: VoLTE surviving reboots must not depend on a manual
|
||||
# systemctl enable. Since 1.0.0 the unit needs no configuration file (the
|
||||
# P-CSCF comes from the PDN's PCO) and starts on every phone with the
|
||||
# package; without a SIM it exits cleanly and stays inactive (IMSD_IDLE),
|
||||
# and `systemctl mask --now imsd` opts out (a disable is undone by
|
||||
# imsd-ofonod's Wants=imsd.service)
|
||||
install -Dm644 "$srcdir"/80-imsd.preset \
|
||||
"$pkgdir"/usr/lib/systemd/system-preset/80-imsd.preset
|
||||
mkdir -p "$pkgdir"/etc/systemd/system/multi-user.target.wants
|
||||
ln -s /usr/lib/systemd/system/imsd.service \
|
||||
"$pkgdir"/etc/systemd/system/multi-user.target.wants/imsd.service
|
||||
# ...but only actually start once the carrier config exists, so
|
||||
# unconfigured systems don't boot into a failing unit
|
||||
mkdir -p "$pkgdir"/usr/lib/systemd/system/imsd.service.d
|
||||
printf '[Unit]\nConditionPathExists=/etc/imsd.env\n' \
|
||||
> "$pkgdir"/usr/lib/systemd/system/imsd.service.d/10-require-config.conf
|
||||
install -Dm755 imsd-ofonod "$pkgdir"/usr/bin/imsd-ofonod
|
||||
install -Dm755 imsd-ofono-restart-calls.sh \
|
||||
"$pkgdir"/usr/libexec/imsd-ofono-restart-calls.sh
|
||||
|
|
|
|||
|
|
@ -10,8 +10,10 @@
|
|||
# the first discovered address (the daemon's fresh target) a host route —
|
||||
# never a default route (F15 below). Idempotent — safe to run when the PDN
|
||||
# is already up. Runs as
|
||||
# ExecStartPre of imsd.service, so imsd only starts once the PDN exists; a
|
||||
# nonzero exit fails the unit and systemd retries per Restart/RestartSec.
|
||||
# ExecStartPre of imsd.service, so imsd starts once the PDN exists — or once
|
||||
# this script has written IMSD_IDLE= and exited 0 with nothing to do, when
|
||||
# imsd logs the idle and exits cleanly (idle_exit below); a nonzero exit
|
||||
# fails the unit and systemd retries per Restart/RestartSec.
|
||||
#
|
||||
# Configuration, via the unit's EnvironmentFile /etc/imsd.env (also read
|
||||
# directly so manual runs behave the same):
|
||||
|
|
@ -32,6 +34,9 @@
|
|||
# /run/imsd.env, the first of them routed
|
||||
# IMSD_PCSCF the reader's path (default /usr/libexec/imsd-pcscf) — a
|
||||
# bench seam, from the environment only (not from the file)
|
||||
# IMSD_ENV the configuration file's path (default /etc/imsd.env) —
|
||||
# a bench seam. The file is optional (overrides only); its
|
||||
# existence changes nothing
|
||||
|
||||
log() { echo "ims-pdn-up: $*"; }
|
||||
|
||||
|
|
@ -52,9 +57,15 @@ bearer_paths() {
|
|||
# `enabled` with the modem still in low power; switching the radio back on
|
||||
# then no longer brings the modem up (dev phone 2026-10-06).
|
||||
radio_off() { [ "$(modem_state)" = disabled ]; }
|
||||
# A locked SIM (a PIN not entered yet) is the same kind of state the user
|
||||
# controls: the modem registers once the PIN is in, so like a disabled radio
|
||||
# it exits non-zero and the unit's restart tries again.
|
||||
sim_locked() { [ "$(modem_state)" = locked ]; }
|
||||
unlock_required() { kv -m "$MODEM" | sed -n 's/^modem\.generic\.unlock-required *: *//p'; }
|
||||
|
||||
IMSD_ENV=${IMSD_ENV:-/etc/imsd.env}
|
||||
envval() { # $1 = key — for manual runs; under systemd the vars are inherited
|
||||
sed -n "s/^$1=//p" /etc/imsd.env 2>/dev/null | tail -n1 | tr -d '"'
|
||||
sed -n "s/^$1=//p" "$IMSD_ENV" 2>/dev/null | tail -n1 | tr -d '"'
|
||||
}
|
||||
IP_TYPE=${IMS_IP_TYPE:-$(envval IMS_IP_TYPE)}
|
||||
IP_TYPE=${IP_TYPE:-ipv6}
|
||||
|
|
@ -75,6 +86,44 @@ while :; do
|
|||
done
|
||||
log "modem $MODEM"
|
||||
|
||||
# ---- nothing to do: a clean exit, not a retry loop. imsd reads IMSD_IDLE=
|
||||
# from /run/imsd.env, logs it and exits 0, which Restart=on-failure leaves
|
||||
# alone — the unit stays inactive until the next boot, a `systemctl start
|
||||
# imsd` or a start of imsd-ofonod (its Wants=imsd.service, the Phosh image).
|
||||
# Installing the package and inserting a SIM is the opt-in; no configuration
|
||||
# file changes what follows. The dead ends are facts: no SIM (the modem's
|
||||
# `failed` state with a SIM reason; final until ModemManager re-probes;
|
||||
# checked here and while waiting for registration below), a profile-indexed
|
||||
# connect series refused ten times in a row — each a persistent 3GPP cause
|
||||
# in ModemManager's typed error (not subscribed, unknown APN, barred, not
|
||||
# authorised in this PLMN, authentication failed, the service option not
|
||||
# supported, the wrong PDN type), with packet service; counted only while the
|
||||
# modem reads registered, connecting or connected with packet service not
|
||||
# detached — any other read, a timeout or a transient cause starts the count
|
||||
# again — and — the daemon's own, with the same clean exit — a
|
||||
# bring-up that ends with no P-CSCF (none configured, the PCO names none)
|
||||
# and no registration of imsd's own to resume. One attempt per boot (or per
|
||||
# such imsd-ofonod start). What is not a fact exits 1 like a disabled radio
|
||||
# and the restart retries: ten failures without coverage, a SIM still waiting
|
||||
# for its PIN, a modem without an ims profile (the APN-string connect is
|
||||
# tried), an APN-string series (the modem's own wall under an IPv4 attach,
|
||||
# F6), a P-CSCF reader that did not answer (PCSCF_DISCOVERY=failed, below).
|
||||
idle_exit() { # $1 = key, $2 = log text
|
||||
log "$2"
|
||||
printf 'IMSD_IDLE=%s\n' "$1" > /run/imsd.env
|
||||
log "wrote /run/imsd.env: IMSD_IDLE=$1"
|
||||
exit 0
|
||||
}
|
||||
sim_check() {
|
||||
[ "$(modem_state)" = failed ] || return 0
|
||||
REASON=$(kv -m "$MODEM" | sed -n 's/^modem\.generic\.state-failed-reason *: *//p')
|
||||
case "$REASON" in
|
||||
sim-missing|esim-without-profiles|sim-error)
|
||||
idle_exit no-sim "no SIM ($REASON): not bringing the ims PDN up — systemctl start imsd once one is in" ;;
|
||||
esac
|
||||
}
|
||||
sim_check
|
||||
|
||||
# ---- the ims profile index (WDS profile list), unless configured. The
|
||||
# lookup itself can fail while ModemManager is still enabling the modem; a
|
||||
# failed lookup is not "no profile" (the APN-string request it would fall
|
||||
|
|
@ -175,7 +224,7 @@ if [ -z "$BEARER" ]; then
|
|||
waited=0 last=
|
||||
while :; do
|
||||
STATE=$(modem_state)
|
||||
case "$STATE" in registered|connecting|connected) break ;; esac
|
||||
case "$STATE" in registered|connecting|connected) break ;; failed) sim_check ;; esac
|
||||
[ "$STATE" != "$last" ] && log "waiting for registration (state: ${STATE:-unknown})"
|
||||
last=$STATE
|
||||
if [ "$waited" -ge "$REG_TIMEOUT" ]; then
|
||||
|
|
@ -183,6 +232,10 @@ if [ -z "$BEARER" ]; then
|
|||
log "modem still disabled after $REG_TIMEOUT s (radio off): not connecting"
|
||||
exit 1
|
||||
fi
|
||||
if [ "$STATE" = locked ]; then
|
||||
log "SIM locked ($(unlock_required)) after $REG_TIMEOUT s: not connecting until the PIN is entered"
|
||||
exit 1
|
||||
fi
|
||||
log "not registered after $REG_TIMEOUT s — attempting anyway"
|
||||
break
|
||||
fi
|
||||
|
|
@ -208,10 +261,31 @@ if [ -z "$BEARER" ]; then
|
|||
fi
|
||||
|
||||
n=0
|
||||
REFUSED=0 # connects refused in a row, each a persistent 3GPP cause with the modem registered/connecting/connected and packet service not detached (stricter than the daemon's PacketServiceUp)
|
||||
while [ -z "$BEARER" ]; do
|
||||
n=$((n + 1))
|
||||
[ "$n" -gt 10 ] && { log "bearer connect failed after 10 attempts"; exit 1; }
|
||||
if [ "$n" -gt 10 ]; then
|
||||
# only a profile-indexed series the network refused says anything
|
||||
# about it: an APN-string series (a lookup that kept failing, a late
|
||||
# registration) meets the modem's own wall under an IPv4 attach (F6),
|
||||
# which the restart's fresh lookup gets past, and a failure without
|
||||
# packet service is a coverage gap, not a refusal — counted only while
|
||||
# the modem reads registered, connecting or connected with packet
|
||||
# service not `detached` (a CS-registered modem still attaching) and
|
||||
# only for a persistent 3GPP cause in ModemManager's typed error; any
|
||||
# other read, `disconnecting`, `unknown` or an unanswered mmcli among
|
||||
# them, and a timeout or a transient cause, reset it — stricter than
|
||||
# the daemon's PacketServiceUp, which counts an unknown state as
|
||||
# service — so the
|
||||
# count is of refusals in a row, each with packet service at the time,
|
||||
# never one state read after the series: a series that failed without
|
||||
# coverage and found the network at its end exits 1 like any other,
|
||||
# and the restart retries on the attached modem
|
||||
[ -z "$PROFILE_ID" ] || [ "$REFUSED" -lt 10 ] || idle_exit no-ims-pdn "bearer connect refused 10 times in a row by profile index with packet service and a persistent cause (last: $(squash "$OUT")): no ims PDN for this SIM here — systemctl start imsd to try again"
|
||||
log "bearer connect failed after 10 attempts (modem state: $(modem_state), last: $(squash "$OUT"))"; exit 1
|
||||
fi
|
||||
radio_off && { log "modem disabled (radio off): not connecting"; exit 1; }
|
||||
sim_locked && { log "SIM locked ($(unlock_required)): not connecting until the PIN is entered"; exit 1; }
|
||||
B=$(find_ims_bearer no) # reuse a stale disconnected ims bearer of this run's shape
|
||||
if [ -z "$B" ]; then
|
||||
if [ -n "$PROFILE_ID" ]; then
|
||||
|
|
@ -225,6 +299,7 @@ while [ -z "$BEARER" ]; do
|
|||
log "created bearer $B ($SPEC)"
|
||||
else
|
||||
log "create-bearer attempt $n failed: $(squash "$OUT"); retrying in 10 s"
|
||||
REFUSED=0 # not the network's answer to a connect
|
||||
sleep 10
|
||||
continue
|
||||
fi
|
||||
|
|
@ -232,7 +307,24 @@ while [ -z "$BEARER" ]; do
|
|||
if OUT=$(mmcli -b "$B" --connect 2>&1); then
|
||||
BEARER=$B
|
||||
else
|
||||
log "connect attempt $n failed (state: $(modem_state)): $(squash "$OUT"); retrying in 10 s"
|
||||
ST=$(modem_state); PS=$(packet_state)
|
||||
log "connect attempt $n failed (state: ${ST:-unknown}, packet service: ${PS:-unknown}): $(squash "$OUT"); retrying in 10 s"
|
||||
# a refusal is the network's answer, as ModemManager types it: a
|
||||
# persistent 3GPP cause — not subscribed, unknown APN, barred, not
|
||||
# authorised in this PLMN, authentication failed, the service option
|
||||
# not supported, the wrong PDN type for this attach — with the modem
|
||||
# attached. A timeout (D-Bus, esm-proc-timeout), a transient cause
|
||||
# (insufficient resources, out of order, a network failure) or any
|
||||
# other error starts the count again, as a read without packet service
|
||||
# does: an outage must not idle VoLTE until the next boot
|
||||
PERSISTENT=
|
||||
case "$OUT" in
|
||||
*MobileEquipment.ServiceOptionNotSubscribed:*|*MobileEquipment.MissingOrUnknownApn:*|*MobileEquipment.OperatorDeterminedBarring:*|*MobileEquipment.ServiceOptionNotAuthorizedInPlmn:*|*MobileEquipment.UserAuthenticationFailed:*|*MobileEquipment.ServiceOptionNotSupported:*|*MobileEquipment.Ipv4OnlyAllowed:*|*MobileEquipment.Ipv6OnlyAllowed:*) PERSISTENT=1 ;;
|
||||
esac
|
||||
case "$ST" in
|
||||
registered|connecting|connected) if [ "$PS" = detached ] || [ -z "$PERSISTENT" ]; then REFUSED=0; else REFUSED=$((REFUSED + 1)); fi ;;
|
||||
*) REFUSED=0 ;;
|
||||
esac
|
||||
case "$OUT" in *call-already-present*)
|
||||
# the PDN is up on a bearer this run did not recognise (ip-type or
|
||||
# APN spelling); use it rather than ask the modem for a second one
|
||||
|
|
@ -309,9 +401,15 @@ fi
|
|||
# pass for the admin's PCSCF=, and it is never read from the environment
|
||||
# here). The PDN is brought up IPv6-only, so only IPv6 addresses are usable;
|
||||
# IPv4 ones and names are logged and dropped. Nothing usable is not this
|
||||
# script's failure: imsd resumes a registration of its own when it can; with
|
||||
# nothing to resume it says so and exits, and the unit's restart runs
|
||||
# discovery again.
|
||||
# script's failure, and PCSCF_DISCOVERY= in the hand-off tells the daemon
|
||||
# which kind it was: `none` — the PCO carried no IPv6 P-CSCF (the reader's
|
||||
# exit 2), a fact: a registration of imsd's own that can be resumed is
|
||||
# resumed and stays up, else the daemon idles (its `idle:` exit; after a
|
||||
# resumable pair's failed attempt the fatal and the restart) — or `failed` —
|
||||
# the reader did not answer (an error, a timeout, nothing printed), no fact:
|
||||
# a daemon with nothing to resume exits 1 and the unit's restart runs
|
||||
# discovery again. `found` and `skipped` (PCSCF= set, an IPv4-only PDN, no
|
||||
# rmnet mux id) are the other two values.
|
||||
# The P-CSCF is reachable through the ims PDN only: a host route stands in
|
||||
# for the default — `dev` without a gateway (a raw-IP netdev without
|
||||
# header_ops, so the kernel resolves no neighbours), the shape imsd uses for
|
||||
|
|
@ -331,6 +429,7 @@ route_pcscf() { # $1 = address
|
|||
}
|
||||
PCSCF=${PCSCF:-$(envval PCSCF)}
|
||||
DISCOVERED=
|
||||
DISCOVERY=skipped
|
||||
case "$PCSCF" in
|
||||
'')
|
||||
if [ "$IP_TYPE" = ipv4 ]; then
|
||||
|
|
@ -340,6 +439,7 @@ case "$PCSCF" in
|
|||
if [ -z "$MUX" ]; then
|
||||
log "P-CSCF discovery: no rmnet mux id on $IFACE (ip -d link); set PCSCF="
|
||||
else
|
||||
DISCOVERY=failed
|
||||
OUT=$("${IMSD_PCSCF:-/usr/libexec/imsd-pcscf}" --mux "$MUX" ${PROFILE_ID:+--profile "$PROFILE_ID"} --timeout 10 2>&1); rc=$?
|
||||
V6=; SHOWN=; OTHER=; FLAGS=; ERR=; n=0
|
||||
while IFS= read -r line; do
|
||||
|
|
@ -356,13 +456,14 @@ case "$PCSCF" in
|
|||
$OUT
|
||||
EOF
|
||||
if [ -n "$V6" ]; then
|
||||
DISCOVERY=found
|
||||
log "P-CSCF discovered: $SHOWN${FLAGS:+ ($FLAGS)}"
|
||||
[ -n "$OTHER" ] && log "P-CSCF discovery also gave $OTHER (not usable on the IPv6-only ims PDN)"
|
||||
route_pcscf "${V6%% *}" # the first: the daemon's fresh target; imsd routes the P-CSCF it uses itself as well (the fresh target, the resumed peer)
|
||||
else
|
||||
case $rc in
|
||||
0) log "P-CSCF discovery: nothing usable (imsd-pcscf printed no address${ERR:+: $ERR})" ;;
|
||||
2) log "P-CSCF discovery: nothing usable (the PCO carried no IPv6 P-CSCF${OTHER:+; it gave $OTHER}${FLAGS:+; $FLAGS})" ;;
|
||||
2) DISCOVERY=none; log "P-CSCF discovery: nothing usable (the PCO carried no IPv6 P-CSCF${OTHER:+; it gave $OTHER}${FLAGS:+; $FLAGS})" ;;
|
||||
*) log "P-CSCF discovery: nothing usable (imsd-pcscf rc $rc${ERR:+: $ERR})" ;;
|
||||
esac
|
||||
fi
|
||||
|
|
@ -381,9 +482,17 @@ esac
|
|||
# the RA, which the netdev lists first and which is deprecated after a day
|
||||
# and removed after two (ims ledger F18; the SAs and the Contact would go
|
||||
# with it). PCSCF_DISCOVERED= is written on every run, empty when nothing
|
||||
# was discovered or PCSCF= is set.
|
||||
printf 'DEV=%s\nLOCAL=%s\nPCSCF_DISCOVERED=%s\n' "$IFACE" "$ADDR" "$DISCOVERED" > /run/imsd.env
|
||||
log "wrote /run/imsd.env: DEV=$IFACE LOCAL=$ADDR PCSCF_DISCOVERED=$DISCOVERED"
|
||||
# was discovered or PCSCF= is set; PCSCF_DISCOVERY= says how it came to be
|
||||
# (found, none, failed, skipped — above).
|
||||
# The dead end after the PDN is up — nothing to register to (no PCSCF=,
|
||||
# nothing discovered, no registration of imsd's own in the kernel to resume)
|
||||
# — is the daemon's to decide (its `idle:` exit before it takes the bus name,
|
||||
# §7.3): it knows the fresh target, the kernel's SA pair and the holds, and
|
||||
# PCSCF_DISCOVERY= tells it whether an empty list is the PCO's answer (none:
|
||||
# the idle) or the reader's failure (failed: exit 1, the restart runs
|
||||
# discovery again). The hand-off is written on every run.
|
||||
printf 'DEV=%s\nLOCAL=%s\nPCSCF_DISCOVERED=%s\nPCSCF_DISCOVERY=%s\n' "$IFACE" "$ADDR" "$DISCOVERED" "$DISCOVERY" > /run/imsd.env
|
||||
log "wrote /run/imsd.env: DEV=$IFACE LOCAL=$ADDR PCSCF_DISCOVERED=$DISCOVERED PCSCF_DISCOVERY=$DISCOVERY"
|
||||
|
||||
# ---- open the IMS protected ports in the firewall (rung 5b root cause,
|
||||
# journal/ims.md s56): pmOS's default nftables INPUT chain is policy-drop and
|
||||
|
|
|
|||
|
|
@ -11,7 +11,9 @@ Documentation=https://forgejo.catcrafts.net/Catcrafts/imsd
|
|||
# — Plasma Mobile uses imsd-dialerd instead. Both may run at once (different
|
||||
# buses, different consumers), but a real ofonod must not.
|
||||
Conflicts=ofono.service
|
||||
After=imsd.service
|
||||
# Wants, not After: imsd starts on every phone since 1.0.0 and its start runs
|
||||
# the ims PDN bring-up (the whole ExecStartPre, up to TimeoutStartSec=900);
|
||||
# this backend starts at once and republishes when imsd's name appears (OnImsdAppeared)
|
||||
Wants=imsd.service
|
||||
|
||||
[Service]
|
||||
|
|
|
|||
|
|
@ -21,7 +21,9 @@ ExecStartPre=-/usr/libexec/imsd-handover.sh
|
|||
TimeoutStartSec=900
|
||||
# /run/imsd.env: written by ims-pdn-up.sh (DEV= the connected ims netdev,
|
||||
# LOCAL= the PDN address ModemManager assigned to it, PCSCF_DISCOVERED= the
|
||||
# P-CSCF list the network put in the PDN's PCO, read from the modem).
|
||||
# P-CSCF list the network put in the PDN's PCO, read from the modem,
|
||||
# PCSCF_DISCOVERY= how that list came to be: found, none, failed, skipped;
|
||||
# or IMSD_IDLE=<reason> alone when the bring-up found nothing to do).
|
||||
# /etc/imsd.env: admin configuration, none required where discovery finds a
|
||||
# P-CSCF (AGENTS.md §10, the README); it wins over the runtime-derived values
|
||||
# — PCSCF= overrides discovery.
|
||||
|
|
|
|||
Loading…
Reference in a new issue