How to get the client IP in ASP.NET Core even behind a proxy
Part of implementing an error monitoring platform like elmah.io is dealing with the IP addresses of the clients generating errors. In this post, I'll show you parts of how we have implemented this in ASP.NET Core, to make sure that different hosting scenarios still produce the correct IP address.
Let's jump right in. ASP.NET Core supports getting the client IP directly on the HttpContext
object available throughout various places. For Razor pages app, simply get the value of the RemoteIpAddress
property and set it on the view model to see the value:
public class IndexModel : PageModel
{
public string? Ip { get; set; }
public void OnGet()
{
Ip = HttpContext.Connection.RemoteIpAddress?.ToString();
}
}
Similarly, in ASP.NET Core MVC you can access the HttpContext
from a controller:
public class HomeController : Controller
{
public IActionResult Index()
{
ViewBag.Ip = HttpContext.Connection.RemoteIpAddress?.ToString();
return View();
}
}
Include a reference to the Ip
string either through the Ip
property in IndexModel
or ViewBag
(depending on which of the above examples you are using):
<h1 class="display-4">@Model.Ip</h1>
And launch the project to inspect the value:
Notice how the value is ::1
when running the code locally. The value is a compressed version of the IPV6 loopback address 0:0:0:0:0:0:0:1
equivalent of the IPV4 address 127.0.0.1
. When deploying this to a web server somewhere, the IP will change to the real IP of the client.
Before we start digging into scenarios where the client IP cannot be resolved, let's look at other ways of getting hold of the IP. Not all classes have access to the HttpContext
object as illustrated in the examples above. ASP.NET Core provides a nice little helper named IHttpContextAccessor
. It is automatically registered as a dependency in all newer versions of ASP.NET Core, while you need to register it manually in old versions. Set up constructor injection of this interface:
public class MyService
{
private readonly IHttpContextAccessor httpContextAccessor;
public MyService(IHttpContextAccessor httpContextAccessor)
{
this.httpContextAccessor = httpContextAccessor;
}
public string? GetClientIp()
{
return httpContextAccessor.HttpContext?.Connection.RemoteIpAddress?.ToString();
}
}
Notice how the code for getting the remote IP address is similar to the examples before. The only difference here is that we use the IHttpContextAccessor
to get the current HttpContext
. We need a null
check on the HttpContext
property in case the invocation is not involved in a current HTTP request.
If the httpContextAccessor
object is null
, make sure to register it in Program.cs
or Startup.cs
:
builder.Services.AddHttpContextAccessor();
In most cases, websites are hosted on a web server somewhere. Sometimes, deployments involve various technologies like reverse proxies and/or cloud hosting. You may have Cloudflare, nginx, or similar products configured to run in front of your website. Some of the products go to some lengths of either hiding the original client IP or overwriting the client IP in the original request with the IP of the proxy.
Let's look at an example where a website runs on a web server somewhere with Cloudflare configured. Cloudflare can proxy domains and sub-domains as shown here:
This basically means that when requesting that domain, Cloudflare can execute code before forwarding the request to the proxied domain. In this example, running the code above would change the value of the RemoteIpAddress
property to now includes Cloudflare's IP address and not the original client initiating the request.
Luckily, most reverse proxies support some kind of feature to get the original IP. This is typically done by including custom headers to the request. The header mainly used to include this piece of information is named X-Forwarded-For
. This header is the de-facto standard that everyone adopts or at least should adopt. Let's look at how to get the original IP address through this header from ASP.NET Core MVC:
public IActionResult Index()
{
ViewBag.Ip = Request.Headers["X-Forwarded-For"].ToString();
return View();
}
The code fetches the value of the X-Forwarded-For
header through the current request. The IHttpContextAccessor
interface can be in a similar way:
httpContextAccessor.HttpContext?.Request.Headers["X-Forwarded-For"].ToString()
As you may already know, the Headers
property is a dictionary of header names and values. In the example above, I simply use ToString
to get the entire value of the X-Forwarded-For
header. If you inspect the class returned by the indexer, it is of type StringValues
. That's right, headers can have multiple values. In the case of the X-Forwarded-For
header, the value can be one or more IP addresses depending on the request chain involved in serving this request. If there are multiple proxies between the client initiating the request and your server, all of the proxies supporting this header will include their IP address in the list. If you want the IP of the initiating client, this should be available as the first value. Be aware that bad proxies may try to spoof IP addresses.
This manual approach can be fine if you always want to read the header value. In case your code or some third-party library needs the RemoveIpAddress
set, you can use the forwarded headers middleware built into the Microsoft.AspNetCore.HttpOverrides
NuGet package by including the following in your Program.cs
or Startup.cs
file:
app.UseForwardedHeaders();
This will detect any value in the X-Forwarded-For
header and update the request accordingly. As already mentioned, some products have settled on using other headers like the X-Coming-From
value instead. You can either check for this header name manually or you can configure the forwarded headers middleware to look for alternative header names by providing it with a custom configuration:
builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedForHeaderName = "X-Coming-From";
});
Again, be aware that IP addresses can be easily spoofed. It doesn't take more than one misbehaving or misconfigured proxy between the client and your server to fill in spoofed IP addresses. When using the ForwaredHeadersMiddleware
provided by Microsoft, a property named ForwardLimit
is set to 1
on the options as a default. This is done to only include one IP address in the header. The ForwardedHeadersOptions
class provides multiple options for controller the IP addresses to add, known proxies, and more.
That's it. There are many ways to get hold of the user's IP address. It all depends on how your website is deployed. Before I let you go, I want to put a few words on storing IP addresses in general. IP addresses are classified as personal information in some countries. If an IP address is linked to a user's activity, it can reveal their online behavior and in that way identify them. It is important to be aware of how IP addresses are stored and used, especially in situations where they may be linked to identifiable individuals. You should always provide information to your users about how their IP addresses are being collected and used, and obtain their consent where necessary. You should also make sure to implement data retention to make sure IP addresses are not stored longer than needed.
elmah.io: Error logging and Uptime Monitoring for your web apps
This blog post is brought to you by elmah.io. elmah.io is error logging, uptime monitoring, deployment tracking, and service heartbeats for your .NET and JavaScript applications. Stop relying on your users to notify you when something is wrong or dig through hundreds of megabytes of log files spread across servers. With elmah.io, we store all of your log messages, notify you through popular channels like email, Slack, and Microsoft Teams, and help you fix errors fast.
See how we can help you monitor your website for crashes Monitor your website