feat(server): adds minimum keepalive and headroom, chore: updates packages to fix vulnerabilities

This commit is contained in:
2026-07-29 13:18:19 +02:00
parent 72051aae0e
commit ca71fafa4f
12 changed files with 181 additions and 22 deletions
@@ -0,0 +1,18 @@
using Ngino.Server;
using Xunit;
namespace ReverseLlama.Client.Tests;
public class GroupKeepalivePolicyTests
{
[Fact]
public void KeepalivePolicy_IsRoundTripped_ThroughGroupClientInfo()
{
var policy = new GroupClientKeepalivePolicy(2, 3, 4);
var info = new GroupClientInfo(1, "group-1", "client-1", "model-a", "pattern", policy);
Assert.Equal(2, info.KeepalivePolicy?.InstancesToKeepAlive);
Assert.Equal(3, info.KeepalivePolicy?.MaxParallelismPerClient);
Assert.Equal(4, info.KeepalivePolicy?.ParallelismHeadroom);
}
}
@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.8.1" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Ngino.Client\Ngino.Client.csproj" />
<ProjectReference Include="..\..\src\Ngino.Server\Ngino.Server.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,153 @@
using System.Diagnostics;
using System.Text.Json;
using Xunit;
namespace Ngino.Client.Tests;
public sealed class PackageAuditTests
{
[Fact]
public async Task Solution_HasNoKnownVulnerablePackages()
{
var solutionPath = FindSolutionPath();
var result = await RunDotnetPackageAuditAsync(solutionPath);
Assert.True(
result.ExitCode == 0,
$"Package audit command failed with exit code {result.ExitCode}.{Environment.NewLine}{result.Output}{result.Error}");
using var document = JsonDocument.Parse(result.Output);
var findings = new List<string>();
CollectVulnerablePackages(document.RootElement, findings);
Assert.True(
findings.Count == 0,
"Known vulnerable packages were found:"
+ Environment.NewLine
+ string.Join(Environment.NewLine, findings));
}
private static string FindSolutionPath()
{
var directory = new DirectoryInfo(AppContext.BaseDirectory);
while (directory is not null)
{
var solutionPath = Path.Combine(directory.FullName, "Ngino.sln");
if (File.Exists(solutionPath))
{
return solutionPath;
}
directory = directory.Parent;
}
throw new InvalidOperationException("Could not find Ngino.sln from the test output directory.");
}
private static async Task<CommandResult> RunDotnetPackageAuditAsync(string solutionPath)
{
var startInfo = new ProcessStartInfo
{
FileName = "dotnet",
WorkingDirectory = Path.GetDirectoryName(solutionPath)!,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false
};
startInfo.ArgumentList.Add("list");
startInfo.ArgumentList.Add(solutionPath);
startInfo.ArgumentList.Add("package");
startInfo.ArgumentList.Add("--vulnerable");
startInfo.ArgumentList.Add("--include-transitive");
startInfo.ArgumentList.Add("--format");
startInfo.ArgumentList.Add("json");
startInfo.Environment["DOTNET_CLI_TELEMETRY_OPTOUT"] = "1";
using var process = Process.Start(startInfo)
?? throw new InvalidOperationException("Could not start dotnet package audit.");
var outputTask = process.StandardOutput.ReadToEndAsync();
var errorTask = process.StandardError.ReadToEndAsync();
using var timeout = new CancellationTokenSource(TimeSpan.FromMinutes(2));
try
{
await process.WaitForExitAsync(timeout.Token);
}
catch (OperationCanceledException)
{
try
{
process.Kill(entireProcessTree: true);
}
catch
{
// Best effort; the assertion below will fail with the timeout message.
}
throw new TimeoutException("dotnet package audit did not finish within 2 minutes.");
}
return new CommandResult(
process.ExitCode,
await outputTask,
await errorTask);
}
private static void CollectVulnerablePackages(JsonElement element, List<string> findings)
{
if (element.ValueKind == JsonValueKind.Object)
{
if (element.TryGetProperty("vulnerabilities", out var vulnerabilities)
&& vulnerabilities.ValueKind == JsonValueKind.Array
&& vulnerabilities.GetArrayLength() > 0)
{
findings.Add(DescribePackageFinding(element, vulnerabilities));
}
foreach (var property in element.EnumerateObject())
{
CollectVulnerablePackages(property.Value, findings);
}
}
else if (element.ValueKind == JsonValueKind.Array)
{
foreach (var item in element.EnumerateArray())
{
CollectVulnerablePackages(item, findings);
}
}
}
private static string DescribePackageFinding(JsonElement package, JsonElement vulnerabilities)
{
var id = package.TryGetProperty("id", out var idElement)
? idElement.GetString()
: "<unknown package>";
var version = package.TryGetProperty("resolvedVersion", out var versionElement)
? versionElement.GetString()
: "<unknown version>";
var advisories = vulnerabilities
.EnumerateArray()
.Select(vulnerability => DescribeVulnerability(vulnerability))
.ToArray();
return $"- {id} {version}: {string.Join(", ", advisories)}";
}
private static string DescribeVulnerability(JsonElement vulnerability)
{
var severity = vulnerability.TryGetProperty("severity", out var severityElement)
? severityElement.GetString()
: "unknown severity";
var advisoryUrl = vulnerability.TryGetProperty("advisoryUrl", out var advisoryElement)
? advisoryElement.GetString()
: "unknown advisory";
return $"{severity} {advisoryUrl}";
}
private sealed record CommandResult(int ExitCode, string Output, string Error);
}
@@ -0,0 +1,62 @@
using System.Text.Json;
using Ngino.Client;
using Xunit;
namespace Ngino.Client.Tests;
public sealed class TunnelClientModelTests
{
private static readonly Uri Upstream = new("http://localhost:11434");
[Fact]
public void ExtractModelNames_ReadsActiveOllamaPsModelNames()
{
using var document = JsonDocument.Parse(
"""
{
"models": [
{ "name": "qwen3.5:0.8b", "model": "qwen3.5:0.8b" },
{ "name": "llama3.2:latest", "model": "llama3.2:latest" },
{ "name": " QWEN3.5:0.8B " }
]
}
""");
var models = TunnelClient.ExtractModelNames(document.RootElement);
Assert.Equal(["llama3.2:latest", "qwen3.5:0.8b"], models);
}
[Fact]
public async Task BuildModelCommandRequest_LoadUsesOllamaPreloadRequest()
{
using var request = TunnelClient.BuildModelCommandRequest(Upstream, "load", " qwen3.5:0.8b ");
var body = await request.Content!.ReadAsStringAsync();
using var document = JsonDocument.Parse(body);
Assert.Equal(HttpMethod.Post, request.Method);
Assert.Equal("http://localhost:11434/api/generate", request.RequestUri!.AbsoluteUri);
Assert.Equal("qwen3.5:0.8b", document.RootElement.GetProperty("model").GetString());
Assert.False(document.RootElement.TryGetProperty("prompt", out _));
Assert.False(document.RootElement.GetProperty("stream").GetBoolean());
Assert.Equal(-1, document.RootElement.GetProperty("keep_alive").GetInt32());
}
[Theory]
[InlineData("load", -1)]
[InlineData("unload", 0)]
public async Task BuildEmbeddingModelCommandRequest_UsesOllamaEmbedWarmupRequest(
string command,
int expectedKeepAlive)
{
using var request = TunnelClient.BuildEmbeddingModelCommandRequest(Upstream, command, " bge-m3:latest ");
var body = await request.Content!.ReadAsStringAsync();
using var document = JsonDocument.Parse(body);
Assert.Equal(HttpMethod.Post, request.Method);
Assert.Equal("http://localhost:11434/api/embed", request.RequestUri!.AbsoluteUri);
Assert.Equal("bge-m3:latest", document.RootElement.GetProperty("model").GetString());
Assert.Equal("Ngino warmup", document.RootElement.GetProperty("input").GetString());
Assert.Equal(expectedKeepAlive, document.RootElement.GetProperty("keep_alive").GetInt32());
}
}
@@ -0,0 +1,35 @@
using Ngino.Client;
using Xunit;
namespace Ngino.Client.Tests;
public sealed class UpstreamRequestTests
{
private static readonly Uri Upstream = new("http://localhost:11434");
[Theory]
[InlineData("/api/tags", "http://localhost:11434/api/tags")]
[InlineData("/api/tags?model=llama3.1", "http://localhost:11434/api/tags?model=llama3.1")]
[InlineData("/api//tags", "http://localhost:11434/api//tags")]
public void BuildUpstreamUri_AcceptsOriginFormPaths(string pathAndQuery, string expected)
{
var uri = UpstreamRequest.BuildUpstreamUri(Upstream, pathAndQuery);
Assert.Equal(expected, uri.AbsoluteUri);
}
[Theory]
[InlineData("//169.254.169.254/latest")]
[InlineData("http://169.254.169.254/latest")]
[InlineData("https://localhost:11434/api/tags")]
[InlineData(@"\\169.254.169.254\latest")]
[InlineData(@"/\169.254.169.254/latest")]
[InlineData("api/tags")]
public void BuildUpstreamUri_RejectsPathsThatCanEscapeTheUpstreamOrigin(string pathAndQuery)
{
var exception = Assert.Throws<InvalidOperationException>(
() => UpstreamRequest.BuildUpstreamUri(Upstream, pathAndQuery));
Assert.Contains("origin-form path", exception.Message);
}
}