diff --git a/README.md b/README.md
index 64b76b2..9fc6455 100644
--- a/README.md
+++ b/README.md
@@ -1,18 +1,44 @@
# GOST-PLUS
-A cross-platform GUI client for [GOST.PLUS](https://gost.plus) built with [gioui](https://gioui.org).
+A cross-platform GUI client for GOST.PLUS built with [gioui](https://gioui.org).
+
+## Features
+
+### File Tunnel
+
+Expose local files to the public network.
+
+
+
+### HTTP Tunnel
+
+Expose local HTTP service to the public network.
+
+
+
+### TCP Tunnel
+
+Expose local TCP service to the public network.
+
+
+
+### UDP Tunnel
+
+Expose local UDP service to the public network.
+
+
## Screenshot
### Desktop
-
-
-
-
+
+
+
+
### Mobile
-
-
-
\ No newline at end of file
+
+
+
\ No newline at end of file
diff --git a/img/add-android.png b/assets/add-android.png
similarity index 100%
rename from img/add-android.png
rename to assets/add-android.png
diff --git a/img/add.png b/assets/add.png
similarity index 100%
rename from img/add.png
rename to assets/add.png
diff --git a/img/edit-android.png b/assets/edit-android.png
similarity index 100%
rename from img/edit-android.png
rename to assets/edit-android.png
diff --git a/img/edit.png b/assets/edit.png
similarity index 100%
rename from img/edit.png
rename to assets/edit.png
diff --git a/assets/file-tunnel.gif b/assets/file-tunnel.gif
new file mode 100644
index 0000000..3cb7854
Binary files /dev/null and b/assets/file-tunnel.gif differ
diff --git a/assets/http-tunnel.gif b/assets/http-tunnel.gif
new file mode 100644
index 0000000..738e9cd
Binary files /dev/null and b/assets/http-tunnel.gif differ
diff --git a/img/list-android.png b/assets/list-android.png
similarity index 100%
rename from img/list-android.png
rename to assets/list-android.png
diff --git a/img/list.png b/assets/list.png
similarity index 100%
rename from img/list.png
rename to assets/list.png
diff --git a/img/menu.png b/assets/menu.png
similarity index 100%
rename from img/menu.png
rename to assets/menu.png
diff --git a/assets/tcp-tunnel.gif b/assets/tcp-tunnel.gif
new file mode 100644
index 0000000..7e71854
Binary files /dev/null and b/assets/tcp-tunnel.gif differ
diff --git a/assets/udp-tunnel.gif b/assets/udp-tunnel.gif
new file mode 100644
index 0000000..e5d62f3
Binary files /dev/null and b/assets/udp-tunnel.gif differ
diff --git a/config/config.go b/config/config.go
index c72a66b..99407fe 100644
--- a/config/config.go
+++ b/config/config.go
@@ -101,6 +101,8 @@ type Tunnel struct {
Username string `yaml:",omitempty"`
Password string `yaml:",omitempty"`
EnableTLS bool `yaml:"enableTLS,omitempty"`
+ Keepalive bool `yaml:",omitempty"`
+ TTL int `yaml:"ttl,omitempty"`
Favorite bool
Closed bool
diff --git a/go.mod b/go.mod
index 8aa5de1..5b1dbde 100644
--- a/go.mod
+++ b/go.mod
@@ -6,7 +6,7 @@ require (
gioui.org v0.4.1
gioui.org/x v0.4.0
github.com/go-gost/core v0.0.0-20231119081403-abc73f2ca2b7
- github.com/go-gost/x v0.0.0-20231130113937-b1390dda1cc8
+ github.com/go-gost/x v0.0.0-20231216062858-f847fa533e98
github.com/google/uuid v1.4.0
github.com/spf13/viper v1.18.1
golang.org/x/exp/shiny v0.0.0-20231206192017-f3f8817b8deb
diff --git a/go.sum b/go.sum
index 4db3a8c..a773229 100644
--- a/go.sum
+++ b/go.sum
@@ -35,6 +35,8 @@ github.com/go-gost/tls-dissector v0.0.2-0.20220408131628-aac992c27451 h1:xj8gUZG
github.com/go-gost/tls-dissector v0.0.2-0.20220408131628-aac992c27451/go.mod h1:/9QfdewqmHdaE362Hv5nDaSWLx3pCmtD870d6GaquXs=
github.com/go-gost/x v0.0.0-20231130113937-b1390dda1cc8 h1:aM2tAfTg4+oBiB161GrYbRBhUHQM1CYNiDNYsqoVAlE=
github.com/go-gost/x v0.0.0-20231130113937-b1390dda1cc8/go.mod h1:YaeMQsu+I8Q3bxPFeo5MbnmLsAdbGUxxG3A4mvUt2YE=
+github.com/go-gost/x v0.0.0-20231216062858-f847fa533e98 h1:dFjwqp6SUJhDlFOBeBZXn5b2DqRjdbyBt+6w40+6cR8=
+github.com/go-gost/x v0.0.0-20231216062858-f847fa533e98/go.mod h1:YaeMQsu+I8Q3bxPFeo5MbnmLsAdbGUxxG3A4mvUt2YE=
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
github.com/go-text/typesetting v0.0.0-20230803102845-24e03d8b5372 h1:FQivqchis6bE2/9uF70M2gmmLpe82esEm2QadL0TEJo=
diff --git a/tunnel/entrypoint/entrypoint.go b/tunnel/entrypoint/entrypoint.go
index 12e106e..b3d4342 100644
--- a/tunnel/entrypoint/entrypoint.go
+++ b/tunnel/entrypoint/entrypoint.go
@@ -97,32 +97,34 @@ func Delete(id string) {
}
func LoadConfig() {
- for _, ep := range config.Global().EntryPoints {
- if ep == nil {
+ for _, cfg := range config.Global().EntryPoints {
+ if cfg == nil {
continue
}
- s := createEntryPoint(ep.Type, tunnel.Options{
- ID: ep.ID,
- Name: ep.Name,
- Endpoint: ep.Endpoint,
- Hostname: ep.Hostname,
- Username: ep.Username,
- Password: ep.Password,
- EnableTLS: ep.EnableTLS,
+ ep := createEntryPoint(cfg.Type, tunnel.Options{
+ ID: cfg.ID,
+ Name: cfg.Name,
+ Endpoint: cfg.Endpoint,
+ Hostname: cfg.Hostname,
+ Username: cfg.Username,
+ Password: cfg.Password,
+ EnableTLS: cfg.EnableTLS,
+ Keepalive: cfg.Keepalive,
+ TTL: cfg.TTL,
})
- if s == nil {
+ if ep == nil {
continue
}
- if ep.Closed {
- s.Close()
+ if cfg.Closed {
+ ep.Close()
} else {
- s.Run()
+ ep.Run()
}
- s.Favorite(ep.Favorite)
- Add(s)
+ ep.Favorite(cfg.Favorite)
+ Add(ep)
}
}
@@ -174,8 +176,8 @@ func createEntryPoint(st string, opts tunnel.Options) EntryPoint {
switch st {
case TCPEntryPoint:
return NewTCPEntryPoint(options...)
- // case UDPEntryPoint:
- // return NewU(options...)
+ case UDPEntryPoint:
+ return NewUDPEntryPoint(options...)
default:
return nil
}
diff --git a/tunnel/entrypoint/udp.go b/tunnel/entrypoint/udp.go
index 5543291..f5e9faa 100644
--- a/tunnel/entrypoint/udp.go
+++ b/tunnel/entrypoint/udp.go
@@ -6,6 +6,7 @@ import (
"fmt"
"sync"
"sync/atomic"
+ "time"
"github.com/go-gost/core/chain"
"github.com/go-gost/core/handler"
@@ -17,7 +18,7 @@ import (
chain_parser "github.com/go-gost/x/config/parsing/chain"
"github.com/go-gost/x/handler/forward/local"
"github.com/go-gost/x/hop"
- "github.com/go-gost/x/listener/tcp"
+ "github.com/go-gost/x/listener/udp"
mdx "github.com/go-gost/x/metadata"
xservice "github.com/go-gost/x/service"
"github.com/google/uuid"
@@ -108,6 +109,10 @@ func (s *udpEntryPoint) init() error {
},
Listener: &config.ListenerConfig{
Type: "udp",
+ Metadata: map[string]any{
+ "keepalive": s.opts.Keepalive,
+ "ttl": time.Duration(s.opts.TTL) * time.Second,
+ },
},
Forwarder: &config.ForwarderConfig{
Nodes: []*config.ForwardNodeConfig{
@@ -153,7 +158,7 @@ func (s *udpEntryPoint) Run() (err error) {
}
cfg := s.config.Services[0]
- ln := tcp.NewListener(
+ ln := udp.NewListener(
listener.AddrOption(cfg.Addr),
listener.LoggerOption(log.WithFields(map[string]any{"kind": "listener", "listener": "udp"})),
)
diff --git a/tunnel/tunnel.go b/tunnel/tunnel.go
index ee9fe8c..cf3e785 100644
--- a/tunnel/tunnel.go
+++ b/tunnel/tunnel.go
@@ -36,6 +36,8 @@ type Options struct {
Username string
Password string
EnableTLS bool
+ Keepalive bool
+ TTL int
}
type Option func(opts *Options)
@@ -82,6 +84,18 @@ func EnableTLSOption(b bool) Option {
}
}
+func KeepaliveOption(b bool) Option {
+ return func(opts *Options) {
+ opts.Keepalive = b
+ }
+}
+
+func TTLOption(ttl int) Option {
+ return func(opts *Options) {
+ opts.TTL = ttl
+ }
+}
+
type Tunnel interface {
ID() string
Type() string
diff --git a/ui/icons/icons.go b/ui/icons/icons.go
index f31da71..3eae400 100644
--- a/ui/icons/icons.go
+++ b/ui/icons/icons.go
@@ -32,20 +32,22 @@ func init() {
var (
IconApp *widget.Image
- IconHome = mustIcon(icons.ActionHome)
- IconFavorite = mustIcon(icons.ActionFavorite)
- IconAdd = mustIcon(icons.ContentAdd)
- IconSettings = mustIcon(icons.ActionSettings)
- IconDone = mustIcon(icons.ActionDone)
- IconTunnelState = mustIcon(icons.ToggleRadioButtonChecked)
- IconForward = mustIcon(icons.NavigationChevronRight)
- IconEdit = mustIcon(icons.EditorModeEdit)
- IconDelete = mustIcon(icons.ActionDelete)
- IconStart = mustIcon(icons.AVPlayArrow)
- IconStop = mustIcon(icons.AVStop)
- IconBack = mustIcon(icons.NavigationArrowBack)
- IconClose = mustIcon(icons.ContentClear)
- IconCopy = mustIcon(icons.ContentContentCopy)
+ IconHome = mustIcon(icons.ActionHome)
+ IconFavorite = mustIcon(icons.ActionFavorite)
+ IconAdd = mustIcon(icons.ContentAdd)
+ IconSettings = mustIcon(icons.ActionSettings)
+ IconDone = mustIcon(icons.ActionDone)
+ IconTunnelState = mustIcon(icons.ToggleRadioButtonChecked)
+ IconForward = mustIcon(icons.NavigationChevronRight)
+ IconEdit = mustIcon(icons.EditorModeEdit)
+ IconDelete = mustIcon(icons.ActionDelete)
+ IconStart = mustIcon(icons.AVPlayArrow)
+ IconStop = mustIcon(icons.AVStop)
+ IconBack = mustIcon(icons.NavigationArrowBack)
+ IconClose = mustIcon(icons.ContentClear)
+ IconCopy = mustIcon(icons.ContentContentCopy)
+ IconVisibility = mustIcon(icons.ActionVisibility)
+ IconVisibilityOff = mustIcon(icons.ActionVisibilityOff)
)
func mustIcon(data []byte) *widget.Icon {
diff --git a/ui/page/entrypoint.go b/ui/page/entrypoint.go
index 62fb27f..4f428b4 100644
--- a/ui/page/entrypoint.go
+++ b/ui/page/entrypoint.go
@@ -117,6 +117,8 @@ func (p *entryPointPage) Layout(gtx C, th *material.Theme) D {
switch s.Type() {
case entrypoint.TCPEntryPoint:
p.router.SwitchTo(Route{Path: PageEditTCPEntryPoint, ID: s.ID()})
+ case entrypoint.UDPEntryPoint:
+ p.router.SwitchTo(Route{Path: PageEditUDPEntryPoint, ID: s.ID()})
}
op.InvalidateOp{}.Add(gtx.Ops)
}
diff --git a/ui/page/entrypoint_tcp.go b/ui/page/entrypoint_tcp.go
index a8484dc..a287de3 100644
--- a/ui/page/entrypoint_tcp.go
+++ b/ui/page/entrypoint_tcp.go
@@ -55,9 +55,9 @@ func NewTCPEntryPointAddPage(r *Router) Page {
}
func (p *tcpEntryPointAddPage) Init(opts ...PageOption) {
- p.name.SetText("")
- p.tunnelID.SetText("")
- p.addr.SetText("")
+ p.name.Clear()
+ p.tunnelID.Clear()
+ p.addr.Clear()
p.router.bar.SetActions(
[]component.AppBarAction{
@@ -67,6 +67,9 @@ func (p *tcpEntryPointAddPage) Init(opts ...PageOption) {
Tag: &p.wgDone,
},
Layout: func(gtx C, bg, fg color.NRGBA) D {
+ if !p.isValid() {
+ gtx = gtx.Disabled()
+ }
if p.wgDone.Clicked(gtx) {
defer p.router.SwitchTo(Route{Path: PageEntryPoint})
p.createEntryPoint()
@@ -81,6 +84,14 @@ func (p *tcpEntryPointAddPage) Init(opts ...PageOption) {
p.router.bar.NavigationIcon = icons.IconClose
}
+func (p *tcpEntryPointAddPage) isValid() bool {
+ if p.tunnelID.Text() == "" || p.tunnelID.IsErrored() ||
+ p.addr.Text() == "" || p.addr.IsErrored() {
+ return false
+ }
+ return true
+}
+
func (p *tcpEntryPointAddPage) Layout(gtx C, th *material.Theme) D {
return p.list.Layout(gtx, 1, func(gtx C, _ int) D {
return layout.Center.Layout(gtx, func(gtx C) D {
@@ -99,7 +110,7 @@ func (p *tcpEntryPointAddPage) layout(gtx C, th *material.Theme) D {
return layout.Flex{
Axis: layout.Vertical,
}.Layout(gtx,
- layout.Rigid(material.Body1(th, "Create an entrypoint to connect to the specified TCP tunnel").Layout),
+ layout.Rigid(material.Body1(th, "Create an entrypoint to the specified TCP tunnel").Layout),
layout.Rigid(layout.Spacer{Height: 10}.Layout),
layout.Rigid(func(gtx C) D {
return material.Body1(th, "Entrypoint name").Layout(gtx)
@@ -113,13 +124,17 @@ func (p *tcpEntryPointAddPage) layout(gtx C, th *material.Theme) D {
}),
layout.Rigid(func(gtx C) D {
if err := func() error {
- tid := strings.TrimSpace(p.tunnelID.Text())
+ tid := strings.ToLower(strings.TrimSpace(p.tunnelID.Text()))
if tid == "" {
return nil
}
if _, err := uuid.Parse(tid); err != nil {
return fmt.Errorf("invalid tunnel ID, should be a valid UUID")
}
+
+ if ep := entrypoint.Get(tid); ep != nil {
+ return fmt.Errorf("the entrypoint for this tunnel exists")
+ }
return nil
}(); err != nil {
p.tunnelID.SetError(err.Error())
@@ -155,16 +170,15 @@ func (p *tcpEntryPointAddPage) layout(gtx C, th *material.Theme) D {
}
func (p *tcpEntryPointAddPage) createEntryPoint() error {
- tun := entrypoint.NewTCPEntryPoint(
+ ep := entrypoint.NewTCPEntryPoint(
tunnel.NameOption(strings.TrimSpace(p.name.Text())),
tunnel.IDOption(strings.ToLower(strings.TrimSpace(p.tunnelID.Text()))),
tunnel.EndpointOption(strings.TrimSpace(p.addr.Text())),
)
- entrypoint.Add(tun)
+ entrypoint.Add(ep)
- if err := tun.Run(); err != nil {
- tun.Close()
+ if err := ep.Run(); err != nil {
return err
}
@@ -264,7 +278,12 @@ func (p *tcpEntryPointEditPage) Init(opts ...PageOption) {
s := entrypoint.Get(p.id)
if p.wgState.Clicked(gtx) && s != nil {
if s.IsClosed() {
- s = p.createEntryPoint()
+ opts := s.Options()
+ s = p.createEntryPoint(
+ tunnel.NameOption(opts.Name),
+ tunnel.IDOption(opts.ID),
+ tunnel.EndpointOption(opts.Endpoint),
+ )
} else {
s.Close()
}
@@ -298,6 +317,10 @@ func (p *tcpEntryPointEditPage) Init(opts ...PageOption) {
Tag: &p.wgDone,
},
Layout: func(gtx C, bg, fg color.NRGBA) D {
+ if !p.isValid() {
+ gtx = gtx.Disabled()
+ }
+
if p.wgDone.Clicked(gtx) {
defer p.router.SwitchTo(Route{Path: PageEntryPoint})
@@ -316,20 +339,31 @@ func (p *tcpEntryPointEditPage) Init(opts ...PageOption) {
p.router.bar.NavigationIcon = icons.IconClose
}
-func (p *tcpEntryPointEditPage) createEntryPoint() entrypoint.EntryPoint {
- s := entrypoint.NewTCPEntryPoint(
- tunnel.IDOption(p.id),
- tunnel.NameOption(strings.TrimSpace(p.name.Text())),
- tunnel.IDOption(strings.ToLower(strings.TrimSpace(p.tunnelID.Text()))),
- tunnel.EndpointOption(strings.TrimSpace(p.addr.Text())),
- )
+func (p *tcpEntryPointEditPage) isValid() bool {
+ if p.tunnelID.Text() == "" || p.tunnelID.IsErrored() ||
+ p.addr.Text() == "" || p.addr.IsErrored() {
+ return false
+ }
+ return true
+}
- if err := s.Run(); err != nil {
+func (p *tcpEntryPointEditPage) createEntryPoint(opts ...tunnel.Option) entrypoint.EntryPoint {
+ if opts == nil {
+ opts = []tunnel.Option{
+ tunnel.NameOption(strings.TrimSpace(p.name.Text())),
+ tunnel.IDOption(p.id),
+ tunnel.EndpointOption(strings.TrimSpace(p.addr.Text())),
+ }
+ }
+ ep := entrypoint.NewTCPEntryPoint(opts...)
+
+ entrypoint.Set(ep)
+
+ if err := ep.Run(); err != nil {
log.Println(err)
}
- entrypoint.Set(s)
- return s
+ return ep
}
func (p *tcpEntryPointEditPage) Layout(gtx C, th *material.Theme) D {
@@ -350,7 +384,6 @@ func (p *tcpEntryPointEditPage) layout(gtx C, th *material.Theme) D {
return layout.Flex{
Axis: layout.Vertical,
}.Layout(gtx,
- layout.Rigid(layout.Spacer{Height: 10}.Layout),
layout.Rigid(func(gtx C) D {
return material.Body1(th, "Entrypoint name").Layout(gtx)
}),
diff --git a/ui/page/entrypoint_udp.go b/ui/page/entrypoint_udp.go
new file mode 100644
index 0000000..323819f
--- /dev/null
+++ b/ui/page/entrypoint_udp.go
@@ -0,0 +1,535 @@
+package page
+
+import (
+ "fmt"
+ "image/color"
+ "log"
+ "net"
+ "strconv"
+ "strings"
+ "unicode"
+
+ "gioui.org/layout"
+ "gioui.org/widget"
+ "gioui.org/widget/material"
+ "gioui.org/x/component"
+ "github.com/go-gost/gost-plus/tunnel"
+ "github.com/go-gost/gost-plus/tunnel/entrypoint"
+ "github.com/go-gost/gost-plus/ui/icons"
+ "github.com/google/uuid"
+ "golang.org/x/exp/shiny/materialdesign/colornames"
+)
+
+type udpEntryPointAddPage struct {
+ router *Router
+
+ list layout.List
+ wgDone widget.Clickable
+
+ name component.TextField
+ tunnelID component.TextField
+ addr component.TextField
+
+ bKeepalive widget.Bool
+ ttl component.TextField
+}
+
+func NewUDPEntryPointAddPage(r *Router) Page {
+ return &udpEntryPointAddPage{
+ router: r,
+ list: layout.List{
+ Axis: layout.Vertical,
+ Alignment: layout.Middle,
+ },
+ name: component.TextField{
+ Editor: widget.Editor{
+ SingleLine: true,
+ },
+ },
+ tunnelID: component.TextField{
+ Editor: widget.Editor{
+ SingleLine: true,
+ },
+ },
+ addr: component.TextField{
+ Editor: widget.Editor{
+ SingleLine: true,
+ },
+ },
+ ttl: component.TextField{
+ Editor: widget.Editor{
+ SingleLine: true,
+ },
+ },
+ }
+}
+
+func (p *udpEntryPointAddPage) Init(opts ...PageOption) {
+ p.name.SetText("")
+ p.tunnelID.SetText("")
+ p.addr.SetText("")
+ p.bKeepalive.Value = false
+ p.ttl.SetText("")
+
+ p.router.bar.SetActions(
+ []component.AppBarAction{
+ {
+ OverflowAction: component.OverflowAction{
+ Name: "Create",
+ Tag: &p.wgDone,
+ },
+ Layout: func(gtx C, bg, fg color.NRGBA) D {
+ if !p.isValid() {
+ gtx = gtx.Disabled()
+ }
+ if p.wgDone.Clicked(gtx) {
+ defer p.router.SwitchTo(Route{Path: PageEntryPoint})
+ p.createEntryPoint()
+ entrypoint.SaveConfig()
+ }
+ return component.SimpleIconButton(bg, fg, &p.wgDone, icons.IconDone).Layout(gtx)
+ },
+ },
+ }, nil)
+
+ p.router.bar.Title = "UDP"
+ p.router.bar.NavigationIcon = icons.IconClose
+}
+
+func (p *udpEntryPointAddPage) isValid() bool {
+ if p.tunnelID.Text() == "" || p.tunnelID.IsErrored() ||
+ p.addr.Text() == "" || p.addr.IsErrored() {
+ return false
+ }
+ return true
+}
+
+func (p *udpEntryPointAddPage) Layout(gtx C, th *material.Theme) D {
+ return p.list.Layout(gtx, 1, func(gtx C, _ int) D {
+ return layout.Center.Layout(gtx, func(gtx C) D {
+ return layout.UniformInset(10).Layout(gtx, func(gtx C) D {
+ return component.Surface(th).Layout(gtx, func(gtx C) D {
+ return layout.UniformInset(10).Layout(gtx, func(gtx C) D {
+ return p.layout(gtx, th)
+ })
+ })
+ })
+ })
+ })
+}
+
+func (p *udpEntryPointAddPage) layout(gtx C, th *material.Theme) D {
+ return layout.Flex{
+ Axis: layout.Vertical,
+ }.Layout(gtx,
+ layout.Rigid(material.Body1(th, "Create an entrypoint to the specified UDP tunnel").Layout),
+ layout.Rigid(layout.Spacer{Height: 10}.Layout),
+ layout.Rigid(func(gtx C) D {
+ return material.Body1(th, "Entrypoint name").Layout(gtx)
+ }),
+ layout.Rigid(func(gtx C) D {
+ return p.name.Layout(gtx, th, "Name")
+ }),
+ layout.Rigid(layout.Spacer{Height: 10}.Layout),
+ layout.Rigid(func(gtx C) D {
+ return material.Body1(th, "Tunnel ID").Layout(gtx)
+ }),
+ layout.Rigid(func(gtx C) D {
+ if err := func() error {
+ tid := strings.ToLower(strings.TrimSpace(p.tunnelID.Text()))
+ if tid == "" {
+ return nil
+ }
+ if _, err := uuid.Parse(tid); err != nil {
+ return fmt.Errorf("invalid tunnel ID, should be a valid UUID")
+ }
+ if ep := entrypoint.Get(tid); ep != nil {
+ return fmt.Errorf("the entrypoint for this tunnel exists")
+ }
+ return nil
+ }(); err != nil {
+ p.tunnelID.SetError(err.Error())
+ } else {
+ p.tunnelID.ClearError()
+ }
+
+ return p.tunnelID.Layout(gtx, th, "ID")
+ }),
+ layout.Rigid(layout.Spacer{Height: 10}.Layout),
+ layout.Rigid(func(gtx C) D {
+ return material.Body1(th, "Entrypoint address").Layout(gtx)
+ }),
+ layout.Rigid(func(gtx C) D {
+ if err := func() error {
+ addr := strings.TrimSpace(p.addr.Text())
+ if addr == "" {
+ return nil
+ }
+ if _, err := net.ResolveUDPAddr("udp", addr); err != nil {
+ return fmt.Errorf("invalid address format, should be [IP]:PORT or [HOST]:PORT")
+ }
+ return nil
+ }(); err != nil {
+ p.addr.SetError(err.Error())
+ } else {
+ p.addr.ClearError()
+ }
+
+ return p.addr.Layout(gtx, th, "Address")
+ }),
+ layout.Rigid(layout.Spacer{Height: 10}.Layout),
+ layout.Rigid(func(gtx C) D {
+ return layout.Inset{Top: 10, Bottom: 10}.Layout(gtx, func(gtx C) D {
+ return layout.Flex{
+ Spacing: layout.SpaceBetween,
+ }.Layout(gtx,
+ layout.Flexed(1, material.Body1(th, "Enable keepalive").Layout),
+ layout.Rigid(material.Switch(th, &p.bKeepalive, "keepalive").Layout),
+ )
+ })
+ }),
+ layout.Rigid(func(gtx C) D {
+ if !p.bKeepalive.Value {
+ p.ttl.Clear()
+ return layout.Dimensions{}
+ }
+ p.ttl.Suffix = func(gtx C) D {
+ return material.Label(th, th.TextSize, "s").Layout(gtx)
+ }
+
+ if err := func() string {
+ for _, r := range p.ttl.Text() {
+ if !unicode.IsDigit(r) {
+ return "Must contain only digits"
+ }
+ }
+ return ""
+ }(); err != "" {
+ p.ttl.SetError(err)
+ } else {
+ p.ttl.ClearError()
+ }
+ return p.ttl.Layout(gtx, th, "TTL")
+ }),
+ )
+}
+
+func (p *udpEntryPointAddPage) createEntryPoint() error {
+ ttl, _ := strconv.Atoi(strings.TrimSpace(p.ttl.Text()))
+ ep := entrypoint.NewUDPEntryPoint(
+ tunnel.NameOption(strings.TrimSpace(p.name.Text())),
+ tunnel.IDOption(strings.ToLower(strings.TrimSpace(p.tunnelID.Text()))),
+ tunnel.EndpointOption(strings.TrimSpace(p.addr.Text())),
+ tunnel.KeepaliveOption(p.bKeepalive.Value),
+ tunnel.TTLOption(ttl),
+ )
+
+ entrypoint.Add(ep)
+
+ if err := ep.Run(); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+type udpEntryPointEditPage struct {
+ router *Router
+
+ id string
+
+ list layout.List
+ wgFavorite widget.Clickable
+ wgState widget.Clickable
+ wgDelete widget.Clickable
+ wgDone widget.Clickable
+
+ name component.TextField
+ tunnelID component.TextField
+ addr component.TextField
+
+ bKeepalive widget.Bool
+ ttl component.TextField
+}
+
+func NewUDPEntryPointEditPage(r *Router) Page {
+ return &udpEntryPointEditPage{
+ router: r,
+ list: layout.List{
+ Axis: layout.Vertical,
+ Alignment: layout.Middle,
+ },
+ name: component.TextField{
+ Editor: widget.Editor{
+ SingleLine: true,
+ },
+ },
+ tunnelID: component.TextField{
+ Editor: widget.Editor{
+ SingleLine: true,
+ ReadOnly: true,
+ },
+ },
+ addr: component.TextField{
+ Editor: widget.Editor{
+ SingleLine: true,
+ },
+ },
+ ttl: component.TextField{
+ Editor: widget.Editor{
+ SingleLine: true,
+ },
+ },
+ }
+}
+
+func (p *udpEntryPointEditPage) Init(opts ...PageOption) {
+ var options PageOptions
+ for _, opt := range opts {
+ opt(&options)
+ }
+
+ p.id = options.ID
+
+ s := entrypoint.Get(p.id)
+ if s != nil {
+ sopts := s.Options()
+ p.name.SetText(sopts.Name)
+ p.tunnelID.SetText(sopts.ID)
+ p.addr.SetText(sopts.Endpoint)
+ p.bKeepalive.Value = sopts.Keepalive
+ p.ttl.SetText(strconv.Itoa(sopts.TTL))
+ }
+
+ actions := []component.AppBarAction{
+ {
+ OverflowAction: component.OverflowAction{
+ Name: "Favorite",
+ Tag: &p.wgFavorite,
+ },
+ Layout: func(gtx C, bg, fg color.NRGBA) D {
+ s := entrypoint.Get(p.id)
+ if s == nil {
+ return D{}
+ }
+
+ if p.wgFavorite.Clicked(gtx) {
+ s.Favorite(!s.IsFavorite())
+ entrypoint.SaveConfig()
+ }
+
+ btn := component.SimpleIconButton(bg, fg, &p.wgFavorite, icons.IconFavorite)
+ if s.IsFavorite() {
+ btn.Color = color.NRGBA(colornames.Red500)
+ } else {
+ btn.Color = fg
+ }
+ return btn.Layout(gtx)
+ },
+ },
+ {
+ OverflowAction: component.OverflowAction{
+ Name: "Start/Stop",
+ Tag: &p.wgState,
+ },
+ Layout: func(gtx C, bg, fg color.NRGBA) D {
+ s := entrypoint.Get(p.id)
+ if p.wgState.Clicked(gtx) && s != nil {
+ if s.IsClosed() {
+ opts := s.Options()
+ s = p.createEntryPoint(
+ tunnel.NameOption(opts.Name),
+ tunnel.IDOption(opts.ID),
+ tunnel.EndpointOption(opts.Endpoint),
+ tunnel.KeepaliveOption(opts.Keepalive),
+ tunnel.TTLOption(opts.TTL),
+ )
+ } else {
+ s.Close()
+ }
+ entrypoint.SaveConfig()
+ }
+
+ if s != nil && !s.IsClosed() {
+ return component.SimpleIconButton(bg, fg, &p.wgState, icons.IconStop).Layout(gtx)
+ } else {
+ return component.SimpleIconButton(bg, fg, &p.wgState, icons.IconStart).Layout(gtx)
+ }
+ },
+ },
+ {
+ OverflowAction: component.OverflowAction{
+ Name: "Delete",
+ Tag: &p.wgDelete,
+ },
+ Layout: func(gtx C, bg, fg color.NRGBA) D {
+ if p.wgDelete.Clicked(gtx) {
+ entrypoint.Delete(p.id)
+ entrypoint.SaveConfig()
+ p.router.SwitchTo(Route{Path: PageEntryPoint})
+ }
+ return component.SimpleIconButton(bg, fg, &p.wgDelete, icons.IconDelete).Layout(gtx)
+ },
+ },
+ {
+ OverflowAction: component.OverflowAction{
+ Name: "Save",
+ Tag: &p.wgDone,
+ },
+ Layout: func(gtx C, bg, fg color.NRGBA) D {
+ if !p.isValid() {
+ gtx = gtx.Disabled()
+ }
+
+ if p.wgDone.Clicked(gtx) {
+ defer p.router.SwitchTo(Route{Path: PageEntryPoint})
+
+ if s := entrypoint.Get(p.id); s != nil {
+ s.Close()
+ p.createEntryPoint()
+ entrypoint.SaveConfig()
+ }
+ }
+ return component.SimpleIconButton(bg, fg, &p.wgDone, icons.IconDone).Layout(gtx)
+ },
+ },
+ }
+ p.router.bar.SetActions(actions, nil)
+ p.router.bar.Title = "UDP"
+ p.router.bar.NavigationIcon = icons.IconClose
+}
+
+func (p *udpEntryPointEditPage) isValid() bool {
+ if p.tunnelID.Text() == "" || p.tunnelID.IsErrored() ||
+ p.addr.Text() == "" || p.addr.IsErrored() {
+ return false
+ }
+ return true
+}
+
+func (p *udpEntryPointEditPage) createEntryPoint(opts ...tunnel.Option) entrypoint.EntryPoint {
+ if opts == nil {
+ ttl, _ := strconv.Atoi(strings.TrimSpace(p.ttl.Text()))
+ opts = []tunnel.Option{
+ tunnel.NameOption(strings.TrimSpace(p.name.Text())),
+ tunnel.IDOption(p.id),
+ tunnel.EndpointOption(strings.TrimSpace(p.addr.Text())),
+ tunnel.KeepaliveOption(p.bKeepalive.Value),
+ tunnel.TTLOption(ttl),
+ }
+ }
+ ep := entrypoint.NewUDPEntryPoint(opts...)
+
+ entrypoint.Set(ep)
+
+ if err := ep.Run(); err != nil {
+ log.Println(err)
+ }
+
+ return ep
+}
+
+func (p *udpEntryPointEditPage) Layout(gtx C, th *material.Theme) D {
+ return p.list.Layout(gtx, 1, func(gtx C, _ int) D {
+ return layout.Center.Layout(gtx, func(gtx C) D {
+ return layout.UniformInset(10).Layout(gtx, func(gtx C) D {
+ return component.Surface(th).Layout(gtx, func(gtx C) D {
+ return layout.UniformInset(10).Layout(gtx, func(gtx C) D {
+ return p.layout(gtx, th)
+ })
+ })
+ })
+ })
+ })
+}
+
+func (p *udpEntryPointEditPage) layout(gtx C, th *material.Theme) D {
+ return layout.Flex{
+ Axis: layout.Vertical,
+ }.Layout(gtx,
+ layout.Rigid(func(gtx C) D {
+ return material.Body1(th, "Entrypoint name").Layout(gtx)
+ }),
+ layout.Rigid(func(gtx C) D {
+ return p.name.Layout(gtx, th, "Name")
+ }),
+ layout.Rigid(layout.Spacer{Height: 10}.Layout),
+ layout.Rigid(func(gtx C) D {
+ return material.Body1(th, "Tunnel ID").Layout(gtx)
+ }),
+ layout.Rigid(func(gtx C) D {
+ if err := func() error {
+ tid := strings.TrimSpace(p.tunnelID.Text())
+ if tid == "" {
+ return nil
+ }
+ if _, err := uuid.Parse(tid); err != nil {
+ return fmt.Errorf("invalid tunnel ID, should be a valid UUID")
+ }
+ return nil
+ }(); err != nil {
+ p.tunnelID.SetError(err.Error())
+ } else {
+ p.tunnelID.ClearError()
+ }
+
+ return p.tunnelID.Layout(gtx, th, "ID")
+ }),
+ layout.Rigid(layout.Spacer{Height: 10}.Layout),
+ layout.Rigid(func(gtx C) D {
+ return material.Body1(th, "Entrypoint address").Layout(gtx)
+ }),
+ layout.Rigid(func(gtx C) D {
+ if err := func() error {
+ addr := strings.TrimSpace(p.addr.Text())
+ if addr == "" {
+ return nil
+ }
+ if _, err := net.ResolveTCPAddr("tcp", addr); err != nil {
+ return fmt.Errorf("invalid address format, should be [IP]:PORT or [HOST]:PORT")
+ }
+ return nil
+ }(); err != nil {
+ p.addr.SetError(err.Error())
+ } else {
+ p.addr.ClearError()
+ }
+
+ return p.addr.Layout(gtx, th, "Address")
+ }),
+ layout.Rigid(layout.Spacer{Height: 10}.Layout),
+ layout.Rigid(func(gtx C) D {
+ return layout.Inset{Top: 10, Bottom: 10}.Layout(gtx, func(gtx C) D {
+ return layout.Flex{
+ Spacing: layout.SpaceBetween,
+ }.Layout(gtx,
+ layout.Flexed(1, material.Body1(th, "Enable keepalive").Layout),
+ layout.Rigid(material.Switch(th, &p.bKeepalive, "keepalive").Layout),
+ )
+ })
+ }),
+ layout.Rigid(func(gtx C) D {
+ if !p.bKeepalive.Value {
+ p.ttl.Clear()
+ return layout.Dimensions{}
+ }
+ p.ttl.Suffix = func(gtx C) D {
+ return material.Label(th, th.TextSize, "s").Layout(gtx)
+ }
+
+ if err := func() string {
+ for _, r := range p.ttl.Text() {
+ if !unicode.IsDigit(r) {
+ return "Must contain only digits"
+ }
+ }
+ return ""
+ }(); err != "" {
+ p.ttl.SetError(err)
+ } else {
+ p.ttl.ClearError()
+ }
+ return p.ttl.Layout(gtx, th, "TTL")
+ }),
+ )
+}
diff --git a/ui/page/file.go b/ui/page/file.go
index 340ccea..9f9137f 100644
--- a/ui/page/file.go
+++ b/ui/page/file.go
@@ -27,6 +27,9 @@ type fileAddPage struct {
cbBasicAuth widget.Bool
username component.TextField
password component.TextField
+
+ wgPassword widget.Clickable
+ passwordVisible bool
}
func NewFileAddPage(r *Router) Page {
@@ -60,11 +63,12 @@ func NewFileAddPage(r *Router) Page {
}
func (p *fileAddPage) Init(opts ...PageOption) {
- p.name.SetText("")
- p.path.SetText("")
+ p.name.Clear()
+ p.path.Clear()
p.cbBasicAuth.Value = false
- p.username.SetText("")
- p.password.SetText("")
+ p.username.Clear()
+ p.password.Clear()
+ p.passwordVisible = false
p.router.bar.SetActions(
[]component.AppBarAction{
@@ -158,16 +162,36 @@ func (p *fileAddPage) layout(gtx C, th *material.Theme) D {
}),
layout.Rigid(func(gtx C) D {
if !p.cbBasicAuth.Value {
- p.username.SetText("")
+ p.username.Clear()
return layout.Dimensions{}
}
return p.username.Layout(gtx, th, "Username")
}),
layout.Rigid(func(gtx C) D {
if !p.cbBasicAuth.Value {
- p.password.SetText("")
+ p.password.Clear()
return layout.Dimensions{}
}
+
+ if p.wgPassword.Clicked(gtx) {
+ p.passwordVisible = !p.passwordVisible
+ }
+
+ if p.passwordVisible {
+ p.password.Suffix = func(gtx layout.Context) layout.Dimensions {
+ return p.wgPassword.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
+ return icons.IconVisibility.Layout(gtx, color.NRGBA(colornames.Grey500))
+ })
+ }
+ p.password.Mask = 0
+ } else {
+ p.password.Suffix = func(gtx layout.Context) layout.Dimensions {
+ return p.wgPassword.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
+ return icons.IconVisibilityOff.Layout(gtx, color.NRGBA(colornames.Grey500))
+ })
+ }
+ p.password.Mask = '*'
+ }
return p.password.Layout(gtx, th, "Password")
}),
)
@@ -215,6 +239,9 @@ type fileEditPage struct {
cbBasicAuth widget.Bool
username component.TextField
password component.TextField
+
+ wgPassword widget.Clickable
+ passwordVisible bool
}
func NewFileEditPage(r *Router) Page {
@@ -248,6 +275,8 @@ func NewFileEditPage(r *Router) Page {
}
func (p *fileEditPage) Init(opts ...PageOption) {
+ p.passwordVisible = false
+
var options PageOptions
for _, opt := range opts {
opt(&options)
@@ -305,7 +334,14 @@ func (p *fileEditPage) Init(opts ...PageOption) {
if p.wgState.Clicked(gtx) {
if s.IsClosed() {
- s = p.createTunnel()
+ opts := s.Options()
+ s = p.createTunnel(
+ tunnel.NameOption(opts.Name),
+ tunnel.IDOption(opts.ID),
+ tunnel.EndpointOption(opts.Endpoint),
+ tunnel.UsernameOption(opts.Username),
+ tunnel.PasswordOption(opts.Password),
+ )
} else {
s.Close()
}
@@ -357,19 +393,22 @@ func (p *fileEditPage) Init(opts ...PageOption) {
p.router.bar.NavigationIcon = icons.IconClose
}
-func (p *fileEditPage) createTunnel() tunnel.Tunnel {
- var username, password string
- if p.cbBasicAuth.Value {
- username = strings.TrimSpace(p.username.Text())
- password = strings.TrimSpace(p.password.Text())
+func (p *fileEditPage) createTunnel(opts ...tunnel.Option) tunnel.Tunnel {
+ if opts == nil {
+ var username, password string
+ if p.cbBasicAuth.Value {
+ username = strings.TrimSpace(p.username.Text())
+ password = strings.TrimSpace(p.password.Text())
+ }
+ opts = []tunnel.Option{
+ tunnel.NameOption(strings.TrimSpace(p.name.Text())),
+ tunnel.IDOption(p.id),
+ tunnel.EndpointOption(strings.TrimSpace(p.path.Text())),
+ tunnel.UsernameOption(username),
+ tunnel.PasswordOption(password),
+ }
}
- tun := tunnel.NewFileTunnel(
- tunnel.IDOption(p.id),
- tunnel.NameOption(strings.TrimSpace(p.name.Text())),
- tunnel.EndpointOption(strings.TrimSpace(p.path.Text())),
- tunnel.UsernameOption(username),
- tunnel.PasswordOption(password),
- )
+ tun := tunnel.NewFileTunnel(opts...)
tunnel.Set(tun)
@@ -455,16 +494,37 @@ func (p *fileEditPage) layout(gtx C, th *material.Theme) D {
}),
layout.Rigid(func(gtx C) D {
if !p.cbBasicAuth.Value {
- p.username.SetText("")
+ p.username.Clear()
return layout.Dimensions{}
}
return p.username.Layout(gtx, th, "Username")
}),
layout.Rigid(func(gtx C) D {
if !p.cbBasicAuth.Value {
- p.password.SetText("")
+ p.password.Clear()
return layout.Dimensions{}
}
+
+ if p.wgPassword.Clicked(gtx) {
+ p.passwordVisible = !p.passwordVisible
+ }
+
+ if p.passwordVisible {
+ p.password.Suffix = func(gtx layout.Context) layout.Dimensions {
+ return p.wgPassword.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
+ return icons.IconVisibility.Layout(gtx, color.NRGBA(colornames.Grey500))
+ })
+ }
+ p.password.Mask = 0
+ } else {
+ p.password.Suffix = func(gtx layout.Context) layout.Dimensions {
+ return p.wgPassword.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
+ return icons.IconVisibilityOff.Layout(gtx, color.NRGBA(colornames.Grey500))
+ })
+ }
+ p.password.Mask = '*'
+ }
+
return p.password.Layout(gtx, th, "Password")
}),
)
diff --git a/ui/page/http.go b/ui/page/http.go
index 7d95aa8..545679d 100644
--- a/ui/page/http.go
+++ b/ui/page/http.go
@@ -33,6 +33,9 @@ type httpAddPage struct {
password component.TextField
bTLS widget.Bool
+
+ wgPassword widget.Clickable
+ passwordVisible bool
}
func NewHTTPAddPage(r *Router) Page {
@@ -71,13 +74,14 @@ func NewHTTPAddPage(r *Router) Page {
}
func (p *httpAddPage) Init(opts ...PageOption) {
- p.name.SetText("")
- p.addr.SetText("")
- p.hostname.SetText("")
+ p.name.Clear()
+ p.addr.Clear()
+ p.hostname.Clear()
p.bBasicAuth.Value = false
- p.username.SetText("")
- p.password.SetText("")
+ p.username.Clear()
+ p.password.Clear()
p.bTLS.Value = false
+ p.passwordVisible = false
p.router.bar.SetActions(
[]component.AppBarAction{
@@ -186,6 +190,27 @@ func (p *httpAddPage) layout(gtx C, th *material.Theme) D {
p.password.SetText("")
return layout.Dimensions{}
}
+
+ if p.wgPassword.Clicked(gtx) {
+ p.passwordVisible = !p.passwordVisible
+ }
+
+ if p.passwordVisible {
+ p.password.Suffix = func(gtx layout.Context) layout.Dimensions {
+ return p.wgPassword.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
+ return icons.IconVisibility.Layout(gtx, color.NRGBA(colornames.Grey500))
+ })
+ }
+ p.password.Mask = 0
+ } else {
+ p.password.Suffix = func(gtx layout.Context) layout.Dimensions {
+ return p.wgPassword.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
+ return icons.IconVisibilityOff.Layout(gtx, color.NRGBA(colornames.Grey500))
+ })
+ }
+ p.password.Mask = '*'
+ }
+
return p.password.Layout(gtx, th, "Password")
}),
layout.Rigid(layout.Spacer{Height: 10}.Layout),
@@ -254,6 +279,9 @@ type httpEditPage struct {
wgID widget.Clickable
wgEntrypoint widget.Clickable
+
+ wgPassword widget.Clickable
+ passwordVisible bool
}
func NewHTTPEditPage(r *Router) Page {
@@ -292,6 +320,8 @@ func NewHTTPEditPage(r *Router) Page {
}
func (p *httpEditPage) Init(opts ...PageOption) {
+ p.passwordVisible = false
+
var options PageOptions
for _, opt := range opts {
opt(&options)
@@ -350,7 +380,16 @@ func (p *httpEditPage) Init(opts ...PageOption) {
s := tunnel.Get(p.id)
if p.wgState.Clicked(gtx) && s != nil {
if s.IsClosed() {
- s = p.createTunnel()
+ opts := s.Options()
+ s = p.createTunnel(
+ tunnel.NameOption(opts.Name),
+ tunnel.IDOption(opts.ID),
+ tunnel.EndpointOption(opts.Endpoint),
+ tunnel.HostnameOption(opts.Hostname),
+ tunnel.UsernameOption(opts.Username),
+ tunnel.PasswordOption(opts.Password),
+ tunnel.EnableTLSOption(opts.EnableTLS),
+ )
} else {
s.Close()
}
@@ -402,25 +441,29 @@ func (p *httpEditPage) Init(opts ...PageOption) {
p.router.bar.NavigationIcon = icons.IconClose
}
-func (p *httpEditPage) createTunnel() tunnel.Tunnel {
- var username, password string
- if p.cbBasicAuth.Value {
- username = strings.TrimSpace(p.username.Text())
- password = strings.TrimSpace(p.password.Text())
- }
- var hostname string
- if p.bHost.Value {
- hostname = strings.TrimSpace(p.hostname.Text())
+func (p *httpEditPage) createTunnel(opts ...tunnel.Option) tunnel.Tunnel {
+ if opts == nil {
+ var username, password string
+ if p.cbBasicAuth.Value {
+ username = strings.TrimSpace(p.username.Text())
+ password = strings.TrimSpace(p.password.Text())
+ }
+ var hostname string
+ if p.bHost.Value {
+ hostname = strings.TrimSpace(p.hostname.Text())
+ }
+
+ opts = []tunnel.Option{
+ tunnel.NameOption(strings.TrimSpace(p.name.Text())),
+ tunnel.IDOption(p.id),
+ tunnel.EndpointOption(strings.TrimSpace(p.addr.Text())),
+ tunnel.UsernameOption(username),
+ tunnel.PasswordOption(password),
+ tunnel.HostnameOption(hostname),
+ tunnel.EnableTLSOption(p.bTLS.Value),
+ }
}
- tun := tunnel.NewHTTPTunnel(
- tunnel.IDOption(p.id),
- tunnel.NameOption(strings.TrimSpace(p.name.Text())),
- tunnel.EndpointOption(strings.TrimSpace(p.addr.Text())),
- tunnel.UsernameOption(username),
- tunnel.PasswordOption(password),
- tunnel.HostnameOption(hostname),
- tunnel.EnableTLSOption(p.bTLS.Value),
- )
+ tun := tunnel.NewHTTPTunnel(opts...)
tunnel.Set(tun)
@@ -522,6 +565,27 @@ func (p *httpEditPage) layout(gtx C, th *material.Theme) D {
p.password.SetText("")
return layout.Dimensions{}
}
+
+ if p.wgPassword.Clicked(gtx) {
+ p.passwordVisible = !p.passwordVisible
+ }
+
+ if p.passwordVisible {
+ p.password.Suffix = func(gtx layout.Context) layout.Dimensions {
+ return p.wgPassword.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
+ return icons.IconVisibility.Layout(gtx, color.NRGBA(colornames.Grey500))
+ })
+ }
+ p.password.Mask = 0
+ } else {
+ p.password.Suffix = func(gtx layout.Context) layout.Dimensions {
+ return p.wgPassword.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
+ return icons.IconVisibilityOff.Layout(gtx, color.NRGBA(colornames.Grey500))
+ })
+ }
+ p.password.Mask = '*'
+ }
+
return p.password.Layout(gtx, th, "Password")
}),
layout.Rigid(func(gtx C) D {
diff --git a/ui/page/menu.go b/ui/page/menu.go
index d593c7e..dc07af2 100644
--- a/ui/page/menu.go
+++ b/ui/page/menu.go
@@ -66,6 +66,10 @@ func (p *menuPage) Layout(gtx C, th *material.Theme) D {
p.router.SwitchTo(Route{Path: PageNewTCPEntryPoint})
return true
}
+ if p.wgEntryPointUDP.Clicked(gtx) {
+ p.router.SwitchTo(Route{Path: PageNewUDPEntryPoint})
+ return true
+ }
return false
}(); clicked {
@@ -138,7 +142,18 @@ func (p *menuPage) Layout(gtx C, th *material.Theme) D {
return component.Surface(th).Layout(gtx, func(gtx C) D {
return p.wgEntryPointTCP.Layout(gtx, func(gtx C) D {
return layout.UniformInset(10).Layout(gtx, func(gtx C) D {
- return p.layoutCard(gtx, th, "TCP", "Create an entrypoint to connect to the specified TCP tunnel")
+ return p.layoutCard(gtx, th, "TCP", "Create an entrypoint to the specified TCP tunnel")
+ })
+ })
+ })
+ })
+ }),
+ layout.Rigid(func(gtx C) D {
+ return layout.Inset{Top: 5, Bottom: 5}.Layout(gtx, func(gtx C) D {
+ return component.Surface(th).Layout(gtx, func(gtx C) D {
+ return p.wgEntryPointUDP.Layout(gtx, func(gtx C) D {
+ return layout.UniformInset(10).Layout(gtx, func(gtx C) D {
+ return p.layoutCard(gtx, th, "UDP", "Create an entrypoint to the specified UDP tunnel")
})
})
})
diff --git a/ui/page/page.go b/ui/page/page.go
index d955be2..7fc4705 100644
--- a/ui/page/page.go
+++ b/ui/page/page.go
@@ -28,6 +28,8 @@ const (
PageEntryPoint = "/entrypoint"
PageNewTCPEntryPoint = "/entrypoint/tcp/create"
PageEditTCPEntryPoint = "/entrypoint/tcp/edit"
+ PageNewUDPEntryPoint = "/entrypoint/udp/create"
+ PageEditUDPEntryPoint = "/entrypoint/udp/edit"
PageAbout = "/about"
)
diff --git a/ui/page/router.go b/ui/page/router.go
index 431a497..9cc4a97 100644
--- a/ui/page/router.go
+++ b/ui/page/router.go
@@ -44,8 +44,10 @@ func NewRouter() *Router {
r.Register(PageEditUDP, NewUDPEditPage(r))
r.Register(PageEntryPoint, NewEntryPointPage(r))
- r.Register(PageEditTCPEntryPoint, NewTCPEntryPointEditPage(r))
r.Register(PageNewTCPEntryPoint, NewTCPEntryPointAddPage(r))
+ r.Register(PageEditTCPEntryPoint, NewTCPEntryPointEditPage(r))
+ r.Register(PageNewUDPEntryPoint, NewUDPEntryPointAddPage(r))
+ r.Register(PageEditUDPEntryPoint, NewUDPEntryPointEditPage(r))
r.Register(PageAbout, NewAboutPage(r))
@@ -75,8 +77,11 @@ func (r *Router) Layout(gtx layout.Context, th *material.Theme) layout.Dimension
for _, event := range r.bar.Events(gtx) {
switch event := event.(type) {
case component.AppBarNavigationClicked:
+ path := r.current.Path
// log.Printf("navigation clicked: %+v", event)
- if r.current.Path == PageTunnel {
+ if path == PageTunnel ||
+ path == PageNewTCPEntryPoint || path == PageEditTCPEntryPoint ||
+ path == PageNewUDPEntryPoint || path == PageEditUDPEntryPoint {
r.SwitchTo(Route{Path: PageEntryPoint})
} else {
r.SwitchTo(Route{Path: PageTunnel})
diff --git a/ui/page/tcp.go b/ui/page/tcp.go
index 35036b6..6cffa0a 100644
--- a/ui/page/tcp.go
+++ b/ui/page/tcp.go
@@ -47,8 +47,8 @@ func NewTCPAddPage(r *Router) Page {
}
func (p *tcpAddPage) Init(opts ...PageOption) {
- p.name.SetText("")
- p.addr.SetText("")
+ p.name.Clear()
+ p.addr.Clear()
p.router.bar.SetActions(
[]component.AppBarAction{
@@ -226,7 +226,12 @@ func (p *tcpEditPage) Init(opts ...PageOption) {
s := tunnel.Get(p.id)
if p.wgState.Clicked(gtx) && s != nil {
if s.IsClosed() {
- s = p.createTunnel()
+ opts := s.Options()
+ s = p.createTunnel(
+ tunnel.NameOption(opts.Name),
+ tunnel.IDOption(opts.ID),
+ tunnel.EndpointOption(opts.Endpoint),
+ )
} else {
s.Close()
}
@@ -278,12 +283,15 @@ func (p *tcpEditPage) Init(opts ...PageOption) {
p.router.bar.NavigationIcon = icons.IconClose
}
-func (p *tcpEditPage) createTunnel() tunnel.Tunnel {
- tun := tunnel.NewTCPTunnel(
- tunnel.IDOption(p.id),
- tunnel.NameOption(strings.TrimSpace(p.name.Text())),
- tunnel.EndpointOption(strings.TrimSpace(p.addr.Text())),
- )
+func (p *tcpEditPage) createTunnel(opts ...tunnel.Option) tunnel.Tunnel {
+ if opts == nil {
+ opts = []tunnel.Option{
+ tunnel.IDOption(p.id),
+ tunnel.NameOption(strings.TrimSpace(p.name.Text())),
+ tunnel.EndpointOption(strings.TrimSpace(p.addr.Text())),
+ }
+ }
+ tun := tunnel.NewTCPTunnel(opts...)
tunnel.Set(tun)
diff --git a/ui/page/udp.go b/ui/page/udp.go
index d2ab7e9..5c4eb4f 100644
--- a/ui/page/udp.go
+++ b/ui/page/udp.go
@@ -47,8 +47,8 @@ func NewUDPAddPage(r *Router) Page {
}
func (p *udpAddPage) Init(opts ...PageOption) {
- p.name.SetText("")
- p.addr.SetText("")
+ p.name.Clear()
+ p.addr.Clear()
p.router.bar.SetActions(
[]component.AppBarAction{
@@ -226,7 +226,12 @@ func (p *udpEditPage) Init(opts ...PageOption) {
s := tunnel.Get(p.id)
if p.wgState.Clicked(gtx) && s != nil {
if s.IsClosed() {
- s = p.createTunnel()
+ opts := s.Options()
+ s = p.createTunnel(
+ tunnel.NameOption(opts.Name),
+ tunnel.IDOption(opts.ID),
+ tunnel.EndpointOption(opts.Endpoint),
+ )
} else {
s.Close()
}
@@ -279,12 +284,15 @@ func (p *udpEditPage) Init(opts ...PageOption) {
p.router.bar.NavigationIcon = icons.IconClose
}
-func (p *udpEditPage) createTunnel() tunnel.Tunnel {
- tun := tunnel.NewUDPTunnel(
- tunnel.IDOption(p.id),
- tunnel.NameOption(strings.TrimSpace(p.name.Text())),
- tunnel.EndpointOption(strings.TrimSpace(p.addr.Text())),
- )
+func (p *udpEditPage) createTunnel(opts ...tunnel.Option) tunnel.Tunnel {
+ if opts == nil {
+ opts = []tunnel.Option{
+ tunnel.IDOption(p.id),
+ tunnel.NameOption(strings.TrimSpace(p.name.Text())),
+ tunnel.EndpointOption(strings.TrimSpace(p.addr.Text())),
+ }
+ }
+ tun := tunnel.NewUDPTunnel(opts...)
tunnel.Set(tun)