1# This is a distributed test of the Squid as a forward proxy
2# - "external" -- i.e. the internet, where the proxy and server communicate
3# - "internal" -- i.e. an office LAN, where the client and proxy communicat
4
5{
6 pkgs,
7 lib,
8 ...
9}:
10# VLANS:
11# 1 -- simulates the internal network
12# 2 -- simulates the external network
13let
14 commonConfig = {
15 # Disable eth0 autoconfiguration
16 networking.useDHCP = false;
17
18 environment.systemPackages = [
19 (pkgs.writeScriptBin "check-connection" ''
20 #!/usr/bin/env bash
21
22 set -e
23
24 if [[ "$2" == "" || "$1" == "--help" || "$1" == "-h" ]];
25 then
26 echo "check-connection <target-address> <[expect-success|expect-failure]>"
27 exit 1
28 fi
29
30 ADDRESS="$1"
31
32 function test_icmp() { timeout 3 ping -c 1 "$ADDRESS"; }
33
34 if [[ "$2" == "expect-success" ]];
35 then
36 test_icmp
37 else
38 ! test_icmp
39 fi
40 '')
41 ];
42 };
43in
44{
45 name = "squid";
46 meta.maintainers = with lib.maintainers; [ cobalt ];
47
48 node.pkgsReadOnly = false;
49
50 nodes = {
51 client =
52 { ... }:
53 lib.mkMerge [
54 commonConfig
55 {
56 virtualisation.vlans = [ 1 ];
57 networking.firewall.enable = true;
58
59 # NOTE: the client doesn't need a HTTP server, this is here to allow a validation of the proxy acl
60 networking.firewall.allowedTCPPorts = [ 80 ];
61
62 services.nginx = {
63 enable = true;
64
65 virtualHosts."server" = {
66 root = "/etc";
67 locations."/".index = "hostname";
68 listen = [
69 {
70 addr = "0.0.0.0";
71 port = 80;
72 }
73 ];
74 };
75 };
76 }
77 ];
78
79 proxy =
80 { config, nodes, ... }:
81 let
82 clientIp = (pkgs.lib.head nodes.client.networking.interfaces.eth1.ipv4.addresses).address;
83 serverIp = (pkgs.lib.head nodes.server.networking.interfaces.eth1.ipv4.addresses).address;
84 in
85 lib.mkMerge [
86 commonConfig
87 {
88 nixpkgs.config.permittedInsecurePackages = [ "squid-7.0.1" ];
89
90 virtualisation.vlans = [
91 1
92 2
93 ];
94 networking.firewall.enable = true;
95 networking.firewall.allowedTCPPorts = [ config.services.squid.proxyPort ];
96
97 services.squid = {
98 enable = true;
99
100 extraConfig = ''
101 acl client src ${clientIp}
102 acl server dst ${serverIp}
103 http_access allow client server
104 http_access deny all
105 '';
106 };
107 }
108 ];
109
110 server =
111 { ... }:
112 lib.mkMerge [
113 commonConfig
114 {
115 virtualisation.vlans = [ 2 ];
116 networking.firewall.enable = true;
117 networking.firewall.allowedTCPPorts = [ 80 ];
118
119 services.nginx = {
120 enable = true;
121
122 virtualHosts."server" = {
123 root = "/etc";
124 locations."/".index = "hostname";
125 listen = [
126 {
127 addr = "0.0.0.0";
128 port = 80;
129 }
130 ];
131 };
132 };
133 }
134 ];
135 };
136
137 testScript =
138 { nodes, ... }:
139 let
140 clientIp = (pkgs.lib.head nodes.client.networking.interfaces.eth1.ipv4.addresses).address;
141 serverIp = (pkgs.lib.head nodes.server.networking.interfaces.eth1.ipv4.addresses).address;
142 proxyExternalIp = (pkgs.lib.head nodes.proxy.networking.interfaces.eth2.ipv4.addresses).address;
143 proxyInternalIp = (pkgs.lib.head nodes.proxy.networking.interfaces.eth1.ipv4.addresses).address;
144 in
145 ''
146 client.start()
147 proxy.start()
148 server.start()
149
150 proxy.wait_for_unit("network.target")
151 proxy.wait_for_unit("squid.service")
152 client.wait_for_unit("network.target")
153 server.wait_for_unit("network.target")
154 server.wait_for_unit("nginx.service")
155
156 # Topology checks.
157 with subtest("proxy connectivity"):
158 ## The proxy should have direct access to the server and client
159 proxy.succeed("check-connection ${serverIp} expect-success")
160 proxy.succeed("check-connection ${clientIp} expect-success")
161
162 with subtest("server connectivity"):
163 ## The server should have direct access to the proxy
164 server.succeed("check-connection ${proxyExternalIp} expect-success")
165 ## ... and not have access to the client
166 server.succeed("check-connection ${clientIp} expect-failure")
167
168 with subtest("client connectivity"):
169 # The client should be also able to connect to the proxy
170 client.succeed("check-connection ${proxyInternalIp} expect-success")
171 # but not the client to the server
172 client.succeed("check-connection ${serverIp} expect-failure")
173
174 with subtest("HTTP"):
175 # the client cannot reach the server directly over HTTP
176 client.fail('[[ `timeout 3 curl --fail-with-body http://${serverIp}` ]]')
177 # ... but can with the proxy
178 client.succeed('[[ `timeout 3 curl --fail-with-body --proxy http://${proxyInternalIp}:3128 http://${serverIp}` == "server" ]]')
179 # and cannot from the server (with a 4xx error code) and ...
180 server.fail('[[ `timeout 3 curl --fail-with-body --proxy http://${proxyExternalIp}:3128 http://${clientIp}` == "client" ]]')
181 # .. not the client hostname
182 server.fail('[[ `timeout 3 curl --proxy http://${proxyExternalIp}:3128 http://${clientIp}` == "client" ]]')
183 # with an explicit deny message (no --fail because we want to parse the returned message)
184 server.succeed('[[ `timeout 3 curl --proxy http://${proxyExternalIp}:3128 http://${clientIp}` == *"ERR_ACCESS_DENIED"* ]]')
185 '';
186}