Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow specifying host and port or port in ASPNETCORE_URLS #43135

Closed
1 task done
davidfowl opened this issue Aug 8, 2022 · 14 comments · Fixed by #44194
Closed
1 task done

Allow specifying host and port or port in ASPNETCORE_URLS #43135

davidfowl opened this issue Aug 8, 2022 · 14 comments · Fixed by #44194
Assignees
Labels
area-networking Includes servers, yarp, json patch, bedrock, websockets, http client factory, and http abstractions
Milestone

Comments

@davidfowl
Copy link
Member

davidfowl commented Aug 8, 2022

Is there an existing issue for this?

  • I have searched the existing issues

Is your feature request related to a problem? Please describe the problem.

Today ASP.NET Core allows specifying the scheme (http/https), host and port via a URL like syntax in ASPNETCORE_URLS and via the IServerAddressesFeature. This issue suggests we expand that the syntax supported to make it a bit more intuitive as this is a common production and container scenario. We want to make it more intuitive to specify the host and port or just the port to use for ASP.NET CORE applications. We can also take the opportunity to revisit what this new syntax would mean with respect to what the defaults are when pieces are unspecified.

Note: * and + are hold overs from HTTP.sys that we support on Kestrel as well but it's not obvious to most users (in fact + and * mean different things with HTTP.sys but not with Kestrel). I'm not sure if 0.0.0.0 work with HTTP.sys.

Describe the solution you'd like

When specifying the URL, we should support the following syntax:

  • {host}:{port} - Scheme = http, The host and port are specified
  • :{port} or {port} = Scheme = http, host = bind to all addresses (the equivalent of + or * today), port specified.

e.g.

via code

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World");

app.Urls.Add("8080"); // 8080 and all addresses
app.Urls.Add(":8081"); // 8081 and all addresses 
app.Urls.Add("localhost:8082"); // 8082 and localhost
app.Urls.Add("http://+:80"); // 80 and all addresses

app.Run();
ASPNETCORE_URLS=:8080;8081;localhost:5000;http://+:80

Open questions:

  • Do we listen on IPV6Any (::) or IPV4Any (0.0.0.0) by default? We should mimic the behavior or + and * today.
  • If the port is specified as 443, do we infer https? I think no, if you want TLS, specify the full URL including the scheme, host and port.
  • Do we support this with HTTP.sys? Yes we should pick if this syntax means * or +.
  • When this syntax is specified in code or via configuration what happens when you read IServerAddressesFeature.Addresses? These should return the processed urls.
  • Do we make a breaking change to rename Urls to Addresses on WebApplicationBuilder? No.
  • Should we rename url to address in WebApplicatiion.Run? Yes we can consider it as its much less of a breaking change.
  • Do we rename ASPNETCORE_URLS to ASPNETCORE_ADDRESSES? No. The value is too low.

Golang supports this syntax with their Listen API https://pkg.go.dev/net#Listen (the address portion).
nodejs defaults to listening on all addresses when the host is omitted.

Additional context

See dotnet/dotnet-docker#3968 for some background on this but it's been discussed in the past.

cc @richlander

@richlander
Copy link
Member

richlander commented Aug 8, 2022

That looks good. Thanks for writing it up. I think this proposal is a big improvement on approachability.

There is still the question on what we should do in our Dockerfiles. I'd like to move away from a privileged port as the default. As a result, we could go with:

ENV ASPNETCORE_URLS=5000

Your question on 443/HTTPS is a good one. 443 is also a privileged port, so that don't help for the non-root scenario.

Here's what we should probably include in our runtime-deps Dockerfile as a service to users.

# Directs Kestrel to listen on port 5000 on all addresses
ENV ASPNETCORE_URLS=5000
# To also enable HTTPS on port 5001, copy the following example to your app Dockerfile
# ENV ASPNETCORE_URLS=5000;https://+:5001
# To use privileged ports, copy the following example to your app Dockerfile
# ENV ASPNETCORE_URLS=http://+:80;https://+:443

@Tratcher
Copy link
Member

Tratcher commented Aug 8, 2022

Pros:

  • It's convenient and demos well.
  • This improves the situation where people don't know what host wildcard to use when they want to accept all requests. They often use 0.0.0.0 and only listen on IPv4, not IPv6.

Concerns:

  • The ambiguous scheme of a standalone port has been the biggest reason we've avoided this in the past. 443 is a great example of that. This will be a very common source of mistaken assumptions. Requiring the scheme avoids that.
  • The asymmetry with https: ASPNETCORE_URLS=5000;https://+:5001
  • HTTPS is slowly phasing out HTTP both on the internet and on intranets. There's no path forward here to change the default scheme to https in the future without a major breaking change.

@richlander
Copy link
Member

HTTPS is slowly phasing out HTTP both on the internet and on intranets.

But not for basic dev. Most devs likely use raw HTTP on their desktops.

asymmetry

Ya. Agreed. That also popped for me.

Our guidance could be to use symmetry (including in our Dockerfiles) when specifying both HTTP/HTTPS. And the raw form is solely for raw HTTP.

If we feel that folks are moving to HTTPS only, then we could offer: ASPNETCORE_HTTPS_URLS. the port values would then be interpreted as TLS.

@Tratcher
Copy link
Member

Tratcher commented Aug 8, 2022

If we feel that folks are moving to HTTPS only, then we could offer: ASPNETCORE_HTTPS_URLS. the port values would then be interpreted as TLS.

More like: ASPNETCORE_HTTPS_PORTS=443;5001
You wouldn't want people to put http schemes into the https_urls field...

@davidfowl
Copy link
Member Author

davidfowl commented Aug 9, 2022

The asymmetry with https: ASPNETCORE_URLS=5000;https://+:5001

I don't think this is a big problem. HTTPs is already asymmetric since you need to provide a certificate elsewhere.

HTTPS is slowly phasing out HTTP both on the internet and on intranets.

Very slowly. We'll have jumped the shark when you see things like golang and node start defaulting to https. Nobody else does.

There's no path forward here to change the default scheme to https in the future without a major breaking change.

Many apps run behind proxies that terminate TLS.

If we feel that folks are moving to HTTPS only, then we could offer: ASPNETCORE_HTTPS_URLS. the port values would then be interpreted as TLS.

That sounds fine to me as well.

@davidfowl
Copy link
Member Author

I added an open question about defaulting to :: or 0.0.0.0 (ipv6 or ipv4 addresses).

@richlander
Copy link
Member

I don't know. It's quite a tradeoff.

  • Make it very easy for someone to share their app with another device or a friend on the same network.
  • Default to no outside-the-machine exposure so that secure intra-machine communication is easy.

Ideally, we push people to unix domain sockets for the latter so that secure concerns for intra-machine scenarios are resolved.

@Tratcher
Copy link
Member

Our guidance could be to use symmetry (including in our Dockerfiles) when specifying both HTTP/HTTPS. And the raw form is solely for raw HTTP.

That makes me think we should be using different fields for different formats:
ASPNETCORE_HTTP_PORTS: 5000
ASPNETCORE_HTTPS_PORTS: 5001
ASPNETCORE_URLS: http://+:5000;https://+:5001 // Overrides ASPNETCORE_HTTP_PORTS, ASPNETCORE_HTTPS_PORTS

@halter73
Copy link
Member

I added an open question about defaulting to :: or 0.0.0.0 (ipv6 or ipv4 addresses).

Kestrel binds to :: (IPv6Any) which is inclusive of 0.0.0.0 (IPv4Any) when it is not bound to "localhost" or a specific IP address (e.g. "*", "+", "example.org"). The IPv6 binding can fail if there is no IPv6 support. If it fails, we fall back to IPv4 0.0.0.0 (IPv4Any).

internal AnyIPListenOptions(int port)
: base(new IPEndPoint(IPAddress.IPv6Any, port))
{
}
internal override async Task BindAsync(AddressBindContext context, CancellationToken cancellationToken)
{
Debug.Assert(IPEndPoint != null);
// when address is 'http://hostname:port', 'http://*:port', or 'http://+:port'
try
{
await base.BindAsync(context, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex) when (!(ex is IOException))
{
context.Logger.LogDebug(CoreStrings.FormatFallbackToIPv4Any(IPEndPoint.Port));
// for machines that do not support IPv6
EndPoint = new IPEndPoint(IPAddress.Any, IPEndPoint.Port);
await base.BindAsync(context, cancellationToken).ConfigureAwait(false);
}

Notice how much simpler this is compared the localhost case.

internal LocalhostListenOptions(int port)
: base(new IPEndPoint(IPAddress.Loopback, port))
{
if (port == 0)
{
throw new InvalidOperationException(CoreStrings.DynamicPortOnLocalhostNotSupported);
}
}
/// <summary>
/// Gets the name of this endpoint to display on command-line when the web server starts.
/// </summary>
internal override string GetDisplayName()
{
return $"{Scheme}://localhost:{IPEndPoint!.Port}";
}
internal override async Task BindAsync(AddressBindContext context, CancellationToken cancellationToken)
{
var exceptions = new List<Exception>();
try
{
var v4Options = Clone(IPAddress.Loopback);
await AddressBinder.BindEndpointAsync(v4Options, context, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex) when (!(ex is IOException or OperationCanceledException))
{
context.Logger.LogInformation(0, CoreStrings.NetworkInterfaceBindingFailed, GetDisplayName(), "IPv4 loopback", ex.Message);
exceptions.Add(ex);
}
try
{
var v6Options = Clone(IPAddress.IPv6Loopback);
await AddressBinder.BindEndpointAsync(v6Options, context, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex) when (!(ex is IOException or OperationCanceledException))
{
context.Logger.LogInformation(0, CoreStrings.NetworkInterfaceBindingFailed, GetDisplayName(), "IPv6 loopback", ex.Message);
exceptions.Add(ex);
}
if (exceptions.Count == 2)
{
throw new IOException(CoreStrings.FormatAddressBindingFailed(GetDisplayName()), new AggregateException(exceptions));
}
// If StartLocalhost doesn't throw, there is at least one listener.
// The port cannot change for "localhost".
context.Addresses.Add(GetDisplayName());
}
// used for cloning to two IPEndpoints
internal ListenOptions Clone(IPAddress address)
{
var options = new ListenOptions(new IPEndPoint(address, IPEndPoint!.Port))
{
KestrelServerOptions = KestrelServerOptions,
Protocols = Protocols,
DisableAltSvcHeader = DisableAltSvcHeader,
IsTls = IsTls,
HttpsOptions = HttpsOptions,
HttpsCallbackOptions = HttpsCallbackOptions,
EndpointConfig = EndpointConfig
};
options._middleware.AddRange(_middleware);
options._multiplexedMiddleware.AddRange(_multiplexedMiddleware);
return options;
}

@halter73
Copy link
Member

That makes me think we should be using different fields for different formats

I think I agree with this. Would we ignore ASPNETCORE_URLS if ASPNETCORE_HTTP(S)_PORTS exists? Or merge them? What if there are port conflicts between the two?

Kestrel ignores ASPNETCORE_URLS if its ListenOptions are configured directly if PreferHostingUrls is false (which it usually is AFAIK). I assume Kestrel would ignore the ports in the same scenarios.

@richlander
Copy link
Member

@Tratcher was proposing that ASPNETCORE_URLS would win. That makes sense to me since it is more capable/descriptive.

@davidfowl
Copy link
Member Author

@Tratcher should we also have a simplified API for binding directly to a port?

@Tratcher
Copy link
Member

Tratcher commented Oct 1, 2022

Where, Hosting? Kestrel already has one.

The target customer is config/clusters, not code.

@davidfowl
Copy link
Member Author

Where, Hosting? Kestrel already has one.

The WebApplication class. The issue above has a proposal but I'm not coupled to it.

The target customer is config/clusters, not code.

That's not true, my original issue has both. If you look at other platforms, they are APIs that allow specifying a port (look at the original issue and the golang sample).

@ghost ghost locked as resolved and limited conversation to collaborators Nov 1, 2022
@amcasey amcasey added area-networking Includes servers, yarp, json patch, bedrock, websockets, http client factory, and http abstractions and removed area-runtime labels Aug 25, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-networking Includes servers, yarp, json patch, bedrock, websockets, http client factory, and http abstractions
Projects
None yet
Development

Successfully merging a pull request may close this issue.

6 participants