mirror of
https://github.com/boringproxy/boringproxy.git
synced 2024-07-04 11:22:57 -05:00
This lets you use a proxy for connecting to the SSH server, which is useful on networks that block SSH/port 22. For example you can use the boringproxy tuntls command to create a proxy that will tunnel the client's SSH connections over TLS to the server. It's all very meta and forces at least double encryption, but it could be useful.
248 lines
5.3 KiB
Go
248 lines
5.3 KiB
Go
package boringproxy
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"crypto/x509"
|
|
"encoding/pem"
|
|
"errors"
|
|
"fmt"
|
|
"github.com/caddyserver/certmagic"
|
|
"golang.org/x/crypto/ssh"
|
|
"io/ioutil"
|
|
"log"
|
|
"os"
|
|
"os/user"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
type TunnelManager struct {
|
|
config *Config
|
|
db *Database
|
|
mutex *sync.Mutex
|
|
certConfig *certmagic.Config
|
|
user *user.User
|
|
}
|
|
|
|
func NewTunnelManager(config *Config, db *Database, certConfig *certmagic.Config) *TunnelManager {
|
|
|
|
user, err := user.Current()
|
|
if err != nil {
|
|
log.Fatalf("Unable to get current user: %v", err)
|
|
}
|
|
|
|
if config.autoCerts {
|
|
for domainName, tun := range db.GetTunnels() {
|
|
if tun.TlsTermination == "server" || tun.TlsTermination == "server-tls" {
|
|
err = certConfig.ManageSync(context.Background(), []string{domainName})
|
|
if err != nil {
|
|
log.Println("CertMagic error at startup")
|
|
log.Println(err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
mutex := &sync.Mutex{}
|
|
return &TunnelManager{config, db, mutex, certConfig, user}
|
|
}
|
|
|
|
func (m *TunnelManager) GetTunnels() map[string]Tunnel {
|
|
return m.db.GetTunnels()
|
|
}
|
|
|
|
func (m *TunnelManager) RequestCreateTunnel(tunReq Tunnel) (Tunnel, error) {
|
|
|
|
if tunReq.Domain == "" {
|
|
return Tunnel{}, errors.New("Domain required")
|
|
}
|
|
|
|
if tunReq.Owner == "" {
|
|
return Tunnel{}, errors.New("Owner required")
|
|
}
|
|
|
|
if tunReq.TlsTermination == "server" || tunReq.TlsTermination == "server-tls" {
|
|
if m.config.autoCerts {
|
|
err := m.certConfig.ManageSync(context.Background(), []string{tunReq.Domain})
|
|
if err != nil {
|
|
return Tunnel{}, errors.New("Failed to get cert")
|
|
}
|
|
}
|
|
}
|
|
|
|
m.mutex.Lock()
|
|
defer m.mutex.Unlock()
|
|
|
|
if tunReq.TunnelPort == 0 {
|
|
var err error
|
|
tunReq.TunnelPort, err = randomOpenPort()
|
|
if err != nil {
|
|
return Tunnel{}, err
|
|
}
|
|
}
|
|
|
|
for _, tun := range m.db.GetTunnels() {
|
|
if tunReq.Domain == tun.Domain {
|
|
return Tunnel{}, errors.New("Tunnel domain already in use")
|
|
}
|
|
|
|
if tunReq.TunnelPort == tun.TunnelPort {
|
|
return Tunnel{}, errors.New("Tunnel port already in use")
|
|
}
|
|
}
|
|
|
|
privKey, err := m.addToAuthorizedKeys(tunReq.Domain, tunReq.TunnelPort, tunReq.AllowExternalTcp)
|
|
if err != nil {
|
|
return Tunnel{}, err
|
|
}
|
|
|
|
tunReq.ServerPublicKey = ""
|
|
tunReq.Username = m.user.Username
|
|
tunReq.TunnelPrivateKey = privKey
|
|
|
|
m.db.SetTunnel(tunReq.Domain, tunReq)
|
|
|
|
return tunReq, nil
|
|
}
|
|
|
|
func (m *TunnelManager) DeleteTunnel(domain string) error {
|
|
m.mutex.Lock()
|
|
defer m.mutex.Unlock()
|
|
|
|
tunnel, exists := m.db.GetTunnel(domain)
|
|
if !exists {
|
|
return errors.New("Tunnel doesn't exist")
|
|
}
|
|
|
|
m.db.DeleteTunnel(domain)
|
|
|
|
authKeysPath := fmt.Sprintf("%s/.ssh/authorized_keys", m.user.HomeDir)
|
|
|
|
akBytes, err := ioutil.ReadFile(authKeysPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
akStr := string(akBytes)
|
|
|
|
lines := strings.Split(akStr, "\n")
|
|
|
|
tunnelId := fmt.Sprintf("boringproxy-%s-%d", domain, tunnel.TunnelPort)
|
|
|
|
outLines := []string{}
|
|
|
|
for _, line := range lines {
|
|
if strings.Contains(line, tunnelId) {
|
|
continue
|
|
}
|
|
|
|
outLines = append(outLines, line)
|
|
}
|
|
|
|
outStr := strings.Join(outLines, "\n")
|
|
|
|
err = ioutil.WriteFile(authKeysPath, []byte(outStr), 0600)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (m *TunnelManager) GetPort(domain string) (int, error) {
|
|
tunnel, exists := m.db.GetTunnel(domain)
|
|
|
|
if !exists {
|
|
return 0, errors.New("Doesn't exist")
|
|
}
|
|
|
|
return tunnel.TunnelPort, nil
|
|
}
|
|
|
|
func (m *TunnelManager) addToAuthorizedKeys(domain string, port int, allowExternalTcp bool) (string, error) {
|
|
|
|
authKeysPath := fmt.Sprintf("%s/.ssh/authorized_keys", m.user.HomeDir)
|
|
|
|
akFile, err := os.OpenFile(authKeysPath, os.O_RDWR|os.O_CREATE, 0600)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer akFile.Close()
|
|
|
|
akBytes, err := ioutil.ReadAll(akFile)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
akStr := string(akBytes)
|
|
|
|
var privKey string
|
|
var pubKey string
|
|
|
|
pubKey, privKey, err = MakeSSHKeyPair()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
pubKey = strings.TrimSpace(pubKey)
|
|
|
|
bindAddr := "127.0.0.1"
|
|
if allowExternalTcp {
|
|
bindAddr = "0.0.0.0"
|
|
}
|
|
|
|
options := fmt.Sprintf(`command="echo This key permits tunnels only",permitopen="fakehost:1",permitlisten="%s:%d"`, bindAddr, port)
|
|
|
|
tunnelId := fmt.Sprintf("boringproxy-%s-%d", domain, port)
|
|
|
|
newAk := fmt.Sprintf("%s%s %s %s\n", akStr, options, pubKey, tunnelId)
|
|
|
|
// Clear the file
|
|
err = akFile.Truncate(0)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
_, err = akFile.Seek(0, 0)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
_, err = akFile.Write([]byte(newAk))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return privKey, nil
|
|
}
|
|
|
|
// Adapted from https://stackoverflow.com/a/34347463/943814
|
|
// MakeSSHKeyPair make a pair of public and private keys for SSH access.
|
|
// Public key is encoded in the format for inclusion in an OpenSSH authorized_keys file.
|
|
// Private Key generated is PEM encoded
|
|
func MakeSSHKeyPair() (string, string, error) {
|
|
privateKey, err := rsa.GenerateKey(rand.Reader, 1024)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
|
|
// generate and write private key as PEM
|
|
var privKeyBuf strings.Builder
|
|
|
|
privateKeyPEM := &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)}
|
|
if err := pem.Encode(&privKeyBuf, privateKeyPEM); err != nil {
|
|
return "", "", err
|
|
}
|
|
|
|
// generate and write public key
|
|
pub, err := ssh.NewPublicKey(&privateKey.PublicKey)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
|
|
pubKey := string(ssh.MarshalAuthorizedKey(pub))
|
|
|
|
return pubKey, privKeyBuf.String(), nil
|
|
}
|