Skip to content

Commit b06bac8

Browse files
shcherbakasp24
andauthored
Synapse envoy route hashing (#23)
* Configure route-specific Envoy hashing * Add whoami-based Envoy hashing tests * Route client-reader endpoints through Envoy * Route federation endpoints by source IP * Move hash fallback logging into Lua helper * revert 1.1.10 routing * introduce masterOverridesRoutes * reorderign of typing stream writer * reordering of to_device stream writer * reordering accountData and receipts stream writers * reordering for presence stream writer * introducing new streamwrites deviceListsRoutes, pushRulesRoutes * remove unused generics * fix-pdb-for-singletons * fix thread subscriptions --------- Co-authored-by: asp24 <488588+asp24@users.noreply.github.com>
1 parent 38e49ad commit b06bac8

13 files changed

Lines changed: 1329 additions & 263 deletions

Makefile

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
VERSION=v1.149.1
22

3+
.PHONY: all build-main push-main build-e2e push-e2e test-lua
4+
35
all: build-main push-main build-e2e push-e2e
46

57
build-main:
@@ -13,3 +15,6 @@ build-e2e:
1315

1416
push-e2e: build-e2e
1517
docker push ghcr.io/code-tool/matrix-stack/synapse:${VERSION}-e2e-optimized
18+
19+
test-lua:
20+
lua charts/synapse/scripts/synapse_test.lua

charts/synapse/Chart.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
---
2+
apiVersion: v2
23
name: synapse
4+
description: matrix synapse kubernetes deployment
35
version: 1.2.10
6+
appVersion: 1.149.1

charts/synapse/scripts/envoy.yaml

Lines changed: 384 additions & 15 deletions
Large diffs are not rendered by default.

charts/synapse/scripts/synapse.lua

Lines changed: 204 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,230 @@
1-
local function get_hash_key_from_path(path)
1+
local room_id_pattern = "(![A-Za-z0-9._=%%%-/]+:[A-Za-z0-9.%-]+)"
2+
local whoami_cache = {}
3+
4+
local function normalize_matrix_room_separators(path)
5+
return path:gsub("%%21", "!"):gsub("%%3[Aa]", ":")
6+
end
7+
8+
local function get_room_id_from_namespace_path(path, namespace)
9+
local _, _, room_id = string.find(path, "^/_matrix/" .. namespace .. "/.-" .. room_id_pattern)
10+
11+
return room_id
12+
end
13+
14+
local function get_room_id_from_path(path)
15+
local normalized_path = normalize_matrix_room_separators(path)
16+
17+
local key = get_room_id_from_namespace_path(normalized_path, "client")
18+
if key ~= nil then
19+
return key
20+
end
21+
22+
key = get_room_id_from_namespace_path(normalized_path, "federation")
23+
if key ~= nil then
24+
return key
25+
end
26+
227
local _, _, key = string.find(path, "/_matrix/client/v3/rooms/([^/]+)/messages")
328

429
return key
530
end
631

7-
local function parse_username_from_token(token)
8-
local _, _, username = string.find(token, "[^_]+_([^_]+)_.*$")
9-
if username ~= nil then
10-
return username
11-
end
32+
local function get_room_id_from_request(headers)
33+
local path = headers:get(":path")
1234

13-
return token
35+
return get_room_id_from_path(path)
1436
end
1537

16-
local function get_auth_token(auth_header, path)
38+
local function get_access_token(auth_header, path)
1739
if auth_header ~= nil and string.len(auth_header) > 0 then
18-
return parse_username_from_token(auth_header)
40+
local _, _, bearer_token = string.find(auth_header, "^[Bb]earer%s+(.+)$")
41+
if bearer_token ~= nil then
42+
return bearer_token
43+
end
44+
45+
return auth_header
1946
end
2047

2148
local _, _, token_param = string.find(path, "access_token=([^&]+)")
2249
if token_param ~= nil then
23-
return parse_username_from_token(token_param)
50+
return token_param
2451
end
2552

2653
return auth_header
2754
end
2855

29-
local function get_hash_key_from_request(headers)
56+
local function get_access_token_from_request(headers)
3057
local path = headers:get(":path")
3158

32-
local result = get_hash_key_from_path(path)
33-
if result ~= nil then
34-
return result
59+
return get_access_token(headers:get("authorization"), path)
60+
end
61+
62+
local function log_hash_fallback(request_handle, headers, fallback_type, request_id)
63+
request_handle:logWarn(
64+
"synapse_envoy_" .. fallback_type .. "_hash_fallback: method="
65+
.. tostring(headers:get(":method"))
66+
.. " path="
67+
.. tostring(headers:get(":path"))
68+
.. " authority="
69+
.. tostring(headers:get(":authority"))
70+
.. " request_id="
71+
.. tostring(request_id)
72+
)
73+
end
74+
75+
local function set_request_id_hash_key_with_fallback_log(request_handle, headers, fallback_type)
76+
local request_id = headers:get("x-request-id")
77+
78+
log_hash_fallback(request_handle, headers, fallback_type, request_id)
79+
headers:add("X-Hash-Key", request_id)
80+
end
81+
82+
local function get_option(options, key, default)
83+
if options ~= nil and options[key] ~= nil then
84+
return options[key]
85+
end
86+
87+
return default
88+
end
89+
90+
local function log(request_handle, options, level, message)
91+
if not get_option(options, "logging_enabled", false) then
92+
return
93+
end
94+
95+
local prefixed_message = "whoami_sync_worker_router: " .. message
96+
if level == "error" then
97+
request_handle:logErr(prefixed_message)
98+
return
99+
end
100+
101+
request_handle:logWarn(prefixed_message)
102+
end
103+
104+
local function truncate_token(token, options)
105+
local token_length = get_option(options, "logging_token_length", 8)
106+
if token == nil or string.len(token) <= token_length then
107+
return token
108+
end
109+
110+
return string.sub(token, 1, token_length) .. "..."
111+
end
112+
113+
local function extract_localpart(user_id)
114+
if user_id == nil or string.sub(user_id, 1, 1) ~= "@" then
115+
return nil
35116
end
36117

37-
return get_auth_token(headers:get("authorization"), path)
118+
local colon_index = string.find(user_id, ":", 2, true)
119+
if colon_index == nil then
120+
return nil
121+
end
122+
123+
return string.sub(user_id, 2, colon_index - 1)
124+
end
125+
126+
local function extract_user_id_from_whoami_body(body)
127+
local _, _, user_id = string.find(body, '"user_id"%s*:%s*"([^"]+)"')
128+
129+
return user_id
130+
end
131+
132+
local function get_cached_username(token, options)
133+
local entry = whoami_cache[token]
134+
if entry == nil then
135+
return nil
136+
end
137+
138+
if entry.expires_at > os.time() then
139+
return entry.username
140+
end
141+
142+
whoami_cache[token] = nil
143+
return nil
144+
end
145+
146+
local function cache_username(token, username, options)
147+
local ttl_seconds = get_option(options, "cache_ttl_seconds", 300)
148+
whoami_cache[token] = {
149+
username = username,
150+
expires_at = os.time() + ttl_seconds
151+
}
152+
end
153+
154+
local function lookup_whoami(request_handle, token, options)
155+
local headers = request_handle:headers()
156+
local authority = headers:get(":authority")
157+
if authority == nil then
158+
authority = "synapse-client-reader-headless"
159+
end
160+
161+
log(request_handle, options, "warn", "performing whoami lookup for token " .. truncate_token(token, options))
162+
163+
local ok, response_headers, response_body = pcall(function()
164+
return request_handle:httpCall(
165+
get_option(options, "whoami_cluster", "httpd"),
166+
{
167+
[":method"] = "GET",
168+
[":path"] = get_option(options, "whoami_path", "/_matrix/client/v3/account/whoami"),
169+
[":authority"] = authority,
170+
["authorization"] = "Bearer " .. token
171+
},
172+
"",
173+
get_option(options, "timeout_ms", 5000)
174+
)
175+
end)
176+
177+
if not ok then
178+
log(request_handle, options, "error", "whoami lookup failed: " .. tostring(response_headers))
179+
return nil
180+
end
181+
182+
local status = response_headers[":status"]
183+
if status ~= "200" then
184+
if status == "401" then
185+
log(request_handle, options, "warn", "whoami lookup returned 401 for token " .. truncate_token(token, options))
186+
else
187+
log(request_handle, options, "error", "whoami lookup returned status " .. tostring(status))
188+
end
189+
return nil
190+
end
191+
192+
local user_id = extract_user_id_from_whoami_body(response_body)
193+
local username = extract_localpart(user_id)
194+
if username ~= nil then
195+
log(request_handle, options, "warn", "whoami lookup success: " .. user_id .. " -> " .. username)
196+
end
197+
198+
return username
199+
end
200+
201+
local function get_user_identifier_from_request(request_handle, options)
202+
local headers = request_handle:headers()
203+
local token = get_access_token_from_request(headers)
204+
if token == nil or string.len(token) == 0 then
205+
log(request_handle, options, "warn", "no token found in request")
206+
return nil
207+
end
208+
209+
local cached_username = get_cached_username(token, options)
210+
if cached_username ~= nil then
211+
log(request_handle, options, "warn", "cache hit for token " .. truncate_token(token, options) .. " -> " .. cached_username)
212+
return cached_username
213+
end
214+
215+
local username = lookup_whoami(request_handle, token, options)
216+
if username ~= nil then
217+
cache_username(token, username, options)
218+
return username
219+
end
220+
221+
log(request_handle, options, "warn", "whoami lookup failed, falling back to token-based routing")
222+
return token
38223
end
39224

40225
return {
41-
get_hash_key_from_request = get_hash_key_from_request
226+
get_access_token_from_request = get_access_token_from_request,
227+
get_room_id_from_request = get_room_id_from_request,
228+
set_request_id_hash_key_with_fallback_log = set_request_id_hash_key_with_fallback_log,
229+
get_user_identifier_from_request = get_user_identifier_from_request
42230
}

0 commit comments

Comments
 (0)