├── .gitignore ├── .goreleaser.yml ├── LICENSE ├── README.md ├── contrib ├── zerotier-systemd-manager.service └── zerotier-systemd-manager.timer ├── generate.sh ├── go.mod ├── go.sum ├── mgr.go ├── pkgsrc ├── postinstall.sh └── postremove.sh └── template.network /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # This is an example .goreleaser.yml file with some sane defaults. 2 | # Make sure to check the documentation at http://goreleaser.com 3 | before: 4 | hooks: 5 | # You may remove this if you don't use go modules. 6 | - go mod tidy 7 | builds: 8 | - env: 9 | - CGO_ENABLED=0 10 | goos: 11 | - linux 12 | goarch: 13 | - 386 14 | - amd64 15 | - arm 16 | - arm64 17 | - s390x 18 | - ppc64le 19 | goarm: 20 | - 6 21 | - 7 22 | archives: 23 | - wrap_in_directory: true 24 | files: 25 | - LICENSE 26 | - README.md 27 | - contrib/* 28 | nfpms: 29 | - formats: 30 | - deb 31 | - rpm 32 | bindir: /usr/bin 33 | maintainer: Erik Hollensbe 34 | dependencies: 35 | - systemd 36 | contents: 37 | - src: contrib/zerotier-systemd-manager.timer 38 | dst: /lib/systemd/system/zerotier-systemd-manager.timer 39 | type: "config" 40 | - src: contrib/zerotier-systemd-manager.service 41 | dst: /lib/systemd/system/zerotier-systemd-manager.service 42 | type: "config" 43 | scripts: 44 | postinstall: "pkgsrc/postinstall.sh" 45 | postremove: "pkgsrc/postremove.sh" 46 | checksum: 47 | name_template: 'checksums.txt' 48 | snapshot: 49 | name_template: "{{ .Tag }}-next" 50 | changelog: 51 | sort: asc 52 | filters: 53 | exclude: 54 | - '^docs:' 55 | - '^test:' 56 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2021 ZeroTier, Inc. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this 7 | list of conditions and the following disclaimer. 8 | 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | 3. Neither the name of the copyright holder nor the names of its contributors 14 | may be used to endorse or promote products derived from this software without 15 | specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 18 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 19 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 21 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 22 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 23 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 24 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 25 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 26 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Get ZeroTier playing nice with systemd-networkd and resolvectl 2 | 3 | This is a small tool to enable the systemd-networkd service as well as resolvectl to enable per-interface DNS settings. We take this directly from zerotier-one (on your machine) metadata. This service does not reach out to the internet on its own. 4 | 5 | The result is per-interface DNS settings, which is especially nice when you are using [zeronsd](https://github.com/zerotier/zeronsd) with multiple networks. 6 | 7 | ## Usage 8 | 9 | **Make sure your distro supports systemd-networkd and that it is enabled and started** 10 | 11 | [Check out our releases for debian and redhat packages that automate this on a variety of platforms](https://github.com/zerotier/zerotier-systemd-manager/releases). 12 | 13 | ### Installing From Source 14 | 15 | Compile it with [golang 1.16 or later](https://golang.org): 16 | 17 | ```bash 18 | # be outside of gopath when you do this 19 | go get github.com/zerotier/zerotier-systemd-manager 20 | ``` 21 | 22 | Ensure `systemd-networkd` is properly configured and `resolvectl` works as intended. 23 | Additionally, make sure to have a `/etc/hosts` file that allows to locally resolve `localhost` e.g containing at least: 24 | ``` 25 | 127.0.0.1 localhost 26 | ::1 localhost 27 | ``` 28 | Finally, run the tool as `root`: `zerotier-systemd-manager`. If you have interfaces with DNS assignments in ZeroTier, it will populate files in `/etc/systemd/network`. No DNS assignment, no file. Unless you have passed `-auto-restart=false`, it will restart `systemd-networkd` for you if things have changed. 29 | 30 | If you have a DNS-over-TLS configuration provided by zeronsd (v0.4.0 or later), you can enable using it by providing `-dns-over-tls=true` in the supervisor (a systemd timer in the default case). You will have to hand-edit this in for now. 31 | 32 | If you want to enable multicast DNS / bonjour / mDNS you can enable it by providing `-multicast-dns`. 33 | 34 | Finally, if you have left a DNS-controlled network it will try to remove the old files if `-reconcile=true` is set (the default). This way you can stuff it in cron and not think about it too much. 35 | 36 | Enjoy! 37 | 38 | ## Author 39 | 40 | Erik Hollensbe 41 | 42 | ## License 43 | 44 | BSD 3-Clause 45 | 46 | ## Releasing 47 | This repo uses [goreleaser](https://goreleaser.com/quick-start/). 48 | 49 | -------------------------------------------------------------------------------- /contrib/zerotier-systemd-manager.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Update zerotier per-interface DNS settings 3 | Requires=zerotier-one.service 4 | After=zerotier-one.service 5 | 6 | [Service] 7 | Type=oneshot 8 | ExecStart=/usr/bin/zerotier-systemd-manager 9 | -------------------------------------------------------------------------------- /contrib/zerotier-systemd-manager.timer: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Update zerotier per-interface DNS settings 3 | 4 | [Timer] 5 | OnStartupSec=1min 6 | OnUnitInactiveSec=1min 7 | 8 | [Install] 9 | WantedBy=timers.target 10 | -------------------------------------------------------------------------------- /generate.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | if [ $# != 2 ] 6 | then 7 | echo "Please read this script before executing it" 8 | exit 1 9 | fi 10 | 11 | PACKAGE=$1 12 | SPEC=$2 13 | 14 | HOST=${HOST:-docs.zerotier.com} 15 | 16 | rm -rf ./${PACKAGE} 17 | mkdir -p ./${PACKAGE} 18 | # github.com/deepmap/oapi-codegen/cmd/oapi-codegen 19 | # note there is an open bug here: https://github.com/deepmap/oapi-codegen/issues/357 related to generation of this code 20 | 21 | oapi-codegen -generate types,client -package ${PACKAGE} -o ${PACKAGE}/gen.go https://${HOST}/openapi/${SPEC}v1.json 22 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/zerotier/zerotier-systemd-manager 2 | 3 | go 1.16 4 | 5 | require github.com/zerotier/go-zerotier-one v0.1.1 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 2 | github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV+0YP4qX0UQ7r2MoYZ+AvYDp12OF5yg4q8rGnyNh4= 3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= 7 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d/go.mod h1:tmAIfUFEirG/Y8jhZ9M+h36obRZAk/1fcSpXwAVlfqE= 8 | github.com/deepmap/oapi-codegen v1.9.0 h1:qpyRY+dzjMai5QejjA53ebnBtcSvIcZOtYwVlsgdxOc= 9 | github.com/deepmap/oapi-codegen v1.9.0/go.mod h1:7t4DbSxmAffcTEgrWvsPYEE2aOARZ8ZKWp3hDuZkHNc= 10 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 11 | github.com/getkin/kin-openapi v0.80.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg= 12 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 13 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 14 | github.com/gin-gonic/gin v1.7.4/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY= 15 | github.com/go-chi/chi/v5 v5.0.0/go.mod h1:BBug9lr0cqtdAhsu6R4AAdvufI0/XBzAQSsUqJpoZOs= 16 | github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= 17 | github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= 18 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 19 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= 20 | github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= 21 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= 22 | github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= 23 | github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= 24 | github.com/go-playground/validator/v10 v10.9.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= 25 | github.com/goccy/go-json v0.7.8/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 26 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 27 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 28 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 29 | github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219/go.mod h1:/X8TswGSh1pIozq4ZwCfxS0WA5JGXguxk94ar/4c87Y= 30 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 31 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 32 | github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= 33 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 34 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 35 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 36 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 37 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 38 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 39 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 40 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 41 | github.com/labstack/echo/v4 v4.2.1/go.mod h1:AA49e0DZ8kk5jTOOCKNuPR6oTnBS0dYiM4FW1e6jwpg= 42 | github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= 43 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= 44 | github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= 45 | github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y= 46 | github.com/lestrrat-go/blackmagic v1.0.0/go.mod h1:TNgH//0vYSs8VXDCfkZLgIrVTTXQELZffUV0tz3MtdQ= 47 | github.com/lestrrat-go/codegen v1.0.2/go.mod h1:JhJw6OQAuPEfVKUCLItpaVLumDGWQznd1VaXrBk9TdM= 48 | github.com/lestrrat-go/httpcc v1.0.0/go.mod h1:tGS/u00Vh5N6FHNkExqGGNId8e0Big+++0Gf8MBnAvE= 49 | github.com/lestrrat-go/iter v1.0.1/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc= 50 | github.com/lestrrat-go/jwx v1.2.7/go.mod h1:bw24IXWbavc0R2RsOtpXL7RtMyP589yZ1+L7kd09ZGA= 51 | github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= 52 | github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 53 | github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 54 | github.com/matryer/moq v0.0.0-20190312154309-6cfb0558e1bd/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ= 55 | github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 56 | github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 57 | github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 58 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 59 | github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= 60 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 61 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 62 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 63 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 64 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 65 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 66 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 67 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 68 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 69 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 70 | github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= 71 | github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= 72 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 73 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 74 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 75 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 76 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 77 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 78 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 79 | github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= 80 | github.com/ugorji/go v1.2.6/go.mod h1:anCg0y61KIhDlPZmnH+so+RQbysYVyDko0IMgJv0Nn0= 81 | github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= 82 | github.com/ugorji/go/codec v1.2.6/go.mod h1:V6TCNZ4PHqoHGFZuSG1W8nrCzzdgA2DozYxWFFpvxTw= 83 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 84 | github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= 85 | github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= 86 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 87 | github.com/zerotier/go-zerotier-one v0.1.1 h1:JfqyrZ0iI8a6zamUjJ5gaDX48kRttvUntUM7sjWtezU= 88 | github.com/zerotier/go-zerotier-one v0.1.1/go.mod h1:Vl9TCuoR1RzKlf7dT1U5F9P0zjGxHd2kibDREY1e1/s= 89 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 90 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 91 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 92 | golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 93 | golang.org/x/crypto v0.0.0-20201217014255-9d1352758620/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= 94 | golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 95 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 96 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 97 | golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 98 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 99 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 100 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 101 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 102 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 103 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 104 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 105 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 106 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 107 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 108 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 109 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 110 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 111 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 112 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 113 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 114 | golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 115 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 116 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 117 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 118 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 119 | golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 120 | golang.org/x/sys v0.0.0-20211031064116-611d5d643895/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 121 | golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= 122 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 123 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 124 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 125 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 126 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 127 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 128 | golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 129 | golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 130 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 131 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 132 | golang.org/x/tools v0.0.0-20200918232735-d647fc253266/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= 133 | golang.org/x/tools v0.0.0-20210114065538-d78b04bdf963/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 134 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 135 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 136 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 137 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 138 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 139 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 140 | google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 141 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 142 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 143 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 144 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 145 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 146 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 147 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 148 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 149 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 150 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= 151 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 152 | -------------------------------------------------------------------------------- /mgr.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | _ "embed" 7 | "flag" 8 | "fmt" 9 | "html/template" 10 | "io/ioutil" 11 | "math" 12 | "net" 13 | "net/http" 14 | "os" 15 | "os/exec" 16 | "path" 17 | "path/filepath" 18 | "runtime" 19 | "sort" 20 | "strings" 21 | 22 | "github.com/zerotier/go-zerotier-one/service" 23 | ) 24 | 25 | //go:embed template.network 26 | var networkTemplate string 27 | 28 | const ( 29 | magicComment = "--- Managed by zerotier-systemd-manager. Do not remove this comment. ---" 30 | networkDir = "/etc/systemd/network" 31 | ipv4bits = 32 32 | ) 33 | 34 | // parameter list for multiple template operations 35 | type templateScaffold struct { 36 | Interface string 37 | NetworkName string 38 | DNS []string 39 | DNSSearch string 40 | MagicComment string 41 | DNSOverTLS bool 42 | MulticastDNS bool 43 | } 44 | 45 | // wrapped openapi client. should probably be replaced with a code generator in 46 | // a separate repository as it's always out of date. 47 | type serviceAPIClient struct { 48 | apiKey string 49 | client *http.Client 50 | } 51 | 52 | func newServiceAPI() (*serviceAPIClient, error) { 53 | content, err := ioutil.ReadFile("/var/lib/zerotier-one/authtoken.secret") 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | return &serviceAPIClient{apiKey: strings.TrimSpace(string(content)), client: &http.Client{}}, nil 59 | } 60 | 61 | func errExit(msg interface{}) { 62 | fmt.Fprintf(os.Stderr, "%v\n", msg) 63 | os.Exit(1) 64 | } 65 | 66 | // Do initiates a client transaction. 99% of what the wrapped client does is inject the right header. 67 | func (c *serviceAPIClient) Do(req *http.Request) (*http.Response, error) { 68 | req.Header.Add("X-ZT1-Auth", c.apiKey) 69 | return c.client.Do(req) 70 | } 71 | 72 | func main() { 73 | // two flags for the CLI auto-restart and reconcile are defaulted to true, so you rarely need them. 74 | autoRestartFlag := flag.Bool("auto-restart", true, "Automatically restart systemd-resolved when things change") 75 | reconcileFlag := flag.Bool("reconcile", true, "Automatically remove left networks from systemd-networkd configuration") 76 | dnsOverTLSFlag := flag.Bool("dns-over-tls", false, "Automatically prefer DNS-over-TLS. Requires ZeroNSd v0.4 or better") 77 | multicastDNSFlag := flag.Bool("multicast-dns", false, "Enable mDNS resolution on the zerotier interface.") 78 | flag.Parse() 79 | 80 | if os.Geteuid() != 0 { 81 | errExit("You need to be root to run this program") 82 | } 83 | 84 | if runtime.GOOS != "linux" { 85 | errExit("This tool is only needed (and useful) on linux") 86 | } 87 | 88 | t, err := template.New("network").Parse(networkTemplate) 89 | if err != nil { 90 | errExit("your template is busted; get a different version or stop modifying the source code :)") 91 | } 92 | 93 | /* 94 | this bit is fundamentally a set difference of the networks zerotier knows 95 | about, and systemd-resolved knows about. If reconcile is true, this is 96 | corrected. If any corrections are made, or any networks added, and 97 | auto-restart is true, then systemd-resolved is reloaded near the end. 98 | */ 99 | 100 | sAPI, err := newServiceAPI() 101 | if err != nil { 102 | errExit(err) 103 | } 104 | 105 | client, err := service.NewClient("http://localhost:9993", service.WithHTTPClient(sAPI)) 106 | if err != nil { 107 | errExit(err) 108 | } 109 | 110 | resp, err := client.GetNetworks(context.Background()) 111 | if err != nil { 112 | errExit(err) 113 | } 114 | 115 | networks, err := service.ParseGetNetworksResponse(resp) 116 | if err != nil { 117 | errExit(err) 118 | } 119 | 120 | dir, err := os.ReadDir(networkDir) 121 | if err != nil { 122 | errExit(err) 123 | } 124 | 125 | found := map[string]struct{}{} 126 | 127 | for _, item := range dir { 128 | if item.Type().IsRegular() && strings.HasSuffix(item.Name(), ".network") { 129 | content, err := ioutil.ReadFile(filepath.Join(networkDir, item.Name())) 130 | if err != nil { 131 | errExit(err) 132 | } 133 | 134 | if bytes.Contains(content, []byte(magicComment)) { 135 | found[item.Name()] = struct{}{} 136 | } 137 | } 138 | } 139 | 140 | var changed bool 141 | 142 | for _, network := range *networks.JSON200 { 143 | if network.Dns != nil && len(*network.Dns.Servers) != 0 { 144 | fn := fmt.Sprintf("/etc/systemd/network/99-%s.network", *network.PortDeviceName) 145 | 146 | delete(found, path.Base(fn)) 147 | 148 | search := map[string]struct{}{} 149 | 150 | if network.Dns.Domain != nil { 151 | search[*network.Dns.Domain] = struct{}{} 152 | } 153 | 154 | // This calculates in-addr.arpa and ip6.arpa search domains by calculating them from the IP assignments. 155 | if network.AssignedAddresses != nil && len(*network.AssignedAddresses) > 0 { 156 | for _, addr := range *network.AssignedAddresses { 157 | ip, ipnet, err := net.ParseCIDR(addr) 158 | if err != nil { 159 | errExit(fmt.Sprintf("Could not parse CIDR %q: %v", addr, err)) 160 | } 161 | 162 | used, total := ipnet.Mask.Size() 163 | 164 | bits := int(math.Trunc(math.Ceil(float64(total) / float64(used)))) 165 | 166 | octets := make([]byte, bits+1) 167 | if total == ipv4bits { 168 | ip = ip.To4() 169 | } 170 | 171 | for i := 0; i <= bits; i++ { 172 | octets[i] = ip[i] 173 | } 174 | 175 | searchLine := "~" 176 | for i := len(octets) - 1; i >= 0; i-- { 177 | if total > ipv4bits { 178 | searchLine += fmt.Sprintf("%x.", (octets[i] & 0xf)) 179 | searchLine += fmt.Sprintf("%x.", ((octets[i] >> 4) & 0xf)) 180 | } else { 181 | searchLine += fmt.Sprintf("%d.", octets[i]) 182 | } 183 | } 184 | 185 | if total == ipv4bits { 186 | searchLine += "in-addr.arpa" 187 | } else { 188 | searchLine += "ip6.arpa" 189 | } 190 | 191 | search[searchLine] = struct{}{} 192 | } 193 | } 194 | 195 | searchkeys := []string{} 196 | 197 | for key := range search { 198 | searchkeys = append(searchkeys, key) 199 | } 200 | 201 | sort.Strings(searchkeys) 202 | 203 | out := templateScaffold{ 204 | Interface: *network.PortDeviceName, 205 | NetworkName: *network.Name, 206 | DNS: *network.Dns.Servers, 207 | DNSSearch: strings.Join(searchkeys, " "), 208 | MagicComment: magicComment, 209 | DNSOverTLS: *dnsOverTLSFlag, 210 | MulticastDNS: *multicastDNSFlag, 211 | } 212 | 213 | buf := bytes.NewBuffer(nil) 214 | 215 | if err := t.Execute(buf, out); err != nil { 216 | errExit(fmt.Errorf("%q: %w", fn, err)) 217 | } 218 | 219 | if _, err := os.Stat(fn); err == nil { 220 | content, err := ioutil.ReadFile(fn) 221 | if err != nil { 222 | errExit(fmt.Errorf("In %v: %w", fn, err)) 223 | } 224 | 225 | if bytes.Equal(content, buf.Bytes()) { 226 | fmt.Fprintf(os.Stderr, "%q hasn't changed; skipping\n", fn) 227 | continue 228 | } 229 | } 230 | 231 | fmt.Printf("Generating %q\n", fn) 232 | f, err := os.Create(fn) 233 | if err != nil { 234 | errExit(fmt.Errorf("%q: %w", fn, err)) 235 | } 236 | 237 | if _, err := f.Write(buf.Bytes()); err != nil { 238 | errExit(err) 239 | } 240 | 241 | f.Close() 242 | 243 | changed = true 244 | } 245 | } 246 | 247 | if len(found) > 0 && *reconcileFlag { 248 | fmt.Println("Found unused networks, reconciling...") 249 | 250 | for fn := range found { 251 | fmt.Printf("Removing %q\n", fn) 252 | 253 | if err := os.Remove(filepath.Join(networkDir, fn)); err != nil { 254 | errExit(fmt.Errorf("While removing %q: %w", fn, err)) 255 | } 256 | } 257 | } 258 | 259 | if (changed || len(found) > 0) && *autoRestartFlag { 260 | fmt.Println("Files changed; reloading systemd-networkd...") 261 | 262 | if err := exec.Command("networkctl", "reload").Run(); err != nil { 263 | errExit(fmt.Errorf("While reloading systemd-networkd: %v", err)) 264 | } 265 | } 266 | } 267 | -------------------------------------------------------------------------------- /pkgsrc/postinstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -xeu 4 | 5 | systemctl daemon-reload 6 | systemctl enable zerotier-systemd-manager.timer 7 | systemctl start zerotier-systemd-manager.timer 8 | -------------------------------------------------------------------------------- /pkgsrc/postremove.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -xeu 4 | systemctl daemon-reload 5 | -------------------------------------------------------------------------------- /template.network: -------------------------------------------------------------------------------- 1 | # vim: ft=systemd 2 | # {{ .MagicComment }} 3 | [Match] 4 | Name={{ .Interface }} 5 | 6 | [Network] 7 | Description={{ .NetworkName }} 8 | DHCP=no 9 | {{ range $key := .DNS -}} 10 | DNS={{ $key }} 11 | {{ end -}} 12 | {{ if .DNSOverTLS -}} 13 | DNSOverTLS=yes 14 | {{ end -}} 15 | {{ if .MulticastDNS -}} 16 | MulticastDNS=yes 17 | {{ end -}} 18 | Domains=~{{ .DNSSearch }} 19 | ConfigureWithoutCarrier=true 20 | KeepConfiguration=static 21 | --------------------------------------------------------------------------------