| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738 |
- // Copyright (c) Tailscale Inc & AUTHORS
- // SPDX-License-Identifier: BSD-3-Clause
- // Package interfaces contains helpers for looking up system network interfaces.
- package interfaces
- import (
- "bytes"
- "fmt"
- "net"
- "net/http"
- "net/netip"
- "runtime"
- "slices"
- "sort"
- "strings"
- "tailscale.com/hostinfo"
- "tailscale.com/net/netaddr"
- "tailscale.com/net/tsaddr"
- "tailscale.com/net/tshttpproxy"
- )
- // LoginEndpointForProxyDetermination is the URL used for testing
- // which HTTP proxy the system should use.
- var LoginEndpointForProxyDetermination = "https://controlplane.tailscale.com/"
- func isUp(nif *net.Interface) bool { return nif.Flags&net.FlagUp != 0 }
- func isLoopback(nif *net.Interface) bool { return nif.Flags&net.FlagLoopback != 0 }
- func isProblematicInterface(nif *net.Interface) bool {
- name := nif.Name
- // Don't try to send disco/etc packets over zerotier; they effectively
- // DoS each other by doing traffic amplification, both of them
- // preferring/trying to use each other for transport. See:
- // https://github.com/tailscale/tailscale/issues/1208
- if strings.HasPrefix(name, "zt") || (runtime.GOOS == "windows" && strings.Contains(name, "ZeroTier")) {
- return true
- }
- return false
- }
- // LocalAddresses returns the machine's IP addresses, separated by
- // whether they're loopback addresses. If there are no regular addresses
- // it will return any IPv4 linklocal or IPv6 unique local addresses because we
- // know of environments where these are used with NAT to provide connectivity.
- func LocalAddresses() (regular, loopback []netip.Addr, err error) {
- // TODO(crawshaw): don't serve interface addresses that we are routing
- ifaces, err := netInterfaces()
- if err != nil {
- return nil, nil, err
- }
- var regular4, regular6, linklocal4, ula6 []netip.Addr
- for _, iface := range ifaces {
- stdIf := iface.Interface
- if !isUp(stdIf) || isProblematicInterface(stdIf) {
- // Skip down interfaces and ones that are
- // problematic that we don't want to try to
- // send Tailscale traffic over.
- continue
- }
- ifcIsLoopback := isLoopback(stdIf)
- addrs, err := iface.Addrs()
- if err != nil {
- return nil, nil, err
- }
- for _, a := range addrs {
- switch v := a.(type) {
- case *net.IPNet:
- ip, ok := netip.AddrFromSlice(v.IP)
- if !ok {
- continue
- }
- ip = ip.Unmap()
- // TODO(apenwarr): don't special case cgNAT.
- // In the general wireguard case, it might
- // very well be something we can route to
- // directly, because both nodes are
- // behind the same CGNAT router.
- if tsaddr.IsTailscaleIP(ip) {
- continue
- }
- if ip.IsLoopback() || ifcIsLoopback {
- loopback = append(loopback, ip)
- } else if ip.IsLinkLocalUnicast() {
- if ip.Is4() {
- linklocal4 = append(linklocal4, ip)
- }
- // We know of no cases where the IPv6 fe80:: addresses
- // are used to provide WAN connectivity. It is also very
- // common for users to have no IPv6 WAN connectivity,
- // but their OS supports IPv6 so they have an fe80::
- // address. We don't want to report all of those
- // IPv6 LL to Control.
- } else if ip.Is6() && ip.IsPrivate() {
- // Google Cloud Run uses NAT with IPv6 Unique
- // Local Addresses to provide IPv6 connectivity.
- ula6 = append(ula6, ip)
- } else {
- if ip.Is4() {
- regular4 = append(regular4, ip)
- } else {
- regular6 = append(regular6, ip)
- }
- }
- }
- }
- }
- if len(regular4) == 0 && len(regular6) == 0 {
- // if we have no usable IP addresses then be willing to accept
- // addresses we otherwise wouldn't, like:
- // + 169.254.x.x (AWS Lambda and Azure App Services use NAT with these)
- // + IPv6 ULA (Google Cloud Run uses these with address translation)
- regular4 = linklocal4
- regular6 = ula6
- }
- regular = append(regular4, regular6...)
- sortIPs(regular)
- sortIPs(loopback)
- return regular, loopback, nil
- }
- func sortIPs(s []netip.Addr) {
- sort.Slice(s, func(i, j int) bool { return s[i].Less(s[j]) })
- }
- // Interface is a wrapper around Go's net.Interface with some extra methods.
- type Interface struct {
- *net.Interface
- AltAddrs []net.Addr // if non-nil, returned by Addrs
- Desc string // extra description (used on Windows)
- }
- func (i Interface) IsLoopback() bool { return isLoopback(i.Interface) }
- func (i Interface) IsUp() bool { return isUp(i.Interface) }
- func (i Interface) Addrs() ([]net.Addr, error) {
- if i.AltAddrs != nil {
- return i.AltAddrs, nil
- }
- return i.Interface.Addrs()
- }
- // ForeachInterfaceAddress is a wrapper for GetList, then
- // List.ForeachInterfaceAddress.
- func ForeachInterfaceAddress(fn func(Interface, netip.Prefix)) error {
- ifaces, err := GetList()
- if err != nil {
- return err
- }
- return ifaces.ForeachInterfaceAddress(fn)
- }
- // ForeachInterfaceAddress calls fn for each interface in ifaces, with
- // all its addresses. The IPPrefix's IP is the IP address assigned to
- // the interface, and Bits are the subnet mask.
- func (ifaces List) ForeachInterfaceAddress(fn func(Interface, netip.Prefix)) error {
- for _, iface := range ifaces {
- addrs, err := iface.Addrs()
- if err != nil {
- return err
- }
- for _, a := range addrs {
- switch v := a.(type) {
- case *net.IPNet:
- if pfx, ok := netaddr.FromStdIPNet(v); ok {
- fn(iface, pfx)
- }
- }
- }
- }
- return nil
- }
- // ForeachInterface is a wrapper for GetList, then
- // List.ForeachInterface.
- func ForeachInterface(fn func(Interface, []netip.Prefix)) error {
- ifaces, err := GetList()
- if err != nil {
- return err
- }
- return ifaces.ForeachInterface(fn)
- }
- // ForeachInterface calls fn for each interface in ifaces, with
- // all its addresses. The IPPrefix's IP is the IP address assigned to
- // the interface, and Bits are the subnet mask.
- func (ifaces List) ForeachInterface(fn func(Interface, []netip.Prefix)) error {
- for _, iface := range ifaces {
- addrs, err := iface.Addrs()
- if err != nil {
- return err
- }
- var pfxs []netip.Prefix
- for _, a := range addrs {
- switch v := a.(type) {
- case *net.IPNet:
- if pfx, ok := netaddr.FromStdIPNet(v); ok {
- pfxs = append(pfxs, pfx)
- }
- }
- }
- sort.Slice(pfxs, func(i, j int) bool {
- return pfxs[i].Addr().Less(pfxs[j].Addr())
- })
- fn(iface, pfxs)
- }
- return nil
- }
- // State is intended to store the state of the machine's network interfaces,
- // routing table, and other network configuration.
- // For now it's pretty basic.
- type State struct {
- // InterfaceIPs maps from an interface name to the IP addresses
- // configured on that interface. Each address is represented as an
- // IPPrefix, where the IP is the interface IP address and Bits is
- // the subnet mask.
- InterfaceIPs map[string][]netip.Prefix
- Interface map[string]Interface
- // HaveV6 is whether this machine has an IPv6 Global or Unique Local Address
- // which might provide connectivity on a non-Tailscale interface that's up.
- HaveV6 bool
- // HaveV4 is whether the machine has some non-localhost,
- // non-link-local IPv4 address on a non-Tailscale interface that's up.
- HaveV4 bool
- // IsExpensive is whether the current network interface is
- // considered "expensive", which currently means LTE/etc
- // instead of Wifi. This field is not populated by GetState.
- IsExpensive bool
- // DefaultRouteInterface is the interface name for the
- // machine's default route.
- //
- // It is not yet populated on all OSes.
- //
- // When non-empty, its value is the map key into Interface and
- // InterfaceIPs.
- DefaultRouteInterface string
- // HTTPProxy is the HTTP proxy to use, if any.
- HTTPProxy string
- // PAC is the URL to the Proxy Autoconfig URL, if applicable.
- PAC string
- }
- func (s *State) String() string {
- var sb strings.Builder
- fmt.Fprintf(&sb, "interfaces.State{defaultRoute=%v ", s.DefaultRouteInterface)
- if s.DefaultRouteInterface != "" {
- if iface, ok := s.Interface[s.DefaultRouteInterface]; ok && iface.Desc != "" {
- fmt.Fprintf(&sb, "(%s) ", iface.Desc)
- }
- }
- sb.WriteString("ifs={")
- var ifs []string
- for k := range s.Interface {
- if s.keepInterfaceInStringSummary(k) {
- ifs = append(ifs, k)
- }
- }
- sort.Slice(ifs, func(i, j int) bool {
- upi, upj := s.Interface[ifs[i]].IsUp(), s.Interface[ifs[j]].IsUp()
- if upi != upj {
- // Up sorts before down.
- return upi
- }
- return ifs[i] < ifs[j]
- })
- for i, ifName := range ifs {
- if i > 0 {
- sb.WriteString(" ")
- }
- iface := s.Interface[ifName]
- if iface.Interface == nil {
- fmt.Fprintf(&sb, "%s:nil", ifName)
- continue
- }
- if !iface.IsUp() {
- fmt.Fprintf(&sb, "%s:down", ifName)
- continue
- }
- fmt.Fprintf(&sb, "%s:[", ifName)
- needSpace := false
- for _, pfx := range s.InterfaceIPs[ifName] {
- a := pfx.Addr()
- if a.IsMulticast() {
- continue
- }
- fam := "4"
- if a.Is6() {
- fam = "6"
- }
- if needSpace {
- sb.WriteString(" ")
- }
- needSpace = true
- switch {
- case a.IsLoopback():
- fmt.Fprintf(&sb, "lo%s", fam)
- case a.IsLinkLocalUnicast():
- fmt.Fprintf(&sb, "llu%s", fam)
- default:
- fmt.Fprintf(&sb, "%s", pfx)
- }
- }
- sb.WriteString("]")
- }
- sb.WriteString("}")
- if s.IsExpensive {
- sb.WriteString(" expensive")
- }
- if s.HTTPProxy != "" {
- fmt.Fprintf(&sb, " httpproxy=%s", s.HTTPProxy)
- }
- if s.PAC != "" {
- fmt.Fprintf(&sb, " pac=%s", s.PAC)
- }
- fmt.Fprintf(&sb, " v4=%v v6=%v}", s.HaveV4, s.HaveV6)
- return sb.String()
- }
- // Equal reports whether s and s2 are exactly equal.
- func (s *State) Equal(s2 *State) bool {
- if s == nil && s2 == nil {
- return true
- }
- if s == nil || s2 == nil {
- return false
- }
- if s.HaveV6 != s2.HaveV6 ||
- s.HaveV4 != s2.HaveV4 ||
- s.IsExpensive != s2.IsExpensive ||
- s.DefaultRouteInterface != s2.DefaultRouteInterface ||
- s.HTTPProxy != s2.HTTPProxy ||
- s.PAC != s2.PAC {
- return false
- }
- for iname, i := range s.Interface {
- i2, ok := s2.Interface[iname]
- if !ok {
- return false
- }
- if !i.Equal(i2) {
- return false
- }
- }
- for iname, vv := range s.InterfaceIPs {
- if !slices.Equal(vv, s2.InterfaceIPs[iname]) {
- return false
- }
- }
- return true
- }
- // HasIP reports whether any interface has the provided IP address.
- func (s *State) HasIP(ip netip.Addr) bool {
- if s == nil {
- return false
- }
- for _, pv := range s.InterfaceIPs {
- for _, p := range pv {
- if p.Contains(ip) {
- return true
- }
- }
- }
- return false
- }
- func (a Interface) Equal(b Interface) bool {
- if (a.Interface == nil) != (b.Interface == nil) {
- return false
- }
- if !(a.Desc == b.Desc && netAddrsEqual(a.AltAddrs, b.AltAddrs)) {
- return false
- }
- if a.Interface != nil && !(a.Index == b.Index &&
- a.MTU == b.MTU &&
- a.Name == b.Name &&
- a.Flags == b.Flags &&
- bytes.Equal([]byte(a.HardwareAddr), []byte(b.HardwareAddr))) {
- return false
- }
- return true
- }
- func (s *State) HasPAC() bool { return s != nil && s.PAC != "" }
- // AnyInterfaceUp reports whether any interface seems like it has Internet access.
- func (s *State) AnyInterfaceUp() bool {
- if runtime.GOOS == "js" || runtime.GOOS == "tamago" {
- return true
- }
- return s != nil && (s.HaveV4 || s.HaveV6)
- }
- func netAddrsEqual(a, b []net.Addr) bool {
- if len(a) != len(b) {
- return false
- }
- for i, av := range a {
- if av.Network() != b[i].Network() || av.String() != b[i].String() {
- return false
- }
- }
- return true
- }
- func hasTailscaleIP(pfxs []netip.Prefix) bool {
- for _, pfx := range pfxs {
- if tsaddr.IsTailscaleIP(pfx.Addr()) {
- return true
- }
- }
- return false
- }
- func isTailscaleInterface(name string, ips []netip.Prefix) bool {
- if runtime.GOOS == "darwin" && strings.HasPrefix(name, "utun") && hasTailscaleIP(ips) {
- // On macOS in the sandboxed app (at least as of
- // 2021-02-25), we often see two utun devices
- // (e.g. utun4 and utun7) with the same IPv4 and IPv6
- // addresses. Just remove all utun devices with
- // Tailscale IPs until we know what's happening with
- // macOS NetworkExtensions and utun devices.
- return true
- }
- return name == "Tailscale" || // as it is on Windows
- strings.HasPrefix(name, "tailscale") // TODO: use --tun flag value, etc; see TODO in method doc
- }
- // getPAC, if non-nil, returns the current PAC file URL.
- var getPAC func() string
- // GetState returns the state of all the current machine's network interfaces.
- //
- // It does not set the returned State.IsExpensive. The caller can populate that.
- //
- // Deprecated: use netmon.Monitor.InterfaceState instead.
- func GetState() (*State, error) {
- s := &State{
- InterfaceIPs: make(map[string][]netip.Prefix),
- Interface: make(map[string]Interface),
- }
- if err := ForeachInterface(func(ni Interface, pfxs []netip.Prefix) {
- ifUp := ni.IsUp()
- s.Interface[ni.Name] = ni
- s.InterfaceIPs[ni.Name] = append(s.InterfaceIPs[ni.Name], pfxs...)
- if !ifUp || isTailscaleInterface(ni.Name, pfxs) {
- return
- }
- for _, pfx := range pfxs {
- if pfx.Addr().IsLoopback() {
- continue
- }
- s.HaveV6 = s.HaveV6 || isUsableV6(pfx.Addr())
- s.HaveV4 = s.HaveV4 || isUsableV4(pfx.Addr())
- }
- }); err != nil {
- return nil, err
- }
- dr, _ := DefaultRoute()
- s.DefaultRouteInterface = dr.InterfaceName
- // Populate description (for Windows, primarily) if present.
- if desc := dr.InterfaceDesc; desc != "" {
- if iface, ok := s.Interface[dr.InterfaceName]; ok {
- iface.Desc = desc
- s.Interface[dr.InterfaceName] = iface
- }
- }
- if s.AnyInterfaceUp() {
- req, err := http.NewRequest("GET", LoginEndpointForProxyDetermination, nil)
- if err != nil {
- return nil, err
- }
- if u, err := tshttpproxy.ProxyFromEnvironment(req); err == nil && u != nil {
- s.HTTPProxy = u.String()
- }
- if getPAC != nil {
- s.PAC = getPAC()
- }
- }
- return s, nil
- }
- // HTTPOfListener returns the HTTP address to ln.
- // If the listener is listening on the unspecified address, it
- // it tries to find a reasonable interface address on the machine to use.
- func HTTPOfListener(ln net.Listener) string {
- ta, ok := ln.Addr().(*net.TCPAddr)
- if !ok || !ta.IP.IsUnspecified() {
- return fmt.Sprintf("http://%v/", ln.Addr())
- }
- var goodIP string
- var privateIP string
- ForeachInterfaceAddress(func(i Interface, pfx netip.Prefix) {
- ip := pfx.Addr()
- if ip.IsPrivate() {
- if privateIP == "" {
- privateIP = ip.String()
- }
- return
- }
- goodIP = ip.String()
- })
- if privateIP != "" {
- goodIP = privateIP
- }
- if goodIP != "" {
- return fmt.Sprintf("http://%v/", net.JoinHostPort(goodIP, fmt.Sprint(ta.Port)))
- }
- return fmt.Sprintf("http://localhost:%v/", fmt.Sprint(ta.Port))
- }
- var likelyHomeRouterIP func() (netip.Addr, bool)
- // LikelyHomeRouterIP returns the likely IP of the residential router,
- // which will always be an IPv4 private address, if found.
- // In addition, it returns the IP address of the current machine on
- // the LAN using that gateway.
- // This is used as the destination for UPnP, NAT-PMP, PCP, etc queries.
- func LikelyHomeRouterIP() (gateway, myIP netip.Addr, ok bool) {
- if likelyHomeRouterIP != nil {
- gateway, ok = likelyHomeRouterIP()
- if !ok {
- return
- }
- }
- if !ok {
- return
- }
- ForeachInterfaceAddress(func(i Interface, pfx netip.Prefix) {
- if !i.IsUp() {
- // Skip interfaces that aren't up.
- return
- } else if myIP.IsValid() {
- // We already have a valid self IP; skip this one.
- return
- }
- ip := pfx.Addr()
- if !ip.IsValid() || !ip.Is4() {
- // Skip IPs that aren't valid or aren't IPv4, since we
- // always return an IPv4 address.
- return
- }
- if gateway.IsPrivate() && ip.IsPrivate() {
- myIP = ip
- ok = true
- return
- }
- })
- return gateway, myIP, myIP.IsValid()
- }
- // isUsableV4 reports whether ip is a usable IPv4 address which could
- // conceivably be used to get Internet connectivity. Globally routable and
- // private IPv4 addresses are always Usable, and link local 169.254.x.x
- // addresses are in some environments.
- func isUsableV4(ip netip.Addr) bool {
- if !ip.Is4() || ip.IsLoopback() {
- return false
- }
- if ip.IsLinkLocalUnicast() {
- switch hostinfo.GetEnvType() {
- case hostinfo.AWSLambda:
- return true
- case hostinfo.AzureAppService:
- return true
- default:
- return false
- }
- }
- return true
- }
- // isUsableV6 reports whether ip is a usable IPv6 address which could
- // conceivably be used to get Internet connectivity. Globally routable
- // IPv6 addresses are always Usable, and Unique Local Addresses
- // (fc00::/7) are in some environments used with address translation.
- func isUsableV6(ip netip.Addr) bool {
- return v6Global1.Contains(ip) ||
- (ip.Is6() && ip.IsPrivate() && !tsaddr.TailscaleULARange().Contains(ip))
- }
- var (
- v6Global1 = netip.MustParsePrefix("2000::/3")
- )
- // keepInterfaceInStringSummary reports whether the named interface should be included
- // in the String method's summary string.
- func (s *State) keepInterfaceInStringSummary(ifName string) bool {
- iface, ok := s.Interface[ifName]
- if !ok || iface.Interface == nil {
- return false
- }
- if ifName == s.DefaultRouteInterface {
- return true
- }
- up := iface.IsUp()
- for _, p := range s.InterfaceIPs[ifName] {
- a := p.Addr()
- if a.IsLinkLocalUnicast() || a.IsLoopback() {
- continue
- }
- if up || a.IsGlobalUnicast() || a.IsPrivate() {
- return true
- }
- }
- return false
- }
- // isInterestingIP reports whether ip is an interesting IP that we
- // should log in interfaces.State logging. We don't need to show
- // loopback, link-local addresses, or non-Tailscale ULA addresses.
- func isInterestingIP(ip netip.Addr) bool {
- if ip.IsLoopback() || ip.IsLinkLocalUnicast() {
- return false
- }
- return true
- }
- var altNetInterfaces func() ([]Interface, error)
- // RegisterInterfaceGetter sets the function that's used to query
- // the system network interfaces.
- func RegisterInterfaceGetter(getInterfaces func() ([]Interface, error)) {
- altNetInterfaces = getInterfaces
- }
- // List is a list of interfaces on the machine.
- type List []Interface
- // GetList returns the list of interfaces on the machine.
- func GetList() (List, error) {
- return netInterfaces()
- }
- // netInterfaces is a wrapper around the standard library's net.Interfaces
- // that returns a []*Interface instead of a []net.Interface.
- // It exists because Android SDK 30 no longer permits Go's net.Interfaces
- // to work (Issue 2293); this wrapper lets us the Android app register
- // an alternate implementation.
- func netInterfaces() ([]Interface, error) {
- if altNetInterfaces != nil {
- return altNetInterfaces()
- }
- ifs, err := net.Interfaces()
- if err != nil {
- return nil, err
- }
- ret := make([]Interface, len(ifs))
- for i := range ifs {
- ret[i].Interface = &ifs[i]
- }
- return ret, nil
- }
- // DefaultRouteDetails are the details about a default route returned
- // by DefaultRoute.
- type DefaultRouteDetails struct {
- // InterfaceName is the interface name. It must always be populated.
- // It's like "eth0" (Linux), "Ethernet 2" (Windows), "en0" (macOS).
- InterfaceName string
- // InterfaceDesc is populated on Windows at least. It's a
- // longer description, like "Red Hat VirtIO Ethernet Adapter".
- InterfaceDesc string
- // InterfaceIndex is like net.Interface.Index.
- // Zero means not populated.
- InterfaceIndex int
- // TODO(bradfitz): break this out into v4-vs-v6 once that need arises.
- }
- // DefaultRouteInterface is like DefaultRoute but only returns the
- // interface name.
- func DefaultRouteInterface() (string, error) {
- dr, err := DefaultRoute()
- if err != nil {
- return "", err
- }
- return dr.InterfaceName, nil
- }
- // DefaultRoute returns details of the network interface that owns
- // the default route, not including any tailscale interfaces.
- func DefaultRoute() (DefaultRouteDetails, error) {
- return defaultRoute()
- }
- // HasCGNATInterface reports whether there are any non-Tailscale interfaces that
- // use a CGNAT IP range.
- func HasCGNATInterface() (bool, error) {
- hasCGNATInterface := false
- cgnatRange := tsaddr.CGNATRange()
- err := ForeachInterface(func(i Interface, pfxs []netip.Prefix) {
- if hasCGNATInterface || !i.IsUp() || isTailscaleInterface(i.Name, pfxs) {
- return
- }
- for _, pfx := range pfxs {
- if cgnatRange.Overlaps(pfx) {
- hasCGNATInterface = true
- break
- }
- }
- })
- if err != nil {
- return false, err
- }
- return hasCGNATInterface, nil
- }
- var interfaceDebugExtras func(ifIndex int) (string, error)
- // InterfaceDebugExtras returns extra debugging information about an interface
- // if any (an empty string will be returned if there are no additional details).
- // Formatting is platform-dependent and should not be parsed.
- func InterfaceDebugExtras(ifIndex int) (string, error) {
- if interfaceDebugExtras != nil {
- return interfaceDebugExtras(ifIndex)
- }
- return "", nil
- }
|