public ClientConfiguration(ClientConfigurationList parentList) { _parentList = parentList; // Client properties PrivateKeyProperty.TargetTypes.Add(GetType()); DnsProperty.TargetTypes.Add(GetType()); AddressProperty.TargetTypes.Add(GetType()); // Server properties PresharedKeyProperty.TargetTypes.Add(typeof(ServerConfiguration)); PublicKeyProperty.TargetTypes.Add(typeof(ServerConfiguration)); ServerPersistentKeepaliveProperty.TargetTypes.Add(typeof(ServerConfiguration)); var serverConfiguration = new ServerConfiguration().Load <ServerConfiguration>(Configuration.LoadFromFile(ServerConfigurationPrerequisite.ServerDataPath)); string serverIp = serverConfiguration.AddressProperty.Value; // Add support for generating client IP AddressProperty.Action = new ConfigurationPropertyAction(this) { Name = nameof(Resources.GenerateFromServerAction), Description = string.Format(Resources.GenerateClientAddressActionDescription, serverIp), DependentProperty = serverConfiguration.AddressProperty, DependencySatisfiedFunc = prop => string.IsNullOrEmpty(prop.Validation?.Validate?.Invoke(prop)), Action = (conf, prop) => { IPNetwork serverNetwork = IPNetwork.Parse(serverConfiguration.AddressProperty.Value); var possibleAddresses = serverNetwork.ListIPAddress().Skip(2).SkipLast(1).ToList(); // Skip reserved .0 and .1 and .255. // If the current address is already in range, we're done if (possibleAddresses.Select(a => a.ToString()).Contains(prop.Value)) { return; } WaitCursor.SetOverrideCursor(Cursors.Wait); var existingAddresses = parentList.List.Select(c => c.AddressProperty.Value); // Find the first address that isn't used by another client prop.Value = possibleAddresses.FirstOrDefault(a => existingAddresses.Contains(a.ToString()) == false)?.ToString(); WaitCursor.SetOverrideCursor(null); } }; // Do custom validation on the Address (we want a specific IP or a CIDR with /32) AddressProperty.Validation = new ConfigurationPropertyValidation { Validate = obj => { string result = default; // First, try parsing with IPNetwork to see if it's in CIDR format if (IPNetwork.TryParse(obj.Value, out var network)) { // At this point, we know it's a valid network. Let's see how many addresses are in range if (network.Usable > 1) { // It's CIDR, but it defines more than one address. // However, IPNetwork has a quirk that parses single addresses (without mask) as a range. // So now let's see if it's a single address if (IPAddress.TryParse(obj.Value, out _) == false) { // If we get here, it passed CIDR parsing, but it defined more than one address (i.e., had a mask). It's bad! result = Resources.ClientAddressValidationError; } // Else, it's a single address as parsed by IPAddress, so we're good! } // Else // It's in CIDR notation and only defines a single address (/32) so we're good! } else { // Not even IPNetwork could parse it, so it's really bad! result = Resources.ClientAddressValidationError; } return(result); } }; // The client generates the PSK PresharedKeyProperty.Action = new ConfigurationPropertyAction(this) { Name = $"{nameof(PresharedKeyProperty)}{nameof(ConfigurationProperty.Action)}", Action = (conf, prop) => { WaitCursor.SetOverrideCursor(Cursors.Wait); prop.Value = new WireGuardExe().ExecuteCommand(new GeneratePresharedKeyCommand()); WaitCursor.SetOverrideCursor(null); } }; // Allowed IPs is special, because for the server, it's the same as the Address property for the client var allowedIpsProperty = new ConfigurationProperty(this) { PersistentPropertyName = "AllowedIPs", Value = AddressProperty.Value, IsHidden = true }; allowedIpsProperty.TargetTypes.Add(typeof(ServerConfiguration)); AddressProperty.PropertyChanged += (_, args) => { if (args.PropertyName == nameof(AddressProperty.Value)) { allowedIpsProperty.Value = AddressProperty.Value; } }; Properties.Add(allowedIpsProperty); // Adjust index of properties and resort AddressProperty.Index = 1; DnsProperty.Index = 2; SortProperties(); Properties.ForEach(p => p.PropertyChanged += (_, args) => { if (args.PropertyName == nameof(p.Value)) { GenerateQrCodeAction.RaisePropertyChanged(nameof(GenerateQrCodeAction.DependencySatisfied)); ExportConfigurationFileAction.RaisePropertyChanged(nameof(ExportConfigurationFileAction.DependencySatisfied)); } }); }
public override void Configure() { ClientConfigurationList clientConfigurations = new ClientConfigurationList(); List <ClientConfiguration> clientConfigurationsFromFile = new List <ClientConfiguration>(); // Load the clients from the conf files into a temporary list foreach (string clientConfigurationFile in Directory.GetFiles(ClientDataDirectory, "*.conf")) { clientConfigurationsFromFile.Add(new ClientConfiguration(clientConfigurations).Load <ClientConfiguration>(Configuration.LoadFromFile(clientConfigurationFile))); } // Now add them to the ObservableCollection, after sorting the temporary list foreach (ClientConfiguration clientConfiguration in clientConfigurationsFromFile.OrderBy(c => c.IndexProperty.Value)) { clientConfigurations.List.Add(clientConfiguration); } ClientConfigurationEditorWindow clientConfigurationEditorWindow = new ClientConfigurationEditorWindow { DataContext = clientConfigurations }; WaitCursor.SetOverrideCursor(Cursors.Wait); if (clientConfigurationEditorWindow.ShowDialog() == true) { WaitCursor.SetOverrideCursor(Cursors.Wait); // Delete the existing files (can't rely on updating them since the name of the client may have changed) foreach (string clientConfigurationFile in Directory.GetFiles(ClientDataDirectory, "*.conf")) { File.Delete(clientConfigurationFile); } foreach (string clientConfigurationFile in Directory.GetFiles(ClientWGDirectory, "*.conf")) { File.Delete(clientConfigurationFile); } // Check for duplicate names HashSet <string> discoveredDuplicateNames = new HashSet <string>(); foreach (ClientConfiguration clientConfiguration in clientConfigurations.List) { int i = 1; string originalName = clientConfiguration.NameProperty.Value; while (clientConfigurations.List.Any(c => c != clientConfiguration && c.NameProperty.Value == clientConfiguration.NameProperty.Value)) { if (discoveredDuplicateNames.Contains(originalName) == false) { // This is a duplicate name, but we haven't discovered it yet, meaning it's the first of its kind. // We want to rename the SECOND one, so we'll skip this one. discoveredDuplicateNames.Add(originalName); break; } clientConfiguration.NameProperty.Value = $"{originalName} ({i++})"; } } // Save to Data foreach (ClientConfiguration clientConfiguration in clientConfigurations.List) { clientConfiguration.IndexProperty.Value = clientConfigurations.List.IndexOf(clientConfiguration).ToString(); SaveData(clientConfiguration); } // Save to WG foreach (ClientConfiguration clientConfiguration in clientConfigurations.List) { SaveWG(clientConfiguration); } // Update server var serverConfigurationPrerequisite = new ServerConfigurationPrerequisite(); serverConfigurationPrerequisite.Update(); // Update the tunnel service, if everyone is happy if ((Fulfilled || !AnyClients) && serverConfigurationPrerequisite.Fulfilled && new TunnelServicePrerequisite().Fulfilled) { string output = new WireGuardExe().ExecuteCommand(new SyncConfigurationCommand(ServerConfigurationPrerequisite.WireGuardServerInterfaceName, ServerConfigurationPrerequisite.ServerWGPath), out int exitCode); if (exitCode != 0) { // Notify the user that there was an error syncing the server conf. WaitCursor.SetOverrideCursor(null); new UnhandledErrorWindow { DataContext = new UnhandledErrorWindowModel { Title = Resources.Error, Text = $"{Resources.ServerSyncError}{Environment.NewLine}{Environment.NewLine}{output}", Exception = new Exception(output) } }.ShowDialog(); } } WaitCursor.SetOverrideCursor(null); } Refresh(); }