October 23, 2025
Oracle VirtualBox 7.2.2升级到7.2.4 VMMR0.r0故障
Oracle VirtualBox 从版本7.2.2升级到7.2.4虚拟机启动不了,报错如下:
Failed to load R0 module C:\Program Files\Oracle\VirtualBox/VMMR0.r0: SUP_IOCTL_LDR_OPEN failed (VERR_LDR_IMPORTED_SYMBOL_NOT_FOUND).
Failed to load VMMR0.r0 (VERR_LDR_IMPORTED_SYMBOL_NOT_FOUND).
返回 代码:
E_FAIL (0x80004005)
组件:
ConsoleWrap
界面:
IConsole {6ac83d89-6ee7-4e33-8ae6-b257b2e81be8}
这个应该是在升级安装过程中报VirtualBox Interface进程在运行,直接结束任务继续安装导致旧的驱动残留和新版本不兼容导致。解决办法如下:
先卸载VirtualBox,然后去 C:\Windows\System32\drivers 目录下看看是否存在这两个旧的驱动文件:VBoxNetAdp6.sys、VBoxNetLwf.sys,如果有就重启后删除干净,然后重新安装7.2.4即可。
May 24, 2025
支持Xinnet域名APIv2接口的Certbot工具从Let's Encrypt申请免费的通配符SSL证书并自动续期
#by:vitter
#blog.vfocus.net
*.example.com 这种通配符域名证书的好处是可以覆盖一个域名的所有子域名,但是用Cerbot从Let's Encrypt申请的时候必须通过DNS验证方式检查域名所有权。手动申请没什么问题,但是涉及到自动续期就需要自动添加域名解析验证,查了下国内几个大的云厂商域名解析都有相关API及开源脚本插件可以实现Cretbot Hook自动化,但是没看到Xinnet的API接口插件。从https://apidoc.xin.cn/官方文档看了看,自己写了一个。
首先简单说下Certbot申请和自动续期SSL证书这部分。你先要有一个域名(下面以tmd2.com为例)并能进行修改域名解析(要能添加删除DNS的TXT记录,下面以Xinnet域名解析为例);第二要跑WEB服务,下面以Nginx为例;第三要有Xinnet的agent帐号(有这个才能申请API接口)。
一、Certbot申请证书
以docker部署为例,其他安装Certbot可参考官网https://certbot.openssl.ac.cn/
1、手动申请
docker run -it --rm --name certbot -v "/etc/letsencrypt/:/etc/letsencrypt/" certbot/certbot certonly --manual --preferred-challenges dns -d tmd2.com -d *.tmd2.com
Certbot会提示要求你添加一个_acme-challenge.tmd2.com的TXT域名解析记录(随机字符串)验证域名所有权,按要求去域名服务商平台添加解析,稍等片刻后用dig或者nslookup看看生效与否,生效后继续完成申请,Certbot会签发证书。
成功后的提示会有如下类似内容:
Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/tmd2.com/fullchain.pem
Key is saved at: /etc/letsencrypt/live/tmd2.com/privkey.pem
2、自动申请
docker run -it --rm --name certbot -v "/root/xinnetapi/:/root/xinnetapi/" -v "/etc/letsencrypt/:/etc/letsencrypt/" certbot/certbot certonly --manual --preferred-challenges dns --manual-auth-hook "/root/xinnetapi/xinnet_auth.py" --manual-cleanup-hook "/root/xinnetapi/xinnet_cleanup.py" -d tmd2.com -d *.tmd2.com
不需要手动添加域名解析,后面会有相关代码。
3、自动续期
由于签发的证书只有3个月,因此需要在过期前申请续期。
同2一样可以自动续期。也可以加到crontab里面每天或者每周自动执行。
/usr/bin/docker run -i --rm --name certbot -v "/root/xinnetapi/:/root/xinnetapi/" -v "/etc/letsencrypt/:/etc/letsencrypt/" certbot/certbot renew --manual --preferred-challenges dns --manual-auth-hook "/root/xinnetapi/xinnet_auth.py" --manual-cleanup-hook "/root/xinnetapi/xinnet_cleanup.py"
更新证书后记得重启Nginx。放crontab里面的可以加--deploy-hook "service nginx restart" 参数完成自动重启nginx服务(nginx和certbot在同一个系统或者docker容器里)。
*注意:以上certbot命令certonly或renew的时候强烈建议在正式运行前先加--dry-run参数测试。
二、Nginx配置
server {
listen 80;
server_name tmd2.com *.tmd2.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name tmd2.com *.tmd2.com;
ssl_certificate /etc/letsencrypt/live/tmd2.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/tmd2.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
#ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
location / {
proxy_pass http://172.16.1.110:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location ~ /\. {
deny all;
}
}
三、Xinnet API脚本工具
首先你要在Xinnet后台申请域名API管理:开通成功后,请添加接入IP,添加后方可正常使用API。API开通后有效期为1年,需在到期前一个月内进行在线年审。逾期未年审,将限制API使用。
API 账号:agent12345
API 密钥:是一串随机字符串
记得添加白名单IP,就是你执行接口程序的IP。
下面是代码部分:
文件结构
xinnetapi/
├── .env <-- 配置Xinnet的API帐号和密钥
├── cli_dns_log.txt <-- log记录文件
├── cli.py <-- 功能命令行工具
├── logger.py <-- 日志记录器
├── xinnet_auth.py <-- Certbot Auth Hook:用于certbot在验证前添加TXT记录 ( chmod +x )
├── xinnet_cleanup.py <-- Certbot Cleanup Hook:用于certbot验证完成后删除TXT记录 ( chmod +x )
└── xinnet_dns_api.py <-- 新网DNS API 执行 增/查/改/删 记录的类
root@Vfocus:~/xinnetapi# python cli.py
usage: cli.py [-h] {query-domain,list,create,modify,delete} ...
Xinnet DNS CLI 工具
positional arguments:
{query-domain,list,create,modify,delete}
命令
query-domain 查询域名信息
list 查询解析记录
create 添加解析记录
modify 修改解析记录
delete 删除解析记录
optional arguments:
-h, --help show this help message and exit
root@Vfocus:~/xinnetapi# cat cli.py
#by:vitter
#blog.vfocus.net
import argparse
import json
import sys
from xinnet_dns_api import (
query_domain,
query_records,
query_record_unique,
create_record,
modify_record,
delete_record
)
def run():
parser = argparse.ArgumentParser(description="Xinnet DNS CLI 工具")
subparsers = parser.add_subparsers(dest="command", help="命令")
# 查询域名
domain_parser = subparsers.add_parser("query-domain", help="查询域名信息")
domain_parser.add_argument("domain", help="域名,例如 tmd2.com")
# 查询解析记录
list_parser = subparsers.add_parser("list", help="查询解析记录")
list_parser.add_argument("domain", help="域名")
list_parser.add_argument("domain_id", help="域名 ID")
# 创建解析记录
create_parser = subparsers.add_parser("create", help="添加解析记录")
create_parser.add_argument("domain", help="域名")
create_parser.add_argument("name", help="记录名称")
create_parser.add_argument("type", help="记录类型,例如 A、CNAME")
create_parser.add_argument("value", help="记录值")
create_parser.add_argument("line", default="默认", help="线路类型,默认 默认")
create_parser.add_argument("ttl", type=int, default=600, help="TTL 值")
create_parser.add_argument("mx", type=int, default=0, help="MX 优先级")
create_parser.add_argument("status", type=int, default=0, help="状态,0 表示启用")
# 修改解析记录
modify_parser = subparsers.add_parser("modify", help="修改解析记录")
modify_parser.add_argument("domain", help="域名")
modify_parser.add_argument("record_id", help="记录 ID")
modify_parser.add_argument("value", help="新值")
modify_parser.add_argument("ttl", type=int, default=600, help="TTL 值")
modify_parser.add_argument("mx", type=int, default=0, help="MX 优先级")
modify_parser.add_argument("status", type=int, default=0, help="状态")
# 删除解析记录
delete_parser = subparsers.add_parser("delete", help="删除解析记录")
delete_parser.add_argument("domain", help="域名")
delete_parser.add_argument("record_id", help="记录 ID")
args = parser.parse_args()
if args.command == "query-domain":
res = query_domain(args.domain)
print(json.dumps(res, indent=2, ensure_ascii=False))
elif args.command == "list":
res = query_records(args.domain, args.domain_id)
print(json.dumps(res, indent=2, ensure_ascii=False))
elif args.command == "create":
res = create_record(
domain_name=args.domain,
record_name=args.name,
rtype=args.type,
value=args.value,
line=args.line,
ttl=args.ttl,
mx=args.mx,
status=args.status,
)
print(json.dumps(res, indent=2, ensure_ascii=False))
elif args.command == "modify":
res = modify_record(
record_id=args.record_id,
domain_name=args.domain,
value=args.value,
ttl=args.ttl,
mx=args.mx,
status=args.status
)
print(json.dumps(res, indent=2, ensure_ascii=False))
elif args.command == "delete":
res = delete_record(record_id=args.record_id, domain_name=args.domain)
print(json.dumps(res, indent=2, ensure_ascii=False))
else:
parser.print_help()
if __name__ == "__main__":
run()
root@Vfocus:~/xinnetapi# cat logger.py
#by:vitter
#blog.vfocus.net
import logging
import os
LOG_FILE = "cli_dns_log.txt"
os.makedirs(os.path.dirname(LOG_FILE) or ".", exist_ok=True)
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
handlers=[
logging.FileHandler(LOG_FILE, encoding="utf-8"),
logging.StreamHandler()
]
)
def log_info(msg):
logging.info(msg)
def log_error(msg):
logging.error(msg)
def log_debug(msg):
logging.debug(msg)
root@Vfocus:~/xinnetapi# cat xinnet_auth.py
#!/usr/bin/env python3
#by:vitter
#blog.vfocus.net
import os
import sys
from xinnet_dns_api import query_domain, create_record
from logger import log_info, log_error
def main():
certbot_domain = os.environ.get("CERTBOT_DOMAIN")
certbot_validation = os.environ.get("CERTBOT_VALIDATION")
if not certbot_domain or not certbot_validation:
log_error("CERTBOT_DOMAIN 或 CERTBOT_VALIDATION 环境变量未设置。")
sys.exit(1)
# 提取主域名
log_info(f"认证域名: {certbot_domain}, 验证值: {certbot_validation}")
acme_record_name = "_acme-challenge"
# 获取域名信息
domain_info = query_domain(certbot_domain)
if not domain_info or not domain_info.get("data"):
log_error(f"无法查询域名信息: {certbot_domain}")
sys.exit(1)
response = query_domain(certbot_domain)
if not response or "data" not in response:
log_error(f"查询域名失败:{response}")
exit(1)
domain_data = response["data"]
top_domain_name = domain_data.get("name")
domain_id = domain_data["id"]
# 创建 TXT 记录
result = create_record(
domain_name=top_domain_name,
record_name=f"{acme_record_name}.{top_domain_name}",
rtype="TXT",
value=certbot_validation,
ttl=600
)
if result and result.get("code") == "0":
log_info(f"成功添加 TXT 记录: _acme-challenge.{certbot_domain}")
else:
log_error(f"添加 TXT 记录失败: {result}")
sys.exit(1)
# 等待解析生效
import time
time.sleep(25)
if __name__ == "__main__":
main()
root@Vfocus:~/xinnetapi# cat xinnet_cleanup.py
#!/usr/bin/env python3
#by:vitter
#blog.vfocus.net
import os
import sys
from xinnet_dns_api import query_domain, query_records, delete_record
from logger import log_info, log_error
def main():
certbot_domain = os.environ.get("CERTBOT_DOMAIN")
certbot_validation = os.environ.get("CERTBOT_VALIDATION")
acme_record_name = "_acme-challenge"
if not certbot_domain or not certbot_validation:
log_error("CERTBOT_DOMAIN 或 CERTBOT_VALIDATION 环境变量未设置。")
sys.exit(1)
# 获取域名信息
domain_info = query_domain(certbot_domain)
if not domain_info or not domain_info.get("data"):
log_error(f"无法查询域名信息: {certbot_domain}")
sys.exit(1)
response = query_domain(certbot_domain)
if not response or "data" not in response:
log_error(f"查询域名失败:{response}")
exit(1)
domain_data = response["data"]
top_domain_name = domain_data.get("name")
domain_id = domain_data["id"]
# 查询所有记录
records_info = query_records(top_domain_name, domain_id)
if not records_info or not records_info.get("data"):
log_error(f"无法获取 DNS 记录: {certbot_domain}")
sys.exit(1)
# 找出对应的 TXT 记录并删除
records = records_info["data"].get("list", [])
for record in records:
if (record['recordName'] == f"{acme_record_name}.{top_domain_name}" and
record["type"] == "TXT" and
record["value"] == certbot_validation):
log_info(f"删除 TXT 记录: ID={record['recordId']}, 值={record['value']}")
delete_record(record["recordId"], top_domain_name)
break
else:
log_error("未找到需要删除的 TXT 验证记录。")
if __name__ == "__main__":
main()
root@Vfocus:~/xinnetapi# cat xinnet_dns_api.py
#by:vitter
#blog.vfocus.net
import requests
import json
import hmac
import hashlib
from datetime import datetime, timezone
from urllib.parse import urlparse
from dotenv import load_dotenv
import os
from logger import log_info, log_error
load_dotenv()
BASE_URL = "https://apiv2.xinnet.com"
ACCESS_ID = os.getenv("XINNET_ACCESS_ID")
ACCESS_SECRET = os.getenv("XINNET_ACCESS_SECRET")
assert ACCESS_ID and ACCESS_SECRET, "请在.env文件中设置 XINNET_ACCESS_ID 和 XINNET_ACCESS_SECRET"
def _get_utc_timestamp():
return datetime.utcnow().strftime("%Y%m%dT%H%M%SZ")
def _sign_request(method, url_path, body, timestamp):
algorithm = "HMAC-SHA256"
string_to_sign = f"{algorithm}\n{timestamp}\n{method}\n{url_path}\n{body}"
signature = hmac.new(
ACCESS_SECRET.encode("utf-8"),
string_to_sign.encode("utf-8"),
hashlib.sha256
).hexdigest()
auth = f"{algorithm} Access={ACCESS_ID}, Signature={signature}"
return auth
def _post(endpoint, payload, use_cache=False, cache_expire=300):
url = BASE_URL + endpoint
url_path = urlparse(url).path + "/" # 注意尾部的 /
body_str = json.dumps(payload, separators=(",", ":"), ensure_ascii=False)
timestamp = _get_utc_timestamp()
headers = {
"timestamp": timestamp,
"authorization": _sign_request("POST", url_path, body_str, timestamp),
"Content-Type": "application/json"
}
try:
log_info(f"[POST] {url_path} -> {body_str}")
response = requests.post(url, headers=headers, data=body_str.encode('utf-8'), timeout=10)
response.raise_for_status()
result = response.json()
log_info(f"[RESPONSE] {result}")
return result
except Exception as e:
log_error(f"[ERROR] {e}")
return None
# --------------------------------------------
# API 功能封装函数
def query_domain(domain_name, use_cache=True):
return _post("/api/dns/queryDomain", {"domainName": domain_name})
def query_records(domain_name, domain_id, page_no=1, page_size=20):
return _post("/api/dns/queryRecordsPage", {
"domainName": domain_name,
"domainId": str(domain_id),
"pageNo": page_no,
"pageSize": page_size
})
def query_record_unique(domain_name, record_name, rtype, value, line="默认"):
return _post("/api/dns/queryRecordsUnique", {
"domainName": domain_name,
"recordName": record_name,
"type": rtype,
"value": value,
"line": line
})
def create_record(domain_name, record_name, rtype, value, line="默认", ttl=600, mx=0, status=0):
return _post("/api/dns/create", {
"domainName": domain_name,
"recordName": record_name,
"type": rtype,
"value": value,
"line": line,
"ttl": ttl,
"mx": mx,
"status": status
})
def modify_record(record_id, domain_name, value=None, ttl=600, mx=0, status=0):
data = {
"recordId": record_id,
"domainName": domain_name,
"ttl": ttl,
"mx": mx,
"status": status
}
if value:
data["value"] = value
return _post("/api/dns/modify", data)
def delete_record(record_id, domain_name):
return _post("/api/dns/delete", {
"recordId": record_id,
"domainName": domain_name
})
May 21, 2025
May 20, 2025
自动更新证书
#!/bin/bash LOG_FILE="/data/log/certrenew.log" echo > $LOG_FILE exec > >(tee -a "$LOG_FILE") 2> >(tee -a "$LOG_FILE" >&2) set -x echo "start" date # Renew Certificate /usr/bin/docker run -i --rm --name certbot -v "/etc/letsencrypt/:/etc/letsencrypt/" -v "/usr/share/nginx/html/xxx.com/:/var/www/html/" certbot/certbot certonly -n --no-eff-email --email admin@xxxx.com --agree-tos --webroot -w /var/www/html -d xxxx.com,www.xxxx.com # reload nginx date /usr/sbin/service nginx restart echo "end"






