From 2212a438b234f34f32e08efef2f841ba55a3b6a0 Mon Sep 17 00:00:00 2001
From: sskaje <sskaje@gmail.com>
Date: Tue, 31 Dec 2024 10:44:01 +0800
Subject: wireguard: T4930: allow peers via FQDN

* set interfaces wireguard wgXX peer YY hostname <fqdn>
---
 interface-definitions/interfaces_wireguard.xml.in |  25 +++
 op-mode-definitions/reset-wireguard.xml.in        |  34 ++++
 python/vyos/ifconfig/control.py                   |   4 +-
 python/vyos/ifconfig/wireguard.py                 | 196 +++++++++++++++++-----
 src/conf_mode/interfaces_wireguard.py             |  40 ++++-
 src/conf_mode/nat.py                              |   8 +-
 src/op_mode/reset_wireguard.py                    |  55 ++++++
 src/services/vyos-domain-resolver                 |  48 ++++++
 8 files changed, 360 insertions(+), 50 deletions(-)
 create mode 100644 op-mode-definitions/reset-wireguard.xml.in
 create mode 100755 src/op_mode/reset_wireguard.py

diff --git a/interface-definitions/interfaces_wireguard.xml.in b/interface-definitions/interfaces_wireguard.xml.in
index ce49de038..4f8b6c751 100644
--- a/interface-definitions/interfaces_wireguard.xml.in
+++ b/interface-definitions/interfaces_wireguard.xml.in
@@ -40,6 +40,19 @@
             </properties>
             <defaultValue>0</defaultValue>
           </leafNode>
+          <leafNode name="max-dns-retry">
+            <properties>
+              <help>DNS retries when resolve fails</help>
+              <valueHelp>
+                <format>u32:1-15</format>
+                <description>Maximum number of retries</description>
+              </valueHelp>
+              <constraint>
+                <validator name="numeric" argument="--range 1-15"/>
+              </constraint>
+            </properties>
+            <defaultValue>3</defaultValue>
+          </leafNode>
           <leafNode name="private-key">
             <properties>
               <help>Base64 encoded private key</help>
@@ -104,6 +117,18 @@
                   </constraint>
                 </properties>
               </leafNode>
+              <leafNode name="host-name">
+                <properties>
+                  <help>Hostname of tunnel endpoint</help>
+                  <valueHelp>
+                    <format>hostname</format>
+                    <description>FQDN of WireGuard endpoint</description>
+                  </valueHelp>
+                  <constraint>
+                    <validator name="fqdn"/>
+                  </constraint>
+                </properties>
+              </leafNode>
               #include <include/port-number.xml.i>
               <leafNode name="persistent-keepalive">
                 <properties>
diff --git a/op-mode-definitions/reset-wireguard.xml.in b/op-mode-definitions/reset-wireguard.xml.in
new file mode 100644
index 000000000..c2243f519
--- /dev/null
+++ b/op-mode-definitions/reset-wireguard.xml.in
@@ -0,0 +1,34 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+  <node name="reset">
+    <children>
+      <node name="wireguard">
+        <properties>
+          <help>Reset WireGuard Peers</help>
+        </properties>
+        <children>
+          <tagNode name="interface">
+            <properties>
+              <help>WireGuard interface name</help>
+              <completionHelp>
+                <path>interfaces wireguard</path>
+              </completionHelp>
+            </properties>
+            <command>sudo ${vyos_op_scripts_dir}/reset_wireguard.py reset_peer --interface="$4"</command>
+            <children>
+              <tagNode name="peer">
+                <properties>
+                  <help>WireGuard peer name</help>
+                  <completionHelp>
+                    <path>interfaces wireguard ${COMP_WORDS[3]} peer</path>
+                  </completionHelp>
+                </properties>
+                <command>sudo ${vyos_op_scripts_dir}/reset_wireguard.py reset_peer --interface="$4" --peer="$6"</command>
+              </tagNode>
+            </children>
+          </tagNode>
+        </children>
+      </node>
+    </children>
+  </node>
+</interfaceDefinition>
diff --git a/python/vyos/ifconfig/control.py b/python/vyos/ifconfig/control.py
index 7402da55a..a886c1b9e 100644
--- a/python/vyos/ifconfig/control.py
+++ b/python/vyos/ifconfig/control.py
@@ -48,7 +48,7 @@ class Control(Section):
     def _popen(self, command):
         return popen(command, self.debug)
 
-    def _cmd(self, command):
+    def _cmd(self, command, env=None):
         import re
         if 'netns' in self.config:
             # This command must be executed from default netns 'ip link set dev X netns X'
@@ -61,7 +61,7 @@ class Control(Section):
                 command = command
             else:
                 command = f'ip netns exec {self.config["netns"]} {command}'
-        return cmd(command, self.debug)
+        return cmd(command, self.debug, env=env)
 
     def _get_command(self, config, name):
         """
diff --git a/python/vyos/ifconfig/wireguard.py b/python/vyos/ifconfig/wireguard.py
index 519012625..9d0bd1a6c 100644
--- a/python/vyos/ifconfig/wireguard.py
+++ b/python/vyos/ifconfig/wireguard.py
@@ -25,6 +25,7 @@ from hurry.filesize import alternative
 from vyos.ifconfig import Interface
 from vyos.ifconfig import Operational
 from vyos.template import is_ipv6
+from vyos.template import is_ipv4
 
 class WireGuardOperational(Operational):
     def _dump(self):
@@ -90,12 +91,15 @@ class WireGuardOperational(Operational):
         c.set_level(['interfaces', 'wireguard', self.config['ifname']])
         description = c.return_effective_value(['description'])
         ips = c.return_effective_values(['address'])
+        hostnames = c.return_effective_values(['host-name'])
 
         answer = 'interface: {}\n'.format(self.config['ifname'])
         if description:
             answer += '  description: {}\n'.format(description)
         if ips:
             answer += '  address: {}\n'.format(', '.join(ips))
+        if hostnames:
+            answer += '  hostname: {}\n'.format(', '.join(hostnames))
 
         answer += '  public key: {}\n'.format(wgdump['public_key'])
         answer += '  private key: (hidden)\n'
@@ -155,6 +159,94 @@ class WireGuardOperational(Operational):
                 answer += '\n'
         return answer
 
+    def get_latest_handshakes(self):
+        """Get latest handshake time for each peer"""
+        output = {}
+
+        # Dump wireguard last handshake
+        tmp = self._cmd(f'wg show {self.ifname} latest-handshakes')
+        # Output:
+        # PUBLIC-KEY=    1732812147
+        for line in tmp.split('\n'):
+            if not line:
+                # Skip empty lines and last line
+                continue
+            items = line.split('\t')
+
+            if len(items) != 2:
+                continue
+
+            output[items[0]] = int(items[1])
+
+        return output
+
+    def reset_peer(self, peer_name=None, public_key=None):
+        from vyos.configquery import ConfigTreeQuery
+
+        c = ConfigTreeQuery()
+
+        max_dns_retry = c.value(
+            ['interfaces', 'wireguard', self.ifname, 'max-dns-retry']
+        )
+        if max_dns_retry is None:
+            max_dns_retry = 3
+
+        current_peers = self._dump().get(self.ifname, {}).get('peers', {})
+
+        for peer in c.list_nodes(['interfaces', 'wireguard', self.ifname, 'peer']):
+            peer_public_key = c.value(
+                ['interfaces', 'wireguard', self.ifname, 'peer', peer, 'public-key']
+            )
+            if peer_name is None or peer == peer_name or public_key == peer_public_key:
+                address = c.value(
+                    ['interfaces', 'wireguard', self.ifname, 'peer', peer, 'address']
+                )
+                host_name = c.value(
+                    ['interfaces', 'wireguard', self.ifname, 'peer', peer, 'host-name']
+                )
+                port = c.value(
+                    ['interfaces', 'wireguard', self.ifname, 'peer', peer, 'port']
+                )
+
+                if (not address and not host_name) or not port:
+                    if peer_name is not None:
+                        print(f'Peer {peer_name} endpoint not set')
+                    continue
+
+                # address has higher priority than host-name
+                if address:
+                    new_endpoint = f'{address}:{port}'
+                else:
+                    new_endpoint = f'{host_name}:{port}'
+
+                if c.exists(
+                    ['interfaces', 'wireguard', self.ifname, 'peer', peer, 'disable']
+                ):
+                    continue
+
+                cmd = f'wg set {self.ifname} peer {peer_public_key} endpoint {new_endpoint}'
+                try:
+                    if (
+                        peer_public_key in current_peers
+                        and 'endpoint' in current_peers[peer_public_key]
+                        and current_peers[peer_public_key]['endpoint'] is not None
+                    ):
+                        current_endpoint = current_peers[peer_public_key]['endpoint']
+                        message = f'Resetting {self.ifname} peer {peer_public_key} from {current_endpoint} endpoint to {new_endpoint} ... '
+                    else:
+                        message = f'Resetting {self.ifname} peer {peer_public_key} endpoint to {new_endpoint} ... '
+                    print(
+                        message,
+                        end='',
+                    )
+
+                    self._cmd(
+                        cmd, env={'WG_ENDPOINT_RESOLUTION_RETRIES': str(max_dns_retry)}
+                    )
+                    print('done')
+                except:
+                    print(f'Error\nPlease try to run command manually:\n{cmd}\n')
+
 
 @Interface.register
 class WireGuardIf(Interface):
@@ -186,16 +278,23 @@ class WireGuardIf(Interface):
         tmp_file.flush()
 
         # Wireguard base command is identical for every peer
-        base_cmd = 'wg set {ifname}'
+        base_cmd = 'wg set ' + config['ifname']
+        max_dns_retry = config['max_dns_retry'] if 'max_dns_retry' in config else 3
+
+        interface_cmd = base_cmd
         if 'port' in config:
-            base_cmd += ' listen-port {port}'
+            interface_cmd += ' listen-port {port}'
         if 'fwmark' in config:
-            base_cmd += ' fwmark {fwmark}'
+            interface_cmd += ' fwmark {fwmark}'
 
-        base_cmd += f' private-key {tmp_file.name}'
-        base_cmd = base_cmd.format(**config)
+        interface_cmd += f' private-key {tmp_file.name}'
+        interface_cmd = interface_cmd.format(**config)
         # T6490: execute command to ensure interface configured
-        self._cmd(base_cmd)
+        self._cmd(interface_cmd)
+
+        # If no PSK is given remove it by using /dev/null - passing keys via
+        # the shell (usually bash) is considered insecure, thus we use a file
+        no_psk_file = '/dev/null'
 
         if 'peer' in config:
             for peer, peer_config in config['peer'].items():
@@ -203,43 +302,62 @@ class WireGuardIf(Interface):
                 # marked as disabled - also active sessions are terminated as
                 # the public key was already removed when entering this method!
                 if 'disable' in peer_config:
+                    # remove peer if disabled, no error report even if peer not exists
+                    cmd = base_cmd + ' peer {public_key} remove'
+                    self._cmd(cmd.format(**peer_config))
                     continue
 
-                # start of with a fresh 'wg' command
-                cmd = base_cmd + ' peer {public_key}'
-
-                # If no PSK is given remove it by using /dev/null - passing keys via
-                # the shell (usually bash) is considered insecure, thus we use a file
-                no_psk_file = '/dev/null'
                 psk_file = no_psk_file
-                if 'preshared_key' in peer_config:
-                    psk_file = '/tmp/tmp.wireguard.psk'
-                    with open(psk_file, 'w') as f:
-                        f.write(peer_config['preshared_key'])
-                cmd += f' preshared-key {psk_file}'
-
-                # Persistent keepalive is optional
-                if 'persistent_keepalive' in peer_config:
-                    cmd += ' persistent-keepalive {persistent_keepalive}'
-
-                # Multiple allowed-ip ranges can be defined - ensure we are always
-                # dealing with a list
-                if isinstance(peer_config['allowed_ips'], str):
-                    peer_config['allowed_ips'] = [peer_config['allowed_ips']]
-                cmd += ' allowed-ips ' + ','.join(peer_config['allowed_ips'])
-
-                # Endpoint configuration is optional
-                if {'address', 'port'} <= set(peer_config):
-                    if is_ipv6(peer_config['address']):
-                        cmd += ' endpoint [{address}]:{port}'
-                    else:
-                        cmd += ' endpoint {address}:{port}'
-
-                self._cmd(cmd.format(**peer_config))
 
-                # PSK key file is not required to be stored persistently as its backed by CLI
-                if psk_file != no_psk_file and os.path.exists(psk_file):
-                    os.remove(psk_file)
+                # start of with a fresh 'wg' command
+                peer_cmd = base_cmd + ' peer {public_key}'
+
+                try:
+                    cmd = peer_cmd
+
+                    if 'preshared_key' in peer_config:
+                        psk_file = '/tmp/tmp.wireguard.psk'
+                        with open(psk_file, 'w') as f:
+                            f.write(peer_config['preshared_key'])
+                    cmd += f' preshared-key {psk_file}'
+
+                    # Persistent keepalive is optional
+                    if 'persistent_keepalive' in peer_config:
+                        cmd += ' persistent-keepalive {persistent_keepalive}'
+
+                    # Multiple allowed-ip ranges can be defined - ensure we are always
+                    # dealing with a list
+                    if isinstance(peer_config['allowed_ips'], str):
+                        peer_config['allowed_ips'] = [peer_config['allowed_ips']]
+                    cmd += ' allowed-ips ' + ','.join(peer_config['allowed_ips'])
+
+                    self._cmd(cmd.format(**peer_config))
+
+                    cmd = peer_cmd
+
+                    # Ensure peer is created even if dns not working
+                    if {'address', 'port'} <= set(peer_config):
+                        if is_ipv6(peer_config['address']):
+                            cmd += ' endpoint [{address}]:{port}'
+                        elif is_ipv4(peer_config['address']):
+                            cmd += ' endpoint {address}:{port}'
+                        else:
+                            # don't set endpoint if address uses domain name
+                            continue
+                    elif {'host_name', 'port'} <= set(peer_config):
+                        cmd += ' endpoint {host_name}:{port}'
+
+                    self._cmd(
+                        cmd.format(**peer_config),
+                        env={'WG_ENDPOINT_RESOLUTION_RETRIES': str(max_dns_retry)},
+                    )
+                except:
+                    # todo: logging
+                    pass
+                finally:
+                    # PSK key file is not required to be stored persistently as its backed by CLI
+                    if psk_file != no_psk_file and os.path.exists(psk_file):
+                        os.remove(psk_file)
 
         # call base class
         super().update(config)
diff --git a/src/conf_mode/interfaces_wireguard.py b/src/conf_mode/interfaces_wireguard.py
index b6fd6b0b2..1dbaa9d4e 100755
--- a/src/conf_mode/interfaces_wireguard.py
+++ b/src/conf_mode/interfaces_wireguard.py
@@ -29,11 +29,12 @@ from vyos.ifconfig import WireGuardIf
 from vyos.utils.kernel import check_kmod
 from vyos.utils.network import check_port_availability
 from vyos.utils.network import is_wireguard_key_pair
+from vyos.utils.process import call
 from vyos import ConfigError
 from vyos import airbag
+from pathlib import Path
 airbag.enable()
 
-
 def get_config(config=None):
     """
     Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
@@ -54,6 +55,12 @@ def get_config(config=None):
     if is_node_changed(conf, base + [ifname, 'peer']):
         wireguard.update({'rebuild_required': {}})
 
+    wireguard['peers_need_resolve'] = []
+    if 'peer' in wireguard:
+        for peer, peer_config in wireguard['peer'].items():
+            if 'disable' not in peer_config and 'host_name' in peer_config:
+                wireguard['peers_need_resolve'].append(peer)
+
     return wireguard
 
 def verify(wireguard):
@@ -82,16 +89,15 @@ def verify(wireguard):
         for tmp in wireguard['peer']:
             peer = wireguard['peer'][tmp]
 
+            if 'host_name' in peer and 'address' in peer:
+                raise ConfigError('"host-name" and "address" are mutually exclusive')
+
             if 'allowed_ips' not in peer:
                 raise ConfigError(f'Wireguard allowed-ips required for peer "{tmp}"!')
 
             if 'public_key' not in peer:
                 raise ConfigError(f'Wireguard public-key required for peer "{tmp}"!')
 
-            if ('address' in peer and 'port' not in peer) or ('port' in peer and 'address' not in peer):
-                raise ConfigError('Both Wireguard port and address must be defined '
-                                  f'for peer "{tmp}" if either one of them is set!')
-
             if peer['public_key'] in public_keys:
                 raise ConfigError(f'Duplicate public-key defined on peer "{tmp}"')
 
@@ -99,6 +105,13 @@ def verify(wireguard):
                 if is_wireguard_key_pair(wireguard['private_key'], peer['public_key']):
                     raise ConfigError(f'Peer "{tmp}" has the same public key as the interface "{wireguard["ifname"]}"')
 
+            if 'port' not in peer:
+                if 'host_name' in peer or 'address' in peer:
+                    raise ConfigError(f'Missing "host-name" or "address" on peer "{tmp}"')
+            else:
+                if 'host_name' not in peer and 'address' not in peer:
+                    raise ConfigError(f'Missing "host-name" and "address" on peer "{tmp}"')
+
             public_keys.append(peer['public_key'])
 
 def generate(wireguard):
@@ -122,6 +135,23 @@ def apply(wireguard):
         wg = WireGuardIf(**wireguard)
         wg.update(wireguard)
 
+    domain_resolver_usage = '/run/use-vyos-domain-resolver-interfaces-wireguard-' + wireguard['ifname']
+
+    ## DOMAIN RESOLVER
+    domain_action = 'restart'
+    if 'peers_need_resolve' in wireguard and len(wireguard['peers_need_resolve']) > 0 and 'disable' not in wireguard:
+        from vyos.utils.file import write_file
+
+        text = f'# Automatically generated by interfaces_wireguard.py\nThis file indicates that vyos-domain-resolver service is used by the interfaces_wireguard.\n'
+        text += "intefaces:\n" + "".join([f"  - {peer}\n" for peer in wireguard['peers_need_resolve']])
+        Path(domain_resolver_usage).write_text(text)
+        write_file(domain_resolver_usage, text)
+    else:
+        Path(domain_resolver_usage).unlink(missing_ok=True)
+        if not Path('/run').glob('use-vyos-domain-resolver*'):
+            domain_action = 'stop'
+    call(f'systemctl {domain_action} vyos-domain-resolver.service')
+
     return None
 
 if __name__ == '__main__':
diff --git a/src/conf_mode/nat.py b/src/conf_mode/nat.py
index 98b2f3f29..504b3e82a 100755
--- a/src/conf_mode/nat.py
+++ b/src/conf_mode/nat.py
@@ -17,6 +17,7 @@
 import os
 
 from sys import exit
+from pathlib import Path
 
 from vyos.base import Warning
 from vyos.config import Config
@@ -43,7 +44,6 @@ k_mod = ['nft_nat', 'nft_chain_nat']
 nftables_nat_config = '/run/nftables_nat.conf'
 nftables_static_nat_conf = '/run/nftables_static-nat-rules.nft'
 domain_resolver_usage = '/run/use-vyos-domain-resolver-nat'
-domain_resolver_usage_firewall = '/run/use-vyos-domain-resolver-firewall'
 
 valid_groups = [
     'address_group',
@@ -265,9 +265,9 @@ def apply(nat):
             text = f'# Automatically generated by nat.py\nThis file indicates that vyos-domain-resolver service is used by nat.\n'
             write_file(domain_resolver_usage, text)
         elif os.path.exists(domain_resolver_usage):
-            os.unlink(domain_resolver_usage)
-            if not os.path.exists(domain_resolver_usage_firewall):
-                # Firewall not using domain resolver
+            Path(domain_resolver_usage).unlink(missing_ok=True)
+
+            if not Path('/run').glob('use-vyos-domain-resolver*'):
                 domain_action = 'stop'
         call(f'systemctl {domain_action} vyos-domain-resolver.service')
 
diff --git a/src/op_mode/reset_wireguard.py b/src/op_mode/reset_wireguard.py
new file mode 100755
index 000000000..1fcfb31b5
--- /dev/null
+++ b/src/op_mode/reset_wireguard.py
@@ -0,0 +1,55 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2025 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 sys
+import typing
+
+import vyos.opmode
+
+from vyos.ifconfig import WireGuardIf
+from vyos.configquery import ConfigTreeQuery
+
+
+def _verify(func):
+    """Decorator checks if WireGuard interface config exists"""
+    from functools import wraps
+
+    @wraps(func)
+    def _wrapper(*args, **kwargs):
+        config = ConfigTreeQuery()
+        interface = kwargs.get('interface')
+        if not config.exists(['interfaces', 'wireguard', interface]):
+            unconf_message = f'WireGuard interface {interface} is not configured'
+            raise vyos.opmode.UnconfiguredSubsystem(unconf_message)
+        return func(*args, **kwargs)
+
+    return _wrapper
+
+
+@_verify
+def reset_peer(interface: str, peer: typing.Optional[str] = None):
+    intf = WireGuardIf(interface, create=False, debug=False)
+    return intf.operational.reset_peer(peer)
+
+
+if __name__ == '__main__':
+    try:
+        res = vyos.opmode.run(sys.modules[__name__])
+        if res:
+            print(res)
+    except (ValueError, vyos.opmode.Error) as e:
+        print(e)
+        sys.exit(1)
diff --git a/src/services/vyos-domain-resolver b/src/services/vyos-domain-resolver
index bc74a05d1..6eab7e7e5 100755
--- a/src/services/vyos-domain-resolver
+++ b/src/services/vyos-domain-resolver
@@ -27,12 +27,14 @@ from vyos.utils.dict import dict_search_args
 from vyos.utils.process import cmd
 from vyos.utils.process import run
 from vyos.xml_ref import get_defaults
+from vyos.template import is_ip
 
 base = ['firewall']
 timeout = 300
 cache = False
 base_firewall = ['firewall']
 base_nat = ['nat']
+base_interfaces = ['interfaces']
 
 domain_state = {}
 
@@ -171,6 +173,50 @@ def update_fqdn(config, node):
 
     logger.info(f'Updated {count} sets in {node} - result: {code}')
 
+def update_interfaces(config, node):
+    if node == 'interfaces':
+        wireguard_interfaces = dict_search_args(config, 'wireguard')
+
+        # WireGuard redo handshake usually every 180 seconds, but not documented officially.
+        # If peer with domain name in its endpoint didn't get handshake for over 300 seconds,
+        # we do re-resolv and reset its endpoint from config tree.
+        handshake_threshold = 300
+
+        from vyos.ifconfig import WireGuardIf
+
+        check_wireguard_peer_public_keys = {}
+        # for each wireguard interfaces
+        for interface, wireguard in wireguard_interfaces.items():
+            check_wireguard_peer_public_keys[interface] = []
+            for peer, peer_config in wireguard['peer'].items():
+                # check peer if peer host-name or address is set
+                if 'host-name' in peer_config or 'address' in peer_config:
+                    # check latest handshake
+                    check_wireguard_peer_public_keys[interface].append(
+                        peer_config['public_key']
+                    )
+
+        now_time = time.time()
+        for (
+            interface,
+            check_peer_public_keys
+        ) in check_wireguard_peer_public_keys.items():
+            if len(check_peer_public_keys) == 0:
+                continue
+
+            intf = WireGuardIf(interface, create=False, debug=False)
+            handshakes = intf.operational.get_latest_handshakes()
+
+            for public_key, handshake_time in handshakes.items():
+                if public_key in check_peer_public_keys and (
+                    handshake_time == 0
+                    or now_time - handshake_time > handshake_threshold
+                ):
+                    intf.operational.reset_peer(public_key=public_key)
+
+                    print(f'Wireguard: reset {interface} peer {public_key}')
+
+
 if __name__ == '__main__':
     logger.info(f'VyOS domain resolver')
 
@@ -184,10 +230,12 @@ if __name__ == '__main__':
     conf = ConfigTreeQuery()
     firewall = get_config(conf, base_firewall)
     nat = get_config(conf, base_nat)
+    interfaces = get_config(conf, base_interfaces)
 
     logger.info(f'interval: {timeout}s - cache: {cache}')
 
     while True:
         update_fqdn(firewall, 'firewall')
         update_fqdn(nat, 'nat')
+        update_interfaces(interfaces, 'interfaces')
         time.sleep(timeout)
-- 
cgit v1.2.3