| 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 |
3 |
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 |
3 |
(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 |
6 |
and_then( |
| 136 |
|
list_or_single(either(recommended, ip_mask())), |
| 137 |
|
fun(L) -> |
| 138 |
3 |
lists:usort( |
| 139 |
|
lists:flatmap( |
| 140 |
|
fun(recommended) -> |
| 141 |
3 |
?RECOMMENDED_BLACKLIST; |
| 142 |
|
(Network) -> |
| 143 |
6 |
[Network] |
| 144 |
|
end, L)) |
| 145 |
|
end). |
| 146 |
|
|
| 147 |
|
-spec module_validator() -> yval:validator(). |
| 148 |
|
module_validator() -> |
| 149 |
3 |
and_then( |
| 150 |
|
map( |
| 151 |
|
beam([{handle_event, 2}, {options, 0}]), |
| 152 |
|
options(#{'_' => any()}), |
| 153 |
|
[unique]), |
| 154 |
|
fun(L) -> |
| 155 |
3 |
lists:foldl( |
| 156 |
|
fun({Mod, Opts}, Acc) -> |
| 157 |
9 |
{Validators, |
| 158 |
|
ValidatorOpts0} = eturnal_module:options(Mod), |
| 159 |
9 |
ValidatorOpts = [unique, |
| 160 |
|
{return, map} | ValidatorOpts0], |
| 161 |
9 |
Acc#{Mod => (options(Validators, ValidatorOpts))(Opts)} |
| 162 |
|
end, #{}, L) |
| 163 |
|
end). |
| 164 |
|
|
| 165 |
|
-spec listen_validator() -> yval:validator(). |
| 166 |
|
listen_validator() -> |
| 167 |
3 |
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 |
12 |
DefP = fun(udp) -> 3478; |
| 178 |
3 |
(tcp) -> 3478; |
| 179 |
3 |
(tls) -> 5349; |
| 180 |
3 |
(auto) -> 3478 |
| 181 |
|
end, |
| 182 |
12 |
I = proplists:get_value(ip, Opts, {0, 0, 0, 0, 0, 0, 0, 0}), |
| 183 |
12 |
T = proplists:get_value(transport, Opts, udp), |
| 184 |
12 |
P = proplists:get_value(port, Opts, DefP(T)), |
| 185 |
12 |
X = proplists:get_value(proxy_protocol, Opts, false), |
| 186 |
12 |
E = proplists:get_value(enable_turn, Opts, true), |
| 187 |
12 |
{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 |
3 |
ok = check_overlapping_listeners(Listeners, fun(L) -> L end), |
| 194 |
3 |
ok = check_overlapping_listeners(Listeners, fun lists:reverse/1), |
| 195 |
3 |
Listeners. |
| 196 |
|
|
| 197 |
|
-spec check_overlapping_listeners([listener()], |
| 198 |
|
fun(([listener()]) -> [listener()])) |
| 199 |
|
-> ok. |
| 200 |
|
check_overlapping_listeners(Listeners, PrepareFun) -> |
| 201 |
6 |
_ = lists:foldl( |
| 202 |
|
fun({IP, Port, Transport, _ProxyProtocol, _EnableTURN} = Listener, |
| 203 |
|
Acc) -> |
| 204 |
24 |
Key = case Transport of |
| 205 |
|
udp -> |
| 206 |
6 |
{IP, Port, udp}; |
| 207 |
|
_ -> |
| 208 |
18 |
{IP, Port, tcp} |
| 209 |
|
end, |
| 210 |
24 |
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 |
24 |
AnyIP = case tuple_size(IP) of |
| 218 |
:-( |
8 -> {0, 0, 0, 0, 0, 0, 0, 0}; |
| 219 |
24 |
4 -> {0, 0, 0, 0} |
| 220 |
|
end, |
| 221 |
24 |
Key1 = {AnyIP, Port, Transport}, |
| 222 |
24 |
case lists:member(Key1, Acc) of |
| 223 |
|
true -> |
| 224 |
:-( |
fail({duplicated_value, |
| 225 |
|
format_listener(Listener)}); |
| 226 |
|
false -> |
| 227 |
24 |
[Key | Acc] |
| 228 |
|
end |
| 229 |
|
end |
| 230 |
|
end, [], PrepareFun(Listeners)), |
| 231 |
6 |
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 |
3 |
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 |
6 |
{Vsn, Opt, ParseAddr, MyAddr} = |
| 254 |
|
case Family of |
| 255 |
|
ipv4 -> |
| 256 |
3 |
{4, relay_ipv4_addr, |
| 257 |
|
fun inet:parse_ipv4strict_address/1, |
| 258 |
|
fun eturnal_misc:my_ipv4_addr/0}; |
| 259 |
|
ipv6 -> |
| 260 |
3 |
{6, relay_ipv6_addr, |
| 261 |
|
fun inet:parse_ipv6strict_address/1, |
| 262 |
|
fun eturnal_misc:my_ipv6_addr/0} |
| 263 |
|
end, |
| 264 |
6 |
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 |
6 |
MyAddr() |
| 274 |
|
end. |
| 275 |
|
|
| 276 |
|
-spec get_default_port(boundary(), inet:port_number()) -> inet:port_number(). |
| 277 |
|
get_default_port(MinMax, Default) -> |
| 278 |
6 |
MinMaxStr = from_atom(MinMax), |
| 279 |
6 |
Opt = to_atom(<<"relay_", MinMaxStr/binary, "_port">>), |
| 280 |
6 |
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 |
6 |
Default |
| 293 |
|
end. |
| 294 |
|
|
| 295 |
|
-spec get_default(atom() | string(), Term) -> binary() | Term. |
| 296 |
|
get_default(Opt, Default) when is_atom(Opt) -> |
| 297 |
15 |
get_default(get_env_name(Opt), Default); |
| 298 |
|
get_default(Var, Default) -> |
| 299 |
21 |
case os:getenv(Var) of |
| 300 |
|
Val when is_list(Val), length(Val) > 0 -> |
| 301 |
:-( |
unicode:characters_to_binary(Val); |
| 302 |
|
_ -> |
| 303 |
21 |
Default |
| 304 |
|
end. |
| 305 |
|
|
| 306 |
|
-spec get_env_name(atom()) -> string(). |
| 307 |
|
get_env_name(Opt) -> |
| 308 |
15 |
"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 |
3 |
rand:bytes(16). |
| 317 |
|
-endif. |
| 318 |
|
|
| 319 |
|
-spec openssl_list(char()) -> fun((binary() | [binary()]) -> binary()). |
| 320 |
|
openssl_list(Sep) -> |
| 321 |
6 |
fun(L) when is_list(L) -> |
| 322 |
6 |
(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 |
6 |
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 |
6 |
from_atom(A) -> atom_to_binary(A). |
| 338 |
6 |
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). |