From f0acda0f0185d9872d4afb006e2f3fe9ff456bdd Mon Sep 17 00:00:00 2001 From: zarzet Date: Fri, 26 Jun 2026 20:29:15 +0700 Subject: [PATCH] feat(network): add opt-in allow local/private network setting Add a setting that relaxes the SSRF guard so extensions and built-in network code can reach private/local/loopback targets, for users routing traffic through a local proxy or custom DNS. Disabled by default. Wired end-to-end: Go backend (SetAllowPrivateNetwork toggles isPrivateIP guard), Android/iOS platform bridge, Dart settings model/provider, and a toggle in Download settings. --- .../kotlin/com/zarz/spotiflac/MainActivity.kt | 7 +++++ go_backend/extension_runtime.go | 30 +++++++++++++++++++ ios/Runner/AppDelegate.swift | 6 ++++ lib/models/settings.dart | 5 ++++ lib/models/settings.g.dart | 2 ++ lib/providers/settings_provider.dart | 12 ++++++++ .../settings/download_settings_page.dart | 11 +++++++ lib/services/platform_bridge.dart | 6 ++++ 8 files changed, 79 insertions(+) diff --git a/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt b/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt index d3ce0957..11e088a6 100644 --- a/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt +++ b/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt @@ -2297,6 +2297,13 @@ class MainActivity: FlutterFragmentActivity() { } result.success(null) } + "setAllowPrivateNetwork" -> { + val allowed = call.argument("allowed") ?: false + withContext(Dispatchers.IO) { + Gobackend.setAllowPrivateNetwork(allowed) + } + result.success(null) + } "checkDuplicate" -> { val outputDir = call.argument("output_dir") ?: "" val isrc = call.argument("isrc") ?: "" diff --git a/go_backend/extension_runtime.go b/go_backend/extension_runtime.go index 3f0b97a0..ba968ff1 100644 --- a/go_backend/extension_runtime.go +++ b/go_backend/extension_runtime.go @@ -8,11 +8,35 @@ import ( "strconv" "strings" "sync" + "sync/atomic" "time" "github.com/dop251/goja" ) +// allowPrivateNetworkAccess, when enabled, disables the SSRF guard that blocks +// requests resolving to private/local/loopback addresses. This is opt-in and +// intended for users who route the app's traffic through a local proxy or +// custom DNS (e.g. a local mirror of api.zarz.moe). Disabled by default. +var allowPrivateNetworkAccess atomic.Bool + +// SetAllowPrivateNetwork toggles whether extensions and built-in network code +// are permitted to reach private/local network targets. Exposed to the Flutter +// layer via the platform bridge. +func SetAllowPrivateNetwork(allowed bool) { + allowPrivateNetworkAccess.Store(allowed) + if allowed { + GoLog("[HTTP] Private/local network access ENABLED (SSRF guard relaxed)\n") + } else { + GoLog("[HTTP] Private/local network access disabled (default)\n") + } +} + +// IsPrivateNetworkAllowed reports the current state of the private-network guard. +func IsPrivateNetworkAllowed() bool { + return allowPrivateNetworkAccess.Load() +} + const DefaultJSTimeout = 30 * time.Second var ( @@ -303,6 +327,12 @@ func (e *RedirectBlockedError) Error() string { } func isPrivateIP(host string) bool { + // Opt-in escape hatch: when the user has enabled private/local network + // access, treat every host as public so local proxies / custom DNS work. + if allowPrivateNetworkAccess.Load() { + return false + } + hostLower := strings.ToLower(strings.TrimSpace(host)) if hostLower == "" { return false diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 65f30cc5..134227d9 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -393,6 +393,12 @@ import Gobackend // Import Go framework let insecureTLS = args["insecure_tls"] as? Bool ?? false GobackendSetNetworkCompatibilityOptions(allowHTTP, insecureTLS) return nil + + case "setAllowPrivateNetwork": + let args = call.arguments as! [String: Any] + let allowed = args["allowed"] as? Bool ?? false + GobackendSetAllowPrivateNetwork(allowed) + return nil case "checkDuplicate": let args = call.arguments as! [String: Any] diff --git a/lib/models/settings.dart b/lib/models/settings.dart index 866ea7a0..925439d5 100644 --- a/lib/models/settings.dart +++ b/lib/models/settings.dart @@ -55,6 +55,8 @@ class AppSettings { downloadNetworkMode; // 'any' = WiFi + Mobile, 'wifi_only' = WiFi only final bool networkCompatibilityMode; // Try HTTP + allow invalid TLS cert for API requests + final bool + allowLocalNetwork; // Allow requests to private/local network targets (local proxy / custom DNS) final String songLinkRegion; // SongLink userCountry region code used for platform lookup final bool @@ -135,6 +137,7 @@ class AppSettings { this.autoExportFailedDownloads = false, this.downloadNetworkMode = 'any', this.networkCompatibilityMode = false, + this.allowLocalNetwork = false, this.songLinkRegion = 'US', this.nativeDownloadWorkerEnabled = false, this.localLibraryEnabled = false, @@ -200,6 +203,7 @@ class AppSettings { bool? autoExportFailedDownloads, String? downloadNetworkMode, bool? networkCompatibilityMode, + bool? allowLocalNetwork, String? songLinkRegion, bool? nativeDownloadWorkerEnabled, bool? localLibraryEnabled, @@ -275,6 +279,7 @@ class AppSettings { downloadNetworkMode: downloadNetworkMode ?? this.downloadNetworkMode, networkCompatibilityMode: networkCompatibilityMode ?? this.networkCompatibilityMode, + allowLocalNetwork: allowLocalNetwork ?? this.allowLocalNetwork, songLinkRegion: songLinkRegion ?? this.songLinkRegion, nativeDownloadWorkerEnabled: nativeDownloadWorkerEnabled ?? this.nativeDownloadWorkerEnabled, diff --git a/lib/models/settings.g.dart b/lib/models/settings.g.dart index dfc1daae..83647667 100644 --- a/lib/models/settings.g.dart +++ b/lib/models/settings.g.dart @@ -56,6 +56,7 @@ AppSettings _$AppSettingsFromJson(Map json) => AppSettings( json['autoExportFailedDownloads'] as bool? ?? false, downloadNetworkMode: json['downloadNetworkMode'] as String? ?? 'any', networkCompatibilityMode: json['networkCompatibilityMode'] as bool? ?? false, + allowLocalNetwork: json['allowLocalNetwork'] as bool? ?? false, songLinkRegion: json['songLinkRegion'] as String? ?? 'US', nativeDownloadWorkerEnabled: json['nativeDownloadWorkerEnabled'] as bool? ?? false, @@ -130,6 +131,7 @@ Map _$AppSettingsToJson( 'autoExportFailedDownloads': instance.autoExportFailedDownloads, 'downloadNetworkMode': instance.downloadNetworkMode, 'networkCompatibilityMode': instance.networkCompatibilityMode, + 'allowLocalNetwork': instance.allowLocalNetwork, 'songLinkRegion': instance.songLinkRegion, 'nativeDownloadWorkerEnabled': instance.nativeDownloadWorkerEnabled, 'localLibraryEnabled': instance.localLibraryEnabled, diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index 13bf20cf..ea5114e6 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -125,6 +125,12 @@ class SettingsNotifier extends Notifier { ).catchError((Object e) { _log.w('Failed to sync network compatibility options to backend: $e'); }); + + PlatformBridge.setAllowPrivateNetwork(state.allowLocalNetwork).catchError(( + Object e, + ) { + _log.w('Failed to sync allow local network option to backend: $e'); + }); } void _syncExtensionFallbackSettingsToBackend() { @@ -575,6 +581,12 @@ class SettingsNotifier extends Notifier { _syncNetworkCompatibilitySettingsToBackend(); } + void setAllowLocalNetwork(bool enabled) { + state = state.copyWith(allowLocalNetwork: enabled); + _saveSettings(); + _syncNetworkCompatibilitySettingsToBackend(); + } + void setSongLinkRegion(String region) { final normalized = _normalizeSongLinkRegion(region); state = state.copyWith(songLinkRegion: normalized); diff --git a/lib/screens/settings/download_settings_page.dart b/lib/screens/settings/download_settings_page.dart index 9359fbfd..924870cb 100644 --- a/lib/screens/settings/download_settings_page.dart +++ b/lib/screens/settings/download_settings_page.dart @@ -221,6 +221,17 @@ class _DownloadSettingsPageState extends ConsumerState { onChanged: (value) => ref .read(settingsProvider.notifier) .setNetworkCompatibilityMode(value), + ), + SettingsSwitchItem( + icon: Icons.lan_outlined, + title: context.l10n.downloadAllowLocalNetwork, + subtitle: settings.allowLocalNetwork + ? context.l10n.downloadAllowLocalNetworkEnabled + : context.l10n.downloadAllowLocalNetworkDisabled, + value: settings.allowLocalNetwork, + onChanged: (value) => ref + .read(settingsProvider.notifier) + .setAllowLocalNetwork(value), showDivider: false, ), ], diff --git a/lib/services/platform_bridge.dart b/lib/services/platform_bridge.dart index 29758ac1..a171a72b 100644 --- a/lib/services/platform_bridge.dart +++ b/lib/services/platform_bridge.dart @@ -546,6 +546,12 @@ class PlatformBridge { }); } + static Future setAllowPrivateNetwork(bool allowed) async { + await _channel.invokeMethod('setAllowPrivateNetwork', { + 'allowed': allowed, + }); + } + static Future> checkDuplicate( String outputDir, String isrc,