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). |