diff --git a/.github/actions/collect-logs/action.yml b/.github/actions/collect-logs/action.yml
index cc69445152..bf74a79d78 100644
--- a/.github/actions/collect-logs/action.yml
+++ b/.github/actions/collect-logs/action.yml
@@ -34,13 +34,17 @@ runs:
if test -f ~/.kube/config; then
make tools/bin/kubectl
mkdir /tmp/test-logs/cluster
+ tools/bin/kubectl get hosts --all-namespaces -o yaml >/tmp/test-logs/cluster/all-hosts.yaml || true
tools/bin/kubectl get pods --all-namespaces >/tmp/test-logs/cluster/all-pods.txt || true
tools/bin/kubectl describe pods --all-namespaces >/tmp/test-logs/cluster/all-pods-described.txt || true
tools/bin/kubectl get pods --all-namespaces -ocustom-columns="name:.metadata.name,namespace:.metadata.namespace" --no-headers | while read -r name namespace; do
tools/bin/kubectl --namespace="$namespace" logs "$name" >"/tmp/test-logs/cluster/pod.${namespace}.${name}.log" || true
done
+
+ tools/bin/kubectl cp xfpredirect:/tmp/ambassador/snapshots /tmp/test-logs/cluster/xfpredirect.snapshots || true
fi
+ cp /tmp/*.yaml /tmp/test-logs || true
- name: "Upload Logs"
uses: actions/upload-artifact@v2
with:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1163ff7ae9..8e39cbfa77 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -132,15 +132,29 @@ it will be removed; but as it won't be user-visible this isn't considered a brea
the specified non-negative window period in seconds before doing an Envoy reconfiguration. Default
is "1" if not set.
-- Bugfix: If a `Host` or `TLSContext` contained a hostname with a `:` then when using the
+- Bugfix: If a `Host` or `TLSContext` contained a hostname with a `:` then when using the
diagnostics endpoints `ambassador/v0/diagd` then an error would be thrown due to the parsing logic
- not being able to handle the extra colon. This has been fixed and Emissary-ingress will not throw
+ not being able to handle the extra colon. This has been fixed and Emissary-ingress will not throw
an error when parsing envoy metrics for the diagnostics user interface.
- Feature: It is now possible to set `custom_tags` in the `TracingService`. Trace tags can be set
based on literal values, environment variables, or request headers. (Thanks to Paul!) ([#4181])
+- Bugfix: Emissary-ingress 2.0.0 introduced a bug where a `TCPMapping` that uses SNI, instead of
+ using the hostname glob in the `TCPMapping`, uses the hostname glob in the `Host` that the TLS
+ termination configuration comes from.
+
+- Bugfix: Emissary-ingress 2.0.0 introduced a bug where a `TCPMapping` that terminates TLS must have
+ a corresponding `Host` that it can take the TLS configuration from. This was semi-intentional, but
+ didn't make much sense. You can now use a `TLSContext` without a `Host`as in Emissary-ingress 1.y
+ releases, or a `Host` with or without a `TLSContext` as in prior 2.y releases.
+
+- Bugfix: Prior releases of Emissary-ingress had the arbitrary limitation that a `TCPMapping` cannot
+ be used on the same port that HTTP is served on, even if TLS+SNI would make this possible.
+ Emissary-ingress now allows `TCPMappings` to be used on the same `Listener` port as HTTP `Hosts`,
+ as long as that `Listener` terminates TLS.
+
[#4354]: https://github.com/emissary-ingress/emissary/issues/4354
[#4181]: https://github.com/emissary-ingress/emissary/pull/4181
@@ -315,6 +329,20 @@ it will be removed; but as it won't be user-visible this isn't considered a brea
the specified non-negative window period in seconds before doing an Envoy reconfiguration. Default
is "1" if not set.
+- Bugfix: Emissary-ingress 2.0.0 introduced a bug where a `TCPMapping` that uses SNI, instead of
+ using the hostname glob in the `TCPMapping`, uses the hostname glob in the `Host` that the TLS
+ termination configuration comes from.
+
+- Bugfix: Emissary-ingress 2.0.0 introduced a bug where a `TCPMapping` that terminates TLS must have
+ a corresponding `Host` that it can take the TLS configuration from. This was semi-intentional, but
+ didn't make much sense. You can now use a `TLSContext` without a `Host`as in Emissary-ingress 1.y
+ releases, or a `Host` with or without a `TLSContext` as in prior 2.y releases.
+
+- Bugfix: Prior releases of Emissary-ingress had the arbitrary limitation that a `TCPMapping` cannot
+ be used on the same port that HTTP is served on, even if TLS+SNI would make this possible.
+ Emissary-ingress now allows `TCPMappings` to be used on the same `Listener` port as HTTP `Hosts`,
+ as long as that `Listener` terminates TLS.
+
## [1.14.5] TBD
[1.14.5]: https://github.com/emissary-ingress/emissary/compare/v2.3.2...v1.14.5
diff --git a/build-aux/.gitignore b/build-aux/.gitignore
index ee1bac1479..945919888f 100644
--- a/build-aux/.gitignore
+++ b/build-aux/.gitignore
@@ -33,6 +33,10 @@
# Remove the tail of this list when the commit making the change gets
# far enough in to the past.
+# whenever-we-stop-working-on-2.x
+/pytest-kat-envoy2.txt
+/pytest-kat-envoy3.txt
+
# 2019-07-01
/teleproxy
/kubeapply
diff --git a/builder/builder.mk b/builder/builder.mk
index 5179bd7b35..48a94f47b6 100644
--- a/builder/builder.mk
+++ b/builder/builder.mk
@@ -321,7 +321,7 @@ pytest-kat-envoy3: push-pytest-images # doing this all at once is too much for C
$(MAKE) pytest KAT_RUN_MODE=envoy PYTEST_ARGS="$$PYTEST_ARGS python/tests/kat"
# ... so we have a separate rule to run things split up
build-aux/.pytest-kat.txt.stamp: $(OSS_HOME)/venv push-pytest-images FORCE
- . venv/bin/activate && set -o pipefail && pytest --collect-only python/tests/kat 2>&1 | sed -En 's/.*/\1/p' | sed 's/[].].*//' | sort -u > $@
+ . venv/bin/activate && set -o pipefail && pytest --collect-only python/tests/kat 2>&1 | sed -En 's/.*/\1/p' | cut -d. -f1 | sort -u > $@
build-aux/pytest-kat.txt: build-aux/%: build-aux/.%.stamp $(tools/copy-ifchanged)
$(tools/copy-ifchanged) $< $@
clean: build-aux/.pytest-kat.txt.stamp.rm build-aux/pytest-kat.txt.rm
diff --git a/builder/copy-gold.sh b/builder/copy-gold.sh
index 4f3ef85963..30d4736d98 100644
--- a/builder/copy-gold.sh
+++ b/builder/copy-gold.sh
@@ -116,5 +116,19 @@ copy_gold xfpredirect
copy_gold empty empty-namespace
copy_gold plain plain-namespace
copy_gold tcpmappingtest tcp-namespace
+copy_gold tcpmappingbasictest
+copy_gold tcpmappingcrossnamespacetest
+copy_gold tcpmappingtlsoriginationbooltest
+copy_gold tcpmappingtlsoriginationv2schemetest
+copy_gold tcpmappingtlsoriginationcontexttest
+copy_gold tcpmappingtlsoriginationcontextwithdottest
+copy_gold tcpmappingtlsoriginationcontextcrossnamespacetest
+copy_gold tcpmappingtlsterminationbasictest
+copy_gold tcpmappingtlsterminationcrossnamespacetest
+copy_gold tcpmappingsnisharedcontexttest
+copy_gold tcpmappingsniseparatecontextstest
+copy_gold tcpmappingsniwithhttptest
+copy_gold tcpmappingaddresstest
+copy_gold tcpmappingweighttest
printf "\n"
diff --git a/docs/releaseNotes.yml b/docs/releaseNotes.yml
index 1a871861d3..fe71118dc0 100644
--- a/docs/releaseNotes.yml
+++ b/docs/releaseNotes.yml
@@ -102,12 +102,12 @@ items:
body: >-
The AMBASSADOR_RECONFIG_MAX_DELAY
env var can be optionally set to batch changes for the specified
non-negative window period in seconds before doing an Envoy reconfiguration. Default is "1" if not set.
-
+
- title: Diagnostics stats properly handles parsing envoy metrics with colons
type: bugfix
body: >-
- If a Host
or TLSContext
contained a hostname with a :
then when using the
- diagnostics endpoints ambassador/v0/diagd
then an error would be thrown due to the parsing logic not
+ If a Host
or TLSContext
contained a hostname with a :
then when using the
+ diagnostics endpoints ambassador/v0/diagd
then an error would be thrown due to the parsing logic not
being able to handle the extra colon. This has been fixed and $productName$ will not throw an error when parsing
envoy metrics for the diagnostics user interface.
@@ -122,6 +122,31 @@ items:
- title: "#4181"
link: https://github.com/emissary-ingress/emissary/pull/4181
+ - title: TCPMappings use correct SNI configuration
+ type: bugfix
+ body: >-
+ $productName$ 2.0.0 introduced a bug where a TCPMapping
that uses SNI,
+ instead of using the hostname glob in the TCPMapping
, uses the hostname glob
+ in the Host
that the TLS termination configuration comes from.
+
+ - title: TCPMappings configure TLS termination without a Host resource
+ type: bugfix
+ body: >-
+ $productName$ 2.0.0 introduced a bug where a TCPMapping
that terminates TLS
+ must have a corresponding Host
that it can take the TLS configuration from.
+ This was semi-intentional, but didn't make much sense. You can now use a
+ TLSContext
without a Host
as in $productName$ 1.y releases, or a
+ Host
with or without a TLSContext
as in prior 2.y releases.
+
+ - title: TCPMappings and HTTP Hosts can coexist on Listeners that terminate TLS
+ type: bugfix
+ body: >-
+ Prior releases of $productName$ had the arbitrary limitation that a
+ TCPMapping
cannot be used on the same port that HTTP is served on, even if
+ TLS+SNI would make this possible. $productName$ now allows TCPMappings
to be
+ used on the same Listener
port as HTTP Hosts
, as long as that
+ Listener
terminates TLS.
+
- version: 3.1.1
prevVersion: 3.1.0
date: 'TBD'
@@ -352,6 +377,31 @@ items:
The AMBASSADOR_RECONFIG_MAX_DELAY
env var can be optionally set to batch changes for the specified
non-negative window period in seconds before doing an Envoy reconfiguration. Default is "1" if not set.
+ - title: TCPMappings use correct SNI configuration
+ type: bugfix
+ body: >-
+ $productName$ 2.0.0 introduced a bug where a TCPMapping
that uses SNI,
+ instead of using the hostname glob in the TCPMapping
, uses the hostname glob
+ in the Host
that the TLS termination configuration comes from.
+
+ - title: TCPMappings configure TLS termination without a Host resource
+ type: bugfix
+ body: >-
+ $productName$ 2.0.0 introduced a bug where a TCPMapping
that terminates TLS
+ must have a corresponding Host
that it can take the TLS configuration from.
+ This was semi-intentional, but didn't make much sense. You can now use a
+ TLSContext
without a Host
as in $productName$ 1.y releases, or a
+ Host
with or without a TLSContext
as in prior 2.y releases.
+
+ - title: TCPMappings and HTTP Hosts can coexist on Listeners that terminate TLS
+ type: bugfix
+ body: >-
+ Prior releases of $productName$ had the arbitrary limitation that a
+ TCPMapping
cannot be used on the same port that HTTP is served on, even if
+ TLS+SNI would make this possible. $productName$ now allows TCPMappings
to be
+ used on the same Listener
port as HTTP Hosts
, as long as that
+ Listener
terminates TLS.
+
- version: 1.14.5
date: 'TBD'
notes:
diff --git a/python/ambassador/envoy/v3/v3listener.py b/python/ambassador/envoy/v3/v3listener.py
index 05a1f6a3e0..c76481cd37 100644
--- a/python/ambassador/envoy/v3/v3listener.py
+++ b/python/ambassador/envoy/v3/v3listener.py
@@ -14,7 +14,7 @@
import logging
import sys
from os import environ
-from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple
+from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Tuple, Union
from typing import cast as typecast
from ...ir.irhost import IRHost
@@ -41,51 +41,74 @@
# also can have a TLS context to say which certificate to serve if a connection is to be
# processed by the chain.
#
-# A basic asymmetry of the chain is that the filter_chain_match can only do hostname matching
-# if TLS (and thus SNI) is in play, which means for our purposes that a chain _with_ TLS enabled
-# is fundamentally different from a chain _without_ TLS enabled. We encapsulate that idea in
-# the "type" parameter, which can be "http", "https", or "tcp" depending on how the chain will
-# be used. (And yes, that implies that at the moment, you can't mix HTTP Mappings and TCP Mappings
-# on the same port. Possible near-future feature.)
+# A basic asymmetry of the chain is that the filter_chain_match can only do hostname matching if SNI
+# is available (i.e. we're terminating TLS), which means for our purposes that a chain _with_ TLS
+# enabled is fundamentally different from a chain _without_ TLS enabled. Whether a chain has TLS
+# enabled can be checked with the truthiness of `chain.context`.
-class V3Chain(dict):
- def __init__(self, config: "V3Config", type: str, host: Optional[IRHost]) -> None:
+class V3Chain:
+ _config: "V3Config"
+ _logger: logging.Logger
+ _log_debug: bool
+
+ # We can have multiple hosts here, primarily so that cleartext HTTP chains can DTRT.
+ context: Optional["IRTLSContext"]
+ hosts: Dict[str, Union[IRHost, IRTCPMappingGroup]]
+ routes: List[DictifiedV3Route]
+
+ def __init__(self, config: "V3Config", context: Optional["IRTLSContext"]) -> None:
self._config = config
self._logger = self._config.ir.logger
self._log_debug = self._logger.isEnabledFor(logging.DEBUG)
- self.type = type
-
- # We can have multiple hosts here, primarily so that HTTP chains can DTRT --
- # but it would be fine to have multiple HTTPS hosts too, as long as they all
- # share a TLSContext.
- self.context: Optional[IRTLSContext] = None
- self.hosts: Dict[str, IRHost] = {}
-
- # It's OK if an HTTP chain has no Host.
- if host:
- self.add_host(host)
-
- self.routes: List[DictifiedV3Route] = []
- self.tcpmappings: List[IRTCPMappingGroup] = []
+ self.context = context
+ self.hosts = {}
+ self.routes = []
- def add_host(self, host: IRHost) -> None:
- self.hosts[host.hostname] = host
+ def add_tcphost(self, tcpmapping: IRTCPMappingGroup) -> None:
+ if self._log_debug:
+ self._logger.debug(
+ f" CHAIN UPDATE: add TCP host: hostname={repr(tcpmapping.get('host'))}"
+ )
- # Don't mess with the context if we're an HTTP chain...
- if self.type.lower() == "http":
+ if len(self.hosts) > 0:
+ # If we have SNI, then each host gets its own chain, so we should never have more than 1
+ # self.hosts; if we don't have SNI then a single TLSContext takes over the entire chain
+ # and so we shouldn't have more than 1 self.hosts then either.
+ other = next(iter(self.hosts.values()))
+ other_type = "TCPMapping" if isinstance(other, IRTCPMappingGroup) else "Host"
+ tcpmapping.post_error(
+ "TCPMapping {tcpmapping.name}: discarding because it conflicts with {other_type} {other.name}"
+ )
return
- # OK, we're some type where TLS makes sense. Do the thing.
- if host.context:
- if not self.context:
- self.context = host.context
- elif self.context != host.context:
- self._config.ir.post_error(
- "Chain context mismatch: Host %s cannot combine with %s"
- % (host.name, ", ".join(sorted(self.hosts.keys())))
+ self.hosts[tcpmapping.get("host") or "*"] = tcpmapping
+
+ def add_httphost(self, host: IRHost) -> None:
+ if self._log_debug:
+ self._logger.debug(f" CHAIN UPDATE: add HTTP host: hostname={repr(host.hostname)}")
+
+ if self.context:
+ # If we have SNI, then each host gets its own chain, so we should never have more than 1
+ # self.hosts
+ if len(self.hosts) > 0:
+ other = next(iter(self.hosts.values()))
+ other_type = "TCPMapping" if isinstance(other, IRTCPMappingGroup) else "Host"
+ host.post_error(
+ "TLS Host {host.name}: discarding because it conflicts with {other_type} {other.name}"
)
+ return
+ else:
+ # If we don't have SNI then a single TLSContext takes over the entire chain.
+ for other in self.hosts.values():
+ if isinstance(other, IRTCPMappingGroup):
+ host.post_error(
+ "Cleartext Host {host.name}: discarding because it conflicts with TCPMapping {other.name}"
+ )
+ return
+
+ self.hosts[host.hostname] = host
def hostglobs(self) -> List[str]:
# Get a list of host globs currently set up for this chain.
@@ -93,26 +116,47 @@ def hostglobs(self) -> List[str]:
def matching_hosts(self, route: V3Route) -> List[IRHost]:
# Get a list of _IRHosts_ that the given route should be matched with.
- rv: List[IRHost] = [
- host for host in self.hosts.values() if host.matches_httpgroup(route._group)
- ]
-
+ rv: List[IRHost] = []
+ for host in self.hosts.values():
+ if isinstance(host, IRHost) and host.matches_httpgroup(route._group):
+ rv.append(host)
return rv
def add_route(self, route: DictifiedV3Route) -> None:
self.routes.append(route)
- def add_tcpmapping(self, tcpmapping: IRTCPMappingGroup) -> None:
- self.tcpmappings.append(tcpmapping)
-
def __str__(self) -> str:
ctxstr = f" ctx {self.context.name}" if self.context else ""
- return "CHAIN: %s%s [ %s ]" % (
- self.type.upper(),
- ctxstr,
- ", ".join(sorted(self.hostglobs())),
- )
+ return f"CHAIN: tls={bool(self.context)} hostglobs={repr(sorted(self.hostglobs()))}"
+
+
+def tlscontext_for_tcpmapping(
+ irgroup: IRTCPMappingGroup, config: "V3Config"
+) -> Optional["IRTLSContext"]:
+ group_host = irgroup.get("host")
+ if not group_host:
+ return None
+
+ # We can pair directly with a 'TLSContext', or get a TLS config through a 'Host'.
+ #
+ # Give 'Hosts' precedence. Why? IDK, it felt right.
+
+ # First, Hosts:
+
+ for irhost in sorted(config.ir.get_hosts(), key=lambda h: h.hostname):
+ if irhost.context and hostglob_matches(irhost.hostname, group_host):
+ return irhost.context
+
+ # Second, TLSContexts:
+
+ for context in config.ir.get_tls_contexts():
+ for context_host in context.get("hosts") or []:
+ # Note: this is *not* glob matching.
+ if context_host == group_host:
+ return context
+
+ return None
# Model an Envoy listener.
@@ -124,16 +168,55 @@ def __str__(self) -> str:
# here is all about constructing the Envoy configuration implied by the IRListener.
-class V3Listener(dict):
+class V3Listener:
+ config: "V3Config"
+ _irlistener: IRListener
+
+ @property
+ def http3_enabled(self) -> bool:
+ return self._irlistener.http3_enabled
+
+ @property
+ def socket_protocol(self) -> Literal["TCP", "UDP"]:
+ return self._irlistener.socket_protocol
+
+ @property
+ def bind_address(self) -> str:
+ return self._irlistener.bind_address
+
+ @property
+ def port(self) -> int:
+ return self._irlistener.port
+
+ @property
+ def bind_to(self) -> str:
+ return self._irlistener.bind_to()
+
+ @property
+ def _stats_prefix(self) -> str:
+ return self._irlistener.statsPrefix
+
+ @property
+ def _security_model(self) -> Literal["XFP", "SECURE", "INSECURE"]:
+ return self._irlistener.securityModel
+
+ @property
+ def _l7_depth(self) -> int:
+ return self._irlistener.get("l7Depth", 0)
+
+ @property
+ def _insecure_only(self) -> bool:
+ return self._irlistener.insecure_only
+
+ @property
+ def per_connection_buffer_limit_bytes(self) -> Optional[int]:
+ return self.config.ir.ambassador_module.get("buffer_limit_bytes", None)
+
def __init__(self, config: "V3Config", irlistener: IRListener) -> None:
super().__init__()
self.config = config
- self.http3_enabled = irlistener.http3_enabled
- self.socket_protocol = irlistener.socket_protocol
- self.bind_address = irlistener.bind_address
- self.port = irlistener.port
- self.bind_to = irlistener.bind_to()
+ self._irlistener = irlistener # We cache the IRListener to use its match method later
bindstr = (
f"-{irlistener.socket_protocol.lower()}-{self.bind_address}"
@@ -142,15 +225,8 @@ def __init__(self, config: "V3Config", irlistener: IRListener) -> None:
)
self.name = irlistener.name or f"ambassador-listener{bindstr}-{self.port}"
- self.use_proxy_proto = False
self.listener_filters: List[dict] = []
self.traffic_direction: str = "UNSPECIFIED"
- self.per_connection_buffer_limit_bytes: Optional[int] = None
- self._irlistener = irlistener # We cache the IRListener to use its match method later
- self._stats_prefix = irlistener.statsPrefix
- self._security_model: str = irlistener.securityModel
- self._l7_depth: int = irlistener.get("l7Depth", 0)
- self._insecure_only: bool = False
self._filter_chains: List[dict] = []
self._base_http_config: Optional[Dict[str, Any]] = None
self._chains: Dict[str, V3Chain] = {}
@@ -162,16 +238,9 @@ def __init__(self, config: "V3Config", irlistener: IRListener) -> None:
self._log_debug = self.config.ir.logger.isEnabledFor(logging.DEBUG)
if self._log_debug:
self.config.ir.logger.debug(
- f"V3Listener {self.name} created -- {self._security_model}, l7Depth {self._l7_depth}"
+ f"V3Listener {self.name}: created: port={self.port} security_model={self._security_model} l7depth={self._l7_depth}"
)
- # If the IRListener is marked insecure-only, so are we.
- self._insecure_only = irlistener.insecure_only
-
- buffer_limit_bytes = self.config.ir.ambassador_module.get("buffer_limit_bytes", None)
- if buffer_limit_bytes:
- self.per_connection_buffer_limit_bytes = buffer_limit_bytes
-
# Build out our listener filters, and figure out if we're an HTTP listener
# in the process.
for proto in irlistener.protocolStack:
@@ -195,22 +264,15 @@ def __init__(self, config: "V3Config", irlistener: IRListener) -> None:
self.listener_filters.append({"name": "envoy.filters.listener.tls_inspector"})
if proto == "TCP":
- # TCP doesn't require any specific listener filters, but it
- # does require stuff in the filter chains. We can go ahead and
- # tackle that here.
- for irgroup in self.config.ir.ordered_groups():
- # Only look at TCPMappingGroups here...
- if not isinstance(irgroup, IRTCPMappingGroup):
- continue
-
- # ...and make sure the group in question wants the same bind
- # address that we do.
- if irgroup.bind_to() != self.bind_to:
- continue
-
- self.add_tcp_group(irgroup)
-
- def add_chain(self, chain_type: str, host: Optional[IRHost]) -> V3Chain:
+ # Nothing to do.
+ pass
+
+ def add_chain(
+ self,
+ chain_type: Literal["tcp", "http", "https"],
+ context: Optional["IRTLSContext"],
+ hostname: str,
+ ) -> V3Chain:
# Add a chain for a specific Host to this listener, while dealing with the fundamental
# asymmetry that filter_chain_match can - and should - use SNI whenever the chain has
# TLS available, but that's simply not available for chains without TLS.
@@ -223,87 +285,35 @@ def add_chain(self, chain_type: str, host: Optional[IRHost]) -> V3Chain:
# answer is just that it would needlessly add nesting to all our loops and such (this
# is also why there's no vhost data structure).
- chain_key = chain_type
- hoststr = host.hostname if host else "(no host)"
- hostname = (host.hostname if host else None) or "*"
+ if chain_type == "http":
+ assert not context
+ if chain_type == "https":
+ assert context
- if host:
- chain_key = "%s-%s" % (chain_type, hostname)
+ hostname = hostname or "*"
- chain = self._chains.get(chain_key)
+ chain_key = "tls" if context else "cleartext"
+ # I (LukeShu) can't really give an explanation of why `or chain_type == 'http'` belongs in
+ # this expression (it's what the above comment "we can - and do - separate HTTP chains into
+ # specific domains" is referring to), other than that it needs to be here in order for
+ # compute_routes() to work correctly. Maybe that's bad and we should go fix
+ # compute_routes() and remove `or chain_type = 'http'`... I'd have to study compute_routes()
+ # a lot more in order to be able to answer that; but in the mean time, including it in the
+ # expression keeps things working.
+ if context or chain_type == "http":
+ chain_key += f"-{hostname}"
- if chain is not None:
- if host:
- chain.add_host(host)
- if self._log_debug:
- self.config.ir.logger.debug(
- " CHAIN ADD: host %s chain_key %s -- %s", hoststr, chain_key, chain
- )
- else:
- if self._log_debug:
- self.config.ir.logger.debug(
- " CHAIN NOOP: host %s chain_key %s -- %s", hoststr, chain_key, chain
- )
- else:
- chain = V3Chain(self.config, chain_type, host)
+ chain = self._chains.get(chain_key)
+ verb = "REUSED" if chain else "CREATE"
+ if chain is None:
+ chain = V3Chain(self.config, context)
self._chains[chain_key] = chain
- if self._log_debug:
- self.config.ir.logger.debug(
- " CHAIN CREATE: host %s chain_key %s -- %s", hoststr, chain_key, chain
- )
-
- return chain
-
- def add_tcp_group(self, irgroup: IRTCPMappingGroup) -> None:
- # The TCP analog of add_chain -- it adds a chain, too, but works with a TCP
- # mapping group rather than a Host. Same deal applies with TLS: you can't do
- # host-based matching without it.
-
- group_host = irgroup.get("host", None)
-
if self._log_debug:
self.config.ir.logger.debug(
- "V3Listener %s on %s: take TCPMappingGroup on %s (%s)",
- self.name,
- self.bind_to,
- irgroup.bind_to(),
- group_host or "i'*'",
+ f" CHAIN {verb}: tls={bool(context)} host={repr(hostname)} => chains[{repr(chain_key)}]={chain}"
)
- if not group_host:
- # Special case. No Host in a TCPMapping means an unconditional forward,
- # so just add this immediately as a "*" chain.
- chain = self.add_chain("tcp", None)
- chain.add_tcpmapping(irgroup)
- else:
- # What matching Hosts do we have?
- for host in sorted(self.config.ir.get_hosts(), key=lambda h: h.hostname):
- # They're asking for a hostname match here, which _cannot happen_ without
- # SNI -- so don't take any hosts that don't have a TLSContext.
-
- if not host.context:
- if self._log_debug:
- self.config.ir.logger.debug(
- "V3Listener %s @ %s TCP %s: skip %s",
- self.name,
- self.bind_to,
- group_host,
- host,
- )
- continue
-
- if self._log_debug:
- self.config.ir.logger.debug(
- "V3Listener %s @ %s TCP %s: consider %s",
- self.name,
- self.bind_to,
- group_host,
- host,
- )
-
- if hostglob_matches(host.hostname, group_host):
- chain = self.add_chain("tcp", host)
- chain.add_tcpmapping(irgroup)
+ return chain
# access_log constructs the access_log configuration for this V3Listener
def access_log(self) -> List[dict]:
@@ -597,39 +607,38 @@ def base_http_config(self) -> Dict[str, Any]:
def finalize(self) -> None:
if self._log_debug:
- self.config.ir.logger.debug(f"V3Listener: ==== finalize {self}")
-
- # OK. Assemble the high-level stuff for Envoy.
- self.address = {
- "socket_address": {
- "address": self.bind_address,
- "port_value": self.port,
- "protocol": self.socket_protocol, ## "TCP" or "UDP"
- }
- }
+ self.config.ir.logger.debug(f"V3Listener {self}: finalize ============================")
+
+ # We do TCP chains before HTTP chains so that TCPMappings have precedence over Hosts. This
+ # is important because 2.x releases prior to 2.4 required you to create a Host for the
+ # TCPMapping to steal the TLS termination config from (so TCPMapping users coming from 2.3
+ # will _very likely_ have "conflicting" Hosts and TCPMappings), and also didn't support
+ # TCPMappings and Hosts on the same Listener (so 2.3 didn't see these as "conflicts"). But
+ # now that we do support them together on the same Listener, we do see them as conflicts,
+ # and so we keep compatibility with 2.3 by saying "in the event of a conflict, TCPMappings
+ # have precedence over Hosts."
+ self.compute_tcpchains()
+ self.finalize_tcp()
- # Next, deal with HTTP stuff if this is an HTTP Listener.
if self._base_http_config:
- self.compute_chains()
+ self.compute_httpchains()
self.compute_routes()
self.finalize_http()
- else:
- # TCP is a lot simpler.
- self.finalize_tcp()
def finalize_tcp(self) -> None:
# Finalize a TCP listener, which amounts to walking all our TCP chains and
# setting up Envoy configuration structures for them.
- logger = self.config.ir.logger
- for chain_key, chain in self._chains.items():
- if chain.type != "tcp":
- continue
+ self.config.ir.logger.debug(" finalize_tcp")
+ for chain_key, chain in self._chains.items():
if self._log_debug:
- logger.debug("BUILD CHAIN %s - %s", chain_key, chain)
+ self.config.ir.logger.debug(f" build chain[{repr(chain_key)}]={chain}")
+
+ for irgroup in chain.hosts.values():
+ if not isinstance(irgroup, IRTCPMappingGroup):
+ continue
- for irgroup in chain.tcpmappings:
# First up, which clusters do we need to talk to?
clusters = [
{"name": mapping.cluster.envoy_name, "weight": mapping._weight}
@@ -647,7 +656,10 @@ def finalize_tcp(self) -> None:
}
# OK. Basic filter chain entry next.
- filter_chain: Dict[str, Any] = {"filters": [tcp_filter]}
+ filter_chain: Dict[str, Any] = {
+ "name": f"tcphost-{irgroup.name}",
+ "filters": [tcp_filter],
+ }
# The chain as a whole has a single matcher.
filter_chain_match: Dict[str, Any] = {}
@@ -675,7 +687,7 @@ def finalize_tcp(self) -> None:
# make sure that we don't have two chains with an empty filter_match
# criterion (since Envoy will reject such a configuration).
- if len(chain_hosts) > 0:
+ if len(chain_hosts) > 0 and ("*" not in chain_hosts):
filter_chain_match["server_names"] = chain_hosts
# Once all of that is done, hook in the match...
@@ -684,19 +696,54 @@ def finalize_tcp(self) -> None:
# ...and stick this chain into our filter.
self._filter_chains.append(filter_chain)
- def compute_chains(self) -> None:
+ def compute_tcpchains(self) -> None:
+ self.config.ir.logger.debug(" compute_tcpchains")
+
+ for irgroup in self.config.ir.ordered_groups():
+ # Only look at TCPMappingGroups here...
+ if not isinstance(irgroup, IRTCPMappingGroup):
+ continue
+
+ if self._log_debug:
+ self.config.ir.logger.debug(f" consider {irgroup}")
+
+ # ...and make sure the group in question wants the same bind
+ # address that we do.
+ if irgroup.bind_to() != self.bind_to:
+ self.config.ir.logger.debug(" reject")
+ continue
+
+ self.config.ir.logger.debug(" accept")
+
+ # Add a chain, same as we do in compute_httpchains, just for a 'TCPMappingGroup' rather
+ # than for a 'Host'. Same deal applies with TLS: you can't do host-based matching
+ # without it.
+
+ group_host = irgroup.get("host", None)
+ if not group_host: # cleartext
+ # Special case. No Host in a TCPMapping means an unconditional forward,
+ # so just add this immediately as a "*" chain.
+ self.add_chain("tcp", None, "*").add_tcphost(irgroup)
+ else: # TLS/SNI
+ context = tlscontext_for_tcpmapping(irgroup, self.config)
+ if not context:
+ irgroup.post_error("No matching TLSContext found, disabling!")
+ continue
+ self.add_chain("tcp", context, group_host).add_tcphost(irgroup)
+
+ def compute_httpchains(self) -> None:
# Compute the set of chains we need, HTTP version. The core here is matching
# up Hosts with this Listener, and creating a chain for each Host.
- self.config.ir.logger.debug("V3Listener %s: checking hosts for %s", self.name, self)
+ self.config.ir.logger.debug(" compute_httpchains")
for host in sorted(self.config.ir.get_hosts(), key=lambda h: h.hostname):
if self._log_debug:
- self.config.ir.logger.debug(" consider %s", host)
+ self.config.ir.logger.debug(f" consider {host}")
# First up: drop this host if nothing matches at all.
if not self._irlistener.matches_host(host):
- # Bzzzt.
+ self.config.ir.logger.debug(" reject: hostglobs don't match")
continue
# OK, if we're still here, then it's a question of matching the Listener's
@@ -711,55 +758,46 @@ def compute_chains(self) -> None:
# here.)
if self._insecure_only and (self.port != host.insecure_addl_port):
- if self._log_debug:
- self.config.ir.logger.debug(
- " drop %s, insecure-only port mismatch", host.name
- )
-
+ self.config.ir.logger.debug(" reject: insecure-only port mismatch")
continue
# OK, we can't drop it for that, so we need to check the actions.
- security_model = self._security_model
- secure_action = host.secure_action
- insecure_action = host.insecure_action
-
# If the Listener's securityModel is SECURE, but this host has a secure_action
# of Reject (or empty), we'll skip this host, because the only requests this
# Listener can ever produce will be rejected. In any other case, we'll set up an
# HTTPS chain for this Host, as long as we think TLS is OK.
-
- will_reject_secure = (not secure_action) or (secure_action == "Reject")
- if self._tls_ok and (not ((security_model == "SECURE") and will_reject_secure)):
- if self._log_debug:
- self.config.ir.logger.debug(" take SECURE %s", host)
-
- self.add_chain("https", host)
+ host_will_reject_secure = (not host.secure_action) or (host.secure_action == "Reject")
+ if (
+ self._tls_ok
+ and host.context
+ and (not ((self._security_model == "SECURE") and host_will_reject_secure))
+ ):
+ self.config.ir.logger.debug(" accept SECURE")
+ self.add_chain("https", host.context, host.hostname).add_httphost(host)
# Same idea on the insecure side: only skip the Host if the Listener's securityModel
# is INSECURE but the Host's insecure_action is Reject.
-
- if not ((security_model == "INSECURE") and (insecure_action == "Reject")):
- if self._log_debug:
- self.config.ir.logger.debug(" take INSECURE %s", host)
-
- self.add_chain("http", host)
+ if not ((self._security_model == "INSECURE") and (host.insecure_action == "Reject")):
+ self.config.ir.logger.debug(" accept INSECURE")
+ self.add_chain("http", None, host.hostname).add_httphost(host)
def compute_routes(self) -> None:
# Compute the set of valid HTTP routes for _each chain_ in this Listener.
#
# Note that a route using XFP can match _any_ chain, whether HTTP or HTTPS.
- logger = self.config.ir.logger
+ self.config.ir.logger.debug(" compute_routes")
for chain_key, chain in self._chains.items():
+ if self._log_debug:
+ self.config.ir.logger.debug(f" consider chain[{repr(chain_key)}]={chain}")
+
# Only look at HTTP(S) chains.
- if (chain.type != "http") and (chain.type != "https"):
+ if not any(isinstance(h, IRHost) for h in chain.hosts.values()):
+ self.config.ir.logger.debug(" reject: is non-HTTP")
continue
- if self._log_debug:
- logger.debug("MATCH CHAIN %s - %s", chain_key, chain)
-
# Remember whether we found an ACME route.
found_acme = False
@@ -768,19 +806,21 @@ def compute_routes(self) -> None:
# V3RouteVariants to lazily cache some of the work that we're doing across chains.
for rv in self.config.route_variants:
if self._log_debug:
- logger.debug(" CHECK ROUTE: %s", v3prettyroute(dict(rv.route)))
+ self.config.ir.logger.debug(
+ f" consider route {v3prettyroute(dict(rv.route))}"
+ )
matching_hosts = chain.matching_hosts(rv.route)
if self._log_debug:
- logger.debug(
- " = matching_hosts %s", ", ".join([h.hostname for h in matching_hosts])
+ self.config.ir.logger.debug(
+ f" matching_hosts={[h.hostname for h in matching_hosts]}"
)
if not matching_hosts:
if self._log_debug:
- logger.debug(
- f" drop outright: no hosts match {sorted(rv.route['_host_constraints'])}"
+ self.config.ir.logger.debug(
+ f" reject: no hosts match {sorted(rv.route['_host_constraints'])}"
)
continue
@@ -793,6 +833,9 @@ def compute_routes(self) -> None:
candidates: List[Tuple[IRHost, str, str, V3RouteVariants]] = []
hostname = host.hostname
+ if self._log_debug:
+ self.config.ir.logger.debug(f" host={hostname}")
+
if (host.secure_action is not None) and (self._security_model != "INSECURE"):
# We have a secure action, and we're willing to believe that at least some of
# our requests will be secure.
@@ -834,13 +877,8 @@ def compute_routes(self) -> None:
# really mean "redirect to HTTPS" specifically.
if self._log_debug:
- logger.debug(
- " %s - %s: accept on %s %s%s",
- matcher,
- action,
- self.name,
- hostname,
- extra_info,
+ self.config.ir.logger.debug(
+ f" route: accept matcher={matcher} action={action} {extra_info}"
)
variant = dict(rv.get_variant(matcher, action.lower()))
@@ -848,40 +886,44 @@ def compute_routes(self) -> None:
chain.add_route(variant)
else:
if self._log_debug:
- logger.debug(
- " %s - %s: drop from %s %s%s",
- matcher,
- action,
- self.name,
- hostname,
- extra_info,
+ self.config.ir.logger.debug(
+ f" route: reject matcher={matcher} action={action} {extra_info}"
)
# If we're on Edge Stack and we don't already have an ACME route, add one.
if self.config.ir.edge_stack_allowed and not found_acme:
- # This route is needed to trigger an ExtAuthz request for the AuthService.
- # The auth service grabs the challenge and does the right thing.
- # Rather than try to route to some existing cluster we can just return a
- # direct response. What we return doesn't really matter but
- # to match existing Edge Stack behavior we return a 404 response.
+ # The target cluster doesn't actually matter -- the auth service grabs the
+ # challenge and does the right thing. But we do need a cluster that actually
+ # exists, so use the sidecar cluster.
+
+ if not self.config.ir.sidecar_cluster_name:
+ # Uh whut? how is Edge Stack running exactly?
+ raise Exception(
+ "Edge Stack claims to be running, but we have no sidecar cluster??"
+ )
- if self._log_debug:
- logger.debug(" punching a hole for ACME")
+ self.config.ir.logger.debug(" punching a hole for ACME")
+
+ # Make sure to include _host_constraints in here for now.
+ #
+ # XXX This is needed only because we're dictifying the V3Route too early.
- # Make sure to include _host_constraints in here for now so it can be
- # applied to the correct vhost during future proccessing
chain.routes.insert(
0,
{
"_host_constraints": set(),
"match": {"case_sensitive": True, "prefix": "/.well-known/acme-challenge/"},
- "direct_response": {"status": 404},
+ "route": {
+ "cluster": self.config.ir.sidecar_cluster_name,
+ "prefix_rewrite": "/.well-known/acme-challenge/",
+ "timeout": "3.000s",
+ },
},
)
if self._log_debug:
for route in chain.routes:
- logger.debug(" CHAIN ROUTE: %s" % v3prettyroute(route))
+ self.config.ir.logger.debug(" CHAIN ROUTE: %s" % v3prettyroute(route))
def finalize_http(self) -> None:
# Finalize everything HTTP. Like the TCP side of the world, this is about walking
@@ -890,21 +932,30 @@ def finalize_http(self) -> None:
# All of our HTTP chains get collapsed into a single chain with (likely) multiple
# domains here.
+ self.config.ir.logger.debug(" finalize_http")
+
filter_chains: Dict[str, Dict[str, Any]] = {}
for chain_key, chain in self._chains.items():
+ if not any(isinstance(h, IRHost) for h in chain.hosts.values()):
+ continue
+
if self._log_debug:
- self._irlistener.logger.debug("FHTTP %s / %s / %s", self, chain_key, chain)
+ self._irlistener.logger.debug(f" build chain[{repr(chain_key)}]={chain}")
filter_chain: Optional[Dict[str, Any]] = None
- # http/3 is built on quic which has TLS built-in. This means that our UDP Listener will only ever need routes
- # that match the TLS Filter chain and will diverge from the TCP listener in that it will not support redirect
- # therefore, we can exclude duplicating the filterchain and routes so hitting this endpoint using non-tls http will fail
- if (chain.type == "http") and self.isProtocolUDP() and self.http3_enabled:
- continue
+ if not chain.context: # cleartext
+ # http/3 is built on quic which has TLS built-in. This means that our UDP Listener will only ever need routes
+ # that match the TLS Filter chain and will diverge from the TCP listener in that it will not support redirect
+ # therefore, we can exclude duplicating the filterchain and routes so hitting this endpoint using non-tls http will fail
+ if self.isProtocolUDP() and self.http3_enabled:
+ continue
- if chain.type == "http":
+ if self._log_debug:
+ self._irlistener.logger.debug(
+ f" cleartext for hostglobs={chain.hostglobs()}"
+ )
# All HTTP chains get collapsed into one here, using domains to separate them.
# This works because we don't need to offer TLS certs (we can't anyway), and
# because of that, SNI (and thus filter server_names matches) aren't things.
@@ -917,7 +968,11 @@ def finalize_http(self) -> None:
self._irlistener.logger.debug(
"FHTTP create filter_chain %s / empty match", chain_key
)
- filter_chain = {"filter_chain_match": {}, "_vhosts": {}}
+ filter_chain = {
+ "name": "httphost-shared",
+ "filter_chain_match": {},
+ "_vhosts": {},
+ }
filter_chains[chain_key] = filter_chain
else:
@@ -927,15 +982,21 @@ def finalize_http(self) -> None:
chain_key,
len(filter_chain["_vhosts"]),
)
- elif chain.type == "https":
+ else: # TLS/SNI
# Since chain_key is a dictionary key in its own right, we can't already
# have a matching chain for this.
- filter_chain = {"_vhosts": {}}
+ filter_chain = {
+ "name": f"httpshost-{next(iter(chain.hosts.values())).name}",
+ "_vhosts": {},
+ }
filter_chain_match: Dict[str, Any] = {}
chain_hosts = chain.hostglobs()
+ if self._log_debug:
+ self._irlistener.logger.debug(f" tls for hostglobs={chain_hosts}")
+
# Set up the server_names part of the match, if we have any names.
#
# Note that "*" is _not allowed_ in server_names, though e.g. "*.example.com"
@@ -958,52 +1019,42 @@ def finalize_http(self) -> None:
"quic" if self.isProtocolUDP() and self.http3_enabled else "tls"
)
- if chain.context:
- # ...uh. How could we not have a context if we're doing TLS?
- # Note that we're modifying the filter_chain itself here, not
- # filter_chain_match.
- envoy_ctx = V3TLSContext(chain.context)
+ envoy_ctx = V3TLSContext(chain.context)
+ envoy_tls_config = {
+ "name": "envoy.transport_sockets.tls",
+ "typed_config": {
+ "@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext",
+ **envoy_ctx,
+ },
+ }
+
+ if self.isProtocolUDP():
envoy_tls_config = {
- "name": "envoy.transport_sockets.tls",
+ "name": "envoy.transport_sockets.quic",
"typed_config": {
- "@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext",
- **envoy_ctx,
+ "@type": "type.googleapis.com/envoy.extensions.transport_sockets.quic.v3.QuicDownstreamTransport",
+ "downstream_tls_context": {**envoy_ctx},
},
}
- if self.isProtocolUDP():
- envoy_tls_config = {
- "name": "envoy.transport_sockets.quic",
- "typed_config": {
- "@type": "type.googleapis.com/envoy.extensions.transport_sockets.quic.v3.QuicDownstreamTransport",
- "downstream_tls_context": {**envoy_ctx},
- },
- }
-
- filter_chain["transport_socket"] = envoy_tls_config
-
- else:
- # Envoy doesn't like having a UDP Listener with QUIC filter chain that doesn't have a "transport_socket" set with a TLS
- # certificate. If the fall-back cert is removed or no certificate is provided then we should not add the quic filter chain
- if self.isProtocolUDP() and self.http3_enabled:
- self._irlistener.logger.warn(
- f"Listener: quic network protocol requires a TLSContext to be provided, no tls context found for Listener: {self._irlistener.bind_to()}"
- )
- continue
+ filter_chain["transport_socket"] = envoy_tls_config
# Finally, stash the match in the chain...
filter_chain["filter_chain_match"] = filter_chain_match
# ...and save it.
filter_chains[chain_key] = filter_chain
- else:
- # The chain type is neither HTTP nor HTTPS -- must be a TCP chain. Skip it.
- continue
# OK, we have the filter_chain variable set -- build the Envoy virtual_hosts for it.
for host in chain.hosts.values():
+ if not isinstance(host, IRHost):
+ continue
+
+ if self._log_debug:
+ self._irlistener.logger.debug(f" adding vhost {repr(host.hostname)}")
+
# Make certain that no internal keys from the route make it into the Envoy
# configuration.
routes = []
@@ -1087,7 +1138,13 @@ def finalize_http(self) -> None:
def as_dict(self) -> dict:
listener: dict = {
"name": self.name,
- "address": self.address,
+ "address": {
+ "socket_address": {
+ "address": self.bind_address,
+ "port_value": self.port,
+ "protocol": self.socket_protocol, ## "TCP" or "UDP"
+ }
+ },
"filter_chains": self._filter_chains,
"traffic_direction": self.traffic_direction,
}
@@ -1108,14 +1165,6 @@ def as_dict(self) -> dict:
return listener
- def pretty(self) -> dict:
- return {
- "name": self.name,
- "bind_address": self.bind_address,
- "port": self.port,
- "chains": self._chains,
- }
-
def __str__(self) -> str:
return "" % (
"HTTP" if self._base_http_config else "TCP",
@@ -1136,25 +1185,23 @@ def isProtocolUDP(self) -> bool:
@classmethod
def generate(cls, config: "V3Config") -> None:
config.listeners = []
- logger = config.ir.logger
for key in config.ir.listeners.keys():
irlistener = config.ir.listeners[key]
v3listener = V3Listener(config, irlistener)
v3listener.finalize()
- config.ir.logger.info(f"V3Listener: ==== GENERATED {v3listener}")
-
- if v3listener._log_debug:
- for k in sorted(v3listener._chains.keys()):
- chain = v3listener._chains[k]
- config.ir.logger.debug(" %s", chain)
-
- for r in chain.routes:
- config.ir.logger.debug(" %s", v3prettyroute(r))
+ config.ir.logger.info(f"V3Listener {v3listener}: generated ===========================")
+ if config.ir.logger.isEnabledFor(logging.DEBUG):
+ if v3listener._log_debug:
+ for k in sorted(v3listener._chains.keys()):
+ chain = v3listener._chains[k]
+ config.ir.logger.debug(f" chain {chain}")
+ for r in chain.routes:
+ config.ir.logger.debug(f" route {v3prettyroute(r)}")
# Does this listener have any filter chains?
if v3listener._filter_chains:
config.listeners.append(v3listener)
else:
- irlistener.post_error("No matching Hosts found, disabling!")
+ irlistener.post_error("No matching Hosts/TCPMappings found, disabling!")
diff --git a/python/ambassador/ir/irutils.py b/python/ambassador/ir/irutils.py
index 886394b775..de6fb36cba 100644
--- a/python/ambassador/ir/irutils.py
+++ b/python/ambassador/ir/irutils.py
@@ -189,13 +189,13 @@ def selector_matches(
if not match:
# If there's no matchLabels to match, return True.
- logger.debug(" no matchLabels in selector => True")
+ logger.debug(" no matchLabels in selector => True")
return True
# If we have stuff to match on, but no labels to actually match them, we
# can short-circuit (and skip a weirder conditional down in the loop).
if not labels:
- logger.debug(" no incoming labels => False")
+ logger.debug(" no incoming labels => False")
return False
if disable_strict_selectors():
@@ -204,18 +204,18 @@ def selector_matches(
logger.debug(" selector match for %s=%s => True", k, v)
return True
- logger.debug(" selector miss on %s=%s", k, v)
+ logger.debug(" selector miss on %s=%s", k, v)
- logger.debug(" all selectors miss => False")
+ logger.debug(" all selectors miss => False")
return False
else:
# For every label in mappingSelector, there must be a label with same value in the Mapping itself.
for k, v in match.items():
if labels.get(k) == v:
- logger.debug(" selector match for %s=%s => True", k, v)
+ logger.debug(" selector match for %s=%s => True", k, v)
else:
- logger.debug(" selector miss for %s=%s => False", k, v)
+ logger.debug(" selector miss for %s=%s => False", k, v)
return False
- logger.debug(" all selectors match => True")
+ logger.debug(" all selectors match => True")
return True
diff --git a/python/kat/harness.py b/python/kat/harness.py
index b0d8c6f917..5d4c3cf1b1 100755
--- a/python/kat/harness.py
+++ b/python/kat/harness.py
@@ -344,6 +344,7 @@ class Node(ABC):
namespace: str = None # type: ignore
is_ambassador = False
local_result: Optional[Dict[str, str]] = None
+ xfail: Optional[str]
def __init__(self, *args, **kwargs) -> None:
# If self.skip is set to true, this node is skipped
diff --git a/python/tests/kat/t_redirect.py b/python/tests/kat/t_redirect.py
index c8b8f056a0..0be52e6054 100644
--- a/python/tests/kat/t_redirect.py
+++ b/python/tests/kat/t_redirect.py
@@ -261,7 +261,7 @@ def manifests(self):
apiVersion: getambassador.io/v3alpha1
kind: Listener
metadata:
- name: ambassador-listener-8080
+ name: {self.path.k8s}
spec:
ambassador_id: [{self.ambassador_id}]
port: 8080
@@ -275,7 +275,7 @@ def manifests(self):
apiVersion: getambassador.io/v3alpha1
kind: Host
metadata:
- name: weird-xfp-test-host
+ name: {self.path.k8s}
spec:
ambassador_id: [{self.ambassador_id}]
requestPolicy:
diff --git a/python/tests/kat/t_tcpmapping.py b/python/tests/kat/t_tcpmapping.py
index fd4c4bf736..fd5e721624 100644
--- a/python/tests/kat/t_tcpmapping.py
+++ b/python/tests/kat/t_tcpmapping.py
@@ -1,7 +1,7 @@
-from typing import Generator, Tuple, Union
+from typing import Dict, Generator, Literal, Tuple, Union
from abstract_tests import HTTP, AmbassadorTest, Node, ServiceType
-from kat.harness import Query
+from kat.harness import Query, abstract_test
from tests.integration.manifests import namespace_manifest
from tests.selfsigned import TLSCerts
@@ -321,3 +321,991 @@ def check(self):
assert (
tls_enabled == tls_wanted
), f"{idx}: TLS status {tls_enabled} != wanted {tls_wanted}"
+
+
+class TCPMappingBasicTest(AmbassadorTest):
+ extra_ports = [6789]
+ target: ServiceType
+
+ def init(self) -> None:
+ self.target = HTTP()
+
+ def manifests(self) -> str:
+ return (
+ format(
+ """
+---
+apiVersion: getambassador.io/v2
+kind: TCPMapping
+metadata:
+ name: {self.name.k8s}
+spec:
+ ambassador_id: [ {self.ambassador_id} ]
+ port: 6789
+ service: {self.target.path.fqdn}:80
+"""
+ )
+ + super().manifests()
+ )
+
+ def queries(self):
+ yield Query(self.url("", port=6789))
+
+ def check(self):
+ assert self.results[0].json["backend"] == self.target.path.k8s
+ assert self.results[0].json["request"]["tls"]["enabled"] == False
+
+
+class TCPMappingCrossNamespaceTest(AmbassadorTest):
+ extra_ports = [6789]
+ target: ServiceType
+
+ def init(self) -> None:
+ self.target = HTTP(namespace="other-namespace")
+
+ def manifests(self) -> str:
+ return (
+ namespace_manifest("other-namespace")
+ + format(
+ """
+---
+apiVersion: getambassador.io/v2
+kind: TCPMapping
+metadata:
+ name: {self.name.k8s}
+spec:
+ ambassador_id: [ {self.ambassador_id} ]
+ port: 6789
+ service: {self.target.path.fqdn}:80
+"""
+ )
+ + super().manifests()
+ )
+
+ def queries(self):
+ yield Query(self.url("", port=6789))
+
+ def check(self):
+ assert self.results[0].json["backend"] == self.target.path.k8s
+ assert self.results[0].json["request"]["tls"]["enabled"] == False
+
+
+class TCPMappingTLSOriginationBoolTest(AmbassadorTest):
+ extra_ports = [6789]
+ target: ServiceType
+
+ def init(self) -> None:
+ self.target = HTTP()
+
+ def manifests(self) -> str:
+ return (
+ format(
+ """
+---
+apiVersion: getambassador.io/v2
+kind: TCPMapping
+metadata:
+ name: {self.name.k8s}
+spec:
+ ambassador_id: [ {self.ambassador_id} ]
+ port: 6789
+ service: {self.target.path.fqdn}:443
+ tls: true
+"""
+ )
+ + super().manifests()
+ )
+
+ def queries(self):
+ yield Query(self.url("", port=6789))
+
+ def check(self):
+ assert self.results[0].json["backend"] == self.target.path.k8s
+ assert self.results[0].json["request"]["tls"]["enabled"] == True
+
+
+class TCPMappingTLSOriginationV2SchemeTest(AmbassadorTest):
+ """apiVersion v2 TCPMappings don't support a scheme:// on the 'service' field; if you provide
+ one, then it is ignored. Since apiVersion v3alpha1 adds support for scheme://, add a test to
+ make sure we don't break anyone who is inadvertently depending on it being ignored in v2."""
+
+ extra_ports = [6789, 6790]
+ target: ServiceType
+
+ def init(self) -> None:
+ self.xfail = "bug (2.3): v2 TCPMappings don't ignore the scheme"
+ self.target = HTTP()
+
+ def manifests(self) -> str:
+ return (
+ format(
+ """
+---
+apiVersion: getambassador.io/v2
+kind: TCPMapping
+metadata:
+ name: {self.name.k8s}-1
+spec:
+ ambassador_id: [ {self.ambassador_id} ]
+ port: 6789
+ service: https://{self.target.path.fqdn}:443
+---
+apiVersion: getambassador.io/v2
+kind: TCPMapping
+metadata:
+ name: {self.name.k8s}-2
+spec:
+ ambassador_id: [ {self.ambassador_id} ]
+ port: 6790
+ service: https://{self.target.path.fqdn}:80
+"""
+ )
+ + super().manifests()
+ )
+
+ def queries(self):
+ yield Query(
+ self.url("", port=6789), expected=400
+ ) # kat-server returns HTTP 400 "Client sent an HTTP request to an HTTPS server."
+ yield Query(self.url("", port=6789, scheme="https"), insecure=True)
+ yield Query(self.url("", port=6790))
+
+ def check(self):
+ assert self.results[1].json["backend"] == self.target.path.k8s
+ assert self.results[1].json["request"]["tls"]["enabled"] == True
+ assert self.results[2].json["backend"] == self.target.path.k8s
+ assert self.results[2].json["request"]["tls"]["enabled"] == False
+
+
+class TCPMappingTLSOriginationV3SchemeTest(AmbassadorTest):
+ extra_ports = [6789]
+ target: ServiceType
+
+ def init(self) -> None:
+ self.target = HTTP()
+
+ def manifests(self) -> str:
+ return (
+ format(
+ """
+---
+apiVersion: getambassador.io/v3alpha1
+kind: TCPMapping
+metadata:
+ name: {self.name.k8s}-1
+spec:
+ ambassador_id: [ {self.ambassador_id} ]
+ port: 6789
+ service: https://{self.target.path.fqdn}:443
+"""
+ )
+ + super().manifests()
+ )
+
+ def queries(self):
+ yield Query(self.url("", port=6789))
+
+ def check(self):
+ assert self.results[0].json["backend"] == self.target.path.k8s
+ assert self.results[0].json["request"]["tls"]["enabled"] == True
+
+
+class TCPMappingTLSOriginationContextTest(AmbassadorTest):
+ extra_ports = [6789]
+ target: ServiceType
+
+ def init(self) -> None:
+ self.target = HTTP()
+
+ def manifests(self) -> str:
+ # Hafta provide a client cert, see https://github.com/emissary-ingress/emissary/issues/4476
+ return (
+ f"""
+---
+apiVersion: v1
+kind: Secret
+metadata:
+ name: {self.name.k8s}-clientcert
+type: kubernetes.io/tls
+data:
+ tls.crt: {TLSCerts["presto.example.com"].k8s_crt}
+ tls.key: {TLSCerts["presto.example.com"].k8s_key}
+---
+apiVersion: getambassador.io/v2
+kind: TLSContext
+metadata:
+ name: {self.name.k8s}-tlsclient
+spec:
+ ambassador_id: [ {self.ambassador_id} ]
+ secret: {self.name.k8s}-clientcert
+ sni: my-funny-name
+---
+apiVersion: getambassador.io/v2
+kind: TCPMapping
+metadata:
+ name: {self.name.k8s}
+spec:
+ ambassador_id: [ {self.ambassador_id} ]
+ port: 6789
+ service: {self.target.path.fqdn}:443
+ tls: {self.name.k8s}-tlsclient
+"""
+ + super().manifests()
+ )
+
+ def queries(self):
+ yield Query(self.url("", port=6789))
+
+ def check(self):
+ assert self.results[0].json["backend"] == self.target.path.k8s
+ assert self.results[0].json["request"]["tls"]["enabled"] == True
+ assert self.results[0].json["request"]["tls"]["server-name"] == "my-funny-name"
+
+
+class TCPMappingTLSOriginationContextWithDotTest(AmbassadorTest):
+ extra_ports = [6789]
+ target: ServiceType
+
+ def init(self) -> None:
+ self.target = HTTP()
+
+ def manifests(self) -> str:
+ # Hafta provide a client cert, see https://github.com/emissary-ingress/emissary/issues/4476
+ return (
+ f"""
+---
+apiVersion: v1
+kind: Secret
+metadata:
+ name: {self.name.k8s}-clientcert
+type: kubernetes.io/tls
+data:
+ tls.crt: {TLSCerts["presto.example.com"].k8s_crt}
+ tls.key: {TLSCerts["presto.example.com"].k8s_key}
+---
+apiVersion: getambassador.io/v2
+kind: TLSContext
+metadata:
+ name: {self.name.k8s}.tlsclient
+spec:
+ ambassador_id: [ {self.ambassador_id} ]
+ secret: {self.name.k8s}-clientcert
+ sni: my-hilarious-name
+---
+apiVersion: getambassador.io/v2
+kind: TCPMapping
+metadata:
+ name: {self.name.k8s}
+spec:
+ ambassador_id: [ {self.ambassador_id} ]
+ port: 6789
+ service: {self.target.path.fqdn}:443
+ tls: {self.name.k8s}.tlsclient
+"""
+ + super().manifests()
+ )
+
+ def queries(self):
+ yield Query(self.url("", port=6789))
+
+ def check(self):
+ assert self.results[0].json["backend"] == self.target.path.k8s
+ assert self.results[0].json["request"]["tls"]["enabled"] == True
+ assert self.results[0].json["request"]["tls"]["server-name"] == "my-hilarious-name"
+
+
+class TCPMappingTLSOriginationContextCrossNamespaceTest(AmbassadorTest):
+ """This test is a little funny. You can actually select a TLSContext from any namespace without
+ specifying the namespace. That's bad design, but at the same time we don't want to break anyone
+ by changing it."""
+
+ extra_ports = [6789]
+ target: ServiceType
+
+ def init(self) -> None:
+ self.target = HTTP()
+
+ def manifests(self) -> str:
+ # Hafta provide a client cert, see https://github.com/emissary-ingress/emissary/issues/4476
+ return (
+ namespace_manifest("other-namespace")
+ + f"""
+---
+apiVersion: v1
+kind: Secret
+metadata:
+ name: {self.name.k8s}-clientcert
+ namespace: other-namespace
+type: kubernetes.io/tls
+data:
+ tls.crt: {TLSCerts["presto.example.com"].k8s_crt}
+ tls.key: {TLSCerts["presto.example.com"].k8s_key}
+---
+apiVersion: getambassador.io/v2
+kind: TLSContext
+metadata:
+ name: {self.name.k8s}-tlsclient
+ namespace: other-namespace
+spec:
+ ambassador_id: [ {self.ambassador_id} ]
+ secret: {self.name.k8s}-clientcert
+ sni: my-hysterical-name
+---
+apiVersion: getambassador.io/v2
+kind: TCPMapping
+metadata:
+ name: {self.name.k8s}
+spec:
+ ambassador_id: [ {self.ambassador_id} ]
+ port: 6789
+ service: {self.target.path.fqdn}:443
+ tls: {self.name.k8s}-tlsclient
+"""
+ + super().manifests()
+ )
+
+ def queries(self):
+ yield Query(self.url("", port=6789))
+
+ def check(self):
+ assert self.results[0].json["backend"] == self.target.path.k8s
+ assert self.results[0].json["request"]["tls"]["enabled"] == True
+ assert self.results[0].json["request"]["tls"]["server-name"] == "my-hysterical-name"
+
+
+@abstract_test
+class TCPMappingTLSTerminationTest(AmbassadorTest):
+ tls_src: Literal["tlscontext", "host"]
+
+ @classmethod
+ def variants(cls) -> Generator[Node, None, None]:
+ for tls_src in ["tlscontext", "host"]:
+ yield cls(tls_src, name="{self.tls_src}")
+
+ def init(self, tls_src: Literal["tlscontext", "host"]) -> None:
+ self.tls_src = tls_src
+
+ def manifests(self) -> str:
+ return (
+ f"""
+---
+apiVersion: getambassador.io/v2
+kind: Host
+metadata:
+ name: {self.path.k8s}
+spec:
+ ambassador_id: [ {self.ambassador_id} ]
+ hostname: {self.path.fqdn}
+ acmeProvider:
+ authority: none
+ requestPolicy:
+ insecure:
+ action: Route
+ additionalPort: 8080
+"""
+ + super().manifests()
+ )
+
+
+class TCPMappingTLSTerminationBasicTest(TCPMappingTLSTerminationTest):
+ extra_ports = [6789]
+ target: ServiceType
+
+ def init(self, tls_src: Literal["tlscontext", "host"]) -> None:
+ super().init(tls_src)
+ self.target = HTTP()
+
+ def manifests(self) -> str:
+ return (
+ f"""
+---
+apiVersion: v1
+kind: Secret
+metadata:
+ name: {self.name.k8s}-servercert
+type: kubernetes.io/tls
+data:
+ tls.crt: {TLSCerts["tls-context-host-2"].k8s_crt}
+ tls.key: {TLSCerts["tls-context-host-2"].k8s_key}
+"""
+ + (
+ f"""
+---
+apiVersion: getambassador.io/v2
+kind: TLSContext
+metadata:
+ name: {self.name.k8s}-tlsserver
+spec:
+ ambassador_id: [ {self.ambassador_id} ]
+ secret: {self.name.k8s}-servercert
+ hosts: [ "tls-context-host-2" ]
+"""
+ if self.tls_src == "tlscontext"
+ else f"""
+---
+apiVersion: getambassador.io/v2
+kind: Host
+metadata:
+ name: {self.name.k8s}-tlsserver
+spec:
+ ambassador_id: [ {self.ambassador_id} ]
+ hostname: "tls-context-host-2"
+ tlsSecret:
+ name: {self.name.k8s}-servercert
+"""
+ )
+ + f"""
+---
+apiVersion: getambassador.io/v2
+kind: TCPMapping
+metadata:
+ name: {self.name.k8s}
+spec:
+ ambassador_id: [ {self.ambassador_id} ]
+ port: 6789
+ host: tls-context-host-2
+ service: {self.target.path.fqdn}:80
+"""
+ + super().manifests()
+ )
+
+ def queries(self):
+ yield Query(
+ self.url("", scheme="https", port=6789),
+ sni=True,
+ headers={"Host": "tls-context-host-2"},
+ ca_cert=TLSCerts["tls-context-host-2"].pubcert,
+ )
+
+ def check(self):
+ assert self.results[0].json["backend"] == self.target.path.k8s
+ assert self.results[0].json["request"]["tls"]["enabled"] == False
+
+
+class TCPMappingTLSTerminationCrossNamespaceTest(TCPMappingTLSTerminationTest):
+ extra_ports = [6789]
+ target: ServiceType
+
+ def init(self, tls_src: Literal["tlscontext", "host"]) -> None:
+ super().init(tls_src)
+ self.target = HTTP()
+
+ def manifests(self) -> str:
+ return (
+ namespace_manifest("other-namespace")
+ + f"""
+---
+apiVersion: v1
+kind: Secret
+metadata:
+ name: {self.name.k8s}-servercert
+ namespace: other-namespace
+type: kubernetes.io/tls
+data:
+ tls.crt: {TLSCerts["tls-context-host-2"].k8s_crt}
+ tls.key: {TLSCerts["tls-context-host-2"].k8s_key}
+"""
+ + (
+ f"""
+---
+apiVersion: getambassador.io/v2
+kind: TLSContext
+metadata:
+ name: {self.name.k8s}-tlsserver
+ namespace: other-namespace
+spec:
+ ambassador_id: [ {self.ambassador_id} ]
+ secret: {self.name.k8s}-servercert
+ hosts: [ "tls-context-host-2" ]
+"""
+ if self.tls_src == "tlscontext"
+ else f"""
+---
+apiVersion: getambassador.io/v2
+kind: Host
+metadata:
+ name: {self.name.k8s}-tlsserver
+ namespace: other-namespace
+spec:
+ ambassador_id: [ {self.ambassador_id} ]
+ hostname: "tls-context-host-2"
+ tlsSecret:
+ name: {self.name.k8s}-servercert
+"""
+ )
+ + f"""
+---
+apiVersion: getambassador.io/v2
+kind: TCPMapping
+metadata:
+ name: {self.name.k8s}
+spec:
+ ambassador_id: [ {self.ambassador_id} ]
+ port: 6789
+ host: tls-context-host-2
+ service: {self.target.path.fqdn}:80
+"""
+ + super().manifests()
+ )
+
+ def queries(self):
+ yield Query(
+ self.url("", scheme="https", port=6789),
+ sni=True,
+ headers={"Host": "tls-context-host-2"},
+ ca_cert=TLSCerts["tls-context-host-2"].pubcert,
+ )
+
+ def check(self):
+ assert self.results[0].json["backend"] == self.target.path.k8s
+ assert self.results[0].json["request"]["tls"]["enabled"] == False
+
+
+class TCPMappingSNISharedContextTest(TCPMappingTLSTerminationTest):
+ extra_ports = [6789]
+ target_a: ServiceType
+ target_b: ServiceType
+
+ def init(self, tls_src: Literal["tlscontext", "host"]) -> None:
+ super().init(tls_src)
+ self.target_a = HTTP(name="target-a")
+ self.target_b = HTTP(name="target-b")
+
+ def manifests(self) -> str:
+ # Note that TCPMapping.spec.host matches with TLSContext.spec.hosts based on simple string
+ # matching, not globbing. See irbasemapping.py:match_tls_context()
+ return (
+ f"""
+---
+apiVersion: v1
+kind: Secret
+metadata:
+ name: {self.name.k8s}-servercert
+type: kubernetes.io/tls
+data:
+ tls.crt: {TLSCerts["*.domain.com"].k8s_crt}
+ tls.key: {TLSCerts["*.domain.com"].k8s_key}
+"""
+ + (
+ f"""
+---
+apiVersion: getambassador.io/v2
+kind: TLSContext
+metadata:
+ name: {self.name.k8s}-tlsserver
+spec:
+ ambassador_id: [ {self.ambassador_id} ]
+ secret: {self.name.k8s}-servercert
+ hosts:
+ - "a.domain.com"
+ - "b.domain.com"
+"""
+ if self.tls_src == "tlscontext"
+ else f"""
+---
+apiVersion: getambassador.io/v2
+kind: Host
+metadata:
+ name: {self.name.k8s}-tlsserver
+spec:
+ ambassador_id: [ {self.ambassador_id} ]
+ hostname: "*.domain.com"
+ tlsSecret:
+ name: {self.name.k8s}-servercert
+ requestPolicy:
+ insecure:
+ action: Route
+ additionalPort: 8080
+"""
+ )
+ + f"""
+---
+apiVersion: getambassador.io/v2
+kind: TCPMapping
+metadata:
+ name: {self.name.k8s}-a
+spec:
+ ambassador_id: [ {self.ambassador_id} ]
+ port: 6789
+ host: a.domain.com
+ service: {self.target_a.path.fqdn}:80
+---
+apiVersion: getambassador.io/v2
+kind: TCPMapping
+metadata:
+ name: {self.name.k8s}-b
+spec:
+ ambassador_id: [ {self.ambassador_id} ]
+ port: 6789
+ host: b.domain.com
+ service: {self.target_b.path.fqdn}:80
+"""
+ + super().manifests()
+ )
+
+ def queries(self):
+ yield Query(
+ self.url("", scheme="https", port=6789),
+ sni=True,
+ headers={"Host": "a.domain.com"},
+ ca_cert=TLSCerts["*.domain.com"].pubcert,
+ )
+ yield Query(
+ self.url("", scheme="https", port=6789),
+ sni=True,
+ headers={"Host": "b.domain.com"},
+ ca_cert=TLSCerts["*.domain.com"].pubcert,
+ )
+
+ def check(self):
+ assert self.results[0].json["backend"] == self.target_a.path.k8s
+ assert self.results[0].json["request"]["tls"]["enabled"] == False
+ assert self.results[1].json["backend"] == self.target_b.path.k8s
+ assert self.results[1].json["request"]["tls"]["enabled"] == False
+
+
+class TCPMappingSNISeparateContextsTest(TCPMappingTLSTerminationTest):
+ extra_ports = [6789]
+ target_a: ServiceType
+ target_b: ServiceType
+
+ def init(self, tls_src: Literal["tlscontext", "host"]) -> None:
+ super().init(tls_src)
+ self.target_a = HTTP(name="target-a")
+ self.target_b = HTTP(name="target-b")
+
+ def manifests(self) -> str:
+ return (
+ f"""
+---
+apiVersion: v1
+kind: Secret
+metadata:
+ name: {self.name.k8s}-servercert-a
+type: kubernetes.io/tls
+data:
+ tls.crt: {TLSCerts["tls-context-host-1"].k8s_crt}
+ tls.key: {TLSCerts["tls-context-host-1"].k8s_key}
+---
+apiVersion: v1
+kind: Secret
+metadata:
+ name: {self.name.k8s}-servercert-b
+type: kubernetes.io/tls
+data:
+ tls.crt: {TLSCerts["tls-context-host-2"].k8s_crt}
+ tls.key: {TLSCerts["tls-context-host-2"].k8s_key}
+"""
+ + (
+ f"""
+---
+apiVersion: getambassador.io/v2
+kind: TLSContext
+metadata:
+ name: {self.name.k8s}-tlsserver-a
+spec:
+ ambassador_id: [ {self.ambassador_id} ]
+ secret: {self.name.k8s}-servercert-a
+ hosts: [tls-context-host-1]
+---
+apiVersion: getambassador.io/v2
+kind: TLSContext
+metadata:
+ name: {self.name.k8s}-tlsserver-b
+spec:
+ ambassador_id: [ {self.ambassador_id} ]
+ secret: {self.name.k8s}-servercert-b
+ hosts: [tls-context-host-2]
+"""
+ if self.tls_src == "tlscontext"
+ else f"""
+---
+apiVersion: getambassador.io/v2
+kind: Host
+metadata:
+ name: {self.name.k8s}-tlsserver-a
+spec:
+ ambassador_id: [ {self.ambassador_id} ]
+ hostname: "tls-context-host-1"
+ tlsSecret:
+ name: {self.name.k8s}-servercert-a
+---
+apiVersion: getambassador.io/v2
+kind: Host
+metadata:
+ name: {self.name.k8s}-tlsserver-b
+spec:
+ ambassador_id: [ {self.ambassador_id} ]
+ hostname: "tls-context-host-2"
+ tlsSecret:
+ name: {self.name.k8s}-servercert-b
+"""
+ )
+ + f"""
+---
+apiVersion: getambassador.io/v2
+kind: TCPMapping
+metadata:
+ name: {self.name.k8s}-a
+spec:
+ ambassador_id: [ {self.ambassador_id} ]
+ port: 6789
+ host: tls-context-host-1
+ service: {self.target_a.path.fqdn}:80
+---
+apiVersion: getambassador.io/v2
+kind: TCPMapping
+metadata:
+ name: {self.name.k8s}-b
+spec:
+ ambassador_id: [ {self.ambassador_id} ]
+ port: 6789
+ host: tls-context-host-2
+ service: {self.target_b.path.fqdn}:80
+"""
+ + super().manifests()
+ )
+
+ def queries(self):
+ yield Query(
+ self.url("", scheme="https", port=6789),
+ sni=True,
+ headers={"Host": "tls-context-host-1"},
+ ca_cert=TLSCerts["tls-context-host-1"].pubcert,
+ )
+ yield Query(
+ self.url("", scheme="https", port=6789),
+ sni=True,
+ headers={"Host": "tls-context-host-2"},
+ ca_cert=TLSCerts["tls-context-host-2"].pubcert,
+ )
+
+ def check(self):
+ assert self.results[0].json["backend"] == self.target_a.path.k8s
+ assert self.results[0].json["request"]["tls"]["enabled"] == False
+ assert self.results[1].json["backend"] == self.target_b.path.k8s
+ assert self.results[1].json["request"]["tls"]["enabled"] == False
+
+
+class TCPMappingSNIWithHTTPTest(AmbassadorTest):
+ # Note: TCPMappingSNIWithHTTPTest does *not* inherit from TCPMappingTLSTerminationTest because
+ # TCPMappingSNIWithHTTPTest wants to take more ownership of the HTTP Host.
+
+ target: ServiceType
+
+ tls_src: Literal["tlscontext", "host"]
+
+ @classmethod
+ def variants(cls) -> Generator[Node, None, None]:
+ for tls_src in ["tlscontext", "host"]:
+ yield cls(tls_src, name="{self.tls_src}")
+
+ def init(self, tls_src: Literal["tlscontext", "host"]) -> None:
+ self.tls_src = tls_src
+ self.target = HTTP()
+
+ def manifests(self) -> str:
+ return (
+ f"""
+# HTTP Host ##########################################################
+---
+apiVersion: v1
+kind: Secret
+metadata:
+ name: {self.name.k8s}
+type: kubernetes.io/tls
+data:
+ tls.crt: {TLSCerts["tls-context-host-1"].k8s_crt}
+ tls.key: {TLSCerts["tls-context-host-1"].k8s_key}
+---
+apiVersion: getambassador.io/v2
+kind: Host
+metadata:
+ name: {self.path.k8s}
+spec:
+ ambassador_id: [ {self.ambassador_id} ]
+ hostname: {self.path.fqdn}
+ acmeProvider:
+ authority: none
+ tlsSecret:
+ name: {self.name.k8s}
+# TCPMapping #########################################################
+---
+apiVersion: v1
+kind: Secret
+metadata:
+ name: {self.name.k8s}-servercert
+type: kubernetes.io/tls
+data:
+ tls.crt: {TLSCerts["tls-context-host-2"].k8s_crt}
+ tls.key: {TLSCerts["tls-context-host-2"].k8s_key}
+"""
+ + (
+ f"""
+---
+apiVersion: getambassador.io/v2
+kind: TLSContext
+metadata:
+ name: {self.name.k8s}-tlsserver
+spec:
+ ambassador_id: [ {self.ambassador_id} ]
+ secret: {self.name.k8s}-servercert
+ hosts: [ "tls-context-host-2" ]
+"""
+ if self.tls_src == "tlscontext"
+ else f"""
+---
+apiVersion: getambassador.io/v2
+kind: Host
+metadata:
+ name: {self.name.k8s}-tlsserver
+spec:
+ ambassador_id: [ {self.ambassador_id} ]
+ hostname: "tls-context-host-2"
+ tlsSecret:
+ name: {self.name.k8s}-servercert
+"""
+ )
+ + f"""
+---
+apiVersion: getambassador.io/v2
+kind: TCPMapping
+metadata:
+ name: {self.name.k8s}
+spec:
+ ambassador_id: [ {self.ambassador_id} ]
+ port: 8443
+ host: tls-context-host-2
+ service: {self.target.path.fqdn}:80
+"""
+ + super().manifests()
+ )
+
+ def scheme(self):
+ return "https"
+
+ def queries(self):
+ yield Query(
+ self.url(""),
+ sni=True,
+ headers={"Host": "tls-context-host-2"},
+ ca_cert=TLSCerts["tls-context-host-2"].pubcert,
+ )
+
+ def check(self):
+ assert self.results[0].json["backend"] == self.target.path.k8s
+ assert self.results[0].json["request"]["tls"]["enabled"] == False
+
+
+class TCPMappingAddressTest(AmbassadorTest):
+ extra_ports = [6789, 6790]
+ target: ServiceType
+
+ def init(self) -> None:
+ self.target = HTTP()
+
+ def manifests(self) -> str:
+ return (
+ format(
+ """
+---
+apiVersion: getambassador.io/v2
+kind: TCPMapping
+metadata:
+ name: {self.name.k8s}-local-only
+spec:
+ ambassador_id: [ {self.ambassador_id} ]
+ port: 6789
+ address: 127.0.0.1
+ service: {self.target.path.fqdn}:80
+---
+apiVersion: getambassador.io/v2
+kind: TCPMapping
+metadata:
+ name: {self.name.k8s}-proxy
+spec:
+ ambassador_id: [ {self.ambassador_id} ]
+ port: 6790
+ service: localhost:6789
+"""
+ )
+ + super().manifests()
+ )
+
+ def queries(self):
+ # Check that it only bound to localhost and doesn't allow external connections.
+ yield Query(
+ self.url("", port=6789), error=["connection reset by peer", "EOF", "connection refused"]
+ )
+ # Use a second mapping that proxies to the first to check that it was even created.
+ yield Query(self.url("", port=6790))
+
+ def check(self):
+ assert self.results[1].json["backend"] == self.target.path.k8s
+ assert self.results[1].json["request"]["tls"]["enabled"] == False
+
+
+class TCPMappingWeightTest(AmbassadorTest):
+ extra_ports = [6789]
+ target70: ServiceType
+ target30: ServiceType
+
+ def init(self) -> None:
+ self.target70 = HTTP(name="tgt70")
+ self.target30 = HTTP(name="tgt30")
+
+ def manifests(self) -> str:
+ return (
+ format(
+ """
+---
+apiVersion: getambassador.io/v2
+kind: TCPMapping
+metadata:
+ name: {self.name.k8s}-70
+spec:
+ ambassador_id: [ {self.ambassador_id} ]
+ port: 6789
+ service: {self.target70.path.fqdn}:80
+ weight: 70
+---
+apiVersion: getambassador.io/v2
+kind: TCPMapping
+metadata:
+ name: {self.name.k8s}-30
+spec:
+ ambassador_id: [ {self.ambassador_id} ]
+ port: 6789
+ service: {self.target30.path.fqdn}:80
+ weight: 30
+"""
+ )
+ + super().manifests()
+ )
+
+ def queries(self):
+ for i in range(1000):
+ yield Query(self.url("", port=6789))
+
+ def check(self):
+ counts: Dict[str, int] = {}
+ for result in self.results:
+ backend = result.json["backend"]
+ counts[backend] = counts.get(backend, 0) + 1
+ assert counts[self.target70.path.k8s] + counts[self.target30.path.k8s] == 1000
+ # Probabalistic, margin might need tuned
+ margin = 150
+ assert abs(counts[self.target70.path.k8s] - 700) < margin
+ assert abs(counts[self.target30.path.k8s] - 300) < margin
+
+
+# TODO: Add tests for all of the config knobs for the upstream connection:
+# - enable_ipv4: false
+# - enable_ipv6: false
+# - circuit_breakers
+# - idle_timeout_ms
+# - resolver
+#
+# TODO: Add tests for the config knobs for stats:
+# - cluster_tag
+# - stats_name (v3alpha1 only)
diff --git a/python/tests/kat/t_tls.py b/python/tests/kat/t_tls.py
index b9547cc99a..15aa76fa62 100644
--- a/python/tests/kat/t_tls.py
+++ b/python/tests/kat/t_tls.py
@@ -1,3 +1,5 @@
+import hashlib
+from base64 import b64decode
from typing import Generator, List, Tuple, Union
from abstract_tests import HTTP, AmbassadorTest, Node, ServiceType
@@ -193,19 +195,28 @@ def queries(self):
)
def check(self):
+ cert = TLSCerts["presto.example.com"].pubcert
+ # base64-decode the cert data after removing the "---BEGIN CERTIFICATE---" / "---END CERTIFICATE---" lines.
+ certraw = b64decode("\n".join(l for l in cert.split("\n") if not l.startswith("-")))
+ # take the sha256 sum aof that.
+ certhash = hashlib.sha256(certraw).hexdigest()
+
assert self.results[0].backend
assert self.results[0].backend.request
assert self.results[0].backend.request.headers["x-forwarded-client-cert"] == [
- 'Hash=c2d41a5977dcd28a3ba21f59ed5508cc6538defa810843d8a593e668306c8c4f;Subject="CN=presto.example.com,OU=Engineering,O=Presto,L=Bangalore,ST=KA,C=IN"'
- ]
+ f'Hash={certhash};Subject="CN=presto.example.com,OU=Engineering,O=Ambassador Labs,L=Boston,ST=MA,C=US"'
+ ], (
+ "unexpected x-forwarded-client-cert value: %s"
+ % self.results[0].backend.request.headers["x-forwarded-client-cert"]
+ )
assert self.results[0].backend.request.headers["x-cert-start"] == [
- "2019-01-10T19:19:52.000Z"
+ "2021-11-10T13:12:00.000Z"
], (
"unexpected x-cert-start value: %s"
% self.results[0].backend.request.headers["x-cert-start"]
)
assert self.results[0].backend.request.headers["x-cert-end"] == [
- "2118-12-17T19:19:52.000Z"
+ "2099-11-10T13:12:00.000Z"
], (
"unexpected x-cert-end value: %s"
% self.results[0].backend.request.headers["x-cert-end"]
@@ -213,13 +224,13 @@ def check(self):
assert self.results[1].backend
assert self.results[1].backend.request
assert self.results[0].backend.request.headers["x-cert-start-custom"] == [
- "Jan 10 19:19:52 2019 UTC"
+ "Nov 10 13:12:00 2021 UTC"
], (
"unexpected x-cert-start-custom value: %s"
% self.results[1].backend.request.headers["x-cert-start-custom"]
)
assert self.results[0].backend.request.headers["x-cert-end-custom"] == [
- "Dec 17 19:19:52 2118 UTC"
+ "Nov 10 13:12:00 2099 UTC"
], (
"unexpected x-cert-end-custom value: %s"
% self.results[0].backend.request.headers["x-cert-end-custom"]
@@ -454,8 +465,17 @@ def manifests(self) -> str:
)
def config(self) -> Generator[Union[str, Tuple[Node, str]], None, None]:
- yield self, self.format(
- """
+ fingerprint = (
+ hashlib.sha1(
+ (
+ TLSCerts["localhost"].pubcert + "\n" + TLSCerts["localhost"].privkey + "\n"
+ ).encode("utf-8")
+ )
+ .hexdigest()
+ .upper()
+ )
+
+ yield self, f"""
---
apiVersion: getambassador.io/v3alpha1
kind: Module
@@ -465,10 +485,9 @@ def config(self) -> Generator[Union[str, Tuple[Node, str]], None, None]:
upstream:
secret: test-origination-secret
upstream-files:
- cert_chain_file: /tmp/ambassador/snapshots/default/secrets-decoded/test-origination-secret/F94E4DCF30ABC50DEF240AA8024599B67CC03991.crt
- private_key_file: /tmp/ambassador/snapshots/default/secrets-decoded/test-origination-secret/F94E4DCF30ABC50DEF240AA8024599B67CC03991.key
+ cert_chain_file: /tmp/ambassador/snapshots/default/secrets-decoded/test-origination-secret/{fingerprint}.crt
+ private_key_file: /tmp/ambassador/snapshots/default/secrets-decoded/test-origination-secret/{fingerprint}.key
"""
- )
yield self, self.format(
"""