summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorOskar <oskar@mullvad.net>2025-09-12 16:21:06 +0200
committerOskar <oskar@mullvad.net>2025-10-01 13:19:43 +0200
commitf89775b79f7ce79cf1c8b0cb58d6212d4f9c09fa (patch)
tree05098a9dc5c5faf5739f038869c3e9283dae4f04
parent34812fa0e81b275ef271a6027cf854aa124f21c4 (diff)
downloadmullvadvpn-f89775b79f7ce79cf1c8b0cb58d6212d4f9c09fa.tar.xz
mullvadvpn-f89775b79f7ce79cf1c8b0cb58d6212d4f9c09fa.zip
Add check for if system time changes
-rw-r--r--desktop/packages/mullvad-vpn/src/main/account.ts42
-rw-r--r--desktop/packages/mullvad-vpn/src/main/system-time-monitor.ts15
-rw-r--r--desktop/packages/mullvad-vpn/test/e2e/mocked/account-expiry.spec.ts36
-rw-r--r--desktop/packages/mullvad-vpn/test/unit/system-time-monitor.spec.ts29
4 files changed, 103 insertions, 19 deletions
diff --git a/desktop/packages/mullvad-vpn/src/main/account.ts b/desktop/packages/mullvad-vpn/src/main/account.ts
index 7fc6b48d47..7ee415c0b4 100644
--- a/desktop/packages/mullvad-vpn/src/main/account.ts
+++ b/desktop/packages/mullvad-vpn/src/main/account.ts
@@ -1,4 +1,4 @@
-import { closeToExpiry } from '../shared/account-expiry';
+import { closeToExpiry, hasExpired } from '../shared/account-expiry';
import {
AccountDataError,
AccountNumber,
@@ -19,6 +19,7 @@ import AccountDataCache from './account-data-cache';
import { DaemonRpc } from './daemon-rpc';
import { IpcMainEventChannel } from './ipc-event-channel';
import { NotificationSender } from './notification-controller';
+import { systemTimeMonitor } from './system-time-monitor';
import { TunnelStateProvider } from './tunnel-state';
export interface LocaleProvider {
@@ -35,16 +36,14 @@ export default class Account {
private expiryNotificationFrequencyScheduler = new Scheduler();
private firstExpiryNotificationScheduler = new Scheduler();
+ private hasExpired = false;
+
private accountDataCache = new AccountDataCache(
(accountNumber) => {
return this.daemonRpc.getAccountData(accountNumber);
},
(accountData) => {
- this.accountDataValue = accountData;
-
- IpcMainEventChannel.account.notify?.(this.accountData);
-
- this.handleAccountExpiry();
+ this.handleAccountData(accountData);
},
);
@@ -53,7 +52,9 @@ export default class Account {
public constructor(
private delegate: AccountDelegate & TunnelStateProvider & LocaleProvider & NotificationSender,
private daemonRpc: DaemonRpc,
- ) {}
+ ) {
+ this.monitorExpiredChange();
+ }
public get accountData() {
return this.accountDataValue;
@@ -110,10 +111,10 @@ export default class Account {
};
public detectStaleAccountExpiry(tunnelState: TunnelState) {
- const hasExpired = !this.accountData || new Date() >= new Date(this.accountData.expiry);
+ const expired = !this.accountData || hasExpired(this.accountData.expiry);
// It's likely that the account expiry is stale if the daemon managed to establish the tunnel.
- if (tunnelState.state === 'connected' && hasExpired) {
+ if (tunnelState.state === 'connected' && expired) {
log.info('Detected the stale account expiry.');
this.accountDataCache.invalidate();
}
@@ -146,6 +147,16 @@ export default class Account {
IpcMainEventChannel.accountHistory.notify?.(accountHistory);
}
+ // This function monitors if the account is expired due to system clock changes.
+ private monitorExpiredChange() {
+ systemTimeMonitor(() => {
+ const expired = this.accountData && hasExpired(this.accountData.expiry);
+ if (expired !== this.hasExpired) {
+ this.handleAccountData(this.accountData);
+ }
+ });
+ }
+
private async createNewAccount(): Promise<string> {
try {
return await this.daemonRpc.createNewAccount();
@@ -180,7 +191,14 @@ export default class Account {
}
}
- private handleAccountExpiry() {
+ private handleAccountData(accountData?: IAccountData) {
+ this.accountDataValue = accountData;
+ this.hasExpired = this.accountData !== undefined && hasExpired(this.accountData?.expiry);
+ IpcMainEventChannel.account.notify?.(this.accountData);
+ this.showNotifications();
+ }
+
+ private showNotifications() {
if (this.accountData) {
const expiredNotification = new AccountExpiredNotificationProvider({
accountExpiry: this.accountData.expiry,
@@ -205,7 +223,7 @@ export default class Account {
const twelveHours = 12 * 60 * 60 * 1000;
const remainingMilliseconds = new Date(this.accountData.expiry).getTime() - Date.now();
const delay = Math.min(twelveHours, remainingMilliseconds);
- this.expiryNotificationFrequencyScheduler.schedule(() => this.handleAccountExpiry(), delay);
+ this.expiryNotificationFrequencyScheduler.schedule(() => this.showNotifications(), delay);
} else if (!closeToExpiry(this.accountData.expiry)) {
this.expiryNotificationFrequencyScheduler.cancel();
// If no longer close to expiry, all previous notifications should be closed
@@ -217,7 +235,7 @@ export default class Account {
// Add 10 seconds to be on the safe side. Never make it longer than a 24 days since
// the timeout needs to fit into a signed 32-bit integer.
const timeout = Math.min(expiry - now - threeDays + 10_000, 24 * 24 * 60 * 60 * 1000);
- this.firstExpiryNotificationScheduler.schedule(() => this.handleAccountExpiry(), timeout);
+ this.firstExpiryNotificationScheduler.schedule(() => this.showNotifications(), timeout);
}
}
}
diff --git a/desktop/packages/mullvad-vpn/src/main/system-time-monitor.ts b/desktop/packages/mullvad-vpn/src/main/system-time-monitor.ts
new file mode 100644
index 0000000000..308c38e527
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/main/system-time-monitor.ts
@@ -0,0 +1,15 @@
+const INTERVAL = 1000;
+
+// This functions monitors the system clock for changes, such as NTP corrections or user manually
+// changing the time. It probably has a lot of false positives, e.g. after suspend. And it only
+// checks once a second so the event will be a bit delayed.
+export function systemTimeMonitor(listener: () => void) {
+ let prevDate = Date.now();
+ setInterval(() => {
+ const now = Date.now();
+ if (Math.abs(now - prevDate - INTERVAL) > 500) {
+ listener();
+ }
+ prevDate = now;
+ }, INTERVAL);
+}
diff --git a/desktop/packages/mullvad-vpn/test/e2e/mocked/account-expiry.spec.ts b/desktop/packages/mullvad-vpn/test/e2e/mocked/account-expiry.spec.ts
index 6e259752e7..bd8f7b0317 100644
--- a/desktop/packages/mullvad-vpn/test/e2e/mocked/account-expiry.spec.ts
+++ b/desktop/packages/mullvad-vpn/test/e2e/mocked/account-expiry.spec.ts
@@ -48,15 +48,37 @@ test.describe('Account expiry', () => {
await routes.expired.waitForRoute();
});
- test('Should move clock back', async () => {
- await page.clock.setSystemTime('2025-04-03T14:00:00');
- await util.ipc.account[''].notify({
- expiry: new Date('2025-04-03T13:00:00').toISOString(),
+ // These tests verify that the renderer process will handle receiving the same expiry as
+ // previously but at different system times, where for one system time the expiry is passed but
+ // not for the other. This can happen if the system clock is changed.
+ test.describe('Handle system clock changes', () => {
+ test('Should move clock back', async () => {
+ const expiry = {
+ expiry: new Date('2025-04-03T13:00:00').toISOString(),
+ };
+
+ await page.clock.setSystemTime('2025-04-03T14:00:00');
+ await util.ipc.account[''].notify(expiry);
+
+ await routes.expired.waitForRoute();
+ await page.clock.setSystemTime('2025-01-01T12:00');
+ await util.ipc.account[''].notify(expiry);
+ await routes.main.waitForRoute();
});
- await routes.expired.waitForRoute();
- await page.clock.setSystemTime('2025-01-01T12:00');
- await routes.main.waitForRoute();
+ test('Should move clock forward', async () => {
+ const expiry = {
+ expiry: new Date('2025-04-03T13:00:00').toISOString(),
+ };
+
+ await page.clock.setSystemTime('2025-04-03T12:00:00');
+ await util.ipc.account[''].notify(expiry);
+
+ await routes.main.waitForRoute();
+ await page.clock.setSystemTime('2025-04-04T12:00');
+ await util.ipc.account[''].notify(expiry);
+ await routes.expired.waitForRoute();
+ });
});
function addTimeTests(newAccount: boolean) {
diff --git a/desktop/packages/mullvad-vpn/test/unit/system-time-monitor.spec.ts b/desktop/packages/mullvad-vpn/test/unit/system-time-monitor.spec.ts
new file mode 100644
index 0000000000..f207447288
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/test/unit/system-time-monitor.spec.ts
@@ -0,0 +1,29 @@
+import { expect, spy } from 'chai';
+import sinon from 'sinon';
+
+import { systemTimeMonitor } from '../../src/main/system-time-monitor';
+
+describe('IAccountData cache', () => {
+ let clock: sinon.SinonFakeTimers;
+
+ beforeEach(() => {
+ clock = sinon.useFakeTimers({ shouldAdvanceTime: true });
+ });
+
+ afterEach(() => {
+ clock.restore();
+ });
+
+ it('should notify when system clock changes', () => {
+ const systemTimeListener = spy();
+
+ clock.setSystemTime(new Date('2025-01-01'));
+ systemTimeMonitor(systemTimeListener);
+ clock.setSystemTime(new Date('2025-01-02'));
+ clock.tick(1001);
+ clock.setSystemTime(new Date('2025-01-01'));
+ clock.tick(1900);
+
+ expect(systemTimeListener).to.have.been.called.twice;
+ });
+});