Add new log field to output server name sent by client in TLS handshake.
diff --git a/doc/admin-guide/logging/formatting.en.rst b/doc/admin-guide/logging/formatting.en.rst
index fd6a396..182199c 100644
--- a/doc/admin-guide/logging/formatting.en.rst
+++ b/doc/admin-guide/logging/formatting.en.rst
@@ -579,6 +579,7 @@
 SSL / Encryption
 ~~~~~~~~~~~~~~~~
 
+.. _cssn:
 .. _cqssl:
 .. _cqssr:
 .. _cqssv:
@@ -592,6 +593,10 @@
 ===== ============== ==========================================================
 Field Source         Description
 ===== ============== ==========================================================
+cssn  Client TLS     SNI server name in client Hello message in TLS handshake.
+      Hello          If no server name present in Hello, or the transaction
+                     was not over TLS (over TCP), this field will contain
+                     ``-``.
 cqssl Client Request SSL client request status indicates if this client
                      connection is over SSL.
 cqssr Client Request SSL session ticket reused status; indicates if the current
diff --git a/iocore/net/P_SSLNetVConnection.h b/iocore/net/P_SSLNetVConnection.h
index 60ce374..83ff6d6 100644
--- a/iocore/net/P_SSLNetVConnection.h
+++ b/iocore/net/P_SSLNetVConnection.h
@@ -36,6 +36,8 @@
 #include "tscore/ink_platform.h"
 #include "ts/apidefs.h"
 #include <string_view>
+#include <cstring>
+#include <memory>
 
 #include <openssl/ssl.h>
 #include <openssl/err.h>
@@ -480,6 +482,7 @@
   bool tunnel_decrypt         = false;
   X509_STORE_CTX *verify_cert = nullptr;
 
+  // Null-terminated string, or nullptr if there is no SNI server name.
   std::unique_ptr<char[]> _serverName;
 };
 
diff --git a/proxy/Makefile.am b/proxy/Makefile.am
index bac2ea4..33a2ad1 100644
--- a/proxy/Makefile.am
+++ b/proxy/Makefile.am
@@ -18,6 +18,8 @@
 
 include $(top_srcdir)/build/tidy.mk
 
+include private/Makefile.inc
+
 SUBDIRS = hdrs shared http http2 logging
 if ENABLE_QUIC
 SUBDIRS += http3
@@ -44,6 +46,7 @@
 	Show.h
 
 libproxy_a_SOURCES = \
+	$(PRIVATE_SOURCES_) \
 	CacheControl.cc \
 	CacheControl.h \
 	ControlBase.cc \
diff --git a/proxy/ProxySession.cc b/proxy/ProxySession.cc
index d3b7eb4..0298866 100644
--- a/proxy/ProxySession.cc
+++ b/proxy/ProxySession.cc
@@ -24,6 +24,7 @@
 #include "HttpConfig.h"
 #include "HttpDebugNames.h"
 #include "ProxySession.h"
+#include "P_SSLNetVConnection.h"
 
 ProxySession::ProxySession() : VConnection(nullptr)
 {
@@ -79,6 +80,7 @@
   this->api_hooks.clear();
   this->mutex.clear();
   this->acl.clear();
+  this->_ssl.reset();
 }
 
 int
@@ -244,9 +246,20 @@
   NetVConnection *netvc = get_netvc();
   return netvc ? netvc->get_remote_addr() : nullptr;
 }
+
 sockaddr const *
 ProxySession::get_local_addr()
 {
   NetVConnection *netvc = get_netvc();
   return netvc ? netvc->get_local_addr() : nullptr;
 }
+
+void
+ProxySession::_handle_if_ssl(NetVConnection *new_vc)
+{
+  auto ssl_vc = dynamic_cast<SSLNetVConnection *>(new_vc);
+  if (ssl_vc) {
+    _ssl = std::make_unique<SSLProxySession>();
+    _ssl.get()->init(*ssl_vc);
+  }
+}
diff --git a/proxy/ProxySession.h b/proxy/ProxySession.h
index 478c592..e70b683 100644
--- a/proxy/ProxySession.h
+++ b/proxy/ProxySession.h
@@ -27,11 +27,13 @@
 #include "tscore/ink_resolver.h"
 #include "tscore/TSSystemState.h"
 #include <string_view>
+#include <memory>
 #include "P_Net.h"
 #include "InkAPIInternal.h"
 #include "http/Http1ServerSession.h"
 #include "http/HttpSessionAccept.h"
 #include "IPAllow.h"
+#include "private/SSLProxySession.h"
 
 // Emit a debug message conditional on whether this particular client session
 // has debugging enabled. This should only be called from within a client session
@@ -143,6 +145,10 @@
 
   APIHook *hook_get(TSHttpHookID id) const;
   HttpAPIHooks const *feature_hooks() const;
+
+  // Returns null pointer if session does not use a TLS connection.
+  SSLProxySession const *ssl() const;
+
   ////////////////////
   // Members
 
@@ -167,6 +173,10 @@
   int64_t con_id        = 0;
   Event *schedule_event = nullptr;
 
+  // This function should be called in all overrides of new_connection() where
+  // the new_vc may be an SSLNetVConnection object.
+  void _handle_if_ssl(NetVConnection *new_vc);
+
 private:
   void handle_api_return(int event);
   int state_api_callout(int event, void *edata);
@@ -180,6 +190,8 @@
   // be active until the transaction goes through or the client
   // aborts.
   bool m_active = false;
+
+  std::unique_ptr<SSLProxySession> _ssl;
 };
 
 ///////////////////
@@ -267,3 +279,9 @@
 {
   return this->api_hooks.has_hooks() || http_global_hooks->has_hooks();
 }
+
+inline SSLProxySession const *
+ProxySession::ssl() const
+{
+  return _ssl.get();
+}
diff --git a/proxy/http/Http1ClientSession.cc b/proxy/http/Http1ClientSession.cc
index 38705df..1196b3d 100644
--- a/proxy/http/Http1ClientSession.cc
+++ b/proxy/http/Http1ClientSession.cc
@@ -195,6 +195,8 @@
   _reader     = reader ? reader : read_buffer->alloc_reader();
   trans.set_reader(_reader);
 
+  _handle_if_ssl(new_vc);
+
   // INKqa11186: Use a local pointer to the mutex as
   // when we return from do_api_callout, the ClientSession may
   // have already been deallocated.
diff --git a/proxy/http2/Http2ClientSession.cc b/proxy/http2/Http2ClientSession.cc
index d703925..e2fdcdb 100644
--- a/proxy/http2/Http2ClientSession.cc
+++ b/proxy/http2/Http2ClientSession.cc
@@ -216,6 +216,8 @@
   this->write_buffer = new_MIOBuffer(HTTP2_HEADER_BUFFER_SIZE_INDEX);
   this->sm_writer    = this->write_buffer->alloc_reader();
 
+  this->_handle_if_ssl(new_vc);
+
   do_api_callout(TS_HTTP_SSN_START_HOOK);
 }
 
diff --git a/proxy/logging/Log.cc b/proxy/logging/Log.cc
index 3584cb0..9f39ea9 100644
--- a/proxy/logging/Log.cc
+++ b/proxy/logging/Log.cc
@@ -462,6 +462,11 @@
   global_field_list.add(field, false);
   field_symbol_hash.emplace("cluc", field);
 
+  field = new LogField("client_sni_server_name", "cssn", LogField::STRING, &LogAccess::marshal_client_sni_server_name,
+                       reinterpret_cast<LogField::UnmarshalFunc>(&LogAccess::unmarshal_str));
+  global_field_list.add(field, false);
+  field_symbol_hash.emplace("cssn", field);
+
   field = new LogField("process_uuid", "puuid", LogField::STRING, &LogAccess::marshal_process_uuid,
                        reinterpret_cast<LogField::UnmarshalFunc>(&LogAccess::unmarshal_str));
   global_field_list.add(field, false);
diff --git a/proxy/logging/LogAccess.cc b/proxy/logging/LogAccess.cc
index 22c964a..6e3aac4 100644
--- a/proxy/logging/LogAccess.cc
+++ b/proxy/logging/LogAccess.cc
@@ -1266,6 +1266,38 @@
 
 /*-------------------------------------------------------------------------
   -------------------------------------------------------------------------*/
+int
+LogAccess::marshal_client_sni_server_name(char *buf)
+{
+  // NOTE:  For this string_view, data() must always be nul-terminated, but the nul character must not be included in
+  // the length.
+  //
+  std::string_view server_name = "";
+
+  if (m_http_sm) {
+    auto txn = m_http_sm->get_ua_txn();
+    if (txn) {
+      auto ssn = txn->get_proxy_ssn();
+      if (ssn) {
+        auto ssl = ssn->ssl();
+        if (ssl) {
+          auto server_name_str = ssl->client_sni_server_name();
+          if (server_name_str) {
+            server_name = server_name_str;
+          }
+        }
+      }
+    }
+  }
+  int len = round_strlen(server_name.length() + 1);
+  if (buf) {
+    marshal_str(buf, server_name.data(), len);
+  }
+  return len;
+}
+
+/*-------------------------------------------------------------------------
+  -------------------------------------------------------------------------*/
 
 int
 LogAccess::marshal_client_host_port(char *buf)
diff --git a/proxy/logging/LogAccess.h b/proxy/logging/LogAccess.h
index 0cfb333..fa83bc8 100644
--- a/proxy/logging/LogAccess.h
+++ b/proxy/logging/LogAccess.h
@@ -250,6 +250,7 @@
   inkcoreapi int marshal_client_http_transaction_priority_weight(char *);     // INT
   inkcoreapi int marshal_client_http_transaction_priority_dependence(char *); // INT
   inkcoreapi int marshal_cache_lookup_url_canon(char *);                      // STR
+  inkcoreapi int marshal_client_sni_server_name(char *);                      // STR
 
   // named fields from within a http header
   //
diff --git a/proxy/private/Makefile.inc b/proxy/private/Makefile.inc
new file mode 100644
index 0000000..1a4a127
--- /dev/null
+++ b/proxy/private/Makefile.inc
@@ -0,0 +1,22 @@
+#  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.
+
+# Header files in this directory should only be included by files in the
+# parent directory.
+
+PRIVATE_SOURCES_ = \
+	private/SSLProxySession.cc \
+	private/SSLProxySession.h
diff --git a/proxy/private/SSLProxySession.cc b/proxy/private/SSLProxySession.cc
new file mode 100644
index 0000000..544d0af
--- /dev/null
+++ b/proxy/private/SSLProxySession.cc
@@ -0,0 +1,39 @@
+/** @file
+
+  Implementation file for SSLProxySession class.
+
+  @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 <cstring>
+
+#include "SSLProxySession.h"
+#include "P_Net.h"
+
+void
+SSLProxySession::init(SSLNetVConnection const &new_vc)
+{
+  char const *name = new_vc.get_server_name();
+  int length       = std::strlen(name) + 1;
+  if (length > 1) {
+    char *n = new char[length];
+    std::memcpy(n, name, length);
+    _client_sni_server_name.reset(n);
+  }
+}
diff --git a/proxy/private/SSLProxySession.h b/proxy/private/SSLProxySession.h
new file mode 100644
index 0000000..37e572f
--- /dev/null
+++ b/proxy/private/SSLProxySession.h
@@ -0,0 +1,46 @@
+/** @file
+
+  Header file for SSLProxySession class.
+
+  @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.
+ */
+
+#pragma once
+
+#include <memory>
+#include <string_view>
+
+class SSLNetVConnection;
+
+class SSLProxySession
+{
+public:
+  // Returns null pointer if no SNI server name, otherwise pointer to null-terminated string.
+  //
+  char const *
+  client_sni_server_name() const
+  {
+    return _client_sni_server_name.get();
+  }
+
+  void init(SSLNetVConnection const &new_vc);
+
+private:
+  std::unique_ptr<char[]> _client_sni_server_name;
+};
diff --git a/tests/gold_tests/logging/ccid_ctid.test.py b/tests/gold_tests/logging/new_log_flds.test.py
similarity index 77%
rename from tests/gold_tests/logging/ccid_ctid.test.py
rename to tests/gold_tests/logging/new_log_flds.test.py
index 34f9d75..fb787a5 100644
--- a/tests/gold_tests/logging/ccid_ctid.test.py
+++ b/tests/gold_tests/logging/new_log_flds.test.py
@@ -19,7 +19,7 @@
 import os
 
 Test.Summary = '''
-Test new ccid and ctid log fields
+Test new log fields
 '''
 # need Curl
 Test.SkipUnless(
@@ -47,6 +47,10 @@
     'map https://127.0.0.1:{0} https://httpbin.org/ip'.format(ts.Variables.ssl_port)
 )
 
+ts.Disk.remap_config.AddLine(
+    'map https://reallyreallyreallyreallylong.com https://httpbin.org/ip'.format(ts.Variables.ssl_port)
+)
+
 ts.Disk.ssl_multicert_config.AddLine(
     'dest_ip=* ssl_cert_name=server.pem ssl_key_name=server.key'
 )
@@ -56,9 +60,9 @@
 logging:
   formats:
     - name: custom
-      format: "%<ccid> %<ctid>"
+      format: "%<ccid> %<ctid> %<cssn>"
   logs:
-    - filename: test_ccid_ctid
+    - filename: test_new_log_flds
       format: custom
 '''.split("\n")
 )
@@ -82,8 +86,17 @@
 tr.Processes.Default.ReturnCode = 0
 
 tr = Test.AddTestRun()
-tr.Processes.Default.Command = 'curl "https://127.0.0.1:{0}" "https://127.0.0.1:{0}" --http2 --insecure --verbose'.format(
-    ts.Variables.ssl_port)
+tr.Processes.Default.Command = (
+    'curl "https://127.0.0.1:{0}" "https://127.0.0.1:{0}" --http2 --insecure --verbose'.format(
+        ts.Variables.ssl_port)
+)
+tr.Processes.Default.ReturnCode = 0
+
+tr = Test.AddTestRun()
+tr.Processes.Default.Command = (
+    'curl "https://reallyreallyreallyreallylong.com:{0}" --http2 --insecure --verbose' +
+    ' --resolve reallyreallyreallyreallylong.com:{0}:127.0.0.1'
+).format(ts.Variables.ssl_port)
 tr.Processes.Default.ReturnCode = 0
 
 # Delay to allow TS to flush report to disk, then validate generated log.
@@ -91,6 +104,6 @@
 tr = Test.AddTestRun()
 tr.DelayStart = 10
 tr.Processes.Default.Command = 'python {0} < {1}'.format(
-    os.path.join(Test.TestDirectory, 'ccid_ctid_observer.py'),
-    os.path.join(ts.Variables.LOGDIR, 'test_ccid_ctid.log'))
+    os.path.join(Test.TestDirectory, 'new_log_flds_observer.py'),
+    os.path.join(ts.Variables.LOGDIR, 'test_new_log_flds.log'))
 tr.Processes.Default.ReturnCode = 0
diff --git a/tests/gold_tests/logging/ccid_ctid_observer.py b/tests/gold_tests/logging/new_log_flds_observer.py
similarity index 76%
rename from tests/gold_tests/logging/ccid_ctid_observer.py
rename to tests/gold_tests/logging/new_log_flds_observer.py
index 1b4cee5..0fba8c1 100644
--- a/tests/gold_tests/logging/ccid_ctid_observer.py
+++ b/tests/gold_tests/logging/new_log_flds_observer.py
@@ -1,5 +1,5 @@
 '''
-Examines log generated by ccid_ctid.test.py, returns 0 if valid, 1 if not.
+Examines log generated by new_log_flds.test.py, returns 0 if valid, 1 if not.
 '''
 #  Licensed to the Apache Software Foundation (ASF) under one
 #  or more contributor license agreements.  See the NOTICE file
@@ -23,10 +23,12 @@
 ccid = []
 ctid = []
 
-# Read in ccid and ctid fields from each line of the generated report.
+# Read in log fields from each line of the generated report.
 #
+ln_num = 0
 for ln in csv.reader(sys.stdin, delimiter=' '):
-    if len(ln) != 2:
+    ln_num += 1
+    if len(ln) != 3:
         exit(code=1)
     i = int(ln[0])
     if i < 0:
@@ -36,6 +38,12 @@
     if i < 0:
         exit(code=1)
     ctid.append(i)
+    if ln_num == 7:
+        if ln[2] != "reallyreallyreallyreallylong.com":
+            exit(code=1)
+    else:
+        if ln[2] != "-":
+            exit(code=1)
 
 # Validate contents of report.
 #
@@ -45,7 +53,8 @@
     ctid[2] != ctid[3] and
     ccid[3] != ccid[4] and
     ccid[4] == ccid[5] and
-    ctid[4] != ctid[5]):
+    ctid[4] != ctid[5] and
+    ccid[5] != ccid[6]):
     exit(code=0)
 
 # Failure exit if report was not valid.