ASUS 66u 搭建透明代理

安装软件

1
2
3
4
5
6
7
8
9
10
/usr/sbin/entware-setup.sh

opkg install bind-dig
opkg install shadowsocks-libev-ss-redir
opkg install shadowsocks-libev-ss-tunnel
opkg install libc libssp libev libmbedtls libpcre libpthread libsodium haveged zlib libopenssl
opkg install ipset4

//ac66u only
opkg install iptables

配置ss-redir服务

修改配置文件

  1. vim /opt/etc/init.d/S22shadowsocks
  2. ss-local改为ss-redir
  3. 添加启动参数-b 0.0.0.0
  4. ENABLED值改为 yes,变成开机启动

修改shadowsocks.json

修改服务器IP,端口,加密码方式。

配置dnsmasq

创建配置文件

  • vim /jffs/configs/dnsmasq.conf.add
  • 添加conf-dir=/opt/etc/dnsmasq.d

创建目录

  • mkdir /opt/etc/dnsmasq.d/

  • 下载规则文件

1
2
cd /opt/etc/dnsmasq.d/
curl -O https://cokebar.github.io/gfwlist2dnsmasq/dnsmasq_gfwlist.conf

配置ss-tunnel服务

ss-tunnel主要用于解决dns污染的问题,dnsmasq会将解析请求转发到ss-tunnel

创建开机启动脚本

  • 转到目录/jffs/scripts/
  • 创建名为比如start-ss-tunnel.sh文件,内容如下:
1
2
3
#!/bin/sh

/opt/bin/ss-tunnel -c /tmp/mnt/onmp/entware/etc/shadowsocks.json -l 5353 -L 8.8.8.8:53 -u > /dev/null 2>&1 &

创建service-start文件

1
chmod +x service-start

/jffs/scripts/start-ss-tunnel.sh 加到service-start

至此ss-tunnel就能开机启动了

配置iptables

下载chnroute.txt

1
wget -O- 'http://ftp.apnic.net/apnic/stats/apnic/delegated-apnic-latest' | awk -F\| '/CN\|ipv4/ { printf("%s/%d\n", $4, 32-log($5)/log(2)) }' > /jffs/configs/chnroute.txt

生成ipset集合

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/sh

wget -O- 'http://ftp.apnic.net/apnic/stats/apnic/delegated-apnic-latest' | awk -F\| '/CN\|ipv4/ { printf("%s/%d\n", $4, 32-log($5)/log(2)) }' > /jffs/configs/chnroute.txt

ipset -N chnroute nethash

for ip in $(cat '/jffs/configs/chnroute.txt'); do
ipset -A chnroute $ip
done


ipset --save > /jffs/configs/ipset.conf

因为chnroute.txt文件比较大,生成会比较慢,故将生成的ipset保存到ipset.conf中,每次启动时从ipset.conf中导入IP.

启动脚本

SS启动脚本ss-up.sh

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
#!/bin/sh

alias iptables='/opt/sbin/iptables'

ipset -R < /jffs/configs/ipset.conf

# ipset -N chnroute iphash

# for ip in $(cat '/jffs/scripts/chnroute.txt'); do
# ipset -A chnroute $ip
# done

iptables -t nat -N SHADOWSOCKS
iptables -t mangle -N SHADOWSOCKS

# 直连服务器 IP
iptables -t nat -A SHADOWSOCKS -d [SS-SERVER-IP]/24 -j RETURN

# 允许连接保留地址
iptables -t nat -A SHADOWSOCKS -d 0.0.0.0/8 -j RETURN
iptables -t nat -A SHADOWSOCKS -d 10.0.0.0/8 -j RETURN
iptables -t nat -A SHADOWSOCKS -d 127.0.0.0/8 -j RETURN
iptables -t nat -A SHADOWSOCKS -d 169.254.0.0/16 -j RETURN
iptables -t nat -A SHADOWSOCKS -d 172.16.0.0/12 -j RETURN
iptables -t nat -A SHADOWSOCKS -d 192.168.0.0/16 -j RETURN
iptables -t nat -A SHADOWSOCKS -d 224.0.0.0/4 -j RETURN
iptables -t nat -A SHADOWSOCKS -d 240.0.0.0/4 -j RETURN

# 直连中国 IP
iptables -t nat -A SHADOWSOCKS -p tcp -m set --match-set chnroute dst -j RETURN
iptables -t nat -A SHADOWSOCKS -p icmp -m set --match-set chnroute dst -j RETURN

# 重定向到 ss-redir 端口
iptables -t nat -A SHADOWSOCKS -p tcp -j REDIRECT --to-ports 1080
iptables -t nat -A SHADOWSOCKS -p udp -j REDIRECT --to-ports 1080
iptables -t nat -A OUTPUT -p tcp -j SHADOWSOCKS

# Apply the rules
iptables -t nat -A PREROUTING -p tcp -j SHADOWSOCKS
iptables -t mangle -A PREROUTING -j SHADOWSOCKS

将其中的SS-SERVER-IP修改成你的服务器地址

创建post-mount文件

1
chmod +x post-mount

ss-up.sh加入

1
2
#!/bin/sh
/jffs/scripts/ss-up.sh

经测试只有将启动脚本加入到post-mount中对能开机启动,可能的原因是ss-up.sh中设及到挂载分区的访问。

停止脚本

1
2
3
4
5
6
#!/bin/sh
iptables -t nat -D OUTPUT -p icmp -j SHADOWSOCKS
iptables -t nat -D OUTPUT -p tcp -j SHADOWSOCKS
iptables -t nat -F SHADOWSOCKS
iptables -t nat -X SHADOWSOCKS
ipset -X chnroute

参考

ShadowsocksX-NG实现白名单模式

ShadowsocksX-NG Proxy Auto Configure Mode使用的是黑名单模式,默认直连,符合规则的走代理。要实现白名单模式,默认代理,符合规则的直连,有一种hack方法:

  • 找到程序文件夹下的abp.js文件,路径一般为/Applications/ShadowsocksX-NG.app/Contents/Resources/abp.js

  • 修改FindProxyForURL方法,将direct,proxy对调,改成下面代码中的样子

    1
    2
    3
    4
    5
    6
    function FindProxyForURL(url, host) {
    if (defaultMatcher.matchesAny(url, host) instanceof BlockingFilter) {
    return direct;
    }
    return proxy;
    }
  • 重启程序

  • 修改程序preferences里的GFW List url,改成白名单规则
  • 然后点击Update PAC from GFW List更新pac文件即可

Groovy+Grails迁移到Kotlin+Spring Boot的一些研究

Kotlin是静态语言,可以运行在JVM上且与java百分百可互操作。自从Google将Kotlin正式做为Android开发的语言后,Kotlin的一下子就火了起来。Kotlin不仅可以做为Android的首选开发语言,而且它也非常适合服务端的开发。

为什么选择Kotlin

与Java百分百可互操作groovy-grails-to-kotlin-springbootgroovy-grails-to-kotlin-springboot

Kotlin可以使用JVM平台上的任何库与框架,Kotlin编写的代码也可以供Java调用

简洁

Kotlin的代码非常简洁:

变量

变量声明:

1
2
3
4
val a: Int = 1  // immediate assignment
val b = 2 // `Int` type is inferred
val c: Int // Type required when no initializer is provided
c = 3 // deferred assignment

可变变量:

1
2
var x = 5 // `Int` type is inferred
x += 1

可空值与不可空值:

1
2
val s: String? = null //May be null
val s2: String = "" //May not be null

方法

1
2
3
4
5
6
fun sum(a: Int, b: Int): Int {
return a + b
}

//Function with an expression body and inferred return type:
fun sum(a: Int, b: Int) = a + b

扩展函数

1
2
3
4
fun String.lastChar(): Char = this.get(this.length - 1)

>>> println("Kotlin".lastChar())
n

infix函数

1
2
3
infix fun Any.to(other: Any) = Pair(this, other)

val (number, name) = 1 to "one"

1
2
3
4
5
6
7
8
9
10
11
class Greeter(val name: String) {
fun greet() {
println("Hello, $name")
}
}

fun main(args: Array<String>) {
val greeter = Greeter("John")
greeter.greet()
println(greeter.name)
}

数据类(data class)

自带equals hashcode tostring copy等方法

1
data class User(val name: String, val age: Int)

DSL(Domain-specific language)

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
open class Tag(val name: String) {
private val children = mutableListOf<Tag>()

protected fun <T : Tag> doInit(child: T, init: T.() -> Unit) {
child.init()
children.add(child)
}

override fun toString() = "<$name>${children.joinToString("")}</$name>"
}

class TABLE : Tag("table") {
fun tr(init: TR.() -> Unit) = doInit(TR(), init)
}

class TR : Tag("tr") {
fun td(init: TD.() -> Unit) = doInit(TD(), init)
}

class TD : Tag("td")

fun table(init: TABLE.() -> Unit) = TABLE().apply(init)

fun createTable() = table {
tr {
td {

}
}
}

>>> println(createTable())
<table><tr><td></td></tr></table>

空安全

空安全(null safe)可以说是kotlin杀手级的特性了,它能排除一切空指针的可能,让你写出更安全的代码。

如下面这段代码会直接编译不通过:

1
2
var output: String
output = null // Compilation error
1
2
val name: String? = null    // Nullable type
println(name.length()) // Compilation error

自动类型转换:

1
2
3
4
fun calculateTotal(obj: Any) {
if (obj is Invoice)
obj.calculateTotal()
}

静态类型(Statically Typed)

动态语言是一把双刃剑,groovy的代码非常简洁,弥补了java语言太啰嗦的缺点,同时groovy的闭包也非常好,正因为如此我们选择了groovy。但时做为动态语言,许多行为是在代码运行时才会确定,编译错误在写代码时无法检测到,除非加上@TypeChecked注解。

Kotlin的代码非常简洁,更重要的是它是静态类型语言,能检测到编译错误,减少了代码中潜在的问题。

为什么选择Spring Boot

  • 从2015年初开始,Sring Boot的热度就超过了Grails,可参考:Grails && Spring Boot Stack Overflow Trends
  • Grails 2.x版本有上千个插件,但升级到Grails 3.x之后,这些插件就不再被支持,更糟糕的是没有人将这些插件迁移到Grails 3.x,Grails 3.x现在只有244个插件。
  • 尽管Grails 3是基于Spring Boot的,但其自身也带来了些奇怪的bug, 有时候解决框架bug的时间甚至超过了使用框架节省的时间。

Kotlin+Spring Boot的一些不足

没有Gorm查询

Grails框架的最大亮点是Gorm:动态方法和DSL查询:

1
2
3
4
5
6
7
8
9
10
11
12
13
//will be translated to hql automatically
def person = Person.findByFirstName('xxx')
equals:
"FROM Person WHERE firstname = ?"

Person.withCretia {
or {
ilike('name','%j%')
gte('age',22)
}
}
equals:
"FROM person WHERE name ilike ? OR age >= ?"

在Spring Boot中你只能自己写查询语句或者使用类似spring data jpa query dsl插件:

1
2
3
4
5
6
internal interface TodoRepository : Repository<Todo, Long> {

fun findFirst3ByTitleOrderByTitleAsc(title: String): List<Todo>

fun findTop3ByTitleOrderByTitleAsc(title: String): List<Todo>
}

数据库迁移(Database Migration)

Grails 的database migration在数据库表结构和数据迁移两方面都非常强,它可以对比项目中的实体和数据库表来自动生成表结构修改语句,同时你可以写sql来完成数据迁移。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
changeSet(author: "xxx (generated)", id: "1521527665533-1") {
addColumn(tableName: "staff") {
column(name: "date_created", type: "TIMESTAMP WITHOUT TIME ZONE") {
constraints(nullable: "true")
}
}
}

changeSet(author: "xxxx", id: "1521528248") {
grailsChange {
change {
sql.execute('''
UPDATE staff
SET date_created = now()
''')
}
}
}

而在Spring Boot方面,数据库迁移插件有flyway和liquibase,两者各有千秋,flyway在数据迁移上比较强,但缺少自动生成表结构修改语句功能。liquibase正好相反,它有自动生成表结构修改语句功能,但在写数据迁移sql方面比较弱。

表单校验

在自定义校验方法上Grails非常方便,只需要一个闭句就可以了:

1
2
3
4
5
6
7
8
9
10
11
12
13
class User {
String name
static constraints = {
name unique: true, validator: { val, obj ->

if (!val.matches('[A-Z0-9-]+')) {
return 'acronym.format.error'
}

true
}
}
}

而在Spring Boot方面比如用hiberate validator你需要定义类,定义注解,写很多代码来实现自定义校验。

总结

在项目开发速度上,Groovy+Grails上占优,如果时间比较紧的话,Groovy+Grails会是一个不错的选择,它能在较短时间内做出项目。如果你需要一个bug少,更稳定的项目或者你的项目用不到Gorm的话,你可以选择Kotlin+Spring Boot.

简而言之,100分做为满分的话,Groovy+Grails能在最短的时间内达到80分,而要达到90甚至95分的话,选择Kotlin+Spring Boot无疑。

Amazon API Gateway 踩坑记

最近项目中用到了Amazon API Gateway,当中遇到了不少的坑。本文主要罗列了在使用amazon api gateway中遇的问题,希望能帮助到遇到相同问题的开发者。

不支持parmeters array

1
GET /user/1/addresses?addressId=1&addressId=2

服务端接收到的只有一个值,解决办法passing-array-query-parameters-with-api-gateway-to-lambda

POST/PUT 返回 406

当使用json body发送put/post请求时,api gateway有一定机率返回406,查了半天也找出什么原国,后来在stackoverflow上找到了答案,需要在Integration Request中添加Accept头,并且设置其值为空字符串。具体什么原因估计只有AWS api gateway的开发者才知道了。
aws-api-gateway-returns-http-406

URL Query String Parameter、header需要显示设定

如果不在Method Request里显示声名URL Query String Parameter的话,请求参数是不会转发到后台的,request header同理。

Grails参数绑定不支持application/x-amz-json-1.0/application/x-amz-json-1.0 content type.

请求转发到后台时,API gateway会将application/json转换成application/x-amz-json-1.0/application/x-amz-json-1.0,需要修改代码,添加对这两个header的支持,不然会无法绑定参数。

Java 浮点数存储方式

Java浮点数使用的是IEEE二进制浮点数算术标准。

浮点数分为符号,指数,分数三部分。以32位浮点数为例,分为1符号位+8指数位+23分数位。

1(bit) 8(bit) 23(bit)
符号 指数 分数

以173.7为例:

173.7为正数,故符号位为0

0 00000000 00000000000000000000000

将其转换成1.??? * 2^?形式

1
2
3
4
5
6
7
173.7/2=86.85            2^1
86.85/2=43.425 2^2
43.425/2=21.7125 2^3
21.7125/2=10.85625 2^4
10.85625/2=5.428125 2^5
5.428125/2=2.7140625 2^6
2.7140625/2=1.35703125 2^7

最终格式为1.35703125*2^7 ,指数位的值为7,由于指数部分是由Bias偏移量来表示的,float的偏移量为Bias=2^k-1 -1=2^8-1 -1=127,做指数部分的值为127+7=134,134的二进制值为 1000 0110,则符号位+指数为存储格式如下:

0 1000 0110 00000000000000000000000

计算尾数部分,由于浮点数最终都是以1.??? * 2^?的形式保存的,小数点左边永远是1,所以我们只需关心小数点后面部分

Number X2 1/0
0.35703125 0.7140625 0
0.7140625 1.428125 1
0.428125 0.85625 0
0.85625 1.7125 1
0.7125 1.425 1
0.425 0.85 0
0.85 1.7 1
0.7 1.4 1
0.4 0.8 0
0.8 1.6 1
0.6 1.2 1
0.2 0.4 0
0.4 0.8 0

由于到了0.4这一步这边是会个循环,0110 0110 …
加上尾数部分,最终存储格式如下:
0 1000 0110 01011011011001100110011

我们可以通过Float.floatToIntBits(173.7)来验证结果:

1
2
3
Float.floatToIntBits(173.7) = 1127068467
1127068467用二进制表示,就是
0 10000110 01011011011001100110011

Grails 3.2.11 Bugs

  • Functional Testing setup无法回滚
1
2
3
4
5
6
7
8
9
10
class BaseFunctionalTest extends GebSpec {
def setup() {
Dog dog = new Dog(name:'xxxx')
}

//需要手动删除
def cleanup() {
Dog.findByName('xxxx')?.delete()
}
}
  • JSON Views循环渲染bug

如果多个GORM实体存在循环引用,则会产生stackover flow异常,如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
Class A {
static hasMany = [children: B]
}

Class B {
A parent
}

def a = new A()
def b = new B(parent:a)
a.addToChildren(b)

json g.render(a, [deep: true])
  • isDirty方法实际上时通过检测类属性指向的引用是否改变来实现的,假如是属性内部的属性发生改变,isDirty方法实际是检测不到的。如:
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
class Person{
String firstName
String lastName
Address address
}

class Address{
String city
String street
}

Address address = new Address('shanghai','street')
Person person = new Person('fname','lname',address)

person.save()
person.firstName = 'xxxx'
person.isDirty() == true
person.isDirty('firstName') == true

person.save()
address.city = 'beijing'
person.isDirty('address') == false
person.isDirty() == false

person.address.isDirty() == true
person.address.isDirty('city') == true
  • build test data plugin 不支持自定义约束的属性,就算在TestDataConfig.groovy中设置了也没用,如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
testDataConfig {
sampleData {
Dog {
name = { ->
UUID.randomUUID().toString().toUpperCase().replace('-', '')
}
}
}
}

Class Dog {
String owner
String name

static constraints = {
name validator: { val, obj ->
...
}
}
}

Dog dog = Dog.build()
dog.name == 'name'
  • one-to-many关联中,用left join的方式从many的一方查找出来one,one关联的many对象不正确,如:
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
class Person {
static hasMany = [dogs:Dog]
}

class Dog {
String name
}

Dog a = new Dog('A')
Dog b = new Dog('B')

Person p = new Person('P')
person.addToDogs(a)
person.addToDogs(b)
person.save()

Collection<Person> result = Person.withCriteria {
createAlias('dogs', 'd', org.hibernate.sql.JoinType.LEFT_OUTER_JOIN)
eq('d.name', 'A')
}

result[0].dogs.size() == 1

result = Person.withCriteria {
dogs {
eq('name','A')
}
}

result[0].dogs.size() == 2

Spring RestTemplate 打印Request及Response内容

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
import org.apache.commons.io.IOUtils
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.http.HttpRequest
import org.springframework.http.client.ClientHttpRequestExecution
import org.springframework.http.client.ClientHttpRequestInterceptor
import org.springframework.http.client.ClientHttpResponse


class LoggingRequestInterceptor implements ClientHttpRequestInterceptor {

static final String DEFAULT_ENCODING = 'UTF-8'
static final Logger LOGGER = LoggerFactory.getLogger(LoggingRequestInterceptor)

@Override
ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
traceRequest(request, body)
ClientHttpResponse response = execution.execute(request, body)
traceResponse(response)
return response
}

private void traceRequest(HttpRequest request, byte[] body) throws IOException {
LOGGER.info('===========================request begin================================================')
LOGGER.info('URI : {}', request.URI)
LOGGER.info('Method : {}', request.method)
LOGGER.info('Headers : {}', request.headers)
LOGGER.info('Request body: {}', new String(body, 'UTF-8'))
LOGGER.info('==========================request end================================================')
}

private void traceResponse(ClientHttpResponse response) throws IOException {
LOGGER.info('============================response begin==========================================')
LOGGER.info('Status code : {}', response.statusCode)
LOGGER.info('Status text : {}', response.statusText)
LOGGER.info('Headers : {}', response.headers)
LOGGER.info('Response body: {}', IOUtils.toString(response.body, DEFAULT_ENCODING))
LOGGER.info('=======================response end=================================================')
}

}
  • 配置RestTemplate
1
2
3
RestTemplate restTemplate = new RestTemplate()
restTemplate.setRequestFactory(new BufferingClientHttpRequestFactory(new SimpleClientHttpRequestFactory()))
restTemplate.setInterceptors([new LoggingRequestInterceptor()])

由于会多次读取request及response body,所以我们这里会使用BufferingClientHttpRequestFactory类来保证能够读取多次。

Groovy正则表达式用法

Matcher example:

1
2
3
4
5
6
7
8
9
10
11
//Matcher example
String regexStr = /gr.*/
String str = 'groovy'

Matcher matcher0 = (str =~ regexStr)
boolean result0 = (str ==~ regexStr)
assert matcher0.matches() == result0

Matcher matcher1 = (str =~ /$regexStr/)
boolean result1 = (str ==~ /$regexStr/)
assert matcher1.matches() == result1

Find example:

1
2
3
4
5
6
def cool = /gr\w{4}/  // Start with gr followed by 4 characters.
Matcher matcher2 = ('groovy, java and grails rock!' =~ /$cool/)
assert 2 == matcher2.count
assert 2 == matcher2.size() // Groovy adds size() method.
assert 'groovy' == matcher2[0] // Array-like access to match results.
assert 'grails' == matcher2.getAt(1)

Sl4j+Logback依赖配置

1
2
3
4
5
6
7
8
9
def sl4jVersion = '1.7.25'
def logbackVersion = '1.2.3'

compile group: 'ch.qos.logback', name: 'logback-core', version: logbackVersion
compile group: 'ch.qos.logback', name: 'logback-classic', version: logbackVersion
compile group: 'ch.qos.logback', name: 'logback-access', version: logbackVersion
compile group: 'org.slf4j', name: 'slf4j-api', version: sl4jVersion
compile group: 'org.slf4j', name: 'jcl-over-slf4j', version: sl4jVersion
compile group: 'org.slf4j', name: 'log4j-over-slf4j', version: sl4jVersion