diff --git a/shadowsocks-csharp/Controller/Logging.cs b/shadowsocks-csharp/Controller/Logging.cs index 81f16dd8..145547bb 100755 --- a/shadowsocks-csharp/Controller/Logging.cs +++ b/shadowsocks-csharp/Controller/Logging.cs @@ -31,6 +31,14 @@ namespace Shadowsocks.Controller } } + public static void Debug(object o) + { + +#if DEBUG + Console.WriteLine(o); +#endif + } + public static void LogUsefulException(Exception e) { // just log useful exceptions, not all of them diff --git a/shadowsocks-csharp/Controller/Service/TCPRelay.cs b/shadowsocks-csharp/Controller/Service/TCPRelay.cs index 49abc6db..8e753a0b 100644 --- a/shadowsocks-csharp/Controller/Service/TCPRelay.cs +++ b/shadowsocks-csharp/Controller/Service/TCPRelay.cs @@ -34,6 +34,7 @@ namespace Shadowsocks.Controller Server server = _controller.GetCurrentStrategy().GetAServer(IStrategyCallerType.TCP, (IPEndPoint)socket.RemoteEndPoint); handler.encryptor = EncryptorFactory.GetEncryptor(server.method, server.password); handler.server = server; + handler.controller = _controller; handler.Start(firstPacket, length); return true; @@ -48,6 +49,7 @@ namespace Shadowsocks.Controller // Client socket. public Socket remote; public Socket connection; + public ShadowsocksController controller; private byte command; private byte[] _firstPacket; @@ -55,6 +57,10 @@ namespace Shadowsocks.Controller // Size of receive buffer. public const int RecvSize = 16384; public const int BufferSize = RecvSize + 32; + + private int totalRead = 0; + private int totalWrite = 0; + // remote receive buffer private byte[] remoteRecvBuffer = new byte[RecvSize]; // remote send buffer @@ -72,6 +78,8 @@ namespace Shadowsocks.Controller private object encryptionLock = new object(); private object decryptionLock = new object(); + private DateTime _startConnectTime; + public void Start(byte[] firstPacket, int length) { this._firstPacket = firstPacket; @@ -305,6 +313,8 @@ namespace Shadowsocks.Controller SocketType.Stream, ProtocolType.Tcp); remote.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.NoDelay, true); + _startConnectTime = DateTime.Now; + // Connect to the remote endpoint. remote.BeginConnect(remoteEP, new AsyncCallback(ConnectCallback), null); @@ -330,10 +340,14 @@ namespace Shadowsocks.Controller //Console.WriteLine("Socket connected to {0}", // remote.RemoteEndPoint.ToString()); + var latency = DateTime.Now - _startConnectTime; + controller.GetCurrentStrategy().UpdateLatency(this.server, latency); + StartPipe(); } catch (Exception e) { + controller.GetCurrentStrategy().SetFailure(this.server); Logging.LogUsefulException(e); this.Close(); } @@ -368,6 +382,7 @@ namespace Shadowsocks.Controller try { int bytesRead = remote.EndReceive(ar); + totalRead += bytesRead; if (bytesRead > 0) { @@ -381,6 +396,8 @@ namespace Shadowsocks.Controller encryptor.Decrypt(remoteRecvBuffer, bytesRead, remoteSendBuffer, out bytesToSend); } connection.BeginSend(remoteSendBuffer, 0, bytesToSend, 0, new AsyncCallback(PipeConnectionSendCallback), null); + + controller.GetCurrentStrategy().UpdateLastRead(this.server); } else { @@ -388,6 +405,12 @@ namespace Shadowsocks.Controller connection.Shutdown(SocketShutdown.Send); connectionShutdown = true; CheckClose(); + + if (totalRead == 0) + { + // closed before anything received, reports as failure + controller.GetCurrentStrategy().SetFailure(this.server); + } } } catch (Exception e) @@ -406,6 +429,7 @@ namespace Shadowsocks.Controller try { int bytesRead = connection.EndReceive(ar); + totalWrite += bytesRead; if (bytesRead > 0) { @@ -419,6 +443,8 @@ namespace Shadowsocks.Controller encryptor.Encrypt(connetionRecvBuffer, bytesRead, connetionSendBuffer, out bytesToSend); } remote.BeginSend(connetionSendBuffer, 0, bytesToSend, 0, new AsyncCallback(PipeRemoteSendCallback), null); + + controller.GetCurrentStrategy().UpdateLastWrite(this.server); } else { diff --git a/shadowsocks-csharp/Controller/ShadowsocksController.cs b/shadowsocks-csharp/Controller/ShadowsocksController.cs index a3e56391..19637c3d 100755 --- a/shadowsocks-csharp/Controller/ShadowsocksController.cs +++ b/shadowsocks-csharp/Controller/ShadowsocksController.cs @@ -282,6 +282,11 @@ namespace Shadowsocks.Controller polipoRunner.Stop(); try { + foreach (var strategy in GetStrategies()) + { + strategy.ReloadServers(); + } + polipoRunner.Start(_config); TCPRelay tcpRelay = new TCPRelay(this); diff --git a/shadowsocks-csharp/Controller/Strategy/BalancingStrategy.cs b/shadowsocks-csharp/Controller/Strategy/BalancingStrategy.cs index 1e4f8a9a..15d4920f 100644 --- a/shadowsocks-csharp/Controller/Strategy/BalancingStrategy.cs +++ b/shadowsocks-csharp/Controller/Strategy/BalancingStrategy.cs @@ -20,20 +20,19 @@ namespace Shadowsocks.Controller.Strategy public string Name { - get - { - return "Load Balance"; - } + get { return I18N.GetString("Load Balance"); } } public string ID { - get - { - return "com.shadowsocks.strategy.balancing"; - } + get { return "com.shadowsocks.strategy.balancing"; } } - + + public void ReloadServers() + { + // do nothing + } + public Server GetAServer(IStrategyCallerType type, IPEndPoint localIPEndPoint) { var configs = _controller.GetCurrentConfiguration().configs; @@ -49,7 +48,7 @@ namespace Shadowsocks.Controller.Strategy return configs[index % configs.Count]; } - public void UpdateLatency(Model.Server server) + public void UpdateLatency(Model.Server server, TimeSpan latency) { // do nothing } diff --git a/shadowsocks-csharp/Controller/Strategy/HighAvailabilityStrategy.cs b/shadowsocks-csharp/Controller/Strategy/HighAvailabilityStrategy.cs new file mode 100644 index 00000000..f54c8eac --- /dev/null +++ b/shadowsocks-csharp/Controller/Strategy/HighAvailabilityStrategy.cs @@ -0,0 +1,126 @@ +using Shadowsocks.Model; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Shadowsocks.Controller.Strategy +{ + class HighAvailabilityStrategy : IStrategy + { + protected Server _currentServer; + protected Dictionary _serverStatus; + ShadowsocksController _controller; + Random _random; + + public class ServerStatus + { + // time interval between SYN and SYN+ACK + public TimeSpan latency; + + // last time anything received + public DateTime lastRead; + + // last time anything sent + public DateTime lastWrite; + + // connection refused or closed before anything received + public DateTime lastFailure; + + public Server server; + } + + /** + * if last failure is > 10 min + * and (last write > last read) and (now - last read < 5s) // means not stuck + * choose the lowest latency + */ + + public HighAvailabilityStrategy(ShadowsocksController controller) + { + _controller = controller; + _random = new Random(); + _serverStatus = new Dictionary(); + } + + public string Name + { + get { return I18N.GetString("High Availability"); } + } + + public string ID + { + get { return "com.shadowsocks.strategy.ha"; } + } + + public void ReloadServers() + { + // make a copy to avoid locking + var newServerStatus = new Dictionary(_serverStatus); + + foreach (var server in _controller.GetCurrentConfiguration().configs) + { + if (!newServerStatus.ContainsKey(server)) + { + var status = new ServerStatus(); + status.server = server; + newServerStatus[server] = status; + } + } + _serverStatus = newServerStatus; + + // just leave removed servers there + + // TODO + _currentServer = _controller.GetCurrentConfiguration().configs[0]; + } + + public Server GetAServer(IStrategyCallerType type, System.Net.IPEndPoint localIPEndPoint) + { + return _currentServer; + } + + public void UpdateLatency(Model.Server server, TimeSpan latency) + { + Logging.Debug(String.Format("latency: {0} {1}", server.FriendlyName(), latency)); + + ServerStatus status; + if (_serverStatus.TryGetValue(server, out status)) + { + status.latency = latency; + } + } + + public void UpdateLastRead(Model.Server server) + { + Logging.Debug(String.Format("last read: {0}", server.FriendlyName())); + + ServerStatus status; + if (_serverStatus.TryGetValue(server, out status)) + { + status.lastRead = DateTime.Now; + } + } + + public void UpdateLastWrite(Model.Server server) + { + Logging.Debug(String.Format("last write: {0}", server.FriendlyName())); + + ServerStatus status; + if (_serverStatus.TryGetValue(server, out status)) + { + status.lastWrite = DateTime.Now; + } + } + + public void SetFailure(Model.Server server) + { + Logging.Debug(String.Format("failure: {0}", server.FriendlyName())); + + ServerStatus status; + if (_serverStatus.TryGetValue(server, out status)) + { + status.lastFailure = DateTime.Now; + } + } + } +} diff --git a/shadowsocks-csharp/Controller/Strategy/IStrategy.cs b/shadowsocks-csharp/Controller/Strategy/IStrategy.cs index 8e4f65a0..ca3036fb 100644 --- a/shadowsocks-csharp/Controller/Strategy/IStrategy.cs +++ b/shadowsocks-csharp/Controller/Strategy/IStrategy.cs @@ -12,19 +12,45 @@ namespace Shadowsocks.Controller.Strategy UDP } + /* + * IStrategy + * + * Subclasses must be thread-safe + */ public interface IStrategy { string Name { get; } + string ID { get; } + /* + * Called when servers need to be reloaded, i.e. new configuration saved + */ + void ReloadServers(); + + /* + * Get a new server to use in TCPRelay or UDPRelay + */ Server GetAServer(IStrategyCallerType type, IPEndPoint localIPEndPoint); - void UpdateLatency(Server server); + /* + * TCPRelay will call this when latency of a server detected + */ + void UpdateLatency(Server server, TimeSpan latency); + /* + * TCPRelay will call this when reading from a server + */ void UpdateLastRead(Server server); + /* + * TCPRelay will call this when writing to a server + */ void UpdateLastWrite(Server server); + /* + * TCPRelay will call this when fatal failure detected + */ void SetFailure(Server server); } } diff --git a/shadowsocks-csharp/Controller/Strategy/StrategyManager.cs b/shadowsocks-csharp/Controller/Strategy/StrategyManager.cs index 254dd636..be8869da 100644 --- a/shadowsocks-csharp/Controller/Strategy/StrategyManager.cs +++ b/shadowsocks-csharp/Controller/Strategy/StrategyManager.cs @@ -12,6 +12,8 @@ namespace Shadowsocks.Controller.Strategy { _strategies = new List(); _strategies.Add(new BalancingStrategy(controller)); + _strategies.Add(new HighAvailabilityStrategy(controller)); + // TODO: load DLL plugins } public IList GetStrategies() { diff --git a/shadowsocks-csharp/Data/cn.txt b/shadowsocks-csharp/Data/cn.txt index cf0054f0..da498e42 100644 --- a/shadowsocks-csharp/Data/cn.txt +++ b/shadowsocks-csharp/Data/cn.txt @@ -24,6 +24,7 @@ About...=关于... Quit=退出 Edit Servers=编辑服务器 Load Balance=负载均衡 +High Availability=高可用 # Config Form diff --git a/shadowsocks-csharp/Model/Server.cs b/shadowsocks-csharp/Model/Server.cs index 4acc3a0e..24dd1162 100755 --- a/shadowsocks-csharp/Model/Server.cs +++ b/shadowsocks-csharp/Model/Server.cs @@ -18,6 +18,17 @@ namespace Shadowsocks.Model public string method; public string remarks; + public override int GetHashCode() + { + return server.GetHashCode() ^ server_port; + } + + public override bool Equals(object obj) + { + Server o2 = (Server)obj; + return this.server == o2.server && this.server_port == o2.server_port; + } + public string FriendlyName() { if (string.IsNullOrEmpty(server)) diff --git a/shadowsocks-csharp/View/ConfigForm.cs b/shadowsocks-csharp/View/ConfigForm.cs index d67acec5..91303536 100755 --- a/shadowsocks-csharp/View/ConfigForm.cs +++ b/shadowsocks-csharp/View/ConfigForm.cs @@ -136,7 +136,11 @@ namespace Shadowsocks.View _modifiedConfiguration = controller.GetConfigurationCopy(); LoadConfiguration(_modifiedConfiguration); _oldSelectedIndex = _modifiedConfiguration.index; - ServersListBox.SelectedIndex = _modifiedConfiguration.index; + if (_oldSelectedIndex < 0) + { + _oldSelectedIndex = 0; + } + ServersListBox.SelectedIndex = _oldSelectedIndex; LoadSelectedServer(); } diff --git a/shadowsocks-csharp/View/MenuViewController.cs b/shadowsocks-csharp/View/MenuViewController.cs index ac144569..629f248b 100755 --- a/shadowsocks-csharp/View/MenuViewController.cs +++ b/shadowsocks-csharp/View/MenuViewController.cs @@ -127,7 +127,7 @@ namespace Shadowsocks.View string serverInfo = null; if (config.strategy != null) { - serverInfo = I18N.GetString(controller.GetCurrentStrategy().Name); + serverInfo = controller.GetCurrentStrategy().Name; } else { @@ -275,17 +275,18 @@ namespace Shadowsocks.View int i = 0; foreach (var strategy in controller.GetStrategies()) { - MenuItem item = new MenuItem(I18N.GetString(strategy.Name)); + MenuItem item = new MenuItem(strategy.Name); item.Tag = strategy.ID; item.Click += AStrategyItem_Click; items.Add(i, item); i++; } + int strategyCount = i; Configuration configuration = controller.GetConfigurationCopy(); foreach (var server in configuration.configs) { MenuItem item = new MenuItem(server.FriendlyName()); - item.Tag = i; + item.Tag = i - strategyCount; item.Click += AServerItem_Click; items.Add(i, item); i++; diff --git a/shadowsocks-csharp/shadowsocks-csharp.csproj b/shadowsocks-csharp/shadowsocks-csharp.csproj index 7ec2eccf..43be9673 100755 --- a/shadowsocks-csharp/shadowsocks-csharp.csproj +++ b/shadowsocks-csharp/shadowsocks-csharp.csproj @@ -124,6 +124,7 @@ +