From bb91c2e6d60141f01c9574ea7a10e672cdf4840b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sybren=20A=2E=20St=C3=BCvel?= Date: Tue, 8 Mar 2022 11:03:48 +0100 Subject: [PATCH] UPnP/SSDP: prefer link-local addresses if available --- internal/own_url/interfaces.go | 77 +++++++++++++++++++++-------- internal/own_url/interfaces_test.go | 76 ++++++++++++++++++++++++++++ 2 files changed, 133 insertions(+), 20 deletions(-) create mode 100644 internal/own_url/interfaces_test.go diff --git a/internal/own_url/interfaces.go b/internal/own_url/interfaces.go index 1ac140d1..61c93906 100644 --- a/internal/own_url/interfaces.go +++ b/internal/own_url/interfaces.go @@ -78,16 +78,10 @@ func networkInterfaces(includeLinkLocal, includeLocalhost bool) ([]net.IP, error Str("iface", iface.Name). Logger() switch { - case ip.IsMulticast(): - logger.Debug().Msg(" - skipping multicast") case ip.IsMulticast(): logger.Debug().Msg(" - skipping multicast") case ip.IsUnspecified(): logger.Debug().Msg(" - skipping unspecified") - case !includeLinkLocal && ip.IsLinkLocalUnicast(): - logger.Debug().Msg(" - skipping link-local") - case !includeLocalhost && ip.IsLoopback(): - logger.Debug().Msg(" - skipping localhost") default: logger.Debug().Msg(" - usable") ifaceAddresses = append(ifaceAddresses, ip) @@ -104,26 +98,69 @@ func networkInterfaces(includeLinkLocal, includeLocalhost bool) ([]net.IP, error return usableAddresses, nil } -// filterAddresses removes "privacy extension" addresses. -// It assumes the list of addresses belong to the same network interface, and -// that the OS reports preferred (i.e. private/random) addresses before -// non-random ones. +// filterAddresses reduces the number of IPv6 addresses. +// It prefers link-local addresses; if these are in the list, all the other IPv6 +// addresses will be removed. Link-local addresses are stable and meant for +// same-network connections, which is exactly what Flamenco needs. +// Loopback addresses (localhost) are always filtered out, unless they're the only addresses available. func filterAddresses(addrs []net.IP) []net.IP { - keep := make([]net.IP, 0) + keepAddrs := make([]net.IP, 0) - var lastSeenIP net.IP + if hasOnlyLoopback(addrs) { + return addrs + } + + var keepLinkLocalv6 = hasLinkLocalv6(addrs) + var keepLinkLocalv4 = hasLinkLocalv4(addrs) + + var keep bool for _, addr := range addrs { - if addr.To4() != nil { - // IPv4 addresses are always kept. - keep = append(keep, addr) + if addr.IsLoopback() { continue } - lastSeenIP = addr - } - if len(lastSeenIP) > 0 { - keep = append(keep, lastSeenIP) + isv4 := isIPv4(addr) + if isv4 { + keep = keepLinkLocalv4 == addr.IsLinkLocalUnicast() + } else { + keep = keepLinkLocalv6 == addr.IsLinkLocalUnicast() + } + + if keep { + keepAddrs = append(keepAddrs, addr) + } } - return keep + return keepAddrs +} + +func isIPv4(addr net.IP) bool { + return addr.To4() != nil +} + +func hasLinkLocalv6(addrs []net.IP) bool { + for _, addr := range addrs { + if !isIPv4(addr) && addr.IsLinkLocalUnicast() { + return true + } + } + return false +} + +func hasLinkLocalv4(addrs []net.IP) bool { + for _, addr := range addrs { + if isIPv4(addr) && addr.IsLinkLocalUnicast() { + return true + } + } + return false +} + +func hasOnlyLoopback(addrs []net.IP) bool { + for _, addr := range addrs { + if !addr.IsLoopback() { + return false + } + } + return true } diff --git a/internal/own_url/interfaces_test.go b/internal/own_url/interfaces_test.go new file mode 100644 index 00000000..3832c219 --- /dev/null +++ b/internal/own_url/interfaces_test.go @@ -0,0 +1,76 @@ +package own_url + +// SPDX-License-Identifier: GPL-3.0-or-later + +import ( + "net" + "testing" + + "github.com/stretchr/testify/assert" +) + +var ( + globalIPv6 = net.ParseIP("2a10:3780:2:52:185:93:175:46") + lanIPv6 = globalIPv6 + linkLocalIPv6 = net.ParseIP("fe80::5054:ff:fede:2ad7") + localhostIPv6 = net.ParseIP("::1") + + globalIPv4 = net.ParseIP("8.8.8.8") + lanIPv4 = net.ParseIP("192.168.0.1") + linkLocalIPv4 = net.ParseIP("169.254.47.42") + localhostIPv4 = net.ParseIP("127.0.0.1") +) + +func Test_filterAddresses(t *testing.T) { + tests := []struct { + name string + expect []net.IP + input []net.IP + }{ + // IPv6 tests: + // Not a link-local address present, then use all but localhost + {"IPv6 without link-local", + []net.IP{globalIPv6, lanIPv6}, + []net.IP{globalIPv6, lanIPv6, localhostIPv6}}, + // Link-local address present, just use that one. + {"IPv6 with link-local", + []net.IP{linkLocalIPv6}, + []net.IP{linkLocalIPv6, lanIPv6, localhostIPv6}}, + // Only loopback + {"IPv6 with only loopback", + []net.IP{localhostIPv6}, + []net.IP{localhostIPv6}}, + + // IPv4 tests: + // Not a link-local address present, then use all but localhost + {"IPv4 without link-local", + []net.IP{globalIPv4, lanIPv4}, + []net.IP{globalIPv4, lanIPv4, localhostIPv4}}, + // Link-local address present, just use that one. + {"IPv4 with link-local", + []net.IP{linkLocalIPv4}, + []net.IP{linkLocalIPv4, lanIPv4, localhostIPv4}}, + // Only loopback + {"IPv4 with only loopback", + []net.IP{localhostIPv4}, + []net.IP{localhostIPv4}}, + + // Mixed IPv4/IPv6 tests: + // IPv4 no link-local, but IPv6 with link-local: + {"IPv4 w/o, IPv6 w/ link-local", + []net.IP{lanIPv4, linkLocalIPv6}, + []net.IP{lanIPv4, localhostIPv4, lanIPv6, linkLocalIPv6}}, + // IPv4 link-local, IPv6 without: + {"IPv4 w/, IPv4 w/o link-local", + []net.IP{linkLocalIPv4, lanIPv6}, + []net.IP{linkLocalIPv4, lanIPv4, lanIPv6}}, + // Only loopback + {"IPv4 + IPv6 with only loopback", + []net.IP{localhostIPv4, localhostIPv6}, + []net.IP{localhostIPv4, localhostIPv6}}, + } + for _, tt := range tests { + got := filterAddresses(tt.input) + assert.EqualValues(t, tt.expect, got, "for test %q", tt.name) + } +}