mirror of
https://github.com/CHN-beta/nixos.git
synced 2026-01-12 04:39:23 +08:00
modules.services.xray: split
This commit is contained in:
@@ -1,540 +0,0 @@
|
||||
inputs:
|
||||
{
|
||||
options.nixos.services.xray = let inherit (inputs.lib) mkOption types; in
|
||||
{
|
||||
client = mkOption
|
||||
{
|
||||
type = types.nullOr (types.submodule (submoduleInputs: { options =
|
||||
{
|
||||
xray =
|
||||
{
|
||||
serverName = mkOption { type = types.nonEmptyStr; default = "xserver2.chn.moe"; };
|
||||
serverAddress = mkOption
|
||||
{
|
||||
type = types.nonEmptyStr;
|
||||
default = inputs.topInputs.self.config.dns."chn.moe".getAddress
|
||||
(inputs.lib.removeSuffix ".chn.moe" submoduleInputs.config.xray.serverName);
|
||||
};
|
||||
};
|
||||
dnsmasq =
|
||||
{
|
||||
extraInterfaces = mkOption { type = types.listOf types.nonEmptyStr; default = []; };
|
||||
hosts = mkOption { type = types.attrsOf types.nonEmptyStr; default = {}; };
|
||||
};
|
||||
v2ray-forwarder.noproxyUsers = mkOption { type = types.listOf types.nonEmptyStr; default = [ "gb" "xll" ]; };
|
||||
};}));
|
||||
default = null;
|
||||
};
|
||||
server = mkOption
|
||||
{
|
||||
type = types.nullOr (types.submodule { options =
|
||||
{
|
||||
serverName = mkOption { type = types.nonEmptyStr; default = "xserver2.chn.moe"; };
|
||||
};});
|
||||
default = null;
|
||||
};
|
||||
};
|
||||
config = let inherit (inputs.config.nixos.services) xray; in inputs.lib.mkMerge
|
||||
[
|
||||
{
|
||||
assertions =
|
||||
[{
|
||||
assertion = !(xray.client != null && xray.server != null);
|
||||
message = "Currenty xray.client and xray.server could not be simutaniusly enabled.";
|
||||
}];
|
||||
}
|
||||
(
|
||||
inputs.lib.mkIf (xray.client != null)
|
||||
{
|
||||
services =
|
||||
{
|
||||
xray = { enable = true; settingsFile = inputs.config.sops.templates."xray-client.json".path; };
|
||||
dnsmasq =
|
||||
{
|
||||
enable = true;
|
||||
settings =
|
||||
{
|
||||
no-poll = true;
|
||||
log-queries = true;
|
||||
server = [ "127.0.0.1#10853" ];
|
||||
interface = xray.client.dnsmasq.extraInterfaces ++ [ "lo" ];
|
||||
bind-dynamic = true;
|
||||
address = builtins.map (host: "/${host.name}/${host.value}")
|
||||
(inputs.localLib.attrsToList xray.client.dnsmasq.hosts);
|
||||
};
|
||||
};
|
||||
resolved.enable = false;
|
||||
};
|
||||
sops =
|
||||
{
|
||||
templates."xray-client.json" =
|
||||
{
|
||||
owner = inputs.config.users.users.v2ray.name;
|
||||
group = inputs.config.users.users.v2ray.group;
|
||||
content = let chinaDns = "223.5.5.5"; foreignDns = "8.8.8.8"; in builtins.toJSON
|
||||
{
|
||||
log.loglevel = "warning";
|
||||
dns =
|
||||
{
|
||||
servers =
|
||||
# 先尝试匹配域名列表进行查询,若匹配成功则使用前两个 dns 查询。
|
||||
# 若匹配域名列表失败,或者匹配成功但是查询到的 IP 不在期望的 IP 列表中,则回落到使用后两个 dns 依次查询。
|
||||
[
|
||||
{
|
||||
address = chinaDns;
|
||||
domains = [ "geosite:geolocation-cn" ];
|
||||
expectIPs = [ "geoip:cn" ];
|
||||
skipFallback = true;
|
||||
}
|
||||
{
|
||||
address = foreignDns;
|
||||
domains = [ "geosite:geolocation-!cn" ];
|
||||
expectIPs = [ "geoip:!cn" ];
|
||||
skipFallback = true;
|
||||
}
|
||||
{ address = chinaDns; expectIPs = [ "geoip:cn" ]; }
|
||||
{ address = foreignDns; }
|
||||
];
|
||||
disableCache = true;
|
||||
queryStrategy = "UseIPv4";
|
||||
tag = "dns-internal";
|
||||
};
|
||||
inbounds =
|
||||
[
|
||||
{
|
||||
port = 10853;
|
||||
protocol = "dokodemo-door";
|
||||
settings = { address = "8.8.8.8"; network = "tcp,udp"; port = 53; };
|
||||
tag = "dns-in";
|
||||
}
|
||||
{
|
||||
port = 10880;
|
||||
protocol = "dokodemo-door";
|
||||
settings = { network = "tcp,udp"; followRedirect = true; };
|
||||
streamSettings.sockopt.tproxy = "tproxy";
|
||||
sniffing = { enabled = true; destOverride = [ "http" "tls" "quic" ]; routeOnly = true; };
|
||||
tag = "common-in";
|
||||
}
|
||||
{
|
||||
port = 10881;
|
||||
protocol = "dokodemo-door";
|
||||
settings = { network = "tcp,udp"; followRedirect = true; };
|
||||
streamSettings.sockopt.tproxy = "tproxy";
|
||||
tag = "xmu-in";
|
||||
}
|
||||
{
|
||||
port = 10883;
|
||||
protocol = "dokodemo-door";
|
||||
settings = { network = "tcp,udp"; followRedirect = true; };
|
||||
streamSettings.sockopt.tproxy = "tproxy";
|
||||
tag = "proxy-in";
|
||||
}
|
||||
{ port = 10884; protocol = "socks"; settings.udp = true; tag = "proxy-socks-in"; }
|
||||
{ port = 10882; protocol = "socks"; settings.udp = true; tag = "direct-in"; }
|
||||
];
|
||||
outbounds =
|
||||
[
|
||||
{
|
||||
protocol = "vless";
|
||||
settings.vnext =
|
||||
[{
|
||||
address = xray.client.xray.serverAddress;
|
||||
port = 443;
|
||||
users =
|
||||
[{
|
||||
id = inputs.config.sops.placeholder."xray-client/uuid";
|
||||
encryption = "none";
|
||||
flow = "xtls-rprx-vision-udp443";
|
||||
}];
|
||||
}];
|
||||
streamSettings =
|
||||
{
|
||||
network = "raw";
|
||||
security = "reality";
|
||||
realitySettings =
|
||||
{
|
||||
inherit (xray.client.xray) serverName;
|
||||
publicKey = "Nl0eVZoDF9d71_3dVsZGJl3UWR9LCv3B14gu7G6vhjk";
|
||||
fingerprint = "firefox";
|
||||
};
|
||||
};
|
||||
tag = "proxy-vless";
|
||||
}
|
||||
{ protocol = "freedom"; tag = "direct"; }
|
||||
{ protocol = "dns"; tag = "dns-out"; }
|
||||
{
|
||||
protocol = "socks";
|
||||
settings.servers = [{ address = "127.0.0.1"; port = 10069; }];
|
||||
tag = "xmu-out";
|
||||
}
|
||||
{ protocol = "blackhole"; tag = "block"; }
|
||||
];
|
||||
routing =
|
||||
{
|
||||
domainStrategy = "AsIs";
|
||||
rules = builtins.map (rule: rule // { type = "field"; })
|
||||
[
|
||||
{ inboundTag = [ "dns-in" ]; outboundTag = "dns-out"; }
|
||||
{ inboundTag = [ "dns-internal" ]; ip = [ chinaDns ]; outboundTag = "direct"; }
|
||||
{ inboundTag = [ "dns-internal" ]; ip = [ foreignDns ]; outboundTag = "proxy-vless"; }
|
||||
{ inboundTag = [ "dns-internal" ]; outboundTag = "block"; }
|
||||
{ inboundTag = [ "xmu-in" ]; outboundTag = "xmu-out"; }
|
||||
{ inboundTag = [ "direct-in" ]; outboundTag = "direct"; }
|
||||
{ inboundTag = [ "proxy-in" "proxy-socks-in" ]; outboundTag = "proxy-vless"; }
|
||||
{ inboundTag = [ "common-in" ]; domain = [ "geosite:geolocation-cn" ]; outboundTag = "direct"; }
|
||||
{
|
||||
inboundTag = [ "common-in" ];
|
||||
domain = [ "geosite:geolocation-!cn" ];
|
||||
outboundTag = "proxy-vless";
|
||||
}
|
||||
{ inboundTag = [ "common-in" ]; ip = [ "geoip:cn" ]; outboundTag = "direct"; }
|
||||
{ inboundTag = [ "common-in" ]; outboundTag = "proxy-vless"; }
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
secrets."xray-client/uuid" = {};
|
||||
};
|
||||
systemd.services =
|
||||
{
|
||||
xray =
|
||||
{
|
||||
serviceConfig =
|
||||
{
|
||||
DynamicUser = inputs.lib.mkForce false;
|
||||
User = "v2ray";
|
||||
Group = "v2ray";
|
||||
CapabilityBoundingSet = "CAP_NET_ADMIN CAP_NET_BIND_SERVICE";
|
||||
AmbientCapabilities = "CAP_NET_ADMIN CAP_NET_BIND_SERVICE";
|
||||
LimitNPROC = 65536;
|
||||
LimitNOFILE = 524288;
|
||||
CPUSchedulingPolicy = "rr";
|
||||
};
|
||||
restartTriggers = [ inputs.config.sops.templates."xray-client.json".file ];
|
||||
};
|
||||
v2ray-forwarder =
|
||||
{
|
||||
description = "v2ray-forwarder Daemon";
|
||||
after = [ "network.target" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
serviceConfig = let ip = "${inputs.pkgs.iproute2}/bin/ip"; in
|
||||
{
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
ExecStart = inputs.pkgs.writeShellScript "v2ray-forwarder.start"
|
||||
''
|
||||
${ip} rule add fwmark 1/1 table 100
|
||||
${ip} route add local 0.0.0.0/0 dev lo table 100
|
||||
'';
|
||||
ExecStop = inputs.pkgs.writeShellScript "v2ray-forwarder.stop"
|
||||
''
|
||||
${ip} rule del fwmark 1/1 table 100
|
||||
${ip} route del local 0.0.0.0/0 dev lo table 100
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
users =
|
||||
{
|
||||
users.v2ray = { uid = inputs.config.nixos.user.uid.v2ray; group = "v2ray"; isSystemUser = true; };
|
||||
groups.v2ray.gid = inputs.config.nixos.user.gid.v2ray;
|
||||
};
|
||||
environment.etc."resolv.conf".text = "nameserver 127.0.0.1";
|
||||
networking =
|
||||
{
|
||||
nftables.tables.v2ray =
|
||||
{
|
||||
family = "inet";
|
||||
content =
|
||||
let
|
||||
autoPort = "10880";
|
||||
xmuPort = "10881";
|
||||
proxyPort = "10883";
|
||||
loNet =
|
||||
[
|
||||
"0.0.0.0/8" "10.0.0.0/8" "100.64.0.0/10" "127.0.0.0/8" "169.254.0.0/16" "172.16.0.0/12"
|
||||
"192.0.0.0/24" "192.88.99.0/24" "192.168.0.0/16" "59.77.0.143" "198.18.0.0/15"
|
||||
"198.51.100.0/24" "203.0.113.0/24" "224.0.0.0/4" "240.0.0.0/4"
|
||||
];
|
||||
loNetStr = builtins.concatStringsSep ", " loNet;
|
||||
noproxyUserStr = builtins.concatStringsSep ", " (builtins.map
|
||||
(user: builtins.toString inputs.config.nixos.user.uid.${user})
|
||||
(xray.client.v2ray-forwarder.noproxyUsers ++ [ "v2ray" ]));
|
||||
in
|
||||
''
|
||||
set lo_net { type ipv4_addr; flags interval; elements = { ${loNetStr} }; }
|
||||
set xmu_net { type ipv4_addr; flags interval; }
|
||||
set noproxy_net { type ipv4_addr; flags interval; elements = { 223.5.5.5 }; }
|
||||
set noproxy_src_net { type ipv4_addr; flags interval; }
|
||||
set proxy_net { type ipv4_addr; flags interval; elements = { 8.8.8.8 }; }
|
||||
|
||||
chain prerouting {
|
||||
type filter hook prerouting priority mangle; policy accept;
|
||||
meta l4proto != { tcp, udp } counter return
|
||||
|
||||
# 对于目标地址为本机的新建的流,标记并永不代理
|
||||
fib daddr type local ct state new counter ct mark set ct mark | 1 return
|
||||
ct mark & 1 == 1 counter return
|
||||
|
||||
ip saddr @noproxy_src_net counter return
|
||||
ip daddr @noproxy_net counter return
|
||||
ip saddr != 172.16.0.0/12 ip daddr @xmu_net meta l4proto { tcp, udp } counter \
|
||||
tproxy ip to :${xmuPort} meta mark set meta mark | 1 return
|
||||
ip daddr @proxy_net meta l4proto { tcp, udp } counter tproxy ip to :${proxyPort} \
|
||||
meta mark set meta mark | 1 return
|
||||
ip daddr @lo_net counter return
|
||||
meta l4proto { tcp, udp } counter tproxy ip to :${autoPort} meta mark set meta mark | 1 return
|
||||
return
|
||||
}
|
||||
|
||||
chain output {
|
||||
type route hook output priority mangle; policy accept;
|
||||
ct mark & 1 == 1 counter return
|
||||
meta skuid { ${noproxyUserStr} } counter return
|
||||
|
||||
ip saddr @noproxy_src_net counter return
|
||||
ip daddr @noproxy_net counter return
|
||||
ip daddr @xmu_net counter meta mark set meta mark | 1 return
|
||||
ip daddr @proxy_net counter meta mark set meta mark | 1 return
|
||||
ip daddr @lo_net counter return
|
||||
meta l4proto { tcp, udp } counter meta mark set meta mark | 1 return
|
||||
return
|
||||
}
|
||||
'';
|
||||
};
|
||||
firewall =
|
||||
{
|
||||
allowedTCPPorts = [ 53 ];
|
||||
allowedUDPPorts = [ 53 ];
|
||||
allowedTCPPortRanges = [{ from = 10880; to = 10884; }];
|
||||
allowedUDPPortRanges = [{ from = 10880; to = 10884; }];
|
||||
};
|
||||
};
|
||||
}
|
||||
)
|
||||
(
|
||||
inputs.lib.mkIf (xray.server != null)
|
||||
(
|
||||
let userList = builtins.attrNames
|
||||
(inputs.pkgs.localPackages.fromYaml (builtins.readFile inputs.config.sops.defaultSopsFile))
|
||||
.xray-server.clients;
|
||||
in
|
||||
{
|
||||
services.xray = { enable = true; settingsFile = inputs.config.sops.templates."xray-server.json".path; };
|
||||
sops =
|
||||
{
|
||||
templates."xray-server.json" =
|
||||
{
|
||||
owner = inputs.config.users.users.v2ray.name;
|
||||
group = inputs.config.users.users.v2ray.group;
|
||||
content = builtins.toJSON
|
||||
{
|
||||
log.loglevel = "warning";
|
||||
inbounds =
|
||||
[
|
||||
(
|
||||
let fallbackPort = builtins.toString
|
||||
(with inputs.config.nixos.services.nginx.global; httpsPort + httpsPortShift.http2);
|
||||
in
|
||||
{
|
||||
port = 4726;
|
||||
listen = "127.0.0.1";
|
||||
protocol = "vless";
|
||||
settings =
|
||||
{
|
||||
clients = builtins.map
|
||||
(n:
|
||||
{
|
||||
id = inputs.config.sops.placeholder."xray-server/clients/${n}";
|
||||
flow = "xtls-rprx-vision";
|
||||
email = "${n}@xray.chn.moe";
|
||||
})
|
||||
userList;
|
||||
decryption = "none";
|
||||
fallbacks = [{ dest = "127.0.0.1:${fallbackPort}"; }];
|
||||
};
|
||||
streamSettings =
|
||||
{
|
||||
network = "raw";
|
||||
security = "reality";
|
||||
realitySettings =
|
||||
{
|
||||
dest = "127.0.0.1:${fallbackPort}";
|
||||
serverNames = [ xray.server.serverName ];
|
||||
privateKey = inputs.config.sops.placeholder."xray-server/private-key";
|
||||
minClientVer = "1.8.0";
|
||||
shortIds = [ "" ];
|
||||
};
|
||||
};
|
||||
sniffing = { enabled = true; destOverride = [ "http" "tls" "quic" ]; routeOnly = true; };
|
||||
tag = "in-legacy";
|
||||
}
|
||||
)
|
||||
{
|
||||
port = 4638;
|
||||
listen = "127.0.0.1";
|
||||
protocol = "vless";
|
||||
settings = { clients = [{ id = "be01f0a0-9976-42f5-b9ab-866eba6ed393"; }]; decryption = "none"; };
|
||||
streamSettings.network = "raw";
|
||||
sniffing = { enabled = true; destOverride = [ "http" "tls" "quic" ]; };
|
||||
tag = "in-localdns";
|
||||
}
|
||||
{
|
||||
listen = "127.0.0.1";
|
||||
port = 6149;
|
||||
protocol = "dokodemo-door";
|
||||
settings.address = "127.0.0.1";
|
||||
tag = "api";
|
||||
}
|
||||
];
|
||||
outbounds =
|
||||
[
|
||||
{ protocol = "freedom"; tag = "freedom"; }
|
||||
{
|
||||
protocol = "vless";
|
||||
settings.vnext =
|
||||
[{
|
||||
address = "127.0.0.1";
|
||||
port = 4638;
|
||||
users = [{ id = "be01f0a0-9976-42f5-b9ab-866eba6ed393"; encryption = "none"; }];
|
||||
}];
|
||||
streamSettings.network = "raw";
|
||||
tag = "loopback-localdns";
|
||||
}
|
||||
];
|
||||
routing =
|
||||
{
|
||||
domainStrategy = "AsIs";
|
||||
rules = builtins.map (rule: rule // { type = "field"; })
|
||||
[
|
||||
{
|
||||
inboundTag = [ "in-legacy" ];
|
||||
domain = [ "domain:openai.com" ];
|
||||
outboundTag = "loopback-localdns";
|
||||
}
|
||||
{ inboundTag = [ "in-legacy" ]; outboundTag = "freedom"; }
|
||||
{ inboundTag = [ "in-localdns" ]; outboundTag = "freedom"; }
|
||||
{ inboundTag = [ "api" ]; outboundTag = "api"; }
|
||||
];
|
||||
};
|
||||
stats = {};
|
||||
api = { tag = "api"; services = [ "StatsService" ]; };
|
||||
policy =
|
||||
{
|
||||
levels."0" = { statsUserUplink = true; statsUserDownlink = true; };
|
||||
system =
|
||||
{
|
||||
statsInboundUplink = true;
|
||||
statsInboundDownlink = true;
|
||||
statsOutboundUplink = true;
|
||||
statsOutboundDownlink = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
secrets = builtins.listToAttrs
|
||||
(builtins.map (n: { name = "xray-server/clients/${n}"; value = {}; }) userList)
|
||||
// (builtins.listToAttrs (builtins.map
|
||||
(name:
|
||||
{
|
||||
name = "telegram/${name}";
|
||||
value =
|
||||
{
|
||||
group = "telegram";
|
||||
mode = "0440";
|
||||
sopsFile = "${inputs.config.nixos.system.sops.crossSopsDir}/default.yaml";
|
||||
};
|
||||
})
|
||||
[ "token" "user/chn" ]))
|
||||
// { "xray-server/private-key" = {}; };
|
||||
};
|
||||
systemd =
|
||||
{
|
||||
services =
|
||||
{
|
||||
xray =
|
||||
{
|
||||
serviceConfig =
|
||||
{
|
||||
DynamicUser = inputs.lib.mkForce false;
|
||||
User = "v2ray";
|
||||
Group = "v2ray";
|
||||
CapabilityBoundingSet = "CAP_NET_ADMIN CAP_NET_BIND_SERVICE";
|
||||
AmbientCapabilities = "CAP_NET_ADMIN CAP_NET_BIND_SERVICE";
|
||||
LimitNPROC = 65536;
|
||||
LimitNOFILE = 524288;
|
||||
};
|
||||
restartTriggers = [ inputs.config.sops.templates."xray-server.json".file ];
|
||||
};
|
||||
xray-stat =
|
||||
{
|
||||
script =
|
||||
let
|
||||
xray = "${inputs.pkgs.xray}/bin/xray";
|
||||
awk = "${inputs.pkgs.gawk}/bin/awk";
|
||||
curl = "${inputs.pkgs.curl}/bin/curl";
|
||||
jq = "${inputs.pkgs.jq}/bin/jq";
|
||||
sed = "${inputs.pkgs.gnused}/bin/sed";
|
||||
cat = "${inputs.pkgs.coreutils}/bin/cat";
|
||||
token = inputs.config.sops.secrets."telegram/token".path;
|
||||
chat = inputs.config.sops.secrets."telegram/user/chn".path;
|
||||
in
|
||||
''
|
||||
message='${inputs.config.nixos.model.hostname} xray:\n'
|
||||
for i in ${builtins.concatStringsSep " " userList}
|
||||
do
|
||||
upload_bytes=$(${xray} api stats --server=127.0.0.1:6149 \
|
||||
-name "user>>>''${i}@xray.chn.moe>>>traffic>>>uplink" | ${jq} '.stat.value' | ${sed} 's/"//g')
|
||||
[ -z "$upload_bytes" ] && upload_bytes=0
|
||||
download_bytes=$(${xray} api stats --server=127.0.0.1:6149 \
|
||||
-name "user>>>''${i}@xray.chn.moe>>>traffic>>>downlink" | ${jq} '.stat.value' | ${sed} 's/"//g')
|
||||
[ -z "$download_bytes" ] && download_bytes=0
|
||||
traffic_gb=$(echo | ${awk} "{printf \"%.3f\",(''${upload_bytes}+''${download_bytes})/1073741824}")
|
||||
message="$message$i"'\t'"''${traffic_gb}"'G\n'
|
||||
done
|
||||
${curl} -X POST -H 'Content-Type: application/json' \
|
||||
-d "{\"chat_id\": \"$(${cat} ${chat})\", \"text\": \"$message\"}" \
|
||||
https://api.telegram.org/bot$(${cat} ${token})/sendMessage
|
||||
'';
|
||||
serviceConfig = { Type = "oneshot"; User = "v2ray"; Group = "v2ray"; };
|
||||
};
|
||||
};
|
||||
timers.xray-stat =
|
||||
{
|
||||
wantedBy = [ "timers.target" ];
|
||||
timerConfig = { OnCalendar = "*-*-* 0:00:00"; Unit = "xray-stat.service"; };
|
||||
};
|
||||
};
|
||||
users =
|
||||
{
|
||||
users.v2ray =
|
||||
{
|
||||
uid = inputs.config.nixos.user.uid.v2ray;
|
||||
group = "v2ray";
|
||||
extraGroups = [ "telegram" ];
|
||||
isSystemUser = true;
|
||||
};
|
||||
groups =
|
||||
{
|
||||
v2ray.gid = inputs.config.nixos.user.gid.v2ray;
|
||||
telegram.gid = inputs.config.nixos.user.gid.telegram;
|
||||
};
|
||||
};
|
||||
nixos.services =
|
||||
{
|
||||
acme.cert.${xray.server.serverName}.group = inputs.config.users.users.nginx.group;
|
||||
nginx =
|
||||
{
|
||||
enable = true;
|
||||
transparentProxy.map.${xray.server.serverName} = 4726;
|
||||
https.${xray.server.serverName} =
|
||||
{
|
||||
listen.main = { proxyProtocol = false; addToTransparentProxy = false; };
|
||||
location."/".return.return = "400";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
))
|
||||
];
|
||||
}
|
||||
293
modules/services/xray/client.nix
Normal file
293
modules/services/xray/client.nix
Normal file
@@ -0,0 +1,293 @@
|
||||
inputs:
|
||||
{
|
||||
options.nixos.services.xray.client = let inherit (inputs.lib) mkOption types; in mkOption
|
||||
{
|
||||
type = types.nullOr (types.submodule (submoduleInputs: { options =
|
||||
{
|
||||
xray =
|
||||
{
|
||||
serverName = mkOption { type = types.nonEmptyStr; default = "xserver2.chn.moe"; };
|
||||
serverAddress = mkOption
|
||||
{
|
||||
type = types.nonEmptyStr;
|
||||
default = inputs.topInputs.self.config.dns."chn.moe".getAddress
|
||||
(inputs.lib.removeSuffix ".chn.moe" submoduleInputs.config.xray.serverName);
|
||||
};
|
||||
};
|
||||
dnsmasq =
|
||||
{
|
||||
extraInterfaces = mkOption { type = types.listOf types.nonEmptyStr; default = []; };
|
||||
hosts = mkOption { type = types.attrsOf types.nonEmptyStr; default = {}; };
|
||||
};
|
||||
v2ray-forwarder.noproxyUsers = mkOption { type = types.listOf types.nonEmptyStr; default = [ "gb" "xll" ]; };
|
||||
};}));
|
||||
default = null;
|
||||
};
|
||||
config = let inherit (inputs.config.nixos.services.xray) client; in inputs.lib.mkIf (client != null)
|
||||
{
|
||||
services =
|
||||
{
|
||||
xray = { enable = true; settingsFile = inputs.config.sops.templates."xray-client.json".path; };
|
||||
dnsmasq =
|
||||
{
|
||||
enable = true;
|
||||
settings =
|
||||
{
|
||||
no-poll = true;
|
||||
log-queries = true;
|
||||
server = [ "127.0.0.1#10853" ];
|
||||
interface = client.dnsmasq.extraInterfaces ++ [ "lo" ];
|
||||
bind-dynamic = true;
|
||||
address = builtins.map (host: "/${host.name}/${host.value}")
|
||||
(inputs.localLib.attrsToList client.dnsmasq.hosts);
|
||||
};
|
||||
};
|
||||
resolved.enable = false;
|
||||
};
|
||||
sops =
|
||||
{
|
||||
templates."xray-client.json" =
|
||||
{
|
||||
owner = inputs.config.users.users.v2ray.name;
|
||||
group = inputs.config.users.users.v2ray.group;
|
||||
content = let chinaDns = "223.5.5.5"; foreignDns = "8.8.8.8"; in builtins.toJSON
|
||||
{
|
||||
log.loglevel = "warning";
|
||||
dns =
|
||||
{
|
||||
servers =
|
||||
# 先尝试匹配域名列表进行查询,若匹配成功则使用前两个 dns 查询。
|
||||
# 若匹配域名列表失败,或者匹配成功但是查询到的 IP 不在期望的 IP 列表中,则回落到使用后两个 dns 依次查询。
|
||||
[
|
||||
{
|
||||
address = chinaDns;
|
||||
domains = [ "geosite:geolocation-cn" ];
|
||||
expectIPs = [ "geoip:cn" ];
|
||||
skipFallback = true;
|
||||
}
|
||||
{
|
||||
address = foreignDns;
|
||||
domains = [ "geosite:geolocation-!cn" ];
|
||||
expectIPs = [ "geoip:!cn" ];
|
||||
skipFallback = true;
|
||||
}
|
||||
{ address = chinaDns; expectIPs = [ "geoip:cn" ]; }
|
||||
{ address = foreignDns; }
|
||||
];
|
||||
disableCache = true;
|
||||
queryStrategy = "UseIPv4";
|
||||
tag = "dns-internal";
|
||||
};
|
||||
inbounds =
|
||||
[
|
||||
{
|
||||
port = 10853;
|
||||
protocol = "dokodemo-door";
|
||||
settings = { address = "8.8.8.8"; network = "tcp,udp"; port = 53; };
|
||||
tag = "dns-in";
|
||||
}
|
||||
{
|
||||
port = 10880;
|
||||
protocol = "dokodemo-door";
|
||||
settings = { network = "tcp,udp"; followRedirect = true; };
|
||||
streamSettings.sockopt.tproxy = "tproxy";
|
||||
sniffing = { enabled = true; destOverride = [ "http" "tls" "quic" ]; routeOnly = true; };
|
||||
tag = "common-in";
|
||||
}
|
||||
{
|
||||
port = 10881;
|
||||
protocol = "dokodemo-door";
|
||||
settings = { network = "tcp,udp"; followRedirect = true; };
|
||||
streamSettings.sockopt.tproxy = "tproxy";
|
||||
tag = "xmu-in";
|
||||
}
|
||||
{
|
||||
port = 10883;
|
||||
protocol = "dokodemo-door";
|
||||
settings = { network = "tcp,udp"; followRedirect = true; };
|
||||
streamSettings.sockopt.tproxy = "tproxy";
|
||||
tag = "proxy-in";
|
||||
}
|
||||
{ port = 10884; protocol = "socks"; settings.udp = true; tag = "proxy-socks-in"; }
|
||||
{ port = 10882; protocol = "socks"; settings.udp = true; tag = "direct-in"; }
|
||||
];
|
||||
outbounds =
|
||||
[
|
||||
{
|
||||
protocol = "vless";
|
||||
settings.vnext =
|
||||
[{
|
||||
address = client.xray.serverAddress;
|
||||
port = 443;
|
||||
users =
|
||||
[{
|
||||
id = inputs.config.sops.placeholder."xray-client/uuid";
|
||||
encryption = "none";
|
||||
flow = "xtls-rprx-vision-udp443";
|
||||
}];
|
||||
}];
|
||||
streamSettings =
|
||||
{
|
||||
network = "raw";
|
||||
security = "reality";
|
||||
realitySettings =
|
||||
{
|
||||
inherit (client.xray) serverName;
|
||||
publicKey = "Nl0eVZoDF9d71_3dVsZGJl3UWR9LCv3B14gu7G6vhjk";
|
||||
fingerprint = "firefox";
|
||||
};
|
||||
};
|
||||
tag = "proxy-vless";
|
||||
}
|
||||
{ protocol = "freedom"; tag = "direct"; }
|
||||
{ protocol = "dns"; tag = "dns-out"; }
|
||||
{
|
||||
protocol = "socks";
|
||||
settings.servers = [{ address = "127.0.0.1"; port = 10069; }];
|
||||
tag = "xmu-out";
|
||||
}
|
||||
{ protocol = "blackhole"; tag = "block"; }
|
||||
];
|
||||
routing =
|
||||
{
|
||||
domainStrategy = "AsIs";
|
||||
rules = builtins.map (rule: rule // { type = "field"; })
|
||||
[
|
||||
{ inboundTag = [ "dns-in" ]; outboundTag = "dns-out"; }
|
||||
{ inboundTag = [ "dns-internal" ]; ip = [ chinaDns ]; outboundTag = "direct"; }
|
||||
{ inboundTag = [ "dns-internal" ]; ip = [ foreignDns ]; outboundTag = "proxy-vless"; }
|
||||
{ inboundTag = [ "dns-internal" ]; outboundTag = "block"; }
|
||||
{ inboundTag = [ "xmu-in" ]; outboundTag = "xmu-out"; }
|
||||
{ inboundTag = [ "direct-in" ]; outboundTag = "direct"; }
|
||||
{ inboundTag = [ "proxy-in" "proxy-socks-in" ]; outboundTag = "proxy-vless"; }
|
||||
{ inboundTag = [ "common-in" ]; domain = [ "geosite:geolocation-cn" ]; outboundTag = "direct"; }
|
||||
{
|
||||
inboundTag = [ "common-in" ];
|
||||
domain = [ "geosite:geolocation-!cn" ];
|
||||
outboundTag = "proxy-vless";
|
||||
}
|
||||
{ inboundTag = [ "common-in" ]; ip = [ "geoip:cn" ]; outboundTag = "direct"; }
|
||||
{ inboundTag = [ "common-in" ]; outboundTag = "proxy-vless"; }
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
secrets."xray-client/uuid" = {};
|
||||
};
|
||||
systemd.services =
|
||||
{
|
||||
xray =
|
||||
{
|
||||
serviceConfig =
|
||||
{
|
||||
DynamicUser = inputs.lib.mkForce false;
|
||||
User = "v2ray";
|
||||
Group = "v2ray";
|
||||
CapabilityBoundingSet = "CAP_NET_ADMIN CAP_NET_BIND_SERVICE";
|
||||
AmbientCapabilities = "CAP_NET_ADMIN CAP_NET_BIND_SERVICE";
|
||||
LimitNPROC = 65536;
|
||||
LimitNOFILE = 524288;
|
||||
CPUSchedulingPolicy = "rr";
|
||||
};
|
||||
restartTriggers = [ inputs.config.sops.templates."xray-client.json".file ];
|
||||
};
|
||||
v2ray-forwarder =
|
||||
{
|
||||
description = "v2ray-forwarder Daemon";
|
||||
after = [ "network.target" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
serviceConfig = let ip = "${inputs.pkgs.iproute2}/bin/ip"; in
|
||||
{
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
ExecStart = inputs.pkgs.writeShellScript "v2ray-forwarder.start"
|
||||
''
|
||||
${ip} rule add fwmark 1/1 table 100
|
||||
${ip} route add local 0.0.0.0/0 dev lo table 100
|
||||
'';
|
||||
ExecStop = inputs.pkgs.writeShellScript "v2ray-forwarder.stop"
|
||||
''
|
||||
${ip} rule del fwmark 1/1 table 100
|
||||
${ip} route del local 0.0.0.0/0 dev lo table 100
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
users =
|
||||
{
|
||||
users.v2ray = { uid = inputs.config.nixos.user.uid.v2ray; group = "v2ray"; isSystemUser = true; };
|
||||
groups.v2ray.gid = inputs.config.nixos.user.gid.v2ray;
|
||||
};
|
||||
environment.etc."resolv.conf".text = "nameserver 127.0.0.1";
|
||||
networking =
|
||||
{
|
||||
nftables.tables.v2ray =
|
||||
{
|
||||
family = "inet";
|
||||
content =
|
||||
let
|
||||
autoPort = "10880";
|
||||
xmuPort = "10881";
|
||||
proxyPort = "10883";
|
||||
loNet =
|
||||
[
|
||||
"0.0.0.0/8" "10.0.0.0/8" "100.64.0.0/10" "127.0.0.0/8" "169.254.0.0/16" "172.16.0.0/12"
|
||||
"192.0.0.0/24" "192.88.99.0/24" "192.168.0.0/16" "59.77.0.143" "198.18.0.0/15"
|
||||
"198.51.100.0/24" "203.0.113.0/24" "224.0.0.0/4" "240.0.0.0/4"
|
||||
];
|
||||
loNetStr = builtins.concatStringsSep ", " loNet;
|
||||
noproxyUserStr = builtins.concatStringsSep ", " (builtins.map
|
||||
(user: builtins.toString inputs.config.nixos.user.uid.${user})
|
||||
(client.v2ray-forwarder.noproxyUsers ++ [ "v2ray" ]));
|
||||
in
|
||||
''
|
||||
set lo_net { type ipv4_addr; flags interval; elements = { ${loNetStr} }; }
|
||||
set xmu_net { type ipv4_addr; flags interval; }
|
||||
set noproxy_net { type ipv4_addr; flags interval; elements = { 223.5.5.5 }; }
|
||||
set noproxy_src_net { type ipv4_addr; flags interval; }
|
||||
set proxy_net { type ipv4_addr; flags interval; elements = { 8.8.8.8 }; }
|
||||
|
||||
chain prerouting {
|
||||
type filter hook prerouting priority mangle; policy accept;
|
||||
meta l4proto != { tcp, udp } counter return
|
||||
|
||||
# 对于目标地址为本机的新建的流,标记并永不代理
|
||||
fib daddr type local ct state new counter ct mark set ct mark | 1 return
|
||||
ct mark & 1 == 1 counter return
|
||||
|
||||
ip saddr @noproxy_src_net counter return
|
||||
ip daddr @noproxy_net counter return
|
||||
ip saddr != 172.16.0.0/12 ip daddr @xmu_net meta l4proto { tcp, udp } counter \
|
||||
tproxy ip to :${xmuPort} meta mark set meta mark | 1 return
|
||||
ip daddr @proxy_net meta l4proto { tcp, udp } counter tproxy ip to :${proxyPort} \
|
||||
meta mark set meta mark | 1 return
|
||||
ip daddr @lo_net counter return
|
||||
meta l4proto { tcp, udp } counter tproxy ip to :${autoPort} meta mark set meta mark | 1 return
|
||||
return
|
||||
}
|
||||
|
||||
chain output {
|
||||
type route hook output priority mangle; policy accept;
|
||||
ct mark & 1 == 1 counter return
|
||||
meta skuid { ${noproxyUserStr} } counter return
|
||||
|
||||
ip saddr @noproxy_src_net counter return
|
||||
ip daddr @noproxy_net counter return
|
||||
ip daddr @xmu_net counter meta mark set meta mark | 1 return
|
||||
ip daddr @proxy_net counter meta mark set meta mark | 1 return
|
||||
ip daddr @lo_net counter return
|
||||
meta l4proto { tcp, udp } counter meta mark set meta mark | 1 return
|
||||
return
|
||||
}
|
||||
'';
|
||||
};
|
||||
firewall =
|
||||
{
|
||||
allowedTCPPorts = [ 53 ];
|
||||
allowedUDPPorts = [ 53 ];
|
||||
allowedTCPPortRanges = [{ from = 10880; to = 10884; }];
|
||||
allowedUDPPortRanges = [{ from = 10880; to = 10884; }];
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
12
modules/services/xray/default.nix
Normal file
12
modules/services/xray/default.nix
Normal file
@@ -0,0 +1,12 @@
|
||||
inputs:
|
||||
{
|
||||
imports = inputs.localLib.findModules ./.;
|
||||
config = let inherit (inputs.config.nixos.services) xray; in
|
||||
{
|
||||
assertions =
|
||||
[{
|
||||
assertion = !(xray.client != null && xray.server != null);
|
||||
message = "Currenty xray.client and xray.server could not be simutaniusly enabled.";
|
||||
}];
|
||||
};
|
||||
}
|
||||
233
modules/services/xray/server.nix
Normal file
233
modules/services/xray/server.nix
Normal file
@@ -0,0 +1,233 @@
|
||||
inputs:
|
||||
{
|
||||
options.nixos.services.xray.server = let inherit (inputs.lib) mkOption types; in mkOption
|
||||
{
|
||||
type = types.nullOr (types.submodule { options =
|
||||
{
|
||||
serverName = mkOption { type = types.nonEmptyStr; default = "xserver2.chn.moe"; };
|
||||
};});
|
||||
default = null;
|
||||
};
|
||||
config = let inherit (inputs.config.nixos.services.xray) server; in inputs.lib.mkIf (server != null)
|
||||
(
|
||||
let userList = builtins.attrNames
|
||||
(inputs.pkgs.localPackages.fromYaml (builtins.readFile inputs.config.sops.defaultSopsFile)).xray-server.clients;
|
||||
in
|
||||
{
|
||||
services.xray = { enable = true; settingsFile = inputs.config.sops.templates."xray-server.json".path; };
|
||||
sops =
|
||||
{
|
||||
templates."xray-server.json" =
|
||||
{
|
||||
owner = inputs.config.users.users.v2ray.name;
|
||||
group = inputs.config.users.users.v2ray.group;
|
||||
content = builtins.toJSON
|
||||
{
|
||||
log.loglevel = "warning";
|
||||
inbounds =
|
||||
[
|
||||
(
|
||||
let fallbackPort = builtins.toString
|
||||
(with inputs.config.nixos.services.nginx.global; httpsPort + httpsPortShift.http2);
|
||||
in
|
||||
{
|
||||
port = 4726;
|
||||
listen = "127.0.0.1";
|
||||
protocol = "vless";
|
||||
settings =
|
||||
{
|
||||
clients = builtins.map
|
||||
(n:
|
||||
{
|
||||
id = inputs.config.sops.placeholder."xray-server/clients/${n}";
|
||||
flow = "xtls-rprx-vision";
|
||||
email = "${n}@xray.chn.moe";
|
||||
})
|
||||
userList;
|
||||
decryption = "none";
|
||||
fallbacks = [{ dest = "127.0.0.1:${fallbackPort}"; }];
|
||||
};
|
||||
streamSettings =
|
||||
{
|
||||
network = "raw";
|
||||
security = "reality";
|
||||
realitySettings =
|
||||
{
|
||||
dest = "127.0.0.1:${fallbackPort}";
|
||||
serverNames = [ server.serverName ];
|
||||
privateKey = inputs.config.sops.placeholder."xray-server/private-key";
|
||||
minClientVer = "1.8.0";
|
||||
shortIds = [ "" ];
|
||||
};
|
||||
};
|
||||
sniffing = { enabled = true; destOverride = [ "http" "tls" "quic" ]; routeOnly = true; };
|
||||
tag = "in-legacy";
|
||||
}
|
||||
)
|
||||
{
|
||||
port = 4638;
|
||||
listen = "127.0.0.1";
|
||||
protocol = "vless";
|
||||
settings = { clients = [{ id = "be01f0a0-9976-42f5-b9ab-866eba6ed393"; }]; decryption = "none"; };
|
||||
streamSettings.network = "raw";
|
||||
sniffing = { enabled = true; destOverride = [ "http" "tls" "quic" ]; };
|
||||
tag = "in-localdns";
|
||||
}
|
||||
{
|
||||
listen = "127.0.0.1";
|
||||
port = 6149;
|
||||
protocol = "dokodemo-door";
|
||||
settings.address = "127.0.0.1";
|
||||
tag = "api";
|
||||
}
|
||||
];
|
||||
outbounds =
|
||||
[
|
||||
{ protocol = "freedom"; tag = "freedom"; }
|
||||
{
|
||||
protocol = "vless";
|
||||
settings.vnext =
|
||||
[{
|
||||
address = "127.0.0.1";
|
||||
port = 4638;
|
||||
users = [{ id = "be01f0a0-9976-42f5-b9ab-866eba6ed393"; encryption = "none"; }];
|
||||
}];
|
||||
streamSettings.network = "raw";
|
||||
tag = "loopback-localdns";
|
||||
}
|
||||
];
|
||||
routing =
|
||||
{
|
||||
domainStrategy = "AsIs";
|
||||
rules = builtins.map (rule: rule // { type = "field"; })
|
||||
[
|
||||
{
|
||||
inboundTag = [ "in-legacy" ];
|
||||
domain = [ "domain:openai.com" ];
|
||||
outboundTag = "loopback-localdns";
|
||||
}
|
||||
{ inboundTag = [ "in-legacy" ]; outboundTag = "freedom"; }
|
||||
{ inboundTag = [ "in-localdns" ]; outboundTag = "freedom"; }
|
||||
{ inboundTag = [ "api" ]; outboundTag = "api"; }
|
||||
];
|
||||
};
|
||||
stats = {};
|
||||
api = { tag = "api"; services = [ "StatsService" ]; };
|
||||
policy =
|
||||
{
|
||||
levels."0" = { statsUserUplink = true; statsUserDownlink = true; };
|
||||
system =
|
||||
{
|
||||
statsInboundUplink = true;
|
||||
statsInboundDownlink = true;
|
||||
statsOutboundUplink = true;
|
||||
statsOutboundDownlink = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
secrets = builtins.listToAttrs
|
||||
(builtins.map (n: { name = "xray-server/clients/${n}"; value = {}; }) userList)
|
||||
// (builtins.listToAttrs (builtins.map
|
||||
(name:
|
||||
{
|
||||
name = "telegram/${name}";
|
||||
value =
|
||||
{
|
||||
group = "telegram";
|
||||
mode = "0440";
|
||||
sopsFile = "${inputs.config.nixos.system.sops.crossSopsDir}/default.yaml";
|
||||
};
|
||||
})
|
||||
[ "token" "user/chn" ]))
|
||||
// { "xray-server/private-key" = {}; };
|
||||
};
|
||||
systemd =
|
||||
{
|
||||
services =
|
||||
{
|
||||
xray =
|
||||
{
|
||||
serviceConfig =
|
||||
{
|
||||
DynamicUser = inputs.lib.mkForce false;
|
||||
User = "v2ray";
|
||||
Group = "v2ray";
|
||||
CapabilityBoundingSet = "CAP_NET_ADMIN CAP_NET_BIND_SERVICE";
|
||||
AmbientCapabilities = "CAP_NET_ADMIN CAP_NET_BIND_SERVICE";
|
||||
LimitNPROC = 65536;
|
||||
LimitNOFILE = 524288;
|
||||
};
|
||||
restartTriggers = [ inputs.config.sops.templates."xray-server.json".file ];
|
||||
};
|
||||
xray-stat =
|
||||
{
|
||||
script =
|
||||
let
|
||||
xray = "${inputs.pkgs.xray}/bin/xray";
|
||||
awk = "${inputs.pkgs.gawk}/bin/awk";
|
||||
curl = "${inputs.pkgs.curl}/bin/curl";
|
||||
jq = "${inputs.pkgs.jq}/bin/jq";
|
||||
sed = "${inputs.pkgs.gnused}/bin/sed";
|
||||
cat = "${inputs.pkgs.coreutils}/bin/cat";
|
||||
token = inputs.config.sops.secrets."telegram/token".path;
|
||||
chat = inputs.config.sops.secrets."telegram/user/chn".path;
|
||||
in
|
||||
''
|
||||
message='${inputs.config.nixos.model.hostname} xray:\n'
|
||||
for i in ${builtins.concatStringsSep " " userList}
|
||||
do
|
||||
upload_bytes=$(${xray} api stats --server=127.0.0.1:6149 \
|
||||
-name "user>>>''${i}@xray.chn.moe>>>traffic>>>uplink" | ${jq} '.stat.value' | ${sed} 's/"//g')
|
||||
[ -z "$upload_bytes" ] && upload_bytes=0
|
||||
download_bytes=$(${xray} api stats --server=127.0.0.1:6149 \
|
||||
-name "user>>>''${i}@xray.chn.moe>>>traffic>>>downlink" | ${jq} '.stat.value' | ${sed} 's/"//g')
|
||||
[ -z "$download_bytes" ] && download_bytes=0
|
||||
traffic_gb=$(echo | ${awk} "{printf \"%.3f\",(''${upload_bytes}+''${download_bytes})/1073741824}")
|
||||
message="$message$i"'\t'"''${traffic_gb}"'G\n'
|
||||
done
|
||||
${curl} -X POST -H 'Content-Type: application/json' \
|
||||
-d "{\"chat_id\": \"$(${cat} ${chat})\", \"text\": \"$message\"}" \
|
||||
https://api.telegram.org/bot$(${cat} ${token})/sendMessage
|
||||
'';
|
||||
serviceConfig = { Type = "oneshot"; User = "v2ray"; Group = "v2ray"; };
|
||||
};
|
||||
};
|
||||
timers.xray-stat =
|
||||
{
|
||||
wantedBy = [ "timers.target" ];
|
||||
timerConfig = { OnCalendar = "*-*-* 0:00:00"; Unit = "xray-stat.service"; };
|
||||
};
|
||||
};
|
||||
users =
|
||||
{
|
||||
users.v2ray =
|
||||
{
|
||||
uid = inputs.config.nixos.user.uid.v2ray;
|
||||
group = "v2ray";
|
||||
extraGroups = [ "telegram" ];
|
||||
isSystemUser = true;
|
||||
};
|
||||
groups =
|
||||
{
|
||||
v2ray.gid = inputs.config.nixos.user.gid.v2ray;
|
||||
telegram.gid = inputs.config.nixos.user.gid.telegram;
|
||||
};
|
||||
};
|
||||
nixos.services =
|
||||
{
|
||||
acme.cert.${server.serverName}.group = inputs.config.users.users.nginx.group;
|
||||
nginx =
|
||||
{
|
||||
enable = true;
|
||||
transparentProxy.map.${server.serverName} = 4726;
|
||||
https.${server.serverName} =
|
||||
{
|
||||
listen.main = { proxyProtocol = false; addToTransparentProxy = false; };
|
||||
location."/".return.return = "400";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user