sshkeeper: v0.2.0 stabilization (route display, forward validation, tunnel UX, README)
This commit is contained in:
parent
21444d3826
commit
77a84a487f
10
README.md
10
README.md
|
|
@ -36,7 +36,7 @@ Download the latest Linux x86_64 release from:
|
||||||
https://github.com/mirivlad/sshkeeper/releases/latest
|
https://github.com/mirivlad/sshkeeper/releases/latest
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
tar -xzf sshkeeper_v0.1.0_linux_amd64.tar.gz
|
tar -xzf sshkeeper_v0.2.0_linux_amd64.tar.gz
|
||||||
chmod +x sshkeeper-linux-amd64
|
chmod +x sshkeeper-linux-amd64
|
||||||
sudo install -m 0755 sshkeeper-linux-amd64 /usr/local/bin/sshkeeper
|
sudo install -m 0755 sshkeeper-linux-amd64 /usr/local/bin/sshkeeper
|
||||||
sshkeeper
|
sshkeeper
|
||||||
|
|
@ -156,6 +156,14 @@ sshkeeper tunnel web
|
||||||
# Starts SSH session with all configured forwards active.
|
# Starts SSH session with all configured forwards active.
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Connect vs Tunnel
|
||||||
|
|
||||||
|
- **`sshkeeper connect <alias>`** (or `Enter` in TUI) — standard SSH session, no port forwards.
|
||||||
|
- **`sshkeeper forward`** (or `Ctrl+W` in TUI) — manage port forwards for a server.
|
||||||
|
- **`sshkeeper tunnel <alias>`** — start SSH session **with** all configured forwards active.
|
||||||
|
- **`sshkeeper tunnel <alias> --forward-only`** — start tunnel only (`ssh -N`), useful for background port forwarding.
|
||||||
|
- **TUI Action Menu** (`Ctrl+X`) — offers Connect, Start tunnel, and Tunnel mode for the selected server.
|
||||||
|
|
||||||
Commands that only read profile metadata, such as `list`, `show`, `search`,
|
Commands that only read profile metadata, such as `list`, `show`, `search`,
|
||||||
`config path`, `group list`, and `export`, do not require the master password.
|
`config path`, `group list`, and `export`, do not require the master password.
|
||||||
Commands that need secrets ask for the master password in that process. Adding
|
Commands that need secrets ask for the master password in that process. Adding
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,47 @@ var forwardAddCmd = &cobra.Command{
|
||||||
remoteAddr, _ := cmd.Flags().GetString("remote-addr")
|
remoteAddr, _ := cmd.Flags().GetString("remote-addr")
|
||||||
remotePort, _ := cmd.Flags().GetInt("remote-port")
|
remotePort, _ := cmd.Flags().GetInt("remote-port")
|
||||||
|
|
||||||
|
// Validate type
|
||||||
|
if fwdType != "local" && fwdType != "remote" && fwdType != "dynamic" {
|
||||||
|
return fmt.Errorf("invalid forward type %q: must be local, remote, or dynamic", fwdType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate ports
|
||||||
|
if localPort < 1 || localPort > 65535 {
|
||||||
|
return fmt.Errorf("invalid local port %d: must be 1-65535", localPort)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate fields based on type
|
||||||
|
switch fwdType {
|
||||||
|
case "local":
|
||||||
|
if localAddr == "" || localAddr == "0.0.0.0" {
|
||||||
|
localAddr = "0.0.0.0"
|
||||||
|
}
|
||||||
|
if remoteAddr == "" {
|
||||||
|
return fmt.Errorf("remote-addr is required for local forward")
|
||||||
|
}
|
||||||
|
if remotePort < 1 || remotePort > 65535 {
|
||||||
|
return fmt.Errorf("invalid remote port %d: must be 1-65535", remotePort)
|
||||||
|
}
|
||||||
|
case "remote":
|
||||||
|
if remoteAddr == "" {
|
||||||
|
return fmt.Errorf("remote-addr is required for remote forward")
|
||||||
|
}
|
||||||
|
if remotePort < 1 || remotePort > 65535 {
|
||||||
|
return fmt.Errorf("invalid remote port %d: must be 1-65535", remotePort)
|
||||||
|
}
|
||||||
|
if localAddr == "" {
|
||||||
|
localAddr = "0.0.0.0"
|
||||||
|
}
|
||||||
|
case "dynamic":
|
||||||
|
if localAddr == "" || localAddr == "0.0.0.0" {
|
||||||
|
localAddr = "0.0.0.0"
|
||||||
|
}
|
||||||
|
// dynamic doesn't use target fields — clear them
|
||||||
|
remoteAddr = ""
|
||||||
|
remotePort = 0
|
||||||
|
}
|
||||||
|
|
||||||
fwd := &model.Forward{
|
fwd := &model.Forward{
|
||||||
ServerID: server.ID,
|
ServerID: server.ID,
|
||||||
Type: model.ForwardType(fwdType),
|
Type: model.ForwardType(fwdType),
|
||||||
|
|
@ -74,10 +115,11 @@ var forwardAddCmd = &cobra.Command{
|
||||||
RemotePort: remotePort,
|
RemotePort: remotePort,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := appDB.AddForward(fwd.ServerID, fwd.Type, fwd.LocalAddr, fwd.LocalPort, fwd.RemoteAddr, fwd.RemotePort); err != nil {
|
fwdID, err := appDB.AddForward(fwd.ServerID, fwd.Type, fwd.LocalAddr, fwd.LocalPort, fwd.RemoteAddr, fwd.RemotePort)
|
||||||
|
if err != nil {
|
||||||
return fmt.Errorf("add forward: %w", err)
|
return fmt.Errorf("add forward: %w", err)
|
||||||
}
|
}
|
||||||
fmt.Printf("✓ Forward added [%d]\n", fwd.ID)
|
fmt.Printf("✓ Forward added [%d]\n", fwdID)
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -92,10 +134,25 @@ var forwardDeleteCmd = &cobra.Command{
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("invalid forward ID: %s", args[1])
|
return fmt.Errorf("invalid forward ID: %s", args[1])
|
||||||
}
|
}
|
||||||
// Verify server exists
|
server, err := appDB.GetServer(alias)
|
||||||
if _, err := appDB.GetServer(alias); err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("server not found: %s", alias)
|
return fmt.Errorf("server not found: %s", alias)
|
||||||
}
|
}
|
||||||
|
// Verify forward belongs to this server
|
||||||
|
forwards, err := appDB.GetForwards(server.ID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("load forwards: %w", err)
|
||||||
|
}
|
||||||
|
found := false
|
||||||
|
for _, f := range forwards {
|
||||||
|
if f.ID == id {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
return fmt.Errorf("forward %d does not belong to server %s", id, alias)
|
||||||
|
}
|
||||||
if err := appDB.DeleteForward(id); err != nil {
|
if err := appDB.DeleteForward(id); err != nil {
|
||||||
return fmt.Errorf("delete forward: %w", err)
|
return fmt.Errorf("delete forward: %w", err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,9 @@ package cmd
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/charmbracelet/lipgloss"
|
"github.com/charmbracelet/lipgloss"
|
||||||
"github.com/mirivlad/sshkeeper/internal/model"
|
"github.com/mirivlad/sshkeeper/internal/model"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
var listCmd = &cobra.Command{
|
var listCmd = &cobra.Command{
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/mirivlad/sshkeeper/internal/ssh"
|
"github.com/mirivlad/sshkeeper/internal/ssh"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
var sshConfigCmd = &cobra.Command{
|
var sshConfigCmd = &cobra.Command{
|
||||||
|
|
|
||||||
43
cmd/tui.go
43
cmd/tui.go
|
|
@ -128,7 +128,8 @@ func runTUI() error {
|
||||||
return appDB.GetForwards(serverID)
|
return appDB.GetForwards(serverID)
|
||||||
}
|
}
|
||||||
tui.SaveForward = func(fwd *model.Forward) error {
|
tui.SaveForward = func(fwd *model.Forward) error {
|
||||||
return appDB.AddForward(fwd.ServerID, fwd.Type, fwd.LocalAddr, fwd.LocalPort, fwd.RemoteAddr, fwd.RemotePort)
|
_, err := appDB.AddForward(fwd.ServerID, fwd.Type, fwd.LocalAddr, fwd.LocalPort, fwd.RemoteAddr, fwd.RemotePort)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
tui.DeleteForward = func(forwardID int64) error {
|
tui.DeleteForward = func(forwardID int64) error {
|
||||||
return appDB.DeleteForward(forwardID)
|
return appDB.DeleteForward(forwardID)
|
||||||
|
|
@ -208,6 +209,46 @@ func runTUI() error {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if result != nil && (result.Action == "tunnel" || result.Action == "tunnel_n") && result.Server != nil {
|
||||||
|
server := result.Server
|
||||||
|
fresh, err := appDB.GetServer(server.Alias)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Server not found: %s\n", server.Alias)
|
||||||
|
servers, _ = appDB.ListServers()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load forwards
|
||||||
|
forwards, err := appDB.GetForwards(fresh.ID)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Load forwards: %v\n", err)
|
||||||
|
servers, _ = appDB.ListServers()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
forwardOnly := result.Action == "tunnel_n"
|
||||||
|
if len(forwards) > 0 {
|
||||||
|
fmt.Printf("Starting tunnel to %s with %d forward(s)...\n", fresh.Alias, len(forwards))
|
||||||
|
} else {
|
||||||
|
fmt.Printf("Starting session to %s...\n", fresh.Alias)
|
||||||
|
}
|
||||||
|
|
||||||
|
sshArgs := ssh.BuildSSHArgs(fresh, forwards, forwardOnly)
|
||||||
|
if err := ssh.ConnectWithArgs(cfg, sshArgs, vaultFunc, fresh); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Tunnel error: %v\n", err)
|
||||||
|
} else {
|
||||||
|
fmt.Println("Tunnel closed.")
|
||||||
|
}
|
||||||
|
appDB.UpdateLastConnected(fresh.Alias)
|
||||||
|
|
||||||
|
fmt.Println("\n[Press Enter to return to sshkeeper]")
|
||||||
|
buf := make([]byte, 1)
|
||||||
|
os.Stdin.Read(buf)
|
||||||
|
|
||||||
|
servers, _ = appDB.ListServers()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
// Normal quit (q or Esc)
|
// Normal quit (q or Esc)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
## Theme
|
## Status
|
||||||
|
|
||||||
|
✅ Completed — released as v0.2.0.
|
||||||
|
|
||||||
Routes, tunnels and cleaner TUI.
|
Routes, tunnels and cleaner TUI.
|
||||||
|
|
||||||
|
|
@ -322,12 +322,15 @@ func (db *DB) GetServerTags(serverID int64) ([]string, error) {
|
||||||
|
|
||||||
// --- Forward methods ---
|
// --- Forward methods ---
|
||||||
|
|
||||||
func (db *DB) AddForward(serverID int64, fwdType model.ForwardType, localAddr string, localPort int, remoteAddr string, remotePort int) error {
|
func (db *DB) AddForward(serverID int64, fwdType model.ForwardType, localAddr string, localPort int, remoteAddr string, remotePort int) (int64, error) {
|
||||||
_, err := db.conn.Exec(`
|
result, err := db.conn.Exec(`
|
||||||
INSERT INTO forwards (server_id, type, local_addr, local_port, remote_addr, remote_port)
|
INSERT INTO forwards (server_id, type, local_addr, local_port, remote_addr, remote_port)
|
||||||
VALUES (?, ?, ?, ?, ?, ?)`,
|
VALUES (?, ?, ?, ?, ?, ?)`,
|
||||||
serverID, fwdType, localAddr, localPort, remoteAddr, remotePort)
|
serverID, fwdType, localAddr, localPort, remoteAddr, remotePort)
|
||||||
return err
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return result.LastInsertId()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *DB) GetForwards(serverID int64) ([]*model.Forward, error) {
|
func (db *DB) GetForwards(serverID int64) ([]*model.Forward, error) {
|
||||||
|
|
|
||||||
|
|
@ -961,10 +961,34 @@ func (m *tuiModel) updateActionMenu(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if action != nil {
|
if action != nil {
|
||||||
m.screen = screenList
|
|
||||||
m.actionMenu = nil
|
|
||||||
// Handle known actions
|
// Handle known actions
|
||||||
switch *action {
|
switch *action {
|
||||||
|
case "connect":
|
||||||
|
if item, ok := m.list.SelectedItem().(serverItem); ok {
|
||||||
|
m.screen = screenList
|
||||||
|
m.actionMenu = nil
|
||||||
|
return m, func() tea.Msg {
|
||||||
|
return connectRequestMsg{server: item.server}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "tunnel":
|
||||||
|
if item, ok := m.list.SelectedItem().(serverItem); ok {
|
||||||
|
m.result = &TUIResult{
|
||||||
|
Server: item.server,
|
||||||
|
Action: "tunnel",
|
||||||
|
Servers: []*model.Server{item.server},
|
||||||
|
}
|
||||||
|
return m, tea.Quit
|
||||||
|
}
|
||||||
|
case "tunnel_n":
|
||||||
|
if item, ok := m.list.SelectedItem().(serverItem); ok {
|
||||||
|
m.result = &TUIResult{
|
||||||
|
Server: item.server,
|
||||||
|
Action: "tunnel_n",
|
||||||
|
Servers: []*model.Server{item.server},
|
||||||
|
}
|
||||||
|
return m, tea.Quit
|
||||||
|
}
|
||||||
case "delete":
|
case "delete":
|
||||||
if item, ok := m.list.SelectedItem().(serverItem); ok {
|
if item, ok := m.list.SelectedItem().(serverItem); ok {
|
||||||
return m, func() tea.Msg {
|
return m, func() tea.Msg {
|
||||||
|
|
@ -1069,7 +1093,7 @@ func (m *tuiModel) viewServerList() string {
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
b.WriteString(helpStyle.Render(fmt.Sprintf("Vault unlocked | %s", testSummary(m.servers))))
|
b.WriteString(helpStyle.Render(fmt.Sprintf("Vault unlocked | %s", testSummary(m.servers))))
|
||||||
b.WriteString("\n\n")
|
b.WriteString("\n\n")
|
||||||
b.WriteString(listHeaderStyle.Render(fmt.Sprintf(" %-20s %-20s %-34s %-12s %-10s %s", "NAME", "ALIAS", "TARGET", "AUTH", "GROUP", "STATUS")))
|
b.WriteString(listHeaderStyle.Render(fmt.Sprintf(" %-20s %-20s %-34s %-12s %-10s %s", "NAME", "ALIAS", "ROUTE", "AUTH", "GROUP", "STATUS")))
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
|
|
||||||
if len(m.servers) == 0 {
|
if len(m.servers) == 0 {
|
||||||
|
|
@ -1096,6 +1120,16 @@ func (m *tuiModel) viewServerList() string {
|
||||||
name = server.Alias
|
name = server.Alias
|
||||||
}
|
}
|
||||||
target := fmt.Sprintf("%s@%s:%d", server.User, server.Host, server.Port)
|
target := fmt.Sprintf("%s@%s:%d", server.User, server.Host, server.Port)
|
||||||
|
routeStr := server.Route.DisplaySummary(target)
|
||||||
|
// If too long, collapse middle hops
|
||||||
|
if len(routeStr) > 34 && len(server.Route.Hops) > 2 {
|
||||||
|
first := server.Route.Hops[0]
|
||||||
|
firstName := first.Alias
|
||||||
|
if !first.IsProfile {
|
||||||
|
firstName = first.Raw
|
||||||
|
}
|
||||||
|
routeStr = fmt.Sprintf("%s → … → %s", firstName, truncate(target, 34-len(firstName)-6))
|
||||||
|
}
|
||||||
group := server.GroupName
|
group := server.GroupName
|
||||||
if group == "" {
|
if group == "" {
|
||||||
group = "-"
|
group = "-"
|
||||||
|
|
@ -1104,7 +1138,7 @@ func (m *tuiModel) viewServerList() string {
|
||||||
marker,
|
marker,
|
||||||
truncate(name, 20),
|
truncate(name, 20),
|
||||||
truncate(server.Alias, 20),
|
truncate(server.Alias, 20),
|
||||||
truncate(target, 34),
|
truncate(routeStr, 34),
|
||||||
authLabel(server.AuthMethod),
|
authLabel(server.AuthMethod),
|
||||||
truncate(group, 10),
|
truncate(group, 10),
|
||||||
testStatusLabel(server),
|
testStatusLabel(server),
|
||||||
|
|
@ -1236,9 +1270,10 @@ func (m *tuiModel) viewSelectedServer(server *model.Server) string {
|
||||||
b.WriteString(fmt.Sprintf(" Host: %s\n", server.Host))
|
b.WriteString(fmt.Sprintf(" Host: %s\n", server.Host))
|
||||||
b.WriteString(fmt.Sprintf(" Port: %d\n", server.Port))
|
b.WriteString(fmt.Sprintf(" Port: %d\n", server.Port))
|
||||||
b.WriteString(fmt.Sprintf(" User: %s\n", server.User))
|
b.WriteString(fmt.Sprintf(" User: %s\n", server.User))
|
||||||
|
target := fmt.Sprintf("%s@%s:%d", server.User, server.Host, server.Port)
|
||||||
|
b.WriteString(fmt.Sprintf(" Target: %s\n", target))
|
||||||
b.WriteString(fmt.Sprintf(" Auth: %s\n", authLabel(server.AuthMethod)))
|
b.WriteString(fmt.Sprintf(" Auth: %s\n", authLabel(server.AuthMethod)))
|
||||||
if len(server.Route.Hops) > 0 {
|
if len(server.Route.Hops) > 0 {
|
||||||
target := fmt.Sprintf("%s@%s:%d", server.User, server.Host, server.Port)
|
|
||||||
b.WriteString(fmt.Sprintf(" Route: %s\n", server.Route.DisplaySummary(target)))
|
b.WriteString(fmt.Sprintf(" Route: %s\n", server.Route.DisplaySummary(target)))
|
||||||
} else if server.ProxyJump != "" {
|
} else if server.ProxyJump != "" {
|
||||||
b.WriteString(fmt.Sprintf(" ProxyJump: %s\n", server.ProxyJump))
|
b.WriteString(fmt.Sprintf(" ProxyJump: %s\n", server.ProxyJump))
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ func TestServerListViewUsesDashboardLayout(t *testing.T) {
|
||||||
"2 servers",
|
"2 servers",
|
||||||
"Vault",
|
"Vault",
|
||||||
"NAME",
|
"NAME",
|
||||||
"TARGET",
|
"ROUTE",
|
||||||
"AUTH",
|
"AUTH",
|
||||||
"GROUP",
|
"GROUP",
|
||||||
"STATUS",
|
"STATUS",
|
||||||
|
|
|
||||||
|
|
@ -281,19 +281,59 @@ func (fm *forwardFormModel) runSave() tea.Cmd {
|
||||||
return func() tea.Msg {
|
return func() tea.Msg {
|
||||||
fwdType := model.ForwardType(strings.TrimSpace(fm.inputs[0].Value()))
|
fwdType := model.ForwardType(strings.TrimSpace(fm.inputs[0].Value()))
|
||||||
if fwdType == "" {
|
if fwdType == "" {
|
||||||
fwdType = model.ForwardLocal
|
return saveDoneMsg{err: fmt.Errorf("forward type is required (local/remote/dynamic)")}
|
||||||
}
|
}
|
||||||
|
if fwdType != model.ForwardLocal && fwdType != model.ForwardRemote && fwdType != model.ForwardDynamic {
|
||||||
|
return saveDoneMsg{err: fmt.Errorf("invalid forward type %q", fwdType)}
|
||||||
|
}
|
||||||
|
|
||||||
localPort := 0
|
localPort := 0
|
||||||
fmt.Sscanf(fm.inputs[2].Value(), "%d", &localPort)
|
fmt.Sscanf(fm.inputs[2].Value(), "%d", &localPort)
|
||||||
remotePort := 0
|
remotePort := 0
|
||||||
fmt.Sscanf(fm.inputs[4].Value(), "%d", &remotePort)
|
fmt.Sscanf(fm.inputs[4].Value(), "%d", &remotePort)
|
||||||
|
|
||||||
|
localAddr := strings.TrimSpace(fm.inputs[1].Value())
|
||||||
|
remoteAddr := strings.TrimSpace(fm.inputs[3].Value())
|
||||||
|
|
||||||
|
if localPort < 1 || localPort > 65535 {
|
||||||
|
return saveDoneMsg{err: fmt.Errorf("invalid listen port %d: must be 1-65535", localPort)}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch fwdType {
|
||||||
|
case model.ForwardLocal:
|
||||||
|
if localAddr == "" {
|
||||||
|
localAddr = "0.0.0.0"
|
||||||
|
}
|
||||||
|
if remoteAddr == "" {
|
||||||
|
return saveDoneMsg{err: fmt.Errorf("target address is required for local forward")}
|
||||||
|
}
|
||||||
|
if remotePort < 1 || remotePort > 65535 {
|
||||||
|
return saveDoneMsg{err: fmt.Errorf("invalid target port %d: must be 1-65535", remotePort)}
|
||||||
|
}
|
||||||
|
case model.ForwardRemote:
|
||||||
|
if remoteAddr == "" {
|
||||||
|
return saveDoneMsg{err: fmt.Errorf("target address is required for remote forward")}
|
||||||
|
}
|
||||||
|
if remotePort < 1 || remotePort > 65535 {
|
||||||
|
return saveDoneMsg{err: fmt.Errorf("invalid target port %d: must be 1-65535", remotePort)}
|
||||||
|
}
|
||||||
|
if localAddr == "" {
|
||||||
|
localAddr = "0.0.0.0"
|
||||||
|
}
|
||||||
|
case model.ForwardDynamic:
|
||||||
|
if localAddr == "" {
|
||||||
|
localAddr = "0.0.0.0"
|
||||||
|
}
|
||||||
|
remoteAddr = ""
|
||||||
|
remotePort = 0
|
||||||
|
}
|
||||||
|
|
||||||
fwd := &model.Forward{
|
fwd := &model.Forward{
|
||||||
ServerID: fm.serverID,
|
ServerID: fm.serverID,
|
||||||
Type: fwdType,
|
Type: fwdType,
|
||||||
LocalAddr: fm.inputs[1].Value(),
|
LocalAddr: localAddr,
|
||||||
LocalPort: localPort,
|
LocalPort: localPort,
|
||||||
RemoteAddr: fm.inputs[3].Value(),
|
RemoteAddr: remoteAddr,
|
||||||
RemotePort: remotePort,
|
RemotePort: remotePort,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -120,6 +120,9 @@ type actionMenuModel struct {
|
||||||
|
|
||||||
func newActionMenuModel(w, h int) *actionMenuModel {
|
func newActionMenuModel(w, h int) *actionMenuModel {
|
||||||
items := []list.Item{
|
items := []list.Item{
|
||||||
|
actionMenuItem{label: "Connect", action: "connect"},
|
||||||
|
actionMenuItem{label: "Start tunnel", action: "tunnel"},
|
||||||
|
actionMenuItem{label: "Tunnel mode (-N)", action: "tunnel_n"},
|
||||||
actionMenuItem{label: "Delete", action: "delete"},
|
actionMenuItem{label: "Delete", action: "delete"},
|
||||||
actionMenuItem{label: "Test connection", action: "test"},
|
actionMenuItem{label: "Test connection", action: "test"},
|
||||||
actionMenuItem{label: "Tags", action: "tags"},
|
actionMenuItem{label: "Tags", action: "tags"},
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue