现在大部分软硬件系统都是基于网络的,有走局域网(私网)的,有走外网(公网)的,会不可避免地出现很多与网络相关的问题,特别是将产品部署到安全级别较高的客户环境中,会出现各式各样的复杂网络问题。今天我们就来分享一下实际项目中遇到的多个网络问题,以供参考!
1、Windows防火墙拦截了客户端发来的TCP连接请求,导致客户端与服务器建链失败
这是一个Windows系统自带的防火墙拦截程序网络数据的例子。客户端和服务器程序运行在两台Windows电脑上,客户端需要连接到远端的服务器上获取数据,但客户端始终连接不上远端的服务器。用wireshark抓包看,**客户端给服务器发送TCP三次握手的SYN包,服务器始终没有回应,导致TCP连接始终无法建立。**
我们先是ping了服务器所在机器的IP地址,是能ping通的。又在服务器所在电脑上使用netstat -a命令查看到服务器的9001端口是出于Listening监听状态的,如下所示:
这就奇怪了,服务器的ip能ping的通的,服务器的端口也处于正常的监听状态,为啥始终没法和服务器建链呢?
后来想到我们在Widnows系统上第一次运行程序时,一般都会弹出类似下面的截图:
一般情况下我们使用默认的选择,没有全部勾选,直接就点击下面的“允许访问”的按钮了。窗口中提示Windows防火墙已经阻止了部分功能,应该是将公网网络和专网网络都勾选上的,估计是Windows防火墙将发给该服务器程序的部分数据包拦截了,于是将服务器程序所在系统的Windows防火墙关闭,然后客户端可以正常连接了。
其实可以在服务器侧抓包,客户端发来的用于三次握手的SYN包,服务器所在机器的网卡应该收到了,只是向应用层传递数据时数据被防火墙拦截了。
最终的解决办法是允许该服务器程序能通过防火墙进行通信,在控制面板中点击**系统和安全->Widnows Defender防火墙->允许应用或功能通过Windows防火墙**,在打开的界面中找到服务器程序:
将专用网络和公用都勾上,点击确定就好了。即允许服务器程序通过防火墙进行通信,防火墙就不会拦截发给服务器的数据包了。
2、在Linux服务器侧抓包选错网卡,导致服务器侧的抓包信息与客户端的对不上
客户端和服务器通信的过程中出现了问题,导致业务出现了异常,于是要在客户端和服务端两侧抓包,对照两边的网络数据包,看看到底是哪一侧出问题了。
客户端运行在Windows系统中,直接启动WireShark就可以直击抓包了。服务器运行在远端的Linux系统上,需要使用SSH工具远程登录到Linux系统中,然后使用tcpdump命令进行抓包,然后再将抓包文件下载到Windows系统中,然后使用WireShark打开查看。
打开服务器的抓包文件后,发现有问题,和客户端抓的数据包对不上,**服务器侧的抓包文件中显示的服务器IP地址,和终端侧抓包文件中显示的服务器IP地址是不一致的**。服务器侧抓到的包中显示的是服务器IP是**内网的IP**,而终端侧抓包显示连接的服务器IP是**外网的IP**,所以两边对不上的,后来想起来可能**输入tcpdump命令时选错了网卡**导致的。
后经平台侧的运维同事确认,Linux服务器上确实有两张物理网卡,在Linux命令行中使用ifconfig命令就可以查看到服务器上的网卡信息,**一个是配置了内网的eth0网卡,一个是配置了外网IP的eth1网卡:**
所以要修改之前输入的tcpdump命令,命令中指定抓eth1网卡的数据包:
tcpdump** -i eth1** -s 0 -w dvsserver.pcap
或者抓所有网卡的数据包:
tcpdump -i any -s 0 -w dvsserver.pcap
3、升级服务器的端口改变了,导致软件无法进行在线升级
某日测试同事在客户端软件上发起在线版本监测,结果始终连不上服务器。使用wireshark抓包看到,软件在发送TCP三次握手的SYN包后,远端服务器直接回了个RST包,强行将客户端的连接请求给终止了。
首先,服务器回包了,那服务器肯定是能ping通的,于是使用telnet命令检测升级服务器的63000服务端口是否正常,结果该端口是连不上的。**一般情况下直接回复RST可能是端口不存在引起的**,经后来和升级服务器开发确认,升级服务器的端口已经变更了,不再是之前的60000端口号了。
其实这个问题中,还有两点是有问题的:
(1)客户端软件侧处理的有问题,不应该将升级服务器的端口在代码中固定为某个数字,应该使用登录时平台返回的升级服务器端口。
(2)平台变更端口后应该发邮件通知客户端侧,平台侧应该对老的客户端提供兼容支持,老的版本已经发布已经交付给客户使用,平台要对老版本做兼容,应该做个端口重定向,老版本使用60000端口发起连接时应该重定向到最新的端口上。
4、Linux服务器系统中开启了reuse和recycle选项,导致客户端会时不时连不上服务器
有用户反馈软件客户端会时不时出现无法登录服务器的问题。使用Wireshark抓包看,客户端在发出三次握手的SYN包,始终收不到服务器的ACK包,甚至触发了客户端的丢包重传,即**多次发送SYN包,服务器都没有回应,导致客户端和服务器建TCP连接失败**。
后来在平台侧也进行了抓包,发现服务器**确实收到了客户端发来的SYN包,但就是没有回ACK应答包**。经排查得知,服务器的Linux系统的TCP/IP协议栈开启了**reuse和recycle选项**,这和协议栈的**timestamp时间戳策略**会冲突,如果短时间内多次收到SYN包,平台侧TCP/IP协议栈会直接将请求拒绝掉,不给SYN包发送端任何回应。
服务器侧这两个选项一般都不能开启,特别是**tcp_tw_recycle选项开启后,可能会导致部分连接请求不响应,导致连接失败。**在服务器侧,可以通过命令直接将这两个选项关闭掉:
echo 0 > /proc/sys/net/ipv4/tcp_tw_reuse
echo 0 > /proc/sys/net/ipv4/tcp_tw_recycle
关于这两个选项的说明如下:
(1)tcp_tw_reuse:主要用于端口复用,用在客户端侧,将其设置为1表示允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭。
(2)tcp_tw_recycle:将其设置为1表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭。
5、Windows系统中使用双网卡时,可能需要添加策略路由
在一台测试用的Windows PC机上,配置了两张网卡,一张是连接外网的网卡,用于上外网;一张是连接内网的网卡,用于连局域网,如下:
而Windows操作系统只允许设置一个默认网关,所以只能有一个网卡配置默认网关,一般是连接外网的网卡配置默认网关,因为外网的IP地址是不固定的。内网的网卡则不配置网关,对于内网的IP地址是相对固定的,比如以192.168开头的、以172.16开头的、以10.开头的。当要访问这些地址时,可以通过添加策略路由的方式指定访问这些开头的IP地址从指定的局域网的网关出去。
只要是有网卡配置了默认网关,都会在路由表中添加一条走默认网关的默认路由,如下所示:
通过命令去添加策略路由时,也会向系统中添加对应的路由条目,添加策略路由的命令如下:
route add 172.16.0.0 mask 255.255.0.0 172.16.125.88(内网的网关)
这条添加路由命令的含义是:所有访问以172.16开头的IP地址,都从网关172.16.125.88出去。
当我们发起对一个IP地址的访问时(也可能通过域名去访问,会先将域名解析为IP地址,然后用IP地址去访问),系统在查找路由时,会优先匹配系统的非默认路由,即会匹配添加的策略路由,当匹配不上时才会去使用默认路由。
所以,访问外网的地址时会走连接外网的网卡出去,访问以192.168开头的、以172.16开头的等内网地址时,会走策略路由中指定的连接内网的网关出去。
6、连接线路中的网络设备将客户端与其之间的连接单方面关闭掉,导致后续登录服务器时出现异常
在公司局域网的测试环境中,客户端自动重连服务器出现问题。根据打印日志发现,客户端和服务器之间的TCP长连接因为网络问题出现断链,**客户端在收到断链通知后,会去自动重连服务器,但始终都连接不上。**
根据打印日志看到,**服务器返回的错误码是用户已登录**。这个就奇怪了,明明是客户端收到与服务器的连接断开的通知后去重连的,为啥服务器侧还反馈我们的账号还处于登录状态呢?既然连接断了,服务器应该也能感知到的,账户不太可能还出于连接状态的!
于是使用SSH远程登录到服务器上,使用netstat查看服务器当前的TCP连接列表,在列表中看到了客户端的IP,客户端居然和服务器还处于连接状态。
于是找公司大牛帮忙排查分析一下,他查下来怀疑可能是客户端与服务器之间的**路由器单方面将路由器与客户端之间的链路给断开了,但路由器与服务器之间的链路还保持着,还没断开**。该路由器是好多年前购买的老式华为路由器,可能是路由器有问题,**估计是因为客户端与服务器长时间没有数据交互,路由器认为客户端与其的链路失去活性了,强行将其与客户端之间的链路释放了。**
客户端与服务器之间使用websocket网络库(libwebsockets开源库)进行通信的,libwebsockets库支持开启心跳机制、设置心跳参数的。为了解决连接链路上长时间不跑数据导致链路被释放问题,在初始化libwebsockets库时,设置一下心跳参数就可以了。
libwebsockets库中设置心跳参数的结构体如下所示:
/**
* struct lws_context_creation_info - parameters to create context with
*
* This is also used to create vhosts.... if LWS_SERVER_OPTION_EXPLICIT_VHOSTS
* is not given, then for backwards compatibility one vhost is created at
* context-creation time using the info from this struct.
*
* If LWS_SERVER_OPTION_EXPLICIT_VHOSTS is given, then no vhosts are created
* at the same time as the context, they are expected to be created afterwards.
*
* @port: VHOST: Port to listen on... you can use CONTEXT_PORT_NO_LISTEN to
* suppress listening on any port, that's what you want if you are
* not running a websocket server at all but just using it as a
* client
* @iface: VHOST: NULL to bind the listen socket to all interfaces, or the
* interface name, eg, "eth2"
* If options specifies LWS_SERVER_OPTION_UNIX_SOCK, this member is
* the pathname of a UNIX domain socket. you can use the UNIX domain
* sockets in abstract namespace, by prepending an @ symbole to the
* socket name.
* @protocols: VHOST: Array of structures listing supported protocols and a protocol-
* specific callback for each one. The list is ended with an
* entry that has a NULL callback pointer.
* It's not const because we write the owning_server member
* @extensions: VHOST: NULL or array of lws_extension structs listing the
* extensions this context supports. If you configured with
* --without-extensions, you should give NULL here.
* @token_limits: CONTEXT: NULL or struct lws_token_limits pointer which is initialized
* with a token length limit for each possible WSI_TOKEN_***
* @ssl_cert_filepath: VHOST: If libwebsockets was compiled to use ssl, and you want
* to listen using SSL, set to the filepath to fetch the
* server cert from, otherwise NULL for unencrypted
* @ssl_private_key_filepath: VHOST: filepath to private key if wanting SSL mode;
* if this is set to NULL but sll_cert_filepath is set, the
* OPENSSL_CONTEXT_REQUIRES_PRIVATE_KEY callback is called
* to allow setting of the private key directly via openSSL
* library calls
* @ssl_ca_filepath: VHOST: CA certificate filepath or NULL
* @ssl_cipher_list: VHOST: List of valid ciphers to use (eg,
* "RC4-MD5:RC4-SHA:AES128-SHA:AES256-SHA:HIGH:!DSS:!aNULL"
* or you can leave it as NULL to get "DEFAULT"
* @http_proxy_address: VHOST: If non-NULL, attempts to proxy via the given address.
* If proxy auth is required, use format
* "username:password@server:port"
* @http_proxy_port: VHOST: If http_proxy_address was non-NULL, uses this port at
* the address
* @gid: CONTEXT: group id to change to after setting listen socket, or -1.
* @uid: CONTEXT: user id to change to after setting listen socket, or -1.
* @options: VHOST + CONTEXT: 0, or LWS_SERVER_OPTION_... bitfields
* @user: CONTEXT: optional user pointer that can be recovered via the context
* pointer using lws_context_user
* @ka_time: CONTEXT: 0 for no keepalive, otherwise apply this keepalive timeout to
* all libwebsocket sockets, client or server
* @ka_probes: CONTEXT: if ka_time was nonzero, after the timeout expires how many
* times to try to get a response from the peer before giving up
* and killing the connection
* @ka_interval: CONTEXT: if ka_time was nonzero, how long to wait before each ka_probes
* attempt
* @provided_client_ssl_ctx: CONTEXT: If non-null, swap out libwebsockets ssl
* implementation for the one provided by provided_ssl_ctx.
* Libwebsockets no longer is responsible for freeing the context
* if this option is selected.
* @max_http_header_data: CONTEXT: The max amount of header payload that can be handled
* in an http request (unrecognized header payload is dropped)
* @max_http_header_pool: CONTEXT: The max number of connections with http headers that
* can be processed simultaneously (the corresponding memory is
* allocated for the lifetime of the context). If the pool is
* busy new incoming connections must wait for accept until one
* becomes free.
* @count_threads: CONTEXT: how many contexts to create in an array, 0 = 1
* @fd_limit_per_thread: CONTEXT: nonzero means restrict each service thread to this
* many fds, 0 means the default which is divide the process fd
* limit by the number of threads.
* @timeout_secs: VHOST: various processes involving network roundtrips in the
* library are protected from hanging forever by timeouts. If
* nonzero, this member lets you set the timeout used in seconds.
* Otherwise a default timeout is used.
* @ecdh_curve: VHOST: if NULL, defaults to initializing server with "prime256v1"
* @vhost_name: VHOST: name of vhost, must match external DNS name used to
* access the site, like "warmcat.com" as it's used to match
* Host: header and / or SNI name for SSL.
* @plugin_dirs: CONTEXT: NULL, or NULL-terminated array of directories to
* scan for lws protocol plugins at context creation time
* @pvo: VHOST: pointer to optional linked list of per-vhost
* options made accessible to protocols
* @keepalive_timeout: VHOST: (default = 0 = 60s) seconds to allow remote
* client to hold on to an idle HTTP/1.1 connection
* @log_filepath: VHOST: filepath to append logs to... this is opened before
* any dropping of initial privileges
* @mounts: VHOST: optional linked list of mounts for this vhost
* @server_string: CONTEXT: string used in HTTP headers to identify server
* software, if NULL, "libwebsockets".
*/
struct lws_context_creation_info {
int port; /* VH */
const char *iface; /* VH */
const struct lws_protocols *protocols; /* VH */
const struct lws_extension *extensions; /* VH */
const struct lws_token_limits *token_limits; /* context */
const char *ssl_private_key_password; /* VH */
const char *ssl_cert_filepath; /* VH */
const char *ssl_private_key_filepath; /* VH */
const char *ssl_ca_filepath; /* VH */
const char *ssl_cipher_list; /* VH */
const char *http_proxy_address; /* VH */
unsigned int http_proxy_port; /* VH */
int gid; /* context */
int uid; /* context */
unsigned int options; /* VH + context */
void *user; /* context */
int ka_time; /* context */
int ka_probes; /* context */
int ka_interval; /* context */
#ifdef LWS_OPENSSL_SUPPORT
SSL_CTX *provided_client_ssl_ctx; /* context */
#else /* maintain structure layout either way */
void *provided_client_ssl_ctx;
#endif
short max_http_header_data; /* context */
short max_http_header_pool; /* context */
unsigned int count_threads; /* context */
unsigned int fd_limit_per_thread; /* context */
unsigned int timeout_secs; /* VH */
const char *ecdh_curve; /* VH */
const char *vhost_name; /* VH */
const char * const *plugin_dirs; /* context */
const struct lws_protocol_vhost_options *pvo; /* VH */
int keepalive_timeout; /* VH */
const char *log_filepath; /* VH */
const struct lws_http_mount *mounts; /* VH */
const char *server_string; /* context */
/* Add new things just above here ---^
* This is part of the ABI, don't needlessly break compatibility
*
* The below is to ensure later library versions with new
* members added above will see 0 (default) even if the app
* was not built against the newer headers.
*/
void *_unused[8];
};
上述结构体中的ka_time、ka_interval和ka_probes三个字段,是心跳参数,这三个参数的含义是:
ka_time:两个心跳包之间的时间间隔;
ka_interval:给对端发送心跳包之后,收不到对端ACK确认超时时间;
ka_probes:心跳包探测次数。
我们在调用lws_create_context接口初始化libwebsockets库时,可以指定这三个参数
static lws_context* CreateContext()
{
lws_set_log_level( 0xFF, NULL );
lws_context* plcContext = NULL;
lws_context_creation_info tCreateinfo;
memset(&tCreateinfo, 0, sizeof tCreateinfo);
tCreateinfo.port = CONTEXT_PORT_NO_LISTEN;
tCreateinfo.protocols = protocols;
tCreateinfo.ka_time = 10; // 心跳包间的时间间隔
tCreateinfo.ka_interval = 10; // 发出心跳包后没有收到ACK确认包时重发心跳包的超时时间
tCreateinfo.ka_probes = 3; // 心跳探测次数,对于windows操作系统,此设置是无效的,Windows系统时固定为10次,不可修改
tCreateinfo.options = LWS_SERVER_OPTION_DISABLE_IPV6;
plcContext = lws_create_context(&tCreateinfo);
return plcContext;
}
跟进libwebsockets库的开源代码中,函数lws_create_context的内部,最终调用的是lws_plat_set_socket_options接口,该接口内部最终是给对应的socket套接字设置心跳参数的,如下:
LWS_VISIBLE int
lws_plat_set_socket_options(struct lws_vhost *vhost, lws_sockfd_type fd)
{
int optval = 1;
int optlen = sizeof(optval);
u_long optl = 1;
DWORD dwBytesRet;
struct tcp_keepalive alive;
int protonbr;
#ifndef _WIN32_WCE
struct protoent *tcp_proto;
#endif
if (vhost->ka_time) {
/* enable keepalive on this socket */
// 先调用setsockopt打开发送心跳包(设置)选项
optval = 1;
if (setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE,
(const char *)&optval, optlen) < 0)
return 1;
alive.onoff = TRUE;
alive.keepalivetime = vhost->ka_time*1000;
alive.keepaliveinterval = vhost->ka_interval*1000;
if (WSAIoctl(fd, SIO_KEEPALIVE_VALS, &alive, sizeof(alive),
NULL, 0, &dwBytesRet, NULL, NULL))
return 1;
}
/* Disable Nagle */
optval = 1;
#ifndef _WIN32_WCE
tcp_proto = getprotobyname("TCP");
if (!tcp_proto) {
lwsl_err("getprotobyname() failed with error %d\n", LWS_ERRNO);
return 1;
}
protonbr = tcp_proto->p_proto;
#else
protonbr = 6;
#endif
setsockopt(fd, protonbr, TCP_NODELAY, (const char *)&optval, optlen);
/* We are nonblocking... */
ioctlsocket(fd, FIONBIO, &optl);
return 0;
}
所以libwebsockets库的心跳设置,使用的还是TCPIP协议栈的心跳,不是应用层自己实现的心跳机制。
关于TCPIP协议栈的三个心跳参数的详细说明如下:
(1)keepalivetime:心跳正常时,本端发送一个心跳包给对端,收到对端心跳包的回应,间隔keepalivetime时间后,发下一包心跳包,windows默认的心跳包发送间隔是2小时。
(2)keepaliveinterval:心跳异常时,本端发送心跳包后没收到对端的回应,间隔keepaliveinterval时间后,发送下一个心跳包(继续探测)。如果多次没有收到对端的回应,当探测次数达到上限(keep-alive probes)时,则协议栈认为连接出问题。
(3)keep-alive probes:windows系统中 ,心跳包探测次数keep-alive probes是不可改变的,协议栈固定为10次。
7、在复杂网络环境中主从服务器切换时遇到的多个网络异常问题
主服务器和从服务器共用一个IP,当主服务器出问题时,切换到从服务器上,然后服务器以组播的方式将抢IP的数据包发出去,这个数据包始终没有发出来,导致抢IP操作失败。通过排查得知,**组播数据包会被客户网络环境中的一台华为路由器拦截**,可能是这台华为路由器有问题,但客户要求我们从我们服务器这一侧去修改,后来**将多播改成单播**才解决问题。
客户的网络设备上配置了很多安全规则,其中一个规则是**将IP-MAC地址绑定**,**如果设备的IP和MAC地址对不上,设备发出来的数据包就会被网络设备认为是不安全的数据,会直接被拦截。**在从服务器拿到主服务器的IP之后,IP对应的MAC地址就变了,正好就触发了这个IP-MAC绑定规则,导致数据包被拦截。后来的解决办法是**将主从服务器公用的IP作为特例进行放行**,即对这个IP不进行拦截。
8、Linux系统的TCP/IP协议栈重定向选项被关闭,无法响应网关发来的ICMP重定向消息,导致收发数据时出现严重的丢包问题
给客户部署的系统中,有台设备放置于某个网络节点下,给该设备配置了该节点下的默认网关,结果联调下来发现,**所有的其他节点下的其他设备都没问题,就这台设备有问题**,这台设备发出来的数据有严重的丢包问题。
现场人员和客户一起做了对比测试,把客户之前购买的别的厂商的设备放置在该网络节点下,别的厂商的设备都没有丢包问题,就我们公司的设备有问题。期间,我们给客户调拨了一个我们几年前研发的一款老式设备,放置在该节点下也没问题,就当前使用的新式设备有问题。
这个问题折腾的比较久,始终没有查出来问题,后来找公司的顶级专家来排查,才查出来问题。这台设备发出去的数据,默认情况下都要通过其配置的默认网关发出去,抓包发现,默认网关会给设备发了**ICMP重定向消息**,**该消息中携带一个IP地址**,**该消息是用来告诉设备,要发送数据都从这个IP发出去。**
** **一般情况下,协议栈在收到这个ICMP重定向消息后,会向系统路由表中添加一条路由,这样要发送的数据会使用这条路由中的IP发送出去。通过大量的抓包分析之后,找到了问题的症结,是因为设备内置的Linux系统的TCP/IP协议栈的重定向选项都被关闭导致的,在linux命令行使用命令sysctrl -a | grep redirects可以查看到:
我们硬件设备中的使用的Linux系统是经过裁剪后部署进去的,之前**在系统裁剪时,出于安全考虑,将系统的TCP/IP网络协议栈中所有重定向选项都关闭了,所以此案例中默认网关发过来的ICMP重定向消息被丢弃了,导致发出的数据还是发到默认网关上,但从默认网关出去的数据会有明显的丢包问题**(客户网络环境故意这么处理的,不让数据从默认网关出去),所以出现了最开始出现的问题。
此问题的临时解决办法是**手动将这些重定向选项打开**,后续进行Linux系统裁剪时要将这些重定向选项打开。
版权归原作者 dvlinker 所有, 如有侵权,请联系我们删除。