黑群晖网络配置折腾记


黑群晕的网络配置在网上有很多教程,包括 ipv6 支持,DDNS 配置等,老规矩,本文只讲 要点及遇到过的坑,以备查。

1 为什么不选 IPv4 公网访问方案

两个原因,其一是申请不到 ipv4 地址,其二是确实比较穷。

2 为什么不把群晖做主路由

优点是:

  1. 可以减少接入公网的网络层次,减少上级路由问题带来的服务不可访问的风险。
  2. 可以配置成一个强大的软路由,做端口转发可在子网内支持 IPv4 设备在公网访问。

缺点是:

  1. NAS 上PPPOE 连接网络,再启动虚拟机或者Docker openwrt 系统,网络配置麻烦。
  2. 如果出现问题需要重启机子很麻烦,普通路由的话直接拔电源重启就行,但群晖上还带

有存储,可能破坏数据,NAS 主要还是干该干的事,当然如果能兼职从路由的事当然是极好 的。

虽然缺点不多,但最后一条就够了。

3 使用Docker 实现Aliyun DDNS

黑群晖上跟踪IP变化实现DDNS 的方法至少有3中,我对以下几种方案都有尝试,用虚拟机或 者Docker openwrt虚拟机最耗资源,配置麻烦,直接放弃。

如果想在 NAS 主机上利用 python 脚本跟踪IP变 化,不太现实,安装阿里云的sdk 需要 autoconf 和 gcc 等开发和编译工具,安装这些东 西还可能涉及相关依赖。NAS 本来被设计出来就应该是交叉编译的装个环境是比较麻烦。

但是如果恰好工作电脑比NAS主机 glibc 库版本要低,我们可以使用 nutika 将脚本打包后 上传服务器,设置 crontab 实现DDNS,这个最理想,文件又小,设置又简单。

不过还是使用 docker 运行脚本实现DDNS 比较容易,下载镜像设置几个参数,运行容器, 就OK。

当然在我修改脚本,准备 Docker 镜像的过程还是麻烦的。本来想站在前人的肩上,实现一 下就够了,但已有的两个镜像我这使用不了老是报错。于是自己从头学起,做一个。

4 DDNS 稳定性问题探讨

实验表明,网络设备都不断电的情况下,网络是相当稳定的,不稳定基本上都是由上级路 由或者网关断电后重启造成的。实验发现,上级路由在断电后重新上电的情况下,主机网 卡不重启刷新IP 会造成网络不通情况。

我使用以下办法来最大程度确保NAS网络畅通:

  1. 如果在脚本中实现定时任务,那么异常检查要确保定时检查和更新任务不中断;如果使 用 crontab 设置定时任务,可以确保定时任务的执行。
  2. 脚本中错误检查要全面,必要的时候重启网卡。
  3. 所以减少路由层级可以避免发生在上级路由的意外情况导致网络中断的问题。
  4. 不要将网关和NAS前置路由直接到后备电源上,原因同前所述,就是要保证断电后网络设 备同步重启刷新网卡。假设主网络停电造成路由重启但是家庭网关和路由没有重启,网 络不通的问题就来了。

5 ipv6 aliyun ddns 实现

5.1 docker ipv6 支持

修改NAS主机/usr/syno/etc/packages/Docker/dockerd.json (DSM7.0)增加以下内容,重启 Docker 套件,可以让 Docker 内支持 ipv6 及 dns。虽然DNS 对于我设置 DDNS 并没有卵 用。

"ipv6":true,
"fixed-cidr-v6":"efb0:399:4ac:c114::/64",
"dns":["114.114.114.114","8.8.8.8"]

5.2 运行 Docker 容器关键参数

  1. 使用主机网络即 –net=host 这样在docker 中看到的就是主机的网络情况
  2. 使用特权运行 –privileged=true 这样在 docker 中才可以重启网卡

5.3 docker 安装及镜像构建错误及解决办法

  1. docker 安装总是提示服务启动失败

检查发现安装 docker 要求内核需要开启 layer 文件系统 bridge nf_table 等相关模块。 下载 https://github.com/moby/moby/raw/master/contrib/check-config.sh 并运行检查 内核支持情况。

  1. linux 5.14 构建镜像失败,报 bridge 网络相关问题

回到标准内核就没有问题,同上还是内核少东西出的错,正确配置后重新编译。

  1. EntryPoint 和 CMD 设置错误容器启动失败

参考 Dockerfile 中的 CMD 与 ENTRYPOINT - sparkdev - 博客园 修改Dockerfile 后重新 构建。

对于 CMD 和 ENTRYPOINT 的设计而言,多数情况下它们应该是单独使用的。 • 如果 ENTRYPOINT 使用了 shell 模式,CMD 指令会被忽略。 • 如果 ENTRYPOINT 使用了 exec 模式,CMD 指定的内容被追加为 ENTRYPOINT 指定命令的 参数。 • 如果 ENTRYPOINT 使用了 exec 模式,CMD 也应该使用 exec 模式。

exec 模式的特点是不会通过 shell 执行相关的命令,所以像 $HOME 这样的环境变量是取不到的

5.4 附录

5.4.1 docker 命令

# 重要命令记录,意义同命令
docker command --help

docker search xxx
docker pull xxx
docker run xxx --name abc -it command  args1 args2

docker ps -a
docker rm xxx

docker images xxx
docker image rm xxx
docker save abc.tar xxx -t abc/xyz:lasted

docker start xxx
docker attach xxx

5.4.2 aliyunddns.py

from aliyunsdkcore.client import AcsClient
from aliyunsdkcore.acs_exception.exceptions import ClientException
from aliyunsdkcore.acs_exception.exceptions import ServerException
from aliyunsdkalidns.request.v20150109.DescribeSubDomainRecordsRequest import DescribeSubDomainRecordsRequest
from aliyunsdkalidns.request.v20150109.DescribeDomainRecordsRequest import DescribeDomainRecordsRequest
from aliyunsdkalidns.request.v20150109.DeleteSubDomainRecordsRequest import DeleteSubDomainRecordsRequest
from aliyunsdkalidns.request.v20150109.AddDomainRecordRequest import AddDomainRecordRequest
from aliyunsdkalidns.request.v20150109.UpdateDomainRecordRequest import UpdateDomainRecordRequest
import requests
from   urllib.request import urlopen
import json
import subprocess
import ssl
import certifi
import os
import argparse
import ipaddress
import time
import re
import sys

def getip(ipapi):
    context = ssl._create_unverified_context()
    ip = urlopen(ipapi,context=context).read()
    return str(ip, encoding='utf-8').strip()


def update(RecordId, RR, Type, Value):  # 修改域名解析记录
    request = UpdateDomainRecordRequest()
    request.set_accept_format('json')
    request.set_RecordId(RecordId)
    request.set_RR(RR)
    request.set_Type(Type)
    request.set_Value(Value)
    response = client.do_action_with_exception(request)
    #print(response,flush=True)


def add(DomainName, RR, Type, Value):  # 添加新的域名解析记录
    request = AddDomainRecordRequest()
    request.set_accept_format('json')
    request.set_DomainName(DomainName)
    request.set_RR(RR)
    request.set_Type(Type)
    request.set_Value(Value)
    response = client.do_action_with_exception(request)
    #print(response,flush=True)

def updateRecord(subdomain,recordtype,ip):
    request = DescribeSubDomainRecordsRequest()
    request.set_accept_format('json')
    request.set_DomainName(domain)
    request.set_SubDomain(subdomain+ '.' + domain)
    request.set_Type(recordtype)
    try:
        response = client.do_action_with_exception(request)  # 获取域名解析记录列表
        #print(response,flush=True)
    except Exception as e:
        print("ERROR:",str(e),flush=True)
        return
    domain_list = json.loads(response)  # 将返回的JSON数据转化为Python能识别的

    #print(domain_list)
    #return

    if domain_list['TotalCount'] == 0:
        try:
            add(domain, subdomain,recordtype, ip)
            print("新建域名解析成功",flush=True)
        except Exception as e:
            print("ERROR:",str(e),flush=True)
    elif domain_list['TotalCount'] == 1:
        if domain_list['DomainRecords']['Record'][0]['Value'].strip() != ip.strip():
            try:
                update(domain_list['DomainRecords']['Record'][0]['RecordId'], subdomain,recordtype, ip)
                print("修改域名解析成功",flush=True)
            except Exception as e:
                print("ERROR:",str(e),flush=True)
        else: 
            print("IP地址没变",flush=True)
    elif domain_list['TotalCount'] > 1:
        request = DeleteSubDomainRecordsRequest()
        request.set_accept_format('json')
        request.set_DomainName(domain)
        request.set_RR(subdomain)
        request.set_Type(recordtype)
        try:
            response = client.do_action_with_exception(request)
            #print(response,flush=True)
            add(domain,subdomain, recordtype,ip)
            print("修改域名解析成功",flush=True)
        except Exception as e:
            print("ERROR:",str(e),flush=True)

if __name__=='__main__':
    ap = argparse.ArgumentParser(description='Aliyun DDNS toll args.')
    ap.add_argument('--acid', type=str,required=True,
                help='aliyun accessKeyId')
    ap.add_argument('--acsec', type=str,required=True,
                help='aliyun accessSecret')
    ap.add_argument('--domain', type=str,required=True,
                help='domain name, exp. example.com')
    ap.add_argument('--subnames', type=str,nargs="*",default=['www','@'],
                help="sub domain name list , default ['www','@']")
    ap.add_argument('--iface', type=str,default='eth0',
                help='net card interface, which can be controlled by ifconfig, must run container use --privileged=true and --net=host args')
    ap.add_argument('--redo', type=int,default=300,
                help='rechek after redo second, default 300')

    ap.add_argument('--ipurl', type=str,default='https://api-ipv6.ip.sb/ip',
                help='url from where can get ip address as plain text')
    args = ap.parse_args()
    accessKeyId = args.acid  # 将accessKeyId改成自己的accessKeyId
    accessSecret = args.acsec # 将accessSecret改成自己的accessSecret
    domain =args.domain  # 你的主域名
    names = args.subnames  # 要进行ddns解析的子域名
    iface = args.iface
    redo = args.redo
    ipurl = args.ipurl

    global client
    client = AcsClient(accessKeyId, accessSecret, 'cn-hangzhou')
    def getRecordType(ip):
        if re.compile("^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$").match(ip) :
            recordtype= "A"
        elif re.compile("^([\da-fA-F]{1,4}:){7}[\da-fA-F]{1,4}$").match(ip):
            recordtype= "AAAA"
        else:
            raise Exception("获取IP地址不正确,需检查并重新设置 ipurl")
        return recordtype

    def proc():
        # 查看本机网卡是否可以被访问,可能因为域名解析服务不通畅造成网卡不断重启
        if  subprocess.call(['ping', '-c', '1', domain])!=0:
            cmd_pre = 'sudo ' if userid!='0' else ''
            userid = str(os.popen("id -u").readlines()[0]).strip()
            print('服务器不可访问,可能是上级路由停电等原因,可能需要手动重启网关或者路由,本机现尝试重启网卡...',flush=True)
            try:
                my_cmd = cmd_pre + ' ifdown ' +iface +' && '+cmd_pre + ' ifup ' + iface
                print(os.popen(my_cmd).readlines(),flush=True)
            except Exception as e:
                print('网卡重启失败,可能是iface设置不正确或者没有使用特权运行容器,现忽略错误继续运行,但可能无法成功修改解析记录,仍需人工干预',flush=True)

        try:
            ip=getip(ipurl)
        except Exception as e:
            print('未获取到本机IP地址',str(e),flush=True)
            return
        try :
            recordtype=getRecordType(ip)
        except Exception as e:
            print(str(e),flush=True)
            return
        print("获取到IP地址:{}".format(ip),flush=True)
        for subdom in names:
            print("subdomain:"+subdom,flush=True)
            updateRecord(subdom,recordtype,ip)
    while True:
        proc()
        time.sleep(redo)

5.4.3 Dockerfile

FROM debian:bullseye
COPY sources.list /etc/apt/sources.list
COPY aliyunddns /opt/aliyunddns
RUN apt update &&  apt install iputils-ping ifupdown locales -y 

RUN sed -i -e "s/# zh_CN.UTF-8.*/zh_CN.UTF-8 UTF-8/" /etc/locale.gen && locale-gen

ENV LANG=zh_CN.UTF-8
ENV LANGUAGE=zh_CN.UTF-8
ENV LC_ALL=zh_CN.UTF-8


WORKDIR /opt/aliyunddns

ENV IPURL       https://api-ipv6.ip.sb/ip
ENV ACCESSID    AabdSdsfdfdhlkHT1jk 
ENV ACCESSKEY   dslfkjlgjlkjdsflkhsdHSfdlj
ENV DOMAIN      example.cn
ENV SUBNAMES    @ www
ENV HARTBEAT    300
ENV INTERFACE   eth0

CMD /opt/aliyunddns/aliyunddns --ipurl $IPURL --acid $ACCESSID --acsec $ACCESSKEY --domain $DOMAIN --subnames $SUBNAMES --redo $HARTBEAT --iface $INTERFACE


本作品采用知识共享署名-非商业性使用-禁止演绎 3.0 未本地化版本许可协议 进行许可。

posted on 2022-08-12 16:13  YourTech-WuPeng  阅读(1899)  评论(0编辑  收藏  举报

导航