nixpkgs/nixos/modules/services/networking/opengfw.nix
2024-07-24 15:11:23 +02:00

415 lines
13 KiB
Nix

{
lib,
pkgs,
config,
...
}:
let
inherit (lib)
mkOption
types
mkIf
optionalString
;
cfg = config.services.opengfw;
in
{
options.services.opengfw = {
enable = lib.mkEnableOption ''
OpenGFW, A flexible, easy-to-use, open source implementation of GFW on Linux
'';
package = lib.mkPackageOption pkgs "opengfw" { default = "opengfw"; };
user = mkOption {
default = "opengfw";
type = types.singleLineStr;
description = "Username of the OpenGFW user.";
};
dir = mkOption {
default = "/var/lib/opengfw";
type = types.singleLineStr;
description = ''
Working directory of the OpenGFW service and home of `opengfw.user`.
'';
};
logFile = mkOption {
default = null;
type = types.nullOr types.path;
example = "/var/lib/opengfw/opengfw.log";
description = ''
File to write the output to instead of systemd.
'';
};
logFormat = mkOption {
description = ''
Format of the logs. [logFormatMap](https://github.com/apernet/OpenGFW/blob/d7737e92117a11c9a6100d53019fac3b9d724fe3/cmd/root.go#L62)
'';
default = "json";
example = "console";
type = types.enum [
"json"
"console"
];
};
pcapReplay = mkOption {
default = null;
example = "./opengfw.pcap";
type = types.nullOr types.path;
description = ''
Path to PCAP replay file.
In pcap mode, none of the actions in the rules have any effect.
This mode is mainly for debugging.
'';
};
logLevel = mkOption {
description = ''
Level of the logs. [logLevelMap](https://github.com/apernet/OpenGFW/blob/d7737e92117a11c9a6100d53019fac3b9d724fe3/cmd/root.go#L55)
'';
default = "info";
example = "warn";
type = types.enum [
"debug"
"info"
"warn"
"error"
];
};
rulesFile = mkOption {
default = null;
type = types.nullOr types.path;
description = ''
Path to file containing OpenGFW rules.
'';
};
settingsFile = mkOption {
default = null;
type = types.nullOr types.path;
description = ''
Path to file containing OpenGFW settings.
'';
};
settings = mkOption {
default = null;
description = ''
Settings passed to OpenGFW. [Example config](https://gfw.dev/docs/build-run/#config-example)
'';
type = types.nullOr (
types.submodule {
options = {
replay = mkOption {
description = ''
PCAP replay settings.
'';
default = { };
type = types.submodule {
options = {
realtime = mkOption {
description = ''
Whether the packets in the PCAP file should be replayed in "real time" (instead of as fast as possible).
'';
default = false;
example = true;
type = types.bool;
};
};
};
};
io = mkOption {
description = ''
IO settings.
'';
default = { };
type = types.submodule {
options = {
queueSize = mkOption {
description = "IO queue size.";
type = types.int;
default = 1024;
example = 2048;
};
local = mkOption {
description = ''
Set to false if you want to run OpenGFW on FORWARD chain. (e.g. on a router)
'';
type = types.bool;
default = true;
example = false;
};
rst = mkOption {
description = ''
Set to true if you want to send RST for blocked TCP connections, needs `local = false`.
'';
type = types.bool;
default = !cfg.settings.io.local;
defaultText = "`!config.services.opengfw.settings.io.local`";
example = false;
};
rcvBuf = mkOption {
description = "Netlink receive buffer size.";
type = types.int;
default = 4194304;
example = 2097152;
};
sndBuf = mkOption {
description = "Netlink send buffer size.";
type = types.int;
default = 4194304;
example = 2097152;
};
};
};
};
ruleset = mkOption {
description = ''
The path to load specific local geoip/geosite db files.
If not set, they will be automatically downloaded from (Loyalsoldier/v2ray-rules-dat)[https://github.com/Loyalsoldier/v2ray-rules-dat].
'';
default = { };
type = types.submodule {
options = {
geoip = mkOption {
description = "Path to `geoip.dat`.";
default = null;
type = types.nullOr types.path;
};
geosite = mkOption {
description = "Path to `geosite.dat`.";
default = null;
type = types.nullOr types.path;
};
};
};
};
workers = mkOption {
default = { };
description = "Worker settings.";
type = types.submodule {
options = {
count = mkOption {
type = types.int;
description = ''
Number of workers.
Recommended to be no more than the number of CPU cores
'';
default = 4;
example = 8;
};
queueSize = mkOption {
type = types.int;
description = "Worker queue size.";
default = 16;
example = 32;
};
tcpMaxBufferedPagesTotal = mkOption {
type = types.int;
description = ''
TCP max total buffered pages.
'';
default = 4096;
example = 8192;
};
tcpMaxBufferedPagesPerConn = mkOption {
type = types.int;
description = ''
TCP max total bufferd pages per connection.
'';
default = 64;
example = 128;
};
tcpTimeout = mkOption {
type = types.str;
description = ''
How long a connection is considered dead when no data is being transferred.
Dead connections are purged from TCP reassembly pools once per minute.
'';
default = "10m";
example = "5m";
};
udpMaxStreams = mkOption {
type = types.int;
description = "UDP max streams.";
default = 4096;
example = 8192;
};
};
};
};
};
}
);
};
rules = mkOption {
default = [ ];
description = ''
Rules passed to OpenGFW. [Example rules](https://gfw.dev/docs/rules)
'';
type = types.listOf (
types.submodule {
options = {
name = mkOption {
description = "Name of the rule.";
example = "block google dns";
type = types.singleLineStr;
};
action = mkOption {
description = ''
Action of the rule. [Supported actions](https://gfw.dev/docs/rules#supported-actions)
'';
default = "allow";
example = "block";
type = types.enum [
"allow"
"block"
"drop"
"modify"
];
};
log = mkOption {
description = "Whether to enable logging for the rule.";
default = true;
example = false;
type = types.bool;
};
expr = mkOption {
description = ''
[Expr Language](https://expr-lang.org/docs/language-definition) expression using [analyzers](https://gfw.dev/docs/analyzers) and [functions](https://gfw.dev/docs/functions).
'';
type = types.str;
example = ''dns != nil && dns.qr && any(dns.questions, {.name endsWith "google.com"})'';
};
modifier = mkOption {
default = null;
description = ''
Modification of specified packets when using the `modify` action. [Available modifiers](https://github.com/apernet/OpenGFW/tree/master/modifier)
'';
type = types.nullOr (
types.submodule {
options = {
name = mkOption {
description = "Name of the modifier.";
type = types.singleLineStr;
example = "dns";
};
args = mkOption {
description = "Arguments passed to the modifier.";
type = types.attrs;
example = {
a = "0.0.0.0";
aaaa = "::";
};
};
};
}
);
};
};
}
);
example = [
{
name = "block v2ex http";
action = "block";
expr = ''string(http?.req?.headers?.host) endsWith "v2ex.com"'';
}
{
name = "block google socks";
action = "block";
expr = ''string(socks?.req?.addr) endsWith "google.com" && socks?.req?.port == 80'';
}
{
name = "v2ex dns poisoning";
action = "modify";
modifier = {
name = "dns";
args = {
a = "0.0.0.0";
aaaa = "::";
};
};
expr = ''dns != nil && dns.qr && any(dns.questions, {.name endsWith "v2ex.com"})'';
}
];
};
};
config =
let
format = pkgs.formats.yaml { };
settings =
if cfg.settings != null then
format.generate "opengfw-config.yaml" cfg.settings
else
cfg.settingsFile;
rules = if cfg.rules != [ ] then format.generate "opengfw-rules.yaml" cfg.rules else cfg.rulesFile;
in
mkIf cfg.enable {
security.wrappers.OpenGFW = {
owner = cfg.user;
group = cfg.user;
capabilities = "cap_net_admin+ep";
source = "${cfg.package}/bin/OpenGFW";
};
systemd.services.opengfw = {
description = "OpenGFW";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
path = with pkgs; [ iptables ];
preStart = ''
${optionalString (rules != null) "ln -sf ${rules} rules.yaml"}
${optionalString (settings != null) "ln -sf ${settings} config.yaml"}
'';
script = ''
${config.security.wrapperDir}/OpenGFW \
-f ${cfg.logFormat} \
-l ${cfg.logLevel} \
${optionalString (cfg.pcapReplay != null) "-p ${cfg.pcapReplay}"} \
-c config.yaml \
rules.yaml
'';
serviceConfig = rec {
WorkingDirectory = cfg.dir;
ExecReload = "kill -HUP $MAINPID";
Restart = "always";
User = cfg.user;
StandardOutput = mkIf (cfg.logFile != null) "append:${cfg.logFile}";
StandardError = StandardOutput;
};
};
users = {
groups.${cfg.user} = { };
users.${cfg.user} = {
description = "opengfw user";
isSystemUser = true;
group = cfg.user;
home = cfg.dir;
createHome = true;
homeMode = "750";
};
};
};
meta.maintainers = with lib.maintainers; [ eum3l ];
}