package gui import ( "archive/zip" "errors" "fmt" "io" "io/ioutil" "os" "os/exec" "path/filepath" "regexp" "sort" "strconv" "strings" "time" "github.com/ungerik/go-dry" "golang.zx2c4.com/wireguard/wgctrl/wgtypes" "github.com/UnnoTed/wireguird/gui/get" "github.com/UnnoTed/wireguird/settings" "github.com/dustin/go-humanize" "github.com/gotk3/gotk3/gdk" "github.com/gotk3/gotk3/glib" "github.com/gotk3/gotk3/gtk" "github.com/rs/zerolog/log" "gopkg.in/ini.v1" ) var ( Connected = false Settings = &settings.Settings{} ) func init() { if err := Settings.Init(); err != nil { log.Error().Err(err).Msg("Error on settings init") } } type Tunnels struct { Interface struct { Status *gtk.Label PublicKey *gtk.Label ListenPort *gtk.Label Addresses *gtk.Label DNS *gtk.Label } Peer struct { PublicKey *gtk.Label AllowedIPs *gtk.Label Endpoint *gtk.Label LatestHandshake *gtk.Label Transfer *gtk.Label } Settings struct { MultipleTunnels *gtk.CheckButton StartOnTray *gtk.CheckButton CheckUpdates *gtk.CheckButton } ButtonChangeState *gtk.Button icons map[string]*gtk.Image ticker *time.Ticker lastSelected string } func (t *Tunnels) Create() error { t.icons = map[string]*gtk.Image{} t.ticker = time.NewTicker(1 * time.Second) tl, err := get.ListBox("tunnel_list") if err != nil { return err } if err := t.ScanTunnels(); err != nil { return err } window.Connect("key-press-event", func(win *gtk.ApplicationWindow, ev *gdk.Event) { keyEvent := &gdk.EventKey{ev} if keyEvent.KeyVal() == gdk.KEY_F5 { wlog("INFO", "Scanning tunnels from keyboard command (F5)") if err := t.ScanTunnels(); err != nil { wlog("ERROR", err.Error()) } } }) // menu { mb, err := get.MenuButton("menu") if err != nil { return err } menu, err := gtk.MenuNew() if err != nil { return err } mSettings, err := gtk.MenuItemNew() if err != nil { return err } mSettings.SetLabel("Settings") //mSettings.SetSensitive(false) mSettings.Connect("activate", func() { settingsWindow.ShowAll() }) menu.Append(mSettings) mVersion, err := gtk.MenuItemNew() if err != nil { return err } mVersion.SetLabel("VERSION: v" + Version) //mVersion.SetSensitive(false) mVersion.Connect("activate", func() { if err := exec.Command("xdg-open", Repo).Start(); err != nil { ShowError(window, err, "open repo url error") } }) menu.Append(mVersion) menu.SetHAlign(gtk.ALIGN_CENTER) menu.SetVAlign(gtk.ALIGN_CENTER) menu.ShowAll() mb.SetPopup(menu) } t.Interface.Status, err = get.Label("label_interface_status") if err != nil { return err } t.Interface.PublicKey, err = get.Label("label_interface_public_key") if err != nil { return err } t.Interface.ListenPort, err = get.Label("label_interface_listen_port") if err != nil { return err } t.Interface.Addresses, err = get.Label("label_interface_addresses") if err != nil { return err } t.Interface.DNS, err = get.Label("label_interface_dns_servers") if err != nil { return err } t.Peer.PublicKey, err = get.Label("label_peer_public_key") if err != nil { return err } t.Peer.AllowedIPs, err = get.Label("label_peer_allowed_ips") if err != nil { return err } t.Peer.Endpoint, err = get.Label("label_peer_endpoint") if err != nil { return err } t.Peer.LatestHandshake, err = get.Label("label_peer_latest_handshake") if err != nil { return err } t.Peer.Transfer, err = get.Label("label_peer_transfer") if err != nil { return err } t.ButtonChangeState, err = get.Button("button_change_state") if err != nil { return err } t.ButtonChangeState.Connect("clicked", func() { err := func() error { list, err := wgc.Devices() if err != nil { return err } activeNames := t.ActiveDeviceName() row := tl.GetSelectedRow() // row not found for config if row == nil { return nil } // conf name name, err := row.GetName() if err != nil { return err } // https://github.com/UnnoTed/wireguird/issues/11#issuecomment-1332047191 if len(name) >= 16 { ShowError(window, errors.New("Tunnel's file name is too long ("+strconv.Itoa(len(name))+"), max length: 15")) } // disconnect from given tunnel dc := func(d *wgtypes.Device) error { gray, err := gtk.ImageNewFromFile(IconPath + "not_connected.png") if err != nil { return err } glib.IdleAdd(func() { t.icons[d.Name].SetFromPixbuf(gray.GetPixbuf()) }) c := exec.Command("wg-quick", "down", d.Name) output, err := c.Output() if err != nil { es := string(err.(*exec.ExitError).Stderr) log.Error().Err(err).Str("output", string(output)).Str("error", es).Msg("wg-quick down error") oerr := err.Error() + "\nwg-quick's output:\n" + es wlog("ERROR", oerr) return errors.New(oerr) } indicator.SetIcon("wireguard_off") return wlog("INFO", "Disconnected from "+d.Name) } // disconnects from all tunnels before connecting to a new one // when the multipleTunnels option is disabled if Settings.MultipleTunnels { log.Info().Str("name", name).Msg("NAME") d, err := wgc.Device(name) if err != nil && !errors.Is(err, os.ErrNotExist) { return err } if !errors.Is(err, os.ErrNotExist) { if err := dc(d); err != nil { return err } } } else { for _, d := range list { if err := dc(d); err != nil { return err } } } // dont connect to the new one as this is a disconnect action if dry.StringListContains(activeNames, name) { t.UpdateRow(row) glib.IdleAdd(func() { if len(activeNames) == 1 { header.SetSubtitle("Not connected!") } else { activeNames := t.ActiveDeviceName() header.SetSubtitle("Connected to " + strings.Join(activeNames, ", ")) } }) return nil } // connect to a tunnel c := exec.Command("wg-quick", "up", name) output, err := c.Output() if err != nil { es := string(err.(*exec.ExitError).Stderr) log.Error().Err(err).Str("output", string(output)).Str("error", es).Msg("wg-quick up error") oerr := err.Error() + "\nwg-quick's output:\n" + es wlog("ERROR", oerr) return errors.New(oerr) } if err != nil { return err } // update header label with tunnel names glib.IdleAdd(func() { activeNames := t.ActiveDeviceName() header.SetSubtitle("Connected to " + strings.Join(activeNames, ", ")) }) green, err := gtk.ImageNewFromFile(IconPath + "connected.png") if err != nil { return err } // set icon to connected for the tunnel's row glib.IdleAdd(func() { t.icons[name].SetFromPixbuf(green.GetPixbuf()) t.UpdateRow(row) indicator.SetIcon("wg_connected") }) if err := wlog("INFO", "Connected to "+name); err != nil { return err } return nil }() if err != nil { ShowError(window, err) } }) // boxPeers, err := get.Box("box_peers") // if err != nil { // return err // } tl.Connect("row-activated", func(l *gtk.ListBox, row *gtk.ListBoxRow) { t.UpdateRow(row) }) // button: add tunnel btnAddTunnel, err := get.Button("button_add_tunnel") if err != nil { return err } btnAddTunnel.Connect("clicked", func() { err := func() error { log.Print("btn add tunnel") dialog, err := gtk.FileChooserNativeDialogNew("Wireguird - Choose tunnel files (*.conf, *.zip)", window, gtk.FILE_CHOOSER_ACTION_OPEN, "OK", "Cancel") if err != nil { return err } defer dialog.Destroy() // filter *.conf and *.zip files filter, err := gtk.FileFilterNew() if err != nil { return err } filter.AddPattern("*.conf") filter.AddPattern("*.zip") filter.SetName("*.conf / *.zip") dialog.AddFilter(filter) dialog.SetSelectMultiple(true) res := dialog.Run() if gtk.ResponseType(res) == gtk.RESPONSE_ACCEPT { list, err := dialog.GetFilenames() if err != nil { return err } for _, fname := range list { // if file is zip archive if strings.HasSuffix(fname, ".zip") { err := parseZipArchive(fname) if err != nil { return err } continue } data, err := ioutil.ReadFile(fname) if err != nil { return err } err = ioutil.WriteFile(filepath.Join(TunnelsPath, filepath.Base(fname)), data, 666) if err != nil { return err } } if err := t.ScanTunnels(); err != nil { return err } } return nil }() if err != nil { ShowError(window, err, "add tunnel error") } }) // button: delete tunnel btnDelTunnel, err := get.Button("button_del_tunnel") if err != nil { return err } btnDelTunnel.Connect("clicked", func() { err := func() error { row := tl.GetSelectedRow() if row == nil { return errors.New("No row selected.") } name, err := row.GetName() if err != nil { return err } ext := "" if !strings.HasSuffix(name, ".conf") { ext = ".conf" } path := filepath.Join(TunnelsPath, name+ext) d := gtk.MessageDialogNew(window, gtk.DIALOG_MODAL, gtk.MESSAGE_QUESTION, gtk.BUTTONS_YES_NO, "Do you really want to delete "+name+"?") defer d.Destroy() res := d.Run() if res == gtk.RESPONSE_YES { if err := os.Remove(path); err != nil { return err } if err := t.ScanTunnels(); err != nil { return err } return nil } return nil }() if err != nil { ShowError(window, err, "tunnel delete error") } }) // button: zip tunnels btnZipTunnels, err := get.Button("button_zip_tunnel") if err != nil { return err } btnZipTunnels.Connect("clicked", func() { err := func() error { d, err := gtk.FileChooserDialogNewWith2Buttons("Wireguird - zip tunnels -> Save zip file", window, gtk.FILE_CHOOSER_ACTION_SAVE, "Cancel", gtk.RESPONSE_CANCEL, "Save", gtk.RESPONSE_ACCEPT) if err != nil { panic(err) } defer d.Destroy() d.SetDoOverwriteConfirmation(true) t := time.Now() d.SetCurrentName(fmt.Sprint("wg_tunnels_", t.Day(), "_", t.Month(), "_", t.Year(), ".zip")) res := d.Run() if res == gtk.RESPONSE_ACCEPT { dest := d.GetFilename() base := strings.TrimSuffix(filepath.Base(dest), filepath.Ext(dest)) zf, err := os.Create(dest) if err != nil { return err } defer zf.Close() archive := zip.NewWriter(zf) defer archive.Close() return filepath.Walk(TunnelsPath, func(path string, info os.FileInfo, err error) error { if err != nil { return err } header, err := zip.FileInfoHeader(info) if err != nil { return err } header.Name = filepath.Join(base, strings.TrimPrefix(path, TunnelsPath)) if info.IsDir() { header.Name += "/" } else { header.Method = zip.Deflate header.SetMode(0777) } log.Debug().Interface("header", header).Interface("info", info).Msg("walk") writer, err := archive.CreateHeader(header) if err != nil { return err } if info.IsDir() { return nil } f, err := os.Open(path) if err != nil { return err } defer f.Close() if _, err = io.Copy(writer, f); err != nil { return err } return nil }) } return nil }() if err != nil { ShowError(window, err, "tunnel delete error") } }) // stores a modified state for the editor modified := false editorWindow.Connect("hide", func() { if err := t.ScanTunnels(); err != nil { ShowError(window, err, "scan tunnel after closing editor window error") } modified = false }) ne, err := get.Entry("editor_name") if err != nil { return err } ne.Connect("changed", func() { modified = true }) et, err := get.TextView("editor_text") if err != nil { return err } etb, err := et.GetBuffer() if err != nil { return err } etb.Connect("changed", func() { modified = true }) // button: edit tunnel btnEditTunnel, err := get.Button("button_edit_tunnel") if err != nil { return err } btnEditTunnel.Connect("clicked", func() { modified = false err := func() error { row := tl.GetSelectedRow() if row == nil { return nil } name, err := row.GetName() if err != nil { return err } ext := "" if !strings.HasSuffix(name, ".conf") { ext = ".conf" } path := filepath.Join(TunnelsPath, name+ext) log.Print(path) ew, err := createEditor(name, true) if err != nil { return err } et, err := get.TextView("editor_text") if err != nil { return err } data, err := ioutil.ReadFile(path) if err != nil { return err } // low budget GtkSourceView propertyColor := "purple" sectionColor := "green" conf := string(data) // gets public key to use in the PublicKey entry re := regexp.MustCompile(`(?m)PublicKey[\s]+=[\s]+(.*)`) m := re.FindStringSubmatch(conf) if len(m) >= 2 { epk, err := get.Entry("editor_publickey") if err != nil { return err } epk.SetText(m[1]) } r := strings.NewReplacer( "&", "&", "PrivateKey", "PrivateKey", "PublicKey", "PublicKey", "Address", "Address", "DNS", "DNS", "AllowedIPs", "AllowedIPs", "Endpoint", "Endpoint", "PostUp", "PostUp", "PreDown", "PreDown", "PreSharedKey", "PreSharedKey", "PersistentKeepalive", "PersistentKeepalive", "[Interface]", "[Interface]", "[Peer]", "[Peer]", ) conf = r.Replace(conf) ttt, err := gtk.TextTagTableNew() if err != nil { return err } tb, err := gtk.TextBufferNew(ttt) if err != nil { return err } tb.InsertMarkup(tb.GetStartIter(), conf) et.SetBuffer(tb) ew.ShowAll() return nil }() if err != nil { ShowError(window, err, "edit tunnel error") } }) // button: editor's cancel btnEditorCancel, err := get.Button("editor_button_cancel") if err != nil { return err } btnEditorCancel.Connect("clicked", func() { if !modified { editorWindow.Close() return } err := func() error { d := gtk.MessageDialogNew(editorWindow, gtk.DIALOG_MODAL, gtk.MESSAGE_QUESTION, gtk.BUTTONS_YES_NO, "Do you want to cancel any modification done to the tunnel?") defer d.Destroy() res := d.Run() if res == gtk.RESPONSE_YES { modified = false editorWindow.Close() return nil } return nil }() if err != nil { ShowError(window, err, "cancel tunnel error") } }) // button: editor's save btnEditorSave, err := get.Button("editor_button_save") if err != nil { return err } btnEditorSave.Connect("clicked", func() { err := func() error { row := tl.GetSelectedRow() if row == nil { return nil } name, err := row.GetName() if err != nil { return err } // adds extension when empty ext := "" if !strings.HasSuffix(name, ".conf") { ext = ".conf" } path := filepath.Join(TunnelsPath, name+ext) // get the new tunnel name nameEntry, err := get.Entry("editor_name") if err != nil { return err } newName, err := nameEntry.GetText() if err != nil { return err } newName = strings.TrimSpace(newName) // rename the tunnel if name != newName { newPath := filepath.Join(TunnelsPath, newName+ext) if err := os.Rename(path, newPath); err != nil { return err } path = newPath } // get the tunnels' edited text through editor_text's buffer etxt, err := get.TextView("editor_text") if err != nil { return err } b, err := etxt.GetBuffer() if err != nil { return err } data, err := b.GetText(b.GetStartIter(), b.GetEndIter(), false) if err != nil { return err } // write changes if err := ioutil.WriteFile(path, []byte(data), 666); err != nil { return err } modified = false if err := t.ScanTunnels(); err != nil { return err } editorWindow.Close() return nil }() if err != nil { ShowError(window, err, "save tunnel error") } }) ////////////// // Settings // button: settings save btnSettingsSave, err := get.Button("button_settings_save") if err != nil { return err } btnSettingsSave.Connect("clicked", func() { err := func() error { t.ToSettings() Settings.Save() settingsWindow.Close() return nil }() if err != nil { ShowError(window, err, "settings save error") } }) // button: settings cancel btnSettingsCancel, err := get.Button("button_settings_cancel") if err != nil { return err } btnSettingsCancel.Connect("clicked", func() { err := func() error { Settings.Init() settingsWindow.Close() return nil }() if err != nil { ShowError(window, err, "settings cancel error") } }) if err := t.FromSettings(); err != nil { return err } go func() { for { <-t.ticker.C if !window.HasToplevelFocus() { continue } row := tl.GetSelectedRow() if row == nil { t.UnknownLabels() continue } name, err := row.GetName() if err != nil { log.Error().Err(err).Msg("row get name err") continue } if !dry.StringListContains(t.ActiveDeviceName(), name) { continue } d, err := wgc.Device(name) if err != nil { log.Error().Err(err).Msg("wgc get device err") continue } t.Interface.PublicKey.SetText(d.PublicKey.String()) t.Interface.ListenPort.SetText(strconv.Itoa(d.ListenPort)) for _, p := range d.Peers { hs := humanize.Time(p.LastHandshakeTime) glib.IdleAdd(func() { t.Peer.LatestHandshake.SetText(hs) t.Peer.Transfer.SetText(humanize.Bytes(uint64(p.ReceiveBytes)) + " received, " + humanize.Bytes(uint64(p.TransmitBytes)) + " sent") }) } } }() return nil } func (t *Tunnels) ToSettings() { Settings.MultipleTunnels = t.Settings.MultipleTunnels.GetActive() Settings.StartOnTray = t.Settings.StartOnTray.GetActive() Settings.CheckUpdates = t.Settings.CheckUpdates.GetActive() } func (t *Tunnels) FromSettings() error { log.Debug().Interface("settings", Settings).Msg("tunnel.FromSettings()") var err error // checkbox: multiple tunnels t.Settings.MultipleTunnels, err = get.CheckButton("settings_multiple_active_tunnels") if err != nil { return err } t.Settings.MultipleTunnels.SetActive(Settings.MultipleTunnels) // checkbox: start on tray t.Settings.StartOnTray, err = get.CheckButton("settings_start_tray") if err != nil { return err } t.Settings.StartOnTray.SetActive(Settings.StartOnTray) // checkbox: check updates t.Settings.CheckUpdates, err = get.CheckButton("settings_check_updates") if err != nil { return err } t.Settings.CheckUpdates.SetActive(Settings.CheckUpdates) return nil } func (t *Tunnels) UpdateRow(row *gtk.ListBoxRow) { err := func() error { ds, err := wgc.Devices() if err != nil { return err } log.Debug().Interface("list", ds).Msg("devices") id, err := row.GetName() if err != nil { return err } cfg, err := ini.Load(TunnelsPath + id + ".conf") if err != nil { return err } t.lastSelected = id t.UnknownLabels() peersec := cfg.Section("Peer") insec := cfg.Section("Interface") glib.IdleAdd(func() { t.Interface.Addresses.SetText(insec.Key("Address").String()) t.Interface.Status.SetText("Inactive") t.Interface.DNS.SetText(insec.Key("DNS").String()) t.ButtonChangeState.SetLabel("Activate") t.Peer.AllowedIPs.SetText(peersec.Key("AllowedIPs").String()) t.Peer.PublicKey.SetText(peersec.Key("PublicKey").String()) t.Peer.Endpoint.SetText(peersec.Key("Endpoint").String()) }) for _, d := range ds { if d.Name != id { continue } // i'll do this later // _ = boxPeers // boxPeers.GetChildren().Foreach(func(item interface{}) { // boxPeers.Remove(item.(*gtk.Widget)) // }) glib.IdleAdd(func() { t.Interface.Status.SetText("Active") t.ButtonChangeState.SetLabel("Deactivate") t.Interface.PublicKey.SetText(d.PublicKey.String()) t.Interface.ListenPort.SetText(strconv.Itoa(d.ListenPort)) }) for _, p := range d.Peers { hs := humanize.Time(p.LastHandshakeTime) glib.IdleAdd(func() { t.Peer.LatestHandshake.SetText(hs) t.Peer.Transfer.SetText(humanize.Bytes(uint64(p.ReceiveBytes)) + " received, " + humanize.Bytes(uint64(p.TransmitBytes)) + " sent") }) } break } return nil }() if err != nil { log.Error().Err(err).Msg("row activated") ShowError(window, err) } } func (t *Tunnels) ScanTunnels() error { var err error var configList []string list, err := dry.ListDirFiles(TunnelsPath) if err != nil { // showError(err) return err } for _, fileName := range list { if !strings.HasSuffix(fileName, ".conf") { continue } configList = append(configList, strings.TrimSuffix(fileName, ".conf")) } tl, err := get.ListBox("tunnel_list") if err != nil { return err } for { row := tl.GetRowAtIndex(0) if row == nil { break } row.Destroy() } sort.Slice(configList, func(a, b int) bool { return configList[a] < configList[b] }) activeNames := t.ActiveDeviceName() header.SetSubtitle("Connected to " + strings.Join(activeNames, ", ")) lasti := len(configList) - 1 for i, name := range configList { row, err := gtk.ListBoxRowNew() if err != nil { return err } row.SetName(name) row.SetMarginStart(8) row.SetMarginEnd(8) if i == 0 { row.SetMarginTop(8) } else if i == lasti { row.SetMarginBottom(8) } var img *gtk.Image if dry.StringListContains(activeNames, name) { green, err := gtk.ImageNewFromFile(IconPath + "connected.png") if err != nil { return err } img = green } else { gray, err := gtk.ImageNewFromFile(IconPath + "not_connected.png") if err != nil { return err } img = gray } t.icons[name] = img img.SetVAlign(gtk.ALIGN_CENTER) img.SetHAlign(gtk.ALIGN_START) img.SetSizeRequest(10, 10) img.SetVExpand(false) img.SetHExpand(false) label, err := gtk.LabelNew(name) if err != nil { return err } label.SetHAlign(gtk.ALIGN_START) label.SetMarginStart(8) label.SetMarginEnd(8) label.SetMarginTop(8) label.SetMarginBottom(8) box, err := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 4) if err != nil { return err } box.Add(img) box.Add(label) row.SetName(name) row.Add(box) row.ShowAll() tl.Insert(row, -1) if name == t.lastSelected { tl.SelectRow(row) t.UpdateRow(row) } } if t.lastSelected == "" { row := tl.GetRowAtIndex(0) if row != nil { tl.SelectRow(row) t.UpdateRow(row) } } return nil } func (t *Tunnels) UnknownLabels() { glib.IdleAdd(func() { t.ButtonChangeState.SetLabel("unknown") t.Interface.Addresses.SetText("unknown") t.Interface.Status.SetText("unknown") t.Interface.DNS.SetText("unknown") t.Peer.AllowedIPs.SetText("unknown") t.Peer.PublicKey.SetText("unknown") t.Peer.Endpoint.SetText("unknown") t.Interface.Status.SetText("unknown") t.ButtonChangeState.SetLabel("unknown") t.Interface.PublicKey.SetText("unknown") t.Interface.ListenPort.SetText("unknown") t.Peer.LatestHandshake.SetText("unknown") t.Peer.Transfer.SetText("unknown") }) } func (t *Tunnels) ActiveDeviceName() []string { ds, _ := wgc.Devices() var names []string for _, d := range ds { names = append(names, d.Name) } return names } func wlog(t string, text string) error { wlogs, err := get.ListBox("wireguard_logs") if err != nil { return err } l, err := gtk.LabelNew("") if err != nil { return err } if t == "ERROR" { t = `` + t + "" } l.SetMarkup(`[` + time.Now().Format("02/Jan/06 15:04:05 MST") + `][` + t + `]: ` + text) l.SetHExpand(true) l.SetHAlign(gtk.ALIGN_START) glib.IdleAdd(func() { l.Show() wlogs.Add(l) }) return nil } func parseZipArchive(target string) error { rc, err := zip.OpenReader(target) if err != nil { return err } defer rc.Close() for _, f := range rc.File { // parse only .conf files from zip archive if !f.FileInfo().IsDir() && strings.HasSuffix(f.FileInfo().Name(), ".conf") { fr, err := f.Open() if err != nil { return err } defer fr.Close() data, err := ioutil.ReadAll(fr) if err != nil { return err } err = ioutil.WriteFile(filepath.Join(TunnelsPath, f.FileInfo().Name()), data, 666) if err != nil { return err } } } return nil }