太长不想读

请直接看解决 UFW 和 Docker 的问题

问题

UFW 是 Ubuntu 上很流行的一个 iptables 前端,可以非常方便的管理防火墙的规则。但是当安装了 Docker,UFW 无法管理 Docker 发布出来的端口了。

具体现象是:

  1. 在一个对外提供服务的服务器上启用了 UFW,并且默认阻止所有未被允许的传入连接。
  2. 运行了一个 Docker 容器,并且使用 -p 选项来把该容器的某个端口发布到服务器的所有 IP 地址上。比如:docker run -d --name httpd -p 0.0.0.0:8080:80 httpd:alpine 将会运行一个 httpd 服务,并且将容器的 80 端口发布到服务器的 8080 端口上。
  3. UFW 将不会阻止所有对 8080 端口访问的请求,用命令 ufw deny 8080 也无法阻止外部访问这个端口。

这个问题其实挺严重的,这意味着本来只是为了在内部提供服务的一个端口被暴露在公共网络上。

在网络上搜索 “ufw docker” 可以发现很多的讨论:

基本上可以找到的解决办法就是首先禁用 docker 的 iptables 功能,但这也意味着放弃了 docker 的网络管理功能,很典型的现象就是容器将无法访问外部网络。在有的文章中也提到了可以在 UFW 的配置文件中手工添加一条规则,比如 -A POSTROUTING ! -o docker0 -s 172.17.0.0/16 -j MASQUERADE。但这也只是允许了 172.17.0.0/16 这个网络。如果有了新增的网络,我们也必须手工再为新增的网络添加这样类似的 iptables 规则。

期望的目标

目前网络上的解决方案都非常类似,而且也不优雅,我希望一个新的解决方案可以:

  1. 不要禁用 Docker 的 iptables,像往常一样由 Docker 来管理自己的网络。这样有任何新增的 Docker 网络时都无需手工维护 iptables 规则,也避免了在 Docker 中禁用 iptables 之后可能带来的副作用。
  2. 公共网络不可以访问 Docker 发布出来的端口,即使是使用类似 -p 0.0.0.0:8080:80 的选项把端口发布在所有的 IP 地址上。容器之间、内部网络之间都可以正常互相访问,只有公共网络不可以访问。 虽然可以让 Docker 把容器的某一个端口映射到服务器的私有 IP 地址上,这样公共网络上将不会访问到这个端口。但是这个服务器可能有多个私有 IP 地址,这些私有 IP 地址可能也会发生变化。
  3. 可以很方便的允许公共网络直接访问某个容器的端口,而无需额外的软件和配置。就像是用 ufw allow 8080 这样允许外部访问 8080 端口,然后用 ufw delete allow 8080 就不再允许外部访问。

如何做?

撤销原先的修改

如果已经按照目前网络上搜索到解决方案修改过了,请先修改回来,包括:

  1. 启用 Docker 的 iptables 功能,删除所有类似 --iptables=false 的修改,包括 /etc/docker/daemon.json 配置文件。
  2. UFW 的默认 FORWARD 规则改回默认的 DROP,而非 ACCEPT
  3. 删除 UFW 配置文件 /etc/ufw/after.rules 中与 Docker 网络相关的规则。
  4. 如果修改了 Docker 相关的配置文件,重启 Docker。稍后还要修改 UFW 的配置,可以一并重启。

解决 UFW 和 Docker 的问题

目前新的解决方案只需要修改一个 UFW 配置文件即可,Docker 的所有配置和选项都保持默认。

修改 UFW 的配置文件 /etc/ufw/after.rules,在最后添加上如下规则:

# BEGIN UFW AND DOCKER
*filter
:ufw-user-forward - [0:0]
:DOCKER-USER - [0:0]
-A DOCKER-USER -j RETURN -s 10.0.0.0/8
-A DOCKER-USER -j RETURN -s 172.16.0.0/12
-A DOCKER-USER -j RETURN -s 192.168.0.0/16

-A DOCKER-USER -j ufw-user-forward

-A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 192.168.0.0/16
-A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 10.0.0.0/8
-A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 172.16.0.0/12
-A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 192.168.0.0/16
-A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 10.0.0.0/8
-A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 172.16.0.0/12

-A DOCKER-USER -j RETURN
COMMIT
# END UFW AND DOCKER

然后重启 UFW,sudo systemctl restart ufw。现在外部就已经无法访问 Docker 发布出来的任何端口了,但是容器内部以及私有网络地址上可以正常互相访问,而且容器也可以正常访问外部的网络。可能由于某些未知原因,重启 UFW 之后规则也无法生效,请重启服务器。

如果希望允许外部网络访问 Docker 容器提供的服务,比如有一个容器的服务端口是 80。那就可以用以下命令来允许外部网络访问这个服务:

ufw route allow proto tcp from any to any port 80

这个命令会允许外部网络访问所有用 Docker 发布出来的并且内部服务端口为 80 的所有服务。

请注意,这个端口 80 是容器的端口,而非使用 -p 0.0.0.0:8080:80 选项发布在服务器上的 8080 端口。

如果有多个容器的服务端口为 80,但只希望外部网络访问某个特定的容器。比如该容器的私有地址为 172.17.0.2,就用类似下面的命令:

ufw route allow proto tcp from any to 172.17.0.2 port 80

如果一个容器的服务是 UDP 协议,假如是 DNS 服务,可以用下面的命令来允许外部网络访问所有发布出来的 DNS 服务:

ufw route allow proto udp from any to any port 53

同样的,如果只针对一个特定的容器,比如 IP 地址为 172.17.0.2:

ufw route allow proto udp from any to 172.17.0.2 port 53

解释

在新增的这段规则中,下面这段规则是为了让私有网络地址可以互相访问。通常情况下,私有网络是比公共网络更信任的。

-A DOCKER-USER -j RETURN -s 10.0.0.0/8
-A DOCKER-USER -j RETURN -s 172.16.0.0/12
-A DOCKER-USER -j RETURN -s 192.168.0.0/16

下面的规则是为了可以用 UFW 来管理外部网络是否允许访问 Docker 容器提供的服务,这样我们就可以在一个地方来管理防火墙的规则了。

-A DOCKER-USER -j ufw-user-forward

下面的规则阻止了所有外部网络发起的连接请求,但是允许内部网络访问外部网络。对于 TCP 协议,是阻止了从外部网络主动建立 TCP 连接。对于 UDP,是阻止了所有小余端口 32767 的访问。为什么是这个端口的?由于 UDP 协议是无状态的,无法像 TCP 那样阻止发起建立连接请求的握手信号。在 GNU/Linux 上查看文件 /proc/sys/net/ipv4/ip_local_port_range 可以看到发出 TCP/UDP 数据后,本地源端口的范围,默认为 32768 60999。当从一个运行的容器对外访问一个 UDP 协议的服务时,本地端口将会从这个端口范围里面随机选择一个,服务器将会把数据返回到这个随机端口上。所以,我们可以假定所有容器内部的 UDP 协议的监听端口都小余 32768,不允许外部网络主动连接小余 32768 的 UDP 端口。

-A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 192.168.0.0/16
-A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 10.0.0.0/8
-A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 172.16.0.0/12
-A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 192.168.0.0/16
-A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 10.0.0.0/8
-A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 172.16.0.0/12

-A DOCKER-USER -j RETURN

一个典型的 Shell 脚本的详细讲解和重构

上周无意中看到了一个 Shell 脚本,是一个很典型的开发工程师思维的脚本。我将会先分步讲解一下这个看上去很简单的脚本,随后再看如何重构。

脚本的完整代码如下:

PACKAGE="\"$1\""

line=$(java -version 2>&1  | grep $PACKAGE | grep -iv openjdk | wc -l)
#echo $line

if [[ $line =~ 0 ]]; then
  echo '{ "found": false , "not_found": true  }'

else
  echo '{ "found": true  , "not_found": false }'

fi

TL;DR 警告:如果太长不想看,就翻到最后看重构结果。

脚本的详细讲解

这段脚本的需求就是判断是否已经安装了特定版本的 Oracle JDK。在调用的时候会传入 Java 的版本号码,比如:1.8.0_161,这就是 $1 的值。

首先定义了 PACKAGE 这个变量,为什么在把 $1 赋值给 PACKAGE 的时候要转义双引号呢?是因为 java -version 这个命令输出的时候,版本号码是有双引号的。所以定义这个 PACKAGE 的作用是要精准匹配 java 的版本。

# Oracle JDK
chaifeng@local ~ $ java -version
java version "1.8.0_161"
Java(TM) SE Runtime Environment (build 1.8.0_161-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.161-b12, mixed mode)

# OpenJDK
chaifeng@local ~ $ java -version
openjdk version "1.8.0_144"
OpenJDK Runtime Environment (Zulu 8.23.0.3-macosx) (build 1.8.0_144-b01)
OpenJDK 64-Bit Server VM (Zulu 8.23.0.3-macosx) (build 25.144-b01, mixed mode)

然后就开始执行判断,并把结果赋值给变量 line。这行脚本看上去做了很多的事情,我们来拆开看一下。

java -version 用来输出版本号,但是输出到了标准错误上。而默认情况下,标准错误是无法通过管道传递给下一个命令的。所以需要 2>&1 来把标准错误的内容重定向到标准输出,这样才可以把 java 的版本号码传递给 grep 命令,否则 grep 是无论如何都无法获取到 java -version 的输出的。

第一个 grep 命令用来过滤 java -version 输出中包含版本号的所有行,并且这个版本号是用双引号包含起来的。正常情况下将会得到 java -version 命令输出的第一行。

然后再把结果传递给 grep -iv openjdk 命令。grep-i 选项是匹配时忽略大小写。-v 选项是排除,也就是反向匹配,将会输出不匹配的所有行。如果系统中正在使用的是 OpenJDK,前一个 grep 命令只是判断是否是指定版本的 JDK,包括 Oracle JDK 和 OpenJDK。这个 grep 命令将排除 OpenJDK 的输出,所以如果当前系统中使用的是 OpenJDK,这个命令执行之后可能会得到空的结果。

最后再用 wc -l 命令来得到结果的行数。如果是特定版本的 Oracle JDK,那结果就是一行。如果不是特定版本的 Oracle JDK,或者是 OpenJDK,就会输出空的结果,所以行数将会是 0

line 这个变量就会保存最后结果的行数,如果是 0 行显然就是系统中没有使用特定版本的 Oracle JDK。所以这里用了 [[ $line =~ 0 ]] 来判断变量 line 是否为 0

为什么最后会根据 line 是否为 0 来输出一个 JSON 的字符串呢?是因为这个脚本将在 Ansible Playbook 中调用。相关的 Ansible 代码如下:

- name: copy scripts to server
  copy: src="../files/check-java-version.sh"  dest="{{ java_download_path }}/"  mode="a+x"

- name: check if specific version of Oracle JDK is installed?
  shell: LC_ALL="en_US.UTF-8"  {{ java_download_path }}/check-java-version.sh  "{{ jdk_version }}"
  register: jdk_info
  changed_when: false
  failed_when: jdk_info.rc > 0

#- debug: var=jdk_info

- include: install.yml
  when: (jdk_info.stdout|from_json).not_found

这里有3个 task。第一个 task 是把脚本复制到服务器上,并设置了执行权限。第二个 task 是执行这个脚本,并且传入了 java 的版本,最后把 task 的执行结果保存到 Ansible 变量 jdk_info 中。jdk_infostdout 就将是 Shell 脚本最后输出的那个 JSON 字符串,比如:{ "found": false , "not_found": true }。第三个 task 是把第二个 task 的执行结果转换为 JSON 对象,并判断这个对象中的 not_found 的值是否为 true 来决定是否加载 install.yml 这个文件。

开始重构

所有相关的代码都分析完了,我们来看看如何重构,以及里面有哪些需要注意的问题。

之前也给几位朋友看过这个脚本,大部分人觉得问题主要在最后的 if 那里,尤其是输出 JSON 字符串的时候有互为相反的 foundnot_found 两个值。

先说典型问题,首先就是在 Bash 脚本中,如果不是特别需要,所有的变量一定要加上双引号!有没有双引号在 Bash 中是不一样的,不仅仅是双引号里面变量会被替换,而单引号不替换这个区别。

其次 if 的条件判断用了 =~ 这个操作符,这是 Bash 中的正则匹配的判断。判断语句 [[ $line =~ 0 ]] 的含义是判断变量 line 这个字符串里面是否包含字符 0。所以当 line 的值为 10, 201 都可以成功匹配的,这里显然不是期望这么判断的。当然了,正常情况下,line 的值要么是 1 要么是 0。正确的做法是,这里应该用 -eq 操作符来判断 line 这个变量是否等于数值 0

[[ "$line" -eq 0 ]]

最后是这个 if 判断有没有必要呢?

通常我们在 Java、Python、C 等语言里面函数的执行结果就是其返回值。在 Shell 脚本中,每一个命令的执行结果就是其输出。所以,对于刚从开发转到运维的工程师来说,就会习惯的想到根据命令的输出结果来判断。但 Shell 中还有一个很常用的判断方法就是根据命令的退出状态。如果命令执行成功,那退出状态就是 0,否则就是非 0。根据退出状态的不同,我们还可以知道命令为什么会执行失败。在 Bash 中,内部变量 $? 保存的就是前一个命令的退出状态。其实 Shell 脚本里的退出状态也可以类比为其他编程语言中的抛出异常。

Shell 脚本的退出状态就是脚本里面执行的最后一个命令的退出状态。通常我们不在意命令的输出,而只要根据退出状态来判断执行结果就可以了。对于现在重构的这个检查 Java 版本的脚本也一样,如果退出状态是 0 说明系统中是特定版本的 Oracle JDK。如果退出状态为非 0 说明不是特定版本的 Oracle JDK 或者安装的是 OpenJDK。

如果用退出状态来判断 Java 版本,那就没有必要输出 JSON 格式的结果了。if 的那5行代码就可以换成一行:

[[ "$line" -eq 1 ]]

如果是特定版本的 Oracle JDK,那变量 line 的值就是 1,这个判断就会成功,Shell 的退出状态就将是 0,否则是 1

可不可以再简单一点儿呢?是的,返回结果的行数也是没必要的。在获取行数那行里面

line=$(java -version 2>&1  | grep $PACKAGE | grep -iv openjdk | wc -l)

其实 java -version 2>&1 | grep $PACKAGE | grep -iv openjdk 就已经完成了特定 Oracle JDK 的判断。只有当前就是特定版本的 Oracle JDK 的时候,这个管道命令的退出状态才是 0,否则就是 1。那这个脚本就可以改写为 1 行:

java -version 2>&1  | grep "\"$1\"" | grep -iv openjdk

既然我们改用退出状态来判断结果了,那 Ansible 的 3 个 task 也要修改一下。由于脚本已经被改写为 1 行,其实也没必要放到一个脚本中专门执行了,所以就可以把第一个 task 删除掉。

第 2 个 task,原先是:

- name: check if specific version of Oracle JDK is installed?
  shell: LC_ALL="en_US.UTF-8"  {{ java_download_path }}/check-java-version.sh  "{{ jdk_version }}"
  register: jdk_info
  changed_when: false
  failed_when: jdk_info.rc > 0

最后有一行 failed_when: jdk_info.rc > 0,这行的含义就是如果命令执行失败就标记这个 task 为失败。类似于下面的代码:

if(jdk_info.rc == false)
  return false;
else
  return true;

所以其实这行本身就是没有用处的。但是现在我们要在 Ansible 中根据这个新的命令的退出状态来判断是否需要安装 Oracle JDK,所以需要忽略这个 task 的失败。因为 Ansible 在遇到任何 task 执行失败后都会终止执行剩余的 task。

改写后的 Ansible 代码如下:

- name: check if specific version of Oracle JDK is installed?
  shell: LC_ALL="en_US.UTF-8" java -version 2>&1 | grep  '"{{ jdk_version }}"' | grep -iv openjdk
  register: check_jdk
  changed_when: false
  ignore_errors: true

- include: install.yml
  when: ansible_check_mode or check_jdk is failed

最后用到了 Ansible 的内部变量 ansible_check_mode,用来判断是否运行在检查模式下。因为 Ansible 的 shell 模块不支持检查模式,所以当 Ansible 运行在检查模式下的时候,这个 task 会被忽略。导致 check_jdk 这个变量没有被定义,下一个 task 就会执行失败。

重构结果

删掉了一个脚本和一个 Ansible 的 task。

所以,原先脚本中的 java -version 2>&1 | grep $PACKAGE | grep -iv openjdk 这行就足够了,其它代码都不需要。Ansible 的 3 个相关 task 也可以删掉 1 个。

代码越多,Bug 也越多。

在我们老家,一些私人的重要纪念日还是按照农历日期的,比如生日什么的。如果是一些公共的假期,比如中秋、端午还好说,有很多现成的公共农历日历。如果是自己关心的几个农历日期就要自己添加提醒了,尤其是要批量添加20、30年的农历日历提醒,而且每年好几个。

为了省事,有两个事情需要解决:

  • 获取特定农历日期对应的公历日期
  • 在命令行下添加一个提醒到 Google Calendar 或者 Apple Calendar

用 lunar 来获取农历和公历的对应日期

编译

在 Debian 的仓库里面有一个名为 lunar 的软件可以查询农历和公历的对应日期。这个软件非常的老了,最新的版本是2001年10月发布的 2.2 版本。能够查询的日期范围是1900年到2049年之间的。在 macOS 上需要自己下载编译,目前无法从 Debian 的 git 仓库中获取源码,还好有打包好的 lunar 源码可以下载。

下载并解压后,执行 make 命令即可。虽然可以看到很多的警告信息,但在源码目录下已经编译成功。


基本用法

使用上非常简单,直接执行会看到简单的帮助信息。

注意到有一个选项 -h 是输出中文信息的,但由于这个软件太过于『古老』,以至于没有支持 UTF-8。所以如果你的终端的字符集不是 GBK 或者 GB18030,在使用 -h 选项的时候需要用 iconv 命令来把输出转换为当前终端的字符集。

lunar -h 2019 10 13 | iconv -f gb18030

有一个比较有趣的选项是 -b 可以以『位图』的方式输出

查询公历对应的农历日期

比如:要查询2019年10月13日对应的农历日期

lunar 2019 10 13

可以看到那天是农历的九月十五日。

-i 选项来查询农历对应的公历日期

比如:要查询2019年农历九月十五日对应的公历日期

lunar -i 2019 9 15

使用 Fantastical2 来添加日历

我购买了 Fantastical 2 for Mac,这是一个非常易用的 macOS 桌面日历工具。有一些比较强大的功能,比如:

  • 支持自然语言来添加提醒
  • 跨日历合并相同的提醒
  • 支持 AppleScript
  • ……

给一个特定的日期添加一个提醒

只要在 Fantastical 的输入框里面输入一行文字,带上日期即可。要指定一个地点,就在用『at』加上地址。如果有多个不同的日历来源,希望添加到特定的日历中,那就在行首加上『/日历名称』。

只有日期没有具体的时间时,默认是全天的事项。由于 Fantastical2 对中文的自然语言其实是没有什么支持了,所以精准的时间还是别用自然语言了。

比如我的默认日历是『工作』,希望添加到『家庭』的日历,就在最前面添加『/家庭』。

把添加事项自动化!

在 Fantastical2 的帮助文档的与其他应用整合里面有提到多种自动化的添加方式,比如用 URL 的方式:

open "x-fantastical2://parse?s=/家庭 2018-6-7 6:45 在太原机场打印行程单"

这会直接打开 Fantastical 的快速添加窗口,按下回车键即可添加一个事项。

这需要我们手工确认一下!自动化一定是 100% 的,没有 99% 的自动化。还有有个 add 选项,用上选项 add=1 就可以直接添加事项而无需确认了。

open "x-fantastical2://parse?s=/家庭 2018-6-7 6:45 在太原机场打印行程单&add=1"

执行这个命令后就直接在『家庭』日历中添加了一个事项。看上去不错了,但有个问题是,每次执行了命令 Fantastical 的窗口都会打开。毕竟 open 命令就是打开应用程序的。

自动化的实现中还是要避免本没有必要显示的窗口。

用 AppleScript 来实现完全的自动化

在 Fantastical 的帮助文档中可以看到一个 AppleScript 的例子

tell application "Fantastical 2" 
    parse sentence "Wake up at 8am" 
end tell

要实现完全的自动化,就加上 with add immediately

tell application "Fantastical 2" 
    parse sentence "Wake up at 8am" with add immediately 
end tell

现在我们已经知道了所有需要的信息了,创建一个 AppleScript 脚本 fantastical.scpt,内容如下:

on run argv
  set query to (item 1 of argv)
  tell application "Fantastical" to parse sentence query with add immediately
  return "添加日历:" & query
end run

然后就可以用 osascript 来执行这个脚本,并加上我们要添加的事项。

osascript fantastical.scpt "/家庭 2018-11-4 9am 去买个蛋糕"

执行这个命令时,Fantastical 的窗口也没有打开,完美。

写个 Shell 通过 lunar、Fantastical 来按照农历日期批量添加事项提醒

虽然我们已经搞定了几乎所有的事情,也只需要用 osascript 这个命令一行就可以自动的添加提醒事项,但是我们需要总是记得这个 fantastical.scpt 脚本在哪里。我希望直接就执行一个命令,不要再带上路径什么的。虽然有 PATH 这个环境变量,但只是对命令生效。如何可以直接执行 fantastical.scpt 而无需带上路径呢?

Shebang!

我们可以在网络上搜索到很多的 AppleScript 的代码,发现里面的注释都是用类似 SQL 的方式,是两个短线 --。但其实 AppleScript 从 Mac OS X Leopard 发布的 2.0 版本开始就支持用 # 来做注释了,所以 Shebang (Unix)

在第一行添加上 #!/usr/bin/env osascript,并且给文件添加上可执行权限。

#!/usr/bin/env osascript

on run argv
  set query to (item 1 of argv)
  tell application "Fantastical" to parse sentence query with add immediately
  return "添加日历:" & query
end run

添加可执行权限

chmod +x fantastical.scpt

我习惯上把所有自己写的脚本都放到 $HOME/bin 这个目录中,并且这个目录也添加到了 PATH 环境变量里面。

把文件 fantastical.scpt 移动到 ~/bin 目录中,现在就可以直接执行啦!

mv fantastical.scpt ~/bin

fantastical.scpt "/家庭 2018-11-4 9:30 再去买一个蛋糕吧"

目前感觉已经不错了,可以无需确认的添加一个事项,而且 Fantastical 的窗口也不会打开,在执行命令的时候也不用指定路径了。

但还是有一点 小小的缺陷,就是 fantastical.scpt 其实只支持一个参数。就是说,在添加事项的时候,一定别忘记用引号,否则会出问题的。比如

fantastical.scpt 买个蛋糕 2018-11-4

其实真正传递给 Fantastical 的内容只有『买个蛋糕』,而没有后面的的日期。所以默认就是在当天添加了一个『买个蛋糕』的事项,这不是我们要的。

虽然记得加上引号不是什么问题,但我们还可以做的更好。

一个简单的解决办法就是用 AppleScript 把传递进来的 argv 数组用空格连接起来,而不是只是获取数组的第一个元素。但对于自动化的实现来说,还有一种常用的传递参数的方式,是通过管道来传递。这种方式用 AppleScript 麻烦了一些,用 Shell 就很容易了。

用 Shell 让自动化再完美一些

用 Shell 可以解决两个问题:

  • 把所有的参数用空格连接起来,作为一个参数传递给 fantastical.scpt
  • 可以把通过管道传递过来的数据,转换为调用 fantastical.scpt 的参数

把所有的参数打包作为一个参数

用 Shell 接受参数是很容易的,在脚本中使用 $1$2$3、…… 就可以获得第1、2、3、……个参数。说到这里,或许有人不知道如何获取第10个或者以上参数。难道不就是 $10 吗?还真的不是!

看下面的 Bash 脚本

#!/bin/bash

echo 第十个参数是 $10

就是直接输出第 10 个参数,如果执行命令

./test-parameter.sh 1 2 3 4 5 6 7 8 9 a b c d e f

是不是应该就是输出:第十个参数是 a?让我们执行一下看看效果。

结果竟然是『第十个参数是 10』!莫非是把 a 当做十六进制转换为十进制?其实在 Bash 中,第1-9个参数可以直接用 $ 加数字,但第10个及其以上,需要用花括号{}包含起来,所以正确的代码应该是:

#!/bin/bash

echo 第十个参数是 ${10}

再次执行

./test-parameter.sh 1 2 3 4 5 6 7 8 9 a b c d e f

就会看到正确的结果:第10个参数是 a

回到我们要解决的问题上,如何用 Bash 把接受到的所有参数用『空格』连接起来传递给 fantastical.scpt 呢?

很多人会知道在 Bash 下可以用 $@ 来代表所有的参数!

#!/usr/bin/env bash

fantastical.scpt "$@" # it's wrong

这样其实是不对的!有两个解决办法,一是先把$@赋值给一个变量,然后把变量传递给 fantastical.scpt

#!/usr/bin/env bash

QUERY="$@"
fantastical.scpt "$QUERY"

另一个办法是用 $*

#!/usr/bin/env bash

fantastical.scpt "$*"

以上两种方式可以任选一个。在 Bash 里面 $@$* 是不一样的,这里就不细谈了。

把通过管道接受的数据传递过去

通过管道传递的数据是无法用 $@ 或者 $* 来获取的,但是可以用 cat 来获取,代码如下:

#!/usr/bin/env bash
set -eu

QUERY="$(cat)"
"${BASH_SOURCE}.scpt" "$QUERY"

现在有了新问题,如何判断当前是要通过管道接受数据呢?通常情况下,通过管道接受数据的时候是没有参数的,所以一个办法就是通过内置变量 $# 来判断是否有参数。但这个判断并不准确,有时候我们需要在代码中判断如果没有任何选项参数的时候就显示一个简单的帮助信息。还有一个常见的判断方法就是利用 tty 这个命令。tty 会显示当前终端的名字,如果是通过管道接受数据,则会出错,显示 not a tty 的错误信息。

利用 tty 来区分是否从管道获取数据,代码如下

#!/usr/bin/env bash
set -eu

if tty; then
  QUERY="$@"
else
  QUERY="$(cat)"
fi

fantastical.scpt "$QUERY"

最后再改进一点儿,从管道获取数据的时候允许合并命令行上额外的参数,并且不显示 tty 的任何输出。

注意:IO 重定向 >/dev/null 2>&12>&1 >/dev/null 这两个是不同的。

#!/usr/bin/env bash
set -eu

QUERY="$@"

if ! tty >/dev/null 2>&1; then
  QUERY="$QUERY $(cat)"
fi

fantastical.scpt "$QUERY"

最后如果发现没有任何信息要添加,也就是变量 QUERY 为空值,那就显示帮助信息,搞定!

完整的脚本

这两个脚本都放置在 PATH 里面某一个目录中建议是 $HOME/bin,并且用 chmod +x fantastical* 设置可执行的权限。

fantastical

#!/usr/bin/env bash
set -eu

QUERY="$@"

if ! tty >/dev/null 2>&1; then
  QUERY="$QUERY $(cat)"
fi

if [[ -z "$QUERY" ]]; then
  echo "Usage: "
  echo "  $0 /家庭 2019-10-13 8am 记得去买个蛋糕"
  exit 1
else
  "${BASH_SOURCE}.scpt" "$QUERY"
fi

fantastical.scpt

#!/usr/bin/env osascript

on run argv
  set query to (item 1 of argv)
  tell application "Fantastical" to parse sentence query with add immediately
  return "添加日历:" & query
end run

开始执行

比如我要给每年的农历九月十五这一天加一个提醒

for YEAR in {2018..2049}; do
  fantastical "$(lunar -i $YEAR 9 15 | grep -m1 -Eo "[0-9]{4}\.[0-9]{1,2}\.[0-9]{1,2}")" 这一天是农历的九月十五; 
done

就这么几行代码,搞定!写 Shell 脚本是不是很简单呢?

写完这两个脚本,并添加所有的农历日期提醒,也就半个来小时,写文章就多半天过去了。

TL;DR

请直接跳到最后看总结。

Dev 与 Ops 的技术栈对比

DevOps 是最近几年非常流行的词汇,有越来越多的公司开始实践 DevOps,但如何可以真正的把 DevOps 做好呢?

我曾经和几个业内朋友聊过,为什么现在的 DevOps 培训关注在协作或者工具上?这些就是把 DevOps 做好落地所需要的吗?那几个朋友也同意,只是关注在协作和工具上是不够的。

首先,运维(Ops)是一个全面并复杂的工作,并不是把开发团队开发完成的应用部署到生产环境,如果出了问题就找开发团队来解决。运维包括了系统、网络、数据库、安全、开发等不同领域的工作。要把运维这个工作做好也不止是会一些工具这么简单的事情。其次,也不只有 DevOps 才关注团队之间的协作。如果协作和工具对于做好 DevOps 是足够的,就好比协作和开发语言或者框架对于做好开发是足够的。但我们都知道,对于开发来说,除了协作、编程语言、开发框架外还有不少很重要的技能,而这些技能也是区分资深开发工程师和初级工程师的标准之一。作为一名资深的开发工程师,除了基本的编程语言和框架外,还需要了解和掌握一些高级知识。比如资深的 Java 开发工程师需要了解并掌握反射、类加载、垃圾回收多线程开发、调试工具等等高级知识。这些高级知识可以帮助他们更好地处理一些疑难复杂问题,并构建更加健壮灵活的应用程序。而且也有越来越多的开发工程师开始意识到 TDD、重构这类工程实践可以帮助他们提升开发技能,以开发出代码质量更好,也易维护的应用。

对于 DevOps 来说,技术栈不仅仅包括开发和运维,还包括测试、安全、网络、业务等等领域,但开发和运维是相对重要的两个。对于提升开发质量的高级知识和技能,很多人都已经很熟悉了,但有哪些是做好运维的高级知识和技能呢?如果运维做不好,那也很难做好 DevOps 的落地实践。下面我们以 Java Web 开发工程师和 Unix/Linux 运维工程师为例,来做一个技术栈的对比。通过这个技术栈的对比,可以让我们更加清楚的看到做好运维的基础在哪里。

Java Web 开发技术栈

主要开发语言

对于 Java 开发工程师来说,哪个编程语言是主要开发语言呢?很显然是 Java。如果一个工程师的主要开发语言是 PHP,那他就是 PHP 开发工程师了。作为一名合格的 Java 开发工程师,显然要对 Java 语言非常熟悉。不需要对 Java 语言的每一个细节都了如指掌,但最起码对绝大多数的语法非常了解。很难想象一名对于 if 语句都不甚明了的 Java 开发工程师可以开发出高质量的 Java 应用。

还需要掌握其他编程语言或标记语言

Java 工程师在开发应用时只会用 Java 语言是不够的,还需要了解并掌握一些其他编程语言或标记语言。比如 SQL、JavaScript、CSS、HTML、XML 等等。如果使用了一些CSS框架,可能还要掌握 Less (层叠样式表) 或者 Sass

核心开发库

在学习 Java 编程的时候,主要是在学习什么?事实上Java 语法并不多、也不复杂,在几天内就可以把所有的语法学习完毕。那在学习 Java 编程的时候是在学习什么呢?其实是在学习 Java Development Kit(JDK),这是 Java 开发的核心开发库。在 JDK 中有非常多的包和类,几乎没人可以记得有哪些包,每个包里面有哪些类。但作为一名合格的 Java 开发者,一定会熟悉一些常用包,比如: java.lang、 java.util、 java.io 等等。同样的,也很少有人可以记得这些常用的包里面有哪些类,但一定非常熟悉一些常用的类及其方法,比如:java.lang.Stringjava.io.Filejava.util.HashMap 等等。

如果要开发嵌入式或者企业级 Java 应用,可能还要熟练掌握 Java ME 或者 Jakarta EE(以前的 Java EE、J2EE)

除了 JDK 的核心类库以外,资深的 Java 工程师还要了解很多 Java 虚拟机的相关知识。比如Java 内存模型垃圾回收、Java 的类加载、高效并发的 Java 多线程开发技术等等。

第三方类库

理论上,当一名 Java 开发工程师掌握了 Java 语法以及 JDK,并且也熟悉一些第三方的语言,他已经可以开发出几乎任何的 Java 应用程序了,虽然可能会花费比较多的精力和代价。我们可能会使用一些第三方的类库来避免『重新发明轮子』,比如 Apache CommonsGoogle GuavaSLF4J 等等。借助很多的第三方类库,Java 开发工程师可以非常方便的处理 Excel 文档、解码音频文件、校验 OpenPGP 签名、处理日志等等。这些第三方类库可能也依赖其他的类库,所以可能就需要 Apache Maven 或者 Gradle 来帮助进行依赖管理和构建。

框架

虽然有很多的第三方类库可以帮助 Java 开发工程师减少一些重复的代码开发工作,但是对于不同的项目可能会有一些类似的行为逻辑。比如对于绝大多数的 Web 项目,总是需要处理模板、数据与表单的绑定、异常的拦截和处理、权限和安全认证等等重复的工作,于是需要开发框架来简化这些重复的工作。开发框架与具体的应用和业务无关,但是实现了最为基本的软件架构和体系,并提供了通用的功能,以便让开发者关注在业务逻辑的实现上。所以,开发框架对于业务逻辑来说什么也没有做,开发工程师还是需要自己去实现所有的业务逻辑。常见的 Java 开发框架有 Spring FrameworkApache Struts 2Play Framework 等。

框架固然重要,但编程语言的基础和核心开发库的掌握更重要。尽管有的公司内部有自己开发框架,并且内部的开发框架也实现了一些通用的业务逻辑。但对于每一个特定的内部项目来说,还是需要开发工程师去实现这些特定的业务逻辑代码。

以上已经列出了最为基本的 Java Web 开发技术栈,绝大部分的 Java Web 应用都是基于以上的技术栈来开发的。

基本的运行环境

对于 Java Web 应用,最基本的运行环境有 Apache TomcatJetty 等。通常 Java 开发工程师会在开发环境、测试环境使用这些基本的运行环境来调试、测试应用程序。如果没有太多的用户,甚至也可以用到生产环境中。

一旦需要为更多的用户提供服务,单个的最基本的运行环境可能就不足以支撑了。

集群和高可用的运行环境

虽然也可以用 Tomcat 或者 Jetty 来搭建一个规模不大的集群环境,但是使用一些 Java 企业级应用服务器会让事情变的更简单,比如 IBM WebSphereOracle WebLogic ServerJBoss Enterprise Application Platform 等等。不仅有了更高的性能、更高可用性、增强的安全控制,也更方便的去管理和部署 Java 应用集群。

Java 的开发工作环境

作为一名 Java 开发工程师,平时的工作环境通常是在一个 Java 集成开发环境 中,可以是 IntelliJ IDEAEclipse 或者 NetBeans,甚至是使用 Vim 或者 Emacs 搭配一些 Java 开发相关的插件和命令行。不管是哪一个,至少一定会非常熟悉其中的一个。

资深的 Java 开发工程师可以在自己熟悉的开发环境中非常高效的工作,熟悉常用的配置、快捷键。通常通过观察一名 Java 开发工程师对于日常工作环境的熟练程度,基本上可以判断是否是一名经验丰富的开发工程师。

Unix/Linux 运维技术栈

主要的开发语言

Java 开发工程师的主要工作语言是 Java。那什么是 Unix/Linux 运维工程师的主要语言呢?有一些人会说是 Python,但其实应该是 Bash 或者其它某一个 Unix shell。对于这个结论,有些人可能会对此有些争议。现在我先简单的说明一下,首先 Bash 是一个 Shell,而 Python 不是。我们必须通过操作系统来使用计算机,但我们无法直接使用操作系统内核。Shell 是一个用户界面,用于访问操作系统内核所提供的服务,比如文件管理、进程管理等。其次 Bash 作为一个编程语言,绝大部分的人对 Bash 的语法了解比较有限。常见的一个误区是,认为 Bash 的语法比较奇怪。比如绝大部分编程语言的 ifwhile 等关键字后面是圆括号,而 Bash 用的是方括号 [ ],并且这个方括号和表达式之间必须有个空格,而且如果没有空格就会出现语法错误。如果认为这是 Bash 奇怪语法的一部分,那就完全错误了。

请看下面的 5 个 Bash if 语句的例子,这些语句之间的区别就是 if 后面的『括号』,哪些语法是正确的?

  1. if [ ... ]; then ...; fi
  2. if [[ ... ]]; then ...; fi
  3. if ( ... ); then ...; fi
  4. if (( ... )); then ...; fi
  5. if { ... }; then ...; fi

大部分人会认为第1、2个是正确的,毕竟经常在各种 Shell Script 里面见到,虽然不一定明白一个方括号和两个方括号的区别。少部分人猜测第3、4个或许也是正确的,多数也仅仅只是猜测而已。但几乎所有的人认为第5个是错误的,因为从来没有见过 if 关键字后面可以用『花括号』作为语法的编程语言。

其实,所有的5个语法都是正确的。如果答案不是这个,说明对 Bash 的语法并不熟悉,甚至不了解。很遗憾的是,甚至市面上的一些有关 Unix/Linux 运维的技术书籍里面也有这样的错误,有从国外翻译的也有国人编写的。

如果一名开发者在对于一个编程语言了解极为有限,甚至连语法都不熟悉的情况下,是如何得出这个语言『很简单』、『仅适合简单的任务,不适合做复杂的工作』、『语法怪异』、『没有某某编程语言强大』等结论的?

我猜这来源于一个根深蒂固的认知,认为脚本和代码不一样,脚本更简单,无需编译就可以执行。比如 Java 代码需要用 javac 命令编译后,才能用 java 命令来执行,不可以像 Bash 这样直接运行。

下面这个截图没有做过任何的修改,从截图上可以看到 Java 代码可以像 Bash 脚本一样直接执行!这怎么可能?但是在我的电脑上可以随时演示这个奇怪的『真正的 Java Script』例子,因为我用了 Zsh 的一些特性来实现了直接执行 Java 代码的功能。我只是通过这个例子来展示代码和脚本其实是同样的东西! 脚本只是隐含了编译的过程,没有显式的编译并不意味着不需要编译,难道 CPU 可以直接执行脚本吗?别忘记在十多年前,我们不也把 Python,JavaScript 称作脚本语言吗?但现在都叫它们代码

下面是我拍摄的几本书里面关于 Bash if 语法错误的例子:

这里就不继续讨论了,我会在另一篇文章里来单独聊一下 Bash。

还需要掌握其他编程语言或标记语言

仅仅会用 Shell 是不够的,合格的运维工程师也需要了解并掌握其他语言,比如:Python、Perl、SQL、XML 等等。也包括一些 sedawk 这些命令也有自己的一套开发语言。

核心开发库

Java Development Kit(JDK)是 Java 开发的核心库,每一位合格的 Java 开发工程师都应该了解并掌握。那对于 Unix/Linux 运维工程师来说,运维工作的核心开发库是什么?

在学习 Java 的时候,我们花了绝大部分的时间在学习 JDK 上。那在学习 Shell 的时候,我们也不是一直在学习 Bash 的语法。虽然 Bash 的语法与其他编程语言有些不同,但也不多不复杂,只要花上几天也可以把所有语法了解完毕。那花了绝大部分的时间是在学习什么呢?是 Unix/Linux 的核心命令。

任何一个 Java 源码里面都会用到很多 JDK 里面的类库,同样,任何一个 Bash 脚本里面也会使用很多的 Unix/Linux 核心命令,比如 ls、cd、ps、grep、kill、cut、sort、uniq、wc、mkdir、rm、……。这些命令会随着 Unix/Linux 一起发布,随着系统一起更新。通常位于 /bin/usr/bin/sbin/usr/sbin 等路径中。

与 Java 开发类似,几乎没有谁可以记得 JDK 的所有包和类。同样的,对于运维工程师来说也很少有谁可以记得所有的 Unix/Linux 的核心命令以及每个命令的所有选项。但是作为合格的运维工程师,需要非常熟悉并掌握一些常用的核心命令,以及这些命令常用的选项参数。

资深的 Java 工程师需要了解 JDK 和 Java 虚拟机的一些高级知识。对于资深的运维工程师来说,也一样需要了解 Unix/Linux 的一些高级知识,比如信号、进程、内存管理、磁盘管理、RAID、防火墙、路由表等等。所以如果一名运维工程师不明白 HUP 信号,而到处使用 nohup 命令启动程序,或者不分青红皂白的总是使用 kill -9 来杀掉进程,很难相信他会是一名资深或者合格的运维工程师。

第三方类库

理论上,当一名运维工程师掌握了 Shell、其他编程语言以及Unix/Linux 的核心命令,他已经可以做几乎所有的运维工作了,但是将会花费他很多的精力和代价。比如要在不同的服务器之间同步文件,是可以通过 Shell 以及系统的核心命令来完成这个工作,但使用类似 rsync 这样的第三方工具会节省很多的时间和精力。

Unix/Linux 运维的第三方库就是不属于操作系统核心命令的那些命令,比如 rsync、 curl、zip、unzip、unrar、tmux、Xvnc、……等等,这些命令通常位于 /usr/bin/usr/local/bin 等路径中。这些第三方的工具可就非常的多了。

框架

Java 开发者有不少的 Java 开发框架可以使用,比如 Spring、Struts 什么的。那运维的框架有哪些呢?或者运维工作中有『框架』这样的工具吗?好像从来没有听说过。

让我们先看一下 Java 开发工程师使用的开发框架,这些框架提供了一些通用的功能,可以帮助我们去做一些重复的工作,但是与具体的业务无关。在运维工程师在工作中有哪些通用重复的工作呢?比如:在安装一个软件包之前,可能需要检查这个软件包是否已经安装;在启动一个服务之前,要检查这个服务是否已经启动;在修改用户密码之前,先检查用户的密码是不是就是当前就要修改的这个密码;在完成一系列的运维工作后,可能需要生成一份报表,在哪些服务器上做了哪些变更,哪些服务器出现了错误等等;……。

所以配置管理工具就好比运维工程师的开发框架,比如 PuppetChefAnsible 等等。框架与具体的业务无关,运维工程师需要在服务器上安装什么软件包、需要做什么配置变更、该向哪个进程发送什么信号,不会因为使用了配置管理工具就可以不用去做。也或者,配置管理工具虽然可以帮助运维工程师重启某个特定的服务,但对于自定义的应用来说,重启时该做什么操作、需要监控哪个进程、应该切换到哪个用户的权限下,这些工作还是需要运维工程师自己去实现。

以上列出了最基本的 Unix/Linux 运维工程师的技术栈,绝大部分的 Unix/Linux 运行环境都是基于这个技术栈来维护管理的。

基本的运行环境

对于运维工程师来说,最基本的管理环境就是一台独立的服务器、虚拟机、VPS、或者一个 Docker 容器等等。通常开发环境或者测试环境都运行在一个比较基本的运行环境中,如果没有太多的用户,也可以用于生产环境。

一旦要为更多的用户提供服务,一台或者几台服务器可能就不够了,需要更多的服务器、VPS、Docker 容器。

集群和高可用的运行环境

如果运行节点很多,环境也比较复杂,可能就需要比如 Amazon AWSOpenStackGoogle云端平台 等平台帮我们管理大规模的 VPS 环境。对于 Docker 容器,可能就需要 KubernetesDocker Swarm 等平台。

借助这些 IaaS 平台,不仅有了更灵活的管理、更高的可用性和安全性。运维工程师可以非常方便的把应用部署到一个集群中,或者一个简单的命令就可以增加或减少服务节点数量,监控和维护整个应用集群。

运维的工作环境

Java IDE 是 Java 开发工程师的工作环境,运维工程师的工作环境呢?有人会说是 Vim,因为很多的运维工程师在服务器上都是用 Vim 来修改配置文件的。Java 工程师可以在一个 Java IDE 中去完成几乎所有的开发工作,运维工程师可以不用退出 Vim 而完成几乎所有的工作吗?(如果是使用 Emacs 还是有可能的,但 Vim 真的有点儿够呛。开个玩笑,不是要在这里引发编辑器之战,但的确也是事实!)

事实上,运维工程师的工作环境就是操作系统的 Shell,对于 Unix/Linux 来说就是命令行 或者 GNOME 这类的图形用户界面

一名合格的运维工程师一定会非常熟悉他的工作环境的,如果一位 Windows 运维工程师不知道 Ctrl-xCtrl-vCtrl-z 等常用快捷键,很难相信他会是一位合格或者资深的 Windows 系统的运维工程师。有多少人知道可以在命令行进行剪切、粘贴、撤销等操作?

总结

Dev & Ops 技术栈对比

通过这个对比,可以帮助我们看清楚一些问题。比如有人会问,已经在项目中使用了 Kubernates,还有必要使用 Ansible 吗?这就好比已经在项目中使用了 JBoss,还有必要使用 Spring Framework 吗?还有人会问,Unix/Linux 的各种命令实在太多了,学习使用 Chef 是不是可以容易点儿?这就好比学习 JDK 的各种类库太多了,学习 Play Framework 是不是可以让开发更容易些?

基础技能

从这个对比可以看出,表格的上半部分是基础技能,不管是对于 Java 开发工程师还是 Unix/Linux 运维工程师。

如果一个团队的开发质量比较差,系统 bug 多,对这个团队进行三天的 WebSphere 或者 Tomcat 培训可以起多大的作用?运维工作的质量不高,系统不稳定,三天的 Docker 或者 Kubernates 培训能改善多少?想想看,在三天的 Docker、Kubernates 培训中,花在 Dockerfile 的 RUN 指令后面的脚本上的时间有多少?花在与命令行与各种奇怪问题斗争的时间有多少?

有很多资深的开发工程师在写代码的时候总是会记得重构代码、要解耦、注意好的命名,但一写 Shell 脚本就全都忘记了。别忘记,脚本就是代码,代码有各种臭味,脚本也有。

一名优秀的运维工程师一定是优秀的开发工程师,但优秀的开发工程师不一定是优秀的运维工程师。

19. March 2014 · Write a comment · Categories: 技术文章 · Tags:

3月9日星期天举行了太原 QClub 社区在2014年的第一次活动,这次是在太原理工大学迎西校区的学术交流中心,所以活动现场的学生就占了绝大多数。大概到场人数有50人,除了工大的同学之外,还有一些职业程序员。我们还准备了5本签名版的《MacTalk:人生元编程》作为小礼品。

这次的活动主题是聊一聊2014年我们程序员该关注哪些技术,考虑到同学们估计比较多,所以还有一个面向同学们的话题是雷晓宝分享的《大学计算机课程与实践》。首先是我分享了一个云计算的普及话题,IaaS、PaaS、SaaS 分别为我们解决了什么问题?很多同学都认为云计算就是一堆虚拟机,也确实没错,绝大多数的云计算都有虚拟化的应用。而且虚拟化也确实是云计算中一个重要的技术,但仅仅虚拟化是不够的,用虚拟机代替物理机也不是云计算的全部。这些名词或技术的出现,一定是为我们解决了某一方面的问题。然后我用了一些比喻和例子给同学们分享了一下这三个名词分别做了什么。随后就是雷晓宝同学的《大学计算机课程与实践》的话题了,在这个话题中晓宝分享了计算机专业的一些理论课程分别对应在平时实际使用的哪些技术上,也提到了 IT 行业中不同的工作需要侧重哪些不同的技术。这个话题其实是比较大的,而且内容也很多,足足分享了一个多小时。最后就是解读 ThoughtWorks 的 2014 年第一季度技术雷达,TW 的技术雷达可以说是 IT 行业的风向标。因为涉及的内容非常多,所以我们精选了一些比较有趣的内容。比如移动设备的持续交付、客户机和服务器使用相同的代码(我们在去年的一次活动上就是一起写代码研究这个技术)、云开发环境、JavaScript 的依赖管理、Docker、Clojure、CoffeeScript、等等。活动的过程中,有5位同学与我们的讲师互动,幸运的获得《MacTalk:人生元编程》,在 5点半的时候结束当天的活动。最后我们还和几位热心的同学一起聚餐,又聊到晚上8点多。

话不多说,上图:

在前几天刚刚闭幕的 QCon 北京大会最后一天下午,有几场小型的演讲。其中,国内知名的安全专家、微信“道哥的黑板报”的作者道哥就分享了一个有关黑客的话题,现场观众们不时的发出一阵阵的惊呼。关于安全呢,我也不算是小白。在大学以及刚毕业两三年的时候,对安全领域还是很关心的,也见到并认识了不少国内安全领域的朋友们。随着在二线城市苦逼的 Java 码农日子一天天的混下去,我已经渐渐远离这一领域,但是很多安全的习惯还基本保留着。

道哥在 QCon 上就说到,他的电脑一直就是裸奔的,也就是没有安装任何的安全防护软件。我在 06 年之前主要使用 Windows XP,就是裸奔的。一是因为机器配置低;二是也没啥必要;三是 Linux 内核与笔记本上的某个芯片组不太兼容,会时不时的死机。07年之后,我就全面切换到 Linux 下工作和娱乐,这就更是裸奔了。在 2010 年的 QCon 北京大会的某一天晚餐后,我遇见了国内技术领域的大牛周爱民,我们相谈甚欢,和大众点评、百姓网的几位朋友一直聊到凌晨2点。那个晚上我就提到了平时我是怎样注重网络安全和隐私的,记得当时他们用“变态”一词来形容我。

又回到今年 QCon 闭幕的第二天,我在地铁上犯了安全大忌,连接了一个未知的 WiFi,并发了一条微博。很快大连的谊昌老兄就立刻回复我说“看了昨天道哥的演讲后,俺决定再也不连自己不清楚的wifi了~”,这个我得承认,当时没有开启 VPN。虽然这个 WiFi 的名称是 “AndroidAP”,但是不能保证这是一个普通的 Android 手机。

说了这么多,想必很多人已经知道不能随便连接免费的 WiFi,但是他们却很放心的连接酒店里提供的网络。事实上,免费的 WiFi 背后不一定都是坏人,但是国内酒店网络的背后则一定是不安全的,泄露自己的隐私的几率一定是 100%,你的一举一动都在老大哥的眼皮底下。

回到酒店后,我决定把酒店网络背后的东西拿出来给大家看看。这里就不讨论如何入侵酒店的网络和后台,只给大家看看背后的这一个管理平台。

正题开始……

酒店里上网可能会需要密码,就在这里设定的。当时我入住的就是 2111 这个房间,第一个密码显然是前一个入住客人的,酒店前台没有删除。第二个密码是在我办理入住的时候设定的,但是我一直没有使用过。

20130430161855

 

还能看到 IP 地址与个人的关系。

20130430164951

 

在这里还能获取到入住客人的身份证信息,省略省略

20130430162203

 

一旦你连入酒店网络,你的哪些隐私会被泄露呢?哇哦,有个用户上网日志查询,就是这里,但是还需要一个密码。这个密码应该是给平平和安安用的,要不酒店的前台就能随便看了。而且这个密码或许是个比较复杂的,md5 的哈希值是 47e57be0904ce7174115a08cb1a1e304,我用几个在线的 md5 数据库都没有查到对应的明文,如果有查到的朋友还望不吝告知。大家看到这里不要失望,跳过去也是很容易的,略过不表,且看下文。

20130430201827

 

输入了查询密码后就能查询用户上网日志了,看看吧,会不会让你的后背发凉?搞不明白的是,为啥还要监控这几个游戏呢?

20130430163934

 

 

给大家看看 MSN 的日志,查询了最近三天的记录,只有一条。结合前面的用户管理功能,我很容易知道这个 MSN 的主人是谁。

20130430164333

 

哈哈,肯定有人迫不及待的想知道 QQ 的搜索结果,给大家看两条,关键字2那里就是 QQ 号码。同理,结合房间号,嗯嗯,哈哈。

20130430164645

 

还有网站浏览记录,这里只记录了域名,显然酒店员工黑夜就是去youku看看视频。

20130430203804

 

看看电子邮件的记录,2221 这个客房的客人真是发了好多啊,应该是给公司同事群发的。

20130430203144

 

FTP 的日志,关键字1就是 IP,关键字2就是用户名

20130430203503

 

还有个系统用户日志查询,记录的还挺详细。

20130430202922

 

20130430202650

 

管理界面里还有系统设置,我们再去看看。有个 VIP 主机的设置,应该就是设置不纳入计费范围的,反正我是把自己电脑的 MAC 地址填入这里,就直接上网了,当然了,这个 MAC 地址是伪造的啦。再没有弹出需要密码的页面,至于监控与否就不清楚了。

20130430165554

 

 

这里可以设置哪些 IP 段需要认证,我把起始IP修改到了 192.168.50.3,把自己电脑的 IP 设置为 192.168.50.2,好像是不能上网的。

20130430165956

 

 

有一个关键的设置,一定是大家很感兴趣的,就是这些信息被传送到什么地方呢?答案就在这里,有兴趣的朋友可以去研究研究哈。

20130430170257

 

呸,网络神探

20130430170815

 

还有一个设置应该是配合网络神探使用的,估计要是我修改了这里的设置,我用自己的电脑就能监控整个酒店的网络了,嗯,或许吧。

20130430204216

 

大家可能感兴趣的内容大概就这些吧,记住了,在酒店上网一定要开启 VPN。建议使用 OpenVPN,如果 OpenVPN 的服务器端口是 80 就更好了。

强烈建议大家禁止系统中的 CNNIC 的根证书,在酒店的这个网络环境中,很容易实现中间人攻击。

另外,酒店应该不会记录很详细的网络内容,因为这个会占用大量的存储空间,除非触发了敏感词。平平和安安要求保留至少90天的记录,如果记录下详细的内容,可能会占用大量的存储空间。

 

这一次的 QClub 活动是我的老东家山西鑫慧林赞助的,本来打算邀请两位讲师,后来一位来自北京的讲师因为时间和行程问题,最终只邀请到 Tinyfool 一人。不过 Tinyfool 还是很给力的,一口气说了一个半小时,最后意犹未尽的结束演讲。微博上也有朋友戏言,太原一时伞贵啊 http://weibo.com/1400229064/z1najn7yL

这次活动到场将近40人,其中有几位山大的学生,当然还是出现了一些新面孔。本来预定的是一楼的100人会场,但是因为会场工作人员的失误,让我们和另外一个会议冲突了,于是就把我们安排在三楼的大会议室,结果诺大的会议室坐着3、40人显得空空荡荡。在照片中看到人不多,细数下来也将近40人。

Tinyfool 演讲完毕后,赞助商买了不少的啤酒,给 Tinyfool 单独准备了一瓶竹叶青。大家一起喝酒聊天,聊的是不亦乐乎。

Tinyfool 的微博上也分享了他的首次太原之旅 http://weibo.com/1400229064/z1Z5lykVU

 

17. May 2012 · Write a comment · Categories: 技术文章 · Tags:

(Source: http://sw1nn.com/blog/2012/04/11/clojure-stm-what-why-how/

Clojure 有很多功能可以帮助在程序中处理并发问题。本文将关注软件事务内存(Software Transactional Memory,缩写 STM),不过这些并发问题的共性都是由多个部分共同运行来得以解决,因此我们期待一些其他思想的渗入……

STM 是什么?

软件事务内存 (STM) 是一种模拟数据库事务的并发控制机制来控制在并行计算时对共享内存的访问控制。它是锁的一种替代机制。

维基百科 – http://zh.wikipedia.org/wiki/软件事务内存

STM 允许在内存中更新多个数据,这些变更对于其他同时在执行相同逻辑的线程来说,显而易见是原子性的。类似数据库中的事务,如果因为某些原因有些更新没有完成,然后就会取消所有更新操作。

一个典型例子就是银行的交易 – 你希望把你的账户中的一些钱转账到我的账户……,我们将使用这个作为我们的例子,因为从概念上来说大家都熟悉。

目前在计算机科学中,STM 的实现还不是一个已经解决的问题,正在进行的研究是怎样更好的完成它。Clojure 选择了一个基于多版本并发控制(MVCC)的方法,MVCC在一个事务中维护多个(逻辑)版本数据的引用。在你的事务执行过程中,你看到的是一份数据的快照,与事务开始时看到的数据是一样的。当你认为在Clojure中普遍使用持久性数据完成你的很多事情,而不需要做太多额外的工作时,MVCC就是一个显而易见的选择。

为什么我们需要 STM?

STM 的核心是 ref 和 dosync,让我们看一个例子……

(def account1 (ref 100))
(def account2 (ref 0))

; to read the current value of a ref, use (deref refname):
;=> (deref account1)
100
;=> @account1 ; @refname is equivalent to (deref refname)
100

(defn transfer [amount from to]
    (dosync
       (alter from - amount)   ; alter from => (- @from amount)
       (alter to   + amount))) ; alter to   => (+ @to amount)

;=> @account1
100
;=> @account2
0
;=> (transfer 100 account1 account2)
100
;=> @account1
0
;=> @account2
100

你们看到我们在这里定义了两个账户,第一个账户 account1 初始化了 100。(transfer) 函数接收一个数量参数amount,一个来源账户 from 和一个目标账户 to,然后使用 alter 修改两个账户,从 from 账户中减去 amount 的数量,添加同样的 amount 数量给 to 账户。这段代码运行在一个单线程中,但是考虑一下,如果还有一个线程在两个 alter 语句之间运行,修改了两个账户中的值,这会发生什么呢?如果没有 STM 的保护,就很容易丢掉其中一个或者两个账户的变更。

另外一个例子;假设在一天结束时,银行希望给所有账户生成一份结算报表。可能这份报表需要很长时间的运行,但是在报表的生成过程中,依然可以对账户交易,并且为了报表目的,我们应该看到的是一致的数据视图。

在 Clojure 中 STM 是怎样工作的

看下面的这张图,你会看到粉色盒子中的三个事务,你也会看到最左边一列中ref值的不同版本。当一个事务通过 (dosync) 开始后,就会获取到ref的版本。获取到的值是在事务执行过程中 (deref) 返回的值(也就是说在事务中读取的ref都是不变的)。

让我们看看最左面的两个事务,两个事务同时开始,同时获取到了相同的 ref 值,因此两个获得的都是 ref 版本为 0 的一份副本。在事务中会执行一些操作,ref 的值将会被更新。第一个(最左面)的事务首先结束,所以赢得了比赛,把 ref 更新为新值。当第二个事务结束时,它尝试写入它的值,但是写失败了(图例中的红色箭头),因为 ref 的版本不是预期的。在这个例子中,事务就重新执行了。注意当事务重新执行时,它首先获得了 ref 新版本的副本,也就是看到的是第一个事务中变更后的值。然后由于没有其他事务尝试更新 ref,这次第二个事务就完成了。

你也看到这一切在进行的同时,第三个事务一直在运行,但是事务的处理过程中没有更新 ref 的值,所以不需要重试事务操作,事务运行完成。

如果保存在 ref 中的值是持久化的数据结构,在内存中保存这些数据结构的多个逻辑版本是很有效率的,因为这些数据结构会共享内部结构。当然,也会使用额外的资源,在一些场景中可能也会出现问题。当定义 ref 时,你还有一些选项来决定在运行时如何管理这些 ref 的历史(也就是上面讨论的这些版本数字)。

; pre-allocate history and limit max-history
(def myref (ref 1 :min-history 5 :max-history: 10))

; supply a function to validate the ref before commit.
(def myvalidref (ref 1 :validator pos?))

这个可以让你通过使用预分配和限制历史资源的使用,来提高在读取-失败中的边界效率。

宽松的一致性和副作用

在一些并发的事例中,你可以放宽松一点,以获得一些的效率。举个例子,假设你要保留一天的交易日志。如果你知道最后的交易结果始终是正确的,你可能就不大关心这些交易的顺序。说实在的,如果你收到两笔分别是 ¥100和¥50的存款,你可能就不在乎它们是被记录为¥100然后¥50,还是¥50然后¥100。存款的两个事务是可交换的,Clojure 也提供了一个并发操作 (commute) 来完成这样的事情……

(defn log-deposit [account amount]
     (dosync
        (println "Depositing $" amount " into account, balance now: "
            (commute account + amount))))

(def myaccount (ref 0))

(log-deposit myaccount 100)
(log-deposit myaccount 50)

; (as good as) equivalent to 

(log-deposit myaccount 50)
(log-deposit myaccount 100)

需要注意 (commute) 的是,当函数调用时,它设置了ref在事务中的值,但是在提交的时候才会真正的做出修改,是通过再次运行传入给 commute 的函数和最新的 ref 值。 这意味着在你的事务中你计算的值可能不是最终提交给 ref 的值。这需要考虑的认真点,你要确保你的操作过程中不依赖最新的 ref 值。

最后,你可能会疑惑上面例子中的 (println),在事务重试的事件中会产生什么副作用呢?会发生一个很简单的副作用。对于上面的例子,这个可能是个不太重要的事情,就是日志将会不一致,但是数据的真实来源将会是正确的。

Clojure 也提供了一个宏,也就是 io!,这个允许你把代码标记为不允许运行在一个事务中。你可以用这个来保护你自己无意中在一个事务里面调用了有副作用的代码。

例如:

(defn log [s]
   (io!
      (println s)))

(log "Hello World") ; succeeds

(dosync (log "Hello World!")) ; throws IllegalStateException

要正确的在一个事务内部完成 IO,你最好从事务内部把消息扔给Agent,Clojure 中的 Agent 是和 STM 整合在一起的,这样当事务成功时就会发送消息,如果事务失败就会被丢弃。

可能很多人不知道,Emacs 里包含了一组对 GNU 网络工具包的包装。

多数的工具只是简单的包装,与直接执行命令差不多,但是增加了高亮。但是有一些增加了完整的 Emacs comint 的支持,比如 nslookup。补充一下,Comint 是 Emacs 的一个包,用于与外部程序进行交互。

另一个有用的特性是内建了 ffap(Find File At Point) 支持,也就是说如果使用的是下面列出的交互式网络工具,会判断当前的光标所在位置是主机名还是 IP 地址或者是自己默认的什么东西。

这些网络工具包是用 GNU 开发包编写的,所以 Windows 的用户还得自己去下载 for Win32 的版本。这里附上两个地址 GnuWinGNU Utils for Win32,大家各取所需。

下面列出了 Emacs 支持的工具,直接用 M-x 来调用。你也可以配置成自己喜欢的程序或者调整一下参数,配置的方法是 M-x customize-group RET net-utils RET

命令 描述
ifconfig 和 ipconfig 运行 ifconfig 或者 ipconfig
iwconfig 运行 iwconfig 工具
netstat 运行 netstat 工具
arp 运行 arp 工具
route 运行 route 工具
traceroute 运行 traceroute 工具
ping 运行 ping,但是在多数系统中,这个命令会无限制的执行下去; 可以去配置一下 ping-program-options
nslookup-host 以非交互模式运行 nslookup 。
nslookup 以交互模式运行 nslookup 。
dns-lookup-host 用 host 命令查询 IP 或者主机名的 DNS 信息。
run-dig and dig 以交互模式运行 dig 。
ftp 对 ftp 命令行工具的非常简单的包装。
smbclient andsmbclient-list-shares 运行 smbclient 或者列出主机的所有共享。
finger 运行 finger 工具
whois and whois-reverse-lookup 运行 whois 工具,但是会去尝试猜测正确的 WHOIS 服务器,你可以去调整一下 whois-server-tldwhois-server-list 或者把 whois-guess-server 设置为 nil


原文:http://www.masteringemacs.org/articles/2011/03/02/network-utilities-emacs/

最近发现把 MBP 的屏幕合上以后需要至少 30 秒才可以进入待机状态,以前就记得速度很快。Google 了一下,发现遇到类似问题的人也不少,基本上可以用以下几种办法来解决:

  • 打开 http://127.0.0.1:631/jobs/ 看看是不是有没完成或挂起的打印任务,清空。
  • 拔除外部设备,比如移动硬盘、闪存盘之类的。
  • 运行  Activity Monitor,看看是不是有程序耗费了比较多的资源,退出大程序。
  • 关闭 Safari 或其他Web浏览器
  • 退出正在观看的视频或正在播放音乐的 iTunes

我的情况就是第一种,有个没有完成的打印任务,删除之后待机速度就正常了。