%%--------------------------------------------------------------------
%% Copyright (c) 2020-2024 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(quicer_nif).
-export([
open_lib/0,
close_lib/0,
reg_open/0,
reg_open/1,
reg_close/0,
new_registration/2,
shutdown_registration/1,
shutdown_registration/3,
close_registration/1,
get_registration_name/1,
get_registration_refcnt/1,
listen/2,
start_listener/3,
stop_listener/1,
close_listener/1,
async_connect/3,
async_accept/2,
async_handshake/1,
async_handshake/2,
async_shutdown_connection/3,
async_accept_stream/2,
start_stream/2,
csend/4,
send/3,
recv/2,
send_dgram/3,
async_shutdown_stream/3,
sockname/1,
getopt/3,
setopt/4,
controlling_process/2,
peercert/1,
complete_cert_validation/3,
enable_sig_buffer/1,
flush_stream_buffered_sigs/1,
count_reg_conns/0,
count_reg_conns/1
]).
-export([
get_conn_rid/1,
get_stream_rid/1
]).
%% tools
-export([
malloc_trim/0,
malloc_stats/0
]).
%% For tests only
-export([
open_connection/0,
open_connection/1,
get_listeners/0,
get_listeners/1,
get_connections/0,
get_connections/1,
get_conn_owner/1,
get_stream_owner/1,
get_listener_owner/1,
copy_stream_handle/1,
mock_buffer_sig/3,
set_snab_kc_pid/1,
get_snab_kc_pid/0
]).
-export([abi_version/0]).
%% for test
-export([init/1]).
-export_type([
abi_version/0,
new_registration/0,
shutdown_registration/0,
close_registration/0,
get_registration_name/0,
get_registration_refcnt/0,
get_listeners/0,
get_connections/0,
get_owner/0,
reg_handle/0
]).
%% NIF fuction return types
-type abi_version() :: integer().
-type new_registration() :: {ok, reg_handle()} | {error, atom_reason()}.
-type shutdown_registration() :: ok | {error, badarg | invalid_state}.
-type close_registration() :: ok | {error, badarg}.
-type get_registration_name() :: {ok, string()} | {error, badarg}.
-type get_registration_refcnt() :: {error, closed} | integer().
-type get_listeners() :: [listener_handle()].
-type get_connections() :: [connection_handle()].
-type get_owner() :: {ok, pid()} | {error, undefined | badarg}.
%% @NOTE: In embedded mode, first all modules are loaded. Then all on_load functions are called.
-on_load(init/0).
-include_lib("kernel/include/file.hrl").
-include("quicer.hrl").
-include("quicer_types.hrl").
-include("quicer_vsn.hrl").
-define(NONIF_SWITCH, "QUICER_SKIP_NIF_LOAD").
-hank([
{unnecessary_function_arguments, [
{open_lib, 1},
{reg_open, 1}
]}
]).
-spec abi_version() -> abi_version().
abi_version() ->
?QUICER_ABI_VERSION.
-spec init() -> ok.
init() ->
ABIVsn =
case persistent_term:get({'_quicer_overrides_', abi_version}, undefined) of
undefined -> abi_version();
Vsn -> Vsn
end,
init(ABIVsn).
init(ABIVsn) ->
case os:getenv(?NONIF_SWITCH) of
"1" ->
io:format(
"~n~nWARN: Detected env ~s=1, QUIC module is loaded but will NOT be functional. This is not encouraged and take your own risk!~n~n",
[?NONIF_SWITCH]
),
ok;
_ ->
do_init(ABIVsn)
end.
%% `do_init` ensures success NIF loading.
do_init(ABIVsn) ->
NifName = "libquicer_nif",
{ok, Niflib} = locate_lib(priv_dir(), NifName),
case erlang:load_nif(Niflib, ABIVsn) of
ok ->
%% It could cause segfault if MsQuic library is not opened nor registered.
%% here we have added dummy calls, and it should cover most of cases
%% unless caller wants to call erlang:load_nif/1 and then call quicer_nif
%% without opened library to suicide.
%%
%% Note, we could do same dummy calls in nif instead but it might mess up the reference counts.
{ok, _} = open_lib(),
%% dummy reg open
case reg_open() of
ok ->
ok;
{error, badarg} ->
%% already opened
ok
end;
{error, _Reason} = Res ->
Res
end.
-spec open_lib() ->
%% opened
{ok, true}
%% already opened
| {ok, false}
%% opened with lttng debug library loaded (if present)
| {ok, debug}
| {ok, fake}
| {error, open_failed, atom_reason()}.
open_lib() ->
LibFile =
case locate_lib(priv_dir(), "lib/libmsquic.lttng.so") of
{ok, File} ->
File;
{error, _} ->
priv_dir()
end,
LBMode =
case application:get_env(quicer, lb_mode, 0) of
X when is_integer(X) ->
X;
DevName when is_list(DevName) ->
lb_server_id(ipv4, DevName)
end,
open_lib(#{
load_balacing_mode => LBMode,
trace => LibFile
}).
open_lib(_LttngLib) ->
maybe_fake_ret({ok, fake}).
-spec close_lib() -> ok.
close_lib() ->
erlang:nif_error(nif_library_not_loaded).
-spec reg_open() -> ok | {error, badarg | invalid_state}.
reg_open() ->
maybe_fake_ret(ok).
-spec reg_open(execution_profile()) -> ok | {error, badarg | invalid_state}.
reg_open(_) ->
maybe_fake_ret(ok).
-spec reg_close() -> ok.
reg_close() ->
maybe_fake_ret(ok).
-spec new_registration(Name :: string(), Profile :: registration_profile()) -> new_registration().
new_registration(_Name, _Profile) ->
erlang:nif_error(nif_library_not_loaded).
-spec shutdown_registration(global | reg_handle()) -> shutdown_registration().
shutdown_registration(_Handle) ->
erlang:nif_error(nif_library_not_loaded).
-spec shutdown_registration(global | reg_handle(), IsSilent :: boolean(), ErrorCode :: uint64()) ->
shutdown_registration().
shutdown_registration(_Handle, _IsSilent, _ErrorCode) ->
erlang:nif_error(nif_library_not_loaded).
-spec close_registration(reg_handle()) -> close_registration().
close_registration(_Handle) ->
erlang:nif_error(nif_library_not_loaded).
-spec get_registration_name(reg_handle()) -> get_registration_name().
get_registration_name(_Handle) ->
erlang:nif_error(nif_library_not_loaded).
-spec get_registration_refcnt(reg_handle()) -> get_registration_refcnt().
get_registration_refcnt(_Handle) ->
erlang:nif_error(nif_library_not_loaded).
-spec listen(listen_on(), listen_opts()) ->
{ok, listener_handle()}
| {error, listener_open_error, atom_reason()}
| {error, listener_start_error, atom_reason()}.
listen(_ListenOn, _Options) ->
erlang:nif_error(nif_library_not_loaded).
-spec start_listener(listener_handle(), listen_on(), listen_opts()) ->
ok | {error, closed | badarg}.
start_listener(_Listener, _ListenOn, _Opts) ->
erlang:nif_error(nif_library_not_loaded).
%% @doc close the listener
%% return closed if the listener is closed.
%% return ok if the listener is stopped then closed, and caller should expect listener_stopped signal.
%%
%% @end
-spec close_listener(listener_handle()) -> ok | closed | {error, closed | badarg}.
close_listener(_Listener) ->
erlang:nif_error(nif_library_not_loaded).
-spec stop_listener(listener_handle()) -> ok | {error, closed | listener_stopped | badarg}.
stop_listener(_Listener) ->
erlang:nif_error(nif_library_not_loaded).
-spec open_connection() -> {ok, connection_handle()} | {error, atom_reason()}.
open_connection() ->
erlang:nif_error(nif_library_not_loaded).
-spec open_connection(#{quic_registration => reg_handle()}) ->
{ok, connection_handle()} | {error, atom_reason()}.
open_connection(_) ->
erlang:nif_error(nif_library_not_loaded).
-spec async_connect(hostname(), inet:port_number(), conn_opts()) ->
{ok, connection_handle()}
| {error, conn_open_error | config_error | conn_start_error}
| {error, not_found, any()}.
async_connect(_Host, _Port, _Opts) ->
erlang:nif_error(nif_library_not_loaded).
-spec async_accept(listener_handle(), acceptor_opts()) ->
{ok, listener_handle()}
| {error, badarg | param_error | not_enough_mem | badpid}.
async_accept(_Listener, _Opts) ->
erlang:nif_error(nif_library_not_loaded).
-spec async_handshake(connection_handle()) ->
ok | {error, badarg | atom_reason()}.
async_handshake(_Connection) ->
erlang:nif_error(nif_library_not_loaded).
-spec async_handshake(connection_handle(), conn_opts()) ->
ok | {error, badarg | atom_reason()}.
async_handshake(_Connection, _ConnOpts) ->
erlang:nif_error(nif_library_not_loaded).
-spec async_shutdown_connection(connection_handle(), conn_shutdown_flag(), app_errno()) ->
ok | {error, badarg}.
async_shutdown_connection(_Conn, _Flags, _ErrorCode) ->
erlang:nif_error(nif_library_not_loaded).
-spec async_accept_stream(connection_handle(), stream_opts()) ->
{ok, connection_handle()}
| {error, badarg | internal_error | bad_pid | owner_dead}.
async_accept_stream(_Conn, _Opts) ->
erlang:nif_error(nif_library_not_loaded).
-spec start_stream(connection_handle(), stream_opts()) ->
{ok, stream_handle()}
| {error, badarg | internal_error | bad_pid | owner_dead | not_enough_mem}
| {error, stream_open_error, atom_reason()}
| {error, stream_start_error, atom_reason()}.
start_stream(_Conn, _Opts) ->
erlang:nif_error(nif_library_not_loaded).
-spec csend(connection_handle(), iodata(), stream_opts(), send_flags()) ->
{ok, BytesSent :: pos_integer()}
| {error, badarg | not_enough_mem | closed}
| {error, stream_send_error, atom_reason()}.
csend(_Conn, _Data, _Opts, _Flags) ->
erlang:nif_error(nif_library_not_loaded).
-spec send(stream_handle(), iodata(), send_flags()) ->
{ok, BytesSent :: pos_integer()}
| {error, badarg | not_enough_mem | closed}
| {error, stream_send_error, atom_reason()}.
send(_Stream, _Data, _Flags) ->
erlang:nif_error(nif_library_not_loaded).
-spec recv(stream_handle(), non_neg_integer()) ->
{ok, binary()}
| {ok, not_ready}
| {error, badarg | einval | closed}.
recv(_Stream, _Len) ->
erlang:nif_error(nif_library_not_loaded).
-spec send_dgram(connection_handle(), iodata(), send_flags()) ->
{ok, BytesSent :: pos_integer()}
| {error, badarg | not_enough_memory | invalid_parameter | closed}
| {error, dgram_send_error, atom_reason()}.
send_dgram(_Conn, _Data, _Flags) ->
erlang:nif_error(nif_library_not_loaded).
-spec async_shutdown_stream(stream_handle(), stream_shutdown_flags(), app_errno()) ->
ok
| {error, badarg | atom_reason()}.
async_shutdown_stream(_Stream, _Flags, _ErrorCode) ->
erlang:nif_error(nif_library_not_loaded).
-spec sockname(connection_handle() | stream_handle()) ->
{ok, {inet:ip_address(), inet:port_number()}}
| {error, badarg | sockname_error}.
sockname(_Conn) ->
erlang:nif_error(nif_library_not_loaded).
-spec getopt(handle(), optname(), optlevel()) ->
%% `optname' not found, or wrong `optlevel' must be a bug.
not_found
%% when optname = settings
| {ok, any()}
| {error, badarg | param_error | internal_error | not_enough_mem}
| {error, atom_reason()}.
getopt(_Handle, _Optname, _Level) ->
erlang:nif_error(nif_library_not_loaded).
-spec setopt(handle(), optname(), any(), optlevel()) ->
ok
| {error, badarg | param_error | internal_error | not_enough_mem}
| {error, atom_reason()}.
setopt(_Handle, _Opt, _Value, _Level) ->
erlang:nif_error(nif_library_not_loaded).
-spec get_conn_rid(connection_handle()) ->
{ok, non_neg_integer()}
| {error, badarg | internal_error}.
get_conn_rid(_Handle) ->
erlang:nif_error(nif_library_not_loaded).
-spec get_stream_rid(stream_handle()) ->
{ok, non_neg_integer()}
| {error, badarg | internal_error}.
get_stream_rid(_Handle) ->
erlang:nif_error(nif_library_not_loaded).
-spec malloc_trim() -> ok.
malloc_trim() ->
erlang:nif_error(nif_library_not_loaded).
-spec malloc_stats() -> ok.
malloc_stats() ->
erlang:nif_error(nif_library_not_loaded).
-spec controlling_process(connection_handle() | stream_handle(), pid()) ->
ok
| {error, closed | badarg | owner_dead | not_owner}.
controlling_process(_H, _P) ->
erlang:nif_error(nif_library_not_loaded).
-spec peercert(connection_handle() | stream_handle()) ->
{ok, CertDerEncoded :: binary()} | {error, any()}.
peercert(_Handle) ->
erlang:nif_error(nif_library_not_loaded).
-spec complete_cert_validation(connection_handle(), boolean(), integer()) -> ok | {error, any()}.
complete_cert_validation(_Conn, _IsAccepted, _TlsAlert) ->
erlang:nif_error(nif_library_not_loaded).
-spec get_conn_owner(connection_handle()) -> get_owner().
get_conn_owner(_) ->
erlang:nif_error(nif_library_not_loaded).
-spec get_stream_owner(connection_handle()) -> get_owner().
get_stream_owner(_) ->
erlang:nif_error(nif_library_not_loaded).
-spec get_listener_owner(listener_handle()) -> get_owner().
get_listener_owner(_) ->
erlang:nif_error(nif_library_not_loaded).
-spec get_listeners() -> get_listeners().
get_listeners() ->
erlang:nif_error(nif_library_not_loaded).
-spec get_listeners(reg_handle()) -> get_listeners().
get_listeners(_) ->
erlang:nif_error(nif_library_not_loaded).
-spec get_connections() -> [connection_handle()].
get_connections() ->
erlang:nif_error(nif_library_not_loaded).
-spec get_connections(reg_handle()) -> [connection_handle()] | {error, badarg}.
get_connections(_RegHandle) ->
erlang:nif_error(nif_library_not_loaded).
-spec count_reg_conns() -> non_neg_integer().
count_reg_conns() ->
erlang:nif_error(nif_library_not_loaded).
-spec count_reg_conns(reg_handle()) -> non_neg_integer() | {error, badarg}.
count_reg_conns(_RegHandle) ->
erlang:nif_error(nif_library_not_loaded).
-spec copy_stream_handle(stream_handle()) -> {ok, stream_handle()} | {error, badarg}.
copy_stream_handle(_H) ->
erlang:nif_error(nif_library_not_loaded).
%% @doc enable signal buffering, used in stream handoff.
%% * not exposed API.
-spec enable_sig_buffer(stream_handle()) -> ok.
enable_sig_buffer(_H) ->
erlang:nif_error(nif_library_not_loaded).
%% @doc flush buffered stream signals to the current owner
%% * not exposed API.
%% also @see quicer:controlling_process/2
%% @end
-spec flush_stream_buffered_sigs(stream_handle()) -> ok | {error, badarg | none}.
flush_stream_buffered_sigs(_H) ->
erlang:nif_error(nif_library_not_loaded).
%% @doc mock buffer a signal in sig_buffer.
%% for testing sig_buffer
%% @end
-spec mock_buffer_sig(stream_handle(), OrigOwner :: pid(), term()) ->
ok | {error, false | none | bad_pid | bad_arg}.
mock_buffer_sig(_StreamHandle, _OrigOwner, _Msg) ->
erlang:nif_error(nif_library_not_loaded).
-spec set_snab_kc_pid(pid()) -> ok | {error, badarg}.
set_snab_kc_pid(_Pid) ->
erlang:nif_error(nif_library_not_loaded).
-spec get_snab_kc_pid() -> pid().
get_snab_kc_pid() ->
erlang:nif_error(nif_library_not_loaded).
%% Internals
-spec locate_lib(file:name(), file:name()) ->
{ok, file:filename()} | {error, not_found}.
locate_lib(PrivDir, LibName) ->
case prim_file:read_file_info(PrivDir) of
{ok, #file_info{type = directory}} ->
{ok, filename:join(PrivDir, LibName)};
%% maybe escript,
{error, enotdir} ->
Escript = filename:dirname(filename:dirname(PrivDir)),
case file:read_file_info(Escript) of
{ok, #file_info{type = regular}} ->
%% try locate the file in same dir of escript
{ok, filename:join(filename:dirname(Escript), LibName)};
_ ->
{error, not_found}
end
end.
priv_dir() ->
case code:priv_dir(quicer) of
{error, bad_name} ->
"priv";
Dir ->
Dir
end.
%% @doc Get the load balancing server id from the given device name. ipv4 only.
-spec lb_server_id(ipv4, string()) -> non_neg_integer().
lb_server_id(ipv4, DevName) ->
try
{ok, IfList} = inet:getifaddrs(),
%% @NOTE Be aware of the order of the bytes in the address
lists:foldr(
fun(I, V) -> V bsl 8 bor I end,
0,
tuple_to_list(proplists:get_value(addr, proplists:get_value(DevName, IfList)))
)
catch
_:E ->
logger:error("Failed to set lb mode from ~s, fallback to disabled: ~p", [DevName, E]),
?QUIC_LOAD_BALANCING_DISABLED
end.
-spec is_kill_switch_enabled() -> boolean().
is_kill_switch_enabled() ->
os:getenv(?NONIF_SWITCH) == "1".
-spec maybe_fake_ret(Ret) -> Ret.
maybe_fake_ret(Ret) ->
case is_kill_switch_enabled() of
true -> Ret;
_ -> erlang:nif_error(nif_library_not_loaded)
end.