SUNRISE

From launch to orbit

One boot runs from the first HTTP request to the orbit screen. At each step the game sends a request, and Sunrise answers it from a known place in the source.

The game’s own boot steps are in Bootflow. The wire protocols are in Network stack.

Summary

  1. The game posts a SignOn request. Sunrise answers it inside the game process.
  2. The SignOn answer names 127.0.0.1 and a port. The game opens a TCP connection there.
  3. The Sunrise BAP listener takes the connection. Services 30 and 25 set up the link.
  4. After service 26 every frame is encrypted. The link reaches _connected.
  5. The game asks for its account over the link. Sunrise answers from its local database.
  6. The game walks its boot steps to orbit.

How the traffic reaches Sunrise

Sunrise is a DLL that runs inside the game. The server runs in the same process, on its own thread. There are two paths into it.

HTTP: SignOn

The game sends its HTTP requests through one internal request executor. Sunrise replaces that executor (src/client/hooks/network/http/http_descriptor_route.cpp).

  • A POST goes to the server’s HTTP consumer (src/server/http/server_http.cpp).
  • The consumer answers any URL that contains /SignOn. It writes the reply straight into the game’s response buffer. No bytes go on the network.
  • Any other route is refused and logged as ev=http stage=route result=unmapped. A request that is not a POST is refused the same way.
  • When the external server setting is on, the executor rewrites the URL host and lets the game’s own request go out. See Settings, logs and in-game tools.

The BAP link is a real TCP connection. Sunrise runs a listener on the loopback address (src/server/transport/bap_listener.cpp).

  • It binds 127.0.0.1 on the BAP port. The default port is 30974. The same setting feeds the SignOn reply, so both always agree.
  • It holds up to 8 connections at once (src/client/network/consumer.h). The main link and each Activity Host link take one slot.
  • It is nonblocking. The server thread services it in short slices.
  • Each connection gets its own session slot (src/server/bap/bap_route.cpp). Closing the socket clears the slot.

The game’s own BAP code does all the client work. Sunrise does not patch the connect. The address comes from the SignOn reply, as it did on the live service.

SignOn

SignOn is one HTTPS POST with a protobuf body. The game reports its state and asks for a session.

The request

Sunrise does not read the request body. It checks only the URL. For reference, the request carries these fields, among others:

fieldholds
1protocol version, 16 on this build
2the platform credential, a Steam ticket on PC
5, 6the content version and build strings
15the entitlements the client owns
17the SKU name
20a failure report, only after a failed attempt
22the account id, name and country

The response

The reply is built in src/middleware/signon/response.cpp. Field 1 selects the branch. Sunrise always sends 0, success.

fieldSunrise sends
1 response_type0, success
2 successthe sub-message below
4 client_configid 1, silo 1, and the content configuration blob
5 silo_id1
6 content_build_version"d2legacy"
8 capability_flags0
10 owned_entitlementsthe entitlement rows from the local database
12 server_timethe server clock, Unix seconds
13 environment"live"
14 external_ip127.0.0.1, the client’s own address

The success sub-message:

fieldSunrise sends
116 zero bytes
2, 316 random bytes each; the client uses them to open the service 26 reply
4 session_token32 random bytes
5the token expiry, Unix seconds
6 relay_ip127.0.0.1
7 relay_portthe BAP port
12 extendedone network id in its field 1

Rules:

  • Fields 1 to 7 of success are required. The client refuses the whole reply if one is missing.
  • Sunrise makes the random values when it starts (src/state/runtime/state_runtime.cpp). They are never written to disk.
  • The client config blob in field 4 lets the client register its content packages. Its contents are not described here.
  • A successful SignOn also records the sign-in time. The characters later report it as their last daily and weekly reset.

BAP is the platform link. Every service on it uses the same framing. All integers are big-endian.

Framing

Each frame is a 6-byte outer header, then an inner header, then the body. The frame type says whether the rest is plain or encrypted. The codec is src/middleware/bap/bap_frame.cpp.

struct BapOuterHeader {        /* 6 bytes */
    uint8_t  magic;            /* +0x00, always 1 */
    uint8_t  frame_type;       /* +0x01, 0 or 2 = plaintext, 1 = encrypted */
    uint32_t length;           /* +0x02, bytes after this header */
};

struct BapRequestHeader {      /* 6 bytes, inside the payload */
    uint16_t service;          /* +0x00, service id */
    uint32_t task;             /* +0x02, correlation id */
};

struct BapResponseHeader {     /* 8 bytes */
    uint16_t service;          /* +0x00, response service id */
    uint32_t task;             /* +0x02, the request's task, echoed */
    uint16_t status;           /* +0x06, must be 200 */
};

Rules:

  • A request uses the 6-byte header. A reply uses the 8-byte header with status 200. Any other status is fatal to the link.
  • A reply echoes the request’s task id, including 0. The client matches replies to requests by the pair (response service, task).
  • A server push, such as service 123, uses the 6-byte header and has no status. Sunrise sends task 0 in it.
  • length must cover the payload exactly. Sunrise refuses a frame with extra bytes.
  • In an encrypted frame the payload is a 16-byte authentication tag, then the ciphertext. The ciphertext holds the inner header and the body.

The handshake

The game drives the link through a fixed state machine. Sunrise only answers.

/* One BAP link, as the game drives it. The comments say what Sunrise answers. */
tcp_connect(signon.relay_ip, signon.relay_port);   /* state 1 _acquiring_server */
set_tcp_nodelay();                                 /* state 2 _connecting */

send(30, nonce_128_bytes);                         /* state 4 _channel_starting */
recv(31);           /* Sunrise echoes the 128 bytes; the client checks only the length */

send(25, { session_token, link_kind });            /* state 5 _authenticating */
recv(26);           /* an 84-byte sealed envelope with this link's frame key */
/* from here on, every frame in both directions is frame type 1 */

send(121); recv(122);                              /* state 6, empty reply */
send(302); recv(303);                              /* state 7, empty reply */
/* states 8 and 9 exchange a Steam relay certificate; they run only on the relay path */

for (;;) {                                         /* state 10 _connected */
    send(250); recv(251);                          /* keepalive */
    /* ... every other request and push ... */
}

The two plaintext services are handled in src/server/bap/plaintext.cpp.

  • Service 30 -> 31. Sunrise sends the request body back unchanged. It is always 128 bytes.
  • Service 25 -> 26. The body echoes the SignOn session token and names the link kind: 1 for the main link, 2 for an Activity Host link. Sunrise checks the shape and the token and logs a mismatch, but it answers every service 25. A refused hello would leave the link stuck in _authenticating.

The secure channel

The channel is encrypted after services 25 and 26.

  • The service 26 reply is a sealed envelope. Only a client that holds the SignOn reply can open it.
  • The envelope gives the client a frame key for this one connection. Sunrise makes a fresh key and nonce for every service 25 (new_bap_session in src/state/runtime/state_runtime.cpp), so no two links share one.
  • Frame type 1 uses AES-128-GCM (src/middleware/secure_channel/encrypted_frame.cpp). Each direction keeps its own counter. A counter moves on only after a frame is complete.
  • The tag covers the ciphertext only. The outer header is not covered.

The service table is src/server/bap/encrypted/routing/bap_service_routing.cpp. The main services:

request -> replywhat it isSunrise answers
10 -> 11web service RPCsee Accounts, characters and saves
110 -> 112web service RPC, server rolesame codec as 10
12 -> 13subscribe to a queuez familyempty reply, then the family’s first snapshot on 123
14 -> 15unsubscribeempty reply
6 -> 7ask for an Activity Hostsee The Activity Host
16 -> 17ask where an Activity Host livesthe same loopback address and port
8message to the Activity Hostno reply; answers go out as pushes on 9
23 -> 24translate an id to a SOIDthe account SOID, or a session id on an Activity Host link
121 -> 122, 302 -> 303link setupempty reply
250 -> 251keepaliveempty reply

Server pushes have no request:

pushcarries
9one message from the Activity Host
123one or more queuez family updates

Rules:

  • Every request that has a response service gets an answer. A body that fails to decode is still answered, with an empty body. The client matches only the head of its pending queue, so one unanswered request blocks every later reply.
  • A service Sunrise does not know gets no reply and no error. An error would drop the link.

From sign-in to orbit

The boot is a ladder of numbered steps. Most of them run inside the client. This table lists the steps between package registration and orbit, and what each one needs from Sunrise. SignOn has already run when these steps start.

#stepwaits forSunrise answers
21package_registrationthe content packages to registerthe content configuration in the SignOn reply
22bap_signinthe BAP link to reach state 10 _connectedservices 30, 25, 121 and 302
23investment_signinthe ws opcode 503 reply, then a family 4 snapshot keyed by the account SOIDthe 503 reply and the family 4 push
24profile_setupthe settings screens and a selected characternothing; the screens are local
25prepare_for_orbita slice-set load and local port setupnothing
26rejoin_activityaccount data, read when the step startsnothing new; it reads the family 4 data
27character:signinthe family 3 roster, then the character pickthe ws opcode 206 answer and the ws opcode 504 reply
29setup:orbitthe fireteam goal; it hands on to activity setupthe Activity Host services

Notes on the steps that need the server:

  • Step 23. The client sends one request here, ws opcode 503. The step clears when the client holds a family 4 object keyed by the SOID in that reply. Both answers are described in Accounts, characters and saves.
  • Step 26. If the account data is absent when this step starts, the step does nothing and reports no error.
  • Step 27. The family 3 roster object holds the character count. A count of zero sends the client to character creation instead of character selection.
  • Step 29. This is not the end. It hands on to setup:activity_session_creation, which starts the flow in The Activity Host.

Step 28 cleanup is not in the table. Most failures are reported there. It is not a step on the way to orbit.

Where to look when a boot stops

symptomlook for
SignOn never answeredev=http method=post route=signon lines
stuck in bap_signinev=transport stage=accept and ev=bap svc=25 lines
link drops right after connectev=bap svc=none stage=decrypt result=fail
stuck in investment_signinev=queuez lines, most of all stage=snapshot result=empty family=4
all slots takenev=transport stage=accept result=full

Log locations and levels are in Settings, logs and in-game tools.

Open questions

  • Which boot step sends SignOn is not verified.