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.
This commit is contained in:
zarzet
2026-06-26 20:29:15 +07:00
parent af4e4561ec
commit f0acda0f01
8 changed files with 79 additions and 0 deletions
@@ -2297,6 +2297,13 @@ class MainActivity: FlutterFragmentActivity() {
}
result.success(null)
}
"setAllowPrivateNetwork" -> {
val allowed = call.argument<Boolean>("allowed") ?: false
withContext(Dispatchers.IO) {
Gobackend.setAllowPrivateNetwork(allowed)
}
result.success(null)
}
"checkDuplicate" -> {
val outputDir = call.argument<String>("output_dir") ?: ""
val isrc = call.argument<String>("isrc") ?: ""
+30
View File
@@ -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
+6
View File
@@ -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]
+5
View File
@@ -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,
+2
View File
@@ -56,6 +56,7 @@ AppSettings _$AppSettingsFromJson(Map<String, dynamic> 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<String, dynamic> _$AppSettingsToJson(
'autoExportFailedDownloads': instance.autoExportFailedDownloads,
'downloadNetworkMode': instance.downloadNetworkMode,
'networkCompatibilityMode': instance.networkCompatibilityMode,
'allowLocalNetwork': instance.allowLocalNetwork,
'songLinkRegion': instance.songLinkRegion,
'nativeDownloadWorkerEnabled': instance.nativeDownloadWorkerEnabled,
'localLibraryEnabled': instance.localLibraryEnabled,
+12
View File
@@ -125,6 +125,12 @@ class SettingsNotifier extends Notifier<AppSettings> {
).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<AppSettings> {
_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);
@@ -221,6 +221,17 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
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,
),
],
+6
View File
@@ -546,6 +546,12 @@ class PlatformBridge {
});
}
static Future<void> setAllowPrivateNetwork(bool allowed) async {
await _channel.invokeMethod('setAllowPrivateNetwork', {
'allowed': allowed,
});
}
static Future<Map<String, dynamic>> checkDuplicate(
String outputDir,
String isrc,