网站跑在运行Fedora 32 + K8s + CRIO的树莓派集群上

成功把整个网站放到自己的树莓派集群上了,现在有数据在自己手上(物理的)的感觉了。

树莓派4-4G X 4的集群

自己动手搞了个架子,然后弄了几个开关(下方四个小开关)方便断路,并给每个开关加了个自恢复保险防止手滑短路,并且可以增加仪式感。启动集群的时候一个开关一个开关地打开感觉自己在启动核反应堆😁,不过这个24小时常开的,基本上用不太到开关功能。自恢复保险确实起了一次作用,在面包版插元件不小心短路了,直接断开。

搭建基本参照K8s官网,用的Kubeadm启动集群,Calico作为CNI。然后左上方路由器跑的HAproxy通过autossh穿透到外网,然后反代到Nginx Ingress,CEPH作为存储(另有一个现成的CEPH集群),(恩,在那个CEPH集群上再跑K8s更合理一些毕竟性能好,但我就是觉得树莓派在日常工作的桌子旁边跑更好玩…)。

内核需要自己编译,然后配置引导,参考树莓派官网,uboot形式暂时走不通。Gentoo上有很好的关于如何boot一个64bit kernel的说明。其他也遇到不少坑,其中就包括Fedora里的K8s太过陈旧,正在帮maintainer更新包发个新Release,PR已提,快了™。

在那之前我自己弄了个COPR:https://copr.fedorainfracloud.org/coprs/kasong/kubernetes/

其他大部分坑都包含在这个playbook里,可以参考,请不要直接用,IP地址域名都不通用:

- - hosts: all
  tasks:
  - name: Enable crio 1.17 dnf module
    command: "dnf module enable cri-o:1.17/default -y"

  - name: Basic K8s package
    yum:
      name: "{{ packages }}"
      state: installed
    vars:
      packages:
      - kubernetes-node
      - kubernetes-client
      - kubernetes-kubeadm
      - kubernetes-master
      - cri-tools
      - crio

  - name: Ensure overlayfs, br_netfilter is loaded
    lineinfile:
      path: "/etc/modules-load.d/kubernetes-crio.conf"
      line: "{{ item }}"
      create: yes
    with_items:
      - br_netfilter
      - overlay

  - name: Enable Kluster firewall zone
    firewalld:
      zone: pi-cluster
      state: present
      permanent: yes

  - name: Enable 192.168.2.0/24 to Kluster zone
    firewalld:
      source: 192.168.2.0/24
      zone: pi-cluster
      state: enabled
      permanent: yes

  - name: Enable Kluster firewall zone
    firewalld:
      zone: intra-intra-net
      state: present
      permanent: yes

  - name: Enable 192.168.2.0/24 to Intra zone
    firewalld:
      source: 192.168.12.0/24
      zone: intra-intra-net
      state: enabled
      permanent: yes

  - name: Enable 192.168.2.0/24 to Intra zone
    firewalld:
      source: 192.168.0.0/24
      zone: intra-intra-net
      state: enabled
      permanent: yes

  - name: Enable K8s API port to Intra
    firewalld:
      port: 6443/tcp
      permanent: yes
      zone: intra-intra-net
      state: enabled

  - name: Enable K8s API port to Intra
    firewalld:
      service: ssh
      permanent: yes
      zone: intra-intra-net
      state: enabled

  - name: Enable NodePort Services to Intra
    firewalld:
      port: 30000-32767/tcp
      permanent: yes
      zone: intra-intra-net
      state: enabled

  - name: Enable crio
    service:
      name: crio
      enabled: yes
      state: started

  - name: Enable kubelet
    service:
      name: kubelet
      enabled: yes

#  - name: Disable ZRAM
#    service:
#      name: zram-swap
#      enabled: no
#      state: stopped

  - name: Enable K8s API port
    firewalld:
      port: 6443/tcp
      permanent: yes
      zone: pi-cluster
      state: enabled

  - name: Enable etcd ports
    firewalld:
      port: 2379-2380/tcp
      permanent: yes
      zone: pi-cluster
      state: enabled

  - name: Enable mdns port
    firewalld:
      service: mdns
      permanent: yes
      zone: pi-cluster
      state: enabled

  - name: Enable kubelet port
    firewalld:
      port: 10250/tcp
      permanent: yes
      zone: pi-cluster
      state: enabled

  - name: Enable kube-scheduler port
    firewalld:
      port: 10251/tcp
      permanent: yes
      zone: pi-cluster
      state: enabled

  - name: Enable kube-controller-manager port
    firewalld:
      port: 10252/tcp
      permanent: yes
      zone: pi-cluster
      state: enabled

  - name: Enable NodePort Services port
    firewalld:
      port: 30000-32767/tcp
      permanent: yes
      zone: pi-cluster
      state: enabled

  - name: Enable BGP ports
    firewalld:
      port: 179/tcp
      permanent: yes
      zone: pi-cluster
      state: enabled

  - name: QUIRK Firewalld disable nft
    lineinfile:
      dest: "/etc/firewalld/firewalld.conf"
      regexp: '^FirewallBackend=.*'
      line: 'FirewallBackend=iptables'

  - name: Start/Restart Firewalld
    service:
      name: firewalld
      state: restarted

  - name: QUIRK Remove invalid Kubelet config option
    replace:
      path: /etc/systemd/system/kubelet.service.d/kubeadm.conf
      regexp: '^(.+)--allow-privileged=([^ "]*)(.*)'
      replace: '\1\3'

  - name: QUIRK Kubelet don't depend on docker
    lineinfile:
      dest: "/usr/lib/systemd/system/kubelet.service"
      regexp: '^(Requires=docker.service)$'
      line: '# \1'
      backrefs: yes

  - name: Tell NetworkManager not to control calico interface
    blockinfile:
      create: yes
      path: /etc/NetworkManager/conf.d/calico.conf
      block: |
        [keyfile]
        unmanaged-devices=interface-name:cali*;interface-name:tunl*

  - name: Start/Restart NetworkManager
    service:
      name: NetworkManager
      state: restarted

  - name: K8s kernel modules
    blockinfile:
      create: yes
      path: /etc/modules-load.d/kubernetes-crio.conf
      block: |
        br_netfilter
        overlay

  - name: K8s kernel sysctl
    blockinfile:
      create: yes
      path: /etc/sysctl.d/99-calico.conf
      block: |
        net.ipv4.conf.all.rp_filter = 1

  - name: K8s kernel sysctl
    blockinfile:
      create: yes
      path: /etc/sysctl.d/99-kubernetes-cri.conf
      block: |
        net.bridge.bridge-nf-call-iptables  = 1
        net.ipv4.ip_forward                 = 1
        net.bridge.bridge-nf-call-ip6tables = 1

  - name: Update Kubelet config
    lineinfile:
      dest: "/etc/systemd/system/kubelet.service.d/kubeadm.conf"
      line: 'Environment="KUBELET_CRIO_ARGS=--container-runtime=remote --container-runtime-endpoint=/var/run/crio/crio.sock"'
      insertbefore: "ExecStart=.*"

  - name: Update Kubelet config
    lineinfile:
      dest: "/etc/systemd/system/kubelet.service.d/kubeadm.conf"
      regexp: '^ExecStart=(((?!\$KUBELET_CRIO_ARGS).)+)$'
      line: 'ExecStart=\1 $KUBELET_CRIO_ARGS'
      backrefs: yes

  - name: Update CRIO config for insecure repo
    ini_file:
      dest: "/etc/crio/crio.conf"
      section: "crio.image"
      option: 'insecure_registries'
      value: '[ "registry.intra.intra-net.com" ]'

  - name: Update common container config for insecure repo
    ini_file:
      dest: "/etc/containers/registries.conf"
      section: "registries.insecure"
      option: 'registries'
      value: '[ "registry.intra.intra-net.com" ]'

  - name: Ensure /opt/cni exists for symlink creation in next step
    file:
      path: "/opt/cni"
      state: directory

  - name: Create symbolic link for CNI Plugin
    file:
      src: "/usr/libexec/cni"
      dest: "/opt/cni/bin"
      state: link

  - name: "Now run 'sudo kubeadm init --cri-socket /var/run/crio/crio.sock --pod-network-cidr=10.24.0.0/16'"
    debug:

# Misc TODO: Apply a Docker mirror
#
# Misc TODO: Apply following config for calico yaml
#  - name: IP
#    value: "autodetect"
#  - name: IP_AUTODETECTION_METHOD
#    value: "interface=eth.*"
#  - name: IP6_AUTODETECTION_METHOD
#    value: "interface=eth.*"
#
# Misc TODO: Black list vc4 if kernel is failing
# echo blacklist vc4 > /etc/modprobe.d/blacklist-vc4.conf
#
# Misc TODO: Still need old fashion cgroup
# cgroup_enable=memory systemd.unified_cgroup_hierarchy=0
#
# Misc TODO: Ensure following kernel configs are enabled
# CONFIG_NETFILTER_XT_MATCH_CGROUP
# CONFIG_F2FS_FS_SECURITY
# CONFIG_CFS_BANDWIDTH
# CONFIG_BLK_DEV_RBD
# CONFIG_BRIDGE_NETFILTER
#
# Misc TODO: Tune etcd
# - name: ETCD_MAX_WALS
#   value: "5"
# - name: ETCD_HEARTBEAT_INTERVAL
#   value: "500"
# - name: ETCD_ELECTION_TIMEOUT
#   value: "10000"
# - name: ETCD_SNAPSHOT_COUNT
#   value: "5000"
# - name: ETCD_LOG_LEVEL
#   value: "error"

正如所见,还有一堆TODO没写成playbook形式,回头需要再搭一遍的时候再说了XD。很多坑我会尝试在打包环节修一下,playbook中的一部分就不再需要了。

优雅地让docker,nginx,acme.sh一起工作

前阵子买了个配置不错的阿里云,先搭了个Minecraft上去,Minecraft也跑在Docker里,并加载了DynMap显示实时地图。为了日后好扩展,方便上一些比较好看的页面,先用一个nginx反代一下,顺便挂上HTTPS,证书当然要从Letsencrypt搞。

看了下现有的方案,感觉都不太好看,都需要独立维护一个Dockerfile,不能直接用Nginx mainline,刷新证书的时候需要先关闭服务器,而且acme.sh和nginx跑在同一个容器里。想了一下,打算让acme.sh和nginx分开跑,nginx把challenge内容转发给acme.sh,这样应该就妥了,惟一的不好的地方是需要写个crontab,每隔2个月重启一下nginx的容器。不过其他方案也有downtime,也算可以接受。

目录结构和配置文件:

/
    acme.sh  -  SSL证书存放和acme帐号信息,配置信息
    nginx  -  自定义nginx配置存放的地方
        conf.d  -  一些nginx全局配置
        sites-enabled - 各个domain设置
        sites-conf  -  domain之间共用的设置

nignx/conf.d/acme.sh.conf

upstream acme-sh {
  server acme-sh:80;
}

nignx/conf.d/enabled-sites.conf

include /etc/nginx/sites-enabled/*.conf;

nginx/sites-conf/acme.sh.conf

location ~ "^/\.well-known/acme-challenge/([-_a-zA-Z0-9]+)$" {
    default_type text/plain;
    proxy_read_timeout    60;
    proxy_connect_timeout 60;
    proxy_redirect        off;
    proxy_pass http://acme-sh;

    proxy_set_header      X-Real-IP $remote_addr;
    proxy_set_header      X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header      X-Forwarded-Proto $scheme;
    proxy_set_header      Host $host;
}

nginx/sites-enabled/example.com.conf

server {
    server_name example.com;

    listen 0.0.0.0:443 ssl;
    listen [::]:443 ssl;

    ssl on;
    ssl_certificate /etc/ssl/letsencrypt/example.com/fullchain.cer;
    ssl_certificate_key /etc/ssl/letsencrypt/example.com/example.com.key;
    include sites-conf/acme.sh.conf;

    ...
}

server {
    server_name example.com;

    listen 0.0.0.0:80;
    listen [::]:80;

    include sites-conf/acme.sh.conf;

    location / {
        return 301 https://example.com$request_uri;
    }
}

第一次要先获取一个有效证书:

docker run --rm  -it  \
  -v "$(pwd)/acme.sh":/acme.sh  \
  --net=host \
  neilpang/acme.sh  --issue -d example.com  --standalone

使用docker-compose做容器管理,yml内容如下:

some-other-service:
    ...

nginx-srv:
    image: nginx:mainline
    volumes:
        - ./nginx/conf.d:/etc/nginx/conf.d
        - ./nginx/sites-conf:/etc/nginx/sites-conf
        - ./nginx/sites-enabled:/etc/nginx/sites-enabled
        - ./acme.sh:/etc/ssl/letsencrypt
    links:
        - some-other-service:upstream
        - acme-sh:acme-sh
    ports:
        - "443:443/tcp"
        - "80:80/tcp"

acme-sh:
    image: neilpang/acme.sh
    command: daemon
    volumes:
        - ./acme.sh:/acme.sh

调 docker-compose up就可以了。

最后加一个cronjob让nginx定期重新读取证书:

/var/spool/cron/root:

cd /path/to/docker-compose.yaml && /usr/bin/docker-compose exec nginx-srv nginx -s reload

 

记一次openwrt上折腾使用xkcptun,openvpn,ipset的策略路由

工作需求,需要加速外网访问,因此一个国外VPS服务器是必须的。由于外网条件限制,常常会有丢包,因此使用KCP进行加速可以极大的改善高丢包网络下的通讯速度,副作用是会重复发包放大流量,不过一般流量管够随便用,不需要担心。

首先要安装xkcptun:

基于Golang的kcptun无法很好的运行在MIPS环境上,因此使用C编写的xkcptun。

服务端:

我在repo中加入了可以build使用的Dockerfile,直接clone:

git clone https://github.com/ryncsn/xkcptun

之后直接进入目录,修改一下Dockerfile里面的参数,比如密码,端口地址等等,可以直接build了。这些参数应该单独拿出来,不过一时省事先不管了。

docker build -t xkcptun .

我一般习惯用docker-compose来管理:

xkcptun-ovpn:
  image: xkcptun
  net: "host"
  command: xkcp_server -c /etc/xkcptun.json -f
  restart: always
Openwrt(客户端):

直接从这里下载包安装:

https://github.com/gigibox/openwrt-xkcptun/releases

之后修改/etc/config/xkcptun,之后执行

/etc/init.d/xkcptun_client start
/etc/init.d/xkcptun_client enable

没研究UI,我的WNDR4300上只安装openwrt_xkcptun_0.4.409-1_ar71xx.ipk就够了。

此处挖坑

Openwrt-xkcptun release的build编译的有问题,用原repo的编译出来client和server无法连接,经过各种抓包和调试,发现是竟然是大小端的问题。应该重新编译一下修复大小端问题,考虑到Openwrt编译又要好久,就先在自己的repo里dirty hack了一下来兼容老的ar71xx build,因此这个repo编译出的xkcptun与其他的不兼容。

Openvpn配置:

直接使用docker-openvpn进行配置:

https://github.com/kylemanna/docker-openvpn

其中udp://VPN.SERVERNAME.COM换成vpn的地址,并且换成tcp协议,方便kcp进行包装。可以加上一个不存在的子域名前缀,方便之后添加hosts项来让kcp充当代理的作用。

CLIENTNAME是要链接的client name。

生成ovpn文件之后,将其拷到Openwrt设备上,并且在/etc/hosts中添加127.0.0.1 VPN.SERVERNAME.COM,让xkcp监听本地的1194转发到远端的1194,这样就可以让openvpn跑在kcp上面了。

策略路由:

为了完成策略路由,要先安装相关包:

opkg install iptables-mod-nat-extra ipset

opkg remove dnsmasq && opkg install dnsmasq-full

修改/etc/dnsmasq.conf,加入confdir=/etc/dnsmasq.d

建立/etc/dnsmasq.d,使用下面的脚本生成一个配置文件,命名dnsmasq_flist_ipset.conf,扔到/etc/dnsmasq.d/文件夹中:

https://github.com/cokebar/gfwlist2dnsmasq/blob/master/gfwlist2dnsmasq.sh

使用如下命令建立一个ipset list:

ipset -N flist iphash

防止DNS解析问题,先加入两个DNS服务器:

ipset add flist 8.8.8.8
ipset add flist 8.8.4.4

使用iptables做一下标记和允许转发,让标记包走特定路由表:

# iptables mark package matches the ipset
iptables -t mangle -A OUTPUT -j fwmark
iptables -t mangle -A fwmark -m set --match-set flist dst -j MARK --set-mark 0xffff

# Masquirade the package as routing is done before iptables mangle
iptables -t nat -A POSTROUTING -m mark --mark 0xffff -j MASQUERADE

# Route all package using ovpn table
ip rule add fwmark 0xffff table ovpn

# And allow forwarding
iptables -I FORWARD 1 -o tun0 -j ACCEPT

在之前的ovpn文件中加入,实现自动恢复:

script-security 2

up '/bin/sh -c "ip route add default dev tun0 table ovpn"'

这样便可以实现vpn连接自动添加路由和允许iptables转发的功能了,大功告成。

将iptables和ipset相关命令放入/etc/firewall.user,iproute2相关放入/etc/rc.local即可完成自动恢复。

Python Hybrid Method

这两天有一个特殊需求,在一个Python class上实现一个method,可以被当作Class method调用,也可以被当作Instance的Method调用。看起来不是很Pythonic,一个函数有两个不同的行为,不过在特殊情况下确实很实用。

尝试对性能进行了一下优化,并没有用到太复杂的优化方法,所以只是相对性能可以接受。调用消耗大概是普通调用的一倍。

from functools import wraps
class hybridmethod(object):
    """
    High performance hybrid function wrapper
    """
    __slot__ = ['context', 'method', 'clsmethod']

    def __init__(self, func):
        self.clsmethod = self.method = wraps(func)(lambda *a, **kw: func(self.context, *a, **kw))

    def classmethod(self, func):
        """
        Function to call when calling with class
        """
        self.clsmethod = wraps(func)(lambda *a, **kw: func(self.context, *a, **kw))
        return self

    def __get__(self, instance, cls):
        if instance is None:
            self.context = cls
            return self.clsmethod
        self.context = instance
        return self.method

自己用到的场景:

class Model(object):
    @hybridmethod
    def from_dict(self, dict_):
        """
        Inflate the instance with dict
        """
        for k, v in dict_.items():
            setattr(self, k, v)
        return self

    @from_dict.classmethod
    def from_dict(cls, dict_):
        """
        Inflate the instance with dict
        """
        instance = cls()
        for k, v in dict_.items():
            setattr(instance, k, v)
        return instance

新技能GET – 修热水器

家里的燃气热水器坏了一阵子,本来打算打算趁五一电商活动买个新的换上。不过,本着电器不用到彻底报废就不打算坏的原则,决定自己修一下。

打开盖子前后还是有点犹豫,毕竟涉及天然气,水路和电路,万一搞出差错出问题就不好办了。不过一番七手八脚折腾打开之后,经过仔细端详感觉原理也挺简单。感觉主要是温度反馈控制和可靠性设计这两天比较考验厂家实力。

好了不装逼了,经过大致判断应该是水流传感器坏了。依据是:打开水龙头由于有旧的存水,所以随着新水源进入温度变化看显示面板是有变化的,不过就是不打火。而且是完全没有打火的痕迹,比如风扇预启动,打火装置充电之类的都没发生。

水流传感器样子看起来是一个管子上多了一个方形盒子的感觉。淘宝上20一个,快递要好几天才能到,那就愉快地拆传感器吧。不过家里正好水阀坏了,ORZ。但拆都拆了,决定带着水压尝试拆一下电路部分。(似乎有的水流传感器电路部分和密封的盖子是一起固定的,拆开就崩。而家里的水流传感器可能是分离设计的或者锈地太厉害结果密封部分粘住了)

打开之后发现果然,电路部分整个浸水了,而且都锈了。再一看发现这个水流传感器似乎安装有问题,塑料外壳上有两个突起用来卡住电路,但直接怼在的电路板上了,导致密封不严实。猜测这应该也是进水的原因。直接把电路拆下来之后对着传感器剩下部分观察了一下,看起来大概原理是密封部分里面一个水轮,末端是磁铁,正对着电路部分的一个霍尔元件,水流导致水轮转动就有信号了。霍尔元件直接三个引脚接出来。其中两个脚分别是+5V和GND,另一个是信号脚,+5V处串一个5K电阻。其中电阻锈地彻底废了,但还好霍尔原件完好。

尝试修复比较简单就不做更多监测了,用飞线大法重连接了一下电路板短路的地方,朽烂了的电阻从其他报废电器上拆一个过来。装回热水器,安全起见先关闭燃起阀门测试一下水流,热水器立马嗡嗡地跑起来了,稳如狗。

Gentoo Zsh 缺少 curses.so 的问题

长期以来一直快乐的使用着 oh-my-zsh 里的 zsh-navigation-tools plugin 来进行模糊查询,不知道哪次 Gentoo 升级之后这个插件不能用了,报错:
command not found: zcurses
网上简单查询发现这个是个 zsh 内置模块的指令,应该是编译的时候 curses.so 被跳过了。
检查一下ebuild,没有看到有相关的USE flag,尝试调整ncurse和zsh包的版本和USE flag均无果。
检查一下 zsh 代码中的 configure.ac,发现$LIB中没有加入 ncurses 相关的 lib,zsh 会编译的时候会跳过 ncurses 相关模块的编译。

临时解决方法:
1、本地建立一个 Portage Overlay – https://forums.gentoo.org/viewtopic-t-827407-start-0.html,按需求 copy & paste 相关 ebuild
2、src_configure 中加入一行 myconf += ( –enable-libs=-lncursesw )
3、emerge zsh

zsh-navigation-tools plugin 又欢乐的跑起来了。

自己的Node.JS + Arduino的家居控制系统

很久以前的课设时实现了自己的一个想法,无线控制屋里的灯,再也不用每次都要站起来去手动关灯了。为了能通用些,写了成了一个小的框架,每个控制功能作为一个“驱动/模块“,然后在这个基础上把温湿度传感,红外/空调遥控之类的都做了出来。之后借来的Intel Gailieo还回去了,开始磨磨蹭蹭地把系统放到了树梅派上,然后改进了一下Arduino端的结构,也尝试写了一个比较通用的框架。

使用Node.js作为服务器提供Web端控制,通过不同的通讯模块(现在有NRF24L01,网络和WIFI)处理请求和传感器发回的信息等,将用户请求发送到下位机(比如Arduino),服务器处理各种传感器和控制器的具体任务。

Arduino上尝试实现了一个库,提供一个框架,方便实现硬件逻辑,可以将一个操作绑定到一个函数上,实现不同操作的处理。

服务器端对应的灯的“驱动”目前看起来是这样的:

var light = {};
var plat = undefined;

light.decorate = function (thing, desc){
  thing.turnOff = function(){
    var op = {
      'op':'turnOff',
    }
    thing.send(op);
  }
  thing.turnOn = function(){
    var op = {
      'op':'turnOn',
    }
    thing.send(op);
  }
  thing.powerOff = thing.turnOff;
  thing.powerOn = thing.turnOn;
  thing.receives.add(function(data){
    thing.status = data.status;
  })
  thing.interfaces['Power On'] = {
    type:'switch',
    name:'Power On',
    desc:'开灯',
    action:{
      1:'turnOn',
      0:'turnOff'
    },
    value:'power'
  };
}

light.init = function (base){
  plat = base;
}


module.exports = light;

Arduino上用自己的这个框架的话代码也比较干净利索,目前看起来是这样:

#define RF24_ADDR 0xFFFF000001
//#define SOME_DEBUG
//#define SOME_ERROR
#include <ArduinoJson.h>
#include <EEPROM.h>
#include <SPI.h>
#include "printf.h"
#include "Some.h"

Thing<link_RF24> me(0,
                    "{\"name\":\"智能灯\",\"groups\":{\"power\":{\"hasBattery\":false},\"smart\":{\"alwaysOn\":true,\"heartBeat\":false},\"light\":{}}}");


void turnOn(void) {
  Serial.println("turnOn");
  digitalWrite(6, LOW);
  me.send("{\"power\":1}");
}

void turnOff(void) {
  Serial.println("turnOff");
  digitalWrite(6, HIGH);
  me.send("{\"power\":0}");
}

void setup() {
//  Serial.begin(115200);
//  printf_begin();
  pinMode(6, OUTPUT);
  digitalWrite(6, LOW); //Turn the light on as soon as possible.
  me.setup();
  me.poke();
  me.on("turnOn", turnOn);
  me.on("turnOff", turnOff);
  turnOn();
}

void loop(void) {
  delay(50);
  me.refresh();
}

不少地方需要跟进,比如使用Arduino设备的自动发现,给每个设备分配更加独立可识别的ID,加密和安全性等,服务器中对事件的处理等等。

目前服务器跑在树梅派上,使用Arduino改造了自己的屋顶灯,做了几个传感器,跑得很欢。

持续更新中…

Github:

Server:https://github.com/ryncsn/some

Arduino:https://github.com/ryncsn/some-arduino

Gentoo Tips

记录一下一些有用不好找的小知识:

忽略一些package自带的pre emerge check failed:

export I_KNOW_WHAT_I_AM_DOING=yes

 

建立Hardened Desktop Profile:

Gentoo 以前是有 Hardened Desktop 的Profile的,后来取消了。

使用的话可以自己建一个

mkdir -p  /etc/portage/make.profile && cd /etc/portage/make.profile

echo 5 > eapi

echo “gentoo:hardened/linux/amd64
gentoo:targets/desktop” > parent

echo “-abi_x86_32” > use.mask (启用multilib,不用的话跳过)

 

建立 Package Set:

将一堆包归为一个Set,方便安装和管理:

mkdir /etc/portage/sets && cd /etc/portage/sets

echo “<packages>” > <set name>

比如建立Android编译环境的包:

echo “media-gfx/pngcrush
app-arch/lz4
dev-python/lz4
sys-devel/bison
dev-python/buildutils
net-misc/curl
sys-devel/flex
dev-vcs/git
app-crypt/gnupg
dev-util/gperf
media-libs/audiofile
media-libs/alsa-lib
app-arch/unzip
dev-util/valgrind” > android

emerge -av @android

 

分步编译安装Package:

ebuild myebuild fetch (if you don’t have it in distfiles)

ebuild myebuild unpack (unpacked to /var/tmp/portage/packagename/something)

ebuild myebuild compil

ebuild myebuild install

ebuild myebuild qmerge

 

将package.use,package.keywords等建立为目录:

可以将这些文件用同名目录替代,portage会遍历目录下文件

 

忽略emerge过程中部分失败的包

emerge –keep-going

 

 

ESP8266当成Arduino使用

近期入了个ESP8266-12,发现可以直接当作MCU使用。

ESP8266 + 面包板 = 可以直接当作带Wifi的开发版来用,缺点就是IO太少,比较费电

(费电估计是我配置问题)

1、把引脚接到面包板上

2、CE,VCC,GPIO X接3.3v

3、GND,GPIO X接GND

4、用USB-TTL和TX,RX,GND相连。GND相连确保信号电平正常,注意ESP8266尽量使用外接电源,一般USB-TTL转接器(我用的PL2312)供电不够ESP8266用的,非常不稳定。

5、ESP8266加电时GPIO 2的电位决定了Boot Mode,接VCC的话即运行已烧录的程序,接地者进入烧录模式。

Arduino IDE:

git clone https://github.com/esp8266/Arduino/<Arduino Diretory>/libraries/hardware/esp8266

重起或启动Arduino IDE,硬件选Genertic ESP8266,下载模式可以换成QIO(DIO,QIO的D,Q分别代表Dual,Quard,双通,四通,四通下载会快一些,不工作可换回双通),其他参数还没动过。

下载方法和Arduino一样,在Arduino IDE里写写写,然后确保启动为烧录模式后下载即可,之后把GPIO 2接VCC,Reset或者重接电就可以拿着烧好的固件到处跑了,连着串口的话可以调试。

esp8266-arduino项目:https://github.com/esp8266/Arduino

 

使用Postfix,Dovecot,MySQL搭建的邮件服务器

前阵子和几位朋友一同建起了一个小社区网站,期间搭建了一个团队服务器用同步代码和收发邮件,搭建邮件服务还是头一回。现在现成的邮件服务器解决方案也是有的,不过还是自己搭建了一遍,熟悉了一下具体流程。这两天在自己的服务器上也部署了,大致记录下:

结构:

Postfix 提供 SMTP,submission 服务,并对请求进行过滤,转发。SMTP 是邮件投递的基础。Postfix 各种功能可以添加外部实现,比如这里会将 Virtual Mail 通过 LMTP 转给Dovecot,Email 信息查询部分由 MySQL 提供,通过 pypolicyd-spf 进行过滤,用户验证也由 Dovecot 完成。

Dovecot 负责 IMAP 等服务的请求和用户验证,管理虚拟用户的邮箱内容等,和Postfix通过 LMTP 链接。

使用 MySQL 存储邮箱用户的账户信息,MySQL是可选的存储方式之一。

为了安全起见所有链接均用了加密的方式,普通 STMP,IMAP 链接强制必须使用 STARTTLS 升级为安全链接,STMPS,IMAPS 链接和 HTTPS 一样一开始就为加密链接。

安装Postfix、Dovecot、MySQL、pypolicyd-spf

视不同发行版安装相应的包即可。

 

配置:

系统:

建立vmail用户,vmail文件夹

useradd -s /sbin/nologin -d /var/vmail -m -r -g mail vmail

chown vmail:mail /var/vmail

chmod o-rwx vmail /var/vmail

grep vmail /etc/passwd | awk -F : '{print $3}'

建立并获得的vmail的uid,稍后会用到。

也可以指定uid建立vmail用户:

useradd -s /sbin/nologin -d /var/vmail -m -r -u <uid> vmail

建立 aliases.db(如果不存在的话),Postfix会用到

touch /etc/alises

newaliases

MySQL:

初次开启的话记得运行一下mysql_secure_installation。

通过 mysql -u root -p 输入密码进入 Mysql,参考以下步骤建表,插入数据。

create user [email protected] identified by 'vmailpassword';

create database vmail;

grant select on vmail.* to [email protected] identified by 'vmailpassword';

use vmail;

create table domains (name char(30) not null primary key);

create table users (email char(64) not null primary key, password char(128) not null, domain char(30) not null, foreign key (domain) references domains(name));

create table aliases (source char(64) not null primary key, destination char(64) not null, foreign key (destination) references users(email));

insert into domains values('----.com');

insert into users values('[email protected]', ENCRYPT('--password--here--', CONCAT('$6$', SUBSTRING(SHA(RAND()), -16))),'----.com');

insert into aliases values ('[email protected]', '[email protected]');

domains表存放virtual domian,user表存放可登陆的用户邮箱和密码。

Postfix 配置:

配置文件:

注意修改前备份,方便恢复和参考。主要修改以下参数:

设置服务器信息,有单个服务器的情况下设置比较简单,domain为服务器域名:

mydestination 不可与 virtual main domain 冲突,否则发往 Dovecot 的邮件会被 Postfix 截下来,一般会找不到用户而 Reject,如果正好有重名的用户的话应该会发往错误的用户。

mydestination = localhost
mydomain = <domain>
myorigin = <domain>
myhostname = <domain>
mynetworks = 127.0.0.0/8
mynetworks_style = host
relay_domains =

TLS证书设置:

smtpd_tls_cert_file = /etc/ssl/private/----crt----
smtpd_tls_key_file = /etc/ssl/private/----key----
smtpd_tls_CApath = /etc/ssl/certs
smtpd_use_tls = yes

SASL支持设置,设置为不兼容老旧客户端:

smtpd_sasl_type = dovecot
smtpd_sasl_path = private/auth
smtpd_sasl_auth_enable = yes
smtpd_sasl_security_options = noanonymous, noactive, nodictionary
smtpd_sasl_local_domain = $mydomain
broken_sasl_auth_clients = no

一些安全强化选项,使用高强度的加密算法,和启用TLS,以及设置空的readme文件夹等。

smtpd_tls_mandatory_exclude_ciphers = aNULL, eNULL, EXPORT, DES, RC4, MD5, PSK, aECDH, EDH-DSS-DES-CBC3-SHA, EDH-RSA-DES-CDC3-SHA, KRB5-DE5, CBC3-SHA
smtpd_tls_mandatory_ciphers = high
smtpd_tls_mandatory_protocols = !SSLv2, !SSLv3, TLSv1, TLSv1.1, TLSv1.2
smtpd_tls_exclude_ciphers = $smtpd_tls_mandatory_exclude_ciphers
smtpd_tls_ciphers = $smtpd_tls_mandatory_ciphers
smtpd_tls_protocols = $smtpd_tls_mandatory_protocols
smtpd_tls_auth_only = yes
smtpd_tls_dh1024_param_file = /etc/postfix/dh2048.pem
smtpd_tls_dh512_param_file = /etc/postfix/dh512.pem
smtpd_tls_eecdh_grade = ultra
smtpd_tls_security_level = may
smtpd_tls_loglevel = 1
smtpd_tls_received_header = yes

# outgoing
smtp_tls_mandatory_exclude_ciphers = aNULL, eNULL, EXPORT, DES, RC4, MD5, PSK, aECDH, EDH-DSS-DES-CBC3-SHA, EDH-RSA-DES-CDC3-SHA, KRB5-DE5, CBC3-SHA
smtp_tls_mandatory_ciphers = high
smtp_tls_mandatory_protocols = !SSLv2, !SSLv3, TLSv1, TLSv1.1, TLSv1.2
smtp_tls_exclude_ciphers = $smtp_tls_mandatory_exclude_ciphers
smtp_tls_ciphers = $smtp_tls_mandatory_ciphers
smtp_tls_protocols = $smtp_tls_mandatory_protocols
smtp_tls_security_level = may
smtp_tls_note_starttls_offer = yes
smtp_tls_loglevel = 1


tls_random_source = dev:/dev/urandom
tls_preempt_cipherlist = yes
# Log the hostname of a remote SMTP server that offers STARTTLS, when TLS is not already enabled for that server. 

# No readme for more security
readme_directory = no
sample_directory = no

生成对应的文件,在Shell中执行以下语句:

openssl dhparam -out /etc/postfix/dh2048.pem 2048
openssl dhparam -out /etc/postfix/dh512.pem 512

如果强制启用TLS的话,将上面的 *_level = may 改为 enforce。不过会导致和一些邮箱不兼容,比如126,163。

一些行为配置,酌情修改:

在找不到收信人的情况下返回永久错误,让发信服务器不再重试投递:

unknown_local_recipient_reject_code = 550

超时和错误次数限制:

# Time before log a delayed warning
delay_warning_time = 4h
# how long to keep message on queue before return as failed.
maximal_queue_lifetime = 7d
# max and min time in seconds between retries if connection failed
minimal_backoff_time = 1000s
maximal_backoff_time = 8000s
# how long to wait when servers connect before receiving rest of data
smtp_helo_timeout = 60s
# how many address can be used in one message.
# effective stopper to mass spammers, accidental copy in whole address list
# but may restrict intentional mail shots.
smtpd_recipient_limit = 16
# how many error before back off.
smtpd_soft_error_limit = 3
# how many max errors before blocking it.
smtpd_hard_error_limit = 12

配置和Dovecot的链接参数:

通过lmtp协议连接到dovecot,设置vmail的文件夹,用户等参数。

其中 uid 为 vmail 的 uid,gid 为 mail 的 gid,如果不是12的话酌情修改。(grep mail /etc/group)

virtual_transport = lmtp:unix:private/dovecot-lmtp
address_verify_virtual_transport = lmtp:unix:private/dovecot-lmtp
virtual_mailbox_domains = mysql:/etc/postfix/mysql-virtual-mailbox-domains.cf
virtual_mailbox_maps = mysql:/etc/postfix/mysql-virtual-mailbox-maps.cf
virtual_alias_maps = mysql:/etc/postfix/mysql-virtual-alias-maps.cf
virtual_mailbox_base = /var/vmail
virtual_uid_maps = static:998
virtual_gid_maps = static:12

其中一些位置可以写死,减少SQL调用,比如只有一个 virtual_mailbox_domain:

virtual_mailbox_domains = ----.com

其中需要三个SQL查询用的配置文件:

/etc/postfix/mysql-virtual-mailbox-domains.cf:

vim mysql-virtual-mailbox-domains.cf

user = vmail
password = vmailpassword
hosts = 127.0.0.1
dbname = vmail
query = SELECT 1 FROM domains WHERE name='%s'

mysql-virtual-mailbox-maps.cf:

vim /etc/postfix/mysql-virtual-mailbox-maps.cf

user = vmail
password = vmailpassword
hosts = 127.0.0.1
dbname = vmail
query = SELECT 1 FROM users WHERE email='%s'

/etc/postfix/mysql-virtual-alias-maps.cf:

vim /etc/postfix/mysql-virtual-alias-maps.cf

user = vmail
password = vmailpassword
hosts = 127.0.0.1
dbname = vmail
query = SELECT destination FROM aliases WHERE source='%s'

最后邮件发送/接受限制,阻止一些简单的垃圾/骚扰邮件:

强制 Client 发送 HELO,启用 REJECT DELAY 和下面的 client 中的 reject_unknown_client_hostname 过滤配合,只让有MX记录/通过验证的用户使用SMTP,禁用VRFY:

smtpd_helo_required = yes
smtpd_delay_reject = yes
disable_vrfy_command = yes

restriction 规则:

tpd_client_restrictions = reject_unauth_pipelining, permit_mynetworks, permit_sasl_authenticated, warn_if_reject, reject_plaintext_session, reject_unknown_client_hostname, permit
smtpd_relay_restrictions = reject_unauth_pipelining, permit_mynetworks, permit_sasl_authenticated, warn_if_reject, reject_non_fqdn_recipient, reject_unknown_recipient_domain, reject_unauth_destination, check_policy_service unix:private/policyd-spf, permit
smtpd_helo_restrictions = reject_unauth_pipelining, permit_mynetworks, permit_sasl_authenticated, warn_if_reject, reject_invalid_helo_hostname, reject_non_fqdn_helo_hostname, reject_unknown_helo_hostname, permit
smtpd_sender_restrictions = reject_unauth_pipelining, permit_mynetworks, permit_sasl_authenticated, warn_if_reject, reject_non_fqdn_sender, reject_unknown_sender_domain, reject_unverified_sender, permit
smtpd_recipient_restrictions = reject_unauth_pipelining, permit_mynetworks, permit_sasl_authenticated, warn_if_reject, reject_non_fqdn_recipient, reject_unknown_recipient_domain, reject_unauth_destination, check_policy_service unix:private/policyd-spf, permit
smtpd_data_restrictions = reject_unauth_pipelining, permit_mynetworks, permit_sasl_authenticated, warn_if_reject, reject_multi_recipient_bounce, permit
smtpd_end_of_data_restrictions = reject_unauth_pipelining, permit_mynetworks, permit_sasl_authenticated, warn_if_reject, reject_multi_recipient_bounce, permit
smtpd_etrn_restrictions = reject

SPF设置:延长SPF验证超时时间:

policy_time_limit = 3600

 

编辑 /etc/postfix/master.cf 加入以下来开启SPF服务,配合上面的过滤:

policyd-spf  unix  -       n       n       -       0       spawn
  user=tpe argv=/usr/bin/policyd-spf /etc/policyd-spf/policyd-spf.conf

继续编辑 /etc/postfix/master.cf ,开启SMTP服务:

smtp      inet  n       -       n       -       -       smtpd
...

submission inet n       -       n       -       -       smtpd
-o syslog_name=postfix/submission
-o smtpd_tls_security_level=encrypt
-o smtpd_sasl_auth_enable=yes
....

smtps     inet  n       -       n       -       -       smtpd
-o syslog_name=postfix/smtps
-o smtpd_tls_wrappermode=yes
-o smtpd_sasl_auth_enable=yes
....

smpts工作于wraper模式,即不要STARTTLS,直接建立TLS链接,其他参数按照main.cf为默认不修改。

执行:

postfix set-permissions

service postfix start

启动 postfix。

 

Dovecot:

配置文件,修改前注意备份:

编辑 /etc/dovecot/dovecot.conf,开启 imap lmtp。 imap 提供外部访问,lmtp给postfix用。

protocols = imap lmtp

需要这一行来加载conf.d中的配置文件。

!include conf.d/*.conf

 

编辑 /etc/dovecot/conf.d/10-mail.conf,指定vmail的文件位置和UID,GID,同时限制Dovecot能使用的GID,UID。如果inbox没有启用的话,取消注释启用。

mail_location = maildir:/var/vmail/%d/%n

mail_uid = vmail
mail_gid = mail

first_valid_uid = 998
last_valid_uid = 998

first_valid_gid = 12
last_valid_gid = 12

 

编辑: /etc/dovecot/conf.d/10-auth.conf,设置验证:

disable_plaintext_auth = yes

auth_mechanisms = plain login

注释除了sql以外的验证途径,仅保留:

!include auth-sql.conf.ext

编辑 conf.d/auth-sql.conf.ext,配置SQL验证的参数:

密码通过MySQL验证,用户登陆后使用的GID,UID统一使用vmail,mail,文件位置统一为/var/mail/。

passdb {
driver = sql

# Path for SQL configuration file, see example-config/dovecot-sql.conf.ext
args = /etc/dovecot/dovecot-sql.conf.ext
}

userdb {
driver = static
args = uid=vmail gid=vmail home=/var/vmail/%d/%n
}

编辑 /etc/dovecot/dovecot-sql.conf.ext,配置查询语句:

driver = mysql

connect = host=localhost dbname=vmail user=vmail password=vmailpassword

default_pass_scheme = SHA512-CRYPT

password_query = \
SELECT email as user, password FROM users WHERE email='%u';

 

编辑 /etc/dovecot/conf.d/10-logging.conf,显示更多日志:

auth_verbose = yes

 

编辑 /etc/dovecot/conf.d/10-master.conf,配置 Dovecot 提供的各种服务参数:

LMTP 和 Auth 服务均以 unix socket 形式与 Postfix 链接。worker 进程以 dovecot 身份运行。socket的位置均放在 /var/spool/postfix/private/ 中:

default_internal_user = dovecot

service lmtp {
unix_listener /var/spool/postfix/private/dovecot-lmtp {
mode = 0666
}

# Create inet listener only if you can't use the above UNIX socket
#inet_listener lmtp {
# Avoid making LMTP visible for the entire internet
#address =
#port =
#}
}

service auth {
# auth_socket_path points to this userdb socket by default. It's typically
# used by dovecot-lda, doveadm, possibly imap process, etc. Users that have
# full permissions to this socket are able to get a list of all usernames and
# get the results of everyone's userdb lookups.
#
# The default 0666 mode allows anyone to connect to the socket, but the
# userdb lookups will succeed only if the userdb returns an "uid" field that
# matches the caller process's UID. Also if caller's uid or gid matches the
# socket's uid or gid the lookup succeeds. Anything else causes a failure.
#
# To give the caller full permissions to lookup all users, set the mode to
# something else than 0666 and Dovecot lets the kernel enforce the
# permissions (e.g. 0777 allows everyone full permissions).
unix_listener auth-userdb {
mode = 0666
#user =
#group =
}

# Postfix smtp-auth
unix_listener /var/spool/postfix/private/auth {
mode = 0666
}

# Auth process is run as this user.
user = $default_internal_user
}

default_internal_user = dovecot

service auth-worker {
# Auth worker process is run as root by default, so that it can access
# /etc/shadow. If this isn't necessary, the user should be changed to
# $default_internal_user.
user = $default_internal_user
}

 

编辑 /etc/dovecot/conf.d/10-ssl.conf,管理IMAP服务的链接安全性:

ssl = required

ssl_cert = </etc/ssl/private/<cert_file>
ssl_key = </etc/ssl/private/<key_file>

ssl_dh_parameters_length = 2048

ssl_protocols = !SSLv2 !SSLv3 TLSv1 TLSv1.1 TLSv1.2

ssl_cipher_list = DEFAULT:!EXPORT:!LOW:!MEDIUM:!MD5

ssl_prefer_server_ciphers = yes

其中将dh key长度设置为了2048,启动dovecot 后 dovecot 会去生成新的 ssl-parameters,如果设备性能不高的话会花费很长时间。如果ssl-parameters出现问题可以删除旧的 ssl-parameters (/var/lib/ssl-parameters.dat) 之后重启dovecot。

 

编辑 15-lda.conf,配置LMTP参数:

postmaster_address = [email protected]

hostname = --domain.com--

 

防火墙:

使用Iptables的话,需要打开以下端口:

参考设置(没启用POP3):

#imap/imaps
-A INPUT -p tcp -m tcp --dport 143 -m conntrack --ctstate NEW -j ACCEPT
-A INPUT -p tcp -m tcp --dport 993 -m conntrack --ctstate NEW -j ACCEPT
#smtp/smpts
-A INPUT -p tcp -m tcp --dport 25 -m conntrack --ctstate NEW -j ACCEPT
-A INPUT -p tcp -m tcp --dport 465 -m conntrack --ctstate NEW -j ACCEPT

重新加载iptables,postfix,dovecot,之后服务器应该就可以用了。

 

配置域名:

首先要有个域名,添加MX记录,SPF记录,MX记录用于在接收邮件的时候解析,这样其他邮件服务器可以知道给你投递邮件的话投递到什么地方,SPF记录用来判断一个IP是否有权利以此域名的名义发送邮件,防止别人冒充你。

MX记录为服务器IP即可。

TXT记录可以参考:

v=spf1 mx ~all

意为允许MX记录中的IP地址使用你的域名发信,其他地址均不能以你的名义发信。

 

完成:

邮件服务器建好了,启动服务,之后可以用Thunderbird之类的客户端登陆。

 

TODO:

反垃圾,Docker,性能,Header checker