Add ALPN support for TLS-terminated HTTPS LBs
ALPN is a TLS extension for application-layer protocol negotiation within the TLS handshake [1]. This patch extends the Listener API to include a new 'alpn_protocols' parameter. With this parameter, users can set an ALPN preference list (descending order of preference). Presently, the amphora provider driver is limited to http/1.0 and http/1.1 ALPN protocol IDs. Support for "h2" (HTTP/2 over TLS) depends on HAProxy 2.0 or newer. [1] https://tools.ietf.org/html/rfc7301 Change-Id: If08a8169498cdfaa75440e8971ba0caff45ac4c4
This commit is contained in:
parent
0b1d8dd5e7
commit
a5f0524fd0
@ -183,6 +183,22 @@ allowed_cidrs-optional:
|
|||||||
min_version: 2.12
|
min_version: 2.12
|
||||||
required: false
|
required: false
|
||||||
type: array
|
type: array
|
||||||
|
alpn_protocols:
|
||||||
|
description: |
|
||||||
|
A list of ALPN protocols.
|
||||||
|
Available protocols: http/1.0, http/1.1, h2
|
||||||
|
in: body
|
||||||
|
min_version: 2.20
|
||||||
|
required: true
|
||||||
|
type: array
|
||||||
|
alpn_protocols-optional:
|
||||||
|
description: |
|
||||||
|
A list of ALPN protocols.
|
||||||
|
Available protocols: http/1.0, http/1.1, h2
|
||||||
|
in: body
|
||||||
|
min_version: 2.20
|
||||||
|
required: false
|
||||||
|
type: array
|
||||||
amphora-id:
|
amphora-id:
|
||||||
description: |
|
description: |
|
||||||
The associated amphora ID.
|
The associated amphora ID.
|
||||||
|
@ -1 +1 @@
|
|||||||
curl -X POST -H "Content-Type: application/json" -H "X-Auth-Token: <token>" -d '{"listener": {"protocol": "TERMINATED_HTTPS", "description": "A great TLS listener", "admin_state_up": true, "connection_limit": 200, "protocol_port": "443", "loadbalancer_id": "607226db-27ef-4d41-ae89-f2a800e9c2db", "name": "great_tls_listener", "insert_headers": {"X-Forwarded-For": "true", "X-Forwarded-Port": "true"}, "default_tls_container_ref": "http://198.51.100.10:9311/v1/containers/a570068c-d295-4780-91d4-3046a325db51", "sni_container_refs": ["http://198.51.100.10:9311/v1/containers/a570068c-d295-4780-91d4-3046a325db51", "http://198.51.100.10:9311/v1/containers/aaebb31e-7761-4826-8cb4-2b829caca3ee"], "timeout_client_data": 50000, "timeout_member_connect": 5000, "timeout_member_data": 50000, "timeout_tcp_inspect": 0, "tags": ["test_tag"], "client_ca_tls_container_ref": "http://198.51.100.10:9311/v1/containers/35649991-49f3-4625-81ce-2465fe8932e5", "client_authentication": "MANDATORY", "client_crl_container_ref": "http://198.51.100.10:9311/v1/containers/e222b065-b93b-4e2a-9a02-804b7a118c3c", "allowed_cidrs": ["192.0.2.0/24", "198.51.100.0/24"], "tls_ciphers": "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256", "tls_versions": ["TLSv1.2", "TLSv1.3"]}}' http://198.51.100.10:9876/v2/lbaas/listeners
|
curl -X POST -H "Content-Type: application/json" -H "X-Auth-Token: <token>" -d '{"listener": {"protocol": "TERMINATED_HTTPS", "description": "A great TLS listener", "admin_state_up": true, "connection_limit": 200, "protocol_port": "443", "loadbalancer_id": "607226db-27ef-4d41-ae89-f2a800e9c2db", "name": "great_tls_listener", "insert_headers": {"X-Forwarded-For": "true", "X-Forwarded-Port": "true"}, "default_tls_container_ref": "http://198.51.100.10:9311/v1/containers/a570068c-d295-4780-91d4-3046a325db51", "sni_container_refs": ["http://198.51.100.10:9311/v1/containers/a570068c-d295-4780-91d4-3046a325db51", "http://198.51.100.10:9311/v1/containers/aaebb31e-7761-4826-8cb4-2b829caca3ee"], "timeout_client_data": 50000, "timeout_member_connect": 5000, "timeout_member_data": 50000, "timeout_tcp_inspect": 0, "tags": ["test_tag"], "client_ca_tls_container_ref": "http://198.51.100.10:9311/v1/containers/35649991-49f3-4625-81ce-2465fe8932e5", "client_authentication": "MANDATORY", "client_crl_container_ref": "http://198.51.100.10:9311/v1/containers/e222b065-b93b-4e2a-9a02-804b7a118c3c", "allowed_cidrs": ["192.0.2.0/24", "198.51.100.0/24"], "tls_ciphers": "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256", "tls_versions": ["TLSv1.2", "TLSv1.3"], "alpn_protocols": ["http/1.1", "http/1.0"]}}' http://198.51.100.10:9876/v2/lbaas/listeners
|
||||||
|
@ -29,6 +29,7 @@
|
|||||||
"198.51.100.0/24"
|
"198.51.100.0/24"
|
||||||
],
|
],
|
||||||
"tls_ciphers": "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256",
|
"tls_ciphers": "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256",
|
||||||
"tls_versions": ["TLSv1.2", "TLSv1.3"]
|
"tls_versions": ["TLSv1.2", "TLSv1.3"],
|
||||||
|
"alpn_protocols": ["http/1.1", "http/1.0"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -44,6 +44,7 @@
|
|||||||
"198.51.100.0/24"
|
"198.51.100.0/24"
|
||||||
],
|
],
|
||||||
"tls_ciphers": "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256",
|
"tls_ciphers": "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256",
|
||||||
"tls_versions": ["TLSv1.2", "TLSv1.3"]
|
"tls_versions": ["TLSv1.2", "TLSv1.3"],
|
||||||
|
"alpn_protocols": ["http/1.1", "http/1.0"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -44,6 +44,7 @@
|
|||||||
"198.51.100.0/24"
|
"198.51.100.0/24"
|
||||||
],
|
],
|
||||||
"tls_ciphers": "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256",
|
"tls_ciphers": "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256",
|
||||||
"tls_versions": ["TLSv1.2", "TLSv1.3"]
|
"tls_versions": ["TLSv1.2", "TLSv1.3"],
|
||||||
|
"alpn_protocols": ["http/1.1", "http/1.0"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1 +1 @@
|
|||||||
curl -X PUT -H "Content-Type: application/json" -H "X-Auth-Token: <token>" -d '{"listener": {"description": "An updated great TLS listener", "admin_state_up": true, "connection_limit": 200, "name": "great_updated_tls_listener", "insert_headers": {"X-Forwarded-For": "false", "X-Forwarded-Port": "true"}, "default_tls_container_ref": "http://198.51.100.10:9311/v1/containers/a570068c-d295-4780-91d4-3046a325db51", "sni_container_refs": ["http://198.51.100.10:9311/v1/containers/a570068c-d295-4780-91d4-3046a325db51", "http://198.51.100.10:9311/v1/containers/aaebb31e-7761-4826-8cb4-2b829caca3ee"], "timeout_client_data": 100000, "timeout_member_connect": 1000, "timeout_member_data": 100000, "timeout_tcp_inspect": 5, "tags": ["updated_tag"], "client_ca_tls_container_ref": null, "allowed_cidrs": ["192.0.2.0/24", "198.51.100.0/24"], "tls_ciphers": "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256", "tls_versions": ["TLSv1.2", "TLSv1.3"]}}' http://198.51.100.10:9876/v2/lbaas/listeners/023f2e34-7806-443b-bfae-16c324569a3d
|
curl -X PUT -H "Content-Type: application/json" -H "X-Auth-Token: <token>" -d '{"listener": {"description": "An updated great TLS listener", "admin_state_up": true, "connection_limit": 200, "name": "great_updated_tls_listener", "insert_headers": {"X-Forwarded-For": "false", "X-Forwarded-Port": "true"}, "default_tls_container_ref": "http://198.51.100.10:9311/v1/containers/a570068c-d295-4780-91d4-3046a325db51", "sni_container_refs": ["http://198.51.100.10:9311/v1/containers/a570068c-d295-4780-91d4-3046a325db51", "http://198.51.100.10:9311/v1/containers/aaebb31e-7761-4826-8cb4-2b829caca3ee"], "timeout_client_data": 100000, "timeout_member_connect": 1000, "timeout_member_data": 100000, "timeout_tcp_inspect": 5, "tags": ["updated_tag"], "client_ca_tls_container_ref": null, "allowed_cidrs": ["192.0.2.0/24", "198.51.100.0/24"], "tls_ciphers": "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256", "tls_versions": ["TLSv1.2", "TLSv1.3"], "alpn_protocols": ["http/1.1", "http/1.0"]}}' http://198.51.100.10:9876/v2/lbaas/listeners/023f2e34-7806-443b-bfae-16c324569a3d
|
||||||
|
@ -25,6 +25,7 @@
|
|||||||
"198.51.100.0/24"
|
"198.51.100.0/24"
|
||||||
],
|
],
|
||||||
"tls_ciphers": "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256",
|
"tls_ciphers": "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256",
|
||||||
"tls_versions": ["TLSv1.2", "TLSv1.3"]
|
"tls_versions": ["TLSv1.2", "TLSv1.3"],
|
||||||
|
"alpn_protocols": ["http/1.1", "http/1.0"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -44,6 +44,7 @@
|
|||||||
"198.51.100.0/24"
|
"198.51.100.0/24"
|
||||||
],
|
],
|
||||||
"tls_ciphers": "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256",
|
"tls_ciphers": "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256",
|
||||||
"tls_versions": ["TLSv1.2", "TLSv1.3"]
|
"tls_versions": ["TLSv1.2", "TLSv1.3"],
|
||||||
|
"alpn_protocols": ["http/1.1", "http/1.0"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -46,7 +46,8 @@
|
|||||||
"198.51.100.0/24"
|
"198.51.100.0/24"
|
||||||
],
|
],
|
||||||
"tls_ciphers": "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256",
|
"tls_ciphers": "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256",
|
||||||
"tls_versions": ["TLSv1.2", "TLSv1.3"]
|
"tls_versions": ["TLSv1.2", "TLSv1.3"],
|
||||||
|
"alpn_protocols": ["http/1.1", "http/1.0"]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -47,6 +47,7 @@ Response Parameters
|
|||||||
|
|
||||||
- admin_state_up: admin_state_up
|
- admin_state_up: admin_state_up
|
||||||
- allowed_cidrs: allowed_cidrs
|
- allowed_cidrs: allowed_cidrs
|
||||||
|
- alpn_protocols: alpn_protocols
|
||||||
- client_authentication: client_authentication
|
- client_authentication: client_authentication
|
||||||
- client_ca_tls_container_ref: client_ca_tls_container_ref
|
- client_ca_tls_container_ref: client_ca_tls_container_ref
|
||||||
- client_crl_container_ref: client_crl_container_ref
|
- client_crl_container_ref: client_crl_container_ref
|
||||||
@ -143,6 +144,7 @@ Request
|
|||||||
|
|
||||||
- admin_state_up: admin_state_up-default-optional
|
- admin_state_up: admin_state_up-default-optional
|
||||||
- allowed_cidrs: allowed_cidrs-optional
|
- allowed_cidrs: allowed_cidrs-optional
|
||||||
|
- alpn_protocols: alpn_protocols-optional
|
||||||
- client_authentication: client_authentication-optional
|
- client_authentication: client_authentication-optional
|
||||||
- client_ca_tls_container_ref: client_ca_tls_container_ref-optional
|
- client_ca_tls_container_ref: client_ca_tls_container_ref-optional
|
||||||
- client_crl_container_ref: client_crl_container_ref-optional
|
- client_crl_container_ref: client_crl_container_ref-optional
|
||||||
@ -266,6 +268,7 @@ Response Parameters
|
|||||||
|
|
||||||
- admin_state_up: admin_state_up
|
- admin_state_up: admin_state_up
|
||||||
- allowed_cidrs: allowed_cidrs
|
- allowed_cidrs: allowed_cidrs
|
||||||
|
- alpn_protocols: alpn_protocols
|
||||||
- client_authentication: client_authentication
|
- client_authentication: client_authentication
|
||||||
- client_ca_tls_container_ref: client_ca_tls_container_ref
|
- client_ca_tls_container_ref: client_ca_tls_container_ref
|
||||||
- client_crl_container_ref: client_crl_container_ref
|
- client_crl_container_ref: client_crl_container_ref
|
||||||
@ -346,6 +349,7 @@ Response Parameters
|
|||||||
|
|
||||||
- admin_state_up: admin_state_up
|
- admin_state_up: admin_state_up
|
||||||
- allowed_cidrs: allowed_cidrs
|
- allowed_cidrs: allowed_cidrs
|
||||||
|
- alpn_protocols: alpn_protocols
|
||||||
- client_authentication: client_authentication
|
- client_authentication: client_authentication
|
||||||
- client_ca_tls_container_ref: client_ca_tls_container_ref
|
- client_ca_tls_container_ref: client_ca_tls_container_ref
|
||||||
- client_crl_container_ref: client_crl_container_ref
|
- client_crl_container_ref: client_crl_container_ref
|
||||||
@ -416,6 +420,7 @@ Request
|
|||||||
|
|
||||||
- admin_state_up: admin_state_up-default-optional
|
- admin_state_up: admin_state_up-default-optional
|
||||||
- allowed_cidrs: allowed_cidrs-optional
|
- allowed_cidrs: allowed_cidrs-optional
|
||||||
|
- alpn_protocols: alpn_protocols-optional
|
||||||
- client_authentication: client_authentication-optional
|
- client_authentication: client_authentication-optional
|
||||||
- client_ca_tls_container_ref: client_ca_tls_container_ref-optional
|
- client_ca_tls_container_ref: client_ca_tls_container_ref-optional
|
||||||
- client_crl_container_ref: client_crl_container_ref-optional
|
- client_crl_container_ref: client_crl_container_ref-optional
|
||||||
@ -454,6 +459,7 @@ Response Parameters
|
|||||||
|
|
||||||
- admin_state_up: admin_state_up
|
- admin_state_up: admin_state_up
|
||||||
- allowed_cidrs: allowed_cidrs
|
- allowed_cidrs: allowed_cidrs
|
||||||
|
- alpn_protocols: alpn_protocols
|
||||||
- client_authentication: client_authentication
|
- client_authentication: client_authentication
|
||||||
- client_ca_tls_container_ref: client_ca_tls_container_ref
|
- client_ca_tls_container_ref: client_ca_tls_container_ref
|
||||||
- client_crl_container_ref: client_crl_container_ref
|
- client_crl_container_ref: client_crl_container_ref
|
||||||
|
@ -38,6 +38,14 @@ cli=openstack loadbalancer listener create [--allowed-cidr <allowed_cidr>] <load
|
|||||||
driver.amphora=complete
|
driver.amphora=complete
|
||||||
driver.ovn=missing
|
driver.ovn=missing
|
||||||
|
|
||||||
|
[operation.alpn_protocol]
|
||||||
|
title=alpn_protocol
|
||||||
|
status=optional
|
||||||
|
notes=List of accepted ALPN protocols (can be set multiple times).
|
||||||
|
cli=openstack loadbalancer listener create [--alpn-protocol <protocol>] <loadbalancer>
|
||||||
|
driver.amphora=complete
|
||||||
|
driver.ovn=missing
|
||||||
|
|
||||||
[operation.client_authentication]
|
[operation.client_authentication]
|
||||||
title=client_authentication
|
title=client_authentication
|
||||||
status=optional
|
status=optional
|
||||||
|
@ -84,6 +84,11 @@
|
|||||||
# either. Available versions: SSLv3, TLSv1, TLSv1.1, TLSv1.2, TLSv1.3
|
# either. Available versions: SSLv3, TLSv1, TLSv1.1, TLSv1.2, TLSv1.3
|
||||||
# minimum_tls_version =
|
# minimum_tls_version =
|
||||||
|
|
||||||
|
# List of default ALPN protocols to be used on new TLS-terminated
|
||||||
|
# listeners. Available protocols: http/1.0, http/1.1, h2
|
||||||
|
# default_listener_alpn_protocols = http/1.1, http/1.0
|
||||||
|
|
||||||
|
|
||||||
[database]
|
[database]
|
||||||
# This line MUST be changed to actually run the plugin.
|
# This line MUST be changed to actually run the plugin.
|
||||||
# Example:
|
# Example:
|
||||||
|
@ -70,7 +70,7 @@ munch==2.2.0
|
|||||||
netaddr==0.7.19
|
netaddr==0.7.19
|
||||||
netifaces==0.10.4
|
netifaces==0.10.4
|
||||||
networkx==1.11
|
networkx==1.11
|
||||||
octavia-lib==2.0.0
|
octavia-lib==2.2.0
|
||||||
openstacksdk==0.12.0
|
openstacksdk==0.12.0
|
||||||
os-client-config==1.29.0
|
os-client-config==1.29.0
|
||||||
os-service-types==1.2.0
|
os-service-types==1.2.0
|
||||||
|
@ -18,6 +18,7 @@ from dateutil import parser
|
|||||||
import netaddr
|
import netaddr
|
||||||
from wsme import types as wtypes
|
from wsme import types as wtypes
|
||||||
|
|
||||||
|
from octavia.common import constants
|
||||||
from octavia.common import exceptions
|
from octavia.common import exceptions
|
||||||
from octavia.common import validate
|
from octavia.common import validate
|
||||||
|
|
||||||
@ -55,6 +56,19 @@ class CidrType(wtypes.UserType):
|
|||||||
raise ValueError(error) from e
|
raise ValueError(error) from e
|
||||||
|
|
||||||
|
|
||||||
|
class AlpnProtocolType(wtypes.UserType):
|
||||||
|
basetype = str
|
||||||
|
name = 'alpn_protocol'
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def validate(value):
|
||||||
|
"""Validates whether value is a valid ALPN protocol ID."""
|
||||||
|
if value in constants.SUPPORTED_ALPN_PROTOCOLS:
|
||||||
|
return value
|
||||||
|
error = 'Value should be a valid ALPN protocol ID'
|
||||||
|
raise ValueError(error)
|
||||||
|
|
||||||
|
|
||||||
class URLType(wtypes.UserType):
|
class URLType(wtypes.UserType):
|
||||||
basetype = str
|
basetype = str
|
||||||
name = 'url'
|
name = 'url'
|
||||||
|
@ -62,6 +62,19 @@ class AmphoraProviderDriver(driver_base.ProviderDriver):
|
|||||||
user_fault_string=msg,
|
user_fault_string=msg,
|
||||||
operator_fault_string=msg)
|
operator_fault_string=msg)
|
||||||
|
|
||||||
|
def _validate_alpn_protocols(self, listener):
|
||||||
|
if not listener.alpn_protocols:
|
||||||
|
return
|
||||||
|
supported = consts.AMPHORA_SUPPORTED_ALPN_PROTOCOLS
|
||||||
|
not_supported = set(listener.alpn_protocols) - set(supported)
|
||||||
|
if not_supported:
|
||||||
|
msg = ('Amphora provider does not support %s ALPN protocol(s). '
|
||||||
|
'Supported: %s'
|
||||||
|
% (", ".join(not_supported), ", ".join(supported)))
|
||||||
|
raise exceptions.UnsupportedOptionError(
|
||||||
|
user_fault_string=msg,
|
||||||
|
operator_fault_string=msg)
|
||||||
|
|
||||||
# Load Balancer
|
# Load Balancer
|
||||||
def create_vip_port(self, loadbalancer_id, project_id, vip_dictionary):
|
def create_vip_port(self, loadbalancer_id, project_id, vip_dictionary):
|
||||||
vip_obj = driver_utils.provider_vip_dict_to_vip_obj(vip_dictionary)
|
vip_obj = driver_utils.provider_vip_dict_to_vip_obj(vip_dictionary)
|
||||||
@ -123,6 +136,7 @@ class AmphoraProviderDriver(driver_base.ProviderDriver):
|
|||||||
|
|
||||||
# Listener
|
# Listener
|
||||||
def listener_create(self, listener):
|
def listener_create(self, listener):
|
||||||
|
self._validate_alpn_protocols(listener)
|
||||||
payload = {consts.LISTENER_ID: listener.listener_id}
|
payload = {consts.LISTENER_ID: listener.listener_id}
|
||||||
self.client.cast({}, 'create_listener', **payload)
|
self.client.cast({}, 'create_listener', **payload)
|
||||||
|
|
||||||
@ -132,6 +146,7 @@ class AmphoraProviderDriver(driver_base.ProviderDriver):
|
|||||||
self.client.cast({}, 'delete_listener', **payload)
|
self.client.cast({}, 'delete_listener', **payload)
|
||||||
|
|
||||||
def listener_update(self, old_listener, new_listener):
|
def listener_update(self, old_listener, new_listener):
|
||||||
|
self._validate_alpn_protocols(new_listener)
|
||||||
listener_dict = new_listener.to_dict()
|
listener_dict = new_listener.to_dict()
|
||||||
if 'admin_state_up' in listener_dict:
|
if 'admin_state_up' in listener_dict:
|
||||||
listener_dict['enabled'] = listener_dict.pop('admin_state_up')
|
listener_dict['enabled'] = listener_dict.pop('admin_state_up')
|
||||||
|
@ -64,6 +64,19 @@ class AmphoraProviderDriver(driver_base.ProviderDriver):
|
|||||||
user_fault_string=msg,
|
user_fault_string=msg,
|
||||||
operator_fault_string=msg)
|
operator_fault_string=msg)
|
||||||
|
|
||||||
|
def _validate_alpn_protocols(self, listener):
|
||||||
|
if not listener.alpn_protocols:
|
||||||
|
return
|
||||||
|
supported = consts.AMPHORA_SUPPORTED_ALPN_PROTOCOLS
|
||||||
|
not_supported = set(listener.alpn_protocols) - set(supported)
|
||||||
|
if not_supported:
|
||||||
|
msg = ('Amphora provider does not support %s ALPN protocol(s). '
|
||||||
|
'Supported: %s'
|
||||||
|
% (", ".join(not_supported), ", ".join(supported)))
|
||||||
|
raise exceptions.UnsupportedOptionError(
|
||||||
|
user_fault_string=msg,
|
||||||
|
operator_fault_string=msg)
|
||||||
|
|
||||||
# Load Balancer
|
# Load Balancer
|
||||||
def create_vip_port(self, loadbalancer_id, project_id, vip_dictionary):
|
def create_vip_port(self, loadbalancer_id, project_id, vip_dictionary):
|
||||||
vip_obj = driver_utils.provider_vip_dict_to_vip_obj(vip_dictionary)
|
vip_obj = driver_utils.provider_vip_dict_to_vip_obj(vip_dictionary)
|
||||||
@ -136,6 +149,7 @@ class AmphoraProviderDriver(driver_base.ProviderDriver):
|
|||||||
|
|
||||||
# Listener
|
# Listener
|
||||||
def listener_create(self, listener):
|
def listener_create(self, listener):
|
||||||
|
self._validate_alpn_protocols(listener)
|
||||||
payload = {consts.LISTENER: listener.to_dict()}
|
payload = {consts.LISTENER: listener.to_dict()}
|
||||||
self._encrypt_listener_dict(payload)
|
self._encrypt_listener_dict(payload)
|
||||||
|
|
||||||
@ -146,6 +160,7 @@ class AmphoraProviderDriver(driver_base.ProviderDriver):
|
|||||||
self.client.cast({}, 'delete_listener', **payload)
|
self.client.cast({}, 'delete_listener', **payload)
|
||||||
|
|
||||||
def listener_update(self, old_listener, new_listener):
|
def listener_update(self, old_listener, new_listener):
|
||||||
|
self._validate_alpn_protocols(new_listener)
|
||||||
original_listener = old_listener.to_dict()
|
original_listener = old_listener.to_dict()
|
||||||
listener_updates = new_listener.to_dict()
|
listener_updates = new_listener.to_dict()
|
||||||
|
|
||||||
|
@ -122,6 +122,9 @@ class RootController(object):
|
|||||||
self._add_a_version(versions, 'v2.18', 'v2', 'SUPPORTED',
|
self._add_a_version(versions, 'v2.18', 'v2', 'SUPPORTED',
|
||||||
'2020-04-29T01:00:00Z', host_url)
|
'2020-04-29T01:00:00Z', host_url)
|
||||||
# Add quota support to octavia's l7policy and l7rule
|
# Add quota support to octavia's l7policy and l7rule
|
||||||
self._add_a_version(versions, 'v2.19', 'v2', 'CURRENT',
|
self._add_a_version(versions, 'v2.19', 'v2', 'SUPPORTED',
|
||||||
'2020-05-12T00:00:00Z', host_url)
|
'2020-05-12T00:00:00Z', host_url)
|
||||||
|
# ALPN protocols
|
||||||
|
self._add_a_version(versions, 'v2.20', 'v2', 'CURRENT',
|
||||||
|
'2020-08-02T00:00:00Z', host_url)
|
||||||
return {'versions': versions}
|
return {'versions': versions}
|
||||||
|
@ -298,6 +298,8 @@ class ListenersController(base.BaseController):
|
|||||||
validate.check_tls_version_list(listener_dict['tls_versions'])
|
validate.check_tls_version_list(listener_dict['tls_versions'])
|
||||||
# Validate TLS versions against minimum
|
# Validate TLS versions against minimum
|
||||||
validate.check_tls_version_min(listener_dict['tls_versions'])
|
validate.check_tls_version_min(listener_dict['tls_versions'])
|
||||||
|
# Validate ALPN protocol list
|
||||||
|
validate.check_alpn_protocols(listener_dict['alpn_protocols'])
|
||||||
|
|
||||||
try:
|
try:
|
||||||
db_listener = self.repositories.listener.create(
|
db_listener = self.repositories.listener.create(
|
||||||
@ -511,6 +513,10 @@ class ListenersController(base.BaseController):
|
|||||||
# Validate TLS versions against minimum
|
# Validate TLS versions against minimum
|
||||||
validate.check_tls_version_min(listener.tls_versions)
|
validate.check_tls_version_min(listener.tls_versions)
|
||||||
|
|
||||||
|
if listener.alpn_protocols is not wtypes.Unset:
|
||||||
|
# Validate ALPN protocol list
|
||||||
|
validate.check_alpn_protocols(listener.alpn_protocols)
|
||||||
|
|
||||||
def _set_default_on_none(self, listener):
|
def _set_default_on_none(self, listener):
|
||||||
"""Reset settings to their default values if None/null was passed in
|
"""Reset settings to their default values if None/null was passed in
|
||||||
|
|
||||||
@ -543,6 +549,9 @@ class ListenersController(base.BaseController):
|
|||||||
if listener.tls_versions is None:
|
if listener.tls_versions is None:
|
||||||
listener.tls_versions = (
|
listener.tls_versions = (
|
||||||
CONF.api_settings.default_listener_tls_versions)
|
CONF.api_settings.default_listener_tls_versions)
|
||||||
|
if listener.alpn_protocols is None:
|
||||||
|
listener.alpn_protocols = (
|
||||||
|
CONF.api_settings.default_listener_alpn_protocols)
|
||||||
|
|
||||||
@wsme_pecan.wsexpose(listener_types.ListenerRootResponse, wtypes.text,
|
@wsme_pecan.wsexpose(listener_types.ListenerRootResponse, wtypes.text,
|
||||||
body=listener_types.ListenerRootPUT, status_code=200)
|
body=listener_types.ListenerRootPUT, status_code=200)
|
||||||
|
@ -65,6 +65,7 @@ class ListenerResponse(BaseListenerType):
|
|||||||
allowed_cidrs = wtypes.wsattr([types.CidrType()])
|
allowed_cidrs = wtypes.wsattr([types.CidrType()])
|
||||||
tls_ciphers = wtypes.StringType()
|
tls_ciphers = wtypes.StringType()
|
||||||
tls_versions = wtypes.wsattr(wtypes.ArrayType(wtypes.StringType()))
|
tls_versions = wtypes.wsattr(wtypes.ArrayType(wtypes.StringType()))
|
||||||
|
alpn_protocols = wtypes.wsattr(wtypes.ArrayType(types.AlpnProtocolType()))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_data_model(cls, data_model, children=False):
|
def from_data_model(cls, data_model, children=False):
|
||||||
@ -87,6 +88,7 @@ class ListenerResponse(BaseListenerType):
|
|||||||
l7policy_type.from_data_model(i) for i in data_model.l7policies]
|
l7policy_type.from_data_model(i) for i in data_model.l7policies]
|
||||||
|
|
||||||
listener.tls_versions = data_model.tls_versions
|
listener.tls_versions = data_model.tls_versions
|
||||||
|
listener.alpn_protocols = data_model.alpn_protocols
|
||||||
|
|
||||||
return listener
|
return listener
|
||||||
|
|
||||||
@ -158,6 +160,7 @@ class ListenerPOST(BaseListenerType):
|
|||||||
tls_ciphers = wtypes.StringType(max_length=2048)
|
tls_ciphers = wtypes.StringType(max_length=2048)
|
||||||
tls_versions = wtypes.wsattr(wtypes.ArrayType(wtypes.StringType(
|
tls_versions = wtypes.wsattr(wtypes.ArrayType(wtypes.StringType(
|
||||||
max_length=32)))
|
max_length=32)))
|
||||||
|
alpn_protocols = wtypes.wsattr(wtypes.ArrayType(types.AlpnProtocolType()))
|
||||||
|
|
||||||
|
|
||||||
class ListenerRootPOST(types.BaseType):
|
class ListenerRootPOST(types.BaseType):
|
||||||
@ -198,6 +201,7 @@ class ListenerPUT(BaseListenerType):
|
|||||||
tls_ciphers = wtypes.StringType(max_length=2048)
|
tls_ciphers = wtypes.StringType(max_length=2048)
|
||||||
tls_versions = wtypes.wsattr(wtypes.ArrayType(wtypes.StringType(
|
tls_versions = wtypes.wsattr(wtypes.ArrayType(wtypes.StringType(
|
||||||
max_length=32)))
|
max_length=32)))
|
||||||
|
alpn_protocols = wtypes.wsattr(wtypes.ArrayType(types.AlpnProtocolType()))
|
||||||
|
|
||||||
|
|
||||||
class ListenerRootPUT(types.BaseType):
|
class ListenerRootPUT(types.BaseType):
|
||||||
@ -251,6 +255,7 @@ class ListenerSingleCreate(BaseListenerType):
|
|||||||
tls_ciphers = wtypes.StringType(max_length=2048)
|
tls_ciphers = wtypes.StringType(max_length=2048)
|
||||||
tls_versions = wtypes.wsattr(wtypes.ArrayType(wtypes.StringType(
|
tls_versions = wtypes.wsattr(wtypes.ArrayType(wtypes.StringType(
|
||||||
max_length=32)))
|
max_length=32)))
|
||||||
|
alpn_protocols = wtypes.wsattr(wtypes.ArrayType(types.AlpnProtocolType()))
|
||||||
|
|
||||||
|
|
||||||
class ListenerStatusResponse(BaseListenerType):
|
class ListenerStatusResponse(BaseListenerType):
|
||||||
|
@ -128,7 +128,12 @@ api_opts = [
|
|||||||
cfg.StrOpt('minimum_tls_version',
|
cfg.StrOpt('minimum_tls_version',
|
||||||
default=None,
|
default=None,
|
||||||
choices=constants.TLS_ALL_VERSIONS + [None],
|
choices=constants.TLS_ALL_VERSIONS + [None],
|
||||||
help=_('Minimum allowed TLS version for listeners and pools.'))
|
help=_('Minimum allowed TLS version for listeners and pools.')),
|
||||||
|
cfg.ListOpt('default_listener_alpn_protocols',
|
||||||
|
default=[lib_consts.ALPN_PROTOCOL_HTTP_1_1,
|
||||||
|
lib_consts.ALPN_PROTOCOL_HTTP_1_0],
|
||||||
|
help=_('List of ALPN protocols to use for new TLS-enabled '
|
||||||
|
'listeners.')),
|
||||||
]
|
]
|
||||||
|
|
||||||
# Options only used by the amphora agent
|
# Options only used by the amphora agent
|
||||||
|
@ -854,3 +854,10 @@ OCTAVIA_OWNED = 'octavia_owned'
|
|||||||
# but they should be on the pool. Dealing with it until v3.
|
# but they should be on the pool. Dealing with it until v3.
|
||||||
LISTENER_PROTOCOLS_SUPPORTING_HEADER_INSERTION = [PROTOCOL_HTTP,
|
LISTENER_PROTOCOLS_SUPPORTING_HEADER_INSERTION = [PROTOCOL_HTTP,
|
||||||
PROTOCOL_TERMINATED_HTTPS]
|
PROTOCOL_TERMINATED_HTTPS]
|
||||||
|
|
||||||
|
SUPPORTED_ALPN_PROTOCOLS = [lib_consts.ALPN_PROTOCOL_HTTP_2,
|
||||||
|
lib_consts.ALPN_PROTOCOL_HTTP_1_1,
|
||||||
|
lib_consts.ALPN_PROTOCOL_HTTP_1_0]
|
||||||
|
|
||||||
|
AMPHORA_SUPPORTED_ALPN_PROTOCOLS = [lib_consts.ALPN_PROTOCOL_HTTP_1_1,
|
||||||
|
lib_consts.ALPN_PROTOCOL_HTTP_1_0]
|
||||||
|
@ -395,7 +395,8 @@ class Listener(BaseDataModel):
|
|||||||
timeout_member_data=None, timeout_tcp_inspect=None,
|
timeout_member_data=None, timeout_tcp_inspect=None,
|
||||||
tags=None, client_ca_tls_certificate_id=None,
|
tags=None, client_ca_tls_certificate_id=None,
|
||||||
client_authentication=None, client_crl_container_id=None,
|
client_authentication=None, client_crl_container_id=None,
|
||||||
allowed_cidrs=None, tls_ciphers=None, tls_versions=None):
|
allowed_cidrs=None, tls_ciphers=None, tls_versions=None,
|
||||||
|
alpn_protocols=None):
|
||||||
self.id = id
|
self.id = id
|
||||||
self.project_id = project_id
|
self.project_id = project_id
|
||||||
self.name = name
|
self.name = name
|
||||||
@ -430,6 +431,7 @@ class Listener(BaseDataModel):
|
|||||||
self.allowed_cidrs = allowed_cidrs or []
|
self.allowed_cidrs = allowed_cidrs or []
|
||||||
self.tls_ciphers = tls_ciphers
|
self.tls_ciphers = tls_ciphers
|
||||||
self.tls_versions = tls_versions
|
self.tls_versions = tls_versions
|
||||||
|
self.alpn_protocols = alpn_protocols
|
||||||
|
|
||||||
def update(self, update_dict):
|
def update(self, update_dict):
|
||||||
for key, value in update_dict.items():
|
for key, value in update_dict.items():
|
||||||
|
@ -289,6 +289,8 @@ class JinjaTemplater(object):
|
|||||||
ret_value['tls_ciphers'] = listener.tls_ciphers
|
ret_value['tls_ciphers'] = listener.tls_ciphers
|
||||||
if listener.tls_versions is not None:
|
if listener.tls_versions is not None:
|
||||||
ret_value['tls_versions'] = listener.tls_versions
|
ret_value['tls_versions'] = listener.tls_versions
|
||||||
|
if listener.alpn_protocols is not None:
|
||||||
|
ret_value['alpn_protocols'] = ",".join(listener.alpn_protocols)
|
||||||
|
|
||||||
pools = []
|
pools = []
|
||||||
pool_gen = (pool for pool in listener.pools if
|
pool_gen = (pool for pool in listener.pools if
|
||||||
|
@ -66,8 +66,14 @@ peers {{ "%s_peers"|format(loadbalancer.id.replace("-", ""))|trim() }}
|
|||||||
{% set tls_versions_opt = tls_versions_opt + " no-tlsv13" %}
|
{% set tls_versions_opt = tls_versions_opt + " no-tlsv13" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if listener.alpn_protocols is defined %}
|
||||||
|
{% set alpn_opt = "alpn %s"|format(listener.alpn_protocols)|trim() %}
|
||||||
|
{% else %}
|
||||||
|
{% set alpn_opt = "" %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
bind {{ lb_vip_address }}:{{ listener.protocol_port }} {{
|
bind {{ lb_vip_address }}:{{ listener.protocol_port }} {{
|
||||||
"%s %s %s %s%s"|format(def_crt_opt, client_ca_opt, ca_crl_opt, ciphers_opt, tls_versions_opt)|trim() }}
|
"%s %s %s %s%s %s"|format(def_crt_opt, client_ca_opt, ca_crl_opt, ciphers_opt, tls_versions_opt, alpn_opt)|trim() }}
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
|
|
||||||
|
@ -515,3 +515,21 @@ def check_default_tls_versions_min_conflict():
|
|||||||
|
|
||||||
check_tls_version_min(CONF.api_settings.default_pool_tls_versions,
|
check_tls_version_min(CONF.api_settings.default_pool_tls_versions,
|
||||||
message=pool_message)
|
message=pool_message)
|
||||||
|
|
||||||
|
|
||||||
|
def check_alpn_protocols(protocols):
|
||||||
|
if protocols == []:
|
||||||
|
raise exceptions.ValidationException(
|
||||||
|
detail=_('Empty ALPN protocol list. Either specify at least one '
|
||||||
|
'ALPN protocol or remove this parameter to use the '
|
||||||
|
'default.'))
|
||||||
|
|
||||||
|
# Unset action
|
||||||
|
if protocols is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
invalid_protocols = [p for p in protocols
|
||||||
|
if p not in constants.SUPPORTED_ALPN_PROTOCOLS]
|
||||||
|
if invalid_protocols:
|
||||||
|
raise exceptions.ValidationException(
|
||||||
|
detail=_('Invalid ALPN protocol: ' + ', '.join(invalid_protocols)))
|
||||||
|
@ -0,0 +1,35 @@
|
|||||||
|
# Copyright 2020 Red Hat, Inc. All rights reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
"""add listener alpn protocols column
|
||||||
|
|
||||||
|
Revision ID: 2ab994dd3ec2
|
||||||
|
Revises: 32e5c35b26a8
|
||||||
|
Create Date: 2020-08-02 21:51:21.261087
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '2ab994dd3ec2'
|
||||||
|
down_revision = '32e5c35b26a8'
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
op.add_column(
|
||||||
|
'listener',
|
||||||
|
sa.Column('alpn_protocols', sa.String(512), nullable=True)
|
||||||
|
)
|
@ -540,6 +540,7 @@ class Listener(base_models.BASE, base_models.IdMixin,
|
|||||||
client_crl_container_id = sa.Column(sa.String(255), nullable=True)
|
client_crl_container_id = sa.Column(sa.String(255), nullable=True)
|
||||||
tls_ciphers = sa.Column(sa.String(2048), nullable=True)
|
tls_ciphers = sa.Column(sa.String(2048), nullable=True)
|
||||||
tls_versions = sa.Column(ScalarListType(), nullable=True)
|
tls_versions = sa.Column(ScalarListType(), nullable=True)
|
||||||
|
alpn_protocols = sa.Column(ScalarListType(), nullable=True)
|
||||||
|
|
||||||
_tags = orm.relationship(
|
_tags = orm.relationship(
|
||||||
'Tags',
|
'Tags',
|
||||||
|
@ -113,6 +113,10 @@ def create_listener(listener_dict, lb_id):
|
|||||||
listener_dict['tls_versions'] is None):
|
listener_dict['tls_versions'] is None):
|
||||||
listener_dict['tls_versions'] = (
|
listener_dict['tls_versions'] = (
|
||||||
CONF.api_settings.default_listener_tls_versions)
|
CONF.api_settings.default_listener_tls_versions)
|
||||||
|
if ('alpn_protocols' not in listener_dict or
|
||||||
|
listener_dict['alpn_protocols'] is None):
|
||||||
|
listener_dict['alpn_protocols'] = (
|
||||||
|
CONF.api_settings.default_listener_alpn_protocols)
|
||||||
|
|
||||||
return listener_dict
|
return listener_dict
|
||||||
|
|
||||||
|
@ -469,7 +469,9 @@ class SampleDriverDataModels(object):
|
|||||||
constants.CLIENT_CRL_CONTAINER_ID: self.client_crl_container_ref,
|
constants.CLIENT_CRL_CONTAINER_ID: self.client_crl_container_ref,
|
||||||
lib_consts.ALLOWED_CIDRS: ['192.0.2.0/24', '198.51.100.0/24'],
|
lib_consts.ALLOWED_CIDRS: ['192.0.2.0/24', '198.51.100.0/24'],
|
||||||
lib_consts.TLS_CIPHERS: constants.CIPHERS_OWASP_SUITE_B,
|
lib_consts.TLS_CIPHERS: constants.CIPHERS_OWASP_SUITE_B,
|
||||||
lib_consts.TLS_VERSIONS: constants.TLS_VERSIONS_OWASP_SUITE_B
|
lib_consts.TLS_VERSIONS: constants.TLS_VERSIONS_OWASP_SUITE_B,
|
||||||
|
lib_consts.ALPN_PROTOCOLS:
|
||||||
|
constants.AMPHORA_SUPPORTED_ALPN_PROTOCOLS
|
||||||
}
|
}
|
||||||
|
|
||||||
self.test_listener1_dict.update(self._common_test_dict)
|
self.test_listener1_dict.update(self._common_test_dict)
|
||||||
@ -509,6 +511,7 @@ class SampleDriverDataModels(object):
|
|||||||
self.provider_listener1_dict = {
|
self.provider_listener1_dict = {
|
||||||
lib_consts.ADMIN_STATE_UP: True,
|
lib_consts.ADMIN_STATE_UP: True,
|
||||||
lib_consts.ALLOWED_CIDRS: ['192.0.2.0/24', '198.51.100.0/24'],
|
lib_consts.ALLOWED_CIDRS: ['192.0.2.0/24', '198.51.100.0/24'],
|
||||||
|
lib_consts.ALPN_PROTOCOLS: [lib_consts.ALPN_PROTOCOL_HTTP_1_1],
|
||||||
lib_consts.CONNECTION_LIMIT: 10000,
|
lib_consts.CONNECTION_LIMIT: 10000,
|
||||||
lib_consts.DEFAULT_POOL: self.provider_pool1_dict,
|
lib_consts.DEFAULT_POOL: self.provider_pool1_dict,
|
||||||
lib_consts.DEFAULT_POOL_ID: self.pool1_id,
|
lib_consts.DEFAULT_POOL_ID: self.pool1_id,
|
||||||
@ -538,7 +541,9 @@ class SampleDriverDataModels(object):
|
|||||||
lib_consts.CLIENT_CRL_CONTAINER_REF: self.client_crl_container_ref,
|
lib_consts.CLIENT_CRL_CONTAINER_REF: self.client_crl_container_ref,
|
||||||
lib_consts.CLIENT_CRL_CONTAINER_DATA: crl_file_content,
|
lib_consts.CLIENT_CRL_CONTAINER_DATA: crl_file_content,
|
||||||
lib_consts.TLS_CIPHERS: constants.CIPHERS_OWASP_SUITE_B,
|
lib_consts.TLS_CIPHERS: constants.CIPHERS_OWASP_SUITE_B,
|
||||||
lib_consts.TLS_VERSIONS: constants.TLS_VERSIONS_OWASP_SUITE_B
|
lib_consts.TLS_VERSIONS: constants.TLS_VERSIONS_OWASP_SUITE_B,
|
||||||
|
lib_consts.ALPN_PROTOCOLS:
|
||||||
|
constants.AMPHORA_SUPPORTED_ALPN_PROTOCOLS
|
||||||
}
|
}
|
||||||
|
|
||||||
self.provider_listener2_dict = copy.deepcopy(
|
self.provider_listener2_dict = copy.deepcopy(
|
||||||
|
@ -45,7 +45,7 @@ class TestRootController(base_db_test.OctaviaDBTestBase):
|
|||||||
def test_api_versions(self):
|
def test_api_versions(self):
|
||||||
versions = self._get_versions_with_config()
|
versions = self._get_versions_with_config()
|
||||||
version_ids = tuple(v.get('id') for v in versions)
|
version_ids = tuple(v.get('id') for v in versions)
|
||||||
self.assertEqual(20, len(version_ids))
|
self.assertEqual(21, len(version_ids))
|
||||||
self.assertIn('v2.0', version_ids)
|
self.assertIn('v2.0', version_ids)
|
||||||
self.assertIn('v2.1', version_ids)
|
self.assertIn('v2.1', version_ids)
|
||||||
self.assertIn('v2.2', version_ids)
|
self.assertIn('v2.2', version_ids)
|
||||||
@ -66,6 +66,7 @@ class TestRootController(base_db_test.OctaviaDBTestBase):
|
|||||||
self.assertIn('v2.17', version_ids)
|
self.assertIn('v2.17', version_ids)
|
||||||
self.assertIn('v2.18', version_ids)
|
self.assertIn('v2.18', version_ids)
|
||||||
self.assertIn('v2.19', version_ids)
|
self.assertIn('v2.19', version_ids)
|
||||||
|
self.assertIn('v2.20', version_ids)
|
||||||
|
|
||||||
# Each version should have a 'self' 'href' to the API version URL
|
# Each version should have a 'self' 'href' to the API version URL
|
||||||
# [{u'rel': u'self', u'href': u'http://localhost/v2'}]
|
# [{u'rel': u'self', u'href': u'http://localhost/v2'}]
|
||||||
|
@ -1755,6 +1755,8 @@ class TestListener(base.BaseAPITest):
|
|||||||
self.conf.config(group='api_settings',
|
self.conf.config(group='api_settings',
|
||||||
default_listener_ciphers=(
|
default_listener_ciphers=(
|
||||||
constants.CIPHERS_OWASP_SUITE_B))
|
constants.CIPHERS_OWASP_SUITE_B))
|
||||||
|
self.conf.config(group='api_settings',
|
||||||
|
default_listener_alpn_protocols=['http/1.1'])
|
||||||
|
|
||||||
self.cert_manager_mock().get_secret.side_effect = [
|
self.cert_manager_mock().get_secret.side_effect = [
|
||||||
sample_certs.X509_CA_CERT, sample_certs.X509_CA_CRL,
|
sample_certs.X509_CA_CERT, sample_certs.X509_CA_CRL,
|
||||||
@ -1781,7 +1783,8 @@ class TestListener(base.BaseAPITest):
|
|||||||
client_crl_container_ref=crl_tls_uuid,
|
client_crl_container_ref=crl_tls_uuid,
|
||||||
client_ca_tls_container_ref=ca_tls_uuid,
|
client_ca_tls_container_ref=ca_tls_uuid,
|
||||||
tls_versions=[lib_consts.TLS_VERSION_1_3],
|
tls_versions=[lib_consts.TLS_VERSION_1_3],
|
||||||
tls_ciphers='TLS_AES_256_GCM_SHA384').get(self.root_tag)
|
tls_ciphers='TLS_AES_256_GCM_SHA384',
|
||||||
|
alpn_protocols=['http/1.0']).get(self.root_tag)
|
||||||
self.set_lb_status(self.lb_id)
|
self.set_lb_status(self.lb_id)
|
||||||
unset_params = {
|
unset_params = {
|
||||||
'name': None, 'description': None, 'connection_limit': None,
|
'name': None, 'description': None, 'connection_limit': None,
|
||||||
@ -1791,7 +1794,7 @@ class TestListener(base.BaseAPITest):
|
|||||||
'timeout_tcp_inspect': None, 'client_ca_tls_container_ref': None,
|
'timeout_tcp_inspect': None, 'client_ca_tls_container_ref': None,
|
||||||
'client_authentication': None, 'default_pool_id': None,
|
'client_authentication': None, 'default_pool_id': None,
|
||||||
'client_crl_container_ref': None, 'tls_versions': None,
|
'client_crl_container_ref': None, 'tls_versions': None,
|
||||||
'tls_ciphers': None}
|
'tls_ciphers': None, 'alpn_protocols': None}
|
||||||
body = self._build_body(unset_params)
|
body = self._build_body(unset_params)
|
||||||
listener_path = self.LISTENER_PATH.format(
|
listener_path = self.LISTENER_PATH.format(
|
||||||
listener_id=listener['id'])
|
listener_id=listener['id'])
|
||||||
@ -1817,6 +1820,7 @@ class TestListener(base.BaseAPITest):
|
|||||||
api_listener['tls_versions'])
|
api_listener['tls_versions'])
|
||||||
self.assertEqual(constants.CIPHERS_OWASP_SUITE_B,
|
self.assertEqual(constants.CIPHERS_OWASP_SUITE_B,
|
||||||
api_listener['tls_ciphers'])
|
api_listener['tls_ciphers'])
|
||||||
|
self.assertEqual(['http/1.1'], api_listener['alpn_protocols'])
|
||||||
|
|
||||||
@mock.patch('octavia.common.tls_utils.cert_parser.load_certificates_data')
|
@mock.patch('octavia.common.tls_utils.cert_parser.load_certificates_data')
|
||||||
def test_update_with_bad_ca_cert(self, mock_cert_data):
|
def test_update_with_bad_ca_cert(self, mock_cert_data):
|
||||||
@ -2403,6 +2407,95 @@ class TestListener(base.BaseAPITest):
|
|||||||
.format(constants.PROTOCOL_TERMINATED_HTTPS),
|
.format(constants.PROTOCOL_TERMINATED_HTTPS),
|
||||||
listener.get('faultstring'))
|
listener.get('faultstring'))
|
||||||
|
|
||||||
|
# TODO(johnsom) Fix this when there is a noop certificate manager
|
||||||
|
@mock.patch('octavia.common.tls_utils.cert_parser.load_certificates_data')
|
||||||
|
def test_create_with_alpn(self, mock_cert_data):
|
||||||
|
cert1 = data_models.TLSContainer(certificate='cert 1')
|
||||||
|
mock_cert_data.return_value = {'tls_cert': cert1}
|
||||||
|
cert_id = uuidutils.generate_uuid()
|
||||||
|
alpn_protocols = [lib_consts.ALPN_PROTOCOL_HTTP_2,
|
||||||
|
lib_consts.ALPN_PROTOCOL_HTTP_1_1]
|
||||||
|
listener = self.create_listener(constants.PROTOCOL_TERMINATED_HTTPS,
|
||||||
|
80, self.lb_id,
|
||||||
|
default_tls_container_ref=cert_id,
|
||||||
|
alpn_protocols=['h2', 'http/1.1'])
|
||||||
|
listener_path = self.LISTENER_PATH.format(
|
||||||
|
listener_id=listener['listener']['id'])
|
||||||
|
get_listener = self.get(listener_path).json['listener']
|
||||||
|
self.assertEqual(alpn_protocols, get_listener['alpn_protocols'])
|
||||||
|
|
||||||
|
# TODO(johnsom) Fix this when there is a noop certificate manager
|
||||||
|
@mock.patch('octavia.common.tls_utils.cert_parser.load_certificates_data')
|
||||||
|
def test_create_with_alpn_negative(self, mock_cert_data):
|
||||||
|
cert1 = data_models.TLSContainer(certificate='cert 1')
|
||||||
|
mock_cert_data.return_value = {'tls_cert': cert1}
|
||||||
|
cert_id = uuidutils.generate_uuid()
|
||||||
|
req_dict = {'protocol': constants.PROTOCOL_TERMINATED_HTTPS,
|
||||||
|
'protocol_port': 80,
|
||||||
|
'loadbalancer_id': self.lb_id,
|
||||||
|
'default_tls_container_ref': cert_id,
|
||||||
|
'alpn_protocols': [lib_consts.ALPN_PROTOCOL_HTTP_1_1,
|
||||||
|
'invalid-proto']}
|
||||||
|
res = self.post(self.LISTENERS_PATH, self._build_body(req_dict),
|
||||||
|
status=400)
|
||||||
|
fault = res.json['faultstring']
|
||||||
|
self.assertIn(
|
||||||
|
'Invalid input for field/attribute alpn_protocols', fault)
|
||||||
|
self.assertIn('Value should be a valid ALPN protocol ID', fault)
|
||||||
|
self.assert_correct_status(lb_id=self.lb_id)
|
||||||
|
|
||||||
|
# TODO(johnsom) Fix this when there is a noop certificate manager
|
||||||
|
@mock.patch('octavia.common.tls_utils.cert_parser.load_certificates_data')
|
||||||
|
def test_update_with_alpn(self, mock_cert_data):
|
||||||
|
cert_id = uuidutils.generate_uuid()
|
||||||
|
cert1 = data_models.TLSContainer(certificate='cert 1')
|
||||||
|
mock_cert_data.return_value = {'tls_cert': cert1}
|
||||||
|
alpn_protocols_orig = [lib_consts.ALPN_PROTOCOL_HTTP_1_0]
|
||||||
|
alpn_protocols = [lib_consts.ALPN_PROTOCOL_HTTP_2,
|
||||||
|
lib_consts.ALPN_PROTOCOL_HTTP_1_1]
|
||||||
|
listener = self.create_listener(
|
||||||
|
constants.PROTOCOL_TERMINATED_HTTPS, 80, self.lb_id,
|
||||||
|
default_tls_container_ref=cert_id,
|
||||||
|
alpn_protocols=alpn_protocols_orig)
|
||||||
|
self.set_lb_status(self.lb_id)
|
||||||
|
listener_path = self.LISTENER_PATH.format(
|
||||||
|
listener_id=listener['listener']['id'])
|
||||||
|
get_listener = self.get(listener_path).json['listener']
|
||||||
|
self.assertEqual(alpn_protocols_orig,
|
||||||
|
get_listener.get('alpn_protocols'))
|
||||||
|
self.put(listener_path,
|
||||||
|
self._build_body({'alpn_protocols': alpn_protocols}))
|
||||||
|
get_listener = self.get(listener_path).json['listener']
|
||||||
|
self.assertEqual(alpn_protocols, get_listener.get('alpn_protocols'))
|
||||||
|
|
||||||
|
# TODO(johnsom) Fix this when there is a noop certificate manager
|
||||||
|
@mock.patch('octavia.common.tls_utils.cert_parser.load_certificates_data')
|
||||||
|
def test_update_with_alpn_negative(self, mock_cert_data):
|
||||||
|
cert_id = uuidutils.generate_uuid()
|
||||||
|
cert1 = data_models.TLSContainer(certificate='cert 1')
|
||||||
|
mock_cert_data.return_value = {'tls_cert': cert1}
|
||||||
|
alpn_protocols_orig = [lib_consts.ALPN_PROTOCOL_HTTP_1_0]
|
||||||
|
listener = self.create_listener(
|
||||||
|
constants.PROTOCOL_TERMINATED_HTTPS, 80, self.lb_id,
|
||||||
|
default_tls_container_ref=cert_id,
|
||||||
|
alpn_protocols=alpn_protocols_orig)
|
||||||
|
self.set_lb_status(self.lb_id)
|
||||||
|
listener_path = self.LISTENER_PATH.format(
|
||||||
|
listener_id=listener['listener']['id'])
|
||||||
|
get_listener = self.get(listener_path).json['listener']
|
||||||
|
self.assertEqual(alpn_protocols_orig,
|
||||||
|
get_listener.get('alpn_protocols'))
|
||||||
|
|
||||||
|
req_dict = {'alpn_protocols': [
|
||||||
|
lib_consts.ALPN_PROTOCOL_HTTP_1_1, 'invalid-proto']}
|
||||||
|
res = self.put(self.LISTENERS_PATH, self._build_body(req_dict),
|
||||||
|
status=400)
|
||||||
|
fault = res.json['faultstring']
|
||||||
|
self.assertIn(
|
||||||
|
'Invalid input for field/attribute alpn_protocols', fault)
|
||||||
|
self.assertIn('Value should be a valid ALPN protocol ID', fault)
|
||||||
|
self.assert_correct_status(lb_id=self.lb_id)
|
||||||
|
|
||||||
# TODO(johnsom) Fix this when there is a noop certificate manager
|
# TODO(johnsom) Fix this when there is a noop certificate manager
|
||||||
@mock.patch('octavia.common.tls_utils.cert_parser.load_certificates_data')
|
@mock.patch('octavia.common.tls_utils.cert_parser.load_certificates_data')
|
||||||
def test_create_with_sni_data(self, mock_cert_data):
|
def test_create_with_sni_data(self, mock_cert_data):
|
||||||
|
@ -17,6 +17,7 @@ import random
|
|||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
from octavia_lib.api.drivers import exceptions as lib_exceptions
|
from octavia_lib.api.drivers import exceptions as lib_exceptions
|
||||||
|
from octavia_lib.common import constants as lib_consts
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
from oslo_config import fixture as oslo_fixture
|
from oslo_config import fixture as oslo_fixture
|
||||||
from oslo_utils import uuidutils
|
from oslo_utils import uuidutils
|
||||||
@ -2623,7 +2624,8 @@ class TestLoadBalancerGraph(base.BaseAPITest):
|
|||||||
'client_crl_container_ref': None,
|
'client_crl_container_ref': None,
|
||||||
'allowed_cidrs': None,
|
'allowed_cidrs': None,
|
||||||
'tls_ciphers': None,
|
'tls_ciphers': None,
|
||||||
'tls_versions': None
|
'tls_versions': None,
|
||||||
|
'alpn_protocols': None
|
||||||
}
|
}
|
||||||
if create_sni_containers:
|
if create_sni_containers:
|
||||||
create_listener['sni_container_refs'] = create_sni_containers
|
create_listener['sni_container_refs'] = create_sni_containers
|
||||||
@ -2673,6 +2675,9 @@ class TestLoadBalancerGraph(base.BaseAPITest):
|
|||||||
expected_listener['tls_ciphers'] = constants.CIPHERS_OWASP_SUITE_B
|
expected_listener['tls_ciphers'] = constants.CIPHERS_OWASP_SUITE_B
|
||||||
expected_listener['tls_versions'] = (
|
expected_listener['tls_versions'] = (
|
||||||
constants.TLS_VERSIONS_OWASP_SUITE_B)
|
constants.TLS_VERSIONS_OWASP_SUITE_B)
|
||||||
|
expected_listener['alpn_protocols'] = (
|
||||||
|
[lib_consts.ALPN_PROTOCOL_HTTP_1_1,
|
||||||
|
lib_consts.ALPN_PROTOCOL_HTTP_1_0])
|
||||||
|
|
||||||
return create_listener, expected_listener
|
return create_listener, expected_listener
|
||||||
|
|
||||||
|
@ -122,11 +122,24 @@ class TestAmphoraDriver(base.TestRpc):
|
|||||||
@mock.patch('oslo_messaging.RPCClient.cast')
|
@mock.patch('oslo_messaging.RPCClient.cast')
|
||||||
def test_listener_create(self, mock_cast):
|
def test_listener_create(self, mock_cast):
|
||||||
provider_listener = driver_dm.Listener(
|
provider_listener = driver_dm.Listener(
|
||||||
listener_id=self.sample_data.listener1_id)
|
listener_id=self.sample_data.listener1_id,
|
||||||
|
alpn_protocols=consts.AMPHORA_SUPPORTED_ALPN_PROTOCOLS)
|
||||||
self.amp_driver.listener_create(provider_listener)
|
self.amp_driver.listener_create(provider_listener)
|
||||||
payload = {consts.LISTENER_ID: self.sample_data.listener1_id}
|
payload = {consts.LISTENER_ID: self.sample_data.listener1_id}
|
||||||
mock_cast.assert_called_with({}, 'create_listener', **payload)
|
mock_cast.assert_called_with({}, 'create_listener', **payload)
|
||||||
|
|
||||||
|
@mock.patch('oslo_messaging.RPCClient.cast')
|
||||||
|
def test_listener_create_unsupported_alpn(self, mock_cast):
|
||||||
|
provider_listener = driver_dm.Listener(
|
||||||
|
listener_id=self.sample_data.listener1_id)
|
||||||
|
# NOTE(cgoncalves): test will fail once HTTP/2 is supported
|
||||||
|
provider_listener.alpn_protocols = ['http/1.1', 'h2']
|
||||||
|
self.assertRaises(
|
||||||
|
exceptions.UnsupportedOptionError,
|
||||||
|
self.amp_driver.listener_create,
|
||||||
|
provider_listener)
|
||||||
|
mock_cast.assert_not_called()
|
||||||
|
|
||||||
@mock.patch('oslo_messaging.RPCClient.cast')
|
@mock.patch('oslo_messaging.RPCClient.cast')
|
||||||
def test_listener_delete(self, mock_cast):
|
def test_listener_delete(self, mock_cast):
|
||||||
provider_listener = driver_dm.Listener(
|
provider_listener = driver_dm.Listener(
|
||||||
@ -161,6 +174,20 @@ class TestAmphoraDriver(base.TestRpc):
|
|||||||
consts.LISTENER_UPDATES: listener_dict}
|
consts.LISTENER_UPDATES: listener_dict}
|
||||||
mock_cast.assert_called_with({}, 'update_listener', **payload)
|
mock_cast.assert_called_with({}, 'update_listener', **payload)
|
||||||
|
|
||||||
|
@mock.patch('oslo_messaging.RPCClient.cast')
|
||||||
|
def test_listener_update_unsupported_alpn(self, mock_cast):
|
||||||
|
old_provider_listener = driver_dm.Listener(
|
||||||
|
listener_id=self.sample_data.listener1_id)
|
||||||
|
# NOTE(cgoncalves): test will fail once HTTP/2 is supported
|
||||||
|
provider_listener = driver_dm.Listener(
|
||||||
|
listener_id=self.sample_data.listener1_id,
|
||||||
|
alpn_protocols=['http/1.1', 'h2'])
|
||||||
|
self.assertRaises(
|
||||||
|
exceptions.UnsupportedOptionError,
|
||||||
|
self.amp_driver.listener_update,
|
||||||
|
old_provider_listener,
|
||||||
|
provider_listener)
|
||||||
|
|
||||||
# Pool
|
# Pool
|
||||||
@mock.patch('oslo_messaging.RPCClient.cast')
|
@mock.patch('oslo_messaging.RPCClient.cast')
|
||||||
def test_pool_create(self, mock_cast):
|
def test_pool_create(self, mock_cast):
|
||||||
|
@ -122,11 +122,24 @@ class TestAmphoraDriver(base.TestRpc):
|
|||||||
@mock.patch('oslo_messaging.RPCClient.cast')
|
@mock.patch('oslo_messaging.RPCClient.cast')
|
||||||
def test_listener_create(self, mock_cast):
|
def test_listener_create(self, mock_cast):
|
||||||
provider_listener = driver_dm.Listener(
|
provider_listener = driver_dm.Listener(
|
||||||
listener_id=self.sample_data.listener1_id)
|
listener_id=self.sample_data.listener1_id,
|
||||||
|
alpn_protocols=consts.AMPHORA_SUPPORTED_ALPN_PROTOCOLS)
|
||||||
self.amp_driver.listener_create(provider_listener)
|
self.amp_driver.listener_create(provider_listener)
|
||||||
payload = {consts.LISTENER: provider_listener.to_dict()}
|
payload = {consts.LISTENER: provider_listener.to_dict()}
|
||||||
mock_cast.assert_called_with({}, 'create_listener', **payload)
|
mock_cast.assert_called_with({}, 'create_listener', **payload)
|
||||||
|
|
||||||
|
@mock.patch('oslo_messaging.RPCClient.cast')
|
||||||
|
def test_listener_create_unsupported_alpn(self, mock_cast):
|
||||||
|
provider_listener = driver_dm.Listener(
|
||||||
|
listener_id=self.sample_data.listener1_id)
|
||||||
|
# NOTE(cgoncalves): test will fail once HTTP/2 is supported
|
||||||
|
provider_listener.alpn_protocols = ['http/1.1', 'h2']
|
||||||
|
self.assertRaises(
|
||||||
|
exceptions.UnsupportedOptionError,
|
||||||
|
self.amp_driver.listener_create,
|
||||||
|
provider_listener)
|
||||||
|
mock_cast.assert_not_called()
|
||||||
|
|
||||||
@mock.patch('oslo_messaging.RPCClient.cast')
|
@mock.patch('oslo_messaging.RPCClient.cast')
|
||||||
def test_listener_delete(self, mock_cast):
|
def test_listener_delete(self, mock_cast):
|
||||||
provider_listener = driver_dm.Listener(
|
provider_listener = driver_dm.Listener(
|
||||||
@ -163,6 +176,20 @@ class TestAmphoraDriver(base.TestRpc):
|
|||||||
consts.LISTENER_UPDATES: listener_dict}
|
consts.LISTENER_UPDATES: listener_dict}
|
||||||
mock_cast.assert_called_with({}, 'update_listener', **payload)
|
mock_cast.assert_called_with({}, 'update_listener', **payload)
|
||||||
|
|
||||||
|
@mock.patch('oslo_messaging.RPCClient.cast')
|
||||||
|
def test_listener_update_unsupported_alpn(self, mock_cast):
|
||||||
|
old_provider_listener = driver_dm.Listener(
|
||||||
|
listener_id=self.sample_data.listener1_id)
|
||||||
|
# NOTE(cgoncalves): test will fail once HTTP/2 is supported
|
||||||
|
provider_listener = driver_dm.Listener(
|
||||||
|
listener_id=self.sample_data.listener1_id,
|
||||||
|
alpn_protocols=['http/1.1', 'h2'])
|
||||||
|
self.assertRaises(
|
||||||
|
exceptions.UnsupportedOptionError,
|
||||||
|
self.amp_driver.listener_update,
|
||||||
|
old_provider_listener,
|
||||||
|
provider_listener)
|
||||||
|
|
||||||
# Pool
|
# Pool
|
||||||
@mock.patch('oslo_messaging.RPCClient.cast')
|
@mock.patch('oslo_messaging.RPCClient.cast')
|
||||||
def test_pool_create(self, mock_cast):
|
def test_pool_create(self, mock_cast):
|
||||||
|
@ -137,6 +137,13 @@ class TestListenerPOST(base.BaseTypesTest, TestListener):
|
|||||||
listener = wsme_json.fromjson(self._type, body)
|
listener = wsme_json.fromjson(self._type, body)
|
||||||
self.assertEqual(listener.project_id, body['project_id'])
|
self.assertEqual(listener.project_id, body['project_id'])
|
||||||
|
|
||||||
|
def test_invalid_alpn_protocols(self):
|
||||||
|
body = {"protocol": constants.PROTOCOL_HTTP, "protocol_port": 80,
|
||||||
|
"loadbalancer_id": uuidutils.generate_uuid(),
|
||||||
|
"alpn_protocols": ["bad", "boy"]}
|
||||||
|
self.assertRaises(exc.InvalidInput, wsme_json.fromjson, self._type,
|
||||||
|
body)
|
||||||
|
|
||||||
|
|
||||||
class TestListenerPUT(base.BaseTypesTest, TestListener):
|
class TestListenerPUT(base.BaseTypesTest, TestListener):
|
||||||
|
|
||||||
@ -153,3 +160,8 @@ class TestListenerPUT(base.BaseTypesTest, TestListener):
|
|||||||
"tags": ['test_tag']}
|
"tags": ['test_tag']}
|
||||||
listener = wsme_json.fromjson(self._type, body)
|
listener = wsme_json.fromjson(self._type, body)
|
||||||
self.assertEqual(wsme_types.Unset, listener.admin_state_up)
|
self.assertEqual(wsme_types.Unset, listener.admin_state_up)
|
||||||
|
|
||||||
|
def test_invalid_alpn_protocols(self):
|
||||||
|
body = {"alpn_protocols": ["bad", "boy"]}
|
||||||
|
self.assertRaises(exc.InvalidInput, wsme_json.fromjson, self._type,
|
||||||
|
body)
|
||||||
|
@ -52,13 +52,14 @@ class TestHaproxyCfg(base.TestCase):
|
|||||||
"ca-file /var/lib/octavia/certs/sample_loadbalancer_id_1/"
|
"ca-file /var/lib/octavia/certs/sample_loadbalancer_id_1/"
|
||||||
"client_ca.pem verify required crl-file /var/lib/octavia/"
|
"client_ca.pem verify required crl-file /var/lib/octavia/"
|
||||||
"certs/sample_loadbalancer_id_1/SHA_ID.pem ciphers {ciphers} "
|
"certs/sample_loadbalancer_id_1/SHA_ID.pem ciphers {ciphers} "
|
||||||
"no-sslv3 no-tlsv10 no-tlsv11\n"
|
"no-sslv3 no-tlsv10 no-tlsv11 alpn {alpn}\n"
|
||||||
" mode http\n"
|
" mode http\n"
|
||||||
" default_backend sample_pool_id_1:sample_listener_id_1\n"
|
" default_backend sample_pool_id_1:sample_listener_id_1\n"
|
||||||
" timeout client 50000\n").format(
|
" timeout client 50000\n").format(
|
||||||
maxconn=constants.HAPROXY_DEFAULT_MAXCONN,
|
maxconn=constants.HAPROXY_DEFAULT_MAXCONN,
|
||||||
crt_list=FAKE_CRT_LIST_FILENAME,
|
crt_list=FAKE_CRT_LIST_FILENAME,
|
||||||
ciphers=constants.CIPHERS_OWASP_SUITE_B)
|
ciphers=constants.CIPHERS_OWASP_SUITE_B,
|
||||||
|
alpn=",".join(constants.AMPHORA_SUPPORTED_ALPN_PROTOCOLS))
|
||||||
be = ("backend sample_pool_id_1:sample_listener_id_1\n"
|
be = ("backend sample_pool_id_1:sample_listener_id_1\n"
|
||||||
" mode http\n"
|
" mode http\n"
|
||||||
" balance roundrobin\n"
|
" balance roundrobin\n"
|
||||||
@ -105,13 +106,14 @@ class TestHaproxyCfg(base.TestCase):
|
|||||||
" maxconn {maxconn}\n"
|
" maxconn {maxconn}\n"
|
||||||
" redirect scheme https if !{{ ssl_fc }}\n"
|
" redirect scheme https if !{{ ssl_fc }}\n"
|
||||||
" bind 10.0.0.2:443 ssl crt-list {crt_list}"
|
" bind 10.0.0.2:443 ssl crt-list {crt_list}"
|
||||||
" ciphers {ciphers} no-sslv3 no-tlsv10 no-tlsv11\n"
|
" ciphers {ciphers} no-sslv3 no-tlsv10 no-tlsv11 alpn {alpn}\n"
|
||||||
" mode http\n"
|
" mode http\n"
|
||||||
" default_backend sample_pool_id_1:sample_listener_id_1\n"
|
" default_backend sample_pool_id_1:sample_listener_id_1\n"
|
||||||
" timeout client 50000\n").format(
|
" timeout client 50000\n").format(
|
||||||
maxconn=constants.HAPROXY_DEFAULT_MAXCONN,
|
maxconn=constants.HAPROXY_DEFAULT_MAXCONN,
|
||||||
crt_list=FAKE_CRT_LIST_FILENAME,
|
crt_list=FAKE_CRT_LIST_FILENAME,
|
||||||
ciphers=constants.CIPHERS_OWASP_SUITE_B)
|
ciphers=constants.CIPHERS_OWASP_SUITE_B,
|
||||||
|
alpn=",".join(constants.AMPHORA_SUPPORTED_ALPN_PROTOCOLS))
|
||||||
be = ("backend sample_pool_id_1:sample_listener_id_1\n"
|
be = ("backend sample_pool_id_1:sample_listener_id_1\n"
|
||||||
" mode http\n"
|
" mode http\n"
|
||||||
" balance roundrobin\n"
|
" balance roundrobin\n"
|
||||||
@ -155,12 +157,13 @@ class TestHaproxyCfg(base.TestCase):
|
|||||||
" maxconn {maxconn}\n"
|
" maxconn {maxconn}\n"
|
||||||
" redirect scheme https if !{{ ssl_fc }}\n"
|
" redirect scheme https if !{{ ssl_fc }}\n"
|
||||||
" bind 10.0.0.2:443 ssl crt-list {crt_list} "
|
" bind 10.0.0.2:443 ssl crt-list {crt_list} "
|
||||||
"no-sslv3 no-tlsv10 no-tlsv11\n"
|
"no-sslv3 no-tlsv10 no-tlsv11 alpn {alpn}\n"
|
||||||
" mode http\n"
|
" mode http\n"
|
||||||
" default_backend sample_pool_id_1:sample_listener_id_1\n"
|
" default_backend sample_pool_id_1:sample_listener_id_1\n"
|
||||||
" timeout client 50000\n").format(
|
" timeout client 50000\n").format(
|
||||||
maxconn=constants.HAPROXY_DEFAULT_MAXCONN,
|
maxconn=constants.HAPROXY_DEFAULT_MAXCONN,
|
||||||
crt_list=FAKE_CRT_LIST_FILENAME)
|
crt_list=FAKE_CRT_LIST_FILENAME,
|
||||||
|
alpn=",".join(constants.AMPHORA_SUPPORTED_ALPN_PROTOCOLS))
|
||||||
be = ("backend sample_pool_id_1:sample_listener_id_1\n"
|
be = ("backend sample_pool_id_1:sample_listener_id_1\n"
|
||||||
" mode http\n"
|
" mode http\n"
|
||||||
" balance roundrobin\n"
|
" balance roundrobin\n"
|
||||||
@ -207,13 +210,15 @@ class TestHaproxyCfg(base.TestCase):
|
|||||||
"ssl crt-list {crt_list} "
|
"ssl crt-list {crt_list} "
|
||||||
"ca-file /var/lib/octavia/certs/sample_loadbalancer_id_1/"
|
"ca-file /var/lib/octavia/certs/sample_loadbalancer_id_1/"
|
||||||
"client_ca.pem verify required crl-file /var/lib/octavia/"
|
"client_ca.pem verify required crl-file /var/lib/octavia/"
|
||||||
"certs/sample_loadbalancer_id_1/SHA_ID.pem ciphers {ciphers}\n"
|
"certs/sample_loadbalancer_id_1/SHA_ID.pem ciphers {ciphers} "
|
||||||
|
"alpn {alpn}\n"
|
||||||
" mode http\n"
|
" mode http\n"
|
||||||
" default_backend sample_pool_id_1:sample_listener_id_1\n"
|
" default_backend sample_pool_id_1:sample_listener_id_1\n"
|
||||||
" timeout client 50000\n").format(
|
" timeout client 50000\n").format(
|
||||||
maxconn=constants.HAPROXY_DEFAULT_MAXCONN,
|
maxconn=constants.HAPROXY_DEFAULT_MAXCONN,
|
||||||
crt_list=FAKE_CRT_LIST_FILENAME,
|
crt_list=FAKE_CRT_LIST_FILENAME,
|
||||||
ciphers=constants.CIPHERS_OWASP_SUITE_B)
|
ciphers=constants.CIPHERS_OWASP_SUITE_B,
|
||||||
|
alpn=",".join(constants.AMPHORA_SUPPORTED_ALPN_PROTOCOLS))
|
||||||
be = ("backend sample_pool_id_1:sample_listener_id_1\n"
|
be = ("backend sample_pool_id_1:sample_listener_id_1\n"
|
||||||
" mode http\n"
|
" mode http\n"
|
||||||
" balance roundrobin\n"
|
" balance roundrobin\n"
|
||||||
@ -259,12 +264,14 @@ class TestHaproxyCfg(base.TestCase):
|
|||||||
fe = ("frontend sample_listener_id_1\n"
|
fe = ("frontend sample_listener_id_1\n"
|
||||||
" maxconn {maxconn}\n"
|
" maxconn {maxconn}\n"
|
||||||
" redirect scheme https if !{{ ssl_fc }}\n"
|
" redirect scheme https if !{{ ssl_fc }}\n"
|
||||||
" bind 10.0.0.2:443 ssl crt-list {crt_list}\n"
|
" bind 10.0.0.2:443 ssl crt-list {crt_list} "
|
||||||
|
"alpn {alpn}\n"
|
||||||
" mode http\n"
|
" mode http\n"
|
||||||
" default_backend sample_pool_id_1:sample_listener_id_1\n"
|
" default_backend sample_pool_id_1:sample_listener_id_1\n"
|
||||||
" timeout client 50000\n").format(
|
" timeout client 50000\n").format(
|
||||||
maxconn=constants.HAPROXY_DEFAULT_MAXCONN,
|
maxconn=constants.HAPROXY_DEFAULT_MAXCONN,
|
||||||
crt_list=FAKE_CRT_LIST_FILENAME)
|
crt_list=FAKE_CRT_LIST_FILENAME,
|
||||||
|
alpn=",".join(constants.AMPHORA_SUPPORTED_ALPN_PROTOCOLS))
|
||||||
be = ("backend sample_pool_id_1:sample_listener_id_1\n"
|
be = ("backend sample_pool_id_1:sample_listener_id_1\n"
|
||||||
" mode http\n"
|
" mode http\n"
|
||||||
" balance roundrobin\n"
|
" balance roundrobin\n"
|
||||||
@ -299,6 +306,110 @@ class TestHaproxyCfg(base.TestCase):
|
|||||||
frontend=fe, backend=be),
|
frontend=fe, backend=be),
|
||||||
rendered_obj)
|
rendered_obj)
|
||||||
|
|
||||||
|
def test_render_template_tls_alpn(self):
|
||||||
|
conf = oslo_fixture.Config(cfg.CONF)
|
||||||
|
conf.config(group="haproxy_amphora", base_cert_dir='/fake_cert_dir')
|
||||||
|
FAKE_CRT_LIST_FILENAME = os.path.join(
|
||||||
|
CONF.haproxy_amphora.base_cert_dir,
|
||||||
|
'sample_loadbalancer_id_1/sample_listener_id_1.pem')
|
||||||
|
alpn_protocols = ['chip', 'dale']
|
||||||
|
fe = ("frontend sample_listener_id_1\n"
|
||||||
|
" maxconn {maxconn}\n"
|
||||||
|
" redirect scheme https if !{{ ssl_fc }}\n"
|
||||||
|
" bind 10.0.0.2:443 ssl crt-list {crt_list} "
|
||||||
|
"ciphers {ciphers} no-sslv3 no-tlsv10 no-tlsv11 alpn {alpn}\n"
|
||||||
|
" mode http\n"
|
||||||
|
" default_backend sample_pool_id_1:sample_listener_id_1\n"
|
||||||
|
" timeout client 50000\n").format(
|
||||||
|
maxconn=constants.HAPROXY_DEFAULT_MAXCONN,
|
||||||
|
crt_list=FAKE_CRT_LIST_FILENAME,
|
||||||
|
ciphers=constants.CIPHERS_OWASP_SUITE_B,
|
||||||
|
alpn=",".join(alpn_protocols))
|
||||||
|
be = ("backend sample_pool_id_1:sample_listener_id_1\n"
|
||||||
|
" mode http\n"
|
||||||
|
" balance roundrobin\n"
|
||||||
|
" cookie SRV insert indirect nocache\n"
|
||||||
|
" timeout check 31s\n"
|
||||||
|
" option httpchk GET /index.html HTTP/1.0\\r\\n\n"
|
||||||
|
" http-check expect rstatus 418\n"
|
||||||
|
" fullconn {maxconn}\n"
|
||||||
|
" option allbackups\n"
|
||||||
|
" timeout connect 5000\n"
|
||||||
|
" timeout server 50000\n"
|
||||||
|
" server sample_member_id_1 10.0.0.99:82 "
|
||||||
|
"weight 13 check inter 30s fall 3 rise 2 "
|
||||||
|
"cookie sample_member_id_1\n"
|
||||||
|
" server sample_member_id_2 10.0.0.98:82 "
|
||||||
|
"weight 13 check inter 30s fall 3 rise 2 "
|
||||||
|
"cookie sample_member_id_2\n\n").format(
|
||||||
|
maxconn=constants.HAPROXY_DEFAULT_MAXCONN)
|
||||||
|
rendered_obj = self.jinja_cfg.render_loadbalancer_obj(
|
||||||
|
sample_configs_combined.sample_amphora_tuple(),
|
||||||
|
[sample_configs_combined.sample_listener_tuple(
|
||||||
|
proto='TERMINATED_HTTPS', tls=True,
|
||||||
|
alpn_protocols=alpn_protocols)],
|
||||||
|
tls_certs={'cont_id_1':
|
||||||
|
sample_configs_combined.sample_tls_container_tuple(
|
||||||
|
id='tls_container_id',
|
||||||
|
certificate='ImAalsdkfjCert',
|
||||||
|
private_key='ImAsdlfksdjPrivateKey',
|
||||||
|
primary_cn="FakeCN")})
|
||||||
|
self.assertEqual(
|
||||||
|
sample_configs_combined.sample_base_expected_config(
|
||||||
|
frontend=fe, backend=be),
|
||||||
|
rendered_obj)
|
||||||
|
|
||||||
|
def test_render_template_tls_no_alpn(self):
|
||||||
|
conf = oslo_fixture.Config(cfg.CONF)
|
||||||
|
conf.config(group="haproxy_amphora", base_cert_dir='/fake_cert_dir')
|
||||||
|
FAKE_CRT_LIST_FILENAME = os.path.join(
|
||||||
|
CONF.haproxy_amphora.base_cert_dir,
|
||||||
|
'sample_loadbalancer_id_1/sample_listener_id_1.pem')
|
||||||
|
fe = ("frontend sample_listener_id_1\n"
|
||||||
|
" maxconn {maxconn}\n"
|
||||||
|
" redirect scheme https if !{{ ssl_fc }}\n"
|
||||||
|
" bind 10.0.0.2:443 ssl crt-list {crt_list} "
|
||||||
|
"ciphers {ciphers} no-sslv3 no-tlsv10 no-tlsv11\n"
|
||||||
|
" mode http\n"
|
||||||
|
" default_backend sample_pool_id_1:sample_listener_id_1\n"
|
||||||
|
" timeout client 50000\n").format(
|
||||||
|
maxconn=constants.HAPROXY_DEFAULT_MAXCONN,
|
||||||
|
crt_list=FAKE_CRT_LIST_FILENAME,
|
||||||
|
ciphers=constants.CIPHERS_OWASP_SUITE_B)
|
||||||
|
be = ("backend sample_pool_id_1:sample_listener_id_1\n"
|
||||||
|
" mode http\n"
|
||||||
|
" balance roundrobin\n"
|
||||||
|
" cookie SRV insert indirect nocache\n"
|
||||||
|
" timeout check 31s\n"
|
||||||
|
" option httpchk GET /index.html HTTP/1.0\\r\\n\n"
|
||||||
|
" http-check expect rstatus 418\n"
|
||||||
|
" fullconn {maxconn}\n"
|
||||||
|
" option allbackups\n"
|
||||||
|
" timeout connect 5000\n"
|
||||||
|
" timeout server 50000\n"
|
||||||
|
" server sample_member_id_1 10.0.0.99:82 "
|
||||||
|
"weight 13 check inter 30s fall 3 rise 2 "
|
||||||
|
"cookie sample_member_id_1\n"
|
||||||
|
" server sample_member_id_2 10.0.0.98:82 "
|
||||||
|
"weight 13 check inter 30s fall 3 rise 2 "
|
||||||
|
"cookie sample_member_id_2\n\n").format(
|
||||||
|
maxconn=constants.HAPROXY_DEFAULT_MAXCONN)
|
||||||
|
rendered_obj = self.jinja_cfg.render_loadbalancer_obj(
|
||||||
|
sample_configs_combined.sample_amphora_tuple(),
|
||||||
|
[sample_configs_combined.sample_listener_tuple(
|
||||||
|
proto='TERMINATED_HTTPS', tls=True,
|
||||||
|
alpn_protocols=None)],
|
||||||
|
tls_certs={'cont_id_1':
|
||||||
|
sample_configs_combined.sample_tls_container_tuple(
|
||||||
|
id='tls_container_id',
|
||||||
|
certificate='ImAalsdkfjCert',
|
||||||
|
private_key='ImAsdlfksdjPrivateKey',
|
||||||
|
primary_cn="FakeCN")})
|
||||||
|
self.assertEqual(
|
||||||
|
sample_configs_combined.sample_base_expected_config(
|
||||||
|
frontend=fe, backend=be),
|
||||||
|
rendered_obj)
|
||||||
|
|
||||||
def test_render_template_http(self):
|
def test_render_template_http(self):
|
||||||
be = ("backend sample_pool_id_1:sample_listener_id_1\n"
|
be = ("backend sample_pool_id_1:sample_listener_id_1\n"
|
||||||
" mode http\n"
|
" mode http\n"
|
||||||
@ -1397,7 +1508,7 @@ class TestHaproxyCfg(base.TestCase):
|
|||||||
" maxconn {maxconn}\n"
|
" maxconn {maxconn}\n"
|
||||||
" redirect scheme https if !{{ ssl_fc }}\n"
|
" redirect scheme https if !{{ ssl_fc }}\n"
|
||||||
" bind 10.0.0.2:443 ciphers {ciphers} "
|
" bind 10.0.0.2:443 ciphers {ciphers} "
|
||||||
"no-sslv3 no-tlsv10 no-tlsv11\n"
|
"no-sslv3 no-tlsv10 no-tlsv11 alpn {alpn}\n"
|
||||||
" mode http\n"
|
" mode http\n"
|
||||||
" acl sample_l7rule_id_1 path -m beg /api\n"
|
" acl sample_l7rule_id_1 path -m beg /api\n"
|
||||||
" use_backend sample_pool_id_2:sample_listener_id_1"
|
" use_backend sample_pool_id_2:sample_listener_id_1"
|
||||||
@ -1433,7 +1544,8 @@ class TestHaproxyCfg(base.TestCase):
|
|||||||
" default_backend sample_pool_id_1:sample_listener_id_1\n"
|
" default_backend sample_pool_id_1:sample_listener_id_1\n"
|
||||||
" timeout client 50000\n".format(
|
" timeout client 50000\n".format(
|
||||||
maxconn=constants.HAPROXY_DEFAULT_MAXCONN,
|
maxconn=constants.HAPROXY_DEFAULT_MAXCONN,
|
||||||
ciphers=constants.CIPHERS_OWASP_SUITE_B))
|
ciphers=constants.CIPHERS_OWASP_SUITE_B,
|
||||||
|
alpn=",".join(constants.AMPHORA_SUPPORTED_ALPN_PROTOCOLS)))
|
||||||
be = ("backend sample_pool_id_1:sample_listener_id_1\n"
|
be = ("backend sample_pool_id_1:sample_listener_id_1\n"
|
||||||
" mode http\n"
|
" mode http\n"
|
||||||
" balance roundrobin\n"
|
" balance roundrobin\n"
|
||||||
|
@ -605,13 +605,16 @@ def sample_listener_tuple(proto=None, monitor=True, alloc_default_pool=True,
|
|||||||
backend_tls_ciphers=None,
|
backend_tls_ciphers=None,
|
||||||
tls_versions=constants.TLS_VERSIONS_OWASP_SUITE_B,
|
tls_versions=constants.TLS_VERSIONS_OWASP_SUITE_B,
|
||||||
backend_tls_versions=constants.
|
backend_tls_versions=constants.
|
||||||
TLS_VERSIONS_OWASP_SUITE_B):
|
TLS_VERSIONS_OWASP_SUITE_B,
|
||||||
|
alpn_protocols=constants.
|
||||||
|
AMPHORA_SUPPORTED_ALPN_PROTOCOLS):
|
||||||
proto = 'HTTP' if proto is None else proto
|
proto = 'HTTP' if proto is None else proto
|
||||||
if be_proto is None:
|
if be_proto is None:
|
||||||
be_proto = 'HTTP' if proto == 'TERMINATED_HTTPS' else proto
|
be_proto = 'HTTP' if proto == 'TERMINATED_HTTPS' else proto
|
||||||
if proto != constants.PROTOCOL_TERMINATED_HTTPS:
|
if proto != constants.PROTOCOL_TERMINATED_HTTPS:
|
||||||
tls_ciphers = None
|
tls_ciphers = None
|
||||||
tls_versions = None
|
tls_versions = None
|
||||||
|
alpn_protocols = None
|
||||||
if pool_cert is False:
|
if pool_cert is False:
|
||||||
backend_tls_versions = None
|
backend_tls_versions = None
|
||||||
topology = 'SINGLE' if topology is None else topology
|
topology = 'SINGLE' if topology is None else topology
|
||||||
@ -628,7 +631,7 @@ def sample_listener_tuple(proto=None, monitor=True, alloc_default_pool=True,
|
|||||||
'timeout_tcp_inspect, client_ca_tls_certificate_id, '
|
'timeout_tcp_inspect, client_ca_tls_certificate_id, '
|
||||||
'client_ca_tls_certificate, client_authentication, '
|
'client_ca_tls_certificate, client_authentication, '
|
||||||
'client_crl_container_id, provisioning_status, '
|
'client_crl_container_id, provisioning_status, '
|
||||||
'tls_ciphers, tls_versions')
|
'tls_ciphers, tls_versions, alpn_protocols')
|
||||||
if l7:
|
if l7:
|
||||||
pools = [
|
pools = [
|
||||||
sample_pool_tuple(
|
sample_pool_tuple(
|
||||||
@ -747,7 +750,8 @@ def sample_listener_tuple(proto=None, monitor=True, alloc_default_pool=True,
|
|||||||
client_crl_container_id='cont_id_crl' if client_crl_cert else '',
|
client_crl_container_id='cont_id_crl' if client_crl_cert else '',
|
||||||
provisioning_status=provisioning_status,
|
provisioning_status=provisioning_status,
|
||||||
tls_ciphers=tls_ciphers,
|
tls_ciphers=tls_ciphers,
|
||||||
tls_versions=tls_versions
|
tls_versions=tls_versions,
|
||||||
|
alpn_protocols=alpn_protocols
|
||||||
)
|
)
|
||||||
if recursive_nest:
|
if recursive_nest:
|
||||||
listener.load_balancer.listeners.append(listener)
|
listener.load_balancer.listeners.append(listener)
|
||||||
|
@ -513,3 +513,17 @@ class TestValidations(base.TestCase):
|
|||||||
'TLSv1', 'TLSv1.3'])
|
'TLSv1', 'TLSv1.3'])
|
||||||
self.assertRaises(exceptions.ValidationException,
|
self.assertRaises(exceptions.ValidationException,
|
||||||
validate.check_default_tls_versions_min_conflict)
|
validate.check_default_tls_versions_min_conflict)
|
||||||
|
|
||||||
|
def test_check_alpn_protocols(self):
|
||||||
|
# Test valid list
|
||||||
|
validate.check_alpn_protocols(['h2', 'http/1.1', 'http/1.0'])
|
||||||
|
# Test invalid list
|
||||||
|
self.assertRaises(
|
||||||
|
exceptions.ValidationException,
|
||||||
|
validate.check_alpn_protocols,
|
||||||
|
['httpie', 'foobar/1.2.3'])
|
||||||
|
# Test empty list
|
||||||
|
self.assertRaises(
|
||||||
|
exceptions.ValidationException,
|
||||||
|
validate.check_alpn_protocols,
|
||||||
|
[])
|
||||||
|
@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- |
|
||||||
|
Added support for TLS extension Application Layer Protocol Negotiation
|
||||||
|
(ALPN) to TLS-terminated HTTPS load balancers. A new parameter
|
||||||
|
``alpn_protocols`` was added to the Listener API.
|
||||||
|
- |
|
||||||
|
Octavia provider drivers can now offer HTTP/2 over TLS (protocol
|
||||||
|
negotiation via ALPN) to clients.
|
@ -43,7 +43,7 @@ castellan>=0.16.0 # Apache-2.0
|
|||||||
tenacity>=5.0.4 # Apache-2.0
|
tenacity>=5.0.4 # Apache-2.0
|
||||||
distro>=1.2.0 # Apache-2.0
|
distro>=1.2.0 # Apache-2.0
|
||||||
jsonschema>=3.2.0 # MIT
|
jsonschema>=3.2.0 # MIT
|
||||||
octavia-lib>=2.0.0 # Apache-2.0
|
octavia-lib>=2.2.0 # Apache-2.0
|
||||||
netaddr>=0.7.19 # BSD
|
netaddr>=0.7.19 # BSD
|
||||||
simplejson>=3.13.2 # MIT
|
simplejson>=3.13.2 # MIT
|
||||||
setproctitle>=1.1.10 # BSD
|
setproctitle>=1.1.10 # BSD
|
||||||
|
Loading…
Reference in New Issue
Block a user