mirror of
https://github.com/zrepl/zrepl.git
synced 2025-06-19 17:27:46 +02:00
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
This commit is contained in:
parent
47ed599db7
commit
d35e2400b2
@ -3,9 +3,9 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
_ "golang.org/x/tools/cmd/stringer"
|
|
||||||
_ "github.com/golang/protobuf/protoc-gen-go"
|
|
||||||
_ "github.com/alvaroloes/enumer"
|
_ "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"
|
_ "github.com/golangci/golangci-lint/cmd/golangci-lint"
|
||||||
|
_ "golang.org/x/tools/cmd/goimports"
|
||||||
|
_ "golang.org/x/tools/cmd/stringer"
|
||||||
)
|
)
|
||||||
|
@ -245,14 +245,16 @@ type ServeCommon struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type TCPServe struct {
|
type TCPServe struct {
|
||||||
ServeCommon `yaml:",inline"`
|
ServeCommon `yaml:",inline"`
|
||||||
Listen string `yaml:"listen,hostport"`
|
Listen string `yaml:"listen,hostport"`
|
||||||
Clients map[string]string `yaml:"clients"`
|
ListenFreeBind bool `yaml:"listen_freebind,default=false"`
|
||||||
|
Clients map[string]string `yaml:"clients"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type TLSServe struct {
|
type TLSServe struct {
|
||||||
ServeCommon `yaml:",inline"`
|
ServeCommon `yaml:",inline"`
|
||||||
Listen string `yaml:"listen,hostport"`
|
Listen string `yaml:"listen,hostport"`
|
||||||
|
ListenFreeBind bool `yaml:"listen_freebind,default=false"`
|
||||||
Ca string `yaml:"ca"`
|
Ca string `yaml:"ca"`
|
||||||
Cert string `yaml:"cert"`
|
Cert string `yaml:"cert"`
|
||||||
Key string `yaml:"key"`
|
Key string `yaml:"key"`
|
||||||
@ -331,8 +333,9 @@ type MonitoringEnum struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type PrometheusMonitoring struct {
|
type PrometheusMonitoring struct {
|
||||||
Type string `yaml:"type"`
|
Type string `yaml:"type"`
|
||||||
Listen string `yaml:"listen,hostport"`
|
Listen string `yaml:"listen,hostport"`
|
||||||
|
ListenFreeBind bool `yaml:"listen_freebind,default=false"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SyslogFacility syslog.Priority
|
type SyslogFacility syslog.Priority
|
||||||
|
@ -12,18 +12,20 @@ import (
|
|||||||
"github.com/zrepl/zrepl/daemon/job"
|
"github.com/zrepl/zrepl/daemon/job"
|
||||||
"github.com/zrepl/zrepl/logger"
|
"github.com/zrepl/zrepl/logger"
|
||||||
"github.com/zrepl/zrepl/rpc/dataconn/frameconn"
|
"github.com/zrepl/zrepl/rpc/dataconn/frameconn"
|
||||||
|
"github.com/zrepl/zrepl/util/tcpsock"
|
||||||
"github.com/zrepl/zrepl/zfs"
|
"github.com/zrepl/zrepl/zfs"
|
||||||
)
|
)
|
||||||
|
|
||||||
type prometheusJob struct {
|
type prometheusJob struct {
|
||||||
listen string
|
listen string
|
||||||
|
freeBind bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func newPrometheusJobFromConfig(in *config.PrometheusMonitoring) (*prometheusJob, error) {
|
func newPrometheusJobFromConfig(in *config.PrometheusMonitoring) (*prometheusJob, error) {
|
||||||
if _, _, err := net.SplitHostPort(in.Listen); err != nil {
|
if _, _, err := net.SplitHostPort(in.Listen); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &prometheusJob{in.Listen}, nil
|
return &prometheusJob{in.Listen, in.ListenFreeBind}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var prom struct {
|
var prom struct {
|
||||||
@ -60,7 +62,7 @@ func (j *prometheusJob) Run(ctx context.Context) {
|
|||||||
|
|
||||||
log := job.GetLogger(ctx)
|
log := job.GetLogger(ctx)
|
||||||
|
|
||||||
l, err := net.Listen("tcp", j.listen)
|
l, err := tcpsock.Listen(j.listen, j.freeBind)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithError(err).Error("cannot listen")
|
log.WithError(err).Error("cannot listen")
|
||||||
return
|
return
|
||||||
|
@ -27,6 +27,11 @@ We use the following annotations for classifying changes:
|
|||||||
* |bugfix| Change that fixes a bug, no regressions or incompatibilities expected.
|
* |bugfix| Change that fixes a bug, no regressions or incompatibilities expected.
|
||||||
* |docs| Change to the documentation.
|
* |docs| Change to the documentation.
|
||||||
|
|
||||||
|
0.2.2 (unreleased)
|
||||||
|
------------------
|
||||||
|
|
||||||
|
* |feature| New option ``listen_freebind`` (tcp, tls, prometheus listener)
|
||||||
|
|
||||||
0.2.1
|
0.2.1
|
||||||
-----
|
-----
|
||||||
|
|
||||||
|
@ -14,6 +14,7 @@ Prometheus & Grafana
|
|||||||
|
|
||||||
zrepl can expose `Prometheus metrics <https://prometheus.io/docs/instrumenting/exposition_formats/>`_ via HTTP.
|
zrepl can expose `Prometheus metrics <https://prometheus.io/docs/instrumenting/exposition_formats/>`_ via HTTP.
|
||||||
The ``listen`` attribute is a `net.Listen <https://golang.org/pkg/net/#Listen>`_ string for tcp, e.g. ``:9091`` or ``127.0.0.1:9091``.
|
The ``listen`` attribute is a `net.Listen <https://golang.org/pkg/net/#Listen>`_ string for tcp, e.g. ``:9091`` or ``127.0.0.1:9091``.
|
||||||
|
The ``listen_freebind`` attribute is :ref:`explained here <listen-freebind-explanation>`.
|
||||||
The Prometheues monitoring job appears in the ``zrepl control`` job list and may be specified **at most once**.
|
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 <https://grafana.com>`_ dashboard that consumes the Prometheus metrics:
|
zrepl also ships with an importable `Grafana <https://grafana.com>`_ dashboard that consumes the Prometheus metrics:
|
||||||
@ -30,6 +31,7 @@ The dashboard also contains some advice on which metrics are important to monito
|
|||||||
monitoring:
|
monitoring:
|
||||||
- type: prometheus
|
- type: prometheus
|
||||||
listen: ':9091'
|
listen: ':9091'
|
||||||
|
listen_freebind: true # optional, default false
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -50,12 +50,18 @@ Serve
|
|||||||
serve:
|
serve:
|
||||||
type: tcp
|
type: tcp
|
||||||
listen: ":8888"
|
listen: ":8888"
|
||||||
|
listen_freebind: true # optional, default false
|
||||||
clients: {
|
clients: {
|
||||||
"192.168.122.123" : "mysql01"
|
"192.168.122.123" : "mysql01"
|
||||||
"192.168.122.123" : "mx01"
|
"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
|
Connect
|
||||||
~~~~~~~
|
~~~~~~~
|
||||||
|
|
||||||
@ -101,6 +107,7 @@ Serve
|
|||||||
serve:
|
serve:
|
||||||
type: tls
|
type: tls
|
||||||
listen: ":8888"
|
listen: ":8888"
|
||||||
|
listen_freebind: true # optional, default false
|
||||||
ca: /etc/zrepl/ca.crt
|
ca: /etc/zrepl/ca.crt
|
||||||
cert: /etc/zrepl/prod.fullchain
|
cert: /etc/zrepl/prod.fullchain
|
||||||
key: /etc/zrepl/prod.key
|
key: /etc/zrepl/prod.key
|
||||||
@ -110,6 +117,7 @@ Serve
|
|||||||
|
|
||||||
The ``ca`` field specified the certificate authority used to validate client certificates.
|
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 ``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 <listen-freebind-explanation>`.
|
||||||
|
|
||||||
Connect
|
Connect
|
||||||
~~~~~~~
|
~~~~~~~
|
||||||
|
@ -9,6 +9,7 @@ import (
|
|||||||
|
|
||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/config"
|
"github.com/zrepl/zrepl/config"
|
||||||
"github.com/zrepl/zrepl/daemon/logging"
|
"github.com/zrepl/zrepl/daemon/logging"
|
||||||
"github.com/zrepl/zrepl/logger"
|
"github.com/zrepl/zrepl/logger"
|
||||||
|
@ -6,6 +6,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/zfs"
|
"github.com/zrepl/zrepl/zfs"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/platformtest"
|
"github.com/zrepl/zrepl/platformtest"
|
||||||
"github.com/zrepl/zrepl/zfs"
|
"github.com/zrepl/zrepl/zfs"
|
||||||
)
|
)
|
||||||
|
@ -8,6 +8,7 @@ import (
|
|||||||
|
|
||||||
"github.com/zrepl/zrepl/config"
|
"github.com/zrepl/zrepl/config"
|
||||||
"github.com/zrepl/zrepl/transport"
|
"github.com/zrepl/zrepl/transport"
|
||||||
|
"github.com/zrepl/zrepl/util/tcpsock"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ipMapEntry struct {
|
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) {
|
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)
|
clientMap, err := ipMapFromConfig(in.Clients)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "cannot parse client IP map")
|
return nil, errors.Wrap(err, "cannot parse client IP map")
|
||||||
}
|
}
|
||||||
lf := func() (transport.AuthenticatedListener, error) {
|
lf := func() (transport.AuthenticatedListener, error) {
|
||||||
l, err := net.ListenTCP("tcp", addr)
|
l, err := tcpsock.Listen(in.Listen, in.ListenFreeBind)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
@ -12,6 +11,7 @@ import (
|
|||||||
"github.com/zrepl/zrepl/config"
|
"github.com/zrepl/zrepl/config"
|
||||||
"github.com/zrepl/zrepl/tlsconf"
|
"github.com/zrepl/zrepl/tlsconf"
|
||||||
"github.com/zrepl/zrepl/transport"
|
"github.com/zrepl/zrepl/transport"
|
||||||
|
"github.com/zrepl/zrepl/util/tcpsock"
|
||||||
)
|
)
|
||||||
|
|
||||||
type TLSListenerFactory struct{}
|
type TLSListenerFactory struct{}
|
||||||
@ -45,12 +45,11 @@ func TLSListenerFactoryFromConfig(c *config.Global, in *config.TLSServe) (transp
|
|||||||
}
|
}
|
||||||
|
|
||||||
lf := func() (transport.AuthenticatedListener, error) {
|
lf := func() (transport.AuthenticatedListener, error) {
|
||||||
l, err := net.Listen("tcp", address)
|
l, err := tcpsock.Listen(address, in.ListenFreeBind)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
tcpL := l.(*net.TCPListener)
|
tl := tlsconf.NewClientAuthListener(l, clientCA, serverCert, handshakeTimeout)
|
||||||
tl := tlsconf.NewClientAuthListener(tcpL, clientCA, serverCert, handshakeTimeout)
|
|
||||||
return &tlsAuthListener{tl, clientCNs}, nil
|
return &tlsAuthListener{tl, clientCNs}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
27
util/tcpsock/tcpsock.go
Normal file
27
util/tcpsock/tcpsock.go
Normal file
@ -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
|
||||||
|
}
|
25
util/tcpsock/tcpsock_freebind_freebsd.go
Normal file
25
util/tcpsock/tcpsock_freebind_freebsd.go
Normal file
@ -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
|
||||||
|
}
|
19
util/tcpsock/tcpsock_freebind_linux.go
Normal file
19
util/tcpsock/tcpsock_freebind_linux.go
Normal file
@ -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
|
||||||
|
}
|
12
util/tcpsock/tcpsock_freebind_unsupported.go
Normal file
12
util/tcpsock/tcpsock_freebind_unsupported.go
Normal file
@ -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")
|
||||||
|
}
|
@ -20,6 +20,7 @@ import (
|
|||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/util/envconst"
|
"github.com/zrepl/zrepl/util/envconst"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user