gssh-agent 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +175 -0
- package/bin/daemon +0 -0
- package/bin/gssh +0 -0
- package/bin/gssh-daemon +0 -0
- package/cmd/daemon/main.go +45 -0
- package/cmd/gssh/main.go +394 -0
- package/go.mod +10 -0
- package/go.sum +8 -0
- package/gssh-darwin-arm64.tar.gz +0 -0
- package/homebrew/gssh.plist +22 -0
- package/homebrew/install.sh +43 -0
- package/idea.md +271 -0
- package/internal/client/ssh.go +143 -0
- package/internal/client/ssh_test.go +33 -0
- package/internal/portforward/forwarder.go +187 -0
- package/internal/protocol/types.go +90 -0
- package/internal/protocol/types_test.go +87 -0
- package/internal/session/manager.go +380 -0
- package/internal/session/manager_test.go +52 -0
- package/package.json +25 -0
- package/pkg/rpc/handler.go +206 -0
- package/skill.md +96 -0
package/README.md
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# gssh - SSH Session Manager for Agents
|
|
2
|
+
|
|
3
|
+
gssh 是一个供 Agent 使用的 SSH Session 管理工具。通过 Go 语言实现,支持多个 SSH 会话管理、命令执行、端口转发和自动重连。
|
|
4
|
+
|
|
5
|
+
## 特性
|
|
6
|
+
|
|
7
|
+
- 多个 SSH Session 并发管理
|
|
8
|
+
- 命令执行与结果透传(stdout/stderr/exit code)
|
|
9
|
+
- TCP 端口转发(本地/远程)
|
|
10
|
+
- 断线自动重连
|
|
11
|
+
- 支持密码认证和 SSH 密钥认证
|
|
12
|
+
- 被连接机器零配置(普通 SSH 即可)
|
|
13
|
+
|
|
14
|
+
## 安装
|
|
15
|
+
|
|
16
|
+
### 方式一:Homebrew(推荐)
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
# 添加 tap(未来会支持)
|
|
20
|
+
# brew tap forechoandlook/gssh
|
|
21
|
+
# brew install gssh
|
|
22
|
+
|
|
23
|
+
# 暂时使用手动安装方式
|
|
24
|
+
git clone https://github.com/forechoandlook/gssh.git
|
|
25
|
+
cd gssh
|
|
26
|
+
go build -o bin/daemon cmd/daemon/main.go
|
|
27
|
+
go build -o bin/gssh cmd/gssh/main.go
|
|
28
|
+
./homebrew/install.sh
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### 方式二:直接安装
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
# 克隆项目
|
|
35
|
+
git clone https://github.com/forechoandlook/gssh.git
|
|
36
|
+
cd gssh
|
|
37
|
+
|
|
38
|
+
# 构建
|
|
39
|
+
go build -o bin/daemon cmd/daemon/main.go
|
|
40
|
+
go build -o bin/gssh cmd/gssh/main.go
|
|
41
|
+
|
|
42
|
+
# 复制到系统路径
|
|
43
|
+
cp bin/daemon /usr/local/bin/gssh-daemon
|
|
44
|
+
cp bin/gssh /usr/local/bin/gssh
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## 服务管理(macOS)
|
|
48
|
+
|
|
49
|
+
### 使用 launchctl
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
# 启动服务
|
|
53
|
+
launchctl start com.gssh.daemon
|
|
54
|
+
|
|
55
|
+
# 停止服务
|
|
56
|
+
launchctl stop com.gssh.daemon
|
|
57
|
+
|
|
58
|
+
# 查看状态
|
|
59
|
+
launchctl list | grep gssh
|
|
60
|
+
|
|
61
|
+
# 卸载服务
|
|
62
|
+
launchctl unload ~/Library/LaunchAgents/gssh.plist
|
|
63
|
+
rm ~/Library/LaunchAgents/gssh.plist
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### 使用 Homebrew services
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
# 启动(首次安装后自动启动)
|
|
70
|
+
brew services start gssh
|
|
71
|
+
|
|
72
|
+
# 停止
|
|
73
|
+
brew services stop gssh
|
|
74
|
+
|
|
75
|
+
# 查看状态
|
|
76
|
+
brew services list
|
|
77
|
+
|
|
78
|
+
# 重启
|
|
79
|
+
brew services restart gssh
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### 使用 PM2(跨平台)
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
# 安装 PM2
|
|
86
|
+
npm install -g pm2
|
|
87
|
+
|
|
88
|
+
# 启动 daemon
|
|
89
|
+
pm2 start gssh-daemon.sh --name gssh
|
|
90
|
+
|
|
91
|
+
# 保存并设置开机自启
|
|
92
|
+
pm2 save
|
|
93
|
+
pm2 startup
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## 使用方法
|
|
97
|
+
|
|
98
|
+
### 连接 SSH(密码认证)
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
gssh connect -u admin1 -h 139.196.175.163 -p 7080 -P 1234
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### 连接 SSH(密钥认证)
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
gssh connect -u admin1 -h example.com -i ~/.ssh/id_rsa
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### 执行命令
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
# 使用默认 session
|
|
114
|
+
gssh exec "ls -la"
|
|
115
|
+
|
|
116
|
+
# 指定 session
|
|
117
|
+
gssh exec -s <session_id> "pwd"
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### 端口转发
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
# 本地端口转发:本地 8080 -> 远程 80
|
|
124
|
+
gssh forward -l 8080 -r 80
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Session 管理
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
# 列出所有 session
|
|
131
|
+
gssh list
|
|
132
|
+
|
|
133
|
+
# 切换默认 session
|
|
134
|
+
gssh use <session_id>
|
|
135
|
+
|
|
136
|
+
# 断开 session
|
|
137
|
+
gssh disconnect -s <session_id>
|
|
138
|
+
|
|
139
|
+
# 重连 session
|
|
140
|
+
gssh reconnect -s <session_id>
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## 命令行选项
|
|
144
|
+
|
|
145
|
+
| 选项 | 说明 |
|
|
146
|
+
|------|------|
|
|
147
|
+
| `-socket path` | Unix socket 路径(默认:/tmp/gssh.sock) |
|
|
148
|
+
| `-s session_id` | Session ID |
|
|
149
|
+
| `-u user` | 用户名 |
|
|
150
|
+
| `-h host` | 主机地址 |
|
|
151
|
+
| `-p port` | 端口(默认:22) |
|
|
152
|
+
| `-P password` | 密码 |
|
|
153
|
+
| `-i key_path` | SSH 密钥路径 |
|
|
154
|
+
| `-l local` | 本地端口 |
|
|
155
|
+
| `-r remote` | 远程端口 |
|
|
156
|
+
| `-R` | 远程端口转发 |
|
|
157
|
+
|
|
158
|
+
## 开发
|
|
159
|
+
|
|
160
|
+
### 运行测试
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
go test ./...
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### 代码结构
|
|
167
|
+
|
|
168
|
+
- `cmd/daemon/` - daemon 主程序
|
|
169
|
+
- `cmd/gssh/` - CLI 主程序
|
|
170
|
+
- `internal/client/` - SSH 客户端封装
|
|
171
|
+
- `internal/session/` - Session 管理器
|
|
172
|
+
- `internal/portforward/` - 端口转发
|
|
173
|
+
- `internal/protocol/` - 协议类型定义
|
|
174
|
+
- `pkg/rpc/` - RPC 处理器
|
|
175
|
+
- `homebrew/` - Homebrew 安装文件
|
package/bin/daemon
ADDED
|
Binary file
|
package/bin/gssh
ADDED
|
Binary file
|
package/bin/gssh-daemon
ADDED
|
Binary file
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"flag"
|
|
5
|
+
"log"
|
|
6
|
+
"os"
|
|
7
|
+
"os/signal"
|
|
8
|
+
"syscall"
|
|
9
|
+
|
|
10
|
+
"gssh/internal/session"
|
|
11
|
+
"gssh/pkg/rpc"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
const (
|
|
15
|
+
defaultSocketPath = "/tmp/gssh.sock"
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
func main() {
|
|
19
|
+
socketPath := flag.String("socket", defaultSocketPath, "Unix socket path")
|
|
20
|
+
flag.Parse()
|
|
21
|
+
|
|
22
|
+
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
|
23
|
+
log.SetOutput(os.Stderr)
|
|
24
|
+
|
|
25
|
+
log.Printf("Starting gssh daemon on %s", *socketPath)
|
|
26
|
+
|
|
27
|
+
// Create session manager
|
|
28
|
+
manager := session.NewManager()
|
|
29
|
+
|
|
30
|
+
// Start RPC server
|
|
31
|
+
go func() {
|
|
32
|
+
if err := rpc.ServeUnixSocket(manager, *socketPath); err != nil {
|
|
33
|
+
log.Printf("RPC server error: %v", err)
|
|
34
|
+
os.Exit(1)
|
|
35
|
+
}
|
|
36
|
+
}()
|
|
37
|
+
|
|
38
|
+
// Wait for interrupt signal
|
|
39
|
+
sigCh := make(chan os.Signal, 1)
|
|
40
|
+
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
|
41
|
+
<-sigCh
|
|
42
|
+
|
|
43
|
+
log.Println("Shutting down...")
|
|
44
|
+
os.Remove(*socketPath)
|
|
45
|
+
}
|
package/cmd/gssh/main.go
ADDED
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bufio"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
"flag"
|
|
7
|
+
"fmt"
|
|
8
|
+
"net"
|
|
9
|
+
"os"
|
|
10
|
+
"strings"
|
|
11
|
+
|
|
12
|
+
"gssh/internal/protocol"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
const (
|
|
16
|
+
defaultSocketPath = "/tmp/gssh.sock"
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
var (
|
|
20
|
+
socketPath = flag.String("socket", defaultSocketPath, "Unix socket path")
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
func main() {
|
|
24
|
+
flag.Parse()
|
|
25
|
+
|
|
26
|
+
if flag.NArg() < 1 {
|
|
27
|
+
printUsage()
|
|
28
|
+
os.Exit(1)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
cmd := flag.Arg(0)
|
|
32
|
+
|
|
33
|
+
var err error
|
|
34
|
+
switch cmd {
|
|
35
|
+
case "connect":
|
|
36
|
+
err = handleConnect()
|
|
37
|
+
case "disconnect":
|
|
38
|
+
err = handleDisconnect()
|
|
39
|
+
case "reconnect":
|
|
40
|
+
err = handleReconnect()
|
|
41
|
+
case "exec":
|
|
42
|
+
err = handleExec()
|
|
43
|
+
case "list", "ls":
|
|
44
|
+
err = handleList()
|
|
45
|
+
case "use":
|
|
46
|
+
err = handleUse()
|
|
47
|
+
case "forward":
|
|
48
|
+
err = handleForward()
|
|
49
|
+
case "forwards":
|
|
50
|
+
err = handleForwards()
|
|
51
|
+
case "forward-close":
|
|
52
|
+
err = handleForwardClose()
|
|
53
|
+
case "help", "-h", "--help":
|
|
54
|
+
printUsage()
|
|
55
|
+
default:
|
|
56
|
+
fmt.Fprintf(os.Stderr, "Unknown command: %s\n", cmd)
|
|
57
|
+
printUsage()
|
|
58
|
+
os.Exit(1)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if err != nil {
|
|
62
|
+
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
|
63
|
+
os.Exit(1)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
func sendRequest(method string, params interface{}) ([]byte, error) {
|
|
68
|
+
conn, err := net.Dial("unix", *socketPath)
|
|
69
|
+
if err != nil {
|
|
70
|
+
return nil, fmt.Errorf("failed to connect to daemon: %w", err)
|
|
71
|
+
}
|
|
72
|
+
defer conn.Close()
|
|
73
|
+
|
|
74
|
+
paramsJSON, err := json.Marshal(params)
|
|
75
|
+
if err != nil {
|
|
76
|
+
return nil, fmt.Errorf("failed to marshal params: %w", err)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
req := protocol.Request{
|
|
80
|
+
JSONRPC: "2.0",
|
|
81
|
+
Method: method,
|
|
82
|
+
Params: paramsJSON,
|
|
83
|
+
ID: 1,
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
reqData, err := json.Marshal(req)
|
|
87
|
+
if err != nil {
|
|
88
|
+
return nil, fmt.Errorf("failed to marshal request: %w", err)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Add newline to signal end of request
|
|
92
|
+
reqData = append(reqData, '\n')
|
|
93
|
+
|
|
94
|
+
_, err = conn.Write(reqData)
|
|
95
|
+
if err != nil {
|
|
96
|
+
return nil, fmt.Errorf("failed to send request: %w", err)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Read line by line
|
|
100
|
+
reader := bufio.NewReader(conn)
|
|
101
|
+
line, err := reader.ReadBytes('\n')
|
|
102
|
+
if err != nil {
|
|
103
|
+
return nil, fmt.Errorf("failed to read response: %w", err)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Remove trailing newline
|
|
107
|
+
line = line[:len(line)-1]
|
|
108
|
+
|
|
109
|
+
var resp protocol.Response
|
|
110
|
+
if err := json.Unmarshal(line, &resp); err != nil {
|
|
111
|
+
return nil, fmt.Errorf("failed to parse response: %w", err)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if resp.Error != nil {
|
|
115
|
+
return nil, fmt.Errorf("RPC error: %s", resp.Error.Message)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
result, err := json.Marshal(resp.Result)
|
|
119
|
+
if err != nil {
|
|
120
|
+
return nil, fmt.Errorf("failed to marshal result: %w", err)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return result, nil
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
func handleConnect() error {
|
|
127
|
+
user := flag.String("u", "", "Username")
|
|
128
|
+
host := flag.String("h", "", "Host")
|
|
129
|
+
port := flag.Int("p", 22, "Port")
|
|
130
|
+
password := flag.String("P", "", "Password")
|
|
131
|
+
keyPath := flag.String("i", "", "SSH key path")
|
|
132
|
+
|
|
133
|
+
flag.CommandLine.Parse(flag.Args()[1:])
|
|
134
|
+
|
|
135
|
+
if *user == "" || *host == "" {
|
|
136
|
+
return fmt.Errorf("user and host are required")
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
params := protocol.ConnectParams{
|
|
140
|
+
User: *user,
|
|
141
|
+
Host: *host,
|
|
142
|
+
Port: *port,
|
|
143
|
+
Password: *password,
|
|
144
|
+
KeyPath: *keyPath,
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
result, err := sendRequest("connect", params)
|
|
148
|
+
if err != nil {
|
|
149
|
+
return err
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
var session protocol.Session
|
|
153
|
+
if err := json.Unmarshal(result, &session); err != nil {
|
|
154
|
+
return fmt.Errorf("failed to parse result: %w", err)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
fmt.Printf("Connected: %s@%s (ID: %s)\n", session.User, session.Host, session.ID)
|
|
158
|
+
return nil
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
func handleDisconnect() error {
|
|
162
|
+
sessionID := flag.String("s", "", "Session ID")
|
|
163
|
+
flag.CommandLine.Parse(flag.Args()[1:])
|
|
164
|
+
|
|
165
|
+
params := protocol.DisconnectParams{
|
|
166
|
+
SessionID: *sessionID,
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
_, err := sendRequest("disconnect", params)
|
|
170
|
+
if err != nil {
|
|
171
|
+
return err
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
fmt.Println("Disconnected")
|
|
175
|
+
return nil
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
func handleReconnect() error {
|
|
179
|
+
sessionID := flag.String("s", "", "Session ID")
|
|
180
|
+
flag.CommandLine.Parse(flag.Args()[1:])
|
|
181
|
+
|
|
182
|
+
params := protocol.ReconnectParams{
|
|
183
|
+
SessionID: *sessionID,
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
result, err := sendRequest("reconnect", params)
|
|
187
|
+
if err != nil {
|
|
188
|
+
return err
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
var session protocol.Session
|
|
192
|
+
if err := json.Unmarshal(result, &session); err != nil {
|
|
193
|
+
return fmt.Errorf("failed to parse result: %w", err)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
fmt.Printf("Reconnected: %s@%s (ID: %s)\n", session.User, session.Host, session.ID)
|
|
197
|
+
return nil
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
func handleExec() error {
|
|
201
|
+
// Get remaining args after "exec"
|
|
202
|
+
args := flag.Args()[1:]
|
|
203
|
+
|
|
204
|
+
sessionID := flag.String("s", "", "Session ID")
|
|
205
|
+
flag.CommandLine.Parse(args)
|
|
206
|
+
|
|
207
|
+
if flag.NArg() < 1 {
|
|
208
|
+
return fmt.Errorf("command required")
|
|
209
|
+
}
|
|
210
|
+
command := strings.Join(flag.Args(), " ")
|
|
211
|
+
|
|
212
|
+
params := protocol.ExecParams{
|
|
213
|
+
SessionID: *sessionID,
|
|
214
|
+
Command: command,
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
result, err := sendRequest("exec", params)
|
|
218
|
+
if err != nil {
|
|
219
|
+
return err
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
var execResult protocol.ExecResult
|
|
223
|
+
if err := json.Unmarshal(result, &execResult); err != nil {
|
|
224
|
+
return fmt.Errorf("failed to parse result: %w", err)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
fmt.Print(execResult.Stdout)
|
|
228
|
+
if execResult.Stderr != "" {
|
|
229
|
+
fmt.Fprintf(os.Stderr, "%s", execResult.Stderr)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if execResult.ExitCode != 0 {
|
|
233
|
+
os.Exit(execResult.ExitCode)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return nil
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
func handleList() error {
|
|
240
|
+
result, err := sendRequest("list", nil)
|
|
241
|
+
if err != nil {
|
|
242
|
+
return err
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
var sessions []*protocol.Session
|
|
246
|
+
if err := json.Unmarshal(result, &sessions); err != nil {
|
|
247
|
+
return fmt.Errorf("failed to parse result: %w", err)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if len(sessions) == 0 {
|
|
251
|
+
fmt.Println("No sessions")
|
|
252
|
+
return nil
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
fmt.Println("ID HOST USER STATUS")
|
|
256
|
+
for _, s := range sessions {
|
|
257
|
+
fmt.Printf("%-38s %-17s %-7s %s\n", s.ID, s.Host, s.User, s.Status)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return nil
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
func handleUse() error {
|
|
264
|
+
flag.CommandLine.Parse(flag.Args()[1:])
|
|
265
|
+
|
|
266
|
+
if flag.NArg() < 1 {
|
|
267
|
+
return fmt.Errorf("session ID required")
|
|
268
|
+
}
|
|
269
|
+
sessionIDStr := flag.Arg(0)
|
|
270
|
+
|
|
271
|
+
params := protocol.UseParams{
|
|
272
|
+
SessionID: sessionIDStr,
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
_, err := sendRequest("use", params)
|
|
276
|
+
if err != nil {
|
|
277
|
+
return err
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
fmt.Printf("Using session: %s\n", sessionIDStr)
|
|
281
|
+
return nil
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
func handleForward() error {
|
|
285
|
+
sessionID := flag.String("s", "", "Session ID")
|
|
286
|
+
local := flag.Int("l", 0, "Local port")
|
|
287
|
+
remote := flag.Int("r", 0, "Remote port")
|
|
288
|
+
isRemote := flag.Bool("R", false, "Remote port forward")
|
|
289
|
+
|
|
290
|
+
flag.CommandLine.Parse(flag.Args()[1:])
|
|
291
|
+
|
|
292
|
+
if *local == 0 || *remote == 0 {
|
|
293
|
+
return fmt.Errorf("local and remote ports are required")
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
forwardType := "local"
|
|
297
|
+
if *isRemote {
|
|
298
|
+
forwardType = "remote"
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
params := protocol.ForwardParams{
|
|
302
|
+
SessionID: *sessionID,
|
|
303
|
+
Type: forwardType,
|
|
304
|
+
Local: *local,
|
|
305
|
+
Remote: *remote,
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
result, err := sendRequest("forward", params)
|
|
309
|
+
if err != nil {
|
|
310
|
+
return err
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
var forward protocol.Forward
|
|
314
|
+
if err := json.Unmarshal(result, &forward); err != nil {
|
|
315
|
+
return fmt.Errorf("failed to parse result: %w", err)
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
fmt.Printf("Forward started: %s %d:%d (ID: %s)\n", forward.Type, forward.Local, forward.Remote, forward.ID)
|
|
319
|
+
return nil
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
func handleForwards() error {
|
|
323
|
+
result, err := sendRequest("forwards", nil)
|
|
324
|
+
if err != nil {
|
|
325
|
+
return err
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
var forwards []*protocol.Forward
|
|
329
|
+
if err := json.Unmarshal(result, &forwards); err != nil {
|
|
330
|
+
return fmt.Errorf("failed to parse result: %w", err)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if len(forwards) == 0 {
|
|
334
|
+
fmt.Println("No forwards")
|
|
335
|
+
return nil
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
fmt.Println("ID LOCAL REMOTE TYPE")
|
|
339
|
+
for _, f := range forwards {
|
|
340
|
+
fmt.Printf("%-5s %-7d %-8d %s\n", f.ID[:8], f.Local, f.Remote, f.Type)
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return nil
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
func handleForwardClose() error {
|
|
347
|
+
flag.CommandLine.Parse(flag.Args()[1:])
|
|
348
|
+
|
|
349
|
+
if flag.NArg() < 1 {
|
|
350
|
+
return fmt.Errorf("forward ID required")
|
|
351
|
+
}
|
|
352
|
+
forwardID := flag.Arg(0)
|
|
353
|
+
|
|
354
|
+
params := protocol.ForwardCloseParams{
|
|
355
|
+
ForwardID: forwardID,
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
_, err := sendRequest("forward_close", params)
|
|
359
|
+
if err != nil {
|
|
360
|
+
return err
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
fmt.Printf("Forward %s closed\n", forwardID)
|
|
364
|
+
return nil
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
func printUsage() {
|
|
368
|
+
fmt.Println(`gssh - SSH Session Manager for Agents (Stateless)
|
|
369
|
+
|
|
370
|
+
Usage:
|
|
371
|
+
gssh connect -u user -h host [-p port] [-i key_path] [-P password]
|
|
372
|
+
gssh disconnect [-s session_id]
|
|
373
|
+
gssh reconnect [-s session_id]
|
|
374
|
+
gssh exec [-s session_id] "command"
|
|
375
|
+
gssh list
|
|
376
|
+
gssh use <session_id>
|
|
377
|
+
gssh forward [-s session_id] -l local_port -r remote_port
|
|
378
|
+
gssh forwards
|
|
379
|
+
gssh forward-close <forward_id>
|
|
380
|
+
|
|
381
|
+
Note: For sudo commands, use key-based authentication or configure passwordless sudo.
|
|
382
|
+
|
|
383
|
+
Options:
|
|
384
|
+
-socket path Unix socket path (default: /tmp/gssh.sock)
|
|
385
|
+
-s session_id Session ID
|
|
386
|
+
-u user Username
|
|
387
|
+
-h host Host
|
|
388
|
+
-p port Port (default: 22)
|
|
389
|
+
-P password Password
|
|
390
|
+
-i key_path SSH key path
|
|
391
|
+
-l local Local port
|
|
392
|
+
-r remote Remote port
|
|
393
|
+
-R Remote port forward`)
|
|
394
|
+
}
|
package/go.mod
ADDED
package/go.sum
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
|
|
2
|
+
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
|
3
|
+
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
|
|
4
|
+
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
|
5
|
+
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
|
|
6
|
+
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|
7
|
+
golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE=
|
|
8
|
+
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
|
|
Binary file
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
+
<plist version="1.0">
|
|
4
|
+
<dict>
|
|
5
|
+
<key>Label</key>
|
|
6
|
+
<string>com.gssh.daemon</string>
|
|
7
|
+
<key>ProgramArguments</key>
|
|
8
|
+
<array>
|
|
9
|
+
<string>/usr/local/bin/gssh-daemon</string>
|
|
10
|
+
<string>-socket</string>
|
|
11
|
+
<string>/tmp/gssh.sock</string>
|
|
12
|
+
</array>
|
|
13
|
+
<key>RunAtLoad</key>
|
|
14
|
+
<true/>
|
|
15
|
+
<key>KeepAlive</key>
|
|
16
|
+
<true/>
|
|
17
|
+
<key>StandardOutPath</key>
|
|
18
|
+
<string>/var/log/gssh.log</string>
|
|
19
|
+
<key>StandardErrorPath</key>
|
|
20
|
+
<string>/var/log/gssh.error.log</string>
|
|
21
|
+
</dict>
|
|
22
|
+
</plist>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# gssh service installation script
|
|
3
|
+
|
|
4
|
+
set -e
|
|
5
|
+
|
|
6
|
+
# Detect architecture
|
|
7
|
+
ARCH=$(uname -m)
|
|
8
|
+
if [ "$ARCH" = "arm64" ]; then
|
|
9
|
+
PREFIX="/opt/homebrew"
|
|
10
|
+
else
|
|
11
|
+
PREFIX="/usr/local"
|
|
12
|
+
fi
|
|
13
|
+
|
|
14
|
+
DAEMON_BIN="$PREFIX/bin/gssh-daemon"
|
|
15
|
+
CLI_BIN="$PREFIX/bin/gssh"
|
|
16
|
+
|
|
17
|
+
echo "Installing gssh to $PREFIX/bin/..."
|
|
18
|
+
|
|
19
|
+
# Copy binaries
|
|
20
|
+
cp bin/daemon "$DAEMON_BIN"
|
|
21
|
+
cp bin/gssh "$CLI_BIN"
|
|
22
|
+
|
|
23
|
+
chmod +x "$DAEMON_BIN" "$CLI_BIN"
|
|
24
|
+
|
|
25
|
+
# Install launchd plist
|
|
26
|
+
PLIST_DIR="$HOME/Library/LaunchAgents"
|
|
27
|
+
mkdir -p "$PLIST_DIR"
|
|
28
|
+
cp homebrew/gssh.plist "$PLIST_DIR/"
|
|
29
|
+
|
|
30
|
+
echo "Loading gssh service..."
|
|
31
|
+
launchctl load "$PLIST_DIR/gssh.plist"
|
|
32
|
+
|
|
33
|
+
echo "gssh installed successfully!"
|
|
34
|
+
echo ""
|
|
35
|
+
echo "Usage:"
|
|
36
|
+
echo " Start: launchctl start com.gssh.daemon"
|
|
37
|
+
echo " Stop: launchctl stop com.gssh.daemon"
|
|
38
|
+
echo " Status: launchctl list | grep gssh"
|
|
39
|
+
echo ""
|
|
40
|
+
echo "Or use Homebrew services:"
|
|
41
|
+
echo " brew services start gssh"
|
|
42
|
+
echo " brew services stop gssh"
|
|
43
|
+
echo " brew services list"
|