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 代理并转发到公网服务器上,实现免认证、无限速的上网。
以下为一般情况下的网络拓扑:

为了方便讲解,我们假设以下典型情况:
+ 校园网交换机无 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 的流量审查。
以下为典型网络拓扑:

> 此处网络拓扑仅为讲解使用,实际使用时可以让光猫桥接减少性能浪费,不过目前大部分新版光猫不存在性能瓶颈,千兆级别下基本没有压力。
正常情况下,大部分家庭宽带为:光猫对接上游网络,使用宽带拨号获取运营商分配的 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=
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
SYMBOL INDEX (98 symbols across 21 files)
FILE: cmd/asset/asset.go
function extractFile (line 9) | func extractFile(archive string, geoFile string, targetDir string) { // ...
function Load (line 19) | func Load(assetFile string, assetDir string) {
FILE: cmd/asset/update.go
type Config (line 13) | type Config struct
function updateAsset (line 22) | func updateAsset(urls map[string]string, assetDir string, updateProxy st...
function AutoUpdate (line 40) | func AutoUpdate(config *Config, assetDir string) { // set cron task for ...
FILE: cmd/common/file.go
function CreateFolder (line 17) | func CreateFolder(folderPath string) {
function IsFileExist (line 28) | func IsFileExist(filePath string) bool {
function WriteFile (line 36) | func WriteFile(filePath string, content string, overwrite bool) {
function ListFiles (line 47) | func ListFiles(folderPath string, suffix string) []string {
function CopyFile (line 61) | func CopyFile(source string, target string) {
function DownloadBytes (line 81) | func DownloadBytes(fileUrl string, proxyUrl string) ([]byte, error) {
function DownloadFile (line 132) | func DownloadFile(fileUrl string, filePath string, proxyUrl string) bool {
FILE: cmd/common/func.go
function isIP (line 12) | func isIP(ipAddr string, isCidr bool) bool {
function IsIPv4 (line 20) | func IsIPv4(ipAddr string, isCidr bool) bool {
function IsIPv6 (line 24) | func IsIPv6(ipAddr string, isCidr bool) bool {
function JsonEncode (line 28) | func JsonEncode(raw interface{}) string {
function RunCommand (line 33) | func RunCommand(command ...string) (int, string) {
FILE: cmd/config/decode.go
type NetConfig (line 17) | type NetConfig struct
type RawConfig (line 22) | type RawConfig struct
function configDecode (line 38) | func configDecode(raw []byte, fileSuffix string) RawConfig {
function decodeDev (line 58) | func decodeDev(rawConfig *RawConfig, config *Config) {
function decodeDns (line 70) | func decodeDns(rawConfig *RawConfig, config *Config) {
function decodeBypass (line 81) | func decodeBypass(rawConfig *RawConfig, config *Config) {
function decodeExclude (line 95) | func decodeExclude(rawConfig *RawConfig, config *Config) {
function decodeIPv4 (line 109) | func decodeIPv4(rawConfig *RawConfig, config *Config) {
function decodeIPv6 (line 121) | func decodeIPv6(rawConfig *RawConfig, config *Config) {
function decodeProxy (line 133) | func decodeProxy(rawConfig *RawConfig, config *Config) {
function decodeRadvd (line 148) | func decodeRadvd(rawConfig *RawConfig, config *Config) {
function decodeDhcp (line 164) | func decodeDhcp(rawConfig *RawConfig, config *Config) {
function decodeUpdate (line 172) | func decodeUpdate(rawConfig *RawConfig, config *Config) {
function decodeCustom (line 186) | func decodeCustom(rawConfig *RawConfig, config *Config) {
FILE: cmd/config/default.go
function toJSON (line 37) | func toJSON(config interface{}) string { // convert to JSON string
function toYAML (line 42) | func toYAML(config interface{}) string { // convert to YAML string
function toTOML (line 50) | func toTOML(config interface{}) string { // convert to TOML string
function loadDefaultConfig (line 56) | func loadDefaultConfig(configFile string) {
FILE: cmd/config/main.go
type Config (line 16) | type Config struct
function Load (line 28) | func Load(configFile string, config *Config) {
FILE: cmd/controller.go
function runProcess (line 21) | func runProcess(env []string, command ...string) {
function blockWait (line 28) | func blockWait() {
function loadRadvd (line 34) | func loadRadvd(settings *config.Config) {
function loadDhcp (line 42) | func loadDhcp(settings *config.Config) {
function loadAsset (line 50) | func loadAsset(settings *config.Config) {
function loadNetwork (line 59) | func loadNetwork(settings *config.Config) {
function loadProxy (line 67) | func loadProxy(settings *config.Config) {
function runProxy (line 76) | func runProxy(settings *config.Config) {
function runRadvd (line 84) | func runRadvd(settings *config.Config) {
function runDhcp (line 98) | func runDhcp(settings *config.Config) {
FILE: cmd/custom/main.go
type Config (line 9) | type Config struct
function runScript (line 14) | func runScript(command string) {
function RunPreScript (line 27) | func RunPreScript(config *Config) {
function RunPostScript (line 34) | func RunPostScript(config *Config) {
FILE: cmd/dhcp/main.go
type dhcpConfig (line 11) | type dhcpConfig struct
type Config (line 16) | type Config struct
function Load (line 21) | func Load(config *Config) {
FILE: cmd/network/dns.go
function loadDns (line 8) | func loadDns(dns []string) {
FILE: cmd/network/main.go
type Config (line 7) | type Config struct
function Load (line 18) | func Load(dns []string, dev string, ipv4 *Config, ipv6 *Config) {
FILE: cmd/network/network.go
function getV4Cidr (line 9) | func getV4Cidr() []string { // fetch ipv4 network range
function getV6Cidr (line 18) | func getV6Cidr() []string { // fetch ipv6 network range
function enableIpForward (line 27) | func enableIpForward() { // enable ip forward function
function flushNetwork (line 34) | func flushNetwork(dev string, isV4 bool, isV6 bool) { // flush ipv4 and ...
function loadV4Network (line 46) | func loadV4Network(v4 *Config, dev string) { // setting up ipv4 network
function loadV6Network (line 56) | func loadV6Network(v6 *Config, dev string) { // setting up ipv6 network
function loadNetwork (line 66) | func loadNetwork(dev string, v4 *Config, v6 *Config) {
FILE: cmd/network/tproxy.go
function loadV4TProxy (line 8) | func loadV4TProxy(v4 *Config, v4SysCidr []string) {
function loadV6TProxy (line 29) | func loadV6TProxy(v6 *Config, v6SysCidr []string) {
FILE: cmd/process/daemon.go
function daemonSub (line 8) | func daemonSub(sub *Process) {
method Daemon (line 22) | func (p *Process) Daemon() {
FILE: cmd/process/exit.go
function Exit (line 10) | func Exit(subProcess ...*Process) {
FILE: cmd/process/main.go
type Process (line 10) | type Process struct
method Run (line 25) | func (p *Process) Run(isOutput bool, env []string) {
method Signal (line 43) | func (p *Process) Signal(signal syscall.Signal) {
method Wait (line 50) | func (p *Process) Wait() {
function New (line 17) | func New(command ...string) *Process {
FILE: cmd/proxy/config.go
type logObject (line 18) | type logObject struct
type inboundsObject (line 26) | type inboundsObject struct
type sniffObject (line 30) | type sniffObject struct
type inboundObject (line 37) | type inboundObject struct
function loadLogConfig (line 46) | func loadLogConfig(logLevel string, logDir string) string {
function loadHttpConfig (line 62) | func loadHttpConfig(tag string, port int, sniff sniffObject) interface{} {
function loadSocksConfig (line 74) | func loadSocksConfig(tag string, port int, sniff sniffObject) interface{} {
function loadTProxyConfig (line 89) | func loadTProxyConfig(tag string, port int, sniff sniffObject) interface...
FILE: cmd/proxy/main.go
type Config (line 9) | type Config struct
function saveConfig (line 24) | func saveConfig(configDir string, caption string, content string, overwr...
function loadInbounds (line 29) | func loadInbounds(config *Config) string {
function Load (line 53) | func Load(configDir string, exposeDir string, config *Config) {
FILE: cmd/radvd/radvd.go
type Config (line 9) | type Config struct
function genSpace (line 33) | func genSpace(num int) string {
function loadOption (line 37) | func loadOption(options map[string]string, intend int) string { // load ...
function loadClient (line 45) | func loadClient(clients []string) string { // load radvd client configure
function loadPrefix (line 56) | func loadPrefix(prefix string, option map[string]string) string { // loa...
function loadRoute (line 64) | func loadRoute(cidr string, option map[string]string) string { // load r...
function loadRdnss (line 72) | func loadRdnss(ip []string, option map[string]string) string { // load r...
function loadDnssl (line 80) | func loadDnssl(suffix []string, option map[string]string) string { // lo...
function Load (line 88) | func Load(Radvd *Config) {
FILE: cmd/xproxy.go
function logInit (line 31) | func logInit(isDebug bool, logDir string) {
function xproxyInit (line 49) | func xproxyInit() {
function main (line 96) | func main() {
Condensed preview — 29 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (100K chars).
[
{
"path": ".gitignore",
"chars": 8,
"preview": "/.idea/\n"
},
{
"path": "Dockerfile",
"chars": 1428,
"preview": "ARG ALPINE=\"alpine:3.20\"\nARG GOLANG=\"golang:1.24-alpine3.22\"\n\nFROM --platform=${BUILDPLATFORM} ${GOLANG} AS xray\nENV XRA"
},
{
"path": "LICENSE",
"chars": 1065,
"preview": "MIT License\n\nCopyright (c) 2022 Dnomd343\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\no"
},
{
"path": "README.md",
"chars": 13125,
"preview": "# XProxy\n\n> 虚拟代理网关,对局域网设备进行透明代理\n\n+ ✅ 基于容器运行,无需修改主机路由配置,开箱即用\n\n+ ✅ 独立的 MAC 地址,与宿主机网络栈无耦合,随开随关\n\n+ ✅ 允许自定义 DNS 、上游网关、IP 地址等网"
},
{
"path": "cmd/asset/asset.go",
"chars": 683,
"preview": "package asset\n\nimport (\n \"XProxy/cmd/common\"\n log \"github.com/sirupsen/logrus\"\n \"path\"\n)\n\nfunc extractFile(arch"
},
{
"path": "cmd/asset/update.go",
"chars": 1824,
"preview": "package asset\n\nimport (\n \"XProxy/cmd/common\"\n \"github.com/robfig/cron\"\n log \"github.com/sirupsen/logrus\"\n \"o"
},
{
"path": "cmd/common/file.go",
"chars": 4647,
"preview": "package common\n\nimport (\n \"github.com/andybalholm/brotli\"\n \"github.com/go-http-utils/headers\"\n \"github.com/klau"
},
{
"path": "cmd/common/func.go",
"chars": 1150,
"preview": "package common\n\nimport (\n \"encoding/json\"\n log \"github.com/sirupsen/logrus\"\n \"net\"\n \"os/exec\"\n \"strings\"\n"
},
{
"path": "cmd/config/decode.go",
"chars": 8042,
"preview": "package config\n\nimport (\n \"XProxy/cmd/asset\"\n \"XProxy/cmd/common\"\n \"XProxy/cmd/custom\"\n \"XProxy/cmd/dhcp\"\n "
},
{
"path": "cmd/config/default.go",
"chars": 1894,
"preview": "package config\n\nimport (\n \"XProxy/cmd/common\"\n \"bytes\"\n \"encoding/json\"\n \"github.com/BurntSushi/toml\"\n lo"
},
{
"path": "cmd/config/main.go",
"chars": 1290,
"preview": "package config\n\nimport (\n \"XProxy/cmd/asset\"\n \"XProxy/cmd/common\"\n \"XProxy/cmd/custom\"\n \"XProxy/cmd/dhcp\"\n "
},
{
"path": "cmd/controller.go",
"chars": 3742,
"preview": "package main\n\nimport (\n \"XProxy/cmd/asset\"\n \"XProxy/cmd/common\"\n \"XProxy/cmd/config\"\n \"XProxy/cmd/dhcp\"\n "
},
{
"path": "cmd/custom/main.go",
"chars": 884,
"preview": "package custom\n\nimport (\n log \"github.com/sirupsen/logrus\"\n \"os\"\n \"os/exec\"\n)\n\ntype Config struct {\n Pre []"
},
{
"path": "cmd/dhcp/main.go",
"chars": 783,
"preview": "package dhcp\n\nimport (\n \"XProxy/cmd/common\"\n log \"github.com/sirupsen/logrus\"\n \"path\"\n)\n\nvar WorkDir = \"/etc/dh"
},
{
"path": "cmd/network/dns.go",
"chars": 451,
"preview": "package network\n\nimport (\n \"XProxy/cmd/common\"\n log \"github.com/sirupsen/logrus\"\n)\n\nfunc loadDns(dns []string) {\n "
},
{
"path": "cmd/network/main.go",
"chars": 467,
"preview": "package network\n\nimport (\n \"XProxy/cmd/common\"\n)\n\ntype Config struct {\n RouteTable int\n TProxyPort int\n Addr"
},
{
"path": "cmd/network/network.go",
"chars": 2731,
"preview": "package network\n\nimport (\n log \"github.com/sirupsen/logrus\"\n \"regexp\"\n \"time\"\n)\n\nfunc getV4Cidr() []string { //"
},
{
"path": "cmd/network/tproxy.go",
"chars": 2292,
"preview": "package network\n\nimport (\n log \"github.com/sirupsen/logrus\"\n \"strconv\"\n)\n\nfunc loadV4TProxy(v4 *Config, v4SysCidr "
},
{
"path": "cmd/process/daemon.go",
"chars": 864,
"preview": "package process\n\nimport (\n log \"github.com/sirupsen/logrus\"\n \"time\"\n)\n\nfunc daemonSub(sub *Process) {\n for sub."
},
{
"path": "cmd/process/exit.go",
"chars": 624,
"preview": "package process\n\nimport (\n log \"github.com/sirupsen/logrus\"\n \"syscall\"\n)\n\nvar exitFlag bool\n\nfunc Exit(subProcess "
},
{
"path": "cmd/process/main.go",
"chars": 1355,
"preview": "package process\n\nimport (\n log \"github.com/sirupsen/logrus\"\n \"os\"\n \"os/exec\"\n \"syscall\"\n)\n\ntype Process stru"
},
{
"path": "cmd/proxy/config.go",
"chars": 3125,
"preview": "package proxy\n\nimport (\n \"XProxy/cmd/common\"\n log \"github.com/sirupsen/logrus\"\n \"path\"\n)\n\nvar outboundsConfig ="
},
{
"path": "cmd/proxy/main.go",
"chars": 2659,
"preview": "package proxy\n\nimport (\n \"XProxy/cmd/common\"\n log \"github.com/sirupsen/logrus\"\n \"path\"\n)\n\ntype Config struct {\n"
},
{
"path": "cmd/radvd/radvd.go",
"chars": 3749,
"preview": "package radvd\n\nimport (\n \"XProxy/cmd/common\"\n log \"github.com/sirupsen/logrus\"\n \"strings\"\n)\n\ntype Config struct"
},
{
"path": "cmd/xproxy.go",
"chars": 3637,
"preview": "package main\n\nimport (\n \"XProxy/cmd/common\"\n \"XProxy/cmd/config\"\n \"XProxy/cmd/custom\"\n \"XProxy/cmd/process\"\n"
},
{
"path": "docs/campus_network_cracking.md",
"chars": 7740,
"preview": "# 使用 XProxy 绕过校园网认证登录\n\n部分校园网在登录认证时需要 DNS 解析,因而在防火墙上允许 `TCP/53` 或 `UDP/53` 端口通行,借助这个漏洞,可将内网流量用 XProxy 代理并转发到公网服务器上,实现免认证、"
},
{
"path": "docs/dual_stack_network_proxy.md",
"chars": 6145,
"preview": "# 家庭网络的 IPv4 与 IPv6 透明代理\n\n家庭网络光纤入网,支持 IPv4 与 IPv6 网络,需要在内网搭建透明代理,让设备的国内流量直连,出境流量转发到代理服务器上,避开 GFW 的流量审查。\n\n以下为典型网络拓扑:\n\n![N"
},
{
"path": "go.mod",
"chars": 383,
"preview": "module XProxy\n\ngo 1.24.5\n\nrequire (\n\tgithub.com/BurntSushi/toml v1.5.0\n\tgithub.com/andybalholm/brotli v1.2.0\n\tgithub.com"
},
{
"path": "go.sum",
"chars": 2652,
"preview": "github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=\ngithub.com/BurntSushi/toml v1.5.0/go.m"
}
]
About this extraction
This page contains the full source code of the dnomd343/XProxy GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 29 files (77.6 KB), approximately 29.0k tokens, and a symbol index with 98 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.