You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficserver.apache.org by am...@apache.org on 2017/10/02 19:50:52 UTC

[trafficserver] branch master updated: Add support for Forwarded HTTP header tag (RFC7239).

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

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


The following commit(s) were added to refs/heads/master by this push:
     new d91662d  Add support for Forwarded HTTP header tag (RFC7239).
d91662d is described below

commit d91662dafb1d826603769ba6202d502789ee49b6
Author: Walt Karas <wk...@yahoo-inc.com>
AuthorDate: Fri Aug 4 23:58:13 2017 +0000

    Add support for Forwarded HTTP header tag (RFC7239).
---
 .../transparent-forward-proxying.en.rst            |   3 +
 doc/admin-guide/files/records.config.en.rst        |  30 +++
 .../api/functions/TSHttpOverridableConfig.en.rst   |   1 +
 lib/ts/apidefs.h.in                                |   3 +
 mgmt/RecordsConfig.cc                              |   2 +
 .../header_normalize/header_normalize.cc           |   1 +
 plugins/experimental/ts_lua/ts_lua_http_config.c   |   2 +
 plugins/s3_auth/aws_auth_v4.cc                     |   1 +
 plugins/s3_auth/s3_auth.cc                         |   1 +
 proxy/InkAPI.cc                                    |  21 ++
 proxy/InkAPITest.cc                                | 227 ++++++++--------
 proxy/hdrs/HdrToken.cc                             |  12 +-
 proxy/hdrs/MIME.cc                                 |   6 +
 proxy/hdrs/MIME.h                                  |   2 +
 proxy/http/ForwardedConfig.cc                      | 189 ++++++++++++++
 proxy/http/HttpConfig.cc                           |  43 +++
 proxy/http/HttpConfig.h                            |  37 +++
 proxy/http/HttpTransact.cc                         |   1 +
 proxy/http/HttpTransactHeaders.cc                  | 265 ++++++++++++++++---
 proxy/http/HttpTransactHeaders.h                   |   7 +
 proxy/http/Makefile.am                             |  17 +-
 proxy/http/unit-tests/sym-links/MemView.cc         |   1 +
 proxy/http/unit-tests/test_ForwardedConfig.cc      | 169 ++++++++++++
 .../http/unit-tests/test_ForwardedConfig_mocks.cc  |  86 ++++++
 tests/gold_tests/headers/forwarded-observer.py     |  63 +++++
 tests/gold_tests/headers/forwarded.gold            |  41 +++
 tests/gold_tests/headers/forwarded.test.py         | 289 +++++++++++++++++++++
 27 files changed, 1372 insertions(+), 148 deletions(-)

diff --git a/doc/admin-guide/configuration/transparent-forward-proxying.en.rst b/doc/admin-guide/configuration/transparent-forward-proxying.en.rst
index cee8ba6..d573c28 100644
--- a/doc/admin-guide/configuration/transparent-forward-proxying.en.rst
+++ b/doc/admin-guide/configuration/transparent-forward-proxying.en.rst
@@ -88,6 +88,9 @@ You may also want to consider some of these configuration options:
 - The client request header X-Forwarded-For may be toggled with
   :ts:cv:`proxy.config.http.insert_squid_x_forwarded_for`.
 
+- The client request header Forwarded may be configured with
+  :ts:cv:`proxy.config.http.insert_forwarded`.
+
 Client Configuration
 ====================
 
diff --git a/doc/admin-guide/files/records.config.en.rst b/doc/admin-guide/files/records.config.en.rst
index 0aaf67e..cf1b921 100644
--- a/doc/admin-guide/files/records.config.en.rst
+++ b/doc/admin-guide/files/records.config.en.rst
@@ -1666,6 +1666,36 @@ Proxy User Variables
 
    When enabled (``1``), Traffic Server adds the client IP address to the ``X-Forwarded-For`` header.
 
+.. ts:cv:: CONFIG proxy.config.http.insert_forwarded STRING none
+   :reloadable:
+   :overridable:
+
+   The default value (``none``) means that Traffic Server does not insert or append information to any
+   ``Forwarded`` header (described in IETF RFC 7239) in the request message.  To put information into a
+   ``Forwarded`` header in the request, the value of this variable must be a list of the ``Forwarded``
+   parameters to be inserted.
+
+   ==================  ===============================================================
+   Parameter           Value of parameter place in outgoing Forwarded header
+   ==================  ===============================================================
+   for                 Client IP address
+   by=ip               Proxy IP address
+   by=unknown          The literal string ``unknown``
+   by=servername       Proxy server name
+   by=uuid             Server UUID prefixed with ``_``
+   proto               Protocol of incoming request
+   host                The host specified in the incoming request
+   connection=compact  Connection with basic transaction codes.
+   connection=std      Connection with detailed transaction codes.
+   connection=full     Full user agent connection :ref:`protocol tags <protocol_tags>`
+   ==================  ===============================================================
+
+   Each paramater in the list must be separated by ``|`` or ``:``.  For example, ``for|by=uuid|proto`` is
+   a valid value for this variable.  Note that the ``connection`` parameter is a non-standard extension to
+   RFC 7239.  Also note that, while Traffic Server allows multiple ``by`` parameters for the same proxy, this
+   is prohibited by RFC 7239. Currently, for the ``host`` parameter to provide the original host from the
+   incoming client request, `proxy.config.url_remap.pristine_host_hdr`_ must be enabled.
+
 .. ts:cv:: CONFIG proxy.config.http.normalize_ae INT 1
    :reloadable:
    :overridable:
diff --git a/doc/developer-guide/api/functions/TSHttpOverridableConfig.en.rst b/doc/developer-guide/api/functions/TSHttpOverridableConfig.en.rst
index f8f191e..e1340ad 100644
--- a/doc/developer-guide/api/functions/TSHttpOverridableConfig.en.rst
+++ b/doc/developer-guide/api/functions/TSHttpOverridableConfig.en.rst
@@ -123,6 +123,7 @@ c:member:`TS_CONFIG_HTTP_INSERT_AGE_IN_RESPONSE`                    :ts:cv:`prox
 c:member:`TS_CONFIG_HTTP_INSERT_REQUEST_VIA_STR`                    :ts:cv:`proxy.config.http.insert_request_via_str`
 c:member:`TS_CONFIG_HTTP_INSERT_RESPONSE_VIA_STR`                   :ts:cv:`proxy.config.http.insert_response_via_str`
 c:member:`TS_CONFIG_HTTP_INSERT_SQUID_X_FORWARDED_FOR`              :ts:cv:`proxy.config.http.insert_squid_x_forwarded_for`
+c:member:`TS_CONFIG_HTTP_INSERT_FORWARDED`                          :ts:cv:`proxy.config.http.insert_forwarded`
 c:member:`TS_CONFIG_HTTP_KEEP_ALIVE_ENABLED_IN`                     :ts:cv:`proxy.config.http.keep_alive_enabled_in`
 c:member:`TS_CONFIG_HTTP_KEEP_ALIVE_ENABLED_OUT`                    :ts:cv:`proxy.config.http.keep_alive_enabled_out`
 c:member:`TS_CONFIG_HTTP_KEEP_ALIVE_NO_ACTIVITY_TlMEOUT_IN`         :ts:cv:`proxy.config.http.keep_alive_no_activity_timeout_in`
diff --git a/lib/ts/apidefs.h.in b/lib/ts/apidefs.h.in
index 0765cee..a9bfa8c 100644
--- a/lib/ts/apidefs.h.in
+++ b/lib/ts/apidefs.h.in
@@ -756,6 +756,7 @@ typedef enum {
   TS_CONFIG_HTTP_PER_PARENT_CONNECT_ATTEMPTS,
   TS_CONFIG_HTTP_PARENT_CONNECT_ATTEMPT_TIMEOUT,
   TS_CONFIG_HTTP_NORMALIZE_AE,
+  TS_CONFIG_HTTP_INSERT_FORWARDED,
   TS_CONFIG_LAST_ENTRY
 } TSOverridableConfigKey;
 
@@ -1008,6 +1009,7 @@ extern tsapi const char *TS_MIME_FIELD_WARNING;
 extern tsapi const char *TS_MIME_FIELD_WWW_AUTHENTICATE;
 extern tsapi const char *TS_MIME_FIELD_XREF;
 extern tsapi const char *TS_MIME_FIELD_X_FORWARDED_FOR;
+extern tsapi const char *TS_MIME_FIELD_FORWARDED;
 
 /* --------------------------------------------------------------------------
    MIME fields string lengths */
@@ -1083,6 +1085,7 @@ extern tsapi int TS_MIME_LEN_WARNING;
 extern tsapi int TS_MIME_LEN_WWW_AUTHENTICATE;
 extern tsapi int TS_MIME_LEN_XREF;
 extern tsapi int TS_MIME_LEN_X_FORWARDED_FOR;
+extern tsapi int TS_MIME_LEN_FORWARDED;
 
 /* --------------------------------------------------------------------------
    HTTP values */
diff --git a/mgmt/RecordsConfig.cc b/mgmt/RecordsConfig.cc
index 74010e2..6868337 100644
--- a/mgmt/RecordsConfig.cc
+++ b/mgmt/RecordsConfig.cc
@@ -560,6 +560,8 @@ static const RecordElement RecordsConfig[] =
   ,
   {RECT_CONFIG, "proxy.config.http.insert_squid_x_forwarded_for", RECD_INT, "1", RECU_DYNAMIC, RR_NULL, RECC_NULL, nullptr, RECA_NULL}
   ,
+  {RECT_CONFIG, "proxy.config.http.insert_forwarded", RECD_STRING, "none", RECU_DYNAMIC, RR_NULL, RECC_NULL, nullptr, RECA_NULL}
+  ,
   {RECT_CONFIG, "proxy.config.http.insert_age_in_response", RECD_INT, "1", RECU_DYNAMIC, RR_NULL, RECC_INT, "[0-1]", RECA_NULL}
   ,
   {RECT_CONFIG, "proxy.config.http.enable_http_stats", RECD_INT, "1", RECU_DYNAMIC, RR_NULL, RECC_INT, "[0-1]", RECA_NULL}
diff --git a/plugins/experimental/header_normalize/header_normalize.cc b/plugins/experimental/header_normalize/header_normalize.cc
index c4d9e6b..fc99f89 100644
--- a/plugins/experimental/header_normalize/header_normalize.cc
+++ b/plugins/experimental/header_normalize/header_normalize.cc
@@ -135,6 +135,7 @@ buildHdrMap()
   hdrMap["xref"]                      = "Xref";
   hdrMap["x-id"]                      = "X-ID";
   hdrMap["x-forwarded-for"]           = "X-Forwarded-For";
+  hdrMap["forwarded"]                 = "Forwarded";
   hdrMap["sec-websocket-key"]         = "Sec-WebSocket-Key";
   hdrMap["sec-websocket-version"]     = "Sec-WebSocket-Version";
 }
diff --git a/plugins/experimental/ts_lua/ts_lua_http_config.c b/plugins/experimental/ts_lua/ts_lua_http_config.c
index 8b6f8e2..1a57d79 100644
--- a/plugins/experimental/ts_lua/ts_lua_http_config.c
+++ b/plugins/experimental/ts_lua/ts_lua_http_config.c
@@ -40,6 +40,7 @@ typedef enum {
   TS_LUA_CONFIG_HTTP_ANONYMIZE_INSERT_CLIENT_IP               = TS_CONFIG_HTTP_ANONYMIZE_INSERT_CLIENT_IP,
   TS_LUA_CONFIG_HTTP_RESPONSE_SERVER_ENABLED                  = TS_CONFIG_HTTP_RESPONSE_SERVER_ENABLED,
   TS_LUA_CONFIG_HTTP_INSERT_SQUID_X_FORWARDED_FOR             = TS_CONFIG_HTTP_INSERT_SQUID_X_FORWARDED_FOR,
+  TS_LUA_CONFIG_HTTP_INSERT_FORWARDED                         = TS_CONFIG_HTTP_INSERT_FORWARDED,
   TS_LUA_CONFIG_HTTP_SERVER_TCP_INIT_CWND                     = TS_CONFIG_HTTP_SERVER_TCP_INIT_CWND,
   TS_LUA_CONFIG_HTTP_SEND_HTTP11_REQUESTS                     = TS_CONFIG_HTTP_SEND_HTTP11_REQUESTS,
   TS_LUA_CONFIG_HTTP_CACHE_HTTP                               = TS_CONFIG_HTTP_CACHE_HTTP,
@@ -163,6 +164,7 @@ ts_lua_var_item ts_lua_http_config_vars[] = {
   TS_LUA_MAKE_VAR_ITEM(TS_LUA_CONFIG_HTTP_ANONYMIZE_INSERT_CLIENT_IP),
   TS_LUA_MAKE_VAR_ITEM(TS_LUA_CONFIG_HTTP_RESPONSE_SERVER_ENABLED),
   TS_LUA_MAKE_VAR_ITEM(TS_LUA_CONFIG_HTTP_INSERT_SQUID_X_FORWARDED_FOR),
+  TS_LUA_MAKE_VAR_ITEM(TS_LUA_CONFIG_HTTP_INSERT_FORWARDED),
   TS_LUA_MAKE_VAR_ITEM(TS_LUA_CONFIG_HTTP_SERVER_TCP_INIT_CWND),
   TS_LUA_MAKE_VAR_ITEM(TS_LUA_CONFIG_HTTP_SEND_HTTP11_REQUESTS),
   TS_LUA_MAKE_VAR_ITEM(TS_LUA_CONFIG_HTTP_CACHE_HTTP),
diff --git a/plugins/s3_auth/aws_auth_v4.cc b/plugins/s3_auth/aws_auth_v4.cc
index f522118..5c3cd66 100644
--- a/plugins/s3_auth/aws_auth_v4.cc
+++ b/plugins/s3_auth/aws_auth_v4.cc
@@ -470,6 +470,7 @@ createDefaultExcludeHeaders()
   StringSet m;
   /* exclude headers that are meant to be changed */
   m.insert("x-forwarded-for");
+  m.insert("forwarded");
   m.insert("via");
   return m;
 }
diff --git a/plugins/s3_auth/s3_auth.cc b/plugins/s3_auth/s3_auth.cc
index 2172170..97e7d8d 100644
--- a/plugins/s3_auth/s3_auth.cc
+++ b/plugins/s3_auth/s3_auth.cc
@@ -364,6 +364,7 @@ public:
 
     /* Exclude headers that are meant to be changed */
     _v4excludeHeaders.insert("x-forwarded-for");
+    _v4excludeHeaders.insert("forwarded");
     _v4excludeHeaders.insert("via");
   }
 
diff --git a/proxy/InkAPI.cc b/proxy/InkAPI.cc
index 5114cc4..a710a5e 100644
--- a/proxy/InkAPI.cc
+++ b/proxy/InkAPI.cc
@@ -216,6 +216,7 @@ tsapi const char *TS_MIME_FIELD_WARNING;
 tsapi const char *TS_MIME_FIELD_WWW_AUTHENTICATE;
 tsapi const char *TS_MIME_FIELD_XREF;
 tsapi const char *TS_MIME_FIELD_X_FORWARDED_FOR;
+tsapi const char *TS_MIME_FIELD_FORWARDED;
 
 /* MIME fields string lengths */
 tsapi int TS_MIME_LEN_ACCEPT;
@@ -290,6 +291,7 @@ tsapi int TS_MIME_LEN_WARNING;
 tsapi int TS_MIME_LEN_WWW_AUTHENTICATE;
 tsapi int TS_MIME_LEN_XREF;
 tsapi int TS_MIME_LEN_X_FORWARDED_FOR;
+tsapi int TS_MIME_LEN_FORWARDED;
 
 /* HTTP miscellaneous values */
 tsapi const char *TS_HTTP_VALUE_BYTES;
@@ -1500,6 +1502,7 @@ api_init()
     TS_MIME_FIELD_WWW_AUTHENTICATE          = MIME_FIELD_WWW_AUTHENTICATE;
     TS_MIME_FIELD_XREF                      = MIME_FIELD_XREF;
     TS_MIME_FIELD_X_FORWARDED_FOR           = MIME_FIELD_X_FORWARDED_FOR;
+    TS_MIME_FIELD_FORWARDED                 = MIME_FIELD_FORWARDED;
 
     TS_MIME_LEN_ACCEPT                    = MIME_LEN_ACCEPT;
     TS_MIME_LEN_ACCEPT_CHARSET            = MIME_LEN_ACCEPT_CHARSET;
@@ -1573,6 +1576,7 @@ api_init()
     TS_MIME_LEN_WWW_AUTHENTICATE          = MIME_LEN_WWW_AUTHENTICATE;
     TS_MIME_LEN_XREF                      = MIME_LEN_XREF;
     TS_MIME_LEN_X_FORWARDED_FOR           = MIME_LEN_X_FORWARDED_FOR;
+    TS_MIME_LEN_FORWARDED                 = MIME_LEN_FORWARDED;
 
     /* HTTP methods */
     TS_HTTP_METHOD_CONNECT = HTTP_METHOD_CONNECT;
@@ -7841,6 +7845,9 @@ _conf_to_memberp(TSOverridableConfigKey conf, OverridableHttpConfigParams *overr
   case TS_CONFIG_HTTP_INSERT_SQUID_X_FORWARDED_FOR:
     ret = _memberp_to_generic(&overridableHttpConfig->insert_squid_x_forwarded_for, typep);
     break;
+  case TS_CONFIG_HTTP_INSERT_FORWARDED:
+    ret = _memberp_to_generic(&overridableHttpConfig->insert_forwarded, typep);
+    break;
   case TS_CONFIG_HTTP_SERVER_TCP_INIT_CWND:
     ret = _memberp_to_generic(&overridableHttpConfig->server_tcp_init_cwnd, typep);
     break;
@@ -8265,6 +8272,17 @@ TSHttpTxnConfigStringSet(TSHttpTxn txnp, TSOverridableConfigKey conf, const char
       s->t_state.txn_conf->client_cert_filepath = const_cast<char *>(value);
     }
     break;
+  case TS_CONFIG_HTTP_INSERT_FORWARDED:
+    if (value && length > 0) {
+      ts::LocalBufferWriter<1024> error;
+      HttpForwarded::OptionBitSet bs = HttpForwarded::optStrToBitset(ts::string_view(value, length), error);
+      if (!error.size()) {
+        s->t_state.txn_conf->insert_forwarded = bs;
+      } else {
+        Error("HTTP %.*s", static_cast<int>(error.size()), error.data());
+      }
+    }
+    break;
   default:
     return TS_ERROR;
     break;
@@ -8361,6 +8379,9 @@ TSHttpTxnConfigFind(const char *name, int length, TSOverridableConfigKey *conf,
       cnf = TS_CONFIG_HTTP_CACHE_GENERATION;
     } else if (!strncmp(name, "proxy.config.http.insert_client_ip", length)) {
       cnf = TS_CONFIG_HTTP_ANONYMIZE_INSERT_CLIENT_IP;
+    } else if (!strncmp(name, "proxy.config.http.insert_forwarded", length)) {
+      cnf = TS_CONFIG_HTTP_INSERT_FORWARDED;
+      typ = TS_RECORDDATATYPE_STRING;
     }
     break;
 
diff --git a/proxy/InkAPITest.cc b/proxy/InkAPITest.cc
index c7ce69b..bc97c62 100644
--- a/proxy/InkAPITest.cc
+++ b/proxy/InkAPITest.cc
@@ -7477,120 +7477,119 @@ EXCLUSIVE_REGRESSION_TEST(SDK_API_TSHttpConnectServerIntercept)(RegressionTest *
 ////////////////////////////////////////////////
 
 // The order of these should be the same as TSOverridableConfigKey
-const char *SDK_Overridable_Configs[TS_CONFIG_LAST_ENTRY] = {
-  "proxy.config.url_remap.pristine_host_hdr",
-  "proxy.config.http.chunking_enabled",
-  "proxy.config.http.negative_caching_enabled",
-  "proxy.config.http.negative_caching_lifetime",
-  "proxy.config.http.cache.when_to_revalidate",
-  "proxy.config.http.keep_alive_enabled_in",
-  "proxy.config.http.keep_alive_enabled_out",
-  "proxy.config.http.keep_alive_post_out",
-  "proxy.config.http.server_session_sharing.match",
-  "proxy.config.net.sock_recv_buffer_size_out",
-  "proxy.config.net.sock_send_buffer_size_out",
-  "proxy.config.net.sock_option_flag_out",
-  "proxy.config.http.forward.proxy_auth_to_parent",
-  "proxy.config.http.anonymize_remove_from",
-  "proxy.config.http.anonymize_remove_referer",
-  "proxy.config.http.anonymize_remove_user_agent",
-  "proxy.config.http.anonymize_remove_cookie",
-  "proxy.config.http.anonymize_remove_client_ip",
-  "proxy.config.http.insert_client_ip",
-  "proxy.config.http.response_server_enabled",
-  "proxy.config.http.insert_squid_x_forwarded_for",
-  "proxy.config.http.server_tcp_init_cwnd",
-  "proxy.config.http.send_http11_requests",
-  "proxy.config.http.cache.http",
-  "proxy.config.http.cache.ignore_client_no_cache",
-  "proxy.config.http.cache.ignore_client_cc_max_age",
-  "proxy.config.http.cache.ims_on_client_no_cache",
-  "proxy.config.http.cache.ignore_server_no_cache",
-  "proxy.config.http.cache.cache_responses_to_cookies",
-  "proxy.config.http.cache.ignore_authentication",
-  "proxy.config.http.cache.cache_urls_that_look_dynamic",
-  "proxy.config.http.cache.required_headers",
-  "proxy.config.http.insert_request_via_str",
-  "proxy.config.http.insert_response_via_str",
-  "proxy.config.http.cache.heuristic_min_lifetime",
-  "proxy.config.http.cache.heuristic_max_lifetime",
-  "proxy.config.http.cache.guaranteed_min_lifetime",
-  "proxy.config.http.cache.guaranteed_max_lifetime",
-  "proxy.config.http.cache.max_stale_age",
-  "proxy.config.http.keep_alive_no_activity_timeout_in",
-  "proxy.config.http.keep_alive_no_activity_timeout_out",
-  "proxy.config.http.transaction_no_activity_timeout_in",
-  "proxy.config.http.transaction_no_activity_timeout_out",
-  "proxy.config.http.transaction_active_timeout_out",
-  "proxy.config.http.origin_max_connections",
-  "proxy.config.http.connect_attempts_max_retries",
-  "proxy.config.http.connect_attempts_max_retries_dead_server",
-  "proxy.config.http.connect_attempts_rr_retries",
-  "proxy.config.http.connect_attempts_timeout",
-  "proxy.config.http.post_connect_attempts_timeout",
-  "proxy.config.http.down_server.cache_time",
-  "proxy.config.http.down_server.abort_threshold",
-  "proxy.config.http.doc_in_cache_skip_dns",
-  "proxy.config.http.background_fill_active_timeout",
-  "proxy.config.http.response_server_str",
-  "proxy.config.http.cache.heuristic_lm_factor",
-  "proxy.config.http.background_fill_completed_threshold",
-  "proxy.config.net.sock_packet_mark_out",
-  "proxy.config.net.sock_packet_tos_out",
-  "proxy.config.http.insert_age_in_response",
-  "proxy.config.http.chunking.size",
-  "proxy.config.http.flow_control.enabled",
-  "proxy.config.http.flow_control.low_water",
-  "proxy.config.http.flow_control.high_water",
-  "proxy.config.http.cache.range.lookup",
-  "proxy.config.http.default_buffer_size",
-  "proxy.config.http.default_buffer_water_mark",
-  "proxy.config.http.request_header_max_size",
-  "proxy.config.http.response_header_max_size",
-  "proxy.config.http.negative_revalidating_enabled",
-  "proxy.config.http.negative_revalidating_lifetime",
-  "proxy.config.ssl.hsts_max_age",
-  "proxy.config.ssl.hsts_include_subdomains",
-  "proxy.config.http.cache.open_read_retry_time",
-  "proxy.config.http.cache.max_open_read_retries",
-  "proxy.config.http.cache.range.write",
-  "proxy.config.http.post.check.content_length.enabled",
-  "proxy.config.http.global_user_agent_header",
-  "proxy.config.http.auth_server_session_private",
-  "proxy.config.http.slow.log.threshold",
-  "proxy.config.http.cache.generation",
-  "proxy.config.body_factory.template_base",
-  "proxy.config.http.cache.open_write_fail_action",
-  "proxy.config.http.number_of_redirections",
-  "proxy.config.http.cache.max_open_write_retries",
-  "proxy.config.http.redirect_use_orig_cache_key",
-  "proxy.config.http.attach_server_session_to_client",
-  "proxy.config.http.origin_max_connections_queue",
-  "proxy.config.websocket.no_activity_timeout",
-  "proxy.config.websocket.active_timeout",
-  "proxy.config.http.uncacheable_requests_bypass_parent",
-  "proxy.config.http.parent_proxy.total_connect_attempts",
-  "proxy.config.http.transaction_active_timeout_in",
-  "proxy.config.srv_enabled",
-  "proxy.config.http.forward_connect_method",
-  "proxy.config.ssl.client.cert.filename",
-  "proxy.config.ssl.client.cert.path",
-  "proxy.config.http.parent_proxy.mark_down_hostdb",
-  "proxy.config.ssl.client.verify.server",
-  "proxy.config.http.cache.enable_default_vary_headers",
-  "proxy.config.http.cache.vary_default_text",
-  "proxy.config.http.cache.vary_default_images",
-  "proxy.config.http.cache.vary_default_other",
-  "proxy.config.http.cache.ignore_accept_mismatch",
-  "proxy.config.http.cache.ignore_accept_language_mismatch",
-  "proxy.config.http.cache.ignore_accept_encoding_mismatch",
-  "proxy.config.http.cache.ignore_accept_charset_mismatch",
-  "proxy.config.http.parent_proxy.fail_threshold",
-  "proxy.config.http.parent_proxy.retry_time",
-  "proxy.config.http.parent_proxy.per_parent_connect_attempts",
-  "proxy.config.http.parent_proxy.connect_attempts_timeout",
-  "proxy.config.http.normalize_ae",
-};
+const char *SDK_Overridable_Configs[TS_CONFIG_LAST_ENTRY] = {"proxy.config.url_remap.pristine_host_hdr",
+                                                             "proxy.config.http.chunking_enabled",
+                                                             "proxy.config.http.negative_caching_enabled",
+                                                             "proxy.config.http.negative_caching_lifetime",
+                                                             "proxy.config.http.cache.when_to_revalidate",
+                                                             "proxy.config.http.keep_alive_enabled_in",
+                                                             "proxy.config.http.keep_alive_enabled_out",
+                                                             "proxy.config.http.keep_alive_post_out",
+                                                             "proxy.config.http.server_session_sharing.match",
+                                                             "proxy.config.net.sock_recv_buffer_size_out",
+                                                             "proxy.config.net.sock_send_buffer_size_out",
+                                                             "proxy.config.net.sock_option_flag_out",
+                                                             "proxy.config.http.forward.proxy_auth_to_parent",
+                                                             "proxy.config.http.anonymize_remove_from",
+                                                             "proxy.config.http.anonymize_remove_referer",
+                                                             "proxy.config.http.anonymize_remove_user_agent",
+                                                             "proxy.config.http.anonymize_remove_cookie",
+                                                             "proxy.config.http.anonymize_remove_client_ip",
+                                                             "proxy.config.http.insert_client_ip",
+                                                             "proxy.config.http.response_server_enabled",
+                                                             "proxy.config.http.insert_squid_x_forwarded_for",
+                                                             "proxy.config.http.server_tcp_init_cwnd",
+                                                             "proxy.config.http.send_http11_requests",
+                                                             "proxy.config.http.cache.http",
+                                                             "proxy.config.http.cache.ignore_client_no_cache",
+                                                             "proxy.config.http.cache.ignore_client_cc_max_age",
+                                                             "proxy.config.http.cache.ims_on_client_no_cache",
+                                                             "proxy.config.http.cache.ignore_server_no_cache",
+                                                             "proxy.config.http.cache.cache_responses_to_cookies",
+                                                             "proxy.config.http.cache.ignore_authentication",
+                                                             "proxy.config.http.cache.cache_urls_that_look_dynamic",
+                                                             "proxy.config.http.cache.required_headers",
+                                                             "proxy.config.http.insert_request_via_str",
+                                                             "proxy.config.http.insert_response_via_str",
+                                                             "proxy.config.http.cache.heuristic_min_lifetime",
+                                                             "proxy.config.http.cache.heuristic_max_lifetime",
+                                                             "proxy.config.http.cache.guaranteed_min_lifetime",
+                                                             "proxy.config.http.cache.guaranteed_max_lifetime",
+                                                             "proxy.config.http.cache.max_stale_age",
+                                                             "proxy.config.http.keep_alive_no_activity_timeout_in",
+                                                             "proxy.config.http.keep_alive_no_activity_timeout_out",
+                                                             "proxy.config.http.transaction_no_activity_timeout_in",
+                                                             "proxy.config.http.transaction_no_activity_timeout_out",
+                                                             "proxy.config.http.transaction_active_timeout_out",
+                                                             "proxy.config.http.origin_max_connections",
+                                                             "proxy.config.http.connect_attempts_max_retries",
+                                                             "proxy.config.http.connect_attempts_max_retries_dead_server",
+                                                             "proxy.config.http.connect_attempts_rr_retries",
+                                                             "proxy.config.http.connect_attempts_timeout",
+                                                             "proxy.config.http.post_connect_attempts_timeout",
+                                                             "proxy.config.http.down_server.cache_time",
+                                                             "proxy.config.http.down_server.abort_threshold",
+                                                             "proxy.config.http.doc_in_cache_skip_dns",
+                                                             "proxy.config.http.background_fill_active_timeout",
+                                                             "proxy.config.http.response_server_str",
+                                                             "proxy.config.http.cache.heuristic_lm_factor",
+                                                             "proxy.config.http.background_fill_completed_threshold",
+                                                             "proxy.config.net.sock_packet_mark_out",
+                                                             "proxy.config.net.sock_packet_tos_out",
+                                                             "proxy.config.http.insert_age_in_response",
+                                                             "proxy.config.http.chunking.size",
+                                                             "proxy.config.http.flow_control.enabled",
+                                                             "proxy.config.http.flow_control.low_water",
+                                                             "proxy.config.http.flow_control.high_water",
+                                                             "proxy.config.http.cache.range.lookup",
+                                                             "proxy.config.http.default_buffer_size",
+                                                             "proxy.config.http.default_buffer_water_mark",
+                                                             "proxy.config.http.request_header_max_size",
+                                                             "proxy.config.http.response_header_max_size",
+                                                             "proxy.config.http.negative_revalidating_enabled",
+                                                             "proxy.config.http.negative_revalidating_lifetime",
+                                                             "proxy.config.ssl.hsts_max_age",
+                                                             "proxy.config.ssl.hsts_include_subdomains",
+                                                             "proxy.config.http.cache.open_read_retry_time",
+                                                             "proxy.config.http.cache.max_open_read_retries",
+                                                             "proxy.config.http.cache.range.write",
+                                                             "proxy.config.http.post.check.content_length.enabled",
+                                                             "proxy.config.http.global_user_agent_header",
+                                                             "proxy.config.http.auth_server_session_private",
+                                                             "proxy.config.http.slow.log.threshold",
+                                                             "proxy.config.http.cache.generation",
+                                                             "proxy.config.body_factory.template_base",
+                                                             "proxy.config.http.cache.open_write_fail_action",
+                                                             "proxy.config.http.number_of_redirections",
+                                                             "proxy.config.http.cache.max_open_write_retries",
+                                                             "proxy.config.http.redirect_use_orig_cache_key",
+                                                             "proxy.config.http.attach_server_session_to_client",
+                                                             "proxy.config.http.origin_max_connections_queue",
+                                                             "proxy.config.websocket.no_activity_timeout",
+                                                             "proxy.config.websocket.active_timeout",
+                                                             "proxy.config.http.uncacheable_requests_bypass_parent",
+                                                             "proxy.config.http.parent_proxy.total_connect_attempts",
+                                                             "proxy.config.http.transaction_active_timeout_in",
+                                                             "proxy.config.srv_enabled",
+                                                             "proxy.config.http.forward_connect_method",
+                                                             "proxy.config.ssl.client.cert.filename",
+                                                             "proxy.config.ssl.client.cert.path",
+                                                             "proxy.config.http.parent_proxy.mark_down_hostdb",
+                                                             "proxy.config.ssl.client.verify.server",
+                                                             "proxy.config.http.cache.enable_default_vary_headers",
+                                                             "proxy.config.http.cache.vary_default_text",
+                                                             "proxy.config.http.cache.vary_default_images",
+                                                             "proxy.config.http.cache.vary_default_other",
+                                                             "proxy.config.http.cache.ignore_accept_mismatch",
+                                                             "proxy.config.http.cache.ignore_accept_language_mismatch",
+                                                             "proxy.config.http.cache.ignore_accept_encoding_mismatch",
+                                                             "proxy.config.http.cache.ignore_accept_charset_mismatch",
+                                                             "proxy.config.http.parent_proxy.fail_threshold",
+                                                             "proxy.config.http.parent_proxy.retry_time",
+                                                             "proxy.config.http.parent_proxy.per_parent_connect_attempts",
+                                                             "proxy.config.http.parent_proxy.connect_attempts_timeout",
+                                                             "proxy.config.http.normalize_ae",
+                                                             "proxy.config.http.insert_forwarded"};
 
 REGRESSION_TEST(SDK_API_OVERRIDABLE_CONFIGS)(RegressionTest *test, int /* atype ATS_UNUSED */, int *pstatus)
 {
diff --git a/proxy/hdrs/HdrToken.cc b/proxy/hdrs/HdrToken.cc
index 8cb1f3b..6f8064c 100644
--- a/proxy/hdrs/HdrToken.cc
+++ b/proxy/hdrs/HdrToken.cc
@@ -108,7 +108,9 @@ static const char *_hdrtoken_strs[] = {
 
   // Header extensions
   "X-ID", "X-Forwarded-For", "TE", "Strict-Transport-Security", "100-continue",
-};
+
+  // RFC-2739
+  "Forwarded"};
 
 static HdrTokenTypeBinding _hdrtoken_strs_type_initializers[] = {
   {"file", HDRTOKEN_TYPE_SCHEME},
@@ -233,6 +235,7 @@ static HdrTokenFieldInfo _hdrtoken_strs_field_initializers[] = {
   {"Xref", MIME_SLOTID_NONE, MIME_PRESENCE_XREF, HTIF_NONE},
   {"X-ID", MIME_SLOTID_NONE, MIME_PRESENCE_NONE, (HTIF_COMMAS | HTIF_MULTVALS | HTIF_HOPBYHOP)},
   {"X-Forwarded-For", MIME_SLOTID_NONE, MIME_PRESENCE_NONE, (HTIF_COMMAS | HTIF_MULTVALS)},
+  {"Forwarded", MIME_SLOTID_NONE, MIME_PRESENCE_NONE, (HTIF_COMMAS | HTIF_MULTVALS)},
   {"Sec-WebSocket-Key", MIME_SLOTID_NONE, MIME_PRESENCE_NONE, HTIF_NONE},
   {"Sec-WebSocket-Version", MIME_SLOTID_NONE, MIME_PRESENCE_NONE, HTIF_NONE},
   {nullptr, 0, 0, 0},
@@ -291,6 +294,9 @@ hdrtoken_hash(const unsigned char *string, unsigned int length)
 /*-------------------------------------------------------------------------
   -------------------------------------------------------------------------*/
 
+// WARNING:  Indexes into this array are stored on disk for cached objects.  New strings must be added at the end of the array to
+// avoid changing the indexes of pre-existing entries, unless the cache format version number is increased.
+//
 static const char *_hdrtoken_commonly_tokenized_strs[] = {
   // MIME Field names
   "Accept-Charset", "Accept-Encoding", "Accept-Language", "Accept-Ranges", "Accept", "Age", "Allow",
@@ -352,7 +358,9 @@ static const char *_hdrtoken_commonly_tokenized_strs[] = {
 
   // Header extensions
   "X-ID", "X-Forwarded-For", "TE", "Strict-Transport-Security", "100-continue",
-};
+
+  // RFC-2739
+  "Forwarded"};
 
 /*-------------------------------------------------------------------------
   -------------------------------------------------------------------------*/
diff --git a/proxy/hdrs/MIME.cc b/proxy/hdrs/MIME.cc
index e2b0c1b..18de0f6 100644
--- a/proxy/hdrs/MIME.cc
+++ b/proxy/hdrs/MIME.cc
@@ -148,6 +148,7 @@ const char *MIME_FIELD_XREF;
 const char *MIME_FIELD_ATS_INTERNAL;
 const char *MIME_FIELD_X_ID;
 const char *MIME_FIELD_X_FORWARDED_FOR;
+const char *MIME_FIELD_FORWARDED;
 const char *MIME_FIELD_SEC_WEBSOCKET_KEY;
 const char *MIME_FIELD_SEC_WEBSOCKET_VERSION;
 const char *MIME_FIELD_HTTP2_SETTINGS;
@@ -263,6 +264,7 @@ int MIME_LEN_XREF;
 int MIME_LEN_ATS_INTERNAL;
 int MIME_LEN_X_ID;
 int MIME_LEN_X_FORWARDED_FOR;
+int MIME_LEN_FORWARDED;
 int MIME_LEN_SEC_WEBSOCKET_KEY;
 int MIME_LEN_SEC_WEBSOCKET_VERSION;
 int MIME_LEN_HTTP2_SETTINGS;
@@ -341,6 +343,7 @@ int MIME_WKSIDX_XREF;
 int MIME_WKSIDX_ATS_INTERNAL;
 int MIME_WKSIDX_X_ID;
 int MIME_WKSIDX_X_FORWARDED_FOR;
+int MIME_WKSIDX_FORWARDED;
 int MIME_WKSIDX_SEC_WEBSOCKET_KEY;
 int MIME_WKSIDX_SEC_WEBSOCKET_VERSION;
 int MIME_WKSIDX_HTTP2_SETTINGS;
@@ -733,6 +736,7 @@ mime_init()
     MIME_FIELD_ATS_INTERNAL              = hdrtoken_string_to_wks("@Ats-Internal");
     MIME_FIELD_X_ID                      = hdrtoken_string_to_wks("X-ID");
     MIME_FIELD_X_FORWARDED_FOR           = hdrtoken_string_to_wks("X-Forwarded-For");
+    MIME_FIELD_FORWARDED                 = hdrtoken_string_to_wks("Forwarded");
 
     MIME_FIELD_SEC_WEBSOCKET_KEY     = hdrtoken_string_to_wks("Sec-WebSocket-Key");
     MIME_FIELD_SEC_WEBSOCKET_VERSION = hdrtoken_string_to_wks("Sec-WebSocket-Version");
@@ -813,6 +817,7 @@ mime_init()
     MIME_LEN_ATS_INTERNAL              = hdrtoken_wks_to_length(MIME_FIELD_ATS_INTERNAL);
     MIME_LEN_X_ID                      = hdrtoken_wks_to_length(MIME_FIELD_X_ID);
     MIME_LEN_X_FORWARDED_FOR           = hdrtoken_wks_to_length(MIME_FIELD_X_FORWARDED_FOR);
+    MIME_LEN_FORWARDED                 = hdrtoken_wks_to_length(MIME_FIELD_FORWARDED);
 
     MIME_LEN_SEC_WEBSOCKET_KEY     = hdrtoken_wks_to_length(MIME_FIELD_SEC_WEBSOCKET_KEY);
     MIME_LEN_SEC_WEBSOCKET_VERSION = hdrtoken_wks_to_length(MIME_FIELD_SEC_WEBSOCKET_VERSION);
@@ -892,6 +897,7 @@ mime_init()
     MIME_WKSIDX_XREF                      = hdrtoken_wks_to_index(MIME_FIELD_XREF);
     MIME_WKSIDX_X_ID                      = hdrtoken_wks_to_index(MIME_FIELD_X_ID);
     MIME_WKSIDX_X_FORWARDED_FOR           = hdrtoken_wks_to_index(MIME_FIELD_X_FORWARDED_FOR);
+    MIME_WKSIDX_FORWARDED                 = hdrtoken_wks_to_index(MIME_FIELD_FORWARDED);
     MIME_WKSIDX_SEC_WEBSOCKET_KEY         = hdrtoken_wks_to_index(MIME_FIELD_SEC_WEBSOCKET_KEY);
     MIME_WKSIDX_SEC_WEBSOCKET_VERSION     = hdrtoken_wks_to_index(MIME_FIELD_SEC_WEBSOCKET_VERSION);
     MIME_WKSIDX_HTTP2_SETTINGS            = hdrtoken_wks_to_index(MIME_FIELD_HTTP2_SETTINGS);
diff --git a/proxy/hdrs/MIME.h b/proxy/hdrs/MIME.h
index 7df16ec..207cb15 100644
--- a/proxy/hdrs/MIME.h
+++ b/proxy/hdrs/MIME.h
@@ -388,6 +388,7 @@ extern const char *MIME_FIELD_XREF;
 extern const char *MIME_FIELD_ATS_INTERNAL;
 extern const char *MIME_FIELD_X_ID;
 extern const char *MIME_FIELD_X_FORWARDED_FOR;
+extern const char *MIME_FIELD_FORWARDED;
 extern const char *MIME_FIELD_SEC_WEBSOCKET_KEY;
 extern const char *MIME_FIELD_SEC_WEBSOCKET_VERSION;
 extern const char *MIME_FIELD_HTTP2_SETTINGS;
@@ -491,6 +492,7 @@ extern int MIME_LEN_XREF;
 extern int MIME_LEN_ATS_INTERNAL;
 extern int MIME_LEN_X_ID;
 extern int MIME_LEN_X_FORWARDED_FOR;
+extern int MIME_LEN_FORWARDED;
 
 extern int MIME_LEN_BYTES;
 extern int MIME_LEN_CHUNKED;
diff --git a/proxy/http/ForwardedConfig.cc b/proxy/http/ForwardedConfig.cc
new file mode 100644
index 0000000..27ccb2f
--- /dev/null
+++ b/proxy/http/ForwardedConfig.cc
@@ -0,0 +1,189 @@
+/** @file
+
+  Configuration of Forwarded HTTP header option.
+
+  @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 <bitset>
+#include <string>
+#include <cctype>
+
+#include <ts/string_view.h>
+#include <ts/MemView.h>
+
+#include <HttpConfig.h>
+
+namespace
+{
+class BadOptionsErrMsg
+{
+public:
+  // Construct with referece to string that will contain error message.
+  //
+  BadOptionsErrMsg(ts::FixedBufferWriter &err) : _err(err), _count(0) {}
+
+  // Add a bad option.
+  //
+  void
+  add(ts::StringView badOpt)
+  {
+    if (_count == 0) {
+      _err << "\"Forwarded\" configuration: ";
+      _addQuoted(badOpt);
+      _count = 1;
+    } else if (_count == 1) {
+      _saveLast = badOpt;
+      _count    = 2;
+    } else {
+      _err << ", ";
+      _addQuoted(_saveLast);
+      _saveLast = badOpt;
+      ++_count;
+    }
+  }
+
+  // Returns true it error seen.
+  //
+  bool
+  done()
+  {
+    if (_count == 0) {
+      return false;
+    }
+
+    if (_count == 1) {
+      _err << " is a bad option.";
+
+    } else if (_count != 0) {
+      _err << " and ";
+      _addQuoted(_saveLast);
+      _err << " are bad options.";
+    }
+    return true;
+  }
+
+private:
+  void
+  _addQuoted(ts::StringView sv)
+  {
+    _err << '\"' << ts::string_view(sv.begin(), sv.size()) << '\"';
+  }
+
+  ts::FixedBufferWriter &_err;
+
+  ts::StringView _saveLast;
+
+  int _count;
+};
+
+// Compare a StringView to a nul-termimated string, converting the StringView to lower case and ignoring whitespace in it.
+//
+bool
+eqIgnoreCaseWs(ts::StringView sv, const char *target)
+{
+  const char *s = sv.begin();
+
+  std::size_t skip = 0;
+  std::size_t i    = 0;
+
+  while ((i + skip) < sv.size()) {
+    if (std::isspace(s[i + skip])) {
+      ++skip;
+    } else if (std::tolower(s[i + skip]) != target[i]) {
+      return false;
+    } else {
+      ++i;
+    }
+  }
+
+  return target[i] == '\0';
+}
+
+} // end anonymous namespace
+
+namespace HttpForwarded
+{
+OptionBitSet
+optStrToBitset(ts::string_view optConfigStr, ts::FixedBufferWriter &error)
+{
+  const ts::StringView Delimiters(":|");
+
+  OptionBitSet optBS;
+
+  // Convert to TS StringView to be able to use parsing members.
+  //
+  ts::StringView oCS(optConfigStr.data(), optConfigStr.size());
+
+  if (eqIgnoreCaseWs(oCS, "none")) {
+    return OptionBitSet();
+  }
+
+  BadOptionsErrMsg em(error);
+
+  do {
+    ts::StringView optStr = oCS.extractPrefix(Delimiters);
+
+    if (eqIgnoreCaseWs(optStr, "for")) {
+      optBS.set(FOR);
+
+    } else if (eqIgnoreCaseWs(optStr, "by=ip")) {
+      optBS.set(BY_IP);
+
+    } else if (eqIgnoreCaseWs(optStr, "by=unknown")) {
+      optBS.set(BY_UNKNOWN);
+
+    } else if (eqIgnoreCaseWs(optStr, "by=servername")) {
+      optBS.set(BY_SERVER_NAME);
+
+    } else if (eqIgnoreCaseWs(optStr, "by=uuid")) {
+      optBS.set(BY_UUID);
+
+    } else if (eqIgnoreCaseWs(optStr, "proto")) {
+      optBS.set(PROTO);
+
+    } else if (eqIgnoreCaseWs(optStr, "host")) {
+      optBS.set(HOST);
+
+    } else if (eqIgnoreCaseWs(optStr, "connection=compact")) {
+      optBS.set(CONNECTION_COMPACT);
+
+    } else if (eqIgnoreCaseWs(optStr, "connection=std")) {
+      optBS.set(CONNECTION_STD);
+
+    } else if (eqIgnoreCaseWs(optStr, "connection=standard")) {
+      optBS.set(CONNECTION_STD);
+
+    } else if (eqIgnoreCaseWs(optStr, "connection=full")) {
+      optBS.set(CONNECTION_FULL);
+
+    } else {
+      em.add(optStr);
+    }
+  } while (oCS);
+
+  if (em.done()) {
+    return OptionBitSet();
+  }
+
+  return optBS;
+
+} // end optStrToBitset()
+
+} // end namespace HttpForwarded
diff --git a/proxy/http/HttpConfig.cc b/proxy/http/HttpConfig.cc
index 169439d..1ab8796 100644
--- a/proxy/http/HttpConfig.cc
+++ b/proxy/http/HttpConfig.cc
@@ -179,6 +179,33 @@ http_server_session_sharing_cb(const char *name, RecDataT dtype, RecData data, v
   return REC_ERR_OKAY;
 }
 
+static int
+http_insert_forwarded_cb(const char *name, RecDataT dtype, RecData data, void *cookie)
+{
+  bool valid_p        = false;
+  HttpConfigParams *c = static_cast<HttpConfigParams *>(cookie);
+
+  if (0 == strcasecmp("proxy.config.http.insert_forwarded", name)) {
+    if (RECD_STRING == dtype) {
+      ts::LocalBufferWriter<1024> error;
+      HttpForwarded::OptionBitSet bs = HttpForwarded::optStrToBitset(ts::string_view(data.rec_string), error);
+      if (!error.size()) {
+        c->oride.insert_forwarded = bs;
+        valid_p                   = true;
+      } else {
+        Error("HTTP %.*s", static_cast<int>(error.size()), error.data());
+      }
+    }
+  }
+
+  // Signal an update if valid value arrived.
+  if (valid_p) {
+    http_config_cb(name, dtype, data, cookie);
+  }
+
+  return REC_ERR_OKAY;
+}
+
 void
 register_stat_callbacks()
 {
@@ -938,6 +965,21 @@ HttpConfig::startup()
                         c.oride.server_session_sharing_match);
   http_config_enum_read("proxy.config.http.server_session_sharing.pool", SessionSharingPoolStrings, c.server_session_sharing_pool);
 
+  RecRegisterConfigUpdateCb("proxy.config.http.insert_forwarded", &http_insert_forwarded_cb, &c);
+  {
+    char str[512];
+
+    if (REC_ERR_OKAY == RecGetRecordString("proxy.config.http.insert_forwarded", str, sizeof(str))) {
+      ts::LocalBufferWriter<1024> error;
+      HttpForwarded::OptionBitSet bs = HttpForwarded::optStrToBitset(ts::string_view(str), error);
+      if (!error.size()) {
+        c.oride.insert_forwarded = bs;
+      } else {
+        Error("HTTP %.*s", static_cast<int>(error.size()), error.data());
+      }
+    }
+  }
+
   HttpEstablishStaticConfigByte(c.oride.auth_server_session_private, "proxy.config.http.auth_server_session_private");
 
   HttpEstablishStaticConfigByte(c.oride.keep_alive_post_out, "proxy.config.http.keep_alive_post_out");
@@ -1278,6 +1320,7 @@ HttpConfig::reconfigure()
   params->oride.proxy_response_server_enabled = m_master.oride.proxy_response_server_enabled;
 
   params->oride.insert_squid_x_forwarded_for = INT_TO_BOOL(m_master.oride.insert_squid_x_forwarded_for);
+  params->oride.insert_forwarded             = m_master.oride.insert_forwarded;
   params->oride.insert_age_in_response       = INT_TO_BOOL(m_master.oride.insert_age_in_response);
   params->enable_http_stats                  = INT_TO_BOOL(m_master.enable_http_stats);
   params->oride.normalize_ae                 = m_master.oride.normalize_ae;
diff --git a/proxy/http/HttpConfig.h b/proxy/http/HttpConfig.h
index bd4c4f7..b4e1d81 100644
--- a/proxy/http/HttpConfig.h
+++ b/proxy/http/HttpConfig.h
@@ -36,6 +36,7 @@
 
 #include <stdlib.h>
 #include <stdio.h>
+#include <bitset>
 
 #ifdef HAVE_CTYPE_H
 #include <ctype.h>
@@ -44,6 +45,8 @@
 #include "ts/ink_platform.h"
 #include "ts/ink_inet.h"
 #include "ts/Regex.h"
+#include "ts/string_view.h"
+#include "ts/BufferWriter.h"
 #include "HttpProxyAPIEnums.h"
 #include "ProxyConfig.h"
 #include "P_RecProcess.h"
@@ -358,6 +361,34 @@ struct HttpConfigPortRange {
   }
 };
 
+namespace HttpForwarded
+{
+// Options for what parameters will be included in "Forwarded" field header.
+//
+enum Option {
+  FOR,
+  BY_IP,              // by=<numeric IP address>.
+  BY_UNKNOWN,         // by=unknown.
+  BY_SERVER_NAME,     // by=<configured server name>.
+  BY_UUID,            // Obfuscated value for by, by=_<UUID>.
+  PROTO,              // Basic protocol (http, https) of incoming message.
+  HOST,               // Host from URL before any remapping.
+  CONNECTION_COMPACT, // Same value as 'proto' parameter.
+  CONNECTION_STD,     // Verbose protocol from Via: field, with dashes instead of spaces.
+  CONNECTION_FULL,    // Ultra-verbose protocol from Via: field, with dashes instead of spaces.
+
+  NUM_OPTIONS // Number of options.
+};
+
+using OptionBitSet = std::bitset<NUM_OPTIONS>;
+
+// Converts string specifier for Forwarded options to bitset of options, and return the result.  If there are errors, an error
+// message will be inserted into 'error'.
+//
+OptionBitSet optStrToBitset(ts::string_view optConfigStr, ts::FixedBufferWriter &error);
+
+} // end HttpForwarded namespace
+
 /////////////////////////////////////////////////////////////
 // This is a little helper class, used by the HttpConfigParams
 // and State (txn) structure. It allows for certain configs
@@ -388,6 +419,7 @@ struct OverridableHttpConfigParams {
       proxy_response_server_enabled(1),
       proxy_response_hsts_include_subdomains(0),
       insert_squid_x_forwarded_for(1),
+      insert_forwarded(HttpForwarded::OptionBitSet()),
       send_http11_requests(1),
       cache_http(1),
       cache_ignore_client_no_cache(1),
@@ -528,6 +560,11 @@ struct OverridableHttpConfigParams {
   /////////////////////
   MgmtByte insert_squid_x_forwarded_for;
 
+  ///////////////
+  // Forwarded //
+  ///////////////
+  HttpForwarded::OptionBitSet insert_forwarded;
+
   //////////////////////
   //  Version Hell    //
   //////////////////////
diff --git a/proxy/http/HttpTransact.cc b/proxy/http/HttpTransact.cc
index 51cdcd3..2c26aee 100644
--- a/proxy/http/HttpTransact.cc
+++ b/proxy/http/HttpTransact.cc
@@ -7536,6 +7536,7 @@ HttpTransact::build_request(State *s, HTTPHdr *base_request, HTTPHdr *outgoing_r
 
   HttpTransactHeaders::copy_header_fields(base_request, outgoing_request, s->txn_conf->fwd_proxy_auth_to_parent);
   add_client_ip_to_outgoing_request(s, outgoing_request);
+  HttpTransactHeaders::add_forwarded_field_to_request(s, outgoing_request);
   HttpTransactHeaders::remove_privacy_headers_from_request(s->http_config_param, s->txn_conf, outgoing_request);
   HttpTransactHeaders::add_global_user_agent_header_to_request(s->txn_conf, outgoing_request);
   handle_request_keep_alive_headers(s, outgoing_version, outgoing_request);
diff --git a/proxy/http/HttpTransactHeaders.cc b/proxy/http/HttpTransactHeaders.cc
index a8cb790..358dd45 100644
--- a/proxy/http/HttpTransactHeaders.cc
+++ b/proxy/http/HttpTransactHeaders.cc
@@ -20,7 +20,12 @@
   See the License for the specific language governing permissions and
   limitations under the License.
  */
-#include "ts/ink_platform.h"
+
+#include <bitset>
+#include <algorithm>
+
+#include <ts/ink_platform.h>
+#include <ts/BufferWriter.h>
 
 #include "HttpTransact.h"
 #include "HttpTransactHeaders.h"
@@ -686,50 +691,56 @@ HttpTransactHeaders::insert_server_header_in_response(const char *server_tag, in
 
 /// write the protocol stack to the @a via_string.
 /// If @a detailed then do the full stack, otherwise just the "top level" protocol.
-size_t
-write_via_protocol_stack(char *via_string, size_t len, bool detailed, ts::StringView *proto_buf, int n_proto)
+/// Returns the number of characters appended to hdr_string (no nul appended).
+int
+HttpTransactHeaders::write_hdr_protocol_stack(char *hdr_string, size_t len, ProtocolStackDetail pSDetail, ts::StringView *proto_buf,
+                                              int n_proto, char separator)
 {
-  char *via   = via_string; // keep original pointer for size computation later.
-  char *limit = via_string + len;
+  char *hdr   = hdr_string; // keep original pointer for size computation later.
+  char *limit = hdr_string + len;
   static constexpr ts::StringView tls_prefix{"tls/", ts::StringView::literal};
 
-  if (n_proto <= 0 || via == nullptr || len <= 0) {
+  if (n_proto <= 0 || hdr == nullptr || len <= 0) {
     // nothing
-  } else if (detailed) {
-    for (ts::StringView *v = proto_buf, *v_limit = proto_buf + n_proto; v < v_limit && (via + v->size() + 1) < limit; ++v) {
+  } else if (ProtocolStackDetail::Full == pSDetail) {
+    for (ts::StringView *v = proto_buf, *v_limit = proto_buf + n_proto; v < v_limit && (hdr + v->size() + 1) < limit; ++v) {
       if (v != proto_buf) {
-        *via++ = ' ';
+        *hdr++ = separator;
       }
-      memcpy(via, v->ptr(), v->size());
-      via += v->size();
+      memcpy(hdr, v->ptr(), v->size());
+      hdr += v->size();
     }
   } else {
     ts::StringView *proto_end = proto_buf + n_proto;
     bool http_1_0_p           = std::find(proto_buf, proto_end, IP_PROTO_TAG_HTTP_1_0) != proto_end;
     bool http_1_1_p           = std::find(proto_buf, proto_end, IP_PROTO_TAG_HTTP_1_1) != proto_end;
 
-    if ((http_1_0_p || http_1_1_p) && via + 10 < limit) {
+    if ((http_1_0_p || http_1_1_p) && hdr + 10 < limit) {
       bool tls_p = std::find_if(proto_buf, proto_end, [](ts::StringView tag) { return tls_prefix.isPrefixOf(tag); }) != proto_end;
-      bool http_2_p = std::find(proto_buf, proto_end, IP_PROTO_TAG_HTTP_2_0) != proto_end;
 
-      memcpy(via, "http", 4);
-      via += 4;
+      memcpy(hdr, "http", 4);
+      hdr += 4;
       if (tls_p)
-        *via++ = 's';
-      *via++   = '/';
-      if (http_2_p) {
-        *via++ = '2';
-      } else if (http_1_0_p) {
-        memcpy(via, "1.0", 3);
-        via += 3;
-      } else if (http_1_1_p) {
-        memcpy(via, "1.1", 3);
-        via += 3;
+        *hdr++ = 's';
+
+      // If detail level is compact (RFC 7239 compliant "proto" value for Forwarded field), stop here.
+
+      if (ProtocolStackDetail::Standard == pSDetail) {
+        *hdr++        = '/';
+        bool http_2_p = std::find(proto_buf, proto_end, IP_PROTO_TAG_HTTP_2_0) != proto_end;
+        if (http_2_p) {
+          *hdr++ = '2';
+        } else if (http_1_0_p) {
+          memcpy(hdr, "1.0", 3);
+          hdr += 3;
+        } else if (http_1_1_p) {
+          memcpy(hdr, "1.1", 3);
+          hdr += 3;
+        }
       }
-      *via++ = ' ';
     }
   }
-  return via - via_string;
+  return hdr - hdr_string;
 }
 
 ///////////////////////////////////////////////////////////////////////////////
@@ -792,7 +803,10 @@ HttpTransactHeaders::insert_via_header_in_request(HttpTransact::State *s, HTTPHd
   std::array<ts::StringView, 10> proto_buf; // 10 seems like a reasonable number of protos to print
   int n_proto = s->state_machine->populate_client_protocol(proto_buf.data(), proto_buf.size());
 
-  via_string += write_via_protocol_stack(via_string, via_limit - via_string, false, proto_buf.data(), n_proto);
+  via_string +=
+    write_hdr_protocol_stack(via_string, via_limit - via_string, ProtocolStackDetail::Standard, proto_buf.data(), n_proto);
+  *via_string++ = ' ';
+
   via_string += nstrcpy(via_string, s->http_config_param->proxy_hostname);
 
   *via_string++ = '[';
@@ -822,7 +836,8 @@ HttpTransactHeaders::insert_via_header_in_request(HttpTransact::State *s, HTTPHd
     if (via_limit - via_string > 4 && s->txn_conf->insert_request_via_string > 3) { // Ultra highest verbosity
       *via_string++ = ' ';
       *via_string++ = '[';
-      via_string += write_via_protocol_stack(via_string, via_limit - via_string - 3, true, proto_buf.data(), n_proto);
+      via_string +=
+        write_hdr_protocol_stack(via_string, via_limit - via_string - 3, ProtocolStackDetail::Full, proto_buf.data(), n_proto);
       *via_string++ = ']';
     }
   }
@@ -877,7 +892,9 @@ HttpTransactHeaders::insert_via_header_in_response(HttpTransact::State *s, HTTPH
   if (ss) {
     n_proto += ss->populate_protocol(proto_buf.data() + n_proto, proto_buf.size() - n_proto);
   }
-  via_string += write_via_protocol_stack(via_string, via_limit - via_string, false, proto_buf.data(), n_proto);
+  via_string +=
+    write_hdr_protocol_stack(via_string, via_limit - via_string, ProtocolStackDetail::Standard, proto_buf.data(), n_proto);
+  *via_string++ = ' ';
 
   via_string += nstrcpy(via_string, s->http_config_param->proxy_hostname);
   *via_string++ = ' ';
@@ -902,7 +919,8 @@ HttpTransactHeaders::insert_via_header_in_response(HttpTransact::State *s, HTTPH
     if (via_limit - via_string > 4 && s->txn_conf->insert_response_via_string > 3) { // Ultra highest verbosity
       *via_string++ = ' ';
       *via_string++ = '[';
-      via_string += write_via_protocol_stack(via_string, via_limit - via_string - 3, true, proto_buf.data(), n_proto);
+      via_string +=
+        write_hdr_protocol_stack(via_string, via_limit - via_string - 3, ProtocolStackDetail::Full, proto_buf.data(), n_proto);
       *via_string++ = ']';
     }
   }
@@ -996,6 +1014,191 @@ HttpTransactHeaders::add_global_user_agent_header_to_request(OverridableHttpConf
 }
 
 void
+HttpTransactHeaders::add_forwarded_field_to_request(HttpTransact::State *s, HTTPHdr *request)
+{
+  HttpForwarded::OptionBitSet optSet = s->txn_conf->insert_forwarded;
+
+  if (optSet.any()) { // One or more Forwarded parameters enabled, so insert/append to Forwarded header.
+
+    ts::LocalBufferWriter<1024> hdr;
+
+    if (optSet[HttpForwarded::FOR] and ats_is_ip(&s->client_info.src_addr.sa)) {
+      // NOTE:  The logic within this if statement assumes that hdr is empty at this point.
+
+      hdr << "for=";
+
+      bool is_ipv6 = ats_is_ip6(&s->client_info.src_addr.sa);
+
+      if (is_ipv6) {
+        hdr << "\"[";
+      }
+
+      if (ats_ip_ntop(&s->client_info.src_addr.sa, hdr.auxBuffer(), hdr.remaining()) == nullptr) {
+        Debug("http_trans", "[add_forwarded_field_to_outgoing_request] ats_ip_ntop() call failed");
+        return;
+      }
+
+      // Fail-safe.
+      hdr.auxBuffer()[hdr.remaining() - 1] = '\0';
+
+      hdr.write(strlen(hdr.auxBuffer()));
+
+      if (is_ipv6) {
+        hdr << "]\"";
+      }
+    }
+
+    if (optSet[HttpForwarded::BY_UNKNOWN]) {
+      if (hdr.size()) {
+        hdr << ';';
+      }
+
+      hdr << "by=unknown";
+    }
+
+    if (optSet[HttpForwarded::BY_SERVER_NAME]) {
+      if (hdr.size()) {
+        hdr << ';';
+      }
+
+      hdr << "by=" << s->http_config_param->proxy_hostname;
+    }
+
+    const Machine &m = *Machine::instance();
+
+    if (optSet[HttpForwarded::BY_UUID] and m.uuid.valid()) {
+      if (hdr.size()) {
+        hdr << ';';
+      }
+
+      hdr << "by=_" << m.uuid.getString();
+    }
+
+    if (optSet[HttpForwarded::BY_IP] and (m.ip_string_len > 0)) {
+      if (hdr.size()) {
+        hdr << ';';
+      }
+
+      hdr << "by=";
+
+      bool is_ipv6 = ats_is_ip6(&s->client_info.dst_addr.sa);
+
+      if (is_ipv6) {
+        hdr << "\"[";
+      }
+
+      if (ats_ip_ntop(&s->client_info.dst_addr.sa, hdr.auxBuffer(), hdr.remaining()) == nullptr) {
+        Debug("http_trans", "[add_forwarded_field_to_outgoing_request] ats_ip_ntop() call failed");
+        return;
+      }
+
+      // Fail-safe.
+      hdr.auxBuffer()[hdr.remaining() - 1] = '\0';
+
+      hdr.write(strlen(hdr.auxBuffer()));
+
+      if (is_ipv6) {
+        hdr << "]\"";
+      }
+    }
+
+    std::array<ts::StringView, 10> protoBuf; // 10 seems like a reasonable number of protos to print
+    int nProto = 0;                          // Indulge clang's incorrect claim that this need to be initialized.
+
+    static const HttpForwarded::OptionBitSet OptionsNeedingProtocol = HttpForwarded::OptionBitSet()
+                                                                        .set(HttpForwarded::PROTO)
+                                                                        .set(HttpForwarded::CONNECTION_COMPACT)
+                                                                        .set(HttpForwarded::CONNECTION_STD)
+                                                                        .set(HttpForwarded::CONNECTION_FULL);
+
+    if ((optSet bitand OptionsNeedingProtocol).any()) {
+      nProto = s->state_machine->populate_client_protocol(protoBuf.data(), protoBuf.size());
+    }
+
+    if (optSet[HttpForwarded::PROTO] and (nProto > 0)) {
+      if (hdr.size()) {
+        hdr << ';';
+      }
+
+      hdr << "proto=";
+
+      int numChars = HttpTransactHeaders::write_hdr_protocol_stack(
+        hdr.auxBuffer(), hdr.remaining(), HttpTransactHeaders::ProtocolStackDetail::Compact, protoBuf.data(), nProto, '-');
+      if (numChars > 0) {
+        hdr.write(size_t(numChars));
+      }
+    }
+
+    if (optSet[HttpForwarded::HOST]) {
+      const MIMEField *hostField = s->hdr_info.client_request.field_find(MIME_FIELD_HOST, MIME_LEN_HOST);
+
+      if (hostField and hostField->m_len_value) {
+        ts::string_view hSV{hostField->m_ptr_value, hostField->m_len_value};
+
+        bool needsDoubleQuotes = hSV.find(':') != ts::string_view::npos;
+
+        if (hdr.size()) {
+          hdr << ';';
+        }
+
+        hdr << "host=";
+        if (needsDoubleQuotes) {
+          hdr << '"';
+        }
+        hdr << hSV;
+        if (needsDoubleQuotes) {
+          hdr << '"';
+        }
+      }
+    }
+
+    if (nProto > 0) {
+      auto Conn = [&](HttpForwarded::Option opt, HttpTransactHeaders::ProtocolStackDetail detail) -> void {
+        if (optSet[opt]) {
+          int revert = hdr.size();
+
+          if (hdr.size()) {
+            hdr << ';';
+          }
+
+          hdr << "connection=";
+
+          int numChars =
+            HttpTransactHeaders::write_hdr_protocol_stack(hdr.auxBuffer(), hdr.remaining(), detail, protoBuf.data(), nProto, '-');
+          if (numChars > 0) {
+            hdr.write(size_t(numChars));
+          }
+
+          if ((numChars <= 0) or (hdr.size() >= hdr.capacity())) {
+            // Remove parameter with potentially incomplete value.
+            //
+            hdr.reduce(revert);
+          }
+        }
+      };
+
+      Conn(HttpForwarded::CONNECTION_COMPACT, HttpTransactHeaders::ProtocolStackDetail::Compact);
+      Conn(HttpForwarded::CONNECTION_STD, HttpTransactHeaders::ProtocolStackDetail::Standard);
+      Conn(HttpForwarded::CONNECTION_FULL, HttpTransactHeaders::ProtocolStackDetail::Full);
+    }
+
+    // Add or append to the Forwarded header.  As a fail-safe against corrupting the MIME header, don't add Forwarded if
+    // it's size is exactly the capacity of the buffer.
+    //
+    if (hdr.size() and !hdr.error() and (hdr.size() < hdr.capacity())) {
+      ts::string_view sV = hdr.view();
+
+      request->value_append(MIME_FIELD_FORWARDED, MIME_LEN_FORWARDED, sV.data(), sV.size(), true, ','); // true => separator must
+                                                                                                        // be inserted
+
+      Debug("http_trans", "[add_forwarded_field_to_outgoing_request] Forwarded header (%.*s) added", static_cast<int>(hdr.size()),
+            hdr.data());
+    }
+  }
+
+} // end HttpTransact::add_forwarded_field_to_outgoing_request()
+
+void
 HttpTransactHeaders::add_server_header_to_response(OverridableHttpConfigParams *http_txn_conf, HTTPHdr *header)
 {
   if (http_txn_conf->proxy_response_server_enabled && http_txn_conf->proxy_response_server_string) {
diff --git a/proxy/http/HttpTransactHeaders.h b/proxy/http/HttpTransactHeaders.h
index f4b243d..57d468f 100644
--- a/proxy/http/HttpTransactHeaders.h
+++ b/proxy/http/HttpTransactHeaders.h
@@ -62,6 +62,11 @@ public:
 
   static void generate_and_set_squid_codes(HTTPHdr *header, char *via_string, HttpTransact::SquidLogInfo *squid_codes);
 
+  enum class ProtocolStackDetail { Compact, Standard, Full };
+
+  static int write_hdr_protocol_stack(char *hdr_string, size_t len, ProtocolStackDetail pSDetail, ts::StringView *proto_buf,
+                                      int n_proto, char separator = ' ');
+
   // Removing handle_conditional_headers.  Functionality appears to be elsewhere (issue_revalidate)
   // and the only condition when it does anything causes an assert to go
   // off
@@ -75,6 +80,8 @@ public:
   static void insert_via_header_in_response(HttpTransact::State *s, HTTPHdr *header);
   static void insert_hsts_header_in_response(HttpTransact::State *s, HTTPHdr *header);
 
+  static void add_forwarded_field_to_request(HttpTransact::State *s, HTTPHdr *request);
+
   static bool is_request_proxy_authorized(HTTPHdr *incoming_hdr);
 
   static void normalize_accept_encoding(const OverridableHttpConfigParams *ohcp, HTTPHdr *header);
diff --git a/proxy/http/Makefile.am b/proxy/http/Makefile.am
index fc8ee91..a08737e 100644
--- a/proxy/http/Makefile.am
+++ b/proxy/http/Makefile.am
@@ -72,13 +72,28 @@ libhttp_a_SOURCES = \
   HttpTunnel.cc \
   HttpTunnel.h \
   HttpUpdateSM.cc \
-  HttpUpdateSM.h
+  HttpUpdateSM.h \
+  ForwardedConfig.cc
 
 if BUILD_TESTS
   libhttp_a_SOURCES += HttpUpdateTester.cc \
     RegressionHttpTransact.cc
 endif
 
+check_PROGRAMS = \
+test_ForwardedConfig
+
+TESTS = $(check_PROGRAMS)
+
+test_ForwardedConfig_CPPFLAGS = $(AM_CPPFLAGS)\
+  -I$(abs_top_srcdir)/tests/include
+
+test_ForwardedConfig_SOURCES = \
+  unit-tests/test_ForwardedConfig.cc \
+  ForwardedConfig.cc \
+  unit-tests/test_ForwardedConfig_mocks.cc \
+  unit-tests/sym-links/MemView.cc
+
 tidy-local: $(libhttp_a_SOURCES) $(noinst_HEADERS)
 	$(CXX_Clang_Tidy)
 
diff --git a/proxy/http/unit-tests/sym-links/MemView.cc b/proxy/http/unit-tests/sym-links/MemView.cc
new file mode 120000
index 0000000..51e80fb
--- /dev/null
+++ b/proxy/http/unit-tests/sym-links/MemView.cc
@@ -0,0 +1 @@
+../../../../lib/ts/MemView.cc
\ No newline at end of file
diff --git a/proxy/http/unit-tests/test_ForwardedConfig.cc b/proxy/http/unit-tests/test_ForwardedConfig.cc
new file mode 100644
index 0000000..712eb9b
--- /dev/null
+++ b/proxy/http/unit-tests/test_ForwardedConfig.cc
@@ -0,0 +1,169 @@
+/** @file
+
+  Catch-based tests for ForwardedConfig.cc.
+
+  @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 <string>
+#include <cstring>
+#include <cctype>
+#include <bitset>
+#include <initializer_list>
+
+#define CATCH_CONFIG_MAIN
+#include "catch.hpp"
+
+#include "HttpConfig.h"
+
+using namespace HttpForwarded;
+
+class OptionBitSetListInit : public OptionBitSet
+{
+public:
+  OptionBitSetListInit(std::initializer_list<std::size_t> il)
+  {
+    for (std::size_t i : il) {
+      this->set(i);
+    }
+  }
+};
+
+namespace
+{
+const char *wsTbl[] = {"", " ", "  ", nullptr};
+
+int wsIdx{0};
+
+const char *
+nextWs()
+{
+  ++wsIdx;
+
+  if (!wsTbl[wsIdx]) {
+    wsIdx = 0;
+  }
+
+  return wsTbl[wsIdx];
+}
+// Alternate upper/lower case and add blanks.
+class XS
+{
+private:
+  std::string s;
+
+public:
+  XS(const char *in) : s{nextWs()}
+  {
+    bool upper{true};
+    for (; *in; ++in) {
+      if (islower(*in)) {
+        s += upper ? toupper(*in) : *in;
+        upper = !upper;
+
+      } else if (isupper(*in)) {
+        s += upper ? *in : tolower(*in);
+        upper = !upper;
+
+      } else {
+        s += *in;
+      }
+      s += nextWs();
+    }
+    s += nextWs();
+  }
+
+  operator ts::string_view() const { return ts::string_view(s.c_str()); }
+};
+
+void
+test(const char *spec, const char *reqErr, OptionBitSet bS)
+{
+  ts::LocalBufferWriter<1024> error;
+
+  error << "cheese";
+
+  REQUIRE(bS == optStrToBitset(XS(spec), error));
+  std::size_t len = std::strlen(reqErr);
+  REQUIRE((error.size() - sizeof("cheese") + 1) == len);
+  REQUIRE(std::memcmp(error.data() + sizeof("cheese") - 1, reqErr, len) == 0);
+}
+
+} // end annonymous namespace
+
+TEST_CASE("Forwarded", "[FWD]")
+{
+  test("none", "", OptionBitSet());
+
+  test("", "\"Forwarded\" configuration: \"   \" is a bad option.", OptionBitSet());
+
+  test("\t", "\"Forwarded\" configuration: \"\t   \" is a bad option.", OptionBitSet());
+
+  test(":", "\"Forwarded\" configuration: \"   \" is a bad option.", OptionBitSet());
+
+  test("|", "\"Forwarded\" configuration: \"   \" is a bad option.", OptionBitSet());
+
+  test("by=ip", "", OptionBitSetListInit{BY_IP});
+
+  test("by=unknown", "", OptionBitSetListInit{BY_UNKNOWN});
+
+  test("by=servername", "", OptionBitSetListInit{BY_SERVER_NAME});
+
+  test("by=uuid", "", OptionBitSetListInit{BY_UUID});
+
+  test("for", "", OptionBitSetListInit{FOR});
+
+  test("proto", "", OptionBitSetListInit{PROTO});
+
+  test("host", "", OptionBitSetListInit{HOST});
+
+  test("connection=compact", "", OptionBitSetListInit{CONNECTION_COMPACT});
+
+  test("connection=standard", "", OptionBitSetListInit{CONNECTION_STD});
+
+  test("connection=std", "", OptionBitSetListInit{CONNECTION_STD});
+
+  test("connection=full", "", OptionBitSetListInit{CONNECTION_FULL});
+
+  test("proto:by=uuid|for", "", OptionBitSetListInit{PROTO, BY_UUID, FOR});
+
+  test("proto:by=cheese|fur", "\"Forwarded\" configuration: \" b  Y= c  He E  sE \" and \"  fU r  \" are bad options.",
+       OptionBitSet());
+
+  test("proto:by=cheese|fur|compact=",
+       "\"Forwarded\" configuration: \" b  Y= c  He E  sE \", \"  fU r  \" and \"C o  Mp A  cT =  \" are bad options.",
+       OptionBitSet());
+
+#undef X
+#define X(S)                                                                                                                  \
+  "by=ip" S "by=unknown" S "by=servername" S "by=uuid" S "for" S "proto" S "host" S "connection=compact" S "connection=std" S \
+  "connection=full"
+
+  test(X(":"), "", OptionBitSet().set());
+
+  test(X("|"), "", OptionBitSet().set());
+
+  test(X("|") "|" X(":"), "", OptionBitSet().set());
+
+  test(X("|") ":abcd", "\"Forwarded\" configuration: \"  aB c  D \" is a bad option.", OptionBitSet());
+
+  test(X("|") ":for=abcd", "\"Forwarded\" configuration: \" f  Or =  Ab C  d \" is a bad option.", OptionBitSet());
+
+  test(X("|") ":by", "\"Forwarded\" configuration: \" b  Y \" is a bad option.", OptionBitSet());
+}
diff --git a/proxy/http/unit-tests/test_ForwardedConfig_mocks.cc b/proxy/http/unit-tests/test_ForwardedConfig_mocks.cc
new file mode 100644
index 0000000..a0fe062
--- /dev/null
+++ b/proxy/http/unit-tests/test_ForwardedConfig_mocks.cc
@@ -0,0 +1,86 @@
+/** @file
+
+  Mocks for unit test of ForwardedConfig.cc
+
+  @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 <cstdlib>
+#include <iostream>
+
+#include <I_EventSystem.h>
+#include <I_Thread.h>
+
+void
+_ink_assert(const char *expression, const char *file, int line)
+{
+  std::cerr << "fatal error: ink_assert: file: " << file << " line: " << line << " expression: " << expression << std::endl;
+
+  std::exit(1);
+}
+
+namespace
+{
+void
+stub(const char *file, int line)
+{
+  std::cerr << "fatal error: call to link stub: file: " << file << " line: " << line << std::endl;
+
+  std::exit(1);
+}
+}
+
+#define STUB stub(__FILE__, __LINE__);
+
+inkcoreapi void
+ink_freelist_init(InkFreeList **fl, const char *name, uint32_t type_size, uint32_t chunk_size, uint32_t alignment)
+{
+}
+inkcoreapi void
+ink_freelist_free(InkFreeList *f, void *item){STUB} inkcoreapi
+  void ink_freelist_free_bulk(InkFreeList *f, void *head, void *tail, size_t num_item)
+{
+  STUB
+}
+void ink_mutex_destroy(pthread_mutex_t *){STUB} inkcoreapi ClassAllocator<ProxyMutex> mutexAllocator("ARGH");
+inkcoreapi ink_thread_key Thread::thread_data_key;
+volatile int res_track_memory;
+void ResourceTracker::increment(const char *, long){STUB} inkcoreapi Allocator ioBufAllocator[DEFAULT_BUFFER_SIZES];
+void
+ats_free(void *)
+{
+  STUB
+}
+int thread_freelist_high_watermark;
+int thread_freelist_low_watermark;
+inkcoreapi ClassAllocator<IOBufferBlock> ioBlockAllocator("ARGH");
+inkcoreapi ClassAllocator<IOBufferData> ioDataAllocator("ARGH");
+IOBufferBlock::IOBufferBlock()
+{
+}
+
+void
+IOBufferBlock::free()
+{
+}
+
+void
+IOBufferData::free()
+{
+}
diff --git a/tests/gold_tests/headers/forwarded-observer.py b/tests/gold_tests/headers/forwarded-observer.py
new file mode 100644
index 0000000..91b7baf
--- /dev/null
+++ b/tests/gold_tests/headers/forwarded-observer.py
@@ -0,0 +1,63 @@
+'''
+Extract the protocol information from the FORWARDED headers and store it in a log file for later verification.
+'''
+#  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 re
+import subprocess
+
+log = open('forwarded.log', 'w')
+
+regexByEqualUuid = re.compile("^by=_[0-9a-f-]+$")
+
+byCount = 0;
+byEqualUuid = "__INVALID__"
+
+def observe(headers):
+
+    global byCount
+    global byEqualUuid
+
+    seen = False
+    for h in headers.items():
+        if h[0].lower() == "forwarded":
+
+            content = h[1]
+
+            if content.startswith("by="):
+
+                byCount += 1
+
+                if ((byCount == 4) or (byCount == 5)) and regexByEqualUuid.match(content):  # "by" should give UUID
+
+                    # I don't think there is a way to know what UUID traffic_server generates, so I just do a crude format
+                    # check and make sure the same value is used consistently.
+
+                    byEqualUuid = content
+
+            content = content.replace(byEqualUuid, "__BY_EQUAL_UUID__", 1)
+
+            log.write(content + "\n")
+            seen = True
+
+    if not seen:
+        log.write("FORWARDED MISSING\n")
+    log.write("-\n")
+    log.flush()
+
+
+Hooks.register(Hooks.ReadRequestHook, observe)
diff --git a/tests/gold_tests/headers/forwarded.gold b/tests/gold_tests/headers/forwarded.gold
new file mode 100644
index 0000000..45451d6
--- /dev/null
+++ b/tests/gold_tests/headers/forwarded.gold
@@ -0,0 +1,41 @@
+FORWARDED MISSING
+-
+FORWARDED MISSING
+-
+for=127.0.0.1
+-
+by=127.0.0.1
+-
+by=unknown
+-
+by=Poxy_Proxy
+-
+__BY_EQUAL_UUID__
+-
+proto=http
+-
+host=www.forwarded-host.com
+-
+connection=http
+-
+connection=http/1.1
+-
+connection=http/1.1-tcp-ipv4
+-
+__BY_EQUAL_UUID__
+-
+for=127.0.0.1;by=unknown;by=Poxy_Proxy;__BY_EQUAL_UUID__;by=127.0.0.1;proto=http;host=www.no-oride.com;connection=http;connection=http/1.1;connection=http/1.1-tcp-ipv4
+-
+for=127.0.0.1;by=unknown;by=Poxy_Proxy;__BY_EQUAL_UUID__;by=127.0.0.1;proto=http;host=www.no-oride.com;connection=http;connection=http/1.0;connection=http/1.0-tcp-ipv4
+-
+for=0.6.6.6
+for=_argh, for=127.0.0.1;by=unknown;by=Poxy_Proxy;__BY_EQUAL_UUID__;by=127.0.0.1;proto=http;host=www.no-oride.com;connection=http;connection=http/1.0;connection=http/1.0-tcp-ipv4
+-
+for=127.0.0.1;by=unknown;by=Poxy_Proxy;__BY_EQUAL_UUID__;by=127.0.0.1;proto=https;host=www.no-oride.com;connection=https;connection=https/2;connection=http/1.1-h2-tls/1.2-tcp-ipv4
+-
+for=127.0.0.1;by=unknown;by=Poxy_Proxy;__BY_EQUAL_UUID__;by=127.0.0.1;proto=https;host=www.no-oride.com;connection=https;connection=https/1.1;connection=http/1.1-tls/1.2-tcp-ipv4
+-
+for="[::1]";by=unknown;by=Poxy_Proxy;__BY_EQUAL_UUID__;by="[::1]";proto=http;host=www.no-oride.com;connection=http;connection=http/1.1;connection=http/1.1-tcp-ipv6
+-
+for="[::1]";by=unknown;by=Poxy_Proxy;__BY_EQUAL_UUID__;by="[::1]";proto=https;host=www.no-oride.com;connection=https;connection=https/1.1;connection=http/1.1-tls/1.2-tcp-ipv6
+-
diff --git a/tests/gold_tests/headers/forwarded.test.py b/tests/gold_tests/headers/forwarded.test.py
new file mode 100644
index 0000000..e45a9d4
--- /dev/null
+++ b/tests/gold_tests/headers/forwarded.test.py
@@ -0,0 +1,289 @@
+'''
+Test the Forwarded header and related configuration..
+'''
+#  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 subprocess
+
+Test.Summary = '''
+Test FORWARDED header.
+'''
+
+Test.SkipUnless(
+    Condition.HasATSFeature('TS_USE_TLS_ALPN'),
+    Condition.HasCurlFeature('http2'),
+    Condition.HasCurlFeature('IPv6')
+)
+Test.ContinueOnFail = True
+
+testName = "FORWARDED"
+
+server = Test.MakeOriginServer("server", options={'--load': os.path.join(Test.TestDirectory, 'forwarded-observer.py')})
+
+request_header = {
+    "headers": "GET / HTTP/1.1\r\nHost: www.no-oride.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""}
+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)
+request_header = {
+    "headers": "GET / HTTP/1.1\r\nHost: www.forwarded-none.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""}
+server.addResponse("sessionlog.json", request_header, response_header)
+request_header = {
+    "headers": "GET / HTTP/1.1\r\nHost: www.forwarded-for.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""}
+server.addResponse("sessionlog.json", request_header, response_header)
+request_header = {
+    "headers": "GET / HTTP/1.1\r\nHost: www.forwarded-by-ip.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""}
+server.addResponse("sessionlog.json", request_header, response_header)
+request_header = {
+    "headers": "GET / HTTP/1.1\r\nHost: www.forwarded-by-unknown.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""}
+server.addResponse("sessionlog.json", request_header, response_header)
+request_header = {
+    "headers": "GET / HTTP/1.1\r\nHost: www.forwarded-by-server-name.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""}
+server.addResponse("sessionlog.json", request_header, response_header)
+request_header = {
+    "headers": "GET / HTTP/1.1\r\nHost: www.forwarded-by-uuid.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""}
+server.addResponse("sessionlog.json", request_header, response_header)
+request_header = {
+    "headers": "GET / HTTP/1.1\r\nHost: www.forwarded-proto.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""}
+server.addResponse("sessionlog.json", request_header, response_header)
+request_header = {
+    "headers": "GET / HTTP/1.1\r\nHost: www.forwarded-host.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""}
+server.addResponse("sessionlog.json", request_header, response_header)
+request_header = {
+    "headers": "GET / HTTP/1.1\r\nHost: www.forwarded-connection-compact.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""}
+server.addResponse("sessionlog.json", request_header, response_header)
+request_header = {
+    "headers": "GET / HTTP/1.1\r\nHost: www.forwarded-connection-std.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""}
+server.addResponse("sessionlog.json", request_header, response_header)
+request_header = {
+    "headers": "GET / HTTP/1.1\r\nHost: www.forwarded-connection-full.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""}
+server.addResponse("sessionlog.json", request_header, response_header)
+
+# Set up to check the output after the tests have run.
+#
+forwarded_log_id = Test.Disk.File("forwarded.log")
+forwarded_log_id.Content = "forwarded.gold"
+
+def baselineTsSetup(ts, sslPort):
+
+    ts.addSSLfile("../remap/ssl/server.pem")
+    ts.addSSLfile("../remap/ssl/server.key")
+
+    ts.Variables.ssl_port = sslPort
+
+    ts.Disk.records_config.update({
+        # 'proxy.config.diags.debug.enabled': 1,
+        'proxy.config.url_remap.pristine_host_hdr': 1, # Retain Host header in original incoming client request.
+        'proxy.config.http.cache.http': 0, # Make sure each request is forwarded to the origin server.
+        'proxy.config.proxy_name': 'Poxy_Proxy', # This will be the server name.
+        'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir),
+        'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir),
+        'proxy.config.http.server_ports': (
+            'ipv4:{0} ipv4:{1}:proto=http2;http:ssl ipv6:{0} ipv6:{1}:proto=http2;http:ssl'
+                .format(ts.Variables.port, ts.Variables.ssl_port))
+    })
+
+    ts.Disk.ssl_multicert_config.AddLine(
+        'dest_ip=* ssl_cert_name=server.pem ssl_key_name=server.key'
+    )
+
+    ts.Disk.remap_config.AddLine(
+        'map http://www.no-oride.com http://127.0.0.1:{0}'.format(server.Variables.Port)
+    )
+
+ts = Test.MakeATSProcess("ts", select_ports=False)
+
+baselineTsSetup(ts, 4443)
+
+ts.Disk.remap_config.AddLine(
+    'map http://www.forwarded-none.com http://127.0.0.1:{0}'.format(server.Variables.Port) +
+    ' @plugin=conf_remap.so @pparam=proxy.config.http.insert_forwarded=none'
+)
+ts.Disk.remap_config.AddLine(
+    'map http://www.forwarded-for.com http://127.0.0.1:{0}'.format(server.Variables.Port) +
+    ' @plugin=conf_remap.so @pparam=proxy.config.http.insert_forwarded=for'
+)
+ts.Disk.remap_config.AddLine(
+    'map http://www.forwarded-by-ip.com http://127.0.0.1:{0}'.format(server.Variables.Port) +
+    ' @plugin=conf_remap.so @pparam=proxy.config.http.insert_forwarded=by=ip'
+)
+ts.Disk.remap_config.AddLine(
+    'map http://www.forwarded-by-unknown.com http://127.0.0.1:{0}'.format(server.Variables.Port) +
+    ' @plugin=conf_remap.so @pparam=proxy.config.http.insert_forwarded=by=unknown'
+)
+ts.Disk.remap_config.AddLine(
+    'map http://www.forwarded-by-server-name.com http://127.0.0.1:{0}'.format(server.Variables.Port) +
+    ' @plugin=conf_remap.so @pparam=proxy.config.http.insert_forwarded=by=serverName'
+)
+ts.Disk.remap_config.AddLine(
+    'map http://www.forwarded-by-uuid.com http://127.0.0.1:{0}'.format(server.Variables.Port) +
+    ' @plugin=conf_remap.so @pparam=proxy.config.http.insert_forwarded=by=uuid'
+)
+ts.Disk.remap_config.AddLine(
+    'map http://www.forwarded-proto.com http://127.0.0.1:{0}'.format(server.Variables.Port) +
+    ' @plugin=conf_remap.so @pparam=proxy.config.http.insert_forwarded=proto'
+)
+ts.Disk.remap_config.AddLine(
+    'map http://www.forwarded-host.com http://127.0.0.1:{0}'.format(server.Variables.Port) +
+    ' @plugin=conf_remap.so @pparam=proxy.config.http.insert_forwarded=host'
+)
+ts.Disk.remap_config.AddLine(
+    'map http://www.forwarded-connection-compact.com http://127.0.0.1:{0}'.format(server.Variables.Port) +
+    ' @plugin=conf_remap.so @pparam=proxy.config.http.insert_forwarded=connection=compact'
+)
+ts.Disk.remap_config.AddLine(
+    'map http://www.forwarded-connection-std.com http://127.0.0.1:{0}'.format(server.Variables.Port) +
+    ' @plugin=conf_remap.so @pparam=proxy.config.http.insert_forwarded=connection=std'
+)
+ts.Disk.remap_config.AddLine(
+    'map http://www.forwarded-connection-full.com http://127.0.0.1:{0}'.format(server.Variables.Port) +
+    ' @plugin=conf_remap.so @pparam=proxy.config.http.insert_forwarded=connection=full'
+)
+
+# Ask the OS if the port is ready for connect()
+#
+def CheckPort(Port):
+    return lambda: 0 == subprocess.call('netstat --listen --tcp -n | grep -q :{}'.format(Port), shell=True)
+
+# Basic HTTP 1.1 -- No Forwarded by default
+tr = Test.AddTestRun()
+# Wait for the micro server
+tr.Processes.Default.StartBefore(server, ready=CheckPort(server.Variables.Port))
+# Delay on readiness of our ssl ports
+tr.Processes.Default.StartBefore(Test.Processes.ts, ready=CheckPort(ts.Variables.ssl_port))
+#
+tr.Processes.Default.Command = (
+  'curl --verbose --ipv4 --http1.1 --proxy localhost:{} http://www.no-oride.com'.format(ts.Variables.port)
+)
+tr.Processes.Default.ReturnCode = 0
+
+def TestHttp1_1(host):
+
+    tr = Test.AddTestRun()
+    tr.Processes.Default.Command = (
+        'curl --verbose --ipv4 --http1.1 --proxy localhost:{} http://{}'.format(ts.Variables.port, host)
+    )
+    tr.Processes.Default.ReturnCode = 0
+
+# Basic HTTP 1.1 -- No Forwarded -- explicit configuration.
+#
+TestHttp1_1('www.forwarded-none.com')
+
+# Test enabling of each forwarded parameter singly.
+
+TestHttp1_1('www.forwarded-for.com')
+
+# Note:  forwaded-obsersver.py counts on the "by" tests being done in the order below.
+
+TestHttp1_1('www.forwarded-by-ip.com')
+TestHttp1_1('www.forwarded-by-unknown.com')
+TestHttp1_1('www.forwarded-by-server-name.com')
+TestHttp1_1('www.forwarded-by-uuid.com')
+
+TestHttp1_1('www.forwarded-proto.com')
+TestHttp1_1('www.forwarded-host.com')
+TestHttp1_1('www.forwarded-connection-compact.com')
+TestHttp1_1('www.forwarded-connection-std.com')
+TestHttp1_1('www.forwarded-connection-full.com')
+
+ts2 = Test.MakeATSProcess("ts2", command="traffic_manager", select_ports=False)
+
+ts2.Variables.port += 1
+
+baselineTsSetup(ts2, 4444)
+
+ts2.Disk.records_config.update({
+    'proxy.config.url_remap.pristine_host_hdr': 1, # Retain Host header in original incoming client request.
+    'proxy.config.http.insert_forwarded': 'by=uuid'})
+
+ts2.Disk.remap_config.AddLine(
+    'map https://www.no-oride.com http://127.0.0.1:{0}'.format(server.Variables.Port)
+)
+
+# Forwarded header with UUID of 2nd ATS.
+tr = Test.AddTestRun()
+# Delay on readiness of our ssl ports
+tr.Processes.Default.StartBefore(Test.Processes.ts2, ready=CheckPort(ts2.Variables.ssl_port))
+#
+tr.Processes.Default.Command = (
+    'curl --verbose --ipv4 --http1.1 --proxy localhost:{} http://www.no-oride.com'.format(ts2.Variables.port)
+)
+tr.Processes.Default.ReturnCode = 0
+
+# Call traffic_ctrl to set insert_forwarded
+tr = Test.AddTestRun()
+tr.Processes.Default.Command = (
+    'traffic_ctl --debug config set proxy.config.http.insert_forwarded' +
+    ' "for|by=ip|by=unknown|by=servername|by=uuid|proto|host|connection=compact|connection=std|connection=full"'
+)
+tr.Processes.Default.ForceUseShell = False
+tr.Processes.Default.Env = ts2.Env
+tr.Processes.Default.ReturnCode = 0
+
+# HTTP 1.1
+tr = Test.AddTestRun()
+# Delay to give traffic_ctl config change time to take effect.
+tr.DelayStart = 15
+tr.Processes.Default.Command = (
+    'curl --verbose --ipv4 --http1.1 --proxy localhost:{} http://www.no-oride.com'.format(ts2.Variables.port)
+)
+tr.Processes.Default.ReturnCode = 0
+
+# HTTP 1.0
+tr = Test.AddTestRun()
+tr.Processes.Default.Command = (
+    'curl --verbose --ipv4 --http1.0 --proxy localhost:{} http://www.no-oride.com'.format(ts2.Variables.port)
+)
+tr.Processes.Default.ReturnCode = 0
+
+# HTTP 1.0 -- Forwarded headers already present
+tr = Test.AddTestRun()
+tr.Processes.Default.Command = (
+    "curl --verbose -H 'forwarded:for=0.6.6.6' -H 'forwarded:for=_argh' --ipv4 --http1.0" +
+    " --proxy localhost:{} http://www.no-oride.com".format(ts2.Variables.port)
+)
+tr.Processes.Default.ReturnCode = 0
+
+# HTTP 2
+tr = Test.AddTestRun()
+tr.Processes.Default.Command = (
+    'curl --verbose --ipv4 --http2 --insecure --header "Host: www.no-oride.com"' +
+    ' https://localhost:{}'.format(ts2.Variables.ssl_port)
+)
+tr.Processes.Default.ReturnCode = 0
+
+# TLS
+tr = Test.AddTestRun()
+tr.Processes.Default.Command = (
+    'curl --verbose --ipv4 --http1.1 --insecure --header "Host: www.no-oride.com" https://localhost:{}'
+        .format(ts2.Variables.ssl_port)
+)
+tr.Processes.Default.ReturnCode = 0
+
+# IPv6
+
+tr = Test.AddTestRun()
+tr.Processes.Default.Command = (
+    'curl --verbose --ipv6 --http1.1 --proxy localhost:{} http://www.no-oride.com'.format(ts2.Variables.port)
+)
+tr.Processes.Default.ReturnCode = 0
+
+tr = Test.AddTestRun()
+tr.Processes.Default.Command = (
+    'curl --verbose --ipv6 --http1.1 --insecure --header "Host: www.no-oride.com" https://localhost:{}'.format(ts2.Variables.ssl_port)
+)
+tr.Processes.Default.ReturnCode = 0

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