分类: 网络

  • 我要看动画! | 公网samba服务抗干扰指南

    在过去一段相当长的时间里,我都在使用移动的宽带IPv6访问家中的移动网络,用Samba服务(工作在标准端口445,伏笔之一)观看硬盘上的动画,虽然两地距离上千公里,延迟和速度也都十分不错,相当稳定。

    后来由于一些原因我开始使用联通的宽带,光猫没有开启IPv6,无线质量也十分不理想,算是和家里的网络服务失联了一段时间。后来换了新的路由器,顺带把光猫改为了桥接,成功拿到了IPv6地址,就在我以为终于可以开始重新看动画片的时候,新的问题已经出现。

    PING延迟优秀,Web服务(工作在443端口,伏笔之二)速度正常,可Samba无论如何无法连接,甚至是“找不到网络名”这样莫名其妙的问题。由于事出诡异,我随即开始排查。

    首先排除本地设备的问题,用电脑连接手机流量(中国移动,伏笔之三),Samba服务正常,回到原来的网络,无法连接。我本以为是OpenWrt关闭了445端口,但查看后发现并没有这样的规则。索性把防火墙整个关掉也没有效果。至此,基本可以确认不是本地设备和网络的原因。

    遇事不决,开始上网求助。一通搜索无果,猛然想起来445端口这些年在相关报道上的“高风亮节”,于是开始怀疑运营商封堵了该端口,将Samba服务改用非标端口后,果然可以连上了。

    (这里不得不再提一句Windows的事情,时至今日微软居然还没有做出允许文件资源管理器挂载非标端口的SMB服务端功能,还需要进行本地的端口转发,实在让人难以评价。)

    连是连上了……这速度怎么不对劲呢。十分反常地稳定在1Mb/s左右的水平,最初还能让人以为是网络波动,时间久了也不得不开始怀疑了。难道是哪里出现了瓶颈吗?

    也许是最近折腾VPS折腾魔怔了,我首先想到的居然是Samba服务端的TCP拥塞控制算法,以为是IPv6跨网延迟导致的如此表现,但服务器在没有开启优化算法的情况下,HTTPS服务也没有出现显著降速,也许问题不在这里。

    IPv6的MTU问题?现有的网络将MTU值固定在了1300+,手动修改该值一通折腾后,也没有任何改善。

    SMB握手到了过时协议?把Windows对SMB1的支持关掉,再在服务端强制要求SMB3,用Get-SmbConnection查看握手协议也确实已经是最新了,为什么还是不行?

    运营商使用了包检测?在服务端上架起Xray,把SMB流量加密后再传输,依旧无果。(其实到这里我就该想到端口号的问题了(悲))

    我完全陷入了这个谜之问题的深渊,寤寐思服,辗转反侧。某天一个念头击中了我:都是基于TLS的加密,为什么HTTPS就可以?难道,不是443就不行?

    我不信邪,把服务挪到了用于HTTP的80,不行;再挪到同样加密的SSH的22,还是不行。

    我放弃一切幻想,把本来对外提供Web服务的Nginx关掉,让Xray用443,接收SMB流量。再测速,跑满宽带。

    我彻底破防了。

    (以下省略114514字节的对联通的真挚赞美)

    总结

    在公网环境下,为确保Samba服务的稳定可用,应在443端口加密传输。

    至于本来应该用443的正常HTTPS服务,就让它们去Xray后面待着吧。

  • 适用于个体驾驶员的信息高速公路指南

    越过长城,走向世界。
    Across the Great Wall we can reach every corner in the world.

    ——中国第一封电子邮件

    1987年9月14日,一封跨国电子邮件宣告了中国互联网的开端;1994年4月,中国正式接入国际互联网,成为地球村的一员。

    时至今日,光纤走入千家万户,无线覆盖五湖四海。连接网络,访问信息已然不再困难。互联网以前所未有的力度改变着生活,也潜移默化地催生着新的价值观念。对成长在信息时代的人们而言,屏幕上跳动的信息不再只是惊喜和趣味,而是深植于内心的牵挂;虚拟的账号ID不再只是单纯的口令,而是区别于现实角色的另一重自我。

    网络,是一种生活方式。


    在自己所珍视的地方,无人甘愿成为单纯的过客。我们几乎本能地想要留下痕迹,想要与人交往,想要得到同属于这个世界的其他个体的认可。

    一封信笺,一张邮票,千言万语便可传达。哪怕远在天涯海角,只要收件地址准确无误,那封承载着万千思绪的信纸,终会交给牵挂之人。

    生活在现世的我们总会有归宿。即使几度变迁,即使并不起眼,也总有一串字符,一行号码,能将我们与这个连接。

    而在光与电的世界里,我们又身在何方呢?

    输入ipconfig,看到的是并不为外界所知晓的四段数字;重连网络,得到的是刚刚还不存在于这世界上的晦涩字符。

    究竟如何,才能在嘈杂的电波之中,找到一片栖身之所呢?

    让我们来注册一个域名吧。即使需要金钱维持,即使找不到“永远”的选项,它也是独属于自己的,是独一无二的。

    接着,让我们凝望着信息高速公路上的车水马龙,然后自己也跃入其中。


    一切的基础:域名

    DNS服务器就像查号台,把自己的名字(域名)和号码(IP)加进去,就可供人查阅,就提供了被访问的可能。

    坏消息是,这是要钱的。好消息是,不同的顶级域价格相当悬殊,而功能几乎一样,并没有什么特别重要的理由,让我们在这上面投资过多。比起这笔开销,将自己的域名分发到全球的DNS服务器中显得相当有吸引力。

    那真正是“莫愁前路无知己,天下谁人不识君”。

    个人用途的域名,不在于有无商业价值,也不在于顶级域是否优雅。简单好记,价格实惠,足矣。

    个人在此推荐域名商Namesoil,提供免费的隐私保护服务,同时对部分顶级域有首年1美元的优惠活动,十分适合新手尝试。

    拿到域名之后,可以注册Cloudflare账号,将域名的nameserver改为Cloudflare提供的服务器。原因有二:首先是cf的界面更加现代,其次是cf本身提供了公共DNS服务,搭配自家的nameserver可有效缩短解析更新延迟。将域名托管至Cloudflare可为后续的动态域名配置提供便利。

    梦开始的地方:第一台服务器

    第二春

    显然,诸如手机和笔记本电脑等移动设备并不适合作为全天候运行的服务器设备。如果感觉现在就购买云服务显得为时过早,不妨先拿家中的旧设备试试水。

    实际上,即使是家中的古董电脑,其(多核)性能也远超月付两位数的云服务器,如若配置得当,完全可以胜任服务器的职责。

    已经给那台尘封的老家伙通上电了吗?那么,来给她装个新系统吧。

    那个看起来很憨的企鹅

    它不叫企鹅,叫Linux,准确地说,是一个叫做Linux的内核和若干外围软件组成的操作系统。不必过分关心它的历史渊源,我们只需选择时下流行的通用发行版即可,Debian

    下载最新版镜像,制作启动介质,调整启动顺序,安装新的系统。注意要提前备份旧设备上可能存在的重要内容,数据无价。

    陌生的新界面

    也许还有很多人从没有接触过成熟的图形操作系统以外的电子设备,面对陌生的界面感到手足无措。然而界面简洁并不代表功能简陋,接下来,我们将采取一系列步骤,把这个系统设置为符合目标用途的状态:

    找到终端,配置并确认SSH服务可用,这要求这台设备已经通过合适的方法接入了家庭网络;

    新增非root的新用户,但这并不意味着密码可以随意填写,请设置足够强的密码并牢记;

    为新用户赋予使用sudo提权运行的能力。

    关于具体的操作方法,容我不再赘述。

    现代Linux发行版通常已经具有了相当可用的图形界面,然而,对于服务器用途,这套图形界面几乎没有任何用处,所以从这一步开始,就可以把旧电脑放回不碍事的地方,转而用刚刚设置的root密码在其它设备上远程操作了。

    守护者之谜:IPv6那些事

    一夫当关

    让我们暂时把目光投向默默无闻的光调制解调器。家庭网关几乎都配置了IPv6防火墙,阻断来自外部的访问,这无疑不利于服务器的建设。

    解决这个问题有两种思路:关闭光猫防火墙,或是将光猫配置为桥接,由路由器拨号上网,再在路由器上进行设置。

    无论哪种方法都需要光猫的管理员密码,而这串神秘字符通常并不容易获取。一些运营商的一些设备型号可能有便捷的破解方案,但并非总是如此。如果在这一步无论如何也无法解决防火墙问题的话,利用家庭宽带搭建服务器的路线就到此为止了。如果不幸遇到此类情况,还是阅读下面的章节,去物色一个合适的云服务产品吧。

    嗯?IPv4公网地址?那是什么?知らない~

    不安分的IPv6地址

    如果成功关闭了IPv6防火墙,理论上这台Linux设备已经可以通过IPv6被外部网络访问(这里的“访问”并不仅限于能够ping通,还要能够顺畅访问运行在Linux上的网络服务,如SSH)。可以执行ip -6 addr show来查看设备当前的IPv6地址。

    ~ $ sudo ip -6 addr show
    1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 state UNKNOWN qlen 1000
        inet6 ::1/128 scope host proto kernel_lo
           valid_lft forever preferred_lft forever
    2: dummy0: <BROADCAST,NOARP,UP,LOWER_UP> mtu 1500 state UNKNOWN qlen 1000
        inet6 fe80::c451:46ff:fe43:5b59/64 scope link proto kernel_ll
           valid_lft forever preferred_lft forever
    3: ifb0: <BROADCAST,NOARP,UP,LOWER_UP> mtu 1500 state UNKNOWN qlen 1000
        inet6 fe80::fcb7:a3ff:fea9:c18a/64 scope link proto kernel_ll
           valid_lft forever preferred_lft forever
    4: ifb1: <BROADCAST,NOARP,UP,LOWER_UP> mtu 1500 state UNKNOWN qlen 1000
        inet6 fe80::582e:c3ff:fe05:206f/64 scope link proto kernel_ll
           valid_lft forever preferred_lft forever
    18: ifb2: <BROADCAST,NOARP,UP,LOWER_UP> mtu 1500 state UNKNOWN qlen 1000
        inet6 fe80::58e8:d9ff:fe46:fd96/64 scope link proto kernel_ll
           valid_lft forever preferred_lft forever
    19: wlan0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 state UP qlen 3000
        inet6 2409:891f:6862:ade7:fff8:a769:89d9:1839/64 scope global temporary dynamic
           valid_lft 6721sec preferred_lft 6721sec
        inet6 2409:891f:6862:ade7:1997:2cbf:cc3d:93a3/64 scope global dynamic mngtmpaddr stable-privacy proto kernel_ra
           valid_lft 6721sec preferred_lft 6721sec
        inet6 fe80::8ee8:80d1:5cc:7ee0/64 scope link stable-privacy proto kernel_ll
           valid_lft forever preferred_lft forever

    可以注意到,IPv6地址显然不甚简洁,且不止一个。此外,如果重启设备或网卡,还会发现地址的后缀发生改变,如果重启网关设备,还可能导致前缀变化。哪怕什么都不做,ISP也可能定期强制更换分配的IPv6地址前缀,这都给我们的服务器搭建造成困难。

    造成以上现象的原因有:

    • IPv6隐私扩展:由于用于对外上网的IPv6地址多为全球单播地址,对公网可访问,若前缀泄露则可能导致该网络下的其他设备地址暴露(根据设备MAC推断后缀),因此,大多数IPv6客户端设备都会启用IPv6隐私扩展,根据原始 IPv6 地址计算生成一个临时地址,并在连接远程服务器时优先使用这个临时地址。对应上述输出中的temporary标志。
    • DHCPv6:适用于IPv6的DHCP,用于从网关下发IPv6地址、路由和DNS信息。DHCP下发的地址具有有效时间(租期),过期或设备离线都可能导致地址后缀变更。然而,在ISP定期更换地址前缀的背景下,使用DHCPv6进行地址管理可能出现前缀更新不及时的情况。
    • 前缀动态分配:家庭网络的IPv6地址通常采用PD(前缀委托)模式,即ISP为用户网关分配IPv6前缀,由用户侧设备决定后缀生成方式。然而,出于成本和监管问题,ISP所分配的地址前缀会不定期更换,这为内网前缀的更新速度提出更高要求。

    基于以上讨论,我们可以明确要实现的目标:关闭隐私扩展,使用设备MAC生成地址后缀;停用DHCPv6,采用SLAAC分配地址;配置DDNS,将IP变化及时更新到域名解析结果中。

    本文中,我们采用systemd-networkdsystemd-resolved来配置和管理设备网络,若设备上已经运行有NetworkManager,则需要先将其禁用:

    Bash
    sudo systemctl disable --now NetworkManager

    启用systemd-networkdsystemd-resolved

    Bash
    sudo systemctl enable --now systemd-networkd
    sudo systemctl enable --now systemd-resolved

    /etc/resolv.conf 链接到 systemd-resolved 提供的解析服务:

    Bash
    sudo ln -sf /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf

    创建配置文件/etc/systemd/network/10-default.network

    INI
    [Match]
    Name=<interface> # 替换为实际的网络接口名称
    
    [Network]
    DHCP=ipv4 # 使用DHCP分配的IPv4地址
    IPv6AcceptRA=yes # 接受RA(路由器通告)以无状态分配IPv6地址和路由
    
    [DHCPv4]
    UseDNS=false

    顺带一提,在此处指定接受RA后,内核参数net.ipv6.conf.<interface>.accept_ra的值就不再反映系统真实行为,对该参数的修改也无效,原因是systemd会屏蔽内核相关实现,详细信息参见

    创建或修改配置文件/etc/systemd/resolved.conf

    INI
    [Resolve]
    DNS=223.5.5.5 2400:3200::1 # 阿里DNS
    Domains=~.
    LLMNR=no
    MulticastDNS=no
    DNSSEC=no # 禁用域名系统安全扩展,以防DNS无法解析
    Cache=yes # 使用缓存加速解析
    DNSStubListener=yes

    配置DDNS(动态域名)的方案有多种,这里推荐使用Lucky,根据提示填入Cloudflare令牌,即可自动完成配置。

    完成上述更改后,重启设备,等待新地址更新完成。在任意设备上执行nslookup <domain> 1.1.1.1,看看出现的是否是那串独一无二的字符吧。

    “我是卖木梳的。”

    “有桃木的吗?”

    世界之窗:Web服务器

    获得了独属于自己的域名,拿到了全球无二的IP地址,前面的辛勤准备,已经为后续的服务搭建打下了基础。

    解决了联通性问题,我们接下来将着眼于本机的Web服务建设,由此打开一扇面向世界的窗口。

    下文将首先演示在设备上搭建WordPress站点。

    小身板大能量

    本文使用Nginx作为Web服务器。可直接通过如下命令安装:

    Bash
    sudo apt install nginx

    若需要自定义安装路径,或有额外模块需求(如rtmp直播),则可选择手动下载源码并编译安装。

    正确安装并运行Nginx后,访问设备IP或域名即可看到Nginx的默认页面,若无法访问,则需要检查设备的防火墙设置,放行相关端口:

    Bash
    sudo ufw allow 80
    sudo ufw allow 443

    Nginx还可用于反向代理等场景,本文将在之后演示。

    PHP是最……

    要搭建WordPress这样的动态网站,PHP环境必不可少。

    PHP亦可通过apt安装,或下载源码编译:

    Bash
    sudo apt install php php-fpm

    为运行WordPress,还需要为PHP安装相关拓展:

    Bash
    sudo apt install php-mysql php-curl php-json php-mbstring php-xml php-zip php-gd

    php.ini(通常位于/etc/php/)中启用这些拓展。

    最后,在php-fpm.ini中配置监听:

    INI
    [global]
    pid = /var/run/php-fpm.pid
    error_log = /var/log/php-fpm.log
    log_level = notice
    
    [www]
    listen = /tmp/php-cgi.sock
    listen.backlog = 8192
    listen.allowed_clients = 127.0.0.1
    listen.owner = www
    listen.group = www
    listen.mode = 0600
    user = www
    group = www
    pm = dynamic
    pm.status_path = /phpfpm_status
    pm.max_children = 150
    pm.start_servers = 15
    pm.min_spare_servers = 15
    pm.max_spare_servers = 30
    request_terminate_timeout = 100
    request_slowlog_timeout = 30

    其中各项参数可自行按需调整,尤其是用户权限部分。

    可靠的扳手

    在安装数据库之前,不妨先配置一套有用的工具:phpMyAdmin。它需要PHP和Nginx作为前置,所以要确保两者可用。

    严格来说,phpMyAdmin并不是一个软件,它不能像可执行文件一样安装。我们需要从网络上下载最新的phpMyAdmin压缩包并移动到适当目录:

    Bash
    wget https://files.phpmyadmin.net/phpMyAdmin/5.2.3/phpMyAdmin-5.2.3-all-languages.zip # 可替换为最新版
    unzip phpMyAdmin-5.2.3-all-languages.zip
    mv phpMyAdmin-5.2.3-all-languages /www/wwwroot/phpmyadmin

    配置Nginx(通常为/usr/local/nginx/conf/nginx.conf):

    Nginx
    worker_processes auto;
    user www www;
    events {
        worker_connections  768;
    }
    
    http {
        include       mime.types;
        default_type  application/octet-stream;
        client_max_body_size 100m;
        server_names_hash_bucket_size 64; 
    
        server {
            listen 8787; # 监听ipv4
            listen [::]:8787; # 监听ipv6
    
            location  /{
                root   /www/wwwroot/phpmyadmin/;
                index  index.php index.html index.htm;
                try_files $uri $uri/ /index.php?$args;
            }
    
            location ~ \.php$ {
                root           /www/wwwroot/phpmyadmin/;
                fastcgi_pass   unix:/tmp/php-cgi.sock;
                fastcgi_index  index.php;
                fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
                include        fastcgi_params;
            }
        }
    }

    重启Nginx,现在理论上已经可以通过该设备的8787端口访问phpMyAdmin。别忘了防火墙。

    我不认识MySQL

    本文使用MariaDB来作为数据库软件。

    依然可以使用apt安装:

    Bash
    apt install mariadb-server

    运行配置脚本:

    Bash
    sudo mariadb_secure_installation

    拿出刚刚配置好的phpMyAdmin,通过刚刚设置的root密码登录MariaDB,新建一个用户,为其新建数据库并授权。

    WordPress,始動!

    终于到了最后的环节。WordPress本质上和phpMyAdmin类似,都是依靠PHP和数据库工作的动态站点。

    下载最新版WordPress压缩包,解压并移动到合适位置:

    Bash
    wget https://cn.wordpress.org/latest-zh_CN.zip
    unzip latest-zh_CN.zip
    mv latest-zh_CN /www/wwwroot/wp

    也要在Nginx中配置对应的server块:

    Nginx
        server {
            listen 80;
            listen [::]:80;
            location  /{
                root   /www/wwwroot/wp/;
                index  index.php index.html index.htm;
                try_files $uri $uri/ /index.php?$args;
            }
            location ~ \.php$ {
                root           /www/wwwroot/wp/;
                fastcgi_pass   unix:/tmp/php-cgi.sock;
                fastcgi_index  index.php;
                fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
                include        fastcgi_params;
            }
        }

    重载配置,访问网址,WordPress的安装界面就在眼前。

    填入刚刚在MariaDB中新建的用户信息和数据库名,还没忘吧?

    All in One:Host分流与反向代理

    并不完美

    通过之前的操作,我们成功把一个域名和一个特定的Web服务关联了起来,由此实现了对外访问。这么做最大的缺点莫过于无法多用IP:当多个不同的三级域名解析到同一个IP地址时,Web服务器只会为所有访问者提供相同的服务。

    使用不同的端口号?域名后面还要加数字,多不优雅。

    当通过http协议访问网站时,请求头中的Host字段会显示目标域名,可以基于这一特性,在Nginx中配置分流,来实现端口/IP重用。

    修改此前对WordPress站点的配置:

    Nginx
        server {
            listen 80;
            listen [::]:80;
            server_name www.hacinsl.top; # 添加此行
            location  /{
                root   /www/wwwroot/wp/;
                index  index.php index.html index.htm;
                try_files $uri $uri/ /index.php?$args;
            }
            location ~ \.php$ {
                root           /www/wwwroot/wp/;
                fastcgi_pass   unix:/tmp/php-cgi.sock;
                fastcgi_index  index.php;
                fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
                include        fastcgi_params;
            }
        }

    然后,还可以给phpMyAdmin“转正”,给它一个好看的三级域:

    Nginx
        server {
            listen 80;
            listen [::]:80;
            server_name pma.hacinsl.top; # 不用再通过端口号访问了
            location  /{
                root   /www/wwwroot/phpmyadmin/;
                index  index.php index.html index.htm;
                try_files $uri $uri/ /index.php?$args;
            }
    
            location ~ \.php$ {
                root           /www/wwwroot/phpmyadmin/;
                fastcgi_pass   unix:/tmp/php-cgi.sock;
                fastcgi_index  index.php;
                fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
                include        fastcgi_params;
            }
        }

    大一统

    对于本身就自带Web服务器的工具,如Lucky、宝塔面板之类,可以通过配置反向代理,为它们分配域名。

    Nginx
        server {
            listen 80;
            listen [::]:80;
            server_name lucky.hacinsl.top;
            location / {
                proxy_pass http://127.0.0.1:11451; # 填写上游地址
            }
        }

    注意,配置反向代理后,虽然用户侧并无感知,但在对应的服务看来,所有访问者的IP都变成了本地地址(经过Nginx转发),需要多加注意。特别是对于一些默认不对本机地址启用验证的服务,务必要小心配置权限。

    通过上述配置,我们成功实现了端口重用的目标。别忘了在DDNS服务中为新增的三级域名配置解析。

    安全第一:TLS加密

    “不安全连接”

    普通的http协议采用明文传输内容。无论是否有价值,通信存在被轻易截获甚至修改的可能性都不会令人愉快。本节将通过为Web服务启用TLS加密来增强安全性。

    许多CA都提供了免费的TLS证书服务,例如Let’s Encrypt。该机构还支持签发泛域名证书,可将一套证书用于多个域名。

    签发泛域名证书需要验证用户对域名DNS记录的所有权,如果此前采用Cloudflare管理DNS记录,可以通过Lucky快捷地签发和更新证书,具体操作不再赘述。

    修改Nginx配置以启用TLS:

    Nginx
        server {
            listen 443 ssl; # 修改监听端口为443
            listen [::]:443 ssl;
            server_name www.hacinsl.top;
            ssl_certificate     /www/cert/SSL.pem;  # pem文件的路径
            ssl_certificate_key  /www/cert/SSL.key; # key文件的路径;
            ssl_session_timeout  5m;
            ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;
            ssl_protocols TLSv1.3;
            ssl_prefer_server_ciphers on;
    
            location  /{
                root   /www/wwwroot/wp/;
                index  index.php index.html index.htm;
                try_files $uri $uri/ /index.php?$args;
            }
    
            location ~ \.php$ {
                root           /www/wwwroot/wp/;
                fastcgi_pass   unix:/tmp/php-cgi-82.sock;
                fastcgi_index  index.php;
                fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
                include        fastcgi_params;
            }
        }

    以上内容以WordPress站点为例,其它服务亦可按需配置。

    真的不是广告

    除了自动续签外,Lucky还提供了证书更新后的映射和自动执行功能。可以将申请到的证书映射到相应目录,并在自动执行命令中填入systemctl restart nginx来使用新的证书。

    “好用的很呐。”

    Fly Me to the Moon:???

    海的那边

    也许在不知多少次遭遇Secure Connection Failed或Connection Timed out后,我们会突然觉得,是时候买一台海外服务器了。

    也许动机不纯,也许心怀芥蒂,但是终究,那个订单还是被提交,一台远在大洋彼岸的服务器跃然屏上。

    一台拥有IPv6地址的服务器也许会更好,这意味着其可以与位于家中的服务器直接通信。

    新机器到手后的那套准备自然不能落下:权限配置,软件安装……也许新设备的性能孱弱到连PHP都无法支持,但总有某些东西必须要装上:

    Bash
    sudo bash -c "$(curl -L https://github.com/XTLS/Xray-install/raw/main/install-release.sh)" @ install

    火星车票

    Xray的配置文件位于/usr/local/etc/xray/config.json,先来进行简单且必要的配置:

    JSONC
    {
      "log": {
        "loglevel": "warning",
        "access": "/var/log/xray/access.log", // 日志配置
        "error": "/var/log/xray/error.log",
        "dnsLog": true // 启用DNS日志以追踪耗时
      },
      "dns": {
        "servers": [
          "https+local://1.1.1.1/dns-query", // 使用Cloudflare的公共DNS,启用DoH确保隐私
          "localhost"
        ]
      },
      "routing": {
        "domainStrategy": "IPIfNonMatch",
        "rules": [
          {
            "type": "field",
            "outboundTag": "direct"
          }
        ]
      },
      "inbounds": [
        {
          "port": 443,
          "protocol": "vless",
          "settings": {
            "clients": [
              {
                "id": "<id>", // 可使用xray uuid生成随机id
                "flow": "xtls-rprx-vision",
                "level": 0,
                "email": "<email>" // 用户标识
              }
            ],
            "decryption": "none",
            "fallbacks": [ // 回落设置
              {
                "dest": 8080,
                "xver": 1
              }
            ]
          },
          "streamSettings": {
            "network": "tcp",
            "security": "tls",
            "tlsSettings": {
              "alpn": "http/1.1",
              "certificates": [
                {
                  "certificateFile": "/www/cert/SSL.pem", // 证书设置
                  "keyFile": "/www/cert/SSL.key"
                }
              ]
            }
          }
        }
      ],
      "outbounds": [
        {
          "tag": "direct",
          "protocol": "freedom"
        },
        {
          "tag": "block",
          "protocol": "blackhole"
        }
      ]
    }

    理论上,上述配置可以在这台服务器上开放一个用于流量转发的服务。显然,为了让这一功能稳定安全地运行,这台机器也需要取得对应的TLS证书。解决证书问题的思路有两种:首先当然是通过前述方案再申请一套,然而,如果想另辟蹊径,也不是不可以定期从别的服务器拉取证书。这么做不太安全,尤其是在证书存放路径过于简单的情况下。

    为了防止神秘力量的主动探测,可以在Xray的回落方向配置Nginx,将无法被识别的数据包(通常是正常的https流量)发送过去。

    安装并配置Nginx:

    Nginx
    user www;
    worker_processes auto;
    pid /run/nginx.pid;
    error_log /var/log/nginx/error.log;
    access_log /var/log/nginx/access.log;
    events {
        worker_connections 768;
    }
    
    http {
        sendfile on;
        tcp_nopush on;
        types_hash_max_size 2048;
        include /usr/local/nginx/conf/mime.types;
        client_max_body_size 5000m;
        gzip on;
    
        server {
            listen 127.0.0.1:8080;
            server_name misaka.hacinsl.top;
            location  /{
                root /www/wwwroot/wp/;
                absolute_redirect off; # 使用相对路径重定向,以解决重定向后主机名改变的问题
                index  index.html index.htm;
                try_files $uri $uri/ =404;
            }
        }
    }

    以上配置可以实现简单的对8080端口的监听。可参照这篇文章,将前文建立的WordPress静态化部署在这台服务器上。若监听本地回落端口,无需配置TLS即可实现https访问支持。

    互联世界

    接着来看看Xray客户端的配置。

    JSONC
    {
      "log": {
        "access": "D:/Program Files/Xray/access.log", // 依然是日志记录
        "error": "D:/Program Files/Xray/error.log",
        "loglevel": "warning",
        "dnsLog": true
      },
      "inbounds": [ //设立两种情景模式
        {
          "tag": "default",
          "port": 49,
          "listen": "0.0.0.0",
          "protocol": "socks",
          "sniffing": {
            "enabled": true,
            "destOverride": [
              "http",
              "tls"
            ],
            "routeOnly": false
          },
          "settings": {
            "auth": "noauth",
            "udp": true,
            "allowTransparent": false
          }
        },
        {
          "tag": "global",
          "port": 50,
          "listen": "0.0.0.0",
          "protocol": "socks",
          "sniffing": {
            "enabled": true,
            "destOverride": [
              "http",
              "tls"
            ],
            "routeOnly": false
          },
          "settings": {
            "auth": "noauth",
            "udp": true,
            "allowTransparent": false
          }
        }
      ],
      "outbounds": [
        {
          "tag": "direct", // 直连出站
          "protocol": "freedom"
        },
        {
          "tag": "proxy-tky", // 代理出站
          "protocol": "vless",
          "settings": {
            "vnext": [
              {
                "address": "<domain>", // 填入远程服务器地址
                "port": 443,
                "users": [
                  {
                    "id": "<id>", // 使用在服务端配置好的合法id
                    "security": "auto",
                    "encryption": "none",
                    "flow": "xtls-rprx-vision"
                  }
                ]
              }
            ]
          },
          "streamSettings": {
            "network": "tcp",
            "security": "tls",
            "tlsSettings": {
              "allowInsecure": false,
              "serverName": "<domain>", // 使用与上相同的地址
              "fingerprint": "chrome"
            }
          },
          "mux": {
            "enabled": false,
            "concurrency": -1
          }
        },
        {
          "tag": "block",
          "protocol": "blackhole",
          "settings": {
            "response": {
              "type": "http"
            }
          }
        }
      ],
      "dns": {
        "servers": ["223.5.5.5"]
      },
      "routing": {
        "domainStrategy": "IPIfNonMatch",
        "domainMatcher": "mph",
        "rules": [
          {
            "outboundTag": "proxy-tky",
            "domain": [
              "geosite:google-cn"
            ]
          },
          {
            "outboundTag": "direct",
            "ip": [
              "geoip:cn",
              "geoip:private"
            ]
          },
          {
            "outboundTag": "direct",
            "domain":[
              "geosite:cn",
              "geosite:private"
            ]
          },
          {
            "outboundTag": "proxy-tky",
            "domain": [
              "geosite:google" // 按需扩充
            ]
          },
          {
            "inboundTag": "global",
            "outboundTag": "proxy-tky",
            "domain": [
              "regexp:."
            ]
          }
        ]
      }
    }

    整体上采用的思路很简单:让流量根据域名和IP去它该去的地方。设置了default和global两种模式,以满足日常使用和特殊情况的需求。需要注意的是,为尽可能降低延时,本地DNS查询仍然通过传统方式,无隐私保护,仅在global模式下才会把未解析的域名发送给服务端处理。

    先前对于服务端的配置也太过简单,让我们来增加一些功能:

    JSONC
    {
      "log": {
        "loglevel": "warning",
        "access": "/var/log/xray/access.log", // 日志配置
        "error": "/var/log/xray/error.log",
        "dnsLog": true // 启用DNS日志以追踪耗时
      },
      "dns": {
        "servers": [
          "https+local://1.1.1.1/dns-query", // 使用Cloudflare的公共DNS,启用DoH确保隐私
          "localhost"
        ]
      },
      "routing": {
        "domainStrategy": "IPIfNonMatch",
        "rules": [
          {
            "type": "field", // 阻断对内网地址的访问
            "ip": [
              "geoip:private"
            ],
            "outboundTag": "block"
          },
          {
            "type": "field",
            "domain": ["geosite:google-cn"], // 明明并不cn(恼)
            "outboundTag": "direct"
          },
          {
            "type": "field",
            "ip": ["geoip:cn"], // 防止直连国内
            "outboundTag": "wireguard"
          },
          {
            "type": "field",
            "domain": ["geosite:cn"],
            "outboundTag": "wireguard"
          },
          {
            "type": "field",
            "domain":[
              "dmm" // 后续再谈
            ],
            "outboundTag": "wireguard"
          }
        ]
      },
      "inbounds": [
        {
          "port": 443,
          "protocol": "vless",
          "settings": {
            "clients": [
              {
                "id": "<id>", // 可使用xray uuid生成随机id
                "flow": "xtls-rprx-vision",
                "level": 0,
                "email": "<email>" // 用户标识
              }
            ],
            "decryption": "none",
            "fallbacks": [ // 回落设置
              {
                "dest": 8080,
                "xver": 1
              }
            ]
          },
          "streamSettings": {
            "network": "tcp",
            "security": "tls",
            "tlsSettings": {
              "alpn": "http/1.1",
              "certificates": [
                {
                  "certificateFile": "/www/cert/SSL.pem", // 证书设置
                  "keyFile": "/www/cert/SSL.key"
                }
              ]
            }
          }
        }
      ],
      "outbounds": [
        {
          "tag": "direct",
          "protocol": "freedom"
        },
        {
          "tag": "wireguard", // 新增一个Cloudflare wireguard出站,获取key可参见:https://xtls.github.io/config/outbounds/wireguard.html
          "protocol": "wireguard",
          "settings": {
            "secretKey": "",
            "address": [],
            "peers": [
              {
                "publicKey": "",
                "endpoint": ""
              }
            ],
            "reserved": [0, 0, 0]
          }
        },
        {
          "tag": "block",
          "protocol": "blackhole"
        }
      ]
    }
    

    有证据表明,把本不需要转发的流量也发送出去,既耗费时间又不够安全,实在称不上是明智的选择。但是,假如真的因为各种原因把不该转发的流量发了出去,也应当设计相应的防护措施。

    Cloudflare提供了免费的Wireguard服务,为用户提供更加安全的上网体验,我们可以将目标地址为大陆的流量通过wireguard出站,降低被检测的可能性。

    如果购买的服务器并不拥有当地的原生IP(注册地为别国),且并不具备IPv6或目标服务器不支持IPv6,使用wireguard出站还可为解锁区域限制内容提供可能。虽然Cloudflare已经提供了向目标服务器暴露源IP的方法,但该策略仍可用于部分没有响应Cloudflare的服务。

    给我过来

    显然,有不少软件既不遵从系统代理,也不支持设置代理服务器。如果想让这类不听话的程序通过代理上网,就需要一些更为强力的代理工具,如Proxifier

    让我们先忽略Proxifier是一个付费软件的事实,来看看怎么设置它:

    Profile - Proxy Servers - Add,填入本机地址和Xray监听的端口。为两种模式对应的两个端口都建立对应的代理服务器。

    Profile - Proxification Rules - Add,填入程序的进程名(文件名)以添加规则。对于日常使用,建议将本机应用流量全部代理至Xray的defaultTag上。

    还有一点很重要,别忘了为Xray创建例外规则,不然就要死循环了。

    物尽其用

    既然这是一台功能完备的服务器,也许还能开发出别的用途。

    例如,在这台服务器的nginx上也可配置基于主机名的分流和本地反向代理,为其它Web服务提供支持。

    此外,还可利用这台服务器的IPv4公网访问能力,为之前仅具有IPv6公网地址的设备提供转发。

    Nginx
    user www;
    worker_processes auto;
    pid /run/nginx.pid;
    error_log /var/log/nginx/error.log;
    events {
        worker_connections 768;
    }
    
    http {
        sendfile on;
        tcp_nopush on;
        types_hash_max_size 2048;
        include /usr/local/nginx/conf/mime.types;
        default_type application/octet-stream;
        client_max_body_size 5000m;
        log_format mylog '$proxy_protocol_addr - $remote_addr [$time_local] ' # 日志功能
                            '"$request" $status $body_bytes_sent '
                            '"$http_referer" "$http_user_agent"';
        access_log /var/log/nginx/access.log mylog;
        gzip on;
        
        server { # 提供80跳转443的功能
            listen 80;
            return 301 https://$http_host$request_uri;
        }
        
        server {
            listen 127.0.0.1:8080 proxy_protocol; # 支持proxy_protocol
            server_name misaka.hacinsl.top;
            location  /{
                root /www/wwwroot/wp/;
                absolute_redirect off;
                index  index.html index.htm;
                try_files $uri $uri/ =404;
            }
        }
        server {
            listen 127.0.0.1:8080 proxy_protocol;
            server_name www.hacinsl.top;
            location / {
                set $hacdomain "hacinsl.top";
                proxy_pass https://$hacdomain;
                proxy_set_header Host $host;
                resolver 1.1.1.1 valid=60s;
            } 
        }
    }
    

    以上配置作出了两点重要改动:

    • 使用proxy_protocol进行监听,解析Xray传递的客户端真实IP,便于日志分析等。
    • 对远程服务器进行反向代理,为确保DNS及时更新,采用“先设置变量再引用”和设置记录有效期的方式,避免转发时使用过期IP。

    改善生活:更多服务

    至此,基本的网络架构已经建成,不妨来做些有趣的事情。

    网络存储/文件共享

    移动存储价格居高不下,付费网盘难保服务质量。在此背景下,给服务器多塞几块淘来的硬盘,也许是扩容个人存储的不错选择。

    HFS提供了便捷的文件存储与共享服务,支持精细的权限配置,使用虚拟文件夹简化目录结构,十分适合文件分享和简单的媒体播放。部署方法可参照官方说明。

    频繁的媒体播放需求可由Samba胜任,别忘了仔细处理权限问题。

    以下是一份可供参考的配置:

    INI
    [global]
    security = user
    log file = /var/log/samba/log.%m
    max log size = 1000
    logging = file
    
    server role = standalone server
    obey pam restrictions = yes
    unix password sync = yes
    passwd program = /usr/bin/passwd %u
    passwd chat = *Enter\snew\s*\spassword:* %n\n *Retype\snew\s*\spassword:* %n\n *password\supdated\ssuccessfully* .
    pam password change = yes
    map to guest = never
    
    read only = yes
    create mask = 0700
    directory mask = 0700
    
    [Yanami]
    comment = Yanami
    path = /media/Yanami
    public = yes
    read only = yes
    browseable = yes
    directory mode = 0777
    valid users = hacinsl

    该配置文件会使用Linux用户体系进行身份验证,同时阻止任何匿名账户的访问。出于安全考虑,该配置没有启用文件读写。

    RSS订阅/BitTorrent下载

    这部分内容的典型应用场景就是动画下载,以下是简单的操作说明。

    安装qbittorrent-nox,这是一个仅有WebUI的用户界面,适合服务器使用:

    Bash
    sudo apt install qbittorrent-nox

    修改qbittorrent-nox的启动配置:

    Bash
    sudo nano /etc/systemd/system/qbittorrent-nox.service

    可在其中修改qbittorrent使用的用户和组,以及WebUI的端口。

    预设管理密码可能出现在运行日志中。

    在合适的站点取得RSS订阅URL,填入qbittorrent中,设置自动下载规则即可。

    可在BitTorrent - 做种限制中设置超过一定时间或分享率超过预设值后删除torrent,防止上传流量过高或种子积压过多。

    若出现权限问题,可将qbittorrent使用的用户加入对应目录所有者的组。


    想写的还有很多,但是在此告一段落吧。毕竟,我还才疏学浅,网络的魅力,也终究要自己去探索。

    当 代 互 联 网 天 · 下 · 第 · 一 !!!

    ——レイン

    她说得对吗?我觉得对。

  • OpenConnect部署——将网卡代理纳入路由

    许多公司与高校都在使用Cisco’s AnyConnect作为为员工师生提供外部上网的解决方案,然而,这类VPN客户端通过创建虚拟网络接口并修改路由策略来实现网络代理,开启后将会接管设备上所有网络流量。如此一来,不但正常的网络连接会被意外代理,导致上网体验受到影响,在开关虚拟网络时也会导致短暂断网,着实不够优雅。

    作为一种解决思路,我们可以通过在远程Linux设备上运行OpenConnect创建虚拟网络接口,并使用自定义脚本配置路由规则来实现更加灵活的分流,同时为其他设备快速接入,实现无感上网提供支持。

    本文,我们将以虹梅师专的校园网VPN为例,演示如何将网卡代理纳入个人网络路由。


    准备开始

    插入一条来自未来的Hacinsl的友情提醒:如果您也在尝试为自己的,尤其是和鄙人同一所学校的校园网配置VPN,可能会更想要先跳转到这个位置看一看踩坑实录。

    首先,在Linux设备上安装openconnect软件包。

    Bash
    sudo apt install openconnect

    尝试连接。

    Bash
    sudo /usr/sbin/openconnect "<yourvpnserver>"

    回车!

    hacinsl@Misaka:~> /usr/sbin/openconnect  vpn-cn.ecnu.edu.cn 
    POST https://vpn-cn.ecnu.edu.cn/
    Connected to 202.120.88.66:443
    SSL negotiation with vpn-cn.ecnu.edu.cn
    Connected to HTTPS on vpn-cn.ecnu.edu.cn with ciphersuite (TLS1.2)-(ECDHE-X25519)-(RSA-SHA256)-(AES-256-GCM)
    Got HTTP response: HTTP/1.1 404 Not Found
    Unexpected 404 result from server
    GET https://vpn-cn.ecnu.edu.cn/
    Connected to 202.120.88.66:443
    SSL negotiation with vpn-cn.ecnu.edu.cn
    Connected to HTTPS on vpn-cn.ecnu.edu.cn with ciphersuite (TLS1.2)-(ECDHE-X25519)-(RSA-SHA256)-(AES-256-GCM)
    Got HTTP response: HTTP/1.0 302 Object Moved
    GET https://vpn-cn.ecnu.edu.cn/+webvpn+/index.html
    SSL negotiation with vpn-cn.ecnu.edu.cn
    Connected to HTTPS on vpn-cn.ecnu.edu.cn with ciphersuite (TLS1.2)-(ECDHE-X25519)-(RSA-SHA256)-(AES-256-GCM)
    Got HTTP response: HTTP/1.1 301 Moved Permanently
    GET https://vpn-cn.ecnu.edu.cn/+CSCOU+/anyconnect_unsupported_version.html
    Please upgrade your AnyConnect Client
    Failed to complete authentication

    Boom!

    Why Boom?

    怎么回事呢?原来是Please upgrade your AnyConnect Client,提示所用的客户端版本不受支持。然而问题不在我,并不是客户端版本太低,而是不在受支持的版本列表内,换句话说,是太高了。

    那么去看一眼虹梅师专的使用说明里用的是什么版本呢,哦,是v5.02,发布于2014年……

    虽然很想吐槽,但对于一个能正常使用的系统,不去乱动它显然是明智之举(?)。

    对于这个问题,最直接的解决方案显然是换用老版本客户端,但Debian12的官方软件源显然不会支持如此古典的版本,比起寻找和添加新软件源的麻烦,也许自己编译才更为省事。

    是的没错我又踩坑了,以下内容仅供参考。

    在官方的发布页面找到v5.02的源代码,解压到合适的目录,执行以下操作进行编译。

    Bash
    cd ./openconnect
    ./autogen.sh
    ./version.sh version.c
    ./configure
    make

    回车!

    In file included from openconnect-internal.h:35,
                     from dtls.c:37:
    /usr/include/openssl/ssl.h:2041:50: note: declared here
     2041 | OSSL_DEPRECATEDIN_1_1_0 __owur const SSL_METHOD *DTLSv1_client_method(void);
          |                                                  ^~~~~~~~~~~~~~~~~~~~
    dtls.c:161:38: error: invalid use of incomplete typedef ‘SSL_SESSION’ {aka ‘struct ssl_session_st’}
      161 |                 vpninfo->dtls_session->ssl_version = 0x0100; /* DTLS1_BAD_VER */
          |                                      ^~
    dtls.c:165:30: error: invalid use of incomplete typedef ‘SSL_SESSION’ {aka ‘struct ssl_session_st’}
      165 |         vpninfo->dtls_session->master_key_length = sizeof(vpninfo->dtls_secret);
          |                              ^~
    dtls.c:166:37: error: invalid use of incomplete typedef ‘SSL_SESSION’ {aka ‘struct ssl_session_st’}
      166 |         memcpy(vpninfo->dtls_session->master_key, vpninfo->dtls_secret,
          |                                     ^~
    dtls.c:169:30: error: invalid use of incomplete typedef ‘SSL_SESSION’ {aka ‘struct ssl_session_st’}
      169 |         vpninfo->dtls_session->session_id_length = sizeof(vpninfo->dtls_session_id);
          |                              ^~
    dtls.c:170:37: error: invalid use of incomplete typedef ‘SSL_SESSION’ {aka ‘struct ssl_session_st’}
      170 |         memcpy(vpninfo->dtls_session->session_id, vpninfo->dtls_session_id,
          |                                     ^~
    dtls.c:187:21: warning: assignment discards ‘const’ qualifier from pointer target type [-Wdiscarded-qualifiers]
      187 |         dtls_cipher = sk_SSL_CIPHER_value(ciphers, 0);
          |                     ^
    dtls.c:190:30: error: invalid use of incomplete typedef ‘SSL_SESSION’ {aka ‘struct ssl_session_st’}
      190 |         vpninfo->dtls_session->cipher = dtls_cipher;
          |                              ^~
    dtls.c:191:30: error: invalid use of incomplete typedef ‘SSL_SESSION’ {aka ‘struct ssl_session_st’}
      191 |         vpninfo->dtls_session->cipher_id = dtls_cipher->id;
          |                              ^~
    dtls.c:191:55: error: invalid use of incomplete typedef ‘SSL_CIPHER’ {aka ‘struct ssl_cipher_st’}
      191 |         vpninfo->dtls_session->cipher_id = dtls_cipher->id;
          |                                                       ^~
    dtls.c:200:51: error: invalid use of incomplete typedef ‘SSL_SESSION’ {aka ‘struct ssl_session_st’}
      200 |                              vpninfo->dtls_session->ssl_version);
          |                                                   ^~
    openconnect-internal.h:338:75: note: in definition of macro ‘vpn_progress’
      338 | #define vpn_progress(vpninfo, ...) (vpninfo)->progress((vpninfo)->cbdata, __VA_ARGS__)
          |                                                                           ^~~~~~~~~~~
    make[1]: *** [Makefile:815: openconnect-dtls.o] Error 1
    make[1]: Leaving directory '/home/hacinsl/openconnect'
    make: *** [Makefile:969: all-recursive] Error 1

    Boom!

    这次又怎么了呢?原来,用小了十岁(指版本发布日期)的OpenSSL去编译OpenConnect会因为ssl-dtls的兼容性而产生问题。

    难道为此还要再去编译一份古典OpenSSL吗?

    也许不至于此。抱着试一试的心态,我下载了最新版OpenConnect的源码,并尝试修改其中的版本标识符来绕过客户端版本检测。

    这次成功了(至少对我来说),以下内容可供实践。

    先把之前编译失败的废品删掉,然后下载解压新的源码,运行官方的生成脚本。

    Bash
    cd ./openconnect
    ./autogen.sh
    ./version.sh version.c

    到这一步打住。手动修改刚刚生成的version.c,将其中的版本号改为5.02,致敬经典。

    然后继续编译操作。

    Bash
    ./configure
    make

    这次没出什么幺蛾子。连接VPN试试。

    hacinsl@hacserver:~> /home/hacinsl/openconnect/openconnect  vpn-cn.ecnu.edu.cn 
    POST https://vpn-cn.ecnu.edu.cn/
    Connected to 202.120.88.66:443
    SSL negotiation with vpn-cn.ecnu.edu.cn
    Connected to HTTPS on vpn-cn.ecnu.edu.cn with ciphersuite TLSv1.2-ECDHE-RSA-AES256-GCM-SHA384
    XML POST enabled
    GROUP: [ECNU]:ECNU
    POST https://vpn-cn.ecnu.edu.cn/
    XML POST enabled
    Username:

    这次顺利来到了登录阶段。搞定。

    流程自动化

    如果每次连接VPN都要手动输入账户密码就丧失了这项工程的意义,好在OpenConnect支持从标准输入(stdin)读取密码, 方便用户通过脚本自动化登录流程。

    编写登录脚本。

    Bash
    #!/bin/bash
    # start_vpn.sh
    echo "<yourpasswd>" | /usr/sbin/openconnect \
      --user=<yourid> \
      --passwd-on-stdin \
      --authgroup=<yourgroup> \
      --script=</path/to/your/configscript.sh> \
      --interface=tun0 \
      "<yourvpnhost>"

    通过自定义OpenConnect启动时调用的脚本,可以防止OpenConnect在建立连接后修改设备默认路由导致的意外代理,但也同时要求用户合理配置设备路由表和路由规则,让目标流量正确通过虚拟网络接口出站。

    为实现这一需求,我们编写一个网络配置脚本,在OpenConnect启动时调用。

    Bash
    #!/bin/bash
    # configscript.sh
    
    TUNNAME="${TUNNAME:-tun0}" # 为防止openconnect在未建立连接时不能正确传递接口名称,在此设定默认值。
    VPN_SERVER="<vpnhost>" # 如果VPN服务器本身也在目标网段内,则需要特别注明,让通往服务器的流量通过物理网络接口出站。
    CERNET_IP="<targetip>" # 将希望通过虚拟网络接口出站的目标网段写入,若为校园网配置,则可直接将学校所在教育网网段写入。
    
    echo "设置接口和IP地址"
    ip link set "$TUNNAME" up
    ip addr add "$INTERNAL_IP4_ADDRESS" dev "$TUNNAME" # 这里只配置了接口的IPv4地址,如有需要也可配置IPv6,前提是VPN服务器提供了IPv6地址,反正虹梅师专没有。
    
    echo "创建自定义路由表"
    TABLE_NAME="vpn"
    TABLE_NUM=1016
    
    echo "检查表是否存在"
    grep -q "^$TABLE_NUM $TABLE_NAME" /etc/iproute2/rt_tables 2>/dev/null || {
      echo "$TABLE_NUM $TABLE_NAME" | sudo tee -a /etc/iproute2/rt_tables
    }
    
    echo "清空自定义表中的默认路由"
    ip route flush table $TABLE_NUM 2>/dev/null
    
    echo "在自定义路由表中添加默认路由,指向VPN接口"
    ip route add default dev "$TUNNAME" table $TABLE_NUM # 让进入该路由表的流量默认都从VPN接口出站。
    
    echo "添加路由规则"
    ip rule add to "$VPN_SERVER" table main priority 100 # 设置高优先级规则,对于和VPN服务器直接通信的流量通过物理接口出站。
    ip rule add to "$CERNET_IP" table $TABLE_NUM priority 200 # 次高优先级规则,对于目标地址为目标网段的流量通过自定义路由表走VPN接口出站。
    
    echo "VPN 接口 $TUNNAME 已启动,策略路由已配置。"

    到这一步,理论上就可以通过登录脚本创建VPN接口了,本设备上去往目标网段的流量都会通过VPN接口出站。

    接下来就是用适当的方式让自己的上网设备将相关流量转发到这台Linux设备上处理,方法不一而足,本文不再讨论。

    更进一步

    通过制定教育网网段的方法分流已经能避免大部分问题,但还不够好,部分部署于公网的学术资源/教育优惠仍然会查验访问者的IP。鉴于此,我们需要一个更强大的,最好是基于域名分流的方案。

    下面演示一个基于Xray和nftables的实现路径。

    为什么不用iptables?这年头连OpenWrt都用上nftables了,还是赶紧拥抱更新更好的防火墙罢。

    Linux的路由规则/Netfilter框架显然不具备处理域名的能力(指嗅探流量中的域名而不是在配置规则时用域名代替IP),需要Xray的辅助。

    一种常见的用于网关设备的代理方案是在PREROUTING链上添加规则,将网关接受的流量通过TPROXY发往本机地址,并配合标记(mark)和策略路由实现透明代理。为了代理本机流量,这种方案选择在OUTPUT链上将本机发出的流量也打上相同的标记,通过策略路由再次导入PREROUTING链处理。

    由于nftables不允许在目标地址转换(DNAT)和重定向(REDIRECT)后继续更多操作(如标记),所以本文采用PREROUTING链上透明代理的方式。也有可能有更优雅的姿势但是我不知道

    Bash
    # create route table for xray
    echo "Create xray table..."
    grep -q "^114 xray" /etc/iproute2/rt_tables 2>/dev/null || {
      echo "114 xray" | tee -a /etc/iproute2/rt_tables
    } # add 114 table if not exist
    grep -q "^116 xray6" /etc/iproute2/rt_tables 2>/dev/null || {
      echo "116 xray6" | tee -a /etc/iproute2/rt_tables
    } # add 116 table if not exist
    ip route flush table 114 2>/dev/null # flush 114 table
    ip route add local 0.0.0.0/0 dev lo table 114 # add default route for 114
    ip rule add fwmark 1 table 114 priority 99 # add rule to mark package
    ip -6 route flush table 116 2>/dev/null # flush 116 table
    ip -6 route add local ::/0 dev lo table 116 # add default route for 116
    ip -6 rule add fwmark 1 table 116 priority 99 # add rule to mark package
    
    # add nft rules
    echo "Create nft rules..."
    nft add table inet xray
    nft add chain inet xray prerouting { type filter hook prerouting priority mangle \; }
    nft flush chain inet xray prerouting
    nft add rule inet xray prerouting ip daddr { 0.0.0.0/8, 10.0.0.0/8, 127.0.0.0/8, 169.254.0.0/16, 172.16.0.0/12, 192.168.0.0/16, 224.0.0.0/4, 240.0.0.0/4 } return
    nft add rule inet xray prerouting ip6 daddr { ::/128, ::1/128, ::ffff:0:0/96, 64:ff9b::/96, 100::/64, 2001::/32, 2001:20::/28, 2001:db8::/32, 2002::/16, fc00::/7, fe80::/10, ff00::/8 } return
    nft add rule inet xray prerouting tcp dport 0-65535 tproxy ip to 0.0.0.0:12345 meta mark set 1 accept
    nft add chain inet xray output { type filter hook output priority mangle \; }
    nft flush chain inet xray output
    nft add rule inet xray output ip daddr { 0.0.0.0/8, 10.0.0.0/8, 127.0.0.0/8, 169.254.0.0/16, 172.16.0.0/12, 192.168.0.0/16, 224.0.0.0/4, 240.0.0.0/4 } return
    nft add rule inet xray output ip6 daddr { ::/128, ::1/128, ::ffff:0:0/96, 64:ff9b::/96, 100::/64, 2001::/32, 2001:20::/28, 2001:db8::/32, 2002::/16, fc00::/7, fe80::/10, ff00::/8 } return
    nft add rule inet xray output meta mark 9 return
    nft add rule inet xray output tcp dport 0-65535 meta mark set 1 accept

    Xray相关配置如下。

    JSONC
    {
      "inbounds": [
        {
          "port": 12345,
          "protocol": "dokodemo-door", // 任意门(迫真)
          "settings": {
            "network": "tcp",
            "followRedirect": true
          },
          "sniffing": {
            "enabled": true,
            "destOverride": ["http", "tls"]
          },
          "streamSettings": {
            "sockopt": {
              "tproxy": "tproxy"
            }
          },
          "tag": "tproxy"
        }
      ],
      "outbounds": [
        {
          "protocol": "freedom",
          "tag": "direct",
          "streamSettings": {
              "sockopt": {
                "mark": 9 // 标记直连流量,该outbound置顶作为默认出站
            }
          }
        },
        {
          "protocol": "freedom",
          "tag": "proxy-ecnu",
          "streamSettings": {
              "sockopt": {
                "mark": 3
            }
          } 
        }
      ],
      "dns": {
        "servers": ["223.5.5.5"]
      },
      "routing": {
        "domainStrategy": "AsIs",
        "rules": [
          {
            "type": "field",
            "outboundTag": "proxy-ecnu",
            "domain": [
              "domain:lib.ecnu.edu.cn"
            ]
          }
        ]
      }
    }

    由此,我们可以更新将流量导入openconnect的方式,从匹配出站流量到匹配Xray的标记。

    Bash
    # create route table for ecnu
    echo "Create ecnu table..."
    grep -q "^1016 ecnu" /etc/iproute2/rt_tables 2>/dev/null || {
      echo "1016 ecnu" | tee -a /etc/iproute2/rt_tables
    } # add 1016 table if not exist
    ip route flush table 1016 2>/dev/null # flush 1016 table
    ip route add default dev tun0 table 1016 # add default route for 1016
    ip rule add fwmark 3 table 1016 priority 90 # add rule to mark package

    添加默认路由的操作会因对应虚拟网卡接口不存在而无法进行,因此,应当把上述内容放在openconnect的配置脚本里,或者在启动时检测接口是否存在。以下是一种检测接口和Xray进程的不太靠谱的思路。

    Bash
    # wait for openconnect and xray to start
    while ! pidof xray > /dev/null 2>&1; do
        sleep 1
    done
    echo "xray started"
    while ! ip addr show 2>/dev/null | grep -q "tun0"; do
        sleep 1
    done
    sleep 5
    echo "tun0 found"

    因为接口出现并不代表就能被使用,因此添加了一定的延时以确保执行后续操作时接口已经就绪。

    连接保持

    有些VPN服务器可能会自动关闭一段时间内没有流量的连接,为了保证服务随时可用,我们可以创建一个定时任务,不断请求目标网段,防止连接被意外关闭。

    Bash
    #!/bin/bash
    # keep_alive.sh
    curl "<properwebsite>"
    Bash
    sudo crontab -e
    # 防止掉线
    */3 * * * * /opt/keep_alive.sh
    
    # 开机自启
    @reboot /opt/start_vpn.sh