搭建VPN中转服务器

家里用的垃圾的宽带通服务,宽带质量实在没法恭维。晚上高峰时期下载速度估计只有1Mbps,然而打广告时却标榜100Mbps,虽然有心理准备根本达不到这么高的带宽,可谁曾想到会如此之慢。不过,这些都忍了,最让人无法接受的是这破宽带竟然连不到linode主机上。。我试过在其他运营商的宽带下是可以访问的。

最终,在暂时不换运营商的前提下,我找了一台国内中转服务器,使用haproxy来转发流量连接到linode。然而这种方式电脑用起来挺方便的,但是对iPhone等移动设备却不那么友好,众所周知,iPhone**的最好方法是使用VPN。为了手上的iPhone也能利用上我花钱买的两台主机,最近研究了一下使用国内中转服务器来转发VPN至海外服务器。

下面我讲一下主要思路,具体的步骤可以参考引文。

这个搭建过程主要有以下几个步骤:

  • 在国内主机上安装OpenVPN服务端
  • 在国内主机上安装ShadowVPN客户端
  • 在国外主机上安装ShadowVPN服务端
  • 在国内主机上将OpenVPN接受的流量转发到ShadowVPN
  • 最后就是在苹果设备上使用OpenVPN客户端来连接国内主机了(当然电脑也可以安装相关软件连接)

附:ShadowVPN启动和停止时的脚本

启动:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#!/bin/sh

# shadow routing table is for shadowVPN
# echo "200 shadow" >> /etc/iproute2/rt_tables

# tun1 is shadowVPN interface
ip route add default dev tun1

# specify which ip range to use this routing table, 10.8.0.0/24 is the source ip range for openVPN
ip rule add from 10.8.0.0/24 table shadow

# use SNAT to change ip packets' source ip address, 10.7.0.2 is shadowVPN's ip
iptables -t nat -A POSTROUTING -s 10.8.0.0/24 -j SNAT --to-source 10.7.0.2

# flush routing cache
ip route flush cache

# start shadowVPN process
shadowvpn -c /etc/shadowvpn/client.conf -s start

停止:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/bin/sh

# shadow routing table is for shadowVPN
# echo "200 shadow" >> /etc/iproute2/rt_tables

# tun1 is shadowVPN interface
# delete interface
ip route del default dev tun1

# specify which ip range to use this routing table, 10.8.0.0/24 is the source ip range for openVPN
# delete rule
ip rule del from 10.8.0.0/24 table shadow

# use SNAT to change ip packets' source ip address, 10.7.0.2 is shadowVPN's ip
# remove rule
iptables -t nat -D POSTROUTING -s 10.8.0.0/24 -j SNAT --to-source 10.7.0.2

# flush routing cache
ip route flush cache

# stop shadowVPN process
shadowvpn -c /etc/shadowvpn/client.conf -s stop

References:

browserify和webpack

browserify最核心的思想是让node代码也在能浏览器端执行,核心思想之外的东西则全都交给了transformplug-in。而webpack则把一部分前端自动化中的步骤,如代码压缩、代码打包,整合到了wepback的核心思想中。打个比方,用browserify就像在一片空地上造房子,而用webpack就相当于在已经有了房屋的架子的基础上进行修修改改。

browserify天然只支持CommonJS的模块加载格式,而webpack对CommonJS、AMD加载格式都天然支持。当然,browserify可以通过使用插件(如deAMDify)来支持AMD的写法。

browserify原生不支持代码拆分(Code Splitting),而webpack支持,这也是它的一大特点。但是browserify通过使用插件(如Factor-Bundle),也可以支持代码拆分。

webpack的社区流行度似乎更高,vuejs、react似乎都倾向于使用webpack作为它们的模块加载工具。

browserify只适用于js资源,而webpack除了js资源,还能作用于css、图片等资源。

browserify通常需要和grunt、gulp一起配合使用(比如必须要使用它们来处理css),但webpack基本不需要。

webpack支持热替换(Hot Module Replacement),无须刷新页面就能实现页面更新(这跟自动刷新还是有区别的)。如果使用ReactJS话,react-hot-loader很赞,但browserify通过插件(livereactload)也能支持。

browserify和webpack和grunt一样都是配置式的,跟gulp不同,但很多观点认为配置式并无不好之处,很多时候只是不同人有不同的喜好而已。

webpack在开发的时候能实现自动编译只发生过更改的模块,速度回非常快,browserify通过插件也能实现类似功能。

References:

一个webpack的煎蛋demo

从各方面的消息能明显感觉到大家对webpack的评价很高,webpack是个配置很灵活的功能强大的前端工程自动化的工具,但是webpack有个让很多人都感受的到的缺陷就是它的文档实在是不能说写得很好。很多初学者都不知道怎么开始使用webpack,webpack的文档的严重影响了它的普及。

下面我就简单讲一个使用webpack的例子,让大家对webpack有个初步的感知。

创建新工程

毫无疑问,我们使用git来进行代码的版本管理。

1
2
3
4
5
mkdir webpack-example
cd webpack-example
echo 'a webpack example' > README.md
git init
touch .gitignore

首先我们新建一个文件夹作为工程目录,创建README.md文件,并初始化git配置,同时可以按情况创建.gitignore文件

初始化npm

近期比较流行的一种前端技术栈是

  • 使用npm来管理依赖
  • 使用babel来让我们使用ES6/ES7语法
  • 使用webpack来处理文件之间的相互依赖关系和代码的编译

所以接下来我们首先需要初始化npm,将所有依赖的模块都引入。

1
npm init

执行上述命令,按照实际情况输入,完成之后就会有个初始化的package.json文件。

引入babel依赖

使用最新的javascript规范的语法来书写我们的代码能提升我们的开发体验,但是现在浏览器对新规范的支持情况又让开发者对这些令人着迷的新特性望而却步,鉴于这样一种矛盾的情况,babel就应运而生了。

babel可以和很多其他的技术栈结合应用,从这个setup页就能可见一斑。本文讨论的情况是babel和webpack的结合运用。

我们需要引入babel的三个模块:babel-corebabel-loaderbabel-preset-es2015

1
npm install --save-dev babel-core babel-loader babel-preset-es2015

babel-core是babel的核心代码,babel-loader是当babel和webpack结合使用时需要载入的一个包,然而只有这两个,babel并不会为你编译转换代码,你还需要引入一个preset

所谓preset,可以看成语法规范,引入之后就能支持对符合引入规范的js语法进行编译转换。babel支持的所有preset可以去这里了解。

引入webpack依赖

babel已经引入,接下来引入webpack

1
npm install --save-dev webpack

安装好webpack之后,最后的重头戏就是webpack的配置了,从这里开始,webpack的灵活性就体现出来了,配置方式五花八门,可以根据项目的实际情况任你挑选。本文会对一个简单的多入口文件的例子进行配置。

配置可以直接作为webpack的参数传入,但是更好的方式是使用配置文件。

配置webpack

假设项目的文件结构如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
webpack-example
├── build
├── js
│ ├── pageA.js
│ ├── pageB.js
│ ├── shared.js
│ └── shared2.js
├── html
│ ├── pageA.html
│ └── pageB.html
├── .gitignore
├── package.json
├── README.md
└── webpack.config.js

js目录下的pageA.jspageB.js是两个入口文件,会被两个页面pageA.htmlpageB.html分别引用。pageA.js依赖shared.jsshared2.js两个js文件,pageB.js依赖shared.js文件。

在这个背景下,webpack要做的就是将所有js文件进行打包,最终形成两个html页面需要引入的打包后的js文件。为此,webpack.config.js可以这么写

webpack.config.js
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
var path = require('path');
var AssetsPlugin = require('assets-webpack-plugin');
var assetsPluginInstance = new AssetsPlugin({
path: path.join(__dirname, 'build')
});

module.exports = {
// 指定入口文件
entry: {
pageA: './js/pageA.js',
pageB: './js/pageB.js',
},
// 输出配置
output: {
path: path.join(__dirname, 'build/js/'),
filename: '[name].bundle.[chunkhash].js',
chunkFilename: '[id].chunk.[chunkhash].js',
},
// 模拟代码中出现的node相关的模块(不写的话,在引入node自身的模块时会引起报错)
node: {
path: 'empty',
fs: 'empty'
},
module: {
loaders: [
{
test: /\.js$/,
exclude: /node_modules/,
// 指定loader为babel
loader: 'babel',
query: {
// 指定preset
presets: ['es2015']
},
},
]
},
// 引入额外插件
plugins: [assetsPluginInstance]
};

可以发现我们引入了一个叫assets-webpack-plugin的插件,所以我们得执行

1
npm install --save-dev assets-webpack-plugin

这个插件的作用是把编译前的文件名和编译后的文件名对应起来形成一个映射对象,并写入文件中。

各个js文件的内容如下

pageA.js
1
2
3
4
require(["./shared", "./shared2"], function(shared, shared2) {
shared("This is page A");
shared2("This is page A");
});
pageB.js
1
2
3
require(["./shared"], function(shared) {
shared("This is page B");
});
shared.js
1
2
3
module.exports = function(msg) {
console.log(1, msg);
};
shared2.js
1
2
3
module.exports = function(msg) {
console.log(2, msg);
};

接着我们执行

1
./node_modules/.bin/webpack webpack.config.js

就能在build目录下看到编译后的相关文件

1
2
3
4
5
6
7
8
build
├── js
│ ├── 2.chunk.4217cd33df3cca54e202.js
│ ├── 4.chunk.d2804f94e850044225d1.js
│ ├── main.bundle.25c68114fd6604d4f2d5.js
│ ├── pageA.bundle.14aba2165c3c2e4ffb06.js
│ └── pageB.bunlde.b07d4b8d5c9da565424a.js
└── webpack.assets.json

结束语

上面就是一个简单的demo尝试,然而,我并没有将如何将这些带hash值的文件名嵌入到html文件中去。简单说一下两个思路,一个是动态模板的思路,因为assets-webpack-plugin会生成一个资源映射文件,所以只要在动态模板生成的时候用函数调取这个映射文件匹配资源从而动态生成资源名然后嵌入至html中;另一个思路是想办法解析html然后自动替换相应资源名,这个思路如果用gulp来实现已经就有现成的插件了,但是我希望的是尽量不用gulp而用纯粹的webpack来实现,为了实现这个目标,目前看来只能自己写loader或plugin了。

关于在Javascript代码块中写函数申明

最近在看ECMA-262-3 in detail. Chapter 5. Functions.中对一个问题的剖析时突然回想起之前在工作开发中遇到的一个诡异的问题。问题代码大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
vm.save = function () {

if ($scope.form.$valid) {
var diff = diffToHistory(vm.queryObj);

function submit() {
// ...
}

if (diff.length) {
// ...
} else {
submit();
}
}
};

这段代码我在Chrome中测试的时候发现没有什么问题,然后在Safari下却报了一个ES5语法相关的错误。当时觉得特别纳闷,如果是明显的语法错误,为什么Chrome不报错呢?

看了上面那篇文章,才算明白了。总结一下就是,对于某个特定的语法,不同浏览器对待的方式不一样

实际上,根据规范,函数申明只能写在全局下或者是另一个函数块下,而不能写在其它普通的代码块下,如if代码块、for代码块等,如果出现这种情况,应该视为语法错误。然而以前的(包括现在的)很多浏览器具体实现时并不认为这是错误,而是把它视为正确的语法。

上面说的Safari报错,至少说明那个版本的Safari对规范遵循地比较严格,将这种规范中定义为错误的情况如实地视为错误。

再举个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if (true) {

function foo() {
alert(0);
}

} else {

function foo() {
alert(1);
}

}

foo(); // 1 or 0 ? test in different implementations

这种情况下,因为规范并没有说明(它已经将这种认定为语法错误了啊!!),所以不同浏览器就可能会有各自不同的处理方式了,有的直接报错(如上面的Safari),有的会用第二个申明覆盖第一个,有的还会有其他处理方式。

因此,最为保险的写法就是按照规范来,不要把函数申明写到if代码块、for代码块等代码块下。

References:

关于Javascript的function

三种function:

  • 函数申明(Function Declaration)
  • 函数表达式(Function Expression)
  • 用函数构造器创建的函数(Function created via Function constructor)

函数申明

有以下特点:

  • 必须要指定函数名
  • 可以在全局或另一个函数内部定义(按照规范,在另一个函数内部时必须是在最外层的函数块(block)下定义,不能在其他块下定义,如if块)
  • 在进入上下文阶段(entering the context stage)创建(执行一个函数有两个阶段:1.进入上下文阶段,2.代码执行阶段)
  • 会影响变量对象(variable object)

函数表达式

有以下特点:

  • 处于表达式的位置(js解析器认为是表达式的各种情况下的位置)
  • 函数名可有可无
  • 对变量对象没有影响
  • 在代码执行阶段(code execution stage)创建

用函数构造器创建的函数

有以下特点:

1
2
3
4
5
6
7
8
9
10
11
12
var x = 10;

function foo() {

var x = 20;
var y = 30;

var bar = new Function('alert(x); alert(y);');

bar(); // 10, "y" is not defined

}

上面用Function定义的函数,它的原型链上只有window对象

1
2
3
4
5
var a = [];

for (var k = 0; k < 100; k++) {
a[k] = function () {}; // possibly, joined objects are used
}

如果浏览器有优化的话,上面a中每个元素都会共同使用一个函数,大大减少了函数消耗(但每个函数自身的数据还是独立的,比如如果有闭包的时候),对比之下,下面用Function创建的函数则会给每个元素都创建一个完全独立的函数。

1
2
3
4
5
var a = [];

for (var k = 0; k < 100; k++) {
a[k] = Function(''); // always 100 different funcitons
}

References:

LeetCode中的Single Number III这道题的思路

LeetCode中有道题,我觉得还蛮有意思的,摘录过来和大家分享一下思路


Given an array of numbers nums, in which exactly two elements appear only once and all the other elements appear exactly twice. Find the two elements that appear only once.

For example:

Given nums = [1, 2, 1, 3, 2, 5], return [3, 5].

Note:
The order of the result is not important. So in the above example, [5, 3] is also correct.
Your algorithm should run in linear runtime complexity. Could you implement it using only constant space complexity?

大致意思是有一列数,其中除了两个不同的数以外,其他数都是成双成对的,现在目标是需要找出这两个特别的数,并且要求算法的达到线性时间复杂度和常数空间复杂度。

乍看之下似乎不太可能,但是知道解法之后我们就会发现,其实很多信息以及蕴藏在数字自身当中,只是我们没有利用起来。而这些所谓的信息,其实就是这些数字的二进制形式中包含的一些特点。

下面是我用js实现的解法

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
/**
* @param {number[]} nums
* @return {number[]}
*/

var singleNumber = function(nums) {
var xor = nums[0];
for (var i = 1; i < nums.length; i++) {
xor ^= nums[i];
}

// choose one number to indicate which bit of the two desired numbers are different
var diffBitMarker = 1;
while ((diffBitMarker & xor) !== diffBitMarker) {
diffBitMarker = diffBitMarker << 1;
}

var target1 = 0;
// choose number that has bit 1 at the certain position specified by diffBitMarker
for (var j = 0; j < nums.length; j++) {
if ((diffBitMarker & nums[j]) === diffBitMarker) {
target1 ^= nums[j];
}
}

target2 = xor ^ target1;

return [target1, target2];
};

主要提两个要点:

  1. 相同的数字经过XOR运算之后的值为0
  2. 两个不同的数字,它们的二进制值必然至少有一位上的数值一个是0,一个是1,依据这个可以把这一列数按这个标准分为两堆,使得这两个数分别位于两堆数中,这样,每一堆数中,就只有唯一一个“落单”的数了,根据上面的要点1,就能把这个数揪出来。

另外值得一提的是我在应用Javascript的位操作符时遇到操作符优先级的问题,Bitwise XOR(其他如Bitwise AND、Bitwise OR、Logical AND、Logical OR)的操作优先级均比比较运算符低,因此 (diffBitMarker & xor) !== diffBitMarker 这语句里的括号是不能缺省的,否则实际的执行顺序就不是我们所期望的了。

使用tcpdump来发现受Xcode Ghost感染的App

昨天一条微博引发了国内安全界的巨震,微博里透露说对国内若干个知名的IOS App的网络流量抓包的时候发现了一个不明网址,这些App会向这个叫init.icloud-analysis.com的不明网址发送当前App的一些信息,并且有潜在地可能钓鱼窃取用户的iCloud账号密码。

而这些App有个共同点是都使用了一份被修改并注入恶意代码的Xcode开发工具所开发,这些被注入的Xcode的安装包并非是由官网下载,而是通过百度云盘或迅雷下载而来。因为官网源在国内的下载速度非常慢,所以很多开发者,甚至是这些大公司的开发者们也会去国内的这些网盘下载Xcode安装包。更多具体信息大家去网上一搜就是一大把,我这就不多说了。

那我要在这篇文章里说的就是怎么去抓包检测你当前的手机里的App是否“中毒”了。用什么工具呢?其实就是之前在我的博文里提到的tcpdump。主要的思路就是自建wifi网络,用手机去连这个自建的网络,然后在电脑里抓包分析某个App是否有向那个不明网站发送数据。

如何在mac osx上自建wifi网络?具体的我也不说了,大致的做法就是先在右上角的wifi图标点击后的下拉列表下选择 Create Network,然后在 System Preferences > Sharing 下的 Internet Sharing 里选择将 Thunderbolt Bridge共享,下面的选项里选wifi,也就是说将你电脑的有线网络分享给连接wifi的设备。

然后用手机连上刚才创建的wifi,先把所有app进程杀掉。

在命令行里输入

1
sudo tcpdump -i any -lvvvA port 53 | grep icloud-analysis

这个时候,就可以开始在手机上测试App了,比如首先点开“网易云音乐”,不一会儿,你就会看到相关的请求数据包出现了

1
2
3
4
5
6
7
8
9
10
11
12
    192.168.2.3.61037 > 192.168.2.1.domain: [udp sum ok] 44733+ A? init.icloud-analysis.com. (42)
..........m.5.2...............init.icloud-analysis.com.....
192.168.2.3.61037 > 192.168.2.1.domain: [udp sum ok] 44733+ A? init.icloud-analysis.com. (42)
..........m.5.2...............init.icloud-analysis.com.....
192.168.2.3.61037 > 192.168.2.1.domain: [udp sum ok] 44733+ A? init.icloud-analysis.com. (42)
b... d.c.[1/..E..F....@............m.5.2...............init.icloud-analysis.com.....
192.168.2.3.61037 > 192.168.2.1.domain: [udp sum ok] 44733+ A? init.icloud-analysis.com. (42)
b... d.c.[1/..E..F....@............m.5.2...............init.icloud-analysis.com.....
192.168.2.3.61037 > 192.168.2.1.domain: [udp sum ok] 44733+ A? init.icloud-analysis.com. (42)
b... d.c.[1/..E..F`n..@............m.5.2...............init.icloud-analysis.com.....
192.168.2.3.61037 > 192.168.2.1.domain: [udp sum ok] 44733+ A? init.icloud-analysis.com. (42)
b... d.c.[1/..E..F`n..@............m.5.2...............init.icloud-analysis.com.....

这些其实是init.icloud-analysis.com的DNS域名解析请求,有域名解析请求意味着代码中含有对改地址的请求。

打开一个App,如果有类似上面的请求,说明中招了,没有的话则没问题。测完一个之后,杀掉进程,再开启另外一个App测。

tcpdump入门用法举例

我们在平时的开发过程中会遇到很多网络相关的问题,这些情况下如若能正确地使用tcpdump指令,找到问题症结的效率可能就会大大提高。

tcpdump是linux下比较常用的一项用来抓取分析网络流量包的命令。相比wireshark等一些可视化的流量抓包工具,tcpdump因其极大的可定制性而深受一些高级的网络分析工程师的喜欢。

初步理解tcpdump主要在于理解两个方面,一个是tcpdump的各种附加参数的使用方法,另一个是对tcpdump输出的理解。

tcpdump基础用法举例

tcpdump命令在使用时,后面可以跟两部分东西,一部分是各种选项的指定(-v,-i等等),另一部分是过滤器(filters)的指定(例如host 105.111.111.111)下面给了一些常用的例子

1
2
3
4
5
6
7
8
9
10
# 不要把地址和端口转成名字,verbose
tcpdump -nv
# 不要把地址和端口转成名字,very verbose
tcpdump -nvv
# 只抓取网络接口eth0的流量
tcpdump -i eth0
# 将抓取的结果同时用hex和ascii的方式展示
tcpdump -XX
# 将抓取的结果同时只用ascii的方式展示(比较实用)
tcpdump -A
1
2
3
4
5
6
# 只抓取本机和105.111.111.111之间的流量
tcpdump -nv host 105.111.111.111
# 只抓取端口22的流量
tcpdump -nvv port 22
# 只抓取本机和105.111.111.111的22端口之间的流量
tcpdump -nv 'host 105.111.111.111 and port 22'

tcpdump结果的格式说明

这是一条tcpdump返回的结果

Read More

chmod之setuid和setgid

你可能见过下面这句命令

1
chmod 6555 htop

与通常chmod带上三个数字的用法相比,这句多了个数字。

其实三个数字的形式默认是省略了第一个数字0

1
2
3
chmod 644 htop
# is the same as
chmod 0644 htop

这四个数字中的第一位是用来设置setuid、setgid以及sticky bit用的

setuid是针对owner的,setgid是针对group member的,当赋予了setuid和setgid后,文件或文件夹相应掩码的x位就会变成s

1
-r-sr-sr-x  1 root  wheel  1111 Jan 24 2013 mtr

setuid和setgid被用于文件和文件夹时有着完全不同的含义

作用于文件时

对于文件来说,setuid和setgid只会对可执行文件起效果。它们的作用是,当可执行文件执行时,自动将该文件的owner和group的角色赋予给当前的操作者,也即当前的操作者自动拥有了该文件的owner权限和group权限,而无需关心当前操作者自己的owner角色和所属group。

比如有如下文件,不管是哪个用户去执行它,都是以root:wheel的身份去执行的

1
-r-sr-sr--  1 root  wheel  1111 Jan 24 2013 start.sh

作用于文件夹时

当setuid设置在文件夹上时,大多linux版本时无视的,所以一般来说,setgid作用于文件夹的情况比较多。当给某一文件夹设置setgid时,所有在该文件夹下创建的文件或文件夹将自动继承这个父文件夹的group角色。这个功能在组内成员协作的时候会比较有用,例如我在之前的一片博文在服务器上部署git仓库时的要点剖析里提到过setgid的应用。