fix(network): priorizar Ethernet e APIPA no multicast

LocalNetworkInfo prefere cabo antes de Wi-Fi e 169.254.x na interface;
documenta escolha de interface no apendice de multicast.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Gabriel Felipe Souza SantosandCursor committed 2026-10-27 15:39:00 -03:00
1 parent 39c1e504bb
commit 8fe9bee453
2 files changed
+77 -13

No files matched your search

@@ -29,6 +29,10 @@ Executar o Pitiupi no WSL e peers no Windows (ou vice-versa) pode falhar em mult
- todos os peers no mesmo ambiente (Linux nativo ou VMs), ou - todos os peers no mesmo ambiente (Linux nativo ou VMs), ou
- dois PCs físicos na mesma sub-rede. - dois PCs físicos na mesma sub-rede.
## Escolha de interface (multicast)
O núcleo associa o socket multicast à interface de rede preferida: **cabo (Ethernet) antes de Wi‑Fi**; em cabo com link-local APIPA, prioriza IPv4 **`169.254.*`**. Isso não substitui a exigência de **mesma sub-rede** entre os dois PCs — `169.254.x` e `192.168.x` ao mesmo tempo em hosts diferentes não compartilham heartbeat.
## Checklist — dois PCs na mesma Wi‑Fi ## Checklist — dois PCs na mesma Wi‑Fi
1. **Mesma porta no login** (ex.: `5000` nos dois). Portas diferentes isolam a sala. 1. **Mesma porta no login** (ex.: `5000` nos dois). Portas diferentes isolam a sala.
@@ -11,14 +11,19 @@ import java.util.Locale;
*/ */
public final class LocalNetworkInfo { public final class LocalNetworkInfo {
private static final String APIPA_PREFIX = "169.254.";
private LocalNetworkInfo() { private LocalNetworkInfo() {
} }
/** /**
* Interface preferida para LAN (Wi‑Fi/cabo), ignorando bridges Docker/WSL/VPN virtuais quando possível. * Interface preferida para LAN: cabo antes de Wi‑Fi; APIPA (169.254.x) antes de outro IPv4 no cabo.
*/ */
public static NetworkInterface preferredNetworkInterface() { public static NetworkInterface preferredNetworkInterface() {
try { try {
NetworkInterface wiredApipa = null;
NetworkInterface wiredOther = null;
NetworkInterface wireless = null;
NetworkInterface fallback = null; NetworkInterface fallback = null;
Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces(); Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
while (interfaces.hasMoreElements()) { while (interfaces.hasMoreElements()) {
@@ -26,12 +31,30 @@ public final class LocalNetworkInfo {
if (!isCandidateInterface(ni)) { if (!isCandidateInterface(ni)) {
continue; continue;
} }
if (isPreferredLanInterfaceName(ni.getName())) {
return ni;
}
if (fallback == null) { if (fallback == null) {
fallback = ni; fallback = ni;
} }
String name = ni.getName();
if (isWiredLanInterfaceName(name)) {
if (hasApipaIpv4(ni)) {
if (wiredApipa == null) {
wiredApipa = ni;
}
} else if (wiredOther == null) {
wiredOther = ni;
}
} else if (isWirelessLanInterfaceName(name) && wireless == null) {
wireless = ni;
}
}
if (wiredApipa != null) {
return wiredApipa;
}
if (wiredOther != null) {
return wiredOther;
}
if (wireless != null) {
return wireless;
} }
return fallback; return fallback;
} catch (Exception ex) { } catch (Exception ex) {
@@ -43,7 +66,7 @@ public final class LocalNetworkInfo {
try { try {
NetworkInterface preferred = preferredNetworkInterface(); NetworkInterface preferred = preferredNetworkInterface();
if (preferred != null) { if (preferred != null) {
String fromPreferred = firstIpv4OnInterface(preferred); String fromPreferred = bestIpv4OnInterface(preferred);
if (fromPreferred != null) { if (fromPreferred != null) {
return fromPreferred; return fromPreferred;
} }
@@ -54,7 +77,7 @@ public final class LocalNetworkInfo {
if (!isCandidateInterface(ni)) { if (!isCandidateInterface(ni)) {
continue; continue;
} }
String ip = firstIpv4OnInterface(ni); String ip = bestIpv4OnInterface(ni);
if (ip != null) { if (ip != null) {
return ip; return ip;
} }
@@ -80,7 +103,7 @@ public final class LocalNetworkInfo {
if (ni.isVirtual() || isVirtualInterfaceName(ni.getName())) { if (ni.isVirtual() || isVirtualInterfaceName(ni.getName())) {
return false; return false;
} }
return firstIpv4OnInterface(ni) != null; return bestIpv4OnInterface(ni) != null;
} }
private static boolean isVirtualInterfaceName(String name) { private static boolean isVirtualInterfaceName(String name) {
@@ -97,18 +120,55 @@ public final class LocalNetworkInfo {
|| n.startsWith("tailscale"); || n.startsWith("tailscale");
} }
private static boolean isPreferredLanInterfaceName(String name) { private static boolean isWiredLanInterfaceName(String name) {
if (name == null) {
return false;
}
String n = name.toLowerCase(Locale.ROOT);
return n.startsWith("eth")
|| n.startsWith("eno")
|| n.startsWith("enp")
|| n.startsWith("ens");
}
private static boolean isWirelessLanInterfaceName(String name) {
if (name == null) { if (name == null) {
return false; return false;
} }
String n = name.toLowerCase(Locale.ROOT); String n = name.toLowerCase(Locale.ROOT);
return n.startsWith("wlan") return n.startsWith("wlan")
|| n.startsWith("wlp") || n.startsWith("wlp")
|| n.startsWith("wl") || n.startsWith("wl");
|| n.startsWith("eth") }
|| n.startsWith("eno")
|| n.startsWith("enp") private static boolean hasApipaIpv4(NetworkInterface ni) {
|| n.startsWith("ens"); return apipaIpv4OnInterface(ni) != null;
}
private static String apipaIpv4OnInterface(NetworkInterface ni) {
try {
Enumeration<InetAddress> addresses = ni.getInetAddresses();
while (addresses.hasMoreElements()) {
InetAddress addr = addresses.nextElement();
if (addr instanceof Inet4Address && !addr.isLoopbackAddress()) {
String host = addr.getHostAddress();
if (host != null && host.startsWith(APIPA_PREFIX)) {
return host;
}
}
}
} catch (Exception ignored) {
return null;
}
return null;
}
private static String bestIpv4OnInterface(NetworkInterface ni) {
String apipa = apipaIpv4OnInterface(ni);
if (apipa != null) {
return apipa;
}
return firstIpv4OnInterface(ni);
} }
private static String firstIpv4OnInterface(NetworkInterface ni) { private static String firstIpv4OnInterface(NetworkInterface ni) {