mirror of
https://github.com/sudosylabs/vnidrop.git
synced 2026-08-05 10:29:58 +02:00
feat(network): add relay connection policies
This commit is contained in:
19
README.md
19
README.md
@@ -53,10 +53,21 @@ encrypted packets; it is not a VniDrop file store.
|
|||||||
### Custom relay servers
|
### Custom relay servers
|
||||||
|
|
||||||
VniDrop uses Iroh's public relay and discovery infrastructure by default. In
|
VniDrop uses Iroh's public relay and discovery infrastructure by default. In
|
||||||
**Settings → Network**, advanced users can instead configure up to eight custom
|
**Settings → Network**, users can select one of four policies:
|
||||||
HTTPS servers that implement Iroh's relay protocol. Custom mode is strict: the
|
|
||||||
app uses only those relays and does not silently fall back to public relays or
|
- **Automatic (recommended):** use Iroh's public relays, with direct P2P/LAN
|
||||||
public discovery. Direct peer-to-peer paths remain available.
|
connections whenever possible.
|
||||||
|
- **Strict custom:** use only up to eight configured custom HTTPS relays or
|
||||||
|
direct connections. Startup reports an error if none of the custom relays can
|
||||||
|
be established.
|
||||||
|
- **Custom with direct fallback:** prefer the configured custom relays, but
|
||||||
|
continue with direct connections if they are unavailable.
|
||||||
|
- **Local only:** disable every relay and allow direct connections only,
|
||||||
|
primarily for devices on the same network.
|
||||||
|
|
||||||
|
Strict custom, custom with direct fallback, and local only never use public
|
||||||
|
relays or public discovery, including relay addresses advertised by incoming
|
||||||
|
invitations.
|
||||||
|
|
||||||
Applying a relay change restarts VniDrop's network engine, so active transfers
|
Applying a relay change restarts VniDrop's network engine, so active transfers
|
||||||
and shares must be stopped first. The app tests the new configuration and
|
and shares must be stopped first. The app tests the new configuration and
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ final class AppModelTests: XCTestCase {
|
|||||||
func testInitializesCoreWithSavedCustomRelayConfiguration() async {
|
func testInitializesCoreWithSavedCustomRelayConfiguration() async {
|
||||||
let core = FakeCoreGateway()
|
let core = FakeCoreGateway()
|
||||||
let preferences = Fixtures.preferences()
|
let preferences = Fixtures.preferences()
|
||||||
let configuration = RelayConfiguration(mode: .custom, relayURLs: ["https://relay.example"])
|
let configuration = RelayConfiguration(mode: .strictCustom, relayURLs: ["https://relay.example"])
|
||||||
preferences.setRelayConfiguration(configuration)
|
preferences.setRelayConfiguration(configuration)
|
||||||
_ = makeModel(core, preferences: preferences)
|
_ = makeModel(core, preferences: preferences)
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ final class AppPreferencesRepositoryTests: XCTestCase {
|
|||||||
repo.setNotificationsEnabled(true)
|
repo.setNotificationsEnabled(true)
|
||||||
repo.setReceiveFolder(ReceiveFolder(kind: .iosSecurityScopedUrl, value: "file:///x", displayName: "Custom"))
|
repo.setReceiveFolder(ReceiveFolder(kind: .iosSecurityScopedUrl, value: "file:///x", displayName: "Custom"))
|
||||||
repo.setRelayConfiguration(RelayConfiguration(
|
repo.setRelayConfiguration(RelayConfiguration(
|
||||||
mode: .custom,
|
mode: .strictCustom,
|
||||||
relayURLs: ["https://relay-one.example", "https://relay-two.example:443"]
|
relayURLs: ["https://relay-one.example", "https://relay-two.example:443"]
|
||||||
))
|
))
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ final class AppPreferencesRepositoryTests: XCTestCase {
|
|||||||
XCTAssertEqual(reloaded.preferences.receiveFolder.displayName, "Custom")
|
XCTAssertEqual(reloaded.preferences.receiveFolder.displayName, "Custom")
|
||||||
XCTAssertEqual(reloaded.preferences.receiveFolder.kind, .iosSecurityScopedUrl)
|
XCTAssertEqual(reloaded.preferences.receiveFolder.kind, .iosSecurityScopedUrl)
|
||||||
XCTAssertEqual(reloaded.preferences.relayConfiguration, RelayConfiguration(
|
XCTAssertEqual(reloaded.preferences.relayConfiguration, RelayConfiguration(
|
||||||
mode: .custom,
|
mode: .strictCustom,
|
||||||
relayURLs: ["https://relay-one.example", "https://relay-two.example:443"]
|
relayURLs: ["https://relay-one.example", "https://relay-two.example:443"]
|
||||||
))
|
))
|
||||||
XCTAssertNotNil(store.data(forKey: "relay_configuration"))
|
XCTAssertNotNil(store.data(forKey: "relay_configuration"))
|
||||||
@@ -60,7 +60,7 @@ final class AppPreferencesRepositoryTests: XCTestCase {
|
|||||||
|
|
||||||
XCTAssertEqual(
|
XCTAssertEqual(
|
||||||
repo.preferences.relayConfiguration,
|
repo.preferences.relayConfiguration,
|
||||||
RelayConfiguration(mode: .custom, relayURLs: [])
|
RelayConfiguration(mode: .strictCustom, relayURLs: [])
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,7 +75,7 @@ final class AppPreferencesRepositoryTests: XCTestCase {
|
|||||||
|
|
||||||
XCTAssertEqual(
|
XCTAssertEqual(
|
||||||
repo.preferences.relayConfiguration,
|
repo.preferences.relayConfiguration,
|
||||||
RelayConfiguration(mode: .custom, relayURLs: [])
|
RelayConfiguration(mode: .strictCustom, relayURLs: [])
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ final class CoreRepositoryLifecycleTests: XCTestCase {
|
|||||||
|
|
||||||
let concurrentInitialization = await repository.initialize(
|
let concurrentInitialization = await repository.initialize(
|
||||||
appDataDir: "/tmp/second",
|
appDataDir: "/tmp/second",
|
||||||
networkConfiguration: RelayConfiguration(mode: .custom, relayURLs: ["https://relay.example"])
|
networkConfiguration: RelayConfiguration(mode: .strictCustom, relayURLs: ["https://relay.example"])
|
||||||
)
|
)
|
||||||
assertLifecycleFailure(concurrentInitialization, equals: .transitionInProgress)
|
assertLifecycleFailure(concurrentInitialization, equals: .transitionInProgress)
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,32 @@ import XCTest
|
|||||||
@testable import VniDrop
|
@testable import VniDrop
|
||||||
|
|
||||||
final class RelayConfigurationTests: XCTestCase {
|
final class RelayConfigurationTests: XCTestCase {
|
||||||
|
func testCustomFallbackValidatesAndPreservesItsMode() throws {
|
||||||
|
let result = try RelayConfigurationValidator.validate(
|
||||||
|
mode: .customWithDirectFallback,
|
||||||
|
relayURLs: ["https://relay.example/"]
|
||||||
|
)
|
||||||
|
|
||||||
|
XCTAssertEqual(
|
||||||
|
result,
|
||||||
|
RelayConfiguration(
|
||||||
|
mode: .customWithDirectFallback,
|
||||||
|
relayURLs: ["https://relay.example"]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testLocalOnlyRetainsPreviouslySavedRelayURLs() throws {
|
||||||
|
let retained = ["https://relay.example"]
|
||||||
|
let result = try RelayConfigurationValidator.validate(
|
||||||
|
mode: .localOnly,
|
||||||
|
relayURLs: ["not a URL"],
|
||||||
|
retainedRelayURLs: retained
|
||||||
|
)
|
||||||
|
|
||||||
|
XCTAssertEqual(result, RelayConfiguration(mode: .localOnly, relayURLs: retained))
|
||||||
|
}
|
||||||
|
|
||||||
func testAutomaticModeIgnoresRelayDrafts() throws {
|
func testAutomaticModeIgnoresRelayDrafts() throws {
|
||||||
let result = try RelayConfigurationValidator.validate(
|
let result = try RelayConfigurationValidator.validate(
|
||||||
mode: .automatic,
|
mode: .automatic,
|
||||||
@@ -24,32 +50,32 @@ final class RelayConfigurationTests: XCTestCase {
|
|||||||
|
|
||||||
func testCustomModeTrimsValidHTTPSRelayURLs() throws {
|
func testCustomModeTrimsValidHTTPSRelayURLs() throws {
|
||||||
let result = try RelayConfigurationValidator.validate(
|
let result = try RelayConfigurationValidator.validate(
|
||||||
mode: .custom,
|
mode: .strictCustom,
|
||||||
relayURLs: [" https://relay.example/ ", "https://backup.example:443"]
|
relayURLs: [" https://relay.example/ ", "https://backup.example:443"]
|
||||||
)
|
)
|
||||||
XCTAssertEqual(result, RelayConfiguration(
|
XCTAssertEqual(result, RelayConfiguration(
|
||||||
mode: .custom,
|
mode: .strictCustom,
|
||||||
relayURLs: ["https://relay.example", "https://backup.example"]
|
relayURLs: ["https://relay.example", "https://backup.example"]
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
func testCustomModeIgnoresEmptyURLRows() throws {
|
func testCustomModeIgnoresEmptyURLRows() throws {
|
||||||
let result = try RelayConfigurationValidator.validate(
|
let result = try RelayConfigurationValidator.validate(
|
||||||
mode: .custom,
|
mode: .strictCustom,
|
||||||
relayURLs: ["", " ", "https://relay.example"]
|
relayURLs: ["", " ", "https://relay.example"]
|
||||||
)
|
)
|
||||||
XCTAssertEqual(result.relayURLs, ["https://relay.example"])
|
XCTAssertEqual(result.relayURLs, ["https://relay.example"])
|
||||||
}
|
}
|
||||||
|
|
||||||
func testCustomModeRequiresAtLeastOneRelay() {
|
func testCustomModeRequiresAtLeastOneRelay() {
|
||||||
XCTAssertThrowsError(try RelayConfigurationValidator.validate(mode: .custom, relayURLs: [])) { error in
|
XCTAssertThrowsError(try RelayConfigurationValidator.validate(mode: .strictCustom, relayURLs: [])) { error in
|
||||||
XCTAssertEqual(error as? RelayConfigurationValidationError, .missingURL)
|
XCTAssertEqual(error as? RelayConfigurationValidationError, .missingURL)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func testCustomModeRequiresHTTPS() {
|
func testCustomModeRequiresHTTPS() {
|
||||||
XCTAssertThrowsError(try RelayConfigurationValidator.validate(
|
XCTAssertThrowsError(try RelayConfigurationValidator.validate(
|
||||||
mode: .custom,
|
mode: .strictCustom,
|
||||||
relayURLs: ["http://relay.example"]
|
relayURLs: ["http://relay.example"]
|
||||||
)) { error in
|
)) { error in
|
||||||
XCTAssertEqual(error as? RelayConfigurationValidationError, .httpsRequired(index: 0))
|
XCTAssertEqual(error as? RelayConfigurationValidationError, .httpsRequired(index: 0))
|
||||||
@@ -67,7 +93,7 @@ final class RelayConfigurationTests: XCTestCase {
|
|||||||
]
|
]
|
||||||
for relayURL in invalidURLs {
|
for relayURL in invalidURLs {
|
||||||
XCTAssertThrowsError(
|
XCTAssertThrowsError(
|
||||||
try RelayConfigurationValidator.validate(mode: .custom, relayURLs: [relayURL]),
|
try RelayConfigurationValidator.validate(mode: .strictCustom, relayURLs: [relayURL]),
|
||||||
"Expected \(relayURL) to be rejected"
|
"Expected \(relayURL) to be rejected"
|
||||||
) { error in
|
) { error in
|
||||||
XCTAssertEqual(error as? RelayConfigurationValidationError, .invalidURL(index: 0))
|
XCTAssertEqual(error as? RelayConfigurationValidationError, .invalidURL(index: 0))
|
||||||
@@ -77,7 +103,7 @@ final class RelayConfigurationTests: XCTestCase {
|
|||||||
|
|
||||||
func testCustomModeRejectsNormalizedDuplicate() {
|
func testCustomModeRejectsNormalizedDuplicate() {
|
||||||
XCTAssertThrowsError(try RelayConfigurationValidator.validate(
|
XCTAssertThrowsError(try RelayConfigurationValidator.validate(
|
||||||
mode: .custom,
|
mode: .strictCustom,
|
||||||
relayURLs: ["https://relay.example", "https://RELAY.example:443/"]
|
relayURLs: ["https://relay.example", "https://RELAY.example:443/"]
|
||||||
)) { error in
|
)) { error in
|
||||||
XCTAssertEqual(error as? RelayConfigurationValidationError, .duplicateURL(index: 1))
|
XCTAssertEqual(error as? RelayConfigurationValidationError, .duplicateURL(index: 1))
|
||||||
@@ -88,7 +114,7 @@ final class RelayConfigurationTests: XCTestCase {
|
|||||||
let relayURLs = (0...RelayConfigurationValidator.maximumRelayCount).map {
|
let relayURLs = (0...RelayConfigurationValidator.maximumRelayCount).map {
|
||||||
"https://relay-\($0).example"
|
"https://relay-\($0).example"
|
||||||
}
|
}
|
||||||
XCTAssertThrowsError(try RelayConfigurationValidator.validate(mode: .custom, relayURLs: relayURLs)) { error in
|
XCTAssertThrowsError(try RelayConfigurationValidator.validate(mode: .strictCustom, relayURLs: relayURLs)) { error in
|
||||||
XCTAssertEqual(error as? RelayConfigurationValidationError, .tooManyURLs)
|
XCTAssertEqual(error as? RelayConfigurationValidationError, .tooManyURLs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,13 +59,13 @@ final class SettingsModelTests: XCTestCase {
|
|||||||
let core = FakeCoreGateway()
|
let core = FakeCoreGateway()
|
||||||
let preferences = Fixtures.preferences()
|
let preferences = Fixtures.preferences()
|
||||||
let model = makeModel(core, preferences: preferences)
|
let model = makeModel(core, preferences: preferences)
|
||||||
model.setRelayMode(.custom)
|
model.setRelayMode(.strictCustom)
|
||||||
model.setRelayURL(" https://relay.example/ ", at: 0)
|
model.setRelayURL(" https://relay.example/ ", at: 0)
|
||||||
|
|
||||||
model.applyRelayConfiguration()
|
model.applyRelayConfiguration()
|
||||||
|
|
||||||
await waitUntil { preferences.preferences.relayConfiguration.mode == .custom }
|
await waitUntil { preferences.preferences.relayConfiguration.mode == .strictCustom }
|
||||||
let expected = RelayConfiguration(mode: .custom, relayURLs: ["https://relay.example"])
|
let expected = RelayConfiguration(mode: .strictCustom, relayURLs: ["https://relay.example"])
|
||||||
XCTAssertEqual(preferences.preferences.relayConfiguration, expected)
|
XCTAssertEqual(preferences.preferences.relayConfiguration, expected)
|
||||||
XCTAssertEqual(core.initializedNetworkConfigurations, [expected])
|
XCTAssertEqual(core.initializedNetworkConfigurations, [expected])
|
||||||
XCTAssertFalse(model.state.relayConfigurationIsDirty)
|
XCTAssertFalse(model.state.relayConfigurationIsDirty)
|
||||||
@@ -75,7 +75,7 @@ final class SettingsModelTests: XCTestCase {
|
|||||||
let core = FakeCoreGateway()
|
let core = FakeCoreGateway()
|
||||||
let preferences = Fixtures.preferences()
|
let preferences = Fixtures.preferences()
|
||||||
let relayURLs = ["https://relay.example", "https://backup.example"]
|
let relayURLs = ["https://relay.example", "https://backup.example"]
|
||||||
preferences.setRelayConfiguration(RelayConfiguration(mode: .custom, relayURLs: relayURLs))
|
preferences.setRelayConfiguration(RelayConfiguration(mode: .strictCustom, relayURLs: relayURLs))
|
||||||
let model = makeModel(core, preferences: preferences)
|
let model = makeModel(core, preferences: preferences)
|
||||||
|
|
||||||
model.setRelayMode(.automatic)
|
model.setRelayMode(.automatic)
|
||||||
@@ -86,7 +86,7 @@ final class SettingsModelTests: XCTestCase {
|
|||||||
XCTAssertEqual(core.initializedNetworkConfigurations, [
|
XCTAssertEqual(core.initializedNetworkConfigurations, [
|
||||||
RelayConfiguration(mode: .automatic, relayURLs: relayURLs),
|
RelayConfiguration(mode: .automatic, relayURLs: relayURLs),
|
||||||
])
|
])
|
||||||
model.setRelayMode(.custom)
|
model.setRelayMode(.strictCustom)
|
||||||
XCTAssertEqual(model.state.relayURLs, relayURLs)
|
XCTAssertEqual(model.state.relayURLs, relayURLs)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,7 +98,7 @@ final class SettingsModelTests: XCTestCase {
|
|||||||
isInitialized: true,
|
isInitialized: true,
|
||||||
status: CoreStatus(endpointId: "endpoint", activeTransfers: 0, activeShares: 1)
|
status: CoreStatus(endpointId: "endpoint", activeTransfers: 0, activeShares: 1)
|
||||||
))
|
))
|
||||||
model.setRelayMode(.custom)
|
model.setRelayMode(.strictCustom)
|
||||||
model.setRelayURL("https://relay.example", at: 0)
|
model.setRelayURL("https://relay.example", at: 0)
|
||||||
|
|
||||||
model.applyRelayConfiguration()
|
model.applyRelayConfiguration()
|
||||||
@@ -114,8 +114,8 @@ final class SettingsModelTests: XCTestCase {
|
|||||||
core.initializeResult = .failure(CoreNetworkLifecycleError.activeNetworkWork)
|
core.initializeResult = .failure(CoreNetworkLifecycleError.activeNetworkWork)
|
||||||
let preferences = Fixtures.preferences()
|
let preferences = Fixtures.preferences()
|
||||||
let model = makeModel(core, preferences: preferences)
|
let model = makeModel(core, preferences: preferences)
|
||||||
let attempted = RelayConfiguration(mode: .custom, relayURLs: ["https://relay.example"])
|
let attempted = RelayConfiguration(mode: .strictCustom, relayURLs: ["https://relay.example"])
|
||||||
model.setRelayMode(.custom)
|
model.setRelayMode(.strictCustom)
|
||||||
model.setRelayURL(attempted.relayURLs[0], at: 0)
|
model.setRelayURL(attempted.relayURLs[0], at: 0)
|
||||||
|
|
||||||
model.applyRelayConfiguration()
|
model.applyRelayConfiguration()
|
||||||
@@ -134,8 +134,8 @@ final class SettingsModelTests: XCTestCase {
|
|||||||
core.initializeResults = [.failure(TestError.unimplemented), .success(())]
|
core.initializeResults = [.failure(TestError.unimplemented), .success(())]
|
||||||
let preferences = Fixtures.preferences()
|
let preferences = Fixtures.preferences()
|
||||||
let model = makeModel(core, preferences: preferences)
|
let model = makeModel(core, preferences: preferences)
|
||||||
let attempted = RelayConfiguration(mode: .custom, relayURLs: ["https://relay.example"])
|
let attempted = RelayConfiguration(mode: .strictCustom, relayURLs: ["https://relay.example"])
|
||||||
model.setRelayMode(.custom)
|
model.setRelayMode(.strictCustom)
|
||||||
model.setRelayURL(attempted.relayURLs[0], at: 0)
|
model.setRelayURL(attempted.relayURLs[0], at: 0)
|
||||||
|
|
||||||
model.applyRelayConfiguration()
|
model.applyRelayConfiguration()
|
||||||
|
|||||||
@@ -21,7 +21,13 @@ enum FolderAccessStatus {
|
|||||||
|
|
||||||
enum RelayPreferenceMode: String, Codable, CaseIterable, Sendable {
|
enum RelayPreferenceMode: String, Codable, CaseIterable, Sendable {
|
||||||
case automatic
|
case automatic
|
||||||
case custom
|
case strictCustom = "custom"
|
||||||
|
case customWithDirectFallback = "custom-with-direct-fallback"
|
||||||
|
case localOnly = "local-only"
|
||||||
|
|
||||||
|
var usesCustomRelayURLs: Bool {
|
||||||
|
self == .strictCustom || self == .customWithDirectFallback
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct RelayConfiguration: Equatable, Codable, Sendable {
|
struct RelayConfiguration: Equatable, Codable, Sendable {
|
||||||
@@ -55,8 +61,8 @@ enum RelayConfigurationValidator {
|
|||||||
relayURLs: [String],
|
relayURLs: [String],
|
||||||
retainedRelayURLs: [String] = []
|
retainedRelayURLs: [String] = []
|
||||||
) throws -> RelayConfiguration {
|
) throws -> RelayConfiguration {
|
||||||
guard mode == .custom else {
|
guard mode.usesCustomRelayURLs else {
|
||||||
return RelayConfiguration(mode: .automatic, relayURLs: retainedRelayURLs)
|
return RelayConfiguration(mode: mode, relayURLs: retainedRelayURLs)
|
||||||
}
|
}
|
||||||
|
|
||||||
let relayEntries = relayURLs.enumerated().compactMap { index, value -> (Int, String)? in
|
let relayEntries = relayURLs.enumerated().compactMap { index, value -> (Int, String)? in
|
||||||
@@ -104,7 +110,7 @@ enum RelayConfigurationValidator {
|
|||||||
normalizedURLs.append(canonicalURL)
|
normalizedURLs.append(canonicalURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
return RelayConfiguration(mode: .custom, relayURLs: normalizedURLs)
|
return RelayConfiguration(mode: mode, relayURLs: normalizedURLs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,10 +183,9 @@ final class AppPreferencesRepository: ObservableObject {
|
|||||||
let data = defaults.data(forKey: Key.relayConfiguration),
|
let data = defaults.data(forKey: Key.relayConfiguration),
|
||||||
let configuration = try? JSONDecoder().decode(RelayConfiguration.self, from: data)
|
let configuration = try? JSONDecoder().decode(RelayConfiguration.self, from: data)
|
||||||
else {
|
else {
|
||||||
// A stored profile must never silently fall back to public relays. Keeping
|
// A stored profile must never silently fall back to public relays. Strict
|
||||||
// Custom selected with no URLs makes startup fail closed and lets Settings
|
// custom with no URLs makes startup fail closed until Settings repairs it.
|
||||||
// offer an explicit repair or reset to Automatic.
|
return RelayConfiguration(mode: .strictCustom, relayURLs: [])
|
||||||
return RelayConfiguration(mode: .custom, relayURLs: [])
|
|
||||||
}
|
}
|
||||||
return configuration
|
return configuration
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,11 +40,18 @@ struct NativeCoreBindingFactory: CoreBindingFactory {
|
|||||||
switch networkConfiguration.mode {
|
switch networkConfiguration.mode {
|
||||||
case .automatic:
|
case .automatic:
|
||||||
nativeConfiguration = defaultCoreNetworkConfig()
|
nativeConfiguration = defaultCoreNetworkConfig()
|
||||||
case .custom:
|
case .strictCustom:
|
||||||
nativeConfiguration = CoreNetworkConfig(
|
nativeConfiguration = CoreNetworkConfig(
|
||||||
mode: .custom,
|
mode: .strictCustom,
|
||||||
relayUrls: networkConfiguration.relayURLs
|
relayUrls: networkConfiguration.relayURLs
|
||||||
)
|
)
|
||||||
|
case .customWithDirectFallback:
|
||||||
|
nativeConfiguration = CoreNetworkConfig(
|
||||||
|
mode: .customWithDirectFallback,
|
||||||
|
relayUrls: networkConfiguration.relayURLs
|
||||||
|
)
|
||||||
|
case .localOnly:
|
||||||
|
nativeConfiguration = CoreNetworkConfig(mode: .localOnly, relayUrls: [])
|
||||||
}
|
}
|
||||||
return try VnidropCore.initializeWithNetworkConfig(
|
return try VnidropCore.initializeWithNetworkConfig(
|
||||||
appDataDir: appDataDir,
|
appDataDir: appDataDir,
|
||||||
|
|||||||
@@ -248,7 +248,7 @@ final class SettingsModel: ObservableObject {
|
|||||||
func setRelayMode(_ mode: RelayPreferenceMode) {
|
func setRelayMode(_ mode: RelayPreferenceMode) {
|
||||||
hasRelayConfigurationDraft = true
|
hasRelayConfigurationDraft = true
|
||||||
state.relayMode = mode
|
state.relayMode = mode
|
||||||
if mode == .custom && state.relayURLs.isEmpty { state.relayURLs = [""] }
|
if mode.usesCustomRelayURLs && state.relayURLs.isEmpty { state.relayURLs = [""] }
|
||||||
updateRelayConfigurationDraft()
|
updateRelayConfigurationDraft()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -351,7 +351,7 @@ final class SettingsModel: ObservableObject {
|
|||||||
state.relayValidationError = nil
|
state.relayValidationError = nil
|
||||||
state.relayApplyErrorKey = nil
|
state.relayApplyErrorKey = nil
|
||||||
let saved = preferences.preferences.relayConfiguration
|
let saved = preferences.preferences.relayConfiguration
|
||||||
let draftURLs = state.relayMode == .automatic ? saved.relayURLs : state.relayURLs
|
let draftURLs = state.relayMode.usesCustomRelayURLs ? state.relayURLs : saved.relayURLs
|
||||||
state.relayConfigurationIsDirty = saved != RelayConfiguration(mode: state.relayMode, relayURLs: draftURLs)
|
state.relayConfigurationIsDirty = saved != RelayConfiguration(mode: state.relayMode, relayURLs: draftURLs)
|
||||||
hasRelayConfigurationDraft = state.relayConfigurationIsDirty
|
hasRelayConfigurationDraft = state.relayConfigurationIsDirty
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -125,7 +125,18 @@ private struct SettingsSectionContent: View {
|
|||||||
func relayModeLabel(_ mode: RelayPreferenceMode) -> String {
|
func relayModeLabel(_ mode: RelayPreferenceMode) -> String {
|
||||||
switch mode {
|
switch mode {
|
||||||
case .automatic: return String(localized: "relay_mode_automatic")
|
case .automatic: return String(localized: "relay_mode_automatic")
|
||||||
case .custom: return String(localized: "relay_mode_custom")
|
case .strictCustom: return String(localized: "relay_mode_custom")
|
||||||
|
case .customWithDirectFallback: return String(localized: "relay_mode_custom_direct_fallback")
|
||||||
|
case .localOnly: return String(localized: "relay_mode_local_only")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func relayModeDescriptionKey(_ mode: RelayPreferenceMode) -> String {
|
||||||
|
switch mode {
|
||||||
|
case .automatic: return "relay_mode_automatic_description"
|
||||||
|
case .strictCustom: return "relay_mode_custom_description"
|
||||||
|
case .customWithDirectFallback: return "relay_mode_custom_direct_fallback_description"
|
||||||
|
case .localOnly: return "relay_mode_local_only_description"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -70,15 +70,10 @@ struct NetworkSettings: View {
|
|||||||
Text(relayModeLabel(mode)).tag(mode)
|
Text(relayModeLabel(mode)).tag(mode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.pickerStyle(.segmented)
|
.pickerStyle(.inline)
|
||||||
.labelsHidden()
|
|
||||||
.disabled(model.state.isApplyingRelayConfiguration)
|
.disabled(model.state.isApplyingRelayConfiguration)
|
||||||
} footer: {
|
} footer: {
|
||||||
Text(LocalizedStringKey(
|
Text(LocalizedStringKey(relayModeDescriptionKey(model.state.relayMode)))
|
||||||
model.state.relayMode == .automatic
|
|
||||||
? "relay_mode_automatic_description"
|
|
||||||
: "relay_mode_custom_description"
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
@@ -99,15 +94,17 @@ struct NetworkSettings: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if model.state.relayMode == .custom {
|
if model.state.relayMode.usesCustomRelayURLs {
|
||||||
Section {
|
Section {
|
||||||
Label {
|
if model.state.relayMode == .strictCustom {
|
||||||
Text(LocalizedStringKey("relay_strict_warning"))
|
Label {
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
Text(LocalizedStringKey("relay_strict_warning"))
|
||||||
} icon: {
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
Image(systemName: "exclamationmark.shield.fill")
|
} icon: {
|
||||||
|
Image(systemName: "exclamationmark.shield.fill")
|
||||||
|
}
|
||||||
|
.foregroundStyle(.orange)
|
||||||
}
|
}
|
||||||
.foregroundStyle(.orange)
|
|
||||||
|
|
||||||
ForEach(Array(model.state.relayURLs.indices), id: \.self) { index in
|
ForEach(Array(model.state.relayURLs.indices), id: \.self) { index in
|
||||||
VStack(alignment: .leading, spacing: 6) {
|
VStack(alignment: .leading, spacing: 6) {
|
||||||
|
|||||||
@@ -10756,55 +10756,55 @@
|
|||||||
"de": {
|
"de": {
|
||||||
"stringUnit": {
|
"stringUnit": {
|
||||||
"state": "needs_review",
|
"state": "needs_review",
|
||||||
"value": "Automatisch"
|
"value": "Automatisch (empfohlen)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"en": {
|
"en": {
|
||||||
"stringUnit": {
|
"stringUnit": {
|
||||||
"state": "translated",
|
"state": "translated",
|
||||||
"value": "Automatic"
|
"value": "Automatic (recommended)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"es": {
|
"es": {
|
||||||
"stringUnit": {
|
"stringUnit": {
|
||||||
"state": "needs_review",
|
"state": "needs_review",
|
||||||
"value": "Automático"
|
"value": "Automático (recomendado)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"fr": {
|
"fr": {
|
||||||
"stringUnit": {
|
"stringUnit": {
|
||||||
"state": "needs_review",
|
"state": "needs_review",
|
||||||
"value": "Automatique"
|
"value": "Automatique (recommandé)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"it": {
|
"it": {
|
||||||
"stringUnit": {
|
"stringUnit": {
|
||||||
"state": "needs_review",
|
"state": "needs_review",
|
||||||
"value": "Automatica"
|
"value": "Automatica (consigliata)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nl": {
|
"nl": {
|
||||||
"stringUnit": {
|
"stringUnit": {
|
||||||
"state": "needs_review",
|
"state": "needs_review",
|
||||||
"value": "Automatisch"
|
"value": "Automatisch (aanbevolen)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"pl": {
|
"pl": {
|
||||||
"stringUnit": {
|
"stringUnit": {
|
||||||
"state": "needs_review",
|
"state": "needs_review",
|
||||||
"value": "Automatyczny"
|
"value": "Automatyczny (zalecany)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"pt": {
|
"pt": {
|
||||||
"stringUnit": {
|
"stringUnit": {
|
||||||
"state": "needs_review",
|
"state": "needs_review",
|
||||||
"value": "Automático"
|
"value": "Automático (recomendado)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ru": {
|
"ru": {
|
||||||
"stringUnit": {
|
"stringUnit": {
|
||||||
"state": "needs_review",
|
"state": "needs_review",
|
||||||
"value": "Автоматически"
|
"value": "Автоматически (рекомендуется)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -10876,55 +10876,55 @@
|
|||||||
"de": {
|
"de": {
|
||||||
"stringUnit": {
|
"stringUnit": {
|
||||||
"state": "needs_review",
|
"state": "needs_review",
|
||||||
"value": "Benutzerdefiniert"
|
"value": "Strikt benutzerdefiniert"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"en": {
|
"en": {
|
||||||
"stringUnit": {
|
"stringUnit": {
|
||||||
"state": "translated",
|
"state": "translated",
|
||||||
"value": "Custom"
|
"value": "Strict custom"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"es": {
|
"es": {
|
||||||
"stringUnit": {
|
"stringUnit": {
|
||||||
"state": "needs_review",
|
"state": "needs_review",
|
||||||
"value": "Personalizado"
|
"value": "Personalizado estricto"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"fr": {
|
"fr": {
|
||||||
"stringUnit": {
|
"stringUnit": {
|
||||||
"state": "needs_review",
|
"state": "needs_review",
|
||||||
"value": "Personnalisé"
|
"value": "Personnalisé strict"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"it": {
|
"it": {
|
||||||
"stringUnit": {
|
"stringUnit": {
|
||||||
"state": "needs_review",
|
"state": "needs_review",
|
||||||
"value": "Personalizzata"
|
"value": "Personalizzata rigorosa"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nl": {
|
"nl": {
|
||||||
"stringUnit": {
|
"stringUnit": {
|
||||||
"state": "needs_review",
|
"state": "needs_review",
|
||||||
"value": "Aangepast"
|
"value": "Strikt aangepast"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"pl": {
|
"pl": {
|
||||||
"stringUnit": {
|
"stringUnit": {
|
||||||
"state": "needs_review",
|
"state": "needs_review",
|
||||||
"value": "Niestandardowy"
|
"value": "Ścisły niestandardowy"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"pt": {
|
"pt": {
|
||||||
"stringUnit": {
|
"stringUnit": {
|
||||||
"state": "needs_review",
|
"state": "needs_review",
|
||||||
"value": "Personalizado"
|
"value": "Personalizado estrito"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ru": {
|
"ru": {
|
||||||
"stringUnit": {
|
"stringUnit": {
|
||||||
"state": "needs_review",
|
"state": "needs_review",
|
||||||
"value": "Пользовательский"
|
"value": "Строго пользовательский"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -10936,55 +10936,295 @@
|
|||||||
"de": {
|
"de": {
|
||||||
"stringUnit": {
|
"stringUnit": {
|
||||||
"state": "needs_review",
|
"state": "needs_review",
|
||||||
"value": "Verwendet ausschließlich die unten aufgeführten Relay-Server."
|
"value": "Verwendet nur die konfigurierten eigenen Relays oder Direktverbindungen. Meldet einen Fehler, wenn kein eigenes Relay erreichbar ist."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"en": {
|
"en": {
|
||||||
"stringUnit": {
|
"stringUnit": {
|
||||||
"state": "translated",
|
"state": "translated",
|
||||||
"value": "Use only the relay servers below."
|
"value": "Use only the configured custom relays or direct connections. Report an error if no custom relay can be established."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"es": {
|
"es": {
|
||||||
"stringUnit": {
|
"stringUnit": {
|
||||||
"state": "needs_review",
|
"state": "needs_review",
|
||||||
"value": "Usa únicamente los servidores de retransmisión indicados a continuación."
|
"value": "Usa solo los relés personalizados configurados o conexiones directas. Informa de un error si no se puede establecer ningún relé personalizado."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"fr": {
|
"fr": {
|
||||||
"stringUnit": {
|
"stringUnit": {
|
||||||
"state": "needs_review",
|
"state": "needs_review",
|
||||||
"value": "Utiliser uniquement les serveurs relais ci-dessous."
|
"value": "Utilise uniquement les relais personnalisés configurés ou les connexions directes. Signale une erreur si aucun relais personnalisé ne peut être établi."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"it": {
|
"it": {
|
||||||
"stringUnit": {
|
"stringUnit": {
|
||||||
"state": "needs_review",
|
"state": "needs_review",
|
||||||
"value": "Usa solo i server relay indicati di seguito."
|
"value": "Usa solo i relay personalizzati configurati o connessioni dirette. Segnala un errore se non è possibile stabilire alcun relay personalizzato."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nl": {
|
"nl": {
|
||||||
"stringUnit": {
|
"stringUnit": {
|
||||||
"state": "needs_review",
|
"state": "needs_review",
|
||||||
"value": "Gebruikt alleen de onderstaande relayservers."
|
"value": "Gebruikt alleen de ingestelde aangepaste relays of rechtstreekse verbindingen. Meldt een fout als geen aangepaste relay bereikbaar is."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"pl": {
|
"pl": {
|
||||||
"stringUnit": {
|
"stringUnit": {
|
||||||
"state": "needs_review",
|
"state": "needs_review",
|
||||||
"value": "Używa wyłącznie poniższych serwerów przekaźnikowych."
|
"value": "Używa tylko skonfigurowanych własnych przekaźników lub połączeń bezpośrednich. Zgłasza błąd, jeśli nie można połączyć się z żadnym własnym przekaźnikiem."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"pt": {
|
"pt": {
|
||||||
"stringUnit": {
|
"stringUnit": {
|
||||||
"state": "needs_review",
|
"state": "needs_review",
|
||||||
"value": "Utiliza apenas os servidores de retransmissão abaixo."
|
"value": "Utiliza apenas os retransmissores personalizados configurados ou ligações diretas. Apresenta um erro se não for possível estabelecer nenhum retransmissor personalizado."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ru": {
|
"ru": {
|
||||||
"stringUnit": {
|
"stringUnit": {
|
||||||
"state": "needs_review",
|
"state": "needs_review",
|
||||||
"value": "Использовать только указанные ниже серверы-ретрансляторы."
|
"value": "Использует только настроенные пользовательские ретрансляторы или прямые соединения. Сообщает об ошибке, если ни один пользовательский ретранслятор недоступен."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"relay_mode_custom_direct_fallback": {
|
||||||
|
"comment": "Network settings label for custom relays that allow direct-only startup fallback.",
|
||||||
|
"extractionState": "manual",
|
||||||
|
"localizations": {
|
||||||
|
"de": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "needs_review",
|
||||||
|
"value": "Benutzerdefiniert mit direktem Rückfall"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Custom with direct fallback"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "needs_review",
|
||||||
|
"value": "Personalizado con conexión directa de reserva"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "needs_review",
|
||||||
|
"value": "Personnalisé avec repli direct"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"it": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "needs_review",
|
||||||
|
"value": "Personalizzata con ripiego diretto"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nl": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "needs_review",
|
||||||
|
"value": "Aangepast met directe terugval"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pl": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "needs_review",
|
||||||
|
"value": "Niestandardowy z trybem bezpośrednim"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pt": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "needs_review",
|
||||||
|
"value": "Personalizado com alternativa direta"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ru": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "needs_review",
|
||||||
|
"value": "Пользовательский с прямым резервом"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"relay_mode_custom_direct_fallback_description": {
|
||||||
|
"comment": "Network settings description of custom relays with direct-only startup fallback.",
|
||||||
|
"extractionState": "manual",
|
||||||
|
"localizations": {
|
||||||
|
"de": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "needs_review",
|
||||||
|
"value": "Bevorzugt die konfigurierten eigenen Relays. Sind sie nicht verfügbar, werden nur Direktverbindungen verwendet. Öffentliche Relays werden nie genutzt."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Prefer the configured custom relays. If unavailable, continue with direct connections only. Never use public relays."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "needs_review",
|
||||||
|
"value": "Prefiere los relés personalizados configurados. Si no están disponibles, continúa solo con conexiones directas. Nunca usa relés públicos."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "needs_review",
|
||||||
|
"value": "Préfère les relais personnalisés configurés. S’ils sont indisponibles, continue uniquement avec des connexions directes. N’utilise jamais les relais publics."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"it": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "needs_review",
|
||||||
|
"value": "Preferisce i relay personalizzati configurati. Se non sono disponibili, continua solo con connessioni dirette. Non usa mai relay pubblici."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nl": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "needs_review",
|
||||||
|
"value": "Geeft de voorkeur aan de ingestelde aangepaste relays. Als die niet beschikbaar zijn, worden alleen rechtstreekse verbindingen gebruikt. Openbare relays worden nooit gebruikt."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pl": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "needs_review",
|
||||||
|
"value": "Preferuje skonfigurowane własne przekaźniki. Jeśli są niedostępne, kontynuuje tylko przez połączenia bezpośrednie. Nigdy nie używa publicznych przekaźników."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pt": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "needs_review",
|
||||||
|
"value": "Prefere os retransmissores personalizados configurados. Se não estiverem disponíveis, continua apenas com ligações diretas. Nunca utiliza retransmissores públicos."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ru": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "needs_review",
|
||||||
|
"value": "Предпочитает настроенные пользовательские ретрансляторы. Если они недоступны, продолжает работу только через прямые соединения. Публичные ретрансляторы не используются."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"relay_mode_local_only": {
|
||||||
|
"comment": "Network settings label for direct connections without any relay.",
|
||||||
|
"extractionState": "manual",
|
||||||
|
"localizations": {
|
||||||
|
"de": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "needs_review",
|
||||||
|
"value": "Nur lokales Netzwerk"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Local only"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "needs_review",
|
||||||
|
"value": "Solo red local"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "needs_review",
|
||||||
|
"value": "Réseau local uniquement"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"it": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "needs_review",
|
||||||
|
"value": "Solo rete locale"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nl": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "needs_review",
|
||||||
|
"value": "Alleen lokaal netwerk"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pl": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "needs_review",
|
||||||
|
"value": "Tylko sieć lokalna"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pt": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "needs_review",
|
||||||
|
"value": "Apenas rede local"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ru": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "needs_review",
|
||||||
|
"value": "Только локальная сеть"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"relay_mode_local_only_description": {
|
||||||
|
"comment": "Network settings description of direct-only local-network mode.",
|
||||||
|
"extractionState": "manual",
|
||||||
|
"localizations": {
|
||||||
|
"de": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "needs_review",
|
||||||
|
"value": "Deaktiviert alle Relays und erlaubt nur Direktverbindungen, hauptsächlich für Geräte im selben Netzwerk."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Disable all relays and allow only direct connections, primarily for devices on the same network."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "needs_review",
|
||||||
|
"value": "Desactiva todos los relés y permite solo conexiones directas, principalmente para dispositivos de la misma red."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "needs_review",
|
||||||
|
"value": "Désactive tous les relais et autorise uniquement les connexions directes, principalement pour les appareils sur le même réseau."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"it": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "needs_review",
|
||||||
|
"value": "Disattiva tutti i relay e consente solo connessioni dirette, soprattutto per dispositivi sulla stessa rete."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nl": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "needs_review",
|
||||||
|
"value": "Schakelt alle relays uit en staat alleen rechtstreekse verbindingen toe, vooral voor apparaten in hetzelfde netwerk."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pl": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "needs_review",
|
||||||
|
"value": "Wyłącza wszystkie przekaźniki i zezwala tylko na połączenia bezpośrednie, głównie dla urządzeń w tej samej sieci."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pt": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "needs_review",
|
||||||
|
"value": "Desativa todos os retransmissores e permite apenas ligações diretas, principalmente para dispositivos na mesma rede."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ru": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "needs_review",
|
||||||
|
"value": "Отключает все ретрансляторы и разрешает только прямые соединения, прежде всего для устройств в одной сети."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -11236,55 +11476,55 @@
|
|||||||
"de": {
|
"de": {
|
||||||
"stringUnit": {
|
"stringUnit": {
|
||||||
"state": "needs_review",
|
"state": "needs_review",
|
||||||
"value": "Der benutzerdefinierte Modus ist strikt: VniDrop greift weder auf öffentliche Relays noch auf öffentliche Erkennung zurück. Andere Geräte müssen Ihre konfigurierten Relays erreichen können."
|
"value": "Der strikt benutzerdefinierte Modus startet nur, wenn mindestens ein konfiguriertes Relay erreichbar ist. VniDrop verwendet in diesem Modus nie öffentliche Relays oder öffentliche Erkennung."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"en": {
|
"en": {
|
||||||
"stringUnit": {
|
"stringUnit": {
|
||||||
"state": "translated",
|
"state": "translated",
|
||||||
"value": "Custom mode is strict: VniDrop will not fall back to public relays or public discovery. Other devices must be able to reach your configured relays."
|
"value": "Strict custom mode will not start unless at least one configured relay is reachable. VniDrop never uses public relays or public discovery in this mode."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"es": {
|
"es": {
|
||||||
"stringUnit": {
|
"stringUnit": {
|
||||||
"state": "needs_review",
|
"state": "needs_review",
|
||||||
"value": "El modo personalizado es estricto: VniDrop no recurrirá a relés públicos ni al descubrimiento público. Los demás dispositivos deben poder acceder a los relés configurados."
|
"value": "El modo personalizado estricto no se inicia a menos que se pueda acceder al menos a un relé configurado. VniDrop nunca usa relés ni descubrimiento públicos en este modo."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"fr": {
|
"fr": {
|
||||||
"stringUnit": {
|
"stringUnit": {
|
||||||
"state": "needs_review",
|
"state": "needs_review",
|
||||||
"value": "Le mode personnalisé est strict : VniDrop n’utilisera ni les relais publics ni la découverte publique comme solution de repli. Les autres appareils doivent pouvoir accéder aux relais configurés."
|
"value": "Le mode personnalisé strict ne démarre que si au moins un relais configuré est accessible. VniDrop n’utilise jamais de relais ni de découverte publics dans ce mode."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"it": {
|
"it": {
|
||||||
"stringUnit": {
|
"stringUnit": {
|
||||||
"state": "needs_review",
|
"state": "needs_review",
|
||||||
"value": "La modalità personalizzata è rigorosa: VniDrop non userà relay pubblici o il rilevamento pubblico come ripiego. Gli altri dispositivi devono poter raggiungere i relay configurati."
|
"value": "La modalità personalizzata rigorosa si avvia solo se almeno un relay configurato è raggiungibile. In questa modalità VniDrop non usa mai relay o rilevamento pubblici."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nl": {
|
"nl": {
|
||||||
"stringUnit": {
|
"stringUnit": {
|
||||||
"state": "needs_review",
|
"state": "needs_review",
|
||||||
"value": "De aangepaste modus is strikt: VniDrop valt niet terug op openbare relays of openbare detectie. Andere apparaten moeten uw ingestelde relays kunnen bereiken."
|
"value": "De strikt aangepaste modus start alleen als minstens één ingestelde relay bereikbaar is. VniDrop gebruikt in deze modus nooit openbare relays of openbare detectie."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"pl": {
|
"pl": {
|
||||||
"stringUnit": {
|
"stringUnit": {
|
||||||
"state": "needs_review",
|
"state": "needs_review",
|
||||||
"value": "Tryb niestandardowy jest rygorystyczny: VniDrop nie użyje awaryjnie publicznych przekaźników ani publicznego wykrywania. Inne urządzenia muszą mieć dostęp do skonfigurowanych przekaźników."
|
"value": "Ścisły tryb niestandardowy uruchamia się tylko wtedy, gdy co najmniej jeden skonfigurowany przekaźnik jest dostępny. VniDrop nigdy nie używa w tym trybie publicznych przekaźników ani publicznego wykrywania."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"pt": {
|
"pt": {
|
||||||
"stringUnit": {
|
"stringUnit": {
|
||||||
"state": "needs_review",
|
"state": "needs_review",
|
||||||
"value": "O modo personalizado é estrito: o VniDrop não recorrerá a retransmissores públicos nem à descoberta pública. Os outros dispositivos têm de conseguir aceder aos retransmissores configurados."
|
"value": "O modo personalizado estrito só inicia se pelo menos um retransmissor configurado estiver acessível. Neste modo, o VniDrop nunca utiliza retransmissores nem descoberta públicos."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ru": {
|
"ru": {
|
||||||
"stringUnit": {
|
"stringUnit": {
|
||||||
"state": "needs_review",
|
"state": "needs_review",
|
||||||
"value": "Пользовательский режим работает строго: VniDrop не будет переключаться на публичные ретрансляторы или публичное обнаружение. Другие устройства должны иметь доступ к настроенным ретрансляторам."
|
"value": "Строго пользовательский режим запускается, только если доступен хотя бы один настроенный ретранслятор. В этом режиме VniDrop никогда не использует публичные ретрансляторы или публичное обнаружение."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,9 @@ pub(crate) const MAX_RELAY_URL_BYTES: usize = 2_048;
|
|||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, uniffi::Enum)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, uniffi::Enum)]
|
||||||
pub enum CoreRelayMode {
|
pub enum CoreRelayMode {
|
||||||
Automatic,
|
Automatic,
|
||||||
Custom,
|
StrictCustom,
|
||||||
|
CustomWithDirectFallback,
|
||||||
|
LocalOnly,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, uniffi::Record)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, uniffi::Record)]
|
||||||
@@ -33,13 +35,21 @@ impl Default for CoreNetworkConfig {
|
|||||||
impl CoreNetworkConfig {
|
impl CoreNetworkConfig {
|
||||||
pub(crate) fn validated_relay_urls(&self) -> anyhow::Result<Vec<RelayUrl>> {
|
pub(crate) fn validated_relay_urls(&self) -> anyhow::Result<Vec<RelayUrl>> {
|
||||||
match self.mode {
|
match self.mode {
|
||||||
CoreRelayMode::Automatic => {
|
CoreRelayMode::Automatic | CoreRelayMode::LocalOnly => {
|
||||||
if !self.relay_urls.is_empty() {
|
if !self.relay_urls.is_empty() {
|
||||||
anyhow::bail!("automatic relay mode must not include custom relay URLs");
|
anyhow::bail!(
|
||||||
|
"{} relay mode must not include custom relay URLs",
|
||||||
|
match self.mode {
|
||||||
|
CoreRelayMode::Automatic => "automatic",
|
||||||
|
CoreRelayMode::LocalOnly => "local-only",
|
||||||
|
CoreRelayMode::StrictCustom
|
||||||
|
| CoreRelayMode::CustomWithDirectFallback => unreachable!(),
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
Ok(Vec::new())
|
Ok(Vec::new())
|
||||||
}
|
}
|
||||||
CoreRelayMode::Custom => {
|
CoreRelayMode::StrictCustom | CoreRelayMode::CustomWithDirectFallback => {
|
||||||
if self.relay_urls.is_empty() {
|
if self.relay_urls.is_empty() {
|
||||||
anyhow::bail!("custom relay mode requires at least one relay URL");
|
anyhow::bail!("custom relay mode requires at least one relay URL");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,6 +61,23 @@ use crate::{
|
|||||||
|
|
||||||
const RELAY_CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
|
const RELAY_CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub(crate) enum RelayStatus {
|
||||||
|
Disabled,
|
||||||
|
Connected,
|
||||||
|
Unreachable,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RelayStatus {
|
||||||
|
fn as_str(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Disabled => "disabled",
|
||||||
|
Self::Connected => "connected",
|
||||||
|
Self::Unreachable => "unreachable",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Owns the Iroh endpoint, blob store, transfer history, and byte streaming.
|
/// Owns the Iroh endpoint, blob store, transfer history, and byte streaming.
|
||||||
/// Kotlin owns app lifecycle and platform file picking.
|
/// Kotlin owns app lifecycle and platform file picking.
|
||||||
pub(super) struct CoreInner {
|
pub(super) struct CoreInner {
|
||||||
@@ -122,7 +139,7 @@ impl CoreInner {
|
|||||||
.bind()
|
.bind()
|
||||||
.await?
|
.await?
|
||||||
}
|
}
|
||||||
CoreRelayMode::Custom => {
|
CoreRelayMode::StrictCustom | CoreRelayMode::CustomWithDirectFallback => {
|
||||||
let relay_map = RelayMap::from_iter(relay_urls.iter().cloned().map(|url| {
|
let relay_map = RelayMap::from_iter(relay_urls.iter().cloned().map(|url| {
|
||||||
// Loopback HTTP is a development escape hatch. Without TLS the
|
// Loopback HTTP is a development escape hatch. Without TLS the
|
||||||
// relay cannot serve Iroh's QUIC address-discovery endpoint.
|
// relay cannot serve Iroh's QUIC address-discovery endpoint.
|
||||||
@@ -141,13 +158,22 @@ impl CoreInner {
|
|||||||
.bind()
|
.bind()
|
||||||
.await?
|
.await?
|
||||||
}
|
}
|
||||||
|
CoreRelayMode::LocalOnly => {
|
||||||
|
Endpoint::builder(presets::Minimal)
|
||||||
|
.relay_mode(RelayMode::Disabled)
|
||||||
|
.secret_key(secret_key)
|
||||||
|
.bind()
|
||||||
|
.await?
|
||||||
|
}
|
||||||
};
|
};
|
||||||
if let Err(error) =
|
let relay_status =
|
||||||
wait_for_relay(&endpoint, relay_mode, &relay_urls, RELAY_CONNECT_TIMEOUT).await
|
match wait_for_relay(&endpoint, relay_mode, &relay_urls, RELAY_CONNECT_TIMEOUT).await {
|
||||||
{
|
Ok(status) => status,
|
||||||
endpoint.close().await;
|
Err(error) => {
|
||||||
return Err(error);
|
endpoint.close().await;
|
||||||
}
|
return Err(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Provider events are where the sender sees remote readers. The core
|
// Provider events are where the sender sees remote readers. The core
|
||||||
// uses them for send progress and for the current approval gate.
|
// uses them for send progress and for the current approval gate.
|
||||||
@@ -160,6 +186,14 @@ impl CoreInner {
|
|||||||
limits.event_queue_capacity as usize,
|
limits.event_queue_capacity as usize,
|
||||||
limits.max_events,
|
limits.max_events,
|
||||||
));
|
));
|
||||||
|
event_hub.emit_endpoint(
|
||||||
|
"network",
|
||||||
|
"relay-status",
|
||||||
|
json!({
|
||||||
|
"mode": relay_mode_label(relay_mode),
|
||||||
|
"status": relay_status.as_str(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
for recovered in recovered_transfers {
|
for recovered in recovered_transfers {
|
||||||
event_hub.emit_transfer(
|
event_hub.emit_transfer(
|
||||||
recovered.transfer_id,
|
recovered.transfer_id,
|
||||||
@@ -377,7 +411,7 @@ pub(crate) fn filter_peer_addr_for_relay_mode(
|
|||||||
) -> Result<EndpointAddr> {
|
) -> Result<EndpointAddr> {
|
||||||
match relay_mode {
|
match relay_mode {
|
||||||
CoreRelayMode::Automatic => Ok(addr.clone()),
|
CoreRelayMode::Automatic => Ok(addr.clone()),
|
||||||
CoreRelayMode::Custom => {
|
CoreRelayMode::StrictCustom | CoreRelayMode::CustomWithDirectFallback => {
|
||||||
let mut filtered = EndpointAddr::new(addr.id);
|
let mut filtered = EndpointAddr::new(addr.id);
|
||||||
for ip_addr in addr.ip_addrs().copied() {
|
for ip_addr in addr.ip_addrs().copied() {
|
||||||
filtered = filtered.with_ip_addr(ip_addr);
|
filtered = filtered.with_ip_addr(ip_addr);
|
||||||
@@ -396,6 +430,16 @@ pub(crate) fn filter_peer_addr_for_relay_mode(
|
|||||||
}
|
}
|
||||||
Ok(filtered)
|
Ok(filtered)
|
||||||
}
|
}
|
||||||
|
CoreRelayMode::LocalOnly => {
|
||||||
|
let mut filtered = EndpointAddr::new(addr.id);
|
||||||
|
for ip_addr in addr.ip_addrs().copied() {
|
||||||
|
filtered = filtered.with_ip_addr(ip_addr);
|
||||||
|
}
|
||||||
|
if filtered.is_empty() {
|
||||||
|
anyhow::bail!("invitation has no direct address allowed by local-only mode");
|
||||||
|
}
|
||||||
|
Ok(filtered)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -404,17 +448,16 @@ pub(crate) async fn wait_for_relay(
|
|||||||
relay_mode: CoreRelayMode,
|
relay_mode: CoreRelayMode,
|
||||||
relay_urls: &[RelayUrl],
|
relay_urls: &[RelayUrl],
|
||||||
timeout: Duration,
|
timeout: Duration,
|
||||||
) -> Result<()> {
|
) -> Result<RelayStatus> {
|
||||||
|
if relay_mode == CoreRelayMode::LocalOnly {
|
||||||
|
return Ok(RelayStatus::Disabled);
|
||||||
|
}
|
||||||
if tokio::time::timeout(timeout, endpoint.online())
|
if tokio::time::timeout(timeout, endpoint.online())
|
||||||
.await
|
.await
|
||||||
.is_err()
|
.is_err()
|
||||||
{
|
{
|
||||||
match relay_mode {
|
match relay_mode {
|
||||||
CoreRelayMode::Automatic => anyhow::bail!(
|
CoreRelayMode::StrictCustom => {
|
||||||
"timed out after {} seconds while connecting to automatic relays; verify network access and relay availability",
|
|
||||||
timeout.as_secs_f32(),
|
|
||||||
),
|
|
||||||
CoreRelayMode::Custom => {
|
|
||||||
let configured_relays = relay_urls
|
let configured_relays = relay_urls
|
||||||
.iter()
|
.iter()
|
||||||
.map(ToString::to_string)
|
.map(ToString::to_string)
|
||||||
@@ -425,9 +468,22 @@ pub(crate) async fn wait_for_relay(
|
|||||||
timeout.as_secs_f32(),
|
timeout.as_secs_f32(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
CoreRelayMode::Automatic | CoreRelayMode::CustomWithDirectFallback => {
|
||||||
|
return Ok(RelayStatus::Unreachable);
|
||||||
|
}
|
||||||
|
CoreRelayMode::LocalOnly => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(RelayStatus::Connected)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn relay_mode_label(relay_mode: CoreRelayMode) -> &'static str {
|
||||||
|
match relay_mode {
|
||||||
|
CoreRelayMode::Automatic => "automatic",
|
||||||
|
CoreRelayMode::StrictCustom => "strict-custom",
|
||||||
|
CoreRelayMode::CustomWithDirectFallback => "custom-with-direct-fallback",
|
||||||
|
CoreRelayMode::LocalOnly => "local-only",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn share_tag_name(local_id: &str) -> String {
|
pub(super) fn share_tag_name(local_id: &str) -> String {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use crate::{
|
|||||||
default_core_network_config, CoreNetworkConfig, CoreRelayMode, MAX_CUSTOM_RELAYS,
|
default_core_network_config, CoreNetworkConfig, CoreRelayMode, MAX_CUSTOM_RELAYS,
|
||||||
MAX_RELAY_URL_BYTES,
|
MAX_RELAY_URL_BYTES,
|
||||||
},
|
},
|
||||||
runtime::{filter_peer_addr_for_relay_mode, wait_for_relay},
|
runtime::{filter_peer_addr_for_relay_mode, wait_for_relay, RelayStatus},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -32,17 +32,28 @@ fn relay_mode_and_url_list_must_be_consistent() {
|
|||||||
};
|
};
|
||||||
assert!(automatic_with_url.validated_relay_urls().is_err());
|
assert!(automatic_with_url.validated_relay_urls().is_err());
|
||||||
|
|
||||||
let custom_without_url = CoreNetworkConfig {
|
for mode in [
|
||||||
mode: CoreRelayMode::Custom,
|
CoreRelayMode::StrictCustom,
|
||||||
relay_urls: Vec::new(),
|
CoreRelayMode::CustomWithDirectFallback,
|
||||||
|
] {
|
||||||
|
let custom_without_url = CoreNetworkConfig {
|
||||||
|
mode,
|
||||||
|
relay_urls: Vec::new(),
|
||||||
|
};
|
||||||
|
assert!(custom_without_url.validated_relay_urls().is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
let local_only_with_url = CoreNetworkConfig {
|
||||||
|
mode: CoreRelayMode::LocalOnly,
|
||||||
|
relay_urls: vec!["https://relay.example.com".to_string()],
|
||||||
};
|
};
|
||||||
assert!(custom_without_url.validated_relay_urls().is_err());
|
assert!(local_only_with_url.validated_relay_urls().is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn custom_relay_urls_allow_https_and_loopback_http() {
|
fn custom_relay_urls_allow_https_and_loopback_http() {
|
||||||
let config = CoreNetworkConfig {
|
let config = CoreNetworkConfig {
|
||||||
mode: CoreRelayMode::Custom,
|
mode: CoreRelayMode::StrictCustom,
|
||||||
relay_urls: vec![
|
relay_urls: vec![
|
||||||
"https://relay.example.com".to_string(),
|
"https://relay.example.com".to_string(),
|
||||||
"http://localhost:3340".to_string(),
|
"http://localhost:3340".to_string(),
|
||||||
@@ -68,7 +79,7 @@ fn custom_relay_urls_reject_unsafe_or_ambiguous_values() {
|
|||||||
" https://relay.example.com",
|
" https://relay.example.com",
|
||||||
] {
|
] {
|
||||||
let config = CoreNetworkConfig {
|
let config = CoreNetworkConfig {
|
||||||
mode: CoreRelayMode::Custom,
|
mode: CoreRelayMode::StrictCustom,
|
||||||
relay_urls: vec![value.to_string()],
|
relay_urls: vec![value.to_string()],
|
||||||
};
|
};
|
||||||
assert!(
|
assert!(
|
||||||
@@ -81,7 +92,7 @@ fn custom_relay_urls_reject_unsafe_or_ambiguous_values() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn custom_relay_urls_are_bounded_and_unique_after_normalization() {
|
fn custom_relay_urls_are_bounded_and_unique_after_normalization() {
|
||||||
let duplicates = CoreNetworkConfig {
|
let duplicates = CoreNetworkConfig {
|
||||||
mode: CoreRelayMode::Custom,
|
mode: CoreRelayMode::StrictCustom,
|
||||||
relay_urls: vec![
|
relay_urls: vec![
|
||||||
"https://relay.example.com".to_string(),
|
"https://relay.example.com".to_string(),
|
||||||
"https://relay.example.com/".to_string(),
|
"https://relay.example.com/".to_string(),
|
||||||
@@ -90,7 +101,7 @@ fn custom_relay_urls_are_bounded_and_unique_after_normalization() {
|
|||||||
assert!(duplicates.validated_relay_urls().is_err());
|
assert!(duplicates.validated_relay_urls().is_err());
|
||||||
|
|
||||||
let too_many = CoreNetworkConfig {
|
let too_many = CoreNetworkConfig {
|
||||||
mode: CoreRelayMode::Custom,
|
mode: CoreRelayMode::StrictCustom,
|
||||||
relay_urls: (0..=MAX_CUSTOM_RELAYS)
|
relay_urls: (0..=MAX_CUSTOM_RELAYS)
|
||||||
.map(|index| format!("https://relay-{index}.example.com"))
|
.map(|index| format!("https://relay-{index}.example.com"))
|
||||||
.collect(),
|
.collect(),
|
||||||
@@ -98,7 +109,7 @@ fn custom_relay_urls_are_bounded_and_unique_after_normalization() {
|
|||||||
assert!(too_many.validated_relay_urls().is_err());
|
assert!(too_many.validated_relay_urls().is_err());
|
||||||
|
|
||||||
let too_long = CoreNetworkConfig {
|
let too_long = CoreNetworkConfig {
|
||||||
mode: CoreRelayMode::Custom,
|
mode: CoreRelayMode::StrictCustom,
|
||||||
relay_urls: vec![format!(
|
relay_urls: vec![format!(
|
||||||
"https://{}.example.com",
|
"https://{}.example.com",
|
||||||
"a".repeat(MAX_RELAY_URL_BYTES)
|
"a".repeat(MAX_RELAY_URL_BYTES)
|
||||||
@@ -119,7 +130,7 @@ fn strict_custom_mode_filters_peer_relays_but_retains_direct_addresses() {
|
|||||||
|
|
||||||
let filtered = filter_peer_addr_for_relay_mode(
|
let filtered = filter_peer_addr_for_relay_mode(
|
||||||
&addr,
|
&addr,
|
||||||
CoreRelayMode::Custom,
|
CoreRelayMode::StrictCustom,
|
||||||
std::slice::from_ref(&allowed),
|
std::slice::from_ref(&allowed),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -140,14 +151,32 @@ fn strict_custom_mode_filters_peer_relays_but_retains_direct_addresses() {
|
|||||||
EndpointAddr::new(SecretKey::generate().public()).with_relay_url(disallowed);
|
EndpointAddr::new(SecretKey::generate().public()).with_relay_url(disallowed);
|
||||||
assert!(filter_peer_addr_for_relay_mode(
|
assert!(filter_peer_addr_for_relay_mode(
|
||||||
&disallowed_only,
|
&disallowed_only,
|
||||||
CoreRelayMode::Custom,
|
CoreRelayMode::StrictCustom,
|
||||||
std::slice::from_ref(&allowed),
|
std::slice::from_ref(&allowed),
|
||||||
)
|
)
|
||||||
.is_err());
|
.is_err());
|
||||||
|
|
||||||
|
let fallback_filtered = filter_peer_addr_for_relay_mode(
|
||||||
|
&addr,
|
||||||
|
CoreRelayMode::CustomWithDirectFallback,
|
||||||
|
std::slice::from_ref(&allowed),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(fallback_filtered, filtered);
|
||||||
|
|
||||||
|
let local_only = filter_peer_addr_for_relay_mode(&addr, CoreRelayMode::LocalOnly, &[]).unwrap();
|
||||||
|
assert_eq!(local_only.relay_urls().count(), 0);
|
||||||
|
assert_eq!(
|
||||||
|
local_only.ip_addrs().copied().collect::<Vec<_>>(),
|
||||||
|
vec![direct]
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
filter_peer_addr_for_relay_mode(&disallowed_only, CoreRelayMode::LocalOnly, &[]).is_err()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
async fn unreachable_relay_wait_is_bounded_and_actionable_for_each_mode() {
|
async fn relay_wait_enforces_only_strict_custom_mode() {
|
||||||
let relay_url: RelayUrl = "http://127.0.0.1:9".parse().unwrap();
|
let relay_url: RelayUrl = "http://127.0.0.1:9".parse().unwrap();
|
||||||
let endpoint = Endpoint::builder(presets::Minimal)
|
let endpoint = Endpoint::builder(presets::Minimal)
|
||||||
.relay_mode(RelayMode::custom([relay_url.clone()]))
|
.relay_mode(RelayMode::custom([relay_url.clone()]))
|
||||||
@@ -158,7 +187,7 @@ async fn unreachable_relay_wait_is_bounded_and_actionable_for_each_mode() {
|
|||||||
|
|
||||||
let error = wait_for_relay(
|
let error = wait_for_relay(
|
||||||
&endpoint,
|
&endpoint,
|
||||||
CoreRelayMode::Custom,
|
CoreRelayMode::StrictCustom,
|
||||||
std::slice::from_ref(&relay_url),
|
std::slice::from_ref(&relay_url),
|
||||||
Duration::from_millis(50),
|
Duration::from_millis(50),
|
||||||
)
|
)
|
||||||
@@ -169,18 +198,35 @@ async fn unreachable_relay_wait_is_bounded_and_actionable_for_each_mode() {
|
|||||||
assert!(error.to_string().contains(relay_url.as_str()));
|
assert!(error.to_string().contains(relay_url.as_str()));
|
||||||
assert!(error.to_string().contains("verify the URLs"));
|
assert!(error.to_string().contains("verify the URLs"));
|
||||||
|
|
||||||
let automatic_error = wait_for_relay(
|
let automatic_status = wait_for_relay(
|
||||||
&endpoint,
|
&endpoint,
|
||||||
CoreRelayMode::Automatic,
|
CoreRelayMode::Automatic,
|
||||||
&[],
|
&[],
|
||||||
Duration::from_millis(50),
|
Duration::from_millis(50),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap_err();
|
.unwrap();
|
||||||
assert!(started.elapsed() < Duration::from_secs(1));
|
assert!(started.elapsed() < Duration::from_secs(1));
|
||||||
assert!(automatic_error.to_string().contains("automatic relays"));
|
assert_eq!(automatic_status, RelayStatus::Unreachable);
|
||||||
assert!(automatic_error
|
|
||||||
.to_string()
|
let fallback_status = wait_for_relay(
|
||||||
.contains("verify network access"));
|
&endpoint,
|
||||||
|
CoreRelayMode::CustomWithDirectFallback,
|
||||||
|
std::slice::from_ref(&relay_url),
|
||||||
|
Duration::from_millis(50),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(fallback_status, RelayStatus::Unreachable);
|
||||||
|
|
||||||
|
let local_only_status = wait_for_relay(
|
||||||
|
&endpoint,
|
||||||
|
CoreRelayMode::LocalOnly,
|
||||||
|
&[],
|
||||||
|
Duration::from_millis(50),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(local_only_status, RelayStatus::Disabled);
|
||||||
endpoint.close().await;
|
endpoint.close().await;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,14 +120,21 @@ fn saved_ticket_relay_profile_matching_is_mode_aware_and_order_insensitive() {
|
|||||||
assert!(ticket_matches_relay_profile(
|
assert!(ticket_matches_relay_profile(
|
||||||
&custom_ticket,
|
&custom_ticket,
|
||||||
&limits,
|
&limits,
|
||||||
CoreRelayMode::Custom,
|
CoreRelayMode::StrictCustom,
|
||||||
|
&[relay_b.clone(), relay_a.clone()],
|
||||||
|
)
|
||||||
|
.unwrap());
|
||||||
|
assert!(ticket_matches_relay_profile(
|
||||||
|
&custom_ticket,
|
||||||
|
&limits,
|
||||||
|
CoreRelayMode::CustomWithDirectFallback,
|
||||||
&[relay_b.clone(), relay_a.clone()],
|
&[relay_b.clone(), relay_a.clone()],
|
||||||
)
|
)
|
||||||
.unwrap());
|
.unwrap());
|
||||||
assert!(!ticket_matches_relay_profile(
|
assert!(!ticket_matches_relay_profile(
|
||||||
&custom_ticket,
|
&custom_ticket,
|
||||||
&limits,
|
&limits,
|
||||||
CoreRelayMode::Custom,
|
CoreRelayMode::StrictCustom,
|
||||||
&[relay_a.clone(), relay_c],
|
&[relay_a.clone(), relay_c],
|
||||||
)
|
)
|
||||||
.unwrap());
|
.unwrap());
|
||||||
@@ -145,10 +152,21 @@ fn saved_ticket_relay_profile_matching_is_mode_aware_and_order_insensitive() {
|
|||||||
assert!(!ticket_matches_relay_profile(
|
assert!(!ticket_matches_relay_profile(
|
||||||
&automatic_ticket,
|
&automatic_ticket,
|
||||||
&limits,
|
&limits,
|
||||||
CoreRelayMode::Custom,
|
CoreRelayMode::StrictCustom,
|
||||||
&[relay_a],
|
&[relay_a],
|
||||||
)
|
)
|
||||||
.unwrap());
|
.unwrap());
|
||||||
|
assert!(ticket_matches_relay_profile(
|
||||||
|
&automatic_ticket,
|
||||||
|
&limits,
|
||||||
|
CoreRelayMode::LocalOnly,
|
||||||
|
&[],
|
||||||
|
)
|
||||||
|
.unwrap());
|
||||||
|
assert!(
|
||||||
|
!ticket_matches_relay_profile(&custom_ticket, &limits, CoreRelayMode::LocalOnly, &[],)
|
||||||
|
.unwrap()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ pub(crate) fn parse_transfer_ticket_with_limits(
|
|||||||
Vec::new()
|
Vec::new()
|
||||||
} else {
|
} else {
|
||||||
CoreNetworkConfig {
|
CoreNetworkConfig {
|
||||||
mode: CoreRelayMode::Custom,
|
mode: CoreRelayMode::StrictCustom,
|
||||||
relay_urls: ticket.relay_urls,
|
relay_urls: ticket.relay_urls,
|
||||||
}
|
}
|
||||||
.validated_relay_urls()
|
.validated_relay_urls()
|
||||||
@@ -171,7 +171,7 @@ pub(crate) fn ticket_matches_relay_profile(
|
|||||||
let parsed = parse_transfer_ticket_with_limits(value, limits)?;
|
let parsed = parse_transfer_ticket_with_limits(value, limits)?;
|
||||||
match relay_mode {
|
match relay_mode {
|
||||||
CoreRelayMode::Automatic => Ok(parsed.advertised_custom_relay_urls.is_empty()),
|
CoreRelayMode::Automatic => Ok(parsed.advertised_custom_relay_urls.is_empty()),
|
||||||
CoreRelayMode::Custom => {
|
CoreRelayMode::StrictCustom | CoreRelayMode::CustomWithDirectFallback => {
|
||||||
let advertised = parsed
|
let advertised = parsed
|
||||||
.advertised_custom_relay_urls
|
.advertised_custom_relay_urls
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -179,6 +179,8 @@ pub(crate) fn ticket_matches_relay_profile(
|
|||||||
let configured = custom_relay_urls.iter().cloned().collect::<BTreeSet<_>>();
|
let configured = custom_relay_urls.iter().cloned().collect::<BTreeSet<_>>();
|
||||||
Ok(advertised == configured)
|
Ok(advertised == configured)
|
||||||
}
|
}
|
||||||
|
CoreRelayMode::LocalOnly => Ok(parsed.advertised_custom_relay_urls.is_empty()
|
||||||
|
&& parsed.blob_ticket.addr().relay_urls().next().is_none()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,24 +11,64 @@ use vnidrop::{CoreNetworkConfig, CoreRelayMode};
|
|||||||
|
|
||||||
fn custom_config(relay_urls: &[&str]) -> CoreNetworkConfig {
|
fn custom_config(relay_urls: &[&str]) -> CoreNetworkConfig {
|
||||||
CoreNetworkConfig {
|
CoreNetworkConfig {
|
||||||
mode: CoreRelayMode::Custom,
|
mode: CoreRelayMode::StrictCustom,
|
||||||
relay_urls: relay_urls.iter().map(ToString::to_string).collect(),
|
relay_urls: relay_urls.iter().map(ToString::to_string).collect(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn local_only_config() -> CoreNetworkConfig {
|
||||||
|
CoreNetworkConfig {
|
||||||
|
mode: CoreRelayMode::LocalOnly,
|
||||||
|
relay_urls: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn read_blob_ticket(ticket: &str) -> (Value, BlobTicket) {
|
fn read_blob_ticket(ticket: &str) -> (Value, BlobTicket) {
|
||||||
let encoded = ticket.strip_prefix("vnd1:").unwrap();
|
let encoded = ticket.strip_prefix("vnd1:").unwrap();
|
||||||
let payload = BASE64URL_NOPAD.decode(encoded.as_bytes()).unwrap();
|
let payload = BASE64URL_NOPAD.decode(encoded.as_bytes()).unwrap();
|
||||||
let value: Value = serde_json::from_slice(&payload).unwrap();
|
let value: Value = serde_json::from_slice(&payload).unwrap();
|
||||||
let blob_ticket = BlobTicket::from_str(value["blob_ticket"].as_str().unwrap()).unwrap();
|
let blob_ticket = BlobTicket::from_str(value["blob_ticket"].as_str().unwrap()).unwrap();
|
||||||
let (mut addr, hash, format) = blob_ticket.into_parts();
|
let (mut addr, hash, format) = blob_ticket.into_parts();
|
||||||
for relay_url in value["relay_urls"].as_array().unwrap() {
|
if let Some(relay_urls) = value["relay_urls"].as_array() {
|
||||||
addr = addr.with_relay_url(relay_url.as_str().unwrap().parse().unwrap());
|
for relay_url in relay_urls {
|
||||||
|
addr = addr.with_relay_url(relay_url.as_str().unwrap().parse().unwrap());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let blob_ticket = BlobTicket::new(addr, hash, format);
|
let blob_ticket = BlobTicket::new(addr, hash, format);
|
||||||
(value, blob_ticket)
|
(value, blob_ticket)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn local_only_mode_advertises_direct_addresses_and_transfers_on_lan() {
|
||||||
|
let sender = TestNode::with_network_config(local_only_config());
|
||||||
|
let receiver = TestNode::with_network_config(local_only_config());
|
||||||
|
let source_dir = tempfile::tempdir().unwrap();
|
||||||
|
let output_dir = tempfile::tempdir().unwrap();
|
||||||
|
let source_path = source_dir.path().join("local-only.txt");
|
||||||
|
std::fs::write(&source_path, b"direct on the local network").unwrap();
|
||||||
|
|
||||||
|
let share = share_path(&sender.core, &source_path, 402, "local-only.txt", false);
|
||||||
|
let (ticket_value, blob_ticket) = read_blob_ticket(&share.ticket);
|
||||||
|
assert!(ticket_value.get("relay_urls").is_none());
|
||||||
|
assert_eq!(blob_ticket.addr().relay_urls().count(), 0);
|
||||||
|
assert!(!sender.core.status().addr.contains("iroh.link"));
|
||||||
|
|
||||||
|
receive_with_response(
|
||||||
|
&sender.core,
|
||||||
|
share.transfer_id,
|
||||||
|
receiver.core.arc(),
|
||||||
|
share.ticket,
|
||||||
|
output_dir.path(),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
std::fs::read(output_dir.path().join("local-only.txt")).unwrap(),
|
||||||
|
b"direct on the local network"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
fn with_relay_only_address(ticket: &str, relay_url: &str) -> String {
|
fn with_relay_only_address(ticket: &str, relay_url: &str) -> String {
|
||||||
let (mut value, blob_ticket) = read_blob_ticket(ticket);
|
let (mut value, blob_ticket) = read_blob_ticket(ticket);
|
||||||
let relay_url: RelayUrl = relay_url.parse().unwrap();
|
let relay_url: RelayUrl = relay_url.parse().unwrap();
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ fn persisted_share_is_revoked_when_restarted_with_a_different_relay_profile() {
|
|||||||
core_dir.path(),
|
core_dir.path(),
|
||||||
Arc::new(RecordingSink::default()),
|
Arc::new(RecordingSink::default()),
|
||||||
CoreNetworkConfig {
|
CoreNetworkConfig {
|
||||||
mode: CoreRelayMode::Custom,
|
mode: CoreRelayMode::StrictCustom,
|
||||||
relay_urls: vec![relay_a.url.clone()],
|
relay_urls: vec![relay_a.url.clone()],
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -163,7 +163,7 @@ fn persisted_share_is_revoked_when_restarted_with_a_different_relay_profile() {
|
|||||||
core_dir.path(),
|
core_dir.path(),
|
||||||
Arc::new(RecordingSink::default()),
|
Arc::new(RecordingSink::default()),
|
||||||
CoreNetworkConfig {
|
CoreNetworkConfig {
|
||||||
mode: CoreRelayMode::Custom,
|
mode: CoreRelayMode::StrictCustom,
|
||||||
relay_urls: vec![relay_b.url.clone()],
|
relay_urls: vec![relay_b.url.clone()],
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2598,15 +2598,15 @@
|
|||||||
"relay_mode_automatic": {
|
"relay_mode_automatic": {
|
||||||
"context": "Network settings label for VniDrop's automatic public relay mode.",
|
"context": "Network settings label for VniDrop's automatic public relay mode.",
|
||||||
"translations": {
|
"translations": {
|
||||||
"en": "Automatic",
|
"en": "Automatic (recommended)",
|
||||||
"fr": "Automatique",
|
"fr": "Automatique (recommandé)",
|
||||||
"es": "Automático",
|
"es": "Automático (recomendado)",
|
||||||
"it": "Automatica",
|
"it": "Automatica (consigliata)",
|
||||||
"de": "Automatisch",
|
"de": "Automatisch (empfohlen)",
|
||||||
"pt": "Automático",
|
"pt": "Automático (recomendado)",
|
||||||
"pl": "Automatyczny",
|
"pl": "Automatyczny (zalecany)",
|
||||||
"nl": "Automatisch",
|
"nl": "Automatisch (aanbevolen)",
|
||||||
"ru": "Автоматически"
|
"ru": "Автоматически (рекомендуется)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"relay_mode_automatic_description": {
|
"relay_mode_automatic_description": {
|
||||||
@@ -2626,29 +2626,85 @@
|
|||||||
"relay_mode_custom": {
|
"relay_mode_custom": {
|
||||||
"context": "Network settings label for strict custom relay mode.",
|
"context": "Network settings label for strict custom relay mode.",
|
||||||
"translations": {
|
"translations": {
|
||||||
"en": "Custom",
|
"en": "Strict custom",
|
||||||
"fr": "Personnalisé",
|
"fr": "Personnalisé strict",
|
||||||
"es": "Personalizado",
|
"es": "Personalizado estricto",
|
||||||
"it": "Personalizzata",
|
"it": "Personalizzata rigorosa",
|
||||||
"de": "Benutzerdefiniert",
|
"de": "Strikt benutzerdefiniert",
|
||||||
"pt": "Personalizado",
|
"pt": "Personalizado estrito",
|
||||||
"pl": "Niestandardowy",
|
"pl": "Ścisły niestandardowy",
|
||||||
"nl": "Aangepast",
|
"nl": "Strikt aangepast",
|
||||||
"ru": "Пользовательский"
|
"ru": "Строго пользовательский"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"relay_mode_custom_description": {
|
"relay_mode_custom_description": {
|
||||||
"context": "Network settings description of strict custom relay behavior.",
|
"context": "Network settings description of strict custom relay behavior.",
|
||||||
"translations": {
|
"translations": {
|
||||||
"en": "Use only the relay servers below.",
|
"en": "Use only the configured custom relays or direct connections. Report an error if no custom relay can be established.",
|
||||||
"fr": "Utiliser uniquement les serveurs relais ci-dessous.",
|
"fr": "Utilise uniquement les relais personnalisés configurés ou les connexions directes. Signale une erreur si aucun relais personnalisé ne peut être établi.",
|
||||||
"es": "Usa únicamente los servidores de retransmisión indicados a continuación.",
|
"es": "Usa solo los relés personalizados configurados o conexiones directas. Informa de un error si no se puede establecer ningún relé personalizado.",
|
||||||
"it": "Usa solo i server relay indicati di seguito.",
|
"it": "Usa solo i relay personalizzati configurati o connessioni dirette. Segnala un errore se non è possibile stabilire alcun relay personalizzato.",
|
||||||
"de": "Verwendet ausschließlich die unten aufgeführten Relay-Server.",
|
"de": "Verwendet nur die konfigurierten eigenen Relays oder Direktverbindungen. Meldet einen Fehler, wenn kein eigenes Relay erreichbar ist.",
|
||||||
"pt": "Utiliza apenas os servidores de retransmissão abaixo.",
|
"pt": "Utiliza apenas os retransmissores personalizados configurados ou ligações diretas. Apresenta um erro se não for possível estabelecer nenhum retransmissor personalizado.",
|
||||||
"pl": "Używa wyłącznie poniższych serwerów przekaźnikowych.",
|
"pl": "Używa tylko skonfigurowanych własnych przekaźników lub połączeń bezpośrednich. Zgłasza błąd, jeśli nie można połączyć się z żadnym własnym przekaźnikiem.",
|
||||||
"nl": "Gebruikt alleen de onderstaande relayservers.",
|
"nl": "Gebruikt alleen de ingestelde aangepaste relays of rechtstreekse verbindingen. Meldt een fout als geen aangepaste relay bereikbaar is.",
|
||||||
"ru": "Использовать только указанные ниже серверы-ретрансляторы."
|
"ru": "Использует только настроенные пользовательские ретрансляторы или прямые соединения. Сообщает об ошибке, если ни один пользовательский ретранслятор недоступен."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"relay_mode_custom_direct_fallback": {
|
||||||
|
"context": "Network settings label for custom relays that allow direct-only startup fallback.",
|
||||||
|
"translations": {
|
||||||
|
"en": "Custom with direct fallback",
|
||||||
|
"fr": "Personnalisé avec repli direct",
|
||||||
|
"es": "Personalizado con conexión directa de reserva",
|
||||||
|
"it": "Personalizzata con ripiego diretto",
|
||||||
|
"de": "Benutzerdefiniert mit direktem Rückfall",
|
||||||
|
"pt": "Personalizado com alternativa direta",
|
||||||
|
"pl": "Niestandardowy z trybem bezpośrednim",
|
||||||
|
"nl": "Aangepast met directe terugval",
|
||||||
|
"ru": "Пользовательский с прямым резервом"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"relay_mode_custom_direct_fallback_description": {
|
||||||
|
"context": "Network settings description of custom relays with direct-only startup fallback.",
|
||||||
|
"translations": {
|
||||||
|
"en": "Prefer the configured custom relays. If unavailable, continue with direct connections only. Never use public relays.",
|
||||||
|
"fr": "Préfère les relais personnalisés configurés. S’ils sont indisponibles, continue uniquement avec des connexions directes. N’utilise jamais les relais publics.",
|
||||||
|
"es": "Prefiere los relés personalizados configurados. Si no están disponibles, continúa solo con conexiones directas. Nunca usa relés públicos.",
|
||||||
|
"it": "Preferisce i relay personalizzati configurati. Se non sono disponibili, continua solo con connessioni dirette. Non usa mai relay pubblici.",
|
||||||
|
"de": "Bevorzugt die konfigurierten eigenen Relays. Sind sie nicht verfügbar, werden nur Direktverbindungen verwendet. Öffentliche Relays werden nie genutzt.",
|
||||||
|
"pt": "Prefere os retransmissores personalizados configurados. Se não estiverem disponíveis, continua apenas com ligações diretas. Nunca utiliza retransmissores públicos.",
|
||||||
|
"pl": "Preferuje skonfigurowane własne przekaźniki. Jeśli są niedostępne, kontynuuje tylko przez połączenia bezpośrednie. Nigdy nie używa publicznych przekaźników.",
|
||||||
|
"nl": "Geeft de voorkeur aan de ingestelde aangepaste relays. Als die niet beschikbaar zijn, worden alleen rechtstreekse verbindingen gebruikt. Openbare relays worden nooit gebruikt.",
|
||||||
|
"ru": "Предпочитает настроенные пользовательские ретрансляторы. Если они недоступны, продолжает работу только через прямые соединения. Публичные ретрансляторы не используются."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"relay_mode_local_only": {
|
||||||
|
"context": "Network settings label for direct connections without any relay.",
|
||||||
|
"translations": {
|
||||||
|
"en": "Local only",
|
||||||
|
"fr": "Réseau local uniquement",
|
||||||
|
"es": "Solo red local",
|
||||||
|
"it": "Solo rete locale",
|
||||||
|
"de": "Nur lokales Netzwerk",
|
||||||
|
"pt": "Apenas rede local",
|
||||||
|
"pl": "Tylko sieć lokalna",
|
||||||
|
"nl": "Alleen lokaal netwerk",
|
||||||
|
"ru": "Только локальная сеть"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"relay_mode_local_only_description": {
|
||||||
|
"context": "Network settings description of direct-only local-network mode.",
|
||||||
|
"translations": {
|
||||||
|
"en": "Disable all relays and allow only direct connections, primarily for devices on the same network.",
|
||||||
|
"fr": "Désactive tous les relais et autorise uniquement les connexions directes, principalement pour les appareils sur le même réseau.",
|
||||||
|
"es": "Desactiva todos los relés y permite solo conexiones directas, principalmente para dispositivos de la misma red.",
|
||||||
|
"it": "Disattiva tutti i relay e consente solo connessioni dirette, soprattutto per dispositivi sulla stessa rete.",
|
||||||
|
"de": "Deaktiviert alle Relays und erlaubt nur Direktverbindungen, hauptsächlich für Geräte im selben Netzwerk.",
|
||||||
|
"pt": "Desativa todos os retransmissores e permite apenas ligações diretas, principalmente para dispositivos na mesma rede.",
|
||||||
|
"pl": "Wyłącza wszystkie przekaźniki i zezwala tylko na połączenia bezpośrednie, głównie dla urządzeń w tej samej sieci.",
|
||||||
|
"nl": "Schakelt alle relays uit en staat alleen rechtstreekse verbindingen toe, vooral voor apparaten in hetzelfde netwerk.",
|
||||||
|
"ru": "Отключает все ретрансляторы и разрешает только прямые соединения, прежде всего для устройств в одной сети."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"relay_privacy_description": {
|
"relay_privacy_description": {
|
||||||
@@ -2710,15 +2766,15 @@
|
|||||||
"relay_strict_warning": {
|
"relay_strict_warning": {
|
||||||
"context": "Network settings warning that custom relay mode has no public fallback or discovery.",
|
"context": "Network settings warning that custom relay mode has no public fallback or discovery.",
|
||||||
"translations": {
|
"translations": {
|
||||||
"en": "Custom mode is strict: VniDrop will not fall back to public relays or public discovery. Other devices must be able to reach your configured relays.",
|
"en": "Strict custom mode will not start unless at least one configured relay is reachable. VniDrop never uses public relays or public discovery in this mode.",
|
||||||
"fr": "Le mode personnalisé est strict : VniDrop n’utilisera ni les relais publics ni la découverte publique comme solution de repli. Les autres appareils doivent pouvoir accéder aux relais configurés.",
|
"fr": "Le mode personnalisé strict ne démarre que si au moins un relais configuré est accessible. VniDrop n’utilise jamais de relais ni de découverte publics dans ce mode.",
|
||||||
"es": "El modo personalizado es estricto: VniDrop no recurrirá a relés públicos ni al descubrimiento público. Los demás dispositivos deben poder acceder a los relés configurados.",
|
"es": "El modo personalizado estricto no se inicia a menos que se pueda acceder al menos a un relé configurado. VniDrop nunca usa relés ni descubrimiento públicos en este modo.",
|
||||||
"it": "La modalità personalizzata è rigorosa: VniDrop non userà relay pubblici o il rilevamento pubblico come ripiego. Gli altri dispositivi devono poter raggiungere i relay configurati.",
|
"it": "La modalità personalizzata rigorosa si avvia solo se almeno un relay configurato è raggiungibile. In questa modalità VniDrop non usa mai relay o rilevamento pubblici.",
|
||||||
"de": "Der benutzerdefinierte Modus ist strikt: VniDrop greift weder auf öffentliche Relays noch auf öffentliche Erkennung zurück. Andere Geräte müssen Ihre konfigurierten Relays erreichen können.",
|
"de": "Der strikt benutzerdefinierte Modus startet nur, wenn mindestens ein konfiguriertes Relay erreichbar ist. VniDrop verwendet in diesem Modus nie öffentliche Relays oder öffentliche Erkennung.",
|
||||||
"pt": "O modo personalizado é estrito: o VniDrop não recorrerá a retransmissores públicos nem à descoberta pública. Os outros dispositivos têm de conseguir aceder aos retransmissores configurados.",
|
"pt": "O modo personalizado estrito só inicia se pelo menos um retransmissor configurado estiver acessível. Neste modo, o VniDrop nunca utiliza retransmissores nem descoberta públicos.",
|
||||||
"pl": "Tryb niestandardowy jest rygorystyczny: VniDrop nie użyje awaryjnie publicznych przekaźników ani publicznego wykrywania. Inne urządzenia muszą mieć dostęp do skonfigurowanych przekaźników.",
|
"pl": "Ścisły tryb niestandardowy uruchamia się tylko wtedy, gdy co najmniej jeden skonfigurowany przekaźnik jest dostępny. VniDrop nigdy nie używa w tym trybie publicznych przekaźników ani publicznego wykrywania.",
|
||||||
"nl": "De aangepaste modus is strikt: VniDrop valt niet terug op openbare relays of openbare detectie. Andere apparaten moeten uw ingestelde relays kunnen bereiken.",
|
"nl": "De strikt aangepaste modus start alleen als minstens één ingestelde relay bereikbaar is. VniDrop gebruikt in deze modus nooit openbare relays of openbare detectie.",
|
||||||
"ru": "Пользовательский режим работает строго: VniDrop не будет переключаться на публичные ретрансляторы или публичное обнаружение. Другие устройства должны иметь доступ к настроенным ретрансляторам."
|
"ru": "Строго пользовательский режим запускается, только если доступен хотя бы один настроенный ретранслятор. В этом режиме VniDrop никогда не использует публичные ретрансляторы или публичное обнаружение."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"relay_validation_duplicate_url": {
|
"relay_validation_duplicate_url": {
|
||||||
|
|||||||
@@ -177,15 +177,19 @@
|
|||||||
<string name="relay_apply_restart_description">Beim Anwenden wird die Netzwerkverbindung von VniDrop neu gestartet. Beenden Sie zuerst aktive Übertragungen und Freigaben. Vorhandene Einladungen müssen eventuell erneut geteilt werden.</string>
|
<string name="relay_apply_restart_description">Beim Anwenden wird die Netzwerkverbindung von VniDrop neu gestartet. Beenden Sie zuerst aktive Übertragungen und Freigaben. Vorhandene Einladungen müssen eventuell erneut geteilt werden.</string>
|
||||||
<string name="relay_custom_urls_help">Geben Sie pro Zeile eine HTTPS-Relay-URL ein. Anmeldedaten in URLs werden nicht unterstützt. Das TLS-Zertifikat muss von einer öffentlich vertrauenswürdigen Zertifizierungsstelle ausgestellt sein.</string>
|
<string name="relay_custom_urls_help">Geben Sie pro Zeile eine HTTPS-Relay-URL ein. Anmeldedaten in URLs werden nicht unterstützt. Das TLS-Zertifikat muss von einer öffentlich vertrauenswürdigen Zertifizierungsstelle ausgestellt sein.</string>
|
||||||
<string name="relay_custom_urls_label">Relay-URLs</string>
|
<string name="relay_custom_urls_label">Relay-URLs</string>
|
||||||
<string name="relay_mode_automatic">Automatisch</string>
|
<string name="relay_mode_automatic">Automatisch (empfohlen)</string>
|
||||||
<string name="relay_mode_automatic_description">Verwendet die öffentliche Standard-Relay-Infrastruktur von VniDrop, wenn keine direkte Verbindung möglich ist.</string>
|
<string name="relay_mode_automatic_description">Verwendet die öffentliche Standard-Relay-Infrastruktur von VniDrop, wenn keine direkte Verbindung möglich ist.</string>
|
||||||
<string name="relay_mode_custom">Benutzerdefiniert</string>
|
<string name="relay_mode_custom">Strikt benutzerdefiniert</string>
|
||||||
<string name="relay_mode_custom_description">Verwendet ausschließlich die unten aufgeführten Relay-Server.</string>
|
<string name="relay_mode_custom_description">Verwendet nur die konfigurierten eigenen Relays oder Direktverbindungen. Meldet einen Fehler, wenn kein eigenes Relay erreichbar ist.</string>
|
||||||
|
<string name="relay_mode_custom_direct_fallback">Benutzerdefiniert mit direktem Rückfall</string>
|
||||||
|
<string name="relay_mode_custom_direct_fallback_description">Bevorzugt die konfigurierten eigenen Relays. Sind sie nicht verfügbar, werden nur Direktverbindungen verwendet. Öffentliche Relays werden nie genutzt.</string>
|
||||||
|
<string name="relay_mode_local_only">Nur lokales Netzwerk</string>
|
||||||
|
<string name="relay_mode_local_only_description">Deaktiviert alle Relays und erlaubt nur Direktverbindungen, hauptsächlich für Geräte im selben Netzwerk.</string>
|
||||||
<string name="relay_privacy_description">Relays leiten verschlüsselten Datenverkehr weiter und können Ihre Dateien nicht lesen, ihr Betreiber kann jedoch Verbindungsmetadaten sehen.</string>
|
<string name="relay_privacy_description">Relays leiten verschlüsselten Datenverkehr weiter und können Ihre Dateien nicht lesen, ihr Betreiber kann jedoch Verbindungsmetadaten sehen.</string>
|
||||||
<string name="relay_remove_url">Relay-Server entfernen</string>
|
<string name="relay_remove_url">Relay-Server entfernen</string>
|
||||||
<string name="relay_restore_failed">Die vorherigen Netzwerkeinstellungen konnten nicht wiederhergestellt werden. Starten Sie VniDrop neu und prüfen Sie Ihre Relay-Konfiguration.</string>
|
<string name="relay_restore_failed">Die vorherigen Netzwerkeinstellungen konnten nicht wiederhergestellt werden. Starten Sie VniDrop neu und prüfen Sie Ihre Relay-Konfiguration.</string>
|
||||||
<string name="relay_settings_applied">Netzwerkeinstellungen angewendet.</string>
|
<string name="relay_settings_applied">Netzwerkeinstellungen angewendet.</string>
|
||||||
<string name="relay_strict_warning">Der benutzerdefinierte Modus ist strikt: VniDrop greift weder auf öffentliche Relays noch auf öffentliche Erkennung zurück. Andere Geräte müssen Ihre konfigurierten Relays erreichen können.</string>
|
<string name="relay_strict_warning">Der strikt benutzerdefinierte Modus startet nur, wenn mindestens ein konfiguriertes Relay erreichbar ist. VniDrop verwendet in diesem Modus nie öffentliche Relays oder öffentliche Erkennung.</string>
|
||||||
<string name="relay_validation_duplicate_url">Die Relay-URL in Zeile %1$d ist bereits zuvor eingetragen.</string>
|
<string name="relay_validation_duplicate_url">Die Relay-URL in Zeile %1$d ist bereits zuvor eingetragen.</string>
|
||||||
<string name="relay_validation_https_required">Die Relay-URL in Zeile %1$d muss mit https:// beginnen.</string>
|
<string name="relay_validation_https_required">Die Relay-URL in Zeile %1$d muss mit https:// beginnen.</string>
|
||||||
<string name="relay_validation_invalid_url">Die Relay-URL in Zeile %1$d ist ungültig.</string>
|
<string name="relay_validation_invalid_url">Die Relay-URL in Zeile %1$d ist ungültig.</string>
|
||||||
|
|||||||
@@ -177,15 +177,19 @@
|
|||||||
<string name="relay_apply_restart_description">Al aplicar los ajustes, se reinicia la conexión de red de VniDrop. Detenga primero las transferencias y los elementos compartidos activos. Es posible que tenga que volver a compartir las invitaciones existentes.</string>
|
<string name="relay_apply_restart_description">Al aplicar los ajustes, se reinicia la conexión de red de VniDrop. Detenga primero las transferencias y los elementos compartidos activos. Es posible que tenga que volver a compartir las invitaciones existentes.</string>
|
||||||
<string name="relay_custom_urls_help">Introduzca una URL HTTPS de relé por línea. No se admiten credenciales en las URL. El certificado TLS debe ser emitido por una autoridad de certificación de confianza pública.</string>
|
<string name="relay_custom_urls_help">Introduzca una URL HTTPS de relé por línea. No se admiten credenciales en las URL. El certificado TLS debe ser emitido por una autoridad de certificación de confianza pública.</string>
|
||||||
<string name="relay_custom_urls_label">URL de relés</string>
|
<string name="relay_custom_urls_label">URL de relés</string>
|
||||||
<string name="relay_mode_automatic">Automático</string>
|
<string name="relay_mode_automatic">Automático (recomendado)</string>
|
||||||
<string name="relay_mode_automatic_description">Usa la infraestructura pública de relés predeterminada de VniDrop cuando no haya una conexión directa disponible.</string>
|
<string name="relay_mode_automatic_description">Usa la infraestructura pública de relés predeterminada de VniDrop cuando no haya una conexión directa disponible.</string>
|
||||||
<string name="relay_mode_custom">Personalizado</string>
|
<string name="relay_mode_custom">Personalizado estricto</string>
|
||||||
<string name="relay_mode_custom_description">Usa únicamente los servidores de retransmisión indicados a continuación.</string>
|
<string name="relay_mode_custom_description">Usa solo los relés personalizados configurados o conexiones directas. Informa de un error si no se puede establecer ningún relé personalizado.</string>
|
||||||
|
<string name="relay_mode_custom_direct_fallback">Personalizado con conexión directa de reserva</string>
|
||||||
|
<string name="relay_mode_custom_direct_fallback_description">Prefiere los relés personalizados configurados. Si no están disponibles, continúa solo con conexiones directas. Nunca usa relés públicos.</string>
|
||||||
|
<string name="relay_mode_local_only">Solo red local</string>
|
||||||
|
<string name="relay_mode_local_only_description">Desactiva todos los relés y permite solo conexiones directas, principalmente para dispositivos de la misma red.</string>
|
||||||
<string name="relay_privacy_description">Los relés reenvían tráfico cifrado y no pueden leer sus archivos, pero su operador puede observar los metadatos de conexión.</string>
|
<string name="relay_privacy_description">Los relés reenvían tráfico cifrado y no pueden leer sus archivos, pero su operador puede observar los metadatos de conexión.</string>
|
||||||
<string name="relay_remove_url">Eliminar servidor de retransmisión</string>
|
<string name="relay_remove_url">Eliminar servidor de retransmisión</string>
|
||||||
<string name="relay_restore_failed">No se han podido restaurar los ajustes de red anteriores. Reinicie VniDrop y revise la configuración de relés.</string>
|
<string name="relay_restore_failed">No se han podido restaurar los ajustes de red anteriores. Reinicie VniDrop y revise la configuración de relés.</string>
|
||||||
<string name="relay_settings_applied">Ajustes de red aplicados.</string>
|
<string name="relay_settings_applied">Ajustes de red aplicados.</string>
|
||||||
<string name="relay_strict_warning">El modo personalizado es estricto: VniDrop no recurrirá a relés públicos ni al descubrimiento público. Los demás dispositivos deben poder acceder a los relés configurados.</string>
|
<string name="relay_strict_warning">El modo personalizado estricto no se inicia a menos que se pueda acceder al menos a un relé configurado. VniDrop nunca usa relés ni descubrimiento públicos en este modo.</string>
|
||||||
<string name="relay_validation_duplicate_url">La URL de relé de la línea %1$d duplica una entrada anterior.</string>
|
<string name="relay_validation_duplicate_url">La URL de relé de la línea %1$d duplica una entrada anterior.</string>
|
||||||
<string name="relay_validation_https_required">La URL de relé de la línea %1$d debe empezar por https://.</string>
|
<string name="relay_validation_https_required">La URL de relé de la línea %1$d debe empezar por https://.</string>
|
||||||
<string name="relay_validation_invalid_url">La URL de relé de la línea %1$d no es válida.</string>
|
<string name="relay_validation_invalid_url">La URL de relé de la línea %1$d no es válida.</string>
|
||||||
|
|||||||
@@ -177,15 +177,19 @@
|
|||||||
<string name="relay_apply_restart_description">L’application de ces réglages redémarre la connexion réseau de VniDrop. Arrêtez d’abord les transferts et partages actifs. Il peut être nécessaire de partager à nouveau les invitations existantes.</string>
|
<string name="relay_apply_restart_description">L’application de ces réglages redémarre la connexion réseau de VniDrop. Arrêtez d’abord les transferts et partages actifs. Il peut être nécessaire de partager à nouveau les invitations existantes.</string>
|
||||||
<string name="relay_custom_urls_help">Saisissez une URL de relais HTTPS par ligne. Les identifiants dans les URL ne sont pas pris en charge. Le certificat TLS doit être émis par une autorité de certification reconnue publiquement.</string>
|
<string name="relay_custom_urls_help">Saisissez une URL de relais HTTPS par ligne. Les identifiants dans les URL ne sont pas pris en charge. Le certificat TLS doit être émis par une autorité de certification reconnue publiquement.</string>
|
||||||
<string name="relay_custom_urls_label">URL des relais</string>
|
<string name="relay_custom_urls_label">URL des relais</string>
|
||||||
<string name="relay_mode_automatic">Automatique</string>
|
<string name="relay_mode_automatic">Automatique (recommandé)</string>
|
||||||
<string name="relay_mode_automatic_description">Utiliser l’infrastructure de relais publique par défaut de VniDrop lorsqu’une connexion directe est indisponible.</string>
|
<string name="relay_mode_automatic_description">Utiliser l’infrastructure de relais publique par défaut de VniDrop lorsqu’une connexion directe est indisponible.</string>
|
||||||
<string name="relay_mode_custom">Personnalisé</string>
|
<string name="relay_mode_custom">Personnalisé strict</string>
|
||||||
<string name="relay_mode_custom_description">Utiliser uniquement les serveurs relais ci-dessous.</string>
|
<string name="relay_mode_custom_description">Utilise uniquement les relais personnalisés configurés ou les connexions directes. Signale une erreur si aucun relais personnalisé ne peut être établi.</string>
|
||||||
|
<string name="relay_mode_custom_direct_fallback">Personnalisé avec repli direct</string>
|
||||||
|
<string name="relay_mode_custom_direct_fallback_description">Préfère les relais personnalisés configurés. S’ils sont indisponibles, continue uniquement avec des connexions directes. N’utilise jamais les relais publics.</string>
|
||||||
|
<string name="relay_mode_local_only">Réseau local uniquement</string>
|
||||||
|
<string name="relay_mode_local_only_description">Désactive tous les relais et autorise uniquement les connexions directes, principalement pour les appareils sur le même réseau.</string>
|
||||||
<string name="relay_privacy_description">Les relais transmettent du trafic chiffré et ne peuvent pas lire vos fichiers, mais leur opérateur peut observer les métadonnées de connexion.</string>
|
<string name="relay_privacy_description">Les relais transmettent du trafic chiffré et ne peuvent pas lire vos fichiers, mais leur opérateur peut observer les métadonnées de connexion.</string>
|
||||||
<string name="relay_remove_url">Supprimer le serveur relais</string>
|
<string name="relay_remove_url">Supprimer le serveur relais</string>
|
||||||
<string name="relay_restore_failed">Impossible de restaurer les réglages réseau précédents. Redémarrez VniDrop et vérifiez votre configuration de relais.</string>
|
<string name="relay_restore_failed">Impossible de restaurer les réglages réseau précédents. Redémarrez VniDrop et vérifiez votre configuration de relais.</string>
|
||||||
<string name="relay_settings_applied">Réglages réseau appliqués.</string>
|
<string name="relay_settings_applied">Réglages réseau appliqués.</string>
|
||||||
<string name="relay_strict_warning">Le mode personnalisé est strict : VniDrop n’utilisera ni les relais publics ni la découverte publique comme solution de repli. Les autres appareils doivent pouvoir accéder aux relais configurés.</string>
|
<string name="relay_strict_warning">Le mode personnalisé strict ne démarre que si au moins un relais configuré est accessible. VniDrop n’utilise jamais de relais ni de découverte publics dans ce mode.</string>
|
||||||
<string name="relay_validation_duplicate_url">L’URL de relais à la ligne %1$d est identique à une entrée précédente.</string>
|
<string name="relay_validation_duplicate_url">L’URL de relais à la ligne %1$d est identique à une entrée précédente.</string>
|
||||||
<string name="relay_validation_https_required">L’URL de relais à la ligne %1$d doit commencer par https://.</string>
|
<string name="relay_validation_https_required">L’URL de relais à la ligne %1$d doit commencer par https://.</string>
|
||||||
<string name="relay_validation_invalid_url">L’URL de relais à la ligne %1$d n’est pas valide.</string>
|
<string name="relay_validation_invalid_url">L’URL de relais à la ligne %1$d n’est pas valide.</string>
|
||||||
|
|||||||
@@ -177,15 +177,19 @@
|
|||||||
<string name="relay_apply_restart_description">L’applicazione riavvia la connessione di rete di VniDrop. Interrompa prima i trasferimenti e le condivisioni attivi. Potrebbe essere necessario condividere di nuovo gli inviti esistenti.</string>
|
<string name="relay_apply_restart_description">L’applicazione riavvia la connessione di rete di VniDrop. Interrompa prima i trasferimenti e le condivisioni attivi. Potrebbe essere necessario condividere di nuovo gli inviti esistenti.</string>
|
||||||
<string name="relay_custom_urls_help">Inserisca un URL relay HTTPS per riga. Le credenziali negli URL non sono supportate. Il certificato TLS deve essere emesso da un’autorità di certificazione pubblicamente attendibile.</string>
|
<string name="relay_custom_urls_help">Inserisca un URL relay HTTPS per riga. Le credenziali negli URL non sono supportate. Il certificato TLS deve essere emesso da un’autorità di certificazione pubblicamente attendibile.</string>
|
||||||
<string name="relay_custom_urls_label">URL relay</string>
|
<string name="relay_custom_urls_label">URL relay</string>
|
||||||
<string name="relay_mode_automatic">Automatica</string>
|
<string name="relay_mode_automatic">Automatica (consigliata)</string>
|
||||||
<string name="relay_mode_automatic_description">Usa l’infrastruttura relay pubblica predefinita di VniDrop quando non è disponibile una connessione diretta.</string>
|
<string name="relay_mode_automatic_description">Usa l’infrastruttura relay pubblica predefinita di VniDrop quando non è disponibile una connessione diretta.</string>
|
||||||
<string name="relay_mode_custom">Personalizzata</string>
|
<string name="relay_mode_custom">Personalizzata rigorosa</string>
|
||||||
<string name="relay_mode_custom_description">Usa solo i server relay indicati di seguito.</string>
|
<string name="relay_mode_custom_description">Usa solo i relay personalizzati configurati o connessioni dirette. Segnala un errore se non è possibile stabilire alcun relay personalizzato.</string>
|
||||||
|
<string name="relay_mode_custom_direct_fallback">Personalizzata con ripiego diretto</string>
|
||||||
|
<string name="relay_mode_custom_direct_fallback_description">Preferisce i relay personalizzati configurati. Se non sono disponibili, continua solo con connessioni dirette. Non usa mai relay pubblici.</string>
|
||||||
|
<string name="relay_mode_local_only">Solo rete locale</string>
|
||||||
|
<string name="relay_mode_local_only_description">Disattiva tutti i relay e consente solo connessioni dirette, soprattutto per dispositivi sulla stessa rete.</string>
|
||||||
<string name="relay_privacy_description">I relay inoltrano traffico cifrato e non possono leggere i suoi file, ma il loro operatore può osservare i metadati di connessione.</string>
|
<string name="relay_privacy_description">I relay inoltrano traffico cifrato e non possono leggere i suoi file, ma il loro operatore può osservare i metadati di connessione.</string>
|
||||||
<string name="relay_remove_url">Rimuovi server relay</string>
|
<string name="relay_remove_url">Rimuovi server relay</string>
|
||||||
<string name="relay_restore_failed">Impossibile ripristinare le impostazioni di rete precedenti. Riavvii VniDrop e verifichi la configurazione dei relay.</string>
|
<string name="relay_restore_failed">Impossibile ripristinare le impostazioni di rete precedenti. Riavvii VniDrop e verifichi la configurazione dei relay.</string>
|
||||||
<string name="relay_settings_applied">Impostazioni di rete applicate.</string>
|
<string name="relay_settings_applied">Impostazioni di rete applicate.</string>
|
||||||
<string name="relay_strict_warning">La modalità personalizzata è rigorosa: VniDrop non userà relay pubblici o il rilevamento pubblico come ripiego. Gli altri dispositivi devono poter raggiungere i relay configurati.</string>
|
<string name="relay_strict_warning">La modalità personalizzata rigorosa si avvia solo se almeno un relay configurato è raggiungibile. In questa modalità VniDrop non usa mai relay o rilevamento pubblici.</string>
|
||||||
<string name="relay_validation_duplicate_url">L’URL relay alla riga %1$d duplica una voce precedente.</string>
|
<string name="relay_validation_duplicate_url">L’URL relay alla riga %1$d duplica una voce precedente.</string>
|
||||||
<string name="relay_validation_https_required">L’URL relay alla riga %1$d deve iniziare con https://.</string>
|
<string name="relay_validation_https_required">L’URL relay alla riga %1$d deve iniziare con https://.</string>
|
||||||
<string name="relay_validation_invalid_url">L’URL relay alla riga %1$d non è valido.</string>
|
<string name="relay_validation_invalid_url">L’URL relay alla riga %1$d non è valido.</string>
|
||||||
|
|||||||
@@ -177,15 +177,19 @@
|
|||||||
<string name="relay_apply_restart_description">Bij het toepassen wordt de netwerkverbinding van VniDrop opnieuw gestart. Stop eerst actieve overdrachten en gedeelde items. Bestaande uitnodigingen moeten mogelijk opnieuw worden gedeeld.</string>
|
<string name="relay_apply_restart_description">Bij het toepassen wordt de netwerkverbinding van VniDrop opnieuw gestart. Stop eerst actieve overdrachten en gedeelde items. Bestaande uitnodigingen moeten mogelijk opnieuw worden gedeeld.</string>
|
||||||
<string name="relay_custom_urls_help">Voer per regel één HTTPS-relay-URL in. Aanmeldgegevens in URL\'s worden niet ondersteund. Het TLS-certificaat moet zijn uitgegeven door een openbaar vertrouwde certificeringsinstantie.</string>
|
<string name="relay_custom_urls_help">Voer per regel één HTTPS-relay-URL in. Aanmeldgegevens in URL\'s worden niet ondersteund. Het TLS-certificaat moet zijn uitgegeven door een openbaar vertrouwde certificeringsinstantie.</string>
|
||||||
<string name="relay_custom_urls_label">Relay-URL\'s</string>
|
<string name="relay_custom_urls_label">Relay-URL\'s</string>
|
||||||
<string name="relay_mode_automatic">Automatisch</string>
|
<string name="relay_mode_automatic">Automatisch (aanbevolen)</string>
|
||||||
<string name="relay_mode_automatic_description">Gebruikt de standaard openbare relay-infrastructuur van VniDrop wanneer geen directe verbinding beschikbaar is.</string>
|
<string name="relay_mode_automatic_description">Gebruikt de standaard openbare relay-infrastructuur van VniDrop wanneer geen directe verbinding beschikbaar is.</string>
|
||||||
<string name="relay_mode_custom">Aangepast</string>
|
<string name="relay_mode_custom">Strikt aangepast</string>
|
||||||
<string name="relay_mode_custom_description">Gebruikt alleen de onderstaande relayservers.</string>
|
<string name="relay_mode_custom_description">Gebruikt alleen de ingestelde aangepaste relays of rechtstreekse verbindingen. Meldt een fout als geen aangepaste relay bereikbaar is.</string>
|
||||||
|
<string name="relay_mode_custom_direct_fallback">Aangepast met directe terugval</string>
|
||||||
|
<string name="relay_mode_custom_direct_fallback_description">Geeft de voorkeur aan de ingestelde aangepaste relays. Als die niet beschikbaar zijn, worden alleen rechtstreekse verbindingen gebruikt. Openbare relays worden nooit gebruikt.</string>
|
||||||
|
<string name="relay_mode_local_only">Alleen lokaal netwerk</string>
|
||||||
|
<string name="relay_mode_local_only_description">Schakelt alle relays uit en staat alleen rechtstreekse verbindingen toe, vooral voor apparaten in hetzelfde netwerk.</string>
|
||||||
<string name="relay_privacy_description">Relays sturen versleuteld verkeer door en kunnen uw bestanden niet lezen, maar de beheerder kan verbindingsmetadata bekijken.</string>
|
<string name="relay_privacy_description">Relays sturen versleuteld verkeer door en kunnen uw bestanden niet lezen, maar de beheerder kan verbindingsmetadata bekijken.</string>
|
||||||
<string name="relay_remove_url">Relayserver verwijderen</string>
|
<string name="relay_remove_url">Relayserver verwijderen</string>
|
||||||
<string name="relay_restore_failed">De vorige netwerkinstellingen konden niet worden hersteld. Start VniDrop opnieuw en controleer uw relayconfiguratie.</string>
|
<string name="relay_restore_failed">De vorige netwerkinstellingen konden niet worden hersteld. Start VniDrop opnieuw en controleer uw relayconfiguratie.</string>
|
||||||
<string name="relay_settings_applied">Netwerkinstellingen toegepast.</string>
|
<string name="relay_settings_applied">Netwerkinstellingen toegepast.</string>
|
||||||
<string name="relay_strict_warning">De aangepaste modus is strikt: VniDrop valt niet terug op openbare relays of openbare detectie. Andere apparaten moeten uw ingestelde relays kunnen bereiken.</string>
|
<string name="relay_strict_warning">De strikt aangepaste modus start alleen als minstens één ingestelde relay bereikbaar is. VniDrop gebruikt in deze modus nooit openbare relays of openbare detectie.</string>
|
||||||
<string name="relay_validation_duplicate_url">De relay-URL op regel %1$d is gelijk aan een eerdere invoer.</string>
|
<string name="relay_validation_duplicate_url">De relay-URL op regel %1$d is gelijk aan een eerdere invoer.</string>
|
||||||
<string name="relay_validation_https_required">De relay-URL op regel %1$d moet beginnen met https://.</string>
|
<string name="relay_validation_https_required">De relay-URL op regel %1$d moet beginnen met https://.</string>
|
||||||
<string name="relay_validation_invalid_url">De relay-URL op regel %1$d is ongeldig.</string>
|
<string name="relay_validation_invalid_url">De relay-URL op regel %1$d is ongeldig.</string>
|
||||||
|
|||||||
@@ -177,15 +177,19 @@
|
|||||||
<string name="relay_apply_restart_description">Zastosowanie ustawień ponownie uruchamia połączenie sieciowe VniDrop. Najpierw zatrzymaj aktywne transfery i udostępnienia. Istniejące zaproszenia mogą wymagać ponownego udostępnienia.</string>
|
<string name="relay_apply_restart_description">Zastosowanie ustawień ponownie uruchamia połączenie sieciowe VniDrop. Najpierw zatrzymaj aktywne transfery i udostępnienia. Istniejące zaproszenia mogą wymagać ponownego udostępnienia.</string>
|
||||||
<string name="relay_custom_urls_help">Wprowadź po jednym adresie URL HTTPS przekaźnika w każdym wierszu. Dane logowania w adresach URL nie są obsługiwane. Certyfikat TLS musi być wystawiony przez publicznie zaufany urząd certyfikacji.</string>
|
<string name="relay_custom_urls_help">Wprowadź po jednym adresie URL HTTPS przekaźnika w każdym wierszu. Dane logowania w adresach URL nie są obsługiwane. Certyfikat TLS musi być wystawiony przez publicznie zaufany urząd certyfikacji.</string>
|
||||||
<string name="relay_custom_urls_label">Adresy URL przekaźników</string>
|
<string name="relay_custom_urls_label">Adresy URL przekaźników</string>
|
||||||
<string name="relay_mode_automatic">Automatyczny</string>
|
<string name="relay_mode_automatic">Automatyczny (zalecany)</string>
|
||||||
<string name="relay_mode_automatic_description">Używa domyślnej publicznej infrastruktury przekaźników VniDrop, gdy połączenie bezpośrednie jest niedostępne.</string>
|
<string name="relay_mode_automatic_description">Używa domyślnej publicznej infrastruktury przekaźników VniDrop, gdy połączenie bezpośrednie jest niedostępne.</string>
|
||||||
<string name="relay_mode_custom">Niestandardowy</string>
|
<string name="relay_mode_custom">Ścisły niestandardowy</string>
|
||||||
<string name="relay_mode_custom_description">Używa wyłącznie poniższych serwerów przekaźnikowych.</string>
|
<string name="relay_mode_custom_description">Używa tylko skonfigurowanych własnych przekaźników lub połączeń bezpośrednich. Zgłasza błąd, jeśli nie można połączyć się z żadnym własnym przekaźnikiem.</string>
|
||||||
|
<string name="relay_mode_custom_direct_fallback">Niestandardowy z trybem bezpośrednim</string>
|
||||||
|
<string name="relay_mode_custom_direct_fallback_description">Preferuje skonfigurowane własne przekaźniki. Jeśli są niedostępne, kontynuuje tylko przez połączenia bezpośrednie. Nigdy nie używa publicznych przekaźników.</string>
|
||||||
|
<string name="relay_mode_local_only">Tylko sieć lokalna</string>
|
||||||
|
<string name="relay_mode_local_only_description">Wyłącza wszystkie przekaźniki i zezwala tylko na połączenia bezpośrednie, głównie dla urządzeń w tej samej sieci.</string>
|
||||||
<string name="relay_privacy_description">Przekaźniki przesyłają zaszyfrowany ruch i nie mogą odczytać plików, ale ich operator może obserwować metadane połączenia.</string>
|
<string name="relay_privacy_description">Przekaźniki przesyłają zaszyfrowany ruch i nie mogą odczytać plików, ale ich operator może obserwować metadane połączenia.</string>
|
||||||
<string name="relay_remove_url">Usuń serwer przekaźnikowy</string>
|
<string name="relay_remove_url">Usuń serwer przekaźnikowy</string>
|
||||||
<string name="relay_restore_failed">Nie udało się przywrócić poprzednich ustawień sieci. Uruchom ponownie VniDrop i sprawdź konfigurację przekaźników.</string>
|
<string name="relay_restore_failed">Nie udało się przywrócić poprzednich ustawień sieci. Uruchom ponownie VniDrop i sprawdź konfigurację przekaźników.</string>
|
||||||
<string name="relay_settings_applied">Zastosowano ustawienia sieci.</string>
|
<string name="relay_settings_applied">Zastosowano ustawienia sieci.</string>
|
||||||
<string name="relay_strict_warning">Tryb niestandardowy jest rygorystyczny: VniDrop nie użyje awaryjnie publicznych przekaźników ani publicznego wykrywania. Inne urządzenia muszą mieć dostęp do skonfigurowanych przekaźników.</string>
|
<string name="relay_strict_warning">Ścisły tryb niestandardowy uruchamia się tylko wtedy, gdy co najmniej jeden skonfigurowany przekaźnik jest dostępny. VniDrop nigdy nie używa w tym trybie publicznych przekaźników ani publicznego wykrywania.</string>
|
||||||
<string name="relay_validation_duplicate_url">Adres URL przekaźnika w wierszu %1$d powtarza wcześniejszy wpis.</string>
|
<string name="relay_validation_duplicate_url">Adres URL przekaźnika w wierszu %1$d powtarza wcześniejszy wpis.</string>
|
||||||
<string name="relay_validation_https_required">Adres URL przekaźnika w wierszu %1$d musi zaczynać się od https://.</string>
|
<string name="relay_validation_https_required">Adres URL przekaźnika w wierszu %1$d musi zaczynać się od https://.</string>
|
||||||
<string name="relay_validation_invalid_url">Adres URL przekaźnika w wierszu %1$d jest nieprawidłowy.</string>
|
<string name="relay_validation_invalid_url">Adres URL przekaźnika w wierszu %1$d jest nieprawidłowy.</string>
|
||||||
|
|||||||
@@ -177,15 +177,19 @@
|
|||||||
<string name="relay_apply_restart_description">A aplicação reinicia a ligação de rede do VniDrop. Pare primeiro as transferências e partilhas ativas. Poderá ser necessário voltar a partilhar os convites existentes.</string>
|
<string name="relay_apply_restart_description">A aplicação reinicia a ligação de rede do VniDrop. Pare primeiro as transferências e partilhas ativas. Poderá ser necessário voltar a partilhar os convites existentes.</string>
|
||||||
<string name="relay_custom_urls_help">Introduza um URL HTTPS de retransmissor por linha. Não são suportadas credenciais nos URLs. O certificado TLS tem de ser emitido por uma autoridade de certificação publicamente reconhecida.</string>
|
<string name="relay_custom_urls_help">Introduza um URL HTTPS de retransmissor por linha. Não são suportadas credenciais nos URLs. O certificado TLS tem de ser emitido por uma autoridade de certificação publicamente reconhecida.</string>
|
||||||
<string name="relay_custom_urls_label">URLs dos retransmissores</string>
|
<string name="relay_custom_urls_label">URLs dos retransmissores</string>
|
||||||
<string name="relay_mode_automatic">Automático</string>
|
<string name="relay_mode_automatic">Automático (recomendado)</string>
|
||||||
<string name="relay_mode_automatic_description">Utiliza a infraestrutura pública de retransmissores predefinida do VniDrop quando não está disponível uma ligação direta.</string>
|
<string name="relay_mode_automatic_description">Utiliza a infraestrutura pública de retransmissores predefinida do VniDrop quando não está disponível uma ligação direta.</string>
|
||||||
<string name="relay_mode_custom">Personalizado</string>
|
<string name="relay_mode_custom">Personalizado estrito</string>
|
||||||
<string name="relay_mode_custom_description">Utiliza apenas os servidores de retransmissão abaixo.</string>
|
<string name="relay_mode_custom_description">Utiliza apenas os retransmissores personalizados configurados ou ligações diretas. Apresenta um erro se não for possível estabelecer nenhum retransmissor personalizado.</string>
|
||||||
|
<string name="relay_mode_custom_direct_fallback">Personalizado com alternativa direta</string>
|
||||||
|
<string name="relay_mode_custom_direct_fallback_description">Prefere os retransmissores personalizados configurados. Se não estiverem disponíveis, continua apenas com ligações diretas. Nunca utiliza retransmissores públicos.</string>
|
||||||
|
<string name="relay_mode_local_only">Apenas rede local</string>
|
||||||
|
<string name="relay_mode_local_only_description">Desativa todos os retransmissores e permite apenas ligações diretas, principalmente para dispositivos na mesma rede.</string>
|
||||||
<string name="relay_privacy_description">Os retransmissores encaminham tráfego cifrado e não conseguem ler os seus ficheiros, mas o operador pode observar metadados da ligação.</string>
|
<string name="relay_privacy_description">Os retransmissores encaminham tráfego cifrado e não conseguem ler os seus ficheiros, mas o operador pode observar metadados da ligação.</string>
|
||||||
<string name="relay_remove_url">Remover servidor de retransmissão</string>
|
<string name="relay_remove_url">Remover servidor de retransmissão</string>
|
||||||
<string name="relay_restore_failed">Não foi possível restaurar as definições de rede anteriores. Reinicie o VniDrop e reveja a configuração dos retransmissores.</string>
|
<string name="relay_restore_failed">Não foi possível restaurar as definições de rede anteriores. Reinicie o VniDrop e reveja a configuração dos retransmissores.</string>
|
||||||
<string name="relay_settings_applied">Definições de rede aplicadas.</string>
|
<string name="relay_settings_applied">Definições de rede aplicadas.</string>
|
||||||
<string name="relay_strict_warning">O modo personalizado é estrito: o VniDrop não recorrerá a retransmissores públicos nem à descoberta pública. Os outros dispositivos têm de conseguir aceder aos retransmissores configurados.</string>
|
<string name="relay_strict_warning">O modo personalizado estrito só inicia se pelo menos um retransmissor configurado estiver acessível. Neste modo, o VniDrop nunca utiliza retransmissores nem descoberta públicos.</string>
|
||||||
<string name="relay_validation_duplicate_url">O URL do retransmissor na linha %1$d duplica uma entrada anterior.</string>
|
<string name="relay_validation_duplicate_url">O URL do retransmissor na linha %1$d duplica uma entrada anterior.</string>
|
||||||
<string name="relay_validation_https_required">O URL do retransmissor na linha %1$d tem de começar por https://.</string>
|
<string name="relay_validation_https_required">O URL do retransmissor na linha %1$d tem de começar por https://.</string>
|
||||||
<string name="relay_validation_invalid_url">O URL do retransmissor na linha %1$d não é válido.</string>
|
<string name="relay_validation_invalid_url">O URL do retransmissor na linha %1$d não é válido.</string>
|
||||||
|
|||||||
@@ -177,15 +177,19 @@
|
|||||||
<string name="relay_apply_restart_description">При применении сетевое соединение VniDrop перезапускается. Сначала остановите активные передачи и раздачи. Возможно, существующие приглашения потребуется отправить повторно.</string>
|
<string name="relay_apply_restart_description">При применении сетевое соединение VniDrop перезапускается. Сначала остановите активные передачи и раздачи. Возможно, существующие приглашения потребуется отправить повторно.</string>
|
||||||
<string name="relay_custom_urls_help">Введите по одному HTTPS-адресу ретранслятора в строке. Учётные данные в URL-адресах не поддерживаются. Сертификат TLS должен быть выдан общедоступным доверенным центром сертификации.</string>
|
<string name="relay_custom_urls_help">Введите по одному HTTPS-адресу ретранслятора в строке. Учётные данные в URL-адресах не поддерживаются. Сертификат TLS должен быть выдан общедоступным доверенным центром сертификации.</string>
|
||||||
<string name="relay_custom_urls_label">URL-адреса ретрансляторов</string>
|
<string name="relay_custom_urls_label">URL-адреса ретрансляторов</string>
|
||||||
<string name="relay_mode_automatic">Автоматически</string>
|
<string name="relay_mode_automatic">Автоматически (рекомендуется)</string>
|
||||||
<string name="relay_mode_automatic_description">Использовать стандартную публичную инфраструктуру ретрансляторов VniDrop, если прямое соединение недоступно.</string>
|
<string name="relay_mode_automatic_description">Использовать стандартную публичную инфраструктуру ретрансляторов VniDrop, если прямое соединение недоступно.</string>
|
||||||
<string name="relay_mode_custom">Пользовательский</string>
|
<string name="relay_mode_custom">Строго пользовательский</string>
|
||||||
<string name="relay_mode_custom_description">Использовать только указанные ниже серверы-ретрансляторы.</string>
|
<string name="relay_mode_custom_description">Использует только настроенные пользовательские ретрансляторы или прямые соединения. Сообщает об ошибке, если ни один пользовательский ретранслятор недоступен.</string>
|
||||||
|
<string name="relay_mode_custom_direct_fallback">Пользовательский с прямым резервом</string>
|
||||||
|
<string name="relay_mode_custom_direct_fallback_description">Предпочитает настроенные пользовательские ретрансляторы. Если они недоступны, продолжает работу только через прямые соединения. Публичные ретрансляторы не используются.</string>
|
||||||
|
<string name="relay_mode_local_only">Только локальная сеть</string>
|
||||||
|
<string name="relay_mode_local_only_description">Отключает все ретрансляторы и разрешает только прямые соединения, прежде всего для устройств в одной сети.</string>
|
||||||
<string name="relay_privacy_description">Ретрансляторы передают зашифрованный трафик и не могут читать ваши файлы, но их оператор может видеть метаданные соединения.</string>
|
<string name="relay_privacy_description">Ретрансляторы передают зашифрованный трафик и не могут читать ваши файлы, но их оператор может видеть метаданные соединения.</string>
|
||||||
<string name="relay_remove_url">Удалить сервер-ретранслятор</string>
|
<string name="relay_remove_url">Удалить сервер-ретранслятор</string>
|
||||||
<string name="relay_restore_failed">Не удалось восстановить предыдущие настройки сети. Перезапустите VniDrop и проверьте конфигурацию ретрансляторов.</string>
|
<string name="relay_restore_failed">Не удалось восстановить предыдущие настройки сети. Перезапустите VniDrop и проверьте конфигурацию ретрансляторов.</string>
|
||||||
<string name="relay_settings_applied">Настройки сети применены.</string>
|
<string name="relay_settings_applied">Настройки сети применены.</string>
|
||||||
<string name="relay_strict_warning">Пользовательский режим работает строго: VniDrop не будет переключаться на публичные ретрансляторы или публичное обнаружение. Другие устройства должны иметь доступ к настроенным ретрансляторам.</string>
|
<string name="relay_strict_warning">Строго пользовательский режим запускается, только если доступен хотя бы один настроенный ретранслятор. В этом режиме VniDrop никогда не использует публичные ретрансляторы или публичное обнаружение.</string>
|
||||||
<string name="relay_validation_duplicate_url">URL-адрес ретранслятора в строке %1$d повторяет предыдущую запись.</string>
|
<string name="relay_validation_duplicate_url">URL-адрес ретранслятора в строке %1$d повторяет предыдущую запись.</string>
|
||||||
<string name="relay_validation_https_required">URL-адрес ретранслятора в строке %1$d должен начинаться с https://.</string>
|
<string name="relay_validation_https_required">URL-адрес ретранслятора в строке %1$d должен начинаться с https://.</string>
|
||||||
<string name="relay_validation_invalid_url">URL-адрес ретранслятора в строке %1$d недействителен.</string>
|
<string name="relay_validation_invalid_url">URL-адрес ретранслятора в строке %1$d недействителен.</string>
|
||||||
|
|||||||
@@ -177,15 +177,19 @@
|
|||||||
<string name="relay_apply_restart_description">Applying restarts VniDrop’s network connection. Stop active transfers and shares first. Existing invitations may need to be shared again.</string>
|
<string name="relay_apply_restart_description">Applying restarts VniDrop’s network connection. Stop active transfers and shares first. Existing invitations may need to be shared again.</string>
|
||||||
<string name="relay_custom_urls_help">Enter one HTTPS relay URL per line. URL credentials are not supported. The TLS certificate must be issued by a publicly trusted certificate authority.</string>
|
<string name="relay_custom_urls_help">Enter one HTTPS relay URL per line. URL credentials are not supported. The TLS certificate must be issued by a publicly trusted certificate authority.</string>
|
||||||
<string name="relay_custom_urls_label">Relay URLs</string>
|
<string name="relay_custom_urls_label">Relay URLs</string>
|
||||||
<string name="relay_mode_automatic">Automatic</string>
|
<string name="relay_mode_automatic">Automatic (recommended)</string>
|
||||||
<string name="relay_mode_automatic_description">Use VniDrop’s default public relay infrastructure when a direct connection is unavailable.</string>
|
<string name="relay_mode_automatic_description">Use VniDrop’s default public relay infrastructure when a direct connection is unavailable.</string>
|
||||||
<string name="relay_mode_custom">Custom</string>
|
<string name="relay_mode_custom">Strict custom</string>
|
||||||
<string name="relay_mode_custom_description">Use only the relay servers below.</string>
|
<string name="relay_mode_custom_description">Use only the configured custom relays or direct connections. Report an error if no custom relay can be established.</string>
|
||||||
|
<string name="relay_mode_custom_direct_fallback">Custom with direct fallback</string>
|
||||||
|
<string name="relay_mode_custom_direct_fallback_description">Prefer the configured custom relays. If unavailable, continue with direct connections only. Never use public relays.</string>
|
||||||
|
<string name="relay_mode_local_only">Local only</string>
|
||||||
|
<string name="relay_mode_local_only_description">Disable all relays and allow only direct connections, primarily for devices on the same network.</string>
|
||||||
<string name="relay_privacy_description">Relays forward encrypted traffic and cannot read your files, but their operator can observe connection metadata.</string>
|
<string name="relay_privacy_description">Relays forward encrypted traffic and cannot read your files, but their operator can observe connection metadata.</string>
|
||||||
<string name="relay_remove_url">Remove relay server</string>
|
<string name="relay_remove_url">Remove relay server</string>
|
||||||
<string name="relay_restore_failed">Could not restore the previous network settings. Restart VniDrop and review your relay configuration.</string>
|
<string name="relay_restore_failed">Could not restore the previous network settings. Restart VniDrop and review your relay configuration.</string>
|
||||||
<string name="relay_settings_applied">Network settings applied.</string>
|
<string name="relay_settings_applied">Network settings applied.</string>
|
||||||
<string name="relay_strict_warning">Custom mode is strict: VniDrop will not fall back to public relays or public discovery. Other devices must be able to reach your configured relays.</string>
|
<string name="relay_strict_warning">Strict custom mode will not start unless at least one configured relay is reachable. VniDrop never uses public relays or public discovery in this mode.</string>
|
||||||
<string name="relay_validation_duplicate_url">Relay URL on line %1$d duplicates an earlier entry.</string>
|
<string name="relay_validation_duplicate_url">Relay URL on line %1$d duplicates an earlier entry.</string>
|
||||||
<string name="relay_validation_https_required">Relay URL on line %1$d must start with https://.</string>
|
<string name="relay_validation_https_required">Relay URL on line %1$d must start with https://.</string>
|
||||||
<string name="relay_validation_invalid_url">Relay URL on line %1$d is not valid.</string>
|
<string name="relay_validation_invalid_url">Relay URL on line %1$d is not valid.</string>
|
||||||
|
|||||||
@@ -29,7 +29,9 @@ enum class ShareAccessPolicy {
|
|||||||
|
|
||||||
enum class RelayMode {
|
enum class RelayMode {
|
||||||
Automatic,
|
Automatic,
|
||||||
Custom,
|
StrictCustom,
|
||||||
|
CustomWithDirectFallback,
|
||||||
|
LocalOnly,
|
||||||
}
|
}
|
||||||
|
|
||||||
data class RelaySettings(
|
data class RelaySettings(
|
||||||
@@ -37,6 +39,9 @@ data class RelaySettings(
|
|||||||
val relayUrls: List<String> = emptyList(),
|
val relayUrls: List<String> = emptyList(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val RelayMode.usesCustomRelayUrls: Boolean
|
||||||
|
get() = this == RelayMode.StrictCustom || this == RelayMode.CustomWithDirectFallback
|
||||||
|
|
||||||
enum class TransferDirection {
|
enum class TransferDirection {
|
||||||
Send,
|
Send,
|
||||||
Receive,
|
Receive,
|
||||||
|
|||||||
@@ -293,10 +293,18 @@ class CoreRepository(
|
|||||||
|
|
||||||
private fun RelaySettings.toNative(): CoreNetworkConfig = when (mode) {
|
private fun RelaySettings.toNative(): CoreNetworkConfig = when (mode) {
|
||||||
RelayMode.Automatic -> defaultCoreNetworkConfig()
|
RelayMode.Automatic -> defaultCoreNetworkConfig()
|
||||||
RelayMode.Custom -> CoreNetworkConfig(
|
RelayMode.StrictCustom -> CoreNetworkConfig(
|
||||||
mode = CoreRelayMode.CUSTOM,
|
mode = CoreRelayMode.STRICT_CUSTOM,
|
||||||
relayUrls = relayUrls,
|
relayUrls = relayUrls,
|
||||||
)
|
)
|
||||||
|
RelayMode.CustomWithDirectFallback -> CoreNetworkConfig(
|
||||||
|
mode = CoreRelayMode.CUSTOM_WITH_DIRECT_FALLBACK,
|
||||||
|
relayUrls = relayUrls,
|
||||||
|
)
|
||||||
|
RelayMode.LocalOnly -> CoreNetworkConfig(
|
||||||
|
mode = CoreRelayMode.LOCAL_ONLY,
|
||||||
|
relayUrls = emptyList(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun CoreEvent.toModel(): CoreEventModel = CoreEventModel(
|
private fun CoreEvent.toModel(): CoreEventModel = CoreEventModel(
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.vnidrop.app.core.RelayMode
|
import com.vnidrop.app.core.RelayMode
|
||||||
|
import com.vnidrop.app.core.usesCustomRelayUrls
|
||||||
import com.vnidrop.app.ui.components.Field
|
import com.vnidrop.app.ui.components.Field
|
||||||
import com.vnidrop.app.ui.components.PrimaryButton
|
import com.vnidrop.app.ui.components.PrimaryButton
|
||||||
import com.vnidrop.app.ui.icons.AppIcon
|
import com.vnidrop.app.ui.icons.AppIcon
|
||||||
@@ -30,7 +31,11 @@ import vnidrop.shared.generated.resources.relay_custom_urls_label
|
|||||||
import vnidrop.shared.generated.resources.relay_mode_automatic
|
import vnidrop.shared.generated.resources.relay_mode_automatic
|
||||||
import vnidrop.shared.generated.resources.relay_mode_automatic_description
|
import vnidrop.shared.generated.resources.relay_mode_automatic_description
|
||||||
import vnidrop.shared.generated.resources.relay_mode_custom
|
import vnidrop.shared.generated.resources.relay_mode_custom
|
||||||
|
import vnidrop.shared.generated.resources.relay_mode_custom_direct_fallback
|
||||||
|
import vnidrop.shared.generated.resources.relay_mode_custom_direct_fallback_description
|
||||||
import vnidrop.shared.generated.resources.relay_mode_custom_description
|
import vnidrop.shared.generated.resources.relay_mode_custom_description
|
||||||
|
import vnidrop.shared.generated.resources.relay_mode_local_only
|
||||||
|
import vnidrop.shared.generated.resources.relay_mode_local_only_description
|
||||||
import vnidrop.shared.generated.resources.relay_privacy_description
|
import vnidrop.shared.generated.resources.relay_privacy_description
|
||||||
import vnidrop.shared.generated.resources.relay_restore_failed
|
import vnidrop.shared.generated.resources.relay_restore_failed
|
||||||
import vnidrop.shared.generated.resources.relay_strict_warning
|
import vnidrop.shared.generated.resources.relay_strict_warning
|
||||||
@@ -73,15 +78,33 @@ internal fun NetworkSettings(
|
|||||||
)
|
)
|
||||||
SettingsDivider()
|
SettingsDivider()
|
||||||
RelayModeRow(
|
RelayModeRow(
|
||||||
icon = AppIcon.Radio,
|
icon = AppIcon.Shield,
|
||||||
title = stringResource(Res.string.relay_mode_custom),
|
title = stringResource(Res.string.relay_mode_custom),
|
||||||
description = stringResource(Res.string.relay_mode_custom_description),
|
description = stringResource(Res.string.relay_mode_custom_description),
|
||||||
selected = state.relayMode == RelayMode.Custom,
|
selected = state.relayMode == RelayMode.StrictCustom,
|
||||||
enabled = !state.isApplyingRelaySettings,
|
enabled = !state.isApplyingRelaySettings,
|
||||||
onClick = { onModeChanged(RelayMode.Custom) },
|
onClick = { onModeChanged(RelayMode.StrictCustom) },
|
||||||
|
)
|
||||||
|
SettingsDivider()
|
||||||
|
RelayModeRow(
|
||||||
|
icon = AppIcon.Radio,
|
||||||
|
title = stringResource(Res.string.relay_mode_custom_direct_fallback),
|
||||||
|
description = stringResource(Res.string.relay_mode_custom_direct_fallback_description),
|
||||||
|
selected = state.relayMode == RelayMode.CustomWithDirectFallback,
|
||||||
|
enabled = !state.isApplyingRelaySettings,
|
||||||
|
onClick = { onModeChanged(RelayMode.CustomWithDirectFallback) },
|
||||||
|
)
|
||||||
|
SettingsDivider()
|
||||||
|
RelayModeRow(
|
||||||
|
icon = AppIcon.CloudOff,
|
||||||
|
title = stringResource(Res.string.relay_mode_local_only),
|
||||||
|
description = stringResource(Res.string.relay_mode_local_only_description),
|
||||||
|
selected = state.relayMode == RelayMode.LocalOnly,
|
||||||
|
enabled = !state.isApplyingRelaySettings,
|
||||||
|
onClick = { onModeChanged(RelayMode.LocalOnly) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (state.relayMode == RelayMode.Custom) {
|
if (state.relayMode.usesCustomRelayUrls) {
|
||||||
Field(
|
Field(
|
||||||
value = state.relayUrlsText,
|
value = state.relayUrlsText,
|
||||||
onValueChange = onUrlsChanged,
|
onValueChange = onUrlsChanged,
|
||||||
@@ -94,12 +117,14 @@ internal fun NetworkSettings(
|
|||||||
color = colors.foregroundLighter,
|
color = colors.foregroundLighter,
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
)
|
)
|
||||||
Text(
|
if (state.relayMode == RelayMode.StrictCustom) {
|
||||||
stringResource(Res.string.relay_strict_warning),
|
Text(
|
||||||
color = colors.foregroundLight,
|
stringResource(Res.string.relay_strict_warning),
|
||||||
style = MaterialTheme.typography.bodySmall,
|
color = colors.foregroundLight,
|
||||||
fontWeight = FontWeight.Medium,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
)
|
fontWeight = FontWeight.Medium,
|
||||||
|
)
|
||||||
|
}
|
||||||
Text(
|
Text(
|
||||||
stringResource(Res.string.relay_privacy_description),
|
stringResource(Res.string.relay_privacy_description),
|
||||||
color = colors.foregroundLighter,
|
color = colors.foregroundLighter,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.vnidrop.app.feature.settings
|
|||||||
|
|
||||||
import com.vnidrop.app.core.RelayMode
|
import com.vnidrop.app.core.RelayMode
|
||||||
import com.vnidrop.app.core.RelaySettings
|
import com.vnidrop.app.core.RelaySettings
|
||||||
|
import com.vnidrop.app.core.usesCustomRelayUrls
|
||||||
|
|
||||||
sealed interface RelaySettingsInputError {
|
sealed interface RelaySettingsInputError {
|
||||||
data object MissingUrl : RelaySettingsInputError
|
data object MissingUrl : RelaySettingsInputError
|
||||||
@@ -21,7 +22,7 @@ fun validateRelaySettings(
|
|||||||
urlsText: String,
|
urlsText: String,
|
||||||
retainedUrls: List<String> = emptyList(),
|
retainedUrls: List<String> = emptyList(),
|
||||||
): RelaySettingsValidation {
|
): RelaySettingsValidation {
|
||||||
if (mode == RelayMode.Automatic) {
|
if (!mode.usesCustomRelayUrls) {
|
||||||
return RelaySettingsValidation(RelaySettings(mode, retainedUrls))
|
return RelaySettingsValidation(RelaySettings(mode, retainedUrls))
|
||||||
}
|
}
|
||||||
val lines = urlsText.lineSequence()
|
val lines = urlsText.lineSequence()
|
||||||
@@ -49,7 +50,7 @@ fun validateRelaySettings(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return RelaySettingsValidation(RelaySettings(RelayMode.Custom, normalized))
|
return RelaySettingsValidation(RelaySettings(mode, normalized))
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed interface RelayUrlResult {
|
private sealed interface RelayUrlResult {
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ import vnidrop.shared.generated.resources.notifications_title
|
|||||||
import vnidrop.shared.generated.resources.preferences_title
|
import vnidrop.shared.generated.resources.preferences_title
|
||||||
import vnidrop.shared.generated.resources.relay_mode_automatic
|
import vnidrop.shared.generated.resources.relay_mode_automatic
|
||||||
import vnidrop.shared.generated.resources.relay_mode_custom
|
import vnidrop.shared.generated.resources.relay_mode_custom
|
||||||
|
import vnidrop.shared.generated.resources.relay_mode_custom_direct_fallback
|
||||||
|
import vnidrop.shared.generated.resources.relay_mode_local_only
|
||||||
import vnidrop.shared.generated.resources.settings_title
|
import vnidrop.shared.generated.resources.settings_title
|
||||||
import vnidrop.shared.generated.resources.settings_network_title
|
import vnidrop.shared.generated.resources.settings_network_title
|
||||||
import vnidrop.shared.generated.resources.storage_title
|
import vnidrop.shared.generated.resources.storage_title
|
||||||
@@ -91,7 +93,9 @@ internal fun SettingsOverview(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun relayModeLabel(mode: RelayMode): String = when (mode) {
|
private fun relayModeLabel(mode: RelayMode): String = when (mode) {
|
||||||
RelayMode.Automatic -> stringResource(Res.string.relay_mode_automatic)
|
RelayMode.Automatic -> stringResource(Res.string.relay_mode_automatic)
|
||||||
RelayMode.Custom -> stringResource(Res.string.relay_mode_custom)
|
RelayMode.StrictCustom -> stringResource(Res.string.relay_mode_custom)
|
||||||
|
RelayMode.CustomWithDirectFallback -> stringResource(Res.string.relay_mode_custom_direct_fallback)
|
||||||
|
RelayMode.LocalOnly -> stringResource(Res.string.relay_mode_local_only)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import com.vnidrop.app.core.ReceiveFolder
|
|||||||
import com.vnidrop.app.core.RelayMode
|
import com.vnidrop.app.core.RelayMode
|
||||||
import com.vnidrop.app.core.RelaySettings
|
import com.vnidrop.app.core.RelaySettings
|
||||||
import com.vnidrop.app.core.TransferStatus
|
import com.vnidrop.app.core.TransferStatus
|
||||||
|
import com.vnidrop.app.core.usesCustomRelayUrls
|
||||||
import com.vnidrop.app.diagnostics.BugReportDraft
|
import com.vnidrop.app.diagnostics.BugReportDraft
|
||||||
import com.vnidrop.app.diagnostics.BugReportService
|
import com.vnidrop.app.diagnostics.BugReportService
|
||||||
import com.vnidrop.app.diagnostics.DiagnosticsBuildConfig
|
import com.vnidrop.app.diagnostics.DiagnosticsBuildConfig
|
||||||
@@ -113,7 +114,7 @@ data class SettingsState(
|
|||||||
) {
|
) {
|
||||||
val hasRelaySettingsChanges: Boolean
|
val hasRelaySettingsChanges: Boolean
|
||||||
get() = relayMode != savedRelaySettings.mode ||
|
get() = relayMode != savedRelaySettings.mode ||
|
||||||
(relayMode == RelayMode.Custom && relayUrlsText != savedRelaySettings.relayUrls.joinToString("\n"))
|
(relayMode.usesCustomRelayUrls && relayUrlsText != savedRelaySettings.relayUrls.joinToString("\n"))
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed interface SettingsEffect {
|
sealed interface SettingsEffect {
|
||||||
|
|||||||
@@ -58,14 +58,14 @@ class AppPreferencesRepository(
|
|||||||
) : PreferencesRepository {
|
) : PreferencesRepository {
|
||||||
override val preferences: Flow<AppPreferences> = dataStore.data
|
override val preferences: Flow<AppPreferences> = dataStore.data
|
||||||
.catch {
|
.catch {
|
||||||
emit(preferencesOf(PreferenceKeys.RelayMode to RelayMode.Custom.name))
|
emit(preferencesOf(PreferenceKeys.RelayMode to RelayMode.StrictCustom.name))
|
||||||
}
|
}
|
||||||
.map { prefs ->
|
.map { prefs ->
|
||||||
val storedRelayMode = prefs[PreferenceKeys.RelayMode]
|
val storedRelayMode = prefs[PreferenceKeys.RelayMode]
|
||||||
val parsedRelayMode = storedRelayMode?.let(::relayModeOrNull)
|
val parsedRelayMode = storedRelayMode?.let(::relayModeOrNull)
|
||||||
val relayMode = when (storedRelayMode) {
|
val relayMode = when (storedRelayMode) {
|
||||||
null -> RelayMode.Automatic
|
null -> RelayMode.Automatic
|
||||||
else -> parsedRelayMode ?: RelayMode.Custom
|
else -> parsedRelayMode ?: RelayMode.StrictCustom
|
||||||
}
|
}
|
||||||
val relayUrls = if (storedRelayMode != null && parsedRelayMode == null) {
|
val relayUrls = if (storedRelayMode != null && parsedRelayMode == null) {
|
||||||
emptyList()
|
emptyList()
|
||||||
@@ -199,6 +199,7 @@ private fun themeModeOrNull(raw: String): ThemeMode? =
|
|||||||
runCatching { ThemeMode.valueOf(raw) }.getOrNull()
|
runCatching { ThemeMode.valueOf(raw) }.getOrNull()
|
||||||
|
|
||||||
private fun relayModeOrNull(raw: String): RelayMode? =
|
private fun relayModeOrNull(raw: String): RelayMode? =
|
||||||
runCatching { RelayMode.valueOf(raw) }.getOrNull()
|
if (raw == "Custom") RelayMode.StrictCustom
|
||||||
|
else runCatching { RelayMode.valueOf(raw) }.getOrNull()
|
||||||
|
|
||||||
private const val AppPreferencesFileName = "app_preferences.preferences_pb"
|
private const val AppPreferencesFileName = "app_preferences.preferences_pb"
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ class ViewModelsTest {
|
|||||||
@Test
|
@Test
|
||||||
fun appViewModelInitializesCoreWithSavedRelaySettings() = runTest {
|
fun appViewModelInitializesCoreWithSavedRelaySettings() = runTest {
|
||||||
Dispatchers.setMain(StandardTestDispatcher(testScheduler))
|
Dispatchers.setMain(StandardTestDispatcher(testScheduler))
|
||||||
val custom = RelaySettings(RelayMode.Custom, listOf("https://relay.example.com"))
|
val custom = RelaySettings(RelayMode.StrictCustom, listOf("https://relay.example.com"))
|
||||||
val preferences = preferences().apply {
|
val preferences = preferences().apply {
|
||||||
mutablePreferences.value = mutablePreferences.value.copy(relaySettings = custom)
|
mutablePreferences.value = mutablePreferences.value.copy(relaySettings = custom)
|
||||||
}
|
}
|
||||||
@@ -144,13 +144,13 @@ class ViewModelsTest {
|
|||||||
val viewModel = settingsViewModel(preferences = preferences, repository = core)
|
val viewModel = settingsViewModel(preferences = preferences, repository = core)
|
||||||
advanceUntilIdle()
|
advanceUntilIdle()
|
||||||
|
|
||||||
viewModel.setRelayMode(RelayMode.Custom)
|
viewModel.setRelayMode(RelayMode.StrictCustom)
|
||||||
viewModel.setRelayUrlsText(" HTTPS://Relay.Example.com/ \nhttps://backup.example.com:443")
|
viewModel.setRelayUrlsText(" HTTPS://Relay.Example.com/ \nhttps://backup.example.com:443")
|
||||||
viewModel.applyRelaySettings()
|
viewModel.applyRelaySettings()
|
||||||
advanceUntilIdle()
|
advanceUntilIdle()
|
||||||
|
|
||||||
val expected = RelaySettings(
|
val expected = RelaySettings(
|
||||||
RelayMode.Custom,
|
RelayMode.StrictCustom,
|
||||||
listOf("https://relay.example.com", "https://backup.example.com"),
|
listOf("https://relay.example.com", "https://backup.example.com"),
|
||||||
)
|
)
|
||||||
assertEquals(expected, preferences.mutablePreferences.value.relaySettings)
|
assertEquals(expected, preferences.mutablePreferences.value.relaySettings)
|
||||||
@@ -159,6 +159,35 @@ class ViewModelsTest {
|
|||||||
assertFalse(viewModel.state.value.hasRelaySettingsChanges)
|
assertFalse(viewModel.state.value.hasRelaySettingsChanges)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun settingsAppliesCustomFallbackAndLocalOnlyWhileRetainingRelayDrafts() = runTest {
|
||||||
|
Dispatchers.setMain(StandardTestDispatcher(testScheduler))
|
||||||
|
val preferences = preferences()
|
||||||
|
val core = FakeCoreGateway()
|
||||||
|
val viewModel = settingsViewModel(preferences = preferences, repository = core)
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
viewModel.setRelayMode(RelayMode.CustomWithDirectFallback)
|
||||||
|
viewModel.setRelayUrlsText("https://relay.example.com")
|
||||||
|
viewModel.applyRelaySettings()
|
||||||
|
advanceUntilIdle()
|
||||||
|
viewModel.setRelayMode(RelayMode.LocalOnly)
|
||||||
|
viewModel.applyRelaySettings()
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
val fallback = RelaySettings(
|
||||||
|
RelayMode.CustomWithDirectFallback,
|
||||||
|
listOf("https://relay.example.com"),
|
||||||
|
)
|
||||||
|
val localOnly = RelaySettings(
|
||||||
|
RelayMode.LocalOnly,
|
||||||
|
listOf("https://relay.example.com"),
|
||||||
|
)
|
||||||
|
assertEquals(listOf(fallback, localOnly), core.initializedRelaySettings)
|
||||||
|
assertEquals(localOnly, preferences.mutablePreferences.value.relaySettings)
|
||||||
|
assertFalse(viewModel.state.value.hasRelaySettingsChanges)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun settingsIgnoresDuplicateApplyBeforeRestartBegins() = runTest {
|
fun settingsIgnoresDuplicateApplyBeforeRestartBegins() = runTest {
|
||||||
Dispatchers.setMain(StandardTestDispatcher(testScheduler))
|
Dispatchers.setMain(StandardTestDispatcher(testScheduler))
|
||||||
@@ -166,7 +195,7 @@ class ViewModelsTest {
|
|||||||
val viewModel = settingsViewModel(repository = core)
|
val viewModel = settingsViewModel(repository = core)
|
||||||
advanceUntilIdle()
|
advanceUntilIdle()
|
||||||
|
|
||||||
viewModel.setRelayMode(RelayMode.Custom)
|
viewModel.setRelayMode(RelayMode.StrictCustom)
|
||||||
viewModel.setRelayUrlsText("https://relay.example.com")
|
viewModel.setRelayUrlsText("https://relay.example.com")
|
||||||
viewModel.applyRelaySettings()
|
viewModel.applyRelaySettings()
|
||||||
viewModel.applyRelaySettings()
|
viewModel.applyRelaySettings()
|
||||||
@@ -186,7 +215,7 @@ class ViewModelsTest {
|
|||||||
advanceUntilIdle()
|
advanceUntilIdle()
|
||||||
assertEquals("endpoint", viewModel.state.value.endpointId)
|
assertEquals("endpoint", viewModel.state.value.endpointId)
|
||||||
|
|
||||||
viewModel.setRelayMode(RelayMode.Custom)
|
viewModel.setRelayMode(RelayMode.StrictCustom)
|
||||||
viewModel.setRelayUrlsText("https://relay.example.com")
|
viewModel.setRelayUrlsText("https://relay.example.com")
|
||||||
viewModel.applyRelaySettings()
|
viewModel.applyRelaySettings()
|
||||||
advanceUntilIdle()
|
advanceUntilIdle()
|
||||||
@@ -200,7 +229,7 @@ class ViewModelsTest {
|
|||||||
Dispatchers.setMain(StandardTestDispatcher(testScheduler))
|
Dispatchers.setMain(StandardTestDispatcher(testScheduler))
|
||||||
val core = FakeCoreGateway().apply {
|
val core = FakeCoreGateway().apply {
|
||||||
initializeHandler = { settings ->
|
initializeHandler = { settings ->
|
||||||
if (settings.mode == RelayMode.Custom) Result.failure(IllegalStateException("unreachable"))
|
if (settings.mode == RelayMode.StrictCustom) Result.failure(IllegalStateException("unreachable"))
|
||||||
else Result.success(Unit)
|
else Result.success(Unit)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -208,14 +237,14 @@ class ViewModelsTest {
|
|||||||
val viewModel = settingsViewModel(preferences = preferences, repository = core)
|
val viewModel = settingsViewModel(preferences = preferences, repository = core)
|
||||||
advanceUntilIdle()
|
advanceUntilIdle()
|
||||||
|
|
||||||
viewModel.setRelayMode(RelayMode.Custom)
|
viewModel.setRelayMode(RelayMode.StrictCustom)
|
||||||
viewModel.setRelayUrlsText("https://relay.example.com")
|
viewModel.setRelayUrlsText("https://relay.example.com")
|
||||||
viewModel.applyRelaySettings()
|
viewModel.applyRelaySettings()
|
||||||
advanceUntilIdle()
|
advanceUntilIdle()
|
||||||
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
listOf(
|
listOf(
|
||||||
RelaySettings(RelayMode.Custom, listOf("https://relay.example.com")),
|
RelaySettings(RelayMode.StrictCustom, listOf("https://relay.example.com")),
|
||||||
RelaySettings(),
|
RelaySettings(),
|
||||||
),
|
),
|
||||||
core.initializedRelaySettings,
|
core.initializedRelaySettings,
|
||||||
|
|||||||
@@ -6,16 +6,46 @@ import kotlin.test.Test
|
|||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
class RelaySettingsValidationTest {
|
class RelaySettingsValidationTest {
|
||||||
|
@Test
|
||||||
|
fun customFallbackUsesTheSameValidatedRelayList() {
|
||||||
|
val result = validateRelaySettings(
|
||||||
|
mode = RelayMode.CustomWithDirectFallback,
|
||||||
|
urlsText = "https://relay.example.com/",
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
RelaySettings(
|
||||||
|
RelayMode.CustomWithDirectFallback,
|
||||||
|
listOf("https://relay.example.com"),
|
||||||
|
),
|
||||||
|
result.settings,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun modesWithoutCustomRelaysRetainTheLastRelayList() {
|
||||||
|
val retained = listOf("https://relay.example.com")
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
RelaySettings(RelayMode.Automatic, retained),
|
||||||
|
validateRelaySettings(RelayMode.Automatic, "invalid", retained).settings,
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
RelaySettings(RelayMode.LocalOnly, retained),
|
||||||
|
validateRelaySettings(RelayMode.LocalOnly, "invalid", retained).settings,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun customRelayUrlsAreNormalized() {
|
fun customRelayUrlsAreNormalized() {
|
||||||
val result = validateRelaySettings(
|
val result = validateRelaySettings(
|
||||||
mode = RelayMode.Custom,
|
mode = RelayMode.StrictCustom,
|
||||||
urlsText = " HTTPS://Relay.Example.com/ \nhttps://[2001:DB8::1]:443",
|
urlsText = " HTTPS://Relay.Example.com/ \nhttps://[2001:DB8::1]:443",
|
||||||
)
|
)
|
||||||
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
RelaySettings(
|
RelaySettings(
|
||||||
RelayMode.Custom,
|
RelayMode.StrictCustom,
|
||||||
listOf("https://relay.example.com", "https://[2001:db8::1]"),
|
listOf("https://relay.example.com", "https://[2001:db8::1]"),
|
||||||
),
|
),
|
||||||
result.settings,
|
result.settings,
|
||||||
@@ -27,18 +57,18 @@ class RelaySettingsValidationTest {
|
|||||||
fun customRelayUrlsRequireHttpsAndRootPath() {
|
fun customRelayUrlsRequireHttpsAndRootPath() {
|
||||||
assertEquals(
|
assertEquals(
|
||||||
RelaySettingsInputError.HttpsRequired(1),
|
RelaySettingsInputError.HttpsRequired(1),
|
||||||
validateRelaySettings(RelayMode.Custom, "http://relay.example.com").error,
|
validateRelaySettings(RelayMode.StrictCustom, "http://relay.example.com").error,
|
||||||
)
|
)
|
||||||
assertEquals(
|
assertEquals(
|
||||||
RelaySettingsInputError.InvalidUrl(1),
|
RelaySettingsInputError.InvalidUrl(1),
|
||||||
validateRelaySettings(RelayMode.Custom, "https://relay.example.com/custom").error,
|
validateRelaySettings(RelayMode.StrictCustom, "https://relay.example.com/custom").error,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun duplicateNormalizedRelayUrlsAreRejected() {
|
fun duplicateNormalizedRelayUrlsAreRejected() {
|
||||||
val result = validateRelaySettings(
|
val result = validateRelaySettings(
|
||||||
RelayMode.Custom,
|
RelayMode.StrictCustom,
|
||||||
"https://relay.example.com\nHTTPS://RELAY.EXAMPLE.COM:443/",
|
"https://relay.example.com\nHTTPS://RELAY.EXAMPLE.COM:443/",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -48,13 +78,13 @@ class RelaySettingsValidationTest {
|
|||||||
@Test
|
@Test
|
||||||
fun structurallyValidIpv6RelayUrlsAreAccepted() {
|
fun structurallyValidIpv6RelayUrlsAreAccepted() {
|
||||||
val result = validateRelaySettings(
|
val result = validateRelaySettings(
|
||||||
RelayMode.Custom,
|
RelayMode.StrictCustom,
|
||||||
"https://[::1]:443\nhttps://[2001:db8::1]\nhttps://[::ffff:192.0.2.1]",
|
"https://[::1]:443\nhttps://[2001:db8::1]\nhttps://[::ffff:192.0.2.1]",
|
||||||
)
|
)
|
||||||
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
RelaySettings(
|
RelaySettings(
|
||||||
RelayMode.Custom,
|
RelayMode.StrictCustom,
|
||||||
listOf(
|
listOf(
|
||||||
"https://[::1]",
|
"https://[::1]",
|
||||||
"https://[2001:db8::1]",
|
"https://[2001:db8::1]",
|
||||||
@@ -75,7 +105,7 @@ class RelaySettingsValidationTest {
|
|||||||
).forEach { url ->
|
).forEach { url ->
|
||||||
assertEquals(
|
assertEquals(
|
||||||
RelaySettingsInputError.InvalidUrl(1),
|
RelaySettingsInputError.InvalidUrl(1),
|
||||||
validateRelaySettings(RelayMode.Custom, url).error,
|
validateRelaySettings(RelayMode.StrictCustom, url).error,
|
||||||
url,
|
url,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ class AppPreferencesRepositoryTest {
|
|||||||
fun customRelaySettingsArePersisted() = runBlocking {
|
fun customRelaySettingsArePersisted() = runBlocking {
|
||||||
val repository = repositoryForTest()
|
val repository = repositoryForTest()
|
||||||
val custom = RelaySettings(
|
val custom = RelaySettings(
|
||||||
mode = RelayMode.Custom,
|
mode = RelayMode.StrictCustom,
|
||||||
relayUrls = listOf("https://relay.example.com", "https://backup.example.com:443"),
|
relayUrls = listOf("https://relay.example.com", "https://backup.example.com:443"),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -42,6 +42,29 @@ class AppPreferencesRepositoryTest {
|
|||||||
assertEquals(custom, repository.preferences.first().relaySettings)
|
assertEquals(custom, repository.preferences.first().relaySettings)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun legacyCustomRelayModeMigratesToStrictCustom() = runBlocking {
|
||||||
|
val directory = Files.createTempDirectory("vnidrop-preferences-test").toString()
|
||||||
|
val dataStore = createAppPreferencesDataStore(directory)
|
||||||
|
dataStore.edit { preferences ->
|
||||||
|
preferences[stringPreferencesKey("relay_mode")] = "Custom"
|
||||||
|
preferences[stringPreferencesKey("relay_urls")] = "https://relay.example.com"
|
||||||
|
}
|
||||||
|
val repository = AppPreferencesRepository(
|
||||||
|
dataStore = dataStore,
|
||||||
|
defaults = AppPreferencesDefaults(
|
||||||
|
username = "Device Name",
|
||||||
|
receiveFolder = defaultFolder,
|
||||||
|
themeMode = ThemeMode.System,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
RelaySettings(RelayMode.StrictCustom, listOf("https://relay.example.com")),
|
||||||
|
repository.preferences.first().relaySettings,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun unknownStoredRelayModeFailsClosedAndCanBeReset() = runBlocking {
|
fun unknownStoredRelayModeFailsClosedAndCanBeReset() = runBlocking {
|
||||||
val directory = Files.createTempDirectory("vnidrop-preferences-test").toString()
|
val directory = Files.createTempDirectory("vnidrop-preferences-test").toString()
|
||||||
@@ -59,7 +82,7 @@ class AppPreferencesRepositoryTest {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
assertEquals(RelaySettings(RelayMode.Custom), repository.preferences.first().relaySettings)
|
assertEquals(RelaySettings(RelayMode.StrictCustom), repository.preferences.first().relaySettings)
|
||||||
|
|
||||||
repository.setRelaySettings(RelaySettings())
|
repository.setRelaySettings(RelaySettings())
|
||||||
assertEquals(RelaySettings(), repository.preferences.first().relaySettings)
|
assertEquals(RelaySettings(), repository.preferences.first().relaySettings)
|
||||||
@@ -85,7 +108,7 @@ class AppPreferencesRepositoryTest {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
assertEquals(RelaySettings(RelayMode.Custom), repository.preferences.first().relaySettings)
|
assertEquals(RelaySettings(RelayMode.StrictCustom), repository.preferences.first().relaySettings)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -145,10 +145,10 @@ class FoundationComposeTest {
|
|||||||
|
|
||||||
onNodeWithText("Network").performClick()
|
onNodeWithText("Network").performClick()
|
||||||
onNodeWithText("Device ID: endpoint-for-allowlist").assertIsDisplayed()
|
onNodeWithText("Device ID: endpoint-for-allowlist").assertIsDisplayed()
|
||||||
onNodeWithText("Custom").performClick()
|
onNodeWithText("Strict custom").performClick()
|
||||||
onNodeWithText(
|
onNodeWithText(
|
||||||
"Custom mode is strict: VniDrop will not fall back to public relays or public discovery. " +
|
"Strict custom mode will not start unless at least one configured relay is reachable. " +
|
||||||
"Other devices must be able to reach your configured relays.",
|
"VniDrop never uses public relays or public discovery in this mode.",
|
||||||
).assertIsDisplayed()
|
).assertIsDisplayed()
|
||||||
onNodeWithText("Apply network settings").performClick()
|
onNodeWithText("Apply network settings").performClick()
|
||||||
runOnIdle { assertTrue(applied) }
|
runOnIdle { assertTrue(applied) }
|
||||||
|
|||||||
Reference in New Issue
Block a user