@ -18,7 +18,11 @@
// previously advertised routes. To accept routes, use TS_EXTRA_ARGS to pass
// previously advertised routes. To accept routes, use TS_EXTRA_ARGS to pass
// in --accept-routes.
// in --accept-routes.
// - TS_DEST_IP: proxy all incoming Tailscale traffic to the given
// - TS_DEST_IP: proxy all incoming Tailscale traffic to the given
// destination.
// destination defined by an IP address.
// - TS_EXPERIMENTAL_DEST_DNS_NAME: proxy all incoming Tailscale traffic to the given
// destination defined by a DNS name. The DNS name will be periodically resolved and firewall rules updated accordingly.
// This is currently intended to be used by the Kubernetes operator (ExternalName Services).
// This is an experimental env var and will likely change in the future.
// - TS_TAILNET_TARGET_IP: proxy all incoming non-Tailscale traffic to the given
// - TS_TAILNET_TARGET_IP: proxy all incoming non-Tailscale traffic to the given
// destination defined by an IP.
// destination defined by an IP.
// - TS_TAILNET_TARGET_FQDN: proxy all incoming non-Tailscale traffic to the given
// - TS_TAILNET_TARGET_FQDN: proxy all incoming non-Tailscale traffic to the given
@ -82,12 +86,15 @@ import (
"fmt"
"fmt"
"io/fs"
"io/fs"
"log"
"log"
"math"
"net"
"net/netip"
"net/netip"
"os"
"os"
"os/exec"
"os/exec"
"os/signal"
"os/signal"
"path/filepath"
"path/filepath"
"reflect"
"reflect"
"slices"
"strconv"
"strconv"
"strings"
"strings"
"sync"
"sync"
@ -122,7 +129,8 @@ func main() {
Hostname : defaultEnv ( "TS_HOSTNAME" , "" ) ,
Hostname : defaultEnv ( "TS_HOSTNAME" , "" ) ,
Routes : defaultEnvStringPointer ( "TS_ROUTES" ) ,
Routes : defaultEnvStringPointer ( "TS_ROUTES" ) ,
ServeConfigPath : defaultEnv ( "TS_SERVE_CONFIG" , "" ) ,
ServeConfigPath : defaultEnv ( "TS_SERVE_CONFIG" , "" ) ,
ProxyTo : defaultEnv ( "TS_DEST_IP" , "" ) ,
ProxyTargetIP : defaultEnv ( "TS_DEST_IP" , "" ) ,
ProxyTargetDNSName : defaultEnv ( "TS_EXPERIMENTAL_DEST_DNS_NAME" , "" ) ,
TailnetTargetIP : defaultEnv ( "TS_TAILNET_TARGET_IP" , "" ) ,
TailnetTargetIP : defaultEnv ( "TS_TAILNET_TARGET_IP" , "" ) ,
TailnetTargetFQDN : defaultEnv ( "TS_TAILNET_TARGET_FQDN" , "" ) ,
TailnetTargetFQDN : defaultEnv ( "TS_TAILNET_TARGET_FQDN" , "" ) ,
DaemonExtraArgs : defaultEnv ( "TS_TAILSCALED_EXTRA_ARGS" , "" ) ,
DaemonExtraArgs : defaultEnv ( "TS_TAILSCALED_EXTRA_ARGS" , "" ) ,
@ -150,8 +158,8 @@ func main() {
if err := ensureTunFile ( cfg . Root ) ; err != nil {
if err := ensureTunFile ( cfg . Root ) ; err != nil {
log . Fatalf ( "Unable to create tuntap device file: %v" , err )
log . Fatalf ( "Unable to create tuntap device file: %v" , err )
}
}
if cfg . ProxyT o != "" || cfg . Routes != nil || cfg . TailnetTargetIP != "" || cfg . TailnetTargetFQDN != "" {
if cfg . ProxyT argetIP != "" || cfg . Pr oxyTargetDNSName != "" || cfg . Routes != nil || cfg . TailnetTargetIP != "" || cfg . TailnetTargetFQDN != "" {
if err := ensureIPForwarding ( cfg . Root , cfg . ProxyT o , cfg . TailnetTargetIP , cfg . TailnetTargetFQDN , cfg . Routes ) ; err != nil {
if err := ensureIPForwarding ( cfg . Root , cfg . ProxyT argetIP , cfg . TailnetTargetIP , cfg . TailnetTargetFQDN , cfg . Routes ) ; err != nil {
log . Printf ( "Failed to enable IP forwarding: %v" , err )
log . Printf ( "Failed to enable IP forwarding: %v" , err )
log . Printf ( "To run tailscale as a proxy or router container, IP forwarding must be enabled." )
log . Printf ( "To run tailscale as a proxy or router container, IP forwarding must be enabled." )
if cfg . InKubernetes {
if cfg . InKubernetes {
@ -341,7 +349,7 @@ authLoop:
}
}
var (
var (
wantProxy = cfg . ProxyT o != "" || cfg . TailnetTargetIP != "" || cfg . TailnetTargetFQDN != "" || cfg . AllowProxyingClusterTrafficViaIngress
wantProxy = cfg . ProxyT argetIP != "" || cfg . Pr oxyTargetDNSName != "" || cfg . TailnetTargetIP != "" || cfg . TailnetTargetFQDN != "" || cfg . AllowProxyingClusterTrafficViaIngress
wantDeviceInfo = cfg . InKubernetes && cfg . KubeSecret != "" && cfg . KubernetesCanPatch
wantDeviceInfo = cfg . InKubernetes && cfg . KubeSecret != "" && cfg . KubernetesCanPatch
startupTasksDone = false
startupTasksDone = false
currentIPs deephash . Sum // tailscale IPs assigned to device
currentIPs deephash . Sum // tailscale IPs assigned to device
@ -349,6 +357,9 @@ authLoop:
currentEgressIPs deephash . Sum
currentEgressIPs deephash . Sum
addrs [ ] netip . Prefix
backendAddrs [ ] net . IP
certDomain = new ( atomic . Pointer [ string ] )
certDomain = new ( atomic . Pointer [ string ] )
certDomainChanged = make ( chan bool , 1 )
certDomainChanged = make ( chan bool , 1 )
)
)
@ -362,6 +373,44 @@ authLoop:
log . Fatalf ( "error creating new netfilter runner: %v" , err )
log . Fatalf ( "error creating new netfilter runner: %v" , err )
}
}
}
}
// Setup for proxies that are configured to proxy to a target specified
// by a DNS name (TS_EXPERIMENTAL_DEST_DNS_NAME).
const defaultCheckPeriod = time . Minute * 10 // how often to check what IPs the DNS name resolves to
var (
tc = make ( chan string , 1 )
failedResolveAttempts int
t * time . Timer = time . AfterFunc ( defaultCheckPeriod , func ( ) {
if cfg . ProxyTargetDNSName != "" {
tc <- "recheck"
}
} )
)
defer t . Stop ( )
// resetTimer resets timer for when to next attempt to resolve the DNS
// name for the proxy configured with TS_EXPERIMENTAL_DEST_DNS_NAME. The
// timer gets reset to 10 minutes from now unless the last resolution
// attempt failed. If one or more consecutive previous resolution
// attempts failed, the next resolution attempt will happen after the smallest
// of (10 minutes, 2 ^ number-of-consecutive-failed-resolution-attempts
// seconds) i.e 2s, 4s, 8s ... 10 minutes.
resetTimer := func ( lastResolveFailed bool ) {
if ! lastResolveFailed {
log . Printf ( "reconfigureTimer: next DNS resolution attempt in %s" , defaultCheckPeriod )
t . Reset ( defaultCheckPeriod )
failedResolveAttempts = 0
return
}
minDelay := 2 // 2 seconds
nextTick := time . Second * time . Duration ( math . Pow ( float64 ( minDelay ) , float64 ( failedResolveAttempts ) ) )
if nextTick > defaultCheckPeriod {
nextTick = defaultCheckPeriod // cap at 10 minutes
}
log . Printf ( "reconfigureTimer: last DNS resolution attempt failed, next DNS resolution attempt in %v" , nextTick )
t . Reset ( nextTick )
failedResolveAttempts ++
}
notifyChan := make ( chan ipn . Notify )
notifyChan := make ( chan ipn . Notify )
errChan := make ( chan error )
errChan := make ( chan error )
go func ( ) {
go func ( ) {
@ -399,7 +448,7 @@ runLoop:
log . Fatalf ( "tailscaled left running state (now in state %q), exiting" , * n . State )
log . Fatalf ( "tailscaled left running state (now in state %q), exiting" , * n . State )
}
}
if n . NetMap != nil {
if n . NetMap != nil {
addrs : = n . NetMap . SelfNode . Addresses ( ) . AsSlice ( )
addrs = n . NetMap . SelfNode . Addresses ( ) . AsSlice ( )
newCurrentIPs := deephash . Hash ( & addrs )
newCurrentIPs := deephash . Hash ( & addrs )
ipsHaveChanged := newCurrentIPs != currentIPs
ipsHaveChanged := newCurrentIPs != currentIPs
@ -425,7 +474,7 @@ runLoop:
egressAddrs = node . Addresses ( ) . AsSlice ( )
egressAddrs = node . Addresses ( ) . AsSlice ( )
newCurentEgressIPs = deephash . Hash ( & egressAddrs )
newCurentEgressIPs = deephash . Hash ( & egressAddrs )
egressIPsHaveChanged = newCurentEgressIPs != currentEgressIPs
egressIPsHaveChanged = newCurentEgressIPs != currentEgressIPs
if egressIPsHaveChanged && len ( egressAddrs ) > 0 {
if egressIPsHaveChanged && len ( egressAddrs ) != 0 {
for _ , egressAddr := range egressAddrs {
for _ , egressAddr := range egressAddrs {
ea := egressAddr . Addr ( )
ea := egressAddr . Addr ( )
// TODO (irbekrm): make it work for IPv6 too.
// TODO (irbekrm): make it work for IPv6 too.
@ -441,13 +490,32 @@ runLoop:
}
}
currentEgressIPs = newCurentEgressIPs
currentEgressIPs = newCurentEgressIPs
}
}
if cfg . ProxyT o != "" && len ( addrs ) > 0 && ipsHaveChanged {
if cfg . ProxyT argetIP != "" && len ( addrs ) != 0 && ipsHaveChanged {
log . Printf ( "Installing proxy rules" )
log . Printf ( "Installing proxy rules" )
if err := installIngressForwardingRule ( ctx , cfg . ProxyT o , addrs , nfr ) ; err != nil {
if err := installIngressForwardingRule ( ctx , cfg . ProxyT argetIP , addrs , nfr ) ; err != nil {
log . Fatalf ( "installing ingress proxy rules: %v" , err )
log . Fatalf ( "installing ingress proxy rules: %v" , err )
}
}
}
}
if cfg . ServeConfigPath != "" && len ( n . NetMap . DNS . CertDomains ) > 0 {
if cfg . ProxyTargetDNSName != "" && len ( addrs ) != 0 && ipsHaveChanged {
newBackendAddrs , err := resolveDNS ( ctx , cfg . ProxyTargetDNSName )
if err != nil {
log . Printf ( "[unexpected] error resolving DNS name %s: %v" , cfg . ProxyTargetDNSName , err )
resetTimer ( true )
continue
}
backendsHaveChanged := ! ( slices . EqualFunc ( backendAddrs , newBackendAddrs , func ( ip1 net . IP , ip2 net . IP ) bool {
return slices . ContainsFunc ( newBackendAddrs , func ( ip net . IP ) bool { return ip . Equal ( ip1 ) } )
} ) )
if backendsHaveChanged {
log . Printf ( "installing ingress proxy rules for backends %v" , newBackendAddrs )
if err := installIngressForwardingRuleForDNSTarget ( ctx , newBackendAddrs , addrs , nfr ) ; err != nil {
log . Fatalf ( "error installing ingress proxy rules: %v" , err )
}
}
resetTimer ( false )
backendAddrs = newBackendAddrs
}
if cfg . ServeConfigPath != "" && len ( n . NetMap . DNS . CertDomains ) != 0 {
cd := n . NetMap . DNS . CertDomains [ 0 ]
cd := n . NetMap . DNS . CertDomains [ 0 ]
prev := certDomain . Swap ( ptr . To ( cd ) )
prev := certDomain . Swap ( ptr . To ( cd ) )
if prev == nil || * prev != cd {
if prev == nil || * prev != cd {
@ -457,7 +525,7 @@ runLoop:
}
}
}
}
}
}
if cfg . TailnetTargetIP != "" && ipsHaveChanged && len ( addrs ) > 0 {
if cfg . TailnetTargetIP != "" && ipsHaveChanged && len ( addrs ) != 0 {
log . Printf ( "Installing forwarding rules for destination %v" , cfg . TailnetTargetIP )
log . Printf ( "Installing forwarding rules for destination %v" , cfg . TailnetTargetIP )
if err := installEgressForwardingRule ( ctx , cfg . TailnetTargetIP , addrs , nfr ) ; err != nil {
if err := installEgressForwardingRule ( ctx , cfg . TailnetTargetIP , addrs , nfr ) ; err != nil {
log . Fatalf ( "installing egress proxy rules: %v" , err )
log . Fatalf ( "installing egress proxy rules: %v" , err )
@ -469,7 +537,7 @@ runLoop:
// enabled, set up proxy rule each time the
// enabled, set up proxy rule each time the
// tailnet IPs of this node change (including
// tailnet IPs of this node change (including
// the first time they become available).
// the first time they become available).
if cfg . AllowProxyingClusterTrafficViaIngress && cfg . ServeConfigPath != "" && ipsHaveChanged && len ( addrs ) > 0 {
if cfg . AllowProxyingClusterTrafficViaIngress && cfg . ServeConfigPath != "" && ipsHaveChanged && len ( addrs ) != 0 {
log . Printf ( "installing rules to forward traffic for %s to node's tailnet IP" , cfg . PodIP )
log . Printf ( "installing rules to forward traffic for %s to node's tailnet IP" , cfg . PodIP )
if err := installTSForwardingRuleForDestination ( ctx , cfg . PodIP , addrs , nfr ) ; err != nil {
if err := installTSForwardingRuleForDestination ( ctx , cfg . PodIP , addrs , nfr ) ; err != nil {
log . Fatalf ( "installing rules to forward traffic to node's tailnet IP: %v" , err )
log . Fatalf ( "installing rules to forward traffic to node's tailnet IP: %v" , err )
@ -511,12 +579,29 @@ runLoop:
os . Exit ( 0 )
os . Exit ( 0 )
}
}
}
}
}
}
wg . Add ( 1 )
wg . Add ( 1 )
go reaper ( )
go reaper ( )
}
}
}
}
case <- tc :
newBackendAddrs , err := resolveDNS ( ctx , cfg . ProxyTargetDNSName )
if err != nil {
log . Printf ( "[unexpected] error resolving DNS name %s: %v" , cfg . ProxyTargetDNSName , err )
resetTimer ( true )
continue
}
backendsHaveChanged := ! ( slices . EqualFunc ( backendAddrs , newBackendAddrs , func ( ip1 net . IP , ip2 net . IP ) bool {
return slices . ContainsFunc ( newBackendAddrs , func ( ip net . IP ) bool { return ip . Equal ( ip1 ) } )
} ) )
if backendsHaveChanged && len ( addrs ) != 0 {
log . Printf ( "Backend address change detected, installing proxy rules for backends %v" , newBackendAddrs )
if err := installIngressForwardingRuleForDNSTarget ( ctx , newBackendAddrs , addrs , nfr ) ; err != nil {
log . Fatalf ( "installing ingress proxy rules for DNS target %s: %v" , cfg . ProxyTargetDNSName , err )
}
}
backendAddrs = newBackendAddrs
resetTimer ( false )
}
}
}
}
wg . Wait ( )
wg . Wait ( )
@ -757,12 +842,12 @@ func ensureTunFile(root string) error {
}
}
// ensureIPForwarding enables IPv4/IPv6 forwarding for the container.
// ensureIPForwarding enables IPv4/IPv6 forwarding for the container.
func ensureIPForwarding ( root , clusterProxyTarget , tailnetTargeti P, tailnetTargetFQDN string , routes * string ) error {
func ensureIPForwarding ( root , clusterProxyTarget IP, tailnetTargetI P, tailnetTargetFQDN string , routes * string ) error {
var (
var (
v4Forwarding , v6Forwarding bool
v4Forwarding , v6Forwarding bool
)
)
if clusterProxyTarget != "" {
if clusterProxyTarget IP != "" {
proxyIP , err := netip . ParseAddr ( clusterProxyTarget )
proxyIP , err := netip . ParseAddr ( clusterProxyTarget IP )
if err != nil {
if err != nil {
return fmt . Errorf ( "invalid cluster destination IP: %v" , err )
return fmt . Errorf ( "invalid cluster destination IP: %v" , err )
}
}
@ -772,8 +857,8 @@ func ensureIPForwarding(root, clusterProxyTarget, tailnetTargetiP, tailnetTarget
v6Forwarding = true
v6Forwarding = true
}
}
}
}
if tailnetTarget i P != "" {
if tailnetTarget I P != "" {
proxyIP , err := netip . ParseAddr ( tailnetTarget i P)
proxyIP , err := netip . ParseAddr ( tailnetTarget I P)
if err != nil {
if err != nil {
return fmt . Errorf ( "invalid tailnet destination IP: %v" , err )
return fmt . Errorf ( "invalid tailnet destination IP: %v" , err )
}
}
@ -801,7 +886,10 @@ func ensureIPForwarding(root, clusterProxyTarget, tailnetTargetiP, tailnetTarget
}
}
}
}
}
}
return enableIPForwarding ( v4Forwarding , v6Forwarding , root )
}
func enableIPForwarding ( v4Forwarding , v6Forwarding bool , root string ) error {
var paths [ ] string
var paths [ ] string
if v4Forwarding {
if v4Forwarding {
paths = append ( paths , filepath . Join ( root , "proc/sys/net/ipv4/ip_forward" ) )
paths = append ( paths , filepath . Join ( root , "proc/sys/net/ipv4/ip_forward" ) )
@ -918,15 +1006,89 @@ func installIngressForwardingRule(ctx context.Context, dstStr string, tsIPs []ne
return nil
return nil
}
}
func installIngressForwardingRuleForDNSTarget ( ctx context . Context , backendAddrs [ ] net . IP , tsIPs [ ] netip . Prefix , nfr linuxfw . NetfilterRunner ) error {
var (
tsv4 netip . Addr
tsv6 netip . Addr
v4Backends [ ] netip . Addr
v6Backends [ ] netip . Addr
)
for _ , pfx := range tsIPs {
if pfx . IsSingleIP ( ) && pfx . Addr ( ) . Is4 ( ) {
tsv4 = pfx . Addr ( )
continue
}
if pfx . IsSingleIP ( ) && pfx . Addr ( ) . Is6 ( ) {
tsv6 = pfx . Addr ( )
continue
}
}
// TODO: log if more than one backend address is found and firewall is
// in nftables mode that only the first IP will be used.
for _ , ip := range backendAddrs {
if ip . To4 ( ) != nil {
v4Backends = append ( v4Backends , netip . AddrFrom4 ( [ 4 ] byte ( ip . To4 ( ) ) ) )
}
if ip . To16 ( ) != nil {
v6Backends = append ( v6Backends , netip . AddrFrom16 ( [ 16 ] byte ( ip . To16 ( ) ) ) )
}
}
// Enable IP forwarding here as opposed to at the start of containerboot
// as the IPv4/IPv6 requirements might have changed.
// For Kubernetes operator proxies, forwarding for both IPv4 and IPv6 is
// enabled by an init container, so in practice enabling forwarding here
// is only needed if this proxy has been configured by manually setting
// TS_EXPERIMENTAL_DEST_DNS_NAME env var for a containerboot instance.
if err := enableIPForwarding ( len ( v4Backends ) != 0 , len ( v6Backends ) != 0 , "" ) ; err != nil {
log . Printf ( "[unexpected] failed to ensure IP forwarding: %v" , err )
}
updateFirewall := func ( dst netip . Addr , backendTargets [ ] netip . Addr ) error {
if err := nfr . DNATWithLoadBalancer ( dst , backendTargets ) ; err != nil {
return fmt . Errorf ( "installing DNAT rules for ingress backends %+#v: %w" , backendTargets , err )
}
// The backend might advertize MSS higher than that of the
// tailscale interfaces. Clamp MSS of packets going out via
// tailscale0 interface to its MTU to prevent broken connections
// in environments where path MTU discovery is not working.
if err := nfr . ClampMSSToPMTU ( "tailscale0" , dst ) ; err != nil {
return fmt . Errorf ( "adding rule to clamp traffic via tailscale0: %v" , err )
}
return nil
}
if len ( v4Backends ) != 0 {
if ! tsv4 . IsValid ( ) {
log . Printf ( "backend targets %v contain at least one IPv4 address, but this node's Tailscale IPs do not contain a valid IPv4 address: %v" , backendAddrs , tsIPs )
} else if err := updateFirewall ( tsv4 , v4Backends ) ; err != nil {
return fmt . Errorf ( "Installing IPv4 firewall rules: %w" , err )
}
}
if len ( v6Backends ) != 0 && ! tsv6 . IsValid ( ) {
if ! tsv6 . IsValid ( ) {
log . Printf ( "backend targets %v contain at least one IPv6 address, but this node's Tailscale IPs do not contain a valid IPv6 address: %v" , backendAddrs , tsIPs )
} else if ! nfr . HasIPV6NAT ( ) {
log . Printf ( "backend targets %v contain at least one IPv6 address, but the chosen firewall mode does not support IPv6 NAT" , backendAddrs )
} else if err := updateFirewall ( tsv6 , v6Backends ) ; err != nil {
return fmt . Errorf ( "Installing IPv6 firewall rules: %w" , err )
}
}
return nil
}
// settings is all the configuration for containerboot.
// settings is all the configuration for containerboot.
type settings struct {
type settings struct {
AuthKey string
AuthKey string
Hostname string
Hostname string
Routes * string
Routes * string
// ProxyTo is the destination IP to which all incoming
// ProxyT argetIP is the destination IP to which all incoming
// Tailscale traffic should be proxied. If empty, no proxying
// Tailscale traffic should be proxied. If empty, no proxying
// is done. This is typically a locally reachable IP.
// is done. This is typically a locally reachable IP.
ProxyTo string
ProxyTargetIP string
// ProxyTargetDNSName is a DNS name to whose backing IP addresses all
// incoming Tailscale traffic should be proxied.
ProxyTargetDNSName string
// TailnetTargetIP is the destination IP to which all incoming
// TailnetTargetIP is the destination IP to which all incoming
// non-Tailscale traffic should be proxied. This is typically a
// non-Tailscale traffic should be proxied. This is typically a
// Tailscale IP.
// Tailscale IP.
@ -966,9 +1128,15 @@ func (s *settings) validate() error {
return fmt . Errorf ( "error validating tailscaled configfile contents: %w" , err )
return fmt . Errorf ( "error validating tailscaled configfile contents: %w" , err )
}
}
}
}
if s . ProxyT o != "" && s . UserspaceMode {
if s . ProxyT argetIP != "" && s . UserspaceMode {
return errors . New ( "TS_DEST_IP is not supported with TS_USERSPACE" )
return errors . New ( "TS_DEST_IP is not supported with TS_USERSPACE" )
}
}
if s . ProxyTargetDNSName != "" && s . UserspaceMode {
return errors . New ( "TS_EXPERIMENTAL_DEST_DNS_NAME is not supported with TS_USERSPACE" )
}
if s . ProxyTargetDNSName != "" && s . ProxyTargetIP != "" {
return errors . New ( "TS_EXPERIMENTAL_DEST_DNS_NAME and TS_DEST_IP cannot both be set" )
}
if s . TailnetTargetIP != "" && s . UserspaceMode {
if s . TailnetTargetIP != "" && s . UserspaceMode {
return errors . New ( "TS_TAILNET_TARGET_IP is not supported with TS_USERSPACE" )
return errors . New ( "TS_TAILNET_TARGET_IP is not supported with TS_USERSPACE" )
}
}
@ -993,6 +1161,28 @@ func (s *settings) validate() error {
return nil
return nil
}
}
func resolveDNS ( ctx context . Context , name string ) ( [ ] net . IP , error ) {
// TODO (irbekrm): look at using recursive.Resolver instead to resolve
// the DNS names as well as retrieve TTLs. It looks though that this
// seems to return very short TTLs (shorter than on the actual records).
ip4s , err := net . DefaultResolver . LookupIP ( ctx , "ip4" , name )
if err != nil {
if e , ok := err . ( * net . DNSError ) ; ! ( ok && e . IsNotFound ) {
return nil , fmt . Errorf ( "error looking up IPv4 addresses: %v" , err )
}
}
ip6s , err := net . DefaultResolver . LookupIP ( ctx , "ip6" , name )
if err != nil {
if e , ok := err . ( * net . DNSError ) ; ! ( ok && e . IsNotFound ) {
return nil , fmt . Errorf ( "error looking up IPv6 addresses: %v" , err )
}
}
if len ( ip4s ) == 0 && len ( ip6s ) == 0 {
return nil , fmt . Errorf ( "no IPv4 or IPv6 addresses found for host: %s" , name )
}
return append ( ip4s , ip6s ... ) , nil
}
// defaultEnv returns the value of the given envvar name, or defVal if
// defaultEnv returns the value of the given envvar name, or defVal if
// unset.
// unset.
func defaultEnv ( name , defVal string ) string {
func defaultEnv ( name , defVal string ) string {