1 |
|
%%% eturnal STUN/TURN server. |
2 |
|
%%% |
3 |
|
%%% Copyright (c) 2020-2023 Holger Weiss <holger@zedat.fu-berlin.de>. |
4 |
|
%%% Copyright (c) 2020-2023 ProcessOne, SARL. |
5 |
|
%%% All rights reserved. |
6 |
|
%%% |
7 |
|
%%% Licensed under the Apache License, Version 2.0 (the "License"); |
8 |
|
%%% you may not use this file except in compliance with the License. |
9 |
|
%%% You may obtain a copy of the License at |
10 |
|
%%% |
11 |
|
%%% http://www.apache.org/licenses/LICENSE-2.0 |
12 |
|
%%% |
13 |
|
%%% Unless required by applicable law or agreed to in writing, software |
14 |
|
%%% distributed under the License is distributed on an "AS IS" BASIS, |
15 |
|
%%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
16 |
|
%%% See the License for the specific language governing permissions and |
17 |
|
%%% limitations under the License. |
18 |
|
|
19 |
|
-module(eturnal). |
20 |
|
-behaviour(gen_server). |
21 |
|
-export([start/0, |
22 |
|
stop/0]). |
23 |
|
-export([start_link/0]). |
24 |
|
-export([init/1, |
25 |
|
handle_call/3, |
26 |
|
handle_cast/2, |
27 |
|
handle_info/2, |
28 |
|
terminate/2, |
29 |
|
code_change/3]). |
30 |
|
-export([init_config/0, |
31 |
|
config_is_loaded/0, |
32 |
|
run_hook/2, |
33 |
|
get_password/2, |
34 |
|
get_opt/1, |
35 |
|
create_self_signed/1, |
36 |
|
reload/3, |
37 |
|
abort/1]). |
38 |
|
-export_type([transport/0, |
39 |
|
option/0, |
40 |
|
value/0, |
41 |
|
config_changes/0, |
42 |
|
state/0]). |
43 |
|
|
44 |
|
-ifdef(EUNIT). |
45 |
|
-include_lib("eunit/include/eunit.hrl"). |
46 |
|
-endif. |
47 |
|
-include_lib("kernel/include/logger.hrl"). |
48 |
|
-define(PEM_FILE_NAME, "cert.pem"). |
49 |
|
|
50 |
|
-record(eturnal_state, |
51 |
|
{listeners :: listeners(), |
52 |
|
modules :: modules()}). |
53 |
|
|
54 |
|
-type transport() :: udp | tcp | tls | auto. |
55 |
|
-type option() :: atom(). |
56 |
|
-type value() :: term(). |
57 |
|
-type config_changes() :: {[{option(), value()}], |
58 |
|
[{option(), value()}], |
59 |
|
[option()]}. |
60 |
|
|
61 |
|
-opaque state() :: #eturnal_state{}. |
62 |
|
|
63 |
|
-type listeners() :: [{inet:ip_address(), inet:port_number(), transport()}]. |
64 |
|
-type modules() :: [module()]. |
65 |
|
|
66 |
|
%% API: non-release startup and shutdown (used by test suite). |
67 |
|
|
68 |
|
-spec start() -> ok | {error, term()}. |
69 |
|
start() -> |
70 |
1 |
case application:ensure_all_started(eturnal) of |
71 |
|
{ok, _Started} -> |
72 |
1 |
ok; |
73 |
|
{error, _Reason} = Err -> |
74 |
:-( |
Err |
75 |
|
end. |
76 |
|
|
77 |
|
-spec stop() -> ok | {error, term()}. |
78 |
|
stop() -> |
79 |
1 |
application:stop(eturnal). |
80 |
|
|
81 |
|
%% API: supervisor callback. |
82 |
|
|
83 |
|
-spec start_link() -> {ok, pid()} | ignore | {error, term()}. |
84 |
|
start_link() -> |
85 |
1 |
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). |
86 |
|
|
87 |
|
%% API: gen_server callbacks. |
88 |
|
|
89 |
|
-spec init(any()) -> {ok, state()}. |
90 |
|
init(_Opts) -> |
91 |
1 |
process_flag(trap_exit, true), |
92 |
1 |
ok = eturnal_module:init(), |
93 |
1 |
ok = log_relay_addresses(), |
94 |
1 |
ok = log_control_listener(), |
95 |
1 |
try |
96 |
1 |
ok = ensure_run_dir(), |
97 |
1 |
ok = check_turn_config(), |
98 |
1 |
ok = check_proxy_config(), |
99 |
1 |
_R = check_pem_file() |
100 |
|
catch exit:Reason1 -> |
101 |
:-( |
abort(Reason1) |
102 |
|
end, |
103 |
1 |
try {start_modules(), start_listeners()} of |
104 |
|
{Modules, Listeners} -> |
105 |
1 |
?LOG_DEBUG("Started ~B modules", [length(Modules)]), |
106 |
1 |
?LOG_DEBUG("Started ~B listeners", [length(Listeners)]), |
107 |
1 |
{ok, #eturnal_state{listeners = Listeners, modules = Modules}} |
108 |
|
catch exit:Reason2 -> |
109 |
:-( |
abort(Reason2) |
110 |
|
end. |
111 |
|
|
112 |
|
-spec handle_call(reload | get_info | get_version | get_loglevel | |
113 |
|
{set_loglevel, eturnal_logger:level()} | |
114 |
|
{get_password, binary()} | term(), |
115 |
|
{pid(), term()}, state()) |
116 |
|
-> {reply, ok | {ok, term()} | {error, term()}, state()}. |
117 |
|
handle_call(reload, _From, State) -> |
118 |
2 |
case reload(State) of |
119 |
|
{ok, State1} -> |
120 |
2 |
{reply, ok, State1}; |
121 |
|
{error, _Reason} = Err -> |
122 |
:-( |
{reply, Err, State} |
123 |
|
end; |
124 |
|
handle_call(get_info, _From, State) -> |
125 |
1 |
Info = eturnal_misc:info(), |
126 |
1 |
{reply, {ok, Info}, State}; |
127 |
|
handle_call(get_version, _From, State) -> |
128 |
1 |
Version = eturnal_misc:version(), |
129 |
1 |
{reply, {ok, Version}, State}; |
130 |
|
handle_call(get_loglevel, _From, State) -> |
131 |
2 |
Level = eturnal_logger:get_level(), |
132 |
2 |
{reply, {ok, Level}, State}; |
133 |
|
handle_call({set_loglevel, Level}, _From, State) -> |
134 |
1 |
try |
135 |
1 |
ok = eturnal_logger:set_level(Level), |
136 |
1 |
{reply, ok, State} |
137 |
|
catch error:{badmatch, {error, _Reason} = Err} -> |
138 |
:-( |
{reply, Err, State} |
139 |
|
end; |
140 |
|
handle_call({get_password, Username}, _From, State) -> |
141 |
13 |
case {get_opt(secret), is_dynamic_username(Username)} of |
142 |
|
{[Secret | _Secrets], true} -> |
143 |
11 |
Password = derive_password(Username, [Secret]), |
144 |
11 |
{reply, {ok, Password}, State}; |
145 |
|
{_, _} -> |
146 |
2 |
case maps:get(Username, get_opt(credentials), undefined) of |
147 |
|
Password when is_binary(Password) -> |
148 |
1 |
{reply, {ok, Password}, State}; |
149 |
|
undefined -> |
150 |
1 |
{reply, {error, no_credentials}, State} |
151 |
|
end |
152 |
|
end; |
153 |
|
handle_call(Request, From, State) -> |
154 |
:-( |
?LOG_ERROR("Got unexpected request from ~p: ~p", [From, Request]), |
155 |
:-( |
{reply, {error, badarg}, State}. |
156 |
|
|
157 |
|
-spec handle_cast(reload | |
158 |
|
{config_change, config_changes(), |
159 |
|
fun(() -> ok), fun(() -> ok)} | term(), state()) |
160 |
|
-> {noreply, state()}. |
161 |
|
handle_cast(reload, State) -> |
162 |
:-( |
case reload(State) of |
163 |
|
{ok, State1} -> |
164 |
:-( |
{noreply, State1}; |
165 |
|
{error, _Reason} -> |
166 |
:-( |
{noreply, State} |
167 |
|
end; |
168 |
|
handle_cast({config_change, Changes, BeginFun, EndFun}, State) -> |
169 |
:-( |
ok = BeginFun(), |
170 |
:-( |
State1 = apply_config_changes(State, Changes), |
171 |
:-( |
ok = EndFun(), |
172 |
:-( |
{noreply, State1}; |
173 |
|
handle_cast(Msg, State) -> |
174 |
:-( |
?LOG_ERROR("Got unexpected message: ~p", [Msg]), |
175 |
:-( |
{noreply, State}. |
176 |
|
|
177 |
|
-spec handle_info(term(), state()) -> {noreply, state()}. |
178 |
|
handle_info(Info, State) -> |
179 |
:-( |
?LOG_ERROR("Got unexpected info: ~p", [Info]), |
180 |
:-( |
{noreply, State}. |
181 |
|
|
182 |
|
-spec terminate(normal | shutdown | {shutdown, term()} | term(), state()) -> ok. |
183 |
|
terminate(Reason, State) -> |
184 |
1 |
?LOG_DEBUG("Terminating ~s (~p)", [?MODULE, Reason]), |
185 |
1 |
try stop_listeners(State) |
186 |
|
catch exit:Reason1 -> |
187 |
:-( |
?LOG_ERROR(format_error(Reason1)) |
188 |
|
end, |
189 |
1 |
try stop_modules(State) |
190 |
|
catch exit:Reason2 -> |
191 |
:-( |
?LOG_ERROR(format_error(Reason2)) |
192 |
|
end, |
193 |
1 |
try clean_run_dir() |
194 |
|
catch exit:Reason3 -> |
195 |
:-( |
?LOG_ERROR(format_error(Reason3)) |
196 |
|
end, |
197 |
1 |
_ = eturnal_module:terminate(), |
198 |
1 |
ok. |
199 |
|
|
200 |
|
-spec code_change({down, term()} | term(), state(), term()) -> {ok, state()}. |
201 |
|
code_change(_OldVsn, State, _Extra) -> |
202 |
:-( |
?LOG_NOTICE("Upgraded to eturnal ~s, reapplying configuration", |
203 |
:-( |
[eturnal_misc:version()]), |
204 |
:-( |
ok = reload_config(), |
205 |
:-( |
{ok, State}. |
206 |
|
|
207 |
|
%% API: (re)load configuration. |
208 |
|
|
209 |
|
-spec init_config() -> ok. |
210 |
|
init_config() -> % Just to cope with an empty configuration file. |
211 |
1 |
case config_is_loaded() of |
212 |
|
true -> |
213 |
1 |
?LOG_DEBUG("Configuration has been loaded successfully"), |
214 |
1 |
ok; |
215 |
|
false -> |
216 |
:-( |
?LOG_DEBUG("Empty configuration, using defaults"), |
217 |
:-( |
ok = conf:load([{eturnal, []}]) |
218 |
|
end. |
219 |
|
|
220 |
|
-spec config_is_loaded() -> boolean(). |
221 |
|
config_is_loaded() -> |
222 |
1 |
try eturnal:get_opt(realm) of |
223 |
|
Realm when is_binary(Realm) -> |
224 |
1 |
true |
225 |
|
catch error:{badmatch, undefined} -> |
226 |
:-( |
false |
227 |
|
end. |
228 |
|
|
229 |
|
%% API: stun callbacks. |
230 |
|
|
231 |
|
-spec run_hook(eturnal_module:event(), eturnal_module:info()) -> ok. |
232 |
|
run_hook(Event, Info) -> |
233 |
9 |
eturnal_module:handle_event(Event, Info). |
234 |
|
|
235 |
|
-spec get_password(binary(), binary()) |
236 |
|
-> binary() | [binary()] | {expired, binary() | [binary()]}. |
237 |
|
get_password(Username, _Realm) -> |
238 |
4 |
[Expiration | _Suffix] = binary:split(Username, <<$:>>), |
239 |
4 |
try binary_to_integer(Expiration) of |
240 |
|
ExpireTime -> |
241 |
2 |
case erlang:system_time(second) of |
242 |
|
Now when Now < ExpireTime -> |
243 |
2 |
?LOG_DEBUG("Deriving password for: ~ts", [Username]), |
244 |
2 |
derive_password(Username, get_opt(secret)); |
245 |
|
Now when Now >= ExpireTime -> |
246 |
:-( |
case get_opt(strict_expiry) of |
247 |
|
true -> |
248 |
:-( |
?LOG_INFO("Credentials expired: ~ts", [Username]), |
249 |
:-( |
<<>>; |
250 |
|
false -> |
251 |
:-( |
?LOG_DEBUG("Credentials expired: ~ts", [Username]), |
252 |
:-( |
{expired, |
253 |
|
derive_password(Username, get_opt(secret))} |
254 |
|
end |
255 |
|
end |
256 |
|
catch _:badarg -> |
257 |
2 |
?LOG_DEBUG("Looking up password for: ~ts", [Username]), |
258 |
2 |
case maps:get(Username, get_opt(credentials), undefined) of |
259 |
|
Password when is_binary(Password) -> |
260 |
2 |
Password; |
261 |
|
undefined -> |
262 |
:-( |
?LOG_INFO("Have no password for: ~ts", [Username]), |
263 |
:-( |
<<>> |
264 |
|
end |
265 |
|
end. |
266 |
|
|
267 |
|
%% API: retrieve option value. |
268 |
|
|
269 |
|
-spec get_opt(option()) -> value(). |
270 |
|
get_opt(Opt) -> |
271 |
120 |
{ok, Val} = application:get_env(eturnal, Opt), |
272 |
120 |
Val. |
273 |
|
|
274 |
|
%% API: create self-signed certificate. |
275 |
|
|
276 |
|
-spec create_self_signed(file:filename_all()) -> ok. |
277 |
|
create_self_signed(File) -> |
278 |
2 |
try |
279 |
2 |
PEM = eturnal_cert:create(get_opt(realm)), |
280 |
2 |
ok = touch(File), |
281 |
2 |
ok = file:write_file(File, PEM, [raw]) |
282 |
|
catch error:{_, {error, Reason}} -> |
283 |
:-( |
exit({pem_failure, File, Reason}) |
284 |
|
end. |
285 |
|
|
286 |
|
%% API: reload service. |
287 |
|
|
288 |
|
-spec reload(config_changes(), fun(() -> ok), fun(() -> ok)) -> ok. |
289 |
|
reload(ConfigChanges, BeginFun, EndFun) -> |
290 |
:-( |
Msg = {config_change, ConfigChanges, BeginFun, EndFun}, |
291 |
:-( |
ok = gen_server:cast(?MODULE, Msg). |
292 |
|
|
293 |
|
%% API: abnormal termination. |
294 |
|
|
295 |
|
-spec abort(term()) -> no_return(). |
296 |
|
abort(Reason) -> |
297 |
:-( |
case application:get_env(eturnal, on_fail, halt) of |
298 |
|
exit -> |
299 |
:-( |
?LOG_CRITICAL("Stopping: ~s", [format_error(Reason)]), |
300 |
:-( |
exit(Reason); |
301 |
|
_Halt -> |
302 |
:-( |
?LOG_CRITICAL("Aborting: ~s", [format_error(Reason)]), |
303 |
:-( |
eturnal_logger:flush(), |
304 |
:-( |
halt(1) |
305 |
|
end. |
306 |
|
|
307 |
|
%% Internal functions: reload configuration. |
308 |
|
|
309 |
|
-spec reload_config() -> ok. |
310 |
|
reload_config() -> |
311 |
:-( |
ok = gen_server:cast(?MODULE, reload). |
312 |
|
|
313 |
|
%% Internal functions: authentication. |
314 |
|
|
315 |
|
-spec is_dynamic_username(binary()) -> boolean(). |
316 |
|
is_dynamic_username(Username) -> |
317 |
13 |
case string:to_integer(Username) of |
318 |
|
{N, <<":", _Rest/binary>>} when is_integer(N), N > 0 -> |
319 |
10 |
true; |
320 |
|
{N, <<>>} when is_integer(N), N > 0 -> |
321 |
1 |
true; |
322 |
|
{_, _} -> |
323 |
2 |
false |
324 |
|
end. |
325 |
|
|
326 |
|
-spec derive_password(binary(), [binary()]) -> binary() | [binary()]. |
327 |
|
-ifdef(old_crypto). |
328 |
|
derive_password(Username, [Secret]) -> |
329 |
|
base64:encode(crypto:hmac(sha, Secret, Username)); |
330 |
|
derive_password(Username, Secrets) when is_list(Secrets) -> |
331 |
|
[derive_password(Username, [Secret]) || Secret <- Secrets]. |
332 |
|
-else. |
333 |
|
derive_password(Username, [Secret]) -> |
334 |
13 |
base64:encode(crypto:mac(hmac, sha, Secret, Username)); |
335 |
|
derive_password(Username, Secrets) when is_list(Secrets) -> |
336 |
:-( |
[derive_password(Username, [Secret]) || Secret <- Secrets]. |
337 |
|
-endif. |
338 |
|
|
339 |
|
%% Internal functions: log relay address(es) and distribution listener port. |
340 |
|
|
341 |
|
-spec log_relay_addresses() -> ok. |
342 |
|
log_relay_addresses() -> |
343 |
1 |
Min = get_opt(relay_min_port), |
344 |
1 |
Max = get_opt(relay_max_port), |
345 |
1 |
case get_opt(relay_ipv4_addr) of |
346 |
|
{_, _, _, _} = Addr4 -> |
347 |
1 |
?LOG_INFO("Relay IPv4 address: ~s (port range: ~B-~B)", |
348 |
:-( |
[inet:ntoa(Addr4), Min, Max]); |
349 |
|
undefined -> |
350 |
:-( |
?LOG_INFO("Relay IPv4 address not configured") |
351 |
|
end, |
352 |
1 |
case get_opt(relay_ipv6_addr) of |
353 |
|
{_, _, _, _, _, _, _, _} = Addr6 -> |
354 |
:-( |
?LOG_INFO("Relay IPv6 address: ~s (port range: ~B-~B)", |
355 |
:-( |
[inet:ntoa(Addr6), Min, Max]); |
356 |
|
undefined -> |
357 |
1 |
?LOG_INFO("Relay IPv6 address not configured") |
358 |
|
end. |
359 |
|
|
360 |
|
-spec log_control_listener() -> ok. |
361 |
|
-dialyzer({[no_fail_call, no_match], log_control_listener/0}). % OTP 21/22. |
362 |
|
log_control_listener() -> |
363 |
1 |
[Name, Host] = string:split(atom_to_list(node()), "@"), |
364 |
|
% The 'catch' calms Dialyzer on OTP 21 (even though we don't match 'EXIT'). |
365 |
1 |
case catch erl_epmd:port_please(Name, Host, timer:seconds(10)) of |
366 |
|
{port, Port, Version} -> |
367 |
:-( |
?LOG_INFO("Listening on ~s:~B (tcp) (Erlang protocol version ~B)", |
368 |
:-( |
[Host, Port, Version]); |
369 |
|
{error, Reason} -> |
370 |
:-( |
?LOG_INFO("Cannot determine control query port: ~p", [Reason]); |
371 |
|
Reason when is_atom(Reason) -> |
372 |
1 |
?LOG_INFO("Cannot determine control query port: ~s", [Reason]) |
373 |
|
end. |
374 |
|
|
375 |
|
%% Internal functions: module startup/shutdown. |
376 |
|
|
377 |
|
-spec start_modules() -> modules(). |
378 |
|
start_modules() -> |
379 |
3 |
lists:map( |
380 |
|
fun({Mod, _Opts}) -> |
381 |
9 |
case eturnal_module:start(Mod) of |
382 |
|
ok -> |
383 |
9 |
?LOG_INFO("Started ~s", [Mod]), |
384 |
9 |
Mod; |
385 |
|
{error, Reason} -> |
386 |
:-( |
exit({module_failure, start, Mod, Reason}) |
387 |
|
end |
388 |
|
end, maps:to_list(get_opt(modules))). |
389 |
|
|
390 |
|
-spec stop_modules(state()) -> ok. |
391 |
|
stop_modules(#eturnal_state{modules = Modules}) -> |
392 |
3 |
lists:foreach( |
393 |
|
fun(Mod) -> |
394 |
9 |
case eturnal_module:stop(Mod) of |
395 |
|
ok -> |
396 |
9 |
?LOG_INFO("Stopped ~s", [Mod]); |
397 |
|
{error, Reason} -> |
398 |
:-( |
exit({module_failure, stop, Mod, Reason}) |
399 |
|
end |
400 |
|
end, Modules). |
401 |
|
|
402 |
|
%% Internal functions: listener startup/shutdown. |
403 |
|
|
404 |
|
-spec start_listeners() -> listeners(). |
405 |
|
start_listeners() -> |
406 |
1 |
Opts = lists:filtermap( |
407 |
|
fun({InKey, OutKey}) -> |
408 |
9 |
opt_filter({OutKey, get_opt(InKey)}) |
409 |
|
end, opt_map()) ++ [{auth_fun, fun ?MODULE:get_password/2}, |
410 |
|
{hook_fun, fun ?MODULE:run_hook/2}] |
411 |
|
++ blacklist_opts() |
412 |
|
++ whitelist_opts(), |
413 |
1 |
lists:map( |
414 |
|
fun({IP, Port, Transport, ProxyProtocol, EnableTURN}) -> |
415 |
4 |
Opts1 = tls_opts(Transport) ++ Opts, |
416 |
4 |
Opts2 = turn_opts(EnableTURN) ++ Opts1, |
417 |
4 |
Opts3 = proxy_opts(ProxyProtocol) ++ Opts2, |
418 |
4 |
?LOG_DEBUG("Starting listener ~s (~s) with options:~n~p", |
419 |
|
[eturnal_misc:addr_to_str(IP, Port), Transport, |
420 |
:-( |
Opts3]), |
421 |
4 |
InfoArgs = [eturnal_misc:addr_to_str(IP, Port), Transport, |
422 |
|
describe_listener(EnableTURN)], |
423 |
4 |
case stun_listener:add_listener(IP, Port, Transport, Opts3) of |
424 |
|
ok -> |
425 |
4 |
?LOG_INFO("Listening on ~s (~s) (~s)", InfoArgs); |
426 |
|
{error, already_started} -> |
427 |
:-( |
?LOG_INFO("Already listening on ~s (~s) (~s)", InfoArgs); |
428 |
|
{error, Reason} -> |
429 |
:-( |
exit({listener_failure, start, IP, Port, Transport, |
430 |
|
Reason}) |
431 |
|
end, |
432 |
4 |
{IP, Port, Transport} |
433 |
|
end, get_opt(listen)). |
434 |
|
|
435 |
|
-spec stop_listeners(state()) -> ok. |
436 |
|
stop_listeners(#eturnal_state{listeners = Listeners}) -> |
437 |
1 |
lists:foreach( |
438 |
|
fun({IP, Port, Transport}) -> |
439 |
4 |
case stun_listener:del_listener(IP, Port, Transport) of |
440 |
|
ok -> |
441 |
4 |
?LOG_INFO("Stopped listening on ~s (~s)", |
442 |
|
[eturnal_misc:addr_to_str(IP, Port), |
443 |
:-( |
Transport]); |
444 |
|
{error, Reason} -> |
445 |
:-( |
exit({listener_failure, stop, IP, Port, Transport, |
446 |
|
Reason}) |
447 |
|
end |
448 |
|
end, Listeners). |
449 |
|
|
450 |
|
-spec describe_listener(boolean()) -> binary(). |
451 |
|
describe_listener(true = _EnableTURN) -> |
452 |
1 |
<<"STUN/TURN">>; |
453 |
|
describe_listener(false = _EnableTURN) -> |
454 |
3 |
<<"STUN only">>. |
455 |
|
|
456 |
|
-spec opt_map() -> [{atom(), atom()}]. |
457 |
|
opt_map() -> |
458 |
1 |
[{relay_ipv4_addr, turn_ipv4_address}, |
459 |
|
{relay_ipv6_addr, turn_ipv6_address}, |
460 |
|
{relay_min_port, turn_min_port}, |
461 |
|
{relay_max_port, turn_max_port}, |
462 |
|
{max_allocations, turn_max_allocations}, |
463 |
|
{max_permissions, turn_max_permissions}, |
464 |
|
{max_bps, shaper}, |
465 |
|
{realm, auth_realm}, |
466 |
|
{software_name, server_name}]. |
467 |
|
|
468 |
|
-spec opt_filter(Opt) -> {true, Opt} | false when Opt :: {option(), value()}. |
469 |
|
opt_filter({turn_ipv6_address, undefined}) -> |
470 |
1 |
false; % The 'stun' application currently wouldn't accept 'undefined'. |
471 |
|
opt_filter(Opt) -> |
472 |
8 |
{true, Opt}. |
473 |
|
|
474 |
|
-spec turn_opts(boolean()) -> proplists:proplist(). |
475 |
|
turn_opts(EnableTURN) -> |
476 |
4 |
case {EnableTURN, got_credentials(), got_relay_addr()} of |
477 |
|
{true, true, true} -> |
478 |
1 |
[{use_turn, true}, |
479 |
|
{auth_type, user}]; |
480 |
|
{_, _, _} -> |
481 |
3 |
[{use_turn, false}, |
482 |
|
{auth_type, anonymous}] |
483 |
|
end. |
484 |
|
|
485 |
|
-spec proxy_opts(boolean()) -> proplists:proplist(). |
486 |
|
proxy_opts(true = _ProxyProtocol) -> |
487 |
:-( |
[proxy_protocol]; |
488 |
|
proxy_opts(false = _ProxyProtocol) -> |
489 |
4 |
[]. |
490 |
|
|
491 |
|
%% This function can be removed in favor of opt_map/0 entries once the |
492 |
|
%% 'blacklist' option is removed. |
493 |
|
-spec blacklist_opts() -> proplists:proplist(). |
494 |
|
blacklist_opts() -> |
495 |
1 |
case {eturnal:get_opt(blacklist), |
496 |
|
eturnal:get_opt(blacklist_clients), |
497 |
|
eturnal:get_opt(blacklist_peers)} of |
498 |
|
{[], Clients, Peers} -> |
499 |
1 |
[{turn_blacklist_clients, Clients}, |
500 |
|
{turn_blacklist_peers, Peers}]; |
501 |
|
{Blacklist, Clients, Peers} -> |
502 |
:-( |
?LOG_WARNING("The 'blacklist' option is deprecated"), |
503 |
:-( |
?LOG_WARNING("Use 'blacklist_clients' and/or 'blacklist_peers'"), |
504 |
:-( |
[{turn_blacklist_clients, lists:usort(Clients ++ Blacklist)}, |
505 |
|
{turn_blacklist_peers, lists:usort(Peers ++ Blacklist)}] |
506 |
|
end. |
507 |
|
|
508 |
|
%% This function can be removed in favor of opt_map/0 entries once the |
509 |
|
%% 'whitelist' option is removed. |
510 |
|
-spec whitelist_opts() -> proplists:proplist(). |
511 |
|
whitelist_opts() -> |
512 |
1 |
case {eturnal:get_opt(whitelist), |
513 |
|
eturnal:get_opt(whitelist_clients), |
514 |
|
eturnal:get_opt(whitelist_peers)} of |
515 |
|
{[], Clients, Peers} -> |
516 |
1 |
[{turn_whitelist_clients, Clients}, |
517 |
|
{turn_whitelist_peers, Peers}]; |
518 |
|
{Whitelist, Clients, Peers} -> |
519 |
:-( |
?LOG_WARNING("The 'whitelist' option is deprecated"), |
520 |
:-( |
?LOG_WARNING("Use 'whitelist_clients' and/or 'whitelist_peers'"), |
521 |
:-( |
[{turn_whitelist_clients, lists:usort(Clients ++ Whitelist)}, |
522 |
|
{turn_whitelist_peers, lists:usort(Peers ++ Whitelist)}] |
523 |
|
end. |
524 |
|
|
525 |
|
-spec tls_opts(transport()) -> proplists:proplist(). |
526 |
|
-ifdef(old_inet_backend). |
527 |
|
tls_opts(tls) -> |
528 |
|
[{tls, true} | extra_tls_opts()]; |
529 |
|
tls_opts(auto) -> |
530 |
|
exit({otp_too_old, transport, auto, 23}); |
531 |
|
tls_opts(_) -> |
532 |
|
[]. |
533 |
|
-else. |
534 |
|
tls_opts(tls) -> |
535 |
1 |
[{tls, true} | extra_tls_opts()]; |
536 |
|
tls_opts(auto) -> |
537 |
1 |
[{tls, optional} | extra_tls_opts()]; |
538 |
|
tls_opts(_) -> |
539 |
2 |
[]. |
540 |
|
-endif. |
541 |
|
|
542 |
|
-spec extra_tls_opts() -> proplists:proplist(). |
543 |
|
extra_tls_opts() -> |
544 |
2 |
Opts = [{certfile, get_pem_file_path()}, |
545 |
|
{ciphers, get_opt(tls_ciphers)}, |
546 |
|
{protocol_options, get_opt(tls_options)}], |
547 |
2 |
case get_opt(tls_dh_file) of |
548 |
|
Path when is_binary(Path) -> |
549 |
:-( |
[{dhfile, Path} | Opts]; |
550 |
|
none -> |
551 |
2 |
Opts |
552 |
|
end. |
553 |
|
|
554 |
|
%% Internal functions: configuration parsing. |
555 |
|
|
556 |
|
-spec tls_enabled() -> boolean(). |
557 |
|
tls_enabled() -> |
558 |
3 |
lists:any(fun({_IP, _Port, Transport, _ProxyProtocol, _EnableTURN}) -> |
559 |
9 |
(Transport =:= tls) or (Transport =:= auto) |
560 |
|
end, get_opt(listen)). |
561 |
|
|
562 |
|
-spec turn_enabled() -> boolean(). |
563 |
|
turn_enabled() -> |
564 |
1 |
lists:any(fun({_IP, _Port, _Transport, _ProxyProtocol, EnableTURN}) -> |
565 |
1 |
EnableTURN =:= true |
566 |
|
end, get_opt(listen)). |
567 |
|
|
568 |
|
-spec got_credentials() -> boolean(). |
569 |
|
got_credentials() -> |
570 |
4 |
case get_opt(secret) of |
571 |
|
Secrets when is_list(Secrets) -> |
572 |
4 |
lists:all(fun(Secret) -> |
573 |
4 |
is_binary(Secret) and (byte_size(Secret) > 0) |
574 |
|
end, Secrets); |
575 |
|
Secret when is_binary(Secret), byte_size(Secret) > 0 -> |
576 |
:-( |
true; |
577 |
|
undefined -> |
578 |
:-( |
map_size(get_opt(credentials)) > 0 |
579 |
|
end. |
580 |
|
|
581 |
|
-spec got_relay_addr() -> boolean(). |
582 |
|
got_relay_addr() -> |
583 |
5 |
case get_opt(relay_ipv4_addr) of |
584 |
|
{_, _, _, _} -> |
585 |
5 |
true; |
586 |
|
undefined -> |
587 |
:-( |
false |
588 |
|
end. |
589 |
|
|
590 |
|
-spec check_turn_config() -> ok. |
591 |
|
check_turn_config() -> |
592 |
1 |
case turn_enabled() of |
593 |
|
true -> |
594 |
1 |
case {got_relay_addr(), |
595 |
|
get_opt(relay_min_port), |
596 |
|
get_opt(relay_max_port)} of |
597 |
|
{_GotAddr, Min, Max} when Max =< Min -> |
598 |
:-( |
exit(turn_config_failure); |
599 |
|
{false, _Min, _Max} -> |
600 |
:-( |
?LOG_WARNING("Specify a 'relay_ipv4_addr' to enable TURN"); |
601 |
|
{true, _Min, _Max} -> |
602 |
1 |
?LOG_DEBUG("TURN configuration seems fine") |
603 |
|
end; |
604 |
|
false -> |
605 |
:-( |
?LOG_DEBUG("TURN is disabled") |
606 |
|
end. |
607 |
|
|
608 |
|
-spec check_proxy_config() -> ok. |
609 |
|
check_proxy_config() -> |
610 |
1 |
case lists:any( |
611 |
|
fun({_IP, _Port, Transport, ProxyProtocol, _EnableTURN}) -> |
612 |
4 |
(Transport =:= udp) and (ProxyProtocol =:= true) |
613 |
|
end, get_opt(listen)) of |
614 |
|
true -> |
615 |
:-( |
exit(proxy_config_failure); |
616 |
|
false -> |
617 |
1 |
ok |
618 |
|
end. |
619 |
|
|
620 |
|
%% Internal functions: configuration reload. |
621 |
|
|
622 |
|
-spec reload(state()) -> {ok, state()} | {error, term()}. |
623 |
|
reload(State) -> |
624 |
2 |
case conf:reload_file() of |
625 |
|
ok -> |
626 |
2 |
try check_pem_file() of |
627 |
|
ok -> |
628 |
1 |
ok = fast_tls:clear_cache(), |
629 |
1 |
?LOG_INFO("Using new TLS certificate"); |
630 |
|
unchanged -> |
631 |
1 |
?LOG_DEBUG("TLS certificate unchanged") |
632 |
|
catch exit:Reason1 -> |
633 |
:-( |
?LOG_ERROR(format_error(Reason1)) |
634 |
|
end, |
635 |
2 |
try {stop_modules(State), start_modules()} of |
636 |
|
{ok, Modules} -> |
637 |
2 |
?LOG_DEBUG("Restarted modules"), |
638 |
2 |
{ok, State#eturnal_state{modules = Modules}} |
639 |
|
catch exit:Reason2 -> |
640 |
:-( |
?LOG_ERROR(format_error(Reason2)), |
641 |
:-( |
{ok, State} |
642 |
|
end; |
643 |
|
{error, Reason} = Err -> |
644 |
:-( |
?LOG_ERROR("Cannot reload configuration: ~ts", |
645 |
:-( |
[conf:format_error(Reason)]), |
646 |
:-( |
Err |
647 |
|
end. |
648 |
|
|
649 |
|
-spec apply_config_changes(state(), config_changes()) -> state(). |
650 |
|
apply_config_changes(State, {Changed, New, Removed} = ConfigChanges) -> |
651 |
:-( |
if length(Changed) > 0 -> |
652 |
:-( |
?LOG_DEBUG("Changed options: ~p", [Changed]); |
653 |
|
length(Changed) =:= 0 -> |
654 |
:-( |
?LOG_DEBUG("No changed options") |
655 |
|
end, |
656 |
:-( |
if length(Removed) > 0 -> |
657 |
:-( |
?LOG_DEBUG("Removed options: ~p", [Removed]); |
658 |
|
length(Removed) =:= 0 -> |
659 |
:-( |
?LOG_DEBUG("No removed options") |
660 |
|
end, |
661 |
:-( |
if length(New) > 0 -> |
662 |
:-( |
?LOG_DEBUG("New options: ~p", [New]); |
663 |
|
length(New) =:= 0 -> |
664 |
:-( |
?LOG_DEBUG("No new options") |
665 |
|
end, |
666 |
:-( |
try apply_logging_config_changes(ConfigChanges) |
667 |
|
catch exit:Reason1 -> |
668 |
:-( |
?LOG_ERROR(format_error(Reason1)) |
669 |
|
end, |
670 |
:-( |
try apply_run_dir_config_changes(ConfigChanges) |
671 |
|
catch exit:Reason2 -> |
672 |
:-( |
?LOG_ERROR(format_error(Reason2)) |
673 |
|
end, |
674 |
:-( |
try apply_relay_config_changes(ConfigChanges) |
675 |
|
catch exit:Reason3 -> |
676 |
:-( |
?LOG_ERROR(format_error(Reason3)) |
677 |
|
end, |
678 |
:-( |
try apply_listener_config_changes(ConfigChanges, State) |
679 |
|
catch exit:Reason4 -> |
680 |
:-( |
?LOG_ERROR(format_error(Reason4)), |
681 |
:-( |
State |
682 |
|
end. |
683 |
|
|
684 |
|
-spec apply_logging_config_changes(config_changes()) -> ok. |
685 |
|
apply_logging_config_changes(ConfigChanges) -> |
686 |
:-( |
case logging_config_changed(ConfigChanges) of |
687 |
|
true -> |
688 |
:-( |
?LOG_INFO("Using new logging configuration"), |
689 |
:-( |
ok = eturnal_logger:reconfigure(); |
690 |
|
false -> |
691 |
:-( |
?LOG_DEBUG("Logging configuration unchanged") |
692 |
|
end. |
693 |
|
|
694 |
|
-spec apply_run_dir_config_changes(config_changes()) -> ok. |
695 |
|
apply_run_dir_config_changes(ConfigChanges) -> |
696 |
:-( |
case run_dir_config_changed(ConfigChanges) of |
697 |
|
true -> |
698 |
:-( |
?LOG_INFO("Using new run directory configuration"), |
699 |
:-( |
ok = ensure_run_dir(), |
700 |
:-( |
case check_pem_file() of |
701 |
|
ok -> |
702 |
:-( |
ok = fast_tls:clear_cache(); |
703 |
|
unchanged -> |
704 |
:-( |
ok |
705 |
|
end; |
706 |
|
false -> |
707 |
:-( |
?LOG_DEBUG("Run directory configuration unchanged") |
708 |
|
end. |
709 |
|
|
710 |
|
-spec apply_relay_config_changes(config_changes()) -> ok. |
711 |
|
apply_relay_config_changes(ConfigChanges) -> |
712 |
:-( |
case relay_config_changed(ConfigChanges) of |
713 |
|
true -> |
714 |
:-( |
?LOG_INFO("Using new TURN relay configuration"), |
715 |
:-( |
ok = log_relay_addresses(); |
716 |
|
false -> |
717 |
:-( |
?LOG_DEBUG("TURN relay configuration unchanged") |
718 |
|
end. |
719 |
|
|
720 |
|
-spec apply_listener_config_changes(config_changes(), state()) -> state(). |
721 |
|
apply_listener_config_changes(ConfigChanges, State) -> |
722 |
:-( |
case listener_config_changed(ConfigChanges) of |
723 |
|
true -> |
724 |
:-( |
?LOG_INFO("Using new listener configuration"), |
725 |
:-( |
ok = check_turn_config(), |
726 |
:-( |
ok = check_proxy_config(), |
727 |
:-( |
ok = stop_listeners(State), |
728 |
:-( |
ok = timer:sleep(500), |
729 |
:-( |
Listeners = start_listeners(), |
730 |
:-( |
State#eturnal_state{listeners = Listeners}; |
731 |
|
false -> |
732 |
:-( |
?LOG_DEBUG("Listener configuration unchanged"), |
733 |
:-( |
State |
734 |
|
end. |
735 |
|
|
736 |
|
-spec logging_config_changed(config_changes()) -> boolean(). |
737 |
|
logging_config_changed({Changed, New, Removed}) -> |
738 |
2 |
ModifiedKeys = proplists:get_keys(Changed ++ New ++ Removed), |
739 |
2 |
LoggingKeys = [log_dir, |
740 |
|
log_level, |
741 |
|
log_rotate_size, |
742 |
|
log_rotate_count], |
743 |
2 |
lists:any(fun(Key) -> lists:member(Key, ModifiedKeys) end, LoggingKeys). |
744 |
|
|
745 |
|
-spec run_dir_config_changed(config_changes()) -> boolean(). |
746 |
|
run_dir_config_changed({Changed, New, Removed}) -> |
747 |
4 |
ModifiedKeys = proplists:get_keys(Changed ++ New ++ Removed), |
748 |
4 |
RunDirKeys = [run_dir], |
749 |
4 |
lists:any(fun(Key) -> lists:member(Key, ModifiedKeys) end, RunDirKeys). |
750 |
|
|
751 |
|
-spec relay_config_changed(config_changes()) -> boolean(). |
752 |
|
relay_config_changed({Changed, New, Removed}) -> |
753 |
4 |
ModifiedKeys = proplists:get_keys(Changed ++ New ++ Removed), |
754 |
4 |
RelayKeys = [relay_ipv4_addr, |
755 |
|
relay_ipv6_addr, |
756 |
|
relay_min_port, |
757 |
|
relay_max_port], |
758 |
4 |
lists:any(fun(Key) -> lists:member(Key, ModifiedKeys) end, RelayKeys). |
759 |
|
|
760 |
|
-spec listener_config_changed(config_changes()) -> boolean(). |
761 |
|
listener_config_changed({Changed, New, Removed} = ConfigChanges) -> |
762 |
2 |
case relay_config_changed(ConfigChanges) or |
763 |
|
run_dir_config_changed(ConfigChanges) of |
764 |
|
true -> |
765 |
:-( |
true; |
766 |
|
false -> |
767 |
2 |
ModifiedKeys = proplists:get_keys(Changed ++ New ++ Removed), |
768 |
2 |
ListenerKeys = [listen, |
769 |
|
max_allocations, |
770 |
|
max_permissions, |
771 |
|
max_bps, |
772 |
|
blacklist, |
773 |
|
whitelist, |
774 |
|
blacklist_clients, |
775 |
|
whitelist_clients, |
776 |
|
blacklist_peers, |
777 |
|
whitelist_peers, |
778 |
|
realm, |
779 |
|
software_name, |
780 |
|
tls_options, |
781 |
|
tls_ciphers, |
782 |
|
tls_dh_file], |
783 |
2 |
lists:any(fun(Key) -> |
784 |
19 |
lists:member(Key, ModifiedKeys) |
785 |
|
end, ListenerKeys) |
786 |
|
end. |
787 |
|
|
788 |
|
%% Internal functions: PEM file handling. |
789 |
|
|
790 |
|
-spec get_pem_file_path() -> file:filename_all(). |
791 |
|
get_pem_file_path() -> |
792 |
6 |
filename:join(get_opt(run_dir), <<?PEM_FILE_NAME>>). |
793 |
|
|
794 |
|
-spec check_pem_file() -> ok | unchanged. |
795 |
|
check_pem_file() -> |
796 |
3 |
case tls_enabled() of |
797 |
|
true -> |
798 |
3 |
OutFile = get_pem_file_path(), |
799 |
3 |
case {get_opt(tls_crt_file), filelib:last_modified(OutFile)} of |
800 |
|
{none, OutTime} when OutTime =/= 0 -> |
801 |
1 |
?LOG_DEBUG("Keeping PEM file (~ts)", [OutFile]), |
802 |
1 |
unchanged; |
803 |
|
{none, OutTime} when OutTime =:= 0 -> |
804 |
2 |
?LOG_WARNING("TLS enabled without 'tls_crt_file', creating " |
805 |
:-( |
"self-signed certificate"), |
806 |
2 |
ok = create_self_signed(OutFile); |
807 |
|
{CrtFile, OutTime} -> |
808 |
:-( |
case filelib:last_modified(CrtFile) of |
809 |
|
CrtTime when CrtTime =< OutTime -> |
810 |
:-( |
?LOG_DEBUG("Keeping PEM file (~ts)", [OutFile]), |
811 |
:-( |
unchanged; |
812 |
|
CrtTime when CrtTime =/= 0 -> % Assert to be true. |
813 |
:-( |
?LOG_DEBUG("Updating PEM file (~ts)", [OutFile]), |
814 |
:-( |
ok = import_pem_file(CrtFile, OutFile) |
815 |
|
end |
816 |
|
end; |
817 |
|
false -> |
818 |
:-( |
?LOG_DEBUG("TLS not enabled, ignoring certificate configuration"), |
819 |
:-( |
unchanged |
820 |
|
end. |
821 |
|
|
822 |
|
-spec import_pem_file(binary(), file:filename_all()) -> ok. |
823 |
|
import_pem_file(CrtFile, OutFile) -> |
824 |
:-( |
try |
825 |
:-( |
ok = touch(OutFile), |
826 |
:-( |
case get_opt(tls_key_file) of |
827 |
|
KeyFile when is_binary(KeyFile) -> |
828 |
:-( |
ok = copy_file(KeyFile, OutFile, write); |
829 |
|
none -> |
830 |
:-( |
?LOG_INFO("No 'tls_key_file' specified, assuming key in ~ts", |
831 |
:-( |
[CrtFile]) |
832 |
|
end, |
833 |
:-( |
ok = copy_file(CrtFile, OutFile, append) |
834 |
|
catch error:{_, {error, Reason}} -> |
835 |
:-( |
exit({pem_failure, OutFile, Reason}) |
836 |
|
end. |
837 |
|
|
838 |
|
-spec copy_file(file:name_all(), file:name_all(), write | append) -> ok. |
839 |
|
copy_file(Src, Dst, Mode) -> |
840 |
:-( |
SrcMode = [read, binary, raw], |
841 |
:-( |
DstMode = [Mode, binary, raw], |
842 |
:-( |
{ok, _} = file:copy({Src, SrcMode}, {Dst, DstMode}), |
843 |
:-( |
?LOG_DEBUG("Copied ~ts into ~ts", [Src, Dst]). |
844 |
|
|
845 |
|
-spec touch(file:filename_all()) -> ok. |
846 |
|
touch(File) -> |
847 |
2 |
{ok, Fd} = file:open(File, [append, binary, raw]), |
848 |
2 |
ok = file:close(Fd), |
849 |
2 |
ok = file:change_mode(File, 8#00600). |
850 |
|
|
851 |
|
%% Internal functions: run directory. |
852 |
|
|
853 |
|
-spec ensure_run_dir() -> ok. |
854 |
|
ensure_run_dir() -> |
855 |
1 |
RunDir = get_opt(run_dir), |
856 |
1 |
case filelib:ensure_dir(filename:join(RunDir, <<"file">>)) of |
857 |
|
ok -> |
858 |
1 |
?LOG_DEBUG("Using run directory ~ts", [RunDir]); |
859 |
|
{error, Reason} -> |
860 |
:-( |
exit({run_dir_failure, create, RunDir, Reason}) |
861 |
|
end. |
862 |
|
|
863 |
|
-spec clean_run_dir() -> ok. |
864 |
|
clean_run_dir() -> |
865 |
1 |
PEMFile = get_pem_file_path(), |
866 |
1 |
case filelib:is_regular(PEMFile) of |
867 |
|
true -> |
868 |
1 |
case file:delete(PEMFile) of |
869 |
|
ok -> |
870 |
1 |
?LOG_DEBUG("Removed ~ts", [PEMFile]); |
871 |
|
{error, Reason} -> |
872 |
:-( |
exit({run_dir_failure, clean, PEMFile, Reason}) |
873 |
|
end; |
874 |
|
false -> |
875 |
:-( |
?LOG_DEBUG("PEM file doesn't exist: ~ts", [PEMFile]) |
876 |
|
end. |
877 |
|
|
878 |
|
%% Internal functions: error message formatting. |
879 |
|
|
880 |
|
-spec format_error(atom() | tuple()) -> binary(). |
881 |
|
format_error({module_failure, Action, Mod, Reason}) -> |
882 |
:-( |
format("Failed to ~s ~s: ~p", [Action, Mod, Reason]); |
883 |
|
format_error({dependency_failure, Mod, Dep}) -> |
884 |
:-( |
format("Dependency ~s is missing; install it below ~s, or point ERL_LIBS " |
885 |
|
"to it, or disable ~s", [Dep, code:lib_dir(), Mod]); |
886 |
|
format_error({listener_failure, Action, IP, Port, Transport, Reason}) -> |
887 |
:-( |
format("Cannot ~s listening on ~s (~s): ~s", |
888 |
|
[Action, eturnal_misc:addr_to_str(IP, Port), Transport, |
889 |
|
inet:format_error(Reason)]); |
890 |
|
format_error({run_dir_failure, Action, RunDir, Reason}) -> |
891 |
:-( |
format("Cannot ~s run directory ~ts: ~ts", |
892 |
|
[Action, RunDir, file:format_error(Reason)]); |
893 |
|
format_error({pem_failure, File, Reason}) -> |
894 |
:-( |
format("Cannot create PEM file ~ts: ~ts", |
895 |
|
[File, file:format_error(Reason)]); |
896 |
|
format_error({otp_too_old, Key, Value, Vsn}) -> |
897 |
:-( |
format("Setting '~s: ~s' requires Erlang/OTP ~B or later", |
898 |
|
[Key, Value, Vsn]); |
899 |
|
format_error(proxy_config_failure) -> |
900 |
:-( |
<<"The 'proxy_protocol' ist not supported for 'udp'">>; |
901 |
|
format_error(turn_config_failure) -> |
902 |
:-( |
<<"The 'relay_max_port' must be larger than the 'relay_min_port'">>; |
903 |
|
format_error(_Unknown) -> |
904 |
:-( |
<<"Unknown error">>. |
905 |
|
|
906 |
|
-spec format(io:format(), [term()]) -> binary(). |
907 |
|
format(Fmt, Data) -> |
908 |
:-( |
case unicode:characters_to_binary(io_lib:format(Fmt, Data)) of |
909 |
|
Bin when is_binary(Bin) -> |
910 |
:-( |
Bin; |
911 |
|
{_, _, _} = Err -> |
912 |
:-( |
erlang:error(Err) |
913 |
|
end. |
914 |
|
|
915 |
|
%% EUnit tests. |
916 |
|
|
917 |
|
-ifdef(EUNIT). |
918 |
|
config_change_test_() -> |
919 |
4 |
[?_assert(logging_config_changed({[{log_level, info}], [], []})), |
920 |
1 |
?_assert(run_dir_config_changed({[{run_dir, <<"run">>}], [], []})), |
921 |
1 |
?_assert(relay_config_changed({[{relay_min_port, 50000}], [], []})), |
922 |
1 |
?_assert(listener_config_changed({[{max_bps, 42}], [], []})), |
923 |
1 |
?_assertNot(logging_config_changed({[{strict_expiry, false}], [], []})), |
924 |
1 |
?_assertNot(run_dir_config_changed({[{strict_expiry, false}], [], []})), |
925 |
1 |
?_assertNot(relay_config_changed({[{strict_expiry, false}], [], []})), |
926 |
1 |
?_assertNot(listener_config_changed({[{strict_expiry, false}], [], []}))]. |
927 |
|
-endif. |