├── .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 └── img │ ├── campus_network.png │ └── chinese_network.png ├── go.mod └── go.sum /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea/ 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG ALPINE="alpine:3.20" 2 | ARG GOLANG="golang:1.24-alpine3.20" 3 | 4 | FROM ${GOLANG} AS xray 5 | ENV XRAY="25.3.6" 6 | RUN wget https://github.com/XTLS/Xray-core/archive/v${XRAY}.tar.gz -O- | tar xz 7 | WORKDIR ./Xray-core-${XRAY}/main/ 8 | RUN go get 9 | RUN env CGO_ENABLED=0 go build -v -trimpath -ldflags "-s -w" 10 | RUN mv main /xray 11 | 12 | FROM ${GOLANG} AS xproxy 13 | RUN apk add git 14 | COPY ./ /XProxy/ 15 | WORKDIR /XProxy/cmd/ 16 | RUN go get 17 | RUN env CGO_ENABLED=0 go build -v -trimpath -ldflags "-X main.version=$(git describe --tag) -s -w" 18 | RUN mv cmd /xproxy 19 | 20 | FROM ${ALPINE} AS assets 21 | RUN apk add xz 22 | RUN wget "https://cdn.dnomd343.top/v2ray-rules-dat/geoip.dat" 23 | RUN wget "https://cdn.dnomd343.top/v2ray-rules-dat/geosite.dat" 24 | RUN tar cJf /assets.tar.xz geoip.dat geosite.dat 25 | 26 | FROM ${ALPINE} AS release 27 | RUN apk add upx 28 | WORKDIR /release/run/radvd/ 29 | WORKDIR /release/var/lib/dhcp/ 30 | RUN touch dhcpd.leases dhcpd6.leases 31 | COPY --from=xproxy /xproxy /release/usr/bin/ 32 | COPY --from=assets /assets.tar.xz /release/ 33 | COPY --from=xray /xray /release/usr/bin/ 34 | WORKDIR /release/usr/bin/ 35 | RUN ls | xargs -n1 -P0 upx -9 36 | 37 | FROM ${ALPINE} 38 | RUN apk add --no-cache dhcp radvd iptables ip6tables 39 | COPY --from=release /release/ / 40 | WORKDIR /xproxy/ 41 | ENTRYPOINT ["xproxy"] 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Dnomd343 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # XProxy 2 | 3 | > 虚拟代理网关,对局域网设备进行透明代理 4 | 5 | + ✅ 基于容器运行,无需修改主机路由配置,开箱即用 6 | 7 | + ✅ 独立的 MAC 地址,与宿主机网络栈无耦合,随开随关 8 | 9 | + ✅ 允许自定义 DNS 、上游网关、IP 地址等网络选项 10 | 11 | + ✅ 支持 TCP 、UDP 流量代理,完整的 Full-cone NAT 支持 12 | 13 | + ✅ 完全兼容 IPv6 ,支持 SLAAC 地址分配,RDNSS 与 DNSSL 配置 14 | 15 | + ✅ 内置 DHCP 与 DHCPv6 服务器,支持 IP 地址自动分配 16 | 17 | ## 拓扑模型 18 | 19 | XProxy 部署在内网 Linux 主机上,通过 `macvlan` 网络创建独立 MAC 地址的虚拟网关,劫持内网设备的网络流量进行透明代理;宿主机一般以单臂旁路由的方式接入,虚拟网关运行时不会干扰宿主机网络,同时宿主机系统的流量也可被网关代理。 20 | 21 | ```mermaid 22 | graph TD 23 | net{{互联网}} === route(路由器) 24 | subgraph 内网 25 | route --- client_1(设备1) 26 | route --- client_2(设备2) 27 | route --- client_3(设备3) 28 | subgraph 宿主设备 29 | client(虚拟网关) 30 | end 31 | route -.- client 32 | end 33 | ``` 34 | 35 | XProxy 运行以后,内网流量将被收集到代理内核上,支持 `xray` ,`v2ray` ,`sagray` 等多种内核,支持 `Shadowsocks` ,`ShadowsocksR` ,`VMess` ,`VLESS` ,`Trojan` ,`WireGuard` ,`SSH` ,`PingTunnel` 等多种代理协议,支持 `XTLS` ,`Reality` ,`WebSocket` ,`QUIC` ,`gRPC` 等多种传输方式。同时,得益于 V2ray 的路由设计,代理的网络流量可被精确地分流,可以依据内网设备、目标地址、访问端口、连接域名、流量类型等多种方式进行路由。 36 | 37 | 由于 XProxy 与宿主机网络完全解耦,一台主机上可运行多个虚拟网关,它们拥有不同的 MAC 地址,在网络模型上是多台独立的主机;因此各个虚拟网关能负责不同的功能,甚至它们之间还能互为上下级路由的关系,灵活实现多种网络功能。 38 | 39 | ## 配置格式 40 | 41 | > XProxy 支持 JSON 、YAML 与 TOML 格式的配置文件,其中 `.json` 与 `.toml` 后缀的文件分别以 JSON 与 TOML 格式解析,其余将以 YAML 格式解析。 42 | 43 | XProxy 的配置文件包含以下部分: 44 | 45 | ```yaml 46 | proxy: 47 | ··· 代理选项 ··· 48 | 49 | network: 50 | ··· 网络选项 ··· 51 | 52 | asset: 53 | ··· 路由资源 ··· 54 | 55 | custom: 56 | ··· 自定义脚本 ··· 57 | 58 | radvd: 59 | ··· IPv6路由广播 ··· 60 | 61 | dhcp: 62 | ··· DHCP服务选项 ··· 63 | 64 | ``` 65 | 66 | ### 代理选项 67 | 68 | ```yaml 69 | # 以下配置仅为示范 70 | proxy: 71 | bin: xray 72 | log: info 73 | http: 74 | web: 8080 75 | socks: 76 | proxy4: 1094 77 | direct4: 1084 78 | proxy6: 1096 79 | direct6: 1086 80 | addon: 81 | - tag: metrics 82 | port: 9090 83 | protocol: dokodemo-door 84 | settings: 85 | address: 127.0.0.1 86 | sniff: 87 | enable: true 88 | redirect: false 89 | exclude: 90 | - Mijia Cloud 91 | - courier.push.apple.com 92 | ``` 93 | 94 | > 入站代理中内置 `tproxy4` 与 `tproxy6` 两个接口,分别对应 IPv4 与 IPv6 的透明代理,可作为 `inboundTag` 在路由中引用。 95 | 96 | + `bin` :指定内核名称,默认为 `xray` 97 | 98 | > 自 `1.0.2` 起,XProxy 镜像仅自带 `xray` 内核,其他内核需要用户自行添加。 99 | > 100 | > 例如在 Docker 启动命令中加入 `-v {V2RAY_BIN}:/usr/bin/v2ray` 可以将 `v2ray` 内核添加到容器中,在 `bin` 选项中指定内核名称即可生效,或者使用 `PROXY_BIN=v2ray` 环境变量指定。 101 | 102 | + `log` :代理日志级别,可选 `debug` 、`info` 、`warning` 、`error` 、`none` ,默认为 `warning` 103 | 104 | + `http` 与 `socks` :配置 http 与 socks5 入站代理,使用 `key: value` 格式,前者指定入站标志(路由配置中的 inboundTag),后者指定监听端口,默认为空。 105 | 106 | + `addon` :自定义入站配置,每一项为单个内核 inbound 接口,具体格式可见[内核文档](https://xtls.github.io/config/inbound.html#inboundobject),默认为空。 107 | 108 | + `sniff` :嗅探选项,用于获取透明代理中的连接域名: 109 | 110 | + `enable` :是否启用嗅探功能,默认为 `false` 111 | 112 | + `redirect` :是否使用嗅探结果覆盖目标地址,默认为 `false`(v2ray 内核不支持) 113 | 114 | + `exclude` :不进行覆盖的域名列表,默认为空(仅 xray 内核支持) 115 | 116 | ### 网络选项 117 | 118 | ```yaml 119 | # 以下配置仅为示范 120 | network: 121 | dev: eth0 122 | dns: 123 | - 223.6.6.6 124 | - 119.29.29.29 125 | ipv4: 126 | gateway: 192.168.2.2 127 | address: 192.168.2.4/24 128 | ipv6: 129 | gateway: fc00::2 130 | address: fc00::4/64 131 | bypass: 132 | - 169.254.0.0/16 133 | - 224.0.0.0/3 134 | - fc00::/7 135 | - fe80::/10 136 | - ff00::/8 137 | exclude: 138 | - 192.168.2.2 139 | - 192.168.2.240/28 140 | ``` 141 | 142 | + `dev` :指定运行的网卡,一般与物理网卡同名,默认为空。 143 | 144 | + `dns` :指定系统默认 DNS 服务器,留空时保持原配置不变,默认为空。 145 | 146 | + `ipv4` 与 `ipv6` :指定 IPv4 与 IPv6 的网络信息,其中 `gateway` 为上游网关地址,`address` 为虚拟网关地址(CIDR 格式,包含子网长度),不填写时保持不变,默认为空。 147 | 148 | + `bypass` :绕过代理的目标网段或 IP,默认为空,建议绕过以下5个网段: 149 | 150 | + `169.254.0.0/16` :IPv4 链路本地地址 151 | 152 | + `224.0.0.0/3` :D类多点播送地址,E类保留地址 153 | 154 | + `fc00::/7` :IPv6 唯一本地地址 155 | 156 | + `fe80::/10` :IPv6 链路本地地址 157 | 158 | + `ff00::/8` :IPv6 组播地址 159 | 160 | + `exclude` :不代理的来源网段或 IP 161 | 162 | > `bypass` 与 `exclude` 中指定的 IP 或 CIDR,在运行时将不会被 TProxy 捕获,即不进入用户态的代理路由,相当于无损耗的直连。 163 | 164 | ### 路由资源 165 | 166 | ```yaml 167 | # 以下配置仅为示范 168 | asset: 169 | disable: false 170 | update: 171 | cron: "0 5 6 * * *" # 每天凌晨06点05分更新 172 | proxy: "socks5://[IP]:[PORT]" # 通过 socks5 代理更新资源 173 | url: 174 | geoip.dat: "https://cdn.dnomd343.top/v2ray-rules-dat/geoip.dat" 175 | geosite.dat: "https://cdn.dnomd343.top/v2ray-rules-dat/geosite.dat" 176 | ``` 177 | 178 | + `disable` :是否关闭路由资源文件载入,默认为 `false` 179 | 180 | + `cron` :触发更新的 Cron 表达式,留空时关闭自动升级,默认为空。 181 | 182 | + `proxy` :通过指定的代理服务更新资源文件,留空时直连更新,默认为空。 183 | 184 | + `url` :更新的文件名及下载地址,文件保存至 `assets` 中,默认为空。 185 | 186 | > 默认链接为 `Loyalsoldier/v2ray-rules-dat` 的镜像,如果您可以正常访问 Github,请换用以下 URL: 187 | > 188 | > + `https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat` 189 | > 190 | > + `https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat` 191 | 192 | ### 自定义脚本 193 | 194 | ```yaml 195 | # 以下配置仅为示范 196 | # fc00::4 tcp/53 & udp/53 <---> fc00::3 tcp/5353 & udp/5353 197 | # 192.168.2.4 tcp/53 & udp/53 <---> 192.168.2.3 tcp/53 & udp/5353 198 | custom: 199 | pre: 200 | - "iptables -t nat -A PREROUTING -d 192.168.2.4 -p udp --dport 53 -j DNAT --to-destination 192.168.2.3:5353" 201 | - "iptables -t nat -A POSTROUTING -d 192.168.2.3 -p udp --dport 5353 -j SNAT --to 192.168.2.4" 202 | - "iptables -t nat -A PREROUTING -d 192.168.2.4 -p tcp --dport 53 -j DNAT --to-destination 192.168.2.3:5353" 203 | - "iptables -t nat -A POSTROUTING -d 192.168.2.3 -p tcp --dport 5353 -j SNAT --to 192.168.2.4" 204 | - "ip6tables -t nat -A PREROUTING -d fc00::4 -p udp --dport 53 -j DNAT --to-destination [fc00::3]:5353" 205 | - "ip6tables -t nat -A POSTROUTING -d fc00::3 -p udp --dport 5353 -j SNAT --to fc00::4" 206 | - "ip6tables -t nat -A PREROUTING -d fc00::4 -p tcp --dport 53 -j DNAT --to-destination [fc00::3]:5353" 207 | - "ip6tables -t nat -A POSTROUTING -d fc00::3 -p tcp --dport 5353 -j SNAT --to fc00::4" 208 | post: 209 | - "echo Here is post process" 210 | - "echo Goodbye" 211 | ``` 212 | 213 | > 本功能用于注入自定义功能,基于 Alpine 的 ash 执行,可能不支持部分 bash 语法。 214 | 215 | + `pre` :自定义脚本命令,在代理启动前执行,默认为空。 216 | 217 | + `post` :自定义脚本命令,在服务关闭前执行,默认为空。 218 | 219 | ### IPv6路由广播 220 | 221 | Radvd 有大量配置选项,`XProxy` 均对其保持兼容,以下仅介绍部分常用选项,更多详细参数可参考[man文档](https://www.systutorials.com/docs/linux/man/5-radvd.conf/)。 222 | 223 | > 注意以下的 `on` 与 `off` 为字符串,但在部分 YAML 库中可能被解析成布尔值,为了安全起见,下游项目请注意转换时添加引号限定。 224 | 225 | ```yaml 226 | # 以下配置仅为示范 227 | radvd: 228 | log: 3 229 | dev: eth0 230 | enable: true 231 | option: 232 | AdvSendAdvert: on 233 | AdvManagedFlag: off 234 | AdvOtherConfigFlag: off 235 | client: 236 | - fe80::215:5dff:feb1:df9b 237 | - fe80::21d:72ff:fe96:aaff 238 | prefix: 239 | cidr: fc00::/64 240 | option: 241 | AdvOnLink: on 242 | AdvAutonomous: on 243 | AdvRouterAddr: off 244 | AdvValidLifetime: 43200 245 | AdvPreferredLifetime: 7200 246 | route: 247 | cidr: "" 248 | option: null 249 | rdnss: 250 | ip: 251 | - fc00::3 252 | - fc00::4 253 | option: null 254 | dnssl: 255 | suffix: 256 | - scut.343.re 257 | option: null 258 | ``` 259 | 260 | + `log` :RADVD 日志级别,可选 `0-5`,数值越大越详细,默认为 `0` 261 | 262 | + `dev` :执行 RA 广播的网卡,`enable` 为 `true` 时必选,一般与 `network` 中配置相同,默认为空。 263 | 264 | + `enable` :是否启动 RADVD,默认为 `false` 265 | 266 | + `option` :RADVD 主选项,完整参数列表查看[这里](https://code.tools/man/5/radvd.conf/#lbAD): 267 | 268 | + `AdvSendAdvert` :是否开启 RA 报文广播,启用 IPv6 时必须打开,默认为 `off` 269 | 270 | + `AdvManagedFlag` :指示 IPv6 管理地址配置,即 M 位,默认为 `off` 271 | 272 | + `AdvOtherConfigFlag` :指示 IPv6 其他有状态配置,即 O 位,默认为 `off` 273 | 274 | + > M 位和 O 位的详细定义在 [RFC4862](https://www.rfc-editor.org/rfc/rfc4862) 中给出: 275 | 276 | + `M=off` 且 `O=off` :使用 `Stateless` 模式,设备通过 RA 广播的前缀,配合 `EUI-64` 算法直接得到接口地址,即 `SLAAC` 方式。 277 | 278 | + `M=off` 且 `O=on` :使用 `Stateless DHCPv6` 模式,设备通过 RA 广播前缀与 `EUI-64` 计算接口地址,同时从 `DHCPv6` 获取 DNS 等其他配置。 279 | 280 | + `M=on` 且 `O=on` :使用 `Stateful DHCPv6` 模式,设备通过 `DHCPv6` 获取地址以及 DNS 等其他配置。 281 | 282 | + `M=on` 且 `O=off` :理论上不存在此配置。 283 | 284 | + `client` :配置此项后,仅发送 RA 通告到指定 IPv6 单播地址而非组播地址,默认为空(组播发送) 285 | 286 | + `prefix` :IPv6 地址前缀配置,`cidr` 指定分配的前缀及掩码长度,`option` 指定[前缀选项](https://code.tools/man/5/radvd.conf/#lbAE)。 287 | 288 | + `route` :指定 IPv6 路由,`cidr` 为通告的路由 CIDR(注意客户端仅将 RA 报文来源链路地址设置为 IPv6 网关,此处设置并不能更改路由网关地址),`option` 指定[路由选项](https://code.tools/man/5/radvd.conf/#lbAF)。 289 | 290 | + `rdnss` :递归 DNS 服务器地址,`ip` 指定 IPv6 下的 DNS 服务器列表,`option` 指定[RDNSS选项](https://code.tools/man/5/radvd.conf/#lbAG)。 291 | 292 | + `dnssl` :DNS 搜寻域名,`suffix` 指定 DNS 解析的搜寻后缀列表,`option` 指定[DNSSL选项](https://code.tools/man/5/radvd.conf/#lbAH)。 293 | 294 | > `RDNSS` 与 `DNSSL` 在 [RFC6106](https://www.rfc-editor.org/rfc/rfc6106) 中定义,将 DNS 配置信息直接放置在 RA 报文中发送,使用 `SLAAC` 时无需 `DHCPv6` 即可获取 DNS 服务器,但旧版本 Windows 与 Android 等系统不支持该功能。 295 | 296 | ### DHCP服务选项 297 | 298 | > DHCP 与 DHCPv6 功能由 [ISC-DHCP](https://www.isc.org/dhcp/) 项目提供。 299 | 300 | ```yaml 301 | # 以下配置仅为示范 302 | dhcp: 303 | ipv4: 304 | enable: false 305 | config: | 306 | default-lease-time 600; 307 | max-lease-time 7200; 308 | 309 | subnet 192.168.2.0 netmask 255.255.255.0 { 310 | range 192.168.2.100 192.168.2.200; 311 | } 312 | 313 | host example { 314 | hardware ethernet 03:48:c0:5d:bd:95; 315 | fixed-address 192.168.2.233; 316 | } 317 | ipv6: 318 | enable: false 319 | config: | 320 | ... 321 | ``` 322 | 323 | + `ipv4` 和 `ipv6` :分别配置 DHCPv4 与 DHCPv6 服务。 324 | 325 | + `enable` :是否启动 DHCP 服务,默认为 `false` 326 | 327 | + `config` :DHCP 服务配置文件,具体配置内容参考[man文档](https://linux.die.net/man/5/dhcpd.conf)。 328 | 329 | ## 部署流程 330 | 331 | ### 1. 初始配置 332 | 333 | > XProxy 基于 macvlan 网络,开启网卡混杂模式后可以捕获非本机 MAC 地址的数据包,以此模拟出不同 MAC 地址的网卡。 334 | 335 | ```bash 336 | # 开启混杂模式,网卡按实际情况指定 337 | $ ip link set eth0 promisc on 338 | 339 | # 启用IPv6内核模块 340 | $ modprobe ip6table_filter 341 | ``` 342 | 343 | 在 Docker 中创建 macvlan 网络: 344 | 345 | ```bash 346 | # 网络配置按实际情况指定 347 | docker network create -d macvlan \ 348 | --subnet={IPv4网段} --gateway={IPv4网关} \ 349 | --subnet={IPv6网段} --gateway={IPv6网关} \ 350 | --ipv6 -o parent=eth0 macvlan # 在eth0网卡上运行 351 | ``` 352 | 353 | ### 2. 开始部署 354 | 355 | > 本项目基于 Docker 构建,在 [Docker Hub](https://hub.docker.com/r/dnomd343/xproxy) 或 [Github Package](https://github.com/dnomd343/XProxy/pkgs/container/xproxy) 可以查看已构建的各版本镜像。 356 | 357 | XProxy 同时发布在多个镜像源上: 358 | 359 | + `Docker Hub` :`dnomd343/xproxy` 360 | 361 | + `Github Package` :`ghcr.io/dnomd343/xproxy` 362 | 363 | + `阿里云镜像` :`registry.cn-shenzhen.aliyuncs.com/dnomd343/xproxy` 364 | 365 | > 下述命令中,容器路径可替换为上述其他源,国内网络建议首选阿里云仓库。 366 | 367 | 使用以下命令启动虚拟网关,配置文件将存储在本机 `/etc/xproxy/` 目录下: 368 | 369 | ```bash 370 | docker run --restart always \ 371 | --privileged --network macvlan -dt \ 372 | --name xproxy --hostname xproxy \ # 可选,指定容器名称与主机名 373 | --volume /etc/xproxy/:/xproxy/ \ 374 | --volume /etc/timezone:/etc/timezone:ro \ # 以下两句可选,用于映射宿主机时区信息(容器内默认为UTC0时区) 375 | --volume /etc/localtime:/etc/localtime:ro \ 376 | dnomd343/xproxy:latest 377 | ``` 378 | 379 | 成功运行以后,存储目录将生成以下文件夹: 380 | 381 | + `assets` :存储路由资源文件 382 | 383 | + `config` :存储代理配置文件 384 | 385 | + `log` :存储日志文件 386 | 387 | + `dhcp` :存储 DHCP 数据库文件(仅当 DHCP 服务开启) 388 | 389 | #### 路由资源文件夹 390 | 391 | `assets` 目录默认放置 `geoip.dat` 与 `geosite.dat` 路由规则文件,分别存储IP与域名归属信息,在 `update` 中配置的自动更新将保存到此处;本目录亦可放置自定义规则文件,在[路由配置](https://xtls.github.io/config/routing.html#ruleobject)中以 `ext:${FILE}:tag` 格式引用。 392 | 393 | #### 代理配置文件夹 394 | 395 | `config` 目录存储代理配置文件,所有 `.json` 后缀文件均会被载入,用户可配置除 `inbounds` 与 `log` 以外的所有代理选项,多配置文件需要注意[合并规则](https://xtls.github.io/config/features/multiple.html#%E8%A7%84%E5%88%99%E8%AF%B4%E6%98%8E)。 396 | 397 | 为了正常工作,容器初始化时会载入以下 `outbounds.json` 作为默认出站配置,其指定所有流量为直连: 398 | 399 | ```json 400 | { 401 | "outbounds": [ 402 | { 403 | "protocol": "freedom", 404 | "settings": {} 405 | } 406 | ] 407 | } 408 | ``` 409 | 410 | #### 日志文件夹 411 | 412 | `log` 目录用于放置日志文件 413 | 414 | + `xproxy.log` 记录 XProxy 工作信息 415 | 416 | + `access.log` 记录代理流量连接信息 417 | 418 | + `error.log` 记录代理连接错误信息 419 | 420 | + 若启用RADVD功能,其日志将保存到 `radvd.log` 中 421 | 422 | ### 3. 调整配置文件 423 | 424 | 容器首次初始化时将生成默认配置文件 `xproxy.yml` ,其内容如下: 425 | 426 | ```yaml 427 | # default configure file for xproxy 428 | proxy: 429 | core: xray 430 | log: warning 431 | 432 | network: 433 | bypass: 434 | - 169.254.0.0/16 435 | - 224.0.0.0/3 436 | - fc00::/7 437 | - fe80::/10 438 | - ff00::/8 439 | 440 | asset: 441 | update: 442 | cron: "0 5 6 * * *" 443 | url: 444 | geoip.dat: "https://cdn.dnomd343.top/v2ray-rules-dat/geoip.dat" 445 | geosite.dat: "https://cdn.dnomd343.top/v2ray-rules-dat/geosite.dat" 446 | ``` 447 | 448 | 用户需要根据实际需求更改配置文件,保存以后重启容器即可生效: 449 | 450 | ```bash 451 | docker restart xproxy 452 | ``` 453 | 454 | 如果配置文件出错,`XProxy` 将无法正常工作,您可以使用以下命令查看工作日志: 455 | 456 | ```bash 457 | docker logs -f xproxy 458 | ``` 459 | 460 | ### 4. 宿主机访问虚拟网关 461 | 462 | > 这一步旨在让宿主机能够使用虚拟网关,若无此需求可以跳过。 463 | 464 | 由于 macvlan 限制,宿主机网卡无法直接与虚拟网关通讯,需要另外配置网桥才可连接。 465 | 466 | > 以下为配置基于 Debian,基于 RH、Arch 等的发行版配置略有不同。 467 | 468 | 编辑网卡配置文件: 469 | 470 | ```bash 471 | vim /etc/network/interfaces 472 | ``` 473 | 474 | 补充如下配置,具体网络信息需要按实际情况指定: 475 | 476 | ```ini 477 | auto eth0 # 宿主机物理网卡 478 | iface eth0 inet manual 479 | 480 | auto macvlan 481 | iface macvlan inet static 482 | address 192.168.2.34 # 宿主机静态IP地址 483 | netmask 255.255.255.0 # 子网掩码 484 | gateway 192.168.2.2 # 虚拟网关IP地址 485 | dns-nameservers 192.168.2.3 # DNS主服务器 486 | dns-nameservers 192.168.2.1 # DNS备用服务器 487 | pre-up ip link add macvlan link eth0 type macvlan mode bridge # 宿主机网卡上创建网桥 488 | post-down ip link del macvlan link eth0 type macvlan mode bridge # 退出时删除网桥 489 | ``` 490 | 491 | 重启宿主机网络生效(或直接重启系统): 492 | 493 | ```bash 494 | $ /etc/init.d/networking restart 495 | [ ok ] Restarting networking (via systemctl): networking.service. 496 | ``` 497 | 498 | ### 5. 局域网设备访问 499 | 500 | > 对于手动配置了静态 IP 的设备,需要修改网关地址为虚拟网关 IP 地址。 501 | 502 | 配置完成后,容器 IP 即为虚拟网关地址,内网其他设备的网关设置为该地址即可被透明代理,因此这里需要配置 DHCP 与 RADVD 路由广播,让内网设备自动接入虚拟网关。 503 | 504 | > 您可以监视 `log/access.log` 文件,设备正常接入后会在此输出访问日志。 505 | 506 | + IPv4 下,修改内网 DHCP 服务器配置(一般位于路由器上),将网关改为容器 IP 地址,保存后重新接入设备即可生效。 507 | 508 | + IPv6 下,你需要关闭路由或上级网络的 RA 广播功能,然后开启配置中的 RADVD 选项,如果需要使用 DHCPv6 ,可调整配置中的 M 位和 O 位开启状态,保存后将设备重新接入网络即可。 509 | 510 | ## 演示实例 511 | 512 | 由于 XProxy 涉及较为复杂的网络配置,这里准备了两个详细的实例供您了解: 513 | 514 | + 实例1. [使用 XProxy 绕过校园网认证登录](./docs/campus_network_cracking.md) 515 | 516 | + 实例2. [家庭网络的 IPv4 与 IPv6 透明代理](./docs/dual_stack_network_proxy.md) 517 | 518 | ## 开发相关 519 | 520 | ### 运行参数 521 | 522 | XProxy 默认使用 `/xproxy` 作为存储文件夹,该文件夹映射到外部主机作为持久存储,您可以使用 `EXPOSE_DIR` 环境变量修改该文件夹路径。 523 | 524 | XProxy 默认使用 `xray` 作为代理内核,您可以使用 `PROXY_BIN` 环境变量来指定其他内核。 525 | 526 | + `--config` :指定配置文件名称,默认为 `xproxy.yml` 527 | 528 | + `--debug` :开启调试模式,输出日志切换到 DEBUG 级别。 529 | 530 | ### TProxy 配置 531 | 532 | XProxy 默认使用以下配置: 533 | 534 | + IPv4 路由表号:`104`,使用 `IPV4_TABLE` 环境变量修改。 535 | 536 | + IPv6 路由表号:`106`,使用 `IPV6_TABLE` 环境变量修改。 537 | 538 | + IPv4 透明代理端口:`7288`,使用 `IPV4_TPROXY` 环境变量修改。 539 | 540 | + IPv6 透明代理端口:`7289`,使用 `IPV6_TPROXY` 环境变量修改。 541 | 542 | ### 容器构建 543 | 544 | #### 本地构建 545 | 546 | ```bash 547 | $ git clone https://github.com/dnomd343/XProxy.git 548 | $ cd ./XProxy/ 549 | $ docker build -t xproxy . 550 | ··· 551 | ``` 552 | 553 | #### 交叉构建 554 | 555 | > XProxy 针对 `buildkit` 进行优化,使用 `buildx` 命令可加快构建速度 556 | 557 | ```bash 558 | $ git clone https://github.com/dnomd343/XProxy.git 559 | $ cd ./XProxy/ 560 | $ docker buildx build \ 561 | -t dnomd343/xproxy \ 562 | -t ghcr.io/dnomd343/xproxy \ 563 | -t registry.cn-shenzhen.aliyuncs.com/dnomd343/xproxy \ 564 | --platform="linux/amd64,linux/arm64,linux/386,linux/arm/v7" . --push 565 | ``` 566 | 567 | ## 许可证 568 | 569 | MIT ©2022 [@dnomd343](https://github.com/dnomd343) 570 | -------------------------------------------------------------------------------- /cmd/asset/asset.go: -------------------------------------------------------------------------------- 1 | package asset 2 | 3 | import ( 4 | "XProxy/cmd/common" 5 | log "github.com/sirupsen/logrus" 6 | "path" 7 | ) 8 | 9 | func extractFile(archive string, geoFile string, targetDir string) { // extract `.dat` file into targetDir 10 | filePath := path.Join(targetDir, geoFile) 11 | if common.IsFileExist(filePath) { 12 | log.Debugf("Asset %s exist -> skip extract", geoFile) 13 | return 14 | } 15 | log.Infof("Extract asset file -> %s", filePath) 16 | common.RunCommand("tar", "xf", archive, geoFile, "-C", targetDir) 17 | } 18 | 19 | func Load(assetFile string, assetDir string) { 20 | common.CreateFolder(assetDir) 21 | extractFile(assetFile, "geoip.dat", assetDir) 22 | extractFile(assetFile, "geosite.dat", assetDir) 23 | } 24 | -------------------------------------------------------------------------------- /cmd/asset/update.go: -------------------------------------------------------------------------------- 1 | package asset 2 | 3 | import ( 4 | "XProxy/cmd/common" 5 | "github.com/robfig/cron" 6 | log "github.com/sirupsen/logrus" 7 | "os" 8 | "os/signal" 9 | "path" 10 | "syscall" 11 | ) 12 | 13 | type Config struct { 14 | Disable bool `yaml:"disable" json:"disable" toml:"disable"` 15 | Update struct { 16 | Proxy string `yaml:"proxy" json:"proxy" toml:"proxy"` 17 | Cron string `yaml:"cron" json:"cron" toml:"cron"` 18 | Url map[string]string `yaml:"url" json:"url" toml:"url"` 19 | } 20 | } 21 | 22 | func updateAsset(urls map[string]string, assetDir string, updateProxy string) { // download new assets 23 | defer func() { 24 | if err := recover(); err != nil { 25 | log.Errorf("Update failed -> %v", err) 26 | } 27 | }() 28 | if len(urls) != 0 { 29 | log.Info("Start update assets") 30 | for file, url := range urls { 31 | if !common.DownloadFile(url, path.Join(assetDir, file), updateProxy) { // maybe override old asset 32 | log.Infof("Try to download asset `%s` again", file) 33 | common.DownloadFile(url, path.Join(assetDir, file), updateProxy) // download retry 34 | } 35 | } 36 | log.Infof("Assets update complete") 37 | } 38 | } 39 | 40 | func AutoUpdate(config *Config, assetDir string) { // set cron task for auto update 41 | if config.Update.Cron != "" { 42 | autoUpdate := cron.New() 43 | _ = autoUpdate.AddFunc(config.Update.Cron, func() { // cron function 44 | updateAsset(config.Update.Url, assetDir, config.Update.Proxy) 45 | }) 46 | autoUpdate.Start() 47 | } 48 | updateChan := make(chan os.Signal, 1) 49 | go func() { 50 | for { 51 | <-updateChan 52 | log.Infof("Receive SIGALRM -> update assets") 53 | updateAsset(config.Update.Url, assetDir, config.Update.Proxy) 54 | } 55 | }() 56 | signal.Notify(updateChan, syscall.SIGALRM) 57 | } 58 | -------------------------------------------------------------------------------- /cmd/common/file.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "github.com/andybalholm/brotli" 5 | "github.com/go-http-utils/headers" 6 | "github.com/klauspost/compress/flate" 7 | "github.com/klauspost/compress/gzip" 8 | log "github.com/sirupsen/logrus" 9 | "io" 10 | "io/ioutil" 11 | "net/http" 12 | "net/url" 13 | "os" 14 | "strings" 15 | ) 16 | 17 | func CreateFolder(folderPath string) { 18 | folder, err := os.Stat(folderPath) 19 | if err == nil && folder.IsDir() { // folder exist -> skip create process 20 | return 21 | } 22 | log.Debugf("Create folder -> %s", folderPath) 23 | if err := os.MkdirAll(folderPath, 0755); err != nil { 24 | log.Errorf("Failed to create folder -> %s", folderPath) 25 | } 26 | } 27 | 28 | func IsFileExist(filePath string) bool { 29 | s, err := os.Stat(filePath) 30 | if err != nil { // file or folder not exist 31 | return false 32 | } 33 | return !s.IsDir() 34 | } 35 | 36 | func WriteFile(filePath string, content string, overwrite bool) { 37 | if !overwrite && IsFileExist(filePath) { // file exist and don't overwrite 38 | log.Debugf("File `%s` exist -> skip write", filePath) 39 | return 40 | } 41 | log.Debugf("Write file `%s` -> \n%s", filePath, content) 42 | if err := os.WriteFile(filePath, []byte(content), 0644); err != nil { 43 | log.Panicf("Failed to write `%s` -> %v", filePath, err) 44 | } 45 | } 46 | 47 | func ListFiles(folderPath string, suffix string) []string { 48 | var fileList []string 49 | files, err := ioutil.ReadDir(folderPath) 50 | if err != nil { 51 | log.Panicf("Failed to list folder -> %s", folderPath) 52 | } 53 | for _, file := range files { 54 | if strings.HasSuffix(file.Name(), suffix) { 55 | fileList = append(fileList, file.Name()) 56 | } 57 | } 58 | return fileList 59 | } 60 | 61 | func CopyFile(source string, target string) { 62 | log.Infof("Copy file `%s` => `%s`", source, target) 63 | if IsFileExist(target) { 64 | log.Debugf("File `%s` will be overridden", target) 65 | } 66 | srcFile, err := os.Open(source) 67 | defer srcFile.Close() 68 | if err != nil { 69 | log.Panicf("Failed to open file -> %s", source) 70 | } 71 | dstFile, err := os.OpenFile(target, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0644) 72 | defer dstFile.Close() 73 | if err != nil { 74 | log.Panicf("Failed to open file -> %s", target) 75 | } 76 | if _, err = io.Copy(dstFile, srcFile); err != nil { 77 | log.Panicf("Failed to copy from `%s` to `%s`", source, target) 78 | } 79 | } 80 | 81 | func DownloadBytes(fileUrl string, proxyUrl string) ([]byte, error) { 82 | client := http.Client{} 83 | if proxyUrl == "" { 84 | log.Infof("Download `%s` without proxy", fileUrl) 85 | } else { // use proxy for download 86 | log.Infof("Download `%s` via `%s`", fileUrl, proxyUrl) 87 | rawUrl, _ := url.Parse(proxyUrl) 88 | client = http.Client{ 89 | Transport: &http.Transport{ 90 | Proxy: http.ProxyURL(rawUrl), 91 | }, 92 | } 93 | } 94 | 95 | req, err := http.NewRequest("GET", fileUrl, nil) 96 | if err != nil { 97 | log.Errorf("Failed to create http request") 98 | return nil, err 99 | } 100 | req.Header.Set(headers.AcceptEncoding, "gzip, deflate, br") 101 | resp, err := client.Do(req) 102 | if err != nil { 103 | log.Errorf("Failed to execute http GET request") 104 | return nil, err 105 | } 106 | if resp != nil { 107 | defer resp.Body.Close() 108 | log.Debugf("Remote data downloaded successfully") 109 | } 110 | 111 | switch resp.Header.Get(headers.ContentEncoding) { 112 | case "br": 113 | log.Debugf("Downloaded content using brolti encoding") 114 | return io.ReadAll(brotli.NewReader(resp.Body)) 115 | case "gzip": 116 | log.Debugf("Downloaded content using gzip encoding") 117 | gr, err := gzip.NewReader(resp.Body) 118 | if err != nil { 119 | return nil, err 120 | } 121 | return io.ReadAll(gr) 122 | case "deflate": 123 | log.Debugf("Downloaded content using deflate encoding") 124 | zr := flate.NewReader(resp.Body) 125 | defer zr.Close() 126 | return io.ReadAll(zr) 127 | default: 128 | return io.ReadAll(resp.Body) 129 | } 130 | } 131 | 132 | func DownloadFile(fileUrl string, filePath string, proxyUrl string) bool { 133 | log.Debugf("File download `%s` => `%s`", fileUrl, filePath) 134 | data, err := DownloadBytes(fileUrl, proxyUrl) 135 | if err != nil { 136 | log.Errorf("Download `%s` error -> %v", fileUrl, err) 137 | return false 138 | } 139 | 140 | file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0644) 141 | defer file.Close() 142 | if err != nil { 143 | log.Panicf("Open `%s` error -> %v", filePath, err) 144 | return false 145 | } 146 | if _, err = file.Write(data); err != nil { 147 | log.Errorf("File `%s` save error -> %v", filePath, err) 148 | return false 149 | } 150 | log.Infof("Download success `%s` => `%s`", fileUrl, filePath) 151 | return true 152 | } 153 | -------------------------------------------------------------------------------- /cmd/common/func.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "encoding/json" 5 | log "github.com/sirupsen/logrus" 6 | "net" 7 | "os/exec" 8 | "strings" 9 | "syscall" 10 | ) 11 | 12 | func isIP(ipAddr string, isCidr bool) bool { 13 | if !isCidr { 14 | return net.ParseIP(ipAddr) != nil 15 | } 16 | _, _, err := net.ParseCIDR(ipAddr) 17 | return err == nil 18 | } 19 | 20 | func IsIPv4(ipAddr string, isCidr bool) bool { 21 | return isIP(ipAddr, isCidr) && strings.Contains(ipAddr, ".") 22 | } 23 | 24 | func IsIPv6(ipAddr string, isCidr bool) bool { 25 | return isIP(ipAddr, isCidr) && strings.Contains(ipAddr, ":") 26 | } 27 | 28 | func JsonEncode(raw interface{}) string { 29 | jsonOutput, _ := json.MarshalIndent(raw, "", " ") // json encode 30 | return string(jsonOutput) 31 | } 32 | 33 | func RunCommand(command ...string) (int, string) { 34 | log.Debugf("Running system command -> %v", command) 35 | process := exec.Command(command[0], command[1:]...) 36 | output, _ := process.CombinedOutput() 37 | log.Debugf("Command %v -> \n%s", command, string(output)) 38 | code := process.ProcessState.Sys().(syscall.WaitStatus).ExitStatus() 39 | if code != 0 { 40 | log.Warningf("Command %v return code %d", command, code) 41 | } 42 | return code, string(output) 43 | } 44 | -------------------------------------------------------------------------------- /cmd/config/decode.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "XProxy/cmd/asset" 5 | "XProxy/cmd/common" 6 | "XProxy/cmd/custom" 7 | "XProxy/cmd/dhcp" 8 | "XProxy/cmd/proxy" 9 | "XProxy/cmd/radvd" 10 | "encoding/json" 11 | "github.com/BurntSushi/toml" 12 | log "github.com/sirupsen/logrus" 13 | "gopkg.in/yaml.v3" 14 | "net/url" 15 | ) 16 | 17 | type NetConfig struct { 18 | Gateway string `yaml:"gateway" json:"gateway" toml:"gateway"` // network gateway 19 | Address string `yaml:"address" json:"address" toml:"address"` // network address 20 | } 21 | 22 | type RawConfig struct { 23 | Asset asset.Config `yaml:"asset" json:"asset" toml:"asset"` 24 | Radvd radvd.Config `yaml:"radvd" json:"radvd" toml:"radvd"` 25 | DHCP dhcp.Config `yaml:"dhcp" json:"dhcp" toml:"dhcp"` 26 | Proxy proxy.Config `yaml:"proxy" json:"proxy" toml:"proxy"` 27 | Custom custom.Config `yaml:"custom" json:"custom" toml:"custom"` 28 | Network struct { 29 | Dev string `yaml:"dev" json:"dev" toml:"dev"` 30 | DNS []string `yaml:"dns" json:"dns" toml:"dns"` 31 | ByPass []string `yaml:"bypass" json:"bypass" toml:"bypass"` 32 | Exclude []string `yaml:"exclude" json:"exclude" toml:"exclude"` 33 | IPv4 NetConfig `yaml:"ipv4" json:"ipv4" toml:"ipv4"` 34 | IPv6 NetConfig `yaml:"ipv6" json:"ipv6" toml:"ipv6"` 35 | } `yaml:"network" json:"network" toml:"network"` 36 | } 37 | 38 | func configDecode(raw []byte, fileSuffix string) RawConfig { 39 | var config RawConfig 40 | log.Debugf("Config raw content -> \n%s", string(raw)) 41 | if fileSuffix == ".json" { 42 | if err := json.Unmarshal(raw, &config); err != nil { // json format decode 43 | log.Panicf("Decode JSON config file error -> %v", err) 44 | } 45 | } else if fileSuffix == ".toml" { 46 | if err := toml.Unmarshal(raw, &config); err != nil { // toml format decode 47 | log.Panicf("Decode TOML config file error -> %v", err) 48 | } 49 | } else { 50 | if err := yaml.Unmarshal(raw, &config); err != nil { // yaml format decode 51 | log.Panicf("Decode YAML config file error -> %v", err) 52 | } 53 | } 54 | log.Debugf("Decoded configure -> %v", config) 55 | return config 56 | } 57 | 58 | func decodeDev(rawConfig *RawConfig, config *Config) { 59 | config.Dev = rawConfig.Network.Dev 60 | if config.Dev == "" { 61 | setV4 := rawConfig.Network.IPv4.Address != "" || rawConfig.Network.IPv4.Gateway != "" 62 | setV6 := rawConfig.Network.IPv6.Address != "" || rawConfig.Network.IPv6.Gateway != "" 63 | if setV4 || setV6 { 64 | log.Panicf("Missing dev option in network settings") 65 | } 66 | } 67 | log.Debugf("Network device -> %s", config.Dev) 68 | } 69 | 70 | func decodeDns(rawConfig *RawConfig, config *Config) { 71 | for _, address := range rawConfig.Network.DNS { // dns options 72 | if common.IsIPv4(address, false) || common.IsIPv6(address, false) { 73 | config.DNS = append(config.DNS, address) 74 | } else { 75 | log.Panicf("Invalid DNS server -> %s", address) 76 | } 77 | } 78 | log.Debugf("DNS server -> %v", config.DNS) 79 | } 80 | 81 | func decodeBypass(rawConfig *RawConfig, config *Config) { 82 | for _, address := range rawConfig.Network.ByPass { // bypass options 83 | if common.IsIPv4(address, true) || common.IsIPv4(address, false) { 84 | config.IPv4.Bypass = append(config.IPv4.Bypass, address) 85 | } else if common.IsIPv6(address, true) || common.IsIPv6(address, false) { 86 | config.IPv6.Bypass = append(config.IPv6.Bypass, address) 87 | } else { 88 | log.Panicf("Invalid bypass IP or CIDR -> %s", address) 89 | } 90 | } 91 | log.Debugf("IPv4 bypass -> %s", config.IPv4.Bypass) 92 | log.Debugf("IPv6 bypass -> %s", config.IPv6.Bypass) 93 | } 94 | 95 | func decodeExclude(rawConfig *RawConfig, config *Config) { 96 | for _, address := range rawConfig.Network.Exclude { // exclude options 97 | if common.IsIPv4(address, true) || common.IsIPv4(address, false) { 98 | config.IPv4.Exclude = append(config.IPv4.Exclude, address) 99 | } else if common.IsIPv6(address, true) || common.IsIPv6(address, false) { 100 | config.IPv6.Exclude = append(config.IPv6.Exclude, address) 101 | } else { 102 | log.Panicf("Invalid exclude IP or CIDR -> %s", address) 103 | } 104 | } 105 | log.Debugf("IPv4 exclude -> %s", config.IPv4.Exclude) 106 | log.Debugf("IPv6 exclude -> %s", config.IPv6.Exclude) 107 | } 108 | 109 | func decodeIPv4(rawConfig *RawConfig, config *Config) { 110 | config.IPv4.Address = rawConfig.Network.IPv4.Address 111 | config.IPv4.Gateway = rawConfig.Network.IPv4.Gateway 112 | if config.IPv4.Address != "" && !common.IsIPv4(config.IPv4.Address, true) { 113 | log.Panicf("Invalid IPv4 address (CIDR) -> %s", config.IPv4.Address) 114 | } 115 | if config.IPv4.Gateway != "" && !common.IsIPv4(config.IPv4.Gateway, false) { 116 | log.Panicf("Invalid IPv4 gateway -> %s", config.IPv4.Gateway) 117 | } 118 | log.Debugf("IPv4 -> address = %s | gateway = %s", config.IPv4.Address, config.IPv4.Gateway) 119 | } 120 | 121 | func decodeIPv6(rawConfig *RawConfig, config *Config) { 122 | config.IPv6.Address = rawConfig.Network.IPv6.Address 123 | config.IPv6.Gateway = rawConfig.Network.IPv6.Gateway 124 | if config.IPv6.Address != "" && !common.IsIPv6(config.IPv6.Address, true) { 125 | log.Panicf("Invalid IPv6 address (CIDR) -> %s", config.IPv6.Address) 126 | } 127 | if config.IPv6.Gateway != "" && !common.IsIPv6(config.IPv6.Gateway, false) { 128 | log.Panicf("Invalid IPv6 gateway -> %s", config.IPv6.Gateway) 129 | } 130 | log.Debugf("IPv6 -> address = %s | gateway = %s", config.IPv6.Address, config.IPv6.Gateway) 131 | } 132 | 133 | func decodeProxy(rawConfig *RawConfig, config *Config) { 134 | config.Proxy = rawConfig.Proxy 135 | if config.Proxy.Bin == "" { 136 | config.Proxy.Bin = "xray" // default proxy bin 137 | } 138 | log.Debugf("Proxy bin -> %s", config.Proxy.Bin) 139 | log.Debugf("Proxy log level -> %s", config.Proxy.Log) 140 | log.Debugf("Http inbounds -> %v", config.Proxy.Http) 141 | log.Debugf("Socks5 inbounds -> %v", config.Proxy.Socks) 142 | log.Debugf("Add-on inbounds -> %v", config.Proxy.AddOn) 143 | log.Debugf("Connection sniff -> %t", config.Proxy.Sniff.Enable) 144 | log.Debugf("Connection redirect -> %t", config.Proxy.Sniff.Redirect) 145 | log.Debugf("Connection sniff exclude -> %v", config.Proxy.Sniff.Exclude) 146 | } 147 | 148 | func decodeRadvd(rawConfig *RawConfig, config *Config) { 149 | config.Radvd = rawConfig.Radvd 150 | if config.Radvd.Enable && config.Radvd.Dev == "" { 151 | log.Panicf("Radvd enabled without dev option") 152 | } 153 | log.Debugf("Radvd log level -> %d", config.Radvd.Log) 154 | log.Debugf("Radvd network dev -> %s", config.Radvd.Dev) 155 | log.Debugf("Radvd enable -> %t", config.Radvd.Enable) 156 | log.Debugf("Radvd options -> %v", config.Radvd.Option) 157 | log.Debugf("Radvd prefix -> %v", config.Radvd.Prefix) 158 | log.Debugf("Radvd route -> %v", config.Radvd.Route) 159 | log.Debugf("Radvd clients -> %v", config.Radvd.Client) 160 | log.Debugf("Radvd RDNSS -> %v", config.Radvd.RDNSS) 161 | log.Debugf("Radvd DNSSL -> %v", config.Radvd.DNSSL) 162 | } 163 | 164 | func decodeDhcp(rawConfig *RawConfig, config *Config) { 165 | config.DHCP = rawConfig.DHCP 166 | log.Debugf("DHCPv4 enable -> %t", config.DHCP.IPv4.Enable) 167 | log.Debugf("DHCPv4 config -> \n%s", config.DHCP.IPv4.Configure) 168 | log.Debugf("DHCPv6 enable -> %t", config.DHCP.IPv6.Enable) 169 | log.Debugf("DHCPv6 config -> \n%s", config.DHCP.IPv6.Configure) 170 | } 171 | 172 | func decodeUpdate(rawConfig *RawConfig, config *Config) { 173 | config.Asset = rawConfig.Asset 174 | if config.Asset.Update.Proxy != "" { 175 | _, err := url.Parse(config.Asset.Update.Proxy) // check proxy info 176 | if err != nil { 177 | log.Panicf("Invalid asset update proxy -> %s", config.Asset.Update.Proxy) 178 | } 179 | } 180 | log.Debugf("Asset disable -> %t", config.Asset.Disable) 181 | log.Debugf("Asset update proxy -> %s", config.Asset.Update.Proxy) 182 | log.Debugf("Asset update cron -> %s", config.Asset.Update.Cron) 183 | log.Debugf("Asset update urls -> %v", config.Asset.Update.Url) 184 | } 185 | 186 | func decodeCustom(rawConfig *RawConfig, config *Config) { 187 | config.Custom = rawConfig.Custom 188 | log.Debugf("Custom pre-script -> %v", config.Custom.Pre) 189 | log.Debugf("Custom post-script -> %v", config.Custom.Post) 190 | } 191 | -------------------------------------------------------------------------------- /cmd/config/default.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "XProxy/cmd/common" 5 | "bytes" 6 | "encoding/json" 7 | "github.com/BurntSushi/toml" 8 | log "github.com/sirupsen/logrus" 9 | "gopkg.in/yaml.v3" 10 | "path" 11 | ) 12 | 13 | var defaultConfig = map[string]interface{}{ 14 | "proxy": map[string]string{ 15 | "log": "warning", 16 | }, 17 | "network": map[string]interface{}{ 18 | "bypass": []string{ 19 | "169.254.0.0/16", 20 | "224.0.0.0/3", 21 | "fc00::/7", 22 | "fe80::/10", 23 | "ff00::/8", 24 | }, 25 | }, 26 | "asset": map[string]interface{}{ 27 | "update": map[string]interface{}{ 28 | "cron": "0 5 6 * * *", 29 | "url": map[string]string{ 30 | "geoip.dat": "https://cdn.dnomd343.top/v2ray-rules-dat/geoip.dat", 31 | "geosite.dat": "https://cdn.dnomd343.top/v2ray-rules-dat/geosite.dat", 32 | }, 33 | }, 34 | }, 35 | } 36 | 37 | func toJSON(config interface{}) string { // convert to JSON string 38 | jsonRaw, _ := json.MarshalIndent(config, "", " ") 39 | return string(jsonRaw) 40 | } 41 | 42 | func toYAML(config interface{}) string { // convert to YAML string 43 | buf := new(bytes.Buffer) 44 | encoder := yaml.NewEncoder(buf) 45 | encoder.SetIndent(2) // with 2 space indent 46 | _ = encoder.Encode(config) 47 | return buf.String() 48 | } 49 | 50 | func toTOML(config interface{}) string { // convert to TOML string 51 | buf := new(bytes.Buffer) 52 | _ = toml.NewEncoder(buf).Encode(config) 53 | return buf.String() 54 | } 55 | 56 | func loadDefaultConfig(configFile string) { 57 | log.Infof("Load default configure -> %s", configFile) 58 | suffix := path.Ext(configFile) 59 | if suffix == ".json" { 60 | common.WriteFile(configFile, toJSON(defaultConfig), false) // JSON format 61 | } else if suffix == ".toml" { 62 | common.WriteFile(configFile, toTOML(defaultConfig), false) // TOML format 63 | } else { 64 | common.WriteFile(configFile, toYAML(defaultConfig), false) // YAML format 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /cmd/config/main.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "XProxy/cmd/asset" 5 | "XProxy/cmd/common" 6 | "XProxy/cmd/custom" 7 | "XProxy/cmd/dhcp" 8 | "XProxy/cmd/network" 9 | "XProxy/cmd/proxy" 10 | "XProxy/cmd/radvd" 11 | log "github.com/sirupsen/logrus" 12 | "os" 13 | "path" 14 | ) 15 | 16 | type Config struct { 17 | Dev string 18 | DNS []string 19 | IPv4 network.Config 20 | IPv6 network.Config 21 | Proxy proxy.Config 22 | Asset asset.Config 23 | Radvd radvd.Config 24 | Custom custom.Config 25 | DHCP dhcp.Config 26 | } 27 | 28 | func Load(configFile string, config *Config) { 29 | if !common.IsFileExist(configFile) { // configure not exist -> load default 30 | loadDefaultConfig(configFile) 31 | } 32 | raw, err := os.ReadFile(configFile) // read configure content 33 | if err != nil { 34 | log.Panicf("Failed to open %s -> %v", configFile, err) 35 | } 36 | rawConfig := configDecode(raw, path.Ext(configFile)) // decode configure content 37 | decodeDev(&rawConfig, config) 38 | decodeDns(&rawConfig, config) 39 | decodeBypass(&rawConfig, config) 40 | decodeExclude(&rawConfig, config) 41 | decodeIPv4(&rawConfig, config) 42 | decodeIPv6(&rawConfig, config) 43 | decodeProxy(&rawConfig, config) 44 | decodeUpdate(&rawConfig, config) 45 | decodeCustom(&rawConfig, config) 46 | decodeRadvd(&rawConfig, config) 47 | decodeDhcp(&rawConfig, config) 48 | } 49 | -------------------------------------------------------------------------------- /cmd/controller.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "XProxy/cmd/asset" 5 | "XProxy/cmd/common" 6 | "XProxy/cmd/config" 7 | "XProxy/cmd/dhcp" 8 | "XProxy/cmd/network" 9 | "XProxy/cmd/process" 10 | "XProxy/cmd/proxy" 11 | "XProxy/cmd/radvd" 12 | log "github.com/sirupsen/logrus" 13 | "os" 14 | "os/signal" 15 | "path" 16 | "strconv" 17 | "syscall" 18 | "time" 19 | ) 20 | 21 | func runProcess(env []string, command ...string) { 22 | sub := process.New(command...) 23 | sub.Run(true, env) 24 | sub.Daemon() 25 | subProcess = append(subProcess, sub) 26 | } 27 | 28 | func blockWait() { 29 | sigExit := make(chan os.Signal, 1) 30 | signal.Notify(sigExit, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGTERM) // wait until get exit signal 31 | <-sigExit 32 | } 33 | 34 | func loadRadvd(settings *config.Config) { 35 | if settings.Radvd.Enable { 36 | radvd.Load(&settings.Radvd) 37 | } else { 38 | log.Infof("Skip loading radvd") 39 | } 40 | } 41 | 42 | func loadDhcp(settings *config.Config) { 43 | common.CreateFolder(dhcp.WorkDir) 44 | if settings.DHCP.IPv4.Enable || settings.DHCP.IPv6.Enable { 45 | common.CreateFolder(path.Join(exposeDir, "dhcp")) 46 | dhcp.Load(&settings.DHCP) 47 | } 48 | } 49 | 50 | func loadAsset(settings *config.Config) { 51 | if settings.Asset.Disable { 52 | log.Infof("Skip loading asset") 53 | } else { 54 | asset.Load(assetFile, assetDir) 55 | asset.AutoUpdate(&settings.Asset, assetDir) 56 | } 57 | } 58 | 59 | func loadNetwork(settings *config.Config) { 60 | settings.IPv4.RouteTable = v4RouteTable 61 | settings.IPv4.TProxyPort = v4TProxyPort 62 | settings.IPv6.RouteTable = v6RouteTable 63 | settings.IPv6.TProxyPort = v6TProxyPort 64 | network.Load(settings.DNS, settings.Dev, &settings.IPv4, &settings.IPv6) 65 | } 66 | 67 | func loadProxy(settings *config.Config) { 68 | if proxyBin != "" { 69 | settings.Proxy.Bin = proxyBin // setting proxy bin from env 70 | } 71 | settings.Proxy.V4TProxyPort = v4TProxyPort 72 | settings.Proxy.V6TProxyPort = v6TProxyPort 73 | proxy.Load(configDir, exposeDir, &settings.Proxy) 74 | } 75 | 76 | func runProxy(settings *config.Config) { 77 | assetEnv := []string{ 78 | "XRAY_LOCATION_ASSET=" + assetDir, // xray asset folder 79 | "V2RAY_LOCATION_ASSET=" + assetDir, // v2ray / sagray asset folder 80 | } 81 | runProcess(assetEnv, settings.Proxy.Bin, "run", "-confdir", configDir) 82 | } 83 | 84 | func runRadvd(settings *config.Config) { 85 | if settings.Radvd.Enable { 86 | radvdCmd := []string{"radvd", "--nodaemon"} 87 | if settings.Radvd.Log > 0 { // with log option 88 | radvdCmd = append(radvdCmd, "--logmethod", "logfile") 89 | radvdCmd = append(radvdCmd, "--logfile", path.Join(exposeDir, "log/radvd.log")) 90 | radvdCmd = append(radvdCmd, "--debug", strconv.Itoa(settings.Radvd.Log)) 91 | } 92 | runProcess(nil, radvdCmd...) 93 | } else { 94 | log.Infof("Skip running radvd") 95 | } 96 | } 97 | 98 | func runDhcp(settings *config.Config) { 99 | leaseDir := path.Join(exposeDir, "dhcp") 100 | if settings.DHCP.IPv4.Enable { 101 | v4Leases := path.Join(leaseDir, "dhcp4.leases") 102 | v4Config := path.Join(dhcp.WorkDir, "dhcp4.conf") 103 | if !common.IsFileExist(v4Leases) { 104 | common.WriteFile(v4Leases, "", true) 105 | } 106 | runProcess(nil, "dhcpd", "-4", "-f", "-cf", v4Config, "-lf", v4Leases) 107 | time.Sleep(time.Second) // wait 1s for avoid cluttered output 108 | } else { 109 | log.Infof("Skip running DHCPv4") 110 | } 111 | if settings.DHCP.IPv6.Enable { 112 | v6Leases := path.Join(leaseDir, "dhcp6.leases") 113 | v6Config := path.Join(dhcp.WorkDir, "dhcp6.conf") 114 | if !common.IsFileExist(v6Leases) { 115 | common.WriteFile(v6Leases, "", true) 116 | } 117 | runProcess(nil, "dhcpd", "-6", "-f", "-cf", v6Config, "-lf", v6Leases) 118 | time.Sleep(time.Second) // wait 1s for avoid cluttered output 119 | } else { 120 | log.Infof("Skip running DHCPv6") 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /cmd/custom/main.go: -------------------------------------------------------------------------------- 1 | package custom 2 | 3 | import ( 4 | log "github.com/sirupsen/logrus" 5 | "os" 6 | "os/exec" 7 | ) 8 | 9 | type Config struct { 10 | Pre []string `yaml:"pre" json:"pre" toml:"pre"` 11 | Post []string `yaml:"post" json:"post" toml:"post"` 12 | } 13 | 14 | func runScript(command string) { 15 | log.Debugf("Run script -> %s", command) 16 | cmd := exec.Command("sh", "-c", command) 17 | cmd.Stdout = os.Stdout 18 | cmd.Stderr = os.Stderr 19 | err := cmd.Start() 20 | if err != nil { 21 | log.Warningf("Script `%s` working error", command) 22 | } else { 23 | _ = cmd.Wait() 24 | } 25 | } 26 | 27 | func RunPreScript(config *Config) { 28 | for _, script := range config.Pre { 29 | log.Infof("Run pre-script command -> %s", script) 30 | runScript(script) 31 | } 32 | } 33 | 34 | func RunPostScript(config *Config) { 35 | for _, script := range config.Post { 36 | log.Infof("Run post-script command -> %s", script) 37 | runScript(script) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /cmd/dhcp/main.go: -------------------------------------------------------------------------------- 1 | package dhcp 2 | 3 | import ( 4 | "XProxy/cmd/common" 5 | log "github.com/sirupsen/logrus" 6 | "path" 7 | ) 8 | 9 | var WorkDir = "/etc/dhcp" 10 | 11 | type dhcpConfig struct { 12 | Enable bool `yaml:"enable" json:"enable" toml:"enable"` 13 | Configure string `yaml:"config" json:"config" toml:"config"` 14 | } 15 | 16 | type Config struct { 17 | IPv4 dhcpConfig `yaml:"ipv4" json:"ipv4" toml:"ipv4"` 18 | IPv6 dhcpConfig `yaml:"ipv6" json:"ipv6" toml:"ipv6"` 19 | } 20 | 21 | func Load(config *Config) { 22 | if config.IPv4.Enable { 23 | log.Infof("Load DHCPv4 configure") 24 | common.WriteFile(path.Join(WorkDir, "dhcp4.conf"), config.IPv4.Configure, true) 25 | } 26 | if config.IPv6.Enable { 27 | log.Infof("Load DHCPv6 configure") 28 | common.WriteFile(path.Join(WorkDir, "dhcp6.conf"), config.IPv6.Configure, true) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /cmd/network/dns.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | "XProxy/cmd/common" 5 | log "github.com/sirupsen/logrus" 6 | ) 7 | 8 | func loadDns(dns []string) { 9 | if len(dns) == 0 { // without dns server 10 | log.Info("Using system DNS server") 11 | return 12 | } 13 | log.Infof("Setting up DNS server -> %v", dns) 14 | dnsConfig := "" 15 | for _, dnsAddr := range dns { 16 | dnsConfig += "nameserver " + dnsAddr + "\n" 17 | } 18 | common.WriteFile("/etc/resolv.conf", dnsConfig, true) 19 | } 20 | -------------------------------------------------------------------------------- /cmd/network/main.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | "XProxy/cmd/common" 5 | ) 6 | 7 | type Config struct { 8 | RouteTable int 9 | TProxyPort int 10 | Address string 11 | Gateway string 12 | Bypass []string 13 | Exclude []string 14 | } 15 | 16 | var run = common.RunCommand 17 | 18 | func Load(dns []string, dev string, ipv4 *Config, ipv6 *Config) { 19 | loadDns(dns) // init dns server 20 | enableIpForward() 21 | loadNetwork(dev, ipv4, ipv6) 22 | loadV4TProxy(ipv4, getV4Cidr()) 23 | loadV6TProxy(ipv6, getV6Cidr()) 24 | } 25 | -------------------------------------------------------------------------------- /cmd/network/network.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | log "github.com/sirupsen/logrus" 5 | "regexp" 6 | "time" 7 | ) 8 | 9 | func getV4Cidr() []string { // fetch ipv4 network range 10 | var v4Cidr []string 11 | _, output := run("ip", "-4", "addr") 12 | for _, temp := range regexp.MustCompile(`inet (\S+)`).FindAllStringSubmatch(output, -1) { 13 | v4Cidr = append(v4Cidr, temp[1]) 14 | } 15 | return v4Cidr 16 | } 17 | 18 | func getV6Cidr() []string { // fetch ipv6 network range 19 | var v6Cidr []string 20 | _, output := run("ip", "-6", "addr") 21 | for _, temp := range regexp.MustCompile(`inet6 (\S+)`).FindAllStringSubmatch(output, -1) { 22 | v6Cidr = append(v6Cidr, temp[1]) 23 | } 24 | return v6Cidr 25 | } 26 | 27 | func enableIpForward() { // enable ip forward function 28 | log.Info("Enabled IPv4 forward") 29 | run("sysctl", "-w", "net.ipv4.ip_forward=1") 30 | log.Info("Enabled IPv6 forward") 31 | run("sysctl", "-w", "net.ipv6.conf.all.forwarding=1") 32 | } 33 | 34 | func flushNetwork(dev string, isV4 bool, isV6 bool) { // flush ipv4 and ipv6 network 35 | log.Info("Flush system IP configure") 36 | run("ip", "link", "set", dev, "down") 37 | if isV4 { 38 | run("ip", "-4", "addr", "flush", "dev", dev) 39 | } 40 | if isV6 { 41 | run("ip", "-6", "addr", "flush", "dev", dev) 42 | } 43 | run("ip", "link", "set", dev, "up") 44 | } 45 | 46 | func loadV4Network(v4 *Config, dev string) { // setting up ipv4 network 47 | log.Info("Setting up system IPv4 configure") 48 | if v4.Address != "" { 49 | run("ip", "-4", "addr", "add", v4.Address, "dev", dev) 50 | } 51 | if v4.Gateway != "" { 52 | run("ip", "-4", "route", "add", "default", "via", v4.Gateway, "dev", dev) 53 | } 54 | } 55 | 56 | func loadV6Network(v6 *Config, dev string) { // setting up ipv6 network 57 | log.Info("Setting up system IPv6 configure") 58 | if v6.Address != "" { 59 | run("ip", "-6", "addr", "add", v6.Address, "dev", dev) 60 | } 61 | if v6.Gateway != "" { 62 | run("ip", "-6", "route", "add", "default", "via", v6.Gateway, "dev", dev) 63 | } 64 | } 65 | 66 | func loadNetwork(dev string, v4 *Config, v6 *Config) { 67 | setV4 := v4.Address != "" || v4.Gateway != "" 68 | setV6 := v6.Address != "" || v6.Gateway != "" 69 | if setV4 && setV6 { // load both ipv4 and ipv6 70 | flushNetwork(dev, true, true) 71 | loadV4Network(v4, dev) 72 | loadV6Network(v6, dev) 73 | } else if setV4 { // only load ipv4 network 74 | flushNetwork(dev, true, false) 75 | loadV4Network(v4, dev) 76 | } else if setV6 { // only load ipv6 network 77 | flushNetwork(dev, false, true) 78 | loadV6Network(v6, dev) 79 | } else { // skip network settings 80 | log.Infof("Skip system IP configure") 81 | } 82 | if setV6 { 83 | log.Info("Wait 1s for IPv6 setting up") 84 | time.Sleep(time.Second) // wait for ipv6 setting up (ND protocol) -> RA should reply less than 0.5s 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /cmd/network/tproxy.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | log "github.com/sirupsen/logrus" 5 | "strconv" 6 | ) 7 | 8 | func loadV4TProxy(v4 *Config, v4SysCidr []string) { 9 | log.Info("Setting up TProxy of IPv4") 10 | tableNum := strconv.Itoa(v4.RouteTable) 11 | v4Bypass := append(v4SysCidr, v4.Bypass...) 12 | run("ip", "-4", "rule", "add", "fwmark", "1", "table", tableNum) 13 | run("ip", "-4", "route", "add", "local", "0.0.0.0/0", "dev", "lo", "table", tableNum) 14 | run("iptables", "-t", "mangle", "-N", "XPROXY") 15 | log.Infof("Setting up IPv4 bypass CIDR -> %v", v4Bypass) 16 | for _, bypass := range v4Bypass { 17 | run("iptables", "-t", "mangle", "-A", "XPROXY", "-d", bypass, "-j", "RETURN") 18 | } 19 | for _, exclude := range v4.Exclude { 20 | run("iptables", "-t", "mangle", "-A", "XPROXY", "-s", exclude, "-j", "RETURN") 21 | } 22 | run("iptables", "-t", "mangle", "-A", "XPROXY", 23 | "-p", "tcp", "-j", "TPROXY", "--on-port", strconv.Itoa(v4.TProxyPort), "--tproxy-mark", "1") 24 | run("iptables", "-t", "mangle", "-A", "XPROXY", 25 | "-p", "udp", "-j", "TPROXY", "--on-port", strconv.Itoa(v4.TProxyPort), "--tproxy-mark", "1") 26 | run("iptables", "-t", "mangle", "-A", "PREROUTING", "-j", "XPROXY") 27 | } 28 | 29 | func loadV6TProxy(v6 *Config, v6SysCidr []string) { 30 | log.Info("Setting up TProxy of IPv6") 31 | tableNum := strconv.Itoa(v6.RouteTable) 32 | v6Bypass := append(v6SysCidr, v6.Bypass...) 33 | run("ip", "-6", "rule", "add", "fwmark", "1", "table", tableNum) 34 | run("ip", "-6", "route", "add", "local", "::/0", "dev", "lo", "table", tableNum) 35 | run("ip6tables", "-t", "mangle", "-N", "XPROXY6") 36 | log.Infof("Setting up IPv6 bypass CIDR -> %v", v6Bypass) 37 | for _, bypass := range v6Bypass { 38 | run("ip6tables", "-t", "mangle", "-A", "XPROXY6", "-d", bypass, "-j", "RETURN") 39 | } 40 | for _, exclude := range v6.Exclude { 41 | run("ip6tables", "-t", "mangle", "-A", "XPROXY6", "-s", exclude, "-j", "RETURN") 42 | } 43 | run("ip6tables", "-t", "mangle", "-A", "XPROXY6", 44 | "-p", "tcp", "-j", "TPROXY", "--on-port", strconv.Itoa(v6.TProxyPort), "--tproxy-mark", "1") 45 | run("ip6tables", "-t", "mangle", "-A", "XPROXY6", 46 | "-p", "udp", "-j", "TPROXY", "--on-port", strconv.Itoa(v6.TProxyPort), "--tproxy-mark", "1") 47 | run("ip6tables", "-t", "mangle", "-A", "PREROUTING", "-j", "XPROXY6") 48 | } 49 | -------------------------------------------------------------------------------- /cmd/process/daemon.go: -------------------------------------------------------------------------------- 1 | package process 2 | 3 | import ( 4 | log "github.com/sirupsen/logrus" 5 | "time" 6 | ) 7 | 8 | func daemonSub(sub *Process) { 9 | for sub.process.ProcessState == nil { // until process exit 10 | sub.Wait() 11 | } 12 | log.Warningf("Catch process %s exit", sub.name) 13 | time.Sleep(5 * time.Second) // delay 3s -> try to restart 14 | if !exitFlag { 15 | log.Debugf("Process %s restart -> %v", sub.name, sub.command) 16 | sub.Run(true, sub.env) 17 | log.Infof("Process %s restart success", sub.name) 18 | daemonSub(sub) 19 | } 20 | } 21 | 22 | func (p *Process) Daemon() { 23 | if p.process == nil { // process not running 24 | log.Infof("Process %s disabled -> skip daemon", p.name) 25 | return 26 | } 27 | log.Infof("Daemon of process %s start", p.name) 28 | go func() { 29 | daemonSub(p) // start daemon process 30 | log.Infof("Process %s exit daemon mode", p.name) 31 | }() 32 | } 33 | -------------------------------------------------------------------------------- /cmd/process/exit.go: -------------------------------------------------------------------------------- 1 | package process 2 | 3 | import ( 4 | log "github.com/sirupsen/logrus" 5 | "syscall" 6 | ) 7 | 8 | var exitFlag bool 9 | 10 | func Exit(subProcess ...*Process) { 11 | exitFlag = true // setting up exit flag -> exit daemon mode 12 | log.Warningf("Start exit process") 13 | for _, sub := range subProcess { 14 | if sub.process != nil { 15 | log.Infof("Send kill signal to process %s", sub.name) 16 | sub.Signal(syscall.SIGTERM) 17 | } 18 | } 19 | log.Info("Wait all sub process exit") 20 | for _, sub := range subProcess { 21 | if sub.process != nil { 22 | _ = sub.process.Wait() 23 | } 24 | } 25 | log.Infof("Exit complete") 26 | } 27 | -------------------------------------------------------------------------------- /cmd/process/main.go: -------------------------------------------------------------------------------- 1 | package process 2 | 3 | import ( 4 | log "github.com/sirupsen/logrus" 5 | "os" 6 | "os/exec" 7 | "syscall" 8 | ) 9 | 10 | type Process struct { 11 | name string 12 | env []string 13 | command []string 14 | process *exec.Cmd 15 | } 16 | 17 | func New(command ...string) *Process { 18 | process := new(Process) 19 | process.name = command[0] 20 | process.command = command 21 | log.Debugf("New process %s -> %v", process.name, process.command) 22 | return process 23 | } 24 | 25 | func (p *Process) Run(isOutput bool, env []string) { 26 | p.process = exec.Command(p.command[0], p.command[1:]...) 27 | if isOutput { 28 | p.process.Stdout = os.Stdout 29 | p.process.Stderr = os.Stderr 30 | } 31 | p.env = env 32 | if len(p.env) != 0 { 33 | p.process.Env = p.env 34 | log.Infof("Process %s with env -> %v", p.name, p.env) 35 | } 36 | err := p.process.Start() 37 | if err != nil { 38 | log.Errorf("Failed to start %s -> %v", p.name, err) 39 | } 40 | log.Infof("Start process %s -> PID = %d", p.name, p.process.Process.Pid) 41 | } 42 | 43 | func (p *Process) Signal(signal syscall.Signal) { 44 | if p.process != nil { 45 | log.Debugf("Send signal %v to %s", signal, p.name) 46 | _ = p.process.Process.Signal(signal) 47 | } 48 | } 49 | 50 | func (p *Process) Wait() { 51 | if p.process != nil { 52 | err := p.process.Wait() 53 | if err != nil { 54 | log.Warningf("Wait process %s -> %v", p.name, err) 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /cmd/proxy/config.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "XProxy/cmd/common" 5 | log "github.com/sirupsen/logrus" 6 | "path" 7 | ) 8 | 9 | var outboundsConfig = `{ 10 | "outbounds": [ 11 | { 12 | "protocol": "freedom", 13 | "settings": {} 14 | } 15 | ] 16 | }` 17 | 18 | type logObject struct { 19 | Log struct { 20 | Loglevel string `json:"loglevel"` 21 | Access string `json:"access"` 22 | Error string `json:"error"` 23 | } `json:"log"` 24 | } 25 | 26 | type inboundsObject struct { 27 | Inbounds []interface{} `json:"inbounds"` 28 | } 29 | 30 | type sniffObject struct { 31 | Enabled bool `json:"enabled"` 32 | RouteOnly bool `json:"routeOnly"` 33 | DestOverride []string `json:"destOverride"` 34 | DomainsExcluded []string `json:"domainsExcluded"` 35 | } 36 | 37 | type inboundObject struct { 38 | Tag string `json:"tag"` 39 | Port int `json:"port"` 40 | Protocol string `json:"protocol"` 41 | Settings interface{} `json:"settings"` 42 | StreamSettings interface{} `json:"streamSettings"` 43 | Sniffing sniffObject `json:"sniffing"` 44 | } 45 | 46 | func loadLogConfig(logLevel string, logDir string) string { 47 | if logLevel == "" { 48 | logLevel = "warning" // using warning level without log output 49 | } 50 | if logLevel != "debug" && logLevel != "info" && 51 | logLevel != "warning" && logLevel != "error" && logLevel != "none" { 52 | log.Warningf("Unknown log level -> %s", logLevel) 53 | logLevel = "warning" // using `warning` as default 54 | } 55 | logConfig := logObject{} 56 | logConfig.Log.Loglevel = logLevel 57 | logConfig.Log.Access = path.Join(logDir, "access.log") 58 | logConfig.Log.Error = path.Join(logDir, "error.log") 59 | return common.JsonEncode(logConfig) 60 | } 61 | 62 | func loadHttpConfig(tag string, port int, sniff sniffObject) interface{} { 63 | type empty struct{} 64 | return inboundObject{ 65 | Tag: tag, 66 | Port: port, 67 | Protocol: "http", 68 | Settings: empty{}, 69 | StreamSettings: empty{}, 70 | Sniffing: sniff, 71 | } 72 | } 73 | 74 | func loadSocksConfig(tag string, port int, sniff sniffObject) interface{} { 75 | type empty struct{} 76 | type socksObject struct { 77 | UDP bool `json:"udp"` 78 | } 79 | return inboundObject{ 80 | Tag: tag, 81 | Port: port, 82 | Protocol: "socks", 83 | Settings: socksObject{UDP: true}, 84 | StreamSettings: empty{}, 85 | Sniffing: sniff, 86 | } 87 | } 88 | 89 | func loadTProxyConfig(tag string, port int, sniff sniffObject) interface{} { 90 | type tproxyObject struct { 91 | Network string `json:"network"` 92 | FollowRedirect bool `json:"followRedirect"` 93 | } 94 | type tproxyStreamObject struct { 95 | Sockopt struct { 96 | Tproxy string `json:"tproxy"` 97 | } `json:"sockopt"` 98 | } 99 | tproxyStream := tproxyStreamObject{} 100 | tproxyStream.Sockopt.Tproxy = "tproxy" 101 | return inboundObject{ 102 | Tag: tag, 103 | Port: port, 104 | Protocol: "dokodemo-door", 105 | Settings: tproxyObject{ 106 | Network: "tcp,udp", 107 | FollowRedirect: true, 108 | }, 109 | StreamSettings: tproxyStream, 110 | Sniffing: sniff, 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /cmd/proxy/main.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "XProxy/cmd/common" 5 | log "github.com/sirupsen/logrus" 6 | "path" 7 | ) 8 | 9 | type Config struct { 10 | Bin string `yaml:"bin" json:"bin" toml:"bin"` 11 | Log string `yaml:"log" json:"log" toml:"log"` 12 | Http map[string]int `yaml:"http" json:"http" toml:"http"` 13 | Socks map[string]int `yaml:"socks" json:"socks" toml:"socks"` 14 | AddOn []interface{} `yaml:"addon" json:"addon" toml:"addon"` 15 | Sniff struct { 16 | Enable bool `yaml:"enable" json:"enable" toml:"enable"` 17 | Redirect bool `yaml:"redirect" json:"redirect" toml:"redirect"` 18 | Exclude []string `yaml:"exclude" json:"exclude" toml:"exclude"` 19 | } `yaml:"sniff" json:"sniff" toml:"sniff"` 20 | V4TProxyPort int 21 | V6TProxyPort int 22 | } 23 | 24 | func saveConfig(configDir string, caption string, content string, overwrite bool) { 25 | filePath := path.Join(configDir, caption+".json") 26 | common.WriteFile(filePath, content+"\n", overwrite) 27 | } 28 | 29 | func loadInbounds(config *Config) string { 30 | sniff := sniffObject{ 31 | Enabled: config.Sniff.Enable, 32 | RouteOnly: !config.Sniff.Redirect, 33 | DestOverride: []string{"http", "tls", "quic"}, 34 | DomainsExcluded: config.Sniff.Exclude, 35 | } 36 | var inbounds []interface{} 37 | inbounds = append(inbounds, loadTProxyConfig("tproxy4", config.V4TProxyPort, sniff)) 38 | inbounds = append(inbounds, loadTProxyConfig("tproxy6", config.V6TProxyPort, sniff)) 39 | for tag, port := range config.Http { 40 | inbounds = append(inbounds, loadHttpConfig(tag, port, sniff)) 41 | } 42 | for tag, port := range config.Socks { 43 | inbounds = append(inbounds, loadSocksConfig(tag, port, sniff)) 44 | } 45 | for _, addon := range config.AddOn { 46 | inbounds = append(inbounds, addon) 47 | } 48 | return common.JsonEncode(inboundsObject{ 49 | Inbounds: inbounds, 50 | }) 51 | } 52 | 53 | func Load(configDir string, exposeDir string, config *Config) { 54 | common.CreateFolder(path.Join(exposeDir, "log")) 55 | common.CreateFolder(path.Join(exposeDir, "config")) 56 | common.CreateFolder(configDir) 57 | saveConfig(path.Join(exposeDir, "config"), "outbounds", outboundsConfig, false) 58 | saveConfig(configDir, "inbounds", loadInbounds(config), true) 59 | saveConfig(configDir, "log", loadLogConfig(config.Log, path.Join(exposeDir, "log")), true) 60 | for _, configFile := range common.ListFiles(path.Join(exposeDir, "config"), ".json") { 61 | if configFile == "log.json" || configFile == "inbounds" { 62 | log.Warningf("Config file %s will be overridden", configFile) 63 | } 64 | common.CopyFile(path.Join(exposeDir, "config", configFile), path.Join(configDir, configFile)) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /cmd/radvd/radvd.go: -------------------------------------------------------------------------------- 1 | package radvd 2 | 3 | import ( 4 | "XProxy/cmd/common" 5 | log "github.com/sirupsen/logrus" 6 | "strings" 7 | ) 8 | 9 | type Config struct { 10 | Log int `yaml:"log" json:"log" toml:"log"` 11 | Dev string `yaml:"dev" json:"dev" toml:"dev"` 12 | Enable bool `yaml:"enable" json:"enable" toml:"enable"` 13 | Client []string `yaml:"client" json:"client" toml:"client"` 14 | Option map[string]string `yaml:"option" json:"option" toml:"option"` 15 | Route struct { 16 | Cidr string `yaml:"cidr" json:"cidr" toml:"cidr"` 17 | Option map[string]string `yaml:"option" json:"option" toml:"option"` 18 | } `yaml:"route" json:"route" toml:"route"` 19 | Prefix struct { 20 | Cidr string `yaml:"cidr" json:"cidr" toml:"cidr"` 21 | Option map[string]string `yaml:"option" json:"option" toml:"option"` 22 | } `yaml:"prefix" json:"prefix" toml:"prefix"` 23 | DNSSL struct { // DNS Search List 24 | Suffix []string `yaml:"suffix" json:"suffix" toml:"suffix"` 25 | Option map[string]string `yaml:"option" json:"option" toml:"option"` 26 | } `yaml:"dnssl" json:"dnssl" toml:"dnssl"` 27 | RDNSS struct { // Recursive DNS Server 28 | IP []string `yaml:"ip" json:"ip" toml:"ip"` 29 | Option map[string]string `yaml:"option" json:"option" toml:"option"` 30 | } `yaml:"rdnss" json:"rdnss" toml:"rdnss"` 31 | } 32 | 33 | func genSpace(num int) string { 34 | return strings.Repeat(" ", num) 35 | } 36 | 37 | func loadOption(options map[string]string, intend int) string { // load options into radvd config format 38 | var ret string 39 | for option, value := range options { 40 | ret += genSpace(intend) + option + " " + value + ";\n" 41 | } 42 | return ret 43 | } 44 | 45 | func loadClient(clients []string) string { // load radvd client configure 46 | if len(clients) == 0 { 47 | return "" // without client settings 48 | } 49 | ret := genSpace(4) + "clients {\n" 50 | for _, client := range clients { 51 | ret += genSpace(8) + client + ";\n" 52 | } 53 | return ret + genSpace(4) + "};\n" 54 | } 55 | 56 | func loadPrefix(prefix string, option map[string]string) string { // load radvd prefix configure 57 | if prefix == "" { 58 | return "" // without prefix settings 59 | } 60 | header := genSpace(4) + "prefix " + prefix + " {\n" 61 | return header + loadOption(option, 8) + genSpace(4) + "};\n" 62 | } 63 | 64 | func loadRoute(cidr string, option map[string]string) string { // load radvd route configure 65 | if cidr == "" { 66 | return "" // without route settings 67 | } 68 | header := genSpace(4) + "route " + cidr + " {\n" 69 | return header + loadOption(option, 8) + genSpace(4) + "};\n" 70 | } 71 | 72 | func loadRdnss(ip []string, option map[string]string) string { // load radvd RDNSS configure 73 | if len(ip) == 0 { 74 | return "" // without rdnss settings 75 | } 76 | header := genSpace(4) + "RDNSS " + strings.Join(ip, " ") + " {\n" 77 | return header + loadOption(option, 8) + genSpace(4) + "};\n" 78 | } 79 | 80 | func loadDnssl(suffix []string, option map[string]string) string { // load radvd DNSSL configure 81 | if len(suffix) == 0 { 82 | return "" // without dnssl settings 83 | } 84 | header := genSpace(4) + "DNSSL " + strings.Join(suffix, " ") + " {\n" 85 | return header + loadOption(option, 8) + genSpace(4) + "};\n" 86 | } 87 | 88 | func Load(Radvd *Config) { 89 | config := "interface " + Radvd.Dev + " {\n" 90 | config += loadOption(Radvd.Option, 4) 91 | config += loadPrefix(Radvd.Prefix.Cidr, Radvd.Prefix.Option) 92 | config += loadRoute(Radvd.Route.Cidr, Radvd.Route.Option) 93 | config += loadClient(Radvd.Client) 94 | config += loadRdnss(Radvd.RDNSS.IP, Radvd.RDNSS.Option) 95 | config += loadDnssl(Radvd.DNSSL.Suffix, Radvd.DNSSL.Option) 96 | config += "};\n" 97 | log.Debugf("Radvd configure -> \n%s", config) 98 | common.WriteFile("/etc/radvd.conf", config, true) 99 | } 100 | -------------------------------------------------------------------------------- /cmd/xproxy.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "XProxy/cmd/common" 5 | "XProxy/cmd/config" 6 | "XProxy/cmd/custom" 7 | "XProxy/cmd/process" 8 | "flag" 9 | "fmt" 10 | log "github.com/sirupsen/logrus" 11 | "io" 12 | "os" 13 | "path" 14 | "runtime" 15 | "strconv" 16 | ) 17 | 18 | var version = "dev" 19 | var v4RouteTable = 104 20 | var v6RouteTable = 106 21 | var v4TProxyPort = 7288 22 | var v6TProxyPort = 7289 23 | 24 | var proxyBin = "" 25 | var configDir = "/etc/xproxy" 26 | var assetFile = "/assets.tar.xz" 27 | 28 | var subProcess []*process.Process 29 | var assetDir, exposeDir, configFile string 30 | 31 | func logInit(isDebug bool, logDir string) { 32 | log.SetFormatter(&log.TextFormatter{ 33 | ForceColors: true, 34 | FullTimestamp: true, 35 | TimestampFormat: "2006-01-02 15:04:05", 36 | }) 37 | log.SetLevel(log.InfoLevel) // default log level 38 | if isDebug { 39 | log.SetLevel(log.DebugLevel) 40 | } 41 | common.CreateFolder(logDir) // confirm log folder exist 42 | logFile, err := os.OpenFile(path.Join(logDir, "xproxy.log"), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) 43 | if err != nil { 44 | log.Errorf("Unable to open log file -> %s", path.Join(logDir, "xproxy.log")) 45 | } 46 | log.SetOutput(io.MultiWriter(os.Stderr, logFile)) 47 | } 48 | 49 | func xproxyInit() { 50 | xproxyConfig := "xproxy.yml" 51 | if os.Getenv("CONFIG") != "" { 52 | xproxyConfig = os.Getenv("CONFIG") 53 | } 54 | isVersion := flag.Bool("version", false, "Show version") 55 | isDebug := flag.Bool("debug", os.Getenv("DEBUG") == "true", "Enable debug mode") 56 | configName := flag.String("config", xproxyConfig, "Config file name") 57 | flag.Parse() 58 | if *isVersion { // show version info and exit 59 | fmt.Printf("XProxy version %s (%s %s/%s)\n", version, runtime.Version(), runtime.GOOS, runtime.GOARCH) 60 | os.Exit(0) 61 | } 62 | 63 | exposeDir = "/xproxy" // default folder 64 | if os.Getenv("EXPOSE_DIR") != "" { 65 | exposeDir = os.Getenv("EXPOSE_DIR") 66 | } 67 | logInit(*isDebug, path.Join(exposeDir, "log")) 68 | common.CreateFolder(exposeDir) 69 | assetDir = path.Join(exposeDir, "assets") 70 | configFile = path.Join(exposeDir, *configName) 71 | log.Debugf("Expose folder -> %s", exposeDir) 72 | log.Debugf("Assets folder -> %s", assetDir) 73 | log.Debugf("Config file -> %s", configFile) 74 | 75 | if os.Getenv("PROXY_BIN") != "" { 76 | proxyBin = os.Getenv("PROXY_BIN") 77 | } 78 | if os.Getenv("IPV4_TABLE") != "" { 79 | v4RouteTable, _ = strconv.Atoi(os.Getenv("IPV4_TABLE")) 80 | } 81 | if os.Getenv("IPV6_TABLE") != "" { 82 | v6RouteTable, _ = strconv.Atoi(os.Getenv("IPV6_TABLE")) 83 | } 84 | if os.Getenv("IPV4_TPROXY") != "" { 85 | v4TProxyPort, _ = strconv.Atoi(os.Getenv("IPV4_TPROXY")) 86 | } 87 | if os.Getenv("IPV6_TPROXY") != "" { 88 | v6TProxyPort, _ = strconv.Atoi(os.Getenv("IPV6_TPROXY")) 89 | } 90 | log.Debugf("IPv4 Route Table -> %d", v4RouteTable) 91 | log.Debugf("IPv6 Route Table -> %d", v6RouteTable) 92 | log.Debugf("IPv4 TProxy Port -> %d", v4TProxyPort) 93 | log.Debugf("IPv6 TProxy Port -> %d", v6TProxyPort) 94 | } 95 | 96 | func main() { 97 | defer func() { 98 | if err := recover(); err != nil { 99 | log.Errorf("Panic exit -> %v", err) 100 | } 101 | }() 102 | xproxyInit() 103 | var settings config.Config 104 | log.Infof("XProxy %s start (%s %s/%s)", version, runtime.Version(), runtime.GOOS, runtime.GOARCH) 105 | config.Load(configFile, &settings) 106 | loadNetwork(&settings) 107 | loadProxy(&settings) 108 | loadAsset(&settings) 109 | loadRadvd(&settings) 110 | loadDhcp(&settings) 111 | custom.RunPreScript(&settings.Custom) 112 | runDhcp(&settings) 113 | runRadvd(&settings) 114 | runProxy(&settings) 115 | blockWait() 116 | process.Exit(subProcess...) 117 | custom.RunPostScript(&settings.Custom) 118 | log.Warningf("All done, goodbye!") 119 | } 120 | -------------------------------------------------------------------------------- /docs/campus_network_cracking.md: -------------------------------------------------------------------------------- 1 | # 使用 XProxy 绕过校园网认证登录 2 | 3 | 部分校园网在登录认证时需要 DNS 解析,因而在防火墙上允许 `TCP/53` 或 `UDP/53` 端口通行,借助这个漏洞,可将内网流量用 XProxy 代理并转发到公网服务器上,实现免认证、无限速的上网。 4 | 5 | 以下为一般情况下的网络拓扑: 6 | 7 | ![Network](./img/campus_network.png) 8 | 9 | 为了方便讲解,我们假设以下典型情况: 10 | 11 | + 校园网交换机无 IPv6 支持,同时存在 QoS; 12 | 13 | + 无认证时允许 53 端口通行,ICMP 流量无法通过; 14 | 15 | + 使用三台公网服务器负载均衡,其 53 端口上运行有代理服务; 16 | 17 | + 三台服务器只有一台支持 IPv4 与 IPv6 双栈,其余只支持 IPv4; 18 | 19 | ## 代理协议 20 | 21 | 从部署成本与便捷性方面考虑,socks 类代理是最合适的工具:无需修改服务器网卡路由表等配置,方便多级负载均衡,软件只在用户态运行,实测速度也相对 `IPSec` 、`L2TP` 等协议更有优势;但 socks 代理只接收 TCP 与 UDP 流量,ICMP 流量无法被直接代理(例如 PING 命令),不过大多数情况下我们不会用到公网 ICMP 流量,如果确实需要也可以曲线救国给它补上。 22 | 23 | 在选定代理类型后,我们需要考虑具体的传输方式,由于存在 QoS 问题,这里应该倾向于选择基于 TCP 的代理方式,同时为了避免校园网的流量审查,我们应该将流量加密传输。考虑到软路由性能一般较差,而自建的代理服务器无需考虑协议兼容性问题,这里更建议选择基于 XTLS 的传输方式,它避开了对 TLS 流量的二次加密,可以显著降低代理 https 流量时的性能开销,提升性能上限;至于延迟方面的问题,如果选择 `gRPC` 等协议,虽然有 0-rtt 握手的延迟优势,但这种场景下延迟一般不高(甚至服务器可以直接部署在校内),用微弱的延迟优势换取性能开销并不值得,且前者也可以开启 mux 多路复用来优化延迟。 24 | 25 | 既然我们已经选择 XTLS 方式,那使用轻量的无加密类型(在加密的 XTLS 隧道里传输)是当前网络的最优解,譬如 VLESS 或者 Trojan 协议,下面将用 VLESS + XTLS 代理进行配置演示;当然,具体的选择还是取决于您的实际应用场景,只要按需调整 XProxy 的配置文件即可。 26 | 27 | ## 初始化配置 28 | 29 | > 分配 `192.168.2.0/24` 和 `fc00::/64` 给内网使用。 30 | 31 | 路由器 WAN 口接入学校交换机,构建一个 NAT 转换,代理流量在路由器转发后送到公网服务器的 53 端口上;假设内网中路由器地址为 `192.168.2.1` ,配置虚拟网关 IPv4 地址为 `192.168.2.2` ,IPv6 地址为 `fc00::2` ;在网关中,无论 IPv4 还是 IPv6 流量都会被透明代理,由于校园网无 IPv6 支持,数据被封装后只通过 IPv4 网络发送,代理服务器接收以后再将其解开,对于 IPv6 流量,这里相当于一个 `6to4` 隧道。 32 | 33 | ```bash 34 | # 宿主机网卡假定为 eth0 35 | $ ip link set eth0 promisc on 36 | $ modprobe ip6table_filter 37 | $ docker network create -d macvlan \ 38 | --subnet=192.168.2.0/24 \ # 此处指定的参数为容器的默认网络配置 39 | --gateway=192.168.2.1 \ 40 | --subnet=fc00::/64 \ 41 | --gateway=fc00::1 \ 42 | --ipv6 -o parent=eth0 macvlan 43 | ``` 44 | 45 | 我们将配置文件保存在 `/etc/scutweb` 目录下,使用以下命令开启 XProxy 服务: 46 | 47 | ```bash 48 | docker run --restart always \ 49 | --privileged --network macvlan -dt \ 50 | --name scutweb --hostname scutweb \ 51 | --volume /etc/scutweb/:/xproxy/ \ 52 | --volume /etc/timezone:/etc/timezone:ro \ 53 | --volume /etc/localtime:/etc/localtime:ro \ 54 | dnomd343/xproxy:latest 55 | ``` 56 | 57 | ## 参数配置 58 | 59 | 我们将三台服务器分别称为 `nodeA` ,`nodeB` 与 `nodeC` ,其中只有 `nodeC` 支持IPv6网络;此外,我们在内网分别暴露 3 个 socks5 端口,分别用于检测服务器的可用性。 60 | 61 | 由于校园网无 IPv6 支持,这里 IPv6 上游网关可以不填写;虚拟网关对内网发布 RA 通告,让内网设备使用 SLAAC 配置网络地址,同时将其作为 IPv6 网关;此外,如果路由器开启了 DHCP 服务,需要将默认网关改为 `192.168.2.2` ,也可以启用 XProxy 自带的 DHCPv4 服务。 62 | 63 | 最后,由于我们代理全部流量,无需根据域名或者 IP 进行任何分流,因此路由资源自动更新部分可以省略。 64 | 65 | 修改 `xproxy.yml` ,写入以下配置: 66 | 67 | ```yaml 68 | proxy: 69 | log: warning 70 | socks: 71 | nodeA: 1081 72 | nodeB: 1082 73 | nodeC: 1083 74 | 75 | network: 76 | dev: eth0 77 | dns: 78 | - 192.168.2.1 79 | ipv4: 80 | gateway: 192.168.2.1 81 | address: 192.168.2.2/24 82 | ipv6: 83 | gateway: null 84 | address: fc00::2/64 85 | bypass: 86 | - 169.254.0.0/16 87 | - 224.0.0.0/3 88 | - fc00::/7 89 | - fe80::/10 90 | - ff00::/8 91 | 92 | radvd: 93 | log: 5 94 | dev: eth0 95 | enable: true 96 | option: 97 | AdvSendAdvert: on 98 | prefix: 99 | cidr: fc00::/64 100 | 101 | custom: 102 | pre: 103 | - "iptables -t nat -N FAKE_PING" 104 | - "iptables -t nat -A FAKE_PING -j DNAT --to-destination 192.168.2.2" 105 | - "iptables -t nat -A PREROUTING -i eth0 -p icmp -j FAKE_PING" 106 | - "ip6tables -t nat -N FAKE_PING" 107 | - "ip6tables -t nat -A FAKE_PING -j DNAT --to-destination fc00::2" 108 | - "ip6tables -t nat -A PREROUTING -i eth0 -p icmp -j FAKE_PING" 109 | ``` 110 | 111 | 在开始代理前,我们使用 `custom` 注入了一段脚本配置:由于这里我们只代理 TCP 与 UDP 流量,ICMP 数据包不走代理,内网设备 ping 外网时会一直无响应,加入这段脚本可以创建一个 NAT,假冒远程主机返回成功回复,但实际上 ICMP 数据包并未实际到达,效果上表现为 ping 成功且延迟为内网访问时间。 112 | 113 | > 这段脚本并无实质作用,仅用于演示 `custom` 功能。 114 | 115 | ## 代理配置 116 | 117 | 接下来,我们应该配置出站代理,修改 `config/outbounds.json` 文件,填入公网代理服务器参数: 118 | 119 | ```json 120 | { 121 | "outbounds": [ 122 | { 123 | "tag": "nodeA", 124 | "...": "..." 125 | }, 126 | { 127 | "tag": "nodeB", 128 | "...": "..." 129 | }, 130 | { 131 | "tag": "nodeC", 132 | "...": "..." 133 | } 134 | ] 135 | } 136 | ``` 137 | 138 | 接着配置路由部分,让暴露的三个 socks5 接口对接到三台服务器上,并分别配置 IPv4 与 IPv6 的负载均衡;路由核心在这里接管所有流量,IPv4 流量应将随机转发到三台服务器,而 IPv6 流量只送往 `nodeC` 服务器;创建 `config/routing.json` 文件,写入以下配置: 139 | 140 | ```json 141 | { 142 | "routing": { 143 | "domainStrategy": "AsIs", 144 | "rules": [ 145 | { 146 | "type": "field", 147 | "inboundTag": ["nodeA"], 148 | "outboundTag": "nodeA" 149 | }, 150 | { 151 | "type": "field", 152 | "inboundTag": ["nodeB"], 153 | "outboundTag": "nodeB" 154 | }, 155 | { 156 | "type": "field", 157 | "inboundTag": ["nodeC"], 158 | "outboundTag": "nodeC" 159 | }, 160 | { 161 | "type": "field", 162 | "ip": ["0.0.0.0/0"], 163 | "balancerTag": "ipv4" 164 | }, 165 | { 166 | "type": "field", 167 | "ip": ["::/0"], 168 | "balancerTag": "ipv6" 169 | } 170 | ], 171 | "balancers": [ 172 | { 173 | "tag": "ipv4", 174 | "selector": [ "nodeA", "nodeB", "nodeC" ] 175 | }, 176 | { 177 | "tag": "ipv6", 178 | "selector": [ "nodeC" ] 179 | } 180 | ] 181 | } 182 | } 183 | ``` 184 | 185 | 重启 XProxy 容器使配置生效: 186 | 187 | ```bash 188 | docker restart scutweb 189 | ``` 190 | 191 | 最后,验证代理服务是否正常工作,若出现问题可以查看 `/etc/scutweb/log` 文件夹下的日志,定位错误原因。 192 | 193 | ## 代理 ICMP 流量 194 | 195 | > 这一步仅用于修复 ICMP 代理,无此需求可以忽略。 196 | 197 | 由于 socks5 代理服务不支持 ICMP 协议,当前搭建的网络只有 TCP 与 UDP 发往外网,即使在上文我们注入了一段命令用于劫持 PING 流量,但是返回的仅仅是虚假结果,并没有实际意义;所以如果对这个缺陷不满,您可以考虑使用以下方法修复这个问题。 198 | 199 | 为了代理 ICMP 流量,我们必须选择网络层的 VPN 工具,从简单轻量可用方面考虑,`WireGuard` 比较适合当前应用场景:TCP 与 UDP 流量走 VLESS + XTLS 代理,ICMP 流量进入 WireGuard ,而 WireGuard 本身使用 UDP 协议传输,这些数据包通过 Xray 隧道再次代理送到远端服务器,解开后将 ICMP 流量送至公网;这种方式虽然略显繁杂,但实际场景中 ICMP 流量很少且数据包不大,并不存在性能问题。 200 | 201 | 具体实现上,我们需要在容器中安装 WireGuard 工具包,然后在 XProxy 中配置启动注入脚本,开启 WireGuard 对 ICMP 流量的代理。 202 | 203 | ### 1. 拉取 WireGuard 安装包 204 | 205 | XProxy 容器默认不自带 WireGuard 功能,需要额外安装 `wireguard-tools` 包,您可以在原有镜像上添加一层,或是使用以下方式安装离线包。 206 | 207 | > 以下代码用于生成 Alpine 的 WireGuard 安装脚本,您也可以选择手动拉取 apk 安装包 208 | 209 | ```python 210 | #!/usr/bin/env python3 211 | # -*- coding: utf-8 -*- 212 | 213 | Alpine = '3.17' 214 | 215 | import os, re, sys 216 | 217 | pkgName = sys.argv[1] 218 | workDir = os.path.join(os.path.dirname(os.path.realpath(__file__)), pkgName) 219 | 220 | rawOutput = os.popen(' '.join([ 221 | 'docker run --rm -w /tmp -v %s:/tmp' % workDir, 222 | 'alpine:%s' % Alpine, 'sh -c "apk update && apk fetch -R %s"' % pkgName 223 | ])).read() 224 | 225 | print("%s\n%s%s" % ('=' * 88, rawOutput, '=' * 88), file = sys.stderr) 226 | 227 | with open(os.path.join(workDir, 'setup'), 'w') as script: 228 | setupCmd = '#!/usr/bin/env sh\ncd "$(dirname "$0")" && apk add --no-network --quiet ' 229 | setupCmd += ' '.join([ 230 | '%s.apk' % x for x in re.findall(r'Downloading (\S+)', rawOutput) 231 | ]) 232 | script.write(setupCmd) 233 | 234 | os.system('chmod +x %s' % os.path.join(workDir, 'setup')) 235 | ``` 236 | 237 | ```bash 238 | # fetch.py 为上述脚本 239 | $ cd /etc/scutweb 240 | $ mkdir -p ./toolset && cd ./toolset 241 | $ python3 fetch.py wireguard-tools # 拉取wireguard-tools依赖 242 | ··· 243 | ``` 244 | 245 | 拉取成功后将生成 `wireguard-tools` 文件夹,包含多个依赖的 `.apk` 安装包与 `setup` 安装脚本。 246 | 247 | ### 2. 写入 WireGuard 配置文件 248 | 249 | 一个典型的客户端配置文件如下: 250 | 251 | ```ini 252 | [Interface] 253 | PrivateKey = 客户端私钥 254 | 255 | [Peer] 256 | PublicKey = 服务端公钥 257 | Endpoint = 服务器IP:端口 258 | AllowedIPs = 0.0.0.0/0 259 | ``` 260 | 261 | 将其保存至 `/etc/scutweb/config/wg.conf` 262 | 263 | ### 3. 容器注入 WireGuard 服务 264 | 265 | WireGuard 在这里使用 `192.168.1.0/24` 的 VPN 网段,客户端 IP 地址为 `192.168.1.2`,注意服务端应允许 `192.168.2.2/24` 网段,否则必须在容器中多做一层 NAT 才能代理。 266 | 267 | 此外,XProxy 默认没有加入网关自身的代理,只在 `PREROUTING` 链上劫持流量,因此这里需要修改 `OUTPUT` 链,让 WireGuard 的流量被 XProxy 代理;将发往 WireGurad 服务器的流量打上标志 `0x1`,该数据包就会被重新路由到 `PREROUTING` 链上(netfilter 特性),从而进行透明代理。 268 | 269 | ```yaml 270 | custom: 271 | pre: 272 | - /xproxy/toolset/wireguard-tools/setup # 安装离线包 273 | - ip link add wg0 type wireguard 274 | - wg setconf wg0 /xproxy/config/wg.conf # 加载配置文件 275 | - ip addr add 192.168.1.2/24 dev wg0 # 添加本机WireGuard地址 276 | - ip link set mtu 1420 up dev wg0 # 启动VPN服务 277 | - ip rule add fwmark 51820 table 51820 278 | - ip route add 0.0.0.0/0 dev wg0 table 51820 # WireGuard路由表 279 | - iptables -t mangle -N WGPROXY 280 | - iptables -t mangle -A WGPROXY -d 127.0.0.0/8 -j RETURN 281 | - iptables -t mangle -A WGPROXY -d 192.168.2.0/24 -j RETURN 282 | - iptables -t mangle -A WGPROXY -d 169.254.0.0/16 -j RETURN 283 | - iptables -t mangle -A WGPROXY -d 224.0.0.0/3 -j RETURN 284 | - iptables -t mangle -A WGPROXY -p icmp -j MARK --set-mark 51820 # ICMP流量送至WireGuard路由表 285 | - iptables -t mangle -A PREROUTING -j WGPROXY 286 | - iptables -t mangle -A OUTPUT -p udp -d 服务器IP –-dport 服务器端口 -j MARK --set-mark 1 # 重定向到PREROUTING 287 | ``` 288 | 289 | 配置完成后,重启 XProxy 容器生效,在内网设备上执行 PING 命令,如果返回正常延迟则配置成功。 290 | -------------------------------------------------------------------------------- /docs/dual_stack_network_proxy.md: -------------------------------------------------------------------------------- 1 | # 家庭网络的 IPv4 与 IPv6 透明代理 2 | 3 | 家庭网络光纤入网,支持 IPv4 与 IPv6 网络,需要在内网搭建透明代理,让设备的国内流量直连,出境流量转发到代理服务器上,避开 GFW 的流量审查。 4 | 5 | 以下为典型网络拓扑: 6 | 7 | ![Network](./img/chinese_network.png) 8 | 9 | > 此处网络拓扑仅为讲解使用,实际使用时可以让光猫桥接减少性能浪费,不过目前大部分新版光猫不存在性能瓶颈,千兆级别下基本没有压力。 10 | 11 | 正常情况下,大部分家庭宽带为:光猫对接上游网络,使用宽带拨号获取运营商分配的 IPv4 地址与 IPv6 前缀,在 LAN 侧提供网络服务,其中 IPv4 为 NAT 方式,IPv6 发布 RA 广播,同时运行 DHCPv6 服务;路由器在 IPv4 上 NAT ,在 IPv6 上桥接,内网设备统一接入路由器。 12 | 13 | 大多数地区的运营商不会提供 IPv4 公网地址,IPv6 分配一般为 64 位长度的公网网段;虚拟网关在这里需要收集内网的所有 IPv4 与 IPv6 流量,将国内流量直接送出,国外流量发往代理服务器;为了增加难度,我们假设有两台境外代理服务器,一台支持IPv6,一台只支持IPv4,我们需要将IPv6代理流量发送给前者,其余代理流量送往后者。 14 | 15 | ## 分流规则 16 | 17 | 代理内核需要区分出哪些流量可以直连,哪些流量需要送往代理服务器,为了更准确地分流,这里需要开启嗅探功能,获取访问的域名信息,同时允许流量重定向(目标地址修改为域名,送至代理服务器解析,避开 DNS 污染)。 18 | 19 | 目前路由资源中包含了一份国内常见域名列表(即 `geosite.dat` ,XProxy 已集成),如果嗅探后的域名在其中,那可以直接判定为直连流量,但是对于其他流量,即使它不在列表内,但仍可能是国内服务,我们不能直接将它送往代理服务器;因此下一步我们需要引出分流的核心规则,它取决于 DNS 污染的一个特性:受污染的域名返回解析必然为境外 IP ,基于这个原则,我们将嗅探到的域名使用国内 DNS 进行一次解析,如果结果是国内 IP 地址,那就直连该流量,否则发往代理服务器,IPv4 与 IPv6 均使用该逻辑分流。 20 | 21 | 如果有可能的话,您可以在内网搭建一个无污染的解析服务,比如 [ClearDNS](https://github.com/dnomd343/ClearDNS),它的作用在于消除 DNS 污染,准确地给出国内外的解析地址,这样子可以在分流时就不用多做一次 DNS 解析,减少这一步导致的延迟(DNS 流量通过代理送出,远程解析以后再返回,其耗时较长且不稳定),无污染 DNS 可以更快更准确地进行分流。 22 | 23 | ## 网络配置 24 | 25 | 网络地址方面,内网 IPv4 段由我们自己决定,这一部分取决于路由器设置的 LAN 侧 IP 段,我们假设为 `192.168.2.0/24` ,其中路由器地址为 `192.168.2.1` ,虚拟网关分配为 `192.168.2.2` ,由于 IPv4 部分由路由器 NAT 隔离,这里不需要修改光猫配置;虚拟网关上游配置为路由器地址,修改内网 DHCP 服务,让网关指向 `192.168.2.2` 。 26 | 27 | IPv6部分,由于路由器桥接,地址分配等操作均为光猫负责,它拥有一个链路本地地址,在 LAN 侧向内网发送 RA 广播,一些光猫还会开启 DHCPv6 服务,为内网分配 DNS 等选项;RA 通告发布的 IPv6 前缀一般为运营商分配的 64 位长度地址,内网所有设备将获取到一个独立的 IPv6 地址(部分地区也有做 NAT6 的,具体取决于运营商),我们要做的就是将这部分工作转移给虚拟网关来完成。 28 | 29 | 在开始之前,我们需要先拿到光猫分配的 IPv6 前缀与网关(即光猫的链路地址),由于光猫默认会发布 RA 广播,你可以直接从当前接入设备上获取这些信息,也可以登录光猫管理页面查看(登录账号与密码一般会印在光猫背面);这里假设运营商分配的 IPv6 网段为 `2409:8a55:e2a7:3a0::/64` ,光猫地址为 `fe80::1`(绝大多数光猫都使用这个链路地址),虚拟网关的上游应该配置为光猫链路地址,而自身地址可以在分配的 IPv6 网段中任意选择,方便起见,我们这里配置为 `2409:8a55:e2a7:3a0::` 。 30 | 31 | 虚拟网关需要对内网发布 RA 通告,广播 `2409:8a55:e2a7:3a0::/64` 这段地址,接收到这段信息的设备会将虚拟网关作为公网 IPv6 的下一跳地址(即网关链路地址);但是这种情况下,不应该存在多个 RA 广播源同时运行,所以需要关闭光猫的 RA 广播功能,如果不需要 DHCPv6 功能,这里也可以一并关闭;这一步在部分光猫上需要超级管理员权限,一般情况下,你可以在网络上搜索到不同型号光猫的默认超级管理员账号密码,如果无法成功,可以联系宽带师傅帮忙登入。 32 | 33 | 三大运营商的光猫,超级管理员默认账号密码: 34 | 35 | + 移动 :`CMCCAdmin` ,`aDm8H%MdA` 36 | 37 | + 电信 :`telecomadmin` ,`nE7jA%5m` 38 | 39 | + 联通 :`CUAdmin` ,`CUAdmin` 40 | 41 | 这是 IPv6 在代理方面的缺点,它将发送 RA 广播的链路地址直接视为路由网关,且该地址无法通过其他协议更改,我们没法像 DHCPv4 一样直接配置网关地址,这在透明代理时远没有 IPv4 方便,只能将 RA 广播源放在网关上。 42 | 43 | ## 启动服务 44 | 45 | 首先创建 macvlan 网络: 46 | 47 | ```bash 48 | # 宿主机网卡假定为 eth0 49 | $ ip link set eth0 promisc on 50 | $ modprobe ip6table_filter 51 | # IPv6网段后续由XProxy更改,这里可以随意指定 52 | $ docker network create -d macvlan --subnet=fe80::/10 --ipv6 -o parent=eth0 macvlan 53 | ``` 54 | 55 | 将配置文件保存在 `/etc/route` 目录下,使用以下命令开启 XProxy 服务: 56 | 57 | ```bash 58 | docker run --restart always \ 59 | --privileged --network macvlan -dt \ 60 | --name route --hostname route \ 61 | --volume /etc/route/:/xproxy/ \ 62 | --volume /etc/timezone:/etc/timezone:ro \ 63 | --volume /etc/localtime:/etc/localtime:ro \ 64 | dnomd343/xproxy:latest 65 | ``` 66 | 67 | ## 参数配置 68 | 69 | 在设计上,应该配置四个出口,分别为 IPv4 直连、IPv4 代理、IPv6 直连、IPv6 代理,这里创建 4 个对应的 socks5 接口 `direct4` 、`proxy4` 、`direct6` 、`proxy6` ,用于检测对应出口是否正常工作。 70 | 71 | 此外,我们需要判断 IP 与域名的地理信息,而该数据库一直变动,需要持续更新;由于该项目的 Github Action 配置为 UTC 22:00 触发,即 UTC8+ 的 06:00 ,所以这里配置为每天早上 06 点 05 分更新,延迟 5 分钟拉取当日的新版本路由资源。 72 | 73 | 修改 `xproxy.yml` ,写入以下配置: 74 | 75 | ```yaml 76 | proxy: 77 | log: info 78 | socks: 79 | proxy4: 1094 80 | direct4: 1084 81 | proxy6: 1096 82 | direct6: 1086 83 | sniff: 84 | enable: true 85 | redirect: true 86 | 87 | network: 88 | dev: eth0 89 | dns: 90 | - 192.168.2.1 91 | ipv4: 92 | gateway: 192.168.2.1 93 | address: 192.168.2.2/24 94 | ipv6: 95 | gateway: fe80::1 96 | address: 2409:8a55:e2a7:3a0::/64 97 | bypass: 98 | - 169.254.0.0/16 99 | - 224.0.0.0/3 100 | - fc00::/7 101 | - fe80::/10 102 | - ff00::/8 103 | 104 | radvd: 105 | log: 3 106 | dev: eth0 107 | enable: true 108 | option: 109 | AdvSendAdvert: on 110 | AdvManagedFlag: off 111 | AdvOtherConfigFlag: off 112 | prefix: 113 | cidr: 2409:8a55:e2a7:3a0::/64 114 | 115 | asset: 116 | update: 117 | cron: "0 5 6 * * *" 118 | proxy: "socks5://192.168.2.2:1094" # 通过代理下载 Github 文件 119 | url: 120 | geoip.dat: "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat" 121 | geosite.dat: "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat" 122 | ``` 123 | 124 | ## 代理配置 125 | 126 | 配置出站代理,修改 `config/outbounds.json` 文件,其中 direct 直连到国内网络,proxy 填入代理服务器参数: 127 | 128 | ```json 129 | { 130 | "outbounds": [ 131 | { 132 | "tag": "direct4", 133 | "protocol": "freedom", 134 | "settings": { 135 | "domainStrategy": "UseIPv4" 136 | } 137 | }, 138 | { 139 | "tag": "direct6", 140 | "protocol": "freedom", 141 | "settings": { 142 | "domainStrategy": "UseIPv6" 143 | } 144 | }, 145 | { 146 | "tag": "proxy4", 147 | "...": "..." 148 | }, 149 | { 150 | "tag": "proxy6", 151 | "...": "..." 152 | } 153 | ] 154 | } 155 | ``` 156 | 157 | 接着配置路由部分,让暴露的 4 个 socks5 接口对接上,并依据上文的分流方式编写路由规则;创建 `config/routing.json` 文件,写入以下配置: 158 | 159 | ```json 160 | { 161 | "routing": { 162 | "domainStrategy": "IPOnDemand", 163 | "rules": [ 164 | { 165 | "type": "field", 166 | "inboundTag": ["direct4"], 167 | "outboundTag": "direct4" 168 | }, 169 | { 170 | "type": "field", 171 | "inboundTag": ["direct6"], 172 | "outboundTag": "direct6" 173 | }, 174 | { 175 | "type": "field", 176 | "inboundTag": ["proxy4"], 177 | "outboundTag": "proxy4" 178 | }, 179 | { 180 | "type": "field", 181 | "inboundTag": ["proxy6"], 182 | "outboundTag": "proxy6" 183 | }, 184 | { 185 | "type": "field", 186 | "inboundTag": ["tproxy4"], 187 | "domain": ["geosite:cn"], 188 | "outboundTag": "direct4" 189 | }, 190 | { 191 | "type": "field", 192 | "inboundTag": ["tproxy6"], 193 | "domain": ["geosite:cn"], 194 | "outboundTag": "direct6" 195 | }, 196 | { 197 | "type": "field", 198 | "inboundTag": ["tproxy4"], 199 | "ip": [ 200 | "geoip:cn", 201 | "geoip:private" 202 | ], 203 | "outboundTag": "direct4" 204 | }, 205 | { 206 | "type": "field", 207 | "inboundTag": ["tproxy6"], 208 | "ip": [ 209 | "geoip:cn", 210 | "geoip:private" 211 | ], 212 | "outboundTag": "direct6" 213 | }, 214 | { 215 | "type": "field", 216 | "inboundTag": ["tproxy4"], 217 | "outboundTag": "proxy4" 218 | }, 219 | { 220 | "type": "field", 221 | "inboundTag": ["tproxy6"], 222 | "outboundTag": "proxy6" 223 | } 224 | ] 225 | } 226 | } 227 | ``` 228 | 229 | 重启 XProxy 容器使配置生效: 230 | 231 | ```bash 232 | docker restart route 233 | ``` 234 | 235 | 最后,验证代理服务是否正常工作,若出现问题可以查看 `/etc/route/log` 文件夹下的日志,定位错误原因。 236 | -------------------------------------------------------------------------------- /docs/img/campus_network.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnomd343/XProxy/d3612e95463e8db2b1dda999b4cc7a0a787acc72/docs/img/campus_network.png -------------------------------------------------------------------------------- /docs/img/chinese_network.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnomd343/XProxy/d3612e95463e8db2b1dda999b4cc7a0a787acc72/docs/img/chinese_network.png -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module XProxy 2 | 3 | go 1.24.2 4 | 5 | require ( 6 | github.com/BurntSushi/toml v1.5.0 7 | github.com/andybalholm/brotli v1.1.1 8 | github.com/go-http-utils/headers v0.0.0-20181008091004-fed159eddc2a 9 | github.com/klauspost/compress v1.18.0 10 | github.com/robfig/cron v1.2.0 11 | github.com/sirupsen/logrus v1.9.3 12 | gopkg.in/yaml.v3 v3.0.1 13 | ) 14 | 15 | require golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect 16 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= 2 | github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= 3 | github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= 4 | github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= 5 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 7 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/go-http-utils/headers v0.0.0-20181008091004-fed159eddc2a h1:v6zMvHuY9yue4+QkG/HQ/W67wvtQmWJ4SDo9aK/GIno= 9 | github.com/go-http-utils/headers v0.0.0-20181008091004-fed159eddc2a/go.mod h1:I79BieaU4fxrw4LMXby6q5OS9XnoR9UIKLOzDFjUmuw= 10 | github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= 11 | github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 12 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 13 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 14 | github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ= 15 | github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k= 16 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 17 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 18 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 19 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 20 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 21 | github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= 22 | github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= 23 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= 24 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 25 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 26 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 27 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 28 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 29 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 30 | --------------------------------------------------------------------------------