857 lines
22 KiB
Go
857 lines
22 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/md5"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"math/rand/v2"
|
|
"net"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"git.terah.dev/imterah/hermes/backend/backendutil"
|
|
"git.terah.dev/imterah/hermes/backend/commonbackend"
|
|
"git.terah.dev/imterah/hermes/backend/sshappbackend/datacommands"
|
|
"git.terah.dev/imterah/hermes/backend/sshappbackend/gaslighter"
|
|
"git.terah.dev/imterah/hermes/backend/sshappbackend/local-code/porttranslation"
|
|
"github.com/charmbracelet/log"
|
|
"github.com/go-playground/validator/v10"
|
|
"github.com/pkg/sftp"
|
|
"golang.org/x/crypto/ssh"
|
|
)
|
|
|
|
type TCPProxy struct {
|
|
proxyInformation *commonbackend.AddProxy
|
|
connections map[uint16]net.Conn
|
|
}
|
|
|
|
type UDPProxy struct {
|
|
proxyInformation *commonbackend.AddProxy
|
|
portTranslation *porttranslation.PortTranslation
|
|
}
|
|
|
|
type SSHAppBackendData struct {
|
|
IP string `json:"ip" validate:"required"`
|
|
Port uint16 `json:"port" validate:"required"`
|
|
Username string `json:"username" validate:"required"`
|
|
PrivateKey string `json:"privateKey" validate:"required"`
|
|
ListenOnIPs []string `json:"listenOnIPs"`
|
|
}
|
|
|
|
type SSHAppBackend struct {
|
|
config *SSHAppBackendData
|
|
conn *ssh.Client
|
|
listener net.Listener
|
|
currentSock net.Conn
|
|
|
|
tcpProxies map[uint16]*TCPProxy
|
|
udpProxies map[uint16]*UDPProxy
|
|
|
|
// globalNonCriticalMessageLock: Locks all messages that don't need low-latency transmissions & high
|
|
// speed behind a lock. This ensures safety when it comes to handling messages correctly.
|
|
globalNonCriticalMessageLock sync.Mutex
|
|
// globalNonCriticalMessageChan: Channel for handling messages that need a reply / aren't critical.
|
|
globalNonCriticalMessageChan chan interface{}
|
|
}
|
|
|
|
func (backend *SSHAppBackend) StartBackend(configBytes []byte) (bool, error) {
|
|
log.Info("SSHAppBackend is initializing...")
|
|
backend.globalNonCriticalMessageChan = make(chan interface{})
|
|
backend.tcpProxies = map[uint16]*TCPProxy{}
|
|
backend.udpProxies = map[uint16]*UDPProxy{}
|
|
|
|
var backendData SSHAppBackendData
|
|
|
|
if err := json.Unmarshal(configBytes, &backendData); err != nil {
|
|
return false, err
|
|
}
|
|
|
|
if err := validator.New().Struct(&backendData); err != nil {
|
|
return false, err
|
|
}
|
|
|
|
backend.config = &backendData
|
|
|
|
if len(backend.config.ListenOnIPs) == 0 {
|
|
backend.config.ListenOnIPs = []string{"0.0.0.0"}
|
|
}
|
|
|
|
signer, err := ssh.ParsePrivateKey([]byte(backendData.PrivateKey))
|
|
|
|
if err != nil {
|
|
log.Warnf("Failed to initialize: %s", err.Error())
|
|
return false, err
|
|
}
|
|
|
|
auth := ssh.PublicKeys(signer)
|
|
|
|
config := &ssh.ClientConfig{
|
|
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
|
User: backendData.Username,
|
|
Auth: []ssh.AuthMethod{
|
|
auth,
|
|
},
|
|
}
|
|
|
|
conn, err := ssh.Dial("tcp", fmt.Sprintf("%s:%d", backendData.IP, backendData.Port), config)
|
|
|
|
if err != nil {
|
|
log.Warnf("Failed to initialize: %s", err.Error())
|
|
return false, err
|
|
}
|
|
|
|
backend.conn = conn
|
|
|
|
log.Debug("SSHAppBackend has connected successfully.")
|
|
log.Debug("Getting CPU architecture...")
|
|
|
|
session, err := backend.conn.NewSession()
|
|
|
|
if err != nil {
|
|
log.Warnf("Failed to create session: %s", err.Error())
|
|
conn.Close()
|
|
backend.conn = nil
|
|
return false, err
|
|
}
|
|
|
|
var stdoutBuf bytes.Buffer
|
|
session.Stdout = &stdoutBuf
|
|
|
|
err = session.Run("uname -m")
|
|
|
|
if err != nil {
|
|
log.Warnf("Failed to run uname command: %s", err.Error())
|
|
conn.Close()
|
|
backend.conn = nil
|
|
return false, err
|
|
}
|
|
|
|
cpuArchBytes := make([]byte, stdoutBuf.Len())
|
|
stdoutBuf.Read(cpuArchBytes)
|
|
|
|
cpuArch := string(cpuArchBytes)
|
|
cpuArch = cpuArch[:len(cpuArch)-1]
|
|
|
|
var backendBinary string
|
|
|
|
// Ordered in (subjective) popularity
|
|
if cpuArch == "x86_64" {
|
|
backendBinary = "remote-bin/rt-amd64"
|
|
} else if cpuArch == "aarch64" {
|
|
backendBinary = "remote-bin/rt-arm64"
|
|
} else if cpuArch == "arm" {
|
|
backendBinary = "remote-bin/rt-arm"
|
|
} else if len(cpuArch) == 4 && string(cpuArch[0]) == "i" && strings.HasSuffix(cpuArch, "86") {
|
|
backendBinary = "remote-bin/rt-386"
|
|
} else {
|
|
log.Warn("Failed to determine executable to use: CPU architecture not compiled/supported currently")
|
|
conn.Close()
|
|
backend.conn = nil
|
|
return false, fmt.Errorf("CPU architecture not compiled/supported currently")
|
|
}
|
|
|
|
log.Debug("Checking if we need to copy the application...")
|
|
|
|
var binary []byte
|
|
needsToCopyBinary := true
|
|
|
|
session, err = backend.conn.NewSession()
|
|
|
|
if err != nil {
|
|
log.Warnf("Failed to create session: %s", err.Error())
|
|
conn.Close()
|
|
backend.conn = nil
|
|
return false, err
|
|
}
|
|
|
|
session.Stdout = &stdoutBuf
|
|
|
|
err = session.Start("[ -f /tmp/sshappbackend.runtime ] && md5sum /tmp/sshappbackend.runtime | cut -d \" \" -f 1")
|
|
|
|
if err != nil {
|
|
log.Warnf("Failed to calculate hash of possibly existing backend: %s", err.Error())
|
|
conn.Close()
|
|
backend.conn = nil
|
|
return false, err
|
|
}
|
|
|
|
fileExists := stdoutBuf.Len() != 0
|
|
|
|
if fileExists {
|
|
remoteMD5HashStringBuf := make([]byte, stdoutBuf.Len())
|
|
stdoutBuf.Read(remoteMD5HashStringBuf)
|
|
|
|
remoteMD5HashString := string(remoteMD5HashStringBuf)
|
|
remoteMD5HashString = remoteMD5HashString[:len(remoteMD5HashString)-1]
|
|
|
|
remoteMD5Hash, err := hex.DecodeString(remoteMD5HashString)
|
|
|
|
if err != nil {
|
|
log.Warnf("Failed to decode hex: %s", err.Error())
|
|
conn.Close()
|
|
backend.conn = nil
|
|
return false, err
|
|
}
|
|
|
|
binary, err = binFiles.ReadFile(backendBinary)
|
|
|
|
if err != nil {
|
|
log.Warnf("Failed to read file in the embedded FS: %s", err.Error())
|
|
conn.Close()
|
|
backend.conn = nil
|
|
return false, fmt.Errorf("(embedded FS): %s", err.Error())
|
|
}
|
|
|
|
localMD5Hash := md5.Sum(binary)
|
|
|
|
log.Infof("remote: %s, local: %s", remoteMD5HashString, hex.EncodeToString(localMD5Hash[:]))
|
|
|
|
if bytes.Compare(localMD5Hash[:], remoteMD5Hash) == 0 {
|
|
needsToCopyBinary = false
|
|
}
|
|
}
|
|
|
|
if needsToCopyBinary {
|
|
log.Debug("Copying binary...")
|
|
|
|
sftpInstance, err := sftp.NewClient(conn)
|
|
|
|
if err != nil {
|
|
log.Warnf("Failed to initialize SFTP: %s", err.Error())
|
|
conn.Close()
|
|
backend.conn = nil
|
|
return false, err
|
|
}
|
|
|
|
defer sftpInstance.Close()
|
|
|
|
if len(binary) == 0 {
|
|
binary, err = binFiles.ReadFile(backendBinary)
|
|
|
|
if err != nil {
|
|
log.Warnf("Failed to read file in the embedded FS: %s", err.Error())
|
|
conn.Close()
|
|
backend.conn = nil
|
|
return false, fmt.Errorf("(embedded FS): %s", err.Error())
|
|
}
|
|
}
|
|
|
|
var file *sftp.File
|
|
|
|
if fileExists {
|
|
file, err = sftpInstance.OpenFile("/tmp/sshappbackend.runtime", os.O_WRONLY)
|
|
} else {
|
|
file, err = sftpInstance.Create("/tmp/sshappbackend.runtime")
|
|
}
|
|
|
|
if err != nil {
|
|
log.Warnf("Failed to create (or open) file: %s", err.Error())
|
|
conn.Close()
|
|
backend.conn = nil
|
|
return false, err
|
|
}
|
|
|
|
_, err = file.Write(binary)
|
|
|
|
if err != nil {
|
|
log.Warnf("Failed to write file: %s", err.Error())
|
|
conn.Close()
|
|
backend.conn = nil
|
|
return false, err
|
|
}
|
|
|
|
err = file.Chmod(0755)
|
|
|
|
if err != nil {
|
|
log.Warnf("Failed to change permissions on file: %s", err.Error())
|
|
conn.Close()
|
|
backend.conn = nil
|
|
return false, err
|
|
}
|
|
|
|
log.Debug("Done copying file.")
|
|
sftpInstance.Close()
|
|
} else {
|
|
log.Debug("Skipping copying as there's a copy on disk already.")
|
|
}
|
|
|
|
log.Debug("Initializing Unix socket...")
|
|
|
|
socketPath := fmt.Sprintf("/tmp/sock-%d.sock", rand.Uint())
|
|
listener, err := conn.ListenUnix(socketPath)
|
|
|
|
if err != nil {
|
|
log.Warnf("Failed to listen on socket: %s", err.Error())
|
|
conn.Close()
|
|
backend.conn = nil
|
|
return false, err
|
|
}
|
|
|
|
log.Debug("Starting process...")
|
|
|
|
session, err = backend.conn.NewSession()
|
|
|
|
if err != nil {
|
|
log.Warnf("Failed to create session: %s", err.Error())
|
|
conn.Close()
|
|
backend.conn = nil
|
|
return false, err
|
|
}
|
|
|
|
backend.listener = listener
|
|
|
|
session.Stdout = WriteLogger{}
|
|
session.Stderr = WriteLogger{}
|
|
|
|
go func() {
|
|
for {
|
|
err := session.Run(fmt.Sprintf("HERMES_LOG_LEVEL=\"%s\" HERMES_API_SOCK=\"%s\" /tmp/sshappbackend.runtime", os.Getenv("HERMES_LOG_LEVEL"), socketPath))
|
|
|
|
if err != nil && !errors.Is(err, &ssh.ExitError{}) && !errors.Is(err, &ssh.ExitMissingError{}) {
|
|
log.Errorf("Critically failed during execution of remote code: %s", err.Error())
|
|
return
|
|
} else {
|
|
log.Warn("Remote code failed for an unknown reason. Restarting...")
|
|
}
|
|
}
|
|
}()
|
|
|
|
go backend.sockServerHandler()
|
|
|
|
log.Debug("Started process. Waiting for Unix socket connection...")
|
|
|
|
for backend.currentSock == nil {
|
|
time.Sleep(10 * time.Millisecond)
|
|
}
|
|
|
|
log.Debug("Detected connection. Sending initialization command...")
|
|
|
|
proxyStatusRaw, err := backend.SendNonCriticalMessage(&commonbackend.Start{
|
|
Arguments: []byte{},
|
|
})
|
|
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
proxyStatus, ok := proxyStatusRaw.(*commonbackend.BackendStatusResponse)
|
|
|
|
if !ok {
|
|
return false, fmt.Errorf("recieved invalid response type: %T", proxyStatusRaw)
|
|
}
|
|
|
|
if proxyStatus.StatusCode == commonbackend.StatusFailure {
|
|
if proxyStatus.Message == "" {
|
|
return false, fmt.Errorf("failed to initialize backend in remote code")
|
|
} else {
|
|
return false, fmt.Errorf("failed to initialize backend in remote code: %s", proxyStatus.Message)
|
|
}
|
|
}
|
|
|
|
log.Info("SSHAppBackend has initialized successfully.")
|
|
|
|
return true, nil
|
|
}
|
|
|
|
func (backend *SSHAppBackend) StopBackend() (bool, error) {
|
|
err := backend.conn.Close()
|
|
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
func (backend *SSHAppBackend) GetBackendStatus() (bool, error) {
|
|
return backend.conn != nil, nil
|
|
}
|
|
|
|
func (backend *SSHAppBackend) StartProxy(command *commonbackend.AddProxy) (bool, error) {
|
|
proxyStatusRaw, err := backend.SendNonCriticalMessage(command)
|
|
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
proxyStatus, ok := proxyStatusRaw.(*datacommands.ProxyStatusResponse)
|
|
|
|
if !ok {
|
|
return false, fmt.Errorf("recieved invalid response type: %T", proxyStatusRaw)
|
|
}
|
|
|
|
if !proxyStatus.IsActive {
|
|
return false, fmt.Errorf("failed to initialize proxy in remote code")
|
|
}
|
|
|
|
if command.Protocol == "tcp" {
|
|
backend.tcpProxies[proxyStatus.ProxyID] = &TCPProxy{
|
|
proxyInformation: command,
|
|
}
|
|
|
|
backend.tcpProxies[proxyStatus.ProxyID].connections = map[uint16]net.Conn{}
|
|
} else if command.Protocol == "udp" {
|
|
backend.udpProxies[proxyStatus.ProxyID] = &UDPProxy{
|
|
proxyInformation: command,
|
|
portTranslation: &porttranslation.PortTranslation{},
|
|
}
|
|
|
|
backend.udpProxies[proxyStatus.ProxyID].portTranslation.UDPAddr = &net.UDPAddr{
|
|
IP: net.ParseIP(command.SourceIP),
|
|
Port: int(command.SourcePort),
|
|
}
|
|
|
|
udpMessageCommand := &datacommands.UDPProxyData{}
|
|
udpMessageCommand.ProxyID = proxyStatus.ProxyID
|
|
|
|
backend.udpProxies[proxyStatus.ProxyID].portTranslation.WriteFrom = func(ip string, port uint16, data []byte) {
|
|
udpMessageCommand.ClientIP = ip
|
|
udpMessageCommand.ClientPort = port
|
|
udpMessageCommand.DataLength = uint16(len(data))
|
|
|
|
marshalledCommand, err := datacommands.Marshal(udpMessageCommand)
|
|
|
|
if err != nil {
|
|
log.Warnf("Failed to marshal UDP message header")
|
|
return
|
|
}
|
|
|
|
if _, err := backend.currentSock.Write(marshalledCommand); err != nil {
|
|
log.Warnf("Failed to write UDP message header")
|
|
return
|
|
}
|
|
|
|
if _, err := backend.currentSock.Write(data); err != nil {
|
|
log.Warnf("Failed to write UDP message")
|
|
return
|
|
}
|
|
}
|
|
|
|
go func() {
|
|
for {
|
|
time.Sleep(3 * time.Minute)
|
|
|
|
// Checks if the proxy still exists before continuing
|
|
_, ok := backend.udpProxies[proxyStatus.ProxyID]
|
|
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
// Then attempt to run cleanup tasks
|
|
log.Debug("Running UDP proxy cleanup tasks (invoking CleanupPorts() on portTranslation)")
|
|
backend.udpProxies[proxyStatus.ProxyID].portTranslation.CleanupPorts()
|
|
}
|
|
}()
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
func (backend *SSHAppBackend) StopProxy(command *commonbackend.RemoveProxy) (bool, error) {
|
|
if command.Protocol == "tcp" {
|
|
for proxyIndex, proxy := range backend.tcpProxies {
|
|
if proxy.proxyInformation.DestPort != command.DestPort {
|
|
continue
|
|
}
|
|
|
|
onDisconnect := &datacommands.TCPConnectionClosed{
|
|
ProxyID: proxyIndex,
|
|
}
|
|
|
|
for connectionIndex, connection := range proxy.connections {
|
|
connection.Close()
|
|
delete(proxy.connections, connectionIndex)
|
|
|
|
onDisconnect.ConnectionID = connectionIndex
|
|
disconnectionCommandMarshalled, err := datacommands.Marshal(onDisconnect)
|
|
|
|
if err != nil {
|
|
log.Errorf("failed to marshal disconnection message: %s", err.Error())
|
|
}
|
|
|
|
backend.currentSock.Write(disconnectionCommandMarshalled)
|
|
}
|
|
|
|
proxyStatusRaw, err := backend.SendNonCriticalMessage(&datacommands.RemoveProxy{
|
|
ProxyID: proxyIndex,
|
|
})
|
|
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
proxyStatus, ok := proxyStatusRaw.(*datacommands.ProxyStatusResponse)
|
|
|
|
if !ok {
|
|
log.Warn("Failed to stop proxy: typecast failed")
|
|
return true, fmt.Errorf("failed to stop proxy: typecast failed")
|
|
}
|
|
|
|
if proxyStatus.IsActive {
|
|
log.Warn("Failed to stop proxy: still running")
|
|
return true, fmt.Errorf("failed to stop proxy: still running")
|
|
}
|
|
}
|
|
} else if command.Protocol == "udp" {
|
|
for proxyIndex, proxy := range backend.udpProxies {
|
|
if proxy.proxyInformation.DestPort != command.DestPort {
|
|
continue
|
|
}
|
|
|
|
proxyStatusRaw, err := backend.SendNonCriticalMessage(&datacommands.RemoveProxy{
|
|
ProxyID: proxyIndex,
|
|
})
|
|
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
proxyStatus, ok := proxyStatusRaw.(*datacommands.ProxyStatusResponse)
|
|
|
|
if !ok {
|
|
log.Warn("Failed to stop proxy: typecast failed")
|
|
return true, fmt.Errorf("failed to stop proxy: typecast failed")
|
|
}
|
|
|
|
if proxyStatus.IsActive {
|
|
log.Warn("Failed to stop proxy: still running")
|
|
return true, fmt.Errorf("failed to stop proxy: still running")
|
|
}
|
|
|
|
proxy.portTranslation.StopAllPorts()
|
|
delete(backend.udpProxies, proxyIndex)
|
|
}
|
|
}
|
|
|
|
return false, fmt.Errorf("could not find the proxy")
|
|
}
|
|
|
|
func (backend *SSHAppBackend) GetAllClientConnections() []*commonbackend.ProxyClientConnection {
|
|
connections := []*commonbackend.ProxyClientConnection{}
|
|
informationRequest := &datacommands.ProxyConnectionInformationRequest{}
|
|
|
|
for proxyID, tcpProxy := range backend.tcpProxies {
|
|
informationRequest.ProxyID = proxyID
|
|
|
|
for connectionID := range tcpProxy.connections {
|
|
informationRequest.ConnectionID = connectionID
|
|
|
|
proxyStatusRaw, err := backend.SendNonCriticalMessage(informationRequest)
|
|
|
|
if err != nil {
|
|
log.Warnf("Failed to get connection information for Proxy ID: %d, Connection ID: %d: %s", proxyID, connectionID, err.Error())
|
|
return connections
|
|
}
|
|
|
|
connectionStatus, ok := proxyStatusRaw.(*datacommands.ProxyConnectionInformationResponse)
|
|
|
|
if !ok {
|
|
log.Warn("Failed to get connection response: typecast failed")
|
|
return connections
|
|
}
|
|
|
|
if !connectionStatus.Exists {
|
|
log.Warnf("Connection with proxy ID: %d, Connection ID: %d is reported to not exist!", proxyID, connectionID)
|
|
tcpProxy.connections[connectionID].Close()
|
|
}
|
|
|
|
connections = append(connections, &commonbackend.ProxyClientConnection{
|
|
SourceIP: tcpProxy.proxyInformation.SourceIP,
|
|
SourcePort: tcpProxy.proxyInformation.SourcePort,
|
|
DestPort: tcpProxy.proxyInformation.DestPort,
|
|
ClientIP: connectionStatus.ClientIP,
|
|
ClientPort: connectionStatus.ClientPort,
|
|
})
|
|
}
|
|
}
|
|
|
|
return connections
|
|
}
|
|
|
|
// We don't have any parameter limitations, so we should be good.
|
|
func (backend *SSHAppBackend) CheckParametersForConnections(clientParameters *commonbackend.CheckClientParameters) *commonbackend.CheckParametersResponse {
|
|
return &commonbackend.CheckParametersResponse{
|
|
IsValid: true,
|
|
}
|
|
}
|
|
|
|
func (backend *SSHAppBackend) CheckParametersForBackend(arguments []byte) *commonbackend.CheckParametersResponse {
|
|
var backendData SSHAppBackendData
|
|
|
|
if err := json.Unmarshal(arguments, &backendData); err != nil {
|
|
return &commonbackend.CheckParametersResponse{
|
|
IsValid: false,
|
|
Message: fmt.Sprintf("could not read json: %s", err.Error()),
|
|
}
|
|
}
|
|
|
|
if err := validator.New().Struct(&backendData); err != nil {
|
|
return &commonbackend.CheckParametersResponse{
|
|
IsValid: false,
|
|
Message: fmt.Sprintf("failed validation of parameters: %s", err.Error()),
|
|
}
|
|
}
|
|
|
|
return &commonbackend.CheckParametersResponse{
|
|
IsValid: true,
|
|
}
|
|
}
|
|
|
|
func (backend *SSHAppBackend) OnTCPConnectionOpened(proxyID, connectionID uint16) {
|
|
conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", backend.tcpProxies[proxyID].proxyInformation.SourceIP, backend.tcpProxies[proxyID].proxyInformation.SourcePort))
|
|
|
|
if err != nil {
|
|
log.Warnf("failed to dial sock: %s", err.Error())
|
|
}
|
|
|
|
go func() {
|
|
dataBuf := make([]byte, 65535)
|
|
|
|
tcpData := &datacommands.TCPProxyData{
|
|
ProxyID: proxyID,
|
|
ConnectionID: connectionID,
|
|
}
|
|
|
|
for {
|
|
len, err := conn.Read(dataBuf)
|
|
|
|
if err != nil {
|
|
if errors.Is(err, net.ErrClosed) {
|
|
return
|
|
} else if err.Error() != "EOF" {
|
|
log.Warnf("failed to read from sock: %s", err.Error())
|
|
}
|
|
|
|
conn.Close()
|
|
break
|
|
}
|
|
|
|
tcpData.DataLength = uint16(len)
|
|
marshalledMessageCommand, err := datacommands.Marshal(tcpData)
|
|
|
|
if err != nil {
|
|
log.Warnf("failed to marshal message data: %s", err.Error())
|
|
|
|
conn.Close()
|
|
break
|
|
}
|
|
|
|
if _, err := backend.currentSock.Write(marshalledMessageCommand); err != nil {
|
|
log.Warnf("failed to send marshalled message data: %s", err.Error())
|
|
|
|
conn.Close()
|
|
break
|
|
}
|
|
|
|
if _, err := backend.currentSock.Write(dataBuf[:len]); err != nil {
|
|
log.Warnf("failed to send raw message data: %s", err.Error())
|
|
|
|
conn.Close()
|
|
break
|
|
}
|
|
}
|
|
|
|
onDisconnect := &datacommands.TCPConnectionClosed{
|
|
ProxyID: proxyID,
|
|
ConnectionID: connectionID,
|
|
}
|
|
|
|
disconnectionCommandMarshalled, err := datacommands.Marshal(onDisconnect)
|
|
|
|
if err != nil {
|
|
log.Errorf("failed to marshal disconnection message: %s", err.Error())
|
|
}
|
|
|
|
backend.currentSock.Write(disconnectionCommandMarshalled)
|
|
}()
|
|
|
|
backend.tcpProxies[proxyID].connections[connectionID] = conn
|
|
}
|
|
|
|
func (backend *SSHAppBackend) OnTCPConnectionClosed(proxyID, connectionID uint16) {
|
|
proxy, ok := backend.tcpProxies[proxyID]
|
|
|
|
if !ok {
|
|
log.Warn("Could not find TCP proxy")
|
|
}
|
|
|
|
connection, ok := proxy.connections[connectionID]
|
|
|
|
if !ok {
|
|
log.Warn("Could not find connection in TCP proxy")
|
|
}
|
|
|
|
connection.Close()
|
|
delete(proxy.connections, connectionID)
|
|
}
|
|
|
|
func (backend *SSHAppBackend) HandleTCPMessage(message *datacommands.TCPProxyData, data []byte) {
|
|
proxy, ok := backend.tcpProxies[message.ProxyID]
|
|
|
|
if !ok {
|
|
log.Warn("Could not find TCP proxy")
|
|
}
|
|
|
|
connection, ok := proxy.connections[message.ConnectionID]
|
|
|
|
if !ok {
|
|
log.Warn("Could not find connection in TCP proxy")
|
|
}
|
|
|
|
connection.Write(data)
|
|
}
|
|
|
|
func (backend *SSHAppBackend) HandleUDPMessage(message *datacommands.UDPProxyData, data []byte) {
|
|
proxy, ok := backend.udpProxies[message.ProxyID]
|
|
|
|
if !ok {
|
|
log.Warn("Could not find UDP proxy")
|
|
}
|
|
|
|
if _, err := proxy.portTranslation.WriteTo(message.ClientIP, message.ClientPort, data); err != nil {
|
|
log.Warnf("Failed to write to UDP: %s", err.Error())
|
|
}
|
|
}
|
|
|
|
func (backend *SSHAppBackend) SendNonCriticalMessage(iface interface{}) (interface{}, error) {
|
|
if backend.currentSock == nil {
|
|
return nil, fmt.Errorf("socket connection not initialized yet")
|
|
}
|
|
|
|
bytes, err := datacommands.Marshal(iface)
|
|
|
|
if err != nil && err.Error() == "unsupported command type" {
|
|
bytes, err = commonbackend.Marshal(iface)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
backend.globalNonCriticalMessageLock.Lock()
|
|
|
|
if _, err := backend.currentSock.Write(bytes); err != nil {
|
|
backend.globalNonCriticalMessageLock.Unlock()
|
|
return nil, fmt.Errorf("failed to write message: %s", err.Error())
|
|
}
|
|
|
|
reply, ok := <-backend.globalNonCriticalMessageChan
|
|
|
|
if !ok {
|
|
backend.globalNonCriticalMessageLock.Unlock()
|
|
return nil, fmt.Errorf("failed to get reply back: chan not OK")
|
|
}
|
|
|
|
backend.globalNonCriticalMessageLock.Unlock()
|
|
return reply, nil
|
|
}
|
|
|
|
func (backend *SSHAppBackend) sockServerHandler() {
|
|
for {
|
|
conn, err := backend.listener.Accept()
|
|
|
|
if err != nil {
|
|
log.Warnf("Failed to accept remote connection: %s", err.Error())
|
|
}
|
|
|
|
log.Debug("Successfully connected.")
|
|
|
|
backend.currentSock = conn
|
|
|
|
commandID := make([]byte, 1)
|
|
|
|
gaslighter := &gaslighter.Gaslighter{}
|
|
gaslighter.ProxiedReader = conn
|
|
|
|
dataBuffer := make([]byte, 65535)
|
|
|
|
var commandRaw interface{}
|
|
|
|
for {
|
|
if _, err := conn.Read(commandID); err != nil {
|
|
log.Warnf("Failed to read command ID: %s", err.Error())
|
|
return
|
|
}
|
|
|
|
gaslighter.Byte = commandID[0]
|
|
gaslighter.HasGaslit = false
|
|
|
|
if gaslighter.Byte > 100 {
|
|
commandRaw, err = datacommands.Unmarshal(gaslighter)
|
|
} else {
|
|
commandRaw, err = commonbackend.Unmarshal(gaslighter)
|
|
}
|
|
|
|
if err != nil {
|
|
log.Warnf("Failed to parse command: %s", err.Error())
|
|
}
|
|
|
|
switch command := commandRaw.(type) {
|
|
case *datacommands.TCPConnectionOpened:
|
|
backend.OnTCPConnectionOpened(command.ProxyID, command.ConnectionID)
|
|
case *datacommands.TCPConnectionClosed:
|
|
backend.OnTCPConnectionClosed(command.ProxyID, command.ConnectionID)
|
|
case *datacommands.TCPProxyData:
|
|
if _, err := io.ReadFull(conn, dataBuffer[:command.DataLength]); err != nil {
|
|
log.Warnf("Failed to read entire data buffer: %s", err.Error())
|
|
break
|
|
}
|
|
|
|
backend.HandleTCPMessage(command, dataBuffer[:command.DataLength])
|
|
case *datacommands.UDPProxyData:
|
|
if _, err := io.ReadFull(conn, dataBuffer[:command.DataLength]); err != nil {
|
|
log.Warnf("Failed to read entire data buffer: %s", err.Error())
|
|
break
|
|
}
|
|
|
|
backend.HandleUDPMessage(command, dataBuffer[:command.DataLength])
|
|
default:
|
|
select {
|
|
case backend.globalNonCriticalMessageChan <- command:
|
|
default:
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func main() {
|
|
logLevel := os.Getenv("HERMES_LOG_LEVEL")
|
|
|
|
if logLevel != "" {
|
|
switch logLevel {
|
|
case "debug":
|
|
log.SetLevel(log.DebugLevel)
|
|
|
|
case "info":
|
|
log.SetLevel(log.InfoLevel)
|
|
|
|
case "warn":
|
|
log.SetLevel(log.WarnLevel)
|
|
|
|
case "error":
|
|
log.SetLevel(log.ErrorLevel)
|
|
|
|
case "fatal":
|
|
log.SetLevel(log.FatalLevel)
|
|
}
|
|
}
|
|
|
|
backend := &SSHAppBackend{}
|
|
|
|
application := backendutil.NewHelper(backend)
|
|
err := application.Start()
|
|
|
|
if err != nil {
|
|
log.Fatalf("failed execution in application: %s", err.Error())
|
|
}
|
|
}
|