# Measure your ISP's service quality

My home is only wired for access to two internet service providers, with the fastest download speed from one at 50 Mbps and the other at 500. Naturally, I use the faster option, but I rarely achieve the speeds I pay for, and that's while giving them the benefit of doubt -- I'm no network expert so I assume the fastest I've noticed my wireless devices achieve, ~350 Mbps, is the best possible under my combinations of router, phone, laptop, and network drivers. I frequently encounter network issues, from slow websites, video streaming, or other downloads to dropped calls, both personal and work-related. Recent occurrences had me searching for ways to monitor my network, to confidently explain my ISP's poor service quality.

Having worked in IT, I've dabbled with the packet analysis tool, [Wireshark](https://www.wireshark.org/), so my first thought was: *can Wireshark also monitor network latency?* A quick search showed that, while technically possible, this is not the tool for dedicated latency monitoring. The answers also led me to a tool for this purpose, [SmokePing](https://oss.oetiker.ch/smokeping/). Unfortunately, as a Windows user, `SmokePing` is only built for Linux, but I did find a healthier alternative, [Vaping](https://github.com/20c/vaping).

![SmokePing UI Example](https://cdn.hashnode.com/res/hashnode/image/upload/v1709505614202/6e855f8a-bf6d-47c7-8592-ca00681e9789.png align="center")

With a few tweaks to the original repository, available in the [develop branch of my Vaping fork](https://github.com/CAndRyan/vaping/tree/develop), and some irony, I achieved a Linux-based Docker deployment. The original project includes some outdated dependencies with known vulnerabilities, according to [Docker Scout](https://docs.docker.com/scout/), hence my own modifications.

![Vaping UI Example](https://cdn.hashnode.com/res/hashnode/image/upload/v1709506505527/992cfb37-49e4-42d0-8ede-b4e705b0774d.png align="center")

You may notice, both `SmokePing` and `Vaping` employ some ancient-looking graphing tools. They certainly function well, but I'm not one to pass up an opportunity for improvement. So I set off to make my own tool using dependencies and languages I'm more familiar with. Enter: [NetMon](https://github.com/CAndRyan/MTBjorn.NetMon) with a .NET back-end, SQL persistence, and JavaScript UI.

## Anatomy of a network monitor

At their core, `SmokePing` and `Vaping` use [fping](https://www.fping.org/) as their network "probe." `Fping` is an efficient utility for making ping, or ICMP, sweeps -- i.e., pinging multiple hosts at once. Perhaps in the future I'll update my `NetMon` tool for efficiency but for now [.NET's Ping](https://learn.microsoft.com/en-us/dotnet/api/system.net.networkinformation.ping?view=net-6.0) ability will do just fine.

```csharp
private static async Task<(DateTime, long?)> Ping(int requestTimeout = 1000)
{
    var googleDnsIpAddress = "8.8.8.8";
    var ipAddress = IPAddress.Parse(googleDnsIpAddress);

    var byteSize = 32;
    var data = string.Join(string.Empty, Enumerable.Repeat('a', byteSize));
    var buffer = Encoding.ASCII.GetBytes(data);

    var allowedNumberOfHops = 64;
    var dontFragment = true;
    var options = new PingOptions(allowedNumberOfHops, dontFragment);

	var timestamp = DateTime.Now;
    var result = await new Ping().SendPingAsync(ipAddress, requestTimeout, buffer, options);

    var latency = result.Status is IPStatus.Success ? result.RoundtripTime : (long?)null;

	return (timestamp, latency);
}
```

To monitor the latency between your network and Google's DNS server (i.e., `8.8.8.8`), this `Ping` method just needs to be called in a loop -- e.g., every 3 seconds over a 5 minute window.

```csharp
public async Task Monitor()
{
	var resolution = 3000;
	var window = TimeSpan.FromMinutes(5);
    var windowMs = (int)window.TotalMilliseconds;

    var monitorStopwatch = Stopwatch.StartNew();

    do
    {
        var pingStopwatch = Stopwatch.StartNew();
        var (timestamp, latency) = await Ping();

		await SaveResult(timestamp, latency);
        pingStopwatch.Stop();

		var timeBeforeNextRequest = resolution - (int)pingStopwatch.ElapsedMilliseconds;
		Thread.Sleep(timeBeforeNextRequest);
    } while (monitorStopwatch.ElapsedMilliseconds < windowMs);

    monitorStopwatch.Stop();
}
```

For data storage, `SmokePing` and `Vaping` rely on fixed-size round robin databases (RDD) such as [RRDtool](https://oss.oetiker.ch/rrdtool/) or [Graphite's whisper](https://graphite.readthedocs.io/en/latest/whisper.html). These tools are specifically built to handle time series data, making them ideal for monitoring tasks. For my implementation, I chose the lightweight file-based SQL database, [SQLite](https://www.sqlite.org/).

```csharp
private async Task SaveResult(DateTime timestamp, long? latency)
{
    var connectionString = "Data Source=PATH_TO_FILE.db";

    await using var connection = new SqliteConnection(connectionString);
    connection.Open();

    var command = connection.CreateCommand();
    command.CommandText = @"
INSERT INTO PingResults (Timestamp, Latency)
VALUES ($timestamp, $latency)
";
    command.Parameters.AddWithValue("$timestamp", timestamp);
    command.Parameters.AddWithValue("$latency", latency is null ? DBNull.Value : latency.Value);

    _ = await command.ExecuteNonQueryAsync();
}
```

To display it's data, `SmokePing` leverages that aspect of `RDDtool` while `Vaping` uses it's author's [graphsrv](https://github.com/20c/graphsrv) utility and/or [graphite](https://graphiteapp.org/), depending on how it's been configured. My own tool uses a simple API built on ASP.NET and renders graphs with [Plotly.js](https://plotly.com/javascript/).

```javascript
const getMonitorResults = async () => {
	const getUrl = "/NetworkLatency/GetResults"
	const response = await fetch(getUrl, { method: 'get' });

	return await response.json();
};

const renderPlot = (element, points) => {
	const xValues = points.map(({ timestamp }) => timestamp);
	const yValues = points.map(({ latency }) => latency);
	const trace = {
		x: xValues,
		y: yValues,
		type: 'scatter'
	};
	const largestYValue = [...yValues].sort((a, b) => a - b)[yValues.length - 1];
	const maxYValue = largestYValue > 100 ? largestYValue : 100;
	const layout = {
		title: { text: 'Network Latency'},
		xaxis: { title: { text: 'Time' } },
		yaxis: {
			title: { text: 'Latency (ms)' },
			range: [0, maxYValue]
		}
	};

	Plotly.newPlot(element, [trace], layout);
}
```

While the networking monitoring runs on the server, the UI is refreshed on the same cadence using simple JavaScript.

```javascript
const getIsMonitorActive = async () => {
	const getUrl = "/NetworkLatency/IsMonitorActive"
	const response = await fetch(getUrl, { method: 'get' });
	const result = await response.text();

	return result.toLowerCase() === 'true';
};

const pollForResults = async () => {
	const isMonitorActive = await getIsMonitorActive();
	const results = await getMonitorResults();

	if (results.length > 0)
		plotMonitorResults(results);

	if (isMonitorActive)
		setTimeout(pollForResults, 3000);
	else
		console.log('Monitor not active');
};

document.addEventListener("DOMContentLoaded", function () {
    setTimeout(pollForResults, 100);
};
```

Altogether my `NetMon` tool looks like the following...

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1709502488101/2c71fc2c-6d0c-4a30-832d-19a7309e4080.png align="center")

## Home Setup

To truly monitor my home network and avoid leaving my laptop on with `NetMon` constantly running, I decided to utilize my old [Raspberry Pi 3 Model B](https://www.raspberrypi.com/products/raspberry-pi-3-model-b/) which comes equipped with WiFi.

To simplify this process, I containerized `NetMon` with [Docker](https://www.docker.com/)! The following `Dockerfile` will compile the ASP.NET application to run on the RasPi's 32-bit `ARMv7` processor and build an image with the appropriate runtime.

```dockerfile
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build-env
WORKDIR /app

# Copy, restore as distinct layers, and build/publish
COPY . ./
RUN dotnet restore
RUN dotnet publish -c Release -r linux-arm -o out

# Build runtime image
FROM mcr.microsoft.com/dotnet/aspnet:6.0.0-focal-arm32v7
ENV TZ="America/New_York"
WORKDIR /app
COPY --from=build-env /app/out .
ENTRYPOINT ["dotnet", "MTBjorn.NetMon.Web.dll"]

EXPOSE 7129
```

The image is built on my laptop and then loaded on the RasPi. The following PowerShell script uploads a compressed version of the Docker image to the RasPi over SSH using [Putty tools](https://www.putty.org/), into the Downloads directory of my RasPi user. This assumes `Docker` has already been installed on the RasPi and SSH has been configured with private keys managed in `Pageant`, Putty's authentication agent -- of course, it could also be handled via password authentication.

```powershell
# Build image
docker build -t netmon/v0.1 .

# Save image as a tarball
docker save -o "C:\image-netmon-v0.1.tar" netmon/v0.1

# Upload image over Putty's SSH file transfer
PSCP.EXE "C:\image-netmon-v0.1.tar" user@SERVER:Downloads/image-netmon-v0.1.tar
```

Once uploaded to the RasPi, a `Docker` container can be built and started.

```bash
# Load image into Docker
docker load -i Downloads/image-netmon-v0.1.tar

# Create container and map local port 7129 to container port 80
docker create --name netmon -p 7129:80 netmon/v0.1

# Start container
docker start netmon
```

With the container running on my RasPi, I'm able to access `NetMon` from any device in my home network at `http://my-local-raspi:7129` 😎

## Conclusion

I plan to enhance `NetMon` with more functionality and an improved UI; check it out on GitHub: [NetMon](https://github.com/CAndRyan/MTBjorn.NetMon).
