EthernetOnTCP--基于Qt QSslSocket 套接字在PCAP 集线器上实现以太网隧道

上一篇文章中,我们使用PCAP建立了本地的软件集线器(Hub)。考虑到较远距离的跨车间调试,有必要使用Tcp连接构造一个以太网的隧道,使得两个车间之间的调试设备可以虚拟的连接在一个Hub上。当然,我们可以使用QTcpSocket实现连接,或者尝试一下QSslSocket安全套接字,恰好使用前序文章中生成的证书,来玩一玩Ssl连接。

完整工程参考上一篇文章中的Git链接。

具有远程Ssl隧道的调试工作站的原理图如下:
远程调试工作站

1. 点对点TCP连接

我们希望两个调试工作站上的PCAPHub进程可以通过一个点对点的信道连接起来。工作站A 处于监听模式,工作站B处于Client模式。
PCAP集线器这样,一对TCP连接相当于一根逻辑的网线,与各个本地网口是对等的关系。上图中,所有远程的MAC地址都会出现在“TCP隧道对端连接的MAC栏”中。

2. 搭建SSL服务器

在Qt6中,设置了QSslServer类,可以直接构造Ssl服务器。但在Qt5中,并没有这个类。为了兼容Qt5 ,要重载QTcpServer,构造一个SslServer

//"sslserver.h"
class SSLServer : public QTcpServer
{
    
    
	Q_OBJECT
public:
	explicit SSLServer(QObject *parent = nullptr);
protected:
	void incomingConnection(qintptr socketDescriptor) override;
signals:
	void sig_newClient(qintptr socketDescriptor);
};
//CPP
void SSLServer::incomingConnection(qintptr socketDescriptor)
{
    
    
	emit sig_newClient(socketDescriptor);
}

而后,在QObject派生的TcpTunnel类中进行响应,sig_newClient会把套接字描述符泵给tcpTunnel 。在源码中,您会发现这些信号与曹都是在独立的线程中运行的,而不是主UI线程。tcpTunnel 类就是用来实例化工作在QThread上的对象,从而使用QThread独立的消息循环驱动信号、事件的流转。

class tcpTunnel : public QObject
{
    
    
	//...
	//Tunnel
protected:
	SSLServer * m_svr = nullptr;
	QTcpSocket * m_sock = nullptr;
protected slots:
	void slot_new_connection(qintptr socketDescriptor);
};
//New Connections
void tcpTunnel::slot_new_connection(qintptr socketDescriptor)
{
    
    
	QTcpSocket * sock = m_bSSL? new QSslSocket(this):new QTcpSocket(this);
	if (sock->setSocketDescriptor(socketDescriptor)) {
    
    
		connect(sock,&QTcpSocket::readyRead,this,&tcpTunnel::slot_read_sock);
		//SSL Handshake
		QSslSocket * sslsock = qobject_cast<QSslSocket*>(sock);
		if (sslsock)
		{
    
    
			QString strCerPath =  ":/certs/svr_cert.pem";
			QString strPkPath =  ":/certs/svr_privkey.pem";
			sslsock->setLocalCertificate(strCerPath);
			sslsock->setPrivateKey(strPkPath);
			sslsock->startServerEncryption();
		}
	}
}

这里需要解释的有以下几点:

  1. QSslSocket与QTcpSocket具备高度一致的状态机,也就是state()函数的行为高度一致。正因为如此,界面上通过一个简单的标记,可以开启或者关闭Ssl功能。
  2. 作为服务器套接字,要在握手前指定证书。这里使用范例证书直接嵌入在资源里,是有问题的。因为范例证书的服务器地址是127.0.0.1,和真实环境不同,会导致客户端连接失败。本例子里,客户端会忽略地址不一致的错误,这是一种不好的行为。在生产环境下,还是要根据具体情况准备合适的证书。

有了上述逻辑,tcpTunnel 就能够在客户端到来时,响应消息并接受连接开始握手。

3 客户端发起连接

在tcpTunnel类的槽函数里,会发起指向服务器的连接. 这里需要注意的是,要及时响应QSslSocket::sslErrors信号,并处理几类证书错误,以便在含有错误的证书情况下,依旧可以建立连接。相关的错误是:

  • QSslError::CertificateUntrusted 不信任的证书
  • QSslError::CertificateNotYetValid 尚未生效的证书(未来的时刻)
  • QSslError::CertificateExpired 过期的证书
  • QSslError::HostNameMismatch 证书的服务器地址和当前连接的HostAddress不同。

此外,对于Qt5,由于信号QSslSocket::sslErrors存在重载,导致必须要进行显式的类型约束,才能进行functional 样式的connect。Qt6里已经避免了这个问题。

//H
class tcpTunnel : public QObject
{
    
    
public slots:
	void startWork(QString address, QString port, bool listen,bool ssl);
};
//CPP
void tcpTunnel::startWork(QString address, QString port, bool ssl)
{
    
    
	if (m_bSSL)
	{
    
    
		QSslSocket * sslsock = new QSslSocket(this);
		connect(sslsock,static_cast<void (QSslSocket::*)(const QList <QSslError> &)>(&QSslSocket::sslErrors),
						[this,sslsock](const QList <QSslError> & err)->void
		{
    
    
					QList<QSslError> errIgnore;
					foreach (QSslError e, err)
					{
    
    
						emit sig_message(tr("SSL Error %1:%2 .").arg((int)e.error()).arg(e.errorString()));
						if (e.error()==QSslError::HostNameMismatch) errIgnore<<e;
						else if (e.error()==QSslError::CertificateUntrusted) errIgnore<<e;
						else if (e.error()==QSslError::CertificateNotYetValid) errIgnore<<e;
						else if (e.error()==QSslError::CertificateExpired) errIgnore<<e;
					}
					sslsock->ignoreSslErrors(errIgnore);
		});
				m_sock = sslsock;
				sslsock->connectToHostEncrypted(m_str_addr,m_n_port);
	}
	else
	{
    
    
		m_sock = new QTcpSocket(this);
		m_sock->connectToHost(QHostAddress(m_str_addr),m_n_port);
	}
	connect(m_sock,&QTcpSocket::readyRead,this,&tcpTunnel::slot_read_sock);
	emit sig_message(tr("connecting to: %1:%2").arg(m_str_addr).arg(m_n_port));
}

4. Ethernet on TCP 协议

在上一篇文章里,对于抓取的以太网数据,存储在一个环状高速缓存中。如果通过tcp传输以太网数据,需要设计一种切割包的协议。我们用最简单的方法来做。

Magic 长度 数据
4Bytes 2Bytes UShort N1 Bytes
4Bytes 2Bytes UShort N2 Bytes
4Bytes 2Bytes UShort N3 Bytes
……

这样,只需要在接收时,检测并取数据即可完成分包。发包的代码如下:

			quint64 rp = PCAPIO::pcap_recv_pos;
			while (m_nTPos < rp) {
    
    
				const int nPOS = m_nTPos % PCAPIO_BUFCNT;
				const int fromID = PCAPIO::global_buffer[nPOS].from_id;
				if (fromID !=TCPTUNID
						&& PCAPIO::global_buffer[nPOS].len < 65536
						&& PCAPIO::global_buffer[nPOS].len >0)
				{
    
    
					const unsigned char  hd[] = {
    
    0x18u,0x24u,0x7eu,0x69u};
					const unsigned short len = PCAPIO::global_buffer[nPOS].len;
					m_sock->write((const char *)hd,4);
					m_sock->write((const char *)&len,2);
					m_sock->write(PCAPIO::global_buffer[nPOS].data.constData(),len);
				}
				++m_nTPos;
		}

收包的代码如下:

class tcpTunnel : public QObject
{
    
    
	void dealPack(const  char * pack, const int len);
	//Tunnel
private:
	quint64 m_nTPos = 0;
	QByteArray m_package_array;
protected slots:
	void slot_read_sock();
};

void tcpTunnel::slot_read_sock()
{
    
    
	QByteArray arrData = m_sock->readAll();
	m_package_array.append(arrData);
	while (static_cast<size_t>(m_package_array.size())>=6)
	{
    
    
		//检查Magic
		int goodoff = 0;
		while (!(m_package_array[0+goodoff]==(char)0x18 && m_package_array[1+goodoff]==(char)0x24
				 &&m_package_array[2+goodoff]==(char)0x7E  &&m_package_array[3+goodoff]==(char)0x69 ))
		{
    
    
			++goodoff;
			if (goodoff+3>= m_package_array.size())
				break;
		}
		if (goodoff)
			m_package_array.remove(0,goodoff);
		if (m_package_array.size()< 6 )
			break;
		const unsigned short  * ptrlen = (const unsigned short *)
				(m_package_array.constData() + 4);
		const unsigned short datalen = *ptrlen;
		if (m_package_array.size()<datalen+6)
			break;
		const char * dptr = m_package_array.constData()+6;
		//Enqueue入队
		dealPack(dptr,datalen);
		//清除当前包
		m_package_array.remove(0,6+datalen);
	}
}

5 避免嵌套风暴

通过上述操作,理论上可以把远程的以太网包带到本地。但是,由于TCP隧道本身也运行在以太网协议上,如果PCAP在抓取的时候,把TCP隧道的内容也给抓了,那样的话会出现流量风暴。

如何避免流量风暴呢?只要在抓取前,把 “not tcp port 12345” 这样的过滤条件追加在用户的条件之后,即可避免风暴生成。

//2. Run Cap Thread on interface.
	void recv_loop(QString itstr, QString filterStr, int id, int tcp_exlude,std::function<void (QString) > msg)
	{
    
    
		while (!pcap_stop)
		{
    
    
			pcap_t *handle = NULL;
			//...
			struct bpf_program filter;
			//Combine Filter
			QString ExFlt ;
			if (tcp_exlude > 0 && tcp_exlude < 65536)
			{
    
    
				if (filterStr.trimmed().length())
					ExFlt = "(" + filterStr.trimmed() + QString(") and not tcp port %1" ).arg(tcp_exlude);
				else
					ExFlt = QString("not tcp port %1" ).arg(tcp_exlude);
			}
			else
				ExFlt = filterStr.trimmed();
			//Compile Filter
			if (ExFlt.size())
			{
    
    
				std::string filter_app = ExFlt.toStdString();
				pcap_compile(handle, &filter, filter_app.c_str(), 0, net);
				pcap_setfilter(handle, &filter);
			}
		}
	}
}

当然,有时候这样做还不够,还要结合各个网口的抓包条件,共同进行约束。原则上,所有和隧道相关的流量都要排除在PCAP条件之外。

比如,如果这个TCP隧道是通过ssh的代理进行 -L或者-R接续的,则连带SSH也要排除。同时,由于HUB是低效的广播,要把类似远程桌面3389这种持续的流量全给关了。否则,可能会和设备抢夺带宽。

6 在HUB和交换机模式间灵活切换

我们只需要在像网卡回放时,通过一个开关来控制是否回放属于其他所有网口的内容,即可使得本网口工作于 集线器 或者 交换机模式下了。

	struct tag_packages{
    
    
		int from_id;//From which port
		int to_id;//To which port
		int len;
		QByteArray data;
	};
	void recv_loop(QString itstr, QString filterStr, int id, int tcp_exlude,std::function<void (QString) > msg)
	{
    
    
					//...Dst Mac
					const bool newDstMac = pcap_ports.contains(mac_dst);
					if (newDstMac)
					{
    
    
						if(pcap_ports[mac_dst].dtmLastAck.msecsTo(dtm) <=CAP_FADE_MSECS)
							dst_id = pcap_ports[mac_dst].curr_id;
					}
	}	
	//3. Run Send Thread on interface
	void send_loop(QString itstr,int id, bool bSwitchMod,std::function<void (QString) > msg)
	{
    
    
						if (global_buffer[pos].from_id!=id &&
						((!bSwitchMod)||(global_buffer[pos].to_id==id || global_buffer[pos].to_id==-1))
						)
				{
    
    
				}
	}

使用这个策略,在网口很多时,就能灵活的控制PCAPHub,使他工作在效率与范围兼顾的混合模式。

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/goldenhawking/article/details/128672649