You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficserver.apache.org by zw...@apache.org on 2017/07/26 23:40:49 UTC

[trafficserver] branch 7.1.x updated: Rework SSL handshake hooks and add tls_hooks tests.

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

zwoop pushed a commit to branch 7.1.x
in repository https://gitbox.apache.org/repos/asf/trafficserver.git


The following commit(s) were added to refs/heads/7.1.x by this push:
     new 3c2ab23  Rework SSL handshake hooks and add tls_hooks tests.
3c2ab23 is described below

commit 3c2ab2338ebbb0ddde28c685eb3b4d0ea54cfa14
Author: Susan Hinrichs <sh...@spellhotel.corp.ne1.yahoo.com>
AuthorDate: Wed Jul 5 16:23:17 2017 +0000

    Rework SSL handshake hooks and add tls_hooks tests.
    
    (cherry picked from commit f68361e53d059b5773b055e0186b0224b1b7db51)
    
    Conflicts:
    	iocore/net/P_SSLNetVConnection.h
    	iocore/net/SSLNetVConnection.cc
---
 iocore/net/P_SSLNetVConnection.h                   |  52 ++++-
 iocore/net/SSLNetVConnection.cc                    | 225 +++++++++++--------
 .../autest-site/trafficserver_plugins.test.ext     |   4 +-
 tests/gold_tests/tls_hooks/gold/cert-1.gold        |   0
 tests/gold_tests/tls_hooks/gold/preaccept-1.gold   |   0
 tests/gold_tests/tls_hooks/gold/sni-1.gold         |   0
 .../gold_tests/tls_hooks/gold/ts-cert-1-im-2.gold  |   5 +
 tests/gold_tests/tls_hooks/gold/ts-cert-1.gold     |   3 +
 tests/gold_tests/tls_hooks/gold/ts-cert-2.gold     |   5 +
 tests/gold_tests/tls_hooks/gold/ts-cert-im-1.gold  |   2 +
 .../gold_tests/tls_hooks/gold/ts-preaccept-1.gold  |   3 +
 .../gold_tests/tls_hooks/gold/ts-preaccept-2.gold  |   4 +
 .../gold/ts-preaccept-delayed-1-immdate-2.gold     |   5 +
 .../tls_hooks/gold/ts-preaccept-delayed-1.gold     |   3 +
 .../tls_hooks/gold/ts-preaccept1-sni1-cert1.gold   |   5 +
 tests/gold_tests/tls_hooks/gold/ts-sni-1.gold      |   3 +
 tests/gold_tests/tls_hooks/gold/ts-sni-2.gold      |   4 +
 tests/gold_tests/tls_hooks/ssl/server.key          |  15 ++
 tests/gold_tests/tls_hooks/ssl/server.pem          |  32 +++
 tests/gold_tests/tls_hooks/tls_hooks.test.py       |  78 +++++++
 tests/gold_tests/tls_hooks/tls_hooks10.test.py     |  79 +++++++
 tests/gold_tests/tls_hooks/tls_hooks11.test.py     |  76 +++++++
 tests/gold_tests/tls_hooks/tls_hooks12.test.py     |  87 ++++++++
 tests/gold_tests/tls_hooks/tls_hooks2.test.py      |  77 +++++++
 tests/gold_tests/tls_hooks/tls_hooks3.test.py      |  77 +++++++
 tests/gold_tests/tls_hooks/tls_hooks4.test.py      |  83 +++++++
 tests/gold_tests/tls_hooks/tls_hooks6.test.py      |  81 +++++++
 tests/gold_tests/tls_hooks/tls_hooks7.test.py      |  80 +++++++
 tests/gold_tests/tls_hooks/tls_hooks8.test.py      |  80 +++++++
 tests/gold_tests/tls_hooks/tls_hooks9.test.py      |  76 +++++++
 tests/tools/plugins/ssl_hook_test.cc               | 248 +++++++++++++++++++++
 tools/tsxs.in                                      |   1 +
 32 files changed, 1388 insertions(+), 105 deletions(-)

diff --git a/iocore/net/P_SSLNetVConnection.h b/iocore/net/P_SSLNetVConnection.h
index 1878400..3b26b84 100644
--- a/iocore/net/P_SSLNetVConnection.h
+++ b/iocore/net/P_SSLNetVConnection.h
@@ -215,7 +215,44 @@ public:
 
   // Returns true if we have already called at
   // least some of the hooks
-  bool calledHooks(TSEvent /* eventId */) const { return (this->sslHandshakeHookState != HANDSHAKE_HOOKS_PRE); }
+  bool
+  calledHooks(TSEvent eventId) const
+  {
+    bool retval = false;
+    switch (this->sslHandshakeHookState) {
+    case HANDSHAKE_HOOKS_PRE:
+    case HANDSHAKE_HOOKS_PRE_INVOKE:
+      if (eventId == TS_EVENT_VCONN_PRE_ACCEPT) {
+        if (curHook) {
+          retval = true;
+        }
+      }
+      break;
+    case HANDSHAKE_HOOKS_SNI:
+      if (eventId == TS_EVENT_VCONN_PRE_ACCEPT) {
+        retval = true;
+      } else if (eventId == TS_EVENT_SSL_SERVERNAME) {
+        if (curHook) {
+          retval = true;
+        }
+      }
+      break;
+    case HANDSHAKE_HOOKS_CERT:
+    case HANDSHAKE_HOOKS_CERT_INVOKE:
+      if (eventId == TS_EVENT_VCONN_PRE_ACCEPT || eventId == TS_EVENT_SSL_SERVERNAME) {
+        retval = true;
+      } else if (eventId == TS_EVENT_SSL_CERT) {
+        if (curHook) {
+          retval = true;
+        }
+      }
+      break;
+    case HANDSHAKE_HOOKS_DONE:
+      retval = true;
+      break;
+    }
+    return retval;
+  }
   bool
   getSSLTrace() const
   {
@@ -281,19 +318,12 @@ private:
   /// @note For @C SSL_HOOKS_INVOKE, this is the hook to invoke.
   class APIHook *curHook;
 
-  enum {
-    SSL_HOOKS_INIT,     ///< Initial state, no hooks called yet.
-    SSL_HOOKS_INVOKE,   ///< Waiting to invoke hook.
-    SSL_HOOKS_ACTIVE,   ///< Hook invoked, waiting for it to complete.
-    SSL_HOOKS_CONTINUE, ///< All hooks have been called and completed
-    SSL_HOOKS_DONE      ///< All hooks have been called and completed
-  } sslPreAcceptHookState;
-
   enum SSLHandshakeHookState {
     HANDSHAKE_HOOKS_PRE,
+    HANDSHAKE_HOOKS_PRE_INVOKE,
+    HANDSHAKE_HOOKS_SNI,
     HANDSHAKE_HOOKS_CERT,
-    HANDSHAKE_HOOKS_POST,
-    HANDSHAKE_HOOKS_INVOKE,
+    HANDSHAKE_HOOKS_CERT_INVOKE,
     HANDSHAKE_HOOKS_DONE
   } sslHandshakeHookState;
 
diff --git a/iocore/net/SSLNetVConnection.cc b/iocore/net/SSLNetVConnection.cc
index e510d84..53d7012 100644
--- a/iocore/net/SSLNetVConnection.cc
+++ b/iocore/net/SSLNetVConnection.cc
@@ -381,6 +381,8 @@ SSLNetVConnection::read_raw_data()
     BIO *rbio = BIO_new_mem_buf(start, this->handShakeBioStored);
     BIO_set_mem_eof_return(rbio, -1);
     SSL_set0_rbio(this->ssl, rbio);
+  } else {
+    this->handShakeBioStored = 0;
   }
 
   Debug("ssl", "%p read r=%" PRId64 " total=%" PRId64 " bio=%d\n", this, r, total_read, this->handShakeBioStored);
@@ -419,7 +421,8 @@ SSLNetVConnection::update_rbio(bool move_to_socket)
       BIO_set_mem_eof_return(rbio, -1);
       SSL_set0_rbio(this->ssl, rbio);
       retval = true;
-    } else if (move_to_socket) { // Handshake buffer is empty, move to the socket rbio
+      // Handshake buffer is empty but we have read something, move to the socket rbio
+    } else if (move_to_socket && this->handShakeHolder->is_read_avail_more_than(0)) {
       BIO *rbio = BIO_new_fd(this->get_socket(), BIO_NOCLOSE);
       BIO_set_mem_eof_return(rbio, -1);
       SSL_set0_rbio(this->ssl, rbio);
@@ -486,10 +489,7 @@ SSLNetVConnection::net_read_io(NetHandler *nh, EThread *lthread)
     }
     // If we have flipped to blind tunnel, don't read ahead
     if (this->handShakeReader) {
-      if (this->attributes != HttpProxyPort::TRANSPORT_BLIND_TUNNEL) {
-        // Check and consume data that has been read
-        update_rbio(false);
-      } else {
+      if (this->attributes == HttpProxyPort::TRANSPORT_BLIND_TUNNEL) {
         // Now in blind tunnel. Set things up to read what is in the buffer
         // Must send the READ_COMPLETE here before considering
         // forwarding on the handshake buffer, so the
@@ -544,7 +544,6 @@ SSLNetVConnection::net_read_io(NetHandler *nh, EThread *lthread)
       if (this->handShakeBuffer) {
         read.triggered = update_rbio(true);
       } else {
-        Debug("ssl", "Want read from socket");
         read.triggered = 0;
       }
       if (!read.triggered) {
@@ -817,7 +816,6 @@ SSLNetVConnection::SSLNetVConnection()
     handShakeHolder(nullptr),
     handShakeReader(nullptr),
     handShakeBioStored(0),
-    sslPreAcceptHookState(SSL_HOOKS_INIT),
     sslHandshakeHookState(HANDSHAKE_HOOKS_PRE),
     npnSet(nullptr),
     npnEndpoint(nullptr),
@@ -910,16 +908,11 @@ SSLNetVConnection::free(EThread *t)
   sslClientRenegotiationAbort = false;
   sslSessionCacheHit          = false;
 
-  if (SSL_HOOKS_ACTIVE == sslPreAcceptHookState) {
-    Error("SSLNetVconnection freed with outstanding hook");
-  }
-
-  sslPreAcceptHookState = SSL_HOOKS_INIT;
-  curHook               = nullptr;
-  hookOpRequested       = SSL_HOOK_OP_DEFAULT;
-  npnSet                = nullptr;
-  npnEndpoint           = nullptr;
-  sslHandShakeComplete  = false;
+  curHook              = nullptr;
+  hookOpRequested      = SSL_HOOK_OP_DEFAULT;
+  npnSet               = nullptr;
+  npnEndpoint          = nullptr;
+  sslHandShakeComplete = false;
   free_handshake_buffers();
   sslTrace = false;
 
@@ -1050,41 +1043,29 @@ SSLNetVConnection::sslStartHandShake(int event, int &err)
 int
 SSLNetVConnection::sslServerHandShakeEvent(int &err)
 {
-  if (SSL_HOOKS_DONE != sslPreAcceptHookState) {
-    // Get the first hook if we haven't started invoking yet.
-    if (SSL_HOOKS_INIT == sslPreAcceptHookState) {
-      curHook               = ssl_hooks->get(TS_VCONN_PRE_ACCEPT_INTERNAL_HOOK);
-      sslPreAcceptHookState = SSL_HOOKS_INVOKE;
-    } else if (SSL_HOOKS_INVOKE == sslPreAcceptHookState) {
-      // if the state is anything else, we haven't finished
-      // the previous hook yet.
+  // Continue on if we are in the invoked state.  The hook has not yet reenabled
+  if (sslHandshakeHookState == HANDSHAKE_HOOKS_CERT_INVOKE || sslHandshakeHookState == HANDSHAKE_HOOKS_PRE_INVOKE) {
+    return SSL_WAIT_FOR_HOOK;
+  }
+
+  // Go do the preaccept hooks
+  if (sslHandshakeHookState == HANDSHAKE_HOOKS_PRE) {
+    if (!curHook) {
+      Debug("ssl", "Initialize preaccept curHook from NULL");
+      curHook = ssl_hooks->get(TS_VCONN_PRE_ACCEPT_INTERNAL_HOOK);
+    } else {
       curHook = curHook->next();
     }
-
-    if (SSL_HOOKS_INVOKE == sslPreAcceptHookState) {
-      if (nullptr == curHook) { // no hooks left, we're done
-        sslPreAcceptHookState = SSL_HOOKS_DONE;
-      } else {
-        sslPreAcceptHookState = SSL_HOOKS_ACTIVE;
-        ContWrapper::wrap(mutex.get(), curHook->m_cont, TS_EVENT_VCONN_PRE_ACCEPT, this);
-        return SSL_WAIT_FOR_HOOK;
-      }
-    } else { // waiting for hook to complete
-             /* A note on waiting for the hook. I believe that because this logic
-                cannot proceed as long as a hook is outstanding, the underlying VC
-                can't go stale. If that can happen for some reason, we'll need to be
-                more clever and provide some sort of cancel mechanism. I have a trap
-                in SSLNetVConnection::free to check for this.
-             */
+    // If no more hooks, move onto SNI
+    if (nullptr == curHook) {
+      sslHandshakeHookState = HANDSHAKE_HOOKS_SNI;
+    } else {
+      sslHandshakeHookState = HANDSHAKE_HOOKS_PRE_INVOKE;
+      ContWrapper::wrap(nh->mutex.get(), curHook->m_cont, TS_EVENT_VCONN_PRE_ACCEPT, this);
       return SSL_WAIT_FOR_HOOK;
     }
   }
 
-  // handle SNI Hooks after PreAccept Hooks
-  if (HANDSHAKE_HOOKS_DONE != sslHandshakeHookState && HANDSHAKE_HOOKS_PRE != sslHandshakeHookState) {
-    return SSL_WAIT_FOR_HOOK;
-  }
-
   // If a blind tunnel was requested in the pre-accept calls, convert.
   // Again no data has been exchanged, so we can go directly
   // without data replay.
@@ -1102,11 +1083,14 @@ SSLNetVConnection::sslServerHandShakeEvent(int &err)
     sslHandShakeComplete = true;
     return EVENT_DONE;
   }
+
+  Debug("ssl", "Go on with the handshake state=%d", sslHandshakeHookState);
+
   // All the pre-accept hooks have completed, proceed with the actual accept.
   if (this->handShakeReader) {
     if (BIO_eof(SSL_get_rbio(this->ssl))) { // No more data in the buffer
       // Is this the first read?
-      if (!this->handShakeReader->is_read_avail_more_than(0)) {
+      if (!this->handShakeReader->is_read_avail_more_than(0) && !this->handShakeHolder->is_read_avail_more_than(0)) {
         Debug("ssl", "%p first read\n", this);
         // Read from socket to fill in the BIO buffer with the
         // raw handshake data before calling the ssl accept calls.
@@ -1114,7 +1098,7 @@ SSLNetVConnection::sslServerHandShakeEvent(int &err)
         if (retval < 0) {
           if (retval == -EAGAIN) {
             // No data at the moment, hang tight
-            // SSLDebugVC(this, "SSL handshake: EAGAIN");
+            SSLDebugVC(this, "SSL handshake: EAGAIN");
             return SSL_HANDSHAKE_WANT_READ;
           } else {
             // An error, make us go away
@@ -1424,27 +1408,61 @@ SSLNetVConnection::select_next_protocol(SSL *ssl, const unsigned char **out, uns
 void
 SSLNetVConnection::reenable(NetHandler *nh)
 {
-  if (sslPreAcceptHookState != SSL_HOOKS_DONE) {
-    sslPreAcceptHookState = SSL_HOOKS_INVOKE;
-  } else if (sslHandshakeHookState == HANDSHAKE_HOOKS_INVOKE) {
-    // Reenabling from the handshake callback
-    //
-    // Originally, we would wait for the callback to go again to execute additinonal
-    // hooks, but since the callbacks are associated with the context and the context
-    // can be replaced by the plugin, it didn't seem reasonable to assume that the
-    // callback would be executed again.  So we walk through the rest of the hooks
-    // here in the reenable.
-    if (curHook != nullptr) {
-      curHook = curHook->next();
-    }
-    if (curHook != nullptr) {
-      // Invoke the hook and return, wait for next reenable
+  Debug("ssl", "Handshake reenable from state=%d", sslHandshakeHookState);
+
+  switch (sslHandshakeHookState) {
+  case HANDSHAKE_HOOKS_PRE_INVOKE:
+    sslHandshakeHookState = HANDSHAKE_HOOKS_PRE;
+    break;
+  case HANDSHAKE_HOOKS_CERT_INVOKE:
+    sslHandshakeHookState = HANDSHAKE_HOOKS_CERT;
+    break;
+  default:
+    break;
+  }
+
+  // Reenabling from the handshake callback
+  //
+  // Originally, we would wait for the callback to go again to execute additinonal
+  // hooks, but since the callbacks are associated with the context and the context
+  // can be replaced by the plugin, it didn't seem reasonable to assume that the
+  // callback would be executed again.  So we walk through the rest of the hooks
+  // here in the reenable.
+  if (curHook != nullptr) {
+    curHook = curHook->next();
+    Debug("ssl", "iterate from reenable curHook=%p", curHook);
+  }
+  if (curHook != nullptr) {
+    // Invoke the hook and return, wait for next reenable
+    if (sslHandshakeHookState == HANDSHAKE_HOOKS_CERT) {
+      sslHandshakeHookState = HANDSHAKE_HOOKS_CERT_INVOKE;
       curHook->invoke(TS_EVENT_SSL_CERT, this);
-      return;
-    } else { // curHook == nullptr
-      // empty, set state to HOOKS_DONE
-      this->sslHandshakeHookState = HANDSHAKE_HOOKS_DONE;
+    } else if (sslHandshakeHookState == HANDSHAKE_HOOKS_SNI) {
+      curHook->invoke(TS_EVENT_SSL_SERVERNAME, this);
+    } else if (sslHandshakeHookState == HANDSHAKE_HOOKS_PRE) {
+      Debug("ssl", "Reenable preaccept");
+      sslHandshakeHookState = HANDSHAKE_HOOKS_PRE_INVOKE;
+      ContWrapper::wrap(nh->mutex.get(), curHook->m_cont, TS_EVENT_VCONN_PRE_ACCEPT, this);
+    }
+    return;
+  } else {
+    // Move onto the "next" state
+    switch (this->sslHandshakeHookState) {
+    case HANDSHAKE_HOOKS_PRE:
+    case HANDSHAKE_HOOKS_PRE_INVOKE:
+      sslHandshakeHookState = HANDSHAKE_HOOKS_SNI;
+      break;
+    case HANDSHAKE_HOOKS_SNI:
+      sslHandshakeHookState = HANDSHAKE_HOOKS_CERT;
+      break;
+    case HANDSHAKE_HOOKS_CERT:
+    case HANDSHAKE_HOOKS_CERT_INVOKE:
+      sslHandshakeHookState = HANDSHAKE_HOOKS_DONE;
+      break;
+    default:
+      break;
     }
+    Debug("ssl", "iterate from reenable curHook=%p %d", curHook, sslHandshakeHookState);
   }
   this->readReschedule(nh);
 }
@@ -1471,42 +1489,65 @@ SSLNetVConnection::callHooks(TSEvent eventId)
   ink_assert(eventId == TS_EVENT_SSL_CERT || eventId == TS_EVENT_SSL_SERVERNAME);
   Debug("ssl", "callHooks sslHandshakeHookState=%d", this->sslHandshakeHookState);
 
-  // First time through, set the type of the hook that is currently being invoked
-  if ((this->sslHandshakeHookState == HANDSHAKE_HOOKS_PRE || this->sslHandshakeHookState == HANDSHAKE_HOOKS_DONE) &&
-      eventId == TS_EVENT_SSL_CERT) {
-    // the previous hook should be DONE and set curHook to nullptr before trigger the sni hook.
-    ink_assert(curHook == nullptr);
-    // set to HOOKS_CERT means CERT/SNI hooks has called by SSL_accept()
-    this->sslHandshakeHookState = HANDSHAKE_HOOKS_CERT;
-    // get Hooks
-    curHook = ssl_hooks->get(TS_SSL_CERT_INTERNAL_HOOK);
-  } else if (eventId == TS_EVENT_SSL_SERVERNAME) {
+  // Move state if it is appropriate
+  switch (this->sslHandshakeHookState) {
+  case HANDSHAKE_HOOKS_PRE:
+    if (eventId == TS_EVENT_SSL_SERVERNAME) {
+      this->sslHandshakeHookState = HANDSHAKE_HOOKS_SNI;
+    } else if (eventId == TS_EVENT_SSL_CERT) {
+      this->sslHandshakeHookState = HANDSHAKE_HOOKS_CERT;
+    }
+    break;
+  case HANDSHAKE_HOOKS_SNI:
+    if (eventId == TS_EVENT_SSL_CERT) {
+      this->sslHandshakeHookState = HANDSHAKE_HOOKS_CERT;
+    }
+    break;
+  default:
+    break;
+  }
+
+  // Look for hooks associated with the event
+  switch (this->sslHandshakeHookState) {
+  case HANDSHAKE_HOOKS_SNI:
     if (!curHook) {
       curHook = ssl_hooks->get(TS_SSL_SERVERNAME_INTERNAL_HOOK);
+    } else {
+      curHook = curHook->next();
     }
-  } else {
-    // Not in the right state
-    // reenable and continue
+    if (!curHook) {
+      this->sslHandshakeHookState = HANDSHAKE_HOOKS_CERT;
+    }
+    break;
+  case HANDSHAKE_HOOKS_CERT:
+  case HANDSHAKE_HOOKS_CERT_INVOKE:
+    if (!curHook) {
+      curHook = ssl_hooks->get(TS_SSL_CERT_INTERNAL_HOOK);
+    } else {
+      curHook = curHook->next();
+    }
+    if (curHook == nullptr) {
+      this->sslHandshakeHookState = HANDSHAKE_HOOKS_DONE;
+    } else {
+      this->sslHandshakeHookState = HANDSHAKE_HOOKS_CERT_INVOKE;
+    }
+    break;
+  default:
+    curHook                     = nullptr;
+    this->sslHandshakeHookState = HANDSHAKE_HOOKS_DONE;
     return true;
   }
 
+  Debug("ssl", "callHooks iterated to curHook=%p", curHook);
+
   bool reenabled = true;
   if (curHook != nullptr) {
-    this->sslHandshakeHookState = HANDSHAKE_HOOKS_INVOKE;
     curHook->invoke(eventId, this);
-    reenabled = eventId != TS_EVENT_SSL_CERT || (this->sslHandshakeHookState != HANDSHAKE_HOOKS_INVOKE);
+    reenabled =
+      (this->sslHandshakeHookState != HANDSHAKE_HOOKS_CERT_INVOKE && this->sslHandshakeHookState != HANDSHAKE_HOOKS_PRE_INVOKE);
+    Debug("ssl", "Called hook on state=%d reenabled=%d", sslHandshakeHookState, reenabled);
   }
 
-  // All done with the current hook chain
-  if (curHook == nullptr) {
-    if (eventId == TS_EVENT_SSL_CERT) {
-      // Set the HookState to done because we are all done with the CERT/SERVERNAME hook chains
-      sslHandshakeHookState = HANDSHAKE_HOOKS_DONE;
-    } else if (eventId == TS_EVENT_SSL_SERVERNAME) {
-      // Reset the HookState to PRE, so the cert hook chain can start
-      sslHandshakeHookState = HANDSHAKE_HOOKS_PRE;
-    }
-  }
   return reenabled;
 }
 
diff --git a/tests/gold_tests/autest-site/trafficserver_plugins.test.ext b/tests/gold_tests/autest-site/trafficserver_plugins.test.ext
index fb53bd7..9ba2718 100644
--- a/tests/gold_tests/autest-site/trafficserver_plugins.test.ext
+++ b/tests/gold_tests/autest-site/trafficserver_plugins.test.ext
@@ -19,7 +19,7 @@ Builds, installs, and enables an ATS plugin in the sandbox environment
 
 import os
 
-def prepare_plugin(self, path, tsproc):
+def prepare_plugin(self, path, tsproc, plugin_args = ""):
     """Builds, installs, and enables an ATS plugin in the sandbox environment
 
     The source file at the given path is copied to the sandbox directory of the
@@ -39,6 +39,6 @@ def prepare_plugin(self, path, tsproc):
     tsproc.Setup.RunCommand("tsxs -c {0} -o {1}".format(in_path, out_path))
 
     # Add an entry to plugin.config.
-    tsproc.Disk.plugin_config.AddLine(out_basename)
+    tsproc.Disk.plugin_config.AddLine("{0} {1}".format(out_basename,plugin_args))
 
 ExtendTest(prepare_plugin, name="prepare_plugin")
diff --git a/tests/gold_tests/tls_hooks/gold/cert-1.gold b/tests/gold_tests/tls_hooks/gold/cert-1.gold
new file mode 100644
index 0000000..e69de29
diff --git a/tests/gold_tests/tls_hooks/gold/preaccept-1.gold b/tests/gold_tests/tls_hooks/gold/preaccept-1.gold
new file mode 100644
index 0000000..e69de29
diff --git a/tests/gold_tests/tls_hooks/gold/sni-1.gold b/tests/gold_tests/tls_hooks/gold/sni-1.gold
new file mode 100644
index 0000000..e69de29
diff --git a/tests/gold_tests/tls_hooks/gold/ts-cert-1-im-2.gold b/tests/gold_tests/tls_hooks/gold/ts-cert-1-im-2.gold
new file mode 100644
index 0000000..a2ec9b7
--- /dev/null
+++ b/tests/gold_tests/tls_hooks/gold/ts-cert-1-im-2.gold
@@ -0,0 +1,5 @@
+`` DIAG: (ssl_hook_test) Setup callbacks pa=0 sni=0 cert=1 cert_imm=2 pa_delay=0
+`` DIAG: (ssl_hook_test) Cert callback 0 ssl_vc=``
+`` DIAG: (ssl_hook_test) Callback reenable ssl_vc=``
+`` DIAG: (ssl_hook_test) Cert callback 0 ssl_vc=``
+`` DIAG: (ssl_hook_test) Cert callback 1 ssl_vc=``
diff --git a/tests/gold_tests/tls_hooks/gold/ts-cert-1.gold b/tests/gold_tests/tls_hooks/gold/ts-cert-1.gold
new file mode 100644
index 0000000..91f7b38
--- /dev/null
+++ b/tests/gold_tests/tls_hooks/gold/ts-cert-1.gold
@@ -0,0 +1,3 @@
+`` DIAG: (ssl_hook_test) Setup callbacks pa=0 sni=0 cert=1 cert_imm=0 pa_delay=0
+`` DIAG: (ssl_hook_test) Cert callback 0 ssl_vc=``
+`` DIAG: (ssl_hook_test) Callback reenable ssl_vc=``
diff --git a/tests/gold_tests/tls_hooks/gold/ts-cert-2.gold b/tests/gold_tests/tls_hooks/gold/ts-cert-2.gold
new file mode 100644
index 0000000..355c5ae
--- /dev/null
+++ b/tests/gold_tests/tls_hooks/gold/ts-cert-2.gold
@@ -0,0 +1,5 @@
+`` DIAG: (ssl_hook_test) Setup callbacks pa=0 sni=0 cert=2 cert_imm=0 pa_delay=0
+`` DIAG: (ssl_hook_test) Cert callback 0 ssl_vc=``
+`` DIAG: (ssl_hook_test) Callback reenable ssl_vc=``
+`` DIAG: (ssl_hook_test) Cert callback 1 ssl_vc=``
+`` DIAG: (ssl_hook_test) Callback reenable ssl_vc=``
diff --git a/tests/gold_tests/tls_hooks/gold/ts-cert-im-1.gold b/tests/gold_tests/tls_hooks/gold/ts-cert-im-1.gold
new file mode 100644
index 0000000..c571bae
--- /dev/null
+++ b/tests/gold_tests/tls_hooks/gold/ts-cert-im-1.gold
@@ -0,0 +1,2 @@
+`` DIAG: (ssl_hook_test) Setup callbacks pa=0 sni=0 cert=0 cert_imm=1 pa_delay=0
+`` DIAG: (ssl_hook_test) Cert callback 0 ssl_vc=``
diff --git a/tests/gold_tests/tls_hooks/gold/ts-preaccept-1.gold b/tests/gold_tests/tls_hooks/gold/ts-preaccept-1.gold
new file mode 100644
index 0000000..c8278ea
--- /dev/null
+++ b/tests/gold_tests/tls_hooks/gold/ts-preaccept-1.gold
@@ -0,0 +1,3 @@
+`` DIAG: (ssl_hook_test) Setup callbacks pa=1 sni=0 cert=0 cert_imm=0 pa_delay=0
+`` DIAG: (ssl_hook_test) Pre accept callback 0 `` - event is good
+``
diff --git a/tests/gold_tests/tls_hooks/gold/ts-preaccept-2.gold b/tests/gold_tests/tls_hooks/gold/ts-preaccept-2.gold
new file mode 100644
index 0000000..cfac682
--- /dev/null
+++ b/tests/gold_tests/tls_hooks/gold/ts-preaccept-2.gold
@@ -0,0 +1,4 @@
+`` DIAG: (ssl_hook_test) Setup callbacks pa=2 sni=0 cert=0 cert_imm=0 pa_delay=0
+`` DIAG: (ssl_hook_test) Pre accept callback 0 `` - event is good
+`` DIAG: (ssl_hook_test) Pre accept callback 1 `` - event is good
+``
diff --git a/tests/gold_tests/tls_hooks/gold/ts-preaccept-delayed-1-immdate-2.gold b/tests/gold_tests/tls_hooks/gold/ts-preaccept-delayed-1-immdate-2.gold
new file mode 100644
index 0000000..16427c9
--- /dev/null
+++ b/tests/gold_tests/tls_hooks/gold/ts-preaccept-delayed-1-immdate-2.gold
@@ -0,0 +1,5 @@
+``DIAG: (ssl_hook_test) Setup callbacks pa=2 sni=0 cert=0 cert_imm=0 pa_delay=1
+``DIAG: (ssl_hook_test) Pre accept callback 0 `` - event is good
+``DIAG: (ssl_hook_test) Pre accept callback 1 `` - event is good
+``DIAG: (ssl_hook_test) Pre accept delay callback 0 `` - event is good
+``DIAG: (ssl_hook_test) Callback reenable ssl_vc=``
diff --git a/tests/gold_tests/tls_hooks/gold/ts-preaccept-delayed-1.gold b/tests/gold_tests/tls_hooks/gold/ts-preaccept-delayed-1.gold
new file mode 100644
index 0000000..0b85e17
--- /dev/null
+++ b/tests/gold_tests/tls_hooks/gold/ts-preaccept-delayed-1.gold
@@ -0,0 +1,3 @@
+`` DIAG: (ssl_hook_test) Setup callbacks pa=0 sni=0 cert=0 cert_imm=0 pa_delay=1
+`` DIAG: (ssl_hook_test) Pre accept delay callback 0 `` - event is good
+``
diff --git a/tests/gold_tests/tls_hooks/gold/ts-preaccept1-sni1-cert1.gold b/tests/gold_tests/tls_hooks/gold/ts-preaccept1-sni1-cert1.gold
new file mode 100644
index 0000000..07ee131
--- /dev/null
+++ b/tests/gold_tests/tls_hooks/gold/ts-preaccept1-sni1-cert1.gold
@@ -0,0 +1,5 @@
+`` DIAG: (ssl_hook_test) Setup callbacks pa=1 sni=1 cert=1 cert_imm=0 pa_delay=0
+`` DIAG: (ssl_hook_test) Pre accept callback 0 `` - event is good
+`` DIAG: (ssl_hook_test) SNI callback 0 ``
+`` DIAG: (ssl_hook_test) Cert callback 0 ssl_vc=``
+`` DIAG: (ssl_hook_test) Callback reenable ssl_vc=``
diff --git a/tests/gold_tests/tls_hooks/gold/ts-sni-1.gold b/tests/gold_tests/tls_hooks/gold/ts-sni-1.gold
new file mode 100644
index 0000000..4b7d335
--- /dev/null
+++ b/tests/gold_tests/tls_hooks/gold/ts-sni-1.gold
@@ -0,0 +1,3 @@
+`` DIAG: (ssl_hook_test) Setup callbacks pa=0 sni=1 cert=0 cert_imm=0 pa_delay=0
+`` DIAG: (ssl_hook_test) SNI callback 0 ``
+``
diff --git a/tests/gold_tests/tls_hooks/gold/ts-sni-2.gold b/tests/gold_tests/tls_hooks/gold/ts-sni-2.gold
new file mode 100644
index 0000000..ecb2cb6
--- /dev/null
+++ b/tests/gold_tests/tls_hooks/gold/ts-sni-2.gold
@@ -0,0 +1,4 @@
+`` DIAG: (ssl_hook_test) Setup callbacks pa=0 sni=2 cert=0 cert_imm=0 pa_delay=0
+`` DIAG: (ssl_hook_test) SNI callback 0 ``
+`` DIAG: (ssl_hook_test) SNI callback 1 ``
+``
diff --git a/tests/gold_tests/tls_hooks/ssl/server.key b/tests/gold_tests/tls_hooks/ssl/server.key
new file mode 100644
index 0000000..4c7a661
--- /dev/null
+++ b/tests/gold_tests/tls_hooks/ssl/server.key
@@ -0,0 +1,15 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIICXQIBAAKBgQDWMHOiUF+ORmZjAxI8MWE9dblb7gQSJ36WCXlPFiFx6ynF+S1E
+kXAYpIip5X0pzDUaIbLukxJUAAnOtMEO0PCgxJQUrEtRWh8wiJdbdQJF0Zs/9R+u
+SUgb61f+mdTQvhqefBGx+xrpfAcgtcWiZuSA9Q3fvpDj5WOWSPWXBUuxywIDAQAB
+AoGBAJPxRX2gjFAGWmQbU/YVmXfNH6navh8X/nx9sLeqrpE0AFeJI/ZPiqDKzMal
+B43eSfNxwVi+ZxN0L1ICUbL9KKZvHs/QBxWLA1fGVAXrz7sRplEVvakPpTfHoEnv
+sKaMWVKaK/S5WGbDhElb6zb/Lwo19DsIAPjGYqFvzFJBmobJAkEA9iSeTGkR9X26
+GywZoYrIMlRh34htOIRx1UUq88rFzdrCF21kQ4lhBIkX5OZMMy652i2gyak4OZTe
+YewIv8jw9QJBAN7EQNHG8jPwXfVp91/fqxVQEfumuP2i6uiWWYQgZCmla2+0xcLZ
+pMQ6sQEe10hhTrVnzHgAUVp50Ntn2jwBX78CQF09veGAI9d1Cxzj9cmmAvRd1r2Q
+tp8kPOLnUsALXib+6WtqewLCdcf8DtsdClyRJMIraq85tRzK8fryKNZNzkkCQEgA
+yS7FDj5JgCU15hZgFk1iPx3HCt44jZM2HaL+UUHAzRQjKxTLAl3G1rWVAWLMyQML
+lORoveLvotl4HOruSsMCQQCAx9dV9JUSFoyc1CWILp/FgUH/se4cjQCThGO0DoQQ
+vGTYmntY7j9WRJ9esQrjdD6Clw8zM/45GIBNwnXzqo7Z
+-----END RSA PRIVATE KEY-----
diff --git a/tests/gold_tests/tls_hooks/ssl/server.pem b/tests/gold_tests/tls_hooks/ssl/server.pem
new file mode 100644
index 0000000..a1de94f
--- /dev/null
+++ b/tests/gold_tests/tls_hooks/ssl/server.pem
@@ -0,0 +1,32 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIICXQIBAAKBgQDWMHOiUF+ORmZjAxI8MWE9dblb7gQSJ36WCXlPFiFx6ynF+S1E
+kXAYpIip5X0pzDUaIbLukxJUAAnOtMEO0PCgxJQUrEtRWh8wiJdbdQJF0Zs/9R+u
+SUgb61f+mdTQvhqefBGx+xrpfAcgtcWiZuSA9Q3fvpDj5WOWSPWXBUuxywIDAQAB
+AoGBAJPxRX2gjFAGWmQbU/YVmXfNH6navh8X/nx9sLeqrpE0AFeJI/ZPiqDKzMal
+B43eSfNxwVi+ZxN0L1ICUbL9KKZvHs/QBxWLA1fGVAXrz7sRplEVvakPpTfHoEnv
+sKaMWVKaK/S5WGbDhElb6zb/Lwo19DsIAPjGYqFvzFJBmobJAkEA9iSeTGkR9X26
+GywZoYrIMlRh34htOIRx1UUq88rFzdrCF21kQ4lhBIkX5OZMMy652i2gyak4OZTe
+YewIv8jw9QJBAN7EQNHG8jPwXfVp91/fqxVQEfumuP2i6uiWWYQgZCmla2+0xcLZ
+pMQ6sQEe10hhTrVnzHgAUVp50Ntn2jwBX78CQF09veGAI9d1Cxzj9cmmAvRd1r2Q
+tp8kPOLnUsALXib+6WtqewLCdcf8DtsdClyRJMIraq85tRzK8fryKNZNzkkCQEgA
+yS7FDj5JgCU15hZgFk1iPx3HCt44jZM2HaL+UUHAzRQjKxTLAl3G1rWVAWLMyQML
+lORoveLvotl4HOruSsMCQQCAx9dV9JUSFoyc1CWILp/FgUH/se4cjQCThGO0DoQQ
+vGTYmntY7j9WRJ9esQrjdD6Clw8zM/45GIBNwnXzqo7Z
+-----END RSA PRIVATE KEY-----
+-----BEGIN CERTIFICATE-----
+MIICszCCAhwCCQCRJsJJ+mTsdDANBgkqhkiG9w0BAQsFADCBnTELMAkGA1UEBhMC
+VVMxCzAJBgNVBAgMAklMMRIwEAYDVQQHDAlDaGFtcGFpZ24xDjAMBgNVBAoMBVlh
+aG9vMQ0wCwYDVQQLDARFZGdlMSgwJgYDVQQDDB9qdWljZXByb2R1Y2UuY29ycC5u
+ZTEueWFob28uY29tMSQwIgYJKoZIhvcNAQkBFhVwZXJzaWEuYXppekB5YWhvby5j
+b20wHhcNMTYwODI1MjI1NzIxWhcNMTcwODI1MjI1NzIxWjCBnTELMAkGA1UEBhMC
+VVMxCzAJBgNVBAgMAklMMRIwEAYDVQQHDAlDaGFtcGFpZ24xDjAMBgNVBAoMBVlh
+aG9vMQ0wCwYDVQQLDARFZGdlMSgwJgYDVQQDDB9qdWljZXByb2R1Y2UuY29ycC5u
+ZTEueWFob28uY29tMSQwIgYJKoZIhvcNAQkBFhVwZXJzaWEuYXppekB5YWhvby5j
+b20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBANYwc6JQX45GZmMDEjwxYT11
+uVvuBBInfpYJeU8WIXHrKcX5LUSRcBikiKnlfSnMNRohsu6TElQACc60wQ7Q8KDE
+lBSsS1FaHzCIl1t1AkXRmz/1H65JSBvrV/6Z1NC+Gp58EbH7Gul8ByC1xaJm5ID1
+Dd++kOPlY5ZI9ZcFS7HLAgMBAAEwDQYJKoZIhvcNAQELBQADgYEAXSVfZ5p1TkhW
+QiYq9nfQlBnX2NVaf8ymA8edQR0qH/QBv4/52bNNXC7V/V+ev9LCho2iRMeYYyXB
+yo1wBAGR83lS9cF/tOABcYrxjdP54Sfkyh5fomcg8SV7zap6C8mhbV8r3EujbKCx
+igH3fMX5F/eRwNCzaMMyQsXaxTJ3trk=
+-----END CERTIFICATE-----
diff --git a/tests/gold_tests/tls_hooks/tls_hooks.test.py b/tests/gold_tests/tls_hooks/tls_hooks.test.py
new file mode 100644
index 0000000..d56a82c
--- /dev/null
+++ b/tests/gold_tests/tls_hooks/tls_hooks.test.py
@@ -0,0 +1,78 @@
+'''
+Test 1 preaccept callback (without delay)
+'''
+#  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 os
+import re
+
+Test.Summary = '''
+Test different combinations of TLS handshake hooks to ensure they are applied consistently.
+'''
+
+Test.SkipUnless(Condition.HasProgram("grep", "grep needs to be installed on system for this test to work"))
+
+ts = Test.MakeATSProcess("ts", select_ports=False)
+server = Test.MakeOriginServer("server")
+request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""}
+# desired response form the origin server
+response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""}
+server.addResponse("sessionlog.json", request_header, response_header)
+
+ts.addSSLfile("ssl/server.pem")
+ts.addSSLfile("ssl/server.key")
+
+ts.Variables.ssl_port = 4443
+ts.Disk.records_config.update({
+    'proxy.config.diags.debug.enabled': 1,
+    'proxy.config.diags.debug.tags': 'ssl_hook_test',
+    'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir),
+    'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir),
+    # enable ssl port
+    'proxy.config.http.server_ports': '{0}:ssl'.format(ts.Variables.ssl_port),
+    'proxy.config.ssl.client.verify.server':  0,
+    'proxy.config.ssl.server.cipher_suite': 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:AES128-GCM-SHA256:AES256-GCM-SHA384:ECDHE-RSA-RC4-SHA:ECDHE-RSA-AES128-SHA:ECDHE-RSA-AES256-SHA:RC4-SHA:RC4-MD5:AES128-SHA:AES256-SHA:DES-CBC3-SHA!SRP:!DSS:!PSK:!aNULL:!eNULL:!SSLv2',
+})
+
+ts.Disk.ssl_multicert_config.AddLine(
+    'dest_ip=* ssl_cert_name=server.pem ssl_key_name=server.key'
+)
+
+ts.Disk.remap_config.AddLine(
+    'map https://example.com:4443 http://127.0.0.1:{0}'.format(server.Variables.Port)
+)
+
+Test.prepare_plugin(os.path.join(Test.Variables.AtsTestToolsDir, 'plugins', 'ssl_hook_test.cc'), ts, '-preaccept=1')
+
+tr = Test.AddTestRun("Test one preaccept hook")
+tr.Processes.Default.StartBefore(server)
+tr.Processes.Default.StartBefore(Test.Processes.ts, ready=When.PortOpen(ts.Variables.ssl_port))
+tr.StillRunningAfter = ts
+tr.StillRunningAfter = server
+tr.Processes.Default.Command = 'curl -k -H \'host:example.com:{0}\' https://127.0.0.1:{0}'.format(ts.Variables.ssl_port)
+tr.Processes.Default.ReturnCode = 0
+tr.Processes.Default.Streams.stdout = "gold/preaccept-1.gold"
+
+ts.Streams.stderr = "gold/ts-preaccept-1.gold"
+
+# the preaccept may get triggered twice because the test framework creates a TCP connection before handing off to traffic_server
+preacceptstring = "Pre accept callback 0"
+ts.Streams.All = Testers.ContainsExpression(
+    "\A(?:(?!{0}).)*{0}.*({0})?(?!.*{0}).*\Z".format(preacceptstring), "Pre accept message appears only once or twice", reflags=re.S | re.M)
+
+tr.Processes.Default.TimeOut = 5
+tr.TimeOut = 5
diff --git a/tests/gold_tests/tls_hooks/tls_hooks10.test.py b/tests/gold_tests/tls_hooks/tls_hooks10.test.py
new file mode 100644
index 0000000..4ff930a
--- /dev/null
+++ b/tests/gold_tests/tls_hooks/tls_hooks10.test.py
@@ -0,0 +1,79 @@
+'''
+Test one delayed cert callback and two immediate cert callbacks
+'''
+#  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 os
+import re
+
+Test.Summary = '''
+Test different combinations of TLS handshake hooks to ensure they are applied consistently.
+'''
+
+Test.SkipUnless(Condition.HasProgram("grep", "grep needs to be installed on system for this test to work"))
+
+ts = Test.MakeATSProcess("ts", select_ports=False)
+server = Test.MakeOriginServer("server")
+request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""}
+# desired response form the origin server
+response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""}
+server.addResponse("sessionlog.json", request_header, response_header)
+
+ts.addSSLfile("ssl/server.pem")
+ts.addSSLfile("ssl/server.key")
+
+ts.Variables.ssl_port = 4443
+ts.Disk.records_config.update({
+    'proxy.config.diags.debug.enabled': 1,
+    'proxy.config.diags.debug.tags': 'ssl_hook_test',
+    'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir),
+    'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir),
+    # enable ssl port
+    'proxy.config.http.server_ports': '{0}:ssl'.format(ts.Variables.ssl_port),
+    'proxy.config.ssl.client.verify.server':  0,
+    'proxy.config.ssl.server.cipher_suite': 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:AES128-GCM-SHA256:AES256-GCM-SHA384:ECDHE-RSA-RC4-SHA:ECDHE-RSA-AES128-SHA:ECDHE-RSA-AES256-SHA:RC4-SHA:RC4-MD5:AES128-SHA:AES256-SHA:DES-CBC3-SHA!SRP:!DSS:!PSK:!aNULL:!eNULL:!SSLv2',
+})
+
+ts.Disk.ssl_multicert_config.AddLine(
+    'dest_ip=* ssl_cert_name=server.pem ssl_key_name=server.key'
+)
+
+ts.Disk.remap_config.AddLine(
+    'map https://example.com:4443 http://127.0.0.1:{0}'.format(server.Variables.Port)
+)
+
+Test.prepare_plugin(os.path.join(Test.Variables.AtsTestToolsDir, 'plugins', 'ssl_hook_test.cc'), ts, '-cert=1 -i=2')
+
+tr = Test.AddTestRun("Test a combination of delayed and immediate cert hooks")
+tr.Processes.Default.StartBefore(server)
+tr.Processes.Default.StartBefore(Test.Processes.ts, ready=When.PortOpen(ts.Variables.ssl_port))
+tr.StillRunningAfter = ts
+tr.StillRunningAfter = server
+tr.Processes.Default.Command = 'curl -k -H \'host:example.com:{0}\' https://127.0.0.1:{0}'.format(ts.Variables.ssl_port)
+tr.Processes.Default.ReturnCode = 0
+tr.Processes.Default.Streams.stdout = "gold/preaccept-1.gold"
+
+ts.Streams.stderr = "gold/ts-cert-1-im-2.gold"
+
+certstring0 = "Cert callback 0"
+certstring1 = "Cert callback 1"
+ts.Streams.All = Testers.ContainsExpression(
+    "\A(?:(?!{0}).)*{0}.*{0}(?!.*{0}).*\Z".format(certstring0), "Cert message appears twicd", reflags=re.S | re.M)
+ts.Streams.All = Testers.ContainsExpression(
+    "\A(?:(?!{0}).)*{0}(?!.*{0}).*\Z".format(certstring1), "Cert message appears only once", reflags=re.S | re.M)
+tr.Processes.Default.TimeOut = 5
+tr.TimeOut = 5
diff --git a/tests/gold_tests/tls_hooks/tls_hooks11.test.py b/tests/gold_tests/tls_hooks/tls_hooks11.test.py
new file mode 100644
index 0000000..fd23c66
--- /dev/null
+++ b/tests/gold_tests/tls_hooks/tls_hooks11.test.py
@@ -0,0 +1,76 @@
+'''
+Test one delayed preaccept callback
+'''
+#  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 os
+import re
+
+Test.Summary = '''
+Test different combinations of TLS handshake hooks to ensure they are applied consistently.
+'''
+
+Test.SkipUnless(Condition.HasProgram("grep", "grep needs to be installed on system for this test to work"))
+
+ts = Test.MakeATSProcess("ts", select_ports=False)
+server = Test.MakeOriginServer("server")
+request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""}
+# desired response form the origin server
+response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""}
+server.addResponse("sessionlog.json", request_header, response_header)
+
+ts.addSSLfile("ssl/server.pem")
+ts.addSSLfile("ssl/server.key")
+
+ts.Variables.ssl_port = 4443
+ts.Disk.records_config.update({
+    'proxy.config.diags.debug.enabled': 1,
+    'proxy.config.diags.debug.tags': 'ssl_hook_test',
+    'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir),
+    'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir),
+    # enable ssl port
+    'proxy.config.http.server_ports': '{0}:ssl'.format(ts.Variables.ssl_port),
+    'proxy.config.ssl.client.verify.server':  0,
+    'proxy.config.ssl.server.cipher_suite': 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:AES128-GCM-SHA256:AES256-GCM-SHA384:ECDHE-RSA-RC4-SHA:ECDHE-RSA-AES128-SHA:ECDHE-RSA-AES256-SHA:RC4-SHA:RC4-MD5:AES128-SHA:AES256-SHA:DES-CBC3-SHA!SRP:!DSS:!PSK:!aNULL:!eNULL:!SSLv2',
+})
+
+ts.Disk.ssl_multicert_config.AddLine(
+    'dest_ip=* ssl_cert_name=server.pem ssl_key_name=server.key'
+)
+
+ts.Disk.remap_config.AddLine(
+    'map https://example.com:4443 http://127.0.0.1:{0}'.format(server.Variables.Port)
+)
+
+Test.prepare_plugin(os.path.join(Test.Variables.AtsTestToolsDir, 'plugins', 'ssl_hook_test.cc'), ts, '-d=1')
+
+tr = Test.AddTestRun("Test one delayed preaccept hook")
+tr.Processes.Default.StartBefore(server)
+tr.Processes.Default.StartBefore(Test.Processes.ts, ready=When.PortOpen(ts.Variables.ssl_port))
+tr.StillRunningAfter = ts
+tr.StillRunningAfter = server
+tr.Processes.Default.Command = 'curl -k -H \'host:example.com:{0}\' https://127.0.0.1:{0}'.format(ts.Variables.ssl_port)
+tr.Processes.Default.ReturnCode = 0
+tr.Processes.Default.Streams.stdout = "gold/preaccept-1.gold"
+
+ts.Streams.stderr = "gold/ts-preaccept-delayed-1.gold"
+
+preacceptstring = "Pre accept delay callback 0"
+ts.Streams.All = Testers.ContainsExpression(
+    "\A(?:(?!{0}).)*{0}.*({0})?(?!.*{0}).*\Z".format(preacceptstring), "Pre accept message appears only once or twice", reflags=re.S | re.M)
+tr.Processes.Default.TimeOut = 5
+tr.TimeOut = 5
diff --git a/tests/gold_tests/tls_hooks/tls_hooks12.test.py b/tests/gold_tests/tls_hooks/tls_hooks12.test.py
new file mode 100644
index 0000000..571538b
--- /dev/null
+++ b/tests/gold_tests/tls_hooks/tls_hooks12.test.py
@@ -0,0 +1,87 @@
+'''
+Test a combination of delayed and immediate preaccept callbacks
+'''
+#  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 os
+import re
+
+Test.Summary = '''
+Test different combinations of TLS handshake hooks to ensure they are applied consistently.
+'''
+
+Test.SkipUnless(Condition.HasProgram("grep", "grep needs to be installed on system for this test to work"))
+
+ts = Test.MakeATSProcess("ts", select_ports=False)
+server = Test.MakeOriginServer("server")
+request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""}
+# desired response form the origin server
+response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""}
+server.addResponse("sessionlog.json", request_header, response_header)
+
+ts.addSSLfile("ssl/server.pem")
+ts.addSSLfile("ssl/server.key")
+
+ts.Variables.ssl_port = 4443
+ts.Disk.records_config.update({
+    'proxy.config.diags.debug.enabled': 1,
+    'proxy.config.diags.debug.tags': 'ssl_hook_test',
+    'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir),
+    'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir),
+    # enable ssl port
+    'proxy.config.http.server_ports': '{0}:ssl'.format(ts.Variables.ssl_port),
+    'proxy.config.ssl.client.verify.server':  0,
+    'proxy.config.ssl.server.cipher_suite': 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:AES128-GCM-SHA256:AES256-GCM-SHA384:ECDHE-RSA-RC4-SHA:ECDHE-RSA-AES128-SHA:ECDHE-RSA-AES256-SHA:RC4-SHA:RC4-MD5:AES128-SHA:AES256-SHA:DES-CBC3-SHA!SRP:!DSS:!PSK:!aNULL:!eNULL:!SSLv2',
+})
+
+ts.Disk.ssl_multicert_config.AddLine(
+    'dest_ip=* ssl_cert_name=server.pem ssl_key_name=server.key'
+)
+
+ts.Disk.remap_config.AddLine(
+    'map https://example.com:4443 http://127.0.0.1:{0}'.format(server.Variables.Port)
+)
+
+Test.prepare_plugin(os.path.join(Test.Variables.AtsTestToolsDir, 'plugins', 'ssl_hook_test.cc'), ts, '-p=2 -d=1')
+
+tr = Test.AddTestRun("Test combination of delayed and immediate preaccept hook2")
+tr.Processes.Default.StartBefore(server)
+tr.Processes.Default.StartBefore(Test.Processes.ts, ready=When.PortOpen(ts.Variables.ssl_port))
+tr.StillRunningAfter = ts
+tr.StillRunningAfter = server
+tr.Processes.Default.Command = 'curl -k -H \'host:example.com:{0}\' https://127.0.0.1:{0}'.format(ts.Variables.ssl_port)
+tr.Processes.Default.ReturnCode = 0
+tr.Processes.Default.Streams.stdout = "gold/preaccept-1.gold"
+
+ts.Streams.stderr = "gold/ts-preaccept-delayed-1-immdate-2.gold"
+
+# Not going to check for number of times the message appears.  With the current test framework
+# a probing TCP connection is made to test that the port is listening.  The entire preaccept hook
+# sequence may appear on that probe.  Or it may not.  If we move away from the probe connection
+# we can check for the right number of each message.
+#preacceptstring0 = "Pre accept delay callback 0"
+#ts.Streams.All = Testers.ContainsExpression(
+#    "\A(?:(?!{0}).)*{0}.*({0})?(?!.*{0}).*\Z".format(preacceptstring0), "Pre accept message appears only once or twice", reflags=re.S | re.M)
+#preacceptstring1 = "Pre accept callback 0"
+#ts.Streams.All = Testers.ContainsExpression(
+#    "\A(?:(?!{0}).)*{0}.*({0})?(?!.*{0}).*\Z".format(preacceptstring1), "Pre accept message appears only once or twice", reflags=re.S | re.M)
+#preacceptstring2 = "Pre accept callback 1"
+#ts.Streams.All = Testers.ContainsExpression(
+#    "\A(?:(?!{0}).)*{0}.*({0})?(?!.*{0}).*\Z".format(preacceptstring2), "Pre accept message appears only once or twice", reflags=re.S | re.M)
+
+tr.Processes.Default.TimeOut = 5
+tr.TimeOut = 5
diff --git a/tests/gold_tests/tls_hooks/tls_hooks2.test.py b/tests/gold_tests/tls_hooks/tls_hooks2.test.py
new file mode 100644
index 0000000..e6dbd50
--- /dev/null
+++ b/tests/gold_tests/tls_hooks/tls_hooks2.test.py
@@ -0,0 +1,77 @@
+'''
+Test single SNI hook
+'''
+#  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 os
+import re
+
+Test.Summary = '''
+Test different combinations of TLS handshake hooks to ensure they are applied consistently.
+'''
+
+Test.SkipUnless(Condition.HasProgram("grep", "grep needs to be installed on system for this test to work"))
+
+ts = Test.MakeATSProcess("ts", select_ports=False)
+server = Test.MakeOriginServer("server")
+request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""}
+# desired response form the origin server
+response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""}
+server.addResponse("sessionlog.json", request_header, response_header)
+
+ts.addSSLfile("ssl/server.pem")
+ts.addSSLfile("ssl/server.key")
+
+ts.Variables.ssl_port = 4443
+ts.Disk.records_config.update({
+    'proxy.config.diags.debug.enabled': 1,
+    'proxy.config.diags.debug.tags': 'ssl_hook_test',
+    'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir),
+    'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir),
+    # enable ssl port
+    'proxy.config.http.server_ports': '{0}:ssl'.format(ts.Variables.ssl_port),
+    'proxy.config.ssl.client.verify.server':  0,
+    'proxy.config.ssl.server.cipher_suite': 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:AES128-GCM-SHA256:AES256-GCM-SHA384:ECDHE-RSA-RC4-SHA:ECDHE-RSA-AES128-SHA:ECDHE-RSA-AES256-SHA:RC4-SHA:RC4-MD5:AES128-SHA:AES256-SHA:DES-CBC3-SHA!SRP:!DSS:!PSK:!aNULL:!eNULL:!SSLv2',
+})
+
+ts.Disk.ssl_multicert_config.AddLine(
+    'dest_ip=* ssl_cert_name=server.pem ssl_key_name=server.key'
+)
+
+ts.Disk.remap_config.AddLine(
+    'map https://example.com:4443 http://127.0.0.1:{0}'.format(server.Variables.Port)
+)
+
+Test.prepare_plugin(os.path.join(Test.Variables.AtsTestToolsDir, 'plugins', 'ssl_hook_test.cc'), ts, '-sni=1')
+
+tr = Test.AddTestRun("Test one sni hook")
+tr.Processes.Default.StartBefore(server)
+tr.Processes.Default.StartBefore(Test.Processes.ts, ready=When.PortOpen(ts.Variables.ssl_port))
+tr.StillRunningAfter = ts
+tr.StillRunningAfter = server
+tr.Processes.Default.Command = 'curl -k -H \'host:example.com:{0}\' https://127.0.0.1:{0}'.format(ts.Variables.ssl_port)
+tr.Processes.Default.ReturnCode = 0
+tr.Processes.Default.Streams.stdout = "gold/preaccept-1.gold"
+
+ts.Streams.stderr = "gold/ts-sni-1.gold"
+
+snistring = "SNI callback 0"
+ts.Streams.All = Testers.ContainsExpression(
+    "\A(?:(?!{0}).)*{0}(?!.*{0}).*\Z".format(snistring), "SNI message appears only once", reflags=re.S | re.M)
+
+tr.Processes.Default.TimeOut = 5
+tr.TimeOut = 5
diff --git a/tests/gold_tests/tls_hooks/tls_hooks3.test.py b/tests/gold_tests/tls_hooks/tls_hooks3.test.py
new file mode 100644
index 0000000..c6ce668
--- /dev/null
+++ b/tests/gold_tests/tls_hooks/tls_hooks3.test.py
@@ -0,0 +1,77 @@
+'''
+Test single delayed cert callback
+'''
+#  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 os
+import re
+
+Test.Summary = '''
+Test different combinations of TLS handshake hooks to ensure they are applied consistently.
+'''
+
+Test.SkipUnless(Condition.HasProgram("grep", "grep needs to be installed on system for this test to work"))
+
+ts = Test.MakeATSProcess("ts", select_ports=False)
+server = Test.MakeOriginServer("server")
+request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""}
+# desired response form the origin server
+response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""}
+server.addResponse("sessionlog.json", request_header, response_header)
+
+ts.addSSLfile("ssl/server.pem")
+ts.addSSLfile("ssl/server.key")
+
+ts.Variables.ssl_port = 4443
+ts.Disk.records_config.update({
+    'proxy.config.diags.debug.enabled': 1,
+    'proxy.config.diags.debug.tags': 'ssl_hook_test',
+    'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir),
+    'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir),
+    # enable ssl port
+    'proxy.config.http.server_ports': '{0}:ssl'.format(ts.Variables.ssl_port),
+    'proxy.config.ssl.client.verify.server':  0,
+    'proxy.config.ssl.server.cipher_suite': 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:AES128-GCM-SHA256:AES256-GCM-SHA384:ECDHE-RSA-RC4-SHA:ECDHE-RSA-AES128-SHA:ECDHE-RSA-AES256-SHA:RC4-SHA:RC4-MD5:AES128-SHA:AES256-SHA:DES-CBC3-SHA!SRP:!DSS:!PSK:!aNULL:!eNULL:!SSLv2',
+})
+
+ts.Disk.ssl_multicert_config.AddLine(
+    'dest_ip=* ssl_cert_name=server.pem ssl_key_name=server.key'
+)
+
+ts.Disk.remap_config.AddLine(
+    'map https://example.com:4443 http://127.0.0.1:{0}'.format(server.Variables.Port)
+)
+
+Test.prepare_plugin(os.path.join(Test.Variables.AtsTestToolsDir, 'plugins', 'ssl_hook_test.cc'), ts, '-cert=1')
+
+tr = Test.AddTestRun("Test one cert hook")
+tr.Processes.Default.StartBefore(server)
+tr.Processes.Default.StartBefore(Test.Processes.ts, ready=When.PortOpen(ts.Variables.ssl_port))
+tr.StillRunningAfter = ts
+tr.StillRunningAfter = server
+tr.Processes.Default.Command = 'curl -k -H \'host:example.com:{0}\' https://127.0.0.1:{0}'.format(ts.Variables.ssl_port)
+tr.Processes.Default.ReturnCode = 0
+tr.Processes.Default.Streams.stdout = "gold/preaccept-1.gold"
+
+ts.Streams.stderr = "gold/ts-cert-1.gold"
+
+certstring = "Cert callback 0"
+ts.Streams.All = Testers.ContainsExpression(
+    "\A(?:(?!{0}).)*{0}(?!.*{0}).*\Z".format(certstring), "Cert message appears only once", reflags=re.S | re.M)
+
+tr.Processes.Default.TimeOut = 5
+tr.TimeOut = 5
diff --git a/tests/gold_tests/tls_hooks/tls_hooks4.test.py b/tests/gold_tests/tls_hooks/tls_hooks4.test.py
new file mode 100644
index 0000000..8a9fb48
--- /dev/null
+++ b/tests/gold_tests/tls_hooks/tls_hooks4.test.py
@@ -0,0 +1,83 @@
+'''
+Test 1 preaccept, 1 sni, and 1 cert callback (with delay)
+'''
+#  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 os
+import re
+
+Test.Summary = '''
+Test different combinations of TLS handshake hooks to ensure they are applied consistently.
+'''
+
+Test.SkipUnless(Condition.HasProgram("grep", "grep needs to be installed on system for this test to work"))
+
+ts = Test.MakeATSProcess("ts", select_ports=False)
+server = Test.MakeOriginServer("server")
+request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""}
+# desired response form the origin server
+response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""}
+server.addResponse("sessionlog.json", request_header, response_header)
+
+ts.addSSLfile("ssl/server.pem")
+ts.addSSLfile("ssl/server.key")
+
+ts.Variables.ssl_port = 4443
+ts.Disk.records_config.update({
+    'proxy.config.diags.debug.enabled': 1,
+    'proxy.config.diags.debug.tags': 'ssl_hook_test',
+    'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir),
+    'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir),
+    # enable ssl port
+    'proxy.config.http.server_ports': '{0}:ssl'.format(ts.Variables.ssl_port),
+    'proxy.config.ssl.client.verify.server':  0,
+    'proxy.config.ssl.server.cipher_suite': 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:AES128-GCM-SHA256:AES256-GCM-SHA384:ECDHE-RSA-RC4-SHA:ECDHE-RSA-AES128-SHA:ECDHE-RSA-AES256-SHA:RC4-SHA:RC4-MD5:AES128-SHA:AES256-SHA:DES-CBC3-SHA!SRP:!DSS:!PSK:!aNULL:!eNULL:!SSLv2',
+})
+
+ts.Disk.ssl_multicert_config.AddLine(
+    'dest_ip=* ssl_cert_name=server.pem ssl_key_name=server.key'
+)
+
+ts.Disk.remap_config.AddLine(
+    'map https://example.com:4443 http://127.0.0.1:{0}'.format(server.Variables.Port)
+)
+
+Test.prepare_plugin(os.path.join(Test.Variables.AtsTestToolsDir, 'plugins', 'ssl_hook_test.cc'), ts, '-cert=1 -sni=1 -preaccept=1')
+
+tr = Test.AddTestRun("Test one sni, one preaccept, and one cert hook")
+tr.Processes.Default.StartBefore(server)
+tr.Processes.Default.StartBefore(Test.Processes.ts, ready=When.PortOpen(ts.Variables.ssl_port))
+tr.StillRunningAfter = ts
+tr.StillRunningAfter = server
+tr.Processes.Default.Command = 'curl -k -H \'host:example.com:{0}\' https://127.0.0.1:{0}'.format(ts.Variables.ssl_port)
+tr.Processes.Default.ReturnCode = 0
+tr.Processes.Default.Streams.stdout = "gold/preaccept-1.gold"
+
+ts.Streams.stderr = "gold/ts-preaccept1-sni1-cert1.gold"
+snistring = "SNI callback 0"
+preacceptstring = "Pre accept callback 0"
+certstring = "Cert callback 0"
+ts.Streams.All = Testers.ContainsExpression(
+    "\A(?:(?!{0}).)*{0}(?!.*{0}).*\Z".format(snistring), "SNI message appears only once", reflags=re.S | re.M)
+# the preaccept may get triggered twice because the test framework creates a TCP connection before handing off to traffic_server
+ts.Streams.All += Testers.ContainsExpression("\A(?:(?!{0}).)*{0}.*({0})?(?!.*{0}).*\Z".format(
+    preacceptstring), "Pre accept message appears only once or twice", reflags=re.S | re.M)
+ts.Streams.All += Testers.ContainsExpression("\A(?:(?!{0}).)*{0}(?!.*{0}).*\Z".format(certstring),
+                                             "Cert message appears only once", reflags=re.S | re.M)
+
+tr.Processes.Default.TimeOut = 5
+tr.TimeOut = 5
diff --git a/tests/gold_tests/tls_hooks/tls_hooks6.test.py b/tests/gold_tests/tls_hooks/tls_hooks6.test.py
new file mode 100644
index 0000000..bfc0800
--- /dev/null
+++ b/tests/gold_tests/tls_hooks/tls_hooks6.test.py
@@ -0,0 +1,81 @@
+'''
+Test 1 preaccept callback (without delay) two times
+'''
+#  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 os
+import re
+
+Test.Summary = '''
+Test different combinations of TLS handshake hooks to ensure they are applied consistently.
+'''
+
+Test.SkipUnless(Condition.HasProgram("grep", "grep needs to be installed on system for this test to work"))
+
+ts = Test.MakeATSProcess("ts", select_ports=False)
+server = Test.MakeOriginServer("server")
+request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""}
+# desired response form the origin server
+response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""}
+server.addResponse("sessionlog.json", request_header, response_header)
+
+ts.addSSLfile("ssl/server.pem")
+ts.addSSLfile("ssl/server.key")
+
+ts.Variables.ssl_port = 4443
+ts.Disk.records_config.update({
+    'proxy.config.diags.debug.enabled': 1,
+    'proxy.config.diags.debug.tags': 'ssl_hook_test',
+    'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir),
+    'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir),
+    # enable ssl port
+    'proxy.config.http.server_ports': '{0}:ssl'.format(ts.Variables.ssl_port),
+    'proxy.config.ssl.client.verify.server':  0,
+    'proxy.config.ssl.server.cipher_suite': 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:AES128-GCM-SHA256:AES256-GCM-SHA384:ECDHE-RSA-RC4-SHA:ECDHE-RSA-AES128-SHA:ECDHE-RSA-AES256-SHA:RC4-SHA:RC4-MD5:AES128-SHA:AES256-SHA:DES-CBC3-SHA!SRP:!DSS:!PSK:!aNULL:!eNULL:!SSLv2',
+})
+
+ts.Disk.ssl_multicert_config.AddLine(
+    'dest_ip=* ssl_cert_name=server.pem ssl_key_name=server.key'
+)
+
+ts.Disk.remap_config.AddLine(
+    'map https://example.com:4443 http://127.0.0.1:{0}'.format(server.Variables.Port)
+)
+
+Test.prepare_plugin(os.path.join(Test.Variables.AtsTestToolsDir, 'plugins', 'ssl_hook_test.cc'), ts, '-preaccept=2')
+
+tr = Test.AddTestRun("Test two preaccept hooks")
+tr.Processes.Default.StartBefore(server)
+tr.Processes.Default.StartBefore(Test.Processes.ts, ready=When.PortOpen(ts.Variables.ssl_port))
+tr.StillRunningAfter = ts
+tr.StillRunningAfter = server
+tr.Processes.Default.Command = 'curl -k -H \'host:example.com:{0}\' https://127.0.0.1:{0}'.format(ts.Variables.ssl_port)
+tr.Processes.Default.ReturnCode = 0
+tr.Processes.Default.Streams.stdout = "gold/preaccept-1.gold"
+
+ts.Streams.stderr = "gold/ts-preaccept-2.gold"
+
+# the preaccept may get triggered twice because the test framework creates a TCP connection before handing off to traffic_server
+preacceptstring0 = "Pre accept callback 0"
+preacceptstring1 = "Pre accept callback 1"
+ts.Streams.All = Testers.ContainsExpression(
+    "\A(?:(?!{0}).)*{0}.*({0})?(?!.*{0}).*\Z".format(preacceptstring0), "Pre accept message appears only once or twice", reflags=re.S | re.M)
+ts.Streams.All = Testers.ContainsExpression(
+    "\A(?:(?!{0}).)*{0}.*({0})?(?!.*{0}).*\Z".format(preacceptstring1), "Pre accept message appears only once or twice", reflags=re.S | re.M)
+
+tr.Processes.Default.TimeOut = 5
+tr.TimeOut = 5
diff --git a/tests/gold_tests/tls_hooks/tls_hooks7.test.py b/tests/gold_tests/tls_hooks/tls_hooks7.test.py
new file mode 100644
index 0000000..bf93223
--- /dev/null
+++ b/tests/gold_tests/tls_hooks/tls_hooks7.test.py
@@ -0,0 +1,80 @@
+'''
+Test two SNI hooks
+'''
+#  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 os
+import re
+
+Test.Summary = '''
+Test different combinations of TLS handshake hooks to ensure they are applied consistently.
+'''
+
+Test.SkipUnless(Condition.HasProgram("grep", "grep needs to be installed on system for this test to work"))
+
+ts = Test.MakeATSProcess("ts", select_ports=False)
+server = Test.MakeOriginServer("server")
+request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""}
+# desired response form the origin server
+response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""}
+server.addResponse("sessionlog.json", request_header, response_header)
+
+ts.addSSLfile("ssl/server.pem")
+ts.addSSLfile("ssl/server.key")
+
+ts.Variables.ssl_port = 4443
+ts.Disk.records_config.update({
+    'proxy.config.diags.debug.enabled': 1,
+    'proxy.config.diags.debug.tags': 'ssl_hook_test',
+    'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir),
+    'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir),
+    # enable ssl port
+    'proxy.config.http.server_ports': '{0}:ssl'.format(ts.Variables.ssl_port),
+    'proxy.config.ssl.client.verify.server':  0,
+    'proxy.config.ssl.server.cipher_suite': 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:AES128-GCM-SHA256:AES256-GCM-SHA384:ECDHE-RSA-RC4-SHA:ECDHE-RSA-AES128-SHA:ECDHE-RSA-AES256-SHA:RC4-SHA:RC4-MD5:AES128-SHA:AES256-SHA:DES-CBC3-SHA!SRP:!DSS:!PSK:!aNULL:!eNULL:!SSLv2',
+})
+
+ts.Disk.ssl_multicert_config.AddLine(
+    'dest_ip=* ssl_cert_name=server.pem ssl_key_name=server.key'
+)
+
+ts.Disk.remap_config.AddLine(
+    'map https://example.com:4443 http://127.0.0.1:{0}'.format(server.Variables.Port)
+)
+
+Test.prepare_plugin(os.path.join(Test.Variables.AtsTestToolsDir, 'plugins', 'ssl_hook_test.cc'), ts, '-sni=2')
+
+tr = Test.AddTestRun("Test two sni hooks")
+tr.Processes.Default.StartBefore(server)
+tr.Processes.Default.StartBefore(Test.Processes.ts, ready=When.PortOpen(ts.Variables.ssl_port))
+tr.StillRunningAfter = ts
+tr.StillRunningAfter = server
+tr.Processes.Default.Command = 'curl -k -H \'host:example.com:{0}\' https://127.0.0.1:{0}'.format(ts.Variables.ssl_port)
+tr.Processes.Default.ReturnCode = 0
+tr.Processes.Default.Streams.stdout = "gold/preaccept-1.gold"
+
+ts.Streams.stderr = "gold/ts-sni-2.gold"
+
+snistring0 = "SNI callback 0"
+snistring1 = "SNI callback 1"
+ts.Streams.All = Testers.ContainsExpression(
+    "\A(?:(?!{0}).)*{0}(?!.*{0}).*\Z".format(snistring0), "SNI message appears only once", reflags=re.S | re.M)
+ts.Streams.All = Testers.ContainsExpression(
+    "\A(?:(?!{0}).)*{0}(?!.*{0}).*\Z".format(snistring1), "SNI message appears only once", reflags=re.S | re.M)
+
+tr.Processes.Default.TimeOut = 5
+tr.TimeOut = 5
diff --git a/tests/gold_tests/tls_hooks/tls_hooks8.test.py b/tests/gold_tests/tls_hooks/tls_hooks8.test.py
new file mode 100644
index 0000000..1e201e7
--- /dev/null
+++ b/tests/gold_tests/tls_hooks/tls_hooks8.test.py
@@ -0,0 +1,80 @@
+'''
+Test two delayed cert callbacks
+'''
+#  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 os
+import re
+
+Test.Summary = '''
+Test different combinations of TLS handshake hooks to ensure they are applied consistently.
+'''
+
+Test.SkipUnless(Condition.HasProgram("grep", "grep needs to be installed on system for this test to work"))
+
+ts = Test.MakeATSProcess("ts", select_ports=False)
+server = Test.MakeOriginServer("server")
+request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""}
+# desired response form the origin server
+response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""}
+server.addResponse("sessionlog.json", request_header, response_header)
+
+ts.addSSLfile("ssl/server.pem")
+ts.addSSLfile("ssl/server.key")
+
+ts.Variables.ssl_port = 4443
+ts.Disk.records_config.update({
+    'proxy.config.diags.debug.enabled': 1,
+    'proxy.config.diags.debug.tags': 'ssl_hook_test',
+    'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir),
+    'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir),
+    # enable ssl port
+    'proxy.config.http.server_ports': '{0}:ssl'.format(ts.Variables.ssl_port),
+    'proxy.config.ssl.client.verify.server':  0,
+    'proxy.config.ssl.server.cipher_suite': 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:AES128-GCM-SHA256:AES256-GCM-SHA384:ECDHE-RSA-RC4-SHA:ECDHE-RSA-AES128-SHA:ECDHE-RSA-AES256-SHA:RC4-SHA:RC4-MD5:AES128-SHA:AES256-SHA:DES-CBC3-SHA!SRP:!DSS:!PSK:!aNULL:!eNULL:!SSLv2',
+})
+
+ts.Disk.ssl_multicert_config.AddLine(
+    'dest_ip=* ssl_cert_name=server.pem ssl_key_name=server.key'
+)
+
+ts.Disk.remap_config.AddLine(
+    'map https://example.com:4443 http://127.0.0.1:{0}'.format(server.Variables.Port)
+)
+
+Test.prepare_plugin(os.path.join(Test.Variables.AtsTestToolsDir, 'plugins', 'ssl_hook_test.cc'), ts, '-cert=2')
+
+tr = Test.AddTestRun("Test two cert hooks")
+tr.Processes.Default.StartBefore(server)
+tr.Processes.Default.StartBefore(Test.Processes.ts, ready=When.PortOpen(ts.Variables.ssl_port))
+tr.StillRunningAfter = ts
+tr.StillRunningAfter = server
+tr.Processes.Default.Command = 'curl -k -H \'host:example.com:{0}\' https://127.0.0.1:{0}'.format(ts.Variables.ssl_port)
+tr.Processes.Default.ReturnCode = 0
+tr.Processes.Default.Streams.stdout = "gold/preaccept-1.gold"
+
+ts.Streams.stderr = "gold/ts-cert-2.gold"
+
+certstring0 = "Cert callback 0"
+certstring1 = "Cert callback 1"
+ts.Streams.All = Testers.ContainsExpression(
+    "\A(?:(?!{0}).)*{0}(?!.*{0}).*\Z".format(certstring0), "Cert message appears only once", reflags=re.S | re.M)
+ts.Streams.All = Testers.ContainsExpression(
+    "\A(?:(?!{0}).)*{0}(?!.*{0}).*\Z".format(certstring1), "Cert message appears only once", reflags=re.S | re.M)
+
+tr.Processes.Default.TimeOut = 5
+tr.TimeOut = 5
diff --git a/tests/gold_tests/tls_hooks/tls_hooks9.test.py b/tests/gold_tests/tls_hooks/tls_hooks9.test.py
new file mode 100644
index 0000000..a975f0f
--- /dev/null
+++ b/tests/gold_tests/tls_hooks/tls_hooks9.test.py
@@ -0,0 +1,76 @@
+'''
+Test one immediate cert callback
+'''
+#  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 os
+import re
+
+Test.Summary = '''
+Test different combinations of TLS handshake hooks to ensure they are applied consistently.
+'''
+
+Test.SkipUnless(Condition.HasProgram("grep", "grep needs to be installed on system for this test to work"))
+
+ts = Test.MakeATSProcess("ts", select_ports=False)
+server = Test.MakeOriginServer("server")
+request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""}
+# desired response form the origin server
+response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""}
+server.addResponse("sessionlog.json", request_header, response_header)
+
+ts.addSSLfile("ssl/server.pem")
+ts.addSSLfile("ssl/server.key")
+
+ts.Variables.ssl_port = 4443
+ts.Disk.records_config.update({
+    'proxy.config.diags.debug.enabled': 1,
+    'proxy.config.diags.debug.tags': 'ssl_hook_test',
+    'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir),
+    'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir),
+    # enable ssl port
+    'proxy.config.http.server_ports': '{0}:ssl'.format(ts.Variables.ssl_port),
+    'proxy.config.ssl.client.verify.server':  0,
+    'proxy.config.ssl.server.cipher_suite': 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:AES128-GCM-SHA256:AES256-GCM-SHA384:ECDHE-RSA-RC4-SHA:ECDHE-RSA-AES128-SHA:ECDHE-RSA-AES256-SHA:RC4-SHA:RC4-MD5:AES128-SHA:AES256-SHA:DES-CBC3-SHA!SRP:!DSS:!PSK:!aNULL:!eNULL:!SSLv2',
+})
+
+ts.Disk.ssl_multicert_config.AddLine(
+    'dest_ip=* ssl_cert_name=server.pem ssl_key_name=server.key'
+)
+
+ts.Disk.remap_config.AddLine(
+    'map https://example.com:4443 http://127.0.0.1:{0}'.format(server.Variables.Port)
+)
+
+Test.prepare_plugin(os.path.join(Test.Variables.AtsTestToolsDir, 'plugins', 'ssl_hook_test.cc'), ts, '-i=1')
+
+tr = Test.AddTestRun("Test one immediate cert hooks")
+tr.Processes.Default.StartBefore(server)
+tr.Processes.Default.StartBefore(Test.Processes.ts, ready=When.PortOpen(ts.Variables.ssl_port))
+tr.StillRunningAfter = ts
+tr.StillRunningAfter = server
+tr.Processes.Default.Command = 'curl -k -H \'host:example.com:{0}\' https://127.0.0.1:{0}'.format(ts.Variables.ssl_port)
+tr.Processes.Default.ReturnCode = 0
+tr.Processes.Default.Streams.stdout = "gold/preaccept-1.gold"
+
+ts.Streams.stderr = "gold/ts-cert-im-1.gold"
+
+certstring0 = "Cert callback 0"
+ts.Streams.All = Testers.ContainsExpression(
+    "\A(?:(?!{0}).)*{0}(?!.*{0}).*\Z".format(certstring0), "Cert message appears only once", reflags=re.S | re.M)
+tr.Processes.Default.TimeOut = 5
+tr.TimeOut = 5
diff --git a/tests/tools/plugins/ssl_hook_test.cc b/tests/tools/plugins/ssl_hook_test.cc
new file mode 100644
index 0000000..d0075b4
--- /dev/null
+++ b/tests/tools/plugins/ssl_hook_test.cc
@@ -0,0 +1,248 @@
+/** @file
+
+  SSL Preaccept test plugin
+  Implements blind tunneling based on the client IP address
+  The client ip addresses are specified in the plugin's
+  config file as an array of IP addresses or IP address ranges under the
+  key "client-blind-tunnel"
+
+  @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 <ts/ts.h>
+#include <ts/remap.h>
+#include <getopt.h>
+#include <openssl/ssl.h>
+#include <string.h>
+
+#define PN "ssl_hook_test"
+#define PCP "[" PN " Plugin] "
+
+int
+ReenableSSL(TSCont cont, TSEvent event, void *edata)
+{
+  TSVConn ssl_vc = reinterpret_cast<TSVConn>(TSContDataGet(cont));
+  TSVConnReenable(ssl_vc);
+  TSContDestroy(cont);
+  TSDebug(PN, "Callback reenable ssl_vc=%p", ssl_vc);
+  return TS_SUCCESS;
+}
+
+int
+CB_Pre_Accept(TSCont cont, TSEvent event, void *edata)
+{
+  TSVConn ssl_vc = reinterpret_cast<TSVConn>(edata);
+
+  int count = reinterpret_cast<intptr_t>(TSContDataGet(cont));
+
+  TSDebug(PN, "Pre accept callback %d %p - event is %s", count, ssl_vc, event == TS_EVENT_VCONN_PRE_ACCEPT ? "good" : "bad");
+
+  // All done, reactivate things
+  TSVConnReenable(ssl_vc);
+  return TS_SUCCESS;
+}
+
+int
+CB_Pre_Accept_Delay(TSCont cont, TSEvent event, void *edata)
+{
+  TSVConn ssl_vc = reinterpret_cast<TSVConn>(edata);
+
+  int count = reinterpret_cast<intptr_t>(TSContDataGet(cont));
+
+  TSDebug(PN, "Pre accept delay callback %d %p - event is %s", count, ssl_vc, event == TS_EVENT_VCONN_PRE_ACCEPT ? "good" : "bad");
+
+  TSCont cb = TSContCreate(&ReenableSSL, TSMutexCreate());
+
+  TSContDataSet(cb, ssl_vc);
+
+  // Schedule to reenable in a bit
+  TSContSchedule(cb, 2000, TS_THREAD_POOL_NET);
+
+  return TS_SUCCESS;
+}
+
+int
+CB_SNI(TSCont cont, TSEvent event, void *edata)
+{
+  TSVConn ssl_vc = reinterpret_cast<TSVConn>(edata);
+
+  int count = reinterpret_cast<intptr_t>(TSContDataGet(cont));
+
+  TSDebug(PN, "SNI callback %d %p", count, ssl_vc);
+
+  // All done, reactivate things
+  TSVConnReenable(ssl_vc);
+  return TS_SUCCESS;
+}
+
+int
+CB_Cert_Immediate(TSCont cont, TSEvent event, void *edata)
+{
+  TSVConn ssl_vc = reinterpret_cast<TSVConn>(edata);
+
+  int count = reinterpret_cast<intptr_t>(TSContDataGet(cont));
+
+  TSDebug(PN, "Cert callback %d ssl_vc=%p", count, ssl_vc);
+
+  TSVConnReenable(ssl_vc);
+  return TS_SUCCESS;
+}
+
+int
+CB_Cert(TSCont cont, TSEvent event, void *edata)
+{
+  TSVConn ssl_vc = reinterpret_cast<TSVConn>(edata);
+
+  int count = reinterpret_cast<intptr_t>(TSContDataGet(cont));
+
+  TSDebug(PN, "Cert callback %d ssl_vc=%p", count, ssl_vc);
+
+  TSCont cb = TSContCreate(&ReenableSSL, TSMutexCreate());
+
+  TSContDataSet(cb, ssl_vc);
+
+  // Schedule to reenable in a bit
+  TSContSchedule(cb, 2000, TS_THREAD_POOL_NET);
+
+  return TS_SUCCESS;
+}
+
+void
+parse_callbacks(int argc, const char *argv[], int &preaccept_count, int &sni_count, int &cert_count, int &cert_count_immediate,
+                int &preaccept_count_delay)
+{
+  int i = 0;
+  const char *ptr;
+  for (i = 0; i < argc; i++) {
+    if (argv[i][0] == '-') {
+      switch (argv[i][1]) {
+      case 'p':
+        ptr = index(argv[i], '=');
+        if (ptr) {
+          preaccept_count = atoi(ptr + 1);
+        }
+        break;
+      case 's':
+        ptr = index(argv[i], '=');
+        if (ptr) {
+          sni_count = atoi(ptr + 1);
+        }
+        break;
+      case 'c':
+        ptr = index(argv[i], '=');
+        if (ptr) {
+          cert_count = atoi(ptr + 1);
+        }
+        break;
+      case 'd':
+        ptr = index(argv[i], '=');
+        if (ptr) {
+          preaccept_count_delay = atoi(ptr + 1);
+        }
+        break;
+      case 'i':
+        ptr = index(argv[i], '=');
+        if (ptr) {
+          cert_count_immediate = atoi(ptr + 1);
+        }
+        break;
+      }
+    }
+  }
+}
+
+void
+setup_callbacks(TSHttpTxn txn, int preaccept_count, int sni_count, int cert_count, int cert_count_immediate,
+                int preaccept_count_delay)
+{
+  TSCont cb = nullptr; // pre-accept callback continuation
+  int i;
+
+  TSDebug(PN, "Setup callbacks pa=%d sni=%d cert=%d cert_imm=%d pa_delay=%d", preaccept_count, sni_count, cert_count,
+          cert_count_immediate, preaccept_count_delay);
+  for (i = 0; i < preaccept_count; i++) {
+    cb = TSContCreate(&CB_Pre_Accept, TSMutexCreate());
+    TSContDataSet(cb, (void *)(intptr_t)i);
+    if (txn) {
+      TSHttpTxnHookAdd(txn, TS_VCONN_PRE_ACCEPT_HOOK, cb);
+    } else {
+      TSHttpHookAdd(TS_VCONN_PRE_ACCEPT_HOOK, cb);
+    }
+  }
+  for (i = 0; i < preaccept_count_delay; i++) {
+    cb = TSContCreate(&CB_Pre_Accept_Delay, TSMutexCreate());
+    TSContDataSet(cb, (void *)(intptr_t)i);
+    if (txn) {
+      TSHttpTxnHookAdd(txn, TS_VCONN_PRE_ACCEPT_HOOK, cb);
+    } else {
+      TSHttpHookAdd(TS_VCONN_PRE_ACCEPT_HOOK, cb);
+    }
+  }
+  for (i = 0; i < sni_count; i++) {
+    cb = TSContCreate(&CB_SNI, TSMutexCreate());
+    TSContDataSet(cb, (void *)(intptr_t)i);
+    if (txn) {
+      TSHttpTxnHookAdd(txn, TS_SSL_SERVERNAME_HOOK, cb);
+    } else {
+      TSHttpHookAdd(TS_SSL_SERVERNAME_HOOK, cb);
+    }
+  }
+  for (i = 0; i < cert_count; i++) {
+    cb = TSContCreate(&CB_Cert, TSMutexCreate());
+    TSContDataSet(cb, (void *)(intptr_t)i);
+    if (txn) {
+      TSHttpTxnHookAdd(txn, TS_SSL_CERT_HOOK, cb);
+    } else {
+      TSHttpHookAdd(TS_SSL_CERT_HOOK, cb);
+    }
+  }
+  for (i = 0; i < cert_count_immediate; i++) {
+    cb = TSContCreate(&CB_Cert_Immediate, TSMutexCreate());
+    TSContDataSet(cb, (void *)(intptr_t)i);
+    if (txn) {
+      TSHttpTxnHookAdd(txn, TS_SSL_CERT_HOOK, cb);
+    } else {
+      TSHttpHookAdd(TS_SSL_CERT_HOOK, cb);
+    }
+  }
+
+  return;
+}
+
+// Called by ATS as our initialization point
+void
+TSPluginInit(int argc, const char *argv[])
+{
+  TSPluginRegistrationInfo info;
+  info.plugin_name   = const_cast<char *>("SSL hooks test");
+  info.vendor_name   = const_cast<char *>("yahoo");
+  info.support_email = const_cast<char *>("shinrich@yahoo-inc.com");
+  if (TSPluginRegister(&info) != TS_SUCCESS) {
+    TSError("[%s] Plugin registration failed", PN);
+  }
+
+  int preaccept_count       = 0;
+  int sni_count             = 0;
+  int cert_count            = 0;
+  int cert_count_immediate  = 0;
+  int preaccept_count_delay = 0;
+  parse_callbacks(argc, argv, preaccept_count, sni_count, cert_count, cert_count_immediate, preaccept_count_delay);
+  setup_callbacks(nullptr, preaccept_count, sni_count, cert_count, cert_count_immediate, preaccept_count_delay);
+  return;
+}
diff --git a/tools/tsxs.in b/tools/tsxs.in
index aa7c0eb..704300a 100755
--- a/tools/tsxs.in
+++ b/tools/tsxs.in
@@ -29,6 +29,7 @@ CFLAGS="$CFLAGS @CFLAGS@"
 CXXFLAGS="$CXXFLAGS @CXXFLAGS@"
 BUILD=
 DEBUGECHO=
+LDFLAGS="$LDFLAGS @LDFLAGS@"
 
 if [ -z "$CC" ]; then
 	CC="@CC@"

-- 
To stop receiving notification emails like this one, please contact
['"commits@trafficserver.apache.org" <co...@trafficserver.apache.org>'].