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 [], this.matchGeosites = const [], this.matchOutbounds = const [], this.matchInbounds = const [], }); /// 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 matchDomainSuffixes; /// Advanced sing-box geosite selectors (e.g. `geosite:cn`). final List 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 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 matchInbounds; } class SingboxProxyDnsConfig { SingboxProxyDnsConfig({ required this.servers, this.finalServerTag, required this.domainStrategy, }); final List 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 endpoints; final String? message; } class SingboxProxyConfigResult { SingboxProxyConfigResult({required this.configJson, required this.endpoints}); final String configJson; final List 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 profiles, SingboxProxyRuntimeOptions options, ); @async SingboxProxyRuntimeState start( List profiles, SingboxProxyRuntimeOptions options, ); @async void stop(List profileIds); @async void stopAll(); SingboxProxyRuntimeState getState(); } @FlutterApi() abstract class SingboxProxyEventsApi { void onStateChanged(SingboxProxyRuntimeState state); void onLogMessage(SingboxProxyLogMessage message); }