diff --git a/app/android/app/src/main/AndroidManifest.xml b/app/android/app/src/main/AndroidManifest.xml
index 6d3c2292..24d5a69d 100644
--- a/app/android/app/src/main/AndroidManifest.xml
+++ b/app/android/app/src/main/AndroidManifest.xml
@@ -212,10 +212,6 @@
android:foregroundServiceType="mediaPlayback"
android:exported="false" />
-
-
- r'9ded7af1d745e25c59e22edecea582e25230e5e7';
+ r'e4c5e35b9aab2aae60e3f09a99472f98a7beb69e';
abstract class _$ProxySettingsReplication extends $Notifier {
void build();
diff --git a/app/lib/features/tor/data/models/moat.dart b/app/lib/features/tor/data/models/moat.dart
new file mode 100644
index 00000000..f58e65e6
--- /dev/null
+++ b/app/lib/features/tor/data/models/moat.dart
@@ -0,0 +1,177 @@
+/*
+ * Copyright (c) 2024-2025 Fabian Freund.
+ *
+ * This file is part of WebLibre
+ * (see https://weblibre.eu).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import 'package:json_annotation/json_annotation.dart';
+
+part 'moat.g.dart';
+
+enum MoatTransportType {
+ obfs4('obfs4'),
+ snowflake('snowflake'),
+ meek('meek'),
+ meekAzure('meek-azure'),
+ webtunnel('webtunnel');
+
+ const MoatTransportType(this.value);
+ final String value;
+
+ @override
+ String toString() => value;
+
+ static MoatTransportType? fromString(String value) {
+ for (final transport in MoatTransportType.values) {
+ if (transport.value == value) return transport;
+ }
+ return null;
+ }
+}
+
+@JsonSerializable()
+class SettingsRequest {
+ final String? country;
+ @JsonKey(toJson: _transportsToJson, fromJson: _transportsFromJson)
+ final List transports;
+
+ const SettingsRequest({
+ this.country,
+ this.transports = const [
+ MoatTransportType.obfs4,
+ MoatTransportType.snowflake,
+ MoatTransportType.webtunnel,
+ ],
+ });
+
+ static List _transportsToJson(List transports) =>
+ transports.map((t) => t.value).toList();
+
+ static List _transportsFromJson(List json) => json
+ .cast()
+ .map((s) => MoatTransportType.fromString(s))
+ .where((t) => t != null)
+ .cast()
+ .toList();
+
+ factory SettingsRequest.fromJson(Map json) =>
+ _$SettingsRequestFromJson(json);
+
+ Map toJson() => _$SettingsRequestToJson(this);
+}
+
+@JsonSerializable()
+class SettingsResponse {
+ final List? settings;
+ final String? country;
+ final List? errors;
+
+ const SettingsResponse({this.settings, this.country, this.errors});
+
+ factory SettingsResponse.fromJson(Map json) =>
+ _$SettingsResponseFromJson(json);
+
+ Map toJson() => _$SettingsResponseToJson(this);
+}
+
+@JsonSerializable()
+class Setting {
+ @JsonKey(name: 'bridges')
+ final Bridge bridge;
+
+ const Setting({required this.bridge});
+
+ factory Setting.fromJson(Map json) =>
+ _$SettingFromJson(json);
+
+ Map toJson() => _$SettingToJson(this);
+}
+
+@JsonSerializable()
+class Bridge {
+ @JsonKey(toJson: _transportToJson, fromJson: _transportFromJson)
+ final MoatTransportType type;
+ final String source;
+ @JsonKey(name: 'bridge_strings')
+ final List? bridges;
+
+ const Bridge({required this.type, required this.source, this.bridges});
+
+ static String _transportToJson(MoatTransportType transport) =>
+ transport.value;
+
+ static MoatTransportType _transportFromJson(dynamic json) =>
+ MoatTransportType.fromString(json as String)!;
+
+ factory Bridge.fromJson(Map json) => _$BridgeFromJson(json);
+
+ Map toJson() => _$BridgeToJson(this);
+}
+
+@JsonSerializable()
+class MoatError implements Exception {
+ final String? id;
+ final String? type;
+ final String? version;
+ final int? code;
+ final String? status;
+ final String? detail;
+
+ const MoatError({
+ this.id,
+ this.type,
+ this.version,
+ this.code,
+ this.status,
+ this.detail,
+ });
+
+ factory MoatError.fromJson(Map json) =>
+ _$MoatErrorFromJson(json);
+
+ Map toJson() => _$MoatErrorToJson(this);
+
+ @override
+ String toString() {
+ if (detail != null && detail!.isNotEmpty) {
+ return detail!;
+ }
+ return '$code $status';
+ }
+}
+
+@JsonSerializable()
+class BuiltInBridges {
+ final List meek;
+
+ @JsonKey(name: 'meek-azure')
+ final List meekAzure;
+
+ final List obfs4;
+ final List snowflake;
+
+ const BuiltInBridges({
+ required this.meek,
+ required this.meekAzure,
+ required this.obfs4,
+ required this.snowflake,
+ });
+
+ factory BuiltInBridges.fromJson(Map json) =>
+ _$BuiltInBridgesFromJson(json);
+
+ Map toJson() => _$BuiltInBridgesToJson(this);
+}
diff --git a/app/lib/features/tor/data/models/moat.g.dart b/app/lib/features/tor/data/models/moat.g.dart
new file mode 100644
index 00000000..e0eab57d
--- /dev/null
+++ b/app/lib/features/tor/data/models/moat.g.dart
@@ -0,0 +1,102 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'moat.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+SettingsRequest _$SettingsRequestFromJson(Map json) =>
+ SettingsRequest(
+ country: json['country'] as String?,
+ transports: json['transports'] == null
+ ? const [
+ MoatTransportType.obfs4,
+ MoatTransportType.snowflake,
+ MoatTransportType.webtunnel,
+ ]
+ : SettingsRequest._transportsFromJson(json['transports'] as List),
+ );
+
+Map _$SettingsRequestToJson(SettingsRequest instance) =>
+ {
+ 'country': instance.country,
+ 'transports': SettingsRequest._transportsToJson(instance.transports),
+ };
+
+SettingsResponse _$SettingsResponseFromJson(Map json) =>
+ SettingsResponse(
+ settings: (json['settings'] as List?)
+ ?.map((e) => Setting.fromJson(e as Map))
+ .toList(),
+ country: json['country'] as String?,
+ errors: (json['errors'] as List?)
+ ?.map((e) => MoatError.fromJson(e as Map))
+ .toList(),
+ );
+
+Map _$SettingsResponseToJson(SettingsResponse instance) =>
+ {
+ 'settings': instance.settings?.map((e) => e.toJson()).toList(),
+ 'country': instance.country,
+ 'errors': instance.errors?.map((e) => e.toJson()).toList(),
+ };
+
+Setting _$SettingFromJson(Map json) =>
+ Setting(bridge: Bridge.fromJson(json['bridges'] as Map));
+
+Map _$SettingToJson(Setting instance) => {
+ 'bridges': instance.bridge.toJson(),
+};
+
+Bridge _$BridgeFromJson(Map json) => Bridge(
+ type: Bridge._transportFromJson(json['type']),
+ source: json['source'] as String,
+ bridges: (json['bridge_strings'] as List?)
+ ?.map((e) => e as String)
+ .toList(),
+);
+
+Map _$BridgeToJson(Bridge instance) => {
+ 'type': Bridge._transportToJson(instance.type),
+ 'source': instance.source,
+ 'bridge_strings': instance.bridges,
+};
+
+MoatError _$MoatErrorFromJson(Map json) => MoatError(
+ id: json['id'] as String?,
+ type: json['type'] as String?,
+ version: json['version'] as String?,
+ code: (json['code'] as num?)?.toInt(),
+ status: json['status'] as String?,
+ detail: json['detail'] as String?,
+);
+
+Map _$MoatErrorToJson(MoatError instance) => {
+ 'id': instance.id,
+ 'type': instance.type,
+ 'version': instance.version,
+ 'code': instance.code,
+ 'status': instance.status,
+ 'detail': instance.detail,
+};
+
+BuiltInBridges _$BuiltInBridgesFromJson(Map json) =>
+ BuiltInBridges(
+ meek: (json['meek'] as List).map((e) => e as String).toList(),
+ meekAzure: (json['meek-azure'] as List)
+ .map((e) => e as String)
+ .toList(),
+ obfs4: (json['obfs4'] as List).map((e) => e as String).toList(),
+ snowflake: (json['snowflake'] as List)
+ .map((e) => e as String)
+ .toList(),
+ );
+
+Map _$BuiltInBridgesToJson(BuiltInBridges instance) =>
+ {
+ 'meek': instance.meek,
+ 'meek-azure': instance.meekAzure,
+ 'obfs4': instance.obfs4,
+ 'snowflake': instance.snowflake,
+ };
diff --git a/app/lib/features/tor/data/services/builtin_bridges.dart b/app/lib/features/tor/data/services/builtin_bridges.dart
index 67facec5..3e7a34d8 100644
--- a/app/lib/features/tor/data/services/builtin_bridges.dart
+++ b/app/lib/features/tor/data/services/builtin_bridges.dart
@@ -23,8 +23,8 @@ import 'dart:io';
import 'package:flutter/services.dart' show rootBundle;
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart' as path_provider;
-import 'package:pluggable_transports_proxy/pluggable_transports_proxy.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
+import 'package:weblibre/features/tor/data/models/moat.dart';
part 'builtin_bridges.g.dart';
diff --git a/app/lib/features/tor/data/services/moat_api.dart b/app/lib/features/tor/data/services/moat_api.dart
new file mode 100644
index 00000000..a7db217b
--- /dev/null
+++ b/app/lib/features/tor/data/services/moat_api.dart
@@ -0,0 +1,99 @@
+/*
+ * Copyright (c) 2024-2025 Fabian Freund.
+ *
+ * This file is part of WebLibre
+ * (see https://weblibre.eu).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import 'dart:convert';
+
+import 'package:http/http.dart' as http;
+import 'package:weblibre/features/tor/data/models/moat.dart';
+
+class MoatApi {
+ static const String _baseUrl =
+ 'https://bridges.torproject.org/moat/circumvention/';
+
+ final http.Client _client;
+
+ MoatApi(this._client);
+
+ Future settings([SettingsRequest? request]) async {
+ final response = await _post(
+ 'settings',
+ request ?? const SettingsRequest(),
+ );
+ return SettingsResponse.fromJson(
+ jsonDecode(response.body) as Map,
+ );
+ }
+
+ Future defaults([SettingsRequest? request]) async {
+ final response = await _post(
+ 'defaults',
+ request ?? const SettingsRequest(),
+ );
+ return SettingsResponse.fromJson(
+ jsonDecode(response.body) as Map,
+ );
+ }
+
+ Future