分类: 未分类

  • 解决Windows Server环境下有线网卡驱动安装失败

    因为某些原因,我需要在一个小型家用主机(联想MXXX系列)上安装Windows Server系统(2025),大部分硬件设备都能正常运行,唯独有线网卡(后面确定是Intel Ethernet Connection I219-V)无法正常工作。首先,在设备管理上无法看到网卡的生产厂商信息,即使是AIDA工具也没法看到,因此一开始我甚至不知道应当下载哪个驱动,后面采用了hwinfo等工具才最终确定。然而即使知道了型号也没用,采用主流的驱动管理工具、联想官方的电脑管家或者直接到Intel官网下载驱动都无法解决问题。

    一开始我怀疑是网卡本身坏掉了,但是采用Live CD启动的Linux系统(Ubuntu)时,网卡可以正常工作,这说明网卡本身没有问题。确定不是硬件问题后,我开始怀疑是Windows Server系统或者驱动的问题。经过多次尝试和搜索,最终我发现问题出在Intel上,原因是Intel为了商业利益,限制了某些个人电脑的网卡在Windows Server系统上的驱动支持。也就是说,实际上Intel的驱动程序能完美运行在WinServer上,但由于Intel对驱动的限制,导致无法正常安装和使用。

    理解了问题的关键之后,我开始寻找解决方案,根据这篇文章的提示,我最终成功解决问题。具体的内容可以参考文章链接,我这里只是简单描述一下关键过程。

    1. 通过Windows Server的设备管理器,找到Intel I219-V网卡,在详细信息或者事件选项卡中找到网卡的“设备实例路径”,即类似于PCI\VEN_8086&DEV_15B8&SUBSYS_380217AA&REV_00\3&11583659&0&FE 的字符串。这段字符串中有两个信息比较重要:VEN_8086表示Intel的厂商ID,DEV_15B8表示网卡的设备ID。在谷歌上搜索这个字符串,可以找到对应的设备名称。

    2. 在Intel的官方网站上,找到对应的驱动程序下载页面。通常情况下,Intel会提供一个通用的驱动包,里面包含了多个设备的驱动。我们首先需要确定修改的inf文件的位置,一般来说PRO1000文件夹是千兆网卡的驱动文件夹,而PRO2500文件夹是2.5G网卡的驱动文件夹。找到文件夹后可以穷举里面的inf文件,直到找到包含VEN_8086&DEV_15B8的inf文件。

    3. 最关键的步骤是修改这个inf文件。打开找到的inf文件,搜索VEN_8086&DEV_15B8,然后往前翻找,找到类似于[Intel.NTamd64]的部分。这一部分定义了设备的兼容性和驱动安装信息。比如说对于[Intel.NTamd64.10.0.1],最末尾的1代表家用或者个人电脑的驱动,删掉这个1,改为[Intel.NTamd64.10.0],这样就可以让驱动程序在Windows Server系统上安装。

    4. 此时还不能直接使用这个修改后的inf文件,因为Windows Server系统会对驱动进行签名验证。我们可以通过进入Windows Server的安全模式,或者使用一些工具来禁用驱动签名验证。具体方法可以参考网上的教程,通常是在启动时按F8键,然后选择“禁用驱动签名强制”。

    5. 保存修改后的inf文件,然后回到设备管理器,右键点击Intel I219-V网卡,选择“更新驱动程序”。选择“浏览计算机以查找驱动程序软件”,然后指向刚才修改过的inf文件所在的文件夹。Windows Server会识别到修改后的驱动并进行安装。

    最终,经过以上步骤,Intel I219-V网卡在Windows Server系统上成功安装并正常工作。只能说这个问题在一开始让我非常困惑,因为我没有想到是Intel的商业限制导致的。希望这篇博客能帮助到遇到类似问题的朋友们,节省一些时间和精力。

  • CentOS 7 与 Windows 10 双系统安装

    因为某项目的需要,我被要求在一个工作站上安装 CentOS 7 和 Windows 10/11 双系统。考虑到自从研究生毕业先是参加工作后是重回学校读博,很久没有搞过太应用的东西了,而且印象中 Linux 和 Windows 10 系统两者安装起来并不是那么的一帆风顺,因此考虑认真看看具体的步骤。经过一下午的尝试和研究,发现整个安装过程中还是收获蛮大的,遂考虑将这一经历整理一下,供后续自己遇到类似问题能更快结果,当然如果能帮助到其他读者那我更是倍感欣慰。

    XFS 并不支持缩小空间!

    首先,在网上找到一个相关的教程非常容易,尤其是这种 Linux 和 Windows双系统混装的场景更是非常常见,我没花多少功夫便找到一个非常详尽的博客:centos7中安装win10搭建双系统

    由于我在开始着手这一工作时还没有拿到最终需要安装的机器,因此我考虑直接在一个 VMware Workstation 的虚拟机中安装测试。首先是比较顺利的下载CentOS 7 安装 ISO,导入到 VMware Workstation 中安装,所有配置均选择默认选项,磁盘分配了 100 GB 。因为我想完整模拟从一个仅安装了 CentOS 7 系统的机器到双系统的过程,所以我在磁盘配置里选择让 CentOS 安装程序自动为我分区。

    按照博客内容,我应该在第二步的时候将 100 GB 的空间缩小,然后把空余的空间分配到 Windows。然而,我在这一步就首先遇到了问题:CentOS 安装程序默认情况下会采用 LVM 的存储管理方法,一共分配了 3 个逻辑卷(Logical Volume),分别是 swap、root和 home,其中 root 和 home 采用 xfs 文件系统。结果当我用 gparted LiveCD 引导 gparted 图形程序打算缩小 LVM 分区时,发现完全无法缩小。使用命令行 lvreduce --resizefs -L -50G centos 命令手动缩放(https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/6/html/logical_volume_manager_administration/lv_reduce#LV_reduce),发现直接报错file system reduce is required and not supported (xfs),即xfs 不能执行 reduce 操作。因为对 xfs 并不是那么了解(经常用 Ubuntu/Debian 这类发行版的更多的和 ext4/3 等文件系统接触更多),我搜索了相关资料(https://unix.stackexchange.com/questions/619099/why-cant-we-reduce-xfs-filesystem),发现结论是

    XFS 并不支持缩小空间!

    之所以没有这样做,是因为基本上没有缩小大型文件系统的需求。存储很便宜,而且在大多数环境中,数据集和容量只会增长。

    大致意思就是说,xfs 的开发人员认为目前而言磁盘空间应该不是太大问题了,所以他们并不是很愿意增加这个功能,虽然按照 StackOverflow 的说法后续的一些 xfsutils 工具提供了体验版的缩小功能,但是我看到这个回复的时间(2021 年)后我决定直接放弃这一想法了,因为我只是做测试环境,环境应当是完全可控的。但是这里就留下来一个未来需要考虑的问题,假如**碰到那种xfs 占据了整个硬盘的系统该如何解决呢?**将所有文件全部备份再安装到一个新系统里固然可行,但是非常繁琐麻烦,如果以后有时间的话可以看看后续的新工具是否有效。

    Resizefs 选项在很多场景下不可用

    我决定绕过 xfs 的这一障碍,因此我开始重新安装了 CentOS 系统,这次我选择先让 CentOS 安装程序默认为我规划一个分区过程,然后我手动删除了 home 逻辑区,用 root 做 Linux 的根目录。安装过程非常顺利,anaconda 提供的安装程序界面也非常简洁美观。安装完毕后我顺利打开了 gparted LiveCD,结果发现 LVM 管理的磁盘分区仍然不支持直接用 gparted 来缩小,所以这次我先运行 lvreduce 命令,结果再一次碰到了问题:

    /usr/libexec/lvresize_fs_helper: execvp failed: No such file or directory
    

    看起来是缺少某个具体做 resize 工作的可执行文件,如果是在联网且系统安装完毕的环境中这个问题应该不大,但是在目前这个 LiveCD 的环境下面对这个问题是有些棘手的。我认为这个问题应当是 lvm 软件包可能缺失了一些非必需文件,讲道理不应当在这种一应俱全的 LiveCD 里出现。直接搜索这一问题有很多解决方案,可见这个问题并不是一个罕见问题,多数需要重新安装包或者做一些 patch 工作等,但是显然这些在 LiveCD 里执行太麻烦了,好在我在某个 maillist 下找到了最快的解决方案(https://bugs-devel.debian.org/cgi-bin/bugreport.cgi?bug=1065606):使用 –fs resize_fsadm 而不是 –resizefs。

    VMware SCSI 控制器大敌

    完成了 CentOS 7 的安装,也为 Windows 系统留足了存储空间,只要完成Windows 安装这一最简单的步骤,是否很快就能开始做引导调整这一核心工作了呢?答案是否定的,因为一直到这里我才了解到一个收获最大的难点。

    按照步骤打开Windows 安装程序,选择刚才分配好的 NTFS 分区安装!界面马上坡上冷水:

    嗯?这什么情况?硬盘没了?我立马重新引导 CentOS,结果一切正常啊,那看来问题应该不在硬盘这里。搜索下 Vmware、Windows、no drive等关键词,一些链接提到 ESXi下安装 Windows 可能出现这一情况,且最终答案是 SCSI 控制器的类型问题(https://answers.microsoft.com/en-us/insider/forum/all/win-10-doesnt-seem-to-install-on-vmware-it-doesnt/d277bcc1-9edf-431d-bb05-dd940ab7dbe3),但是没有找到和 Workstation 有关的。这时候我突然想到,因为我这次使用虚拟机的逻辑和往常有很大不同,一般而言虚拟机在安装后操作系统就已经确定了,一般也没有人会选择在虚拟机里装一个双系统,会不会是因为VMWare 给 CentOS 选择的支持的 SCSI 控制器和 Windows 并不那么无缝兼容(比如需要借助一些驱动程序),因此导致我换成 Windows 安装程序后无法读到原有的磁盘。经过一番查找我终于发现了https://www.nakivo.com/blog/scsi-controller-and-other-vmware-controller-types/这篇帖子,到这里我终于明白了,VMware Workstation 默认给 CentOS 7 准备的 SCSI 控制器是LSI Logic Parallel,但是 Windows 系统在 2008 版本后最佳支持的是LSI Logic SAS,这就是为什么找不到磁盘的原因。由于 ESXI 同样作为 VMware 的虚拟化产品,所以我猜想类似的解决方案应当能直接应用在 Workstation 上。最直接的解决方法当然是用额外的驱动找到原有的 SCSI 控制器(可参考这个帖子https://blog.csdn.net/weixin_42376940/article/details/113322604 ,虽然他是缺少RST 驱动),但是这样做要麻烦很多,由于在测试环境下我采用直接更改 SCSI 控制器类型的方法,由于Workstation 的管理界面并不支持直接指定类型,我直接修改vmx 文件中的scsi0:0.deviceType,根据博客信息:

    • buslogic – BusLogic SCSI
    • lsilogic – LSI Logic SCSI
    • lsisas1068 – LSI Logic SAS
    • pvscsi – VMware Paravirtual SCSI

    所以从 lsilogic 改成 lsisas1068。修改后重新开机,完美!Windows 安装程序已经能够正常运作了,但是这里就会留下另一个隐患:CentOS7 那边是不是就不能正常引导了呢?暂时不考虑这个问题,我们先按部就班把双系统引导工作做了。

    按照双系统安装博客的内容,安装好 Windows 后果然 CentOS 的引导被 Windows 覆盖了,此时系统只能进入 Windows。我重新用 CentOS 7 LiveCD 引导,选择 Troubleshooting — Rescue a CentOS System选项,让 LiveCD 自动找到CentOS 安装位置并挂载到/mnt/sysimage 目录下,然后就是chroot 操作切换到根,执行 grub2-install 和 grub2-mkconfig 操作,将 grub 和 initramdisk 问题解决。操作完成后,此时只看到了 CentOS的两个启动项,Windows 启动项没有出现,这个问题先放一下,我们先进入 CentOS 来看看。

    结果果然没那么简单,点击 CentOS 后系统迟迟没有进入,等待很久后出现了报错:

    dracut-initqueue[1697]: Warning: dracut-initqueue timeout - staring timeout scripts
    dracut-initqueue[1697]: Warning: Could not boot. 
    dracut-initqueue[1697]: Warning: dev/centos/root does not exist 
    dracut-initqueue[1697]: Warning: dev/centos/swap does not exist
    Starting Dracut Emergency Shell...
    

    也就是说,本应存在的两个LVM 分区找不到了。不算太出乎预料,我猜测应当是因为我改了 SCSI 设备的原因,导致 CentOS 已有的驱动并不能识别出来LSI Logic SAS。经过一番搜索,我找到了类似情况https://blog.csdn.net/Micha_Lu/article/details/125063992,但是帖子里对/etc/dracut.conf 文件进行了修改,然而在我的 Dracut 这个紧急情况终端下(我猜测是只有 initramfs ),根本没有/etc/、/boot 这些目录的挂载,甚至也没有大部分终端命令,只能用 journalctl 看看崩溃日志等等。

    因此这里我想到了,如果我进入到刚才用 LiveCD 引导后找到的根目录,chroot 后再执行这些功能应当可行。这样操作后我确实能够直接访问根系统了,但是发现没有/etc/dracut.conf 文件,这个我认为可能是发行版的差异问题。我在https://serverfault.com/questions/296015/change-vmware-scsi-in-redhat 这里终于知道了最终解决方案,帖子里提到 RHEL 7.0(猜想和 CentOS 7 仅仅有商业版和社区版的差别)没法通过 modprobe.conf 来应用驱动,所以应当用dracut -f -v --add-drivers mptsas 命令实现驱动在 initramfs加载,如此操作后问题终于得以解决,重启后 CentOS 7 成功引导。

    最为简单的双系统引导

    结果,我认为可能是最复杂的一个步骤反而没有遇到太大问题。成功进入 CentOS 后确实无论怎么执行 grub-install 和 grub-mkconfig 都没办法自动找到 Windows,因此我根据双系统安装帖子的方法,手动编辑/etc/grub.d/40_custem 文件,根据引导方式(BIOS 还是 UEFI)来决定添加内容。

    UEFI

    menuentry 'Windows 10' { 
      insmod part_gpt 
      insmod ntfs 
      set root='UUID=YOUR_WINDOWS_PARTITION_UUID' 
      chainloader EFI/Microsoft/Boot/bootmgfw.efi
    }
    

    UUID 可以通过 blkid 命令找到指定分区。

    BIOS

    menuentry 'Windows 10' {
      insmod part_msdos
      insmod ntfs
      set root='hd0,msdos3'
      chainloader +1
    }
    

    hd0,msdos3 指/dev/sda3,如果是其他分区则修改数字即可。

    自定义 Windows 位置后执行grub2-mkconfig -o /boot/grub2/grub.cfg 即可。

    结语

    没有想到双系统本身需要做的引导工作并不多,大部分时间竟然花在了测试环境所使用的SCSI 驱动问题上,不过总的来说还是收获不少的😄。

  • Go语言项目的组织方式

    构建方式

    最好保持传统使用make构建,避免新构建工具的构建成本

    Lint

    golangci-lint,并开启所有默认的linter规则

    文档

    doc.go存放该包的一般描述 如果一个文件放不下,那么可以准备一个docs文件夹

    Readme文件

    • 项目目标
    • 快速入门部分:开始处理项目时应该做的事情,任何依赖项,基本示例,描述项目的外部链接

    包组织方式

    最推荐的方法:在不被迫添加新包的前提下,直接将整个代码保存在根目录下,避免循环依赖 创建新包的理由:

    1. 当你有不止一种启动应用程序的方式,例如Web API和CLI共存的时候,创建一个/cmd包并包含cliweb子包,并在运行时指定到文件go run ./cmd/cli
    2. 当你想提取更详细的实现时
    3. 当你开始为密切相关的事物添加公共前缀时
    r := networkReader{}
    //
    r := network.Reader{}
    

    模块化

    两种组织方法

    1. 按种类组织
    .
    |-- handlers
    |   |-- course.go
    |   |-- lecture.go
    |   |-- profile.go
    |   |-- user.go
    |-- main.go
    |-- models
    |   |-- course.go
    |   |-- lecture.go
    |   |-- user.go
    |-- repositories
    |   |-- course.go
    |   |-- lecture.go
    |   |-- user.go
    |-- services
    |   |-- course.go
    |   |-- user.go
    |-- utils
        |-- strings.go
    

    缺点:每个类型、常量或函数都必须是公开的,才能在项目的另一部分访问;在写代码或者debug的时候,经常需要在多个包之间寻找问题。

    1. 按组件组织
    .
    |-- course
    |   |-- httphandler.go
    |   |-- model.go
    |   |-- repository.go
    |   |-- service.go
    |-- main.go
    |-- profile
        |-- httphandler.go
        |-- model.go
        |-- repository.go
        |-- service.go
    

    缺点:容易发生相互依赖的问题 例如,想在用户的个人资料上显示最近的课程,从个人资料的角度上来看,课程是一种外部依赖,解决方法是在profile下创建一个接口。

    type Courses interface {
        MostRecent(ctx context.Context, userID string, max int) ([]course.Model, error)
    }
    

    然后在course包中,公开实现该接口的服务

    type Courses struct {
        // 一些不能导出的变量
    }
    
    func (c Courses) MostRecent(ctx context.Context, userID string, max int) ([]Model, error) {
        // 实现逻辑
    }
    

    最后,在main.go中从course包中创建Courses实例并传递给profile包,便可以在不引发循环依赖的情况下实现该功能。此外,写profile的测试时,可以直接mock创建一个实现,甚至可以在没有course实现包的情况下开发和测试profile的功能。

    简洁架构

    应用程序或模块有4层,分别是Domain、Application、Ports、Adapters。

    • Domain:业务核心,没有外部依赖,不应该知道代码是在哪个上下文执行的
    • Application:应用程序用法的粘合点,所有Domain、Ports、Adapter的逻辑串联位置
    • Ports: 用户输入,获取信息后传入给Application
    • Adapters: 与外部通信,存储和获取数据,对低层细节的抽象。
  • Consul 学习

    consul agent -dev启动一个调试用的Consul agent,该agent运行在Server模式下。使用 <kbd>Ctrl</kbd> + <kbd>C</kbd>

  • 通过WIM的方式控制Windows

    首先需要开启windows中WIM服务,并且授权所有用户都能够进行访问(HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\policies\system中LocalAccountTokenFilterPolicy设置成1)。

    其次,如果要支持psexec,还需要开启C盘的文件共享

    如果在linux下,使用到的是impacket里的psexec和wimexec工具。(使用时注意windows的防火墙和Windows defender)

    clone下来后在examples里面找到相关工具,不过首先需要先pip install这个包。

    使用方法参考: https://www.freebuf.com/sectool/175208.html https://blog.csdn.net/qq_37077262/article/details/103979530

    此外,可以在windows中直接用wbemtest工具来检查WIM的相关状况。

  • Win10 高版本不支持SNMP的解决方法

    参考:snmp-service-on-windows-10

    Win10 1809版本之后直接去掉了SNMP的支持,改成用自家的CIM模型来配置Windows Remote Management。如果仍然需要SNMP的配置,那么需要使用FoD(features on demand)的方法来安装。

    在管理员权限的powershell里输入

    Add-WindowsCapability -Online -Name "SNMP.Client~~~~0.0.1.0"
    

    或者是DISM的方式

    DISM /online /add-capability /capabilityname:SNMP.Client~~~~0.0.1.0
    

    检查是否安装成功

    Get-WindowsCapability -Online -Name "SNMP*"
    

    卸载方式

    Remove-WindowsCapability -Online -Name "SNMP.Client~~~~0.0.1.0"
    

    为了让SNMP能够访问,还需要进行相关的配置

  • 使用VMware vctl工具创建K8s集群

    vctl是VMware最新开发的一个CLI工具,主要是用来创建满足OCI标准的容器。vctl包含在VMware Workstation Pro 16 和 VMware Fusion 12以上版本中。

    使用vctl

    首先在使用vctl之前,我们首先需要知道,该工具只是一个管理前端,真正的容器操作是由守护进程来实现的。因此在使用该命令之前,我们要首先检查守护进程的状态(默认情况下是关闭的)。vctl的容器运行时包括三个文件:

    • bin/containerd:这个进程就是用来执行容器相关指令的,检查方法是vctl system info,开启方法是vctl system start,结束方法是vctl system stop
    • bin/containerd-shim-crx-v2:看到shim这个词,应该能够从docker联想到是用来作为容器根进程的,工作在CRX VM的容器和containerd进程之间。
    • bin/vctl:这个就是我们上面提到的前端管理工具。

    上面我们提到了CRX VM,这个其实就是VMware专门用来运行容器的轻量级虚拟机,只要有一个容器创建那么就会开启一个CRX VM,同样删掉这个容器这个VM就会停止并删除。容器名和CRX VM的名字是一样的。

    vctl的基本命令和docker完全一致,这也就是说,ps/run/start/stop/rm/rmi/build/create等命令都可以使用相同的方式使用。这里作为示例,给出一个nginx容器的创建过程。

    vctl pull nginx
    vctl run --name=myNginx -t -p 8888:80 -d nginx
    vctl ps -a
    vctl stop nginx
    vctl rm nginx
    vctl rmi nginx
    

    可以从任务管理器上看到相关进程的启动情况。

    进程图

    vctl的详细使用方法可以参考VMware的官方文档

    创建k8s集群

    从上面我们可以看到,vctl提供了类似于docker的容器管理功能,那么现在要创建k8s集群,只需要使用类似kubeadm的工具创建多个容器构成k8s的运行环境即可。这其中,KIND就是一个比较好用的k8s安装工具。从VMware Fusion 12开始,vctl工具提供了KIND的支持。使用方法也非常简单,只需要运行vctl kind即可。这条命令会做四件事情:

    • 在用户目录下创建一个.vctl文件夹

    • 下载kubectlkindcrx.vmdk三个文件,保存在bin目录下

    • 创建一个名为docker的链接文件,指向vctl

    • 创建一个基于vctl的KIND上下文,主要是将~/.vctl/bin文件夹加入到PATH环境变量中

    运行这条命令后,会弹出一个新的终端,这个终端上的kubectl、kind、docker等命令均指向的是刚才安装的KIND上下文环境,而不是原先系统安装的。

    需要注意的是,vctl并不支持kind buildkind export logs命令,且CRX VM的默认配置是2G 2C。

    现在已经有了kind的环境了,剩下的事情就交给kind了。KIND文档

    最简单的命令,kind create cluster

    C:\Program Files (x86)\VMware\VMware Workstation\bin>kind create cluster
    Creating cluster "kind" ...
     • Ensuring node image (kindest/node:v1.18.2) ?  ...
     ✓ Ensuring node image (kindest/node:v1.18.2) ?
     • Preparing nodes ?   ...
     ✓ Preparing nodes ?
     • Writing configuration ?  ...
     ✓ Writing configuration ?
     • Starting control-plane ?️  ...
     ✓ Starting control-plane ?️
     • Installing CNI ?  ...
     ✓ Installing CNI ?
     • Installing StorageClass ?  ...
     ✓ Installing StorageClass ?
    Set kubectl context to "kind-kind"
    You can now use your cluster with:
    
    kubectl cluster-info --context kind-kind
    
    Thanks for using kind! ?
    

    使用kubectl cluster --context kind-kind可以看看刚刚创建的k8s集群状态。

    C:\Program Files (x86)\VMware\VMware Workstation\bin>kubectl cluster-info --context kind-kind
    Kubernetes master is running at https://127.0.0.1:64039
    KubeDNS is running at https://127.0.0.1:64039/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
    
    To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.
    
    

    此时,kubectl get nodeskubectl get pods等命令已经可以正常使用了,我们已经使用vctl创建了一个单节点的k8s环境。

    看看原理

    安装过程到这里就差不多了,更复杂的k8s集群只需要在kind中传入自定义的配置文件即可。如果这个时候我们使用docker ps命令查看真正用来承载k8s集群的容器,就会发现实际上只有一个容器在运行。

    C:\Program Files (x86)\VMware\VMware Workstation\bin>docker ps -a
    ────                 ─────                                                                                  ───────                   ──                ─────            ──────    ─────────────
    NAME                 IMAGE                                                                                  COMMAND                   IP                PORTS            STATUS    CREATION TIME
    ────                 ─────                                                                                  ───────                   ──                ─────            ──────    ─────────────
    kind-control-plane   kindest/node@sha256:7b27a6d0f2517ff88ba444025beae41491b016bc6af573ba467b70c5e8e0d85f   /usr/local/bin/entry...   192.168.100.132   64039:6443/tcp   running   2020-09-17T16:50:22+08:00
    

    是不是感觉很奇怪,因为如果你尝试过其他的安装方法,应该会有印象,那就是k8s的运行组件相当多,控制节点的apiServerschedulercontroller-manager以及计算节点上kubelet这些在其他安装方法中一般都是由独立的容器来运行的,那么在这里为什么会只有一个呢?

    通过查阅KIND文档可以发现,原来KIND使用node-image来运行这些组件,例如kubeadmkubelet等等,而这些运行依赖和node-image本身则是由base-image来生成的。生成过程我们可以不用关心,现在让我们主要来看看node-image这个镜像生成的单个容器是如何运行所有的k8s组件。

    使用docker exec -it kind-control-plane /bin/bash进入到容器中,首先ps看看运行的进程。

    root@kind-control-plane:/kind# ps ajxf
     PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
        0  6178  6178  6178 pts/3     6913 Ss       0   0:00 /bin/bash
     6178  6913  6913  6178 pts/3     6913 R+       0   0:00  \_ ps ajxf
        0     1     1     1 ?           -1 Ss       0   0:00 /sbin/init
        1   104   104   104 ?           -1 S<s      0   0:00 /lib/systemd/systemd-journald
        1   113   113   113 ?           -1 Ssl      0   0:18 /usr/local/bin/containerd
        1   311   311   113 ?           -1 Sl       0   0:00 /usr/local/bin/containerd-shim-runc-v2 -namespace k8s.io -id 00d79d58327ca059b96cdd9c651d12853a2e2622e65472230414dc15d65821bd -address /run/containerd/con
      311   405   405   405 ?           -1 Ss       0   0:00  \_ /pause
      311   507   507   507 ?           -1 Ssl      0   0:38  \_ kube-controller-manager --allocate-node-cidrs=true --authentication-kubeconfig=/etc/kubernetes/controller-manager.conf --authorization-kubeconfig=/etc
        1   322   322   113 ?           -1 Sl       0   0:00 /usr/local/bin/containerd-shim-runc-v2 -namespace k8s.io -id 482207771a3b7eba237dc037dba4f5717e54842f7bf38d367f391030a9091f38 -address /run/containerd/con
      322   401   401   401 ?           -1 Ss       0   0:00  \_ /pause
      322   547   547   547 ?           -1 Ssl      0   1:56  \_ kube-apiserver --advertise-address=192.168.100.132 --allow-privileged=true --authorization-mode=Node,RBAC --client-ca-file=/etc/kubernetes/pki/ca.crt
        1   323   323   113 ?           -1 Sl       0   0:00 /usr/local/bin/containerd-shim-runc-v2 -namespace k8s.io -id 5101a9a24071e5c10d052d01918361dee8a97a2822b4bb82bc772460ce0045a2 -address /run/containerd/con
      323   399   399   399 ?           -1 Ss       0   0:00  \_ /pause
      323   586   586   586 ?           -1 Rsl      0   0:47  \_ etcd --advertise-client-urls=https://192.168.100.132:2379 --cert-file=/etc/kubernetes/pki/etcd/server.crt --client-cert-auth=true --data-dir=/var/lib/
        1   324   324   113 ?           -1 Sl       0   0:00 /usr/local/bin/containerd-shim-runc-v2 -namespace k8s.io -id e68a9f43205edd282548f6db0942c0c0b503a66894b8bf5e3d7d1b7e5be8a71d -address /run/containerd/con
      324   404   404   404 ?           -1 Ss       0   0:00  \_ /pause
      324   500   500   500 ?           -1 Ssl      0   0:10  \_ kube-scheduler --authentication-kubeconfig=/etc/kubernetes/scheduler.conf --authorization-kubeconfig=/etc/kubernetes/scheduler.conf --bind-address=127
        1   632   632   632 pts/0      632 Ssl+     0   0:50 /usr/bin/kubelet --bootstrap-kubeconfig=/etc/kubernetes/bootstrap-kubelet.conf --kubeconfig=/etc/kubernetes/kubelet.conf --config=/var/lib/kubelet/config.
        1   810   810   113 ?           -1 Sl       0   0:00 /usr/local/bin/containerd-shim-runc-v2 -namespace k8s.io -id 6bb16a0dfa1b006c0c241d54f84c8fc17d719ca791f482ede021e0a9ae118eec -address /run/containerd/con
      810   860   860   860 ?           -1 Ss       0   0:00  \_ /pause
      810   928   928   928 ?           -1 Ssl      0   0:01  \_ /usr/local/bin/kube-proxy --config=/var/lib/kube-proxy/config.conf --hostname-override=kind-control-plane
      928   986   983   928 ?           -1 Z        0   0:01      \_ [iptables-nft-sa] <defunct>
        1   837   837   113 ?           -1 Sl       0   0:00 /usr/local/bin/containerd-shim-runc-v2 -namespace k8s.io -id e17005058a2afb322c2c9179d5468b65be0bd8c2a24e3774373988413c93a7ec -address /run/containerd/con
      837   877   877   877 ?           -1 Ss       0   0:00  \_ /pause
      837   935   935   935 ?           -1 Ssl      0   0:00  \_ /bin/kindnetd
      935   994   992   935 ?           -1 Z        0   0:00      \_ [iptables-nft-sa] <defunct>
        1  1108  1108   113 ?           -1 Sl       0   0:00 /usr/local/bin/containerd-shim-runc-v2 -namespace k8s.io -id 905595d932140fd9406ae9487893d6e6351b15f33ab39528a4da00650df485ec -address /run/containerd/con
     1108  1131  1131  1131 ?           -1 Ss       0   0:00  \_ /pause
     1108  1160  1160  1160 ?           -1 Ssl      0   0:03  \_ local-path-provisioner --debug start --helper-image k8s.gcr.io/debian-base:v2.0.0 --config /etc/config/config.json
        1  1244  1244   113 ?           -1 Sl       0   0:00 /usr/local/bin/containerd-shim-runc-v2 -namespace k8s.io -id 410e09a86cb001d1b40378e153406d7a1c85c9f04f3e68ded23dc19c1fe50940 -address /run/containerd/con
     1244  1305  1305  1305 ?           -1 Ss       0   0:00  \_ /pause
     1244  1380  1380  1380 ?           -1 Ssl      0   0:08  \_ /coredns -conf /etc/coredns/Corefile
        1  1252  1252   113 ?           -1 Sl       0   0:00 /usr/local/bin/containerd-shim-runc-v2 -namespace k8s.io -id 0fdc07bfbd62610b783122e3d8dca8f5b1ad2a1aa07f2bb7b1194df280116957 -address /run/containerd/con
     1252  1294  1294  1294 ?           -1 Ss       0   0:00  \_ /pause
     1252  1374  1374  1374 ?           -1 Ssl      0   0:08  \_ /coredns -conf /etc/coredns/Corefile
    

    那么从这里看就可以看出来KIND的构建方法了,在kind-control-plane这个容器中运行由kubelet程序用来支持一个运行节点,控制节点所需要的组件则是由内置的一个containerd来运行的,使用ctr -n k8s.io containers ls命令可以更加直观的看到。

    root@kind-control-plane:/kind# ctr -n k8s.io containers ls
    CONTAINER                                                           IMAGE                                               RUNTIME
    00d79d58327ca059b96cdd9c651d12853a2e2622e65472230414dc15d65821bd    k8s.gcr.io/pause:3.2                                io.containerd.runc.v2
    07dd95f8b88f11974154232d894e1ac9be76dec8c7e88c74a5d7f611d4bf94a0    k8s.gcr.io/kube-controller-manager:v1.18.2          io.containerd.runc.v2
    0fdc07bfbd62610b783122e3d8dca8f5b1ad2a1aa07f2bb7b1194df280116957    k8s.gcr.io/pause:3.2                                io.containerd.runc.v2
    410e09a86cb001d1b40378e153406d7a1c85c9f04f3e68ded23dc19c1fe50940    k8s.gcr.io/pause:3.2                                io.containerd.runc.v2
    470d408aa74f66355ab637295a8e4777ec6775f84aba75a17efa861e5ede6470    k8s.gcr.io/kube-scheduler:v1.18.2                   io.containerd.runc.v2
    482207771a3b7eba237dc037dba4f5717e54842f7bf38d367f391030a9091f38    k8s.gcr.io/pause:3.2                                io.containerd.runc.v2
    5101a9a24071e5c10d052d01918361dee8a97a2822b4bb82bc772460ce0045a2    k8s.gcr.io/pause:3.2                                io.containerd.runc.v2
    652f25e9876c8efaae6b21f7289bfd88372ce9ec4ae6593f9e1c81bb3f513ddd    k8s.gcr.io/etcd:3.4.3-0                             io.containerd.runc.v2
    6bb16a0dfa1b006c0c241d54f84c8fc17d719ca791f482ede021e0a9ae118eec    k8s.gcr.io/pause:3.2                                io.containerd.runc.v2
    7f9f6cd14c83c08f5906a280f7fc5b178e766c0e4bb137a316d35c61f05c5031    k8s.gcr.io/kube-apiserver:v1.18.2                   io.containerd.runc.v2
    905595d932140fd9406ae9487893d6e6351b15f33ab39528a4da00650df485ec    k8s.gcr.io/pause:3.2                                io.containerd.runc.v2
    c90fa3e64763263042ac6f29657e3a76de67a90308b9c7cf3a5cefbeda6788c8    k8s.gcr.io/kube-proxy:v1.18.2                       io.containerd.runc.v2
    d0c86ec34d96251e5bc4992183b21021c803e033463f581de9adf3bdadd4b4c8    k8s.gcr.io/coredns:1.6.7                            io.containerd.runc.v2
    d43682f949020c15ff5a7997bfbd872fd6959949962e57e9ccfaf4633ae77039    k8s.gcr.io/coredns:1.6.7                            io.containerd.runc.v2
    dbabbb66c17ba1317706b3e13a473b7e52653d6097f1fb4e71ca3aca51c17e37    docker.io/rancher/local-path-provisioner:v0.0.12    io.containerd.runc.v2
    e17005058a2afb322c2c9179d5468b65be0bd8c2a24e3774373988413c93a7ec    k8s.gcr.io/pause:3.2                                io.containerd.runc.v2
    e68a9f43205edd282548f6db0942c0c0b503a66894b8bf5e3d7d1b7e5be8a71d    k8s.gcr.io/pause:3.2                                io.containerd.runc.v2
    f49eaf48b0b9ac0ef29d5dc6a8c3b8e26c9dd9a681715e8f431ad6ec4d452dbd    docker.io/kindest/kindnetd:0.5.4                    io.containerd.runc.v2
    

    这里还有一个疑问,大部分容器引擎在Nesting运行的时候是需要做一定设置的,然而在containerd这里我没有看到额外的设置。是containerd原生就支持nesting运行还是说这是因为vctl的特殊运行方式(指一个容器一个VM)就不得而知了,未来有空会看看原理。

  • Suricata学习

    Suricata是一款开源免费的网络威胁检测系统,可以在网络中作为IDS(Intrusion Detection System,入侵检测系统)、IPS(Intrusion Prevention System,入侵防御系统)和NSM(Network Security Monitoring,网络安全监控)使用,同样还可以离线分析pcap文件。Suricata使用专门的语言编写的规则来对网络流量进行分析,还可以利用Lua脚本来更加精确地分析,并以类似YAML或JSON的形式输出,可以方便存储在数据库中。目前Suricata项目属于OISF所有,OISF是一个非营利组织。

    Installation

    Suricata和其他的Linux软件类似,也具有两种安装方法,即直接安装发行版和编译安装。

    PPA安装法

    以下操作均在Ubuntu 16.04中,其他发行版可以查阅官方wiki。

    sudo add-apt-repository ppa:oisf/suricata-stable
    sudo apt-get update 
    sudo apt-get install suricata 
    

    编译安装法

    首先需要安装程序依赖库

    sudo apt-get -y install libpcre3 libpcre3-dbg libpcre3-dev \
    build-essential autoconf automake libtool libpcap-dev libnet1-dev \
    libyaml-0-2 libyaml-dev zlib1g zlib1g-dev libcap-ng-dev libcap-ng0 \
    make libmagic-dev libjansson-dev libjansson4 pkg-config
    

    下载源码

    VER=3.1
    wget "http://www.openinfosecfoundation.org/download/suricata-$VER.tar.gz" 
    tar -xvzf "suricata-$VER.tar.gz" 
    cd "suricata-$VER" 
    

    配置安装

    ./configure --prefix=/usr --sysconfdir=/etc --localstatedir=/var
    make
    sudo make install
    sudo ldconfig
    

    Suricata还提供了一些自动安装的脚本

    make install-conf 自动创建和安装配置文件
    make install-rules 自动从Emergeing Threats下载最新的规则集
    make install-full 将上面两者都包括
    

    Setup

    接下来我们需要部署Suricata,保证下面的命令均具备管理员权限。

    mkdir /var/log/suricata # 日志信息
    mkdir /etc/suricata # 配置文件
    cp classification.config /etc/suricata
    cp reference.config /etc/suricata
    cp suricata.yaml /etc/suricata
    

    /etc/suricata/suricata.yaml文件中正确配置好变量。HOME_NET设置为本地网络的IP地址,而EXTERNAL_NET建议的设置值是!$HOME_SET,这样所有不是本地IP的流量均被当作外界地址,当然设置成any也是可以的,只是这样的话会产生一些假的警报。下面的一些服务器均会被默认设置为$HOME_NETAIM_SERVERS设置为any

    Run

    Suricata的运行方式比较简单,只需要选择需要监听的网络接口,使用类似于下面的命令即可。

    sudo suricata -c /etc/suricata/suricata.yaml -i wlan0
    

    运行后产生的日志文件在/var/log/suricata目录下,我们可以使用类似于tail -f http.log stats.log的命令来监视程序的执行结果。

    Rules

    Suricata中最重要的就是关于规则的指定,使用特定的规则集就可以对特定的流量进行分析和处理,如果使用了IPS模式那么还可以直接处理报文内容。一般来说,我们会可以从互联网上下载最新的规则集,一般来自于Emerging Threats(Pro)和Sourcefire的VRT。手动管理的方式比较麻烦,我们可以使用到一款工具Oinkmasteroinkmaster可以自动化下载、管理rules。一般来说,规则由三个部分构成:Action、Header和Rule options。 例如下面这条

    alert tcp $EXTERNAL_NET any -> $HOME_NET 8888 (msg: "meow"; content: "meow"; )
    
    • alert表示动作,表示匹配后将发出警报。
    • tcp表示是TCP报文,还可以是ipudpicmp等,还包括一些常见的应用层协议。
    • $EXTERNAL表示使用前面定义的外部地址,可以使用!1.1.1.1![1.1.1.1, 1.1.1.2][10.0.0.0/24, !10.0.0.5]等形式。
    • any表示端口,有[79,80:82,83]这样的形式。
    • ->表示方向,可以是-><>
    • (msg: "meow"; content: "meow"; )表示规则选项,中间使用分号断开,包括meta-information、headers、payloads和flows等选项。具体内容将在后面说明。

    Meta-settings

    Meta-settings不会影响检测过程,只是用来完成记录等附属功能。

    msg: "some description"; 将显示在日志中
    sid: 123; 每条规则的编号
    rev: 123; 规则的版本号
    gid: 1; 组编号
    classtype: trojan-activity; 规则的分类
    reference: bugtraq, 123; http://www.securityfocus.com/bid; 规则的参考位置
    priority:1; 规则优先级
    metadata: ...;
    target: [src_ip|dest_ip];
    

    Header Keywords

    ttl: 10;
    ipopts: lsrr; IP选项
    sameip; 源IP和目的IP相同
    ip_proto: TCP;
    id: 1;
    geoip: src, RU;
    fragbits:[*+!]<[MDR]>;
    fragoffset:[!|<|>]<number>;
    seq:0;
    ack:1;
    window:[!]<number>;
    itype:min<>max;
    itype:[<|>]<number>;
    icode:min<>max;
    icode:[<|>]<number>;
    icmp_id:<number>;
    icmp_seq:<number>;
    

    Payload Keywords

    content:"a|0D|bc";
    content:"|61 0D 62 63|";
    content:"a|0D|b|63|";
    nocase;
    depth:12;
    offset:3;
    

    Flowbits

    通过在Suricata中保存标志位来判断若干个流量的关联性

    flowbits: set, name                设置name指定的条件
    flowbits: isset, name              检查是否有name指定的条件设置
    flowbits: toggle, name             切换name指定的条件设置情况
    flowbits: unset, name              取消设置name指定的条件
    flowbits: isnotset, name           检查是否没有name指定的条件设置
    flowbits: noalert                  不产生alert
    

    Flow

    匹配流的方向,是否建立连接等

    flow:to_client, established
    flow:to_server, established, only_stream
    flow:to_server, not_established, no_frag
    

    原理

    Suricata有几个关键组件构成:线程、线程模块和队列。Suricata以多线程的方式运行,而线程模块即对应其包获取、解码、检测和输出模块。一个包在Suricata会以类似流水线的方式一级一级地传递给下一个线程模块处理,而在这里的“传送带”就是队列。一个线程可以包含多个线程模块,这就是Runmode。 使用suricata --list-runmodes可以看到Suricata目前可以使用的runmodes。

    ------------------------------------- Runmodes ------------------------------------------
    | RunMode Type      | Custom Mode       | Description 
    |----------------------------------------------------------------------------------------
    | PCAP_DEV          | single            | Single threaded pcap live mode 
    |                   ---------------------------------------------------------------------
    |                   | autofp            | Multi threaded pcap live mode.  Packets from each flow are assigned to a single detect thread, unlike "pcap_live_auto" where packe
    ts from the same flow can be processed by any detect thread 
    |                   ---------------------------------------------------------------------
    |                   | workers           | Workers pcap live mode, each thread does all tasks from acquisition to logging 
    |----------------------------------------------------------------------------------------
    | PCAP_FILE         | single            | Single threaded pcap file mode 
    |                   ---------------------------------------------------------------------
    |                   | autofp            | Multi threaded pcap file mode.  Packets from each flow are assigned to a single detect thread, unlike "pcap-file-auto" where packe
    ts from the same flow can be processed by any detect thread 
    |----------------------------------------------------------------------------------------
    | PFRING(DISABLED)  | autofp            | Multi threaded pfring mode.  Packets from each flow are assigned to a single detect thread, unlike "pfring_auto" where packets fro
    m the same flow can be processed by any detect thread 
    |                   ---------------------------------------------------------------------
    |                   | single            | Single threaded pfring mode 
    |                   ---------------------------------------------------------------------
    |                   | workers           | Workers pfring mode, each thread does all tasks from acquisition to logging 
    |----------------------------------------------------------------------------------------
    | NFQ               | autofp            | Multi threaded NFQ IPS mode with respect to flow 
    |                   ---------------------------------------------------------------------
    |                   | workers           | Multi queue NFQ IPS mode with one thread per queue 
    |----------------------------------------------------------------------------------------
    | NFLOG             | autofp            | Multi threaded nflog mode   
    |                   ---------------------------------------------------------------------
    |                   | single            | Single threaded nflog mode  
    |                   ---------------------------------------------------------------------
    |                   | workers           | Workers nflog mode          
    |----------------------------------------------------------------------------------------
    | IPFW              | autofp            | Multi threaded IPFW IPS mode with respect to flow 
    |                   ---------------------------------------------------------------------
    |                   | workers           | Multi queue IPFW IPS mode with one thread per queue 
    |----------------------------------------------------------------------------------------
    | ERF_FILE          | single            | Single threaded ERF file mode 
    |                   ---------------------------------------------------------------------
    |                   | autofp            | Multi threaded ERF file mode.  Packets from each flow are assigned to a single detect thread 
    |----------------------------------------------------------------------------------------
    | ERF_DAG           | autofp            | Multi threaded DAG mode.  Packets from each flow are assigned to a single detect thread, unlike "dag_auto" where packets from the 
    same flow can be processed by any detect thread 
    |                   ---------------------------------------------------------------------
    |                   | single            | Singled threaded DAG mode   
    |                   ---------------------------------------------------------------------
    |                   | workers           | Workers DAG mode, each thread does all  tasks from acquisition to logging 
    |----------------------------------------------------------------------------------------
    | AF_PACKET_DEV     | single            | Single threaded af-packet mode 
    |                   ---------------------------------------------------------------------
    |                   | workers           | Workers af-packet mode, each thread does all tasks from acquisition to logging 
    |                   ---------------------------------------------------------------------
    |                   | autofp            | Multi socket AF_PACKET mode.  Packets from each flow are assigned to a single detect thread. 
    |----------------------------------------------------------------------------------------
    | NETMAP(DISABLED)  | single            | Single threaded netmap mode 
    |                   ---------------------------------------------------------------------
    |                   | workers           | Workers netmap mode, each thread does all tasks from acquisition to logging 
    |                   ---------------------------------------------------------------------
    |                   | autofp            | Multi threaded netmap mode.  Packets from each flow are assigned to a single detect thread. 
    |----------------------------------------------------------------------------------------
    | UNIX_SOCKET       | single            | Unix socket mode            
    |                   ---------------------------------------------------------------------
    |                   | autofp            | Unix socket mode            
    |----------------------------------------------------------------------------------------
    

    可以看到,在Suricata中包含三种Custom Mode,single/workers/autofp,根据右边的介绍我们能够知道当前模式的运行特点。在workers模式下,每一个线程上包含一个完整的包处理模块,也就是说将获取到的报文将分发到包处理线程中,而Suricata将会将属于同一个flow的流量放在一个线程中避免出现问题。

    其他支持软件

    Oinkmaster

    oinkmaster.pl -C /etc/oinkmaster.conf -o /etc/suricata/rules -i
    

    Suricata配置文件suricata.yaml中的outputs2 > unified2-alert可以设定在产生alert时dump出可疑数据包的信息,这个格式的好处是:

    • 方便归档管理
    • 生成速度快。

    Barnyard2

    Barnyard2就是个类似Syslog的东西,从Snort/Suricata处取得unified2格式的输入,产生其他格式的输出,比如给Prelude Hybrid IDS system、Syslog、MySQL。

  • OpenFlow协议学习

    OpenFlow 1.0 协议总结

    OpenFlow交换机中包含了1个以上的流表,流表又包含多个流表项。对于接收到的数据包,OpenFlow交换机在流表中选择一个最合适的流表项,并按流表项的内容对数据包进行处理。

    流表中包含的流表项由以下3个基本要素组成:Head Field, Counter, Action。Head Field即匹配规则,Counter即匹配次数,Action即匹配后所需要采取的操作。

    流表项

    Head Field

    • Ingress Port
    • Ethernet source address
    • Ethernet destination address
    • Ethernet type
    • VLAN id
    • VLAN Priority(802.1q PCP)
    • IP source address
    • IP destination address
    • ToS
    • Transport source port/ICMP type
    • Transport destination port/ICMP code

    在1.0版本中,STP(802.1d)数据包不执行流表匹配。完成STP处理后,执行“头字段解析之后”,会在此基础上进行数据包与流表的匹配。当数据包与刘表内设置的多条流表项匹配时,优先级最高的流表项即为匹配结果。OpenFlow中可以存在多个流表,但必须从流表0开始匹配。若编号为0的流表中不存在与数据包相匹配的流表项,而在其他流表中存在的话,则前进到下一流表中执行匹配。 数据包与OpenFlow交换机中任何一个流表项都不匹配的情况称之为Table-miss,这种情况下会利用Packet-In消息将数据包转发至控制器,或者丢弃。具体情况会根据设置决定。

    Counter

    4种计数器 Per Table, Per Port, Per Flow, Per Queue。

    Per Table: Active Entries, Packet Lookups, Packet Matches

    Per Flow: Received Packets/Bytes, Duration(msec/usec)

    Per Queue: Transmit Packets/Bytes, Transmit Overrun Errors

    Per Port: Received Packets/Bytes/Drops/Errors, Transmit Packets/Bytes/Drops/Errors, Receive Frame Alignment Errors, Received Overrun Errors, Received CRC Errors, Collisions

    Action

    Forward, Drop, Enqueue(Optional), Modify-Field(Optional)

    Forward

    名称 说明 虚拟端口
    (port) 转发至指定端口 port
    ALL 除接收端口以外的所有端口 0xfffc
    CONTROLLER 控制器 0xfffd
    LOCAL 发送至本地网络栈 0xfffe
    TABLE 执行流表中行动(仅Packet-Out) 0xfff9
    IN_PORT 从输入端口发出 0xfff8
    NORMAL(optional) 传统L2或L3交换机动作 0xfffa
    FLOOD(optional) 按照STP发送 0xfffb

    Drop

    未在协议中明确说明

    Enqueue

    在等待队列的末尾添加数据包

    Modify-Field

    • 设置VLAN ID
    • 设置VLAN priority
    • 去掉VLAN头
    • 修改源MAC
    • 修改目的MAC
    • 修改源IP
    • 修改目的IP
    • 修改ToS
    • 修改源端口
    • 修改目的端口

    控制器和交换机之间的安全通道

    安全通道通过控制面网络来建立,不受OpenFlow交换机中的流表项的影响。规范中安全通道应该使用TLS实现,但在OpenFlow 1.1开始添加了TCP明文的方式,默认的TCP端口为6653。

    struct openflow_header {
        uint8_t version; // 版本,OpenFlow 1.0-1.3分别对应0x01-0x04
        uint8_t type; // 消息类型,表明OpenFlow消息的类型
        uint16_t length; // Byte数
        uint32_t xid; // 事务ID
    }
    

    安全通道的建立过程

    1. 建立TCP/TLS连接
    2. 确定使用的OpenFlow版本,type字段值为OFPT_HELLO,在version中放入各自支持的最大版本号。
    3. 握手,type字段值为OFPT_FEATURES_REQUEST和OFPT_FEATURES_RESPONSE
    struct features_response {
        uint64_t datapath_id; // datapath id唯一标识OpenFlow交换机
        uint32_t n_buffer; // 缓存数据包的最大个数
        uint8_t n_tables; // 支持的流表个数
        char pad[24]; 
        uint32_t capabilities; // 支持的容量
        uint32_t actions; // 支持的行动
        struct ofp_phy_port ports[??]; // 物理端口信息
    }
    
    struct ofp_phy_port {
        uint16_t port_no; // 物理端口号
        uint8_t hw_addr[OFP_ETH_LENGTH]; // 48位的以太网地址
        uint8_t name[OFP_MAX_PORT_NAME_LEN]; // 128位的端口名称
        uint32_t config; // 端口设置bitmap
        uint32_t state; // 端口状态bitmap
        uint32_t curr; // 当前功能
        uint32_t advertised; // 广播功能
        uint32_t supported;  // 支持功能
        uint32_t peer;  // 连接方广播功能
    }
    
    1. 交换设置(optional)。type为OFPT_SET_CONFIG/OFPT_GET_CONFIG。
    struct set_config_message {
        uint16_t flags; // IP碎片处理方法
        uint16_t miss_send_len; // Table-miss数据包个数
    }
    
    enum set_config_flag {
        OFPC_FRAG_NORMAL = 0, // 依据流表处理
        OFPC_FRAG_DROP, // 丢弃
        OFPC_FRAG_REASM, // 重组
        OFPC_FRAG_MASK // unknown
    }
    
    1. 其他可能交换的内容 STATS,QUEUE_GET_CONFIG,Vendor, …

    Flow-Mod

    对于流表进行修改的消息,可以进行添加、删除和变更设置等操作。 种类包括以下几种

    1. OFPFC_ADD
    2. OFPFC_MODIFY
    3. OFPFC_MODIFY_STRICT
    4. OFPFC_DELETE
    5. OFPFC_DELETE_STRICT 后面加上STRICT表示要完全匹配。如果有错误发生,那么将回复错误信息。如果在流表中存在相同项,那么会将原有计数器清零。
    struct flow_mod_message {
        struct ofp_match match; // 数据包匹配信息
        uint8_t cookie[64];
        uint8_t action[??]; 
        
    }
    
    struct ofp_match {
        uint32_t wildcards; // 无视哪个字段的通配符
        uint16_t in_port; // 输入物理端口
        uint8_t dl_src[OFP_ETH_ALEN]; // 源MAC地址
        uint8_t dl_dst[OFP_ETH_ALEN]; // 目的MAC地址
        uint16_t dl_vlan; // VLAN id
        uint8_t dl_vlan_pcp;  // VLAN 优先级
        uint8_t pad1; 
        uint16_t dl_type; // 以太网帧类型 
        uint8_t nw_tos; // ToS字段
        uint8_t nw_proto; // IP协议号
        uint16_t pad2;
        uint32_t nw_src; // 源IPv4地址
        uint32_t nw_dst; // 目的IPv4地址
        uint16_t tp_src; // 源端口或ICMP类型
        uint16_t tp_dst; // 目的端口或ICMP代码
    }
    
    struct flow_mod_action {
        uint16_t command; // 即上面提到的action种类
        uint16_t idle_timeout; // 如果本条规则在idle_timeout时间内没有应用成功,那么将删除该规则
        uint16_t hard_timeout; // 如果本条规则在hard_timeout时间内还未添加成功,那么将取消添加该规则。
        uint16_t priority; // 规则优先级
        uint32_t buffer_id; // 缓存ID
        uint16_t out_port; // 输出端口号
        uint16_t flags;
        uint8_t actions[??]; // TLV结构,一个头部包含修改种类,后面接上具体的数据
    }
    

    Packet-In

    Packet-In消息用于将到达OpenFlow交换机的数据包发送至OpenFlow交换机。根据交换机是否缓存数据包来设置buffer_id的值:不缓存则设置为-1,将整个数据包发送;缓存则会根据SET_CONFIG消息设置的miss_send_len为最大值的数据包发送,默认值为128。

    struct packet_in_message {
        uint32_t buffer_id; // 缓存ID
        uint16_t total_len; // 帧长度
        uint16_t in_port; // 接受帧端口
        uint8_t reason; // 发送原因(不匹配0,流表指定1)
        uint8_t pad;
        uint8_t data[??]; // 具体数据包
    }
    

    Packet-Out

    Packet-Out即为控制器向交换机发送的消息。

    struct packet_out_message {
        uint32_t buffer_id; // 缓存ID
        uint16_t in_port; // 输入端口,用OFPP_NONE表示未指定,用OFPP_CONTROLLER表示是控制器创建的数据包
        uint16_t actions_len;
        uint8_t actions[actions_len]; // 类似于Flow-Mod时的action
        uint8_t data[??]; // 当buffer_id为-1的时候,即不指定交换器缓存时
    }
    

    Port-Status

    在OpenFlow交换机添加、删除或修改物理端口时,需要发送Port-Status消息来通知OpenFlow控制器。

    struct port_status_message {
        uint8_t reason; // OFPPR_ADD(0)、OFPPR_DELETE(1)、OFPPR_MODIFY(2)
        uint8_t pad[56];
        struct ofp_phy_port; 
    }
    

    Flow-Removed

    当OpenFlow交换机设置的流表项超时时,会向控制器发送Flow-Removed消息。

    struct flow_remove_message {
        struct ofp_match match;
        uint8_t cookie[64];
        uint16_t priority;
        uint8_t reason; // OFPRR_IDLE_TIMEOUT(0)、OFPRR_HARD_TIMEOUT(1)、OFPRR_DELETE(2)
        uint8_t pad;
        uint32_t duration_sec; // 有效时间(秒)
        uint32_t duration_nsec; // 纳秒
        uint16_t idle_timeout;
        uint8_t pad2;
        uint64_t packet_count; // 数据包数
        uint64_t byte_count; // 总字节数
    }
    

    大部分字段可以直接复制于Flow-Mod的信息。

    Error

    当在处理过程中出现错误的时候发送,控制器和交换机均可使用,type为OFPT_ERROR_MSG。

    struct error_message {
        uint16_t type;
        uint16_t code;
        uint8_t data[??];
    }
    
    enum error_type {
        OFPET_HELLO_FAILED,
        OFPET_BAD_REQUEST,
        OFPET_BAD_ACTION,
        OFPET_FLOW_MOD_FAILED,
        OFPET_QUEUE_OP_FAILED
    }
    

    具体code参考OpenFlow说明书。

    Barrier

    用于双方对于事务的完成程度的通信,避免发生有顺序的事务因执行程度未达到而造成错误的情况。例如在发送了三条Flow-Mod信息,xid分别为1、2、3后,可以发送xid为4的Barrier请求,如果可以得到对方xid为4的Barrier响应,则表示前面的消息已经处理完毕。

    Echo

    用于测试两方的连接情况、通信延迟和通信带宽等。

    LLDP

    Link Layer Discovery Protocol

    IEEE 802.1ab

    许多的OpenFlow控制器利用LLDP来检测网络拓扑结构。一般来说,OpenFlow控制器会利用Packet-Out消息向OpenFlow交换机下达发送LLDP帧的命令,接下来利用接收到LLDP帧的OpenFlow交换机向控制器发送的Packet-In消息中得到拓扑消息,构建整个网络拓扑结构。

    LLDP的机制

    • 每隔一段时间(标准中建议是30秒)向L2的组播地址发送LLDP帧,这个特性无视链路状态。
    • 发送源地址为发送设备的MAC地址。
    • 单向发送设备的识别号、端口识别号,这样接收方就可以得知发送设备和端口的信息。

    LLDP使用三种全局性组播群地址

    名称 数值 说明
    Nearest Bridge 01-80-C2-00-00-0E 所有的桥和路由设备不转发
    Nearest non-TPMR Bridge 01-80-C2-00-00-C3 跨越TPMR桥
    Nearest Customer Bridge 01-80-C2-00-00-00 跨越TPMR和S-VLAN桥

    一般来说,仅OpenFlow交换机构成的网络拓扑,会使用Nearest Bridge地址,而如果在中间存在有非OpenFlow的普通L2交换机则使用Nearest non-TPMR Bridge地址,借助WAN远程使用OpenFlow交换机则使用Nearest Customer Bridge地址。

    -----------------------------------------------------
    | 组播地址 | 设备的以太网地址 | 0x88cc(LLDP) | LLDPDU |
    -----------------------------------------------------
    

    OpenFlow 1.1 更新

    头字段变更为匹配字段

    在匹配字段中添加了MPLS标签、MPLS流量类别、元数据

    多级流表

    OpenFlow交换机可以设置多个流表,还可为一个数据包匹配多个流表项,但是在流表内还是只选择一个流表项

    采用流水线处理的方式,新定义了“行动集”的概念,即将所有的行动统一添加到行动集中,但是执行顺序与计入顺序不相同,采用copy TTL inwards->pop->push->copy TTL outwards->decrement TTL->set->qos->group->output的方式,这里面每种类型仅能设置一个。

    如果发生了Table-miss的情况,那么将根据OFPT_TABLE_MOD对流表的设置来决定,具体的方法有发送至控制器、前进到下一流表和丢弃。

  • ONOS官方示例应用解析

    onos-app-calendar

    一个RESTful的web应用,提供添加链路时延、带宽限制的Intent。用到了ConnectivityIntent。完成功能的逻辑

        /**
         * Create an Intent for a bidirectional path with constraints.
         *
         * @param key optional intent key
         * @param src the path source (DPID or hostID)
         * @param dst the path destination (DPID or hostID)
         * @param srcPort the source port (-1 if src/dest is a host)
         * @param dstPort the destination port (-1 if src/dest is a host)
         * @param bandwidth the bandwidth (mbps) requirement for the path
         * @param latency the latency (micro sec) requirement for the path
         * @return the appropriate intent
         */
        private Intent createIntent(Key key,
                                    String src,
                                    String dst,
                                    String srcPort,
                                    String dstPort,
                                    Long bandwidth,
                                    Long latency) {
    
            TrafficSelector selector = buildTrafficSelector();
            TrafficTreatment treatment = builder().build();
    
            final Constraint constraintBandwidth =
                    new BandwidthConstraint(Bandwidth.mbps(bandwidth));
            final Constraint constraintLatency =
                    new LatencyConstraint(Duration.of(latency, ChronoUnit.MICROS));
            final List<Constraint> constraints = new LinkedList<>();
    
            constraints.add(constraintBandwidth);
            constraints.add(constraintLatency);
    
            if (srcPort.equals("-1")) {
                HostId srcPoint = HostId.hostId(src);
                HostId dstPoint = HostId.hostId(dst);
                return HostToHostIntent.builder()
                        .appId(appId())
                        .key(key)
                        .one(srcPoint)
                        .two(dstPoint)
                        .selector(selector)
                        .treatment(treatment)
                        .constraints(constraints)
                        .build();
    
            } else {
                ConnectPoint srcPoint = new ConnectPoint(deviceId(src), portNumber(srcPort));
                ConnectPoint dstPoint = new ConnectPoint(deviceId(dst), portNumber(dstPort));
                return TwoWayP2PIntent.builder()
                        .appId(appId())
                        .key(key)
                        .one(srcPoint)
                        .two(dstPoint)
                        .selector(selector)
                        .treatment(treatment)
                        .constraints(constraints)
                        .build();
            }
        }
    
    
        /**
         * Synchronously submits an intent to the Intent Service.
         *
         * @param intent intent to submit
         * @return true if operation succeed, false otherwise
         */
        private boolean submitIntent(Intent intent)
                throws InterruptedException {
            IntentService service = get(IntentService.class);
    
            CountDownLatch latch = new CountDownLatch(1);
            InternalIntentListener listener = new InternalIntentListener(intent, service, latch);
            service.addListener(listener);
            service.submit(intent);
            log.info("Submitted Calendar App intent and waiting: {}", intent);
            if (latch.await(TIMEOUT, TimeUnit.SECONDS) &&
                    listener.getState() == INSTALLED) {
                return true;
            }
            return false;
        }
    
    

    onos-app-carrierethernet

    根据pom.xml文件的内容,我们可以知道这个应用是用于运营以太网服务(Carrier Ethernet),具体介绍在Carrier Ethernet,由城域以太网论坛(MEF)建立。CE包含五个模块:保护、QoS、扩展、业务管理、TDM。个人理解是,希望以太网上的流量,能够在SDN环境下加入一定的识别特征,这样才能方便城域网中的网络设备根据这些特征进行服务的定制。这个项目相对来说比较复杂,而且更新也非常的频繁,先挖个坑在这里,以后有时间的话可以认真阅读一下其实现。

    onos-app-database-perf

    一个用来测试ONOS集群存储数据性能的应用,在activate中包含有对于StorageService的使用,包括创建并发式Map的方法,注册序列化器KryoNamespace等,之后创建多个线程测试具体的性能指标。

    onos-app-ecord-co

    一个CORD的实现应用,CORD(Central Office Re-architected as a DataCenter),意为在家庭、公司等网络边界的基础网络设备,实现一个数据中心的服务功能。CORD目前有三种类型:ECORD、RCORD和MCORD,具体的详情可以查看CORD。这个应用给出了一个ECORD的实现方案。这个应用比较全面地展示了ONOS的抽象子系统概念,我们可以从项目的结构看出来。

    CentralOffice.java文件是组件的主文件,主要工作是注册应用,并且创建了一个名为BigSwitchDeviceProvider的对象,那么接下来我们去找找实现。BigSwitchDeviceProvider继承于DeviceProvider。从前面两次的loadConfig操作中我们可以看出来这个应用支持修改config文件,并且提供有RPC和RESTful两种修改模式,然而目前我对于ConfigService还没有太深的了解,因此这里填个坑等到以后再来了解。在activate方法中基本就是向deviceProvider中注册了一个设备,接下来使用LLDP协议发现网络拓扑。为了完成LLDP的工作,程序中使用到了onlab-misc中的ONOSLLDP工具类,方便使用LLDP协议。

        @Activate
        public void activate(ComponentContext context) {
            cfgService.registerProperties(getClass()); // 在ComponentConfigService上进行注册
            loadRpcConfig(context); 
            loadRestConfig(context); 
    
            // setup service to, and register with, providers
            try {
                remoteServiceContext = rpcService.get(URI.create(remoteUri));
            } catch (UnsupportedOperationException e) {
                log.warn("Unsupported URI: {}", remoteUri);
            }
            providerId = new ProviderId(schemeProp, idProp);
            executor = newSingleThreadScheduledExecutor(groupedThreads("onos/bigswitch", "discovery-%d"));
            registerToDeviceProvider();
            prepareProbe();
            registerToLinkServices();
    
            // start listening to config changes
            NetworkConfigListener cfglistener = new InternalConfigListener();
            cfgRegistry.addListener(cfglistener);
            cfgRegistry.registerConfigFactory(xcConfigFactory);
            log.info("Started");
        }
    
        @Deactivate
        public void deactivate() {
            packetService.removeProcessor(packetProcessor);
    
    
            // advertise all Links as vanished
            knownLinks.invalidateAll();
    
            cfgRegistry.unregisterConfigFactory(xcConfigFactory);
            cfgService.unregisterProperties(getClass(), false);
            unregisterFromLinkServices();
            executor.shutdownNow();
            unregisterFromDeviceProvider();
            // Won't hurt but necessary?
            deviceProviderService = null;
            providerId = null;
            log.info("Stopped");
        }
    
        @Modified
        public void modified(ComponentContext context) {
            log.info("Reloading config...");
            // Needs re-registration to DeviceProvider
            if (loadRpcConfig(context)) {
                // unregister from Device and Link Providers with old parameters
                unregisterFromLinkServices();
                unregisterFromDeviceProvider();
                // register to Device and Link Providers with new parameters
                try {
                    remoteServiceContext = rpcService.get(URI.create(remoteUri));
                    providerId = new ProviderId(schemeProp, idProp);
                    registerToDeviceProvider();
                    registerToLinkServices();
                } catch (UnsupportedOperationException e) {
                    log.warn("Unsupported URI: {}", remoteUri);
                }
                log.info("Re-registered with Device and Link Providers");
            }
    
            // Needs to advertise cross-connect links
            if (loadRestConfig(context)) {
                advertiseCrossConnectLinksOnAllPorts();
            }
        }