diff --git a/.github/workflows/cover.yaml b/.github/workflows/cover.yaml index c4744df5..4bb16565 100644 --- a/.github/workflows/cover.yaml +++ b/.github/workflows/cover.yaml @@ -37,8 +37,9 @@ jobs: uses: coverallsapp/github-action@master with: github-token: ${{ secrets.GITHUB_TOKEN }} - flag-name: run-${{ matrix.test_number }} + flag-name: run-c-lcov parallel: true + github-branch: ${{ github.ref_name }} - name: Coveralls Erl env: diff --git a/c_src/quicer_connection.c b/c_src/quicer_connection.c index c91e2024..76df97a3 100644 --- a/c_src/quicer_connection.c +++ b/c_src/quicer_connection.c @@ -201,17 +201,17 @@ _IRQL_requires_max_(DISPATCH_LEVEL) BOOLEAN is_destroy = FALSE; QUIC_STATUS status = QUIC_STATUS_SUCCESS; - assert(Connection == c_ctx->Connection); - if (!(Connection == c_ctx->Connection)) + // Connecion Handle must match unless NULL (closed) + assert(Connection == c_ctx->Connection || NULL == c_ctx->Connection); + + if (Connection == NULL) { - c_ctx->Connection = Connection; + return status; } enif_mutex_lock(c_ctx->lock); TP_CB_3(event, (uintptr_t)Connection, Event->Type); - // dbg("client connection event: %d", Event->Type); - switch (Event->Type) { case QUIC_CONNECTION_EVENT_CONNECTED: @@ -256,14 +256,8 @@ _IRQL_requires_max_(DISPATCH_LEVEL) // The connection has completed the shutdown process and is ready to be // safely cleaned up. // - // This is special case for client, - // it could happen that the connection is opened but never get started. // @see async_connect3 - // in this case, we don't need to report closed to the owner - if (!c_ctx->is_closed) // owner doesn't know it is closed - { - status = handle_connection_event_shutdown_complete(c_ctx, Event); - } + status = handle_connection_event_shutdown_complete(c_ctx, Event); is_destroy = TRUE; c_ctx->is_closed = TRUE; // client shutdown completed break; @@ -329,11 +323,12 @@ ServerConnectionCallback(HQUIC Connection, BOOLEAN is_destroy = FALSE; QUIC_STATUS status = QUIC_STATUS_SUCCESS; - assert(Connection == c_ctx->Connection); + // Connecion Handle must match unless NULL (closed) + assert(Connection == c_ctx->Connection || NULL == c_ctx->Connection); - if (!(Connection == c_ctx->Connection)) + if (Connection == NULL) { - c_ctx->Connection = Connection; + return status; } enif_mutex_lock(c_ctx->lock); @@ -516,9 +511,7 @@ async_connect3(ErlNifEnv *env, } // allocate config_resource for client connection - if (NULL - == (c_ctx->config_resource - = enif_alloc_resource(ctx_config_t, sizeof(QuicerConfigCTX)))) + if (NULL == (c_ctx->config_resource = init_config_ctx())) { res = ERROR_TUPLE_2(ATOM_ERROR_NOT_ENOUGH_MEMORY); goto Error; @@ -536,8 +529,6 @@ async_connect3(ErlNifEnv *env, goto Error; } - enif_monitor_process(NULL, c_ctx, &c_ctx->owner->Pid, &c_ctx->owner_mon); - ERL_NIF_TERM ecacertfile; X509_STORE *trusted = NULL; if (enif_get_map_value(env, eoptions, ATOM_CACERTFILE, &ecacertfile)) @@ -596,10 +587,15 @@ async_connect3(ErlNifEnv *env, c_ctx, &(c_ctx->Connection)))) { + assert(c_ctx->Connection == NULL); res = ERROR_TUPLE_2(ATOM_CONN_OPEN_ERROR); goto Error; } } + + assert(c_ctx->is_closed); + c_ctx->is_closed = FALSE; // connection opened. + ERL_NIF_TERM essl_keylogfile; if (enif_get_map_value( env, eoptions, ATOM_SSL_KEYLOGFILE_NAME, &essl_keylogfile)) @@ -698,6 +694,7 @@ async_connect3(ErlNifEnv *env, // @TODO client async_connect_3 should able to take a config_resource as // input ERL TERM so that we don't need to call ClientLoadConfiguration // + assert(!c_ctx->is_closed && c_ctx->Connection); if (QUIC_FAILED(Status = MsQuic->ConnectionStart( c_ctx->Connection, c_ctx->config_resource->Configuration, @@ -705,19 +702,30 @@ async_connect3(ErlNifEnv *env, host, port))) { + AcceptorDestroy(c_ctx->owner); + c_ctx->owner = NULL; + + /* Although MsQuic internally close the connection after failed to start, + we still do not need to set is_closed here, we expect callback to set + it while handling the shutdown complete event otherwise could cause + race cond. + */ + // c_ctx->is_closed = TRUE; + + c_ctx->Connection = NULL; + res = ERROR_TUPLE_2(ATOM_CONN_START_ERROR); - enif_release_resource(c_ctx->config_resource); + TP_NIF_3(start_fail, (uintptr_t)(c_ctx->Connection), Status); goto Error; } - c_ctx->is_closed = FALSE; // connection started + + assert(c_ctx->owner); + enif_monitor_process(NULL, c_ctx, &c_ctx->owner->Pid, &c_ctx->owner_mon); eHandle = enif_make_resource(env, c_ctx); return SUCCESS(eHandle); Error: - // Error exit, it must not be started! - assert(c_ctx->is_closed); - if (c_ctx->Connection) { // when is opened @@ -760,7 +768,10 @@ async_connect3(ErlNifEnv *env, MsQuic->ConnectionClose(c_ctx->Connection); // prevent double ConnectionClose c_ctx->Connection = NULL; + c_ctx->is_closed = TRUE; } + // Error exit, it must be closed or Handle is NULL + assert(c_ctx->is_closed || NULL == c_ctx->Connection); return res; } @@ -1151,6 +1162,7 @@ handle_connection_event_shutdown_complete( TP_CB_3(shutdown_complete, (uintptr_t)c_ctx->Connection, Event->SHUTDOWN_COMPLETE.AppCloseInProgress); + ERL_NIF_TERM props_name[] = { ATOM_IS_HANDSHAKE_COMPLETED, ATOM_IS_PEER_ACKED, ATOM_IS_APP_CLOSING }; @@ -1165,8 +1177,11 @@ handle_connection_event_shutdown_complete( props_name, props_value, 3); - enif_send(NULL, &(c_ctx->owner->Pid), NULL, report); + if (c_ctx->owner) + { + enif_send(NULL, &(c_ctx->owner->Pid), NULL, report); + } // // Now inform the stream acceptors // diff --git a/c_src/quicer_ctx.c b/c_src/quicer_ctx.c index bad33f1e..37b7b880 100644 --- a/c_src/quicer_ctx.c +++ b/c_src/quicer_ctx.c @@ -29,9 +29,7 @@ init_l_ctx() } CxPlatZeroMemory(l_ctx, sizeof(QuicerListenerCTX)); l_ctx->env = enif_alloc_env(); - l_ctx->config_resource - = enif_alloc_resource(ctx_config_t, sizeof(QuicerConfigCTX)); - CxPlatZeroMemory(l_ctx->config_resource, sizeof(QuicerConfigCTX)); + l_ctx->config_resource = init_config_ctx(); l_ctx->acceptor_queue = AcceptorQueueNew(); l_ctx->lock = enif_mutex_create("quicer:l_ctx"); l_ctx->cacertfile = NULL; @@ -46,7 +44,7 @@ deinit_l_ctx(QuicerListenerCTX *l_ctx) AcceptorQueueDestroy(l_ctx->acceptor_queue); if (l_ctx->config_resource) { - enif_release_resource(l_ctx->config_resource); + destroy_config_ctx(l_ctx->config_resource); } enif_mutex_destroy(l_ctx->lock); enif_free_env(l_ctx->env); diff --git a/c_src/quicer_listener.c b/c_src/quicer_listener.c index c1d06ab4..7ba3090f 100644 --- a/c_src/quicer_listener.c +++ b/c_src/quicer_listener.c @@ -447,6 +447,7 @@ listen2(ErlNifEnv *env, __unused_parm__ int argc, const ERL_NIF_TERM argv[]) Status = MsQuic->ListenerStart( l_ctx->Listener, alpn_buffers, alpn_buffer_length, &Address))) { + TP_NIF_3(start_fail, (uintptr_t)(l_ctx->Listener), Status); destroy_l_ctx(l_ctx); return ERROR_TUPLE_3(ATOM_LISTENER_START_ERROR, ATOM_STATUS(Status)); } @@ -473,6 +474,7 @@ close_listener1(ErlNifEnv *env, // It is safe to close it without holding the lock // This also ensures no ongoing listener callbacks + // This is a blocking call. @TODO have async version or use dirty scheduler MsQuic->ListenerClose(l); return ATOM_OK; diff --git a/c_src/quicer_nif.c b/c_src/quicer_nif.c index 501ab4cf..83a0a2e5 100644 --- a/c_src/quicer_nif.c +++ b/c_src/quicer_nif.c @@ -753,6 +753,7 @@ resource_conn_dealloc_callback(__unused_parm__ ErlNifEnv *env, void *obj) QuicerConnCTX *c_ctx = (QuicerConnCTX *)obj; TP_CB_3(start, (uintptr_t)c_ctx->Connection, c_ctx->is_closed); // must be closed otherwise will trigger callback and casue race cond. + // This ensures no callbacks during cleanup here. assert(c_ctx->is_closed == TRUE); // in dealloc if (c_ctx->Connection) { @@ -842,6 +843,7 @@ resource_config_dealloc_callback(__unused_parm__ ErlNifEnv *env, { MsQuic->ConfigurationClose(config_ctx->Configuration); } + deinit_config_ctx(config_ctx); TP_CB_3(end, (uintptr_t)obj, 0); } diff --git a/c_src/quicer_stream.c b/c_src/quicer_stream.c index da8c6acd..6eb5f495 100644 --- a/c_src/quicer_stream.c +++ b/c_src/quicer_stream.c @@ -73,10 +73,6 @@ ServerStreamCallback(HQUIC Stream, void *Context, QUIC_STREAM_EVENT *Event) TP_CB_3(event, (uintptr_t)Stream, Event->Type); switch (Event->Type) { - case QUIC_STREAM_EVENT_START_COMPLETE: - // Only for Local initiated stream - status = handle_stream_event_start_complete(s_ctx, Event); - break; case QUIC_STREAM_EVENT_SEND_COMPLETE: // @@ -110,12 +106,6 @@ ServerStreamCallback(HQUIC Stream, void *Context, QUIC_STREAM_EVENT *Event) // status = handle_stream_event_peer_receive_aborted(s_ctx, Event); break; - case QUIC_STREAM_EVENT_PEER_ACCEPTED: - // - // The peer aborted its send direction of the stream. - // - status = handle_stream_event_peer_accepted(s_ctx, Event); - break; case QUIC_STREAM_EVENT_SHUTDOWN_COMPLETE: // // Both directions of the stream have been shut down and MsQuic is done @@ -998,6 +988,7 @@ handle_stream_event_recv(HQUIC Stream, return status; } +// @doc Only for *Local* initiated stream static QUIC_STATUS handle_stream_event_start_complete(QuicerStreamCTX *s_ctx, __unused_parm__ QUIC_STREAM_EVENT *Event) diff --git a/include/quicer_types.hrl b/include/quicer_types.hrl index 6598729e..a44fc420 100644 --- a/include/quicer_types.hrl +++ b/include/quicer_types.hrl @@ -223,7 +223,7 @@ param_conn_datagram_receive_enabled | %% | X | X | @TODO param_conn_datagram_send_enabled | %% | X | | @TODO param_conn_disable_1rtt_encryption | %% | X | X | - param_conn_resumption_ticket | %% | | X | @TODO + param_conn_resumption_ticket | %% | | X | param_conn_peer_certificate_valid | %% | | X | @TODO param_conn_local_interface. %% | | X | @TODO diff --git a/rebar.config b/rebar.config index cc82ffa2..ea29359c 100644 --- a/rebar.config +++ b/rebar.config @@ -39,6 +39,7 @@ %% Coveralls {plugins , [coveralls]}. % use hex package {cover_enabled , true}. +{cover_excl_mods, [qt, qt_ssl, rev, user_default]}. {cover_export_enabled , true}. {coveralls_coverdata , "_build/test/cover/*.coverdata"}. % or a string with wildcards or a list of files {coveralls_service_name , "github"}. diff --git a/src/quicer.erl b/src/quicer.erl index 64b88474..b1250201 100644 --- a/src/quicer.erl +++ b/src/quicer.erl @@ -213,7 +213,7 @@ close_listener(Listener) -> inet:port_number(), conn_opts(), timeout()) -> {ok, connection_handle()} | {error, conn_open_error | config_error | conn_start_error} | - {error, timeout}. + {error, timeout} | {error, nst_not_found}. connect(Host, Port, Opts, Timeout) when is_list(Opts) -> connect(Host, Port, maps:from_list(Opts), Timeout); connect(Host, Port, Opts, Timeout) when is_tuple(Host) -> @@ -235,7 +235,10 @@ connect(Host, Port, Opts, Timeout) when is_map(Opts) -> {error, transport_down, Reason} end; {error, _} = Err -> - Err + Err; + {error, not_found, _} -> + %% nst error + {error, nst_not_found} end. %% @doc diff --git a/src/quicer_nif.erl b/src/quicer_nif.erl index 82c0a0c1..0c20602c 100644 --- a/src/quicer_nif.erl +++ b/src/quicer_nif.erl @@ -122,7 +122,8 @@ open_connection() -> -spec async_connect(hostname(), inet:port_number(), conn_opts()) -> {ok, connection_handle()} | - {error, conn_open_error | config_error | conn_start_error}. + {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). diff --git a/test/quicer_SUITE.erl b/test/quicer_SUITE.erl index 34b7f9a7..ec96f2aa 100644 --- a/test/quicer_SUITE.erl +++ b/test/quicer_SUITE.erl @@ -48,7 +48,13 @@ , tc_open_listener_neg_1/1 , tc_open_listener_neg_2/1 , tc_open_listener_inval_parm/1 + , tc_open_listener_inval_cacertfile_1/1 + , tc_open_listener_inval_cacertfile_2/1 + , tc_open_listener_inval_cacertfile_3/1 + , tc_start_listener_alpn_too_long/1 , tc_close_listener/1 + , tc_close_listener_twice/1 + , tc_close_listener_dealloc/1 , tc_get_listeners/1 , tc_get_listener/1 @@ -100,6 +106,7 @@ % , tc_getopt_raw/1 , tc_getopt/1 , tc_setopt_bad_opt/1 + , tc_setopt_bad_nst/1 , tc_getopt_stream_active/1 , tc_setopt/1 @@ -146,7 +153,8 @@ , tc_perf_counters/1 %% stream event masks - , tc_event_start_compl/1 + , tc_event_start_compl_client/1 + , tc_event_start_compl_server/1 %% API: csend , tc_direct_send_over_conn/1 @@ -314,12 +322,6 @@ tc_open_listener_neg_2(Config) -> %% {error, badarg} = quicer:listen("8.8.8.8:4567", default_listen_opts(Config)), ok. -tc_open_listener_inval_parm(Config) -> - ?assertEqual({error, config_error, invalid_parameter}, - quicer:listen(4567, [{stream_recv_buffer_default, 1024} % too small - | default_listen_opts(Config)])), - ok. - tc_lib_re_registration(_Config) -> case quicer:reg_open() of ok -> @@ -332,6 +334,34 @@ tc_lib_re_registration(_Config) -> ok = quicer:reg_close(), ok = quicer:reg_close(). +tc_open_listener_inval_parm(Config) -> + Port = select_port(), + ?assertEqual({error, config_error, invalid_parameter}, + quicer:listen(Port, [ {stream_recv_buffer_default, 1024} % too small + | default_listen_opts(Config)])), + ok. + +tc_open_listener_inval_cacertfile_1(Config) -> + Port = select_port(), + ?assertEqual({error, badarg}, + quicer:listen(Port, [ {cacertfile, atom} + | default_listen_opts(Config)])), + ok. + +tc_open_listener_inval_cacertfile_2(Config) -> + Port = select_port(), + ?assertMatch({ok, _}, + quicer:listen(Port, [ {cacertfile, [1,2,3,4]} + | default_listen_opts(Config)])), + ok. + +tc_open_listener_inval_cacertfile_3(Config) -> + Port = select_port(), + ?assertEqual({error, badarg}, + quicer:listen(Port, [ {cacertfile, [-1]} + | default_listen_opts(Config)])), + ok. + tc_open_listener(Config) -> Port = select_port(), {ok, L} = quicer:listen(Port, default_listen_opts(Config)), @@ -385,6 +415,33 @@ tc_open_listener_bind_v6(Config) -> tc_close_listener(_Config) -> {error,badarg} = quicer:close_listener(make_ref()). +tc_close_listener_twice(Config) -> + Port = select_port(), + {ok, L} = quicer:listen(Port, default_listen_opts(Config)), + quicer:close_listener(L), + quicer:close_listener(L). + +tc_close_listener_dealloc(Config) -> + Port = select_port(), + {Pid, Ref} = spawn_monitor(fun() -> + {ok, _L} = quicer:listen(Port, default_listen_opts(Config)) + end), + receive {'DOWN', Ref, process, Pid, normal} -> + ok + end. + +tc_start_listener_alpn_too_long(Config) -> + Port = select_port(), + {Pid, Ref} = + spawn_monitor(fun() -> + {error, config_error, invalid_parameter} + = quicer:listen(Port, default_listen_opts(Config) ++ + [{alpn, [lists:duplicate(256, $p)]}]) + end), + receive {'DOWN', Ref, process, Pid, normal} -> + ok + end. + tc_start_acceptor_without_callback(Config) -> Port = select_port(), {ok, L} = quicer:listen(Port, default_listen_opts(Config)), @@ -1682,6 +1739,12 @@ tc_setopt_bad_opt(_Config)-> [{nst, foobar} %% BAD opt | default_conn_opts()], 5000). +tc_setopt_bad_nst(_Config)-> + Port = select_port(), + {error, nst_not_found} = quicer:connect("localhost", Port, + [{nst, <<"">>} + | default_conn_opts()], 5000). + tc_setopt_conn_local_addr(Config) -> Port = select_port(), @@ -2393,7 +2456,7 @@ tc_insecure_traffic(Config) -> tc_perf_counters(_Config) -> {ok, _} = quicer:perf_counters(). -tc_event_start_compl(Config) -> +tc_event_start_compl_client(Config) -> Port = select_port(), application:ensure_all_started(quicer), ListenerOpts = [{allow_insecure, true}, {conn_acceptors, 32} @@ -2433,6 +2496,57 @@ tc_event_start_compl(Config) -> quicer:close_connection(Conn), ok. +tc_event_start_compl_server(Config) -> + Port = select_port(), + application:ensure_all_started(quicer), + ListenerOpts = [{allow_insecure, true}, {conn_acceptors, 32} + | default_listen_opts(Config)], + ConnectionOpts = [ {conn_callback, quicer_server_conn_callback} + , {stream_acceptors, 32} + | default_conn_opts()], + StreamOpts = [ {stream_callback, quicer_echo_server_stream_callback} + , {is_echo_new_stream, true} %% server reply us on a server to client stream + , {quic_event_mask, ?QUICER_STREAM_EVENT_MASK_START_COMPLETE} + | default_stream_opts() ], + Options = {ListenerOpts, ConnectionOpts, StreamOpts}, + ct:pal("Listener Options: ~p", [Options]), + {ok, _QuicApp} = quicer:start_listener(mqtt, Port, Options), + {ok, Conn} = quicer:connect("localhost", Port, + [ {param_conn_disable_1rtt_encryption, true} + | default_conn_opts()], 5000), + %% Stream 1 enabled + {ok, Stm} = quicer:start_stream(Conn, [{active, true}, + {quic_event_mask, ?QUICER_STREAM_EVENT_MASK_START_COMPLETE}]), + %% Stream 2 disabled + {ok, Stm2} = quicer:start_stream(Conn, [{active, true}, {quic_event_mask, 0}]), + {ok, 5} = quicer:async_send(Stm, <<"ping1">>), + {ok, 5} = quicer:async_send(Stm, <<"ping2">>), + {ok, Conn} = quicer:async_accept_stream(Conn, [{active, true}]), + receive + {quic, start_completed, Stm, + #{status := success, stream_id := StreamId, is_peer_accepted := true}} -> + ct:pal("Stream ~p started", [StreamId]); + {quic, start_completed, Stm, + #{status := Reason, stream_id := StreamId}} -> + ct:fail("Stream ~p failed to start: ~p", [StreamId, Reason]) + end, + receive + {quic, start_completed, Stm2, + #{status := Status}} -> + ct:fail("Stream ~p should NOT recv event : ~p", [Stm, Status]) + after 0 -> + ok + end, + receive + {quic, Data, NewStream, _} = Evt + when is_binary(Data) andalso + NewStream =/= Stm andalso + NewStream =/= Stm2 -> + ct:pal("recv ~p", [Evt]) + end, + quicer:close_connection(Conn), + ok. + tc_direct_send_over_conn(Config) -> Port = select_port(), application:ensure_all_started(quicer), diff --git a/test/quicer_echo_server_stream_callback.erl b/test/quicer_echo_server_stream_callback.erl index 89da6012..9084f135 100644 --- a/test/quicer_echo_server_stream_callback.erl +++ b/test/quicer_echo_server_stream_callback.erl @@ -34,11 +34,13 @@ -export([handle_stream_data/4]). +%% @doc handle handoff from other stream owner. init_handoff(Stream, StreamOpts, Conn, #{is_orphan := true, flags := Flags}) -> InitState = #{ stream => Stream , conn => Conn , is_local => false , is_unidir => quicer:is_unidirectional(Flags) + , echo_stream => undefined , sent_bytes => 0 }, ct:pal("init_handoff ~p", [{InitState, StreamOpts}]), @@ -48,8 +50,17 @@ post_handoff(Stream, _PostData, State) -> quicer:setopt(Stream, active, true), {ok, State}. -new_stream(_, _, _) -> - InitState = #{sent_bytes => 0}, + +%% @doc accepted new stream. +new_stream(Stream, #{flags := Flags} = StreamOpts, Conn) -> + InitState = #{ stream => Stream + , conn => Conn + , is_local => false + , echo_stream => undefined + , stream_opts => StreamOpts + , is_unidir => quicer:is_unidirectional(Flags) + , sent_bytes => 0 + }, {ok, InitState}. peer_accepted(_Stream, _Flags, S) -> @@ -75,7 +86,25 @@ send_shutdown_complete(_Stream, _Flags, S) -> start_completed(_Stream, _Flags, S) -> {ok, S}. -handle_stream_data(Stream, Bin, _Opts, #{sent_bytes := Cnt} = State) -> +handle_stream_data(Stream, Bin, _Opts, #{ sent_bytes := Cnt + , stream_opts := StreamOpts + , conn := Conn + , echo_stream := undefined + , stream := Stream + } = State) -> + case maps:get(is_echo_new_stream, StreamOpts, false) of + false -> + {ok, Size} = quicer:send(Stream, echo_msg(Bin, State)), + {ok, State#{ sent_bytes => Cnt + Size }}; + true -> + %% echo reply with a new stream from server to client. + {ok, EchoStream} = quicer:start_stream(Conn, StreamOpts), + {ok, Size} = quicer:send(EchoStream, echo_msg(Bin, State)), + {ok, State#{ sent_bytes => Cnt + Size, echo_stream => EchoStream }} + end; +handle_stream_data(Stream, Bin, _Opts, #{ sent_bytes := Cnt + , echo_stream := _Ignore + } = State) -> {ok, Size} = quicer:send(Stream, Bin), {ok, State#{ sent_bytes => Cnt + Size }}. @@ -88,3 +117,9 @@ handle_call(_Stream, _Request, _Opts, _CBState) -> stream_closed(_Stream, _Flags, S) -> {stop, normal, S}. + +%% For snabbkaffe, RID is meaningful on the same node. +echo_msg(<<"__STATE__">>, State) -> + quicer_test_lib:encode_stream_term(State); +echo_msg(Msg, _State) -> + Msg. diff --git a/test/quicer_snb_SUITE.erl b/test/quicer_snb_SUITE.erl index 0b81f206..5f6fee7a 100644 --- a/test/quicer_snb_SUITE.erl +++ b/test/quicer_snb_SUITE.erl @@ -44,6 +44,8 @@ tc_conn_resume_nst_async/1, tc_conn_resume_nst_with_data/1, tc_listener_no_acceptor/1, + tc_listener_inval_local_addr/1, + tc_conn_start_inval_port/1, tc_conn_stop_notify_acceptor/1, tc_accept_stream_active_once/1, tc_accept_stream_active_N/1, @@ -124,6 +126,12 @@ end_per_group(_GroupName, _Config) -> %% Reason = term() %% @end %%-------------------------------------------------------------------- +init_per_testcase(tc_listener_inval_local_addr, Config) -> + case os:type() of + {unix, darwin} -> {skip, "Not runnable on MacOS"}; + _ -> + Config + end; init_per_testcase(_TestCase, Config) -> Config. @@ -183,6 +191,8 @@ all() -> , tc_conn_resume_nst_with_data , tc_conn_resume_nst_async , tc_listener_no_acceptor + , tc_listener_inval_local_addr + , tc_conn_start_inval_port , tc_conn_stop_notify_acceptor , tc_accept_stream_active_once , tc_accept_stream_active_N @@ -516,9 +526,12 @@ tc_conn_close_flag_1(Config) -> begin {ok, _QuicApp} = quicer_start_listener(mqtt, Port, Options), {ok, Conn} = quicer:connect("localhost", Port, default_conn_opts(), 5000), + {ok, CRid} = quicer:get_conn_rid(Conn), + ct:pal("Client connection Rid: ~p", [CRid]), {ok, Stm} = quicer:start_stream(Conn, [{active, false}]), - {ok, 4} = quicer:async_send(Stm, <<"ping">>), - quicer:recv(Stm, 4), + {ok, 9} = quicer:async_send(Stm, <<"__STATE__">>), + #{conn := SConn} = quicer_test_lib:recv_term_from_stream(Stm), + {ok, SRid} = quicer:get_conn_rid(SConn), quicer:close_connection(Conn, ?QUIC_CONNECTION_SHUTDOWN_FLAG_NONE, 111), {ok, _} = ?block_until( #{ ?snk_kind := debug @@ -530,14 +543,15 @@ tc_conn_close_flag_1(Config) -> #{ ?snk_kind := debug , context := "callback" , function := "ClientConnectionCallback" + , resource_id := CRid , mark := ?QUIC_CONNECTION_EVENT_SHUTDOWN_COMPLETE , tag := "event"}, 1000, 3000), ct:pal("stop listener"), - ok = quicer:stop_listener(mqtt) + ok = quicer:stop_listener(mqtt), + {CRid, SRid} end, - fun(Result, Trace) -> + fun({_CRid, SRid}, Trace) -> ct:pal("Trace is ~p", [Trace]), - ?assertEqual(ok, Result), %% verify that client close_connection with default flag %% triggers a close at server side ?assert(?strict_causality(#{ ?snk_kind := debug @@ -545,14 +559,14 @@ tc_conn_close_flag_1(Config) -> , function := "ServerConnectionCallback" , mark := ?QUIC_CONNECTION_EVENT_SHUTDOWN_INITIATED_BY_PEER , tag := "event" - , resource_id := _CRid + , resource_id := SRid }, #{ ?snk_kind := debug , context := "callback" , function := "ServerConnectionCallback" , tag := "event" , mark := ?QUIC_CONNECTION_EVENT_SHUTDOWN_COMPLETE - , resource_id := _CRid + , resource_id := SRid }, Trace)) end), @@ -767,7 +781,7 @@ tc_conn_gc(Config) -> end), %% Server Process - {ok, #{resource_id := _SRid}} + {ok, #{resource_id := SRid}} = ?block_until(#{ ?snk_kind := debug , context := "callback" , function := "ServerConnectionCallback" @@ -789,11 +803,11 @@ tc_conn_gc(Config) -> , resource_id := CRid , tag := "end"}, 5000, 1000), - ok + {SRid, CRid} end, - fun(Result, Trace) -> + fun({_SRid, _CRid}, Trace) -> ct:pal("Trace is ~p", [Trace]), - ?assertEqual(ok, Result), + ct:pal("Target SRid: ~p, CRid: ~p", [_SRid, _CRid]), %% check that at client side, GC is triggered after connection close. %% check that at server side, connection was shutdown by client. ?assert(?strict_causality(#{ ?snk_kind := debug @@ -801,12 +815,12 @@ tc_conn_gc(Config) -> , function := "ClientConnectionCallback" , tag := "event" , mark := ?QUIC_CONNECTION_EVENT_SHUTDOWN_COMPLETE - , resource_id := _RidC + , resource_id := _CRid }, #{ ?snk_kind := debug , context := "callback" , function := "resource_conn_dealloc_callback" - , resource_id := _RidC + , resource_id := _CRid , tag := "end"}, Trace)), ?assert(?strict_causality(#{ ?snk_kind := debug @@ -814,12 +828,12 @@ tc_conn_gc(Config) -> , function := "ServerConnectionCallback" , tag := "event" , mark := ?QUIC_CONNECTION_EVENT_SHUTDOWN_INITIATED_BY_PEER - , resource_id := _RidS + , resource_id := _SRid }, #{ ?snk_kind := debug , context := "callback" , function := "ServerConnectionCallback" - , resource_id := _RidS + , resource_id := _SRid , mark := ?QUIC_CONNECTION_EVENT_SHUTDOWN_COMPLETE , tag := "event"}, Trace)), @@ -860,7 +874,7 @@ tc_conn_no_gc(Config) -> quicer:shutdown_connection(Conn, 0, 0) end), %% Server Process - {ok, #{resource_id := _SRid}} + {ok, #{resource_id := SRid}} = ?block_until(#{ ?snk_kind := debug , context := "callback" , function := "ServerConnectionCallback" @@ -884,10 +898,10 @@ tc_conn_no_gc(Config) -> , resource_id := CRid , tag := "end"}, 5000, 1000), - {ok, CRid, Conn} + {ok, CRid, SRid, Conn} end, - fun({ok, CRid, Conn}, Trace) -> + fun({ok, CRid, _RidS, Conn}, Trace) -> ct:pal("Trace is ~p", [Trace]), %% check that at server side, connection was shutdown by client. ?assert(?strict_causality(#{ ?snk_kind := debug @@ -952,7 +966,7 @@ tc_conn_no_gc_2(Config) -> {PRef, C, ConnRid, S} -> {C, ConnRid, S} end, %% Server Process - {ok, #{resource_id := _SRid}} + {ok, #{resource_id := SRid}} = ?block_until(#{ ?snk_kind := debug , context := "callback" , function := "ServerConnectionCallback" @@ -990,10 +1004,10 @@ tc_conn_no_gc_2(Config) -> timer:sleep(1000), %% We can get segfault here if it is use-after-free quicer:getstat(ClientConn, [send_cnt, recv_oct, send_pend]), - {ok, CRid, ClientConn} %% Ensure we hold the ref here + {ok, CRid, SRid, ClientConn} %% Ensure we hold the ref here end, - fun({ok, CRid, _}, Trace) -> + fun({ok, CRid, _SRid, _}, Trace) -> ct:pal("Trace is ~p", [Trace]), %% check that at server side, connection was shutdown by client. ?assert(?strict_causality(#{ ?snk_kind := debug @@ -1001,12 +1015,12 @@ tc_conn_no_gc_2(Config) -> , function := "ServerConnectionCallback" , tag := "event" , mark := ?QUIC_CONNECTION_EVENT_SHUTDOWN_INITIATED_BY_PEER - , resource_id := _RidS + , resource_id := _SRid }, #{ ?snk_kind := debug , context := "callback" , function := "ServerConnectionCallback" - , resource_id := _RidS + , resource_id := _SRid , mark := ?QUIC_CONNECTION_EVENT_SHUTDOWN_COMPLETE , tag := "event"}, Trace)), @@ -1273,7 +1287,8 @@ tc_conn_resume_nst_with_data(Config) -> quicer:close_connection(Conn, ?QUIC_CONNECTION_SHUTDOWN_FLAG_NONE, 111), {ok, NewConn} = quicer_nif:open_connection(), %% Send data over new stream in the resumed connection - {ok, Stm2} = quicer:async_csend(NewConn, <<"ping_from_resumed">>, [{active, false}], ?QUIC_SEND_FLAG_ALLOW_0_RTT), + {ok, Stm2} = quicer:async_csend(NewConn, <<"ping_from_resumed">>, [{active, false}], + ?QUIC_SEND_FLAG_ALLOW_0_RTT bor ?QUICER_SEND_FLAG_SYNC), %% Now we could start the connection to ensure 0-RTT data in use {ok, ConnResumed} = quicer:async_connect("localhost", Port, [{nst, NST}, {handle, NewConn}, @@ -1385,6 +1400,61 @@ tc_listener_no_acceptor(Config) -> end), ok. +%% @doc this triggers listener start fail +tc_listener_inval_local_addr(Config) -> + BadListenOn = "8.8.8.8:443", + ?check_trace(#{timetrap => 10000}, + begin + Res = quicer:listen(BadListenOn, default_listen_opts(Config)), + ?block_until(#{ ?snk_kind := debug + , context := "callback" + , function := "resource_config_dealloc_callback" + , tag := "end"}, 1000, 1000), + Res + end, + fun(Result, Trace) -> + ct:pal("Trace is ~p", [Trace]), + ?assertMatch({error, listener_start_error, + {unknown_quic_status, _}}, Result), + ?assertMatch([#{ context := "nif" + , function := "listen2" + , tag := "start_fail" + }], + lists:filter(fun(Event) -> + "nif" == maps:get(context, Event, undefined) + end, Trace)) + end). + +tc_conn_start_inval_port(_Config) -> + application:ensure_all_started(quicer), + BadPort = 65536, + ?check_trace(#{timetrap => 10000}, + begin + Res = quicer:connect("localhost", BadPort, default_conn_opts(), infinity), + receive + {quic, closed, _, _} = Msg-> + ct:fail("shall not recv msg for failed connection ~p", [Msg]) + after 100 -> + ok + end, + ?block_until(#{ ?snk_kind := debug + , context := "callback" + , function := "resource_config_dealloc_callback" + , tag := "end"}, 1000, 1000), + Res + end, + fun(Result, Trace) -> + ct:pal("Trace is ~p", [Trace]), + ?assertMatch({error, conn_start_error}, Result), + ?assertMatch([#{ context := "nif" + , function := "async_connect3" + , tag := "start_fail" + }], + lists:filter(fun(Event) -> + "nif" == maps:get(context, Event, undefined) + end, Trace)) + end). + tc_conn_stop_notify_acceptor(Config) -> Port = select_port(), application:ensure_all_started(quicer), @@ -2031,6 +2101,7 @@ default_listen_opts(Config) -> , {alpn, ["sample"]} , {verify, none} , {idle_timeout_ms, 10000} + , {handshake_idle_timeout_ms, 10000} %% some CI runner is slow on this , {server_resumption_level, 2} % QUIC_SERVER_RESUME_AND_ZERORTT , {peer_bidi_stream_count, 10} ]. diff --git a/test/quicer_test_lib.erl b/test/quicer_test_lib.erl index cd5e377f..7b30fa19 100644 --- a/test/quicer_test_lib.erl +++ b/test/quicer_test_lib.erl @@ -21,9 +21,28 @@ -export([gen_ca/2, gen_host_cert/3, gen_host_cert/4, - receive_all/0 + receive_all/0, + recv_term_from_stream/1, + encode_stream_term/1 ]). +-define(MAGIC_HEADER, 4294967293). + +%% @doc recv erlang term from stream +-spec recv_term_from_stream(quicer:stream_handle()) -> term(). +recv_term_from_stream(Stream) -> + {ok, << ?MAGIC_HEADER:32/unsigned, Len:64/unsigned>>} = quicer:recv(Stream, 12), + {ok, Payload} = quicer:recv(Stream, Len), + binary_to_term(Payload). + +%% @doc wrap one erlang term for transfer on the quic stream +-spec encode_stream_term(term()) -> binary(). +encode_stream_term(Payload) when not is_binary(Payload) -> + encode_stream_term(term_to_binary(Payload)); +encode_stream_term(Payload) when is_binary(Payload)-> + Len = byte_size(Payload), + << ?MAGIC_HEADER:32/unsigned, Len:64/unsigned, Payload/binary>>. + gen_ca(Path, Name) -> %% Generate ca.pem and ca.key which will be used to generate certs %% for hosts server and clients