1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7
8let
9 cfg = config.programs.captive-browser;
10
11 inherit (lib)
12 concatStringsSep
13 escapeShellArgs
14 optionalString
15 literalExpression
16 mkEnableOption
17 mkPackageOption
18 mkIf
19 mkOption
20 mkOptionDefault
21 types
22 ;
23
24 requiresSetcapWrapper = config.boot.kernelPackages.kernelOlder "5.7" && cfg.bindInterface;
25
26 browserDefault =
27 chromium:
28 concatStringsSep " " [
29 ''env XDG_CONFIG_HOME="$PREV_CONFIG_HOME"''
30 ''${chromium}/bin/chromium''
31 ''--user-data-dir=''${XDG_DATA_HOME:-$HOME/.local/share}/chromium-captive''
32 ''--proxy-server="socks5://$PROXY"''
33 ''--host-resolver-rules="MAP * ~NOTFOUND , EXCLUDE localhost"''
34 ''--no-first-run''
35 ''--new-window''
36 ''--incognito''
37 ''-no-default-browser-check''
38 ''http://cache.nixos.org/''
39 ];
40
41 desktopItem = pkgs.makeDesktopItem {
42 name = "captive-browser";
43 desktopName = "Captive Portal Browser";
44 exec = "captive-browser";
45 icon = "nix-snowflake";
46 categories = [ "Network" ];
47 };
48
49 captive-browser-configured = pkgs.writeShellScriptBin "captive-browser" ''
50 export PREV_CONFIG_HOME="$XDG_CONFIG_HOME"
51 export XDG_CONFIG_HOME=${pkgs.writeTextDir "captive-browser.toml" ''
52 browser = """${cfg.browser}"""
53 dhcp-dns = """${cfg.dhcp-dns}"""
54 socks5-addr = """${cfg.socks5-addr}"""
55 ${optionalString cfg.bindInterface ''
56 bind-device = """${cfg.interface}"""
57 ''}
58 ''}
59 exec ${cfg.package}/bin/captive-browser
60 '';
61in
62{
63 ###### interface
64
65 options = {
66 programs.captive-browser = {
67 enable = mkEnableOption "captive browser, a dedicated Chrome instance to log into captive portals without messing with DNS settings";
68
69 package = mkPackageOption pkgs "captive-browser" { };
70
71 interface = mkOption {
72 type = types.str;
73 description = "your public network interface (wlp3s0, wlan0, eth0, ...)";
74 };
75
76 # the options below are the same as in "captive-browser.toml"
77 browser = mkOption {
78 type = types.str;
79 default = browserDefault pkgs.chromium;
80 defaultText = literalExpression (browserDefault "\${pkgs.chromium}");
81 description = ''
82 The shell (/bin/sh) command executed once the proxy starts.
83 When browser exits, the proxy exits. An extra env var PROXY is available.
84
85 Here, we use a separate Chrome instance in Incognito mode, so that
86 it can run (and be waited for) alongside the default one, and that
87 it maintains no state across runs. To configure this browser open a
88 normal window in it, settings will be preserved.
89
90 @volth: chromium is to open a plain HTTP (not HTTPS nor redirect to HTTPS!) website.
91 upstream uses http://example.com but I have seen captive portals whose DNS server resolves "example.com" to 127.0.0.1
92 '';
93 };
94
95 dhcp-dns = mkOption {
96 type = types.str;
97 description = ''
98 The shell (/bin/sh) command executed to obtain the DHCP
99 DNS server address. The first match of an IPv4 regex is used.
100 IPv4 only, because let's be real, it's a captive portal.
101 '';
102 };
103
104 socks5-addr = mkOption {
105 type = types.str;
106 default = "localhost:1666";
107 description = "the listen address for the SOCKS5 proxy server";
108 };
109
110 bindInterface = mkOption {
111 default = true;
112 type = types.bool;
113 description = ''
114 Binds `captive-browser` to the network interface declared in
115 `cfg.interface`. This can be used to avoid collisions
116 with private subnets.
117 '';
118 };
119 };
120 };
121
122 ###### implementation
123
124 config = mkIf cfg.enable {
125 environment.systemPackages = [
126 (pkgs.runCommand "captive-browser-desktop-item" { } ''
127 install -Dm444 -t $out/share/applications ${desktopItem}/share/applications/*.desktop
128 '')
129 captive-browser-configured
130 ];
131
132 programs.captive-browser.dhcp-dns =
133 let
134 iface =
135 prefixes: optionalString cfg.bindInterface (escapeShellArgs (prefixes ++ [ cfg.interface ]));
136 in
137 mkOptionDefault (
138 if config.networking.networkmanager.enable then
139 "${pkgs.networkmanager}/bin/nmcli dev show ${iface [ ]} | ${pkgs.gnugrep}/bin/fgrep IP4.DNS"
140 else if config.networking.dhcpcd.enable then
141 "${pkgs.dhcpcd}/bin/dhcpcd ${iface [ "-U" ]} | ${pkgs.gnugrep}/bin/fgrep domain_name_servers"
142 else if config.networking.useNetworkd then
143 "${cfg.package}/bin/systemd-networkd-dns ${iface [ ]}"
144 else
145 "${config.security.wrapperDir}/udhcpc --quit --now -f ${iface [ "-i" ]} -O dns --script ${pkgs.writeShellScript "udhcp-script" ''
146 if [ "$1" = bound ]; then
147 echo "$dns"
148 fi
149 ''}"
150 );
151
152 security.wrappers.udhcpc = {
153 owner = "root";
154 group = "root";
155 capabilities = "cap_net_raw+p";
156 source = "${pkgs.busybox}/bin/udhcpc";
157 };
158
159 security.wrappers.captive-browser = mkIf requiresSetcapWrapper {
160 owner = "root";
161 group = "root";
162 capabilities = "cap_net_raw+p";
163 source = "${captive-browser-configured}/bin/captive-browser";
164 };
165 };
166}