This commit is contained in:
femsci 2023-06-14 11:22:29 +02:00
parent 2f87a95828
commit c838711e3e
Signed by: femsci
GPG key ID: 08F7911F0E650C67
23 changed files with 454 additions and 25 deletions

57
README.md Normal file
View file

@ -0,0 +1,57 @@
# Nyangate
NYANET reverse proxy for HTTP
## Features
- [ ] configurable routing
- [ ] load balancing
- [ ] access control
- [ ] integration with IPMeow IPAM
- [ ] header stripping and rewriting
- [ ] intrusion detection
- [ ] rate limiting
- [ ] CLI interface
- [ ] REST API
## Building
Building with Docker
```bash
docker build . --tag nyangate
```
Local compilation
```bash
dotnet build
```
## Running
To run with Docker
```bash
docker run -it nyanet/nyangate
```
or docker-compose
```yaml
version: '3.3'
services:
nyangate:
ports:
- '80:80'
- '443:443'
volumes:
- './data:/srv/nyangate/config'
- './certs:/etc/tls/certs:ro'
restart: always
image: nyanet/nyangate
```
```bash
docker-compose up -d
```

52
main.key.pem Normal file
View file

@ -0,0 +1,52 @@
-----BEGIN PRIVATE KEY-----
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
A6a/XxsG5QEM+okhljJX1TmpmsjsiA==
-----END PRIVATE KEY-----

29
main.pem Normal file
View file

@ -0,0 +1,29 @@
-----BEGIN CERTIFICATE-----
MIIFAzCCAuugAwIBAgIUSSnKwcDCcz5MhNWZ/eV+uhBap6kwDQYJKoZIhvcNAQEL
BQAwETEPMA0GA1UEAwwGTllBTkVUMB4XDTIzMDMyODEwMzMxM1oXDTI0MDMyNzEw
MzMxM1owETEPMA0GA1UEAwwGTllBTkVUMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A
MIICCgKCAgEAtxOJBxEe9EWGYZgzQfH9mnvoTwVMVPgIPpos4PYYFvJeXrCitwIA
j0ZCmT4Pekvq/5RH4n4NOltpSEXjM1ghbt8Yo/W5y5YquQGO74nK6HQhQBT+ix0l
9M3xKvZZv8H8R6FLHT0Y0IHz5dV0BjAXsztT2tFvYDOXZHz+gehp9fZ2ToqJKa4W
2avhMWL4v/OaP48WmW/42q6sDIqbvQ+TvAQKMFRrDSKoeC8wN1uOh2BFv0FIsrA5
PhGlFnD9GmNYW5HZAi0RmiQVdlq6abZud9Ve/ny9SpT7+3XP2QjE737lRbkELnfQ
KTo17eHfFGv82yUY0PImzkSBojuZg4wf44dpml5RiXYprJGVirxeTkMVOS3tnFA4
75Ux0MoS9xSxwznHJpDQ+7NMooxQSDk1KY5cBqAdymSAptAL0jJBMg83hkBpGEx6
tzEoIlH4skGKoV4HuSBlihk8uLcV/NpW49Th8THVr55lZZwXCma8JgWpFd9eF1Es
mPGzQzRUeiunBpWwDvGKjWZ6jxXR/2OCofKPQOMOkWq8TXZiV0Xbg6KXFJTisZB2
nwC6hqKzaudGxuBnbogwMaLBqqfndrcWO9pJhEMIow6OEUhEIvmUl8B8HFXu+dOR
dxRyNQldEBU7erN+dLrhIjbO0LayRHIQge6mjiikLJF0IHEVL4v5GOECAwEAAaNT
MFEwHQYDVR0OBBYEFPwC9awfqcrGqfdfByhXS7eF0/9JMB8GA1UdIwQYMBaAFPwC
9awfqcrGqfdfByhXS7eF0/9JMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL
BQADggIBAKN1U3DSTamMqHb5mETk55wokbGGOJfxz7WDfqzbOnpUkyuvMEiZ0QI9
nMHFJd7LHGca2f28y3yZWAcvGkhLk2A6LDeBBoE1q+V2Ss4LR7dMK/UU895yJKLC
whs+04cR8kKPLOlaOUdO/uqCJ0JDBxXTsIj2VIHVeF3FybvRgPSjuO0nej+SQDT1
dG3ZJV09AVujWBA9gYLl/7Cr9GdgJmfx+JJ685R7qDrO5n7hpW5P7HAa+7viXzhd
x34er0l8b4pLzBXn2bl2gsaKvXCBXs6TkL2i+FwoihDk7rMiMpxKvzaHmk/9F6xD
HrOrJHCSv5vTxsPTHsfgGa2D8eNg7qK8rySEbzOiMw2CgT6peQMlDTvQbSCE4E/q
7g5rOc4GSFOd6C6HMP18BGBFNW3iGwrB7I3s+sonLzEesKlG9JnmQTTIC0bnZjzl
5RDkpaQjawBkU0jVYx7//2Fmj7SplLCuAj54LHTh1bhk8kduEi7qX2TmaTJboVX2
J1JB3OhEOkaJ4rd+u3LED6sbkO8TWNsI7/Vyz3gbfB5E/nop8jibpoZ67LI1Yjf4
eior7k6HOJ7uisnCp3n69aVkUd0FEEVEXr1+LcNt2V5iR24jhs76XSLYqsUwVdU/
6zfPt1qT//7MS/KZNU69oJzstqkprXRkelc0SoBB+BY4oWUIvSXR
-----END CERTIFICATE-----

View file

@ -1,5 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\Nyangate.Lib\Nyangate.Lib.csproj" />
</ItemGroup>
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>

View file

@ -0,0 +1,33 @@
using System.Net;
namespace Nyangate.Lib.Data;
public enum RouteType : byte
{
STATIC_ADDRESS = 0x0,
HOSTNAME = 0x1
}
public class IPRoute
{
public IPRoute(string addrOrName)
{
if (!IPAddress.TryParse(addrOrName, out _addr))
{
_dnsName = addrOrName;
}
}
private readonly IPAddress? _addr;
private readonly string? _dnsName;
public RouteType Type { get; set; }
public IPAddress Resolve()
{
return _addr ?? Dns.GetHostEntry(_dnsName!, System.Net.Sockets.AddressFamily.Unspecified).AddressList.First();//TODO: Cleanup
}
public string GetHost()
{
return _dnsName ?? _addr!.ToString();
}
}

View file

@ -0,0 +1,8 @@
namespace Nyangate.Lib.Data;
public class RoutingInfo
{
public bool UsesTls { get; set; }
public required IPRoute Route { get; set; }
public required int Port { get; set; }
}

View file

@ -0,0 +1,15 @@
namespace Nyangate.Lib.Data;
public class ServerHost
{
public required string Hostname { get; set; }
public ICollection<string> Aliases { get; set; } = Array.Empty<string>();
public required RoutingInfo Route { get; set; }
public Uri CreateBaseUri() => new UriBuilder()
{
Scheme = Route.UsesTls ? "https" : "http",
Host = Route.Route.GetHost(),
Port = Route.Port,
}.Uri;
}

View file

@ -0,0 +1,27 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions;
namespace Nyangate.Lib.Http;
public class HttpContextAdapter
{
public HttpContextAdapter(HttpContext ctx)
{
_ctx = ctx;
_msg = new HttpRequestMessage(new HttpMethod(ctx.Request.Method), ctx.Request.GetEncodedUrl());
foreach (var header in ctx.Request.Headers)
{
_msg.Headers.Add(header.Key, header.Value.AsEnumerable());
}
_msg.VersionPolicy = HttpVersionPolicy.RequestVersionOrHigher;
}
private readonly HttpContext _ctx;
public HttpRequestMessage _msg;
public HttpRequestMessage Message => _msg;
}

View file

@ -6,4 +6,8 @@
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore" Version="2.2.0" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,8 @@
using Nyangate.Lib.Data;
namespace Nyangate.Lib.Routing;
public abstract class RoutingStrategy
{
public abstract IPRoute Next();
}

View file

@ -0,0 +1,21 @@
namespace Nyangate.Filters;
public class ContextFilter
{
private readonly List<IFilter> _filters = typeof(IFilter).Assembly.GetTypes().Where(t => t.IsAssignableFrom(typeof(IFilter))).Select(t => (IFilter)Activator.CreateInstance(t)!).ToList();
public void ApplyAll(FilterType type, HttpContext ctx)
{
foreach (var filter in _filters.Where(f => f.Type.HasFlag(type)))
{
filter.Apply(ctx); //TODO: Priorities
}
}
}
[Flags]
public enum FilterType : int
{
Proxy = 0x1,
Sensitive = 0x2,
All = 0xff
}

View file

@ -0,0 +1,8 @@
namespace Nyangate.Filters;
public interface IFilter
{
public void Apply(HttpContext context);
public FilterType Type { get; }
}

View file

@ -0,0 +1,40 @@
using System.Collections.Immutable;
using Microsoft.Net.Http.Headers;
namespace Nyangate.Filters;
public class ProxyHeaderFilter : IFilter
{
private static readonly HashSet<string> _headersToExclude = new(18, StringComparer.OrdinalIgnoreCase)
{
HeaderNames.Connection,
HeaderNames.TransferEncoding,
HeaderNames.KeepAlive,
HeaderNames.Upgrade,
HeaderNames.ProxyConnection,
HeaderNames.ProxyAuthenticate,
"Proxy-Authentication-Info",
HeaderNames.ProxyAuthorization,
"Proxy-Features",
"Proxy-Instruction",
"Security-Scheme",
"ALPN",
"Close",
"HTTP2-Settings",
HeaderNames.UpgradeInsecureRequests,
HeaderNames.TE,
HeaderNames.AltSvc,
HeaderNames.StrictTransportSecurity,
};
public FilterType Type => FilterType.Proxy;
public void Apply(HttpContext context)
{
foreach (var header in context.Request.Headers.Where(x => _headersToExclude.Contains(x.Key, StringComparer.OrdinalIgnoreCase)).ToArray())
{
context.Request.Headers.Remove(header);
}
}
}

View file

@ -0,0 +1,17 @@
namespace Nyangate.Middleware;
public class Annotator
{
private readonly RequestDelegate _next;
public Annotator(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext ctx)
{
ctx.Request.Headers.Add("Forwarded", $"for={ctx.Connection.RemoteIpAddress};by={ctx.Connection.LocalIpAddress};proto={ctx.Request.Scheme}");
await _next.Invoke(ctx);
}
}

View file

@ -1,8 +1,3 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Nyangate.Middleware;
public class Filter

View file

@ -1,3 +1,6 @@
using Microsoft.Extensions.Primitives;
using Nyangate.Filters;
using Nyangate.Lib.Http;
using Nyangate.Services;
namespace Nyangate.Middleware
@ -15,11 +18,50 @@ namespace Nyangate.Middleware
public async Task Invoke(HttpContext ctx)
{
string host = ctx.Request.Host.Host;
string hostname = ctx.Request.Host.Host;
//check ctx.Request.IsHttps if strict https required
var host = _map.Get(hostname) ?? _map.GetFallback();
if (host is null)
{
ctx.Response.StatusCode = 404;
await ctx.Response.WriteAsync("meowless~");
await ctx.Response.CompleteAsync();
return;
}
ctx.Request.Host = new HostString(host.Route.Route.GetHost(), host.Route.Port);
ctx.Request.Scheme = host.Route.UsesTls ? "https" : "http";
new ProxyHeaderFilter().Apply(ctx);
using HttpClient http = new();
HttpRequestMessage request = new HttpContextAdapter(ctx).Message;
if (ctx.Request.Body.CanRead)
{
request.Content = new StreamContent(ctx.Request.Body);
}
var response = await http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
Console.WriteLine($"meow: {ctx.Connection.RemoteIpAddress}");
ctx.Response.StatusCode = 200;
await _next.Invoke(ctx);
ctx.Response.StatusCode = (int)response.StatusCode;
ctx.Response.Headers.Clear();
foreach (var header in response.Headers.NonValidated)
{
ctx.Response.Headers.Add(header.Key, new StringValues(header.Value.ToArray()));
}
ctx.Response.ContentType = response.Content.Headers.ContentType?.ToString();
ctx.Response.Headers.Add("Content-Encoding", new StringValues(response.Content.Headers.ContentEncoding.ToArray()));
await response.Content.CopyToAsync(ctx.Response.Body);
await ctx.Response.CompleteAsync();
}
}
}

View file

@ -1,5 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<ItemGroup>
<ProjectReference Include="..\Nyangate.Lib\Nyangate.Lib.csproj" />
</ItemGroup>
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Nullable>enable</Nullable>

View file

@ -1,5 +1,3 @@
using Microsoft.AspNetCore.Components.Routing;
using Nyangate;
using Nyangate.Middleware;
using Nyangate.Services;
@ -29,6 +27,9 @@ var app = builder.Build();
app.UseMiddleware<Authenticator>();
app.UseMiddleware<Filter>();
app.UseMiddleware<IPFilter>();
app.UseMiddleware<IDSFilter>();
app.UseMiddleware<Annotator>();
app.UseMiddleware<Nyangate.Middleware.Router>();
app.Run();

View file

@ -1,11 +1,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Nyangate.Services;
public class BlacklistRegistry
{
public BlacklistRegistry()
{
}
private readonly Dictionary<string, long> Blacklisted = new();
public async Task<bool> IsBlacklisted(HttpContext ctx)
{
throw new NotImplementedException();
}
}

View file

@ -0,0 +1,26 @@
using System.Net;
namespace Nyangate.Services;
public class RateLimitService
{
public async Task<bool> IsRateLimited(IPAddress addr, bool update = true)
{
throw new NotImplementedException();
}
public async Task<bool> IsRateLimited(HttpContext ctx, bool update = true)
{
throw new NotImplementedException();
}
public async Task UpdateActivity(HttpContext ctx)
{
throw new NotImplementedException();
}
public async Task UpdateActivity(IPAddress addr)
{
throw new NotImplementedException();
}
}

View file

@ -1,11 +1,43 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Nyangate.Lib.Data;
namespace Nyangate.Services;
public class RoutingMap
{
public RoutingMap()
{
_hosts.Add("meow", new()
{
Hostname = "meow",
Route = new()
{
UsesTls = false,
Port = 8888,
Route = new("localhost")
}
});
}
private readonly Dictionary<string, ServerHost> _hosts = new();
private readonly ServerHost? _fallbackHost;
public ServerHost? Get(string hostname)
{
return GetByMainHostname(hostname) ?? GetByAlias(hostname);
}
public ServerHost? GetByMainHostname(string hostname)
{
return _hosts.GetValueOrDefault(hostname);
}
public ServerHost? GetByAlias(string alias)
{
return _hosts.SingleOrDefault(x => x.Value.Aliases.Contains(alias)).Value;
}
public ServerHost? GetFallback()
{
return _fallbackHost;
}
}

View file

@ -1,14 +1,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;
namespace Nyangate.Services;
public class ServerCertificateRegistry
{
public ServerCertificateRegistry()
{
_main = X509Certificate2.CreateFromPemFile("/home/nya/Dev/csharp/nyangate/main.pem", "/home/nya/Dev/csharp/nyangate/main.key.pem");
}
private readonly Dictionary<string, X509Certificate2> _registry = new();
private readonly X509Certificate2 _main;
public X509Certificate2 GetOrDefault(string hostname)
{
return _registry.GetValueOrDefault(hostname, Default());
@ -16,6 +18,6 @@ public class ServerCertificateRegistry
public X509Certificate2 Default()
{
throw new NotImplementedException();
return _main;
}
}

View file

@ -1 +0,0 @@
global using Xunit;