mirror of
https://github.com/tailscale/tailscale.git
synced 2024-12-01 22:15:51 +00:00
net/dns: make exit node DNS ask OSConfigurator for backup resolvers
Updates #1713 Change-Id: I7be9dab2b2c03749b4c2d99f9f45c11422ac915a Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
parent
c2efe46f72
commit
4c7ee0c9f9
@ -18,6 +18,7 @@
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"tailscale.com/atomicfile"
|
"tailscale.com/atomicfile"
|
||||||
|
"tailscale.com/types/dnstype"
|
||||||
"tailscale.com/types/logger"
|
"tailscale.com/types/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -173,6 +174,10 @@ func (m *resolvconfManager) GetBaseConfig() (OSConfig, error) {
|
|||||||
return readResolv(&conf)
|
return readResolv(&conf)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *resolvconfManager) GetExitNodeForwardResolver() ([]dnstype.Resolver, error) {
|
||||||
|
return getExitNodeForwardResolverFromBaseConfig(m)
|
||||||
|
}
|
||||||
|
|
||||||
func (m *resolvconfManager) Close() error {
|
func (m *resolvconfManager) Close() error {
|
||||||
if err := m.deleteTailscaleConfig(); err != nil {
|
if err := m.deleteTailscaleConfig(); err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"inet.af/netaddr"
|
"inet.af/netaddr"
|
||||||
|
"tailscale.com/types/dnstype"
|
||||||
"tailscale.com/types/logger"
|
"tailscale.com/types/logger"
|
||||||
"tailscale.com/util/dnsname"
|
"tailscale.com/util/dnsname"
|
||||||
)
|
)
|
||||||
@ -183,6 +184,24 @@ func (m *directManager) readResolvFile(path string) (OSConfig, error) {
|
|||||||
return readResolv(bytes.NewReader(b))
|
return readResolv(bytes.NewReader(b))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *directManager) GetExitNodeForwardResolver() (ret []dnstype.Resolver, retErr error) {
|
||||||
|
for _, filename := range []string{backupConf, resolvConf} {
|
||||||
|
if oc, err := m.readResolvFile(filename); err == nil {
|
||||||
|
for _, ip := range oc.Nameservers {
|
||||||
|
if ip != netaddr.IPv4(100, 100, 100, 100) {
|
||||||
|
ret = append(ret, dnstype.Resolver{Addr: netaddr.IPPortFrom(ip, 53).String()})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(ret) > 0 {
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
|
} else if !os.IsNotExist(err) && retErr == nil {
|
||||||
|
retErr = err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, retErr
|
||||||
|
}
|
||||||
|
|
||||||
// ownedByTailscale reports whether /etc/resolv.conf seems to be a
|
// ownedByTailscale reports whether /etc/resolv.conf seems to be a
|
||||||
// tailscale-managed file.
|
// tailscale-managed file.
|
||||||
func (m *directManager) ownedByTailscale() (bool, error) {
|
func (m *directManager) ownedByTailscale() (bool, error) {
|
||||||
|
@ -62,6 +62,11 @@ func (m *Manager) Set(cfg Config) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
exitNodeBackupResolvers, err := m.os.GetExitNodeForwardResolver()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
rcfg.ExitNodeBackupResolvers = exitNodeBackupResolvers
|
||||||
|
|
||||||
m.logf("Resolvercfg: %v", logger.ArgWriter(func(w *bufio.Writer) {
|
m.logf("Resolvercfg: %v", logger.ArgWriter(func(w *bufio.Writer) {
|
||||||
rcfg.WriteToBufioWriter(w)
|
rcfg.WriteToBufioWriter(w)
|
||||||
|
@ -45,6 +45,10 @@ func (c *fakeOSConfigurator) GetBaseConfig() (OSConfig, error) {
|
|||||||
return c.BaseConfig, nil
|
return c.BaseConfig, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *fakeOSConfigurator) GetExitNodeForwardResolver() ([]dnstype.Resolver, error) {
|
||||||
|
return getExitNodeForwardResolverFromBaseConfig(c)
|
||||||
|
}
|
||||||
|
|
||||||
func (c *fakeOSConfigurator) Close() error { return nil }
|
func (c *fakeOSConfigurator) Close() error { return nil }
|
||||||
|
|
||||||
func TestManager(t *testing.T) {
|
func TestManager(t *testing.T) {
|
||||||
@ -213,6 +217,7 @@ func TestManager(t *testing.T) {
|
|||||||
Routes: upstreams(
|
Routes: upstreams(
|
||||||
".", "8.8.8.8:53",
|
".", "8.8.8.8:53",
|
||||||
"corp.com.", "2.2.2.2:53"),
|
"corp.com.", "2.2.2.2:53"),
|
||||||
|
ExitNodeBackupResolvers: []dnstype.Resolver{{Addr: "8.8.8.8:53"}},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -249,6 +254,7 @@ func TestManager(t *testing.T) {
|
|||||||
".", "8.8.8.8:53",
|
".", "8.8.8.8:53",
|
||||||
"corp.com.", "2.2.2.2:53",
|
"corp.com.", "2.2.2.2:53",
|
||||||
"bigco.net.", "3.3.3.3:53"),
|
"bigco.net.", "3.3.3.3:53"),
|
||||||
|
ExitNodeBackupResolvers: []dnstype.Resolver{{Addr: "8.8.8.8:53"}},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -293,7 +299,8 @@ func TestManager(t *testing.T) {
|
|||||||
Hosts: hosts(
|
Hosts: hosts(
|
||||||
"dave.ts.com.", "1.2.3.4",
|
"dave.ts.com.", "1.2.3.4",
|
||||||
"bradfitz.ts.com.", "2.3.4.5"),
|
"bradfitz.ts.com.", "2.3.4.5"),
|
||||||
LocalDomains: fqdns("ts.com."),
|
LocalDomains: fqdns("ts.com."),
|
||||||
|
ExitNodeBackupResolvers: []dnstype.Resolver{{Addr: "8.8.8.8:53"}},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -342,7 +349,8 @@ func TestManager(t *testing.T) {
|
|||||||
Hosts: hosts(
|
Hosts: hosts(
|
||||||
"dave.ts.com.", "1.2.3.4",
|
"dave.ts.com.", "1.2.3.4",
|
||||||
"bradfitz.ts.com.", "2.3.4.5"),
|
"bradfitz.ts.com.", "2.3.4.5"),
|
||||||
LocalDomains: fqdns("ts.com."),
|
LocalDomains: fqdns("ts.com."),
|
||||||
|
ExitNodeBackupResolvers: []dnstype.Resolver{{Addr: "8.8.8.8:53"}},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
"golang.org/x/sys/windows/registry"
|
"golang.org/x/sys/windows/registry"
|
||||||
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
|
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
|
||||||
"inet.af/netaddr"
|
"inet.af/netaddr"
|
||||||
|
"tailscale.com/types/dnstype"
|
||||||
"tailscale.com/types/logger"
|
"tailscale.com/types/logger"
|
||||||
"tailscale.com/util/dnsname"
|
"tailscale.com/util/dnsname"
|
||||||
)
|
)
|
||||||
@ -340,6 +341,10 @@ func (m windowsManager) GetBaseConfig() (OSConfig, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m windowsManager) GetExitNodeForwardResolver() ([]dnstype.Resolver, error) {
|
||||||
|
return getExitNodeForwardResolverFromBaseConfig(m)
|
||||||
|
}
|
||||||
|
|
||||||
// getBasePrimaryResolver returns a guess of the non-Tailscale primary
|
// getBasePrimaryResolver returns a guess of the non-Tailscale primary
|
||||||
// resolver on the system.
|
// resolver on the system.
|
||||||
// It's used on Windows 7 to emulate split DNS by trying to figure out
|
// It's used on Windows 7 to emulate split DNS by trying to figure out
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
"github.com/godbus/dbus/v5"
|
"github.com/godbus/dbus/v5"
|
||||||
"inet.af/netaddr"
|
"inet.af/netaddr"
|
||||||
"tailscale.com/net/interfaces"
|
"tailscale.com/net/interfaces"
|
||||||
|
"tailscale.com/types/dnstype"
|
||||||
"tailscale.com/util/dnsname"
|
"tailscale.com/util/dnsname"
|
||||||
"tailscale.com/util/endian"
|
"tailscale.com/util/endian"
|
||||||
)
|
)
|
||||||
@ -374,6 +375,10 @@ type dnsPrio struct {
|
|||||||
return ret, nil
|
return ret, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *nmManager) GetExitNodeForwardResolver() ([]dnstype.Resolver, error) {
|
||||||
|
return getExitNodeForwardResolverFromBaseConfig(m)
|
||||||
|
}
|
||||||
|
|
||||||
func (m *nmManager) Close() error {
|
func (m *nmManager) Close() error {
|
||||||
// No need to do anything on close, NetworkManager will delete our
|
// No need to do anything on close, NetworkManager will delete our
|
||||||
// settings when the tailscale interface goes away.
|
// settings when the tailscale interface goes away.
|
||||||
|
@ -4,14 +4,21 @@
|
|||||||
|
|
||||||
package dns
|
package dns
|
||||||
|
|
||||||
|
import "tailscale.com/types/dnstype"
|
||||||
|
|
||||||
type noopManager struct{}
|
type noopManager struct{}
|
||||||
|
|
||||||
|
var _ OSConfigurator = noopManager{}
|
||||||
|
|
||||||
func (m noopManager) SetDNS(OSConfig) error { return nil }
|
func (m noopManager) SetDNS(OSConfig) error { return nil }
|
||||||
func (m noopManager) SupportsSplitDNS() bool { return false }
|
func (m noopManager) SupportsSplitDNS() bool { return false }
|
||||||
func (m noopManager) Close() error { return nil }
|
func (m noopManager) Close() error { return nil }
|
||||||
func (m noopManager) GetBaseConfig() (OSConfig, error) {
|
func (m noopManager) GetBaseConfig() (OSConfig, error) {
|
||||||
return OSConfig{}, ErrGetBaseConfigNotSupported
|
return OSConfig{}, ErrGetBaseConfigNotSupported
|
||||||
}
|
}
|
||||||
|
func (m noopManager) GetExitNodeForwardResolver() ([]dnstype.Resolver, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
func NewNoopManager() (noopManager, error) {
|
func NewNoopManager() (noopManager, error) {
|
||||||
return noopManager{}, nil
|
return noopManager{}, nil
|
||||||
|
@ -12,6 +12,8 @@
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"tailscale.com/types/dnstype"
|
||||||
)
|
)
|
||||||
|
|
||||||
// openresolvManager manages DNS configuration using the openresolv
|
// openresolvManager manages DNS configuration using the openresolv
|
||||||
@ -90,6 +92,10 @@ func (m openresolvManager) GetBaseConfig() (OSConfig, error) {
|
|||||||
return readResolv(&buf)
|
return readResolv(&buf)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m openresolvManager) GetExitNodeForwardResolver() ([]dnstype.Resolver, error) {
|
||||||
|
return getExitNodeForwardResolverFromBaseConfig(m)
|
||||||
|
}
|
||||||
|
|
||||||
func (m openresolvManager) Close() error {
|
func (m openresolvManager) Close() error {
|
||||||
return m.deleteTailscaleConfig()
|
return m.deleteTailscaleConfig()
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
"inet.af/netaddr"
|
"inet.af/netaddr"
|
||||||
|
"tailscale.com/types/dnstype"
|
||||||
"tailscale.com/util/dnsname"
|
"tailscale.com/util/dnsname"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -18,20 +19,35 @@ type OSConfigurator interface {
|
|||||||
// configuration is removed.
|
// configuration is removed.
|
||||||
// SetDNS must not be called after Close.
|
// SetDNS must not be called after Close.
|
||||||
SetDNS(cfg OSConfig) error
|
SetDNS(cfg OSConfig) error
|
||||||
|
|
||||||
// SupportsSplitDNS reports whether the configurator is capable of
|
// SupportsSplitDNS reports whether the configurator is capable of
|
||||||
// installing a resolver only for specific DNS suffixes. If false,
|
// installing a resolver only for specific DNS suffixes. If false,
|
||||||
// the configurator can only set a global resolver.
|
// the configurator can only set a global resolver.
|
||||||
SupportsSplitDNS() bool
|
SupportsSplitDNS() bool
|
||||||
|
|
||||||
// GetBaseConfig returns the OS's "base" configuration, i.e. the
|
// GetBaseConfig returns the OS's "base" configuration, i.e. the
|
||||||
// resolver settings the OS would use without Tailscale
|
// resolver settings the OS would use without Tailscale
|
||||||
// contributing any configuration.
|
// contributing any configuration.
|
||||||
// GetBaseConfig must return the tailscale-free base config even
|
// GetBaseConfig must return the tailscale-free base config even
|
||||||
// after SetDNS has been called to set a Tailscale configuration.
|
// after SetDNS has been called to set a Tailscale configuration.
|
||||||
// Only works when SupportsSplitDNS=false.
|
// Only works when SupportsSplitDNS=false.
|
||||||
|
//
|
||||||
// Implementations that don't support getting the base config must
|
// Implementations that don't support getting the base config must
|
||||||
// return ErrGetBaseConfigNotSupported.
|
// return ErrGetBaseConfigNotSupported.
|
||||||
GetBaseConfig() (OSConfig, error)
|
GetBaseConfig() (OSConfig, error)
|
||||||
|
|
||||||
|
// GetExitNodeForwardResolver returns the resolver(s) that should
|
||||||
|
// be used as a fallback for the exit node's DNS-over-HTTP peerapi
|
||||||
|
// to send DNS queries from peers on to, in the case where the tailnet
|
||||||
|
// doesn't have global DNS servers configured.
|
||||||
|
//
|
||||||
|
// For example, on Linux with systemd-resolved, this will
|
||||||
|
// return 127.0.0.53:53.
|
||||||
|
//
|
||||||
|
// On other systems, it'll usually be the value of
|
||||||
|
// GetBaseConfig.Nameservers.
|
||||||
|
GetExitNodeForwardResolver() ([]dnstype.Resolver, error)
|
||||||
|
|
||||||
// Close removes Tailscale-related DNS configuration from the OS.
|
// Close removes Tailscale-related DNS configuration from the OS.
|
||||||
Close() error
|
Close() error
|
||||||
}
|
}
|
||||||
@ -90,3 +106,16 @@ func (a OSConfig) Equal(b OSConfig) bool {
|
|||||||
// OSConfigurator.GetBaseConfig returns when the OSConfigurator
|
// OSConfigurator.GetBaseConfig returns when the OSConfigurator
|
||||||
// doesn't support reading the underlying configuration out of the OS.
|
// doesn't support reading the underlying configuration out of the OS.
|
||||||
var ErrGetBaseConfigNotSupported = errors.New("getting OS base config is not supported")
|
var ErrGetBaseConfigNotSupported = errors.New("getting OS base config is not supported")
|
||||||
|
|
||||||
|
func getExitNodeForwardResolverFromBaseConfig(o OSConfigurator) (ret []dnstype.Resolver, retErr error) {
|
||||||
|
oc, err := o.GetBaseConfig()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, ip := range oc.Nameservers {
|
||||||
|
if ip != netaddr.IPv4(100, 100, 100, 100) {
|
||||||
|
ret = append(ret, dnstype.Resolver{Addr: netaddr.IPPortFrom(ip, 53).String()})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
"golang.org/x/sys/unix"
|
"golang.org/x/sys/unix"
|
||||||
"inet.af/netaddr"
|
"inet.af/netaddr"
|
||||||
"tailscale.com/health"
|
"tailscale.com/health"
|
||||||
|
"tailscale.com/types/dnstype"
|
||||||
"tailscale.com/types/logger"
|
"tailscale.com/types/logger"
|
||||||
"tailscale.com/util/dnsname"
|
"tailscale.com/util/dnsname"
|
||||||
)
|
)
|
||||||
@ -332,6 +333,10 @@ func (m *resolvedManager) GetBaseConfig() (OSConfig, error) {
|
|||||||
return OSConfig{}, ErrGetBaseConfigNotSupported
|
return OSConfig{}, ErrGetBaseConfigNotSupported
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *resolvedManager) GetExitNodeForwardResolver() ([]dnstype.Resolver, error) {
|
||||||
|
return []dnstype.Resolver{{Addr: "127.0.0.53:53"}}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (m *resolvedManager) Close() error {
|
func (m *resolvedManager) Close() error {
|
||||||
m.cancelSyncer()
|
m.cancelSyncer()
|
||||||
|
|
||||||
|
@ -83,6 +83,14 @@ type Config struct {
|
|||||||
// LocalDomains is a list of DNS name suffixes that should not be
|
// LocalDomains is a list of DNS name suffixes that should not be
|
||||||
// routed to upstream resolvers.
|
// routed to upstream resolvers.
|
||||||
LocalDomains []dnsname.FQDN
|
LocalDomains []dnsname.FQDN
|
||||||
|
// ExitNodeBackupResolvers are where the local node when
|
||||||
|
// acting as an exit node and serving a DNS proxy should
|
||||||
|
// forward DNS requests to in the case where there are no
|
||||||
|
// routes found. For example, for Linux systemd-resolved
|
||||||
|
// machines this is likely 127.0.0.53:53.
|
||||||
|
// If it's empty, there are no backups and the OS should
|
||||||
|
// be queried directly using its OS-level DNS APIs.
|
||||||
|
ExitNodeBackupResolvers []dnstype.Resolver
|
||||||
}
|
}
|
||||||
|
|
||||||
// WriteToBufioWriter write a debug version of c for logs to w, omitting
|
// WriteToBufioWriter write a debug version of c for logs to w, omitting
|
||||||
@ -202,10 +210,11 @@ type Resolver struct {
|
|||||||
wg sync.WaitGroup
|
wg sync.WaitGroup
|
||||||
|
|
||||||
// mu guards the following fields from being updated while used.
|
// mu guards the following fields from being updated while used.
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
localDomains []dnsname.FQDN
|
localDomains []dnsname.FQDN
|
||||||
hostToIP map[dnsname.FQDN][]netaddr.IP
|
hostToIP map[dnsname.FQDN][]netaddr.IP
|
||||||
ipToHost map[netaddr.IP]dnsname.FQDN
|
ipToHost map[netaddr.IP]dnsname.FQDN
|
||||||
|
exitNodeBackupResolvers []dnstype.Resolver
|
||||||
}
|
}
|
||||||
|
|
||||||
type ForwardLinkSelector interface {
|
type ForwardLinkSelector interface {
|
||||||
@ -253,9 +262,16 @@ func (r *Resolver) SetConfig(cfg Config) error {
|
|||||||
r.localDomains = cfg.LocalDomains
|
r.localDomains = cfg.LocalDomains
|
||||||
r.hostToIP = cfg.Hosts
|
r.hostToIP = cfg.Hosts
|
||||||
r.ipToHost = reverse
|
r.ipToHost = reverse
|
||||||
|
r.exitNodeBackupResolvers = append([]dnstype.Resolver(nil), cfg.ExitNodeBackupResolvers...)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Resolver) exitNodeForwardResolvers() []dnstype.Resolver {
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
return r.exitNodeBackupResolvers
|
||||||
|
}
|
||||||
|
|
||||||
// Close shuts down the resolver and ensures poll goroutines have exited.
|
// Close shuts down the resolver and ensures poll goroutines have exited.
|
||||||
// The Resolver cannot be used again after Close is called.
|
// The Resolver cannot be used again after Close is called.
|
||||||
func (r *Resolver) Close() {
|
func (r *Resolver) Close() {
|
||||||
@ -312,34 +328,13 @@ func (r *Resolver) HandleExitNodeDNSQuery(ctx context.Context, q []byte, from ne
|
|||||||
|
|
||||||
err = r.forwarder.forwardWithDestChan(ctx, packet{q, from}, ch)
|
err = r.forwarder.forwardWithDestChan(ctx, packet{q, from}, ch)
|
||||||
if err == errNoUpstreams {
|
if err == errNoUpstreams {
|
||||||
// Handle to the system resolver.
|
backup := r.exitNodeForwardResolvers()
|
||||||
switch runtime.GOOS {
|
if len(backup) > 0 {
|
||||||
case "linux":
|
var extra []resolverAndDelay
|
||||||
// Assume for now that we don't have an upstream because
|
for _, v := range backup {
|
||||||
// they're using systemd-resolved and we're in Split DNS mode
|
extra = append(extra, resolverAndDelay{name: v})
|
||||||
// where we don't know the base config.
|
}
|
||||||
//
|
err = r.forwarder.forwardWithDestChan(ctx, packet{q, from}, ch, extra...)
|
||||||
// TODO(bradfitz): this is a lazy assumption. Do better, and
|
|
||||||
// maybe move the HandleExitNodeDNSQuery method to the dns.Manager
|
|
||||||
// instead? But this works for now.
|
|
||||||
err = r.forwarder.forwardWithDestChan(ctx, packet{q, from}, ch, resolverAndDelay{
|
|
||||||
name: dnstype.Resolver{
|
|
||||||
Addr: "127.0.0.1:53",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
default:
|
|
||||||
// TODO(bradfitz): if we're on an exit node
|
|
||||||
// on, say, Windows, we need to parse the DNS
|
|
||||||
// packet in q and call OS-native APIs for
|
|
||||||
// each question. But we'll want to strip out
|
|
||||||
// questions for MagicDNS names probably, so
|
|
||||||
// they don't loop back into
|
|
||||||
// 100.100.100.100. We don't want to resolve
|
|
||||||
// MagicDNS names across Tailnets once we
|
|
||||||
// permit sharing exit nodes.
|
|
||||||
//
|
|
||||||
// For now, just return an error.
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
Loading…
Reference in New Issue
Block a user