From 057586f4b43b920eb22eef6f056581603cab0398 Mon Sep 17 00:00:00 2001 From: dovholuknf <46322585+dovholuknf@users.noreply.github.com> Date: Wed, 7 Oct 2020 18:07:39 -0400 Subject: [PATCH 01/18] fixes #116 - remove id/key from config file when persisting --- service/ziti-tunnel/service/state.go | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/service/ziti-tunnel/service/state.go b/service/ziti-tunnel/service/state.go index 3d0e27aa..1d5d1025 100644 --- a/service/ziti-tunnel/service/state.go +++ b/service/ziti-tunnel/service/state.go @@ -77,7 +77,7 @@ func (t *RuntimeState) SaveState() { w := bufio.NewWriter(bufio.NewWriter(cfg)) enc := json.NewEncoder(w) enc.SetIndent("", " ") - _ = enc.Encode(t.state) + _ = enc.Encode(t.ToStatus()) _ = w.Flush() err = cfg.Close() @@ -100,10 +100,13 @@ func (t *RuntimeState) ToStatus() dto.TunnelStatus { } clean := dto.TunnelStatus{ - Active: t.state.Active, - Duration: uptime, - Identities: make([]*dto.Identity, idCount), - IpInfo: t.state.IpInfo, + Active: t.state.Active, + Duration: uptime, + Identities: make([]*dto.Identity, idCount), + IpInfo: t.state.IpInfo, + LogLevel: t.state.LogLevel, + TunIpv4: t.state.TunIpv4, + TunIpv4Mask: t.state.TunIpv4Mask, } for i, id := range t.state.Identities { From ff39c945d167d764ecdbe721ac5ffdc6328909d5 Mon Sep 17 00:00:00 2001 From: dovholuknf <46322585+dovholuknf@users.noreply.github.com> Date: Wed, 7 Oct 2020 18:09:41 -0400 Subject: [PATCH 02/18] update release notes for service and bump version --- release-notes-service.md | 10 ++++++++++ version | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/release-notes-service.md b/release-notes-service.md index 1ed783a7..7589bc28 100644 --- a/release-notes-service.md +++ b/release-notes-service.md @@ -1,6 +1,16 @@ These release notes will catalog the changes in the Windows service. For UI changes or installer changes see [release-notes.md]() +# Release 0.0.30 + +* What's New + + * Nothing new + +* Bug fixes + + * [#116](https://github.com/openziti/desktop-edge-win/issues/116) - Removes information from the config that wasn't needed in config.json + # Release 0.0.29 * What's New diff --git a/version b/version index 369bd4c2..f092e2be 100644 --- a/version +++ b/version @@ -1 +1 @@ -0.0.29 +0.0.30 From 3fbe2378e0044c287a6be4259b5ffc3eb31bf3ef Mon Sep 17 00:00:00 2001 From: dovholuknf <46322585+dovholuknf@users.noreply.github.com> Date: Wed, 7 Oct 2020 19:20:53 -0400 Subject: [PATCH 03/18] update UI service client to allow for SetLogLevel --- DesktopEdge/ServiceClient/ClientImpl.cs | 28 +++++++++++++++++++++ DesktopEdge/ServiceClient/DataStructures.cs | 17 +++++++++++++ 2 files changed, 45 insertions(+) diff --git a/DesktopEdge/ServiceClient/ClientImpl.cs b/DesktopEdge/ServiceClient/ClientImpl.cs index a656335c..901c7078 100644 --- a/DesktopEdge/ServiceClient/ClientImpl.cs +++ b/DesktopEdge/ServiceClient/ClientImpl.cs @@ -24,8 +24,20 @@ using System.Windows.Documents; /// namespace ZitiDesktopEdge.ServiceClient { + public enum LogLevelEnum + { + FATAL = 0, + ERROR = 1, + WARN = 2, + INFO = 3, + DEBUG = 4, + TRACE = 5, + VERBOSE = 6, + } + internal class Client { + public const int EXPECTED_API_VERSION = 1; public event EventHandler OnTunnelStatusEvent; @@ -378,6 +390,22 @@ namespace ZitiDesktopEdge.ServiceClient } } + public void SetLogLevel(LogLevelEnum level) + { + try + { + send(new SetLogLevelFunction(Enum.GetName(level.GetType(), level))); + SvcResponse resp = read(ipcReader); + return; + } + catch (IOException ioe) + { + //almost certainly a problem with the pipe - recreate the pipe... + //setupPipe(); + throw ioe; + } + } + public Identity IdentityOnOff(string fingerprint, bool onOff) { try diff --git a/DesktopEdge/ServiceClient/DataStructures.cs b/DesktopEdge/ServiceClient/DataStructures.cs index 2487e0a6..b49e47f8 100644 --- a/DesktopEdge/ServiceClient/DataStructures.cs +++ b/DesktopEdge/ServiceClient/DataStructures.cs @@ -73,6 +73,10 @@ namespace ZitiDesktopEdge.ServiceClient public bool OnOff { get; set; } public string Fingerprint { get; set; } } + public class SetLogLevelPayload + { + public string Level { get; set; } + } public class IdentityToggleFunction : ServiceFunction { @@ -88,6 +92,19 @@ namespace ZitiDesktopEdge.ServiceClient public IdentityTogglePayload Payload { get; set; } } + public class SetLogLevelFunction : ServiceFunction + { + public SetLogLevelFunction(string level) + { + this.Function = "SetLogLevel"; + this.Payload = new SetLogLevelPayload() + { + Level = level + }; + } + public SetLogLevelPayload Payload { get; set; } + } + public class FingerprintPayload { public string Fingerprint { get; set; } From aac8ba9f5bf061c44a0ad344f85a8ac1b8710854 Mon Sep 17 00:00:00 2001 From: dovholuknf <46322585+dovholuknf@users.noreply.github.com> Date: Wed, 7 Oct 2020 19:22:40 -0400 Subject: [PATCH 04/18] resolves #120 - set log level dynamically --- service/cziti/sdk.go | 13 ++++++++++- service/ziti-tunnel/globals/loghelper.go | 29 +++++++++++------------- service/ziti-tunnel/main.go | 3 +-- service/ziti-tunnel/service/ipc.go | 11 +++++++-- 4 files changed, 35 insertions(+), 21 deletions(-) diff --git a/service/cziti/sdk.go b/service/cziti/sdk.go index ec27966a..0de185ee 100644 --- a/service/cziti/sdk.go +++ b/service/cziti/sdk.go @@ -178,9 +178,20 @@ func serviceCB(ziti_ctx C.ziti_context, service *C.ziti_service, status C.int, t ZitiContext: ctx, } } else { - log.Warnf("could not find a service with id: %s, name: %s", service.id, service.name) + log.Warnf("could not remove service? service not found with id: %s, name: %s in context %d", service.id, service.name, &ctx) } } else if status == C.ZITI_OK { + //first thing's first - determine if the service is already in this runtime + //if it is that means this is 'probably' a config change. to make it easy + //just dereg/disconnect the service and then let the rest of this code execute + found, ok := ctx.Services.Load(svcId) + if ok && found != nil { + log.Infof("service with id: %s, name: %s exists. updating service.", service.id, service.name) + DNS.DeregisterService(ctx, name) + ctx.Services.Delete(svcId) + } else { + log.Debugf("service not found with id: %s, name: %s in context %d", service.id, service.name, &ctx) + } if C.ZITI_CAN_BIND == ( service.perm_flags & C.ZITI_CAN_BIND ) { var v1Config C.ziti_server_cfg_v1 diff --git a/service/ziti-tunnel/globals/loghelper.go b/service/ziti-tunnel/globals/loghelper.go index de47197b..db193ff8 100644 --- a/service/ziti-tunnel/globals/loghelper.go +++ b/service/ziti-tunnel/globals/loghelper.go @@ -17,12 +17,13 @@ package globals +import "C" import ( rotatelogs "github.com/lestrrat-go/file-rotatelogs" "github.com/michaelquigley/pfxlog" + "github.com/openziti/desktop-edge-win/service/cziti" "github.com/sirupsen/logrus" "golang.org/x/sys/windows/svc/debug" - "golang.org/x/sys/windows/svc/eventlog" "io" "os" "strings" @@ -43,8 +44,9 @@ func Logger() *logrus.Entry { var loggerInitialized = false -func InitLogger(logLevel logrus.Level) { - logrus.SetLevel(logLevel) +func InitLogger(level string) { + l, _ := ParseLevel(level) + logrus.SetLevel(l) rl, _ := rotatelogs.New(config.LogFile() + ".%Y%m%d%H%M.log", rotatelogs.WithRotationTime(24 * time.Hour), @@ -65,6 +67,13 @@ func InitLogger(logLevel logrus.Level) { } } +func SetLogLevel(level string){ + logrus.Infof("Setting logger levels to %s", level) + l, cLevel := ParseLevel(level) + logrus.SetLevel(l) + cziti.SetLogLevel(cLevel) +} + func ParseLevel(lvl string) (logrus.Level, int) { switch strings.ToLower(lvl) { case "panic": @@ -87,16 +96,4 @@ func ParseLevel(lvl string) (logrus.Level, int) { logrus.Warnf("level not recognized: [%s]. Using Info", lvl) return logrus.InfoLevel, 3 } -} - -func InitEventLog(svcName string, interactive bool) { - var err error - if !interactive { - Elog = debug.New(svcName) - } else { - Elog, err = eventlog.Open(svcName) - if err != nil { - return - } - } -} +} \ No newline at end of file diff --git a/service/ziti-tunnel/main.go b/service/ziti-tunnel/main.go index d1a4c2ec..375c4c79 100644 --- a/service/ziti-tunnel/main.go +++ b/service/ziti-tunnel/main.go @@ -33,8 +33,7 @@ import ( var log = globals.Logger() func main() { - l, _ := globals.ParseLevel("info") - globals.InitLogger(l) + globals.InitLogger("info") //if len(os.Args) < 2 { // // if no args supplied - expect to run as service diff --git a/service/ziti-tunnel/service/ipc.go b/service/ziti-tunnel/service/ipc.go index 3a1b3f4a..5bb4217a 100644 --- a/service/ziti-tunnel/service/ipc.go +++ b/service/ziti-tunnel/service/ipc.go @@ -63,8 +63,8 @@ func SubMain(ops chan string, changes chan<- svc.Status) error { rts.LoadConfig() l := rts.state.LogLevel - logLevel, czitiLevel := globals.ParseLevel(l) - globals.InitLogger(logLevel) + _, czitiLevel := globals.ParseLevel(l) + globals.InitLogger(l) _ = globals.Elog.Info(InformationEvent, SvcName+" starting. log file located at "+config.LogFile()) @@ -406,6 +406,8 @@ func serveIpc(conn net.Conn) { onOff := cmd.Payload["OnOff"].(bool) fingerprint := cmd.Payload["Fingerprint"].(string) toggleIdentity(enc, fingerprint, onOff) + case "SetLogLevel": + setLogLevel(enc, cmd.Payload["Level"].(string)) case "Debug": dbg() respond(enc, dto.Response{ @@ -426,6 +428,11 @@ func serveIpc(conn net.Conn) { } } +func setLogLevel(out *json.Encoder, level string) { + globals.SetLogLevel(level) + respond(out, dto.Response{Message: "log level set", Code: SUCCESS, Error: "", Payload: nil}) +} + func serveLogs(conn net.Conn) { log.Debug("accepted a logs connection, writing logs to pipe") w := bufio.NewWriter(conn) From 4a44f793b26dab8e44ccefa91cb53422c5631173 Mon Sep 17 00:00:00 2001 From: dovholuknf <46322585+dovholuknf@users.noreply.github.com> Date: Wed, 7 Oct 2020 19:25:09 -0400 Subject: [PATCH 05/18] update readme for services --- release-notes-service.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/release-notes-service.md b/release-notes-service.md index 7589bc28..cbae505c 100644 --- a/release-notes-service.md +++ b/release-notes-service.md @@ -5,7 +5,7 @@ For UI changes or installer changes see [release-notes.md]() * What's New - * Nothing new + * [#120](https://github.com/openziti/desktop-edge-win/issues/120) - Allow UI/client to set log level dynamically via ipc * Bug fixes From 329e8acb7ed7b69ff746780f4c0c341e3d0d6d44 Mon Sep 17 00:00:00 2001 From: dovholuknf <46322585+dovholuknf@users.noreply.github.com> Date: Wed, 7 Oct 2020 19:30:15 -0400 Subject: [PATCH 06/18] fix indentation of ease-notes-service.md --- release-notes-service.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/release-notes-service.md b/release-notes-service.md index cbae505c..e2d65965 100644 --- a/release-notes-service.md +++ b/release-notes-service.md @@ -5,7 +5,7 @@ For UI changes or installer changes see [release-notes.md]() * What's New - * [#120](https://github.com/openziti/desktop-edge-win/issues/120) - Allow UI/client to set log level dynamically via ipc + * [#120](https://github.com/openziti/desktop-edge-win/issues/120) - Allow UI/client to set log level dynamically via ipc * Bug fixes @@ -15,8 +15,8 @@ For UI changes or installer changes see [release-notes.md]() * What's New - * Continually improved logging - * Add support for 'verbose' logging along with error, warn, info, debug, trace + * Continually improved logging + * Add support for 'verbose' logging along with error, warn, info, debug, trace * Bug fixes @@ -27,8 +27,8 @@ For UI changes or installer changes see [release-notes.md]() * What's New - * Continually improved logging - * Better DNS removal when services are no longer available or when an identity is removed + * Continually improved logging + * Better DNS removal when services are no longer available or when an identity is removed * Bug fixes From ab601afc2f27c7b689ecf95283bdc08b509e793e Mon Sep 17 00:00:00 2001 From: dovholuknf <46322585+dovholuknf@users.noreply.github.com> Date: Thu, 8 Oct 2020 08:45:03 -0400 Subject: [PATCH 07/18] refine how log level is set to facilitate returning to UI/client --- service/ziti-tunnel/globals/loghelper.go | 8 +++----- service/ziti-tunnel/service/ipc.go | 5 ++++- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/service/ziti-tunnel/globals/loghelper.go b/service/ziti-tunnel/globals/loghelper.go index db193ff8..3259555a 100644 --- a/service/ziti-tunnel/globals/loghelper.go +++ b/service/ziti-tunnel/globals/loghelper.go @@ -17,7 +17,6 @@ package globals -import "C" import ( rotatelogs "github.com/lestrrat-go/file-rotatelogs" "github.com/michaelquigley/pfxlog" @@ -67,10 +66,9 @@ func InitLogger(level string) { } } -func SetLogLevel(level string){ - logrus.Infof("Setting logger levels to %s", level) - l, cLevel := ParseLevel(level) - logrus.SetLevel(l) +func SetLogLevel(goLevel logrus.Level, cLevel int) { + logrus.Infof("Setting logger levels to %s", goLevel) + logrus.SetLevel(goLevel) cziti.SetLogLevel(cLevel) } diff --git a/service/ziti-tunnel/service/ipc.go b/service/ziti-tunnel/service/ipc.go index 5bb4217a..96b39d0c 100644 --- a/service/ziti-tunnel/service/ipc.go +++ b/service/ziti-tunnel/service/ipc.go @@ -64,6 +64,7 @@ func SubMain(ops chan string, changes chan<- svc.Status) error { rts.LoadConfig() l := rts.state.LogLevel _, czitiLevel := globals.ParseLevel(l) + globals.InitLogger(l) _ = globals.Elog.Info(InformationEvent, SvcName+" starting. log file located at "+config.LogFile()) @@ -429,7 +430,9 @@ func serveIpc(conn net.Conn) { } func setLogLevel(out *json.Encoder, level string) { - globals.SetLogLevel(level) + goLevel, cLevel := globals.ParseLevel(level) + globals.SetLogLevel(goLevel, cLevel) + rts.state.LogLevel = goLevel.String() respond(out, dto.Response{Message: "log level set", Code: SUCCESS, Error: "", Payload: nil}) } From b939c1c180c447fb65a395fc022f7c6c9f01f08e Mon Sep 17 00:00:00 2001 From: dovholuknf <46322585+dovholuknf@users.noreply.github.com> Date: Thu, 8 Oct 2020 08:46:26 -0400 Subject: [PATCH 08/18] add ip version into the function to help with log reading/diagnostics --- service/cziti/dns-proxy.go | 51 ++++++++++++++++++++++---------------- service/cziti/sdk.go | 2 +- 2 files changed, 30 insertions(+), 23 deletions(-) diff --git a/service/cziti/dns-proxy.go b/service/cziti/dns-proxy.go index 03a3f557..b28b79ea 100644 --- a/service/cziti/dns-proxy.go +++ b/service/cziti/dns-proxy.go @@ -33,7 +33,7 @@ var reqch = make(chan dnsreq, 64) var proxiedRequests = make(chan *proxiedReq, 64) var respChan = make(chan []byte, 64) -func processDNSquery(packet []byte, p *net.UDPAddr, s *net.UDPConn) { +func processDNSquery(packet []byte, p *net.UDPAddr, s *net.UDPConn, ipVer int) { q := &dns.Msg{} if err := q.Unpack(packet); err != nil { log.Errorf("ERROR", err) @@ -101,14 +101,15 @@ func processDNSquery(packet []byte, p *net.UDPAddr, s *net.UDPConn) { } } else { // log.Debug("proxying ", dns.Type(query.Qtype), query.Name, q.Id, " for ", p) - proxyDNS(q, p, s) + proxyDNS(q, p, s, ipVer) } } type dnsreq struct { - q []byte - s *net.UDPConn - p *net.UDPAddr + q []byte + s *net.UDPConn + p *net.UDPAddr + ifId int } func RunDNSserver(dnsBind []net.IP, ready chan bool) { @@ -124,7 +125,7 @@ func RunDNSserver(dnsBind []net.IP, ready chan bool) { ready <- true for req := range reqch { - processDNSquery(req.q, req.p, req.s) + processDNSquery(req.q, req.p, req.s, req.ifId) } } @@ -136,8 +137,10 @@ func runListener(ip net.IP, port int, reqch chan dnsreq) { Zone: "", } + id := 6 network := "udp6" if len(ip.To4()) == net.IPv4len { + id = 4 network = "udp4" } @@ -161,27 +164,30 @@ func runListener(ip net.IP, port int, reqch chan dnsreq) { } reqch <- dnsreq{ - q: b[:nb], - s: server, - p: peer, + q: b[:nb], + s: server, + p: peer, + ifId: id, } } } /*******************************************************************/ type proxiedReq struct { - req *dns.Msg - peer *net.UDPAddr - s *net.UDPConn - exp time.Time + req *dns.Msg + peer *net.UDPAddr + s *net.UDPConn + exp time.Time + ipVer int } -func proxyDNS(req *dns.Msg, peer *net.UDPAddr, serv *net.UDPConn) { +func proxyDNS(req *dns.Msg, peer *net.UDPAddr, serv *net.UDPConn, ipVer int) { proxiedRequests <- &proxiedReq{ - req: req, - peer: peer, - s: serv, - exp: time.Now().Add(30 * time.Second), + req: req, + peer: peer, + s: serv, + exp: time.Now().Add(30 * time.Second), + ipVer: ipVer, } } @@ -266,7 +272,7 @@ func runDNSproxy(dnsServers []string) { proxy.RemoteAddr(), err) } else { - log.Tracef("Proxied request sent to %v from %v", proxy.RemoteAddr(), proxy.LocalAddr()) + log.Tracef("Proxied request sent to %v from ipv%d listener", proxy.RemoteAddr(), pr.ipVer) } } @@ -277,11 +283,12 @@ func runDNSproxy(dnsServers []string) { req, found := reqs[id] if found { delete(reqs, id) - log.Tracef("proxy resolved %v from %v", reply.Question[0].Name, req.s.RemoteAddr()) + log.Tracef("proxy resolved request for %v id:%d", reply.Question[0].Name, reply.Id) req.s.WriteMsgUDP(rep, nil, req.peer) } else { - log.Tracef("matching request was not found for %s %s", - dns.Type(reply.Question[0].Qtype), reply.Question[0].Name) + // keep this log but leave commented out. When two listeners are enabled (ipv4/v6) this msg will + // just mean some other request was processed successfully and removed the entry from the map + //log.Tracef("matching request was not found for id:%d. %s %s", reply.Id, dns.Type(reply.Question[0].Qtype), reply.Question[0].Name) } } case <-time.After(time.Minute): diff --git a/service/cziti/sdk.go b/service/cziti/sdk.go index 0de185ee..28b33bf5 100644 --- a/service/cziti/sdk.go +++ b/service/cziti/sdk.go @@ -190,7 +190,7 @@ func serviceCB(ziti_ctx C.ziti_context, service *C.ziti_service, status C.int, t DNS.DeregisterService(ctx, name) ctx.Services.Delete(svcId) } else { - log.Debugf("service not found with id: %s, name: %s in context %d", service.id, service.name, &ctx) + log.Debugf("service not found with id: %s, name: %s in context %d", svcId, name, &ctx) } if C.ZITI_CAN_BIND == ( service.perm_flags & C.ZITI_CAN_BIND ) { From bb5590aeb9ed090251c38382f01b487bbf53cacb Mon Sep 17 00:00:00 2001 From: dovholuknf <46322585+dovholuknf@users.noreply.github.com> Date: Thu, 8 Oct 2020 08:47:52 -0400 Subject: [PATCH 09/18] ensure UI can get the current log level of the service so the user can be shown what it's at currently add/refine debug output --- DesktopEdge/MainWindow.xaml.cs | 28 +++++++++++++-------- DesktopEdge/ServiceClient/DataStructures.cs | 24 +++++++++++++++--- 2 files changed, 39 insertions(+), 13 deletions(-) diff --git a/DesktopEdge/MainWindow.xaml.cs b/DesktopEdge/MainWindow.xaml.cs index a69a9ae5..e92300d0 100644 --- a/DesktopEdge/MainWindow.xaml.cs +++ b/DesktopEdge/MainWindow.xaml.cs @@ -4,21 +4,11 @@ using System.Windows; using System.Windows.Input; using System.IO; using ZitiDesktopEdge.Models; -using System.IO.Compression; - using ZitiDesktopEdge.ServiceClient; using System.ServiceProcess; using System.Linq; -using System.Runtime.Remoting.Messaging; using System.Diagnostics; -using System.Security.Principal; -using System.Net; using System.Windows.Controls; -using System.Threading.Tasks; -using System.Threading; -using System.Text; -using System.Runtime.InteropServices; -using System.Windows.Interop; namespace ZitiDesktopEdge { @@ -250,6 +240,7 @@ namespace ZitiDesktopEdge { private void ServiceClient_OnTunnelStatusEvent(object sender, TunnelStatusEvent e) { if (e == null) return; //just skip it for now... Debug.WriteLine($"==== TunnelStatusEvent: "); + e.Status.Dump(Console.Out); this.Dispatcher.Invoke(() => { if(e.ApiVersion != Client.EXPECTED_API_VERSION) { SetCantDisplay("Version mismatch!", "The version of the Service is not compatible"); @@ -543,5 +534,22 @@ namespace ZitiDesktopEdge { private void Label_MouseDoubleClick(object sender, MouseButtonEventArgs e) { Placement(); } + + private void Button_Click(object sender, RoutedEventArgs e) + { + serviceClient.SetLogLevel(NextLevel()); + } + + int cur = 0; + LogLevelEnum[] levels = new LogLevelEnum[] { LogLevelEnum.FATAL, LogLevelEnum.ERROR, LogLevelEnum.WARN, LogLevelEnum.INFO, LogLevelEnum.DEBUG, LogLevelEnum.TRACE, LogLevelEnum.VERBOSE }; + public LogLevelEnum NextLevel() + { + cur++; + if (cur > 6) + { + cur = 0; + } + return levels[cur]; + } } } diff --git a/DesktopEdge/ServiceClient/DataStructures.cs b/DesktopEdge/ServiceClient/DataStructures.cs index b49e47f8..a3329785 100644 --- a/DesktopEdge/ServiceClient/DataStructures.cs +++ b/DesktopEdge/ServiceClient/DataStructures.cs @@ -180,15 +180,19 @@ namespace ZitiDesktopEdge.ServiceClient public IpInfo IpInfo { get; set; } + public string LogLevel { get; set; } + public void Dump(System.IO.TextWriter writer) { writer.WriteLine($"Tunnel Active: {Active}"); + writer.WriteLine($" LogLevel : {LogLevel}"); + writer.WriteLine($" EvaluatedLogLevel: {EvaluateLogLevel()}"); foreach (Identity id in Identities) { writer.WriteLine($" FingerPrint: {id.FingerPrint}"); - writer.WriteLine($" Name: {id.Name}"); - writer.WriteLine($" Active: {id.Active}"); - writer.WriteLine($" Status: {id.Status}"); + writer.WriteLine($" Name : {id.Name}"); + writer.WriteLine($" Active : {id.Active}"); + writer.WriteLine($" Status : {id.Status}"); writer.WriteLine($" Services:"); foreach (Service s in id.Services) { @@ -197,6 +201,20 @@ namespace ZitiDesktopEdge.ServiceClient writer.WriteLine("============================================="); } } + + public LogLevelEnum EvaluateLogLevel() + { + try + { + LogLevelEnum l = (LogLevelEnum) Enum.Parse(typeof(LogLevelEnum), LogLevel.ToUpper()); + return l; + } + catch + { + System.Diagnostics.Debug.WriteLine("Could not parse response LogLevel from sevice - guessing INFO? " + LogLevel); + return LogLevelEnum.INFO; + } + } } public class ServiceException : System.Exception From 2fd6dcde7910d5d32ba6fb050c0301e908d0f4ab Mon Sep 17 00:00:00 2001 From: dovholuknf <46322585+dovholuknf@users.noreply.github.com> Date: Thu, 8 Oct 2020 09:05:36 -0400 Subject: [PATCH 10/18] update release notes for service --- release-notes-service.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/release-notes-service.md b/release-notes-service.md index e2d65965..fa84513f 100644 --- a/release-notes-service.md +++ b/release-notes-service.md @@ -4,8 +4,9 @@ For UI changes or installer changes see [release-notes.md]() # Release 0.0.30 * What's New - - * [#120](https://github.com/openziti/desktop-edge-win/issues/120) - Allow UI/client to set log level dynamically via ipc + + * Continually improved logging + * [#120](https://github.com/openziti/desktop-edge-win/issues/120) - Allow UI/client to get and set log level dynamically via ipc * Bug fixes From 4d908d535c8b79e769746729ff3719e8d6a92aa2 Mon Sep 17 00:00:00 2001 From: dovholuknf <46322585+dovholuknf@users.noreply.github.com> Date: Thu, 8 Oct 2020 20:42:51 -0400 Subject: [PATCH 11/18] add version data to TunnelStatus for use in the UI --- DesktopEdge/ServiceClient/DataStructures.cs | 17 ++++++++++++- service/cziti/dns-proxy.go | 27 ++++++++++++--------- service/ziti-tunnel/dto/dtos.go | 21 ++++++++++------ service/ziti-tunnel/main.go | 6 +++++ service/ziti-tunnel/service/pkg-vars.go | 2 +- service/ziti-tunnel/service/service.go | 1 - service/ziti-tunnel/service/state.go | 15 ++++++------ 7 files changed, 60 insertions(+), 29 deletions(-) diff --git a/DesktopEdge/ServiceClient/DataStructures.cs b/DesktopEdge/ServiceClient/DataStructures.cs index a3329785..cc464fde 100644 --- a/DesktopEdge/ServiceClient/DataStructures.cs +++ b/DesktopEdge/ServiceClient/DataStructures.cs @@ -165,6 +165,18 @@ namespace ZitiDesktopEdge.ServiceClient public string DNS { get; set; } } + class ServiceVersion + { + public string Version { get; set; } + public string Revision { get; set; } + public string BuildDate { get; set; } + + public override string ToString() + { + return $"Version: {Version}, Revision: {Revision}, BuildDate: {BuildDate}"; + } + } + class ZitiTunnelStatus : SvcResponse { public TunnelStatus Status { get; set; } @@ -182,9 +194,12 @@ namespace ZitiDesktopEdge.ServiceClient public string LogLevel { get; set; } + public ServiceVersion ServiceVersion { get; set; } + public void Dump(System.IO.TextWriter writer) { writer.WriteLine($"Tunnel Active: {Active}"); + writer.WriteLine($" ServiceVersion : {ServiceVersion}"); writer.WriteLine($" LogLevel : {LogLevel}"); writer.WriteLine($" EvaluatedLogLevel: {EvaluateLogLevel()}"); foreach (Identity id in Identities) @@ -194,7 +209,7 @@ namespace ZitiDesktopEdge.ServiceClient writer.WriteLine($" Active : {id.Active}"); writer.WriteLine($" Status : {id.Status}"); writer.WriteLine($" Services:"); - foreach (Service s in id.Services) + foreach (Service s in id?.Services) { writer.WriteLine($" Name: {s.Name} HostName: {s.InterceptHost} Port: {s.InterceptPort}"); } diff --git a/service/cziti/dns-proxy.go b/service/cziti/dns-proxy.go index b28b79ea..c081e2e7 100644 --- a/service/cziti/dns-proxy.go +++ b/service/cziti/dns-proxy.go @@ -28,10 +28,10 @@ import ( ) var domains []string // get any connection-specific local domains - -var reqch = make(chan dnsreq, 64) -var proxiedRequests = make(chan *proxiedReq, 64) -var respChan = make(chan []byte, 64) +const MaxDnsRequests = 64 +var reqch = make(chan dnsreq, MaxDnsRequests) +var proxiedRequests = make(chan *proxiedReq, MaxDnsRequests) +var respChan = make(chan []byte, MaxDnsRequests) func processDNSquery(packet []byte, p *net.UDPAddr, s *net.UDPConn, ipVer int) { q := &dns.Msg{} @@ -155,10 +155,10 @@ func runListener(ip net.IP, port int, reqch chan dnsreq) { if err != nil { if err == io.EOF || err == os.ErrClosed || strings.HasSuffix(err.Error(), "use of closed network connection") { log.Warnf("DNS listener closing.") - server.Close() + _ = server.Close() break } else { - server.Close() + _ = server.Close() panic(err) } } @@ -230,14 +230,14 @@ func runDNSproxy(dnsServers []string) { } for _, proxy := range dnsUpstreams { - go func() { + go func(p *net.UDPConn) { resp := make([]byte, 1024) //make a buffer which is reused for { - n, err := proxy.Read(resp) + n, err := p.Read(resp) if err != nil { // something is wrong with the DNS connection panic and let DNS recovery kick in if err == io.EOF || err == os.ErrClosed || strings.HasSuffix(err.Error(), "use of closed network connection") { - log.Errorf("error receiving from ip: %v. error %v", proxy.RemoteAddr(), err) + log.Errorf("error receiving from ip: %v. error %v", p.RemoteAddr(), err) return } else { log.Warnf("odd error: %v", err) @@ -246,7 +246,7 @@ func runDNSproxy(dnsServers []string) { respChan <- resp[:n] } } - }() + }(proxy) } reqs := make(map[uint32]*proxiedReq) @@ -261,7 +261,7 @@ func runDNSproxy(dnsServers []string) { if _, err := proxy.Write(b); err != nil { //TODO: if this happens -does this mean the next dns server is not available? - proxy.Close() //first thing - close the proxy connection + _ = proxy.Close() //first thing - close the proxy connection // when this hits - it never seems to recover. throw a panic which will get recovered // via the defer specified above and try to recreate the connections. this is a heavy @@ -284,7 +284,10 @@ func runDNSproxy(dnsServers []string) { if found { delete(reqs, id) log.Tracef("proxy resolved request for %v id:%d", reply.Question[0].Name, reply.Id) - req.s.WriteMsgUDP(rep, nil, req.peer) + n, oobn, err := req.s.WriteMsgUDP(rep, nil, req.peer) + if err != nil { + log.Errorf("an error has occurred while trying to write a udp message. n:%d, oobn:%d, err:%v", n, oobn, err) + } } else { // keep this log but leave commented out. When two listeners are enabled (ipv4/v6) this msg will // just mean some other request was processed successfully and removed the entry from the map diff --git a/service/ziti-tunnel/dto/dtos.go b/service/ziti-tunnel/dto/dtos.go index 13a724e7..d543a434 100644 --- a/service/ziti-tunnel/dto/dtos.go +++ b/service/ziti-tunnel/dto/dtos.go @@ -83,13 +83,20 @@ func (id *Identity) Path() string { } type TunnelStatus struct { - Active bool - Duration int64 - Identities []*Identity - IpInfo *TunIpInfo `json:"IpInfo,omitempty"` - LogLevel string - TunIpv4 string - TunIpv4Mask int + Active bool + Duration int64 + Identities []*Identity + IpInfo *TunIpInfo `json:"IpInfo,omitempty"` + LogLevel string + ServiceVersion ServiceVersion + TunIpv4 string + TunIpv4Mask int +} + +type ServiceVersion struct { + Version string + Revision string + BuildDate string } type ZitiTunnelStatus struct { diff --git a/service/ziti-tunnel/main.go b/service/ziti-tunnel/main.go index 375c4c79..1f868bdc 100644 --- a/service/ziti-tunnel/main.go +++ b/service/ziti-tunnel/main.go @@ -20,6 +20,7 @@ package main import ( "fmt" + "github.com/openziti/desktop-edge-win/service/ziti-tunnel/dto" "golang.org/x/sys/windows/svc/debug" "golang.org/x/sys/windows/svc/eventlog" "os" @@ -33,6 +34,11 @@ import ( var log = globals.Logger() func main() { + service.Version = dto.ServiceVersion{ + Version: Version, + Revision: Revision, + BuildDate: BuildDate, + } globals.InitLogger("info") //if len(os.Args) < 2 { diff --git a/service/ziti-tunnel/service/pkg-vars.go b/service/ziti-tunnel/service/pkg-vars.go index 564bada3..e757f2cc 100644 --- a/service/ziti-tunnel/service/pkg-vars.go +++ b/service/ziti-tunnel/service/pkg-vars.go @@ -24,7 +24,7 @@ import ( "github.com/openziti/desktop-edge-win/service/ziti-tunnel/dto" "github.com/openziti/desktop-edge-win/service/ziti-tunnel/globals" ) - +var Version dto.ServiceVersion var pipeBase = `\\.\pipe\NetFoundry\tunneler\` var activeIds = make(map[string]*dto.Identity) diff --git a/service/ziti-tunnel/service/service.go b/service/ziti-tunnel/service/service.go index 5583f3a6..97b5e2a6 100644 --- a/service/ziti-tunnel/service/service.go +++ b/service/ziti-tunnel/service/service.go @@ -26,7 +26,6 @@ import ( "github.com/openziti/desktop-edge-win/service/ziti-tunnel/globals" ) - type zitiService struct{} func (m *zitiService) Execute(args []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (ssec bool, errno uint32) { diff --git a/service/ziti-tunnel/service/state.go b/service/ziti-tunnel/service/state.go index 1d5d1025..5956a3ec 100644 --- a/service/ziti-tunnel/service/state.go +++ b/service/ziti-tunnel/service/state.go @@ -100,13 +100,14 @@ func (t *RuntimeState) ToStatus() dto.TunnelStatus { } clean := dto.TunnelStatus{ - Active: t.state.Active, - Duration: uptime, - Identities: make([]*dto.Identity, idCount), - IpInfo: t.state.IpInfo, - LogLevel: t.state.LogLevel, - TunIpv4: t.state.TunIpv4, - TunIpv4Mask: t.state.TunIpv4Mask, + Active: t.state.Active, + Duration: uptime, + Identities: make([]*dto.Identity, idCount), + IpInfo: t.state.IpInfo, + LogLevel: t.state.LogLevel, + ServiceVersion: Version, + TunIpv4: t.state.TunIpv4, + TunIpv4Mask: t.state.TunIpv4Mask, } for i, id := range t.state.Identities { From 714969003ef4bce79c82f5e8506dedf1055ff335 Mon Sep 17 00:00:00 2001 From: dovholuknf <46322585+dovholuknf@users.noreply.github.com> Date: Fri, 9 Oct 2020 10:23:44 -0400 Subject: [PATCH 12/18] update UI to show actual service version from the service bump version of the csdk / ziti tunnel sdk too --- DesktopEdge/MainMenu.xaml.cs | 7 +++---- DesktopEdge/MainWindow.xaml.cs | 1 + release-notes.md | 12 ++++++++++++ service/build.bat | 2 +- 4 files changed, 17 insertions(+), 5 deletions(-) diff --git a/DesktopEdge/MainMenu.xaml.cs b/DesktopEdge/MainMenu.xaml.cs index ed405bdf..397b93b4 100644 --- a/DesktopEdge/MainMenu.xaml.cs +++ b/DesktopEdge/MainMenu.xaml.cs @@ -76,15 +76,14 @@ namespace ZitiDesktopEdge LogsItems.Visibility = Visibility.Collapsed; ConfigItems.Visibility = Visibility.Collapsed; - if (menuState=="About") { + if (menuState == "About") { MenuTitle.Content = "About"; AboutItemsArea.Visibility = Visibility.Visible; AboutItems.Visibility = Visibility.Visible; BackArrow.Visibility = Visibility.Visible; - // Service Version - Lose the snapshot - string version = File.ReadAllText(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "OpenZiti", "Service", "Version.txt")); - if (version.IndexOf("-") > 0) version = version.Split('-')[0]; + ZitiDesktopEdge.ServiceClient.TunnelStatus s = (ZitiDesktopEdge.ServiceClient.TunnelStatus)Application.Current.Properties["CurrentTunnelStatus"]; + string version = $"{s.ServiceVersion.Version}@{s.ServiceVersion.Revision}"; // Interface Version VersionInfo.Content = "App: " + System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.ToString()+" Service: "+ version; diff --git a/DesktopEdge/MainWindow.xaml.cs b/DesktopEdge/MainWindow.xaml.cs index e92300d0..1e93941d 100644 --- a/DesktopEdge/MainWindow.xaml.cs +++ b/DesktopEdge/MainWindow.xaml.cs @@ -240,6 +240,7 @@ namespace ZitiDesktopEdge { private void ServiceClient_OnTunnelStatusEvent(object sender, TunnelStatusEvent e) { if (e == null) return; //just skip it for now... Debug.WriteLine($"==== TunnelStatusEvent: "); + Application.Current.Properties.Add("CurrentTunnelStatus", e.Status); e.Status.Dump(Console.Out); this.Dispatcher.Invoke(() => { if(e.ApiVersion != Client.EXPECTED_API_VERSION) { diff --git a/release-notes.md b/release-notes.md index 12652b80..12b27a73 100644 --- a/release-notes.md +++ b/release-notes.md @@ -1,6 +1,18 @@ These release notes will try to cover the changes made for the installer. For changes made to the Windows service see [release-notes-service.md]() +# Release 1.2.6 + +* What's New + + * 'About' updated to reflect actual service version + * Added ability to set the log level of the service dynamically through Advanced Setttings + * + +* Bug fixes + + * [#102](https://github.com/openziti/desktop-edge-win/pull/102) - DNS requests with "connection-specific local domain" would not resolve + # Release 1.0.4 * What's New diff --git a/service/build.bat b/service/build.bat index e9d8cb2f..707a992b 100644 --- a/service/build.bat +++ b/service/build.bat @@ -34,7 +34,7 @@ echo changing to service folder: %SVC_ROOT_DIR% cd %SVC_ROOT_DIR% SET REPO_URL=https://github.com/openziti/ziti-tunneler-sdk-c.git -SET ZITI_TUNNEL_REPO_BRANCH=v0.6.8 +SET ZITI_TUNNEL_REPO_BRANCH=v0.6.9 SET TUNNELER_SDK_DIR=%SVC_ROOT_DIR%deps\ziti-tunneler-sdk-c\ SET CGO_CFLAGS=-DNOGDI -I %TUNNELER_SDK_DIR%install\include SET CGO_LDFLAGS=-L %TUNNELER_SDK_DIR%install\lib From 6da771305d166af6f10396dbb327f63ffbec4b24 Mon Sep 17 00:00:00 2001 From: dovholuknf <46322585+dovholuknf@users.noreply.github.com> Date: Fri, 9 Oct 2020 13:11:06 -0400 Subject: [PATCH 13/18] refactor globals -> constants/logging change ip and mask to 100.64.0.0/10 --- service/cziti/dns.go | 7 ++- service/ziti-tunnel/constants/consts.go | 7 +++ service/ziti-tunnel/main.go | 16 +++---- service/ziti-tunnel/service/ipc.go | 46 ++++++++++--------- service/ziti-tunnel/service/pkg-vars.go | 8 ++-- service/ziti-tunnel/service/service.go | 24 +++++----- service/ziti-tunnel/service/state.go | 11 +++-- .../{ipchanges => util}/detect-ip-changes.go | 2 +- .../ziti-tunnel/{ => util}/idutil/identity.go | 0 .../{globals => util/logging}/loghelper.go | 2 +- 10 files changed, 66 insertions(+), 57 deletions(-) create mode 100644 service/ziti-tunnel/constants/consts.go rename service/ziti-tunnel/{ipchanges => util}/detect-ip-changes.go (99%) rename service/ziti-tunnel/{ => util}/idutil/identity.go (100%) rename service/ziti-tunnel/{globals => util/logging}/loghelper.go (99%) diff --git a/service/cziti/dns.go b/service/cziti/dns.go index 1eb2834e..43c4a030 100644 --- a/service/cziti/dns.go +++ b/service/cziti/dns.go @@ -21,6 +21,8 @@ import "C" import ( "encoding/binary" "fmt" + "github.com/openziti/desktop-edge-win/service/ziti-tunnel/constants" + "net" "strings" "sync" @@ -35,9 +37,6 @@ type DnsManager interface { GetService(ip net.IP, port uint16) (*CZitiCtx, string, error) } -const defaultCidr = "169.254.0.0" -const defaultMaskBits = 16 - var initOnce = sync.Once{} var DNS = &dnsImpl{} @@ -80,7 +79,7 @@ func normalizeDnsName(dnsName string) string { // assigned to a hostname an error will also be returned indicating why. func (dns *dnsImpl) RegisterService(svcId string, dnsNameToReg string, port uint16, ctx *CZitiCtx, svcName string) (net.IP, error) { log.Infof("adding DNS entry for service name %s@%s:%d", svcName, dnsNameToReg, port) - DnsInit(defaultCidr, defaultMaskBits) + DnsInit(constants.Ipv4ip, constants.Ipv4DefaultMask) dnsName := normalizeDnsName(dnsNameToReg) key := fmt.Sprint(dnsName, ':', port) diff --git a/service/ziti-tunnel/constants/consts.go b/service/ziti-tunnel/constants/consts.go new file mode 100644 index 00000000..809cb599 --- /dev/null +++ b/service/ziti-tunnel/constants/consts.go @@ -0,0 +1,7 @@ +package constants + +const( + Ipv4ip = "100.64.0.0" + Ipv4MaxMask = 10 + Ipv4DefaultMask = 24 +) \ No newline at end of file diff --git a/service/ziti-tunnel/main.go b/service/ziti-tunnel/main.go index 1f868bdc..ac96141f 100644 --- a/service/ziti-tunnel/main.go +++ b/service/ziti-tunnel/main.go @@ -21,17 +21,17 @@ package main import ( "fmt" "github.com/openziti/desktop-edge-win/service/ziti-tunnel/dto" + "github.com/openziti/desktop-edge-win/service/ziti-tunnel/util/logging" "golang.org/x/sys/windows/svc/debug" "golang.org/x/sys/windows/svc/eventlog" "os" "strings" - "github.com/openziti/desktop-edge-win/service/ziti-tunnel/globals" - "golang.org/x/sys/windows/svc" "github.com/openziti/desktop-edge-win/service/ziti-tunnel/service" + "golang.org/x/sys/windows/svc" ) -var log = globals.Logger() +var log = logging.Logger() func main() { service.Version = dto.ServiceVersion{ @@ -39,7 +39,7 @@ func main() { Revision: Revision, BuildDate: BuildDate, } - globals.InitLogger("info") + logging.InitLogger("info") //if len(os.Args) < 2 { // // if no args supplied - expect to run as service @@ -57,9 +57,9 @@ func main() { var err error if hasArgs { - globals.Elog = debug.New(service.SvcName) + logging.Elog = debug.New(service.SvcName) } else { - globals.Elog, err = eventlog.Open(service.SvcName) + logging.Elog, err = eventlog.Open(service.SvcName) if err != nil { return } @@ -74,9 +74,9 @@ func main() { usage("no command specified") } - defer globals.Elog.Close() + defer logging.Elog.Close() - elog := globals.Elog + elog := logging.Elog cmd := strings.ToLower(os.Args[1]) switch cmd { diff --git a/service/ziti-tunnel/service/ipc.go b/service/ziti-tunnel/service/ipc.go index 96b39d0c..037c01bb 100644 --- a/service/ziti-tunnel/service/ipc.go +++ b/service/ziti-tunnel/service/ipc.go @@ -23,14 +23,16 @@ import ( "encoding/json" "fmt" "github.com/Microsoft/go-winio" + "github.com/openziti/desktop-edge-win/service/ziti-tunnel/constants" + "github.com/openziti/desktop-edge-win/service/ziti-tunnel/util/idutil" + "github.com/openziti/desktop-edge-win/service/ziti-tunnel/util/logging" "github.com/openziti/foundation/identity/identity" idcfg "github.com/openziti/sdk-golang/ziti/config" "github.com/openziti/sdk-golang/ziti/enroll" "github.com/openziti/desktop-edge-win/service/cziti" "github.com/openziti/desktop-edge-win/service/ziti-tunnel/config" "github.com/openziti/desktop-edge-win/service/ziti-tunnel/dto" - "github.com/openziti/desktop-edge-win/service/ziti-tunnel/globals" - "github.com/openziti/desktop-edge-win/service/ziti-tunnel/idutil" + "github.com/sirupsen/logrus" "golang.org/x/sys/windows/svc" "golang.zx2c4.com/wireguard/tun" "io" @@ -63,11 +65,11 @@ func SubMain(ops chan string, changes chan<- svc.Status) error { rts.LoadConfig() l := rts.state.LogLevel - _, czitiLevel := globals.ParseLevel(l) + _, czitiLevel := logging.ParseLevel(l) - globals.InitLogger(l) + logging.InitLogger(l ) - _ = globals.Elog.Info(InformationEvent, SvcName+" starting. log file located at "+config.LogFile()) + _ = logging.Elog.Info(InformationEvent, SvcName+" starting. log file located at "+config.LogFile()) // create a channel for notifying any connections that they are to be interrupted interrupt = make(chan struct{}, 8) @@ -100,7 +102,7 @@ func SubMain(ops chan string, changes chan<- svc.Status) error { // notify the service is running changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted} - _ = globals.Elog.Info(InformationEvent, SvcName+" status set to running") + _ = logging.Elog.Info(InformationEvent, SvcName+" status set to running") log.Info(SvcName + " status set to running. starting cancel loop") waitForStopRequest(ops) @@ -245,12 +247,12 @@ func initialize(ipv4 string, ipv4mask int) error { func setTunInfo(s *dto.TunnelStatus, ipv4 string, ipv4mask int) { if strings.TrimSpace(ipv4) == "" { log.Infof("ip not provided using default: %v", ipv4) - ipv4 = Ipv4ip + ipv4 = constants.Ipv4ip rts.UpdateIpv4(ipv4) } - if ipv4mask < 8 || ipv4mask > 24 { - log.Warnf("provided mask is invalid: %d. using default value: %d", ipv4mask, Ipv4mask) - ipv4mask = Ipv4mask + if ipv4mask < 8 || ipv4mask > constants.Ipv4MaxMask { + log.Warnf("provided mask is invalid: %d. using default value: %d", ipv4mask, constants.Ipv4DefaultMask) + ipv4mask = constants.Ipv4DefaultMask rts.UpdateIpv4Mask(ipv4mask) } _, ipnet, err := net.ParseCIDR(fmt.Sprintf("%s/%d", ipv4, ipv4mask)) @@ -295,7 +297,7 @@ func accept(p net.Listener, serveFunction func(net.Conn), debug string) { c, err := p.Accept() if err != nil { if err != winio.ErrPipeListenerClosed { - log.Errorf("unexpected error while accepting a connection. exiting loop. %v", err) + log.Errorf("%v unexpected error while accepting a connection. exiting loop. %v", debug, err) } return } @@ -332,7 +334,7 @@ func serveIpc(conn net.Conn) { select { case <-interrupt: log.Info("request to interrupt read loop received") - conn.Close() + _ = conn.Close() log.Info("read loop interrupted") case <-done: log.Debug("loop finished normally") @@ -430,8 +432,10 @@ func serveIpc(conn net.Conn) { } func setLogLevel(out *json.Encoder, level string) { - goLevel, cLevel := globals.ParseLevel(level) - globals.SetLogLevel(goLevel, cLevel) + goLevel, cLevel := logging.ParseLevel(level) + logrus.Infof("Setting logger levels to %s", goLevel) + logrus.SetLevel(goLevel) + cziti.SetLogLevel(cLevel) rts.state.LogLevel = goLevel.String() respond(out, dto.Response{Message: "log level set", Code: SUCCESS, Error: "", Payload: nil}) } @@ -600,7 +604,7 @@ func toggleIdentity(out *json.Encoder, fingerprint string, onOff bool) { if onOff { connectIdentity(id) } else { - disconnectIdentity(id) + _ = disconnectIdentity(id) } respond(out, dto.Response{Message: "identity toggled", Code: SUCCESS, Error: "", Payload: idutil.Clean(*id)}) @@ -846,7 +850,7 @@ func acceptServices() { switch c.Operation { case cziti.ADDED: //add the service to the identity - svc := dto.Service{ + s := dto.Service{ Name: c.Service.Name, InterceptHost: c.Service.InterceptHost, InterceptPort: c.Service.InterceptPort, @@ -854,22 +858,22 @@ func acceptServices() { AssignedIP: c.Service.AssignedIP, OwnsIntercept: c.Service.OwnsIntercept, } - id.Services = append(id.Services, &svc) + id.Services = append(id.Services, &s) events.broadcast <- dto.ServiceEvent{ ActionEvent: SERVICE_ADDED, Fingerprint: id.FingerPrint, - Service: svc, + Service: s, } log.Debug("dispatched added service change event") case cziti.REMOVED: - for idx, svc := range id.Services { - if svc.Name == c.Service.Name { + for idx, s := range id.Services { + if s.Name == c.Service.Name { id.Services = append(id.Services[:idx], id.Services[idx+1:]...) events.broadcast <- dto.ServiceEvent{ ActionEvent: SERVICE_REMOVED, Fingerprint: id.FingerPrint, - Service: *svc, + Service: *s, } log.Debug(" dispatched remove service change event") } diff --git a/service/ziti-tunnel/service/pkg-vars.go b/service/ziti-tunnel/service/pkg-vars.go index e757f2cc..de1934da 100644 --- a/service/ziti-tunnel/service/pkg-vars.go +++ b/service/ziti-tunnel/service/pkg-vars.go @@ -18,11 +18,11 @@ package service import ( + "github.com/openziti/desktop-edge-win/service/ziti-tunnel/dto" + "github.com/openziti/desktop-edge-win/service/ziti-tunnel/util/logging" "golang.org/x/sys/windows/svc" "sync" "time" - "github.com/openziti/desktop-edge-win/service/ziti-tunnel/dto" - "github.com/openziti/desktop-edge-win/service/ziti-tunnel/globals" ) var Version dto.ServiceVersion var pipeBase = `\\.\pipe\NetFoundry\tunneler\` @@ -39,7 +39,7 @@ var eventsConnections int var Debug bool var TunStarted time.Time -var log = globals.Logger() +var log = logging.Logger() var events = newTopic() @@ -87,8 +87,6 @@ const ( NF_GROUP_NAME = "NetFoundry Tunneler Users" TunName = "ZitiTUN" - Ipv4ip = "169.254.1.1" - Ipv4mask = 24 Ipv4dns = "127.0.0.1" // use lo -- don't pass DNS queries through tunneler SDK // IPv6 CIDR fe80:6e66:7a69:7469::/64 diff --git a/service/ziti-tunnel/service/service.go b/service/ziti-tunnel/service/service.go index 97b5e2a6..a9b52130 100644 --- a/service/ziti-tunnel/service/service.go +++ b/service/ziti-tunnel/service/service.go @@ -21,9 +21,9 @@ package service import ( "fmt" + "github.com/openziti/desktop-edge-win/service/ziti-tunnel/util/logging" "golang.org/x/sys/windows/svc" "golang.org/x/sys/windows/svc/debug" - "github.com/openziti/desktop-edge-win/service/ziti-tunnel/globals" ) type zitiService struct{} @@ -51,26 +51,26 @@ loop: switch c.Cmd { case svc.Interrogate: changes <- c.CurrentStatus - _ = globals.Elog.Info(InterrogateEvent, "interrogate request received") + _ = logging.Elog.Info(InterrogateEvent, "interrogate request received") changes <- c.CurrentStatus case svc.Stop: - _ = globals.Elog.Info(StopEvent, "issuing stop") + _ = logging.Elog.Info(StopEvent, "issuing stop") control <- "stop" - _ = globals.Elog.Info(StopEvent, "stop issued") + _ = logging.Elog.Info(StopEvent, "stop issued") break loop case svc.Shutdown: - _ = globals.Elog.Info(ShutdownEvent, "issuing shutdown") + _ = logging.Elog.Info(ShutdownEvent, "issuing shutdown") control <- "stop" - _ = globals.Elog.Info(ShutdownEvent, "shutdown issued") + _ = logging.Elog.Info(ShutdownEvent, "shutdown issued") break loop case svc.Pause: changes <- svc.Status{State: svc.Paused, Accepts: cmdsAccepted} - _ = globals.Elog.Info(PauseEvent, "request to pause service received. todo - unimplemented") + _ = logging.Elog.Info(PauseEvent, "request to pause service received. todo - unimplemented") case svc.Continue: changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted} - _ = globals.Elog.Info(ContinueEvent, "request to continue service received. todo - unimplemented") + _ = logging.Elog.Info(ContinueEvent, "request to continue service received. todo - unimplemented") default: - _ = globals.Elog.Error(1, fmt.Sprintf("unexpected control request #%d", c)) + _ = logging.Elog.Error(1, fmt.Sprintf("unexpected control request #%d", c)) } } } @@ -85,7 +85,7 @@ loop: } func RunService(isDebug bool) { - _ = globals.Elog.Info(InformationEvent, fmt.Sprintf("starting %s service", SvcStartName)) + _ = logging.Elog.Info(InformationEvent, fmt.Sprintf("starting %s service", SvcStartName)) run := svc.Run if isDebug { log.Info("debug specified. using debug.run") @@ -94,8 +94,8 @@ func RunService(isDebug bool) { err := run(SvcStartName, &zitiService{}) if err != nil { - _ = globals.Elog.Error(ErrorEvent, fmt.Sprintf("%s service failed: %v", SvcStartName, err)) + _ = logging.Elog.Error(ErrorEvent, fmt.Sprintf("%s service failed: %v", SvcStartName, err)) return } - _ = globals.Elog.Info(StopEvent, fmt.Sprintf("%s service stopped", SvcStartName)) + _ = logging.Elog.Info(StopEvent, fmt.Sprintf("%s service stopped", SvcStartName)) } diff --git a/service/ziti-tunnel/service/state.go b/service/ziti-tunnel/service/state.go index 5956a3ec..d7a3aa90 100644 --- a/service/ziti-tunnel/service/state.go +++ b/service/ziti-tunnel/service/state.go @@ -23,8 +23,9 @@ import ( "fmt" "github.com/openziti/desktop-edge-win/service/cziti" "github.com/openziti/desktop-edge-win/service/ziti-tunnel/config" + "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/idutil" + "github.com/openziti/desktop-edge-win/service/ziti-tunnel/util/idutil" "golang.org/x/sys/windows/registry" "golang.zx2c4.com/wireguard/tun" "golang.zx2c4.com/wireguard/windows/tunnel/winipcfg" @@ -147,11 +148,11 @@ func (t *RuntimeState) CreateTun(ipv4 string, ipv4mask int) error { if strings.TrimSpace(ipv4) == "" { log.Infof("ip not provided using default: %v", ipv4) - ipv4 = Ipv4ip + ipv4 = constants.Ipv4ip } - if ipv4mask < 8 || ipv4mask > 24 { - log.Warnf("provided mask is invalid: %d. using default value: %d", ipv4mask, Ipv4mask) - ipv4mask = Ipv4mask + if ipv4mask < 8 || ipv4mask > constants.Ipv4MaxMask { + log.Warnf("provided mask is invalid: %d. using default value: %d", ipv4mask, constants.Ipv4DefaultMask) + ipv4mask = constants.Ipv4DefaultMask rts.UpdateIpv4Mask(ipv4mask) } ip, ipnet, err := net.ParseCIDR(fmt.Sprintf("%s/%d", ipv4, ipv4mask)) diff --git a/service/ziti-tunnel/ipchanges/detect-ip-changes.go b/service/ziti-tunnel/util/detect-ip-changes.go similarity index 99% rename from service/ziti-tunnel/ipchanges/detect-ip-changes.go rename to service/ziti-tunnel/util/detect-ip-changes.go index 4c7511cb..51623cb2 100644 --- a/service/ziti-tunnel/ipchanges/detect-ip-changes.go +++ b/service/ziti-tunnel/util/detect-ip-changes.go @@ -1,4 +1,4 @@ -package ipchanges +package util import ( "github.com/michaelquigley/pfxlog" diff --git a/service/ziti-tunnel/idutil/identity.go b/service/ziti-tunnel/util/idutil/identity.go similarity index 100% rename from service/ziti-tunnel/idutil/identity.go rename to service/ziti-tunnel/util/idutil/identity.go diff --git a/service/ziti-tunnel/globals/loghelper.go b/service/ziti-tunnel/util/logging/loghelper.go similarity index 99% rename from service/ziti-tunnel/globals/loghelper.go rename to service/ziti-tunnel/util/logging/loghelper.go index 3259555a..28cf343f 100644 --- a/service/ziti-tunnel/globals/loghelper.go +++ b/service/ziti-tunnel/util/logging/loghelper.go @@ -15,7 +15,7 @@ * */ -package globals +package logging import ( rotatelogs "github.com/lestrrat-go/file-rotatelogs" From b73d8c5563fcb51ed0c170b6da19f6e3d29d0e55 Mon Sep 17 00:00:00 2001 From: dovholuknf <46322585+dovholuknf@users.noreply.github.com> Date: Fri, 9 Oct 2020 13:12:40 -0400 Subject: [PATCH 14/18] Update release-notes for service --- release-notes-service.md | 1 + 1 file changed, 1 insertion(+) diff --git a/release-notes-service.md b/release-notes-service.md index fa84513f..1dad7035 100644 --- a/release-notes-service.md +++ b/release-notes-service.md @@ -6,6 +6,7 @@ For UI changes or installer changes see [release-notes.md]() * What's New * Continually improved logging + * changed ip from 169.254.0.0/16 to 100.64.0.0/10 * [#120](https://github.com/openziti/desktop-edge-win/issues/120) - Allow UI/client to get and set log level dynamically via ipc * Bug fixes From 2cf7bc8aca83d5e179ecace2e2efeef61133c791 Mon Sep 17 00:00:00 2001 From: Jeremy Tellier - ROG Date: Fri, 9 Oct 2020 23:05:04 +0100 Subject: [PATCH 15/18] Set Log Level Menu --- DesktopEdge/Assets/Images/Success.png | Bin 0 -> 1535 bytes DesktopEdge/MainMenu.xaml | 10 ++++ DesktopEdge/MainMenu.xaml.cs | 41 ++++++++++++++ DesktopEdge/MainWindow.xaml.cs | 8 ++- DesktopEdge/ServiceClient/ClientImpl.cs | 12 +++++ DesktopEdge/ServiceClient/DataStructures.cs | 34 ++++++------ .../Views/ItemRenderers/SubOptionItem.xaml | 48 +++++++++++++++++ .../Views/ItemRenderers/SubOptionItem.xaml.cs | 50 ++++++++++++++++++ DesktopEdge/ZitiDesktopEdge.csproj | 10 ++++ 9 files changed, 197 insertions(+), 16 deletions(-) create mode 100644 DesktopEdge/Assets/Images/Success.png create mode 100644 DesktopEdge/Views/ItemRenderers/SubOptionItem.xaml create mode 100644 DesktopEdge/Views/ItemRenderers/SubOptionItem.xaml.cs diff --git a/DesktopEdge/Assets/Images/Success.png b/DesktopEdge/Assets/Images/Success.png new file mode 100644 index 0000000000000000000000000000000000000000..d9532ab4975ee9b75bf9d2d939d43cad5c9e4c66 GIT binary patch literal 1535 zcmZ`(X;4#F7`+g{KtNDqWEmAuYgN>V6-DWwKoBB<210DYzDU@_4FQW5*%DSEpx^>U zK%6R4Kp8>Mjs`c7fPtbcAsc%T3`*6#)B_ts$fXt8`(vHYMcKW&yjj2*C= z9b~{hxt2|Wb3)d0LJc|LMx1bCZp3EpX;bc*t-N#Fc;~nCqJHFE*f|irYaqsAAZE|N zMdV=IzQMRdLszVa5^RT49EMYmjifq`q+v!fT}HFqM|1FFxrDK6KKue-{*B=A!tl>` zB0ra&`CN8>qC9@GlKiD6P4GBN@FZJMN1dwApK2_aes*VuRyx!4VD??5u%%kqS|g;_ z&b8OgF&gGNY4hE07JAzjIqi!ahKSc8;&m?#vzGXS;>lt0ml26zR3hL@rpG1I6O!5K z<@wp=1>y4IoK!R~6)i}Y7Ntufsdz~$5lbb@GO1K1mC0o68@lT94>JCOyDJW;Lo_tC zRzbCOV7hv%^$pgng(Ef?8Jlc0HAh<79CJiFV_aO_a2^C-|C6M!$hhCHBqSxLkSS@I zSvl0)yldABZr&>V^ZtX1%D<{=>*}92KL7g#?e)9nmR5TE2S!KdKV6@?d-}M%fx+RC zQU3U(U}}11Ryen?C|VLrq#J@xqU4jP1v)#TdSi8Bt>iyUJ{o1~VQ6%frfH9JGt}b# z;s9qLYYf&yAP_IQ5uxXhH5Pdks|L~VEkZtfW?ge>WMcKH`DK&X7(oUrs(#c=kMjEZ z8h)>+x$U-PWrUpu`Q^=1dnnaS(~)2vef9YeJ+W>d(qg}g1fX>^9$+ol(TQEq&{)FcL?BaAKt5<*d>& zBOLQ8)<3Vzx#f2z5rf9f7(zE+%D!$_vEesz;hNjpADdGH4%i_N-j;OTN#&A0-aDQ} z()j7o0Zr&Z+h5Zk91aWnL$863*cxD1gXq(VbIkRAWgB4-isxvR?`?cFWWKnwh*5 + + + + + + + + + + diff --git a/DesktopEdge/MainMenu.xaml.cs b/DesktopEdge/MainMenu.xaml.cs index ed405bdf..8424f74e 100644 --- a/DesktopEdge/MainMenu.xaml.cs +++ b/DesktopEdge/MainMenu.xaml.cs @@ -21,8 +21,11 @@ namespace ZitiDesktopEdge public delegate void AttachementChanged(bool attached); public event AttachementChanged OnAttachmentChange; + public delegate void LogLevelChanged(string level); + public event LogLevelChanged OnLogLevelChanged; public string menuState = "Main"; public string licenseData = "it's open source."; + public string LogLevel = ""; public MainMenu() { InitializeComponent(); @@ -64,6 +67,10 @@ namespace ZitiDesktopEdge menuState = "UILogs"; UpdateState(); } + private void SetLogLevel(object sender, MouseButtonEventArgs e) { + menuState = "LogLevel"; + UpdateState(); + } private void UpdateState() { MainItems.Visibility = Visibility.Collapsed; @@ -75,6 +82,7 @@ namespace ZitiDesktopEdge LicensesItems.Visibility = Visibility.Collapsed; LogsItems.Visibility = Visibility.Collapsed; ConfigItems.Visibility = Visibility.Collapsed; + LogLevelItems.Visibility = Visibility.Collapsed; if (menuState=="About") { MenuTitle.Content = "About"; @@ -108,6 +116,12 @@ namespace ZitiDesktopEdge LogsItems.Text = UILog.GetLogs(); LogsItems.Visibility = Visibility.Visible; BackArrow.Visibility = Visibility.Visible; + } else if (menuState == "LogLevel") { + ResetLevels(); + + MenuTitle.Content = "Set Log Level"; + LogLevelItems.Visibility = Visibility.Visible; + BackArrow.Visibility = Visibility.Visible; } else if (menuState=="Config") { MenuTitle.Content = "Tunnel Configuration"; ConfigItems.Visibility = Visibility.Visible; @@ -214,5 +228,32 @@ namespace ZitiDesktopEdge OnAttachmentChange(true); } } + + private void ResetLevels() { + if (this.LogLevel == "") this.LogLevel = "error"; + LogVerbose.IsSelected = false; + LogDebug.IsSelected = false; + LogInfo.IsSelected = false; + LogError.IsSelected = false; + LogFatal.IsSelected = false; + LogWarn.IsSelected = false; + LogTrace.IsSelected = false; + if (this.LogLevel == "verbose") LogVerbose.IsSelected = true; + else if (this.LogLevel == "debug") LogDebug.IsSelected = true; + else if (this.LogLevel == "info") LogInfo.IsSelected = true; + else if (this.LogLevel == "error") LogError.IsSelected = true; + else if (this.LogLevel == "fatal") LogFatal.IsSelected = true; + else if (this.LogLevel == "warn") LogWarn.IsSelected = true; + else if (this.LogLevel == "trace") LogTrace.IsSelected = true; + } + + private void SetLevel(object sender, MouseButtonEventArgs e) { + SubOptionItem item = (SubOptionItem)sender; + this.LogLevel = item.Label.ToLower(); + if (OnLogLevelChanged != null) { + OnLogLevelChanged(this.LogLevel); + } + ResetLevels(); + } } } diff --git a/DesktopEdge/MainWindow.xaml.cs b/DesktopEdge/MainWindow.xaml.cs index e92300d0..f95efc99 100644 --- a/DesktopEdge/MainWindow.xaml.cs +++ b/DesktopEdge/MainWindow.xaml.cs @@ -127,6 +127,7 @@ namespace ZitiDesktopEdge { Application.Current.Properties.Add("ServiceClient", serviceClient); Application.Current.Properties.Add("Identities", new List()); MainMenu.OnAttachmentChange += AttachmentChanged; + MainMenu.OnLogLevelChanged += LogLevelChanged; IdentityMenu.OnError += IdentityMenu_OnError; try { @@ -141,6 +142,10 @@ namespace ZitiDesktopEdge { IdentityMenu.OnForgot += IdentityForgotten; } + private void LogLevelChanged(string level) { + serviceClient.SetLogLevel(level); + } + private void IdentityMenu_OnError(string message) { ShowError("Identity Error", message); } @@ -246,7 +251,7 @@ namespace ZitiDesktopEdge { SetCantDisplay("Version mismatch!", "The version of the Service is not compatible"); return; } - + this.MainMenu.LogLevel = e.Status.LogLevel; InitializeTimer((int)e.Status.Duration); LoadStatusFromService(e.Status); LoadIdentities(false); @@ -394,6 +399,7 @@ namespace ZitiDesktopEdge { try { Identity createdId = serviceClient.AddIdentity(System.IO.Path.GetFileName(jwtDialog.FileName), false, fileContent); ServiceClient.Client client = (ServiceClient.Client)Application.Current.Properties["ServiceClient"]; + client.IdentityOnOff(createdId.FingerPrint, true); if (createdId != null) { identities.Add(ZitiIdentity.FromClient(createdId)); diff --git a/DesktopEdge/ServiceClient/ClientImpl.cs b/DesktopEdge/ServiceClient/ClientImpl.cs index 901c7078..e2cb9c03 100644 --- a/DesktopEdge/ServiceClient/ClientImpl.cs +++ b/DesktopEdge/ServiceClient/ClientImpl.cs @@ -390,6 +390,18 @@ namespace ZitiDesktopEdge.ServiceClient } } + public void SetLogLevel(string level) { + try { + send(new SetLogLevelFunction(level)); + SvcResponse resp = read(ipcReader); + return; + } catch (IOException ioe) { + //almost certainly a problem with the pipe - recreate the pipe... + //setupPipe(); + throw ioe; + } + } + public void SetLogLevel(LogLevelEnum level) { try diff --git a/DesktopEdge/ServiceClient/DataStructures.cs b/DesktopEdge/ServiceClient/DataStructures.cs index a3329785..ede83adf 100644 --- a/DesktopEdge/ServiceClient/DataStructures.cs +++ b/DesktopEdge/ServiceClient/DataStructures.cs @@ -184,22 +184,26 @@ namespace ZitiDesktopEdge.ServiceClient public void Dump(System.IO.TextWriter writer) { - writer.WriteLine($"Tunnel Active: {Active}"); - writer.WriteLine($" LogLevel : {LogLevel}"); - writer.WriteLine($" EvaluatedLogLevel: {EvaluateLogLevel()}"); - foreach (Identity id in Identities) - { - writer.WriteLine($" FingerPrint: {id.FingerPrint}"); - writer.WriteLine($" Name : {id.Name}"); - writer.WriteLine($" Active : {id.Active}"); - writer.WriteLine($" Status : {id.Status}"); - writer.WriteLine($" Services:"); - foreach (Service s in id.Services) - { - writer.WriteLine($" Name: {s.Name} HostName: {s.InterceptHost} Port: {s.InterceptPort}"); + try { + writer.WriteLine($"Tunnel Active: {Active}"); + writer.WriteLine($" LogLevel : {LogLevel}"); + writer.WriteLine($" EvaluatedLogLevel: {EvaluateLogLevel()}"); + foreach (Identity id in Identities) { + writer.WriteLine($" FingerPrint: {id.FingerPrint}"); + writer.WriteLine($" Name : {id.Name}"); + writer.WriteLine($" Active : {id.Active}"); + writer.WriteLine($" Status : {id.Status}"); + writer.WriteLine($" Services:"); + foreach (Service s in id.Services) { + writer.WriteLine($" Name: {s.Name} HostName: {s.InterceptHost} Port: {s.InterceptPort}"); + } + writer.WriteLine("============================================="); } - writer.WriteLine("============================================="); - } + + } catch (Exception e) { + + } + } public LogLevelEnum EvaluateLogLevel() diff --git a/DesktopEdge/Views/ItemRenderers/SubOptionItem.xaml b/DesktopEdge/Views/ItemRenderers/SubOptionItem.xaml new file mode 100644 index 00000000..07088256 --- /dev/null +++ b/DesktopEdge/Views/ItemRenderers/SubOptionItem.xaml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/DesktopEdge/Views/ItemRenderers/SubOptionItem.xaml.cs b/DesktopEdge/Views/ItemRenderers/SubOptionItem.xaml.cs new file mode 100644 index 00000000..8a4467e8 --- /dev/null +++ b/DesktopEdge/Views/ItemRenderers/SubOptionItem.xaml.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Navigation; +using System.Windows.Shapes; + +namespace ZitiDesktopEdge { + /// + /// Interaction logic for MenuItem.xaml + /// + public partial class SubOptionItem : UserControl { + + private string _label = ""; + private bool _isSelected = false; + + public string Label { + get { + return _label; + } + set { + this._label = value; + MainLabel.Content = this._label; + } + } + + public bool IsSelected { + get { + return _isSelected; + } + set { + this._isSelected = value; + if (this._isSelected) SelectedCheck.Visibility = Visibility.Visible; + else SelectedCheck.Visibility = Visibility.Collapsed; + } + } + + public SubOptionItem() { + InitializeComponent(); + } + } +} diff --git a/DesktopEdge/ZitiDesktopEdge.csproj b/DesktopEdge/ZitiDesktopEdge.csproj index d745cd13..d3bfcd1a 100644 --- a/DesktopEdge/ZitiDesktopEdge.csproj +++ b/DesktopEdge/ZitiDesktopEdge.csproj @@ -147,6 +147,9 @@ RegistrationScreen.xaml + + SubOptionItem.xaml + SubMenuItem.xaml @@ -201,6 +204,10 @@ Designer MSBuild:Compile + + MSBuild:Compile + Designer + MSBuild:Compile Designer @@ -353,5 +360,8 @@ + + + \ No newline at end of file From 9ed19066539f4063a1f7706a896b021441673654 Mon Sep 17 00:00:00 2001 From: Jeremy Tellier - ROG Date: Sat, 10 Oct 2020 00:16:39 +0100 Subject: [PATCH 16/18] Fix where the Version is set and pulled from. --- DesktopEdge/MainMenu.xaml.cs | 9 +++++++-- DesktopEdge/Properties/AssemblyInfo.cs | 4 ++-- DesktopEdge/ServiceClient/DataStructures.cs | 18 ------------------ DesktopEdge/ZitiDesktopEdge.csproj | 2 +- 4 files changed, 10 insertions(+), 23 deletions(-) diff --git a/DesktopEdge/MainMenu.xaml.cs b/DesktopEdge/MainMenu.xaml.cs index 311dfbe3..f3eb8df2 100644 --- a/DesktopEdge/MainMenu.xaml.cs +++ b/DesktopEdge/MainMenu.xaml.cs @@ -90,8 +90,13 @@ namespace ZitiDesktopEdge AboutItems.Visibility = Visibility.Visible; BackArrow.Visibility = Visibility.Visible; - ZitiDesktopEdge.ServiceClient.TunnelStatus s = (ZitiDesktopEdge.ServiceClient.TunnelStatus)Application.Current.Properties["CurrentTunnelStatus"]; - string version = $"{s.ServiceVersion.Version}@{s.ServiceVersion.Revision}"; + string version = ""; + try { + ZitiDesktopEdge.ServiceClient.TunnelStatus s = (ZitiDesktopEdge.ServiceClient.TunnelStatus)Application.Current.Properties["CurrentTunnelStatus"]; + version = $"{s.ServiceVersion.Version}@{s.ServiceVersion.Revision}"; + } catch (Exception e) { + + } // Interface Version VersionInfo.Content = "App: " + System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.ToString()+" Service: "+ version; diff --git a/DesktopEdge/Properties/AssemblyInfo.cs b/DesktopEdge/Properties/AssemblyInfo.cs index 9155285a..e3620742 100644 --- a/DesktopEdge/Properties/AssemblyInfo.cs +++ b/DesktopEdge/Properties/AssemblyInfo.cs @@ -51,5 +51,5 @@ 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.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] +[assembly: AssemblyVersion("1.2.6.0")] +[assembly: AssemblyFileVersion("1.2.6.0")] diff --git a/DesktopEdge/ServiceClient/DataStructures.cs b/DesktopEdge/ServiceClient/DataStructures.cs index 5e47e8d6..4bb916af 100644 --- a/DesktopEdge/ServiceClient/DataStructures.cs +++ b/DesktopEdge/ServiceClient/DataStructures.cs @@ -198,7 +198,6 @@ namespace ZitiDesktopEdge.ServiceClient public void Dump(System.IO.TextWriter writer) { -<<<<<<< HEAD try { writer.WriteLine($"Tunnel Active: {Active}"); writer.WriteLine($" LogLevel : {LogLevel}"); @@ -213,24 +212,7 @@ namespace ZitiDesktopEdge.ServiceClient writer.WriteLine($" Name: {s.Name} HostName: {s.InterceptHost} Port: {s.InterceptPort}"); } writer.WriteLine("============================================="); -======= - writer.WriteLine($"Tunnel Active: {Active}"); - writer.WriteLine($" ServiceVersion : {ServiceVersion}"); - writer.WriteLine($" LogLevel : {LogLevel}"); - writer.WriteLine($" EvaluatedLogLevel: {EvaluateLogLevel()}"); - foreach (Identity id in Identities) - { - writer.WriteLine($" FingerPrint: {id.FingerPrint}"); - writer.WriteLine($" Name : {id.Name}"); - writer.WriteLine($" Active : {id.Active}"); - writer.WriteLine($" Status : {id.Status}"); - writer.WriteLine($" Services:"); - foreach (Service s in id?.Services) - { - writer.WriteLine($" Name: {s.Name} HostName: {s.InterceptHost} Port: {s.InterceptPort}"); ->>>>>>> b73d8c5563fcb51ed0c170b6da19f6e3d29d0e55 } - } catch (Exception e) { } diff --git a/DesktopEdge/ZitiDesktopEdge.csproj b/DesktopEdge/ZitiDesktopEdge.csproj index d3bfcd1a..76aca7dd 100644 --- a/DesktopEdge/ZitiDesktopEdge.csproj +++ b/DesktopEdge/ZitiDesktopEdge.csproj @@ -34,7 +34,7 @@ Ziti true 0 - 1.2.1.%2a + 1.2.7.%2a false true true From f85ac73934067c6233da75fb69a602ddef8e4b24 Mon Sep 17 00:00:00 2001 From: Jeremy Tellier - ROG Date: Mon, 12 Oct 2020 00:27:47 +0100 Subject: [PATCH 17/18] Show the update link if there is an update available. --- DesktopEdge/MainMenu.xaml | 3 +- DesktopEdge/MainMenu.xaml.cs | 45 ++++++++++++++++++++- DesktopEdge/MainWindow.xaml.cs | 1 - DesktopEdge/Properties/AssemblyInfo.cs | 4 +- DesktopEdge/ServiceClient/DataStructures.cs | 2 +- 5 files changed, 48 insertions(+), 7 deletions(-) diff --git a/DesktopEdge/MainMenu.xaml b/DesktopEdge/MainMenu.xaml index 0fc2a30e..36ba6a25 100644 --- a/DesktopEdge/MainMenu.xaml +++ b/DesktopEdge/MainMenu.xaml @@ -81,7 +81,8 @@ - + + diff --git a/DesktopEdge/MainMenu.xaml.cs b/DesktopEdge/MainMenu.xaml.cs index f3eb8df2..04204d7c 100644 --- a/DesktopEdge/MainMenu.xaml.cs +++ b/DesktopEdge/MainMenu.xaml.cs @@ -10,6 +10,9 @@ using System.Reflection; using System.Web; using System.Net.Mail; using System.IO; +using System.Net; +using Newtonsoft.Json.Linq; +using System.Threading.Tasks; namespace ZitiDesktopEdge { @@ -26,10 +29,13 @@ namespace ZitiDesktopEdge public string menuState = "Main"; public string licenseData = "it's open source."; public string LogLevel = ""; + private string _updateUrl = "https://api.github.com/repos/openziti/desktop-edge-win/releases/latest"; + private string _downloadUrl = ""; public MainMenu() { InitializeComponent(); LicensesItems.Text = licenseData; + CheckUpdates(); } private void HideMenu(object sender, MouseButtonEventArgs e) { @@ -42,6 +48,10 @@ namespace ZitiDesktopEdge Application.Current.Shutdown(); } + private void DoUpdate(object sender, MouseButtonEventArgs e) { + System.Diagnostics.Process.Start(_downloadUrl); + } + private void ShowAbout(object sender, MouseButtonEventArgs e) { menuState = "About"; UpdateState(); @@ -72,6 +82,35 @@ namespace ZitiDesktopEdge UpdateState(); } + private void CheckUpdates() { + HttpWebRequest httpWebRequest = WebRequest.CreateHttp(_updateUrl); + httpWebRequest.Method = "GET"; + httpWebRequest.ContentType = "application/json"; + httpWebRequest.UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36"; + HttpWebResponse httpResponse = (HttpWebResponse)httpWebRequest.GetResponse(); + StreamReader streamReader = new StreamReader(httpResponse.GetResponseStream()); + string result = streamReader.ReadToEnd(); + JObject json = JObject.Parse(result); + string currentVersion = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.ToString(); + string serverVersion = json.Property("tag_name").Value.ToString() + ".0"; + + Version installed = new Version(currentVersion); + Version published = new Version(serverVersion); + int compare = installed.CompareTo(published); + if (compare<0) { + UpdateAvailable.Content = "An Upgrade is available, click to download"; + UpdateAvailable.Visibility = Visibility.Visible; + } else if (compare>0) { + UpdateAvailable.Content = "Your version is newer than the released version"; + UpdateAvailable.Visibility = Visibility.Visible; + } + JArray assets = JArray.Parse(json.Property("assets").Value.ToString()); + foreach (JObject asset in assets.Children()) { + _downloadUrl = asset.Property("browser_download_url").Value.ToString(); + break; + } + } + private void UpdateState() { MainItems.Visibility = Visibility.Collapsed; AboutItems.Visibility = Visibility.Collapsed; @@ -92,10 +131,12 @@ namespace ZitiDesktopEdge string version = ""; try { - ZitiDesktopEdge.ServiceClient.TunnelStatus s = (ZitiDesktopEdge.ServiceClient.TunnelStatus)Application.Current.Properties["CurrentTunnelStatus"]; + ServiceClient.TunnelStatus s = (ServiceClient.TunnelStatus)Application.Current.Properties["CurrentTunnelStatus"]; version = $"{s.ServiceVersion.Version}@{s.ServiceVersion.Revision}"; } catch (Exception e) { - +#if DEBUG + Debug.WriteLine(e.ToString()); +#endif } // Interface Version diff --git a/DesktopEdge/MainWindow.xaml.cs b/DesktopEdge/MainWindow.xaml.cs index bcfb94d2..ec61e71c 100644 --- a/DesktopEdge/MainWindow.xaml.cs +++ b/DesktopEdge/MainWindow.xaml.cs @@ -65,7 +65,6 @@ namespace ZitiDesktopEdge { LaunchOrInstall(); SetNotifyIcon("white"); - InitializeComponent(); } private void MainWindow_Activated(object sender, EventArgs e) { diff --git a/DesktopEdge/Properties/AssemblyInfo.cs b/DesktopEdge/Properties/AssemblyInfo.cs index e3620742..73575b2d 100644 --- a/DesktopEdge/Properties/AssemblyInfo.cs +++ b/DesktopEdge/Properties/AssemblyInfo.cs @@ -51,5 +51,5 @@ 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.0.*")] -[assembly: AssemblyVersion("1.2.6.0")] -[assembly: AssemblyFileVersion("1.2.6.0")] +[assembly: AssemblyVersion("1.2.7.0")] +[assembly: AssemblyFileVersion("1.2.7.0")] diff --git a/DesktopEdge/ServiceClient/DataStructures.cs b/DesktopEdge/ServiceClient/DataStructures.cs index 4bb916af..d4e163d6 100644 --- a/DesktopEdge/ServiceClient/DataStructures.cs +++ b/DesktopEdge/ServiceClient/DataStructures.cs @@ -214,7 +214,7 @@ namespace ZitiDesktopEdge.ServiceClient writer.WriteLine("============================================="); } } catch (Exception e) { - + if (writer!=null) writer.WriteLine(e.ToString()); } } From 1d8d75b9f7cd9a42e717a7010e5ee2994f06bae2 Mon Sep 17 00:00:00 2001 From: dovholuknf <46322585+dovholuknf@users.noreply.github.com> Date: Mon, 12 Oct 2020 15:14:32 -0400 Subject: [PATCH 18/18] add adv installer project --- ZitiDesktopEdge.aip | 361 ++++++++++++++++++++++++++++++++++++++++ ZitiWintunInstaller.sln | 34 +--- ziti.ico | Bin 0 -> 43851 bytes 3 files changed, 363 insertions(+), 32 deletions(-) create mode 100644 ZitiDesktopEdge.aip create mode 100644 ziti.ico diff --git a/ZitiDesktopEdge.aip b/ZitiDesktopEdge.aip new file mode 100644 index 00000000..95885b86 --- /dev/null +++ b/ZitiDesktopEdge.aip @@ -0,0 +1,361 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ZitiWintunInstaller.sln b/ZitiWintunInstaller.sln index 1b2d63b6..56f0a223 100644 --- a/ZitiWintunInstaller.sln +++ b/ZitiWintunInstaller.sln @@ -11,8 +11,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution EndProject Project("{930C7802-8A8C-48F9-8165-68863BCCD9DD}") = "ZitiWintunInstaller", "ZitiWintunInstaller\ZitiWintunInstaller.wixproj", "{1A5D850F-2E38-4524-8745-29A96FA62683}" EndProject -Project("{C7167F0D-BC9F-4E6E-AFE1-012C56B48DB5}") = "OpenZitiWintunInstaller", "OpenZitiWintunInstaller\OpenZitiWintunInstaller.wapproj", "{84DBA1A4-D8E9-4282-94FF-121809B48F1F}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -28,47 +26,19 @@ Global EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {1A5D850F-2E38-4524-8745-29A96FA62683}.Debug|Any CPU.ActiveCfg = Debug|x86 + {1A5D850F-2E38-4524-8745-29A96FA62683}.Debug|Any CPU.Build.0 = Debug|x86 {1A5D850F-2E38-4524-8745-29A96FA62683}.Debug|ARM.ActiveCfg = Debug|x86 {1A5D850F-2E38-4524-8745-29A96FA62683}.Debug|ARM64.ActiveCfg = Debug|x86 {1A5D850F-2E38-4524-8745-29A96FA62683}.Debug|x64.ActiveCfg = Debug|x86 {1A5D850F-2E38-4524-8745-29A96FA62683}.Debug|x86.ActiveCfg = Debug|x86 {1A5D850F-2E38-4524-8745-29A96FA62683}.Debug|x86.Build.0 = Debug|x86 {1A5D850F-2E38-4524-8745-29A96FA62683}.Release|Any CPU.ActiveCfg = Release|x86 + {1A5D850F-2E38-4524-8745-29A96FA62683}.Release|Any CPU.Build.0 = Release|x86 {1A5D850F-2E38-4524-8745-29A96FA62683}.Release|ARM.ActiveCfg = Release|x86 {1A5D850F-2E38-4524-8745-29A96FA62683}.Release|ARM64.ActiveCfg = Release|x86 {1A5D850F-2E38-4524-8745-29A96FA62683}.Release|x64.ActiveCfg = Release|x86 {1A5D850F-2E38-4524-8745-29A96FA62683}.Release|x86.ActiveCfg = Release|x86 {1A5D850F-2E38-4524-8745-29A96FA62683}.Release|x86.Build.0 = Release|x86 - {84DBA1A4-D8E9-4282-94FF-121809B48F1F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {84DBA1A4-D8E9-4282-94FF-121809B48F1F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {84DBA1A4-D8E9-4282-94FF-121809B48F1F}.Debug|Any CPU.Deploy.0 = Debug|Any CPU - {84DBA1A4-D8E9-4282-94FF-121809B48F1F}.Debug|ARM.ActiveCfg = Debug|ARM - {84DBA1A4-D8E9-4282-94FF-121809B48F1F}.Debug|ARM.Build.0 = Debug|ARM - {84DBA1A4-D8E9-4282-94FF-121809B48F1F}.Debug|ARM.Deploy.0 = Debug|ARM - {84DBA1A4-D8E9-4282-94FF-121809B48F1F}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {84DBA1A4-D8E9-4282-94FF-121809B48F1F}.Debug|ARM64.Build.0 = Debug|ARM64 - {84DBA1A4-D8E9-4282-94FF-121809B48F1F}.Debug|ARM64.Deploy.0 = Debug|ARM64 - {84DBA1A4-D8E9-4282-94FF-121809B48F1F}.Debug|x64.ActiveCfg = Debug|x64 - {84DBA1A4-D8E9-4282-94FF-121809B48F1F}.Debug|x64.Build.0 = Debug|x64 - {84DBA1A4-D8E9-4282-94FF-121809B48F1F}.Debug|x64.Deploy.0 = Debug|x64 - {84DBA1A4-D8E9-4282-94FF-121809B48F1F}.Debug|x86.ActiveCfg = Debug|x86 - {84DBA1A4-D8E9-4282-94FF-121809B48F1F}.Debug|x86.Build.0 = Debug|x86 - {84DBA1A4-D8E9-4282-94FF-121809B48F1F}.Debug|x86.Deploy.0 = Debug|x86 - {84DBA1A4-D8E9-4282-94FF-121809B48F1F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {84DBA1A4-D8E9-4282-94FF-121809B48F1F}.Release|Any CPU.Build.0 = Release|Any CPU - {84DBA1A4-D8E9-4282-94FF-121809B48F1F}.Release|Any CPU.Deploy.0 = Release|Any CPU - {84DBA1A4-D8E9-4282-94FF-121809B48F1F}.Release|ARM.ActiveCfg = Release|ARM - {84DBA1A4-D8E9-4282-94FF-121809B48F1F}.Release|ARM.Build.0 = Release|ARM - {84DBA1A4-D8E9-4282-94FF-121809B48F1F}.Release|ARM.Deploy.0 = Release|ARM - {84DBA1A4-D8E9-4282-94FF-121809B48F1F}.Release|ARM64.ActiveCfg = Release|ARM64 - {84DBA1A4-D8E9-4282-94FF-121809B48F1F}.Release|ARM64.Build.0 = Release|ARM64 - {84DBA1A4-D8E9-4282-94FF-121809B48F1F}.Release|ARM64.Deploy.0 = Release|ARM64 - {84DBA1A4-D8E9-4282-94FF-121809B48F1F}.Release|x64.ActiveCfg = Release|x64 - {84DBA1A4-D8E9-4282-94FF-121809B48F1F}.Release|x64.Build.0 = Release|x64 - {84DBA1A4-D8E9-4282-94FF-121809B48F1F}.Release|x64.Deploy.0 = Release|x64 - {84DBA1A4-D8E9-4282-94FF-121809B48F1F}.Release|x86.ActiveCfg = Release|x86 - {84DBA1A4-D8E9-4282-94FF-121809B48F1F}.Release|x86.Build.0 = Release|x86 - {84DBA1A4-D8E9-4282-94FF-121809B48F1F}.Release|x86.Deploy.0 = Release|x86 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/ziti.ico b/ziti.ico new file mode 100644 index 0000000000000000000000000000000000000000..141e513d9cb6798652607a400debf101c0f8cbf7 GIT binary patch literal 43851 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 VEu=RKC~JiAT188__N7(${{WTZr4s-E literal 0 HcmV?d00001