Copyright (C) 2008 Remco Hoef All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ $shortcut_section = "haproxy"; require("guiconfig.inc"); require_once("haproxy.inc"); require_once("haproxy_utils.inc"); require_once("haproxy_htmllist.inc"); require_once("pkg_haproxy_tabs.inc"); if (!is_array($config['installedpackages']['haproxy']['ha_pools']['item'])) { $config['installedpackages']['haproxy']['ha_pools']['item'] = array(); } $a_pools = &$config['installedpackages']['haproxy']['ha_pools']['item']; $a_files = haproxy_get_fileslist(); if (isset($_POST['id'])) $id = $_POST['id']; else $id = $_GET['id']; $tmp = get_backend_id($id); if (is_numeric($tmp)) $id = $tmp; if (isset($_GET['dup'])) $id = $_GET['dup']; global $simplefields; $simplefields = array( "name","balance","transparent_clientip","transparent_interface", "check_type","checkinter","log-health-checks","httpcheck_method","monitor_uri","monitor_httpversion","monitor_username","monitor_domain","monitor_agentport", "agent_check","agent_port","agent_inter", "connection_timeout","server_timeout","retries", "stats_enabled","stats_username","stats_password","stats_uri","stats_scope","stats_realm","stats_admin","stats_node","stats_desc","stats_refresh", "persist_stick_expire","persist_stick_tablesize","persist_stick_length","persist_stick_cookiename","persist_sticky_type", "persist_cookie_enabled","persist_cookie_name","persist_cookie_mode","persist_cookie_cachable", "strict_transport_security", "cookie_attribute_secure", "email_level", "email_to" ); $primaryfrontends = get_haproxy_frontends(); $none = array(); $none['']['name']="Address+Port:"; $primaryfrontends = $none + $primaryfrontends; $default = array(); $default['']['name'] = "Default level from global"; $none = array(); $none['dontlog']['name'] = "Dont log"; $a_sysloglevel = $default + $none + $a_sysloglevel; $fields_servers=array(); $fields_servers[0]['name']="status"; $fields_servers[0]['columnheader']="Mode"; $fields_servers[0]['colwidth']="5%"; $fields_servers[0]['type']="select"; $fields_servers[0]['size']="70px"; $fields_servers[0]['items']=&$a_servermodes; $fields_servers[1]['name']="name"; $fields_servers[1]['columnheader']="Name"; $fields_servers[1]['colwidth']="20%"; $fields_servers[1]['type']="textbox"; $fields_servers[1]['size']="30"; $fields_servers[2]['name']="forwardto"; $fields_servers[2]['columnheader']="Forwardto"; $fields_servers[2]['colwidth']="15%"; $fields_servers[2]['type']="select"; $fields_servers[2]['size']="100px"; $fields_servers[2]['items']=&$primaryfrontends; $fields_servers[3]['name']="address"; $fields_servers[3]['columnheader']="Address"; $fields_servers[3]['colwidth']="10%"; $fields_servers[3]['type']="textbox"; $fields_servers[3]['size']="20"; $fields_servers[4]['name']="port"; $fields_servers[4]['columnheader']="Port"; $fields_servers[4]['colwidth']="5%"; $fields_servers[4]['type']="textbox"; $fields_servers[4]['size']="5"; $fields_servers[5]['name']="ssl"; $fields_servers[5]['columnheader']="SSL"; $fields_servers[5]['colwidth']="5%"; $fields_servers[5]['type']="checkbox"; $fields_servers[5]['size']="30"; $fields_servers[6]['name']="weight"; $fields_servers[6]['columnheader']="Weight"; $fields_servers[6]['colwidth']="8%"; $fields_servers[6]['type']="textbox"; $fields_servers[6]['size']="5"; $listitem_none['']['name']="None"; $certs_ca = haproxy_get_certificates('ca'); $certs_ca = $listitem_none + $certs_ca; $certs_client = haproxy_get_certificates('server,user'); $certs_client = $listitem_none + $certs_client; $certs_crl = haproxy_get_crls(); $certs_crl = $listitem_none + $certs_crl; $fields_servers_details=array(); $fields_servers_details[0]['name']="sslserververify"; $fields_servers_details[0]['columnheader']="Check certificate"; $fields_servers_details[0]['description']="SSL servers only, The server certificate will be verified against the CA and CRL certificate configured below."; $fields_servers_details[0]['colwidth']="5%"; $fields_servers_details[0]['type']="checkbox"; $fields_servers_details[0]['size']="5"; $fields_servers_details[1]['name']="verifyhost"; $fields_servers_details[1]['columnheader']="Certificate check CN"; $fields_servers_details[1]['description']="SSL servers only, when set, must match the hostnames in the subject and subjectAlternateNames of the certificate provided by the server."; $fields_servers_details[1]['colwidth']="5%"; $fields_servers_details[1]['type']="textbox"; $fields_servers_details[1]['size']="50"; $fields_servers_details[2]['name']="ssl-server-ca"; $fields_servers_details[2]['columnheader']="CA"; $fields_servers_details[2]['description']="SSL servers only, Select the CA authority to check the server certificate against."; $fields_servers_details[2]['colwidth']="15%"; $fields_servers_details[2]['type']="select"; $fields_servers_details[2]['size']="200px"; $fields_servers_details[2]['items']=$certs_ca; $fields_servers_details[3]['name']="ssl-server-crl"; $fields_servers_details[3]['columnheader']="CRL"; $fields_servers_details[3]['description']="SSL servers only, Select the CRL to check revoked certificates."; $fields_servers_details[3]['colwidth']="15%"; $fields_servers_details[3]['type']="select"; $fields_servers_details[3]['size']="200px"; $fields_servers_details[3]['items']=$certs_crl; $fields_servers_details[4]['name']="ssl-server-clientcert"; $fields_servers_details[4]['columnheader']="Client certificate"; $fields_servers_details[4]['description']="SSL servers only, This certificate will be sent if the server send a client certificate request."; $fields_servers_details[4]['colwidth']="15%"; $fields_servers_details[4]['type']="select"; $fields_servers_details[4]['size']="200px"; $fields_servers_details[4]['items']=$certs_client; $fields_servers_details[5]['name']="cookie"; $fields_servers_details[5]['columnheader']="Cookie"; $fields_servers_details[5]['description']="Persistence only, Used to identify server when cookie persistence is configured for the backend."; $fields_servers_details[5]['colwidth']="10%"; $fields_servers_details[5]['type']="textbox"; $fields_servers_details[5]['size']="10"; $fields_servers_details[6]['name']="maxconn"; $fields_servers_details[6]['columnheader']="Max conn"; $fields_servers_details[6]['description']="Tuning, If the number of incoming concurrent requests goes higher than this value, they will be queued"; $fields_servers_details[6]['colwidth']="15%"; $fields_servers_details[6]['type']="textbox"; $fields_servers_details[6]['size']="10"; $fields_servers_details[7]['name']="advanced"; $fields_servers_details[7]['columnheader']="Advanced"; $fields_servers_details[7]['description']="Advanced, Allows for adding custom HAProxy settings to the server. These are passed as written, use escaping where needed."; $fields_servers_details[7]['colwidth']="15%"; $fields_servers_details[7]['type']="textbox"; $fields_servers_details[7]['size']="80"; $fields_errorfile = array(); $fields_errorfile[0]['name']="errorcode"; $fields_errorfile[0]['columnheader']="errorcode(s)"; $fields_errorfile[0]['colwidth']="15%"; $fields_errorfile[0]['type']="textbox"; $fields_errorfile[0]['size']="70px"; $fields_errorfile[1]['name']="errorfile"; $fields_errorfile[1]['columnheader']="Error Page"; $fields_errorfile[1]['colwidth']="30%"; $fields_errorfile[1]['type']="select"; $fields_errorfile[1]['size']="170px"; $fields_errorfile[1]['items']=&$a_files; if (isset($id) && $a_pools[$id]) { $pconfig['advanced'] = base64_decode($a_pools[$id]['advanced']); $pconfig['advanced_backend'] = base64_decode($a_pools[$id]['advanced_backend']); $pconfig['a_servers']=&$a_pools[$id]['ha_servers']['item']; foreach($simplefields as $stat) $pconfig[$stat] = $a_pools[$id][$stat]; $a_errorfiles = &$a_pools[$id]['errorfiles']['item']; if (!is_array($a_errorfiles)) $a_errorfiles = array(); } if (isset($_GET['dup'])) unset($id); $changedesc = "Services: HAProxy: Backend server pool: "; $changecount = 0; if ($_POST) { $changecount++; unset($input_errors); $pconfig = $_POST; $reqdfields = explode(" ", "name"); $reqdfieldsn = explode(",", "Name"); do_input_validation($_POST, $reqdfields, $reqdfieldsn, $input_errors); if ($_POST['stats_enabled']) { $reqdfields = explode(" ", "name stats_uri"); $reqdfieldsn = explode(",", "Name,Stats Uri"); do_input_validation($_POST, $reqdfields, $reqdfieldsn, $input_errors); if ($_POST['stats_username']) { $reqdfields = explode(" ", "stats_password stats_realm"); $reqdfieldsn = explode(",", "Stats Password,Stats Realm"); do_input_validation($_POST, $reqdfields, $reqdfieldsn, $input_errors); } } if (preg_match("/[^a-zA-Z0-9\.\-_]/", $_POST['name'])) $input_errors[] = "The field 'Name' contains invalid characters."; if ($_POST['checkinter'] !== "" && !is_numeric($_POST['checkinter'])) $input_errors[] = "The field 'Check frequency' value is not a number."; if ($_POST['connection_timeout'] !== "" && !is_numeric($_POST['connection_timeout'])) $input_errors[] = "The field 'Connection timeout' value is not a number."; if ($_POST['server_timeout'] !== "" && !is_numeric($_POST['server_timeout'])) $input_errors[] = "The field 'Server timeout' value is not a number."; if ($_POST['retries'] !== "" && !is_numeric($_POST['retries'])) $input_errors[] = "The field 'Retries' value is not a number."; // the colon ":" is invalid in the username, other than that pretty much any character can be used. if (preg_match("/[^a-zA-Z0-9!-\/;-~ ]/", $_POST['stats_username'])) $input_errors[] = "The field 'Stats Username' contains invalid characters."; // the colon ":" can also be used in the password if (preg_match("/[^a-zA-Z0-9!-~ ]/", $_POST['stats_password'])) $input_errors[] = "The field 'Stats Password' contains invalid characters."; if (preg_match("/[^a-zA-Z0-9\-_]/", $_POST['stats_node'])) $input_errors[] = "The field 'Stats Node' contains invalid characters. Should be a string with digits(0-9), letters(A-Z, a-z), hyphen(-) or underscode(_)"; /* Ensure that our pool names are unique */ for ($i=0; isset($config['installedpackages']['haproxy']['ha_pools']['item'][$i]); $i++) if (($_POST['name'] == $config['installedpackages']['haproxy']['ha_pools']['item'][$i]['name']) && ($i != $id)) $input_errors[] = "This pool name has already been used. Pool names must be unique."; $a_servers = haproxy_htmllist_get_values(array_merge($fields_servers,$fields_servers_details)); foreach($a_servers as $server){ $server_name = $server['name']; $server_address = $server['address']; $server_port = $server['port']; $server_weight = $server['weight']; if (preg_match("/[^a-zA-Z0-9\.\-_]/", $server_name)) $input_errors[] = "The field 'Name' contains invalid characters."; if (!isset($server['forwardto']) || $server['forwardto'] == "") { if (!is_ipaddr($server_address) && !is_hostname($server_address) && !haproxy_is_frontendname($server_address)) $input_errors[] = "The field 'Address' for server $server_name is not a valid ip address or hostname." . $server_address; } else { if ( ($server_address && $server_address != "") || ($server_port && !is_numeric($server_port))) { $input_errors[] = "'Address' and 'port' should be empty when a 'Forwardto' frontend is chosen other than 'Address+Port'."; } } if (!preg_match("/.{2,}/", $server_name)) $input_errors[] = "The field 'Name' is required (and must be at least 2 characters)."; if ($server_weight && !is_numeric($server_weight)) $input_errors[] = "The field 'Weight' value is not a number."; if ($server_port && !is_numeric($server_port)) $input_errors[] = "The field 'Port' value is not a number."; } $a_errorfiles = haproxy_htmllist_get_values($fields_errorfile); if ($_POST['strict_transport_security'] !== "" && !is_numeric($_POST['strict_transport_security'])) $input_errors[] = "The field 'Strict-Transport-Security' is not empty or a number."; // if (!$input_errors) { $pool = array(); if(isset($id) && $a_pools[$id]) $pool = $a_pools[$id]; if ($pool['name'] != $_POST['name']) { // name changed: if (!is_array($config['installedpackages']['haproxy']['ha_backends']['item'])) { $config['installedpackages']['haproxy']['ha_backends']['item'] = array(); } $a_backend = &$config['installedpackages']['haproxy']['ha_backends']['item']; for ( $i = 0; $i < count($a_backend); $i++) { if ($a_backend[$i]['backend_serverpool'] == $pool['name']) $a_backend[$i]['backend_serverpool'] = $_POST['name']; } } if($pool['name'] != "") $changedesc .= " modified pool: '{$pool['name']}'"; $pool['ha_servers']['item']=$a_servers; update_if_changed("advanced", $pool['advanced'], base64_encode($_POST['advanced'])); update_if_changed("advanced_backend", $pool['advanced_backend'], base64_encode($_POST['advanced_backend'])); global $simplefields; foreach($simplefields as $stat) update_if_changed($stat, $pool[$stat], $_POST[$stat]); if (isset($id) && $a_pools[$id]) { $a_pools[$id] = $pool; } else { $a_pools[] = $pool; } if (!isset($input_errors)) { if ($changecount > 0) { touch($d_haproxyconfdirty_path); write_config($changedesc); /* echo "
";
			print_r($config);
			echo "
"; */ } header("Location: haproxy_pools.php"); exit; } $pconfig['a_servers']=&$a_pools[$id]['ha_servers']['item']; } $closehead = false; $pgtitle = "HAProxy: Backend server pool: Edit"; include("head.inc"); // 'processing' done, make all simple fields usable in html. foreach($simplefields as $field){ $pconfig[$field] = htmlspecialchars($pconfig[$field]); } ?>
= '1.6' ) { ?>
Edit HAProxy Backend server pool
Name size="16" maxlength="16" />
Server listShow advanced options(servers need to first be saved to configure these settings) Toggle serverlist help. ">help
Mode: Active: server will be used normally
Backup: server is only used in load balancing when all other non-backup servers are unavailable
Disabled: server is marked down in maintenance mode
Inactive: server will not be available for use
Name: Used to as a name for the server in for example the stats
EXAMPLE: MyWebServer
Address: IP or hostname(only resolved on start-up.)
EXAMPLE: 192.168.1.22 , fe80::1000:2000:3000:4000%em0 , WebServer1.localdomain
Port: The port of the backend.
EXAMPLE: 80 or 443
SSL: Is the backend using SSL (commonly with port 443)
Weight: A weight between 0 and 256, this setting can be used when multiple servers on different hardware need to be balanced with with a different part the traffic. A server with weight 0 wont get new traffic. Default if empty: 1
Cookie: the value of the cookie used to identify a server (only when cookie-persistence is enabled below)
Advanced: More advanced settings like rise,fall,error-limit,send-proxy and others can be configured here.
For a full list of options see the HAProxy manual: Server and default-server options
Balance
/>Round robin Each server is used in turns, according to their weights. This is the smoothest and fairest algorithm when the server's processing time remains equally distributed. This algorithm is dynamic, which means that server weights may be adjusted on the fly for slow starts for instance.
/>Static Round Robin Each server is used in turns, according to their weights. This algorithm is as similar to roundrobin except that it is static, which means that changing a server's weight on the fly will have no effect. On the other hand, it has no design limitation on the number of servers, and when a server goes up, it is always immediately reintroduced into the farm, once the full map is recomputed. It also uses slightly less CPU to run (around -1%).
/>Least Connections The server with the lowest number of connections receives the connection. Round-robin is performed within groups of servers of the same load to ensure that all servers will be used. Use of this algorithm is recommended where very long sessions are expected, such as LDAP, SQL, TSE, etc... but is not very well suited for protocols using short sessions such as HTTP. This algorithm is dynamic, which means that server weights may be adjusted on the fly for slow starts for instance.
/>Source The source IP address is hashed and divided by the total weight of the running servers to designate which server will receive the request. This ensures that the same client IP address will always reach the same server as long as no server goes down or up. If the hash result changes due to the number of running servers changing, many clients will be directed to a different server. This algorithm is generally used in TCP mode where no cookie may be inserted. It may also be used on the Internet to provide a best-effort stickyness to clients which refuse session cookies. This algorithm is static, which means that changing a server's weight on the fly will have no effect.
Transparent ClientIP WARNING Activating this option will load rules in IPFW and might interfere with CaptivePortal and possibly other services due to the way server return traffic must be 'captured' with a automatically created fwd rule. This also breaks directly accessing the (web)server on the ports configured above. Also a automatic sloppy pf rule is made to allow HAProxy to server traffic.
onclick='updatevisibility();' /> Use Client-IP to connect to backend servers.
$name) { $interfaces2[$key]['name'] = $name; } echo_html_select("transparent_interface",$interfaces2,$pconfig['transparent_interface']?$pconfig['transparent_interface']:"lan","","updatevisibility();"); ?>Interface that will connect to the backend server. (this will generally be your LAN or OPT1(dmz) interface)

Connect transparently to the backend server's so the connection seams to come straight from the client ip address. To work properly this requires the reply traffic to pass through pfSense by means of correct routing.
When using IPv6 only routable ip addresses can be used, host names or link-local addresses (FE80) will not work.
(uses the option "source 0.0.0.0 usesrc clientip" or "source ipv6@ usesrc clientip")

Note : When this is enabled for any backend HAProxy will run as 'root' instead of chrooting to a lower privileged user, this reduces security in case a vulnerability is found.
Per server pass thru ' size="64" />
NOTE: paste text into this box that you would like to pass thru. Applied to each 'server' line.
Backend pass thru
NOTE: paste text into this box that you would like to pass thru. Applied to the backend section.
 
Health checking
Health check method
Check frequency size="20" /> milliseconds
For HTTP/HTTPS defaults to 1000 if left blank. For TCP no check will be performed if left empty.
Log checks onclick='updatevisibility();' /> When this option is enabled, any change of the health check status or to the server's health will be logged.
By default, failed health check are logged if server is UP and successful health checks are logged if server is DOWN, so the amount of additional information is limited.
Http check method
OPTIONS is the method usually best to perform server checks, HEAD and GET can also be used
Http check URI size="64" />
Defaults to / if left blank.
Http check version size="64" />
Defaults to "HTTP/1.0" if left blank. Note that the Host field is mandatory in HTTP/1.1, and as a trick, it is possible to pass it after "\r\n" following the version string like this:
    "HTTP/1.1\r\nHost:\ www"
Also some hosts might require an accept parameter like this:
    "HTTP/1.0\r\nHost:\ webservername:8080\r\nAccept:\ */*"
Check with Username size="64" onchange="updatevisibility();" onkeyup="updatevisibility();" />
This is the username which will be used when connecting to MySQL/PostgreSQL server.
USE mysql;
CREATE USER ''@'<pfSenseIP>';
FLUSH PRIVILEGES;
Domain size="64" />
Agentport size="64" />
Fill in the TCP portnumber the healthcheck should be performed on.
 
Agent checks
Use agent checks onclick='updatevisibility();' /> Use a TCP connection to read an ASCII string of the form 100%,75%,drain,down (more about this in the haproxy manual)
Agent port size="64" />
Fill in the TCP portnumber the healthcheck should be performed on.
Agent interval size="64" />
Interval between two agent checks, defaults to 2000 ms.
 
Advanced settings
Connection timeout size="20" />
the time (in milliseconds) we give up if the connection does not complete within (default 30000).
Server timeout size="20" />
the time (in milliseconds) we accept to wait for data from the server, or for the server to accept data (default 30000).
Retries size="20" />
After a connection failure to a server, it is possible to retry, potentially on another server. This is useful if health-checks are too rare and you don't want the clients to see the failures. The number of attempts to reconnect is set by the 'retries' parameter.
 
Cookie persistence
Cookie Enabled onclick='updatevisibility();' /> Enables cookie based persistence. (only used on 'http' frontends)
 
Stick-table persistence
These options are used to make sure seperate requests from a single client go to the same backend. This can be required for servers that keep track of for example a shopping cart.
Stick tables Sticktables that are kept in memory, and when matched make sure the same server will be used.
Stick cookie name size="20" /> Cookiename to use for sticktable
Stick cookie length size="20" /> The maximum number of characters that will be stored in a "string" type stick-table
stick-table expire size="20" /> d=days h=hour m=minute s=seconds ms=miliseconds(default)
Defines the maximum duration of an entry in the stick-table since it was last created, refreshed or matched.
EXAMPLE: 30m
stick-table size size="20" /> maximum number of entries supports suffixes "k", "m", "g" for 2^10, 2^20 and 2^30 factors.
Is the maximum number of entries that can fit in the table. This value directly impacts memory usage. Count approximately 50 bytes per entry, plus the size of a string if any.
EXAMPLE: 50k
 
Email notifications
Mail level Define the maximum loglevel to send emails for.
Mail to size="50"/>
Email address to send emails to, defaults to the value set on the global settings tab.
 
Statistics
Stats Enabled onclick='updatevisibility();' /> Enables the haproxy statistics page (only used on 'http' frontends)
Stats Uri size="64" />
This url can be used when this same backend is used for passing connections to backends
EXAMPLE: / or /haproxy?stats
Stats Scope size="64" />
Determines which frontends and backends are shown, leave empty to show all.
EXAMPLE: frontendA,backend1,backend2
Stats Realm size="64" />
The realm is shown when authentication is requested by haproxy.
EXAMPLE: haproxystats
Stats Username size="64" /> EXAMPLE: admin
Stats Password size="64" /> EXAMPLE: 1Your2Secret3P@ssword
Stats Admin /> Makes available the options disable/enable/softstop/softstart/killsessions from the stats page.
Note: This is not persisted when haproxy restarts. For publicly visible stats pages this should be disabled.
Stats Nodename size="64" />
The short name is displayed in the stats and helps to differentiate which server in a cluster is actually serving clients.
Stats Description size="64" />

The description is displayed behind the Nodename set above.
Stats Refresh size="10" maxlength="30" />
Specify the refresh rate of the stats page in seconds, or specified time unit (us, ms, s, m, h, d).
 
Error files
Use these to replace the error pages that haproxy can generate by custom pages created on the files tab. For example haproxy will generate a 503 error page when no backend is available, you can replace that page here.

 
Advanced
HSTS Strict-Transport-Security When configured enables "HTTP Strict Transport Security" leave empty to disable. (only used on 'http' frontends)
WARNING! the domain will only work over https with a valid certificate!
size="20" /> Seconds
If configured clients that requested the page with this setting active will not be able to visit this domain over a unencrypted http connection. So make sure you understand the consequence of this setting or start with a really low value.
EXAMPLE: 60 for testing if you are absolutely sure you want this 31536000 (12 months) would be good for production.
Cookie protection. onclick='updatevisibility();' /> Set 'secure' attribure on cookies (only used on 'http' frontends)
This configuration option sets up the Secure attribute on cookies if it has not been setup by the application server while the client was browsing the application over a ciphered connection.