diff --git a/data/templates/load-balancing/haproxy.cfg.j2 b/data/templates/load-balancing/haproxy.cfg.j2 index 0a40e1ecf..a75ee9904 100644 --- a/data/templates/load-balancing/haproxy.cfg.j2 +++ b/data/templates/load-balancing/haproxy.cfg.j2 @@ -1,164 +1,164 @@ # Generated by ${vyos_conf_scripts_dir}/load-balancing-haproxy.py global log /dev/log local0 log /dev/log local1 notice chroot /var/lib/haproxy stats socket /run/haproxy/admin.sock mode 660 level admin stats timeout 30s user haproxy group haproxy daemon {% if global_parameters is vyos_defined %} {% if global_parameters.max_connections is vyos_defined %} maxconn {{ global_parameters.max_connections }} {% endif %} # Default SSL material locations ca-base /etc/ssl/certs crt-base /etc/ssl/private {% if global_parameters.ssl_bind_ciphers is vyos_defined %} # https://ssl-config.mozilla.org/#server=haproxy&version=2.6.12-1&config=intermediate&openssl=3.0.8-1&guideline=5.6 ssl-default-bind-ciphers {{ global_parameters.ssl_bind_ciphers | join(':') | upper }} {% endif %} ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 {% if global_parameters.tls_version_min is vyos_defined('1.3') %} ssl-default-bind-options force-tlsv13 {% else %} ssl-default-bind-options ssl-min-ver TLSv1.2 no-tls-tickets {% endif %} {% endif %} defaults log global mode http option dontlognull timeout connect 10s timeout client 50s timeout server 50s errorfile 400 /etc/haproxy/errors/400.http errorfile 403 /etc/haproxy/errors/403.http errorfile 408 /etc/haproxy/errors/408.http errorfile 500 /etc/haproxy/errors/500.http errorfile 502 /etc/haproxy/errors/502.http errorfile 503 /etc/haproxy/errors/503.http errorfile 504 /etc/haproxy/errors/504.http # Frontend {% if service is vyos_defined %} {% for front, front_config in service.items() %} frontend {{ front }} {% set ssl_front = 'ssl crt /run/haproxy/' ~ front_config.ssl.certificate ~ '.pem' if front_config.ssl.certificate is vyos_defined else '' %} {% if front_config.listen_address is vyos_defined %} {% for address in front_config.listen_address %} bind {{ address | bracketize_ipv6 }}:{{ front_config.port }} {{ ssl_front }} {% endfor %} {% else %} bind :::{{ front_config.port }} v4v6 {{ ssl_front }} {% endif %} {% if front_config.redirect_http_to_https is vyos_defined %} http-request redirect scheme https unless { ssl_fc } {% endif %} {% if front_config.mode is vyos_defined %} mode {{ front_config.mode }} {% endif %} {% if front_config.rule is vyos_defined %} {% for rule, rule_config in front_config.rule.items() %} # rule {{ rule }} {% if rule_config.domain_name is vyos_defined and rule_config.set.backend is vyos_defined %} {% set rule_options = 'hdr(host)' %} {% if rule_config.ssl is vyos_defined %} {% set ssl_rule_translate = {'req-ssl-sni': 'req_ssl_sni', 'ssl-fc-sni': 'ssl_fc_sni', 'ssl-fc-sni-end': 'ssl_fc_sni_end'} %} {% set rule_options = ssl_rule_translate[rule_config.ssl] %} {% endif %} {% for domain in rule_config.domain_name %} acl {{ rule }} {{ rule_options }} -i {{ domain }} {% endfor %} use_backend {{ rule_config.set.backend }} if {{ rule }} {% endif %} {# path url #} {% if rule_config.url_path is vyos_defined and rule_config.set.redirect_location is vyos_defined %} {% set path_mod_translate = {'begin': '-i -m beg', 'end': '-i -m end', 'exact': ''} %} {% for path, path_config in rule_config.url_path.items() %} {% for url in path_config %} acl {{ rule }} path {{ path_mod_translate[path] }} {{ url }} {% endfor %} {% endfor %} http-request redirect location {{ rule_config.set.redirect_location }} code 301 if {{ rule }} {% endif %} {# endpath #} {% endfor %} {% endif %} {% if front_config.backend is vyos_defined %} {% for backend in front_config.backend %} default_backend {{ backend }} {% endfor %} {% endif %} {% endfor %} {% endif %} # Backend {% if backend is vyos_defined %} {% for back, back_config in backend.items() %} backend {{ back }} {% if back_config.balance is vyos_defined %} {% set balance_translate = {'least-connection': 'leastconn', 'round-robin': 'roundrobin', 'source-address': 'source'} %} balance {{ balance_translate[back_config.balance] }} {% endif %} {# If mode is not TCP skip Forwarded #} {% if back_config.mode is not vyos_defined('tcp') %} option forwardfor http-request set-header X-Forwarded-Port %[dst_port] http-request add-header X-Forwarded-Proto https if { ssl_fc } {% endif %} {% if back_config.mode is vyos_defined %} mode {{ back_config.mode }} {% endif %} {% if back_config.rule is vyos_defined %} {% for rule, rule_config in back_config.rule.items() %} {% if rule_config.domain_name is vyos_defined and rule_config.set.server is vyos_defined %} {% set rule_options = 'hdr(host)' %} {% if rule_config.ssl is vyos_defined %} {% set ssl_rule_translate = {'req-ssl-sni': 'req_ssl_sni', 'ssl-fc-sni': 'ssl_fc_sni', 'ssl-fc-sni-end': 'ssl_fc_sni_end'} %} {% set rule_options = ssl_rule_translate[rule_config.ssl] %} {% endif %} {% for domain in rule_config.domain_name %} acl {{ rule }} {{ rule_options }} -i {{ domain }} {% endfor %} use-server {{ rule_config.set.server }} if {{ rule }} {% endif %} {# path url #} {% if rule_config.url_path is vyos_defined and rule_config.set.redirect_location is vyos_defined %} {% set path_mod_translate = {'begin': '-i -m beg', 'end': '-i -m end', 'exact': ''} %} {% for path, path_config in rule_config.url_path.items() %} {% for url in path_config %} acl {{ rule }} path {{ path_mod_translate[path] }} {{ url }} {% endfor %} {% endfor %} http-request redirect location {{ rule_config.set.redirect_location }} code 301 if {{ rule }} {% endif %} {# endpath #} {% endfor %} {% endif %} {% if back_config.server is vyos_defined %} {% set ssl_back = 'ssl ca-file /run/haproxy/' ~ back_config.ssl.ca_certificate ~ '.pem' if back_config.ssl.ca_certificate is vyos_defined else '' %} {% for server, server_config in back_config.server.items() %} - server {{ server }} {{ server_config.address }}:{{ server_config.port }}{{ ' check' if server_config.check is vyos_defined }}{{ ' send-proxy' if server_config.send_proxy is vyos_defined }}{{ ' send-proxy-v2' if server_config.send_proxy_v2 is vyos_defined }} {{ ssl_back }} + server {{ server }} {{ server_config.address }}:{{ server_config.port }}{{ ' check' if server_config.check is vyos_defined }}{{ ' backup' if server_config.backup is vyos_defined }}{{ ' send-proxy' if server_config.send_proxy is vyos_defined }}{{ ' send-proxy-v2' if server_config.send_proxy_v2 is vyos_defined }} {{ ssl_back }} {% endfor %} {% endif %} {% if back_config.timeout.check is vyos_defined %} timeout check {{ back_config.timeout.check }}s {% endif %} {% if back_config.timeout.connect is vyos_defined %} timeout connect {{ back_config.timeout.connect }}s {% endif %} {% if back_config.timeout.server is vyos_defined %} timeout server {{ back_config.timeout.server }}s {% endif %} {% endfor %} {% endif %} diff --git a/interface-definitions/load-balancing-haproxy.xml.in b/interface-definitions/load-balancing-haproxy.xml.in index f955a2fb7..564c335ec 100644 --- a/interface-definitions/load-balancing-haproxy.xml.in +++ b/interface-definitions/load-balancing-haproxy.xml.in @@ -1,248 +1,254 @@ <?xml version="1.0"?> <interfaceDefinition> <node name="load-balancing"> <children> <node name="reverse-proxy" owner="${vyos_conf_scripts_dir}/load-balancing-haproxy.py"> <properties> <help>Configure reverse-proxy</help> </properties> <children> <tagNode name="service"> <properties> <help>Frontend service name</help> <constraint> #include <include/constraint/alpha-numeric-hyphen-underscore.xml.i> </constraint> <constraintErrorMessage>Server name must be alphanumeric and can contain hyphen and underscores</constraintErrorMessage> </properties> <children> <leafNode name="backend"> <properties> <help>Backend member</help> <constraint> #include <include/constraint/alpha-numeric-hyphen-underscore.xml.i> </constraint> <constraintErrorMessage>Backend name must be alphanumeric and can contain hyphen and underscores</constraintErrorMessage> <valueHelp> <format>txt</format> <description>Name of reverse-proxy backend system</description> </valueHelp> <completionHelp> <path>load-balancing reverse-proxy backend</path> </completionHelp> <multi/> </properties> </leafNode> #include <include/generic-description.xml.i> #include <include/listen-address.xml.i> #include <include/haproxy/mode.xml.i> #include <include/port-number.xml.i> #include <include/haproxy/rule-frontend.xml.i> <leafNode name="redirect-http-to-https"> <properties> <help>Redirect HTTP to HTTPS</help> <valueless/> </properties> </leafNode> <node name="ssl"> <properties> <help>SSL Certificate, SSL Key and CA</help> </properties> <children> #include <include/pki/certificate.xml.i> </children> </node> </children> </tagNode> <tagNode name="backend"> <properties> <help>Backend server name</help> <constraint> #include <include/constraint/alpha-numeric-hyphen-underscore.xml.i> </constraint> <constraintErrorMessage>Backend name must be alphanumeric and can contain hyphen and underscores</constraintErrorMessage> </properties> <children> <leafNode name="balance"> <properties> <help>Load-balancing algorithm</help> <completionHelp> <list>source-address round-robin least-connection</list> </completionHelp> <valueHelp> <format>source-address</format> <description>Based on hash of source IP address</description> </valueHelp> <valueHelp> <format>round-robin</format> <description>Round robin</description> </valueHelp> <valueHelp> <format>least-connection</format> <description>Least connection</description> </valueHelp> <constraint> <regex>(source-address|round-robin|least-connection)</regex> </constraint> </properties> <defaultValue>round-robin</defaultValue> </leafNode> #include <include/generic-description.xml.i> #include <include/haproxy/mode.xml.i> <node name="parameters"> <properties> <help>Backend parameters</help> </properties> <children> <leafNode name="http-check"> <properties> <help>HTTP health check</help> <valueless/> </properties> </leafNode> </children> </node> #include <include/haproxy/rule-backend.xml.i> <tagNode name="server"> <properties> <help>Backend server name</help> </properties> <children> <leafNode name="address"> <properties> <help>Backend server address</help> <valueHelp> <format>ipv4</format> <description>IPv4 unicast peer address</description> </valueHelp> <valueHelp> <format>ipv6</format> <description>IPv6 unicast peer address</description> </valueHelp> <constraint> <validator name="ip-address"/> </constraint> </properties> </leafNode> + <leafNode name="backup"> + <properties> + <help>Use backup server if other servers are not available</help> + <valueless/> + </properties> + </leafNode> <leafNode name="check"> <properties> <help>Active health check backend server</help> <valueless/> </properties> </leafNode> #include <include/port-number.xml.i> <leafNode name="send-proxy"> <properties> <help>Send a Proxy Protocol version 1 header (text format)</help> <valueless/> </properties> </leafNode> <leafNode name="send-proxy-v2"> <properties> <help>Send a Proxy Protocol version 2 header (binary format)</help> <valueless/> </properties> </leafNode> </children> </tagNode> <node name="ssl"> <properties> <help>SSL Certificate, SSL Key and CA</help> </properties> <children> #include <include/pki/ca-certificate.xml.i> </children> </node> #include <include/haproxy/timeout.xml.i> </children> </tagNode> <node name="global-parameters"> <properties> <help>Global perfomance parameters and limits</help> </properties> <children> <leafNode name="max-connections"> <properties> <help>Maximum allowed connections</help> <valueHelp> <format>u32:1-2000000</format> <description>Maximum allowed connections</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 1-2000000"/> </constraint> </properties> </leafNode> <leafNode name="ssl-bind-ciphers"> <properties> <help>Cipher algorithms ("cipher suite") used during SSL/TLS handshake for all frontend servers</help> <completionHelp> <list>ecdhe-ecdsa-aes128-gcm-sha256 ecdhe-rsa-aes128-gcm-sha256 ecdhe-ecdsa-aes256-gcm-sha384 ecdhe-rsa-aes256-gcm-sha384 ecdhe-ecdsa-chacha20-poly1305 ecdhe-rsa-chacha20-poly1305 dhe-rsa-aes128-gcm-sha256 dhe-rsa-aes256-gcm-sha384</list> </completionHelp> <valueHelp> <format>ecdhe-ecdsa-aes128-gcm-sha256</format> <description>ecdhe-ecdsa-aes128-gcm-sha256</description> </valueHelp> <valueHelp> <format>ecdhe-rsa-aes128-gcm-sha256</format> <description>ecdhe-rsa-aes128-gcm-sha256</description> </valueHelp> <valueHelp> <format>ecdhe-ecdsa-aes256-gcm-sha384</format> <description>ecdhe-ecdsa-aes256-gcm-sha384</description> </valueHelp> <valueHelp> <format>ecdhe-rsa-aes256-gcm-sha384</format> <description>ecdhe-rsa-aes256-gcm-sha384</description> </valueHelp> <valueHelp> <format>ecdhe-ecdsa-chacha20-poly1305</format> <description>ecdhe-ecdsa-chacha20-poly1305</description> </valueHelp> <valueHelp> <format>ecdhe-rsa-chacha20-poly1305</format> <description>ecdhe-rsa-chacha20-poly1305</description> </valueHelp> <valueHelp> <format>dhe-rsa-aes128-gcm-sha256</format> <description>dhe-rsa-aes128-gcm-sha256</description> </valueHelp> <valueHelp> <format>dhe-rsa-aes256-gcm-sha384</format> <description>dhe-rsa-aes256-gcm-sha384</description> </valueHelp> <constraint> <regex>(ecdhe-ecdsa-aes128-gcm-sha256|ecdhe-rsa-aes128-gcm-sha256|ecdhe-ecdsa-aes256-gcm-sha384|ecdhe-rsa-aes256-gcm-sha384|ecdhe-ecdsa-chacha20-poly1305|ecdhe-rsa-chacha20-poly1305|dhe-rsa-aes128-gcm-sha256|dhe-rsa-aes256-gcm-sha384)</regex> </constraint> <multi/> </properties> <defaultValue>ecdhe-ecdsa-aes128-gcm-sha256 ecdhe-rsa-aes128-gcm-sha256 ecdhe-ecdsa-aes256-gcm-sha384 ecdhe-rsa-aes256-gcm-sha384 ecdhe-ecdsa-chacha20-poly1305 ecdhe-rsa-chacha20-poly1305 dhe-rsa-aes128-gcm-sha256 dhe-rsa-aes256-gcm-sha384</defaultValue> </leafNode> <leafNode name="tls-version-min"> <properties> <help>Specify the minimum required TLS version</help> <completionHelp> <list>1.2 1.3</list> </completionHelp> <valueHelp> <format>1.2</format> <description>TLS v1.2</description> </valueHelp> <valueHelp> <format>1.3</format> <description>TLS v1.3</description> </valueHelp> <constraint> <regex>(1.2|1.3)</regex> </constraint> </properties> <defaultValue>1.3</defaultValue> </leafNode> </children> </node> #include <include/interface/vrf.xml.i> </children> </node> </children> </node> </interfaceDefinition> diff --git a/smoketest/scripts/cli/test_load_balancing_reverse_proxy.py b/smoketest/scripts/cli/test_load_balancing_reverse_proxy.py index a33fd5c18..274b97f22 100755 --- a/smoketest/scripts/cli/test_load_balancing_reverse_proxy.py +++ b/smoketest/scripts/cli/test_load_balancing_reverse_proxy.py @@ -1,112 +1,114 @@ #!/usr/bin/env python3 # # Copyright (C) 2023 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. import unittest from base_vyostest_shim import VyOSUnitTestSHIM from vyos.configsession import ConfigSessionError from vyos.utils.process import process_named_running from vyos.utils.file import read_file PROCESS_NAME = 'haproxy' HAPROXY_CONF = '/run/haproxy/haproxy.cfg' base_path = ['load-balancing', 'reverse-proxy'] proxy_interface = 'eth1' class TestLoadBalancingReverseProxy(VyOSUnitTestSHIM.TestCase): def tearDown(self): # Check for running process self.assertTrue(process_named_running(PROCESS_NAME)) self.cli_delete(['interfaces', 'ethernet', proxy_interface, 'address']) self.cli_delete(base_path) self.cli_commit() # Process must be terminated after deleting the config self.assertFalse(process_named_running(PROCESS_NAME)) def test_01_lb_reverse_proxy_domain(self): domains_bk_first = ['n1.example.com', 'n2.example.com', 'n3.example.com'] domain_bk_second = 'n5.example.com' frontend = 'https_front' front_port = '4433' bk_server_first = '192.0.2.11' bk_server_second = '192.0.2.12' bk_first_name = 'bk-01' bk_second_name = 'bk-02' bk_server_port = '9090' mode = 'http' rule_ten = '10' rule_twenty = '20' send_proxy = 'send-proxy' max_connections = '1000' back_base = base_path + ['backend'] self.cli_set(base_path + ['service', frontend, 'mode', mode]) self.cli_set(base_path + ['service', frontend, 'port', front_port]) for domain in domains_bk_first: self.cli_set(base_path + ['service', frontend, 'rule', rule_ten, 'domain-name', domain]) self.cli_set(base_path + ['service', frontend, 'rule', rule_ten, 'set', 'backend', bk_first_name]) self.cli_set(base_path + ['service', frontend, 'rule', rule_twenty, 'domain-name', domain_bk_second]) self.cli_set(base_path + ['service', frontend, 'rule', rule_twenty, 'set', 'backend', bk_second_name]) self.cli_set(back_base + [bk_first_name, 'mode', mode]) self.cli_set(back_base + [bk_first_name, 'server', bk_first_name, 'address', bk_server_first]) self.cli_set(back_base + [bk_first_name, 'server', bk_first_name, 'port', bk_server_port]) self.cli_set(back_base + [bk_first_name, 'server', bk_first_name, send_proxy]) self.cli_set(back_base + [bk_second_name, 'mode', mode]) self.cli_set(back_base + [bk_second_name, 'server', bk_second_name, 'address', bk_server_second]) self.cli_set(back_base + [bk_second_name, 'server', bk_second_name, 'port', bk_server_port]) + self.cli_set(back_base + [bk_second_name, 'server', bk_second_name, 'backup']) self.cli_set(base_path + ['global-parameters', 'max-connections', max_connections]) # commit changes self.cli_commit() config = read_file(HAPROXY_CONF) # Global self.assertIn(f'maxconn {max_connections}', config) # Frontend self.assertIn(f'frontend {frontend}', config) self.assertIn(f'bind :::{front_port} v4v6', config) self.assertIn(f'mode {mode}', config) for domain in domains_bk_first: self.assertIn(f'acl {rule_ten} hdr(host) -i {domain}', config) self.assertIn(f'use_backend {bk_first_name} if {rule_ten}', config) self.assertIn(f'acl {rule_twenty} hdr(host) -i {domain_bk_second}', config) self.assertIn(f'use_backend {bk_second_name} if {rule_twenty}', config) # Backend self.assertIn(f'backend {bk_first_name}', config) self.assertIn(f'balance roundrobin', config) self.assertIn(f'option forwardfor', config) self.assertIn('http-request add-header X-Forwarded-Proto https if { ssl_fc }', config) self.assertIn(f'mode {mode}', config) self.assertIn(f'server {bk_first_name} {bk_server_first}:{bk_server_port} send-proxy', config) self.assertIn(f'backend {bk_second_name}', config) self.assertIn(f'mode {mode}', config) self.assertIn(f'server {bk_second_name} {bk_server_second}:{bk_server_port}', config) + self.assertIn(f'server {bk_second_name} {bk_server_second}:{bk_server_port} backup', config) if __name__ == '__main__': unittest.main(verbosity=2)