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