From e78a75d6bbc56699afcb7c9e04d51f512ded2f17 Mon Sep 17 00:00:00 2001 From: ginuerzh Date: Sat, 16 Dec 2023 11:58:48 +0800 Subject: [PATCH] add tcp entrypoint --- Makefile | 4 +- README.md | 14 +- config/config.go | 7 +- main.go | 2 + tunnel/entrypoint/entrypoint.go | 182 ++++++++++++++ tunnel/entrypoint/tcp.go | 226 ++++++++++++++++++ tunnel/entrypoint/udp.go | 226 ++++++++++++++++++ tunnel/file.go | 59 +++-- tunnel/http.go | 51 ++-- tunnel/tcp.go | 51 ++-- tunnel/tunnel.go | 121 ++++------ tunnel/udp.go | 51 ++-- ui/icons/icons.go | 1 + ui/page/entrypoint.go | 177 ++++++++++++++ ui/page/entrypoint_tcp.go | 405 ++++++++++++++++++++++++++++++++ ui/page/file.go | 42 ++-- ui/page/http.go | 48 ++-- ui/page/menu.go | 28 ++- ui/page/page.go | 27 ++- ui/page/router.go | 17 +- ui/page/tcp.go | 48 ++-- ui/page/tunnel.go | 20 +- ui/page/udp.go | 48 ++-- version/version.go | 2 +- winres/winres.json | 8 +- 25 files changed, 1607 insertions(+), 258 deletions(-) create mode 100644 tunnel/entrypoint/entrypoint.go create mode 100644 tunnel/entrypoint/tcp.go create mode 100644 tunnel/entrypoint/udp.go create mode 100644 ui/page/entrypoint.go create mode 100644 ui/page/entrypoint_tcp.go diff --git a/Makefile b/Makefile index 3889a4f..5103011 100644 --- a/Makefile +++ b/Makefile @@ -7,15 +7,13 @@ GOBUILD=CGO_ENABLED=0 go build --ldflags="-s -w" -v -x -a GOFILES=*.go PLATFORM_LIST = \ - darwin-amd64 \ - darwin-arm64 \ linux-amd64 WINDOWS_ARCH_LIST = \ windows-amd64 linux-amd64: - GOOS=linux GOARCH=amd64 CGO_ENABLED=1 go build --ldflags="-s -w" -v -x -a + GOOS=linux GOARCH=amd64 CGO_ENABLED=1 go build --ldflags="-s -w" -v -x -a -o $(BINDIR)/$(NAME)-$@ $(GOFILES) darwin-amd64: GOOS=darwin GOARCH=amd64 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ $(GOFILES) diff --git a/README.md b/README.md index 5106aa5..64b76b2 100644 --- a/README.md +++ b/README.md @@ -6,13 +6,13 @@ A cross-platform GUI client for [GOST.PLUS](https://gost.plus) built with [gioui ### Desktop - - - - + + + + ### Mobile - - - \ No newline at end of file + + + \ No newline at end of file diff --git a/config/config.go b/config/config.go index 10e13d9..c72a66b 100644 --- a/config/config.go +++ b/config/config.go @@ -107,9 +107,10 @@ type Tunnel struct { } type Config struct { - Settings *Settings - Tunnels []*Tunnel - Log *config.LogConfig + Settings *Settings + Tunnels []*Tunnel + EntryPoints []*Tunnel + Log *config.LogConfig } func (c *Config) Load() error { diff --git a/main.go b/main.go index df016fc..253da97 100644 --- a/main.go +++ b/main.go @@ -12,12 +12,14 @@ import ( "gioui.org/op" "github.com/go-gost/gost-plus/config" "github.com/go-gost/gost-plus/tunnel" + "github.com/go-gost/gost-plus/tunnel/entrypoint" "github.com/go-gost/gost-plus/ui" ) func main() { config.Init() tunnel.LoadConfig() + entrypoint.LoadConfig() go func() { w := app.NewWindow( diff --git a/tunnel/entrypoint/entrypoint.go b/tunnel/entrypoint/entrypoint.go new file mode 100644 index 0000000..12e106e --- /dev/null +++ b/tunnel/entrypoint/entrypoint.go @@ -0,0 +1,182 @@ +package entrypoint + +import ( + "errors" + "sync" + + "github.com/go-gost/core/logger" + "github.com/go-gost/gost-plus/config" + "github.com/go-gost/gost-plus/tunnel" +) + +const ( + TCPEntryPoint = "tcp" + UDPEntryPoint = "udp" +) + +var ( + ErrEntryPointClosed = errors.New("entrypoint closed") +) + +type EntryPoint = tunnel.Tunnel + +type entryPointList struct { + list []EntryPoint + mux sync.RWMutex +} + +var ( + entryPoints entryPointList +) + +func Count() int { + entryPoints.mux.RLock() + defer entryPoints.mux.RUnlock() + return len(entryPoints.list) +} + +func Add(s EntryPoint) { + entryPoints.mux.Lock() + defer entryPoints.mux.Unlock() + entryPoints.list = append(entryPoints.list, s) +} + +func Set(s EntryPoint) { + if s == nil { + return + } + + old := Get(s.ID()) + if old == nil { + return + } + s.Favorite(old.IsFavorite()) + + entryPoints.mux.Lock() + defer entryPoints.mux.Unlock() + + for i, ep := range entryPoints.list { + if ep != nil && ep.ID() == s.ID() { + entryPoints.list[i] = s + } + } +} + +func GetIndex(index int) EntryPoint { + entryPoints.mux.RLock() + defer entryPoints.mux.RUnlock() + if index < 0 || index >= len(entryPoints.list) { + return nil + } + return entryPoints.list[index] +} + +func Get(id string) EntryPoint { + entryPoints.mux.RLock() + defer entryPoints.mux.RUnlock() + + for _, s := range entryPoints.list { + if s != nil && s.ID() == id { + return s + } + } + return nil +} + +func Delete(id string) { + entryPoints.mux.Lock() + defer entryPoints.mux.Unlock() + + for i, s := range entryPoints.list { + if s != nil && s.ID() == id { + s.Close() + entryPoints.list[i] = nil + return + } + } +} + +func LoadConfig() { + for _, ep := range config.Global().EntryPoints { + if ep == 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, + }) + if s == nil { + continue + } + + if ep.Closed { + s.Close() + } else { + s.Run() + } + + s.Favorite(ep.Favorite) + Add(s) + } +} + +func SaveConfig() error { + cfg := config.Global() + cfg.EntryPoints = nil + + for i := 0; i < Count(); i++ { + ep := GetIndex(i) + if ep == nil { + continue + } + + opts := ep.Options() + + cfg.EntryPoints = append(cfg.EntryPoints, &config.Tunnel{ + ID: ep.ID(), + Name: ep.Name(), + Type: ep.Type(), + Endpoint: ep.Entrypoint(), + Hostname: opts.Hostname, + Username: opts.Username, + Password: opts.Password, + EnableTLS: opts.EnableTLS, + Favorite: ep.IsFavorite(), + Closed: ep.IsClosed(), + }) + } + + config.Set(cfg) + + if err := cfg.Write(); err != nil { + logger.Default().Error(err) + return err + } + return nil +} + +func createEntryPoint(st string, opts tunnel.Options) EntryPoint { + options := []tunnel.Option{ + tunnel.IDOption(opts.ID), + tunnel.NameOption(opts.Name), + tunnel.EndpointOption(opts.Endpoint), + tunnel.HostnameOption(opts.Hostname), + tunnel.UsernameOption(opts.Username), + tunnel.PasswordOption(opts.Password), + tunnel.EnableTLSOption(opts.EnableTLS), + } + switch st { + case TCPEntryPoint: + return NewTCPEntryPoint(options...) + // case UDPEntryPoint: + // return NewU(options...) + default: + return nil + } +} diff --git a/tunnel/entrypoint/tcp.go b/tunnel/entrypoint/tcp.go new file mode 100644 index 0000000..a15573b --- /dev/null +++ b/tunnel/entrypoint/tcp.go @@ -0,0 +1,226 @@ +package entrypoint + +import ( + "crypto/md5" + "encoding/hex" + "fmt" + "sync" + "sync/atomic" + + "github.com/go-gost/core/chain" + "github.com/go-gost/core/handler" + "github.com/go-gost/core/listener" + "github.com/go-gost/core/logger" + "github.com/go-gost/core/service" + "github.com/go-gost/gost-plus/tunnel" + "github.com/go-gost/x/config" + 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" + mdx "github.com/go-gost/x/metadata" + xservice "github.com/go-gost/x/service" + "github.com/google/uuid" +) + +type tcpEntryPoint struct { + endpoint string + opts tunnel.Options + config *config.Config + forward service.Service + favorite atomic.Bool + + cclose chan struct{} + + err error + mu sync.RWMutex +} + +func NewTCPEntryPoint(opts ...tunnel.Option) EntryPoint { + var options tunnel.Options + for _, opt := range opts { + opt(&options) + } + + if options.ID == "" { + options.ID = uuid.NewString() + } + + v := md5.Sum([]byte(options.ID)) + endpoint := hex.EncodeToString(v[:8]) + + if options.Endpoint == "" { + options.Endpoint = "localhost:8000" + } + + if options.Name == "" { + options.Name = endpoint + } + + s := &tcpEntryPoint{ + endpoint: endpoint, + opts: options, + cclose: make(chan struct{}), + } + + return s +} + +func (s *tcpEntryPoint) ID() string { + return s.opts.ID +} + +func (s *tcpEntryPoint) Type() string { + return TCPEntryPoint +} + +func (s *tcpEntryPoint) Name() string { + return s.opts.Name +} + +func (s *tcpEntryPoint) Endpoint() string { + return fmt.Sprintf("%s.%s", s.endpoint, tunnel.EndpointAddr) +} + +func (s *tcpEntryPoint) Entrypoint() string { + return s.opts.Endpoint +} + +func (s *tcpEntryPoint) Options() tunnel.Options { + return s.opts +} + +func (s *tcpEntryPoint) Favorite(b bool) { + s.favorite.Store(b) +} + +func (s *tcpEntryPoint) IsFavorite() bool { + return s.favorite.Load() +} + +func (s *tcpEntryPoint) init() error { + tcp := &config.ServiceConfig{ + Name: s.opts.Name, + Addr: s.opts.Endpoint, + Handler: &config.HandlerConfig{ + Type: "tcp", + Chain: s.opts.Name, + }, + Listener: &config.ListenerConfig{ + Type: "tcp", + }, + Forwarder: &config.ForwarderConfig{ + Nodes: []*config.ForwardNodeConfig{ + { + Name: s.opts.Name, + Addr: s.Endpoint(), + }, + }, + }, + } + + s.config = &config.Config{ + Services: []*config.ServiceConfig{tcp}, + Chains: []*config.ChainConfig{tunnel.ChainConfig(s.opts.ID, s.opts.Name)}, + } + return nil +} + +func (s *tcpEntryPoint) Run() (err error) { + if s.IsClosed() { + return ErrEntryPointClosed + } + + defer func() { + s.setErr(err) + }() + + if err = s.init(); err != nil { + return + } + + log := logger.Default().WithFields(map[string]any{ + "kind": "service", + "service": s.opts.Name, + }) + + { + var ch chain.Chainer + ch, err = chain_parser.ParseChain(s.config.Chains[0], log) + if err != nil { + log.Error(err) + return + } + + cfg := s.config.Services[0] + ln := tcp.NewListener( + listener.AddrOption(cfg.Addr), + listener.LoggerOption(log.WithFields(map[string]any{"kind": "listener", "listener": "tcp"})), + ) + if err = ln.Init(mdx.NewMetadata(cfg.Listener.Metadata)); err != nil { + return + } + + h := local.NewHandler( + handler.RouterOption(chain.NewRouter( + chain.ChainRouterOption(ch), + chain.LoggerRouterOption(log), + )), + handler.LoggerOption(log.WithFields(map[string]any{"kind": "handler", "handler": "tcp"})), + ) + if err = h.Init(mdx.NewMetadata(cfg.Handler.Metadata)); err != nil { + return + } + + node := cfg.Forwarder.Nodes[0] + if forwarder, ok := h.(handler.Forwarder); ok { + forwarder.Forward(hop.NewHop( + hop.NodeOption(chain.NewNode(node.Name, node.Addr)), + hop.LoggerOption(log.WithFields(map[string]any{"kind": "hop"})), + )) + } + s.forward = xservice.NewService(s.opts.Name, ln, h, xservice.LoggerOption(log)) + } + + go func() { + s.setErr(s.forward.Serve()) + }() + + return nil +} + +func (s *tcpEntryPoint) Close() error { + defer func() { + select { + case <-s.cclose: + default: + close(s.cclose) + } + }() + + if s.forward != nil { + return s.forward.Close() + } + return nil +} + +func (s *tcpEntryPoint) IsClosed() bool { + select { + case <-s.cclose: + return true + default: + return false + } +} + +func (s *tcpEntryPoint) setErr(err error) { + s.mu.Lock() + defer s.mu.Unlock() + s.err = err +} + +func (s *tcpEntryPoint) Err() error { + s.mu.RLock() + defer s.mu.RUnlock() + return s.err +} diff --git a/tunnel/entrypoint/udp.go b/tunnel/entrypoint/udp.go new file mode 100644 index 0000000..5543291 --- /dev/null +++ b/tunnel/entrypoint/udp.go @@ -0,0 +1,226 @@ +package entrypoint + +import ( + "crypto/md5" + "encoding/hex" + "fmt" + "sync" + "sync/atomic" + + "github.com/go-gost/core/chain" + "github.com/go-gost/core/handler" + "github.com/go-gost/core/listener" + "github.com/go-gost/core/logger" + "github.com/go-gost/core/service" + "github.com/go-gost/gost-plus/tunnel" + "github.com/go-gost/x/config" + 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" + mdx "github.com/go-gost/x/metadata" + xservice "github.com/go-gost/x/service" + "github.com/google/uuid" +) + +type udpEntryPoint struct { + endpoint string + opts tunnel.Options + config *config.Config + forward service.Service + favorite atomic.Bool + + cclose chan struct{} + + err error + mu sync.RWMutex +} + +func NewUDPEntryPoint(opts ...tunnel.Option) EntryPoint { + var options tunnel.Options + for _, opt := range opts { + opt(&options) + } + + if options.ID == "" { + options.ID = uuid.NewString() + } + + v := md5.Sum([]byte(options.ID)) + endpoint := hex.EncodeToString(v[:8]) + + if options.Endpoint == "" { + options.Endpoint = "localhost:8000" + } + + if options.Name == "" { + options.Name = endpoint + } + + s := &udpEntryPoint{ + endpoint: endpoint, + opts: options, + cclose: make(chan struct{}), + } + + return s +} + +func (s *udpEntryPoint) ID() string { + return s.opts.ID +} + +func (s *udpEntryPoint) Type() string { + return UDPEntryPoint +} + +func (s *udpEntryPoint) Name() string { + return s.opts.Name +} + +func (s *udpEntryPoint) Endpoint() string { + return fmt.Sprintf("%s.%s", s.endpoint, tunnel.EndpointAddr) +} + +func (s *udpEntryPoint) Entrypoint() string { + return s.opts.Endpoint +} + +func (s *udpEntryPoint) Options() tunnel.Options { + return s.opts +} + +func (s *udpEntryPoint) Favorite(b bool) { + s.favorite.Store(b) +} + +func (s *udpEntryPoint) IsFavorite() bool { + return s.favorite.Load() +} + +func (s *udpEntryPoint) init() error { + tcp := &config.ServiceConfig{ + Name: s.opts.Name, + Addr: s.opts.Endpoint, + Handler: &config.HandlerConfig{ + Type: "udp", + Chain: s.opts.Name, + }, + Listener: &config.ListenerConfig{ + Type: "udp", + }, + Forwarder: &config.ForwarderConfig{ + Nodes: []*config.ForwardNodeConfig{ + { + Name: s.opts.Name, + Addr: s.Endpoint(), + }, + }, + }, + } + + s.config = &config.Config{ + Services: []*config.ServiceConfig{tcp}, + Chains: []*config.ChainConfig{tunnel.ChainConfig(s.opts.ID, s.opts.Name)}, + } + return nil +} + +func (s *udpEntryPoint) Run() (err error) { + if s.IsClosed() { + return ErrEntryPointClosed + } + + defer func() { + s.setErr(err) + }() + + if err = s.init(); err != nil { + return + } + + log := logger.Default().WithFields(map[string]any{ + "kind": "service", + "service": s.opts.Name, + }) + + { + var ch chain.Chainer + ch, err = chain_parser.ParseChain(s.config.Chains[0], log) + if err != nil { + log.Error(err) + return + } + + cfg := s.config.Services[0] + ln := tcp.NewListener( + listener.AddrOption(cfg.Addr), + listener.LoggerOption(log.WithFields(map[string]any{"kind": "listener", "listener": "udp"})), + ) + if err = ln.Init(mdx.NewMetadata(cfg.Listener.Metadata)); err != nil { + return + } + + h := local.NewHandler( + handler.RouterOption(chain.NewRouter( + chain.ChainRouterOption(ch), + chain.LoggerRouterOption(log), + )), + handler.LoggerOption(log.WithFields(map[string]any{"kind": "handler", "handler": "udp"})), + ) + if err = h.Init(mdx.NewMetadata(cfg.Handler.Metadata)); err != nil { + return + } + + node := cfg.Forwarder.Nodes[0] + if forwarder, ok := h.(handler.Forwarder); ok { + forwarder.Forward(hop.NewHop( + hop.NodeOption(chain.NewNode(node.Name, node.Addr)), + hop.LoggerOption(log.WithFields(map[string]any{"kind": "hop"})), + )) + } + s.forward = xservice.NewService(s.opts.Name, ln, h, xservice.LoggerOption(log)) + } + + go func() { + s.setErr(s.forward.Serve()) + }() + + return nil +} + +func (s *udpEntryPoint) Close() error { + defer func() { + select { + case <-s.cclose: + default: + close(s.cclose) + } + }() + + if s.forward != nil { + return s.forward.Close() + } + return nil +} + +func (s *udpEntryPoint) IsClosed() bool { + select { + case <-s.cclose: + return true + default: + return false + } +} + +func (s *udpEntryPoint) setErr(err error) { + s.mu.Lock() + defer s.mu.Unlock() + s.err = err +} + +func (s *udpEntryPoint) Err() error { + s.mu.RLock() + defer s.mu.RUnlock() + return s.err +} diff --git a/tunnel/file.go b/tunnel/file.go index c50dd16..5c8df92 100644 --- a/tunnel/file.go +++ b/tunnel/file.go @@ -5,6 +5,7 @@ import ( "encoding/hex" "fmt" "os" + "sync" "sync/atomic" "github.com/go-gost/core/auth" @@ -35,6 +36,9 @@ type fileTunnel struct { favorite atomic.Bool cclose chan struct{} + + err error + mu sync.RWMutex } func NewFileTunnel(opts ...Option) Tunnel { @@ -55,7 +59,7 @@ func NewFileTunnel(opts ...Option) Tunnel { } if options.Name == "" { - options.Name = fmt.Sprintf("FILE-%s", endpoint) + options.Name = endpoint } s := &fileTunnel{ @@ -83,8 +87,8 @@ func (s *fileTunnel) Endpoint() string { return s.opts.Endpoint } -func (f *fileTunnel) Entrypoint() string { - return fmt.Sprintf("https://%s.%s", f.endpoint, endpointAddr) +func (s *fileTunnel) Entrypoint() string { + return fmt.Sprintf("https://%s.%s", s.endpoint, EndpointAddr) } func (s *fileTunnel) Options() Options { @@ -132,19 +136,23 @@ func (s *fileTunnel) init() error { s.config = &config.Config{ Services: []*config.ServiceConfig{file, rtcp}, - Chains: []*config.ChainConfig{chainConfig(s.opts.ID, s.opts.Name)}, + Chains: []*config.ChainConfig{ChainConfig(s.opts.ID, s.opts.Name)}, } return nil } -func (s *fileTunnel) Run() error { +func (s *fileTunnel) Run() (err error) { if s.IsClosed() { return ErrTunnelClosed } - if err := s.init(); err != nil { - return err + defer func() { + s.setErr(err) + }() + + if err = s.init(); err != nil { + return } log := logger.Default().WithFields(map[string]any{ @@ -157,8 +165,8 @@ func (s *fileTunnel) Run() error { ln := tcp.NewListener( listener.LoggerOption(log.WithFields(map[string]any{"kind": "listener", "listener": "tcp"})), ) - if err := ln.Init(nil); err != nil { - return err + if err = ln.Init(nil); err != nil { + return } log.Infof("listen on %s", ln.Addr()) @@ -170,17 +178,18 @@ func (s *fileTunnel) Run() error { handler.LoggerOption(log.WithFields(map[string]any{"kind": "handler", "handler": "file"})), handler.AutherOption(auther), ) - if err := h.Init(mdx.NewMetadata(cfg.Handler.Metadata)); err != nil { - return err + if err = h.Init(mdx.NewMetadata(cfg.Handler.Metadata)); err != nil { + return } s.file = xservice.NewService(s.opts.Name, ln, h, xservice.LoggerOption(log)) } { - ch, err := chain_parser.ParseChain(s.config.Chains[0], log) + var ch chain.Chainer + ch, err = chain_parser.ParseChain(s.config.Chains[0], log) if err != nil { log.Error(err) - return err + return } cfg := s.config.Services[1] @@ -189,15 +198,15 @@ func (s *fileTunnel) Run() error { listener.ChainOption(ch), listener.LoggerOption(log.WithFields(map[string]any{"kind": "listener", "listener": "rtcp"})), ) - if err := ln.Init(mdx.NewMetadata(cfg.Listener.Metadata)); err != nil { - return err + if err = ln.Init(mdx.NewMetadata(cfg.Listener.Metadata)); err != nil { + return } h := remote.NewHandler( handler.LoggerOption(log.WithFields(map[string]any{"kind": "handler", "handler": "rtcp"})), ) - if err := h.Init(mdx.NewMetadata(cfg.Handler.Metadata)); err != nil { - return err + if err = h.Init(mdx.NewMetadata(cfg.Handler.Metadata)); err != nil { + return } if forwarder, ok := h.(handler.Forwarder); ok { forwarder.Forward(hop.NewHop( @@ -210,7 +219,9 @@ func (s *fileTunnel) Run() error { } go s.file.Serve() - go s.forward.Serve() + go func() { + s.setErr(s.forward.Serve()) + }() log.Infof("file service run at %s", s.file.Addr()) return nil @@ -242,3 +253,15 @@ func (s *fileTunnel) IsClosed() bool { return false } } + +func (s *fileTunnel) setErr(err error) { + s.mu.Lock() + defer s.mu.Unlock() + s.err = err +} + +func (s *fileTunnel) Err() error { + s.mu.RLock() + defer s.mu.RUnlock() + return s.err +} diff --git a/tunnel/http.go b/tunnel/http.go index ac384ef..8223b4e 100644 --- a/tunnel/http.go +++ b/tunnel/http.go @@ -4,6 +4,7 @@ import ( "crypto/md5" "encoding/hex" "fmt" + "sync" "sync/atomic" "github.com/go-gost/core/chain" @@ -30,6 +31,9 @@ type httpTunnel struct { favorite atomic.Bool cclose chan struct{} + + err error + mu sync.RWMutex } func NewHTTPTunnel(opts ...Option) Tunnel { @@ -50,7 +54,7 @@ func NewHTTPTunnel(opts ...Option) Tunnel { } if options.Name == "" { - options.Name = fmt.Sprintf("HTTP-%s", endpoint) + options.Name = endpoint } s := &httpTunnel{ @@ -78,8 +82,8 @@ func (s *httpTunnel) Endpoint() string { return s.opts.Endpoint } -func (f *httpTunnel) Entrypoint() string { - return fmt.Sprintf("https://%s.%s", f.endpoint, endpointAddr) +func (s *httpTunnel) Entrypoint() string { + return fmt.Sprintf("https://%s.%s", s.endpoint, EndpointAddr) } func (s *httpTunnel) Options() Options { @@ -134,18 +138,22 @@ func (s *httpTunnel) init() error { s.config = &config.Config{ Services: []*config.ServiceConfig{rtcp}, - Chains: []*config.ChainConfig{chainConfig(s.opts.ID, s.opts.Name)}, + Chains: []*config.ChainConfig{ChainConfig(s.opts.ID, s.opts.Name)}, } return nil } -func (s *httpTunnel) Run() error { +func (s *httpTunnel) Run() (err error) { if s.IsClosed() { return ErrTunnelClosed } - if err := s.init(); err != nil { - return err + defer func() { + s.setErr(err) + }() + + if err = s.init(); err != nil { + return } log := logger.Default().WithFields(map[string]any{ @@ -154,10 +162,11 @@ func (s *httpTunnel) Run() error { }) { - ch, err := chain_parser.ParseChain(s.config.Chains[0], log) + var ch chain.Chainer + ch, err = chain_parser.ParseChain(s.config.Chains[0], log) if err != nil { log.Error(err) - return err + return } cfg := s.config.Services[0] @@ -166,15 +175,15 @@ func (s *httpTunnel) Run() error { listener.ChainOption(ch), listener.LoggerOption(log.WithFields(map[string]any{"kind": "listener", "listener": "rtcp"})), ) - if err := ln.Init(mdx.NewMetadata(cfg.Listener.Metadata)); err != nil { - return err + if err = ln.Init(mdx.NewMetadata(cfg.Listener.Metadata)); err != nil { + return } h := remote.NewHandler( handler.LoggerOption(log.WithFields(map[string]any{"kind": "handler", "handler": "rtcp"})), ) - if err := h.Init(mdx.NewMetadata(cfg.Handler.Metadata)); err != nil { - return err + if err = h.Init(mdx.NewMetadata(cfg.Handler.Metadata)); err != nil { + return } node := cfg.Forwarder.Nodes[0] @@ -203,7 +212,9 @@ func (s *httpTunnel) Run() error { s.forward = xservice.NewService(s.opts.Name, ln, h, xservice.LoggerOption(log)) } - go s.forward.Serve() + go func() { + s.setErr(s.forward.Serve()) + }() return nil } @@ -231,3 +242,15 @@ func (s *httpTunnel) IsClosed() bool { return false } } + +func (s *httpTunnel) setErr(err error) { + s.mu.Lock() + defer s.mu.Unlock() + s.err = err +} + +func (s *httpTunnel) Err() error { + s.mu.RLock() + defer s.mu.RUnlock() + return s.err +} diff --git a/tunnel/tcp.go b/tunnel/tcp.go index a4ce61c..8f6680b 100644 --- a/tunnel/tcp.go +++ b/tunnel/tcp.go @@ -4,6 +4,7 @@ import ( "crypto/md5" "encoding/hex" "fmt" + "sync" "sync/atomic" "github.com/go-gost/core/chain" @@ -29,6 +30,9 @@ type tcpTunnel struct { favorite atomic.Bool cclose chan struct{} + + err error + mu sync.RWMutex } func NewTCPTunnel(opts ...Option) Tunnel { @@ -49,7 +53,7 @@ func NewTCPTunnel(opts ...Option) Tunnel { } if options.Name == "" { - options.Name = fmt.Sprintf("TCP-%s", endpoint) + options.Name = endpoint } s := &tcpTunnel{ @@ -77,8 +81,8 @@ func (s *tcpTunnel) Endpoint() string { return s.opts.Endpoint } -func (f *tcpTunnel) Entrypoint() string { - return fmt.Sprintf("%s.%s", f.endpoint, endpointAddr) +func (s *tcpTunnel) Entrypoint() string { + return fmt.Sprintf("%s.%s", s.endpoint, EndpointAddr) } func (s *tcpTunnel) Options() Options { @@ -116,18 +120,22 @@ func (s *tcpTunnel) init() error { s.config = &config.Config{ Services: []*config.ServiceConfig{rtcp}, - Chains: []*config.ChainConfig{chainConfig(s.opts.ID, s.opts.Name)}, + Chains: []*config.ChainConfig{ChainConfig(s.opts.ID, s.opts.Name)}, } return nil } -func (s *tcpTunnel) Run() error { +func (s *tcpTunnel) Run() (err error) { if s.IsClosed() { return ErrTunnelClosed } - if err := s.init(); err != nil { - return err + defer func() { + s.setErr(err) + }() + + if err = s.init(); err != nil { + return } log := logger.Default().WithFields(map[string]any{ @@ -136,10 +144,11 @@ func (s *tcpTunnel) Run() error { }) { - ch, err := chain_parser.ParseChain(s.config.Chains[0], log) + var ch chain.Chainer + ch, err = chain_parser.ParseChain(s.config.Chains[0], log) if err != nil { log.Error(err) - return err + return } cfg := s.config.Services[0] @@ -148,15 +157,15 @@ func (s *tcpTunnel) Run() error { listener.ChainOption(ch), listener.LoggerOption(log.WithFields(map[string]any{"kind": "listener", "listener": "rtcp"})), ) - if err := ln.Init(mdx.NewMetadata(cfg.Listener.Metadata)); err != nil { - return err + if err = ln.Init(mdx.NewMetadata(cfg.Listener.Metadata)); err != nil { + return } h := remote.NewHandler( handler.LoggerOption(log.WithFields(map[string]any{"kind": "handler", "handler": "rtcp"})), ) - if err := h.Init(mdx.NewMetadata(cfg.Handler.Metadata)); err != nil { - return err + if err = h.Init(mdx.NewMetadata(cfg.Handler.Metadata)); err != nil { + return } node := cfg.Forwarder.Nodes[0] @@ -169,7 +178,9 @@ func (s *tcpTunnel) Run() error { s.forward = xservice.NewService(s.opts.Name, ln, h, xservice.LoggerOption(log)) } - go s.forward.Serve() + go func() { + s.setErr(s.forward.Serve()) + }() return nil } @@ -197,3 +208,15 @@ func (s *tcpTunnel) IsClosed() bool { return false } } + +func (s *tcpTunnel) setErr(err error) { + s.mu.Lock() + defer s.mu.Unlock() + s.err = err +} + +func (s *tcpTunnel) Err() error { + s.mu.RLock() + defer s.mu.RUnlock() + return s.err +} diff --git a/tunnel/tunnel.go b/tunnel/tunnel.go index ead7482..ee9fe8c 100644 --- a/tunnel/tunnel.go +++ b/tunnel/tunnel.go @@ -12,9 +12,9 @@ import ( ) const ( - endpointAddr = "gost.plus" - serverName = "tunnel.gost.plus" - serverAddr = serverName + ":443" + EndpointAddr = "gost.plus" + ServerName = "tunnel.gost.plus" + ServerAddr = ServerName + ":443" ) const ( @@ -94,54 +94,64 @@ type Tunnel interface { IsFavorite() bool Close() error IsClosed() bool + Err() error } type tunnelList struct { - tunnels []Tunnel - mux sync.RWMutex + list []Tunnel + mux sync.RWMutex } -func (sl *tunnelList) Count() int { - sl.mux.RLock() - defer sl.mux.RUnlock() - return len(sl.tunnels) +var ( + tunnels tunnelList +) + +func Count() int { + tunnels.mux.RLock() + defer tunnels.mux.RUnlock() + return len(tunnels.list) } -func (sl *tunnelList) Add(s Tunnel) { - sl.mux.Lock() - defer sl.mux.Unlock() - sl.tunnels = append(sl.tunnels, s) +func Add(s Tunnel) { + tunnels.mux.Lock() + defer tunnels.mux.Unlock() + tunnels.list = append(tunnels.list, s) } -func (sl *tunnelList) Set(s Tunnel) { +func Set(s Tunnel) { if s == nil { return } + t := Get(s.ID()) + if t == nil { + return + } + s.Favorite(t.IsFavorite()) - sl.mux.Lock() - defer sl.mux.Unlock() + tunnels.mux.Lock() + defer tunnels.mux.Unlock() - for i, sv := range sl.tunnels { + for i, sv := range tunnels.list { if sv != nil && sv.ID() == s.ID() { - sl.tunnels[i] = s + tunnels.list[i] = s } } } -func (sl *tunnelList) Get(index int) Tunnel { - sl.mux.RLock() - defer sl.mux.RUnlock() - if index < 0 || index >= len(sl.tunnels) { +func GetIndex(index int) Tunnel { + tunnels.mux.RLock() + defer tunnels.mux.RUnlock() + if index < 0 || index >= len(tunnels.list) { return nil } - return sl.tunnels[index] + return tunnels.list[index] } -func (sl *tunnelList) GetID(id string) Tunnel { - sl.mux.RLock() - defer sl.mux.RUnlock() +func Get(id string) Tunnel { + tunnels.mux.RLock() + defer tunnels.mux.RUnlock() - for _, s := range sl.tunnels { + for _, s := range tunnels.list { if s != nil && s.ID() == id { return s } @@ -149,53 +159,20 @@ func (sl *tunnelList) GetID(id string) Tunnel { return nil } -func (sl *tunnelList) DeleteID(id string) { - sl.mux.Lock() - defer sl.mux.Unlock() +func Delete(id string) { + tunnels.mux.Lock() + defer tunnels.mux.Unlock() - for i, s := range sl.tunnels { + for i, s := range tunnels.list { if s != nil && s.ID() == id { s.Close() - sl.tunnels[i] = nil + tunnels.list[i] = nil return } } } -var ( - tunnels tunnelList -) - -func TunnelCount() int { - return tunnels.Count() -} - -func AddTunnel(s Tunnel) { - tunnels.Add(s) -} - -func SetTunnel(s Tunnel) { - t := tunnels.GetID(s.ID()) - if t == nil { - return - } - s.Favorite(t.IsFavorite()) - tunnels.Set(s) -} - -func GetTunnel(index int) Tunnel { - return tunnels.Get(index) -} - -func GetTunnelID(id string) Tunnel { - return tunnels.GetID(id) -} - -func DeleteTunnel(id string) { - tunnels.DeleteID(id) -} - -func chainConfig(id string, name string) *xconfig.ChainConfig { +func ChainConfig(id string, name string) *xconfig.ChainConfig { return &xconfig.ChainConfig{ Name: name, Hops: []*xconfig.HopConfig{ @@ -204,7 +181,7 @@ func chainConfig(id string, name string) *xconfig.ChainConfig { Nodes: []*xconfig.NodeConfig{ { Name: name, - Addr: serverAddr, + Addr: ServerAddr, Connector: &xconfig.ConnectorConfig{ Type: "tunnel", Metadata: map[string]any{"tunnel.id": id}, @@ -213,7 +190,7 @@ func chainConfig(id string, name string) *xconfig.ChainConfig { Type: "wss", TLS: &xconfig.TLSConfig{ Secure: true, - ServerName: serverName, + ServerName: ServerName, }, }, }, @@ -249,16 +226,16 @@ func LoadConfig() { } s.Favorite(tun.Favorite) - tunnels.Add(s) + Add(s) } } -func SaveTunnel() error { +func SaveConfig() error { cfg := config.Global() cfg.Tunnels = nil - for i := 0; i < tunnels.Count(); i++ { - tun := tunnels.Get(i) + for i := 0; i < Count(); i++ { + tun := GetIndex(i) if tun == nil { continue } diff --git a/tunnel/udp.go b/tunnel/udp.go index cf3911e..9d7516f 100644 --- a/tunnel/udp.go +++ b/tunnel/udp.go @@ -4,6 +4,7 @@ import ( "crypto/md5" "encoding/hex" "fmt" + "sync" "sync/atomic" "github.com/go-gost/core/chain" @@ -29,6 +30,9 @@ type udpTunnel struct { favorite atomic.Bool cclose chan struct{} + + err error + mu sync.RWMutex } func NewUDPTunnel(opts ...Option) Tunnel { @@ -49,7 +53,7 @@ func NewUDPTunnel(opts ...Option) Tunnel { } if options.Name == "" { - options.Name = fmt.Sprintf("UDP-%s", endpoint) + options.Name = endpoint } s := &udpTunnel{ @@ -77,8 +81,8 @@ func (s *udpTunnel) Endpoint() string { return s.opts.Endpoint } -func (f *udpTunnel) Entrypoint() string { - return fmt.Sprintf("%s.%s", f.endpoint, endpointAddr) +func (s *udpTunnel) Entrypoint() string { + return fmt.Sprintf("%s.%s", s.endpoint, EndpointAddr) } func (s *udpTunnel) Options() Options { @@ -116,18 +120,22 @@ func (s *udpTunnel) init() error { s.config = &config.Config{ Services: []*config.ServiceConfig{rudp}, - Chains: []*config.ChainConfig{chainConfig(s.opts.ID, s.opts.Name)}, + Chains: []*config.ChainConfig{ChainConfig(s.opts.ID, s.opts.Name)}, } return nil } -func (s *udpTunnel) Run() error { +func (s *udpTunnel) Run() (err error) { if s.IsClosed() { return ErrTunnelClosed } - if err := s.init(); err != nil { - return err + defer func() { + s.setErr(err) + }() + + if err = s.init(); err != nil { + return } log := logger.Default().WithFields(map[string]any{ @@ -136,10 +144,11 @@ func (s *udpTunnel) Run() error { }) { - ch, err := chain_parser.ParseChain(s.config.Chains[0], log) + var ch chain.Chainer + ch, err = chain_parser.ParseChain(s.config.Chains[0], log) if err != nil { log.Error(err) - return err + return } cfg := s.config.Services[0] @@ -148,15 +157,15 @@ func (s *udpTunnel) Run() error { listener.ChainOption(ch), listener.LoggerOption(log.WithFields(map[string]any{"kind": "listener", "listener": "rudp"})), ) - if err := ln.Init(mdx.NewMetadata(cfg.Listener.Metadata)); err != nil { - return err + if err = ln.Init(mdx.NewMetadata(cfg.Listener.Metadata)); err != nil { + return } h := remote.NewHandler( handler.LoggerOption(log.WithFields(map[string]any{"kind": "handler", "handler": "rudp"})), ) - if err := h.Init(mdx.NewMetadata(cfg.Handler.Metadata)); err != nil { - return err + if err = h.Init(mdx.NewMetadata(cfg.Handler.Metadata)); err != nil { + return } node := cfg.Forwarder.Nodes[0] @@ -169,7 +178,9 @@ func (s *udpTunnel) Run() error { s.forward = xservice.NewService(s.opts.Name, ln, h, xservice.LoggerOption(log)) } - go s.forward.Serve() + go func() { + s.setErr(s.forward.Serve()) + }() return nil } @@ -197,3 +208,15 @@ func (s *udpTunnel) IsClosed() bool { return false } } + +func (s *udpTunnel) setErr(err error) { + s.mu.Lock() + defer s.mu.Unlock() + s.err = err +} + +func (s *udpTunnel) Err() error { + s.mu.RLock() + defer s.mu.RUnlock() + return s.err +} diff --git a/ui/icons/icons.go b/ui/icons/icons.go index 7f1ab36..f31da71 100644 --- a/ui/icons/icons.go +++ b/ui/icons/icons.go @@ -45,6 +45,7 @@ var ( IconStop = mustIcon(icons.AVStop) IconBack = mustIcon(icons.NavigationArrowBack) IconClose = mustIcon(icons.ContentClear) + IconCopy = mustIcon(icons.ContentContentCopy) ) func mustIcon(data []byte) *widget.Icon { diff --git a/ui/page/entrypoint.go b/ui/page/entrypoint.go new file mode 100644 index 0000000..62fb27f --- /dev/null +++ b/ui/page/entrypoint.go @@ -0,0 +1,177 @@ +package page + +import ( + "fmt" + "image/color" + "strings" + "sync/atomic" + + "gioui.org/font" + "gioui.org/layout" + "gioui.org/op" + "gioui.org/widget" + "gioui.org/widget/material" + "gioui.org/x/component" + "github.com/go-gost/gost-plus/tunnel/entrypoint" + "github.com/go-gost/gost-plus/ui/icons" + "golang.org/x/exp/shiny/materialdesign/colornames" +) + +type entryPointState struct { + editor widget.Clickable +} + +type entryPointPage struct { + router *Router + + list layout.List + + wgFavorite widget.Clickable + wgAdd widget.Clickable + + entryPoints map[int]*entryPointState + favorite atomic.Bool +} + +func NewEntryPointPage(r *Router) Page { + return &entryPointPage{ + router: r, + list: layout.List{ + Axis: layout.Vertical, + Alignment: layout.Middle, + }, + entryPoints: make(map[int]*entryPointState), + } +} + +func (p *entryPointPage) Init(opts ...PageOption) { + p.router.bar.SetActions( + []component.AppBarAction{ + { + OverflowAction: component.OverflowAction{ + Name: "Favorite", + Tag: &p.wgFavorite, + }, + Layout: func(gtx C, bg, fg color.NRGBA) D { + if p.wgFavorite.Clicked(gtx) { + p.favorite.Store(!p.favorite.Load()) + } + + btn := component.SimpleIconButton(bg, fg, &p.wgFavorite, icons.IconFavorite) + if p.favorite.Load() { + btn.Color = color.NRGBA(colornames.Red500) + } else { + btn.Color = fg + } + return btn.Layout(gtx) + }, + }, + { + OverflowAction: component.OverflowAction{ + Name: "Add", + Tag: &p.wgAdd, + }, + Layout: func(gtx C, bg, fg color.NRGBA) D { + if p.wgAdd.Clicked(gtx) { + p.router.SwitchTo(Route{Path: PageMenu}) + } + return component.SimpleIconButton(bg, fg, &p.wgAdd, icons.IconAdd).Layout(gtx) + }, + }, + }, + []component.OverflowAction{ + { + Name: "About", + Tag: OverflowActionAbout, + }, + }, + ) + + p.router.bar.Title = "EntryPoints" + p.router.bar.NavigationIcon = icons.IconHome +} + +func (p *entryPointPage) Layout(gtx C, th *material.Theme) D { + favorite := p.favorite.Load() + // gtx.Constraints.Min.X = gtx.Constraints.Max.X + return p.list.Layout(gtx, entrypoint.Count(), func(gtx C, index int) D { + s := entrypoint.GetIndex(index) + if s == nil { + delete(p.entryPoints, index) + return D{} + } + + if p.entryPoints[index] == nil { + p.entryPoints[index] = &entryPointState{} + } + + if favorite && !s.IsFavorite() { + return 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 { + state := p.entryPoints[index] + if state.editor.Clicked(gtx) { + switch s.Type() { + case entrypoint.TCPEntryPoint: + p.router.SwitchTo(Route{Path: PageEditTCPEntryPoint, ID: s.ID()}) + } + op.InvalidateOp{}.Add(gtx.Ops) + } + return state.editor.Layout(gtx, func(gtx C) D { + return layout.UniformInset(10).Layout(gtx, func(gtx C) D { + return p.layout(gtx, th, s) + }) + }) + }) + }) + }) + }) +} + +func (p *entryPointPage) layout(gtx C, th *material.Theme, ep entrypoint.EntryPoint) D { + return layout.Flex{ + Alignment: layout.Middle, + Spacing: layout.SpaceBetween, + }.Layout(gtx, + layout.Flexed(1, func(gtx C) D { + return layout.Flex{Axis: layout.Vertical}.Layout(gtx, + layout.Rigid(func(gtx C) D { + label := material.Body1(th, ep.ID()) + label.Font.Weight = font.Bold + return label.Layout(gtx) + }), + layout.Rigid(layout.Spacer{Height: 5}.Layout), + layout.Rigid(material.Body2(th, fmt.Sprintf("Type: %s", strings.ToUpper(ep.Type()))).Layout), + layout.Rigid(layout.Spacer{Height: 5}.Layout), + layout.Rigid(material.Body2(th, fmt.Sprintf("Name: %s", ep.Name())).Layout), + layout.Rigid(layout.Spacer{Height: 5}.Layout), + layout.Rigid(material.Body2(th, fmt.Sprintf("Endpoint: %s", ep.Endpoint())).Layout), + layout.Rigid(layout.Spacer{Height: 5}.Layout), + layout.Rigid(material.Body2(th, fmt.Sprintf("Entrypoint: %s", ep.Entrypoint())).Layout), + layout.Rigid(layout.Spacer{Height: 5}.Layout), + layout.Rigid(func(gtx C) D { + if err := ep.Err(); !ep.IsClosed() && err != nil { + label := material.Body2(th, err.Error()) + label.Color = color.NRGBA(colornames.Red500) + return label.Layout(gtx) + } + return D{} + }), + ) + }), + layout.Rigid(layout.Spacer{Width: 10}.Layout), + layout.Rigid(func(gtx C) D { + c := colornames.Green500 + if ep.Err() != nil { + c = colornames.Red500 + } + if ep.IsClosed() { + c = colornames.Grey500 + } + return icons.IconTunnelState.Layout(gtx, color.NRGBA(c)) + }), + ) +} diff --git a/ui/page/entrypoint_tcp.go b/ui/page/entrypoint_tcp.go new file mode 100644 index 0000000..a8484dc --- /dev/null +++ b/ui/page/entrypoint_tcp.go @@ -0,0 +1,405 @@ +package page + +import ( + "fmt" + "image/color" + "log" + "net" + "strings" + + "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 tcpEntryPointAddPage struct { + router *Router + + list layout.List + wgDone widget.Clickable + + name component.TextField + tunnelID component.TextField + addr component.TextField +} + +func NewTCPEntryPointAddPage(r *Router) Page { + return &tcpEntryPointAddPage{ + 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, + }, + }, + } +} + +func (p *tcpEntryPointAddPage) Init(opts ...PageOption) { + p.name.SetText("") + p.tunnelID.SetText("") + p.addr.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.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 = "TCP" + p.router.bar.NavigationIcon = icons.IconClose +} + +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 { + 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 *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(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.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") + }), + ) +} + +func (p *tcpEntryPointAddPage) createEntryPoint() error { + tun := 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) + + if err := tun.Run(); err != nil { + tun.Close() + return err + } + + return nil +} + +type tcpEntryPointEditPage 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 +} + +func NewTCPEntryPointEditPage(r *Router) Page { + return &tcpEntryPointEditPage{ + 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, + }, + }, + } +} + +func (p *tcpEntryPointEditPage) 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) + } + + 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() { + s = p.createEntryPoint() + } 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.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 = "TCP" + 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())), + ) + + if err := s.Run(); err != nil { + log.Println(err) + } + entrypoint.Set(s) + + return s +} + +func (p *tcpEntryPointEditPage) 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 *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) + }), + 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") + }), + ) +} diff --git a/ui/page/file.go b/ui/page/file.go index 901c34b..340ccea 100644 --- a/ui/page/file.go +++ b/ui/page/file.go @@ -75,9 +75,9 @@ func (p *fileAddPage) Init(opts ...PageOption) { }, Layout: func(gtx C, bg, fg color.NRGBA) D { if p.wgDone.Clicked(gtx) { - defer p.router.SwitchTo(Route{Path: PageHome}) + defer p.router.SwitchTo(Route{Path: PageTunnel}) p.createTunnel() - tunnel.SaveTunnel() + tunnel.SaveConfig() } return component.SimpleIconButton(bg, fg, &p.wgDone, icons.IconDone).Layout(gtx) }, @@ -186,11 +186,13 @@ func (p *fileAddPage) createTunnel() error { tunnel.PasswordOption(password), ) + tunnel.Add(tun) + if err := tun.Run(); err != nil { + tun.Close() return err } - tunnel.AddTunnel(tun) return nil } @@ -252,7 +254,7 @@ func (p *fileEditPage) Init(opts ...PageOption) { } p.id = options.ID - s := tunnel.GetTunnelID(p.id) + s := tunnel.Get(p.id) if s != nil { sopts := s.Options() p.name.SetText(sopts.Name) @@ -271,14 +273,14 @@ func (p *fileEditPage) Init(opts ...PageOption) { Tag: &p.wgFavorite, }, Layout: func(gtx C, bg, fg color.NRGBA) D { - s := tunnel.GetTunnelID(p.id) + s := tunnel.Get(p.id) if s == nil { return D{} } if p.wgFavorite.Clicked(gtx) { s.Favorite(!s.IsFavorite()) - tunnel.SaveTunnel() + tunnel.SaveConfig() } btn := component.SimpleIconButton(bg, fg, &p.wgFavorite, icons.IconFavorite) @@ -296,7 +298,7 @@ func (p *fileEditPage) Init(opts ...PageOption) { Tag: &p.wgState, }, Layout: func(gtx C, bg, fg color.NRGBA) D { - s := tunnel.GetTunnelID(p.id) + s := tunnel.Get(p.id) if s == nil { return D{} } @@ -307,7 +309,7 @@ func (p *fileEditPage) Init(opts ...PageOption) { } else { s.Close() } - tunnel.SaveTunnel() + tunnel.SaveConfig() } if s != nil && !s.IsClosed() { @@ -324,9 +326,9 @@ func (p *fileEditPage) Init(opts ...PageOption) { }, Layout: func(gtx C, bg, fg color.NRGBA) D { if p.wgDelete.Clicked(gtx) { - tunnel.DeleteTunnel(p.id) - tunnel.SaveTunnel() - p.router.SwitchTo(Route{Path: PageHome}) + tunnel.Delete(p.id) + tunnel.SaveConfig() + p.router.SwitchTo(Route{Path: PageTunnel}) } return component.SimpleIconButton(bg, fg, &p.wgDelete, icons.IconDelete).Layout(gtx) }, @@ -338,12 +340,12 @@ func (p *fileEditPage) Init(opts ...PageOption) { }, Layout: func(gtx C, bg, fg color.NRGBA) D { if p.wgDone.Clicked(gtx) { - defer p.router.SwitchTo(Route{Path: PageHome}) + defer p.router.SwitchTo(Route{Path: PageTunnel}) - if s := tunnel.GetTunnelID(p.id); s != nil { + if s := tunnel.Get(p.id); s != nil { s.Close() p.createTunnel() - tunnel.SaveTunnel() + tunnel.SaveConfig() } } return component.SimpleIconButton(bg, fg, &p.wgDone, icons.IconDone).Layout(gtx) @@ -361,7 +363,7 @@ func (p *fileEditPage) createTunnel() tunnel.Tunnel { username = strings.TrimSpace(p.username.Text()) password = strings.TrimSpace(p.password.Text()) } - s := tunnel.NewFileTunnel( + tun := tunnel.NewFileTunnel( tunnel.IDOption(p.id), tunnel.NameOption(strings.TrimSpace(p.name.Text())), tunnel.EndpointOption(strings.TrimSpace(p.path.Text())), @@ -369,12 +371,14 @@ func (p *fileEditPage) createTunnel() tunnel.Tunnel { tunnel.PasswordOption(password), ) - if err := s.Run(); err != nil { + tunnel.Set(tun) + + if err := tun.Run(); err != nil { + tun.Close() log.Println(err) } - tunnel.SetTunnel(s) - return s + return tun } func (p *fileEditPage) Layout(gtx C, th *material.Theme) D { @@ -396,7 +400,7 @@ func (p *fileEditPage) layout(gtx C, th *material.Theme) D { Axis: layout.Vertical, }.Layout(gtx, layout.Rigid(func(gtx C) D { - return layoutHeader(gtx, th, tunnel.GetTunnelID(p.id), &p.wgID, &p.wgEntrypoint) + return layoutHeader(gtx, th, tunnel.Get(p.id), &p.wgID, &p.wgEntrypoint) }), layout.Rigid(func(gtx C) D { div := component.Divider(th) diff --git a/ui/page/http.go b/ui/page/http.go index 9f4afe9..7d95aa8 100644 --- a/ui/page/http.go +++ b/ui/page/http.go @@ -88,9 +88,9 @@ func (p *httpAddPage) Init(opts ...PageOption) { }, Layout: func(gtx C, bg, fg color.NRGBA) D { if p.wgDone.Clicked(gtx) { - defer p.router.SwitchTo(Route{Path: PageHome}) + defer p.router.SwitchTo(Route{Path: PageTunnel}) p.createTunnel() - tunnel.SaveTunnel() + tunnel.SaveConfig() } return component.SimpleIconButton(bg, fg, &p.wgDone, icons.IconDone).Layout(gtx) }, @@ -210,7 +210,7 @@ func (p *httpAddPage) createTunnel() error { if p.bHost.Value { hostname = strings.TrimSpace(p.hostname.Text()) } - srv := tunnel.NewHTTPTunnel( + tun := tunnel.NewHTTPTunnel( tunnel.NameOption(strings.TrimSpace(p.name.Text())), tunnel.EndpointOption(strings.TrimSpace(p.addr.Text())), tunnel.UsernameOption(username), @@ -219,11 +219,13 @@ func (p *httpAddPage) createTunnel() error { tunnel.EnableTLSOption(p.bTLS.Value), ) - if err := srv.Run(); err != nil { + tunnel.Add(tun) + + if err := tun.Run(); err != nil { + tun.Close() return err } - tunnel.AddTunnel(srv) return nil } @@ -296,7 +298,7 @@ func (p *httpEditPage) Init(opts ...PageOption) { } p.id = options.ID - s := tunnel.GetTunnelID(p.id) + s := tunnel.Get(p.id) if s != nil { sopts := s.Options() p.name.SetText(sopts.Name) @@ -320,14 +322,14 @@ func (p *httpEditPage) Init(opts ...PageOption) { Tag: &p.wgFavorite, }, Layout: func(gtx C, bg, fg color.NRGBA) D { - s := tunnel.GetTunnelID(p.id) + s := tunnel.Get(p.id) if s == nil { return D{} } if p.wgFavorite.Clicked(gtx) { s.Favorite(!s.IsFavorite()) - tunnel.SaveTunnel() + tunnel.SaveConfig() } btn := component.SimpleIconButton(bg, fg, &p.wgFavorite, icons.IconFavorite) @@ -345,14 +347,14 @@ func (p *httpEditPage) Init(opts ...PageOption) { Tag: &p.wgState, }, Layout: func(gtx C, bg, fg color.NRGBA) D { - s := tunnel.GetTunnelID(p.id) + s := tunnel.Get(p.id) if p.wgState.Clicked(gtx) && s != nil { if s.IsClosed() { s = p.createTunnel() } else { s.Close() } - tunnel.SaveTunnel() + tunnel.SaveConfig() } if s != nil && !s.IsClosed() { @@ -369,9 +371,9 @@ func (p *httpEditPage) Init(opts ...PageOption) { }, Layout: func(gtx C, bg, fg color.NRGBA) D { if p.wgDelete.Clicked(gtx) { - tunnel.DeleteTunnel(p.id) - tunnel.SaveTunnel() - p.router.SwitchTo(Route{Path: PageHome}) + tunnel.Delete(p.id) + tunnel.SaveConfig() + p.router.SwitchTo(Route{Path: PageTunnel}) } return component.SimpleIconButton(bg, fg, &p.wgDelete, icons.IconDelete).Layout(gtx) }, @@ -383,12 +385,12 @@ func (p *httpEditPage) Init(opts ...PageOption) { }, Layout: func(gtx C, bg, fg color.NRGBA) D { if p.wgDone.Clicked(gtx) { - defer p.router.SwitchTo(Route{Path: PageHome}) + defer p.router.SwitchTo(Route{Path: PageTunnel}) - if s := tunnel.GetTunnelID(p.id); s != nil { + if s := tunnel.Get(p.id); s != nil { s.Close() p.createTunnel() - tunnel.SaveTunnel() + tunnel.SaveConfig() } } return component.SimpleIconButton(bg, fg, &p.wgDone, icons.IconDone).Layout(gtx) @@ -410,7 +412,7 @@ func (p *httpEditPage) createTunnel() tunnel.Tunnel { if p.bHost.Value { hostname = strings.TrimSpace(p.hostname.Text()) } - s := tunnel.NewHTTPTunnel( + tun := tunnel.NewHTTPTunnel( tunnel.IDOption(p.id), tunnel.NameOption(strings.TrimSpace(p.name.Text())), tunnel.EndpointOption(strings.TrimSpace(p.addr.Text())), @@ -420,12 +422,14 @@ func (p *httpEditPage) createTunnel() tunnel.Tunnel { tunnel.EnableTLSOption(p.bTLS.Value), ) - if err := s.Run(); err != nil { + tunnel.Set(tun) + + if err := tun.Run(); err != nil { + tun.Close() log.Println(err) } - tunnel.SetTunnel(s) - return s + return tun } func (p *httpEditPage) Layout(gtx C, th *material.Theme) D { @@ -447,7 +451,7 @@ func (p *httpEditPage) layout(gtx C, th *material.Theme) D { Axis: layout.Vertical, }.Layout(gtx, layout.Rigid(func(gtx C) D { - return layoutHeader(gtx, th, tunnel.GetTunnelID(p.id), &p.wgID, &p.wgEntrypoint) + return layoutHeader(gtx, th, tunnel.Get(p.id), &p.wgID, &p.wgEntrypoint) }), layout.Rigid(layout.Spacer{Height: 10}.Layout), layout.Rigid(func(gtx C) D { @@ -466,7 +470,7 @@ func (p *httpEditPage) layout(gtx C, th *material.Theme) D { if addr == "" { return nil } - if _, _, err := net.SplitHostPort(addr); err != nil { + if _, err := net.ResolveTCPAddr("tcp", addr); err != nil { return fmt.Errorf("invalid address format, should be [IP]:PORT or [HOST]:PORT") } return nil diff --git a/ui/page/menu.go b/ui/page/menu.go index 972ebba..d593c7e 100644 --- a/ui/page/menu.go +++ b/ui/page/menu.go @@ -22,6 +22,9 @@ type menuPage struct { wgHTTP widget.Clickable wgTCP widget.Clickable wgUDP widget.Clickable + + wgEntryPointTCP widget.Clickable + wgEntryPointUDP widget.Clickable } func NewMenuPage(r *Router) Page { @@ -59,6 +62,10 @@ func (p *menuPage) Layout(gtx C, th *material.Theme) D { p.router.SwitchTo(Route{Path: PageNewUDP}) return true } + if p.wgEntryPointTCP.Clicked(gtx) { + p.router.SwitchTo(Route{Path: PageNewTCPEntryPoint}) + return true + } return false }(); clicked { @@ -120,6 +127,23 @@ func (p *menuPage) Layout(gtx C, th *material.Theme) D { }) }) }), + layout.Rigid(layout.Spacer{Height: 10}.Layout), + layout.Rigid(func(gtx C) D { + label := material.H6(th, "EntryPoints") + label.Font.Weight = font.Bold + return layout.Inset{Top: 5, Bottom: 5}.Layout(gtx, label.Layout) + }), + 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.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") + }) + }) + }) + }) + }), ) }) }) @@ -141,14 +165,14 @@ func (p *menuPage) layoutCard(gtx C, th *material.Theme, name, desc string) D { title.Font.Weight = font.Bold return title.Layout(gtx) }), - layout.Rigid(layout.Spacer{Height: 10}.Layout), + layout.Rigid(layout.Spacer{Height: 5}.Layout), layout.Rigid(func(gtx C) D { title := material.Body1(th, desc) return title.Layout(gtx) }), ) }), - layout.Rigid(layout.Spacer{Width: 10}.Layout), + layout.Rigid(layout.Spacer{Width: 5}.Layout), layout.Rigid(func(gtx C) D { return icons.IconForward.Layout(gtx, color.NRGBA(colornames.Grey500)) }), diff --git a/ui/page/page.go b/ui/page/page.go index 7d756ff..d955be2 100644 --- a/ui/page/page.go +++ b/ui/page/page.go @@ -9,12 +9,13 @@ import ( "gioui.org/widget" "gioui.org/widget/material" "github.com/go-gost/gost-plus/tunnel" + "github.com/go-gost/gost-plus/ui/icons" "golang.org/x/exp/shiny/materialdesign/colornames" ) const ( - PageHome = "/" PageMenu = "/menu" + PageTunnel = "/tunnel" PageNewFile = "/tunnel/file/create" PageNewHTTP = "/tunnel/http/create" PageNewTCP = "/tunnel/tcp/create" @@ -24,6 +25,10 @@ const ( PageEditTCP = "/tunnel/tcp/edit" PageEditUDP = "/tunnel/udp/edit" + PageEntryPoint = "/entrypoint" + PageNewTCPEntryPoint = "/entrypoint/tcp/create" + PageEditTCPEntryPoint = "/entrypoint/tcp/edit" + PageAbout = "/about" ) @@ -74,15 +79,13 @@ func layoutHeader(gtx C, th *material.Theme, tun tunnel.Tunnel, wgID, wgEntrypoi label.Font.Weight = font.Bold return label.Layout(gtx) }), - layout.Rigid(layout.Spacer{Width: 10}.Layout), + layout.Rigid(layout.Spacer{Width: 5}.Layout), layout.Rigid(func(gtx C) D { - label := material.Body1(th, "Copy") - label.Color = color.NRGBA(colornames.Blue500) + c := color.NRGBA(colornames.Blue500) if copied { - label = material.Body1(th, "Copied") - label.Color = color.NRGBA(colornames.Green500) + c = color.NRGBA(colornames.Green500) } - return label.Layout(gtx) + return icons.IconCopy.Layout(gtx, c) }), ) }) @@ -107,15 +110,13 @@ func layoutHeader(gtx C, th *material.Theme, tun tunnel.Tunnel, wgID, wgEntrypoi label := material.Body1(th, tun.Entrypoint()) return label.Layout(gtx) }), - layout.Rigid(layout.Spacer{Width: 10}.Layout), + layout.Rigid(layout.Spacer{Width: 5}.Layout), layout.Rigid(func(gtx C) D { - label := material.Body1(th, "Copy") - label.Color = color.NRGBA(colornames.Blue500) + c := color.NRGBA(colornames.Blue500) if copied { - label = material.Body1(th, "Copied") - label.Color = color.NRGBA(colornames.Green500) + c = color.NRGBA(colornames.Green500) } - return label.Layout(gtx) + return icons.IconCopy.Layout(gtx, c) }), ) }) diff --git a/ui/page/router.go b/ui/page/router.go index 6167348..431a497 100644 --- a/ui/page/router.go +++ b/ui/page/router.go @@ -32,7 +32,7 @@ func NewRouter() *Router { pages: make(map[string]Page), } - r.Register(PageHome, NewTunnelPage(r)) + r.Register(PageTunnel, NewTunnelPage(r)) r.Register(PageMenu, NewMenuPage(r)) r.Register(PageNewFile, NewFileAddPage(r)) r.Register(PageEditFile, NewFileEditPage(r)) @@ -42,9 +42,14 @@ func NewRouter() *Router { r.Register(PageEditTCP, NewTCPEditPage(r)) r.Register(PageNewUDP, NewUDPAddPage(r)) r.Register(PageEditUDP, NewUDPEditPage(r)) + + r.Register(PageEntryPoint, NewEntryPointPage(r)) + r.Register(PageEditTCPEntryPoint, NewTCPEntryPointEditPage(r)) + r.Register(PageNewTCPEntryPoint, NewTCPEntryPointAddPage(r)) + r.Register(PageAbout, NewAboutPage(r)) - r.SwitchTo(Route{Path: PageHome}) + r.SwitchTo(Route{Path: PageTunnel}) return r } @@ -71,10 +76,14 @@ func (r *Router) Layout(gtx layout.Context, th *material.Theme) layout.Dimension switch event := event.(type) { case component.AppBarNavigationClicked: // log.Printf("navigation clicked: %+v", event) - r.SwitchTo(Route{Path: PageHome}) + if r.current.Path == PageTunnel { + r.SwitchTo(Route{Path: PageEntryPoint}) + } else { + r.SwitchTo(Route{Path: PageTunnel}) + } case component.AppBarContextMenuDismissed: // log.Printf("Context menu dismissed: %+v", event) - r.SwitchTo(Route{Path: PageHome}) + r.SwitchTo(Route{Path: PageTunnel}) case component.AppBarOverflowActionClicked: if event.Tag == OverflowActionAbout { r.SwitchTo(Route{Path: PageAbout}) diff --git a/ui/page/tcp.go b/ui/page/tcp.go index 5a677c0..35036b6 100644 --- a/ui/page/tcp.go +++ b/ui/page/tcp.go @@ -59,9 +59,9 @@ func (p *tcpAddPage) Init(opts ...PageOption) { }, Layout: func(gtx C, bg, fg color.NRGBA) D { if p.wgDone.Clicked(gtx) { - defer p.router.SwitchTo(Route{Path: PageHome}) + defer p.router.SwitchTo(Route{Path: PageTunnel}) p.createTunnel() - tunnel.SaveTunnel() + tunnel.SaveConfig() } return component.SimpleIconButton(bg, fg, &p.wgDone, icons.IconDone).Layout(gtx) }, @@ -108,7 +108,7 @@ func (p *tcpAddPage) layout(gtx C, th *material.Theme) D { if addr == "" { return nil } - if _, _, err := net.SplitHostPort(addr); err != nil { + if _, err := net.ResolveTCPAddr("tcp", addr); err != nil { return fmt.Errorf("invalid address format, should be [IP]:PORT or [HOST]:PORT") } return nil @@ -124,16 +124,18 @@ func (p *tcpAddPage) layout(gtx C, th *material.Theme) D { } func (p *tcpAddPage) createTunnel() error { - srv := tunnel.NewTCPTunnel( + tun := tunnel.NewTCPTunnel( tunnel.NameOption(strings.TrimSpace(p.name.Text())), tunnel.EndpointOption(strings.TrimSpace(p.addr.Text())), ) - if err := srv.Run(); err != nil { + tunnel.Add(tun) + + if err := tun.Run(); err != nil { + tun.Close() return err } - tunnel.AddTunnel(srv) return nil } @@ -182,7 +184,7 @@ func (p *tcpEditPage) Init(opts ...PageOption) { } p.id = options.ID - s := tunnel.GetTunnelID(p.id) + s := tunnel.Get(p.id) if s != nil { sopts := s.Options() p.name.SetText(sopts.Name) @@ -196,14 +198,14 @@ func (p *tcpEditPage) Init(opts ...PageOption) { Tag: &p.wgFavorite, }, Layout: func(gtx C, bg, fg color.NRGBA) D { - s := tunnel.GetTunnelID(p.id) + s := tunnel.Get(p.id) if s == nil { return D{} } if p.wgFavorite.Clicked(gtx) { s.Favorite(!s.IsFavorite()) - tunnel.SaveTunnel() + tunnel.SaveConfig() } btn := component.SimpleIconButton(bg, fg, &p.wgFavorite, icons.IconFavorite) @@ -221,14 +223,14 @@ func (p *tcpEditPage) Init(opts ...PageOption) { Tag: &p.wgState, }, Layout: func(gtx C, bg, fg color.NRGBA) D { - s := tunnel.GetTunnelID(p.id) + s := tunnel.Get(p.id) if p.wgState.Clicked(gtx) && s != nil { if s.IsClosed() { s = p.createTunnel() } else { s.Close() } - tunnel.SaveTunnel() + tunnel.SaveConfig() } if s != nil && !s.IsClosed() { @@ -245,9 +247,9 @@ func (p *tcpEditPage) Init(opts ...PageOption) { }, Layout: func(gtx C, bg, fg color.NRGBA) D { if p.wgDelete.Clicked(gtx) { - tunnel.DeleteTunnel(p.id) - tunnel.SaveTunnel() - p.router.SwitchTo(Route{Path: PageHome}) + tunnel.Delete(p.id) + tunnel.SaveConfig() + p.router.SwitchTo(Route{Path: PageTunnel}) } return component.SimpleIconButton(bg, fg, &p.wgDelete, icons.IconDelete).Layout(gtx) }, @@ -259,12 +261,12 @@ func (p *tcpEditPage) Init(opts ...PageOption) { }, Layout: func(gtx C, bg, fg color.NRGBA) D { if p.wgDone.Clicked(gtx) { - defer p.router.SwitchTo(Route{Path: PageHome}) + defer p.router.SwitchTo(Route{Path: PageTunnel}) - if s := tunnel.GetTunnelID(p.id); s != nil { + if s := tunnel.Get(p.id); s != nil { s.Close() p.createTunnel() - tunnel.SaveTunnel() + tunnel.SaveConfig() } } return component.SimpleIconButton(bg, fg, &p.wgDone, icons.IconDone).Layout(gtx) @@ -277,18 +279,20 @@ func (p *tcpEditPage) Init(opts ...PageOption) { } func (p *tcpEditPage) createTunnel() tunnel.Tunnel { - s := tunnel.NewTCPTunnel( + tun := tunnel.NewTCPTunnel( tunnel.IDOption(p.id), tunnel.NameOption(strings.TrimSpace(p.name.Text())), tunnel.EndpointOption(strings.TrimSpace(p.addr.Text())), ) - if err := s.Run(); err != nil { + tunnel.Set(tun) + + if err := tun.Run(); err != nil { + tun.Close() log.Println(err) } - tunnel.SetTunnel(s) - return s + return tun } func (p *tcpEditPage) Layout(gtx C, th *material.Theme) D { @@ -310,7 +314,7 @@ func (p *tcpEditPage) layout(gtx C, th *material.Theme) D { Axis: layout.Vertical, }.Layout(gtx, layout.Rigid(func(gtx C) D { - return layoutHeader(gtx, th, tunnel.GetTunnelID(p.id), &p.wgID, &p.wgEntrypoint) + return layoutHeader(gtx, th, tunnel.Get(p.id), &p.wgID, &p.wgEntrypoint) }), layout.Rigid(layout.Spacer{Height: 10}.Layout), layout.Rigid(func(gtx C) D { diff --git a/ui/page/tunnel.go b/ui/page/tunnel.go index 7865933..e0ed4a9 100644 --- a/ui/page/tunnel.go +++ b/ui/page/tunnel.go @@ -94,8 +94,8 @@ func (p *tunnelPage) Init(opts ...PageOption) { func (p *tunnelPage) Layout(gtx C, th *material.Theme) D { favorite := p.favorite.Load() // gtx.Constraints.Min.X = gtx.Constraints.Max.X - return p.list.Layout(gtx, tunnel.TunnelCount(), func(gtx C, index int) D { - s := tunnel.GetTunnel(index) + return p.list.Layout(gtx, tunnel.Count(), func(gtx C, index int) D { + s := tunnel.GetIndex(index) if s == nil { delete(p.tunnels, index) return D{} @@ -129,7 +129,7 @@ func (p *tunnelPage) Layout(gtx C, th *material.Theme) D { } return state.editor.Layout(gtx, func(gtx C) D { return layout.UniformInset(10).Layout(gtx, func(gtx C) D { - return p.layoutTunnel(gtx, th, s) + return p.layout(gtx, th, s) }) }) }) @@ -138,7 +138,7 @@ func (p *tunnelPage) Layout(gtx C, th *material.Theme) D { }) } -func (p *tunnelPage) layoutTunnel(gtx C, th *material.Theme, s tunnel.Tunnel) D { +func (p *tunnelPage) layout(gtx C, th *material.Theme, s tunnel.Tunnel) D { return layout.Flex{ Alignment: layout.Middle, Spacing: layout.SpaceBetween, @@ -158,11 +158,23 @@ func (p *tunnelPage) layoutTunnel(gtx C, th *material.Theme, s tunnel.Tunnel) D layout.Rigid(material.Body2(th, fmt.Sprintf("Endpoint: %s", s.Endpoint())).Layout), layout.Rigid(layout.Spacer{Height: 5}.Layout), layout.Rigid(material.Body2(th, fmt.Sprintf("Entrypoint: %s", s.Entrypoint())).Layout), + layout.Rigid(layout.Spacer{Height: 5}.Layout), + layout.Rigid(func(gtx C) D { + if err := s.Err(); !s.IsClosed() && err != nil { + label := material.Body2(th, err.Error()) + label.Color = color.NRGBA(colornames.Red500) + return label.Layout(gtx) + } + return D{} + }), ) }), layout.Rigid(layout.Spacer{Width: 10}.Layout), layout.Rigid(func(gtx C) D { c := colornames.Green500 + if s.Err() != nil { + c = colornames.Red500 + } if s.IsClosed() { c = colornames.Grey500 } diff --git a/ui/page/udp.go b/ui/page/udp.go index 5e565d7..d2ab7e9 100644 --- a/ui/page/udp.go +++ b/ui/page/udp.go @@ -59,9 +59,9 @@ func (p *udpAddPage) Init(opts ...PageOption) { }, Layout: func(gtx C, bg, fg color.NRGBA) D { if p.wgDone.Clicked(gtx) { - defer p.router.SwitchTo(Route{Path: PageHome}) + defer p.router.SwitchTo(Route{Path: PageTunnel}) p.createTunnel() - tunnel.SaveTunnel() + tunnel.SaveConfig() } return component.SimpleIconButton(bg, fg, &p.wgDone, icons.IconDone).Layout(gtx) }, @@ -108,7 +108,7 @@ func (p *udpAddPage) layout(gtx C, th *material.Theme) D { if addr == "" { return nil } - if _, _, err := net.SplitHostPort(addr); err != nil { + if _, err := net.ResolveUDPAddr("udp", addr); err != nil { return fmt.Errorf("invalid address format, should be [IP]:PORT or [HOST]:PORT") } return nil @@ -124,16 +124,18 @@ func (p *udpAddPage) layout(gtx C, th *material.Theme) D { } func (p *udpAddPage) createTunnel() error { - srv := tunnel.NewUDPTunnel( + tun := tunnel.NewUDPTunnel( tunnel.NameOption(strings.TrimSpace(p.name.Text())), tunnel.EndpointOption(strings.TrimSpace(p.addr.Text())), ) - if err := srv.Run(); err != nil { + tunnel.Add(tun) + + if err := tun.Run(); err != nil { + tun.Close() return err } - tunnel.AddTunnel(srv) return nil } @@ -182,7 +184,7 @@ func (p *udpEditPage) Init(opts ...PageOption) { } p.id = options.ID - s := tunnel.GetTunnelID(p.id) + s := tunnel.Get(p.id) if s != nil { sopts := s.Options() p.name.SetText(sopts.Name) @@ -196,14 +198,14 @@ func (p *udpEditPage) Init(opts ...PageOption) { Tag: &p.wgFavorite, }, Layout: func(gtx C, bg, fg color.NRGBA) D { - s := tunnel.GetTunnelID(p.id) + s := tunnel.Get(p.id) if s == nil { return D{} } if p.wgFavorite.Clicked(gtx) { s.Favorite(!s.IsFavorite()) - tunnel.SaveTunnel() + tunnel.SaveConfig() } btn := component.SimpleIconButton(bg, fg, &p.wgFavorite, icons.IconFavorite) @@ -221,14 +223,14 @@ func (p *udpEditPage) Init(opts ...PageOption) { Tag: &p.wgState, }, Layout: func(gtx C, bg, fg color.NRGBA) D { - s := tunnel.GetTunnelID(p.id) + s := tunnel.Get(p.id) if p.wgState.Clicked(gtx) && s != nil { if s.IsClosed() { s = p.createTunnel() } else { s.Close() } - tunnel.SaveTunnel() + tunnel.SaveConfig() } if s != nil && !s.IsClosed() { @@ -245,9 +247,9 @@ func (p *udpEditPage) Init(opts ...PageOption) { }, Layout: func(gtx C, bg, fg color.NRGBA) D { if p.wgDelete.Clicked(gtx) { - tunnel.DeleteTunnel(p.id) - tunnel.SaveTunnel() - p.router.SwitchTo(Route{Path: PageHome}) + tunnel.Delete(p.id) + tunnel.SaveConfig() + p.router.SwitchTo(Route{Path: PageTunnel}) } return component.SimpleIconButton(bg, fg, &p.wgDelete, icons.IconDelete).Layout(gtx) }, @@ -259,13 +261,13 @@ func (p *udpEditPage) Init(opts ...PageOption) { }, Layout: func(gtx C, bg, fg color.NRGBA) D { if p.wgDone.Clicked(gtx) { - defer p.router.SwitchTo(Route{Path: PageHome}) + defer p.router.SwitchTo(Route{Path: PageTunnel}) p.createTunnel() - if s := tunnel.GetTunnelID(p.id); s != nil { + if s := tunnel.Get(p.id); s != nil { s.Close() p.createTunnel() - tunnel.SaveTunnel() + tunnel.SaveConfig() } } return component.SimpleIconButton(bg, fg, &p.wgDone, icons.IconDone).Layout(gtx) @@ -278,18 +280,20 @@ func (p *udpEditPage) Init(opts ...PageOption) { } func (p *udpEditPage) createTunnel() tunnel.Tunnel { - s := tunnel.NewUDPTunnel( + tun := tunnel.NewUDPTunnel( tunnel.IDOption(p.id), tunnel.NameOption(strings.TrimSpace(p.name.Text())), tunnel.EndpointOption(strings.TrimSpace(p.addr.Text())), ) - if err := s.Run(); err != nil { + tunnel.Set(tun) + + if err := tun.Run(); err != nil { + tun.Close() log.Println(err) } - tunnel.SetTunnel(s) - return s + return tun } func (p *udpEditPage) Layout(gtx C, th *material.Theme) D { @@ -311,7 +315,7 @@ func (p *udpEditPage) layout(gtx C, th *material.Theme) D { Axis: layout.Vertical, }.Layout(gtx, layout.Rigid(func(gtx C) D { - return layoutHeader(gtx, th, tunnel.GetTunnelID(p.id), &p.wgID, &p.wgEntrypoint) + return layoutHeader(gtx, th, tunnel.Get(p.id), &p.wgID, &p.wgEntrypoint) }), layout.Rigid(layout.Spacer{Height: 10}.Layout), layout.Rigid(func(gtx C) D { diff --git a/version/version.go b/version/version.go index c103455..a33bb34 100644 --- a/version/version.go +++ b/version/version.go @@ -1,3 +1,3 @@ package version -var Version = "0.1.0" +var Version = "0.2.0" diff --git a/winres/winres.json b/winres/winres.json index 79a021c..e6e0a99 100644 --- a/winres/winres.json +++ b/winres/winres.json @@ -35,22 +35,22 @@ "#1": { "0000": { "fixed": { - "file_version": "0.1.0.0", - "product_version": "0.1.0.0" + "file_version": "0.2.0.0", + "product_version": "0.2.0.0" }, "info": { "0409": { "Comments": "", "CompanyName": "GOST.PLUS", "FileDescription": "A simple GUI client for GOST.PLUS", - "FileVersion": "0.1.0", + "FileVersion": "0.2.0", "InternalName": "gost.plus", "LegalCopyright": "Copyright © 2024 GOST.PLUS", "LegalTrademarks": "GOST.PLUS", "OriginalFilename": "gost-plus", "PrivateBuild": "", "ProductName": "GOST.PLUS", - "ProductVersion": "0.1.0", + "ProductVersion": "0.2.0", "SpecialBuild": "" } }