at 25.11-pre 13 kB view raw
1{ 2 lib, 3 pkgs, 4 config, 5 ... 6}: 7let 8 inherit (lib) 9 mkOption 10 types 11 mkIf 12 optionalString 13 ; 14 cfg = config.services.opengfw; 15in 16{ 17 options.services.opengfw = { 18 enable = lib.mkEnableOption '' 19 OpenGFW, A flexible, easy-to-use, open source implementation of GFW on Linux 20 ''; 21 22 package = lib.mkPackageOption pkgs "opengfw" { default = "opengfw"; }; 23 24 user = mkOption { 25 default = "opengfw"; 26 type = types.singleLineStr; 27 description = "Username of the OpenGFW user."; 28 }; 29 30 dir = mkOption { 31 default = "/var/lib/opengfw"; 32 type = types.singleLineStr; 33 description = '' 34 Working directory of the OpenGFW service and home of `opengfw.user`. 35 ''; 36 }; 37 38 logFile = mkOption { 39 default = null; 40 type = types.nullOr types.path; 41 example = "/var/lib/opengfw/opengfw.log"; 42 description = '' 43 File to write the output to instead of systemd. 44 ''; 45 }; 46 47 logFormat = mkOption { 48 description = '' 49 Format of the logs. [logFormatMap](https://github.com/apernet/OpenGFW/blob/d7737e92117a11c9a6100d53019fac3b9d724fe3/cmd/root.go#L62) 50 ''; 51 default = "json"; 52 example = "console"; 53 type = types.enum [ 54 "json" 55 "console" 56 ]; 57 }; 58 59 pcapReplay = mkOption { 60 default = null; 61 example = "./opengfw.pcap"; 62 type = types.nullOr types.path; 63 description = '' 64 Path to PCAP replay file. 65 In pcap mode, none of the actions in the rules have any effect. 66 This mode is mainly for debugging. 67 ''; 68 }; 69 70 logLevel = mkOption { 71 description = '' 72 Level of the logs. [logLevelMap](https://github.com/apernet/OpenGFW/blob/d7737e92117a11c9a6100d53019fac3b9d724fe3/cmd/root.go#L55) 73 ''; 74 default = "info"; 75 example = "warn"; 76 type = types.enum [ 77 "debug" 78 "info" 79 "warn" 80 "error" 81 ]; 82 }; 83 84 rulesFile = mkOption { 85 default = null; 86 type = types.nullOr types.path; 87 description = '' 88 Path to file containing OpenGFW rules. 89 ''; 90 }; 91 92 settingsFile = mkOption { 93 default = null; 94 type = types.nullOr types.path; 95 description = '' 96 Path to file containing OpenGFW settings. 97 ''; 98 }; 99 100 settings = mkOption { 101 default = null; 102 description = '' 103 Settings passed to OpenGFW. [Example config](https://gfw.dev/docs/build-run/#config-example) 104 ''; 105 type = types.nullOr ( 106 types.submodule { 107 options = { 108 replay = mkOption { 109 description = '' 110 PCAP replay settings. 111 ''; 112 default = { }; 113 type = types.submodule { 114 options = { 115 realtime = mkOption { 116 description = '' 117 Whether the packets in the PCAP file should be replayed in "real time" (instead of as fast as possible). 118 ''; 119 default = false; 120 example = true; 121 type = types.bool; 122 }; 123 }; 124 }; 125 }; 126 127 io = mkOption { 128 description = '' 129 IO settings. 130 ''; 131 default = { }; 132 type = types.submodule { 133 options = { 134 queueSize = mkOption { 135 description = "IO queue size."; 136 type = types.int; 137 default = 1024; 138 example = 2048; 139 }; 140 local = mkOption { 141 description = '' 142 Set to false if you want to run OpenGFW on FORWARD chain. (e.g. on a router) 143 ''; 144 type = types.bool; 145 default = true; 146 example = false; 147 }; 148 rst = mkOption { 149 description = '' 150 Set to true if you want to send RST for blocked TCP connections, needs `local = false`. 151 ''; 152 type = types.bool; 153 default = !cfg.settings.io.local; 154 defaultText = "`!config.services.opengfw.settings.io.local`"; 155 example = false; 156 }; 157 rcvBuf = mkOption { 158 description = "Netlink receive buffer size."; 159 type = types.int; 160 default = 4194304; 161 example = 2097152; 162 }; 163 sndBuf = mkOption { 164 description = "Netlink send buffer size."; 165 type = types.int; 166 default = 4194304; 167 example = 2097152; 168 }; 169 }; 170 }; 171 }; 172 ruleset = mkOption { 173 description = '' 174 The path to load specific local geoip/geosite db files. 175 If not set, they will be automatically downloaded from [Loyalsoldier/v2ray-rules-dat](https://github.com/Loyalsoldier/v2ray-rules-dat). 176 ''; 177 default = { }; 178 type = types.submodule { 179 options = { 180 geoip = mkOption { 181 description = "Path to `geoip.dat`."; 182 default = null; 183 type = types.nullOr types.path; 184 }; 185 geosite = mkOption { 186 description = "Path to `geosite.dat`."; 187 default = null; 188 type = types.nullOr types.path; 189 }; 190 }; 191 }; 192 }; 193 workers = mkOption { 194 default = { }; 195 description = "Worker settings."; 196 type = types.submodule { 197 options = { 198 count = mkOption { 199 type = types.int; 200 description = '' 201 Number of workers. 202 Recommended to be no more than the number of CPU cores 203 ''; 204 default = 4; 205 example = 8; 206 }; 207 queueSize = mkOption { 208 type = types.int; 209 description = "Worker queue size."; 210 default = 16; 211 example = 32; 212 }; 213 tcpMaxBufferedPagesTotal = mkOption { 214 type = types.int; 215 description = '' 216 TCP max total buffered pages. 217 ''; 218 default = 4096; 219 example = 8192; 220 }; 221 tcpMaxBufferedPagesPerConn = mkOption { 222 type = types.int; 223 description = '' 224 TCP max total bufferd pages per connection. 225 ''; 226 default = 64; 227 example = 128; 228 }; 229 tcpTimeout = mkOption { 230 type = types.str; 231 description = '' 232 How long a connection is considered dead when no data is being transferred. 233 Dead connections are purged from TCP reassembly pools once per minute. 234 ''; 235 default = "10m"; 236 example = "5m"; 237 }; 238 udpMaxStreams = mkOption { 239 type = types.int; 240 description = "UDP max streams."; 241 default = 4096; 242 example = 8192; 243 }; 244 }; 245 }; 246 }; 247 }; 248 } 249 ); 250 }; 251 252 rules = mkOption { 253 default = [ ]; 254 description = '' 255 Rules passed to OpenGFW. [Example rules](https://gfw.dev/docs/rules) 256 ''; 257 type = types.listOf ( 258 types.submodule { 259 options = { 260 name = mkOption { 261 description = "Name of the rule."; 262 example = "block google dns"; 263 type = types.singleLineStr; 264 }; 265 266 action = mkOption { 267 description = '' 268 Action of the rule. [Supported actions](https://gfw.dev/docs/rules#supported-actions) 269 ''; 270 default = "allow"; 271 example = "block"; 272 type = types.enum [ 273 "allow" 274 "block" 275 "drop" 276 "modify" 277 ]; 278 }; 279 280 log = mkOption { 281 description = "Whether to enable logging for the rule."; 282 default = true; 283 example = false; 284 type = types.bool; 285 }; 286 287 expr = mkOption { 288 description = '' 289 [Expr Language](https://expr-lang.org/docs/language-definition) expression using [analyzers](https://gfw.dev/docs/analyzers) and [functions](https://gfw.dev/docs/functions). 290 ''; 291 type = types.str; 292 example = ''dns != nil && dns.qr && any(dns.questions, {.name endsWith "google.com"})''; 293 }; 294 295 modifier = mkOption { 296 default = null; 297 description = '' 298 Modification of specified packets when using the `modify` action. [Available modifiers](https://github.com/apernet/OpenGFW/tree/master/modifier) 299 ''; 300 type = types.nullOr ( 301 types.submodule { 302 options = { 303 name = mkOption { 304 description = "Name of the modifier."; 305 type = types.singleLineStr; 306 example = "dns"; 307 }; 308 309 args = mkOption { 310 description = "Arguments passed to the modifier."; 311 type = types.attrs; 312 example = { 313 a = "0.0.0.0"; 314 aaaa = "::"; 315 }; 316 }; 317 }; 318 } 319 ); 320 }; 321 }; 322 } 323 ); 324 325 example = [ 326 { 327 name = "block v2ex http"; 328 action = "block"; 329 expr = ''string(http?.req?.headers?.host) endsWith "v2ex.com"''; 330 } 331 { 332 name = "block google socks"; 333 action = "block"; 334 expr = ''string(socks?.req?.addr) endsWith "google.com" && socks?.req?.port == 80''; 335 } 336 { 337 name = "v2ex dns poisoning"; 338 action = "modify"; 339 modifier = { 340 name = "dns"; 341 args = { 342 a = "0.0.0.0"; 343 aaaa = "::"; 344 }; 345 }; 346 expr = ''dns != nil && dns.qr && any(dns.questions, {.name endsWith "v2ex.com"})''; 347 } 348 ]; 349 }; 350 }; 351 352 config = 353 let 354 format = pkgs.formats.yaml { }; 355 356 settings = 357 if cfg.settings != null then 358 format.generate "opengfw-config.yaml" cfg.settings 359 else 360 cfg.settingsFile; 361 rules = if cfg.rules != [ ] then format.generate "opengfw-rules.yaml" cfg.rules else cfg.rulesFile; 362 in 363 mkIf cfg.enable { 364 security.wrappers.OpenGFW = { 365 owner = cfg.user; 366 group = cfg.user; 367 capabilities = "cap_net_admin+ep"; 368 source = "${cfg.package}/bin/OpenGFW"; 369 }; 370 371 systemd.services.opengfw = { 372 description = "OpenGFW"; 373 wantedBy = [ "multi-user.target" ]; 374 after = [ "network.target" ]; 375 path = with pkgs; [ iptables ]; 376 377 preStart = '' 378 ${optionalString (rules != null) "ln -sf ${rules} rules.yaml"} 379 ${optionalString (settings != null) "ln -sf ${settings} config.yaml"} 380 ''; 381 382 script = '' 383 ${config.security.wrapperDir}/OpenGFW \ 384 -f ${cfg.logFormat} \ 385 -l ${cfg.logLevel} \ 386 ${optionalString (cfg.pcapReplay != null) "-p ${cfg.pcapReplay}"} \ 387 -c config.yaml \ 388 rules.yaml 389 ''; 390 391 serviceConfig = rec { 392 WorkingDirectory = cfg.dir; 393 ExecReload = "kill -HUP $MAINPID"; 394 Restart = "always"; 395 User = cfg.user; 396 StandardOutput = mkIf (cfg.logFile != null) "append:${cfg.logFile}"; 397 StandardError = StandardOutput; 398 }; 399 }; 400 401 users = { 402 groups.${cfg.user} = { }; 403 users.${cfg.user} = { 404 description = "opengfw user"; 405 isSystemUser = true; 406 group = cfg.user; 407 home = cfg.dir; 408 createHome = true; 409 homeMode = "750"; 410 }; 411 }; 412 }; 413 meta.maintainers = with lib.maintainers; [ eum3l ]; 414}