go-socks5-ssh-proxy/vendor/github.com/cloudfoundry/socks5-proxy/socks5_proxy.go

176 lines
3.3 KiB
Go

package proxy
import (
"errors"
"fmt"
"net"
"strconv"
"sync"
"time"
socks5 "github.com/cloudfoundry/go-socks5"
"log"
"golang.org/x/crypto/ssh"
"golang.org/x/net/context"
)
var netListen = net.Listen
type hostKey interface {
Get(username, privateKey, serverURL string) (ssh.PublicKey, error)
}
type DialFunc func(network, address string) (net.Conn, error)
type Socks5Proxy struct {
hostKey hostKey
port int
started bool
keepAliveInterval time.Duration
logger *log.Logger
mtx sync.Mutex
}
func NewSocks5Proxy(hostKey hostKey, logger *log.Logger, keepAliveInterval time.Duration) *Socks5Proxy {
return &Socks5Proxy{
hostKey: hostKey,
started: false,
logger: logger,
keepAliveInterval: keepAliveInterval,
}
}
func (s *Socks5Proxy) SetListenPort(port int) {
s.port = port
}
func (s *Socks5Proxy) Start(username, key, url string) error {
if s.isStarted() {
return nil
}
dialer, err := s.Dialer(username, key, url)
if err != nil {
return err
}
err = s.StartWithDialer(dialer)
if err != nil {
return err
}
return nil
}
// thread safety
func (s *Socks5Proxy) isStarted() bool {
s.mtx.Lock()
defer s.mtx.Unlock()
return s.started
}
func (s *Socks5Proxy) Dialer(username, key, url string) (DialFunc, error) {
if username == "" {
username = "jumpbox"
}
signer, err := ssh.ParsePrivateKey([]byte(key))
if err != nil {
return nil, fmt.Errorf("parse private key: %s", err)
}
hostKey, err := s.hostKey.Get(username, key, url)
if err != nil {
return nil, fmt.Errorf("get host key: %s", err)
}
clientConfig := NewSSHClientConfig(username, ssh.FixedHostKey(hostKey), ssh.PublicKeys(signer))
conn, err := ssh.Dial("tcp", url, clientConfig)
if err != nil {
return nil, fmt.Errorf("ssh dial: %s", err)
}
errChan := make(chan error)
go func(cl *ssh.Client, errChan chan error) {
t := time.NewTicker(s.keepAliveInterval)
for {
select {
case <-t.C:
_, _, err := cl.SendRequest("bosh-cli-keep-alive@bosh.io", true, nil)
if err != nil {
errChan <- err
}
}
}
}(conn, errChan)
go func(errChan chan error) {
for {
select {
case err := <-errChan:
s.logger.Printf("error sending ssh keep-alive: %s", err)
}
}
}(errChan)
return conn.Dial, nil
}
func (s *Socks5Proxy) StartWithDialer(dialer DialFunc) error {
conf := &socks5.Config{
Dial: func(ctx context.Context, network, addr string) (net.Conn, error) {
return dialer(network, addr)
},
Logger: s.logger,
}
server, err := socks5.New(conf)
if err != nil {
return fmt.Errorf("new socks5 server: %s", err) // not tested
}
s.mtx.Lock()
defer s.mtx.Unlock()
if s.port == 0 {
s.port, err = openPort()
if err != nil {
return fmt.Errorf("open port: %s", err)
}
}
go func() {
server.ListenAndServe("tcp", fmt.Sprintf("127.0.0.1:%d", s.port))
}()
s.started = true
return nil
}
func (s *Socks5Proxy) Addr() (string, error) {
s.mtx.Lock()
defer s.mtx.Unlock()
if s.port == 0 {
return "", errors.New("socks5 proxy is not running")
}
return fmt.Sprintf("127.0.0.1:%d", s.port), nil
}
func openPort() (int, error) {
l, err := netListen("tcp", "localhost:0")
if err != nil {
return 0, err
}
defer l.Close()
_, port, err := net.SplitHostPort(l.Addr().String())
if err != nil {
return 0, err
}
return strconv.Atoi(port)
}