github.com/richardwilkes/toolbox@v1.121.0/xio/network/addresses.go (about)

     1  // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved.
     2  //
     3  // This Source Code Form is subject to the terms of the Mozilla Public
     4  // License, version 2.0. If a copy of the MPL was not distributed with
     5  // this file, You can obtain one at http://mozilla.org/MPL/2.0/.
     6  //
     7  // This Source Code Form is "Incompatible With Secondary Licenses", as
     8  // defined by the Mozilla Public License, version 2.0.
     9  
    10  // Package network provides network-related utilities.
    11  package network
    12  
    13  import (
    14  	"math/rand/v2"
    15  	"net"
    16  	"sort"
    17  	"strings"
    18  	"time"
    19  
    20  	"github.com/richardwilkes/toolbox/collection"
    21  	"github.com/richardwilkes/toolbox/txt"
    22  )
    23  
    24  // Constants for common network addresses.
    25  const (
    26  	IPv4LoopbackAddress = "127.0.0.1"
    27  	IPv6LoopbackAddress = "::1"
    28  	LocalHost           = "localhost"
    29  )
    30  
    31  // PrimaryIPAddress returns the primary IP address.
    32  func PrimaryIPAddress() string {
    33  	// Try up to 3 times in case of transient errors
    34  	for i := 0; i < 3; i++ {
    35  		if addresses, err := net.InterfaceAddrs(); err == nil {
    36  			var fallback string
    37  			for _, address := range addresses {
    38  				var ip net.IP
    39  				switch v := address.(type) {
    40  				case *net.IPNet:
    41  					ip = v.IP
    42  				case *net.IPAddr:
    43  					ip = v.IP
    44  				default:
    45  					continue
    46  				}
    47  				if ip.IsGlobalUnicast() {
    48  					if ip.To4() != nil {
    49  						return ip.String()
    50  					}
    51  					if fallback == "" {
    52  						fallback = ip.String()
    53  					}
    54  				}
    55  			}
    56  			if fallback != "" {
    57  				return fallback
    58  			}
    59  		}
    60  		//nolint:gosec // Yes, it is ok to use a weak prng here
    61  		time.Sleep(time.Duration(100+rand.IntN(50)) * time.Millisecond)
    62  	}
    63  	return IPv4LoopbackAddress
    64  }
    65  
    66  // PrimaryAddress returns the primary hostname and its associated IP address and MAC address.
    67  func PrimaryAddress() (hostname, ipAddress, macAddress string) {
    68  	// Try up to 3 times in case of transient errors
    69  	for i := 0; i < 3; i++ {
    70  		lowest := 1000000
    71  		for address, iFace := range ActiveAddresses() {
    72  			if iFace.Index < lowest {
    73  				lowest = iFace.Index
    74  				hostname = address
    75  				macAddress = iFace.HardwareAddr.String()
    76  			}
    77  		}
    78  		if hostname != "" {
    79  			if ips, err := net.LookupIP(hostname); err == nil && len(ips) > 0 {
    80  				for _, ip := range ips {
    81  					if ip.To4() != nil {
    82  						ipAddress = ip.String()
    83  						break
    84  					} else if ipAddress == "" {
    85  						ipAddress = ip.String()
    86  					}
    87  				}
    88  				if ipAddress != "" {
    89  					return hostname, ipAddress, macAddress
    90  				}
    91  			}
    92  		}
    93  		//nolint:gosec // Yes, it is ok to use a weak prng here
    94  		time.Sleep(time.Duration(100+rand.IntN(50)) * time.Millisecond)
    95  	}
    96  	return LocalHost, IPv4LoopbackAddress, "00:00:00:00:00:00"
    97  }
    98  
    99  // ActiveAddresses determines the best address for each active network interface. IPv4 addresses will be selected over
   100  // IPv6 addresses on the same interface. Numeric addresses are resolved into names where possible.
   101  func ActiveAddresses() map[string]net.Interface {
   102  	result := make(map[string]net.Interface)
   103  	if iFaces, err := net.Interfaces(); err == nil {
   104  		for _, iFace := range iFaces {
   105  			const interesting = net.FlagUp | net.FlagBroadcast
   106  			if iFace.Flags&interesting == interesting {
   107  				if name := Address(iFace); name != "" {
   108  					result[name] = iFace
   109  				}
   110  			}
   111  		}
   112  	}
   113  	return result
   114  }
   115  
   116  // Address returns the best address for the network interface. IPv4 addresses will be selected over IPv6 addresses on
   117  // the same interface. Numeric addresses are resolved into names where possible. An empty string will be returned if the
   118  // network interface cannot be resolved into an IPv4 or IPv6 address.
   119  func Address(iFace net.Interface) string {
   120  	if addrs, err := iFace.Addrs(); err == nil {
   121  		var fallback string
   122  		for _, addr := range addrs {
   123  			var ip net.IP
   124  			switch v := addr.(type) {
   125  			case *net.IPNet:
   126  				ip = v.IP
   127  			case *net.IPAddr:
   128  				ip = v.IP
   129  			default:
   130  				continue
   131  			}
   132  			if ip.IsGlobalUnicast() {
   133  				ipAddr := ip.String()
   134  				var names []string
   135  				if names, err = net.LookupAddr(ipAddr); err == nil {
   136  					if len(names) > 0 {
   137  						name := strings.TrimSuffix(names[0], ".")
   138  						if ip.To4() != nil {
   139  							return name
   140  						}
   141  						if fallback == "" {
   142  							fallback = name
   143  						}
   144  						continue
   145  					}
   146  				}
   147  				if ip.To4() != nil {
   148  					return ipAddr
   149  				}
   150  				if fallback == "" {
   151  					fallback = ipAddr
   152  				}
   153  			}
   154  		}
   155  		if fallback != "" {
   156  			return fallback
   157  		}
   158  	}
   159  	return ""
   160  }
   161  
   162  // AddressesForHost returns the addresses/names for the given host. If an IP number is passed in, then it will be
   163  // returned. If a host name is passed in, the host name plus the IP address(es) it resolves to will be returned. If the
   164  // empty string is passed in, then the host names and IP addresses for all active interfaces will be returned.
   165  func AddressesForHost(host string) []string {
   166  	ss := collection.NewSet[string]()
   167  	if host == "" { // All address on machine
   168  		if iFaces, err := net.Interfaces(); err == nil {
   169  			for _, iFace := range iFaces {
   170  				const interesting = net.FlagUp | net.FlagBroadcast
   171  				if iFace.Flags&interesting == interesting {
   172  					var addrs []net.Addr
   173  					if addrs, err = iFace.Addrs(); err == nil {
   174  						for _, addr := range addrs {
   175  							var ip net.IP
   176  							switch v := addr.(type) {
   177  							case *net.IPNet:
   178  								ip = v.IP
   179  							case *net.IPAddr:
   180  								ip = v.IP
   181  							default:
   182  								continue
   183  							}
   184  							if ip.IsGlobalUnicast() {
   185  								ss.Add(ip.String())
   186  								var names []string
   187  								if names, err = net.LookupAddr(ip.String()); err == nil {
   188  									for _, name := range names {
   189  										ss.Add(strings.TrimSuffix(name, "."))
   190  									}
   191  								}
   192  							}
   193  						}
   194  					}
   195  				}
   196  			}
   197  		}
   198  	} else {
   199  		ss.Add(host)
   200  		if net.ParseIP(host) == nil {
   201  			if ips, err := net.LookupIP(host); err == nil && len(ips) > 0 {
   202  				for _, ip := range ips {
   203  					ss.Add(ip.String())
   204  				}
   205  			}
   206  		}
   207  	}
   208  	for _, one := range []string{"::", IPv6LoopbackAddress, IPv4LoopbackAddress} {
   209  		if ss.Contains(one) {
   210  			delete(ss, one)
   211  			ss.Add(LocalHost)
   212  		}
   213  	}
   214  	addrs := ss.Values()
   215  	sort.Slice(addrs, func(i, j int) bool {
   216  		isName1 := net.ParseIP(addrs[i]) == nil
   217  		isName2 := net.ParseIP(addrs[j]) == nil
   218  		if isName1 == isName2 {
   219  			return txt.NaturalLess(addrs[i], addrs[j], true)
   220  		}
   221  		return isName1
   222  	})
   223  	return addrs
   224  }