Skip to content

Commit 4ef9eed

Browse files
olethanhnesitor
authored andcommitted
HaProxy: Fix targeting issue
Problem: Haproxy was not properly configured. And request could go to any server Analysis: Previously we added backend server via but not the mapping. And the map file was not correctly configured Solution: Update HaProxy mapping as well as backend server via the control socket Fix the way mapping is done: Target should be server name not addr:port Update the haproxy config to only use the fallback server if no mapping if found, otherwise it could be randomly used. Update tests
1 parent cebba2e commit 4ef9eed

File tree

3 files changed

+158
-83
lines changed

3 files changed

+158
-83
lines changed

packaging/aleph-vm/etc/haproxy/haproxy-aleph.cfg

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@ frontend stats
4848
# Frontend for HTTPS traffic (with SNI extraction)
4949
frontend ft_ssl
5050
bind :::443 v4v6
51-
5251
mode tcp
5352

5453
# Inspect SSL handshake
@@ -68,6 +67,7 @@ frontend ft_http
6867
bind :::80 v4v6
6968
mode http
7069

70+
# log-format "%ci:%cp [%tr] %ft %b/%s %TR/%Tw/%Tc/%Tr/%Ta %ST %B %CC %CS %tsc %ac/%fc/%bc/%sc/%rc %sq/%bq %hr %hs %{+Q}r"
7171
# Extract Host header and store it
7272
http-request set-var(txn.host) hdr(host)
7373

@@ -98,21 +98,25 @@ backend bk_ssl
9898

9999
# Use the appropriate variable based on the traffic type
100100
# For HTTPS - Use SNI
101-
use-server %[var(txn.sni),lower,map(/etc/haproxy/https_domains.map)] if { var(txn.sni) -m found }
102-
server fallback_local 127.0.0.1:4443 send-proxy
101+
acl server_found var(txn.sni),lower,map(/etc/haproxy/https_domains.map) -m found
102+
use-server %[var(txn.sni),lower,map(/etc/haproxy/https_domains.map)] if server_found
103+
use-server fallback_local unless server_found
104+
server fallback_local 127.0.0.1:4443 send-proxy
103105

104106
backend bk_ssh
105107
mode tcp
106-
# Use the appropriate variable based on the traffic type
107-
# For SSL - Use SNI
108-
use-server %[var(txn.sni),lower,map(/etc/haproxy/ssh_domains.map)] if { var(txn.sni) -m found }
108+
# Use the appropriate variable based on the traffic type, for SSL - Use SNI
109+
acl server_found var(txn.sni),lower,map(/etc/haproxy/ssh_domains.map) -m found
110+
use-server %[var(txn.sni),lower,map(/etc/haproxy/ssh_domains.map)] if server_found
109111

110112
backend bk_http
111113
mode http
112114
# Use the appropriate variable based on the traffic type
113115
# For HTTP - Use Host header
116+
acl server_found var(txn.host),lower,map(/etc/haproxy/http_domains.map) -m found
114117
http-request set-header Host %[req.hdr(host)]
115-
use-server %[var(txn.host),lower,map(/etc/haproxy/http_domains.map)] if { var(txn.host) -m found }
118+
use-server %[var(txn.host),lower,map(/etc/haproxy/http_domains.map)] if server_found
119+
use-server fallback_local unless server_found
116120
server fallback_local 127.0.0.1:4020
117121

118122
# Default to fallback to the aleph-vm supervisor

src/aleph/vm/haproxy.py

Lines changed: 71 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,16 @@
33
Instance domain support.
44
aka HAProxy Dynamic Configuration Updater
55
6-
HAProxy is a proxy server that is used to redirect the HTTP, HTTPS and SSL trafic
7-
to the instance, if they have it configured.
6+
HAProxy is a proxy server used to redirect the HTTP, HTTPS and SSL traffic
7+
to the instances if they have it configured.
88
9-
This module get the instance domain ip mapping and update the HAProxy config
10-
both live via it's unix socket and via the map file.
9+
This module gets the instance domain ip mapping and updates the HAProxy config
10+
both live via its unix control socket and via the map file.
1111
1212
13-
For the HAP protocol and commands used refer to
13+
For the control protocol and commands used, refer to
1414
https://www.haproxy.com/documentation/haproxy-configuration-manual/2-8r1/management/
1515
16-
FIXME A known bug is that at HAProxy startup, the map file is loaded but the backend are
17-
not set.
1816
"""
1917

2018
import dataclasses
@@ -210,21 +208,42 @@ def get_current_backends(socket_path, backend_name):
210208
return servers
211209

212210

213-
def update_haproxy_backends(socket_path, backend_name, map_file_path, weight=1):
211+
def get_current_mappings(socket_path, map_file) -> dict[str, str]:
212+
"""Get a list of current mapping from HaProxy"""
213+
# show map /etc/haproxy/http_domains.map
214+
command = f"show map {map_file}"
215+
response = send_socket_command(socket_path, command)
216+
217+
if not response:
218+
return {}
219+
try:
220+
mappings = {}
221+
lines = response.splitlines()
222+
for line in lines:
223+
if not line:
224+
continue
225+
mapping_id, mapping_name, mapping_target = line.split()
226+
mappings[mapping_name] = mapping_target
227+
228+
except Exception as e:
229+
msg = f"Error retrieving current mapping: {e!s}"
230+
raise Exception(msg) from e
231+
return mappings
232+
233+
234+
def update_haproxy_backend(socket_path, backend_name, instances, map_file_path, port, weight=1):
214235
"""Update HAProxy backend servers config based on the map file.
215236
216237
Sync the running config with the content of the map file.
217238
218-
This allow us to update the config without needing to reload or restart HAProxy.
239+
This allows us to update the config without needing to reload or restart HAProxy.
219240
220241
It reads domain-to-IP mappings from a map file and uses HAProxy's
221242
socket commands to dynamically add/update backend servers allowing update without requiring a reload
222243
HAProxy.
223244
"""
224-
mappings = parse_map_file(map_file_path)
225-
if not mappings:
226-
logger.error("No valid mappings found in the map file.")
227-
return False
245+
246+
mappings = get_current_mappings(socket_path, map_file_path)
228247

229248
# Get current backend servers
230249
current_servers = get_current_backends(socket_path, backend_name)
@@ -234,39 +253,50 @@ def update_haproxy_backends(socket_path, backend_name, map_file_path, weight=1):
234253
processed_servers = set()
235254

236255
# Process each mapping
237-
for domain, target in mappings:
238-
server_name = domain
256+
for instance in instances:
257+
server_name = instance["name"]
258+
local_ip = instance["ipv4"]["local"]
259+
# custom domain name doesn't return the ip addr but the network range
260+
addr = local_ip.split("/")[0]
261+
if addr.endswith(".1"):
262+
addr = addr.rstrip(".1") + ".2"
239263
processed_servers.add(server_name)
240264

241-
# Check if server already exists in mapping
265+
# Check if the server already exists
242266
if server_name in current_servers:
243-
# FIXME : In the future, don't update the address if it hasn't changed'
244267
# Update existing server
245-
addr, port = target.split(":")
246268
command = f"set server {backend_name}/{server_name} addr {addr} port {port}"
247269
logger.info(f"Updating server: {command}")
248270
response = send_socket_command(socket_path, command)
249-
if response and "not found" in response:
271+
if response and "not found" in response: # Fall back
250272
logger.warning(f"Server not found: {server_name}, trying to add it")
251273
# If server doesn't exist, add it
252-
command = f"add server {backend_name}/{server_name} {target} weight {weight} maxconn 30"
274+
command = f"add server {backend_name}/{server_name} {addr}:{port} weight {weight} maxconn 30"
253275
logger.info(f"Adding server: {command}")
254276
response = send_socket_command(socket_path, command)
255-
else:
256-
# Add new server
257-
command = f"add server {backend_name}/{server_name} {target} weight {weight} maxconn 30"
258-
logger.info(f"Adding server: {command}")
259-
response = send_socket_command(socket_path, command)
277+
if response.strip() != "":
278+
logger.info(f"Error adding server {response}")
260279

261-
# Check response
262-
if response and "not found" in response:
263-
logger.error(f"Error processing server {server_name}: {response}")
264-
else:
265-
command = f"enable server {backend_name}/{server_name}"
266-
logger.info(f"Enable server: {command}")
280+
else: # Add the new server
281+
command = f"add server {backend_name}/{server_name} {addr}:{port} weight {weight} maxconn 30"
282+
logger.info(f"Adding server: {command}")
267283
response = send_socket_command(socket_path, command)
268284
if response.strip() != "":
269-
logger.info("Error enabling server Response")
285+
logger.info(f"Error adding server {response}")
286+
# Enable the server
287+
command = f"enable server {backend_name}/{server_name}"
288+
logger.info(f"Enable server: {command}")
289+
response = send_socket_command(socket_path, command)
290+
if response.strip() != "":
291+
logger.info(f"Error enabling server {response}")
292+
293+
# Add/set mapping
294+
if server_name not in mappings:
295+
response = send_socket_command(socket_path, f"add map {map_file_path} {server_name} {server_name}")
296+
logger.info(f"Added mapping: {server_name} {response=}")
297+
elif mappings[server_name] != server_name:
298+
response = send_socket_command(socket_path, f"set map {map_file_path} {server_name} {server_name}")
299+
logger.info(f"updated mapping: {server_name} {response=}")
270300

271301
# Remove servers that are not in the map file
272302
servers_to_remove = set(current_servers) - processed_servers
@@ -306,36 +336,33 @@ async def fetch_list_and_update(socket_path, local_vms: list[str], force_update)
306336
instances = [i for i in instances if i["item_hash"] in local_vms]
307337
# This should match the config in haproxy.cfg
308338
for backend in HAPROXY_BACKENDS:
309-
update_backend(backend["name"], backend["map_file"], backend["port"], socket_path, instances, force_update)
339+
update_backends(backend["name"], backend["map_file"], backend["port"], socket_path, instances, force_update)
310340

311341

312-
def update_backend(backend_name, map_file_path, port, socket_path, instances, force_update=False):
313-
updated = update_mapfile(instances, map_file_path, port)
342+
def update_backends(backend_name, map_file_path, port, socket_path, instances, force_update=False):
343+
updated = update_mapfile(instances, map_file_path)
314344
if force_update:
315345
logger.info("Updating backends")
316-
update_haproxy_backends(socket_path, backend_name, map_file_path, weight=1)
346+
update_haproxy_backend(socket_path, backend_name, instances, map_file_path, port, weight=1)
317347
elif updated:
318348
logger.info("Map file content changed, updating backends")
319-
update_haproxy_backends(socket_path, backend_name, map_file_path, weight=1)
349+
update_haproxy_backend(socket_path, backend_name, instances, map_file_path, port, weight=1)
320350

321351
else:
322352
logger.debug("Map file content no modification")
323353

324354

325-
def update_mapfile(instances: list, map_file_path: str, port) -> bool:
355+
def update_mapfile(instances: list, map_file_path: str) -> bool:
326356
mapfile = Path(map_file_path)
327357
previous_mapfile = ""
328358
if mapfile.exists():
329359
content = mapfile.read_text()
330360
previous_mapfile = content
331361
current_content = ""
362+
332363
for instance in instances:
333-
local_ip = instance["ipv4"]["local"]
334-
if local_ip:
335-
local_ip = local_ip.split("/")[0]
336-
if local_ip.endswith(".1"):
337-
local_ip = local_ip.rstrip(".1") + ".2"
338-
current_content += f"{instance['name']} {local_ip}:{port}\n"
364+
if instance["ipv4"]["local"]:
365+
current_content += f"{instance['name']} {instance['name']}\n"
339366
updated = current_content != previous_mapfile
340367
if updated:
341368
mapfile.write_text(current_content)

0 commit comments

Comments
 (0)