Skip to content

Commit eb1150c

Browse files
author
Connor McMahon
authored
Change local RPC port selection logic (#1800)
Current RPC port selection logic favors ephemeral ports. This leads to issues with the web apps sandbox. Instead, we now use randomized port selection from an approved range of ports.
1 parent 124874b commit eb1150c

File tree

2 files changed

+32
-29
lines changed

2 files changed

+32
-29
lines changed

release_notes.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
## New Features
22

33
## Bug fixes
4+
- Fix issue with local RPC endpoint used by non-.NET languages on Windows apps (#1800)
45

56
## Breaking Changes

src/WebJobs.Extensions.DurableTask/LocalHttpListener.cs

Lines changed: 31 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,18 @@ internal class LocalHttpListener : IDisposable
2222
{
2323
private const int DefaultPort = 17071;
2424

25+
// Pick a large, fixed range of ports that are going to be valid in all environment.
26+
// Avoiding ports below 1024 as those are blocked by app service sandbox.
27+
// Ephemeral ports for most OS start well above 32768. See https://www.ncftp.com/ncftpd/doc/misc/ephemeral_ports.html
28+
private const int MinPort = 30000;
29+
private const int MaxPort = 31000;
30+
2531
private readonly Func<HttpRequestMessage, Task<HttpResponseMessage>> handler;
2632
private readonly EndToEndTraceHelper traceHelper;
2733
private readonly DurableTaskOptions durableTaskOptions;
34+
private readonly Random portGenerator;
35+
private readonly HashSet<int> attemptedPorts;
36+
2837
private IWebHost localWebHost;
2938

3039
public LocalHttpListener(
@@ -39,6 +48,8 @@ public LocalHttpListener(
3948
// Set to a non null value
4049
this.InternalRpcUri = new Uri($"http://uninitialized");
4150
this.localWebHost = new NoOpWebHost();
51+
this.portGenerator = new Random();
52+
this.attemptedPorts = new HashSet<int>();
4253
}
4354

4455
public Uri InternalRpcUri { get; private set; }
@@ -59,10 +70,12 @@ public async Task StartAsync()
5970
int numAttempts = 1;
6071
do
6172
{
62-
int availablePort = this.GetAvailablePort();
73+
int listeningPort = numAttempts == 1
74+
? DefaultPort
75+
: this.GetRandomPort();
6376
try
6477
{
65-
this.InternalRpcUri = new Uri($"http://127.0.0.1:{availablePort}/durabletask/");
78+
this.InternalRpcUri = new Uri($"http://127.0.0.1:{listeningPort}/durabletask/");
6679
var listenUri = new Uri(this.InternalRpcUri.GetLeftPart(UriPartial.Authority));
6780
this.localWebHost = new WebHostBuilder()
6881
.UseKestrel()
@@ -80,11 +93,9 @@ public async Task StartAsync()
8093
this.durableTaskOptions.HubName,
8194
functionName: string.Empty,
8295
instanceId: string.Empty,
83-
message: $"Failed to open local socket {availablePort}. This was attempt #{numAttempts} to open a local port.");
96+
message: $"Failed to open local port {listeningPort}. This was attempt #{numAttempts} to open a local port.");
97+
this.attemptedPorts.Add(listeningPort);
8498
numAttempts++;
85-
var random = new Random();
86-
var millisecondsToWait = (int)Math.Round(random.NextDouble() * 1000);
87-
await Task.Delay(millisecondsToWait);
8899
}
89100
}
90101
while (numAttempts <= maxAttempts);
@@ -99,6 +110,20 @@ public async Task StartAsync()
99110
#endif
100111
}
101112

113+
private int GetRandomPort()
114+
{
115+
// Get a random port that has not already been attempted so we don't waste time trying
116+
// to listen to a port we know is not free.
117+
int randomPort;
118+
do
119+
{
120+
randomPort = this.portGenerator.Next(MinPort, MaxPort);
121+
}
122+
while (this.attemptedPorts.Contains(randomPort));
123+
124+
return randomPort;
125+
}
126+
102127
public async Task StopAsync()
103128
{
104129
#if !FUNCTIONS_V1
@@ -110,29 +135,6 @@ public async Task StopAsync()
110135
this.IsListening = false;
111136
}
112137

113-
private int GetAvailablePort()
114-
{
115-
// If we are able to successfully start a listener looking on the default port without
116-
// an exception, we can use the default port. Otherwise, let the TcpListener class decide for us.
117-
try
118-
{
119-
var listener = new TcpListener(IPAddress.Loopback, DefaultPort);
120-
listener.Start();
121-
listener.Stop();
122-
return DefaultPort;
123-
}
124-
catch (SocketException)
125-
{
126-
// Following guidance of this stack overflow answer
127-
// to find available port: https://stackoverflow.com/a/150974/9035640
128-
var listener = new TcpListener(IPAddress.Loopback, 0);
129-
listener.Start();
130-
int availablePort = ((IPEndPoint)listener.LocalEndpoint).Port;
131-
listener.Stop();
132-
return availablePort;
133-
}
134-
}
135-
136138
private async Task HandleRequestAsync(HttpContext context)
137139
{
138140
try

0 commit comments

Comments
 (0)