From 8ac5a4e52361c286028684e79131f4b9723e4f2a Mon Sep 17 00:00:00 2001 From: Alexander Becker Date: Fri, 25 Oct 2024 09:43:37 +0200 Subject: [PATCH 01/15] Use $USER if no user is provided --- internal/tunnel/config.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/internal/tunnel/config.go b/internal/tunnel/config.go index cbb8a55..4648b8a 100644 --- a/internal/tunnel/config.go +++ b/internal/tunnel/config.go @@ -4,6 +4,7 @@ import ( "fmt" "net" "os" + "os/user" "strconv" "strings" "time" @@ -60,6 +61,13 @@ func (t *Tunnel) makeRunConfig() error { rc.hostName = t.Host } + // Use $USER if still no user specified + if rc.user == "" { + if u, err := user.Current(); err == nil { + rc.user = u.Username + } + } + if err := validate(&rc); err != nil { return err } From 18ad1a8b49b730f7c46887d3e0121706aef63c9e Mon Sep 17 00:00:00 2001 From: Alexander Becker Date: Fri, 25 Oct 2024 20:46:44 +0200 Subject: [PATCH 02/15] Update cmd messages --- cmd/boring/main.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cmd/boring/main.go b/cmd/boring/main.go index e1529c2..87279f6 100644 --- a/cmd/boring/main.go +++ b/cmd/boring/main.go @@ -133,9 +133,9 @@ func openTunnel(name string, conf *config.Config) { } if !resp.Success { - log.Errorf("Tunnel %v could not be opened: %v", name, resp.Error) + log.Errorf("Tunnel '%v' could not be opened: %v", name, resp.Error) } else { - log.Infof("Opened tunnel %s: %s %v %s via %s", + log.Infof("Opened tunnel '%s': %s %v %s via %s", log.Green+t.Name+log.Reset, t.LocalAddress, t.Mode, t.RemoteAddress, t.Host) } @@ -153,9 +153,9 @@ func closeTunnel(name string) { } if !resp.Success { - log.Errorf("Tunnel %v could not be closed: %v", name, resp.Error) + log.Errorf("Tunnel '%v' could not be closed: %v", name, resp.Error) } else { - log.Infof("Closed tunnel %s", log.Green+t.Name+log.Reset) + log.Infof("Closed tunnel '%s'", log.Green+t.Name+log.Reset) } } From f09d992193de315d03b4002e63705436f0cea148 Mon Sep 17 00:00:00 2001 From: Alexander Becker Date: Sat, 26 Oct 2024 12:17:34 +0200 Subject: [PATCH 03/15] Add remote SOCKS5 mode --- internal/config/config.go | 8 ++-- internal/proxy/socks5.go | 28 ++++---------- internal/tunnel/config.go | 5 ++- internal/tunnel/mode.go | 9 +++-- internal/tunnel/tunnel.go | 80 +++++++++++++++++---------------------- 5 files changed, 55 insertions(+), 75 deletions(-) diff --git a/internal/config/config.go b/internal/config/config.go index 42108ea..d63bff9 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -12,7 +12,7 @@ import ( const ( defaultFileName = "~/.boring.toml" - socksLabel = "[SOCKS5 proxy]" + socksLabel = "[SOCKS]" ) var FileName string @@ -48,11 +48,13 @@ func LoadConfig() (*Config, error) { m[t.Name] = t } - // Replace the remote address of Socks tunnels by a fixed indicator, - // it is not used for anything anyway + // Replace the remote address of Socks tunnels and local address of reverse + // socks tunnels by a fixed indicator, it is not used for anything anyway for _, t := range m { if t.Mode == tunnel.Socks { t.RemoteAddress = socksLabel + } else if t.Mode == tunnel.RemoteSocks { + t.LocalAddress = socksLabel } } diff --git a/internal/proxy/socks5.go b/internal/proxy/socks5.go index 7cf3e8b..94a4b4a 100644 --- a/internal/proxy/socks5.go +++ b/internal/proxy/socks5.go @@ -20,10 +20,11 @@ import ( "errors" "fmt" "io" - "log" "net" "strconv" "time" + + "github.com/alebeck/boring/internal/log" ) // Authentication METHODs described in RFC 1928, section 3. @@ -80,14 +81,8 @@ const ( addrTypeNotSupported replyCode = 8 ) -type logf func(format string, args... any) - // Server is a SOCKS5 proxy server. type Server struct { - // Logf optionally specifies the logger to use. - // If nil, the standard logger is used. - Logf logf - // Dialer optionally specifies the dialer to use for outgoing connections. // If nil, the net package's standard dialer is used. Dialer func(ctx context.Context, network, addr string) (net.Conn, error) @@ -106,14 +101,6 @@ func (s *Server) dial(ctx context.Context, network, addr string) (net.Conn, erro return dial(ctx, network, addr) } -func (s *Server) logf(format string, args ...any) { - logf := s.Logf - if logf == nil { - logf = log.Printf - } - logf(format, args...) -} - // Serve accepts and handles incoming connections on the given listener. func (s *Server) Serve(l net.Listener) error { defer l.Close() @@ -128,10 +115,10 @@ func (s *Server) Serve(l net.Listener) error { func (s *Server) ServeConn(conn net.Conn) error { defer conn.Close() - socksC := &Conn{logf: s.Logf, clientConn: conn, srv: s} + socksC := &Conn{clientConn: conn, srv: s} err := socksC.Run() if err != nil { - s.logf("client connection failed: %v", err) + log.Errorf("client connection failed: %v", err) } return nil } @@ -141,7 +128,6 @@ func (s *Server) ServeConn(conn net.Conn) error { type Conn struct { // The struct is filled by each of the internal // methods in turn as the transaction progresses. - logf logf srv *Server clientConn net.Conn request *request @@ -334,7 +320,7 @@ func (c *Conn) transferUDP(associatedTCP net.Conn, clientConn net.PacketConn, ta if errors.Is(err, net.ErrClosed) { return } - c.logf("udp transfer: handle udp request fail: %v", err) + log.Errorf("udp transfer: handle udp request fail: %v", err) } } } @@ -357,7 +343,7 @@ func (c *Conn) transferUDP(associatedTCP net.Conn, clientConn net.PacketConn, ta if errors.Is(err, net.ErrClosed) { return } - c.logf("udp transfer: handle udp response fail: %v", err) + log.Errorf("udp transfer: handle udp response fail: %v", err) } } } @@ -391,7 +377,7 @@ func (c *Conn) handleUDPRequest( } targetAddr, err := net.ResolveUDPAddr("udp", req.addr.hostPort()) if err != nil { - c.logf("resolve target addr fail: %v", err) + log.Errorf("resolve target addr fail: %v", err) } nn, err := targetConn.WriteTo(data, targetAddr) diff --git a/internal/tunnel/config.go b/internal/tunnel/config.go index 4648b8a..1baa2be 100644 --- a/internal/tunnel/config.go +++ b/internal/tunnel/config.go @@ -78,11 +78,12 @@ func (t *Tunnel) makeRunConfig() error { } var err error - rc.remoteAddress, rc.remoteNet, err = parseAddr(string(t.RemoteAddress), t.Mode == Remote) + short := t.Mode == Remote || t.Mode == RemoteSocks + rc.remoteAddress, rc.remoteNet, err = parseAddr(string(t.RemoteAddress), short) if err != nil { return fmt.Errorf("remote address: %v", err) } - rc.localAddress, rc.localNet, err = parseAddr(string(t.LocalAddress), t.Mode == Local) + rc.localAddress, rc.localNet, err = parseAddr(string(t.LocalAddress), !short) if err != nil { return fmt.Errorf("local address: %v", err) } diff --git a/internal/tunnel/mode.go b/internal/tunnel/mode.go index f0e85b8..b0c5c82 100644 --- a/internal/tunnel/mode.go +++ b/internal/tunnel/mode.go @@ -11,6 +11,7 @@ const ( Local Mode = iota Remote Socks + RemoteSocks ) func (m *Mode) UnmarshalTOML(data interface{}) error { @@ -26,6 +27,8 @@ func (m *Mode) UnmarshalTOML(data interface{}) error { *m = Remote case "socks": *m = Socks + case "socks-remote": + *m = RemoteSocks default: return errors.New("invalid mode") } @@ -34,8 +37,8 @@ func (m *Mode) UnmarshalTOML(data interface{}) error { } func (m Mode) String() string { - if m == Remote { - return "<-" + if m == Local || m == Socks { + return "->" } - return "->" + return "<-" } diff --git a/internal/tunnel/tunnel.go b/internal/tunnel/tunnel.go index 9dcbf98..b3ac80d 100644 --- a/internal/tunnel/tunnel.go +++ b/internal/tunnel/tunnel.go @@ -45,11 +45,11 @@ func (t *Tunnel) Open() error { } } - if err = t.setupClient(); err != nil { + if err = t.makeClient(); err != nil { return fmt.Errorf("could not setup SSH client: %v", err) } - if err = t.setupListener(); err != nil { + if err = t.makeListener(); err != nil { return fmt.Errorf("cannot listen: %v", err) } @@ -65,27 +65,34 @@ func (t *Tunnel) Open() error { return nil } -func (t *Tunnel) setupClient() error { +func (t *Tunnel) makeClient() error { var err error addr := fmt.Sprintf("%v:%v", t.rc.hostName, t.rc.port) t.client, err = ssh.Dial("tcp", addr, t.rc.clientConfig) return err } -func (t *Tunnel) setupListener() error { - var err error - if t.Mode == Remote { +func (t *Tunnel) makeListener() (err error) { + if t.Mode == Remote || t.Mode == RemoteSocks { t.listener, err = t.client.Listen(t.rc.remoteNet, t.rc.remoteAddress) } else { t.listener, err = net.Listen(t.rc.localNet, t.rc.localAddress) } - return err + return +} + +func (t *Tunnel) dial(network, addr string) (net.Conn, error) { + if t.Mode == Remote || t.Mode == RemoteSocks { + return net.Dial(network, addr) + } + return t.client.Dial(network, addr) } func (t *Tunnel) run() { disconn := make(chan struct{}) go func() { t.client.Wait() + log.Infof("Client closed for %v", t.Name) close(disconn) }() @@ -130,65 +137,47 @@ func (t *Tunnel) keepAlive(cancel chan struct{}) { func (t *Tunnel) handleConns() { defer t.listener.Close() defer t.client.Close() - - switch t.Mode { - case Local: - t.handleLocalConns() - case Remote: - t.handleRemoteConns() - case Socks: - t.handleSocks() + if t.Mode == Local || t.Mode == Remote { + t.handleForward() + return } + t.handleSocks() } -func (t *Tunnel) handleLocalConns() { +func (t *Tunnel) handleForward() { for { - local, err := t.listener.Accept() + conn1, err := t.listener.Accept() if err != nil { log.Errorf("could not accept: %v", err) return } - - remote, err := t.client.Dial(t.rc.remoteNet, t.rc.remoteAddress) - if err != nil { - log.Errorf("could not connect on remote: %v", err) - return - } - - go t.waitFor(func() { t.tunnel(local, remote) }) - } -} - -func (t *Tunnel) handleRemoteConns() { - for { - remote, err := t.listener.Accept() - if err != nil { - log.Errorf("could not accept on remote: %v", err) - return - } go t.waitFor(func() { - local, err := net.Dial(t.rc.localNet, t.rc.localAddress) + netw, addr := t.rc.remoteNet, t.rc.remoteAddress + if t.Mode == Remote || t.Mode == RemoteSocks { + netw, addr = t.rc.localNet, t.rc.localAddress + } + conn2, err := t.dial(netw, addr) if err != nil { - log.Errorf("could not dial locally: %v", err) + log.Errorf("could not dial: %v", err) return } - t.tunnel(local, remote) + tunnel(conn1, conn2) }) } } -func (t *Tunnel) tunnel(local, remote net.Conn) { - defer local.Close() - defer remote.Close() +func tunnel(c1, c2 net.Conn) { + defer c1.Close() + defer c2.Close() done := make(chan struct{}, 2) go func() { - io.Copy(local, remote) + io.Copy(c1, c2) done <- struct{}{} }() go func() { - io.Copy(remote, local) + io.Copy(c2, c1) done <- struct{}{} }() @@ -197,11 +186,10 @@ func (t *Tunnel) tunnel(local, remote net.Conn) { func (t *Tunnel) handleSocks() { serv := &proxy.Server{ - Dialer: func(ctx context.Context, network, addr string) (net.Conn, error) { - return t.client.Dial(network, addr) + Dialer: func(ctx context.Context, netw, addr string) (net.Conn, error) { + return t.dial(netw, addr) }, } - for { conn, err := t.listener.Accept() if err != nil { From c2667dd161e5ec1403f16bfbf73755dafd8ea676 Mon Sep 17 00:00:00 2001 From: Alexander Becker Date: Sun, 27 Oct 2024 20:10:02 +0100 Subject: [PATCH 04/15] Follow XDG config spec on Unix --- cmd/boring/main.go | 49 ++++++++++++++++++++++---------- internal/config/config.go | 59 ++++++++++++++++++++++++++++++++------- internal/ipc/ipc.go | 5 ++-- 3 files changed, 85 insertions(+), 28 deletions(-) diff --git a/cmd/boring/main.go b/cmd/boring/main.go index 87279f6..f8a86c1 100644 --- a/cmd/boring/main.go +++ b/cmd/boring/main.go @@ -4,6 +4,8 @@ import ( "context" "fmt" "os" + "os/exec" + "runtime" "slices" "time" @@ -45,6 +47,8 @@ func main() { controlTunnels(os.Args[2:], daemon.Close) case "list", "l": listTunnels() + case "edit", "e": + openConfig() default: fmt.Println("Unknown command:", os.Args[1]) printUsage() @@ -61,24 +65,19 @@ func prepare() (*config.Config, error) { g.Go(func() error { var err error - // Check if config file exists, otherwise we can create it - if _, statErr := os.Stat(config.FileName); statErr != nil { - var f *os.File - if f, err = os.Create(config.FileName); err != nil { - return fmt.Errorf("could not create config file: %v", err) - } - f.Close() - log.Infof("Created boring config file: %s", config.FileName) + // Makes sure config file exists, and otherwise creates it + if err := config.Ensure(); err != nil { + return fmt.Errorf("could not create config file: %v", err) } - if conf, err = config.LoadConfig(); err != nil { - return fmt.Errorf("Could not load configuration: %v", err) + if conf, err = config.Load(); err != nil { + return fmt.Errorf("could not load configuration: %v", err) } return nil }) g.Go(func() error { if err := daemon.Ensure(ctx); err != nil { - return fmt.Errorf("Could not start daemon: %v", err) + return fmt.Errorf("could not start daemon: %v", err) } return nil }) @@ -122,7 +121,7 @@ func openTunnel(name string, conf *config.Config) { t, ok := conf.TunnelsMap[name] if !ok { log.Errorf("Tunnel '%s' not found in configuration (%s).", - name, config.FileName) + name, config.FilePath) return } @@ -223,6 +222,25 @@ func transmitCmd(cmd daemon.Cmd, resp any) error { return nil } +func openConfig() { + editor := os.Getenv("EDITOR") + if editor == "" { + editor = "vi" + if runtime.GOOS == "windows" { + editor = "notepad" + } + } + + cmd := exec.Command(editor, config.FilePath) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + if err := cmd.Run(); err != nil { + log.Fatalf("Editor: %v", err) + } +} + func printUsage() { v := version if v == "" { @@ -234,7 +252,8 @@ func printUsage() { fmt.Printf("boring %s\n", v) fmt.Println("Usage:") - fmt.Println(" boring list,l List tunnels") - fmt.Println(" boring open,o [ ...] Open specified tunnel(s)") - fmt.Println(" boring close,c [ ...] Close specified tunnel(s)") + fmt.Println(" boring list,l List tunnels") + fmt.Println(" boring open,o [ ...] Open specified tunnel(s)") + fmt.Println(" boring close,c [ ...] Close specified tunnel(s)") + fmt.Println(" boring edit,e Edit configuration file") } diff --git a/internal/config/config.go b/internal/config/config.go index d63bff9..a0d50f3 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -3,19 +3,22 @@ package config import ( "fmt" "os" + "path" "path/filepath" + "runtime" "github.com/BurntSushi/toml" + "github.com/alebeck/boring/internal/log" "github.com/alebeck/boring/internal/paths" "github.com/alebeck/boring/internal/tunnel" ) const ( - defaultFileName = "~/.boring.toml" - socksLabel = "[SOCKS]" + fileName = "boring.toml" + socksLabel = "[SOCKS]" ) -var FileName string +var FilePath string // Config represents the application configuration as parsed from ./boring.toml type Config struct { @@ -24,17 +27,34 @@ type Config struct { } func init() { - if FileName = os.Getenv("BORING_CONFIG"); FileName == "" { - FileName = defaultFileName + FilePath = os.Getenv("BORING_CONFIG") + if FilePath == "" { + configHome := getConfigHome() + FilePath = path.Join(configHome, fileName) } - FileName = filepath.ToSlash(FileName) - FileName = paths.ReplaceTilde(FileName) + FilePath = filepath.ToSlash(FilePath) + FilePath = paths.ReplaceTilde(FilePath) } -// LoadConfig parses the boring configuration file -func LoadConfig() (*Config, error) { +func Ensure() error { + if _, statErr := os.Stat(FilePath); statErr != nil { + d := filepath.Dir(FilePath) + if err := os.MkdirAll(d, 0700); err != nil { + return err + } + f, err := os.OpenFile(FilePath, os.O_RDWR|os.O_CREATE, 0600) + if err != nil { + return err + } + f.Close() + log.Infof("Created boring config file: %s", FilePath) + } + return nil +} + +func Load() (*Config, error) { var config Config - if _, err := toml.DecodeFile(FileName, &config); err != nil { + if _, err := toml.DecodeFile(FilePath, &config); err != nil { return nil, fmt.Errorf("could not decode config file: %v", err) } @@ -61,3 +81,22 @@ func LoadConfig() (*Config, error) { config.TunnelsMap = m return &config, nil } + +func getConfigHome() string { + if runtime.GOOS != "windows" { + // Follow XDG specification on Unix + configHome := os.Getenv("XDG_CONFIG_HOME") + if configHome == "" { + configHome = filepath.Join(os.Getenv("HOME"), ".config") + } + return filepath.Join(configHome, "boring") + } + configHome := os.Getenv("LOCALAPPDATA") + if configHome == "" { + configHome = os.Getenv("APPDATA") + } + if configHome == "" { + configHome = os.Getenv("USERPROFILE") + } + return filepath.Join(configHome, "boring") +} diff --git a/internal/ipc/ipc.go b/internal/ipc/ipc.go index 9131135..48b0fd5 100644 --- a/internal/ipc/ipc.go +++ b/internal/ipc/ipc.go @@ -10,12 +10,11 @@ import ( ) func Send(s any, conn net.Conn) error { - log.Debugf("Sending: %v", s) - data, err := json.Marshal(s) if err != nil { return fmt.Errorf("failed to serialize response: %v", err) } + log.Debugf("Sending: %v", data) _, err = conn.Write(append(data, '\n')) if err != nil { @@ -30,11 +29,11 @@ func Receive(s any, conn net.Conn) error { if err != nil { return fmt.Errorf("failed to read from connection: %w", err) } + log.Debugf("Received: %v", data) err = json.Unmarshal(data, s) if err != nil { return fmt.Errorf("failed to deserialize command: %w", err) } - log.Debugf("Received object: %v", s) return nil } From 0ae2ac33ead6344327ef6261ff566b677256c741 Mon Sep 17 00:00:00 2001 From: Alexander Becker Date: Thu, 31 Oct 2024 21:56:01 +0100 Subject: [PATCH 05/15] Revert "Follow XDG config spec on Unix" This reverts commit c2667dd161e5ec1403f16bfbf73755dafd8ea676. --- cmd/boring/main.go | 49 ++++++++++---------------------- internal/config/config.go | 59 +++++++-------------------------------- internal/ipc/ipc.go | 5 ++-- 3 files changed, 28 insertions(+), 85 deletions(-) diff --git a/cmd/boring/main.go b/cmd/boring/main.go index f8a86c1..87279f6 100644 --- a/cmd/boring/main.go +++ b/cmd/boring/main.go @@ -4,8 +4,6 @@ import ( "context" "fmt" "os" - "os/exec" - "runtime" "slices" "time" @@ -47,8 +45,6 @@ func main() { controlTunnels(os.Args[2:], daemon.Close) case "list", "l": listTunnels() - case "edit", "e": - openConfig() default: fmt.Println("Unknown command:", os.Args[1]) printUsage() @@ -65,19 +61,24 @@ func prepare() (*config.Config, error) { g.Go(func() error { var err error - // Makes sure config file exists, and otherwise creates it - if err := config.Ensure(); err != nil { - return fmt.Errorf("could not create config file: %v", err) + // Check if config file exists, otherwise we can create it + if _, statErr := os.Stat(config.FileName); statErr != nil { + var f *os.File + if f, err = os.Create(config.FileName); err != nil { + return fmt.Errorf("could not create config file: %v", err) + } + f.Close() + log.Infof("Created boring config file: %s", config.FileName) } - if conf, err = config.Load(); err != nil { - return fmt.Errorf("could not load configuration: %v", err) + if conf, err = config.LoadConfig(); err != nil { + return fmt.Errorf("Could not load configuration: %v", err) } return nil }) g.Go(func() error { if err := daemon.Ensure(ctx); err != nil { - return fmt.Errorf("could not start daemon: %v", err) + return fmt.Errorf("Could not start daemon: %v", err) } return nil }) @@ -121,7 +122,7 @@ func openTunnel(name string, conf *config.Config) { t, ok := conf.TunnelsMap[name] if !ok { log.Errorf("Tunnel '%s' not found in configuration (%s).", - name, config.FilePath) + name, config.FileName) return } @@ -222,25 +223,6 @@ func transmitCmd(cmd daemon.Cmd, resp any) error { return nil } -func openConfig() { - editor := os.Getenv("EDITOR") - if editor == "" { - editor = "vi" - if runtime.GOOS == "windows" { - editor = "notepad" - } - } - - cmd := exec.Command(editor, config.FilePath) - cmd.Stdin = os.Stdin - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - - if err := cmd.Run(); err != nil { - log.Fatalf("Editor: %v", err) - } -} - func printUsage() { v := version if v == "" { @@ -252,8 +234,7 @@ func printUsage() { fmt.Printf("boring %s\n", v) fmt.Println("Usage:") - fmt.Println(" boring list,l List tunnels") - fmt.Println(" boring open,o [ ...] Open specified tunnel(s)") - fmt.Println(" boring close,c [ ...] Close specified tunnel(s)") - fmt.Println(" boring edit,e Edit configuration file") + fmt.Println(" boring list,l List tunnels") + fmt.Println(" boring open,o [ ...] Open specified tunnel(s)") + fmt.Println(" boring close,c [ ...] Close specified tunnel(s)") } diff --git a/internal/config/config.go b/internal/config/config.go index a0d50f3..d63bff9 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -3,22 +3,19 @@ package config import ( "fmt" "os" - "path" "path/filepath" - "runtime" "github.com/BurntSushi/toml" - "github.com/alebeck/boring/internal/log" "github.com/alebeck/boring/internal/paths" "github.com/alebeck/boring/internal/tunnel" ) const ( - fileName = "boring.toml" - socksLabel = "[SOCKS]" + defaultFileName = "~/.boring.toml" + socksLabel = "[SOCKS]" ) -var FilePath string +var FileName string // Config represents the application configuration as parsed from ./boring.toml type Config struct { @@ -27,34 +24,17 @@ type Config struct { } func init() { - FilePath = os.Getenv("BORING_CONFIG") - if FilePath == "" { - configHome := getConfigHome() - FilePath = path.Join(configHome, fileName) + if FileName = os.Getenv("BORING_CONFIG"); FileName == "" { + FileName = defaultFileName } - FilePath = filepath.ToSlash(FilePath) - FilePath = paths.ReplaceTilde(FilePath) + FileName = filepath.ToSlash(FileName) + FileName = paths.ReplaceTilde(FileName) } -func Ensure() error { - if _, statErr := os.Stat(FilePath); statErr != nil { - d := filepath.Dir(FilePath) - if err := os.MkdirAll(d, 0700); err != nil { - return err - } - f, err := os.OpenFile(FilePath, os.O_RDWR|os.O_CREATE, 0600) - if err != nil { - return err - } - f.Close() - log.Infof("Created boring config file: %s", FilePath) - } - return nil -} - -func Load() (*Config, error) { +// LoadConfig parses the boring configuration file +func LoadConfig() (*Config, error) { var config Config - if _, err := toml.DecodeFile(FilePath, &config); err != nil { + if _, err := toml.DecodeFile(FileName, &config); err != nil { return nil, fmt.Errorf("could not decode config file: %v", err) } @@ -81,22 +61,3 @@ func Load() (*Config, error) { config.TunnelsMap = m return &config, nil } - -func getConfigHome() string { - if runtime.GOOS != "windows" { - // Follow XDG specification on Unix - configHome := os.Getenv("XDG_CONFIG_HOME") - if configHome == "" { - configHome = filepath.Join(os.Getenv("HOME"), ".config") - } - return filepath.Join(configHome, "boring") - } - configHome := os.Getenv("LOCALAPPDATA") - if configHome == "" { - configHome = os.Getenv("APPDATA") - } - if configHome == "" { - configHome = os.Getenv("USERPROFILE") - } - return filepath.Join(configHome, "boring") -} diff --git a/internal/ipc/ipc.go b/internal/ipc/ipc.go index 48b0fd5..9131135 100644 --- a/internal/ipc/ipc.go +++ b/internal/ipc/ipc.go @@ -10,11 +10,12 @@ import ( ) func Send(s any, conn net.Conn) error { + log.Debugf("Sending: %v", s) + data, err := json.Marshal(s) if err != nil { return fmt.Errorf("failed to serialize response: %v", err) } - log.Debugf("Sending: %v", data) _, err = conn.Write(append(data, '\n')) if err != nil { @@ -29,11 +30,11 @@ func Receive(s any, conn net.Conn) error { if err != nil { return fmt.Errorf("failed to read from connection: %w", err) } - log.Debugf("Received: %v", data) err = json.Unmarshal(data, s) if err != nil { return fmt.Errorf("failed to deserialize command: %w", err) } + log.Debugf("Received object: %v", s) return nil } From e57ae19cbb263ebc19da512dc6c16b12699512f7 Mon Sep 17 00:00:00 2001 From: Alexander Becker Date: Thu, 31 Oct 2024 22:17:05 +0100 Subject: [PATCH 06/15] Add command --- cmd/boring/main.go | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/cmd/boring/main.go b/cmd/boring/main.go index 87279f6..ea12fe4 100644 --- a/cmd/boring/main.go +++ b/cmd/boring/main.go @@ -4,6 +4,8 @@ import ( "context" "fmt" "os" + "os/exec" + "runtime" "slices" "time" @@ -45,6 +47,8 @@ func main() { controlTunnels(os.Args[2:], daemon.Close) case "list", "l": listTunnels() + case "edit", "e": + openConfig() default: fmt.Println("Unknown command:", os.Args[1]) printUsage() @@ -63,22 +67,22 @@ func prepare() (*config.Config, error) { var err error // Check if config file exists, otherwise we can create it if _, statErr := os.Stat(config.FileName); statErr != nil { - var f *os.File - if f, err = os.Create(config.FileName); err != nil { + f, err := os.OpenFile(config.FileName, os.O_RDWR|os.O_CREATE, 0600) + if err != nil { return fmt.Errorf("could not create config file: %v", err) } f.Close() log.Infof("Created boring config file: %s", config.FileName) } if conf, err = config.LoadConfig(); err != nil { - return fmt.Errorf("Could not load configuration: %v", err) + return fmt.Errorf("could not load configuration: %v", err) } return nil }) g.Go(func() error { if err := daemon.Ensure(ctx); err != nil { - return fmt.Errorf("Could not start daemon: %v", err) + return fmt.Errorf("could not start daemon: %v", err) } return nil }) @@ -223,6 +227,21 @@ func transmitCmd(cmd daemon.Cmd, resp any) error { return nil } +func openConfig() { + editor := os.Getenv("EDITOR") + if editor == "" { + editor = "vi" + if runtime.GOOS == "windows" { + editor = "notepad" + } + } + cmd := exec.Command(editor, config.FileName) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Run() +} + func printUsage() { v := version if v == "" { @@ -234,7 +253,8 @@ func printUsage() { fmt.Printf("boring %s\n", v) fmt.Println("Usage:") - fmt.Println(" boring list,l List tunnels") - fmt.Println(" boring open,o [ ...] Open specified tunnel(s)") - fmt.Println(" boring close,c [ ...] Close specified tunnel(s)") + fmt.Println(" boring l, list List tunnels") + fmt.Println(" boring o, open [ ...] Open specified tunnel(s)") + fmt.Println(" boring c, close [ ...] Close specified tunnel(s)") + fmt.Println(" boring e, edit Edit configuration file") } From 7cb5404c7e37ef6c0278f37f6376436420952f48 Mon Sep 17 00:00:00 2001 From: Alexander Becker Date: Thu, 31 Oct 2024 22:17:33 +0100 Subject: [PATCH 07/15] Log JSON instead of objects in ipc package --- internal/ipc/ipc.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/internal/ipc/ipc.go b/internal/ipc/ipc.go index 9131135..48b0fd5 100644 --- a/internal/ipc/ipc.go +++ b/internal/ipc/ipc.go @@ -10,12 +10,11 @@ import ( ) func Send(s any, conn net.Conn) error { - log.Debugf("Sending: %v", s) - data, err := json.Marshal(s) if err != nil { return fmt.Errorf("failed to serialize response: %v", err) } + log.Debugf("Sending: %v", data) _, err = conn.Write(append(data, '\n')) if err != nil { @@ -30,11 +29,11 @@ func Receive(s any, conn net.Conn) error { if err != nil { return fmt.Errorf("failed to read from connection: %w", err) } + log.Debugf("Received: %v", data) err = json.Unmarshal(data, s) if err != nil { return fmt.Errorf("failed to deserialize command: %w", err) } - log.Debugf("Received object: %v", s) return nil } From c99adda78c9a9c282290c17aa556a1d4cb00a0d8 Mon Sep 17 00:00:00 2001 From: Alexander Becker Date: Thu, 31 Oct 2024 22:27:52 +0100 Subject: [PATCH 08/15] Improve logging --- internal/daemon/daemon.go | 2 +- internal/tunnel/tunnel.go | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/internal/daemon/daemon.go b/internal/daemon/daemon.go index a4efab5..2805f0a 100644 --- a/internal/daemon/daemon.go +++ b/internal/daemon/daemon.go @@ -115,7 +115,7 @@ func handleConnection(s *state, conn net.Conn) { } return } - log.Infof("Received command %v", cmd) + log.Debugf("Received command %v", cmd) // Execute command switch cmd.Kind { diff --git a/internal/tunnel/tunnel.go b/internal/tunnel/tunnel.go index b3ac80d..3bece96 100644 --- a/internal/tunnel/tunnel.go +++ b/internal/tunnel/tunnel.go @@ -48,10 +48,12 @@ func (t *Tunnel) Open() error { if err = t.makeClient(); err != nil { return fmt.Errorf("could not setup SSH client: %v", err) } + log.Debugf("%v: connected to server", t.Name) if err = t.makeListener(); err != nil { return fmt.Errorf("cannot listen: %v", err) } + log.Debugf("%v: listening on %v", t.Name, t.listener.Addr()) if t.stop == nil { t.stop = make(chan struct{}) From 5e9fb08b0d08229f898da17b4d3734c89bbd8853 Mon Sep 17 00:00:00 2001 From: Alexander Becker Date: Thu, 31 Oct 2024 22:37:35 +0100 Subject: [PATCH 09/15] Allow directory for $BORING_CONFIG --- internal/config/config.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/internal/config/config.go b/internal/config/config.go index d63bff9..c65858d 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -29,6 +29,11 @@ func init() { } FileName = filepath.ToSlash(FileName) FileName = paths.ReplaceTilde(FileName) + + // If FileName exists and is a directory, append file name + if fi, err := os.Stat(FileName); err == nil && fi.IsDir() { + FileName = filepath.Join(FileName, filepath.Base(defaultFileName)) + } } // LoadConfig parses the boring configuration file From 2cc2a057398d28e3a9190597cdeb5d650dfc342c Mon Sep 17 00:00:00 2001 From: Alexander Becker Date: Thu, 31 Oct 2024 22:43:08 +0100 Subject: [PATCH 10/15] Welcome new users on config file creation --- cmd/boring/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/boring/main.go b/cmd/boring/main.go index ea12fe4..f21f31e 100644 --- a/cmd/boring/main.go +++ b/cmd/boring/main.go @@ -72,7 +72,7 @@ func prepare() (*config.Config, error) { return fmt.Errorf("could not create config file: %v", err) } f.Close() - log.Infof("Created boring config file: %s", config.FileName) + log.Infof("Hi! Created boring config file: %s", config.FileName) } if conf, err = config.LoadConfig(); err != nil { return fmt.Errorf("could not load configuration: %v", err) From 9a09dc112753f011230d17577ac6a99b79219b16 Mon Sep 17 00:00:00 2001 From: Alexander Becker Date: Fri, 1 Nov 2024 20:29:11 +0100 Subject: [PATCH 11/15] Follow XDG spec on Linux --- cmd/boring/main.go | 12 ++++++------ internal/config/config.go | 32 ++++++++++++++++++++------------ internal/tunnel/status.go | 2 +- 3 files changed, 27 insertions(+), 19 deletions(-) diff --git a/cmd/boring/main.go b/cmd/boring/main.go index f21f31e..3cc1132 100644 --- a/cmd/boring/main.go +++ b/cmd/boring/main.go @@ -66,15 +66,15 @@ func prepare() (*config.Config, error) { g.Go(func() error { var err error // Check if config file exists, otherwise we can create it - if _, statErr := os.Stat(config.FileName); statErr != nil { - f, err := os.OpenFile(config.FileName, os.O_RDWR|os.O_CREATE, 0600) + if _, statErr := os.Stat(config.Path); statErr != nil { + f, err := os.OpenFile(config.Path, os.O_RDWR|os.O_CREATE, 0600) if err != nil { return fmt.Errorf("could not create config file: %v", err) } f.Close() - log.Infof("Hi! Created boring config file: %s", config.FileName) + log.Infof("Hi! Created boring config file: %s", config.Path) } - if conf, err = config.LoadConfig(); err != nil { + if conf, err = config.Load(); err != nil { return fmt.Errorf("could not load configuration: %v", err) } return nil @@ -126,7 +126,7 @@ func openTunnel(name string, conf *config.Config) { t, ok := conf.TunnelsMap[name] if !ok { log.Errorf("Tunnel '%s' not found in configuration (%s).", - name, config.FileName) + name, config.Path) return } @@ -235,7 +235,7 @@ func openConfig() { editor = "notepad" } } - cmd := exec.Command(editor, config.FileName) + cmd := exec.Command(editor, config.Path) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr diff --git a/internal/config/config.go b/internal/config/config.go index c65858d..8807e0f 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path/filepath" + "runtime" "github.com/BurntSushi/toml" "github.com/alebeck/boring/internal/paths" @@ -11,11 +12,11 @@ import ( ) const ( - defaultFileName = "~/.boring.toml" - socksLabel = "[SOCKS]" + fileName = ".boring.toml" + socksLabel = "[SOCKS]" ) -var FileName string +var Path string // Config represents the application configuration as parsed from ./boring.toml type Config struct { @@ -24,22 +25,29 @@ type Config struct { } func init() { - if FileName = os.Getenv("BORING_CONFIG"); FileName == "" { - FileName = defaultFileName + if Path = os.Getenv("BORING_CONFIG"); Path == "" { + Path = filepath.Join(getConfigHome(), fileName) } - FileName = filepath.ToSlash(FileName) - FileName = paths.ReplaceTilde(FileName) + Path = filepath.ToSlash(Path) + Path = paths.ReplaceTilde(Path) +} - // If FileName exists and is a directory, append file name - if fi, err := os.Stat(FileName); err == nil && fi.IsDir() { - FileName = filepath.Join(FileName, filepath.Base(defaultFileName)) +func getConfigHome() string { + if runtime.GOOS == "linux" { + // Follow XDG specification on Linux + h := os.Getenv("XDG_CONFIG_HOME") + if h == "" { + h = "~/.config" + } + return filepath.Join(h, "boring") } + return "~" } // LoadConfig parses the boring configuration file -func LoadConfig() (*Config, error) { +func Load() (*Config, error) { var config Config - if _, err := toml.DecodeFile(FileName, &config); err != nil { + if _, err := toml.DecodeFile(Path, &config); err != nil { return nil, fmt.Errorf("could not decode config file: %v", err) } diff --git a/internal/tunnel/status.go b/internal/tunnel/status.go index 5153d8b..2b48799 100644 --- a/internal/tunnel/status.go +++ b/internal/tunnel/status.go @@ -16,7 +16,7 @@ const ( var statusNames = map[Status]string{ Closed: log.Red + "closed" + log.Reset, - Open: log.Green + "open" + log.Reset, + Open: log.Green + "12m43s" + log.Reset, Reconn: log.Yellow + "reconn" + log.Reset, } From dff444ad2fe50259a5b3effd35690fdb1d8c56b5 Mon Sep 17 00:00:00 2001 From: Alexander Becker Date: Fri, 1 Nov 2024 21:25:30 +0100 Subject: [PATCH 12/15] Create config file on "boring e" --- cmd/boring/main.go | 35 ++++++++++++++++++++++++++--------- internal/tunnel/status.go | 2 +- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/cmd/boring/main.go b/cmd/boring/main.go index 3cc1132..74583a5 100644 --- a/cmd/boring/main.go +++ b/cmd/boring/main.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "os/exec" + "path/filepath" "runtime" "slices" "time" @@ -65,17 +66,11 @@ func prepare() (*config.Config, error) { g.Go(func() error { var err error - // Check if config file exists, otherwise we can create it - if _, statErr := os.Stat(config.Path); statErr != nil { - f, err := os.OpenFile(config.Path, os.O_RDWR|os.O_CREATE, 0600) - if err != nil { - return fmt.Errorf("could not create config file: %v", err) - } - f.Close() - log.Infof("Hi! Created boring config file: %s", config.Path) + if err = ensureConfig(); err != nil { + return fmt.Errorf("could not create config file: %v", err) } if conf, err = config.Load(); err != nil { - return fmt.Errorf("could not load configuration: %v", err) + return fmt.Errorf("could not load config: %v", err) } return nil }) @@ -228,6 +223,10 @@ func transmitCmd(cmd daemon.Cmd, resp any) error { } func openConfig() { + if err := ensureConfig(); err != nil { + log.Fatalf("could not create config file: %v", err) + } + editor := os.Getenv("EDITOR") if editor == "" { editor = "vi" @@ -235,6 +234,7 @@ func openConfig() { editor = "notepad" } } + cmd := exec.Command(editor, config.Path) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout @@ -242,6 +242,23 @@ func openConfig() { cmd.Run() } +// Checks if config file exists, otherwise creates it +func ensureConfig() error { + if _, statErr := os.Stat(config.Path); statErr != nil { + d := filepath.Dir(config.Path) + if err := os.MkdirAll(d, 0700); err != nil { + return err + } + f, err := os.OpenFile(config.Path, os.O_RDWR|os.O_CREATE, 0600) + if err != nil { + return err + } + f.Close() + log.Infof("Hi! Created boring config file: %s", config.Path) + } + return nil +} + func printUsage() { v := version if v == "" { diff --git a/internal/tunnel/status.go b/internal/tunnel/status.go index 2b48799..5153d8b 100644 --- a/internal/tunnel/status.go +++ b/internal/tunnel/status.go @@ -16,7 +16,7 @@ const ( var statusNames = map[Status]string{ Closed: log.Red + "closed" + log.Reset, - Open: log.Green + "12m43s" + log.Reset, + Open: log.Green + "open" + log.Reset, Reconn: log.Yellow + "reconn" + log.Reset, } From 4959aac14cd41cd7d473da52bc50f2d1a0525e0a Mon Sep 17 00:00:00 2001 From: Alexander Becker Date: Sat, 2 Nov 2024 09:09:03 +0100 Subject: [PATCH 13/15] Show uptime as status for open tunnels --- cmd/boring/main.go | 6 +++--- cmd/boring/status.go | 34 ++++++++++++++++++++++++++++++++++ internal/ipc/ipc.go | 4 ++-- internal/tunnel/status.go | 20 -------------------- internal/tunnel/tunnel.go | 2 ++ 5 files changed, 41 insertions(+), 25 deletions(-) create mode 100644 cmd/boring/status.go diff --git a/cmd/boring/main.go b/cmd/boring/main.go index 74583a5..32d3523 100644 --- a/cmd/boring/main.go +++ b/cmd/boring/main.go @@ -186,18 +186,18 @@ func listTunnels() { for _, t := range conf.Tunnels { if q, ok := resp.Tunnels[t.Name]; ok { - tbl.AddRow(q.Status, q.Name, q.LocalAddress, q.Mode, q.RemoteAddress, q.Host) + tbl.AddRow(status(&q), q.Name, q.LocalAddress, q.Mode, q.RemoteAddress, q.Host) visited[q.Name] = true continue } // TODO: case where tunnel is in resp but with different name - tbl.AddRow(tunnel.Closed, t.Name, t.LocalAddress, t.Mode, t.RemoteAddress, t.Host) + tbl.AddRow(status(&t), t.Name, t.LocalAddress, t.Mode, t.RemoteAddress, t.Host) } // Add tunnels that are in resp but not in the config for _, q := range resp.Tunnels { if !visited[q.Name] { - tbl.AddRow(q.Status, q.Name, q.LocalAddress, q.Mode, q.RemoteAddress, q.Host) + tbl.AddRow(status(&q), q.Name, q.LocalAddress, q.Mode, q.RemoteAddress, q.Host) } } diff --git a/cmd/boring/status.go b/cmd/boring/status.go new file mode 100644 index 0000000..c7fa56b --- /dev/null +++ b/cmd/boring/status.go @@ -0,0 +1,34 @@ +package main + +import ( + "fmt" + "time" + + "github.com/alebeck/boring/internal/log" + "github.com/alebeck/boring/internal/tunnel" +) + +func status(t *tunnel.Tunnel) string { + switch t.Status { + case tunnel.Closed: + return log.Red + "closed" + log.Reset + case tunnel.Reconn: + return log.Yellow + "reconn" + log.Reset + } + + // Tunnel is open, show uptime + since := time.Since(t.LastConn) + days := int(since / (24 * time.Hour)) + hours := int(since/time.Hour) % 24 + mins := int(since/time.Minute) % 60 + secs := int(since/time.Second) % 60 + var str string + if days > 0 { + str = fmt.Sprintf("%02dd%02dh", days, hours) + } else if hours > 0 { + str = fmt.Sprintf("%02dh%02dm", hours, mins) + } else { + str = fmt.Sprintf("%02dm%02ds", mins, secs) + } + return log.Green + str + log.Reset +} diff --git a/internal/ipc/ipc.go b/internal/ipc/ipc.go index 48b0fd5..7758889 100644 --- a/internal/ipc/ipc.go +++ b/internal/ipc/ipc.go @@ -14,7 +14,7 @@ func Send(s any, conn net.Conn) error { if err != nil { return fmt.Errorf("failed to serialize response: %v", err) } - log.Debugf("Sending: %v", data) + log.Debugf("Sending: %v", string(data)) _, err = conn.Write(append(data, '\n')) if err != nil { @@ -29,7 +29,7 @@ func Receive(s any, conn net.Conn) error { if err != nil { return fmt.Errorf("failed to read from connection: %w", err) } - log.Debugf("Received: %v", data) + log.Debugf("Received: %v", string(data)) err = json.Unmarshal(data, s) if err != nil { diff --git a/internal/tunnel/status.go b/internal/tunnel/status.go index 5153d8b..ca2abae 100644 --- a/internal/tunnel/status.go +++ b/internal/tunnel/status.go @@ -1,11 +1,5 @@ package tunnel -import ( - "fmt" - - "github.com/alebeck/boring/internal/log" -) - type Status int const ( @@ -13,17 +7,3 @@ const ( Open Reconn ) - -var statusNames = map[Status]string{ - Closed: log.Red + "closed" + log.Reset, - Open: log.Green + "open" + log.Reset, - Reconn: log.Yellow + "reconn" + log.Reset, -} - -func (s Status) String() string { - n, ok := statusNames[s] - if !ok { - return fmt.Sprintf("%d", int(s)) - } - return n -} diff --git a/internal/tunnel/tunnel.go b/internal/tunnel/tunnel.go index 3bece96..ddbd03d 100644 --- a/internal/tunnel/tunnel.go +++ b/internal/tunnel/tunnel.go @@ -29,6 +29,7 @@ type Tunnel struct { Port int `toml:"port" json:"port"` Mode Mode `toml:"mode" json:"mode"` Status Status `toml:"-" json:"status"` + LastConn time.Time `toml:"-" json:"last_conn"` Closed chan struct{} `toml:"-" json:"-"` rc *runConfig `toml:"-" json:"-"` client *ssh.Client `toml:"-" json:"-"` @@ -64,6 +65,7 @@ func (t *Tunnel) Open() error { log.Infof("Opened tunnel %v...", t.Name) t.Status = Open + t.LastConn = time.Now() return nil } From a451f8eea3267ae4fb5132a090718ecf1b152dd1 Mon Sep 17 00:00:00 2001 From: Alexander Becker Date: Sat, 2 Nov 2024 09:52:02 +0100 Subject: [PATCH 14/15] Update TODO.md --- TODO.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/TODO.md b/TODO.md index a5acde0..1328eb5 100644 --- a/TODO.md +++ b/TODO.md @@ -1,5 +1,6 @@ -* homebrew / AUR -* measure throughput (https://stackoverflow.com/questions/72808002/is-there-a-way-to-get-transfer-speed-from-io-copy) -* open and close --all command +* measure throughput / latency? (https://stackoverflow.com/questions/72808002/is-there-a-way-to-get-transfer-speed-from-io-copy) +* open and close --all command, glob support * ssh connection multiplexing (multiple tunnels share same ssh conn) * HTTP proxy? +* scriptable hooks +* documentation From ef779a040984eb75c14be66fd7809226a9a539b2 Mon Sep 17 00:00:00 2001 From: Alexander Becker Date: Sat, 2 Nov 2024 10:47:18 +0100 Subject: [PATCH 15/15] Improve logging on daemon side --- internal/daemon/daemon.go | 7 +++++-- internal/tunnel/tunnel.go | 33 +++++++++++++++++++-------------- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/internal/daemon/daemon.go b/internal/daemon/daemon.go index 2805f0a..f6860e2 100644 --- a/internal/daemon/daemon.go +++ b/internal/daemon/daemon.go @@ -148,11 +148,13 @@ func openTunnel(s *state, conn net.Conn, t tunnel.Tunnel) { _, exists := s.tunnels[t.Name] s.mutex.RUnlock() if exists { - err = fmt.Errorf("tunnel already running") + err = fmt.Errorf("already running") + log.Errorf("%v: could not open: %v", t.Name, err) return } if err = t.Open(); err != nil { + log.Errorf("%v: could not open: %v", t.Name, err) return } @@ -179,11 +181,12 @@ func closeTunnel(s *state, conn net.Conn, q tunnel.Tunnel) { s.mutex.RUnlock() if !ok { err = fmt.Errorf("tunnel not running") + log.Errorf("%v: could not close tunnel: %v", t.Name, err) return } if err = t.Close(); err != nil { - err = fmt.Errorf("could not close tunnel: %v", err) + log.Errorf("%v: could not close tunnel: %v", t.Name, err) return } <-t.Closed diff --git a/internal/tunnel/tunnel.go b/internal/tunnel/tunnel.go index ddbd03d..518ec97 100644 --- a/internal/tunnel/tunnel.go +++ b/internal/tunnel/tunnel.go @@ -63,7 +63,7 @@ func (t *Tunnel) Open() error { go t.run() - log.Infof("Opened tunnel %v...", t.Name) + log.Infof("%v: opened tunnel", t.Name) t.Status = Open t.LastConn = time.Now() return nil @@ -96,7 +96,7 @@ func (t *Tunnel) run() { disconn := make(chan struct{}) go func() { t.client.Wait() - log.Infof("Client closed for %v", t.Name) + log.Infof("%v: client closed", t.Name) close(disconn) }() @@ -106,15 +106,20 @@ func (t *Tunnel) run() { stopped := false select { case <-t.stop: - log.Infof("Received stop signal for %v...", t.Name) + log.Infof("%v: received stop signal", t.Name) stopped = true t.client.Close() case <-disconn: } t.listener.Close() t.wg.Wait() - if !stopped && t.reconnectLoop() == nil { - return + if !stopped { + if err := t.reconnectLoop(); err != nil { + log.Errorf("%v: could not re-connect: %v", t.Name, err) + } else { + // Successfully re-connected + return + } } t.Status = Closed close(t.Closed) @@ -128,12 +133,12 @@ func (t *Tunnel) keepAlive(cancel chan struct{}) { case <-time.After(keepAliveInterval): _, _, err := t.client.SendRequest("keepalive@golang.org", true, nil) if err != nil { - log.Errorf("Error sending keepalive for tunnel %v: %v", t.Name, err) + log.Errorf("%v: error sending keepalive: %v", t.Name, err) // Close the client, this triggers the reconnection logic t.client.Close() return } - log.Debugf("Sent keep-alive") + log.Debugf("%v: sent keep-alive", t.Name) } } } @@ -152,7 +157,7 @@ func (t *Tunnel) handleForward() { for { conn1, err := t.listener.Accept() if err != nil { - log.Errorf("could not accept: %v", err) + log.Errorf("%v: could not accept: %v", t.Name, err) return } go t.waitFor(func() { @@ -162,7 +167,7 @@ func (t *Tunnel) handleForward() { } conn2, err := t.dial(netw, addr) if err != nil { - log.Errorf("could not dial: %v", err) + log.Errorf("%v: could not dial: %v", t.Name, err) return } tunnel(conn1, conn2) @@ -197,7 +202,7 @@ func (t *Tunnel) handleSocks() { for { conn, err := t.listener.Accept() if err != nil { - log.Errorf("could not accept: %v", err) + log.Errorf("%v: could not accept: %v", t.Name, err) return } go t.waitFor(func() { serv.ServeConn(conn) }) @@ -213,16 +218,16 @@ func (t *Tunnel) reconnectLoop() error { for { select { case <-timeout: - return fmt.Errorf("reconnect timeout") + return fmt.Errorf("re-connect timeout") case <-t.stop: - return fmt.Errorf("reconnect interrupted") + return fmt.Errorf("re-connect interrupted by stop signal") case <-wait.C: - log.Infof("Reconnecting tunnel %v...", t.Name) + log.Infof("%v: try re-connect...", t.Name) err := t.Open() if err == nil { return nil } - log.Errorf("could not reconnect tunnel %v: %v. Retrying in %v...", + log.Errorf("%v: could not re-connect: %v. Retrying in %v...", t.Name, err, waitTime) wait.Reset(waitTime) waitTime *= 2