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": ""
}
}