From fa038f76016883a1d361ba4712bc9be0aa2f283a Mon Sep 17 00:00:00 2001 From: Dan Fuhry Date: Tue, 27 Feb 2024 15:58:30 -0500 Subject: [PATCH] machines: add agent, coredns plugin and conf file templates Port of the python machines client. Not all templates are fixed up and ready to go, only the ones that are known to be needed on linux routers. No captive portal support for Linux yet. --- go.mod | 74 +- go.sum | 125 +++- machines/agent.go | 217 ++++++ machines/client.go | 185 ++++- machines/coredns_plugin/registry.go | 864 ++++++++++++++++++++++ machines/coredns_plugin/registry_store.go | 251 +++++++ machines/coredns_plugin/setup.go | 125 ++++ machines/coredns_plugin/stats.go | 52 ++ machines/event_watcher.go | 27 +- machines/machines_agent/Makefile | 14 + machines/machines_agent/main.go | 184 +++++ machines/oauth2.go | 14 +- machines/render.go | 574 ++++++++++++++ machines/router_addresses_bsd.go | 123 +++ machines/router_addresses_linux.go | 104 +++ machines/services.go | 37 + machines/services_linux.go | 255 +++++++ machines/stats.go | 35 + machines/templates/corefile.conf.j2 | 10 + machines/templates/dhcpd4.conf.j2 | 90 +++ machines/templates/dhcpd6.conf.j2 | 75 ++ machines/templates/forward.zone.j2 | 44 ++ machines/templates/hostname.bridge.j2 | 4 + machines/templates/hostname.native.j2 | 7 + machines/templates/hostname.vlan.j2 | 9 + machines/templates/maclist.conf.j2 | 8 + machines/templates/named.conf.j2 | 6 + machines/templates/rad.conf.j2 | 15 + machines/templates/radvd.conf.j2 | 31 + machines/templates/reverse.zone.j2 | 15 + machines/templates/rtadvd.conf.j2 | 10 + machines/templates/unreg-lockdown.conf.j2 | 34 + machines/types_test.go | 30 + utils/strings2/strings2.go | 18 + 34 files changed, 3614 insertions(+), 52 deletions(-) create mode 100644 machines/agent.go create mode 100644 machines/coredns_plugin/registry.go create mode 100644 machines/coredns_plugin/registry_store.go create mode 100644 machines/coredns_plugin/setup.go create mode 100644 machines/coredns_plugin/stats.go create mode 100644 machines/machines_agent/Makefile create mode 100644 machines/machines_agent/main.go create mode 100644 machines/render.go create mode 100644 machines/router_addresses_bsd.go create mode 100644 machines/router_addresses_linux.go create mode 100644 machines/services.go create mode 100644 machines/services_linux.go create mode 100644 machines/stats.go create mode 100644 machines/templates/corefile.conf.j2 create mode 100644 machines/templates/dhcpd4.conf.j2 create mode 100644 machines/templates/dhcpd6.conf.j2 create mode 100644 machines/templates/forward.zone.j2 create mode 100644 machines/templates/hostname.bridge.j2 create mode 100644 machines/templates/hostname.native.j2 create mode 100644 machines/templates/hostname.vlan.j2 create mode 100644 machines/templates/maclist.conf.j2 create mode 100644 machines/templates/named.conf.j2 create mode 100644 machines/templates/rad.conf.j2 create mode 100644 machines/templates/radvd.conf.j2 create mode 100644 machines/templates/reverse.zone.j2 create mode 100644 machines/templates/rtadvd.conf.j2 create mode 100644 machines/templates/unreg-lockdown.conf.j2 create mode 100644 machines/types_test.go create mode 100644 utils/strings2/strings2.go diff --git a/go.mod b/go.mod index 13a3f50..64e2ce9 100644 --- a/go.mod +++ b/go.mod @@ -7,12 +7,13 @@ require ( github.com/google/go-attestation v0.4.3 github.com/google/go-tpm v0.3.3 // indirect github.com/google/go-tspi v0.2.1-0.20190423175329-115dea689aad // indirect - golang.org/x/crypto v0.14.0 // indirect - golang.org/x/sys v0.13.0 // indirect + golang.org/x/crypto v0.18.0 // indirect + golang.org/x/sys v0.16.0 // indirect ) require ( github.com/ThalesIgnite/crypto11 v1.2.5 + github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf github.com/distribution/distribution/v3 v3.0.0-20230621170613-87b280718d38 github.com/eclipse/paho.mqtt.golang v1.4.2 github.com/go-kit/log v0.2.1 @@ -21,19 +22,22 @@ require ( github.com/hashicorp/golang-lru v0.5.4 github.com/keybase/go-keychain v0.0.0-20230523030712-b5615109f100 github.com/mdlayher/apcupsd v0.0.0-20230802135538-48f5030bcd58 - github.com/miekg/dns v1.1.50 - github.com/prometheus/client_golang v1.14.0 + github.com/miekg/dns v1.1.55 + github.com/nikolalohinski/gonja v1.5.3 + github.com/prometheus/client_golang v1.18.0 github.com/prometheus/exporter-toolkit v0.8.1 github.com/quic-go/quic-go v0.39.0 - github.com/sirupsen/logrus v1.9.0 + github.com/sirupsen/logrus v1.9.3 + github.com/stretchr/testify v1.8.4 github.com/urfave/cli/v2 v2.23.5 - go.etcd.io/etcd/client/v3 v3.5.5 + go.etcd.io/etcd/client/v3 v3.5.12 go.fuhry.dev/fsnotify v1.7.2 go.fuhry.dev/grpc-quic v0.1.2 - golang.org/x/exp v0.0.0-20221205204356-47842c84f3db + golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 golang.org/x/sync v0.3.0 - golang.org/x/term v0.13.0 + golang.org/x/term v0.16.0 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c + gopkg.in/ini.v1 v1.67.0 gopkg.in/yaml.v3 v3.0.1 howett.net/plist v1.0.0 ) @@ -41,69 +45,85 @@ require ( require ( github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e // indirect github.com/BurntSushi/toml v1.2.1 // indirect + github.com/apparentlymart/go-cidr v1.1.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect - github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba // indirect github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect github.com/docker/go-metrics v0.0.1 // indirect github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect github.com/felixge/httpsnoop v1.0.1 // indirect + github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect github.com/go-logfmt/logfmt v0.5.1 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect + github.com/godbus/dbus v4.1.0+incompatible // indirect github.com/gomodule/redigo v1.8.2 // indirect - github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect + github.com/google/pprof v0.0.0-20230509042627-b1315fad0c5a // indirect + github.com/goph/emperror v0.17.2 // indirect github.com/gorilla/handlers v1.5.1 // indirect github.com/gorilla/mux v1.8.0 // indirect + github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 // indirect github.com/jpillora/backoff v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.16.5 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/kr/text v0.2.0 // indirect - github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/miekg/pkcs11 v1.1.1 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect - github.com/onsi/ginkgo/v2 v2.9.5 // indirect + github.com/onsi/ginkgo/v2 v2.11.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.0.2 // indirect + github.com/opentracing/opentracing-go v1.2.0 // indirect + github.com/pelletier/go-toml/v2 v2.0.9 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_model v0.5.0 // indirect - github.com/prometheus/common v0.37.0 // indirect - github.com/prometheus/procfs v0.8.0 // indirect + github.com/prometheus/common v0.48.0 // indirect + github.com/prometheus/procfs v0.12.0 // indirect github.com/quic-go/qtls-go1-20 v0.3.4 // indirect - github.com/rogpeppe/go-internal v1.9.0 // indirect + github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/stretchr/testify v1.8.4 // indirect github.com/thales-e-security/pool v0.0.2 // indirect + github.com/vishvananda/netlink v1.1.0 // indirect + github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect + github.com/yargevad/filepathx v1.0.0 // indirect go.uber.org/mock v0.3.0 // indirect golang.org/x/mod v0.11.0 // indirect - golang.org/x/oauth2 v0.10.0 // indirect + golang.org/x/oauth2 v0.16.0 // indirect golang.org/x/tools v0.10.0 // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) require ( + github.com/coredns/caddy v1.1.1 + github.com/coredns/coredns v1.11.1 github.com/coreos/go-semver v0.3.0 // indirect github.com/coreos/go-systemd/v22 v22.4.0 // indirect github.com/go-ldap/ldap/v3 v3.4.4 github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/hashicorp/golang-lru/v2 v2.0.2 - go.etcd.io/etcd/api/v3 v3.5.5 // indirect - go.etcd.io/etcd/client/pkg/v3 v3.5.5 - go.uber.org/atomic v1.9.0 + go.etcd.io/etcd/api/v3 v3.5.12 // indirect + go.etcd.io/etcd/client/pkg/v3 v3.5.12 + go.uber.org/atomic v1.11.0 go.uber.org/multierr v1.8.0 // indirect go.uber.org/zap v1.21.0 // indirect - golang.org/x/net v0.17.0 // indirect - golang.org/x/text v0.13.0 - google.golang.org/genproto v0.0.0-20230711160842-782d3b101e98 // indirect - google.golang.org/grpc v1.58.3 - google.golang.org/protobuf v1.31.0 + golang.org/x/net v0.20.0 // indirect + golang.org/x/text v0.14.0 + google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d // indirect + google.golang.org/grpc v1.59.0 + google.golang.org/protobuf v1.32.0 ) replace github.com/keybase/go-keychain => github.com/fuhry/go-keychain v0.0.0-20231123062916-83673b79739d diff --git a/go.sum b/go.sum index 434c6da..559d600 100644 --- a/go.sum +++ b/go.sum @@ -52,6 +52,7 @@ github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuN github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/ThalesIgnite/crypto11 v1.2.5 h1:1IiIIEqYmBvUYFeMnHqRft4bwf/O36jryEUpY+9ef8E= github.com/ThalesIgnite/crypto11 v1.2.5/go.mod h1:ILDKtnCKiQ7zRoNxcp36Y1ZR8LBPmR2E23+wTQe/MlE= +github.com/airbrake/gobrake v3.6.1+incompatible/go.mod h1:wM4gu3Cn0W0K7GUuVWnlXZU11AGBXMILnrdOU8Kn00o= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= @@ -60,6 +61,8 @@ github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk5 github.com/antihax/optional v0.0.0-20180407024304-ca021399b1a6/go.mod h1:V8iCPQYkqmusNa815XgQio277wI47sdRh1dUOLdyC6Q= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/aokoli/goutils v1.0.1/go.mod h1:SijmP0QR8LtwsmDs8Yii5Z/S4trXFGFC2oO5g9DP+DQ= +github.com/apparentlymart/go-cidr v1.1.0 h1:2mAhrMoF+nhXqxTzSZMUzDHkLjmIHC+Zzn4tdgBZjnU= +github.com/apparentlymart/go-cidr v1.1.0/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/YjEn/vI25Lg7Gwc= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= @@ -73,8 +76,13 @@ github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+Ce github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= +github.com/bugsnag/bugsnag-go v1.4.0/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= +github.com/bugsnag/panicwrap v1.2.0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/certifi/gocertifi v0.0.0-20190105021004-abcd57078448/go.mod h1:GJKEexRPVJrBSOjoqN5VNOIKJ5Q3RViH6eu3puDRwx4= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -88,6 +96,10 @@ github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGX github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= +github.com/coredns/caddy v1.1.1 h1:2eYKZT7i6yxIfGP3qLJoJ7HAsDJqYB+X68g4NYjSrE0= +github.com/coredns/caddy v1.1.1/go.mod h1:A6ntJQlAWuQfFlsd9hvigKbo2WS0VUs2l1e2F+BawD4= +github.com/coredns/coredns v1.11.1 h1:IYBM+j/Xx3nTV4HE1s626G9msmJZSdKL9k0ZagYcZFQ= +github.com/coredns/coredns v1.11.1/go.mod h1:X0ac9RLzd/WAxKuEe3A52miPSm6XjfoxVNAjEQgjphk= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= @@ -131,6 +143,8 @@ github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1 h1:ZClxb8laGDf5arX github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/eclipse/paho.mqtt.golang v1.4.2 h1:66wOzfUHSSI1zamx7jR6yMEI5EuHnT1G6rNA5PM12m4= github.com/eclipse/paho.mqtt.golang v1.4.2/go.mod h1:JGt0RsEwEX+Xa/agj90YJ9d9DH2b7upDZMK9HRbFvCA= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -144,13 +158,17 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7 github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ= github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ= +github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fuhry/go-keychain v0.0.0-20231123062916-83673b79739d h1:2Ow4x25aoCCeuc77bpZKsQtgnVEr2A2qqg26VtjDgD0= github.com/fuhry/go-keychain v0.0.0-20231123062916-83673b79739d/go.mod h1:phb4Vcwy5vWTWputEGnbcsrrSNviOcQBj6Yz9PQGLwc= github.com/fullstorydev/grpcurl v1.6.0/go.mod h1:ZQ+ayqbKMJNhzLmbpCiurTVlaK2M/3nqZCxaQ2Ze/sM= +github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-asn1-ber/asn1-ber v1.5.4 h1:vXT6d/FNDiELJnLb6hGNa309LMsrCoYFvpwHDF0+Y1A= github.com/go-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= +github.com/go-check/check v0.0.0-20180628173108-788fd7840127 h1:0gkP6mzaMqkmpcJYCFOLkIBwI7xFExG03bbkOkCvUPI= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= @@ -173,9 +191,12 @@ github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LB github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/godbus/dbus v4.1.0+incompatible h1:WqqLRTsQic3apZUK9qC5sGNfXthmPXzUZ7nQPrNITa4= +github.com/godbus/dbus v4.1.0+incompatible/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= @@ -261,6 +282,8 @@ github.com/google/pprof v0.0.0-20200507031123-427632fa3b1c/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20230509042627-b1315fad0c5a h1:PEOGDI1kkyW37YqPWHLHc+D20D9+87Wt12TCcfTUo5Q= +github.com/google/pprof v0.0.0-20230509042627-b1315fad0c5a/go.mod h1:79YE0hCXdHag9sBkw2o+N/YnZtTkXi0UT9Nnixa5eYk= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/trillian v1.3.11/go.mod h1:0tPraVHrSDkA3BO6vKX67zgLXs6SsOAbHEivX+9mPgw= github.com/google/uuid v0.0.0-20161128191214-064e2069ce9c/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -269,6 +292,8 @@ github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/goph/emperror v0.17.2 h1:yLapQcmEsO0ipe9p5TaN22djm3OFV/TfM/fcYP0/J18= +github.com/goph/emperror v0.17.2/go.mod h1:+ZbQ+fUNO/6FNiUo0ujtMjhgad9Xa6fQL9KhH4LNHic= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gordonklaus/ineffassign v0.0.0-20200309095847-7953dde2c7bf/go.mod h1:cuNKsD1zp2v6XfE/orVX2QE1LC+i254ceGcVeDT3pTU= github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= @@ -289,6 +314,8 @@ github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.12.1/go.mod h1:8XEsbTttt/W+VvjtQhLACqCisSPWTxCZ7sBRjU6iH9c= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 h1:MJG/KsmcqMwFAkh8mTnAwhyKoB+sTAnY4CACC110tbU= +github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645/go.mod h1:6iZfnjpejD4L/4DwD7NryNaJyCQdzwWwH2MWhCA90Kw= github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -332,6 +359,7 @@ github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCV github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= @@ -339,6 +367,7 @@ github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfV github.com/juju/ratelimit v1.0.1/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= @@ -363,21 +392,28 @@ github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= +github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/mdlayher/apcupsd v0.0.0-20230802135538-48f5030bcd58 h1:e1AuZg7Lk0WSy8OiFaoLV+gXIUH1+Bg3tcYLLQJCZBs= github.com/mdlayher/apcupsd v0.0.0-20230802135538-48f5030bcd58/go.mod h1:ngUsvRNfxdlJb0cHAlP6xDmCDJGJhXPKAH0ExDdDAU0= +github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA= github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= +github.com/miekg/dns v1.1.55 h1:GoQ4hpsj0nFLYe+bWiCToyrBEJXkQfOOIvFGFy0lEgo= +github.com/miekg/dns v1.1.55/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY= github.com/miekg/pkcs11 v1.0.2/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= github.com/miekg/pkcs11 v1.0.3-0.20190429190417-a667d056470f/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= @@ -395,9 +431,11 @@ github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/mitchellh/reflectwalk v1.0.1/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= @@ -405,15 +443,21 @@ github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+ github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-proto-validators v0.0.0-20180403085117-0950a7990007/go.mod h1:m2XC9Qq0AlmmVksL6FktJCdTYyLk7V3fKyp0sl1yWQo= github.com/mwitkow/go-proto-validators v0.2.0/go.mod h1:ZfA1hW+UH/2ZHOWvQ3HnQaU0DtnpXu850MZiy+YUgcc= +github.com/nikolalohinski/gonja v1.5.3 h1:GsA+EEaZDZPGJ8JtpeGN78jidhOlxeJROpqMT9fTj9c= +github.com/nikolalohinski/gonja v1.5.3/go.mod h1:RmjwxNiXAEqcq1HeK5SSMmqFJvKOfTfXhkJv6YBtPa4= github.com/nishanths/predeclared v0.0.0-20190419143655-18a43bb90ffc/go.mod h1:62PewwiQTlm/7Rj+cxVYqZvDIUc+JjZq6GHAC1fsObQ= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= github.com/olekukonko/tablewriter v0.0.2/go.mod h1:rSAaSIOAGT9odnlyGlUfAJaoc5w2fSBUmeGDbRWPxyQ= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q= github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k= +github.com/onsi/ginkgo/v2 v2.11.0 h1:WgqUCUt/lT6yXoQ8Wef0fsNn5cAuMK7+KT9UFRz2tcU= +github.com/onsi/ginkgo/v2 v2.11.0/go.mod h1:ZhrRA5XmEE3x3rhlzamx/JJvujdZoJ2uvgI7kR0iZvM= +github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= @@ -421,8 +465,12 @@ github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3I github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= +github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pelletier/go-toml/v2 v2.0.9 h1:uH2qQXheeefCCkuBBSLi7jCiSmj3VRh2+Goq2N7Xxu0= +github.com/pelletier/go-toml/v2 v2.0.9/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -442,6 +490,10 @@ github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqr github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw= github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y= +github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8= +github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc= +github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk= +github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= @@ -457,6 +509,10 @@ github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9 github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE= github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA= +github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY= +github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY= +github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE= +github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= github.com/prometheus/exporter-toolkit v0.8.1 h1:TpKt8z55q1zF30BYaZKqh+bODY0WtByHDOhDA2M9pEs= github.com/prometheus/exporter-toolkit v0.8.1/go.mod h1:00shzmJL7KxcsabLWcONwpyNEuWhREOnFqZW7vadFS0= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= @@ -468,6 +524,10 @@ github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1 github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo= github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= +github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg= +github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM= +github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= +github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/pseudomuto/protoc-gen-doc v1.3.2/go.mod h1:y5+P6n3iGrbKG+9O04V5ld71in3v/bX88wUwgt+U8EA= github.com/pseudomuto/protokit v0.2.0/go.mod h1:2PdH30hxVHsup8KpBTOXTBeMVhJZVio3Q8ViKSAXT0Q= @@ -480,6 +540,9 @@ github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6L github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/rollbar/rollbar-go v1.0.2/go.mod h1:AcFs5f0I+c71bpHlXNNDbOWJiKwjFDtISeXco0L5PKQ= github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -492,8 +555,8 @@ github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeV github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= -github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= -github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= @@ -513,6 +576,8 @@ github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/y github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v0.0.0-20170130113145-4d4bfba8f1d1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= @@ -520,7 +585,9 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= @@ -536,10 +603,17 @@ github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijb github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli/v2 v2.23.5 h1:xbrU7tAYviSpqeR3X4nEFWUdB/uDZ6DE+HxmRU7Xtyw= github.com/urfave/cli/v2 v2.23.5/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc= +github.com/vishvananda/netlink v1.1.0 h1:1iyaYNBLmP6L0220aDnYQpo1QEV4t4hJ+xEEhhJH8j0= +github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= +github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df h1:OviZH7qLw/7ZovXvuNyL3XQl8UFofeikI1NW1Gypu7k= +github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= +github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= +github.com/yargevad/filepathx v1.0.0 h1:SYcT+N3tYGi+NvazubCNlvgIPbzAk7i7y2dwg3I5FYc= +github.com/yargevad/filepathx v1.0.0/go.mod h1:BprfX/gpYNJHJfc35GjRRpVcwWXS89gGulUIU5tK3tA= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -551,10 +625,20 @@ go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= go.etcd.io/etcd v0.0.0-20200513171258-e048e166ab9c/go.mod h1:xCI7ZzBfRuGgBXyXO6yfWfDmlWd35khcWpUa4L0xI/k= go.etcd.io/etcd/api/v3 v3.5.5 h1:BX4JIbQ7hl7+jL+g+2j5UAr0o1bctCm6/Ct+ArBGkf0= go.etcd.io/etcd/api/v3 v3.5.5/go.mod h1:KFtNaxGDw4Yx/BA4iPPwevUTAuqcsPxzyX8PHydchN8= +go.etcd.io/etcd/api/v3 v3.5.9/go.mod h1:uyAal843mC8uUVSLWz6eHa/d971iDGnCRpmKd2Z+X8k= +go.etcd.io/etcd/api/v3 v3.5.12 h1:W4sw5ZoU2Juc9gBWuLk5U6fHfNVyY1WC5g9uiXZio/c= +go.etcd.io/etcd/api/v3 v3.5.12/go.mod h1:Ot+o0SWSyT6uHhA56al1oCED0JImsRiU9Dc26+C2a+4= go.etcd.io/etcd/client/pkg/v3 v3.5.5 h1:9S0JUVvmrVl7wCF39iTQthdaaNIiAaQbmK75ogO6GU8= go.etcd.io/etcd/client/pkg/v3 v3.5.5/go.mod h1:ggrwbk069qxpKPq8/FKkQ3Xq9y39kbFR4LnKszpRXeQ= +go.etcd.io/etcd/client/pkg/v3 v3.5.9/go.mod h1:y+CzeSmkMpWN2Jyu1npecjB9BBnABxGM4pN8cGuJeL4= +go.etcd.io/etcd/client/pkg/v3 v3.5.12 h1:EYDL6pWwyOsylrQyLp2w+HkQ46ATiOvoEdMarindU2A= +go.etcd.io/etcd/client/pkg/v3 v3.5.12/go.mod h1:seTzl2d9APP8R5Y2hFL3NVlD6qC/dOT+3kvrqPyTas4= go.etcd.io/etcd/client/v3 v3.5.5 h1:q++2WTJbUgpQu4B6hCuT7VkdwaTP7Qz6Daak3WzbrlI= go.etcd.io/etcd/client/v3 v3.5.5/go.mod h1:aApjR4WGlSumpnJ2kloS75h6aHUmAyaPLjHMxpc7E7c= +go.etcd.io/etcd/client/v3 v3.5.9 h1:r5xghnU7CwbUxD/fbUtRyJGaYNfDun8sp/gTr1hew6E= +go.etcd.io/etcd/client/v3 v3.5.9/go.mod h1:i/Eo5LrZ5IKqpbtpPDuaUnDOUv471oDg8cjQaUr2MbA= +go.etcd.io/etcd/client/v3 v3.5.12 h1:v5lCPXn1pf1Uu3M4laUE2hp/geOTc5uPcYYsNe1lDxg= +go.etcd.io/etcd/client/v3 v3.5.12/go.mod h1:tSbBCakoWmmddL+BKVAJHa9km+O/E+bumDe9mSbPiqw= go.fuhry.dev/fsnotify v1.7.2 h1:jPBFFKaJKUv8kSl66IHkkbBmV499/y1qqjHmecApbtg= go.fuhry.dev/fsnotify v1.7.2/go.mod h1:/nwTYd9m6GEeyIewNJFg3ikw+GsMe3EwpzlI5wB0Cz4= go.fuhry.dev/grpc-quic v0.1.2 h1:wJsr1rtkDxcX4fBaZDlRodBnvl2PIw9001EQR0zFbys= @@ -571,6 +655,8 @@ go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/mock v0.3.0 h1:3mUxI1No2/60yUYax92Pt8eNOEecx2D3lcXZh2NEZJo= @@ -600,6 +686,8 @@ golang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b/go.mod h1:T9bdIzuCu7OtxOm golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -611,8 +699,8 @@ golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw= -golang.org/x/exp v0.0.0-20221205204356-47842c84f3db h1:D/cFflL63o2KSLJIwjlcIt8PR064j/xsmdEJL/YvY/o= -golang.org/x/exp v0.0.0-20221205204356-47842c84f3db/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= +golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 h1:MGwJjxBy0HJshjDNfLsYO8xppfqWlA5ZT9OhtUUhTNw= +golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -685,6 +773,8 @@ golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -694,6 +784,9 @@ golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/oauth2 v0.10.0 h1:zHCpF2Khkwy4mMB4bv0U37YtJdTGW8jI0glAApi0Kh8= golang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQI= +golang.org/x/oauth2 v0.11.0/go.mod h1:LdF7O/8bLR/qWK9DrpXmbHLTouvRHK0SgJl0GmDBchk= +golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ= +golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -724,6 +817,7 @@ golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -767,10 +861,14 @@ golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= +golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE= +golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -781,6 +879,8 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -913,10 +1013,21 @@ google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= google.golang.org/genproto v0.0.0-20230711160842-782d3b101e98 h1:Z0hjGZePRE0ZBWotvtrwxFNrNE9CUAGtplaDK5NNI/g= google.golang.org/genproto v0.0.0-20230711160842-782d3b101e98/go.mod h1:S7mY02OqCJTD0E1OiQy1F72PWFB4bZJ87cAtLPYgDR0= +google.golang.org/genproto v0.0.0-20230803162519-f966b187b2e5 h1:L6iMMGrtzgHsWofoFcihmDEMYeDR9KN/ThbPWGrh++g= +google.golang.org/genproto v0.0.0-20230803162519-f966b187b2e5/go.mod h1:oH/ZOT02u4kWEp7oYBGYFFkCdKS/uYR9Z7+0/xuuFp8= +google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d h1:VBu5YqKPv6XiJ199exd8Br+Aetz+o08F+PLMnwJQHAY= +google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d/go.mod h1:yZTlhN0tQnXo3h00fuXNCxJdLdIdnVFVBaRJ5LWBbw4= google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98 h1:FmF5cCW94Ij59cfpoLiwTgodWmm60eEV0CjlsVg2fuw= google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98/go.mod h1:rsr7RhLuwsDKL7RmgDDCUc6yaGr1iqceVb5Wv6f6YvQ= +google.golang.org/genproto/googleapis/api v0.0.0-20230803162519-f966b187b2e5/go.mod h1:5DZzOUPCLYL3mNkQ0ms0F3EuUNZ7py1Bqeq6sxzI7/Q= +google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d h1:DoPTO70H+bcDXcd39vOqb2viZxgqeBeSGtZ55yZU4/Q= +google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d/go.mod h1:KjSP20unUpOx5kyQUFa7k4OJg0qeJ7DEZflGDu2p6Bk= google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 h1:bVf09lpb+OJbByTj913DRJioFFAjf/ZGxEz7MajTp2U= google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98/go.mod h1:TUfxEVdsvPg18p6AslUXFoLdpED4oBnGwyqk3dV1XzM= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230807174057-1744710a1577 h1:wukfNtZmZUurLN/atp2hiIeTKn7QJWIQdHzqmsOnAOk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230807174057-1744710a1577/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d h1:uvYuEyMHKNt+lT4K3bN6fGswmK8qSvcreM3BwjDh+y4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M= google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= @@ -940,6 +1051,8 @@ google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQ google.golang.org/grpc v1.41.0/go.mod h1:U3l9uK9J0sini8mHphKoXyaqDA/8VyGnDee1zzIUK6k= google.golang.org/grpc v1.58.3 h1:BjnpXut1btbtgN/6sp+brB2Kbm2LjNXnidYujAVbSoQ= google.golang.org/grpc v1.58.3/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0= +google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk= +google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -955,6 +1068,8 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= +google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -966,6 +1081,8 @@ gopkg.in/cheggaaa/pb.v1 v1.0.28/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qS gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg= diff --git a/machines/agent.go b/machines/agent.go new file mode 100644 index 0000000..dc5a8cf --- /dev/null +++ b/machines/agent.go @@ -0,0 +1,217 @@ +package machines + +import ( + "fmt" + "net/url" + "sync" + + "go.fuhry.dev/runtime/utils/hashset" +) + +type fileGeneratorFunc = func(MachinesClient, *MachinesMqttEvent) []string + +func staticFiles(files ...string) fileGeneratorFunc { + return func(MachinesClient, *MachinesMqttEvent) []string { + return files + } +} + +type domainsCache struct { + domains []*Domain + sites map[string]*Site + mu sync.Mutex +} + +func (dc *domainsCache) getDomains(client MachinesClient) []*Domain { + dc.mu.Lock() + defer dc.mu.Unlock() + + if dc.domains != nil { + return dc.domains + } + + domains := []*Domain{} + err := client.APICall("domains", nil, &domains) + if err != nil { + logger.AppendPrefix(".domainsCache").Warningf("failed to get domains: %v", err) + return domains + } + + dc.domains = domains + return domains +} + +func (dc *domainsCache) getSite(client MachinesClient, siteID string) *Site { + dc.mu.Lock() + defer dc.mu.Unlock() + + if dc.sites == nil { + dc.sites = make(map[string]*Site) + } + + if site, ok := dc.sites[siteID]; ok { + return site + } + + site := &Site{} + err := client.APICall("site/"+siteID, nil, site) + if err != nil { + logger.AppendPrefix(".domainsCache").Warningf("failed to get site with id %s: %v", siteID, err) + return site + } + + dc.sites[siteID] = site + return site +} + +func (dc *domainsCache) hostSeenGenerator(client MachinesClient, ev *MachinesMqttEvent) []string { + if via, ok := ev.Tags["via"]; !ok || via != "dhcp" { + logger.Debugf("hostSeenGenerator: rejecting event due to missing \"via\" tag: %s", via) + return []string{} + } + + ip, ok := ev.Tags["ip"] + if !ok { + logger.Debugf("hostSeenGenerator: rejecting event due to missing \"ip\" tag") + return []string{} + } + + ipstr := IPString(ip) + var previp IPString + if previpstr, ok := ev.Tags["prev_ip"]; ok { + previp = IPString(previpstr) + } + + var prevDomain string + if pd, ok := ev.Tags["prev_domain"]; ok { + prevDomain = pd + } + + zones := hashset.NewHashSet[string]() + + var site *Site + if machinesSiteName != "" { + site = dc.getSite(client, url.PathEscape(machinesSiteName)) + } + + for _, domain := range dc.getDomains(client) { + if site != nil && site.ID() != domain.Site.ID() { + continue + } + + if ipstr.IsIPv4() && domain.IPv4Address.Defined() { + logger.Debugf("ipv4 address: checking if %s is on network %s", ipstr, domain.IPv4Address) + mask := domain.IPv4PrefixLength.IPMask() + ipnet := ipstr.AsIP().Mask(mask) + logger.Debugf("%s / %s", ipnet.String(), domain.IPv4Address.AsIP().String()) + match := ipnet.Equal(domain.IPv4Address.AsIP()) + + if !match && previp.Defined() { + // if the host moved away from this network, we also need to regen this zone + mask := domain.IPv4PrefixLength.IPMask() + ipnet := previp.AsIP().Mask(mask) + match = ipnet.Equal(domain.IPv4Address.AsIP()) + } + + if match || domain.ID() == prevDomain { + zones.Add(fmt.Sprintf("forward.zone/%s", domain.Name)) + zones.Add(fmt.Sprintf("reverse.zone/%s", domain.ReverseDNSZoneIPv4)) + } + } + + if !ipstr.IsIPv4() && domain.IPv6Address.Defined() { + mask := domain.IPv6PrefixLength.IPMask() + ipnet := ipstr.AsIP().Mask(mask) + + match := ipnet.Equal(domain.IPv6Address.AsIP()) + + if !match && previp.Defined() { + // if the host moved away from this network, we also need to regen this zone + mask := domain.IPv6PrefixLength.IPMask() + ipnet := previp.AsIP().Mask(mask) + match = ipnet.Equal(domain.IPv6Address.AsIP()) + } + + if match || domain.ID() == prevDomain { + zones.Add(fmt.Sprintf("forward.zone/%s", domain.Name)) + zones.Add(fmt.Sprintf("reverse.zone/%s", domain.ReverseDNSZoneIPv6)) + } + } + } + + return zones.AsSlice() +} + +func (dc *domainsCache) domainModifiedGenerator(client MachinesClient, ev *MachinesMqttEvent) []string { + dc.domains = nil + + return []string{"dhcpd4.conf", "dhcpd6.conf"} +} + +var domainsCacheSingleton = &domainsCache{} + +func multiFile(funcs ...fileGeneratorFunc) fileGeneratorFunc { + return func(c MachinesClient, ev *MachinesMqttEvent) []string { + s := hashset.NewHashSet[string]() + for _, f := range funcs { + s.Add(f(c, ev)...) + } + return s.AsSlice() + } +} + +type actionHook struct { + thing string + action string + fileGenerator fileGeneratorFunc +} + +var actionHooks = []*actionHook{ + { + thing: "reservation", + action: "created", + fileGenerator: staticFiles("dhcpd4.conf", "dhcpd6.conf"), + }, + { + thing: "reservation", + action: "deleted", + fileGenerator: staticFiles("dhcpd4.conf", "dhcpd6.conf"), + }, + // { + // thing: "host", + // action: "seen", + // fileGenerator: domainsCacheSingleton.hostSeenGenerator, + // }, + { + thing: "domain", + action: "*", + fileGenerator: multiFile( + domainsCacheSingleton.domainModifiedGenerator, + staticFiles("dhcpd4.conf", "dhcpd6.conf"), + ), + }, + { + thing: "iface", + action: "*", + fileGenerator: multiFile( + staticFiles("dhcpd4.conf", "dhcpd6.conf"), + ), + }, +} + +func (ah *actionHook) Matches(cl MachinesClient, ev *MachinesMqttEvent) bool { + if ah.action == "" && ah.thing == "" { + // invalid hook + return false + } + + if ah.thing != "*" && ev.ItemType != ah.thing { + return false + } + + if ah.action != "*" && ev.Event != ah.action { + return false + } + + return true +} diff --git a/machines/client.go b/machines/client.go index baffa99..0959092 100644 --- a/machines/client.go +++ b/machines/client.go @@ -9,24 +9,46 @@ import ( "fmt" "io" "net/http" + "strconv" "strings" + "sync" + "time" + + "gopkg.in/ini.v1" "go.fuhry.dev/runtime/constants" + "go.fuhry.dev/runtime/metrics/metricbus/mbclient" "go.fuhry.dev/runtime/utils/log" ) +const ( + defaultRouterAddressOffset uint = 1 +) + var ( - defaultMachinesApiUrl = "https://" + constants.MachinesHost + "/api/" - defaultMachinesEventsUrl = "wss://" + constants.MachinesHost + "/events" - defaultMachinesApiScopes = "read" - machinesApiUrl, machinesEventsUrl string - machinesOauthClientId, machinesOauthClientSecret string - machinesApiScopes string + defaultMachinesApiUrl, + defaultMachinesEventsUrl, + defaultMachinesApiScopes, + defaultMachinesClientIniPath, + machinesSiteName, + machinesApiUrl, + machinesEventsUrl, + machinesClientIniPath, + machinesOauthClientId, + machinesOauthClientSecret, + machinesApiScopes string + + machinesRouterAddressOffset uint + + machinesIniOnce sync.Once + + logger = log.WithPrefix("machines.client") ) type MachinesClient interface { APICall(route string, data interface{}, response any) error NewEventListener(ctx context.Context) (chan MachinesMqttEvent, error) + SetupStats(metrics *mbclient.MetricBusService) } type machinesClient struct { @@ -36,23 +58,35 @@ type machinesClient struct { baseUrl string eventsUrl string logger *log.Logger + stats *clientStats } func init() { + defaultMachinesApiUrl = "https://" + constants.MachinesHost + "/api/" + defaultMachinesEventsUrl = "wss://" + constants.MachinesHost + "/events" + defaultMachinesApiScopes = "read" + // this path and the usage of ini are what we know as "legacy cruft" + defaultMachinesClientIniPath = constants.SystemConfDir + "-machines/client.conf" + + flag.StringVar(&machinesClientIniPath, "machines.client.config", defaultMachinesClientIniPath, "Absolute path to the configuration file for the Machines API client.") flag.StringVar(&machinesApiUrl, "machines.api-url", defaultMachinesApiUrl, "URL to the Machines API") flag.StringVar(&machinesEventsUrl, "machines.events-url", defaultMachinesEventsUrl, "Machines MQTT WebSocket URL") flag.StringVar(&machinesOauthClientId, "machines.client-id", "", "OAuth client ID for the Machines API") flag.StringVar(&machinesOauthClientSecret, "machines.client-secret", "", "Client secret for the Machines API") flag.StringVar(&machinesApiScopes, "machines.scopes", defaultMachinesApiScopes, "comma-separated list of OAuth scopes for the Machines API") + flag.StringVar(&machinesSiteName, "machines.site-name", "", "name of the local site - required by the agent to generate templates") + flag.UintVar(&machinesRouterAddressOffset, "machines.router-address", defaultRouterAddressOffset, "IP address offset of the router from the start of the subnet") } -func NewDefaultMachinesClient(scopes ...string) (*machinesClient, error) { +func NewDefaultMachinesClient(scopes ...string) (MachinesClient, error) { if !flag.Parsed() { return nil, errors.New("flags have not been parsed yet") } + machinesIniOnce.Do(parseMachinesClientIni) + if len(scopes) == 0 { - scopes = strings.Split(machinesApiScopes, ",") + scopes = strings.Split(machinesApiScopes, " ") } if machinesOauthClientId == "" || machinesOauthClientSecret == "" { @@ -62,8 +96,14 @@ func NewDefaultMachinesClient(scopes ...string) (*machinesClient, error) { return NewMachinesClient(machinesApiUrl, machinesEventsUrl, machinesOauthClientId, machinesOauthClientSecret, scopes) } -func NewMachinesClient(apiUrl, eventsUrl, clientId, clientSecret string, scopes []string) (*machinesClient, error) { - httpClient := &http.Client{} +func NewMachinesClient(apiUrl, eventsUrl, clientId, clientSecret string, scopes []string) (MachinesClient, error) { + httpClient := &http.Client{ + Transport: &http.Transport{ + TLSHandshakeTimeout: 5 * time.Second, + ResponseHeaderTimeout: 10 * time.Second, + }, + } + apiUrl = strings.TrimRight(apiUrl, "/") err := SetupOAuthClient(httpClient, fmt.Sprintf("%s/oauth/token", apiUrl), @@ -105,6 +145,7 @@ func (mc *machinesClient) APICall(route string, data interface{}, response any) route = strings.TrimLeft(route, "/") + logger.V(1).Infof("making request: %s %s", method, route) req, err := http.NewRequest(method, fmt.Sprintf("%s/%s", mc.baseUrl, route), body) if err != nil { return err @@ -114,28 +155,74 @@ func (mc *machinesClient) APICall(route string, data interface{}, response any) req.Header.Set("content-type", "application/json") } - resp, err := mc.client.Do(req) + resp, err := mc.client.Transport.RoundTrip(req) if err != nil { + if mc.stats != nil { + mc.stats.apiRequests.WithLabelValues(mbclient.KV{ + "method": method, + "route": route, + "response_code": "0", + "result": "network_failure", + }).Add(1) + } return err } if resp.StatusCode != http.StatusOK { + if mc.stats != nil { + mc.stats.apiRequests.WithLabelValues(mbclient.KV{ + "method": method, + "route": route, + "response_code": strconv.Itoa(resp.StatusCode), + "result": "invalid_response", + }).Add(1) + } + return NewInvalidHttpResponseError(resp) } respBody, err := io.ReadAll(resp.Body) if err != nil { + if mc.stats != nil { + mc.stats.apiRequests.WithLabelValues(mbclient.KV{ + "method": method, + "route": route, + "response_code": strconv.Itoa(resp.StatusCode), + "result": "empty_response", + }).Add(1) + } return err } err = json.Unmarshal(respBody, response) if err != nil { + if mc.stats != nil { + mc.stats.apiRequests.WithLabelValues(mbclient.KV{ + "method": method, + "route": route, + "response_code": strconv.Itoa(resp.StatusCode), + "result": "body_decode_failed", + }).Add(1) + } return fmt.Errorf("error decoding JSON body:\n=====BEGIN JSON=====\n%s\n=====END JSON=====\nerror: %v", respBody, err) } + if mc.stats != nil { + mc.stats.apiRequests.WithLabelValues(mbclient.KV{ + "method": method, + "route": route, + "response_code": strconv.Itoa(resp.StatusCode), + "result": "success", + }).Add(1) + } + return nil } +func (mc *machinesClient) SetupStats(metrics *mbclient.MetricBusService) { + mc.stats = makeStats(metrics) +} + func NewInvalidHttpResponseError(resp *http.Response) error { var bodyStr string body, err := io.ReadAll(resp.Body) @@ -147,3 +234,79 @@ func NewInvalidHttpResponseError(resp *http.Response) error { return fmt.Errorf("received invalid HTTP response with status %d (%s): %s", resp.StatusCode, resp.Status, bodyStr) } + +func parseMachinesClientIni() { + clientIni, err := ini.Load(machinesClientIniPath) + if err != nil { + logger.V(1).Noticef("unable to read Machines client configuration file %s, continuing "+ + "without: %v", machinesClientIniPath, err) + + return + } + + clientSections, err := clientIni.SectionsByName("router") + if err != nil { + logger.V(1).Noticef("Machines client configuration file %s does not contain a [router]"+ + "section, continuing without: %v", machinesClientIniPath, err) + return + } + + for _, sec := range clientSections { + logger.V(2).Infof("parsing ini section: %s", sec.Name()) + + if machinesSiteName == "" { + if key, err := sec.GetKey("site"); err == nil { + machinesSiteName = key.String() + logger.V(2).Infof("set site from machines client config: %s", machinesSiteName) + } + } + + if machinesOauthClientId == "" { + if key, err := sec.GetKey("client_id"); err == nil { + machinesOauthClientId = key.String() + logger.V(2).Infof("set client_id from machines client config: %s", machinesOauthClientId) + } + } + + if machinesOauthClientSecret == "" { + if key, err := sec.GetKey("client_secret"); err == nil { + machinesOauthClientSecret = key.String() + logger.V(2).Infof("set client_secret from machines client config: %s", strings.Repeat("*", len(machinesOauthClientSecret))) + } + } + + if machinesApiUrl == "" || machinesApiUrl == defaultMachinesApiUrl { + if key, err := sec.GetKey("api_url"); err == nil { + machinesApiUrl = key.String() + logger.V(2).Infof("set api_url from machines client config: %s", machinesApiUrl) + } + } + + if machinesEventsUrl == "" || machinesEventsUrl == defaultMachinesEventsUrl { + if key, err := sec.GetKey("mqtt_url"); err == nil { + machinesEventsUrl = key.String() + logger.V(2).Infof("set mqtt_url from machines client config: %s", machinesEventsUrl) + } + } + + if machinesApiScopes == "" || machinesApiScopes == defaultMachinesApiScopes { + if key, err := sec.GetKey("oauth_scope"); err == nil { + machinesApiScopes = key.String() + logger.V(2).Infof("set oauth_scope from machines client config: %s", machinesApiScopes) + } + } + + if machinesRouterAddressOffset == 0 || machinesRouterAddressOffset == defaultRouterAddressOffset { + if key, err := sec.GetKey("gateway_address"); err == nil { + if value, err := key.Uint(); err == nil { + machinesRouterAddressOffset = value + logger.V(2).Infof("set gateway_address from machines client config: %d", machinesRouterAddressOffset) + } + } + } + } +} + +func SiteName() string { + return machinesSiteName +} diff --git a/machines/coredns_plugin/registry.go b/machines/coredns_plugin/registry.go new file mode 100644 index 0000000..8ba93ae --- /dev/null +++ b/machines/coredns_plugin/registry.go @@ -0,0 +1,864 @@ +package coredns_plugin + +import ( + "context" + "errors" + "flag" + "fmt" + "math/rand" + "net" + "slices" + "strings" + "time" + + "github.com/miekg/dns" + + "go.fuhry.dev/runtime/constants" + "go.fuhry.dev/runtime/machines" + "go.fuhry.dev/runtime/metrics/metricbus/mbclient" + "go.fuhry.dev/runtime/utils/hostname" + "go.fuhry.dev/runtime/utils/log" + "go.fuhry.dev/runtime/utils/strings2" +) + +type Registry interface { + LookupHost(fqdn string) (*Result, error) + LookupRecord(dns.Question) (int, []dns.RR) + AuthorityRecords(dns.Question) []dns.RR + AdditionalRecords(dns.Question, []dns.RR) []dns.RR + Startup() error + Shutdown() error +} + +type Result struct { + domain *machines.Domain + host *machines.Host + iface *machines.Iface +} + +type registry struct { + log *log.Logger + ctx context.Context + cancel context.CancelFunc + client machines.MachinesClient + didLiveFetch bool + + mb *mbclient.MetricBusService + stats *dnsStats + + defDomain string + stopCh chan struct{} + eventsCh <-chan machines.MachinesMqttEvent + + store *registryStore +} + +var ErrUnmanagedDomain = errors.New("domain is not managed by this plugin") +var ErrRecordsNoneMatch = errors.New("no custom records match this query") +var ErrUninitialized = errors.New("registry has not finished initializing yet") + +var refreshInterval = 15 * time.Minute +var refreshSplay = 30 * time.Second + +func init() { + setRefresh := func(durVar *time.Duration) func(string) error { + return func(val string) error { + dur, err := time.ParseDuration(val) + if err != nil { + return err + } + *durVar = dur + return nil + } + } + + flag.Func("machines.dns.refresh", + fmt.Sprintf("interval at which to refresh records if no events are received (default: %s)", refreshInterval), + setRefresh(&refreshInterval)) + flag.Func("machines.dns.refresh.splay", + fmt.Sprintf("duration to stagger refresh interval (default: %s)", refreshSplay), + setRefresh(&refreshSplay)) +} + +func NewRegistry(ctx context.Context) Registry { + ctx, cancel := context.WithCancel(ctx) + + metrics := mbclient.NewService(ctx) + + r := ®istry{ + ctx: ctx, + cancel: cancel, + mb: metrics, + stats: makeStats(metrics), + stopCh: make(chan struct{}), + log: log.WithPrefix("machines.dns"), + store: ®istryStore{ + log: log.WithPrefix("machines.dns.registryStore"), + }, + } + + return r +} + +func (r *registry) Startup() error { + err := r.store.loadState() + if err == nil { + r.log.Infof("successfully loaded saved state from %s", machinesRegistryStorePath) + } else { + r.log.Noticef("failed to load saved state from %s, no records will be served "+ + "until the Machines API is reachable: %v", machinesRegistryStorePath, err) + } + + err = r.tryInit() + if err != nil { + r.log.Warningf("failed to initialize API state, continuing startup; " + + "Machines records will be served once the API is available") + } + + go r.monitorEvents() + + return nil +} + +func (r *registry) Shutdown() error { + if r.stopCh == nil { + r.log.Errorf("server is already shutting down") + return nil + } + + r.cancel() + + r.mb.Flush() + + <-r.stopCh + close(r.stopCh) + + r.stopCh = nil + return nil +} + +func (r *registry) defaultDomain() *machines.Domain { + if r.defDomain != "" { + if d, ok := r.store.Domains[r.defDomain]; ok && d != nil { + return d + } + } + // if there is only one local interface aligned to a domain's network address, + // prefer that domain - this server may be a relay target + systemDomainName := hostname.DomainName() + + localDomains := 0 + var localDomain *machines.Domain + for _, domain := range r.store.Domains { + if domain.IPv4Address.Defined() { + if ra, ok := r.store.Addrs.IPv4[domain.IPv4Address.String()]; ok && ra.IsLocalInterface() { + localDomains++ + localDomain = domain + if domain.Name == systemDomainName { + break + } + continue + } + } + + if domain.IPv6Address.Defined() { + if ra, ok := r.store.Addrs.IPv6[domain.IPv6Address.String()]; ok && ra.IsLocalInterface() { + localDomains++ + localDomain = domain + if domain.Name == systemDomainName { + break + } + continue + } + } + } + if localDomains == 1 { + r.defDomain = localDomain.ID() + r.log.Infof("determined default domain using local interface enumeration: %s", localDomain.Name) + return localDomain + } + + // fall back to domain name based check + // track domain with the lowest IP address and use that as fallback + var lowestDomain *machines.Domain + for _, domain := range r.store.Domains { + if domain.IPv4Address.Defined() { + if lowestDomain == nil { + lowestDomain = domain + } else { + if domain.IPv4Address.AsInt32() < lowestDomain.IPv4Address.AsInt32() { + lowestDomain = domain + } + } + } + if domain.Name == systemDomainName { + r.defDomain = domain.ID() + r.log.Infof("determined default domain using hostname detection: %s", domain.Name) + return domain + } + } + + return lowestDomain +} + +func (r *registry) domainFromQuestion(ques dns.Question) *machines.Domain { + qname := strings.TrimSuffix(ques.Name, ".") + for _, domain := range r.store.Domains { + if qname == domain.Name || strings.HasSuffix(qname, "."+domain.Name) { + return domain + } + } + + return nil +} + +func (r *registry) AuthorityRecords(origQues dns.Question) []dns.RR { + qDomain := r.domainFromQuestion(origQues) + if qDomain == nil { + return nil + } + + ques := dns.Question{ + Name: qDomain.Name + ".", + Qtype: dns.TypeNS, + Qclass: dns.ClassINET, + } + + _, nameservers := r.lookupApex(ques, qDomain) + return nameservers +} + +func (r *registry) AdditionalRecords(ques dns.Question, answers []dns.RR) []dns.RR { + addl := make([]dns.RR, 0) + + for _, answer := range answers { + if answer.Header().Rrtype == dns.TypeCNAME { + target := answer.(*dns.CNAME).Target + ques.Name = target + r.log.V(2).Debugf("proposed answer is a CNAME targeting %s, trying recursive lookup", target) + + if rcode, rrs := r.LookupRecord(ques); rcode == dns.RcodeSuccess { + r.log.V(2).Debugf("%s successfully recursed to %+v via LookupRecord", target, rrs) + addl = append(addl, rrs...) + } + if result, err := r.LookupHost(target); result != nil && err == nil { + if rcode, rrs := result.Answer(ques); rcode == dns.RcodeSuccess { + r.log.V(2).Debugf("%s successfully recursed to %+v via LookupHost", target, rrs) + addl = append(addl, rrs...) + } + } + } + } + + return addl +} + +func (r *registry) LookupRecord(ques dns.Question) (int, []dns.RR) { + qname := strings.TrimSuffix(ques.Name, ".") + r.log.V(2).Debugf("LookupRecord(%s)", qname) + + if !r.store.initialized() { + r.log.V(1).Debugf(" store not initialized, returning NXDOMAIN") + return dns.RcodeNameError, nil + } + + myHostname := hostname.Hostname() + + if defDomain := r.defaultDomain(); defDomain != nil { + if qname == defDomain.ReverseDNSZoneIPv4 || qname == defDomain.ReverseDNSZoneIPv6 { + return r.lookupApex(ques, defDomain) + } + } + + for _, domain := range r.store.Domains { + myfqdn := fmt.Sprintf("%s.%s", myHostname, domain.Name) + + if qname == domain.Name || qname == domain.ReverseDNSZoneIPv4 || qname == domain.ReverseDNSZoneIPv6 { + return r.lookupApex(ques, domain) + } else if qname == myfqdn { + return r.lookupRouterAddr(ques, domain) + } + + suffix := "." + domain.Name + if !strings.HasSuffix(qname, suffix) { + continue + } + + key := recordKey(strings.TrimSuffix(qname, suffix)) + + if _, ok := r.store.Records[domain.ID()]; !ok { + continue + } + + records, ok := r.store.Records[domain.ID()][key] + if !ok { + continue + } + + answers := make([]dns.RR, 0) + haveCname := false + for _, record := range records { + if msg, err := record.ToRR(); err == nil && msg != nil { + msg.Header().Name = qname + "." + if msg.Header().Rrtype == dns.TypeCNAME { + haveCname = true + } + if ques.Qtype == dns.TypeANY || ques.Qtype == msg.Header().Rrtype || msg.Header().Rrtype == dns.TypeCNAME { + answers = append(answers, msg) + } + } + } + + if haveCname && len(answers) > 1 { + r.log.Errorf("rname %q: CNAME records may not coexist with other types", qname) + return dns.RcodeServerFailure, nil + } + + return dns.RcodeSuccess, answers + } + return dns.RcodeNameError, nil +} + +func (r *registry) LookupHost(qname string) (*Result, error) { + if !r.store.initialized() { + return nil, fmt.Errorf("plugin not yet initialized") + } + + fqdn := strings.TrimSuffix(qname, ".") + r.log.V(2).Debugf("LookupHost(%s)", fqdn) + + if strings.HasSuffix(fqdn, ".in-addr.arpa") { + return r.lookupReverseIPv4(fqdn) + } else if strings.HasSuffix(fqdn, ".ip6.arpa") { + return r.lookupReverseIPv6(fqdn) + } + + nameParts := strings.Split(fqdn, ".") + basename := strings.ToLower(nameParts[0]) + domainName := strings.ToLower(strings.Join(nameParts[1:], ".")) + + var domain *machines.Domain + for _, d := range r.store.Domains { + if d.Name == domainName { + domain = d + break + } + } + + if domain == nil { + // domain name is not known to us, maybe the query is for a domain at another site + r.log.V(1).Debugf("domain is not known to us: %s", domainName) + return nil, fmt.Errorf("domain is not known to us: %s", domainName) + } + + hostID, ok := r.store.HostNames[basename] + if !ok { + // host not found + r.log.V(1).Debugf("hostname not found: %s", basename) + return nil, nil + } + host, ok := r.store.Hosts[hostID] + if !ok { + // hostname map out of date??? + r.log.V(1).Debugf("host %q, hostID %s found in hostname table but not in host list", basename, hostID) + return nil, fmt.Errorf("hostID %s found in hostname table but not in host list", hostID) + } + + ifaceID := host.LastSeenIface.ID() + iface, ok := r.store.Ifaces[ifaceID] + if ifaceID == "" || !ok { + // host has never been seen on any interface + r.log.V(1).Debugf("host %q: host has never been seen on any interface", basename) + return nil, nil + } + if iface.LastDomain.ID() == "" { + // no valid record of the last domain this iface was seen on + r.log.V(1).Debugf("host %q: no valid LastDomain.ID", basename) + return nil, nil + } + domain, ok = r.store.Domains[iface.LastDomain.ID()] + if !ok { + // last seen domain is not known to us, maybe the host was last seen at a different site + r.log.V(1).Debugf("host %q: last seen domain is not known to us: %s", basename, iface.LastDomain.ID()) + return nil, nil + } + + return &Result{ + domain: domain, + host: host, + iface: iface, + }, nil +} + +func (r *registry) lookupReverseIPv4(ptrName string) (*Result, error) { + parts := strings.Split(ptrName, ".") + if len(parts) != 6 { + r.log.Errorf("got ptr query for invalid name: %q", ptrName) + return nil, fmt.Errorf("ptr query has wrong number of parts (%d != 6)", len(parts)) + } + + parts = parts[0:4] + slices.Reverse(parts) + + ip := net.ParseIP(strings.Join(parts, ".")) + if ip == nil { + r.log.Errorf("failed to parse IP in ptr query: %s", strings.Join(parts, ".")) + return nil, fmt.Errorf("failed to parse IP") + } + + var domain *machines.Domain + for _, d := range r.store.Domains { + if !ip.Mask(d.IPv4PrefixLength.IPMask()).Equal(d.IPv4Address.AsIP()) { + continue + } + + domain = d + break + } + if domain == nil { + return nil, fmt.Errorf("IP is not on any subnet associated with a Machines domain") + } + + routerAddress := r.store.Addrs.IPv4[domain.IPv4Address.String()].Address + if ip.String() == routerAddress.String() { + return &Result{ + domain: domain, + host: &machines.Host{ + Name: hostname.Hostname(), + }, + iface: &machines.Iface{ + LastIPv4: routerAddress, + }, + }, nil + } + + var iface *machines.Iface + var host *machines.Host + for _, i := range r.store.Ifaces { + if i.LastDomain.ID() != domain.ID() { + continue + } + if i.Host.ID() == "" { + continue + } + if i.LastIPv4.AsIP().Equal(ip) { + if h, ok := r.store.Hosts[i.Host.ID()]; ok { + host = h + iface = i + break + } + } + } + + if domain != nil && host != nil && iface != nil { + return &Result{ + domain: domain, + host: host, + iface: iface, + }, nil + } + + return nil, fmt.Errorf("IP not known to us") +} + +func (r *registry) lookupReverseIPv6(ptrName string) (*Result, error) { + parts := strings.Split(ptrName, ".") + if len(parts) != 34 { + return nil, fmt.Errorf("ptr query has wrong number of parts (%d != 34)", len(parts)) + } + + parts = parts[0:32] + slices.Reverse(parts) + addr := strings.Join(strings2.Chunk(strings.Join(parts, ""), 4), ":") + + ip := net.ParseIP(addr) + if ip == nil { + r.log.Errorf("failed to parse IPv6 address: %s", addr) + return nil, fmt.Errorf("failed to parse IP") + } + + r.log.V(1).Debugf("processing PTR lookup for IP %s", ip.String()) + + var domain *machines.Domain + for _, d := range r.store.Domains { + if !ip.Mask(d.IPv6PrefixLength.IPMask()).Equal(d.IPv6Address.AsIP()) { + continue + } + + domain = d + break + } + if domain == nil { + return nil, fmt.Errorf("IP is not on any subnet associated with a Machines domain") + } + + routerAddress := r.store.Addrs.IPv6[domain.IPv6Address.String()].Address + r.log.Debugf("routerAddress = %s", routerAddress) + if ip.Equal(routerAddress.AsIP()) { + return &Result{ + domain: domain, + host: &machines.Host{ + Name: hostname.Hostname(), + }, + iface: &machines.Iface{ + LastIPv6: routerAddress, + }, + }, nil + } + + var iface *machines.Iface + var host *machines.Host + for _, i := range r.store.Ifaces { + if i.LastDomain.ID() != domain.ID() { + continue + } + if i.Host.ID() == "" { + continue + } + if i.LastIPv6.AsIP().Equal(ip) { + if h, ok := r.store.Hosts[i.Host.ID()]; ok { + host = h + iface = i + break + } + } + } + + if domain != nil && host != nil && iface != nil { + return &Result{ + domain: domain, + host: host, + iface: iface, + }, nil + } + + return nil, fmt.Errorf("IP not known to us") +} + +func (r *registry) lookupApex(ques dns.Question, domain *machines.Domain) (int, []dns.RR) { + myHostname := hostname.Hostname() + qname := strings.TrimSuffix(ques.Name, ".") + defDomain := r.defaultDomain() + + r.log.V(2).Debugf("lookupApex(%s)", ques.Name) + + switch ques.Qtype { + case dns.TypeSOA: + return dns.RcodeSuccess, []dns.RR{ + &dns.SOA{ + Hdr: dns.RR_Header{ + Name: qname + ".", + Rrtype: dns.TypeSOA, + Class: dns.ClassINET, + Ttl: 86400, + }, + Ns: fmt.Sprintf("%s.%s.", myHostname, defDomain.Name), + Mbox: fmt.Sprintf("%s-machines-managed.%s.", constants.OrgSlug, domain.Name), + Serial: uint32(domain.LastModified), + Refresh: 300, + Retry: 30, + Expire: 604800, + Minttl: 30, + }, + } + case dns.TypeNS: + return dns.RcodeSuccess, []dns.RR{ + &dns.NS{ + Hdr: dns.RR_Header{ + Name: qname + ".", + Rrtype: dns.TypeNS, + Class: dns.ClassINET, + Ttl: 86400, + }, + Ns: fmt.Sprintf("%s.%s.", myHostname, defDomain.Name), + }, + } + } + + return dns.RcodeSuccess, nil +} + +func (r *registry) lookupRouterAddr(ques dns.Question, domain *machines.Domain) (int, []dns.RR) { + r.log.Debugf("lookupRouterAddr(%s)", ques.Name) + + myHostname := hostname.Hostname() + myfqdn := fmt.Sprintf("%s.%s", myHostname, domain.Name) + + answers := make([]dns.RR, 0) + + switch ques.Qtype { + case dns.TypeA: + mynetaddr := domain.IPv4Address.AsIP().Mask(domain.IPv4PrefixLength.IPMask()).String() + if routeraddr, ok := r.store.Addrs.IPv4[mynetaddr]; ok { + rr := &dns.A{ + Hdr: dns.RR_Header{ + Name: myfqdn + ".", + Rrtype: dns.TypeA, + Class: dns.ClassINET, + Ttl: 300, + }, + A: routeraddr.Address.AsIP(), + } + answers = append(answers, rr) + } + case dns.TypeAAAA: + mynetaddr := domain.IPv6Address.AsIP().Mask(domain.IPv6PrefixLength.IPMask()).String() + if routeraddr, ok := r.store.Addrs.IPv6[mynetaddr]; ok { + rr := &dns.AAAA{ + Hdr: dns.RR_Header{ + Name: myfqdn + ".", + Rrtype: dns.TypeAAAA, + Class: dns.ClassINET, + Ttl: 300, + }, + AAAA: routeraddr.Address.AsIP(), + } + answers = append(answers, rr) + } + } + + return dns.RcodeSuccess, answers +} + +func (r *registry) processHostSeen(hostID, ifaceID, newIP string) { + ip := machines.IPString(newIP) + if !ip.Defined() { + r.log.Warningf("failed to parse newIP from host seen event: %q", newIP) + return + } + + // host and iface are only fetched if they are not previously known to us + if _, ok := r.store.Hosts[hostID]; !ok { + host := &machines.Host{} + err := r.client.APICall("host/"+hostID, nil, host) + if err != nil { + r.log.Warningf("error getting hostID %q from machines API: %v", hostID, err) + return + } + + r.store.patchHost(host) + } + + if _, ok := r.store.Ifaces[ifaceID]; !ok { + iface := &machines.Iface{} + err := r.client.APICall("interface/"+ifaceID, nil, iface) + if err != nil { + r.log.Warningf("error getting ifaceID %q from machines API: %v", ifaceID, err) + return + } + + r.store.patchIface(iface) + } + + iface := r.store.Ifaces[ifaceID] + + now := time.Now().Unix() + iface.LastSeen = machines.Timestamp(now) + if ip.IsIPv4() { + iface.LastIPv4 = ip + + } else { + iface.LastIPv6 = ip + } + + r.store.patchIface(iface) +} + +func (r *registry) tryInit() error { + client, err := machines.NewDefaultMachinesClient() + if err != nil { + return err + } + client.SetupStats(r.mb) + + eventsCh, err := client.NewEventListener(r.ctx) + if err != nil { + return err + } + + if !r.store.initialized() { + err = r.store.fetch(client) + if err != nil { + return err + } + r.didLiveFetch = true + } + + r.client = client + r.eventsCh = eventsCh + + return nil +} + +func (r *Result) Answer(ques dns.Question) (int, []dns.RR) { + answers := make([]dns.RR, 0) + qname := strings.TrimSuffix(ques.Name, ".") + + nameParts := strings.Split(qname, ".") + basename := strings.ToLower(nameParts[0]) + domainName := strings.ToLower(strings.Join(nameParts[1:], ".")) + + if strings.HasSuffix(qname, ".in-addr.arpa") || strings.HasSuffix(qname, ".ip6.arpa") { + if (r.iface.LastIPv4.Defined() || r.iface.LastIPv6.Defined()) && (ques.Qtype == dns.TypePTR || ques.Qtype == dns.TypeANY) { + msg := &dns.PTR{ + Hdr: dns.RR_Header{ + Name: qname + ".", + Rrtype: dns.TypePTR, + Class: dns.ClassINET, + Ttl: 60, + }, + Ptr: fmt.Sprintf("%s.%s.", r.host.Name, r.domain.Name), + } + answers = append(answers, msg) + } + } else if domainName != r.domain.Name { + msg := &dns.CNAME{ + Hdr: dns.RR_Header{ + Name: qname + ".", + Rrtype: dns.TypeCNAME, + Class: dns.ClassINET, + Ttl: 60, + }, + Target: fmt.Sprintf("%s.%s.", basename, r.domain.Name), + } + answers = append(answers, msg) + } else if strings.HasPrefix(qname, r.host.Name+".") { + if r.iface.LastIPv4.Defined() && (ques.Qtype == dns.TypeA || ques.Qtype == dns.TypeANY) { + msg := &dns.A{ + Hdr: dns.RR_Header{ + Name: qname + ".", + Rrtype: dns.TypeA, + Class: dns.ClassINET, + Ttl: 60, + }, + A: r.iface.LastIPv4.AsIP(), + } + answers = append(answers, msg) + } + + if r.iface.LastIPv6.Defined() && (ques.Qtype == dns.TypeAAAA || ques.Qtype == dns.TypeANY) { + msg := &dns.AAAA{ + Hdr: dns.RR_Header{ + Name: qname + ".", + Rrtype: dns.TypeAAAA, + Class: dns.ClassINET, + Ttl: 60, + }, + AAAA: r.iface.LastIPv6.AsIP(), + } + answers = append(answers, msg) + } + + if ques.Qtype == dns.TypeTXT || ques.Qtype == dns.TypeANY { + txtStr := fmt.Sprintf( + "%s-machines:i=%s,t=%d,h=%s,m=%s;", + constants.OrgSlug, + r.iface.ID(), + r.iface.LastSeen, + r.iface.HardwareAddress, + r.iface.MediaType, + ) + + msg := &dns.TXT{ + Hdr: dns.RR_Header{ + Name: qname + ".", + Rrtype: dns.TypeTXT, + Class: dns.ClassINET, + Ttl: 60, + }, + Txt: []string{ + txtStr, + }, + } + answers = append(answers, msg) + } + } + + return dns.RcodeSuccess, answers +} + +func (r *registry) monitorEvents() { + defer (func() { r.stopCh <- struct{}{} })() + + if r.client == nil { + initTicker := time.NewTicker(1 * time.Second) + + initLoop: + for { + select { + case <-initTicker.C: + err := r.tryInit() + if err == nil { + break initLoop + } + case <-r.ctx.Done(): + initTicker.Stop() + return + } + } + + initTicker.Stop() + } + + if !r.didLiveFetch { + r.log.Noticef("No live fetch was done yet - attempting one now") + if err := r.store.fetch(r.client); err != nil { + r.log.Warningf("live fetch failed, continuing with (potentially stale!) on-disk data: %v", err) + } else { + r.didLiveFetch = true + } + } + + r.log.Noticef("Starting event loop") + + refreshTicker := time.NewTicker(refreshInterval) + defer refreshTicker.Stop() + + for { + select { + case event := <-r.eventsCh: + r.stats.eventsReceived.WithLabelValues(mbclient.KV{"thing": event.ItemType, "action": event.Event}).Add(1) + if event.ItemType == "host" && event.Event == "seen" { + if via, ok := event.Tags["via"]; !ok || via != "dhcp" { + continue + } + ip, ok := event.Tags["ip"] + if !ok || ip == "" { + continue + } + + hostID, ok := event.Tags["host"] + if !ok || hostID == "" { + continue + } + + ifaceID, ok := event.Tags["iface"] + if !ok || ifaceID == "" { + continue + } + + r.processHostSeen(hostID, ifaceID, ip) + } else if event.ItemType == "dns_record" { + r.store.fetchRecords(r.client) + } else if event.ItemType == "interface" || event.ItemType == "host" || event.ItemType == "domain" { + r.store.fetch(r.client) + staggeredInterval := refreshInterval - refreshSplay + staggeredInterval += time.Duration(rand.Int63n(int64(2 * refreshSplay))).Truncate(100 * time.Millisecond) + + r.log.V(2).Infof("resetting refresh interval to %s", staggeredInterval) + refreshTicker.Reset(staggeredInterval) + } + case <-refreshTicker.C: + r.log.Infof("local state has not been refreshed for %s, refreshing now", refreshInterval) + r.store.fetch(r.client) + case <-r.ctx.Done(): + err := r.store.saveState() + if err == nil { + r.log.Infof("saved current state to %s", machinesRegistryStorePath) + } else { + r.log.Warningf("failed to save state to %s: %v", machinesRegistryStorePath, err) + } + return + } + } +} diff --git a/machines/coredns_plugin/registry_store.go b/machines/coredns_plugin/registry_store.go new file mode 100644 index 0000000..de1f9e3 --- /dev/null +++ b/machines/coredns_plugin/registry_store.go @@ -0,0 +1,251 @@ +package coredns_plugin + +import ( + "encoding/json" + "flag" + "fmt" + "os" + "sync" + + "go.fuhry.dev/runtime/constants" + "go.fuhry.dev/runtime/machines" + "go.fuhry.dev/runtime/utils/log" +) + +type registryStore struct { + mu sync.Mutex `json:"-"` + log *log.Logger + + Sites map[string]*machines.Site `json:"Sites"` + Domains map[string]*machines.Domain `json:"Domains"` + Hosts map[string]*machines.Host `json:"Hosts"` + Ifaces map[string]*machines.Iface `json:"Ifaces"` + Records map[string]map[recordKey][]*machines.DNSRecord `json:"Records"` + HostNames map[string]string `json:"-"` + Addrs *machines.RouterAddresses `json:"-"` +} + +type recordKey string + +var machinesRegistryStorePath string + +func init() { + machinesRegistryStorePath = fmt.Sprintf("/var/cache/%s-machines/cache.json", constants.OrgSlug) + + flag.StringVar(&machinesRegistryStorePath, + "machines.dns.cache", + machinesRegistryStorePath, + "path to json cache file where state will be saved and loaded") +} + +func (rs *registryStore) initialized() bool { + rs.mu.Lock() + defer rs.mu.Unlock() + + return rs.Sites != nil && rs.Domains != nil && rs.Hosts != nil && rs.Ifaces != nil && rs.Records != nil +} + +func (rs *registryStore) saveState() error { + if !rs.initialized() { + return nil + } + + // wait to acquire lock until after initialization check to avoid deadlock + rs.mu.Lock() + defer rs.mu.Unlock() + + contents, err := json.Marshal(rs) + if err != nil { + rs.log.Error("failed to save state to %s: %v", machinesRegistryStorePath, err) + return err + } + + err = os.WriteFile(machinesRegistryStorePath, contents, os.FileMode(0600)) + if err != nil { + return err + } + return nil +} + +func (rs *registryStore) loadState() error { + rs.mu.Lock() + defer rs.mu.Unlock() + + contents, err := os.ReadFile(machinesRegistryStorePath) + if err != nil { + return err + } + + err = json.Unmarshal(contents, rs) + if err != nil { + rs.Sites, rs.Domains, rs.Hosts, rs.HostNames, rs.Ifaces, rs.Records = nil, nil, nil, nil, nil, nil + rs.log.Error("error unmarshaling state from file %s: %v", machinesRegistryStorePath, err) + return err + } + + domains := make([]*machines.Domain, 0, len(rs.Domains)) + for _, d := range rs.Domains { + domains = append(domains, d) + } + + rs.Addrs, err = machines.GetRouterAddresses(domains) + if err != nil { + rs.log.Error("error getting router addresses: %v", machinesRegistryStorePath, err) + return err + } + + rs.HostNames = make(map[string]string) + for _, host := range rs.Hosts { + rs.HostNames[host.Name] = host.ID() + } + + return nil +} + +func (rs *registryStore) fetch(client machines.MachinesClient) error { + rs.mu.Lock() + defer rs.mu.Unlock() + + sites := []*machines.Site{} + err := client.APICall("sites", nil, &sites) + if err != nil { + rs.log.Errorf("error fetching sites from api: %v", err) + return err + } + + domains := []*machines.Domain{} + err = client.APICall("domains", nil, &domains) + if err != nil { + rs.log.Errorf("error fetching domains from api: %v", err) + return err + } + + hosts := []*machines.Host{} + err = client.APICall("hosts?expand=0", nil, &hosts) + if err != nil { + rs.log.Errorf("error fetching hosts from api: %v", err) + return err + } + + ifaces := []*machines.Iface{} + err = client.APICall("interfaces?expand=0", nil, &ifaces) + if err != nil { + rs.log.Errorf("error fetching interfaces from api: %v", err) + return err + } + + records := []*machines.DNSRecord{} + err = client.APICall("records", nil, &records) + if err != nil { + rs.log.Errorf("error fetching DNS records from api: %v", err) + return err + } + + ras, err := machines.GetRouterAddresses(domains) + if err != nil { + rs.log.Errorf("error refreshing local interface addresses: %v", err) + return err + } + + // update state only after all information is collected + rs.Sites = make(map[string]*machines.Site) + for _, s := range sites { + rs.Sites[s.ID()] = s + } + + rs.Domains = make(map[string]*machines.Domain) + for _, d := range domains { + // if rs.Sites[d.Site.ID()].Name != machines.SiteName() { + // continue + // } + rs.Domains[d.ID()] = d + } + + rs.Addrs = ras + + rs.HostNames = make(map[string]string) + rs.Hosts = make(map[string]*machines.Host) + for _, h := range hosts { + rs.Hosts[h.ID()] = h + rs.HostNames[h.Name] = h.ID() + } + + rs.Ifaces = make(map[string]*machines.Iface) + for _, i := range ifaces { + rs.Ifaces[i.ID()] = i + } + // loop through again and populate LastSeenIface + for _, i := range ifaces { + if host, ok := rs.Hosts[i.Host.ID()]; ok && i.Host.ID() != "" { + update := false + + if host.LastSeenIface.ID() == "" { + update = true + } else if lsi, ok := rs.Ifaces[host.LastSeenIface.ID()]; ok && host.LastSeenIface.ID() != "" { + if i.LastSeen > lsi.LastSeen { + update = true + } + } + + if update { + host.LastSeenIface.Set(i) + host.LastSeen = i.LastSeen + } + } + } + + rs.processRecords(records) + + return nil +} + +func (rs *registryStore) fetchRecords(client machines.MachinesClient) error { + rs.mu.Lock() + defer rs.mu.Unlock() + + records := []*machines.DNSRecord{} + err := client.APICall("records", nil, &records) + if err != nil { + return err + } + + rs.processRecords(records) + + return nil +} + +func (rs *registryStore) processRecords(records []*machines.DNSRecord) { + rs.Records = make(map[string]map[recordKey][]*machines.DNSRecord) + + for _, rec := range records { + if _, ok := rs.Records[rec.Domain.ID()]; !ok { + rs.Records[rec.Domain.ID()] = make(map[recordKey][]*machines.DNSRecord) + } + + rkey := recordKey(rec.RName) + + if _, ok := rs.Records[rec.Domain.ID()][rkey]; !ok { + rs.Records[rec.Domain.ID()][rkey] = []*machines.DNSRecord{} + } + + rs.Records[rec.Domain.ID()][rkey] = append(rs.Records[rec.Domain.ID()][rkey], rec) + } +} + +func (rs *registryStore) patchHost(host *machines.Host) { + rs.mu.Lock() + defer rs.mu.Unlock() + + if oldHost, ok := rs.Hosts[host.ID()]; ok { + delete(rs.HostNames, oldHost.Name) + } + rs.Hosts[host.ID()] = host + rs.HostNames[host.Name] = host.ID() +} + +func (rs *registryStore) patchIface(iface *machines.Iface) { + rs.mu.Lock() + defer rs.mu.Unlock() + + rs.Ifaces[iface.ID()] = iface +} diff --git a/machines/coredns_plugin/setup.go b/machines/coredns_plugin/setup.go new file mode 100644 index 0000000..54b7775 --- /dev/null +++ b/machines/coredns_plugin/setup.go @@ -0,0 +1,125 @@ +package coredns_plugin + +import ( + "context" + "fmt" + + "github.com/coredns/caddy" + "github.com/coredns/coredns/core/dnsserver" + "github.com/coredns/coredns/plugin" + "github.com/miekg/dns" + + "go.fuhry.dev/runtime/constants" + "go.fuhry.dev/runtime/metrics/metricbus/mbclient" + "go.fuhry.dev/runtime/utils/log" +) + +func init() { plugin.Register(constants.OrgSlug+"_machines", setup) } + +func setup(c *caddy.Controller) error { + reg := NewRegistry(context.Background()) + + m := &Machines{ + log: *log.WithPrefix(fmt.Sprintf("plugin/%s_machines", constants.OrgSlug)), + r: reg, + } + + c.OnStartup(reg.Startup) + c.OnShutdown(reg.Shutdown) + + dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler { + m.Next = next + + return m + }) + + return nil +} + +type Machines struct { + Next plugin.Handler + + log log.Logger + r Registry +} + +func (m *Machines) Name() string { + return constants.OrgSlug + "_machines" +} + +func (m *Machines) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + var handled bool + rcode := dns.RcodeSuccess + + answer := &dns.Msg{ + MsgHdr: dns.MsgHdr{ + Id: r.Id, + Authoritative: true, + Response: true, + }, + Answer: make([]dns.RR, 0), + } + + for _, ques := range r.Question { + if rc, rrs := m.r.LookupRecord(ques); rc != dns.RcodeNameError { + domain := m.r.(*registry).domainFromQuestion(ques) + m.r.(*registry).stats.recordLookups.WithLabelValues(mbclient.KV{ + "domain": domain.Name, + "rcode": rcodeToStr(rc), + }).Add(1) + handled = true + answer.Question = append(answer.Question, ques) + m.log.V(2).Debugf(" -> rcode %d, rrs %+v", rcode, rrs) + if rc != dns.RcodeSuccess { + m.log.Errorf("error in LookupRecord(%s): rcode=%d", ques.Name, rc) + rcode = rc + } else { + answer.Answer = append(answer.Answer, rrs...) + answer.Answer = append(answer.Answer, m.r.AdditionalRecords(ques, rrs)...) + answer.Ns = m.r.AuthorityRecords(ques) + } + } else if result, err := m.r.LookupHost(ques.Name); err == nil { + handled = true + answer.Question = append(answer.Question, ques) + if result == nil { + m.log.V(2).Debugf(" -> not found") + rcode = dns.RcodeNameError + + m.r.(*registry).stats.hostLookups.WithLabelValues(mbclient.KV{ + "domain": m.r.(*registry).domainFromQuestion(ques).Name, + "rcode": rcodeToStr(rcode), + }).Add(1) + } else { + m.log.V(2).Debugf(" -> domain %s, host %s, iface %s", result.domain.ID(), result.host.ID(), result.iface.ID()) + + r, rrs := result.Answer(ques) + m.r.(*registry).stats.hostLookups.WithLabelValues(mbclient.KV{ + "domain": result.domain.Name, + "rcode": rcodeToStr(r), + }).Add(1) + if r != dns.RcodeSuccess { + rcode = r + } else { + answer.Answer = append(answer.Answer, rrs...) + answer.Answer = append(answer.Answer, m.r.AdditionalRecords(ques, rrs)...) + answer.Ns = m.r.AuthorityRecords(ques) + } + } + } else { + m.log.V(3).Debugf(" -> not handled: %s", ques.Name) + } + } + + if handled { + m.log.V(3).Debugf(" -> WRITE %+v", answer) + answer.MsgHdr.Rcode = rcode + err := w.WriteMsg(answer) + if err != nil { + m.log.Errorf("while writing answer: %+v", err) + return dns.RcodeServerFailure, err + } + return rcode, nil + } + m.log.V(3).Debugf("NEXT!") + return plugin.NextOrFailure(m.Name(), m.Next, ctx, w, r) +} diff --git a/machines/coredns_plugin/stats.go b/machines/coredns_plugin/stats.go new file mode 100644 index 0000000..8751ef8 --- /dev/null +++ b/machines/coredns_plugin/stats.go @@ -0,0 +1,52 @@ +package coredns_plugin + +import ( + "fmt" + + "github.com/miekg/dns" + + "go.fuhry.dev/runtime/constants" + "go.fuhry.dev/runtime/metrics/metricbus/mbclient" +) + +type dnsStats struct { + recordLookups mbclient.CounterMetric + hostLookups mbclient.CounterMetric + eventsReceived mbclient.CounterMetric +} + +func makeStats(client *mbclient.MetricBusService) *dnsStats { + recordLookups := client.DefineCounter( + constants.OrgSlug+"_machines_dns_record_lookups", + "DNS lookups that resolved to custom DNS records", + "domain", "rcode") + + hostLookups := client.DefineCounter( + constants.OrgSlug+"_machines_dns_host_lookups", + "DNS lookups that resolved to Machines hosts", + "domain", "rcode") + + eventsReceived := client.DefineCounter( + constants.OrgSlug+"_machines_dns_mqtt_events", + "events received over mqtt", + "thing", "action") + + return &dnsStats{ + recordLookups: recordLookups, + hostLookups: hostLookups, + eventsReceived: eventsReceived, + } +} + +func rcodeToStr(rcode int) string { + switch rcode { + case dns.RcodeSuccess: + return "success" + case dns.RcodeNameError: + return "nxdomain" + case dns.RcodeServerFailure: + return "servfail" + default: + return fmt.Sprintf("unknown(%d)", rcode) + } +} diff --git a/machines/event_watcher.go b/machines/event_watcher.go index 8a5db9e..679a919 100644 --- a/machines/event_watcher.go +++ b/machines/event_watcher.go @@ -6,9 +6,12 @@ import ( "errors" "fmt" "net/url" + "time" mqtt "github.com/eclipse/paho.mqtt.golang" + "go.fuhry.dev/runtime/constants" + "go.fuhry.dev/runtime/metrics/metricbus/mbclient" "go.fuhry.dev/runtime/utils/log" ) @@ -54,10 +57,14 @@ func (mc *machinesClient) NewEventListener(ctx context.Context) (chan MachinesMq } mqttOpts.Servers = append(mqttOpts.Servers, eventsUrl) - - mqttOpts.OnReconnecting = func(client mqtt.Client, opts *mqtt.ClientOptions) { + mqttOpts.SetReconnectingHandler(func(client mqtt.Client, opts *mqtt.ClientOptions) { updateCreds(opts) - } + }) + mqttOpts.SetPingTimeout(10 * time.Second) + mqttOpts.SetConnectTimeout(10 * time.Second) + mqttOpts.SetAutoReconnect(true) + mqttOpts.SetConnectRetry(false) + mqttOpts.SetMaxReconnectInterval(30 * time.Second) client := mqtt.NewClient(mqttOpts) t := client.Connect() @@ -84,7 +91,21 @@ func (mc *machinesClient) NewEventListener(ctx context.Context) (chan MachinesMq obj := MachinesMqttEvent{} if err := json.Unmarshal(msg.Payload(), &obj); err == nil { + if mc.stats != nil { + mc.stats.mqttMessages.WithLabelValues(mbclient.KV{ + "thing": obj.ItemType, + "action": obj.Event, + "parse_result": "success", + }).Add(1) + } + msgChan <- obj + } else { + if mc.stats != nil { + mc.stats.mqttMessages.WithLabelValues(mbclient.KV{ + "parse_result": "failure", + }).Add(1) + } } } diff --git a/machines/machines_agent/Makefile b/machines/machines_agent/Makefile new file mode 100644 index 0000000..bfab546 --- /dev/null +++ b/machines/machines_agent/Makefile @@ -0,0 +1,14 @@ +GOSRC = $(wildcard *.go) +GOEXE = $(shell basename `pwd`) +GOBUILDFLAGS := -buildmode=pie -trimpath + +all: $(GOEXE) + +clean: + rm -fv $(GOEXE) + +.PHONY: all clean + +$(GOEXE): %: $(GOSRC) + go build $(GOBUILDFLAGS) -o $@ $< + diff --git a/machines/machines_agent/main.go b/machines/machines_agent/main.go new file mode 100644 index 0000000..dba82d8 --- /dev/null +++ b/machines/machines_agent/main.go @@ -0,0 +1,184 @@ +package main + +import ( + "archive/tar" + "bytes" + "context" + "flag" + "fmt" + "io" + "os" + "os/signal" + "strings" + "syscall" + "time" + + "go.fuhry.dev/runtime/machines" + "go.fuhry.dev/runtime/metrics/metricbus/mbclient" + "go.fuhry.dev/runtime/utils/log" +) + +func main() { + templateToRender := flag.String("render", "", "render this template to stdout and exit") + commitIPAddress := flag.String("ip-address", "", "passed by dhcpd when a host is seen") + commitMACAddress := flag.String("mac-address", "", "passed by dhcpd when a host is seen") + renderAll := flag.Bool("render-all", false, "render all files") + + flag.Parse() + logger := log.WithPrefix("main") + + client, err := machines.NewDefaultMachinesClient() + if err != nil { + logger.Panic(err) + } + + if *commitIPAddress != "" && *commitMACAddress != "" { + url := fmt.Sprintf("host/seen/%s/%s", *commitMACAddress, *commitIPAddress) + host := &machines.Host{} + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + go (func() { + err := client.APICall(url, nil, host) + if err != nil { + logger.Fatal(err) + } + cancel() + })() + <-ctx.Done() + if ctx.Err() == context.DeadlineExceeded { + logger.Error("timed out calling Machines API for host seen notification") + os.Exit(1) + } + return + } + + renderer := machines.NewRenderer(client) + if *renderAll || (templateToRender != nil && *templateToRender != "") { + if *renderAll { + files, err := renderer.RenderAll() + if err != nil { + logger.Fatal(err) + } + + tarBuf := new(bytes.Buffer) + tw := tar.NewWriter(tarBuf) + + now := time.Now() + + for path, contents := range files { + hdr := &tar.Header{ + Name: path[1:], + Size: int64(len(contents)), + Mode: 0644, + Uid: 0, Gid: 0, + Uname: "root", Gname: "root", + ModTime: now, AccessTime: now, ChangeTime: now, + } + tarBuf.Grow(len(contents) + 512) + err := tw.WriteHeader(hdr) + if err != nil { + logger.Fatal(err) + } + + _, err = io.Copy(tw, bytes.NewBuffer(contents)) + if err != nil { + logger.Fatal(err) + } + } + + err = tw.Close() + if err != nil { + logger.Fatal(err) + } + + io.Copy(os.Stdout, tarBuf) + + return + } + args := strings.Split(*templateToRender, "/") + out, err := renderer.Render(args...) + if err != nil { + logger.Fatal(err) + } + + fmt.Fprint(os.Stdout, string(out)) + return + } + + ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) + defer cancel() + + metrics := mbclient.NewService(ctx) + + eventsProcessed := metrics.DefineCounter("machines_agent_events", + "Events received by the Machines agent", + "thing", "action") + + serviceRestarts := metrics.DefineCounter("machines_agent_service_restarts", + "Services restarted by the Machines agent", + "service", "status") + + defer metrics.FlushAndWait() + + var eventsChan <-chan machines.MachinesMqttEvent + for { + ectx, cancel := context.WithCancel(ctx) + eventsChan, err = client.NewEventListener(ectx) + + if err == nil { + defer cancel() + break + } + + cancel() + + logger.Warningf("creating mqtt connection failed: %v", err) + logger.Warningf("retrying in 10sec") + retry, cancel := context.WithTimeout(ctx, 10*time.Second) + <-retry.Done() + if retry.Err() != context.DeadlineExceeded { + // caught signal + return + } + cancel() + } + + logger.Info("successfully created event monitor, listening for events") +mainLoop: + for { + select { + case evt := <-eventsChan: + eventsProcessed.WithLabelValues(mbclient.KV{"thing": evt.ItemType, "action": evt.Event}).Add(1) + logger.Noticef("got event with thing=%s, action=%s, tags=<%+v>\n", evt.ItemType, evt.Event, evt.Tags) + + outs, svcs, err := renderer.RenderEvent(&evt) + if err != nil { + logger.Warningf("failed to render files following the event: %v", err) + continue + } + + for path, contents := range outs { + err := os.WriteFile(path, contents, os.FileMode(0644)) + if err != nil { + logger.Warningf("failed to write file %q following a change event: %v", path, err) + continue + } + + logger.Infof("wrote %s", path) + } + + for _, svcName := range svcs { + svc := machines.GetService(svcName) + if err := svc.ReloadOrRestart(false); err != nil { + logger.Warningf("failed to restart service %+v: %v", svcName, err) + serviceRestarts.WithLabelValues(mbclient.KV{"service": svcName, "status": "failed"}).Add(1) + } else { + logger.Noticef("restarted service %+v", svcName) + serviceRestarts.WithLabelValues(mbclient.KV{"service": svcName, "status": "success"}).Add(1) + } + } + case <-ctx.Done(): + break mainLoop + } + } +} diff --git a/machines/oauth2.go b/machines/oauth2.go index bdd740b..5843142 100644 --- a/machines/oauth2.go +++ b/machines/oauth2.go @@ -47,16 +47,16 @@ type oauthAuthParams struct { type oauthClient struct { *http.Transport - client *http.Client + inner http.RoundTripper store *oauthClientCredentials_Store params *oauthAuthParams log *log.Logger } func SetupOAuthClient(client *http.Client, tokenEndpoint string, clientId string, clientSecret string, scope []string) error { - clientCopy := *client + origTransport := client.Transport client.Transport = &oauthClient{ - client: &clientCopy, + inner: origTransport, params: &oauthAuthParams{ tokenEndpoint: tokenEndpoint, clientId: clientId, @@ -78,7 +78,7 @@ func (oc *oauthClient) RoundTrip(request *http.Request) (*http.Response, error) request.Header.Set("authorization", fmt.Sprintf("Bearer %s", accessToken)) - return oc.client.Do(request) + return oc.inner.RoundTrip(request) } func (oc *oauthClient) credentialCachePath() string { @@ -140,7 +140,7 @@ func (oc *oauthClient) renewAccessToken() (string, error) { httpReq.Header.Set("content-type", "application/json") - response, err := oc.client.Do(httpReq) + response, err := oc.inner.RoundTrip(httpReq) if err != nil { return "", err } @@ -192,8 +192,8 @@ func (oc *oauthClient) readCredentialCacheFromDisk() error { reqScope := oc.params.scope.Dup() reqScope.Diff(tokenScope) if reqScope.Len() > 0 { - return fmt.Errorf("cannot use stored token: missing scopes: \"%s\"", - strings.Join(reqScope.AsSlice(), "\", \"")) + return fmt.Errorf("cannot use stored token: missing scopes: \"%s\" (stored scopes: \"%s\", diff: %+v)", + strings.Join(reqScope.AsSlice(), "\", \""), store.Token.Scope, reqScope) } oc.store = &store diff --git a/machines/render.go b/machines/render.go new file mode 100644 index 0000000..4fafca6 --- /dev/null +++ b/machines/render.go @@ -0,0 +1,574 @@ +package machines + +import ( + "embed" + "encoding/json" + "errors" + "fmt" + "io" + "net/url" + "strings" + "sync" + + "github.com/nikolalohinski/gonja" + gonja_config "github.com/nikolalohinski/gonja/config" + + "go.fuhry.dev/runtime/constants" + "go.fuhry.dev/runtime/utils/hashset" + "go.fuhry.dev/runtime/utils/hostname" +) + +// via /usr/include/linux/if_addr.h +const ( + IFA_F_PERMANENT = 0x80 + RTNL_SCOPE_LINK = 0xfd +) + +type varMap = map[string][]string + +type artifact struct { + filePath string + serviceName string +} + +var templateVarMap = varMap{ + "corefile.conf": {"zones"}, + "named.conf": {"zones"}, + "dhcpd4.conf": {"sites", "domains", "routeraddresses"}, + "dhcpd6.conf": {"sites", "domains", "routeraddresses"}, + "forward.zone": {"router_name", "domain", "routeraddresses", "site"}, + "reverse.zone": {"router_name", "router_domain", "reverse"}, + "rad.conf": {"domains", "site", "routeraddresses"}, + "radvd.conf": {"domains", "site", "routeraddresses"}, +} + +var baseVars = []string{ + "orgSlug", + "siteName", +} + +var outputFileMap = map[string]*artifact{ + "corefile.conf": { + filePath: "/etc/coredns/machines-zones.conf", + serviceName: "coredns", + }, + "dhcpd4.conf": { + filePath: "/etc/dhcpd4.conf", + serviceName: "dhcpd4", + }, + "dhcpd6.conf": { + filePath: "/etc/dhcpd6.conf", + serviceName: "dhcpd6", + }, + "forward.zone": { + filePath: "/var/named/etc/named/zones/%s.db", + serviceName: "zones", + }, + "reverse.zone": { + filePath: "/var/named/etc/named/zones/%s.db", + serviceName: "zones", + }, + "rad.conf": { + filePath: "/etc/rad.conf", + serviceName: "rad", + }, + "radvd.conf": { + filePath: "/etc/radvd.conf", + serviceName: "radvd", + }, +} + +type calculator struct { + mu sync.Mutex + client MachinesClient + cache map[string]any +} + +type Renderer struct { + calc *calculator + env *gonja.Environment +} + +type templateLoader struct{} + +var ( + //go:embed templates + templates embed.FS +) + +func NewRenderer(client MachinesClient) *Renderer { + gonjaConf := &gonja_config.Config{} + + calc := &calculator{ + client: client, + } + + renderer := &Renderer{ + calc: calc, + env: gonja.NewEnvironment(gonjaConf, &templateLoader{}), + } + + return renderer +} + +func (r *Renderer) Render(args ...string) ([]byte, error) { + r.calc.begin() + defer r.calc.end() + + return r.render(args...) +} + +func (r *Renderer) RenderMulti(templates []string) (map[string][]byte, error) { + r.calc.begin() + defer r.calc.end() + + return r.renderMulti(templates) +} + +func (r *Renderer) RenderAll() (map[string][]byte, error) { + r.calc.begin() + defer r.calc.end() + + out := make(map[string][]byte) + + for key, artf := range outputFileMap { + if strings.Contains(artf.filePath, "%s") { + continue + } + + tmpl, err := r.render(key) + if err != nil { + return nil, fmt.Errorf("while rendering %s: %v", key, err) + } + + out[artf.filePath] = tmpl + } + + zones, err := r.renderZones() + if err != nil { + return nil, err + } + for templateKey, contents := range zones { + args := strings.Split(templateKey, "/") + filePath := fmt.Sprintf(outputFileMap[args[0]].filePath, args[1]) + + out[filePath] = contents + } + + return out, nil +} + +func (r *Renderer) RenderZones() (map[string][]byte, error) { + r.calc.begin() + defer r.calc.end() + + return r.renderZones() +} + +func (r *Renderer) RenderEvent(ev *MachinesMqttEvent) (map[string][]byte, []string, error) { + files := hashset.NewHashSet[string]() + svcs := hashset.NewHashSet[string]() + + for _, hook := range actionHooks { + if hook.Matches(r.calc.client, ev) { + keys := hook.fileGenerator(r.calc.client, ev) + logger.V(2).Debugf("hook %+v matched event: %+v", hook, ev) + logger.V(2).Debugf("regenerating templates: %v", keys) + files.Add(keys...) + } + } + + outs, err := r.RenderMulti(files.AsSlice()) + if err != nil { + return nil, nil, err + } + + out := make(map[string][]byte) + for templateKey, contents := range outs { + args := strings.Split(templateKey, "/") + filePath := outputFileMap[args[0]].filePath + svcs.Add(outputFileMap[args[0]].serviceName) + if len(args) > 1 { + filePath = fmt.Sprintf(outputFileMap[args[0]].filePath, args[1]) + } + out[filePath] = contents + } + + return out, svcs.AsSlice(), nil +} + +func (r *Renderer) renderMulti(templates []string) (map[string][]byte, error) { + results := make(map[string][]byte) + + for _, argGroup := range templates { + args := strings.Split(argGroup, "/") + out, err := r.render(args...) + if err != nil { + return nil, fmt.Errorf("while rendering %s: %v", argGroup, err) + } + + results[argGroup] = out + } + + return results, nil +} + +func (r *Renderer) renderZones() (map[string][]byte, error) { + templates := make([]string, 0) + + zones, err := r.calc.get("zones") + if err != nil { + return nil, err + } + + for _, zone := range zones.([]string) { + if strings.HasSuffix(zone, ".arpa") { + templates = append(templates, fmt.Sprintf("reverse.zone/%s", zone)) + } else { + templates = append(templates, fmt.Sprintf("forward.zone/%s", zone)) + } + } + + return r.renderMulti(templates) +} + +func (r *Renderer) render(args ...string) ([]byte, error) { + tmpl := args[0] + args = args[1:] + + ctx := make(map[string]interface{}) + + reqVars, _ := templateVarMap[tmpl] + + reqVars = append(reqVars, baseVars...) + for _, varName := range reqVars { + varExtra := "" + if len(args) > 0 { + varExtra = "/" + strings.Join(args, "/") + } + value, err := r.calc.get(varName + varExtra) + if err != nil { + return nil, fmt.Errorf("while getting dynamic variable %q: %v", varName, err) + } + + ctx[varName] = value + } + + template, err := r.env.GetTemplate(tmpl) + if err != nil { + return nil, err + } + + j, err := json.MarshalIndent(ctx, "", " ") + if err != nil { + return nil, err + } + logger.AppendPrefix(".render").V(3).Debugf("template context for %q:\n%s", tmpl, string(j)) + + return template.ExecuteBytes(ctx) +} + +func (l *templateLoader) Get(pathName string) (io.Reader, error) { + if pathName == "" { + return nil, errors.New("template name may not be empty") + } + pathName = fmt.Sprintf("templates/%s.j2", pathName) + return templates.Open(pathName) +} + +func (l *templateLoader) Path(pathName string) (string, error) { + return pathName, nil +} + +func (c *calculator) begin() { + c.mu.Lock() + + c.cache = make(map[string]any, 0) +} + +func (c *calculator) get(varName string) (any, error) { + if cached, ok := c.cache[varName]; ok { + return cached, nil + } + + args := strings.Split(varName, "/") + + val, err := c.realGet(args[0], args[1:]...) + if err != nil { + return nil, err + } + + c.cache[varName] = val + return val, nil +} + +func (c *calculator) getV(args ...string) (any, error) { + return c.get(strings.Join(args, "/")) +} + +func (c *calculator) realGet(varName string, args ...string) (any, error) { + switch varName { + case "router_name": + return hostname.Hostname(), nil + case "router_domain": + return hostname.DomainName(), nil + case "siteName": + return machinesSiteName, nil + case "orgSlug": + return constants.OrgSlug, nil + case "site": + if machinesSiteName == "" { + return nil, errors.New("no site name defined in client.conf or on cli") + } + + site := &Site{} + uri := fmt.Sprintf("site/%s", url.PathEscape(machinesSiteName)) + err := c.client.APICall(uri, nil, site) + if err != nil { + return nil, err + } + + return site, nil + case "sites": + sites := make([]*Site, 0) + err := c.client.APICall("sites", nil, &sites) + if err != nil { + return nil, err + } + + sitesMap := make(map[string]*Site) + for _, site := range sites { + sitesMap[site.ID()] = site + } + + return sitesMap, nil + case "interfaces": + ifaces := make([]*Iface, 0) + err := c.client.APICall("interfaces", nil, &ifaces) + if err != nil { + return nil, fmt.Errorf("while fetching interfaces: %+v", err) + } + + return ifaces, nil + case "records": + uri := "records" + if len(args) > 0 { + uri = fmt.Sprintf("domain/%s/records", args[0]) + } + + records := make([]*DNSRecord, 0) + err := c.client.APICall(uri, nil, &records) + if err != nil { + return nil, err + } + + return records, nil + case "domain": + domainID := args[0] + domain := &Domain{} + + err := c.client.APICall("domain/"+domainID, nil, domain) + if err != nil { + return nil, err + } + + ifaces, err := c.get("interfaces") + if err != nil { + return nil, err + } + + for _, i := range ifaces.([]*Iface) { + if i.ShouldPublishInDNS() { + domain.Interfaces = append(domain.Interfaces, i) + if i.LastSeen > domain.LastModified { + domain.LastModified = i.LastSeen + } + } + } + + records, err := c.getV("records", domain.ID()) + if err != nil { + return nil, err + } + + domain.Records = records.([]*DNSRecord) + + return domain, nil + + case "domains": + domains := make([]*Domain, 0) + err := c.client.APICall("domains", nil, &domains) + if err != nil { + return nil, err + } + + for _, domain := range domains { + ranges, err := c.getV("ranges", domain.ID()) + if err != nil { + return nil, fmt.Errorf("while getting ranges for domain %q: %v", domain.Name, err) + } + domain.Ranges = make(map[string]*Range, 0) + + for _, dhcpRange := range ranges.([]*Range) { + domain.Ranges[dhcpRange.ID()] = dhcpRange + + if dhcpRange.ID() == domain.DefaultRange.ID() { + domain.DefaultRange.Set(dhcpRange) + } + } + + domain.DNSSearch = []string{domain.Name} + for _, otherDomain := range domains { + if otherDomain.ID() == domain.ID() { + continue + } + + if otherDomain.Site.ID() == domain.Site.ID() { + domain.DNSSearch = append(domain.DNSSearch, otherDomain.Name) + } + } + } + + return domains, nil + case "ranges": + if len(args) < 1 { + return nil, errors.New("ranges needs an argument (domain_id)") + } + domainId := args[0] + ranges := make([]*Range, 0) + err := c.client.APICall(fmt.Sprintf("domain/%s/ranges?expand[]=reservations", domainId), nil, &ranges) + if err != nil { + return nil, err + } + + return ranges, nil + case "routeraddresses": + var domains []*Domain + if len(args) > 0 { + r, err := c.getV("domain", args[0]) + if err != nil { + return nil, err + } + + domains = []*Domain{r.(*Domain)} + } else { + r, err := c.get("domains") + if err != nil { + return nil, err + } + domains = r.([]*Domain) + } + + return GetRouterAddresses(domains) + case "zones": + zones := hashset.NewHashSet[string]() + + var site *Site + if machinesSiteName != "" { + s, err := c.get("site") + if err != nil { + return nil, err + } + + site = s.(*Site) + } + + domains, err := c.get("domains") + if err != nil { + return nil, err + } + + for _, domain := range domains.([]*Domain) { + if site != nil && domain.Site.ID() != site.ID() { + logger.Debugf("skip domain %q: not local to this site (%s)", domain.Name, machinesSiteName) + continue + } + + zones.Add(domain.Name) + + if domain.IPv4Address.Defined() { + zones.Add(domain.ReverseDNSZoneIPv4) + } + + if domain.IPv6Address.Defined() { + zones.Add(domain.ReverseDNSZoneIPv6) + } + } + + return zones.AsSortedSlice(), nil + case "reverse": + if len(args) < 1 { + return nil, errors.New("need arg (name of reverse zone)") + } + + type reverseZone struct { + ZoneName string + LastModified Timestamp + Records map[string]string + } + + rz := &reverseZone{ + ZoneName: args[0], + Records: make(map[string]string), + } + + if !strings.HasSuffix(rz.ZoneName, ".in-addr.arpa") && !strings.HasSuffix(rz.ZoneName, ".ip6.arpa") { + return nil, errors.New("reverse zone name must end in .in-addr.arpa or .ip6.arpa") + } + + ifaces, err := c.get("interfaces") + if err != nil { + return nil, err + } + + for _, iface := range ifaces.([]*Iface) { + if !iface.Host.Defined() || !iface.LastDomain.Defined() { + continue + } + if !iface.ShouldPublishInDNS() { + continue + } + if machinesSiteName != "" { + if !iface.LastDomain.Get().Site.Defined() { + continue + } + if iface.LastDomain.Get().Site.Get().Name != machinesSiteName { + continue + } + } + + forwardName := fmt.Sprintf("%s.%s", iface.Host.Get().Name, iface.LastDomain.Get().Name) + if iface.LastIPv4.Defined() { + reverseName := iface.LastIPv4.ReverseDNSName() + if strings.HasSuffix(reverseName, "."+rz.ZoneName) { + rz.Records[reverseName] = forwardName + if iface.LastSeen > rz.LastModified { + rz.LastModified = iface.LastSeen + } + } + } + + if iface.LastIPv6.Defined() { + reverseName := iface.LastIPv6.ReverseDNSName() + if strings.HasSuffix(reverseName, "."+rz.ZoneName) { + rz.Records[reverseName] = forwardName + if iface.LastSeen > rz.LastModified { + rz.LastModified = iface.LastSeen + } + } + } + } + + return rz, nil + } + return nil, fmt.Errorf("unsupported dynvar: %q", varName) +} + +func (c *calculator) end() { + c.cache = nil + c.mu.Unlock() +} + +func (ra *RouterAddress) IsLocalInterface() bool { + return ra.Interface != "relay_only" && ra.Interface != "managed_but_not_found" +} diff --git a/machines/router_addresses_bsd.go b/machines/router_addresses_bsd.go new file mode 100644 index 0000000..560200e --- /dev/null +++ b/machines/router_addresses_bsd.go @@ -0,0 +1,123 @@ +//go:build darwin || dragonfly || freebsd || netbsd || openbsd + +package machines + +import ( + "fmt" + "net" + + "go.fuhry.dev/runtime/utils/hashset" +) + +func GetRouterAddresses(domains []*Domain) (*RouterAddresses, error) { + routerAddresses := &RouterAddresses{ + IPv4: make(map[string]*RouterAddress), + IPv6: make(map[string]*RouterAddress), + } + + localAddresses := &RouterAddresses{ + IPv4: make(map[string]*RouterAddress), + IPv6: make(map[string]*RouterAddress), + } + + interfaces, err := net.Interfaces() + if err != nil { + return nil, fmt.Errorf("while attempting to list links: %v", err) + } + for _, link := range interfaces { + if (link.Flags & net.FlagLoopback) == net.FlagLoopback { + continue + } + + if addrs, err := link.Addrs(); err == nil { + for _, addr := range addrs { + if addr.Network() != "ip+net" { + continue + } + + ipaddr, ipnet, err := net.ParseCIDR(addr.String()) + if err != nil { + continue + } + + if ipv4 := ipaddr.To4(); ipv4 != nil { + localAddresses.IPv4[ipnet.IP.To4().String()] = &RouterAddress{ + Address: IPString(ipv4.String()), + Interface: link.Name, + } + logger.Infof("discovered interface %q with IP %s on network %s", + link.Name, ipv4, ipnet.IP.To4()) + } else { + if net.ParseIP("fe00::").Equal(ipaddr.Mask(net.CIDRMask(8, 128))) { + // no link-local addresses! + logger.Debugf("skip link-local address: %s", ipaddr) + continue + } + logger.Infof("discovered interface %q with IP %s on network %s", + link.Name, ipaddr, ipnet.IP) + localAddresses.IPv6[ipnet.IP.String()] = &RouterAddress{ + Address: IPString(ipaddr.String()), + Interface: link.Name, + } + } + /* + switch addr.Network() { + case "ip": + netaddr := addr.IP.Mask(addr.Mask).To4().String() + logger.V(2).Debugf("interface %q has permanent addr %s for network %s", + link.Attrs().Name, addr.IP.To4(), netaddr) + localAddresses.IPv4[netaddr] = &RouterAddress{ + Address: IPString(addr.IP.To4().String()), + Interface: link.Attrs().Name, + } + case "ipv6": + netaddr := addr.IP.Mask(addr.Mask).String() + logger.V(2).Debugf("interface %q has permanent addr %s for network %s", + link.Attrs().Name, addr.IP, netaddr) + localAddresses.IPv6[netaddr] = &RouterAddress{ + Address: IPString(addr.IP.String()), + Interface: link.Attrs().Name, + } + } + */ + } + } + } + + for _, domain := range domains { + features := hashset.FromSlice(domain.Features) + ifname := "relay_only" + + if features.Contains("interface") { + ifname = "managed_but_not_found" + } + + if domain.IPv4Address.Defined() { + ip := domain.IPv4Address.AsIP() + ip[15] += byte(machinesRouterAddressOffset) + ra := &RouterAddress{ + Address: IPString(ip.To4().String()), + Interface: ifname, + } + if lra, ok := localAddresses.IPv4[domain.IPv4Address.String()]; ok { + ra = lra + } + routerAddresses.IPv4[domain.IPv4Address.String()] = ra + } + if domain.IPv6Address.Defined() { + ip := domain.IPv6Address.AsIP() + ip[15] += byte(machinesRouterAddressOffset) + ra := &RouterAddress{ + Address: IPString(ip.String()), + Interface: ifname, + } + if lra, ok := localAddresses.IPv6[domain.IPv6Address.String()]; ok { + ra = lra + } + + routerAddresses.IPv6[domain.IPv6Address.String()] = ra + } + } + + return routerAddresses, nil +} diff --git a/machines/router_addresses_linux.go b/machines/router_addresses_linux.go new file mode 100644 index 0000000..aec1ed6 --- /dev/null +++ b/machines/router_addresses_linux.go @@ -0,0 +1,104 @@ +//go:build linux + +package machines + +import ( + "fmt" + "net" + + "github.com/vishvananda/netlink" + "go.fuhry.dev/runtime/utils/hashset" + "golang.org/x/sys/unix" +) + +func GetRouterAddresses(domains []*Domain) (*RouterAddresses, error) { + routerAddresses := &RouterAddresses{ + IPv4: make(map[string]*RouterAddress), + IPv6: make(map[string]*RouterAddress), + } + + localAddresses := &RouterAddresses{ + IPv4: make(map[string]*RouterAddress), + IPv6: make(map[string]*RouterAddress), + } + + netlinks, err := netlink.LinkList() + if err != nil { + return nil, fmt.Errorf("while attempting to list links: %v", err) + } + for _, link := range netlinks { + if (link.Attrs().Flags & net.FlagLoopback) == net.FlagLoopback { + continue + } + + if v4addrs, err := netlink.AddrList(link, unix.AF_INET); err == nil { + for _, addr := range v4addrs { + if (addr.Flags & IFA_F_PERMANENT) != IFA_F_PERMANENT { + continue + } + netaddr := addr.IP.Mask(addr.Mask).To4().String() + logger.V(2).Debugf("interface %q has permanent addr %s for network %s", + link.Attrs().Name, addr.IP.To4(), netaddr) + localAddresses.IPv4[netaddr] = &RouterAddress{ + Address: IPString(addr.IP.To4().String()), + Interface: link.Attrs().Name, + } + } + } + + if v6addrs, err := netlink.AddrList(link, unix.AF_INET6); err == nil { + for _, addr := range v6addrs { + if (addr.Flags & IFA_F_PERMANENT) != IFA_F_PERMANENT { + continue + } + if addr.Scope == RTNL_SCOPE_LINK { + continue + } + netaddr := addr.IP.Mask(addr.Mask).String() + logger.V(2).Debugf("interface %q has permanent addr %s for network %s", + link.Attrs().Name, addr.IP, netaddr) + localAddresses.IPv6[netaddr] = &RouterAddress{ + Address: IPString(addr.IP.String()), + Interface: link.Attrs().Name, + } + } + } + } + + for _, domain := range domains { + features := hashset.FromSlice(domain.Features) + ifname := "relay_only" + + if features.Contains("interface") { + ifname = "managed_but_not_found" + } + + if domain.IPv4Address.Defined() { + ip := domain.IPv4Address.AsIP() + ip[15] += byte(machinesRouterAddressOffset) + ra := &RouterAddress{ + Address: IPString(ip.To4().String()), + Interface: ifname, + } + if lra, ok := localAddresses.IPv4[domain.IPv4Address.String()]; ok { + ra = lra + } + routerAddresses.IPv4[domain.IPv4Address.String()] = ra + } + if domain.IPv6Address.Defined() { + ip := domain.IPv6Address.AsIP() + ip[15] += byte(machinesRouterAddressOffset) + ra := &RouterAddress{ + Address: IPString(ip.String()), + Interface: ifname, + } + if lra, ok := localAddresses.IPv6[domain.IPv6Address.String()]; ok { + ra = lra + } + + routerAddresses.IPv6[domain.IPv6Address.String()] = ra + } + } + + return routerAddresses, nil +} diff --git a/machines/services.go b/machines/services.go new file mode 100644 index 0000000..4e71146 --- /dev/null +++ b/machines/services.go @@ -0,0 +1,37 @@ +package machines + +type ServiceStatus struct { + Running bool + Pid int +} + +type Service interface { + EnsureStarted() error + EnsureStopped() error + ReloadOrRestart(startIfStopped bool) error + Status() (*ServiceStatus, error) +} + +type noopService struct{} + +func (s *noopService) EnsureStarted() error { return nil } +func (s *noopService) EnsureStopped() error { return nil } +func (s *noopService) ReloadOrRestart(bool) error { return nil } + +func (s *noopService) Status() (*ServiceStatus, error) { + return &ServiceStatus{Running: true, Pid: 0}, nil +} + +var services = make(map[string]Service) + +func registerService(name string, svc Service) { + services[name] = svc +} + +func GetService(name string) Service { + if svc, ok := services[name]; ok { + return svc + } + + return nil +} diff --git a/machines/services_linux.go b/machines/services_linux.go new file mode 100644 index 0000000..93dc755 --- /dev/null +++ b/machines/services_linux.go @@ -0,0 +1,255 @@ +//go:build linux + +package machines + +import ( + "context" + "fmt" + "strconv" + "sync" + "time" + + systemd_dbus "github.com/coreos/go-systemd/v22/dbus" +) + +type systemdService struct { + unit string +} + +const ( + propActiveState = "ActiveState" + propSubState = "SubState" + propMainPID = "MainPID" + + unitStateInactive = "inactive" + unitStateFailed = "failed" + unitStateActivating = "activating" + unitStateDeactivating = "deactivating" + unitStateActive = "active" + + unitSubstateRunning = "running" + unitSubstateExited = "exited" + unitSubstateDead = "dead" + + modeReplace = "replace" + + jobStatusDone = "done" + + unitStartTimeout = 5 * time.Second +) + +var systemd *systemd_dbus.Conn +var systemdMu sync.Mutex + +func sdConn() (*systemd_dbus.Conn, error) { + systemdMu.Lock() + defer systemdMu.Unlock() + + if systemd == nil { + sdconn, err := systemd_dbus.New() + if err != nil { + return nil, err + } + systemd = sdconn + } + + return systemd, nil +} + +func (s *systemdService) EnsureStarted() error { + sd, err := sdConn() + if err != nil { + return err + } + + active, err := sd.GetUnitProperty(s.unit, propActiveState) + if err != nil { + return err + } + + switch active.Value.Value().(string) { + case unitStateActive: + // unit is already active + return nil + case unitStateActivating, unitStateInactive, unitStateFailed: + ctx, cancel := context.WithTimeout(context.Background(), unitStartTimeout) + defer cancel() + + statusChan := make(chan string) + defer close(statusChan) + + _, err = sd.StartUnit(s.unit, modeReplace, statusChan) + + for { + select { + case status := <-statusChan: + if status == jobStatusDone { + return nil + } + return fmt.Errorf("job failed with final status: %s", status) + case <-ctx.Done(): + return ctx.Err() + } + } + default: + return fmt.Errorf("don't know how to handle ActiveState %q", active.Value.Value().(string)) + } +} + +func (s *systemdService) EnsureStopped() error { + sd, err := sdConn() + if err != nil { + return err + } + + active, err := sd.GetUnitProperty(s.unit, propActiveState) + if err != nil { + return err + } + + switch active.Value.Value().(string) { + case unitStateInactive, unitStateFailed: + // unit is already inactive + return nil + case unitStateActive, unitStateActivating, unitStateDeactivating: + ctx, cancel := context.WithTimeout(context.Background(), unitStartTimeout) + defer cancel() + + statusChan := make(chan string) + defer close(statusChan) + + _, err = sd.StopUnit(s.unit, modeReplace, statusChan) + + for { + select { + case status := <-statusChan: + if status == jobStatusDone { + return nil + } + return fmt.Errorf("job failed with final status: %s", status) + case <-ctx.Done(): + return ctx.Err() + } + } + default: + return fmt.Errorf("don't know how to handle ActiveState %q", active.Value.Value().(string)) + } +} + +func (s *systemdService) ReloadOrRestart(startIfStopped bool) error { + sd, err := sdConn() + if err != nil { + return err + } + + active, err := sd.GetUnitProperty(s.unit, propActiveState) + if err != nil { + return err + } + + switch active.Value.Value().(string) { + case unitStateActive: + ctx, cancel := context.WithTimeout(context.Background(), unitStartTimeout) + defer cancel() + + statusChan := make(chan string) + defer close(statusChan) + + _, err = sd.ReloadOrRestartUnit(s.unit, modeReplace, statusChan) + + for { + select { + case status := <-statusChan: + if status == jobStatusDone { + return nil + } + return fmt.Errorf("job failed with final status: %s", status) + case <-ctx.Done(): + return ctx.Err() + } + } + case unitStateInactive, unitStateFailed: + if !startIfStopped { + // unit is stopped and we were asked not to start it + return nil + } + + return s.EnsureStarted() + case unitStateDeactivating: + err := s.EnsureStopped() + if err != nil { + return err + } + + err = s.EnsureStarted() + if err != nil { + return err + } + + return nil + case unitStateActivating: + err := s.EnsureStarted() + if err != nil { + return err + } + + return s.ReloadOrRestart(startIfStopped) + default: + return fmt.Errorf("don't know how to handle ActiveState %q", active.Value.Value().(string)) + } +} + +func (s *systemdService) Status() (*ServiceStatus, error) { + sd, err := sdConn() + if err != nil { + return nil, err + } + + active, err := sd.GetUnitProperty(s.unit, propActiveState) + if err != nil { + return nil, err + } + + switch active.Value.Value().(string) { + case unitStateActive: + substate, err := sd.GetUnitProperty(s.unit, propSubState) + if err != nil { + return nil, err + } + + if substate.Value.Value().(string) == unitSubstateRunning { + mainPid, err := sd.GetUnitProperty(s.unit, propSubState) + if err != nil { + return nil, err + } + + pid, err := strconv.Atoi(mainPid.Value.Value().(string)) + if err != nil { + return nil, err + } + + return &ServiceStatus{ + Running: true, + Pid: pid, + }, nil + } + + return &ServiceStatus{ + Running: true, + Pid: 0, + }, nil + default: + return &ServiceStatus{ + Running: false, + Pid: 0, + }, nil + } +} + +func init() { + registerService("coredns", &systemdService{unit: "coredns.service"}) + registerService("dhcpd4", &systemdService{unit: "dhcpd4.service"}) + registerService("dhcpd6", &systemdService{unit: "dhcpd6.service"}) + registerService("radvd", &systemdService{unit: "radvd.service"}) + registerService("zones", &noopService{}) +} diff --git a/machines/stats.go b/machines/stats.go new file mode 100644 index 0000000..ea2da48 --- /dev/null +++ b/machines/stats.go @@ -0,0 +1,35 @@ +package machines + +import ( + "go.fuhry.dev/runtime/constants" + "go.fuhry.dev/runtime/metrics/metricbus/mbclient" +) + +type clientStats struct { + apiRequests mbclient.CounterMetric + oauthRequests mbclient.CounterMetric + mqttMessages mbclient.CounterMetric +} + +func makeStats(client *mbclient.MetricBusService) *clientStats { + apiRequests := client.DefineCounter( + constants.OrgSlug+"_machines_client_api_requests", + "Requests made by the Machines client to the API", + "method", "route", "response_code", "result") + + oauthRequests := client.DefineCounter( + constants.OrgSlug+"_machines_client_oauth_requests", + "Requests made by the Machines client for OAuth tokens", + "result") + + mqttMessages := client.DefineCounter( + constants.OrgSlug+"_machines_mqtt_messages", + "MQTT messages received", + "parse_result", "thing", "action") + + return &clientStats{ + apiRequests: apiRequests, + oauthRequests: oauthRequests, + mqttMessages: mqttMessages, + } +} diff --git a/machines/templates/corefile.conf.j2 b/machines/templates/corefile.conf.j2 new file mode 100644 index 0000000..48e12fa --- /dev/null +++ b/machines/templates/corefile.conf.j2 @@ -0,0 +1,10 @@ +root /var/named/etc/named/zones + +{% for zone in zones -%} +# file {{zone}}.db {{zone}} +{% endfor %} + +auto {{zones | join(" ")}} { + directory /var/named/etc/named/zones ^({{ zones | join("|") | replace(".", "\\.") }})\.db$ {1} + reload 10s +} diff --git a/machines/templates/dhcpd4.conf.j2 b/machines/templates/dhcpd4.conf.j2 new file mode 100644 index 0000000..49ef12c --- /dev/null +++ b/machines/templates/dhcpd4.conf.j2 @@ -0,0 +1,90 @@ +# dhcpd4.conf + +update-static-leases on; +authoritative; +option pxe-vendor-string code 43 = text; +option pxe-tftp-server code 66 = ip-address; +option pxe-filename code 67 = text; +option pxe-vendor-class code 60 = text; + +option space pxelinux; +option pxelinux.magic code 208 = string; +option pxelinux.configfile code 209 = text; +option pxelinux.pathprefix code 210 = text; +option pxelinux.reboottime code 211 = unsigned integer 32; +option architecture-type code 93 = unsigned integer 16; + +on commit { + set clip = binary-to-ascii(10, 8, ".", leased-address); + set clhw = binary-to-ascii(16, 8, ":", substring(hardware, 1, 6)); + execute("/usr/local/bin/xm-dhcp-commit", "--ip-address", clip, "--mac-address", clhw); +} + +{% for domain in domains %} +{% if (sites[domain.Site.ID()].Name == siteName) and ('dhcp4' in domain.Features) and domain.IPv4Address and domain.DefaultRange.Defined() and domain.DefaultRange.Get().IPv4Start and domain.DefaultRange.Get().IPv4End %} +# {{ domain.Name }} +subnet {{ domain.IPv4Address }} netmask {{ domain.IPv4PrefixLength.Mask() }} { + {% if routeraddresses.IPv4[domain.IPv4Address] is defined %} + option routers {{ routeraddresses.IPv4[domain.IPv4Address].Address }}; + option domain-name-servers {{ routeraddresses.IPv4[domain.IPv4Address].Address }}; + {% endif %} + + option domain-name "{{ domain.DNSSearch[0] }}"; + # option domain-search "{{ domain.DNSSearch[0] }}"; + option domain-search "{{ domain.DNSSearch | join("\", \"") }}"; + + {% if domain.PXEServerIPv4 and (domain.PXEFilenameBIOS or domain.PXEFilenameUEFI) %} + class "pxeclients" { + match if substring (option vendor-class-identifier, 0, 9) = "PXEClient"; + next-server {{ domain.PXEServerIPv4 }}; + if exists user-class and option user-class = "iPXE" { + {% if domain.PXEFilenameIPXE -%} + filename "{{ domain.PXEFilenameIPXE }}"; + {%- endif %} + } else if option architecture-type = 00:07 or option architecture-type = 00:06 { + {% if domain.PXEFilenameUEFI -%} + filename "{{ domain.PXEFilenameUEFI }}"; + {%- endif %} + } else { + {% if domain.PXEFilenameBIOS -%} + filename "{{ domain.PXEFilenameBIOS }}"; + {%- endif %} + } + } + {% endif %} + + {% for id, range in domain.Ranges %} + {% if range.IPv4Start and range.IPv4End %} + # {{ range.Name }} + pool { + range {{ domain.Ranges[id].IPv4Start }} {{ domain.Ranges[id].IPv4End }}; + {% if domain.Ranges[id].ID() == domain.DefaultRange.ID() %} + allow unknown-clients; + {%- else -%} + deny unknown-clients; + {%- endif %} + + {% for res in domain.Ranges[id].Reservations %} + {%- if res.AddressFamily == 'inet' -%} + # reservation: {{ res.ID() }} + host {{ res.Iface.Get().NameScrubbed }}.{{ res.Iface.Get().Host.Get().Name }}.{{ domain.Name }} { + hardware ethernet {{ res.Iface.Get().HardwareAddress }}; + fixed-address {{ res.Address }}; + } + {% endif -%} + {%- endfor %} + } + + {%- endif -%} + {%- endfor %} +} + +{%- else %} +# SKIP IPv4 subnet for: {{ domain.Name }} +# siteID: {{ domain.Site.ID() }}, Name = {{sites[domain.Site.ID()].Name}} / local site: {{siteName}} +# inet4 prefix: {% if domain.IPv4Address -%}{{domain.IPv4Address}}/{{ domain.IPv4PrefixLength }}{%- else -%}{%- endif %} +# default range: {% if domain.DefaultRange.ID() -%}{{domain.DefaultRange.ID()}}{%- else -%}{%- endif %} +# default range IPs: {% if domain.DefaultRange.ID() and domain.DefaultRange.Get().IPv4Start and domain.DefaultRange.Get().IPv4End -%}{{domain.DefaultRange.Get().IPv4Start}} - {{domain.DefaultRange.Get().IPv4End}}{%- else -%}{%- endif -%} + +{%- endif -%} +{% endfor %} diff --git a/machines/templates/dhcpd6.conf.j2 b/machines/templates/dhcpd6.conf.j2 new file mode 100644 index 0000000..4a669a4 --- /dev/null +++ b/machines/templates/dhcpd6.conf.j2 @@ -0,0 +1,75 @@ +authoritative; + +on commit { + if substring(option dhcp6.client-id, 0, 2) = 00:03 { + # DUID-LL + execute("/usr/local/bin/xm-dhcp-commit", + "--ip-address", + binary-to-ascii( + 16, 16, ":", substring(pick-first-value( + option dhcp6.ia-na, + option dhcp6.ia-ta, + 00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00 + ), 16, 16)), + "--mac-address", + binary-to-ascii(16, 8, ":", substring(option dhcp6.client-id, 4, 6)) + ); + } + elsif substring(option dhcp6.client-id, 0, 2) = 00:01 { + # DUID-LLT + execute("/usr/local/bin/xm-dhcp-commit", + "--ip-address", + binary-to-ascii( + 16, 16, ":", substring(pick-first-value( + option dhcp6.ia-na, + option dhcp6.ia-ta, + 00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00 + ), 16, 16)), + "--mac-address", + binary-to-ascii(16, 8, ":", substring(option dhcp6.client-id, 8, 6)) + ); + } +} + +{% for domain in domains %} + {% if sites[domain.Site.ID()].Name == siteName and 'dhcp6' in domain.Features and domain.IPv6Address is defined and domain.DefaultRange.Defined() and domain.DefaultRange.Get().IPv6Start and domain.DefaultRange.Get().IPv6End %} + # {{ domain.Name }} + subnet6 {{ domain.IPv6Address }}/{{ domain.IPv6PrefixLength }} { + {% if routeraddresses.IPv6[domain.IPv6Address] -%} + option dhcp6.name-servers {{ routeraddresses.IPv6[domain.IPv6Address].Address }}; + {%- endif %} + # option dhcp6.domain-name {{ domain.DNSSearch[0] }}; + # option dhcp6.domain-search {{ domain.DNSSearch[0] }}; + option dhcp6.domain-search "{{ domain.DNSSearch | join("\", \"") }}"; + + {% for id, range in domain.Ranges -%} + {%- if range.IPv6Start and range.IPv6End -%} + # {{ range.Name }} + pool6 { + range6 {{ range.IPv6Start }} {{ range.IPv6End }}; + {% if range.ID() == domain.DefaultRange.ID() %} + allow unknown-clients; + {%- else -%} + deny unknown-clients; + {%- endif %} + + {%- for res in range.Reservations -%} + {% if res.AddressFamily == 'inet6' %} + # reservation: {{ res.ID() }} + host {{ res.Iface.Get().NameScrubbed }}.{{ res.Iface.Get().Host.Get().Name }}.{{ domain.Name }} { + hardware ethernet {{ res.Iface.Get().HardwareAddress }}; + fixed-address6 {{ res.Address }}; + } + {% endif %} + {% endfor %} + } + {%- endif %} + {% endfor %} + } + {%- else %} + # SKIP IPv6 subnet for: {{ domain.Name }} + # inet6 prefix: {% if domain.IPv6Address -%}{{domain.IPv6Address}}/{{ domain.IPv6PrefixLength }}{%- else -%}{%- endif %} + # default range: {% if domain.DefaultRangeID -%}{{domain.DefaultRangeID}}{%- else -%}{%- endif %} + # default range IPs: {% if domain.DefaultRangeID and domain.DefaultRange.IPv6Start and domain.DefaultRange.IPv6End -%}{{domain.DefaultRange.IPv6Start}} - {{domain.DefaultRange.IPv6End}}{%- else -%}{%- endif -%} + {%- endif -%} +{%- endfor -%} diff --git a/machines/templates/forward.zone.j2 b/machines/templates/forward.zone.j2 new file mode 100644 index 0000000..40d0f88 --- /dev/null +++ b/machines/templates/forward.zone.j2 @@ -0,0 +1,44 @@ +$ORIGIN {{ domain.Name }}. +@ 604800 IN SOA {{ router_name }}.{{ domain.Name }}. root.{{ domain.Name }}. ( + {{ domain.LastModified }} ; Serial + 1800 ; Refresh + 300 ; Retry + 604800 ; Expire + 60 ; Min. TTL + ) + +@ IN NS {{ router_name }}.{{ domain.Name }}. + +$TTL 300 + +; Hosts +{%- if routeraddresses.IPv4[domain.IPv4Address] and routeraddresses.IPv4[domain.IPv4Address].Interface != 'relay_only' %} +{{ router_name }} 3600 IN A {{ routeraddresses.IPv4[domain.IPv4Address].Address }} +{%- endif %} +{%- if routeraddresses.IPv6[domain.IPv6Address] and routeraddresses.IPv6[domain.IPv6Address].Interface != 'relay_only' %} +{{ router_name }} 3600 IN AAAA {{ routeraddresses.IPv6[domain.IPv6Address].Address }} +{%- endif %} + +{%- for iface in domain.Interfaces %} +{% if iface.Host.Get() and iface.LastDomain -%} +{%- if iface.LastDomain.ID() == domain.ID() -%} {# only publish A/AAAA records in this domain if this is where the interface was last seen #} +{{ iface.NameScrubbed }}.{{ iface.Host.Get().Name }} 3600 IN TXT "{{orgSlug}}-machines:i={{ iface.ID() }};t={{ iface.LastSeen.AsTime() }};h={{ iface.HardwareAddress }};m={{ iface.MediaType }}" +{%- if iface.LastIPv4 %} +{{ iface.NameScrubbed }}.{{ iface.Host.Get().Name }} 3600 IN A {{ iface.LastIPv4 }} +{%- endif -%} +{%- if iface.LastIPv6 %} +{{ iface.NameScrubbed }}.{{ iface.Host.Get().Name }} 3600 IN AAAA {{ iface.LastIPv6 }} +{%- endif -%} {# inet4/inet6 records #} +{%- endif -%} {# domain check #} +{%- if iface.ID() == iface.Host.Get().LastSeenIface.ID() and iface.Host.Get().Name != router_name and iface.LastDomain.Get().Site.ID() == site.ID() %} +{{ iface.Host.Get().Name }} 300 IN CNAME {{ iface.NameScrubbed }}.{{ iface.Host.Get().Name }}.{{ iface.LastDomain.Get().Name }}. +{%- endif -%} +{%- else -%} +; skip interface {{ iface.Name }} (hwaddr {{ iface.HardwareAddress }}): iface.Host defined = {{ iface.Host.ID() != "" }} / iface.last_domain defined {{ iface.LastDomain.ID() != "" }} +{%- endif -%} +{%- endfor %} + +; Custom records +{%- for record in domain.Records %} +{{ record }} +{%- endfor %} diff --git a/machines/templates/hostname.bridge.j2 b/machines/templates/hostname.bridge.j2 new file mode 100644 index 0000000..1c1d849 --- /dev/null +++ b/machines/templates/hostname.bridge.j2 @@ -0,0 +1,4 @@ +add {{ vlan_iface }} +flushrule {{ vlan_iface }} +rulefile /etc/pf/{{ domain.name }}-maclist.conf +up diff --git a/machines/templates/hostname.native.j2 b/machines/templates/hostname.native.j2 new file mode 100644 index 0000000..833d052 --- /dev/null +++ b/machines/templates/hostname.native.j2 @@ -0,0 +1,7 @@ +{% if domain.inet4_address -%} +inet {{domain.inet4_routeraddr}}/{{domain.inet4_prefixlen}} +{% endif -%} +{% if domain.inet6_address -%} +inet6 {{domain.inet6_routeraddr}}/{{domain.inet6_prefixlen}} +{% endif -%} +up diff --git a/machines/templates/hostname.vlan.j2 b/machines/templates/hostname.vlan.j2 new file mode 100644 index 0000000..f8f1921 --- /dev/null +++ b/machines/templates/hostname.vlan.j2 @@ -0,0 +1,9 @@ +vlan {{domain.vlan_id}} +vlandev {{router_interface}} +{% if domain.inet4_address -%} +inet {{domain.inet4_routeraddr}}/{{domain.inet4_prefixlen}} +{% endif -%} +{% if domain.inet6_address -%} +inet6 {{domain.inet6_routeraddr}}/{{domain.inet6_prefixlen}} +{% endif -%} +up diff --git a/machines/templates/maclist.conf.j2 b/machines/templates/maclist.conf.j2 new file mode 100644 index 0000000..b283f67 --- /dev/null +++ b/machines/templates/maclist.conf.j2 @@ -0,0 +1,8 @@ +# This file is automatically generated by machines-agent and will be overwritten +# any time a change is made to registered hosts/interfaces. + +{% for iface in interfaces -%} +{% if iface.host != None and 'disabled' not in iface.host.flags and 'quarantine' not in iface.host.owner.flags -%} +pass in on vlan{{ domain.vlan_id }} src {{ iface.hardware_address }} tag PERMIT +{% endif -%} +{% endfor -%} diff --git a/machines/templates/named.conf.j2 b/machines/templates/named.conf.j2 new file mode 100644 index 0000000..5f1f51f --- /dev/null +++ b/machines/templates/named.conf.j2 @@ -0,0 +1,6 @@ +{% for zone in zones %} +zone "{{zone}}." IN { + type master; + file "/etc/named/zones/{{zone}}.db"; +}; +{% endfor %} diff --git a/machines/templates/rad.conf.j2 b/machines/templates/rad.conf.j2 new file mode 100644 index 0000000..482325f --- /dev/null +++ b/machines/templates/rad.conf.j2 @@ -0,0 +1,15 @@ +{% for domain in domains %} +{%- if domain.Site.ID() == site.ID() -%} +{%- if 'rad' in domain.Features and domain.IPv6Address.Defined() -%} +{%- if routeraddresses.IPv6[domain.IPv6Address] -%} +{%- set ra = routeraddresses.IPv6[domain.IPv6Address] -%} +{%- if ra.Interface != "relay_only" and ra.Interface != "managed_but_not_found" -%} +# {{ domain.Name }} +interface {{ra.Interface }} { + managed address configuration yes +} +{% endif %} +{%- endif -%} +{%- endif -%} +{%- endif -%} +{%- endfor -%} diff --git a/machines/templates/radvd.conf.j2 b/machines/templates/radvd.conf.j2 new file mode 100644 index 0000000..6df1c3c --- /dev/null +++ b/machines/templates/radvd.conf.j2 @@ -0,0 +1,31 @@ +{% for domain in domains %} +{%- if domain.Site.ID() == site.ID() -%} +{%- if 'rad' in domain.Features and domain.IPv6Address.Defined() -%} +{%- if routeraddresses.IPv6[domain.IPv6Address] -%} +{%- set ra = routeraddresses.IPv6[domain.IPv6Address] -%} +{%- if ra.Interface != "relay_only" and ra.Interface != "managed_but_not_found" -%} +# {{ domain.Name }} +interface {{ ra.Interface }} +{ + AdvSendAdvert on; + + MinRtrAdvInterval 5; + MaxRtrAdvInterval 10; + + AdvManagedFlag on; + AdvOtherConfigFlag on; + + prefix {{domain.IPv6Address}}/{{domain.IPv6PrefixLength}} + { + AdvOnLink on; + AdvAutonomous on; + AdvRouterAddr on; + }; + + RDNSS {{ra.Address}} {}; +}; +{% endif %} +{%- endif -%} +{%- endif -%} +{%- endif -%} +{%- endfor -%} diff --git a/machines/templates/reverse.zone.j2 b/machines/templates/reverse.zone.j2 new file mode 100644 index 0000000..a5d0592 --- /dev/null +++ b/machines/templates/reverse.zone.j2 @@ -0,0 +1,15 @@ +$ORIGIN {{reverse.ZoneName}}. + +@ 604800 IN SOA {{ router_name }}.{{ router_domain }}. root.{{ router_domain }}. ( + {{ reverse.LastModified }} ; Serial + 1800 ; Refresh + 300 ; Retry + 604800 ; Expire + 60 ; Min. TTL + ) + +@ IN NS {{ router_name }}.{{ router_domain }}. +{% for ptr, target in reverse.Records %} +{{ ptr }}. 60 IN PTR {{target}}. +{%- endfor -%} + diff --git a/machines/templates/rtadvd.conf.j2 b/machines/templates/rtadvd.conf.j2 new file mode 100644 index 0000000..c5a1cdc --- /dev/null +++ b/machines/templates/rtadvd.conf.j2 @@ -0,0 +1,10 @@ +{% for domain in domains %} +{%- if domain.inet6_address -%} +{%- if routeraddresses.inet6[domain.inet6_address] -%} +# {{ domain.name }} +{{ routeraddresses.inet6[domain.inet6_address].interface }}:\ + :addr="{{ domain.inet6_address }}":{{ domain.inet6_prefixlen }}:raflags=0x80:mtu=auto:rdnss="{{ routeraddresses.inet6[domain.inet6_address].address }}":dnssl="{{ domain.dns_search[0] }}": + +{% endif %} +{%- endif -%} +{%- endfor -%} diff --git a/machines/templates/unreg-lockdown.conf.j2 b/machines/templates/unreg-lockdown.conf.j2 new file mode 100644 index 0000000..4726f5a --- /dev/null +++ b/machines/templates/unreg-lockdown.conf.j2 @@ -0,0 +1,34 @@ +# This file is automatically generated by machines-agent and will be overwritten +# any time a change is made to the domain configuration. + +www_v4={{api_server.inet4_address}} +www_v6={{api_server.inet6_address}} + +{% if ' '.join(lockdown_ifs) != '' %} +block in on { {{' '.join(lockdown_ifs)}} } from any to any +pass in quick on { {{' '.join(lockdown_ifs)}} } from any to any tagged PERMIT + +# pass-all for switch/ap testing +#pass in quick on { {{' '.join(lockdown_ifs)}} } from any to any +#pass out quick on { {{' '.join(lockdown_ifs)}} } from any to any + +# Next three rules are for production: DNS pass and HTTP redirectors +# All traffic other than DNS, HTTP and HTTPS is completely dropped post-DHCP +pass in on { {{' '.join(lockdown_ifs)}} } proto udp to any port { 53 } + +# (unauthed) Permit IPv6 NDP +pass in quick inet6 proto ipv6-icmp to { ff01::/32 } +pass in quick inet6 proto ipv6-icmp to { ff02::/32 } + +# (unauthed) Permit IPv6 traffic to webserver +pass in quick on { {{' '.join(lockdown_ifs)}} } proto tcp to $www_v4 port { 80 443 } +pass in quick on { {{' '.join(lockdown_ifs)}} } inet6 proto tcp to $www_v6 port { 80 443 } +pass in on { {{' '.join(lockdown_ifs)}} } proto tcp to 0.0.0.0/0 port 80 rdr-to 127.0.0.1 port 4480 +pass in on { {{' '.join(lockdown_ifs)}} } proto tcp to 0.0.0.0/0 port 443 rdr-to 127.0.0.1 port 4443 +pass in on { {{' '.join(lockdown_ifs)}} } proto tcp to 0.0.0.0/0 port 4443 rdr-to 127.0.0.1 port 4443 +pass in on { {{' '.join(lockdown_ifs)}} } proto tcp to ::/0 port 80 rdr-to ::1 port 4480 +pass in on { {{' '.join(lockdown_ifs)}} } proto tcp to ::/0 port 443 rdr-to ::1 port 4443 +pass in on { {{' '.join(lockdown_ifs)}} } proto tcp to ::/0 port 4443 rdr-to ::1 port 4443 +{% else %} +# No VLANs use mandatory registration. Nothing to do! +{% endif %} diff --git a/machines/types_test.go b/machines/types_test.go new file mode 100644 index 0000000..a996938 --- /dev/null +++ b/machines/types_test.go @@ -0,0 +1,30 @@ +package machines_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "go.fuhry.dev/runtime/machines" +) + +func TestIPString(t *testing.T) { + type testCase struct { + ip machines.IPString + ipv4 bool + rdns string + } + + testCases := []*testCase{ + { + "10.1.0.1", + true, + "1.0.1.10.in-addr.arpa", + }, + } + + for _, tc := range testCases { + require.Equal(t, tc.ipv4, tc.ip.IsIPv4()) + require.Equal(t, tc.rdns, tc.ip.ReverseDNSName()) + } +} diff --git a/utils/strings2/strings2.go b/utils/strings2/strings2.go new file mode 100644 index 0000000..25e7f57 --- /dev/null +++ b/utils/strings2/strings2.go @@ -0,0 +1,18 @@ +package strings2 + +// Chunk splits a string into chunks of size l characters each. +func Chunk(str string, l int) []string { + nchunks := len(str) / l + if len(str)%l > 0 { + nchunks++ + } + chunks := make([]string, nchunks) + for i := 0; i < len(str); i += l { + ie := i + l + if ie > len(str) { + ie = len(str) + } + chunks[i/l] = str[i:ie] + } + return chunks +} -- 2.50.1