mtp_config: parse default DC id from Telegram config; drop get_netloc_safe; ETS protected

- parse_config/1 now returns {DefaultDc, Downstreams} using a single
  lists:foldl pass; the 'default X;' line sets the fallback DC id
- update_ids/3 stores {default_dc, DcId} in ETS alongside dc_ids
- get_default_dc/0 reads default_dc from ETS (safe from any process)
- get_downstream_safe/2 fallback uses get_default_dc() instead of
  random_choice; errors immediately if default == requested (avoid loop)
- get_netloc_safe/1 removed: dead code since 2018, never called
- ETS table changed from public to protected (only mtp_config writes)
- doc/handler-downstream-flow.md: new sequence diagram + update note
  about pool resolution fallback

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Sergey Prokhorov 2026-04-11 18:27:34 +02:00
parent 8ce8f2196d
commit 4e0db66fc1
No known key found for this signature in database
GPG key ID: 1C570244E4EF3337
2 changed files with 89 additions and 31 deletions

View file

@ -0,0 +1,45 @@
# Handler ↔ downstream lookup and handshake
Shows how `mtp_handler` locates an `mtp_down_conn` for a new client connection
and the steady-state data flow that follows.
**Key actors:**
- `mtp_handler` — one process per Telegram client TCP connection
- `mtp_dc_pool` — manages a pool of downstream connections for one DC
- `mtp_down_conn` — multiplexed TCP connection to a Telegram DC
- `Telegram DC` — the upstream Telegram data-centre server
```mermaid
sequenceDiagram
participant Client as Telegram client
participant Handler as mtp_handler
participant Pool as mtp_dc_pool
participant Down as mtp_down_conn
participant TG as Telegram DC
Client->>Handler: TCP connect + Hello bytes
Note over Handler: decode protocol headers<br/>(fake-TLS / obfuscated / secure)<br/>stage: hello → tunnel
Note over Handler: resolve pool: whereis(dc_to_pool_name(DcId))<br/>(registered name lookup; falls back to default DC from mtp_config if not found)
Handler->>Pool: mtp_dc_pool:get(Pool, self(), Opts) [sync]
Pool-->>Down: upstream_new(Handler, Opts) [cast]
Pool->>Handler: Downstream pid
Note over Handler: down = Downstream<br/>stage = tunnel
loop steady-state data exchange
Client->>Handler: TCP data
Handler->>Down: mtp_down_conn:send(Down, Data) [sync]
Down->>TG: TCP data (RPC-framed)
TG->>Down: TCP data
Down->>Handler: ok
Down-->>Handler: {proxy_ans, Down, Data} [cast]
Handler->>Client: TCP data
Handler-->>Down: mtp_down_conn:ack(Down, Count, Size) [cast]
end
Client->>Handler: TCP close
Handler-->>Pool: mtp_dc_pool:return(Pool, self()) [cast]
Pool-->>Down: upstream_closed(Down, Handler) [cast]
```

View file

@ -18,8 +18,8 @@
-export([get_downstream_safe/2,
get_downstream_pool/1,
get_netloc/1,
get_netloc_safe/1,
get_secret/0,
get_default_dc/0,
status/0,
update/0]).
@ -37,6 +37,7 @@
-define(TAB, ?MODULE).
-define(IPS_KEY(DcId), {id, DcId}).
-define(IDS_KEY, dc_ids).
-define(DEFAULT_DC_KEY, default_dc).
-define(SECRET_URL, "https://core.telegram.org/getProxySecret").
-define(CONFIG_URL, "https://core.telegram.org/getProxyConfig").
@ -65,9 +66,11 @@ get_downstream_safe(DcId, Opts) ->
error({pool_empty, DcId, Pool})
end;
not_found ->
[{?IDS_KEY, L}] = ets:lookup(?TAB, ?IDS_KEY),
NewDcId = random_choice(L),
get_downstream_safe(NewDcId, Opts)
case get_default_dc() of
undefined -> error({no_pool, DcId});
DcId -> error({no_pool, DcId}); % default == requested, avoid loop
NewDcId -> get_downstream_safe(NewDcId, Opts)
end
end.
get_downstream_pool(DcId) ->
@ -78,17 +81,6 @@ get_downstream_pool(DcId) ->
not_found
end.
-spec get_netloc_safe(dc_id()) -> {dc_id(), netloc()}.
get_netloc_safe(DcId) ->
case get_netloc(DcId) of
{ok, Addr} -> {DcId, Addr};
not_found ->
[{?IDS_KEY, L}] = ets:lookup(?TAB, ?IDS_KEY),
NewDcId = random_choice(L),
%% Get random DC; it might return 0 and recurse aggain
get_netloc_safe(NewDcId)
end.
get_netloc(DcId) ->
Key = ?IPS_KEY(DcId),
case ets:lookup(?TAB, Key) of
@ -107,6 +99,13 @@ get_secret() ->
[{_, Key}] = ets:lookup(?TAB, key),
Key.
-spec get_default_dc() -> dc_id() | undefined.
get_default_dc() ->
case ets:lookup(?TAB, ?DEFAULT_DC_KEY) of
[{?DEFAULT_DC_KEY, DcId}] -> DcId;
[] -> undefined
end.
-spec status() -> [mtp_dc_pool:status()].
status() ->
[{?IDS_KEY, L}] = ets:lookup(?TAB, ?IDS_KEY),
@ -130,7 +129,7 @@ init([]) ->
#{timeout => {env, ?APP, conf_refresh_interval, 3600},
unit => second}),
Tab = ets:new(?TAB, [set,
public,
protected,
named_table,
{read_concurrency, true}]),
State = #state{tab = Tab,
@ -189,17 +188,23 @@ update_key(Tab) ->
update_config(Tab) ->
Url = application:get_env(mtproto_proxy, proxy_config_url, ?CONFIG_URL),
{ok, Body} = http_get(Url),
Downstreams = parse_config(Body),
{DefaultDc, Downstreams} = parse_config(Body),
update_downstreams(Downstreams, Tab),
update_ids(Downstreams, Tab).
update_ids(Downstreams, DefaultDc, Tab).
parse_config(Body) ->
Lines = string:lexemes(Body, "\n"),
ProxyLines = lists:filter(
fun("proxy_for " ++ _) -> true;
(_) -> false
end, Lines),
[parse_downstream(Line) || Line <- ProxyLines].
{DefaultDc, Downstreams} =
lists:foldl(
fun("default " ++ _ = Line, {_, Ds}) ->
["default", DcIdStr] = string:lexemes(Line, " ;"),
{list_to_integer(DcIdStr), Ds};
("proxy_for " ++ _ = Line, {Def, Ds}) ->
{Def, [parse_downstream(Line) | Ds]};
(_, Acc) ->
Acc
end, {undefined, []}, Lines),
{DefaultDc, lists:reverse(Downstreams)}.
parse_downstream(Line) ->
["proxy_for",
@ -231,9 +236,10 @@ update_downstreams(Downstreams, Tab) ->
end,
maps:keys(ByDc)).
update_ids(Downstreams, Tab) ->
update_ids(Downstreams, DefaultDc, Tab) ->
Ids = lists:usort([DcId || {DcId, _, _} <- Downstreams]),
true = ets:insert(Tab, {?IDS_KEY, Ids}).
true = ets:insert(Tab, {?IDS_KEY, Ids}),
true = ets:insert(Tab, {?DEFAULT_DC_KEY, DefaultDc}).
update_ip() ->
case application:get_env(?APP, ip_lookup_services) of
@ -283,15 +289,22 @@ random_choice(L) ->
-include_lib("eunit/include/eunit.hrl").
parse_test() ->
Config = ("# force_probability 1 10
proxy_for 1 149.154.175.50:8888;
proxy_for -1 149.154.175.50:8888;
proxy_for 2 149.154.162.39:80;
proxy_for 2 149.154.162.33:80;"),
Config = ("# force_probability 1 10\n"
"default 2;\n"
"proxy_for 1 149.154.175.50:8888;\n"
"proxy_for -1 149.154.175.50:8888;\n"
"proxy_for 2 149.154.162.39:80;\n"
"proxy_for 2 149.154.162.33:80;"),
Expect = [{1, {149, 154, 175, 50}, 8888},
{-1, {149, 154, 175, 50}, 8888},
{2, {149, 154, 162, 39}, 80},
{2, {149, 154, 162, 33},80}],
?assertEqual(Expect, parse_config(Config)).
?assertEqual({2, Expect}, parse_config(Config)).
parse_no_default_test() ->
Config = ("proxy_for 1 149.154.175.50:8888;\n"
"proxy_for 2 149.154.162.39:80;"),
{DefaultDc, _} = parse_config(Config),
?assertEqual(undefined, DefaultDc).
-endif.