44d9ffe40fadd925aa262b9247aa24f1195e3edb
[distro-setup] / dynamic-ip-update
1 #!/bin/bash
2 # I, Ian Kelling, follow the GNU license recommendations at
3 # https://www.gnu.org/licenses/license-recommendations.en.html. They
4 # recommend that small programs, < 300 lines, be licensed under the
5 # Apache License 2.0. This file contains or is part of one or more small
6 # programs. If a small program grows beyond 300 lines, I plan to switch
7 # its license to GPL.
8
9 # Copyright 2024 Ian Kelling
10
11 # Licensed under the Apache License, Version 2.0 (the "License");
12 # you may not use this file except in compliance with the License.
13 # You may obtain a copy of the License at
14
15 # http://www.apache.org/licenses/LICENSE-2.0
16
17 # Unless required by applicable law or agreed to in writing, software
18 # distributed under the License is distributed on an "AS IS" BASIS,
19 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20 # See the License for the specific language governing permissions and
21 # limitations under the License.
22
23
24 set -e; . /usr/local/lib/bash-bear; set +e
25 [[ $EUID == 0 ]] || exec sudo -E "${BASH_SOURCE[0]}" "$@"
26
27 this_file="$(readlink -f -- "${BASH_SOURCE[0]}")"
28 readonly this_file
29 this_dir="${this_file%/*}"
30 readonly this_dir
31 cd "$this_dir"
32
33 usage() {
34 cat <<EOF
35 Usage: ${0##*/} [-f]
36 Update ip in remote nameserver.
37
38 -f Force update even if ip hasn't changed.
39 -h|--help Print help and exit.
40
41 Note: Uses util-linux getopt option parsing: spaces between args and
42 options, short options can be combined, options before args.
43 EOF
44 exit $1
45 }
46
47 ##### begin command line parsing ########
48
49 # ensure we can handle args with spaces or empty.
50 ret=0; getopt -T || ret=$?
51 [[ $ret == 4 ]] || { echo "Install util-linux for enhanced getopt" >&2; exit 1; }
52
53 force=false # default
54 temp=$(getopt -l help hf "$@") || usage 1
55 eval set -- "$temp"
56 while true; do
57 case $1 in
58 -f) force=true ;;
59 --) shift; break ;;
60 *) echo "$0: unexpected args: $*" >&2 ; usage 1 ;;
61 esac
62 shift
63 done
64
65 ##### end command line parsing ########
66
67 main() {
68
69 fqdn=$(hostname -f)
70 domaintmp=${fqdn#*.}
71 hostnametmp=${fqdn%%.*}
72 # i for internet
73 fqdn=${hostnametmp}i.${domaintmp}
74
75 up4=false
76
77 if ! tmp=$(ip -4 route get 85.119.83.50 2>/dev/null); then
78 # our internet is down
79 if [[ $INVOCATION_ID ]]; then
80 return 0
81 else
82 echo $0: failed to get route, giving up
83 exit 0
84 fi
85 fi
86 read -r _ _ gateway _ ifdev _ <<<"$tmp"
87
88 case $gateway in
89 10.2.0.1)
90 dyndomain=b8.nz
91 # This domain is for any case where we want some different
92 # configuration based on lan vs wan. For right now, the only use
93 # is for ssh config to use port forwarding ports on the wan
94 # domain.
95 dyndomain_internet=i.b8.nz
96 ;;
97 *)
98 return 0
99 ;;
100 esac
101
102 # We check if we are at home by testing gateway ssh
103 # fingerprint. However, if we found in the past that we are, I dont
104 # like to spam its logs with ssh login attempts, so just check if our
105 # gateway interface has an increasing amount of packets sent +
106 # received from last time.
107 athome=false
108 if [[ -s /dev/shm/dynamic-ip-update-state ]]; then
109 oldbytes=$(cat /dev/shm/dynamic-ip-update-state)
110 newbytes=$(awk '$1 == "'$ifdev':" {print $2 + $10}' /proc/net/dev)
111 if [[ $oldbytes == [1-9]* ]] && (( newbytes >= oldbytes )); then
112 athome=true
113 printf "%s\n" "$newbytes" >/dev/shm/dynamic-ip-update-state
114 fi
115 fi
116 if ! $athome && timeout -s 9 5 ssh-keyscan -p 2220 -t rsa $gateway 2>/dev/null | grep -qFx "[$gateway]:2220 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCH+/h1dGEfKEusBblndU2e6QT4wLCm5+yqr/sqh/0X9YfjR7BfWWm8nNmuP55cYc+Wuf5ljB1H1acXEcsl1y8e0j3agHfF0V74FE1N1zz5nn2Ep8NHnmqgEhza38ZxMPh+4p3X7zklEKU7+3SzybKBi8sg0wLzlS2LM0JaUN80zR2sK11Kye3dURUXPk78u5wodOkgcEYRwSYaDMJlUzWP+poRXIDJwFaMQnwmxbl/c84yOyaU0x/d6hFwoRscWecihX+vvBNeSyxR4xr2HDOyUWwJkctyAgt2p7w3tfkXOKcCRzTAjGVIMQLTvo0sG/yJbcyHoEFdFybCsgDvfyYn"; then
117 athome=true
118 awk '$1 == "'$ifdev':" {print $2 + $10}' /proc/net/dev > /dev/shm/dynamic-ip-update-state
119 fi
120
121
122 if $athome; then
123 if ! cur4="$(dig +short $dyndomain @iankelling.org | tail -1)"; then
124 if [[ ! $INVOCATION_ID ]]; then
125 echo "$0: dig failed. internet looks down. giving up"
126 fi
127 return 0
128 fi
129 if ip4=$(curl --connect-timeout 10 -s4 https://iankelling.org/cgi/pubip); then
130 if $force || [[ $cur4 && $ip4 && $cur4 != "$ip4" ]]; then
131 up4=true # update ipv4
132 fi
133 fi
134 fi
135
136 # may not be set yet so allow fail
137 cur6="$(host -4 -t aaaa $fqdn iankelling.org | sed -rn 's/.*has IPv6 address (.*)/\1/p;T;q')" ||:
138
139 up6=false
140
141 out6=$(curl --connect-timeout 10 -s6 https://iankelling.org/cgi/pubip) ||: # failure allowed if we have no ipv6
142
143 if [[ $out6 ]]; then
144 dev=$(ip -o a show to $out6 | awk '{print $2}')
145 # we use slaac with privacy extension, so get our less private more permanent address
146 mac=$(cat /sys/class/net/$dev/address)
147
148 IFS=: read -ra f <<<$mac; set -- ${f[@]}
149 ip6=${out6%:*:*:*:*}:$(printf %x $((0x$1 + 2)))$2:$3'ff:fe'$4:$5$6
150 # in case we aren't using slaac
151 if ! ip a | grep "^ *inet6 $ip6/" &>/dev/null; then
152 ip6=$out6
153 fi
154 fi
155
156 if $force || [[ $cur6 != "$ip6" ]]; then
157 up6=true
158 fi
159
160 # if we failed to get our ipv6 addr, we probably have ipv6
161 # connectivity problem.
162 if [[ ! $ip6 ]]; then
163 ip_arg=-4
164 fi
165
166 if ! $up4 && ! $up6; then
167 return 0
168 fi
169
170 # note, a simpler way to do this would be to ssh and use
171 # "${SSH_CLIENT%% *}
172 # to update bind if needed.
173
174 tmpf=$(mktemp)
175 cat >>$tmpf <<EOF
176 server iankelling.org
177 zone b8.nz
178 EOF
179
180 if $up4; then
181 cat >>$tmpf <<EOF
182 update delete $dyndomain. A
183 update add $dyndomain. 300 A $ip4
184 update delete $dyndomain_internet. A
185 update add $dyndomain_internet. 300 A $ip4
186 EOF
187 fi
188
189 if $up6; then
190 if [[ $ip6 ]]; then
191 cat >>$tmpf <<EOF
192 update delete $fqdn. AAAA
193 update add $fqdn. 60 AAAA $ip6
194 EOF
195 else
196 cat >>$tmpf <<EOF
197 update delete $fqdn. AAAA
198 EOF
199 fi
200 fi
201
202 cat >>$tmpf <<EOF
203 show
204 send
205 answer
206 quit
207 EOF
208
209 chronic nsupdate $ip_arg -k /p/c/machine_specific/vps/filesystem/etc/bind/Kb8.nz.*.private <$tmpf || nsupdate_fails=$((nsupdate_fails + 1))
210 sed -i 's/^server .*/server bk.b8.nz/' $tmpf
211 chronic nsupdate $ip_arg -k /p/c/machine_specific/vps/filesystem/etc/bind/Kb8.nz.*.private <$tmpf || nsupdate_fails=$((nsupdate_fails + 1))
212 if (( nsupdate_fails > nsupdate_fail_limit )); then
213 echo error: nsupdate is persistently failing >&2
214 exit 1
215 fi
216 rm -f $tmpf
217 }
218
219 loop-main() {
220 while true; do
221 main
222 sleep 30
223 done
224 }
225
226 nsupdate_fails=0
227 if [[ $INVOCATION_ID ]]; then
228 nsupdate_fail_limit=10
229 loop-main
230 else
231 nsupdate_fail_limit=0
232 main
233 fi
234
235 exit 0
236
237
238 # # # persistent initial setup for this:
239 # # # create files in /a/c/machine_specific/vps/filesystem/etc/bind
240 # # # note, conflink also does some group ownership stuff.
241 # mkc /p/c/machine_specific/vps/filesystem/etc/bind
242 # sudo dnssec-keygen -a HMAC-SHA512 -b 512 -n HOST b8.nz
243 # user=$(id -un)
244 # sudo chown $user:$user *
245
246
247 # f=key.b8.nz
248 # cat >$tmpf <<EOF
249 # key b8.nz. {
250 # algorithm HMAC-SHA512;
251 # secret "$(awk '$1 == "Key:" {print $2}' Kb8.nz.*.private)";
252 # };
253 # EOF
254
255 # chmod 640 [kK]*
256
257 # # push here?
258 # #myunison -ob li
259 # #ssh li conflink
260 # ssh li.b8.nz systemctl reload named
261
262
263 # # b8.nz has address 65.96.178.16
264 # # b8.nz has IPv6 address 2601:197:600:6efb:82fa:5bff:fe1c:6ecf