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即可。

Posted by vitter at 2025-10-23 12:39:31 | 评论 (0) | 引用(0) | 分类:

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

图片

1

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"