使用 Squid 代理的灵活访问控制

作者:Mike Diehl

大型企业和核实验室并不是唯一需要互联网访问策略和执行手段的组织。我的家庭也有互联网访问策略,而我用来执行该策略的技术几乎适用于任何组织。就我们家而言,我不太担心外部安全威胁。我们的网络位于 NAT 路由器之后,并且我们的 Wi-Fi 密码非常复杂。我们的工作站要么是 Linux 系统,要么是正确修补的 Windows 机器(如果存在这种东西)。不,我们担心的是来自我们网络内部的问题:我们的孩子喜欢玩基于 Web 的游戏,这经常妨碍家务和作业。

我们还担心他们可能会偶然发现我们不希望他们访问的 Web 内容。因此,我们不是在保护核机密或知识产权,而是在使家庭能够顺利运行而不会受到不必要的干扰。

一般来说,我和我的妻子并不介意我们的孩子在线玩游戏或流媒体。但是,如果他们的作业或家务没有完成,我们希望有一种方法可以“禁止”他们访问这些内容。问题是我们也进行家庭教育,而且他们的大部分教育内容也在网上。因此,我们不能简单地阻止他们的访问。我们需要更灵活的东西。

当我着手解决这个问题时,我列出了我想实现的目标

  1. 我不希望管理我孩子的互联网访问成为一份全职工作。我希望能够制定策略并使其得到实施。

  2. 我的妻子不想知道如何登录、修改配置文件和重启代理守护程序。她需要能够指向她的浏览器,选中几个复选框,然后继续她的生活。

  3. 我不想编写太多代码。我愿意编写少量代码,但如果已经存在,我对重新发明轮子不感兴趣。

  4. 我希望能够执行几乎任何对我们家庭有意义的策略。

  5. 我不希望我做的任何事情在他们将笔记本电脑带出家门时破坏他们的互联网访问。

我相信我的家庭并不是唯一对这些结果感兴趣的组织。但是,我做了一个假设,这个假设在其他组织中可能没有意义:我的孩子不会采取任何复杂的措施来规避我们的政策。但是,如果他们这样做,我保留参与军备竞赛的权利。

为了本文的目的,任何时候这个假设导致在更复杂的环境中可能没有意义的配置时,我都会尝试讨论一些可以加强您的配置的选项。

我找不到任何一个足够灵活以满足我的需求,并且足够易于使用的软件包,以至于我的妻子和我不需要花费大量精力来使用它。我能够看到 Squid 代理服务器有可能通过我编写少量代码来完成我想要做的事情。我的代码将告诉代理服务器如何处理每个传入的请求。代理将完成用户的请求,或者向用户发送一个网页,指示用户尝试访问的站点已被阻止。这就是代理将如何实施我们选择的任何策略。

我已经决定,我希望能够为我的家人提供四种级别的互联网访问权限。在两个极端,具有“开放”访问权限的家庭成员几乎可以去他们想去的任何地方,而具有“阻止”访问权限的家庭成员则无法访问互联网上的任何地方。例如,我的妻子和我将拥有开放访问权限。如果其中一个男孩被禁止上网,我们将简单地将他设置为阻止访问。

但是,如果能够允许我们的孩子只访问预先确定的站点列表,例如用于教育目的,那可能会很好。在这种情况下,我们需要一个“仅白名单”访问级别。最后,我计划使用“过滤”访问级别,在这种级别下,我们可以更精细地阻止音乐下载、Flash 游戏和 Java applet 等内容。这是男孩们通常会拥有的访问级别。然后我们可以说“不再玩游戏”,并让代理执行该策略。

因为我不想为所有这些编写实际的界面,所以我只是使用 phpMyAdmin 来更新数据库并设置策略(图 1)。为了授予特定的访问级别,我只需更新网格中的相应单元格,1 表示开启,0 表示关闭。

图 1. 用于更改访问策略的 phpMyAdmin 界面

策略执行还需要一些客户端配置,我稍后会讨论。但是,我还将讨论使用 OpenDNS 作为过滤掉我宁愿不花时间测试和过滤的东西的一种手段。这是一个纵深防御姿态的很好的例子。

我已经配置 OpenDNS 来过滤掉我预计永远不会改变主意的内容。我认为我的家人没有任何理由能够访问约会网站、赌博网站或色情网站(图 2)。虽然不完美,但 OpenDNS 的人们在过滤这些内容方面做得相当不错,而无需我自己进行任何测试。当这种测试失败时,可能会出现一些非常尴尬的时刻——我只想避免。

图 2. OpenDNS 过滤掉容易处理的内容。

在本文的前面,我提到这将需要一些客户端配置。大多数 Web 浏览器都允许您将它们配置为使用代理服务器来访问互联网。最简单的方法是简单地通过选中复选框来启用代理访问。但是,如果我的孩子将他们的笔记本电脑带到图书馆,在那里我们的代理不可用,他们将无法访问互联网,这违反了目标五。因此,我选择使用大多数现代浏览器都支持的自动代理配置。这需要我编写一个 JavaScript 函数,该函数确定如何直接或通过代理访问网站(列表 1)。

列表 1. 自动代理配置脚本

 1  function FindProxyForURL(url, host) {
 2
 3      if (!isResolvable("proxy.example.com") {
 4              return "DIRECT";
 5      }
 6
 7      if (shExpMatch(host, "*.example.com")) {
 8              return "DIRECT";
 9      }
10
11      if (isInNet(host, "10.0.0.0", "255.0.0.0")) {
12              return "DIRECT";
13      }
14
15      return "PROXY 10.1.1.158:3128; DIRECT";
16  }

每次您的浏览器访问网站时,它都会调用 FindProxyForURL() 函数来查看它应该使用哪种方法来访问该站点:直接访问还是通过代理访问。列表 1 中显示的函数只是一个示例,但它演示了一些值得一提的用例。正如您从第 15 行看到的,您可以返回要使用的方法的分号分隔列表。您的浏览器将依次尝试它们。在本例中,如果代理碰巧不可访问,您将回退到对相关网站的 DIRECT 访问。在更严格的环境中,这可能不是正确的策略。

在第 11 行,您可以看到我正在确保直接访问我们本地网络上的网站。在第 7 行,我演示了如何测试特定的主机名。有一些网站我通过工作站上的 VPN 隧道访问,所以我不能使用代理。最后,在第 3 行,您会看到一些有趣的东西。在这里,我正在测试特定的主机名是否可以解析为 IP 地址。我已经配置我们局域网的 DNS 服务器来解析该名称,但没有其他 DNS 服务器能够做到。这样,当我们的孩子将他们的笔记本电脑带出我们的网络时,他们的浏览器不会尝试使用我们的代理。当然,我们可以像我们在第 15 行所做的那样简单地故障转移到直接访问,但故障转移需要时间。

自动代理配置是更高级的用户可以规避的东西。各种浏览器都有一些插件可以阻止用户更改此配置。但是,这并不能阻止用户安装新的浏览器或启动新的 Firefox 配置文件。强制执行此策略的万无一失的方法是在网关路由器上:只需设置防火墙规则,阻止来自除代理之外的任何 IP 地址的 Web 访问。如果需要,甚至可以为特定的客户端-主机组合执行此操作。

当您向网关路由器添加防火墙规则时,您可能会想配置路由器以将所有 Web 流量转发到代理,从而形成通常称为透明代理的东西。但是,根据 RFC 3143,这不是推荐的配置,因为它经常会破坏浏览器缓存和历史记录等功能。

现在我已经讨论了客户端、DNS 和可能的路由器配置,现在是时候看看 Squid 代理服务器配置了。安装本身非常简单。我只是使用了我的发行版的软件包管理系统,所以在这里我不讨论它。Squid 代理提供了许多旋钮,您可以转动它们来优化其缓存和您的互联网连接。即使性能提升是实施代理服务器带来的一个不错的辅助好处,但这些配置选项超出了本文的讨论范围。这就剩下一个必要的配置更改,以便将我的代码插入到系统中。所有需要做的就是编辑 /etc/squid/squid.conf 文件并添加一行


redirect_program /etc/squid/redirector.pl

这个指令本质上告诉 Squid 代理“询问”我的程序如何处理客户端发出的每个请求。程序逻辑非常简单

  1. 监听来自 STDIN 的请求。

  2. 解析请求。

  3. 根据策略做出决定。

  4. 将答案返回给代理。

让我们看看列表 2 中的示例代码。

列表 2. 代理重定向器

 1  #!/usr/bin/perl
 2
 3  use DBI;
 4
 5  $blocked = "http://192.168.1.10/blocked.html";
 6
 7  my $dbh = DBI->connect("dbi:mysql:authentication:host=
↪192.168.1.10", "user", "password") || die("Can\'t 
 ↪connect to database.\n");
 8
 9  $|=1;
10
11  while (<STDIN>) {
12          my($sth, $r, $c);
13          my($url, $client, $d, $method, $proxy_ip, $proxy_port);
14
15          chomp($r = $_);
16
17          if ($r !~ m/\S+/) { next; }
18
19          ($url, $client, $d, $method, $proxy_ip, $proxy_port) 
             ↪= split(/\s/, $r);
20
21          $client =~ s/\/-//;
22          $proxy_ip =~ s/myip=//;
23          $proxy_port =~ s/myport=//;
24
25          $sth = $dbh->prepare("select * from web_clients 
             ↪where ip=\'$client\'");
26          $sth->execute();
27          $c = $sth->fetchrow_hashref();
28
29          if ($c->{blocked} eq "1") {
30                  send_answer($blocked);
31                  next;
32          }
33
34          if ($c->{whitelist_only} eq "1") {
35                  if (!is_on_list("dom_whitelist", $url)) {
36                          send_answer($blocked);
37                          next;
38                  }
39          }
40
41          if ($c->{filtered} eq "1") {
42                  if ($c->{games} eq "0") {
43                          # Check URL to see if it's 
                             ↪on our games list
44                  }
45
46                  if ($c->{flash} eq "0") {
47                          # Check URL to see if it looks 
                              ↪like flash
48                  }
49
50                  send_answer($url);
51                  next;
52          }
53
54          if ($c->{open} eq "1") {
55                  send_answer($url);
56                  next;
57          }
58
59          send_answer($url);
60          next;
61  }
62
63  exit 0;
64
65  #############################################################
66
67  sub     send_answer {
68          my($a) = @_;
69          print "$a\n";
70  }
71
72  sub     is_on_list {
73          my($list, $url) = @_;
74          my($o, @a, $i, @b, $b, $sth, $c);
75
76          $url =~ s/^https*:\/\///;
77          $url =~ s/^.+\@//;
78          $url =~ s/[:\/].*//;
79
80          @a = reverse(split(/\./, $url));
81
82          foreach $i (0 .. $#a) {
83                  push(@b, $a[$i]);
84                  $b = join(".", reverse(@b));
85
86                  $sth = $dbh->prepare("select count(*) from 
                     ↪$list where name=\'$b\'");
87                  $sth->execute();
88                  ($c) = $sth->fetchrow_array();
89
90                  if ($c > 0) { return $c; }
91          }
92
93          return $c+0;
94  }
95

主循环从第 11 行开始,它从 STDIN 读取。第 11-24 行主要关注解析来自 Squid 代理的请求。第 25-28 行是程序查询数据库以查看特定客户端的权限的位置。第 29-57 行检查从数据库中读取的权限并返回相应的权限。在客户端被允许“过滤”访问互联网的情况下,我有一个我心目中逻辑的框架。我不想用琐碎的代码来拖累本文。演示 Squid 代理重定向器的结构和一般逻辑比提供完整的代码更重要。但是您可以看到,我可以在几行代码和正则表达式中实现几乎任何可以想象的访问策略。

从第 67 行开始的 send_answer() 函数目前实际上并没有做太多事情,但在未来,我可以很容易地在这里添加一些日志记录功能。

从第 72 行开始的 is_on_list() 函数可能有点有趣。此函数接受客户端尝试访问的主机名,并将其分解为子域列表。然后,它检查这些子域是否在数据库中列出,数据库的名称作为参数传入。这样,我只需将 example.com 放入数据库,它将匹配 example.com、www.example.com 或 webmail.example.com 等。

通过传入不同的表名,我可以使用相同的匹配算法来匹配任意数量的不同访问控制列表。

正如您所看到的,代码实际上不是很复杂。但是,通过增加一点复杂性,我应该能够执行几乎任何我可以想象的访问策略。但是,有一个领域需要改进。顾名思义,该程序为它处理的每个访问请求多次访问数据库。这是非常低效的,当您阅读本文时,我可能已经实现某种缓存机制。

但是,缓存也会使系统对访问策略或访问控制列表的更改反应迟钝,因为我将不得不等待缓存的信息过期或重启代理守护程序。

在实践中,我看到了一些值得一提的事情。大多数 Web 浏览器都有自己的缓存机制。由于此缓存,如果您在代理处更改访问策略,您的客户端并不总是意识到更改。在您“开放”访问权限的情况下,客户将需要刷新他们的缓存才能访问以前被阻止的内容。在您限制访问权限的情况下,该内容仍然可能可用,直到缓存过期。一种解决方案是将本地缓存大小设置为 0,而只依赖代理服务器的缓存。

此外,一旦客户端配置为与本地网络上的代理通信,就可以在无需客户端执行任何操作的情况下,换入不同的代理甚至菊花链式连接代理。这为使用 Dan's Guardian 等工具进行内容过滤以及访问控制打开了可能性。

到这个时候,你们中的许多人可能会认为我是一个超级严格的控制狂。但是,我的家人在互联网上花费了大量时间——有时到了过分的程度。大多数时候,我的家人以适当的方式使用互联网,但当他们不这样做时,我的妻子和我需要一种执行家庭规则的方法,而无需 постоянно 监视我们的孩子。

加载 Disqus 评论