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:
Juergen Hoetzel 2019-12-30 19:42:17 +01:00 committed by Christian Schwarz
parent 47ed599db7
commit d35e2400b2
16 changed files with 123 additions and 20 deletions

View File

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

View File

@ -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

View File

@ -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

View File

@ -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
-----

View File

@ -14,6 +14,7 @@ Prometheus & Grafana
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_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**.
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:
- type: prometheus
listen: ':9091'
listen_freebind: true # optional, default false

View File

@ -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 <listen-freebind-explanation>`.
Connect
~~~~~~~

View File

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

View File

@ -6,6 +6,7 @@ import (
"path/filepath"
"github.com/pkg/errors"
"github.com/zrepl/zrepl/zfs"
)

View File

@ -4,6 +4,7 @@ import (
"fmt"
"github.com/stretchr/testify/require"
"github.com/zrepl/zrepl/platformtest"
"github.com/zrepl/zrepl/zfs"
)

View File

@ -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
}

View File

@ -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
}

27
util/tcpsock/tcpsock.go Normal file
View 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
}

View 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
}

View 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
}

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

View File

@ -20,6 +20,7 @@ import (
"github.com/prometheus/client_golang/prometheus"
"github.com/pkg/errors"
"github.com/zrepl/zrepl/util/envconst"
)