Files

228 lines
6.4 KiB
Dart

import 'package:pigeon/pigeon.dart';
@ConfigurePigeon(
PigeonOptions(
dartPackageName: 'flutter_singbox_proxy',
dartOut: 'lib/src/singbox_proxy_api.g.dart',
dartOptions: DartOptions(),
kotlinOut:
'android/src/main/kotlin/eu/weblibre/flutter_singbox_proxy/generated/SingboxProxyApi.g.kt',
kotlinOptions: KotlinOptions(
package: 'eu.weblibre.flutter_singbox_proxy.generated',
),
),
)
enum SingboxProxyProfileType {
socks,
http,
shadowsocks,
vmess,
vless,
trojan,
naive,
hysteria,
hysteria2,
tuic,
ssh,
wireguard,
shadowTls,
anyTls,
customOutbound,
}
enum SingboxProxyRuntimeStatus { stopped, starting, running, stopping, error }
class SingboxProxyProfile {
SingboxProxyProfile({
required this.id,
required this.name,
required this.type,
required this.configJson,
this.secretJson,
});
final String id;
final String name;
final SingboxProxyProfileType type;
/// Public profile configuration as JSON. The schema is intentionally owned by
/// the profile type so the Pigeon API stays stable while sing-box evolves.
final String configJson;
/// Resolved secret values as JSON. Flutter stores secrets independently and
/// only passes them to native code when building or starting a runtime config.
final String? secretJson;
}
class SingboxProxyRuntimeOptions {
SingboxProxyRuntimeOptions({
this.preferredBasePort,
this.blockUnmatchedTraffic = true,
this.dnsConfig,
this.bootstrapDohUrl,
});
/// Optional preferred start port for generated local SOCKS inbounds.
final int? preferredBasePort;
/// If true, traffic entering sing-box without a matching inbound rule is
/// rejected instead of falling through to direct.
final bool blockUnmatchedTraffic;
/// Optional DNS block emitted into sing-box config. When null, sing-box
/// uses its built-in default (system resolver), which can leak DNS outside
/// the proxy — callers should always provide an explicit configuration.
final SingboxProxyDnsConfig? dnsConfig;
/// DoH endpoint used by the native LocalDNSTransport bridge to bootstrap
/// hostname-only DNS server addresses (and any other hostname appearing in
/// the sing-box config). When null, the bridge refuses to resolve and
/// sing-box's stock `/etc/resolv.conf`/127.0.0.1:53 path runs — which is
/// broken on Android. Callers should always pass the browser DoH URL.
final String? bootstrapDohUrl;
}
class SingboxProxyDnsServerConfig {
SingboxProxyDnsServerConfig({
required this.tag,
required this.address,
this.detourTag,
this.matchDomainSuffixes = const <String>[],
this.matchGeosites = const <String>[],
this.matchOutbounds = const <String>[],
this.matchInbounds = const <String>[],
});
/// sing-box server tag, used to reference the server from `dns.rules`.
final String tag;
/// Server address. `https://...`, `tls://...`, `quic://...`, or plain IP.
/// Hostnames are resolved on demand via the platform LocalDNSTransport
/// bridge (sing-box `type: "local"` with our DoH-backed implementation).
final String address;
/// Outbound tag to dial the resolver through, or `direct` for direct, or
/// null when sing-box should pick automatically.
final String? detourTag;
/// If non-empty, attaches a `dns.rules` entry routing matching domains to
/// this server.
final List<String> matchDomainSuffixes;
/// Advanced sing-box geosite selectors (e.g. `geosite:cn`).
final List<String> matchGeosites;
/// If non-empty, attaches a `dns.rules` entry routing queries that *the
/// listed outbounds* originate to this server. Note: sing-box treats an
/// outbound's own bootstrap lookups (e.g. WireGuard peer hostname
/// resolution) as queries from that outbound, so using this for per-profile
/// scoping creates a chicken-and-egg loop at startup. Prefer
/// [matchInbounds] for "queries from tabs routed through this profile".
final List<String> matchOutbounds;
/// If non-empty, attaches a `dns.rules` entry matching the listed inbound
/// tags. Queries entering via that inbound (e.g. a tab whose container is
/// bound to this profile's local SOCKS inbound) resolve through this
/// server. Endpoint-bootstrap lookups don't come from any inbound, so this
/// scope safely excludes them.
final List<String> matchInbounds;
}
class SingboxProxyDnsConfig {
SingboxProxyDnsConfig({
required this.servers,
this.finalServerTag,
required this.domainStrategy,
});
final List<SingboxProxyDnsServerConfig> servers;
/// Server tag used as `dns.final`. When null, sing-box uses the first
/// server in the list as the fallback.
final String? finalServerTag;
/// sing-box `dns.strategy` string. e.g. `prefer_ipv4`, `ipv4_only`.
final String domainStrategy;
}
class SingboxProxyRuntimeEndpoint {
SingboxProxyRuntimeEndpoint({
required this.profileId,
required this.host,
required this.port,
required this.username,
required this.password,
});
final String profileId;
final String host;
final int port;
final String username;
final String password;
}
class SingboxProxyRuntimeState {
SingboxProxyRuntimeState({
required this.status,
required this.endpoints,
this.message,
});
final SingboxProxyRuntimeStatus status;
final List<SingboxProxyRuntimeEndpoint> endpoints;
final String? message;
}
class SingboxProxyConfigResult {
SingboxProxyConfigResult({required this.configJson, required this.endpoints});
final String configJson;
final List<SingboxProxyRuntimeEndpoint> endpoints;
}
class SingboxProxyLogMessage {
SingboxProxyLogMessage({
required this.level,
required this.message,
required this.timestamp,
this.profileId,
});
final String level;
final String message;
final int timestamp;
final String? profileId;
}
@HostApi()
abstract class SingboxProxyApi {
@async
String? validateProfile(SingboxProxyProfile profile);
@async
SingboxProxyConfigResult buildConfig(
List<SingboxProxyProfile> profiles,
SingboxProxyRuntimeOptions options,
);
@async
SingboxProxyRuntimeState start(
List<SingboxProxyProfile> profiles,
SingboxProxyRuntimeOptions options,
);
@async
void stop(List<String> profileIds);
@async
void stopAll();
SingboxProxyRuntimeState getState();
}
@FlutterApi()
abstract class SingboxProxyEventsApi {
void onStateChanged(SingboxProxyRuntimeState state);
void onLogMessage(SingboxProxyLogMessage message);
}