Skip to content

Commit c63d502

Browse files
committed
First commit
1 parent c5aa1dc commit c63d502

File tree

8 files changed

+581
-0
lines changed

8 files changed

+581
-0
lines changed

Dockerfile

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
FROM golang:alpine AS builder
2+
WORKDIR /go/src/github.com/wzshiming/sshproxy/
3+
COPY . .
4+
ENV CGO_ENABLED=0
5+
RUN go install ./cmd/sshproxy
6+
7+
FROM alpine
8+
EXPOSE 8080
9+
COPY --from=builder /go/bin/sshproxy /usr/local/bin/
10+
ENTRYPOINT [ "/usr/local/bin/sshproxy" ]

client.go

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
package sshproxy
2+
3+
import (
4+
"context"
5+
"io/ioutil"
6+
"net"
7+
"net/url"
8+
"os"
9+
"path/filepath"
10+
"strconv"
11+
"strings"
12+
"sync"
13+
14+
"golang.org/x/crypto/ssh"
15+
)
16+
17+
// NewDialer returns a new Dialer that dials through the provided
18+
// proxy server's network and address.
19+
func NewDialer(addr string) (*Dialer, error) {
20+
host, config, err := clientConfig(addr)
21+
if err != nil {
22+
return nil, err
23+
}
24+
return &Dialer{
25+
host: host,
26+
config: config,
27+
}, nil
28+
}
29+
30+
func clientConfig(addr string) (host string, config *ssh.ClientConfig, err error) {
31+
ur, err := url.Parse(addr)
32+
if err != nil {
33+
return "", nil, err
34+
}
35+
36+
user := ""
37+
pwd := ""
38+
isPwd := false
39+
if ur.User != nil {
40+
user = ur.User.Username()
41+
pwd, isPwd = ur.User.Password()
42+
}
43+
44+
config = &ssh.ClientConfig{
45+
User: user,
46+
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
47+
}
48+
49+
if isPwd {
50+
config.Auth = append(config.Auth, ssh.Password(pwd))
51+
}
52+
53+
identityFiles := ur.Query()["identity_file"]
54+
for _, ident := range identityFiles {
55+
if ident == "" {
56+
continue
57+
}
58+
if strings.HasPrefix(ident, "~") {
59+
home, err := os.UserHomeDir()
60+
if err == nil {
61+
ident = filepath.Join(home, ident[1:])
62+
}
63+
}
64+
65+
file, err := ioutil.ReadFile(ident)
66+
if err != nil {
67+
return "", nil, err
68+
}
69+
signer, err := ssh.ParsePrivateKey(file)
70+
if err != nil {
71+
return "", nil, err
72+
}
73+
config.Auth = append(config.Auth, ssh.PublicKeys(signer))
74+
}
75+
76+
host = ur.Hostname()
77+
port := ur.Port()
78+
if port == "" {
79+
port = "22"
80+
}
81+
host = net.JoinHostPort(host, port)
82+
return host, config, nil
83+
}
84+
85+
type Dialer struct {
86+
mut sync.Mutex
87+
localAddr net.Addr
88+
// ProxyDial specifies the optional dial function for
89+
// establishing the transport connection.
90+
ProxyDial func(context.Context, string, string) (net.Conn, error)
91+
sshCli *ssh.Client
92+
host string
93+
config *ssh.ClientConfig
94+
}
95+
96+
func (c *Dialer) reset() {
97+
c.mut.Lock()
98+
defer c.mut.Unlock()
99+
if c.sshCli == nil {
100+
return
101+
}
102+
c.sshCli.Close()
103+
c.sshCli = nil
104+
}
105+
106+
func (d *Dialer) proxyDial(ctx context.Context, network, address string) (net.Conn, error) {
107+
proxyDial := d.ProxyDial
108+
if proxyDial == nil {
109+
var dialer net.Dialer
110+
proxyDial = dialer.DialContext
111+
}
112+
return proxyDial(ctx, network, address)
113+
}
114+
115+
func (c *Dialer) getCli(ctx context.Context) (*ssh.Client, error) {
116+
c.mut.Lock()
117+
defer c.mut.Unlock()
118+
cli := c.sshCli
119+
if cli != nil {
120+
return cli, nil
121+
}
122+
conn, err := c.proxyDial(ctx, "tcp", c.host)
123+
if err != nil {
124+
return nil, err
125+
}
126+
127+
con, chans, reqs, err := ssh.NewClientConn(conn, c.host, c.config)
128+
if err != nil {
129+
return nil, err
130+
}
131+
cli = ssh.NewClient(con, chans, reqs)
132+
c.sshCli = cli
133+
return cli, nil
134+
}
135+
136+
func (c *Dialer) CommandDialContext(ctx context.Context, name string, args ...string) (net.Conn, error) {
137+
cmd := make([]string, 0, len(args)+1)
138+
cmd = append(cmd, name)
139+
for _, arg := range args {
140+
cmd = append(cmd, strconv.Quote(arg))
141+
}
142+
return c.commandDialContext(ctx, strings.Join(cmd, " "), 1)
143+
}
144+
145+
func (c *Dialer) commandDialContext(ctx context.Context, cmd string, retry int) (net.Conn, error) {
146+
cli, err := c.getCli(ctx)
147+
if err != nil {
148+
return nil, err
149+
}
150+
sess, err := cli.NewSession()
151+
if err != nil {
152+
return nil, err
153+
}
154+
conn1, conn2 := net.Pipe()
155+
sess.Stdin = conn1
156+
sess.Stdout = conn1
157+
sess.Stderr = os.Stderr
158+
err = sess.Start(cmd)
159+
if err != nil {
160+
if retry != 0 {
161+
c.reset()
162+
return c.commandDialContext(ctx, cmd, retry-1)
163+
}
164+
return nil, err
165+
}
166+
ctx, cancel := context.WithCancel(ctx)
167+
go func() {
168+
sess.Wait()
169+
cancel()
170+
}()
171+
go func() {
172+
<-ctx.Done()
173+
174+
// openssh does not support the signal
175+
// command and will not signal remote processes. This may
176+
// be resolved in openssh 7.9 or higher. Please subscribe
177+
// to https://github.com/golang/go/issues/16597.
178+
sess.Signal(ssh.SIGKILL)
179+
sess.Close()
180+
conn1.Close()
181+
}()
182+
conn2 = connWithCloser(conn2, func() error {
183+
cancel()
184+
return nil
185+
})
186+
conn2 = connWithAddr(conn2, c.localAddr, newNetAddr("ssh-cmd", cmd))
187+
return conn2, nil
188+
}
189+
190+
func (c *Dialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
191+
return c.dialContext(ctx, network, address, 1)
192+
}
193+
194+
func (c *Dialer) dialContext(ctx context.Context, network, address string, retry int) (net.Conn, error) {
195+
cli, err := c.getCli(ctx)
196+
if err != nil {
197+
return nil, err
198+
}
199+
conn, err := cli.Dial(network, address)
200+
if err != nil {
201+
if retry != 0 {
202+
c.reset()
203+
return c.dialContext(ctx, network, address, retry-1)
204+
}
205+
return nil, err
206+
}
207+
return conn, nil
208+
}
209+
210+
func (c *Dialer) Listen(ctx context.Context, network, address string) (net.Listener, error) {
211+
host, port, err := net.SplitHostPort(address)
212+
if err != nil {
213+
return nil, err
214+
}
215+
if host == "" {
216+
address = net.JoinHostPort("0.0.0.0", port)
217+
}
218+
return c.listen(ctx, network, address, 1)
219+
}
220+
221+
func (c *Dialer) listen(ctx context.Context, network, address string, retry int) (net.Listener, error) {
222+
cli, err := c.getCli(ctx)
223+
if err != nil {
224+
return nil, err
225+
}
226+
listener, err := cli.Listen(network, address)
227+
if err != nil {
228+
if retry != 0 {
229+
c.reset()
230+
return c.listen(ctx, network, address, retry-1)
231+
}
232+
return nil, err
233+
}
234+
return listener, nil
235+
}

cmd/sshproxy/main.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package main
2+
3+
import (
4+
"flag"
5+
"fmt"
6+
"log"
7+
"os"
8+
9+
_ "github.com/wzshiming/sshproxy"
10+
11+
"github.com/wzshiming/sshd"
12+
"golang.org/x/crypto/ssh"
13+
)
14+
15+
var address string
16+
var username string
17+
var password string
18+
var authorized string
19+
var hostkey string
20+
21+
func init() {
22+
flag.StringVar(&address, "a", ":22", "listen on the address")
23+
flag.StringVar(&username, "u", "", "username")
24+
flag.StringVar(&password, "p", "", "password")
25+
flag.StringVar(&authorized, "f", "", "authorized file")
26+
flag.StringVar(&hostkey, "h", "", "hostkey file")
27+
flag.Parse()
28+
}
29+
30+
func main() {
31+
logger := log.New(os.Stderr, "[ssh proxy] ", log.LstdFlags)
32+
svc := sshd.NewServer()
33+
svc.Logger = logger
34+
if hostkey != "" {
35+
key, err := sshd.GetHostkey(hostkey)
36+
if err != nil {
37+
logger.Println(err)
38+
return
39+
}
40+
svc.ServerConfig.AddHostKey(key)
41+
} else {
42+
key, err := sshd.RandomHostkey()
43+
if err != nil {
44+
logger.Println(err)
45+
return
46+
}
47+
svc.ServerConfig.AddHostKey(key)
48+
}
49+
if username != "" {
50+
svc.ServerConfig.PasswordCallback = func(conn ssh.ConnMetadata, pwd []byte) (*ssh.Permissions, error) {
51+
if conn.User() == username && password == string(pwd) {
52+
return nil, nil
53+
}
54+
return nil, fmt.Errorf("denied")
55+
}
56+
}
57+
if authorized != "" {
58+
keys, err := sshd.GetAuthorizedFile(authorized)
59+
if err != nil {
60+
logger.Println(err)
61+
return
62+
}
63+
svc.ServerConfig.PublicKeyCallback = func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
64+
k := string(key.Marshal())
65+
if _, ok := keys[k]; ok {
66+
return nil, nil
67+
}
68+
return nil, fmt.Errorf("denied")
69+
}
70+
}
71+
if username == "" && authorized == "" {
72+
svc.ServerConfig.NoClientAuth = true
73+
}
74+
err := svc.ListenAndServe("tcp", address)
75+
if err != nil {
76+
logger.Println(err)
77+
}
78+
}

go.mod

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
module github.com/wzshiming/sshproxy
2+
3+
go 1.17
4+
5+
require (
6+
github.com/wzshiming/sshd v0.1.0
7+
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519
8+
)
9+
10+
require golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect

go.sum

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
2+
github.com/wzshiming/sshd v0.1.0 h1:qwayh3xmWl9rKnrPQKjKDfuIO8idFdMOYno8y1WVgcc=
3+
github.com/wzshiming/sshd v0.1.0/go.mod h1:1sQ9cDYmq9xyRQbGh8OB9186sJXLRW8qlPkiADMn5iU=
4+
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg=
5+
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
6+
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
7+
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
8+
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4=
9+
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
10+
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
11+
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
12+
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
13+
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

server.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package sshproxy
2+
3+
import (
4+
_ "github.com/wzshiming/sshd/directtcp"
5+
6+
"github.com/wzshiming/sshd"
7+
)
8+
9+
type Server = sshd.Server

0 commit comments

Comments
 (0)