b14c95e5b992827776ff57a51b3ae8f43b44e2f1
[samba.git] / ctdb / config / events.d / 13.per_ip_routing
1 #!/bin/sh
2
3 [ -n "$CTDB_BASE" ] || \
4     CTDB_BASE=$(d=$(dirname "$0") ; cd -P "$d" ; dirname "$PWD")
5
6 . "${CTDB_BASE}/functions"
7
8 loadconfig
9
10 # service_name is used by various functions
11 # shellcheck disable=SC2034
12 service_name=per_ip_routing
13
14 # Do nothing if unconfigured 
15 [ -n "$CTDB_PER_IP_ROUTING_CONF" ] || exit 0
16
17 table_id_prefix="ctdb."
18
19 [ -n "$CTDB_PER_IP_ROUTING_RULE_PREF" ] || \
20     die "error: CTDB_PER_IP_ROUTING_RULE_PREF not configured"
21
22 [ "$CTDB_PER_IP_ROUTING_TABLE_ID_LOW" -lt "$CTDB_PER_IP_ROUTING_TABLE_ID_HIGH" ] 2>/dev/null || \
23     die "error: CTDB_PER_IP_ROUTING_TABLE_ID_LOW[$CTDB_PER_IP_ROUTING_TABLE_ID_LOW] and/or CTDB_PER_IP_ROUTING_TABLE_ID_HIGH[$CTDB_PER_IP_ROUTING_TABLE_ID_HIGH] improperly configured"
24
25 if [ "$CTDB_PER_IP_ROUTING_TABLE_ID_LOW" -le 253 -a \
26     255 -le "$CTDB_PER_IP_ROUTING_TABLE_ID_HIGH" ] ; then
27     die "error: range CTDB_PER_IP_ROUTING_TABLE_ID_LOW[$CTDB_PER_IP_ROUTING_TABLE_ID_LOW]..CTDB_PER_IP_ROUTING_TABLE_ID_HIGH[$CTDB_PER_IP_ROUTING_TABLE_ID_HIGH] must not include 253-255"
28 fi
29
30 have_link_local_config ()
31 {
32     [ "$CTDB_PER_IP_ROUTING_CONF" = "__auto_link_local__" ]
33 }
34
35 if ! have_link_local_config && [ ! -r "$CTDB_PER_IP_ROUTING_CONF" ] ; then
36     die "error: CTDB_PER_IP_ROUTING_CONF=$CTDB_PER_IP_ROUTING_CONF file not found"
37 fi
38
39 ######################################################################
40
41 ipv4_is_valid_addr()
42 {
43     _ip="$1"
44
45     _count=0
46     # Get the shell to break up the address into 1 word per octet 
47     for _o in $(export IFS="." ; echo $_ip) ; do
48         # The 2>/dev/null stops output from failures where an "octet"
49         # is not numeric.  The test will still fail.
50         if ! [ 0 -le $_o -a $_o -le 255 ] 2>/dev/null ; then
51             return 1
52         fi
53         _count=$((_count + 1))
54     done
55
56     # A valid IPv4 address has 4 octets
57     [ $_count -eq 4 ]
58 }
59
60 ensure_ipv4_is_valid_addr ()
61 {
62     _event="$1"
63     _ip="$2"
64
65     ipv4_is_valid_addr "$_ip" || {
66         echo "$0: $_event not an ipv4 address skipping IP:$_ip"
67         exit 0
68     }
69 }
70
71 ipv4_host_addr_to_net ()
72 {
73     _host="$1"
74     _maskbits="$2"
75
76     # Convert the host address to an unsigned long by splitting out
77     # the octets and doing the math.
78     _host_ul=0
79     for _o in $(export IFS="." ; echo $_host) ; do
80         _host_ul=$(( (_host_ul << 8) + _o)) # work around Emacs color bug
81     done
82
83     # Calculate the mask and apply it.
84     _mask_ul=$(( 0xffffffff << (32 - _maskbits) ))
85     _net_ul=$(( _host_ul & _mask_ul ))
86  
87     # Now convert to a network address one byte at a time.
88     _net=""
89     for _o in $(seq 1 4) ; do
90         _net="$((_net_ul & 255))${_net:+.}${_net}"
91         _net_ul=$((_net_ul >> 8))
92     done
93
94     echo "${_net}/${_maskbits}"
95 }
96
97 ######################################################################
98
99 ensure_rt_tables ()
100 {
101     rt_tables="$CTDB_SYS_ETCDIR/iproute2/rt_tables"
102
103     # This file should always exist.  Even if this didn't exist on the
104     # system, adding a route will have created it.  What if we startup
105     # and immediately shutdown?  Let's be sure.
106     if [ ! -f "$rt_tables" ] ; then
107         mkdir -p "${rt_tables%/*}" # dirname
108         touch "$rt_tables"
109     fi
110 }
111
112 # Setup a table id to use for the given IP.  We don't need to know it,
113 # it just needs to exist in /etc/iproute2/rt_tables.  Fail if no free
114 # table id could be found in the configured range.
115 ensure_table_id_for_ip ()
116 {
117     _ip=$1
118
119     ensure_rt_tables
120
121     # Maintain a table id for each IP address we've ever seen in
122     # rt_tables.  We use a "ctdb." prefix on the label.
123     _label="${table_id_prefix}${_ip}"
124
125     # This finds either the table id corresponding to the label or a
126     # new unused one (that is greater than all the used ones in the
127     # range).
128     (
129         # Note that die() just gets us out of the subshell...
130         flock --timeout 30 0 || \
131             die "ensure_table_id_for_ip: failed to lock file $rt_tables"
132
133         _new=$CTDB_PER_IP_ROUTING_TABLE_ID_LOW
134         while read _t _l ; do
135             # Skip comments
136             case "$_t" in
137                 \#*) continue ;;
138             esac
139             # Found existing: done
140             if [ "$_l" = "$_label" ] ; then
141                 return 0
142             fi
143             # Potentially update the new table id to be used.  The
144             # redirect stops error spam for a non-numeric value.
145             if [ $_new -le $_t -a \
146                 $_t -le $CTDB_PER_IP_ROUTING_TABLE_ID_HIGH ] 2>/dev/null ; then
147                 _new=$((_t + 1))
148             fi
149         done
150
151         # If the new table id is legal then add it to the file and
152         # print it.
153         if [ $_new -le $CTDB_PER_IP_ROUTING_TABLE_ID_HIGH ] ; then
154             printf "%d\t%s\n" "$_new" "$_label" >>"$rt_tables"
155             return 0
156         else
157             return 1
158         fi
159     ) <"$rt_tables"
160 }
161
162 # Clean up all the table ids that we might own.
163 clean_up_table_ids ()
164 {
165     ensure_rt_tables
166
167     (
168         # Note that die() just gets us out of the subshell...
169         flock --timeout 30 0 || \
170             die "clean_up_table_ids: failed to lock file $rt_tables"
171
172         # Delete any items from the file that have a table id in our
173         # range or a label matching our label.  Preserve comments.
174         _tmp="${rt_tables}.$$.ctdb"
175         awk -v min="$CTDB_PER_IP_ROUTING_TABLE_ID_LOW" \
176             -v max="$CTDB_PER_IP_ROUTING_TABLE_ID_HIGH" \
177             -v pre="$table_id_prefix" \
178             '/^#/ || \
179              !(min <= $1 && $1 <= max) && \
180              !(index($2, pre) == 1) \
181              { print $0 }' "$rt_tables" >"$_tmp"
182
183         mv "$_tmp" "$rt_tables"
184         # The lock is gone - don't do anything else here
185     ) <"$rt_tables"
186 }
187
188 ######################################################################
189
190 # This prints the config for an IP, which is either relevant entries
191 # from the config file or, if set to the magic link local value, some
192 # link local routing config for the IP.
193 get_config_for_ip ()
194 {
195     _ip="$1"
196
197     if have_link_local_config ; then
198         # When parsing public_addresses also split on '/'.  This means
199         # that we get the maskbits as item #2 without further parsing.
200         while IFS="/$IFS" read _i _maskbits _x ; do
201             if [ "$_ip" = "$_i" ] ; then
202                 echo -n "$_ip "; ipv4_host_addr_to_net "$_ip" "$_maskbits"
203             fi
204         done <"${CTDB_PUBLIC_ADDRESSES:-/dev/null}"
205     else
206         while read _i _rest ; do
207             if [ "$_ip" = "$_i" ] ; then
208                 printf "%s\t%s\n" "$_ip" "$_rest"
209             fi
210         done <"$CTDB_PER_IP_ROUTING_CONF"
211     fi
212 }
213
214 ip_has_configuration ()
215 {
216     _ip="$1"
217
218     [ -n "$(get_config_for_ip $_ip)" ]
219 }
220
221 add_routing_for_ip ()
222 {
223     _iface="$1"
224     _ip="$2"
225
226     # Do nothing if no config for this IP.
227     ip_has_configuration "$_ip" || return 0
228
229     ensure_table_id_for_ip "$_ip" || \
230         die "add_routing_for_ip: out of table ids in range $CTDB_PER_IP_ROUTING_TABLE_ID_LOW - $CTDB_PER_IP_ROUTING_TABLE_ID_HIGH"
231
232     _pref="$CTDB_PER_IP_ROUTING_RULE_PREF"
233     _table_id="${table_id_prefix}${_ip}"
234
235     del_routing_for_ip "$_ip" >/dev/null 2>&1
236
237     ip rule add from "$_ip" pref "$_pref" table "$_table_id" || \
238         die "add_routing_for_ip: failed to add rule for $_ip"
239
240     # Add routes to table for any lines matching the IP.
241     get_config_for_ip "$_ip" |
242     while read _i _dest _gw ; do
243         _r="$_dest ${_gw:+via} $_gw dev $_iface table $_table_id"
244         ip route add $_r || \
245             die "add_routing_for_ip: failed to add route: $_r"
246     done
247 }
248
249 del_routing_for_ip ()
250 {
251     _ip="$1"
252
253     _pref="$CTDB_PER_IP_ROUTING_RULE_PREF"
254     _table_id="${table_id_prefix}${_ip}"
255
256     # Do this unconditionally since we own any matching table ids.
257     # However, print a meaningful message if something goes wrong.
258     _cmd="ip rule del from $_ip pref $_pref table $_table_id"
259     _out=$($_cmd 2>&1) || \
260         cat <<EOF
261 WARNING: Failed to delete policy routing rule
262   Command "$_cmd" failed:
263   $_out
264 EOF
265     # This should never usually fail, so don't redirect output.
266     # However, it can fail when deleting a rogue IP, since there will
267     # be no routes for that IP.  In this case it should only fail when
268     # the rule deletion above has already failed because the table id
269     # is invalid.  Therefore, go to a little bit of trouble to indent
270     # the failure message so that it is associated with the above
271     # warning message and doesn't look too nasty.
272     ip route flush table "$_table_id" 2>&1 | sed -e 's@^.@  &@'
273 }
274
275 ######################################################################
276
277 flush_rules_and_routes ()
278 {
279         ip rule show |
280         while read _p _x _i _x _t ; do
281             # Remove trailing colon after priority/preference.
282             _p="${_p%:}"
283             # Only remove rules that match our priority/preference.
284             [ "$CTDB_PER_IP_ROUTING_RULE_PREF" = "$_p" ] || continue
285
286             echo "Removing ip rule for public address $_i for routing table $_t"
287             ip rule del from "$_i" table "$_t" pref "$_p"
288             ip route flush table "$_t" 2>/dev/null
289         done
290 }
291
292 # Add any missing routes.  Some might have gone missing if, for
293 # example, all IPs on the network were removed (possibly if the
294 # primary was removed).  If $1 is "force" then (re-)add all the
295 # routes.
296 add_missing_routes ()
297 {
298     $CTDB ip -v -X | {
299         read _x # skip header line
300
301         # Read the rest of the lines.  We're only interested in the
302         # "IP" and "ActiveInterface" columns.  The latter is only set
303         # for addresses local to this node, making it easy to skip
304         # non-local addresses.  For each IP local address we check if
305         # the relevant routing table is populated and populate it if
306         # not.
307         while IFS="|" read _x _ip _x _iface _x ; do
308             [ -n "$_iface" ] || continue
309             
310             _table_id="${table_id_prefix}${_ip}"
311             if [ -z "$(ip route show table $_table_id 2>/dev/null)" -o \
312                 "$1" = "force" ]  ; then
313                 add_routing_for_ip "$_iface" "$_ip"
314             fi
315         done
316     } || exit $?
317 }
318
319 # Remove rules/routes for addresses that we're not hosting.  If a
320 # releaseip event failed in an earlier script then we might not have
321 # had a chance to remove the corresponding rules/routes.
322 remove_bogus_routes ()
323 {
324     # Get a IPs current hosted by this node, each anchored with '@'.
325     _ips=$($CTDB ip -v -X | awk -F'|' 'NR > 1 && $4 != "" {printf "@%s@\n", $2}')
326
327     # x is intentionally ignored
328     # shellcheck disable=SC2034
329     ip rule show |
330     while read _p _x _i _x _t ; do
331         # Remove trailing colon after priority/preference.
332         _p="${_p%:}"
333         # Only remove rules that match our priority/preference.
334         [ "$CTDB_PER_IP_ROUTING_RULE_PREF" = "$_p" ] || continue
335         # Only remove rules for which we don't have an IP.  This could
336         # be done with grep, but let's do it with shell prefix removal
337         # to avoid unnecessary processes.  This falls through if
338         # "@${_i}@" isn't present in $_ips.
339         [ "$_ips" = "${_ips#*@${_i}@}" ] || continue
340
341         echo "Removing ip rule/routes for unhosted public address $_i"
342         del_routing_for_ip "$_i"
343     done
344 }
345
346 ######################################################################
347
348 service_reconfigure ()
349 {
350     add_missing_routes "force"
351     remove_bogus_routes
352
353     # flush our route cache
354     set_proc sys/net/ipv4/route/flush 1
355 }
356
357 ######################################################################
358
359 ctdb_check_args "$@"
360
361 ctdb_service_check_reconfigure
362
363 case "$1" in
364 startup)
365         flush_rules_and_routes
366
367         # make sure that we only respond to ARP messages from the NIC
368         # where a particular ip address is associated.
369         get_proc sys/net/ipv4/conf/all/arp_filter >/dev/null 2>&1 && {
370             set_proc sys/net/ipv4/conf/all/arp_filter 1
371         }
372         ;;
373
374 shutdown)
375         flush_rules_and_routes
376         clean_up_table_ids
377         ;;
378
379 takeip)
380         iface=$2
381         ip=$3
382         # maskbits included here so argument order is obvious
383         # shellcheck disable=SC2034
384         maskbits=$4
385
386         ensure_ipv4_is_valid_addr "$1" "$ip"
387         add_routing_for_ip "$iface" "$ip"
388
389         # flush our route cache
390         set_proc sys/net/ipv4/route/flush 1
391
392         $CTDB gratiousarp "$ip" "$iface"
393         ;;
394
395 updateip)
396         # oiface, maskbits included here so argument order is obvious
397         # shellcheck disable=SC2034
398         oiface=$2
399         niface=$3
400         ip=$4
401         # shellcheck disable=SC2034
402         maskbits=$5
403
404         ensure_ipv4_is_valid_addr "$1" "$ip"
405         add_routing_for_ip "$niface" "$ip"
406
407         # flush our route cache
408         set_proc sys/net/ipv4/route/flush 1
409
410         $CTDB gratiousarp "$ip" "$niface"
411         tickle_tcp_connections "$ip"
412         ;;
413
414 releaseip)
415         iface=$2
416         ip=$3
417         # maskbits included here so argument order is obvious
418         # shellcheck disable=SC2034
419         maskbits=$4
420
421         ensure_ipv4_is_valid_addr "$1" "$ip"
422         del_routing_for_ip "$ip"
423         ;;
424
425 ipreallocated)
426         add_missing_routes
427         remove_bogus_routes
428         ;;
429 esac
430
431 exit 0