121 lines
5.3 KiB
Dart
121 lines
5.3 KiB
Dart
import 'dart:async';
|
|
import 'dart:io';
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
|
|
import '../core/dns_resolver.dart';
|
|
import '../core/local_proxy.dart';
|
|
import '../l10n/tr.dart';
|
|
|
|
/// DNS 서버에 닿지 않았을 때 화면에 알릴 것
|
|
class DnsFailure {
|
|
final String server;
|
|
final String reason;
|
|
final DateTime at;
|
|
const DnsFailure(this.server, this.reason, this.at);
|
|
}
|
|
|
|
/// DNS 강제 (사용자 요청 10/10: "DNS 주소를 강제로 내가 설정한 주소로").
|
|
/// 앱 안 HTTP 프록시 ([LocalProxy] + [DnsConnector]) 를 127.0.0.1 에 띄우고, Tor 밖으로 나가는 앱의 연결을 그리로 보낸다:
|
|
/// - 앱의 HTTP (업데이트 확인 · 컴포넌트 · AI 모델 받기 · WebDAV · 번역 · 자막 검색): [JjHttpOverrides] 의 findProxy
|
|
/// - 웹 브라우저: Windows Edge · Chrome 엔진의 --proxy-server, Android 는 웹 뷰 프록시 (WebProxy)
|
|
/// - 다운로드: yt-dlp --proxy, aria2 --all-proxy (+ --async-dns-server)
|
|
/// 이름은 프록시가 정한 DNS 로 푼다. DNS 서버에 닿지 않으면 기기 DNS 로 몰래 바꾸지 않고 [failure] 로 알린다.
|
|
class NetGuard {
|
|
NetGuard._();
|
|
static final instance = NetGuard._();
|
|
|
|
/// 지금 DNS 설정 (main 에서 환경 설정을 읽게 바꾼다)
|
|
DnsConfig Function() config = () => const DnsConfig.system();
|
|
|
|
late final DnsResolver resolver = DnsResolver(config: () => config());
|
|
LocalProxy? _proxy;
|
|
Future<int>? _starting;
|
|
|
|
/// DNS 서버에 닿지 않음 (화면에 알림 - [시스템 DNS 로 바꾸기])
|
|
final failure = ValueNotifier<DnsFailure?>(null);
|
|
|
|
/// 작업 기록
|
|
void Function(String)? log;
|
|
|
|
/// 프록시 포트 (아직 안 띄웠으면 null)
|
|
int? get port => _proxy?.running == true ? _proxy!.port : null;
|
|
|
|
/// 지금 DNS 를 강제하는지 (설정이 직접 지정 · DoH 이고 프록시가 떠 있음)
|
|
bool get forcing => config().forced && port != null;
|
|
|
|
/// 설정이 강제면 프록시를 띄운다 (이미 떠 있으면 그대로). 설정을 바꾼 뒤에도 부른다
|
|
Future<void> ensure() async {
|
|
resolver.clearCache();
|
|
if (!config().forced) return;
|
|
if (port != null) return;
|
|
_starting ??= () async {
|
|
// 이 앱만 쓰게 (같은 기기의 다른 앱은 407 - [ProxyAuth])
|
|
final p = LocalProxy(DnsConnector(resolver), onError: reportError, auth: ProxyAuth.instance)
|
|
..onRejected = (line, had) => log?.call(had
|
|
? trf('DNS 강제 프록시: 비밀값이 틀린 연결을 거절 ({0})', [line])
|
|
: trf('DNS 강제 프록시: 비밀값이 없는 연결을 거절 ({0})', [line]));
|
|
final n = await p.start();
|
|
_proxy = p;
|
|
log?.call(trf('DNS 강제 프록시 시작: 127.0.0.1:{0} ({1})', [n, config().label]));
|
|
return n;
|
|
}();
|
|
try {
|
|
await _starting;
|
|
} finally {
|
|
_starting = null;
|
|
}
|
|
}
|
|
|
|
DateTime? _lastAlert;
|
|
void reportError(String host, Object e) {
|
|
if (e is! DnsException || !e.unreachable) return;
|
|
log?.call(trf('DNS 실패 ({0}): {1}', [host, e]));
|
|
final now = DateTime.now();
|
|
// 같은 알림을 계속 띄우지 않게 (1분에 한 번)
|
|
if (_lastAlert != null && now.difference(_lastAlert!) < const Duration(minutes: 1)) return;
|
|
_lastAlert = now;
|
|
failure.value = DnsFailure(e.server.isEmpty ? config().label : e.server, e.message, now);
|
|
}
|
|
|
|
/// 이 주소는 프록시를 거치지 않는다: IP 주소 (DNS 를 묻지 않음) · 같은 집 안 이름 (LAN · NAS · Tailscale)
|
|
static bool isDirectHost(String host) => host.isEmpty || InternetAddress.tryParse(host) != null || isLocalName(host);
|
|
|
|
/// Dart HttpClient 의 findProxy
|
|
String findProxy(Uri u) {
|
|
if (!config().forced || isDirectHost(u.host)) return 'DIRECT';
|
|
final p = port;
|
|
// 강제인데 프록시가 아직 없으면 직접 잇지 않는다 (닫힌 포트로 보내 실패하게 - 몰래 기기 DNS 를 쓰지 않음)
|
|
if (p == null) return 'PROXY 127.0.0.1:9';
|
|
final a = ProxyAuth.instance;
|
|
return 'PROXY ${a.user}:${a.password}@127.0.0.1:$p';
|
|
}
|
|
|
|
/// 웹 브라우저 (Edge · Chrome 엔진) 에 줄 프록시 주소. 강제가 아니면 null.
|
|
/// 인증 (비밀값) 은 주소에 넣지 않고 브라우저가 물을 때 답한다 ([ProxyAuth])
|
|
String? get browserProxy => forcing ? 'http://127.0.0.1:$port' : null;
|
|
|
|
/// yt-dlp 인수
|
|
List<String> ytDlpArgs() => forcing ? ['--proxy', ProxyAuth.instance.url(port!)] : const [];
|
|
|
|
/// aria2 인수: http · https · ftp 와 HTTP 트래커는 프록시로, UDP 트래커 · DHT 의 이름은 정한 서버로 ([asyncDns] 기능이 있는 aria2 만)
|
|
List<String> aria2Args({required bool asyncDns}) {
|
|
if (!forcing) return const [];
|
|
final c = config();
|
|
final servers = c.mode == 'custom' ? c.servers.map((a) => a.address).toList() : const <String>[];
|
|
return [
|
|
'--all-proxy=http://127.0.0.1:$port',
|
|
'--all-proxy-user=${ProxyAuth.instance.user}',
|
|
'--all-proxy-passwd=${ProxyAuth.instance.password}',
|
|
if (asyncDns && servers.isNotEmpty) ...['--async-dns=true', '--async-dns-server=${servers.join(',')}'],
|
|
];
|
|
}
|
|
}
|
|
|
|
/// 앱의 모든 HttpClient 가 [NetGuard.findProxy] 를 따르게 (main 에서 HttpOverrides.global 로)
|
|
class JjHttpOverrides extends HttpOverrides {
|
|
@override
|
|
HttpClient createHttpClient(SecurityContext? context) =>
|
|
super.createHttpClient(context)..findProxy = NetGuard.instance.findProxy;
|
|
}
|