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