Skip to content

Commit 814b412

Browse files
fix: opt-in tailscale vpn loop prevention (#148)
Co-authored-by: Dean Sheather <dean@deansheather.com>
1 parent d7f4ac8 commit 814b412

File tree

10 files changed

+62
-34
lines changed

10 files changed

+62
-34
lines changed

App/App.xaml.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ public App()
9292

9393
services.AddSingleton<IDispatcherQueueManager>(_ => this);
9494
services.AddSingleton<IDefaultNotificationHandler>(_ => this);
95+
services.AddSingleton<ISettingsManager<CoderConnectSettings>, SettingsManager<CoderConnectSettings>>();
9596
services.AddSingleton<ICredentialBackend>(_ =>
9697
new WindowsCredentialBackend(WindowsCredentialBackend.CoderCredentialsTargetName));
9798
services.AddSingleton<ICredentialManager, CredentialManager>();
@@ -120,7 +121,6 @@ public App()
120121
// FileSyncListMainPage is created by FileSyncListWindow.
121122
services.AddTransient<FileSyncListWindow>();
122123

123-
services.AddSingleton<ISettingsManager<CoderConnectSettings>, SettingsManager<CoderConnectSettings>>();
124124
services.AddSingleton<IStartupManager, StartupManager>();
125125
// SettingsWindow views and view models
126126
services.AddTransient<SettingsViewModel>();

App/Models/Settings.cs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ public class CoderConnectSettings : ISettings<CoderConnectSettings>
3434
/// </summary>
3535
public bool ConnectOnLaunch { get; set; }
3636

37+
/// <summary>
38+
/// When this is true Coder Connect will not attempt to protect against Tailscale loopback issues.
39+
/// </summary>
40+
public bool EnableCorporateVpnSupport { get; set; }
41+
3742
/// <summary>
3843
/// CoderConnect current settings version. Increment this when the settings schema changes.
3944
/// In future iterations we will be able to handle migrations when the user has
@@ -46,17 +51,21 @@ public CoderConnectSettings()
4651
Version = VERSION;
4752

4853
ConnectOnLaunch = false;
54+
55+
EnableCorporateVpnSupport = false;
4956
}
5057

51-
public CoderConnectSettings(int? version, bool connectOnLaunch)
58+
public CoderConnectSettings(int? version, bool connectOnLaunch, bool enableCorporateVpnSupport)
5259
{
5360
Version = version ?? VERSION;
5461

5562
ConnectOnLaunch = connectOnLaunch;
63+
64+
EnableCorporateVpnSupport = enableCorporateVpnSupport;
5665
}
5766

5867
public CoderConnectSettings Clone()
5968
{
60-
return new CoderConnectSettings(Version, ConnectOnLaunch);
69+
return new CoderConnectSettings(Version, ConnectOnLaunch, EnableCorporateVpnSupport);
6170
}
6271
}

App/Services/CredentialManager.cs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -223,8 +223,9 @@ private async Task<CredentialModel> LoadCredentialsInner(CancellationToken ct)
223223
};
224224
}
225225

226-
// Grab the lock again so we can update the state.
227-
using (await _opLock.LockAsync(ct))
226+
// Grab the lock again so we can update the state. Don't use the CT
227+
// here as it may have already been canceled.
228+
using (await _opLock.LockAsync(TimeSpan.FromSeconds(5), CancellationToken.None))
228229
{
229230
// Prevent new LoadCredentials calls from returning this task.
230231
if (_loadCts != null)
@@ -242,11 +243,8 @@ private async Task<CredentialModel> LoadCredentialsInner(CancellationToken ct)
242243
if (latestCreds is not null) return latestCreds;
243244
}
244245

245-
// If there aren't any latest credentials after a cancellation, we
246-
// most likely timed out and should throw.
247-
ct.ThrowIfCancellationRequested();
248-
249246
UpdateState(model);
247+
ct.ThrowIfCancellationRequested();
250248
return model;
251249
}
252250
}

App/Services/RpcController.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,16 +69,18 @@ public interface IRpcController : IAsyncDisposable
6969
public class RpcController : IRpcController
7070
{
7171
private readonly ICredentialManager _credentialManager;
72+
private readonly ISettingsManager<CoderConnectSettings> _settingsManager;
7273

7374
private readonly RaiiSemaphoreSlim _operationLock = new(1, 1);
7475
private Speaker<ClientMessage, ServiceMessage>? _speaker;
7576

7677
private readonly RaiiSemaphoreSlim _stateLock = new(1, 1);
7778
private readonly RpcModel _state = new();
7879

79-
public RpcController(ICredentialManager credentialManager)
80+
public RpcController(ICredentialManager credentialManager, ISettingsManager<CoderConnectSettings> settingsManager)
8081
{
8182
_credentialManager = credentialManager;
83+
_settingsManager = settingsManager;
8284
}
8385

8486
public event EventHandler<RpcModel>? StateChanged;
@@ -156,6 +158,7 @@ public async Task StartVpn(CancellationToken ct = default)
156158
using var _ = await AcquireOperationLockNowAsync();
157159
AssertRpcConnected();
158160

161+
var coderConnectSettings = await _settingsManager.Read(ct);
159162
var credentials = _credentialManager.GetCachedCredentials();
160163
if (credentials.State != CredentialState.Valid)
161164
throw new RpcOperationException(
@@ -175,6 +178,7 @@ public async Task StartVpn(CancellationToken ct = default)
175178
{
176179
CoderUrl = credentials.CoderUrl?.ToString(),
177180
ApiToken = credentials.ApiToken,
181+
TunnelUseSoftNetIsolation = coderConnectSettings.EnableCorporateVpnSupport,
178182
},
179183
}, ct);
180184
}

App/ViewModels/SettingsViewModel.cs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ public partial class SettingsViewModel : ObservableObject
1313
[ObservableProperty]
1414
public partial bool ConnectOnLaunch { get; set; }
1515

16+
[ObservableProperty]
17+
public partial bool DisableTailscaleLoopProtection { get; set; }
18+
1619
[ObservableProperty]
1720
public partial bool StartOnLoginDisabled { get; set; }
1821

@@ -31,6 +34,7 @@ public SettingsViewModel(ILogger<SettingsViewModel> logger, ISettingsManager<Cod
3134
_connectSettings = settingsManager.Read().GetAwaiter().GetResult();
3235
StartOnLogin = startupManager.IsEnabled();
3336
ConnectOnLaunch = _connectSettings.ConnectOnLaunch;
37+
DisableTailscaleLoopProtection = _connectSettings.EnableCorporateVpnSupport;
3438

3539
// Various policies can disable the "Start on login" option.
3640
// We disable the option in the UI if the policy is set.
@@ -43,6 +47,21 @@ public SettingsViewModel(ILogger<SettingsViewModel> logger, ISettingsManager<Cod
4347
}
4448
}
4549

50+
partial void OnDisableTailscaleLoopProtectionChanged(bool oldValue, bool newValue)
51+
{
52+
if (oldValue == newValue)
53+
return;
54+
try
55+
{
56+
_connectSettings.EnableCorporateVpnSupport = DisableTailscaleLoopProtection;
57+
_connectSettingsManager.Write(_connectSettings);
58+
}
59+
catch (Exception ex)
60+
{
61+
_logger.LogError($"Error saving Coder Connect {nameof(DisableTailscaleLoopProtection)} settings: {ex.Message}");
62+
}
63+
}
64+
4665
partial void OnConnectOnLaunchChanged(bool oldValue, bool newValue)
4766
{
4867
if (oldValue == newValue)
@@ -54,7 +73,7 @@ partial void OnConnectOnLaunchChanged(bool oldValue, bool newValue)
5473
}
5574
catch (Exception ex)
5675
{
57-
_logger.LogError($"Error saving Coder Connect settings: {ex.Message}");
76+
_logger.LogError($"Error saving Coder Connect {nameof(ConnectOnLaunch)} settings: {ex.Message}");
5877
}
5978
}
6079

App/Views/Pages/SettingsMainPage.xaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,12 @@
4444
>
4545
<ToggleSwitch IsOn="{x:Bind ViewModel.ConnectOnLaunch, Mode=TwoWay}" />
4646
</controls:SettingsCard>
47+
<controls:SettingsCard Description="This setting loosens some VPN loop protection checks in Coder Connect, allowing traffic to flow to a Coder deployment behind a corporate VPN. We only recommend enabling this option if Coder Connect doesn't work with your Coder deployment behind a corporate VPN."
48+
Header="Enable support for corporate VPNs"
49+
HeaderIcon="{ui:FontIcon Glyph=&#xE705;}"
50+
>
51+
<ToggleSwitch IsOn="{x:Bind ViewModel.DisableTailscaleLoopProtection, Mode=TwoWay}" />
52+
</controls:SettingsCard>
4753
</StackPanel>
4854
</Grid>
4955
</ScrollViewer>

App/Views/SettingsWindow.xaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99
xmlns:winuiex="using:WinUIEx"
1010
mc:Ignorable="d"
1111
Title="Coder Settings"
12-
Width="600" Height="350"
13-
MinWidth="600" MinHeight="350">
12+
Width="600" Height="500"
13+
MinWidth="600" MinHeight="500">
1414

1515
<Window.SystemBackdrop>
1616
<MicaBackdrop/>

App/Views/TrayWindow.xaml.cs

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,9 @@
55
using Coder.Desktop.App.Views.Pages;
66
using CommunityToolkit.Mvvm.Input;
77
using Microsoft.UI;
8-
using Microsoft.UI.Input;
98
using Microsoft.UI.Windowing;
109
using Microsoft.UI.Xaml;
1110
using Microsoft.UI.Xaml.Controls;
12-
using Microsoft.UI.Xaml.Documents;
1311
using Microsoft.UI.Xaml.Media.Animation;
1412
using System;
1513
using System.Collections.Generic;
@@ -19,6 +17,7 @@
1917
using Windows.Graphics;
2018
using Windows.System;
2119
using Windows.UI.Core;
20+
using Microsoft.UI.Input;
2221
using WinRT.Interop;
2322
using WindowActivatedEventArgs = Microsoft.UI.Xaml.WindowActivatedEventArgs;
2423

@@ -41,7 +40,6 @@ public sealed partial class TrayWindow : Window
4140

4241
private readonly IRpcController _rpcController;
4342
private readonly ICredentialManager _credentialManager;
44-
private readonly ISyncSessionController _syncSessionController;
4543
private readonly IUpdateController _updateController;
4644
private readonly IUserNotifier _userNotifier;
4745
private readonly TrayWindowLoadingPage _loadingPage;
@@ -51,15 +49,13 @@ public sealed partial class TrayWindow : Window
5149

5250
public TrayWindow(
5351
IRpcController rpcController, ICredentialManager credentialManager,
54-
ISyncSessionController syncSessionController, IUpdateController updateController,
55-
IUserNotifier userNotifier,
52+
IUpdateController updateController, IUserNotifier userNotifier,
5653
TrayWindowLoadingPage loadingPage,
5754
TrayWindowDisconnectedPage disconnectedPage, TrayWindowLoginRequiredPage loginRequiredPage,
5855
TrayWindowMainPage mainPage)
5956
{
6057
_rpcController = rpcController;
6158
_credentialManager = credentialManager;
62-
_syncSessionController = syncSessionController;
6359
_updateController = updateController;
6460
_userNotifier = userNotifier;
6561
_loadingPage = loadingPage;
@@ -74,9 +70,7 @@ public TrayWindow(
7470

7571
_rpcController.StateChanged += RpcController_StateChanged;
7672
_credentialManager.CredentialsChanged += CredentialManager_CredentialsChanged;
77-
_syncSessionController.StateChanged += SyncSessionController_StateChanged;
78-
SetPageByState(_rpcController.GetState(), _credentialManager.GetCachedCredentials(),
79-
_syncSessionController.GetState());
73+
SetPageByState(_rpcController.GetState(), _credentialManager.GetCachedCredentials());
8074

8175
// Setting these directly in the .xaml doesn't seem to work for whatever reason.
8276
TrayIcon.OpenCommand = Tray_OpenCommand;
@@ -127,8 +121,7 @@ public TrayWindow(
127121
};
128122
}
129123

130-
private void SetPageByState(RpcModel rpcModel, CredentialModel credentialModel,
131-
SyncSessionControllerStateModel syncSessionModel)
124+
private void SetPageByState(RpcModel rpcModel, CredentialModel credentialModel)
132125
{
133126
if (credentialModel.State == CredentialState.Unknown)
134127
{
@@ -201,18 +194,13 @@ private void MaybeNotifyUser(RpcModel rpcModel)
201194

202195
private void RpcController_StateChanged(object? _, RpcModel model)
203196
{
204-
SetPageByState(model, _credentialManager.GetCachedCredentials(), _syncSessionController.GetState());
197+
SetPageByState(model, _credentialManager.GetCachedCredentials());
205198
MaybeNotifyUser(model);
206199
}
207200

208201
private void CredentialManager_CredentialsChanged(object? _, CredentialModel model)
209202
{
210-
SetPageByState(_rpcController.GetState(), model, _syncSessionController.GetState());
211-
}
212-
213-
private void SyncSessionController_StateChanged(object? _, SyncSessionControllerStateModel model)
214-
{
215-
SetPageByState(_rpcController.GetState(), _credentialManager.GetCachedCredentials(), model);
203+
SetPageByState(_rpcController.GetState(), model);
216204
}
217205

218206
// Sadly this is necessary because Window.Content.SizeChanged doesn't

Tests.App/Services/CredentialManagerTest.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,10 @@ public async Task SetDuringLoad(CancellationToken ct)
317317
var loadTask = manager.LoadCredentials(ct);
318318
// Then fully perform a set.
319319
await manager.SetCredentials(TestServerUrl, TestApiToken, ct).WaitAsync(ct);
320-
// The load should have been cancelled.
321-
Assert.ThrowsAsync<TaskCanceledException>(() => loadTask);
320+
321+
// The load should complete with the new valid credentials
322+
var result = await loadTask;
323+
Assert.That(result.State, Is.EqualTo(CredentialState.Valid));
324+
Assert.That(result.CoderUrl?.ToString(), Is.EqualTo(TestServerUrl));
322325
}
323326
}

Vpn.Proto/vpn.proto

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ message ServiceMessage {
6262
StartResponse start = 2;
6363
StopResponse stop = 3;
6464
Status status = 4; // either in reply to a StatusRequest or broadcasted
65-
StartProgress start_progress = 5; // broadcasted during startup
65+
StartProgress start_progress = 5; // broadcasted during startup (used exclusively by Windows)
6666
}
6767
}
6868

@@ -214,6 +214,7 @@ message NetworkSettingsResponse {
214214
// StartResponse.
215215
message StartRequest {
216216
int32 tunnel_file_descriptor = 1;
217+
bool tunnel_use_soft_net_isolation = 8;
217218
string coder_url = 2;
218219
string api_token = 3;
219220
// Additional HTTP headers added to all requests

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy