From d35e2400b2e57e7a83ffbd95ab51fcae9057bdd6 Mon Sep 17 00:00:00 2001 From: Juergen Hoetzel Date: Mon, 30 Dec 2019 19:42:17 +0100 Subject: [PATCH] transport/{TCP,TLS}: optional IP_FREEBIND / IP_BINDANY bind socketops Allows to bind to an address even if it is not actually (yet or ever) configured. Fixes #238 Rationale: https://www.freedesktop.org/wiki/Software/systemd/NetworkTarget/#whatdoesthismeanformeadeveloper --- build/tools.go | 6 ++--- config/config.go | 13 +++++---- daemon/prometheus.go | 8 +++--- docs/changelog.rst | 5 ++++ docs/configuration/monitoring.rst | 2 ++ docs/configuration/transports.rst | 8 ++++++ platformtest/harness/harness.go | 1 + platformtest/platformtest_zpool.go | 1 + .../tests/undestroyableSnapshotParsing.go | 1 + transport/tcp/serve_tcp.go | 7 ++--- transport/tls/serve_tls.go | 7 +++-- util/tcpsock/tcpsock.go | 27 +++++++++++++++++++ util/tcpsock/tcpsock_freebind_freebsd.go | 25 +++++++++++++++++ util/tcpsock/tcpsock_freebind_linux.go | 19 +++++++++++++ util/tcpsock/tcpsock_freebind_unsupported.go | 12 +++++++++ zfs/zfs.go | 1 + 16 files changed, 123 insertions(+), 20 deletions(-) create mode 100644 util/tcpsock/tcpsock.go create mode 100644 util/tcpsock/tcpsock_freebind_freebsd.go create mode 100644 util/tcpsock/tcpsock_freebind_linux.go create mode 100644 util/tcpsock/tcpsock_freebind_unsupported.go diff --git a/build/tools.go b/build/tools.go index ffb7ac2..a3c7870 100644 --- a/build/tools.go +++ b/build/tools.go @@ -3,9 +3,9 @@ package main import ( - _ "golang.org/x/tools/cmd/stringer" - _ "github.com/golang/protobuf/protoc-gen-go" _ "github.com/alvaroloes/enumer" - _ "golang.org/x/tools/cmd/goimports" + _ "github.com/golang/protobuf/protoc-gen-go" _ "github.com/golangci/golangci-lint/cmd/golangci-lint" + _ "golang.org/x/tools/cmd/goimports" + _ "golang.org/x/tools/cmd/stringer" ) diff --git a/config/config.go b/config/config.go index 9b11c62..76893e9 100644 --- a/config/config.go +++ b/config/config.go @@ -245,14 +245,16 @@ type ServeCommon struct { } type TCPServe struct { - ServeCommon `yaml:",inline"` - Listen string `yaml:"listen,hostport"` - Clients map[string]string `yaml:"clients"` + ServeCommon `yaml:",inline"` + Listen string `yaml:"listen,hostport"` + ListenFreeBind bool `yaml:"listen_freebind,default=false"` + Clients map[string]string `yaml:"clients"` } type TLSServe struct { ServeCommon `yaml:",inline"` Listen string `yaml:"listen,hostport"` + ListenFreeBind bool `yaml:"listen_freebind,default=false"` Ca string `yaml:"ca"` Cert string `yaml:"cert"` Key string `yaml:"key"` @@ -331,8 +333,9 @@ type MonitoringEnum struct { } type PrometheusMonitoring struct { - Type string `yaml:"type"` - Listen string `yaml:"listen,hostport"` + Type string `yaml:"type"` + Listen string `yaml:"listen,hostport"` + ListenFreeBind bool `yaml:"listen_freebind,default=false"` } type SyslogFacility syslog.Priority diff --git a/daemon/prometheus.go b/daemon/prometheus.go index 685a3c6..0de0d4c 100644 --- a/daemon/prometheus.go +++ b/daemon/prometheus.go @@ -12,18 +12,20 @@ import ( "github.com/zrepl/zrepl/daemon/job" "github.com/zrepl/zrepl/logger" "github.com/zrepl/zrepl/rpc/dataconn/frameconn" + "github.com/zrepl/zrepl/util/tcpsock" "github.com/zrepl/zrepl/zfs" ) type prometheusJob struct { - listen string + listen string + freeBind bool } func newPrometheusJobFromConfig(in *config.PrometheusMonitoring) (*prometheusJob, error) { if _, _, err := net.SplitHostPort(in.Listen); err != nil { return nil, err } - return &prometheusJob{in.Listen}, nil + return &prometheusJob{in.Listen, in.ListenFreeBind}, nil } var prom struct { @@ -60,7 +62,7 @@ func (j *prometheusJob) Run(ctx context.Context) { log := job.GetLogger(ctx) - l, err := net.Listen("tcp", j.listen) + l, err := tcpsock.Listen(j.listen, j.freeBind) if err != nil { log.WithError(err).Error("cannot listen") return diff --git a/docs/changelog.rst b/docs/changelog.rst index c52ddd1..264d8b4 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -27,6 +27,11 @@ We use the following annotations for classifying changes: * |bugfix| Change that fixes a bug, no regressions or incompatibilities expected. * |docs| Change to the documentation. +0.2.2 (unreleased) +------------------ + +* |feature| New option ``listen_freebind`` (tcp, tls, prometheus listener) + 0.2.1 ----- diff --git a/docs/configuration/monitoring.rst b/docs/configuration/monitoring.rst index 138b968..5965a4f 100644 --- a/docs/configuration/monitoring.rst +++ b/docs/configuration/monitoring.rst @@ -14,6 +14,7 @@ Prometheus & Grafana zrepl can expose `Prometheus metrics `_ via HTTP. The ``listen`` attribute is a `net.Listen `_ string for tcp, e.g. ``:9091`` or ``127.0.0.1:9091``. +The ``listen_freebind`` attribute is :ref:`explained here `. The Prometheues monitoring job appears in the ``zrepl control`` job list and may be specified **at most once**. zrepl also ships with an importable `Grafana `_ dashboard that consumes the Prometheus metrics: @@ -30,6 +31,7 @@ The dashboard also contains some advice on which metrics are important to monito monitoring: - type: prometheus listen: ':9091' + listen_freebind: true # optional, default false diff --git a/docs/configuration/transports.rst b/docs/configuration/transports.rst index f28814a..0edad9b 100644 --- a/docs/configuration/transports.rst +++ b/docs/configuration/transports.rst @@ -50,12 +50,18 @@ Serve serve: type: tcp listen: ":8888" + listen_freebind: true # optional, default false clients: { "192.168.122.123" : "mysql01" "192.168.122.123" : "mx01" } ... +.. _listen-freebind-explanation: + +``listen_freebind`` controls whether the socket is allowed to bind to non-local or unconfigured IP addresses (Linux ``IP_FREEBIND`` , FreeBSD ``IP_BINDANY``). +Enable this option if you want to ``listen`` on a specific IP address that might not yet be configured when the zrepl daemon starts. + Connect ~~~~~~~ @@ -101,6 +107,7 @@ Serve serve: type: tls listen: ":8888" + listen_freebind: true # optional, default false ca: /etc/zrepl/ca.crt cert: /etc/zrepl/prod.fullchain key: /etc/zrepl/prod.key @@ -110,6 +117,7 @@ Serve The ``ca`` field specified the certificate authority used to validate client certificates. The ``client_cns`` list specifies a list of accepted client common names (which are also the client identities for this transport). +The ``listen_freebind`` field is :ref:`explained here `. Connect ~~~~~~~ diff --git a/platformtest/harness/harness.go b/platformtest/harness/harness.go index 6e47816..eb6ac97 100644 --- a/platformtest/harness/harness.go +++ b/platformtest/harness/harness.go @@ -9,6 +9,7 @@ import ( "github.com/fatih/color" "github.com/pkg/errors" + "github.com/zrepl/zrepl/config" "github.com/zrepl/zrepl/daemon/logging" "github.com/zrepl/zrepl/logger" diff --git a/platformtest/platformtest_zpool.go b/platformtest/platformtest_zpool.go index f86f50c..7d2babd 100644 --- a/platformtest/platformtest_zpool.go +++ b/platformtest/platformtest_zpool.go @@ -6,6 +6,7 @@ import ( "path/filepath" "github.com/pkg/errors" + "github.com/zrepl/zrepl/zfs" ) diff --git a/platformtest/tests/undestroyableSnapshotParsing.go b/platformtest/tests/undestroyableSnapshotParsing.go index cbb2d02..857dd85 100644 --- a/platformtest/tests/undestroyableSnapshotParsing.go +++ b/platformtest/tests/undestroyableSnapshotParsing.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/stretchr/testify/require" + "github.com/zrepl/zrepl/platformtest" "github.com/zrepl/zrepl/zfs" ) diff --git a/transport/tcp/serve_tcp.go b/transport/tcp/serve_tcp.go index 45d3851..b9627dd 100644 --- a/transport/tcp/serve_tcp.go +++ b/transport/tcp/serve_tcp.go @@ -8,6 +8,7 @@ import ( "github.com/zrepl/zrepl/config" "github.com/zrepl/zrepl/transport" + "github.com/zrepl/zrepl/util/tcpsock" ) type ipMapEntry struct { @@ -44,16 +45,12 @@ func (m *ipMap) Get(ip net.IP) (string, error) { } func TCPListenerFactoryFromConfig(c *config.Global, in *config.TCPServe) (transport.AuthenticatedListenerFactory, error) { - addr, err := net.ResolveTCPAddr("tcp", in.Listen) - if err != nil { - return nil, errors.Wrap(err, "cannot parse listen address") - } clientMap, err := ipMapFromConfig(in.Clients) if err != nil { return nil, errors.Wrap(err, "cannot parse client IP map") } lf := func() (transport.AuthenticatedListener, error) { - l, err := net.ListenTCP("tcp", addr) + l, err := tcpsock.Listen(in.Listen, in.ListenFreeBind) if err != nil { return nil, err } diff --git a/transport/tls/serve_tls.go b/transport/tls/serve_tls.go index 8b8360b..4c3e913 100644 --- a/transport/tls/serve_tls.go +++ b/transport/tls/serve_tls.go @@ -4,7 +4,6 @@ import ( "context" "crypto/tls" "fmt" - "net" "time" "github.com/pkg/errors" @@ -12,6 +11,7 @@ import ( "github.com/zrepl/zrepl/config" "github.com/zrepl/zrepl/tlsconf" "github.com/zrepl/zrepl/transport" + "github.com/zrepl/zrepl/util/tcpsock" ) type TLSListenerFactory struct{} @@ -45,12 +45,11 @@ func TLSListenerFactoryFromConfig(c *config.Global, in *config.TLSServe) (transp } lf := func() (transport.AuthenticatedListener, error) { - l, err := net.Listen("tcp", address) + l, err := tcpsock.Listen(address, in.ListenFreeBind) if err != nil { return nil, err } - tcpL := l.(*net.TCPListener) - tl := tlsconf.NewClientAuthListener(tcpL, clientCA, serverCert, handshakeTimeout) + tl := tlsconf.NewClientAuthListener(l, clientCA, serverCert, handshakeTimeout) return &tlsAuthListener{tl, clientCNs}, nil } diff --git a/util/tcpsock/tcpsock.go b/util/tcpsock/tcpsock.go new file mode 100644 index 0000000..bf60c7e --- /dev/null +++ b/util/tcpsock/tcpsock.go @@ -0,0 +1,27 @@ +package tcpsock + +import ( + "context" + "net" + "syscall" +) + +func Listen(address string, tryFreeBind bool) (*net.TCPListener, error) { + control := func(network, address string, c syscall.RawConn) error { + if tryFreeBind { + if err := freeBind(network, address, c); err != nil { + return err + } + } + return nil + } + var listenConfig = net.ListenConfig{ + Control: control, + } + + l, err := listenConfig.Listen(context.Background(), "tcp", address) + if err != nil { + return nil, err + } + return l.(*net.TCPListener), nil +} diff --git a/util/tcpsock/tcpsock_freebind_freebsd.go b/util/tcpsock/tcpsock_freebind_freebsd.go new file mode 100644 index 0000000..d1b3d39 --- /dev/null +++ b/util/tcpsock/tcpsock_freebind_freebsd.go @@ -0,0 +1,25 @@ +// +build freebsd + +package tcpsock + +import ( + "fmt" + "syscall" +) + +func freeBind(network, address string, c syscall.RawConn) error { + var err, sockerr error + err = c.Control(func(fd uintptr) { + if network == "tcp6" { + sockerr = syscall.SetsockoptInt(int(fd), syscall.IPPROTO_IPV6, syscall.IPV6_BINDANY, 1) + } else if network == "tcp4" { + sockerr = syscall.SetsockoptInt(int(fd), syscall.IPPROTO_IP, syscall.IP_BINDANY, 1) + } else { + sockerr = fmt.Errorf("expecting 'tcp6' or 'tcp4', got %q", network) + } + }) + if err != nil { + return err + } + return sockerr +} diff --git a/util/tcpsock/tcpsock_freebind_linux.go b/util/tcpsock/tcpsock_freebind_linux.go new file mode 100644 index 0000000..38d6749 --- /dev/null +++ b/util/tcpsock/tcpsock_freebind_linux.go @@ -0,0 +1,19 @@ +// +build linux + +package tcpsock + +import ( + "syscall" +) + +func freeBind(network, address string, c syscall.RawConn) error { + var err, sockerr error + err = c.Control(func(fd uintptr) { + // apparently, this works for both IPv4 and IPv6 + sockerr = syscall.SetsockoptInt(int(fd), syscall.SOL_IP, syscall.IP_FREEBIND, 1) + }) + if err != nil { + return err + } + return sockerr +} diff --git a/util/tcpsock/tcpsock_freebind_unsupported.go b/util/tcpsock/tcpsock_freebind_unsupported.go new file mode 100644 index 0000000..a015800 --- /dev/null +++ b/util/tcpsock/tcpsock_freebind_unsupported.go @@ -0,0 +1,12 @@ +// +build !linux,!freebsd + +package tcpsock + +import ( + "fmt" + "syscall" +) + +func freeBind(network, address string, c syscall.RawConn) error { + return fmt.Errorf("IP_FREEBIND equivalent functionality not supported on this platform") +} diff --git a/zfs/zfs.go b/zfs/zfs.go index 1cba141..70a1d54 100644 --- a/zfs/zfs.go +++ b/zfs/zfs.go @@ -20,6 +20,7 @@ import ( "github.com/prometheus/client_golang/prometheus" "github.com/pkg/errors" + "github.com/zrepl/zrepl/util/envconst" )