diff --git a/package/gluon-config-mode-mesh-vpn/luasrc/lib/gluon/config-mode/reboot/0100-mesh-vpn.lua b/package/gluon-config-mode-mesh-vpn/luasrc/lib/gluon/config-mode/reboot/0100-mesh-vpn.lua
index b94d194abc59e603abecd5a1edefb3fcc693dfe3..31403ddbc1908f1cd92333eaffb8c2d967ce5d83 100644
--- a/package/gluon-config-mode-mesh-vpn/luasrc/lib/gluon/config-mode/reboot/0100-mesh-vpn.lua
+++ b/package/gluon-config-mode-mesh-vpn/luasrc/lib/gluon/config-mode/reboot/0100-mesh-vpn.lua
@@ -1,20 +1,14 @@
 local site_i18n = i18n 'gluon-site'
 
 local uci = require("simple-uci").cursor()
-local unistd = require 'posix.unistd'
 
 local platform = require 'gluon.platform'
 local site = require 'gluon.site'
 local sysconfig = require 'gluon.sysconfig'
-local util = require "gluon.util"
+local vpn = require 'gluon.mesh-vpn'
 
 local pretty_hostname = require 'pretty_hostname'
 
-
-local has_fastd = unistd.access('/lib/gluon/mesh-vpn/fastd')
-local has_tunneldigger = unistd.access('/lib/gluon/mesh-vpn/tunneldigger')
-
-
 local hostname = pretty_hostname.get(uci)
 local contact = uci:get_first("gluon-node-info", "owner", "contact")
 
@@ -22,19 +16,18 @@ local pubkey
 local msg
 
 
-if has_tunneldigger then
-	local tunneldigger_enabled = uci:get_bool("tunneldigger", "mesh_vpn", "enabled")
-	if not tunneldigger_enabled then
-		msg = site_i18n._translate('gluon-config-mode:novpn')
+if vpn.enabled() then
+	local _, active_vpn = vpn.get_active_provider()
+
+	if active_vpn ~= nil then
+		pubkey = active_vpn.public_key()
 	end
-elseif has_fastd then
-	local fastd_enabled = uci:get_bool("fastd", "mesh_vpn", "enabled")
-	if fastd_enabled then
-		pubkey = util.trim(util.exec("/etc/init.d/fastd show_key mesh_vpn"))
+
+	if pubkey ~= nil then
 		msg = site_i18n._translate('gluon-config-mode:pubkey')
-	else
-		msg = site_i18n._translate('gluon-config-mode:novpn')
 	end
+else
+	msg = site_i18n._translate('gluon-config-mode:novpn')
 end
 
 if not msg then return end
diff --git a/package/gluon-config-mode-mesh-vpn/luasrc/lib/gluon/config-mode/wizard/0300-mesh-vpn.lua b/package/gluon-config-mode-mesh-vpn/luasrc/lib/gluon/config-mode/wizard/0300-mesh-vpn.lua
index 70a1307b522017e6ce5af01f1498c8efb11fb292..bdd091eeebf706e98b315c3e892d6947a7b3731b 100644
--- a/package/gluon-config-mode-mesh-vpn/luasrc/lib/gluon/config-mode/wizard/0300-mesh-vpn.lua
+++ b/package/gluon-config-mode-mesh-vpn/luasrc/lib/gluon/config-mode/wizard/0300-mesh-vpn.lua
@@ -1,10 +1,8 @@
-local unistd = require 'posix.unistd'
-
-local has_fastd = unistd.access('/lib/gluon/mesh-vpn/fastd')
-local has_tunneldigger = unistd.access('/lib/gluon/mesh-vpn/tunneldigger')
+local vpn = require 'gluon.mesh-vpn'
+local _, active_vpn = vpn.get_active_provider()
 
 return function(form, uci)
-	if not (has_fastd or has_tunneldigger) then
+	if active_vpn == nil then
 		return
 	end
 
diff --git a/package/gluon-mesh-vpn-core/luasrc/lib/gluon/mesh-vpn/update-config b/package/gluon-mesh-vpn-core/luasrc/lib/gluon/mesh-vpn/update-config
index 45ecc2d5e3620c51dc64104cb225db74b7a14c8c..935d0b9ad1fe4063039d94c85375ec7614c517de 100755
--- a/package/gluon-mesh-vpn-core/luasrc/lib/gluon/mesh-vpn/update-config
+++ b/package/gluon-mesh-vpn-core/luasrc/lib/gluon/mesh-vpn/update-config
@@ -1,14 +1,7 @@
 #!/usr/bin/lua
 
 local uci = require('simple-uci').cursor()
-local unistd = require 'posix.unistd'
-
-local vpn
-if unistd.access('/lib/gluon/mesh-vpn/fastd') then
-	vpn = 'fastd'
-elseif unistd.access('/lib/gluon/mesh-vpn/tunneldigger') then
-	vpn = 'tunneldigger'
-end
+local vpn_name, vpn = require('gluon.mesh-vpn').get_active_provider()
 
 local vpn_config = {
 	enabled = uci:get_bool('gluon', 'mesh_vpn', 'enabled'),
@@ -17,32 +10,20 @@ local vpn_config = {
 	limit_ingress = uci:get('gluon', 'mesh_vpn', 'limit_ingress'),
 }
 
-uci:delete('simple-tc', 'mesh_vpn')
-uci:section('simple-tc', 'interface', 'mesh_vpn', {
-	ifname = 'mesh-vpn',
-	enabled = vpn_config.limit_enabled,
-	limit_egress = vpn_config.limit_egress,
-})
-
-if vpn == 'fastd' then
-	uci:set('fastd', 'mesh_vpn', 'enabled', vpn_config.enabled)
-	uci:set('simple-tc', 'mesh_vpn', 'limit_ingress', vpn_config.limit_ingress)
-else
+if vpn_name ~= 'fastd' then
 	uci:set('fastd', 'mesh_vpn', 'enabled', false)
+	uci:save('fastd')
 end
-uci:save('fastd')
 
-if vpn == 'tunneldigger' then
-	uci:set('tunneldigger', 'mesh_vpn', 'enabled', vpn_config.enabled)
+if vpn_name ~= 'tunneldigger' then
+	uci:set('tunneldigger', 'mesh_vpn', 'enabled', false)
+	uci:save('tunneldigger')
+end
 
-	if vpn_config.limit_enabled then
-		uci:set('tunneldigger', 'mesh_vpn', 'limit_bw_down', vpn_config.limit_ingress)
-	else
-		uci:delete('tunneldigger', 'mesh_vpn', 'limit_bw_down')
-	end
+vpn.enable(vpn_config.enabled)
+if vpn_config.limit_enabled then
+	vpn.set_limit(vpn_config.limit_ingress, vpn_config.limit_egress)
 else
-	uci:set('tunneldigger', 'mesh_vpn', 'enabled', false)
+	vpn.set_limit(nil, nil)
 end
-uci:save('tunneldigger')
 
-uci:save('simple-tc')
diff --git a/package/gluon-mesh-vpn-core/luasrc/lib/gluon/upgrade/500-mesh-vpn b/package/gluon-mesh-vpn-core/luasrc/lib/gluon/upgrade/500-mesh-vpn
index 019b9afbe10ed8d5b76f80660709accdfc1c559e..58bfa30e35263c844d7d0b888d5410b7f3fb9cbe 100755
--- a/package/gluon-mesh-vpn-core/luasrc/lib/gluon/upgrade/500-mesh-vpn
+++ b/package/gluon-mesh-vpn-core/luasrc/lib/gluon/upgrade/500-mesh-vpn
@@ -5,11 +5,11 @@ local users = require 'gluon.users'
 local util = require 'gluon.util'
 
 local uci = require('simple-uci').cursor()
-local unistd = require 'posix.unistd'
 
+local vpn_core = require 'gluon.mesh-vpn'
 
 uci:section('network', 'interface', 'mesh_vpn', {
-	ifname = 'mesh-vpn',
+	ifname = vpn_core.get_interface(),
 	proto = 'gluon_mesh',
 	transitive = true,
 	fixed_mtu = true,
@@ -35,12 +35,7 @@ uci:save('firewall')
 
 -- VPN migration
 if not uci:get('gluon', 'mesh_vpn') then
-	local vpn
-	if unistd.access('/lib/gluon/mesh-vpn/fastd') then
-		vpn = 'fastd'
-	elseif unistd.access('/lib/gluon/mesh-vpn/tunneldigger') then
-		vpn = 'tunneldigger'
-	end
+	local vpn, _ = vpn_core.get_active_provider()
 
 	local fastd_enabled = uci:get('fastd', 'mesh_vpn', 'enabled')
 	local tunneldigger_enabled = uci:get('tunneldigger', 'mesh_vpn', 'enabled')
diff --git a/package/gluon-mesh-vpn-core/luasrc/usr/lib/lua/gluon/mesh-vpn.lua b/package/gluon-mesh-vpn-core/luasrc/usr/lib/lua/gluon/mesh-vpn.lua
index 99d856c7db360a15bb9811c857c21be5557b3f33..dc5f45c08dbdb43586baf8f7a109615988e774e3 100644
--- a/package/gluon-mesh-vpn-core/luasrc/usr/lib/lua/gluon/mesh-vpn.lua
+++ b/package/gluon-mesh-vpn-core/luasrc/usr/lib/lua/gluon/mesh-vpn.lua
@@ -1,7 +1,47 @@
+local uci = require('simple-uci').cursor()
+
+local util = require 'gluon.util'
+
 local M = {}
 
-function M.get_mesh_vpn_interface()
-  return 'mesh-vpn'
+function M.enabled()
+	return uci:get_bool('gluon', 'mesh_vpn', 'enabled')
+end
+
+function M.enable(val)
+	return uci:set('gluon', 'mesh_vpn', 'enabled', val)
+end
+
+function M.get_interface()
+	return 'mesh-vpn'
+end
+
+function M.get_provider(name)
+	return require('gluon.mesh-vpn.provider.' .. name)
+end
+
+function M.get_provider_names()
+	local out = {}
+
+	for _, v in ipairs(util.glob('/lib/gluon/mesh-vpn/provider/*')) do
+		table.insert(out, v:match('([^/]+)$'))
+	end
+
+	return out
+end
+
+function M.get_active_provider()
+	-- Active provider is the provider in use
+	-- by the currently active site / domain
+
+	for _, name in ipairs(M.get_provider_names()) do
+		local provider = M.get_provider(name)
+		if provider.active() then
+			return name, provider
+		end
+	end
+
+	return nil, nil
 end
 
 return M
diff --git a/package/gluon-mesh-vpn-fastd/files/lib/gluon/mesh-vpn/fastd b/package/gluon-mesh-vpn-fastd/files/lib/gluon/mesh-vpn/provider/fastd
similarity index 100%
rename from package/gluon-mesh-vpn-fastd/files/lib/gluon/mesh-vpn/fastd
rename to package/gluon-mesh-vpn-fastd/files/lib/gluon/mesh-vpn/provider/fastd
diff --git a/package/gluon-mesh-vpn-fastd/luasrc/lib/gluon/upgrade/400-mesh-vpn-fastd b/package/gluon-mesh-vpn-fastd/luasrc/lib/gluon/upgrade/400-mesh-vpn-fastd
index 0312b29c120743d64b6bcd144908ef4a2214cd86..b59ef2c7e7b83164a89fff17e3535665e192e7f5 100755
--- a/package/gluon-mesh-vpn-fastd/luasrc/lib/gluon/upgrade/400-mesh-vpn-fastd
+++ b/package/gluon-mesh-vpn-fastd/luasrc/lib/gluon/upgrade/400-mesh-vpn-fastd
@@ -2,6 +2,7 @@
 
 local site = require 'gluon.site'
 local util = require 'gluon.util'
+local vpn_core = require 'gluon.mesh-vpn'
 
 local uci = require('simple-uci').cursor()
 
@@ -37,7 +38,7 @@ end
 uci:section('fastd', 'fastd', 'mesh_vpn', {
 	group = 'gluon-mesh-vpn',
 	syslog_level = syslog_level,
-	interface = 'mesh-vpn',
+	interface = vpn_core.get_interface(),
 	mode = 'tap',
 	mtu = site.mesh_vpn.mtu(),
 	secure_handshakes = true,
diff --git a/package/gluon-mesh-vpn-fastd/luasrc/usr/lib/lua/gluon/mesh-vpn/provider/fastd.lua b/package/gluon-mesh-vpn-fastd/luasrc/usr/lib/lua/gluon/mesh-vpn/provider/fastd.lua
new file mode 100644
index 0000000000000000000000000000000000000000..1d628dc76e6a490bad3b8e90e143213c33e15aa1
--- /dev/null
+++ b/package/gluon-mesh-vpn-fastd/luasrc/usr/lib/lua/gluon/mesh-vpn/provider/fastd.lua
@@ -0,0 +1,36 @@
+local uci = require('simple-uci').cursor()
+
+local site = require 'gluon.site'
+local util = require 'gluon.util'
+local vpn_core = require 'gluon.mesh-vpn'
+
+local M = {}
+
+function M.public_key()
+	return util.trim(util.exec('/etc/init.d/fastd show_key mesh_vpn'))
+end
+
+function M.enable(val)
+	uci:set('fastd', 'mesh_vpn', 'enabled', val)
+	uci:save('fastd')
+end
+
+function M.active()
+	return site.mesh_vpn.fastd() ~= nil
+end
+
+function M.set_limit(ingress_limit, egress_limit)
+	uci:delete('simple-tc', 'mesh_vpn')
+	if ingress_limit ~= nil and egress_limit ~= nil then
+		uci:section('simple-tc', 'interface', 'mesh_vpn', {
+			ifname = vpn_core.get_interface(),
+			enabled = true,
+			limit_egress = egress_limit,
+			limit_ingress = ingress_limit,
+		})
+	end
+
+	uci:save('simple-tc')
+end
+
+return M
diff --git a/package/gluon-mesh-vpn-tunneldigger/files/lib/gluon/mesh-vpn/tunneldigger b/package/gluon-mesh-vpn-tunneldigger/files/lib/gluon/mesh-vpn/provider/tunneldigger
similarity index 100%
rename from package/gluon-mesh-vpn-tunneldigger/files/lib/gluon/mesh-vpn/tunneldigger
rename to package/gluon-mesh-vpn-tunneldigger/files/lib/gluon/mesh-vpn/provider/tunneldigger
diff --git a/package/gluon-mesh-vpn-tunneldigger/luasrc/lib/gluon/upgrade/400-mesh-vpn-tunneldigger b/package/gluon-mesh-vpn-tunneldigger/luasrc/lib/gluon/upgrade/400-mesh-vpn-tunneldigger
index b91288a234aced219335efbef0e576b45a1a1a97..b37bb4761f8ca147176c67535b505e7a77c96935 100755
--- a/package/gluon-mesh-vpn-tunneldigger/luasrc/lib/gluon/upgrade/400-mesh-vpn-tunneldigger
+++ b/package/gluon-mesh-vpn-tunneldigger/luasrc/lib/gluon/upgrade/400-mesh-vpn-tunneldigger
@@ -2,6 +2,7 @@
 
 local site = require 'gluon.site'
 local util = require 'gluon.util'
+local vpn_core = require 'gluon.mesh-vpn'
 
 local uci = require('simple-uci').cursor()
 
@@ -23,7 +24,7 @@ end
 uci:section('tunneldigger', 'broker', 'mesh_vpn', {
 	enabled = enabled,
 	uuid = util.node_id(),
-	interface = 'mesh-vpn',
+	interface = vpn_core.get_interface(),
 	bind_interface = 'br-wan',
 	group = 'gluon-mesh-vpn',
 	broker_selection = 'usage',
diff --git a/package/gluon-mesh-vpn-tunneldigger/luasrc/lib/lua/gluon/mesh-vpn/provider/tunneldigger.lua b/package/gluon-mesh-vpn-tunneldigger/luasrc/lib/lua/gluon/mesh-vpn/provider/tunneldigger.lua
new file mode 100644
index 0000000000000000000000000000000000000000..d324a3cc1f836c3c568a744ffdfdc01b07e78f58
--- /dev/null
+++ b/package/gluon-mesh-vpn-tunneldigger/luasrc/lib/lua/gluon/mesh-vpn/provider/tunneldigger.lua
@@ -0,0 +1,42 @@
+local uci = require('simple-uci').cursor()
+
+local site = require 'gluon.site'
+local vpn_core = require 'gluon.mesh-vpn'
+
+local M = {}
+
+function M.public_key()
+	return nil
+end
+
+function M.enable(val)
+	uci:set('tunneldigger', 'mesh_vpn', 'enabled', val)
+	uci:save('tunneldigger')
+end
+
+function M.active()
+	return site.mesh_vpn.tunneldigger() ~= nil
+end
+
+function M.set_limit(ingress_limit, egress_limit)
+	if ingress_limit ~= nil then
+		uci:set('tunneldigger', 'mesh_vpn', 'limit_bw_down', ingress_limit)
+	else
+		uci:delete('tunneldigger', 'mesh_vpn', 'limit_bw_down')
+	end
+
+	if egress_limit ~= nil then
+		uci:section('simple-tc', 'interface', 'mesh_vpn', {
+			ifname = vpn_core.get_interface(),
+			enabled = true,
+			limit_egress = egress_limit,
+		})
+	else
+		uci:delete('simple-tc', 'mesh_vpn')
+	end
+
+	uci:save('tunneldigger')
+	uci:save('simple-tc')
+end
+
+return M
diff --git a/package/gluon-web-admin/files/lib/gluon/config-mode/view/admin/info.html b/package/gluon-web-admin/files/lib/gluon/config-mode/view/admin/info.html
index 5f3e9191d2f7d06b1da9dc69a97fbeb7a33eba7e..87c1179e9b0296da878c797690f00a9be93125e9 100644
--- a/package/gluon-web-admin/files/lib/gluon/config-mode/view/admin/info.html
+++ b/package/gluon-web-admin/files/lib/gluon/config-mode/view/admin/info.html
@@ -6,14 +6,19 @@
 	local sysconfig = require 'gluon.sysconfig'
 	local platform = require 'gluon.platform'
 	local util = require "gluon.util"
+	local has_vpn, vpn = pcall(require, 'gluon.mesh-vpn')
 
 	local _ = translate
 
 
 	local pubkey
-	local meshvpn_enabled = uci:get_bool("fastd", "mesh_vpn", "enabled")
-	if meshvpn_enabled then
-		pubkey = util.trim(util.exec('/etc/init.d/fastd show_key mesh_vpn'))
+	if has_vpn and vpn.enabled() then
+		local _, active_vpn = vpn.get_active_provider()
+
+		if active_vpn ~= nil then
+			pubkey = active_vpn.public_key()
+		end
+
 		if pubkey == '' then
 			pubkey = nil
 		end