• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

pomerium / pomerium / 19648088498

24 Nov 2025 08:21PM UTC coverage: 53.835% (-1.0%) from 54.854%
19648088498

push

github

web-flow
Reverse tunnel TUI updates (#5938)

Updates:
- Panels can be selected with tab or the mouse
- Selected panel is highlighted with its accent color
- Help display added at the bottom, which updates based on the selected
panel
- Table rows can now be selected with the mouse, and deselected with esc
- The number keys can be used to toggle visibility of individual panels
(this also affects tab order so that hidden panels cannot be selected)
- Panels have title labels on their border
- Logs now show timestamps
- Duplicate logs within the same second are grouped together
- Logs panel now has a functional scrollbar
- Logs panel can now be scrolled with the mouse

Screenshots
<hr/>
<img width="1057" height="700" alt="image"
src="https://github.com/user-attachments/assets/c57ba32c-1b88-4d99-a317-e1ae77a35a48"
/>
<hr/>
<img width="1057" height="700" alt="image"
src="https://github.com/user-attachments/assets/b55a8ab7-9c71-4e3e-825f-721489274381"
/>
<hr/>
<img width="1057" height="700" alt="image"
src="https://github.com/user-attachments/assets/2445b67c-8520-4c55-aa89-16c608af8ebb"
/>
<hr/>
<img width="1057" height="700" alt="image"
src="https://github.com/user-attachments/assets/c3650d09-92b0-4e5d-b702-372c936644b4"
/>

7 of 1150 new or added lines in 11 files covered. (0.61%)

80 existing lines in 8 files now uncovered.

28807 of 53510 relevant lines covered (53.83%)

91.88 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

45.57
/pkg/ssh/cli.go
1
package ssh
2

3
import (
4
        "errors"
5
        "fmt"
6
        "io"
7
        "strings"
8

9
        tea "charm.land/bubbletea/v2"
10
        "github.com/muesli/termenv"
11
        "github.com/spf13/cobra"
12

13
        "github.com/pomerium/envoy-custom/api/extensions/filters/network/ssh"
14
        "github.com/pomerium/pomerium/config"
15
        "github.com/pomerium/pomerium/pkg/ssh/tui"
16
)
17

18
type CLI struct {
19
        *cobra.Command
20
        tui      *tea.Program
21
        ptyInfo  *ssh.SSHDownstreamPTYInfo
22
        username string
23
        stdin    io.Reader
24
        stdout   io.Writer
25
        stderr   io.Writer
26
}
27

28
func NewCLI(
29
        cfg *config.Config,
30
        ctrl ChannelControlInterface,
31
        ptyInfo *ssh.SSHDownstreamPTYInfo,
32
        stdin io.Reader,
33
        stdout io.Writer,
34
        stderr io.Writer,
35
) *CLI {
29✔
36
        cmd := &cobra.Command{
29✔
37
                Use: "pomerium",
29✔
38
                PersistentPreRunE: func(cmd *cobra.Command, _ []string) error {
52✔
39
                        _, cmdIsInteractive := cmd.Annotations["interactive"]
23✔
40
                        switch {
23✔
41
                        case (ptyInfo == nil) && cmdIsInteractive:
1✔
42
                                return fmt.Errorf("\x1b[31m'%s' is an interactive command and requires a TTY (try passing '-t' to ssh)\x1b[0m", cmd.Use)
1✔
43
                        }
44
                        return nil
22✔
45
                },
46
        }
47

48
        cmd.CompletionOptions.DisableDefaultCmd = true
29✔
49
        // set a non-nil args list, otherwise it will read from os.Args by default
29✔
50
        cmd.SetArgs([]string{})
29✔
51
        cmd.SetIn(stdin)
29✔
52
        cmd.SetOut(stderr)
29✔
53
        cmd.SetErr(stderr)
29✔
54
        cmd.SilenceUsage = true
29✔
55

29✔
56
        cli := &CLI{
29✔
57
                Command:  cmd,
29✔
58
                tui:      nil,
29✔
59
                ptyInfo:  ptyInfo,
29✔
60
                username: *ctrl.Username(),
29✔
61
                stdin:    stdin,
29✔
62
                stdout:   stdout,
29✔
63
                stderr:   stderr,
29✔
64
        }
29✔
65

29✔
66
        if cfg.Options.IsRuntimeFlagSet(config.RuntimeFlagSSHRoutesPortal) {
49✔
67
                cli.AddPortalCommand(ctrl)
20✔
68
        }
20✔
69
        cli.AddTunnelCommand(ctrl)
29✔
70
        cli.AddLogoutCommand(ctrl)
29✔
71
        cli.AddWhoamiCommand(ctrl)
29✔
72

29✔
73
        return cli
29✔
74
}
75

76
func (cli *CLI) AddLogoutCommand(_ ChannelControlInterface) {
29✔
77
        cli.AddCommand(&cobra.Command{
29✔
78
                Use:           "logout",
29✔
79
                Short:         "Log out",
29✔
80
                SilenceErrors: true,
29✔
81
                RunE: func(_ *cobra.Command, _ []string) error {
39✔
82
                        _, _ = cli.stderr.Write([]byte("Logged out successfully\n"))
10✔
83
                        return ErrDeleteSessionOnExit
10✔
84
                },
10✔
85
        })
86
}
87

88
func (cli *CLI) AddWhoamiCommand(ctrl ChannelControlInterface) {
29✔
89
        cli.AddCommand(&cobra.Command{
29✔
90
                Use:   "whoami",
29✔
91
                Short: "Show details for the current session",
29✔
92
                RunE: func(cmd *cobra.Command, _ []string) error {
41✔
93
                        s, err := ctrl.FormatSession(cmd.Context())
12✔
94
                        if err != nil {
14✔
95
                                return fmt.Errorf("couldn't fetch session: %w", err)
2✔
96
                        }
2✔
97
                        _, _ = cli.stderr.Write(s)
10✔
98
                        return nil
10✔
99
                },
100
        })
101
}
102

103
type sshEnviron struct {
104
        Env map[string]string
105
}
106

107
// Environ implements termenv.Environ.
UNCOV
108
func (s *sshEnviron) Environ() []string {
×
UNCOV
109
        kv := make([]string, 0, len(s.Env))
×
UNCOV
110
        for k, v := range s.Env {
×
UNCOV
111
                kv = append(kv, fmt.Sprintf("%s=%s", k, v))
×
UNCOV
112
        }
×
UNCOV
113
        return kv
×
114
}
115

116
// Getenv implements termenv.Environ.
UNCOV
117
func (s *sshEnviron) Getenv(key string) string {
×
UNCOV
118
        return s.Env[key]
×
UNCOV
119
}
×
120

121
var _ termenv.Environ = (*sshEnviron)(nil)
122

123
const (
124
        ptyWidthMax  = 512
125
        ptyHeightMax = 512
126
)
127

128
func (cli *CLI) AddTunnelCommand(ctrl ChannelControlInterface) {
29✔
129
        cli.AddCommand(&cobra.Command{
29✔
130
                Use:    "tunnel",
29✔
131
                Short:  "tunnel status",
29✔
132
                Hidden: true,
29✔
133
                Annotations: map[string]string{
29✔
134
                        "interactive": "",
29✔
135
                },
29✔
136
                RunE: func(cmd *cobra.Command, _ []string) error {
29✔
137
                        env := &sshEnviron{
×
138
                                Env: map[string]string{
×
NEW
139
                                        "TERM":      cli.ptyInfo.TermEnv,
×
NEW
140
                                        "TTY_FORCE": "1",
×
NEW
141

×
NEW
142
                                        // Important: disables synchronized output querying which I think
×
NEW
143
                                        // might be causing the renderer to get stuck
×
NEW
144
                                        "SSH_TTY": "1",
×
145
                                },
×
146
                        }
×
147

×
148
                        prog := tui.NewTunnelStatusProgram(cmd.Context(),
×
149
                                tea.WithInput(cli.stdin),
×
NEW
150
                                tea.WithWindowSize(int(min(cli.ptyInfo.WidthColumns, ptyWidthMax)), int(min(cli.ptyInfo.HeightRows, ptyHeightMax))),
×
151
                                tea.WithOutput(termenv.NewOutput(cli.stdout, termenv.WithEnvironment(env), termenv.WithUnsafe())),
×
152
                                tea.WithEnvironment(env.Environ()),
×
153
                        )
×
154
                        cli.tui = prog.Program
×
155

×
156
                        initDone := make(chan struct{})
×
157
                        go func() {
×
158
                                defer close(initDone)
×
159
                                ctrl.AddPortForwardUpdateListener(prog)
×
160
                        }()
×
161

162
                        _, err := prog.Run()
×
163
                        <-initDone
×
164
                        ctrl.RemovePortForwardUpdateListener(prog)
×
165
                        if err != nil {
×
166
                                return err
×
167
                        }
×
168
                        return nil
×
169
                },
170
        })
171
}
172

173
// ErrHandoff is a sentinel error to indicate that the command triggered a handoff,
174
// and we should not automatically disconnect
175
var ErrHandoff = errors.New("handoff")
176

177
// ErrDeleteSessionOnExit is a sentinel error to indicate that the authorized
178
// session should be deleted once the SSH connection ends.
179
var ErrDeleteSessionOnExit = errors.New("delete_session_on_exit")
180

181
func (cli *CLI) AddPortalCommand(ctrl ChannelControlInterface) {
20✔
182
        cli.AddCommand(&cobra.Command{
20✔
183
                Use:   "portal",
20✔
184
                Short: "Interactive route portal",
20✔
185
                Annotations: map[string]string{
20✔
186
                        "interactive": "",
20✔
187
                },
20✔
188
                RunE: func(cmd *cobra.Command, _ []string) error {
20✔
UNCOV
189
                        var routes []string
×
UNCOV
190
                        for r := range ctrl.AllSSHRoutes() {
×
UNCOV
191
                                routes = append(routes, fmt.Sprintf("%s@%s", *ctrl.Username(), strings.TrimPrefix(r.From, "ssh://")))
×
UNCOV
192
                        }
×
UNCOV
193
                        env := &sshEnviron{
×
UNCOV
194
                                Env: map[string]string{
×
NEW
195
                                        "TERM":      cli.ptyInfo.TermEnv,
×
NEW
196
                                        "TTY_FORCE": "1",
×
NEW
197
                                        "SSH_TTY":   "1",
×
UNCOV
198
                                },
×
UNCOV
199
                        }
×
UNCOV
200
                        signedWidth := int(min(cli.ptyInfo.WidthColumns, ptyWidthMax))
×
UNCOV
201
                        signedHeight := int(min(cli.ptyInfo.HeightRows, ptyHeightMax))
×
UNCOV
202
                        prog := tui.NewPortalProgram(cmd.Context(), routes, max(0, signedWidth-2), max(0, signedHeight-2),
×
UNCOV
203
                                tea.WithInput(cli.stdin),
×
NEW
204
                                tea.WithWindowSize(signedWidth, signedHeight),
×
UNCOV
205
                                tea.WithOutput(termenv.NewOutput(cli.stdout, termenv.WithEnvironment(env), termenv.WithUnsafe())),
×
UNCOV
206
                                tea.WithEnvironment(env.Environ()),
×
UNCOV
207
                        )
×
UNCOV
208
                        cli.tui = prog.Program
×
UNCOV
209

×
UNCOV
210
                        choice, err := prog.Run()
×
UNCOV
211
                        if err != nil {
×
212
                                return err
×
213
                        }
×
UNCOV
214
                        if choice == "" {
×
UNCOV
215
                                return nil // quit/ctrl+c
×
UNCOV
216
                        }
×
217

UNCOV
218
                        username, hostname, _ := strings.Cut(choice, "@")
×
UNCOV
219
                        // Perform authorize check for this route
×
UNCOV
220
                        if username != cli.username {
×
221
                                panic("bug: username mismatch")
×
222
                        }
UNCOV
223
                        if hostname == "" {
×
224
                                panic("bug: hostname is empty")
×
225
                        }
UNCOV
226
                        handoffMsg, err := ctrl.PrepareHandoff(cmd.Context(), hostname, cli.ptyInfo)
×
UNCOV
227
                        if err != nil {
×
228
                                return err
×
229
                        }
×
UNCOV
230
                        if err := ctrl.SendControlAction(handoffMsg); err != nil {
×
231
                                return err
×
232
                        }
×
UNCOV
233
                        return ErrHandoff
×
234
                },
235
        })
236
}
237

UNCOV
238
func (cli *CLI) SendTeaMsg(msg tea.Msg) {
×
UNCOV
239
        if cli.tui != nil {
×
UNCOV
240
                go cli.tui.Send(msg)
×
UNCOV
241
        }
×
242
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc