]> git.ipfire.org Git - thirdparty/curl.git/commitdiff
TLS: add support for ECH (Encrypted Client Hello)
authorStephen Farrell <stephen.farrell@cs.tcd.ie>
Thu, 4 Apr 2024 13:23:35 +0000 (14:23 +0100)
committerDaniel Stenberg <daniel@haxx.se>
Tue, 16 Apr 2024 06:10:53 +0000 (08:10 +0200)
An EXPERIMENTAL feature used with CURLOPT_ECH and --ech.

Closes #11922

40 files changed:
.github/scripts/spellcheck.words
.gitignore
CMakeLists.txt
configure.ac
docs/ECH.md [new file with mode: 0644]
docs/EXPERIMENTAL.md
docs/cmdline-opts/Makefile.inc
docs/cmdline-opts/ech.md [new file with mode: 0644]
docs/libcurl/curl_easy_setopt.md
docs/libcurl/libcurl-errors.md
docs/libcurl/opts/CURLOPT_ECH.md [new file with mode: 0644]
docs/libcurl/opts/Makefile.inc
docs/libcurl/symbols-in-versions
docs/options-in-versions
include/curl/curl.h
include/curl/typecheck-gcc.h
lib/curl_config.h.cmake
lib/doh.c
lib/doh.h
lib/easyoptions.c
lib/hostip.c
lib/hostip.h
lib/setopt.c
lib/strerror.c
lib/urldata.h
lib/vtls/openssl.c
lib/vtls/wolfssl.c
m4/curl-confopts.m4
packages/OS400/ccsidcurl.c
src/tool_cfgable.c
src/tool_cfgable.h
src/tool_getparam.c
src/tool_help.c
src/tool_help.h
src/tool_listhelp.c
src/tool_operate.c
tests/data/test1462
tests/data/test1538
tests/ech_combos.py [new file with mode: 0755]
tests/ech_tests.sh [new file with mode: 0755]

index 050513c76f2769547f80ddf858b04ccf84421174..5e534846c815bf7acf50e15b4a50c6b51ccaa80c 100644 (file)
@@ -2,6 +2,7 @@
 #
 # SPDX-License-Identifier: curl
 #
+AAAA
 ABI
 accessor
 ACK
@@ -10,6 +11,7 @@ AIA
 AIX
 al
 Alessandro
+aliasMode
 allocator
 alnum
 ALPN
@@ -109,6 +111,7 @@ CLA
 CLAs
 cleartext
 CLI
+ClientHello
 clientp
 cliget
 closesocket
@@ -116,6 +119,8 @@ CMake
 cmake
 CMake's
 cmake's
+CNAME
+CNAMEs
 CMakeLists
 CNA
 CodeQL
@@ -146,6 +151,7 @@ cURL
 CURLcode
 curldown
 CURLE
+CURLECH
 CURLH
 curlimages
 CURLINFO
@@ -164,6 +170,7 @@ dbg
 Debian
 DEBUGBUILD
 decrypt
+decrypting
 deepcode
 DELE
 DER
@@ -190,6 +197,7 @@ DNS
 dns
 dnsop
 DoH
+DoT
 doxygen
 drftpd
 dsa
@@ -201,6 +209,9 @@ EBCDIC
 ECC
 ECDHE
 ECH
+ecl
+ECHConfig
+ECHConfigList
 ECONNREFUSED
 eCOS
 EFnet
@@ -284,6 +295,8 @@ GOST
 GPG
 GPL
 GPLed
+GREASE
+GREASEing
 Greear
 groff
 gsasl
@@ -307,6 +320,7 @@ Hards
 Haxx
 haxx
 Heimdal
+HelloRetryRequest
 HELO
 HH
 HMAC
@@ -316,6 +330,7 @@ homebrew
 hostname
 hostnames
 Housley
+HRR
 Hruska
 HSTS
 hsts
@@ -460,6 +475,7 @@ Marek
 Mavrogiannopoulos
 Mbed
 mbedTLS
+md
 Meglio
 memdebug
 MesaLink
@@ -470,6 +486,7 @@ Michal
 Micrium
 MicroBlaze
 MicroOS
+middlebox
 mingw
 MinGW
 MINIX
@@ -590,6 +607,7 @@ pkcs
 PKGBUILD
 PKI
 pluggable
+pn
 PolarSSL
 Polhem
 pollset
@@ -625,6 +643,7 @@ py
 pycurl
 pytest
 Pytest
+qname
 QNX
 QoS
 Qubes
@@ -668,6 +687,9 @@ Roadmap
 Rockbox
 roffit
 RPG
+RR
+RRs
+RRtype
 RSA
 RTMP
 rtmp
@@ -784,6 +806,7 @@ SunSSH
 superset
 svc
 svcb
+SVCB
 Svyatoslav
 Swisscom
 sws
index 2d5c292325d8e34cf2e75ab90febcac0768b396e..6d1e69ed04b589f1712389ba90106410fc716074 100644 (file)
@@ -65,3 +65,4 @@ curl_fuzzer_seed_corpus.zip
 libstandaloneengine.a
 tests/string
 tests/config
+tests/ech-log/
index 4607c51d13c1daf202cac4664165832e7fc16fbc..d5c778bb047d6774023cf43769856eb2e1568175 100644 (file)
@@ -37,6 +37,7 @@
 #   HAVE_GNUTLS_SRP: `gnutls_srp_verifier` present in GnuTLS
 #   HAVE_SSL_CTX_SET_QUIC_METHOD: `SSL_CTX_set_quic_method` present in OpenSSL/wolfSSL
 #   HAVE_QUICHE_CONN_SET_QLOG_FD: `quiche_conn_set_qlog_fd` present in QUICHE
+#   HAVE_ECH: ECH API checks for OpenSSL, boringssl or wolfSSL
 #
 # For each of the above variables, if the variable is DEFINED (either
 # to ON or OFF), the symbol detection will be skipped.  If the
@@ -654,6 +655,31 @@ if(USE_OPENSSL OR USE_WOLFSSL)
   endif()
 endif()
 
+option(USE_HTTPSRR "Enable HTTPS RR support for ECH (experimental)" OFF)
+option(USE_ECH "Enable ECH support" OFF)
+if(USE_ECH)
+  if(USE_OPENSSL OR USE_WOLFSSL)
+    # Be sure that the OpenSSL/wolfSSL library actually supports ECH.
+    if(NOT DEFINED HAVE_ECH)
+      if(USE_OPENSSL AND HAVE_BORINGSSL)
+        openssl_check_symbol_exists(SSL_set1_ech_config_list "openssl/ssl.h" HAVE_ECH)
+      elseif(USE_OPENSSL)
+        openssl_check_symbol_exists(SSL_ech_set1_echconfig "openssl/ech.h" HAVE_ECH)
+      elseif(USE_WOLFSSL)
+        openssl_check_symbol_exists(wolfSSL_CTX_GenerateEchConfig "wolfssl/options.h;wolfssl/ssl.h" HAVE_ECH)
+      endif()
+    endif()
+    if(NOT HAVE_ECH)
+      message(FATAL_ERROR "ECH support missing in OpenSSL/BoringSSL/wolfSSL")
+    else()
+      message("ECH enabled.")
+    endif()
+  else()
+    message(FATAL_ERROR "ECH requires ECH-enablded OpenSSL, BoringSSL or wolfSSL")
+  endif()
+endif()
+
+
 option(USE_NGHTTP2 "Use nghttp2 library" OFF)
 if(USE_NGHTTP2)
   find_package(NGHTTP2 REQUIRED)
@@ -1590,6 +1616,8 @@ if(NOT CURL_DISABLE_INSTALL)
   _add_if("IPFS"          NOT CURL_DISABLE_HTTP)
   _add_if("IPNS"          NOT CURL_DISABLE_HTTP)
   _add_if("HTTPS"         NOT CURL_DISABLE_HTTP AND SSL_ENABLED)
+  _add_if("ECH"           HAVE_ECH)
+  _add_if("HTTPSRR"       HAVE_ECH)
   _add_if("FTP"           NOT CURL_DISABLE_FTP)
   _add_if("FTPS"          NOT CURL_DISABLE_FTP AND SSL_ENABLED)
   _add_if("FILE"          NOT CURL_DISABLE_FILE)
index c31870ded57d987be58d3f234c8e53020d050ef5..21a3debbdc05a8453f9028d7aa8ad709e34346f4 100644 (file)
@@ -51,6 +51,7 @@ CURL_CHECK_OPTION_CURLDEBUG
 CURL_CHECK_OPTION_SYMBOL_HIDING
 CURL_CHECK_OPTION_ARES
 CURL_CHECK_OPTION_RT
+CURL_CHECK_OPTION_HTTPSRR
 CURL_CHECK_OPTION_ECH
 
 XC_CHECK_PATH_SEPARATOR
@@ -4538,6 +4539,16 @@ if test "x$hsts" != "xyes"; then
   AC_DEFINE(CURL_DISABLE_HSTS, 1, [disable alt-svc])
 fi
 
+
+dnl *************************************************************
+dnl check whether HTTPSRR support if desired
+dnl
+if test "x$want_httpsrr" != "xno"; then
+  AC_MSG_RESULT([HTTPSRR support is available])
+  AC_DEFINE(USE_HTTPSRR, 1, [enable HTTPS RR support])
+  experimental="$experimental HTTPSRR"
+fi
+
 dnl *************************************************************
 dnl check whether ECH support, if desired, is actually available
 dnl
@@ -4548,18 +4559,28 @@ if test "x$want_ech" != "xno"; then
   ECH_ENABLED=0
   ECH_SUPPORT=''
 
-  dnl OpenSSL with a chosen ECH function should be enough
-  dnl so more exhaustive checking seems unnecessary for now
+  dnl check for OpenSSL
   if test "x$OPENSSL_ENABLED" = "x1"; then
-    AC_CHECK_FUNCS(SSL_get_ech_status,
-      ECH_SUPPORT="ECH support available (OpenSSL with SSL_get_ech_status)"
+    AC_CHECK_FUNCS(SSL_ech_set1_echconfig,
+      ECH_SUPPORT="ECH support available via OpenSSL with SSL_ech_set1_echconfig"
+      ECH_ENABLED=1)
+  fi
+  dnl check for boringssl equivalent
+  if test "x$OPENSSL_ENABLED" = "x1"; then
+    AC_CHECK_FUNCS(SSL_set1_ech_config_list,
+      ECH_SUPPORT="ECH support available via boringssl with SSL_set1_ech_config_list"
+      ECH_ENABLED=1)
+  fi
+  if test "x$WOLFSSL_ENABLED" = "x1"; then
+    AC_CHECK_FUNCS(wolfSSL_CTX_GenerateEchConfig,
+      ECH_SUPPORT="ECH support available via WolfSSL with wolfSSL_CTX_GenerateEchConfig"
       ECH_ENABLED=1)
-
-  dnl add 'elif' chain here for additional implementations
   fi
 
   dnl now deal with whatever we found
   if test "x$ECH_ENABLED" = "x1"; then
+    dnl force pre-requisites for ECH
+    AC_DEFINE(USE_HTTPSRR, 1, [force HTTPS RR support for ECH])
     AC_DEFINE(USE_ECH, 1, [if ECH support is available])
     AC_MSG_RESULT($ECH_SUPPORT)
     experimental="$experimental ECH"
@@ -4777,10 +4798,6 @@ else
   AC_MSG_RESULT([no])
 fi
 
-if test "x$ECH_ENABLED" = "x1"; then
-  SUPPORT_FEATURES="$SUPPORT_FEATURES ECH"
-fi
-
 if test ${ac_cv_sizeof_curl_off_t} -gt 4; then
   if test ${ac_cv_sizeof_off_t} -gt 4 -o \
      "$curl_win32_file_api" = "win32_large_files"; then
diff --git a/docs/ECH.md b/docs/ECH.md
new file mode 100644 (file)
index 0000000..cfb897d
--- /dev/null
@@ -0,0 +1,479 @@
+<!--
+Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
+
+SPDX-License-Identifier: curl
+-->
+
+# Building curl with HTTPS-RR and ECH support
+
+We've added support for ECH to in this curl build. That can use HTTPS RRs
+published in the DNS, if curl is using DoH, or else can accept the relevant
+ECHConfigList values from the command line. That works with OpenSSL,
+WolfSSL or boringssl as the TLS provider, depending on how you build curl.
+
+This feature is EXPERIMENTAL. DO NOT USE IN PRODUCTION.
+
+This should however provide enough of a proof-of-concept to prompt an informed
+discussion about a good path forward for ECH support in curl, when using
+OpenSSL, or other TLS libraries, as those add ECH support.
+
+## OpenSSL Build
+
+To build our ECH-enabled OpenSSL fork:
+
+```bash
+    cd $HOME/code
+    git clone https://github.com/defo-project/openssl
+    cd openssl
+    ./config --libdir=lib --prefix=$HOME/code/openssl-local-inst
+    ...stuff...
+    make -j8
+    ...stuff (maybe go for coffee)...
+    make install_sw
+    ...a little bit of stuff...
+```
+
+To build curl ECH-enabled, making use of the above:
+
+```bash
+    cd $HOME/code
+    git clone https://github.com/curl/curl
+    cd curl
+    autoreconf -fi
+    LDFLAGS="-Wl,-rpath,$HOME/code/openss-local-inst/lib/" ./configure  --with-ssl=$HOME/code/openssl-local-inst  --enable-ech --enable-httpsrr
+    ...lots of output...
+    WARNING: ech ECH HTTPSRR enabled but marked EXPERIMENTAL...
+    make
+    ...lots more output...
+```
+
+If you do not get that WARNING at the end of the ``configure`` command, then ECH
+is not enabled, so go back some steps and re-do whatever needs re-doing:-) If you
+want to debug curl then you should add ``--enable-debug`` to the ``configure``
+command.
+
+With the above build, I still need to set ``LD_LIBRARY_PATH`` to run the
+version of curl built against OpenSSL in my development environment (Ubuntu
+23.10).
+
+## Using ECH and DoH
+
+Curl supports using DoH for A/AAAA lookups so it was relatively easy to add
+retrieval of HTTPS RRs in that situation. To use ECH and DoH together:
+
+```bash
+    cd $HOME/code/curl
+    LD_LIBRARY_PATH=$HOME/code/openssl ./src/curl --ech true --doh-url https://one.one.one.one/dns-query https://defo.ie/ech-check.php
+    ...
+    SSL_ECH_STATUS: success <img src="greentick-small.png" alt="good" /> <br/>
+    ...
+```
+
+The output snippet above is within the HTML for the webpage, when things work.
+
+The above works for these test sites:
+
+```bash
+    https://defo.ie/ech-check.php
+    https://draft-13.esni.defo.ie:8413/stats
+    https://draft-13.esni.defo.ie:8414/stats
+    https://crypto.cloudflare.com/cdn-cgi/trace
+    https://tls-ech.dev
+```
+
+The list above has 4 different server technologies, implemented by 3 different
+parties, and includes a case (the port 8414 server) where HelloRetryRequest
+(HRR) is forced.
+
+We currently support the following new curl command line arguments/options:
+
+- ``--ech <config>`` - the ``config`` value can be one of:
+    - ``false`` says to not attempt ECH
+    - ``true`` says to attempt ECH, if possible
+    - ``grease`` if attempting ECH is not possible, then send a GREASE ECH extension
+    - ``hard`` hard-fail the connection if ECH cannot be attempted
+    - ``ecl:<b64value>`` a base64 encoded ECHConfigList, rather than one accessed from the DNS
+    - ``pn:<name>`` over-ride the ``public_name`` from an ECHConfigList
+
+Note that in the above "attempt ECH" means the client emitting a TLS
+ClientHello with a "real" ECH extension, but that does not mean that the
+relevant server can succeed in decrypting, as things can fail for other
+reasons.
+
+## Supplying an ECHConfigList on the command line
+
+To supply the ECHConfigList on the command line, you might need a bit of
+cut-and-paste, e.g.:
+
+```bash
+    dig +short https defo.ie
+    1 . ipv4hint=213.108.108.101 ech=AED+DQA8PAAgACD8WhlS7VwEt5bf3lekhHvXrQBGDrZh03n/LsNtAodbUAAEAAEAAQANY292ZXIuZGVmby5pZQAA ipv6hint=2a00:c6c0:0:116:5::10
+```
+
+Then paste the base64 encoded ECHConfigList onto the curl command line:
+
+```bash
+    LD_LIBRARY_PATH=$HOME/code/openssl ./src/curl --ech ecl:AED+DQA8PAAgACD8WhlS7VwEt5bf3lekhHvXrQBGDrZh03n/LsNtAodbUAAEAAEAAQANY292ZXIuZGVmby5pZQAA https://defo.ie/ech-check.php
+    ...
+    SSL_ECH_STATUS: success <img src="greentick-small.png" alt="good" /> <br/>
+    ...
+```
+
+The output snippet above is within the HTML for the webpage.
+
+If you paste in the wrong ECHConfigList (it changes hourly for ``defo.ie``) you
+should get an error like this:
+
+```bash
+    LD_LIBRARY_PATH=$HOME/code/openssl ./src/curl -vvv --ech ecl:AED+DQA8yAAgACDRMQo+qYNsNRNj+vfuQfFIkrrUFmM4vogucxKj/4nzYgAEAAEAAQANY292ZXIuZGVmby5pZQAA https://defo.ie/ech-check.php
+    ...
+    * OpenSSL/3.3.0: error:0A00054B:SSL routines::ech required
+    ...
+```
+
+There is a reason to want this command line option - for use before publishing
+an ECHConfigList in the DNS as per the Internet-draft [A well-known URI for
+publishing ECHConfigList values](https://datatracker.ietf.org/doc/draft-ietf-tls-wkech/).
+
+If you do use a wrong ECHConfigList value, then the server might return a
+good value, via the ``retry_configs`` mechanism. You can see that value in
+the verbose output, e.g.:
+
+```bash
+    LD_LIBRARY_PATH=$HOME/code/openssl ./src/curl -vvv --ech ecl:AED+DQA8yAAgACDRMQo+qYNsNRNj+vfuQfFIkrrUFmM4vogucxKj/4nzYgAEAAEAAQANY292ZXIuZGVmby5pZQAA https://defo.ie/ech-check.php
+    ...
+* ECH: retry_configs AQD+DQA8DAAgACBvYqJy+Hgk33wh/ZLBzKSPgwxeop7gvojQzfASq7zeZQAEAAEAAQANY292ZXIuZGVmby5pZQAA/g0APEMAIAAgXkT5r4cYs8z19q5rdittyIX8gfQ3ENW4wj1fVoiJZBoABAABAAEADWNvdmVyLmRlZm8uaWUAAP4NADw2ACAAINXSE9EdXzEQIJZA7vpwCIQsWqsFohZARXChgPsnfI1kAAQAAQABAA1jb3Zlci5kZWZvLmllAAD+DQA8cQAgACASeiD5F+UoSnVoHvA2l1EifUVMFtbVZ76xwDqmMPraHQAEAAEAAQANY292ZXIuZGVmby5pZQAA
+* ECH: retry_configs for defo.ie from cover.defo.ie, 319
+    ...
+```
+
+At that point, you could copy the base64 encoded value above and try again.
+For now, this only works for the OpenSSL and boringssl builds.
+
+## Default settings
+
+Curl has various ways to configure default settings, e.g. in ``$HOME/.curlrc``,
+so one can set the DoH URL and enable ECH that way:
+
+```bash
+    cat ~/.curlrc
+    doh-url=https://one.one.one.one/dns-query
+    silent
+    ech=true
+```
+
+Note that when you use the system's curl command (rather than our ECH-enabled
+build), it is liable to warn that ``ech`` is an unknown option. If that is an
+issue (e.g. if some script re-directs stdout and stderr somewhere) then adding
+the ``silent`` line above seems to be a good enough fix. (Though of
+course, yet another script could depend on non-silent behavior, so you may have
+to figure out what you prefer yourself.) That seems to have changed with the
+latest build, previously ``silent=TRUE`` was what I used in ``~/.curlrc`` but
+now that seems to cause a problem, so that the following line(s) are ignored.
+
+If you want to always use our OpenSSL build you can set ``LD_LIBRARY_PATH``
+in the environment:
+
+```bash
+    export LD_LIBRARY_PATH=$HOME/code/openssl
+```
+
+When you do the above, there can be a mismatch between OpenSSL versions
+for applications that check that. A ``git push`` for example fails so you
+should unset ``LD_LIBRARY_PATH`` before doing that or use a different shell.
+
+```bash
+    git push
+    OpenSSL version mismatch. Built against 30000080, you have 30200000
+    ...
+```
+
+With all that setup as above the command line gets simpler:
+
+```bash
+    ./src/curl https://defo.ie/ech-check.php
+    ...
+    SSL_ECH_STATUS: success <img src="greentick-small.png" alt="good" /> <br/>
+    ...
+```
+
+The ``--ech true`` option is opportunistic, so tries to do ECH but does not fail if
+the client for example cannot find any ECHConfig values. The ``--ech hard``
+option hard-fails if there is no ECHConfig found in DNS, so for now, that is not
+a good option to set as a default. Once ECH has really been attempted by
+the client, if decryption on the server side fails, then curl fails.
+
+## Code changes for ECH support when using DoH
+
+Code changes are ``#ifdef`` protected via ``USE_ECH`` or ``USE_HTTPSRR``:
+
+- ``USE_HTTPSRR`` is used for HTTPS RR retrieval code that could be generically
+  used should non-ECH uses for HTTPS RRs be identified, e.g. use of ALPN values
+or IP address hints.
+
+- ``USE_ECH`` protects ECH specific code.
+
+There are various obvious code blocks for handling the new command line
+arguments which aren't described here, but should be fairly clear.
+
+As shown in the ``configure`` usage above, there are ``configure.ac`` changes
+that allow separately dis/enabling ``USE_HTTPSRR`` and ``USE_ECH``. If ``USE_ECH``
+is enabled, then ``USE_HTTPSRR`` is forced. In both cases ``USE_DOH``
+is required. (There may be some configuration conflicts available for the
+determined:-)
+
+The main functional change, as you would expect, is in ``lib/vtls/openssl.c``
+where an ECHConfig, if available from command line or DNS cache, is fed into
+the OpenSSL library via the new APIs implemented in our OpenSSL fork for that
+purpose. This code also implements the opportunistic (``--ech true``) or hard-fail
+(``--ech hard``) logic.
+
+Other than that, the main additions are in ``lib/doh.c``
+where we re-use ``dohprobe()`` to retrieve an HTTPS RR value for the target
+domain. If such a value is found, that is stored using a new ``store_https()``
+function in a new field in the ``dohentry`` structure.
+
+The qname for the DoH query is modified if the port number is not 443, as
+defined in the SVCB specification.
+
+When the DoH process has worked, ``Curl_doh_is_resolved()`` now also returns
+the relevant HTTPS RR value data in the ``Curl_dns_entry`` structure.
+That is later accessed when the TLS session is being established, if ECH is
+enabled (from ``lib/vtls/openssl.c`` as described above).
+
+## Limitations
+
+Things that need fixing, but that can probably be ignored for the
+moment:
+
+- We could easily add code to make use of an ``alpn=`` value found in an HTTPS
+  RR, passing that on to OpenSSL for use as the "inner" ALPN value, but have
+yet to do that.
+
+Current limitations (more interesting than the above):
+
+- Only the first HTTPS RR value retrieved is actually processed as described
+  above, that could be extended in future, though picking the "right" HTTPS RR
+could be non-trivial if multiple RRs are published - matching IP address hints
+versus A/AAAA values might be a good basis for that. Last I checked though,
+browsers supporting ECH did not handle multiple HTTPS RRs well, though that
+needs re-checking as it has been a while.
+
+- It is unclear how one should handle any IP address hints found in an HTTPS RR.
+  It may be that a bit of consideration of how "multi-CDN" deployments might
+emerge would provide good answers there, but for now, it is not clear how best
+curl might handle those values when present in the DNS.
+
+- The SVCB/HTTPS RR specification supports a new "CNAME at apex" indirection
+  ("aliasMode") - the current code takes no account of that at all. One could
+envisage implementing the equivalent of following CNAMEs in such cases, but
+it is not clear if that'd be a good plan. (As of now, chrome browsers do not seem
+to have any support for that "aliasMode" and we've not checked Firefox for that
+recently.)
+
+- We have not investigated what related changes or additions might be needed
+  for applications using libcurl, as opposed to use of curl as a command line
+tool.
+
+- We have not yet implemented tests as part of the usual curl test harness as
+doing so would seem to require re-implementing an ECH-enabled server as part
+of the curl test harness. For now, we have a ``./tests/ech_test.sh`` script
+that attempts ECH with various test servers and with many combinations of the
+allowed command line options. While that is a useful test and has find issues,
+it is not comprehensive and we're not (as yet) sure what would be the right
+level of coverage. When running that script you should not have a
+``$HOME/.curlrc`` file that affects ECH or some of the negative tests could
+produce spurious failures.
+
+## Building with cmake
+
+To build with cmake, assuming our ECH-enabled OpenSSL is as before:
+
+```bash
+    cd $HOME/code
+    git clone https://github.com/curl/curl
+    cd curl
+    mkdir build
+    cd build
+    cmake -DOPENSSL_ROOT_DIR=$HOME/code/openssl -DUSE_ECH=1 -DUSE_HTTPSRR=1 ..
+    ...
+    make
+    ...
+    [100%] Built target curl
+```
+
+The binary produced by the cmake build does not need any ECH-specific
+``LD_LIBRARY_PATH`` setting.
+
+## boringssl build
+
+BoringSSL is also supported by curl and also supports ECH, so to build
+with that, instead of our ECH-enabled OpenSSL:
+
+```bash
+    cd $HOME/code
+    git clone https://boringssl.googlesource.com/boringssl
+    cd boringssl
+    cmake -DCMAKE_INSTALL_PREFIX:PATH=$HOME/code/boringssl/inst -DBUILD_SHARED_LIBS=1
+    make
+    ...
+    make install
+```
+
+Then:
+
+```bash
+    cd $HOME/code
+    git clone https://github.com/curl/curl
+    cd curl
+    autoreconf -fi
+    LDFLAGS="-Wl,-rpath,$HOME/code/boringssl/inst/lib" ./configure --with-ssl=$HOME/code/boringssl/inst --enable-ech --enable-httpsrr
+    ...lots of output...
+    WARNING: ech ECH HTTPSRR enabled but marked EXPERIMENTAL. Use with caution!
+    make
+```
+
+The boringssl APIs are fairly similar to those in our ECH-enabled OpenSSL
+fork, so code changes are also in ``lib/vtls/openssl.c``, protected
+via ``#ifdef OPENSSL_IS_BORINGSSL`` and are mostly obvious API variations.
+The boringssl APIs however do not support the ``--ech pn:`` command line
+variant as of now.
+
+## WolfSSL build
+
+WolfSSL also supports ECH and can be used by curl, so here's how:
+
+```bash
+    cd $HOME/code
+    git clone https://github.com/wolfSSL/wolfssl
+    cd wolfssl
+    ./autogen.sh
+    ./configure --prefix=$HOME/code/wolfssl/inst --enable-ech --enable-debug --enable-opensslextra
+    make
+    make install
+```
+
+The install prefix (``inst``) in the above causes WolfSSL to be installed there
+and we seem to need that for the curl configure command to work out. The
+``--enable-opensslextra`` turns out (after much faffing about;-) to be
+important or else we get build problems with curl below.
+
+```bash
+    cd $HOME/code
+    git clone https://github.com/curl/curl
+    cd curl
+    autoreconf -fi
+    ./configure --with-wolfssl=$HOME/code/wolfssl/inst --enable-ech --enable-httpsrr
+    make
+```
+
+There are some known issues with the ECH implementation in WolfSSL:
+
+- The main issue is that the client currently handles HelloRetryRequest
+  incorrectly.  [HRR issue](https://github.com/wolfSSL/wolfssl/issues/6802).)
+  The HRR issue means that the client does not work for
+  [this ECH test web site](https://tls-ech.dev) and any other similarly configured
+  sites.
+- There is also an issue related to so-called middlebox compatibility mode.
+  [middlebox compatibility issue](https://github.com/wolfSSL/wolfssl/issues/6774) 
+
+### Code changes to support WolfSSL
+
+There are what seem like oddball differences:
+
+- The DoH URL in``$HOME/.curlrc`` can use "1.1.1.1" for OpenSSL but has to be
+  "one.one.one.one" for WolfSSL. The latter works for both, so OK, we'll change
+  to that.
+- There seems to be some difference in CA databases too - the WolfSSL version
+  does not like ``defo.ie``, whereas the system and OpenSSL ones do. We can ignore
+  that for our purposes via ``--insecure``/``-k`` but would need to fix for a
+  real setup. (Browsers do like those certificates though.)
+
+Then there are some functional code changes:
+
+- tweak to ``configure.ac`` to check if WolfSSL has ECH or not
+- added code to ``lib/vtls/wolfssl.c`` mirroring what's done in the
+  OpenSSL equivalent above.
+- WolfSSL does not support ``--ech false`` or the ``--ech pn:`` command line
+  argument.
+
+The lack of support for ``--ech false`` is because wolfSSL has decided to
+always at least GREASE if built to support ECH. In other words, GREASE is
+a compile time choice for wolfSSL, but a runtime choice for OpenSSL or
+boringssl. (Both are reasonable.)
+
+## Additional notes
+
+### Supporting ECH without DoH
+
+All of the above only applies if DoH is being used. There should be a use-case
+for ECH when DoH is not used by curl - if a system stub resolver supports DoT
+or DoH, then, considering only ECH and the network threat model, it would make
+sense for curl to support ECH without curl itself using DoH. The author for
+example uses a combination of stubby+unbound as the system resolver listening
+on localhost:53, so would fit this use-case. That said, it is unclear if
+this is a niche that is worth trying to address. (The author is just as happy to
+let curl use DoH to talk to the same public recursive that stubby might use:-)
+
+Assuming for the moment this is a use-case we'd like to support, then
+if DoH is not being used by curl, it is not clear at this time how to provide
+support for ECH. One option would seem to be to extend the ``c-ares`` library
+to support HTTPS RRs, but in that case it is not now clear whether such changes
+would be attractive to the ``c-ares`` maintainers, nor whether the "tag=value"
+extensibility inherent in the HTTPS/SVCB specification is a good match for the
+``c-ares`` approach of defining structures specific to decoded answers for each
+supported RRtype. We're also not sure how many downstream curl deployments
+actually make use of the ``c-ares`` library, which would affect the utility of
+such changes. Another option might be to consider using some other generic DNS
+library that does support HTTPS RRs, but it is unclear if such a library could
+or would be used by all or almost all curl builds and downstream releases of
+curl.
+
+Our current conclusion is that doing the above is likely best left until we
+have some experience with the "using DoH" approach, so we're going to punt on
+this for now.
+
+### Debugging
+
+Just a note to self as remembering this is a nuisance:
+
+```bash
+LD_LIBRARY_PATH=$HOME/code/openssl:./lib/.libs gdb ./src/.libs/curl
+```
+
+### Localhost testing
+
+It can be useful to be able to run against a localhost OpenSSL ``s_server``
+for testing. We have published instructions for such 
+[localhost tests](https://github.com/defo-project/ech-dev-utils/blob/main/howtos/localhost-tests.md)
+in another repository. Once you have that set up, you can start a server
+and then run curl against that:
+
+```bash
+    cd $HOME/code/ech-dev-utils
+    ./scripts/echsvr.sh -d
+    ...
+```
+
+The ``echsvr.sh`` script supports many ECH-related options. Use ``echsvr.sh -h``
+for details.
+
+In another window:
+
+```bash
+    cd $HOME/code/curl/
+    ./src/curl -vvv --insecure  --connect-to foo.example.com:8443:localhost:8443  --ech ecl:AD7+DQA6uwAgACBix2B78sX+EQhEbxMspDOc8Z3xVS5aQpYP0Cxpc2AWPAAEAAEAAQALZXhhbXBsZS5jb20AAA==
+```
+
+### Automated use of ``retry_configs`` not supported so far...
+
+As of now we have not added support for using ``retry_config`` handling in the
+application - for a command line tool, one can just use ``dig`` (or ``kdig``)
+to get the HTTPS RR and pass the ECHConfigList from that on the command line,
+if needed, or one can access the value from command line output in verbose more
+and then re-use that in another invocation.
+
+Both our OpenSSL fork and boringssl have APIs for both controlling GREASE and
+accessing and logging ``retry_configs``, it seems WolfSSL has neither.
+
index 4e781ecba70b5baa78c7fb6d3ae06b5e34f23b91..09a0d166ecbd51a05b99d889302833027afe9d64 100644 (file)
@@ -28,3 +28,4 @@ Experimental support in curl means:
  - HTTP/3 support (using the quiche or msh3 backends)
  - The rustls backend
  - WebSocket
+ - Use of the HTTPS resource record and Encrypted Client Hello (ECH) when using DoH
index 428cc3bab2a066540a855e092fbaa174bd8f0b11..deb4c7c326dc1c121d1f4bd8374b2611db00d831 100644 (file)
@@ -90,6 +90,7 @@ DPAGES = \
   doh-insecure.md \
   doh-url.md \
   dump-header.md \
+  ech.md \
   egd-file.md \
   engine.md \
   etag-compare.md \
diff --git a/docs/cmdline-opts/ech.md b/docs/cmdline-opts/ech.md
new file mode 100644 (file)
index 0000000..f8a9a0d
--- /dev/null
@@ -0,0 +1,54 @@
+---
+c: Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
+SPDX-License-Identifier: curl
+Long: ech
+Arg: <config>
+Help: Configure Encrypted Client Hello (ECH) for use with the TLS session
+Added: 8.8.0
+Category: tls ECH
+Protocols: HTTPS
+Multi: single
+See-also:
+  - doh-url
+Example:
+  - --ech true $URL
+---
+
+# `--ech`
+
+Specifies how to do ECH (Encrypted Client Hello).
+
+The values allowed for \<config\> can be:
+
+## "false"
+Do not attempt ECH
+
+## "grease"
+
+Send a GREASE ECH extension
+
+## "true"
+
+Attempt ECH if possible, but do not fail if ECH is not attempted.
+(The connection fails if ECH is attempted but fails.)
+
+## "hard"
+
+Attempt ECH and fail if that is not possible.
+ECH only works with TLS 1.3 and also requires using
+DoH or providing an ECHConfigList on the command line.
+
+## "ecl:<b64val>"
+
+A base64 encoded ECHConfigList that is used for ECH.
+
+## "pn:<name>"
+
+A name to use to over-ride the `public_name` field of an ECHConfigList
+(only available with OpenSSL TLS support)
+
+## Errors
+
+Most errors cause error
+*CURLE_ECH_REQUIRED* (101).
+
index 770fdc91bc7d6969d5f4cb078c825d3f2f1913e2..b80d0b6640efb9cc9971b6cd0c90e7cafe69b3c3 100644 (file)
@@ -1361,6 +1361,12 @@ int main(void)
 }
 ~~~
 
+# ENCRYPTED CLIENT HELLO OPTIONS
+
+## CURLOPT_ECH
+
+Set the configuration for ECH. See CURLOPT_ECH(3)
+
 # AVAILABILITY
 
 Always
index af16b99dea6326eadd233ab77324c0482f091c2f..ef31b2fc93d05f00fc058415fc74a75a36ab506d 100644 (file)
@@ -487,6 +487,10 @@ An internal call to poll() or select() returned error that is not recoverable.
 
 A value or data field grew larger than allowed.
 
+## CURLE_ECH_REQUIRED (101)"
+
+ECH was attempted but failed.
+
 # CURLMcode
 
 This is the generic return code used by functions in the libcurl multi
diff --git a/docs/libcurl/opts/CURLOPT_ECH.md b/docs/libcurl/opts/CURLOPT_ECH.md
new file mode 100644 (file)
index 0000000..d8f768d
--- /dev/null
@@ -0,0 +1,83 @@
+---
+c: Copyright (C) Daniel Stenberg, <daniel.se>, et al.
+SPDX-License-Identifier: curl
+Title: CURLOPT_ECH
+Section: 3
+Source: libcurl
+See-also:
+  -  (3)
+Protocol:
+  - TLS
+TLS-backend:
+  - OpenSSL
+  - wolfSSL
+---
+
+# NAME
+
+CURLOPT_ECH - configuration for Encrypted Client Hello
+
+# SYNOPSIS
+
+~~~c
+#include <curl/curl.h>
+
+CURLcode curl_easy_setopt(CURL *handle, CURLOPT_ECH, char *config);
+~~~
+
+# DESCRIPTION
+
+ECH is only compatible with TLSv1.3.
+
+This experimental feature requires a special build of OpenSSL, as ECH is not
+yet supported in OpenSSL releases. In contrast ECH is supported by the latest
+BoringSSL and wolfSSL releases. See [ECH.md](../../ECH.md) for details of how
+to build such an OpenSSL library.
+
+There is also a known issue with using wolfSSL which does not support ECH
+when the HelloRetryRequest mechanism is used.
+
+Pass a string that specifies configuration details for ECH.
+In all cases, if ECH is attempted, it may fail for various reasons.
+The keywords supported are:
+
+## false
+Turns off ECH.
+## grease
+Instructs client to emit a GREASE ECH extension.
+(The connection fails if ECH is attempted but fails.)
+## true
+Instructs client to attempt ECH, if possible, but to not fail if attempting ECH is not possible.
+## hard
+Instructs client to attempt ECH and fail if if attempting ECH is not possible.
+## ecl:\<base64-value\>
+If the string starts with "ecl:" then the remainder of the string should be a base64-encoded
+ECHConfigList that is used for ECH rather than attempting to download such a value from
+the DNS.
+## pn:\<name\>
+If the string starts with "pn:" then the remainder of the string should be a DNS/hostname
+that is used to over-ride the public_name field of the ECHConfigList that is used
+for ECH.
+
+# DEFAULT
+
+NULL, meaning ECH is disabled.
+
+# EXAMPLE
+
+~~~c
+CURL *curl = curl_easy_init();
+
+const char *config ="ecl:AED+DQA87wAgACB/RuzUCsW3uBbSFI7mzD63TUXpI8sGDTnFTbFCDpa+CAAEAAEAAQANY292ZXIuZGVmby5pZQAA";
+if(curl) {
+  curl_easy_setopt(curl, CURLOPT_ECH, config);
+  curl_easy_perform(curl);
+}
+~~~
+# AVAILABILITY
+
+Added in 8.8.0
+
+# RETURN VALUE
+
+Returns CURLE_OK on success or CURLE_OUT_OF_MEMORY if there was insufficient heap space.
index 7a292b81d5189d3ac03bf0edddb392bc04853a6c..bf5ea7645faac53f65a10e9de226ea8f47e352e0 100644 (file)
@@ -166,6 +166,7 @@ man_MANS =                                      \
   CURLOPT_DOH_SSL_VERIFYPEER.3                  \
   CURLOPT_DOH_SSL_VERIFYSTATUS.3                \
   CURLOPT_DOH_URL.3                             \
+  CURLOPT_ECH.3                                 \
   CURLOPT_EGDSOCKET.3                           \
   CURLOPT_ERRORBUFFER.3                         \
   CURLOPT_EXPECT_100_TIMEOUT_MS.3               \
index 5f810b802848c3ed0ee2eeffe14dc5d8a41c86c5..6b37e52317a47af17f451f7154d1fb2c983e60cc 100644 (file)
@@ -340,6 +340,7 @@ CURLE_URL_MALFORMAT_USER        7.1           7.17.0
 CURLE_USE_SSL_FAILED            7.17.0
 CURLE_WEIRD_SERVER_REPLY        7.51.0
 CURLE_WRITE_ERROR               7.1
+CURLE_ECH_REQUIRED              8.8.0
 CURLFILETYPE_DEVICE_BLOCK       7.21.0
 CURLFILETYPE_DEVICE_CHAR        7.21.0
 CURLFILETYPE_DIRECTORY          7.21.0
@@ -617,6 +618,7 @@ CURLOPT_DOH_SSL_VERIFYHOST      7.76.0
 CURLOPT_DOH_SSL_VERIFYPEER      7.76.0
 CURLOPT_DOH_SSL_VERIFYSTATUS    7.76.0
 CURLOPT_DOH_URL                 7.62.0
+CURLOPT_ECH                     8.8.0
 CURLOPT_EGDSOCKET               7.7           7.84.0
 CURLOPT_ENCODING                7.10          7.21.6
 CURLOPT_ERRORBUFFER             7.1
index 0905809439c35bee039ab6215f697a0a85ee6ec4..d3513ff119d7f6497e597306c22a4d2773c8f237 100644 (file)
@@ -55,6 +55,7 @@
 --doh-insecure                       7.76.0
 --doh-url                            7.62.0
 --dump-header (-D)                   5.7
+--ech                                8.8.0
 --egd-file                           7.7
 --engine                             7.9.3
 --etag-compare                       7.68.0
index bb9a423baf87c244f2022f76c69af421e36fd0a5..25b3836b4e5275b4ea14fc2e7d3db4eeb7dcd5dd 100644 (file)
@@ -632,6 +632,7 @@ typedef enum {
   CURLE_SSL_CLIENTCERT,          /* 98 - client-side certificate required */
   CURLE_UNRECOVERABLE_POLL,      /* 99 - poll/select returned fatal error */
   CURLE_TOO_LARGE,               /* 100 - a value/data met its maximum */
+  CURLE_ECH_REQUIRED,            /* 101 - ECH tried but failed */
   CURL_LAST /* never use! */
 } CURLcode;
 
@@ -2209,6 +2210,9 @@ typedef enum {
   /* millisecond version */
   CURLOPT(CURLOPT_SERVER_RESPONSE_TIMEOUT_MS, CURLOPTTYPE_LONG, 324),
 
+  /* set ECH configuration  */
+  CURLOPT(CURLOPT_ECH, CURLOPTTYPE_STRINGPOINT, 325),
+
   CURLOPT_LASTENTRY /* the last unused */
 } CURLoption;
 
@@ -3161,7 +3165,7 @@ typedef struct curl_version_info_data curl_version_info_data;
 #define CURL_VERSION_GSASL        (1<<29) /* libgsasl is supported */
 #define CURL_VERSION_THREADSAFE   (1<<30) /* libcurl API is thread-safe */
 
- /*
+/*
  * NAME curl_version_info()
  *
  * DESCRIPTION
index b880f3dc60552ee53a259c84001b9dca0ce9bc5a..873a49e02098407828c910130b78172b1de5cef3 100644 (file)
@@ -275,6 +275,7 @@ CURLWARNING(_curl_easy_getinfo_err_curl_off_t,
    (option) == CURLOPT_DNS_LOCAL_IP6 ||                                       \
    (option) == CURLOPT_DNS_SERVERS ||                                         \
    (option) == CURLOPT_DOH_URL ||                                             \
+   (option) == CURLOPT_ECH        ||                                          \
    (option) == CURLOPT_EGDSOCKET ||                                           \
    (option) == CURLOPT_FTP_ACCOUNT ||                                         \
    (option) == CURLOPT_FTP_ALTERNATIVE_TO_USER ||                             \
index f3904d2675c1f82f3a02e75dd3706cf2fdf5b7cc..11b0cb54fdcafef175c1985dadd100097b8c2d5a 100644 (file)
@@ -805,3 +805,9 @@ ${SIZEOF_TIME_T_CODE}
 
 /* Define to 1 to enable TLS-SRP support. */
 #cmakedefine USE_TLS_SRP 1
+
+/* Define to 1 to query for HTTPSRR when using DoH */
+#cmakedefine USE_HTTPSRR 1
+
+/* if ECH support is available */
+#cmakedefine USE_ECH 1
index 363c1fb6f6c86ac6f8e6d0774e4b11e64caaab25..f3c69ca4bb84903fc54918946c19c24436e89132 100644 (file)
--- a/lib/doh.c
+++ b/lib/doh.c
 #include "curl_printf.h"
 #include "curl_memory.h"
 #include "memdebug.h"
+#include "escape.h"
 
 #define DNS_CLASS_IN 0x01
 
+/* local_print_buf truncates if the hex string will be more than this */
+#define LOCAL_PB_HEXMAX 400
+
 #ifndef CURL_DISABLE_VERBOSE_STRINGS
 static const char * const errors[]={
   "",
@@ -187,6 +191,26 @@ doh_write_cb(const void *contents, size_t size, size_t nmemb, void *userp)
   return realsize;
 }
 
+#if defined(USE_HTTPSRR) && defined(CURLDEBUG)
+static void local_print_buf(struct Curl_easy *data,
+                            const char *prefix,
+                            unsigned char *buf, size_t len)
+{
+  unsigned char hexstr[LOCAL_PB_HEXMAX];
+  size_t hlen = LOCAL_PB_HEXMAX;
+  bool truncated = false;
+
+  if(len > (LOCAL_PB_HEXMAX / 2))
+    truncated = true;
+  Curl_hexencode(buf, len, hexstr, hlen);
+  if(!truncated)
+    infof(data, "%s: len=%d, val=%s", prefix, (int)len, hexstr);
+  else
+    infof(data, "%s: len=%d (truncated)val=%s", prefix, (int)len, hexstr);
+  return;
+}
+#endif
+
 /* called from multi.c when this DoH transfer is complete */
 static int doh_done(struct Curl_easy *doh, CURLcode result)
 {
@@ -379,6 +403,12 @@ struct Curl_addrinfo *Curl_doh(struct Curl_easy *data,
   int slot;
   struct dohdata *dohp;
   struct connectdata *conn = data->conn;
+#ifdef USE_HTTPSRR
+  /* for now, this is only used when ECH is enabled */
+# ifdef USE_ECH
+  char *qname = NULL;
+# endif
+#endif
   *waitp = FALSE;
   (void)hostname;
   (void)port;
@@ -418,6 +448,37 @@ struct Curl_addrinfo *Curl_doh(struct Curl_easy *data,
       goto error;
     dohp->pending++;
   }
+#endif
+
+#ifdef USE_HTTPSRR
+  /*
+   * TODO: Figure out the conditions under which we want to make
+   * a request for an HTTPS RR when we are not doing ECH. For now,
+   * making this request breaks a bunch of DoH tests, e.g. test2100,
+   * where the addiitonal request doesn't match the pre-cooked data
+   * files, so there's a bit of work attached to making the request
+   * in a non-ECH use-case. For the present, we'll only make the
+   * request when ECH is enabled in the build and is being used for
+   * the curl operation.
+   */
+# ifdef USE_ECH
+  if(data->set.tls_ech & CURLECH_ENABLE
+     || data->set.tls_ech & CURLECH_HARD) {
+    if(port == 443)
+      qname = strdup(hostname);
+    else
+      qname = aprintf("_%d._https.%s", port, hostname);
+    if(!qname)
+      goto error;
+    result = dohprobe(data, &dohp->probe[DOH_PROBE_SLOT_HTTPS],
+                      DNS_TYPE_HTTPS, qname, data->set.str[STRING_DOH],
+                      data->multi, dohp->headers);
+    free(qname);
+    if(result)
+      goto error;
+    dohp->pending++;
+  }
+# endif
 #endif
   *waitp = TRUE; /* this never returns synchronously */
   return NULL;
@@ -501,6 +562,25 @@ static DOHcode store_aaaa(const unsigned char *doh,
   return DOH_OK;
 }
 
+#ifdef USE_HTTPSRR
+static DOHcode store_https(const unsigned char *doh,
+                           int index,
+                           struct dohentry *d,
+                           uint16_t len)
+{
+  /* silently ignore RRs over the limit */
+  if(d->numhttps_rrs < DOH_MAX_HTTPS) {
+    struct dohhttps_rr *h = &d->https_rrs[d->numhttps_rrs];
+    h->val = Curl_memdup(&doh[index], len);
+    if(!h->val)
+      return DOH_OUT_OF_MEM;
+    h->len = len;
+    d->numhttps_rrs++;
+  }
+  return DOH_OK;
+}
+#endif
+
 static DOHcode store_cname(const unsigned char *doh,
                            size_t dohlen,
                            unsigned int index,
@@ -563,7 +643,8 @@ static DOHcode rdata(const unsigned char *doh,
   /* RDATA
      - A (TYPE 1):  4 bytes
      - AAAA (TYPE 28): 16 bytes
-     - NS (TYPE 2): N bytes */
+     - NS (TYPE 2): N bytes
+     - HTTPS (TYPE 65): N bytes */
   DOHcode rc;
 
   switch(type) {
@@ -581,6 +662,13 @@ static DOHcode rdata(const unsigned char *doh,
     if(rc)
       return rc;
     break;
+#ifdef USE_HTTPSRR
+  case DNS_TYPE_HTTPS:
+    rc = store_https(doh, index, d, rdlength);
+    if(rc)
+      return rc;
+    break;
+#endif
   case DNS_TYPE_CNAME:
     rc = store_cname(doh, dohlen, index, d);
     if(rc)
@@ -737,7 +825,11 @@ UNITTEST DOHcode doh_decode(const unsigned char *doh,
   if(index != dohlen)
     return DOH_DNS_MALFORMAT; /* something is wrong */
 
+#ifdef USE_HTTTPS
+  if((type != DNS_TYPE_NS) && !d->numcname && !d->numaddr && !d->numhttps_rrs)
+#else
   if((type != DNS_TYPE_NS) && !d->numcname && !d->numaddr)
+#endif
     /* nothing stored! */
     return DOH_NO_CONTENT;
 
@@ -776,6 +868,16 @@ static void showdoh(struct Curl_easy *data,
       infof(data, "%s", buffer);
     }
   }
+#ifdef USE_HTTPSRR
+  for(i = 0; i < d->numhttps_rrs; i++) {
+# ifdef CURLDEBUG
+    local_print_buf(data, "DoH HTTPS",
+                    d->https_rrs[i].val, d->https_rrs[i].len);
+# else
+    infof(data, "DoH HTTPS RR: length %d", d->https_rrs[i].len);
+# endif
+  }
+#endif
   for(i = 0; i < d->numcname; i++) {
     infof(data, "CNAME: %s", Curl_dyn_ptr(&d->cname[i]));
   }
@@ -895,7 +997,18 @@ static CURLcode doh2ai(const struct dohentry *de, const char *hostname,
 #ifndef CURL_DISABLE_VERBOSE_STRINGS
 static const char *type2name(DNStype dnstype)
 {
-  return (dnstype == DNS_TYPE_A)?"A":"AAAA";
+  switch(dnstype) {
+    case DNS_TYPE_A:
+      return "A";
+    case DNS_TYPE_AAAA:
+      return "AAAA";
+#ifdef USE_HTTPSRR
+    case DNS_TYPE_HTTPS:
+      return "HTTPS";
+#endif
+    default:
+       return "unknown";
+  }
 }
 #endif
 
@@ -905,8 +1018,282 @@ UNITTEST void de_cleanup(struct dohentry *d)
   for(i = 0; i < d->numcname; i++) {
     Curl_dyn_free(&d->cname[i]);
   }
+#ifdef USE_HTTPSRR
+  for(i = 0; i < d->numhttps_rrs; i++)
+    free(d->https_rrs[i].val);
+#endif
 }
 
+#ifdef USE_HTTPSRR
+
+/*
+ * @brief decode the DNS name in a binary RRData
+ * @param buf points to the buffer (in/out)
+ * @param remaining points to the remaining buffer length (in/out)
+ * @param dnsname returns the string form name on success
+ * @return is 1 for success, error otherwise
+ *
+ * The encoding here is defined in
+ * https://tools.ietf.org/html/rfc1035#section-3.1
+ *
+ * The input buffer pointer will be modified so it points to
+ * just after the end of the DNS name encoding on output. (And
+ * that's why it's an "unsigned char **" :-)
+ */
+static CURLcode local_decode_rdata_name(unsigned char **buf, size_t *remaining,
+                                        char **dnsname)
+{
+  unsigned char *cp = NULL;
+  int rem = 0;
+  char *thename = NULL, *tp = NULL;
+  unsigned char clen = 0; /* chunk len */
+
+  if(!buf || !remaining || !dnsname)
+    return CURLE_OUT_OF_MEMORY;
+  rem = (int)*remaining;
+  thename = calloc(1, CURL_MAXLEN_host_name);
+  if(!thename)
+    return CURLE_OUT_OF_MEMORY;
+  cp = *buf;
+  tp = thename;
+  clen = *cp++;
+  if(clen == 0) {
+    /* special case - return "." as name */
+    thename[0] = '.';
+    thename[1] = 0x00;
+  }
+  while(clen) {
+    if(clen >= rem) {
+      free(thename);
+      return CURLE_OUT_OF_MEMORY;
+    }
+    if(((tp - thename) + clen) > CURL_MAXLEN_host_name) {
+      free(thename);
+      return CURLE_OUT_OF_MEMORY;
+    }
+    memcpy(tp, cp, clen);
+    tp += clen;
+    *tp++ = '.';
+    cp += clen;
+    rem -= (clen + 1);
+    if(rem <= 0) {
+      free(thename);
+      return CURLE_OUT_OF_MEMORY;
+    }
+    clen = *cp++;
+  }
+  *buf = cp;
+  if(rem <= 0) {
+    free(thename);
+    return CURLE_OUT_OF_MEMORY;
+  }
+  *remaining = rem - 1;
+  *dnsname = thename;
+  return CURLE_OK;
+}
+
+static CURLcode local_decode_rdata_alpn(unsigned char *rrval, size_t len,
+                                        char **alpns)
+{
+  /*
+   * spec here is as per draft-ietf-dnsop-svcb-https, section-7.1.1
+   * encoding is catenated list of strings each preceded by a one
+   * octet length
+   * output is comma-sep list of the strings
+   * implementations may or may not handle quoting of comma within
+   * string values, so we might see a comma within the wire format
+   * version of a string, in which case we'll precede that by a
+   * backslash - same goes for a backslash character, and of course
+   * we need to use two backslashes in strings when we mean one;-)
+   */
+  int remaining = (int) len;
+  char *oval;
+  size_t olen = 0, i;
+  unsigned char *cp = rrval;
+  struct dynbuf dval;
+
+  if(!alpns)
+    return CURLE_OUT_OF_MEMORY;
+  Curl_dyn_init(&dval, DYN_DOH_RESPONSE);
+  remaining = (int)len;
+  cp = rrval;
+  while(remaining > 0) {
+    size_t tlen = (size_t) *cp++;
+
+    /* if not 1st time, add comma */
+    if(remaining != (int)len && Curl_dyn_addn(&dval, ",", 1))
+      goto err;
+    remaining--;
+    if(tlen > (size_t)remaining)
+      goto err;
+    /* add escape char if needed, clunky but easier to read */
+    for(i = 0; i != tlen; i++) {
+      if('\\' == *cp || ',' == *cp) {
+        if(Curl_dyn_addn(&dval, "\\", 1))
+          goto err;
+      }
+      if(Curl_dyn_addn(&dval, cp++, 1))
+        goto err;
+    }
+    remaining -= (int)tlen;
+  }
+  olen = Curl_dyn_len(&dval);
+  /* I think the + 1 here is ok but it could trigger a read error */
+  oval = (char *)Curl_memdup(Curl_dyn_ptr(&dval), olen + 1);
+  if(!oval)
+    goto err;
+  Curl_dyn_free(&dval);
+  oval[olen]='\0';
+  *alpns = oval;
+  return CURLE_OK;
+err:
+  Curl_dyn_free(&dval);
+  return CURLE_BAD_CONTENT_ENCODING;
+}
+
+#ifdef CURLDEBUG
+static CURLcode test_alpn_escapes(void)
+{
+  /* we'll use an example from draft-ietf-dnsop-svcb, figure 10 */
+  static unsigned char example[] = {
+    0x08,                                           /* length 8 */
+    0x66, 0x5c, 0x6f, 0x6f, 0x2c, 0x62, 0x61, 0x72, /* value "f\\oo,bar" */
+    0x02,                                           /* length 2 */
+    0x68, 0x32                                      /* value "h2" */
+  };
+  size_t example_len = sizeof(example);
+  char *aval = NULL;
+  static const char *expected = "f\\\\oo\\,bar,h2";
+
+  if(local_decode_rdata_alpn(example, example_len, &aval) != CURLE_OK)
+    return CURLE_BAD_CONTENT_ENCODING;
+  if(strlen(aval) != strlen(expected))
+    return CURLE_BAD_CONTENT_ENCODING;
+  if(memcmp(aval, expected, strlen(aval)))
+    return CURLE_BAD_CONTENT_ENCODING;
+  return CURLE_OK;
+}
+#endif
+
+static CURLcode Curl_doh_decode_httpsrr(unsigned char *rrval, size_t len,
+                                        struct Curl_https_rrinfo **hrr)
+{
+  size_t remaining = len;
+  unsigned char *cp = rrval;
+  uint16_t pcode = 0, plen = 0;
+  struct Curl_https_rrinfo *lhrr = NULL;
+  char *dnsname = NULL;
+
+#ifdef CURLDEBUG
+  /* a few tests of escaping, shouldn't be here but ok for now */
+  if(test_alpn_escapes() != CURLE_OK)
+    return CURLE_OUT_OF_MEMORY;
+#endif
+  lhrr = calloc(1, sizeof(struct Curl_https_rrinfo));
+  if(!lhrr)
+    return CURLE_OUT_OF_MEMORY;
+  lhrr->val = calloc(1, len);
+  if(!lhrr->val)
+    goto err;
+  lhrr->len = len;
+  memcpy(lhrr->val, rrval, len);
+  if(remaining <= 2)
+    goto err;
+  lhrr->priority = (uint16_t)((cp[0] << 8) + cp[1]);
+  cp += 2;
+  remaining -= (uint16_t)2;
+  if(local_decode_rdata_name(&cp, &remaining, &dnsname) != CURLE_OK)
+    goto err;
+  lhrr->target = dnsname;
+  while(remaining >= 4) {
+    pcode = (uint16_t)((*cp << 8) + (*(cp + 1)));
+    cp += 2;
+    plen = (uint16_t)((*cp << 8) + (*(cp + 1)));
+    cp += 2;
+    remaining -= 4;
+    if(pcode == HTTPS_RR_CODE_ALPN) {
+      if(local_decode_rdata_alpn(cp, plen, &lhrr->alpns) != CURLE_OK)
+        goto err;
+    }
+    if(pcode == HTTPS_RR_CODE_NO_DEF_ALPN)
+      lhrr->no_def_alpn = TRUE;
+    else if(pcode == HTTPS_RR_CODE_IPV4) {
+      lhrr->ipv4hints = Curl_memdup(cp, plen);
+      if(!lhrr->ipv4hints)
+        goto err;
+      lhrr->ipv4hints_len = (size_t)plen;
+    }
+    else if(pcode == HTTPS_RR_CODE_ECH) {
+      lhrr->echconfiglist = Curl_memdup(cp, plen);
+      if(!lhrr->echconfiglist)
+        goto err;
+      lhrr->echconfiglist_len = (size_t)plen;
+    }
+    else if(pcode == HTTPS_RR_CODE_IPV6) {
+      lhrr->ipv6hints = Curl_memdup(cp, plen);
+      if(!lhrr->ipv6hints)
+        goto err;
+      lhrr->ipv6hints_len = (size_t)plen;
+    }
+    if(plen > 0 && plen <= remaining) {
+      cp += plen;
+      remaining -= plen;
+    }
+  }
+  DEBUGASSERT(!remaining);
+  *hrr = lhrr;
+  return CURLE_OK;
+err:
+  if(lhrr) {
+    if(lhrr->target)
+      free(lhrr->target);
+    if(lhrr->echconfiglist)
+      free(lhrr->echconfiglist);
+    if(lhrr->val)
+      free(lhrr->val);
+    free(lhrr);
+  }
+  return CURLE_OUT_OF_MEMORY;
+}
+
+# ifdef CURLDEBUG
+static void local_print_httpsrr(struct Curl_easy *data,
+                                struct Curl_https_rrinfo *hrr)
+{
+  DEBUGASSERT(hrr);
+  infof(data, "HTTPS RR: priority %d, target: %s",
+        hrr->priority, hrr->target);
+  if(hrr->alpns)
+    infof(data, "HTTPS RR: alpns %s", hrr->alpns);
+  else
+    infof(data, "HTTPS RR: no alpns");
+  if(hrr->no_def_alpn)
+    infof(data, "HTTPS RR: no_def_alpn set");
+  else
+    infof(data, "HTTPS RR: no_def_alpn not set");
+  if(hrr->ipv4hints) {
+    local_print_buf(data, "HTTPS RR: ipv4hints",
+                    hrr->ipv4hints, hrr->ipv4hints_len);
+  }
+  else
+    infof(data, "HTTPS RR: no ipv4hints");
+  if(hrr->echconfiglist) {
+    local_print_buf(data, "HTTPS RR: ECHConfigList",
+                    hrr->echconfiglist, hrr->echconfiglist_len);
+  }
+  else
+    infof(data, "HTTPS RR: no ECHConfigList");
+  if(hrr->ipv6hints) {
+    local_print_buf(data, "HTTPS RR: ipv6hint",
+                    hrr->ipv6hints, hrr->ipv6hints_len);
+  }
+  else
+    infof(data, "HTTPS RR: no ipv6hints");
+  return;
+}
+# endif
+#endif
+
 CURLcode Curl_doh_is_resolved(struct Curl_easy *data,
                               struct Curl_dns_entry **dnsp)
 {
@@ -923,9 +1310,15 @@ CURLcode Curl_doh_is_resolved(struct Curl_easy *data,
       CURLE_COULDNT_RESOLVE_HOST;
   }
   else if(!dohp->pending) {
+#ifndef USE_HTTPSRR
     DOHcode rc[DOH_PROBE_SLOTS] = {
       DOH_OK, DOH_OK
     };
+#else
+    DOHcode rc[DOH_PROBE_SLOTS] = {
+      DOH_OK, DOH_OK, DOH_OK
+    };
+#endif
     struct dohentry de;
     int slot;
     /* remove DoH handles from multi handle and close them */
@@ -991,6 +1384,22 @@ CURLcode Curl_doh_is_resolved(struct Curl_easy *data,
     } /* address processing done */
 
     /* Now process any build-specific attributes retrieved from DNS */
+#ifdef USE_HTTPSRR
+    if(de.numhttps_rrs > 0 && result == CURLE_OK && *dnsp) {
+      struct Curl_https_rrinfo *hrr = NULL;
+      result = Curl_doh_decode_httpsrr(de.https_rrs->val, de.https_rrs->len,
+                                       &hrr);
+      if(result) {
+        infof(data, "Failed to decode HTTPS RR");
+        return result;
+      }
+      infof(data, "Some HTTPS RR to process");
+# ifdef CURLDEBUG
+      local_print_httpsrr(data, hrr);
+# endif
+      (*dnsp)->hinfo = hrr;
+    }
+#endif
 
     /* All done */
     de_cleanup(&de);
index ffcf7a033563aa3b96dd6f5ee134c3fa954aebe0..9c6e0767c28837c6df004799d75c6663dcdab61a 100644 (file)
--- a/lib/doh.h
+++ b/lib/doh.h
@@ -26,6 +26,9 @@
 
 #include "urldata.h"
 #include "curl_addrinfo.h"
+#ifdef USE_HTTPSRR
+# include <stdint.h>
+#endif
 
 #ifndef CURL_DISABLE_DOH
 
@@ -51,7 +54,8 @@ typedef enum {
   DNS_TYPE_NS = 2,
   DNS_TYPE_CNAME = 5,
   DNS_TYPE_AAAA = 28,
-  DNS_TYPE_DNAME = 39           /* RFC6672 */
+  DNS_TYPE_DNAME = 39,           /* RFC6672 */
+  DNS_TYPE_HTTPS = 65
 } DNStype;
 
 /* one of these for each DoH request */
@@ -88,6 +92,7 @@ int Curl_doh_getsock(struct connectdata *conn, curl_socket_t *socks);
 
 #define DOH_MAX_ADDR 24
 #define DOH_MAX_CNAME 4
+#define DOH_MAX_HTTPS 4
 
 struct dohaddr {
   int type;
@@ -97,12 +102,44 @@ struct dohaddr {
   } ip;
 };
 
+#ifdef USE_HTTPSRR
+
+/*
+ * These are the code points for DNS wire format SvcParams as
+ * per draft-ietf-dnsop-svcb-https
+ * Not all are supported now, and even those that are may need
+ * more work in future to fully support the spec.
+ */
+#define HTTPS_RR_CODE_ALPN            0x01
+#define HTTPS_RR_CODE_NO_DEF_ALPN     0x02
+#define HTTPS_RR_CODE_PORT            0x03
+#define HTTPS_RR_CODE_IPV4            0x04
+#define HTTPS_RR_CODE_ECH             0x05
+#define HTTPS_RR_CODE_IPV6            0x06
+
+/*
+ * These may need escaping when found within an alpn string
+ * value.
+ */
+#define COMMA_CHAR                    ','
+#define BACKSLASH_CHAR                '\\'
+
+struct dohhttps_rr {
+  uint16_t len; /* raw encoded length */
+  unsigned char *val; /* raw encoded octets */
+};
+#endif
+
 struct dohentry {
   struct dynbuf cname[DOH_MAX_CNAME];
   struct dohaddr addr[DOH_MAX_ADDR];
   int numaddr;
   unsigned int ttl;
   int numcname;
+#ifdef USE_HTTPSRR
+  struct dohhttps_rr https_rrs[DOH_MAX_HTTPS];
+  int numhttps_rrs;
+#endif
 };
 
 
index 9c4438a100e2bc5c8da9bc07aa2d15ed248fa0c8..c79d136707e4d41c23f0b90a094bdba450bdcef8 100644 (file)
@@ -86,6 +86,7 @@ struct curl_easyoption Curl_easyopts[] = {
   {"DOH_SSL_VERIFYPEER", CURLOPT_DOH_SSL_VERIFYPEER, CURLOT_LONG, 0},
   {"DOH_SSL_VERIFYSTATUS", CURLOPT_DOH_SSL_VERIFYSTATUS, CURLOT_LONG, 0},
   {"DOH_URL", CURLOPT_DOH_URL, CURLOT_STRING, 0},
+  {"ECH", CURLOPT_ECH, CURLOT_STRING, 0},
   {"EGDSOCKET", CURLOPT_EGDSOCKET, CURLOT_STRING, 0},
   {"ENCODING", CURLOPT_ACCEPT_ENCODING, CURLOT_STRING, CURLOT_FLAG_ALIAS},
   {"ERRORBUFFER", CURLOPT_ERRORBUFFER, CURLOT_OBJECT, 0},
@@ -375,6 +376,6 @@ struct curl_easyoption Curl_easyopts[] = {
  */
 int Curl_easyopts_check(void)
 {
-  return ((CURLOPT_LASTENTRY%10000) != (324 + 1));
+  return ((CURLOPT_LASTENTRY%10000) != (325 + 1));
 }
 #endif
index 8f13a7c73ebcc542f6720aa7888a90b2b8316fa3..801de5ce43bc1168dd13a3a34e574f9c0cd1478e 100644 (file)
@@ -1070,6 +1070,23 @@ static void freednsentry(void *freethis)
   dns->inuse--;
   if(dns->inuse == 0) {
     Curl_freeaddrinfo(dns->addr);
+#ifdef USE_HTTPSRR
+    if(dns->hinfo) {
+      if(dns->hinfo->target)
+        free(dns->hinfo->target);
+      if(dns->hinfo->alpns)
+        free(dns->hinfo->alpns);
+      if(dns->hinfo->ipv4hints)
+        free(dns->hinfo->ipv4hints);
+      if(dns->hinfo->echconfiglist)
+        free(dns->hinfo->echconfiglist);
+      if(dns->hinfo->ipv6hints)
+        free(dns->hinfo->ipv6hints);
+      if(dns->hinfo->val)
+        free(dns->hinfo->val);
+      free(dns->hinfo);
+    }
+#endif
     free(dns);
   }
 }
index eee86bdf85d524cb953961ebbf6651230d07eee1..3ccf4354fe2632db7ee93d63d17c08cf38530c82 100644 (file)
 
 #include <setjmp.h>
 
+#ifdef USE_HTTPSRR
+# include <stdint.h>
+#endif
+
 /* Allocate enough memory to hold the full name information structs and
  * everything. OSF1 is known to require at least 8872 bytes. The buffer
  * required for storing all possible aliases and IP numbers is according to
@@ -58,8 +62,41 @@ struct connectdata;
  */
 struct Curl_hash *Curl_global_host_cache_init(void);
 
+#ifdef USE_HTTPSRR
+
+#define CURL_MAXLEN_host_name 253
+
+struct Curl_https_rrinfo {
+  size_t len; /* raw encoded length */
+  unsigned char *val; /* raw encoded octets */
+  /*
+   * fields from HTTPS RR, with the mandatory fields
+   * first (priority, target), then the others in the
+   * order of the keytag numbers defined at
+   * https://datatracker.ietf.org/doc/html/rfc9460#section-14.3.2
+   */
+  uint16_t priority;
+  char *target;
+  char *alpns; /* keytag = 1 */
+  bool no_def_alpn; /* keytag = 2 */
+  /*
+   * we don't support ports (keytag = 3) as we don't support
+   * port-switching yet
+   */
+  unsigned char *ipv4hints; /* keytag = 4 */
+  size_t ipv4hints_len;
+  unsigned char *echconfiglist; /* keytag = 5 */
+  size_t echconfiglist_len;
+  unsigned char *ipv6hints; /* keytag = 6 */
+  size_t ipv6hints_len;
+};
+#endif
+
 struct Curl_dns_entry {
   struct Curl_addrinfo *addr;
+#ifdef USE_HTTPSRR
+  struct Curl_https_rrinfo *hinfo;
+#endif
   /* timestamp == 0 -- permanent CURLOPT_RESOLVE entry (doesn't time out) */
   time_t timestamp;
   /* use-counter, use Curl_resolv_unlock to release reference */
index 1ca10401ab51afd7e01cc06b010ac5e5f55b84ac..f719cc43dde29736f1ddcc43c5cd69b1b5cdab31 100644 (file)
@@ -3141,6 +3141,49 @@ CURLcode Curl_vsetopt(struct Curl_easy *data, CURLoption option, va_list param)
     data->set.ws_raw_mode = raw;
     break;
   }
+#endif
+#ifdef USE_ECH
+  case CURLOPT_ECH: {
+    size_t plen = 0;
+
+    argptr = va_arg(param, char *);
+    if(!argptr) {
+      data->set.tls_ech = CURLECH_DISABLE;
+      result = CURLE_BAD_FUNCTION_ARGUMENT;
+      return result;
+    }
+    plen = strlen(argptr);
+    if(plen > CURL_MAX_INPUT_LENGTH) {
+      data->set.tls_ech = CURLECH_DISABLE;
+      result = CURLE_BAD_FUNCTION_ARGUMENT;
+      return result;
+    }
+    /* set tls_ech flag value, preserving CLA_CFG bit */
+    if(plen == 5 && !strcmp(argptr, "false"))
+      data->set.tls_ech = CURLECH_DISABLE
+                          | (data->set.tls_ech & CURLECH_CLA_CFG);
+    else if(plen == 6 && !strcmp(argptr, "grease"))
+      data->set.tls_ech = CURLECH_GREASE
+                          | (data->set.tls_ech & CURLECH_CLA_CFG);
+    else if(plen == 4 && !strcmp(argptr, "true"))
+      data->set.tls_ech = CURLECH_ENABLE
+                          | (data->set.tls_ech & CURLECH_CLA_CFG);
+    else if(plen == 4 && !strcmp(argptr, "hard"))
+      data->set.tls_ech = CURLECH_HARD
+                          | (data->set.tls_ech & CURLECH_CLA_CFG);
+    else if(plen > 5 && !strncmp(argptr, "ecl:", 4)) {
+      result = Curl_setstropt(&data->set.str[STRING_ECH_CONFIG], argptr + 4);
+      if(result)
+        return result;
+      data->set.tls_ech |= CURLECH_CLA_CFG;
+    }
+    else if(plen > 4 && !strncmp(argptr, "pn:", 3)) {
+      result = Curl_setstropt(&data->set.str[STRING_ECH_PUBLIC], argptr + 3);
+      if(result)
+        return result;
+    }
+    break;
+  }
 #endif
   case CURLOPT_QUICK_EXIT:
     data->set.quick_exit = (0 != va_arg(param, long)) ? 1L:0L;
index a900e78d151d462dbbd2324a0a36065c9df02fd3..f142cf181df0593caf36c3b066ca0515906f60ca 100644 (file)
@@ -322,6 +322,9 @@ curl_easy_strerror(CURLcode error)
   case CURLE_TOO_LARGE:
     return "A value or data field grew larger than allowed";
 
+  case CURLE_ECH_REQUIRED:
+    return "ECH attempted but failed";
+
     /* error codes not used by current libcurl */
   case CURLE_OBSOLETE20:
   case CURLE_OBSOLETE24:
index aa3f44fc2f0720c7aff54842523fd6c3f9df6450..8bccffb0f59b2077056d60bcb6380786a1b65786 100644 (file)
 
 struct curl_trc_featt;
 
+#ifdef USE_ECH
+/* CURLECH_ bits for the tls_ech option */
+# define CURLECH_DISABLE    (1<<0)
+# define CURLECH_GREASE     (1<<1)
+# define CURLECH_ENABLE     (1<<2)
+# define CURLECH_HARD       (1<<3)
+# define CURLECH_CLA_CFG    (1<<4)
+#endif
+
 #ifdef USE_WEBSOCKETS
 /* CURLPROTO_GOPHERS (29) is the highest publicly used protocol bit number,
  * the rest are internal information. If we use higher bits we only do this on
@@ -627,6 +636,9 @@ enum doh_slots {
   DOH_PROBE_SLOT_IPADDR_V6 = 1, /* 'V6' likewise */
 
   /* Space here for (possibly build-specific) additional slot definitions */
+#ifdef USE_HTTPSRR
+  DOH_PROBE_SLOT_HTTPS = 2,     /* for HTTPS RR */
+#endif
 
   /* for example */
   /* #ifdef WANT_DOH_FOOBAR_TXT */
@@ -1532,6 +1544,8 @@ enum dupstring {
 #ifndef CURL_DISABLE_PROXY
   STRING_HAPROXY_CLIENT_IP,     /* CURLOPT_HAPROXY_CLIENT_IP */
 #endif
+  STRING_ECH_CONFIG,            /* CURLOPT_ECH_CONFIG */
+  STRING_ECH_PUBLIC,            /* CURLOPT_ECH_PUBLIC */
 
   /* -- end of null-terminated strings -- */
 
@@ -1859,6 +1873,9 @@ struct UserDefined {
 #ifdef USE_WEBSOCKETS
   BIT(ws_raw_mode);
 #endif
+#ifdef USE_ECH
+  int tls_ech;      /* TLS ECH configuration  */
+#endif
 };
 
 #ifndef CURL_DISABLE_MIME
index eada3c398f4ed32b0f71a65a7bce975a3491fe9d..7839ab4ab60d29e3067b6eeeaf0b231984a35462 100644 (file)
 #include <openssl/tls1.h>
 #include <openssl/evp.h>
 
+#ifdef USE_ECH
+# ifndef OPENSSL_IS_BORINGSSL
+#  include <openssl/ech.h>
+# endif
+# include "curl_base64.h"
+# define ECH_ENABLED(__data__) \
+    (__data__->set.tls_ech && \
+     !(__data__->set.tls_ech & CURLECH_DISABLE)\
+    )
+#endif /* USE_ECH */
+
 #if (OPENSSL_VERSION_NUMBER >= 0x0090808fL) && !defined(OPENSSL_NO_OCSP)
 #include <openssl/ocsp.h>
 #endif
@@ -3508,6 +3519,9 @@ CURLcode Curl_ossl_ctx_init(struct ossl_ctx *octx,
   const char * const ssl_cert_type = ssl_config->cert_type;
   const bool verifypeer = conn_config->verifypeer;
   char error_buffer[256];
+#ifdef USE_ECH
+  struct ssl_connect_data *connssl = cf->ctx;
+#endif
 
   /* Make funny stuff to get random input */
   result = ossl_seed(data);
@@ -3843,6 +3857,135 @@ CURLcode Curl_ossl_ctx_init(struct ossl_ctx *octx,
       return CURLE_SSL_CONNECT_ERROR;
     }
   }
+
+#ifdef USE_ECH
+  if(ECH_ENABLED(data)) {
+    unsigned char *ech_config = NULL;
+    size_t ech_config_len = 0;
+    char *outername = data->set.str[STRING_ECH_PUBLIC];
+    int trying_ech_now = 0;
+
+    if(data->set.tls_ech & CURLECH_GREASE) {
+      infof(data, "ECH: will GREASE ClientHello");
+# ifdef OPENSSL_IS_BORINGSSL
+      SSL_set_enable_ech_grease(octx->ssl, 1);
+# else
+      SSL_set_options(octx->ssl, SSL_OP_ECH_GREASE);
+# endif
+    }
+    else if(data->set.tls_ech & CURLECH_CLA_CFG) {
+# ifdef OPENSSL_IS_BORINGSSL
+      /* have to do base64 decode here for boring */
+      const char *b64 = data->set.str[STRING_ECH_CONFIG];
+
+      if(!b64) {
+        infof(data, "ECH: ECHConfig from command line empty");
+        return CURLE_SSL_CONNECT_ERROR;
+      }
+      ech_config_len = 2 * strlen(b64);
+      result = Curl_base64_decode(b64, &ech_config, &ech_config_len);
+      if(result || !ech_config) {
+        infof(data, "ECH: can't base64 decode ECHConfig from command line");
+        if(data->set.tls_ech & CURLECH_HARD)
+          return result;
+      }
+      if(SSL_set1_ech_config_list(octx->ssl, ech_config,
+                                  ech_config_len) != 1) {
+        infof(data, "ECH: SSL_ECH_set1_echconfig failed");
+        if(data->set.tls_ech & CURLECH_HARD) {
+          free(ech_config);
+          return CURLE_SSL_CONNECT_ERROR;
+        }
+      }
+      free(ech_config);
+      trying_ech_now = 1;
+# else
+      ech_config = (unsigned char *) data->set.str[STRING_ECH_CONFIG];
+      if(!ech_config) {
+        infof(data, "ECH: ECHConfig from command line empty");
+        return CURLE_SSL_CONNECT_ERROR;
+      }
+      ech_config_len = strlen(data->set.str[STRING_ECH_CONFIG]);
+      if(SSL_ech_set1_echconfig(octx->ssl, ech_config, ech_config_len) != 1) {
+        infof(data, "ECH: SSL_ECH_set1_echconfig failed");
+        if(data->set.tls_ech & CURLECH_HARD)
+          return CURLE_SSL_CONNECT_ERROR;
+      }
+      else
+        trying_ech_now = 1;
+# endif
+      infof(data, "ECH: ECHConfig from command line");
+    }
+    else {
+      struct Curl_dns_entry *dns = NULL;
+
+      dns = Curl_fetch_addr(data, connssl->peer.hostname, connssl->peer.port);
+      if(!dns) {
+        infof(data, "ECH: requested but no DNS info available");
+        if(data->set.tls_ech & CURLECH_HARD)
+          return CURLE_SSL_CONNECT_ERROR;
+      }
+      else {
+        struct Curl_https_rrinfo *rinfo = NULL;
+
+        rinfo = dns->hinfo;
+        if(rinfo && rinfo->echconfiglist) {
+          unsigned char *ecl = rinfo->echconfiglist;
+          size_t elen = rinfo->echconfiglist_len;
+
+          infof(data, "ECH: ECHConfig from DoH HTTPS RR");
+# ifndef OPENSSL_IS_BORINGSSL
+          if(SSL_ech_set1_echconfig(octx->ssl, ecl, elen) != 1) {
+            infof(data, "ECH: SSL_ECH_set1_echconfig failed");
+            if(data->set.tls_ech & CURLECH_HARD)
+              return CURLE_SSL_CONNECT_ERROR;
+          }
+# else
+          if(SSL_set1_ech_config_list(octx->ssl, ecl, elen) != 1) {
+            infof(data, "ECH: SSL_set1_ech_config_list failed (boring)");
+            if(data->set.tls_ech & CURLECH_HARD)
+              return CURLE_SSL_CONNECT_ERROR;
+          }
+# endif
+          else {
+            trying_ech_now = 1;
+            infof(data, "ECH: imported ECHConfigList of length %ld", elen);
+          }
+        }
+        else {
+          infof(data, "ECH: requested but no ECHConfig available");
+          if(data->set.tls_ech & CURLECH_HARD)
+            return CURLE_SSL_CONNECT_ERROR;
+        }
+        Curl_resolv_unlock(data, dns);
+      }
+    }
+# ifdef OPENSSL_IS_BORINGSSL
+    if(trying_ech_now && outername) {
+      infof(data, "ECH: setting public_name not supported with boringssl");
+      return CURLE_SSL_CONNECT_ERROR;
+    }
+# else
+    if(trying_ech_now && outername) {
+      infof(data, "ECH: inner: '%s', outer: '%s'",
+            connssl->peer.hostname, outername);
+      result = SSL_ech_set_server_names(octx->ssl,
+                                        connssl->peer.hostname, outername,
+                                        0 /* do send outer */);
+      if(result != 1) {
+        infof(data, "ECH: rv failed to set server name(s) %d [ERROR]", result);
+        return CURLE_SSL_CONNECT_ERROR;
+      }
+    }
+# endif  /* not BORING */
+    if(trying_ech_now
+       && SSL_set_min_proto_version(octx->ssl, TLS1_3_VERSION) != 1) {
+      infof(data, "ECH: Can't force TLSv1.3 [ERROR]");
+      return CURLE_SSL_CONNECT_ERROR;
+    }
+  }
+#endif  /* USE_ECH */
+
 #endif
 
   octx->reused_session = FALSE;
@@ -3926,6 +4069,70 @@ static CURLcode ossl_connect_step1(struct Curl_cfilter *cf,
   return CURLE_OK;
 }
 
+#ifdef USE_ECH
+/* If we have retry configs, then trace those out */
+static void ossl_trace_ech_retry_configs(struct Curl_easy *data, SSL* ssl,
+                                         int reason)
+{
+  CURLcode result = CURLE_OK;
+  size_t rcl = 0;
+  int rv = 1;
+# ifndef OPENSSL_IS_BORINGSSL
+  char *inner = NULL;
+  unsigned char *rcs = NULL;
+  char *outer = NULL;
+# else
+  const char *inner = NULL;
+  const uint8_t *rcs = NULL;
+  const char *outer = NULL;
+  size_t out_name_len = 0;
+  int servername_type = 0;
+# endif
+
+  /* nothing to trace if not doing ECH */
+  if(!ECH_ENABLED(data))
+    return;
+# ifndef OPENSSL_IS_BORINGSSL
+  rv = SSL_ech_get_retry_config(ssl, &rcs, &rcl);
+# else
+  SSL_get0_ech_retry_configs(ssl, &rcs, &rcl);
+  rv = (int)rcl;
+# endif
+
+  if(rv && rcs) {
+# define HEXSTR_MAX 800
+    char *b64str = NULL;
+    size_t blen = 0;
+
+    result = Curl_base64_encode((const char *)rcs, rcl,
+                                &b64str, &blen);
+    if(!result && b64str)
+      infof(data, "ECH: retry_configs %s", b64str);
+    free(b64str);
+# ifndef OPENSSL_IS_BORINGSSL
+    rv = SSL_ech_get_status(ssl, &inner, &outer);
+    infof(data, "ECH: retry_configs for %s from %s, %d %d",
+          inner ? inner : "NULL", outer ? outer : "NULL", reason, rv);
+#else
+    rv = SSL_ech_accepted(ssl);
+    servername_type = SSL_get_servername_type(ssl);
+    inner = SSL_get_servername(ssl, servername_type);
+    SSL_get0_ech_name_override(ssl, &outer, &out_name_len);
+    /* TODO: get the inner from boring */
+    infof(data, "ECH: retry_configs for %s from %s, %d %d",
+          inner ? inner : "NULL", outer ? outer : "NULL", reason, rv);
+#endif
+  }
+  else
+    infof(data, "ECH: no retry_configs (rv = %d)", rv);
+# ifndef OPENSSL_IS_BORINGSSL
+  OPENSSL_free((void *)rcs);
+# endif
+  return;
+}
+
+#endif
+
 static CURLcode ossl_connect_step2(struct Curl_cfilter *cf,
                                    struct Curl_easy *data)
 {
@@ -4038,6 +4245,21 @@ static CURLcode ossl_connect_step2(struct Curl_cfilter *cf,
         result = CURLE_SSL_CLIENTCERT;
         ossl_strerror(errdetail, error_buffer, sizeof(error_buffer));
       }
+#endif
+#ifdef USE_ECH
+      else if((lib == ERR_LIB_SSL) &&
+# ifndef OPENSSL_IS_BORINGSSL
+              (reason == SSL_R_ECH_REQUIRED)) {
+# else
+              (reason == SSL_R_ECH_REJECTED)) {
+# endif
+
+        /* trace retry_configs if we got some */
+        ossl_trace_ech_retry_configs(data, octx->ssl, reason);
+
+        result = CURLE_ECH_REQUIRED;
+        ossl_strerror(errdetail, error_buffer, sizeof(error_buffer));
+      }
 #endif
       else {
         result = CURLE_SSL_CONNECT_ERROR;
@@ -4092,6 +4314,68 @@ static CURLcode ossl_connect_step2(struct Curl_cfilter *cf,
           negotiated_group_name? negotiated_group_name : "[blank]",
           OBJ_nid2sn(psigtype_nid));
 
+#ifdef USE_ECH
+# ifndef OPENSSL_IS_BORINGSSL
+    if(ECH_ENABLED(data)) {
+      char *inner = NULL, *outer = NULL;
+      const char *status = NULL;
+      int rv;
+
+      rv = SSL_ech_get_status(octx->ssl, &inner, &outer);
+      switch(rv) {
+      case SSL_ECH_STATUS_SUCCESS:
+        status = "succeeded";
+        break;
+      case SSL_ECH_STATUS_GREASE_ECH:
+        status = "sent GREASE, got retry-configs";
+        break;
+      case SSL_ECH_STATUS_GREASE:
+        status = "sent GREASE";
+        break;
+      case SSL_ECH_STATUS_NOT_TRIED:
+        status = "not attempted";
+        break;
+      case SSL_ECH_STATUS_NOT_CONFIGURED:
+        status = "not configured";
+        break;
+      case SSL_ECH_STATUS_BACKEND:
+        status = "backend (unexpected)";
+        break;
+      case SSL_ECH_STATUS_FAILED:
+        status = "failed";
+        break;
+      case SSL_ECH_STATUS_BAD_CALL:
+        status = "bad call (unexpected)";
+        break;
+      case SSL_ECH_STATUS_BAD_NAME:
+        status = "bad name (unexpected)";
+        break;
+      default:
+        status = "unexpected status";
+        infof(data, "ECH: unexpected status %d",rv);
+      }
+      infof(data, "ECH: result: status is %s, inner is %s, outer is %s",
+             (status?status:"NULL"),
+             (inner?inner:"NULL"),
+             (outer?outer:"NULL"));
+      OPENSSL_free(inner);
+      OPENSSL_free(outer);
+      if(rv == SSL_ECH_STATUS_GREASE_ECH) {
+        /* trace retry_configs if we got some */
+        ossl_trace_ech_retry_configs(data, octx->ssl, 0);
+      }
+      if(rv != SSL_ECH_STATUS_SUCCESS
+         && data->set.tls_ech & CURLECH_HARD) {
+        infof(data, "ECH: ech-hard failed");
+        return CURLE_SSL_CONNECT_ERROR;
+      }
+   }
+   else {
+      infof(data, "ECH: result: status is not attempted");
+   }
+# endif  /* BORING */
+#endif  /* USE_ECH */
+
 #ifdef HAS_ALPN
     /* Sets data and len to negotiated protocol, len is 0 if no protocol was
      * negotiated
index 48cf132a378ec3518efa27d636e0ef4914d51c37..82593f301b0bae066286d8fcd74bf77b62903b6a 100644 (file)
 #include "curl_memory.h"
 #include "memdebug.h"
 
+#ifdef USE_ECH
+# include "curl_base64.h"
+# define ECH_ENABLED(__data__) \
+    (__data__->set.tls_ech && \
+     !(__data__->set.tls_ech & CURLECH_DISABLE)\
+    )
+#endif /* USE_ECH */
+
 /* KEEP_PEER_CERT is a product of the presence of build time symbol
    OPENSSL_EXTRA without NO_CERTS, depending on the version. KEEP_PEER_CERT is
    in wolfSSL's settings.h, and the latter two are build time symbols in
@@ -725,6 +733,82 @@ wolfssl_connect_step1(struct Curl_cfilter *cf, struct Curl_easy *data)
     Curl_ssl_sessionid_unlock(data);
   }
 
+#ifdef USE_ECH
+  if(ECH_ENABLED(data)) {
+    int trying_ech_now = 0;
+
+    if(data->set.str[STRING_ECH_PUBLIC]) {
+      infof(data, "ECH: outername not (yet) supported with WolfSSL");
+      return CURLE_SSL_CONNECT_ERROR;
+    }
+    if(data->set.tls_ech == CURLECH_GREASE) {
+      infof(data, "ECH: GREASE'd ECH not yet supported for wolfSSL");
+      return CURLE_SSL_CONNECT_ERROR;
+    }
+    if(data->set.tls_ech & CURLECH_CLA_CFG
+       && data->set.str[STRING_ECH_CONFIG]) {
+      char *b64val = data->set.str[STRING_ECH_CONFIG];
+      word32 b64len = 0;
+
+      b64len = (word32) strlen(b64val);
+      if(b64len
+         && wolfSSL_SetEchConfigsBase64(backend->handle, b64val, b64len)
+              != WOLFSSL_SUCCESS) {
+        if(data->set.tls_ech & CURLECH_HARD)
+          return CURLE_SSL_CONNECT_ERROR;
+      }
+      else {
+       trying_ech_now = 1;
+       infof(data, "ECH: ECHConfig from command line");
+      }
+    }
+    else {
+      struct Curl_dns_entry *dns = NULL;
+
+      dns = Curl_fetch_addr(data, connssl->peer.hostname, connssl->peer.port);
+      if(!dns) {
+        infof(data, "ECH: requested but no DNS info available");
+        if(data->set.tls_ech & CURLECH_HARD)
+          return CURLE_SSL_CONNECT_ERROR;
+      }
+      else {
+        struct Curl_https_rrinfo *rinfo = NULL;
+
+        rinfo = dns->hinfo;
+        if(rinfo && rinfo->echconfiglist) {
+          unsigned char *ecl = rinfo->echconfiglist;
+          size_t elen = rinfo->echconfiglist_len;
+
+          infof(data, "ECH: ECHConfig from DoH HTTPS RR");
+          if(wolfSSL_SetEchConfigs(backend->handle, ecl, (word32) elen) !=
+                WOLFSSL_SUCCESS) {
+            infof(data, "ECH: wolfSSL_SetEchConfigs failed");
+            if(data->set.tls_ech & CURLECH_HARD)
+              return CURLE_SSL_CONNECT_ERROR;
+          }
+          else {
+            trying_ech_now = 1;
+            infof(data, "ECH: imported ECHConfigList of length %ld", elen);
+          }
+        }
+        else {
+          infof(data, "ECH: requested but no ECHConfig available");
+          if(data->set.tls_ech & CURLECH_HARD)
+            return CURLE_SSL_CONNECT_ERROR;
+        }
+        Curl_resolv_unlock(data, dns);
+      }
+    }
+
+    if(trying_ech_now
+       && SSL_set_min_proto_version(backend->handle, TLS1_3_VERSION) != 1) {
+      infof(data, "ECH: Can't force TLSv1.3 [ERROR]");
+      return CURLE_SSL_CONNECT_ERROR;
+    }
+
+  }
+#endif  /* USE_ECH */
+
 #ifdef USE_BIO_CHAIN
   {
     WOLFSSL_BIO *bio;
@@ -858,6 +942,31 @@ wolfssl_connect_step2(struct Curl_cfilter *cf, struct Curl_easy *data)
                     "continuing anyway");
       }
     }
+#endif
+#ifdef USE_ECH
+    else if(-1 == detail) {
+      /* try access a retry_config ECHConfigList for tracing */
+      byte echConfigs[1000];
+      word32 echConfigsLen = 1000;
+      int rv = 0;
+
+      /* this currently doesn't produce the retry_configs */
+      rv = wolfSSL_GetEchConfigs(backend->handle, echConfigs,
+                                 &echConfigsLen);
+      if(rv != WOLFSSL_SUCCESS) {
+        infof(data, "Failed to get ECHConfigs");
+      }
+      else {
+        char *b64str = NULL;
+        size_t blen = 0;
+
+        rv = Curl_base64_encode((const char *)echConfigs, echConfigsLen,
+                                &b64str, &blen);
+        if(!rv && b64str)
+          infof(data, "ECH: (not yet) retry_configs %s", b64str);
+        free(b64str);
+      }
+    }
 #endif
     else if(backend->io_result == CURLE_AGAIN) {
       return CURLE_OK;
index 1ee5d50c46047113f20a957630acd8ae5e1471e8..5c307fc819585ed994cdabd057fae52510377b44 100644 (file)
@@ -568,6 +568,105 @@ AC_DEFUN([CURL_CHECK_LIB_ARES], [
   fi
 ])
 
+dnl CURL_CHECK_OPTION_NTLM_WB
+dnl -------------------------------------------------
+dnl Verify if configure has been invoked with option
+dnl --enable-ntlm-wb or --disable-ntlm-wb, and set
+dnl shell variable want_ntlm_wb and want_ntlm_wb_file
+dnl as appropriate.
+
+AC_DEFUN([CURL_CHECK_OPTION_NTLM_WB], [
+  AC_BEFORE([$0],[CURL_CHECK_NTLM_WB])dnl
+  OPT_NTLM_WB="default"
+  AC_ARG_ENABLE(ntlm-wb,
+AS_HELP_STRING([--enable-ntlm-wb@<:@=FILE@:>@],[Enable NTLM delegation to winbind's ntlm_auth helper, where FILE is ntlm_auth's absolute filename (default: /usr/bin/ntlm_auth)])
+AS_HELP_STRING([--disable-ntlm-wb],[Disable NTLM delegation to winbind's ntlm_auth helper]),
+  OPT_NTLM_WB=$enableval)
+  want_ntlm_wb_file="/usr/bin/ntlm_auth"
+  case "$OPT_NTLM_WB" in
+    no)
+      dnl --disable-ntlm-wb option used
+      want_ntlm_wb="no"
+      ;;
+    default)
+      dnl configure option not specified
+      want_ntlm_wb="yes"
+      ;;
+    *)
+      dnl --enable-ntlm-wb option used
+      want_ntlm_wb="yes"
+      if test -n "$enableval" && test "$enableval" != "yes"; then
+        want_ntlm_wb_file="$enableval"
+      fi
+      ;;
+  esac
+])
+
+
+dnl CURL_CHECK_NTLM_WB
+dnl -------------------------------------------------
+dnl Check if support for NTLM delegation to winbind's
+dnl ntlm_auth helper will finally be enabled depending
+dnl on given configure options and target platform.
+
+AC_DEFUN([CURL_CHECK_NTLM_WB], [
+  AC_REQUIRE([CURL_CHECK_OPTION_NTLM_WB])dnl
+  AC_REQUIRE([CURL_CHECK_NATIVE_WINDOWS])dnl
+  AC_MSG_CHECKING([whether to enable NTLM delegation to winbind's helper])
+  if test "$curl_cv_native_windows" = "yes" ||
+    test "x$SSL_ENABLED" = "x"; then
+    want_ntlm_wb_file=""
+    want_ntlm_wb="no"
+  elif test "x$ac_cv_func_fork" != "xyes"; then
+    dnl ntlm_wb requires fork
+    want_ntlm_wb="no"
+  fi
+  AC_MSG_RESULT([$want_ntlm_wb])
+  if test "$want_ntlm_wb" = "yes"; then
+    AC_DEFINE(NTLM_WB_ENABLED, 1,
+      [Define to enable NTLM delegation to winbind's ntlm_auth helper.])
+    AC_DEFINE_UNQUOTED(NTLM_WB_FILE, "$want_ntlm_wb_file",
+      [Define absolute filename for winbind's ntlm_auth helper.])
+    NTLM_WB_ENABLED=1
+  fi
+])
+
+dnl CURL_CHECK_OPTION_HTTPSRR
+dnl -----------------------------------------------------
+dnl Verify whether configure has been invoked with option
+dnl --enable-httpsrr or --disable-httpsrr, and set
+dnl shell variable want_httpsrr as appropriate.
+
+AC_DEFUN([CURL_CHECK_OPTION_HTTPSRR], [
+  AC_MSG_CHECKING([whether to enable HTTPSRR support])
+  OPT_HTTPSRR="default"
+  AC_ARG_ENABLE(httpsrr,
+AS_HELP_STRING([--enable-httpsrr],[Enable HTTPSRR support])
+AS_HELP_STRING([--disable-httpsrr],[Disable HTTPSRR support]),
+  OPT_HTTPSRR=$enableval)
+  case "$OPT_HTTPSRR" in
+    no)
+      dnl --disable-httpsrr option used
+      want_httpsrr="no"
+      curl_httpsrr_msg="no      (--enable-httpsrr)"
+      AC_MSG_RESULT([no])
+      ;;
+    default)
+      dnl configure option not specified
+      want_httpsrr="no"
+      curl_httpsrr_msg="no      (--enable-httpsrr)"
+      AC_MSG_RESULT([no])
+      ;;
+    *)
+      dnl --enable-httpsrr option used
+      want_httpsrr="yes"
+      curl_httpsrr_msg="enabled (--disable-httpsrr)"
+      experimental="httpsrr"
+      AC_MSG_RESULT([yes])
+      ;;
+  esac
+])
+
 dnl CURL_CHECK_OPTION_ECH
 dnl -----------------------------------------------------
 dnl Verify whether configure has been invoked with option
@@ -603,3 +702,4 @@ AS_HELP_STRING([--disable-ech],[Disable ECH support]),
       ;;
   esac
 ])
+])
index 596c1f1e150b21f2a35c0bf113409738ae86669b..4d1e2b52addf3baabf3c9aba33844f69d50c2d8f 100644 (file)
@@ -1097,6 +1097,9 @@ curl_easy_setopt_ccsid(CURL *easy, CURLoption tag, ...)
   case CURLOPT_DNS_LOCAL_IP6:
   case CURLOPT_DNS_SERVERS:
   case CURLOPT_DOH_URL:
+#ifdef USE_ECH
+  case CURLOPT_ECH:
+#endif
   case CURLOPT_EGDSOCKET:
   case CURLOPT_FTPPORT:
   case CURLOPT_FTP_ACCOUNT:
index 3259bc7a5f056200a58fcaf3979a549fc1d3c4bb..bb271583263db3109faa93a345dc1c3c3946a9f8 100644 (file)
@@ -176,6 +176,14 @@ static void free_config_fields(struct OperationConfig *config)
   Curl_safefree(config->aws_sigv4);
   Curl_safefree(config->proto_str);
   Curl_safefree(config->proto_redir_str);
+#ifdef USE_ECH
+  Curl_safefree(config->ech);
+  config->ech = NULL;
+  Curl_safefree(config->ech_config);
+  config->ech_config = NULL;
+  Curl_safefree(config->ech_public);
+  config->ech_public = NULL;
+#endif
 }
 
 void config_free(struct OperationConfig *config)
index dfa74d81ff4b81975714af2bfc7461bee2523f4d..74d0c45f2ed12160efa145aa220c7f26d019697f 100644 (file)
@@ -298,6 +298,12 @@ struct OperationConfig {
   struct State state;             /* for create_transfer() */
   bool rm_partial;                /* on error, remove partially written output
                                      files */
+#ifdef USE_ECH
+  char *ech;                      /* Config set by --ech keywords */
+  char *ech_config;               /* Config set by "--ech esl:" option */
+  char *ech_public;               /* Config set by "--ech pn:" option */
+#endif
+
 };
 
 struct GlobalConfig {
index f50992f8a68f5dd664597780bea620e6b4ba788f..f56981a743d86832a2197604edf043a37c30658a 100644 (file)
@@ -123,6 +123,7 @@ typedef enum {
   C_DOH_INSECURE,
   C_DOH_URL,
   C_DUMP_HEADER,
+  C_ECH,
   C_EGD_FILE,
   C_ENGINE,
   C_EPRT,
@@ -404,6 +405,7 @@ static const struct LongShort aliases[]= {
   {"doh-insecure",               ARG_BOOL, ' ', C_DOH_INSECURE},
   {"doh-url"        ,            ARG_STRG, ' ', C_DOH_URL},
   {"dump-header",                ARG_FILE, 'D', C_DUMP_HEADER},
+  {"ech",                        ARG_STRG, ' ', C_ECH},
   {"egd-file",                   ARG_STRG, ' ', C_EGD_FILE},
   {"engine",                     ARG_STRG, ' ', C_ENGINE},
   {"eprt",                       ARG_BOOL, ' ', C_EPRT},
@@ -2079,6 +2081,57 @@ ParameterError getparameter(const char *flag, /* f or -long-flag */
         err = PARAM_ENGINES_REQUESTED;
       }
       break;
+#ifndef USE_ECH
+    case C_ECH: /* --ech, not implemented by default */
+      err = PARAM_LIBCURL_DOESNT_SUPPORT;
+      break;
+#else
+    case C_ECH: /* --ech */
+      if(strlen(nextarg) > 4 && strncasecompare("pn:", nextarg, 3)) {
+        /* a public_name */
+        err = getstr(&config->ech_public, nextarg, DENY_BLANK);
+      }
+      else if(strlen(nextarg) > 5 && strncasecompare("ecl:", nextarg, 4)) {
+        /* an ECHConfigList */
+        if('@' != *(nextarg + 4)) {
+          err = getstr(&config->ech_config, nextarg, DENY_BLANK);
+        }
+        else {
+          /* Indirect case: @filename or @- for stdin */
+          char *tmpcfg = NULL;
+          FILE *file;
+
+          nextarg++;        /* skip over '@' */
+          if(!strcmp("-", nextarg)) {
+            file = stdin;
+          }
+          else {
+            file = fopen(nextarg, FOPEN_READTEXT);
+          }
+          if(!file) {
+            warnf(global,
+                  "Couldn't read file \"%s\" "
+                  "specified for \"--ech ecl:\" option",
+                  nextarg);
+            return PARAM_BAD_USE; /*  */
+          }
+          err = file2string(&tmpcfg, file);
+          if(file != stdin)
+            fclose(file);
+          if(err)
+            return err;
+          config->ech_config = aprintf("ecl:%s",tmpcfg);
+          if(!config->ech_config)
+            return PARAM_NO_MEM;
+          free(tmpcfg);
+      } /* file done */
+    }
+    else {
+      /* Simple case: just a string, with a keyword */
+      err = getstr(&config->ech, nextarg, DENY_BLANK);
+    }
+    break;
+#endif
     case C_CAPATH: /* --capath */
       err = getstr(&config->capath, nextarg, DENY_BLANK);
       break;
index fd16af85f32e3b398b13fdd2dee1cef458c6e570..443e6b492b19caaa2c3cb17c57d955f4866d38c3 100644 (file)
@@ -67,6 +67,7 @@ static const struct category_descriptors categories[] = {
   {"telnet", "TELNET protocol options", CURLHELP_TELNET},
   {"tftp", "TFTP protocol options", CURLHELP_TFTP},
   {"tls", "All TLS/SSL related options", CURLHELP_TLS},
+  {"ech", "All Encrypted Client Hello (ECH) options", CURLHELP_ECH},
   {"upload", "All options for uploads",
    CURLHELP_UPLOAD},
   {"verbose", "Options related to any kind of command line output of curl",
index a05cd84581c47836b2e6a9613fc8f58511504a5f..894e39f85244112fca3d87129833fe69c2dbc56d 100644 (file)
@@ -68,6 +68,7 @@ struct helptxt {
 #define CURLHELP_TLS 1u << 22u
 #define CURLHELP_UPLOAD 1u << 23u
 #define CURLHELP_VERBOSE 1u << 24u
+#define CURLHELP_ECH 1u << 25u
 
 extern const struct helptxt helptext[];
 
index adb01b3bed24bfe5c4193af26f4463ab8175ed85..8429322045fa79fc96db3f39aed20accc01e57d9 100644 (file)
@@ -168,6 +168,9 @@ const struct helptxt helptext[] = {
   {"-D, --dump-header <filename>",
    "Write the received headers to <filename>",
    CURLHELP_HTTP | CURLHELP_FTP},
+  {"    --ech <config>",
+   "Configure Encrypted Client Hello (ECH) for use with the TLS session",
+   CURLHELP_TLS | CURLHELP_ECH},
   {"    --egd-file <file>",
    "EGD socket path for random data",
    CURLHELP_TLS},
index 49ec7d835cfedbdf3bedc56300f6fd851cb37643..9f14b3e586035377571362cafdcec3b13993fbf1 100644 (file)
@@ -2187,6 +2187,16 @@ static CURLcode single_transfer(struct GlobalConfig *global,
         if(config->hsts)
           my_setopt_str(curl, CURLOPT_HSTS, config->hsts);
 
+#ifdef USE_ECH
+        /* only if enabled in configure */
+        if(config->ech) /* only if set (optional) */
+          my_setopt_str(curl, CURLOPT_ECH, config->ech);
+        if(config->ech_public) /* only if set (optional) */
+          my_setopt_str(curl, CURLOPT_ECH, config->ech_public);
+        if(config->ech_config) /* only if set (optional) */
+          my_setopt_str(curl, CURLOPT_ECH, config->ech_config);
+#endif
+
         /* initialize retry vars for loop below */
         per->retry_sleep_default = (config->retry_delay) ?
           config->retry_delay*1000L : RETRY_SLEEP_DEFAULT; /* ms */
index 654e443444b65a74a265b2731060b947687e20f9..a914f305a0ea5ad4bd1712a5386f6b7cdb1d79ae 100644 (file)
@@ -54,6 +54,7 @@ Invalid category provided, here is a list of all categories:
  telnet      TELNET protocol options
  tftp        TFTP protocol options
  tls         All TLS/SSL related options
+ ech         All Encrypted Client Hello (ECH) options
  upload      All options for uploads
  verbose     Options related to any kind of command line output of curl
 </stdout>
index c0f038be402854b4a1357be012e88e3f6cdf491d..7abbc555b48614b96693f3a7a04455d451b8867d 100644 (file)
@@ -133,7 +133,8 @@ e97: proxy handshake error
 e98: SSL Client Certificate required
 e99: Unrecoverable error in select/poll
 e100: A value or data field grew larger than allowed
-e101: Unknown error
+e101: ECH attempted but failed
+e102: Unknown error
 m-1: Please call curl_multi_perform() soon
 m0: No error
 m1: Invalid multi handle
diff --git a/tests/ech_combos.py b/tests/ech_combos.py
new file mode 100755 (executable)
index 0000000..7f41198
--- /dev/null
@@ -0,0 +1,99 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#***************************************************************************
+#                                  _   _ ____  _
+#  Project                     ___| | | |  _ \| |
+#                             / __| | | | |_) | |
+#                            | (__| |_| |  _ <| |___
+#                             \___|\___/|_| \_\_____|
+#
+# Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at https://curl.se/docs/copyright.html.
+#
+# You may opt to use, copy, modify, merge, publish, distribute and/or sell
+# copies of the Software, and permit persons to whom the Software is
+# furnished to do so, under the terms of the COPYING file.
+#
+# This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
+# KIND, either express or implied.
+#
+# SPDX-License-Identifier: curl
+#
+###########################################################################
+#
+# Python3 program to print all combination of size r in an array of size n.
+# This is used to generate test lines in tests/ech_test.sh.
+# This will be discarded in the process of moving from experimental,
+# but is worth preserving for the moment in case of changes to the 
+# ECH command line args
+
+def CombinationRepetitionUtil(chosen, arr, badarr, index,
+                            r, start, end):
+                                
+    # Current combination is ready,
+    # print it
+    if index == r:
+        # figure out if result should be good or bad and 
+        # print prefix, assuming $turl does support ECH so
+        # should work if given "positive" parameters
+        res = 1
+        j = len(chosen) - 1
+        while res and j >= 0:
+            if chosen[j] in badarr:
+                res = 0
+            j = j - 1
+        print("cli_test $turl 1", res, end = " ")
+        # print combination but eliminating any runs of
+        # two identical params
+        for j in range(r):
+            if j != 0 and chosen[j] != chosen[j-1]:
+                print(chosen[j], end = " ")
+            
+        print()
+        return
+        
+    # When no more elements are
+    # there to put in chosen[]
+    if start > n:
+        return
+        
+    # Current is included, put
+    # next at next location
+    chosen[index] = arr[start]
+    
+    # Current is excluded, replace it
+    # with next (Note that i+1 is passed,
+    # but index is not changed)
+    CombinationRepetitionUtil(chosen, arr, badarr, index + 1,
+                            r, start, end)
+    CombinationRepetitionUtil(chosen, arr, badarr, index,
+                            r, start + 1, end)
+
+# The main function that prints all
+# combinations of size r in arr[] of
+# size n. This function mainly uses
+# CombinationRepetitionUtil()
+def CombinationRepetition(arr, badarr, n, r):
+    
+    # A temporary array to store
+    # all combination one by one
+    chosen = [0] * r
+
+    # Print all combination using
+    # temporary array 'chosen[]'
+    CombinationRepetitionUtil(chosen, arr, badarr, 0, r, 0, n)
+
+# Driver code
+badarr = [ '--ech grease', '--ech false', '--ech ecl:$badecl', '--ech pn:$badpn' ]
+goodarr = [ '--ech hard', '--ech true', '--ech ecl:$goodecl',  '--ech pn:$goodpn' ]
+arr = badarr + goodarr
+r = 8
+n = len(arr) - 1
+
+CombinationRepetition(arr, badarr, n, r)
+
+# This code is contributed by Vaibhav Kumar 12.
+
diff --git a/tests/ech_tests.sh b/tests/ech_tests.sh
new file mode 100755 (executable)
index 0000000..df8e45b
--- /dev/null
@@ -0,0 +1,1151 @@
+#!/bin/bash
+#***************************************************************************
+#                                  _   _ ____  _
+#  Project                     ___| | | |  _ \| |
+#                             / __| | | | |_) | |
+#                            | (__| |_| |  _ <| |___
+#                             \___|\___/|_| \_\_____|
+#
+# Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at https://curl.se/docs/copyright.html.
+#
+# You may opt to use, copy, modify, merge, publish, distribute and/or sell
+# copies of the Software, and permit persons to whom the Software is
+# furnished to do so, under the terms of the COPYING file.
+#
+# This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
+# KIND, either express or implied.
+#
+# SPDX-License-Identifier: curl
+#
+###########################################################################
+#
+
+# Run some tests against servers we know to support ECH (CF, defo.ie, etc.).
+# as well as some we know don't do ECH but have an HTTPS RR, and finally some
+# for which neither is the case.
+
+# TODO: Translate this into something that approximates a valid curl test:-)
+# Should be useful though even before such translation and a pile less work
+# to do this than that.  The pile of work required would include making an
+# ECH-enabled server and a DoH server. For now, this is just run manually.
+#
+
+# set -x
+
+# Exit with an error if there's an active ech stanza in ~/.curlrc
+# as that'd likely skew some results (e.g. turning a fail into a
+# success or vice versa)
+: "${CURL_CFG_FILE=$HOME/.curlrc}"
+active_ech=$(grep ech "$CURL_CFG_FILE" | grep -v "#.*ech")
+if [[ "$active_ech" != "" ]]
+then
+    echo "You seem to have an active ECH setting in $CURL_CFG_FILE"
+    echo "That might affect results so please remove that or comment"
+    echo "it out - exiting."
+    exit 1
+fi
+
+
+# Targets we expect to be ECH-enabled servers
+# for which an HTTPS RR is published.
+# structure is host:port mapped to pathname
+# TODO: add negative tests for these
+declare -A ech_targets=(
+    [my-own.net]="ech-check.php"
+    [my-own.net:8443]="ech-check.php"
+    [defo.ie]="ech-check.php"
+    [cover.defo.ie]=""
+    [draft-13.esni.defo.ie:8413]="stats"
+    [draft-13.esni.defo.ie:8414]="stats"
+    [draft-13.esni.defo.ie:9413]=""
+    [draft-13.esni.defo.ie:10413]=""
+    [draft-13.esni.defo.ie:11413]=""
+    [draft-13.esni.defo.ie:12413]=""
+    [draft-13.esni.defo.ie:12414]=""
+    [crypto.cloudflare.com]="cdn-cgi/trace"
+    [tls-ech.dev]=""
+    [epochbelt.com]=""
+)
+
+# Targets we expect not to be ECH-enabled servers
+# but for which an HTTPS RR is published.
+declare -A httpsrr_targets=(
+    [ietf.org]=""
+    [rte.ie]=""
+)
+
+# Targets we expect not to be ECH-enabled servers
+# and for which no HTTPS RR is published.
+declare -A neither_targets=(
+    [www.tcd.ie]=""
+    [jell.ie]=""
+)
+
+#
+# Variables that can be over-ridden from environment
+#
+
+# Top of curl test tree, assume we're there
+: "${CTOP:=.}"
+
+# Plase to put test log output
+: "${LTOP:=$CTOP/tests/ech-log/}"
+
+# place to stash outputs when things go wrong
+: "${BTOP:=$LTOP}"
+
+# time to wait for a remote access to work, 10 seconds
+: "${tout:=10s}"
+
+# Where we find OpenSSL .so's
+: "${OSSL:=$HOME/code/openssl}"
+
+# Where we find WolfSSL .so's
+: "${WSSL:=$HOME/code/wolfssl/inst/lib}"
+
+# Where we find boringssl .so's
+: "${BSSL:=$HOME/code/boringssl/inst/lib}"
+
+# Where we send DoH queries when using kdig or curl
+: "${DOHSERVER:=one.one.one.one}"
+: "${DOHPATH:=dns-query}"
+
+# Whether to send mail when bad things happen (mostly for cronjob)
+: "${DOMAIL:=no}"
+
+# Misc vars and functions
+
+DEFPORT=443
+
+function whenisitagain()
+{
+    /bin/date -u +%Y%m%d-%H%M%S
+}
+
+function fileage()
+{
+    echo $(($(date +%s) - $(date +%s -r "$1")))
+}
+
+function hostport2host()
+{
+    case $1 in
+      *:*) host=${1%:*} port=${1##*:};;
+        *) host=$1      port=$DEFPORT;;
+    esac
+    echo "$host"
+}
+
+function hostport2port()
+{
+    case $1 in
+      *:*) host=${1%:*} port=${1##*:};;
+        *) host=$1      port=$DEFPORT;;
+    esac
+    echo "$port"
+}
+
+function cli_test()
+{
+    # 1st param is target URL
+    turl=$1
+    # 2nd param is 0 if we expect curl to not work or 1 if we expect it
+    # to have worked
+    curl_winorlose=$2
+    # 3rd param is 0 if we expect ECH to not work or 1 if we expect it
+    # to have worked
+    ech_winorlose=$3
+    # remaining params are passed to command line
+    # echparms=(${@:4})
+    IFS=" " read -r -a echparms <<< "${@:4}"
+
+    TMPF=$(mktemp)
+    cmd="timeout $tout $CURL ${CURL_PARAMS[*]} ${echparms[*]} $turl >$TMPF 2>&1"
+    echo "cli_test: $cmd " >> "$logfile"
+    timeout "$tout" "$CURL" "${CURL_PARAMS[@]}" "${echparms[@]}" "$turl" >"$TMPF" 2>&1
+    eres=$?
+    if [[ "$eres" == "124" ]]
+    then
+        allgood="no"
+        echo "cli_test: Timeout running $cmd"
+        cat "$TMPF" >> "$logfile"
+        echo "cli_test: Timeout running $cmd" >> "$logfile"
+    fi
+    if [[ "$eres" != "0" && "$curl_winorlose" == "1" ]]
+    then
+        allgood="no"
+        echo "cli_test: curl failure running $cmd"
+        cat "$TMPF" >> "$logfile"
+        echo "cli_test: curl failure running $cmd" >> "$logfile"
+    fi
+    ech_success=$(grep -c "ECH: result: status is succeeded" "$TMPF")
+    if [[ "$ech_success" == "$ech_winorlose" ]]
+    then
+        echo "cli_test ok for ${echparms[*]}"
+    else
+        allgood="no"
+        echo "cli_test: ECH failure running $cmd"
+        cat "$TMPF" >> "$logfile"
+        echo "cli_test: ECH failure running $cmd" >> "$logfile"
+    fi
+    rm -f "$TMPF"
+}
+
+function get_ech_configlist()
+{
+    domain=$1
+    ecl=$(dig +short https "$domain" | grep "ech=" | sed -e 's/^.*ech=//' | sed -e 's/ .*//')
+    echo "$ecl"
+}
+
+# start of main script
+
+# start by assuming we have nothing we need...
+have_ossl="no"
+have_wolf="no"
+have_bssl="no"
+using_ossl="no"
+using_wolf="no"
+using_bssl="no"
+have_curl="no"
+have_dig="no"
+have_kdig="no"
+have_presout="no"
+have_portsblocked="no"
+
+# setup logging
+NOW=$(whenisitagain)
+BINNAME=$(basename "$0" .sh)
+if [ ! -d "$LTOP" ]
+then
+    mkdir -p "$LTOP"
+fi
+if [ ! -d "$LTOP" ]
+then
+    echo "Can't see $LTOP for logs - exiting"
+    exit 1
+fi
+logfile=$LTOP/${BINNAME}_$NOW.log
+
+echo "-----" > "$logfile"
+echo "Running $0 at $NOW"  >> "$logfile"
+echo "Running $0 at $NOW"
+
+# check we have the binaries needed and which TLS library we'll be using
+if [ -f "$OSSL"/libssl.so ]
+then
+    have_ossl="yes"
+fi
+if [ -f "$WSSL"/libwolfssl.so ]
+then
+    have_wolf="yes"
+fi
+if [ -f "$BSSL"/libssl.so ]
+then
+    have_bssl="yes"
+fi
+CURL="$CTOP/src/curl"
+CURL_PARAMS=(-vvv --doh-url https://one.one.one.one/dns-query)
+if [ -f "$CTOP"/src/curl ]
+then
+    have_curl="yes"
+fi
+ossl_cnt=$(LD_LIBRARY_PATH=$OSSL $CURL "${CURL_PARAMS[@]}" -V 2> /dev/null | grep -c OpenSSL)
+if ((ossl_cnt == 1))
+then
+    using_ossl="yes"
+    # setup access to our .so
+    export LD_LIBRARY_PATH=$OSSL
+fi
+bssl_cnt=$(LD_LIBRARY_PATH=$BSSL $CURL "${CURL_PARAMS[@]}" -V 2> /dev/null | grep -c BoringSSL)
+if ((bssl_cnt == 1))
+then
+    using_bssl="yes"
+    # setup access to our .so
+    export LD_LIBRARY_PATH=$BSSL
+fi
+wolf_cnt=$($CURL "${CURL_PARAMS[@]}" -V 2> /dev/null | grep -c wolfSSL)
+if ((wolf_cnt == 1))
+then
+    using_wolf="yes"
+    # for some reason curl+wolfSSL dislikes certs that are ok
+    # for browsers, so we'll test using "insecure" mode (-k)
+    # but that's ok here as we're only interested in ECH testing
+    CURL_PARAMS+=(-k)
+fi
+# check if we have dig and it knows https or not
+digcmd="dig +short"
+wdig=$(type -p dig)
+if [[ "$wdig" != "" ]]
+then
+    have_dig="yes"
+fi
+wkdig=$(type -p kdig)
+if [[ "$wkdig" != "" ]]
+then
+    have_kdig="yes"
+    digcmd="kdig @$DOHSERVER +https +short"
+fi
+# see if our dig version knows HTTPS
+dout=$($digcmd https defo.ie)
+if [[ $dout != "1 . "* ]]
+then
+    dout=$($digcmd -t TYPE65 defo.ie)
+    if [[ $dout == "1 . "* ]]
+    then
+        # we're good
+        have_presout="yes"
+    fi
+else
+    have_presout="yes"
+fi
+
+# Check if ports other than 443 are blocked from this
+# vantage point (I run tests in a n/w where that's
+# sadly true sometimes;-)
+# echo "Checking if ports other than 443 are maybe blocked"
+not443testurl="https://draft-13.esni.defo.ie:9413/"
+timeout "$tout" "$CURL" "${CURL_PARAMS[@]}" "$not443testurl" >/dev/null 2>&1
+eres=$?
+if [[ "$eres" == "124" ]]
+then
+    echo "Timeout running curl for $not443testurl" >> "$logfile"
+    echo "Timeout running curl for $not443testurl"
+    have_portsblocked="yes"
+fi
+
+{
+    echo "have_ossl: $have_ossl"
+    echo "have_wolf: $have_wolf"
+    echo "have_bssl: $have_bssl"
+    echo "using_ossl: $using_ossl"
+    echo "using_wolf: $using_wolf"
+    echo "using_bssl: $using_bssl"
+    echo "have_curl: $have_curl"
+    echo "have_dig: $have_dig"
+    echo "have_kdig: $have_kdig"
+    echo "have_presout: $have_presout"
+    echo "have_portsblocked: $have_portsblocked"
+} >> "$logfile"
+
+echo "curl: have $have_curl, cURL command: |$CURL ${CURL_PARAMS[*]}|"
+echo "ossl: have: $have_ossl, using: $using_ossl"
+echo "wolf: have: $have_wolf, using: $using_wolf"
+echo "bssl: have: $have_bssl, using: $using_bssl"
+echo "dig: $have_dig, kdig: $have_kdig, HTTPS pres format: $have_presout"
+echo "dig command: |$digcmd|"
+echo "ports != 443 blocked: $have_portsblocked"
+
+if [[ "$have_curl" == "no" ]]
+then
+    echo "Can't proceed without curl - exiting"
+    exit 32
+fi
+
+allgood="yes"
+
+skip="false"
+
+if [[ "$skip" != "true" ]]
+then
+
+# basic ECH good/bad
+for targ in "${!ech_targets[@]}"
+do
+    if [[ "$using_wolf" == "yes" ]] 
+    then
+        case $targ in
+            "draft-13.esni.defo.ie:8414" | "tls-ech.dev" | \
+            "crypto.cloudflare.com" | "epochbelt.com")
+                echo "Skipping $targ 'cause wolf"; continue;;
+            *)
+                ;;
+        esac
+    fi
+    host=$(hostport2host "$targ")
+    port=$(hostport2port "$targ")
+    if [[ "$port" != "443" && "$have_portsblocked" == "yes" ]]
+    then
+        echo "Skipping $targ as ports != 443 seem blocked"
+        continue
+    fi
+    path=${ech_targets[$targ]}
+    turl="https://$host:$port/$path"
+    echo "ECH check for $turl"
+    { 
+        echo ""
+        echo "ECH check for $turl"
+    } >> "$logfile"
+    timeout "$tout" "$CURL" "${CURL_PARAMS[@]}" --ech hard "$turl" >> "$logfile" 2>&1
+    eres=$?
+    if [[ "$eres" == "124" ]]
+    then
+        allgood="no"
+        {
+            echo "Timeout for $turl"
+            echo -e "\tTimeout for $turl"
+            echo "Timeout running curl for $host:$port/$path"
+        } >> "$logfile"
+    fi
+    if [[ "$eres" != "0" ]]
+    then
+        allgood="no"
+        echo "Error ($eres) for $turl" >> "$logfile"
+        echo -e "\tError ($eres) for $turl"
+    fi
+    echo "" >> "$logfile"
+done
+
+# check if public_name override works (OpenSSL only)
+if [[ "$using_ossl" == "yes" ]]
+then
+    for targ in "${!ech_targets[@]}"
+    do
+        host=$(hostport2host "$targ")
+        port=$(hostport2port "$targ")
+        if [[ "$port" != "443" && "$have_portsblocked" == "yes" ]]
+        then
+            echo "Skipping $targ as ports != 443 seem blocked"
+            continue
+        fi
+        path=${ech_targets[$targ]}
+        turl="https://$host:$port/$path"
+        echo "PN override check for $turl"
+        { 
+            echo ""
+            echo "PN override check for $turl"
+        } >> "$logfile"
+        timeout "$tout" "$CURL" "${CURL_PARAMS[@]}" --ech pn:override --ech hard "$turl" >> "$logfile" 2>&1
+        eres=$?
+        if [[ "$eres" == "124" ]]
+        then
+            allgood="no"
+            {
+                echo "Timeout for $turl"
+                echo -e "\tTimeout for $turl"
+                echo "Timeout running curl for $host:$port/$path"
+            } >> "$logfile"
+        fi
+        if [[ "$eres" != "0" ]]
+        then
+            allgood="no"
+            echo "PN override Error ($eres) for $turl" >> "$logfile"
+            echo -e "\tPN override Error ($eres) for $turl"
+        fi
+        echo "" >> "$logfile"
+    done
+fi
+
+for targ in "${!httpsrr_targets[@]}"
+do
+    host=$(hostport2host "$targ")
+    port=$(hostport2port "$targ")
+    if [[ "$port" != "443" && "$have_portsblocked" == "yes" ]]
+    then
+        echo "Skipping $targ as ports != 443 seem blocked"
+        continue
+    fi
+    path=${httpsrr_targets[$targ]}
+    turl="https://$host:$port/$path"
+    echo "HTTPS RR but no ECHConfig check for $turl"
+    {
+        echo ""
+        echo "HTTPS RR but no ECHConfig check for $turl"
+    } >> "$logfile"
+    timeout "$tout" "$CURL" "${CURL_PARAMS[@]}" --ech true "$turl" >> "$logfile" 2>&1
+    eres=$?
+    if [[ "$eres" == "124" ]]
+    then
+        allgood="no"
+        {
+            echo "Timeout for $turl"
+            echo -e "\tTimeout for $turl"
+            echo "Timeout running curl for $host:$port/$path"
+        } >> "$logfile"
+    fi
+    if [[ "$eres" != "0" ]]
+    then
+        allgood="no"
+        echo "Error ($eres) for $turl" >> "$logfile"
+        echo -e "\tError ($eres) for $turl"
+    fi
+    echo "" >> "$logfile"
+done
+
+for targ in "${!neither_targets[@]}"
+do
+    host=$(hostport2host "$targ")
+    port=$(hostport2port "$targ")
+    if [[ "$port" != "443" && "$have_portsblocked" == "yes" ]]
+    then
+        echo "Skipping $targ as ports != 443 seem blocked"
+        continue
+    fi
+    path=${neither_targets[$targ]}
+    turl="https://$host:$port/$path"
+    echo "Neither HTTPS nor ECHConfig check for $turl"
+    {
+        echo ""
+        echo "Neither HTTPS nor ECHConfig check for $turl"
+    } >> "$logfile"
+    timeout "$tout" "$CURL" "${CURL_PARAMS[@]}" --ech true "$turl" >> "$logfile" 2>&1
+    eres=$?
+    if [[ "$eres" == "124" ]]
+    then
+        allgood="no"
+        {
+            echo "Timeout for $turl"
+            echo -e "\tTimeout for $turl"
+            echo "Timeout running curl for $host:$port/$path"
+        } >> "$logfile"
+    fi
+    if [[ "$eres" != "0" ]]
+    then
+        allgood="no"
+        echo "Error ($eres) for $turl" >> "$logfile"
+        echo -e "\tError ($eres) for $turl"
+    fi
+    echo "" >> "$logfile"
+done
+
+
+# Check various command line options, if we're good so far
+if [[ "$using_ossl" == "yes" && "$allgood" == "yes" ]]
+then
+    # use this test URL as it'll tell us if things worked
+    turl="https://defo.ie/ech-check.php"
+    echo "cli_test with $turl"
+    echo "cli_test with $turl" >> "$logfile"
+    cli_test "$turl" 1 1 --ech true
+    cli_test "$turl" 1 0 --ech false
+    cli_test "$turl" 1 1 --ech false --ech true
+    cli_test "$turl" 1 1 --ech false --ech true --ech pn:foobar
+    cli_test "$turl" 1 1 --ech false --ech pn:foobar --ech true
+    echconfiglist=$(get_ech_configlist defo.ie)
+    cli_test "$turl" 1 1 --ech ecl:"$echconfiglist"
+    cli_test "$turl" 1 0 --ech ecl:
+fi
+
+fi # skip
+
+# Check combinations of command line options, if we're good so far
+# Most of this only works for openssl, which is ok, as we're checking
+# the argument handling here, not the ECH protocol
+if [[ "$using_ossl" == "yes" && "$allgood" == "yes" ]]
+then
+    # ech can be hard, true, grease or false
+    # ecl:ecl can be correct, incorrect or missing
+    # ech:pn can be correct, incorrect or missing
+    # in all cases the "last" argument provided should "win"
+    # but only one of hard, true, grease or false will apply
+    turl="https://defo.ie/ech-check.php"
+    echconfiglist=$(get_ech_configlist defo.ie)
+    goodecl=$echconfiglist
+    echconfiglist=$(get_ech_configlist hidden.hoba.ie)
+    badecl=$echconfiglist
+    goodpn="cover.defo.ie"
+    badpn="hoba.ie"
+    echo "more cli_test with $turl"
+    echo "more cli_test with $turl" >> "$logfile"
+
+    # The combinatorics here are handled via the tests/ech_combos.py script
+    # which produces all the relevant combinations or inputs and orders 
+    # thereof. We have to manually assess whether or not ECH is expected to
+    # work for each case.
+    cli_test "$turl" 0 0
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech ecl:"$badecl"
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech ecl:"$badecl" --ech ecl:"$goodecl"
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech ecl:"$badecl" --ech ecl:"$goodecl" --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech hard
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech ecl:"$badecl" --ech hard --ech ecl:"$goodecl"
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech ecl:"$badecl" --ech hard --ech ecl:"$goodecl" --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech hard --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech hard --ech true
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech ecl:"$badecl" --ech hard --ech true --ech ecl:"$goodecl"
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech ecl:"$badecl" --ech hard --ech true --ech ecl:"$goodecl" --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech hard --ech true --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech pn:$badpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech ecl:"$badecl" --ech pn:$badpn --ech ecl:"$goodecl"
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech ecl:"$badecl" --ech pn:$badpn --ech ecl:"$goodecl" --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech pn:$badpn --ech hard
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech ecl:"$badecl" --ech pn:$badpn --ech hard --ech ecl:"$goodecl"
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech ecl:"$badecl" --ech pn:$badpn --ech hard --ech ecl:"$goodecl" --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech pn:$badpn --ech hard --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech pn:$badpn --ech hard --ech true
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech ecl:"$badecl" --ech pn:$badpn --ech hard --ech true --ech ecl:"$goodecl"
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech ecl:"$badecl" --ech pn:$badpn --ech hard --ech true --ech ecl:"$goodecl" --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech pn:$badpn --ech hard --ech true --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech pn:$badpn --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech pn:$badpn --ech true
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech ecl:"$badecl" --ech pn:$badpn --ech true --ech ecl:"$goodecl"
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech ecl:"$badecl" --ech pn:$badpn --ech true --ech ecl:"$goodecl" --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" - 0 --ech ecl:"$badecl" --ech pn:$badpn --ech true --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech true
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech ecl:"$badecl" --ech true --ech ecl:"$goodecl"
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech ecl:"$badecl" --ech true --ech ecl:"$goodecl" --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech true --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech ecl:"$goodecl"
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech ecl:"$goodecl" --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 0 --ech false
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 0 --ech false --ech ecl:"$badecl"
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 0 --ech false --ech ecl:"$badecl" --ech ecl:"$goodecl"
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 0 --ech false --ech ecl:"$badecl" --ech ecl:"$goodecl" --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech hard
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech false --ech ecl:"$badecl" --ech hard --ech ecl:"$goodecl"
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech false --ech ecl:"$badecl" --ech hard --ech ecl:"$goodecl" --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech hard --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech hard --ech true
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech false --ech ecl:"$badecl" --ech hard --ech true --ech ecl:"$goodecl"
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech false --ech ecl:"$badecl" --ech hard --ech true --ech ecl:"$goodecl" --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech hard --ech true --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 0 --ech false --ech ecl:"$badecl" --ech pn:$badpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 0 --ech false --ech ecl:"$badecl" --ech pn:$badpn --ech ecl:"$goodecl"
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 0 --ech false --ech ecl:"$badecl" --ech pn:$badpn --ech ecl:"$goodecl" --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech pn:$badpn --ech hard
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech false --ech ecl:"$badecl" --ech pn:$badpn --ech hard --ech ecl:"$goodecl"
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech false --ech ecl:"$badecl" --ech pn:$badpn --ech hard --ech ecl:"$goodecl" --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech pn:$badpn --ech hard --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech pn:$badpn --ech hard --ech true
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech false --ech ecl:"$badecl" --ech pn:$badpn --ech hard --ech true --ech ecl:"$goodecl"
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech false --ech ecl:"$badecl" --ech pn:$badpn --ech hard --ech true --ech ecl:"$goodecl" --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech pn:$badpn --ech hard --ech true --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 0 --ech false --ech ecl:"$badecl" --ech pn:$badpn --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech pn:$badpn --ech true
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech false --ech ecl:"$badecl" --ech pn:$badpn --ech true --ech ecl:"$goodecl"
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech false --ech ecl:"$badecl" --ech pn:$badpn --ech true --ech ecl:"$goodecl" --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech pn:$badpn --ech true --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 0 --ech false --ech ecl:"$badecl" --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech true
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech false --ech ecl:"$badecl" --ech true --ech ecl:"$goodecl"
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech false --ech ecl:"$badecl" --ech true --ech ecl:"$goodecl" --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech true --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 0 --ech false --ech ecl:"$goodecl"
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 0 --ech false --ech ecl:"$goodecl" --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech false --ech hard
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech false --ech hard --ech ecl:"$goodecl"
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech false --ech hard --ech ecl:"$goodecl" --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech false --ech hard --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech false --ech hard --ech true
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech false --ech hard --ech true --ech ecl:"$goodecl"
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech false --ech hard --ech true --ech ecl:"$goodecl" --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech false --ech hard --ech true --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 0 --ech false --ech pn:$badpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 0 --ech false --ech pn:$badpn --ech ecl:"$goodecl"
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 0 --ech false --ech pn:$badpn --ech ecl:"$goodecl" --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech false --ech pn:$badpn --ech hard
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech false --ech pn:$badpn --ech hard --ech ecl:"$goodecl"
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech false --ech pn:$badpn --ech hard --ech ecl:"$goodecl" --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech false --ech pn:$badpn --ech hard --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech false --ech pn:$badpn --ech hard --ech true
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech false --ech pn:$badpn --ech hard --ech true --ech ecl:"$goodecl"
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech false --ech pn:$badpn --ech hard --ech true --ech ecl:"$goodecl" --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech false --ech pn:$badpn --ech hard --ech true --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 0 --ech false --ech pn:$badpn --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech false --ech pn:$badpn --ech true
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech false --ech pn:$badpn --ech true --ech ecl:"$goodecl"
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech false --ech pn:$badpn --ech true --ech ecl:"$goodecl" --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech false --ech pn:$badpn --ech true --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 0 --ech false --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech false --ech true
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech false --ech true --ech ecl:"$goodecl"
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech false --ech true --ech ecl:"$goodecl" --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech false --ech true --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech hard
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech hard --ech ecl:"$goodecl"
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech hard --ech ecl:"$goodecl" --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech hard --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech hard --ech true
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech hard --ech true --ech ecl:"$goodecl"
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech hard --ech true --ech ecl:"$goodecl" --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech hard --ech true --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 0 --ech pn:$badpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech pn:$badpn --ech ecl:"$goodecl"
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech pn:$badpn --ech ecl:"$goodecl" --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech pn:$badpn --ech hard
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech pn:$badpn --ech hard --ech ecl:"$goodecl"
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech pn:$badpn --ech hard --ech ecl:"$goodecl" --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech pn:$badpn --ech hard --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech pn:$badpn --ech hard --ech true
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech pn:$badpn --ech hard --ech true --ech ecl:"$goodecl"
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech pn:$badpn --ech hard --ech true --ech ecl:"$goodecl" --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech pn:$badpn --ech hard --ech true --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 0 --ech pn:$badpn --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech pn:$badpn --ech true
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech pn:$badpn --ech true --ech ecl:"$goodecl"
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech pn:$badpn --ech true --ech ecl:"$goodecl" --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech pn:$badpn --ech true --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 0 --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech true
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech true --ech ecl:"$goodecl"
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech true --ech ecl:"$goodecl" --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech true --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 0
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech ecl:"$goodecl"
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech ecl:"$goodecl" --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 0 --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech true
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech true --ech ecl:"$goodecl"
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech true --ech ecl:"$goodecl" --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 1 1 --ech true --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+
+    # a target URL that doesn't support ECH
+    turl="https://tcd.ie"
+    echo "cli_test with $turl"
+    echo "cli_test with $turl" >> "$logfile"
+    # the params below don't matter much here as we'll fail anyway
+    echconfiglist=$(get_ech_configlist defo.ie)
+    goodecl=$echconfiglist
+    badecl="$goodecl"
+    goodpn="tcd.ie"
+    badpn="tcd.ie"
+    cli_test "$turl" 1 0
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech ecl:"$badecl"
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech ecl:"$goodecl"
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech ecl:"$goodecl" --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech hard
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech hard --ech ecl:"$goodecl"
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech hard --ech ecl:"$goodecl" --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech hard --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech hard --ech true
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech hard --ech true --ech ecl:"$goodecl"
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech hard --ech true --ech ecl:"$goodecl" --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech hard --ech true --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech pn:$badpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech pn:$badpn --ech ecl:"$goodecl"
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech pn:$badpn --ech ecl:"$goodecl" --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech pn:$badpn --ech hard
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech pn:$badpn --ech hard --ech ecl:"$goodecl"
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech pn:$badpn --ech hard --ech ecl:"$goodecl" --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech pn:$badpn --ech hard --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech pn:$badpn --ech hard --ech true
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech pn:$badpn --ech hard --ech true --ech ecl:"$goodecl"
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech pn:$badpn --ech hard --ech true --ech ecl:"$goodecl" --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech pn:$badpn --ech hard --ech true --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech pn:$badpn --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech pn:$badpn --ech true
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech pn:$badpn --ech true --ech ecl:"$goodecl"
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech pn:$badpn --ech true --ech ecl:"$goodecl" --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech pn:$badpn --ech true --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech true
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech true --ech ecl:"$goodecl"
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech true --ech ecl:"$goodecl" --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech true --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech ecl:"$goodecl"
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech ecl:"$goodecl" --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech false
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl"
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech ecl:"$goodecl"
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech ecl:"$goodecl" --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech hard
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech hard --ech ecl:"$goodecl"
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech hard --ech ecl:"$goodecl" --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech hard --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech hard --ech true
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech hard --ech true --ech ecl:"$goodecl"
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech hard --ech true --ech ecl:"$goodecl" --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech hard --ech true --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech pn:$badpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech pn:$badpn --ech ecl:"$goodecl"
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech pn:$badpn --ech ecl:"$goodecl" --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech pn:$badpn --ech hard
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech pn:$badpn --ech hard --ech ecl:"$goodecl"
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech pn:$badpn --ech hard --ech ecl:"$goodecl" --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech pn:$badpn --ech hard --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech pn:$badpn --ech hard --ech true
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech pn:$badpn --ech hard --ech true --ech ecl:"$goodecl"
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech pn:$badpn --ech hard --ech true --ech ecl:"$goodecl" --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech pn:$badpn --ech hard --ech true --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech pn:$badpn --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech pn:$badpn --ech true
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech pn:$badpn --ech true --ech ecl:"$goodecl"
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech pn:$badpn --ech true --ech ecl:"$goodecl" --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech pn:$badpn --ech true --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech true
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech true --ech ecl:"$goodecl"
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech true --ech ecl:"$goodecl" --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech true --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech false --ech ecl:"$goodecl"
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech false --ech ecl:"$goodecl" --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech false --ech hard
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech false --ech hard --ech ecl:"$goodecl"
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech false --ech hard --ech ecl:"$goodecl" --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech false --ech hard --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech false --ech hard --ech true
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech false --ech hard --ech true --ech ecl:"$goodecl"
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech false --ech hard --ech true --ech ecl:"$goodecl" --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech false --ech hard --ech true --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech false --ech pn:$badpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech false --ech pn:$badpn --ech ecl:"$goodecl"
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech false --ech pn:$badpn --ech ecl:"$goodecl" --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech false --ech pn:$badpn --ech hard
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech false --ech pn:$badpn --ech hard --ech ecl:"$goodecl"
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech false --ech pn:$badpn --ech hard --ech ecl:"$goodecl" --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech false --ech pn:$badpn --ech hard --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech false --ech pn:$badpn --ech hard --ech true
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech false --ech pn:$badpn --ech hard --ech true --ech ecl:"$goodecl"
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech false --ech pn:$badpn --ech hard --ech true --ech ecl:"$goodecl" --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech false --ech pn:$badpn --ech hard --ech true --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech false --ech pn:$badpn --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech false --ech pn:$badpn --ech true
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech false --ech pn:$badpn --ech true --ech ecl:"$goodecl"
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech false --ech pn:$badpn --ech true --ech ecl:"$goodecl" --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech false --ech pn:$badpn --ech true --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech false --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech false --ech true
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech false --ech true --ech ecl:"$goodecl"
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech false --ech true --ech ecl:"$goodecl" --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech false --ech true --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech hard
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech hard --ech ecl:"$goodecl"
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech hard --ech ecl:"$goodecl" --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech hard --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech hard --ech true
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech hard --ech true --ech ecl:"$goodecl"
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech hard --ech true --ech ecl:"$goodecl" --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech hard --ech true --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech pn:$badpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech pn:$badpn --ech ecl:"$goodecl"
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech pn:$badpn --ech ecl:"$goodecl" --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech pn:$badpn --ech hard
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech pn:$badpn --ech hard --ech ecl:"$goodecl"
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech pn:$badpn --ech hard --ech ecl:"$goodecl" --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech pn:$badpn --ech hard --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech pn:$badpn --ech hard --ech true
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech pn:$badpn --ech hard --ech true --ech ecl:"$goodecl"
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech pn:$badpn --ech hard --ech true --ech ecl:"$goodecl" --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech pn:$badpn --ech hard --ech true --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech pn:$badpn --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech pn:$badpn --ech true
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech pn:$badpn --ech true --ech ecl:"$goodecl"
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech pn:$badpn --ech true --ech ecl:"$goodecl" --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech pn:$badpn --ech true --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech true
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech true --ech ecl:"$goodecl"
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech true --ech ecl:"$goodecl" --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech true --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech ecl:"$goodecl"
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech ecl:"$goodecl" --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech true
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech true --ech ecl:"$goodecl"
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech true --ech ecl:"$goodecl" --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+    cli_test "$turl" 0 0 --ech true --ech pn:$goodpn
+    if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi
+fi
+
+
+END=$(whenisitagain)
+echo "Finished $0 at $END"  >> "$logfile"
+echo "-----" >> "$logfile"
+
+if [[ "$allgood" == "yes" ]]
+then
+    echo "Finished $0 at $END"
+    echo "All good, log in $logfile"
+    exit 0
+else
+    echo "Finished $0 at $END"
+    echo "NOT all good, log in $logfile"
+fi
+
+# send a mail to root (will be fwd'd) but just once every 24 hours
+# 'cause we only really need "new" news
+itsnews="yes"
+age_of_news=0
+if [ -f "$LTOP"/bad_runs ]
+then
+    age_of_news=$(fileage "$LTOP"/bad_runs)
+    # only consider news "new" if we haven't mailed today
+    if ((age_of_news < 24*3600))
+    then
+        itsnews="no"
+    fi
+fi
+if [[ "$DOMAIL" == "yes" && "$itsnews" == "yes" ]]
+then
+    echo "ECH badness at $NOW" | mail -s "ECH badness at $NOW" root
+fi
+# add to list of bad runs (updating file age)
+echo "ECH badness at $NOW" >>"$LTOP"/bad_runs
+exit 2
+