#include "DMShell.h" #if defined(ARCH_PORTDUINO) #include "Channels.h" #include "MeshService.h" #include "NodeDB.h" #include "Throttle.h" #include "configuration.h" #include "mesh/generated/meshtastic/mesh.pb.h" #include "mesh/mesh-pb-constants.h" #include "pb_decode.h" #include "pb_encode.h" #include #include #include #include #include #include #include #include #include #include DMShellModule *dmShellModule; namespace { constexpr uint16_t PTY_COLS_DEFAULT = 120; constexpr uint16_t PTY_ROWS_DEFAULT = 40; constexpr size_t MAX_PTY_READ_SIZE = 200; struct BytesDecodeState { uint8_t *buf; size_t maxLen; size_t outLen; }; struct BytesEncodeState { const uint8_t *buf; size_t len; }; bool decodeBytesField(pb_istream_t *stream, const pb_field_iter_t *field, void **arg) { (void)field; auto *state = static_cast(*arg); const size_t fieldLen = stream->bytes_left; if (fieldLen > state->maxLen) { return false; } if (!pb_read(stream, state->buf, fieldLen)) { return false; } state->outLen = fieldLen; return true; } bool encodeBytesField(pb_ostream_t *stream, const pb_field_iter_t *field, void *const *arg) { auto *state = static_cast(*arg); if (!state || !state->buf || state->len == 0) { return true; } if (!pb_encode_tag_for_field(stream, field)) { return false; } return pb_encode_string(stream, state->buf, state->len); } } // namespace DMShellModule::DMShellModule() : SinglePortModule("DMShellModule", meshtastic_PortNum_DM_SHELL_APP), concurrency::OSThread("DMShell", 100) { LOG_WARN("DMShell enabled on Portduino: remote shell access is dangerous and intended for trusted debugging only"); } ProcessMessage DMShellModule::handleReceived(const meshtastic_MeshPacket &mp) { DMShellFrame frame; if (!parseFrame(mp, frame)) { LOG_WARN("DMShell: ignoring malformed frame"); return ProcessMessage::STOP; } if (!isAuthorizedPacket(mp)) { LOG_WARN("DMShell: unauthorized sender 0x%x", mp.from); myReply = allocErrorResponse(meshtastic_Routing_Error_NOT_AUTHORIZED, &mp); return ProcessMessage::STOP; } if (frame.op == meshtastic_DMShell_OpCode_OPEN) { LOG_WARN("DMShell: received OPEN from 0x%x sessionId=0x%x", mp.from, frame.sessionId); if (!openSession(mp, frame)) { const char *msg = "open_failed"; sendFrameToPeer(getFrom(&mp), mp.channel, meshtastic_DMShell_OpCode_ERROR, frame.sessionId, frame.seq, reinterpret_cast(msg), strlen(msg)); } return ProcessMessage::STOP; } if (!session.active || frame.sessionId != session.sessionId || getFrom(&mp) != session.peer) { const char *msg = "invalid_session"; sendFrameToPeer(getFrom(&mp), mp.channel, meshtastic_DMShell_OpCode_ERROR, frame.sessionId, frame.seq, reinterpret_cast(msg), strlen(msg)); return ProcessMessage::STOP; } session.lastActivityMs = millis(); switch (frame.op) { case meshtastic_DMShell_OpCode_INPUT: if (!writeSessionInput(frame)) { sendError("input_write_failed"); } break; case meshtastic_DMShell_OpCode_RESIZE: if (frame.rows > 0 && frame.cols > 0) { struct winsize ws = {}; ws.ws_row = frame.rows; ws.ws_col = frame.cols; if (session.masterFd >= 0) { ioctl(session.masterFd, TIOCSWINSZ, &ws); } } break; case meshtastic_DMShell_OpCode_PING: sendControl(meshtastic_DMShell_OpCode_PONG, nullptr, 0, frame.seq); break; case meshtastic_DMShell_OpCode_CLOSE: closeSession("peer_close", true); break; default: sendError("unsupported_op"); break; } return ProcessMessage::STOP; } int32_t DMShellModule::runOnce() { if (!session.active) { return 100; } reapChildIfExited(); if (!session.active) { return 100; } if (Throttle::isWithinTimespanMs(session.lastActivityMs, SESSION_IDLE_TIMEOUT_MS) == false) { closeSession("idle_timeout", true); return 100; } uint8_t outBuf[MAX_PTY_READ_SIZE]; while (session.masterFd >= 0) { const ssize_t bytesRead = read(session.masterFd, outBuf, sizeof(outBuf)); if (bytesRead > 0) { LOG_WARN("DMShell: read %d bytes from PTY", bytesRead); sendControl(meshtastic_DMShell_OpCode_OUTPUT, outBuf, static_cast(bytesRead), session.txSeq++); session.lastActivityMs = millis(); continue; } if (bytesRead == 0) { closeSession("pty_eof", true); break; } if (errno == EAGAIN || errno == EWOULDBLOCK) { break; } LOG_WARN("DMShell: PTY read error errno=%d", errno); closeSession("pty_read_error", true); break; } return 100; } bool DMShellModule::parseFrame(const meshtastic_MeshPacket &mp, DMShellFrame &outFrame) { if (mp.which_payload_variant != meshtastic_MeshPacket_decoded_tag) { return false; } meshtastic_DMShell decodedMsg = meshtastic_DMShell_init_zero; if (pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, meshtastic_DMShell_fields, &decodedMsg)) { LOG_INFO("Received a DMShell message"); } else { LOG_ERROR("Error decoding DMShell message!"); return false; } outFrame.op = decodedMsg.op; outFrame.sessionId = decodedMsg.session_id; outFrame.seq = decodedMsg.seq; outFrame.ackSeq = decodedMsg.ack_seq; outFrame.cols = decodedMsg.cols; outFrame.rows = decodedMsg.rows; outFrame.flags = decodedMsg.flags; outFrame.payloadLen = decodedMsg.payload.size; memcpy(outFrame.payload, decodedMsg.payload.bytes, outFrame.payloadLen); return true; } bool DMShellModule::isAuthorizedPacket(const meshtastic_MeshPacket &mp) const { if (mp.from == 0) { return !config.security.is_managed; } const meshtastic_Channel *ch = &channels.getByIndex(mp.channel); if (strcasecmp(ch->settings.name, Channels::adminChannel) == 0) { return config.security.admin_channel_enabled; } if (mp.pki_encrypted) { for (uint8_t i = 0; i < 3; ++i) { if (config.security.admin_key[i].size == 32 && memcmp(mp.public_key.bytes, config.security.admin_key[i].bytes, 32) == 0) { return true; } } } return false; } bool DMShellModule::openSession(const meshtastic_MeshPacket &mp, const DMShellFrame &frame) { if (session.active) { closeSession("preempted", true); } int masterFd = -1; struct winsize ws = {}; ws.ws_col = PTY_COLS_DEFAULT; ws.ws_row = PTY_ROWS_DEFAULT; const pid_t childPid = forkpty(&masterFd, nullptr, nullptr, &ws); if (childPid < 0) { LOG_ERROR("DMShell: forkpty failed errno=%d", errno); return false; } if (childPid == 0) { const char *shell = getenv("SHELL"); if (!shell || !*shell) { shell = "/bin/sh"; } execl(shell, shell, "-i", static_cast(nullptr)); _exit(127); } const int flags = fcntl(masterFd, F_GETFL, 0); if (flags >= 0) { fcntl(masterFd, F_SETFL, flags | O_NONBLOCK); } session.active = true; session.sessionId = (frame.sessionId != 0) ? frame.sessionId : static_cast(random(1, 0x7fffffff)); session.peer = getFrom(&mp); session.channel = mp.channel; session.masterFd = masterFd; session.childPid = childPid; session.txSeq = 0; session.lastActivityMs = millis(); uint8_t payload[sizeof(uint32_t)] = {0}; uint32_t pidBE = (static_cast(session.childPid) << 24) | ((static_cast(session.childPid) >> 8) & 0xff00) | ((static_cast(session.childPid) << 8) & 0xff0000) | (static_cast(session.childPid) >> 24); memcpy(payload, &pidBE, sizeof(payload)); sendFrameToPeer(session.peer, session.channel, meshtastic_DMShell_OpCode_OPEN_OK, session.sessionId, frame.seq, payload, sizeof(payload), PTY_COLS_DEFAULT, PTY_ROWS_DEFAULT); LOG_INFO("DMShell: opened session=0x%x peer=0x%x pid=%d", session.sessionId, session.peer, session.childPid); return true; } bool DMShellModule::writeSessionInput(const DMShellFrame &frame) { if (session.masterFd < 0) { return false; } if (frame.payloadLen == 0) { return true; } const ssize_t bytesWritten = write(session.masterFd, frame.payload, frame.payloadLen); return bytesWritten >= 0; } void DMShellModule::closeSession(const char *reason, bool notifyPeer) { if (!session.active) { return; } if (notifyPeer) { const size_t reasonLen = strnlen(reason, 256); sendControl(meshtastic_DMShell_OpCode_CLOSED, reinterpret_cast(reason), reasonLen, session.txSeq++); } if (session.masterFd >= 0) { close(session.masterFd); session.masterFd = -1; } if (session.childPid > 0) { kill(session.childPid, SIGTERM); int status = 0; waitpid(session.childPid, &status, WNOHANG); session.childPid = -1; } LOG_INFO("DMShell: closed session=0x%x reason=%s", session.sessionId, reason); session = DMShellSession{}; } void DMShellModule::reapChildIfExited() { if (!session.active || session.childPid <= 0) { return; } int status = 0; const pid_t result = waitpid(session.childPid, &status, WNOHANG); if (result == session.childPid) { closeSession("shell_exited", true); } } void DMShellModule::sendControl(meshtastic_DMShell_OpCode op, const uint8_t *payload, size_t payloadLen, uint32_t seq) { sendFrameToPeer(session.peer, session.channel, op, session.sessionId, seq, payload, payloadLen); } void DMShellModule::sendFrameToPeer(NodeNum peer, uint8_t channel, meshtastic_DMShell_OpCode op, uint32_t sessionId, uint32_t seq, const uint8_t *payload, size_t payloadLen, uint32_t cols, uint32_t rows, uint32_t ackSeq, uint32_t flags) { meshtastic_MeshPacket *packet = buildFramePacket(op, sessionId, seq, payload, payloadLen, cols, rows, ackSeq, flags); if (!packet) { return; } packet->to = peer; packet->channel = channel; packet->want_ack = false; packet->priority = meshtastic_MeshPacket_Priority_RELIABLE; service->sendToMesh(packet); } void DMShellModule::sendError(const char *message) { const size_t len = strnlen(message, meshtastic_Constants_DATA_PAYLOAD_LEN); sendControl(meshtastic_DMShell_OpCode_ERROR, reinterpret_cast(message), len, session.txSeq++); } meshtastic_MeshPacket *DMShellModule::buildFramePacket(meshtastic_DMShell_OpCode op, uint32_t sessionId, uint32_t seq, const uint8_t *payload, size_t payloadLen, uint32_t cols, uint32_t rows, uint32_t ackSeq, uint32_t flags) { meshtastic_DMShell frame = meshtastic_DMShell_init_zero; frame.op = op; frame.session_id = sessionId; frame.seq = seq; frame.ack_seq = ackSeq; frame.cols = cols; frame.rows = rows; frame.flags = flags; if (payload && payloadLen > 0) { memcpy(frame.payload.bytes, payload, payloadLen); frame.payload.size = payloadLen; } meshtastic_MeshPacket *packet = allocDataPacket(); if (!packet) { return nullptr; } LOG_WARN("DMShell: building packet op=%d session=0x%x seq=%d payloadLen=%d", op, sessionId, seq, payloadLen); const size_t encoded = pb_encode_to_bytes(packet->decoded.payload.bytes, sizeof(packet->decoded.payload.bytes), meshtastic_DMShell_fields, &frame); if (encoded == 0) { return nullptr; } packet->decoded.payload.size = encoded; return packet; } #endif