move implementation to internal/ directory (#828)
This commit is contained in:
committed by
GitHub
parent
b9b9ad10cf
commit
908807bd59
@@ -0,0 +1,30 @@
|
||||
package tcp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
|
||||
"github.com/zrepl/zrepl/internal/config"
|
||||
"github.com/zrepl/zrepl/internal/transport"
|
||||
)
|
||||
|
||||
type TCPConnecter struct {
|
||||
Address string
|
||||
dialer net.Dialer
|
||||
}
|
||||
|
||||
func TCPConnecterFromConfig(in *config.TCPConnect) (*TCPConnecter, error) {
|
||||
dialer := net.Dialer{
|
||||
Timeout: in.DialTimeout,
|
||||
}
|
||||
|
||||
return &TCPConnecter{in.Address, dialer}, nil
|
||||
}
|
||||
|
||||
func (c *TCPConnecter) Connect(dialCtx context.Context) (transport.Wire, error) {
|
||||
conn, err := c.dialer.DialContext(dialCtx, "tcp", c.Address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return conn.(*net.TCPConn), nil
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package tcp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/zrepl/zrepl/internal/config"
|
||||
"github.com/zrepl/zrepl/internal/transport"
|
||||
"github.com/zrepl/zrepl/internal/util/tcpsock"
|
||||
)
|
||||
|
||||
func TCPListenerFactoryFromConfig(c *config.Global, in *config.TCPServe) (transport.AuthenticatedListenerFactory, error) {
|
||||
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 := tcpsock.Listen(in.Listen, in.ListenFreeBind)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &TCPAuthListener{l, clientMap}, nil
|
||||
}
|
||||
return lf, nil
|
||||
}
|
||||
|
||||
type TCPAuthListener struct {
|
||||
*net.TCPListener
|
||||
clientMap *ipMap
|
||||
}
|
||||
|
||||
func (f *TCPAuthListener) Accept(ctx context.Context) (*transport.AuthConn, error) {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
cancel()
|
||||
}()
|
||||
nc, err := f.TCPListener.AcceptTCP()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
clientAddr := &net.IPAddr{
|
||||
IP: nc.RemoteAddr().(*net.TCPAddr).IP,
|
||||
Zone: nc.RemoteAddr().(*net.TCPAddr).Zone,
|
||||
}
|
||||
clientIdent, err := f.clientMap.Get(clientAddr)
|
||||
if err != nil {
|
||||
transport.GetLogger(ctx).WithField("ipaddr", clientAddr).Error("client IP not in client map")
|
||||
nc.Close()
|
||||
return nil, err
|
||||
}
|
||||
return transport.NewAuthConn(nc, clientIdent), nil
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
package tcp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/sys/unix"
|
||||
|
||||
"github.com/zrepl/zrepl/internal/transport"
|
||||
)
|
||||
|
||||
type ipMapEntry struct {
|
||||
subnet *net.IPNet
|
||||
// ident is always not empty
|
||||
ident string
|
||||
// zone may be empty (e.g. for IPv4)
|
||||
zone string
|
||||
}
|
||||
|
||||
type ipMap struct {
|
||||
entries []*ipMapEntry
|
||||
}
|
||||
|
||||
func ipMapFromConfig(clients map[string]string) (*ipMap, error) {
|
||||
|
||||
entries := make([]*ipMapEntry, 0, len(clients))
|
||||
for clientInput, clientIdent := range clients {
|
||||
userIPMapEntry, err := newIPMapEntry(clientInput, clientIdent)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "cannot not parse %q:%q", clientInput, clientIdent)
|
||||
}
|
||||
|
||||
entries = append(entries, userIPMapEntry)
|
||||
}
|
||||
|
||||
sort.Sort(byPrefixlen(entries))
|
||||
|
||||
return &ipMap{entries: entries}, nil
|
||||
}
|
||||
|
||||
func (m *ipMap) Get(ipAddr *net.IPAddr) (string, error) {
|
||||
for _, e := range m.entries {
|
||||
if e.zone != ipAddr.Zone {
|
||||
continue
|
||||
}
|
||||
if e.subnet.Contains(ipAddr.IP) {
|
||||
return zfsDatasetPathComponentCompatibleRepresentation(e.ident, ipAddr), nil
|
||||
}
|
||||
}
|
||||
return "", errors.Errorf("no identity mapping for client IP: %s%%%s", ipAddr.IP, ipAddr.Zone)
|
||||
}
|
||||
|
||||
var ipv6FullySpecifiedMask = bytes.Repeat([]byte{0xff}, net.IPv6len)
|
||||
|
||||
func newIPMapEntry(input string, ident string) (*ipMapEntry, error) {
|
||||
ip := input
|
||||
var zone string
|
||||
|
||||
ipZoneSplit := strings.SplitN(input, "%", 2)
|
||||
if len(ipZoneSplit) > 1 {
|
||||
ip = ipZoneSplit[0]
|
||||
zone = ipZoneSplit[1]
|
||||
}
|
||||
|
||||
_, subnet, err := net.ParseCIDR(ip)
|
||||
if err != nil {
|
||||
// expect full IP, no '*' placeholder expansion
|
||||
|
||||
if strings.Count(ident, "*") != 0 {
|
||||
return nil, fmt.Errorf("non-CIDR matches must not contain '*' placeholder")
|
||||
}
|
||||
|
||||
if err := transport.ValidateClientIdentity(ident); err != nil {
|
||||
return nil, errors.Wrapf(err, "invalid client identity %q for IP %q", ident, ip)
|
||||
}
|
||||
|
||||
parsedIP := net.ParseIP(ip)
|
||||
if parsedIP == nil {
|
||||
return nil, errors.Wrapf(err, "invalid client address %q", ip)
|
||||
}
|
||||
parsedIP = parsedIP.To16()
|
||||
|
||||
return &ipMapEntry{
|
||||
subnet: &net.IPNet{
|
||||
IP: parsedIP,
|
||||
Mask: ipv6FullySpecifiedMask,
|
||||
},
|
||||
zone: zone,
|
||||
ident: ident,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// expect CIDR and '*' placeholder expansion
|
||||
|
||||
if strings.Count(ident, "*") != 1 {
|
||||
err = fmt.Errorf("CIDRs require 1 IP placeholder")
|
||||
return nil, errors.Wrapf(err, "invalid client identity %q for IP %q", ident, ip)
|
||||
}
|
||||
|
||||
longestIPAddr := net.IPAddr{
|
||||
IP: net.IP(bytes.Repeat([]byte{0xff}, net.IPv6len)),
|
||||
Zone: strings.Repeat("i", unix.IFNAMSIZ),
|
||||
}
|
||||
longestIdent := zfsDatasetPathComponentCompatibleRepresentation(ident, &longestIPAddr)
|
||||
|
||||
if err := transport.ValidateClientIdentity(longestIdent); err != nil {
|
||||
return nil, errors.Wrapf(err, "invalid client identity for IP %q", ip)
|
||||
}
|
||||
|
||||
ones, _ := subnet.Mask.Size()
|
||||
preExpansionAddrlen := len(subnet.IP) * 8
|
||||
expanded := subnet.IP.To16()
|
||||
postExpansionAddrlen := len(expanded) * 8
|
||||
return &ipMapEntry{
|
||||
subnet: &net.IPNet{
|
||||
IP: expanded,
|
||||
Mask: net.CIDRMask(postExpansionAddrlen-preExpansionAddrlen+ones, postExpansionAddrlen),
|
||||
},
|
||||
zone: zone,
|
||||
ident: ident,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func zfsDatasetPathComponentCompatibleRepresentation(identityWithWildcard string, addr *net.IPAddr) string {
|
||||
// If a Zone exists we append it after the IP using a "-" because "%"
|
||||
// is a zfs dataset forbidden char.
|
||||
if addr.Zone != "" {
|
||||
return strings.Replace(identityWithWildcard, "*", addr.IP.String()+"-"+addr.Zone, 1)
|
||||
}
|
||||
// newIPMapEntry validates that the line contains exactly one "*"
|
||||
return strings.Replace(identityWithWildcard, "*", addr.IP.String(), 1)
|
||||
}
|
||||
|
||||
func (e *ipMapEntry) String() string {
|
||||
return fmt.Sprintf("&ipMapEntry{subnet=%q, ident=%q, zone=%q}", e.subnet.String(), e.ident, e.zone)
|
||||
}
|
||||
|
||||
func (e *ipMapEntry) PrefixLen() int {
|
||||
ones, bits := e.subnet.Mask.Size()
|
||||
if bits != net.IPv6len*8 {
|
||||
panic(fmt.Sprintf("impl error: we represent all addresses as 16byte internally ones=%v bits=%v: %s", ones, bits, e))
|
||||
}
|
||||
return ones
|
||||
}
|
||||
|
||||
// newtype to support sorting by prefixlength
|
||||
type byPrefixlen []*ipMapEntry
|
||||
|
||||
func (m byPrefixlen) Len() int { return len(m) }
|
||||
func (m byPrefixlen) Less(i, j int) bool {
|
||||
|
||||
if m[i].PrefixLen() != m[j].PrefixLen() {
|
||||
return m[i].PrefixLen() > m[j].PrefixLen()
|
||||
}
|
||||
|
||||
addrCmp := bytes.Compare(m[i].subnet.IP.To16(), m[j].subnet.IP.To16())
|
||||
if addrCmp != 0 {
|
||||
return addrCmp < 0
|
||||
}
|
||||
|
||||
return strings.Compare(m[i].zone, m[j].zone) < 0
|
||||
}
|
||||
func (m byPrefixlen) Swap(i, j int) { m[i], m[j] = m[j], m[i] }
|
||||
@@ -0,0 +1,202 @@
|
||||
package tcp
|
||||
|
||||
import (
|
||||
"net"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/kr/pretty"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestIPMap(t *testing.T) {
|
||||
type testCaseExpect struct {
|
||||
expectNoMapping bool
|
||||
expectIdent string
|
||||
}
|
||||
type testCase struct {
|
||||
name string
|
||||
config map[string]string
|
||||
expectInitErr bool
|
||||
expect map[string]testCaseExpect
|
||||
}
|
||||
|
||||
cases := []testCase{
|
||||
{
|
||||
name: "regular ips",
|
||||
expectInitErr: false,
|
||||
config: map[string]string{
|
||||
"192.168.123.234": "ident1",
|
||||
"192.168.20.23": "ident2",
|
||||
},
|
||||
expect: map[string]testCaseExpect{
|
||||
"192.168.123.234": {expectIdent: "ident1"},
|
||||
"192.168.20.23": {expectIdent: "ident2"},
|
||||
"192.168.20.10": {expectNoMapping: true},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "full wildcard",
|
||||
expectInitErr: false,
|
||||
config: map[string]string{
|
||||
"0.0.0.0/0": "*",
|
||||
"0::/0": "*",
|
||||
},
|
||||
expect: map[string]testCaseExpect{
|
||||
"10.123.234.24": {expectIdent: "10.123.234.24"},
|
||||
// '[' and ']' are forbiddenin dataset names
|
||||
"fe80::23:42": {expectIdent: "fe80::23:42"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "longest prefix matching",
|
||||
expectInitErr: false,
|
||||
config: map[string]string{
|
||||
"10.1.2.3": "specific-host",
|
||||
"10.1.2.0/24": "subnet-one-two-*",
|
||||
"10.1.1.0/24": "subnet-one-one-*",
|
||||
"10.1.0.0/24": "subnet-one-zero-*",
|
||||
"10.1.0.0/16": "subnet-one-*",
|
||||
"fde4:8dba:82e1:1::1": "v6-48-1-specialhost",
|
||||
"fde4:8dba:82e1::/48": "v6-48-*",
|
||||
"fde4:8dba:82e1:1::/64": "v6-64-1-*",
|
||||
"fde4:8dba:82e1:2::/64": "v6-64-2-*",
|
||||
},
|
||||
expect: map[string]testCaseExpect{
|
||||
"10.1.2.3": {expectIdent: "specific-host"},
|
||||
"10.1.2.1": {expectIdent: "subnet-one-two-10.1.2.1"},
|
||||
"10.1.1.1": {expectIdent: "subnet-one-one-10.1.1.1"},
|
||||
"10.1.0.23": {expectIdent: "subnet-one-zero-10.1.0.23"},
|
||||
"10.1.3.1": {expectIdent: "subnet-one-10.1.3.1"},
|
||||
"10.2.1.1": {expectNoMapping: true},
|
||||
"fde4:8dba:82e1:1::1": {expectIdent: "v6-48-1-specialhost"},
|
||||
"fde4:8dba:82e1:23::1": {expectIdent: "v6-48-fde4:8dba:82e1:23::1"},
|
||||
"fde4:8dba:82e1:1::2": {expectIdent: "v6-64-1-fde4:8dba:82e1:1::2"},
|
||||
"fde4:8dba:82e1:2::1": {expectIdent: "v6-64-2-fde4:8dba:82e1:2::1"},
|
||||
"fde4:8dba:82e2::1": {expectNoMapping: true},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "different prefixes, mixed ipv4 ipv6, with interface ids",
|
||||
expectInitErr: false,
|
||||
config: map[string]string{
|
||||
"192.168.23.0/24": "db-*",
|
||||
"192.168.23.23": "db-twentythree",
|
||||
"192.168.42.0/24": "web-*",
|
||||
"10.1.4.0/24": "my-*-server",
|
||||
"2001:0db8:85a3:0000:0000:8a2e:0370:7334": "aspecifichost",
|
||||
"2001:0db8:85a3:0000:0000:8a2e:0370:7334%eth1": "aspecifichost",
|
||||
"fe80::/16%eth1": "san-*",
|
||||
"fde4:8dba:82e1::/64": "sub64-*",
|
||||
},
|
||||
expect: map[string]testCaseExpect{
|
||||
"10.1.2.3": {expectNoMapping: true},
|
||||
"192.168.23.1": {expectIdent: "db-192.168.23.1"},
|
||||
"192.168.42.1": {expectIdent: "web-192.168.42.1"},
|
||||
"192.168.23.23": {expectIdent: "db-twentythree"},
|
||||
"10.1.4.5": {expectIdent: "my-10.1.4.5-server"},
|
||||
|
||||
// v6 matching
|
||||
"fe80::23:42%eth1": {expectIdent: "san-fe80::23:42-eth1"},
|
||||
"fe80::23:42%eth2": {expectNoMapping: true},
|
||||
|
||||
// v6 subnet matching
|
||||
"fde4:8dba:82e1::1": {expectIdent: "sub64-fde4:8dba:82e1::1"},
|
||||
// v6 subnet matching with suffix that matches another allowed IPv4
|
||||
"fde4:8dba:82e1::c0a8:1717": {expectIdent: "sub64-fde4:8dba:82e1::c0a8:1717"},
|
||||
|
||||
"2001:0db8:85a3:0000:0000:8a2e:0370:7334": {expectIdent: "aspecifichost"},
|
||||
"2001:0db8:85a3:0000:0000:8a2e:0370:7334%eth1": {expectIdent: "aspecifichost"},
|
||||
"2001:0db8:85a3:0000:0000:8a2e:0370:7334%eth2": {expectNoMapping: true},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid user input: non ip or cidr",
|
||||
expectInitErr: true,
|
||||
config: map[string]string{
|
||||
"jimmy": "db",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid user input: v4 ip with an identity containing *",
|
||||
expectInitErr: true,
|
||||
config: map[string]string{
|
||||
"192.168.1.2": "db-*",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid user input: v4 subnet without an identity containing *",
|
||||
expectInitErr: true,
|
||||
config: map[string]string{
|
||||
"192.168.1.0/24": "db-",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid user input: v6 ip with an identity containing *",
|
||||
expectInitErr: true,
|
||||
config: map[string]string{
|
||||
"2001:0db8:85a3:0000:0000:8a2e:0370:7334": "aspecifichost*",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid user input: v6 subnet without identity containing *",
|
||||
expectInitErr: true,
|
||||
config: map[string]string{
|
||||
"fe80::/16%eth1": "db-",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid user input with subnet match: client identity with forbidden zfs dataset name char @",
|
||||
expectInitErr: true,
|
||||
config: map[string]string{
|
||||
"fe80::/16": "db@-*",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid user input with IP match: client identity with forbidden zfs dataset name char @",
|
||||
expectInitErr: true,
|
||||
config: map[string]string{
|
||||
"fe80::1": "db@foo",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for i := range cases {
|
||||
c := cases[i]
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
pretty.Fprintf(os.Stderr, "running %#v\n", c)
|
||||
m, err := ipMapFromConfig(c.config)
|
||||
if c.expectInitErr {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, m)
|
||||
}
|
||||
for input, expect := range c.expect {
|
||||
// reuse newIPMapEntry to parse test case input
|
||||
// "test" is not used during testing but must not be empty.
|
||||
ipMapEntry, err := newIPMapEntry(input, "test")
|
||||
require.NoError(t, err)
|
||||
ones, bits := ipMapEntry.subnet.Mask.Size()
|
||||
require.Equal(t, bits, net.IPv6len*8, "and we know ipMapEntry always expands its IPs to 16bytes")
|
||||
require.Equal(t, ones, net.IPv6len*8, "test case addresses must be fully specified")
|
||||
require.NotNil(t, ipMapEntry)
|
||||
ident, err := m.Get(&net.IPAddr{
|
||||
IP: ipMapEntry.subnet.IP,
|
||||
Zone: ipMapEntry.zone,
|
||||
})
|
||||
if expect.expectNoMapping {
|
||||
assert.Empty(t, ident)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expect.expectIdent, ident)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPackageNetAssumptions(t *testing.T) {
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user