0


网络问题排查实例集锦(实战经验分享)

   现在大部分软硬件系统都是基于网络的,有走局域网(私网)的,有走外网(公网)的,会不可避免地出现很多与网络相关的问题,特别是将产品部署到安全级别较高的客户环境中,会出现各式各样的复杂网络问题。今天我们就来分享一下实际项目中遇到的多个网络问题,以供参考!

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重定向消息**,**该消息中携带一个IP地址**,**该消息是用来告诉设备,要发送数据都从这个IP发出去。**

** **一般情况下,协议栈在收到这个ICMP重定向消息后,会向系统路由表中添加一条路由,这样要发送的数据会使用这条路由中的IP发送出去。通过大量的抓包分析之后,找到了问题的症结,是因为设备内置的Linux系统的TCP/IP协议栈的重定向选项都被关闭导致的,在linux命令行使用命令sysctrl -a | grep redirects可以查看到:

   我们硬件设备中的使用的Linux系统是经过裁剪后部署进去的,之前**在系统裁剪时,出于安全考虑,将系统的TCP/IP网络协议栈中所有重定向选项都关闭了,所以此案例中默认网关发过来的ICMP重定向消息被丢弃了,导致发出的数据还是发到默认网关上,但从默认网关出去的数据会有明显的丢包问题**(客户网络环境故意这么处理的,不让数据从默认网关出去),所以出现了最开始出现的问题。        

   此问题的临时解决办法是**手动将这些重定向选项打开**,后续进行Linux系统裁剪时要将这些重定向选项打开。
标签: 网络 服务器 linux

本文转载自: https://blog.csdn.net/chenlycly/article/details/124643918
版权归原作者 dvlinker 所有, 如有侵权,请联系我们删除。

“网络问题排查实例集锦(实战经验分享)”的评论:

还没有评论