diff --git a/docs/package/gluon-scheduled-domain-switch.rst b/docs/package/gluon-scheduled-domain-switch.rst
new file mode 100644
index 0000000000000000000000000000000000000000..aeb0d967a6e35cb51e736e6ca5d75125c85aa930
--- /dev/null
+++ b/docs/package/gluon-scheduled-domain-switch.rst
@@ -0,0 +1,38 @@
+gluon-scheduled-domain-switch
+=============================
+
+This package allows to switch a routers domain at a given point
+in time. This is needed for switching between incompatible transport
+protocols (e.g. 802.11s and IBSS or VXLAN).
+
+Nodes will switch when the defined *switch-time* has passed. In case the node was
+powered off while this was supposed to happen, it might not be able to aquire the
+correct time. In this case, the node will switch after it has not seen any gateway
+for a given period of time.
+
+site.conf
+---------
+All those settings have to be defined exclusively in the domain, not the site.
+
+domain_switch : optional (needed for domains to switch)
+    target_domain :
+        - target domain to switch to
+    switch_after_offline_mins :
+        - amount of time without reachable gateway to switch unconditionally
+    switch_time :
+        - UNIX epoch after which domain will be switched
+    connection_check_targets :
+        - array of IPv6 addresses which are probed to determine if the node is
+	  connected to the mesh
+
+Example::
+
+  domain_switch = {
+    target_domain = 'new_domain',
+    switch_after_offline_mins = 120,
+    switch_time = 1546344000, -- 01.01.2019 - 12:00 UTC
+    connection_check_targets = {
+      '2001:4860:4860::8888',
+      '2001:4860:4860::8844',
+    },
+  },
diff --git a/package/gluon-core/check_site.lua b/package/gluon-core/check_site.lua
index 7017af7e7dc3222e8150c4f17f01ae3757e195f4..ede64cd02ad7a7b8a7e3937486fb270eb96cec73 100644
--- a/package/gluon-core/check_site.lua
+++ b/package/gluon-core/check_site.lua
@@ -3,15 +3,6 @@ need_string(in_site({'site_name'}))
 
 -- this_domain() returns nil when multidomain support is disabled
 if this_domain() then
-	function need_domain_name(path)
-		need_string(path)
-		need(path, function(default_domain)
-			local f = io.open(os.getenv('IPKG_INSTROOT') .. '/lib/gluon/domains/' .. default_domain .. '.json')
-			if not f then return false end
-			f:close()
-			return true
-		end, nil, 'be a valid domain name')
-	end
 	need_domain_name(in_site({'default_domain'}))
 
 	need_table(in_domain({'domain_names'}), function(domain)
diff --git a/package/gluon-core/luasrc/usr/lib/lua/gluon/util.lua b/package/gluon-core/luasrc/usr/lib/lua/gluon/util.lua
index e8b7550be6df02329c3c492c0f0a5eea89a1600e..df9be8d6ef10bdce4ed84b660cbe71cea86b7295 100644
--- a/package/gluon-core/luasrc/usr/lib/lua/gluon/util.lua
+++ b/package/gluon-core/luasrc/usr/lib/lua/gluon/util.lua
@@ -253,3 +253,12 @@ function foreach_radio(uci, f)
 		end
 	end
 end
+
+function get_uptime()
+	local uptime_file = readfile("/proc/uptime")
+	if uptime_file == nil then
+		-- Something went wrong reading "/proc/uptime"
+		return nil
+	end
+	return tonumber(uptime_file:match('^[^ ]+'))
+end
diff --git a/package/gluon-scheduled-domain-switch/Makefile b/package/gluon-scheduled-domain-switch/Makefile
new file mode 100644
index 0000000000000000000000000000000000000000..b1d7f786daa7d14df9b19356d3c5f5efc13eff81
--- /dev/null
+++ b/package/gluon-scheduled-domain-switch/Makefile
@@ -0,0 +1,13 @@
+include $(TOPDIR)/rules.mk
+
+PKG_NAME:=gluon-scheduled-domain-switch
+PKG_VERSION:=1
+
+include ../gluon.mk
+
+define Package/gluon-scheduled-domain-switch
+  TITLE:=Allows scheduled migrations between domains
+  DEPENDS:=+gluon-core @GLUON_MULTIDOMAIN
+endef
+
+$(eval $(call BuildPackageGluon,gluon-scheduled-domain-switch))
diff --git a/package/gluon-scheduled-domain-switch/check_site.lua b/package/gluon-scheduled-domain-switch/check_site.lua
new file mode 100644
index 0000000000000000000000000000000000000000..d1a2fa212606d35796130445a652acade3b50d38
--- /dev/null
+++ b/package/gluon-scheduled-domain-switch/check_site.lua
@@ -0,0 +1,6 @@
+if need_table(in_domain({'domain_switch'}), check_domain_switch, false) then
+	need_domain_name(in_domain({'domain_switch', 'target_domain'}))
+	need_number(in_domain({'domain_switch', 'switch_after_offline_mins'}))
+	need_number(in_domain({'domain_switch', 'switch_time'}))
+	need_string_array_match(in_domain({'domain_switch', 'connection_check_targets'}), '^[%x:]+$')
+end
diff --git a/package/gluon-scheduled-domain-switch/luasrc/lib/gluon/upgrade/950-gluon-scheduled-domain-switch b/package/gluon-scheduled-domain-switch/luasrc/lib/gluon/upgrade/950-gluon-scheduled-domain-switch
new file mode 100755
index 0000000000000000000000000000000000000000..a15ed6822fc6216f0898054ccdd15d062d1f1726
--- /dev/null
+++ b/package/gluon-scheduled-domain-switch/luasrc/lib/gluon/upgrade/950-gluon-scheduled-domain-switch
@@ -0,0 +1,20 @@
+#!/usr/bin/lua
+
+local json = require 'jsonc'
+local site = require 'gluon.site'
+local unistd = require 'posix.unistd'
+
+local cronfile = "/usr/lib/micron.d/gluon-scheduled-domain-switch"
+
+-- Check if domain switch is scheduled
+if site.domain_switch() == nil then
+	-- In case no domain switch is scheduled, remove cronfile
+	os.remove(cronfile)
+	os.exit(0)
+end
+
+-- Only in case domain switch is scheduled
+local f = io.open(cronfile, "w")
+f:write("* * * * *  /usr/bin/gluon-check-connection\n")
+f:write("*/5 * * * *  /usr/bin/gluon-switch-domain\n")
+f:close()
diff --git a/package/gluon-scheduled-domain-switch/luasrc/usr/bin/gluon-check-connection b/package/gluon-scheduled-domain-switch/luasrc/usr/bin/gluon-check-connection
new file mode 100755
index 0000000000000000000000000000000000000000..508cd17a6e7c573e3986c776ad089bc95dccc134
--- /dev/null
+++ b/package/gluon-scheduled-domain-switch/luasrc/usr/bin/gluon-check-connection
@@ -0,0 +1,36 @@
+#!/usr/bin/lua
+
+local unistd = require 'posix.unistd'
+local util = require 'gluon.util'
+local site = require 'gluon.site'
+
+local offline_flag_file = "/tmp/gluon_offline"
+local is_offline = true
+
+-- Check if domain-switch is scheduled
+if site.domain_switch() == nil then
+	-- Switch not applicable for current domain
+	os.exit(0)
+end
+
+-- Check reachability of pre-defined targets
+for _, ip in ipairs(site.domain_switch.connection_check_targets()) do
+	local exit_code = os.execute("ping -c 1 -w 10 " .. ip)
+	if exit_code == 0 then
+		is_offline = false
+		break
+	end
+end
+
+if is_offline then
+	-- Check if we were previously offline
+	if unistd.access(offline_flag_file) then
+		os.exit(0)
+	end
+	-- Create offline flag
+	local f = io.open(offline_flag_file, "w")
+	f:write(tostring(util.get_uptime()))
+	f:close()
+else
+	os.remove(offline_flag_file)
+end
diff --git a/package/gluon-scheduled-domain-switch/luasrc/usr/bin/gluon-switch-domain b/package/gluon-scheduled-domain-switch/luasrc/usr/bin/gluon-switch-domain
new file mode 100755
index 0000000000000000000000000000000000000000..57fed15a2a4ebf380e1005aa378b8a5ad55276e9
--- /dev/null
+++ b/package/gluon-scheduled-domain-switch/luasrc/usr/bin/gluon-switch-domain
@@ -0,0 +1,67 @@
+#!/usr/bin/lua
+
+local uci = require('simple-uci').cursor()
+local unistd = require 'posix.unistd'
+local util = require 'gluon.util'
+local site = require 'gluon.site'
+
+-- Returns true if node was offline long enough to perform domain switch
+function switch_after_min_reached()
+	if not unistd.access("/tmp/gluon_offline") then
+		return false
+	end
+
+	local switch_after_sec = site.domain_switch.switch_after_offline_mins() * 60
+
+	local current_uptime = util.get_uptime()
+	if current_uptime == nil then
+		return false
+	end
+
+	local f = util.readfile("/tmp/gluon_offline")
+	if f == nil then
+		return false
+	end
+	local offline_since = tonumber(f)
+
+	local offline_time_sec = current_uptime - offline_since
+
+	if offline_time_sec > switch_after_sec then
+		return true
+	end
+	return false
+end
+
+-- Returns true in case switch time has passed
+function switch_time_passed()
+	local current_time = os.time()
+	local switch_time = site.domain_switch.switch_time()
+
+	return switch_time < current_time
+end
+
+if site.domain_switch() == nil then
+	-- Switch not applicable for current domain
+	print("No domain switch defined for the current domain.")
+	os.exit(0)
+end
+
+local current_domain = uci:get("gluon", "core", "domain")
+local target_domain = site.domain_switch.target_domain()
+
+if target_domain == current_domain then
+	-- Current and target domain are equal
+	print("Domain '" .. target_domain .. "' equals current domain.")
+	os.exit(1)
+end
+
+if not switch_after_min_reached() and not switch_time_passed() then
+	-- Neither switch-time passed nor switch_after_min reached
+	os.exit(0)
+end
+
+uci:set("gluon", "core", "domain", target_domain)
+uci:commit("gluon")
+
+os.execute("gluon-reconfigure")
+os.execute("reboot")
diff --git a/scripts/check_site.lua b/scripts/check_site.lua
index 41944cb0ca29fdbcbae313d85d84636223df35db..6db3b1c0c7cef45735197b42dd774257b2a2c687 100644
--- a/scripts/check_site.lua
+++ b/scripts/check_site.lua
@@ -305,6 +305,15 @@ function need_array_of(path, array, required)
 	return need_array(path, function(e) need_one_of(e, array) end, required)
 end
 
+function need_domain_name(path)
+	need_string(path)
+	need(path, function(domain_name)
+		local f = io.open(os.getenv('IPKG_INSTROOT') .. '/lib/gluon/domains/' .. domain_name .. '.json')
+		if not f then return false end
+		f:close()
+		return true
+	end, nil, 'be a valid domain name')
+end
 
 local check = assert(loadfile())