diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..6c34532c --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,17 @@ +# Code of Conduct + +All open source projects managed by NetFoundry share a common [code of +conduct](https://netfoundry.github.io/policies/CODE_OF_CONDUCT.html) which all contributors are expected to follow. +Please be sure you read, understand and adhere to the thoughts expressed therein. + +Individuals acting in any way that is considered in violation of the Code of Conduct will receive corrective actions +appropriate for the violation. It is possible to act in ways which technically do not violate the Code of Conduct but +are clearly against the spirit of the project. These situations will also be considered on a case by case basis and if +necessary the Code of Conduct will be revised accordingly and approrpiate notifications will be made to the community +and to the violator. + +## Violations + +Contributors violating the Code of Conduct will receive public notification of the violation. Continued violations or +aggregious violations of the Code of Conduct will result in the contributor being removed from the contributors list as +well as further contributions being declined. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..03bb09f0 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,6 @@ +# Contributing + +NetFoundry welcomes all and any contributions. All open source projects managed by NetFoundry share a common +[guide for contributions](https://netfoundry.github.io/policies/CONTRIBUTING.html). + +If you are eager to contribute to a NetFoundry-managed open source project please read and act accordingly. diff --git a/DesktopEdge/MainWindow.xaml.cs b/DesktopEdge/MainWindow.xaml.cs index 191ea800..e8eecdea 100644 --- a/DesktopEdge/MainWindow.xaml.cs +++ b/DesktopEdge/MainWindow.xaml.cs @@ -131,6 +131,8 @@ namespace ZitiDesktopEdge { } } else if (mfa.Action == "enrollment_remove") { // ShowBlurb("removed mfa: " + mfa.Successful, ""); + } else if (mfa.Action == "mfa_auth_status") { + // ShowBlurb("mfa authenticated: " + mfa.Successful, ""); } else { await ShowBlurbAsync ("Unexpected error when processing MFA", ""); logger.Error("unexpected action: " + mfa.Action); @@ -509,6 +511,7 @@ namespace ZitiDesktopEdge { serviceClient.OnMfaEvent += ServiceClient_OnMfaEvent; serviceClient.OnLogLevelEvent += ServiceClient_OnLogLevelEvent; serviceClient.OnBulkServiceEvent += ServiceClient_OnBulkServiceEvent; + serviceClient.OnNotificationEvent += ServiceClient_OnNotificationEvent; Application.Current.Properties.Add("ServiceClient", serviceClient); monitorClient = new MonitorClient(); @@ -570,7 +573,39 @@ namespace ZitiDesktopEdge { } } - string nextVersionStr = null; + private void ServiceClient_OnNotificationEvent(object sender, NotificationEvent e) { + var displayMFARequired = false; + foreach (var notification in e.Notification) { + var found = identities.Find(id => id.Fingerprint == notification.Fingerprint); + if (found == null) { + logger.Warn($"{e.Op} event for {notification.Fingerprint} but the provided identity fingerprint was not found!"); + continue; + } + if (notification.MfaMaximumTimeout == 0) { + found.MFAInfo.IsAuthenticated = false; + // display notification message - critical + displayMFARequired = true; + } else if (notification.MfaMinimumTimeout == 0) { + // display notification message, only few services are timed out + } else { + // display notification message, only few services are about to timeout + } + } + // we may need to display mfa icon, based on the timer in UI, remove found.MFAInfo.IsAuthenticated setting in this function. + // the below function can show mfa icon even after user authenticates successfully, in race conditions + if (displayMFARequired) { + LoadIdentities(true); + this.Dispatcher.Invoke(() => { + IdentityDetails deets = ((MainWindow)Application.Current.MainWindow).IdentityMenu; + if (deets.IsVisible) { + deets.UpdateView(); + } + }); + } + } + + + string nextVersionStr = null; private void MonitorClient_OnReconnectFailure(object sender, object e) { if (nextVersionStr == null) { // check for the current version @@ -653,7 +688,7 @@ namespace ZitiDesktopEdge { IsUpdateAvailable = true; MainMenu.ShowUpdateAvailable(evt.TimeRemaining, evt.InstallTime); AlertCanvas.Visibility = Visibility.Visible; - ShowToast("An Update is Available for Ziti Desktop Edge"); + ShowToast("An Update is Available for Ziti Desktop Edge, will initiate auto installation by " + evt.InstallTime); SetNotifyIcon(""); // display a tag in UI and a button for the update software } @@ -800,12 +835,16 @@ namespace ZitiDesktopEdge { found.IsEnabled = zid.IsEnabled; found.MFAInfo.IsAuthenticated = !e.Id.MfaNeeded; LoadIdentities(true); - return; } } else if (e.Action == "updated") { //this indicates that all updates have been sent to the UI... wait for 2 seconds then trigger any ui updates needed await Task.Delay(2000); - + } else if (e.Action == "connected") { + //this indicates that all updates have been sent to the UI... wait for 2 seconds then trigger any ui updates needed + await Task.Delay(2000); + } else if (e.Action == "disconnected") { + //this indicates that all updates have been sent to the UI... wait for 2 seconds then trigger any ui updates needed + await Task.Delay(2000); } else { IdentityForgotten(ZitiIdentity.FromClient(e.Id)); } diff --git a/DesktopEdge/Properties/AssemblyInfo.cs b/DesktopEdge/Properties/AssemblyInfo.cs index 4e3bd4b5..4b76b04f 100644 --- a/DesktopEdge/Properties/AssemblyInfo.cs +++ b/DesktopEdge/Properties/AssemblyInfo.cs @@ -50,6 +50,6 @@ using System.Windows; // // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: -// [assembly: AssemblyVersion("1.9.9.0")] -[assembly: AssemblyVersion("1.9.9.0")] -[assembly: AssemblyFileVersion("1.9.9.0")] +// [assembly: AssemblyVersion("1.10.0.0")] +[assembly: AssemblyVersion("1.10.0.0")] +[assembly: AssemblyFileVersion("1.10.0.0")] diff --git a/DesktopEdge/Views/Screens/MainMenu.xaml b/DesktopEdge/Views/Screens/MainMenu.xaml index bd88983c..8ab38a37 100644 --- a/DesktopEdge/Views/Screens/MainMenu.xaml +++ b/DesktopEdge/Views/Screens/MainMenu.xaml @@ -188,6 +188,7 @@ + diff --git a/DesktopEdge/Views/Screens/MainMenu.xaml.cs b/DesktopEdge/Views/Screens/MainMenu.xaml.cs index 87f5d9b5..d1b2bb56 100644 --- a/DesktopEdge/Views/Screens/MainMenu.xaml.cs +++ b/DesktopEdge/Views/Screens/MainMenu.xaml.cs @@ -509,6 +509,33 @@ namespace ZitiDesktopEdge { } } + /// + /// Save the frequency information to the properties and queue for update. + /// + async private void UpdateFrequency() { + logger.Info("updating frequency..."); + DataClient client = (DataClient)Application.Current.Properties["ServiceClient"]; + try { + var newFrequencyVar = 5; // Int32.Parse(FrequencyNew.Text); + + var r = await client.NotificationFrequencyPayloadAsync(newFrequencyVar); + if (r.Code != 0) { + this.OnShowBlurb?.Invoke("Error: " + r.Error); + logger.Debug("ERROR: {0} : {1}", r.Message, r.Error); + } else { + this.OnShowBlurb?.Invoke("Frequency Saved"); + this.CloseEdit(); + } + logger.Info("Got response from update frequency task : {0}", r); + } catch (DataStructures.ServiceException se) { + this.OnShowBlurb?.Invoke("Error: " + se.Message); + logger.Error(se, "service exception in update frequency check: {0}", se.Message); + } catch (Exception ex) { + this.OnShowBlurb?.Invoke("Error: " + ex.Message); + logger.Error(ex, "unexpected error in update frequency check: {0}", ex.Message); + } + } + /// /// Show the Edit Modal and blur the background /// @@ -591,5 +618,9 @@ namespace ZitiDesktopEdge { private void SaveConfig() { this.UpdateConfig(); } + + private void SaveFrequency() { + this.UpdateFrequency(); + } } } \ No newline at end of file diff --git a/Installer/ZitiDesktopEdge.aip b/Installer/ZitiDesktopEdge.aip index 138e20f8..1c3c8f44 100644 --- a/Installer/ZitiDesktopEdge.aip +++ b/Installer/ZitiDesktopEdge.aip @@ -1,5 +1,5 @@ - + @@ -23,10 +23,10 @@ - + - + @@ -45,6 +45,7 @@ + @@ -52,6 +53,7 @@ + @@ -94,6 +96,7 @@ + @@ -260,6 +263,7 @@ + @@ -336,6 +340,7 @@ + diff --git a/Installer/build.ps1 b/Installer/build.ps1 index aeff0416..2083a5dc 100644 --- a/Installer/build.ps1 +++ b/Installer/build.ps1 @@ -24,7 +24,7 @@ msbuild ZitiDesktopEdge.sln /property:Configuration=Release Pop-Location -$ADV_INST_HOME = "C:\Program Files (x86)\Caphyon\Advanced Installer 18.3" +$ADV_INST_HOME = "C:\Program Files (x86)\Caphyon\Advanced Installer 18.4" $ADVINST = "${ADV_INST_HOME}\bin\x86\AdvancedInstaller.com" $ADVPROJECT = "${scriptPath}\ZitiDesktopEdge.aip" $installerVersion=(Get-Content -Path ${scriptPath}\..\version) diff --git a/Installer/reg.bat b/Installer/reg.bat index c8d030ac..044ca561 100644 --- a/Installer/reg.bat +++ b/Installer/reg.bat @@ -1 +1 @@ -"C:\Program Files (x86)\Caphyon\Advanced Installer 18.3\bin\x86\AdvancedInstaller.com" /register %1 +"C:\Program Files (x86)\Caphyon\Advanced Installer 18.4\bin\x86\AdvancedInstaller.com" /register %1 diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..93e58374 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2019 NetFoundry, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/ZitiDesktopEdge.Client/DataStructures/DataStructures.cs b/ZitiDesktopEdge.Client/DataStructures/DataStructures.cs index ccb5c643..78b90f4d 100644 --- a/ZitiDesktopEdge.Client/DataStructures/DataStructures.cs +++ b/ZitiDesktopEdge.Client/DataStructures/DataStructures.cs @@ -321,6 +321,17 @@ namespace ZitiDesktopEdge.DataStructures { } } + public class Notification { + public string IdentityName { get; set; } + public string Fingerprint { get; set; } + public string Message { get; set; } + public int MfaMinimumTimeout { get; set; } + public int MfaMaximumTimeout { get; set; } + public int TimeDuration { get; set; } + public string Severity { get; set; } + + } + public class ZitiTunnelStatus : SvcResponse { public TunnelStatus Status { get; set; } @@ -415,6 +426,11 @@ namespace ZitiDesktopEdge.DataStructures { public List Identities { get; set; } } + public class NotificationEvent : StatusEvent { + public List Notification { get; set; } + + } + public class ServiceEvent : ActionEvent { public string Fingerprint { get; set; } public Service Service { get; set; } @@ -494,4 +510,19 @@ namespace ZitiDesktopEdge.DataStructures { public ConfigPayload Payload { get; set; } } + public class NotificationFrequencyPayload { + public int NotificationFrequency { get; set; } + } + + public class NotificationFrequencyFunction: ServiceFunction { + public NotificationFrequencyFunction(int notificationFrequency) { + this.Function = "UpdateFrequency"; + this.Payload = new NotificationFrequencyPayload() { + NotificationFrequency = notificationFrequency + }; + } + + public NotificationFrequencyPayload Payload { get; set; } + } + } diff --git a/ZitiDesktopEdge.Client/ServiceClient/DataClient.cs b/ZitiDesktopEdge.Client/ServiceClient/DataClient.cs index 62670c69..d5bbac14 100644 --- a/ZitiDesktopEdge.Client/ServiceClient/DataClient.cs +++ b/ZitiDesktopEdge.Client/ServiceClient/DataClient.cs @@ -36,6 +36,7 @@ namespace ZitiDesktopEdge.ServiceClient { public event EventHandler OnLogLevelEvent; public event EventHandler OnMfaEvent; public event EventHandler OnBulkServiceEvent; + public event EventHandler OnNotificationEvent; protected override void ShutdownEvent(StatusEvent e) { Logger.Debug("Clean shutdown detected from ziti"); @@ -71,6 +72,10 @@ namespace ZitiDesktopEdge.ServiceClient { OnMfaEvent?.Invoke(this, e); } + protected virtual void NotificationEvent(NotificationEvent e) { + OnNotificationEvent?.Invoke(this, e); + } + protected override void ClientConnected(object e) { base.ClientConnected(e); } @@ -176,6 +181,7 @@ namespace ZitiDesktopEdge.ServiceClient { Function = "RemoveIdentity", Payload = new FingerprintPayload() { Fingerprint = fingerPrint } }; + Logger.Info("Removing Identity with fingerprint {0}", fingerPrint); await sendAsync(removeFunction); var r = await readAsync(ipcReader); } catch (Exception ioe) { @@ -367,6 +373,11 @@ namespace ZitiDesktopEdge.ServiceClient { MfaEvent mfa = serializer.Deserialize(jsonReader); MfaEvent(mfa); break; + case "notification": + Logger.Debug("Notification event received"); + NotificationEvent notificationEvent = serializer.Deserialize(jsonReader); + NotificationEvent(notificationEvent); + break; default: Logger.Debug("unexpected operation! " + evt.Op); break; @@ -414,6 +425,30 @@ namespace ZitiDesktopEdge.ServiceClient { return resp; } + async public Task NotificationFrequencyPayloadAsync(int frequency) { + SvcResponse resp = null; + try { + + NotificationFrequencyFunction frequencyPayload = new NotificationFrequencyFunction(frequency); + + await sendAsync(frequencyPayload); + resp = await readAsync(ipcReader); + Logger.Debug("frequency update payload is sent to the ziti tunnel"); + } catch (Exception ex) { + //almost certainly a problem with the pipe - recreate the pipe... + //setupPipe(); + //throw; + Logger.Error(ex, "Unexpected error"); + CommunicationError(ex); + throw ex; + } + if (resp?.Code != 0) { + Logger.Warn("failed to update the frequency. {0} {1}", resp.Message, resp.Error); + throw new ServiceException("Failed to update the frequency", resp.Code, "Un expected error."); + } + return resp; + } + async public Task debugAsync() { try { await sendAsync(new ServiceFunction() { Function = "Debug" }); diff --git a/ZitiUpdateService/App.config b/ZitiUpdateService/App.config index d933c958..355a075b 100644 --- a/ZitiUpdateService/App.config +++ b/ZitiUpdateService/App.config @@ -11,7 +11,7 @@ This operation is should probably remain high such as "1:0:0" (every hour) or "1:0:0:0" (every day) --> - + diff --git a/ZitiUpdateService/Properties/AssemblyInfo.cs b/ZitiUpdateService/Properties/AssemblyInfo.cs index d56ff0f6..fbe51c90 100644 --- a/ZitiUpdateService/Properties/AssemblyInfo.cs +++ b/ZitiUpdateService/Properties/AssemblyInfo.cs @@ -30,6 +30,6 @@ using System.Runtime.InteropServices; // // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: -// [assembly: AssemblyVersion("1.9.9.0")] -[assembly: AssemblyVersion("1.9.9.0")] -[assembly: AssemblyFileVersion("1.9.9.0")] +// [assembly: AssemblyVersion("1.10.0.0")] +[assembly: AssemblyVersion("1.10.0.0")] +[assembly: AssemblyFileVersion("1.10.0.0")] diff --git a/ZitiUpdateService/UpdateService.cs b/ZitiUpdateService/UpdateService.cs index 90a91436..a9a18e59 100644 --- a/ZitiUpdateService/UpdateService.cs +++ b/ZitiUpdateService/UpdateService.cs @@ -582,7 +582,7 @@ namespace ZitiUpdateService { ConfigureCheck(); - // for testing checkUpdateImmediately(); + checkUpdateImmediately(); try { dataClient.ConnectAsync().Wait(); @@ -668,22 +668,13 @@ namespace ZitiUpdateService { } if (sender == null && e == null) { - Logger.Info("package is in {0} - moving to install phase", fileDestination); - - if (!check.HashIsValid(updateFolder, filename)) { - Logger.Warn("The file was downloaded but the hash is not valid. The file will be removed: {0}", fileDestination); - File.Delete(fileDestination); - semaphore.Release(); - return; - } - Logger.Debug("downloaded file hash was correct. update can continue."); - new SignedFileValidator(fileDestination).Verify(); - - installZDE(fileDestination); + installZDE(fileDestination, filename); } else { Checkers.ZDEInstallerInfo info = check.GetZDEInstallerInfo(fileDestination); - if (info.IsCritical && _installationReminder == null) { + if (info.IsCritical) { + installZDE(fileDestination, filename); + } else if (!info.IsCritical && _installationReminder == null) { // Timer for installation reminder var installationReminderInterval = ConfigurationManager.AppSettings.Get("InstallationReminder"); var instInt = TimeSpan.Zero; @@ -698,6 +689,10 @@ namespace ZitiUpdateService { _installationReminder = new CustomTimer(callback, state, instInt, instInt); state._timer = _installationReminder; Logger.Info("Installation reminder for ZDE version {0} is set to {1}", info.Version, instInt); + + info.TimeRemaining = _installationReminder.DueTime.TotalMilliseconds / 1000; // converting to seconds + info.InstallTime = DateTime.Now.AddMilliseconds(_installationReminder.DueTime.TotalMilliseconds); + NotifyInstallationUpdates(info, 0, ""); } if (_installationReminder != null) { @@ -705,9 +700,6 @@ namespace ZitiUpdateService { info.InstallTime = DateTime.Now.AddMilliseconds(_installationReminder.DueTime.TotalMilliseconds); Logger.Info("Installation of ZDE version {0} will be initiated in {1} seconds, approximately at {2}", info.Version, info.TimeRemaining, info.InstallTime); } - if (info.Version != null) { - NotifyInstallationUpdates(info, 0, ""); - } } } catch (Exception ex) { try { @@ -724,7 +716,18 @@ namespace ZitiUpdateService { semaphore.Release(); } - private void installZDE(string fileDestination) { + private void installZDE(string fileDestination, string filename) { + Logger.Info("package is in {0} - moving to install phase", fileDestination); + + if (!check.HashIsValid(updateFolder, filename)) { + Logger.Warn("The file was downloaded but the hash is not valid. The file will be removed: {0}", fileDestination); + File.Delete(fileDestination); + semaphore.Release(); + return; + } + Logger.Debug("downloaded file hash was correct. update can continue."); + new SignedFileValidator(fileDestination).Verify(); + try { StopZiti(); StopUI().Wait(); diff --git a/ZitiUpdateService/checkers/GithubCheck.cs b/ZitiUpdateService/checkers/GithubCheck.cs index 0d2b215b..a1b495e4 100644 --- a/ZitiUpdateService/checkers/GithubCheck.cs +++ b/ZitiUpdateService/checkers/GithubCheck.cs @@ -181,10 +181,7 @@ namespace ZitiUpdateService.Checkers { } else { Logger.Debug("Comparing Version {0}, with current {1}", info.Version.ToString(), assemblyVersion.ToString()); - if (info.Version.Build - assemblyVersion.Build >= 5) { - info.IsCritical = true; - Logger.Info("ZDEInstaller is marked as critical because the client is behind 5 updates"); - } else if ((info.Version.Major - assemblyVersion.Major >= 1) || (info.Version.Minor - assemblyVersion.Minor >= 1)) { + if ((info.Version.Major - assemblyVersion.Major >= 1) || (info.Version.Minor - assemblyVersion.Minor >= 1)) { info.IsCritical = true; Logger.Info("ZDEInstaller is marked as critical because the major/minor version has changed"); } else { diff --git a/release-notes.md b/release-notes.md index 570166ff..8913fceb 100644 --- a/release-notes.md +++ b/release-notes.md @@ -4,14 +4,32 @@ * none ## Other changes: -* [#278](https://github.com/openziti/desktop-edge-win/issues/278) inform the user an update is available before automatically updating. So user can manually install the latest version anytime within 1 week after the release is published. If a major/minor version has changed or if the client is missing 5 updates, then the auto installation will start in 4 hours. If the user does not initiate manual installation within the given time, a warning will be displayed for 4 hours and ZDE will auto update. +* [#278](https://github.com/openziti/desktop-edge-win/issues/278) inform the user an update is available before automatically updating. So user can manually install the latest version anytime within 1 week after the release is published. If a major/minor version has changed or if the client is missing 5 updates, then the auto installation will start immediately. If the user does not initiate manual installation within the given time, a warning will be displayed for 2 hours and ZDE will auto update. +* mfa timeout process + +## Other changes: +* none ## Bugs fixed: * none ## Dependency Updates +* updated c-sdk to v0.25.2, updated t-sdk to v0.15.9 + +# Release 1.9.10 + +## What's New * none +## Other changes: +* none + +## Bugs fixed: +* none + +## Dependency Updates +* wintun updated to 0.12 + # Release 1.9.9 ## What's New diff --git a/service/build.bat b/service/build.bat index cfaa6cd2..799c976e 100644 --- a/service/build.bat +++ b/service/build.bat @@ -14,12 +14,12 @@ REM See the License for the specific language governing permissions and REM limitations under the License. REM SET REPO_URL=https://github.com/openziti/ziti-tunnel-sdk-c.git -SET ZITI_TUNNEL_REPO_BRANCH=v0.15.8 +SET ZITI_TUNNEL_REPO_BRANCH=v0.15.10 REM override the c sdk used in the build - leave blank for the same as specified in the tunneler sdk SET ZITI_SDK_C_BRANCH= REM the number of TCP connections the tunneler sdk can have at any one time SET TCP_MAX_CONNECTIONS=256 -SET WINTUN_DL_URL=https://www.wintun.net/builds/wintun-0.11.zip +SET WINTUN_DL_URL=https://www.wintun.net/builds/wintun-0.12.zip set SVC_ROOT_DIR=%~dp0 set CURDIR=%CD% diff --git a/service/cziti/ctun.go b/service/cziti/ctun.go index 09c082cd..62b1508b 100644 --- a/service/cziti/ctun.go +++ b/service/cziti/ctun.go @@ -45,6 +45,7 @@ void readIdle(uv_prepare_t *idler); void call_on_packet(void *packet, ssize_t len, packet_cb cb, void *ctx); void remove_intercepts(uv_async_t *handle); +void add_intercepts(uv_async_t *handle); void free_async(uv_handle_t* timer); dns_manager* get_dns_mgr_from_c(); @@ -242,26 +243,42 @@ func (t *tunnel) runWriteLoop() { } } -type RemoveWG struct { +type TunnelerActionWaitGroup struct { Wg *sync.WaitGroup Czsvc *ZService } -func RemoveIntercept(rwg *RemoveWG) { +func RemoveIntercept(removeWaitGroup *TunnelerActionWaitGroup) { async := (*C.uv_async_t)(C.malloc(C.sizeof_uv_async_t)) - async.data = unsafe.Pointer(rwg) + async.data = unsafe.Pointer(removeWaitGroup) C.uv_async_init(_impl.libuvCtx.l, async, C.uv_async_cb(C.remove_intercepts)) C.uv_async_send((*C.uv_async_t)(unsafe.Pointer(async))) } +func AddIntercept(addWaitGroup *TunnelerActionWaitGroup) { + async := (*C.uv_async_t)(C.malloc(C.sizeof_uv_async_t)) + async.data = unsafe.Pointer(addWaitGroup) + C.uv_async_init(_impl.libuvCtx.l, async, C.uv_async_cb(C.add_intercepts)) + C.uv_async_send((*C.uv_async_t)(unsafe.Pointer(async))) +} + //export remove_intercepts func remove_intercepts(async *C.uv_async_t) { - rwg := (*RemoveWG)(async.data) - cSvcName := C.CString(rwg.Czsvc.Name) + removeWaitGroup := (*TunnelerActionWaitGroup)(async.data) + cSvcName := C.CString(removeWaitGroup.Czsvc.Name) defer C.free(unsafe.Pointer(cSvcName)) - C.ziti_tunneler_stop_intercepting(theTun.tunCtx, unsafe.Pointer(rwg.Czsvc.Czctx), cSvcName) + C.ziti_tunneler_stop_intercepting(theTun.tunCtx, unsafe.Pointer(removeWaitGroup.Czsvc.Czctx), cSvcName) C.uv_close((*C.uv_handle_t)(unsafe.Pointer(async)), C.uv_close_cb(C.free_async)) - rwg.Wg.Done() + removeWaitGroup.Wg.Done() +} + +//export add_intercepts +func add_intercepts(async *C.uv_async_t) { + addWaitGroup := (*TunnelerActionWaitGroup)(async.data) + + C.ziti_sdk_c_on_service(addWaitGroup.Czsvc.Czctx, addWaitGroup.Czsvc.Czsvc, C.ZITI_OK, unsafe.Pointer(theTun.tunCtx)) + C.uv_close((*C.uv_handle_t)(unsafe.Pointer(async)), C.uv_close_cb(C.free_async)) + addWaitGroup.Wg.Done() } //export apply_dns_go diff --git a/service/cziti/dns-proxy.go b/service/cziti/dns-proxy.go index 1209e975..fbc948c0 100644 --- a/service/cziti/dns-proxy.go +++ b/service/cziti/dns-proxy.go @@ -57,7 +57,7 @@ func processDNSquery(packet []byte, p *net.UDPAddr, s *net.UDPConn, ipVer int) { query = q.Question[0] log.Tracef("processing a dns query. type:%s, for:%s on %v. id:%v", dns.Type(query.Qtype), query.Name, p, q.Id) } else { - log.Warnf("DNS MSG had no question! %v", msg.String()) + log.Warnf("discarding dns message as it contained no question! %v", msg.String()) return } @@ -378,7 +378,11 @@ outer: for k, r := range reqs { if now.After(r.exp) { log.Debugf("a DNS request has expired - enable trace logging and reproduce this issue for more information") - log.Tracef("expired DNS req: %s %s", dns.Type(r.req.Question[0].Qtype), r.req.Question[0].Name) + if len(r.req.Question) > 1 { + log.Tracef("expired DNS req: %s %s", dns.Type(r.req.Question[0].Qtype), r.req.Question[0].Name) + } else { + log.Tracef("expired DNS req - had no question???") + } delete(reqs, k) } @@ -399,7 +403,7 @@ outer: case rep := <-respChan: reply := dns.Msg{} if err := reply.Unpack(rep); err == nil { - id := (uint32(reply.Id) << 16) | uint32(reply.Question[0].Qtype) + id := calcDnsId(&reply) dnsReqMutex.Lock() req, found := reqs[id] if found { @@ -428,7 +432,7 @@ outer: log.Debug("Upstream DNS proxy loop begins") for { pr := <-proxiedRequests - id := (uint32(pr.req.Id) << 16) | uint32(pr.req.Question[0].Qtype) + id := calcDnsId(pr.req) dnsReqMutex.Lock() reqs[id] = pr dnsReqMutex.Unlock() @@ -450,6 +454,13 @@ outer: log.Tracef("Proxied request sent to %v from ipv%d listener", proxy.RemoteAddr(), pr.ipVer) } } - + } +} + +func calcDnsId(req *dns.Msg) uint32 { + if len(req.Question) > 0 { + return (uint32(req.Id) << 16) | uint32(req.Question[0].Qtype) + } else { + return 0 //this shouldn't be happening - yet a user hit this situation } } diff --git a/service/cziti/mfa.c b/service/cziti/mfa.c deleted file mode 100644 index 1a8d7f02..00000000 --- a/service/cziti/mfa.c +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright NetFoundry, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -#include "sdk.h" -#include -#include - -void ziti_mfa_auth_request(ziti_ar_mfa_cb response_cb, ziti_context ztx, void *mfa_ctx, char *code, ziti_ar_mfa_status_cb auth_response, char *fingerprint) { - response_cb(ztx, mfa_ctx, code, auth_response, fingerprint); -} \ No newline at end of file diff --git a/service/cziti/mfa.go b/service/cziti/mfa.go index ee8ab036..cce2c9fb 100644 --- a/service/cziti/mfa.go +++ b/service/cziti/mfa.go @@ -47,26 +47,6 @@ type mfaCodes struct { var emptyCodes []string var mfaAuthResults = make(chan string) -//export ziti_aq_mfa_cb_go -func ziti_aq_mfa_cb_go(ztx C.ziti_context, mfa_ctx unsafe.Pointer, aq_mfa *C.ziti_auth_query_mfa, response_cb C.ziti_ar_mfa_cb) { - appCtx := C.ziti_app_ctx(ztx) - if appCtx != C.NULL { - log.Debugf("mfa requested for ziti context %p.", ztx) - zid := (*ZIdentity)(appCtx) - mfa := &Mfa{ - mfaContext: mfa_ctx, - authQuery: aq_mfa, - responseCb: response_cb, - } - zid.mfa = mfa - zid.MfaNeeded = true - zid.MfaEnabled = true - log.Debugf("mfa enabled/needed set to true for ziti context [%p]. Identity name:%s [fingerprint: %s]", zid, zid.Name, zid.Fingerprint) - } else { - log.Debugf("mfa requested for ziti context/mfa context [%p, %p] but the context was NOT found in the map. This is unexpected. Please report.", ztx, mfa_ctx) - } -} - func EnableMFA(id *ZIdentity) { C.ziti_mfa_enroll(id.czctx, C.ziti_mfa_cb(C.ziti_mfa_enroll_cb_go), unsafe.Pointer(C.CString(id.Fingerprint))) } @@ -99,6 +79,7 @@ func ziti_mfa_enroll_cb_go(_ C.ziti_context, status C.int, enrollment *C.ziti_mf } } +// requires that the identity already be fully authenticated and used specifically for enrollment func VerifyMFA(id *ZIdentity, code string) { ccode := C.CString(code) defer C.free(unsafe.Pointer(ccode)) @@ -157,6 +138,53 @@ func ReturnMfaCodes(id *ZIdentity, code string) ([]string, error) { } } +// used when an identity is partially authenticated and waiting on 2fa, i.e. for authentication and for timeouts +func AuthMFA(id *ZIdentity, code string) error { + ccode := C.CString(code) + defer C.free(unsafe.Pointer(ccode)) + + log.Tracef("authenticating MFA for fingerprint: %s using code: %s", id.Fingerprint, code) + C.ziti_mfa_auth(id.czctx, ccode, C.ziti_mfa_cb(C.ziti_auth_mfa_status_cb_go), unsafe.Pointer(C.CString(id.Fingerprint))) + authResult := strings.TrimSpace(<-mfaAuthResults) + + if authResult == "" { + id.MfaEnabled = true + id.MfaNeeded = false + return nil + } + return fmt.Errorf("error in authMFA: %v", authResult) +} + +//export ziti_auth_mfa_status_cb_go +func ziti_auth_mfa_status_cb_go(ztx C.ziti_context, status C.int, cFingerprint *C.char) { + defer C.free(unsafe.Pointer(cFingerprint)) + fp := C.GoString(cFingerprint) + + log.Debugf("ziti_auth_mfa_status_cb_go called for %s. status: %d for ", fp, int(status)) + var m = dto.MfaEvent{ + ActionEvent: dto.MFAAuthenticationEvent, + Fingerprint: fp, + Successful: false, + RecoveryCodes: nil, + } + + if status != C.ZITI_OK { + e := C.ziti_errorstr(status) + ego := C.GoString(e) + log.Errorf("Error encounted when authenticating 2f mfa: %v", ego) + m.Error = ego + mfaAuthResults <- ego + } else { + log.Infof("Identity with fingerprint %s has successfully authenticated MFA", fp) + m.Successful = true + goapi.UpdateMfa(fp, true, false) + mfaAuthResults <- "" + } + + log.Debugf("sending ziti_mfa_auth response back to UI for %s. verified: %t. error: %s", fp, m.Successful, m.Error) + goapi.BroadcastEvent(m) +} + //export ziti_mfa_recovery_codes_cb_return func ziti_mfa_recovery_codes_cb_return(_ C.ziti_context, status C.int, recoveryCodes **C.char, cFingerprint *C.char) { fp := C.GoString(cFingerprint) @@ -237,48 +265,6 @@ func populateStringSlice(c_char_array **C.char) []string { return strs } -func AuthMFA(id *ZIdentity, code string) error { - if id.mfa == nil { - log.Warnf("AuthMFA called but mfa is nil. This usually is because AuthMFA is called from an unenrolled MFA endpoint or the endpoint has already been auth'ed") - return fmt.Errorf("identity has not authenticated yet") - } - if id.mfa.responseCb == nil { - log.Warnf("AuthMFA called but response cb is nil. This usually is because the session is already validiated. returning true from AuthMFA") - return nil - } - - ccode := C.CString(code) - defer C.free(unsafe.Pointer(ccode)) - - cfp := C.CString(id.Fingerprint) - defer C.free(unsafe.Pointer(cfp)) - log.Debugf("beginning authentication for fingerprint %s with code %s", id.Fingerprint, code) - C.ziti_mfa_auth_request(id.mfa.responseCb, id.czctx, id.mfa.mfaContext, ccode, C.ziti_ar_mfa_status_cb(C.ziti_ar_mfa_status_cb_go), cfp) - authResult := strings.TrimSpace(<-mfaAuthResults) - - if authResult == "" { - id.mfa.responseCb = nil - id.MfaNeeded = false - return nil - } - return fmt.Errorf("error in authMFA: %v", authResult) -} - -//export ziti_ar_mfa_status_cb_go -func ziti_ar_mfa_status_cb_go(ztx C.ziti_context, mfa_ctx unsafe.Pointer, status C.int, cFingerprint *C.char) { - fp := C.GoString(cFingerprint) - log.Debugf("ziti_ar_mfa_status_cb_go called for fingerprint: %s with status %v", fp, status) - if status == C.ZITI_OK { - log.Infof("mfa authentication succeeded for fingerprint: %s", fp) - mfaAuthResults <- "" - } else { - log.Warnf("mfa authentication failed for fingerprint: %s", fp) - e := C.ziti_errorstr(status) - ego := C.GoString(e) - mfaAuthResults <- ego - } -} - func RemoveMFA(id *ZIdentity, code string) { ccode := C.CString(code) defer C.free(unsafe.Pointer(ccode)) @@ -314,3 +300,8 @@ func ziti_mfa_cb_remove_go(_ C.ziti_context, status C.int, cFingerprint *C.char) log.Debugf("sending ziti_mfa_verify response back to UI for %s. verified: %t. error: %s", fp, m.Successful, m.Error) goapi.BroadcastEvent(m) } + +func EndpointStateChanged(id *ZIdentity, woken bool, unlocked bool) { + C.ziti_endpoint_state_change(id.czctx, C.bool(woken), C.bool(unlocked)) + log.Debugf("Endpoint status changed for id %s - woken %t, unlocked %t", id.Fingerprint, woken, unlocked) +} diff --git a/service/cziti/sdk.go b/service/cziti/sdk.go index 2f116a56..431a8743 100644 --- a/service/cziti/sdk.go +++ b/service/cziti/sdk.go @@ -47,12 +47,14 @@ import ( "encoding/json" "errors" "github.com/openziti/desktop-edge-win/service/ziti-tunnel/api" + "github.com/openziti/desktop-edge-win/service/ziti-tunnel/constants" "github.com/openziti/desktop-edge-win/service/ziti-tunnel/dto" "github.com/openziti/desktop-edge-win/service/ziti-tunnel/util/logging" "net" "os" "strings" "sync" + "time" "unsafe" ) @@ -73,11 +75,29 @@ type ServiceChange struct { ZitiContext *ZIdentity } type BulkServiceChange struct { - Fingerprint string - HostnamesToAdd map[string]bool - HostnamesToRemove map[string]bool - ServicesToRemove []*dto.Service - ServicesToAdd []*dto.Service + Fingerprint string + HostnamesToAdd map[string]bool + HostnamesToRemove map[string]bool + ServicesToRemove []*dto.Service + ServicesToAdd []*dto.Service + MfaMinTimeout int32 + MfaMaxTimeout int32 + MfaLastUpdatedTime time.Time +} + +type NotificationMessage struct { + IdentityName string + Fingerprint string + Message string + MfaMinimumTimeout int32 + MfaMaximumTimeout int32 + MfaTimeDuration int + Severity string +} + +type TunnelNotificationEvent struct { + Op string + Notification []NotificationMessage } var _impl sdk @@ -109,35 +129,34 @@ func (inst *sdk) run(loglevel int) { C.libuv_run(inst.libuvCtx) } +type RefreshRequiredCheck func(int32, int32) bool + type ZService struct { Name string Id string Service *dto.Service + Czsvc *C.ziti_service Czctx C.ziti_context } type ZIdentity struct { - Options *C.ziti_options - czctx C.ziti_context - czid *C.ziti_identity - status int - statusErr error - Loaded bool - Name string - Version string - Services sync.Map - Fingerprint string - Active bool - StatusChanges func(int) - MfaNeeded bool - MfaEnabled bool - mfa *Mfa -} - -type Mfa struct { - mfaContext unsafe.Pointer - authQuery *C.ziti_auth_query_mfa - responseCb C.ziti_ar_mfa_cb + Options *C.ziti_options + czctx C.ziti_context + czid *C.ziti_identity + status int + statusErr error + Loaded bool + Name string + Version string + Services sync.Map + Fingerprint string + Active bool + StatusChanges func(int) + MfaNeeded bool + MfaEnabled bool + MfaMinTimeout int32 + MfaMaxTimeout int32 + MfaLastUpdatedTime time.Time } func NewZid(statusChange func(int)) *ZIdentity { @@ -145,6 +164,9 @@ func NewZid(statusChange func(int)) *ZIdentity { zid.Services = sync.Map{} zid.Options = (*C.ziti_options)(C.calloc(1, C.sizeof_ziti_options)) zid.StatusChanges = statusChange + zid.MfaMinTimeout = -1 + zid.MfaMaxTimeout = -1 + zid.MfaLastUpdatedTime = time.Now() return zid } @@ -224,6 +246,34 @@ func (zid *ZIdentity) Shutdown() { C.uv_async_send((*C.uv_async_t)(unsafe.Pointer(async))) } +func (zid *ZIdentity) MfaRefreshNeeded() bool { + return !(zid.MfaMinTimeout == -1 && zid.MfaMaxTimeout == -1) +} + +func (zid *ZIdentity) GetRemainingTime(timeout int32) int32 { + var remainingTimeout int32 + if (timeout - int32(time.Since(zid.MfaLastUpdatedTime).Seconds())) <= 0 { + remainingTimeout = 0 + } else { + remainingTimeout = timeout - int32(time.Since(zid.MfaLastUpdatedTime).Seconds()) + } + return remainingTimeout +} + +func (zid *ZIdentity) GetMFAState(interval int32) int { + var mfaState int + if (zid.MfaMaxTimeout > -1) && (zid.MfaMaxTimeout-int32(time.Since(zid.MfaLastUpdatedTime).Seconds())) < 0 { + mfaState = constants.MfaAllSvcTimeout + } else if (zid.MfaMinTimeout - int32(time.Since(zid.MfaLastUpdatedTime).Seconds())) < 0 { + mfaState = constants.MfaFewSvcTimeout + } else if (zid.MfaMinTimeout - int32(time.Since(zid.MfaLastUpdatedTime).Seconds())) < interval { + mfaState = constants.MfaNearingTimeout + } else { + mfaState = -1 + } + return mfaState +} + //export doZitiShutdown func doZitiShutdown(async *C.uv_async_t) { ctx := C.ziti_context(async.data) @@ -315,6 +365,7 @@ func serviceCB(ziti_ctx C.ziti_context, service *C.ziti_service, status C.int, z //if any posture query sets pass - that will grant the user access to that service hasAccess := false + timeout := -1 //find all posture checks sets... for setIdx := 0; true; setIdx++ { @@ -344,17 +395,28 @@ func serviceCB(ziti_ctx C.ziti_context, service *C.ziti_service, status C.int, z var pcId string pcId = C.GoString(pq.id) + log.Infof("Posture query %s, timeout %d", C.GoString(pq.id), int(pq.timeout)) _, found := pcIds[pcId] if found { log.Tracef("posture check with id %s already in failing posture check map", pcId) + for _, pc := range postureChecks { + if pc.Id == C.GoString(pq.id) { + if timeout == -1 || timeout > int(pq.timeout) { + timeout = int(pq.timeout) + } + break + } + } } else { pcIds[C.GoString(pq.id)] = false pc := dto.PostureCheck{ IsPassing: bool(pq.is_passing), QueryType: C.GoString(pq.query_type), Id: pcId, + Timeout: int(pq.timeout), } + timeout = pc.Timeout postureChecks = append(postureChecks, pc) } @@ -370,11 +432,13 @@ func serviceCB(ziti_ctx C.ziti_context, service *C.ziti_service, status C.int, z OwnsIntercept: true, PostureChecks: postureChecks, IsAccessable: hasAccess, + Timeout: int32(timeout), } added := ZService{ Name: name, Id: svcId, Service: svc, + Czsvc: service, Czctx: ziti_ctx, } zid.Services.Store(svcId, &added) @@ -506,6 +570,7 @@ func eventCB(ztx C.ziti_context, event *C.ziti_event_t) { if unsafe.Pointer(added) == C.NULL { break } + svcToAdd := serviceCB(ztx, added, C.ZITI_OK, zid) if svcToAdd != nil { addAddys := svcToAdd.Addresses @@ -517,13 +582,45 @@ func eventCB(ztx C.ziti_context, event *C.ziti_event_t) { servicesToAdd = append(servicesToAdd, svcToAdd) } } + var minimumTimeout int32 = -1 + var maximumTimeout int32 = -1 + noTimeoutSvc := false + if len(servicesToAdd) > 0 { + zid.Services.Range(func(key interface{}, value interface{}) bool { + //string, ZService + val := value.(*ZService) + + if val.Service.Timeout >= 0 { + if minimumTimeout == -1 || minimumTimeout > val.Service.Timeout { + minimumTimeout = val.Service.Timeout + } + if maximumTimeout == -1 || maximumTimeout < val.Service.Timeout { + maximumTimeout = val.Service.Timeout + } + } else { + noTimeoutSvc = true + } + + return true + + }) + if noTimeoutSvc { + maximumTimeout = -1 + } + } + + zid.MfaMinTimeout = minimumTimeout + zid.MfaMaxTimeout = maximumTimeout svcChange := BulkServiceChange{ - Fingerprint: zid.Fingerprint, - HostnamesToAdd: hostnamesToAdd, - HostnamesToRemove: hostnamesToRemove, - ServicesToAdd: servicesToAdd, - ServicesToRemove: servicesToRemove, + Fingerprint: zid.Fingerprint, + HostnamesToAdd: hostnamesToAdd, + HostnamesToRemove: hostnamesToRemove, + ServicesToAdd: servicesToAdd, + ServicesToRemove: servicesToRemove, + MfaMinTimeout: minimumTimeout, + MfaMaxTimeout: maximumTimeout, + MfaLastUpdatedTime: time.Now(), } if len(BulkServiceChanges) == cap(BulkServiceChanges) { @@ -531,6 +628,33 @@ func eventCB(ztx C.ziti_context, event *C.ziti_event_t) { } else { BulkServiceChanges <- svcChange } + case C.ZitiMfaAuthEvent: + zid := (*ZIdentity)(appCtx) + log.Debugf("mfa auth event for finger print %s", zid.Fingerprint) + zid.MfaNeeded = true + zid.MfaEnabled = true + + if zid.Fingerprint != "" { + var id = dto.Identity{ + Name: zid.Name, + FingerPrint: zid.Fingerprint, + Active: zid.Active, + ControllerVersion: zid.Version, + Status: "", + MfaNeeded: true, + MfaEnabled: true, + Tags: nil, + } + + var m = dto.IdentityEvent{ + ActionEvent: dto.IDENTITY_ADDED, + Id: id, + } + goapi.BroadcastEvent(m) + log.Debugf("mfa auth event set enabled/needed to true for ziti context [%p]. Identity name:%s [fingerprint: %s]", zid, zid.Name, zid.Fingerprint) + } else { + log.Debugf("mfa auth event set received with empty finger print, ziti context [%p]", zid) + } default: log.Infof("event %d not handled", event._type) } @@ -576,11 +700,9 @@ func LoadZiti(zid *ZIdentity, cfg string, refreshInterval int) { zid.Options.pq_os_cb = C.ziti_pq_os_cb(C.ziti_pq_os_go) zid.Options.pq_process_cb = C.ziti_pq_process_cb(C.ziti_pq_process_go) - zid.Options.events = C.ZitiContextEvent | C.ZitiServiceEvent | C.ZitiRouterEvent + zid.Options.events = C.ZitiContextEvent | C.ZitiServiceEvent | C.ZitiRouterEvent | C.ZitiMfaAuthEvent zid.Options.event_cb = C.ziti_event_cb(C.eventCB) - zid.Options.aq_mfa_cb = C.ziti_aq_mfa_cb(C.ziti_aq_mfa_cb_go) - ptr := unsafe.Pointer(zid) zid.Options.app_ctx = ptr diff --git a/service/cziti/sdk.h b/service/cziti/sdk.h index d68ead6a..d57ca5e3 100644 --- a/service/cziti/sdk.h +++ b/service/cziti/sdk.h @@ -75,15 +75,14 @@ ziti_service* ziti_service_array_get(ziti_service_array arr, int idx); void ziti_dump_go(char *msg); //declare all mfa callbacks -void ziti_aq_mfa_cb_go(ziti_context ztx, void *mfa_ctx, ziti_auth_query_mfa *aq_mfa, ziti_ar_mfa_cb response_cb); void ziti_mfa_enroll_cb_go(ziti_context ztx, int status, ziti_mfa_enrollment *mfa_enrollment, char *fingerprint); void ziti_mfa_cb_verify_go(ziti_context ztx, int status, char *fingerprint); void ziti_mfa_cb_remove_go(ziti_context ztx, int status, char *fingerprint); void ziti_mfa_recovery_codes_cb_return(ziti_context ztx, int status, char **recovery_codes, char *fingerprint); void ziti_mfa_recovery_codes_cb_generate(ziti_context ztx, int status, char **recovery_codes, char *fingerprint); -void ziti_mfa_auth_request(ziti_ar_mfa_cb response_cb, ziti_context ztx, void *mfa_ctx, char *code, ziti_ar_mfa_status_cb auth_response, char *fingerprint); void ziti_mfa_ar_cb(ziti_context ztx, void *mfa_ctx, int status); void ziti_ar_mfa_status_cb_go(ziti_context ztx, void *mfa_ctx, int status, char *fingerprint); +void ziti_auth_mfa_status_cb_go(ziti_context ztx, int status, char *fingerprint); ziti_posture_query_set* posture_query_set_get(ziti_posture_query_set_array arr, int idx); ziti_posture_query* posture_queries_get(ziti_posture_query_array arr, int idx); diff --git a/service/windns/windns.go b/service/windns/windns.go index 0d38d8f6..701f45a7 100644 --- a/service/windns/windns.go +++ b/service/windns/windns.go @@ -307,4 +307,4 @@ Get-ItemProperty -Path $key | where {$_.ProfileName -match "Ziti.*"} | Remove-It log.Errorf("ERROR Cleaning up the Network Adapter profiles: %v", err) } -} \ No newline at end of file +} diff --git a/service/ziti-tunnel/constants/consts.go b/service/ziti-tunnel/constants/consts.go index cc1c95ea..4496b4f1 100644 --- a/service/ziti-tunnel/constants/consts.go +++ b/service/ziti-tunnel/constants/consts.go @@ -18,8 +18,14 @@ package constants const ( - Ipv4ip = "100.64.0.1" - Ipv4MaxMask = 8 - Ipv4MinMask = 16 - Ipv4DefaultMask = 10 + Ipv4ip = "100.64.0.1" + Ipv4MaxMask = 10 + Ipv4MinMask = 16 + Ipv4DefaultMask = 10 + MinimumFrequency = 5 // minimum notification window + MaximumFrequency = 20 // maximum notification window + + MfaAllSvcTimeout = 0 + MfaFewSvcTimeout = 1 + MfaNearingTimeout = 2 ) diff --git a/service/ziti-tunnel/dto/dtos.go b/service/ziti-tunnel/dto/dtos.go index c8930d43..20c88f2c 100644 --- a/service/ziti-tunnel/dto/dtos.go +++ b/service/ziti-tunnel/dto/dtos.go @@ -19,6 +19,7 @@ package dto import ( "log" + "time" "github.com/openziti/desktop-edge-win/service/ziti-tunnel/config" idcfg "github.com/openziti/sdk-golang/ziti/config" @@ -39,6 +40,7 @@ type Service struct { OwnsIntercept bool PostureChecks []PostureCheck IsAccessable bool + Timeout int32 } type Address struct { @@ -57,6 +59,7 @@ type PostureCheck struct { IsPassing bool QueryType string Id string + Timeout int } type ServiceOwner struct { @@ -68,17 +71,20 @@ type HostContext struct { } type Identity struct { - Name string - FingerPrint string - Active bool - Config idcfg.Config - ControllerVersion string - Status string - MfaEnabled bool - MfaNeeded bool - Services []*Service `json:",omitempty"` - Metrics *Metrics `json:",omitempty"` - Tags []string `json:",omitempty"` + Name string + FingerPrint string + Active bool + Config idcfg.Config + ControllerVersion string + Status string + MfaEnabled bool + MfaNeeded bool + Services []*Service `json:",omitempty"` + Metrics *Metrics `json:",omitempty"` + Tags []string `json:",omitempty"` + MfaMinTimeout int32 + MfaMaxTimeout int32 + MfaLastUpdatedTime time.Time } type Metrics struct { Up int64 @@ -110,16 +116,17 @@ func (id *Identity) Path() string { } type TunnelStatus struct { - Active bool - Duration int64 - Identities []*Identity - IpInfo *TunIpInfo `json:"IpInfo,omitempty"` - LogLevel string - ServiceVersion ServiceVersion - TunIpv4 string - TunIpv4Mask int - Status string - AddDns bool + Active bool + Duration int64 + Identities []*Identity + IpInfo *TunIpInfo `json:"IpInfo,omitempty"` + LogLevel string + ServiceVersion ServiceVersion + TunIpv4 string + TunIpv4Mask int + Status string + AddDns bool + NotificationFrequency int } type ServiceVersion struct { diff --git a/service/ziti-tunnel/dto/events.go b/service/ziti-tunnel/dto/events.go index e2e43379..0fb64b08 100644 --- a/service/ziti-tunnel/dto/events.go +++ b/service/ziti-tunnel/dto/events.go @@ -17,13 +17,15 @@ package dto const ( - ADDED = "added" - BULK = "bulk" - REMOVED = "removed" - ERROR = "error" - UPDATED = "updated" - CHANGED = "changed" - NORMAL = "Normal" + ADDED = "added" + BULK = "bulk" + REMOVED = "removed" + ERROR = "error" + UPDATED = "updated" + CHANGED = "changed" + NORMAL = "Normal" + CONNECTED = "connected" + DISCONNECTED = "disconnected" SERVICE_OP = "service" BULK_SERVICE_OP = "bulkservice" @@ -37,6 +39,7 @@ const ( MFAEnrollmentRemovedAction = "enrollment_remove" MFA_AUTH_CHALLENGE_ACTION = "auth_challenge" + MFAAuthenticationAction = "mfa_auth_status" ) var SERVICE_ADDED = ActionEvent{ @@ -60,6 +63,14 @@ var IDENTITY_REMOVED = ActionEvent{ StatusEvent: StatusEvent{Op: IDENTITY_OP}, Action: REMOVED, } +var IDENTITY_CONNECTED = ActionEvent{ + StatusEvent: StatusEvent{Op: IDENTITY_OP}, + Action: CONNECTED, +} +var IDENTITY_DISCONNECTED = ActionEvent{ + StatusEvent: StatusEvent{Op: IDENTITY_OP}, + Action: DISCONNECTED, +} var LOGLEVEL_CHANGED = ActionEvent{ StatusEvent: StatusEvent{Op: LOGLEVEL_OP}, Action: CHANGED, @@ -96,3 +107,7 @@ var MFAAuthChallengeEvent = ActionEvent{ StatusEvent: StatusEvent{Op: MFA_OP}, Action: MFA_AUTH_CHALLENGE_ACTION, } +var MFAAuthenticationEvent = ActionEvent{ + StatusEvent: StatusEvent{Op: MFA_OP}, + Action: MFAAuthenticationAction, +} diff --git a/service/ziti-tunnel/service/ipc.go b/service/ziti-tunnel/service/ipc.go index 6a4aad58..7ee17ce4 100644 --- a/service/ziti-tunnel/service/ipc.go +++ b/service/ziti-tunnel/service/ipc.go @@ -17,6 +17,7 @@ package service +import "C" import ( "bufio" "crypto/sha1" @@ -37,6 +38,7 @@ import ( "golang.zx2c4.com/wireguard/tun" "io" "io/ioutil" + "math" "math/rand" "net" "os" @@ -45,6 +47,7 @@ import ( "strconv" "strings" "sync" + "sync/atomic" "time" ) @@ -61,8 +64,9 @@ func (p *Pipes) Close() { } var shutdown = make(chan bool, 8) //a channel informing go routines to exit +var notificationFrequency *time.Ticker -func SubMain(ops chan string, changes chan<- svc.Status) error { +func SubMain(ops chan string, changes chan<- svc.Status, winEvents <-chan WindowsEvents) error { log.Info("============================== service begins ==============================") windns.RemoveAllNrptRules() // cleanup old ziti tun profiles @@ -82,6 +86,7 @@ func SubMain(ops chan string, changes chan<- svc.Status) error { // a channel to signal the handleEvents that initialization is complete initialized := make(chan struct{}) + shutdownDelay := make(chan bool) // initialize the network interface err := initialize(cLogLevel) @@ -101,6 +106,32 @@ func SubMain(ops chan string, changes chan<- svc.Status) error { //listen for services that show up go acceptServices() + // listen for windows power events and call mfa auth func + go func() { + for { + select { + case wEvents := <-winEvents: + if wEvents.WinPowerEvent == PBT_APMRESUMESUSPEND || wEvents.WinPowerEvent == PBT_APMRESUMEAUTOMATIC { + log.Debugf("Received Windows Power Event in tunnel %d", wEvents.WinPowerEvent) + for _, id := range rts.ids { + cziti.EndpointStateChanged(id.CId, true, false) + } + } + if wEvents.WinSessionEvent == WTS_SESSION_UNLOCK { + log.Debugf("Received Windows Session Event (device unlocked) in tunnel %d", wEvents.WinSessionEvent) + for _, id := range rts.ids { + if id.CId != nil && id.CId.Loaded { + cziti.EndpointStateChanged(id.CId, false, true) + } + } + } + case <-shutdownDelay: + log.Tracef("Exiting windows power events loop") + return + } + } + }() + // open the pipe for business pipes, err := openPipes() if err != nil { @@ -136,10 +167,9 @@ func SubMain(ops chan string, changes chan<- svc.Status) error { }) // wait 1 second for the shutdown to send to clients - shutdownDelay := make(chan bool) go func() { time.Sleep(1 * time.Second) - shutdownDelay <- true + close(shutdownDelay) }() <-shutdownDelay @@ -173,8 +203,7 @@ func SubMain(ops chan string, changes chan<- svc.Status) error { func requestShutdown(requester string) { log.Infof("shutdown requested by %v", requester) - shutdown <- true // stops the metrics ticker - shutdown <- true // stops the service change listener + close(shutdown) // stops the metrics ticker, service change listener and notification alert loop } func waitForStopRequest(ops <-chan string) { @@ -551,6 +580,9 @@ func serveIpc(conn net.Conn) { fingerprint := cmd.Payload["Fingerprint"].(string) code := cmd.Payload["Code"].(string) removeMFA(enc, fingerprint, code) + case "UpdateFrequency": + notificationFreq := cmd.Payload["NotificationFrequency"].(float64) + updateNotificationFrequency(enc, int(notificationFreq)) case "Debug": dbg() respond(enc, dto.Response{ @@ -931,16 +963,31 @@ func connectIdentity(id *Id) { if id.CId == nil || !id.CId.Loaded { rts.LoadIdentity(id, DEFAULT_REFRESH_INTERVAL) + rts.BroadcastEvent(dto.IdentityEvent{ + ActionEvent: dto.IDENTITY_ADDED, + Id: id.Identity, + }) } else { log.Debugf("%s[%s] is already loaded", id.Name, id.FingerPrint) id.CId.Services.Range(func(key interface{}, value interface{}) bool { id.Services = append(id.Services, nil) + + val := value.(*cziti.ZService) + var wg sync.WaitGroup + wg.Add(1) + rwg := &cziti.TunnelerActionWaitGroup{ + Wg: &wg, + Czsvc: val, + } + cziti.AddIntercept(rwg) + wg.Wait() + return true }) rts.BroadcastEvent(dto.IdentityEvent{ - ActionEvent: dto.IDENTITY_ADDED, + ActionEvent: dto.IDENTITY_CONNECTED, Id: id.Identity, }) log.Infof("connecting identity completed: %s[%s] %t/%t", id.Name, id.FingerPrint, id.MfaEnabled, id.MfaNeeded) @@ -960,7 +1007,7 @@ func disconnectIdentity(id *Id) error { val := value.(*cziti.ZService) var wg sync.WaitGroup wg.Add(1) - rwg := &cziti.RemoveWG{ + rwg := &cziti.TunnelerActionWaitGroup{ Wg: &wg, Czsvc: val, } @@ -968,6 +1015,10 @@ func disconnectIdentity(id *Id) error { wg.Wait() return true }) + rts.BroadcastEvent(dto.IdentityEvent{ + ActionEvent: dto.IDENTITY_DISCONNECTED, + Id: id.Identity, + }) log.Infof("disconnecting identity complete: %s", id.Name) } } else { @@ -1085,10 +1136,38 @@ func handleBulkServiceChange(sc cziti.BulkServiceChange) { rts.BroadcastEvent(m) } +func addUnit(count int, unit string) (result string) { + if (count == 1) || (count == 0) { + result = strconv.Itoa(count) + " " + unit + " " + } else { + result = strconv.Itoa(count) + " " + unit + "s " + } + return +} + +func secondsToReadableFmt(input int32) (result string) { + seconds := input % (60 * 60 * 24) + hours := math.Floor(float64(seconds) / 60 / 60) + seconds = input % (60 * 60) + minutes := math.Floor(float64(seconds) / 60) + seconds = input % 60 + + if hours > 0 { + result = addUnit(int(hours), "hour") + addUnit(int(minutes), "minute") + addUnit(int(seconds), "second") + } else if minutes > 0 { + result = addUnit(int(minutes), "minute") + addUnit(int(seconds), "second") + } else { + result = addUnit(int(seconds), "second") + } + + return +} + func handleEvents(isInitialized chan struct{}) { events.run() d := 5 * time.Second every5s := time.NewTicker(d) + notificationFrequency = time.NewTicker(time.Duration(rts.state.NotificationFrequency) * time.Minute) defer log.Debugf("exiting handleEvents. loops were set for %v", d) <-isInitialized @@ -1100,14 +1179,68 @@ func handleEvents(isInitialized chan struct{}) { case <-every5s.C: s := rts.ToMetrics() + // broadcast metrics rts.BroadcastEvent(dto.MetricsEvent{ StatusEvent: dto.StatusEvent{Op: "metrics"}, Identities: s.Identities, }) + + // notification message + case <-notificationFrequency.C: + cleanNotifications := make([]cziti.NotificationMessage, 0) + for _, id := range rts.ids { + if id.CId == nil || !id.CId.MfaRefreshNeeded() { + continue + } + notificationMessage := "" + + var notificationMinTimeout int32 = 0 + var notificationMaxTimeout int32 = -1 + switch mfaState := id.CId.GetMFAState(int32((constants.MaximumFrequency+rts.state.NotificationFrequency)*60)); mfaState { + case constants.MfaAllSvcTimeout: + notificationMessage = fmt.Sprintf("All of the services of identity %s are timed out", id.Name) + case constants.MfaFewSvcTimeout: + notificationMessage = fmt.Sprintf("Some of the services of identity %s are timed out", id.Name) + case constants.MfaNearingTimeout: + notificationMinTimeout = id.CId.GetRemainingTime(id.CId.MfaMinTimeout) + notificationMessage = fmt.Sprintf("Some of the services of identity %s are timing out in %s", id.Name, secondsToReadableFmt(notificationMinTimeout)) + default: + // do nothing + } + if len(notificationMessage) > 0 { + + if id.CId.MfaMaxTimeout > -1 { + notificationMaxTimeout = id.CId.GetRemainingTime(id.CId.MfaMaxTimeout) + } + notificationMinTimeout = id.CId.GetRemainingTime(id.CId.MfaMinTimeout) + + cleanNotifications = append(cleanNotifications, cziti.NotificationMessage{ + Fingerprint: id.FingerPrint, + IdentityName: id.Name, + Severity: "major", + MfaMinimumTimeout: notificationMinTimeout, + MfaMaximumTimeout: notificationMaxTimeout, + Message: notificationMessage, + MfaTimeDuration: int(time.Since(id.CId.MfaLastUpdatedTime).Seconds()), + }) + } + } + + if len(cleanNotifications) > 0 { + log.Debugf("Sending notification message to UI %v", cleanNotifications) + rts.BroadcastEvent(cziti.TunnelNotificationEvent{ + Op: "notification", + Notification: cleanNotifications, + }) + } } } } +func ResetFrequency(newFrequency int) { + notificationFrequency.Reset(time.Duration(newFrequency) * time.Minute) +} + //Removes the Config from the provided identity and returns a 'cleaned' id func Clean(src *Id) dto.Identity { mfaNeeded := false @@ -1118,7 +1251,7 @@ func Clean(src *Id) dto.Identity { mfaEnabled = src.CId.MfaEnabled } - log.Tracef("cleaning identity: %s %t %t", src.Name, mfaNeeded, mfaEnabled) + log.Tracef("cleaning identity: %s: mfaNeeded: %t mfaEnabled:%t", src.Name, mfaNeeded, mfaEnabled) AddMetrics(src) nid := dto.Identity{ Name: src.Name, @@ -1138,9 +1271,35 @@ func Clean(src *Id) dto.Identity { src.CId.Services.Range(func(key interface{}, value interface{}) bool { //string, ZService val := value.(*cziti.ZService) + + if src.CId.MfaRefreshNeeded() && val.Service.Timeout > 0 { + var svcTimeout int32 = -1 + for _, pc := range val.Service.PostureChecks { + if svcTimeout == -1 || svcTimeout > int32(pc.Timeout) { + svcTimeout = int32(pc.Timeout) + } + break + } + if (svcTimeout - int32(time.Since(src.CId.MfaLastUpdatedTime).Seconds())) < 0 { + atomic.StoreInt32(&val.Service.Timeout, 0) + } else { + atomic.StoreInt32(&val.Service.Timeout, svcTimeout-int32(time.Since(src.CId.MfaLastUpdatedTime).Seconds())) + } + } + nid.Services = append(nid.Services /*svcToDto(val)*/, val.Service) return true }) + nid.MfaMinTimeout = src.CId.MfaMinTimeout + nid.MfaMaxTimeout = src.CId.MfaMaxTimeout + if !src.CId.MfaRefreshNeeded() { + nid.MfaLastUpdatedTime = time.Now() + } else { + nid.MfaLastUpdatedTime = src.CId.MfaLastUpdatedTime + if (nid.MfaMaxTimeout-int32(time.Since(nid.MfaLastUpdatedTime).Seconds())) < 0 && nid.MfaEnabled { + nid.MfaNeeded = true + } + } } nid.Config.ZtAPI = src.Config.ZtAPI @@ -1163,6 +1322,8 @@ func authMfa(out *json.Encoder, fingerprint string, code string) { id := rts.Find(fingerprint) result := cziti.AuthMFA(id.CId, code) if result == nil { + id.CId.MfaNeeded = false + id.CId.MfaLastUpdatedTime = time.Now() respond(out, dto.Response{Message: "AuthMFA complete", Code: SUCCESS, Error: "", Payload: fingerprint}) } else { respondWithError(out, fmt.Sprintf("AuthMFA failed. the supplied code [%s] was not valid: %s", code, result), 1, result) @@ -1199,3 +1360,18 @@ func sendIdentityAndNotifyUI(enc *json.Encoder, fingerprint string) { resp := dto.Response{Message: "", Code: ERROR, Error: "Could not find id matching fingerprint " + fingerprint, Payload: ""} respond(enc, resp) } + +func updateNotificationFrequency(out *json.Encoder, notificationFreq int) { + + if notificationFreq != rts.state.NotificationFrequency { + err := rts.UpdateNotificationFrequency(notificationFreq) + if err != nil { + respondWithError(out, "Could not set notification frequency", UNKNOWN_ERROR, err) + return + } + + ResetFrequency(notificationFreq) + } + respond(out, dto.Response{Message: "Notification frequency is set", Code: SUCCESS, Error: "", Payload: ""}) + +} diff --git a/service/ziti-tunnel/service/pkg-vars.go b/service/ziti-tunnel/service/pkg-vars.go index fdd49633..c4488fb0 100644 --- a/service/ziti-tunnel/service/pkg-vars.go +++ b/service/ziti-tunnel/service/pkg-vars.go @@ -64,7 +64,7 @@ const ( DEFAULT_REFRESH_INTERVAL = 10 - cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown | svc.AcceptPauseAndContinue | svc.AcceptPowerEvent + cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown | svc.AcceptPauseAndContinue | svc.AcceptPowerEvent | svc.AcceptSessionChange InformationEvent = 0 //1 ContinueEvent = 0 //2 @@ -81,6 +81,10 @@ const ( PBT_APMPOWERSTATUSCHANGE = 10 // Power Status Change PBT_APMRESUMEAUTOMATIC = 18 // Resume Automatic + // windows session changes microsoft docs https://docs.microsoft.com/en-us/windows/win32/termserv/wm-wtssession-change + WTS_SESSION_LOCK = 7 + WTS_SESSION_UNLOCK = 8 + // This is the name you will use for the NET START command SvcStartName = "ziti" diff --git a/service/ziti-tunnel/service/service.go b/service/ziti-tunnel/service/service.go index f9ce3fbe..45d47ca0 100644 --- a/service/ziti-tunnel/service/service.go +++ b/service/ziti-tunnel/service/service.go @@ -41,8 +41,9 @@ func (m *zitiService) Execute(args []string, r <-chan svc.ChangeRequest, changes } control := make(chan string) mainLoop := make(chan struct{}) + winEvents := make(chan WindowsEvents) go func() { - err := SubMain(control, changes) + err := SubMain(control, changes, winEvents) if err != nil { log.Errorf("the main loop exited with an unexpected error: %v", err) } @@ -82,6 +83,7 @@ loop: if c.EventType == PBT_APMRESUMESUSPEND || c.EventType == PBT_APMPOWERSTATUSCHANGE || c.EventType == PBT_APMRESUMEAUTOMATIC { // more than one event may be generated powerEvent = fmt.Sprintf("Power event received - resumed, Event Type - %d", c.EventType) + winEvents <- WindowsEvents{WinPowerEvent: c.EventType, WinSessionEvent: 0} } else if c.EventType == PBT_APMSUSPEND { powerEvent = fmt.Sprintf("Power event received - suspend, Event Type - %d", c.EventType) } else { @@ -89,6 +91,18 @@ loop: } _ = logging.Elog.Info(InformationEvent, powerEvent) log.Infof(powerEvent) + case svc.SessionChange: + var sessionChangedEvent string + if c.EventType == WTS_SESSION_UNLOCK { + sessionChangedEvent = fmt.Sprintf("Windows session changed event received - unlocked device, Event Type %d", c.EventType) + winEvents <- WindowsEvents{WinPowerEvent: 0, WinSessionEvent: c.EventType} + } else if c.EventType == WTS_SESSION_LOCK { + sessionChangedEvent = fmt.Sprintf("Windows session changed event received - locked device, Event Type %d", c.EventType) + } else { + sessionChangedEvent = fmt.Sprintf("Windows session changed unknown event received - Event Type %d", c.EventType) + } + _ = logging.Elog.Info(InformationEvent, sessionChangedEvent) + log.Infof(sessionChangedEvent) default: _ = logging.Elog.Error(1, fmt.Sprintf("unexpected control request #%d", c)) } diff --git a/service/ziti-tunnel/service/state.go b/service/ziti-tunnel/service/state.go index 0157a2c5..d53419b3 100644 --- a/service/ziti-tunnel/service/state.go +++ b/service/ziti-tunnel/service/state.go @@ -117,15 +117,16 @@ func (t *RuntimeState) ToStatus(onlyInitialized bool) dto.TunnelStatus { uptime = tunStart.Milliseconds() clean := dto.TunnelStatus{ - Active: t.state.Active, - Duration: uptime, - Identities: make([]*dto.Identity, 0), - IpInfo: t.state.IpInfo, - LogLevel: t.state.LogLevel, - ServiceVersion: Version, - TunIpv4: t.state.TunIpv4, - TunIpv4Mask: t.state.TunIpv4Mask, - AddDns: t.state.AddDns, + Active: t.state.Active, + Duration: uptime, + Identities: make([]*dto.Identity, 0), + IpInfo: t.state.IpInfo, + LogLevel: t.state.LogLevel, + ServiceVersion: Version, + TunIpv4: t.state.TunIpv4, + TunIpv4Mask: t.state.TunIpv4Mask, + AddDns: t.state.AddDns, + NotificationFrequency: t.state.NotificationFrequency, } i := 0 @@ -154,12 +155,15 @@ func (t *RuntimeState) ToMetrics() dto.TunnelStatus { for _, id := range t.ids { AddMetrics(id) clean.Identities[i] = &dto.Identity{ - Name: id.Name, - FingerPrint: id.FingerPrint, - Metrics: id.Metrics, - Active: id.Active, - MfaEnabled: id.MfaEnabled, - MfaNeeded: id.MfaNeeded, + Name: id.Name, + FingerPrint: id.FingerPrint, + Metrics: id.Metrics, + Active: id.Active, + MfaEnabled: id.MfaEnabled, + MfaNeeded: id.MfaNeeded, + MfaMinTimeout: id.MfaMinTimeout, + MfaMaxTimeout: id.MfaMaxTimeout, + MfaLastUpdatedTime: id.MfaLastUpdatedTime, } i++ } @@ -321,6 +325,10 @@ func (t *RuntimeState) LoadConfig() { log.Warnf("provided mask: [%d] is smaller than the minimum permitted: [%d] and will be changed", rts.state.TunIpv4Mask, constants.Ipv4MinMask) rts.UpdateIpv4Mask(constants.Ipv4MinMask) } + + if t.state.NotificationFrequency < constants.MinimumFrequency { + rts.UpdateNotificationFrequency(constants.MinimumFrequency) + } } func (t *RuntimeState) scanForOrphanedIdentities(folder string) { @@ -544,3 +552,18 @@ func (t *RuntimeState) UpdateMfa(fingerprint string, mfaEnabled bool, mfaNeeded id.MfaNeeded = mfaNeeded } } + +func (t *RuntimeState) UpdateNotificationFrequency(notificationFreq int) error { + + log.Infof("setting notification frequency : %d", notificationFreq) + + if notificationFreq < constants.MinimumFrequency || notificationFreq > constants.MaximumFrequency { + return errors.New(fmt.Sprintf("Notification frequency should be between %d and %d minutes", constants.MinimumFrequency, constants.MaximumFrequency)) + } + + rts.state.NotificationFrequency = notificationFreq + + rts.SaveState() + + return nil +} diff --git a/service/ziti-tunnel/service/types.go b/service/ziti-tunnel/service/types.go index 67ec1495..9a6e2b0a 100644 --- a/service/ziti-tunnel/service/types.go +++ b/service/ziti-tunnel/service/types.go @@ -9,3 +9,8 @@ type Id struct { dto.Identity CId *cziti.ZIdentity } + +type WindowsEvents struct { + WinPowerEvent uint32 + WinSessionEvent uint32 +} diff --git a/service/ziti-tunnel/version.go b/service/ziti-tunnel/version.go index 52a10724..a3919eb2 100644 --- a/service/ziti-tunnel/version.go +++ b/service/ziti-tunnel/version.go @@ -20,9 +20,9 @@ package main const ( - Version = "1.9.9" - Revision = "b6a4ad327bd0" - Branch = "main" + Version = "1.10.0" + Revision = "efac1fb0a5f2" + Branch = "release-next" BuildUser = "" - BuildDate = "2021-06-24 10:44:17" + BuildDate = "2021-07-22 15:53:14" )