Skip to content

Commit

Permalink
Merge branch 'develop'
Browse files Browse the repository at this point in the history
  • Loading branch information
alebeck committed Nov 2, 2024
2 parents 23e110e + ef779a0 commit d8c5c67
Show file tree
Hide file tree
Showing 11 changed files with 210 additions and 146 deletions.
7 changes: 4 additions & 3 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -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
81 changes: 59 additions & 22 deletions cmd/boring/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import (
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"slices"
"time"

Expand Down Expand Up @@ -45,6 +48,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()
Expand All @@ -61,24 +66,18 @@ 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)
if err = ensureConfig(); 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 config: %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
})
Expand Down Expand Up @@ -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.Path)
return
}

Expand All @@ -133,9 +132,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)
}
Expand All @@ -153,9 +152,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)
}
}

Expand Down Expand Up @@ -187,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)
}
}

Expand All @@ -223,6 +222,43 @@ func transmitCmd(cmd daemon.Cmd, resp any) error {
return nil
}

func openConfig() {
if err := ensureConfig(); err != nil {
log.Fatalf("could not create config file: %v", err)
}

editor := os.Getenv("EDITOR")
if editor == "" {
editor = "vi"
if runtime.GOOS == "windows" {
editor = "notepad"
}
}

cmd := exec.Command(editor, config.Path)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
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 == "" {
Expand All @@ -234,7 +270,8 @@ func printUsage() {

fmt.Printf("boring %s\n", v)
fmt.Println("Usage:")
fmt.Println(" boring list,l List tunnels")
fmt.Println(" boring open,o <name1> [<name2> ...] Open specified tunnel(s)")
fmt.Println(" boring close,c <name1> [<name2> ...] Close specified tunnel(s)")
fmt.Println(" boring l, list List tunnels")
fmt.Println(" boring o, open <name1> [<name2> ...] Open specified tunnel(s)")
fmt.Println(" boring c, close <name1> [<name2> ...] Close specified tunnel(s)")
fmt.Println(" boring e, edit Edit configuration file")
}
34 changes: 34 additions & 0 deletions cmd/boring/status.go
Original file line number Diff line number Diff line change
@@ -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
}
37 changes: 26 additions & 11 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,19 @@ import (
"fmt"
"os"
"path/filepath"
"runtime"

"github.com/BurntSushi/toml"
"github.com/alebeck/boring/internal/paths"
"github.com/alebeck/boring/internal/tunnel"
)

const (
defaultFileName = "~/.boring.toml"
socksLabel = "[SOCKS5 proxy]"
fileName = ".boring.toml"
socksLabel = "[SOCKS]"
)

var FileName string
var Path string

// Config represents the application configuration as parsed from ./boring.toml
type Config struct {
Expand All @@ -24,17 +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)
}

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)
}

Expand All @@ -48,11 +61,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
}
}

Expand Down
9 changes: 6 additions & 3 deletions internal/daemon/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}

Expand All @@ -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
Expand Down
5 changes: 2 additions & 3 deletions internal/ipc/ipc.go
Original file line number Diff line number Diff line change
Expand Up @@ -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", string(data))

_, err = conn.Write(append(data, '\n'))
if err != nil {
Expand All @@ -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", string(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
}
Loading

0 comments on commit d8c5c67

Please sign in to comment.