Merge master into 10-Dev

Conflicts:
      plugins/lua/ts_lua_http_config.c
      tests/gold_tests/autest-site/trafficserver.test.ext
diff --git a/build/wavm.m4 b/build/wavm.m4
deleted file mode 100644
index 7206eeb..0000000
--- a/build/wavm.m4
+++ /dev/null
@@ -1,170 +0,0 @@
-dnl -------------------------------------------------------- -*- autoconf -*-
-dnl Licensed to the Apache Software Foundation (ASF) under one or more
-dnl contributor license agreements.  See the NOTICE file distributed with
-dnl this work for additional information regarding copyright ownership.
-dnl The ASF licenses this file to You under the Apache License, Version 2.0
-dnl (the "License"); you may not use this file except in compliance with
-dnl the License.  You may obtain a copy of the License at
-dnl
-dnl     http://www.apache.org/licenses/LICENSE-2.0
-dnl
-dnl Unless required by applicable law or agreed to in writing, software
-dnl distributed under the License is distributed on an "AS IS" BASIS,
-dnl WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-dnl See the License for the specific language governing permissions and
-dnl limitations under the License.
-
-dnl
-dnl wavm.m4: Trafficserver's wavm autoconf macros
-dnl
-
-dnl
-dnl TS_CHECK_WAVM: look for wavm libraries and headers
-dnl
-AC_DEFUN([TS_CHECK_WAVM], [
-has_wavm=0
-AC_ARG_WITH(wavm, [AS_HELP_STRING([--with-wavm=DIR], [use specific WAVM library])],
-[
-if test "x$withval" != "xyes" && test "x$withval" != "x"; then
-  wavm_base_dir="$withval"
-  if test "$withval" != "no"; then
-    has_wavm=1
-
-    case "$withval" in
-    *":"*)
-      wavm_include="`echo $withval | sed -e 's/:.*$//'`"
-      wavm_ldflags="`echo $withval | sed -e 's/^.*://'`"
-      AC_MSG_CHECKING(checking for WAVM includes in $wavm_include libs in $wavm_ldflags)
-      ;;
-    *)
-      dir="$withval/include/WAVM"
-      AC_MSG_CHECKING(checking for wavm in $dir)
-      if test -d $dir; then
-          AC_MSG_RESULT([ok])
-          wavm_include=$dir
-        else
-          AC_MSG_RESULT([not found])
-      fi
-
-      if test "x$wavm_include" = "x"; then
-          AC_MSG_ERROR([*** could not find wavm include dir ***])
-      fi
-
-      wavm_ldflags="$withval/lib"
-      ;;
-  esac
-
-  fi
-fi
-
-if test -d $wavm_include && test -d $wavm_ldflags && test -f $wavm_include/Platform/Defines.h; then
-  AC_MSG_RESULT([$wavm_include/WASM/WASM.h found ok])
-else
-  AC_MSG_RESULT([$wavm_include/WASM/WASM.h not found])
-fi
-
-if test "$has_wavm" != "0"; then
-  saved_ldflags=$LDFLAGS
-  saved_cppflags=$CPPFLAGS
-  wavm_have_headers=0
-  wavm_have_libs=0
-
-  TS_ADDTO(CPPFLAGS, [-I${wavm_include}])
-  if test "$wavm_base_dir" != "/usr"; then
-    TS_ADDTO(LDFLAGS, [-L${wavm_ldflags}])
-    TS_ADDTO_RPATH(${wavm_ldflags})
-  fi
-
-  AC_CHECK_LIB([WAVM], wasm_module_new, [wavm_have_libs=1])
-  if test "$wavm_have_libs" == "1"; then
-    AC_LANG_PUSH([C++])
-    AC_CHECK_HEADERS([Platform/Defines.h], [wavm_have_headers=1])
-    AC_LANG_POP([C++])
-  fi
-
-  if test "$wavm_have_headers" == "1"; then
-    AC_SUBST([WAVM_LDFLAGS], ["-L${wavm_ldflags} -lWAVM"])
-    AC_SUBST([WAVM_CPPFLAGS], [-I${wavm_include}])
-    AC_DEFINE([WAVM_API], [], [Define to enable WAVM API])
-    enable_wavm=yes
-  else
-    has_wavm=0
-    AC_MSG_ERROR([*** wavm requested but either libWAVM or WAVM/Platform/Defines.h cannot be found ***])
-  fi
-
-  CPPFLAGS=$saved_cppflags
-  LDFLAGS=$saved_ldflags
-fi
-],
-[
-# add pkg-config search
-#
-
-PKG_CHECK_MODULES([WAVM], [wavm >= 1.0.0], [
-   AC_SUBST([WAVM_LDFLAGS], [$WAVM_LIBS])
-   AC_SUBST([WAVM_CPPFLAGS], [$WAVM_CFLAGS])
-   enable_wavm=yes
-],
-[
-# look in /usr and /usr/local for what we need
-#
-
-AC_MSG_CHECKING([for WAVM location])
-  for wavm_prefix in /usr/local /usr; do
-    dir="$wavm_prefix/include/WAVM"
-
-    if test -d $dir; then
-      wavm_base_dir=$wavm_prefix
-      wavm_include=$dir
-      wavm_ldflags=$wavm_prefix/lib
-      break
-    fi
-  done
-
-  if test "x$wavm_base_dir" = "x"; then
-    enable_wavm=no
-    AC_MSG_RESULT([$dir not found])
-  else
-    enable_wavm=yes
-    AC_MSG_RESULT([$dir found])
-  fi
-
-if test "$enable_wavm" != "no"; then
-  saved_ldflags=$LDFLAGS
-  saved_cppflags=$CPPFLAGS
-  wavm_have_headers=0
-  wavm_have_libs=0
-
-  TS_ADDTO(CPPFLAGS, [-I${wavm_include}])
-  if test "$wavm_base_dir" != "/usr"; then
-    TS_ADDTO(LDFLAGS, [-L${wavm_ldflags}])
-    TS_ADDTO_RPATH(${wavm_ldflags})
-  fi
-
-  AC_CHECK_LIB([WAVM], wasm_module_new, [wavm_have_libs=1])
-  if test "$wavm_have_libs" == "1"; then
-    AC_LANG_PUSH([C++])
-    AC_CHECK_HEADERS([Platform/Defines.h], [wavm_have_headers=1])
-    AC_LANG_POP([C++])
-  fi
-
-  if test "$wavm_have_headers" == "1"; then
-    AC_SUBST([WAVM_LDFLAGS], ["-L${wavm_ldflags} -lWAVM"])
-    AC_SUBST([WAVM_CPPFLAGS], [-I${wavm_include}])
-    AC_DEFINE([WAVM_API], [], [Define to enable WAVM API])
-    enable_wavm=yes
-  else
-    has_wavm=0
-  fi
-
-  CPPFLAGS=$saved_cppflags
-  LDFLAGS=$saved_ldflags
-fi
-
-])
-])
-
-TS_ARG_ENABLE_VAR([has],[wavm])
-AM_CONDITIONAL([HAS_WAVM], [test 0 -ne $has_wavm])
-
-])
diff --git a/configure.ac b/configure.ac
index 70ce422..cdca9a3 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1856,6 +1856,21 @@
 AM_CONDITIONAL([HAS_OTEL], [test "x${has_otel}" = "x1" ])
 AC_LANG_POP([C++])
 
+# Checking if wamr is available
+AC_LANG_PUSH([C++])
+AC_CHECK_HEADERS([wasm_c_api.h], [
+  AC_CHECK_LIB([iwasm], [main], [
+    AC_SUBST([WAMR_LIBS], [" -liwasm "])
+    AC_SUBST(has_wamr, 1)
+  ], [
+    AC_SUBST([WAMR_LIBS], [""])
+    AC_SUBST(has_wamr, 0)
+  ])
+])
+
+AM_CONDITIONAL([HAS_WAMR], [test "x${has_wamr}" = "x1" ])
+AC_LANG_POP([C++])
+
 # Right now, the healthcheck plugins requires inotify_init (and friends)
 AM_CONDITIONAL([BUILD_HEALTHCHECK_PLUGIN], [ test "$ac_cv_func_inotify_init" = "yes" ])
 
diff --git a/doc/Pipfile b/doc/Pipfile
index d481b2a..ba2b3f1 100644
--- a/doc/Pipfile
+++ b/doc/Pipfile
@@ -23,18 +23,14 @@
 
 [packages]
 
-# The latest 4.x sphinx release has issues with style rendering. The 3.x
-# releases build and render fine, however. So we currently pin to that.
-#
-# Once that issue, either with sphinx or our docs, is resolved, then we should
-# unpin sphinx by setting the following to "*".
-sphinx = "==3.*"
+# We pin Sphinx because the sphinx-rtd-theme suggests this as a best practice.
+# If not, we will often face issues when Sphinx updates their version before
+# sphinx-rtd-theme has had time to update their component for the new sphinx
+# version.
+sphinx = "==6.1.3"
 
-# Sphinx 3.x builds break with the latest jinja2. This jinja2 pin can be
-# removed when we move to Sphinx 4.x.
-jinja2 = "<3.1"
-
-sphinx-rtd-theme = "*"
+sphinx-rtd-theme = ">=1.*"
+sphinxcontrib-jquery = "*"
 sphinxcontrib-plantuml = "*"
 # i18n
 sphinx-intl = "*"
diff --git a/doc/admin-guide/files/records.config.en.rst b/doc/admin-guide/files/records.config.en.rst
index 461a055..5f4080f 100644
--- a/doc/admin-guide/files/records.config.en.rst
+++ b/doc/admin-guide/files/records.config.en.rst
@@ -457,6 +457,7 @@
 
 .. ts:cv:: CONFIG proxy.config.net.default_inactivity_timeout INT 86400
    :reloadable:
+   :overridable:
 
    The connection inactivity timeout (in seconds) to apply when
    |TS| detects that no inactivity timeout has been applied
@@ -464,6 +465,17 @@
    `proxy.process.net.default_inactivity_timeout_applied` metric
    is incremented.
 
+   Note that this configuration is overridable. While most overridable
+   configurations conceptually apply to specific transactions,
+   ``default_inactivity_timeout`` is a connection level concept. This is not
+   necessarily a problem, but it does mean that care must be taken when
+   applying the override to consider that all transactions in the connection
+   which has this timeout overriden will be impacted by the override. For
+   instance, if the default inactivity timeout is being overridden via a
+   :ref:`admin-plugins-conf-remap` rule in :file:`remap.config`, then all
+   transactions for that connection will be impacted by the override, not just
+   the ones matching that ``remap.config`` rule.
+
    See :ref:`admin-performance-timeouts` for more discussion on |TS| timeouts.
 
 .. ts:cv:: CONFIG proxy.config.net.inactivity_check_frequency INT 1
diff --git a/doc/admin-guide/plugins/authproxy.en.rst b/doc/admin-guide/plugins/authproxy.en.rst
index b17984a..c788c63 100644
--- a/doc/admin-guide/plugins/authproxy.en.rst
+++ b/doc/admin-guide/plugins/authproxy.en.rst
@@ -80,6 +80,11 @@
   The TCP port of the authorization host. This is only used by the
   ``redirect`` transform.
 
+--forward-header-prefix=PREFIX
+  If the option is enabled, authentication response header fields, which
+  contain the specified prefix, will be sent to the original server in
+  the original request.
+
 --force-cacheability
   If this options is set, the plugin will allow Traffic Server to
   cache the result of authorized requests. In the normal case, requests
@@ -106,10 +111,11 @@
 
 
 In this example, the request is directed to a local authentication server
-that authorizes the request based on internal policy rules::
+that authorizes the request based on internal policy rules. Authentication response
+headers with the prefix will be proxied to the original server::
 
   map http://cache.example.com http://origin.internal.com/ \
-    @plugin=authproxy.so @pparam=--auth-transform=redirect @pparam=--auth-host=127.0.0.1 @pparam=--auth-port=9000
+    @plugin=authproxy.so @pparam=--auth-transform=redirect @pparam=--auth-host=127.0.0.1 @pparam=--auth-port=9000 @pparam=--forward-header-prefix=x-requested
 
   map http://origin.internal.com/ http://origin.internal.com/ \
-    @plugin=authproxy.so @pparam=--auth-transform=redirect @pparam=--auth-host=127.0.0.1 @pparam=--auth-port=9000
+    @plugin=authproxy.so @pparam=--auth-transform=redirect @pparam=--auth-host=127.0.0.1 @pparam=--auth-port=9000 @pparam=--forward-header-prefix=x-requested
diff --git a/doc/admin-guide/plugins/certifier.en.rst b/doc/admin-guide/plugins/certifier.en.rst
index 841f5a3..3527a3a 100644
--- a/doc/admin-guide/plugins/certifier.en.rst
+++ b/doc/admin-guide/plugins/certifier.en.rst
@@ -37,7 +37,7 @@
 When the plugin sees an incoming HTTPS request, it does:
 
 #. Look up the SNI in `SslLRUList` and set up the context if a valid context exists. Otherwise, it will schedule a thread to retrieve such context from disk (or generate). If such a thread is already scheduled, it will put this SSL connection onto the queue.
-#. The retriever thread will first try to load the cert from disk. If no such certs exist and dynamic generation is enabled, the thread will generate a Certificate Signing Request (CSR) with provided SNI and sign it with root CA passed in from config options. The signed certificate is then written to the disk and all queued up SSL connections are set up with correct context and enabled.
+#. The retriever thread will first try to load the cert from disk. If no such certs exist and dynamic generation is enabled, the thread will generate a certificate with provided SNI and sign it with root CA passed in from config options. The signed certificate is then written to the disk and all queued up SSL connections are set up with correct context and enabled.
 
 Setup
 =====
@@ -77,7 +77,7 @@
 =============
 To use this plugin, enable it in a :file:`plugin.config` rule, specifying certificates storage path, max number of certificates in memory, and signing cert+key+serial. For example:
 
-   certifier.so --store=/home/zeyuan/certifier/certs --max=1000 --sign-cert=/home/zeyuan/certifier/root-ca.crt --sign-key=/home/zeyuan/certifier/root-ca.key --sign-serial=/home/zeyuan/certifier/ca-serial.txt
+   ``certifier.so --store=/home/zeyuan/certifier/certs --max=1000 --sign-cert=/home/zeyuan/certifier/root-ca.crt --sign-key=/home/zeyuan/certifier/root-ca.key --sign-serial=/home/zeyuan/certifier/ca-serial.txt``
 
 One use case would be routing incoming CONNECT request to another port on |TS|. With the certifier generating a trusted certificate, other plugins can act with a similar behavior to Man-In-The-Middle (logging interesting data for example).
 
diff --git a/doc/admin-guide/plugins/wasm.en.rst b/doc/admin-guide/plugins/wasm.en.rst
index d67dd7c..6c3f0f5 100644
--- a/doc/admin-guide/plugins/wasm.en.rst
+++ b/doc/admin-guide/plugins/wasm.en.rst
@@ -26,7 +26,9 @@
 This plugins allows WebAssembly/Wasm modules compiled by Proxy-Wasm SDKs to be used in ATS.
 
 See the documentation on specification for Proxy-Wasm at https://github.com/proxy-wasm/spec
+
 C++ SDK is at https://github.com/proxy-wasm/proxy-wasm-cpp-sdk
+
 Rust SDK is at https://github.com/proxy-wasm/proxy-wasm-rust-sdk
 
 How it Works
@@ -34,10 +36,10 @@
 
 The plugin uses the library and header files from the Proxy-Wasm project.
 
-* https://github.com/proxy-wasm/proxy-wasm-cpp-host/tree/73fe589869a0effb0b6e2ed5d018ce8d7768a265
+* https://github.com/proxy-wasm/proxy-wasm-cpp-host/tree/72ce32f7b11f9190edf874028255e1309e41690f
 * https://github.com/proxy-wasm/proxy-wasm-cpp-sdk/tree/fd0be8405db25de0264bdb78fae3a82668c03782
 
-Proxy-Wasm in turn uses an underlying WebAssembly runtime to execute the WebAssembly module. (Currently only WAVM is supported)
+Proxy-Wasm in turn uses an underlying WebAssembly runtime to execute the WebAssembly module. (Currently only WAMR is supported)
 
 The plugin creates a root context when ATS starts and a new context will be created out of the root context for each
 transaction. ATS plugin events will trigger the corresponding functions in the WebAssembly module to be executed through
@@ -47,19 +49,21 @@
 Compiling the Plugin
 ====================
 
-* WAVM runtime needs CMake and LLVM9.0+
-* Compile and install WAVM
+* WAMR runtime needs CMake
+* Compile and install WAMR
 
 ::
 
-  git clone git@github.com:WAVM/WAVM.git
-  cd WAVM
-  git co nightly/2021-05-10 -b wasm
-  cmake "." -DCMAKE_PREFIX_PATH=/usr/lib64/llvm9.0/lib/cmake/llvm
+  wget https://github.com/bytecodealliance/wasm-micro-runtime/archive/c3d66f916ef8093e5c8cacf3329ed968f807cf58.tar.gz
+  tar zxvf c3d66f916ef8093e5c8cacf3329ed968f807cf58.tar.gz
+  cd wasm-micro-runtime-c3d66f916ef8093e5c8cacf3329ed968f807cf58
+  cp core/iwasm/include/* /usr/local/include/
+  cd product-mini/platforms/linux
+  mkdir build
+  cd build
+  cmake .. -DWAMR_BUILD_INTERP=1 -DWAMR_BUILD_FAST_INTERP=1 -DWAMR_BUILD_JIT=0 -DWAMR_BUILD_AOT=0 -DWAMR_BUILD_SIMD=0 -DWAMR_BUILD_MULTI_MODULE=1 -DWAMR_BUILD_LIBC_WASI=0 -DWAMR_BUILD_TAIL_CALL=1 -DWAMR_DISABLE_HW_BOUND_CHECK=1 -DWAMR_BUILD_BULK_MEMORY=1 -DWAMR_BUILD_WASM_CACHE=0
   make
   sudo make install
-  sudo ldconfig
-  sudo cp /usr/local/lib64/libWAVM.so.0.0.0 /lib64/libWAVM.so.0.0.0
 
 * Configure ATS to compile with experimental plugins
 
@@ -73,13 +77,13 @@
 Examples
 ========
 
-Follow the C++ and Rust examples in the examples directory. Instructions are included on how to compile and use
+Follow the C++, Rust and TinyGo examples in the examples directory. Instructions are included on how to compile and use
 generated wasm modules with the plugin.
 
 TODO
 ====
 
-* Currently only the WAVM runtime is supported. We need to also support V8, WAMR, Wasmtime, and WasmEdge as well.
+* Currently only the WAMR runtime is supported. We should also support V8, Wasmtime, and WasmEdge.
 * Need to support functionality for retrieving and setting request/response body
 * Need to support functionality for making async request call
 * Need to support L4 lifecycle handler functions
diff --git a/doc/conf.py b/doc/conf.py
index 88ddc91..89f417a 100644
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -49,8 +49,14 @@
 sys.path.insert(0, os.path.abspath('ext'))
 sys.path.insert(0, os.path.abspath('.'))
 
+
+# Allow for us to add our override CSS file (new with Sphinx 1.x)
+def setup(app):
+    app.add_css_file('override.css')
+
 # -- General configuration -----------------------------------------------------
 
+
 # Add any Sphinx extension module names here, as strings. They can be extensions
 # coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
 extensions = [
@@ -61,6 +67,7 @@
     'sphinx.ext.coverage',
     'sphinx.ext.viewcode',
     'sphinxcontrib.plantuml',
+    'sphinxcontrib.jquery',
     'traffic-server',
 ]
 
@@ -280,22 +287,6 @@
 # so a file named "default.css" will overwrite the builtin "default.css".
 html_static_path = ['static']
 
-# Include a stylesheet that overrides default table styling, to provide
-# content wrapping.
-html_context = {
-    'css_files': [
-        '_static/override.css'
-    ]
-}
-if os.environ.get('READTHEDOCS', None) == 'True':
-    html_context = {
-        'css_files': [
-            'https://media.readthedocs.org/css/sphinx_rtd_theme.css',
-            'https://media.readthedocs.org/css/readthedocs-doc-embed.css',
-            '_static/override.css'
-        ]
-    }
-
 # If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
 # using the given strftime format.
 #html_last_updated_fmt = '%b %d, %Y'
diff --git a/doc/developer-guide/api/functions/TSHttpOverridableConfig.en.rst b/doc/developer-guide/api/functions/TSHttpOverridableConfig.en.rst
index aa374fc..c377212 100644
--- a/doc/developer-guide/api/functions/TSHttpOverridableConfig.en.rst
+++ b/doc/developer-guide/api/functions/TSHttpOverridableConfig.en.rst
@@ -166,6 +166,7 @@
 :c:enumerator:`TS_CONFIG_NET_SOCK_PACKET_MARK_OUT`                        :ts:cv:`proxy.config.net.sock_packet_mark_out`
 :c:enumerator:`TS_CONFIG_NET_SOCK_PACKET_TOS_OUT`                         :ts:cv:`proxy.config.net.sock_packet_tos_out`
 :c:enumerator:`TS_CONFIG_NET_SOCK_RECV_BUFFER_SIZE_OUT`                   :ts:cv:`proxy.config.net.sock_recv_buffer_size_out`
+:c:enumerator:`TS_CONFIG_NET_DEFAULT_INACTIVITY_TIMEOUT`                  :ts:cv:`proxy.config.net.default_inactivity_timeout`
 :c:enumerator:`TS_CONFIG_NET_SOCK_SEND_BUFFER_SIZE_OUT`                   :ts:cv:`proxy.config.net.sock_send_buffer_size_out`
 :c:enumerator:`TS_CONFIG_PARENT_FAILURES_UPDATE_HOSTDB`                   :ts:cv:`proxy.config.http.parent_proxy.mark_down_hostdb`
 :c:enumerator:`TS_CONFIG_SRV_ENABLED`                                     :ts:cv:`proxy.config.srv_enabled`
diff --git a/doc/developer-guide/api/types/TSOverridableConfigKey.en.rst b/doc/developer-guide/api/types/TSOverridableConfigKey.en.rst
index ac20fdf..9b30e52 100644
--- a/doc/developer-guide/api/types/TSOverridableConfigKey.en.rst
+++ b/doc/developer-guide/api/types/TSOverridableConfigKey.en.rst
@@ -153,6 +153,7 @@
 .. c:enumerator:: TS_CONFIG_PLUGIN_VC_DEFAULT_BUFFER_WATER_MARK
 .. c:enumerator:: TS_CONFIG_NET_SOCK_NOTSENT_LOWAT
 .. c:enumerator:: TS_CONFIG_BODY_FACTORY_RESPONSE_SUPPRESSION_MODE
+.. c:enumerator:: TS_CONFIG_NET_DEFAULT_INACTIVITY_TIMEOUT
 
 
 Description
diff --git a/doc/ext/traffic-server.py b/doc/ext/traffic-server.py
index 92b57a2..71edfb6 100644
--- a/doc/ext/traffic-server.py
+++ b/doc/ext/traffic-server.py
@@ -106,7 +106,7 @@
         title['first'] = False
         title['objtype'] = 'cv'
         self.add_name(title)
-        title.set_class('ts-cv-title')
+        title['classes'].append('ts-cv-title')
 
         # Finally, add a desc_name() node to display the name of the
         # configuration variable.
@@ -115,7 +115,7 @@
         node.append(title)
 
         if ('class' in self.options):
-            title.set_class(self.options.get('class'))
+            title['classes'].append(self.options.get('class'))
         # This has to be a distinct node before the title. if nested then
         # the browser will scroll forward to just past the title.
         nodes.target('', '', names=[cv_name])
@@ -244,7 +244,7 @@
         title['first'] = False
         title['objtype'] = 'stat'
         self.add_name(title)
-        title.set_class('ts-stat-title')
+        title['classes'].append('ts-stat-title')
 
         # Finally, add a desc_name() node to display the name of the
         # configuration variable.
diff --git a/doc/static/languages.json b/doc/static/languages.json
index b1f1e54..803d21c 100644
--- a/doc/static/languages.json
+++ b/doc/static/languages.json
@@ -3,6 +3,7 @@
         "name": "English",
         "versions": [
             "latest",
+            "9.2.x",
             "9.1.x",
             "9.0.x",
             "8.1.x",
@@ -13,6 +14,7 @@
         "name": "日本語",
         "versions": [
             "latest",
+            "9.2.x",
             "9.1.x",
             "9.0.x",
             "8.1.x",
diff --git a/include/ts/apidefs.h.in b/include/ts/apidefs.h.in
index dcefc36..519bca4 100644
--- a/include/ts/apidefs.h.in
+++ b/include/ts/apidefs.h.in
@@ -894,6 +894,7 @@
   TS_CONFIG_BODY_FACTORY_RESPONSE_SUPPRESSION_MODE,
   TS_CONFIG_HTTP_ENABLE_PARENT_TIMEOUT_MARKDOWNS,
   TS_CONFIG_HTTP_DISABLE_PARENT_MARKDOWNS,
+  TS_CONFIG_NET_DEFAULT_INACTIVITY_TIMEOUT,
   TS_CONFIG_LAST_ENTRY
 } TSOverridableConfigKey;
 
diff --git a/include/tscore/History.h b/include/tscore/History.h
index de8feb7..203e08a 100644
--- a/include/tscore/History.h
+++ b/include/tscore/History.h
@@ -32,8 +32,8 @@
 
 struct HistoryEntry {
   SourceLocation location;
-  unsigned short event = 0;
-  short reentrancy     = 0;
+  unsigned short event;
+  short reentrancy;
 };
 
 template <unsigned Count> class History
diff --git a/include/tscore/Ptr.h b/include/tscore/Ptr.h
index 27a0221..c2ff69a 100644
--- a/include/tscore/Ptr.h
+++ b/include/tscore/Ptr.h
@@ -113,25 +113,25 @@
   // Making this explicit avoids unwanted conversions.  See https://en.wikibooks.org/wiki/More_C%2B%2B_Idioms/Safe_bool .
   explicit operator bool() const { return m_ptr != nullptr; }
 
-  int
+  bool
   operator==(const T *p)
   {
     return (m_ptr == p);
   }
 
-  int
+  bool
   operator==(const Ptr<T> &p)
   {
     return (m_ptr == p.m_ptr);
   }
 
-  int
+  bool
   operator!=(const T *p)
   {
     return (m_ptr != p);
   }
 
-  int
+  bool
   operator!=(const Ptr<T> &p)
   {
     return (m_ptr != p.m_ptr);
diff --git a/include/tscore/SourceLocation.h b/include/tscore/SourceLocation.h
index e3af488..7e841dd 100644
--- a/include/tscore/SourceLocation.h
+++ b/include/tscore/SourceLocation.h
@@ -34,9 +34,9 @@
 class SourceLocation
 {
 public:
-  const char *file = nullptr;
-  const char *func = nullptr;
-  int line         = 0;
+  const char *file;
+  const char *func;
+  int line;
 
   SourceLocation()                          = default;
   SourceLocation(const SourceLocation &rhs) = default;
diff --git a/include/tscpp/util/TextView.h b/include/tscpp/util/TextView.h
index 77ede19..15638b7 100644
--- a/include/tscpp/util/TextView.h
+++ b/include/tscpp/util/TextView.h
@@ -33,6 +33,7 @@
 #include <string>
 #include <string_view>
 #include <limits>
+#include <cstdint>
 
 #include "swoc/string_view_util.h"
 
diff --git a/iocore/cache/Cache.cc b/iocore/cache/Cache.cc
index eb094b6..cb64b6e 100644
--- a/iocore/cache/Cache.cc
+++ b/iocore/cache/Cache.cc
@@ -453,7 +453,7 @@
   }
 
   MIMEField *field = ainfo->m_alt->m_response_hdr.field_find(MIME_FIELD_CONTENT_LENGTH, MIME_LEN_CONTENT_LENGTH);
-  if (field && !field->value_get_int64()) {
+  if ((field && !field->value_get_int64()) || ainfo->m_alt->m_response_hdr.status_get() == HTTP_STATUS_NO_CONTENT) {
     f.allow_empty_doc = 1;
     // Set the object size here to zero in case this is a cache replace where the new object
     // length is zero but the old object was not.
@@ -997,6 +997,7 @@
           vol_total_cache_bytes = gvol[i]->len - gvol[i]->dirlen();
           total_cache_bytes += vol_total_cache_bytes;
           CACHE_VOL_SUM_DYN_STAT(cache_bytes_total_stat, vol_total_cache_bytes);
+          CACHE_VOL_SUM_DYN_STAT(cache_stripes_stat, 1); // Count the cache stripes
           Debug("cache_init", "CacheProcessor::cacheInitialized - total_cache_bytes = %" PRId64 " = %" PRId64 "Mb",
                 total_cache_bytes, total_cache_bytes / (1024 * 1024));
 
@@ -2688,6 +2689,7 @@
   int volume_number;
   off_t size_in_blocks;
   ConfigVol *config_vol;
+  int assignedVol = 0; // Number of assigned volumes
 
   gnvol = 0;
   if (config_volumes.num_volumes == 0) {
@@ -2725,7 +2727,6 @@
       cp->num_vols += dp[0]->num_volblocks;
       cp->disk_vols[i] = dp[0];
     }
-
   } else {
     for (int i = 0; i < gndisks; i++) {
       if (gdisks[i]->header->num_volumes == 1 && gdisks[i]->disk_vols[0]->vol_number == 0) {
@@ -2746,6 +2747,8 @@
       // in such a way forced volumes will not impact volume percentage calculations.
       if (-1 == gdisks[i]->forced_volume_num) {
         tot_space_in_blks += (gdisks[i]->num_usable_blocks / blocks_per_vol) * blocks_per_vol;
+      } else {
+        ++assignedVol;
       }
     }
 
@@ -2913,6 +2916,9 @@
       gnvol += cp->num_vols;
     }
   }
+
+  GLOBAL_CACHE_SET_DYN_STAT(cache_stripes_stat, gnvol + assignedVol);
+
   return 0;
 }
 
@@ -3053,6 +3059,7 @@
   reg_int("bytes_used", cache_bytes_used_stat, rsb, prefix, cache_stats_bytes_used_cb);
 
   REG_INT("bytes_total", cache_bytes_total_stat);
+  REG_INT("stripes", cache_stripes_stat);
   REG_INT("ram_cache.total_bytes", cache_ram_cache_bytes_total_stat);
   REG_INT("ram_cache.bytes_used", cache_ram_cache_bytes_stat);
   REG_INT("ram_cache.hits", cache_ram_cache_hits_stat);
diff --git a/iocore/cache/P_CacheInternal.h b/iocore/cache/P_CacheInternal.h
index 22c8783..e6c2584 100644
--- a/iocore/cache/P_CacheInternal.h
+++ b/iocore/cache/P_CacheInternal.h
@@ -100,6 +100,7 @@
 enum {
   cache_bytes_used_stat,
   cache_bytes_total_stat,
+  cache_stripes_stat,
   cache_ram_cache_bytes_stat,
   cache_ram_cache_bytes_total_stat,
   cache_direntries_total_stat,
diff --git a/iocore/eventsystem/UnixEventProcessor.cc b/iocore/eventsystem/UnixEventProcessor.cc
index 50b2240..200ef71 100644
--- a/iocore/eventsystem/UnixEventProcessor.cc
+++ b/iocore/eventsystem/UnixEventProcessor.cc
@@ -161,7 +161,7 @@
 ThreadAffinityInitializer::setup_stack_guard(void *stack, int stackguard_pages)
 {
 #if !(defined(__i386__) || defined(__x86_64__) || defined(__arm__) || defined(__arm64__) || defined(__aarch64__) || \
-      defined(__mips__))
+      defined(__mips__) || defined(__powerpc64__))
 #error Unknown stack growth direction.  Determine the stack growth direction of your platform.
 // If your stack grows upwards, you need to change this function and the calculation of stack_begin in do_alloc_stack.
 #endif
diff --git a/iocore/net/I_NetVConnection.h b/iocore/net/I_NetVConnection.h
index 8ecf17b..1389beb 100644
--- a/iocore/net/I_NetVConnection.h
+++ b/iocore/net/I_NetVConnection.h
@@ -598,9 +598,9 @@
   virtual void cancel_active_timeout() = 0;
 
   /**
-    Clears the inactivity timeout. No inactivity timeouts will be
-    sent until set_inactivity_timeout() is used to reset the
-    inactivity timeout.
+    Clears the inactivity timeout. No inactivity timeouts will be sent (aside
+    from the default inactivity timeout) until set_inactivity_timeout() is used
+    to reset the inactivity timeout.
 
   */
   virtual void cancel_inactivity_timeout() = 0;
diff --git a/iocore/net/NetEvent.h b/iocore/net/NetEvent.h
index 4251bcc..27808bf 100644
--- a/iocore/net/NetEvent.h
+++ b/iocore/net/NetEvent.h
@@ -23,6 +23,8 @@
 
 #pragma once
 
+#include <atomic>
+
 #include "I_EventSystem.h"
 
 class NetHandler;
@@ -71,13 +73,34 @@
   int error      = 0;
   NetHandler *nh = nullptr;
 
-  ink_hrtime inactivity_timeout_in      = 0;
-  ink_hrtime active_timeout_in          = 0;
-  ink_hrtime next_inactivity_timeout_at = 0;
-  ink_hrtime next_activity_timeout_at   = 0;
-  ink_hrtime submit_time                = 0;
+  /** The explicitly set inactivity timeout duration in seconds.
+   *
+   * 0 means no timeout.
+   */
+  ink_hrtime inactivity_timeout_in = 0;
 
-  bool default_inactivity_timeout = false;
+  /** The fallback inactivity timeout which is applied if no other timeouts are
+   * set. That is, this timeout is used if inactivity_timeout_in is 0.
+   *
+   * A value of 0 means no timeout. A value of -1 means that no default timeout
+   * has been set yet. This is initialized to -1 instead of 0 so that the
+   * inactivity cop can distinguish between no value having been set and a
+   * value of 0 having been set by some override plugin.
+   */
+  std::atomic<ink_hrtime> default_inactivity_timeout_in = -1;
+
+  /** The active timeout duration in seconds. */
+  ink_hrtime active_timeout_in = 0;
+
+  /** The time of the next inactivity timeout. */
+  ink_hrtime next_inactivity_timeout_at = 0;
+
+  /** The time of the next activity timeout. */
+  ink_hrtime next_activity_timeout_at = 0;
+  ink_hrtime submit_time              = 0;
+
+  /** Whether the current timeout is a default inactivity timeout. */
+  bool use_default_inactivity_timeout = false;
 
   LINK(NetEvent, open_link);
   LINK(NetEvent, cop_link);
diff --git a/iocore/net/ProxyProtocol.cc b/iocore/net/ProxyProtocol.cc
index 0b99355..c18f563 100644
--- a/iocore/net/ProxyProtocol.cc
+++ b/iocore/net/ProxyProtocol.cc
@@ -356,6 +356,7 @@
     bw.fill(len);
   }
 
+  Debug("proxyprotocol_v1", "Proxy Protocol v1: %.*s", static_cast<int>(bw.size()), bw.data());
   bw.write("\r\n");
 
   return bw.size();
@@ -441,6 +442,7 @@
   // Set len field (number of following bytes part of the header) in the hdr
   uint16_t len = htons(bw.size() - PPv2_CONNECTION_HEADER_LEN);
   memcpy(buf + len_field_offset, &len, sizeof(uint16_t));
+  Debug("proxyprotocol_v2", "Proxy Protocol v2 of %zu bytes", bw.size());
   return bw.size();
 }
 
diff --git a/iocore/net/QUICNetVConnection.cc b/iocore/net/QUICNetVConnection.cc
index 2a4127c..84712aa 100644
--- a/iocore/net/QUICNetVConnection.cc
+++ b/iocore/net/QUICNetVConnection.cc
@@ -370,11 +370,7 @@
   // Send this netvc to InactivityCop.
   nh->startCop(this);
 
-  if (inactivity_timeout_in) {
-    set_inactivity_timeout(inactivity_timeout_in);
-  } else {
-    set_inactivity_timeout(0);
-  }
+  set_inactivity_timeout(inactivity_timeout_in);
 
   if (active_timeout_in) {
     set_active_timeout(active_timeout_in);
diff --git a/iocore/net/UnixNet.cc b/iocore/net/UnixNet.cc
index 1446b95..500750f 100644
--- a/iocore/net/UnixNet.cc
+++ b/iocore/net/UnixNet.cc
@@ -23,6 +23,7 @@
 
 #include "P_Net.h"
 #include "I_AIO.h"
+#include "tscore/ink_hrtime.h"
 
 using namespace std::literals;
 
@@ -70,20 +71,30 @@
         continue;
       }
 
+      if (ne->default_inactivity_timeout_in == -1) {
+        // If no context-specific default inactivity timeout has been set by an
+        // override plugin, then use the global default.
+        Debug("inactivity_cop", "vc: %p setting the global default inactivity timeout of %d, next_inactivity_timeout_at: %" PRId64,
+              ne, nh.config.default_inactivity_timeout, ne->next_inactivity_timeout_at);
+        ne->set_default_inactivity_timeout(HRTIME_SECONDS(nh.config.default_inactivity_timeout));
+      }
+
       // set a default inactivity timeout if one is not set
       // The event `EVENT_INACTIVITY_TIMEOUT` only be triggered if a read
       // or write I/O operation was set by `do_io_read()` or `do_io_write()`.
-      if (ne->next_inactivity_timeout_at == 0 && nh.config.default_inactivity_timeout > 0 &&
-          (ne->read.enabled || ne->write.enabled)) {
+      if (ne->next_inactivity_timeout_at == 0 && ne->default_inactivity_timeout_in > 0 && (ne->read.enabled || ne->write.enabled)) {
         Debug("inactivity_cop", "vc: %p inactivity timeout not set, setting a default of %d", ne,
               nh.config.default_inactivity_timeout);
-        ne->set_default_inactivity_timeout(HRTIME_SECONDS(nh.config.default_inactivity_timeout));
+        ne->use_default_inactivity_timeout = true;
+        ne->next_inactivity_timeout_at     = Thread::get_hrtime() + ne->default_inactivity_timeout_in;
+        ne->inactivity_timeout_in          = 0;
         NET_INCREMENT_DYN_STAT(default_inactivity_timeout_applied_stat);
       }
 
       if (ne->next_inactivity_timeout_at && ne->next_inactivity_timeout_at < now) {
         if (ne->is_default_inactivity_timeout()) {
           // track the connections that timed out due to default inactivity
+          Debug("inactivity_cop", "vc: %p timed out due to default inactivity timeout", ne);
           NET_INCREMENT_DYN_STAT(default_inactivity_timeout_count_stat);
         }
         if (nh.keep_alive_queue.in(ne)) {
diff --git a/iocore/net/UnixNetVConnection.cc b/iocore/net/UnixNetVConnection.cc
index dc42372..c36d143 100644
--- a/iocore/net/UnixNetVConnection.cc
+++ b/iocore/net/UnixNetVConnection.cc
@@ -1031,11 +1031,7 @@
   // Send this netvc to InactivityCop.
   nh->startCop(this);
 
-  if (inactivity_timeout_in) {
-    UnixNetVConnection::set_inactivity_timeout(inactivity_timeout_in);
-  } else {
-    set_inactivity_timeout(0);
-  }
+  set_inactivity_timeout(inactivity_timeout_in);
 
   if (active_timeout_in) {
     UnixNetVConnection::set_active_timeout(active_timeout_in);
@@ -1326,15 +1322,13 @@
 UnixNetVConnection::set_default_inactivity_timeout(ink_hrtime timeout_in)
 {
   Debug("socket", "Set default inactive timeout=%" PRId64 ", for NetVC=%p", timeout_in, this);
-  inactivity_timeout_in      = 0;
-  default_inactivity_timeout = true;
-  next_inactivity_timeout_at = Thread::get_hrtime() + timeout_in;
+  default_inactivity_timeout_in = timeout_in;
 }
 
 TS_INLINE bool
 UnixNetVConnection::is_default_inactivity_timeout()
 {
-  return (default_inactivity_timeout && inactivity_timeout_in == 0);
+  return (use_default_inactivity_timeout && inactivity_timeout_in == 0);
 }
 
 /*
diff --git a/plugins/Makefile.am b/plugins/Makefile.am
index 3eb8092..e8c7ec7 100644
--- a/plugins/Makefile.am
+++ b/plugins/Makefile.am
@@ -83,7 +83,7 @@
 include experimental/otel_tracer/Makefile.inc
 endif
 
-if HAS_WAVM
+if HAS_WAMR
 include experimental/wasm/Makefile.inc
 endif
 
diff --git a/plugins/authproxy/Makefile.inc b/plugins/authproxy/Makefile.inc
index a46a242..9e2e569 100644
--- a/plugins/authproxy/Makefile.inc
+++ b/plugins/authproxy/Makefile.inc
@@ -19,3 +19,7 @@
 	authproxy/authproxy.cc \
 	authproxy/utils.cc \
 	authproxy/utils.h
+
+check_PROGRAMS +=  authproxy/authproxy_test
+authproxy_authproxy_test_CPPFLAGS = $(AM_CPPFLAGS) -I$(abs_top_srcdir)/tests/include
+authproxy_authproxy_test_SOURCES = authproxy/tests/authproxy_test.cc
diff --git a/plugins/authproxy/authproxy.cc b/plugins/authproxy/authproxy.cc
index 74f20ce..a8da078 100644
--- a/plugins/authproxy/authproxy.cc
+++ b/plugins/authproxy/authproxy.cc
@@ -39,6 +39,7 @@
 #include <ts/remap.h>
 
 using std::strlen;
+using std::string_view;
 
 struct AuthRequestContext;
 
@@ -55,10 +56,12 @@
 
 struct AuthOptions {
   std::string hostname;
+  std::string forward_header_prefix;
   int hostport                   = -1;
   AuthRequestTransform transform = nullptr;
   bool force                     = false;
   bool cache_internal_requests   = false;
+  string_view forwardHeaderPrefix;
 
   AuthOptions()  = default;
   ~AuthOptions() = default;
@@ -622,6 +625,38 @@
     TSHttpTxnConfigIntSet(auth->txn, TS_CONFIG_HTTP_CACHE_IGNORE_AUTHENTICATION, 1);
   }
 
+  if (!options->forward_header_prefix.empty()) {
+    // Copy headers with configured prefix in the authentication response to the original request
+    TSMLoc field_loc;
+    TSMLoc next_field_loc;
+    TSMBuffer request_bufp;
+    TSMLoc request_hdr;
+
+    TSReleaseAssert(TSHttpTxnClientReqGet(auth->txn, &request_bufp, &request_hdr) == TS_SUCCESS);
+    field_loc = TSMimeHdrFieldGet(auth->rheader.buffer, auth->rheader.header, 0);
+    TSReleaseAssert(field_loc != TS_NULL_MLOC);
+
+    while (field_loc) {
+      int key_len = 0;
+      int val_len = 0;
+
+      char *key = const_cast<char *>(TSMimeHdrFieldNameGet(auth->rheader.buffer, auth->rheader.header, field_loc, &key_len));
+      char *val =
+        const_cast<char *>(TSMimeHdrFieldValueStringGet(auth->rheader.buffer, auth->rheader.header, field_loc, -1, &val_len));
+
+      if (key && val && ContainsPrefix(string_view(key, key_len), options->forward_header_prefix)) {
+        // Append the matched header key/val to the original request
+        HttpSetMimeHeader(request_bufp, request_hdr, string_view(key, key_len), string_view(val, val_len));
+      }
+
+      // Validate the next header field
+      next_field_loc = TSMimeHdrFieldNext(auth->rheader.buffer, auth->rheader.header, field_loc);
+      TSHandleMLocRelease(auth->rheader.buffer, auth->rheader.header, field_loc);
+      field_loc = next_field_loc;
+    }
+  }
+
+  // Proceed with the modified request
   TSHttpTxnReenable(auth->txn, TS_EVENT_HTTP_CONTINUE);
   return TS_EVENT_CONTINUE;
 }
@@ -693,6 +728,7 @@
     {const_cast<char *>("auth-transform"), required_argument, nullptr, 't'},
     {const_cast<char *>("force-cacheability"), no_argument, nullptr, 'c'},
     {const_cast<char *>("cache-internal"), no_argument, nullptr, 'i'},
+    {const_cast<char *>("forward-header-prefix"), required_argument, nullptr, 'f'},
     {nullptr, 0, nullptr, 0},
   };
 
@@ -717,6 +753,9 @@
     case 'i':
       options->cache_internal_requests = true;
       break;
+    case 'f':
+      options->forward_header_prefix = optarg;
+      break;
     case 't':
       if (strcasecmp(optarg, "redirect") == 0) {
         options->transform = AuthWriteRedirectedRequest;
diff --git a/plugins/authproxy/tests/authproxy_test.cc b/plugins/authproxy/tests/authproxy_test.cc
new file mode 100644
index 0000000..c9026cb
--- /dev/null
+++ b/plugins/authproxy/tests/authproxy_test.cc
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <string_view>
+#define CATCH_CONFIG_MAIN /* include main function */
+#include <catch.hpp>      /* catch unit-test framework */
+#include "../utils.h"
+
+using std::string_view;
+TEST_CASE("Util methods", "[authproxy][utility]")
+{
+  SECTION("ContainsPrefix()")
+  {
+    CHECK(ContainsPrefix(string_view{"abcdef"}, "abc") == true);
+    CHECK(ContainsPrefix(string_view{"abc"}, "abcdef") == false);
+    CHECK(ContainsPrefix(string_view{"abcdef"}, "abd") == false);
+    CHECK(ContainsPrefix(string_view{"abc"}, "abc") == true);
+    CHECK(ContainsPrefix(string_view{""}, "") == true);
+    CHECK(ContainsPrefix(string_view{"abc"}, "") == true);
+    CHECK(ContainsPrefix(string_view{""}, "abc") == false);
+    CHECK(ContainsPrefix(string_view{"abcdef"}, "abc\0") == true);
+    CHECK(ContainsPrefix(string_view{"abcdef\0"}, "abc\0") == true);
+  }
+}
diff --git a/plugins/authproxy/utils.cc b/plugins/authproxy/utils.cc
index bbc1ac1..e9a9a75 100644
--- a/plugins/authproxy/utils.cc
+++ b/plugins/authproxy/utils.cc
@@ -26,6 +26,7 @@
 #include <getopt.h>
 #include <netinet/in.h>
 #include <arpa/inet.h>
+#include <string_view>
 
 void
 HttpDebugHeader(TSMBuffer mbuf, TSMLoc mhdr)
@@ -81,6 +82,23 @@
   TSHandleMLocRelease(mbuf, mhdr, mloc);
 }
 
+void
+HttpSetMimeHeader(TSMBuffer mbuf, TSMLoc mhdr, const std::string_view name, const std::string_view value)
+{
+  TSMLoc mloc;
+  mloc = TSMimeHdrFieldFind(mbuf, mhdr, name.data(), name.size());
+  if (mloc == TS_NULL_MLOC) {
+    TSReleaseAssert(TSMimeHdrFieldCreateNamed(mbuf, mhdr, name.data(), name.size(), &mloc) == TS_SUCCESS);
+  } else {
+    TSReleaseAssert(TSMimeHdrFieldValuesClear(mbuf, mhdr, mloc) == TS_SUCCESS);
+  }
+
+  TSReleaseAssert(TSMimeHdrFieldValueStringInsert(mbuf, mhdr, mloc, 0 /* index */, value.data(), value.size()) == TS_SUCCESS);
+  TSReleaseAssert(TSMimeHdrFieldAppend(mbuf, mhdr, mloc) == TS_SUCCESS);
+
+  TSHandleMLocRelease(mbuf, mhdr, mloc);
+}
+
 unsigned
 HttpGetContentLength(TSMBuffer mbuf, TSMLoc mhdr)
 {
diff --git a/plugins/authproxy/utils.h b/plugins/authproxy/utils.h
index 51cf01c..bd672ee 100644
--- a/plugins/authproxy/utils.h
+++ b/plugins/authproxy/utils.h
@@ -18,6 +18,8 @@
 
 #pragma once
 
+#include <string>
+#include <string_view>
 #include <ts/ts.h>
 #include <netinet/in.h>
 #include <memory>
@@ -102,8 +104,15 @@
 // Set the value of an arbitrary HTTP header.
 void HttpSetMimeHeader(TSMBuffer mbuf, TSMLoc mhdr, const char *name, const char *value);
 void HttpSetMimeHeader(TSMBuffer mbuf, TSMLoc mhdr, const char *name, unsigned value);
+void HttpSetMimeHeader(TSMBuffer mbuf, TSMLoc mhdr, const std::string_view name, const std::string_view value);
 
 // Dump the given HTTP header to the debug log.
 void HttpDebugHeader(TSMBuffer mbuf, TSMLoc mhdr);
 
+// Check if the string contains the prefix
+inline bool
+ContainsPrefix(const std::string_view str, const std::string &prefix)
+{
+  return str.size() < prefix.size() ? false : (strncmp(str.data(), prefix.data(), prefix.size()) == 0);
+}
 // vim: set ts=4 sw=4 et :
diff --git a/plugins/certifier/certifier.cc b/plugins/certifier/certifier.cc
index 0916d10..49b6bea 100644
--- a/plugins/certifier/certifier.cc
+++ b/plugins/certifier/certifier.cc
@@ -316,45 +316,33 @@
 static std::unique_ptr<SslLRUList> ssl_list = nullptr;
 static std::string store_path;
 
-/// Local helper function that generates a CSR based on common name
-static scoped_X509_REQ
-mkcsr(const char *cn)
+/**
+ * Local helper function that adds a Subject Alternative Name field into a
+ * certificate.
+ *
+ * @param[out] cert
+ * @param[in] dnsName
+ */
+static void
+addSANExtToCert(X509 *cert, std::string_view dnsName)
 {
-  TSDebug(PLUGIN_NAME, "Entering mkcsr()...");
-  X509_NAME *n;
-  scoped_X509_REQ req;
-  req.reset(X509_REQ_new());
-
-  /// Set X509 version
-  X509_REQ_set_version(req.get(), 1);
-
-  /// Get handle to subject name
-  n = X509_REQ_get_subject_name(req.get());
-
-  /// Set common name field
-  if (X509_NAME_add_entry_by_txt(n, "CN", MBSTRING_ASC, (unsigned char *)cn, -1, -1, 0) != 1) {
-    TSError("[%s] mkcsr(): Failed to add entry.", PLUGIN_NAME);
-    return nullptr;
-  }
-  /// Set Traffic Server public key
-  if (X509_REQ_set_pubkey(req.get(), ca_pkey_scoped.get()) != 1) {
-    TSError("[%s] mkcsr(): Failed to set pubkey.", PLUGIN_NAME);
-    return nullptr;
-  }
-  /// Sign with Traffic Server private key
-  if (X509_REQ_sign(req.get(), ca_pkey_scoped.get(), EVP_sha256()) <= 0) {
-    TSError("[%s] mkcsr(): Failed to Sign.", PLUGIN_NAME);
-    return nullptr;
-  }
-  return req;
+  TSDebug(PLUGIN_NAME, "Adding SAN extension to the cert");
+  GENERAL_NAMES *generalNames = sk_GENERAL_NAME_new_null();
+  GENERAL_NAME *generalName   = GENERAL_NAME_new();
+  ASN1_IA5STRING *ia5         = ASN1_IA5STRING_new();
+  ASN1_STRING_set(ia5, dnsName.data(), dnsName.length());
+  // generalName owns ia5 after this call
+  GENERAL_NAME_set0_value(generalName, GEN_DNS, ia5);
+  sk_GENERAL_NAME_push(generalNames, generalName);
+  X509_add1_ext_i2d(cert, NID_subject_alt_name, generalNames, 0, X509V3_ADD_DEFAULT);
+  sk_GENERAL_NAME_pop_free(generalNames, GENERAL_NAME_free);
 }
 
-/// Local helper function that generates a X509 certificate based on CSR
+/// Local helper function that generates a X509 certificate based on SNI
 static scoped_X509
-mkcrt(X509_REQ *req, int serial)
+mkcrt(const std::string &commonName, int serial)
 {
   TSDebug(PLUGIN_NAME, "Entering mkcrt()...");
-  X509_NAME *subj, *tmpsubj;
   scoped_EVP_PKEY pktmp;
   scoped_X509 cert;
 
@@ -379,29 +367,22 @@
   X509_gmtime_adj(X509_get_notBefore(cert.get()), 0);
   X509_gmtime_adj(X509_get_notAfter(cert.get()), static_cast<long>(3650) * 24 * 3600);
 
-  /// Get a handle to csr subject name
-  subj = X509_REQ_get_subject_name(req);
-  if ((tmpsubj = X509_NAME_dup(subj)) == nullptr) {
-    TSDebug(PLUGIN_NAME, "mkcrt(): Failed to duplicate subject name.");
+  /// Get handle to subject name
+  X509_NAME *n = X509_get_subject_name(cert.get());
+  /// Set common name field
+  if (X509_NAME_add_entry_by_txt(n, "CN", MBSTRING_ASC, (unsigned char *)commonName.c_str(), -1, -1, 0) != 1) {
+    TSError("[%s] mkcsr(): Failed to add entry.", PLUGIN_NAME);
     return nullptr;
   }
-  if ((X509_set_subject_name(cert.get(), tmpsubj)) == 0) {
-    TSDebug(PLUGIN_NAME, "mkcrt(): Failed to set X509 subject name");
-    X509_NAME_free(tmpsubj); ///< explicit call to free X509_NAME object
-    return nullptr;
-  }
-  pktmp.reset(X509_REQ_get_pubkey(req));
-  if (pktmp == nullptr) {
-    TSDebug(PLUGIN_NAME, "mkcrt(): Failed to get CSR public key.");
-    X509_NAME_free(tmpsubj);
-    return nullptr;
-  }
-  if (X509_set_pubkey(cert.get(), pktmp.get()) == 0) {
+  /// Set Traffic Server public key
+  if (X509_set_pubkey(cert.get(), ca_pkey_scoped.get()) == 0) {
     TSDebug(PLUGIN_NAME, "mkcrt(): Failed to set X509 public key.");
-    X509_NAME_free(tmpsubj);
     return nullptr;
   }
+  /// Add the Subject Alternative Name (SAN) extension
+  addSANExtToCert(cert.get(), commonName);
 
+  // Sign the certificate
   X509_sign(cert.get(), ca_pkey_scoped.get(), EVP_sha256());
 
   return cert;
@@ -481,17 +462,8 @@
 
     TSMutexUnlock(serial_mutex);
 
-    /// Create CSR and cert
-    req = mkcsr(commonName.c_str());
-    if (req == nullptr) {
-      TSDebug(PLUGIN_NAME, "[shadow_cert_generator] CSR generation failed");
-      TSContDestroy(contp);
-      ssl_list->set_schedule(commonName, false);
-      return TS_ERROR;
-    }
-
-    cert = mkcrt(req.get(), serial);
-
+    /// Create cert
+    cert = mkcrt(commonName, serial);
     if (cert == nullptr) {
       TSDebug(PLUGIN_NAME, "[shadow_cert_generator] Cert generation failed");
       TSContDestroy(contp);
@@ -602,7 +574,6 @@
     {"store", required_argument, nullptr, 's'},       {nullptr, no_argument, nullptr, 0}};
 
   int opt = 0;
-
   while (opt >= 0) {
     opt = getopt_long(argc, const_cast<char *const *>(argv), "c:k:r:m:s:", longopts, nullptr);
     switch (opt) {
diff --git a/plugins/experimental/slice/Config.h b/plugins/experimental/slice/Config.h
index c83badc..a63716e 100644
--- a/plugins/experimental/slice/Config.h
+++ b/plugins/experimental/slice/Config.h
@@ -26,6 +26,7 @@
 #include <pcre.h>
 #endif
 
+#include <string>
 #include <mutex>
 
 // Data Structures and Classes
diff --git a/plugins/experimental/wasm/Makefile.inc b/plugins/experimental/wasm/Makefile.inc
index 8f2fa75..1cc9cfd 100755
--- a/plugins/experimental/wasm/Makefile.inc
+++ b/plugins/experimental/wasm/Makefile.inc
@@ -16,8 +16,9 @@
 
 AM_CPPFLAGS += @YAMLCPP_INCLUDES@
 
-experimental_wasm_wasm_la_CPPFLAGS = -I$(srcdir)/experimental/wasm/lib -I$(srcdir)/experimental/wasm $(AM_CPPFLAGS) $(WAVM_CPPFLAGS) -DWAVM_API=
-experimental_wasm_wasm_la_LDFLAGS = = $(AM_LDFLAGS) $(WAVM_LDFLAGS)
+experimental_wasm_wasm_la_CPPFLAGS = -I$(srcdir)/experimental/wasm/lib -I$(srcdir)/experimental/wasm $(AM_CPPFLAGS)
+experimental_wasm_wasm_la_LDFLAGS = = $(AM_LDFLAGS)
+experimental_wasm_wasm_la_LIBADD = $(WAMR_LIBS)
 
 pkglib_LTLIBRARIES += experimental/wasm/wasm.la
 
@@ -40,6 +41,8 @@
   experimental/wasm/lib/include/proxy-wasm/signature_util.h \
   experimental/wasm/lib/src/vm_id_handle.cc \
   experimental/wasm/lib/include/proxy-wasm/vm_id_handle.h \
+  experimental/wasm/lib/src/pairs_util.cc \
+  experimental/wasm/lib/include/proxy-wasm/pairs_util.h \
   experimental/wasm/lib/include/proxy-wasm/wasm_vm.h \
   experimental/wasm/lib/src/wasm.cc \
   experimental/wasm/lib/include/proxy-wasm/wasm.h \
@@ -48,7 +51,9 @@
   experimental/wasm/ats_context.cc \
   experimental/wasm/ats_context.h \
   experimental/wasm/wasm_main.cc \
-  experimental/wasm/lib/src/wavm/wavm.cc \
-  experimental/wasm/lib/include/proxy-wasm/wavm.h \
+  experimental/wasm/lib/src/wamr/wamr.cc \
+  experimental/wasm/lib/src/wamr/types.h \
+  experimental/wasm/lib/include/proxy-wasm/wamr.h \
+  experimental/wasm/lib/src/common/types.h \
   experimental/wasm/lib/include/proxy-wasm/word.h
 
diff --git a/plugins/experimental/wasm/ats_wasm.cc b/plugins/experimental/wasm/ats_wasm.cc
index 52833fa..9cd277f 100644
--- a/plugins/experimental/wasm/ats_wasm.cc
+++ b/plugins/experimental/wasm/ats_wasm.cc
@@ -22,6 +22,36 @@
 
 namespace ats_wasm
 {
+// extended Wasm VM Integration object
+proxy_wasm::LogLevel
+ATSWasmVmIntegration::getLogLevel()
+{
+  if (TSIsDebugTagSet(WASM_DEBUG_TAG)) {
+    return proxy_wasm::LogLevel::debug;
+  } else {
+    return proxy_wasm::LogLevel::error;
+  }
+}
+
+void
+ATSWasmVmIntegration::error(std::string_view message)
+{
+  TSError("%.*s", static_cast<int>(message.size()), message.data());
+}
+
+void
+ATSWasmVmIntegration::trace(std::string_view message)
+{
+  TSDebug(WASM_DEBUG_TAG, "%.*s", static_cast<int>(message.size()), message.data());
+}
+
+bool
+ATSWasmVmIntegration::getNullVmFunction(std::string_view function_name, bool returns_word, int number_of_arguments,
+                                        proxy_wasm::NullPlugin *plugin, void *ptr_to_function_return)
+{
+  return false;
+}
+
 // extended constructors to initialize mutex
 Wasm::Wasm(const std::shared_ptr<WasmHandleBase> &base_wasm_handle, const WasmVmFactory &factory)
   : WasmBase(base_wasm_handle, factory)
diff --git a/plugins/experimental/wasm/ats_wasm.h b/plugins/experimental/wasm/ats_wasm.h
index 919e901..476b023 100644
--- a/plugins/experimental/wasm/ats_wasm.h
+++ b/plugins/experimental/wasm/ats_wasm.h
@@ -33,9 +33,26 @@
 using proxy_wasm::WasmHandleBase;
 using proxy_wasm::WasmVmFactory;
 using proxy_wasm::AllowedCapabilitiesMap;
+using proxy_wasm::WasmVmIntegration;
 
 class Context;
 
+class ATSWasmVmIntegration : public WasmVmIntegration
+{
+public:
+  //  proxy_wasm::WasmVmIntegration
+  WasmVmIntegration *
+  clone() override
+  {
+    return new ATSWasmVmIntegration();
+  }
+  bool getNullVmFunction(std::string_view function_name, bool returns_word, int number_of_arguments, proxy_wasm::NullPlugin *plugin,
+                         void *ptr_to_function_return) override;
+  proxy_wasm::LogLevel getLogLevel() override;
+  void error(std::string_view message) override;
+  void trace(std::string_view message) override;
+};
+
 class Wasm : public WasmBase
 {
 public:
diff --git a/plugins/experimental/wasm/examples/cpp/myproject.yaml b/plugins/experimental/wasm/examples/cpp/myproject.yaml
index f22e7d9..56be734 100755
--- a/plugins/experimental/wasm/examples/cpp/myproject.yaml
+++ b/plugins/experimental/wasm/examples/cpp/myproject.yaml
@@ -20,5 +20,5 @@
     code:
       local:
         filename: /usr/local/var/wasm/myproject.wasm
-    runtime: envoy.wasm.runtime.wavm
+    runtime: envoy.wasm.runtime.wamr
     vmId: test
diff --git a/plugins/experimental/wasm/examples/rust/myfilter.yaml b/plugins/experimental/wasm/examples/rust/myfilter.yaml
index aa1b93f..afa4f18 100755
--- a/plugins/experimental/wasm/examples/rust/myfilter.yaml
+++ b/plugins/experimental/wasm/examples/rust/myfilter.yaml
@@ -20,5 +20,5 @@
     code:
       local:
         filename: /usr/local/var/wasm/myfilter.wasm
-    runtime: envoy.wasm.runtime.wavm
+    runtime: envoy.wasm.runtime.wamr
     vmId: test
diff --git a/plugins/experimental/wasm/examples/tinygo/README.md b/plugins/experimental/wasm/examples/tinygo/README.md
index a8f070e..71a0c69 100755
--- a/plugins/experimental/wasm/examples/tinygo/README.md
+++ b/plugins/experimental/wasm/examples/tinygo/README.md
@@ -1,6 +1,6 @@
 TinyGo example for ATS Wasm Plugin
 
-[Coraza Proxy WASM](https://github.com/corazawaf/coraza-proxy-wasm) is WAF wasm filter built on top of [Coraza](https://github.com/corazawaf/coraza) and implementing [proxy-wasm ABI](https://github.com/proxy-wasm/spec)
+[Coraza Proxy WASM](https://github.com/corazawaf/coraza-proxy-wasm) is a WAF wasm filter built on top of [Coraza](https://github.com/corazawaf/coraza) and implementing [proxy-wasm ABI](https://github.com/proxy-wasm/spec)
 
 To retrieve `plugin.wasm` for the Coraza Proxy WASM
 * `docker pull ghcr.io/corazawaf/coraza-proxy-wasm:main`
@@ -10,4 +10,4 @@
 Copy the yaml in this directory to `/usr/local/var/wasm/` and activate the plugin in `plugin.config`
 * `wasm.so /usr/local/var/wasm/plugin.yaml`
 
-After restarting ATS, any request with `perl` as the user agent will retreive a status 403 response.
+After restarting ATS, any request with `perl` as the user agent will return a status 403 response.
diff --git a/plugins/experimental/wasm/examples/tinygo/plugin.yaml b/plugins/experimental/wasm/examples/tinygo/plugin.yaml
index f4a5dd6..fe7fd63 100644
--- a/plugins/experimental/wasm/examples/tinygo/plugin.yaml
+++ b/plugins/experimental/wasm/examples/tinygo/plugin.yaml
@@ -13,6 +13,6 @@
     code:
       local:
         filename: /usr/local/var/wasm/plugin.wasm
-    runtime: envoy.wasm.runtime.wavm
+    runtime: envoy.wasm.runtime.wamr
     vmId: "coraza-filter_vm_id"
     allow_precompiled: true
diff --git a/plugins/experimental/wasm/lib/include/proxy-wasm/exports.h b/plugins/experimental/wasm/lib/include/proxy-wasm/exports.h
index 325d28f..376a4d3 100644
--- a/plugins/experimental/wasm/lib/include/proxy-wasm/exports.h
+++ b/plugins/experimental/wasm/lib/include/proxy-wasm/exports.h
@@ -62,36 +62,6 @@
 
 namespace exports {
 
-template <typename Pairs> size_t pairsSize(const Pairs &result) {
-  size_t size = 4; // number of headers
-  for (auto &p : result) {
-    size += 8;                   // size of key, size of value
-    size += p.first.size() + 1;  // null terminated key
-    size += p.second.size() + 1; // null terminated value
-  }
-  return size;
-}
-
-template <typename Pairs> void marshalPairs(const Pairs &result, char *buffer) {
-  char *b = buffer;
-  *reinterpret_cast<uint32_t *>(b) = htowasm(result.size());
-  b += sizeof(uint32_t);
-  for (auto &p : result) {
-    *reinterpret_cast<uint32_t *>(b) = htowasm(p.first.size());
-    b += sizeof(uint32_t);
-    *reinterpret_cast<uint32_t *>(b) = htowasm(p.second.size());
-    b += sizeof(uint32_t);
-  }
-  for (auto &p : result) {
-    memcpy(b, p.first.data(), p.first.size());
-    b += p.first.size();
-    *b++ = 0;
-    memcpy(b, p.second.data(), p.second.size());
-    b += p.second.size();
-    *b++ = 0;
-  }
-}
-
 // ABI functions exported from host to wasm.
 
 Word get_configuration(Word value_ptr_ptr, Word value_size_ptr);
diff --git a/plugins/experimental/wasm/lib/include/proxy-wasm/limits.h b/plugins/experimental/wasm/lib/include/proxy-wasm/limits.h
index d02d4c5..9b9ac1c 100755
--- a/plugins/experimental/wasm/lib/include/proxy-wasm/limits.h
+++ b/plugins/experimental/wasm/lib/include/proxy-wasm/limits.h
@@ -30,3 +30,13 @@
 #ifndef PROXY_WASM_HOST_WASI_RANDOM_GET_MAX_SIZE_BYTES
 #define PROXY_WASM_HOST_WASI_RANDOM_GET_MAX_SIZE_BYTES (64 * 1024)
 #endif
+
+// Maximum allowed size of Pairs buffer to deserialize.
+#ifndef PROXY_WASM_HOST_PAIRS_MAX_BYTES
+#define PROXY_WASM_HOST_PAIRS_MAX_BYTES (1024 * 1024)
+#endif
+
+// Maximum allowed number of pairs in a Pairs buffer to deserialize.
+#ifndef PROXY_WASM_HOST_PAIRS_MAX_COUNT
+#define PROXY_WASM_HOST_PAIRS_MAX_COUNT 1024
+#endif
diff --git a/plugins/experimental/wasm/lib/include/proxy-wasm/null_vm.h b/plugins/experimental/wasm/lib/include/proxy-wasm/null_vm.h
index 3a38fd9..a0a3798 100644
--- a/plugins/experimental/wasm/lib/include/proxy-wasm/null_vm.h
+++ b/plugins/experimental/wasm/lib/include/proxy-wasm/null_vm.h
@@ -61,6 +61,7 @@
 #undef _REGISTER_CALLBACK
 
   void terminate() override {}
+  bool usesWasmByteOrder() override { return false; }
 
   std::string plugin_name_;
   std::unique_ptr<NullVmPlugin> plugin_;
diff --git a/plugins/experimental/wasm/lib/include/proxy-wasm/pairs_util.h b/plugins/experimental/wasm/lib/include/proxy-wasm/pairs_util.h
new file mode 100644
index 0000000..54df2fd
--- /dev/null
+++ b/plugins/experimental/wasm/lib/include/proxy-wasm/pairs_util.h
@@ -0,0 +1,63 @@
+// Copyright 2016-2019 Envoy Project Authors
+// Copyright 2020 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#pragma once
+
+#include <string>
+#include <string_view>
+#include <vector>
+
+namespace proxy_wasm {
+
+using Pairs = std::vector<std::pair<std::string_view, std::string_view>>;
+using StringPairs = std::vector<std::pair<std::string, std::string>>;
+
+class PairsUtil {
+public:
+  /**
+   * pairsSize returns the buffer size required to serialize Pairs.
+   * @param pairs Pairs to serialize.
+   * @return size of the output buffer.
+   */
+  static size_t pairsSize(const Pairs &pairs);
+
+  static size_t pairsSize(const StringPairs &stringpairs) {
+    Pairs views(stringpairs.begin(), stringpairs.end());
+    return pairsSize(views);
+  }
+
+  /**
+   * marshalPairs serializes Pairs to output buffer.
+   * @param pairs Pairs to serialize.
+   * @param buffer output buffer.
+   * @param size size of the output buffer.
+   * @return indicates whether serialization succeeded or not.
+   */
+  static bool marshalPairs(const Pairs &pairs, char *buffer, size_t size);
+
+  static bool marshalPairs(const StringPairs &stringpairs, char *buffer, size_t size) {
+    Pairs views(stringpairs.begin(), stringpairs.end());
+    return marshalPairs(views, buffer, size);
+  }
+
+  /**
+   * toPairs deserializes input buffer to Pairs.
+   * @param buffer serialized input buffer.
+   * @return deserialized Pairs or an empty instance in case of deserialization failure.
+   */
+  static Pairs toPairs(std::string_view buffer);
+};
+
+} // namespace proxy_wasm
diff --git a/plugins/experimental/wasm/lib/include/proxy-wasm/wasm.h b/plugins/experimental/wasm/lib/include/proxy-wasm/wasm.h
index fbef823..23ed3c1 100644
--- a/plugins/experimental/wasm/lib/include/proxy-wasm/wasm.h
+++ b/plugins/experimental/wasm/lib/include/proxy-wasm/wasm.h
@@ -314,6 +314,10 @@
   std::shared_ptr<VmIdHandle> vm_id_handle_;
 };
 
+using WasmHandleFactory = std::function<std::shared_ptr<WasmHandleBase>(std::string_view vm_id)>;
+using WasmHandleCloneFactory =
+    std::function<std::shared_ptr<WasmHandleBase>(std::shared_ptr<WasmHandleBase> wasm)>;
+
 // Handle which enables shutdown operations to run post deletion (e.g. post listener drain).
 class WasmHandleBase : public std::enable_shared_from_this<WasmHandleBase> {
 public:
@@ -324,6 +328,9 @@
     }
   }
 
+  bool canary(const std::shared_ptr<PluginBase> &plugin,
+              const WasmHandleCloneFactory &clone_factory);
+
   void kill() { wasm_base_ = nullptr; }
 
   std::shared_ptr<WasmBase> &wasm() { return wasm_base_; }
@@ -335,10 +342,6 @@
 std::string makeVmKey(std::string_view vm_id, std::string_view configuration,
                       std::string_view code);
 
-using WasmHandleFactory = std::function<std::shared_ptr<WasmHandleBase>(std::string_view vm_id)>;
-using WasmHandleCloneFactory =
-    std::function<std::shared_ptr<WasmHandleBase>(std::shared_ptr<WasmHandleBase> wasm)>;
-
 // Returns nullptr on failure (i.e. initialization of the VM fails).
 std::shared_ptr<WasmHandleBase> createWasm(const std::string &vm_key, const std::string &code,
                                            const std::shared_ptr<PluginBase> &plugin,
@@ -389,7 +392,15 @@
   if (!malloc_) {
     return nullptr;
   }
+  wasm_vm_->setRestrictedCallback(
+      true, {// logging (Proxy-Wasm)
+             "env.proxy_log",
+             // logging (stdout/stderr)
+             "wasi_unstable.fd_write", "wasi_snapshot_preview1.fd_write",
+             // time
+             "wasi_unstable.clock_time_get", "wasi_snapshot_preview1.clock_time_get"});
   Word a = malloc_(vm_context(), size);
+  wasm_vm_->setRestrictedCallback(false);
   if (!a.u64_) {
     return nullptr;
   }
diff --git a/plugins/experimental/wasm/lib/include/proxy-wasm/wasm_vm.h b/plugins/experimental/wasm/lib/include/proxy-wasm/wasm_vm.h
index 800348a..acf0ccf 100644
--- a/plugins/experimental/wasm/lib/include/proxy-wasm/wasm_vm.h
+++ b/plugins/experimental/wasm/lib/include/proxy-wasm/wasm_vm.h
@@ -20,6 +20,7 @@
 #include <optional>
 #include <string>
 #include <unordered_map>
+#include <unordered_set>
 #include <vector>
 
 #include "include/proxy-wasm/word.h"
@@ -302,6 +303,12 @@
    */
   virtual void terminate() = 0;
 
+  /**
+   * Byte order flag (host or wasm).
+   * @return 'false' for a null VM and 'true' for a wasm VM.
+   */
+  virtual bool usesWasmByteOrder() = 0;
+
   bool isFailed() { return failed_ != FailState::Ok; }
   void fail(FailState fail_state, std::string_view message) {
     integration()->error(message);
@@ -314,6 +321,16 @@
     fail_callbacks_.push_back(fail_callback);
   }
 
+  bool isHostFunctionAllowed(const std::string &name) {
+    return !restricted_callback_ || allowed_hostcalls_.find(name) != allowed_hostcalls_.end();
+  }
+
+  void setRestrictedCallback(bool restricted,
+                             std::unordered_set<std::string> allowed_hostcalls = {}) {
+    restricted_callback_ = restricted;
+    allowed_hostcalls_ = std::move(allowed_hostcalls);
+  }
+
   // Integrator operations.
   std::unique_ptr<WasmVmIntegration> &integration() { return integration_; }
   bool cmpLogLevel(proxy_wasm::LogLevel level) { return integration_->getLogLevel() <= level; }
@@ -322,6 +339,10 @@
   std::unique_ptr<WasmVmIntegration> integration_;
   FailState failed_ = FailState::Ok;
   std::vector<std::function<void(FailState)>> fail_callbacks_;
+
+private:
+  bool restricted_callback_{false};
+  std::unordered_set<std::string> allowed_hostcalls_{};
 };
 
 // Thread local state set during a call into a WASM VM so that calls coming out of the
diff --git a/plugins/experimental/wasm/lib/include/proxy-wasm/word.h b/plugins/experimental/wasm/lib/include/proxy-wasm/word.h
index 559471e..90ea932 100644
--- a/plugins/experimental/wasm/lib/include/proxy-wasm/word.h
+++ b/plugins/experimental/wasm/lib/include/proxy-wasm/word.h
@@ -24,11 +24,11 @@
 // Use byteswap functions only when compiling for big-endian platforms.
 #if defined(__BYTE_ORDER__) && defined(__ORDER_BIG_ENDIAN__) &&                                    \
     __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__
-#define htowasm(x) __builtin_bswap32(x)
-#define wasmtoh(x) __builtin_bswap32(x)
+#define htowasm(x, vm_uses_wasm_byte_order) ((vm_uses_wasm_byte_order) ? __builtin_bswap32(x) : (x))
+#define wasmtoh(x, vm_uses_wasm_byte_order) ((vm_uses_wasm_byte_order) ? __builtin_bswap32(x) : (x))
 #else
-#define htowasm(x) (x)
-#define wasmtoh(x) (x)
+#define htowasm(x, vm_uses_wasm_byte_order) (x)
+#define wasmtoh(x, vm_uses_wasm_byte_order) (x)
 #endif
 
 // Represents a Wasm-native word-sized datum. On 32-bit VMs, the high bits are always zero.
diff --git a/plugins/experimental/wasm/lib/src/bytecode_util.cc b/plugins/experimental/wasm/lib/src/bytecode_util.cc
index 3c5f768..70a373e 100644
--- a/plugins/experimental/wasm/lib/src/bytecode_util.cc
+++ b/plugins/experimental/wasm/lib/src/bytecode_util.cc
@@ -14,6 +14,10 @@
 
 #include "include/proxy-wasm/bytecode_util.h"
 
+#if !defined(_MSC_VER)
+#include <cxxabi.h>
+#endif
+
 #include <cstring>
 
 namespace proxy_wasm {
@@ -44,15 +48,18 @@
       return false;
     }
     if (section_type == 7 /* export section */) {
+      const char *section_end = pos + section_len;
       uint32_t export_vector_size = 0;
-      if (!parseVarint(pos, end, export_vector_size) || pos + export_vector_size > end) {
+      if (!parseVarint(pos, section_end, export_vector_size) ||
+          pos + export_vector_size > section_end) {
         return false;
       }
       // Search thourgh exports.
       for (uint32_t i = 0; i < export_vector_size; i++) {
         // Parse name of the export.
         uint32_t export_name_size = 0;
-        if (!parseVarint(pos, end, export_name_size) || pos + export_name_size > end) {
+        if (!parseVarint(pos, section_end, export_name_size) ||
+            pos + export_name_size > section_end) {
           return false;
         }
         const auto *const name_begin = pos;
@@ -61,7 +68,7 @@
           return false;
         }
         // Check if it is a function type export
-        if (*pos++ == 0x00) {
+        if (*pos++ == 0x00 /* function */) {
           const std::string export_name = {name_begin, export_name_size};
           // Check the name of the function.
           if (export_name == "proxy_abi_version_0_1_0") {
@@ -110,24 +117,25 @@
     }
     if (section_type == 0) {
       // Custom section.
-      const auto *const section_data_start = pos;
+      const char *section_end = pos + section_len;
       uint32_t section_name_len = 0;
-      if (!BytecodeUtil::parseVarint(pos, end, section_name_len) || pos + section_name_len > end) {
+      if (!BytecodeUtil::parseVarint(pos, section_end, section_name_len) ||
+          pos + section_name_len > section_end) {
         return false;
       }
       if (section_name_len == name.size() && ::memcmp(pos, name.data(), section_name_len) == 0) {
         pos += section_name_len;
-        ret = {pos, static_cast<size_t>(section_data_start + section_len - pos)};
+        ret = {pos, static_cast<size_t>(section_end - pos)};
         return true;
       }
-      pos = section_data_start + section_len;
+      pos = section_end;
     } else {
       // Skip other sections.
       pos += section_len;
     }
   }
   return true;
-};
+}
 
 bool BytecodeUtil::getFunctionNameIndex(std::string_view bytecode,
                                         std::unordered_map<uint32_t, std::string> &ret) {
@@ -165,7 +173,16 @@
           if (!parseVarint(pos, end, func_name_size) || pos + func_name_size > end) {
             return false;
           }
-          ret.insert({func_index, std::string(pos, func_name_size)});
+          auto func_name = std::string(pos, func_name_size);
+#if !defined(_MSC_VER)
+          int status;
+          char *data = abi::__cxa_demangle(func_name.c_str(), nullptr, nullptr, &status);
+          if (data != nullptr) {
+            func_name = std::string(data);
+            ::free(data);
+          }
+#endif
+          ret.insert({func_index, func_name});
           pos += func_name_size;
         }
         if (start + subsection_size != pos) {
@@ -229,16 +246,32 @@
 
 bool BytecodeUtil::parseVarint(const char *&pos, const char *end, uint32_t &ret) {
   uint32_t shift = 0;
+  uint32_t total = 0;
+  uint32_t v;
   char b;
-  do {
+  while (pos < end) {
     if (pos + 1 > end) {
+      // overread
       return false;
     }
     b = *pos++;
-    ret += (b & 0x7f) << shift;
+    v = (b & 0x7f);
+    if (shift == 28 && v > 3) {
+      // overflow
+      return false;
+    }
+    total += v << shift;
+    if ((b & 0x80) == 0) {
+      ret = total;
+      return true;
+    }
     shift += 7;
-  } while ((b & 0x80) != 0);
-  return ret != static_cast<uint32_t>(-1);
+    if (shift > 28) {
+      // overflow
+      return false;
+    }
+  }
+  return false;
 }
 
 } // namespace proxy_wasm
diff --git a/plugins/experimental/wasm/lib/src/exports.cc b/plugins/experimental/wasm/lib/src/exports.cc
index bdefdde..0290dcf 100644
--- a/plugins/experimental/wasm/lib/src/exports.cc
+++ b/plugins/experimental/wasm/lib/src/exports.cc
@@ -14,6 +14,7 @@
 // limitations under the License.
 //
 #include "include/proxy-wasm/limits.h"
+#include "include/proxy-wasm/pairs_util.h"
 #include "include/proxy-wasm/wasm.h"
 
 #include <openssl/rand.h>
@@ -58,55 +59,6 @@
 
 namespace exports {
 
-namespace {
-
-Pairs toPairs(std::string_view buffer) {
-  Pairs result;
-  const char *b = buffer.data();
-  if (buffer.size() < sizeof(uint32_t)) {
-    return {};
-  }
-  auto size = wasmtoh(*reinterpret_cast<const uint32_t *>(b));
-  b += sizeof(uint32_t);
-  if (sizeof(uint32_t) + size * 2 * sizeof(uint32_t) > buffer.size()) {
-    return {};
-  }
-  result.resize(size);
-  for (uint32_t i = 0; i < size; i++) {
-    result[i].first = std::string_view(nullptr, wasmtoh(*reinterpret_cast<const uint32_t *>(b)));
-    b += sizeof(uint32_t);
-    result[i].second = std::string_view(nullptr, wasmtoh(*reinterpret_cast<const uint32_t *>(b)));
-    b += sizeof(uint32_t);
-  }
-  for (auto &p : result) {
-    p.first = std::string_view(b, p.first.size());
-    b += p.first.size() + 1;
-    p.second = std::string_view(b, p.second.size());
-    b += p.second.size() + 1;
-  }
-  return result;
-}
-
-template <typename Pairs>
-bool getPairs(ContextBase *context, const Pairs &result, uint64_t ptr_ptr, uint64_t size_ptr) {
-  if (result.empty()) {
-    return context->wasm()->copyToPointerSize("", ptr_ptr, size_ptr);
-  }
-  uint64_t size = pairsSize(result);
-  uint64_t ptr = 0;
-  char *buffer = static_cast<char *>(context->wasm()->allocMemory(size, &ptr));
-  marshalPairs(result, buffer);
-  if (!context->wasmVm()->setWord(ptr_ptr, Word(ptr))) {
-    return false;
-  }
-  if (!context->wasmVm()->setWord(size_ptr, Word(size))) {
-    return false;
-  }
-  return true;
-}
-
-} // namespace
-
 // General ABI.
 
 Word set_property(Word key_ptr, Word key_size, Word value_ptr, Word value_size) {
@@ -200,7 +152,7 @@
   if (!details || !body || !additional_response_header_pairs) {
     return WasmResult::InvalidMemoryAccess;
   }
-  auto additional_headers = toPairs(additional_response_header_pairs.value());
+  auto additional_headers = PairsUtil::toPairs(additional_response_header_pairs.value());
   context->sendLocalResponse(response_code, body.value(), std::move(additional_headers),
                              grpc_status, details.value());
   context->wasm()->stopNextIteration(true);
@@ -435,7 +387,25 @@
   if (result != WasmResult::Ok) {
     return result;
   }
-  if (!getPairs(context, pairs, ptr_ptr, size_ptr)) {
+  if (pairs.empty()) {
+    if (!context->wasm()->copyToPointerSize("", ptr_ptr, size_ptr)) {
+      return WasmResult::InvalidMemoryAccess;
+    }
+    return WasmResult::Ok;
+  }
+  uint64_t size = PairsUtil::pairsSize(pairs);
+  uint64_t ptr = 0;
+  char *buffer = static_cast<char *>(context->wasm()->allocMemory(size, &ptr));
+  if (buffer == nullptr) {
+    return WasmResult::InvalidMemoryAccess;
+  }
+  if (!PairsUtil::marshalPairs(pairs, buffer, size)) {
+    return WasmResult::InvalidMemoryAccess;
+  }
+  if (!context->wasmVm()->setWord(ptr_ptr, Word(ptr))) {
+    return WasmResult::InvalidMemoryAccess;
+  }
+  if (!context->wasmVm()->setWord(size_ptr, Word(size))) {
     return WasmResult::InvalidMemoryAccess;
   }
   return WasmResult::Ok;
@@ -451,7 +421,7 @@
     return WasmResult::InvalidMemoryAccess;
   }
   return context->setHeaderMapPairs(static_cast<WasmHeaderMapType>(type.u64_),
-                                    toPairs(data.value()));
+                                    PairsUtil::toPairs(data.value()));
 }
 
 Word get_header_map_size(Word type, Word result_ptr) {
@@ -485,13 +455,21 @@
     return WasmResult::BadArgument;
   }
   // Don't overread.
-  if (start + length > buffer->size()) {
+  if (start > buffer->size()) {
+    length = 0;
+  } else if (start + length > buffer->size()) {
     length = buffer->size() - start;
   }
-  if (length > 0) {
-    return buffer->copyTo(context->wasm(), start, length, ptr_ptr, size_ptr);
+  if (length == 0) {
+    if (!context->wasmVm()->setWord(ptr_ptr, Word(0))) {
+      return WasmResult::InvalidMemoryAccess;
+    }
+    if (!context->wasmVm()->setWord(size_ptr, Word(0))) {
+      return WasmResult::InvalidMemoryAccess;
+    }
+    return WasmResult::Ok;
   }
-  return WasmResult::Ok;
+  return buffer->copyTo(context->wasm(), start, length, ptr_ptr, size_ptr);
 }
 
 Word get_buffer_status(Word type, Word length_ptr, Word flags_ptr) {
@@ -541,8 +519,8 @@
   if (!uri || !body || !header_pairs || !trailer_pairs) {
     return WasmResult::InvalidMemoryAccess;
   }
-  auto headers = toPairs(header_pairs.value());
-  auto trailers = toPairs(trailer_pairs.value());
+  auto headers = PairsUtil::toPairs(header_pairs.value());
+  auto trailers = PairsUtil::toPairs(trailer_pairs.value());
   uint32_t token = 0;
   // NB: try to write the token to verify the memory before starting the async
   // operation.
@@ -611,7 +589,7 @@
     return WasmResult::InvalidMemoryAccess;
   }
   uint32_t token = 0;
-  auto initial_metadata = toPairs(initial_metadata_pairs.value());
+  auto initial_metadata = PairsUtil::toPairs(initial_metadata_pairs.value());
   auto result = context->grpcCall(service.value(), service_name.value(), method_name.value(),
                                   initial_metadata, request.value(),
                                   std::chrono::milliseconds(timeout_milliseconds), &token);
@@ -637,7 +615,7 @@
     return WasmResult::InvalidMemoryAccess;
   }
   uint32_t token = 0;
-  auto initial_metadata = toPairs(initial_metadata_pairs.value());
+  auto initial_metadata = PairsUtil::toPairs(initial_metadata_pairs.value());
   auto result = context->grpcStream(service.value(), service_name.value(), method_name.value(),
                                     initial_metadata, &token);
   if (result != WasmResult::Ok) {
@@ -715,8 +693,9 @@
     }
     const auto *iovec = reinterpret_cast<const uint32_t *>(memslice.value().data());
     if (iovec[1] != 0U /* buf_len */) {
-      memslice = context->wasmVm()->getMemory(wasmtoh(iovec[0]) /* buf */,
-                                              wasmtoh(iovec[1]) /* buf_len */);
+      const auto buf = wasmtoh(iovec[0], context->wasmVm()->usesWasmByteOrder());
+      const auto buf_len = wasmtoh(iovec[1], context->wasmVm()->usesWasmByteOrder());
+      memslice = context->wasmVm()->getMemory(buf, buf_len);
       if (!memslice) {
         return 21; // __WASI_EFAULT
       }
diff --git a/plugins/experimental/wasm/lib/src/pairs_util.cc b/plugins/experimental/wasm/lib/src/pairs_util.cc
new file mode 100644
index 0000000..d211357
--- /dev/null
+++ b/plugins/experimental/wasm/lib/src/pairs_util.cc
@@ -0,0 +1,190 @@
+// Copyright 2016-2019 Envoy Project Authors
+// Copyright 2020 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "include/proxy-wasm/pairs_util.h"
+
+#include <cstring>
+#include <string_view>
+#include <vector>
+
+#include "include/proxy-wasm/exports.h"
+#include "include/proxy-wasm/limits.h"
+#include "include/proxy-wasm/word.h"
+
+namespace proxy_wasm {
+
+using Sizes = std::vector<std::pair<uint32_t, uint32_t>>;
+
+size_t PairsUtil::pairsSize(const Pairs &pairs) {
+  size_t size = sizeof(uint32_t); // number of headers
+  for (const auto &p : pairs) {
+    size += 2 * sizeof(uint32_t); // size of name, size of value
+    size += p.first.size() + 1;   // NULL-terminated name
+    size += p.second.size() + 1;  // NULL-terminated value
+  }
+  return size;
+}
+
+bool PairsUtil::marshalPairs(const Pairs &pairs, char *buffer, size_t size) {
+  if (buffer == nullptr) {
+    return false;
+  }
+
+  char *pos = buffer;
+  const char *end = buffer + size;
+
+  // Write number of pairs.
+  uint32_t num_pairs =
+      htowasm(pairs.size(), contextOrEffectiveContext() != nullptr
+                                ? contextOrEffectiveContext()->wasmVm()->usesWasmByteOrder()
+                                : false);
+  if (pos + sizeof(uint32_t) > end) {
+    return false;
+  }
+  ::memcpy(pos, &num_pairs, sizeof(uint32_t));
+  pos += sizeof(uint32_t);
+
+  for (const auto &p : pairs) {
+    // Write name length.
+    uint32_t name_len =
+        htowasm(p.first.size(), contextOrEffectiveContext() != nullptr
+                                    ? contextOrEffectiveContext()->wasmVm()->usesWasmByteOrder()
+                                    : false);
+    if (pos + sizeof(uint32_t) > end) {
+      return false;
+    }
+    ::memcpy(pos, &name_len, sizeof(uint32_t));
+    pos += sizeof(uint32_t);
+
+    // Write value length.
+    uint32_t value_len =
+        htowasm(p.second.size(), contextOrEffectiveContext() != nullptr
+                                     ? contextOrEffectiveContext()->wasmVm()->usesWasmByteOrder()
+                                     : false);
+    if (pos + sizeof(uint32_t) > end) {
+      return false;
+    }
+    ::memcpy(pos, &value_len, sizeof(uint32_t));
+    pos += sizeof(uint32_t);
+  }
+
+  for (const auto &p : pairs) {
+    // Write name.
+    if (pos + p.first.size() + 1 > end) {
+      return false;
+    }
+    ::memcpy(pos, p.first.data(), p.first.size());
+    pos += p.first.size();
+    *pos++ = '\0'; // NULL-terminated string.
+
+    // Write value.
+    if (pos + p.second.size() + 1 > end) {
+      return false;
+    }
+    ::memcpy(pos, p.second.data(), p.second.size());
+    pos += p.second.size();
+    *pos++ = '\0'; // NULL-terminated string.
+  }
+
+  return pos == end;
+}
+
+Pairs PairsUtil::toPairs(std::string_view buffer) {
+  if (buffer.data() == nullptr || buffer.size() > PROXY_WASM_HOST_PAIRS_MAX_BYTES) {
+    return {};
+  }
+
+  const char *pos = buffer.data();
+  const char *end = buffer.data() + buffer.size();
+
+  // Read number of pairs.
+  if (pos + sizeof(uint32_t) > end) {
+    return {};
+  }
+  uint32_t num_pairs = wasmtoh(*reinterpret_cast<const uint32_t *>(pos),
+                               contextOrEffectiveContext() != nullptr
+                                   ? contextOrEffectiveContext()->wasmVm()->usesWasmByteOrder()
+                                   : false);
+  pos += sizeof(uint32_t);
+
+  // Check if we're not going to exceed the limit.
+  if (num_pairs > PROXY_WASM_HOST_PAIRS_MAX_COUNT) {
+    return {};
+  }
+  if (pos + num_pairs * 2 * sizeof(uint32_t) > end) {
+    return {};
+  }
+
+  Sizes sizes;
+  sizes.resize(num_pairs);
+
+  for (auto &s : sizes) {
+    // Read name length.
+    if (pos + sizeof(uint32_t) > end) {
+      return {};
+    }
+    s.first = wasmtoh(*reinterpret_cast<const uint32_t *>(pos),
+                      contextOrEffectiveContext() != nullptr
+                          ? contextOrEffectiveContext()->wasmVm()->usesWasmByteOrder()
+                          : false);
+    pos += sizeof(uint32_t);
+
+    // Read value length.
+    if (pos + sizeof(uint32_t) > end) {
+      return {};
+    }
+    s.second = wasmtoh(*reinterpret_cast<const uint32_t *>(pos),
+                       contextOrEffectiveContext() != nullptr
+                           ? contextOrEffectiveContext()->wasmVm()->usesWasmByteOrder()
+                           : false);
+    pos += sizeof(uint32_t);
+  }
+
+  Pairs pairs;
+  pairs.resize(num_pairs);
+
+  for (uint32_t i = 0; i < num_pairs; i++) {
+    auto &s = sizes[i];
+    auto &p = pairs[i];
+
+    // Don't overread.
+    if (pos + s.first + 1 > end) {
+      return {};
+    }
+    p.first = std::string_view(pos, s.first);
+    pos += s.first;
+    if (*pos++ != '\0') { // NULL-terminated string.
+      return {};
+    }
+
+    // Don't overread.
+    if (pos + s.second + 1 > end) {
+      return {};
+    }
+    p.second = std::string_view(pos, s.second);
+    pos += s.second;
+    if (*pos++ != '\0') { // NULL-terminated string.
+      return {};
+    }
+  }
+
+  if (pos != end) {
+    return {};
+  }
+
+  return pairs;
+}
+
+} // namespace proxy_wasm
diff --git a/plugins/experimental/wasm/lib/src/signature_util.cc b/plugins/experimental/wasm/lib/src/signature_util.cc
index a8f2936..46b9d33 100644
--- a/plugins/experimental/wasm/lib/src/signature_util.cc
+++ b/plugins/experimental/wasm/lib/src/signature_util.cc
@@ -86,7 +86,7 @@
 
   uint32_t alg_id;
   std::memcpy(&alg_id, payload.data(), sizeof(uint32_t));
-  alg_id = wasmtoh(alg_id);
+  alg_id = wasmtoh(alg_id, true);
 
   if (alg_id != 2) {
     message = "Signature has a wrong alg_id (want: 2, is: " + std::to_string(alg_id) + ")";
diff --git a/plugins/experimental/wasm/lib/src/v8/v8.cc b/plugins/experimental/wasm/lib/src/v8/v8.cc
index abad6d5..61779c1 100644
--- a/plugins/experimental/wasm/lib/src/v8/v8.cc
+++ b/plugins/experimental/wasm/lib/src/v8/v8.cc
@@ -30,13 +30,10 @@
 
 #include "include/v8-version.h"
 #include "include/v8.h"
+#include "src/flags/flags.h"
 #include "src/wasm/c-api.h"
 #include "wasm-api/wasm.hh"
 
-namespace v8::internal {
-extern unsigned int FLAG_wasm_max_mem_pages;
-} // namespace v8::internal
-
 namespace proxy_wasm {
 namespace v8 {
 
@@ -45,7 +42,8 @@
   static wasm::own<wasm::Engine> engine;
 
   std::call_once(init, []() {
-    ::v8::internal::FLAG_wasm_max_mem_pages =
+    ::v8::internal::v8_flags.liftoff = false;
+    ::v8::internal::v8_flags.wasm_max_mem_pages =
         PROXY_WASM_HOST_MAX_WASM_MEMORY_SIZE_BYTES / PROXY_WASM_HOST_WASM_MEMORY_PAGE_SIZE_BYTES;
     ::v8::V8::EnableWebAssemblyTrapHandler(true);
     engine = wasm::Engine::make();
@@ -103,8 +101,11 @@
 #undef _GET_MODULE_FUNCTION
 
   void terminate() override;
+  bool usesWasmByteOrder() override { return true; }
 
 private:
+  wasm::own<wasm::Trap> trap(std::string message);
+
   std::string getFailMessage(std::string_view function_name, wasm::own<wasm::Trap> trap);
 
   template <typename... Args>
@@ -465,6 +466,10 @@
 
 std::optional<std::string_view> V8::getMemory(uint64_t pointer, uint64_t size) {
   assert(memory_ != nullptr);
+  // Make sure we're operating in a wasm32 memory space.
+  if (pointer > UINT32_MAX || size > UINT32_MAX || pointer + size > UINT32_MAX) {
+    return std::nullopt;
+  }
   if (pointer + size > memory_->data_size()) {
     return std::nullopt;
   }
@@ -473,6 +478,10 @@
 
 bool V8::setMemory(uint64_t pointer, uint64_t size, const void *data) {
   assert(memory_ != nullptr);
+  // Make sure we're operating in a wasm32 memory space.
+  if (pointer > UINT32_MAX || size > UINT32_MAX || pointer + size > UINT32_MAX) {
+    return false;
+  }
   if (pointer + size > memory_->data_size()) {
     return false;
   }
@@ -482,25 +491,37 @@
 
 bool V8::getWord(uint64_t pointer, Word *word) {
   constexpr auto size = sizeof(uint32_t);
+  // Make sure we're operating in a wasm32 memory space.
+  if (pointer > UINT32_MAX || pointer + size > UINT32_MAX) {
+    return false;
+  }
   if (pointer + size > memory_->data_size()) {
     return false;
   }
   uint32_t word32;
   ::memcpy(&word32, memory_->data() + pointer, size);
-  word->u64_ = wasmtoh(word32);
+  word->u64_ = wasmtoh(word32, true);
   return true;
 }
 
 bool V8::setWord(uint64_t pointer, Word word) {
   constexpr auto size = sizeof(uint32_t);
+  // Make sure we're operating in a wasm32 memory space.
+  if (pointer > UINT32_MAX || pointer + size > UINT32_MAX) {
+    return false;
+  }
   if (pointer + size > memory_->data_size()) {
     return false;
   }
-  uint32_t word32 = htowasm(word.u32());
+  uint32_t word32 = htowasm(word.u32(), true);
   ::memcpy(memory_->data() + pointer, &word32, size);
   return true;
 }
 
+wasm::own<wasm::Trap> V8::trap(std::string message) {
+  return wasm::Trap::make(store_.get(), wasm::Message::make(std::move(message)));
+}
+
 template <typename... Args>
 void V8::registerHostFunctionImpl(std::string_view module_name, std::string_view function_name,
                                   void (*function)(Args...)) {
@@ -517,6 +538,9 @@
           func_data->vm_->integration()->trace("[vm->host] " + func_data->name_ + "(" +
                                                printValues(params, sizeof...(Args)) + ")");
         }
+        if (!func_data->vm_->isHostFunctionAllowed(func_data->name_)) {
+          return dynamic_cast<V8 *>(func_data->vm_)->trap("restricted_callback");
+        }
         auto args = convertValTypesToArgsTuple<std::tuple<Args...>>(params);
         auto function = reinterpret_cast<void (*)(Args...)>(func_data->raw_func_);
         std::apply(function, args);
@@ -550,6 +574,9 @@
           func_data->vm_->integration()->trace("[vm->host] " + func_data->name_ + "(" +
                                                printValues(params, sizeof...(Args)) + ")");
         }
+        if (!func_data->vm_->isHostFunctionAllowed(func_data->name_)) {
+          return dynamic_cast<V8 *>(func_data->vm_)->trap("restricted_callback");
+        }
         auto args = convertValTypesToArgsTuple<std::tuple<Args...>>(params);
         auto function = reinterpret_cast<R (*)(Args...)>(func_data->raw_func_);
         R rvalue = std::apply(function, args);
diff --git a/plugins/experimental/wasm/lib/src/wamr/types.h b/plugins/experimental/wasm/lib/src/wamr/types.h
index f6e5168..7252c5c 100644
--- a/plugins/experimental/wasm/lib/src/wamr/types.h
+++ b/plugins/experimental/wasm/lib/src/wamr/types.h
@@ -21,6 +21,7 @@
 using WasmFuncPtr = common::CSmartPtr<wasm_func_t, wasm_func_delete>;
 using WasmStorePtr = common::CSmartPtr<wasm_store_t, wasm_store_delete>;
 using WasmModulePtr = common::CSmartPtr<wasm_module_t, wasm_module_delete>;
+using WasmSharedModulePtr = common::CSmartPtr<wasm_shared_module_t, wasm_shared_module_delete>;
 using WasmMemoryPtr = common::CSmartPtr<wasm_memory_t, wasm_memory_delete>;
 using WasmTablePtr = common::CSmartPtr<wasm_table_t, wasm_table_delete>;
 using WasmInstancePtr = common::CSmartPtr<wasm_instance_t, wasm_instance_delete>;
diff --git a/plugins/experimental/wasm/lib/src/wamr/wamr.cc b/plugins/experimental/wasm/lib/src/wamr/wamr.cc
index 93cae4d..577b678 100644
--- a/plugins/experimental/wasm/lib/src/wamr/wamr.cc
+++ b/plugins/experimental/wasm/lib/src/wamr/wamr.cc
@@ -58,8 +58,8 @@
   std::string_view getEngineName() override { return "wamr"; }
   std::string_view getPrecompiledSectionName() override { return ""; }
 
-  Cloneable cloneable() override { return Cloneable::NotCloneable; }
-  std::unique_ptr<WasmVm> clone() override { return nullptr; }
+  Cloneable cloneable() override { return Cloneable::CompiledBytecode; }
+  std::unique_ptr<WasmVm> clone() override;
 
   bool load(std::string_view bytecode, std::string_view precompiled,
             const std::unordered_map<uint32_t, std::string> &function_names) override;
@@ -87,6 +87,7 @@
 #undef _GET_MODULE_FUNCTION
 
   void terminate() override {}
+  bool usesWasmByteOrder() override { return true; }
 
 private:
   template <typename... Args>
@@ -107,6 +108,7 @@
 
   WasmStorePtr store_;
   WasmModulePtr module_;
+  WasmSharedModulePtr shared_module_;
   WasmInstancePtr instance_;
 
   WasmMemoryPtr memory_;
@@ -131,9 +133,41 @@
     return false;
   }
 
+  shared_module_ = wasm_module_share(module_.get());
+  if (shared_module_ == nullptr) {
+    return false;
+  }
+
   return true;
 }
 
+std::unique_ptr<WasmVm> Wamr::clone() {
+  assert(module_ != nullptr);
+
+  auto vm = std::make_unique<Wamr>();
+  if (vm == nullptr) {
+    return nullptr;
+  }
+
+  vm->store_ = wasm_store_new(engine());
+  if (vm->store_ == nullptr) {
+    return nullptr;
+  }
+
+  vm->module_ = wasm_module_obtain(vm->store_.get(), shared_module_.get());
+  if (vm->module_ == nullptr) {
+    return nullptr;
+  }
+
+  auto *integration_clone = integration()->clone();
+  if (integration_clone == nullptr) {
+    return nullptr;
+  }
+  vm->integration().reset(integration_clone);
+
+  return vm;
+}
+
 static bool equalValTypes(const wasm_valtype_vec_t *left, const wasm_valtype_vec_t *right) {
   if (left->size != right->size) {
     return false;
@@ -296,7 +330,7 @@
     return false;
   }
 
-  wasm_extern_vec_t imports_vec = {imports.size(), imports.data()};
+  wasm_extern_vec_t imports_vec = {imports.size(), imports.data(), imports.size()};
   instance_ = wasm_instance_new(store_.get(), module_.get(), &imports_vec, nullptr);
   if (instance_ == nullptr) {
     fail(FailState::UnableToInitializeCode, "Failed to create new Wasm instance");
@@ -368,7 +402,7 @@
 
   uint32_t word32;
   ::memcpy(&word32, wasm_memory_data(memory_.get()) + pointer, size);
-  word->u64_ = wasmtoh(word32);
+  word->u64_ = wasmtoh(word32, true);
   return true;
 }
 
@@ -377,7 +411,7 @@
   if (pointer + size > wasm_memory_data_size(memory_.get())) {
     return false;
   }
-  uint32_t word32 = htowasm(word.u32());
+  uint32_t word32 = htowasm(word.u32(), true);
   ::memcpy(wasm_memory_data(memory_.get()) + pointer, &word32, size);
   return true;
 }
@@ -578,9 +612,9 @@
     if (trap) {
       WasmByteVec error_message;
       wasm_trap_message(trap.get(), error_message.get());
+      std::string message(error_message.get()->data); // NULL-terminated
       fail(FailState::RuntimeError,
-           "Function: " + std::string(function_name) + " failed:\n" +
-               std::string(error_message.get()->data, error_message.get()->size));
+           "Function: " + std::string(function_name) + " failed: " + message);
       return;
     }
     if (log) {
@@ -628,9 +662,9 @@
     if (trap) {
       WasmByteVec error_message;
       wasm_trap_message(trap.get(), error_message.get());
+      std::string message(error_message.get()->data); // NULL-terminated
       fail(FailState::RuntimeError,
-           "Function: " + std::string(function_name) + " failed:\n" +
-               std::string(error_message.get()->data, error_message.get()->size));
+           "Function: " + std::string(function_name) + " failed: " + message);
       return R{};
     }
     R ret = convertValueTypeToArg<R>(results.data[0]);
diff --git a/plugins/experimental/wasm/lib/src/wasm.cc b/plugins/experimental/wasm/lib/src/wasm.cc
index 2295e98..5519b3e 100644
--- a/plugins/experimental/wasm/lib/src/wasm.cc
+++ b/plugins/experimental/wasm/lib/src/wasm.cc
@@ -355,6 +355,25 @@
 }
 
 void WasmBase::startVm(ContextBase *root_context) {
+  // wasi_snapshot_preview1.clock_time_get
+  wasm_vm_->setRestrictedCallback(
+      true, {// logging (Proxy-Wasm)
+             "env.proxy_log",
+             // logging (stdout/stderr)
+             "wasi_unstable.fd_write", "wasi_snapshot_preview1.fd_write",
+             // args
+             "wasi_unstable.args_sizes_get", "wasi_snapshot_preview1.args_sizes_get",
+             "wasi_unstable.args_get", "wasi_snapshot_preview1.args_get",
+             // environment variables
+             "wasi_unstable.environ_sizes_get", "wasi_snapshot_preview1.environ_sizes_get",
+             "wasi_unstable.environ_get", "wasi_snapshot_preview1.environ_get",
+             // preopened files/directories
+             "wasi_unstable.fd_prestat_get", "wasi_snapshot_preview1.fd_prestat_get",
+             "wasi_unstable.fd_prestat_dir_name", "wasi_snapshot_preview1.fd_prestat_dir_name",
+             // time
+             "wasi_unstable.clock_time_get", "wasi_snapshot_preview1.clock_time_get",
+             // random
+             "wasi_unstable.random_get", "wasi_snapshot_preview1.random_get"});
   if (_initialize_) {
     // WASI reactor.
     _initialize_(root_context);
@@ -370,6 +389,7 @@
     // WASI command.
     _start_(root_context);
   }
+  wasm_vm_->setRestrictedCallback(false);
 }
 
 bool WasmBase::configure(ContextBase *root_context, std::shared_ptr<PluginBase> plugin) {
@@ -447,6 +467,35 @@
   }
 }
 
+bool WasmHandleBase::canary(const std::shared_ptr<PluginBase> &plugin,
+                            const WasmHandleCloneFactory &clone_factory) {
+  if (this->wasm() == nullptr) {
+    return false;
+  }
+  auto configuration_canary_handle = clone_factory(shared_from_this());
+  if (!configuration_canary_handle) {
+    this->wasm()->fail(FailState::UnableToCloneVm, "Failed to clone Base Wasm");
+    return false;
+  }
+  if (!configuration_canary_handle->wasm()->initialize()) {
+    configuration_canary_handle->wasm()->fail(FailState::UnableToInitializeCode,
+                                              "Failed to initialize Wasm code");
+    return false;
+  }
+  auto *root_context = configuration_canary_handle->wasm()->start(plugin);
+  if (root_context == nullptr) {
+    configuration_canary_handle->wasm()->fail(FailState::StartFailed, "Failed to start base Wasm");
+    return false;
+  }
+  if (!configuration_canary_handle->wasm()->configure(root_context, plugin)) {
+    configuration_canary_handle->wasm()->fail(FailState::ConfigureFailed,
+                                              "Failed to configure base Wasm plugin");
+    return false;
+  }
+  configuration_canary_handle->kill();
+  return true;
+}
+
 std::shared_ptr<WasmHandleBase> createWasm(const std::string &vm_key, const std::string &code,
                                            const std::shared_ptr<PluginBase> &plugin,
                                            const WasmHandleFactory &factory,
@@ -465,44 +514,29 @@
         base_wasms->erase(it);
       }
     }
-    if (wasm_handle) {
-      return wasm_handle;
-    }
-    wasm_handle = factory(vm_key);
     if (!wasm_handle) {
-      return nullptr;
+      // If no cached base_wasm, creates a new base_wasm, loads the code and initializes it.
+      wasm_handle = factory(vm_key);
+      if (!wasm_handle) {
+        return nullptr;
+      }
+      if (!wasm_handle->wasm()->load(code, allow_precompiled)) {
+        wasm_handle->wasm()->fail(FailState::UnableToInitializeCode, "Failed to load Wasm code");
+        return nullptr;
+      }
+      if (!wasm_handle->wasm()->initialize()) {
+        wasm_handle->wasm()->fail(FailState::UnableToInitializeCode,
+                                  "Failed to initialize Wasm code");
+        return nullptr;
+      }
+      (*base_wasms)[vm_key] = wasm_handle;
     }
-    (*base_wasms)[vm_key] = wasm_handle;
   }
 
-  if (!wasm_handle->wasm()->load(code, allow_precompiled)) {
-    wasm_handle->wasm()->fail(FailState::UnableToInitializeCode, "Failed to load Wasm code");
+  // Either creating new one or reusing the existing one, apply canary for each plugin.
+  if (!wasm_handle->canary(plugin, clone_factory)) {
     return nullptr;
   }
-  if (!wasm_handle->wasm()->initialize()) {
-    wasm_handle->wasm()->fail(FailState::UnableToInitializeCode, "Failed to initialize Wasm code");
-    return nullptr;
-  }
-  auto configuration_canary_handle = clone_factory(wasm_handle);
-  if (!configuration_canary_handle) {
-    wasm_handle->wasm()->fail(FailState::UnableToCloneVm, "Failed to clone Base Wasm");
-    return nullptr;
-  }
-  if (!configuration_canary_handle->wasm()->initialize()) {
-    wasm_handle->wasm()->fail(FailState::UnableToInitializeCode, "Failed to initialize Wasm code");
-    return nullptr;
-  }
-  auto *root_context = configuration_canary_handle->wasm()->start(plugin);
-  if (root_context == nullptr) {
-    configuration_canary_handle->wasm()->fail(FailState::StartFailed, "Failed to start base Wasm");
-    return nullptr;
-  }
-  if (!configuration_canary_handle->wasm()->configure(root_context, plugin)) {
-    configuration_canary_handle->wasm()->fail(FailState::ConfigureFailed,
-                                              "Failed to configure base Wasm plugin");
-    return nullptr;
-  }
-  configuration_canary_handle->kill();
   return wasm_handle;
 };
 
diff --git a/plugins/experimental/wasm/lib/src/wasmedge/wasmedge.cc b/plugins/experimental/wasm/lib/src/wasmedge/wasmedge.cc
index 0a0a10d..38b8a9c 100644
--- a/plugins/experimental/wasm/lib/src/wasmedge/wasmedge.cc
+++ b/plugins/experimental/wasm/lib/src/wasmedge/wasmedge.cc
@@ -281,6 +281,7 @@
                              std::function<R(ContextBase *, Args...)> *function);
 
   void terminate() override {}
+  bool usesWasmByteOrder() override { return true; }
 
   WasmEdgeLoaderPtr loader_;
   WasmEdgeValidatorPtr validator_;
@@ -416,7 +417,7 @@
   auto *func_type = newWasmEdgeFuncType<std::tuple<Args...>>();
   data->vm_ = this;
   data->raw_func_ = reinterpret_cast<void *>(function);
-  data->callback_ = [](void *data, WasmEdge_MemoryInstanceContext * /*MemCxt*/,
+  data->callback_ = [](void *data, const WasmEdge_CallingFrameContext * /*CallFrameCxt*/,
                        const WasmEdge_Value *Params,
                        WasmEdge_Value * /*Returns*/) -> WasmEdge_Result {
     auto *func_data = reinterpret_cast<HostFuncData *>(data);
@@ -463,7 +464,7 @@
   auto *func_type = newWasmEdgeFuncType<R, std::tuple<Args...>>();
   data->vm_ = this;
   data->raw_func_ = reinterpret_cast<void *>(function);
-  data->callback_ = [](void *data, WasmEdge_MemoryInstanceContext * /*MemCxt*/,
+  data->callback_ = [](void *data, const WasmEdge_CallingFrameContext * /*CallFrameCxt*/,
                        const WasmEdge_Value *Params, WasmEdge_Value *Returns) -> WasmEdge_Result {
     auto *func_data = reinterpret_cast<HostFuncData *>(data);
     const bool log = func_data->vm_->cmpLogLevel(LogLevel::trace);
@@ -540,8 +541,8 @@
     WasmEdge_Result res =
         WasmEdge_ExecutorInvoke(executor_.get(), func_cxt, params, sizeof...(Args), nullptr, 0);
     if (!WasmEdge_ResultOK(res)) {
-      fail(FailState::RuntimeError, "Function: " + std::string(function_name) + " failed:\n" +
-                                        WasmEdge_ResultGetMessage(res));
+      fail(FailState::RuntimeError, "Function: " + std::string(function_name) +
+                                        " failed: " + WasmEdge_ResultGetMessage(res));
       return;
     }
     if (log) {
@@ -594,8 +595,8 @@
     WasmEdge_Result res =
         WasmEdge_ExecutorInvoke(executor_.get(), func_cxt, params, sizeof...(Args), results, 1);
     if (!WasmEdge_ResultOK(res)) {
-      fail(FailState::RuntimeError, "Function: " + std::string(function_name) + " failed:\n" +
-                                        WasmEdge_ResultGetMessage(res));
+      fail(FailState::RuntimeError, "Function: " + std::string(function_name) +
+                                        " failed: " + WasmEdge_ResultGetMessage(res));
       return R{};
     }
     R ret = convValTypeToArg<R>(results[0]);
diff --git a/plugins/experimental/wasm/lib/src/wasmtime/wasmtime.cc b/plugins/experimental/wasm/lib/src/wasmtime/wasmtime.cc
index 06f4f0e..2cfc218 100644
--- a/plugins/experimental/wasm/lib/src/wasmtime/wasmtime.cc
+++ b/plugins/experimental/wasm/lib/src/wasmtime/wasmtime.cc
@@ -98,6 +98,7 @@
                              std::function<R(ContextBase *, Args...)> *function);
 
   void terminate() override {}
+  bool usesWasmByteOrder() override { return true; }
 
   WasmStorePtr store_;
   WasmModulePtr module_;
@@ -394,7 +395,7 @@
 
   uint32_t word32;
   ::memcpy(&word32, wasm_memory_data(memory_.get()) + pointer, size);
-  word->u64_ = wasmtoh(word32);
+  word->u64_ = wasmtoh(word32, true);
   return true;
 }
 
@@ -403,7 +404,7 @@
   if (pointer + size > wasm_memory_data_size(memory_.get())) {
     return false;
   }
-  uint32_t word32 = htowasm(word.u32());
+  uint32_t word32 = htowasm(word.u32(), true);
   ::memcpy(wasm_memory_data(memory_.get()) + pointer, &word32, size);
   return true;
 }
@@ -616,9 +617,9 @@
     if (trap) {
       WasmByteVec error_message;
       wasm_trap_message(trap.get(), error_message.get());
+      std::string message(error_message.get()->data); // NULL-terminated
       fail(FailState::RuntimeError,
-           "Function: " + std::string(function_name) + " failed:\n" +
-               std::string(error_message.get()->data, error_message.get()->size));
+           "Function: " + std::string(function_name) + " failed: " + message);
       return;
     }
     if (log) {
@@ -678,9 +679,9 @@
     if (trap) {
       WasmByteVec error_message;
       wasm_trap_message(trap.get(), error_message.get());
+      std::string message(error_message.get()->data); // NULL-terminated
       fail(FailState::RuntimeError,
-           "Function: " + std::string(function_name) + " failed:\n" +
-               std::string(error_message.get()->data, error_message.get()->size));
+           "Function: " + std::string(function_name) + " failed: " + message);
       return R{};
     }
     R ret = convertValueTypeToArg<R>(results.data[0]);
diff --git a/plugins/experimental/wasm/lib/src/wavm/wavm.cc b/plugins/experimental/wasm/lib/src/wavm/wavm.cc
index 1041b7f..670eb7c 100644
--- a/plugins/experimental/wasm/lib/src/wavm/wavm.cc
+++ b/plugins/experimental/wasm/lib/src/wavm/wavm.cc
@@ -230,6 +230,7 @@
 #undef _REGISTER_CALLBACK
 
   void terminate() override {}
+  bool usesWasmByteOrder() override { return true; }
 
   IR::Module ir_module_;
   WAVM::Runtime::ModuleRef module_ = nullptr;
@@ -389,12 +390,12 @@
   auto *p = reinterpret_cast<char *>(memory_base_ + pointer);
   uint32_t data32;
   memcpy(&data32, p, sizeof(uint32_t));
-  data->u64_ = wasmtoh(data32);
+  data->u64_ = wasmtoh(data32, true);
   return true;
 }
 
 bool Wavm::setWord(uint64_t pointer, Word data) {
-  uint32_t data32 = htowasm(data.u32());
+  uint32_t data32 = htowasm(data.u32(), true);
   return setMemory(pointer, sizeof(uint32_t), &data32);
 }
 
diff --git a/plugins/experimental/wasm/wasm_main.cc b/plugins/experimental/wasm/wasm_main.cc
index e5c92f4..5b691b1 100644
--- a/plugins/experimental/wasm/wasm_main.cc
+++ b/plugins/experimental/wasm/wasm_main.cc
@@ -19,7 +19,7 @@
 #include "ts/ts.h"
 #include <yaml-cpp/yaml.h>
 #include "ats_wasm.h"
-#include "include/proxy-wasm/wavm.h"
+#include "include/proxy-wasm/wamr.h"
 
 #include <getopt.h>
 #include <sys/types.h>
@@ -414,13 +414,14 @@
     return false;
   }
 
-  auto wasm = std::make_shared<ats_wasm::Wasm>(proxy_wasm::createWavmVm(), // VM
+  auto wasm                      = std::make_shared<ats_wasm::Wasm>(proxy_wasm::createWamrVm(), // VM
                                                vm_id,                      // vm_id
                                                vm_configuration,           // vm_configuration
                                                "",                         // vm_key,
                                                envs,                       // envs
                                                cap_maps                    // allowed capabilities
   );
+  wasm->wasm_vm()->integration() = std::make_unique<ats_wasm::ATSWasmVmIntegration>();
 
   auto plugin = std::make_shared<proxy_wasm::PluginBase>(name,          // name
                                                          root_id,       // root_id
diff --git a/plugins/lua/ts_lua_http_config.c b/plugins/lua/ts_lua_http_config.c
index 807008c..95d3e2f 100644
--- a/plugins/lua/ts_lua_http_config.c
+++ b/plugins/lua/ts_lua_http_config.c
@@ -144,6 +144,7 @@
   TS_LUA_CONFIG_BODY_FACTORY_RESPONSE_SUPPRESSION_MODE        = TS_CONFIG_BODY_FACTORY_RESPONSE_SUPPRESSION_MODE,
   TS_LUA_CONFIG_ENABLE_PARENT_TIMEOUT_MARKDOWNS               = TS_CONFIG_HTTP_ENABLE_PARENT_TIMEOUT_MARKDOWNS,
   TS_LUA_CONFIG_DISABLE_PARENT_MARKDOWNS                      = TS_CONFIG_HTTP_DISABLE_PARENT_MARKDOWNS,
+  TS_LUA_CONFIG_NET_DEFAULT_INACTIVITY_TIMEOUT                = TS_CONFIG_NET_DEFAULT_INACTIVITY_TIMEOUT,
   TS_LUA_CONFIG_LAST_ENTRY                                    = TS_CONFIG_LAST_ENTRY,
 } TSLuaOverridableConfigKey;
 
@@ -280,6 +281,7 @@
   TS_LUA_MAKE_VAR_ITEM(TS_LUA_CONFIG_BODY_FACTORY_RESPONSE_SUPPRESSION_MODE),
   TS_LUA_MAKE_VAR_ITEM(TS_LUA_CONFIG_ENABLE_PARENT_TIMEOUT_MARKDOWNS),
   TS_LUA_MAKE_VAR_ITEM(TS_LUA_CONFIG_DISABLE_PARENT_MARKDOWNS),
+  TS_LUA_MAKE_VAR_ITEM(TS_LUA_CONFIG_NET_DEFAULT_INACTIVITY_TIMEOUT),
   TS_LUA_MAKE_VAR_ITEM(TS_LUA_CONFIG_LAST_ENTRY),
 };
 
diff --git a/plugins/s3_auth/s3_auth.cc b/plugins/s3_auth/s3_auth.cc
index 352b44c..b67c8ca 100644
--- a/plugins/s3_auth/s3_auth.cc
+++ b/plugins/s3_auth/s3_auth.cc
@@ -488,7 +488,19 @@
     if (_conf_rld_act != nullptr && !TSActionDone(_conf_rld_act)) {
       TSActionCancel(_conf_rld_act);
     }
-    _conf_rld_act = TSContScheduleOnPool(_conf_rld, delay * 1000, TS_THREAD_POOL_NET);
+    _conf_rld_act = TSContScheduleOnPool(_conf_rld, delay * 1000, TS_THREAD_POOL_TASK);
+  }
+
+  /**
+     Clear _conf_rld_act if the event handler is handling the action
+   */
+  void
+  check_current_action(void *edata)
+  {
+    // Following what's TSContScheduleOnPool does before returning TSAction
+    if (_conf_rld_act == ((TSAction)((uintptr_t)edata | 0x1))) {
+      _conf_rld_act = nullptr;
+    }
   }
 
   ts::shared_mutex reload_mutex;
@@ -1049,6 +1061,7 @@
   {
     std::unique_lock lock(s3->reload_mutex);
     s3->copy_changes_from(file_config);
+    s3->check_current_action(edata);
   }
 
   if (s3->expiration() == 0) {
diff --git a/proxy/ProxySession.cc b/proxy/ProxySession.cc
index 6a00e32..a4feee9 100644
--- a/proxy/ProxySession.cc
+++ b/proxy/ProxySession.cc
@@ -231,6 +231,14 @@
 }
 
 void
+ProxySession::set_default_inactivity_timeout(ink_hrtime timeout_in)
+{
+  if (_vc) {
+    _vc->set_default_inactivity_timeout(timeout_in);
+  }
+}
+
+void
 ProxySession::cancel_inactivity_timeout()
 {
   if (_vc) {
diff --git a/proxy/ProxySession.h b/proxy/ProxySession.h
index 4e5a217..941cc05 100644
--- a/proxy/ProxySession.h
+++ b/proxy/ProxySession.h
@@ -117,6 +117,7 @@
 
   virtual void set_active_timeout(ink_hrtime timeout_in);
   virtual void set_inactivity_timeout(ink_hrtime timeout_in);
+  virtual void set_default_inactivity_timeout(ink_hrtime timeout_in);
   virtual void cancel_inactivity_timeout();
   virtual void cancel_active_timeout();
 
diff --git a/proxy/ProxyTransaction.h b/proxy/ProxyTransaction.h
index 261af68..c6d1ba7 100644
--- a/proxy/ProxyTransaction.h
+++ b/proxy/ProxyTransaction.h
@@ -47,6 +47,7 @@
 
   virtual void set_active_timeout(ink_hrtime timeout_in);
   virtual void set_inactivity_timeout(ink_hrtime timeout_in);
+  virtual void set_default_inactivity_timeout(ink_hrtime timeout_in);
   virtual void cancel_inactivity_timeout();
   virtual void cancel_active_timeout();
 
@@ -255,6 +256,14 @@
 }
 
 inline void
+ProxyTransaction::set_default_inactivity_timeout(ink_hrtime timeout_in)
+{
+  if (_proxy_ssn) {
+    _proxy_ssn->set_default_inactivity_timeout(timeout_in);
+  }
+}
+
+inline void
 ProxyTransaction::cancel_inactivity_timeout()
 {
   if (_proxy_ssn) {
diff --git a/proxy/http/HttpConfig.cc b/proxy/http/HttpConfig.cc
index 78d86cf..377985e 100644
--- a/proxy/http/HttpConfig.cc
+++ b/proxy/http/HttpConfig.cc
@@ -1165,6 +1165,8 @@
 
   HttpEstablishStaticConfigLongLong(c.oride.tunnel_activity_check_period, "proxy.config.tunnel.activity_check_period");
 
+  HttpEstablishStaticConfigLongLong(c.oride.default_inactivity_timeout, "proxy.config.net.default_inactivity_timeout");
+
   HttpEstablishStaticConfigLongLong(c.http_request_line_max_size, "proxy.config.http.request_line_max_size");
   HttpEstablishStaticConfigLongLong(c.http_hdr_field_max_size, "proxy.config.http.header_field_max_size");
 
@@ -1468,6 +1470,8 @@
   params->oride.max_proxy_cycles                = m_master.oride.max_proxy_cycles;
   params->oride.tunnel_activity_check_period    = m_master.oride.tunnel_activity_check_period;
 
+  params->oride.default_inactivity_timeout = m_master.oride.default_inactivity_timeout;
+
   params->http_request_line_max_size = m_master.http_request_line_max_size;
   params->http_hdr_field_max_size    = m_master.http_hdr_field_max_size;
 
diff --git a/proxy/http/HttpConfig.h b/proxy/http/HttpConfig.h
index f22e11c..4ce28a1 100644
--- a/proxy/http/HttpConfig.h
+++ b/proxy/http/HttpConfig.h
@@ -513,6 +513,7 @@
   MgmtByte attach_server_session_to_client    = 0;
   MgmtInt max_proxy_cycles                    = 0;
   MgmtInt tunnel_activity_check_period        = 0;
+  MgmtInt default_inactivity_timeout          = 24 * 60 * 60;
 
   MgmtByte forward_connect_method = 0;
 
diff --git a/proxy/http/HttpSM.cc b/proxy/http/HttpSM.cc
index 8d352ea..e53889f 100644
--- a/proxy/http/HttpSM.cc
+++ b/proxy/http/HttpSM.cc
@@ -127,9 +127,10 @@
       // nothing to forward
       return 0;
     } else {
+      Debug("proxyprotocol", "vc_in had no Proxy Protocol. Manufacturing from the vc_in socket.");
       // set info from incoming NetVConnection
       IpEndpoint local = vc_in->get_local_endpoint();
-      info             = ProxyProtocol{pp_version, local.family(), local, vc_in->get_remote_endpoint()};
+      info             = ProxyProtocol{pp_version, local.family(), vc_in->get_remote_endpoint(), local};
     }
   }
 
@@ -6884,13 +6885,6 @@
 
   nbytes = server_transfer_init(buf, hdr_size);
 
-  if (t_state.is_cacheable_due_to_negative_caching_configuration &&
-      t_state.hdr_info.server_response.status_get() == HTTP_STATUS_NO_CONTENT) {
-    int s = sizeof("No Content") - 1;
-    buf->write("No Content", s);
-    nbytes += s;
-  }
-
   HTTP_SM_SET_DEFAULT_HANDLER(&HttpSM::tunnel_handler);
 
   HttpTunnelProducer *p =
@@ -6996,7 +6990,13 @@
     client_response_hdr_bytes = 0;
   }
 
-  client_request_body_bytes = 0;
+  int64_t nbytes = 0;
+  if (t_state.txn_conf->proxy_protocol_out >= 0) {
+    nbytes = do_outbound_proxy_protocol(from_ua_buf, static_cast<NetVConnection *>(server_entry->vc), ua_txn->get_netvc(),
+                                        t_state.txn_conf->proxy_protocol_out);
+  }
+
+  client_request_body_bytes = nbytes;
   if (ua_raw_buffer_reader != nullptr) {
     client_request_body_bytes += from_ua_buf->write(ua_raw_buffer_reader, client_request_hdr_bytes);
     ua_raw_buffer_reader->dealloc();
diff --git a/proxy/http/HttpTransact.cc b/proxy/http/HttpTransact.cc
index b467e01..c79f698 100644
--- a/proxy/http/HttpTransact.cc
+++ b/proxy/http/HttpTransact.cc
@@ -1034,6 +1034,9 @@
   int host_len;
   const char *host = incoming_request->host_get(&host_len);
   TxnDebug("http_trans", "EndRemapRequest host is %.*s", host_len, host);
+  if (s->state_machine->ua_txn) {
+    s->state_machine->ua_txn->set_default_inactivity_timeout(HRTIME_SECONDS(s->txn_conf->default_inactivity_timeout));
+  }
 
   // Setting enable_redirection according to HttpConfig (master or overridable). We
   // defer this as late as possible, to allow plugins to modify the overridable
diff --git a/proxy/http/remap/UrlRewrite.cc b/proxy/http/remap/UrlRewrite.cc
index 3ae9979..2fede9c 100644
--- a/proxy/http/remap/UrlRewrite.cc
+++ b/proxy/http/remap/UrlRewrite.cc
@@ -766,7 +766,8 @@
     h_table->emplace(src_host, ht_contents);
   }
   if (!ht_contents->Insert(mapping)) {
-    Warning("Could not insert new mapping");
+    // Trie::Insert only fails due to an attempt to add a duplicate entry.
+    Warning("Could not insert new mapping: duplicated entry exists");
     return false;
   }
   return true;
diff --git a/proxy/http2/HPACK.cc b/proxy/http2/HPACK.cc
index 00103a8..57c53fe 100644
--- a/proxy/http2/HPACK.cc
+++ b/proxy/http2/HPACK.cc
@@ -99,6 +99,35 @@
   TS_HPACK_STATIC_TABLE_ENTRY_NUM
 };
 
+constexpr int HPACK_STATIC_TABLE_OFFSET[26] = {
+  TS_HPACK_STATIC_TABLE_ACCEPT_CHARSET,
+  TS_HPACK_STATIC_TABLE_ENTRY_NUM,
+  TS_HPACK_STATIC_TABLE_CACHE_CONTROL,
+  TS_HPACK_STATIC_TABLE_DATE,
+  TS_HPACK_STATIC_TABLE_ETAG,
+  TS_HPACK_STATIC_TABLE_FROM,
+  TS_HPACK_STATIC_TABLE_ENTRY_NUM,
+  TS_HPACK_STATIC_TABLE_HOST,
+  TS_HPACK_STATIC_TABLE_IF_MATCH,
+  TS_HPACK_STATIC_TABLE_ENTRY_NUM,
+  TS_HPACK_STATIC_TABLE_ENTRY_NUM,
+  TS_HPACK_STATIC_TABLE_LAST_MODIFIED,
+  TS_HPACK_STATIC_TABLE_MAX_FORWARDS,
+  TS_HPACK_STATIC_TABLE_ENTRY_NUM,
+  TS_HPACK_STATIC_TABLE_ENTRY_NUM,
+  TS_HPACK_STATIC_TABLE_PROXY_AUTHENTICATE,
+  TS_HPACK_STATIC_TABLE_ENTRY_NUM,
+  TS_HPACK_STATIC_TABLE_RANGE,
+  TS_HPACK_STATIC_TABLE_SERVER,
+  TS_HPACK_STATIC_TABLE_TRANSFER_ENCODING,
+  TS_HPACK_STATIC_TABLE_USER_AGENT,
+  TS_HPACK_STATIC_TABLE_VARY,
+  TS_HPACK_STATIC_TABLE_WWW_AUTHENTICATE,
+  TS_HPACK_STATIC_TABLE_ENTRY_NUM,
+  TS_HPACK_STATIC_TABLE_ENTRY_NUM,
+  TS_HPACK_STATIC_TABLE_ENTRY_NUM,
+};
+
 constexpr HpackHeaderField STATIC_TABLE[] = {{"", ""},
                                              {":authority", ""},
                                              {":method", "GET"},
@@ -189,6 +218,45 @@
   return ftype == HpackField::INDEXED_LITERAL || ftype == HpackField::NOINDEX_LITERAL || ftype == HpackField::NEVERINDEX_LITERAL;
 }
 
+// Try not to use memcmp(sv, sv) and strncasecmp(sv, sv) because we don't care which value comes first on a dictionary.
+// Return immediately if the lengths of given strings don't match.
+// Also, we noticed with profiling that taking char* and int was more performant than taking std::string_view.
+static inline bool
+match(const char *s1, int s1_len, const char *s2, int s2_len)
+{
+  if (s1_len != s2_len) {
+    return false;
+  }
+
+  if (s1 == s2) {
+    return true;
+  }
+
+  if (memcmp(s1, s2, s1_len) != 0) {
+    return false;
+  }
+
+  return true;
+}
+
+static inline bool
+match_ignore_case(const char *s1, int s1_len, const char *s2, int s2_len)
+{
+  if (s1_len != s2_len) {
+    return false;
+  }
+
+  if (s1 == s2) {
+    return true;
+  }
+
+  if (strncasecmp(s1, s2, s1_len) != 0) {
+    return false;
+  }
+
+  return true;
+}
+
 //
 // The first byte of an HPACK field unambiguously tells us what
 // kind of field it is. Field types are specified in the high 4 bits
@@ -227,13 +295,27 @@
   {
     HpackLookupResult result;
 
-    for (unsigned int index = 1; index < TS_HPACK_STATIC_TABLE_ENTRY_NUM; ++index) {
-      std::string_view name  = STATIC_TABLE[index].name;
-      std::string_view value = STATIC_TABLE[index].value;
+    // Limit the search range of static table
+    unsigned int start = 1; // First effective index for TS_HPACK_STATIC_TABLE_ENTRY is 1
+    unsigned int end   = TS_HPACK_STATIC_TABLE_ENTRY_NUM;
+    if (const auto c = header.name[0]; 'a' <= c && c <= 'z') {
+      start = HPACK_STATIC_TABLE_OFFSET[c - 'a'];
+      if ('z' != c) {
+        // This does not always set the ideal end index but works for some cases
+        end = HPACK_STATIC_TABLE_OFFSET[c - 'a' + 1];
+      }
+    } else if (':' == c) {
+      end = HPACK_STATIC_TABLE_OFFSET[0];
+    }
+
+    for (unsigned int index = start; index < end; ++index) {
+      // Profiling showed that use of const reference here is more performant than copying string_views.
+      const std::string_view &name  = STATIC_TABLE[index].name;
+      const std::string_view &value = STATIC_TABLE[index].value;
 
       // Check whether name (and value) are matched
-      if (memcmp(header.name, name) == 0) {
-        if (memcmp(header.value, value) == 0) {
+      if (match(header.name.data(), header.name.length(), name.data(), name.length())) {
+        if (match(header.value.data(), header.value.length(), value.data(), value.length())) {
           result.index      = index;
           result.index_type = HpackIndex::STATIC;
           result.match_type = HpackMatch::EXACT;
@@ -402,10 +484,9 @@
     std::string_view name    = m_field->name_get();
     std::string_view value   = m_field->value_get();
 
-    // TODO: replace `strcasecmp` with `memcmp`
     // Check whether name (and value) are matched
-    if (strcasecmp(header.name, name) == 0) {
-      if (memcmp(header.value, value) == 0) {
+    if (match_ignore_case(header.name.data(), header.name.length(), name.data(), name.length())) {
+      if (match(header.value.data(), header.value.length(), value.data(), value.length())) {
         result.index      = index;
         result.index_type = HpackIndex::DYNAMIC;
         result.match_type = HpackMatch::EXACT;
@@ -924,7 +1005,8 @@
     // - Authorization header obviously should not be indexed
     // - Short Cookie header should not be indexed because of low entropy
     HpackField field_type;
-    if ((value.size() < 20 && memcmp(name, HPACK_HDR_FIELD_COOKIE) == 0) || memcmp(name, HPACK_HDR_FIELD_AUTHORIZATION) == 0) {
+    if ((value.size() < 20 && match(name.data(), name.length(), HPACK_HDR_FIELD_COOKIE.data(), HPACK_HDR_FIELD_COOKIE.length())) ||
+        match(name.data(), name.length(), HPACK_HDR_FIELD_AUTHORIZATION.data(), HPACK_HDR_FIELD_AUTHORIZATION.length())) {
       field_type = HpackField::NEVERINDEX_LITERAL;
     } else {
       field_type = HpackField::INDEXED_LITERAL;
diff --git a/proxy/http2/Http2ConnectionState.h b/proxy/http2/Http2ConnectionState.h
index d1ee37d..ee9bc45 100644
--- a/proxy/http2/Http2ConnectionState.h
+++ b/proxy/http2/Http2ConnectionState.h
@@ -305,8 +305,8 @@
    */
   bool _local_rwnd_is_shrinking_in = false;
 
-  std::vector<size_t> _recent_rwnd_increment = {SIZE_MAX, SIZE_MAX, SIZE_MAX, SIZE_MAX, SIZE_MAX};
-  int _recent_rwnd_increment_index           = 0;
+  std::array<size_t, 5> _recent_rwnd_increment = {SIZE_MAX, SIZE_MAX, SIZE_MAX, SIZE_MAX, SIZE_MAX};
+  int _recent_rwnd_increment_index             = 0;
 
   Http2FrequencyCounter _received_settings_counter;
   Http2FrequencyCounter _received_settings_frame_counter;
diff --git a/proxy/http2/Http2Stream.h b/proxy/http2/Http2Stream.h
index 7647818..01c5b88 100644
--- a/proxy/http2/Http2Stream.h
+++ b/proxy/http2/Http2Stream.h
@@ -216,8 +216,8 @@
   ssize_t _peer_rwnd  = 0;
   ssize_t _local_rwnd = 0;
 
-  std::vector<size_t> _recent_rwnd_increment = {SIZE_MAX, SIZE_MAX, SIZE_MAX, SIZE_MAX, SIZE_MAX};
-  int _recent_rwnd_increment_index           = 0;
+  std::array<size_t, 5> _recent_rwnd_increment = {SIZE_MAX, SIZE_MAX, SIZE_MAX, SIZE_MAX, SIZE_MAX};
+  int _recent_rwnd_increment_index             = 0;
 
   Event *cross_thread_event = nullptr;
   Event *read_event         = nullptr;
diff --git a/src/shared/overridable_txn_vars.cc b/src/shared/overridable_txn_vars.cc
index d54f584..9a89064 100644
--- a/src/shared/overridable_txn_vars.cc
+++ b/src/shared/overridable_txn_vars.cc
@@ -169,5 +169,5 @@
       {TS_CONFIG_BODY_FACTORY_RESPONSE_SUPPRESSION_MODE, TS_RECORDDATATYPE_INT}},
      {"proxy.config.http.parent_proxy.enable_parent_timeout_markdowns",
       {TS_CONFIG_HTTP_ENABLE_PARENT_TIMEOUT_MARKDOWNS, TS_RECORDDATATYPE_INT}},
-     {"proxy.config.http.parent_proxy.disable_parent_markdowns",
-      {TS_CONFIG_HTTP_DISABLE_PARENT_MARKDOWNS, TS_RECORDDATATYPE_INT}}});
+     {"proxy.config.http.parent_proxy.disable_parent_markdowns", {TS_CONFIG_HTTP_DISABLE_PARENT_MARKDOWNS, TS_RECORDDATATYPE_INT}},
+     {"proxy.config.net.default_inactivity_timeout", {TS_CONFIG_NET_DEFAULT_INACTIVITY_TIMEOUT, TS_RECORDDATATYPE_INT}}});
diff --git a/src/traffic_server/InkAPI.cc b/src/traffic_server/InkAPI.cc
index 872dc4e..d9f7ee8 100644
--- a/src/traffic_server/InkAPI.cc
+++ b/src/traffic_server/InkAPI.cc
@@ -8824,6 +8824,9 @@
   case TS_CONFIG_HTTP_DISABLE_PARENT_MARKDOWNS:
     ret = _memberp_to_generic(&overridableHttpConfig->disable_parent_markdowns, conv);
     break;
+  case TS_CONFIG_NET_DEFAULT_INACTIVITY_TIMEOUT:
+    ret = _memberp_to_generic(&overridableHttpConfig->default_inactivity_timeout, conv);
+    break;
 
   // This helps avoiding compiler warnings, yet detect unhandled enum members.
   case TS_CONFIG_NULL:
diff --git a/src/traffic_server/InkAPITest.cc b/src/traffic_server/InkAPITest.cc
index da277bd..74a26b4 100644
--- a/src/traffic_server/InkAPITest.cc
+++ b/src/traffic_server/InkAPITest.cc
@@ -8707,7 +8707,8 @@
    "proxy.config.net.sock_notsent_lowat",
    "proxy.config.body_factory.response_suppression_mode",
    "proxy.config.http.parent_proxy.enable_parent_timeout_markdowns",
-   "proxy.config.http.parent_proxy.disable_parent_markdowns"}};
+   "proxy.config.http.parent_proxy.disable_parent_markdowns",
+   "proxy.config.net.default_inactivity_timeout"}};
 
 extern ClassAllocator<HttpSM> httpSMAllocator;
 
diff --git a/tests/gold_tests/autest-site/trafficserver.test.ext b/tests/gold_tests/autest-site/trafficserver.test.ext
index b958f0b..89d5cfa 100755
--- a/tests/gold_tests/autest-site/trafficserver.test.ext
+++ b/tests/gold_tests/autest-site/trafficserver.test.ext
@@ -41,7 +41,8 @@
 def MakeATSProcess(obj, name, command='traffic_server', select_ports=True,
                    enable_tls=False, enable_cache=True, enable_quic=False,
                    block_for_debug=False, log_data=default_log_data,
-                   use_traffic_out=True, dump_runroot=True):
+                   use_traffic_out=True, dump_runroot=True,
+                   enable_proxy_protocol=False):
     #####################################
     # common locations
 
@@ -327,6 +328,14 @@
         if enable_tls:
             get_port(p, "ssl_port")
             get_port(p, "ssl_portv6")
+
+        if enable_proxy_protocol:
+            get_port(p, "proxy_protocol_port")
+            get_port(p, "proxy_protocol_portv6")
+
+            if enable_tls:
+                get_port(p, "proxy_protocol_ssl_port")
+                get_port(p, "proxy_protocol_ssl_portv6")
     else:
         p.Variables.port = 8080
         p.Variables.portv6 = 8080
@@ -381,6 +390,10 @@
         if enable_quic:
             port_str += " {ssl_port}:quic {ssl_portv6}:quic:ipv6".format(
                 ssl_port=p.Variables.ssl_port, ssl_portv6=p.Variables.ssl_portv6)
+        if enable_proxy_protocol:
+            port_str += f" {p.Variables.proxy_protocol_port}:pp {p.Variables.proxy_protocol_portv6}:pp:ipv6"
+            if enable_tls:
+                port_str += f" {p.Variables.proxy_protocol_ssl_port}:pp:ssl {p.Variables.proxy_protocol_ssl_portv6}:pp:ssl:ipv6"
         #p.Env['PROXY_CONFIG_HTTP_SERVER_PORTS'] = port_str
         p.Disk.records_config.update({
             'proxy.config.http.server_ports': port_str,
diff --git a/tests/gold_tests/connect/connect.test.py b/tests/gold_tests/connect/connect.test.py
index 3552b78..95b2595 100644
--- a/tests/gold_tests/connect/connect.test.py
+++ b/tests/gold_tests/connect/connect.test.py
@@ -18,6 +18,7 @@
 
 from enum import Enum
 import os
+import re
 
 Test.Summary = 'Exercise HTTP CONNECT Method'
 Test.ContinueOnFail = True
@@ -106,3 +107,61 @@
 
 
 ConnectTest().run()
+
+
+class ConnectViaPVTest:
+    # This test also executes the CONNECT request but using proxy verifier to
+    # generate traffic
+    connectReplayFile = "replays/connect.replay.yaml"
+
+    def __init__(self):
+        self.setupOriginServer()
+        self.setupTS()
+
+    def setupOriginServer(self):
+        self.server = Test.MakeVerifierServerProcess(
+            "connect-verifier-server",
+            self.connectReplayFile)
+        # Verify server output
+        self.server.Streams.stdout += Testers.ExcludesExpression(
+            "uuid: 1",
+            "Verify the CONNECT request doesn't reach the server.")
+        self.server.Streams.stdout += Testers.ContainsExpression(
+            "GET /get HTTP/1.1\nuuid: 2", reflags=re.MULTILINE,
+            description="Verify the server gets the second request.")
+
+    def setupTS(self):
+        self.ts = Test.MakeATSProcess("connect-ts")
+
+        self.ts.Disk.records_config.update({
+            'proxy.config.diags.debug.enabled': 1,
+            'proxy.config.diags.debug.tags': 'http',
+            'proxy.config.http.server_ports': f"{self.ts.Variables.port}",
+            'proxy.config.http.connect_ports': f"{self.server.Variables.http_port}",
+        })
+
+        self.ts.Disk.remap_config.AddLines([
+            f"map / http://127.0.0.1:{self.server.Variables.http_port}/",
+        ])
+        # Verify ts logs
+        self.ts.Disk.traffic_out.Content += Testers.ContainsExpression(
+            f"Proxy's Request.*\n.*\nCONNECT 127.0.0.1:{self.server.Variables.http_port} HTTP/1.1", reflags=re.MULTILINE,
+            description="Verify that ATS recognizes the CONNECT request.")
+
+    def runTraffic(self):
+        tr = Test.AddTestRun("Verify correct handling of CONNECT request")
+        tr.AddVerifierClientProcess(
+            "connect-client",
+            self.connectReplayFile,
+            http_ports=[self.ts.Variables.port],
+            other_args='--thread-limit 1')
+        tr.Processes.Default.StartBefore(self.server)
+        tr.Processes.Default.StartBefore(self.ts)
+        tr.StillRunningAfter = self.server
+        tr.StillRunningAfter = self.ts
+
+    def run(self):
+        self.runTraffic()
+
+
+ConnectViaPVTest().run()
diff --git a/tests/gold_tests/connect/replays/connect.replay.yaml b/tests/gold_tests/connect/replays/connect.replay.yaml
new file mode 100644
index 0000000..fd98ff3
--- /dev/null
+++ b/tests/gold_tests/connect/replays/connect.replay.yaml
@@ -0,0 +1,64 @@
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+
+#
+# This replay file executes a CONNECT request and a subsequent GET request
+#
+meta:
+  version: "1.0"
+
+sessions:
+  - transactions:
+      - client-request:
+          method: CONNECT
+          version: "1.1"
+          url: www.example.com:80
+          headers:
+            fields:
+              - [uuid, 1]
+              - [Host, www.example.com:80]
+        # This is a CONNECT request so it should not reach the origin server
+        server-response:
+          status: 200
+
+        # ATS returns a 200 responses to client when it establishes a tunnel
+        # between the client and server
+        proxy-response:
+          status: 200
+
+      # Once the tunnel between client and server is established, subsequent
+      # requests will reach the server(via the tunnel)
+      - client-request:
+          method: GET
+          version: "1.1"
+          url: /get
+          headers:
+            fields:
+              - [uuid, 2]
+              - [Host, www.example.com]
+        server-response:
+          status: 200
+          reason: OK
+          headers:
+            fields:
+              - [X-Response-1, response_tunnel]
+
+        proxy-response:
+          status: 200
+          headers:
+            field:
+              # make sure the client gets the X-Response header
+              - [X-Response-1, { value: response_tunnel, as: equal }]
diff --git a/tests/gold_tests/pluginTest/certifier/certifier.test.py b/tests/gold_tests/pluginTest/certifier/certifier.test.py
new file mode 100644
index 0000000..202259e
--- /dev/null
+++ b/tests/gold_tests/pluginTest/certifier/certifier.test.py
@@ -0,0 +1,170 @@
+'''
+Test certifier plugin behaviors
+'''
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the #  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+import hashlib
+import os
+import re
+
+Test.Summary = '''
+Test certifier plugin behaviors
+'''
+
+Test.SkipUnless(
+    Condition.PluginExists('certifier.so')
+)
+
+
+class DynamicCertTest:
+    httpsReplayFile = "replays/https.replay.yaml"
+    certPathSrc = os.path.join(Test.TestDirectory, "certs")
+    host = "www.tls.com"
+    certPathDest = ""
+
+    def __init__(self):
+        self.setupOriginServer()
+        self.setupTS()
+
+    def setupOriginServer(self):
+        self.server = Test.MakeVerifierServerProcess("verifier-server1", self.httpsReplayFile)
+
+    def setupTS(self):
+        self.ts = Test.MakeATSProcess("ts1", enable_tls=True)
+        self.ts.addDefaultSSLFiles()
+        # copy over the cert store in which the certs will be generated/stored
+        self.certPathDest = os.path.join(self.ts.Variables.CONFIGDIR, "certifier-certs")
+        Setup.Copy(self.certPathSrc, self.certPathDest)
+        Setup.MakeDir(os.path.join(self.certPathDest, 'store'))
+        self.ts.Disk.records_config.update({
+            "proxy.config.diags.debug.enabled": 1,
+            "proxy.config.diags.debug.tags": "http|certifier|ssl",
+            "proxy.config.ssl.server.cert.path": f'{self.ts.Variables.SSLDir}',
+            "proxy.config.ssl.server.private_key.path": f'{self.ts.Variables.SSLDir}',
+        })
+        self.ts.Disk.ssl_multicert_config.AddLine(
+            'dest_ip=* ssl_cert_name=server.pem ssl_key_name=server.key'
+        )
+        self.ts.Disk.remap_config.AddLine(
+            f"map / http://127.0.0.1:{self.server.Variables.http_port}/",
+        )
+        self.ts.Disk.plugin_config.AddLine(
+            f'certifier.so -s {os.path.join(self.certPathDest, "store")} -m 1000 -c {os.path.join(self.certPathDest, "ca.cert")} -k {os.path.join(self.certPathDest, "ca.key")} -r {os.path.join(self.certPathDest, "ca-serial.txt")}')
+        # Verify logs for dynamic generation of certs
+        self.ts.Disk.traffic_out.Content += Testers.ContainsExpression(
+            "Creating shadow certs",
+            "Verify the certifier plugin generates the certificate dynamically.")
+
+    def runHTTPSTraffic(self):
+        tr = Test.AddTestRun("Test dynamic generation of certs")
+        tr.AddVerifierClientProcess(
+            "client1",
+            self.httpsReplayFile,
+            http_ports=[self.ts.Variables.port],
+            https_ports=[self.ts.Variables.ssl_port],
+            other_args='--thread-limit 1')
+        tr.Processes.Default.StartBefore(self.server)
+        tr.Processes.Default.StartBefore(self.ts)
+        tr.StillRunningAfter = self.server
+        tr.StillRunningAfter = self.ts
+
+    def verifyCert(self, certPath):
+        tr = Test.AddTestRun("Verify the content of the generated cert")
+        tr.Processes.Default.Command = f'openssl x509 -in {certPath} -text -noout'
+        tr.Processes.Default.ReturnCode = 0
+        # Verify certificate content
+        tr.Processes.Default.Streams.All += Testers.ContainsExpression(
+            "Subject: CN = www.tls.com", "Subject should match the host in the request")
+        tr.Processes.Default.Streams.All += Testers.ContainsExpression(
+            r"X509v3 extensions:\n.*X509v3 Subject Alternative Name:.*\n.*DNS:www.tls.com",
+            "Should contain the SAN extension",
+            reflags=re.MULTILINE)
+
+    def verifyCertNotExist(self, certPath):
+        tr = Test.AddTestRun("Verify the cert doesn't exist in the store")
+        tr.Processes.Default.Command = "echo verify"
+        certFile = tr.Disk.File(certPath, exists=False)
+
+    def run(self):
+        # the certifier plugin generates the cert and store it in a directory
+        # named with the first three character of the md5 hash of the hostname
+        genCertPath = os.path.join(self.certPathDest,
+                                   'store',
+                                   str(hashlib.md5(self.host.encode('utf-8')).hexdigest()[:3]),
+                                   self.host + ".crt")
+        self.verifyCertNotExist(genCertPath)
+        self.runHTTPSTraffic()
+        self.verifyCert(genCertPath)
+
+
+DynamicCertTest().run()
+
+
+class ReuseExistingCertTest:
+    httpsReplayFile = "replays/https-two-sessions.replay.yaml"
+    certPathSrc = os.path.join(Test.TestDirectory, "certs")
+    host = "www.tls.com"
+    certPathDest = ""
+
+    def __init__(self):
+        self.setupOriginServer()
+        self.setupTS()
+
+    def setupOriginServer(self):
+        self.server = Test.MakeVerifierServerProcess("verifier-server2", self.httpsReplayFile)
+
+    def setupTS(self):
+        self.ts = Test.MakeATSProcess("ts2", enable_tls=True)
+        self.ts.addDefaultSSLFiles()
+        # copy over the cert store in which the certs will be generated/stored
+        self.certPathDest = os.path.join(self.ts.Variables.CONFIGDIR, "certifier-certs")
+        Setup.Copy(self.certPathSrc, self.certPathDest)
+        Setup.MakeDir(os.path.join(self.certPathDest, 'store'))
+        self.ts.Disk.records_config.update({
+            "proxy.config.diags.debug.enabled": 1,
+            "proxy.config.diags.debug.tags": "http|certifier|ssl",
+            "proxy.config.ssl.server.cert.path": f'{self.ts.Variables.SSLDir}',
+            "proxy.config.ssl.server.private_key.path": f'{self.ts.Variables.SSLDir}',
+        })
+        self.ts.Disk.ssl_multicert_config.AddLine(
+            'dest_ip=* ssl_cert_name=server.pem ssl_key_name=server.key'
+        )
+        self.ts.Disk.remap_config.AddLine(
+            f"map / http://127.0.0.1:{self.server.Variables.http_port}/",
+        )
+        self.ts.Disk.plugin_config.AddLine(
+            f'certifier.so -s {os.path.join(self.certPathDest, "store")} -m 1000 -c {os.path.join(self.certPathDest, "ca.cert")} -k {os.path.join(self.certPathDest, "ca.key")} -r {os.path.join(self.certPathDest, "ca-serial.txt")}')
+        # Verify logs for reusing existing cert
+        self.ts.Disk.traffic_out.Content += Testers.ContainsExpression(
+            "Reuse existing cert and context for www.tls.com", "Should reuse the existing certificate")
+
+    def runHTTPSTraffic(self):
+        tr = Test.AddTestRun("Test dynamic generation of certs")
+        tr.AddVerifierClientProcess(
+            "client2",
+            self.httpsReplayFile,
+            http_ports=[self.ts.Variables.port],
+            https_ports=[self.ts.Variables.ssl_port],
+            other_args='--thread-limit 1')
+        tr.Processes.Default.StartBefore(self.server)
+        tr.Processes.Default.StartBefore(self.ts)
+        tr.StillRunningAfter = self.server
+        tr.StillRunningAfter = self.ts
+
+    def run(self):
+        self.runHTTPSTraffic()
+
+
+ReuseExistingCertTest().run()
diff --git a/tests/gold_tests/pluginTest/certifier/certs/ca-serial.txt b/tests/gold_tests/pluginTest/certifier/certs/ca-serial.txt
new file mode 100644
index 0000000..dd11724
--- /dev/null
+++ b/tests/gold_tests/pluginTest/certifier/certs/ca-serial.txt
@@ -0,0 +1 @@
+1001
diff --git a/tests/gold_tests/pluginTest/certifier/certs/ca.cert b/tests/gold_tests/pluginTest/certifier/certs/ca.cert
new file mode 100644
index 0000000..6fd35e6
--- /dev/null
+++ b/tests/gold_tests/pluginTest/certifier/certs/ca.cert
@@ -0,0 +1,21 @@
+-----BEGIN CERTIFICATE-----
+MIIDfzCCAmegAwIBAgIUefFBfvHwnreoKWM/RncW4vqq028wDQYJKoZIhvcNAQEL
+BQAwTzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAklMMRUwEwYDVQQHDAxEZWZhdWx0
+IENpdHkxHDAaBgNVBAoME0RlZmF1bHQgQ29tcGFueSBMdGQwHhcNMjMwMTA5MDUx
+MzAzWhcNMjQwMTA5MDUxMzAzWjBPMQswCQYDVQQGEwJVUzELMAkGA1UECAwCSUwx
+FTATBgNVBAcMDERlZmF1bHQgQ2l0eTEcMBoGA1UECgwTRGVmYXVsdCBDb21wYW55
+IEx0ZDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMR3M/Xwyj0U0aKw
+xGVXvH3mZsLjnnjGJUNk67JkAunbX6vVoVvqgNBhSw0MqXfEDep7c2t5ay2sCh78
+denkLpF6Mx1cF4oUiy8+QzZQbHbD3+8Qd9HbdlmRmp+9QPV9hr/uPkxua72XniEo
+w2caVyG85yhmdfbk320SGVzEU35Dg115Yeq51P/h4IGx+ARSuGGEbLmFlr3GjAiz
+gfUyBqegD7qIggY6g/tGltrIXAM/lIBHBZD1nmcIcCuN4AeglZDAzurq0I10HZyK
+evMDP2z2rPqMP6UGqIWvIvD6JROu9b7hnkXbPGG2C/kyVnEyZ4NTSNzxolBBw/FL
+L/gSiXMCAwEAAaNTMFEwHQYDVR0OBBYEFMrJeTb/59hQWJTjn1PbUNUUUylQMB8G
+A1UdIwQYMBaAFMrJeTb/59hQWJTjn1PbUNUUUylQMA8GA1UdEwEB/wQFMAMBAf8w
+DQYJKoZIhvcNAQELBQADggEBAE+7VWR5nzlIWyP0G1VvcuaIKz9vUZ/64ZIXiV+2
+f7/LTFrxnhh+Jt66z7/ST6T1pb5CvR0mTialSdunMa04281bS5nYjOhaf3sCwsJY
+LRYpSqOJGYRz3o1NijHEPXX4+ER/hGp7Khiait0W8WBCnmLtjHyRxPNM/o1zPkls
+2TmVWPelluu7HcAlGuFGVSWltBjdJFapIOjyKlQBO+1o96SW9ElM0Mk0D3KnzGk0
+NbxVnYKdTG5OBZu52rQ3h+gkQgBqTmw8byd9snLZTFTpaFeLElEASRQVwE3Y5iY4
+daiCk6QI0TLziMADJCjoR01fd2wUk6nAajINttjhj/0/YC8=
+-----END CERTIFICATE-----
diff --git a/tests/gold_tests/pluginTest/certifier/certs/ca.key b/tests/gold_tests/pluginTest/certifier/certs/ca.key
new file mode 100644
index 0000000..a7eac43
--- /dev/null
+++ b/tests/gold_tests/pluginTest/certifier/certs/ca.key
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDEdzP18Mo9FNGi
+sMRlV7x95mbC4554xiVDZOuyZALp21+r1aFb6oDQYUsNDKl3xA3qe3NreWstrAoe
+/HXp5C6RejMdXBeKFIsvPkM2UGx2w9/vEHfR23ZZkZqfvUD1fYa/7j5Mbmu9l54h
+KMNnGlchvOcoZnX25N9tEhlcxFN+Q4NdeWHqudT/4eCBsfgEUrhhhGy5hZa9xowI
+s4H1MganoA+6iIIGOoP7RpbayFwDP5SARwWQ9Z5nCHArjeAHoJWQwM7q6tCNdB2c
+inrzAz9s9qz6jD+lBqiFryLw+iUTrvW+4Z5F2zxhtgv5MlZxMmeDU0jc8aJQQcPx
+Sy/4EolzAgMBAAECggEBALeqRH4wC7CMjOpN3FxtZ/4+7WRWTGjVvDJD34AWtQ6Q
+keDfRwjZsnCb9ACQDMG1G9lQbXdUvBinU5LiKw1rzwkbNzvEQxFH4VJaH5VgG/Xv
+aaJhP9KFGx/i2a1pzJdzMQyumbj0JW7Jlf+jzwDNBBW7NlJzbuZP0ntxxxoNQ4yt
+xuxWlweByX0JK4jrdOL7oHTOuORsQekZXzKwyXPRvlhnI7LFhl4Bz6jj98Pc0p1I
+CyZ6K7JT9fI3JTAEtWA8EyrZq7dpUZk8u2gbgdv92I3/t1EAagSPoRj5zU8zJK8x
+EkDxGiYccZWj6W6CJm1eQmftWAyPjHVn3f76Ks4OGDECgYEA7Lx+G8QX4Jyh4nl9
+gEXWpyNEvb61DCibuLSoEb4YETBHQpVDQZYO1T6+Gy6oKFo+O845ryVpzdjSnIKF
+dUiH0cH/MbE1mW6cLO//LFDDoHVhhckTUp7EKBppwtGQpJCkDgp7RDZ0qeSp8SMo
+H4SHBc0eTFKi3AJt+6Wcs86ZdCkCgYEA1HPStVFUII8DGhCVLruFHbJ/BX1JGJfz
+/8He4yYaS+8entUMQM27vENm5fbKut+xqog4cQnva0LfbGorNpu1LKhIL8It83k4
+vCj+tQ2tX6GPY8TnwSjM17Q1yvxw4J5cuazOXXkZmTHKqXPcbGKnKZPx46qc4+zT
+6hh6d/dHJDsCgYAt7vTZFlc7saiHdOMw/FwF2gYAevxN+6MbV5I0vxmUXDW3aZa/
+JwqBvcbNJ2RhUAE7QWxX236H/kX/MCQM+aHAxU9qqOOpfZi372knhBQxEhV2C+m/
+4iZVUaqrrlXOWHI8dzQrrBU+0atXqmailuhL69yxGmeXILGOXjfle3Sy4QKBgQCc
+nfnScu8axXLa7yia9+BNIT4klNSjDbnxbEFcoMU3/0JKgvJyf3gLdIDRvJp9ItT9
+y0gYhON75iOA78+FhUoNbs0wb+yiFVYu1+XVIQ0Td62WNh1HN0WzKoWRa4HnBLeE
+pZDINbMaTSuCugff2uYyb38df7QDDp62b28xKxjF6wKBgQC04bEipcdqhBygfIHX
+vVVUedLZECxwpd0Li/J7UvwbWVGnV38i1L9rGqut0HqeYRHSrb31+YhJsRXmBL1a
+Ds9VVQ59j6cYcYWS/xUdLyC4cEwgujWn86cqVXfAl1eUJd3RWfxch9CYWBKN+sgQ
+ty11bt1YJPoYzjC7GLiiCIODxQ==
+-----END PRIVATE KEY-----
diff --git a/tests/gold_tests/pluginTest/certifier/replays/https-two-sessions.replay.yaml b/tests/gold_tests/pluginTest/certifier/replays/https-two-sessions.replay.yaml
new file mode 100644
index 0000000..9d2da6d
--- /dev/null
+++ b/tests/gold_tests/pluginTest/certifier/replays/https-two-sessions.replay.yaml
@@ -0,0 +1,66 @@
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+
+meta:
+  version: "1.0"
+
+sessions:
+  - protocol: [{ name: tls, sni: www.tls.com }, { name: tcp }, { name: ip }]
+
+    transactions:
+      - client-request:
+          method: "GET"
+          version: "1.1"
+          url: "/path/4"
+          headers:
+            fields:
+              - [Host, www.tls.com]
+              - [Content-Length, 10]
+              - [uuid, 1]
+
+        server-response:
+          status: 200
+          reason: OK
+          headers:
+            fields:
+              - [Content-Length, 16]
+
+        proxy-response:
+          status: 200
+
+  # separate session to trigger a new TLS handshake to engage the certifier
+  - protocol: [{ name: tls, sni: www.tls.com }, { name: tcp }, { name: ip }]
+
+    transactions:
+      - client-request:
+          method: "GET"
+          version: "1.1"
+          url: "/path/4"
+          headers:
+            fields:
+              - [Host, www.tls.com]
+              - [Content-Length, 10]
+              - [uuid, 2]
+
+        server-response:
+          status: 200
+          reason: OK
+          headers:
+            fields:
+              - [Content-Length, 16]
+
+        proxy-response:
+          status: 200
diff --git a/tests/gold_tests/pluginTest/certifier/replays/https.replay.yaml b/tests/gold_tests/pluginTest/certifier/replays/https.replay.yaml
new file mode 100644
index 0000000..084a94a
--- /dev/null
+++ b/tests/gold_tests/pluginTest/certifier/replays/https.replay.yaml
@@ -0,0 +1,42 @@
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+
+meta:
+  version: "1.0"
+
+sessions:
+  - protocol: [{ name: tls, sni: www.tls.com }, { name: tcp }, { name: ip }]
+
+    transactions:
+      - client-request:
+          method: "POST"
+          version: "1.1"
+          url: "/path/4"
+          headers:
+            fields:
+              - [Host, www.tls.com]
+              - [Content-Length, 10]
+              - [uuid, 4]
+
+        server-response:
+          status: 200
+          reason: OK
+          headers:
+            fields:
+              - [Content-Length, 16]
+
+        proxy-response:
+          status: 200
diff --git a/tests/gold_tests/pluginTest/s3_auth/gold/s3_auth_parsing.gold b/tests/gold_tests/pluginTest/s3_auth/gold/s3_auth_parsing.gold
deleted file mode 100644
index 2303e28..0000000
--- a/tests/gold_tests/pluginTest/s3_auth/gold/s3_auth_parsing.gold
+++ /dev/null
@@ -1,17 +0,0 @@
-*   Trying 127.0.0.1:``...
-* Connected to 127.0.0.1 (127.0.0.1) port `` (#0)
-> GET /s3-bucket HTTP/1.1
-> Host: www.example.com
-> User-Agent: curl/``
-> Accept: */*
-> 
-* Mark bundle as not supporting multiuse
-< HTTP/1.1 200 OK
-< Content-Length: 8
-< Date: ``
-< Age: ``
-< Connection: keep-alive
-< Server: ``
-< 
-{ [8 bytes data]
-* Connection #0 to host 127.0.0.1 left intact
diff --git a/tests/gold_tests/pluginTest/s3_auth/s3_auth_config.test.py b/tests/gold_tests/pluginTest/s3_auth/s3_auth_config.test.py
index 5523ff6..ffcdc3b 100644
--- a/tests/gold_tests/pluginTest/s3_auth/s3_auth_config.test.py
+++ b/tests/gold_tests/pluginTest/s3_auth/s3_auth_config.test.py
@@ -59,7 +59,8 @@
 tr.Processes.Default.ReturnCode = 0
 tr.Processes.Default.StartBefore(server)
 tr.Processes.Default.StartBefore(ts)
-tr.Processes.Default.Streams.stderr = "gold/s3_auth_parsing.gold"
+tr.Processes.Default.Streams.stderr.Content = Testers.ContainsExpression("200 OK", "expected 200 response")
+tr.Processes.Default.Streams.stderr.Content += Testers.ContainsExpression("Content-Length: 8", "expected content-length 8")
 tr.StillRunningAfter = server
 
 ts.Disk.traffic_out.Content = "gold/s3_auth_parsing_ts.gold"
diff --git a/tests/gold_tests/proxy_protocol/proxy_protocol.test.py b/tests/gold_tests/proxy_protocol/proxy_protocol.test.py
index 4125ba0..ed58315 100644
--- a/tests/gold_tests/proxy_protocol/proxy_protocol.test.py
+++ b/tests/gold_tests/proxy_protocol/proxy_protocol.test.py
@@ -17,6 +17,7 @@
 #  limitations under the License.
 
 import os
+from ports import get_port
 import sys
 
 Test.Summary = 'Test PROXY Protocol'
@@ -27,6 +28,8 @@
 
 
 class ProxyProtocolTest:
+    """Test that ATS can receive Proxy Protocol."""
+
     def __init__(self):
         self.setupOriginServer()
         self.setupTS()
@@ -39,7 +42,7 @@
 '''
 
     def setupTS(self):
-        self.ts = Test.MakeATSProcess("ts", enable_tls=True, enable_cache=False)
+        self.ts = Test.MakeATSProcess("ts_in", enable_tls=True, enable_cache=False, enable_proxy_protocol=True)
 
         self.ts.addDefaultSSLFiles()
         self.ts.Disk.ssl_multicert_config.AddLine("dest_ip=* ssl_cert_name=server.pem ssl_key_name=server.key")
@@ -48,7 +51,6 @@
             f"map / http://127.0.0.1:{self.httpbin.Variables.Port}/")
 
         self.ts.Disk.records_config.update({
-            "proxy.config.http.server_ports": f"{self.ts.Variables.port}:pp {self.ts.Variables.ssl_port}:ssl:pp",
             "proxy.config.http.proxy_protocol_allowlist": "127.0.0.1",
             "proxy.config.http.insert_forwarded": "for|by=ip|proto",
             "proxy.config.ssl.server.cert.path": f"{self.ts.Variables.SSLDir}",
@@ -76,7 +78,7 @@
         tr = Test.AddTestRun()
         tr.Processes.Default.StartBefore(self.httpbin)
         tr.Processes.Default.StartBefore(self.ts)
-        tr.Processes.Default.Command = f"curl -vs --haproxy-protocol http://localhost:{self.ts.Variables.port}/get | {self.json_printer}"
+        tr.Processes.Default.Command = f"curl -vs --haproxy-protocol http://localhost:{self.ts.Variables.proxy_protocol_port}/get | {self.json_printer}"
         tr.Processes.Default.ReturnCode = 0
         tr.Processes.Default.Streams.stdout = "gold/test_case_0_stdout.gold"
         tr.Processes.Default.Streams.stderr = "gold/test_case_0_stderr.gold"
@@ -88,7 +90,7 @@
         Incoming PROXY Protocol v1 on SSL port
         """
         tr = Test.AddTestRun()
-        tr.Processes.Default.Command = f"curl -vsk --haproxy-protocol --http1.1 https://localhost:{self.ts.Variables.ssl_port}/get | {self.json_printer}"
+        tr.Processes.Default.Command = f"curl -vsk --haproxy-protocol --http1.1 https://localhost:{self.ts.Variables.proxy_protocol_ssl_port}/get | {self.json_printer}"
         tr.Processes.Default.ReturnCode = 0
         tr.Processes.Default.Streams.stdout = "gold/test_case_1_stdout.gold"
         tr.Processes.Default.Streams.stderr = "gold/test_case_1_stderr.gold"
@@ -100,7 +102,7 @@
         Test with netcat
         """
         tr = Test.AddTestRun()
-        tr.Processes.Default.Command = f"echo 'PROXY TCP4 198.51.100.1 198.51.100.2 51137 80\r\nGET /get HTTP/1.1\r\nHost: 127.0.0.1:80\r\n' | nc localhost {self.ts.Variables.port}"
+        tr.Processes.Default.Command = f"echo 'PROXY TCP4 198.51.100.1 198.51.100.2 51137 80\r\nGET /get HTTP/1.1\r\nHost: 127.0.0.1:80\r\n' | nc localhost {self.ts.Variables.proxy_protocol_port}"
         tr.Processes.Default.ReturnCode = 0
         tr.Processes.Default.Streams.stdout = "gold/test_case_2_stdout.gold"
         tr.StillRunningAfter = self.httpbin
@@ -127,4 +129,160 @@
         self.addTestCase99()
 
 
+class ProxyProtocolOutTest:
+    """Test that ATS can send Proxy Protocol."""
+
+    _pp_server = 'proxy_protocol_server.py'
+
+    _dns_counter = 0
+    _server_counter = 0
+    _ts_counter = 0
+
+    def __init__(self, pp_version: int, is_tunnel: bool) -> None:
+        """Initialize a ProxyProtocolOutTest.
+
+        :param pp_version: The Proxy Protocol version to use (1 or 2).
+        :param is_tunnel: Whether ATS should tunnel to the origin.
+        """
+
+        if pp_version not in (-1, 1, 2):
+            raise ValueError(
+                f'Invalid Proxy Protocol version (not 1 or 2): {pp_version}')
+        self._pp_version = pp_version
+        self._is_tunnel = is_tunnel
+
+    def setupOriginServer(self, tr: 'TestRun') -> None:
+        """Configure the origin server.
+
+        :param tr: The TestRun to associate the origin's Process with.
+        """
+        tr.Setup.CopyAs(self._pp_server, tr.RunDirectory)
+        cert_file = os.path.join(Test.Variables.AtsTestToolsDir, "ssl", "server.pem")
+        key_file = os.path.join(Test.Variables.AtsTestToolsDir, "ssl", "server.key")
+        tr.Setup.Copy(cert_file)
+        tr.Setup.Copy(key_file)
+        server = tr.Processes.Process(
+            f'server-{ProxyProtocolOutTest._server_counter}')
+        ProxyProtocolOutTest._server_counter += 1
+        server_port = get_port(server, "external_port")
+        internal_port = get_port(server, "internal_port")
+        command = (
+            f'{sys.executable} {self._pp_server} '
+            f'server.pem server.key 127.0.0.1 {server_port} {internal_port}')
+        if not self._is_tunnel:
+            command += ' --plaintext'
+        server.Command = command
+        server.Ready = When.PortOpenv4(server_port)
+
+        self._server = server
+
+    def setupDNS(self, tr: 'TestRun') -> None:
+        """Configure the DNS server.
+
+        :param tr: The TestRun to associate the DNS's Process with.
+        """
+        self._dns = tr.MakeDNServer(
+            f'dns-{ProxyProtocolOutTest._dns_counter}',
+            default='127.0.0.1')
+        ProxyProtocolOutTest._dns_counter += 1
+
+    def setupTS(self, tr: 'TestRun') -> None:
+        """Configure Traffic Server."""
+        process_name = f'ts-out-{ProxyProtocolOutTest._ts_counter}'
+        ProxyProtocolOutTest._ts_counter += 1
+        self._ts = tr.MakeATSProcess(process_name, enable_tls=True,
+                                     enable_cache=False)
+
+        self._ts.addDefaultSSLFiles()
+        self._ts.Disk.ssl_multicert_config.AddLine(
+            "dest_ip=* ssl_cert_name=server.pem ssl_key_name=server.key"
+        )
+
+        self._ts.Disk.remap_config.AddLine(
+            f"map / http://backend.pp.origin.com:{self._server.Variables.external_port}/")
+
+        self._ts.Disk.records_config.update({
+            "proxy.config.ssl.server.cert.path": f"{self._ts.Variables.SSLDir}",
+            "proxy.config.ssl.server.private_key.path": f"{self._ts.Variables.SSLDir}",
+            "proxy.config.diags.debug.enabled": 1,
+            "proxy.config.diags.debug.tags": "http|proxyprotocol",
+            "proxy.config.http.proxy_protocol_out": self._pp_version,
+            "proxy.config.dns.nameservers": f"127.0.0.1:{self._dns.Variables.Port}",
+            "proxy.config.dns.resolv_conf": 'NULL'
+        })
+
+        if self._is_tunnel:
+            self._ts.Disk.records_config.update({
+                "proxy.config.http.connect_ports": f'{self._server.Variables.external_port}',
+            })
+
+            self._ts.Disk.sni_yaml.AddLines([
+                'sni:',
+                '- fqdn: pp.origin.com',
+                f'  tunnel_route: backend.pp.origin.com:{self._server.Variables.external_port}',
+            ])
+
+    def setLogExpectations(self, tr: 'TestRun') -> None:
+
+        tr.Processes.Default.Streams.All += Testers.ContainsExpression(
+            "HTTP/1.1 200 OK",
+            "Verify that curl got a 200 response")
+
+        if self._pp_version in (1, 2):
+            expected_pp = (
+                'PROXY TCP4 127.0.0.1 127.0.0.1 '
+                rf'\d+ {self._ts.Variables.ssl_port}'
+            )
+            self._server.Streams.All += Testers.ContainsExpression(
+                expected_pp,
+                "Verify the server got the expected Proxy Protocol string.")
+
+            self._server.Streams.All += Testers.ContainsExpression(
+                f'Received Proxy Protocol v{self._pp_version}',
+                "Verify the server got the expected Proxy Protocol version.")
+
+        if self._pp_version == -1:
+            self._server.Streams.All += Testers.ContainsExpression(
+                'No Proxy Protocol string found',
+                'There should be no Proxy Protocol string.')
+
+    def run(self) -> None:
+        """Run the test."""
+        description = f'Proxy Protocol v{self._pp_version} '
+        if self._is_tunnel:
+            description += "with blind tunneling"
+        else:
+            description += "without blind tunneling"
+        tr = Test.AddTestRun(description)
+
+        self.setupDNS(tr)
+        self.setupOriginServer(tr)
+        self.setupTS(tr)
+
+        self._ts.StartBefore(self._server)
+        self._ts.StartBefore(self._dns)
+        tr.Processes.Default.StartBefore(self._ts)
+
+        origin = f'pp.origin.com:{self._ts.Variables.ssl_port}'
+        command = (
+            'sleep1; curl -vsk --http1.1 '
+            f'--resolve "{origin}:127.0.0.1" '
+            f'https://{origin}/get'
+        )
+
+        tr.Processes.Default.Command = command
+        tr.Processes.Default.ReturnCode = 0
+        # Its only one transaction, so this should complete quickly. The test
+        # server often hangs if there are issues parsing the Proxy Protocol
+        # string.
+        tr.TimeOut = 5
+        self.setLogExpectations(tr)
+
+
 ProxyProtocolTest().run()
+
+ProxyProtocolOutTest(pp_version=-1, is_tunnel=False).run()
+ProxyProtocolOutTest(pp_version=1, is_tunnel=False).run()
+ProxyProtocolOutTest(pp_version=2, is_tunnel=False).run()
+ProxyProtocolOutTest(pp_version=1, is_tunnel=True).run()
+ProxyProtocolOutTest(pp_version=2, is_tunnel=True).run()
diff --git a/tests/gold_tests/proxy_protocol/proxy_protocol_server.py b/tests/gold_tests/proxy_protocol/proxy_protocol_server.py
new file mode 100644
index 0000000..af1a528
--- /dev/null
+++ b/tests/gold_tests/proxy_protocol/proxy_protocol_server.py
@@ -0,0 +1,381 @@
+#!/usr/bin/env python3
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+
+"""A simple server that expects and prints out the Proxy Protocol string."""
+
+import argparse
+import logging
+import socket
+import ssl
+import struct
+import sys
+import threading
+
+
+# Set a 10ms timeout for socket operations.
+TIMEOUT = .010
+
+PP_V2_PREFIX = b'\x0d\x0a\x0d\x0a\x00\x0d\x0a\x51\x55\x49\x54\x0a'
+
+
+# Create a condition variable for thread initialization.
+internal_thread_is_ready = threading.Condition()
+
+
+def parse_args() -> argparse.Namespace:
+    """Parse command line arguments."""
+    parser = argparse.ArgumentParser(description=__doc__)
+
+    parser.add_argument(
+        "certfile",
+        help="The path to the certificate file to use for TLS.")
+    parser.add_argument(
+        "keyfile",
+        help="The path to the key file to use for TLS.")
+    parser.add_argument(
+        "address",
+        help="The IP address to listen on.")
+    parser.add_argument(
+        "port",
+        type=int,
+        help="The port to listen on.")
+    parser.add_argument(
+        "internal_port",
+        type=int,
+        help="The internal port used to parse the TLS content.")
+    parser.add_argument(
+        "--plaintext",
+        action="store_true",
+        help="Listen for plaintext connections instead of TLS.")
+
+    return parser.parse_args()
+
+
+def receive_and_send_http(sock: socket.socket) -> None:
+    """Receive and send an HTTP request and response.
+
+    :param sock: The socket to receive the request on.
+    """
+    sock.settimeout(TIMEOUT)
+
+    # Read the request until the final CRLF is received.
+    received_request = b''
+    while True:
+        data = None
+        try:
+            data = sock.recv(1024)
+            logging.debug(f'Internal: received {len(data)} bytes')
+        except socket.timeout:
+            continue
+        if not data:
+            break
+        received_request += data
+
+        if b'\r\n\r\n' in received_request:
+            break
+    logging.info("Received request:")
+    logging.info(received_request.decode("utf-8"))
+
+    # Send a response.
+    response = (
+        "HTTP/1.1 200 OK\r\n"
+        "Content-Length: 0\r\n"
+        "Connection: close\r\n"
+        "\r\n"
+    )
+    logging.info(f'Sending:\n{response}')
+    try:
+        sock.sendall(response.encode("utf-8"))
+    except socket.timeout:
+        logging.error("Timeout sending a response.")
+
+
+def run_internal_server(cert_file: str, key_file: str,
+                        address: str, port: int,
+                        plaintext: bool) -> None:
+    """Run the internal server.
+
+    This server is receives the HTTP content with the Proxy Protocol prefix
+    stripped off by the client.
+
+    :param cert_file: The path to the certificate file to use for TLS.
+    :param key_file: The path to the key file to use for TLS.
+    :param address: The IP address to listen on.
+    :param port: The port to listen on.
+    :param plaintext: Whether to listen for HTTP rather than HTTPS traffic.
+    """
+
+    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
+        sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+        sock.bind((address, port))
+        sock.listen()
+        logging.info(f"Internal HTTPS server listening on {address}:{port}")
+
+        if plaintext:
+            # Notify the waiting thread that the internal server is ready.
+            with internal_thread_is_ready:
+                internal_thread_is_ready.notify()
+            conn, addr_in = sock.accept()
+            logging.info(f"Internal server accepted plaintext connection from {addr_in}")
+            with conn:
+                receive_and_send_http(conn)
+        else:
+            # Wrap the server socket to handle TLS.
+            context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
+            context.load_cert_chain(certfile=cert_file, keyfile=key_file)
+
+            with context.wrap_socket(sock, server_side=True) as ssock:
+                with internal_thread_is_ready:
+                    internal_thread_is_ready.notify()
+                conn, addr_in = ssock.accept()
+                logging.info(f"Internal server accepted TLS connection from {addr_in}")
+                with conn:
+                    receive_and_send_http(conn)
+
+
+def parse_pp_v1(pp_bytes: bytes) -> int:
+    """Parse and print the Proxy Protocol v1 string.
+
+    :param pp_bytes: The bytes containing the Proxy Protocol string. There may
+    be more bytes than the Proxy Protocol string.
+
+    :returns: The number of bytes occupied by the proxy v1 protcol.
+    """
+    # Proxy Protocol v1 string ends with CRLF.
+    end = pp_bytes.find(b'\r\n')
+    if end == -1:
+        raise ValueError("Proxy Protocol v1 string ending not found")
+    logging.info(pp_bytes[:end].decode("utf-8"))
+    return end + 2
+
+
+def parse_pp_v2(pp_bytes: bytes) -> int:
+    """Parse and print the Proxy Protocol v2 string.
+
+    :param pp_bytes: The bytes containing the Proxy Protocol string. There may
+    be more bytes than the Proxy Protocol string.
+
+    :returns: The number of bytes occupied by the proxy v2 protocol string.
+    """
+
+    # Skip the 12 byte header.
+    pp_bytes = pp_bytes[12:]
+    version_command = pp_bytes[0]
+    pp_bytes = pp_bytes[1:]
+    family_protocol = pp_bytes[0]
+    pp_bytes = pp_bytes[1:]
+    tuple_length = int.from_bytes(pp_bytes[:2], byteorder='big')
+    pp_bytes = pp_bytes[2:]
+
+    # Of version_command, the highest 4 bits is the version and the lowest is
+    # the command.
+    version = version_command >> 4
+    command = version_command & 0x0F
+
+    if version != 2:
+        raise ValueError(
+            f'Invalid version: {version} (by spec, should always be 0x02)')
+
+    if command == 0x0:
+        command_description = 'LOCAL'
+    elif command == 0x1:
+        command_description = 'PROXY'
+    else:
+        raise ValueError(
+            f'Invalid command: {command} (by spec, should be 0x00 or 0x01)')
+
+    # Of address_family, the highest 4 bits is the address family and the
+    # lowest is the transport protocol.
+    if family_protocol == 0x0:
+        transport_protocol_description = 'UNSPEC'
+    elif family_protocol == 0x11:
+        transport_protocol_description = 'TCP4'
+    elif family_protocol == 0x12:
+        transport_protocol_description = 'UDP4'
+    elif family_protocol == 0x21:
+        transport_protocol_description = 'TCP6'
+    elif family_protocol == 0x22:
+        transport_protocol_description = 'UDP6'
+    elif family_protocol == 0x31:
+        transport_protocol_description = 'UNIX_STREAM'
+    elif family_protocol == 0x32:
+        transport_protocol_description = 'UNIX_DGRAM'
+    else:
+        raise ValueError(
+            f'Invalid address family: {family_protocol} (by spec, should be '
+            '0x00, 0x11, 0x12, 0x21, 0x22, 0x31, or 0x32)')
+
+    if family_protocol in (0x11, 0x12):
+        if tuple_length != 12:
+            raise ValueError(
+                "Unexpected tuple length for TCP4/UDP4: "
+                f"{tuple_length} (by spec, should be 12)"
+            )
+        src_addr = socket.inet_ntop(socket.AF_INET, pp_bytes[:4])
+        pp_bytes = pp_bytes[4:]
+        dst_addr = socket.inet_ntop(socket.AF_INET, pp_bytes[:4])
+        pp_bytes = pp_bytes[4:]
+        src_port = int.from_bytes(pp_bytes[:2], byteorder='big')
+        pp_bytes = pp_bytes[2:]
+        dst_port = int.from_bytes(pp_bytes[:2], byteorder='big')
+        pp_bytes = pp_bytes[2:]
+
+    tuple_description = f'{src_addr} {dst_addr} {src_port} {dst_port}'
+    logging.info(
+        f'{command_description} {transport_protocol_description} '
+        f'{tuple_description}')
+
+    return 16 + tuple_length
+
+
+def accept_pp_connection(sock: socket.socket, address: str, internal_port: int) -> bool:
+    """Accept a connection and parse the proxy protocol header.
+
+    :param sock: The socket to accept the connection on.
+    :param address: The address of the internal server to connect to.
+    :param internal_port: The port of the internal server to connect to.
+
+    :returns: True if the connection had a payload, False otherwise.
+    """
+    client_conn, address_in = sock.accept()
+    logging.info(f'Accepted connection from {address_in}')
+    with client_conn:
+        has_pp = False
+        pp_length = 0
+        # Read the Proxy Protocol prefix, which ends with the first CRLF.
+        received_data = b''
+        while True:
+            data = client_conn.recv(1024)
+            if data:
+                logging.debug(f"Received: {len(data)} bytes")
+            else:
+                logging.info("No data received while waiting for "
+                             "Proxy Protocol prefix")
+                return False
+            received_data += data
+
+            if (received_data.startswith(b'PROXY') and
+                    b'\r\n' in received_data):
+                logging.info("Received Proxy Protocol v1")
+                pp_length = parse_pp_v1(received_data)
+                has_pp = True
+                break
+
+            if received_data.startswith(PP_V2_PREFIX):
+                logging.info("Received Proxy Protocol v2")
+                pp_length = parse_pp_v2(received_data)
+                has_pp = True
+                break
+
+            if len(received_data) > 108:
+                # The spec gaurantees that the prefix will be no more than
+                # 108 bytes.
+                logging.info("No Proxy Protocol string found.")
+                break
+        if has_pp:
+            # Now, strip the received_data of the prefix and blind tunnel
+            # the rest of the content.
+            for_internal = received_data[pp_length:]
+            logging.debug(
+                f"Stripped the prefix, now thare are {len(for_internal)} "
+                "bytes for the internal server.")
+        else:
+            for_internal = received_data
+        client_conn.settimeout(TIMEOUT)
+        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as internal_sock:
+            logging.debug(f"Connecting to internal server on {address}:{internal_port}")
+            internal_sock.connect((address, internal_port))
+            internal_sock.settimeout(TIMEOUT)
+            if for_internal:
+                logging.debug('Sending remaining data to internal server: '
+                              f'{len(for_internal)} bytes')
+                internal_sock.sendall(for_internal)
+            while True:
+
+                logging.debug("entering loop")
+
+                try:
+                    from_internal = internal_sock.recv(1024)
+                    logging.debug(f'Received {len(from_internal)} bytes from internal server')
+                    if not from_internal:
+                        logging.debug('No more data from internal server, closing connection')
+                        break
+                    client_conn.sendall(from_internal)
+                    logging.debug(f'Sent {len(from_internal)} bytes to client')
+                except socket.timeout:
+                    pass
+
+                try:
+                    for_internal = client_conn.recv(1024)
+                    logging.debug(f'Received {len(for_internal)} bytes from client')
+                    if not for_internal:
+                        logging.debug('No more data from client, closing connection')
+                        break
+                    internal_sock.sendall(for_internal)
+                    logging.debug(f'Sent {len(for_internal)} bytes to internal server')
+                except socket.timeout:
+                    pass
+
+
+def receive_pp_request(address: str, port: int, internal_port: int) -> None:
+    """Start a server to receive a connection which may have a proxy protocol
+    header.
+
+    :param address: The address to listen on.
+    :param port: The port to listen on.
+    :param internal_port: The port of the internal server to connect to.
+    """
+
+    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
+        sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+        sock.bind((address, port))
+        sock.listen()
+
+        # The PortOpen logic will create an empty request to the server. Ignore
+        # those until we have a connection with a real request which comes in.
+        request_received = False
+        while not request_received:
+            request_received = accept_pp_connection(sock, address,
+                                                    internal_port)
+
+
+def main() -> int:
+    """Run the server listening for Proxy Protocol."""
+    args = parse_args()
+
+    with internal_thread_is_ready:
+        """Start the threads to receive requests."""
+        internal_server = threading.Thread(
+            target=run_internal_server,
+            args=(args.certfile, args.keyfile, args.address,
+                  args.internal_port, args.plaintext))
+        internal_server.start()
+
+        # Wait for the internal server to start before proceeding.
+        internal_thread_is_ready.wait()
+
+    receive_pp_request(args.address, args.port, args.internal_port)
+    internal_server.join()
+
+    return 0
+
+
+if __name__ == "__main__":
+    logging.basicConfig(
+        level=logging.DEBUG,
+        format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
+    sys.exit(main())
diff --git a/tests/gold_tests/timeout/default_inactivity_timeout.test.py b/tests/gold_tests/timeout/default_inactivity_timeout.test.py
new file mode 100644
index 0000000..10ba964
--- /dev/null
+++ b/tests/gold_tests/timeout/default_inactivity_timeout.test.py
@@ -0,0 +1,109 @@
+'''
+Verify correct behavior for default_inactivity_timeout.
+'''
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+
+Test.Summary = __doc__
+
+
+class TestDefaultInactivityTimeout:
+    """Configure a test for default_inactivity_test."""
+
+    replay_file = "replay/default_inactivity_timeout.replay.yaml"
+    client_gold_file = 'gold/client_default_inactivity_timeout.gold'
+    client_counter: int = 0
+    ts_counter: int = 0
+    server_counter: int = 0
+
+    def __init__(self, name: str, use_override: bool):
+        """Initialize the test.
+
+        :param name: The name of the test.
+        :param use_override: Whether to use the override rather than the global
+        configuration.
+        """
+        self.name = name
+        self.use_override = use_override
+
+    def _configure_server(self, tr: 'TestRun') -> None:
+        """Configure the server.
+
+        :param tr: The TestRun object to associate the server process with.
+        """
+        server = tr.AddVerifierServerProcess(
+            f"server_{TestDefaultInactivityTimeout.server_counter}",
+            self.replay_file)
+        TestDefaultInactivityTimeout.server_counter += 1
+        self._server = server
+
+    def _configure_traffic_server(self, tr: 'TestRun') -> None:
+        """Configure Traffic Server.
+
+        :param tr: The TestRun object to associate the ts process with.
+        :return: A Traffic Server Test process.
+        """
+        ts = tr.MakeATSProcess(f"ts-{TestDefaultInactivityTimeout.ts_counter}")
+        TestDefaultInactivityTimeout.ts_counter += 1
+        self._ts = ts
+
+        debug_tags = 'http|cache|socket|net_queue|inactivity_cop|conf_remap'
+        ts.Disk.records_config.update({
+            'proxy.config.diags.debug.enabled': 1,
+            'proxy.config.diags.debug.tags': debug_tags,
+        })
+
+        origin_port = self._server.Variables.http_port
+        remap_line = f'map / http://127.0.0.1:{origin_port}'
+
+        if self.use_override:
+            remap_line += (
+                ' @plugin=conf_remap.so '
+                '@pparam=proxy.config.net.default_inactivity_timeout=2')
+        else:
+            ts.Disk.records_config.update({
+                'proxy.config.net.default_inactivity_timeout': 2,
+            })
+
+        ts.Disk.remap_config.AddLine(remap_line)
+        ts.Disk.traffic_out.Content += Testers.ContainsExpression(
+            'timed out due to default inactivity timeout',
+            'Verify that the default inactivity timeout was triggered.')
+
+    def run(self) -> None:
+        """Run the test."""
+        tr = Test.AddTestRun(self.name)
+        self._configure_server(tr)
+        self._configure_traffic_server(tr)
+
+        tr.Processes.Default.StartBefore(self._server)
+        tr.Processes.Default.StartBefore(self._ts)
+        tr.AddVerifierClientProcess(
+            f'client-{TestDefaultInactivityTimeout.client_counter}',
+            self.replay_file,
+            http_ports=[self._ts.Variables.port])
+        TestDefaultInactivityTimeout.client_counter += 1
+
+        # Set up expectectations for the timeout closing the connection.
+        tr.Processes.Default.ReturnCode = 1
+        tr.Processes.Default.Streams.all = self.client_gold_file
+
+
+test = TestDefaultInactivityTimeout("global config", use_override=False)
+test.run()
+
+test = TestDefaultInactivityTimeout("override config", use_override=True)
+test.run()
diff --git a/tests/gold_tests/timeout/gold/client_default_inactivity_timeout.gold b/tests/gold_tests/timeout/gold/client_default_inactivity_timeout.gold
new file mode 100644
index 0000000..6f195d5
--- /dev/null
+++ b/tests/gold_tests/timeout/gold/client_default_inactivity_timeout.gold
@@ -0,0 +1,4 @@
+```
+``PARSE_INCOMPLETE
+``Failed HTTP/1 transaction with key: timeout2
+``
diff --git a/tests/gold_tests/timeout/replay/default_inactivity_timeout.replay.yaml b/tests/gold_tests/timeout/replay/default_inactivity_timeout.replay.yaml
new file mode 100644
index 0000000..ce0c96d
--- /dev/null
+++ b/tests/gold_tests/timeout/replay/default_inactivity_timeout.replay.yaml
@@ -0,0 +1,73 @@
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+
+#
+# Perform a response with a long delay to verify default_inactivity_timeout.
+#
+
+meta:
+  version: "1.0"
+
+sessions:
+- transactions:
+
+  # Perform a transaction with no timeout.
+  - client-request:
+      method: "GET"
+      version: "1.1"
+      scheme: "http"
+      url: /path/item1
+      headers:
+        fields:
+        - [ Host, example.com ]
+        - [ uuid, timeout1 ]
+        - [ Content-Length, 0 ]
+
+    server-response:
+      status: 200
+      reason: "OK"
+      headers:
+        fields:
+        - [ Content-Length, 300 ]
+
+    proxy-response:
+      status: 200
+
+  # Perform a transaction with 4s delay. This may timeout if
+  # default_inactivity_timeout is below 4s.
+  - client-request:
+      method: "GET"
+      version: "1.1"
+      scheme: "http"
+      url: /path/item2
+      headers:
+        fields:
+        - [ Host, example.com ]
+        - [ uuid, timeout2 ]
+        - [ Content-Length, 0 ]
+
+    server-response:
+      # This is large, but bear in mind that the default inactivity timeout
+      # should kill the connection before this delay finishes. So this should
+      # not slow down the test as much as it seems.
+      delay: 8s
+
+      status: 200
+      reason: "OK"
+
+    proxy-response:
+      status: 200
+
diff --git a/tests/prepare_proxy_verifier.sh b/tests/prepare_proxy_verifier.sh
index 2f40bf0..2c34df7 100755
--- a/tests/prepare_proxy_verifier.sh
+++ b/tests/prepare_proxy_verifier.sh
@@ -40,7 +40,7 @@
 pv_tar_filename="${pv_dir}.tar.gz"
 pv_tar="${pv_top_dir}/${pv_tar_filename}"
 pv_tar_url="https://ci.trafficserver.apache.org/bintray/${pv_tar_filename}"
-expected_sha1="d939629949bafe6df8821e5d441762066cc6d556"
+expected_sha1="011bd50b74a07484683ed56a671f37afbdd0c786"
 pv_client="${bin_dir}/verifier-client"
 pv_server="${bin_dir}/verifier-server"
 TAR=${TAR:-tar}
@@ -76,7 +76,17 @@
     pv_os_dir=""
     case $(uname -s) in
     Darwin)
-        pv_os_dir="${pv_unpack_dir}/${pv_dir}/mac-os"
+        case $(uname -m) in
+        x86_64)
+            pv_os_dir="${pv_unpack_dir}/${pv_dir}/mac-x86_64"
+            ;;
+        arm64)
+            pv_os_dir="${pv_unpack_dir}/${pv_dir}/mac-m1"
+            ;;
+        *)
+            fail "Unrecognized Mac architecture: $(uname -m)"
+            ;;
+        esac
         ;;
     Linux)
         pv_os_dir="${pv_unpack_dir}/${pv_dir}/linux"
diff --git a/tests/proxy-verifier-version.txt b/tests/proxy-verifier-version.txt
index f6dcb64..8a965c1 100644
--- a/tests/proxy-verifier-version.txt
+++ b/tests/proxy-verifier-version.txt
@@ -1 +1 @@
-v2.5.2
+v2.6.0