Files
boringproxy/tunnel_manager.go
Anders Pitman b795ee0bc9 Remove unused custom SSH key code
Was originally planning to allow people to upload public keys to
use for tunnels, but so far no one has asked for this and I
haven't needed it.
2021-12-21 13:14:01 -07:00

245 lines
5.2 KiB
Go

package boringproxy
import (
"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)
}
for domainName, tun := range db.GetTunnels() {
if tun.TlsTermination == "server" {
err = certConfig.ManageSync([]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" {
err := m.certConfig.ManageSync([]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.ServerAddress = m.db.GetAdminDomain()
tunReq.ServerPort = m.config.SshServerPort
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
}