Hasee Z7 sl7d3 archlinux 安装笔记

本来以为很简单的,没想到踩了这么多坑。花了好几天时间才装好。
如果有外置显示器的需求的话,建议不要碰 gnome3 和 wayland
因为这台笔记本的 hdmi 口和 mini dp 口直连独显,如果需要双显卡工作的话必须使用 intel-virtual-output , intel-virtual-output 这个东西貌似不支持 wayland, 总是检测不到 virtual display (wayland 从只差一步到放弃 = =)。
基于 Xorg 的 gnome3 对 intel-virtual-output 支持完好, 不过不知道什么原因, 在连接外置显示器的情况下, 合上盖子或者是只用外置显示器会导致蜜汁卡吨,花大量时间搜寻后无解(起初以为是acpi的问题,不过内核加参数 acpi=off 会导致笔记本开不了机)。经测试,Xfce4 正常, 所以之后选择了 Xfce4。

为了避免以后踩坑,下面记录下步骤。

1 制作 Arch 启动u盘
2 在BIOS下把显卡模式切换为 DISCRETE (BIOS 对于显卡有两个模式,分别是MSHYBRID 和 DISCRETE, MSHYBRID是双显卡切换, DISCRETE是只用独显,MSHYBRID模式不能够启动 Arch U盘 )
3 启动 Arch 安装盘
4 fdisk 分区
5 挂载

1
2
3
4
5
6
7
# /dev/sdb2 是 / , /dev/sdb1 是boot, /dev/sdb3 是 /home
# 具体按照自己的分区来
mount /dev/sdb2 /mnt
mkdir /mnt/boot
mkdir /mnt/home
mount /dev/sdb1 /mnt/boot
mount /dev/sdb3 /mnt/home

6 更新mirrors

1
2
3
4
5
cd /etc/pacman.d
sed -i "s/^\b/#/g" mirrorlist
nano mirrorlist
#将mirrors.ustc.edu.cn和mirrors6.ustc.edu.cn前面的#去掉
pacman -Syy

7 安装base系统

1
2
pacstrap /mnt base base-devel
#如果你想使用ifconfig之类的工具,请在上面加上net-tools

8 生成fstab

1
genfstab -U -p /mnt >>/mnt/etc/fstab

9 chroot

1
arch-chroot /mnt /bin/bash

10 配置locale

1
2
3
4
5
cd /etc
nano locale.gen
#将en_US.UTF-8,zh_CN.GBK,zh_CN.GB2312,zh_CN.GB18030,zh_CN.UTF-8前的#去掉
locale-gen
echo LANG=zh_CN.UTF-8 >> locale.conf

11 设置时区

1
ln -s /usr/share/zoneinfo/Asia/Shanghai /etc/localtime

12 设置主机名

1
echo 主机名 >> /etc/hostname

13 用户配置

1
2
3
4
passwd
#修改root密码
useradd -m -g users -G wheel -s /bin/bash 用户名
passwd 用户名

14 安装GRUB

1
2
3
# 具体硬盘要根据自己的来
pacman -S grub-bios
grub-install /dev/sdb

开机需要内核开启一些参数(如果不加这些参数之后的bbswitch工作会不正常, 见这里

1
2
3
4
5
6
7
8
9
vim /etc/default/grub
# 配置 GRUB_CMDLINE_LINUX_DEFAULT
GRUB_CMDLINE_LINUX_DEFAULT="quiet acpi_osi=! acpi_osi=\"Windows 2009\""
# 之后生成grub.cfg
grub-mkconfig -o /boot/grub/grub.cfg
# 开启dhcpcd
systemctl enable dhcpcd.service

15 退出chroot, 重启
16 安装synaptics驱动

1
pacman -S xf86-input-synaptics

17 安装 xorg

1
pacman -S xorg-server xorg-xinit xorg-utils xorg-server-utils

18 安装xfce4

1
2
3
4
pacman -S xfce4 xfce4-goodies
# 如果需要从命令行启动xfce的话只需要进行如下步骤
cp /etc/X11/xinit/xinitrc ~/.xinitrc
# 在.xinitrc后加上 exec startxfce4 即可

19 配置双显卡

1
2
3
4
5
6
pacman -S bumblebee
pacman -S mesa
pacman -S xf86-video-intel
pacman -S nvidia
gpasswd -a 用户名 bumblebee
systemctl enable bumblebeed.service

在 /etc/X11/xorg.conf.d下创建一个文件 20-intel.conf, 内容如下

1
2
3
4
5
Section "Device"
Identifier "Intel Graphics"
Driver "intel"
BusID "PCI:00:02:0" # <<<< replace with correct address
EndSection

配置 /etc/bumblebee/xorg.conf.nvidia
内容如下

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
Section "ServerLayout"
Identifier "Layout0"
Option "AutoAddDevices" "true"
Option "AutoAddGPU" "false"
EndSection
Section "Device"
Identifier "DiscreteNvidia"
Driver "nvidia"
VendorName "NVIDIA Corporation"
# If the X server does not automatically detect your VGA device,
# you can manually set it here.
# To get the BusID prop, run `lspci | egrep 'VGA|3D'` and input the data
# as you see in the commented example.
# This Setting may be needed in some platforms with more than one
# nvidia card, which may confuse the proprietary driver (e.g.,
# trying to take ownership of the wrong device). Also needed on Ubuntu 13.04.
BusID "PCI:01:00:0"
# Setting ProbeAllGpus to false prevents the new proprietary driver
# instance spawned to try to control the integrated graphics card,
# which is already being managed outside bumblebee.
# This option doesn't hurt and it is required on platforms running
# more than one nvidia graphics card with the proprietary driver.
# (E.g. Macbook Pro pre-2010 with nVidia 9400M + 9600M GT).
# If this option is not set, the new Xorg may blacken the screen and
# render it unusable (unless you have some way to run killall Xorg).
Option "ProbeAllGpus" "false"
Option "AllowEmptyInitialConfiguration"
Option "NoLogo" "true"
Option "UseEDID" "true"
# Option "UseDisplayDevice" "none"
EndSection

具体有哪些地方要改呢?
对于一般用户,去掉BusID的注释即可。
如果有外界屏幕需求的话, 需要把 UseEDIDAutoAddDevices 设置为 true, 然后加上 Option "AllowEmptyInitialConfiguration"
(这三个操作必不可少,缺了AllowEmptyInitialConfiguration会导致 独显初始化失败)
更改之后不能进入X环境(因为系统会使用I卡,但是目前只用独显状态I卡识别不了,导致no screen found)
20 安装 lightdm(如果命令行启动可以不做)

1
2
3
pacman -S lightdm
pacman -S lightdm-gtk-greeter
systemctl enable lightdm.service

21 重启,在BIOS中把显卡选项切换为 MSHYBRID(不切换进不了系统)
22 输入帐号密码之后可以登录
23 安装 libxss(intel-virtual-output 依赖这个)

1
pacman -S libxss

24 激活外接屏幕

1
2
3
4
# 先激活下 独显, 如果没有此操作,外屏不能被激活 = = ,如果失败的话可能因为显卡未初始化完全,等几秒再尝试下
optirun glxspheres64
# 显示成功之后执行下面的命令,外接屏被正确识别
intel-virtual-output

25 安装常用软件(从AUI里面拷贝出来了一些常用的软件,具体根据自己需要装)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pacman -S bc rsync mlocate bash-completion pkgstats arch-wiki-lite
pacman -S zip unzip unrar p7zip lzop cpio
pacman -S avahi nss-mdns
pacman -S alsa-utils alsa-plugins
pacman -S pulseaudio pulseaudio-alsa
pacman -S ntfs-3g dosfstools exfat-utils f2fs-tools fuse fuse-exfat autofs mtpfs
pacman -S --asdeps --needed cairo fontconfig freetype2
pacman -S networkmanager dnsmasq network-manager-applet nm-connection-editor
pacman -S networkmanager-openconnect networkmanager-openvpn networkmanager-pptp networkmanager-vpnc
system_ctl enable NetworkManager.service
# 时间校准
pacman -S ntpd
systemctl enable ntpd

其它输入法之类的就不说了。
最好加一下archlinuxcn的源,里面可以直接安装sublime, vscode, wps之类的软件,非常方便。

参考:

https://my.oschina.net/codeaxe/blog/127533
https://bbs.archlinux.org/viewtopic.php?id=169742
https://github.com/Bumblebee-Project/Bumblebee/issues/764#issuecomment-234494238
https://wiki.archlinux.org/index.php/Clevo_P650RS
https://wiki.archlinux.org/index.php/Kernel_parameters
https://wiki.archlinux.org/index.php/Bumblebee

git repo 永久删除大文件

有时候我们可能会因为.gitignore等原因误添加了一些文件
删除之后会因为之前commit过导致占用git仓库的空间, 这样非常浪费,尤其是对于那些自建git且有不好的commit习惯的用户

那怎么删除呢?
可以借助git的filter-branch(小心使用,因为操作不当可能导致重要记录丢失)

  1. 首先查看哪些文件最占用git仓库
    1
    git rev-list --objects --all | grep "$(git verify-pack -v .git/objects/pack/*.idx | sort -k 3 -n | tail -5 | awk '{print$1}')"

  rev-list命令用来列出Git仓库中的提交,我们用它来列出所有提交中涉及的文件名及其ID。 该命令可以指定只显示某个引用(或分支)的上下游的提交。
  –objects:列出该提交涉及的所有文件ID。
  –all:所有分支的提交,相当于指定了位于/refs下的所有引用。
  verify-pack命令用于显示已打包的内容。

  1. 删除历史提交文件
    1
    git filter-branch --force --index-filter 'git rm -rf --cached --ignore-unmatch filename' --prune-empty --tag-name-filter cat -- --all

把filename改成你想删除的即可

  1. 推送repo
    1
    git push origin master --force

此时remote repo已经被清理了,但是本地的repo多余的文件还没被清理

  1. 清理本地repo
    1
    2
    3
    rm -rf .git/refs/original/
    git reflog expire --expire=now --all
    git gc --prune=now

参考: 记一次删除Git记录中的大文件的过程

Linux 的 ACL

今天在配置 squid 的basic auth的时候遇到了一个问题
我把htpasswd生成的密码文件放在了/root/config目录下,然后在认证的时候老是遇到http 407 deny

查看/var/log/squid/cache.log 的时候发现不能够 stat 那个密码文件。

之后把那个密码文件 chmod 成777,发现还是不行
最后把那个密码文件放在了 /etc/squid 目录下,终于可以了

搜索了一下,发现linux除了一般的文件权限控制还有acl
查看了一下fstab文件,确实已经开启了

1
/dev/vda1 / ext3 noatime,acl,user_xattr 1 1

我去查看了一下那个密码文件的acl,发现是rwx,好像正常的

1
2
3
4
5
6
7
debian:~/config# getfacl pass2
# file: pass2
# owner: root
# group: root
user::rwx
group::rwx
other::rwx

后来才发现,原来是 /root 目录对于非root用户设置成了不可读不可写

1
2
3
4
5
6
7
8
debian:~# getfacl /root
getfacl: Removing leading '/' from absolute path names
# file: root
# owner: root
# group: root
user::rwx
group::---
other::---

运行 squid的用户是 proxy,由于设置了 acl,proxy用户访问不了/root内的文件(就算/root/config文件夹和 /root/config/pass 权限设置成777也没用!),所以就导致了squid没有权限去认证了。

ACL使得Linux可以进行更加复杂的权限控制,详细可以参见Linux ACL 体验

Opengrok 优秀的代码阅读工具

linux下面阅读代码的工具比较少,一般都是IDE,vim + ctags 也不错,不过用着总有些不顺手。
搜了一下,发现还有woboq, LXR(Linux Cross Reference), opengrok 这类网页端的,LXR搭建起来比较麻烦,所以尝试了一下opengrok,感觉不错,这里记录一下搭建的流程(凭着记忆来的,可能会稍微有点问题)。

安装JAVA

首先安装java,先去官网下载jdk,一般解压打/usr/java 就可以了, 然后添加相应的环境变量(自己修改相应的path).

1
2
3
export JAVA_HOME=/usr/java/jdk1.8.0_73
export CLASSPATH=.:%JAVA_HOME%/lib/dt.jar:%JAVA_HOME%/lib/tools.jar
export PATH=$PATH:$JAVA_HOME/bin

安装tomcat

之后去tomcat官网下载tomcat, 并解压。我解压到了/usr/local里面,启动和关闭tomcat很简单,path/to/tomcat/bin下有启动和关闭的脚本(根据名字分辨,一个叫startup.sh, 另一个是 shutdown.sh)。

访问ip:8080,如果出现tomcat的界面就是安装成功了。

安装opengrok

去opengrok观望下载它的安装包,同样只是一个压缩包,随便解压到一个地方就可以了。比如/usr/opengrok
压缩包里面有个source.war,在/usr/opengrok/lib下面,直接扔到path/to/tomcat/webapps目录下就可以了,之后访问ip:8080/source 就是opengrok的默认页面。

那怎么对代码进行索引等操作呢?
opengrok默认源代码处理的位置是在/var/opengrok/src里面(当然也可以改变位置,具体详见参数),创建/var/opengrok/src一系列的目录,把你的源代码解压到里面,安装好ctags,然后在path/to/opengrok/bin目录运行./Opengrok index,等一段时间,数据就处理好了。
之后添加一个环境变量,OPENGROK_TOMCAT_BASE,变量内容是你的tomcat文件夹的位置,然后运行./Opengrok deploy, 重启tomcat,再次访问ip:8080/source 就可以看到已经处理好的代码了。

默认opengrok的背景是白色的,感觉比较伤眼睛,对此可以在浏览器安装一个夜间模式的插件,也可以到/usr/local/tomcat/webapps/source/default目录下修改style.css这个文件,把body->background-color的值改为浅绿色即可,#B9DABD

如果是重要代码,可以按照这里给opengrok加上LDAP验证,也可以让tomcat监听127.0.0.1,然后用nginx反代,在nginx上加上basic-auth即可。

交叉编译树莓派的 aria2c

aria2c 1.19.x 下载https链接始终有问题,所以最近想升级一下

惯例,下源码,编译,不过报错了。expected type-specifier before string constant

看到这里说是g++版本太低。自己懒,不太想折腾树莓派环境了,所以准备交叉编译一个。树莓派官方提供了交叉编译工具,所以很方便。

首先,随便创建一个文件夹,比如rpi

1
2
3
cd ~
mkdir rpi
cd rpi

之后,git clone交叉编译工具

1
git clone git://github.com/raspberrypi/tools.git

clone 之后可以通过pull更新tools

1
2
cd ~/rpi/tools
git pull origin

添加环境变量, 可以加在 .bashrc 中

1
2
3
4
# for 32 bit
export PATH=$PATH:$HOME/rpi/tools/arm-bcm2708/gcc-linaro-arm-linux-gnueabihf-raspbian/bin
# for 64 bit
export PATH=$PATH:$HOME/rpi/tools/arm-bcm2708/gcc-linaro-arm-linux-gnueabihf-raspbian-x64/bin

更新环境变量

1
source ~/.bashrc

这样交叉编译环境就配置完成了。之后编译aria2c

首先下载最新的release源码,这里下载的是1.24版本的,然后解压

1
2
3
wget https://github.com/aria2/aria2/releases/download/release-1.24.0/aria2-1.24.0.tar.gz
tar zxvf aria2-1.24.0.tar.gz
cd aria2-1.24.0

之后进行configure,貌似libxml2有点问题(见这里),所以要加上--without-libxml2选项用Expat代替libxml2, host指编译之后是要运行在树莓派上的。

1
./configure --host=arm-linux-gnueabihf --without-libxml2

如果想要静态链接,则执行

1
./configure --host=arm-linux-gnueabihf --without-libxml2 ARIA2_STATIC=yes

configure之后make就可以了, 编译好的文件放在src/aria2c里。有点大(64mb),下载后放到树莓派中可以strip一下去除符号表以减少程序体积(缩小到了2.4mb)。

1
strip -s aria2c

之后就可以愉快地运行了。

aria2c的配置可以见这里

PS

发现树莓派的交叉编译链没有openssl,所以还需要自己找源码编译
具体过程可以见这个脚本

给 gogs 加上 let's entrypt 证书

想给自己的gogs加个证书,现在 let’s entrypt 这么流行,而且免费,所以就用它了!

搜了一下发现可以又很多工具可以很方便地生成证书,搜了一下,选中了acme.sh

步骤如下:

首先,git clone 获取脚本

1
git clone https://github.com/Neilpang/acme.sh.git

let‘s entrypt 需要验证域名所有权,有好几种方式,有验证文件的,也有验证dns的。
gogs验证文件不太方便,所以使用验证dns的方法

1
acme.sh --issue --dns -d yourdomain.com

之后它会提示让你设置一下子域名的txt记录,设置好之后,运行下面的命令

1
acme.sh --renew -d yourdomain.com

之后它就会自动生成证书,告诉你放在什么地方。

那么nginx怎么使用呢?
首先,生成ssl_certificate

1
2
# 文件路径已省略,需要自己补上
cat yourdomain.com.key fullchain.cer > fullchain.pem

之后在nginx的配置文件中加上以上配置就可以了:

1
2
ssl_certificate path/to/fullcain.pem;
ssl_certificate_key path/to/yourdomain.com.key;

nginx虚拟主机配置文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
upstream gogs {
server 127.0.0.1:3000 weight=1;
}
server {
listen 443;
server_name yourdomain.com;
ssl on;
ssl_certificate path/to/fullcain.pem;
ssl_certificate_key path/to/yourdomain.com.key;
location / {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $http_host;
proxy_pass http://gogs;
}
}

如果需要强制https可以见这里
当然也可以启用HSTS, 见这里这里

注意

gogs 本身有配置证书的地方,但是如果要通过 nginx 访问 gogs的话,证书是需要在nginx配置的,gogs不需要配置。

ngrok 源码解析

ngrok是一个内网穿透工具,主要用途是让用户能够通过一台ngrok的中转服务器访问在内网中的一台机器。

用途有点类似与端口映射,要把一台内网中的机器的端口映射到具有公网ip的另一台机器的端口。

假如被映射的机器不在内网,那么可以直接通过映射的机器向被映射的机器创建链接转发来达到目的,但是如果被映射的机器在一个内网的话,就要复杂很多了。

首先需要内网的机器和外网的机器维护一条链接(因为外网的机器不能够主动连接内网的机器),为了避免链接的中断,需要通过心跳等途径维持链接。这条链接保证了外网机器能够随时和内网通信。

当然,单单一条链接是不够的,一般我们访问一个网页都会打开5-10个tcp链接,如果这些链接都阻塞在同一条链接上的话,性能会受很大的影响。
为了保证性能,外网机器和内网的机器可以制定一种协议,外网的机器可以通过协议请求内网的机器创建多条联通外网机器的链接以供传输。这样就可以保证请求可以被并行地响应,以保证性能。ngrok就是这么做的。

下面来看一下ngrok的执行流程(主要讲一下tcp的映射,省略了http,https和认证过程)。

ngrok主要分为ngrok和ngrokd,ngrok是客户端,ngrokd是服务端。

服务端

ngrokd启动的时候会执行一个tunnelListener函数,用来监听客户端的链接。

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
//server main.go
tunnelListener(opts.tunnelAddr, tlsConfig)
func tunnelListener(addr string, tlsConfig *tls.Config) {
// listen for incoming connections
listener, err := conn.Listen(addr, "tun", tlsConfig)
//...
log.Info("Listening for control and proxy connections on %s", listener.Addr.String())
for c := range listener.Conns {
go func(tunnelConn conn.Conn) {
var rawMsg msg.Message
if rawMsg, err = msg.ReadMsg(tunnelConn); err != nil {
tunnelConn.Warn("Failed to read message: %v", err)
tunnelConn.Close()
return
}
switch m := rawMsg.(type) {
case *msg.Auth:
NewControl(tunnelConn, m)
case *msg.RegProxy:
NewProxy(tunnelConn, m)
default:
tunnelConn.Close()
}
}(c)
}
}

中间有部分不需要的东西被删了。tunnelListener用来监听客户端。当有客户端连接的时候,它会先接收一个rawMsg(既定的协议),如果rawMsg是Auth类型的,那么就代表有新的客户端要连接了,那么就通过NewControl创建了一Control。

这个Control是一条进行控制的链接,是需要一直维护的。所有的控制信息都是通过这条链接来传递的。Control结构如下:

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
type Control struct {
// auth message
auth *msg.Auth
// actual connection
conn conn.Conn
// put a message in this channel to send it over
// conn to the client
out chan (msg.Message)
// read from this channel to get the next message sent
// to us over conn by the client
in chan (msg.Message)
// the last time we received a ping from the client - for heartbeats
lastPing time.Time
// all of the tunnels this control connection handles
tunnels []*Tunnel
// proxy connections
proxies chan conn.Conn
// identifier
id string
// synchronizer for controlled shutdown of writer()
writerShutdown *util.Shutdown
// synchronizer for controlled shutdown of reader()
readerShutdown *util.Shutdown
// synchronizer for controlled shutdown of manager()
managerShutdown *util.Shutdown
// synchronizer for controller shutdown of entire Control
shutdown *util.Shutdown
}

其中,auth是认证信息,ctlConn指的就是控制链接本身。out和in控制着数据的读入和读出,所有加入out的msg都会被发送到对应的客户端,所有ctlConn接收到的msg都会被放入in。分别由reader和writer这两个goroutine实现。

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
func (c *Control) writer() {
// write messages to the control channel
for m := range c.out {
c.conn.SetWriteDeadline(time.Now().Add(controlWriteTimeout))
if err := msg.WriteMsg(c.conn, m); err != nil {
panic(err)
}
}
}
func (c *Control) reader() {
// read messages from the control channel
for {
if msg, err := msg.ReadMsg(c.conn); err != nil {
if err == io.EOF {
c.conn.Info("EOF")
return
} else {
panic(err)
}
} else {
// this can also panic during shutdown
c.in <- msg
}
}
}

还有两个重要的元素是proxies和tunnel。tunnel存放的是外网机器监听外部连接的链接,proxies存放的是外网机器访问内网的链接。

Control建立之后会随机生成一个id,这个id代表着对应的client的id。生成id之后Control会把这个id相关的信息发送给客户端,之后这个id就代表着客户端了。

1
2
3
4
5
c.out <- &msg.AuthResp{
Version: version.Proto,
MmVersion: version.MajorMinor(),
ClientId: c.id,
}

客户端接受到这个id信息之后,会在以后的消息中带上这个id,方便服务端确认是哪个客户端。
为了方便服务端通过id找到对应的Control,服务端会把id和对应Control放在一个map里面,这个map就是controlRegistry。

1
controlRegistry.Add(c.id, c);

之后,Control会监听从客户端发来的请求(这个时候服务端还没建立端口映射,需要客户端发相应的请求:我要吧自己的什么端口映射到服务端的什么端口上)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func (c *Control) manager() {
for {
select {
case mRaw, ok := <-c.in:
// c.in closes to indicate shutdown
if !ok {
return
}
switch m := mRaw.(type) {
case *msg.ReqTunnel:
c.registerTunnel(m)
case *msg.Ping:
c.lastPing = time.Now()
c.out <- &msg.Pong{}
}
}
}
}

其中,msg.Ping是心跳信息,msg.ReqTunnel是客户端请求映射的信息。服务端接收到客户端请求,会创建一个新的tunnel

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
// Register a new tunnel on this control connection
func (c *Control) registerTunnel(rawTunnelReq *msg.ReqTunnel) {
for _, proto := range strings.Split(rawTunnelReq.Protocol, "+") {
tunnelReq := *rawTunnelReq
tunnelReq.Protocol = proto
c.conn.Debug("Registering new tunnel")
t, err := NewTunnel(&tunnelReq, c)
// add it to the list of tunnels
c.tunnels = append(c.tunnels, t)
// acknowledge success
c.out <- &msg.NewTunnel{
Url: t.url,
Protocol: proto,
ReqId: rawTunnelReq.ReqId,
}
rawTunnelReq.Hostname = strings.Replace(t.url, proto+"://", "", 1)
}
}
//创建tunnel
func NewTunnel(m *msg.ReqTunnel, ctl *Control) (t *Tunnel, err error) {
t = &Tunnel{
req: m,
start: time.Now(),
ctl: ctl,
Logger: log.NewPrefixLogger(),
}
proto := t.req.Protocol
switch proto {
case "tcp":
bindTcp := func(port int) error {
if t.listener, err = net.ListenTCP("tcp", &net.TCPAddr{IP: net.ParseIP("0.0.0.0"), Port: port}); err != nil {
err = t.ctl.conn.Error("Error binding TCP listener: %v", err)
return err
}
// create the url
addr := t.listener.Addr().(*net.TCPAddr)
t.url = fmt.Sprintf("tcp://%s:%d", opts.domain, addr.Port)
// register it
if err = tunnelRegistry.RegisterAndCache(t.url, t); err != nil {
// This should never be possible because the OS will
// only assign available ports to us.
t.listener.Close()
err = fmt.Errorf("TCP listener bound, but failed to register %s", t.url)
return err
}
go t.listenTcp(t.listener)
return nil
}
// Listens for new public tcp connections from the internet.
func (t *Tunnel) listenTcp(listener *net.TCPListener) {
// accept public connections
tcpConn, err := listener.AcceptTCP()
conn := conn.Wrap(tcpConn, "pub")
conn.AddLogPrefix(t.Id())
conn.Info("New connection from %v", conn.RemoteAddr())
go t.HandlePublicConnection(conn)
}
}

Control建立一个tunnel之后会发送一个msg.NewTunnel信息给客户端,代表tunnel已经建立。这个tunnel会被放在Control的tunnels结构中去。新建Tunnel的过程中,会把url和tunnel的信息注册到一个map中,方便通过url获取到对应的tunnle,这个map是tunnelRegistry。

tunnel建立之后,基本的工作都已经做完了,只需要在tunnel里面监听外网的链接就行了。下面来看一下对外网链接的处理。

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
func (t *Tunnel) HandlePublicConnection(publicConn conn.Conn) {
var proxyConn conn.Conn
var err error
for i := 0; i < (2 * proxyMaxPoolSize); i++ {
// get a proxy connection
if proxyConn, err = t.ctl.GetProxy(); err != nil {
t.Warn("Failed to get proxy connection: %v", err)
return
}
// tell the client we're going to start using this proxy connection
startPxyMsg := &msg.StartProxy{
Url: t.url,
ClientAddr: publicConn.RemoteAddr().String(),
}
if err = msg.WriteMsg(proxyConn, startPxyMsg); err != nil {
proxyConn.Warn("Failed to write StartProxyMessage: %v, attempt %d", err, i)
proxyConn.Close()
} else {
// success
break
}
}
// join the public and proxy connections
bytesIn, bytesOut := conn.Join(publicConn, proxyConn)
}

因为现在服务端和客户端只有一条链接(Control),这条链接主要是传送相关的控制消息,为了传输数据,请求客户端创建proxy链接。这个操作在 t.ctl.GetProxy()中完成。

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
func (c *Control) GetProxy() (proxyConn conn.Conn, err error) {
var ok bool
// get a proxy connection from the pool
select {
case proxyConn, ok = <-c.proxies:
if !ok {
err = fmt.Errorf("No proxy connections available, control is closing")
return
}
default:
// no proxy available in the pool, ask for one over the control channel
c.conn.Debug("No proxy in pool, requesting proxy from control . . .")
if err = util.PanicToError(func() { c.out <- &msg.ReqProxy{} }); err != nil {
return
}
select {
case proxyConn, ok = <-c.proxies:
if !ok {
err = fmt.Errorf("No proxy connections available, control is closing")
return
}
case <-time.After(pingTimeoutInterval):
err = fmt.Errorf("Timeout trying to get proxy connection")
return
}
}
return
}

getproxy函数会首先在Control的proxies中找有没有已存在的链接,如果有的话直接拿出来用,如果没有的话请求客户端创建proxy链接,并阻塞在select。客户端会连接服务端,并发送注册proxy的消息msg.RegProxy。这个消息中会带有客户端的id。
服务端通过这个id把这条链接存放在c.proxies中,这样的话GetProxy继续运行,并返回可用的proxy链接。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//接收新的proxy链接消息
switch m := rawMsg.(type) {
case *msg.Auth:
NewControl(tunnelConn, m)
case *msg.RegProxy:
NewProxy(tunnelConn, m)
default:
tunnelConn.Close()
}
//根据id找到control,把链接放入proxies
func NewProxy(pxyConn conn.Conn, regPxy *msg.RegProxy) {
ctl := controlRegistry.Get(regPxy.ClientId)
if ctl == nil {
panic("No client found for identifier: " + regPxy.ClientId)
}
ctl.RegisterProxy(pxyConn)
}

获取proxy链接之后只要把tunnel和proxy链接相连就可以实现数据传输了

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
// join the public and proxy connections
bytesIn, bytesOut := conn.Join(publicConn, proxyConn)
func Join(c Conn, c2 Conn) (int64, int64) {
var wait sync.WaitGroup
pipe := func(to Conn, from Conn, bytesCopied *int64) {
defer to.Close()
defer from.Close()
defer wait.Done()
var err error
*bytesCopied, err = io.Copy(to, from)
if err != nil {
from.Warn("Copied %d bytes to %s before failing with error %v", *bytesCopied, to.Id(), err)
} else {
from.Debug("Copied %d bytes to %s", *bytesCopied, to.Id())
}
}
wait.Add(2)
var fromBytes, toBytes int64
go pipe(c, c2, &fromBytes)
go pipe(c2, c, &toBytes)
c.Info("Joined with connection %s", c2.Id())
wait.Wait()
return fromBytes, toBytes
}

客户端

客户端的部分行为在上文中已经提及了,这里简单讲一下。

读取配置文件之后,客户端会创建一个Controller运行。

1
NewController().Run(config)

Controller有一个web端,主要显示一些连接的信息(只有http和https会显示,tcp协议不会),这个部分不说了,主要说一下和服务端通信的部分。
Controller中会执行model.run运行clientmodel。

1
ctl.Go(ctl.model.Run)

clientmodel会执行control函数

1
2
3
4
5
6
7
8
9
10
func (c *ClientModel) Run() {
// how long we should wait before we reconnect
maxWait := 30 * time.Second
wait := 1 * time.Second
for {
// run the control channel
c.control()
}
}

在control函数中,client会根据配置文件连接到相应的服务端进行认证,成功之后发送要进行映射的tunnel信息。发送成功之后就等着听取服务端的命令就好了。(同时要自己维护心跳)

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
// Establishes and manages a tunnel control connection with the server
func (c *ClientModel) control() {
// establish control channel
var (
ctlConn conn.Conn
err error
)
if c.proxyUrl == "" {
// simple non-proxied case, just connect to the server
ctlConn, err = conn.Dial(c.serverAddr, "ctl", c.tlsConfig)
} else {
ctlConn, err = conn.DialHttpProxy(c.proxyUrl, c.serverAddr, "ctl", c.tlsConfig)
}
// authenticate with the server
auth := &msg.Auth{
ClientId: c.id,
OS: runtime.GOOS,
Arch: runtime.GOARCH,
Version: version.Proto,
MmVersion: version.MajorMinor(),
User: c.authToken,
}
if err = msg.WriteMsg(ctlConn, auth); err != nil {
panic(err)
}
// wait for the server to authenticate us
var authResp msg.AuthResp
if err = msg.ReadMsgInto(ctlConn, &authResp); err != nil {
panic(err)
}
c.id = authResp.ClientId
c.serverVersion = authResp.MmVersion
c.Info("Authenticated with server, client id: %v", c.id)
c.update()
if err = SaveAuthToken(c.configPath, c.authToken); err != nil {
c.Error("Failed to save auth token: %v", err)
}
// request tunnels
reqIdToTunnelConfig := make(map[string]*TunnelConfiguration)
for _, config := range c.tunnelConfig {
// create the protocol list to ask for
var protocols []string
for proto, _ := range config.Protocols {
protocols = append(protocols, proto)
}
reqTunnel := &msg.ReqTunnel{
ReqId: util.RandId(8),
Protocol: strings.Join(protocols, "+"),
Hostname: config.Hostname,
Subdomain: config.Subdomain,
HttpAuth: config.HttpAuth,
RemotePort: config.RemotePort,
}
// send the tunnel request
if err = msg.WriteMsg(ctlConn, reqTunnel); err != nil {
panic(err)
}
// save request id association so we know which local address
// to proxy to later
reqIdToTunnelConfig[reqTunnel.ReqId] = config
}
// start the heartbeat
lastPong := time.Now().UnixNano()
c.ctl.Go(func() { c.heartbeat(&lastPong, ctlConn) })
// main control loop
for {
var rawMsg msg.Message
if rawMsg, err = msg.ReadMsg(ctlConn); err != nil {
panic(err)
}
switch m := rawMsg.(type) {
case *msg.ReqProxy:
c.ctl.Go(c.proxy)
case *msg.Pong:
atomic.StoreInt64(&lastPong, time.Now().UnixNano())
case *msg.NewTunnel:
if m.Error != "" {
emsg := fmt.Sprintf("Server failed to allocate tunnel: %s", m.Error)
c.Error(emsg)
c.ctl.Shutdown(emsg)
continue
}
tunnel := mvc.Tunnel{
PublicUrl: m.Url,
LocalAddr: reqIdToTunnelConfig[m.ReqId].Protocols[m.Protocol],
Protocol: c.protoMap[m.Protocol],
}
c.tunnels[tunnel.PublicUrl] = tunnel
c.connStatus = mvc.ConnOnline
c.Info("Tunnel established at %v", tunnel.PublicUrl)
c.update()
default:
ctlConn.Warn("Ignoring unknown control message %v ", m)
}
}
}

这里最重要的信息是msg.ReqProxy,接受到这个信息之后服务端会主动创建客户端到服务端的proxy链接

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
// Establishes and manages a tunnel proxy connection with the server
func (c *ClientModel) proxy() {
var (
remoteConn conn.Conn
err error
)
if c.proxyUrl == "" {
remoteConn, err = conn.Dial(c.serverAddr, "pxy", c.tlsConfig)
} else {
remoteConn, err = conn.DialHttpProxy(c.proxyUrl, c.serverAddr, "pxy", c.tlsConfig)
}
err = msg.WriteMsg(remoteConn, &msg.RegProxy{ClientId: c.id})
// wait for the server to ack our register
var startPxy msg.StartProxy
if err = msg.ReadMsgInto(remoteConn, &startPxy); err != nil {
remoteConn.Error("Server failed to write StartProxy: %v", err)
return
}
tunnel, ok := c.tunnels[startPxy.Url]
if !ok {
remoteConn.Error("Couldn't find tunnel for proxy: %s", startPxy.Url)
return
}
// start up the private connection
start := time.Now()
localConn, err := conn.Dial(tunnel.LocalAddr, "prv", nil)
m := c.metrics
m.proxySetupTimer.Update(time.Since(start))
m.connMeter.Mark(1)
c.update()
m.connTimer.Time(func() {
localConn := tunnel.Protocol.WrapConn(localConn, mvc.ConnectionContext{Tunnel: tunnel, ClientAddr: startPxy.ClientAddr})
bytesIn, bytesOut := conn.Join(localConn, remoteConn)
m.bytesIn.Update(bytesIn)
m.bytesOut.Update(bytesOut)
m.bytesInCount.Inc(bytesIn)
m.bytesOutCount.Inc(bytesOut)
})
c.update()
}

以上就是ngrok服务端和客户端的主要逻辑了。

写的比较匆忙,如果看不明白可以找相关的文章对比着看看。比如这篇:ngrok原理浅析

如果想要搭建ngrok服务端,可以看这篇:搭建 ngrok 服务实现内网穿透

rinetd端口转发

发现linux下面有一个很方便的端口转发和反向代理的工具rinetd

安装后配置文件默认放在etc的rinetd.conf

1
2
vim /etc/rinetd.conf
0.0.0.0 8080 192.168.1.2 8080

上述配置就是把访问将所有发往本机8080端口的请求转发到192.168.1.2的8080端口
修改好之后重启软件就可以了。

1
2
3
4
5
6
关闭进程
pkill rinetd
启动软件
rinetd -c /etc/rinetd.conf
查看端口监听状态
netstat -antup

sched_param

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <sched.h>
struct sched_param {
int32_t sched_priority;
int32_t sched_curpriority;
union {
int32_t reserved[8];
struct {
int32_t __ss_low_priority;
int32_t __ss_max_repl;
struct timespec __ss_repl_period;
struct timespec __ss_init_budget;
} __ss;
} __ss_un;
}
#define sched_ss_low_priority __ss_un.__ss.__ss_low_priority
#define sched_ss_max_repl __ss_un.__ss.__ss_max_repl
#define sched_ss_repl_period __ss_un.__ss.__ss_repl_period
#define sched_ss_init_budget __ss_un.__ss.__ss_init_budget

linux 内核有三种调度策略:

  1. SCHED_OTHER 分时
  2. SCHED_FIFO 实时,先到先服务,一直运行到更高优先级到达或者时间片用完
  3. SCHED_RR 时间片轮转。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//linux 可以通过下面的函数来获取线程最高和最低优先级
int sched_get_priority_max(int policy);
int sched_get_priority_min(int policy);
//设置和获取优先级
int pthread_attr_setschedparam(pthread_attr_t *attr, const struct sched_param *param);
int pthread_attr_getschedparam(const pthread_attr_t *attr, struct sched_param *param);
param.sched_priority = 51; //设置优先级
//改变调度策略
int pthread_attr_setschedpolicy(pthread_attr_t *attr, int policy);
//设置调度策略
#include <sched.h>
int sched_setscheduler(pid_t pid, int policy,
const struct sched_param *param);
/*
sched_setscheduler()函数将pid所指定进程的调度策略和调度参数分别设置为param指向的sched_param结构中指定的policy和参数。sched_param结构中的sched_priority成员的值可以为任何整数,该整数位于policy所指定调度策略的优先级范围内(含边界值)。policy参数的可能值在头文件中定义。
如果存在pid所描述的进程,将会为进程ID等于pid的进程设置调度策略和调度参数。
如果pid为零,将会为调用进程设置调度策略和调度参数。
如果进程pid含多个进程或轻量进程(即该进程是多进程的),此函数将影响进程中各个子进程。
更改其他进程的调度参数需要有相应的特权。调用进程必须具有相应的特权,或者是具有PRIV_RTSCHED权限的组的成员,才能成功调用sched_setscheduler()。如果sched_setscheduler()函数成功地将pid所指定调度策略和调度参数分别设置为policy和结构param指定值 ,则该函数调用成功。
*/

gcc attribute

attribute关键字主要是用来在函数或数据声明中设置其属性。给函数赋给属性的主要目的在于让编译器进行优化。函数声明中的attribute((noreturn)),就是告诉编译器这个函数不会返回给调用者,以便编译器在优化时去掉不必要的函数返回代码。
GNU C的一大特色就是attribute机制。attribute可以设置函数属性(Function Attribute)、变量属性(Variable Attribute)和类型属性(Type Attribute)。

attribute书写特征是:attribute前后都有两个下划线,并且后面会紧跟一对括弧,括弧里面是相应的attribute参数。

attribute语法格式为:

attribute ((attribute-list))

其位置约束:放于声明的尾部“;”之前。

函数属性(Function Attribute):函数属性可以帮助开发者把一些特性添加到函数声明中,从而可以使编译器在错误检查方面的功能更强大。attribute机制也很容易同非GNU应用程序做到兼容之功效。

例子可参见这里