176 lines
3.3 KiB
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)
|
|
}
|