flamenco/internal/own_url/interfaces.go

191 lines
5.5 KiB
Go

package own_url
/* (c) 2019, Blender Foundation - Sybren A. Stüvel
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import (
"bytes"
"errors"
"fmt"
"net"
"sort"
"github.com/rs/zerolog/log"
)
var (
// ErrNoInterface is returned when no network interfaces with a real IP-address were found.
ErrNoInterface = errors.New("no network interface found")
)
// networkInterfaces returns a list of interface addresses.
// Only those addresses that can be reached by a unicast TCP/IP connection are returned.
func networkInterfaces() ([]net.IP, error) {
log.Trace().Msg("iterating over all network interfaces")
interfaces, err := net.Interfaces()
if err != nil {
return []net.IP{}, err
}
usableAddresses := make([]net.IP, 0)
for _, iface := range interfaces {
if iface.Flags&net.FlagUp == 0 {
log.Trace().Str("interface", iface.Name).Msg("skipping down interface")
continue
}
addrs, err := iface.Addrs()
if err != nil {
continue
}
ifaceAddresses := make([]net.IP, 0)
for k := range addrs {
var ip net.IP
switch a := addrs[k].(type) {
case *net.IPAddr:
ip = a.IP
case *net.IPNet:
ip = a.IP
default:
log.Warn().
Interface("addr", addrs[k]).
Str("type", fmt.Sprintf("%T", addrs[k])).
Msg(" - skipping unknown interface type")
continue
}
logger := log.With().
Interface("ip", ip).
Str("iface", iface.Name).
Logger()
switch {
case ip.IsMulticast():
logger.Trace().Msg(" - skipping multicast")
case ip.IsUnspecified():
logger.Trace().Msg(" - skipping unspecified")
default:
logger.Trace().Msg(" - potentially usable")
ifaceAddresses = append(ifaceAddresses, ip)
}
}
usableAddresses = append(usableAddresses, filterAddresses(ifaceAddresses)...)
}
if len(usableAddresses) == 0 {
return usableAddresses, ErrNoInterface
}
sort.Slice(usableAddresses, func(i, j int) bool {
// Sort loopback addresses after others.
if usableAddresses[i].IsLoopback() != usableAddresses[j].IsLoopback() {
return usableAddresses[j].IsLoopback()
}
// Sort IPv4 before IPv6, because people are likely to be more familiar with
// them.
if isIPv4(usableAddresses[i]) != isIPv4(usableAddresses[j]) {
return isIPv4(usableAddresses[i])
}
// Otherwise just order lexicographically.
return bytes.Compare(usableAddresses[i], usableAddresses[j]) < 0
})
return usableAddresses, nil
}
// filterAddresses reduces the number of IP addresses.
//
// The function prefers non-link-local addresses over link-local ones.
// Link-local addresses are stable and meant for same-network connections, but
// they require a "zone index", typically the interface name, so something like
// `[fe80::cafe:f00d%eth0]`. This is not supported by webbrowsers. Furthermore,
// they require the interface name of the side initiating the connection,
// whereas this code is used to answer the question "how can this machine be
// reached?".
//
// Source: https://stackoverflow.com/a/52972417/875379
//
// Loopback addresses (localhost) are always filtered out, unless they're the
// only addresses available.
func filterAddresses(addrs []net.IP) []net.IP {
keepAddrs := make([]net.IP, 0)
keepLinkLocalv4 := hasOnlyLinkLocalv4(addrs)
for _, addr := range addrs {
if addr.IsLoopback() {
continue
}
isv4 := isIPv4(addr)
var keep bool
if isv4 {
keep = keepLinkLocalv4 == addr.IsLinkLocalUnicast()
} else {
// Never keep IPv6 link-local addresses. They need a "zone index" to work,
// and those can only be determined on the connecting side. Furthermore,
// they're incompatible with most webbrowsers.
keep = !addr.IsLinkLocalUnicast()
}
if keep {
keepAddrs = append(keepAddrs, addr)
}
}
// Only when after the filtering there is nothing left, add the loopback
// addresses. This is likely a bit of a strange test, because either this is a
// loopback device (and should only have loopback addresses) or it is not (and
// should only have non-loopback addresses). It does make the code reliable
// even when things are mixed, which is nice.
if len(keepAddrs) == 0 {
for _, addr := range addrs {
if addr.IsLoopback() {
keepAddrs = append(keepAddrs, addr)
}
}
}
return keepAddrs
}
func isIPv4(addr net.IP) bool {
return addr.To4() != nil
}
func hasOnlyLinkLocalv4(addrs []net.IP) bool {
hasLinkLocalv4 := false
for _, addr := range addrs {
// Only consider non-loopback IPv4 addresses.
if addr.IsLoopback() || !isIPv4(addr) {
continue
}
if !addr.IsLinkLocalUnicast() {
return false
}
hasLinkLocalv4 = true
}
return hasLinkLocalv4
}