Repository: dnomd343/XProxy Branch: master Commit: 3e021a41da1c Files: 29 Total size: 77.6 KB Directory structure: gitextract__mb0cpx2/ ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── cmd/ │ ├── asset/ │ │ ├── asset.go │ │ └── update.go │ ├── common/ │ │ ├── file.go │ │ └── func.go │ ├── config/ │ │ ├── decode.go │ │ ├── default.go │ │ └── main.go │ ├── controller.go │ ├── custom/ │ │ └── main.go │ ├── dhcp/ │ │ └── main.go │ ├── network/ │ │ ├── dns.go │ │ ├── main.go │ │ ├── network.go │ │ └── tproxy.go │ ├── process/ │ │ ├── daemon.go │ │ ├── exit.go │ │ └── main.go │ ├── proxy/ │ │ ├── config.go │ │ └── main.go │ ├── radvd/ │ │ └── radvd.go │ └── xproxy.go ├── docs/ │ ├── campus_network_cracking.md │ └── dual_stack_network_proxy.md ├── go.mod └── go.sum ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ /.idea/ ================================================ FILE: Dockerfile ================================================ ARG ALPINE="alpine:3.20" ARG GOLANG="golang:1.24-alpine3.22" FROM --platform=${BUILDPLATFORM} ${GOLANG} AS xray ENV XRAY="25.7.26" RUN wget https://github.com/XTLS/Xray-core/archive/v${XRAY}.tar.gz -O- | tar xz WORKDIR ./Xray-core-${XRAY}/main/ RUN go mod download -x ARG TARGETARCH RUN env GOOS=linux GOARCH=${TARGETARCH} CGO_ENABLED=0 go build -v -trimpath -ldflags "-s -w" RUN mv main /xray FROM --platform=${BUILDPLATFORM} ${GOLANG} AS xproxy RUN apk add git COPY ./ /XProxy/ WORKDIR /XProxy/cmd/ RUN go mod download -x ARG TARGETARCH RUN env GOOS=linux GOARCH=${TARGETARCH} CGO_ENABLED=0 go build -v -trimpath -ldflags "-X main.version=$(git describe --tag) -s -w" RUN mv cmd /xproxy FROM --platform=${BUILDPLATFORM} ${ALPINE} AS assets RUN apk add xz RUN wget "https://cdn.dnomd343.top/v2ray-rules-dat/geoip.dat" RUN wget "https://cdn.dnomd343.top/v2ray-rules-dat/geosite.dat" RUN tar cJf /assets.tar.xz geoip.dat geosite.dat FROM --platform=${BUILDPLATFORM} ${ALPINE} AS release RUN apk add upx WORKDIR /release/run/radvd/ WORKDIR /release/var/lib/dhcp/ RUN touch dhcpd.leases dhcpd6.leases COPY --from=assets /assets.tar.xz /release/ COPY --from=xproxy /xproxy /release/usr/bin/ COPY --from=xray /xray /release/usr/bin/ WORKDIR /release/usr/bin/ RUN ls | xargs -n1 -P0 upx -9 FROM ${ALPINE} RUN apk add --no-cache dhcp radvd iptables ip6tables COPY --from=release /release/ / WORKDIR /xproxy/ ENTRYPOINT ["xproxy"] ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2022 Dnomd343 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # XProxy > 虚拟代理网关,对局域网设备进行透明代理 + ✅ 基于容器运行,无需修改主机路由配置,开箱即用 + ✅ 独立的 MAC 地址,与宿主机网络栈无耦合,随开随关 + ✅ 允许自定义 DNS 、上游网关、IP 地址等网络选项 + ✅ 支持 TCP 、UDP 流量代理,完整的 Full-cone NAT 支持 + ✅ 完全兼容 IPv6 ,支持 SLAAC 地址分配,RDNSS 与 DNSSL 配置 + ✅ 内置 DHCP 与 DHCPv6 服务器,支持 IP 地址自动分配 ## 拓扑模型 XProxy 部署在内网 Linux 主机上,通过 `macvlan` 网络创建独立 MAC 地址的虚拟网关,劫持内网设备的网络流量进行透明代理;宿主机一般以单臂旁路由的方式接入,虚拟网关运行时不会干扰宿主机网络,同时宿主机系统的流量也可被网关代理。 ```mermaid graph TD net{{互联网}} === route(路由器) subgraph 内网 route --- client_1(设备1) route --- client_2(设备2) route --- client_3(设备3) subgraph 宿主设备 client(虚拟网关) end route -.- client end ``` XProxy 运行以后,内网流量将被收集到代理内核上,支持 `xray` ,`v2ray` ,`sagray` 等多种内核,支持 `Shadowsocks` ,`ShadowsocksR` ,`VMess` ,`VLESS` ,`Trojan` ,`WireGuard` ,`SSH` ,`PingTunnel` 等多种代理协议,支持 `XTLS` ,`Reality` ,`WebSocket` ,`QUIC` ,`gRPC` 等多种传输方式。同时,得益于 V2ray 的路由设计,代理的网络流量可被精确地分流,可以依据内网设备、目标地址、访问端口、连接域名、流量类型等多种方式进行路由。 由于 XProxy 与宿主机网络完全解耦,一台主机上可运行多个虚拟网关,它们拥有不同的 MAC 地址,在网络模型上是多台独立的主机;因此各个虚拟网关能负责不同的功能,甚至它们之间还能互为上下级路由的关系,灵活实现多种网络功能。 ## 配置格式 > XProxy 支持 JSON 、YAML 与 TOML 格式的配置文件,其中 `.json` 与 `.toml` 后缀的文件分别以 JSON 与 TOML 格式解析,其余将以 YAML 格式解析。 XProxy 的配置文件包含以下部分: ```yaml proxy: ··· 代理选项 ··· network: ··· 网络选项 ··· asset: ··· 路由资源 ··· custom: ··· 自定义脚本 ··· radvd: ··· IPv6路由广播 ··· dhcp: ··· DHCP服务选项 ··· ``` ### 代理选项 ```yaml # 以下配置仅为示范 proxy: bin: xray log: info http: web: 8080 socks: proxy4: 1094 direct4: 1084 proxy6: 1096 direct6: 1086 addon: - tag: metrics port: 9090 protocol: dokodemo-door settings: address: 127.0.0.1 sniff: enable: true redirect: false exclude: - Mijia Cloud - courier.push.apple.com ``` > 入站代理中内置 `tproxy4` 与 `tproxy6` 两个接口,分别对应 IPv4 与 IPv6 的透明代理,可作为 `inboundTag` 在路由中引用。 + `bin` :指定内核名称,默认为 `xray` > 自 `1.0.2` 起,XProxy 镜像仅自带 `xray` 内核,其他内核需要用户自行添加。 > > 例如在 Docker 启动命令中加入 `-v {V2RAY_BIN}:/usr/bin/v2ray` 可以将 `v2ray` 内核添加到容器中,在 `bin` 选项中指定内核名称即可生效,或者使用 `PROXY_BIN=v2ray` 环境变量指定。 + `log` :代理日志级别,可选 `debug` 、`info` 、`warning` 、`error` 、`none` ,默认为 `warning` + `http` 与 `socks` :配置 http 与 socks5 入站代理,使用 `key: value` 格式,前者指定入站标志(路由配置中的 inboundTag),后者指定监听端口,默认为空。 + `addon` :自定义入站配置,每一项为单个内核 inbound 接口,具体格式可见[内核文档](https://xtls.github.io/config/inbound.html#inboundobject),默认为空。 + `sniff` :嗅探选项,用于获取透明代理中的连接域名: + `enable` :是否启用嗅探功能,默认为 `false` + `redirect` :是否使用嗅探结果覆盖目标地址,默认为 `false`(v2ray 内核不支持) + `exclude` :不进行覆盖的域名列表,默认为空(仅 xray 内核支持) ### 网络选项 ```yaml # 以下配置仅为示范 network: dev: eth0 dns: - 223.6.6.6 - 119.29.29.29 ipv4: gateway: 192.168.2.2 address: 192.168.2.4/24 ipv6: gateway: fc00::2 address: fc00::4/64 bypass: - 169.254.0.0/16 - 224.0.0.0/3 - fc00::/7 - fe80::/10 - ff00::/8 exclude: - 192.168.2.2 - 192.168.2.240/28 ``` + `dev` :指定运行的网卡,一般与物理网卡同名,默认为空。 + `dns` :指定系统默认 DNS 服务器,留空时保持原配置不变,默认为空。 + `ipv4` 与 `ipv6` :指定 IPv4 与 IPv6 的网络信息,其中 `gateway` 为上游网关地址,`address` 为虚拟网关地址(CIDR 格式,包含子网长度),不填写时保持不变,默认为空。 + `bypass` :绕过代理的目标网段或 IP,默认为空,建议绕过以下5个网段: + `169.254.0.0/16` :IPv4 链路本地地址 + `224.0.0.0/3` :D类多点播送地址,E类保留地址 + `fc00::/7` :IPv6 唯一本地地址 + `fe80::/10` :IPv6 链路本地地址 + `ff00::/8` :IPv6 组播地址 + `exclude` :不代理的来源网段或 IP > `bypass` 与 `exclude` 中指定的 IP 或 CIDR,在运行时将不会被 TProxy 捕获,即不进入用户态的代理路由,相当于无损耗的直连。 ### 路由资源 ```yaml # 以下配置仅为示范 asset: disable: false update: cron: "0 5 6 * * *" # 每天凌晨06点05分更新 proxy: "socks5://[IP]:[PORT]" # 通过 socks5 代理更新资源 url: geoip.dat: "https://cdn.dnomd343.top/v2ray-rules-dat/geoip.dat" geosite.dat: "https://cdn.dnomd343.top/v2ray-rules-dat/geosite.dat" ``` + `disable` :是否关闭路由资源文件载入,默认为 `false` + `cron` :触发更新的 Cron 表达式,留空时关闭自动升级,默认为空。 + `proxy` :通过指定的代理服务更新资源文件,留空时直连更新,默认为空。 + `url` :更新的文件名及下载地址,文件保存至 `assets` 中,默认为空。 > 默认链接为 `Loyalsoldier/v2ray-rules-dat` 的镜像,如果您可以正常访问 Github,请换用以下 URL: > > + `https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat` > > + `https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat` ### 自定义脚本 ```yaml # 以下配置仅为示范 # fc00::4 tcp/53 & udp/53 <---> fc00::3 tcp/5353 & udp/5353 # 192.168.2.4 tcp/53 & udp/53 <---> 192.168.2.3 tcp/53 & udp/5353 custom: pre: - "iptables -t nat -A PREROUTING -d 192.168.2.4 -p udp --dport 53 -j DNAT --to-destination 192.168.2.3:5353" - "iptables -t nat -A POSTROUTING -d 192.168.2.3 -p udp --dport 5353 -j SNAT --to 192.168.2.4" - "iptables -t nat -A PREROUTING -d 192.168.2.4 -p tcp --dport 53 -j DNAT --to-destination 192.168.2.3:5353" - "iptables -t nat -A POSTROUTING -d 192.168.2.3 -p tcp --dport 5353 -j SNAT --to 192.168.2.4" - "ip6tables -t nat -A PREROUTING -d fc00::4 -p udp --dport 53 -j DNAT --to-destination [fc00::3]:5353" - "ip6tables -t nat -A POSTROUTING -d fc00::3 -p udp --dport 5353 -j SNAT --to fc00::4" - "ip6tables -t nat -A PREROUTING -d fc00::4 -p tcp --dport 53 -j DNAT --to-destination [fc00::3]:5353" - "ip6tables -t nat -A POSTROUTING -d fc00::3 -p tcp --dport 5353 -j SNAT --to fc00::4" post: - "echo Here is post process" - "echo Goodbye" ``` > 本功能用于注入自定义功能,基于 Alpine 的 ash 执行,可能不支持部分 bash 语法。 + `pre` :自定义脚本命令,在代理启动前执行,默认为空。 + `post` :自定义脚本命令,在服务关闭前执行,默认为空。 ### IPv6路由广播 Radvd 有大量配置选项,`XProxy` 均对其保持兼容,以下仅介绍部分常用选项,更多详细参数可参考[man文档](https://www.systutorials.com/docs/linux/man/5-radvd.conf/)。 > 注意以下的 `on` 与 `off` 为字符串,但在部分 YAML 库中可能被解析成布尔值,为了安全起见,下游项目请注意转换时添加引号限定。 ```yaml # 以下配置仅为示范 radvd: log: 3 dev: eth0 enable: true option: AdvSendAdvert: on AdvManagedFlag: off AdvOtherConfigFlag: off client: - fe80::215:5dff:feb1:df9b - fe80::21d:72ff:fe96:aaff prefix: cidr: fc00::/64 option: AdvOnLink: on AdvAutonomous: on AdvRouterAddr: off AdvValidLifetime: 43200 AdvPreferredLifetime: 7200 route: cidr: "" option: null rdnss: ip: - fc00::3 - fc00::4 option: null dnssl: suffix: - scut.343.re option: null ``` + `log` :RADVD 日志级别,可选 `0-5`,数值越大越详细,默认为 `0` + `dev` :执行 RA 广播的网卡,`enable` 为 `true` 时必选,一般与 `network` 中配置相同,默认为空。 + `enable` :是否启动 RADVD,默认为 `false` + `option` :RADVD 主选项,完整参数列表查看[这里](https://code.tools/man/5/radvd.conf/#lbAD): + `AdvSendAdvert` :是否开启 RA 报文广播,启用 IPv6 时必须打开,默认为 `off` + `AdvManagedFlag` :指示 IPv6 管理地址配置,即 M 位,默认为 `off` + `AdvOtherConfigFlag` :指示 IPv6 其他有状态配置,即 O 位,默认为 `off` + > M 位和 O 位的详细定义在 [RFC4862](https://www.rfc-editor.org/rfc/rfc4862) 中给出: + `M=off` 且 `O=off` :使用 `Stateless` 模式,设备通过 RA 广播的前缀,配合 `EUI-64` 算法直接得到接口地址,即 `SLAAC` 方式。 + `M=off` 且 `O=on` :使用 `Stateless DHCPv6` 模式,设备通过 RA 广播前缀与 `EUI-64` 计算接口地址,同时从 `DHCPv6` 获取 DNS 等其他配置。 + `M=on` 且 `O=on` :使用 `Stateful DHCPv6` 模式,设备通过 `DHCPv6` 获取地址以及 DNS 等其他配置。 + `M=on` 且 `O=off` :理论上不存在此配置。 + `client` :配置此项后,仅发送 RA 通告到指定 IPv6 单播地址而非组播地址,默认为空(组播发送) + `prefix` :IPv6 地址前缀配置,`cidr` 指定分配的前缀及掩码长度,`option` 指定[前缀选项](https://code.tools/man/5/radvd.conf/#lbAE)。 + `route` :指定 IPv6 路由,`cidr` 为通告的路由 CIDR(注意客户端仅将 RA 报文来源链路地址设置为 IPv6 网关,此处设置并不能更改路由网关地址),`option` 指定[路由选项](https://code.tools/man/5/radvd.conf/#lbAF)。 + `rdnss` :递归 DNS 服务器地址,`ip` 指定 IPv6 下的 DNS 服务器列表,`option` 指定[RDNSS选项](https://code.tools/man/5/radvd.conf/#lbAG)。 + `dnssl` :DNS 搜寻域名,`suffix` 指定 DNS 解析的搜寻后缀列表,`option` 指定[DNSSL选项](https://code.tools/man/5/radvd.conf/#lbAH)。 > `RDNSS` 与 `DNSSL` 在 [RFC6106](https://www.rfc-editor.org/rfc/rfc6106) 中定义,将 DNS 配置信息直接放置在 RA 报文中发送,使用 `SLAAC` 时无需 `DHCPv6` 即可获取 DNS 服务器,但旧版本 Windows 与 Android 等系统不支持该功能。 ### DHCP服务选项 > DHCP 与 DHCPv6 功能由 [ISC-DHCP](https://www.isc.org/dhcp/) 项目提供。 ```yaml # 以下配置仅为示范 dhcp: ipv4: enable: false config: | default-lease-time 600; max-lease-time 7200; subnet 192.168.2.0 netmask 255.255.255.0 { range 192.168.2.100 192.168.2.200; } host example { hardware ethernet 03:48:c0:5d:bd:95; fixed-address 192.168.2.233; } ipv6: enable: false config: | ... ``` + `ipv4` 和 `ipv6` :分别配置 DHCPv4 与 DHCPv6 服务。 + `enable` :是否启动 DHCP 服务,默认为 `false` + `config` :DHCP 服务配置文件,具体配置内容参考[man文档](https://linux.die.net/man/5/dhcpd.conf)。 ## 部署流程 ### 1. 初始配置 > XProxy 基于 macvlan 网络,开启网卡混杂模式后可以捕获非本机 MAC 地址的数据包,以此模拟出不同 MAC 地址的网卡。 ```bash # 开启混杂模式,网卡按实际情况指定 $ ip link set eth0 promisc on # 启用IPv6内核模块 $ modprobe ip6table_filter ``` 在 Docker 中创建 macvlan 网络: ```bash # 网络配置按实际情况指定 docker network create -d macvlan \ --subnet={IPv4网段} --gateway={IPv4网关} \ --subnet={IPv6网段} --gateway={IPv6网关} \ --ipv6 -o parent=eth0 macvlan # 在eth0网卡上运行 ``` ### 2. 开始部署 > 本项目基于 Docker 构建,在 [Docker Hub](https://hub.docker.com/r/dnomd343/xproxy) 或 [Github Package](https://github.com/dnomd343/XProxy/pkgs/container/xproxy) 可以查看已构建的各版本镜像。 XProxy 同时发布在多个镜像源上: + `Docker Hub` :`dnomd343/xproxy` + `Github Package` :`ghcr.io/dnomd343/xproxy` + `阿里云镜像` :`registry.cn-shenzhen.aliyuncs.com/dnomd343/xproxy` > 下述命令中,容器路径可替换为上述其他源,国内网络建议首选阿里云仓库。 使用以下命令启动虚拟网关,配置文件将存储在本机 `/etc/xproxy/` 目录下: ```bash docker run --restart always \ --privileged --network macvlan -dt \ --name xproxy --hostname xproxy \ # 可选,指定容器名称与主机名 --volume /etc/xproxy/:/xproxy/ \ --volume /etc/timezone:/etc/timezone:ro \ # 以下两句可选,用于映射宿主机时区信息(容器内默认为UTC0时区) --volume /etc/localtime:/etc/localtime:ro \ dnomd343/xproxy:latest ``` 成功运行以后,存储目录将生成以下文件夹: + `assets` :存储路由资源文件 + `config` :存储代理配置文件 + `log` :存储日志文件 + `dhcp` :存储 DHCP 数据库文件(仅当 DHCP 服务开启) #### 路由资源文件夹 `assets` 目录默认放置 `geoip.dat` 与 `geosite.dat` 路由规则文件,分别存储IP与域名归属信息,在 `update` 中配置的自动更新将保存到此处;本目录亦可放置自定义规则文件,在[路由配置](https://xtls.github.io/config/routing.html#ruleobject)中以 `ext:${FILE}:tag` 格式引用。 #### 代理配置文件夹 `config` 目录存储代理配置文件,所有 `.json` 后缀文件均会被载入,用户可配置除 `inbounds` 与 `log` 以外的所有代理选项,多配置文件需要注意[合并规则](https://xtls.github.io/config/features/multiple.html#%E8%A7%84%E5%88%99%E8%AF%B4%E6%98%8E)。 为了正常工作,容器初始化时会载入以下 `outbounds.json` 作为默认出站配置,其指定所有流量为直连: ```json { "outbounds": [ { "protocol": "freedom", "settings": {} } ] } ``` #### 日志文件夹 `log` 目录用于放置日志文件 + `xproxy.log` 记录 XProxy 工作信息 + `access.log` 记录代理流量连接信息 + `error.log` 记录代理连接错误信息 + 若启用RADVD功能,其日志将保存到 `radvd.log` 中 ### 3. 调整配置文件 容器首次初始化时将生成默认配置文件 `xproxy.yml` ,其内容如下: ```yaml # default configure file for xproxy proxy: core: xray log: warning network: bypass: - 169.254.0.0/16 - 224.0.0.0/3 - fc00::/7 - fe80::/10 - ff00::/8 asset: update: cron: "0 5 6 * * *" url: geoip.dat: "https://cdn.dnomd343.top/v2ray-rules-dat/geoip.dat" geosite.dat: "https://cdn.dnomd343.top/v2ray-rules-dat/geosite.dat" ``` 用户需要根据实际需求更改配置文件,保存以后重启容器即可生效: ```bash docker restart xproxy ``` 如果配置文件出错,`XProxy` 将无法正常工作,您可以使用以下命令查看工作日志: ```bash docker logs -f xproxy ``` ### 4. 宿主机访问虚拟网关 > 这一步旨在让宿主机能够使用虚拟网关,若无此需求可以跳过。 由于 macvlan 限制,宿主机网卡无法直接与虚拟网关通讯,需要另外配置网桥才可连接。 > 以下为配置基于 Debian,基于 RH、Arch 等的发行版配置略有不同。 编辑网卡配置文件: ```bash vim /etc/network/interfaces ``` 补充如下配置,具体网络信息需要按实际情况指定: ```ini auto eth0 # 宿主机物理网卡 iface eth0 inet manual auto macvlan iface macvlan inet static address 192.168.2.34 # 宿主机静态IP地址 netmask 255.255.255.0 # 子网掩码 gateway 192.168.2.2 # 虚拟网关IP地址 dns-nameservers 192.168.2.3 # DNS主服务器 dns-nameservers 192.168.2.1 # DNS备用服务器 pre-up ip link add macvlan link eth0 type macvlan mode bridge # 宿主机网卡上创建网桥 post-down ip link del macvlan link eth0 type macvlan mode bridge # 退出时删除网桥 ``` 重启宿主机网络生效(或直接重启系统): ```bash $ /etc/init.d/networking restart [ ok ] Restarting networking (via systemctl): networking.service. ``` ### 5. 局域网设备访问 > 对于手动配置了静态 IP 的设备,需要修改网关地址为虚拟网关 IP 地址。 配置完成后,容器 IP 即为虚拟网关地址,内网其他设备的网关设置为该地址即可被透明代理,因此这里需要配置 DHCP 与 RADVD 路由广播,让内网设备自动接入虚拟网关。 > 您可以监视 `log/access.log` 文件,设备正常接入后会在此输出访问日志。 + IPv4 下,修改内网 DHCP 服务器配置(一般位于路由器上),将网关改为容器 IP 地址,保存后重新接入设备即可生效。 + IPv6 下,你需要关闭路由或上级网络的 RA 广播功能,然后开启配置中的 RADVD 选项,如果需要使用 DHCPv6 ,可调整配置中的 M 位和 O 位开启状态,保存后将设备重新接入网络即可。 ## 演示实例 由于 XProxy 涉及较为复杂的网络配置,这里准备了两个详细的实例供您了解: + 实例1. [使用 XProxy 绕过校园网认证登录](./docs/campus_network_cracking.md) + 实例2. [家庭网络的 IPv4 与 IPv6 透明代理](./docs/dual_stack_network_proxy.md) ## 开发相关 ### 运行参数 XProxy 默认使用 `/xproxy` 作为存储文件夹,该文件夹映射到外部主机作为持久存储,您可以使用 `EXPOSE_DIR` 环境变量修改该文件夹路径。 XProxy 默认使用 `xray` 作为代理内核,您可以使用 `PROXY_BIN` 环境变量来指定其他内核。 + `--config` :指定配置文件名称,默认为 `xproxy.yml` + `--debug` :开启调试模式,输出日志切换到 DEBUG 级别。 ### TProxy 配置 XProxy 默认使用以下配置: + IPv4 路由表号:`104`,使用 `IPV4_TABLE` 环境变量修改。 + IPv6 路由表号:`106`,使用 `IPV6_TABLE` 环境变量修改。 + IPv4 透明代理端口:`7288`,使用 `IPV4_TPROXY` 环境变量修改。 + IPv6 透明代理端口:`7289`,使用 `IPV6_TPROXY` 环境变量修改。 ### 容器构建 #### 本地构建 ```bash $ git clone https://github.com/dnomd343/XProxy.git $ cd ./XProxy/ $ docker build -t xproxy . ··· ``` #### 交叉构建 > XProxy 针对 `buildkit` 进行优化,使用 `buildx` 命令可加快构建速度 ```bash $ git clone https://github.com/dnomd343/XProxy.git $ cd ./XProxy/ $ docker buildx build \ -t dnomd343/xproxy \ -t ghcr.io/dnomd343/xproxy \ -t registry.cn-shenzhen.aliyuncs.com/dnomd343/xproxy \ --platform="linux/amd64,linux/arm64,linux/386,linux/arm/v7" . --push ``` ## 许可证 MIT ©2022 [@dnomd343](https://github.com/dnomd343) ================================================ FILE: cmd/asset/asset.go ================================================ package asset import ( "XProxy/cmd/common" log "github.com/sirupsen/logrus" "path" ) func extractFile(archive string, geoFile string, targetDir string) { // extract `.dat` file into targetDir filePath := path.Join(targetDir, geoFile) if common.IsFileExist(filePath) { log.Debugf("Asset %s exist -> skip extract", geoFile) return } log.Infof("Extract asset file -> %s", filePath) common.RunCommand("tar", "xf", archive, geoFile, "-C", targetDir) } func Load(assetFile string, assetDir string) { common.CreateFolder(assetDir) extractFile(assetFile, "geoip.dat", assetDir) extractFile(assetFile, "geosite.dat", assetDir) } ================================================ FILE: cmd/asset/update.go ================================================ package asset import ( "XProxy/cmd/common" "github.com/robfig/cron" log "github.com/sirupsen/logrus" "os" "os/signal" "path" "syscall" ) type Config struct { Disable bool `yaml:"disable" json:"disable" toml:"disable"` Update struct { Proxy string `yaml:"proxy" json:"proxy" toml:"proxy"` Cron string `yaml:"cron" json:"cron" toml:"cron"` Url map[string]string `yaml:"url" json:"url" toml:"url"` } } func updateAsset(urls map[string]string, assetDir string, updateProxy string) { // download new assets defer func() { if err := recover(); err != nil { log.Errorf("Update failed -> %v", err) } }() if len(urls) != 0 { log.Info("Start update assets") for file, url := range urls { if !common.DownloadFile(url, path.Join(assetDir, file), updateProxy) { // maybe override old asset log.Infof("Try to download asset `%s` again", file) common.DownloadFile(url, path.Join(assetDir, file), updateProxy) // download retry } } log.Infof("Assets update complete") } } func AutoUpdate(config *Config, assetDir string) { // set cron task for auto update if config.Update.Cron != "" { autoUpdate := cron.New() _ = autoUpdate.AddFunc(config.Update.Cron, func() { // cron function updateAsset(config.Update.Url, assetDir, config.Update.Proxy) }) autoUpdate.Start() } updateChan := make(chan os.Signal, 1) go func() { for { <-updateChan log.Infof("Receive SIGALRM -> update assets") updateAsset(config.Update.Url, assetDir, config.Update.Proxy) } }() signal.Notify(updateChan, syscall.SIGALRM) } ================================================ FILE: cmd/common/file.go ================================================ package common import ( "github.com/andybalholm/brotli" "github.com/go-http-utils/headers" "github.com/klauspost/compress/flate" "github.com/klauspost/compress/gzip" log "github.com/sirupsen/logrus" "io" "io/ioutil" "net/http" "net/url" "os" "strings" ) func CreateFolder(folderPath string) { folder, err := os.Stat(folderPath) if err == nil && folder.IsDir() { // folder exist -> skip create process return } log.Debugf("Create folder -> %s", folderPath) if err := os.MkdirAll(folderPath, 0755); err != nil { log.Errorf("Failed to create folder -> %s", folderPath) } } func IsFileExist(filePath string) bool { s, err := os.Stat(filePath) if err != nil { // file or folder not exist return false } return !s.IsDir() } func WriteFile(filePath string, content string, overwrite bool) { if !overwrite && IsFileExist(filePath) { // file exist and don't overwrite log.Debugf("File `%s` exist -> skip write", filePath) return } log.Debugf("Write file `%s` -> \n%s", filePath, content) if err := os.WriteFile(filePath, []byte(content), 0644); err != nil { log.Panicf("Failed to write `%s` -> %v", filePath, err) } } func ListFiles(folderPath string, suffix string) []string { var fileList []string files, err := ioutil.ReadDir(folderPath) if err != nil { log.Panicf("Failed to list folder -> %s", folderPath) } for _, file := range files { if strings.HasSuffix(file.Name(), suffix) { fileList = append(fileList, file.Name()) } } return fileList } func CopyFile(source string, target string) { log.Infof("Copy file `%s` => `%s`", source, target) if IsFileExist(target) { log.Debugf("File `%s` will be overridden", target) } srcFile, err := os.Open(source) defer srcFile.Close() if err != nil { log.Panicf("Failed to open file -> %s", source) } dstFile, err := os.OpenFile(target, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0644) defer dstFile.Close() if err != nil { log.Panicf("Failed to open file -> %s", target) } if _, err = io.Copy(dstFile, srcFile); err != nil { log.Panicf("Failed to copy from `%s` to `%s`", source, target) } } func DownloadBytes(fileUrl string, proxyUrl string) ([]byte, error) { client := http.Client{} if proxyUrl == "" { log.Infof("Download `%s` without proxy", fileUrl) } else { // use proxy for download log.Infof("Download `%s` via `%s`", fileUrl, proxyUrl) rawUrl, _ := url.Parse(proxyUrl) client = http.Client{ Transport: &http.Transport{ Proxy: http.ProxyURL(rawUrl), }, } } req, err := http.NewRequest("GET", fileUrl, nil) if err != nil { log.Errorf("Failed to create http request") return nil, err } req.Header.Set(headers.AcceptEncoding, "gzip, deflate, br") resp, err := client.Do(req) if err != nil { log.Errorf("Failed to execute http GET request") return nil, err } if resp != nil { defer resp.Body.Close() log.Debugf("Remote data downloaded successfully") } switch resp.Header.Get(headers.ContentEncoding) { case "br": log.Debugf("Downloaded content using brolti encoding") return io.ReadAll(brotli.NewReader(resp.Body)) case "gzip": log.Debugf("Downloaded content using gzip encoding") gr, err := gzip.NewReader(resp.Body) if err != nil { return nil, err } return io.ReadAll(gr) case "deflate": log.Debugf("Downloaded content using deflate encoding") zr := flate.NewReader(resp.Body) defer zr.Close() return io.ReadAll(zr) default: return io.ReadAll(resp.Body) } } func DownloadFile(fileUrl string, filePath string, proxyUrl string) bool { log.Debugf("File download `%s` => `%s`", fileUrl, filePath) data, err := DownloadBytes(fileUrl, proxyUrl) if err != nil { log.Errorf("Download `%s` error -> %v", fileUrl, err) return false } file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0644) defer file.Close() if err != nil { log.Panicf("Open `%s` error -> %v", filePath, err) return false } if _, err = file.Write(data); err != nil { log.Errorf("File `%s` save error -> %v", filePath, err) return false } log.Infof("Download success `%s` => `%s`", fileUrl, filePath) return true } ================================================ FILE: cmd/common/func.go ================================================ package common import ( "encoding/json" log "github.com/sirupsen/logrus" "net" "os/exec" "strings" "syscall" ) func isIP(ipAddr string, isCidr bool) bool { if !isCidr { return net.ParseIP(ipAddr) != nil } _, _, err := net.ParseCIDR(ipAddr) return err == nil } func IsIPv4(ipAddr string, isCidr bool) bool { return isIP(ipAddr, isCidr) && strings.Contains(ipAddr, ".") } func IsIPv6(ipAddr string, isCidr bool) bool { return isIP(ipAddr, isCidr) && strings.Contains(ipAddr, ":") } func JsonEncode(raw interface{}) string { jsonOutput, _ := json.MarshalIndent(raw, "", " ") // json encode return string(jsonOutput) } func RunCommand(command ...string) (int, string) { log.Debugf("Running system command -> %v", command) process := exec.Command(command[0], command[1:]...) output, _ := process.CombinedOutput() log.Debugf("Command %v -> \n%s", command, string(output)) code := process.ProcessState.Sys().(syscall.WaitStatus).ExitStatus() if code != 0 { log.Warningf("Command %v return code %d", command, code) } return code, string(output) } ================================================ FILE: cmd/config/decode.go ================================================ package config import ( "XProxy/cmd/asset" "XProxy/cmd/common" "XProxy/cmd/custom" "XProxy/cmd/dhcp" "XProxy/cmd/proxy" "XProxy/cmd/radvd" "encoding/json" "github.com/BurntSushi/toml" log "github.com/sirupsen/logrus" "gopkg.in/yaml.v3" "net/url" ) type NetConfig struct { Gateway string `yaml:"gateway" json:"gateway" toml:"gateway"` // network gateway Address string `yaml:"address" json:"address" toml:"address"` // network address } type RawConfig struct { Asset asset.Config `yaml:"asset" json:"asset" toml:"asset"` Radvd radvd.Config `yaml:"radvd" json:"radvd" toml:"radvd"` DHCP dhcp.Config `yaml:"dhcp" json:"dhcp" toml:"dhcp"` Proxy proxy.Config `yaml:"proxy" json:"proxy" toml:"proxy"` Custom custom.Config `yaml:"custom" json:"custom" toml:"custom"` Network struct { Dev string `yaml:"dev" json:"dev" toml:"dev"` DNS []string `yaml:"dns" json:"dns" toml:"dns"` ByPass []string `yaml:"bypass" json:"bypass" toml:"bypass"` Exclude []string `yaml:"exclude" json:"exclude" toml:"exclude"` IPv4 NetConfig `yaml:"ipv4" json:"ipv4" toml:"ipv4"` IPv6 NetConfig `yaml:"ipv6" json:"ipv6" toml:"ipv6"` } `yaml:"network" json:"network" toml:"network"` } func configDecode(raw []byte, fileSuffix string) RawConfig { var config RawConfig log.Debugf("Config raw content -> \n%s", string(raw)) if fileSuffix == ".json" { if err := json.Unmarshal(raw, &config); err != nil { // json format decode log.Panicf("Decode JSON config file error -> %v", err) } } else if fileSuffix == ".toml" { if err := toml.Unmarshal(raw, &config); err != nil { // toml format decode log.Panicf("Decode TOML config file error -> %v", err) } } else { if err := yaml.Unmarshal(raw, &config); err != nil { // yaml format decode log.Panicf("Decode YAML config file error -> %v", err) } } log.Debugf("Decoded configure -> %v", config) return config } func decodeDev(rawConfig *RawConfig, config *Config) { config.Dev = rawConfig.Network.Dev if config.Dev == "" { setV4 := rawConfig.Network.IPv4.Address != "" || rawConfig.Network.IPv4.Gateway != "" setV6 := rawConfig.Network.IPv6.Address != "" || rawConfig.Network.IPv6.Gateway != "" if setV4 || setV6 { log.Panicf("Missing dev option in network settings") } } log.Debugf("Network device -> %s", config.Dev) } func decodeDns(rawConfig *RawConfig, config *Config) { for _, address := range rawConfig.Network.DNS { // dns options if common.IsIPv4(address, false) || common.IsIPv6(address, false) { config.DNS = append(config.DNS, address) } else { log.Panicf("Invalid DNS server -> %s", address) } } log.Debugf("DNS server -> %v", config.DNS) } func decodeBypass(rawConfig *RawConfig, config *Config) { for _, address := range rawConfig.Network.ByPass { // bypass options if common.IsIPv4(address, true) || common.IsIPv4(address, false) { config.IPv4.Bypass = append(config.IPv4.Bypass, address) } else if common.IsIPv6(address, true) || common.IsIPv6(address, false) { config.IPv6.Bypass = append(config.IPv6.Bypass, address) } else { log.Panicf("Invalid bypass IP or CIDR -> %s", address) } } log.Debugf("IPv4 bypass -> %s", config.IPv4.Bypass) log.Debugf("IPv6 bypass -> %s", config.IPv6.Bypass) } func decodeExclude(rawConfig *RawConfig, config *Config) { for _, address := range rawConfig.Network.Exclude { // exclude options if common.IsIPv4(address, true) || common.IsIPv4(address, false) { config.IPv4.Exclude = append(config.IPv4.Exclude, address) } else if common.IsIPv6(address, true) || common.IsIPv6(address, false) { config.IPv6.Exclude = append(config.IPv6.Exclude, address) } else { log.Panicf("Invalid exclude IP or CIDR -> %s", address) } } log.Debugf("IPv4 exclude -> %s", config.IPv4.Exclude) log.Debugf("IPv6 exclude -> %s", config.IPv6.Exclude) } func decodeIPv4(rawConfig *RawConfig, config *Config) { config.IPv4.Address = rawConfig.Network.IPv4.Address config.IPv4.Gateway = rawConfig.Network.IPv4.Gateway if config.IPv4.Address != "" && !common.IsIPv4(config.IPv4.Address, true) { log.Panicf("Invalid IPv4 address (CIDR) -> %s", config.IPv4.Address) } if config.IPv4.Gateway != "" && !common.IsIPv4(config.IPv4.Gateway, false) { log.Panicf("Invalid IPv4 gateway -> %s", config.IPv4.Gateway) } log.Debugf("IPv4 -> address = %s | gateway = %s", config.IPv4.Address, config.IPv4.Gateway) } func decodeIPv6(rawConfig *RawConfig, config *Config) { config.IPv6.Address = rawConfig.Network.IPv6.Address config.IPv6.Gateway = rawConfig.Network.IPv6.Gateway if config.IPv6.Address != "" && !common.IsIPv6(config.IPv6.Address, true) { log.Panicf("Invalid IPv6 address (CIDR) -> %s", config.IPv6.Address) } if config.IPv6.Gateway != "" && !common.IsIPv6(config.IPv6.Gateway, false) { log.Panicf("Invalid IPv6 gateway -> %s", config.IPv6.Gateway) } log.Debugf("IPv6 -> address = %s | gateway = %s", config.IPv6.Address, config.IPv6.Gateway) } func decodeProxy(rawConfig *RawConfig, config *Config) { config.Proxy = rawConfig.Proxy if config.Proxy.Bin == "" { config.Proxy.Bin = "xray" // default proxy bin } log.Debugf("Proxy bin -> %s", config.Proxy.Bin) log.Debugf("Proxy log level -> %s", config.Proxy.Log) log.Debugf("Http inbounds -> %v", config.Proxy.Http) log.Debugf("Socks5 inbounds -> %v", config.Proxy.Socks) log.Debugf("Add-on inbounds -> %v", config.Proxy.AddOn) log.Debugf("Connection sniff -> %t", config.Proxy.Sniff.Enable) log.Debugf("Connection redirect -> %t", config.Proxy.Sniff.Redirect) log.Debugf("Connection sniff exclude -> %v", config.Proxy.Sniff.Exclude) } func decodeRadvd(rawConfig *RawConfig, config *Config) { config.Radvd = rawConfig.Radvd if config.Radvd.Enable && config.Radvd.Dev == "" { log.Panicf("Radvd enabled without dev option") } log.Debugf("Radvd log level -> %d", config.Radvd.Log) log.Debugf("Radvd network dev -> %s", config.Radvd.Dev) log.Debugf("Radvd enable -> %t", config.Radvd.Enable) log.Debugf("Radvd options -> %v", config.Radvd.Option) log.Debugf("Radvd prefix -> %v", config.Radvd.Prefix) log.Debugf("Radvd route -> %v", config.Radvd.Route) log.Debugf("Radvd clients -> %v", config.Radvd.Client) log.Debugf("Radvd RDNSS -> %v", config.Radvd.RDNSS) log.Debugf("Radvd DNSSL -> %v", config.Radvd.DNSSL) } func decodeDhcp(rawConfig *RawConfig, config *Config) { config.DHCP = rawConfig.DHCP log.Debugf("DHCPv4 enable -> %t", config.DHCP.IPv4.Enable) log.Debugf("DHCPv4 config -> \n%s", config.DHCP.IPv4.Configure) log.Debugf("DHCPv6 enable -> %t", config.DHCP.IPv6.Enable) log.Debugf("DHCPv6 config -> \n%s", config.DHCP.IPv6.Configure) } func decodeUpdate(rawConfig *RawConfig, config *Config) { config.Asset = rawConfig.Asset if config.Asset.Update.Proxy != "" { _, err := url.Parse(config.Asset.Update.Proxy) // check proxy info if err != nil { log.Panicf("Invalid asset update proxy -> %s", config.Asset.Update.Proxy) } } log.Debugf("Asset disable -> %t", config.Asset.Disable) log.Debugf("Asset update proxy -> %s", config.Asset.Update.Proxy) log.Debugf("Asset update cron -> %s", config.Asset.Update.Cron) log.Debugf("Asset update urls -> %v", config.Asset.Update.Url) } func decodeCustom(rawConfig *RawConfig, config *Config) { config.Custom = rawConfig.Custom log.Debugf("Custom pre-script -> %v", config.Custom.Pre) log.Debugf("Custom post-script -> %v", config.Custom.Post) } ================================================ FILE: cmd/config/default.go ================================================ package config import ( "XProxy/cmd/common" "bytes" "encoding/json" "github.com/BurntSushi/toml" log "github.com/sirupsen/logrus" "gopkg.in/yaml.v3" "path" ) var defaultConfig = map[string]interface{}{ "proxy": map[string]string{ "log": "warning", }, "network": map[string]interface{}{ "bypass": []string{ "169.254.0.0/16", "224.0.0.0/3", "fc00::/7", "fe80::/10", "ff00::/8", }, }, "asset": map[string]interface{}{ "update": map[string]interface{}{ "cron": "0 5 6 * * *", "url": map[string]string{ "geoip.dat": "https://cdn.dnomd343.top/v2ray-rules-dat/geoip.dat", "geosite.dat": "https://cdn.dnomd343.top/v2ray-rules-dat/geosite.dat", }, }, }, } func toJSON(config interface{}) string { // convert to JSON string jsonRaw, _ := json.MarshalIndent(config, "", " ") return string(jsonRaw) } func toYAML(config interface{}) string { // convert to YAML string buf := new(bytes.Buffer) encoder := yaml.NewEncoder(buf) encoder.SetIndent(2) // with 2 space indent _ = encoder.Encode(config) return buf.String() } func toTOML(config interface{}) string { // convert to TOML string buf := new(bytes.Buffer) _ = toml.NewEncoder(buf).Encode(config) return buf.String() } func loadDefaultConfig(configFile string) { log.Infof("Load default configure -> %s", configFile) suffix := path.Ext(configFile) if suffix == ".json" { common.WriteFile(configFile, toJSON(defaultConfig), false) // JSON format } else if suffix == ".toml" { common.WriteFile(configFile, toTOML(defaultConfig), false) // TOML format } else { common.WriteFile(configFile, toYAML(defaultConfig), false) // YAML format } } ================================================ FILE: cmd/config/main.go ================================================ package config import ( "XProxy/cmd/asset" "XProxy/cmd/common" "XProxy/cmd/custom" "XProxy/cmd/dhcp" "XProxy/cmd/network" "XProxy/cmd/proxy" "XProxy/cmd/radvd" log "github.com/sirupsen/logrus" "os" "path" ) type Config struct { Dev string DNS []string IPv4 network.Config IPv6 network.Config Proxy proxy.Config Asset asset.Config Radvd radvd.Config Custom custom.Config DHCP dhcp.Config } func Load(configFile string, config *Config) { if !common.IsFileExist(configFile) { // configure not exist -> load default loadDefaultConfig(configFile) } raw, err := os.ReadFile(configFile) // read configure content if err != nil { log.Panicf("Failed to open %s -> %v", configFile, err) } rawConfig := configDecode(raw, path.Ext(configFile)) // decode configure content decodeDev(&rawConfig, config) decodeDns(&rawConfig, config) decodeBypass(&rawConfig, config) decodeExclude(&rawConfig, config) decodeIPv4(&rawConfig, config) decodeIPv6(&rawConfig, config) decodeProxy(&rawConfig, config) decodeUpdate(&rawConfig, config) decodeCustom(&rawConfig, config) decodeRadvd(&rawConfig, config) decodeDhcp(&rawConfig, config) } ================================================ FILE: cmd/controller.go ================================================ package main import ( "XProxy/cmd/asset" "XProxy/cmd/common" "XProxy/cmd/config" "XProxy/cmd/dhcp" "XProxy/cmd/network" "XProxy/cmd/process" "XProxy/cmd/proxy" "XProxy/cmd/radvd" log "github.com/sirupsen/logrus" "os" "os/signal" "path" "strconv" "syscall" "time" ) func runProcess(env []string, command ...string) { sub := process.New(command...) sub.Run(true, env) sub.Daemon() subProcess = append(subProcess, sub) } func blockWait() { sigExit := make(chan os.Signal, 1) signal.Notify(sigExit, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGTERM) // wait until get exit signal <-sigExit } func loadRadvd(settings *config.Config) { if settings.Radvd.Enable { radvd.Load(&settings.Radvd) } else { log.Infof("Skip loading radvd") } } func loadDhcp(settings *config.Config) { common.CreateFolder(dhcp.WorkDir) if settings.DHCP.IPv4.Enable || settings.DHCP.IPv6.Enable { common.CreateFolder(path.Join(exposeDir, "dhcp")) dhcp.Load(&settings.DHCP) } } func loadAsset(settings *config.Config) { if settings.Asset.Disable { log.Infof("Skip loading asset") } else { asset.Load(assetFile, assetDir) asset.AutoUpdate(&settings.Asset, assetDir) } } func loadNetwork(settings *config.Config) { settings.IPv4.RouteTable = v4RouteTable settings.IPv4.TProxyPort = v4TProxyPort settings.IPv6.RouteTable = v6RouteTable settings.IPv6.TProxyPort = v6TProxyPort network.Load(settings.DNS, settings.Dev, &settings.IPv4, &settings.IPv6) } func loadProxy(settings *config.Config) { if proxyBin != "" { settings.Proxy.Bin = proxyBin // setting proxy bin from env } settings.Proxy.V4TProxyPort = v4TProxyPort settings.Proxy.V6TProxyPort = v6TProxyPort proxy.Load(configDir, exposeDir, &settings.Proxy) } func runProxy(settings *config.Config) { assetEnv := []string{ "XRAY_LOCATION_ASSET=" + assetDir, // xray asset folder "V2RAY_LOCATION_ASSET=" + assetDir, // v2ray / sagray asset folder } runProcess(assetEnv, settings.Proxy.Bin, "run", "-confdir", configDir) } func runRadvd(settings *config.Config) { if settings.Radvd.Enable { radvdCmd := []string{"radvd", "--nodaemon"} if settings.Radvd.Log > 0 { // with log option radvdCmd = append(radvdCmd, "--logmethod", "logfile") radvdCmd = append(radvdCmd, "--logfile", path.Join(exposeDir, "log/radvd.log")) radvdCmd = append(radvdCmd, "--debug", strconv.Itoa(settings.Radvd.Log)) } runProcess(nil, radvdCmd...) } else { log.Infof("Skip running radvd") } } func runDhcp(settings *config.Config) { leaseDir := path.Join(exposeDir, "dhcp") if settings.DHCP.IPv4.Enable { v4Leases := path.Join(leaseDir, "dhcp4.leases") v4Config := path.Join(dhcp.WorkDir, "dhcp4.conf") if !common.IsFileExist(v4Leases) { common.WriteFile(v4Leases, "", true) } runProcess(nil, "dhcpd", "-4", "-f", "-cf", v4Config, "-lf", v4Leases) time.Sleep(time.Second) // wait 1s for avoid cluttered output } else { log.Infof("Skip running DHCPv4") } if settings.DHCP.IPv6.Enable { v6Leases := path.Join(leaseDir, "dhcp6.leases") v6Config := path.Join(dhcp.WorkDir, "dhcp6.conf") if !common.IsFileExist(v6Leases) { common.WriteFile(v6Leases, "", true) } runProcess(nil, "dhcpd", "-6", "-f", "-cf", v6Config, "-lf", v6Leases) time.Sleep(time.Second) // wait 1s for avoid cluttered output } else { log.Infof("Skip running DHCPv6") } } ================================================ FILE: cmd/custom/main.go ================================================ package custom import ( log "github.com/sirupsen/logrus" "os" "os/exec" ) type Config struct { Pre []string `yaml:"pre" json:"pre" toml:"pre"` Post []string `yaml:"post" json:"post" toml:"post"` } func runScript(command string) { log.Debugf("Run script -> %s", command) cmd := exec.Command("sh", "-c", command) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr err := cmd.Start() if err != nil { log.Warningf("Script `%s` working error", command) } else { _ = cmd.Wait() } } func RunPreScript(config *Config) { for _, script := range config.Pre { log.Infof("Run pre-script command -> %s", script) runScript(script) } } func RunPostScript(config *Config) { for _, script := range config.Post { log.Infof("Run post-script command -> %s", script) runScript(script) } } ================================================ FILE: cmd/dhcp/main.go ================================================ package dhcp import ( "XProxy/cmd/common" log "github.com/sirupsen/logrus" "path" ) var WorkDir = "/etc/dhcp" type dhcpConfig struct { Enable bool `yaml:"enable" json:"enable" toml:"enable"` Configure string `yaml:"config" json:"config" toml:"config"` } type Config struct { IPv4 dhcpConfig `yaml:"ipv4" json:"ipv4" toml:"ipv4"` IPv6 dhcpConfig `yaml:"ipv6" json:"ipv6" toml:"ipv6"` } func Load(config *Config) { if config.IPv4.Enable { log.Infof("Load DHCPv4 configure") common.WriteFile(path.Join(WorkDir, "dhcp4.conf"), config.IPv4.Configure, true) } if config.IPv6.Enable { log.Infof("Load DHCPv6 configure") common.WriteFile(path.Join(WorkDir, "dhcp6.conf"), config.IPv6.Configure, true) } } ================================================ FILE: cmd/network/dns.go ================================================ package network import ( "XProxy/cmd/common" log "github.com/sirupsen/logrus" ) func loadDns(dns []string) { if len(dns) == 0 { // without dns server log.Info("Using system DNS server") return } log.Infof("Setting up DNS server -> %v", dns) dnsConfig := "" for _, dnsAddr := range dns { dnsConfig += "nameserver " + dnsAddr + "\n" } common.WriteFile("/etc/resolv.conf", dnsConfig, true) } ================================================ FILE: cmd/network/main.go ================================================ package network import ( "XProxy/cmd/common" ) type Config struct { RouteTable int TProxyPort int Address string Gateway string Bypass []string Exclude []string } var run = common.RunCommand func Load(dns []string, dev string, ipv4 *Config, ipv6 *Config) { loadDns(dns) // init dns server enableIpForward() loadNetwork(dev, ipv4, ipv6) loadV4TProxy(ipv4, getV4Cidr()) loadV6TProxy(ipv6, getV6Cidr()) } ================================================ FILE: cmd/network/network.go ================================================ package network import ( log "github.com/sirupsen/logrus" "regexp" "time" ) func getV4Cidr() []string { // fetch ipv4 network range var v4Cidr []string _, output := run("ip", "-4", "addr") for _, temp := range regexp.MustCompile(`inet (\S+)`).FindAllStringSubmatch(output, -1) { v4Cidr = append(v4Cidr, temp[1]) } return v4Cidr } func getV6Cidr() []string { // fetch ipv6 network range var v6Cidr []string _, output := run("ip", "-6", "addr") for _, temp := range regexp.MustCompile(`inet6 (\S+)`).FindAllStringSubmatch(output, -1) { v6Cidr = append(v6Cidr, temp[1]) } return v6Cidr } func enableIpForward() { // enable ip forward function log.Info("Enabled IPv4 forward") run("sysctl", "-w", "net.ipv4.ip_forward=1") log.Info("Enabled IPv6 forward") run("sysctl", "-w", "net.ipv6.conf.all.forwarding=1") } func flushNetwork(dev string, isV4 bool, isV6 bool) { // flush ipv4 and ipv6 network log.Info("Flush system IP configure") run("ip", "link", "set", dev, "down") if isV4 { run("ip", "-4", "addr", "flush", "dev", dev) } if isV6 { run("ip", "-6", "addr", "flush", "dev", dev) } run("ip", "link", "set", dev, "up") } func loadV4Network(v4 *Config, dev string) { // setting up ipv4 network log.Info("Setting up system IPv4 configure") if v4.Address != "" { run("ip", "-4", "addr", "add", v4.Address, "dev", dev) } if v4.Gateway != "" { run("ip", "-4", "route", "add", "default", "via", v4.Gateway, "dev", dev) } } func loadV6Network(v6 *Config, dev string) { // setting up ipv6 network log.Info("Setting up system IPv6 configure") if v6.Address != "" { run("ip", "-6", "addr", "add", v6.Address, "dev", dev) } if v6.Gateway != "" { run("ip", "-6", "route", "add", "default", "via", v6.Gateway, "dev", dev) } } func loadNetwork(dev string, v4 *Config, v6 *Config) { setV4 := v4.Address != "" || v4.Gateway != "" setV6 := v6.Address != "" || v6.Gateway != "" if setV4 && setV6 { // load both ipv4 and ipv6 flushNetwork(dev, true, true) loadV4Network(v4, dev) loadV6Network(v6, dev) } else if setV4 { // only load ipv4 network flushNetwork(dev, true, false) loadV4Network(v4, dev) } else if setV6 { // only load ipv6 network flushNetwork(dev, false, true) loadV6Network(v6, dev) } else { // skip network settings log.Infof("Skip system IP configure") } if setV6 { log.Info("Wait 1s for IPv6 setting up") time.Sleep(time.Second) // wait for ipv6 setting up (ND protocol) -> RA should reply less than 0.5s } } ================================================ FILE: cmd/network/tproxy.go ================================================ package network import ( log "github.com/sirupsen/logrus" "strconv" ) func loadV4TProxy(v4 *Config, v4SysCidr []string) { log.Info("Setting up TProxy of IPv4") tableNum := strconv.Itoa(v4.RouteTable) v4Bypass := append(v4SysCidr, v4.Bypass...) run("ip", "-4", "rule", "add", "fwmark", "1", "table", tableNum) run("ip", "-4", "route", "add", "local", "0.0.0.0/0", "dev", "lo", "table", tableNum) run("iptables", "-t", "mangle", "-N", "XPROXY") log.Infof("Setting up IPv4 bypass CIDR -> %v", v4Bypass) for _, bypass := range v4Bypass { run("iptables", "-t", "mangle", "-A", "XPROXY", "-d", bypass, "-j", "RETURN") } for _, exclude := range v4.Exclude { run("iptables", "-t", "mangle", "-A", "XPROXY", "-s", exclude, "-j", "RETURN") } run("iptables", "-t", "mangle", "-A", "XPROXY", "-p", "tcp", "-j", "TPROXY", "--on-port", strconv.Itoa(v4.TProxyPort), "--tproxy-mark", "1") run("iptables", "-t", "mangle", "-A", "XPROXY", "-p", "udp", "-j", "TPROXY", "--on-port", strconv.Itoa(v4.TProxyPort), "--tproxy-mark", "1") run("iptables", "-t", "mangle", "-A", "PREROUTING", "-j", "XPROXY") } func loadV6TProxy(v6 *Config, v6SysCidr []string) { log.Info("Setting up TProxy of IPv6") tableNum := strconv.Itoa(v6.RouteTable) v6Bypass := append(v6SysCidr, v6.Bypass...) run("ip", "-6", "rule", "add", "fwmark", "1", "table", tableNum) run("ip", "-6", "route", "add", "local", "::/0", "dev", "lo", "table", tableNum) run("ip6tables", "-t", "mangle", "-N", "XPROXY6") log.Infof("Setting up IPv6 bypass CIDR -> %v", v6Bypass) for _, bypass := range v6Bypass { run("ip6tables", "-t", "mangle", "-A", "XPROXY6", "-d", bypass, "-j", "RETURN") } for _, exclude := range v6.Exclude { run("ip6tables", "-t", "mangle", "-A", "XPROXY6", "-s", exclude, "-j", "RETURN") } run("ip6tables", "-t", "mangle", "-A", "XPROXY6", "-p", "tcp", "-j", "TPROXY", "--on-port", strconv.Itoa(v6.TProxyPort), "--tproxy-mark", "1") run("ip6tables", "-t", "mangle", "-A", "XPROXY6", "-p", "udp", "-j", "TPROXY", "--on-port", strconv.Itoa(v6.TProxyPort), "--tproxy-mark", "1") run("ip6tables", "-t", "mangle", "-A", "PREROUTING", "-j", "XPROXY6") } ================================================ FILE: cmd/process/daemon.go ================================================ package process import ( log "github.com/sirupsen/logrus" "time" ) func daemonSub(sub *Process) { for sub.process.ProcessState == nil { // until process exit sub.Wait() } log.Warningf("Catch process %s exit", sub.name) time.Sleep(5 * time.Second) // delay 3s -> try to restart if !exitFlag { log.Debugf("Process %s restart -> %v", sub.name, sub.command) sub.Run(true, sub.env) log.Infof("Process %s restart success", sub.name) daemonSub(sub) } } func (p *Process) Daemon() { if p.process == nil { // process not running log.Infof("Process %s disabled -> skip daemon", p.name) return } log.Infof("Daemon of process %s start", p.name) go func() { daemonSub(p) // start daemon process log.Infof("Process %s exit daemon mode", p.name) }() } ================================================ FILE: cmd/process/exit.go ================================================ package process import ( log "github.com/sirupsen/logrus" "syscall" ) var exitFlag bool func Exit(subProcess ...*Process) { exitFlag = true // setting up exit flag -> exit daemon mode log.Warningf("Start exit process") for _, sub := range subProcess { if sub.process != nil { log.Infof("Send kill signal to process %s", sub.name) sub.Signal(syscall.SIGTERM) } } log.Info("Wait all sub process exit") for _, sub := range subProcess { if sub.process != nil { _ = sub.process.Wait() } } log.Infof("Exit complete") } ================================================ FILE: cmd/process/main.go ================================================ package process import ( log "github.com/sirupsen/logrus" "os" "os/exec" "syscall" ) type Process struct { name string env []string command []string process *exec.Cmd } func New(command ...string) *Process { process := new(Process) process.name = command[0] process.command = command log.Debugf("New process %s -> %v", process.name, process.command) return process } func (p *Process) Run(isOutput bool, env []string) { p.process = exec.Command(p.command[0], p.command[1:]...) if isOutput { p.process.Stdout = os.Stdout p.process.Stderr = os.Stderr } p.env = env if len(p.env) != 0 { p.process.Env = p.env log.Infof("Process %s with env -> %v", p.name, p.env) } err := p.process.Start() if err != nil { log.Errorf("Failed to start %s -> %v", p.name, err) } log.Infof("Start process %s -> PID = %d", p.name, p.process.Process.Pid) } func (p *Process) Signal(signal syscall.Signal) { if p.process != nil { log.Debugf("Send signal %v to %s", signal, p.name) _ = p.process.Process.Signal(signal) } } func (p *Process) Wait() { if p.process != nil { err := p.process.Wait() if err != nil { log.Warningf("Wait process %s -> %v", p.name, err) } } } ================================================ FILE: cmd/proxy/config.go ================================================ package proxy import ( "XProxy/cmd/common" log "github.com/sirupsen/logrus" "path" ) var outboundsConfig = `{ "outbounds": [ { "protocol": "freedom", "settings": {} } ] }` type logObject struct { Log struct { Loglevel string `json:"loglevel"` Access string `json:"access"` Error string `json:"error"` } `json:"log"` } type inboundsObject struct { Inbounds []interface{} `json:"inbounds"` } type sniffObject struct { Enabled bool `json:"enabled"` RouteOnly bool `json:"routeOnly"` DestOverride []string `json:"destOverride"` DomainsExcluded []string `json:"domainsExcluded"` } type inboundObject struct { Tag string `json:"tag"` Port int `json:"port"` Protocol string `json:"protocol"` Settings interface{} `json:"settings"` StreamSettings interface{} `json:"streamSettings"` Sniffing sniffObject `json:"sniffing"` } func loadLogConfig(logLevel string, logDir string) string { if logLevel == "" { logLevel = "warning" // using warning level without log output } if logLevel != "debug" && logLevel != "info" && logLevel != "warning" && logLevel != "error" && logLevel != "none" { log.Warningf("Unknown log level -> %s", logLevel) logLevel = "warning" // using `warning` as default } logConfig := logObject{} logConfig.Log.Loglevel = logLevel logConfig.Log.Access = path.Join(logDir, "access.log") logConfig.Log.Error = path.Join(logDir, "error.log") return common.JsonEncode(logConfig) } func loadHttpConfig(tag string, port int, sniff sniffObject) interface{} { type empty struct{} return inboundObject{ Tag: tag, Port: port, Protocol: "http", Settings: empty{}, StreamSettings: empty{}, Sniffing: sniff, } } func loadSocksConfig(tag string, port int, sniff sniffObject) interface{} { type empty struct{} type socksObject struct { UDP bool `json:"udp"` } return inboundObject{ Tag: tag, Port: port, Protocol: "socks", Settings: socksObject{UDP: true}, StreamSettings: empty{}, Sniffing: sniff, } } func loadTProxyConfig(tag string, port int, sniff sniffObject) interface{} { type tproxyObject struct { Network string `json:"network"` FollowRedirect bool `json:"followRedirect"` } type tproxyStreamObject struct { Sockopt struct { Tproxy string `json:"tproxy"` } `json:"sockopt"` } tproxyStream := tproxyStreamObject{} tproxyStream.Sockopt.Tproxy = "tproxy" return inboundObject{ Tag: tag, Port: port, Protocol: "dokodemo-door", Settings: tproxyObject{ Network: "tcp,udp", FollowRedirect: true, }, StreamSettings: tproxyStream, Sniffing: sniff, } } ================================================ FILE: cmd/proxy/main.go ================================================ package proxy import ( "XProxy/cmd/common" log "github.com/sirupsen/logrus" "path" ) type Config struct { Bin string `yaml:"bin" json:"bin" toml:"bin"` Log string `yaml:"log" json:"log" toml:"log"` Http map[string]int `yaml:"http" json:"http" toml:"http"` Socks map[string]int `yaml:"socks" json:"socks" toml:"socks"` AddOn []interface{} `yaml:"addon" json:"addon" toml:"addon"` Sniff struct { Enable bool `yaml:"enable" json:"enable" toml:"enable"` Redirect bool `yaml:"redirect" json:"redirect" toml:"redirect"` Exclude []string `yaml:"exclude" json:"exclude" toml:"exclude"` } `yaml:"sniff" json:"sniff" toml:"sniff"` V4TProxyPort int V6TProxyPort int } func saveConfig(configDir string, caption string, content string, overwrite bool) { filePath := path.Join(configDir, caption+".json") common.WriteFile(filePath, content+"\n", overwrite) } func loadInbounds(config *Config) string { sniff := sniffObject{ Enabled: config.Sniff.Enable, RouteOnly: !config.Sniff.Redirect, DestOverride: []string{"http", "tls", "quic"}, DomainsExcluded: config.Sniff.Exclude, } var inbounds []interface{} inbounds = append(inbounds, loadTProxyConfig("tproxy4", config.V4TProxyPort, sniff)) inbounds = append(inbounds, loadTProxyConfig("tproxy6", config.V6TProxyPort, sniff)) for tag, port := range config.Http { inbounds = append(inbounds, loadHttpConfig(tag, port, sniff)) } for tag, port := range config.Socks { inbounds = append(inbounds, loadSocksConfig(tag, port, sniff)) } for _, addon := range config.AddOn { inbounds = append(inbounds, addon) } return common.JsonEncode(inboundsObject{ Inbounds: inbounds, }) } func Load(configDir string, exposeDir string, config *Config) { common.CreateFolder(path.Join(exposeDir, "log")) common.CreateFolder(path.Join(exposeDir, "config")) common.CreateFolder(configDir) saveConfig(path.Join(exposeDir, "config"), "outbounds", outboundsConfig, false) saveConfig(configDir, "inbounds", loadInbounds(config), true) saveConfig(configDir, "log", loadLogConfig(config.Log, path.Join(exposeDir, "log")), true) for _, configFile := range common.ListFiles(path.Join(exposeDir, "config"), ".json") { if configFile == "log.json" || configFile == "inbounds" { log.Warningf("Config file %s will be overridden", configFile) } common.CopyFile(path.Join(exposeDir, "config", configFile), path.Join(configDir, configFile)) } } ================================================ FILE: cmd/radvd/radvd.go ================================================ package radvd import ( "XProxy/cmd/common" log "github.com/sirupsen/logrus" "strings" ) type Config struct { Log int `yaml:"log" json:"log" toml:"log"` Dev string `yaml:"dev" json:"dev" toml:"dev"` Enable bool `yaml:"enable" json:"enable" toml:"enable"` Client []string `yaml:"client" json:"client" toml:"client"` Option map[string]string `yaml:"option" json:"option" toml:"option"` Route struct { Cidr string `yaml:"cidr" json:"cidr" toml:"cidr"` Option map[string]string `yaml:"option" json:"option" toml:"option"` } `yaml:"route" json:"route" toml:"route"` Prefix struct { Cidr string `yaml:"cidr" json:"cidr" toml:"cidr"` Option map[string]string `yaml:"option" json:"option" toml:"option"` } `yaml:"prefix" json:"prefix" toml:"prefix"` DNSSL struct { // DNS Search List Suffix []string `yaml:"suffix" json:"suffix" toml:"suffix"` Option map[string]string `yaml:"option" json:"option" toml:"option"` } `yaml:"dnssl" json:"dnssl" toml:"dnssl"` RDNSS struct { // Recursive DNS Server IP []string `yaml:"ip" json:"ip" toml:"ip"` Option map[string]string `yaml:"option" json:"option" toml:"option"` } `yaml:"rdnss" json:"rdnss" toml:"rdnss"` } func genSpace(num int) string { return strings.Repeat(" ", num) } func loadOption(options map[string]string, intend int) string { // load options into radvd config format var ret string for option, value := range options { ret += genSpace(intend) + option + " " + value + ";\n" } return ret } func loadClient(clients []string) string { // load radvd client configure if len(clients) == 0 { return "" // without client settings } ret := genSpace(4) + "clients {\n" for _, client := range clients { ret += genSpace(8) + client + ";\n" } return ret + genSpace(4) + "};\n" } func loadPrefix(prefix string, option map[string]string) string { // load radvd prefix configure if prefix == "" { return "" // without prefix settings } header := genSpace(4) + "prefix " + prefix + " {\n" return header + loadOption(option, 8) + genSpace(4) + "};\n" } func loadRoute(cidr string, option map[string]string) string { // load radvd route configure if cidr == "" { return "" // without route settings } header := genSpace(4) + "route " + cidr + " {\n" return header + loadOption(option, 8) + genSpace(4) + "};\n" } func loadRdnss(ip []string, option map[string]string) string { // load radvd RDNSS configure if len(ip) == 0 { return "" // without rdnss settings } header := genSpace(4) + "RDNSS " + strings.Join(ip, " ") + " {\n" return header + loadOption(option, 8) + genSpace(4) + "};\n" } func loadDnssl(suffix []string, option map[string]string) string { // load radvd DNSSL configure if len(suffix) == 0 { return "" // without dnssl settings } header := genSpace(4) + "DNSSL " + strings.Join(suffix, " ") + " {\n" return header + loadOption(option, 8) + genSpace(4) + "};\n" } func Load(Radvd *Config) { config := "interface " + Radvd.Dev + " {\n" config += loadOption(Radvd.Option, 4) config += loadPrefix(Radvd.Prefix.Cidr, Radvd.Prefix.Option) config += loadRoute(Radvd.Route.Cidr, Radvd.Route.Option) config += loadClient(Radvd.Client) config += loadRdnss(Radvd.RDNSS.IP, Radvd.RDNSS.Option) config += loadDnssl(Radvd.DNSSL.Suffix, Radvd.DNSSL.Option) config += "};\n" log.Debugf("Radvd configure -> \n%s", config) common.WriteFile("/etc/radvd.conf", config, true) } ================================================ FILE: cmd/xproxy.go ================================================ package main import ( "XProxy/cmd/common" "XProxy/cmd/config" "XProxy/cmd/custom" "XProxy/cmd/process" "flag" "fmt" log "github.com/sirupsen/logrus" "io" "os" "path" "runtime" "strconv" ) var version = "dev" var v4RouteTable = 104 var v6RouteTable = 106 var v4TProxyPort = 7288 var v6TProxyPort = 7289 var proxyBin = "" var configDir = "/etc/xproxy" var assetFile = "/assets.tar.xz" var subProcess []*process.Process var assetDir, exposeDir, configFile string func logInit(isDebug bool, logDir string) { log.SetFormatter(&log.TextFormatter{ ForceColors: true, FullTimestamp: true, TimestampFormat: "2006-01-02 15:04:05", }) log.SetLevel(log.InfoLevel) // default log level if isDebug { log.SetLevel(log.DebugLevel) } common.CreateFolder(logDir) // confirm log folder exist logFile, err := os.OpenFile(path.Join(logDir, "xproxy.log"), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) if err != nil { log.Errorf("Unable to open log file -> %s", path.Join(logDir, "xproxy.log")) } log.SetOutput(io.MultiWriter(os.Stderr, logFile)) } func xproxyInit() { xproxyConfig := "xproxy.yml" if os.Getenv("CONFIG") != "" { xproxyConfig = os.Getenv("CONFIG") } isVersion := flag.Bool("version", false, "Show version") isDebug := flag.Bool("debug", os.Getenv("DEBUG") == "true", "Enable debug mode") configName := flag.String("config", xproxyConfig, "Config file name") flag.Parse() if *isVersion { // show version info and exit fmt.Printf("XProxy version %s (%s %s/%s)\n", version, runtime.Version(), runtime.GOOS, runtime.GOARCH) os.Exit(0) } exposeDir = "/xproxy" // default folder if os.Getenv("EXPOSE_DIR") != "" { exposeDir = os.Getenv("EXPOSE_DIR") } logInit(*isDebug, path.Join(exposeDir, "log")) common.CreateFolder(exposeDir) assetDir = path.Join(exposeDir, "assets") configFile = path.Join(exposeDir, *configName) log.Debugf("Expose folder -> %s", exposeDir) log.Debugf("Assets folder -> %s", assetDir) log.Debugf("Config file -> %s", configFile) if os.Getenv("PROXY_BIN") != "" { proxyBin = os.Getenv("PROXY_BIN") } if os.Getenv("IPV4_TABLE") != "" { v4RouteTable, _ = strconv.Atoi(os.Getenv("IPV4_TABLE")) } if os.Getenv("IPV6_TABLE") != "" { v6RouteTable, _ = strconv.Atoi(os.Getenv("IPV6_TABLE")) } if os.Getenv("IPV4_TPROXY") != "" { v4TProxyPort, _ = strconv.Atoi(os.Getenv("IPV4_TPROXY")) } if os.Getenv("IPV6_TPROXY") != "" { v6TProxyPort, _ = strconv.Atoi(os.Getenv("IPV6_TPROXY")) } log.Debugf("IPv4 Route Table -> %d", v4RouteTable) log.Debugf("IPv6 Route Table -> %d", v6RouteTable) log.Debugf("IPv4 TProxy Port -> %d", v4TProxyPort) log.Debugf("IPv6 TProxy Port -> %d", v6TProxyPort) } func main() { defer func() { if err := recover(); err != nil { log.Errorf("Panic exit -> %v", err) } }() xproxyInit() var settings config.Config log.Infof("XProxy %s start (%s %s/%s)", version, runtime.Version(), runtime.GOOS, runtime.GOARCH) config.Load(configFile, &settings) loadNetwork(&settings) loadProxy(&settings) loadAsset(&settings) loadRadvd(&settings) loadDhcp(&settings) custom.RunPreScript(&settings.Custom) runDhcp(&settings) runRadvd(&settings) runProxy(&settings) blockWait() process.Exit(subProcess...) custom.RunPostScript(&settings.Custom) log.Warningf("All done, goodbye!") } ================================================ FILE: docs/campus_network_cracking.md ================================================ # 使用 XProxy 绕过校园网认证登录 部分校园网在登录认证时需要 DNS 解析,因而在防火墙上允许 `TCP/53` 或 `UDP/53` 端口通行,借助这个漏洞,可将内网流量用 XProxy 代理并转发到公网服务器上,实现免认证、无限速的上网。 以下为一般情况下的网络拓扑: ![Network](./img/campus_network.png) 为了方便讲解,我们假设以下典型情况: + 校园网交换机无 IPv6 支持,同时存在 QoS; + 无认证时允许 53 端口通行,ICMP 流量无法通过; + 使用三台公网服务器负载均衡,其 53 端口上运行有代理服务; + 三台服务器只有一台支持 IPv4 与 IPv6 双栈,其余只支持 IPv4; ## 代理协议 从部署成本与便捷性方面考虑,socks 类代理是最合适的工具:无需修改服务器网卡路由表等配置,方便多级负载均衡,软件只在用户态运行,实测速度也相对 `IPSec` 、`L2TP` 等协议更有优势;但 socks 代理只接收 TCP 与 UDP 流量,ICMP 流量无法被直接代理(例如 PING 命令),不过大多数情况下我们不会用到公网 ICMP 流量,如果确实需要也可以曲线救国给它补上。 在选定代理类型后,我们需要考虑具体的传输方式,由于存在 QoS 问题,这里应该倾向于选择基于 TCP 的代理方式,同时为了避免校园网的流量审查,我们应该将流量加密传输。考虑到软路由性能一般较差,而自建的代理服务器无需考虑协议兼容性问题,这里更建议选择基于 XTLS 的传输方式,它避开了对 TLS 流量的二次加密,可以显著降低代理 https 流量时的性能开销,提升性能上限;至于延迟方面的问题,如果选择 `gRPC` 等协议,虽然有 0-rtt 握手的延迟优势,但这种场景下延迟一般不高(甚至服务器可以直接部署在校内),用微弱的延迟优势换取性能开销并不值得,且前者也可以开启 mux 多路复用来优化延迟。 既然我们已经选择 XTLS 方式,那使用轻量的无加密类型(在加密的 XTLS 隧道里传输)是当前网络的最优解,譬如 VLESS 或者 Trojan 协议,下面将用 VLESS + XTLS 代理进行配置演示;当然,具体的选择还是取决于您的实际应用场景,只要按需调整 XProxy 的配置文件即可。 ## 初始化配置 > 分配 `192.168.2.0/24` 和 `fc00::/64` 给内网使用。 路由器 WAN 口接入学校交换机,构建一个 NAT 转换,代理流量在路由器转发后送到公网服务器的 53 端口上;假设内网中路由器地址为 `192.168.2.1` ,配置虚拟网关 IPv4 地址为 `192.168.2.2` ,IPv6 地址为 `fc00::2` ;在网关中,无论 IPv4 还是 IPv6 流量都会被透明代理,由于校园网无 IPv6 支持,数据被封装后只通过 IPv4 网络发送,代理服务器接收以后再将其解开,对于 IPv6 流量,这里相当于一个 `6to4` 隧道。 ```bash # 宿主机网卡假定为 eth0 $ ip link set eth0 promisc on $ modprobe ip6table_filter $ docker network create -d macvlan \ --subnet=192.168.2.0/24 \ # 此处指定的参数为容器的默认网络配置 --gateway=192.168.2.1 \ --subnet=fc00::/64 \ --gateway=fc00::1 \ --ipv6 -o parent=eth0 macvlan ``` 我们将配置文件保存在 `/etc/scutweb` 目录下,使用以下命令开启 XProxy 服务: ```bash docker run --restart always \ --privileged --network macvlan -dt \ --name scutweb --hostname scutweb \ --volume /etc/scutweb/:/xproxy/ \ --volume /etc/timezone:/etc/timezone:ro \ --volume /etc/localtime:/etc/localtime:ro \ dnomd343/xproxy:latest ``` ## 参数配置 我们将三台服务器分别称为 `nodeA` ,`nodeB` 与 `nodeC` ,其中只有 `nodeC` 支持IPv6网络;此外,我们在内网分别暴露 3 个 socks5 端口,分别用于检测服务器的可用性。 由于校园网无 IPv6 支持,这里 IPv6 上游网关可以不填写;虚拟网关对内网发布 RA 通告,让内网设备使用 SLAAC 配置网络地址,同时将其作为 IPv6 网关;此外,如果路由器开启了 DHCP 服务,需要将默认网关改为 `192.168.2.2` ,也可以启用 XProxy 自带的 DHCPv4 服务。 最后,由于我们代理全部流量,无需根据域名或者 IP 进行任何分流,因此路由资源自动更新部分可以省略。 修改 `xproxy.yml` ,写入以下配置: ```yaml proxy: log: warning socks: nodeA: 1081 nodeB: 1082 nodeC: 1083 network: dev: eth0 dns: - 192.168.2.1 ipv4: gateway: 192.168.2.1 address: 192.168.2.2/24 ipv6: gateway: null address: fc00::2/64 bypass: - 169.254.0.0/16 - 224.0.0.0/3 - fc00::/7 - fe80::/10 - ff00::/8 radvd: log: 5 dev: eth0 enable: true option: AdvSendAdvert: on prefix: cidr: fc00::/64 custom: pre: - "iptables -t nat -N FAKE_PING" - "iptables -t nat -A FAKE_PING -j DNAT --to-destination 192.168.2.2" - "iptables -t nat -A PREROUTING -i eth0 -p icmp -j FAKE_PING" - "ip6tables -t nat -N FAKE_PING" - "ip6tables -t nat -A FAKE_PING -j DNAT --to-destination fc00::2" - "ip6tables -t nat -A PREROUTING -i eth0 -p icmp -j FAKE_PING" ``` 在开始代理前,我们使用 `custom` 注入了一段脚本配置:由于这里我们只代理 TCP 与 UDP 流量,ICMP 数据包不走代理,内网设备 ping 外网时会一直无响应,加入这段脚本可以创建一个 NAT,假冒远程主机返回成功回复,但实际上 ICMP 数据包并未实际到达,效果上表现为 ping 成功且延迟为内网访问时间。 > 这段脚本并无实质作用,仅用于演示 `custom` 功能。 ## 代理配置 接下来,我们应该配置出站代理,修改 `config/outbounds.json` 文件,填入公网代理服务器参数: ```json { "outbounds": [ { "tag": "nodeA", "...": "..." }, { "tag": "nodeB", "...": "..." }, { "tag": "nodeC", "...": "..." } ] } ``` 接着配置路由部分,让暴露的三个 socks5 接口对接到三台服务器上,并分别配置 IPv4 与 IPv6 的负载均衡;路由核心在这里接管所有流量,IPv4 流量应将随机转发到三台服务器,而 IPv6 流量只送往 `nodeC` 服务器;创建 `config/routing.json` 文件,写入以下配置: ```json { "routing": { "domainStrategy": "AsIs", "rules": [ { "type": "field", "inboundTag": ["nodeA"], "outboundTag": "nodeA" }, { "type": "field", "inboundTag": ["nodeB"], "outboundTag": "nodeB" }, { "type": "field", "inboundTag": ["nodeC"], "outboundTag": "nodeC" }, { "type": "field", "ip": ["0.0.0.0/0"], "balancerTag": "ipv4" }, { "type": "field", "ip": ["::/0"], "balancerTag": "ipv6" } ], "balancers": [ { "tag": "ipv4", "selector": [ "nodeA", "nodeB", "nodeC" ] }, { "tag": "ipv6", "selector": [ "nodeC" ] } ] } } ``` 重启 XProxy 容器使配置生效: ```bash docker restart scutweb ``` 最后,验证代理服务是否正常工作,若出现问题可以查看 `/etc/scutweb/log` 文件夹下的日志,定位错误原因。 ## 代理 ICMP 流量 > 这一步仅用于修复 ICMP 代理,无此需求可以忽略。 由于 socks5 代理服务不支持 ICMP 协议,当前搭建的网络只有 TCP 与 UDP 发往外网,即使在上文我们注入了一段命令用于劫持 PING 流量,但是返回的仅仅是虚假结果,并没有实际意义;所以如果对这个缺陷不满,您可以考虑使用以下方法修复这个问题。 为了代理 ICMP 流量,我们必须选择网络层的 VPN 工具,从简单轻量可用方面考虑,`WireGuard` 比较适合当前应用场景:TCP 与 UDP 流量走 VLESS + XTLS 代理,ICMP 流量进入 WireGuard ,而 WireGuard 本身使用 UDP 协议传输,这些数据包通过 Xray 隧道再次代理送到远端服务器,解开后将 ICMP 流量送至公网;这种方式虽然略显繁杂,但实际场景中 ICMP 流量很少且数据包不大,并不存在性能问题。 具体实现上,我们需要在容器中安装 WireGuard 工具包,然后在 XProxy 中配置启动注入脚本,开启 WireGuard 对 ICMP 流量的代理。 ### 1. 拉取 WireGuard 安装包 XProxy 容器默认不自带 WireGuard 功能,需要额外安装 `wireguard-tools` 包,您可以在原有镜像上添加一层,或是使用以下方式安装离线包。 > 以下代码用于生成 Alpine 的 WireGuard 安装脚本,您也可以选择手动拉取 apk 安装包 ```python #!/usr/bin/env python3 # -*- coding: utf-8 -*- Alpine = '3.17' import os, re, sys pkgName = sys.argv[1] workDir = os.path.join(os.path.dirname(os.path.realpath(__file__)), pkgName) rawOutput = os.popen(' '.join([ 'docker run --rm -w /tmp -v %s:/tmp' % workDir, 'alpine:%s' % Alpine, 'sh -c "apk update && apk fetch -R %s"' % pkgName ])).read() print("%s\n%s%s" % ('=' * 88, rawOutput, '=' * 88), file = sys.stderr) with open(os.path.join(workDir, 'setup'), 'w') as script: setupCmd = '#!/usr/bin/env sh\ncd "$(dirname "$0")" && apk add --no-network --quiet ' setupCmd += ' '.join([ '%s.apk' % x for x in re.findall(r'Downloading (\S+)', rawOutput) ]) script.write(setupCmd) os.system('chmod +x %s' % os.path.join(workDir, 'setup')) ``` ```bash # fetch.py 为上述脚本 $ cd /etc/scutweb $ mkdir -p ./toolset && cd ./toolset $ python3 fetch.py wireguard-tools # 拉取wireguard-tools依赖 ··· ``` 拉取成功后将生成 `wireguard-tools` 文件夹,包含多个依赖的 `.apk` 安装包与 `setup` 安装脚本。 ### 2. 写入 WireGuard 配置文件 一个典型的客户端配置文件如下: ```ini [Interface] PrivateKey = 客户端私钥 [Peer] PublicKey = 服务端公钥 Endpoint = 服务器IP:端口 AllowedIPs = 0.0.0.0/0 ``` 将其保存至 `/etc/scutweb/config/wg.conf` ### 3. 容器注入 WireGuard 服务 WireGuard 在这里使用 `192.168.1.0/24` 的 VPN 网段,客户端 IP 地址为 `192.168.1.2`,注意服务端应允许 `192.168.2.2/24` 网段,否则必须在容器中多做一层 NAT 才能代理。 此外,XProxy 默认没有加入网关自身的代理,只在 `PREROUTING` 链上劫持流量,因此这里需要修改 `OUTPUT` 链,让 WireGuard 的流量被 XProxy 代理;将发往 WireGurad 服务器的流量打上标志 `0x1`,该数据包就会被重新路由到 `PREROUTING` 链上(netfilter 特性),从而进行透明代理。 ```yaml custom: pre: - /xproxy/toolset/wireguard-tools/setup # 安装离线包 - ip link add wg0 type wireguard - wg setconf wg0 /xproxy/config/wg.conf # 加载配置文件 - ip addr add 192.168.1.2/24 dev wg0 # 添加本机WireGuard地址 - ip link set mtu 1420 up dev wg0 # 启动VPN服务 - ip rule add fwmark 51820 table 51820 - ip route add 0.0.0.0/0 dev wg0 table 51820 # WireGuard路由表 - iptables -t mangle -N WGPROXY - iptables -t mangle -A WGPROXY -d 127.0.0.0/8 -j RETURN - iptables -t mangle -A WGPROXY -d 192.168.2.0/24 -j RETURN - iptables -t mangle -A WGPROXY -d 169.254.0.0/16 -j RETURN - iptables -t mangle -A WGPROXY -d 224.0.0.0/3 -j RETURN - iptables -t mangle -A WGPROXY -p icmp -j MARK --set-mark 51820 # ICMP流量送至WireGuard路由表 - iptables -t mangle -A PREROUTING -j WGPROXY - iptables -t mangle -A OUTPUT -p udp -d 服务器IP –-dport 服务器端口 -j MARK --set-mark 1 # 重定向到PREROUTING ``` 配置完成后,重启 XProxy 容器生效,在内网设备上执行 PING 命令,如果返回正常延迟则配置成功。 ================================================ FILE: docs/dual_stack_network_proxy.md ================================================ # 家庭网络的 IPv4 与 IPv6 透明代理 家庭网络光纤入网,支持 IPv4 与 IPv6 网络,需要在内网搭建透明代理,让设备的国内流量直连,出境流量转发到代理服务器上,避开 GFW 的流量审查。 以下为典型网络拓扑: ![Network](./img/chinese_network.png) > 此处网络拓扑仅为讲解使用,实际使用时可以让光猫桥接减少性能浪费,不过目前大部分新版光猫不存在性能瓶颈,千兆级别下基本没有压力。 正常情况下,大部分家庭宽带为:光猫对接上游网络,使用宽带拨号获取运营商分配的 IPv4 地址与 IPv6 前缀,在 LAN 侧提供网络服务,其中 IPv4 为 NAT 方式,IPv6 发布 RA 广播,同时运行 DHCPv6 服务;路由器在 IPv4 上 NAT ,在 IPv6 上桥接,内网设备统一接入路由器。 大多数地区的运营商不会提供 IPv4 公网地址,IPv6 分配一般为 64 位长度的公网网段;虚拟网关在这里需要收集内网的所有 IPv4 与 IPv6 流量,将国内流量直接送出,国外流量发往代理服务器;为了增加难度,我们假设有两台境外代理服务器,一台支持IPv6,一台只支持IPv4,我们需要将IPv6代理流量发送给前者,其余代理流量送往后者。 ## 分流规则 代理内核需要区分出哪些流量可以直连,哪些流量需要送往代理服务器,为了更准确地分流,这里需要开启嗅探功能,获取访问的域名信息,同时允许流量重定向(目标地址修改为域名,送至代理服务器解析,避开 DNS 污染)。 目前路由资源中包含了一份国内常见域名列表(即 `geosite.dat` ,XProxy 已集成),如果嗅探后的域名在其中,那可以直接判定为直连流量,但是对于其他流量,即使它不在列表内,但仍可能是国内服务,我们不能直接将它送往代理服务器;因此下一步我们需要引出分流的核心规则,它取决于 DNS 污染的一个特性:受污染的域名返回解析必然为境外 IP ,基于这个原则,我们将嗅探到的域名使用国内 DNS 进行一次解析,如果结果是国内 IP 地址,那就直连该流量,否则发往代理服务器,IPv4 与 IPv6 均使用该逻辑分流。 如果有可能的话,您可以在内网搭建一个无污染的解析服务,比如 [ClearDNS](https://github.com/dnomd343/ClearDNS),它的作用在于消除 DNS 污染,准确地给出国内外的解析地址,这样子可以在分流时就不用多做一次 DNS 解析,减少这一步导致的延迟(DNS 流量通过代理送出,远程解析以后再返回,其耗时较长且不稳定),无污染 DNS 可以更快更准确地进行分流。 ## 网络配置 网络地址方面,内网 IPv4 段由我们自己决定,这一部分取决于路由器设置的 LAN 侧 IP 段,我们假设为 `192.168.2.0/24` ,其中路由器地址为 `192.168.2.1` ,虚拟网关分配为 `192.168.2.2` ,由于 IPv4 部分由路由器 NAT 隔离,这里不需要修改光猫配置;虚拟网关上游配置为路由器地址,修改内网 DHCP 服务,让网关指向 `192.168.2.2` 。 IPv6部分,由于路由器桥接,地址分配等操作均为光猫负责,它拥有一个链路本地地址,在 LAN 侧向内网发送 RA 广播,一些光猫还会开启 DHCPv6 服务,为内网分配 DNS 等选项;RA 通告发布的 IPv6 前缀一般为运营商分配的 64 位长度地址,内网所有设备将获取到一个独立的 IPv6 地址(部分地区也有做 NAT6 的,具体取决于运营商),我们要做的就是将这部分工作转移给虚拟网关来完成。 在开始之前,我们需要先拿到光猫分配的 IPv6 前缀与网关(即光猫的链路地址),由于光猫默认会发布 RA 广播,你可以直接从当前接入设备上获取这些信息,也可以登录光猫管理页面查看(登录账号与密码一般会印在光猫背面);这里假设运营商分配的 IPv6 网段为 `2409:8a55:e2a7:3a0::/64` ,光猫地址为 `fe80::1`(绝大多数光猫都使用这个链路地址),虚拟网关的上游应该配置为光猫链路地址,而自身地址可以在分配的 IPv6 网段中任意选择,方便起见,我们这里配置为 `2409:8a55:e2a7:3a0::` 。 虚拟网关需要对内网发布 RA 通告,广播 `2409:8a55:e2a7:3a0::/64` 这段地址,接收到这段信息的设备会将虚拟网关作为公网 IPv6 的下一跳地址(即网关链路地址);但是这种情况下,不应该存在多个 RA 广播源同时运行,所以需要关闭光猫的 RA 广播功能,如果不需要 DHCPv6 功能,这里也可以一并关闭;这一步在部分光猫上需要超级管理员权限,一般情况下,你可以在网络上搜索到不同型号光猫的默认超级管理员账号密码,如果无法成功,可以联系宽带师傅帮忙登入。 三大运营商的光猫,超级管理员默认账号密码: + 移动 :`CMCCAdmin` ,`aDm8H%MdA` + 电信 :`telecomadmin` ,`nE7jA%5m` + 联通 :`CUAdmin` ,`CUAdmin` 这是 IPv6 在代理方面的缺点,它将发送 RA 广播的链路地址直接视为路由网关,且该地址无法通过其他协议更改,我们没法像 DHCPv4 一样直接配置网关地址,这在透明代理时远没有 IPv4 方便,只能将 RA 广播源放在网关上。 ## 启动服务 首先创建 macvlan 网络: ```bash # 宿主机网卡假定为 eth0 $ ip link set eth0 promisc on $ modprobe ip6table_filter # IPv6网段后续由XProxy更改,这里可以随意指定 $ docker network create -d macvlan --subnet=fe80::/10 --ipv6 -o parent=eth0 macvlan ``` 将配置文件保存在 `/etc/route` 目录下,使用以下命令开启 XProxy 服务: ```bash docker run --restart always \ --privileged --network macvlan -dt \ --name route --hostname route \ --volume /etc/route/:/xproxy/ \ --volume /etc/timezone:/etc/timezone:ro \ --volume /etc/localtime:/etc/localtime:ro \ dnomd343/xproxy:latest ``` ## 参数配置 在设计上,应该配置四个出口,分别为 IPv4 直连、IPv4 代理、IPv6 直连、IPv6 代理,这里创建 4 个对应的 socks5 接口 `direct4` 、`proxy4` 、`direct6` 、`proxy6` ,用于检测对应出口是否正常工作。 此外,我们需要判断 IP 与域名的地理信息,而该数据库一直变动,需要持续更新;由于该项目的 Github Action 配置为 UTC 22:00 触发,即 UTC8+ 的 06:00 ,所以这里配置为每天早上 06 点 05 分更新,延迟 5 分钟拉取当日的新版本路由资源。 修改 `xproxy.yml` ,写入以下配置: ```yaml proxy: log: info socks: proxy4: 1094 direct4: 1084 proxy6: 1096 direct6: 1086 sniff: enable: true redirect: true network: dev: eth0 dns: - 192.168.2.1 ipv4: gateway: 192.168.2.1 address: 192.168.2.2/24 ipv6: gateway: fe80::1 address: 2409:8a55:e2a7:3a0::/64 bypass: - 169.254.0.0/16 - 224.0.0.0/3 - fc00::/7 - fe80::/10 - ff00::/8 radvd: log: 3 dev: eth0 enable: true option: AdvSendAdvert: on AdvManagedFlag: off AdvOtherConfigFlag: off prefix: cidr: 2409:8a55:e2a7:3a0::/64 asset: update: cron: "0 5 6 * * *" proxy: "socks5://192.168.2.2:1094" # 通过代理下载 Github 文件 url: geoip.dat: "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat" geosite.dat: "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat" ``` ## 代理配置 配置出站代理,修改 `config/outbounds.json` 文件,其中 direct 直连到国内网络,proxy 填入代理服务器参数: ```json { "outbounds": [ { "tag": "direct4", "protocol": "freedom", "settings": { "domainStrategy": "UseIPv4" } }, { "tag": "direct6", "protocol": "freedom", "settings": { "domainStrategy": "UseIPv6" } }, { "tag": "proxy4", "...": "..." }, { "tag": "proxy6", "...": "..." } ] } ``` 接着配置路由部分,让暴露的 4 个 socks5 接口对接上,并依据上文的分流方式编写路由规则;创建 `config/routing.json` 文件,写入以下配置: ```json { "routing": { "domainStrategy": "IPOnDemand", "rules": [ { "type": "field", "inboundTag": ["direct4"], "outboundTag": "direct4" }, { "type": "field", "inboundTag": ["direct6"], "outboundTag": "direct6" }, { "type": "field", "inboundTag": ["proxy4"], "outboundTag": "proxy4" }, { "type": "field", "inboundTag": ["proxy6"], "outboundTag": "proxy6" }, { "type": "field", "inboundTag": ["tproxy4"], "domain": ["geosite:cn"], "outboundTag": "direct4" }, { "type": "field", "inboundTag": ["tproxy6"], "domain": ["geosite:cn"], "outboundTag": "direct6" }, { "type": "field", "inboundTag": ["tproxy4"], "ip": [ "geoip:cn", "geoip:private" ], "outboundTag": "direct4" }, { "type": "field", "inboundTag": ["tproxy6"], "ip": [ "geoip:cn", "geoip:private" ], "outboundTag": "direct6" }, { "type": "field", "inboundTag": ["tproxy4"], "outboundTag": "proxy4" }, { "type": "field", "inboundTag": ["tproxy6"], "outboundTag": "proxy6" } ] } } ``` 重启 XProxy 容器使配置生效: ```bash docker restart route ``` 最后,验证代理服务是否正常工作,若出现问题可以查看 `/etc/route/log` 文件夹下的日志,定位错误原因。 ================================================ FILE: go.mod ================================================ module XProxy go 1.24.5 require ( github.com/BurntSushi/toml v1.5.0 github.com/andybalholm/brotli v1.2.0 github.com/go-http-utils/headers v0.0.0-20181008091004-fed159eddc2a github.com/klauspost/compress v1.18.0 github.com/robfig/cron v1.2.0 github.com/sirupsen/logrus v1.9.3 gopkg.in/yaml.v3 v3.0.1 ) require golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect ================================================ FILE: go.sum ================================================ github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-http-utils/headers v0.0.0-20181008091004-fed159eddc2a h1:v6zMvHuY9yue4+QkG/HQ/W67wvtQmWJ4SDo9aK/GIno= github.com/go-http-utils/headers v0.0.0-20181008091004-fed159eddc2a/go.mod h1:I79BieaU4fxrw4LMXby6q5OS9XnoR9UIKLOzDFjUmuw= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ= github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=