diff --git a/Duplicati/Agent/Program.cs b/Duplicati/Agent/Program.cs index 773c860b1..29d4ff500 100644 --- a/Duplicati/Agent/Program.cs +++ b/Duplicati/Agent/Program.cs @@ -127,6 +127,7 @@ public static class Program KeepRemoteConnection.Start( settings.ServerUrl, settings.JWT, + settings.CertificateUrl, settings.ServerCertificates, cts.Token, ReKey, diff --git a/Duplicati/Agent/Settings.cs b/Duplicati/Agent/Settings.cs index d994b5bca..adf1c94b4 100644 --- a/Duplicati/Agent/Settings.cs +++ b/Duplicati/Agent/Settings.cs @@ -39,6 +39,7 @@ public sealed record Settings( string Filename, string JWT, string ServerUrl, + string CertificateUrl, string? SettingsEncryptionKey, IEnumerable ServerCertificates ) @@ -66,7 +67,7 @@ public sealed record Settings( { path ??= DefaultSettingsFile; var tmp = JsonSerializer.Deserialize(File.ReadAllText(path)) - ?? new Settings(path, string.Empty, string.Empty, string.Empty, Array.Empty()); + ?? new Settings(path, string.Empty, string.Empty, string.Empty, string.Empty, Array.Empty()); return tmp with { diff --git a/Duplicati/Library/RemoteControl/KeepRemoteConnection.cs b/Duplicati/Library/RemoteControl/KeepRemoteConnection.cs index b812ae013..587cd9512 100644 --- a/Duplicati/Library/RemoteControl/KeepRemoteConnection.cs +++ b/Duplicati/Library/RemoteControl/KeepRemoteConnection.cs @@ -100,9 +100,9 @@ public class KeepRemoteConnection : IDisposable /// /// The url to use /// The JWT token to use - /// The server key to use + /// The server keys to use /// The token to cancel the connection - private KeepRemoteConnection(string serverUrl, string JWT, IEnumerable serverKeys, CancellationToken cancellationToken, Func onReKey, Func onMessage) + private KeepRemoteConnection(string serverUrl, string JWT, string certificateUrl, IEnumerable serverKeys, CancellationToken cancellationToken, Func onReKey, Func onMessage) { _client = new Websocket.Client.WebsocketClient(new Uri(serverUrl)); _client.ReconnectTimeout = TimeSpan.FromSeconds(30); @@ -115,6 +115,7 @@ public class KeepRemoteConnection : IDisposable _client.DisconnectionHappened.Subscribe(info => { + // TODO: If disconnected due to certifiate error, we should try to get fresh certificates _state = ConnectionState.NotConnected; Log.WriteMessage(LogMessageType.Warning, LogTag, "WebsocketDisconnect", "Disconnected from the server"); }); @@ -159,7 +160,7 @@ public class KeepRemoteConnection : IDisposable throw new EnvelopeJsonParsingException("Invalid Json message"); if ((authMessage.WillReplaceToken ?? false) && authMessage.NewToken != null) - await onReKey(new ClaimedClientData(authMessage.NewToken, serverUrl, serverKeys, null)); + await onReKey(new ClaimedClientData(authMessage.NewToken, serverUrl, certificateUrl, serverKeys, null)); _state = ConnectionState.Authenticated; } @@ -209,15 +210,16 @@ public class KeepRemoteConnection : IDisposable /// /// The url to use /// The JWT to use + /// The certificate url to use /// The server keys to use /// The token to cancel the connection /// The callback to call when rekeying /// The callback to call when a message is received /// - public static Task Start(string serverUrl, string JWT, IEnumerable serverKeys, CancellationToken cancellationToken, Func onReKey, Func onMessage) + public static Task Start(string serverUrl, string JWT, string certificateUrl, IEnumerable serverKeys, CancellationToken cancellationToken, Func onReKey, Func onMessage) => Task.Run(async () => { - using var connection = new KeepRemoteConnection(serverUrl, JWT, serverKeys, cancellationToken, onReKey, onMessage); + using var connection = new KeepRemoteConnection(serverUrl, JWT, certificateUrl, serverKeys, cancellationToken, onReKey, onMessage); await connection._runnerTask; }); @@ -285,13 +287,14 @@ public class KeepRemoteConnection : IDisposable /// /// The url to use /// The JWT token to use - /// The server key to use + /// + /// The server keys to use /// The callback to call when rekeying /// The callback to call when a message is received /// The token to cancel the connection /// The connection object - public static KeepRemoteConnection CreateRemoteListener(string serverUrl, string JWT, IEnumerable serverKeys, CancellationToken cancellationToken, Func onReKey, Func onMessage) - => new KeepRemoteConnection(serverUrl, JWT, serverKeys, cancellationToken, onReKey, onMessage); + public static KeepRemoteConnection CreateRemoteListener(string serverUrl, string JWT, string certificateUrl, IEnumerable serverKeys, CancellationToken cancellationToken, Func onReKey, Func onMessage) + => new KeepRemoteConnection(serverUrl, JWT, certificateUrl, serverKeys, cancellationToken, onReKey, onMessage); /// /// Sends a heartbeat message to the server diff --git a/Duplicati/Library/RemoteControl/RegisterForRemote.cs b/Duplicati/Library/RemoteControl/RegisterForRemote.cs index 83459fb2f..77f661979 100644 --- a/Duplicati/Library/RemoteControl/RegisterForRemote.cs +++ b/Duplicati/Library/RemoteControl/RegisterForRemote.cs @@ -203,10 +203,10 @@ public class RegisterForRemote : IDisposable throw new InvalidOperationException("Resgistration process is not in the registered state"); _state = States.WaitingForClaim; - _cancellationTokenSource.CancelAfter(TimeSpan.FromSeconds(_registerClientData!.MaxLifetimeSeconds)); + _cancellationTokenSource.CancelAfter(TimeSpan.FromSeconds(Math.Min(3600, Math.Max(1, _registerClientData!.MaxLifetimeSeconds)))); try { - _claimedClientData = await RetryHelper.Retry(() => CheckClientClaimed(), _registerClientData!.MaxRetries, TimeSpan.FromSeconds(_registerClientData!.RetrySeconds), _cancellationTokenSource.Token); + _claimedClientData = await RetryHelper.Retry(() => CheckClientClaimed(), Math.Min(100, Math.Max(1, _registerClientData!.MaxRetries)), TimeSpan.FromSeconds(_registerClientData!.RetrySeconds), _cancellationTokenSource.Token); } catch (Exception ex) { diff --git a/Duplicati/Library/RemoteControl/SharedTypes.cs b/Duplicati/Library/RemoteControl/SharedTypes.cs index 2eb11fc2a..665efbf06 100644 --- a/Duplicati/Library/RemoteControl/SharedTypes.cs +++ b/Duplicati/Library/RemoteControl/SharedTypes.cs @@ -42,11 +42,13 @@ public sealed record RegisterClientData( /// /// The JWT token for the machine /// The URL for the remote server -/// The certificate key for the remote server +/// The URL for getting new server certificates +/// The certificates for the remote server /// The encryption key for the local settings public sealed record ClaimedClientData( string JWT, string ServerUrl, + string CertificateUrl, IEnumerable ServerCertificates, string? LocalEncryptionKey ); diff --git a/Duplicati/WebserverCore/Services/RemoteControllerRegistrationService.cs b/Duplicati/WebserverCore/Services/RemoteControllerRegistrationService.cs index c42a5637a..0ef80e28a 100644 --- a/Duplicati/WebserverCore/Services/RemoteControllerRegistrationService.cs +++ b/Duplicati/WebserverCore/Services/RemoteControllerRegistrationService.cs @@ -41,6 +41,10 @@ public sealed record RemoteControlConfig /// public required string ServerUrl { get; init; } /// + /// The URL for getting new server certificates. + /// + public required string CertificateUrl { get; init; } + /// /// The trusted server certificates. /// public required IEnumerable ServerCertificates { get; init; } diff --git a/Duplicati/WebserverCore/Services/RemoteControllerService.cs b/Duplicati/WebserverCore/Services/RemoteControllerService.cs index 35c951fde..12b07ae09 100644 --- a/Duplicati/WebserverCore/Services/RemoteControllerService.cs +++ b/Duplicati/WebserverCore/Services/RemoteControllerService.cs @@ -101,7 +101,8 @@ public class RemoteControllerService(Connection connection, IHttpClientFactory h { Token = data.JWT, ServerCertificates = MiniServerCertificate.MergeCertificates(data.ServerCertificates, oldCerts), - ServerUrl = data.ServerUrl + ServerUrl = data.ServerUrl, + CertificateUrl = data.CertificateUrl }); // TODO: Implement changing the encryption key