月度归档: 2020 年 4 月

  • C++17 std::variant 简单使用

    std::variant应当是用来替代union来使用的,后者从C时代就有了,但是它缺乏类型安全的保障。boost库很早之前就有了实现(其实从C++11开始,感觉boost不知道多少东西被借鉴走了)。
    std::variant的类长这个样子:

    template <class... Types>
    class variant;
    

    Usage

    使用起来也是比较方便的

    std::variant<int, double> v1;
    v1 = 1; // activate the "int" member
    assert(v1.index() == 0);
    assert(std::get<0>(v1) == 1);
    
    v1 = 3.14; // activate the "double" member
    assert(v1.index() == 1);
    assert(std::get<1>(v1) == 3.14);
    assert(std::get<double>(v1) == 3.14);
    
    assert(std::holds_alternative<int>(v1) == false);
    assert(std::holds_alternative<double>(v1) == true);
    
    assert(std::get_if<int>(&v1) == nullptr);
    assert(*std::get_if<double>(&v1) == 3.14);
    

    上面代码里的几个好用的constexpr函数:

    • std::get<>:可以传入index或是type。如果index或者类型不对的话,会跑出std::bad_variant_access异常;
    • std::get_if<>:这个是不抛异常的版本。注意参数和返回值类型都是指针——如果没有有效元素,那就会返回nullptr;
    • std::holds_alternative<Type>:判断一个Type在不在定义的variant参数类里面;

    Visiting variants

    如果要根据当前存储的类型来对variant对象做一些操作,第一会想到写一些if语句

    if (std::holds_alternative<int>(v1) == true) {
      // handle int value
    }
    else {
      // handle double value
    }
    

    但是如果variant的可选类型比较多的话,定义一个Visitor类会有助于按类型来执行操作,逻辑会显得清晰

    struct Visitor {
      double operator() (double d) { return d; }
      double operator() (int i) { return double(i); }
    };
    
    void show(std::variant<double, int> v) {
      std::cout >> std::visit(Visitor{}, v) << std::endl;
    }
    
    void test() {
      show(3.14);
      show(1);
    }
    
  • C++17 std::generate的使用

    std::generate是“遍历——执行”工具函数的一种

    #include <algorithm>
    #include <iostream>
    #include <string>
    #include <vector>
    
    int main()
    {
      std::vector<std::string> v(4);
      std::generate(v.begin(), v.end(), [i=0]() mutable {
          return ++i % 2 ? "hello" : "world";
      });
      
      for (auto&& s : v) {
        std::cout << s << " ";
      }
      return 0;
    }
    

    上面的函数输出是hello world hello world .

    std::generate(beginIt, endIt, generator)函数接受起止的迭代器以及一个生成方法。

  • C++17 新特性—— if constexpr

    看代码说话

    template <typename Iterator>
    auto distance(Iterator begin, Iterator end) {
      using Traits = std::iterator_traits<Iterator>;
      if constexpr (std::is_base_of_v<std::random_access_iterator_tag, typename Traits::iterator_category>) {
        return end - begin;
      } else {
        auto result = typename Traits::difference_type();
        for (auto it = begin; it != end; ++it) {
          ++result;
        }
        return result;
      }
    }

    上面的代码用来判断两个iterator之间的距离。主要逻辑是判断iterator是否是可随机访问的iterator然后作分支处理,可以随机访问的迭代器直接把iterator相减。
    constexpr if语句中,条件的值必须是可按语境转换到 bool 类型的经转换常量表达式。若其值为 true,则舍弃 false分支语句(若存在),否则舍弃 true分支语句。有意思的一点是,被舍弃语句中的 return 语句不参与函数返回类型推导,也就是说两个分支语句中的return推倒出来的类型允许不一样。

    如果没有C++17这种怎么写呢?就是比较麻烦。
    一可以加一个参数,但是每个tag写一个

    template <typename Iterator>
    auto distance(Iterator begin, Iterator end, std::bidirectional_iterator_tag); // one for each tag type

    或者用SFINAE写两个

    template <typename Iterator>
    auto distance(Iterator begin, Iterator end, 
                  typename std::enable_if_t<std::is_base_of_v<一大坨>>* = nullptr);
  • std::unique_ptr 关于智能指针的种种

    这个域名也是突发奇想买到的,作为unique-ptr.com,为了对得起这个名儿,那就说一下智能指针算了。

    前传。裸指针和auto_ptr

    裸指针就是高度自治(要自己管)的指针,在符合基本法(误)的前提下,使用裸指针的人可以为所欲为。裸指针的一个问题是,它有点不符合现代C++内存管理的指导思想,使用者必须对它的生命周期负责,否则有可能发生:

    • 内存泄漏(memory leak):如果new一大片内存但是由于代码复杂到头来忘了释放,就有可能耗尽内存资源;内存泄漏还有一种情况:如果申请内存做事情做了一半抛出了异常——代码跑到了exception的花括号里之后,原来申请的内存直接没办法拿到并且释放;
    • 悬挂指针(dangling reference):指针已经被删除了,但其内存仍然在使用中;另外还有双重删除(double delete)的问题,当程序的某部分要删除一个已经被删除的指针时,即可出现这种情况;还有一种,指针所指的内存已经失效了(比如出了作用域了),但是后面的代码还用着指针;

    应该是为了缓解这类问题,C++ 98标准搞出来了个智能指针:auto_ptr。相当于加了一个“管家”,在对象的内部保存那个裸指针,在管家析构的时候自动释放裸指针的内容,如果赋值的话那就把裸指针的所有权转交给新的管家。
    但是由于移动(move)语义是C++11才有的,auto_ptr在实现的时候,如果在两个智能指针对象之间作赋值(operator=)或者拷贝构造,它的参数都是非const类型的。为啥呢?因为它是会调用original_auto_ptr.release()方法拿到原始裸指针。
    这个有点无奈的操作是为了实现指针所指对象的“占有权”管理:两个auto_ptr不允许管理同一个目标裸指针。在“拷贝”的时候,相当于做了“主权移交”。

    Copying an auto_ptr copies the pointer and transfers ownership to the
    destination: both copy construction and copy assignment of auto_ptr
    modify their right hand arguments, and the “copy” is not equal to the
    original.
    https://en.cppreference.com/w/cpp/memory/auto_ptr

    这种操作并不符合常人对“拷贝“的理解,更像是”剪切“操作。且正是因为这个问题,切不可把auto_ptr放到stl容器里面,否则会得到各种编译错误。典型案例是这个:https://stackoverflow.com/questions/111478/why-is-it-wrong-to-use-stdauto-ptr-with-standard-containers
    另外由于析构的时候是用delete,auto_ptr里也不能放指针数组(那种需要用delete[]).

    unique_ptr

    不同于auto_ptr,unique_ptr 的赋值运算符只接受典型地由 std::move 生成的右值。刚才说了,“右值”是C++11移动语义的产物,它比较好地解决了主权移交时的尴尬问题。这怎么说呢,一个智能指针的命运啊,当然要靠自我奋斗,但是也要考虑到历史的行程
    回顾看之前说的裸指针的两个问题:

    • 内存泄漏:由于析构函数里会delete管理的裸指针对象,所以在跑出对象作用域的时候,编译器会帮我们解决好问题,不用操心了;
    • 悬挂指针:缓解,但不能完全避免。

    一个例子是

    #include <iostream>
    #include <memory>
    #include <string>
        
    int main()
    {
      std::unique_ptr<std::string> str_ptr;
      {
          std::string tmp("bbbababa");
          str_ptr.reset(&tmp);
          std::cout << str_ptr.get() << std::endl;
          std::cout << *str_ptr << std::endl;
      }
      std::string tmp("xxxxxxx");
      std::cout << str_ptr.get() << std::endl;
      std::cout << *str_ptr << std::endl;
      return 0;
    }
    // Output:
    //   0x728562b4f060
    //   bbbababa
    //   0x728562b4f060
    //   xxxxxxx
    

    此外,数组的问题也通过特化std::unique_ptr<T[]> 得到了解决.
    可能有些同学会有性能方面的顾虑,觉得智能指针会比裸指针的操作性能差一点。坦白讲这个是有可能的,但是在实际使用的过程中,往往作profiling后会发现性能出现瓶颈的地方几乎不是智能指针。作为unique_ptr来讲,它不需要维护额外的状态,只是对象的创建和销毁要作一些特殊处理,平时使用的过程中完全可以当作裸指针来用。真正可能要担心性能问题的,是shared_ptr这一类允许共享所有权的智能指针。

  • 因子 收益率 因子暴露 因子载荷 什么东东?

    作为非金融科班的学生,发现有一些术语其实看上去不知所以然,但是理解了之后就会感叹一下:

    这tm不就是xxx么~

    记得刚开始搞量化的时候,经常碰到的词汇就是各种因子,以及因子暴露(factor exposure)因子载荷(factor loading)。然后我就去百度知乎Google搜啊,立马出来一大堆长篇大论告诉你它的由来,各种多因子模型。窃以为对于刚入门的我,最想知道的就是这个东西是什么意思,然后才是它背后的模型。

    那么问题来了

    因子暴露(aka. 因子载荷)是什么?因子暴露就是因子载荷。假设你的投资组合里有p只股票,并且你有m个因子,那么想象因子暴露是一个p * m的矩阵——每只股票在每个因子上的因子值。打个比方,如果你的因子是32个行业分类,那么这个矩阵的元素非0即1——1代表股票属于这个行业. 如果假定一只股票只能属于一个行业,那么这个矩阵的每一行都只可能有一个1,其余都是0.
    这个东西一般是归一化或者说标准化过的值,不然会无法做统计分析。

    那么,这东西能拿来做什么?

    结构化风险模型的假设是,投资组合的收益率是可以由下列公式解释

    r = X * b + u

    如果依旧假设有p只股票,m个因子,投资窗口时间长是l
    其中

    • r是投资组合收益,p*l矩阵
    • X是因子暴露,p*m矩阵
    • b是因子收益率,m*l矩阵
    • u是特异收益(又叫残差收益),相当于前面X*b不能解释的一部分,p*l矩阵
      简而言之就是一个投资组合的收益可以被几个因子来解释,解释不了的部分另归另说。

    如果假定个股残差收益是互不相关的,那么

    1. 比如我假定股票的收益(r)是由不同的行业因子决定的,那么只要有不同行业ETF的收益率(上面的b),我就可以拿来做回归分析得知股票对应的行业(上面的X);
    2. 再如假定股票的收益(r)和股票的Barra风格因子值(X)是已知的,那么也可以通过回归分析的方法拿到每个风格因子的收益率(b);

    题外话:感觉这个有点像期权定价里的implied volatility和realized volatility。如果是从underlying的价格用Black-Scholes公式算出来的叫隐含波动率,而已实现波动率可以通过观测期权价得到。上面通过ETF收益率算出来的因子暴露有点类似,其实股票的行业归属也可以从基本面的数据里拿到……

  • 记一次gtest EXPECT_DEATH遭遇sigsegv的debug过程

    the crash

    实习生小哥上周突然发现单元测试在dev机器上跑不起来了,–verbose一看发现是gtest报错Failure death test:
    gtest_expect_death_failure.png

    现象其实是所有写了EXPECT_DEATH的单元测试都挂掉了,感觉问题并不简单~ 问了一下边上的几位最近代码有什么变动,一说是用了个新的计算库。

    gdb

    gdb一波,除看代码发现这个测试的原理是起一个子进程,然后把stderr写到一个临时文件里,子进程退出后,从临时文件里读取log信息,和预期值比对。发现第一现象是stderr没抓到,结果在找stderr为什么没抓到的邪路上走了很久,发现根本没产生stderr:/tmp下面写的文件,大小是0字节。

    然后继续gdb,由于是要跟踪子进程,要把选项打开:

    set detach-on-fork off
    set follow-fork-mode child

    run了一下,看到了一点端倪:其实在fork之后进程就sigsegv了,而且居然是在动态加载glibc里的chdir函数过程中挂的。查了一下,机器上的glibc已经至少一年没有动过了,因此基本排除是glibc自己有问题。

    然后开始各种翻来覆去的看,先看了几个local变量,发现一切都显得那么完美,根本没有任何内存值被搞烂的迹象。接着只能看挂掉的现场了,info registers看到寄存器的值如下:
    gdb_info_registers.png

    突然发现红框处的rsp地址有点太“整”了,google了一下,确认这个地址应该是个用户态栈地址的下界。然后查看一下当前的指令,发现是一条push %ebx,也就是说还准备压栈————挂掉的清晰一些了,其实是栈地址空间不够了,也就是传说中的stack overflow。这也是为什么gtest能够看到程序挂掉,但是拿不到预期的错误信息的原因。

    the stack overflow

    那么,为什么栈空间不够了呢?翻了一下frame 0里的$rsp值,发现在0x7fc0附近,和刚才看到顶上的$rsp=0x7000相差了4032,也就是估计这个程序栈总的大小在4kb。查了一下ulimit -s有32767,诶? 于是接着看代码,发现gtest-death-test.cc里的做法, ExecDeathTestSpawnChild()里写道

    • mmap一个stack,它的大小是getpagesize() // ==4KB
    • clone一个新的线程来跑测试例,用的这个新创建出来的stack

    真相大白了。解决方法是mmap的时候分配多一点的栈空间。至于为什么到了上周才触发,大概是因为.so又多了几个吧~

    后记

    当然,其实这个bug早就有fix了,只是我是后来通过搜”ExecDeathTestChildMain stack overflow”这个关键词才找到的。之前搜了半天EXPECT_DEATH没什么结果~
    相关的PR有:

    所以说thirdparty code还是要偶尔更新一下的呀~

  • ASUS RT-AC1900P RT-AC68U 强行刷写Merlin方法

    最新的华硕固件(至少是AC68系列)已经开始阻止非官方固件的刷入,具体表现是能够上传固件,但是路由器重启之后其实什么都没发生变化,也并没有看到什么提示。
    对应这种情况,其实可以利用ASUS Firmware Restoration Tool强行刷写固件,也就是在恢复模式下更新固件。鄙人亲自试验表明,原有的系统配置在用这个工具强刷之后并不会改变(甚至Mesh节点都能继续连接)。

    连入救援模式并上传固件的方法,参考官方教程。主要步骤就是:

    1. 拔除路由器电源;把路由器LAN口用网线接入到电脑上,电脑上手工配置有线网卡的IP地址为192.168.1.10,子网掩码为255.255.255.0其余留空;
    2. 按下RESET键的同时接入路由器电源,保持RESET按下状态直到路由器的电源灯开始有规律闪烁;
    3. 打开下面的工具,直接上传Merlin固件;
    4. 上传成功后会自动重启的;

    固件更新程序下载(需要Windows系统)
    链接:https://pan.baidu.com/s/1zApE2OiOq7mX6bGgWGpNvA
    提取码:3idq

  • 联通8元全国流量王保号套餐变更指南

    起因是有了另外一张流量卡, 所以目前的冰激凌套餐也就不需要了,为了更换这个套餐,最终还是去了实体营业厅办理,好在一次成功,遂发个分享。众所周知联通早已取消了所有5元低保套餐,什么小天神之类的早已无法办理,因为她们不属于“在售套餐”了。目前联通最低消费的是这个4G全国流量王8元套餐,内容如下:

    全国流量王8元套餐详情:

    • 30分钟语音
    • 全国流量200MB

    套餐外

    • 全国流量:执行流量放心用,即按照0.1元/MB累计至10元,不再收费,直至1GB,按照10元/GB计费
    • 全国语音:0.15元/分钟 国内短、彩信:0.1元/条赠送来电显示;全国被叫免费

    10010客服——失败

    之前看这篇文章有先例,可以不走线下途径办理,所以打电话一试。
    打10010客服,人工服务后,对方明确表示有这个套餐,但是电话客服给的权限只能申请最低19元的套餐,再往下不行了。好说歹说回复我“帮我这边申请一下,不一定能成功”。然后过了3天,根本没有回访电话,于是我觉得走非投诉途径的话不去实体店是解决不了问题的。

    自营售后营业厅——成功

    务必前往自营的联通营业厅。当然跟业务员沟通需要技巧:业务员面露难色跟我说她能帮我把其他的阻止我变更套餐的东西退了,但是需要我自己打电话去10018或10010申请。
    鄙人打10018直接无反应,然后10010人工坐席全忙——这是为啥呢——现在想来可能是业务员把我的高增值业务退订光后,我就挪到在她们电话客服优先级队列的最底部了。然后我一边等人工坐席一边继续跟业务员argue,说之前打人工客服问过,对方明确表示一定要到营业厅办理,不信可以查电话客服记录。无语了1分钟后,业务员终于点击了鼠标完成了套餐变更,之前“没有权限”的说法也默默消失了~事实证明自有营业厅的客服人员是完全可以当场办理这个套餐变更的,只是她不想那么轻易把这个套餐变更的坏KPI挂在自己头上罢了。
    现在想来,如果当场发送CXXZ查询携转,并且以携号转网要挟,是不是态度会稍微好一点。

    建议

    结合以往经历,想要成功变更套餐,两条路:

    1. 工信部投诉:https://dxss.miit.gov.cn/
    2. 前往自有营业厅,不要怂,直接怼
  • Windows 10 HpyerV OpenWRT 软路由安装 + OpenClash 安装 全教程

    本教程详细说明如何在Windows10下经由Hyper-V技术安装OpenWRT软路由。物理机器配置如下:

    • Intel Celeron J1900
    • 4GB DDR3
    • 4口千兆网卡
      淘宝上这种配置的机器遍地都是,J1900对付软路由绰绰有余了,其实跑Win10浏览网页也不在话下~ 施工结果如下图:

    Win10_OpenWRT.JPG

    I. Hyper-V虚拟交换机配置

    安装前请确保你的 CPU 支持硬件虚拟化(Hyper-V)技术并且已通过BIOS启用

    启用Windows的Hyper-V功能

    1. 进入“控制面板”,然后依次点击 程序 – 程序和功能
    2. 点击「启用或关闭 Windows 功能」,打开 Windows 功能管理窗口,并勾选「Hyper-V」,点击“确定”
      win10_enable_hyperv.JPG

    配置虚拟交换机

    打开开始菜单,搜索或找到“Hyper-V管理器”,点击右侧栏里的“虚拟交换机管理器”链接。本人的物理机器有4个网口,配置的目标是网口1连入互联网,网口2网口3网口4连入下属设备。
    我们首先创建用于外部网络的虚拟交换机,这个交换机相当于你路由器的 WAN 接口,用于将路由器连接到外部网络:
    win10_hyperv_virtual_switch.JPG

    1. 点击“新建虚拟网络交换机”;
    2. 右侧给他起个名字,我是叫它ToExternal,或者随便叫什么“外部网络”也都行;
    3. 下面的“连接类型”选中“外部网络”,然后选择你的物理网口1对应的设备名称。注意这里需要勾选“允许管理操作系统共享此网络适配器”
    4. 点击“应用”
      而后需要增加一个内部网络适配器,是用于给虚拟化宿主机提供来自软路由的网络的。

    win10_hyperv_vswitch_internal.JPG

    接下来给余下的准备做路由器LAN口的网口2网口3网口4创建虚拟交换机。这边千万注意,选择“外部网络”并选定对应的物理网口后,不要勾选“允许管理操作系统共享此网络适配器”。这些网口,可以命名为LAN1, LAN2, LAN3之类。

    创建OpenWRT虚拟机

    虚拟机配置

    首先需要找一个x86_64的OpenWRT固件,这个网上多半可以搜索到。如果实在有点懒,可以找这里或者这里或者这里。请注意,部分固件是需要转换成Hyper-V能够识别的格式的,自行搜索下载“StarWind V2V Image Converter”这个软件转换成VHDX格式即可。

    下载完成后

    1. 打开 Hyper-V 管理器,点击「新建」-「虚拟机」,将会打开一个新的窗口。虚拟机的名称可以随意设置。
    2. 第二步会让选择虚拟机的代数,如果你下载的是uefi固件(一般名称里有uefi或gpt字样),可以选择第二代;否则谨慎起见选择第一代;
    3. 分配内存,个人分配了512MB固定内存,并开启动态内存允许分配到1GB。一般认为512M差不多了,甚至有说256都行了;
    4. 为虚拟机分配第一个网络适配器,选择之前创建的「内部网络」;这一步其实是向虚拟机增加了第一个网口,openwrt底下它会被默认分配为eth0;
    5. 为虚拟机创建硬盘,需要选择之前下载或者转换好的 OpenWRT 硬盘映像;

    完成初始化后,先不开机,点选右侧栏里的“设置”继续配置这个虚拟机:

    1. 点选“添加硬件”——网络适配器,将之前添加的所有外部网口依次加入,这边规定一下顺序,首先是那个通往外部互联网的网口1对应的,其次是剩下三个打算做LAN口的;注意这边添加网口的顺序影响到OpenWRT里eth的序号,按照这样设计,eth1对应的是我们接入互联网的WAN口,eth2-4对应的是未来的LAN口;
      win10_hyperv_openwrt_new_nic.JPG
    2. 如上图,点击所有添加的网口签名的“+”号——高级功能:勾选“启用MAC地址欺骗”。不开启此功能将导致之后软路由下的设备无法上网
      win0.JPG

    随后扩展硬盘

    1. 点击「IDE 控制器」下的「硬盘驱动器」,找到我们添加的 OpenWRT 虚拟硬盘,然后点击「编辑」;
    2. 在新的窗口中选择「扩展」,然后设置新的硬盘容量,推荐至少1GB
      tempsnip.jpg

    至此OpenWRT的配置基本告一段落,选择右侧栏里的“启动”即可启动虚拟机。启动后点击右侧栏的“连接”按钮可以进入虚拟机的shell,正常情况下进入虚拟机按下回车按钮,应该有OpenWRT图样。
    注意:OpenWRT默认的接入地址为192.168.1.1,如果光猫的路由器也是相同地址的话,会引起一些问题。这两者里有一个要改掉,OpenWRT这边可以通过修改/etc/config/network里lan口的内容实现(图中更改到了192.168.99.1):
    openwrt_change_lan_ip.jpg

    上图修改完之后,使用/etc/init.d/network restart命令可以重启网络服务。

    内部网络配置

    打开网络适配器设置,经由以下两种途径之一:

    • 依次点击 开始菜单 – 设置 – 网络和 Internet – 更改适配器选项;或,
    • 打开「网络和共享中心」,点击「更改适配器设置」

    找到名称为「vEthernet(内部网络)」的网络适配器(这边的“内部网路”是刚才给那个内部网路起的名字,比如我的叫Internal),然后点击「更改此连接的设置」以打开设置页面。
    点选「Internet 协议版本 4(TCP/IPv4)」,然后点击「属性」,然后按照下图配置:
    win10_internal_v_net.JPG

    请注意,这边192.168.X.1, 192.168.X.2X取决于刚才说的OpenWRT的内部IP地址。如果默认是192.168.1.???, 如果像我改过的话那就是192.168.99.???.

    点击「确定」保存所有设置。

    OpenWRT配置

    完成之前的步骤后,打开浏览器,并在地址栏中输入 192.168.1.1 (我是192.168.99.1)以打开 OpenWRT 管理页面. 默认的管理后台密码是:koolshare

    1. 依次点击侧边栏的 网络 – 接口;
    2. 你可以看到一个绿色的网络接口名称为「br-lan」。如果有两个多余的红色 WAN 和 WAN6 接口,请点击「删除」来删除它们;
    3. 修改LAN接口:在绿色的 br-lan 接口上点击「修改」,在新的页面中点击「物理设置」,然后将「接口」部分中选中eth0, eth2, eth3, eth4.如上文所述,这四个分别对应我们的内部网路、网口2,3,4。设置完成后点击下方的「保存并应用」,稍后页面将会自动跳转;
      openwrt-brlan.JPG
    4. 创建WAN接口:返回到接口管理页面后,点击「添加新接口」在新的页面中,为此接口名称设置为 WAN 或者你喜欢的名称,然后根据你的实际情况选择接口协议,如 PPPoE 或 DHCP 客户端。在「包括以下接口」部分勾选 eth1。
      openwrt-wan.JPG

    完成后点击「保存并应用」,进入详细设置。在「基本设置」中配合你的外部网络连接方式,如输入你的 PPPoE 账号等。我这边直接是宽带光猫拨号,所以很简单的用了“DHCP客户端”。然后点击「防火墙设置」,将新的接口防火墙区域设置为 WAN。WAN 区域使用红色作为标志。
    openwrt-wan-firewall.JPG
    全部完成后,点击「保存并应用」,将会返回到概览页面。正常情况下,能够连接互联网了:
    openwrt-interfaces.JPG

    修改后台密码

    维持默认的后台密码比较危险,可以用依次点击 系统 – 管理权,在“主机密码”里更换密码。

    OpenClash安装

    呵呵,其实这里是噱头。真正的安装步骤在https://github.com/vernesong/OpenClash 项目的README已经说的很清楚啦。下载地址在图中的”Release”底下也有。由于本网站的是“对大陆访客友好”的带有ICP备案的页面,所以这边只授人以渔。
    openclash-install.JPG
    下载其实可以直接在OpenWRT的命令行下面用wget命令操作,比如

    wget https://github.com/vernesong/OpenClash/releases/download/v0.37.2-beta/luci-app-openclash_0.37.2-beta_all.ipk

    随后使用opkg install luci-app-openclash_0.37.2-beta_all.ipk安装。如果有不懂的话,下面参考链接5里也有介绍。

    参考

    1. 如何通过 Hyper-V 部署 OpenWRT 软路由
    2. openwrt下如何修改默认ip地址
    3. Hyper-V 部署LEDE X64固件全教程
    4. 利用win10自带虚拟机hyper-v搭建软路由教程
    5. 合理的家庭网关方案 OpenClash, 并使用 SmartDNS 加速