# HAProxy for RDP

<https://habr.com/ru/articles/335872/>

Совершенно случайно, в пассивном поиске альтернативы устаревшему 2X-LoadBalancer и тяжелому, непонятному Remote Connection Broker от MS наткнулся на HAProxy и его умению проксировать RDP трафик. ~~В выдачах поисковиков практически не выдается haproxy в качестве прокси для RDP~~. Сейчас вдруг пачками стал выдавать. Вместе с тем, коммерческие продукты с таким же функционалом, такие как упоминались выше, стоят приличных денег.

В общем, мне показалось, что это может быть кому-то интересным. Поэтому я решил осветить это решение. Плюс, в конце продемонстрирую гибкость использования HAProxy, которой нет у именитых конкурентов.

### Как это работает, вкратце

HAProxy умеет идентифицировать RDP, проксировать его и парсить rdp\_cookie для выуживания из них нужной информации и последующего использования ее в механизме маршрутизации.

Клиент подключается к прокси, тот вытаскивает логин из rdp\_cookie, выбирает для него сервер, записывает значения «логин — сервер» в stick-table и подключает пользователя к серверу.

Соотвественно, при следующем подключении этого же клиента(с этим логином) прокси находит запись в таблице и подключает его к тому серверу на котором у пользователя открытая сессия. Гениально и просто!

stick-table — это таблица, хранящаяся в памяти процесса, где для каждой записи можно определить время жизни. Выставить 8 часов, и весь день клиент будет попадать на один и тот же сервер.

Ниже стандартный конфиг:

```
#/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 /

```

## Трудности

Так как stick-table располагается в памяти, при перезагрузки процесса теряется вся информация о парах «клиент-сервер», а это критическая информация в нашем случае. Для выхода из ситуации я написал скриптик, который использую для перезагрузки процесса. Скрипт перед остановкой процесса скидывает stick-table в файл, затем после старта процесса записывает данные обратно(текущие сессии при этом не обрываются):

```
#!/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()

```

### Что еще?

Еще можно гибко управлять тем, на какие сервера проксировать тех или иных клиентов. Делать это можно на основании логина, ip адреса, сети, времени суток и т.п.

Я же приведу пример как на основе групп из AD можно разбить сервера фермы по отделам, например:

два сервера для Бухгалтерии

два сервера для Маркетинга

два сервера для Продажников

два для всех остальных.

Понятно, что в каждой группе серверов могут быть различные мощности: установленное ПО и какие-то специфические настройки, поэтому мы их разделим.

HAProxy позволяет на основании определенных политик гибко определять к какому серверу подключать пользователя, имея одну точку входа для всех RDP подключений (что несомненно удобно).

Для это необходимо немного модифицировать конфиг HAProxy и скрипт перезагрузки:

```
#/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 /

```

модифицированный скрипт перезагрузки:

```
#!/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()

```

Как это работает:

В AD создаются группы (и наверняка такие группы уже есть) Accounts, Marketing и Sales, в эти группы помещаются сотрудники. Скрипт подключается к AD и получает список сотрудников по выбранным группам. Список сотрудников сохраняется в файл с именем группы.

В конфиге HAProxy настроены ACL источником которых являются эти файлы групп. Если в группу добавляется новый сотрудник, необходимо выполнить скрипт для обновления файла группы.

Прокси проверяет, есть-ли логин в указанном файле. Если есть, отправляет на определенный для этой группы бакенд. Все очень просто!

Параметры запуска скрипта:

haproxy.py group group\_name — перезагрузка группы, текущие сессии при этом не обрываются.

haproxy.py restart — перезагрузка процесса (перечитать конфиг), при этом текущие сессии не обрываются.

### Отказоустойчивость

Ее нет!

В данном примере решение не обладает никакой отказоустойчивостью.

Во первых, не зарезервирован haproxy.

Во вторых, решение с записью значений «клиент-сервер» в stick-table не позволяет haproxy подключать пользователей к живым серверам, чьи записи уже есть в таблице, и сервер к которому они были подключены в данным момент недоступен. Он тупо будет пытаться отправить их на сервер из таблицы, несмотря на то, что он не в сети.

Первое, резервирование haproxy можно сделать различными способами.

~~Один из них — модифицированый скрипт перезагрузки. В него можно добавить копирование и загрузку сохраненных таблиц на другом haproxy, с запуском этого скрипта переодически по крону.~~

Спасибо [vasilevkirill](https://habr.com/ru/users/vasilevkirill/), есть встроенное решение, которым он поделился в комментарии

[habrahabr.ru/post/335872/#comment\_10369854](https://habrahabr.ru/post/335872/#comment_10369854)

Второе сложнее. Нужен механизм, который бы точно определял, что с сервером. Сервер может по каким то легальным и не очень причинам быть не доступен по сети некоторое время, скажем 1 минуту, к примеру. Но при этом иметь открытыми все RDP сессии. И если мы решим, что сервер больше не доступен, и нужно всех пользователей переключать на другие сервера, то можем получить несохраненные данные, клиенты могут потерять большой обьем работ и тп.

Технически же, реализовать очистку stick-table не вызывает трудности. Для отслеживания состояния серверов можно использовать различные мониторинговые системы. В том же Zabbix, по событиям можно вызывать локальные скрипты.В нашем случае можно вызывать скрипт очистки stick-table.

В заключении, с учетом тех недостатков, которые я указал выше, HAProxy работает очень стабильно и надежно.


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://book.konstantinsecurity.com/readme/architect/load-balance/haproxy/haproxy-for-rdp.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
