import 'dart:async'; import 'dart:io'; /// X-plore 비교 j: 같은 망에서 SMB (445) 서버 찾기 - 이 기기의 IPv4 망 (/24) 을 짧은 연결 시도로 훑는다. /// 연결만 해 보고 바로 닫는다 (아무것도 보내지 않음). 찾은 것은 이름 (역방향 조회, 모르면 null) 과 함께. /// SMB 서버 추가 창 (88 Android SMB) 의 [찾기] 에서 쓴다. class LanHost { final String address; final String? name; const LanHost(this.address, this.name); } /// 보안 검토 4: Wi-Fi · 유선 연결인지 (이름으로). 모바일 데이터 (통신사 망 - 다른 가입자) · 가상 어댑터 (WSL · Hyper-V · /// VirtualBox · VMware · Docker) · VPN 은 아니다. Wi-Fi 공유 (89) 도 이것으로 고른다 bool isLanInterfaceName(String name) { final n = name.toLowerCase(); return !_mobileIf.hasMatch(n) && !_virtualIf.hasMatch(n); } /// 모바일 데이터 (Android: rmnet · ccmni · pdp · v4-rmnet 등) bool isMobileInterfaceName(String name) => _mobileIf.hasMatch(name.toLowerCase()); final _mobileIf = RegExp(r'^(v4-)?(rmnet|ccmni|pdp|seth|wwan|usb_rmnet|rev_rmnet|clat)'); final _virtualIf = RegExp(r'vethernet|hyper-v|virtualbox|vbox|vmware|vmnet|wsl|docker|^br-|^veth|virbr|^tun|^tap|^ppp|^ipsec|wireguard|^wg|zerotier|openvpn|tailscale'); /// 사설망 IPv4 (10/8 · 172.16/12 · 192.168/16) 인지 bool isPrivateLanAddress(String ip) { final v = ip.split('.').map(int.tryParse).toList(); if (v.length != 4 || v.any((x) => x == null || x < 0 || x > 255)) return false; return v[0] == 10 || (v[0] == 172 && v[1]! >= 16 && v[1]! <= 31) || (v[0] == 192 && v[1] == 168); } /// 이 기기의 사설 IPv4 주소들 (Wi-Fi · 유선만 - Tailscale 100.64/10 은 넓어 훑지 않고, 모바일 데이터 · 가상 어댑터 · VPN 도 뺀다) Future> localPrivateIPv4() async { final out = []; for (final ni in await NetworkInterface.list(type: InternetAddressType.IPv4)) { if (!isLanInterfaceName(ni.name)) continue; for (final a in ni.addresses) { final b = a.rawAddress; final private = b[0] == 10 || (b[0] == 172 && b[1] >= 16 && b[1] <= 31) || (b[0] == 192 && b[1] == 168); if (private && !a.isLoopback) out.add(a); } } return out; } /// [hosts] 의 [port] 가 열려 있는지 동시에 [parallel] 개씩 본다. 열린 것만 (주소 순서대로) Future> scanPort( List hosts, { int port = 445, Duration timeout = const Duration(milliseconds: 400), int parallel = 64, bool Function()? cancelled, void Function(int done, int total)? onProgress, bool lookupNames = true, }) async { final open = {}; var next = 0, done = 0; Future worker() async { while (next < hosts.length) { if (cancelled?.call() ?? false) return; final h = hosts[next++]; try { final s = await Socket.connect(h, port, timeout: timeout); open.add(h); s.destroy(); } catch (_) {} onProgress?.call(++done, hosts.length); } } await Future.wait([for (var i = 0; i < parallel; i++) worker()]); final sorted = hosts.where(open.contains).toList(); final out = []; for (final h in sorted) { String? name; if (lookupNames) { try { final r = await InternetAddress(h).reverse().timeout(const Duration(seconds: 1)); if (r.host != h) name = r.host; } catch (_) {} } out.add(LanHost(h, name)); } return out; } /// 이 기기의 사설망 (/24 마다) 의 SMB 서버들 Future> findSmbServers({bool Function()? cancelled, void Function(int done, int total)? onProgress}) async { final hosts = []; for (final a in await localPrivateIPv4()) { final b = a.rawAddress; for (var i = 1; i < 255; i++) { final h = '${b[0]}.${b[1]}.${b[2]}.$i'; if (!hosts.contains(h)) hosts.add(h); } } return scanPort(hosts, cancelled: cancelled, onProgress: onProgress); }