In this blog, you will know how to install Debian for an Android device with root privileges.
No Termux. No Linux Deploy. No Black Box.
Only Debian offical installer. Everything is under your control.
root privileges
busybox
With Magisk installed, you can get both root privileges and busybox.
debootstrap
Most commands should be executed with root privileges.
With adb, you can type:1
2adb shell
su
debootstrap
requires several basic commands to run.
1 | # The most important program is busybox, and we can use busybox binary from Magisk |
1 | # Set the debian inst directory |
1 | dd if=/dev/zero of=$DEBIMG bs=1G count=10 |
1 | wget -O pkgdetails.c https://salsa.debian.org/installer-team/base-installer/-/raw/master/pkgdetails.c?inline=false |
pkgdetail
to Android deviceTake adb as an example.1
adb push pkgdetail /sdcard
Download debootstrap
and prepare pkgdetails
for it.
Note: the dest file is pkgdetails, not pkgdetail
1 | # Download debootstrap |
1 | # Install Debian |
chroot-debian.sh
1 | BUSYBOX=/data/adb/magisk/busybox |
1 | # Add network related groups required by Android kernel |
1 | cat >/etc/resolv.conf <<EOL |
1 | apt update && apt install neovim |
Applications that rely on System V IPC, such as PostgreSQL, cannot be run directly in Debian on Android.
Possible solutions: Termux emulates System V IPC.
Refer to:
That’s why we add root
to some groups and modify gid of _apt
to aid_inet
Refer to:
If your application fail to run, check if it’s a network issue!
For example, mysqld run as user mysql
, and its default group is mysql
.1
mysql:x:104:104:MySQL Server,,,:/nonexistent:/bin/false
Without modifying its group to aid_inet, service start mysql
/ mysqld
would fail.
If you do not change the group of user mysql
to aid_inet
, command service start mysql
/ mysqld
will fail.
1 | # ~/.bashrc: executed by bash(1) for non-login shells. |
Refer to Tsinghua Mirrors - Debian
sysvinit-core
to run daemons1 | apt update && apt install sysvinit-core |
1 | dpkg-reconfigure tzdata |
1 | apt install locales |
Install mariadb with proper apt source1
apt install mariadb-server
You must change group of user mysql
to aid_inet
1
usermod -g aid_inet mysql
If package sysvinit-core
is installed, start mysqld with1
service mysql start
Without sysvinit-core
, you can run1
mysqld
1 | apt install bash-completion |
This blog grew out of a question I asked in the SuperUser community : Relay TCP upload traffic and make download traffic go directly to the client.
Client
sends request traffic to RelayServer:10080
RelayServer:10080
relay request traffic from client to TargetServer:10080
(Achieved by DNAT rules on the relay server)TargetServer:10080
response client
as if it is RelayServer:10080
(Achieved by SNAT rules on the target server) Client
receives response traffic from RelayServer:10080
, which is actually from TargetServer:10080
It cannot be implemented in the real-world network due to the source address verification.
Devices: 4 Vmware Machines with bridged network
OS: Alpine Linux 3.15.1
192.168.10.2/24
192.168.10.1
00:0c:29:06:c7:7e
192.168.20.2/24
192.168.20.1
00:0c:29:5b:89:3e
192.168.30.2/24
192.168.30.1
00:0c:29:15:da:6a
00:0c:29:65:3c:a3
192.168.10.1/24
00:0c:29:65:3c:ad
192.168.20.1/24
00:0c:29:65:3c:b7
192.168.30.1/24
1 | echo 1 > /proc/sys/net/ipv4/ip_forward |
For linux-based router, refer to kernel sysctl parameter rp_filter
1
2
3echo 0 > /proc/sys/net/ipv4/conf/eth0/rp_filter
echo 0 > /proc/sys/net/ipv4/conf/eth1/rp_filter
echo 0 > /proc/sys/net/ipv4/conf/eth2/rp_filter
1 | echo 1 > /proc/sys/net/ipv4/ip_forward |
1 | table ip route { |
1 | table ip raw { |
On the target server, start a tcp/udp server
1 | nc -s 192.168.30.2 -l -p 10080 # for tcp |
On the client, start a tcp/udp client to server
1 | nc -s 192.168.10.2 -p 12345 192.168.20.2 10080 # for tcp |
1 | router:~# tcpdump '(dst 192.168.10.2 && dst port 12345) || (src 192.168.10.2 && src port 12345)' -e |
1 | -D [bind_address:]port |
Setup a forwarding from remote to local for ssh service with -R
option, on the local PC
1 | ssh -R 1022:localhost:22 ${remoteuser}@${remotehost} |
With that forwarding, you can login ssh of local PC via localhost:1022
on the remote server
Setup a SOCKS tunnel from remote to local with -D
option, on the remote server
1 | ssh -D 1080 -p 1022 ${localuser}@localhost |
socks5://localhost:1080
on the remote server Many network applications already have SOCKS support built into them.
For applications without SOCKS support, proxychains can help.
For example, you can curl with SOCKS proxy:
1
proxychains4 -f ~/.proxychains/proxychains.conf curl google.com
And you can proxychains a bash:
1
proxychains4 bash
Java中可以使用 Proxy.newProxyInstance
创建动态代理,动态代理可以拦截对被代理对象的方法调用,并为其附加额外功能。这种似乎一般称为JDK动态代理。
该方法的描述如下:
1 | public static Object newProxyInstance(ClassLoader loader, |
它的基本原理是按照接口的差异为各种类型的被代理对象动态生成一个新类型,称为代理类。代理类其实就是一个实现了被代理对象所需接口的新类型,它会在生成的各种接口方法中调用 InvocationHandler
的 invoke
方法,所以一般情况下需要在 InvocationHandler
中为被代理对象调用相应的方法,以实现其原本的功能。
下面的例子中声明了两个接口,ISay
和 ISpeak
,Hello
类实现了 ISay
和 ISpeak
两个接口。
MyInvocationHandler
接受一个被代理对象 proxiedObject
保存起来,它会在 invoke
中输出它自己和proxy参数的地址,以及 method
方法的描述,并在最后调用了被代理实例的相应 method
。
main
方法中的 myInvocationHandler.invoke
很容易理解,它就是一个正常的方法调用。关键在于,后面的 proxyInstance.say
其实完全等价于前面的 myInvocationHandler.invoke
调用。
接下来给出源码,后面展示输出并进行进一步的分析。
1 | import java.lang.reflect.InvocationHandler; |
程序输出如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17invocationHandlerHashCode: {789451787}
method: {public abstract void DynamicProxyTest3$ISay.say(java.lang.String)}
proxyInstanceHashCode: {1950409828}
proxyInstanceClass: {class $Proxy0}
Hello.say: we can reach corner in the world
invocationHandlerHashCode: {789451787}
method: {public abstract void DynamicProxyTest3$ISay.say(java.lang.String)}
proxyInstanceHashCode: {1950409828}
proxyInstanceClass: {class $Proxy0}
Hello.say: Across the Great Wall
Exception in thread "main" java.lang.ClassCastException: $Proxy0 cannot be cast to DynamicProxyTest3$Hello
at DynamicProxyTest3.main(DynamicProxyTest3.java:55)
Process finished with exit code 1
可以看到动态代理对象实例 proxyInstance
的类型是 $Proxy0
,这是程序运行时动态生成的一个类型,通过执行Java虚拟机参数 -Dsun.misc.ProxyGenerator.saveGeneratedFiles=true
就可以输出这个动态生成的类的class字节码,反编译可以得到下面的内容。
可以看到,该代理类实现了 ISay
和 ISpeak
接口,它将接口中的每一个方法都写成了 super.h.invoke(this, methodObject, new Object[]{var1, var2, ...})
的形式。
而 super.h
其实就是创建动态代理时传入的 InvocationHandler
对象,在这个例子里就是 myInvocationHandler
。第一个参数 this
就是该代理对象本身,对应于本例的 proxyInstance
,后面两个参数分别是 method
和 method
所需参数。
由此可见 main
方法中的 proxyInstance.say
其实完全等价于前面的 myInvocationHandler.invoke
调用。
这就是动态代理的基本原理,实际应用时可以在 InvocationHandler
中附加自己所需的操作来增强被代理类的功能,比如记录日志。
1 | // |
该实例的最后一句调用试图将动态代理类实例转换成被代理的类型,但并没有成功,因为代理类只是实现了被代理类的接口,它本身并不是被代理的类型。
由于这种 Proxy 动态代理是基于接口的,被代理类需要实现相应的接口才能进行代理,但是实际使用中有可能难以或者懒得对被代理类进行改造,这种情况下可以动态代理吗?答案是可以!不过JDK动态代理显然是不行的,但 cglib 可以实现,它不依赖接口,生成的动态代理对象还可以转换成被代理的类型,具体原理我还没看。
]]>GCC is not just a compiler. It’s an open source project that lets you build all kinds of compilers. Some compilers support multithreading; some support shared libraries; some support multilib. It all depends on how you configure the compiler before building it.
This guide will demonstrate how to build a cross-compiler, which is a compiler that builds programs for another machine. All you need is a Unix-like environment with a recent version of GCC already installed.
In this guide, I’ll use Debian Linux to build a full C++ cross-compiler for AArch64, a 64-bit instruction set available in the latest ARM processors. I don’t actually own an AArch64 device – I just wanted an AArch64 compiler to verify this bug.
Starting with a clean Debian system, you must first install a few packages:
1 | $ sudo apt-get install g++ make gawk |
Everything else will be built from source. Create a new directory somewhere, and download the following source packages. (If you’re following this guide at a later date, there will be more recent releases of each package available. Check for newer releases by pasting each URL into your browser without the filename. For example: http://ftpmirror.gnu.org/binutils/)
1 | $ wget http://ftpmirror.gnu.org/binutils/binutils-2.24.tar.gz |
The first four packages – Binutils, GCC, the Linux kernel and Glibc – are the main ones. We could have installed the next three packages in binary form using our system’s package manager instead, but that tends to provide older versions. The last two packages, ISL and CLooG, are optional, but they enable a few more optimizations in the compiler we’re about to build.
By the time we’re finished, we will have built each of the following programs and libraries. First, we’ll build the tools on the left, then we’ll use those tools to build the programs and libraries on the right. We won’t actually build the target system’s Linux kernel, but we do need the kernel header files in order to build the target system’s standard C library.
The compilers on the left will invoke the assembler & linker as part of their job. All the other packages we downloaded, such as MPFR, GMP and MPC, will be linked into the compilers themselves.
The diagram on the right represents a sample program, a.out
, running on the target OS, built using the cross compiler and linked with the target system’s standard C and C++ libraries. The standard C++ library makes calls to the standard C library, and the C library makes direct system calls to the AArch64 Linux kernel.
Note that instead of using Glibc as the standard C library implementation, we could have used Newlib, an alternative implementation. Newlib is a popular C library implementation for embedded devices. Unlike Glibc, Newlib doesn’t require a complete OS on the target system – just a thin hardware abstraction layer called Libgloss. Newlib doesn’t have regular releases; instead, you’re meant to pull the source directly from the Newlib CVS repository. One limitation of Newlib is that currently, it doesn’t seem to support building multithreaded programs for AArch64. That’s why I chose not to use it here.
Extract all the source packages.
1 | $ for f in *.tar*; do tar xf $f; done |
Create symbolic links from the GCC directory to some of the other directories. These five packages are dependencies of GCC, and when the symbolic links are present, GCC’s build script will build them automatically.
1 | $ cd gcc-4.9.2 |
Choose an installation directory, and make sure you have write permission to it. In the steps that follow, I’ll install the new toolchain to /opt/cross
.
1 | $ sudo mkdir -p /opt/cross |
Throughout the entire build process, make sure the installation’s bin
subdirectory is in your PATH
environment variable. You can remove this directory from your PATH
later, but most of the build steps expect to find aarch64-linux-gcc
and other host tools via the PATH
by default.
1 | $ export PATH=/opt/cross/bin:$PATH |
Pay particular attention to the stuff that gets installed under /opt/cross/aarch64-linux/
. This directory is considered the system root of an imaginary AArch64 Linux target system. A self-hosted AArch64 Linux compiler could, in theory, use all the headers and libraries placed here. Obviously, none of the programs built for the host system, such as the cross-compiler itself, will be installed to this directory.
This step builds and installs the cross-assembler, cross-linker, and other tools.
1 | $ mkdir build-binutils |
aarch64-linux
as the target system type. Binutils’s configure
script will recognize that this target is different from the machine we’re building on, and configure a cross-assembler and cross-linker as a result. The tools will be installed to /opt/cross/bin
, their names prefixed by aarch64-linux-
.--disable-multilib
means that we only want our Binutils installation to work with programs and libraries using the AArch64 instruction set, and not any related instruction sets such as AArch32.This step installs the Linux kernel header files to /opt/cross/aarch64-linux/include
, which will ultimately allow programs built using our new toolchain to make system calls to the AArch64 kernel in the target environment.
1 | $ cd linux-3.17.2 |
configure
script in step 4 expects them to be already installed.ARCH=arm64
All of the remaining steps involve building GCC and Glibc. The trick is that there are parts of GCC which depend on parts of Glibc already being built, and vice versa. We can’t build either package in a single step; we need to go back and forth between the two packages and build their components in a way that satisfies their dependencies.
This step will build GCC’s C and C++ cross-compilers only, and install them to /opt/cross/bin
. It won’t invoke those compilers to build any libraries just yet.
1 | $ mkdir -p build-gcc |
--target=aarch64-linux
, the build script looks for the Binutils cross-tools we built in step 1 with names prefixed by aarch64-linux-
. Likewise, the C/C++ compiler names will be prefixed by aarch64-linux-
.--enable-languages=c,c++
prevents other compilers in the GCC suite, such as Fortran, Go or Java, from being built.In this step, we install Glibc’s standard C library headers to /opt/cross/aarch64-linux/include
. We also use the C compiler built in step 3 to compile the library’s startup files and install them to /opt/cross/aarch64-linux/lib
. Finally, we create a couple of dummy files, libc.so
and stubs.h
, which are expected in step 5, but which will be replaced in step 6.
1 | $ mkdir -p build-glibc |
--prefix=/opt/cross/aarch64-linux
tells Glibc’s configure
script where it should install its headers and libraries. Note that it’s different from the usual --prefix
.configure
script currently requires us to specify all three --build
, --host
and --target
system types.$MACHTYPE
is a predefined environment variable which describes the machine running the build script. --build=$MACHTYPE
is needed because in step 6, the build script will compile some additional tools which run as part of the build process itself.--host
has a different meaning here than we’ve been using so far. In Glibc’s configure
, both the --host
and --target
options are meant to describe the system on which Glibc’s libraries will ultimately run.crt1.o
, crti.o
and crtn.o
, to the installation directory manually. There’s doesn’t seem to a make
rule that does this without having other side effects.This step uses the cross-compilers built in step 3 to build the compiler support library. The compiler support library contains some C++ exception handling boilerplate code, among other things. This library depends on the startup files installed in step 4. The library itself is needed in step 6. Unlike some other guides, we don’t need to re-run GCC’s configure
. We’re just building additional targets in the same configuration.
1 | $ cd build-gcc |
libgcc.a
and libgcc_eh.a
, are installed to /opt/cross/lib/gcc/aarch64-linux/4.9.2/
.libgcc_s.so
, is installed to /opt/cross/aarch64-linux/lib64
.In this step, we finish off the Glibc package, which builds the standard C library and installs its files to /opt/cross/aarch64-linux/lib/
. The static library is named libc.a
and the shared library is libc.so
.
1 | $ cd build-glibc |
Finally, we finish off the GCC package, which builds the standard C++ library and installs it to /opt/cross/aarch64-linux/lib64/
. It depends on the C library built in step 6. The resulting static library is named libstdc++.a
and the shared library is libstdc++.so
.
1 | $ cd build-gcc |
If you encounter any errors during the build process, there are three possibilities:
You’ll have to examine the build logs to determine which case applies. GCC supports a lot of configurations, and some of them may not build right away. The less popular a configuration is, the greater the chance of it being broken. GCC, being an open source project, depends on contributions from its users to keep each configuration working.
I’ve written a small bash script named build_cross_gcc
to perform all of the above steps. You can find it on GitHub. On my Core 2 Quad Q9550 Debian machine, it takes 13 minutes from start to finish. Customize it to your liking before running.
build_cross_gcc
also supports Newlib configurations. When you build a Newlib-based cross-compiler, steps 4, 5 and 6 above can be combined into a single step. (Indeed, that’s what many existing guides do.) For Newlib support, edit the script options as follows:
1 | TARGET=aarch64-elf |
Another way to build a GCC cross-compiler is using a combined tree, where the source code for Binutils, GCC and Newlib are merged into a single directory. A combined tree will only work if the intl
and libiberty
libraries bundled with GCC and Binutils are identical, which is not the case for the versions used in this post. Combined trees don’t support Glibc either, so it wasn’t an option for this configuration.
There are a couple of popular build scripts, namely crosstool-NG and EmbToolkit, which automate the entire process of building cross-compilers. I had mixed results using crosstool-NG, but it helped me make sense of the build process while putting together this guide.
If everything built successfully, let’s check our cross-compiler for a dial tone:
1 | $ aarch64-linux-g++ -v |
We can compile the C++14 program from the previous post, then disassemble it:
1 | $ aarch64-linux-g++ -std=c++14 test.cpp |
This was my first foray into building a cross-compiler. I basically wrote this guide to remember what I’ve learned. I think the above steps serve as a pretty good template for building other configurations; I used build_cross_gcc
to build TARGET=powerpc-eabi
as well. You can browse config.sub
from any of the packages to see what other target environments are supported. Comments and corrections are more than welcome!
Linux命名空间是一种资源隔离机制,Docker for Linux就是主要利用Linux命名空间实现资源隔离的。
目前有八种Linux命名空间。下表列出了这些命名空间及其隔离的资源类型,Flag是用于一些系统调用的标志,Page指示如何找到相关命名空间的详细文档。
Namespace | Flag | Page | Isolates |
---|---|---|---|
Cgroup | CLONE_NEWCGROUP | cgroup_namespaces(7) | Cgroup root directory |
IPC | CLONE_NEWIPC | ipc_namespaces(7) | System V IPC, |
POSIX message queues | |||
Network | CLONE_NEWNET | network_namespaces(7) | Network devices, |
stacks, ports, etc. | |||
Mount | CLONE_NEWNS | mount_namespaces(7) | Mount points |
PID | CLONE_NEWPID | pid_namespaces(7) | Process IDs |
Time | CLONE_NEWTIME | time_namespaces(7) | Boot and monotonic clocks |
User | CLONE_NEWUSER | user_namespaces(7) | User and group IDs |
UTS | CLONE_NEWUTS | uts_namespaces(7) | Hostname and NIS domain name |
veth
本文重点探究Linux网络命名空间,Linux网络命名空间可以隔离网络设备、套接字(端口号)、网络协议栈、路由表、防火墙规则等网络资源。也可以为每个网络命名空间提供不同的DNS配置,比如假如存在一个名为 nstest
的网络命名空间,则对于nstest
, DNS解析将会首先参照 /etc/netns/nstest/resolv.conf
中的DNS配置,其次才是全局DNS配置 /etc/resolv.conf
。
虚拟网络设备 veth
可用于连接两个网络命名空间,可以为隔离于网络命名空间中的进程提供访问外部网络的能力。
虚拟网络设备 veth
总是成对出现的,两个 veth
一端互相连接在一起,另一端与网络协议栈相连,一个 veth
收到的数据包会立即被转发到与之相连的 veth
。
1 | +----------------------------------------------------------------+ |
网络命名空间的介绍参考network_namespaces(7),网络命名空间的的创建、删除与操纵,参考ip-netns(8),veth
的介绍参考veth(4)和veth of Linux Virtual Network Device。
可以用 ip netns
进行网络命名空间的创建、删除、在特定网络命名空间执行进程等操作,用 ip link
可以进行在特定命名空间创建接口,为接口变更命名空间等操作。下面给出几个示例:
nstest
1 | ip netns add nstest |
nstest
1 | ip netns del nstest |
nstest
中执行命令 1 | ip netns exec nstest COMMAND |
veth0s0
和 veth0s1
1 | ip link add veth0s0 type veth peer veth0s1 |
veth0s1
移动至网络命名空间 nstest
1 | # 一旦将网络接口移动至 nstest ,就无法在默认网络命名空间中看到该网络接口 |
veth0s1
移动至默认网络命名空间 1 | # 由于网络接口 veth0s1 属于 nstest ,所以需要在该网络命名空间中操作 |
veth
连通两个网络命名空间大致流程为:
nstest
veth
虚拟网络接口对连接默认网络命名空间和 nstest
veth
配置IP进行测试配置命令如下:
1 | ip netns add nstest |
配置好之后,可以使用 ip addr
确认一下网络情况:
下图是我电脑上的情况,可以看到 nstest
只存在 lo
和 veth0s1
,它与默认网络命名空间的接口是互相隔离的。
然后就可以实现默认网络命名空间与 nstest
之间的通信。
可以通过ping测试:1
2ping 192.168.11.101
ip netns exec nstest ping 192.168.11.100
首先介绍一下如何捕获经过网络接口的数据包,在Linux上可以使用 tcpdump
捕获数据包,如果只是想要捕获特定接口的数据包,可以使用 tcpdump -lni interface_name
。
下面是实验一进行ping测试的抓包截图:
现在nstest
和默认网络命名空间可以互相ping通了,那么nstest
能否访问外部网络呢?结果是不可以。
首先,如果想要访问 baidu.com
,需要配置好网关,并且,由于我们希望访问域名,也就需要配置好DNS解析服务。
那么,新建的网络命名空间中的网关和DNS配置又是怎么的呢?可以通过 ip route
查看网关。而DNS配置仍是通过 /etc/resolv.conf
文件控制,如果仅仅使用 /etc/resolv.conf
控制DNS,那么理论上 nstest
中的DNS配置是与默认网络命名空间相同的,不过如今大部分Linux发行版默认情况下都默认安装了网络服务相关守护进程,Ubuntu Server 20.04中使用的是netplan服务,所以说了这么多,我也不太清楚nstest
中默认的DNS配置是怎么的,可能与网络接口的IP地址配置有关系,如果采用DHCP,应该会根据DHCP服务器返回的结果设置DNS,具体可以查看netplan配置文件。
首先查看 nstest
的网关配置,下图顺便展示了默认网络命名空间的路由信息,用于突出网络命名空间之间路由表的隔离性:
可以看到, nstest
中未配置默认路由,所以自然无法访问 baidu.com
了,下面为它配置默认路由,这里我们将 veth0s0
的IP地址 192.168.11.100
设为 nstest
中的默认网关。
1 | ip netns exec nstest ip route add default via 192.168.11.100 |
然后检查 nstest
的路由表就会看到默认路由,那么我们现在ping 114.114.114.114
和 baidu.com
,看能否通过 tcpdump
抓到包。
可以看到发往 114.114.114.114
的 ICMP echo request
,但是没有回应,也有发往 127.0.0.53
的DNS查询,同样没有回应。(DNS查询应该和DNS配置有关系,这里不深究了)
这里要关注的一点是,veth0s0
收到了来自 nstest
中 veth0s1
的数据包,如果进一步配置NAT,veth0s1
就可以访问外部网络了。其实使用桥接网络模式的docker容器就是利用类似的技术访问外部网络的,只不过docekr容器并非直接配置NAT,而是将对端veth接入了docker网桥,再进一步通过网桥访问外部网络。
接下来,使用iptables为配置NAT:
1 | IFACE=ens33 # 可以访问外网的接口名称 |
然后 nstest
就可以访问外部网络了。
namespaces(7) — Linux manual page
network_namespaces(7) — Linux manual page
ip-netns(8) — Linux manual page
veth(4) — Linux manual page
veth of Linux Virtual Network Device
Setup a network namespace with Internet access
A tcpdump Tutorial with Examples — 50 Ways to Isolate Traffic
又一次打开了很久以前浏览过的一个Chip-8教程,Cowgod’s Chip-8,该教程的完成日期是 August 30, 1997 06:00:00
,比我年龄大一些😂,该网页使用的仅仅是最朴素的白底黑字布局,然而无论是内容质量,还是排版风格,都令人感觉无比舒适,感觉如果有一天这个网站不在了,也是一种遗憾,于是在此备份一下该网页,它的构成是纯html,十分简洁。
1 |
|
首先需要区分开 2^N
和 2N
。2N
很好判断,直接看数字能否被2整除即可。如果想用类似 2N
的思路去判别 2^N
,可能就需要递归计算 N=N/2
。
不过借助二进制表示的性质,可以简化 2^N
的判别。
假设一个正整数 val
的表示形式为1XX...X
。
假如它是2的幂,val
的表示形式必然是 100...0
,并且 val-1
的表示形式会是 011..1
。则有 val & (val-1) == 0
假如它不是2的幂, val
的表示形式会是 1XX...X
,它的末尾数字中至少有一位为1
, 则 val-1
无需借最开始的1
,val-1
表示形式会是 1XX...X
。则有 val & (val-1) != 0
而任意一个正整数 val
要么是2的幂,要么不是2的幂,上面讨论已经概括了所有的正整数。
上述两个命题的逆否命题分别为:
对于一个正整数 val
:
val & (val-1) != 0
,则它不是2的幂val & (val-1) == 0
,则它是2的幂1 | bool isPowOf2(int val) { |
Raspberry Pi 3B+
和 Newifi D2 CPU(Openwrt)
的地位是类似的,它们都同时属于 VLAN-1
和 VLAN-2
,eth0.1
处理 VLAN-1
的数据包,eth0.2
处理 VLAN-2
的数据包 Raspberry Pi 3B+
和 Newifi D2 CPU(Openwrt)
的不同之处仅仅在于,Raspberry Pi 3B+
通过外部接口LAN1
接入交换机,而Newifi D2 CPU(Openwrt)
通过内部接口Internal Port
接入交换机 Newifi D2 CPU(Openwrt)
内部通过网桥 br-lan
将 eth0.1
,wlan0
,wlan1
桥接在一起,使得 wlan0
,wlan1
可以和 eth0.1
所属的 vlan1
互通
LAN2/LAN3/LAN4
属于 untagged VLAN-1
WAN
属于 untagged VLAN-2
LAN1
和 Internal Port
属于 tagged VLAN-1
& tagged VLAN-2
树莓派的 eth0.1
属于 VLAN 1
,eth0.2
属于 VLAN 2
,到达 eth0
的数据包会根据其中包含的 VLAN ID
传递到对应的VLAN虚拟接口,而从VLAN虚拟接口发出的数据包也会打上对应的 VLAN ID
(后两句是我的猜测,然而由于我并不了解Linux协议栈以及虚拟接口的工作原理,无法保证准确性,大致工作原理可能类似)。其实路由器的 eth0.1
和 eth0.2
也是这样的。
配置方法
1 | sudo ip link add link eth0 name eth0.1 type vlan id 1 |
/etc/network/interfaces
中配置 1 | # interfaces(5) file used by ifup(8) and ifdown(8) |
eth0.1
配置静态IP树莓派默认会通过 dhcpcd
为各个接口配置IP,可以为树莓派的eth0.1
配置静态IP便于进行局域网访问并进一步作为网关。可以在 /etc/dhcpcd.conf
中配置:1
2interface eth0.1
static ip_address=192.168.2.10/24
如果仅仅希望阻止 dhcpcd
自动为某些接口通过局域网中的DHCP服务器配置IP,可以这样配置:1
2# deny
denyinterfaces eth0 eth0.2
Raspberry Pi 3B+
可以通过 eth0.2
与经 WAN
连接的运营商通信,就像 Newifi D2 CPU(Openwrt)
一样。
连接步骤
1 | sudo apt install pppoe pppoeconf pppstatus |
1 | sudo pppoeconf eth0.2 |
1 | sudo pon dsl-provider #建立连接 |
连接成功之后,通过 ip -d addr
就能看到PPPoE接口 ppp0
,下面的内容还顺便展示了 eth0.1
和 eth0.2
的接口信息,可以忽略其中的 wlan0
,树莓派的 wlan0
其实连接到了路由器的无线网络 Openwrt
当中,与本次实验关系不大。
1 | 2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000 |
如果树莓派在建立PPPoE之前不存在默认路由,那么在PPPoE连接过程中应该会自动添加至新建的 PPPoE接口 ppp0
的默认路由,否则,它将不会覆盖原本就存在的默认路由。
由于我的树莓派还同时经树莓派的无线网卡 wlan0
连接了路由器的无线网络,原本存在默认路由,故在连接PPPoE之后需要配置默认路由,从而确保树莓派通过其自身建立的 ppp0
连接互联网,而非路由器。
配置好之后的默认路由为PPPoE接口 ppp0
:
1 | pi@rasp:~ $ ip route |
在完成上述配置之后,如果不出意外的话,树莓派应该就可以连接互联网了,可以随便ping一下百度进行测试,或者ping一个知名如 114.114.114.114
,如果能ping IP却不能ping 域名,可以检查下DNS设置,在此不再赘述。
完成上面的配置之后,树莓派本身可以通过其建立的PPPoE连接互联网,但局域网中的其他设备却仍然无法访问互联网,那么如何使其他设备可以借助树莓派的PPPoE访问互联网呢?答案是NAT。
NAT的中文名称是网络地址转换,配置好NAT之后,局域网其他设备就可以将树莓派作为网关,然后该设备(称为设备A)的网络协议栈就会将目的地址为外部网络的数据包发往树莓派(IP数据包目的地址仍然是真实目的IP,MAC数据包的目的地址是树莓派,准确地说是树莓派的eth0.1
),树莓派会将原始IP数据包的 src
改为自身PPPoE接口ppp0
的IP地址,然后发往目标设备,并且,会将目标设备返回的IP数据包的 dst
改为设备A的IP地址,使得局域网设备可以透明地与互联网中的设备进行通信。
如果想要深入了解NAT,可以参考Network address translation - Wikipedia,NAT - Network Address Translation,前一篇文章侧重于NAT的概念和实现,后一篇文章侧重于Linux系统中NAT的配置和用途。
配置方法
配置NAT需要使用*nux的防火墙工具,比如 iptables
。关于防火墙可以参考Linux防火墙配置(iptables, firewalld)。
在本次实验当中,我使用iptables,并且配置为masquerade
,除了masquerade
还可以采用snat
。他们的区别在于,snat
需要指定要改写为哪个IP地址,这种情况适用于静态IP用户,而PPPoE分配的IP一般为动态IP,这时一般应该采用masquerade
,在这种情况下,无需指定要改写为哪个IP,内核会自动读取对应网络接口的IP地址进行改写。
不过在配置NAT之前,需要先开启Linux内核的数据包转发功能,默认情况下会禁止转发数据包。配置命令如下:
1 | sudo bash -c 'echo 1 > /proc/sys/net/ipv4/ip_forward' |
下面是NAT配置命令:
1 | sudo iptables -t nat -A POSTROUTING -s 192.168.2.0/24 -j MASQUERADE |
上面的命令的意思是这样的,在nat表中的POSTROUTING链中添加一条规则:如果IP数据包的 src
属于 192.168.2.0/24
网段,则在发送出去之前,改写其 src
为出口网络接口的IP地址。同时,该规则隐含了一条反向规则,将应答数据包的 dst
改为真实的 dst
(即被改掉的 src
)。实际实现过程中,只改IP应该是不够的,可能还需要修改sport
,并对应修改应答数据包的dport
。
完成上述配置之后,为局域网设备设置合适的IP地址,网关设为树莓派的局域网IP 192.168.2.10
,配置好DNS,应该就可以访问互联网了。
我的测试设备IP是这样的:
192.168.2.51
(Openwrt-5G经路由器的br-lan
与路由器的eth0.1
桥接在一起,从而能够接入vlan1) eth0.1
的IP为 192.168.2.10
,经eth0.2
建立的PPPoE连接 ppp0
的IP为 100.82.232.18
测试方法为,在笔记本电脑上 ping 8.8.4.4
,然后在树莓派的 eth0.1
和 ppp0
上抓包。测试结果如下:
1 | PS C:\Users\lpy> ping -n 4 8.8.4.4 |
1 | pi@rasp:~ $ sudo tcpdump -i eth0.1 dst 8.8.4.4 |
测试结果是:
ppp0
上可以看到 dns.google(即8.8.4.4
) 与 ppp0(即100.82.232.18
)之间的往来数据包,ppp0 发出 ICMP echo request
,然后 dns.google 回应 ICMP echo reply
eth0.1
上可以看到 dns.google(即8.8.4.4
) 与笔记本电脑(即192.168.2.51
)之间的往来数据包,笔记本电脑发出 ICMP echo request
,然后 dns.google 回应 ICMP echo reply
数据交换流程如下所示,由于这个例子是ICMP协议,不涉及端口号,如果是TCP/UDP还需要考虑端口号的转换:
需要再次强调一下,本例只是选用了最简单的ICMP协议,而应用程序大部分情况下使用的是TCP和UDP协议,在这种情况下,NAT是同时涉及IP和Port的,此图展示了两次对NTP服务器的访问,其中就涉及到了端口号。
dnsmasq
,提供DNS Server以及DNS缓存服务 先放结论,经过探究,Newifi D2的网络结构图是这样的:
Port | Switch Port |
---|---|
Internal (CPU) | 6 |
Internet (WAN) | 4 |
LAN1 | 3 |
LAN2 | 2 |
LAN3 | 1 |
LAN4 | 0 |
Interface Name | Description | Default configuration |
---|---|---|
br-lan | LAN & 2.4GHz WiFi & 5GHz WiFi | 192.168.1.1/24 |
vlan1 (eth0.1) | LAN ports(1 - 4) | None |
vlan2 (eth0.2) | WAN port | DHCP |
wlan0 | 2.4GHz WiFI | Disabled |
wlan1 | 5GHz WiFI | Disabled |
Newifi已经倒闭所以找不到官网产品信息页了,根据MediaTek | MT7621A/N的介绍,MT7621A内嵌 5端口千兆以太网交换机,下面是 MT7621A Datasheet
中的芯片功能模块图,图中也可以看出WAN口和4个LAN口同属于一个Switch,他们都是交换机的端口。除了以太网交换机,芯片还包含了两个以PCIe方式连接的WiFi模块,11n WiFi
和 11ac WiFi
,分别用于2.4GHz和5GHz。
下面是摘自 MT7621 Giga Switch Programming Guide
的交换机功能模块图以及部分寄存器配置截图,该文档描述了 MT7621A
内嵌可编程交换机的结构以及编程方法,可以看到内嵌可编程交换机共有 7 个Port,其中有5个Port引出了网口,根据交换机编程手册,可以配置 CPU_PORT
的 Port Number。
我也不懂设备树文件(Device Tree Source)的具体语法,但阅读下面两段代码,大致可以看出Openwrt源码对Newifi D2硬件交换机的配置方式为:
Port0 - Port3
记为 lan4 - lan1
Port4
记为 wan
Port6
定义为 CPU端口
这些都是对 MT7621A
中集成的硬件交换机的配置。
1 | // From: /target/linux/ramips/dts/mt7621.dtsi |
1 | // From: /target/linux/ramips/dts/mt7621_d-team_newifi-d2.dts |
通过 ls /sys/class/net/ -al
可以查看系统中所有的网络设备:
1 | root@OpenWrt:~# ls /sys/class/net/ -l |
根据符号链接指向的位置,可以分辨出Newifi D2中有三个物理网络设备,五个虚拟网络设备。
物理设备有:
虚拟设备有:
通过 ip -d addr
可以查看系统中接口以及其IP地址信息:
1 | 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000 |
接下来,我们逐一分析一下各个接口所展示的信息,为了便于展示以及分析,忽略IPv6地址(已从输出中删掉):
127.0.0.1
20:76:93:44:fe:97
,无IP地址 20:76:93:44:fe:97
,IP地址为 192.168.2.1
eth0
的VLAN设备,VLAN id为1,MAC地址为 20:76:93:44:fe:97
,无IP地址,bridge_slave
表示它是网桥的从设备 eth0
的VLAN设备,VLAN id为2,MAC地址为 20:76:93:44:fe:98
,无IP地址 100.82.7.242
20:76:93:44:fe:96
,无IP地址,bridge_slave
表示它是网桥的从设备 20:76:93:44:fe:98
,无IP地址,bridge_slave
表示它是网桥的从设备 系统自带了 brctl
,通过 brctl show
可以查看系统中的桥信息:
1 | bridge name bridge id STP enabled interfaces |
也可以另外安装 ip-bridge
包,然后使用 bridge
命令进行Linux虚拟网桥操作,比如 bridge link
可以展示桥中的接口:
1 | 6: eth0.1@eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 master br-lan state forwarding priority 32 cost 100 |
上面的信息表明,网桥 br-lan
中包含三个设备:eth0.1
,wlan0
,wlan1
根据上面对各个接口的分析,得知 br-lan
有IP地址,eth0.1
,wlan0
,wlan1
均没有IP地址,而网桥的作用就类似于交换机,可以在桥中各个设备直接交换MAC帧,这三者的关系如下图所示:
1 | +--------------------------------------------------------------------+ |
网络配置位于 /etc/config/network
:
1 |
|
通过查看硬件信息,以及系统中的网络信息,可以大致得知整体的网络拓扑结构为:
网桥是一种网络设备,它能够从多个通信网络创建一个单一的聚合网络,此功能被称为网络桥接。网络桥接不同于路由,路由允许多个独立网络互相通信,但各个网络仍然保持独立,而桥接是将多个独立网络连接成一个单一网络。
网桥工作于OSI模型的数据链路层,它基于以太网地址(而不是IP地址)转发数据包。交换机是一种常见的网络桥接设备,家用路由器内部一般也带有集成交换机。
Linux bridge实现了802.1d标准的一个子集,Linux网桥比纯粹的硬件网桥更加强大,它不仅可以转发数据包,还可以进行过滤和流量调整等功能。
下面介绍一下Linux bridge的配置工具,然后利用树莓派上进行一个简单的桥接实验。
iproute2程序包包含的 ip
和 bridge
命令可用于管理网桥。
创建bridge设备 br0
1 | ip link add name br0 type bridge |
将interface eth0
添加到bridge br0
1 | ip link set eth0 master br0 |
上面是以物理网卡 eth0
为例,但网桥中的设备不仅限于物理设备,tun/tap/veth/vlan/macvlan/macvtap/ipvlan/ipvtap
等各种虚拟设备均可添加到 bridge。
eth0
原本一端连接着外界物理网络,另一端连接着网络协议栈。一旦它接入了 br0
,由外部传入 eth0
的数据包将被直接传递到 br0
,而不再是网络协议栈。这样一来,为 eth0
配置IP就失去了意义,因为 eth0
另一端连接的只是一个虚拟网桥 br0
,这是一个2层设备,它工作于数据链路层,仅基于MAC地址进行数据包转发,形象地说,也就是 eth0
现在只是相当于 br0
的一个 Port,把 br0
比作以太交换机,那么 eth0
就是 br0
的一个网口。
原本的网络结构:
eth0
连接着网络协议栈与外界网络
1 | +-------------------------------------------------+ |
之后的网络结构:
eth0
收到的外界数据包不再发给网络协议栈,而是虚拟2层设备 br0
1 | +-------------------------------------------------+ |
开启bridge br0
1 | set link br0 up |
bridge link
:展示桥接到网桥的interface(相当于交换机的网口) bridge fdb
:展示网桥的转发表bridge vlan
:展示网桥的VLAN配置信息由上面的分析,可以得知Linux虚拟网桥设备 bridge
可以将多个 interface 连接为一个网络,并且虚拟网桥设备有自己的MAC转发表,也可以在虚拟网桥中配置VLAN。
eth0
,一个无线网卡 wlan0
br0
eth0
,WiFi设备接入基于 wlan0
构建的无线局域网 eth0
和 wlan0
添加到 br0
,利用br0
将无线局域网和PC桥接在一起 这样一来,网络结构如下图所示(略去了Network Protocol Stack -> eth0 的单向连接):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
32
33 +-------------------------------------------------+
| +----------------------------------------+ |
| | Network Protocol Stack | |
| +----------------------------------------+ |
| ↑ |
|........................|........................|
| ↓ |
| +----------------------------------+ |
| | br0 | |
| +----------------------------------+ |
| ↑ ↑ |
| | | |
| ↓ ↓ |
| +--------------+ +--------------+ |
| | eth0 | | wlan0 |<---------> WiFi_Device
| +--------------+ +--------------+ |
| ↑ ↑ ↑ |
+------------|------------------|--------|--------+
↓ ↓ ↓
PC WiFi_Device WiFi_Device
```
根据其原理,只要合理地配置PC和无线设备的IP地址,它们理应可以借助虚拟网桥 `br0` 互相访问。
### 关键步骤
#### 树莓派配置AP
借助 `hostapd` 实现,大致步骤为:
1. 安装 `hostapd`
```shell
sudo apt install hostapd
hostapd
,创建 /etc/hostapd/hostapd.conf
1 | interface=wlan0 |
dhcpcd
,启用 hostapd
1 | sudo disable dhcpcd |
RaspberryPi
RaspberryPi
了,由于树莓派没有开启dhcpd,WiFI设备需要配置使用Static IP,比如 192.168.31.100/24
br0
1 | sudo ip link add name br0 type bridge |
eth0
,wlan0
至 br0
1 | sudo ip link set dev eth0 master br0 |
由于树莓派没有开启dhcpd,需要为各个连接到WiFI的设备配置静态IP,当然经网线连接的PC也需要配置,各个设备的IP需要处于同一网段才能互相访问,比如 192.168.31.0/24
PC和无线设备都配置好了却无法互相连接
关掉Windows防火墙再试试,稍等一会儿再试试
在Networking: bridge - Linux Foundation DokuWiki中有这样一句话:
The bridge will take a short amount of time when a device is added to learn the Ethernet addresses on the segment before starting to forward.
暂时没有2
PC ping通了连接WiFI的手机,成功借助虚拟网桥连通了eth0
和wlan0
无线局域网。
1 | Script started on 2021-07-30 20:07:21+08:00 [TERM="linux" TTY="/dev/tty1" COLUMNS="240" LINES="67"] |
192.168.100.100
192.168.100.102
抓包命令:sudo tcpdump -i br0
可以看到两台设备之间的 ICMP echo request 和 ICMP echo reply,至于为什么没有互相询问ARP的消息,大概是因为在我截取消息的时候他们已经知道了对方的MAC。
1 | 19:56:53.340646 ARP, Request who-has 192.168.17.1 tell 192.168.17.100, length 28 |
十几年的老BUG了,Win7时代的修改注册表的方法也失效了。
Win7时代的方案在这里:CTRL-Space always toggles Chinese IME (Windows 7)
需要注意的是:
HKEY_CURRENT_USER/Control Panel/Input Method/Hot Keys
保存的是当前用户的快捷键配置HKEY_USERS\.DEFAULT\Control Panel\Input Method\Hot Keys
保存的是系统默认的快捷键配置当年的方案修改的是用户快捷键配置,现已失效,现在需要修改系统默认的快捷键配置。
HKEY_USERS\.DEFAULT\Control Panel\Input Method\Hot Keys
00000070
是繁体中文快捷键配置,00000010
是简体中文快捷键配置 Key Modifiers
代表 Alt/Ctrl/Shift等修饰键,默认配置是 Ctrl (02c00000)
,把它的第一个字节由 02
改为 00
Virtual Key
代表与修饰键配合使用的辅助键,默认配置为 Space (20000000)
,把它的第一个字节由 20
改为 FF
Ctrl+Space
就无效了,如果只改系统默认配置无效的话,可以再把用户配置也按相同方法改掉,再不行的话就等微软修复BUG吧~~~ 在Ubuntu Server 20.04 虚拟机当中添加了两块虚拟网卡。一块VMWare虚拟网卡以桥接模式使用电脑的网线接口直接与树莓派通信,可拥有独立于物理网卡的Mac地址和IP地址,同时在树莓派上测试了Macvlan虚拟网卡,Macvlan虚拟网卡有多种工作模式,其中桥接模式可使虚拟网络接口借助物理接口与外界直接通信。另一块VMWare虚拟网卡配置为NAT模式,借助主机网络访问互联网。
VMWare默认的桥接网络会自动选择用于桥接的实体网卡,如果需要指定通过网线接口而非无线网卡桥接,就需要专门进行配置。在虚拟网络编辑器当中为默认桥接网络指定网卡即可。
NAT网卡无需特别配置,默认的配置已经开启了DHCP,但是需要注意的是网关可能不是 192.168.227.1
(以我的IP为例说明) ,我原本在虚拟机中配置了静态地址,结果无法访问互联网,也无法ping通 192.168.227.1
,后来改成了 DHCP 模式,发现虚拟机自动配置的网关是 192.168.227.2
,而Windows中可以看到 VMware Virtual Ethernet Adapter for VMnet8
的IP是 192.168.227.1
,值得关注的还有 IPv4 WINS 服务器: 192.168.227.2
,通过查看虚拟机上自动经DHCP配置好的信息,发现虚拟机的网关和DNS服务器恰好都是 192.168.227.2
,因此如果想要配置静态IP,需要注意网关和DNS服务器的配置。我所采用的方法是在开启DHCP Client的同时再额外配置一个自定义的IP地址,这样既可以在局域网通过自定义IP地址访问虚拟机,也可以使虚拟机自动配置网关和DNS服务器。
虚拟机ping不通主机是因为Windows防护墙,关掉就能ping通了,不过即使能ping通,也无法将 192.168.227.1
配置为网关来访问互联网,原因上面说了。
后来发现VMWare网络编辑器里面是能看到网关信息的,确实是 192.168.227.2
,如果想要手动配置,就按这个信息配置。
Ubuntu Server 20.04 使用 netplan
进行网络管理(总是旧的还不熟悉就换了,,),还默认启用 systemd-resolve
进行域名解析,所以网络配置文件不再是 /etc/network/interfaces
,DNS配置文件也不再是 /etc/resolv.conf
。
网络配置文件是 /etc/netplan/*
,我就直接修改了安装时生成的文件 /etc/netplan/00-installer-config.yaml
,修改完毕之后需要执行 sudo netplan try
命令检测配置并按Enter应用配置。
DNS配置文件我没有仔细查,看systemd-resolve
文档即可,不过/etc/resolv.conf
的开头是这样的,最好不要乱改。顺便说一下,查看DNS服务器的命令是 resolvectl status
。1
# This file is managed by man:systemd-resolved(8). Do not edit.
1 | # This is the network config written by 'subiquity' |
ens33
是桥接网卡,配置文件中仅为其配置了静态IP地址,并关闭了dhcpv4,其余的网关、DNS等配置被注释掉是因为如果保留的话,netplan
将为根据设定的网关添加默认路由,而且该默认路由的优先级比 ens38
的默认路由要高,导致各种外网请求走没有互联网连接的 ens33
。这里只为 ens33
配置IP,不配置网关,就可以避免 netplan
添加相应的默认路由。ens38
本来采取的是纯静态配置,结果无法访问互联网,原因在上面讨论了,这里采用了较为稳妥的 DHCP + 静态IP
的方法,既能通过固定IP便捷访问,又能借助DHCP自动配置网关和DNS。之前使用路由器多拨时接触过MacVLAN,感觉十分神奇,居然可以通过一个实体网络接口拓展出众多虚拟接口,并且每个都可以拥有自己的Mac地址和IP,而多拨原理就是每个虚拟接口都拨号,然后带宽叠加。
最近了解到相关虚拟技术是由Linux内核提供支持的,从最初的TUN/TAP,发展到后来的MacVLAN和MacVTap,原本我是打算创建一个MacVTap虚拟接口的,结果好像报错说不支持该类型,就用了MacVLAN,并顺利为虚拟接口配置了IP,经测试可以连通。
配置工具就是一系列 ip 命令,在此不再赘述。下图当中的 macvlan0@eth0
即为基于 eth0
的虚拟网络接口,需要注意的一点是,在使用 ip 命令配置的过程中,使用 macvlan0
引用虚拟网卡,不需要带也不能带 @eth0
以前没有仔细考虑过这件事情,感觉很神奇,在这里简单地说一下现在的想法。
先来回顾一下交换机工作原理:
交换机工作于OSI参考模型中的第二层(数据链路层),交换机的工作依赖于对MAC地址的识别(所有的网络设备都有一个唯一的MAC地址,通常是由厂商直接烧录进网卡中)。
当交换机从其某个端口收到一个数据包时,先读取包头中的源MAC地址(即发送该数据包的设备网卡的MAC地址),将该MAC地址和端口对应起来添加到交换机内存里的地址表中;然后再读取包头中的目的MAC地址,对照内存里的地址表看该MAC地址与哪个端口对应,如果地址表中有该MAC地址的对应端口,则将该数据包直接复制到对应的端口上,如果没有找到,则将该数据帧作为一个广播帧发送到所有的端口,对应的MAC地址设备会自动接受该帧数据,同时,交换机将接受该帧数据的端口与这个目的MAC地址对应起来放入内存中的地址表中。
简单地说交换机的作用就是转发MAC帧,物理交换机可以用于在一些物理接口之间转发MAC帧。
那么网络接口如何实现虚拟化呢?在没有虚拟接口的情况下,一个网卡对应一个MAC地址,交换机按照转发表,把对应的MAC帧发送到特定的物理端口。只要在此基础上,允许一个物理网卡对应多个MAC地址,那么每个MAC地址都可以配置一个虚拟接口,物理网卡所属机器在拿到MAC帧之后,再根据具体内容分发到虚拟接口即可。而一个物理网卡对应多个MAC地址其实只要允许交换机转发表中一个接口对应多个表项即可。
上面说的是数据链路层的虚拟化,只要实现了数据链路层的虚拟化,那么每个虚拟接口都可以独立地实现局域网通信了,它们是物理端口还是虚拟端口对于上层协议栈来说其实是透明的。
如果接口是用软件虚拟的,那么自然地,就可以同样利用软件在这些虚拟接口间转发MAC帧,这些软件就相当于虚拟交换机了?
Virtual networking: TUN/TAP, MacVLAN, and MacVTap
虚拟交换机(vSwitch)原理及配置
Open vSwitch
MacVTap - Linux Virtualization Wiki
Macvlan and IPvlan basics
由于原来的系统出了一些问题,今日重装了Windows10系统,下面简要记录一下各种基本设定以及开发环境的配置过程,供今后参考。
系统安装基本没什么好说的,只是需要注意如果有多块磁盘,并且其中一个磁盘存在ESP分区(Fat32),则在重装Win10时,即使选择安装到另一块未分区的磁盘,似乎Win10安装器也不会在另一块磁盘重建ESP分区,而会使用已有的ESP分区,如果想要Win10安装器在操作系统所在磁盘重建ESP分区,可以把另一块磁盘的ESP分区删掉。
对于无洁癖用户大可不必这样操作,不过给Win10预留一个未分区磁盘,还是比分好一个NTFS分区给它用要好的, 这样Win10安装器似乎会建立好几个分区,也许多余的分区是用于系统恢复的。
efibootmgr
,至少Arch系、Debian系的系统都自带这个工具。 EasyUEFI
,不过试用版过期后就没法用了,即使重装、删除注册表项,也难以重新试用,可以用pj版。 如果按照Win10默认的引导进行配置,它会诱导用户登录微软账户,并自动把微软账户名的前缀(邮箱前缀)作为系统用户名,作为一个经常使用终端的CSer,是无法容忍奇奇怪怪的用户名的。
如果想要自定义用户名,需要先以本地账户登录,自己起一个用户名,之后再切换到微软账户登录即可。
卸载一系列自带的无用软件。
登录Onedrive并开启个人文件夹同步,打开 文档 文件夹的同步功能,这样应该可以实现微信和QQ聊天记录的同步,写此文时尚未安装QQ和微信,所以不是特别确定。
本次重装彻底删掉了Ubuntu系统,打算迁移到WSL2,毕竟双系统切换太麻烦了。
按照官方教程配置即可,主要步骤大概如下:
由于wsl2的ip地址是动态配置的,而自动端口映射功能也并不稳定,为了便捷地从win10访问wsl2,一种可靠的方案是动态获取wsl2的ip地址并在win10的hosts文件中为其设置域名。
go-wsl2-host 项目实现了这个功能,参照项目说明进行配置,使用时可能会遇到一些问题:
开始->Windows管理工具->本地安全策略->本地策略->用户权限分配->作为服务登录->添加用户或组
,把相应用户添加进去 wsl2host debug
命令查看程序输出,如果程序提示无法写入hosts,检查hosts文件是否为只读,把只读属性取消即可。hosts路径为 C:\Windows\System32\drivers\etc\hosts
WSL2不支持systemd,也就无法通过systemctl配置自启服务,可以在 /etc/init.wsl
中写入一些启动服务的命令,并在Windows中配置开机执行 wsl -u root /etc/init.wsl
。可以利用Win10的 任务计划程序
配置执行上述命令。
我导出的任务计划程序配置(wsl2-init.xml)如下: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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
<RegistrationInfo>
<Date>2021-07-01T20:23:06.8672886</Date>
<Author>OMEN\lpyhe</Author>
<Description>Windows开机时自动运行WSL2并且运行 /etc/init.wsl</Description>
<URI>\Custom\WSL-INIT</URI>
</RegistrationInfo>
<Triggers>
<BootTrigger>
<Enabled>true</Enabled>
</BootTrigger>
</Triggers>
<Principals>
<Principal id="Author">
<UserId>S-1-5-21-226882884-566579671-1051654333-1001</UserId>
<LogonType>Password</LogonType>
<RunLevel>LeastPrivilege</RunLevel>
</Principal>
</Principals>
<Settings>
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
<DisallowStartIfOnBatteries>true</DisallowStartIfOnBatteries>
<StopIfGoingOnBatteries>true</StopIfGoingOnBatteries>
<AllowHardTerminate>true</AllowHardTerminate>
<StartWhenAvailable>false</StartWhenAvailable>
<RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
<IdleSettings>
<StopOnIdleEnd>true</StopOnIdleEnd>
<RestartOnIdle>false</RestartOnIdle>
</IdleSettings>
<AllowStartOnDemand>true</AllowStartOnDemand>
<Enabled>true</Enabled>
<Hidden>false</Hidden>
<RunOnlyIfIdle>false</RunOnlyIfIdle>
<WakeToRun>false</WakeToRun>
<ExecutionTimeLimit>PT72H</ExecutionTimeLimit>
<Priority>7</Priority>
</Settings>
<Actions Context="Author">
<Exec>
<Command>wsl</Command>
<Arguments>-u root /etc/init.wsl</Arguments>
</Exec>
</Actions>
</Task>
~
在windows terminal中配置wsl的启动目录为 //wsl$/Ubuntu-20.04/home/用户名/
,可能需要根据实际情况修改wsl名称和用户名。
Win10配置守护服务不像Debian、Arch的systemd那样方便,可以借助第三方程序实现,比如 WinSW。
下面是一个将frpc配置为服务的配置示例:
1 | <service> |
可用于配置快捷键,下面的脚本将 Ctrl + Alt + T
配置为启动Windows Terminal
的快捷键。将写好的ahk脚本放到 C:\Users\用户名\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup
中即可开机自启。
1 | ^!t:: |
推荐使用 update-alternatives
进行配置,这样便于管理。为python3添加alternatives的命令为:
1 | sudo update-alternatives --install /usr/bin/python python /usr/bin/python3 30 |
首先安装 vim-plug,然后准备好vim配置文件 ~/.config/nvim/init.vim
, 接下来启动 vim
等待各种插件安装完成,其中Coc插件需要高版本的node,官方源版本过低,参考下一条进行node的安装。
我的Vim配置文件:
1 | " setting |
官方源中版本过低,很多软件都要求更高版本的node,建议使用nodesource维护的软件源安装,具体参考项目主页。写此文时,安装长期维护版node的具体命令为:1
2
3# Using Ubuntu
curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash -
sudo apt-get install -y nodejs
之前实现RSA加密算法时,计算幂成了程序的瓶颈,前段时间了解了快速幂以及快速幂模运算,这种算法可以用于加速RSA加密解密过程中的幂模计算过程。
用精确的数学符号表示的话,有下面两种写法:
$$ a ^ b = {\begin{cases}(a ^ {\frac b 2}) ^ 2, &if\ b\ is\ even\\a \cdot (a ^ {\frac {b-1} 2}) ^ 2, &if\ b\ is\ odd\end{cases}}$$或
$$ a ^ b = {\begin{cases}(a ^ {\lfloor {\frac b 2} \rfloor}) ^ 2, &if\ b\ is\ even\\a \cdot (a ^ {\lfloor {\frac b 2} \rfloor}) ^ 2, &if\ b\ is\ odd\end{cases}}$$上述公式显然成立,故计算幂可用上述递推式进行。
而在编程语言中,整除一般就相当于向下取整,故上述递推式可以表示成下面的伪代码:
1
2
3
4
5
6
7func pow(a, b):
if b is 0:
return 1
if b is even:
return pow(a, b/2) ^ 2
if b is odd:
return pow(a, b/2) ^ 2 * a
对于 $a^b$,用原始的求幂算法,需要计算 $b$ 次乘法,用上述递推式大约需要运算 $log\ b$ 次
下面的代码实现了朴素求幂算法和快速幂算法,并对比了他们的运算时间。
1 | import time |
求模运算有这样一条性质:
利用这一性质,结合快速幂递推式可得:
$$ a ^ b \% m = {\begin{cases}(a ^ {\lfloor {\frac b 2} \rfloor} \% m) ^ 2 \%m, &if\ b\ is\ even\\(a \%m \cdot (a ^ {\lfloor {\frac b 2} \rfloor} \%m) ^ 2) \%m, &if\ b\ is\ odd\end{cases}}$$表示成伪代码如下所示:
1 | func pow_mod(a, b, m): |
1 | def pow_mod(a, b, m): |
RSA加密和解密需要计算大数幂的模,用原始计算方法难以进行,可以用快速幂模运算加速运算过程。
下面的代码是对 2021-05-23-RSA加密算法原理
中RSA密钥生成、加密、解密的DEMO的改进。
具体代码如下:
1 | from typing import Tuple |
RSA加密算法是一种非对称加密算法。
想要完整地理解RSA加密算法,需要了解不少数论知识,下面介绍几点核心内容。
同余是数论中的一种等价关系。两个整数 $a$、$b$,若它们除以正整数 $m$ 所得的余数相等,则称 $a$、$b$ 对于模 $m$ 同余,表示为:
$$a \equiv b \quad (mod \quad m)$$
同余式有多种性质:
整除性
$$a\equiv b{\pmod {m}}\Rightarrow c\cdot m=a-b,c\in \mathbb {Z}$$传递性
$$ \left.{\begin{matrix}a\equiv b{\pmod {m}}\\b\equiv c{\pmod {m}}\end{matrix}}\right\} \Rightarrow a\equiv c{\pmod {m}}$$保持基本运算
$$\left.{\begin{matrix}a\equiv b{\pmod {m}}\\c\equiv d{\pmod {m}}\end{matrix}}\right\}\Rightarrow \left\{{\begin{matrix}a\pm c\equiv b\pm d{\pmod {m}}\\ac\equiv bd{\pmod {m}}\end{matrix}}\right.$$当 $c=d$ 时,则为等量加法、减法:$a\pm c\equiv b\pm c{\pmod {m}}$
此性质更可进一步引申成:
$$ a\equiv b{\pmod {m}}\Rightarrow {\begin{cases}an\equiv bn{\pmod {m}},\forall n\in \mathbb {Z} \\a^{n}\equiv b^{n}{\pmod {m}},\forall n\in \mathbb {N} ^{0}\end{cases}}$$更多性质见同余 - 维基百科
在数论中,欧拉定理)是一个关于同余的性质。
若 $n$, $a$ 为正整数,且 $n$, $a$ 互质,即 $gcd(n,a)=1$,则
$$a^{\varphi(n)}\equiv1 \quad (mod \quad n)$$
其中 $\varphi(n)$ 为欧拉函数。
在数论中,对正整数 $n$,欧拉函数 $\varphi (n)$ 是小于或等于 $n$ 的正整数中与 $n$ 互质的数的数目。
特别地:
扩展欧几里得算法是欧几里得算法的扩展。已知整数 $a$、$b$,扩展欧几里得算法在求得 $gcd(a,b)$ 的同时,能找到整数 $x$、$y$,使它们满足裴蜀等式:
$$ax+by=gcd(a,b)$$
扩展欧几里得算法可用于求解裴蜀等式:
$$ax+by=m$$
当且仅当 $m$ 是 $a$ 与 $b$ 的最大公约数的倍数时,该方程有解,且有解时必有无数组解。
模反元素也称为模倒数,或者模逆元。
一整数 $a$ 对同余 $n$ 之模逆元是指满足以下公式的整数 $b$
$$a^{-1}\equiv b{\pmod {n}}$$
也可以写成以下的式子
$$ab\equiv 1{\pmod {n}}$$
或者
$$ab{\pmod n}=1$$
由定义可得:
$$ab = 1 + kn \Rightarrow ab - kn = 1$$
要求解 $b$,就需要求不定方程 $ax + ny = 1$ 的解,根据裴蜀定理,当且仅当 $1$ 是 $gcd(a, n)$ 的整数倍时,即 $a$ 与 $n$ 互质时,上述方程有解。用扩展欧几里得算法求得的 $x$ 即为模反元素 $b$。
假设Alice想要通过一个不可靠的媒体接收Bob的一条私人信息。她可以用以下的方式来产生一个公钥和一个私钥:
Alice将她的公钥 $(N,e)$ 传给Bob,而将她的私钥 $(N,d)$ 藏起来。
假设Bob想给Alice送一个消息,他知道Alice产生的 $N$ 和 $e$。他使用起先与Alice约好的格式将消息转换为一个小于 $N$ 的非负整数 $m$,比如他可以将每一个字转换为这个字的Unicode码,然后将这些数字连在一起组成一个数字。假如他的信息非常长的话,他可以将这个信息分为几段,然后将每一段转换为 $m$。用下面这个公式他可以将 $m$ 加密为 $c$:
$$ c\equiv m^e {\pmod N}$$
计算 $c$ 并不复杂。Bob算出 $c$ 后就可以将它传递给Alice。
Alice得到Bob的消息 $c$ 后就可以利用她的密钥 $N$ 和 $d$ 来解码。她可以用以下这个公式来将 $c$ 转换为 $m$:
$$m \equiv c^d {\pmod N}$$
得到 $m$ 后,她可以将原来的信息重新复原。
根据加密公式可得:
$$\begin{equation}\begin{split}&c \equiv m^e {\pmod N} \Rightarrow\\\\&(c)^d \equiv (m^e)^d {\pmod N} \Rightarrow\\\\&c^d \equiv m^{ed} {\pmod N}\\\\\end{split}\end{equation}$$只要能证明下面的式子,就说明解密公式是正确的:
$$\begin{equation}c^d \equiv m^{ed} \equiv m {\pmod N}\end{equation}$$证明:
根据密钥推导过程,可知 $ed = 1 + h\varphi(N) $,则
$$m^{ed} = m^{1+h\varphi(N)} = (m^{\varphi(N)})^h m$$
若明文 $m$ 与 $N$ 互质,根据欧拉定理可得:
$$\begin{equation}\begin{split}&m^{\varphi(N)} \equiv 1 {\pmod N} \Rightarrow\\\\&(m^{\varphi(N)})^h \equiv (1)^h {\pmod N} \Rightarrow\\\\&(m^{\varphi(N)})^hm \equiv (1)^hm {\pmod N} \Rightarrow\\\\&m^{ed} \equiv m {\pmod N} \Rightarrow\\\\&c^{d} \equiv m {\pmod N}\\\\\end{split}\end{equation}$$若明文 $m$ 与 $N$ 不互质,由于 $N=pq$,且$p$、$q$均为质数,而 $0 < m < N$,故 $m=kp$ 或 $m=kq$,下面以 $m=kp$ 为例展开证明:
如果 $m=kp$,则 $m$ 不会是 $q$ 的倍数,否则 $m$ 就会大于等于 $N$,违反了前提条件,而 $q$ 又是质数,故 $m$ 与 $q$ 互质,根据欧拉定理可得:
$$\begin{equation}\begin{split}&m^{\varphi(q)} \equiv 1 {\pmod q} \Rightarrow\\\\&(m^{\varphi(q)})^{\varphi(p)} \equiv (1)^{\varphi(p)} {\pmod q} \Rightarrow\\\\&m^{\varphi(q)\varphi(p)} \equiv 1 {\pmod q} \Rightarrow\\\\&m^{\varphi(N)} \equiv 1 {\pmod q} \Rightarrow\\\\&m^{h\varphi(N)} \equiv 1 {\pmod q} \Rightarrow\\\\&m^{1+h\varphi(N)} \equiv m {\pmod q} \Rightarrow\\\\&(kp)^{1+h\varphi(N)} \equiv kp {\pmod q} \Rightarrow\\\\\end{split}\end{equation}$$
由上式可得:
$$\begin{equation}\begin{split}&kp(kp)^{h\varphi(N)} = kp + tq\\\\\end{split}\end{equation}$$由于$p$ 与 $q$ 互质,要使该式成立,须有 $t=t’p$,代入上式可得:
$$\begin{equation}\begin{split}&(kp)^{1+h\varphi(N)} = kp + t‘qp \Rightarrow\\\\&(kp)^{1+h\varphi(N)} = kp + t‘N \Rightarrow\\\\&m^{1+h\varphi(N)} = m + t‘N \Rightarrow\\\\&m^{1+h\varphi(N)} \equiv m {\pmod N} \Rightarrow\end{split}\end{equation}$$结合 $m^{ed} = m^{1+h\varphi(N)} = (m^{\varphi(N)})^h m$ 可得:
$$\begin{equation}\begin{split}&c^d \equiv m^{ed} \equiv m^{1+h\varphi(N)} \equiv m {\pmod N}\end{split}\end{equation}$$基本原理与加密、解密基本一致,只是用私钥加密,用公钥解密。
假设偷听者Eve获得了Alice的公钥 $n$ 和 $e$ 以及Bob的加密消息 $c$,但她无法直接获得Alice的密钥 $d$。要获得 $d$,最简单的方法是将 $n$ 分解为 $p$ 和 $q$,这样她可以得到同余方程 $ed\equiv 1{\pmod{(p-1)(q-1)}}$并解出 $d$,然后代入解密公式
$$c^{d}\equiv m\ (\mathrm {mod} \ n)$$
导出m(破密)。但至今为止还没有人找到一个多项式时间的算法来分解一个大的整数的因子,同时也还没有人能够证明这种算法不存在(见因数分解)。
至今为止也没有人能够证明对 $n$ 进行因数分解是唯一的从 $c$ 导出 $n$ 的方法,直到今天也还没有找到比它更简单的方法。(至少没有公开的方法。)
因此今天一般认为只要 $n$ 足够大,那么黑客就没有办法了。
假如 $n$ 的长度小于或等于256位,那么用一台个人电脑在几个小时内就可以分解它的因子了。1999年,数百台电脑合作分解了一个512位长的 $n$。一个由Shamir 和Tromer在2003年从理论上构建的硬件TWIRL,使人们开始质疑1024位长的 $n$ 的安全性,目前推荐 $n$ 的长度至少为2048位。
1994年彼得·秀尔(Peter Shor)证明一台量子计算机可以在多项式时间内进行因数分解。假如量子计算机有朝一日可以成为一种可行的技术的话,那么秀尔的算法可以淘汰RSA和相关的派生算法。(即依赖于分解大整数困难性的加密算法)
假如有人能够找到一种有效的分解大整数的算法的话,或者假如量子计算机可行的话,那么在解密和制造更长的钥匙之间就会展开一场竞争。但从原理上来说RSA在这种情况下是不可靠的。
下面的代码根据上述原理实现了一个简单版本的RSA密钥生成、加密、解密的DEMO。
本以为应该不难实现,结果发现大质数的生成本身就是问题,除此之外还有其它的一些问题。关键问题以及大致思路:
质数的生成,经过查阅资料,得知大致方法是随机选取大整数,并通过一些方法判断它是不是质数,然而暴力的方法复杂度极高,目前有几种比较有效的质数判定方法,在此不再赘述,可自行查阅相关资料。下列程序当中使用了 Crypto
模块的素数生成功能。
公钥参数 $e$ 的生成, $e$ 需要与 $\varphi(N)$ 互质,一种简单的思路是选取一个质数 $e$,只要 $\phi(N)$ 不是它的整数倍即可。
具体代码如下:
1 | from typing import Tuple |
扩展欧几里得算法 - 维基百科
同余 - 维基百科
欧拉定理 - 维基百科)
欧拉函数 - 维基百科
模反元素 - 维基百科
RSA加密算法 - 维基百科
阮一峰:RSA算法原理(一)
阮一峰:RSA算法原理(二)
阮一峰:数字签名是什么?
RSA算法的数学原理与证明
上一篇文章简单介绍了欧几里得算法,即辗转相除法,它可以用于求解任意两个自然数 $a$ 和 $b$ 的最大公约数。而扩展欧几里得算法在求得 $gcd(a.b)$ 的同时,也能找到整数 $x$ 和 $y$ ,使它们满足裴蜀等式:
$$ax+by=gcd(a,b)$$
在数论中,裴蜀定理是一个关于最大公约数的定理。
对任意两个整数 $a$、$b$,设 $g=gcd(a,b)$ 是它们的最大公约数。那么关于未知数 $x$ 和 $y$ 的线性裴蜀等式:
$$ax+by=m$$
有整数解 $(x, y)$ 当且仅当 $m$ 是 $g$ 的整数倍。
裴蜀等式有解时必然有无穷多个解,每组解 $(x,y)$ 都称为裴蜀数。
维基百科中证明步骤的大概思路是这样的:
(1) 给出集合 $A = {ax+by|(x,y)\in\mathbb{Z}^2}$
(2) 通过$A\cap\mathbb{N}^*\neq\emptyset$,$\mathbb{N}$良序等讨论,指出$A$中必然存在最小正元素$d_0=ax_0+by_0$基本逻辑是这样的,中包含了$A$中所有的正元素,且$A\cap\mathbb{N}^*$是良序集合自然数集合$\mathbb{N}$的非空子集,故其中存在最小元素。也就是说集合$A$中存在最小正元素,记为$d_0$。
(3) 假设$p=ax+by$为$A$中任何一个正整数,试图计算$p$对$d_0$的带模除法,记为$p=qd_0+r$,因为$d_0$是$A$中最小正整数,故另一正整数$p$必然满足$p \geq d_0$,故$q > 0$,$0\leq r < d_0$
(4) 可得$r=p-qd_0=ax+by-q(ax_0+by_0)=a(x-qx_0)+b(y-qy_0)$,根据其形式可判断$r\in A$,结合上一条的结论$0\leq r < d_0$,且$d_0$是$A$中的最小正元素,可推断$r=0$,则可知$A$中任意正整数$p$都满足$p=qd_0$,即$A$中任意正整数$p$都是$d_0$的整数倍,记为$d_0|p$,并且显然$a \in A,b \in A$,故$d_0|a$,$d_0|b$,于是可知$d_0$是$a$和$b$的公约数只能判断是公约数,结合后面的证明才得出最大公约数的结论,并且,上面的论述没有仔细考虑$a$,$b$为负值的情况,裴蜀定理 - 维基百科的证明也许适用于负数
(5) 设$a$与$b$的任意公约数为$d$,则$a$与$b$可表示为$a=kd$,$b=ld$,那么$d_0=ax_0+by_0=kdx_0+ldy_0=(kx_0+ly_0)d$,可知$d|d_0$,也就是说$d_0$是任何$a$和$b$的公约数$d$的倍数,最大公约数也不例外,且由上一点可知$d_0$是$a$和$b$的公约数,由此可推断$d_0$本身就是$a$与$b$的最大公约数
(6) 关于解的数量无限的讨论,详见裴蜀定理 - 维基百科
(7) 总之,从上面的讨论可以得出如下结论:在集合$A = {ax+by|(x,y)\in\mathbb{Z}^2}$当中,最小正元素$d_0=gcd(a,b)$,且$A$中任意正元素都是$d_0$的倍数
(8) 因此如果$ax+by=m$有整数解,显然$m \in A$,根据上面的讨论可知 $gcd(a,b)|m$,即$m$是$gcd(a,b)$的整数倍
相比欧几里得算法,扩展欧几里得算法在计算余数($q_i$)和商($r_i$)的基础上又增加了两列数据:$s_i$和$t_i$,最终得到的$s_i$和$t_i$满足裴蜀等式:
$$as_i+bt_i=r_i=gcd(a,b)$$
具体算法步骤如下:
序号 | 余数$r_i$ | $s_i$ | $t_i$ |
---|---|---|---|
$0$ | $a$ | $1$ | $0$ |
$1$ | $b$ | $0$ | $1$ |
$\vdots$ | $\vdots$ | $\vdots$ | $\vdots$ |
$i+1$ | $r{i+1}=r{i-1}-q_ir_i$ | $s{i+1}=s{i-1}-q_is_i$ | $t{i+1}=t{i-1}-q_it_i$ |
终止条件与欧几里得算法相同,当$r_{i+1}=0$时终止,此时:
$$as_i+bt_i=r_i=gcd(a,b)$$
上一篇文章证明了欧几里得算法的正确性,在此基础上说明扩展欧几里得算法的正确性。
当$i=0$时,$r_0=a,s_0=1,t_i=0$,显然满足$as_i+bt_i=r_i$
当$i=1$时,$r_1=b,s_0=0,t_i=1$,显然满足$as_i+bt_i=r_i$
由下列递推式可推及所有$i>1$时的情况:
$$\begin{equation}\begin{split} r_{i+1}&=r_{i-1}-q_ir_i\\\\ &=(as_{i-1}+bt_{i-1})-q_i(as_i+bt_i)\\\\ &=a(s_{i-1}-q_is_i)-b(t_{i-1}-q_it_i)\\\\ &=as_{i+1}+bt_{i+1} \end{split}\end{equation}$$
因此当$r_{i+1}=0$,算法终止时:
$$as_i+bt_i=r_i=gcd(a,b)$$
扩展欧几里得算法 - 维基百科
辗转相除法 - 维基百科
裴蜀定理 - 维基百科
丢番图方程 - 维基百科
二元关系 - 维基百科
偏序关系 - 维基百科
全序关系 - 维基百科
良序关系 - 维基百科
辗转相除法,又称欧几里得算法,用于系统性地求两个自然数的最大公约数。
对于任意两个正整数 $a$ 和 $b$ ,求 $a$ 和 $b$ 的最大公约数 $gcd(a, b)$,等价于求解 $gcd(b, a \% b)$,相关资料一般会限定 $a \geq b$,但其实即使 $a < b$ 也无妨,在这种情况下,第一步计算相当于交换 $a$ 和 $b$ 的位置。
下文的论证会用到三种相关的数学方法,分别是数学归纳法、递归和无穷递降。
数学归纳法经常用来证明某个定理对所有自然数成立:首先证明定理对一个特定的数n0成立(通常是1);然后证明如果定理对自然数n成立的话,那么它对自然数n + 1成立。这样,便可证明定理对所有大于n0的自然数也成立。
递归是将相关的数组成一个数列$(a1, a2, a3…)$,当中除初始项外,其中每一项都用前一项或前几项表示。如斐波那契数列就是递归的,每一项$Fn$都等于$F{n−1} + F_{n−2}(n\geq2)$。辗转相除法中的一些等式也是递归的。
最后,无穷递降是用方程的一个自然数解导出比它小的自然数解。但是,这种转化不能永远进行下去,因为只有有限个小于原来的自然数解的自然数。所以,要么方程无解,不然在有限步内必然能得出最小的自然数解。在下文会用到此法来证明辗转相除法一定会在有限步内结束。
下面以非负整数为例说明如何算法步骤,每一次计算的输入都是前两次计算的非负余数 $r{k-1}$和$r{k-2}$,记$a=r{-2}$,$b=r{-1}$,按如下步骤递归计算
$a = p0b + r_0$
$b = p_0r_0 + r_1$
$r_0 = p_0r_1 + r_2$
$r_1 = p_0r_2 + r_3$
…
$r{k-2} = p0r{k-1} + r_k$
按照该算法进行计算,$r_k$必然会逐渐减小,并且由于$[0,r_0]$之间数字有限,该算法必然能够在第$N$步终止。
此时$rN$为0,$r{N-1}$即为$gcd(a,b)$
辗转相除法的正确性可以分成两步来证明。在第一步,我们会证明算法的最终结果$r{N-1}$同时整除$a$和$b$。因为它是一个公约数,所以必然小于或者等于最大公约数$g$。在第二步,我们证明$g$能整除$r{N-1}$。所以g一定小于或等于$r{N-1}$。两个不等式只在$r{N-1}=g$时同时成立。具体证明如下:
在算法终止时,余数$rN=0$。则有:$r{N-2} = p0r{N-1}$
说明$r{N-1}$能整除$r{N-2}$,又根据$r{N-3} = p_0r{N-2} + r{N-1}$
可推断$r{N-1}$能整除$r{N-3}$,以此类推,最终可推断:
**$r{N-1}$能整除$a$和$b$,说明 $r{N-1}$是$a$和$b$的一个公约数**,且由于$g$是最大公约数,应有$r{N-1} \leq g$
根据定义,$g$是$a$和$b$的最大公约数,则$g$能整除$a$和$b$,$a$和$b$可表示为$a=mg$,$b=ng$
由$r0=a-p_0b=mg-p_0ng=(m-p_0n)g$,可知$g|r_0$,即$g$能整除$r_0$,
由$r_1=b-p_0r_0$,$g|r_0$,$g|b$,可得$g|r_1$,
依此类推,最终可得$g|r{N-1}$,则$g \leq r_{N-1}$
综合(1)(2),可知$g=r_{N-1}$
最近读到了阮一峰介绍的的RSA算法原理,本文作为对这篇文章的简单批注。
对称加密算法的加密与解密过程采用相同的密钥,在这种情况下,往往需要传输密钥,一旦密钥被拦截,就会暴露所传递的信息。
非对称加密算法包含一对密钥,称为公钥和私钥,私钥由密钥所有者保存,不必公开,公钥可以公开。非对称加密算法既可以用于加密也可用于签名。
当用于加密时,消息发送者使用接收者的公钥对消息进行加密,然后将密文传输给接收者,接收者使用对应的私钥对密文进行解密即可得到原始消息。由于经公钥加密的密文只能由对应的私钥进行解密,只要不暴露私钥,就可以安全地传输消息。而非对称加密算法都采用了十分安全的算法保证攻击者难以通过公钥推算私钥,因此可以安全地公开公钥。
当用于签名时,私钥持有者使用自己的私钥对消息的摘要进行签名,然后将消息和摘要的签名一并发送给接收者,接收者可以利用对应的公钥对签名进行解密,并将解密结果与消息摘要进行比对,以确定消息是否被篡改。
如果两个正整数,除了1以外,没有其他公因子,我们就称这两个数是互质关系(coprime)。比如,15和32没有公因子,所以它们是互质关系。这说明,不是质数也可以构成互质关系。
关于互质关系,不难得到以下结论:
1. 任意两个质数构成互质关系,比如13和61。
2. 一个数是质数,另一个数只要不是前者的倍数,两者就构成互质关系,比如3和10。
3. 如果两个数之中,较大的那个数是质数,则两者构成互质关系,比如97和57。
4. 1和任意一个自然数是都是互质关系,比如1和99。
5. p是大于1的整数,则p和p-1构成互质关系,比如57和56。
6. p是大于1的奇数,则p和p-2构成互质关系,比如17和15。
上述结论中1-4均显而易见,下面对第5点进行证明:
假设p和p-1存在公因子k,k为正整数,则:
(1) p = mk,m为正整数
(2) p-1 = nk,n为正整数
由(1)(2)可得:(3) (m-n)k = 1
由于m,n均为正整数,则m-n也为整数,记为a=m-n
则 ak=1,由于a和k均为整数,且k>0,可得a=k=1
上述结论说明,p与p-1的公因子k只能为1,即p与p-1互质
下面对第6点进行证明:
假设p和p-1存在公因子k,k为正整数,则:
(1) p = mk,m为正整数
(2) p-2 = nk,n为正整数
由(1)(2)可得:(3) (m-n)k = 2
由于m,n均为正整数,则m-n也为整数,且k>0
则可得两组解:
解(1): m-n=1,k=2
解(2): m-n=2,k=1
由于p为奇数,则因子k不能为偶数,第一组解不成立
故只能使用第二组解,可得p和p-2的公因子k为1,p和p-2互质
需要学的东西还很多,有一些阮一峰未介绍的转换细节,参考RSA算法的数学原理与证明吧,写不动了,太菜了,总是RSA算法很NB,需要用到大量数论知识
对于Ubuntu 20.10已经可以开箱即用,不过对于Debian10,Arch等系统还是需要专门进行配置才行。
此方案在此时最新的Arch上可行(我的是1050Ti),如果不想折腾直接安装nouveau软件包即可。
但是在我的Debian10上并不能直接使用,在此系统上如果没有在内核启动参数添加nouveau.modeset=0
的话会直接卡到无法操作,所以PASS。
步骤如下。
这一步根据系统不同安装不同的软件包即可,一般情况下,安装nvidia软件包的时候会自动blasklist nouveau,如果是上古版本可能需要手动blasklist。
可以在xorg.conf中进行具体配置,也可以在xorg.conf.d中添加一个nvidia配置用于自动配置,第二种方式不需要xorg.conf文件。
为Nvidia显卡配置驱动nvidia,为Intel核显配置驱动modesetting,具体工作原理参见debian和nvidia文档。
1 |
|
其实有一个官方工具nvidia-xconfig
可以自动生成配置,但是它生成的配置只会在外接显示器上显示,所以需要自己配置双屏,不过可以借助此工具生成一个配置框架再修改。
/etc/X11/xorg.conf.d/10-nvidia-drm-outputclass.conf
指导系统进行自动配置这个方法来自Arch的文档,和方法一的核心配置大致相同,只是方法一是直接指定了Device,Screen,ServerLayout等,这里只是添加OutputClass,指导X Server自动配置。
添加文件/etc/X11/xorg.conf.d/10-nvidia-drm-outputclass.conf
,内容如下:
1 | Section "OutputClass" |
xrandr
配置双屏配置完上面的之后可能需要重启X服务,最简单的方法大概就是直接重启机器了。
配置内容如下:
1 | xrandr --setprovideroutputsource modesetting NVIDIA-0 |
其中最关键的是第一条命令。可以把这几条命令写入 ~/.xinitrc
或者 ~/xsession
,根据自己的具体情况吧,也可能需要配置你的Display Manager(如果有的话)。
核心步骤已经说的差不多了,到这里为止应该已经可以在xrandr中看到内置显示器和外置显示器都处于connected状态了,已经可以根据自己的需要进行输出了。
顺便附上自己的 .xinitrc
,这来自一台没有使用Display Manager的Arch系统,在 .xinitrc
的最后启动了i3桌面管理器。
1 |
|
Debian Wiki - NVIDIA Optimus
Nvidia Forums - Prime and Prime synchronization
Arch Wiki - NVIDIA Optimus