Skip to content
Open
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
43 changes: 27 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,17 +114,18 @@ Options:
Default value is: 8.8.8.8.
--no-certificate-validation Disable certificate validation
--no-discovery Disable automatic Kubernetes endpoint discovery
--no-ui Disable terminal UI
-f|--forwarder <FORWARDER> Forwarding method
Allowed values are: tcp, http, hybrid.
Default value is: hybrid.
-r|--routing <ROUTING> Routing method
Allowed values are: hosts, windivert.
Default value is: windivert.
-?|-h|--help Show help information.

Environment variables:
KRP_HOSTS Override path to hosts file
--no-ui Disable terminal UI
-f|--forwarder <FORWARDER> Forwarding method
Allowed values are: tcp, http, hybrid.
Default value is: hybrid.
-r|--routing <ROUTING> Routing method
Allowed values are: hosts, windivert, dnsmasq.
Default value is: windivert.
-?|-h|--help Show help information.

Environment variables:
KRP_HOSTS Override path to hosts file
KRP_DNSMASQ_OVERRIDE Override path to dnsmasq override configuration
```

### Routing methods
Expand All @@ -139,7 +140,7 @@ Environment variables:
- Requires administrator privileges when running in docker.
- Requires a mounted path and env variable `KRP_HOSTS` when running in docker.

#### `windivert` (**default**)
#### `windivert` (**default**)

[WinDivert](https://github.com/basil00/WinDivert) is a Windows packet capture and manipulation tool used by `krp` to implement a transparent UDP proxy for the DNS protocol. It redirect DNS traffic for endpoint hostnames. When enabled, WinDivert allows `krp` to dynamically reroute traffic to loopback addresses.

Expand All @@ -149,10 +150,20 @@ Environment variables:

**Requirements**
- Only supported on Windows.
- Running WinDivert requires administrator privileges to capture and inject network packets.

> [!NOTE]
> WinDiverts installs as an ephemeral windows driver service at runtime. Once stopped, it's automatically removed.
- Running WinDivert requires administrator privileges to capture and inject network packets.

> [!NOTE]
> WinDiverts installs as an ephemeral windows driver service at runtime. Once stopped, it's automatically removed.

#### `dnsmasq`

1. Writes endpoint mappings to a dnsmasq configuration override (default: `/run/dnsmasq/krp.override.conf`).
2. Each endpoint hostname is written as an `address` directive pointing to its assigned loopback IP (e.g., `address=/myapp.local/127.0.0.2`).
3. The override file is ephemeral; remove `krp` or its file to discard the entries.

**Requirements**
- dnsmasq must already be installed and configured to read override files (e.g., `--conf-dir=/run/dnsmasq`)
- Write access to the configured override directory.

### Forwarders available

Expand Down
38 changes: 22 additions & 16 deletions src/Krp/DependencyInjection/KubernetesForwarderBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -119,22 +119,28 @@ public static KubernetesForwarderBuilder UseRouting(this KubernetesForwarderBuil
{
builder.Services.AddHostedService<DnsBackgroundService>();

switch (routing)
{
case DnsOptions.HostsFile:
var hostsPath = Environment.GetEnvironmentVariable("KRP_HOSTS") ?? (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System), @"drivers\etc\hosts")
: "/etc/hosts");

builder.Services.AddSingleton<IDnsHandler, DnsHostsHandler>();
builder.Services.Configure<DnsHostsOptions>(o => o.Path = hostsPath);
break;
case DnsOptions.WinDivert:
builder.Services.AddSingleton<IDnsHandler, DnsWinDivertHandler>();
break;
default:
throw new ArgumentOutOfRangeException(nameof(routing), routing, $"Invalid value for {nameof(DnsOptions)}");
}
switch (routing)
{
case DnsOptions.HostsFile:
var hostsPath = Environment.GetEnvironmentVariable("KRP_HOSTS") ?? (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System), @"drivers\etc\hosts")
: "/etc/hosts");

builder.Services.AddSingleton<IDnsHandler, DnsHostsHandler>();
builder.Services.Configure<DnsHostsOptions>(o => o.Path = hostsPath);
break;
case DnsOptions.WinDivert:
builder.Services.AddSingleton<IDnsHandler, DnsWinDivertHandler>();
break;
case DnsOptions.DnsMasq:
var dnsMasqOverridePath = Environment.GetEnvironmentVariable("KRP_DNSMASQ_OVERRIDE") ?? "/run/dnsmasq/krp.override.conf";

builder.Services.AddSingleton<IDnsHandler, DnsMasqHandler>();
builder.Services.Configure<DnsMasqOptions>(o => o.OverridePath = dnsMasqOverridePath);
break;
default:
throw new ArgumentOutOfRangeException(nameof(routing), routing, $"Invalid value for {nameof(DnsOptions)}");
}

return builder;
}
Expand Down
82 changes: 82 additions & 0 deletions src/Krp/Dns/DnsMasqHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace Krp.Dns;

/// <summary>
/// Writes endpoint mappings to a dnsmasq configuration override.
/// <para>
/// Each hostname is written as an <c>address</c> directive pointing to the
/// assigned loopback IP. The override file is placed in a run-time directory so
/// it remains ephemeral.
/// </para>
/// </summary>
public class DnsMasqHandler : IDnsHandler
{
private readonly DnsMasqOptions _options;
private readonly ILogger<DnsMasqHandler> _logger;

public DnsMasqHandler(IOptions<DnsMasqOptions> options, ILogger<DnsMasqHandler> logger)
{
_options = options.Value;
_logger = logger;
}

public async Task RunAsync(CancellationToken stoppingToken)
{
await Task.CompletedTask;
}

public async Task UpdateAsync(List<string> hostnames)
{
try
{
var configLines = hostnames
.Select(line => line.Split(' ', StringSplitOptions.RemoveEmptyEntries))
.Where(parts => parts.Length == 2)
.Select(parts => $"address=/{parts[1]}/{parts[0]}")
.ToList();

var directory = Path.GetDirectoryName(_options.OverridePath);
if (string.IsNullOrEmpty(directory))
{
_logger.LogError("dnsmasq override path is invalid: {path}", _options.OverridePath);
return;
}

Directory.CreateDirectory(directory);

var lines = new List<string>
{
"# Managed by krp",
"# Generated dnsmasq overrides for loopback routing.",
};

lines.AddRange(configLines);

if (File.Exists(_options.OverridePath))
{
var existing = await File.ReadAllLinesAsync(_options.OverridePath);
if (existing.SequenceEqual(lines))
{
_logger.LogInformation("Skipped updating DNS due to no changes in dnsmasq override file");
return;
}
}

await File.WriteAllLinesAsync(_options.OverridePath, lines);
_logger.LogInformation("Successfully updated dnsmasq override ({count} entries)", configLines.Count);
}
catch (Exception e)
{
_logger.LogError(e, "Error when updating dnsmasq override file");
throw;
}
}
}
9 changes: 9 additions & 0 deletions src/Krp/Dns/DnsMasqOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Krp.Dns;

public class DnsMasqOptions
{
/// <summary>
/// Path to the dnsmasq override configuration file.
/// </summary>
public string OverridePath { get; set; } = "/run/dnsmasq/krp.override.conf";
}
7 changes: 6 additions & 1 deletion src/Krp/Dns/DnsOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,14 @@ public enum DnsOptions
/// Use HOSTS file for DNS routing.
/// </summary>
HostsFile,

/// <summary>
/// Use WinDivert for DNS routing.
/// </summary>
WinDivert,

/// <summary>
/// Use dnsmasq for DNS routing.
/// </summary>
DnsMasq,
}
78 changes: 47 additions & 31 deletions src/Krp/Validation/ValidationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,26 @@ namespace Krp.Validation;
public class ValidationService : IHostedService
{
private readonly ILogger<ValidationService> _logger;
private readonly IOptions<DnsHostsOptions> _dnsOptions;
private readonly IOptions<DnsHostsOptions> _dnsOptions;
private readonly IOptions<DnsMasqOptions> _dnsMasqOptions;
private readonly EndpointManager _endpointManager;
private readonly KubernetesClient _kubernetesClient;
private readonly IDnsHandler _dnsHandler;

public ValidationService(EndpointManager endpointManager, KubernetesClient kubernetesClient, IDnsHandler dnsHandler, ILogger<ValidationService> logger, IOptions<DnsHostsOptions> dnsOptions)
{
_endpointManager = endpointManager;
_kubernetesClient = kubernetesClient;
_dnsHandler = dnsHandler;
_logger = logger;
_dnsOptions = dnsOptions;
}
public ValidationService(EndpointManager endpointManager, KubernetesClient kubernetesClient, IDnsHandler dnsHandler, ILogger<ValidationService> logger, IOptions<DnsHostsOptions> dnsOptions, IOptions<DnsMasqOptions> dnsMasqOptions)
{
_endpointManager = endpointManager;
_kubernetesClient = kubernetesClient;
_dnsHandler = dnsHandler;
_logger = logger;
_dnsOptions = dnsOptions;
_dnsMasqOptions = dnsMasqOptions;
}

public async Task StartAsync(CancellationToken cancellationToken)
{
var hostsPath = _dnsOptions.Value.Path;
var hostsPath = _dnsOptions.Value.Path;
var dnsMasqOverridePath = _dnsMasqOptions.Value.OverridePath;

_logger.LogInformation($"✅ Platform: {RuntimeInformation.OSArchitecture}");

Expand All @@ -45,7 +48,7 @@ public async Task StartAsync(CancellationToken cancellationToken)
_logger.LogInformation(" - Endpoint targets will be selected using IP:PORT");
}

var validationSuccess = ValidateRouting(hostsPath);
var validationSuccess = ValidateRouting(hostsPath, dnsMasqOverridePath);
validationSuccess = await ValidateKubernetes() && validationSuccess;

if (!validationSuccess)
Expand All @@ -62,21 +65,22 @@ public Task StopAsync(CancellationToken cancellationToken)
return Task.CompletedTask;
}

private bool ValidateRouting(string hostsPath)
{
var routing = _dnsHandler.GetType();

var routingName = routing.Name switch
{
nameof(DnsHostsHandler) => "hosts",
nameof(DnsWinDivertHandler) => "windivert",
_ => "unknown"
};

_logger.LogInformation($"✅ Using routing: {routingName}");

if (routing == typeof(DnsWinDivertHandler))
{
private bool ValidateRouting(string hostsPath, string dnsMasqOverridePath)
{
var routing = _dnsHandler.GetType();

var routingName = routing.Name switch
{
nameof(DnsHostsHandler) => "hosts",
nameof(DnsWinDivertHandler) => "windivert",
nameof(DnsMasqHandler) => "dnsmasq",
_ => "unknown"
};

_logger.LogInformation($"✅ Using routing: {routingName}");

if (routing == typeof(DnsWinDivertHandler))
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
_logger.LogInformation("❌ WinDivert routing is only supported on Windows platforms");
Expand All @@ -86,11 +90,23 @@ private bool ValidateRouting(string hostsPath)
_logger.LogInformation("✅ Found windows service: WinDivert");
}

return true;
}

var fileExists = File.Exists(hostsPath);
var hasAccess = FileHelper.HasWriteAccess(hostsPath);
return true;
}

if (routing == typeof(DnsMasqHandler))
{
var directory = Path.GetDirectoryName(dnsMasqOverridePath);
var directoryExists = !string.IsNullOrEmpty(directory) && Directory.Exists(directory);
var hasAccess = directoryExists && FileHelper.HasWriteAccess(directory);

_logger.LogInformation(directoryExists ? "✅ Found dnsmasq directory: '{DirectoryPath}'" : "❌ dnsmasq directory not found: '{DirectoryPath}'", directory ?? "");
_logger.LogInformation(hasAccess ? "✅ Permission to dnsmasq directory" : "❌ Write-access to dnsmasq directory is denied");

return directoryExists && hasAccess;
}

var fileExists = File.Exists(hostsPath);
var hasAccess = FileHelper.HasWriteAccess(hostsPath);

_logger.LogInformation(fileExists ? "✅ Found hosts file: '{HostsPath}'" : "❌ Hosts file not found: '{HostsPath}'", hostsPath);
_logger.LogInformation(hasAccess ? "✅ Permission to hosts file" : "❌ Write-access to hosts file is denied");
Expand Down
35 changes: 19 additions & 16 deletions src/dotnet-krp/Commands/RootCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,15 @@ public class RootCommand
public bool NoDiscovery { get; init; } = false;

[Option("--no-ui", Description = "Disable terminal UI")]
public bool NoTerminalUi { get; init; } = false;
[Option("--forwarder|-f <FORWARDER>", Description = "Forwarding method")]
[AllowedValues("tcp", "http", "hybrid", IgnoreCase = true)]
public string Forwarder { get; init; } = "hybrid";
[Option("--routing|-r <ROUTING>", Description = "Routing method")]
[AllowedValues("hosts", "windivert", IgnoreCase = true)]
public string Routing { get; init; }
public bool NoTerminalUi { get; init; } = false;

[Option("--forwarder|-f <FORWARDER>", Description = "Forwarding method")]
[AllowedValues("tcp", "http", "hybrid", IgnoreCase = true)]
public string Forwarder { get; init; } = "hybrid";

[Option("--routing|-r <ROUTING>", Description = "Routing method")]
[AllowedValues("hosts", "windivert", "dnsmasq", IgnoreCase = true)]
public string Routing { get; init; }

public RootCommand()
{
Expand Down Expand Up @@ -71,13 +71,16 @@ public async Task<int> OnExecuteAsync(CommandLineApplication _, CancellationToke

switch (Routing)
{
case "hosts":
builder.UseRouting(DnsOptions.HostsFile);
break;
case "windivert":
builder.UseRouting(DnsOptions.WinDivert);
break;
}
case "hosts":
builder.UseRouting(DnsOptions.HostsFile);
break;
case "windivert":
builder.UseRouting(DnsOptions.WinDivert);
break;
case "dnsmasq":
builder.UseRouting(DnsOptions.DnsMasq);
break;
}
switch (Forwarder)
{
case "http":
Expand Down