Skip to content

Commit 034796f

Browse files
committed
feat: Redesign SSH tunnel with simplified API
- Removed legacy SSH client and SOCKS5 implementations - Created new simplified Tunnel API with context support - Added connection limits, timeouts, and buffer pooling - Improved error handling and graceful shutdown - Updated README with simplified documentation - Added comprehensive example with host key verification options This is a breaking change that replaces the old API with a much simpler and more efficient implementation.
1 parent 639e695 commit 034796f

File tree

9 files changed

+445
-403
lines changed

9 files changed

+445
-403
lines changed

README.md

Lines changed: 70 additions & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -3,166 +3,100 @@
33
This package enables users to:
44

55
- Establish a tunnel to a remote server via SSH
6-
- Use the remote server as a SOCKS5 proxy via SSH
76
- Verify SSH host keys for improved security
7+
- Graceful shutdown with context cancellation
8+
- Configurable connection limits and timeouts
9+
- Improved performance with buffered data transfer
10+
11+
## Usage
12+
13+
The package provides a simple API for creating SSH tunnels:
14+
15+
```go
16+
// Create tunnel configuration
17+
config := &sshts.TunnelConfig{
18+
User: "username",
19+
AuthMethods: []ssh.AuthMethod{
20+
ssh.PublicKeys(signer),
21+
},
22+
HostKeyCallback: ssh.InsecureIgnoreHostKey(), // Or use knownhosts for security
23+
}
24+
25+
// Create tunnel
26+
tunnel := sshts.NewTunnel(
27+
"localhost:8080", // Local address to listen on
28+
"localhost:80", // Remote address to forward to
29+
config,
30+
)
31+
32+
// Start tunnel with context
33+
ctx, cancel := context.WithCancel(context.Background())
34+
tunnelCancel, err := tunnel.Start(ctx, "ssh-server.example.com:22")
35+
if err != nil {
36+
log.Fatal("Failed to start tunnel:", err)
37+
}
38+
39+
// To stop the tunnel
40+
tunnelCancel()
41+
tunnel.Close()
42+
```
843

944
## Host Key Verification
1045

11-
The package now supports proper SSH host key verification, which is crucial for security. There are three ways to verify host keys:
46+
The package supports proper SSH host key verification for security:
1247

1348
1. **Insecure method (default)**: Uses `ssh.InsecureIgnoreHostKey()` - not recommended for production
1449
2. **Known hosts file**: Verifies host keys against a `known_hosts` file (recommended)
15-
3. **Custom callback**: Provide your own `ssh.HostKeyCallback` function
1650

1751
Example using known hosts verification:
1852
```go
19-
sshC, err := sshts.NewWithKnownHosts("username", "/path/to/private/key", "server:port", "/path/to/known_hosts")
53+
hostKeyCallback, err := knownhosts.New("/path/to/known_hosts")
54+
if err != nil {
55+
log.Fatal("Failed to load known_hosts file:", err)
56+
}
57+
58+
config := &sshts.TunnelConfig{
59+
User: "username",
60+
AuthMethods: []ssh.AuthMethod{
61+
ssh.PublicKeys(signer),
62+
},
63+
HostKeyCallback: hostKeyCallback, // Secure host key verification
64+
}
2065
```
2166

2267
To set up host key verification:
23-
2468
1. Create a `known_hosts` file (usually located at `~/.ssh/known_hosts`)
2569
2. Add the remote server's host key to this file
26-
3. Use `NewWithKnownHosts()` instead of `New()`
70+
3. Use `knownhosts.New()` to create a host key callback
2771

2872
You can manually add entries to known_hosts or use ssh-keyscan:
2973
```bash
3074
ssh-keyscan server.example.com >> ~/.ssh/known_hosts
3175
```
32-
Here's an example of how to use this package:
33-
```go
34-
package main
35-
36-
import (
37-
"fmt"
38-
"io/ioutil"
39-
"log"
40-
"net/http"
41-
"net/url"
42-
"os"
43-
"os/signal"
44-
"syscall"
45-
46-
"golang.org/x/crypto/ssh"
47-
"golang.org/x/crypto/ssh/knownhosts"
48-
"github.com/kslamph/sshts"
49-
)
50-
51-
func main() {
52-
tunnelexample()
53-
}
5476

55-
/* tunnel exmaple:
56-
there is a remote server 101.32.14.95 , we have ssh access to it;
57-
there is a postgres sql server running on it and listen on localhost:5432;
58-
and this 101.32.14.95:5432 is not accessible from outside.
59-
60-
we want to access the postgres server by localhost:15432
61-
62-
this setup increased the security of the postgres server, because it is not accessible from outside, and we can only access it by ssh tunneling.
63-
there are 3 advantages of this setup:
64-
using key to access ssh server is a proven safe way to access remote server
65-
ssh tunneling is encrypted, so the data is safe at transport layer
66-
we dont have to expose the postgres server interfaces other than localhost, so it is more secure
67-
*/
68-
69-
func tunnelexample() {
70-
//address and port of ssh server to connect to
71-
sshAddress := "101.32.14.95:22"
72-
73-
//local port to listen on
74-
localTunnel := "localhost:15432"
75-
76-
//server to tunnel to on remote, usually localhost but can be any address
77-
remoteTunnel := "localhost:5432"
78-
79-
// Method 1: Using the default insecure method (not recommended for production)
80-
// sshC, err := sshts.New("root", "/home/k/.ssh/id_rsa", sshAddress)
81-
82-
// Method 2: Using a custom host key callback
83-
// hostKeyCallback := ssh.FixedHostKey(theHostKey) // where theHostKey is of type ssh.PublicKey
84-
// sshC, err := sshts.NewWithHostKeyCallback("root", "/home/k/.ssh/id_rsa", sshAddress, hostKeyCallback)
85-
86-
// Method 3: Using known_hosts file verification (recommended)
87-
sshC, err := sshts.NewWithKnownHosts("root", "/home/k/.ssh/id_rsa", sshAddress, "/home/k/.ssh/known_hosts")
88-
if err != nil {
89-
log.Fatal(err)
90-
}
91-
err = sshC.Connect()
92-
if err != nil {
93-
log.Fatal(err)
94-
}
95-
defer sshC.Close()
96-
97-
go sshC.StartTunnel(localTunnel, remoteTunnel)
98-
fmt.Println("press ctrl+c to exit")
99-
ch := make(chan os.Signal)
100-
signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
101-
<-ch
102-
fmt.Println("exiting")
77+
## Features
10378

104-
}
79+
### Graceful Shutdown
80+
The package supports context cancellation for graceful shutdowns. When the connection is closed, all goroutines are properly cancelled and resources are cleaned up.
10581

106-
/* socks5 exmaple:
107-
there is a remote server 101.32.14.95 , we have ssh access to it, we want use it as a socks5 proxy
108-
and let the socks5 proxy listen on localhost:1080
109-
110-
this setup has some advantages over proxy:
111-
1. it is encrypted, so the data is safe at transport layer
112-
2. there is no proxy server for maintain and it is easy to setup
113-
*/
114-
115-
func socks5example() {
116-
//address and port of ssh server to connect to
117-
sshAddress := "18.162.151.198:22"
118-
119-
//address and listening port of socks5 server to start
120-
socks5Address := "localhost:1080"
121-
122-
//username, private key path, and address of ssh server to connect to
123-
// Using known_hosts file verification (recommended)
124-
sshC, err := sshts.NewWithKnownHosts("proxy", "/home/k/.ssh/proxy.key", sshAddress, "/home/k/.ssh/known_hosts")
125-
if err != nil {
126-
log.Fatal(err)
127-
}
128-
err = sshC.Connect()
129-
if err != nil {
130-
log.Fatal(err)
131-
}
132-
defer sshC.Close()
133-
134-
go sshC.StartSocks5Server(socks5Address)
135-
for sshC.GetStatus() < 2 {
136-
}
137-
138-
httpget()
139-
140-
fmt.Println("press ctrl+c to exit")
141-
ch := make(chan os.Signal)
142-
signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
143-
<-ch
144-
fmt.Println("exiting")
82+
### Connection Limits and Timeouts
83+
You can configure connection limits and timeouts to prevent resource exhaustion:
14584

85+
```go
86+
config := &sshts.TunnelConfig{
87+
// ... other config
88+
MaxConnections: 100,
89+
DialTimeout: 30 * time.Second,
90+
SSHTimeout: 30 * time.Second,
14691
}
92+
```
14793

148-
func httpget() {
149-
proxyURL, err := url.Parse("socks5://localhost:1080")
150-
if err != nil {
151-
log.Fatal(err)
152-
}
153-
transport := &http.Transport{
154-
Proxy: http.ProxyURL(proxyURL),
155-
}
156-
client := &http.Client{Transport: transport}
157-
resp, err := client.Get("http://ifconfig.me")
158-
if err != nil {
159-
log.Fatal(err)
160-
}
161-
defer resp.Body.Close()
162-
body, err := ioutil.ReadAll(resp.Body)
163-
if err != nil {
164-
log.Fatal(err)
165-
}
166-
fmt.Println(string(body))
94+
### Performance
95+
Data transfer uses `io.CopyBuffer` with a shared buffer pool for better performance:
96+
97+
```go
98+
config := &sshts.TunnelConfig{
99+
// ... other config
100+
BufferSize: 64 * 1024, // 64KB buffers
167101
}
168-
```
102+
```

example/insecure/main.go

Lines changed: 0 additions & 61 deletions
This file was deleted.

example/main.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"log"
7+
"os"
8+
"time"
9+
10+
"golang.org/x/crypto/ssh"
11+
"golang.org/x/crypto/ssh/knownhosts"
12+
13+
"github.com/kslamph/sshts"
14+
)
15+
16+
func main() {
17+
// Example of using the new simplified tunnel API with secure host key verification
18+
19+
// Read private key
20+
key, err := os.ReadFile("/path/to/private/key")
21+
if err != nil {
22+
log.Fatal("Failed to read private key:", err)
23+
}
24+
25+
// Parse private key
26+
signer, err := ssh.ParsePrivateKey(key)
27+
if err != nil {
28+
log.Fatal("Failed to parse private key:", err)
29+
}
30+
31+
// Load host key verification from known_hosts file
32+
hostKeyCallback, err := knownhosts.New("/path/to/known_hosts")
33+
if err != nil {
34+
log.Fatal("Failed to load known_hosts file:", err)
35+
}
36+
37+
// Create tunnel configuration
38+
config := &sshts.TunnelConfig{
39+
User: "username",
40+
AuthMethods: []ssh.AuthMethod{
41+
ssh.PublicKeys(signer),
42+
},
43+
HostKeyCallback: hostKeyCallback, // Secure host key verification
44+
// HostKeyCallback: ssh.InsecureIgnoreHostKey(), // INSECURE: Alternative to disable host key verification
45+
SSHTimeout: 30 * time.Second,
46+
DialTimeout: 10 * time.Second,
47+
MaxConnections: 50,
48+
BufferSize: 64 * 1024, // 64KB
49+
}
50+
51+
// Note: For production use, host key verification is strongly recommended
52+
// If you want to disable host key verification (NOT recommended), uncomment the line above
53+
// Warning: Disabling host key verification makes connections vulnerable to man-in-the-middle attacks
54+
55+
// Create tunnel - tunnel is NOT yet usable, no SSH connection established
56+
tunnel := sshts.NewTunnel(
57+
"localhost:8080", // Local address to listen on
58+
"localhost:80", // Remote address to forward to
59+
config,
60+
)
61+
62+
// Create context for the tunnel
63+
ctx, cancel := context.WithCancel(context.Background())
64+
defer cancel()
65+
66+
// Start tunnel - tunnel becomes usable after this call
67+
// Returns a cancel function to stop the tunnel and an error if startup fails
68+
tunnelCancel, err := tunnel.Start(ctx, "ssh-server.example.com:22")
69+
if err != nil {
70+
log.Fatal("Failed to start tunnel:", err)
71+
}
72+
73+
// TUNNEL IS NOW USABLE - connections will be forwarded through SSH tunnel
74+
fmt.Println("Secure tunnel started successfully")
75+
fmt.Println("Tunnel is now ready to forward connections")
76+
77+
// In a real application, you would typically:
78+
// 1. Run your application logic here
79+
// 2. Call tunnelCancel() and tunnel.Close() when shutting down
80+
81+
// For this example, we'll just sleep to simulate a running service
82+
// In practice, you'd handle graceful shutdown with signal handling or other mechanisms
83+
time.Sleep(10 * time.Second)
84+
85+
// Stop tunnel - after this call, tunnel is NO LONGER USABLE
86+
tunnelCancel()
87+
tunnel.Close()
88+
89+
// TUNNEL IS NOW UNUSABLE - all resources cleaned up
90+
fmt.Println("Tunnel stopped")
91+
}

0 commit comments

Comments
 (0)