Repository: syncthing/syncthing (language: go)

Issue:
Unable to establish/maintain multiple connections due to connection priority error
### What happened?

Connections are getting unexpectedly closed ("replacing connection" on one side, "connection reset by peer" on the other) for no discernible reason, making it so that more than one connection never stays open despite both sides being configured for eight connections. Problem seems to be incorrect connection priorities. Linux side drops the link local IPv6 connection:

```
[PSEUD] 2024/09/20 09:25:37.501964 service.go:1453: DEBUG: Closing connection [fd7a:115c:a1e0::5501:4880]:22000-[fd7a:115c:a1e0::7301:283d]:22000/tcp-server/TLS1.3-TLS_AES_128_GCM_SHA256/WAN-P30-5VMT6ACVKOPSMK1QVES23Q3HUO to HX2ELNU with priority 30 (cutoff 20)
```

Priority 30 in my setup is "TCP WAN", while this is clearly a LAN connection.

Mac side keeps dialing those connections, correctly classifying them as TCP-LAN (priority 10):

```
2024-09-20 11:23:32 dialing PSEUDOP-YLA5DBR-XO2HBNT-2FOG4AC-SWOW5EZ-H3QQY76-GISB7PG-E2SWOA2 tcp://[fd7a:115c:a1e0::5501:4880]:22000 success: [fd7a:115c:a1e0::7301:283d]:22000-[fd7a:115c:a1e0::5501:4880]:22000/tcp-client/TLS1.3-TLS_AES_128_GCM_SHA256/LAN-P10-
```

### Syncthing version

v1.27.13-rc.1

### Platform & operating system

linux/mac

### Browser version

_No response_

### Relevant log output

_No response_


Discussion / hints:
So actually the problem here seems to be not Syncthing but my networking setup. The fd7a address is Tailscale, and how that is set up network-wise differs between Mac and Linux. On Mac there is a tunnel interface:

```
utun4: flags=8051<UP,POINTOPOINT,RUNNING,MULTICAST> mtu 1280
	options=6460<TSO4,TSO6,CHANNEL_IO,PARTIAL_CSUM,ZEROINVERT_CSUM>
	inet6 fe80::f0ed:7e22:2e82:5b83%utun4 prefixlen 64 scopeid 0x16
	inet 100.85.40.61 --> 100.85.40.61 netmask 0xffffffff
	inet6 fd7a:115c:a1e0::7301:283d prefixlen 48
	nd6 options=201<PERFORMNUD,DAD>
```

Note prefixlen 48 on the fd7a address, so Syncthing will see fd7a:115c:a1e0::/48 as a local network.

On Linux, the interface is this:

```
135: tailscale0: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> mtu 1280 qdisc fq_codel state UNKNOWN group default qlen 500
    link/none
    inet 100.116.72.128/32 scope global tailscale0
       valid_lft forever preferred_lft forever
    inet6 fd7a:115c:a1e0::5501:4880/128 scope global
       valid_lft forever preferred_lft forever
    inet6 fe80::d865:aa9f:518a:9904/64 scope link stable-privacy
       valid_lft forever preferred_lft forever
```

That is, a /128 address and nothing else, accompanied by a route for the /48 (not shown here), but that means Syncthing doesn't see it as a LAN.

And really, the Linux side does it better here, because there is no reason in my setup for the connections to go via Tailscale when there is an alternative direct IPv4 network they can use...
Is there a valid reason to not treat all ULAs as LAN? If we're able to dial them, it's some kind of LAN. We're not able to classify VPN'ed connections as WAN either way.
In my case, this can be worked around by either adding fc00::/7 to always local networks (if I wanted connections over Tailscale) or setting allowed networks for the device to something like `0.0.0.0/0, fe80:/10, 2000::/3` to allow IPv4 and IPv6 except ULA.
Well, they're not LAN, in this case it would go via a relay in another country (because reasons). The only problematic thing here is that the classification differs per device since the network setup is different on the two devices. On the Mac it _looks_ like a LAN interface, on Linux it doesn't. 🤷 
Most of my clients are also reachable via an IPv4 in the `10.0.0.0/8` subnet if my VPN connection is up. Syncthing would proudly tell me it has established a LAN connection.

The only difference is that the VPN interface knows about the assigned subnet which Syncthing uses to classify the IP as LAN.

My point is that our current logic fails for the simplest VPN setup while treating any kind of routed subnets in homelab or enterprise networks as WAN.

I'd ditch the interface subnet checks and strictly check by IP prefix.
I'd frankly be inclined to go in the opposite direction and check the interface type, if it's point-to-point it's not LAN.
If we *really* want a meaningful LAN detection we would need to probe the latency of the connection.
We don't need that though, we just need it to be mostly consistent.
> I'd frankly be inclined to go in the opposite direction and check the interface type, if it's point-to-point it's not LAN.

That would drastically reduce false-positives if we want to improve VPN detection :+1:

> We don't need that though, we just need it to be mostly consistent.

I'd argue that classifying by IP prefix would be more consistent.

Is the target IP part of a non-globally routable subnet like `10.0.0.0/8` or `fc00::/7` ? It's either a proper LAN scenario or a virtual one(VPN).
Mmm yes but we do it for a specific reason, to try to select a connection that is more likely to be network-wise close and hence faster and more efficient. Given the increasing sprawl of mesh vpn things (awesome stuff!) there's less and less reason to assume that just because it's not publicly routable it's local.
> there's less and less reason to assume that just because it's not publicly routable it's local.

Good point. Maybe I find some time this week and draft a PR to check the point-to-point flag.

Fix patch:
diff --git a/lib/osutil/net.go b/lib/osutil/net.go
index eeb8bafde26..4400d8f23f0 100644
--- a/lib/osutil/net.go
+++ b/lib/osutil/net.go
@@ -11,21 +11,26 @@ import (
 )
 
 func GetLans() ([]*net.IPNet, error) {
-	ifs, err := net.Interfaces()
+	intfs, err := net.Interfaces()
 	if err != nil {
 		return nil, err
 	}
 	var addrs []net.Addr
 
-	for _, currentIf := range ifs {
-		if currentIf.Flags&net.FlagRunning == 0 {
+	for _, intf := range intfs {
+		if intf.Flags&net.FlagRunning == 0 {
 			continue
 		}
-		currentAddrs, err := currentIf.Addrs()
+		if intf.Flags&net.FlagPointToPoint != 0 {
+			// Point-to-point interfaces are typically VPNs and similar
+			// which, for our purposes, do not qualify as LANs.
+			continue
+		}
+		intfAddrs, err := intf.Addrs()
 		if err != nil {
 			return nil, err
 		}
-		addrs = append(addrs, currentAddrs...)
+		addrs = append(addrs, intfAddrs...)
 	}
 
 	nets := make([]*net.IPNet, 0, len(addrs))


Test patch: