Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
65c8452f3c | ||
|
|
b26271a4f9 |
@@ -19,8 +19,22 @@ namespace MonsterMonitor.Models
|
||||
public bool SavePassword { get; set; } = false;
|
||||
public int RemotePort { get; set; } = 3328;
|
||||
public int LocalPort { get; set; } = 7829;
|
||||
// Устарело: раньше использовалось как порог молчания heartbeat в секундах,
|
||||
// но код брал Math.Max(5, MaxPingFailures), поэтому значение <5 ничего не меняло.
|
||||
// Оставлено для совместимости со старыми settings.ini, см. HeartbeatSilenceSec.
|
||||
public int MaxPingFailures { get; set; } = 3;
|
||||
public int ReconnectTimeoutSec { get; set; } = 45;
|
||||
|
||||
// Сколько секунд молчания heartbeat считать подозрительным. Само по себе
|
||||
// молчание больше НЕ рвёт соединение - оно лишь повод сделать e2e-проверку.
|
||||
// Большая выгрузка легко забивает канал на десятки секунд.
|
||||
public int HeartbeatSilenceSec { get; set; } = 60;
|
||||
|
||||
// e2e-проверка проброса: раз в сколько секунд, таймаут и сколько подряд
|
||||
// неудач требуется, чтобы переподключаться.
|
||||
public int ProbeIntervalSec { get; set; } = 30;
|
||||
public int ProbeTimeoutSec { get; set; } = 10;
|
||||
public int ProbeFailuresBeforeReconnect { get; set; } = 2;
|
||||
public string Proxy { get; set; } = string.Empty;
|
||||
public string SsProcessPath { get; set; } = Path.Combine("App_Data", "ss", "ss.exe");
|
||||
public string SsArguments { get; set; } = string.Empty;
|
||||
@@ -164,6 +178,10 @@ namespace MonsterMonitor.Models
|
||||
result.LocalPort = GetInt(map, nameof(LocalPort), result.LocalPort);
|
||||
result.MaxPingFailures = GetInt(map, nameof(MaxPingFailures), result.MaxPingFailures);
|
||||
result.ReconnectTimeoutSec = GetInt(map, nameof(ReconnectTimeoutSec), result.ReconnectTimeoutSec);
|
||||
result.HeartbeatSilenceSec = GetInt(map, nameof(HeartbeatSilenceSec), result.HeartbeatSilenceSec);
|
||||
result.ProbeIntervalSec = GetInt(map, nameof(ProbeIntervalSec), result.ProbeIntervalSec);
|
||||
result.ProbeTimeoutSec = GetInt(map, nameof(ProbeTimeoutSec), result.ProbeTimeoutSec);
|
||||
result.ProbeFailuresBeforeReconnect = GetInt(map, nameof(ProbeFailuresBeforeReconnect), result.ProbeFailuresBeforeReconnect);
|
||||
result.Proxy = Get(map, nameof(Proxy), result.Proxy);
|
||||
result.SsProcessPath = Get(map, nameof(SsProcessPath), result.SsProcessPath);
|
||||
result.SsArguments = Get(map, nameof(SsArguments), result.SsArguments);
|
||||
@@ -193,6 +211,10 @@ namespace MonsterMonitor.Models
|
||||
$"{nameof(LocalPort)}={LocalPort}",
|
||||
$"{nameof(MaxPingFailures)}={MaxPingFailures}",
|
||||
$"{nameof(ReconnectTimeoutSec)}={ReconnectTimeoutSec}",
|
||||
$"{nameof(HeartbeatSilenceSec)}={HeartbeatSilenceSec}",
|
||||
$"{nameof(ProbeIntervalSec)}={ProbeIntervalSec}",
|
||||
$"{nameof(ProbeTimeoutSec)}={ProbeTimeoutSec}",
|
||||
$"{nameof(ProbeFailuresBeforeReconnect)}={ProbeFailuresBeforeReconnect}",
|
||||
$"{nameof(Proxy)}={Proxy}",
|
||||
$"{nameof(SsProcessPath)}={SsProcessPath}",
|
||||
$"{nameof(SsArguments)}={SsArguments}"
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MonsterMonitor.Models;
|
||||
@@ -14,6 +16,13 @@ namespace MonsterMonitor.Services
|
||||
private readonly object _sync = new object();
|
||||
private SshClient _client;
|
||||
private ForwardedPortRemote _forwardedPort;
|
||||
// Локальный проброс на серверный RemotePort: даёт замкнутый e2e-маршрут
|
||||
// local -> SSH -> сервер:RemotePort -> обратный проброс -> наш LocalPort.
|
||||
private ForwardedPortLocal _probePort;
|
||||
private int _probeLocalPort;
|
||||
private int _probeFailures;
|
||||
private DateTime _lastProbeUtc = DateTime.MinValue;
|
||||
private DateTime _lastGoodProbeUtc = DateTime.MinValue;
|
||||
private SshCommand _heartbeatCommand;
|
||||
private CancellationTokenSource _heartbeatReadTokenSource;
|
||||
private Task _heartbeatReadTask;
|
||||
@@ -63,7 +72,11 @@ namespace MonsterMonitor.Services
|
||||
_forwardedPort.Exception += ForwardedPortOnException;
|
||||
_client.AddForwardedPort(_forwardedPort);
|
||||
_forwardedPort.Start();
|
||||
StartProbePortNoLock();
|
||||
StartRemoteHeartbeatNoLock();
|
||||
_probeFailures = 0;
|
||||
_lastProbeUtc = DateTime.UtcNow;
|
||||
_lastGoodProbeUtc = DateTime.UtcNow;
|
||||
|
||||
_log.Info(
|
||||
$"SSH подключен. Туннель remote:{_settings.RemotePort} -> local:{_settings.LocalPort}");
|
||||
@@ -94,20 +107,63 @@ namespace MonsterMonitor.Services
|
||||
{
|
||||
try
|
||||
{
|
||||
var silenceThresholdSec = Math.Max(
|
||||
5,
|
||||
_settings.MaxPingFailures);
|
||||
if (!IsConnected())
|
||||
{
|
||||
_log.Warn("SSH-сессия не подключена. Переподключаю.");
|
||||
await Reconnect().ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
var silenceThresholdSec = Math.Max(5, _settings.HeartbeatSilenceSec);
|
||||
var probeIntervalSec = Math.Max(5, _settings.ProbeIntervalSec);
|
||||
|
||||
var lastHeartbeat = _lastHeartbeatUtc;
|
||||
var isHeartbeatAlive = lastHeartbeat != DateTime.MinValue &&
|
||||
(DateTime.UtcNow - lastHeartbeat).TotalSeconds <= silenceThresholdSec;
|
||||
var heartbeatSilentSec = lastHeartbeat == DateTime.MinValue
|
||||
? double.MaxValue
|
||||
: (now - lastHeartbeat).TotalSeconds;
|
||||
var heartbeatSuspicious = heartbeatSilentSec > silenceThresholdSec;
|
||||
|
||||
if (!IsConnected() || !isHeartbeatAlive)
|
||||
// Heartbeat живёт в том же SSH-соединении, что и полезный трафик,
|
||||
// поэтому под большой выгрузкой он опаздывает - это НЕ повод рвать
|
||||
// туннель. Решение принимает только e2e-проверка проброса.
|
||||
var needProbe = heartbeatSuspicious ||
|
||||
(now - _lastProbeUtc).TotalSeconds >= probeIntervalSec;
|
||||
|
||||
if (needProbe)
|
||||
{
|
||||
_log.Warn("Нет живого вывода heartbeat-команды на удаленном сервере. Переподключаю SSH.");
|
||||
_lastProbeUtc = now;
|
||||
var ok = await ProbeForwardAsync().ConfigureAwait(false);
|
||||
if (ok)
|
||||
{
|
||||
if (_probeFailures > 0)
|
||||
{
|
||||
_log.Info("e2e-проверка проброса снова проходит.");
|
||||
}
|
||||
|
||||
_probeFailures = 0;
|
||||
_lastGoodProbeUtc = DateTime.UtcNow;
|
||||
}
|
||||
else
|
||||
{
|
||||
_probeFailures++;
|
||||
_log.Warn($"e2e-проверка проброса не прошла ({_probeFailures} подряд).");
|
||||
}
|
||||
}
|
||||
|
||||
if (heartbeatSuspicious && _probeFailures == 0)
|
||||
{
|
||||
_log.Debug(
|
||||
$"Heartbeat молчит {heartbeatSilentSec:F0}с, но проброс работает - вероятно идёт передача. Не переподключаю.");
|
||||
}
|
||||
|
||||
if (_probeFailures >= Math.Max(1, _settings.ProbeFailuresBeforeReconnect))
|
||||
{
|
||||
_log.Warn("Проброс портов не пропускает трафик. Переподключаю SSH.");
|
||||
await Reconnect().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.Warn("Исключение мониторинга heartbeat: " + ex.Message);
|
||||
@@ -183,6 +239,7 @@ namespace MonsterMonitor.Services
|
||||
private void DisconnectCore()
|
||||
{
|
||||
StopRemoteHeartbeatNoLock();
|
||||
StopProbePortNoLock();
|
||||
|
||||
try
|
||||
{
|
||||
@@ -219,6 +276,113 @@ namespace MonsterMonitor.Services
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Поднимает локальный проброс на серверный RemotePort. Подключение к нему
|
||||
/// проходит весь маршрут целиком: наш процесс -> SSH -> слушатель обратного
|
||||
/// проброса на сервере -> SSH обратно -> наш LocalPort (ss.exe).
|
||||
/// Успешный TCP-connect означает, что проброс реально пропускает трафик,
|
||||
/// а не просто "сессия жива".
|
||||
/// </summary>
|
||||
private void StartProbePortNoLock()
|
||||
{
|
||||
StopProbePortNoLock();
|
||||
|
||||
try
|
||||
{
|
||||
_probeLocalPort = FindFreeLocalPort();
|
||||
_probePort = new ForwardedPortLocal(
|
||||
"127.0.0.1",
|
||||
(uint)_probeLocalPort,
|
||||
"127.0.0.1",
|
||||
(uint)_settings.RemotePort);
|
||||
_client.AddForwardedPort(_probePort);
|
||||
_probePort.Start();
|
||||
_log.Info($"e2e-проверка проброса включена (127.0.0.1:{_probeLocalPort}).");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_probePort = null;
|
||||
_log.Warn("Не удалось поднять порт e2e-проверки: " + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private void StopProbePortNoLock()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_probePort != null)
|
||||
{
|
||||
if (_probePort.IsStarted)
|
||||
{
|
||||
_probePort.Stop();
|
||||
}
|
||||
|
||||
_probePort.Dispose();
|
||||
_probePort = null;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore errors on shutdown.
|
||||
}
|
||||
}
|
||||
|
||||
private static int FindFreeLocalPort()
|
||||
{
|
||||
var listener = new TcpListener(IPAddress.Loopback, 0);
|
||||
listener.Start();
|
||||
try
|
||||
{
|
||||
return ((IPEndPoint)listener.LocalEndpoint).Port;
|
||||
}
|
||||
finally
|
||||
{
|
||||
listener.Stop();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> ProbeForwardAsync()
|
||||
{
|
||||
ForwardedPortLocal probe;
|
||||
int port;
|
||||
lock (_sync)
|
||||
{
|
||||
probe = _probePort;
|
||||
port = _probeLocalPort;
|
||||
}
|
||||
|
||||
if (probe == null || !probe.IsStarted || port <= 0)
|
||||
{
|
||||
// Порт проверки поднять не удалось - не выдумываем отказ проброса,
|
||||
// иначе будем рвать рабочий туннель из-за собственной диагностики.
|
||||
return true;
|
||||
}
|
||||
|
||||
var timeoutMs = Math.Max(1, _settings.ProbeTimeoutSec) * 1000;
|
||||
|
||||
try
|
||||
{
|
||||
using (var tcp = new TcpClient())
|
||||
{
|
||||
var connectTask = tcp.ConnectAsync(IPAddress.Loopback, port);
|
||||
var completed = await Task.WhenAny(connectTask, Task.Delay(timeoutMs)).ConfigureAwait(false);
|
||||
if (completed != connectTask)
|
||||
{
|
||||
_log.Debug("e2e-проверка: таймаут подключения через проброс.");
|
||||
return false;
|
||||
}
|
||||
|
||||
await connectTask.ConfigureAwait(false);
|
||||
return tcp.Connected;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.Debug("e2e-проверка: " + ex.Message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void StartRemoteHeartbeatNoLock()
|
||||
{
|
||||
if (_client == null || !_client.IsConnected)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Security.Cryptography;
|
||||
using System.Windows.Forms;
|
||||
@@ -64,7 +65,7 @@ namespace MonsterMonitor.UI
|
||||
AddRow(panel, string.Empty, _chkSavePassword, 4);
|
||||
AddRow(panel, "Удаленный порт:", _numRemotePort, 5);
|
||||
AddRow(panel, "Локальный порт:", _numLocalPort, 6);
|
||||
AddRow(panel, "Макс. потерь:", _numMaxFailures, 7);
|
||||
AddRow(panel, "Молчание heartbeat (сек):", _numMaxFailures, 7);
|
||||
AddRow(panel, "Таймаут reconnect (сек):", _numReconnectTimeout, 8);
|
||||
AddRow(panel, "Прокси (http://host:port):", _txtProxy, 9);
|
||||
AddRow(panel, "Путь к ss:", _txtSsPath, 10);
|
||||
@@ -78,8 +79,8 @@ namespace MonsterMonitor.UI
|
||||
num.Maximum = 65535;
|
||||
}
|
||||
|
||||
_numMaxFailures.Minimum = 1;
|
||||
_numMaxFailures.Maximum = 10;
|
||||
_numMaxFailures.Minimum = 10;
|
||||
_numMaxFailures.Maximum = 600;
|
||||
_numReconnectTimeout.Minimum = 5;
|
||||
_numReconnectTimeout.Maximum = 60;
|
||||
|
||||
@@ -200,7 +201,7 @@ namespace MonsterMonitor.UI
|
||||
_chkSavePassword.Checked = _settings.SavePassword;
|
||||
_numRemotePort.Value = _settings.RemotePort;
|
||||
_numLocalPort.Value = _settings.LocalPort;
|
||||
_numMaxFailures.Value = _settings.MaxPingFailures;
|
||||
_numMaxFailures.Value = Math.Min(600, Math.Max(10, _settings.HeartbeatSilenceSec));
|
||||
_numReconnectTimeout.Value = _settings.ReconnectTimeoutSec;
|
||||
_txtProxy.Text = _settings.Proxy;
|
||||
_txtSsPath.Text = _settings.SsProcessPath;
|
||||
@@ -224,7 +225,7 @@ namespace MonsterMonitor.UI
|
||||
_settings.SetPassword(_txtPassword.Text);
|
||||
_settings.RemotePort = (int)_numRemotePort.Value;
|
||||
_settings.LocalPort = (int)_numLocalPort.Value;
|
||||
_settings.MaxPingFailures = (int)_numMaxFailures.Value;
|
||||
_settings.HeartbeatSilenceSec = (int)_numMaxFailures.Value;
|
||||
_settings.ReconnectTimeoutSec = (int)_numReconnectTimeout.Value;
|
||||
_settings.Proxy = _txtProxy.Text.Trim();
|
||||
_settings.SsProcessPath = string.IsNullOrWhiteSpace(_txtSsPath.Text)
|
||||
|
||||
Reference in New Issue
Block a user