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

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