Skip to content

Commit 95f39ed

Browse files
committed
The new command line argument internalPortRange explicitly defines the ports to be used to communicate with backends. The inquiry response is updated to include the internal port assigned to the client.
1 parent da998f8 commit 95f39ed

1 file changed

Lines changed: 28 additions & 7 deletions

File tree

Program.cs

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,8 @@ static class Program
6464
/// <summary>
6565
/// Sessionless UDP Load Balancer sends packets to targets without session affinity.
6666
/// </summary>
67-
/// <param name="serverPortRange">Set the ports to listen to and forward to backend targets</param>
67+
/// <param name="serverPortRange">Set the ports to listen to and forward to backend targets (can't overlap with internalPortRange or contain adminPort)</param>
68+
/// <param name="internalPortRange">Set the ports to use to forward to backend targets (can't overlap with serverPortRange or contain adminPort)</param>
6869
/// <param name="adminIp">Set the IP to listen on for watchdog events (default is first private IP)</param>
6970
/// <param name="adminPort">Set the port that targets will send watchdog events</param>
7071
/// <param name="clientTimeout">Seconds to allow before cleaning-up idle clients</param>
@@ -75,22 +76,33 @@ static class Program
7576
/// <param name="defaultGroupId">Sets the group ID to assign to backends that when a registration packet doesn't include one, and when port isn't assigned a group</param>
7677
/// <param name="useProxyProtocol">When specified packet data will be prepended with a Proxy Protocol v2 header when sent to the backend</param>
7778
/// <param name="proxyProtocolTLV">Use to specify one or more TLVs to add to PPv2 headers (ignored when PPv2 isn't enabled). Example value: "0xDA=smurf".</param>
78-
static async Task Main(string serverPortRange = "1812-1813", IPAddress adminIp = default, int adminPort = 1111, uint clientTimeout = 30, uint targetTimeout = 30, byte defaultTargetWeight = 100, bool unwise = false, ushort statsPeriodMs = 1000, byte defaultGroupId = 0, bool useProxyProtocol = false, string[] proxyProtocolTLV = default)
79+
static async Task Main(string serverPortRange = "1812-1813", string internalPortRange = "32048-62048", IPAddress adminIp = default, int adminPort = 1111, uint clientTimeout = 30, uint targetTimeout = 30, byte defaultTargetWeight = 100, bool unwise = false, ushort statsPeriodMs = 1000, byte defaultGroupId = 0, bool useProxyProtocol = false, string[] proxyProtocolTLV = default)
7980
{
8081
var ports = serverPortRange.Split("-", StringSplitOptions.RemoveEmptyEntries) switch
8182
{
8283
string[] a when a.Length == 1 => [int.Parse(a[0])],
8384
string[] a when a.Length == 2 => (from: int.Parse(a[0]), to: int.Parse(a[1])).Enumerate().ToArray(),
8485
_ => throw new ArgumentException($"Invalid server port range: {serverPortRange}.", nameof(serverPortRange))
8586
};
87+
var internal_ports = internalPortRange.Split("-", StringSplitOptions.RemoveEmptyEntries) switch
88+
{
89+
string[] a when a.Length == 1 => [int.Parse(a[0])],
90+
string[] a when a.Length == 2 => (from: int.Parse(a[0]), to: int.Parse(a[1])).Enumerate().ToArray(),
91+
_ => throw new ArgumentException($"Invalid internal port range: {internalPortRange}.", nameof(internalPortRange))
92+
};
93+
94+
if (ports.Intersect(internal_ports).Any()) {
95+
throw new ArgumentException($"Server and internal port ranges must not overlap and mustn't include the admin port: {serverPortRange}, {internalPortRange} and {adminPort}.", nameof(serverPortRange));
96+
}
8697

8798
await Console.Out.WriteLineAsync($"{DateTime.UtcNow:s}: Welcome to the simplest UDP Load Balancer. Hit Ctrl-C to Stop.");
8899

89100
var my_ip = NetworkInterface.GetAllNetworkInterfaces().Private().First();
90101
var admin_ip = adminIp ?? my_ip;
91102
await Console.Out.WriteLineAsync($"{DateTime.UtcNow:s}: The server port range is {serverPortRange} ({ports.Length} port{(ports.Length > 1 ? "s" : "")}).");
92-
await Console.Out.WriteLineAsync($"{DateTime.UtcNow:s}: The watchdog endpoint is {admin_ip}:{adminPort}.");
93-
await Console.Out.WriteLineAsync($"{DateTime.UtcNow:s}: Timeouts are: {clientTimeout}s for clients, and {targetTimeout}s for targets.");
103+
await Console.Out.WriteLineAsync($"{DateTime.UtcNow:s}: The internal port range is {internalPortRange} ({internal_ports.Length} port{(internal_ports.Length > 1 ? "s" : "")}).");
104+
await Console.Out.WriteLineAsync($"{DateTime.UtcNow:s}: The admin/watchdog endpoint is {admin_ip}:{adminPort}.");
105+
await Console.Out.WriteLineAsync($"{DateTime.UtcNow:s}: Timeouts are: {clientTimeout}s for clients, and {targetTimeout}s for targets.");
94106
await Console.Out.WriteLineAsync($"{DateTime.UtcNow:s}: Proxy Protocol v2 for targets is {(useProxyProtocol ? "enabled" : "disabled")}.");
95107
await Console.Out.WriteLineAsync($"{DateTime.UtcNow:s}: {(unwise ? "*WARNING* " : string.Empty)}"
96108
+ $"Targets with public IPs {(unwise ? "WILL BE" : "will NOT be")} allowed.");
@@ -129,8 +141,9 @@ Task run(Func<Task> func, string name)
129141
var backend_groups = new ConcurrentDictionary<byte, ConcurrentDictionary<IPAddress, (byte weight, DateTime seen)>>();
130142
var port_group_map = new ConcurrentDictionary<int, byte>(ports.ToDictionary(p => p, p => defaultGroupId));
131143

132-
var clients = new ConcurrentDictionary<(IPEndPoint remote, int external_port), (UdpClient internal_client, DateTime seen)>();
144+
var clients = new ConcurrentDictionary<(IPEndPoint remote, int external_port), (int internal_port, UdpClient internal_client, DateTime seen)>();
133145
var servers = ports.ToDictionary(p => p, p => new UdpClient(p).Configure());
146+
var free_internal_ports = new Queue<int>(internal_ports);
134147

135148
// helper to get requests (inbound packets from external sources) asyncronously
136149
async IAsyncEnumerable<(UdpReceiveResult result, int port)> requests()
@@ -194,7 +207,13 @@ async Task relay()
194207
{
195208
Interlocked.Increment(ref received);
196209

197-
var (internal_client, _) = clients.AddOrUpdate((request.RemoteEndPoint, port), ep => (new UdpClient().Configure(), DateTime.UtcNow), (ep, c) => (c.internal_client, DateTime.UtcNow));
210+
var (_, internal_client, _) = clients.AddOrUpdate((request.RemoteEndPoint, port),
211+
ep => {
212+
var internal_port = free_internal_ports.Dequeue();
213+
return (internal_port, new UdpClient().Configure(), DateTime.UtcNow);
214+
},
215+
(ep, c) => (c.internal_port, c.internal_client, DateTime.UtcNow)
216+
);
198217
if (backend_groups.TryGetValue(port_group_map[port], out var group))
199218
{
200219
var backend = group.Random();
@@ -327,7 +346,8 @@ async Task admin()
327346
}, port_low + (port_high << 8));
328347

329348
if (clients.TryGetValue((client_ep, server_port), out var info)) {
330-
await control.SendAsync([ 0x2e, 0x12, port_high, port_low, ..ep_bytes ], 4 + ep_bytes.Count, packet.RemoteEndPoint);
349+
var internal_port = info.internal_port;
350+
await control.SendAsync([0x2e, 0x12, (byte)(internal_port >> 8), (byte)internal_port, port_high, port_low, .. ep_bytes], 6 + ep_bytes.Count, packet.RemoteEndPoint);
331351
}
332352
}
333353
break;
@@ -358,6 +378,7 @@ async Task prune()
358378
{
359379
clients.TryRemove(c, out var info);
360380
info.internal_client.Dispose();
381+
free_internal_ports.Enqueue(info.internal_port);
361382
await Console.Out.WriteLineAsync($"{DateTime.UtcNow:s}: Expired client {c} (last seen {info.seen:s}).");
362383
}
363384
}

0 commit comments

Comments
 (0)