网络应用与服务
客户端必须与它们相应的网络服务器连接起来才能正常工作。Unix服务器有很多种形式。服务器程序直接或间接地监听端口。另外,服务器功能各异,但没有通用的配置数据库。大多数服务器通过配置文件(尽管格式不统一)来定义自身的行为,并使用操作系统的syslog服务来记录日志。接下来我们会介绍一些常见的服务器以及用于调试的工具。
服务的基本概念
TCP服务是最好理解的概念之一,因为它建立在简单、无中断的双路数据流之上。或许最佳的解释方式,是让你直接通过TCP与某个Web服务器的80端口沟通,去看看数据是如何在该连接上移动的。例如,用以下命令连接一个Web服务器:
$ telnet www.wikipedia.org 80
你会得到类似这样的输出:
Trying some address...
Connected to www.wikipedia.org.
Escape character is '^]'.
现在输入:
GET / HTTP/1.0
这个测试告诉我们:
- 远程主机那里有个Web服务器进程监听着TCP端口80;
- telnet是初始化这个连接的客户端。
输入剖析
$ curl --trace-ascii trace_file http://www.wikipedia.org/
你会得到大量的HTML输出。忽略它们(或将它们重定向到/dev/null),并去看看刚刚创建出来的文件trace_file。假设连接成功,那么文件的开头部分看起来应该是下面这样的,表明刚开始时curl尝试跟该服务器建立TCP连接:
== Info: About to connect() to www.wikipedia.org port 80 (#0)
== Info: Trying 10.80.154.224... == Info: connected
至此你看到的一切都是发生在传输层或其下层的。然而,如果连接成功了,那么curl就会尝试发送请求(即“报头”);这里就开始有应用层了。
=> Send header, 167 bytes (0xa7)
0000: GET / HTTP/1.1
0010: User-Agent: curl/7.22.0 (i686-pc-linux-gnu) libcurl/7.22.0 OpenS
0050: SL/1.0.1 zlib/1.2.3.4 libidn/1.23 librtmp/2.3
007f: Host: www.wikipedia.org
0098: Accept: */*
00a5:
上例第一行是curl的调试信息输出,告诉了你它接下来要做的事情。剩下的行展示了curl发送给服务器的信息。开头的十六进制数只是一个调试偏移量,告诉你收发的数据有多少。
你可以看到curl先发了一个GET命令给服务器(就像用telnet那样),接着是一些有关服务器的额外信息以及一个空行。然后服务器回应了,首先是它的报头(粗体部分)。
<= Recv header, 17 bytes (0x11)
0000: HTTP/1.1 200 OK
<= Recv header, 16 bytes (0x10)
0000: Server: Apache
<= Recv header, 42 bytes (0x2a)
0000: X-Powered-By: PHP/5.3.10-1ubuntu3.9+wmf1
--snip--
跟之前那段输出很像,<=开头的行是调试信息,0000:是偏移量
服务器回应的报头可以很长,然后,某行的报头出现了我们请求的内容,就像下面这样:
<= Recv header, 55 bytes (0x37)
0000: X-Cache: cp1055 hit (16), cp1054 frontend hit (22384)
<= Recv header, 2 bytes (0x2)
0000:
<= Recv data, 877 bytes (0x36d)
0000: 008000
0008: <!DOCTYPE html>.<html lang="mul" dir="ltr">.<head>.<!-- Sysops:
--snip--
尽管调试信息里有Recv header和Recv data,意味着服务器返回的信息可分为两种,但curl向操作系统获取这两种信息的方法却没有不同,操作系统对它们的处理方式也没有不同,甚至底层网络对它们的数据包的处理方式也是一样的。如果说有不同,那完全是在用户空间的curl内部。curl读取报头的时候,如果发现了标志HTTP报头
结束的空行(即中间的两个字节),它就知道接下来将是真正的应答内容了。
在服务器发送数据方面也同样如此。服务器并不区分发给操作系统的报头和内容,区分只发生于用户空间的服务器程序。
网络服务器
大多数网络服务器跟cron之类的服务器守护进程很像,只不过网络服务器是与网络端口进行交互的。
以下是一些常见的网络服务器,在你系统中可能也会见到
- httpd、apache、apache2:Web服务器。
- sshd:Secure shell守护进程。
- postfix、qmail、sendmail:邮件服务器。
- cupsd:打印服务器。
- nfsd、mountd:网络文件系统(文件共享)守护进程。
- smbd、nmbd:文件共享守护进程
- rpcbind:远程程序调用(RPC)端口映射服务守护进程。
大多数网络服务器有一个共同的特性,即它们通常是多进程的。其中至少有一个进程在监听网络端口,而当它接收到一个新的连接时,就会使用fork()来创建一个子进程,负责那个新的连接。该子进程,也叫辅助进程,会随着连接的终止而终止。同时,监听进程会继续接收连接。这样,一个服务器就能轻松地处理多个连接,一般不会有什么问题。
然而,这个模型也会有一些异常情况。调用fork()是会增加系统负担的,而高性能TCP服务器(如Apache Web服务器)能在启动时就创建一定数量的辅助进程,以备连接需要。接受UDP包的服务器只会简单地接收数据并对其做出反应,它们不需要维持连接。
SSH
SSH是最常见的网络服务应用之一,它是一种远程连接Unix机器的标准。配置好之后,我们就能通SSH进行安全的shell登录、执行远程程序、共享简单的文件等。
SSH还凭借公钥认证和简单的会话加密,取代了旧的、不安全的远程登录系统telnet和rlogin。大多数ISP和云提供商都要求以SSH来使用他们的服务,另外很多基于Linux的网络设备(如NAS)也是这样要求的。OpenSSH(http://www.openssh.com/)是一个比较流行的、针对Unix的SSH实现,而且几乎所有的Linux发行版也预装了它。OpenSSH的客户端是ssh,服务器是sshd。SSH协议有两个主要版本:1和2。OpenSSH对两者都支持,但1是很少用的。
SSH的功能和特性使它能做到以下事情。
- 对密码和会话内容加密,保护你不受窃听困扰。
- 作为其他网络连接的管道,包括来自X Window客户端的连接。X会在第14章详细介绍。
- 几乎所有操作系统都可用SSH连接。
- 使用密钥做主机认证。
但SSH也有缺点。其中一个就是,若想建立SSH连接,你必须先知道远程主机的公钥,它是不需要通过什么保密的渠道就能获得的(当然你也可以手动检查它的真假)
SSHD服务器
运行sshd需要一个配置文件以及主机密钥。大多数发行版都将配置文件放在/etc/ssh配置目录中,并在你安装它们的sshd包时,尝试将一切都配置好。(这里的配置文件名sshd_config跟客户端的文件名ssh_config很容易混淆,请注意区分。)
Port 22
#Protocol 2,1
#ListenAddress 0.0.0.0
#ListenAddress ::
HostKey /etc/ssh/ssh_host_key
HostKey /etc/ssh/ssh_host_rsa_key
HostKey /etc/ssh/ssh_host_dsa_key
以#开头的是注释,而sshd_config中的很多的注释都暗示了默认值。sshd_config(5)手册包含了所有可选值的解释,而以下这些是最重要的。
- HostKey file:使用file作为主机密钥。(主机密钥是短的。)
- LogLevel level:按照syslog的级别level来记录信息。
- PermitRootLogin value:value为yes则允许超级用户通过SSH登录,否则不允许。
- SyslogFacility name:按syslog设施的名字name来记录信息。
- X11Forwarding value:若value为yes则允许X Window客户端被SSH管道接驳。
- XAuthLocation path:设置xauth的路径。找不到路径的话,X11的SSH管道将无法工作。如果xauth不在/usr/bin里,则需写明xauth的完整路径。
主机密匙
OpenSSH有三套主机密钥:一套用于版本1的协议,另两套用于版本2。每套都有一个公钥(扩展名为.pub的文件)和一个私钥(无扩展名)。不要让任何人知道你的私钥,否则就有被入侵的危险。
SSH版本1只有RSA密钥,而版本2则有RSA和DSA。RSA和DSA都是公钥加密算法。
文件名 | 密钥类型 |
---|---|
ssh host_rsa_key | RSA私钥(版本2) |
ssh host_rsa_key.pub | RSA公钥(版本2) |
ssh host_dsa_key | DSA私钥(版本2) |
ssh host_dsa_key.pub | DSA公钥(版本2) |
ssh host_key | RSA私钥(版本1) |
ssh host key.pub | RSA公钥(版本1) |
SSH服务器与客户端还用到了一个叫作ssh_known_hosts的密钥文件,里面包含了其他主机的公钥。如果你要使用基于主机的认证,服务器端的ssh_known_hosts必须要包含所有可信客户端的公钥。了解密钥文件有助于你更换机器。当你从头给机器安装系统时,你可以用回旧的机器上的密钥文件,这样用户就能跟旧的密钥配置对上了。
开启SSH服务器
尽管大多数发行版都带有SSH,但默认却不会启动sshd服务器。在Ubuntu和Debian上,安装SSH服务器就会创建密钥、启动服务,并在开机脚本中加上启动SSH。在Fedora上,sshd是预装的,但默认是关闭的。想在开机时启动sshd,可以用chkconfig(但这并不会让服务器马上启动,除非你用service sshd start):
# chkconfig sshd on
Fedora一般会在sshd初次启动时补建漏掉的密钥文件。
如果你还没装好init的支持,就用root来运行sshd,以启动服务器。而一旦启动,sshd就会在/var/run/sshd.pid记下自己的PID。
你还可以在systemd或用inetd以套接字单元来启动sshd,但这不是个好方法,因为服务器偶尔会产生密钥文件,这个进程可能会很费时间。
SSH客户端
想登录远程主机,运行:
ssh remote_username@host
如果你在本机的账号跟远程的一样,可以省略remote_username@。你还可以像下面例子一样用管道符来连接ssh命令,将一个叫dir的目录复制到另一台主机:
tar zcvf - dir | ssh remote_host tar zxvf -
全局的SSH客户端配置文件ssh_config应该在/etc/ssh里,就如sshd_config文件一样。跟服务器配置文件类似,客户端配置文件也有键值对,但你应该不用去改它。
SSH文件传输客户端
OpenSSH包含了文件传输程序:scp和sftp。这两个命令用以取代旧的、不安全的命令rcp和ftp。
你可以用scp在本机和远程主机之间传输文件,它就像cp命令一样。以下是一些例子。
$ scp user@host:file .
$ scp file user@host:dir
$ scp user1@host1:file user2@host2:dir
sftp程序就好比是命令行的ftp客户端,它有get和put命令。远程主机必须装好sftp-server程序(如果远端装了OpenSSH,那通常都会带上这个)。
非Unix平台的SSH客户端
PuTTY是一个不错的基本Windows客户端,它包含了安全的文件复制程序。MacSSH适用于Mac OS 9.x及以下版本。Mac OS X是基于Unix的,也包含OpenSSH
守护进程inetd和xinetd
为每种服务实现独立的服务器好像并不太高效。每个服务器都要分别配置端口监听、访问控制以及端口设置。大多数服务的配置方式都是一样的,只是处理连接的方式不同。
传统的解决方法是使用inetd守护进程,它是一种超级服务器,用于规范网络端口的接入和服务器程序与网络端口之间的接口。启动inetd之后,它会读取自己的配置文件,并监听其中提到的网络端口。当连接到来时,inetd就会新开一个进程来处理它。
xinetd是inetd的新版本,它提供更简单的配置和更优秀的访问控制,但xinetd正被systemd取代,systemd的套接字单元能提供同样的功能。
ident stream tcp nowait root /usr/sbin/sshd sshd -i
其中7个字段从左至右含义分别如下所示。
- 服务名称:来自/etc/services(见9.14.3节)。
- 套接字类型:通常TCP是stream,UDP是dgram。
- 协议:传输协议,通常是tcp或udp。
- 数据报服务器行为:UDP可选wait或nowait。其他传输协议应该用nowait。
- 用户:运行该服务的用户。加上.group的话,可以指定群组。
- 可执行程序:inetd为应付该服务而发起的程序。
- 参数:可执行程序的参数。第一个参数是该程序的名字。
诊断工具
tcpdump
如果你想明确地知道你的网络上有什么在流通,你可用tcpdump将网络接口置于混杂模式,并向你报告每一个通过的数据包。不带参数地运行tcpdump,就包含了ARP请求和Web连接。
你可以加入过滤器,以让tcpdump的内容更精确。过滤器可以是源主机、目标主机、网络、以太网地址、各层协议等等。各种数据包协议中,tcpdump能认出的有:ARP、RARP、ICMP、TCP、UDP、IP、IPv6、AppleTalk、IPX。例如,让tcpdump只输出TCP数据包,可运行:
# tcpdump tcp
想看网页包和UDP包,运行:
# tcpdump udp or port 80
netcat
netcat可以与TCP和UDP端口通信,指定本地端口,监听端口,扫描端口,对网络输入输出重定向到标准输入输出等等。
用netcat连接TCP端口
$ netcat host port
netcat只在对方关闭连接时终止,所以如果你用netcat接收标准输入,就会有点麻烦。你可以使用CTRL-C来随时关闭连接。(如果你希望由标准输入流来决定程序和网络连接的关闭,可以改用sock程序。)
扫描端口
有时你可能想知道你网络中的机器正提供着什么服务,或哪个IP正在被使用,这时你可以用网络映射器(Network Mapper,以下简称Nmap)程序来扫描并列举出一台机器(或一个网络中的机器)的开放端口。
在列举你机器的端口时,至少从这样两个不同的角度来运行Nmap程序会更好:从本机和从另一台机器(可能是本地网络之外的)。这样做可以看到你防火墙屏蔽了什么。
运行nmap host来对一个主机进行端口扫描,例如:
$ nmap 10.1.2.2
Starting Nmap 5.21 ( http://nmap.org ) at 2015-09-21 16:51 PST
Nmap scan report for 10.1.2.2
Host is up (0.00027s latency).
Not shown: 993 closed ports
PORT STATE SERVICE
22/tcp open ssh
25/tcp open smtp
80/tcp open http
111/tcp open rpcbind
8800/tcp open unknown
9000/tcp open cslistener
9090/tcp open zeus-admin
Nmap done: 1 IP address (1 host up) scanned in 0.12 seconds
远程程度调用
RPC意指远程程序调用(Remote Procedure Call),是应用层中较低层的一个系统。它是为了方便程序员访问网络应用而设计的,能使本地程序调用远程程序(按程序号来标识),让远程程序返回结果码或信息。
RPC的实现需要用到传输层协议,如TCP和UDP。它需要一种特别的中介服务来将程序号与TCP和UDP的端口号对应起来。这种服务就是rpcbind,运行RPC服务就需要用到它。
$ rpcinfo -p localhost
RPC是一种难以消亡的协议。网络文件系统(Network File System,以下简称NFS)和网络信息服务(Network Information Service,以下简称NIS)就用得到它,但单机环境下是不需要这些服务的。当你以为你已经对rpcbind不会再有任何需要时,就会有些依赖它的东西出现,例如GNOME的文件访问监控(File Access Monitor,以下简称FAM)。
网络安全
因为Linux是PC平台上风行的Unix系统,尤其是它被广泛用作网页服务器,所以它也招致了很多黑客攻击
网络安全带来了两个极端:真心想攻破系统的人(不管是为了钱还是为了好玩)和精心设计防御方案的人(这个也很有利可图)。幸好,你不需要了解太多东西来维持系统的安全。这里有一些经验法则。
- 打开的服务越少越好:黑客不能攻击你机器上不存在的服务。如果你知道有哪个服务是你不需要的,那就别打开它,等你真正用到那个服务时再打开。
- 防火墙屏蔽得越多越好:Unix系统有一些内部服务是你可能不知道的(例如用于RPC端映射的TCP端口111),因此也别让其他机器知道它们的存在。跟踪和规范本机的服务是很困难的,因为监听的程序和被监听的端口错综复杂。为避免黑客发现你系统上的内部服务,请使用有效的防火墙规则,并为路由器安装防火墙。
- 记录你提供给互联网的服务:如果你运行着SSH服务器、Postfix或类似的东西,请保持你软件的更新,以及使用合适的安全警报
- 让服务器使用“长期支持”的系统版本:安全团队一般在稳定、有支持的系统版本上才能集中精力工作。开发版或测试版如Debian Unstable和Fedora Rawhide不太受他们关注。
- 账号不要发给不用的人:通过本地账号获取超级用户的权限,比远程入侵要简单得多。事实上,大多数系统上的软件都有这样那样的漏洞,只要你能登录shell,就有可能获取超级用户的权力。不要指望你的朋友懂得如何保护密码(或懂得使用复杂的密码)。
- 避免安装可疑的二进制包:它们可能包含木马
有以下三种基本的网络攻击。
- 全面威胁:意思是获取了超级用户的权限(对一台机器完全掌控)。黑客可以通过服务攻击,例如缓存溢出、接管一个没什么保护措施的账号或利用一个写得不太好的setuid,来做到“全面威胁”。
- 拒绝服务(Denial-of-service,简称DoS)攻击:这使得机器无法继续提供服务,或无需通过登录就用某些方法造成机器故障。这种攻击更难防范,但很容易应对。
- 恶意软件:Linux用户大多对恶意软件(如电子邮件蠕虫和病毒)免疫,只因为他们的电子邮件客户端不会蠢到运行附件里的程序。但Linux恶意程序确实存在。请勿从陌生的地方下载并安装二进制软件。
典型漏洞
直接攻击和嗅探明文密码
直接攻击:就是摆明要攻陷一台机器。最常见的做法是利用缓存溢出。该漏洞是因为粗心的程序员没检查数组边界而造成的。攻击者在一大堆数据中编造一个栈帧,然后送到远程服务器,并期望它溢出而覆盖了程序,最终导致服务器执行了栈帧中的东西。尽管这不太容易,但却很容易复制。
嗅探明文密码:是指获取网络上明文传输的密码。一旦攻击者拿到你的密码,那你就玩完了。从那开始,攻击者就会直接登入,尝试获取超级用户的权限(这比远程攻击要简单),或以该机器攻击其他机器,或用其他机器攻击该机器
有些服务因为设计缺陷而经常成为攻击目标。例如以下这些服务:
- ftpd:不管什么原因,所有的FTP服务器都存在漏洞。除此之外,大多数FTP服务器都用明文密码。如果你要在机器之间移动文件,考虑下基于SSH的方案或rsync服务器。
- telnetd、rlogind、rexecd:它们都把会话内容(包括密码)用明文传输。不要使用它们,除非你有Kerberos版本。
- fingerd:黑客可以通过finger服务获取用户列表和其他信息。
安全资源
安全网站:
- http://www.sans.org/:提供培训、服务、免费的高危漏洞周报、安全策略样板等等。
- http://www.cert.org/:这里介绍最危险的漏洞。
- http://www.insecure.org/:这里有Nmap和各种网络开发测试工具,比其他网站做得更详尽、更开放。
套接字:进程与网络的通信方式
在Unix上,进程是用套接字来标识与网络通信的时机与方式的。套接字是进程通过内核访问网络的接口,它代表用户空间与内核空间的边界。它也常被用于进程间通信(Interprocess Communication,以下简称IPC)。
因为进程需要以不同的方式访问网络,所以套接字也分不同种类。例如,TCP连接会用流式套接字(程序员称之为SOCK_STREAM),而UDP连接则用数据报套接字(SOCK_DGRAM)。
建立网络套接字有点复杂,因为你有时需要知道套接字类型、IP地址、端口、传输协议。然而,当所有初始的细节整理好后,服务器就能用相应的标准模型来处理网络通信了。
监听套接字和读写套接字。主进程用监听套接字在网络中寻找连接。当一个新的连接到来,主进程就用accept()系统调用来接收该连接,它能为连接创建专用的读写套接字。接着主进程用fork()创建一个新的子进程来处理该连接。最终,监听套接字继续监听,为主进程带来更多连接。
Unix域套接字
进程间的通信可以使用本地主机(127.0.0.1)来做常规的网络通信,但我们一般使用另一种特别的套接字,进程与Unix域套接字的连接几乎与网络套接字连接一样:进程可以监听和接收套接字上的连接,还可以选择不同类型的套接字来实现TCP式或UDP式的工作方式。
对开发者的好处
- 开发者可以通过管理套接字文件的访问权限来管理Unix域套接字的访问权限。也就是说,不能访问某个套接字文件的进程,也就不能使用该套接字。
- 因为Linux内核与Unix域套接字通信并不需要经历网络层次,所以性能会比网络套接字好。
为Unix域套接字写代码跟支持一般的网络套接字没多大不同。因为它的好处十分明显,所以有些网络服务器会同时提供网络套接字和Unix域套接字的连接。例如,MySQL的数据库服务器mysqld能接受远端的连接,同时也以/var/run/mysqld/mysqld.sock提供Unix域套接字。
列出Unix域套接字
你可以使用lsof -U
来查看系统正在使用中的Unix域套接字:
# lsof -U
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
mysqld 19701 mysql 12u unix 0xe4defcc0 0t0 35201227 /var/run/mysqld/mysqld.sock
chromium- 26534 juser 5u unix 0xeeac9b00 0t0 42445141 socket
tlsmgr 30480 postfix 5u unix 0xc3384240 0t0 17009106 socket
tlsmgr 30480 postfix 6u unix 0xe20161c0 0t0 10965 private/tlsmgr
--snip--