/home/runner/work/eturnal/eturnal/_build/test/cover/eunit/eturnal_yaml.html

1 %%% eturnal STUN/TURN server.
2 %%%
3 %%% Copyright (c) 2020-2025 Holger Weiss <holger@zedat.fu-berlin.de>.
4 %%% Copyright (c) 2020-2025 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_yaml).
20 -behaviour(conf).
21 -export([validator/0]).
22 -import(yval, [and_then/2, any/0, beam/1, binary/0, bool/0, directory/1,
23 either/2, enum/1, file/1, int/2, ip/0, ipv4/0, ipv6/0, ip_mask/0,
24 list/1, list/2, list_or_single/1, map/3, non_empty/1,
25 non_neg_int/0, options/1, options/2, port/0, pos_int/1]).
26
27 -include_lib("kernel/include/logger.hrl").
28
29 -define(RECOMMENDED_BLACKLIST,
30 [{{127, 0, 0, 0}, 8},
31 {{10, 0, 0, 0}, 8},
32 {{100, 64, 0, 0}, 10},
33 {{169, 254, 0, 0}, 16},
34 {{172, 16, 0, 0}, 12},
35 {{192, 0, 0, 0}, 24},
36 {{192, 0, 2, 0}, 24},
37 {{192, 88, 99, 0}, 24},
38 {{192, 168, 0, 0}, 16},
39 {{198, 18, 0, 0}, 15},
40 {{198, 51, 100, 0}, 24},
41 {{203, 0, 113, 0}, 24},
42 {{224, 0, 0, 0}, 4},
43 {{240, 0, 0, 0}, 4},
44 {{0, 0, 0, 0, 0, 0, 0, 1}, 128},
45 {{100, 65435, 0, 0, 0, 0, 0, 0}, 96},
46 {{256, 0, 0, 0, 0, 0, 0, 0}, 64},
47 {{64512, 0, 0, 0, 0, 0, 0, 0}, 7},
48 {{65152, 0, 0, 0, 0, 0, 0, 0}, 10},
49 {{65280, 0, 0, 0, 0, 0, 0, 0}, 8}]).
50
51 -type listener() :: {inet:ip_address(), inet:port_number(), eturnal:transport(),
52 boolean(), boolean()}.
53 -type family() :: ipv4 | ipv6.
54 -type boundary() :: min | max.
55
56 %% API.
57
58 -spec validator() -> yval:validator().
59 validator() ->
60
:-(
options(
61 #{secret => list_or_single(non_empty(binary())),
62 listen => listen_validator(),
63 relay_ipv4_addr => and_then(either(none, ipv4()),
64 fun check_relay_addr/1),
65 relay_ipv6_addr => and_then(either(none, ipv6()),
66 fun check_relay_addr/1),
67 relay_min_port => int(1025, 65535),
68 relay_max_port => int(1025, 65535),
69 tls_crt_file => file(read),
70 tls_key_file => file(read),
71 tls_dh_file => file(read),
72 tls_options => openssl_list($|),
73 tls_ciphers => openssl_list($:),
74 max_allocations => pos_int(unlimited),
75 max_permissions => pos_int(unlimited),
76 max_bps => and_then(pos_int(unlimited),
77
:-(
fun(unlimited) -> none;
78
:-(
(I) -> I
79 end),
80 blacklist => blacklist_validator(),
81 whitelist => list_or_single(ip_mask()),
82 blacklist_clients => list_or_single(ip_mask()),
83 whitelist_clients => list_or_single(ip_mask()),
84 blacklist_peers => blacklist_validator(),
85 whitelist_peers => list_or_single(ip_mask()),
86 strict_expiry => bool(),
87 credentials => map(binary(), binary(), [unique, {return, map}]),
88 realm => non_empty(binary()),
89 software_name => either(none, non_empty(binary())),
90 run_dir => directory(write),
91 log_dir => either(stdout, directory(write)),
92 log_level => enum([critical, error, warning, notice, info, debug]),
93 log_rotate_size => pos_int(infinity),
94 log_rotate_count => non_neg_int(),
95 modules => module_validator()},
96 [unique,
97 {required, []},
98 {defaults,
99 #{listen => [{{0, 0, 0, 0, 0, 0, 0, 0}, 3478, udp, false, true},
100 {{0, 0, 0, 0, 0, 0, 0, 0}, 3478, tcp, false, true}],
101 relay_ipv4_addr => get_default_addr(ipv4),
102 relay_ipv6_addr => get_default_addr(ipv6),
103 relay_min_port => get_default_port(min, 49152),
104 relay_max_port => get_default_port(max, 65535),
105 tls_crt_file => none,
106 tls_key_file => none,
107 tls_dh_file => none,
108 tls_options => <<"cipher_server_preference">>,
109 tls_ciphers => <<"HIGH:!aNULL:@STRENGTH">>,
110 max_allocations => 10,
111 max_permissions => 10,
112 max_bps => none,
113 blacklist => [],
114 whitelist => [],
115 blacklist_clients => [],
116 whitelist_clients => [],
117 blacklist_peers => ?RECOMMENDED_BLACKLIST,
118 whitelist_peers => [],
119 strict_expiry => false,
120 credentials => #{},
121 realm => <<"eturnal.net">>,
122 secret => [get_default(secret, make_random_secret())],
123 software_name => <<"eturnal">>,
124 run_dir => get_default("RUNTIME_DIRECTORY", <<"run">>),
125 log_dir => get_default("LOGS_DIRECTORY", <<"log">>),
126 log_level => info,
127 log_rotate_size => infinity,
128 log_rotate_count => 10,
129 modules => #{}}}]).
130
131 %% Internal functions.
132
133 -spec blacklist_validator() -> yval:validator().
134 blacklist_validator() ->
135
:-(
and_then(
136 list_or_single(either(recommended, ip_mask())),
137 fun(L) ->
138
:-(
lists:usort(
139 lists:flatmap(
140 fun(recommended) ->
141
:-(
?RECOMMENDED_BLACKLIST;
142 (Network) ->
143
:-(
[Network]
144 end, L))
145 end).
146
147 -spec module_validator() -> yval:validator().
148 module_validator() ->
149
:-(
and_then(
150 map(
151 beam([{handle_event, 2}, {options, 0}]),
152 options(#{'_' => any()}),
153 [unique]),
154 fun(L) ->
155
:-(
lists:foldl(
156 fun({Mod, Opts}, Acc) ->
157
:-(
{Validators,
158 ValidatorOpts0} = eturnal_module:options(Mod),
159
:-(
ValidatorOpts = [unique,
160 {return, map} | ValidatorOpts0],
161
:-(
Acc#{Mod => (options(Validators, ValidatorOpts))(Opts)}
162 end, #{}, L)
163 end).
164
165 -spec listen_validator() -> yval:validator().
166 listen_validator() ->
167
:-(
and_then(
168 list(
169 and_then(
170 options(
171 #{ip => ip(),
172 port => port(),
173 transport => enum([tcp, udp, tls, auto]),
174 proxy_protocol => bool(),
175 enable_turn => bool()}),
176 fun(Opts) ->
177
:-(
DefP = fun(udp) -> 3478;
178
:-(
(tcp) -> 3478;
179
:-(
(tls) -> 5349;
180
:-(
(auto) -> 3478
181 end,
182
:-(
I = proplists:get_value(ip, Opts, {0, 0, 0, 0, 0, 0, 0, 0}),
183
:-(
T = proplists:get_value(transport, Opts, udp),
184
:-(
P = proplists:get_value(port, Opts, DefP(T)),
185
:-(
X = proplists:get_value(proxy_protocol, Opts, false),
186
:-(
E = proplists:get_value(enable_turn, Opts, true),
187
:-(
{I, P, T, X, E}
188 end)),
189 fun check_overlapping_listeners/1).
190
191 -spec check_overlapping_listeners([listener()]) -> [listener()].
192 check_overlapping_listeners(Listeners) ->
193
:-(
ok = check_overlapping_listeners(Listeners, fun(L) -> L end),
194
:-(
ok = check_overlapping_listeners(Listeners, fun lists:reverse/1),
195
:-(
Listeners.
196
197 -spec check_overlapping_listeners([listener()],
198 fun(([listener()]) -> [listener()]))
199 -> ok.
200 check_overlapping_listeners(Listeners, PrepareFun) ->
201
:-(
_ = lists:foldl(
202 fun({IP, Port, Transport, _ProxyProtocol, _EnableTURN} = Listener,
203 Acc) ->
204
:-(
Key = case Transport of
205 udp ->
206
:-(
{IP, Port, udp};
207 _ ->
208
:-(
{IP, Port, tcp}
209 end,
210
:-(
case lists:member(Key, Acc) of
211 true ->
212
:-(
fail({duplicated_value,
213 format_listener(Listener)});
214 false ->
215 % With dual-stack sockets, we won't detect conflicts
216 % of IPv4 addresses with "::".
217
:-(
AnyIP = case tuple_size(IP) of
218
:-(
8 -> {0, 0, 0, 0, 0, 0, 0, 0};
219
:-(
4 -> {0, 0, 0, 0}
220 end,
221
:-(
Key1 = {AnyIP, Port, Transport},
222
:-(
case lists:member(Key1, Acc) of
223 true ->
224
:-(
fail({duplicated_value,
225 format_listener(Listener)});
226 false ->
227
:-(
[Key | Acc]
228 end
229 end
230 end, [], PrepareFun(Listeners)),
231
:-(
ok.
232
233 -spec format_listener(listener()) -> binary().
234 format_listener({IP, Port, Transport, _ProxyProtocol, _EnableTURN}) ->
235
:-(
Addr = eturnal_misc:addr_to_str(IP, Port),
236
:-(
list_to_binary(io_lib:format("~s (~s)", [Addr, Transport])).
237
238 -spec check_relay_addr(inet:ip_address() | none)
239 -> inet:ip_address() | undefined.
240 check_relay_addr(none) ->
241
:-(
undefined;
242 check_relay_addr({0, 0, 0, 0} = Addr) ->
243
:-(
fail({bad_ip, inet:ntoa(Addr)});
244 check_relay_addr({_, _, _, _} = Addr) ->
245
:-(
Addr;
246 check_relay_addr({0, 0, 0, 0, 0, 0, 0, 0} = Addr) ->
247
:-(
fail({bad_ip, inet:ntoa(Addr)});
248 check_relay_addr({_, _, _, _, _, _, _, _} = Addr) ->
249
:-(
Addr.
250
251 -spec get_default_addr(family()) -> inet:ip_address() | undefined.
252 get_default_addr(Family) ->
253
:-(
{Vsn, Opt, ParseAddr, MyAddr} =
254 case Family of
255 ipv4 ->
256
:-(
{4, relay_ipv4_addr,
257 fun inet:parse_ipv4strict_address/1,
258 fun eturnal_misc:my_ipv4_addr/0};
259 ipv6 ->
260
:-(
{6, relay_ipv6_addr,
261 fun inet:parse_ipv6strict_address/1,
262 fun eturnal_misc:my_ipv6_addr/0}
263 end,
264
:-(
case get_default(Opt, undefined) of
265 RelayAddr when is_binary(RelayAddr) ->
266
:-(
try
267
:-(
{ok, Addr} = ParseAddr(binary_to_list(RelayAddr)),
268
:-(
check_relay_addr(Addr)
269 catch error:_ ->
270
:-(
abort("Bad ETURNAL_RELAY_IPV~B_ADDR: ~s", [Vsn, RelayAddr])
271 end;
272 undefined ->
273
:-(
MyAddr()
274 end.
275
276 -spec get_default_port(boundary(), inet:port_number()) -> inet:port_number().
277 get_default_port(MinMax, Default) ->
278
:-(
MinMaxStr = from_atom(MinMax),
279
:-(
Opt = to_atom(<<"relay_", MinMaxStr/binary, "_port">>),
280
:-(
case get_default(Opt, Default) of
281 Bin when is_binary(Bin) ->
282
:-(
try
283
:-(
Port = binary_to_integer(Bin),
284
:-(
true = (Port >= 1025),
285
:-(
true = (Port =< 65535),
286
:-(
Port
287 catch error:_ ->
288
:-(
abort("Bad ETURNAL_RELAY_~s_PORT: ~s",
289 [string:uppercase(MinMaxStr), Bin])
290 end;
291 Default ->
292
:-(
Default
293 end.
294
295 -spec get_default(atom() | string(), Term) -> binary() | Term.
296 get_default(Opt, Default) when is_atom(Opt) ->
297
:-(
get_default(get_env_name(Opt), Default);
298 get_default(Var, Default) ->
299
:-(
case os:getenv(Var) of
300 Val when is_list(Val), length(Val) > 0 ->
301
:-(
unicode:characters_to_binary(Val);
302 _ ->
303
:-(
Default
304 end.
305
306 -spec get_env_name(atom()) -> string().
307 get_env_name(Opt) ->
308
:-(
"ETURNAL_" ++ string:uppercase(atom_to_list(Opt)).
309
310 -spec make_random_secret() -> binary().
311 -ifdef(old_rand).
312 make_random_secret() ->
313 <<(rand:uniform(1 bsl 127)):128>>.
314 -else.
315 make_random_secret() ->
316
:-(
rand:bytes(16).
317 -endif.
318
319 -spec openssl_list(char()) -> fun((binary() | [binary()]) -> binary()).
320 openssl_list(Sep) ->
321
:-(
fun(L) when is_list(L) ->
322
:-(
(and_then(list(binary(), [unique]), join(Sep)))(L);
323 (B) ->
324
:-(
(binary())(B)
325 end.
326
327 -spec join(char()) -> fun(([binary()]) -> binary()).
328 join(Sep) ->
329
:-(
fun(Opts) -> unicode:characters_to_binary(lists:join(<<Sep>>, Opts)) end.
330
331 -spec from_atom(atom()) -> binary().
332 -spec to_atom(binary()) -> atom().
333 -ifdef(old_atom_conversion). % Erlang/OTP < 23.0.
334 from_atom(A) -> atom_to_binary(A, utf8).
335 to_atom(S) -> list_to_existing_atom(binary_to_list(S)).
336 -else.
337
:-(
from_atom(A) -> atom_to_binary(A).
338
:-(
to_atom(S) -> binary_to_existing_atom(S).
339 -endif.
340
341 -spec fail({atom(), term()}) -> no_return().
342 fail(Reason) ->
343
:-(
yval:fail(yval, Reason).
344
345 -spec abort(io:format(), [term()]) -> no_return().
346 abort(Format, Data) ->
347
:-(
?LOG_CRITICAL(Format, Data),
348
:-(
eturnal_logger:flush(),
349
:-(
halt(2).
Line Hits Source