Windows Server间的通信: 基于Python-Socket编程的实现

目录

0x00 场景描述

工作中遇到一个需求,有两台windows server,我们希望在一台server A中通知另一台server B执行Python程序,并将结果返回到服务器A中。

0x01 实现1:基于Socket客户端-服务器的实现

思路:在server B中实现一个Socket Server,开放指定端口,等待客户端(Server A)的请求,并进行数据交换:客户端向服务端发送python脚本执行命令:

创建测试用脚本

exec/task.py

import argparse


def run(task_id):
    msg = f"you have successfully run task {task_id}"
    print("print: ", msg)
    return msg


if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("--task_id", type=int, dest="task")
    a = parser.parse_args()
    if a.task is None:
        print("No arg")
    else:
        print(f"called from server {run(task_id=a.task)}")

服务端实现

server.py

功能:在本地(127.0.0.1)的12138端口创建Socket服务 (使用AF_INET地址家族,TCP类型套接字),监听来自客户端的信息(命令),通过subprocess.check_output方法执行命令并得到返回结果。

import socket
import subprocess
from time import ctime
BUF_SIZE = 1024
class SocketServer:
    def __init__(self, host='127.0.0.1', port=12138):
        self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        # 为服务绑定主机地址和端口号
        self.server.bind((host, port))
        self.server.listen(1)

    def run(self):
        while True:
            try:
                print("server listening...")
                # 等待客户端连接(阻塞,直到收到客户端的连接请求)
                client_socket, address = self.server.accept()
                print(f"connected from {address}...")

                command = client_socket.recv(BUF_SIZE).decode()
                # refer to https://docs.python.org/3/library/subprocess.html#subprocess.check_output
                result = subprocess.check_output(command, shell=True)
                print(result)
                # socket.sendall的参数是byte对象,需要将字符串通过encode方法转成byte对象
                client_socket.sendall(f"[{ctime()}]-{result.decode()}".encode())
                client_socket.close()
            except subprocess.CalledProcessError as e:
                # raise RuntimeError("command '{}' return with error (code {}): {}".format(e.cmd, e.returncode, e.output))
                print("command '{}' return with error (code {}): {}".format(e.cmd, e.returncode, e.output))

if __name__ == "__main__":
    server = SocketServer()
    server.run()

注:该socket server的实现在一个无限循环中,如果要实现socket服务关闭的功能,需要调用socket.close方法关闭socket服务器连接,释放内存。

在windows中,可通过netstat命令查看对应端口是否已有对应服务

客户端实现

client.py

功能:向服务地址和端口发起socket连接请求,发送待执行命令(byte),返回执行结果

import socket

def send_command(command:str) -> str:
    host = '127.0.0.1'
    port = 12138
    client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    client_socket.connect_ex((host, port))
    client_socket.sendall(command.encode())
    print(client_socket.family, client_socket.type, client_socket.proto)
    result = client_socket.recv(1024)
    return result.decode()

if __name__ == "__main__":
    print(send_command("python --version"))
    print(send_command("python ..\exec\\task.py --task_id 1"))
    print(send_command("python ..\exec\\task.py --task_id 10"))

先运行server.py,再运行client.py,输出结果:

此时服务器端控制台输出如下:

注:测试代码仅实现远程命令执行,当生产环境开放类似功能给客户端使用时,尽量限制可执行命令权限,避免出现安全隐患。

小结

实现1虽然给出两个windows server间通知脚本执行、数据交换的功能,但没有考虑通信的安全性,服务端没有对客户端进行身份验证 ,传输没有经过加密等,这样的服务不适合开放。如果要完善Socket服务实现(安全性角度),可以考虑如下方面 :

  1. 基于SSL/TLS协议实现对Socket服务的加密通信

  2. 使用Token验证

  3. 使用SSH隧道对Socket服务进行 加密保护

0x02 更多思路

  1. 基于HTTP请求: 在B服务器上部署一个Web服务器,在A服务器上向B服务器发送HTTP请求,请求中添加需要执行的Python脚本路径或者命令、参数,B服务器上的Web服务器解析请求并执行响应命令

  2. 基于SSH服务:在B服务器上安装配置SSH服务并设置允许远程登录,在A服务器上使用诸如paramiko的库来连接到B服务器并执行Python命令

  3. 基于消息队列:使用一个消息队列系统,如RabbitMQ或ZeroMQ,将消息从A服务器发送到消息队列,B服务器上监听消息队列并在接收到执行时执行需要的Python脚本或命令。

0x03 知识点

Socket

https://docs.python.org/3/library/socket.html

Socket的概念

Socket是一种计算机网络中的一种通信机制,用于实现不同进程或不同计算机之间的数据传输,中文名叫“套接字”。通过创建Socket对象并指定相应的地址和端口,进程可以与其他进程建立网络连接,进行数据交换和通信。网络编程中,Socket是一种统一的接口,其提供一种通信方式让两方或多方之间可以进行任意类型的数据传输。

Socket地址家族

AF表示地址家族(Address Family),部分系统会将地址家族表示为域(domain)协议家族(protocol family)

  • AF_UNIX

  • AF_LOCAL

  • AF_INET

    基于网络的地址家族。

  • AF_INET6

    基于Ipv6(第6版因特网协议)寻址。

    如实现1,如果要使用IPv6,则客户端和服务端的代码需要修改以下部分 :

      def send_command(command:str) -> str:
        # host = '127.0.0.1
        # CHANGE: 本地主机的IPv6地址 ::1
        host = '::1'
        port = 12138
        # CHANGE: 地址家族参数设置为socket.AF_INET6
        client_socket = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
        client_socket.connect_ex((host, port))
        client_socket.sendall(command.encode())
        print(client_socket.family, client_socket.type, client_socket.proto)
        result = client_socket.recv(1024)
        return result.decode()
    
    class SocketServer:
        def __init__(self, host='::1', port=12138)
            # CHANGE: 地址家族参数设置为socket.AF_INET6:
            self.server = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
            # 为服务绑定主机地址和端口号
            self.server.bind((host, port))
            self.server.listen(1)
    

  • AF_NETLINK

  • AF_TIPC

Socket的类型

存在两种风格的套接字:面向连接的套接字和无连接的套接字。

面向连接的套接字

该类型连接在通信前需要先建立连接,类似使用电话系统给朋友打电话,该类型的通信也被成为虚拟电路流套接字(Python中对此类套接字的常量命名为socket.SOCK_STREAM);

面向连接的通信提供 序列化、可靠、不重复的数据交付 ,每条消息可以拆分成多个片段 ,每一个消息 片段可以确保到达目的地 并按顺序组合成完整消息传递给正在等待的程序 。

实现这种连接类型需要一种协议,主要采用传输控制协议(Transport Control Protocol,TCP协议),由于 AF_INET家族的套接字通常使用IP协议寻找网络中的主机,所以整个系统常常结合TCP协议和IP协议进行工作 。

无连接的套接字

无连接套接字常被称为数据报类型的套接字,该类型连接在通信开始之前不需要建立连接,传输过程中无法保证消息的顺序性、可靠性和重复性 。

实现该类型连接的主要协议是用户数据报协议(User Datagram Protocol),在python中使用命名为socket.SOCK_DGRAM,由于无连接套接字也使用IP协议寻找网络中 的主机,因此该类系统也被称为UDP/IP系统。

基于Python实现一个简单的基于UDP协议的Socket服务器如下(udp_server.py):

from socket import *
from time import ctime
ADDR = "::1"
PORT = 12135
BUFSIZE = 1024


class UDPServer:
    """UDP服务器"""
    def __init__(self, host, port):
        self.udp_socket = socket(family=AF_INET6, type=SOCK_DGRAM, proto=0)
        self.udp_socket.bind((host, port))

    def run(self):
        try:
            while True:
                print("Server is waiting data...")
                data, addr = self.udp_socket.recvfrom(BUFSIZE)
                self.udp_socket.sendto(("[%s] %s}"%(ctime(), data.decode("utf-8"))).encode(), addr)
                print(f"received {data.decode('utf-8')} from and returned to: {addr}")
        except Exception as e:
            print(repr(e))
        finally:
            self.udp_socket.close()

if __name__ == "__main__":
    UDPServer(host=ADDR, port=PORT).run()

再实现一个udp客户端(udp_client.py)与服务端进行通信

from socket import *
HOST = "::1"
PORT = 12135
BUFSIZE = 1024


if __name__ == "__main__":
    udp_client = socket(AF_INET6, SOCK_DGRAM)
    try:
        while True:
            data = input(">")
            if not data:
                break
            udp_client.sendto(data.encode(), (HOST, PORT))
            data, addr = udp_client.recvfrom(BUFSIZE)
            if not data:
                break
            print(data.decode('utf-8'))
    finally:
        udp_client.close()

可以注意到,udp服务端不需要监听客户端连接并维持一个客户端的socket连接对象,只需要等待数据、发送数据即可。

虚拟电路类型连接 vs 用户数据报类型连接

前者提供对数据传输可靠性、顺序性的保证,但维护“虚拟电路”需要一定开销,传输效率相对较低;后者虽然不可靠,但不需要维护“虚拟电路”,低廉的成本及更好的性能适用于某些对实时性要求高但准确性要求低的程序,如DNS服务器、音视频即时通信等。

socketserver

socketserver是一个socket编程的高级模块,其对socket库进行了封装,可以简化socket编程中一些样板代码。

基于socketserver的socket服务器(TCP)实现示例

# tcp_server.py

"""
使用socket高级模块socketserver
"""
from socketserver import (TCPServer as TCP,
                          StreamRequestHandler as SRH
                          )
from time import ctime

HOST = "127.0.0.1"
# HOST = "::1"

PORT = 12138
ADDR = (HOST, PORT)


class MyRequestHandler(SRH):
    def handle(self) -> None:
        """
        当接收到来自客户端的消息,就会调用handle方法
        :return:
        """
        print(f"connected from: {self.client_address}")
        self.wfile.write(('[%s] %s' % (ctime(), self.rfile.readline().decode())).encode())


if __name__ == "__main__":
    # 基于给定的主机信息和请求处理类创建TCP服务器
    tcp_server = TCP(server_address=ADDR, RequestHandlerClass=MyRequestHandler)
    print("waiting for connection...")
    tcp_server.serve_forever()

socket客户端(TCP)示例代码

# tcp_client.py
import socket
HOST = "127.0.0.1"
PORT = 12138
ADDR = (HOST, PORT)


if __name__ == "__main__":


    while True:
        client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        client_socket.connect(ADDR)
        data = input(">")
        if not data:
            break
        # \r\n是行终止符,由于处理程序类对待套接字通信像对待文件一样所以需要行终止符才能让服务器完成数据读取
        client_socket.send(("%s\r\n"%data).encode())

        # client_socket.send(("%s\r\n"%data).encode())
        result = client_socket.recv(1024)
        print(result.strip())
        client_socket.close()

subprocess.check_output

subprocess.check_output是Python内置的一个标准库函数,用于执行外部命令并返回命令结果的输出内容。该函数可以在Python中运行任何一条系统命令或子进程,其使用方法与subprocess.call、subprocess.Popen类似。

注意:subprocess.check_output是一个阻塞函数,如果执行的命令耗时较长,会阻塞程序的执行,因此如果命令需要长时间执行,推荐使用subprocess.Popen。

SSL/TLS协议

原理

SSL(Secure Socket Layer),安全套接字层。

SSL/TLS in Detail | Microsoft Learn

实践: 基于SSL保护的socket通信服务

基于openssl生成证书文件和密钥(服务端)

openssl提供了通用的密码学库,包括了对称加密、非对称加密和消息摘要等各种密码学算法的实现,其应用范围非常广泛,目前可以用于安全通信、数字签名、证书颁发以及相应的密钥管理功能等。

  1. 生成私钥文件server.key

    server.key 是服务器端 SSL/TLS 通信的私钥文件。在 SSL 连接建立时,SSL/TLS 协议需要使用一种加密密钥来保护通信数据的机密性和完整性,而密钥的安全性取决于私钥的保护。私钥被用于加密数据、解密数据以及创建和验证数字签名,只有获得私钥才能进行这些操作。通过openssl可以指定算法生成私钥文件,私钥格式包括 RSA、DSA 和 ECDSA,以下示例指定RSA算法生成私钥文件。

    openssl genpkey -algorithm RSA -out server.key  
    

    生成的server.key样式如下:

    RSA算法的可靠性由极大整数因数分解的难度保证,目前还没有任何可靠的攻击RSA算法的方式,只有短RSA钥匙才可能被暴力破解。

  2. 生成证书签名请求文件server.csr

    openssl req -new -key server.key -out server.csr
    

    该命令会使用上一步中生成的私钥文件来创建一个证书签名请求 (CSR),其中包含您的站点的信息。您可以在执行此命令需要提供站点信息,或使用默认值。

    生成的证书签名请求文件如下:

  3. 使用csr文件和私钥文件生成证书文件server.crt 将csr文件和私钥文件合并生成一个有效期365天的x509证书

    openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt
    

    证书文件样式如下:

    生成的文件通常存储在管理SSL/TLS通信的服务器上供服务器使用。

服务端实现

通过ssl库实现对socket的加密,代码如下:

# ssl_server.py
import ssl
import socket

# 服务器地址、端口以及协议类型配置
server_address = ('localhost', 12138)
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
ssl_context.load_cert_chain(certfile='server.crt', keyfile='server.key')

# 创建并配置套接字
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.bind(server_address)
server_socket.listen(1)

# 循环等待连接请求并处理
while True:
    client_socket, client_address = server_socket.accept()
    ssl_socket = ssl_context.wrap_socket(client_socket, server_side=True)
    print(f'Got connection from {client_address}')
    received_data = ssl_socket.recv(1024)
    if received_data:
        received_message = received_data.decode()
        print(f"Received message: {received_message}")
        response_message = f"Received message: {received_message}"
        ssl_socket.sendall(response_message.encode())
    ssl_socket.close()
客户端实现
import ssl
import socket

client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
ssl_socket = ssl.wrap_socket(client_socket, ssl_version=ssl.PROTOCOL_TLSv1, ciphers="HIGH")
ssl_socket.connect(("127.0.0.1", 12138))
message = "This is a message encrypted by SSL"
ssl_socket.send(message.encode())
response = ssl_socket.recv(1024)
print(response)

基于Wireshark分析数据包

本地回环数据包抓取

tcpdump命令可以监视网络中传输的数据包,支持指定协议、目的ip、源ip、源端口、目的端口等条件过两次数据包,可用参数如下:

-i:指定监听的网络接口。
-n:不将地址转换为名称。
-X:将数据包以十六进制和ASCII码的形式打印出来。
-s:指定获取数据包的大小。
-c:指定捕获的数据包数量。
-w:将捕获到的数据包存储在文件中。

因此,可通过如下命令监听本地回环网络、tcp协议、端口为12138的数据包,并将数据包写入到12138.pcap(此文件可用wireshark打开分析)。

对于ssl socket通信的数据包抓取保存如下:

无SSL的socket通信

从socket客户端发送到socket服务端的数据包(50298->12138),可以看到,发送的数据以明文方式传输。

从socket服务器发送到socket客户端的数据包(12138->50298):

基于SSL协议保护的socket通信

经ssl协议保护的数据包,经过加密而难以看出其中意义。

注:要在wireshark中查看TLS协议,需要进行一定配置,默认情况下支持TLS协议解析,但如果发现不支持,可以查看wireshark配置文件中的disabled_protos看是否禁用了TLS协议,若发现disabled_protos中有tls,移除之并重启wireshark即可分析数据包中的额TLS层。

SSH协议

https://www.fabfile.org/

Welcome to Paramiko’s documentation! — Paramiko documentation

0x04 扩展阅读

CoolCats
CoolCats
理学学士

我的研究兴趣是时空数据分析、知识图谱、自然语言处理与服务端开发