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"
)