Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,15 @@ at the same time will offer your host as source for other users. You can disable
* Many interface improvements
* Added a security check on all the inbound parameters to avoid command injection
* Added the multi-source traceroute feature to allow to run the traceroute from different sources
* Frontend converted to Blazor

### Running in Docker
You can use the following image to run Visual Trace Route locally:
michele73/traceroute:2.0.2
michele73/traceroute:3.0.0

Example:
```
docker run -d -p 8081:80 --name=traceroute --restart=always -v traecroute_logs:/app/logs michele73/traceroute:2.0.2
docker run -d -p 8081:80 --name=traceroute --restart=always -v traecroute_logs:/app/logs michele73/traceroute:3.0.0
```

The image repository is here: https://hub.docker.com/r/michele73/traceroute
Expand Down
1 change: 1 addition & 0 deletions TraceRoute.sln
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnitTests", "UnitTests\Unit
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{CF5D81FA-5FFF-498E-8626-5FA87E144F26}"
ProjectSection(SolutionItems) = preProject
finecodecoverage-settings.xml = finecodecoverage-settings.xml
SupportFiles\Lighthouse Reports.xlsx = SupportFiles\Lighthouse Reports.xlsx
SupportFiles\logo.png = SupportFiles\logo.png
README.md = README.md
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<key id="13e98c53-dc8f-41f3-9f11-c519e46b229d" version="1">
<creationDate>2025-06-21T10:31:11.3848873Z</creationDate>
<activationDate>2025-06-23T07:15:29.4354054Z</activationDate>
<expirationDate>2025-09-19T10:31:11.3617506Z</expirationDate>
<descriptor deserializerType="Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.AuthenticatedEncryptorDescriptorDeserializer, Microsoft.AspNetCore.DataProtection, Version=9.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60">
<descriptor>
<encryption algorithm="AES_256_CBC" />
<validation algorithm="HMACSHA256" />
<masterKey p4:requiresEncryption="true" xmlns:p4="http://schemas.asp.net/2015/03/dataProtection">
<!-- Warning: the key below is in an unencrypted form. -->
<value>E9TmvshQP+1XTfCT8q1u54zz31kZZIrZFRzCC7AeWeRIFLBtsFbfl8J8ioH/i7o3+NVyorZ9aF+7KtzddpfXHw==</value>
</masterKey>
</descriptor>
</descriptor>
</key>
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<key id="a70cb5e1-3d30-4653-a1c7-143951e01394" version="1">
<creationDate>2025-03-25T07:15:29.4354054Z</creationDate>
<activationDate>2025-03-25T07:15:29.4354054Z</activationDate>
<expirationDate>2025-06-23T07:15:29.4354054Z</expirationDate>
<descriptor deserializerType="Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.AuthenticatedEncryptorDescriptorDeserializer, Microsoft.AspNetCore.DataProtection, Version=9.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60">
<descriptor>
<encryption algorithm="AES_256_CBC" />
<validation algorithm="HMACSHA256" />
<masterKey p4:requiresEncryption="true" xmlns:p4="http://schemas.asp.net/2015/03/dataProtection">
<!-- Warning: the key below is in an unencrypted form. -->
<value>cifuLye+YqH4TtwH2kDyy85Y40mROXO8cbRASNJSCVi/QpqBI5JGcdRu84nVow9fTWbgmJssiwkYlf9pGiUgNw==</value>
</masterKey>
</descriptor>
</descriptor>
</key>
26 changes: 26 additions & 0 deletions TraceRoute/Components/App.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="This utility provides a Trace Route functionality with a nice layout" />
<title>TODO: TITLE - TraceRoute</title>
<link rel="stylesheet" href="@Assets["lib/twitter-bootstrap/css/bootstrap.min.css"]" />
<link rel="stylesheet" href="@Assets["css/site.css"]?t=@DateTime.Now.Ticks" />
<link rel="stylesheet" href="TraceRoute.styles.css">
<link rel="preload" href="@Assets["lib/font-awesome/css/all.min.css"]" as="style" onload="this.onload=null;this.rel='stylesheet'" />
<link rel="preload" href="@Assets["lib/leaflet/leaflet.min.css"]" as="style" onload="this.onload=null;this.rel='stylesheet'" />
<link rel="preconnect" href="https://tile.openstreetmap.org" />
<ImportMap />
<link rel="preload" fetchpriority="high" as="image" href="https://tile.openstreetmap.org/2/1/2.png" />
<HeadOutlet />
</head>
<body>
<Routes @rendermode=RenderMode.InteractiveServer/>
<script src="@Assets["lib/jquery/jquery.slim.min.js"]" asp-append-version="true"></script>
<script src="@Assets["lib/twitter-bootstrap/js/bootstrap.bundle.min.js"]"></script>
<script src="@Assets["js/site.min.js"]?t=@DateTime.Now.Ticks"></script>
<script src="@Assets["lib/leaflet/leaflet.min.js"]" async></script>
<script src="_framework/blazor.web.js"></script>
</body>
</html>
75 changes: 75 additions & 0 deletions TraceRoute/Components/Layout/MainLayout.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
@using Blazored.Toast
@using Blazored.Toast.Configuration
@using TraceRoute.Components.Molecules
@using TraceRoute.Components.Pages
@using TraceRoute.Helpers
@using TraceRoute.Models
@inherits LayoutComponentBase

<nav class="navbar navbar-dark bg-dark navbar-fixed-top">
<div class="container-fluid">
<a class="navbar-brand ms-3" href="/">TraceRoute</a>
<div class="spinner-border d-flex text-light ms-auto @(isTracing ? "d-show" : "d-none")" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<form class="form-inline d-md-flex ms-auto" role="search" @onsubmit="BeginTraceRoute">
<div class="input-group me-2">
<input type="text" @bind-value="hostToTrace" class="form-control my-1" placeholder="IP address or hostname" autocapitalize="off" spellcheck="false">
<button @onclick="BeginTraceRoute" class="btn btn-secondary my-1" title="Perform the Trace Route" type="button">
<i class="fa-solid fa-magnifying-glass"></i>
</button>
</div>
<div class="input-group me-1" hidden="@(!ConfigurationHelper.GetEnableRemoteTraces())">
<label class="input-group-text my-1" for="TraceSource">Source</label>
<select class="form-select my-1 mr-sm-2"
id="TraceSource"
@bind="selectedServerUrl"
@onclick="RefreshServerList"
placeholder="Select the source of the traceroute">
@foreach (ServerEntry server in serverList)
{
<option value="@server.url">@ShowServerEntry(server)</option>
})
</select>
<button @onclick="ShowServerDetails" class="btn btn-secondary my-1" type="button" aria-label="Show server info">
<i class="fa-solid fa-info"></i>
</button>
</div>
</form>
<ul class="navbar-nav">
<li class="nav-item me-2">
<a class="btn btn-secondary ms-1" data-bs-toggle="offcanvas" href="#offcanvasSettings" role="button" title="Settings">
<i class="fa-solid fa-gear"></i>
</a>
<a class="btn btn-secondary ms-1" data-bs-toggle="offcanvas" href="#offcanvasAbout" role="button" title="About...">
<i class="fa-solid">...</i>
</a>
</li>
</ul>
</div>
</nav>
<main role="main" class="container-fluid p-0">
<Home OnShowHopDetails="OnShowHopDetails"
Hops="traceResult?.Hops" />
</main>
<footer class="footer fixed-bottom navbar-dark bg-dark">
<p class="navbar-brand ms-3">
&copy; @DateTime.Now.Year - TraceRoute -
<i class="fa-brands fa-github lightLink"></i> <a class="lightLink" href="https://github.com/mdima/traceroute" target="_blank">GitHub</a> -
<i class="fa-brands fa-docker lightLink"></i> <a class="lightLink" href="https://hub.docker.com/r/michele73/traceroute" target="_blank">Docker</a>
</p>
</footer>
<div class="offcanvas offcanvas-end" tabindex="-1" id="offcanvasAbout" aria-labelledby="offcanvasRightLabel">
<About />
</div>
<div class="offcanvas offcanvas-end" tabindex="-1" id="offcanvasSettings" aria-labelledby="offcanvasRightLabel">
<Settings />
</div>
<BlazoredToasts Position="ToastPosition.TopRight"
ShowCloseButton="true"
ShowProgressBar="true"
Timeout="5"
MaxToastCount="5"
PauseProgressOnHover="true"
/>
<IpDetailsComponent currentHop="currentHop" />
190 changes: 190 additions & 0 deletions TraceRoute/Components/Layout/MainLayout.razor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
using Blazored.Toast;
using Blazored.Toast.Services;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using System.Net;
using System.Runtime;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using System.Xml.Schema;
using TraceRoute.Components.Molecules;
using TraceRoute.Components.Pages;
using TraceRoute.Controllers;
using TraceRoute.Helpers;
using TraceRoute.Models;
using TraceRoute.Services;
using static TraceRoute.Models.TraceResultViewModel;

[assembly: InternalsVisibleTo("UnitTests")]
namespace TraceRoute.Components.Layout
{
public partial class MainLayout(ServerListService serverListService, BogonIPService bogonIPService, IHttpContextAccessor contextAccessor, IJSRuntime jSRuntime,
TracerouteService tracerouteService, TraceRouteApiClient traceRouteApiClient)
{

[Inject]
private IpApiClient _ipApiClient { get; set; } = default!;

[Inject]
private ReverseLookupService _reverseLookupService { get; set; } = default!;

[Inject]
private IToastService _toastService { get; set; } = default!;

private readonly ServerListService _serverListService = serverListService;
private readonly BogonIPService _bogonIPService = bogonIPService;
private readonly IHttpContextAccessor _contextAccessor = contextAccessor;
private readonly IJSRuntime _jSRuntime = jSRuntime;
private readonly TracerouteService _tracerouteService = tracerouteService;
private readonly TraceRouteApiClient _traceRouteApiClient = traceRouteApiClient;
private DotNetObjectReference<MainLayout> _componentReference => DotNetObjectReference.Create(this);

internal List<ServerEntry> serverList = new();
internal String selectedServerUrl = "";
private Boolean isTracing = false;
internal string hostToTrace = "";

internal TraceResultViewModel? traceResult;
internal TraceHop? currentHop;

protected override void OnInitialized()
{
base.OnInitialized();
_serverListService.ServiceInitialized += RefreshServerList;
serverList = _serverListService.GetServerList();
selectedServerUrl = serverList.Where(x => x.isLocalHost).First().url;

if (_contextAccessor.HttpContext != null && _contextAccessor.HttpContext.Connection.RemoteIpAddress != null)
{
string clientIPAddress = _contextAccessor.HttpContext.Connection.RemoteIpAddress.ToString();
if (!_bogonIPService.IsBogonIP(clientIPAddress))
{
hostToTrace = clientIPAddress;
}
}
}

protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await _jSRuntime.InvokeVoidAsync("initMap");
await _jSRuntime.InvokeVoidAsync("setDotNetHelper", _componentReference);
}
}

internal String? ShowServerEntry(ServerEntry serverEntry)
{
if (serverEntry.Details.Country != null && serverEntry.Details.City != null)
{
return serverEntry.Details.Country + " - " + serverEntry.Details.City + " - " + serverEntry.url;
}
else
{
return serverEntry.url;
}
}

internal async void RefreshServerList()
{
serverList = _serverListService.GetServerList();
if (!serverList.Where(x => x.url == selectedServerUrl).Any())
{
selectedServerUrl = serverList.Where(x => x.isLocalHost).First().url;
}
await InvokeAsync(() => {
StateHasChanged();
});
}

internal async Task BeginTraceRoute()
{
isTracing = true;
await _jSRuntime.InvokeVoidAsync("clearMarkersAndPaths");

ServerEntry? selectedServer = serverList.FirstOrDefault(x => x.url == selectedServerUrl);
if (selectedServer == null)
{
_toastService.ShowError("Please select a server to trace.");
isTracing = false;
return;
}

if (selectedServer.isLocalHost)
{
traceResult = await _tracerouteService.TraceRouteFull(hostToTrace);
}
else
{
traceResult = await _traceRouteApiClient.RemoteTrace(hostToTrace, selectedServer.url!);
}

if (traceResult.ErrorDescription != String.Empty)
{
_toastService.ShowError(traceResult.ErrorDescription);
}
else
{
foreach (TraceHop item in traceResult.Hops)
{
if (item.Details.IsBogonIP)
{
item.Details.City = "-";
item.Details.ISP = "Internal IP address";
}
else
{
IpDetails? details = await _ipApiClient.GetTraceHopDetails(item.HopAddress);
if (details != null)
{
item.Details = details;
await _jSRuntime.InvokeVoidAsync("addMarker", new Object[] { details.Latitude!, details.Longitude!, item.Index.ToString(), item.HopAddress });
StateHasChanged();
}
}
await _jSRuntime.InvokeVoidAsync("drawPath", new[] { traceResult.Hops });
}
}
isTracing = false;
}

internal async Task OnShowHopDetails(TraceHop hop)
{
currentHop = hop;
await _jSRuntime.InvokeVoidAsync("showModalDetails");
}

[JSInvokable("OnShowIpDetails")]
public async Task OnShowIpDetails(String iPAddress)
{
if (traceResult == null) return;

TraceHop? details = traceResult.Hops.Where(x => x.HopAddress == iPAddress).FirstOrDefault();

if (details == null)
{
_toastService.ShowError("Unable to retrieve details for IP: " + iPAddress);
}
else
{
currentHop = details;
StateHasChanged();
await _jSRuntime.InvokeVoidAsync("showModalDetails");
}
}

internal async Task ShowServerDetails()
{
ServerEntry? selectedServer = serverList.FirstOrDefault(x => x.url == selectedServerUrl);
if (selectedServer != null)
{
TraceHop traceHop = new()
{
Details = selectedServer.Details,
HopAddress = selectedServer.url!,
};
await OnShowHopDetails(traceHop);
}
}
}
}
Loading
Loading