Exemple #1
0
        /// <summary>
        /// Sessionless UDP Load Balancer sends packets to targets without session affinity.
        /// </summary>
        /// <param name="serverPort">Set the port to listen to and forward to backend targets (default 1812)</param>
        /// <param name="adminPort">Set the port that targets will send watchdog events (default 1111)</param>
        /// <param name="clientTimeout">Seconds to allow before cleaning-up idle clients (default 30)</param>
        /// <param name="targetTimeout">Seconds to allow before removing target missing watchdog events (default 30)</param>
        /// <param name="defaultTargetWeight">Weight to apply to targets when not specified (default 100)</param>
        /// <param name="unwise">Allows public IP addresses for targets (default is to only allow private IPs)</param>
        static async Task Main(int serverPort = 1812, int adminPort = 1111, uint clientTimeout = 30, uint targetTimeout = 30, byte defaultTargetWeight = 100, bool unwise = false)
        {
            await Console.Out.WriteLineAsync($"{DateTime.Now:s}: Welcome to the simplest UDP Load Balancer.  Hit Ctrl-C to Stop.");

            var admin_ip = NetworkInterface.GetAllNetworkInterfaces().Private().First();
            await Console.Out.WriteLineAsync($"{DateTime.Now:s}: The server port is {serverPort}.");

            await Console.Out.WriteLineAsync($"{DateTime.Now:s}: The watchdog endpoint is {admin_ip}:{adminPort}.");

            await Console.Out.WriteLineAsync($"{DateTime.Now:s}: Timeouts are: {clientTimeout}s for clients, and {targetTimeout}s  for targets.");

            await Console.Out.WriteLineAsync($"{DateTime.Now:s}: {(unwise ? "*WARNING* " : string.Empty)}"
                                             + $"Targets with public IPs {(unwise ? "WILL BE" : "will NOT be")} allowed.");

            using var cts = new CancellationTokenSource();

            Console.CancelKeyPress += (s, a) =>
            {
                Console.Out.WriteLine($"{DateTime.Now:s}: Beginning shutdown procedure.");
                cts.Cancel();
                a.Cancel = true;
            };

            // helper to run tasks with cancellation
            Task run(Func <Task> func, string name)
            {
                return(Task.Run(async() =>
                {
                    var ct = cts.Token;
                    while (!ct.IsCancellationRequested)
                    {
                        try
                        {
                            await func();
                        }
                        catch (Exception e)
                        {
                            await Console.Out.WriteLineAsync($"{DateTime.Now:s}: *ERROR* Task {name} encountered a problem: {e.Message}");
                            await Task.Delay(100); // slow fail
                        }
                    }
                    await Console.Out.WriteLineAsync($"{DateTime.Now:s}: {name} is done.");
                }));
            }

            var backends = new ConcurrentDictionary <IPEndPoint, (byte weight, DateTime seen)>();
            var clients  = new ConcurrentDictionary <IPEndPoint, (UdpClient client, DateTime seen)>();

            // task to listen on the server port and relay packets to random backends via a client-specific internal port
            using var server = new UdpClient(serverPort).Configure();
            async Task relay()
            {
                if (server.Available > 0)
                {
                    var packet = await server.ReceiveAsync();

                    Interlocked.Increment(ref received);

                    var client  = clients.AddOrUpdate(packet.RemoteEndPoint, ep => (new UdpClient().Configure(), DateTime.Now), (ep, c) => (c.client, DateTime.Now));
                    var backend = backends.Random();
                    backend?.SendVia(client.client, packet.Buffer, s => Interlocked.Increment(ref relayed));
                }
                else
                {
                    await Task.Delay(10);
                }
            }

            // helper to get replies asyncronously
            async IAsyncEnumerable <(UdpReceiveResult result, IPEndPoint ep)> replies()
            {
                foreach (var c in clients)
                {
                    if (c.Value.client.Available > 0)
                    {
                        yield return(await c.Value.client.ReceiveAsync(), c.Key);
                    }
                }
            }

            // task to listen for responses from backends and re-send them to the correct external client
            async Task reply()
            {
                var any = false;

                await foreach (var(result, ep) in replies())
                {
                    server.BeginSend(result.Buffer, result.Buffer.Length, ep, s => Interlocked.Increment(ref responded), null);
                    any = true;
                }
                if (!any)
                {
                    await Task.Delay(10);
                }
            }

            // task to listen for instances asking to add/remove themselves as a target (watch-dog pattern)
            using var control = new UdpClient(new IPEndPoint(admin_ip, adminPort)).Configure();
            async Task admin()
            {
                if (control.Available > 0)
                {
                    var packet = await control.ReceiveAsync();

                    var payload = new ArraySegment <byte>(packet.Buffer);

                    var header = payload.Slice(0, 2);
                    var ip     = new IPAddress(payload.Slice(2).Slice(0, 4));
                    if (ip.Equals(IPAddress.Any))
                    {
                        ip = packet.RemoteEndPoint.Address;
                    }
                    var port   = BitConverter.ToUInt16(payload.Slice(6).Slice(0, 2));
                    var weight = payload.Count > 8 ? payload[8] : defaultTargetWeight;
                    if (weight > 0 && (unwise || IPNetwork.IsIANAReserved(ip)))
                    {
                        var ep = new IPEndPoint(ip, port);
                        switch (BitConverter.ToInt16(header))
                        {
                        case 0x1111:
                            backends.AddOrUpdate(ep, ep => (weight, DateTime.Now), (ep, d) => (weight, DateTime.Now));
                            await Console.Out.WriteLineAsync($"{DateTime.Now:s}: Refresh {ep} (weight {weight}).");

                            break;

                        case 0x1186:     // see AIEE No. 26
                            backends.Remove(ep, out var seen);
                            await Console.Out.WriteLineAsync($"{DateTime.Now:s}: Remove {ep}.");

                            break;
                        }
                    }
                    else
                    {
                        await Console.Out.WriteLineAsync($"{DateTime.Now:s}: Rejected {ip}:{port} (weight {weight}).");
                    }
                }
                else
                {
                    await Task.Delay(10);
                }
            }

            // task to remove backends and clients we haven't heard from in a while
            async Task purge()
            {
                await Task.Delay(100);

                var remove_backends = backends.Where(kv => kv.Value.seen < DateTime.Now.AddSeconds(-targetTimeout)).Select(kv => kv.Key).ToArray();

                foreach (var b in remove_backends)
                {
                    backends.TryRemove(b, out var seen);
                    await Console.Out.WriteLineAsync($"{DateTime.Now:s}: Expired target {b} (last seen {seen:s}).");
                }
                var remove_clients = clients.Where(kv => kv.Value.seen < DateTime.Now.AddSeconds(-clientTimeout)).Select(kv => kv.Key).ToArray();

                foreach (var c in remove_clients)
                {
                    clients.TryRemove(c, out var info);
                    await Console.Out.WriteLineAsync($"{DateTime.Now:s}: Expired client {c} (last seen {info.seen:s}).");
                }
            }

            // task to occassionally write statistics to the console
            async Task stats()
            {
                await Console.Out.WriteLineAsync($"{DateTime.Now:s}: {received}/{relayed}/{responded}, {clients.Count} => {backends.Count}");

                await Task.Delay(500);
            }

            var tasks = new[] {
                run(relay, "Relay"),
                run(reply, "Reply"),
                run(admin, "Admin"),
                run(purge, "Purge"),
                run(stats, "State")
            };
            await Task.WhenAll(tasks);

            var e = string.Join(", ", tasks.Where(t => t.Exception != null).Select(t => t.Exception.Message));
            await Console.Out.WriteLineAsync($"{DateTime.Now:s}: Bye-now ({(e.Any() ? e : "OK")}).");
        }