Opennet Firmware
 Alle Dateien Funktionen Variablen Gruppen Seiten
openvpn.sh
gehe zur Dokumentation dieser Datei
1 ## @defgroup openvpn OpenVPN (allgemein)
2 ## @brief Vorbereitung, Konfiguration und Prüfung von VPN-Verbindunge (z.B. für Nutzertunnel oder UGW).
3 # Beginn der opnvpn-Doku-Gruppe
4 ## @{
5 
6 
7 VPN_DIR_TEST=/etc/openvpn/opennet_vpntest
8 OPENVPN_CONFIG_BASEDIR=/var/etc/openvpn
9 
10 
11 ## @fn enable_openvpn_service()
12 ## @brief Erzeuge eine funktionierende openvpn-Konfiguration (Datei + UCI).
13 ## @param service_name Name eines Dienstes
14 ## @details Die Konfigurationsdatei wird erzeugt und eine openvpn-uci-Konfiguration wird angelegt.
15 ## Falls zu diesem openvpn-Dienst kein Zertifikat oder kein Schlüssel gefunden wird, dann passiert nichts.
17  trap "error_trap enable_openvpn_service '$*'" $GUARD_TRAPS
18  local service_name="$1"
19  if ! openvpn_service_has_certificate_and_key "$service_name"; then
20  msg_info "Refuse to enable openvpn server ('$service_name'): missing key or certificate"
21  trap "" $GUARD_TRAPS && return 1
22  fi
23  local uci_prefix="openvpn.$service_name"
24  local config_file=$(get_service_value "$service_name" "config_file")
25  # zukuenftige config-Datei referenzieren
26  update_vpn_config "$service_name"
27  # zuvor ankuendigen, dass zukuenftig diese uci-Konfiguration an dem Dienst haengt
28  service_add_uci_dependency "$service_name" "$uci_prefix"
29  # lege die uci-Konfiguration an und aktiviere sie
30  uci set "${uci_prefix}=openvpn"
31  uci set "${uci_prefix}.enabled=1"
32  uci set "${uci_prefix}.config=$config_file"
33  apply_changes openvpn
34 }
35 
36 
37 ## @fn update_vpn_config()
38 ## @brief Schreibe eine openvpn-Konfigurationsdatei.
39 ## @param service_name Name eines Dienstes
41  trap "error_trap update_vpn_config '$*'" $GUARD_TRAPS
42  local service_name="$1"
43  local config_file=$(get_service_value "$service_name" "config_file")
44  service_add_file_dependency "$service_name" "$config_file"
45  # Konfigurationsdatei neu schreiben
46  mkdir -p "$(dirname "$config_file")"
47  get_openvpn_config "$service_name" >"$config_file"
48 }
49 
50 
52 ## @brief Löschung einer openvpn-Verbindung
53 ## @param service_name Name eines Dienstes
54 ## @details Die UCI-Konfiguration, sowie alle anderen mit der Verbindung verbundenen Elemente werden entfernt.
55 ## Die openvpn-Verbindung bleibt bestehen, bis zum nächsten Aufruf von 'apply_changes openvpn'.
57  trap "error_trap disable_openvpn_service '$*'" $GUARD_TRAPS
58  local service_name="$1"
59  # Abbruch, falls es keine openvpn-Instanz gibt
60  [ -z "$(uci_get "openvpn.$service_name")" ] && return 0
61  # openvpn wird automatisch neugestartet
62  cleanup_service_dependencies "$service_name"
63  # nach einem reboot sind eventuell die dependencies verlorengegangen - also loeschen wir manuell
64  uci_delete "openvpn.$service_name"
65 }
66 
67 
68 ## @fn is_openvpn_service_active()
69 ## @brief Prüfung ob eine openvpn-Verbindung besteht.
70 ## @param service_name Name eines Dienstes
71 ## @details Die Prüfung wird anhand der PID-Datei und der Gültigkeit der enthaltenen PID vorgenommen.
73  local service_name="$1"
74  local pid_file
75  local pid
76  # existiert ein VPN-Eintrag?
77  [ -z "$(uci_get "openvpn.$service_name")" ] && trap "" $GUARD_TRAPS && return 1
78  # gibt es einen Verweis auf eine passende PID-Datei?
79  check_pid_file "$(get_service_value "$service_name" "pid_file")" "openvpn" && return 0
80  trap "" $GUARD_TRAPS && return 1
81 }
82 
83 
84 ## @fn _change_openvpn_config_setting()
85 ## @brief Ändere eine Einstellung in einer openvpn-Konfigurationsdatei.
86 ## @param config_file Name der Konfigurationsdatei.
87 ## @param config_key Name der openvpn-Einstellung.
88 ## @param config_value Neuer Inhalt der Einstellung - die Einstellung wird gelöscht, falls dieser Parameter fehlt oder leer ist.
89 ## @attention OpenVPN-Optionen ohne Parameter (z.B. --mtu-test) können nicht mittels dieser Funktion gesetzt werden.
91  local config_file="$1"
92  local config_key="$2"
93  local config_value="${3:-}"
94  sed -i "/^$config_key[\t ]/d" "$config_file"
95  [ -n "$config_value" ] && echo "$config_key $config_value" >>"$config_file"
96  return 0
97 }
98 
99 
100 ## @fn get_openvpn_config()
101 ## @brief liefere openvpn-Konfiguration eines Dienstes zurück
102 ## @param service_name Name eines Dienstes
104  trap "error_trap get_openvpn_config '$*'" $GUARD_TRAPS
105  local service_name="$1"
106  local remote=$(get_service_value "$service_name" "host")
107  local port=$(get_service_value "$service_name" "port")
108  local protocol=$(get_service_value "$service_name" "protocol")
109  [ "$protocol" = "tcp" ] && protocol=tcp-client
110  local template_file=$(get_service_value "$service_name" "template_file")
111  local pid_file=$(get_service_value "$service_name" "pid_file")
112  # schreibe die Konfigurationsdatei
113  echo "# automatically generated by $0"
114  echo "remote $remote $port"
115  echo "proto $protocol"
116  echo "writepid $pid_file"
117  cat "$template_file"
118  # sicherstellen, dass die Konfigurationsdatei mit einem Zeilenumbruch endet (fuer "echo >> ...")
119  echo
120 }
121 
122 
123 ## @fn verify_vpn_connection()
124 ## @brief Prüfe einen VPN-Verbindungsaufbau
125 ## @param service_name Name eines Dienstes
126 ## @param key [optional] Schluesseldatei: z.B. $VPN_DIR/on_aps.key
127 ## @param cert [optional] Zertifikatsdatei: z.B. $VPN_DIR/on_aps.crt
128 ## @param ca-cert [optional] CA-Zertifikatsdatei: z.B. $VPN_DIR/opennet-ca.crt
129 ## @returns Exitcode=0 falls die Verbindung aufgebaut werden konnte
131  trap "error_trap verify_vpn_connection '$*'" $GUARD_TRAPS
132  local service_name="$1"
133  local key_file=${2:-}
134  local cert_file=${3:-}
135  local ca_file=${4:-}
136  local config_file=$(mktemp -t "VERIFY-${service_name}-XXXXXXX")
137  local log_file="$(get_service_log_filename "$service_name" "openvpn" "verify")"
138  local file_opts
139  local wan_dev
140  local hostname
141  local status_output
142 
143  msg_debug "start vpn test of $service_name"
144 
145 
146  # erstelle die config-Datei
147  (
148  # filtere Einstellungen heraus, die wir ueberschreiben wollen
149  # nie die echte PID-Datei ueberschreiben (falls ein Prozess laeuft)
150  get_openvpn_config "$service_name"
151 
152  # some openvpn options:
153  # ifconfig-noexec: we do not want to configure a device (and mess up routing tables)
154  # route-noexec: keinerlei Routen hinzufuegen
155  echo "ifconfig-noexec"
156  echo "route-noexec"
157 
158  # some timing options:
159  # inactive: close connection after 15s without traffic
160  # ping-exit: close connection after 15s without a ping from the other side (which is probably disabled)
161  echo "inactive 15 1000000"
162  echo "ping-exit 15"
163 
164  # other options:
165  # verb: verbose level 3 is required for the TLS messages
166  # nice: testing is not too important
167  # resolv-retry: fuer ipv4/ipv6-Tests sollten wir mehrere Versuche zulassen
168  echo "verb 4"
169  echo "nice 3"
170  echo "resolv-retry 3"
171 
172  # prevent a real connection (otherwise we may break our current vpn tunnel):
173  # tls-exit: stop immediately after tls handshake failure
174  # ns-cert-type: enforce a connection against a server certificate (instead of peer-to-peer)
175  echo "tls-exit"
176  echo "ns-cert-type server"
177 
178  ) >"$config_file"
179 
180  # kein Netzwerkinterface erzeugen
181  _change_openvpn_config_setting "$config_file" "dev" "null"
182  # keine PID-Datei anlegen
183  _change_openvpn_config_setting "$config_file" "writepid" ""
184  # keine Netzwerkkonfiguration via up/down
185  _change_openvpn_config_setting "$config_file" "up" ""
186  _change_openvpn_config_setting "$config_file" "down" ""
187  # TLS-Pruefung immer fehlschlagen lassen
188  _change_openvpn_config_setting "$config_file" "tls-verify" "/bin/false"
189  # Log-Datei anlegen
190  _change_openvpn_config_setting "$config_file" "log" "$log_file"
191 
192  # nur fuer tcp-Verbindungen (ipv4/ipv6)
193  # connect-retry: Sekunden Wartezeit zwischen Versuchen
194  # connect-timeout: Dauer eines Versuchs
195  # connect-retry-max: Anzahl moeglicher Wiederholungen
196  if grep -q "^proto[ \t]\+tcp" "$config_file"; then
197  echo "connect-retry 1"
198  echo "connect-timeout 15"
199  echo "connect-retry-max 1"
200  fi >>"$config_file"
201 
202  # Schluessel und Zertifikate bei Bedarf austauschen
203  [ -n "$key_file" ] && \
204  _change_openvpn_config_setting "$config_file" "key" "$key_file"
205  [ -n "$cert_file" ] && \
206  _change_openvpn_config_setting "$config_file" "cert" "$cert_file"
207  [ -n "$ca_file" ] && \
208  _change_openvpn_config_setting "$config_file" "ca" "$ca_file"
209 
210  # Aufbau der VPN-Verbindung bis zum Timeout oder bis zum Verbindungsabbruch via "tls-exit" (/bin/false)
211  openvpn --config "$config_file" || true
212  # read the additional options from the config file (for debug purposes)
213  file_opts=$(grep -v "^$" "$config_file" | grep -v "^#" | sed 's/^/--/' | tr '\n' ' ')
214  rm -f "$config_file"
215  grep -q "Initial packet" "$log_file" && return 0
216  msg_debug "openvpn test failed: openvpn $file_opts"
217  trap "" $GUARD_TRAPS && return 1
218 }
219 
220 
222 ## @brief Prüfe ob das Zertifikat eines openvpn-basierten Diensts existiert.
223 ## @returns exitcode=0 falls das Zertifikat existiert
224 ## @details Falls der Ort der Zertifikatsdatei nicht zweifelsfrei ermittelt
225 ## werden kann, dann liefert die Funktion "wahr" zurück.
227  local service_name="$1"
228  local cert_file
229  local key_file
230  local config_template=$(get_service_value "$service_name" "template")
231  # im Zweifelsfall (kein Template gefunden) liefern wir "wahr"
232  [ -z "$config_template" ] && return 0
233  # Verweis auf lokale config-Datei (keine uci-basierte Konfiguration)
234  if [ -e "$config_template" ]; then
235  cert_file=$(_get_file_dict_value "cert" "$config_template")
236  key_file=$(_get_file_dict_value "key" "$config_template")
237  else
238  # im Zweifelsfall: liefere "wahr"
239  return 0
240  fi
241  # das Zertifikat scheint irgendwie anders konfiguriert zu sein - im Zeifelsfall: OK
242  [ -z "$cert_file" -o -z "$key_file" ] && return 0
243  # existiert die Datei?
244  [ -e "$cert_file" -a -e "$key_file" ] && return 0
245  trap "" $GUARD_TRAPS && return 1
246 }
247 
248 
249 ## @fn submit_csr_via_http()
250 ## @param upload_url URL des Upload-Formulars
251 ## @param csr_file Dateiname einer Zertifikatsanfrage
252 ## @brief Einreichung einer Zertifikatsanfrage via http (bei http://ca.on)
253 ## @details Eine Prüfung des Ergebniswerts ist aufgrund des auf menschliche Nutzer ausgerichteten Interface nicht so leicht moeglich.
254 ## @todo Umstellung vom Formular auf die zu entwickelnde API
255 ## @returns Das Ergebnis ist die html-Ausgabe des Upload-Formulars.
257  trap "error_trap submit_csr_via_http '$*'" $GUARD_TRAPS
258  # upload_url: z.B. http://ca.on/csr/csr_upload.php
259  local upload_url="$1"
260  local csr_file="$2"
261  local helper="${3:-}"
262  local helper_email="${4:-}"
263  curl -q --silent --capath /etc/ssl/certs --form "file=@$csr_file" --form "opt_name=$helper" --form "opt_mail=$helper_email" "$upload_url" && return 0
264  # ein technischer Verbindungsfehler trat auf
265  trap "" $GUARD_TRAPS && return 1
266 }
267 
268 
269 ## @fn has_openvpn_credentials_by_template()
270 ## @brief Prüft, ob der Nutzer bereits einen Schlüssel und ein Zertifikat angelegt hat.
271 ## @param template_file Name einer openvpn-Konfigurationsdatei (oder einer Vorlage). Aus dieser Datei werden "cert"- und "key"-Werte entnommen.
272 ## @returns Liefert "wahr", falls Schlüssel und Zertifikat vorhanden sind oder falls in irgendeiner Form Unklarheit besteht.
274  trap "error_trap has_openvpn_credentials_by_template '$*'" $GUARD_TRAPS
275  local template_file="$1"
276  local cert_file=$(_get_file_dict_value "cert" "$template_file")
277  local key_file=$(_get_file_dict_value "key" "$template_file")
278  # im Zweifel: liefere "wahr"
279  [ -z "$key_file" -o -z "$cert_file" ] && return 0
280  # beide Dateien existieren
281  [ -e "$key_file" -a -e "$cert_file" ] && return 0
282  trap "" $GUARD_TRAPS && return 1
283 }
284 
285 
286 ## @fn log_openvpn_events_and_disconnect_if_requested()
287 ## @brief Allgemeines Ereignisbehandlung fuer openvpn-Verbindungen: Logging und eventuell Dienst-Bereinigung (nur für "down").
288 ## @details Alle Informationen (bis auf das Log-Ziel) werden aus den Umgebungsvariablen gezogen, die openvpn in
289 ## seinen Ereignisskripten setzt.
291  local log_target="$1"
292  # die config-Datei enthaelt den Dienst-Namen
293  local service_name=$(basename "${config%.conf}")
294  local pid_file=$(get_service_value "$service_name" "pid_file")
295  case "$script_type" in
296  up)
297  append_to_custom_log "$log_target" "up" "Connecting to ${remote_1}:${remote_port_1}"
298  ;;
299  down)
300  # der openwrt-Build von openvpn setzt wohl leider nicht die "time_duration"-Umgebungsvariable
301  [ -z "${time_duration:-}" ] && time_duration=$(($(date +%s) - $daemon_start_time))
302  # Verbindungsverlust durch fehlende openvpn-Pings?
303  if [ "${signal:-}" = "ping-restart" ]; then
304  append_to_custom_log "$log_target" "down" \
305  "Lost connection with ${remote_1}:${remote_port_1} after ${time_duration}s"
306  # Verbindung trennen
307  set_service_value "$service_name" "status" "n"
308  disable_openvpn_service "$service_name"
309  [ -n "$pid_file" ] && rm -f "$pid_file" || true
310  else
311  append_to_custom_log "$log_target" "down" \
312  "Closing connection with ${remote_1}:${remote_port_1} after ${time_duration}s"
313  fi
314  ;;
315  *)
316  append_to_custom_log "$log_target" "other" "${remote_1}:${remote_port_1}"
317  ;;
318  esac
319 }
320 
321 
322 ## @fn prepare_openvpn_service()
323 ## @param Name eines Diensts
324 ## @param template_file Name einer openvpn-Konfigurationsvorlage
325 ## @brief Erzeuge oder aktualisiere einen OpenVPN-Dienst
327  trap "error_trap prepare_openvpn_service '$*'" $GUARD_TRAPS
328  local service_name="$1"
329  local template_file="$2"
330  local pid_file="/var/run/${service_name}.pid"
331  local config_file="$OPENVPN_CONFIG_BASEDIR/${service_name}.conf"
332  set_service_value "$service_name" "template_file" "$template_file"
333  set_service_value "$service_name" "config_file" "$config_file"
334  set_service_value "$service_name" "pid_file" "$pid_file"
335 }
336 
337 
338 ## @fn openvpn_get_mtu()
339 ## @brief Ermittle die MTU auf dem Weg zum Anbieter des Diensts.
340 ## @details The output can be easily parsed via 'cut'. Even the full status output of openvpn is safe for parsing since potential tabulator characters are removed.
341 ## @returns One line consisting of five fields separated by tab characters is returned (tried_to_remote real_to_remote tried_from_remote real_from_remote full_status_output). Failed tests are indicated by an empty result.
342 openvpn_get_mtu() {
343  trap "error_trap openvpn_get_mtu '$*'" $GUARD_TRAPS
344  local service_name="$1"
345  local config_file=$(mktemp -t "MTU-${service_name}-XXXXXXX")
346  local pid_file="$(mktemp)"
347  local log_file="$(get_service_log_filename "$service_name" "openvpn" "mtu")"
348  local host=$(get_service_value "$service_name" "host")
349  local filename
350  local key
351  local default
352 
353  (
354  get_openvpn_config "$service_name"
355  # kein Netzwerk konfigurieren
356  echo "ifconfig-noexec"
357  echo "route-noexec"
358  ) >"$config_file"
359 
360  # kein Netzwerkinterface, keine pid-Datei
361  _change_openvpn_config_setting "$config_file" "dev" "null"
362  _change_openvpn_config_setting "$config_file" "writepid" "$pid_file"
363 
364  # Log-Datei anlegen
365  _change_openvpn_config_setting "$config_file" "log" "$log_file"
366  _change_openvpn_config_setting "$config_file" "verb" "4"
367 
368  # keine Skripte
369  _change_openvpn_config_setting "$config_file" "up" ""
370  _change_openvpn_config_setting "$config_file" "down" ""
371 
372  # Test-Schluessel verwenden, falls kein echter Schluessel vorhanden ist
373  # TOOD: Aktuell funktioniert der MTU-Test nicht mit dem Testzertifikat (es wurde von der falschen CA ausgestellt)
374  while read key default; do
375  filename="$(_get_file_dict_value "key" "$config_file")"
376  [ -e "$filename" ] || _change_openvpn_config_setting "$config_file" "$key" "$default"
377  done <<- EOF
378 ca $VPN_DIR_TEST/opennet-ca.crt
379 key $VPN_DIR_TEST/on_aps.key
380 cert $VPN_DIR_TEST/on_aps.crt
381 EOF
382 
383  openvpn --mtu-test --config "$config_file" 2>&1 &
384  # warte auf den Startvorgang
385  sleep 3
386  local pid="$(cat "$pid_file")"
387  local wait_loops=40
388  local mtu_out
389  local mtu_out_filtered
390  while [ "$wait_loops" -gt 0 ]; do
391  # keine Fehlermeldungen (-s) falls die Log-Datei noch nicht existiert
392  mtu_out=$(grep -s "MTU test completed" "$log_file" || true)
393  # for example
394  # Thu Jul 3 22:23:01 2014 NOTE: Empirical MTU test completed [Tried,Actual] local->remote=[1573,1573] remote->local=[1573,1573]
395  if [ -n "$mtu_out" ]; then
396  # Ausgabe der vier Zahlen getrennt durch Tabulatoren
397  mtu_out_filtered="$(echo "$mtu_out" | tr '[' ',' | tr ']' ',')"
398  # Leider koennen wir nicht alle Felder auf einmal ausgeben (tab-getrennt),
399  # da das busybox-cut nicht den --output-delimiter unterstützt.
400  echo "$mtu_out_filtered" | cut -d , -f 5 | tr '\n' '\t'
401  echo "$mtu_out_filtered" | cut -d , -f 6 | tr '\n' '\t'
402  echo "$mtu_out_filtered" | cut -d , -f 8 | tr '\n' '\t'
403  echo "$mtu_out_filtered" | cut -d , -f 9 | tr '\n' '\t'
404  # wir ersetzen alle eventuell vorhandenen Tabulatoren in der Statusausgabe - zur Vereinfachung des Parsers
405  echo -n "$mtu_out" | tr '\t' ' '
406  break
407  fi
408  if [ -z "$pid" -o ! -d "/proc/$pid" ]; then
409  msg_info "failed to verify MTU resctrictions for '$host'"
410  break
411  fi
412  sleep 10
413  : $((wait_loops--))
414  done
415  # sicherheitshalber brechen wir den Prozess ab und loeschen alle Dateien
416  kill "$pid" >/dev/null 2>&1 || true
417  rm -f "$config_file" "$pid_file"
418  # ist der Zaehler abgelaufen?
419  [ "$wait_loops" -eq 0 ] && msg_info "timeout for openvpn_get_mtu '$host' - aborting."
420  return 0
421 }
422 
423 
424 ## @fn cleanup_stale_openvpn_services()
425 ## @brief Beräumung liegengebliebener openvpn-Konfigurationen, sowie Deaktivierung funktionsunfähiger Verbindungen.
426 ## @details Verwaiste openvpn-Konfigurationen können aus zwei Grunden auftreten:
427 ## 1) nach einem reboot wurde nicht du zuletzt aktive openvpn-Verbindung ausgewählt - somit bleibt der vorher aktive uci-Konfigurationseintrag erhalten
428 ## 2) ein VPN-Verbindungsaufbau scheitert und hinterlässt einen uci-Eintrag, eine PID-Datei, jedoch keinen laufenden Prozess
430  trap "error_trap cleanup_stale_openvpn_services '$*'" $GUARD_TRAPS
431  local service_name
432  local config_file
433  local pid_file
434  local uci_prefix
435  find_all_uci_sections openvpn openvpn | while read uci_prefix; do
436  config_file=$(uci_get "${uci_prefix}.config")
437  # Keine config-Datei? Keine von uns verwaltete Konfiguration ...
438  [ -z "$config_file" ] && continue
439  service_name="${uci_prefix#openvpn.}"
440  # Es scheint sich um eine von uns verwaltete Verbindung zu handeln.
441  # Das "pid_file"-Attribut ist nicht persistent - nach einem Neustart kann es also leer sein.
442  pid_file=$(get_service_value "$service_name" "pid_file")
443  # Falls die config-Datei oder die pid-Datei fehlt, dann ist es ein reboot-Fragment. Wir löschen die Überreste.
444  if [ ! -e "$config_file" -o -z "$pid_file" -o ! -e "$pid_file" ]; then
445  msg_info "Removing a reboot-fragment of a previously used openvpn connection: $service_name"
446  disable_openvpn_service "$service_name"
447  elif check_pid_file "$pid_file" "openvpn"; then
448  # Prozess läuft - alles gut
449  true
450  else
451  # Falls die PID-Datei existiert, jedoch veraltet ist (kein dazugehöriger Prozess läuft), dann
452  # schlug der Verbindungsaufbau fehlt (siehe "tls-exit" und "single-session").
453  # Wir markieren die Verbindung als kaputt.
454  msg_info "Marking a possibly interrupted openvpn connection as broken: $service_name"
455  set_service_value "$service_name" "status" "n"
456  disable_openvpn_service "$service_name"
457  fi
458  done
459  apply_changes openvpn
460 }
461 
462 # Ende der openvpn-Doku-Gruppe
463 ## @}