6 Commits
Author SHA1 Message Date
yuanyuanxianganddeepseek-v4-pro d3a339247b Fix: reconnect after server restart without reinstalling the loader
The S_SERVER_EXIT respawn fix previously lived in test.cpp, which a
deployed TestRun.exe cannot pick up without reinstalling. Move the
reconnect decision into ClientDll::IsStoped(): when the master instance
is in S_SERVER_EXIT, report reconnectable immediately instead of waiting
for live shared instances to drain, so the already-deployed loader
reconnects against a hot-loaded DLL.

Shared instances stay connected to their own servers, preserving the
IsSharedRunning contract that the master's state change must not tear
them down.

Also guard the RUNNING_PARALLEL branch so re-entering StartClient on
reconnect spawns parallel instances once instead of duplicating them.

Co-Authored-By: deepseek-v4-pro
2026-10-16 21:36:52 +02:00
yuanyuanxianganddeepseek-v4-pro 33986f4893 Fix: shared-host respawn block and web login brand placeholder
Two unrelated fixes bundled here.

1. Client respawn (ClientDll.cpp EasyRun/Run, test.cpp Run): after a host
   is shared or transferred to another server, restarting the master
   (server A) left the host's main instance stuck, because the respawn
   loop waited on IsStoped() (g_bThreadExit && GetCount()==0), which never
   becomes true while a still-alive shared instance holds AddCount(1).
   S_SERVER_EXIT now triggers an immediate reconnect without waiting for
   shared instances; client-exit / update still waits for all instances to
   drain. Verified by 100 restart cycles.

2. Web login page (WebService.cpp): the MFC web server only substituted
   __BASE_PATH__ in index.html and left __BRAND_NAME__ as a literal token
   in the login page title/heading. Substitute it with "SimpleRemoter" to
   match the original branding and the Go server's default.

Co-Authored-By: deepseek-v4-pro
2026-10-16 21:08:42 +02:00
yuanyuanxianganddeepseek-v4-pro e5f5720867 Fix: Add missing FileManagerDlg translations to language files
FileManagerDlg.cpp still had several user-facing Chinese strings with no
entry in en_US/zh_TW/de_DE: the "invalid file handle", "no files
selected", "V2 transfer started", "V2 download request sent" and "main
connection not found" messages. Add translations for all five.

The "main connection not found" string contained a literal '=', which
CIniParser treats as the key/value delimiter, so it could never match as
a translation key. Reword it to "clientID %llu" (no '=') so the key
round-trips. The two Mprintf log lines on the V2 checksum path are
log-only and intentionally left untranslated.

Co-Authored-By: deepseek-v4-pro
2026-10-16 13:54:00 +02:00
yuanyuanxianganddeepseek-v4-pro d7b49a3d06 Feat: Add whitelist action to log-list right-click menu
Adds a "白名单" item after "解除封禁" in the message-log context menu.
It extracts the IP from each selected log entry, asks for confirmation,
then adds it to the shared IPWhitelist singleton — which IOCPServer and
the DLL rate limiter already consult live — so the entry takes effect on
new connections immediately, without a restart. The whitelist is
persisted to settings\IPWhitelist the same way the Network dialog does.

- An IP already in the whitelist is reported as such and skipped.
- An IP in the IP blacklist (banned) is reported as banned and skipped,
  rather than silently removed from the blacklist.
- Menu item, confirmation, and result strings are added to the
  en_US/zh_TW/de_DE language files.

Co-Authored-By: deepseek-v4-pro
2026-10-16 13:16:33 +02:00
yuanyuanxianganddeepseek-v4-pro c85039ea0a Fix: Online-host list column-toggle leaves ghost artifact after last column
CListCtrlEx::OnEraseBkgnd (with m_bSkipEraseBkgnd) only erases the empty
area below the last item, not the horizontal strip right of the last
column (the ~20px scrollbar reserve AdjustColumnWidths always leaves).
Changing the column count via SetColumnVisible/ToggleColumnVisibility
redistributed widths but never repainted with background erase, so stale
pixels from the previous last column lingered next to the new one (e.g.
after "TLS") until a tab switch or minimize/restore forced a full redraw.

Mirror the proven OnSize needRefresh path: temporarily lift the
skip-erase flag and issue a full RedrawWindow with
RDW_ERASE|RDW_INVALIDATE|RDW_ALLCHILDREN|RDW_UPDATENOW after each
visibility change.

Co-Authored-By: deepseek-v4-pro
2026-10-16 13:03:05 +02:00
yuanyuanxianganddeepseek-v4-pro a447452521 Fix: Silence SOCKET_DLLLOADER log flood and tighten device-port logging
SOCKET_DLLLOADER (210) is the shellcode / memory-DLL loader asking the
server for the full DLL payload before it can load the client. The MFC
master console serves it via SendServerDll; the Go web device port has no
payload to hand out, so every memory-DLL client startup/reconnect fell
into the default dispatch branch and logged "Unhandled command 210",
flooding the log. Add a protocol constant and drop the command silently.

While auditing the device-port logging surface:

- Downgrade wrapTLS's per-connection hex-dump lines and the screen-spy
  "[timing]" cold-start line from Info to Debug (they fire once per TCP
  connection / screen view and are diagnostics, not operational events).

- Distinguish a sub-connection hijack attempt from a plain orphan: when a
  screen/terminal sub-conn claims a MasterID from a source IP other than
  the device's login conn, log a dedicated Warn instead of mislabeling it
  as "orphan ... unknown device".

Co-Authored-By: deepseek-v4-pro
2026-10-16 12:26:35 +02:00
15 changed files with 176 additions and 19 deletions

No files matched your search

+19 -9
View File
@@ -445,6 +445,11 @@ extern "C" __declspec(dllexport) void StopRun()
// 是否成功停止 // 是否成功停止
extern "C" __declspec(dllexport) bool IsStoped() extern "C" __declspec(dllexport) bool IsStoped()
{ {
// 主控端重启(S_SERVER_EXIT):主实例工作线程已结束,即使分享实例仍在运行,
// 也视为「可重连」。旧 TestRun 加载循环靠它立即重连,无需卸载/重装 TestRun.exe;
// 分享实例继续存活,不受主控端重启影响。
if (S_SERVER_EXIT == g_MyApp.g_bExit)
return true;
return g_MyApp.g_bThreadExit && ClientApp::GetCount() == 0; return g_MyApp.g_bThreadExit && ClientApp::GetCount() == 0;
} }
@@ -462,13 +467,11 @@ extern "C" __declspec(dllexport) int EasyRun()
do { do {
TestRun((char*)settings.ServerIP(), settings.ServerPort()); TestRun((char*)settings.ServerIP(), settings.ServerPort());
if (S_SERVER_EXIT == app.g_bExit)
continue; // 主控端重启:立即重连,不等仍存活的分享实例
// 受控端退出 / 程序更新:等所有实例(含分享实例)退干净再退出
while (!IsStoped()) while (!IsStoped())
Sleep(50); Sleep(50);
if (S_CLIENT_EXIT == app.g_bExit) // 受控端退出
break;
else if (S_SERVER_EXIT == app.g_bExit)
continue;
else // S_CLIENT_UPDATE: 程序更新
break; break;
} while (true); } while (true);
@@ -555,14 +558,14 @@ extern "C" __declspec(dllexport) void Run(HWND hwnd, HINSTANCE hinst, LPSTR lpsz
do { do {
TestRun((char*)result[0].c_str(), atoi(result[1].c_str())); TestRun((char*)result[0].c_str(), atoi(result[1].c_str()));
if (bExit == S_SERVER_EXIT)
continue; // 主控端重启:立即重连,不等仍存活的分享实例
// 受控端退出 / 程序更新:等所有实例(含分享实例)退干净
while (!IsStoped()) while (!IsStoped())
Sleep(20); Sleep(20);
if (bExit == S_CLIENT_EXIT) if (bExit == S_CLIENT_EXIT)
return; return;
else if (bExit == S_SERVER_EXIT) break; // S_CLIENT_UPDATE
continue;
else // S_CLIENT_UPDATE
break;
} while (true); } while (true);
sprintf_s(message, "%s:%d", settings.ServerIP(), settings.ServerPort()); sprintf_s(message, "%s:%d", settings.ServerIP(), settings.ServerPort());
@@ -608,11 +611,18 @@ DWORD WINAPI StartClient(LPVOID lParam)
} }
auto list = app.GetSharedMasterList(); auto list = app.GetSharedMasterList();
if (list.size() > 1 && settings.runningType == RUNNING_PARALLEL) { if (list.size() > 1 && settings.runningType == RUNNING_PARALLEL) {
// 并发上线的并行实例只派生一次:主控端重启重连时会再次进入 StartClient,
// 若不守护会重复派生并行连接。主实例的 SetServer(list[0]) 保持每次执行,
// 让主实例始终指向域名解析出的第一个 IP。
static bool parallelHasRun = false;
if (!parallelHasRun) {
parallelHasRun = true;
for (int i=1; i<list.size(); ++i) { for (int i=1; i<list.size(); ++i) {
std::string addr = list[i] + ":" + std::to_string(settings.ServerPort()); std::string addr = list[i] + ":" + std::to_string(settings.ServerPort());
auto a = NewClientStartArg(addr.c_str(), IsSharedRunning, FALSE); auto a = NewClientStartArg(addr.c_str(), IsSharedRunning, FALSE);
if (nullptr != a) CloseHandle(__CreateThread(0, 0, StartClientApp, a, 0, 0)); if (nullptr != a) CloseHandle(__CreateThread(0, 0, StartClientApp, a, 0, 0));
} }
}
// The main ClientApp. // The main ClientApp.
settings.SetServer(list[0].c_str(), settings.ServerPort()); settings.SetServer(list[0].c_str(), settings.ServerPort());
} }
+4
View File
@@ -495,8 +495,12 @@ BOOL Run(const char* argv1, int argv2, const std::string &runCmd)
} }
do { do {
run(ip, port); run(ip, port);
if (bExit && bExit() != S_SERVER_EXIT) {
// 受控端退出 / 程序更新:等所有实例(含分享实例)退干净,避免下方 FreeLibrary
// 卸载 DLL 时残留分享实例线程。主控端重启(S_SERVER_EXIT)则立即重连,不等仍存活的分享实例。
while (bStop && !bStop() && 0 == status) while (bStop && !bStop() && 0 == status)
Sleep(20); Sleep(20);
}
} while (bExit && !bExit() && 0 == status); } while (bExit && !bExit() && 0 == status);
while (bStop && !bStop() && 1 == status) while (bStop && !bStop() && 1 == status)
+80 -1
View File
@@ -1036,6 +1036,7 @@ BEGIN_MESSAGE_MAP(CMy2015RemoteDlg, CDialogEx)
ON_COMMAND(ID_MSGLOG_LOGIN_NOTIFY, &CMy2015RemoteDlg::OnMsglogLoginNotify) ON_COMMAND(ID_MSGLOG_LOGIN_NOTIFY, &CMy2015RemoteDlg::OnMsglogLoginNotify)
ON_COMMAND(ID_MSGLOG_BAN_IP, &CMy2015RemoteDlg::OnMsglogBanIP) ON_COMMAND(ID_MSGLOG_BAN_IP, &CMy2015RemoteDlg::OnMsglogBanIP)
ON_COMMAND(ID_MSGLOG_UNBAN_IP, &CMy2015RemoteDlg::OnMsglogUnbanIP) ON_COMMAND(ID_MSGLOG_UNBAN_IP, &CMy2015RemoteDlg::OnMsglogUnbanIP)
ON_COMMAND(ID_MSGLOG_WHITELIST_IP, &CMy2015RemoteDlg::OnMsglogWhitelistIP)
ON_COMMAND(ID_ONLINE_ADD_WATCH, &CMy2015RemoteDlg::OnOnlineAddWatch) ON_COMMAND(ID_ONLINE_ADD_WATCH, &CMy2015RemoteDlg::OnOnlineAddWatch)
ON_COMMAND(ID_ONLINE_LOGIN_NOTIFY, &CMy2015RemoteDlg::OnOnlineLoginNotify) ON_COMMAND(ID_ONLINE_LOGIN_NOTIFY, &CMy2015RemoteDlg::OnOnlineLoginNotify)
ON_NOTIFY(NM_CUSTOMDRAW, IDC_ONLINE, &CMy2015RemoteDlg::OnNMCustomdrawOnline) ON_NOTIFY(NM_CUSTOMDRAW, IDC_ONLINE, &CMy2015RemoteDlg::OnNMCustomdrawOnline)
@@ -10236,14 +10237,16 @@ void CMy2015RemoteDlg::OnRClickMessage(NMHDR* pNMHDR, LRESULT* pResult)
menu.AppendMenu(MF_STRING, ID_MSGLOG_LOGIN_NOTIFY, _TR("上线提醒")); menu.AppendMenu(MF_STRING, ID_MSGLOG_LOGIN_NOTIFY, _TR("上线提醒"));
menu.AppendMenu(MF_STRING, ID_MSGLOG_BAN_IP, _TR("封禁IP")); menu.AppendMenu(MF_STRING, ID_MSGLOG_BAN_IP, _TR("封禁IP"));
menu.AppendMenu(MF_STRING, ID_MSGLOG_UNBAN_IP, _TR("解除封禁")); menu.AppendMenu(MF_STRING, ID_MSGLOG_UNBAN_IP, _TR("解除封禁"));
menu.AppendMenu(MF_STRING, ID_MSGLOG_WHITELIST_IP, _TR("白名单"));
// 没有选中项时禁用"删除选中"、"复制选中"、"上线提醒"、"封禁IP"和"解除封禁" // 没有选中项时禁用"删除选中"、"复制选中"、"上线提醒"、"封禁IP"、"解除封禁"和"白名单"
if (m_CList_Message.GetSelectedCount() == 0) { if (m_CList_Message.GetSelectedCount() == 0) {
menu.EnableMenuItem(ID_MSGLOG_DELETE, MF_GRAYED); menu.EnableMenuItem(ID_MSGLOG_DELETE, MF_GRAYED);
menu.EnableMenuItem(ID_MSGLOG_COPY, MF_GRAYED); menu.EnableMenuItem(ID_MSGLOG_COPY, MF_GRAYED);
menu.EnableMenuItem(ID_MSGLOG_LOGIN_NOTIFY, MF_GRAYED); menu.EnableMenuItem(ID_MSGLOG_LOGIN_NOTIFY, MF_GRAYED);
menu.EnableMenuItem(ID_MSGLOG_BAN_IP, MF_GRAYED); menu.EnableMenuItem(ID_MSGLOG_BAN_IP, MF_GRAYED);
menu.EnableMenuItem(ID_MSGLOG_UNBAN_IP, MF_GRAYED); menu.EnableMenuItem(ID_MSGLOG_UNBAN_IP, MF_GRAYED);
menu.EnableMenuItem(ID_MSGLOG_WHITELIST_IP, MF_GRAYED);
} }
// PowerShell不可用时禁用"上线提醒" // PowerShell不可用时禁用"上线提醒"
if (!GetNotifyManager().IsPowerShellAvailable()) { if (!GetNotifyManager().IsPowerShellAvailable()) {
@@ -10630,6 +10633,82 @@ void CMy2015RemoteDlg::OnMsglogUnbanIP()
MessageBoxL(details, _TR("解除封禁"), MB_ICONINFORMATION); MessageBoxL(details, _TR("解除封禁"), MB_ICONINFORMATION);
} }
void CMy2015RemoteDlg::OnMsglogWhitelistIP()
{
// Get selected log messages and extract IP addresses from them
std::vector<std::string> ipsToAdd;
POSITION pos = m_CList_Message.GetFirstSelectedItemPosition();
while (pos) {
int row = m_CList_Message.GetNextSelectedItem(pos);
// Column 2 is the message content ("信息内容")
CString msgText = m_CList_Message.GetItemText(row, 2);
CString extractedIP;
if (ExtractFirstIPFromMessage(msgText, extractedIP)) {
std::string ipUtf8 = CT2A(extractedIP, CP_UTF8);
ipsToAdd.push_back(ipUtf8);
}
}
if (ipsToAdd.empty()) {
MessageBoxL(_TR("所选日志消息中未检测到有效的 IP 地址"), _TR("提示"), MB_ICONINFORMATION);
return;
}
// 二次确认
if (IDYES != MessageBoxL(_TR("确定将选中日志中的 IP 加入白名单吗?"), _TR("提示"), MB_ICONQUESTION | MB_YESNO)) {
return;
}
// Process each extracted IP
int addedCount = 0;
int alreadyCount = 0;
int bannedCount = 0;
for (const auto& ip : ipsToAdd) {
// Already whitelisted
if (IPWhitelist::getInstance().IsWhitelisted(ip)) {
alreadyCount++;
continue;
}
// Banned IPs cannot be whitelisted
if (IPBlacklist::getInstance().IsBlacklisted(ip)) {
bannedCount++;
continue;
}
IPWhitelist::getInstance().AddIP(ip);
addedCount++;
}
// Persist to config
if (addedCount > 0) {
std::string whitelistStr = IPWhitelist::getInstance().Export();
THIS_CFG.SetStr("settings", "IPWhitelist", whitelistStr.c_str());
}
// Build result message
CString details;
if (addedCount > 0) {
details.AppendFormat(_TR("已将 %d 个 IP 加入白名单"), addedCount);
}
if (alreadyCount > 0) {
if (!details.IsEmpty()) details += _T("\n");
details.AppendFormat(_TR("%d 个 IP 已在白名单中"), alreadyCount);
}
if (bannedCount > 0) {
if (!details.IsEmpty()) details += _T("\n");
details.AppendFormat(_TR("%d 个 IP 已封禁,无法加入白名单"), bannedCount);
}
if (details.IsEmpty()) {
details = _TR("未执行任何操作");
}
MessageBoxL(details, _TR("白名单"), MB_ICONINFORMATION);
}
void CMy2015RemoteDlg::OnOnlineAddWatch() void CMy2015RemoteDlg::OnOnlineAddWatch()
{ {
EnterCriticalSection(&m_cs); EnterCriticalSection(&m_cs);
+1
View File
@@ -630,6 +630,7 @@ public:
afx_msg void OnMsglogLoginNotify(); afx_msg void OnMsglogLoginNotify();
afx_msg void OnMsglogBanIP(); afx_msg void OnMsglogBanIP();
afx_msg void OnMsglogUnbanIP(); afx_msg void OnMsglogUnbanIP();
afx_msg void OnMsglogWhitelistIP();
afx_msg void OnOnlineAddWatch(); afx_msg void OnOnlineAddWatch();
afx_msg void OnNMCustomdrawOnline(NMHDR* pNMHDR, LRESULT* pResult); afx_msg void OnNMCustomdrawOnline(NMHDR* pNMHDR, LRESULT* pResult);
afx_msg void OnOnlineRunAsAdmin(); afx_msg void OnOnlineRunAsAdmin();
+8
View File
@@ -160,6 +160,9 @@ void CListCtrlEx::SetColumnVisible(int nCol, BOOL bVisible)
if (m_Columns[nCol].CanHide || bVisible) { // 不允许隐藏的列只能设为可见 if (m_Columns[nCol].CanHide || bVisible) { // 不允许隐藏的列只能设为可见
m_Columns[nCol].Visible = bVisible; m_Columns[nCol].Visible = bVisible;
AdjustColumnWidths(); AdjustColumnWidths();
// 列数变化后完整重绘(含背景擦除),否则最后一列右侧会残留旧列残影。
ScopedEraseBkgnd scope(*this);
RedrawWindow(NULL, NULL, RDW_ERASE | RDW_INVALIDATE | RDW_ALLCHILDREN | RDW_UPDATENOW);
} }
} }
} }
@@ -230,6 +233,11 @@ void CListCtrlEx::ToggleColumnVisibility(int nColumn)
// 重新调整列宽 // 重新调整列宽
AdjustColumnWidths(); AdjustColumnWidths();
// 列数变化后完整重绘(含背景擦除),否则最后一列(TLS 等)右侧会残留旧列残影。
// 与 OnSize 中 needRefresh 分支相同的处理:临时放行背景擦除 + 全量 RedrawWindow。
ScopedEraseBkgnd scope(*this);
RedrawWindow(NULL, NULL, RDW_ERASE | RDW_INVALIDATE | RDW_ALLCHILDREN | RDW_UPDATENOW);
} }
void CListCtrlEx::LoadColumnVisibility() void CListCtrlEx::LoadColumnVisibility()
+1 -1
View File
@@ -3358,7 +3358,7 @@ void CFileManagerDlg::OnTransferV2ToRemote()
g_2015RemoteDlg->SendFilesToClientV2(mainCtx, files, remoteDir); g_2015RemoteDlg->SendFilesToClientV2(mainCtx, files, remoteDir);
ShowMessage(_TRF("V2传输已启动,共 %d 个文件 -> %s"), (int)files.size(), remoteDir.c_str()); ShowMessage(_TRF("V2传输已启动,共 %d 个文件 -> %s"), (int)files.size(), remoteDir.c_str());
} else { } else {
ShowMessage(_TRF("找不到主连接: clientID=%llu"), clientID); ShowMessage(_TRF("找不到主连接: clientID %llu"), clientID);
} }
} }
} }
+5
View File
@@ -245,6 +245,11 @@ void CWebService::ServerThread(int port) {
// server's subpath deployment. The MFC server serves from the domain root, // server's subpath deployment. The MFC server serves from the domain root,
// so strip it (→ "") — otherwise the WebSocket/CSS/JS URLs break. // so strip it (→ "") — otherwise the WebSocket/CSS/JS URLs break.
ReplaceAll(cachedHtml, "__BASE_PATH__", ""); ReplaceAll(cachedHtml, "__BASE_PATH__", "");
// server/web/index.html also carries a __BRAND_NAME__ placeholder for the
// white-label title. The Go server swaps it at serve time (defaulting to
// "SimpleRemoter"); the MFC server does the same here, otherwise the login
// page shows the literal "__BRAND_NAME__" token as <title>/<h1>.
ReplaceAll(cachedHtml, "__BRAND_NAME__", "SimpleRemoter");
// Log loaded size; zero bytes means the RC BINARY resource is missing or // Log loaded size; zero bytes means the RC BINARY resource is missing or
// server/web/index.html was not embedded — check 2015Remote.rc and rebuild. // server/web/index.html was not embedded — check 2015Remote.rc and rebuild.
Mprintf("[WebService] index.html loaded: %zu bytes\n", cachedHtml.size()); Mprintf("[WebService] index.html loaded: %zu bytes\n", cachedHtml.size());
+10
View File
@@ -2087,3 +2087,13 @@ TLS
确定取消选中主机的 TLS 传输吗?(客户端下次启动生效)=TLS-Transport fuer die ausgewaehlten Hosts deaktivieren? (wirkt nach Client-Neustart) 确定取消选中主机的 TLS 传输吗?(客户端下次启动生效)=TLS-Transport fuer die ausgewaehlten Hosts deaktivieren? (wirkt nach Client-Neustart)
服务端未启用 TLS 证书,无法切换客户端 TLS 传输=Server-TLS-Zertifikat ist nicht aktiviert; TLS-Transport der Clients kann nicht umgeschaltet werden 服务端未启用 TLS 证书,无法切换客户端 TLS 传输=Server-TLS-Zertifikat ist nicht aktiviert; TLS-Transport der Clients kann nicht umgeschaltet werden
读取服务端 TLS 证书失败,无法下发=Server-TLS-Zertifikat konnte nicht gelesen werden; Verteilung nicht moeglich 读取服务端 TLS 证书失败,无法下发=Server-TLS-Zertifikat konnte nicht gelesen werden; Verteilung nicht moeglich
白名单=Whitelist
确定将选中日志中的 IP 加入白名单吗?=IP aus den ausgewaehlten Log-Eintraegen zur Whitelist hinzufuegen?
已将 %d 个 IP 加入白名单=%d IP(s) zur Whitelist hinzugefuegt
%d 个 IP 已在白名单中=%d IP(s) bereits in der Whitelist
%d 个 IP 已封禁,无法加入白名单=%d IP(s) sind gesperrt und koennen nicht zur Whitelist hinzugefuegt werden
%s 文件句柄无效=%s ungueltiges Datei-Handle
没有选中任何文件=Keine Dateien ausgewaehlt
V2传输已启动,共 %d 个文件 -> %s=V2-Uebertragung gestartet, %d Datei(en) -> %s
V2下载请求已发送,共 %d 个项目=V2-Download-Anfrage gesendet, %d Element(e)
找不到主连接: clientID %llu=Hauptverbindung nicht gefunden: clientID=%llu
+10
View File
@@ -2102,3 +2102,13 @@ TLS
确定取消选中主机的 TLS 传输吗?(客户端下次启动生效)=Disable TLS transport for the selected host? (takes effect after the client restarts) 确定取消选中主机的 TLS 传输吗?(客户端下次启动生效)=Disable TLS transport for the selected host? (takes effect after the client restarts)
服务端未启用 TLS 证书,无法切换客户端 TLS 传输=Server TLS certificate is not enabled; cannot toggle client TLS transport 服务端未启用 TLS 证书,无法切换客户端 TLS 传输=Server TLS certificate is not enabled; cannot toggle client TLS transport
读取服务端 TLS 证书失败,无法下发=Failed to read the server TLS certificate; cannot distribute 读取服务端 TLS 证书失败,无法下发=Failed to read the server TLS certificate; cannot distribute
白名单=Whitelist
确定将选中日志中的 IP 加入白名单吗?=Add the IP from the selected log entries to the whitelist?
已将 %d 个 IP 加入白名单=Added %d IP(s) to the whitelist
%d 个 IP 已在白名单中=%d IP(s) already in the whitelist
%d 个 IP 已封禁,无法加入白名单=%d IP(s) are banned and cannot be added to the whitelist
%s 文件句柄无效=%s invalid file handle
没有选中任何文件=No files selected
V2传输已启动,共 %d 个文件 -> %s=V2 transfer started, %d file(s) -> %s
V2下载请求已发送,共 %d 个项目=V2 download request sent, %d item(s)
找不到主连接: clientID %llu=Main connection not found: clientID=%llu
+10
View File
@@ -2093,3 +2093,13 @@ TLS
确定取消选中主机的 TLS 传输吗?(客户端下次启动生效)=確定取消選取主機的 TLS 傳輸嗎?(用戶端下次啟動生效) 确定取消选中主机的 TLS 传输吗?(客户端下次启动生效)=確定取消選取主機的 TLS 傳輸嗎?(用戶端下次啟動生效)
服务端未启用 TLS 证书,无法切换客户端 TLS 传输=伺服器未啟用 TLS 憑證,無法切換用戶端 TLS 傳輸 服务端未启用 TLS 证书,无法切换客户端 TLS 传输=伺服器未啟用 TLS 憑證,無法切換用戶端 TLS 傳輸
读取服务端 TLS 证书失败,无法下发=讀取伺服器 TLS 憑證失敗,無法下發 读取服务端 TLS 证书失败,无法下发=讀取伺服器 TLS 憑證失敗,無法下發
白名单=白名單
确定将选中日志中的 IP 加入白名单吗?=確定將選取日誌中的 IP 加入白名單嗎?
已将 %d 个 IP 加入白名单=已將 %d 個 IP 加入白名單
%d 个 IP 已在白名单中=%d 個 IP 已在白名單中
%d 个 IP 已封禁,无法加入白名单=%d 個 IP 已封禁,無法加入白名單
%s 文件句柄无效=%s 檔案控制代碼無效
没有选中任何文件=沒有選取任何檔案
V2传输已启动,共 %d 个文件 -> %s=V2 傳輸已啟動,共 %d 個檔案 -> %s
V2下载请求已发送,共 %d 个项目=V2 下載請求已傳送,共 %d 個項目
找不到主连接: clientID %llu=找不到主連線: clientID=%llu
+2 -1
View File
@@ -1010,6 +1010,7 @@
#define ID_MSGLOG_LOGIN_NOTIFY 33079 #define ID_MSGLOG_LOGIN_NOTIFY 33079
#define ID_MSGLOG_BAN_IP 33080 #define ID_MSGLOG_BAN_IP 33080
#define ID_MSGLOG_UNBAN_IP 33081 #define ID_MSGLOG_UNBAN_IP 33081
#define ID_MSGLOG_WHITELIST_IP 33096
#define ID_WEB_REMOTE_CONTROL 33044 #define ID_WEB_REMOTE_CONTROL 33044
#define ID_TOOL_PLUGIN_SETTINGS 33045 #define ID_TOOL_PLUGIN_SETTINGS 33045
#define ID_MCP_SETTINGS 33077 #define ID_MCP_SETTINGS 33077
@@ -1120,7 +1121,7 @@
#ifdef APSTUDIO_INVOKED #ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS #ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NEXT_RESOURCE_VALUE 412 #define _APS_NEXT_RESOURCE_VALUE 412
#define _APS_NEXT_COMMAND_VALUE 33096 #define _APS_NEXT_COMMAND_VALUE 33097
#define _APS_NEXT_CONTROL_VALUE 2620 #define _APS_NEXT_CONTROL_VALUE 2620
#define _APS_NEXT_SYMED_VALUE 105 #define _APS_NEXT_SYMED_VALUE 105
#endif #endif
+22 -4
View File
@@ -146,6 +146,11 @@ func (h *MyHandler) OnReceive(ctx *connection.Context, data []byte) {
// overlay learns to render arbitrary BGRA images. Drop silently for // overlay learns to render arbitrary BGRA images. Drop silently for
// now; the standard IDC_* index (data[10] of every frame header) is // now; the standard IDC_* index (data[10] of every frame header) is
// what we actually use right now. // what we actually use right now.
case protocol.CmdSockDllLoader:
// SOCKET_DLLLOADER (210): the shellcode / memory-DLL loader asks for the
// full DLL payload before it can load the client. Serving it is the MFC
// 主控端's job (server/2015Remote SendServerDll); this web device port has
// no payload to hand out, so drop it silently instead of logging "Unhandled".
default: default:
// Other commands are not implemented yet // Other commands are not implemented yet
h.log.Info("Unhandled command %d from client %d", cmd, ctx.ID) h.log.Info("Unhandled command %d from client %d", cmd, ctx.ID)
@@ -290,10 +295,16 @@ func (h *MyHandler) handleTerminalStart(ctx *connection.Context, isPTY bool) {
return return
} }
if !h.hub.BindTerminalConn(devID, ctx, isPTY) { if !h.hub.BindTerminalConn(devID, ctx, isPTY) {
// No pending session — this is a stale sub-conn (e.g. browser // Either a stale sub-conn (no pending session — browser gave up and
// gave up and closed term_close already). Drop it. // closed term_close already), or a hijack attempt from a source IP
// other than the device's login conn. Log the latter distinctly.
if mc := h.hub.MainConn(devID); mc != nil && mc.GetPeerIP() != ctx.GetPeerIP() {
h.log.Warn("terminal sub-conn rejected (foreign source IP): device=%s conn=%d src=%s want=%s",
devID, ctx.ID, ctx.GetPeerIP(), mc.GetPeerIP())
} else {
h.log.Warn("orphan terminal sub-conn: device=%s conn=%d isPTY=%v", h.log.Warn("orphan terminal sub-conn: device=%s conn=%d isPTY=%v",
devID, ctx.ID, isPTY) devID, ctx.ID, isPTY)
}
ctx.Close() ctx.Close()
return return
} }
@@ -326,9 +337,16 @@ func (h *MyHandler) handleBitmapInfo(ctx *connection.Context, data []byte) {
deviceID := strconv.FormatUint(clientID, 10) deviceID := strconv.FormatUint(clientID, 10)
if !h.hub.BindScreenConn(deviceID, ctx) { if !h.hub.BindScreenConn(deviceID, ctx) {
// Device not registered — main login hasn't happened (or device just // A failed bind is either a plain orphan (device not registered, or it
// went offline). Drop the orphan sub-conn rather than leak it. // just went offline) or a hijack attempt — a sub-conn claiming a
// MasterID from a different source IP than the device's login conn.
// The latter is a security signal worth surfacing distinctly.
if mc := h.hub.MainConn(deviceID); mc != nil && mc.GetPeerIP() != ctx.GetPeerIP() {
h.log.Warn("screen sub-conn rejected (foreign source IP): device=%s conn=%d src=%s want=%s",
deviceID, ctx.ID, ctx.GetPeerIP(), mc.GetPeerIP())
} else {
h.log.Warn("orphan screen sub-conn %d for unknown device %s; closing", ctx.ID, deviceID) h.log.Warn("orphan screen sub-conn %d for unknown device %s; closing", ctx.ID, deviceID)
}
ctx.Close() ctx.Close()
return return
} }
+1
View File
@@ -126,6 +126,7 @@ const (
TokenTerminalClose byte = 233 // TOKEN_TERMINAL_CLOSE - shell exited / close ack TokenTerminalClose byte = 233 // TOKEN_TERMINAL_CLOSE - shell exited / close ack
TokenConnAuth byte = 246 // TOKEN_CONN_AUTH - sub-connection identity handshake TokenConnAuth byte = 246 // TOKEN_CONN_AUTH - sub-connection identity handshake
CmdCursorImage byte = 93 // CMD_CURSOR_IMAGE - custom cursor bitmap (Phase 5+ feature) CmdCursorImage byte = 93 // CMD_CURSOR_IMAGE - custom cursor bitmap (Phase 5+ feature)
CmdSockDllLoader byte = 210 // SOCKET_DLLLOADER - shellcode/memory-DLL loader requests the full DLL payload
) )
// Sub-connection authentication (matches common/commands.h ConnAuth* structs). // Sub-connection authentication (matches common/commands.h ConnAuth* structs).
+2 -2
View File
@@ -105,9 +105,9 @@ func (s *Server) wrapTLS(conn net.Conn) net.Conn {
bc := &bufferedConn{Conn: conn, r: br} bc := &bufferedConn{Conn: conn, r: br}
if isTLSClientHello(hdr) { if isTLSClientHello(hdr) {
s.logger.Info("wrapTLS: TLS peer %s first6=%x", conn.RemoteAddr(), hdr) s.logger.Debug("wrapTLS: TLS peer %s first6=%x", conn.RemoteAddr(), hdr)
return tls.Server(bc, s.tlsCfg) return tls.Server(bc, s.tlsCfg)
} }
s.logger.Info("wrapTLS: plaintext peer %s first6=%x", conn.RemoteAddr(), hdr) s.logger.Debug("wrapTLS: plaintext peer %s first6=%x", conn.RemoteAddr(), hdr)
return bc return bc
} }
+1 -1
View File
@@ -339,7 +339,7 @@ func (h *wsHub) handleConnect(c *wsClient, raw []byte) {
c.queue(mustJSON(map[string]any{"cmd": "connect_result", "ok": false, "msg": msg})) c.queue(mustJSON(map[string]any{"cmd": "connect_result", "ok": false, "msg": msg}))
return return
} }
h.log.Info("[timing] COMMAND_SCREEN_SPY sent to device=%s (cold start)", in.ID) h.log.Debug("[timing] COMMAND_SCREEN_SPY sent to device=%s (cold start)", in.ID)
c.queue(mustJSON(map[string]any{"cmd": "connect_result", "ok": true})) c.queue(mustJSON(map[string]any{"cmd": "connect_result", "ok": true}))
} }