From 41c867884d86348c03fdc390e0a7db8ad7ddf062 Mon Sep 17 00:00:00 2001 From: Senchong Chen Date: Mon, 3 Nov 2025 11:34:22 +0800 Subject: [PATCH] Add optional group field for organizing tunnels Tunnels can now be organized into groups using an optional 'group' field in the configuration. When listing tunnels, they are displayed grouped by their group field. Tunnels without a group are placed in a 'default' group. This change is fully backward compatible with existing configurations. --- cmd/boring/tunnels.go | 40 ++++++++++++++++++++++++++++++++------- examples/config.toml | 6 ++++++ internal/tunnel/tunnel.go | 1 + 3 files changed, 40 insertions(+), 7 deletions(-) diff --git a/cmd/boring/tunnels.go b/cmd/boring/tunnels.go index 81421be..17250b3 100644 --- a/cmd/boring/tunnels.go +++ b/cmd/boring/tunnels.go @@ -196,27 +196,53 @@ func listTunnels() { return } - tbl := table.New("Status", "Name", "Local", "", "Remote", "Via") + // Group tunnels by their group field + groupedTunnels := make(map[string][]*tunnel.Desc) visited := make(map[string]bool) + // Process configured tunnels for _, t := range conf.Tunnels { + var tunnelToAdd *tunnel.Desc if q, ok := ts[t.Name]; ok { - tbl.AddRow(status(q), q.Name, q.LocalAddress, q.Mode, q.RemoteAddress, q.Host) + tunnelToAdd = q visited[q.Name] = true - continue + } else { + tunnelToAdd = &t } - // TODO: case where tunnel is in resp but with different name - tbl.AddRow(status(&t), t.Name, t.LocalAddress, t.Mode, t.RemoteAddress, t.Host) + + group := tunnelToAdd.Group + if group == "" { + group = "default" + } + groupedTunnels[group] = append(groupedTunnels[group], tunnelToAdd) } // Add tunnels that are in resp but not in the config for _, q := range ts { if !visited[q.Name] { - tbl.AddRow(status(q), q.Name, q.LocalAddress, q.Mode, q.RemoteAddress, q.Host) + group := q.Group + if group == "" { + group = "default" + } + groupedTunnels[group] = append(groupedTunnels[group], q) } } - log.Emitf("%v", tbl) + // Display tunnels grouped by group + first := true + for groupName, tunnels := range groupedTunnels { + if !first { + log.Emitf("\n") + } + first = false + + log.Emitf("Group: %s\n", groupName) + tbl := table.New("Status", "Name", "Local", "", "Remote", "Via") + for _, t := range tunnels { + tbl.AddRow(status(t), t.Name, t.LocalAddress, t.Mode, t.RemoteAddress, t.Host) + } + log.Emitf("%v", tbl) + } } func transmitCmd(cmd daemon.Cmd, resp any) error { diff --git a/examples/config.toml b/examples/config.toml index 8a56e5a..ce717f0 100644 --- a/examples/config.toml +++ b/examples/config.toml @@ -4,6 +4,7 @@ name = "dev" local = "9000" remote = "localhost:9000" host = "dev-server" # automatically matches host against SSH config +group = "development" # optional: group tunnels for organized display # simple remote (-R) tunnel; note that we can also use IP{v4,v6} addresses [[tunnels]] @@ -12,6 +13,7 @@ local = "[::1]:9000" remote = "9000" host = "dev-server" mode = "remote" +group = "development" # example of an explicit host (doesn't use SSH config) [[tunnels]] @@ -21,6 +23,7 @@ remote = "localhost:5001" host = "prod.example.com" user = "root" identity = "~/.ssh/id_prod" # will try default ones if not set +group = "production" # example using Unix sockets, and remote (-R) mode; # note that we can freely mix unix and TCP sockets @@ -30,6 +33,7 @@ local = "/tmp/serve.sock" remote = "/tmp/listen.sock" host = "dev-server" mode = "remote" +group = "development" # example of a SOCKS5 proxy; this will setup a SOCKS5 server at # port 9000 and forward all traffic through `dev-server`. @@ -38,6 +42,7 @@ name = "dev-prox" local = "9000" host = "dev-server" mode = "socks" +group = "proxies" # reverse SOCKS5 proxy; this will setup a SOCKS5 server at # port 9000 on `dev-server` and forward all traffic through @@ -47,3 +52,4 @@ name = "dev-rev-prox" remote = "9000" host = "dev-server" mode = "socks-remote" +group = "proxies" diff --git a/internal/tunnel/tunnel.go b/internal/tunnel/tunnel.go index 25ced69..a419635 100644 --- a/internal/tunnel/tunnel.go +++ b/internal/tunnel/tunnel.go @@ -34,6 +34,7 @@ type Desc struct { Port int `toml:"port" json:"port"` KeepAlive *int `toml:"keep_alive" json:"keep_alive"` Mode Mode `toml:"mode" json:"mode"` + Group string `toml:"group" json:"group"` Status Status `toml:"-" json:"status"` LastConn time.Time `toml:"-" json:"last_conn"` }