📜 ⬆️ ⬇️

HAProxy as LoadBalanсer for RDP truss. Reliable solution for $ 0

Quite by chance, in a passive search for an alternative to the outdated 2X-LoadBalancer and the heavy, incomprehensible Remote Connection Broker from MS, I came across HAProxy and his ability to proxy RDP traffic. In search results, haproxy is practically not issued as a proxy for RDP . Now suddenly he began to give out in batches. However, commercial products with the same functionality, such as mentioned above, are worth decent money.

In general, it seemed to me that this might be interesting to someone. Therefore, I decided to highlight this decision. Plus, at the end I will demonstrate the flexibility of using HAProxy, which is not the case for famous competitors.

How does it work in short


HAProxy can identify RDP, proxy it and parse rdp_cookie to get the necessary information from them and then use it in the routing mechanism.

The client connects to the proxy, he pulls out the login from rdp_cookie, selects a server for it, writes the login - server values ​​to the stick-table and connects the user to the server.
Accordingly, the next time the same client connects (with this login), the proxy finds an entry in the table and connects it to the server on which the user has an open session. Brilliant and easy!
')
A stick-table is a table stored in the process memory, where for each record you can determine the lifetime. Set 8 hours, and all day the client will fall on the same server.

Below is the standard config:

#/usr/local/etc/haproxy.conf global daemon stats socket /var/run/haproxy.sock mode 600 level admin stats timeout 2m defaults log global mode tcp option tcplog option dontlognull frontend fr_rdp mode tcp bind *:3389 name rdp log global option tcplog tcp-request inspect-delay 2s tcp-request content accept if RDP_COOKIE default_backend BK_RDP backend BK_RDP mode tcp balance leastconn timeout server 5s timeout connect 4s log global option tcplog stick-table type string len 32 size 10k expire 8h stick on rdp_cookie(mstshash),bytes(0,6) stick on rdp_cookie(mstshash) option tcp-check tcp-check connect port 3389 default-server inter 3s rise 2 fall 3 server TS01 172.16.50.11:3389 weight 10 check server TS02 172.16.50.12:3389 weight 20 check server TS03 172.16.50.13:3389 weight 10 check server TS04 172.16.50.14:3389 weight 20 check server TS05 172.16.50.15:3389 weight 10 check server TS06 172.16.50.16:3389 weight 10 check server TS07 172.16.50.17:3389 weight 20 check server TS08 172.16.50.18:3389 weight 20 check listen stats bind *:9000 mode http stats enable #stats hide-version stats show-node stats realm Haproxy\ Statistics stats uri / 

Difficulties


Since the stick-table is located in memory, when the process is restarted, all information about client-server pairs is lost, and this is critical information in our case. To get out of the situation, I wrote a scripter that I use to restart the process. Before stopping the process, the script drops the stick-table to a file, then, after starting the process, writes data back (current sessions do not terminate):

 #!/usr/bin/env python import sys import socket import re import subprocess haproxyConf = '/usr/local/etc/haproxy.conf' def restart(): backends = [] with open(haproxyConf) as f: for line in f: lines = line.split(' ') if lines[0] == 'backend': backends.append(lines[1].strip('\n')) for backend in backends: getDataTables(backend) rebootHa() for backend in backends: insertDataTables(backend) # Writes data from stik-tables to external files def getDataTables(table): print table tmp_f = open('/tmp/tmp.' + table,'w') tableVal = {} c = socket.socket( socket.AF_UNIX ) c.connect("/var/run/haproxy.sock") c.send("prompt\r\n") c.send("show table " + table + "\r\n") d = c.recv(10240) for line in d.split('\n'): if re.search('^[a-zA-Z_0-9]',line): line = line.split(' ') del line[0] for item in line: key = item.split('=')[0] val = item.split('=')[1] tableVal[key] = val print tableVal['key'] print tableVal['server_id'] tmp_f.write(tableVal['key'] + ',' + tableVal['server_id'] + '\n') tmp_f.close() def rebootHa(): subprocess.call("/usr/local/etc/rc.d/haproxy reload", shell=True) # Writes data from files to stik-tables def insertDataTables(table): tmp_f = open('/tmp/tmp.' + table,'r') c = socket.socket( socket.AF_UNIX ) c.connect("/var/run/haproxy.sock") c.send("prompt\r\n") for line in tmp_f: line = line.split(',') print "set table " + table + " key " + line[0] + " data.server_id " + line[1] c.send("set table " + table + " key " + line[0] + " data.server_id " + line[1] +"\r\n") c.recv(10240) c.close() restart() 

What else?


You can also flexibly control which servers to proxy for certain clients. This can be done on the basis of the login, ip address, network, time of day, etc.
I will give an example of how, based on groups from AD, you can split farm servers by departments, for example:

two servers for Accounting
two servers for marketing
two servers for Sales Representatives
two for everyone else.

It is clear that in each group of servers there can be different capacities: installed software and some specific settings, so we will separate them.

HAProxy allows, based on certain policies, to flexibly determine which server to connect a user to, having one entry point for all RDP connections (which is undoubtedly convenient).

To do this, you need to slightly modify the HAProxy config and reload script:

 #/usr/local/etc/haproxy.conf global daemon stats socket /var/run/haproxy.sock mode 600 level admin stats timeout 2m defaults log global mode tcp option tcplog option dontlognull frontend fr_rdp mode tcp bind *:3389 name rdp #timeout client 1h log global option tcplog tcp-request inspect-delay 2s tcp-request content accept if RDP_COOKIE acl Accounting_ACL rdp_cookie(mstshash),bytes(0,6) -m str -i -f /usr/local/etc/haproxy/Accounting acl Marketing_ACL rdp_cookie(mstshash),bytes(0,6) -m str -i -f /usr/local/etc/haproxy/Marketing acl Sales_ACL rdp_cookie(mstshash),bytes(0,6) -m str -i -f /usr/local/etc/haproxy/Sales use_backend Accounting_BK if Accounting_ACL use_backend Marketing_BK if Marketing_ACL use_backend Sales_BK if Sales_ACL default_backend DF_RDP backend DF_RDP mode tcp balance leastconn log global option tcplog stick-table type string len 32 size 10k expire 8h stick on rdp_cookie(mstshash),bytes(0,6) option tcp-check tcp-check connect port 3389 default-server inter 3s rise 2 fall 3 server TS01 172.16.50.11:3389 weight 10 check server TS02 172.16.50.12:3389 weight 10 check backend Accounting_BK mode tcp balance leastconn log global stick-table type string len 32 size 10k expire 8h stick on rdp_cookie(mstshash),bytes(0,6) option tcplog tcp-check connect port 3389 default-server inter 3s rise 2 fall 3 server TS03 172.16.50.13:3389 weight 10 check server TS04 172.16.50.14:3389 weight 10 check backend Marketing_BK mode tcp balance leastconn log global stick-table type string len 32 size 10k expire 8h stick on rdp_cookie(mstshash),bytes(0,6) option tcplog tcp-check connect port 3389 default-server inter 3s rise 2 fall 3 server TS05 172.16.50.15:3389 weight 10 check server TS06 172.16.50.16:3389 weight 10 check backend Sales_BK mode tcp balance leastconn log global stick-table type string len 32 size 10k expire 8h stick on rdp_cookie(mstshash),bytes(0,6) option tcplog tcp-check connect port 3389 default-server inter 3s rise 2 fall 3 server TS07 172.16.50.17:3389 weight 10 check server TS08 172.16.50.18:3389 weight 10 check listen stats bind *:9000 mode http stats enable #stats hide-version stats show-node stats realm Haproxy\ Statistics stats uri / 

modified reload script:

 #!/usr/bin/env python import sys import ldap import socket import re import subprocess ldapDomain = '' ldapUser = '' ldapPass = '' ldapDN = '' # OU=GROUPS,DC=domain,DC=tld' haproxyConf = '/usr/local/etc/haproxy.conf' action = sys.argv[1] # Get users from Active Directory Groups and store it to files def getADGroups(): l = ldap.open(ldapDomain) l.simple_bind_s(ldapUser,ldapPass) f = open('/usr/local/etc/haproxy/' + groupName,'w') results = l.search_s("cn=%s, %s" % (groupName, ldapDN), ldap.SCOPE_BASE) for result in results: result_dn = result[0] result_attrs = result[1] if "member" in result_attrs: for member in result_attrs["member"]: f.write(member.split(',')[0].split('=')[1] + '\n') f.close() restart() # Searching stik-tables to save it and to restore after reload def restart(): backends = [] with open(haproxyConf) as f: for line in f: lines = line.split(' ') if lines[0] == 'backend': backends.append(lines[1].strip('\n')) for backend in backends: getDataTables(backend) rebootHa() for backend in backends: insertDataTables(backend) # Writes data from stik-tables to external files def getDataTables(table): print table tmp_f = open('/tmp/tmp.' + table,'w') tableVal = {} c = socket.socket( socket.AF_UNIX ) c.connect("/var/run/haproxy.sock") c.send("prompt\r\n") c.send("show table " + table + "\r\n") d = c.recv(10240) for line in d.split('\n'): if re.search('^[a-zA-Z_0-9]',line): line = line.split(' ') del line[0] for item in line: key = item.split('=')[0] val = item.split('=')[1] tableVal[key] = val print tableVal['key'] print tableVal['server_id'] tmp_f.write(tableVal['key'] + ',' + tableVal['server_id'] + '\n') tmp_f.close() def rebootHa(): #pass subprocess.call("/usr/local/etc/rc.d/haproxy reload", shell=True) # Writes data from files to stik-tables def insertDataTables(table): #pass tmp_f = open('/tmp/tmp.' + table,'r') #tableVal = {} c = socket.socket( socket.AF_UNIX ) c.connect("/var/run/haproxy.sock") c.send("prompt\r\n") for line in tmp_f: line = line.split(',') print "set table " + table + " key " + line[0] + " data.server_id " + line[1] c.send("set table " + table + " key " + line[0] + " data.server_id " + line[1] +"\r\n") c.recv(10240) c.close() if action == 'restart': restart() if action == 'group': groupName = sys.argv[2] getADGroups() 

How it works:
Groups are created in AD (and surely such groups already exist) Accounts, Marketing and Sales, employees are placed in these groups. The script connects to AD and gets a list of employees for the selected groups. The list of employees is saved to a file with the name of the group.

In the HAProxy config, the ACL source is configured by these group files. If a new employee is added to a group, you must run a script to update the group file.

The proxy checks if there is a login in the specified file. If there is, sends to the backend defined for this group. Everything is very simple!

Script launch options:
haproxy.py group group_name - group reloads, current sessions do not terminate.
haproxy.py restart - restart the process (re-read the config), while the current sessions do not terminate.

fault tolerance


She is not!

In this example, the solution does not have any fault tolerance.

Firstly, haproxy is not reserved.

Secondly, the solution with recording client-server values ​​in the stick-table does not allow haproxy to connect users to live servers whose records are already in the table and the server to which they were connected is currently unavailable. He will stupidly try to send them to the server from the table, despite the fact that he is not online.

First, haproxy reservations can be done in various ways.

One of them is a modified reload script. You can add to it the copying and loading of the saved tables on another haproxy, with the launch of this script periodically according to the crown.
Thanks vasilevkirill , there is a built-in solution, which he shared in the comments
habrahabr.ru/post/335872/#comment_10369854

The second is harder. We need a mechanism that would determine exactly what is with the server. The server may for some legal and not very reasons not be available on the network for some time, say 1 minute, for example. But at the same time have open all RDP sessions. And if we decide that the server is no longer available, and we need to switch all users to other servers, we can get unsaved data, customers may lose a large amount of work, and so on.

Technically, it is not difficult to implement stick-table cleaning. To monitor the status of servers, you can use various monitoring systems. In the same Zabbix, local scripts can be called on events. In our case, you can call the stick-table cleanup script.

In conclusion, given the shortcomings that I have indicated above, HAProxy works very stably and reliably.

Source: https://habr.com/ru/post/335872/


All Articles