Skip to content

Commit

Permalink
wlb: T4470: Migrate WAN load balancer to Python/XML
Browse files Browse the repository at this point in the history
  • Loading branch information
sarthurdev committed Nov 18, 2024
1 parent d51765c commit 821f751
Show file tree
Hide file tree
Showing 12 changed files with 658 additions and 262 deletions.
64 changes: 64 additions & 0 deletions data/templates/load-balancing/nftables-wlb.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#!/usr/sbin/nft -f

{% if first_install is not vyos_defined %}
delete table ip vyos_wanloadbalance
{% endif %}
table ip vyos_wanloadbalance {
chain wlb_nat_postrouting {
type nat hook postrouting priority srcnat - 1; policy accept;
{% for ifname, health_conf in interface_health.items() if health_state[ifname].if_addr %}
{% if disable_source_nat is not vyos_defined %}
{% set state = health_state[ifname] %}
ct mark {{ state.mark }} counter snat to {{ state.if_addr }}
{% endif %}
{% endfor %}
}

chain wlb_mangle_prerouting {
type filter hook prerouting priority mangle; policy accept;
{% for ifname, health_conf in interface_health.items() %}
{% set state = health_state[ifname] %}
{% if sticky_connections is vyos_defined %}
iifname "{{ ifname }}" ct state new ct mark set {{ state.mark }}
{% endif %}
{% endfor %}
{% if rule is vyos_defined %}
{% for rule_id, rule_conf in rule.items() %}
{% if rule_conf.exclude is vyos_defined %}
{{ rule_conf | wlb_nft_rule(rule_id, exclude=True, action='accept') }}
{% else %}
{% set limit = rule_conf.limit is vyos_defined %}
{{ rule_conf | wlb_nft_rule(rule_id, limit=limit, weight=True, health_state=health_state) }}
{{ rule_conf | wlb_nft_rule(rule_id, restore_mark=True) }}
{% endif %}
{% endfor %}
{% endif %}
}

chain wlb_mangle_output {
type filter hook output priority -150; policy accept;
{% if enable_local_traffic is vyos_defined %}
meta mark != 0x0 counter accept
meta l4proto icmp counter accept
ip saddr 127.0.0.0/8 ip daddr 127.0.0.0/8 counter accept
{% if rule is vyos_defined %}
{% for rule_id, rule_conf in rule.items() %}
{% if rule_conf.exclude is vyos_defined %}
{{ rule_conf | wlb_nft_rule(rule_id, local=True, exclude=True, action='accept') }}
{% else %}
{% set limit = rule_conf.limit is vyos_defined %}
{{ rule_conf | wlb_nft_rule(rule_id, local=True, limit=limit, weight=True, health_state=health_state) }}
{{ rule_conf | wlb_nft_rule(rule_id, local=True, restore_mark=True) }}
{% endif %}
{% endfor %}
{% endif %}
{% endif %}
}

{% for ifname, health_conf in interface_health.items() %}
{% set state = health_state[ifname] %}
chain wlb_mangle_isp_{{ ifname }} {
meta mark set {{ state.mark }} ct mark set {{ state.mark }} counter accept
}
{% endfor %}
}
134 changes: 0 additions & 134 deletions data/templates/load-balancing/wlb.conf.j2

This file was deleted.

3 changes: 0 additions & 3 deletions debian/control
Original file line number Diff line number Diff line change
Expand Up @@ -205,9 +205,6 @@ Depends:
# For "load-balancing haproxy"
haproxy,
# End "load-balancing haproxy"
# For "load-balancing wan"
vyatta-wanloadbalance,
# End "load-balancing wan"
# For "service dhcp-relay"
isc-dhcp-relay,
# For "service dhcp-server"
Expand Down
3 changes: 2 additions & 1 deletion python/vyos/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@
'isc_dhclient_dir' : '/run/dhclient',
'dhcp6_client_dir' : '/run/dhcp6c',
'vyos_configdir' : '/opt/vyatta/config',
'completion_dir' : f'{base_dir}/completion'
'completion_dir' : f'{base_dir}/completion',
'ppp_nexthop_dir' : '/run/ppp_nexthop'
}

config_status = '/tmp/vyos-config-status'
Expand Down
5 changes: 5 additions & 0 deletions python/vyos/template.py
Original file line number Diff line number Diff line change
Expand Up @@ -779,6 +779,11 @@ def conntrack_ct_policy(protocol_conf):

return ", ".join(output)

@register_filter('wlb_nft_rule')
def wlb_nft_rule(rule_conf, rule_id, local=False, exclude=False, limit=False, weight=None, health_state=None, action=None, restore_mark=False):
from vyos.wanloadbalance import nft_rule as wlb_nft_rule
return wlb_nft_rule(rule_conf, rule_id, local, exclude, limit, weight, health_state, action, restore_mark)

@register_filter('range_to_regex')
def range_to_regex(num_range):
"""Convert range of numbers or list of ranges
Expand Down
148 changes: 148 additions & 0 deletions python/vyos/wanloadbalance.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
#!/usr/bin/env python3
#
# Copyright (C) 2024 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 vyos.defaults import directories
from vyos.utils.process import run

dhclient_lease = 'dhclient_{0}.lease'

def nft_rule(rule_conf, rule_id, local=False, exclude=False, limit=False, weight=None, health_state=None, action=None, restore_mark=False):
output = []

if 'inbound_interface' in rule_conf:
ifname = rule_conf['inbound_interface']
if local and not exclude:
output.append(f'oifname != "{ifname}"')
elif not local:
output.append(f'iifname "{ifname}"')

if 'protocol' in rule_conf and rule_conf['protocol'] != 'all':
protocol = rule_conf['protocol']
operator = ''

if protocol[:1] == '!':
operator = '!='
protocol = protocol[1:]

if protocol == 'tcp_udp':
protocol = '{ tcp, udp }'

output.append(f'meta l4proto {operator} {protocol}')

for direction in ['source', 'destination']:
if direction not in rule_conf:
continue

direction_conf = rule_conf[direction]
prefix = direction[:1]

if 'address' in direction_conf:
operator = ''
address = direction_conf['address']
if address[:1] == '!':
operator = '!='
address = address[1:]
output.append(f'ip {prefix}addr {operator} {address}')

if 'port' in direction_conf:
operator = ''
port = direction_conf['port']
if port[:1] == '!':
operator = '!='
port = port[1:]
output.append(f'th {prefix}port {operator} {port}')

if 'source_based_routing' not in rule_conf and not restore_mark:
output.append('ct state new')

if limit and 'limit' in rule_conf and 'rate' in rule_conf['limit']:
output.append(f'limit rate {rule_conf["limit"]["rate"]}/{rule_conf["limit"]["period"]}')
if 'burst' in rule_conf['limit']:
output.append(f'burst {rule_conf["limit"]["burst"]} packets')

output.append('counter')

if restore_mark:
output.append('meta mark set ct mark')
elif weight:
weights, total_weight = wlb_weight_interfaces(rule_conf, health_state)
if len(weights) > 1: # Create weight-based verdict map
vmap_str = ", ".join(f'{weight} : jump wlb_mangle_isp_{ifname}' for ifname, weight in weights)
output.append(f'numgen random mod {total_weight} vmap {{ {vmap_str} }}')
elif len(weights) == 1: # Jump to single ISP
ifname, _ = weights[0]
output.append(f'jump wlb_mangle_isp_{ifname}')
else: # No healthy interfaces
return ""
elif action:
output.append(action)

return " ".join(output)

def wlb_weight_interfaces(rule_conf, health_state):
interfaces = [(ifname, int(if_conf.get('weight', 1))) for ifname, if_conf in rule_conf['interface'].items() if health_state[ifname]['state']]

if not interfaces:
return [], 0

if 'failover' in rule_conf:
for ifpair in sorted(interfaces, key=lambda i: i[1], reverse=True):
return [ifpair], ifpair[1] # Return highest weight interface that is ACTIVE when in failover

total_weight = sum(weight for ifname, weight in interfaces)
out = []
start = 0
for ifname, weight in sorted(interfaces, key=lambda i: i[1]): # build weight ranges
end = start + weight - 1
out.append((ifname, f'{start}-{end}' if end > start else start))
start = weight

return out, total_weight

def health_ping_host(host, ifname, count=1, wait_time=0):
cmd_str = f'ping -c {count} -W {wait_time} -I {ifname} {host}'
rc = run(cmd_str)
return rc == 0

def health_ping_host_ttl(host, ifname, count=1, ttl_limit=0):
cmd_str = f'ping -c {count} -t {ttl_limit} -I {ifname} {host}'
rc = run(cmd_str)
return rc != 0

def parse_dhcp_nexthop(ifname):
lease_file = os.path.join(directories['isc_dhclient_dir'], dhclient_lease.format(ifname))

if not os.path.exists(lease_file):
return False

with open(lease_file, 'r') as f:
for line in f.readlines():
data = line.replace('\n', '').split('=')
if data[0] == 'new_routers':
return data[1].replace("'", '').split(" ")[0]

return None

def parse_ppp_nexthop(ifname):
nexthop_file = os.path.join(directories['ppp_nexthop_dir'], ifname)

if not os.path.exists(nexthop_file):
return False

with open(nexthop_file, 'r') as f:
return f.read()
9 changes: 9 additions & 0 deletions smoketest/scripts/cli/base_vyostest_shim.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,15 @@ def verify_nftables_chain(self, nftables_search, table, chain, inverse=False, ar
break
self.assertTrue(not matched if inverse else matched, msg=search)

def verify_nftables_chain_exists(self, table, chain, inverse=False):
try:
cmd(f'sudo nft list chain {table} {chain}')
if inverse:
self.fail(f'Chain exists: {table} {chain}')
except OSError:
if not inverse:
self.fail(f'Chain does not exist: {table} {chain}')

# Verify ip rule output
def verify_rules(self, rules_search, inverse=False, addr_family='inet'):
rule_output = cmd(f'ip -family {addr_family} rule show')
Expand Down
Loading

0 comments on commit 821f751

Please sign in to comment.