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