Proxmox VE(PVE)的Web管理界面默认使用HTTPS 8006端口,有点强迫症的我就想将它改成标准的 443 端口。传统方案需修改PVE配置,但每次PVE升级后都会失效。本文通过Linux内核级工具IPVS实现流量转发,无需改动PVE系统本身,系统升级后也对其没有影响。

IPVS技术简介

IPVS(IP Virtual Server)是Linux内核内置的传输层负载均衡器,常用于构建高性能服务器集群。

操作步骤

1. 安装 ipvs 管理工具

sudo apt update 
sudo apt install -y ipvsadm

2. 配置端口转发规则

10.0.0.9 替换成你 pve 的 IP
ipvsadm -A -t 10.0.0.9:443 -s rr
ipvsadm -a -t 10.0.0.9:443 -r 10.0.0.9:8006 -m
ipvsadm -A -t 10.0.0.9:80 -s rr
ipvsadm -a -t 10.0.0.9:80 -r 10.0.0.9:8006 -m

3. 查看规则

ipvsadm -Ln
# 预期输出
IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
  -> RemoteAddress:Port           Forward Weight ActiveConn InActConn
TCP  10.0.0.9:80 rr
  -> 10.0.0.9:8006                Masq    1      0          0         
TCP  10.0.0.9:443 rr
  -> 10.0.0.9:8006                Masq    1      0          0  

验证是否第一次登陆:

cookie 中的 deviceid

初始化,连接服务器后,发送:

{
    "lwp": "/reg",
    "headers": {
        "cache-header": "token app-key did ua vhost wv",
        "vhost": "WK",
        "ua": "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36 OS(windows/6.1) Browser(chrome/63.0.3239.132) DingWeb/3.6.0 LANG/zh_CN",
        "app-key": "85A09F60A599F5E1867EAB915A8BB07F",
        "wv": "im:3,au:3,sy:4",
        "did": "117851a2126f40aaa2f1a3e2df7c00dc",
        "mid": "804b0002 0"
    },
    "body": null
}
  • 主要字段的数据来源(JS实现):
  1. cache-header: 固定
  2. vhost:固定
  3. ua: 当前浏览器的 navigator.userAgent
  4. app-key:固定为 85A09F60A599F5E1867EAB915A8BB07F
  5. wv: 固定
  6. did: cookie 没有 deviceid 时,由客户端生成,详见下面的 getDid(uuid.v4标准);否则不传该字段,但连接 WebSocketHTTP 请求头应该有包含 Cookie:deviceid=7fc0930dba774fcfa7e1d7aac56fabcd; deviceid_exist=true;
  7. mid: genMid() + " 0"
  • 涉及到的代码段:
var i = 0
  , r = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f"];

function genMid() {
    var e = Math.floor(Math.random() * Math.pow(2, 16))
      , t = i++
      , n = "";
    return n += r[e >> 12 & 15],
    n += r[e >> 8 & 15],
    n += r[e >> 4 & 15],
    n += r[15 & e],
    n += r[t >> 12 & 15],
    n += r[t >> 8 & 15],
    n += r[t >> 4 & 15],
    n += r[15 & t]
};

function uuid_v4 () {
    var o = [], i = [], l = {};
    for (var e, t = 0; t < 16; t++)
        0 === (3 & t) && (e = 4294967296 * Math.random()),
            o[t] = e >>> ((3 & t) << 3) & 255;

    for (var u = 0; u < 256; u++)
        i[u] = (u + 256).toString(16).substr(1)


    var n = 0;
    return i[o[n++]] + i[o[n++]] + i[o[n++]] + i[o[n++]] +
        "-" + i[o[n++]] + i[o[n++]] + "-" + i[o[n++]] + i[o[n++]] + "-" + i[o[n++]] + i[o[n++]] + "-" +
        i[o[n++]] + i[o[n++]] + i[o[n++]] + i[o[n++]] + i[o[n++]] + i[o[n++]]
}

function getDid () {
    return uuid_v4().replace(/\-/g, "");
}
对应PHP代码:
function getMid () {
    static $i = 0;

    $r = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'];

    $e = rand(1, 65535);
    $i++;

    return $r[$e >> 12 & 15] .
           $r[$e >> 8 & 15] .
           $r[$e >> 4 & 15] .
           $r[15 & $e] .
           $r[$i >> 12 & 15] .
           $r[$i >> 8 & 15] .
           $r[$i >> 4 & 15] .
           $r[15 & $i];
}

function uuid_v4() {
    return sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',

      // 32 bits for "time_low"
      mt_rand(0, 0xffff), mt_rand(0, 0xffff),

      // 16 bits for "time_mid"
      mt_rand(0, 0xffff),

      // 16 bits for "time_hi_and_version",
      // four most significant bits holds version number 4
      mt_rand(0, 0x0fff) | 0x4000,

      // 16 bits, 8 bits for "clk_seq_hi_res",
      // 8 bits for "clk_seq_low",
      // two most significant bits holds zero and one for variant DCE1.1
      mt_rand(0, 0x3fff) | 0x8000,

      // 48 bits for "node"
      mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
    );
}

function getDid () {
    return str_replace('-', '', uuid_v4());
}

服务器响应注册消息:

{
    "headers": {
        "mid": "804b0002 0",
        "sid": "ac1d503a5a55bf1454bb2724ef29f2f5882550192a20"
    },
    "code": 200
}

输入密码登陆:

{
    "lwp": "/r/Adaptor/LoginI/login",
    "headers": {
        "mid": "2634000f 0"
    },
    "body": [
        {
            "title": "Windows 7 Web", // 最长 20 字节
            "model": "Windows 7",
            "token": "C1515568916640551275078381515568916640194"
        },
        "+86-13900000000", // 登陆手机号
        "这是明文密码",
        "85A09F60A599F5E1867EAB915A8BB07F", // app-key
        null
    ]
}

服务器响应登陆成功的消息:

{
    "headers": {
        "dt": "j",
        "mid": "2634000f 0",
        "sid": "ac1d503a5a55bf1454bb2724ef29f2f5882550192a20"
    },
    "code": 400,
    "body": {
        "reason": "你在新的设备登录钉钉,为了保障你的账户安全,需要使用短信验证码确认",
        "code": "14001"
    }
}

客户端要求发送验证码:

{
    "lwp": "/r/Adaptor/LoginI/sendSmsCode",
    "headers": {
        "mid": "691e0010 0"
    },
    "body": [
        "+86-13900000000" // 登陆手机号
    ]
}

输入验证码:

{
    "lwp": "/r/Adaptor/LoginI/tokenLogin",
    "headers": {
        "mid": "b75d0023 0"
    },
    "body": [
        {
            "title": "Windows 7 Web",
            "model": "Windows 7",
            "token": "C1515568916640551275055381515568916640194"
        },
        "+86-13900000000", // 登陆手机号
        "1111", // 短信验证码
        "85A09F60A599F5E1867EAB915A8BB07F", //app-key
        "0",
        null
    ]
}

用户登陆成功,服务器响应:

{
    "headers": {
        "dt": "j",
        "mid": "b75d0023 0",
        "sid": "ac1d503a5a55bf1454bb2724ef29f2f5882550192a20"
    },
    "code": 200,
    "body": {
        "nickPinyin": "qi",
        "syncProtocol": false,
        "tokenId": "B:45cde49ed06c55fc758fbe1b4a7e7777",
        "openId": 33606666,
        "secretToken": "8reAacn5g=",
        "accessToken": "Gs/Go7jpo=",
        "expiredTime": 1516865322664,
        "nick": "昵称",
        "domain": "dingding",
        "tmpCode": "33405583_E755ADF8-E5A3-42DA-A60C-5555FCFE30B7",
        "appKey": "85A09F60A599F5E1867EAB915A8BB07F",
        "timestamp": "16124915505A584255406478",
        "userProfileExtensionModel": {}, 
        "refreshToken": "c2/kt52S8pvemA=="
    }
}

使用上一步得到的 body.tmpCode 请求服务器设置 cookie

GET https://static.dingtalk.com/media/setCookie?code=33405583_E755ADF8-E5A3-42DA-A60C-5555FCFE30B7&callback=__jp0

订阅消息

{
    "lwp": "/subscribe",
    "headers": {
        "token": "Gs/Go7jpo=", // 登陆成功后服务器推送消息中的 body.accessToken
        "sync": "0,0;0;0;",
        "set-ver": "0",
        "mid": "ab030024 0"
    }
}

服务端响应订阅:

{
    "headers": {
        "reg-sid": "ac1d503a5a55bf1454bb2724efdfdfdfdf5882550192a20",
        "reg-uid": "33400003@dingding",
        "mid": "ab030024 0",
        "uuid": "ggHaACRjNjc2ZmQ3jU5LTQ0MTEtOGQwMS02YWRjYODMCsTMzNDA2NjgzQGRpbmdkaW5n",
        "real-ip": "61.139.59.61",
        "sid": "ac1d503a5a55bf1454bb2724ef29f2f5882550192a20"
    },
    "code": 200
}

获取状态

{
    "lwp": "/r/Sync/getState",
    "headers": {
        "mid": "e4d10004 0"
    },
    "body": [
        {
            "pts": 0,
            "highPts": 0,
            "seq": 0,
            "timestamp": 0,
            "tooLong2Tag": ""
        }
    ]
}

服务端响应获取的状态结果:

{
    "headers": {
        "dt": "j",
        "mid": "e4d10004 0", // 和上面获取状态时发送的 mid 一样,表明是响应的数据
        "sid": "0b8319955a57146440a3207de6bf969793baf23b6490"
    },
    "code": 200,
    "body": {
        "tooLong2Tag": "center,1",
        "topic": "sync",
        "highPts": 1515655826286000,
        "pts": 1515623661092000,
        "seq": 0,
        "timestamp": 1515656293128
    }
}

猜测是获取未读消息,经过这两步后,新消息才会主动通知给客户端

{
    "lwp": "/r/Sync/getDiff",
    "headers": {
        "mid": "2d7b0013 0"
    },
    "body": [
        //这里的数据就是上一步响应的结果
        {
            "tooLong2Tag": "center,1",
            "topic": "sync",
            "highPts": 1515655826286000,
            "pts": 1515623661092000,
            "seq": 0,
            "timestamp": 1515656293128
        }
    ]
}

确认收到未读消息

{
    "lwp": "/r/Sync/ackDiff",
    "headers": {
        "mid": "022b0015 0"
    },
    "body": [
        // 还是上面获取状态时的响应结果
        {
            "tooLong2Tag": "center,1",
            "topic": "sync",
            "highPts": 1515655826286000,
            "pts": 1515623661092000,
            "seq": 0,
            "timestamp": 1515656293128
        }
    ]
}

标记消息为已读

{
    "lwp": "/r/IDLMessageStatus/updateToView",
    "headers": {
        "mid": "6e900034 0"
    },
    "body": [
        "33666666:3833666665", // 上一步得到的 发送者uid:接收者uid
        49827229684 // 上一步得到的消息ID
    ]
}

RocketMQ

  1. 关于事务,RocketMQ会定时(1分钟)回查生产者发送的事务消息,如果是 prepared 状态则会向 Producer 发起 CheckTransaction 请求,根据返回结果来决定是回滚还是继续执行
  2. 消费超时会一直重试

RabbitMQ

  1. 消息发送到交换机,然后根据消息的 routing_key 和交换机类型,自动发送到相应的 队列
  2. 若交换机没有相关联的队列,或关联的队列不存在,则消息会 被抛弃
  3. 所谓的几种类型的交换机,其实只是交换机查找对应队列的方式:

    • Direct: 直连,将消息发送到 路由键 同名的队列上(不能指定交换机名称) -- 只全等判断路由键,不关心 绑定 关系(一对一)
    • Fanout: 扇形,将消息发送到 与当前交换机有绑定关系 的队列上 -- 只判断 绑定 关系,不关心 路由键(一对多)
    • Topic: 主题,将消息发送到 消息的 路由键 与 队列绑定的 路由键 相匹配的队列上 -- 在 绑定 关系基础上再匹配 路由键 (多对多)

最近打算使用Git来管理源码,因此安装了Gogs作为管理系统,装好后发现无法使用SSH方式。经排查发现是SSH无法免密码登陆,按免密码登陆流程设置好以后,发现 ssh git@git.domain.com 无法免密码,而 ssh root@git.domain.com却是可以的

按照网上说的,把 /home/git/home/git/.ssh/home/git/.ssh/authorized_keys都重新设置了一遍权限,但问题依然存在

/etc/ssh/sshd_config里的日志级别改为debug后,查看日志文件:/var/log/secure 发现还是提示/home/git/.ssh/authorized_keys的权限不足:

Mar 17 10:32:02 web12 sshd[18767]: debug1: trying public key file /home/git/.ssh/authorized_keys
Mar 17 10:32:02 web12 sshd[18767]: debug1: Could not open authorized keys '/home/git/.ssh/authorized_keys': Permission denied

后来,网上看到一篇类似问题的文章里有提到日志/var/log/audit/audit.log
于是在里面看到:

type=AVC msg=audit(1489721077.234:763): avc:  denied  { search } for  pid=20284 comm="sshd" name="/" dev=sdb1 ino=2 scontext=unconfined_u:system_r:sshd_t:s0-s0:c0.c1023 tcontext=system_u:object_r:file_t:s0 tclass=dir
type=SYSCALL msg=audit(1489721077.234:763): arch=c000003e syscall=2 success=no exit=-13 a0=7fd8fb6d75e0 a1=800 a2=1 a3=4000 items=0 ppid=20059 pid=20284 auid=0 uid=0 gid=0 euid=501 suid=0 fsuid=501 egid=501 sgid=0 fsgid=501 tty=(none) ses=11 comm="sshd" exe="/usr/sbin/sshd" subj=unconfined_u:system_r:sshd_t:s0-s0:c0.c1023 key=(null)
type=AVC msg=audit(1489721077.234:764): avc:  denied  { search } for  pid=20284 comm="sshd" name="/" dev=sdb1 ino=2 scontext=unconfined_u:system_r:sshd_t:s0-s0:c0.c1023 tcontext=system_u:object_r:file_t:s0 tclass=dir

结合文章的问题,突然想到,我的服务器的/home目录也是外挂的另一个硬盘,那问题也就是出在 /home目录的context上了

然后运行:

restorecon -r -vv /home/

完成后,SSH免密登陆成功

  1. 使用 ssh-keygen 生成密钥,默认参数就行,执行后会在 ~/.ssh/ 目录下生成两个文件:id_rsaid_rsa.pub
  2. 手工将 id_rsa.pub 的内容添加到要登陆到的 目标 服务器的 ~/.ssh/authorized_keys 文件。或使用以下方法自动复制(192.168.1.2为目标机器):

    ssh-copy-id root@192.168.1.2
  3. 使用 ssh root@192.168.1.2 登陆目录服务器
  4. 登陆后会在本机的 ~/.ssh/ 目录下生成 known_hosts 文件,删除此文件的对应行后,再次登陆会重新确认是否免密

多私钥管理

  1. 启动 ssh-agent:

    #注意反引号
    eval `ssh-agent`
  2. 添加私钥:

    ssh-add ~/.ssh/aliyun #私钥文件

github SSH 登陆

将上面 id_rsa.pub 的内容复制到 Settings/SSH and GPG keys 就行

提示

如果希望ssh公钥生效, 目标服务器 需满足至少下面两个条件:

  1. ~/.ssh 目录的权限必须是700
  2. ~/.ssh/authorized_keys 文件权限必须是600