Repository navigation
Conversation
Replace unconditional failure and shared task completion with a precision-checked kernel NTP query and independent SDK event lifetimes. Preserve real failure results and release the guest core across contended service locks.
Translate the Horizon IPv4 socket ABIs, retain generation-safe descriptor lifetimes and bound local discovery. Publish real guest contexts across blocking hooks using the existing per-thread gate stack. Add LDN discovery, membership and lifecycle plus local play-history registration from privately imported resources. Preserve upstream offline account and TLS boundaries. Synthetic and real loopback fixtures, ASan/UBSan, the full host suite, the production HLE registry, iPhoneOS compilation and synthesized resource-decoder tests pass. Mario Kart 3.0.5 hardware results are owner reports from the originating fork, documented separately from this rebased branch. No frontend, Nextendo integration or game/firmware data is included.
Keep the test thread heap-owned like production, release its handle after joining, and include it in the local-network test target. Stop socket fixtures at failed prerequisites rather than continuing with invalid descriptors.
Add the system permission description required by the native socket transport. Remove redirect-only resolver bookkeeping from the upstream local-only backend; no routing provider or UI is included.
…y copyout Kernel recvfrom copyout cannot invoke the guest dirty-page signal hook. Prepare the pending span before each receive attempt, including after readiness waits and partial stream reads, without retrying EFAULT or admitting unrelated invalid memory. The real dirty-tracker fixture passes 43 checks normally and under ASan/UBSan. Negative controls reproduce missing preparation and the wait boundary. The full host suites, iOS compile check and documentation checks also pass.
…e copyout The kernel getsockopt copyout cannot invoke the dirty-page fault hook. Prepare the validated result span without changing SetSockOpt inputs, invalid-memory failures, result sizes or errno translation. The expanded real dirty-tracker fixture reproduces 16 failures in 87 checks before this change and passes all 87 normally and under ASan/UBSan afterward. This synthetic reproduction does not establish a game disconnect cause.
Use owned DNSService queries and cancellation identities instead of leaving resolver work and late completions alive. Retire native references on one serial queue, fence finalization, and retain the offline and synchronous resolver paths.
Use owned DNSService queries and cancellation identities instead of leaving resolver work and late completions alive. Retire native references on one serial queue, fence finalization, and retain the offline and synchronous resolver paths.
Keep per-session read-only snapshots and scoped eligibility checks behind a default-off provider hook. Exercise startup, delivery lifetime and sanitizer failures with original offline fixtures; compose the TLS dependency without replacing upstream filesystem behavior.
Replace unconditional failure and shared task completion with a precision-checked kernel NTP query and independent SDK event lifetimes. Preserve real failure results and release the guest core across contended service locks.
Check Foundation fixture construction and bounded DER reads explicitly, initialize the filesystem fixture handle, and remove a fixed filesystem null-argument baseline entry. No new suppression or baseline expansion.
…tely Ordinary-buffer TLS, policy and resolver tests use the explicit no-tracker fixture hook. The real socket-copyout fixture keeps its registered dirty tracker and links the NSD backend needed by this stack.
I preserved upstream's readback allocation expectation and the existing explicit serial-table release. The LAN/LDN transport and tracked socket copyout corrections remain unchanged. Local network fixtures, 87 normal and sanitized copyout checks, 61 Metal geometry checks, and the documentation tests pass after the merge.
…stream-online-backend-proposal # Conflicts: # dm/host/Makefile # dm/host/hle_net_backend.c # dm/host/tests/Makefile # docs/agents/symptoms.md # docs/reference/services.md
…stream-online-backend-proposal
…stream-online-backend-proposal
…stream-online-backend-proposal # Conflicts: # dm/host/tests/Makefile
…stream-network-clock-main # Conflicts: # docs/agents/symptoms.md
hle_oe.c's dm_set_opmode calls dm_hid_opmode_changed and oe::ExecuteProgram calls dm_host_shutdown_then; the test stubs them as t_notify_sched.c does.
…e_account.c expects hle_account.c keeps main's SDK-built AsyncContext, so there is no constructor hook to call: the test's constructor writes nothing, as the SDK's writes only guest memory the host never reads. Added checks: a move constructor writes the vtable and a null IPC pointer and carries the request; with Nextendo off a context answers as the offline host does.
…c's too The check wanted cdevice=Mac..., but a virtual Mac (a CI runner) reports VirtualMac2,1. It now compares with hw.model, as save_sync.c device_name reads it.
…uncher The online backend from seedds#961 needed an account and a route profile that the repository did not provide. This adds them: - Sign-in (nextendo_login_apple.inc, MIT): OAuth with PKCE and a loopback redirect, the refresh token in a private 0600 file, the game token from /api/nex-token, /api/online-status reasons on refusal, and an /api/oauth/userinfo check that the game token belongs to the signed-in account. The Mac uses dm/tools/nextendo_login; the iPhone signs in on the launcher's settings screen (NextendoSignIn.m). The OAuth client ID is a build setting (DM_NEXTENDO_CLIENT_ID, written into Info.plist as DMNextendoClientID); without one nobody can sign in, and the error says so. - Nextendo's signed patch catalog (dm/tools/nextendo_patches.py, checked with nextendo_verify.swift): when it lists the exact main build, a patched copy is rewritten into patched/nextendo and mapped only in Nextendo mode (loader.c, gen_embed.py module nmain). The public service profile comes from dm/net/nextendo-services.json (nextendo_services.py). - Account: nickname, country and birthday from /api/me with launcher fallbacks, online membership, the BaaS id token with the NEX proof. - Friends and presence: SetDescription runs the SDK's own setter, presence is posted again every 45 s, refused API requests log their status. - SSL: the provider CA answers built-in certificate requests when the player has no firmware store; setup failures and built-in requests are logged once. - nifm::GetCurrentProxySetting answers "no proxy" in a network mode. - Launcher: Network row (Nextendo when built with --nextendo), Online section, player name, birthday picker. - Super Mario Bros. Wonder 1.2.1: grpc_error_create observation gate.
…ceives are no datagram yet A bound, unconnected UDP socket the game closes stays open for 5 s, and a bind to its port in that time takes it back, so datagrams that arrive in between are not answered with port unreachable. A refused or reset error on a UDP receive, which the host raises for an earlier send's ICMP and Horizon does not, counts as no datagram yet. Both only in Nextendo mode. Neither has been seen in a log yet; they follow how peer-to-peer play is expected to behave on a console.
…nline is the found-on The account section still said the flow and client ID were Mush8's and the files GPL-3.0, and three found-on lines said Wonder online was only planned.
eyasbzour
added a commit
to eyasbzour/nxnative
that referenced
this pull request
Nov 13, 2026
…eded The survey docs still said no PRs go to seedds and that Mush8 code may be ported. The descriptor table, EventFd and NPLN gaps are filled by seedds#2589, the client ID is a build setting, and the Mario Kart menu sites move to a kart branch check.
eyasbzour
added a commit
to eyasbzour/nxnative
that referenced
this pull request
Nov 13, 2026
…eded The survey docs still said no PRs go to seedds and that Mush8 code may be ported. The descriptor table, EventFd and NPLN gaps are filled by seedds#2589, the client ID is a build setting, and the Mario Kart menu sites move to a kart branch check. (cherry picked from commit 500a94b)
…t; a send that met an ICMP report is sent again Review fixes for the previous commit. Giving the bind the parked host socket dropped every option the game had set on its new socket and ignored the bind address, so a bind now closes the parked socket and binds the new one. On the host, an earlier datagram's ICMP report also comes back from the next send on a UDP socket, which then sends nothing; in Nextendo mode that send is made again once.
- One refresh at a time, across threads and processes (flock on a file in the sign-in's folder), so a refused old refresh token can no longer delete the newer one another caller just saved. - Sign-in uses the code's own access token for the game token instead of refreshing again right away. - The loopback listener's answers set SO_NOSIGPIPE: a browser preconnect that closes early no longer raises SIGPIPE. - A failed refresh keeps the current game token until its own expiry instead of dropping it. - GetNickname is a wait hook, and callers waiting behind a fetch release their guest core. - /api/me is fetched once per account and shared by the BaaS ID, country and birthday readers; a failure is remembered for 60 s. - Presence requests go out one at a time, so a repeat never lands after a newer publish (status 0 stays last). - services.md: the iPhone carries Nextendo's patched main; hle_net.c's proxy comment matches the game notes.
A slow CI runner stretched the test's 2 s of short sleeps past the 20 s alarm: this branch's tests run 43530203751 stopped there twice in both the plain and the ASan job, with nothing in the log to say which case. The guard is a hang check, not a timing check (the cases count back-offs on poll_clock), so it is now 60 s, and stdout is unbuffered so a timeout names the case.
Mario Kart 8 Deluxe 4.0.1 signed in on an iPhone, then failed its online menu with 2306-0502: the TLS check to g2b309e01-lp1.s.n.srv.nintendo.net refused the certificate. Nextendo's second server presents a self-signed certificate there (CN=nintendo-private-server, no host name, no chain to the provider CA), the same one for g2df33d01-lp1.p.srv.nintendo.net. Both routes are now pinned-exact with that certificate (dm/net/nextendo-pins/<SHA-256>.der, named by certificateSHA256 in nextendo-services.json), and the server's address gets the paired pinned-address route. hle_nextendo_x509.c already accepts a pinned leaf only byte for byte, with its dates, purpose, key and self-signature checked. nextendo_services.py writes the pinned rows and copies the certificate into the profile; build_ipa.sh and main.m carry every certificate file of the profile.
# Conflicts: # dm/host/tests/t_save_sync.c
…ds (main's 16af055 left it out of the table)
# Conflicts: # docs/env_switches.tsv
…nt API with the access token A NEX login refusal (Mario Kart 8 Deluxe 2306-0807) carries no reason. The host now asks /api/online-status with the sign-in's OAuth access token once after the first game credential and again when the game shows 2306-08xx, and logs the answer. /api/me refused the nx2 game token with 401, so the account API takes the access token first and falls back to nx2.
Every minute of a Nextendo session the host sends the account's total for the game plus this run's time to /api/history. The account's Mii from /api/profile joins the game's Mii database when its data CRC holds. The presence's app_detail is the game's own SetDescription text, read from the application field.
# Conflicts: # dm/host/hle_time.c # docs/agents/symptoms.md # docs/reference/services.md
The game token's expiry and the id token's iat, nbf and exp were judged and stamped by the device's clock. The sign-in now keeps Nextendo's clock less the device's from the Date header of each answer, uses it for both, and logs an offset of a minute or more.
…t does nothing Nextendo's start-of-session work (the account's Mii, play time) begins on a background thread while the modules load, before svc_thread_main_init made g_keythr. pthread_getspecific(0) on Darwin is the thread's own pthread structure, never NULL, so the wait took that thread for a guest thread and changed the host's thread state under its lock: Wonder controls-9 stopped at a different place in each run (a black screen).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This builds on #961 (RobiiAragon's opt-in online backend), which is still open, so until #961 merges the diff shows its roughly 10k lines too. The commits that are new here:
b8e25957Serve gRPC's name lookup and RecvMsg/SendMsg in a network modeaa03bca5Set the game's own errno on a failed socket call; RecvMsg/SendMsg edge cases8aace781Merge seedds/main (465 commits; notes below)51c46eceRegenerateddocs/agents/symptoms.mdf1afbb84EventFd in a network mode is a slot of the transport's socket table8a36c156,e3fa880aMerge seedds/main again (no conflicts)e6d91b7fThe BCAT title IDs come from the build instead of the host source (test_no_title_ids_in_the_host), and three tests define the clock and wait symbols that main'shle_os.candhle_thread.cnow provideae3a5f02iOS online: the game is told the address of the adapter the internet route uses, not the first one listedfb1d0b3e,b8bec5a9,cb5bdacdTests that broke after merging main:t_oe_handoffstubs whathle_oe.cnow calls,t_nextendo_accountlets the SDK build the AsyncContext, andt_save_synccompares the device withhw.model(a CI runner is aVirtualMac2,1)9b0ae7b9Merge seedds/main67e62a1bNextendo online: sign-in, signed game patches, service profile and launcher7f9e8736Nextendo mode: keep a closed UDP port briefly; ICMP reports on UDP receives are no datagram yet7ab7b40dservices.md: the sign-in is MIT with a build-time client IDfc73a5a4,72fe137bReview fixes: the UDP rebind and send, one refresh at a time (flock), no second refresh at sign-in, SO_NOSIGPIPE on the loopback listener, a failed refresh keeps the current token until it expires, GetNickname as a wait hook, one /api/me fetch per account, presence requests in ordercd11b50ft_pollwait: a 60 s hang guard and unbuffered PASS lines. A slow runner stretched its short sleeps past the 20 s alarm twice in a row on this branch, in both the plain and the ASan jobc1f67b53Service profile: Mario Kart 8 Deluxe's NEX server presents a self-signed certificate, and its routes now accept exactly that certificate17ad015aMerge seedds/main (t_save_synctakes main'shw.modelcheck)931bc841t_mii_sysdata, new on main, includeshle_fs.c, which weak-imports Propose opt-in online service backends and BCAT delivery #961's BCAT provider. It now includesbcat_absent.h, ast_fs_stub.cdoes, so it linksde960fd2Merge seedds/main (no conflicts)3798ea9e,3c7b6af5A row forDM_EXFAT_DIR, which main's16af055bread without one, then a merge of seedds/main that keeps main's own row from210acc66c5188fcaThe host asks Nextendo why it refuses online play, and the account API uses the OAuth access token (/api/meanswered 401 to the nx2 game token)22bfea82Merge seedds/main (no conflicts)407d44c7The account's play time, Mii and activity on Nextendo7a51a148docs/agents/symptoms.mdregeneratedeb3bdbf9Merge seedds/main. Main's The network clock reads the host's clock, as the user clock, so saves dated by it are no longer dated 1969 #2359 and Propose opt-in online service backends and BCAT delivery #961 both took over the network clock'sGetCurrentTimeandIsAccuracySufficient(below)6f7ff687The game token's expiry and the id token'siat,nbfandexpare judged and stamped by Nextendo's clock: the offset comes from theDateheader of Nextendo's answers, and a device clock a minute or more off is logged3df926e0A host thread's native wait before the guest threads exist does nothing. The start-of-session work in407d44c7began on a background thread while the modules loaded, beforesvc_thread_main_initmadeg_keythr, andpthread_getspecific(0)on Darwin is the thread's own pthread structure, never NULL. The wait took that thread for a guest thread, and Wonder stopped at a different place in each run. Our bug, found on a device build of this branch's code1dd8f85aMerge seedds/main (no conflicts)Why
Games whose online code is NPLN (Super Mario Bros. Wonder and other recent titles) talk to their servers over gRPC, which links its own OpenSSL and does TLS itself over the C socket interface. With #961 alone, Wonder's online menu fails with 2321-4992 (gRPC's UNAVAILABLE) at four separate points. Each fix below sits at the SDK level, with no game offsets in the code, so it applies to any game that makes the same calls.
nnsocketGetAddrInforeturned EAI_FAIL whenever an SDK exports both C++GetAddrInfooverloads, and Wonder's does. The C call takes the plainaddrinfolayout. In Wonder 1.2.1's SDK it branches straight to that overload (sdk+0x1481c0), so the C resolver now uses that layout whenever the SDK has it.gRPC's TCP endpoint reads and writes only through
RecvMsgandSendMsg, which were unsupported. They now scatter and gather through one host buffer with Horizon's 0x30-bytemsghdr. A message over 16 MiB moves in part on a stream socket, as a real socket may, and fails with EMSGSIZE on a datagram one. A datagram longer than the iovecs is cut and reported withMSG_TRUNC. Ancillary data is refused rather than dropped.On Horizon,
nn::socket's errno is the C library's:detail::SetLastErrnowrites*__errno_location(). The host kept only its own copy. gRPC sets EAFNOSUPPORT itself when it falls back from IPv6, then reads errno after a non-blocking connect, so it took that stale value for EINPROGRESS and dropped the address. Every socket hook that fails now also writes the game's errno, andGetLastErrnoreads it.gRPC wakes its pollers with an event descriptor and polls it together with its sockets. In a network mode
EventFdfailed, and main's offline table numbered its descriptors from 0, the same as the transport's sockets. An event descriptor is now a slot in the transport's socket table, backed by a local socket pair, so Poll and Select see sockets and event descriptors together. Offline nothing changes.On iOS,
net_routehas no routing table to read, so the adapternifmreports was the first IPv4 onegetifaddrslists. With mobile data on, that is usually the cellular one, even while the phone plays over Wi-Fi. Pia gives the addressnifmreports to the other players as its local candidate for a direct link. In a Nextendo session the adapter is now the one whose address a UDP socket connected past the local networks gets (connecton UDP sends nothing). If no adapter has that address, any adapter is used, as before. The chosen adapter's name is logged when it changes.Name lookups and resolver options are logged once, in red when they fail, so the next game that stops before its first socket shows why.
What a player needs to go online
#961 takes the account from a credential provider the app installs and routes only the hosts a service profile names. Neither existed in the repository, so nothing could sign in. With these commits a build made with
--nextendoworks once the player signs in. NEX games (Mario Kart 8 Deluxe) and NPLN games (Wonder) go through the same code.dm/host/nextendo_login_apple.inc, MIT): OAuth with PKCE and a127.0.0.1redirect, the refresh token in a private 0600 file, and a game token from/api/nex-tokenwhen a game asks. Refreshes take a file lock, so the Mac tool, the launcher and the game never spend the same refresh token twice. When Nextendo refuses an account,/api/online-statussupplies the reason, which becomes the error text./api/oauth/userinfomust name the same account as the game token. On the Mac,make -C dm/host nextendo_loginand thendm/host/nextendo_login sign-in. On the iPhone, the launcher's Online section opens Nextendo's page in anASWebAuthenticationSession, and a listener in the app takes the loopback redirect. No token is logged.DM_NEXTENDO_CLIENT_ID. The Mac reads it from the environment, andbuild_ipa.shwrites it into the app's Info.plist asDMNextendoClientID. A build without it cannot sign in, and the error says so. This repository ships no client ID. Whoever publishes builds registers a public application at nextendo.network/developers, and Nextendo's sign-in page shows that application's name.dm/tools/nextendo_patches.py(after NX Port Studio, MIT) fetches Nextendo's signed patch catalog and checks its Ed25519 signature withnextendo_verify.swift. If the catalog lists the exact main build (all 32 bytes of the build ID) and every guarded byte matches, it patches a copy of main. The host maps that copy only in Nextendo mode (loader.c; on the iPhone it is modulenmain), so offline and LAN play keep the original. Wonder 1.2.1 has such a patch. Mario Kart 8 Deluxe 4.0.1 is listed without one.dm/tools/nextendo_services.pywrites the public profile fromdm/net/nextendo-services.json: routes, the provider CA checked by SHA-256, and the manifest. When the player has no firmware certificate store, that CA answers the SSL service's built-in certificate requests. A game whose own TLS takes its roots from the store (Wonder's gRPC) then needs nothing beyond the sign-in.g2b309e01-lp1.s.n.srv.nintendo.netandg2df33d01-lp1.p.srv.nintendo.net) presents a self-signed certificate, CN=nintendo-private-server, with no SAN. No CA in the profile issues it, so the SSL service'sverify_peerfailed and the game showed 2306-0502. Those rows are nowpinned-exactroutes carrying the certificate's SHA-256, and the DER file sits indm/net/nextendo-pins/.dm_nextendo_x509_verify_pinfrom Propose opt-in online service backends and BCAT delivery #961 accepts the connection only when the peer sends that exact DER, inside its validity dates, with a valid self-signature and a strong enough key. Every other host still goes through normal verification. Mush8 accepts any certificate from that server; this accepts one./api/online-statuswith the sign-in's access token, once after the first game credential and again when the game shows a 2306-08xx error, and logs the answer in words. The access token stays in memory and is never logged./api/me. That endpoint takes only a session or OAuth access token and answered 401 to the nx2 game token, so the account API now sends the access token from the sign-in's last refresh, and the game token only when there is none. The launcher fills in a country or birthday when the account has none, and the player can type a name of up to 10 characters. The game also gets the online membership and the BaaS id token with the NEX proof.UserPresence::SetDescriptionruns the SDK's own setter. Presence is posted again every 45 s while a game has published one, because Nextendo stops showing a player whose presence stops arriving. A refused friends or presence request logs its HTTP status./api/profile) joins the console's Miis in the game's Mii database when its data CRC holds and no console Mii has its create ID. Nothing is uploaded. The presence'sapp_detailis now the game's ownSetDescriptiontext, read from the application field (key and value strings in turn, undersys_description, as the SDK'sPresenceAccessorlays them out).nifm::GetCurrentProxySettingsucceeds with every field zero in a network mode, the same as a console with no proxy. Wonder's NPLN code returns an error when it fails.--nextendobuild, where it is the default) and an Online section with the sign-in and a birthday picker. The Network row is now shown for every game.patches.pyputs an observation gate on gRPC'sgrpc_error_create, because Nintendo's gRPC has its logging compiled out.The sign-in was first written in the fork after Mush8 (GPL-3.0). It has since been rewritten under MIT from Nextendo's public OAuth interface. Mush8, Ryujinx-Nextendo and NX Port Studio were used for facts: which endpoints Nextendo has, the token shapes, and which calls a game makes. No code was taken from Mush8.
UDP in Nextendo mode (untested)
7f9e8736andfc73a5a4are separate because no log has shown they are needed yet. When a game closes a bound, unconnected UDP socket, the host socket stays open for 5 s. Without this, datagrams that arrive in between are answered with ICMP port unreachable, where a console would drop them. A bind to that port in that time closes the parked socket and binds the game's new one, so the options the game set on it stay. The host also hands an earlier datagram's ICMP report to the next call on a UDP socket, which Horizon does not do. On a receive it counts as no datagram yet. On a send, which then sent nothing, the datagram is sent again once. All of this applies only in Nextendo mode, for peer-to-peer sessions such as Pia's.Merging main
Main's offline socket layer (the event descriptor table, Select as Poll over its sets, Star Fox's SDK 21 overloads) now sits under #961's transport, and the SDK 21 overloads go to the transport in a network mode. Main's ssl stubs stay out because #961's
hle_ssl.cserves those calls. The SDK 4GetPeerNameis registered once.hle_account.ckeeps main's SDK-builtAsyncContext: there is no constructor takeover, a context with Nextendo off answers as main's does, and a move constructor copies the vtable.hle_bcat.cdropsHLE_FORCE_BIG, since main now runs every takeover on the host stack.The network clock is split between the two. Main's #2359 reads the host clock in
GetCurrentTimeandnow(), so a game that dates its saves by the network clock gets the real date (Divinity: Original Sin 2 dated them 1969). #961 hadGetCurrentTimefail unless the kernel's NTP state was synchronized within a second. A Switch reads its network clock unsynchronized too, so main's version stays, andt_network_clockno longer checks the getter.IsAccuracySufficientand the availability task keep #961's kernel NTP check rather than main's fixed "no", since the host can tell whether its clock is synchronized. If you'd rather keep main's "no" there, it's a one-line change. This also changes offline play, not only a network mode: on main,AsyncEnsureNetworkClockAvailabilityTaskalways finishes with a failure, and with #961 it succeeds whenever the device's clock is synchronized. A game that checks it offline takes the "clock available" path (#961's author found Splatoon 2's calendar warning gone with it).Tested
t_netcovers the gathered and scattered round trip, EMSGSIZE,MSG_TRUNC, the resolver layout, the game's errno on a failed call and on an empty event read, mixed Poll and Select over sockets and event descriptors, and the proxy answer.make -C dm/host/tests nextendo-accountalso compiles the Mac sign-in tool.test_nextendo_patches.pyandtest_nextendo_services.pycover the catalog checks and the profile. Tests workflow on the fork for this head, every job green: https://github.com/eyasbzour/nxnative/actions/runs/43613758974Not in this PR: VPN play and the local-wireless room broadcasts, the fork's networking diagnostics ring, and the fork's build workflow (which sets the client ID).