dotnet / aspire

An opinionated, cloud ready stack for building observable, production ready, distributed applications in .NET
https://learn.microsoft.com/dotnet/aspire
MIT License
3.62k stars 407 forks source link

IP binding #2887

Open b0l0k opened 6 months ago

b0l0k commented 6 months ago

Hello,

I've recently found a personal project to test Aspire, and I must say, what you're doing is awesome! The development experience is 👌 ; it took me just a few hours to craft a service that's horizontally scalable built on Kafka and Postgres.

However, as I delved into the deployment aspect, I faced my first challenge. I'm keen on testing the service on a separate laptop within my local network using Aspire to manage it, rather than resorting to Docker Compose or Kubernetes. Essentially, I'm aiming to execute it with a simple dotnet run command. I know that Aspire doesn't aim to be a hosting production solution.

By default, it seems everything is bound to localhost, restricting access to the loopback interface. I managed to override this behavior by configuring a dedicated setup in launchProfile.json:

{
  "$schema": "http://json.schemastore.org/launchsettings.json",
  "profiles": {
    "http": {
      "commandName": "Project",
      "dotnetRunMessages": true,
      "launchBrowser": true,
      "launchUrl": "swagger",
      "applicationUrl": "http://localhost:5163",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    },
    "vps": {
      "commandName": "Project",
      "dotnetRunMessages": true,
      "launchBrowser": true,
      "launchUrl": "swagger",
      "applicationUrl": "http://<ip of the laptop>:5163",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }
  }
}

Please note that using 0.0.0.0 as an alternative isn't effective. It appears there's a check in place to monitor the dashboard, leading to a timeout after 1:00.

The good news is that this configuration works seamlessly for the Dashboard part. However, I'm encountering issues accessing the WebAPI project, which seems to be bound to localhost through the proxy, therefore not accessible remotely.

Do you have any insights or suggestions on how we might resolve this?

davidfowl commented 6 months ago

I know that Aspire doesn't aim to be a hosting production solution.

It's not that it's not just aiming. The local development orchestrator is not optimized, supported and not secure for deployment scenarios.

Do you have any insights or suggestions on how we might resolve this?

Lots of places currently assume localhost in the product. I'm not sure there's a way for you to work around this currently.

cc @karolz-ms

b0l0k commented 5 months ago

I understand but assuming that we're not all using a local development environment and having a solution to bind on 0.0.0.0 would be great.

davidfowl commented 5 months ago

What’s the scenario outside of local development? The orchestrator isn’t supported outside of local development. I’m not just saying that for fun but there’s real security risk in using it outside of that environment (it has not been hardened for that scenario so you run this at your own risk). One thing you can do to circumvent the proxies is to use a proxyless endpoint. In preview 4 that requires excluding the launch profile and adding manual endpoints. Your applicant should be able to listen on 0.0.0.0 directly in that scenario.

b0l0k commented 5 months ago

Thank you David. 🙏

What’s the scenario outside of local development?

I'm using SSH remote feature from VSCode to develop on a personal computer from my professional one. Therefore when I use 'dotnet run' and I would love to test it from my browser. I'm aware about security risks.

mitchdenny commented 5 months ago

You could use the DevTunnels feature?

MichaelSL commented 5 months ago

I have a slightly different case, but I think it's related to this one. Before using Aspire I had two services which had to know about each other and use external OAuth2 provider. To avoid rewriting configs every time I've setup Nginx reverse proxy and pointed it to the ports services run on my dev machine. With Aspire, no matter what do I specify in launchSettings.json: 0.0.0.0:5000 or mydevbox.lan:5000, services still bind to localhost. My Nginx machine can't access the services like that. Is there a way to use the url from launchSettings.json instead of mapping it to localhost?

mitchdenny commented 4 months ago

Aspire is doing some things automatically for you around reading your launch settings and spinning up a reverse proxy of our own. If you want Aspire to get out of this business you can suppress the proxy behavior by doing something like this:

builder.AddProject<Projects.MyApp>("myapp)
       .WithEndpoint("http", endpoint => {
         endpoint.IsProxied = false;
       });

Aspire won't get involved in spinning up a reverse proxy in this case, and whatever is in your launchSettings should be used. Let us know how you go with this!

davidfowl commented 4 months ago

This is an issue on our side, we hardcode localhost in the ASPNETCORE_URLS generation.

divvjson commented 4 months ago

I have a similar issue. I have a Blazor web app. I would like to be able to develop on e.g. iOS Safari on an iPhone, not just the browser on my development machine (Safari is the new Internet Explorer). For this I need to be able to tell Aspire to listen on requests coming from outside localhost. I have successfully been able to configure this for the individual Blazor web app project with the code below. But Aspire does not seem to pickup these changes (which is understandable). I would assume I also have to specify a specific certificate that is valid not just for localhost, but for my internal IP address, e.g. 192.168.1.45.

I would like to get confirmation on:

  1. Is what I am trying to achieve possible with the current Aspire version? If so, how?
  2. If no, is there a workaround?
builder.WebHost.ConfigureKestrel(options =>
{
    options.ListenAnyIP(5063);
    options.ListenAnyIP(7076, options =>
    {
        options.UseHttps("certificate.pfx", "YourPassword");
    });
});
MichaelSL commented 4 months ago

@mitchdenny maybe I misunderstood the workaround, but it doesn't work for me.

I'm running Aspire NuGet version 8.0.0-preview.5.24201.12

In my Program.cs I have following definition for the service:


builder.AddProject<Projects.TripstaFileSharing>("tripstaapi")
    .WithEndpoint("http", endpoint =>
    {
        endpoint.IsProxied = false;
    });

Aspire http profile in launchSettings.json:

"http": {
  "commandName": "Project",
  "dotnetRunMessages": true,
  "launchBrowser": true,
  "applicationUrl": "http://localhost:15192",
  "environmentVariables": {
    "ASPNETCORE_ENVIRONMENT": "Development",
    "DOTNET_ENVIRONMENT": "Development",
    "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19288",
    "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20230",
    "ASPIRE_ALLOW_UNSECURED_TRANSPORT": "true"
  }
}

And finally launch profile for the service itself:

"http": {
  "commandName": "Project",
  "launchBrowser": true,
  "launchUrl": "swagger",
  "environmentVariables": {
    "ASPNETCORE_ENVIRONMENT": "Development"
  },
  "dotnetRunMessages": true,
  "applicationUrl": "http://0.0.0.0:5297"
}

Despite this configuration changes the service starts up on localhost and I can't reach it from external machine:

Now listening on: http://localhost:5297

Please let me know if I can try something else.

mitchdenny commented 4 months ago

As @davidfowl mentioned above we by default bind the the listener in ASP.NET Core to localhost/127.0.0.1 which means you can't access it from another machine. We need to do some work on this scenario which will happen post GA at this point.

As a work around you could use DevTunnels and expose the service to the Internet. Here is the steps I took to make this work:

  1. Create a Dev Tunnel (bring up the DevTunnels window and create a new one).
image
  1. Make the DevTunnel active

  2. Launch the AppHost

  3. Look at the DevTunnels output window for a mapping of internet accessible *.devtunnels.ms URLs.

Note that DevTunnels will map an internet address to the port that your service exposes, not the reverse proxy that Aspire spins up when it launches your ASP.NET Core app. We've got a bunch of work to do here to make this more intuitive but hopefully this can get you going.

For more information on the DevTunnels feature in VS see here: https://learn.microsoft.com/en-us/aspnet/core/test/dev-tunnels?view=aspnetcore-8.0

davidfowl commented 4 months ago

There might be a simpler workaround but it’s easier to wait until preview 6 comes out to do it as you’ll be able to mutate the ASPNETCORE_URLS to replace localhost with * or 0.0.0.0

nilzzzzzz commented 4 months ago

Hi @davidfowl ,

firstly: kudos for aspire, its amazing. We currently have a setup where each GitHub pull request trigger the creation of a virtual machine. On this VM, we run both dotnet Aspire and all our microservices to facilitate API testing.

However, we've encountered an issue. Our API Gateway (Kong), which is running in Docker outside of Aspire, is unable to communicate with the .NET services managed by Aspire. We suspect this might be due to Aspire services binding to localhost rather than to all interfaces (0.0.0.0).

Is there a workaround available that would allow binding to all interfaces, or is waiting for preview 6 the only option?

Thank you.

davidfowl commented 4 months ago

Here's the workaround for preview 6 (which comes out today):

public static class ProjectExtensions
{
    public static IResourceBuilder<ProjectResource> ExposeThePorts(this IResourceBuilder<ProjectResource> builder)
    {
        return builder.WithEnvironment(async context =>
        {
            if (context.ExecutionContext.IsRunMode)
            {
                var urls = context.EnvironmentVariables["ASPNETCORE_URLS"] as ReferenceExpression;

                if (urls is null)
                {
                    return;
                }

                var value = await urls.GetValueAsync(context.CancellationToken);

                if (value is null)
                {
                    return;
                }

                context.EnvironmentVariables["ASPNETCORE_URLS"] = value.Replace("localhost", "*");
            }
        });
    }
}

This won't update the URLs in the dashboard, just the listening URLs for the application itself. I don't know if this will cover all of the cases in this issue but worth a try.

nilzzzzzz commented 4 months ago

Here's the workaround for preview 6 (which comes out today):

public static class ProjectExtensions
{
    public static IResourceBuilder<ProjectResource> ExposeThePorts(this IResourceBuilder<ProjectResource> builder)
    {
        return builder.WithEnvironment(async context =>
        {
            if (context.ExecutionContext.IsRunMode)
            {
                var urls = context.EnvironmentVariables["ASPNETCORE_URLS"] as ReferenceExpression;

                if (urls is null)
                {
                    return;
                }

                var value = await urls.GetValueAsync(context.CancellationToken);

                if (value is null)
                {
                    return;
                }

                context.EnvironmentVariables["ASPNETCORE_URLS"] = value.Replace("localhost", "*");
            }
        });
    }
}

This won't update the URLs in the dashboard, just the listening URLs for the application itself. I don't know if this will cover all of the cases in this issue but worth a try.

Works perfectly for me, thank you!

MichaelSL commented 4 months ago

@davidfowl thank you for the workaround! Workaround produces interesting side effect: services doesn't bind to IPv4 0.0.0.0 and port specified in launchSettings.json, but instead bind to [::] IPv6 address and random port. I can access API from outside but I have to dig out the port from the logs every time.

Also setting ASPNETCORE_URL in launchSettings.json doesn't have any effect despite this line which I believe should not override them?

MichaelSL commented 4 months ago

After some additional digging I found that most likely endpoint configuration from launchSettings.json file can't be applied as port name matches port generation expression here.

When I get into the ExposePorts I can see endpoint definition in the builder: image

davidfowl commented 4 months ago

There's lots of code here and very little docs.

Port = The port used but clients to access a service TargetPort = The port that the service binds to

This is still worthwhile foundational knowledge https://learn.microsoft.com/en-us/dotnet/aspire/fundamentals/networking-overview

TL;DR we currently don't take the address into account when binding address in proxied mode or proxyless mode, only the port. This is the issue we need to resolve.

davidfowl commented 4 months ago

Also setting ASPNETCORE_URL in launchSettings.json doesn't have any effect despite this line which I believe should not override them?

https://github.com/dotnet/aspire/blob/e96138f62e0b22da1d104913dcd195afa91f063d/src/Aspire.Hosting/ProjectResourceBuilderExtensions.cs#L257

MichaelSL commented 4 months ago

@davidfowl thank you! I figured it out, so to get the target port right I had to still use IsProxied = false, so that Aspire would set targetPort = port

divvjson commented 4 months ago

I have updated to preview 6 but can't get the workaround to work, I am still unable to access my web app from my assigned IP 172.30.1.35 on port 5063 (using http).

I must use port 5063 for http and port 7076 for https.

This is what I have:

Program.cs

var builder = DistributedApplication.CreateBuilder(args);

builder
    .AddProject<MyWebApp>("mywebapp")
    .WithHttpEndpoint(port: 5063, targetPort: 5063, name: "http-web", env: "Development", isProxied: false)
    .WithHttpsEndpoint(port: 7076, targetPort: 7076, name: "https-web", env: "Development", isProxied: false)
    .ExposeThePorts();

builder.Build().Run();

launchSettings.json (for my web app project)

{
  "$schema": "http://json.schemastore.org/launchsettings.json",
  "iisSettings": {
    "windowsAuthentication": false,
    "anonymousAuthentication": true,
    "iisExpress": {
      "applicationUrl": "http://localhost:34641",
      "sslPort": 44323
    }
  },
  "profiles": {
    "http": {
      "commandName": "Project",
      "dotnetRunMessages": true,
      "launchBrowser": true,
      "applicationUrl": "http://localhost:5117",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    },
    "https": {
      "commandName": "Project",
      "dotnetRunMessages": true,
      "launchBrowser": true,
      "applicationUrl": "https://localhost:7034;http://localhost:5117",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    },
    "IIS Express": {
      "commandName": "IISExpress",
      "launchBrowser": true,
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }
  }
}

ProjectExtensions.cs

namespace MyNamespace.AppHost;

public static class ProjectExtensions
{
    public static IResourceBuilder<ProjectResource> ExposeThePorts(this IResourceBuilder<ProjectResource> builder)
    {
        return builder.WithEnvironment(async context =>
        {
            if (context.ExecutionContext.IsRunMode)
            {
                if (context.EnvironmentVariables["ASPNETCORE_URLS"] is not ReferenceExpression urls)
                {
                    return;
                }

                var value = await urls.GetValueAsync(context.CancellationToken);

                if (value is null)
                {
                    return;
                }

                context.EnvironmentVariables["ASPNETCORE_URLS"] = value.Replace("localhost", "*");
            }
        });
    }
}
davidfowl commented 4 months ago

Remove env: "Development".

divvjson commented 4 months ago

@davidfowl Thanks, it works!

mitchdenny commented 1 month ago

I think this @davidebbo's change might help here: https://github.com/dotnet/aspire/pull/4679

Its not the complete solution but this would allow us to filter out endpoints being added to ASPNETCORE_URLS and then we could replace it with our own.

davidebbo commented 1 month ago

Its not the complete solution but this would allow us to filter out endpoints being added to ASPNETCORE_URLS and then we could replace it with our own.

@mitchdenny So you mean you'd append the entire thing yourself to ASPNETCORE_URLS instead of just replacing localhost in the existing expression? But wouldn't that mean having to come up with the funky {{- portForServing "apiservice" -}} syntax in user code?

I might be misunderstanding the suggestion.