mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-30 05:25:35 +00:00
0245bbe97b
Also: * add -verbose flag to cmd/tailscale netcheck * remove some API from the interfaces package * convert some of the interfaces package to netaddr.IP * don't even send IPv4 probes on machines with no IPv4 (or only v4 loopback) * and once three regions have replied, stop waiting for other probes at 2x the slowest duration. Updates #376
257 lines
6.9 KiB
Go
257 lines
6.9 KiB
Go
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
// Package interfaces contains helpers for looking up system network interfaces.
|
|
package interfaces
|
|
|
|
import (
|
|
"fmt"
|
|
"net"
|
|
"reflect"
|
|
"strings"
|
|
|
|
"inet.af/netaddr"
|
|
)
|
|
|
|
// Tailscale returns the current machine's Tailscale interface, if any.
|
|
// If none is found, all zero values are returned.
|
|
// A non-nil error is only returned on a problem listing the system interfaces.
|
|
func Tailscale() (net.IP, *net.Interface, error) {
|
|
ifs, err := net.Interfaces()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
for _, iface := range ifs {
|
|
if !maybeTailscaleInterfaceName(iface.Name) {
|
|
continue
|
|
}
|
|
addrs, err := iface.Addrs()
|
|
if err != nil {
|
|
continue
|
|
}
|
|
for _, a := range addrs {
|
|
if ipnet, ok := a.(*net.IPNet); ok && IsTailscaleIP(ipnet.IP) {
|
|
return ipnet.IP, &iface, nil
|
|
}
|
|
}
|
|
}
|
|
return nil, nil, nil
|
|
}
|
|
|
|
// maybeTailscaleInterfaceName reports whether s is an interface
|
|
// name that might be used by Tailscale.
|
|
func maybeTailscaleInterfaceName(s string) bool {
|
|
return strings.HasPrefix(s, "wg") ||
|
|
strings.HasPrefix(s, "ts") ||
|
|
strings.HasPrefix(s, "tailscale") ||
|
|
strings.HasPrefix(s, "utun")
|
|
}
|
|
|
|
// IsTailscaleIP reports whether ip is an IP in a range used by
|
|
// Tailscale virtual network interfaces.
|
|
func IsTailscaleIP(ip net.IP) bool {
|
|
nip, _ := netaddr.FromStdIP(ip) // TODO: push this up to caller, change func signature
|
|
return cgNAT.Contains(nip)
|
|
}
|
|
|
|
func isUp(nif *net.Interface) bool { return nif.Flags&net.FlagUp != 0 }
|
|
func isLoopback(nif *net.Interface) bool { return nif.Flags&net.FlagLoopback != 0 }
|
|
|
|
// LocalAddresses returns the machine's IP addresses, separated by
|
|
// whether they're loopback addresses.
|
|
func LocalAddresses() (regular, loopback []string, err error) {
|
|
// TODO(crawshaw): don't serve interface addresses that we are routing
|
|
ifaces, err := net.Interfaces()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
for i := range ifaces {
|
|
iface := &ifaces[i]
|
|
if !isUp(iface) {
|
|
// Down interfaces don't count
|
|
continue
|
|
}
|
|
ifcIsLoopback := isLoopback(iface)
|
|
|
|
addrs, err := iface.Addrs()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
for _, a := range addrs {
|
|
switch v := a.(type) {
|
|
case *net.IPNet:
|
|
ip, ok := netaddr.FromStdIP(v.IP)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if ip.Is6() {
|
|
// TODO(crawshaw): IPv6 support.
|
|
// Easy to do here, but we need good endpoint ordering logic.
|
|
continue
|
|
}
|
|
// 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 cgNAT.Contains(ip) {
|
|
continue
|
|
}
|
|
if linkLocalIPv4.Contains(ip) {
|
|
continue
|
|
}
|
|
if ip.IsLoopback() || ifcIsLoopback {
|
|
loopback = append(loopback, ip.String())
|
|
} else {
|
|
regular = append(regular, ip.String())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return regular, loopback, nil
|
|
}
|
|
|
|
// Interface is a wrapper around Go's net.Interface with some extra methods.
|
|
type Interface struct {
|
|
*net.Interface
|
|
}
|
|
|
|
func (i Interface) IsLoopback() bool { return isLoopback(i.Interface) }
|
|
func (i Interface) IsUp() bool { return isUp(i.Interface) }
|
|
|
|
// ForeachInterfaceAddress calls fn for each interface's address on the machine.
|
|
func ForeachInterfaceAddress(fn func(Interface, netaddr.IP)) error {
|
|
ifaces, err := net.Interfaces()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for i := range ifaces {
|
|
iface := &ifaces[i]
|
|
addrs, err := iface.Addrs()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, a := range addrs {
|
|
switch v := a.(type) {
|
|
case *net.IPNet:
|
|
if ip, ok := netaddr.FromStdIP(v.IP); ok {
|
|
fn(Interface{iface}, ip)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
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 map[string][]netaddr.IP
|
|
InterfaceUp map[string]bool
|
|
|
|
// HaveV6Global is whether this machine has an IPv6 global address
|
|
// on some interface.
|
|
HaveV6Global bool
|
|
|
|
// HaveV4 is whether the machine has some non-localhost IPv4 address.
|
|
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
|
|
}
|
|
|
|
func (s *State) Equal(s2 *State) bool {
|
|
return reflect.DeepEqual(s, s2)
|
|
}
|
|
|
|
// RemoveTailscaleInterfaces modifes s to remove any interfaces that
|
|
// are owned by this process. (TODO: make this true; currently it
|
|
// makes the Linux-only assumption that the interface is named
|
|
// /^tailscale/)
|
|
func (s *State) RemoveTailscaleInterfaces() {
|
|
for name := range s.InterfaceIPs {
|
|
if strings.HasPrefix(name, "tailscale") { // TODO: use --tun flag value, etc; see TODO in method doc
|
|
delete(s.InterfaceIPs, name)
|
|
delete(s.InterfaceUp, name)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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.
|
|
func GetState() (*State, error) {
|
|
s := &State{
|
|
InterfaceIPs: make(map[string][]netaddr.IP),
|
|
InterfaceUp: make(map[string]bool),
|
|
}
|
|
if err := ForeachInterfaceAddress(func(ni Interface, ip netaddr.IP) {
|
|
s.InterfaceIPs[ni.Name] = append(s.InterfaceIPs[ni.Name], ip)
|
|
s.InterfaceUp[ni.Name] = ni.IsUp()
|
|
s.HaveV6Global = s.HaveV6Global || isGlobalV6(ip)
|
|
s.HaveV4 = s.HaveV4 || (ip.Is4() && !ip.IsLoopback())
|
|
}); err != nil {
|
|
return nil, err
|
|
}
|
|
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, ip netaddr.IP) {
|
|
if isPrivateIP(ip) {
|
|
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))
|
|
|
|
}
|
|
|
|
func isPrivateIP(ip netaddr.IP) bool {
|
|
return private1.Contains(ip) || private2.Contains(ip) || private3.Contains(ip)
|
|
}
|
|
|
|
func isGlobalV6(ip netaddr.IP) bool {
|
|
return v6Global1.Contains(ip)
|
|
}
|
|
|
|
func mustCIDR(s string) netaddr.IPPrefix {
|
|
prefix, err := netaddr.ParseIPPrefix(s)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return prefix
|
|
}
|
|
|
|
var (
|
|
private1 = mustCIDR("10.0.0.0/8")
|
|
private2 = mustCIDR("172.16.0.0/12")
|
|
private3 = mustCIDR("192.168.0.0/16")
|
|
cgNAT = mustCIDR("100.64.0.0/10")
|
|
linkLocalIPv4 = mustCIDR("169.254.0.0/16")
|
|
v6Global1 = mustCIDR("2000::/3")
|
|
)
|