add juicity service module and juicity nixos module

This commit is contained in:
ulic-youthlic 2025-02-03 21:07:31 +08:00
parent 63a8871eaa
commit 1a6d282046
Signed by: youthlic
GPG key ID: 63E86C3C14A0D721
9 changed files with 447 additions and 104 deletions

View file

@ -31,6 +31,7 @@
enable = true;
baseDomain = "youthlic.fun";
};
juicity.server.enable = true;
};
};

View file

@ -39,6 +39,7 @@
open-webui.enable = true;
transmission.enable = true;
nix-ld.enable = true;
juicity.client.enable = true;
};
gui.enabled = "niri";
};

View file

@ -1,5 +1,6 @@
include {
proxy.d/*.dae
local.d/*.dae
}
global {
@ -43,10 +44,12 @@ dns {
group {
proxy {
filter: subtag(wget)
filter: name(local)
policy: min_moving_avg
}
us {
filter: subtag(wget) && name(keyword: "美国")
filter: name(local)
policy: min_moving_avg
}
hk {
@ -59,6 +62,7 @@ group {
routing {
pname(hickory-dns) && dport(53) -> must_direct
pname(mihomo) -> must_direct
pname(juicity-client) -> must_direct
# pname(systemd-resolve) -> must_direct
dip(107.174.145.140) -> must_direct

View file

@ -14,114 +14,126 @@ in
enable = lib.mkEnableOption "dae";
};
};
config = lib.mkIf cfg.enable {
services.dae = {
enable = true;
openFirewall = {
config = lib.mkMerge [
(lib.mkIf cfg.enable {
services.dae = {
enable = true;
port = 12345;
openFirewall = {
enable = true;
port = 12345;
};
disableTxChecksumIpGeneric = false;
config = builtins.readFile ./config.dae;
};
disableTxChecksumIpGeneric = false;
config = builtins.readFile ./config.dae;
};
sops.secrets.url = {
mode = "0444";
sopsFile = rootPath + "/secrets/general.yaml";
};
systemd.services =
let
update = ''
head="user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.182 Safari/537.36"
new_proxy=/etc/dae/proxy.d.new
num=0
check=1
urls="$(cat ${config.sops.secrets.url.path})"
mkdir -p ''${new_proxy}
for url in ''${urls}; do
txt=''${new_proxy}/''${num}.txt
config="''${new_proxy}/''${num}.dae"
echo \'curl -LH \""''${head}"\" \""''${url}"\" -o \""''${txt}"\"\'
curl -LH "''${head}" "''${url}" -o "''${txt}"
echo End curl
echo "" > ''${config}
{
echo 'subscription {'
echo \ \ wget:\ \"file://proxy.d/''${num}.txt\"
echo "}"
} >> ''${config}
if [[ ! -s ''${txt} ]]; then
check=0
fi
chmod 0640 ''${txt}
chmod 0640 ''${config}
num=$((num+1))
sops.secrets.url = {
mode = "0444";
sopsFile = rootPath + "/secrets/general.yaml";
};
systemd.services =
let
update = ''
head="user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.182 Safari/537.36"
new_proxy=/etc/dae/proxy.d.new
num=0
check=1
urls="$(cat ${config.sops.secrets.url.path})"
mkdir -p ''${new_proxy}
for url in ''${urls}; do
txt=''${new_proxy}/''${num}.txt
config="''${new_proxy}/''${num}.dae"
echo \'curl -LH \""''${head}"\" \""''${url}"\" -o \""''${txt}"\"\'
curl -LH "''${head}" "''${url}" -o "''${txt}"
echo End curl
echo "" > ''${config}
{
echo 'subscription {'
echo \ \ wget:\ \"file://proxy.d/''${num}.txt\"
echo "}"
} >> ''${config}
if [[ ! -s ''${txt} ]]; then
check=0
fi
chmod 0640 ''${txt}
chmod 0640 ''${config}
num=$((num+1))
if [[ ''${check} -eq 0 ]]; then
echo "''${txt}" is empty
exit 103
if [[ ''${check} -eq 0 ]]; then
echo "''${txt}" is empty
exit 103
fi
done
if [[ -d /etc/dae/proxy.d ]]; then
mv /etc/dae/proxy.d /etc/dae/proxy.d.old
fi
done
if [[ -d /etc/dae/proxy.d ]]; then
mv /etc/dae/proxy.d /etc/dae/proxy.d.old
fi
mv ''${new_proxy} /etc/dae/proxy.d
'';
updateScript = pkgs.writeShellApplication {
name = "update.sh";
runtimeInputs = with pkgs; [
coreutils
curl
];
text = ''
mkdir -p /etc/proxy.d
if [ -z "$(ls -A /etc/dae/proxy.d 2>/dev/null)" ]; then
echo "No subscription file found in /etc/dae/proxy.d. Update now..."
mv ''${new_proxy} /etc/dae/proxy.d
'';
updateScript = pkgs.writeShellApplication {
name = "update.sh";
runtimeInputs = with pkgs; [
coreutils
curl
];
text = ''
mkdir -p /etc/proxy.d
if [ -z "$(ls -A /etc/dae/proxy.d 2>/dev/null)" ]; then
echo "No subscription file found in /etc/dae/proxy.d. Update now..."
${update}
else
echo "Found existing subscription files. Skipping immediate update."
fi
'';
};
updateForceScript = pkgs.writeShellApplication {
name = "update-force.sh";
runtimeInputs = with pkgs; [
coreutils
curl
];
text = ''
${update}
else
echo "Found existing subscription files. Skipping immediate update."
fi
'';
};
updateForceScript = pkgs.writeShellApplication {
name = "update-force.sh";
runtimeInputs = with pkgs; [
coreutils
curl
];
text = ''
${update}
'';
};
in
{
"update-dae-subscription-immediate" = {
after = [ "network-online.target" ];
wants = [ "network-online.target" ];
before = [ "dae.service" ];
serviceConfig = {
Type = "oneshot";
User = "root";
ExecStart = [
"${updateScript}/bin/update.sh"
];
'';
};
wantedBy = [ "multi-user.target" ];
};
"update-dae-subscription-force" = {
serviceConfig = {
Type = "oneshot";
User = "root";
ExecStartPre = [
"-${pkgs.systemd}/bin/systemctl stop dae.service"
];
ExecStartPost = [
"-${pkgs.systemd}/bin/systemctl start dae.service"
];
ExecStart = [
"${updateForceScript}/bin/update-force.sh"
];
in
{
"update-dae-subscription-immediate" = {
after = [ "network-online.target" ];
wants = [ "network-online.target" ];
before = [ "dae.service" ];
serviceConfig = {
Type = "oneshot";
User = "root";
ExecStart = [
"${updateScript}/bin/update.sh"
];
};
wantedBy = [ "multi-user.target" ];
};
"update-dae-subscription-force" = {
serviceConfig = {
Type = "oneshot";
User = "root";
ExecStartPre = [
"-${pkgs.systemd}/bin/systemctl stop dae.service"
];
ExecStartPost = [
"-${pkgs.systemd}/bin/systemctl start dae.service"
];
ExecStart = [
"${updateForceScript}/bin/update-force.sh"
];
};
};
};
})
(lib.mkIf (cfg.enable && config.youthlic.programs.juicity.client.enable) {
environment.etc."dae/local.d/0.dae" = {
text = ''
node {
local: 'socks5://127.0.0.1:7890/'
}
'';
mode = "0440";
};
};
})
];
}

View file

@ -16,5 +16,6 @@
./transmission.nix
./conduwuit.nix
./nix-ld.nix
./juicity
];
}

View file

@ -0,0 +1,106 @@
{
pkgs,
lib,
config,
...
}:
let
cfg = config.youthlic.programs.juicity;
in
{
imports = [
./template.nix
];
options = {
youthlic.programs.juicity = {
client = {
enable = lib.mkEnableOption "juicity-client";
};
server = {
enable = lib.mkEnableOption "juicity-server";
};
};
};
config = lib.mkMerge [
(lib.mkIf cfg.client.enable {
users.groups.juicity.members = [ "root" ];
sops = {
secrets = {
"juicity/serverIp" = { };
"juicity/sni" = { };
"juicity/certchainSha256" = { };
};
templates."juicity-client-config.json" = {
group = "juicity";
mode = "0440";
content = ''
{
"listen": ":7890",
"server": "${config.sops.placeholder."juicity/serverIp"}:23182",
"uuid": "${config.sops.placeholder."juicity/uuid"}",
"password": "${config.sops.placeholder."juicity/password"}",
"sni": "${config.sops.placeholder."juicity/sni"}",
"allow_insecure": false,
"pinned_certchain_sha256": "${config.sops.placeholder."juicity/certchainSha256"}",
"log_level": "info"
}
'';
};
};
services.juicity.client = {
enable = true;
package = pkgs.juicity;
configFile = "${config.sops.templates."juicity-client-config.json".path}";
allowedOpenFirewallPorts = [
7890
];
group = "juicity";
};
})
(lib.mkIf cfg.server.enable {
users.groups.juicity.members = [ "root" ];
sops = {
secrets = {
"juicity/certificate" = {
group = "juicity";
mode = "0440";
};
"juicity/private_key" = {
group = "juicity";
mode = "0440";
};
};
templates."juicity-server-config.json" = {
group = "juicity";
mode = "0440";
content = ''
{
"listen": ":23182",
"users": {
"${config.sops.placeholder."juicity/uuid"}": "${config.sops.placeholder."juicity/password"}"
},
"certificate": "${config.sops.secrets."juicity/certificate".path}",
"private_key": "${config.sops.secrets."juicity/private_key".path}",
"log_level": "info"
}
'';
};
};
services.juicity.server = {
enable = true;
package = pkgs.juicity;
configFile = "${config.sops.templates."juicity-server-config.json".path}";
allowedOpenFirewallPorts = [
23182
];
group = "juicity";
};
})
(lib.mkIf (cfg.server.enable || cfg.client.enable) {
sops.secrets = {
"juicity/uuid" = { };
"juicity/password" = { };
};
})
];
}

View file

@ -0,0 +1,206 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.juicity;
settingsFormat = pkgs.formats.json { };
clientConfigFile =
if (cfg.client.configFile != null) then
cfg.client.configFile
else
settingsFormat cfg.client.settings;
serverConfigFile =
if (cfg.server.configFile != null) then
cfg.server.configFile
else
settingsFormat cfg.server.settings;
in
{
options = {
services.juicity = {
client = {
enable = lib.mkEnableOption "juicity-client";
package = lib.mkPackageOption pkgs "juicity" { };
group = lib.mkOption {
type = lib.types.nullOr lib.types.str;
example = "juicity";
default = null;
};
settings = lib.mkOption {
type = settingsFormat.type;
default = { };
example = {
listen = ":1000";
server = "112.32.62.11:23182";
uuid = "00000000-0000-0000-0000-000000000000";
password = "my_password";
sni = "www.example.com";
allow_insecure = false;
congestion_control = "bbr";
log_level = "info";
};
description = ''
Juicity client configuration, for configuration options
see example of [client](https://github.com/juicity/juicity/blob/main/install/example-client.json) on github.
'';
};
configFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
example = "/run/juicity/config.json";
default = null;
description = ''
A file which JSON configurations for juicity client. See the {option}`settings` option for more information.
Note: this file will override {options}`settings` option, which is recommanded.
'';
};
allowedOpenFirewallPorts = lib.mkOption {
type = lib.types.nullOr (lib.types.listOf lib.types.port);
example = [ 23182 ];
default = null;
description = ''
the ports should be open
'';
};
};
server = {
enable = lib.mkEnableOption "juicity-server";
package = lib.mkPackageOption pkgs "juicity" { };
group = lib.mkOption {
type = lib.types.nullOr lib.types.str;
example = "juicity";
default = null;
};
settings = lib.mkOption {
type = settingsFormat.type;
default = { };
description = ''
Juicity server configuration, for configuration options
see example of [server](https://github.com/juicity/juicity/blob/main/install/example-server.json) on github.
'';
example = {
listen = ":23182";
users = {
"00000000-0000-0000-0000-000000000000" = "my_password";
};
certificate = "/path/to/fullchain.cer";
private_key = "/path/to/private.key";
congestion_control = "bbr";
log_level = "info";
};
};
configFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
example = "/run/juicity/config.json";
default = null;
description = ''
A file which JSON configurations for juicity server. See the {option}`settings` option for more information.
Note: this file will override {options}`settings` option, which is recommanded.
'';
};
allowedOpenFirewallPorts = lib.mkOption {
type = lib.types.nullOr (lib.types.listOf lib.types.port);
example = [ 23182 ];
default = null;
description = ''
the ports should be open
'';
};
};
};
};
config = lib.mkMerge [
(lib.mkIf (cfg.client.enable && (cfg.client.allowedOpenFirewallPorts != null)) {
networking.firewall.allowedTCPPorts = cfg.client.allowedOpenFirewallPorts;
networking.firewall.allowedUDPPorts = cfg.client.allowedOpenFirewallPorts;
})
(lib.mkIf (cfg.server.enable && (cfg.server.allowedOpenFirewallPorts != null)) {
networking.firewall.allowedTCPPorts = cfg.server.allowedOpenFirewallPorts;
networking.firewall.allowedUDPPorts = cfg.server.allowedOpenFirewallPorts;
})
(lib.mkIf (cfg.server.enable && (cfg.server.group != null)) {
systemd.services."juicity-server".serviceConfig = {
Group = cfg.server.group;
};
})
(lib.mkIf (cfg.client.enable && (cfg.client.group != null)) {
systemd.services."juicity-client".serviceConfig = {
Group = cfg.client.group;
};
})
(lib.mkIf cfg.client.enable {
environment.systemPackages = [
cfg.client.package
];
systemd.services.juicity-client = {
description = ''
juicity-client Service
'';
documentation = [
"https://github.com/juicity/juicity"
];
after = [
"network.target"
"nss-lookup.target"
];
wantedBy = [
"multi-user.target"
];
serviceConfig = {
Type = "simple";
DynamicUser = true;
CapabilityBoundingSet = [
"CAP_NET_ADMIN"
"CAP_NET_BIND_SERVICE"
"CAP_NET_RAW"
];
AmbientCapabilities = [
"CAP_NET_ADMIN"
"CAP_NET_BIND_SERVICE"
"CAP_NET_RAW"
];
ExecStart = ''
${lib.getExe' cfg.client.package "juicity-client"} run --disable-timestamp --config ${clientConfigFile}
'';
Restart = "on-failure";
LimitNPROC = 512;
LimitNOFILE = "infinity";
};
};
})
(lib.mkIf cfg.server.enable {
environment.systemPackages = [
cfg.server.package
];
systemd.services.juicity-server = {
description = ''
juicity-server Service
'';
documentation = [
"https://github.com/juicity/juicity"
];
after = [
"network.target"
"nss-lookup.target"
];
wantedBy = [
"multi-user.target"
];
serviceConfig = {
Type = "simple";
DynamicUser = true;
ExecStart = ''
${lib.getExe' cfg.server.package "juicity-server"} run --disable-timestamp --config ${serverConfigFile}
'';
Restart = "on-failure";
LimitNPROC = 512;
LimitNOFILE = "infinity";
};
};
})
];
}

View file

@ -16,9 +16,13 @@ pkgs.buildGoModule rec {
"cmd/client"
];
vendorHash = "sha256-uULJKg1nh6jU0uIgDf4GMu8O00zifLvU2wv65dlHLAs=";
postInstallPhase = ''
fixupPhase = ''
runHook preFixup
mv $out/bin/server $out/bin/juicity-server
mv $out/bin/client $out/bin/juicity-client
runHook postFixup
'';
ldflags = [

View file

@ -9,6 +9,14 @@ ssh-private-key:
git-credential: ENC[AES256_GCM,data:V86tQJC9Anoe1GEooDZTRsE/n7MPkOIlUYiS4dEYykhnwYtCM8jGPLEYerIwWVhg31zC5JJzhuShDZjj,iv:3D/WmMm3ZLPh8IDChjnruJjKOKvuReB3fjV14B087Ms=,tag:MfjfrqKunGyreM7U1H0MbQ==,type:str]
url: ENC[AES256_GCM,data:snv3FaeR8t30rOX9klSNdY/xqcHGXO1DnVi4GMkvyqaII9l/l8AeSlfOVM4qZq8Mqvn01FaiINOE8WPjhyUs9uYp5pfD7X5EXK+5vWwBYmE/isWlHHHNUhuz3UTV/xiSad4n4MiD8wxlF5u8cImwhDyO+SoG,iv:Tay4S5ZFMEIW6MrHnlen85FGvDJ5ZqfVBlgO5MQWufs=,tag:Njywn0i8W7g6cdDvPeJWEg==,type:str]
open-webui_env: ENC[AES256_GCM,data:HUoNzOqVuu9MtW4VZJfrh4DbzQCtVYa+FzhDs21FpvImuVz9cue0X8s2MXKqYH0LD1US/DJKL4QLLeNTKVMGxmBOCGxSIgeFejnqK5k/r0GF54SBOURWZn/TyzqxZKAym01DUvfNIe68LhvW1LOHaCDK4zsI9BnhkBVjV8/Vmsc=,iv:4aUgQ6HoLqeuUp01fg+yXQRbH6mS/dakZ1ZUdCZzvAM=,tag:GlFnN5bqIcIZadXmFBkSXA==,type:str]
juicity:
serverIp: ENC[AES256_GCM,data:503ZXoFgXMHBslsuhxgt,iv:mQ9e7w41sLyxd+VLfPIx3vRTOZ5+dhLP17ne0hRqb0k=,tag:h5B6QHXNIhj8o7+9vDMUeg==,type:str]
sni: ENC[AES256_GCM,data:aklDmiQFWinfz2JH0fGdU4gXewg=,iv:HxfJvCS3LaFSZP9jTvVLhqheWxLZy2nXoo6HlxDVJNM=,tag:624zVOOVtMzsASQwHeGBuA==,type:str]
uuid: ENC[AES256_GCM,data:D3TSe0Nn2XLn2vWGlrs0Aem6PFN1J7w4J004IKdEXpT/UexM,iv:f0IdDFAdLkAtjYq/Xs3g0eDDDF0srVqjAc6onVPz3yA=,tag:XV8MdOuSSIfpxixO3xkx3w==,type:str]
password: ENC[AES256_GCM,data:kROa/0HCTFdPXLzbV6AEY3PMyg==,iv:jYunQbnA39BkWuytqgvvgX7G6pCTJQVwL8U+9IzXYqM=,tag:B73rxzXhXVSh7Gz+5ACBuw==,type:str]
certchainSha256: ENC[AES256_GCM,data:HH9ZvGxRAfQ4iS92wK0aLSdTCTlVs2kxczecYbtERmnLFPYX+9MZtsa/JTI=,iv:YCOerzGz2QmxI2CIirOldlzoE3eH33xtOqvT/w/UkJs=,tag:jlecqBa+mkvjva5fCZy8rQ==,type:str]
private_key: ENC[AES256_GCM,data:iCz4zjfAZS7QMmkXAOjgD8qhscbM1cclV66Y6r7NokjRgyCtqzicpD5ndGzacl/EMiE3w2oZ1rABbz4lea5NkQl57zxw+XqclieB6OWy1oHO6Nd1bICt4OiO1plCd3T6MuoN5J33oR7/gywlbwyX+Cw7OsCJWc7fv6pYWe9aSDmUWeugCB/jETZ1tSfmUnQ4KQ6VUPSKGEp5tfDhxwvJ9yqRPMaIMfLz6oK+uEnOvAwgow7v8zjygoZtU5SCJitk014kZEbvnKnGmXPWjz1a/44UZYO/I7bhYiHnQwyoTj8QLue3O3oZ625tSEmoPVfQ9F4CRFm/6bd85r/WbyF44lSPcHNUisnUp3K34F7uLIqqALoK09arCoagYoLiPGyOrtbw+s/KEp6jQHF1WtHy1mY=,iv:V5ElPmaU3TJtLBlZT9lLyutR4Y7IoOmQ0N3k0Vuo1ZM=,tag:JRLyJ71+I1fUL70WtBxaGg==,type:str]
certificate: ENC[AES256_GCM,data:Ed//o+qwlSRI2/VlShb/jKHeQgJkH5hlAM85k8ituty+z619HwBLyBma0OqfmHOpZctyjesIz7Y8nnG1jb8Cju12dyPy9Z5sMbvWgKwLH3k/TTU+e42ZhLFT/ZLpo0HMQo52XXyknJHf4hDbfluSpw/rA6Yp79KGLGtEBGZ2BUIWKSs7W/9KOz9yTX+TGVyU2J4jFuTbCDvPwtpotbVRQkqkNSyc/bvuZCJEyAx8B6y3CfkZPNKw8RHJE43Kyh98BtxoIEv3AJB58qPfhL+1jgLWeKfiCZR9VMAZkI2iXmXrxVc2CV9q8d/FMhUhlZkoGO+5Cg6LooBsurIxm+HZ764zYT1OQJoLw/a4jvxiCjoDSC8eTjTTmRUi0f5le8hSapw/GpVGpvFPGXvmQuintnBjYireLFn6QbeEvgPRQigSUiY4TBRypcD1mEpUdKmfk1UMqyvnRsNwBK2kFf1ji9aW7Afj8ukuI3bZ1yv86OeS1bIDmvXM+OsX6xDZ1w+XLnLWTNGg8xP4iLQszNP4d8mEVfJUa0Df5RTSIfnhyMqC6ByGRLzx2nwkya/AX/LNIjSWIrfxTQ0vt60UQUWgpRVkMsAMNBnMUcAcac0JaWdf0X7y47q9ZQ3LEzYGA9u/s6ZDmabbPD/jnIgfk5SDJ42hMhWHPGA6hsno4OAx+DsBj8V2/FqB3Xntru4wevKFgBCYBLlk6fMrya/atoWee9qYDVKj2OTCGwsU5OZNteVtL0GhqMkQL+VqFOIsYGBNBiRbcJL5FQEpn/mES8o9EDPb5foxpIf2yMx5fbwiHMg3wXCGdZP31wLtdbmojmumIQAGdYhM1LXWy6rVW2T6Uf6myQiijjw8Hcb+v9rnnM+l6idwA4E1OVJCOz8wXBhPXjTwSeJT2TeLRQtlAvdK8hk9Ab9bya8yaixpYZO4QgoaEhRyLv095y99vsI0/4V0vsn4yBpIL7+vuiwKCSQ0lQ==,iv:jXIsYPhSM4NaWcsSS0uANsL9NuMXsVjpgVG8AVq/8KE=,tag:u5y6uH2WlKEbFr0EOopq0w==,type:str]
sops:
kms: []
gcp_kms: []
@ -42,8 +50,8 @@ sops:
a1Y1NU9CK2h1SS83VW42bzBMa01yMXMKI1DBtgNlkNCrxUQvnD6a45mQKNfg5gM4
Zb5buo9Jofj4dn/HFwng3T3gxKTrP2Dh74CAH4L0M5yrF9fzk5TCcQ==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2025-02-01T06:13:38Z"
mac: ENC[AES256_GCM,data:mved7T7oQeafIv0BWDHj3C5KaDLKSlxji93xMxunl39ApbtGjQjDqpQwVQ4z4dcMeggJo0SlX+o0tTi8KzruIjgywR0hQvGJFl8Iq7zyC5YB7ojukRI3ZO71ry1+BWNLOSCrnzIxbX3LijbDkXZ4pkC2lwrqy63P8BUPYNySTQQ=,iv:ukjQB5Ax6GASPXdXJAy+yqiTxtxQxa+wNMo2RYZFEgk=,tag:Z/5ImiyIHFXMozZsY6L9Dw==,type:str]
lastmodified: "2025-02-03T11:00:21Z"
mac: ENC[AES256_GCM,data:tWVQhBcoXzdlC5wQkJ3WHxVSMLOGRqQXjvvNA94BnAgqJgTHymuvLik/8zR2OIskZqzYL0lTXq4OFbm8r552fkx+3+ip45wrtjB0hiSp7lq4lITWnjfZDWr3dBkbFfO2aFij37slpwbVmzGmsM1WaIlfRcauwjOD5IkewiBDY9Y=,iv:GK1WcUSK2WPPplo3uoRJNSmEAPpFObDi784kLuYWtww=,tag:M7YLy1o/bhZPV+bUJ/AQow==,type:str]
pgp: []
unencrypted_suffix: _unencrypted
version: 3.9.4