-
Notifications
You must be signed in to change notification settings - Fork 1
/
failover-dhcp
160 lines (145 loc) · 7.28 KB
/
failover-dhcp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
{
# Mikrotik failover script with dual dhcp interfaces
# Copyright (C) 2021 Alexandre PIERRET
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation, either version 3 of the License, or (at your option) any later
# version.
#
# 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/>.
# Changelog:
# - 2021/08/27: Initial release
# Features:
# - main/backup failover with 2 dhcp-client interfaces
# - rely on multiple remote test hosts to decide if the main link is working properly
# - automaticaly add/remove required routes to remote test hosts
# - adjust testHosts routes if the gateway change
# - configurable "ping success rate expected"
# - stop pinging testHosts as soon as the expected rate is reached to avoid unnecessary traffic
# - release DHCP lease in mainInterface on link failure to trigger a renew.
# (This is usefull if the provider change the subnet on its side but our DHCP lease is not expired yet)
# - doesn't need :global variable
# - doesn't need specific configuration other than using dhcp-client on both main/backup interfaces
# Limitations:
# - hosts in testHosts array can't be reach through backupInterface if mainInterface receive a DHCP lease
# - mainInterface and backupInterface must be configured in dhcp-client
# - Only work with IPv4
# How to use it
# - Set the mainInterface and the backupInterface variable (dhcp client configuration must exist)
# - Set the testHosts you want to use as reliable remote host to test connectivity
# - Upload the script to your router
# - Schedule it to run every minutes
# Original testHosts list:
# - 4.2.2.1 => CenturyLink DNS server
# - 192.175.48.1 => AS112 - blackhole DNS for RFC1918
# - 1.0.0.1 => Cloudflare DNS server
# - 137.138.44.217 => CERN looking glass
# - 8.8.4.4 => Google DNS server #2
################################
### User variables
################################
# Basic
:local mainInterface "ether1"; # My FTTH ONU (dhcp)
:local backupInterface "ether2"; # My LTE modem (dhcp)
:local testHosts {
"4.2.2.1";
"192.175.48.1";
"1.0.0.1";
"137.138.44.217";
"8.8.4.4";
}
:local pingPerHost 2; # How many ping per host
:local pingRateSucessExpected 5; # Success rate expected (in percent)
# Expert
:local pingSuccessDelay 100ms; # ping success throttle
:local mainPromoteRouteDistance 10; # Route distance when mainInterface is promote
:local backupRouteDistance 20; # Backup interface route distance
:local mainDemoteRouteDistance 30; # Route distance when mainInterface is demote
:local routeComment "managed-by-failover-script"; # Comment link to routes added to force testHosts through mainInterface
################################
### Code
################################
# Remove previously added routes that are no longer in testHosts (based on route comment)
:foreach managedRoute in=[/ip route find where comment=$routeComment] do={
:local lPrefix [/ip route get $managedRoute dst-address]
:local lHost [:pick $lPrefix 0 [:find $lPrefix "/"]]; # Remove ending /32
# if $lHost not in $testHosts
:if ([:typeof [:find $testHosts $lHost]]="nil") do={
/ip route remove $managedRoute;
}
}
# Intenal variables
:local currentMainRouteDistance [/ip dhcp-client get [/ip dhcp-client find where interface=$mainInterface] default-route-distance];
:local currentBackupRouteDistance [/ip dhcp-client get [/ip dhcp-client find where interface=$backupInterface] default-route-distance];
# If required, set the backup default route distance
:if ($currentBackupRouteDistance != $backupRouteDistance) do={
/ip dhcp-client set [/ip dhcp-client find where interface=$backupInterface] default-route-distance=$backupRouteDistance;
}
# Add/remove more-specific routes to testHosts depending of mainInterface dhcp status and lease
# If DHCP status is bound:
# Remove testHosts routes from previous lease (if any)
# Add testHosts /32 routes if not already exist
# Else (DHCP status not bound):
# Remove all testHosts /32 routes
:local mainGatewayDhcpInfo [:pick [/ip dhcp-client print as-value detail where interface=$mainInterface] 0];
:local mainGatewayDhcpStatus ($mainGatewayDhcpInfo->"status");
:local mainGatewayDhcpGatewayIp ($mainGatewayDhcpInfo->"gateway");
:if ($mainGatewayDhcpStatus = "bound") do={
:foreach testHost in=$testHosts do={
:local testHostIp [:tostr ("$testHost"."/32")]; # Generate x.x.x.x/32 str from x.x.x.x
# Remove route from previous lease (if any)
/ip route remove [/ip route find where dst-address=$testHostIp and gateway!=[:tostr $mainGatewayDhcpGatewayIp]];
# Add route if not already exists
:if ([:len [/ip route find dst-address=$testHostIp]] = 0) do={
/ip route add dst-address=$testHostIp gateway=[:tostr $mainGatewayDhcpGatewayIp] comment=$routeComment;
}
}
} else={
# Remove all testHosts /32 routes
:foreach testHost in=$testHosts do={
:local testHostIp [:tostr ("$testHost"."/32")];
/ip route remove [/ip route find where dst-address=[:tostr $testHostIp]];
}
}
# Ping loop, can exit earlier if the sucess rate is reached
:local pingSuccessCount 0;
:local pingCount 1;
:local pingRateSucess 0;
:while (($pingCount <= $pingPerHost) and ($pingRateSucess < $pingRateSucessExpected)) do={
:foreach tHost in=$testHosts do={
:if ([/ping address=$tHost count=1]=1) do={
:set pingSuccessCount ($pingSuccessCount + 1);
:delay [:totime $pingSuccessDelay];
}
}
:set pingCount ($pingCount + 1);
:set pingRateSucess ($pingSuccessCount * 100 / ([:len $testHosts] * $pingPerHost));
}
:if ($pingRateSucess < $pingRateSucessExpected) do={
# Demote mainInterface
:if ($currentMainRouteDistance != $mainDemoteRouteDistance) do={
/ip dhcp-client set [/ip dhcp-client find where interface=$mainInterface] default-route-distance=$mainDemoteRouteDistance;
:log info "Detected main link failure, switching to backup link"
#>>> ADD HERE ACTIONS TO DO ON MAIN LINK FAILURE (email, http call, logging, ...)
}
# Release DHCP lease if bound to:
# - allow the testHosts to be reached through the backupInterface
# - force a DHCP renew in case of mainInterface subnet has changed on the provider side but our lease is not expired yet
:if ($mainGatewayDhcpStatus = "bound") do={
/ip dhcp-client release [/ip dhcp-client find interface=$mainInterface];
}
} else={
# Promote mainInterface
:if ($currentMainRouteDistance != $mainPromoteRouteDistance) do={
/ip dhcp-client set [/ip dhcp-client find where interface=$mainInterface] default-route-distance=$mainPromoteRouteDistance;
:log info "Main link back online, switching back to main link"
#>>> ADD HERE ACTIONS TO DO ON MAIN LINK BACK ONLINE (email, http call, logging, ...)
}
}
}