diff --git a/data/configd-include.json b/data/configd-include.json
index 28267d575..d25fa3de7 100644
--- a/data/configd-include.json
+++ b/data/configd-include.json
@@ -1,75 +1,76 @@
 [
 "bcast_relay.py",
 "conntrack.py",
 "conntrack_sync.py",
 "dhcp_relay.py",
 "dhcpv6_relay.py",
 "dns_forwarding.py",
 "dynamic_dns.py",
 "firewall_options.py",
 "host_name.py",
 "https.py",
 "igmp_proxy.py",
 "intel_qat.py",
 "interfaces-bonding.py",
 "interfaces-bridge.py",
 "interfaces-dummy.py",
 "interfaces-ethernet.py",
 "interfaces-geneve.py",
 "interfaces-l2tpv3.py",
 "interfaces-loopback.py",
 "interfaces-macsec.py",
 "interfaces-openvpn.py",
 "interfaces-pppoe.py",
 "interfaces-pseudo-ethernet.py",
 "interfaces-tunnel.py",
 "interfaces-vxlan.py",
 "interfaces-wireguard.py",
 "interfaces-wireless.py",
 "interfaces-wirelessmodem.py",
 "ipsec-settings.py",
 "lldp.py",
 "nat.py",
 "nat66.py",
 "ntp.py",
 "policy.py",
 "policy-local-route.py",
 "protocols_bfd.py",
 "protocols_bgp.py",
 "protocols_igmp.py",
 "protocols_isis.py",
 "protocols_mpls.py",
+"protocols_nhrp.py",
 "protocols_ospf.py",
 "protocols_ospfv3.py",
 "protocols_pim.py",
 "protocols_rip.py",
 "protocols_ripng.py",
 "protocols_static.py",
 "protocols_static_multicast.py",
 "salt-minion.py",
 "service_console-server.py",
 "service_ids_fastnetmon.py",
 "service_ipoe-server.py",
 "service_mdns-repeater.py",
 "service_pppoe-server.py",
 "service_router-advert.py",
 "ssh.py",
 "system-ip.py",
 "system-ipv6.py",
 "system-login-banner.py",
 "system-option.py",
 "system-syslog.py",
 "system-timezone.py",
 "system_console.py",
 "system_lcd.py",
 "task_scheduler.py",
 "tftp_server.py",
 "vpn_ipsec.py",
 "vpn_l2tp.py",
 "vpn_pptp.py",
 "vpn_rsa-keys.py",
 "vpn_sstp.py",
 "vrf.py",
 "vrrp.py",
 "vyos_cert.py"
 ]
diff --git a/data/templates/nhrp/opennhrp.conf.tmpl b/data/templates/nhrp/opennhrp.conf.tmpl
new file mode 100644
index 000000000..308459407
--- /dev/null
+++ b/data/templates/nhrp/opennhrp.conf.tmpl
@@ -0,0 +1,41 @@
+# Created by VyOS - manual changes will be overwritten
+
+{%  if tunnel is defined %}
+{%      for name, tunnel_conf in tunnel.items() %}
+{%          set type = 'spoke' if 'map' in tunnel_conf or 'dynamic_map' in tunnel_conf else 'hub' %}
+{%          set profile_name = profile_map[name] if profile_map is defined and name in profile_map else '' %}
+interface {{ name }} #{{ type }} {{ profile_name }}
+{%          if 'map' in tunnel_conf %}
+{%              for map, map_conf in tunnel_conf.map.items() %}
+{%                  set cisco = ' cisco' if 'cisco' in map_conf else '' %}
+{%                  set register = ' register' if 'register' in map_conf else '' %}
+    map {{ map }} {{ map_conf.nbma_address }}{{ register }}{{ cisco }}
+{%              endfor %}
+{%          endif %}
+{%          if 'dynamic_map' in tunnel_conf %}
+{%              for map, map_conf in tunnel_conf.dynamic_map.items() %}
+    dynamic-map {{ map }} {{ map_conf.nbma_domain_name }}
+{%              endfor %}
+{%          endif %}
+{%          if 'cisco_authentication' in tunnel_conf %}
+    cisco-authentication {{ tunnel_conf.cisco_authentication }}
+{%          endif %}
+{%          if 'holding_time' in tunnel_conf %}
+    holding-time {{ tunnel_conf.holding_time }}
+{%          endif %}
+{%          if 'multicast' in tunnel_conf %}
+    multicast {{ tunnel_conf.multicast }}
+{%          endif %}
+{%          for key in ['non_caching', 'redirect', 'shortcut', 'shortcut_destination'] %}
+{%              if key in tunnel_conf %}
+    {{ key | replace("_", "-") }}
+{%              endif %}
+{%          endfor %}
+{%          if 'shortcut_target' in tunnel_conf %}
+{%              for target, shortcut_conf in tunnel_conf.shortcut_target.items() %}
+    shortcut-target {{ target }} {{ shortcut_conf.holding_time if 'holding_time' in shortcut_conf else '' }}
+{%              endfor %}
+{%          endif %}
+
+{%      endfor %}
+{%  endif %}
diff --git a/debian/control b/debian/control
index 105c4dfca..bbe8200ca 100644
--- a/debian/control
+++ b/debian/control
@@ -1,168 +1,169 @@
 Source: vyos-1x
 Section: contrib/net
 Priority: extra
 Maintainer: VyOS Package Maintainers <maintainers@vyos.net>
 Build-Depends:
   debhelper (>= 9),
   fakeroot,
   gcc-multilib [amd64],
   clang [amd64],
   llvm [amd64],
   libelf-dev (>= 0.2) [amd64],
   libpcap-dev [amd64],
   build-essential,
   libvyosconfig0 (>= 0.0.7),
   libzmq3-dev,
   python3,
   python3-coverage,
   python3-lxml,
   python3-netifaces,
   python3-nose,
   python3-jinja2,
   python3-psutil,
   python3-setuptools,
   python3-sphinx,
   python3-xmltodict,
   quilt,
   whois
 Standards-Version: 3.9.6
 
 Package: vyos-1x
 Architecture: amd64 arm64
 Depends:
   accel-ppp,
   beep,
   bmon,
   bsdmainutils,
   conntrack,
   conntrackd,
   conserver-client,
   conserver-server,
   console-data,
   crda,
   cron,
   dbus,
   ddclient (>= 3.9.1),
   dropbear,
   easy-rsa,
   etherwake,
   fastnetmon,
   file,
   frr (>= 7.5),
   frr-pythontools,
   frr-rpki-rtrlib,
   frr-snmp,
   grc,
   hostapd (>= 0.6.8),
   hvinfo,
   igmpproxy,
   ipaddrcheck,
   iperf,
   iperf3,
   iputils-arping,
   isc-dhcp-client,
   isc-dhcp-relay,
   isc-dhcp-server,
   iw,
   keepalived (>=2.0.5),
   lcdproc,
   libatomic1,
   libndp-tools,
   libnetfilter-conntrack3,
   libnfnetlink0,
   libpam-radius-auth (>= 1.5.0),
   libstrongswan-standard-plugins (>=5.8),
   libstrongswan-extra-plugins (>=5.8),
   libcharon-extra-plugins (>=5.8),
   libvyosconfig0,
   lldpd,
   lm-sensors,
   lsscsi,
   mdns-repeater,
   minisign,
   mtr-tiny,
   netplug,
   nftables (>= 0.9.3),
   nginx-light,
   ntp,
   ntpdate,
   nvme-cli,
   ocserv,
   openssh-server,
   openssl,
   openvpn,
   openvpn-auth-ldap,
   openvpn-auth-radius,
   pciutils,
   pdns-recursor,
   pmacct (>= 1.6.0),
   podman,
   pppoe,
   procps,
   python3,
   python3-certbot-nginx,
   python3-crypto,
   ${python3:Depends},
   python3-flask,
   python3-hurry.filesize,
   python3-isc-dhcp-leases,
   python3-jinja2,
   python3-jmespath,
   python3-netaddr,
   python3-netifaces,
   python3-paramiko,
   python3-psutil,
   python3-pystache,
   python3-pyudev,
   python3-six,
   python3-tabulate,
   python3-vici (>= 5.7.2),
   python3-voluptuous,
   python3-waitress,
   python3-xmltodict,
   python3-zmq,
   qrencode,
   radvd,
   salt-minion,
   smartmontools,
   snmp,
   snmpd,
   squid,
   squidclient,
   squidguard,
   ssl-cert,
   strongswan (>= 5.8),
   strongswan-swanctl (>= 5.8),
   systemd,
   tcpdump,
   tcptraceroute,
   telnet,
   tftpd-hpa,
   traceroute,
   tuned,
   udp-broadcast-relay,
   usb-modeswitch,
   usbutils,
+  vyos-opennhrp,
   vyos-http-api-tools,
   vyos-utils,
   wide-dhcpv6-client,
   wireguard-tools,
   wireless-regdb,
   wpasupplicant (>= 0.6.7),
   ndppd
 Description: VyOS configuration scripts and data
  VyOS configuration scripts, interface definitions, and everything
 
 Package: vyos-1x-vmware
 Architecture: amd64
 Depends:
  vyos-1x,
  open-vm-tools
 Description: VyOS configuration scripts and data for VMware
  Adds configuration files required for VyOS running on VMware hosts.
 
 Package: vyos-1x-smoketest
 Architecture: all
 Depends:
  vyos-1x
 Description: VyOS build sanity checking toolkit
diff --git a/debian/vyos-1x.install b/debian/vyos-1x.install
index e5de7f074..0c6c226ee 100644
--- a/debian/vyos-1x.install
+++ b/debian/vyos-1x.install
@@ -1,27 +1,28 @@
 etc/dhcp
 etc/ipsec.d
 etc/netplug
+etc/opennhrp
 etc/ppp
 etc/rsyslog.d
 etc/systemd
 etc/sysctl.d
 etc/udev
 etc/vyos
 lib/
 opt/
 usr/sbin
 usr/bin/initial-setup
 usr/bin/vyos-config-file-query
 usr/bin/vyos-config-to-commands
 usr/bin/vyos-config-to-json
 usr/bin/vyos-hostsd-client
 usr/lib
 usr/libexec/vyos/completion
 usr/libexec/vyos/conf_mode
 usr/libexec/vyos/op_mode
 usr/libexec/vyos/services
 usr/libexec/vyos/system
 usr/libexec/vyos/validators
 usr/libexec/vyos/*.py
 usr/libexec/vyos/*.sh
 usr/share
diff --git a/interface-definitions/protocols-nhrp.xml.in b/interface-definitions/protocols-nhrp.xml.in
new file mode 100644
index 000000000..9dd9d3389
--- /dev/null
+++ b/interface-definitions/protocols-nhrp.xml.in
@@ -0,0 +1,134 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interfaceDefinition>
+  <node name="protocols">
+    <children>
+      <node name="nhrp" owner="${vyos_conf_scripts_dir}/protocols_nhrp.py">
+        <properties>
+          <help>NHRP parameters</help>
+          <priority>680</priority>
+        </properties>
+        <children>
+          <tagNode name="tunnel">
+            <properties>
+              <help>Tunnel for NHRP [REQUIRED]</help>
+              <constraint>
+                <regex>^tun[0-9]+$</regex>
+              </constraint>
+              <valueHelp>
+                <format>tunN</format>
+                <description>NHRP tunnel name</description>
+              </valueHelp>
+            </properties>
+            <children>
+              <leafNode name="cisco-authentication">
+                <properties>
+                  <help>Pass phrase for cisco authentication</help>
+                  <valueHelp>
+                    <format>txt</format>
+                    <description>Pass phrase for cisco authentication</description>
+                  </valueHelp>
+                </properties>
+              </leafNode>
+              <tagNode name="dynamic-map">
+                <properties>
+                  <help>Set an HUB tunnel address</help>
+                  <valueHelp>
+                    <format>ipv4net</format>
+                    <description>Set the IP address and prefix length</description>
+                  </valueHelp>
+                </properties>
+                <children>
+                  <leafNode name="nbma-domain-name">
+                    <properties>
+                      <help>Set HUB fqdn (nbma-address - fqdn) [REQUIRED]</help>
+                      <valueHelp>
+                        <format>&lt;fqdn&gt;</format>
+                        <description>Set the external HUB fqdn</description>
+                      </valueHelp>
+                    </properties>
+                  </leafNode>
+                </children>
+              </tagNode>
+              <leafNode name="holding-time">
+                <properties>
+                  <help>Holding time in seconds</help>
+                </properties>
+              </leafNode>
+              <tagNode name="map">
+                <properties>
+                  <help>Set an HUB tunnel address</help>
+                </properties>
+                <children>
+                  <leafNode name="cisco">
+                    <properties>
+                      <help>If the statically mapped peer is running Cisco IOS, specify this</help>
+                      <valueless/>
+                    </properties>
+                  </leafNode>
+                  <leafNode name="nbma-address">
+                    <properties>
+                      <help>Set HUB address (nbma-address - external hub address or fqdn) [REQUIRED]</help>
+                    </properties>
+                  </leafNode>
+                  <leafNode name="register">
+                    <properties>
+                      <help>Specifies that Registration Request should be sent to this peer on startup</help>
+                      <valueless/>
+                    </properties>
+                  </leafNode>
+                </children>
+              </tagNode>
+              <leafNode name="multicast">
+                <properties>
+                  <help>Set multicast for NHRP</help>
+                  <completionHelp>
+                    <list>dynamic nhs</list>
+                  </completionHelp>
+                  <constraint>
+                    <regex>^(dynamic|nhs)$</regex>
+                  </constraint>
+                </properties>
+              </leafNode>
+              <leafNode name="non-caching">
+                <properties>
+                  <help>This can be used to reduce memory consumption on big NBMA subnets</help>
+                  <valueless/>
+                </properties>
+              </leafNode>
+              <leafNode name="redirect">
+                <properties>
+                  <help>Enable sending of Cisco style NHRP Traffic Indication packets</help>
+                  <valueless/>
+                </properties>
+              </leafNode>
+              <leafNode name="shortcut-destination">
+                <properties>
+                  <help>This instructs opennhrp to reply with authorative answers on NHRP Resolution Requests destined to addresses in this interface</help>
+                  <valueless/>
+                </properties>
+              </leafNode>
+              <tagNode name="shortcut-target">
+                <properties>
+                  <help>Defines an off-NBMA network prefix for which the GRE interface will act as a gateway</help>
+                </properties>
+                <children>
+                  <leafNode name="holding-time">
+                    <properties>
+                      <help>Holding time in seconds</help>
+                    </properties>
+                  </leafNode>
+                </children>
+              </tagNode>
+              <leafNode name="shortcut">
+                <properties>
+                  <help>Enable creation of shortcut routes. A received NHRP Traffic Indication will trigger the resolution and establishment of a shortcut route</help>
+                  <valueless/>
+                </properties>
+              </leafNode>
+            </children>
+          </tagNode>
+        </children>
+      </node>
+    </children>
+  </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/nhrp.xml.in b/op-mode-definitions/nhrp.xml.in
new file mode 100644
index 000000000..9e746cc35
--- /dev/null
+++ b/op-mode-definitions/nhrp.xml.in
@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interfaceDefinition>
+  <node name="reset">
+    <children>
+      <node name="nhrp">
+        <properties>
+          <help>Clear/Purge NHRP entries</help>
+        </properties>
+        <children>
+          <node name="flush">
+            <properties>
+              <help>Clear all non-permanent entries</help>
+            </properties>
+            <children>
+              <tagNode name="tunnel">
+                <properties>
+                  <help>Clear all non-permanent entries</help>
+                </properties>
+                <command>sudo opennhrpctl flush dev $5 || echo OpenNHRP is not running.</command>
+              </tagNode>
+            </children>
+            <command>sudo opennhrpctl flush || echo OpenNHRP is not running.</command>
+          </node>
+          <node name="purge">
+            <properties>
+              <help>Purge entries from NHRP cache</help>
+            </properties>
+            <children>
+              <tagNode name="tunnel">
+                <properties>
+                  <help>Purge all entries from NHRP cache</help>
+                </properties>
+                <command>sudo opennhrpctl purge dev $5 || echo OpenNHRP is not running.</command>
+              </tagNode>
+            </children>
+            <command>sudo opennhrpctl purge || echo OpenNHRP is not running.</command>
+          </node>
+        </children>
+      </node>
+    </children>
+  </node>
+  <node name="show">
+    <children>
+      <node name="nhrp">
+        <properties>
+          <help>Show NHRP info</help>
+        </properties>
+        <children>
+          <leafNode name="interface">
+            <properties>
+              <help>Show NHRP interface connection information</help>
+            </properties>
+            <command>if [ -f /var/run/opennhrp.pid ]; then sudo opennhrpctl interface show; else echo OpenNHRP is not running.; fi</command>
+          </leafNode>
+          <leafNode name="tunnel">
+            <properties>
+              <help>Show NHRP tunnel connection information</help>
+            </properties>
+            <command>if [ -f /var/run/opennhrp.pid ]; then sudo opennhrpctl show ; else echo OpenNHRP is not running.; fi</command>
+          </leafNode>
+        </children>
+      </node>
+    </children>
+  </node>
+</interfaceDefinition>
diff --git a/smoketest/scripts/cli/test_protocols_nhrp.py b/smoketest/scripts/cli/test_protocols_nhrp.py
new file mode 100755
index 000000000..8389e42e9
--- /dev/null
+++ b/smoketest/scripts/cli/test_protocols_nhrp.py
@@ -0,0 +1,97 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021 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 unittest
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+
+from vyos.util import call, process_named_running, read_file
+
+tunnel_path = ['interfaces', 'tunnel']
+nhrp_path = ['protocols', 'nhrp']
+vpn_path = ['vpn', 'ipsec']
+
+class TestProtocolsNHRP(VyOSUnitTestSHIM.TestCase):
+    def tearDown(self):
+        self.cli_delete(nhrp_path)
+        self.cli_delete(tunnel_path)
+        self.cli_commit()
+
+    def test_config(self):
+        self.cli_delete(nhrp_path)
+        self.cli_delete(tunnel_path)
+
+        # Tunnel
+        self.cli_set(tunnel_path + ["tun100", "address", "172.16.253.134/29"])
+        self.cli_set(tunnel_path + ["tun100", "encapsulation", "gre"])
+        self.cli_set(tunnel_path + ["tun100", "source-address", "192.0.2.1"])
+        self.cli_set(tunnel_path + ["tun100", "multicast", "enable"])
+        self.cli_set(tunnel_path + ["tun100", "parameters", "ip", "key", "1"])
+
+        # NHRP
+        self.cli_set(nhrp_path + ["tunnel", "tun100", "cisco-authentication", "secret"])
+        self.cli_set(nhrp_path + ["tunnel", "tun100", "holding-time", "300"])
+        self.cli_set(nhrp_path + ["tunnel", "tun100", "multicast", "dynamic"])
+        self.cli_set(nhrp_path + ["tunnel", "tun100", "redirect"])
+        self.cli_set(nhrp_path + ["tunnel", "tun100", "shortcut"])
+
+        # IKE/ESP Groups
+        self.cli_set(vpn_path + ["esp-group", "ESP-HUB", "compression", "disable"])
+        self.cli_set(vpn_path + ["esp-group", "ESP-HUB", "lifetime", "1800"])
+        self.cli_set(vpn_path + ["esp-group", "ESP-HUB", "mode", "transport"])
+        self.cli_set(vpn_path + ["esp-group", "ESP-HUB", "pfs", "dh-group2"])
+        self.cli_set(vpn_path + ["esp-group", "ESP-HUB", "proposal", "1", "encryption", "aes256"])
+        self.cli_set(vpn_path + ["esp-group", "ESP-HUB", "proposal", "1", "hash", "sha1"])
+        self.cli_set(vpn_path + ["esp-group", "ESP-HUB", "proposal", "2", "encryption", "3des"])
+        self.cli_set(vpn_path + ["esp-group", "ESP-HUB", "proposal", "2", "hash", "md5"])
+        self.cli_set(vpn_path + ["ike-group", "IKE-HUB", "ikev2-reauth", "no"])
+        self.cli_set(vpn_path + ["ike-group", "IKE-HUB", "key-exchange", "ikev1"])
+        self.cli_set(vpn_path + ["ike-group", "IKE-HUB", "lifetime", "3600"])
+        self.cli_set(vpn_path + ["ike-group", "IKE-HUB", "proposal", "1", "dh-group", "2"])
+        self.cli_set(vpn_path + ["ike-group", "IKE-HUB", "proposal", "1", "encryption", "aes256"])
+        self.cli_set(vpn_path + ["ike-group", "IKE-HUB", "proposal", "1", "hash", "sha1"])
+        self.cli_set(vpn_path + ["ike-group", "IKE-HUB", "proposal", "2", "dh-group", "2"])
+        self.cli_set(vpn_path + ["ike-group", "IKE-HUB", "proposal", "2", "encryption", "aes128"])
+        self.cli_set(vpn_path + ["ike-group", "IKE-HUB", "proposal", "2", "hash", "sha1"])
+
+        # Profile - Not doing full DMVPN checks here, just want to verify the profile name in the output
+        self.cli_set(vpn_path + ["ipsec-interfaces", "interface", "eth0"])
+        self.cli_set(vpn_path + ["profile", "NHRPVPN", "authentication", "mode", "pre-shared-secret"])
+        self.cli_set(vpn_path + ["profile", "NHRPVPN", "authentication", "pre-shared-secret", "secret"])
+        self.cli_set(vpn_path + ["profile", "NHRPVPN", "bind", "tunnel", "tun100"])
+        self.cli_set(vpn_path + ["profile", "NHRPVPN", "esp-group", "ESP-HUB"])
+        self.cli_set(vpn_path + ["profile", "NHRPVPN", "ike-group", "IKE-HUB"])
+
+        self.cli_commit()
+
+        opennhrp_lines = [
+            'interface tun100 #hub NHRPVPN',
+            'cisco-authentication secret',
+            'holding-time 300',
+            'shortcut',
+            'multicast dynamic',
+            'redirect'
+        ]
+
+        tmp_opennhrp_conf = read_file('/run/opennhrp/opennhrp.conf')
+
+        for line in opennhrp_lines:
+            self.assertIn(line, tmp_opennhrp_conf)
+
+        self.assertTrue(process_named_running('opennhrp'))
+
+if __name__ == '__main__':
+    unittest.main(verbosity=2)
diff --git a/src/conf_mode/protocols_nhrp.py b/src/conf_mode/protocols_nhrp.py
new file mode 100755
index 000000000..12dacdba0
--- /dev/null
+++ b/src/conf_mode/protocols_nhrp.py
@@ -0,0 +1,122 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021 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/>.
+
+from vyos.config import Config
+from vyos.configdict import node_changed
+from vyos.template import render
+from vyos.util import process_named_running
+from vyos.util import run
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+opennhrp_conf = '/run/opennhrp/opennhrp.conf'
+
+def get_config(config=None):
+    if config:
+        conf = config
+    else:
+        conf = Config()
+    base = ['protocols', 'nhrp']
+
+    nhrp = conf.get_config_dict(base, key_mangling=('-', '_'),
+                                get_first_key=True, no_tag_node_value_mangle=True)
+    nhrp['del_tunnels'] = node_changed(conf, base + ['tunnel'], key_mangling=('-', '_'))
+
+    if not conf.exists(base):
+        return nhrp
+
+    nhrp['if_tunnel'] = conf.get_config_dict(['interfaces', 'tunnel'], key_mangling=('-', '_'),
+                                get_first_key=True, no_tag_node_value_mangle=True)
+
+    nhrp['profile_map'] = {}
+    profile = conf.get_config_dict(['vpn', 'ipsec', 'profile'], key_mangling=('-', '_'),
+                                get_first_key=True, no_tag_node_value_mangle=True)
+
+    for name, profile_conf in profile.items():
+        if 'bind' in profile_conf and 'tunnel' in profile_conf['bind']:
+            interfaces = profile_conf['bind']['tunnel']
+            if isinstance(interfaces, str):
+                interfaces = [interfaces]
+            for interface in interfaces:
+                nhrp['profile_map'][interface] = name
+
+    return nhrp
+
+def verify(nhrp):
+    if 'tunnel' in nhrp:
+        for name, nhrp_conf in nhrp['tunnel'].items():
+            if not nhrp['if_tunnel'] or name not in nhrp['if_tunnel']:
+                raise ConfigError(f'Tunnel interface "{name}" does not exist')
+
+            tunnel_conf = nhrp['if_tunnel'][name]
+
+            if 'encapsulation' not in tunnel_conf or tunnel_conf['encapsulation'] != 'gre':
+                raise ConfigError(f'Tunnel "{name}" is not an mGRE tunnel')
+
+            if 'remote' in tunnel_conf:
+                raise ConfigError(f'Tunnel "{name}" cannot have a remote address defined')
+
+            if 'map' in nhrp_conf:
+                for map_name, map_conf in nhrp_conf['map'].items():
+                    if 'nbma_address' not in map_conf:
+                        raise ConfigError(f'nbma-address missing on map {map_name} on tunnel {name}')
+
+            if 'dynamic_map' in nhrp_conf:
+                for map_name, map_conf in nhrp_conf['dynamic_map'].items():
+                    if 'nbma_domain_name' not in map_conf:
+                        raise ConfigError(f'nbma-domain-name missing on dynamic-map {map_name} on tunnel {name}')
+    return None
+
+def generate(nhrp):
+    render(opennhrp_conf, 'nhrp/opennhrp.conf.tmpl', nhrp)
+    return None
+
+def apply(nhrp):
+    if 'tunnel' in nhrp:
+        for tunnel, tunnel_conf in nhrp['tunnel'].items():
+            if 'source_address' in tunnel_conf:
+                chain = f'VYOS_NHRP_{tunnel}_OUT_HOOK'
+                source_address = tunnel_conf['source_address']
+
+                chain_exists = run(f'sudo iptables --check {chain} -j RETURN') == 0
+                if not chain_exists:
+                    run(f'sudo iptables --new {chain}')
+                    run(f'sudo iptables --append {chain} -p gre -s {source_address} -d 224.0.0.0/4 -j DROP')
+                    run(f'sudo iptables --append {chain} -j RETURN')
+                    run(f'sudo iptables --insert OUTPUT 2 -j {chain}')
+
+    for tunnel in nhrp['del_tunnels']:
+        chain = f'VYOS_NHRP_{tunnel}_OUT_HOOK'
+        chain_exists = run(f'sudo iptables --check {chain} -j RETURN') == 0
+        if chain_exists:
+            run(f'sudo iptables --delete OUTPUT -j {chain}')
+            run(f'sudo iptables --flush {chain}')
+            run(f'sudo iptables --delete-chain {chain}')
+
+    action = 'restart' if nhrp and 'tunnel' in nhrp else 'stop'
+    run(f'systemctl {action} opennhrp')
+    return None
+
+if __name__ == '__main__':
+    try:
+        c = get_config()
+        verify(c)
+        generate(c)
+        apply(c)
+    except ConfigError as e:
+        print(e)
+        exit(1)
diff --git a/src/conf_mode/vpn_ipsec.py b/src/conf_mode/vpn_ipsec.py
index c57697a8f..eedb9098c 100755
--- a/src/conf_mode/vpn_ipsec.py
+++ b/src/conf_mode/vpn_ipsec.py
@@ -1,397 +1,399 @@
 #!/usr/bin/env python3
 #
 # Copyright (C) 2021 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 sys import exit
 from time import sleep
 
 from vyos.config import Config
 from vyos.configdict import leaf_node_changed
 from vyos.configverify import verify_interface_exists
 from vyos.ifconfig import Interface
 from vyos.template import render
 from vyos.util import call
 from vyos.util import dict_search
 from vyos.util import get_interface_address
 from vyos.util import process_named_running
 from vyos.util import run
 from vyos.util import cidr_fit
 from vyos import ConfigError
 from vyos import airbag
 airbag.enable()
 
 authby_translate = {
     'pre-shared-secret': 'secret',
     'rsa': 'rsasig',
     'x509': 'rsasig'
 }
 default_pfs = 'dh-group2'
 pfs_translate = {
     'dh-group1': 'modp768',
     'dh-group2': 'modp1024',
     'dh-group5': 'modp1536',
     'dh-group14': 'modp2048',
     'dh-group15': 'modp3072',
     'dh-group16': 'modp4096',
     'dh-group17': 'modp6144',
     'dh-group18': 'modp8192',
     'dh-group19': 'ecp256',
     'dh-group20': 'ecp384',
     'dh-group21': 'ecp512',
     'dh-group22': 'modp1024s160',
     'dh-group23': 'modp2048s224',
     'dh-group24': 'modp2048s256',
     'dh-group25': 'ecp192',
     'dh-group26': 'ecp224',
     'dh-group27': 'ecp224bp',
     'dh-group28': 'ecp256bp',
     'dh-group29': 'ecp384bp',
     'dh-group30': 'ecp512bp',
     'dh-group31': 'curve25519',
     'dh-group32': 'curve448'
 }
 
 any_log_modes = [
     'dmn', 'mgr', 'ike', 'chd','job', 'cfg', 'knl', 'net', 'asn',
     'enc', 'lib', 'esp', 'tls', 'tnc', 'imc', 'imv', 'pts'
 ]
 
 ike_ciphers = {}
 esp_ciphers = {}
 
 mark_base = 0x900000
 
 CA_PATH = "/etc/ipsec.d/cacerts/"
 CRL_PATH = "/etc/ipsec.d/crls/"
 
 DHCP_BASE = "/var/lib/dhcp/dhclient"
 
 LOCAL_KEY_PATHS = ['/config/auth/', '/config/ipsec.d/rsa-keys/']
 X509_PATH = '/config/auth/'
 
 def get_config(config=None):
     if config:
         conf = config
     else:
         conf = Config()
     base = ['vpn', 'ipsec']
     if not conf.exists(base):
         return None
 
     # retrieve common dictionary keys
     ipsec = conf.get_config_dict(base, key_mangling=('-', '_'),
                                  get_first_key=True, no_tag_node_value_mangle=True)
 
     ipsec['interface_change'] = leaf_node_changed(conf, base + ['ipsec-interfaces', 'interface'])
     ipsec['l2tp_exists'] = conf.exists('vpn l2tp remote-access ipsec-settings ')
     ipsec['nhrp_exists'] = conf.exists('protocols nhrp tunnel')
     ipsec['rsa_keys'] = conf.get_config_dict(['vpn', 'rsa-keys'], key_mangling=('-', '_'),
                                              get_first_key=True, no_tag_node_value_mangle=True)
 
     default_ike_pfs = None
 
     if 'ike_group' in ipsec:
         for group, ike_conf in ipsec['ike_group'].items():
             if 'proposal' in ike_conf:
                 ciphers = []
                 for i in ike_conf['proposal']:
                     proposal = ike_conf['proposal'][i]
                     enc = proposal['encryption'] if 'encryption' in proposal else None
                     hash = proposal['hash'] if 'hash' in proposal else None
                     pfs = ('dh-group' + proposal['dh_group']) if 'dh_group' in proposal else default_pfs
 
                     if not default_ike_pfs:
                         default_ike_pfs = pfs
 
                     if enc and hash:
                         ciphers.append(f"{enc}-{hash}-{pfs_translate[pfs]}" if pfs else f"{enc}-{hash}")
                 ike_ciphers[group] = ','.join(ciphers) + '!'
 
     if 'esp_group' in ipsec:
         for group, esp_conf in ipsec['esp_group'].items():
             pfs = esp_conf['pfs'] if 'pfs' in esp_conf else 'enable'
 
             if pfs == 'disable':
                 pfs = None
 
             if pfs == 'enable':
                 pfs = default_ike_pfs
 
             if 'proposal' in esp_conf:
                 ciphers = []
                 for i in esp_conf['proposal']:
                     proposal = esp_conf['proposal'][i]
                     enc = proposal['encryption'] if 'encryption' in proposal else None
                     hash = proposal['hash'] if 'hash' in proposal else None
                     if enc and hash:
                         ciphers.append(f"{enc}-{hash}-{pfs_translate[pfs]}" if pfs else f"{enc}-{hash}")
                 esp_ciphers[group] = ','.join(ciphers) + '!'
 
     return ipsec
 
 def get_rsa_local_key(ipsec):
     return dict_search('local_key.file', ipsec['rsa_keys'])
 
 def verify_rsa_local_key(ipsec):
     file = get_rsa_local_key(ipsec)
 
     if not file:
         return False
 
     for path in LOCAL_KEY_PATHS:
         full_path = os.path.join(path, file)
         if os.path.exists(full_path):
             return full_path
 
     return False
 
 def verify_rsa_key(ipsec, key_name):
     return dict_search(f'rsa_key_name.{key_name}.rsa_key', ipsec['rsa_keys'])
 
 def verify(ipsec):
     if not ipsec:
         return None
 
     if 'ipsec_interfaces' in ipsec and 'interface' in ipsec['ipsec_interfaces']:
         interfaces = ipsec['ipsec_interfaces']['interface']
         if isinstance(interfaces, str):
             interfaces = [interfaces]
 
         for ifname in interfaces:
             verify_interface_exists(ifname)
 
     if 'profile' in ipsec:
         for profile, profile_conf in ipsec['profile'].items():
             if 'esp_group' in profile_conf:
                 if 'esp_group' not in ipsec or profile_conf['esp_group'] not in ipsec['esp_group']:
                     raise ConfigError(f"Invalid esp-group on {profile} profile")
             else:
                 raise ConfigError(f"Missing esp-group on {profile} profile")
 
             if 'ike_group' in profile_conf:
                 if 'ike_group' not in ipsec or profile_conf['ike_group'] not in ipsec['ike_group']:
                     raise ConfigError(f"Invalid ike-group on {profile} profile")
             else:
                 raise ConfigError(f"Missing ike-group on {profile} profile")
 
             if 'authentication' not in profile_conf:
                 raise ConfigError(f"Missing authentication on {profile} profile")
 
     if 'site_to_site' in ipsec and 'peer' in ipsec['site_to_site']:
         for peer, peer_conf in ipsec['site_to_site']['peer'].items():
             has_default_esp = False
             if 'default_esp_group' in peer_conf:
                 has_default_esp = True
                 if 'esp_group' not in ipsec or peer_conf['default_esp_group'] not in ipsec['esp_group']:
                     raise ConfigError(f"Invalid esp-group on site-to-site peer {peer}")
 
             if 'ike_group' in peer_conf:
                 if 'ike_group' not in ipsec or peer_conf['ike_group'] not in ipsec['ike_group']:
                     raise ConfigError(f"Invalid ike-group on site-to-site peer {peer}")
             else:
                 raise ConfigError(f"Missing ike-group on site-to-site peer {peer}")
 
             if 'authentication' not in peer_conf or 'mode' not in peer_conf['authentication']:
                 raise ConfigError(f"Missing authentication on site-to-site peer {peer}")
 
             if peer_conf['authentication']['mode'] == 'x509':
                 if 'x509' not in peer_conf['authentication']:
                     raise ConfigError(f"Missing x509 settings on site-to-site peer {peer}")
 
                 if 'key' not in peer_conf['authentication']['x509']:
                     raise ConfigError(f"Missing x509 key on site-to-site peer {peer}")
 
                 if 'ca_cert_file' not in peer_conf['authentication']['x509'] or 'cert_file' not in peer_conf['authentication']['x509']:
                     raise ConfigError(f"Missing x509 settings on site-to-site peer {peer}")
 
                 if 'file' not in peer_conf['authentication']['x509']['key']:
                     raise ConfigError(f"Missing x509 key file on site-to-site peer {peer}")
 
                 for key in ['ca_cert_file', 'cert_file', 'crl_file']:
                     if key in peer_conf['authentication']['x509']:
                         path = os.path.join(X509_PATH, peer_conf['authentication']['x509'][key])
                         if not os.path.exists(path):
                             raise ConfigError(f"File not found for {key} on site-to-site peer {peer}")
 
                 key_path = os.path.join(X509_PATH, peer_conf['authentication']['x509']['key']['file'])
                 if not os.path.exists(key_path):
                     raise ConfigError(f"Private key not found on site-to-site peer {peer}")
 
             if peer_conf['authentication']['mode'] == 'rsa':
                 if not verify_rsa_local_key(ipsec):
                     raise ConfigError(f"Invalid key on rsa-keys local-key")
 
                 if 'rsa_key_name' not in peer_conf['authentication']:
                     raise ConfigError(f"Missing rsa-key-name on site-to-site peer {peer}")
 
                 if not verify_rsa_key(ipsec, peer_conf['authentication']['rsa_key_name']):
                     raise ConfigError(f"Invalid rsa-key-name on site-to-site peer {peer}")
 
             if 'local_address' not in peer_conf and 'dhcp_interface' not in peer_conf:
                 raise ConfigError(f"Missing local-address or dhcp-interface on site-to-site peer {peer}")
 
             if 'dhcp_interface' in peer_conf:
                 dhcp_interface = peer_conf['dhcp_interface']
 
                 verify_interface_exists(dhcp_interface)
 
                 if not os.path.exists(f'{DHCP_BASE}_{dhcp_interface}.conf'):
                     raise ConfigError(f"Invalid dhcp-interface on site-to-site peer {peer}")
 
                 address = Interface(dhcp_interface).get_addr()
                 if not address:
                     raise ConfigError(f"Failed to get address from dhcp-interface on site-to-site peer {peer}")
 
             if 'vti' in peer_conf:
                 if 'local_address' in peer_conf and 'dhcp_interface' in peer_conf:
                     raise ConfigError(f"A single local-address or dhcp-interface is required when using VTI on site-to-site peer {peer}")
 
                 if 'bind' in peer_conf['vti']:
                     vti_interface = peer_conf['vti']['bind']
                     if not os.path.exists(f'/sys/class/net/{vti_interface}'):
                         raise ConfigError(f'VTI interface {vti_interface} for site-to-site peer {peer} does not exist!')
 
             if 'vti' not in peer_conf and 'tunnel' not in peer_conf:
                 raise ConfigError(f"No VTI or tunnel specified on site-to-site peer {peer}")
 
             if 'tunnel' in peer_conf:
                 for tunnel, tunnel_conf in peer_conf['tunnel'].items():
                     if 'esp_group' not in tunnel_conf and not has_default_esp:
                         raise ConfigError(f"Missing esp-group on tunnel {tunnel} for site-to-site peer {peer}")
 
                     esp_group_name = tunnel_conf['esp_group'] if 'esp_group' in tunnel_conf else peer_conf['default_esp_group']
 
                     if esp_group_name not in ipsec['esp_group']:
                         raise ConfigError(f"Invalid esp-group on tunnel {tunnel} for site-to-site peer {peer}")
 
                     esp_group = ipsec['esp_group'][esp_group_name]
 
                     if 'mode' in esp_group and esp_group['mode'] == 'transport':
                         if 'protocol' in tunnel_conf and ((peer in ['any', '0.0.0.0']) or ('local_address' not in peer_conf or peer_conf['local_address'] in ['any', '0.0.0.0'])):
                             raise ConfigError(f"Fixed local-address or peer required when a protocol is defined with ESP transport mode on tunnel {tunnel} for site-to-site peer {peer}")
 
                         if ('local' in tunnel_conf and 'prefix' in tunnel_conf['local']) or ('remote' in tunnel_conf and 'prefix' in tunnel_conf['remote']):
                             raise ConfigError(f"Local/remote prefix cannot be used with ESP transport mode on tunnel {tunnel} for site-to-site peer {peer}")
 
 def generate(ipsec):
     data = {}
 
     if ipsec:
         data = ipsec
         data['authby'] = authby_translate
         data['ciphers'] = {'ike': ike_ciphers, 'esp': esp_ciphers}
         data['marks'] = {}
         data['rsa_local_key'] = verify_rsa_local_key(ipsec)
         data['x509_path'] = X509_PATH
 
         if 'site_to_site' in data and 'peer' in data['site_to_site']:
             for peer, peer_conf in ipsec['site_to_site']['peer'].items():
                 if peer_conf['authentication']['mode'] == 'x509':
                     ca_cert_file = os.path.join(X509_PATH, peer_conf['authentication']['x509']['ca_cert_file'])
                     call(f'cp -f {ca_cert_file} {CA_PATH}')
 
                     if 'crl_file' in peer_conf['authentication']['x509']:
                         crl_file = os.path.join(X509_PATH, peer_conf['authentication']['x509']['crl_file'])
                         call(f'cp -f {crl_file} {CRL_PATH}')
 
                 local_ip = ''
                 if 'local_address' in peer_conf:
                     local_ip = peer_conf['local_address']
                 elif 'dhcp_interface' in peer_conf:
                     local_ip = Interface(peer_conf['dhcp_interface']).get_addr()
 
                 data['site_to_site']['peer'][peer]['local_address'] = local_ip
 
                 if 'vti' in peer_conf and 'bind' in peer_conf['vti']:
                     vti_interface = peer_conf['vti']['bind']
                     data['marks'][vti_interface] = get_mark(vti_interface)
                 else:
                     for tunnel, tunnel_conf in peer_conf['tunnel'].items():
                         local_prefix = dict_search('local.prefix', tunnel_conf['local']['prefix'])
                         remote_prefix = dict_search('remote.prefix', tunnel_conf['remote']['prefix'])
 
                         if not local_prefix or not remote_prefix:
                             continue
 
                         passthrough = cidr_fit(local_prefix, remote_prefix)
                         data['site_to_site']['peer'][peer]['tunnel'][tunnel]['passthrough'] = passthrough
 
         if 'logging' in ipsec and 'log_modes' in ipsec['logging']:
             modes = ipsec['logging']['log_modes']
             level = ipsec['logging']['log_level'] if 'log_level' in ipsec['logging'] else '1'
             if isinstance(modes, str):
                 modes = [modes]
             if 'any' in modes:
                 modes = any_log_modes
             data['charondebug'] = f' {level}, '.join(modes) + ' ' + level
 
     render("/etc/ipsec.conf", "ipsec/ipsec.conf.tmpl", data)
     render("/etc/ipsec.secrets", "ipsec/ipsec.secrets.tmpl", data)
     render("/etc/strongswan.d/interfaces_use.conf", "ipsec/interfaces_use.conf.tmpl", data)
     render("/etc/swanctl/swanctl.conf", "ipsec/swanctl.conf.tmpl", data)
 
 def resync_l2tp(ipsec):
     if ipsec and not ipsec['l2tp_exists']:
         return
 
     tmp = run('/usr/libexec/vyos/conf_mode/ipsec-settings.py')
     if tmp > 0:
         print('ERROR: failed to reapply L2TP IPSec settings!')
 
 def resync_nhrp(ipsec):
     if ipsec and not ipsec['nhrp_exists']:
         return
 
-    run('/opt/vyatta/sbin/vyos-update-nhrp.pl --set_ipsec')
+    tmp = run('/usr/libexec/vyos/conf_mode/protocols_nhrp.py')
+    if tmp > 0:
+        print('ERROR: failed to reapply NHRP settings!')
 
 def apply(ipsec):
     if not ipsec:
         call('sudo /usr/sbin/ipsec stop')
     else:
         should_start = ('profile' in ipsec or dict_search('site_to_site.peer', ipsec))
 
         if not process_named_running('charon') and should_start:
             args = f'--auto-update {ipsec["auto_update"]}' if 'auto_update' in ipsec else ''
             call(f'sudo /usr/sbin/ipsec start {args}')
         elif not should_start:
             call('sudo /usr/sbin/ipsec stop')
         elif ipsec['interface_change']:
             call('sudo /usr/sbin/ipsec restart')
         else:
             call('sudo /usr/sbin/ipsec rereadall')
             call('sudo /usr/sbin/ipsec reload')
 
         if should_start:
             sleep(2) # Give charon enough time to start
             call('sudo /usr/sbin/swanctl -q')
 
     resync_l2tp(ipsec)
     resync_nhrp(ipsec)
 
 def get_mark(vti_interface):
     vti_num = int(vti_interface.lstrip('vti'))
     return mark_base + vti_num
 
 if __name__ == '__main__':
     try:
         ipsec = get_config()
         verify(ipsec)
         generate(ipsec)
         apply(ipsec)
     except ConfigError as e:
         print(e)
         exit(1)
diff --git a/src/etc/opennhrp/opennhrp-script.py b/src/etc/opennhrp/opennhrp-script.py
new file mode 100755
index 000000000..74c45f2f6
--- /dev/null
+++ b/src/etc/opennhrp/opennhrp-script.py
@@ -0,0 +1,136 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021 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/>.
+
+from pprint import pprint
+import os
+import re
+import sys
+import vici
+
+from vyos.util import cmd
+from vyos.util import process_named_running
+
+NHRP_CONFIG="/etc/opennhrp/opennhrp.conf"
+
+def parse_type_ipsec(interface):
+    with open(NHRP_CONFIG, 'r') as f:
+        lines = f.readlines()
+        match = rf'^interface {interface} #(hub|spoke)(?:\s([\w-]+))?$'
+        for line in lines:
+            m = re.match(match, line)
+            if m:
+                return m[1], m[2]
+    return None, None
+
+def vici_initiate(conn, child_sa, src_addr, dest_addr):
+    try:
+        session = vici.Session()
+        logs = session.initiate({
+            'ike': conn,
+            'child': child_sa,
+            'timeout': '-1',
+            'my-host': src_addr,
+            'other-host': dest_addr
+        })
+        for log in logs:
+            message = log['msg'].decode('ascii')
+            print('INIT LOG:', message)
+        return True
+    except:
+        return None
+
+def vici_terminate(conn, child_sa, src_addr, dest_addr):
+    try:
+        session = vici.Session()
+        logs = session.terminate({
+            'ike': conn,
+            'child': child_sa,
+            'timeout': '-1',
+            'my-host': src_addr,
+            'other-host': dest_addr
+        })
+        for log in logs:
+            message = log['msg'].decode('ascii')
+            print('TERM LOG:', message)
+        return True
+    except:
+        return None
+
+def iface_up(interface):
+    cmd(f'sudo ip route flush proto 42 dev {interface}')
+    cmd(f'sudo ip neigh flush dev {interface}')
+
+def peer_up(dmvpn_type, conn):
+    src_addr = os.getenv('NHRP_SRCADDR')
+    src_nbma = os.getenv('NHRP_SRCNBMA')
+    dest_addr = os.getenv('NHRP_DESTADDR')
+    dest_nbma = os.getenv('NHRP_DESTNBMA')
+    dest_mtu = os.getenv('NHRP_DESTMTU')
+
+    if dest_mtu:
+        args = cmd(f'sudo ip route get {dest_nbma} from {src_nbma}')
+        cmd(f'sudo ip route add {args} proto 42 mtu {dest_mtu}')
+
+    if conn and dmvpn_type == 'spoke' and process_named_running('charon'):
+        vici_terminate(conn, 'dmvpn', src_nbma, dest_nbma)
+        vici_initiate(conn, 'dmvpn', src_nbma, dest_nbma)
+
+def peer_down(dmvpn_type, conn):
+    src_nbma = os.getenv('NHRP_SRCNBMA')
+    dest_nbma = os.getenv('NHRP_DESTNBMA')
+
+    if conn and dmvpn_type == 'spoke' and process_named_running('charon'):
+        vici_terminate(conn, 'dmvpn', src_nbma, dest_nbma)
+
+    cmd(f'sudo ip route del {dest_nbma} src {src_nbma} proto 42')
+
+def route_up(interface):
+    dest_addr = os.getenv('NHRP_DESTADDR')
+    dest_prefix = os.getenv('NHRP_DESTPREFIX')
+    next_hop = os.getenv('NHRP_NEXTHOP')
+
+    cmd(f'sudo ip route replace {dest_addr}/{dest_prefix} proto 42 via {next_hop} dev {interface}')
+    cmd('sudo ip route flush cache')
+
+def route_down(interface):
+    dest_addr = os.getenv('NHRP_DESTADDR')
+    dest_prefix = os.getenv('NHRP_DESTPREFIX')
+
+    cmd(f'sudo ip route del {dest_addr}/{dest_prefix} proto 42')
+    cmd('sudo ip route flush cache')
+
+if __name__ == '__main__':
+    action = sys.argv[1]
+    interface = os.getenv('NHRP_INTERFACE')
+    dmvpn_type, profile_name = parse_type_ipsec(interface)
+
+    dmvpn_conn = None
+
+    if profile_name:
+        dmvpn_conn = f'dmvpn-{profile_name}-{interface}'
+
+    if action == 'interface-up':
+        iface_up(interface)
+    elif action == 'peer-register':
+        pass
+    elif action == 'peer-up':
+        peer_up(dmvpn_type, dmvpn_conn)
+    elif action == 'peer-down':
+        peer_down(dmvpn_type, dmvpn_conn)
+    elif action == 'route-up':
+        route_up(interface)
+    elif action == 'route-down':
+        route_down(interface)
diff --git a/src/systemd/opennhrp.service b/src/systemd/opennhrp.service
new file mode 100644
index 000000000..70235f89d
--- /dev/null
+++ b/src/systemd/opennhrp.service
@@ -0,0 +1,13 @@
+[Unit]
+Description=OpenNHRP
+After=vyos-router.service
+ConditionPathExists=/run/opennhrp/opennhrp.conf
+StartLimitIntervalSec=0
+
+[Service]
+Type=forking
+ExecStart=/usr/sbin/opennhrp -d -v -a /run/opennhrp.socket -c /run/opennhrp/opennhrp.conf -s /etc/opennhrp/opennhrp-script.py -p /run/opennhrp.pid
+ExecReload=/usr/bin/kill -HUP $MAINPID
+PIDFile=/run/opennhrp.pid
+Restart=on-failure
+RestartSec=20