Python实现清华校园网登录与DDNS

前言

因为疫情的缘故,经常需要在家远程使用学校的计算机,但是有时候会由于电脑重启、停电以及一些莫名其妙的原因导致校园网下线、IP变化,结果就不能远程了。为了解决这个问题,想了一个相对好一点的办法就是,实现校园网的自动联网和DDNS,这样就可以通过URL来远程计算机了。

总体思路

前提条件

这是篇受众明确的短文,不过可以供具有类似条件的读者参考:

  1. 具有公网IP,需要通过登录认证系统联网(这一点可以排除大多数人了,不过很多校园网应该是类似的);
  2. 拥有一个域名,可以通过API进行DNS解析的操作(从阿里云、腾讯云等主流DNS服务商购买的应该都可以)。

流程设计

整个流程其实非常简单,如下图所示:

graph LR
id1[获取本机IP]-->id2[筛选校园网IP]-->id3[校园网登录]-->id4[更新DNS解析记录]-->id5[结束]

下面我们按照这个顺序分别介绍其实现。

获取本机IP

本机IP的获取其实并不难,使用Python其实有好几种方法,我在这里稍做总结:

1. 基于socket

  1. 单网卡

    • 通过gethostname()获取主机名,再用gethostbyname()将主机名转换为IP地址:
    1
    2
    3
    4
    5
    6
    7
    import socket
    # 查看当前主机名
    print('当前主机名称为 : ' + socket.gethostname())
    # 根据主机名称获取当前IP
    print('当前主机的IP为: ' + socket.gethostbyname(socket.gethostname()))

    # Mac下测试上述方法也可以获得IP地址
    • 通过使用socket中的getaddrinfo()中的函数获取IP
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    # 下方代码为获取当前主机IPV4 和IPV6的所有IP地址(所有系统均通用)
    addrs = socket.getaddrinfo(socket.gethostname(),None)
    for item in addrs:
    print(item)
    # 仅获取当前IPV4地址
    print('当前主机IPV4地址为:' + [item[4][0] for item in addrs if ':' not in item[4][0]][0])
    # 同上仅获取当前IPV4地址
    for item in addrs:
    if ':' not in item[4][0]:
    print('当前主机IPV4地址为:' + item[4][0])
    break
  2. 多网卡

    通过使用 socket.gethostbyname_ex() 获取主机IP地址列表:

    1
    2
    3
    4
    5
    6
    try:
    hostname = socket.gethostname()
    ips = socket.gethostbyname_ex(hostname)[2]
    except socket.gaierror:
    logging.warning('failed to resolve host %s', hostname)
    ips = []

2. 基于UDP

利用 UDP 协议生成一个UDP包,把自己的 IP 放如到 UDP 协议头中,然后从UDP包中获取本机的IP:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import socket


def get_host_ip():
"""
查询本机ip地址
:return: ip
"""
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(('8.8.8.8', 80))
ip = s.getsockname()[0]
finally:
s.close()
return ip

if __name__ == '__main__':
print(get_host_ip())

3. 基于netifaces

基于第三方库netifaces获取网卡信息:

1
2
3
4
import netifaces

for interface in netifaces.interfaces():
print(netifaces.ifaddresses(interface))

4. 基于psutil

基于第三方库psutil获取网卡信息:

1
2
3
import psutil

psutil.net_if_addrs()

在以上几种方式中,为了只使用python的原生库,我们选择使用了第一种方式。

筛选校园网IP

在多网卡的情况下,经常会获得多个无效的IP,所以需要筛选其中真正的校园网IP。

清华网段

从网上查了一下清华的IP段,不一定完全准确,仅供参考:

1
2
3
4
5
6
7
8
9
59.66.0.0/16
101.5.0.0/16
101.6.0.0/16
118.229.0.0/19
166.111.0.0/16
183.172.0.0/15
202.112.39.2/32
219.223.168.0/21
219.223.176.0/20

判断IP是否位于网段内

使用Python自带的ipaddress库来判断,具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import ipaddress

TUNET_ZONE = [
"59.66.0.0/16",
"101.5.0.0/16",
"101.6.0.0/16",
"118.229.0.0/19",
"166.111.0.0/16",
"183.172.0.0/15",
"202.112.39.2/32",
"219.223.168.0/21",
"219.223.176.0/20"
]

# 将str转换为IPv4Address类
TUNET = [ipaddress.ip_network(net_mask) for net_mask in TUNET_ZONE]

def ip_in_tunet(ip: str) -> bool:
"""
判断Ip地址是否是有效的清华IP
:param ip:
:type ip:
:return:
:rtype:
"""
ip_addr = ipaddress.ip_address(ip)
for sub_net in TUNET:
if ip_addr in sub_net:
return True

return False

清华校园网登录

关于清华校园网的联网认证,可以从网上找到许多大神的实现,我比较感兴趣的有以下三个:

  1. https://github.com/yuantailing/tunet-python
  2. https://github.com/WhymustIhaveaname/TsinghuaTunet
  3. https://github.com/Berrysoft/tunet-rust

在这里,我使用了第一个项目——“tunet-python”,用的时候遇到了SSL证书的问题,所以把HTTPS的网址改为了HTTP,对安全性有要求的读者可以自行修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 导入https://github.com/yuantailing/tunet-python的tunet.py
import tunet

def connect_to_tunet(username: str, password: str, ip: str, max_tries=5) -> bool:
"""
TUNET 登录出校
:param username:
:type username:
:param password:
:type password:
:param ip:
:type ip:
:param max_tries:
:type max_tries:
:return:
:rtype:
"""
i = 1
for i in range(max_tries):
try:
result = tunet.auth4.login(username, password, net=True, ip=ip)
logging.debug(result)
if result['error'] == 'ok':
return True
else:
tunet.auth4.logout(ip=ip)
except Exception as e:
logging.error(e)
return False

DDNS解析

动态DNS(英语:Dynamic DNS,简称DDNS)是域名系统(DNS)中的一种自动更新名称服务器(Name server)内容的技术。根据互联网的域名订立规则,域名必须跟从固定的IP地址。但动态DNS系统为动态网域提供一个固定的名称服务器(Name server),透过即时更新,使外界用户能够连上动态用户的网址。

阿里云DNS解析

如前所述,我们需要一个域名来做解析。以阿里云的域名解析为例,阿里云提供了线程的SDK,以及实现DDNS的示例,但是为了让脚本尽可能简单,我使用Python自带的urllib发起修改DNS的请求,主要包括以下几种:

  1. 获取解析列表,使用DescribeDomaianRecords;
  2. 如果不存在指定的二级域名解析目录,就添加解析目录
  3. 如果存在指定的二级域名解析目录,就分以下情况:
    1. 要更新的IP数目,与存在的记录数目相同的部分,使用修改解析目录
    2. 要更新的IP数目,多于存在的记录数目的部分,就添加解析目录
    3. 要更新的IP数目,少于存在的记录数目的部分,就删除解析目录

在发起API请求之前,首先需要从阿里云获得AccessKeyAccessSecret

更新部分代码示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

def dynamic_update(aliyun_api_id: str, aliyun_api_secret: str, host_ips:[str], subdomain:str) -> dict:
existing_records = get_exising_subdomain_records(aliyun_api_id, aliyun_api_secret, subdomain)
existing_ip_set = set(existing_records.keys())
host_ip_set = set(host_ips)
to_keep = existing_ip_set & host_ip_set
to_remove = existing_ip_set - to_keep
to_add = host_ip_set - to_keep

for add, remove in zip_longest(to_add, to_remove):
if add is None:
record_id = existing_records.pop(remove)
delete_record_by_id(aliyun_api_id, aliyun_api_secret, record_id)
elif remove is None:
add_domain_record(aliyun_api_id, aliyun_api_secret, subdomain, add)
else:
record_id = existing_records.pop(remove)
update_domain_record(aliyun_api_id, aliyun_api_secret, subdomain, add, record_id)

return get_exising_subdomain_records(aliyun_api_id, aliyun_api_secret, subdomain)

需要注意的是,如果要自己实现阿里云的API请求,需要自己完成计算签名、构造URL等操作。

程序部署与运行

写完Python的程序以后,目前不需要使用任何第三方的库,为了让程序自动运行,需要添加定时任务:

  • 在Windows下,使用任务计划程序:
  • 在MacOS下,使用Launchd;
  • 在Linux下,使用crontab。

注意事项

最需要注意的就是安全。包括但不限于:

  1. Windows远程桌面的RDP端口、SSH端口、MacOS的VNC端口,不建议使用默认端口;
  2. Tunet认证最好使用HTTPS;
  3. 妥善保管用户名和密码,包括Tunet的用户名密码、DNS服务商的AccessID和AccessSecret。

下一步工作

  1. 增加设置定时任务的功能;
  2. 增加非明文保存密码的功能;
  3. 增加更多DNS服务商(如DNSPod)的支持;
  4. 增加对IPv6等Tunet认证的支持;
  5. 是否考虑有路由器情况的端口转发呢?

小结

本文以清华校园网为例,介绍了如何自动化实现认证登录、DDNS的集成功能,以后远程连接就不用那么费事了。由于代码写得比较简陋,就先不公开了。欢迎交流。

参考资料

  1. Python获取本机 IP/MAC(多网卡)
  2. https://cloud.tencent.com/developer/article/1567086
  3. https://en.wikipedia.org/wiki/Dynamic_DNS
  4. https://imchenway.com/2021/02/24/Mac自定义定时任务/