summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristian Breunig <christian@breunig.cc>2023-05-17 18:46:07 +0200
committerGitHub <noreply@github.com>2023-05-17 18:46:07 +0200
commit9e9794a763e7e56a3a4c0001ea2b5bbc46bbb681 (patch)
treeea53b705e7cf44a391a0dbef1323f97b5d735f54
parent0c91c356183bb33dea0b24c9ccd77b55c15e7ea0 (diff)
parent6d0325190fcede5b912c20cfb6ffefab90a3f4f4 (diff)
downloadvyos-1x-9e9794a763e7e56a3a4c0001ea2b5bbc46bbb681.tar.gz
vyos-1x-9e9794a763e7e56a3a4c0001ea2b5bbc46bbb681.zip
Merge pull request #2004 from sever-sever/T5222
T5222: Add load-balancing for web traffic
-rw-r--r--data/templates/load-balancing/haproxy.cfg.j2154
-rw-r--r--data/templates/load-balancing/override_haproxy.conf.j214
-rw-r--r--debian/control1
-rw-r--r--interface-definitions/include/haproxy/mode.xml.i22
-rw-r--r--interface-definitions/include/haproxy/rule.xml.i130
-rw-r--r--interface-definitions/include/haproxy/timeout.xml.i45
-rw-r--r--interface-definitions/load-balancing-haproxy.xml.in245
-rwxr-xr-xsrc/conf_mode/load-balancing-haproxy.py182
8 files changed, 793 insertions, 0 deletions
diff --git a/data/templates/load-balancing/haproxy.cfg.j2 b/data/templates/load-balancing/haproxy.cfg.j2
new file mode 100644
index 000000000..3d98d78b7
--- /dev/null
+++ b/data/templates/load-balancing/haproxy.cfg.j2
@@ -0,0 +1,154 @@
+# 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.tls.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.tls.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.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 server is vyos_defined %}
+{% for front, front_config in server.items() %}
+frontend {{ front }}
+{% set ssl_front = 'ssl crt /run/haproxy/' ~ front_config.ssl.certificate ~ '.pem' if front_config.ssl.certificate is vyos_defined else '' %}
+ bind {{ front_config.listen_address if front_config.listen_address if vyos_defined else '*' }}:{{ front_config.port }} {{ ssl_front }}
+{% 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.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 %}
+ acl {{ rule }} {{ rule_options }} -i {{ rule_config.domain_name }}
+ use_backend {{ 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 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 %}
+ acl {{ rule }} {{ rule_options }} -i {{ rule_config.domain_name }}
+ 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 }} {{ ssl_back }}
+{% endfor %}
+{% endif %}
+{% if back_config.timeout.check is vyos_defined %}
+ timeout check {{ back_config.timeout.check }}
+{% endif %}
+{% if back_config.timeout.connect is vyos_defined %}
+ timeout connect {{ back_config.timeout.connect }}
+{% endif %}
+{% if back_config.timeout.server is vyos_defined %}
+ timeout server {{ back_config.timeout.server }}
+{% endif %}
+
+{% endfor %}
+{% endif %}
+
diff --git a/data/templates/load-balancing/override_haproxy.conf.j2 b/data/templates/load-balancing/override_haproxy.conf.j2
new file mode 100644
index 000000000..395b5d279
--- /dev/null
+++ b/data/templates/load-balancing/override_haproxy.conf.j2
@@ -0,0 +1,14 @@
+{% set haproxy_command = 'ip vrf exec ' ~ vrf ~ ' ' if vrf is vyos_defined else '' %}
+[Unit]
+StartLimitIntervalSec=0
+After=vyos-router.service
+ConditionPathExists=/run/haproxy/haproxy.cfg
+
+[Service]
+EnvironmentFile=
+Environment=
+Environment="CONFIG=/run/haproxy/haproxy.cfg" "PIDFILE=/run/haproxy.pid" "EXTRAOPTS=-S /run/haproxy-master.sock"
+ExecStart=
+ExecStart={{ haproxy_command }}/usr/sbin/haproxy -Ws -f /run/haproxy/haproxy.cfg -p /run/haproxy.pid -S /run/haproxy-master.sock
+Restart=always
+RestartSec=10
diff --git a/debian/control b/debian/control
index 4a2706fc3..ec08968c9 100644
--- a/debian/control
+++ b/debian/control
@@ -65,6 +65,7 @@ Depends:
fuse-overlayfs,
libpam-google-authenticator,
grc,
+ haproxy,
hostapd,
hsflowd,
hvinfo,
diff --git a/interface-definitions/include/haproxy/mode.xml.i b/interface-definitions/include/haproxy/mode.xml.i
new file mode 100644
index 000000000..672ea65b4
--- /dev/null
+++ b/interface-definitions/include/haproxy/mode.xml.i
@@ -0,0 +1,22 @@
+<!-- include start from haproxy/mode.xml.i -->
+<leafNode name="mode">
+ <properties>
+ <help>Proxy mode</help>
+ <completionHelp>
+ <list>http tcp</list>
+ </completionHelp>
+ <constraintErrorMessage>invalid value</constraintErrorMessage>
+ <valueHelp>
+ <format>http</format>
+ <description>HTTP proxy mode</description>
+ </valueHelp>
+ <valueHelp>
+ <format>tcp</format>
+ <description>TCP proxy mode</description>
+ </valueHelp>
+ <constraint>
+ <regex>(http|tcp)</regex>
+ </constraint>
+ </properties>
+</leafNode>
+<!-- include end -->
diff --git a/interface-definitions/include/haproxy/rule.xml.i b/interface-definitions/include/haproxy/rule.xml.i
new file mode 100644
index 000000000..9d9f63c9c
--- /dev/null
+++ b/interface-definitions/include/haproxy/rule.xml.i
@@ -0,0 +1,130 @@
+<!-- include start from haproxy/rule.xml.i -->
+<tagNode name="rule">
+ <properties>
+ <help>Proxy rule number</help>
+ <valueHelp>
+ <format>u32:1-10000</format>
+ <description>Number for this proxy rule</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-10000"/>
+ </constraint>
+ <constraintErrorMessage>Proxy rule number must be between 1 and 10000</constraintErrorMessage>
+ </properties>
+ <children>
+ <leafNode name="domain-name">
+ <properties>
+ <help>Domain name to match</help>
+ <valueHelp>
+ <format>txt</format>
+ <description>Domain address to match</description>
+ </valueHelp>
+ <constraint>
+ <validator name="fqdn"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <node name="set">
+ <properties>
+ <help>Proxy modifications</help>
+ </properties>
+ <children>
+ <leafNode name="redirect-location">
+ <properties>
+ <help>Set URL location</help>
+ <valueHelp>
+ <format>url</format>
+ <description>Set URL location</description>
+ </valueHelp>
+ <constraint>
+ <regex>^\/[\w\-.\/]+$</regex>
+ </constraint>
+ <constraintErrorMessage>Incorrect URL format</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="server">
+ <properties>
+ <help>Server name</help>
+ <constraint>
+ <regex>[-_a-zA-Z0-9]+</regex>
+ </constraint>
+ <constraintErrorMessage>Server name must be alphanumeric and can contain hyphen and underscores</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="ssl">
+ <properties>
+ <help>SSL match options</help>
+ <completionHelp>
+ <list>req-ssl-sni ssl-fc-sni</list>
+ </completionHelp>
+ <valueHelp>
+ <format>req-ssl-sni</format>
+ <description>SSL Server Name Indication (SNI) request match</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ssl-fc-sni</format>
+ <description>SSL frontend connection Server Name Indication match</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ssl-fc-sni-end</format>
+ <description>SSL frontend match end of connection Server Name Indication</description>
+ </valueHelp>
+ <constraint>
+ <regex>(req-ssl-sni|ssl-fc-sni|ssl-fc-sni-end)</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ <node name="url-path">
+ <properties>
+ <help>URL path match</help>
+ </properties>
+ <children>
+ <leafNode name="begin">
+ <properties>
+ <help>Begin URL match</help>
+ <valueHelp>
+ <format>url</format>
+ <description>Begin URL</description>
+ </valueHelp>
+ <constraint>
+ <regex>^\/[\w\-.\/]+$</regex>
+ </constraint>
+ <constraintErrorMessage>Incorrect URL format</constraintErrorMessage>
+ <multi/>
+ </properties>
+ </leafNode>
+ <leafNode name="end">
+ <properties>
+ <help>End URL match</help>
+ <valueHelp>
+ <format>url</format>
+ <description>End URL</description>
+ </valueHelp>
+ <constraint>
+ <regex>^\/[\w\-.\/]+$</regex>
+ </constraint>
+ <constraintErrorMessage>Incorrect URL format</constraintErrorMessage>
+ <multi/>
+ </properties>
+ </leafNode>
+ <leafNode name="exact">
+ <properties>
+ <help>Exactly URL match</help>
+ <valueHelp>
+ <format>url</format>
+ <description>Exactly URL</description>
+ </valueHelp>
+ <constraint>
+ <regex>^\/[\w\-.\/]+$</regex>
+ </constraint>
+ <constraintErrorMessage>Incorrect URL format</constraintErrorMessage>
+ <multi/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+</tagNode>
+<!-- include end -->
diff --git a/interface-definitions/include/haproxy/timeout.xml.i b/interface-definitions/include/haproxy/timeout.xml.i
new file mode 100644
index 000000000..250b35683
--- /dev/null
+++ b/interface-definitions/include/haproxy/timeout.xml.i
@@ -0,0 +1,45 @@
+<!-- include start from haproxy/timeout.xml.i -->
+<node name="timeout">
+ <properties>
+ <help>Tiemout options</help>
+ </properties>
+ <children>
+ <leafNode name="check">
+ <properties>
+ <help>Timeout in seconds for established connections</help>
+ <valueHelp>
+ <format>u32:1-3600</format>
+ <description>Check timeout in seconds</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-3600"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="connect">
+ <properties>
+ <help>Set the maximum time to wait for a connection attempt to a server to succeed</help>
+ <valueHelp>
+ <format>u32:1-3600</format>
+ <description>Connect timeout in seconds</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-3600"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="server">
+ <properties>
+ <help>Set the maximum inactivity time on the server side</help>
+ <valueHelp>
+ <format>u32:1-3600</format>
+ <description>Server timeout in seconds</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-3600"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+</node>
+<!-- include end -->
diff --git a/interface-definitions/load-balancing-haproxy.xml.in b/interface-definitions/load-balancing-haproxy.xml.in
new file mode 100644
index 000000000..268f4cd18
--- /dev/null
+++ b/interface-definitions/load-balancing-haproxy.xml.in
@@ -0,0 +1,245 @@
+<?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="server">
+ <properties>
+ <help>Frontend name</help>
+ <constraint>
+ <regex>[-_a-zA-Z0-9]+</regex>
+ </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>
+ <regex>[-_a-zA-Z0-9]+</regex>
+ </constraint>
+ <constraintErrorMessage>Backend name must be alphanumeric and can contain hyphen and underscores</constraintErrorMessage>
+ <valueHelp>
+ <format>txt</format>
+ <description>Backend name</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.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 name</help>
+ <constraint>
+ <regex>[-_a-zA-Z0-9]+</regex>
+ </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.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="ipv4-address"/>
+ <validator name="ipv6-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="check">
+ <properties>
+ <help>Active health check backend server</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ #include <include/port-number.xml.i>
+ </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>
+ <node name="tls">
+ <properties>
+ <help>Transport Layer Security (TLS) options</help>
+ </properties>
+ <children>
+ <leafNode name="ssl-bind-ciphers">
+ <properties>
+ <help>SSL default bind ciphers</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>
+ </children>
+ </node>
+ #include <include/interface/vrf.xml.i>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/src/conf_mode/load-balancing-haproxy.py b/src/conf_mode/load-balancing-haproxy.py
new file mode 100755
index 000000000..d2b895fbe
--- /dev/null
+++ b/src/conf_mode/load-balancing-haproxy.py
@@ -0,0 +1,182 @@
+#!/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 os
+
+from sys import exit
+from shutil import rmtree
+
+from vyos.config import Config
+from vyos.configdict import dict_merge
+from vyos.util import call
+from vyos.util import check_port_availability
+from vyos.util import is_listen_port_bind_service
+from vyos.pki import wrap_certificate
+from vyos.pki import wrap_private_key
+from vyos.template import render
+from vyos.xml import defaults
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+load_balancing_dir = '/run/haproxy'
+load_balancing_conf_file = f'{load_balancing_dir}/haproxy.cfg'
+systemd_service = 'haproxy.service'
+systemd_override = r'/run/systemd/system/haproxy.service.d/10-override.conf'
+
+
+def get_config(config=None):
+ if config:
+ conf = config
+ else:
+ conf = Config()
+
+ base = ['load-balancing', 'reverse-proxy']
+ lb = conf.get_config_dict(base,
+ get_first_key=True,
+ key_mangling=('-', '_'),
+ no_tag_node_value_mangle=True)
+
+ if lb:
+ lb['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'),
+ get_first_key=True, no_tag_node_value_mangle=True)
+
+ # We have gathered the dict representation of the CLI, but there are default
+ # options which we need to update into the dictionary retrived.
+ default_values = defaults(base)
+ if 'backend' in default_values:
+ del default_values['backend']
+ if lb:
+ lb = dict_merge(default_values, lb)
+
+ if 'backend' in lb:
+ for backend in lb['backend']:
+ default_balues_backend = defaults(base + ['backend'])
+ lb['backend'][backend] = dict_merge(default_balues_backend, lb['backend'][backend])
+
+ return lb
+
+
+def verify(lb):
+ if not lb:
+ return None
+
+ if 'backend' not in lb or 'server' not in lb:
+ raise ConfigError(f'"server" and "backend" must be configured!')
+
+ for front, front_config in lb['server'].items():
+ if 'port' not in front_config:
+ raise ConfigError(f'"{front} server port" must be configured!')
+ # We can use redirect to HTTPS without backend section
+ if 'backend' not in front_config and 'redirect_http_to_https' not in front_config:
+ raise ConfigError(f'"{front} backend" must be configured!')
+
+ # Check if bind address:port are used by another service
+ tmp_address = front_config.get('address', '0.0.0.0')
+ tmp_port = front_config['port']
+ if check_port_availability(tmp_address, int(tmp_port), 'tcp') is not True and \
+ not is_listen_port_bind_service(int(tmp_port), 'haproxy'):
+ raise ConfigError(f'"TCP" port "{tmp_port}" is used by another service')
+
+ for back, back_config in lb['backend'].items():
+ if 'server' not in back_config:
+ raise ConfigError(f'"{back} server" must be configured!')
+ for bk_server, bk_server_conf in back_config['server'].items():
+ if 'address' not in bk_server_conf or 'port' not in bk_server_conf:
+ raise ConfigError(f'"backend {back} server {bk_server} address and port" must be configured!')
+
+
+def generate(lb):
+ if not lb:
+ # Delete /run/haproxy/haproxy.cfg
+ config_files = [load_balancing_conf_file, systemd_override]
+ for file in config_files:
+ if os.path.isfile(file):
+ os.unlink(file)
+ # Delete old directories
+ #if os.path.isdir(load_balancing_dir):
+ # rmtree(load_balancing_dir, ignore_errors=True)
+
+ return None
+
+ # Create load-balance dir
+ if not os.path.isdir(load_balancing_dir):
+ os.mkdir(load_balancing_dir)
+
+ # SSL Certificates for frontend
+ for front, front_config in lb['server'].items():
+ if 'ssl' in front_config:
+ cert_file_path = os.path.join(load_balancing_dir, 'cert.pem')
+ cert_key_path = os.path.join(load_balancing_dir, 'cert.pem.key')
+ ca_cert_file_path = os.path.join(load_balancing_dir, 'ca.pem')
+
+ if 'certificate' in front_config['ssl']:
+ #cert_file_path = os.path.join(load_balancing_dir, 'cert.pem')
+ #cert_key_path = os.path.join(load_balancing_dir, 'cert.key')
+ cert_name = front_config['ssl']['certificate']
+ pki_cert = lb['pki']['certificate'][cert_name]
+
+ with open(cert_file_path, 'w') as f:
+ f.write(wrap_certificate(pki_cert['certificate']))
+
+ if 'private' in pki_cert and 'key' in pki_cert['private']:
+ with open(cert_key_path, 'w') as f:
+ f.write(wrap_private_key(pki_cert['private']['key']))
+
+ if 'ca_certificate' in front_config['ssl']:
+ ca_name = front_config['ssl']['ca_certificate']
+ pki_ca_cert = lb['pki']['ca'][ca_name]
+
+ with open(ca_cert_file_path, 'w') as f:
+ f.write(wrap_certificate(pki_ca_cert['certificate']))
+
+ # SSL Certificates for backend
+ for back, back_config in lb['backend'].items():
+ if 'ssl' in back_config:
+ ca_cert_file_path = os.path.join(load_balancing_dir, 'ca.pem')
+
+ if 'ca_certificate' in back_config['ssl']:
+ ca_name = back_config['ssl']['ca_certificate']
+ pki_ca_cert = lb['pki']['ca'][ca_name]
+
+ with open(ca_cert_file_path, 'w') as f:
+ f.write(wrap_certificate(pki_ca_cert['certificate']))
+
+ render(load_balancing_conf_file, 'load-balancing/haproxy.cfg.j2', lb)
+ render(systemd_override, 'load-balancing/override_haproxy.conf.j2', lb)
+
+ return None
+
+
+def apply(lb):
+ call('systemctl daemon-reload')
+ if not lb:
+ call(f'systemctl stop {systemd_service}')
+ else:
+ call(f'systemctl reload-or-restart {systemd_service}')
+
+ return None
+
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)