You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficserver.apache.org by am...@apache.org on 2018/02/16 21:02:41 UTC

[trafficserver] branch master updated: Add TLS Bridge plugin to plugins/experimental.

This is an automated email from the ASF dual-hosted git repository.

amc pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficserver.git


The following commit(s) were added to refs/heads/master by this push:
     new e3c64e1  Add TLS Bridge plugin to plugins/experimental.
e3c64e1 is described below

commit e3c64e1f3646d52df2cba4795ffc7294a735f3d8
Author: Alan M.Carroll <so...@oath.com>
AuthorDate: Fri Nov 10 15:16:30 2017 +0000

    Add TLS Bridge plugin to plugins/experimental.
    
    (cherry picked from Oath internal commit 0edc61f7637a1680d3d40e69491431ff4571cd95)
---
 .../plugins/example-plugins/index.en.rst           |   1 +
 .../plugins/example-plugins/tls_bridge.en.rst      | 245 ++++++++
 doc/ext/local-config.py.in                         |   2 +-
 doc/uml/TLS-Bridge-Messages.uml                    |  80 +++
 doc/uml/TLS-Bridge-Plugin.uml                      |  81 +++
 plugins/Makefile.am                                |   1 +
 plugins/experimental/tls_bridge/Makefile.inc       |  23 +
 plugins/experimental/tls_bridge/README             |  20 +
 plugins/experimental/tls_bridge/regex.cc           | 126 +++++
 plugins/experimental/tls_bridge/regex.h            |  63 +++
 plugins/experimental/tls_bridge/tls_bridge.cc      | 630 +++++++++++++++++++++
 11 files changed, 1271 insertions(+), 1 deletion(-)

diff --git a/doc/developer-guide/plugins/example-plugins/index.en.rst b/doc/developer-guide/plugins/example-plugins/index.en.rst
index 5141eee..7da4a35 100644
--- a/doc/developer-guide/plugins/example-plugins/index.en.rst
+++ b/doc/developer-guide/plugins/example-plugins/index.en.rst
@@ -28,6 +28,7 @@ Example Plugins
    basic-authorization/index.en
    blacklist/index.en
    query_remap/index.en
+   tls_bridge.en
 
 .. _developer-plugins-header-based-examples:
 
diff --git a/doc/developer-guide/plugins/example-plugins/tls_bridge.en.rst b/doc/developer-guide/plugins/example-plugins/tls_bridge.en.rst
new file mode 100644
index 0000000..31cce7d
--- /dev/null
+++ b/doc/developer-guide/plugins/example-plugins/tls_bridge.en.rst
@@ -0,0 +1,245 @@
+.. 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:: ../../../common.defs
+
+.. highlight:: cpp
+.. default-domain:: cpp
+.. |Name| replace:: TLS Bridge
+
+|Name|
+**********
+
+This plugin is used to provide secured TLS tunnels for connections between a Client and a Service
+via two gateway |TS| instances. By configuring the |TS| instances the level of security in the
+tunnel can be easily controlled for all communications across the tunnels.
+
+Description
+===========
+
+The tunnel is sustained by two instances of |TS|.
+
+.. uml::
+   :align: center
+
+   hide empty members
+
+   cloud "Cloud\nUntrusted\nNetworks" as Cloud
+   node "Ingress ATS"
+   node "Peer ATS"
+
+   [Client] <--> [Ingress ATS] : Unsecure
+   [Ingress ATS] <-> [Cloud] : Secure
+   [Cloud] <-> [Peer ATS] : Secure
+   [Peer ATS] <-u-> [Service] : Unsecure
+
+   [Ingress ATS] ..> [tls_bridge\nPlugin] : Uses
+
+
+The ingress |TS| accepts a connection from the Client. This connection gets intercepted by the
+|Name| plugin inside |TS|. The plugin then makes a TLS connection to the peer |TS| using the
+configured level of security. The original request from the Client to the ingress |TS| is then sent
+to the peer |TS| to create a connection from the peer |TS| to the Service. After this the
+Client has a virtual circut to the Service and can use any TCP based communication (including TLS).
+Effectively the plugin causes the connectivity to work as if the Client had done the ``CONNECT``
+directly to the peer |TS|. Note this means the DNS lookup for the Service is done by the peer |TS|,
+not the ingress |TS|.
+
+The plugin is configured with a mapping of Service names to peer |TS| instances. The Service
+names are URLs which will in the original HTTP request made by the Client after connecting to the
+ingress |TS|. This means the FQDN for the Service is not resolved in the environment of the peer
+|TS| and not the ingress |TS|.
+
+Configuration
+=============
+
+|Name| requires at least two instances of |TS| (Ingress and Peer).
+
+#. Disable caching on |TS| in ``records.config``::
+
+      CONFIG proxy.config.http.cache.http INT 0
+
+#. Configure the ports.
+
+   *  The Peer |TS| must be listening on an SSL enabled proxy port. For instance, if the proxy port for the Peer is 4443, then configuration in ``records.config`` would have::
+
+         CONFIG proxy.config.http.server_ports STRING 4443:ssl
+
+   *  The Ingress |TS| must allow ``CONNECT`` to the Peer proxy port. This would be set in ``records.config`` by::
+
+         CONFIG proxy.config.http.connect_ports STRING 4443
+
+      The Ingress |TS| also needs ``proxy.config.http.server_ports`` configured to have proxy ports
+      to which the Client can connect.
+
+#. Remap is not required, however, |TS| requires remap in order to accept the request. This can be done by disabling the remap requirement::
+
+      CONFIG proxy.config.url_remap.remap_required INT 0
+
+   In this case |TS| will act as an open proxy which is unlikely to be a good idea. |TS| will need
+   to run in a restricted environment or use access control (via ``ip_allow.config`` or
+   ``iptables``).
+
+#. Configure the Ingress |TS| to verify the Peer server certificate::
+
+      CONFIG proxy.config.ssl.client.verify.server INT 1
+
+#. Configure Certificate Authority used by the Ingress |TS| to verify the Peer server certificate. If this
+   is a directory all of the certificates in the directory are treated as Certificate Authorites. ::
+
+      CONFIG proxy.config.ssl.client.CA.cert.filename STRING </path/to/CA_certificate_file_name>
+
+#. Configure the Ingress |TS| to provide a client certificate::
+
+      CONFIG proxy.config.ssl.client.cert.path STRING </path/to/certificate/dir>
+      CONFIG proxy.config.ssl.client.cert.filename STRING <server_certificate_file_name>
+
+#. Configure the Peer |TS| to verify the Ingress client certificate::
+
+      CONFIG proxy.config.ssl.client.certification_level INT 2
+
+#. Enable the |Name| plugin in ``plugin.config``. The plugin is configured by arguments in
+   ``plugin.config``. These are arguments are in pairs of a *destination* and a *peer*. The
+   destination is a anchored regular expression which is matched against the host name in the Client
+   ``CONNECT``. The destinations are checked in order and the first match is used to select the Peer
+   |TS|. The peer should be an FQDN or IP address with an optional port. For the example above, if
+   the Peer |TS| was named "peer.example.com" on port 4443 and the Service at ``*.service.com``, the
+   peer argument would be "peer.example.com:4443". In ``plugin.config`` this would be::
+
+      tls_bridge.so .*[.]service[.]com peer.example.com:4443
+
+Notes
+=====
+
+|Name| is distinct from more basic Layer 4 Routing available in |TS|. For the latter there is no
+intercept or change of the TLS exchange between the Client and the Service. The exchange looks like
+this
+
+.. uml::
+   :align: center
+
+   actor Client
+   participant "Ingress TS" as Ingress
+   participant Service
+
+   Client <-[#green]> Ingress : //TCP Connect//
+   Client -[#blue]-> Ingress : <font color="blue">TLS: ""CLIENT HELLO""</font>
+   note over Ingress : Map SNI to upstream Service
+   Ingress <-[#green]> Service : //TCP Connect//
+   Ingress -[#blue]-> Service : <font color="blue">TLS: ""CLIENT HELLO""</font>
+   note right : Duplicate of data from Client.
+   note over Ingress : Forward bytes between Client <&arrow-thick-left> <&arrow-thick-right> Service
+   Client <--> Service
+
+The key points are
+
+*  |TS| does no TLS negotiation at all. The properties of the connection between the Ingress |TS|
+   and the Service are completely determined by the Client and Server negotation.
+*  No packets are modified, the ""CLIENT HELLO"" sent by the Ingress |TS| is an exact copy of that
+   sent to the Ingress |TS| by the Client. It is only examined for the SNI data in order to select
+   the Service.
+
+Implementation
+==============
+
+The |Name| plugin uses :code:`TSHttpTxnIntercept` to gain control of the ingress Client session.
+If the session is valid then a separate connection to the peer |TS| is created using
+:code:`TSHttpConnect`.
+
+After the ingress |TS| connects to the peer |TS| it sends a duplicate of the Client ``CONNECT``
+request. This is processed by the peer |TS| to connect on to the Service. After this both |TS|
+instances then tunnel data between the Client and the Service, in effect becoming a transparent
+tunnel.
+
+The overall exchange looks like the following:
+
+.. uml::
+   :align: center
+
+   @startuml
+
+   box "Client Network" #DDFFDD
+   actor Client
+   entity "User Agent\nVConn" as lvc
+   participant "Ingress ATS" as ingress
+   entity "Upstream\nVConn" as rvc
+   end box
+   box "Corporate Network" #DDDDFF
+   participant "Peer ATS" as peer
+   database Service
+   end box
+
+   Client -> ingress : TCP or TLS connect
+   activate lvc
+   Client -> ingress : HTTP CONNECT
+   ingress -> lvc : Intercept Transaction
+   ingress -> peer : TLS connect
+   activate rvc
+   note over ingress,peer : Secure Tunnel
+   ingress -> peer : HTTP CONNECT
+   note over peer : DNS for Service is\ndone here.
+   peer -> Service : TCP Connect
+
+   note over Client, Service : At this point data can flow between the Client and Server\nover the secure link as a virtual connection, including any TLS handshake.
+   Client <--> Service
+   lvc <-> ingress : <&arrow-thick-left> Move data <&arrow-thick-right>
+   ingress <-> rvc : <&arrow-thick-left> Move data <&arrow-thick-right>
+   note over ingress : Plugin explicitlys moves this data.
+
+   @enduml
+
+
+A detailed view of the plugin operation.
+
+.. image:: ../../../uml/images/TLS-Bridge-Plugin.svg
+   :align: center
+
+A sequence diagram focusing on the request / response data flow. There is a :code:`NetVConn` for the
+connection to the Peer |TS| which is omitted for clarity.
+
+*  Blue dotted lines are request or response data
+*  Green lines are network connections.
+*  Red lines are programmatic interactions.
+*  Black lines are hook call backs.
+
+The :code:`200 OK` sent from the Peer |TS| is parsed and consumed by the plugin. An non-:code:`200` response
+means there was an error and the tunnel is shut down. To deal with the Client response clean up the
+response code is stored and used later during cleanup.
+
+.. image:: ../../../uml/images/TLS-Bridge-Messages.svg
+   :align: center
+
+A restartable state machine is used to recognize the end of the Peer |TS| response. The initial part
+of the response is easy because all that is needed is to wait until there is sufficient data for a
+minimal parse. The end can be an arbitrary distance in to the stream and may not all be in the same
+socket read.
+
+.. uml::
+   :align: center
+
+   @startuml
+   [*] -r> State_0
+   State_0 --> State_1 : CR
+   State_1 --> State_0 : *
+   State_1 --> State_1 : CR
+   State_1 --> State_2 : LF
+   State_2 --> State_3 : CR
+   State_2 --> State_0 : *
+   State_3 -r> [*] : LF
+   State_3 --> State_1 : CR
+   State_3 --> State_0 : *
+   @enduml
diff --git a/doc/ext/local-config.py.in b/doc/ext/local-config.py.in
index 5851686..cefb5df 100644
--- a/doc/ext/local-config.py.in
+++ b/doc/ext/local-config.py.in
@@ -14,5 +14,5 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-plantuml='@JAVA@ -jar ${PLANTUML_JAR}'
+plantuml='@JAVA@ -jar {}'.format(os.environ['PLANTUML_JAR'])
 plantuml_output_format='svg'
diff --git a/doc/uml/TLS-Bridge-Messages.uml b/doc/uml/TLS-Bridge-Messages.uml
new file mode 100644
index 0000000..dc0ca20
--- /dev/null
+++ b/doc/uml/TLS-Bridge-Messages.uml
@@ -0,0 +1,80 @@
+' 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.
+
+@startuml
+
+scale max 720 width
+
+actor Client
+box "Ingress ATS" #DDFFDD
+entity "Client\nNetVConn" as uanet
+participant "Ingress\nATS" as ingress
+entity "Client\nVConn" as uavc
+entity "TLS Bridge" as plugin
+entity "Peer\nVConn" as peervc
+end box
+box "Peer ATS" #DDDDFF
+participant "Peer\nATS" as peer
+end box
+participant Service
+
+Client <-[#green]> ingress : <font color="green">//TCP//</font> Handshake
+activate uanet
+Client -[#blue]-> uanet : <font color="blue">""CONNECT"" Service</font>
+uanet -> ingress : //Parse request//
+ingress -[#black]> plugin : ""READ_REQUEST_HDR_HOOK""
+plugin -> ingress : //TSHttpTxnIntercept()//
+ingress -> uavc : //Create//
+activate uavc
+uanet -[#blue]-> ingress : <font color="blue">""CONNECT"" Service</font>
+ingress -[#blue]-> uavc : <font color="blue">""CONNECT"" Service</font>
+ingress -[#black]> plugin : ""TS_EVENT_NET_ACCEPT""
+note right : Client VConn is passed in event data.
+
+plugin -\ ingress : //TSHttpConnect()//
+ingress -> peervc : //create//
+activate peervc
+ingress -/ plugin : //return Peer VConn//
+
+plugin -[#blue]-> peervc : <font color="blue">""CONNECT"" Peer</font>
+peervc -> ingress : //parse request//
+ingress <-[#green]> peer : <font color="green">//TCP//</font> Handshake
+ingress <-[#green]> peer : <font color="green">//TLS//</font> Handshake
+ingress -[#blue]-> peervc : <font color="blue">""200 OK""</font>
+peervc -[#blue]-> plugin : <font color="blue">""200 OK""</font>
+note left
+This signals a raw TLS connection
+nto the Peer ATS. The response is
+parsed and consumed by Plugin.
+end note
+
+note over plugin : Plugin switches to byte forwarding.
+uavc -[#blue]-> plugin : <font color="blue">""CONNECT"" Service</font>
+note left: Original Client Request.
+plugin -[#blue]-> peervc : <font color="blue">""CONNECT"" Service</font>
+peervc -[#blue]-> peer : <font color="blue">""CONNECT"" Service</font>
+peer <-[#green]> Service : <font color="green">//TCP//</font> Handshake
+peer <-[#green]> Service : <font color="green">//TLS//</font> Handshake
+note left : Optional, based on 'Service'
+peer -[#blue]-> peervc : <font color="blue">""200 OK""</font>
+peervc -[#blue]-> plugin : <font color="blue">""200 OK""</font>
+plugin -[#blue]-> uavc : <font color="blue">""200 OK""</font>
+uavc -[#blue]-> ingress : <font color="blue">""200 OK""</font>
+note left
+ATS updates the incoming response based on
+local configuration. This means what goes out
+to the Client may be different than what the
+plugin wrote (forwarded from Peer ATS).
+end note
+ingress -[#black]> plugin : ""SEND_RESPONSE_HDR_HOOK""
+note right : Plugin cleans up response here.
+ingress -[#blue]-> uanet : <font color="blue">""200 OK""</font>
+uanet -[#blue]-> Client : <font color="blue">""200 OK""</font>
+
+Client <-[#green]> Service : //TCP/TLS Connect//
+
+@enduml
diff --git a/doc/uml/TLS-Bridge-Plugin.uml b/doc/uml/TLS-Bridge-Plugin.uml
new file mode 100644
index 0000000..ac617ce
--- /dev/null
+++ b/doc/uml/TLS-Bridge-Plugin.uml
@@ -0,0 +1,81 @@
+' 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.
+
+@startuml
+
+scale max 720 width
+
+ReadRequestHdr : Check for ""CONNECT""
+ReadRequestHdr : =====
+ReadRequestHdr : Find Peer for Service.
+
+Intercept : Intercept Client Transaction.
+Intercept : =====
+Intercept : Initialize Bridge Context.
+
+Accept : Initialize ""VConn"" data.
+Accept : =====
+Accept : Create internal transaction.
+Accept : =====
+Accept : Set up Client side tunnel.
+Accept : =====
+Accept : ""CONNECT"" to Peer via internal transaction.
+
+Tunnel : Move data.
+
+state "Flow To Peer" as FlowToPeer
+FlowToPeer : Move data from Client ""TSIOBufferReader""\nto Peer ""TSIOBuffer"".
+FlowToPeer : =====
+FlowToPeer : Reenable VIOs
+
+state "Flow To Client" as FlowToClient
+FlowToClient : Move data from Peer ""TSIOBufferReader""\nto Client ""TSIOBuffer"".
+FlowToClient : =====
+FlowToClient : Reenable VIOs
+
+state "Wait For Peer Response" as WaitForPeerResponse {
+  WaitForStatusCode : Parse for status code.
+
+  WaitForResponseEnd : Parse for double newline.
+
+  BadStatus : Set error data\nin Client Response.
+
+  PeerReady : Update Client Response.
+  PeerReady : =====
+  PeerReady : Set up peer tunnel.
+  PeerReady : =====
+  PeerReady : Start Tunneling.
+
+  [*] --> WaitForStatusCode
+  WaitForStatusCode --> WaitForResponseEnd
+  WaitForStatusCode --> BadStatus
+  BadStatus --> [*]
+  WaitForResponseEnd --> PeerReady
+  PeerReady --> [*]
+}
+
+[*] --> ReadRequestHdr : ""CONNECT"" Service
+ReadRequestHdr --> [*] : Not matched.
+ReadRequestHdr --> Intercept
+Intercept --> Accept : ""TS_EVENT_NET_ACCEPT""
+Accept -r-> WaitForPeerResponse
+WaitForPeerResponse --> WaitForPeerResponse : ""TS_EVENT_VCONN_READ_READY""
+WaitForPeerResponse --> Tunnel : 200 OK
+WaitForPeerResponse -u-> [*] : Peer connect failure
+
+Tunnel --> FlowToClient : ""TS_EVENT_VCONN_READ_READY""\nPeer VIO
+FlowToClient --> Tunnel
+Tunnel --> FlowToPeer : ""TS_EVENT_VCONN_READY_READY""\nClient VIO
+FlowToPeer --> Tunnel
+
+Tunnel -right-> Shutdown : ""TS_EVENT_VCONN_EOS""
+
+Shutdown : Close Client VConn
+Shutdown : =====
+Shutdown : Close Upstream VConn
+
+@enduml
diff --git a/plugins/Makefile.am b/plugins/Makefile.am
index 9b9aefc..20337dc 100644
--- a/plugins/Makefile.am
+++ b/plugins/Makefile.am
@@ -74,6 +74,7 @@ include experimental/sslheaders/Makefile.inc
 include experimental/stale_while_revalidate/Makefile.inc
 include experimental/stream_editor/Makefile.inc
 include experimental/system_stats/Makefile.inc
+include experimental/tls_bridge/Makefile.inc
 include experimental/ts_lua/Makefile.inc
 include experimental/url_sig/Makefile.inc
 
diff --git a/plugins/experimental/tls_bridge/Makefile.inc b/plugins/experimental/tls_bridge/Makefile.inc
new file mode 100644
index 0000000..e791028
--- /dev/null
+++ b/plugins/experimental/tls_bridge/Makefile.inc
@@ -0,0 +1,23 @@
+#  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.
+
+pkglib_LTLIBRARIES += experimental/tls_bridge/tls_bridge.la
+
+experimental_tls_bridge_tls_bridge_la_SOURCES = \
+  experimental/tls_bridge/tls_bridge.cc \
+  experimental/tls_bridge/regex.h \
+  experimental/tls_bridge/regex.cc
+
diff --git a/plugins/experimental/tls_bridge/README b/plugins/experimental/tls_bridge/README
new file mode 100644
index 0000000..51afa46
--- /dev/null
+++ b/plugins/experimental/tls_bridge/README
@@ -0,0 +1,20 @@
+ATS (Apache Traffic Server) TLS Bridge Plugin
+
+This plugin enables a TLS bridge from an "Ingress" ATS instance to a "Peer" ATS instance using
+the `CONNECT` method. A Client sends a CONNECT to the Ingress, which sets up a TLS connection
+to the Peer. The Peer then processes the original Client CONNECT. The behavior is as if the Client
+sent the CONNECT to the Peer.
+
+Configuration is done in 'plugin.config' by passing pairs of arguments. Each pair is a service and a peer.
+The service is an anchored regular expression which is checked against the host name in the Client
+CONNECT. The peer is an FQDN or IP address with an optional port. If matched, the TLS bridge is created
+to the peer using the specified FQDN and port.
+
+Example:
+
+   tls_bridge.so .*[.]service[.]com peer.example.com:4443
+
+This will create a bridge to 'peer.example.com' on port 4443 for CONNECT requests that end in '.service.com'.
+
+Version 7.0.0 [11/10/17, solidwallofcode]
+  - [YTSATS-1633] Initial version.
diff --git a/plugins/experimental/tls_bridge/regex.cc b/plugins/experimental/tls_bridge/regex.cc
new file mode 100644
index 0000000..3e47c40
--- /dev/null
+++ b/plugins/experimental/tls_bridge/regex.cc
@@ -0,0 +1,126 @@
+/** @file
+
+  PCRE support wrapper.
+
+  @section license License
+
+  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 "regex.h"
+
+#ifdef PCRE_CONFIG_JIT
+#include <pthread.h>
+
+struct RegexThreadKey {
+  RegexThreadKey() { pthread_key_create(&this->key, (void (*)(void *)) & pcre_jit_stack_free); }
+  pthread_key_t key;
+};
+
+static RegexThreadKey k;
+
+static pcre_jit_stack *
+get_jit_stack(void *)
+{
+  pcre_jit_stack *jit_stack;
+
+  if ((jit_stack = (pcre_jit_stack *)pthread_getspecific(k.key)) == nullptr) {
+    jit_stack = pcre_jit_stack_alloc(8192, 1024 * 1024); // 1 page min and 1MB max
+    pthread_setspecific(k.key, (void *)jit_stack);
+  }
+
+  return jit_stack;
+}
+#endif
+
+bool
+Regex::compile(const char *pattern, const unsigned flags)
+{
+  const char *error;
+  int erroffset;
+  int options    = 0;
+  int study_opts = 0;
+
+  if (regex)
+    return false;
+
+  if (flags & CASE_INSENSITIVE) {
+    options |= PCRE_CASELESS;
+  }
+
+  if (flags & ANCHORED) {
+    options |= PCRE_ANCHORED;
+  }
+
+  regex = pcre_compile(pattern, options, &error, &erroffset, nullptr);
+  if (error) {
+    regex = nullptr;
+    return false;
+  }
+
+#ifdef PCRE_CONFIG_JIT
+  study_opts |= PCRE_STUDY_JIT_COMPILE;
+#endif
+
+  regex_extra = pcre_study(regex, study_opts, &error);
+
+#ifdef PCRE_CONFIG_JIT
+  if (regex_extra)
+    pcre_assign_jit_stack(regex_extra, &get_jit_stack, nullptr);
+#endif
+
+  return true;
+}
+
+int
+Regex::get_capture_count()
+{
+  int captures = -1;
+  if (pcre_fullinfo(regex, regex_extra, PCRE_INFO_CAPTURECOUNT, &captures) != 0) {
+    return -1;
+  }
+
+  return captures;
+}
+
+bool
+Regex::exec(ts::string_view src) const
+{
+  int ovector[30];
+  return exec(src, ovector, 30);
+}
+
+bool
+Regex::exec(ts::string_view src, int *ovector, int ovecsize) const
+{
+  int rv;
+
+  rv = pcre_exec(regex, regex_extra, src.data(), src.size(), 0, 0, ovector, ovecsize);
+  return rv > 0 ? true : false;
+}
+
+Regex::~Regex()
+{
+  if (regex_extra)
+#ifdef PCRE_CONFIG_JIT
+    pcre_free_study(regex_extra);
+#else
+    pcre_free(regex_extra);
+#endif
+  if (regex)
+    pcre_free(regex);
+}
diff --git a/plugins/experimental/tls_bridge/regex.h b/plugins/experimental/tls_bridge/regex.h
new file mode 100644
index 0000000..6c73c26
--- /dev/null
+++ b/plugins/experimental/tls_bridge/regex.h
@@ -0,0 +1,63 @@
+/** @file
+
+  Wrapper to make PCRE handling easier.
+
+  @section license License
+
+  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.
+ */
+
+#ifndef __TS_REGEX_H__
+#define __TS_REGEX_H__
+
+#include <pcre.h>
+#include <algorithm>
+#include <ts/string_view.h>
+
+class Regex
+{
+  using self_type = Regex;
+
+public:
+  enum Flag {
+    CASE_INSENSITIVE = 0x0001, // default is case sensitive
+    UNANCHORED       = 0x0002, // default (for DFA) is to anchor at the first matching position
+    ANCHORED         = 0x0004, // default (for Regex) is unanchored
+  };
+
+  Regex() = default;
+  Regex(self_type &&that);
+
+  bool compile(const char *pattern, const unsigned flags = 0);
+  // It is safe to call exec() concurrently on the same object instance
+  bool exec(ts::string_view src) const;
+  bool exec(ts::string_view src, int *ovector, int ovecsize) const;
+  int get_capture_count();
+  ~Regex();
+
+private:
+  pcre *regex             = nullptr;
+  pcre_extra *regex_extra = nullptr;
+};
+
+inline Regex::Regex(self_type &&that)
+{
+  std::swap(regex, that.regex);
+  std::swap(regex_extra, that.regex_extra);
+}
+
+#endif /* __TS_REGEX_H__ */
diff --git a/plugins/experimental/tls_bridge/tls_bridge.cc b/plugins/experimental/tls_bridge/tls_bridge.cc
new file mode 100644
index 0000000..0dc9905
--- /dev/null
+++ b/plugins/experimental/tls_bridge/tls_bridge.cc
@@ -0,0 +1,630 @@
+/*
+  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 "ts/ts.h"
+#include <atomic>
+#include <vector>
+#include <inttypes.h>
+#include "ts/TextView.h"
+#include "regex.h"
+
+#define PLUGIN_NAME "TLS Bridge"
+#define PLUGIN_TAG "tls-bridge"
+
+using ts::TextView;
+
+// Base format string for making the internal CONNECT.
+char const CONNECT_FORMAT[] = "CONNECT https:%.*s HTTP/1.1\r\n\r\n";
+
+const TextView METHOD_CONNECT{TS_HTTP_METHOD_CONNECT, TS_HTTP_LEN_CONNECT};
+
+/* ------------------------------------------------------------------------------------ */
+// Utility functions
+
+/** Remove a field from a header.
+    @a field is the name of the field, which is removed from the header specified by @a mbuf and @a hdr_loc.
+*/
+void
+Hdr_Remove_Field(TSMBuffer mbuf, TSMLoc hdr_loc, TextView field)
+{
+  TSMLoc field_loc;
+  if (TS_NULL_MLOC != (field_loc = TSMimeHdrFieldFind(mbuf, hdr_loc, field.data(), field.size()))) {
+    TSMimeHdrFieldDestroy(mbuf, hdr_loc, field_loc);
+    TSHandleMLocRelease(mbuf, hdr_loc, field_loc);
+  }
+}
+
+/* ------------------------------------------------------------------------------------ */
+/** Configuration data.
+    This is a mapping of regular expressions to peer destinations. For an inbound CONNECT the destination
+    is matched against the regular expressions. If matched the associated peer is used, otherwise the
+    transaction is not intercepted.
+ */
+class BridgeConfig
+{
+  using self_type = BridgeConfig;
+
+  /// Configuration item, regex -> destination.
+  struct Item {
+    using self_type = BridgeConfig;
+
+    /// Construct an item.
+    Item(const char *pattern, Regex &&r, const char *dest) : _pattern(pattern), _r(std::move(r)), _dest(dest) {}
+
+    std::string _pattern; ///< Original configuration regular expression.
+    Regex _r;             ///< Compiled regex.
+    std::string _dest;    ///< Destination if matched.
+  };
+
+public:
+  /// Load the configuration from the command line args.
+  void load_config(int argc, const char *argv[]);
+  /// Get the number of configured matches.
+  int count() const;
+  /// Find a match for @a name.
+  /// @return The destination or an empty view if no match.
+  TextView match(TextView name);
+
+private:
+  /// Configuration item storage.
+  std::vector<Item> _items;
+};
+
+inline int
+BridgeConfig::count() const
+{
+  return _items.size();
+}
+
+void
+BridgeConfig::load_config(int argc, const char *argv[])
+{
+  for (int i = 0; i < argc; i += 2) {
+    Regex r;
+    if (i + 1 >= argc) {
+      TSError("%s: Destination regular expression without peer", PLUGIN_TAG);
+    } else {
+      if (r.compile(argv[i]), Regex::ANCHORED) {
+        _items.emplace_back(argv[i], std::move(r), argv[i + 1]);
+      } else {
+        TSError("%s: Failed to compile regular expression '%s'", PLUGIN_TAG, argv[i]);
+      }
+    }
+  }
+}
+
+TextView
+BridgeConfig::match(TextView name)
+{
+  for (auto &item : _items) {
+    if (item._r.exec(name)) {
+      return {item._dest};
+    }
+  }
+  return {};
+}
+
+/// Global instance of the configuration.
+BridgeConfig Config;
+
+/* ------------------------------------------------------------------------------------ */
+/// Operational Context object.
+/// This holds all the data and methods for driving a TLS bridge.
+struct Bridge {
+  /// An I/O operation wrapper.
+  struct Op {
+    TSVIO _vio               = nullptr; ///< VIO for operation.
+    TSIOBuffer _buff         = nullptr; ///< Buffer for operation.
+    TSIOBufferReader _reader = nullptr; ///< Reader for operation.
+
+    /// Initialize - set up buffer and reader.
+    void init();
+    /// Clean up.
+    void close();
+  };
+
+  /// Per VConn data.
+  struct VCData {
+    TSVConn _vc = nullptr; ///< The virtual connection.
+    Op _write;             ///< Write operational data.
+    Op _read;              ///< Read operational data.
+
+    /// Initialize - assign the VC and set up the IOBuffers and readers.
+    void init(TSVConn vc);
+    /// Start a read operation of size @a n.
+    void do_read(TSCont cont, int64_t n);
+    /// Start a write operation of size @a n.
+    void do_write(TSCont cont, int64_t n);
+
+    /// Get a view of the available data in the first block.
+    /// This does @b not consume the data, it is a peek.
+    TextView first_block_data();
+    /// Get amount of available data for the read operation, if any.
+    int64_t available_size();
+    /// Consume @a n bytes of data.
+    void consume(int64_t n);
+    /// Close out the connection.
+    void do_close();
+  };
+
+  TSCont _self_cont; ///< The continuation that handles events for @a this.
+  TSHttpTxn _ua_txn; ///< User Agent transaction.
+  TextView _peer;    ///< ATS peer for upstream connection.
+  VCData _ua;        ///< User agent connection.
+  VCData _out;       ///< Outbound connection.
+
+  sockaddr const *_ua_addr; ///< User Agent address, needed for outbound connect.
+
+  /// Parsing state for the response of the internal connect.
+  enum OutboundState {
+    PRE,    ///< Not ready to try it yet.
+    OPEN,   ///< Initial internal CONNECT sent.
+    OK,     ///< Received '200' local response.
+    READY,  ///< Received local response terminal.
+    STREAM, ///< In byte streaming mode.
+    EOS,    ///< Streaming is done.
+    ERROR,  ///< Upstream connection failure.
+  } _out_resp_state = PRE;
+  /// Track depth into outbound response terminal.
+  int _out_terminal_pos = 0;
+  /// Response code from upstream CONNECT
+  TSHttpStatus _out_response_code = TS_HTTP_STATUS_NONE;
+  /// Response reason, if not TS_HTTP_STATUS_OK
+  std::string _out_response_reason;
+  /// Is the response to the user agent suspended?
+  bool _ua_response_suspended = false;
+
+  /// Bridge requires a continuation for scheduling and the transaction.
+  Bridge(TSCont cont, TSHttpTxn txn, TextView peer);
+
+  /// Called when the intercept (user agent) connection is set up.
+  void net_accept(TSVConn ua_vc);
+  /// Called when data is ready.
+  void read_ready(TSVIO vio);
+  /// Outbound reader, waiting for response code.
+  /// @return @c true if response code found and moved to next state.
+  bool check_outbound_OK();
+  /// Outbound reader, waiting for response termination.
+  /// @return @c true if terminal found and moved to next state.
+  bool check_outbound_terminal();
+  /// Outbound bulk reader.
+  void outbound_reader(TSVIO vio);
+  /// Handle EOS.
+  void eos(TSVIO vio);
+  /// Interfere with sending response to the user agent.
+  void send_response_cb();
+  /// Adjust the UA response to correspond to the actual upstream result.
+  void update_ua_response();
+
+  /// Move data from the outbound READ to the UA WRITE.
+  void flow_to_ua();
+  /// Move data from the UA READ to the outbound WRITE.
+  void flow_to_outbound();
+};
+
+/// Used to generate IDs for the plugin connections.
+std::atomic<int64_t> ConnectionCounter{0};
+
+Bridge::Bridge(TSCont cont, TSHttpTxn txn, TextView peer) : _self_cont(cont), _ua_txn(txn), _peer(peer)
+{
+  _ua_addr = TSHttpTxnClientAddrGet(_ua_txn);
+}
+
+void
+Bridge::net_accept(TSVConn vc)
+{
+  char buff[1024];
+  int64_t n = snprintf(buff, sizeof(buff) - 1, CONNECT_FORMAT, static_cast<int>(_peer.size()), _peer.data());
+
+  TSDebug(PLUGIN_TAG, "Received UA VConn");
+  // UA side intercepted.
+  _ua.init(vc);
+  _ua.do_read(_self_cont, INT64_MAX);
+  _ua.do_write(_self_cont, INT64_MAX);
+  // Start up the outbound connect.
+  _out.init(TSHttpConnectWithPluginId(_ua_addr, PLUGIN_TAG, ConnectionCounter++));
+  _out_resp_state = OPEN;
+  TSIOBufferWrite(_out._write._buff, buff, n);
+  _out.do_write(_self_cont, n);
+  TSVIOReenable(_out._write._vio);
+
+  // Need to verify and strip off the outbound TS response to the internal connect.
+  _out.do_read(_self_cont, INT64_MAX);
+}
+
+void
+Bridge::read_ready(TSVIO vio)
+{
+  using ts::TextView;
+
+  TSDebug(PLUGIN_TAG, "READ READY");
+  if (vio == _out._read._vio) {
+    switch (_out_resp_state) {
+    case PRE:
+      break; // this should never happen.
+    case ERROR:
+      break;
+    case EOS:
+      break;
+    case OPEN:
+      if (!this->check_outbound_OK() || _out_resp_state != OK)
+        break;
+    // FALL THROUGH
+    case OK:
+      if (!this->check_outbound_terminal() || _out_resp_state != READY)
+        break;
+    // FALL THROUGH
+    case READY:
+      // Do setup for flowing upstream data to user agent.
+      _out.do_write(_self_cont, INT64_MAX);
+      TSVIOReenable(_out._write._vio);
+      _out_resp_state = STREAM;
+    // FALL THROUGH
+    case STREAM:
+      this->flow_to_ua();
+      break;
+    }
+  } else if (vio == _ua._read._vio) {
+    this->flow_to_outbound();
+  }
+}
+
+bool
+Bridge::check_outbound_OK()
+{
+  bool zret = false;
+  TextView raw{_out.first_block_data()};
+
+  // Only need to check the first block - it's guaranteed to be big enough to hold the status line
+  // and the status line is always the first part of the response.
+  // Looking for 'HTTP/#.# ### Reason text ...' where '#' is a digit.
+  if (raw.size() > (8 + 3 + 1 + 3)) { // if not at least this much data, no chance of success.
+    TextView block{raw};
+    // Sigh, just unroll the check.
+    if (block[0] == 'H' && block[1] == 'T' && block[2] == 'T' && block[3] == 'P' && block[4] == '/') {
+      block += 5;
+      if (block[1] == '.' && ((block[0] == '1' && (block[2] == '0' || block[2] == '1')) || (block[0] == '0' && block[2] == '9'))) {
+        block += 3;
+        block.ltrim_if(&isspace);
+        TextView code  = block.take_prefix_if(&isspace);
+        TSHttpStatus c = static_cast<TSHttpStatus>(ts::svtoi(code));
+        if (TS_HTTP_STATUS_OK == c) {
+          _out_resp_state = OK;
+        } else {
+          // Save the reason provided from upstream.
+          TextView reason = block.take_prefix_at('\r');
+          _out_response_reason.assign(reason.data(), reason.size());
+          _out_resp_state = ERROR;
+        }
+        // 519 is POOMA, useful for debugging, but may want to change this later.
+        _out_response_code = c ? c : static_cast<TSHttpStatus>(519);
+        if (_ua_response_suspended) {
+          this->update_ua_response();
+          TSHttpTxnReenable(_ua_txn, TS_EVENT_HTTP_CONTINUE);
+          _ua_response_suspended = false;
+          TSDebug(PLUGIN_TAG, "TXN resumed");
+        }
+        _out.consume(block.data() - raw.data());
+        zret = true;
+        TSDebug(PLUGIN_TAG, "Outbound status %d", c);
+      }
+    }
+  }
+  return zret;
+}
+
+bool
+Bridge::check_outbound_terminal()
+{
+  bool zret = false;
+  TextView block;
+
+  // Need to be more careful here than with the status check because the terminator can
+  // be a large distance in to the response.
+  while (READY != _out_resp_state && !(block = _out.first_block_data()).empty()) { // data is available
+    // Loop through the bytes in the block data.
+    int64_t n_bytes = 0;
+    while (block) {
+      char c = *block;
+      if ('\r' == c) {
+        if (_out_terminal_pos == 2)
+          _out_terminal_pos = 3;
+        else
+          _out_terminal_pos = 1;
+      } else if ('\n' == c) {
+        if (_out_terminal_pos == 3) {
+          _out_terminal_pos = 4;
+          _out_resp_state   = READY;
+          zret              = true;
+          TSDebug(PLUGIN_TAG, "Outbound ready");
+        } else if (_out_terminal_pos == 1) {
+          _out_terminal_pos = 2;
+        } else {
+          _out_terminal_pos = 0;
+        }
+      } else {
+        _out_terminal_pos = 0;
+      }
+      ++block;
+      ++n_bytes;
+    }
+    _out.consume(n_bytes);
+  }
+  return zret;
+}
+
+void
+Bridge::flow_to_ua()
+{
+  int64_t avail = _out.available_size();
+  if (avail > 0) {
+    int64_t n = TSIOBufferCopy(_ua._write._buff, _out._read._reader, avail, 0);
+    // Assert for now, need to handle this more gracefully.
+    TSAssert(n == avail);
+
+    _out.consume(n);
+    TSDebug(PLUGIN_TAG, "Wrote %" PRId64 " bytes to UA", n);
+    TSVIOReenable(_ua._write._vio);
+    TSVIOReenable(_out._read._vio);
+  }
+}
+
+void
+Bridge::flow_to_outbound()
+{
+  int64_t avail = _ua.available_size();
+  if (avail > 0) {
+    int64_t n = TSIOBufferCopy(_out._write._buff, _ua._read._reader, avail, 0);
+    // Assert for now, need to handle this more gracefully.
+    TSAssert(n == avail);
+
+    _ua.consume(n);
+    TSDebug(PLUGIN_TAG, "Wrote %" PRId64 " bytes to upstream", n);
+    TSVIOReenable(_out._write._vio);
+    TSVIOReenable(_ua._read._vio);
+  }
+}
+
+void
+Bridge::eos(TSVIO vio)
+{
+  if (vio == _out._write._vio || vio == _out._read._vio) {
+    TSDebug(PLUGIN_TAG, "EOS upstream");
+  } else if (vio == _ua._write._vio || vio == _ua._read._vio) {
+    TSDebug(PLUGIN_TAG, "EOS user agent");
+  } else {
+    TSDebug(PLUGIN_TAG, "EOS from unknown VIO");
+  }
+  _out.do_close();
+  _ua.do_close();
+  _out_resp_state = EOS;
+  if (_ua_response_suspended)
+    TSHttpTxnReenable(_ua_txn, TS_EVENT_HTTP_CONTINUE);
+}
+
+void
+Bridge::send_response_cb()
+{
+  // If the upstream response hasn't been parsed yet, make the UA response wait for that.
+  // Set a flag so the upstream response parser knows to update response and reenable.
+  if (_out_resp_state < OK) {
+    _ua_response_suspended = true;
+    TSDebug(PLUGIN_TAG, "TXN suspended");
+  } else { // Already have all the data needed to do the update, so do it and move on.
+    this->update_ua_response();
+    TSHttpTxnReenable(_ua_txn, TS_EVENT_HTTP_CONTINUE);
+  }
+}
+
+void
+Bridge::update_ua_response()
+{
+  TSMBuffer mbuf;
+  TSMLoc hdr_loc;
+  if (TS_SUCCESS == TSHttpTxnClientRespGet(_ua_txn, &mbuf, &hdr_loc)) {
+    // A 200 for @a out_response_code only means there wasn't an internal failure on the upstream
+    // CONNECT. Network and other failures get reported in this response. This response code will
+    // be more accurate, so use it unless it's 200, in which case use the stored response code if
+    // that's not 200.
+    TSHttpStatus status = TSHttpHdrStatusGet(mbuf, hdr_loc);
+    if (TS_HTTP_STATUS_OK == status && TS_HTTP_STATUS_OK != _out_response_code) {
+      TSHttpHdrStatusSet(mbuf, hdr_loc, _out_response_code);
+      if (!_out_response_reason.empty())
+        TSHttpHdrReasonSet(mbuf, hdr_loc, _out_response_reason.data(), _out_response_reason.size());
+    }
+    // TS insists on adding these fields, despite it being a CONNECT.
+    Hdr_Remove_Field(mbuf, hdr_loc, {TS_MIME_FIELD_TRANSFER_ENCODING, TS_MIME_LEN_TRANSFER_ENCODING});
+    Hdr_Remove_Field(mbuf, hdr_loc, {TS_MIME_FIELD_AGE, TS_MIME_LEN_AGE});
+    Hdr_Remove_Field(mbuf, hdr_loc, {TS_MIME_FIELD_PROXY_CONNECTION, TS_MIME_LEN_PROXY_CONNECTION});
+    TSHandleMLocRelease(mbuf, TS_NULL_MLOC, hdr_loc);
+  } else {
+    TSDebug(PLUGIN_TAG, "Failed to retrieve client response");
+  }
+}
+
+void
+Bridge::VCData::init(TSVConn vc)
+{
+  _vc = vc;
+  _write.init();
+  _read.init();
+}
+
+void
+Bridge::VCData::do_read(TSCont cont, int64_t n)
+{
+  _read._vio = TSVConnRead(_vc, cont, _read._buff, n);
+}
+
+void
+Bridge::VCData::do_write(TSCont cont, int64_t n)
+{
+  _write._vio = TSVConnWrite(_vc, cont, _write._reader, n);
+}
+
+void
+Bridge::VCData::do_close()
+{
+  if (_vc) {
+    TSVConnClose(_vc);
+    _vc = nullptr;
+  }
+  _write.close();
+  _read.close();
+}
+
+int64_t
+Bridge::VCData::available_size()
+{
+  return TSIOBufferReaderAvail(_read._reader);
+}
+
+TextView
+Bridge::VCData::first_block_data()
+{
+  TSIOBufferBlock b = TSIOBufferReaderStart(_read._reader);
+  if (b) {
+    int64_t k;
+    const char *s = TSIOBufferBlockReadStart(b, _read._reader, &k);
+    return {s, static_cast<size_t>(k)};
+  }
+  return {nullptr, 0};
+}
+
+void
+Bridge::VCData::consume(int64_t n)
+{
+  TSIOBufferReaderConsume(_read._reader, n);
+}
+
+void
+Bridge::Op::init()
+{
+  _buff   = TSIOBufferCreate();
+  _reader = TSIOBufferReaderAlloc(_buff);
+}
+
+void
+Bridge::Op::close()
+{
+  if (_reader) {
+    TSIOBufferReaderFree(_reader);
+    _reader = nullptr;
+  }
+  if (_buff) {
+    TSIOBufferDestroy(_buff);
+    _buff = nullptr;
+  }
+}
+
+/* ------------------------------------------------------------------------------------ */
+// Basically a dispatcher - look up the Bridge instance and call the appropriate method.
+int
+CB_Exec(TSCont contp, TSEvent ev_idx, void *data)
+{
+  auto ctx = static_cast<Bridge *>(TSContDataGet(contp));
+
+  switch (ev_idx) {
+  case TS_EVENT_NET_ACCEPT:
+    ctx->net_accept(static_cast<TSVConn>(data));
+    break;
+  case TS_EVENT_VCONN_READ_READY:
+  case TS_EVENT_VCONN_READ_COMPLETE:
+    ctx->read_ready(static_cast<TSVIO>(data));
+    break;
+  case TS_EVENT_VCONN_WRITE_READY:
+    break;
+  case TS_EVENT_VCONN_WRITE_COMPLETE:
+    break;
+  case TS_EVENT_VCONN_INACTIVITY_TIMEOUT:
+  case TS_EVENT_VCONN_ACTIVE_TIMEOUT:
+  case TS_EVENT_VCONN_EOS:
+    ctx->eos(static_cast<TSVIO>(data));
+    break;
+  case TS_EVENT_HTTP_SEND_RESPONSE_HDR:
+    TSDebug(PLUGIN_TAG, "SEND_RESPONSE_HDR");
+    ctx->send_response_cb();
+    break;
+  case TS_EVENT_HTTP_TXN_CLOSE:
+    TSDebug(PLUGIN_TAG, "TXN_CLOSE: cleanup");
+    delete ctx;
+    break;
+  default:
+    TSDebug(PLUGIN_TAG, "Event %d", ev_idx);
+    break;
+  }
+  return TS_EVENT_CONTINUE;
+}
+
+// Handle a new transaction - check if it should be intercepted and if so do the intercept.
+int
+CB_Read_Request_Hdr(TSCont contp, TSEvent ev_idx, void *data)
+{
+  auto txn = static_cast<TSHttpTxn>(data);
+  TSMBuffer mbuf;
+  TSMLoc hdr_loc;
+
+  if (!TSHttpTxnIsInternal(txn)) {
+    if (TS_SUCCESS == TSHttpTxnClientReqGet(txn, &mbuf, &hdr_loc)) {
+      int method_len;
+      const char *method_data = TSHttpHdrMethodGet(mbuf, hdr_loc, &method_len);
+      if (TextView{method_data, method_len} == METHOD_CONNECT) {
+        int host_len          = 0;
+        const char *host_name = TSHttpHdrHostGet(mbuf, hdr_loc, &host_len);
+        TextView peer{Config.match({host_name, host_len})};
+        if (peer) {
+          // Everything checks, let's intercept.
+          auto actor = TSContCreate(CB_Exec, TSContMutexGet(reinterpret_cast<TSCont>(txn)));
+          auto ctx   = new Bridge(actor, txn, peer);
+
+          TSDebug(PLUGIN_TAG, "Intercepting transaction %" PRIu64 " to '%.*s' via '%.*s'", TSHttpTxnIdGet(txn), host_len, host_name,
+                  static_cast<int>(peer.size()), peer.data());
+
+          TSContDataSet(actor, ctx);
+          // Need to play games with the response, delaying it until upstream connection is done.
+          // Also may potentiall modify it to correspond to the upstream result.
+          TSHttpTxnHookAdd(txn, TS_HTTP_SEND_RESPONSE_HDR_HOOK, actor);
+          // Arrange for cleanup.
+          TSHttpTxnHookAdd(txn, TS_HTTP_TXN_CLOSE_HOOK, actor);
+          // Grab the transaction
+          TSHttpTxnIntercept(actor, txn);
+        }
+      }
+    }
+  }
+  TSHttpTxnReenable(txn, TS_EVENT_HTTP_CONTINUE);
+  return TS_EVENT_CONTINUE;
+}
+
+/* ------------------------------------------------------------------------------------ */
+
+void
+TSPluginInit(int argc, char const *argv[])
+{
+  TSPluginRegistrationInfo info{PLUGIN_NAME, "Oath:", "solidwallofcode@oath.com"};
+
+  if (TSPluginRegister(&info) != TS_SUCCESS)
+    TSError(PLUGIN_NAME ": plugin registration failed.");
+
+  Config.load_config(argc - 1, argv + 1);
+  if (Config.count() <= 0)
+    TSError("%s: No destinations defined, plugin disabled", PLUGIN_TAG);
+
+  TSCont contp = TSContCreate(CB_Read_Request_Hdr, TSMutexCreate());
+  TSHttpHookAdd(TS_HTTP_READ_REQUEST_HDR_HOOK, contp);
+}

-- 
To stop receiving notification emails like this one, please contact
amc@apache.org.