diff --git a/apps/emqx/test/emqx_common_test_helpers.erl b/apps/emqx/test/emqx_common_test_helpers.erl index 6f2b05161..96fd83438 100644 --- a/apps/emqx/test/emqx_common_test_helpers.erl +++ b/apps/emqx/test/emqx_common_test_helpers.erl @@ -65,6 +65,11 @@ select_free_port/1 ]). +-export([ + ssl_verify_fun_allow_any_host/0, + ssl_verify_fun_allow_any_host_impl/3 +]). + -export([ emqx_cluster/1, emqx_cluster/2, @@ -1421,3 +1426,24 @@ group_path(Config) -> _:_ -> [] end. + +%% almost verify_none equivalent, but only ignores 'hostname_check_failed' +ssl_verify_fun_allow_any_host_impl(_Cert, Event, State) -> + case Event of + valid -> + {valid, State}; + valid_peer -> + {valid, State}; + {bad_cert, hostname_check_failed} -> + {valid, State}; + {bad_cert, _} -> + {fail, Event}; + {extension, _} -> + {unknown, State} + end. + +ssl_verify_fun_allow_any_host() -> + [ + {verify, verify_peer}, + {verify_fun, {fun ?MODULE:ssl_verify_fun_allow_any_host_impl/3, _State = #{}}} + ]. diff --git a/apps/emqx/test/emqx_cth_suite.erl b/apps/emqx/test/emqx_cth_suite.erl index 5e91b92c9..042ef91db 100644 --- a/apps/emqx/test/emqx_cth_suite.erl +++ b/apps/emqx/test/emqx_cth_suite.erl @@ -58,7 +58,6 @@ -module(emqx_cth_suite). -include_lib("common_test/include/ct.hrl"). --include_lib("emqx/include/emqx_access_control.hrl"). -export([start/2]). -export([stop/1]). diff --git a/apps/emqx/test/emqx_cth_tls.erl b/apps/emqx/test/emqx_cth_tls.erl new file mode 100644 index 000000000..ccec7626d --- /dev/null +++ b/apps/emqx/test/emqx_cth_tls.erl @@ -0,0 +1,339 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2023 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(emqx_cth_tls). + +-include_lib("public_key/include/public_key.hrl"). + +-export([gen_cert/1]). +-export([write_cert/2]). +-export([write_cert/3]). +-export([write_pem/2]). + +%% ------------------------------------------------------------------- +%% Certificate Issuing +%% Heavily inspired by: ${ERL_SRC}/lib/public_key/test/erl_make_certs.erl +%% ------------------------------------------------------------------- + +-type pem_entry() :: public_key:pem_entry(). +-type certificate() :: pem_entry(). +-type private_key() :: pem_entry(). + +-type cert_subject() :: #{ + name => string(), + email => string(), + city => string(), + state => string(), + org => string(), + org_unit => string(), + country => string(), + serial => string(), + title => string(), + dnQualifer => string() +}. + +-type cert_validity() :: + {_From :: calendar:date(), _To :: calendar:date()}. + +-type cert_extensions() :: #{ + basic_constraints => false | ca | _PathLenContraint :: pos_integer(), + key_usage => false | certsign +}. + +%% @doc Generate a certificate and a private key. +%% If you need root (CA) certificate, use `root` as `issuer` option. By default, the +%% generated certificate will have according extensions (constraints, key usage, etc). +%% Once root certificate + private key pair is generated, you can use the result +%% as `issuer` option to generate other certificates signed by this root. +-spec gen_cert(Opts) -> {certificate(), private_key()} when + Opts :: #{ + key := ec | rsa | PrivKeyIn, + issuer := root | {CertificateIn, PrivKeyIn}, + subject => cert_subject(), + validity => cert_validity(), + extensions => cert_extensions() | false + }, + CertificateIn :: certificate() | public_key:der_encoded() | #'OTPCertificate'{}, + PrivKeyIn :: private_key() | _PEM :: binary(). +gen_cert(Opts) -> + SubjectPrivateKey = get_privkey(Opts), + {TBSCert, IssuerKey} = make_tbs(SubjectPrivateKey, Opts), + Cert = public_key:pkix_sign(TBSCert, IssuerKey), + true = verify_signature(Cert, IssuerKey), + {encode_cert(Cert), encode_privkey(SubjectPrivateKey)}. + +get_privkey(#{key := Algo}) when is_atom(Algo) -> + gen_privkey(Algo); +get_privkey(#{key := Key}) -> + decode_privkey(Key). + +make_tbs(SubjectKey, Opts) -> + {Issuer, IssuerKey} = issuer(Opts, SubjectKey), + Subject = + case Opts of + #{issuer := root} -> + Issuer; + #{} -> + subject(Opts) + end, + { + #'OTPTBSCertificate'{ + version = v3, + serialNumber = rand:uniform(1000000000000), + signature = sign_algorithm(IssuerKey, Opts), + issuer = Issuer, + validity = validity(Opts), + subject = Subject, + subjectPublicKeyInfo = publickey(SubjectKey), + extensions = extensions(Opts) + }, + IssuerKey + }. + +issuer(Opts = #{issuer := root}, SubjectKey) -> + %% Self signed + {subject(Opts), SubjectKey}; +issuer(#{issuer := {Issuer, IssuerKey}}, _SubjectKey) -> + {issuer_subject(Issuer), decode_privkey(IssuerKey)}. + +issuer_subject({'Certificate', IssuerDer, _}) when is_binary(IssuerDer) -> + issuer_subject(IssuerDer); +issuer_subject(IssuerDer) when is_binary(IssuerDer) -> + issuer_subject(public_key:pkix_decode_cert(IssuerDer, otp)); +issuer_subject(#'OTPCertificate'{tbsCertificate = #'OTPTBSCertificate'{subject = Subject}}) -> + Subject. + +subject(Opts = #{}) -> + Subject = maps:get(subject, Opts, #{}), + Entries = maps:map( + fun(N, V) -> [subject_entry(N, V)] end, + maps:merge(default_subject(Opts), Subject) + ), + {rdnSequence, maps:values(Entries)}. + +subject_entry(name, Name) -> + typed_attr(?'id-at-commonName', {printableString, Name}); +subject_entry(email, Email) -> + typed_attr(?'id-emailAddress', Email); +subject_entry(city, City) -> + typed_attr(?'id-at-localityName', {printableString, City}); +subject_entry(state, State) -> + typed_attr(?'id-at-stateOrProvinceName', {printableString, State}); +subject_entry(org, Org) -> + typed_attr(?'id-at-organizationName', {printableString, Org}); +subject_entry(org_unit, OrgUnit) -> + typed_attr(?'id-at-organizationalUnitName', {printableString, OrgUnit}); +subject_entry(country, Country) -> + typed_attr(?'id-at-countryName', Country); +subject_entry(serial, Serial) -> + typed_attr(?'id-at-serialNumber', Serial); +subject_entry(title, Title) -> + typed_attr(?'id-at-title', {printableString, Title}); +subject_entry(dnQualifer, DnQ) -> + typed_attr(?'id-at-dnQualifier', DnQ). + +subject_info(Info, Subject, Default) -> + case subject_info(Info, Subject) of + undefined -> Default; + Value -> Value + end. + +subject_info(Info, {rdnSequence, Entries}) -> + subject_info(Info, Entries); +subject_info(name, Entries) when is_list(Entries) -> + get_string(find_subject_entry(?'id-at-commonName', Entries)); +subject_info(org, Entries) when is_list(Entries) -> + get_string(find_subject_entry(?'id-at-organizationName', Entries)); +subject_info(org_unit, Entries) when is_list(Entries) -> + get_string(find_subject_entry(?'id-at-organizationalUnitName', Entries)); +subject_info(country, Entries) when is_list(Entries) -> + find_subject_entry(?'id-at-countryName', Entries). + +find_subject_entry(Oid, Entries) -> + emqx_maybe:from_list([ + Value + || Attrs <- Entries, + #'AttributeTypeAndValue'{type = T, value = Value} <- Attrs, + T =:= Oid + ]). + +get_string({printableString, String}) -> + String; +get_string(undefined) -> + undefined. + +typed_attr(Type, Value) -> + #'AttributeTypeAndValue'{type = Type, value = Value}. + +sign_algorithm(#'ECPrivateKey'{parameters = Parms}, _Opts) -> + #'SignatureAlgorithm'{ + algorithm = ?'ecdsa-with-SHA256', + parameters = Parms + }. + +validity(Opts) -> + {From, To} = maps:get(validity, Opts, default_validity()), + #'Validity'{ + notBefore = {generalTime, format_date(From)}, + notAfter = {generalTime, format_date(To)} + }. + +publickey(#'ECPrivateKey'{parameters = Params, publicKey = PubKey}) -> + #'OTPSubjectPublicKeyInfo'{ + algorithm = #'PublicKeyAlgorithm'{ + algorithm = ?'id-ecPublicKey', + parameters = Params + }, + subjectPublicKey = #'ECPoint'{point = PubKey} + }. + +extensions(#{extensions := false}) -> + asn1_NOVALUE; +extensions(Opts) -> + Exts = maps:get(extensions, Opts, #{}), + Default = default_extensions(Opts), + maps:fold( + fun(Name, Data, Acc) -> Acc ++ extension(Name, Data) end, + [], + maps:merge(Default, Exts) + ). + +extension(basic_constraints, false) -> + []; +extension(basic_constraints, ca) -> + [ + #'Extension'{ + extnID = ?'id-ce-basicConstraints', + extnValue = #'BasicConstraints'{cA = true}, + critical = true + } + ]; +extension(basic_constraints, Len) when is_integer(Len) -> + [ + #'Extension'{ + extnID = ?'id-ce-basicConstraints', + extnValue = #'BasicConstraints'{cA = true, pathLenConstraint = Len}, + critical = true + } + ]; +extension(key_usage, false) -> + []; +extension(key_usage, certsign) -> + [ + #'Extension'{ + extnID = ?'id-ce-keyUsage', + extnValue = [keyCertSign], + critical = true + } + ]. + +default_validity() -> + {shift_date(date(), -1), shift_date(date(), +7)}. + +default_subject(#{issuer := root}) -> + #{ + name => "RootCA", + org => "EMQ", + org_unit => "EMQX", + country => "CN" + }; +default_subject(#{}) -> + #{ + name => "Server", + org => "EMQ", + org_unit => "EMQX", + country => "CN" + }. + +default_extensions(#{issuer := root}) -> + #{ + basic_constraints => ca, + key_usage => certsign + }; +default_extensions(#{}) -> + #{}. + +%% ------------------------------------------------------------------- + +verify_signature(CertDer, #'ECPrivateKey'{parameters = Params, publicKey = PubKey}) -> + public_key:pkix_verify(CertDer, {#'ECPoint'{point = PubKey}, Params}); +verify_signature(CertDer, KeyPem) -> + verify_signature(CertDer, decode_privkey(KeyPem)). + +%% ------------------------------------------------------------------- + +gen_privkey(ec) -> + public_key:generate_key({namedCurve, secp256k1}); +gen_privkey(rsa) -> + public_key:generate_key({rsa, 2048, 17}). + +decode_privkey(#'ECPrivateKey'{} = Key) -> + Key; +decode_privkey(#'RSAPrivateKey'{} = Key) -> + Key; +decode_privkey(PemEntry = {_, _, _}) -> + public_key:pem_entry_decode(PemEntry); +decode_privkey(PemBinary) when is_binary(PemBinary) -> + [KeyInfo] = public_key:pem_decode(PemBinary), + decode_privkey(KeyInfo). + +-spec encode_privkey(#'ECPrivateKey'{} | #'RSAPrivateKey'{}) -> private_key(). +encode_privkey(Key = #'ECPrivateKey'{}) -> + {ok, Der} = 'OTP-PUB-KEY':encode('ECPrivateKey', Key), + {'ECPrivateKey', Der, not_encrypted}; +encode_privkey(Key = #'RSAPrivateKey'{}) -> + {ok, Der} = 'OTP-PUB-KEY':encode('RSAPrivateKey', Key), + {'RSAPrivateKey', Der, not_encrypted}. + +-spec encode_cert(public_key:der_encoded()) -> certificate(). +encode_cert(Der) -> + {'Certificate', Der, not_encrypted}. + +%% ------------------------------------------------------------------- + +shift_date(Date, Offset) -> + calendar:gregorian_days_to_date(calendar:date_to_gregorian_days(Date) + Offset). + +format_date({Y, M, D}) -> + lists:flatten(io_lib:format("~w~2..0w~2..0w000000Z", [Y, M, D])). + +%% ------------------------------------------------------------------- + +%% @doc Write certificate + private key pair to respective files. +%% Files are created in the given directory. The filenames are derived +%% from the subject information in the certificate. +-spec write_cert(_Dir :: file:name(), {certificate(), private_key()}) -> + {file:name(), file:name()}. +write_cert(Dir, {Cert, Key}) -> + Subject = issuer_subject(Cert), + Filename = subject_info(org, Subject, "ORG") ++ "." ++ subject_info(name, Subject, "XXX"), + write_cert(Dir, Filename, {Cert, Key}). + +-spec write_cert(_Dir :: file:name(), _Prefix :: string(), {certificate(), private_key()}) -> + {file:name(), file:name()}. +write_cert(Dir, Filename, {Cert, Key}) -> + Certfile = filename:join(Dir, Filename ++ ".crt"), + Keyfile = filename:join(Dir, Filename ++ ".key"), + ok = write_pem(Certfile, Cert), + ok = write_pem(Keyfile, Key), + {Certfile, Keyfile}. + +-spec write_pem(file:name(), pem_entry() | [pem_entry()]) -> + ok | {error, file:posix()}. +write_pem(Name, Entries = [_ | _]) -> + file:write_file(Name, public_key:pem_encode(Entries)); +write_pem(Name, Entry) -> + write_pem(Name, [Entry]). diff --git a/apps/emqx_gateway/src/emqx_gateway_schema.erl b/apps/emqx_gateway/src/emqx_gateway_schema.erl index c0abb48ce..d51bf93a9 100644 --- a/apps/emqx_gateway/src/emqx_gateway_schema.erl +++ b/apps/emqx_gateway/src/emqx_gateway_schema.erl @@ -174,7 +174,7 @@ fields(dtls_opts) -> reuse_sessions => true, versions => dtls_all_available }, - false + _IsRanchListener = false ). desc(gateway) -> diff --git a/apps/emqx_gateway/src/emqx_gateway_utils.erl b/apps/emqx_gateway/src/emqx_gateway_utils.erl index c8cf979e3..47e1f7583 100644 --- a/apps/emqx_gateway/src/emqx_gateway_utils.erl +++ b/apps/emqx_gateway/src/emqx_gateway_utils.erl @@ -273,7 +273,7 @@ merge_default(Udp, Options) -> udp -> {udp_options, default_udp_options()}; dtls -> - {udp_options, default_udp_options()}; + {dtls_options, default_udp_options()}; tcp -> {tcp_options, default_tcp_options()}; ssl -> @@ -525,9 +525,11 @@ esockd_opts(Type, Opts0) when ?IS_ESOCKD_LISTENER(Type) -> udp -> Opts2#{udp_options => sock_opts(udp_options, Opts0)}; dtls -> + UDPOpts = sock_opts(udp_options, Opts0), + DTLSOpts = ssl_opts(dtls_options, Opts0), Opts2#{ - udp_options => sock_opts(udp_options, Opts0), - dtls_options => ssl_opts(dtls_options, Opts0) + udp_options => UDPOpts, + dtls_options => DTLSOpts } end ). @@ -541,12 +543,37 @@ sock_opts(Name, Opts) -> ). ssl_opts(Name, Opts) -> - Type = - case Name of - ssl_options -> tls; - dtls_options -> dtls - end, - emqx_tls_lib:to_server_opts(Type, maps:get(Name, Opts, #{})). + SSLOpts = maps:get(Name, Opts, #{}), + emqx_utils:run_fold( + [ + fun ssl_opts_crl_config/2, + fun ssl_opts_drop_unsupported/2, + fun ssl_server_opts/2 + ], + SSLOpts, + Name + ). + +ssl_opts_crl_config(#{enable_crl_check := true} = SSLOpts, _Name) -> + HTTPTimeout = emqx_config:get([crl_cache, http_timeout], timer:seconds(15)), + NSSLOpts = maps:remove(enable_crl_check, SSLOpts), + NSSLOpts#{ + %% `crl_check => true' doesn't work + crl_check => peer, + crl_cache => {emqx_ssl_crl_cache, {internal, [{http, HTTPTimeout}]}} + }; +ssl_opts_crl_config(SSLOpts, _Name) -> + %% NOTE: Removing this because DTLS doesn't like any unknown options. + maps:remove(enable_crl_check, SSLOpts). + +ssl_opts_drop_unsupported(SSLOpts, _Name) -> + %% TODO: Support OCSP stapling + maps:without([ocsp], SSLOpts). + +ssl_server_opts(SSLOpts, ssl_options) -> + emqx_tls_lib:to_server_opts(tls, SSLOpts); +ssl_server_opts(SSLOpts, dtls_options) -> + emqx_tls_lib:to_server_opts(dtls, SSLOpts). ranch_opts(Type, ListenOn, Opts) -> NumAcceptors = maps:get(acceptors, Opts, 4), @@ -635,7 +662,7 @@ default_tcp_options() -> ]. default_udp_options() -> - [binary]. + []. default_subopts() -> %% Retain Handling diff --git a/apps/emqx_gateway/test/emqx_gateway_auth_ct.erl b/apps/emqx_gateway/test/emqx_gateway_auth_ct.erl index 215302105..81ab2f368 100644 --- a/apps/emqx_gateway/test/emqx_gateway_auth_ct.erl +++ b/apps/emqx_gateway/test/emqx_gateway_auth_ct.erl @@ -238,9 +238,12 @@ http_authz_config() -> init_gateway_conf() -> ok = emqx_common_test_helpers:load_config( emqx_gateway_schema, - merge_conf([X:default_config() || X <- ?CONFS], []) + merge_conf(list_gateway_conf(), []) ). +list_gateway_conf() -> + [X:default_config() || X <- ?CONFS]. + merge_conf([Conf | T], Acc) -> case re:run(Conf, "\s*gateway\\.(.*)", [global, {capture, all_but_first, list}, dotall]) of {match, [[Content]]} -> diff --git a/apps/emqx_gateway/test/emqx_gateway_authn_SUITE.erl b/apps/emqx_gateway/test/emqx_gateway_authn_SUITE.erl index 0072447b6..7495c5858 100644 --- a/apps/emqx_gateway/test/emqx_gateway_authn_SUITE.erl +++ b/apps/emqx_gateway/test/emqx_gateway_authn_SUITE.erl @@ -22,7 +22,7 @@ -include_lib("eunit/include/eunit.hrl"). -include_lib("common_test/include/ct.hrl"). --import(emqx_gateway_auth_ct, [init_gateway_conf/0, with_resource/3]). +-import(emqx_gateway_auth_ct, [with_resource/3]). -define(checkMatch(Guard), (fun(Expr) -> @@ -54,40 +54,37 @@ groups() -> emqx_gateway_auth_ct:init_groups(?MODULE, ?AUTHNS). init_per_group(AuthName, Conf) -> - ct:pal("on group start:~p~n", [AuthName]), - {ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000), - emqx_gateway_auth_ct:start_auth(AuthName), - timer:sleep(500), - Conf. + Apps = emqx_cth_suite:start( + [ + emqx_conf, + emqx_auth, + emqx_auth_http, + emqx_management, + {emqx_dashboard, "dashboard.listeners.http { enable = true, bind = 18083 }"}, + {emqx_gateway, emqx_gateway_auth_ct:list_gateway_conf()} + | emqx_gateway_test_utils:all_gateway_apps() + ], + #{work_dir => emqx_cth_suite:work_dir(Conf)} + ), + _ = emqx_common_test_http:create_default_app(), + ok = emqx_gateway_auth_ct:start_auth(AuthName), + [{group_apps, Apps} | Conf]. end_per_group(AuthName, Conf) -> - ct:pal("on group stop:~p~n", [AuthName]), - emqx_gateway_auth_ct:stop_auth(AuthName), + ok = emqx_gateway_auth_ct:stop_auth(AuthName), + _ = emqx_common_test_http:delete_default_app(), + ok = emqx_cth_suite:stop(?config(group_apps, Conf)), Conf. init_per_suite(Config) -> - emqx_gateway_test_utils:load_all_gateway_apps(), - emqx_config:erase(gateway), - init_gateway_conf(), - emqx_mgmt_api_test_util:init_suite([grpc, emqx_conf, emqx_auth, emqx_auth_http, emqx_gateway]), - application:ensure_all_started(cowboy), - emqx_gateway_auth_ct:start(), - timer:sleep(500), - Config. + {ok, Apps1} = application:ensure_all_started(grpc), + {ok, Apps2} = application:ensure_all_started(cowboy), + {ok, _} = emqx_gateway_auth_ct:start(), + [{suite_apps, Apps1 ++ Apps2} | Config]. end_per_suite(Config) -> - emqx_gateway_auth_ct:stop(), - emqx_config:erase(gateway), - emqx_mgmt_api_test_util:end_suite([ - cowboy, emqx_conf, emqx_auth, emqx_auth_http, emqx_gateway, grpc - ]), - Config. - -init_per_testcase(_Case, Config) -> - {ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000), - Config. - -end_per_testcase(_Case, Config) -> + ok = emqx_gateway_auth_ct:stop(), + ok = emqx_cth_suite:stop_apps(?config(suite_apps, Config)), Config. %%------------------------------------------------------------------------------ diff --git a/apps/emqx_gateway/test/emqx_gateway_authz_SUITE.erl b/apps/emqx_gateway/test/emqx_gateway_authz_SUITE.erl index dd149133b..9ae464ebb 100644 --- a/apps/emqx_gateway/test/emqx_gateway_authz_SUITE.erl +++ b/apps/emqx_gateway/test/emqx_gateway_authz_SUITE.erl @@ -22,7 +22,7 @@ -include_lib("eunit/include/eunit.hrl"). -include_lib("common_test/include/ct.hrl"). --import(emqx_gateway_auth_ct, [init_gateway_conf/0, with_resource/3]). +-import(emqx_gateway_auth_ct, [with_resource/3]). -define(checkMatch(Guard), (fun(Expr) -> @@ -54,44 +54,33 @@ groups() -> emqx_gateway_auth_ct:init_groups(?MODULE, ?AUTHNS). init_per_group(AuthName, Conf) -> - {ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000), - ok = emqx_authz_test_lib:reset_authorizers(), - emqx_gateway_auth_ct:start_auth(AuthName), - timer:sleep(500), - Conf. + Apps = emqx_cth_suite:start( + [ + {emqx_conf, "authorization { no_match = deny, cache { enable = false } }"}, + emqx_auth, + emqx_auth_http, + {emqx_gateway, emqx_gateway_auth_ct:list_gateway_conf()} + | emqx_gateway_test_utils:all_gateway_apps() + ], + #{work_dir => emqx_cth_suite:work_dir(Conf)} + ), + ok = emqx_gateway_auth_ct:start_auth(AuthName), + [{group_apps, Apps} | Conf]. end_per_group(AuthName, Conf) -> - emqx_gateway_auth_ct:stop_auth(AuthName), + ok = emqx_gateway_auth_ct:stop_auth(AuthName), + ok = emqx_cth_suite:stop(?config(group_apps, Conf)), Conf. init_per_suite(Config) -> - emqx_config:erase(gateway), - emqx_gateway_test_utils:load_all_gateway_apps(), - init_gateway_conf(), - emqx_mgmt_api_test_util:init_suite([ - grpc, emqx_conf, emqx_auth, emqx_auth_http, emqx_gateway - ]), - meck:new(emqx_authz_file, [non_strict, passthrough, no_history, no_link]), - meck:expect(emqx_authz_file, create, fun(S) -> S end), - application:ensure_all_started(cowboy), - emqx_gateway_auth_ct:start(), - Config. + {ok, Apps1} = application:ensure_all_started(grpc), + {ok, Apps2} = application:ensure_all_started(cowboy), + {ok, _} = emqx_gateway_auth_ct:start(), + [{suite_apps, Apps1 ++ Apps2} | Config]. end_per_suite(Config) -> - meck:unload(emqx_authz_file), - emqx_gateway_auth_ct:stop(), - ok = emqx_authz_test_lib:restore_authorizers(), - emqx_config:erase(gateway), - emqx_mgmt_api_test_util:end_suite([ - emqx_gateway, emqx_auth_http, emqx_auth, emqx_conf, grpc - ]), - Config. - -init_per_testcase(_Case, Config) -> - {ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000), - Config. - -end_per_testcase(_Case, Config) -> + ok = emqx_gateway_auth_ct:stop(), + ok = emqx_cth_suite:stop_apps(?config(suite_apps, Config)), Config. %%------------------------------------------------------------------------------ diff --git a/apps/emqx_gateway/test/emqx_gateway_test_utils.erl b/apps/emqx_gateway/test/emqx_gateway_test_utils.erl index 950ae1bcf..2e8be5119 100644 --- a/apps/emqx_gateway/test/emqx_gateway_test_utils.erl +++ b/apps/emqx_gateway/test/emqx_gateway_test_utils.erl @@ -103,12 +103,18 @@ assert_fields_exist(Ks, Map) -> end, Ks ). + load_all_gateway_apps() -> - application:load(emqx_gateway_stomp), - application:load(emqx_gateway_mqttsn), - application:load(emqx_gateway_coap), - application:load(emqx_gateway_lwm2m), - application:load(emqx_gateway_exproto). + emqx_cth_suite:load_apps(all_gateway_apps()). + +all_gateway_apps() -> + [ + emqx_gateway_stomp, + emqx_gateway_mqttsn, + emqx_gateway_coap, + emqx_gateway_lwm2m, + emqx_gateway_exproto + ]. %%-------------------------------------------------------------------- %% http diff --git a/apps/emqx_gateway_exproto/test/emqx_exproto_SUITE.erl b/apps/emqx_gateway_exproto/test/emqx_exproto_SUITE.erl index 74a488abb..2d2583beb 100644 --- a/apps/emqx_gateway_exproto/test/emqx_exproto_SUITE.erl +++ b/apps/emqx_gateway_exproto/test/emqx_exproto_SUITE.erl @@ -20,7 +20,6 @@ -compile(nowarn_export_all). -include_lib("eunit/include/eunit.hrl"). --include_lib("emqx/include/emqx_hooks.hrl"). -include_lib("emqx/include/emqx.hrl"). -include_lib("emqx/include/emqx_mqtt.hrl"). -include_lib("snabbkaffe/include/snabbkaffe.hrl"). @@ -44,14 +43,6 @@ -define(TCPOPTS, [binary, {active, false}]). -define(DTLSOPTS, [binary, {active, false}, {protocol, dtls}]). --define(PORT, 7993). - --define(DEFAULT_CLIENT, #{ - proto_name => <<"demo">>, - proto_ver => <<"v0.1">>, - clientid => <<"test_client_1">> -}). - %%-------------------------------------------------------------------- -define(CONF_DEFAULT, << "\n" @@ -126,15 +117,33 @@ init_per_group(_, Cfg) -> init_per_group(LisType, ServiceName, Scheme, Cfg) -> Svrs = emqx_exproto_echo_svr:start(Scheme), - application:load(emqx_gateway_exproto), - emqx_common_test_helpers:start_apps( - [emqx_conf, emqx_auth, emqx_gateway], - fun(App) -> - set_special_cfg(App, LisType, ServiceName, Scheme) - end + Addrs = lists:flatten(io_lib:format("~s://127.0.0.1:9001", [Scheme])), + GWConfig = #{ + server => #{bind => 9100}, + idle_timeout => 5000, + mountpoint => <<"ct/">>, + handler => #{ + address => Addrs, + service_name => ServiceName, + ssl_options => #{enable => Scheme == https} + }, + listeners => listener_confs(LisType) + }, + Apps = emqx_cth_suite:start( + [ + emqx_conf, + emqx_auth, + {emqx_gateway, #{ + config => + #{gateway => #{exproto => GWConfig}} + }}, + emqx_gateway_exproto + ], + #{work_dir => emqx_cth_suite:work_dir(Cfg)} ), [ {servers, Svrs}, + {apps, Apps}, {listener_type, LisType}, {service_name, ServiceName}, {grpc_client_scheme, Scheme} @@ -142,8 +151,7 @@ init_per_group(LisType, ServiceName, Scheme, Cfg) -> ]. end_per_group(_, Cfg) -> - emqx_config:erase(gateway), - emqx_common_test_helpers:stop_apps([emqx_gateway, emqx_auth, emqx_conf]), + ok = emqx_cth_suite:stop(proplists:get_value(apps, Cfg)), emqx_exproto_echo_svr:stop(proplists:get_value(servers, Cfg)). init_per_testcase(TestCase, Cfg) when @@ -159,27 +167,12 @@ init_per_testcase(_TestCase, Cfg) -> end_per_testcase(_TestCase, _Cfg) -> ok. -set_special_cfg(emqx_gateway, LisType, ServiceName, Scheme) -> - Addrs = lists:flatten(io_lib:format("~s://127.0.0.1:9001", [Scheme])), - emqx_config:put( - [gateway, exproto], - #{ - server => #{bind => 9100}, - idle_timeout => 5000, - mountpoint => <<"ct/">>, - handler => #{ - address => Addrs, - service_name => ServiceName, - ssl_options => #{enable => Scheme == https} - }, - listeners => listener_confs(LisType) - } - ); -set_special_cfg(_, _, _, _) -> - ok. - listener_confs(Type) -> - Default = #{bind => 7993, acceptors => 8}, + Default = #{ + bind => 7993, + max_connections => 64, + access_rules => ["allow all"] + }, #{Type => #{'default' => maps:merge(Default, socketopts(Type))}}. default_config() -> @@ -636,9 +629,13 @@ close({dtls, Sock}) -> %% Server-Opts socketopts(tcp) -> - #{tcp_options => tcp_opts()}; + #{ + acceptors => 8, + tcp_options => tcp_opts() + }; socketopts(ssl) -> #{ + acceptors => 8, tcp_options => tcp_opts(), ssl_options => ssl_opts() }; @@ -646,6 +643,7 @@ socketopts(udp) -> #{udp_options => udp_opts()}; socketopts(dtls) -> #{ + acceptors => 8, udp_options => udp_opts(), dtls_options => dtls_opts() }. diff --git a/apps/emqx_gateway_mqttsn/test/emqx_sn_protocol_SUITE.erl b/apps/emqx_gateway_mqttsn/test/emqx_sn_protocol_SUITE.erl index 0c4c4e6bf..25d8bcc51 100644 --- a/apps/emqx_gateway_mqttsn/test/emqx_sn_protocol_SUITE.erl +++ b/apps/emqx_gateway_mqttsn/test/emqx_sn_protocol_SUITE.erl @@ -66,7 +66,6 @@ -elvis([{elvis_style, dont_repeat_yourself, disable}]). -define(CONF_DEFAULT, << - "\n" "gateway.mqttsn {\n" " gateway_id = 1\n" " broadcast = true\n" @@ -89,6 +88,20 @@ "}\n" >>). +-define(CONF_DTLS, << + "\n" + "gateway.mqttsn {" + " listeners.dtls.default {\n" + " bind = 1885\n" + " dtls_options {\n" + " cacertfile = \"${cacertfile}\"\n" + " certfile = \"${certfile}\"\n" + " keyfile = \"${keyfile}\"\n" + " }\n" + " }\n" + "}\n" +>>). + %%-------------------------------------------------------------------- %% Setups %%-------------------------------------------------------------------- @@ -97,9 +110,22 @@ all() -> emqx_common_test_helpers:all(?MODULE). init_per_suite(Config) -> + PrivDir = ?config(priv_dir, Config), + Root = emqx_cth_tls:gen_cert(#{key => ec, issuer => root}), + Server = emqx_cth_tls:gen_cert(#{key => ec, issuer => Root}), + {CACertfile, _} = emqx_cth_tls:write_cert(PrivDir, Root), + {Certfile, Keyfile} = emqx_cth_tls:write_cert(PrivDir, Server), + Conf = emqx_template:render_strict( + emqx_template:parse([?CONF_DEFAULT, ?CONF_DTLS]), + #{ + cacertfile => CACertfile, + certfile => Certfile, + keyfile => Keyfile + } + ), Apps = emqx_cth_suite:start( [ - {emqx_conf, ?CONF_DEFAULT}, + {emqx_conf, Conf}, emqx_gateway, emqx_auth, emqx_management, @@ -108,7 +134,7 @@ init_per_suite(Config) -> #{work_dir => emqx_cth_suite:work_dir(Config)} ), emqx_common_test_http:create_default_app(), - [{suite_apps, Apps} | Config]. + [{suite_apps, Apps}, {cacertfile, CACertfile} | Config]. end_per_suite(Config) -> {ok, _} = emqx:remove_config([gateway, mqttsn]), @@ -191,6 +217,25 @@ t_first_disconnect(_) -> ?assertEqual(<<2, ?SN_DISCONNECT>>, receive_response(Socket)), gen_udp:close(Socket). +t_connect_dtls(Config) -> + SockName = {'mqttsn:dtls:default', 1885}, + ?assertEqual(true, lists:keymember(SockName, 1, esockd:listeners())), + + ClientOpts = [ + binary, + {active, false}, + {protocol, dtls}, + {cacertfile, ?config(cacertfile, Config)} + | emqx_common_test_helpers:ssl_verify_fun_allow_any_host() + ], + {ok, Socket} = ssl:connect(?HOST, 1885, ClientOpts, 1000), + ok = ssl:send(Socket, make_connect_msg(<<"client_id_test1">>, 1)), + ?assertEqual({ok, <<3, ?SN_CONNACK, 0>>}, ssl:recv(Socket, 0, 1000)), + + ok = ssl:send(Socket, make_disconnect_msg(undefined)), + ?assertEqual({ok, <<2, ?SN_DISCONNECT>>}, ssl:recv(Socket, 0, 1000)), + ssl:close(Socket). + t_subscribe(_) -> Dup = 0, QoS = 0, @@ -2444,10 +2489,7 @@ send_searchgw_msg(Socket) -> Radius = 0, ok = gen_udp:send(Socket, ?HOST, ?PORT, <>). -send_connect_msg(Socket, ClientId) -> - send_connect_msg(Socket, ClientId, 1). - -send_connect_msg(Socket, ClientId, CleanSession) when +make_connect_msg(ClientId, CleanSession) when CleanSession == 0; CleanSession == 1 -> @@ -2460,9 +2502,14 @@ send_connect_msg(Socket, ClientId, CleanSession) when TopicIdType = 0, ProtocolId = 1, Duration = 10, - Packet = - <>, + <>. + +send_connect_msg(Socket, ClientId) -> + send_connect_msg(Socket, ClientId, 1). + +send_connect_msg(Socket, ClientId, CleanSession) -> + Packet = make_connect_msg(ClientId, CleanSession), ok = gen_udp:send(Socket, ?HOST, ?PORT, Packet). send_connect_msg_with_will(Socket, Duration, ClientId) -> @@ -2724,15 +2771,17 @@ send_pingreq_msg(Socket, ClientId) -> ?LOG("send_pingreq_msg ClientId=~p", [ClientId]), ok = gen_udp:send(Socket, ?HOST, ?PORT, PingReqPacket). -send_disconnect_msg(Socket, Duration) -> +make_disconnect_msg(Duration) -> Length = 2, Length2 = 4, MsgType = ?SN_DISCONNECT, - DisConnectPacket = - case Duration of - undefined -> <>; - Other -> <> - end, + case Duration of + undefined -> <>; + Other -> <> + end. + +send_disconnect_msg(Socket, Duration) -> + DisConnectPacket = make_disconnect_msg(Duration), ?LOG("send_disconnect_msg Duration=~p", [Duration]), ok = gen_udp:send(Socket, ?HOST, ?PORT, DisConnectPacket). diff --git a/changes/ce/fix-12180.en.md b/changes/ce/fix-12180.en.md new file mode 100644 index 000000000..3ded8a507 --- /dev/null +++ b/changes/ce/fix-12180.en.md @@ -0,0 +1 @@ +Fix an issue where DTLS enabled MQTT-SN gateways could not be started, caused by incompatibility of default listener configuration with the DTLS implementation.