From 8c56a0260faf08838c147f3764eb887ea9cb4f0d Mon Sep 17 00:00:00 2001 From: William Yang Date: Wed, 27 Dec 2023 13:57:45 +0100 Subject: [PATCH] feat: support restart listener process with new opts --- c_src/quicer_config.c | 107 +++++++++++++++++++++----------- c_src/quicer_config.h | 5 +- c_src/quicer_listener.c | 47 +++++++------- src/quicer.erl | 2 +- src/quicer_listener.erl | 56 +++++++++++++++-- test/example_client_stream.erl | 12 ++-- test/quicer_listener_SUITE.erl | 109 +++++++++++++++++++++++++++++++-- test/quicer_test_lib.erl | 1 + 8 files changed, 263 insertions(+), 76 deletions(-) diff --git a/c_src/quicer_config.c b/c_src/quicer_config.c index 50954918..837ce0ba 100644 --- a/c_src/quicer_config.c +++ b/c_src/quicer_config.c @@ -229,9 +229,9 @@ ServerLoadConfiguration(ErlNifEnv *env, } unsigned alpn_buffer_length = 0; - QUIC_BUFFER alpn_buffers[MAX_ALPN] = { 0 }; + QUIC_BUFFER *alpn_buffers = NULL; - if (!load_alpn(env, option, &alpn_buffer_length, alpn_buffers)) + if (!load_alpn(env, option, &alpn_buffer_length, &alpn_buffers)) { return ATOM_ALPN; } @@ -240,14 +240,15 @@ ServerLoadConfiguration(ErlNifEnv *env, // Allocate/initialize the configuration object, with the configured ALPN // and settings. // - QUIC_STATUS Status = QUIC_STATUS_SUCCESS; - if (QUIC_FAILED(Status = MsQuic->ConfigurationOpen(Registration, - alpn_buffers, - alpn_buffer_length, - &Settings, - sizeof(Settings), - CredConfig, // Context - Configuration))) + QUIC_STATUS Status = MsQuic->ConfigurationOpen(Registration, + alpn_buffers, + alpn_buffer_length, + &Settings, + sizeof(Settings), + CredConfig, // Context + Configuration); + free_alpn_buffers(alpn_buffers, alpn_buffer_length); + if (QUIC_FAILED(Status)) { return ATOM_STATUS(Status); } @@ -313,9 +314,9 @@ ClientLoadConfiguration(ErlNifEnv *env, } unsigned alpn_buffer_length = 0; - QUIC_BUFFER alpn_buffers[MAX_ALPN]; + QUIC_BUFFER *alpn_buffers = NULL; - if (!load_alpn(env, options, &alpn_buffer_length, alpn_buffers)) + if (!load_alpn(env, options, &alpn_buffer_length, &alpn_buffers)) { ret = ATOM_ALPN; goto done; @@ -325,14 +326,15 @@ ClientLoadConfiguration(ErlNifEnv *env, // Allocate/initialize the configuration object, with the configured ALPN // and settings. // - QUIC_STATUS Status = QUIC_STATUS_SUCCESS; - if (QUIC_FAILED(Status = MsQuic->ConfigurationOpen(Registration, - alpn_buffers, - alpn_buffer_length, - &Settings, - sizeof(Settings), - NULL, - Configuration))) + QUIC_STATUS Status = MsQuic->ConfigurationOpen(Registration, + alpn_buffers, + alpn_buffer_length, + &Settings, + sizeof(Settings), + NULL, + Configuration); + free_alpn_buffers(alpn_buffers, alpn_buffer_length); + if (QUIC_FAILED(Status)) { ret = ATOM_STATUS(Status); goto done; @@ -357,50 +359,83 @@ ClientLoadConfiguration(ErlNifEnv *env, return ret; } +/* +** load alpn from eterm options to the alpn_buffers +** @NOTE 1:caller must call free_alpn_buffers after use +*/ bool load_alpn(ErlNifEnv *env, const ERL_NIF_TERM *options, unsigned *alpn_buffer_length, - QUIC_BUFFER alpn_buffers[]) + QUIC_BUFFER **alpn_buffers) { ERL_NIF_TERM alpn_list; + assert(*alpn_buffers == NULL); if (!enif_get_map_value(env, *options, ATOM_ALPN, &alpn_list)) { return false; } - if (!enif_get_list_length(env, alpn_list, alpn_buffer_length)) + if (!enif_get_list_length(env, alpn_list, alpn_buffer_length) + || alpn_buffer_length == 0) { return false; } - ERL_NIF_TERM head, tail; + *alpn_buffers = malloc((*alpn_buffer_length) * sizeof(QUIC_BUFFER)); - if (!enif_get_list_cell(env, alpn_list, &head, &tail)) + if (!*alpn_buffers) { return false; } - for (int i = 0; i < (int)(*alpn_buffer_length); i++) + CxPlatZeroMemory(*alpn_buffers, (*alpn_buffer_length) * sizeof(QUIC_BUFFER)); + + ERL_NIF_TERM list, head, tail; + unsigned i = 0; + list = alpn_list; + while (enif_get_list_cell(env, list, &head, &tail)) { - // @todo check if PATH_MAX is the correct length - char str[PATH_MAX]; - if (enif_get_string(env, head, str, PATH_MAX, ERL_NIF_LATIN1) <= 0) + unsigned len = 0; +#if ERL_NIF_MINOR_VERSION > 16 + if (!enif_get_string_length(env, head, &len, ERL_NIF_LATIN1)) +#else + if (!enif_get_list_length(env, head, &len)) +#endif { - return false; + goto exit; } + len++; // for '\0' + char *str = malloc(len * sizeof(char)); - alpn_buffers[i].Buffer = (uint8_t *)str; - alpn_buffers[i].Length = strlen(str); - - if (!enif_get_list_cell(env, tail, &head, &tail) - && i + 1 < (int)(*alpn_buffer_length)) + if (enif_get_string(env, head, str, len, ERL_NIF_LATIN1) <= 0) { - return false; + free(str); + str = NULL; + goto exit; } - } + (*alpn_buffers)[i].Buffer = (uint8_t *)str; + (*alpn_buffers)[i].Length = len - 1; // msquic doesn't need '\0' + i++; + list = tail; + } return true; + +exit: + free_alpn_buffers(*alpn_buffers, i); + return false; +} + +void +free_alpn_buffers(QUIC_BUFFER *alpn_buffers, unsigned len) +{ + for (unsigned i = 0; i < len; i++) + { + free(alpn_buffers[i].Buffer); + } + free(alpn_buffers); + alpn_buffers = NULL; } bool diff --git a/c_src/quicer_config.h b/c_src/quicer_config.h index 6ef12a77..8972ce12 100644 --- a/c_src/quicer_config.h +++ b/c_src/quicer_config.h @@ -77,7 +77,10 @@ ERL_NIF_TERM ClientLoadConfiguration(ErlNifEnv *env, bool load_alpn(ErlNifEnv *env, const ERL_NIF_TERM *option, unsigned *alpn_buffer_length, - QUIC_BUFFER alpn_buffers[]); + QUIC_BUFFER **alpn_buffers); + +void free_alpn_buffers(QUIC_BUFFER *alpn_buffers, unsigned alpn_buffer_length); + bool load_verify(ErlNifEnv *env, const ERL_NIF_TERM *option, const bool default_verify); diff --git a/c_src/quicer_listener.c b/c_src/quicer_listener.c index 1de6a3cc..fa1c858d 100644 --- a/c_src/quicer_listener.c +++ b/c_src/quicer_listener.c @@ -410,7 +410,7 @@ listen2(ErlNifEnv *env, __unused_parm__ int argc, const ERL_NIF_TERM argv[]) // Now try to start listener unsigned alpn_buffer_length = 0; - QUIC_BUFFER alpn_buffers[MAX_ALPN]; + QUIC_BUFFER *alpn_buffers = NULL; // Allow insecure, default is false ERL_NIF_TERM eisInsecure; @@ -420,16 +420,18 @@ listen2(ErlNifEnv *env, __unused_parm__ int argc, const ERL_NIF_TERM argv[]) l_ctx->allow_insecure = TRUE; } - if (!load_alpn(env, &options, &alpn_buffer_length, alpn_buffers)) + if (!load_alpn(env, &options, &alpn_buffer_length, &alpn_buffers)) { ret = ERROR_TUPLE_2(ATOM_ALPN); goto exit; } // Start Listener - if (QUIC_FAILED( - Status = MsQuic->ListenerStart( - l_ctx->Listener, alpn_buffers, alpn_buffer_length, &Address))) + Status = MsQuic->ListenerStart( + l_ctx->Listener, alpn_buffers, alpn_buffer_length, &Address); + free_alpn_buffers(alpn_buffers, alpn_buffer_length); + + if (QUIC_FAILED(Status)) { TP_NIF_3(start_fail, (uintptr_t)(l_ctx->Listener), Status); HQUIC Listener = l_ctx->Listener; @@ -525,7 +527,7 @@ start_listener3(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) QuicerListenerCTX *l_ctx; unsigned alpn_buffer_length = 0; - QUIC_BUFFER alpn_buffers[MAX_ALPN]; + QUIC_BUFFER *alpn_buffers = NULL; QUIC_ADDR Address = {}; int UdpPort = 0; @@ -562,11 +564,6 @@ start_listener3(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) return ERROR_TUPLE_2(ATOM_BADARG); } - if (!load_alpn(env, &options, &alpn_buffer_length, alpn_buffers)) - { - return ERROR_TUPLE_2(ATOM_ALPN); - } - QuicerConfigCTX *new_config_ctx = init_config_ctx(); if (!new_config_ctx) { @@ -620,23 +617,23 @@ start_listener3(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) l_ctx->config_resource = new_config_ctx; #if defined(QUICER_USE_TRUSTED_STORE) - /*@ - FIXME: ongoing handshake will get segfault if we free it - - free(l_ctx->trusted_store) - - So, currently we allow leakage, possible solutions are: - a. need some refcnt for trusted_store - b. move trusted_store to config_resource - c. remove trusted_store (most likely) - */ + X509_STORE_free(l_ctx->trusted_store); l_ctx->trusted_store = trusted_store; #endif // QUICER_USE_TRUSTED_STORE - // No we swap the config + // Now we swap the config + + if (!load_alpn(env, &options, &alpn_buffer_length, &alpn_buffers)) + { + enif_release_resource(new_config_ctx); + ret = ERROR_TUPLE_2(ATOM_ALPN); + goto exit; + } + Status = MsQuic->ListenerStart( + l_ctx->Listener, alpn_buffers, alpn_buffer_length, &Address); + + free_alpn_buffers(alpn_buffers, alpn_buffer_length); - if (QUIC_FAILED( - Status = MsQuic->ListenerStart( - l_ctx->Listener, alpn_buffers, alpn_buffer_length, &Address))) + if (QUIC_FAILED(Status)) { TP_NIF_3(start_fail, (uintptr_t)(l_ctx->Listener), Status); ret = ERROR_TUPLE_3(ATOM_LISTENER_START_ERROR, ATOM_STATUS(Status)); diff --git a/src/quicer.erl b/src/quicer.erl index 247eea2d..bf268460 100644 --- a/src/quicer.erl +++ b/src/quicer.erl @@ -274,7 +274,7 @@ start_listener(Listener, Port, Options) -> quicer_nif:start_listener(Listener, Port, Options). %% @doc Stop a started listener which could be closed or restarted later. --spec stop_listener(listener_handle()) -> ok. +-spec stop_listener(listener_handle()) -> ok | {error, any()}. stop_listener(Handle) -> case quicer_nif:stop_listener(Handle) of ok -> diff --git a/src/quicer_listener.erl b/src/quicer_listener.erl index 048c3d98..d68d3412 100644 --- a/src/quicer_listener.erl +++ b/src/quicer_listener.erl @@ -21,7 +21,11 @@ -export([ start_link/3, start_listener/3, - stop_listener/1 + stop_listener/1, + lock/2, + unlock/2, + reload/2, + get_handle/2 ]). %% gen_server callbacks @@ -35,9 +39,11 @@ -record(state, { name :: atom(), + listen_on :: quicer:listen_on(), listener :: quicer:listener_handle(), conn_sup :: pid(), - alpn :: [string()] + alpn :: [string()], + opts :: quicer:listener_opts() }). -export_type([listener_name/0]). @@ -72,6 +78,26 @@ start_listener(Name, ListenOn, Options) -> stop_listener(Name) -> quicer_listener_sup:stop_listener(Name). +-spec lock(pid(), timeout()) -> ok | {error, _}. +lock(Pid, Timeout) -> + gen_server:call(Pid, lock, Timeout). + +-spec unlock(pid(), timeout()) -> ok | {error, _}. +unlock(Pid, Timeout) -> + gen_server:call(Pid, unlock, Timeout). + +%% @doc Reload the listener with new *listener* opts. +%% @NOTE: the acceptor opts and stream opts are not reloaded. +%%% if you want to reload them, you should restart the listener (terminate and spawn). +%% @end +-spec reload(pid(), NewConf :: map()) -> ok | {error, _}. +reload(Pid, NewConf) -> + gen_server:call(Pid, {reload, NewConf}, infinity). + +-spec get_handle(pid(), timeout()) -> quicer:listener_handle(). +get_handle(Pid, Timeout) -> + gen_server:call(Pid, get_handle, Timeout). + %%%=================================================================== %%% gen_server callbacks %%%=================================================================== @@ -98,8 +124,10 @@ init([Name, ListenOn, {#{conn_acceptors := N, alpn := Alpn} = LOpts, _COpts, _SO _ = [{ok, _} = supervisor:start_child(ConnSup, [ConnSup]) || _ <- lists:seq(1, N)], {ok, #state{ name = Name, + listen_on = ListenOn, listener = L, conn_sup = ConnSup, + opts = LOpts, alpn = Alpn }}. @@ -118,8 +146,28 @@ init([Name, ListenOn, {#{conn_acceptors := N, alpn := Alpn} = LOpts, _COpts, _SO | {noreply, NewState :: term(), hibernate} | {stop, Reason :: term(), Reply :: term(), NewState :: term()} | {stop, Reason :: term(), NewState :: term()}. -handle_call(_Request, _From, State) -> - Reply = ok, +handle_call(get_handle, _From, State) -> + {reply, {ok, State#state.listener}, State}; +handle_call(lock, _From, State) -> + Res = quicer:stop_listener(State#state.listener), + {reply, Res, State}; +handle_call(unlock, _From, State) -> + Res = quicer:start_listener( + State#state.listener, + State#state.listen_on, + State#state.opts + ), + {reply, Res, State}; +handle_call({reload, NewConf}, _From, State) -> + _ = quicer:stop_listener(State#state.listener), + Res = quicer:start_listener( + State#state.listener, + State#state.listen_on, + NewConf + ), + {reply, Res, State}; +handle_call(Request, _From, State) -> + Reply = {error, {unimpl, Request}}, {reply, Reply, State}. %%-------------------------------------------------------------------- diff --git a/test/example_client_stream.erl b/test/example_client_stream.erl index 54b615fc..32a4a407 100644 --- a/test/example_client_stream.erl +++ b/test/example_client_stream.erl @@ -30,7 +30,8 @@ stream_closed/3, peer_accepted/3, passive/3, - handle_call/3 + handle_call/3, + handle_info/2 ]). -export([handle_stream_data/4]). @@ -115,9 +116,6 @@ passive(Stream, undefined, S) -> ct:fail("Steam ~p go into passive mode", [Stream]), {ok, S}. -handle_call(_Request, _From, S) -> - {reply, {error, not_impl}, S}. - stream_closed( _Stream, #{ @@ -138,3 +136,9 @@ stream_closed( is_integer(Code) -> {stop, normal, S}. + +handle_call(_Request, _From, S) -> + {reply, {error, not_impl}, S}. + +handle_info(_, S) -> + {ok, S}. diff --git a/test/quicer_listener_SUITE.erl b/test/quicer_listener_SUITE.erl index 120d1b39..a49624d1 100644 --- a/test/quicer_listener_SUITE.erl +++ b/test/quicer_listener_SUITE.erl @@ -20,6 +20,8 @@ -include_lib("stdlib/include/assert.hrl"). -include_lib("snabbkaffe/include/snabbkaffe.hrl"). +-include_lib("quicer/include/quicer.hrl"). + -compile(export_all). -compile(nowarn_export_all). @@ -382,6 +384,98 @@ tc_stop_start_listener_with_new_port(Config) -> gen_udp:close(Sock1), ok = quicer:close_listener(L). +tc_listener_conf_reload(Config) -> + process_flag(trap_exit, true), + DataDir = ?config(data_dir, Config), + ServerConnCallback = example_server_connection, + ServerStreamCallback = example_server_stream, + Port = select_port(), + application:ensure_all_started(quicer), + ListenerOpts = [ + {conn_acceptors, 32}, + {peer_bidi_stream_count, 0}, + {peer_unidi_stream_count, 1} + | default_listen_opts(Config) + ], + ConnectionOpts = [ + {conn_callback, ServerConnCallback}, + {stream_acceptors, 2} + | default_conn_opts() + ], + StreamOpts = [ + {stream_callback, ServerStreamCallback} + | default_stream_opts() + ], + Options = {ListenerOpts, ConnectionOpts, StreamOpts}, + + %% Given a QUIC connection between example client and example server + {ok, QuicApp} = quicer:spawn_listener(sample, Port, Options), + ClientConnOpts = default_conn_opts_verify(Config, ca), + {ok, ClientConnPid} = example_client_connection:start_link( + "localhost", + Port, + {ClientConnOpts, default_stream_opts()} + ), + + ct:pal("C1 status : ~p", [sys:get_status(ClientConnPid)]), + {ok, LHandle} = quicer_listener:get_handle(QuicApp, 5000), + + %% WHEN: the listener is reloaded with new listener opts (New cert, key and cacert). + ok = quicer_listener:lock(QuicApp, infinity), + ok = quicer_listener:unlock(QuicApp, infinity), + NewListenerOpts = + ListenerOpts ++ + [ + {certfile, filename:join(DataDir, "other-server.pem")}, + {keyfile, filename:join(DataDir, "other-server.key")}, + {cacertfile, filename:join(DataDir, "other-ca.pem")} + ], + ok = quicer_listener:reload(QuicApp, NewListenerOpts), + %% THEN: the listener handle is unchanged + ?assertEqual({ok, LHandle}, quicer_listener:get_handle(QuicApp, 5000)), + + %% THEN: start new connection with old cacert must fail + ?assertMatch( + {error, transport_down, #{error := _, status := Status}} when + Status =:= bad_certificate; + Status =:= cert_untrusted_root; + Status =:= handshake_failure, + quicer:connect( + "localhost", + Port, + default_conn_opts_verify(Config, 'ca'), + 5000 + ) + ), + %% WHEN: start new connection with new cacert + {ok, Conn2} = quicer:connect( + "localhost", + Port, + default_conn_opts_verify(Config, 'other-ca'), + 5000 + ), + + %% THEN: the new connection shall be established and traffic can be sent and received + {ok, Stream2} = quicer:start_stream( + Conn2, + #{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL} + ), + {ok, _} = quicer:send(Stream2, <<"ping_from_conn_2">>), + + receive + {quic, new_stream, Stream2Remote, _} -> + quicer:setopt(Stream2Remote, active, true), + ok + end, + + receive + {quic, <<"ping_from_conn_2">>, Stream2Remote, _} -> ok + after 2000 -> + ct:fail("nothing from conn 2"), + quicer_test_lib:report_unhandled_messages() + end, + gen_server:stop(ClientConnPid). + tc_stop_close_listener(Config) -> Port = select_port(), {ok, L} = quicer:listen(Port, default_listen_opts(Config)), @@ -629,8 +723,13 @@ default_listener_opts(Config, Verify) -> | tl(default_listen_opts(Config)) ]. -%%%_* Emacs ==================================================================== -%%% Local Variables: -%%% allout-layout: t -%%% erlang-indent-level: 2 -%%% End: +default_conn_opts_verify(Config, Ca) -> + DataDir = ?config(data_dir, Config), + CACertFile = filename:join(DataDir, Ca) ++ ".pem", + [ + {verify, verify_peer}, + {cacertfile, CACertFile}, + {alpn, ["sample"]}, + %% {sslkeylogfile, "/tmp/SSLKEYLOGFILE"}, + {idle_timeout_ms, 5000} + ]. diff --git a/test/quicer_test_lib.erl b/test/quicer_test_lib.erl index 871ca88c..141db8db 100644 --- a/test/quicer_test_lib.erl +++ b/test/quicer_test_lib.erl @@ -289,6 +289,7 @@ generate_tls_certs(Config) -> gen_host_cert("client", "ca", DataDir), gen_ca(DataDir, "other-ca"), gen_host_cert("other-client", "other-ca", DataDir), + gen_host_cert("other-server", "other-ca", DataDir), gen_host_cert("server-password", "ca", DataDir, #{password => ?SERVER_KEY_PASSWORD}), %% New certs for TLS chain tests