diff --git a/maintainers/maintainer-list.nix b/maintainers/maintainer-list.nix index 53ae8880787d808..6eae89711f8bf36 100644 --- a/maintainers/maintainer-list.nix +++ b/maintainers/maintainer-list.nix @@ -6015,6 +6015,11 @@ githubId = 2147649; name = "Euan Kemp"; }; + eum3l = { + githubId = 77971322; + github = "eum3l"; + name = "Emil"; + }; eureka-cpu = { email = "github.eureka@gmail.com"; github = "eureka-cpu"; diff --git a/nixos/doc/manual/release-notes/rl-2405.section.md b/nixos/doc/manual/release-notes/rl-2405.section.md index 1c1fe7f997cf76d..17720603c507c42 100644 --- a/nixos/doc/manual/release-notes/rl-2405.section.md +++ b/nixos/doc/manual/release-notes/rl-2405.section.md @@ -121,6 +121,8 @@ The pre-existing [services.ankisyncd](#opt-services.ankisyncd.enable) has been m - [Mealie](https://nightly.mealie.io/), a self-hosted recipe manager and meal planner with a RestAPI backend and a reactive frontend application built in NuxtJS for a pleasant user experience for the whole family. Available as [services.mealie](#opt-services.mealie.enable) +- [OpenGFW](https://github.com/apernet/OpenGFW), a implementation of the Great Firewall on Linux. Available as [services.opengfw](#opt-services.opengfw.enable). + ## Backward Incompatibilities {#sec-release-24.05-incompatibilities} diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 2ccaea466c6a756..aa9e48413e3c601 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1071,6 +1071,7 @@ ./services/networking/oidentd.nix ./services/networking/onedrive.nix ./services/networking/openconnect.nix + ./services/networking/opengfw.nix ./services/networking/openvpn.nix ./services/networking/ostinato.nix ./services/networking/owamp.nix diff --git a/nixos/modules/services/networking/opengfw.nix b/nixos/modules/services/networking/opengfw.nix new file mode 100644 index 000000000000000..d8e63769331fd87 --- /dev/null +++ b/nixos/modules/services/networking/opengfw.nix @@ -0,0 +1,333 @@ +{ lib +, pkgs +, config +, ... +}: +let + inherit (lib) mkOption types mkIf mdDoc optionalString; + cfg = config.services.opengfw; + 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 +{ + options.services.opengfw = { + enable = lib.mkEnableOption (mdDoc "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 = mdDoc '' + Username of OpenGFW user. + ''; + }; + + dir = mkOption { + default = "/var/lib/opengfw"; + type = types.singleLineStr; + description = mdDoc '' + Working directory of service and home of opengfw.user. + ''; + }; + + logDir = mkOption { + default = null; + type = types.nullOr types.singleLineStr; + example = "/home/user/opengfw.log"; + description = mdDoc '' + File to write the output to instead of systemd. + ''; + }; + + rulesFile = mkOption { + default = null; + type = types.nullOr types.path; + description = mdDoc '' + File instead of declaring opengfw.rules. + ''; + }; + + settingsFile = mkOption { + default = null; + type = types.nullOr types.path; + description = mdDoc '' + File instead of declaring opengfw.settings. + ''; + }; + + settings = mkOption { + default = null; + description = mdDoc '' + Settings passed to OpenGFW. [Example config](https://github.com/apernet/OpenGFW#example-config) + ''; + type = types.nullOr (types.submodule { + options = { + io = mkOption { + description = mdDoc '' + IO settings. + ''; + default = { }; + type = types.submodule { + options = { + queueSize = mkOption { + description = mdDoc '' + IO queue size. + ''; + type = types.int; + default = 1024; + example = 2048; + }; + local = mkOption { + description = mdDoc '' + Set to false if you want to run OpenGFW on FORWARD chain. + ''; + type = types.bool; + default = true; + example = false; + }; + rcvBuf = mkOption { + description = mdDoc '' + Netlink recieve buffer size. + ''; + type = types.int; + default = 4194304; + example = 2097152; + }; + sndBuf = mkOption { + description = mdDoc '' + Netlink send buffer size. + ''; + type = types.int; + default = 4194304; + example = 2097152; + }; + }; + }; + }; + geo = mkOption { + description = mdDoc '' + 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 = mdDoc '' + IO queue size. + ''; + default = null; + type = types.nullOr types.path; + }; + geosite = mkOption { + description = mdDoc '' + Set to false if you want to run OpenGFW on FORWARD chain. + ''; + default = null; + type = types.nullOr types.path; + }; + }; + }; + }; + workers = mkOption { + default = { }; + description = '' + Worker settings. + ''; + type = types.submodule { + options = { + count = mkOption { + type = types.int; + description = mdDoc '' + Number of workers. + ''; + default = 4; + example = 8; + }; + queueSize = mkOption { + type = types.int; + description = mdDoc '' + Worker queue size. + ''; + default = 16; + example = 32; + }; + tcpMaxBufferedPagesTotal = mkOption { + type = types.int; + description = mdDoc '' + TCP max total buffered pages. + ''; + default = 4096; + example = 8192; + }; + tcpMaxBufferedPagesPerConn = mkOption { + type = types.int; + description = mdDoc '' + TCP max total bufferd pages per connection. + ''; + default = 64; + example = 128; + }; + udpMaxStreams = mkOption { + type = types.int; + description = mdDoc '' + UDP max streams. + ''; + default = 4096; + example = 8192; + }; + }; + }; + }; + }; + }); + }; + + rules = mkOption { + default = [ ]; + description = mdDoc '' + Rules passed to OpenGFW. [Example rules](https://github.com/apernet/OpenGFW?tab=readme-ov-file#example-rules) + ''; + type = types.listOf ( + types.submodule { + options = { + name = mkOption { + description = mdDoc "Name of the rule."; + example = "block google dns"; + type = types.singleLineStr; + }; + + action = mkOption { + description = mdDoc '' + Action of the rule. [Supported actions](https://github.com/apernet/OpenGFW?tab=readme-ov-file#supported-actions) + ''; + default = "allow"; + example = "block"; + type = types.enum [ "allow" "block" "drop" "modify" ]; + }; + + log = mkOption { + description = mdDoc "Wether to enable logging for the rule."; + default = true; + example = false; + type = types.bool; + }; + + expr = mkOption { + description = mdDoc '' + [Expr Language](https://expr-lang.org/docs/language-definition) expression using [OpenGFW analyzers](https://github.com/apernet/OpenGFW/blob/master/docs/Analyzers.md). + ''; + type = types.str; + example = ''dns != nil && dns.qr && any(dns.questions, {.name endsWith "google.com"})''; + }; + + modifier = mkOption { + default = null; + description = mdDoc '' + 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 = mdDoc "Name of the modifier."; + type = types.singleLineStr; + example = "dns"; + }; + + args = mkOption { + description = mdDoc "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 = 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"} + ''; + + serviceConfig = rec { + WorkingDirectory = cfg.dir; + ExecStart = "${config.security.wrapperDir}/OpenGFW -c config.yaml rules.yaml"; + ExecReload = "kill -HUP $MAINPID"; + Restart = "always"; + User = cfg.user; + StandardOutput = mkIf (cfg.logDir != null) "append:${cfg.logDir}"; + StandardError = StandardOutput; + }; + }; + + users = { + groups.${cfg.user} = { }; + users.${cfg.user} = { + description = "opengfw user"; + isNormalUser = true; + group = cfg.user; + home = cfg.dir; + }; + }; + }; + + meta.maintainers = with lib.maintainers; [ eum3l ]; +} diff --git a/pkgs/by-name/op/opengfw/package.nix b/pkgs/by-name/op/opengfw/package.nix new file mode 100644 index 000000000000000..b7852259cfa3757 --- /dev/null +++ b/pkgs/by-name/op/opengfw/package.nix @@ -0,0 +1,37 @@ +{ lib +, buildGoModule +, fetchFromGitHub +, +}: +buildGoModule rec { + pname = "opengfw"; + version = "0.2.3"; + + vendorHash = "sha256-nxehSjdVvcexpR2uwR7kNIRoD88ye9caB2RQJvqQFB8="; + src = fetchFromGitHub { + owner = "apernet"; + repo = "OpenGFW"; + rev = "v${version}"; + hash = "sha256-3s4NodBZbLpamNzgDmWt/GBqpcljCoZL4szJzGcnoKw="; + }; + + patches = [ + ./v2geo-remove-test.patch + ]; + + meta = with lib; { + mainProgram = "OpenGFW"; + description = "A flexible, easy-to-use, open source implementation of GFW on Linux"; + longDescription = '' + OpenGFW is your very own DIY Great Firewall of China, available as a flexible, + easy-to-use open source program on Linux. Why let the powers that be have all the fun? + It's time to give power to the people and democratize censorship. + Bring the thrill of cyber-sovereignty right into your home router + and start filtering like a pro - you too can play Big Brother. + ''; + homepage = "https://github.com/apernet/OpenGFW"; + license = licenses.mpl20; + platforms = platforms.linux; + maintainers = with lib.maintainers; [ eum3l ]; + }; +} diff --git a/pkgs/by-name/op/opengfw/v2geo-remove-test.patch b/pkgs/by-name/op/opengfw/v2geo-remove-test.patch new file mode 100644 index 000000000000000..d6398f82ad0d4d8 --- /dev/null +++ b/pkgs/by-name/op/opengfw/v2geo-remove-test.patch @@ -0,0 +1,60 @@ +diff --git a/ruleset/builtins/geo/v2geo/load_test.go b/ruleset/builtins/geo/v2geo/load_test.go +deleted file mode 100644 +index e9c901a..0000000 +--- a/ruleset/builtins/geo/v2geo/load_test.go ++++ /dev/null +@@ -1,54 +0,0 @@ +-package v2geo +- +-import ( +- "testing" +- +- "github.com/stretchr/testify/assert" +-) +- +-func TestLoadGeoIP(t *testing.T) { +- m, err := LoadGeoIP("geoip.dat") +- assert.NoError(t, err) +- +- // Exact checks since we know the data. +- assert.Len(t, m, 252) +- assert.Equal(t, m["cn"].CountryCode, "CN") +- assert.Len(t, m["cn"].Cidr, 10407) +- assert.Equal(t, m["us"].CountryCode, "US") +- assert.Len(t, m["us"].Cidr, 193171) +- assert.Equal(t, m["private"].CountryCode, "PRIVATE") +- assert.Len(t, m["private"].Cidr, 18) +- assert.Contains(t, m["private"].Cidr, &CIDR{ +- Ip: []byte("\xc0\xa8\x00\x00"), +- Prefix: 16, +- }) +-} +- +-func TestLoadGeoSite(t *testing.T) { +- m, err := LoadGeoSite("geosite.dat") +- assert.NoError(t, err) +- +- // Exact checks since we know the data. +- assert.Len(t, m, 1204) +- assert.Equal(t, m["netflix"].CountryCode, "NETFLIX") +- assert.Len(t, m["netflix"].Domain, 25) +- assert.Contains(t, m["netflix"].Domain, &Domain{ +- Type: Domain_Full, +- Value: "netflix.com.edgesuite.net", +- }) +- assert.Contains(t, m["netflix"].Domain, &Domain{ +- Type: Domain_RootDomain, +- Value: "fast.com", +- }) +- assert.Len(t, m["google"].Domain, 1066) +- assert.Contains(t, m["google"].Domain, &Domain{ +- Type: Domain_RootDomain, +- Value: "ggpht.cn", +- Attribute: []*Domain_Attribute{ +- { +- Key: "cn", +- TypedValue: &Domain_Attribute_BoolValue{BoolValue: true}, +- }, +- }, +- }) +-}