diff --git a/backend/backendutil/application.go b/backend/backendutil/application.go index 7f134a2..afa3147 100644 --- a/backend/backendutil/application.go +++ b/backend/backendutil/application.go @@ -132,12 +132,12 @@ func (helper *BackendApplicationHelper) Start() error { ok, err := helper.Backend.StartProxy(command) var hasAnyFailed bool - if !ok { - log.Warnf("failed to add proxy (%s:%d -> remote:%d): StartProxy returned into failure state", command.SourceIP, command.SourcePort, command.DestPort) - hasAnyFailed = true - } else if err != nil { + if err != nil { log.Warnf("failed to add proxy (%s:%d -> remote:%d): %s", command.SourceIP, command.SourcePort, command.DestPort, err.Error()) hasAnyFailed = true + } else if !ok { + log.Warnf("failed to add proxy (%s:%d -> remote:%d): StartProxy returned into failure state", command.SourceIP, command.SourcePort, command.DestPort) + hasAnyFailed = true } response := &commonbackend.ProxyStatusResponse{ @@ -160,12 +160,12 @@ func (helper *BackendApplicationHelper) Start() error { ok, err := helper.Backend.StopProxy(command) var hasAnyFailed bool - if !ok { - log.Warnf("failed to remove proxy (%s:%d -> remote:%d): RemoveProxy returned into failure state", command.SourceIP, command.SourcePort, command.DestPort) - hasAnyFailed = true - } else if err != nil { + if err != nil { log.Warnf("failed to remove proxy (%s:%d -> remote:%d): %s", command.SourceIP, command.SourcePort, command.DestPort, err.Error()) hasAnyFailed = true + } else if !ok { + log.Warnf("failed to remove proxy (%s:%d -> remote:%d): RemoveProxy returned into failure state", command.SourceIP, command.SourcePort, command.DestPort) + hasAnyFailed = true } response := &commonbackend.ProxyStatusResponse{ diff --git a/backend/commonbackend/unmarshal.go b/backend/commonbackend/unmarshal.go index 8e338c6..6bb5af4 100644 --- a/backend/commonbackend/unmarshal.go +++ b/backend/commonbackend/unmarshal.go @@ -213,7 +213,7 @@ func Unmarshal(conn io.Reader) (interface{}, error) { if protocolBytes[0] == TCP { protocol = "tcp" - } else if protocolBytes[1] == UDP { + } else if protocolBytes[0] == UDP { protocol = "udp" } else { return nil, fmt.Errorf("invalid protocol") @@ -270,7 +270,7 @@ func Unmarshal(conn io.Reader) (interface{}, error) { if protocolBytes[0] == TCP { protocol = "tcp" - } else if protocolBytes[1] == UDP { + } else if protocolBytes[0] == UDP { protocol = "udp" } else { return nil, fmt.Errorf("invalid protocol") @@ -364,7 +364,7 @@ func Unmarshal(conn io.Reader) (interface{}, error) { if protocolBytes[0] == TCP { protocol = "tcp" - } else if protocolBytes[1] == UDP { + } else if protocolBytes[0] == UDP { protocol = "udp" } else { return nil, fmt.Errorf("invalid protocol") @@ -523,7 +523,7 @@ func Unmarshal(conn io.Reader) (interface{}, error) { if protocolBytes[0] == TCP { protocol = "tcp" - } else if protocolBytes[1] == UDP { + } else if protocolBytes[0] == UDP { protocol = "udp" } else { return nil, fmt.Errorf("invalid protocol") @@ -580,7 +580,7 @@ func Unmarshal(conn io.Reader) (interface{}, error) { if protocolBytes[0] == TCP { protocol = "tcp" - } else if protocolBytes[1] == UDP { + } else if protocolBytes[0] == UDP { protocol = "udp" } else { return nil, fmt.Errorf("invalid protocol") diff --git a/backend/sshappbackend/gaslighter/gaslighter.go b/backend/sshappbackend/gaslighter/gaslighter.go new file mode 100644 index 0000000..ecccec7 --- /dev/null +++ b/backend/sshappbackend/gaslighter/gaslighter.go @@ -0,0 +1,30 @@ +package gaslighter + +import "io" + +type Gaslighter struct { + Byte byte + HasGaslit bool + ProxiedReader io.Reader +} + +func (gaslighter *Gaslighter) Read(p []byte) (n int, err error) { + if gaslighter.HasGaslit { + return gaslighter.ProxiedReader.Read(p) + } + + if len(p) == 0 { + return 0, nil + } + + p[0] = gaslighter.Byte + gaslighter.HasGaslit = true + + if len(p) > 1 { + n, err := gaslighter.ProxiedReader.Read(p[1:]) + + return n + 1, err + } else { + return 1, nil + } +} diff --git a/backend/sshappbackend/local-code/main.go b/backend/sshappbackend/local-code/main.go index 98a2370..abb0081 100644 --- a/backend/sshappbackend/local-code/main.go +++ b/backend/sshappbackend/local-code/main.go @@ -5,19 +5,37 @@ import ( "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"` @@ -27,14 +45,27 @@ type SSHAppBackendData struct { } type SSHAppBackend struct { - config *SSHAppBackendData - conn *ssh.Client - clients []*commonbackend.ProxyClientConnection - arrayPropMutex sync.Mutex + 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 { @@ -77,8 +108,8 @@ func (backend *SSHAppBackend) StartBackend(configBytes []byte) (bool, error) { backend.conn = conn - log.Info("SSHAppBackend has connected successfully.") - log.Info("Getting CPU architecture...") + log.Debug("SSHAppBackend has connected successfully.") + log.Debug("Getting CPU architecture...") session, err := backend.conn.NewSession() @@ -125,7 +156,7 @@ func (backend *SSHAppBackend) StartBackend(configBytes []byte) (bool, error) { return false, fmt.Errorf("CPU architecture not compiled/supported currently") } - log.Info("Checking if we need to copy the application...") + log.Debug("Checking if we need to copy the application...") var binary []byte needsToCopyBinary := true @@ -187,7 +218,8 @@ func (backend *SSHAppBackend) StartBackend(configBytes []byte) (bool, error) { } if needsToCopyBinary { - log.Info("Copying binary...") + log.Debug("Copying binary...") + sftpInstance, err := sftp.NewClient(conn) if err != nil { @@ -213,13 +245,13 @@ func (backend *SSHAppBackend) StartBackend(configBytes []byte) (bool, error) { var file *sftp.File if fileExists { - file, err = sftpInstance.Create("/tmp/sshappbackend.runtime") - } else { 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 file: %s", err.Error()) + log.Warnf("Failed to create (or open) file: %s", err.Error()) conn.Close() backend.conn = nil return false, err @@ -234,7 +266,7 @@ func (backend *SSHAppBackend) StartBackend(configBytes []byte) (bool, error) { return false, err } - err = file.Chmod(775) + err = file.Chmod(0755) if err != nil { log.Warnf("Failed to change permissions on file: %s", err.Error()) @@ -243,12 +275,25 @@ func (backend *SSHAppBackend) StartBackend(configBytes []byte) (bool, error) { return false, err } - log.Info("Done copying file.") + log.Debug("Done copying file.") + sftpInstance.Close() } else { - log.Info("Skipping copying as there's a copy on disk already.") + log.Debug("Skipping copying as there's a copy on disk already.") } - log.Info("Starting process...") + 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() @@ -259,10 +304,56 @@ func (backend *SSHAppBackend) StartBackend(configBytes []byte) (bool, error) { return false, err } + backend.listener = listener + session.Stdout = WriteLogger{} session.Stderr = WriteLogger{} - go session.Run("/tmp/sshappbackend.runtime") + 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 @@ -283,17 +374,171 @@ func (backend *SSHAppBackend) GetBackendStatus() (bool, error) { } 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") } +// TODO: implement! func (backend *SSHAppBackend) GetAllClientConnections() []*commonbackend.ProxyClientConnection { - return backend.clients + return []*commonbackend.ProxyClientConnection{} } +// 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, @@ -322,6 +567,226 @@ func (backend *SSHAppBackend) CheckParametersForBackend(arguments []byte) *commo } } +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") diff --git a/backend/sshappbackend/local-code/porttranslation/translation.go b/backend/sshappbackend/local-code/porttranslation/translation.go new file mode 100644 index 0000000..b8c0454 --- /dev/null +++ b/backend/sshappbackend/local-code/porttranslation/translation.go @@ -0,0 +1,112 @@ +package porttranslation + +import ( + "fmt" + "net" + "sync" + "time" +) + +type connectionData struct { + udpConn *net.UDPConn + buf []byte + hasBeenAliveFor time.Time +} + +type PortTranslation struct { + UDPAddr *net.UDPAddr + WriteFrom func(ip string, port uint16, data []byte) + + newConnectionLock sync.Mutex + connections map[string]map[uint16]*connectionData +} + +func (translation *PortTranslation) CleanupPorts() { + if translation.connections == nil { + translation.connections = map[string]map[uint16]*connectionData{} + return + } + + for connectionIPIndex, connectionPorts := range translation.connections { + anyAreAlive := false + + for connectionPortIndex, connectionData := range connectionPorts { + if time.Now().Before(connectionData.hasBeenAliveFor.Add(3 * time.Minute)) { + anyAreAlive = true + continue + } + + connectionData.udpConn.Close() + delete(connectionPorts, connectionPortIndex) + } + + if !anyAreAlive { + delete(translation.connections, connectionIPIndex) + } + } +} + +func (translation *PortTranslation) StopAllPorts() { + if translation.connections == nil { + return + } + + for connectionIPIndex, connectionPorts := range translation.connections { + for connectionPortIndex, connectionData := range connectionPorts { + connectionData.udpConn.Close() + delete(connectionPorts, connectionPortIndex) + } + + delete(translation.connections, connectionIPIndex) + } + + translation.connections = nil +} + +func (translation *PortTranslation) WriteTo(ip string, port uint16, data []byte) (int, error) { + if translation.connections == nil { + translation.connections = map[string]map[uint16]*connectionData{} + } + + connectionPortData, ok := translation.connections[ip] + + if !ok { + translation.connections[ip] = map[uint16]*connectionData{} + connectionPortData = translation.connections[ip] + } + + connectionStruct, ok := connectionPortData[port] + + if !ok { + connectionPortData[port] = &connectionData{} + connectionStruct = connectionPortData[port] + + udpConn, err := net.DialUDP("udp", nil, translation.UDPAddr) + + if err != nil { + return 0, fmt.Errorf("failed to initialize UDP socket: %s", err.Error()) + } + + connectionStruct.udpConn = udpConn + connectionStruct.buf = make([]byte, 65535) + + go func() { + for { + n, err := udpConn.Read(connectionStruct.buf) + + if err != nil { + udpConn.Close() + delete(connectionPortData, port) + + return + } + + connectionStruct.hasBeenAliveFor = time.Now() + translation.WriteFrom(ip, port, connectionStruct.buf[:n]) + } + }() + } + + connectionStruct.hasBeenAliveFor = time.Now() + return connectionStruct.udpConn.Write(data) +} diff --git a/backend/sshappbackend/remote-code/backendutil_custom/application.go b/backend/sshappbackend/remote-code/backendutil_custom/application.go index 0e00da7..2747f28 100644 --- a/backend/sshappbackend/remote-code/backendutil_custom/application.go +++ b/backend/sshappbackend/remote-code/backendutil_custom/application.go @@ -8,6 +8,7 @@ import ( "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" "github.com/charmbracelet/log" ) @@ -38,10 +39,28 @@ func (helper *BackendApplicationHelper) Start() error { log.Debug("Sucessfully connected") - for { - commandRaw, err := datacommands.Unmarshal(helper.socket) + gaslighter := &gaslighter.Gaslighter{} + gaslighter.ProxiedReader = helper.socket - if err != nil && err.Error() != "couldn't match command ID" { + commandID := make([]byte, 1) + + for { + if _, err := helper.socket.Read(commandID); err != nil { + return err + } + + gaslighter.Byte = commandID[0] + gaslighter.HasGaslit = false + + var commandRaw interface{} + + if gaslighter.Byte > 100 { + commandRaw, err = datacommands.Unmarshal(gaslighter) + } else { + commandRaw, err = commonbackend.Unmarshal(gaslighter) + } + + if err != nil { return err } @@ -127,155 +146,146 @@ func (helper *BackendApplicationHelper) Start() error { } helper.Backend.HandleUDPMessage(command, bytes) - default: - commandRaw, err := commonbackend.Unmarshal(helper.socket) + case *commonbackend.Start: + ok, err := helper.Backend.StartBackend(command.Arguments) + + var ( + message string + statusCode int + ) + + if err != nil { + message = err.Error() + statusCode = commonbackend.StatusFailure + } else { + statusCode = commonbackend.StatusSuccess + } + + response := &commonbackend.BackendStatusResponse{ + IsRunning: ok, + StatusCode: statusCode, + Message: message, + } + + responseMarshalled, err := commonbackend.Marshal(response) + + if err != nil { + log.Error("failed to marshal response: %s", err.Error()) + continue + } + + helper.socket.Write(responseMarshalled) + case *commonbackend.Stop: + ok, err := helper.Backend.StopBackend() + + var ( + message string + statusCode int + ) + + if err != nil { + message = err.Error() + statusCode = commonbackend.StatusFailure + } else { + statusCode = commonbackend.StatusSuccess + } + + response := &commonbackend.BackendStatusResponse{ + IsRunning: !ok, + StatusCode: statusCode, + Message: message, + } + + responseMarshalled, err := commonbackend.Marshal(response) + + if err != nil { + log.Error("failed to marshal response: %s", err.Error()) + continue + } + + helper.socket.Write(responseMarshalled) + case *commonbackend.BackendStatusRequest: + ok, err := helper.Backend.GetBackendStatus() + + var ( + message string + statusCode int + ) + + if err != nil { + message = err.Error() + statusCode = commonbackend.StatusFailure + } else { + statusCode = commonbackend.StatusSuccess + } + + response := &commonbackend.BackendStatusResponse{ + IsRunning: ok, + StatusCode: statusCode, + Message: message, + } + + responseMarshalled, err := commonbackend.Marshal(response) + + if err != nil { + log.Error("failed to marshal response: %s", err.Error()) + continue + } + + helper.socket.Write(responseMarshalled) + case *commonbackend.AddProxy: + id, ok, err := helper.Backend.StartProxy(command) + var hasAnyFailed bool + + if !ok { + log.Warnf("failed to add proxy (%s:%d -> remote:%d): StartProxy returned into failure state", command.SourceIP, command.SourcePort, command.DestPort) + hasAnyFailed = true + } else if err != nil { + log.Warnf("failed to add proxy (%s:%d -> remote:%d): %s", command.SourceIP, command.SourcePort, command.DestPort, err.Error()) + hasAnyFailed = true + } + + response := &datacommands.ProxyStatusResponse{ + ProxyID: id, + IsActive: !hasAnyFailed, + } + + responseMarshalled, err := datacommands.Marshal(response) + + if err != nil { + log.Error("failed to marshal response: %s", err.Error()) + continue + } + + helper.socket.Write(responseMarshalled) + case *commonbackend.CheckClientParameters: + resp := helper.Backend.CheckParametersForConnections(command) + resp.InResponseTo = "checkClientParameters" + + byteData, err := commonbackend.Marshal(resp) if err != nil { return err } - switch command := commandRaw.(type) { - case *commonbackend.Start: - ok, err := helper.Backend.StartBackend(command.Arguments) - - var ( - message string - statusCode int - ) - - if err != nil { - message = err.Error() - statusCode = commonbackend.StatusFailure - } else { - statusCode = commonbackend.StatusSuccess - } - - response := &commonbackend.BackendStatusResponse{ - IsRunning: ok, - StatusCode: statusCode, - Message: message, - } - - responseMarshalled, err := commonbackend.Marshal(response) - - if err != nil { - log.Error("failed to marshal response: %s", err.Error()) - continue - } - - helper.socket.Write(responseMarshalled) - case *commonbackend.Stop: - ok, err := helper.Backend.StopBackend() - - var ( - message string - statusCode int - ) - - if err != nil { - message = err.Error() - statusCode = commonbackend.StatusFailure - } else { - statusCode = commonbackend.StatusSuccess - } - - response := &commonbackend.BackendStatusResponse{ - IsRunning: !ok, - StatusCode: statusCode, - Message: message, - } - - responseMarshalled, err := commonbackend.Marshal(response) - - if err != nil { - log.Error("failed to marshal response: %s", err.Error()) - continue - } - - helper.socket.Write(responseMarshalled) - case *commonbackend.BackendStatusRequest: - ok, err := helper.Backend.GetBackendStatus() - - var ( - message string - statusCode int - ) - - if err != nil { - message = err.Error() - statusCode = commonbackend.StatusFailure - } else { - statusCode = commonbackend.StatusSuccess - } - - response := &commonbackend.BackendStatusResponse{ - IsRunning: ok, - StatusCode: statusCode, - Message: message, - } - - responseMarshalled, err := commonbackend.Marshal(response) - - if err != nil { - log.Error("failed to marshal response: %s", err.Error()) - continue - } - - helper.socket.Write(responseMarshalled) - case *commonbackend.AddProxy: - id, ok, err := helper.Backend.StartProxy(command) - var hasAnyFailed bool - - if !ok { - log.Warnf("failed to add proxy (%s:%d -> remote:%d): StartProxy returned into failure state", command.SourceIP, command.SourcePort, command.DestPort) - hasAnyFailed = true - } else if err != nil { - log.Warnf("failed to add proxy (%s:%d -> remote:%d): %s", command.SourceIP, command.SourcePort, command.DestPort, err.Error()) - hasAnyFailed = true - } - - response := &datacommands.ProxyStatusResponse{ - ProxyID: id, - IsActive: !hasAnyFailed, - } - - responseMarshalled, err := datacommands.Marshal(response) - - if err != nil { - log.Error("failed to marshal response: %s", err.Error()) - continue - } - - helper.socket.Write(responseMarshalled) - case *commonbackend.CheckClientParameters: - resp := helper.Backend.CheckParametersForConnections(command) - resp.InResponseTo = "checkClientParameters" - - byteData, err := commonbackend.Marshal(resp) - - if err != nil { - return err - } - - if _, err = helper.socket.Write(byteData); err != nil { - return err - } - case *commonbackend.CheckServerParameters: - resp := helper.Backend.CheckParametersForBackend(command.Arguments) - resp.InResponseTo = "checkServerParameters" - - byteData, err := commonbackend.Marshal(resp) - - if err != nil { - return err - } - - if _, err = helper.socket.Write(byteData); err != nil { - return err - } - default: - log.Warnf("Unsupported command recieved: %T", command) + if _, err = helper.socket.Write(byteData); err != nil { + return err } + case *commonbackend.CheckServerParameters: + resp := helper.Backend.CheckParametersForBackend(command.Arguments) + resp.InResponseTo = "checkServerParameters" + + byteData, err := commonbackend.Marshal(resp) + + if err != nil { + return err + } + + if _, err = helper.socket.Write(byteData); err != nil { + return err + } + default: + log.Warnf("Unsupported command recieved: %T", command) } } } diff --git a/backend/sshappbackend/remote-code/main.go b/backend/sshappbackend/remote-code/main.go index a1d9bb7..d56a7a3 100644 --- a/backend/sshappbackend/remote-code/main.go +++ b/backend/sshappbackend/remote-code/main.go @@ -36,6 +36,8 @@ type SSHRemoteAppBackend struct { tcpProxies map[uint16]*TCPProxy udpProxies map[uint16]*UDPProxy + isRunning bool + sock net.Conn } @@ -43,6 +45,8 @@ func (backend *SSHRemoteAppBackend) StartBackend(byte []byte) (bool, error) { backend.tcpProxies = map[uint16]*TCPProxy{} backend.udpProxies = map[uint16]*UDPProxy{} + backend.isRunning = true + return true, nil } @@ -61,11 +65,12 @@ func (backend *SSHRemoteAppBackend) StopBackend() (bool, error) { delete(backend.udpProxies, udpProxyIndex) } + backend.isRunning = false return true, nil } func (backend *SSHRemoteAppBackend) GetBackendStatus() (bool, error) { - return true, nil + return backend.isRunning, nil } func (backend *SSHRemoteAppBackend) StartProxy(command *commonbackend.AddProxy) (uint16, bool, error) { @@ -104,6 +109,8 @@ func (backend *SSHRemoteAppBackend) StartProxy(command *commonbackend.AddProxy) backend.tcpProxies[proxyID].connectionIDIndex++ backend.tcpProxies[proxyID].connectionIDLock.Unlock() + backend.tcpProxies[proxyID].connections[connectionID] = conn + dataBuf := make([]byte, 65535) onConnection := &datacommands.TCPConnectionOpened{ @@ -372,12 +379,14 @@ func (backend *SSHRemoteAppBackend) HandleTCPMessage(message *datacommands.TCPPr tcpProxy, ok := backend.tcpProxies[message.ProxyID] if !ok { + log.Warnf("could not find tcp proxy (ID %d)", message.ProxyID) return } connection, ok := tcpProxy.connections[message.ConnectionID] if !ok { + log.Warnf("could not find tcp proxy (ID %d) with connection ID (%d)", message.ProxyID, message.ConnectionID) return }