JJ_MKVMaker/lib/core/lan_scan.dart

136 lines
5.6 KiB
Dart

import 'dart:async';
import 'dart:io';
import 'package:flutter/services.dart';
/// 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');
/// 28 APK 점검 · 관리자 판단 (10/19): Android 의 망 종류 (ConnectivityManager · NetworkCapabilities) - 주소 → "wifi" · "ethernet" ·
/// "cellular" · "vpn" · "other". 판단하지 못하면 (Android 가 아님 · 오류) null → 연결 이름으로 (예전 방식). 시험에서 바꾼다
Future<Map<String, String>?> Function() netKindsOf = _androidNetKinds;
Future<Map<String, String>?> _androidNetKinds() async {
if (!Platform.isAndroid) return null;
try {
final m = await const MethodChannel('jj_mkvmaker/android').invokeMethod<Map<Object?, Object?>>('netKinds');
return m?.map((k, v) => MapEntry('$k', '$v'));
} catch (_) {
return null;
}
}
/// X-plore 비교 5-1 ② (관리자 지시 10/21): 지금 쓰는 망이 데이터 요금제인지 (모바일 데이터 · 요금제 Wi-Fi). Android 만 묻고,
/// 그 밖 (PC) 은 false. 물을 수 없으면 true (받지 않는 쪽으로). 앱은 PlatformServices.isMetered 로 부른다 (설계 M3 ThumbPolicy - 시험은 그것을 바꿈)
Future<bool> deviceMetered() async {
if (!Platform.isAndroid) return false;
try {
return await const MethodChannel('jj_mkvmaker/android').invokeMethod<bool>('activeMetered') ?? true;
} catch (_) {
return true;
}
}
/// 집 안 망 (Wi-Fi · 유선) 의 주소인지: Android 가 알려 준 망 종류가 있으면 그것으로, 없으면 연결 이름으로
bool isLanAddress(String ip, String interfaceName, Map<String, String>? kinds) {
final k = kinds?[ip];
if (k != null) return k == 'wifi' || k == 'ethernet';
return isLanInterfaceName(interfaceName);
}
/// 사설망 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<List<InternetAddress>> localPrivateIPv4() async {
final out = <InternetAddress>[];
final kinds = await netKindsOf();
for (final ni in await NetworkInterface.list(type: InternetAddressType.IPv4)) {
for (final a in ni.addresses) {
if (!isLanAddress(a.address, ni.name, kinds)) continue;
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<List<LanHost>> scanPort(
List<String> 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 = <String>{};
var next = 0, done = 0;
Future<void> 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 = <LanHost>[];
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<List<LanHost>> findSmbServers({bool Function()? cancelled, void Function(int done, int total)? onProgress}) async {
final hosts = <String>[];
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);
}