1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018 |
- ;;; GNU Guix --- Functional package management for GNU
- ;;; Copyright © 2017 Julien Lepiller <julien@lepiller.eu>
- ;;; Copyright © 2017 Clément Lassieur <clement@lassieur.org>
- ;;; Copyright © 2017 Mathieu Othacehe <m.othacehe@gmail.com>
- ;;; Copyright © 2021 Guillaume Le Vaillant <glv@posteo.net>
- ;;; Copyright © 2021 Solene Rapenne <solene@perso.pw>
- ;;; Copyright © 2021 Domagoj Stolfa <ds815@gmx.com>
- ;;; Copyright © 2021 Tobias Geerinckx-Rice <me@tobias.gr>
- ;;; Copyright © 2021 Raghav Gururajan <rg@raghavgururajan.name>
- ;;; Copyright © 2021 jgart <jgart@dismail.de>
- ;;; Copyright © 2021 Nathan Dehnel <ncdehnel@gmail.com>
- ;;; Copyright © 2022 Cameron V Chaparro <cameron@cameronchaparro.com>
- ;;; Copyright © 2022 Timo Wilken <guix@twilken.net>
- ;;; Copyright © 2023 Maxim Cournoyer <maxim.cournoyer@gmail.com>
- ;;;
- ;;; This file is part of GNU Guix.
- ;;;
- ;;; GNU Guix 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.
- ;;;
- ;;; GNU Guix 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 GNU Guix. If not, see <http://www.gnu.org/licenses/>.
- (define-module (gnu services vpn)
- #:use-module (gnu services)
- #:use-module (gnu services configuration)
- #:use-module (gnu services dbus)
- #:use-module (gnu services mcron)
- #:use-module (gnu services shepherd)
- #:use-module (gnu system shadow)
- #:use-module (gnu packages admin)
- #:use-module (gnu packages vpn)
- #:use-module (guix modules)
- #:use-module (guix packages)
- #:use-module (guix records)
- #:use-module (guix gexp)
- #:use-module (guix i18n)
- #:use-module (guix deprecation)
- #:use-module (srfi srfi-1)
- #:use-module (ice-9 format)
- #:use-module (ice-9 match)
- #:use-module (ice-9 regex)
- #:export (openvpn-client-service ; deprecated
- openvpn-server-service ; deprecated
- openvpn-client-service-type
- openvpn-server-service-type
- openvpn-client-configuration
- openvpn-server-configuration
- openvpn-remote-configuration
- openvpn-ccd-configuration
- generate-openvpn-client-documentation
- generate-openvpn-server-documentation
- strongswan-configuration
- strongswan-service-type
- wireguard-peer
- wireguard-peer?
- wireguard-peer-name
- wireguard-peer-endpoint
- wireguard-peer-allowed-ips
- wireguard-peer-public-key
- wireguard-peer-preshared-key
- wireguard-peer-keep-alive
- wireguard-configuration
- wireguard-configuration?
- wireguard-configuration-wireguard
- wireguard-configuration-interface
- wireguard-configuration-addresses
- wireguard-configuration-port
- wireguard-configuration-dns
- wireguard-configuration-monitor-ips?
- wireguard-configuration-monitor-ips-interval
- wireguard-configuration-private-key
- wireguard-configuration-peers
- wireguard-configuration-pre-up
- wireguard-configuration-post-up
- wireguard-configuration-pre-down
- wireguard-configuration-post-down
- wireguard-configuration-table
- wireguard-service-type))
- ;;;
- ;;; Bitmask.
- ;;;
- (define-public bitmask-service-type
- (service-type
- (name 'bitmask)
- (description "Setup the @uref{https://bitmask.net, Bitmask} VPN application.")
- (default-value bitmask)
- (extensions
- (list
- ;; Add bitmask to the system profile.
- (service-extension profile-service-type list)
- ;; Configure polkit policy of bitmask.
- (service-extension polkit-service-type list)))))
- ;;;
- ;;; OpenVPN.
- ;;;
- (define (uglify-field-name name)
- (match name
- ('verbosity "verb")
- (_ (let ((str (symbol->string name)))
- (if (string-suffix? "?" str)
- (substring str 0 (1- (string-length str)))
- str)))))
- (define (serialize-field field-name val)
- (if (eq? field-name 'pid-file)
- (format #t "")
- (format #t "~a ~a\n" (uglify-field-name field-name) val)))
- (define serialize-string serialize-field)
- (define-maybe string)
- (define (serialize-boolean field-name val)
- (if val
- (serialize-field field-name "")
- (format #t "")))
- (define (ip-mask? val)
- (and (string? val)
- (if (string-match "^([0-9]+\\.){3}[0-9]+ ([0-9]+\\.){3}[0-9]+$" val)
- (let ((numbers (string-tokenize val char-set:digit)))
- (all-lte numbers (list 255 255 255 255 255 255 255 255)))
- #f)))
- (define serialize-ip-mask serialize-string)
- (define-syntax define-enumerated-field-type
- (lambda (x)
- (define (id-append ctx . parts)
- (datum->syntax ctx (apply symbol-append (map syntax->datum parts))))
- (syntax-case x ()
- ((_ name (option ...))
- #`(begin
- (define (#,(id-append #'name #'name #'?) x)
- (memq x '(option ...)))
- (define (#,(id-append #'name #'serialize- #'name) field-name val)
- (serialize-field field-name val)))))))
- (define-enumerated-field-type proto
- (udp tcp udp6 tcp6))
- (define-enumerated-field-type dev
- (tun tap))
- (define key-usage? boolean?)
- (define (serialize-key-usage field-name value)
- (if value
- (format #t "remote-cert-tls server\n")
- #f))
- (define bind? boolean?)
- (define (serialize-bind field-name value)
- (if value
- #f
- (format #t "nobind\n")))
- (define resolv-retry? boolean?)
- (define (serialize-resolv-retry field-name value)
- (if value
- (format #t "resolv-retry infinite\n")
- #f))
- (define (serialize-tls-auth role location)
- (if location
- (serialize-field 'tls-auth
- (string-append location " " (match role
- ('server "0")
- ('client "1"))))
- #f))
- (define (tls-auth? val)
- (or (eq? val #f)
- (string? val)))
- (define (serialize-tls-auth-server field-name val)
- (serialize-tls-auth 'server val))
- (define (serialize-tls-auth-client field-name val)
- (serialize-tls-auth 'client val))
- (define tls-auth-server? tls-auth?)
- (define tls-auth-client? tls-auth?)
- (define (serialize-number field-name val)
- (serialize-field field-name (number->string val)))
- (define (all-lte left right)
- (if (eq? left '())
- (eq? right '())
- (and (<= (string->number (car left)) (car right))
- (all-lte (cdr left) (cdr right)))))
- (define (cidr4? val)
- (if (string? val)
- (if (string-match "^([0-9]+\\.){3}[0-9]+/[0-9]+$" val)
- (let ((numbers (string-tokenize val char-set:digit)))
- (all-lte numbers (list 255 255 255 255 32)))
- #f)
- (eq? val #f)))
- (define (cidr6? val)
- (if (string? val)
- (string-match "^([0-9a-f]{0,4}:){0,8}/[0-9]{1,3}$" val)
- (eq? val #f)))
- (define (serialize-cidr4 field-name val)
- (if (eq? val #f) #f (serialize-field field-name val)))
- (define (serialize-cidr6 field-name val)
- (if (eq? val #f) #f (serialize-field field-name val)))
- (define (ip? val)
- (if (string? val)
- (if (string-match "^([0-9]+\\.){3}[0-9]+$" val)
- (let ((numbers (string-tokenize val char-set:digit)))
- (all-lte numbers (list 255 255 255 255)))
- #f)
- (eq? val #f)))
- (define (serialize-ip field-name val)
- (if (eq? val #f) #f (serialize-field field-name val)))
- (define (keepalive? val)
- (and (list? val)
- (and (number? (car val))
- (number? (car (cdr val))))))
- (define (serialize-keepalive field-name val)
- (format #t "~a ~a ~a\n" (uglify-field-name field-name)
- (number->string (car val)) (number->string (car (cdr val)))))
- (define gateway? boolean?)
- (define (serialize-gateway field-name val)
- (and val
- (format #t "push \"redirect-gateway\"\n")))
- (define-configuration openvpn-remote-configuration
- (name
- (string "my-server")
- "Server name.")
- (port
- (number 1194)
- "Port number the server listens to."))
- (define-configuration openvpn-ccd-configuration
- (name
- (string "client")
- "Client name.")
- (iroute
- (ip-mask #f)
- "Client own network")
- (ifconfig-push
- (ip-mask #f)
- "Client VPN IP."))
- (define (openvpn-remote-list? val)
- (and (list? val)
- (or (eq? val '())
- (and (openvpn-remote-configuration? (car val))
- (openvpn-remote-list? (cdr val))))))
- (define (serialize-openvpn-remote-list field-name val)
- (for-each (lambda (remote)
- (format #t "remote ~a ~a\n" (openvpn-remote-configuration-name remote)
- (number->string (openvpn-remote-configuration-port remote))))
- val))
- (define (openvpn-ccd-list? val)
- (and (list? val)
- (or (eq? val '())
- (and (openvpn-ccd-configuration? (car val))
- (openvpn-ccd-list? (cdr val))))))
- (define (serialize-openvpn-ccd-list field-name val)
- #f)
- (define (create-ccd-directory val)
- "Create a ccd directory containing files for the ccd configuration option
- of OpenVPN. Each file in this directory represents particular settings for a
- client. Each file is named after the name of the client."
- (let ((files (map (lambda (ccd)
- (list (openvpn-ccd-configuration-name ccd)
- (with-output-to-string
- (lambda ()
- (serialize-configuration
- ccd openvpn-ccd-configuration-fields)))))
- val)))
- (computed-file "ccd"
- (with-imported-modules '((guix build utils))
- #~(begin
- (use-modules (guix build utils))
- (use-modules (ice-9 match))
- (mkdir-p #$output)
- (for-each
- (lambda (ccd)
- (match ccd
- ((name config-string)
- (call-with-output-file
- (string-append #$output "/" name)
- (lambda (port) (display config-string port))))))
- '#$files))))))
- (define-syntax define-split-configuration
- (lambda (x)
- (syntax-case x ()
- ((_ name1 name2 (common-option ...) (first-option ...) (second-option ...))
- #`(begin
- (define-configuration #,#'name1
- common-option ...
- first-option ...)
- (define-configuration #,#'name2
- common-option ...
- second-option ...))))))
- (define-split-configuration openvpn-client-configuration
- openvpn-server-configuration
- ((openvpn
- (file-like openvpn)
- "The OpenVPN package.")
- (pid-file
- (string "/var/run/openvpn/openvpn.pid")
- "The OpenVPN pid file.")
- (proto
- (proto 'udp)
- "The protocol (UDP or TCP) used to open a channel between clients and
- servers.")
- (dev
- (dev 'tun)
- "The device type used to represent the VPN connection.")
- (ca
- (maybe-string "/etc/openvpn/ca.crt")
- "The certificate authority to check connections against.")
- (cert
- (maybe-string "/etc/openvpn/client.crt")
- "The certificate of the machine the daemon is running on. It should be signed
- by the authority given in @code{ca}.")
- (key
- (maybe-string "/etc/openvpn/client.key")
- "The key of the machine the daemon is running on. It must be the key whose
- certificate is @code{cert}.")
- (comp-lzo?
- (boolean #t)
- "Whether to use the lzo compression algorithm.")
- (persist-key?
- (boolean #t)
- "Don't re-read key files across SIGUSR1 or --ping-restart.")
- (persist-tun?
- (boolean #t)
- "Don't close and reopen TUN/TAP device or run up/down scripts across
- SIGUSR1 or --ping-restart restarts.")
- (fast-io?
- (boolean #f)
- "(Experimental) Optimize TUN/TAP/UDP I/O writes by avoiding a call to
- poll/epoll/select prior to the write operation.")
- (verbosity
- (number 3)
- "Verbosity level."))
- ;; client-specific configuration
- ((tls-auth
- (tls-auth-client #f)
- "Add an additional layer of HMAC authentication on top of the TLS control
- channel to protect against DoS attacks.")
- (auth-user-pass
- maybe-string
- "Authenticate with server using username/password. The option is a file
- containing username/password on 2 lines. Do not use a file-like object as it
- would be added to the store and readable by any user.")
- (verify-key-usage?
- (key-usage #t)
- "Whether to check the server certificate has server usage extension.")
- (bind?
- (bind #f)
- "Bind to a specific local port number.")
- (resolv-retry?
- (resolv-retry #t)
- "Retry resolving server address.")
- (remote
- (openvpn-remote-list '())
- "A list of remote servers to connect to."))
- ;; server-specific configuration
- ((tls-auth
- (tls-auth-server #f)
- "Add an additional layer of HMAC authentication on top of the TLS control
- channel to protect against DoS attacks.")
- (port
- (number 1194)
- "Specifies the port number on which the server listens.")
- (server
- (ip-mask "10.8.0.0 255.255.255.0")
- "An ip and mask specifying the subnet inside the virtual network.")
- (server-ipv6
- (cidr6 #f)
- "A CIDR notation specifying the IPv6 subnet inside the virtual network.")
- (dh
- (string "/etc/openvpn/dh2048.pem")
- "The Diffie-Hellman parameters file.")
- (ifconfig-pool-persist
- (string "/etc/openvpn/ipp.txt")
- "The file that records client IPs.")
- (redirect-gateway?
- (gateway #f)
- "When true, the server will act as a gateway for its clients.")
- (client-to-client?
- (boolean #f)
- "When true, clients are allowed to talk to each other inside the VPN.")
- (keepalive
- (keepalive '(10 120))
- "Causes ping-like messages to be sent back and forth over the link so that
- each side knows when the other side has gone down. @code{keepalive} requires
- a pair. The first element is the period of the ping sending, and the second
- element is the timeout before considering the other side down.")
- (max-clients
- (number 100)
- "The maximum number of clients.")
- (status
- (string "/var/run/openvpn/status")
- "The status file. This file shows a small report on current connection. It
- is truncated and rewritten every minute.")
- (client-config-dir
- (openvpn-ccd-list '())
- "The list of configuration for some clients.")))
- (define (openvpn-config-file role config)
- (let ((config-str
- (with-output-to-string
- (lambda ()
- (serialize-configuration config
- (match role
- ('server
- openvpn-server-configuration-fields)
- ('client
- openvpn-client-configuration-fields))))))
- (ccd-dir (match role
- ('server (create-ccd-directory
- (openvpn-server-configuration-client-config-dir
- config)))
- ('client #f))))
- (computed-file "openvpn.conf"
- #~(begin
- (use-modules (ice-9 match))
- (call-with-output-file #$output
- (lambda (port)
- (match '#$role
- ('server (display "" port))
- ('client (display "client\n" port)))
- (display #$config-str port)
- (match '#$role
- ('server (display
- (string-append "client-config-dir "
- #$ccd-dir "\n") port))
- ('client (display "" port)))))))))
- (define (openvpn-shepherd-service role)
- (lambda (config)
- (let* ((config-file (openvpn-config-file role config))
- (pid-file ((match role
- ('server openvpn-server-configuration-pid-file)
- ('client openvpn-client-configuration-pid-file))
- config))
- (openvpn ((match role
- ('server openvpn-server-configuration-openvpn)
- ('client openvpn-client-configuration-openvpn))
- config))
- (log-file (match role
- ('server "/var/log/openvpn-server.log")
- ('client "/var/log/openvpn-client.log"))))
- (list (shepherd-service
- (documentation (string-append "Run the OpenVPN "
- (match role
- ('server "server")
- ('client "client"))
- " daemon."))
- (provision (match role
- ('server '(vpn-server))
- ('client '(vpn-client))))
- (requirement '(networking))
- (start #~(make-forkexec-constructor
- (list (string-append #$openvpn "/sbin/openvpn")
- "--writepid" #$pid-file "--config" #$config-file
- "--daemon")
- #:pid-file #$pid-file
- #:log-file #$log-file))
- (stop #~(make-kill-destructor)))))))
- (define %openvpn-accounts
- (list (user-group (name "openvpn") (system? #t))
- (user-account
- (name "openvpn")
- (group "openvpn")
- (system? #t)
- (comment "Openvpn daemon user")
- (home-directory "/var/empty")
- (shell (file-append shadow "/sbin/nologin")))))
- (define %openvpn-activation
- #~(begin
- (use-modules (guix build utils))
- (mkdir-p "/var/run/openvpn")))
- (define openvpn-server-service-type
- (service-type (name 'openvpn-server)
- (extensions
- (list (service-extension shepherd-root-service-type
- (openvpn-shepherd-service 'server))
- (service-extension account-service-type
- (const %openvpn-accounts))
- (service-extension activation-service-type
- (const %openvpn-activation))))
- (description "Run the OpenVPN server, which allows you to
- @emph{host} a @acronym{VPN, virtual private network}.")
- (default-value (openvpn-server-configuration))))
- (define openvpn-client-service-type
- (service-type (name 'openvpn-client)
- (extensions
- (list (service-extension shepherd-root-service-type
- (openvpn-shepherd-service 'client))
- (service-extension account-service-type
- (const %openvpn-accounts))
- (service-extension activation-service-type
- (const %openvpn-activation))))
- (description
- "Run the OpenVPN client service, which allows you to connect
- to an existing @acronym{VPN, virtual private network}.")
- (default-value (openvpn-client-configuration))))
- (define-deprecated
- (openvpn-client-service #:key (config (openvpn-client-configuration)))
- openvpn-client-service-type
- (service openvpn-client-service-type config))
- (define-deprecated
- (openvpn-server-service #:key (config (openvpn-server-configuration)))
- openvpn-server-service-type
- (service openvpn-server-service-type config))
- (define (generate-openvpn-server-documentation)
- (generate-documentation
- `((openvpn-server-configuration
- ,openvpn-server-configuration-fields
- (ccd openvpn-ccd-configuration))
- (openvpn-ccd-configuration ,openvpn-ccd-configuration-fields))
- 'openvpn-server-configuration))
- (define (generate-openvpn-client-documentation)
- (generate-documentation
- `((openvpn-client-configuration
- ,openvpn-client-configuration-fields
- (remote openvpn-remote-configuration))
- (openvpn-remote-configuration ,openvpn-remote-configuration-fields))
- 'openvpn-client-configuration))
- ;;;
- ;;; Strongswan.
- ;;;
- (define-record-type* <strongswan-configuration>
- strongswan-configuration make-strongswan-configuration
- strongswan-configuration?
- (strongswan strongswan-configuration-strongswan ;file-like
- (default strongswan))
- (ipsec-conf strongswan-configuration-ipsec-conf ;string|#f
- (default #f))
- (ipsec-secrets strongswan-configuration-ipsec-secrets ;string|#f
- (default #f)))
- ;; In the future, it might be worth implementing a record type to configure
- ;; all of the plugins, but for *most* basic use cases, simply creating the
- ;; files will be sufficient. Same is true of charon-plugins.
- (define strongswand-configuration-files
- (list "charon" "charon-logging" "pki" "pool" "scepclient"
- "swanctl" "tnc"))
- ;; Plugins to load. All of these plugins end up as configuration files in
- ;; strongswan.d/charon/.
- (define charon-plugins
- (list "aes" "aesni" "attr" "attr-sql" "chapoly" "cmac" "constraints"
- "counters" "curl" "curve25519" "dhcp" "dnskey" "drbg" "eap-aka-3gpp"
- "eap-aka" "eap-dynamic" "eap-identity" "eap-md5" "eap-mschapv2"
- "eap-peap" "eap-radius" "eap-simaka-pseudonym" "eap-simaka-reauth"
- "eap-simaka-sql" "eap-sim" "eap-sim-file" "eap-tls" "eap-tnc"
- "eap-ttls" "ext-auth" "farp" "fips-prf" "gmp" "ha" "hmac"
- "kernel-netlink" "led" "md4" "md5" "mgf1" "nonce" "openssl" "pem"
- "pgp" "pkcs12" "pkcs1" "pkcs7" "pkcs8" "pubkey" "random" "rc2"
- "resolve" "revocation" "sha1" "sha2" "socket-default" "soup" "sql"
- "sqlite" "sshkey" "tnc-tnccs" "vici" "x509" "xauth-eap" "xauth-generic"
- "xauth-noauth" "xauth-pam" "xcbc"))
- (define (strongswan-configuration-file config)
- (match-record config <strongswan-configuration>
- (strongswan ipsec-conf ipsec-secrets)
- (if (eq? (string? ipsec-conf) (string? ipsec-secrets))
- (let* ((strongswan-dir
- (computed-file
- "strongswan.d"
- #~(begin
- (mkdir #$output)
- ;; Create all of the configuration files strongswan.d/.
- (map (lambda (conf-file)
- (let* ((filename (string-append
- #$output "/"
- conf-file ".conf")))
- (call-with-output-file filename
- (lambda (port)
- (display
- "# Created by 'strongswan-service'\n"
- port)))))
- (list #$@strongswand-configuration-files))
- (mkdir (string-append #$output "/charon"))
- ;; Create all of the plugin configuration files.
- (map (lambda (plugin)
- (let* ((filename (string-append
- #$output "/charon/"
- plugin ".conf")))
- (call-with-output-file filename
- (lambda (port)
- (format port "~a {
- load = yes
- }"
- plugin)))))
- (list #$@charon-plugins))))))
- ;; Generate our strongswan.conf to reflect the user configuration.
- (computed-file
- "strongswan.conf"
- #~(begin
- (call-with-output-file #$output
- (lambda (port)
- (display "# Generated by 'strongswan-service'.\n" port)
- (format port "charon {
- load_modular = yes
- plugins {
- include ~a/charon/*.conf"
- #$strongswan-dir)
- (if #$ipsec-conf
- (format port "
- stroke {
- load = yes
- secrets_file = ~a
- }
- }
- }
- starter {
- config_file = ~a
- }
- include ~a/*.conf"
- #$ipsec-secrets
- #$ipsec-conf
- #$strongswan-dir)
- (format port "
- }
- }
- include ~a/*.conf"
- #$strongswan-dir)))))))
- (throw 'error
- (G_ "strongSwan ipsec-conf and ipsec-secrets must both be (un)set")))))
- (define (strongswan-shepherd-service config)
- (let* ((ipsec (file-append strongswan "/sbin/ipsec"))
- (strongswan-conf-path (strongswan-configuration-file config)))
- (list (shepherd-service
- (requirement '(networking))
- (provision '(ipsec))
- (start #~(make-forkexec-constructor
- (list #$ipsec "start" "--nofork")
- #:environment-variables
- (list (string-append "STRONGSWAN_CONF="
- #$strongswan-conf-path))))
- (stop #~(make-kill-destructor))
- (documentation
- "strongSwan's charon IKE keying daemon for IPsec VPN.")))))
- (define strongswan-service-type
- (service-type
- (name 'strongswan)
- (extensions
- (list (service-extension shepherd-root-service-type
- strongswan-shepherd-service)))
- (default-value (strongswan-configuration))
- (description
- "Connect to an IPsec @acronym{VPN, Virtual Private Network} with
- strongSwan.")))
- ;;;
- ;;; Wireguard.
- ;;;
- (define-record-type* <wireguard-peer>
- wireguard-peer make-wireguard-peer
- wireguard-peer?
- (name wireguard-peer-name)
- (endpoint wireguard-peer-endpoint
- (default #f)) ;string
- (public-key wireguard-peer-public-key) ;string
- (preshared-key wireguard-peer-preshared-key
- (default #f)) ;string
- (allowed-ips wireguard-peer-allowed-ips) ;list of strings
- (keep-alive wireguard-peer-keep-alive
- (default #f))) ;integer
- (define-record-type* <wireguard-configuration>
- wireguard-configuration make-wireguard-configuration
- wireguard-configuration?
- (wireguard wireguard-configuration-wireguard ;file-like
- (default wireguard-tools))
- (interface wireguard-configuration-interface ;string
- (default "wg0"))
- (addresses wireguard-configuration-addresses ;string
- (default '("10.0.0.1/32")))
- (port wireguard-configuration-port ;integer
- (default 51820))
- (private-key wireguard-configuration-private-key ;string
- (default "/etc/wireguard/private.key"))
- (peers wireguard-configuration-peers ;list of <wiregard-peer>
- (default '()))
- (dns wireguard-configuration-dns ;list of strings
- (default '()))
- (monitor-ips? wireguard-configuration-monitor-ips? ;boolean
- (default #f))
- (monitor-ips-interval wireguard-configuration-monitor-ips-interval
- (default '(next-minute (range 0 60 5)))) ;string | list
- (pre-up wireguard-configuration-pre-up ;list of strings
- (default '()))
- (post-up wireguard-configuration-post-up ;list of strings
- (default '()))
- (pre-down wireguard-configuration-pre-down ;list of strings
- (default '()))
- (post-down wireguard-configuration-post-down ;list of strings
- (default '()))
- (table wireguard-configuration-table ;string
- (default "auto")))
- (define (wireguard-configuration-file config)
- (define (peer->config peer)
- (match-record peer <wireguard-peer>
- (name public-key endpoint allowed-ips keep-alive)
- (let ((lines (list
- (format #f "[Peer] #~a" name)
- (format #f "PublicKey = ~a" public-key)
- (format #f "AllowedIPs = ~{~a~^, ~}" allowed-ips)
- (format #f "~@[Endpoint = ~a~]" endpoint)
- (format #f "~@[PersistentKeepalive = ~a~]" keep-alive))))
- (string-join (remove string-null? lines) "\n"))))
- (define (peers->preshared-keys peer keys)
- (let ((public-key (wireguard-peer-public-key peer))
- (preshared-key (wireguard-peer-preshared-key peer)))
- (if preshared-key
- (cons* public-key preshared-key keys)
- keys)))
- (match-record config <wireguard-configuration>
- (wireguard interface addresses port private-key peers dns
- pre-up post-up pre-down post-down table)
- (let* ((config-file (string-append interface ".conf"))
- (peer-keys (fold peers->preshared-keys (list) peers))
- (peers (map peer->config peers))
- (config
- (computed-file
- "wireguard-config"
- #~(begin
- (use-modules (ice-9 format)
- (srfi srfi-1))
- (define lines
- (list
- "[Interface]"
- #$@(if (null? addresses)
- '()
- (list (format #f "Address = ~{~a~^, ~}"
- addresses)))
- (format #f "~@[Table = ~a~]" #$table)
- #$@(if (null? pre-up)
- '()
- (list (format #f "~{PreUp = ~a~%~}" pre-up)))
- (format #f "PostUp = ~a set %i private-key ~a\
- ~{ peer ~a preshared-key ~a~}" #$(file-append wireguard "/bin/wg")
- #$private-key '#$peer-keys)
- #$@(if (null? post-up)
- '()
- (list (format #f "~{PostUp = ~a~%~}" post-up)))
- #$@(if (null? pre-down)
- '()
- (list (format #f "~{PreDown = ~a~%~}" pre-down)))
- #$@(if (null? post-down)
- '()
- (list (format #f "~{PostDown = ~a~%~}" post-down)))
- (format #f "~@[ListenPort = ~a~]" #$port)
- #$@(if (null? dns)
- '()
- (list (format #f "DNS = ~{~a~^, ~}" dns)))))
- (mkdir #$output)
- (chdir #$output)
- (call-with-output-file #$config-file
- (lambda (port)
- (format port "~a~%~%~{~a~%~^~%~}"
- (string-join (remove string-null? lines) "\n")
- '#$peers)))))))
- (file-append config "/" config-file))))
- (define (wireguard-activation config)
- (match-record config <wireguard-configuration>
- (private-key wireguard)
- #~(begin
- (use-modules (guix build utils)
- (ice-9 popen)
- (ice-9 rdelim))
- (mkdir-p (dirname #$private-key))
- (unless (file-exists? #$private-key)
- (let* ((pipe
- (open-input-pipe (string-append
- #$(file-append wireguard "/bin/wg")
- " genkey")))
- (key (read-line pipe)))
- (call-with-output-file #$private-key
- (lambda (port)
- (display key port)))
- (chmod #$private-key #o400)
- (close-pipe pipe))))))
- ;;; XXX: Copied from (guix scripts pack), changing define to define*.
- (define-syntax-rule (define-with-source (variable args ...) body body* ...)
- "Bind VARIABLE to a procedure accepting ARGS defined as BODY, also setting
- its source property."
- (begin
- (define* (variable args ...)
- body body* ...)
- (eval-when (load eval)
- (set-procedure-property! variable 'source
- '(define* (variable args ...) body body* ...)))))
- (define (wireguard-service-name interface)
- "Return the WireGuard service name (a symbol) configured to use INTERFACE."
- (symbol-append 'wireguard- (string->symbol interface)))
- (define-with-source (strip-port/maybe endpoint #:key ipv6?)
- "Strip the colon and port, if present in ENDPOINT, a string."
- (if ipv6?
- (if (string-prefix? "[" endpoint)
- (first (string-split (string-drop endpoint 1) #\])) ;ipv6
- endpoint)
- (first (string-split endpoint #\:)))) ;ipv4
- (define* (ipv4-address? address)
- "Predicate to check whether ADDRESS is a valid IPv4 address."
- (let ((address (strip-port/maybe address)))
- (false-if-exception
- (->bool (getaddrinfo address #f AI_NUMERICHOST AF_INET)))))
- (define* (ipv6-address? address)
- "Predicate to check whether ADDRESS is a valid IPv6 address."
- (let ((address (strip-port/maybe address #:ipv6? #t)))
- (false-if-exception
- (->bool (getaddrinfo address #f AI_NUMERICHOST AF_INET6)))))
- (define (host-name? name)
- "Predicate to check whether NAME is a host name, i.e. not an IP address."
- (not (or (ipv6-address? name) (ipv4-address? name))))
- (define (endpoint-host-names peers)
- "Return an association list of endpoint host names keyed by their peer
- public key, if any."
- (reverse
- (fold (lambda (peer host-names)
- (let ((public-key (wireguard-peer-public-key peer))
- (endpoint (wireguard-peer-endpoint peer)))
- (if (and endpoint (host-name? endpoint))
- (cons (cons public-key endpoint) host-names)
- host-names)))
- '()
- peers)))
- (define (wireguard-shepherd-service config)
- (match-record config <wireguard-configuration>
- (wireguard interface)
- (let ((wg-quick (file-append wireguard "/bin/wg-quick"))
- (config (wireguard-configuration-file config)))
- (list (shepherd-service
- (requirement '(networking))
- (provision (list (wireguard-service-name interface)))
- (start #~(lambda _
- (invoke #$wg-quick "up" #$config)))
- (stop #~(lambda _
- (invoke #$wg-quick "down" #$config)
- #f)) ;stopped!
- (actions (list (shepherd-configuration-action config)))
- (documentation "Run the Wireguard VPN tunnel"))))))
- (define (wireguard-monitoring-jobs config)
- ;; Loosely based on WireGuard's own 'reresolve-dns.sh' shell script (see:
- ;; https://raw.githubusercontent.com/WireGuard/wireguard-tools/
- ;; master/contrib/reresolve-dns/reresolve-dns.sh).
- (match-record config <wireguard-configuration>
- (interface monitor-ips? monitor-ips-interval peers)
- (let ((host-names (endpoint-host-names peers)))
- (if monitor-ips?
- (if (null? host-names)
- (begin
- (warn "monitor-ips? is #t but no host name to monitor")
- '())
- ;; The mcron monitor job may be a string or a list; ungexp strips
- ;; one quote level, which must be added back when a list is
- ;; provided.
- (list
- #~(job
- (if (string? #$monitor-ips-interval)
- #$monitor-ips-interval
- '#$monitor-ips-interval)
- #$(program-file
- (format #f "wireguard-~a-monitoring" interface)
- (with-imported-modules (source-module-closure
- '((gnu services herd)
- (guix build utils)))
- #~(begin
- (use-modules (gnu services herd)
- (guix build utils)
- (ice-9 popen)
- (ice-9 match)
- (ice-9 textual-ports)
- (srfi srfi-1)
- (srfi srfi-26))
- (define (resolve-host name)
- "Return the IP address resolved from NAME."
- (let* ((ai (car (getaddrinfo name)))
- (sa (addrinfo:addr ai)))
- (inet-ntop (sockaddr:fam sa)
- (sockaddr:addr sa))))
- (define wg #$(file-append wireguard-tools "/bin/wg"))
- #$(procedure-source strip-port/maybe)
- (define service-name '#$(wireguard-service-name
- interface))
- (when (live-service-running
- (current-service service-name))
- (let* ((pipe (open-pipe* OPEN_READ wg "show"
- #$interface "endpoints"))
- (lines (string-split (get-string-all pipe)
- #\newline))
- ;; IPS is an association list mapping
- ;; public keys to IP addresses.
- (ips (map (match-lambda
- ((public-key ip)
- (cons public-key
- (strip-port/maybe ip))))
- (map (cut string-split <> #\tab)
- (remove string-null?
- lines)))))
- (close-pipe pipe)
- (for-each
- (match-lambda
- ((key . host-name)
- (let ((resolved-ip (resolve-host
- (strip-port/maybe
- host-name)))
- (current-ip (assoc-ref ips key)))
- (unless (string=? resolved-ip current-ip)
- (format #t "resetting `~a' peer \
- endpoint to `~a' due to stale IP (`~a' instead of `~a')~%"
- key host-name
- current-ip resolved-ip)
- (invoke wg "set" #$interface "peer" key
- "endpoint" host-name)))))
- '#$host-names)))))))))
- '())))) ;monitor-ips? is #f
- (define wireguard-service-type
- (service-type
- (name 'wireguard)
- (extensions
- (list (service-extension shepherd-root-service-type
- wireguard-shepherd-service)
- (service-extension activation-service-type
- wireguard-activation)
- (service-extension profile-service-type
- (compose list
- wireguard-configuration-wireguard))
- (service-extension mcron-service-type
- wireguard-monitoring-jobs)))
- (description "Set up Wireguard @acronym{VPN, Virtual Private Network}
- tunnels.")))
|