为拨号 PPP 服务添加功能
我运营着一家小型网络托管服务公司,专门为艺术界和小型本地企业提供服务。按照惯例,我为所有客户提供各种支持服务,包括直接拨号连接到我的服务器,并使用点对点协议 (PPP) 连接到互联网的能力。所有服务,包括 PPP 访问,均由一台历史悠久且极其可靠的 486 计算机提供,该计算机运行着最新生产版本的 Linux 内核。
在互联网早期,我曾花费数年时间帮助建立奥斯汀最早的互联网服务提供商之一,因此我熟悉使用 UNIX 寻找未解决问题的原始解决方案的过程。那时,没有端口管理器,拨号访问由 UNIX 机器、Digiboards 和巧妙的编程提供。今天常见的许多解决方案和工具在当时是未知的或仅处于实验阶段。
现代版本的 Linux 提供了比早期非 Linux 内核更多的功能;然而,当我开发自己的拨号访问时,我发现虽然我的拨号系统的所有部分都可用,但对我来说很重要的几个功能在已发布的软件包中不可用。特别是,我想提供完整的互联网访问作为订阅服务。选择不订阅此服务的客户仍然可以通过 PPP 拨号完全访问他们本地网站上的文件空间,使用 FTP 或 http。我还需要一种方法来根据不活动状态实施会话超时。虽然我的所有客户在对待此类事项的责任感方面都高于平均水平,但每个人有时都会注意力不集中,在线会话后忘记注销和挂断。
我发现容易获得的软件是由 Gert Doering 编写的优秀 mgetty+sendfax 软件包和 Debian/GNU 发行版的 pppd。mgetty 管理与调制解调器的通信并提供必要的登录功能,而 pppd 管理 PPP 协议问题。最新版本的 mgetty(我使用的是 mgetty-0.99.2)能够检测传入的 PPP 协商尝试,并且如果配置正确,将使用各种选项调用 pppd,并将用户身份验证的责任传递给它。
虽然这种组合非常灵活,但它不是一个完全集成的解决方案。pppd 从用户 .ppprc 文件中读取并获取设置指令,我的第一个想法是为每个客户设置一个只读的、root 拥有的 .ppprc 文件,这将为我提供我需要的灵活性,以便在每个帐户的基础上提供互联网访问权限和限制。不幸的是,情况并非如此简单。由于 mgetty 以 root 身份运行并调用 pppd,因此 pppd 读取的唯一 .ppprc 文件是 ~root/.ppprc。用户识别和身份验证发生在读取 ~/.ppprc 文件之后——这对我的目的来说不是很有用。
幸运的是,pppd 以两种内置脚本调用的形式提供了一个非常好的开放式钩子:ip-up(存档文件中的列表 1),它在 PPP 的网络控制协议 (IPCP) 启动后立即执行;以及 ip-down(存档文件中的列表 2),它在链接断开后立即执行。这两个脚本都提供接口名称、tty 设备、速度、本地 IP 地址和远程 IP 地址作为命令参数,并且从中几乎可以发现关于 PPP 会话可能需要知道的一切。ip-up 和 ip-down 都以 root 的真实和有效用户 ID 运行,消除了用户拥有的进程执行系统命令的任何潜在问题。因为我可以从单个脚本中完成每个用户的配置,所以我可以将所有用户信息本地化在一个数据文件中。
为了向 pppd 拨号客户端授予完整的互联网访问权限,pppd 调用了一种称为代理 ARP 的技术。使用代理 ARP 的主机通告与其自身以太网接口地址链接的拨号客户端的 IP 地址。因此,发往拨号客户端的 IP 流量被发送到主机,主机忠实地通过 PPP 接口转发它。pppd 可以在调用时配置为设置代理 ARP,可以在命令行或其多个配置文件中的任何一个中设置,包括用户 .ppprc 文件。也可以使用 arp 命令“手动”设置代理 ARP,以操作内核的 ARP 缓存。由于 pppd 配置文件都不能用于区分用户,我选择在 ip-up Perl 脚本中使用 arp 的 shell 调用来设置代理 ARP。
使用 arp 命令设置代理 ARP 需要两条信息:分配给拨号客户端的 IP 地址和数据包应传递到的以太网接口的机器地址。拨号 IP 地址作为参数传递给 ip-up。适当以太网接口的硬件地址可以从多个来源获得,但最简单的方法是解析从 ifconfig 返回的信息。此地址也可以硬编码到脚本中,因为它在短期内不太可能更改。
为了让 ip-up 知道是否设置代理 ARP,它必须知道为其调用的用户的身份。虽然当前用户的身份不是 pppd 提供给 ip-up 的项目之一,但连接的 tty 设备名称是可用的,并且与系统 utmp 文件中的用户名相关联。调用 who 提供了一个方便格式化的表格,可以对其进行解析以获取当前连接到任何 tty 设备的用户的名称。我使用用户名作为索引到一个小的平面文件 /etc/ppp/proxyarp 中,该文件由一系列制表符分隔的数据对行组成,每对由用户名和“+”或“-”组成,指示是否为该特定用户设置代理 ARP。有了这些信息,ip-up 就拥有了为会话设置代理 ARP 并确定是否适合这样做所需的一切。
在这种方案中必须解决的一个“陷阱”是 LAN 网关的 ARP 缓存。我使用的 Cisco 750 系列路由器不愿提供有关或操作其内部 ARP 缓存的任何信息,并且默认超时(大约五分钟)意味着在前一次呼叫后的此超时期间内建立的任何连接都将继承前一次连接的数据包路由。如果我的非互联网用户偶尔获得完整的互联网访问权限,对我来说这不是一个严重的问题,但繁忙的 ISP 需要能够在此事上进行更严格的控制。
在 Linux 下监控会话时间和网络活动相对容易。Linux 内核在伪文件 /proc/net/dev 中提供了关于每个接口的包流量的所有必要信息,其布局格式既易于阅读又易于解析(参见列表 4)。不活动超时可以由接收的数据包数、发送的数据包数或它们的组合触发。
我的超时机制使用 ip-up 和 ip-down 脚本以及第三个 Perl 脚本 timeout.pl(存档文件中的列表 3),它每五分钟从 root crontab 文件运行一次。ip-up 创建一个会话文件 /var/run/pppn.session(其中 pppn 中的 n 指定适当的接口)。此文件包含六个字段
拥有会话的帐户的用户名(用于日志记录和通知)
ppid 进程的进程 ID
会话开始时间
上次在接口上观察到活动的时间
接口上接收到的数据包总数
发送到接口的数据包总数
timeout.pl 每次运行时都会读取每个 PPP 接口的会话文件。如果总会话时间尚未超过,它会检查接口上的流量。如果它观察到活动,它会记录时间和流量统计信息并重写会话文件。如果自上次检查以来没有发生流量,则脚本会检查自上次观察到流量以来的时间,如果未超过不活动超时,则退出。如果超过了任何一个超时时间,脚本会向 pppd 进程发送 SIGINT 信号,导致它执行有序的挂断,其中包括执行 ip-down 脚本。ip-down 删除会话文件和 ARP 缓存中当前接口的任何代理 ARP 条目。
除了上面提到的不情愿的路由器 ARP 缓存外,该系统在所有方面都运行良好。我在 timeout.pl 中包含了可选的电子邮件通知,因此每当发生超时时,它都会向我发送电子邮件。我也可以通过手动执行带有 -t 或 -i 选项的 timeout.pl 来强制超时。将超时事件的系统日志记录添加到我的“待办事项”列表中,但这应该是一个相对简单的问题。
