Compare commits

..

No commits in common. "059bac76d19d7708dda28d9e519e0f0314f7143f" and "e38ffbedf4e55a7b4440a0f3572b2637df0fec5c" have entirely different histories.

14 changed files with 36 additions and 264 deletions

View file

@ -3,6 +3,5 @@ let
web = import ./endpoints/web.nix;
matrix = import ./endpoints/matrix.nix;
vikunja = import ./endpoints/vikunja.nix;
git = import ./endpoints/git.nix;
in
local ++ web ++ matrix ++ vikunja ++ git ++ []
local ++ web ++ matrix ++ vikunja ++ []

View file

@ -1,18 +0,0 @@
let
net = import ../network.nix;
in
[
{
type = "proxy";
domain = "git.${net.devices.remote_proxy.domain}";
endpoint = "/";
force_ssl = true;
port = 443;
content = {
type = "service";
ip = net.devices.tuserver.ip;
port = 3000;
proxyWebsockets = false;
};
}
]

View file

@ -29,19 +29,6 @@ in
proxyWebsockets = true;
};
}
# {
# type = "proxy";
# domain = net.devices.remote_proxy.domain;
# endpoint = "/.well-known/matrix/";
# force_ssl = true;
# port = 443;
# content = {
# type = "service";
# ip = net.devices.pi.ip;
# port = services.continuwuity.port;
# proxyWebsockets = true;
# };
# }
{
type = "inline";
domain = net.devices.remote_proxy.domain;
@ -50,11 +37,6 @@ in
port = 443;
content = {
contentType = "application/json";
headers = {
Access-Control-Allow-Origin = "*";
Access-Control-Allow-Methods = "GET, POST, PUT, DELETE, OPTIONS";
Access-Control-Allow-Headers = "X-Requested-With, Content-Type, Authorization";
};
status = 200;
body = ''{"m.server":"${net.devices.remote_proxy.domain}:443"}'';
};
@ -67,13 +49,8 @@ in
port = 443;
content = {
contentType = "application/json";
headers = {
Access-Control-Allow-Origin = "*";
Access-Control-Allow-Methods = "GET, POST, PUT, DELETE, OPTIONS";
Access-Control-Allow-Headers = "X-Requested-With, Content-Type, Authorization";
};
status = 200;
body = ''{"m.homeserver": {"base_url": "https://${net.devices.remote_proxy.domain}"},"org.matrix.msc3575.proxy":{"url":"https://nudelerde.de/"},"org.matrix.msc4143.rtc_foci": [{"type": "livekit","livekit_service_url": "https://livekit.${net.devices.remote_proxy.domain}"}]}'';
body = ''{"m.homeserver":{"base_url":"https://${net.devices.remote_proxy.domain}"}}'';
};
}
]

View file

@ -16,15 +16,4 @@ in
proxyWebsockets = true;
};
}
{
type = "redirect";
domain = "wekan.${net.devices.remote_proxy.domain}";
endpoint = "/";
force_ssl = true;
port = 443;
content = {
target = "https://vikunja.${net.devices.remote_proxy.domain}/";
status = 301;
};
}
]

View file

@ -33,18 +33,9 @@ rec {
auto_ssh = {
enable = true;
sshPort = 22;
sshUser = "autossh-incoming";
sshUser = "root";
key = secrets.byName.autossh_remote_proxy_key.path;
known_hosts = secrets.byName.autossh_remote_proxy_known_hosts.path;
portOffset = 10000;
};
};
"tuserver" = {
type = "local";
ip = "192.168.2.102";
reservation = {
hw_address = "00:23:24:f9:43:e6";
hostname = "tuserver";
};
};
};

View file

@ -5,16 +5,6 @@ in
rec {
continuwuity = {
port = 6167;
server_name = "nudelerde.de";
trusted_servers = [ "matrix.org" ];
memory_max = "512M";
livekit_url = "https://livekit.nudelerde.de";
package = {
version = "0.5.6";
sourceHash = "sha256-p6dL1wL9n+1ivUItdlZuLxTneDBjCHEdNr0ukau2rHI=";
cargoHash = "sha256-lLbnFA2WS96er84G2e9bGrYhhqe2zL3Npn1SXB3De2w=";
};
};
qbittorrent = {
@ -35,6 +25,10 @@ rec {
];
};
matrix = {
trusted_servers = [ "matrix.org" ];
};
vikunja = {
port = 8081;
};

View file

@ -9,7 +9,6 @@
timeZone = "Europe/Berlin";
defaultLocale = "en_US.UTF-8";
storageConfig = import ./config/storage.nix;
storageModel = import ./intermediate/storage.nix;
in {
imports = [
@ -22,13 +21,6 @@ in {
fileSystems = storageModel.fileSystems;
swapDevices = [
{
device = "${storageConfig.ssd.path}/swapfile";
size = 8192;
}
];
networking.hostName = "raspberry";
environment.systemPackages = with pkgs; [

View file

@ -205,25 +205,6 @@ let
proxyWebsockets = route.content.proxyWebsockets;
};
}
else if route.type == "redirect" then
let
redirectTarget =
if builtins.isString route.content then
route.content
else
route.content.target;
redirectStatus =
if builtins.isAttrs route.content && route.content ? status then
route.content.status
else
301;
in
{
name = "= ${route.endpoint}";
value = {
return = "${toString redirectStatus} ${redirectTarget}";
};
}
else if route.type == "inline" then
let
inlineBody =
@ -241,14 +222,6 @@ let
route.content.contentType
else
"text/plain; charset=utf-8";
inlineHeaders =
if builtins.isAttrs route.content && route.content ? headers then
route.content.headers
else
{};
inlineHeaderLines = lib.concatStringsSep "\n" (
lib.mapAttrsToList (name: value: " add_header ${name} ${builtins.toJSON value} always;") inlineHeaders
);
in
{
name = "= ${route.endpoint}";
@ -256,7 +229,6 @@ let
return = "${toString inlineStatus} ${builtins.toJSON inlineBody}";
extraConfig = ''
default_type ${inlineContentType};
${lib.optionalString (inlineHeaderLines != "") inlineHeaderLines}
'';
};
}

View file

@ -19,31 +19,22 @@ let
getRemotePortMap = device:
autoSshValidation.getRemotePortMap device;
getPortOffset = device:
autoSshValidation.getPortOffset device;
ensurePortRange = context: port:
if builtins.isInt port && port >= 1 && port <= 65535 then
port
else
throw "${context} must be in range 1..65535.";
resolveRemotePort = remotePortMap: portOffset: endpoint:
resolveRemotePort = remotePortMap: endpoint:
let
localPort =
if endpoint ? port then
ensurePortRange "Endpoint port '${toString endpoint.port}'" endpoint.port
endpoint.port
else
throw "Endpoint is missing required field: port.";
overrides = lib.filter (entry: entry.localPort == localPort) remotePortMap;
in
if overrides != [] then
ensurePortRange "remotePortMap remotePort '${toString (builtins.head overrides).remotePort}'" (builtins.head overrides).remotePort
(builtins.head overrides).remotePort
else
ensurePortRange "Computed remote port (local ${toString localPort} + offset ${toString portOffset})" (localPort + portOffset);
localPort;
mapEndpointToForward = remotePortMap: portOffset: endpoint: {
remote = resolveRemotePort remotePortMap portOffset endpoint;
mapEndpointToForward = remotePortMap: endpoint: {
remote = resolveRemotePort remotePortMap endpoint;
localAddress = "localhost";
localPort = endpoint.port;
};
@ -59,7 +50,6 @@ let
else
throw "Auto SSH device is missing required field: domain.";
remotePortMap = getRemotePortMap device;
portOffset = getPortOffset device;
matchedEndpoints =
lib.filter (endpoint:
if endpoint.force_ssl && endpoint.port == 80 then
@ -71,8 +61,8 @@ let
) endpoints;
forwards = lib.concatMap (endpoint:
let
baseForward = mapEndpointToForward remotePortMap portOffset endpoint;
httpRedirectForward = mapEndpointToForward remotePortMap portOffset (endpointForHttpRedirect endpoint);
baseForward = mapEndpointToForward remotePortMap endpoint;
httpRedirectForward = mapEndpointToForward remotePortMap (endpointForHttpRedirect endpoint);
in
if endpoint.force_ssl then
[ baseForward httpRedirectForward ]

View file

@ -1,5 +1,5 @@
{
"data": "ENC[AES256_GCM,data:2O2/4FIDmmuwG0eRGmPUAm8Ji5lIly4dGT/ll+HZDvlC/sS6cbb3SEyplpVvqjMJNZACZtI9U87WJqKtoj9778Asxu0CFkwrsZK5bOf7XM27ZIN44/KWzbO6xezdcQ==,iv:XdgphFl56RNyjIIr9JQTUbh36j7UB8yjT0jgtCpMXDk=,tag:6OklZR86/gudejOFd9UneQ==,type:str]",
"data": "ENC[AES256_GCM,data:1ZzjJFqw28vXu6cw9efESy11132ntsaZwBmNqupqWju+y2iT2qq1wE+meGVH/e59YMC0ptNpsm+RbcfvC81vFvTFu46FUXPw2fXPb5UQmyDZxN7q+AM8SuOCqSy6,iv:FguIXmdsNUvhoqME97e5OIY+LMy5uuNd+d295U4lTuE=,tag:QlKokl5sOAtgwu06pnuRgA==,type:str]",
"sops": {
"age": [
{
@ -11,9 +11,8 @@
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBiSVhGUHVJNVZVRXhxZzhx\ncEVSNWZBQW8vQXlYdnFUQTg4Q2VlY0lZckRnCkRKNnF5R3ZlUElWN0FOL1hrNHAr\nT2JPaDJ3bzVhZzZONHBkRHBueW16eVEKLS0tIEFxU2pWMVRqaC9BeGRsaWppT1lO\nMzI4UmkybmROL3dKK0xEaWpCM1VPQzQKWsLEb0Z7fbTVF8WQ8a1Lom8Bh7FQ2cPB\nIwIqWRM1L0oXNHyBoFkmHO434DZ8SXlyUYegBwrbVZGedRQFLQ4ibg==\n-----END AGE ENCRYPTED FILE-----\n"
}
],
"lastmodified": "2026-04-04T19:45:45Z",
"mac": "ENC[AES256_GCM,data:EEAh4MsdWJa6GvI2CZ0dS19ERRNtYucvEVCENZnc4Yqyz1vyQbNYuBZKi15D/XL5A8WN0jweBFtJV8bNwbuwW1gJZW8x10uhGJ1WpZrT+/vlFQbEzmEvvjkiO46R4kkQ+yhzkmth+85S0m/nz6p3lgIzcTRBfHIMe9V+kVb2eZI=,iv:u3E9Ybjl9a91Z8ic1fjCUqLqedAVS1e6ZZ9PNE7Em7c=,tag:XOJJBCQBxShz4bC/2t/VXg==,type:str]",
"unencrypted_suffix": "_unencrypted",
"lastmodified": "2026-04-04T09:12:02Z",
"mac": "ENC[AES256_GCM,data:C6jOqg6lFy8A2UTIfZXjtNB/F+0UZJV8/F1sOm4aGgR5B4e2JT+3vL/A62m6pvrj1zSuoD1dH47y3OcpmO/FmsnYcCTdWW2MPEOA63NkHJMScaLJry8JDHpColG8WYmJfGb9QwLJ1O64vpUNXNscmUFs7PomBgJnMCmTyo3twKk=,iv:wiAJw8Vk70b/gnfcgPlT3exsnnp87mGoIXMtzqd5m88=,tag:oT9TPTNSDX9y0cJJCP/JkQ==,type:str]",
"version": "3.12.1"
}
}

View file

@ -1,40 +1,21 @@
{ config, pkgs, lib, ... }:
{ config, pkgs, ... }:
let
net = import ../config/network.nix;
serv = import ../config/services.nix;
serviceValidation = import ../validation/service/continuwuity.nix;
continuwuity = serv.continuwuity;
serverName = serviceValidation.getServerName serv;
serverName =
if net ? devices && builtins.isAttrs net.devices && net.devices ? remote_proxy && net.devices.remote_proxy ? domain && builtins.isString net.devices.remote_proxy.domain then
net.devices.remote_proxy.domain
else
throw "config/network.nix must define devices.remote_proxy.domain as string for continuwuity.";
trustedServers = serviceValidation.getTrustedServers serv;
memoryMax = serviceValidation.getMemoryMax serv;
package = if continuwuity ? package then
pkgs.matrix-continuwuity.overrideAttrs (old: rec {
version = continuwuity.package.version;
pname = old.pname or "matrix-continuwuity";
src = pkgs.fetchFromGitea {
domain = "forgejo.ellis.link";
owner = "continuwuation";
repo = "continuwuity";
tag = "v${version}";
hash = continuwuity.package.sourceHash;
};
cargoDeps = pkgs.rustPlatform.fetchCargoVendor {
inherit src;
hash = continuwuity.package.cargoHash;
};
})
else
pkgs.matrix-continuwuity;
in
{
services.matrix-continuwuity = {
enable = true;
package = package;
settings = {
global = {
server_name = serverName;
@ -43,17 +24,7 @@ in
allow_federation = true;
max_request_size = 20 * 1024 * 1024; # 20 MiB
trusted_servers = trustedServers;
matrix_rtc = {
foci = [
{ type = "livekit"; livekit_service_url = continuwuity.livekit_url; }
];
};
};
};
};
systemd.services.matrix-continuwuity.serviceConfig =
lib.optionalAttrs (memoryMax != null) {
MemoryMax = memoryMax;
};
}

View file

@ -53,23 +53,9 @@ let
else
device.auto_ssh.remotePortMap;
getPortOffset = device:
if !(device ? auto_ssh) then
0
else if !builtins.isAttrs device.auto_ssh then
throw "Device auto_ssh must be an attrset when present."
else if !(device.auto_ssh ? portOffset) then
0
else if !builtins.isInt device.auto_ssh.portOffset then
throw "Device auto_ssh.portOffset must be an int."
else if device.auto_ssh.portOffset < 0 then
throw "Device auto_ssh.portOffset must be >= 0."
else
device.auto_ssh.portOffset;
isSafeName = name:
builtins.match "^[a-z_][a-z0-9_-]*$" name != null;
in
{
inherit getDevices getAutoSshDevices getAutoSshDomains getAutoSshConfig getRemotePortMap getPortOffset isSafeName;
inherit getDevices getAutoSshDevices getAutoSshDomains getAutoSshConfig getRemotePortMap isSafeName;
}

View file

@ -5,8 +5,7 @@ let
allowedProxyContentKeys = [ "type" "ip" "port" "proxyWebsockets" ];
allowedWebContentKeys = [ "type" "files" ];
allowedWebFileKeys = [ "path" "filePath" "contentType" "status" ];
allowedInlineContentKeys = [ "body" "contentType" "headers" "status" ];
allowedRedirectContentKeys = [ "target" "status" ];
allowedInlineContentKeys = [ "body" "contentType" "status" ];
ensureNoUnknownKeys = context: obj: allowedKeys:
let
@ -39,10 +38,10 @@ let
else
throw "Endpoint at index ${toString index} must define type as a string.";
_type =
if lib.elem typeValue [ "proxy" "web" "inline" "redirect" ] then
if lib.elem typeValue [ "proxy" "web" "inline" ] then
null
else
throw "Endpoint at index ${toString index} type must be 'proxy', 'web', 'inline', or 'redirect'.";
throw "Endpoint at index ${toString index} type must be 'proxy', 'web', or 'inline'.";
_domain =
if endpoint ? domain && builtins.isString endpoint.domain && endpoint.domain != "" then
null
@ -132,26 +131,6 @@ let
) filesValue;
in
null
else if typeValue == "redirect" then
if builtins.isString contentValue then
null
else if builtins.isAttrs contentValue then
let
____ = ensureNoUnknownKeys "Redirect content at endpoint index ${toString index}" contentValue allowedRedirectContentKeys;
_____ =
if contentValue ? target && builtins.isString contentValue.target && contentValue.target != "" then
null
else
throw "Redirect endpoint at index ${toString index} must define content.target as a non-empty string when content is an attrset.";
______ =
if !(contentValue ? status) || builtins.isInt contentValue.status then
null
else
throw "Redirect endpoint at index ${toString index} content.status must be an int when provided.";
in
null
else
throw "Redirect endpoint at index ${toString index} must define content as a string or an attrset."
else
if builtins.isString contentValue then
null
@ -168,20 +147,6 @@ let
null
else
throw "Inline endpoint at index ${toString index} content.contentType must be a non-empty string when provided.";
________ =
if contentValue ? headers then
if builtins.isAttrs contentValue.headers then
let
headerValues = builtins.attrValues contentValue.headers;
in
if lib.all (value: builtins.isString value && value != "") headerValues then
null
else
throw "Inline endpoint at index ${toString index} content.headers values must be non-empty strings."
else
throw "Inline endpoint at index ${toString index} content.headers must be an attrset when provided."
else
null;
_______ =
if !(contentValue ? status) || builtins.isInt contentValue.status then
null

View file

@ -2,54 +2,19 @@ let
common = import ./common.nix;
in
rec {
getServerName = serviceData:
let
continuwuity =
if serviceData ? continuwuity then
common.ensureAttrset "config/services.nix continuwuity" serviceData.continuwuity
else
throw "config/services.nix must define continuwuity attrset.";
serverName =
if continuwuity ? server_name then
common.ensureString "config/services.nix continuwuity.server_name" continuwuity.server_name
else
throw "config/services.nix continuwuity.server_name must exist.";
in
if serverName != "" then
serverName
else
throw "config/services.nix continuwuity.server_name must be a non-empty string.";
getTrustedServers = serviceData:
let
continuwuity =
if serviceData ? continuwuity then
common.ensureAttrset "config/services.nix continuwuity" serviceData.continuwuity
matrix =
if serviceData ? matrix then
common.ensureAttrset "config/services.nix matrix" serviceData.matrix
else
throw "config/services.nix must define continuwuity attrset.";
throw "config/services.nix must define matrix attrset.";
trustedServers =
if continuwuity ? trusted_servers then
common.ensureList "config/services.nix continuwuity.trusted_servers" continuwuity.trusted_servers
if matrix ? trusted_servers then
common.ensureList "config/services.nix matrix.trusted_servers" matrix.trusted_servers
else
throw "config/services.nix continuwuity.trusted_servers must exist.";
throw "config/services.nix matrix.trusted_servers must exist.";
in
trustedServers;
getMemoryMax = serviceData:
let
continuwuity =
if serviceData ? continuwuity then
common.ensureAttrset "config/services.nix continuwuity" serviceData.continuwuity
else
throw "config/services.nix must define continuwuity attrset.";
in
if !(continuwuity ? memory_max) then
null
else
let
value = common.ensureString "config/services.nix continuwuity.memory_max" continuwuity.memory_max;
in
if value != "" then value else throw "config/services.nix continuwuity.memory_max must be a non-empty string when provided.";
}