Заменяет tpws на utunws (форк nfqws под utun+BPF из Flowseal/zapret-mac-discord-youtube) на macOS: движок уровня пакетов вместо socks5-прокси даёт macOS те же 22 стратегии Flowseal, что у Windows и Linux, включая fake-пакеты и десинк UDP/QUIC — раньше tpws этого не умел вовсе. Обход теперь ставится как LaunchDaemon (resources/darwin-helper/), пароль администратора спрашивается при каждом включении/выключении, тот же принцип, что pkexec на Linux. Системный прокси и авто-список hostlist-auto больше не нужны и убраны. README: Windows отмечена как гарантированно рабочая, macOS и Linux — как частично.
269 lines
12 KiB
JavaScript
269 lines
12 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* Кладёт бинарники и данные zapret в resources/zapret/ — оттуда electron-builder
|
||
* положит их в сборку через extraResources.
|
||
*
|
||
* Три источника:
|
||
* • bol-van/zapret — движки (winws.exe + WinDivert, nfqws), версия пинована и
|
||
* сверяется по sha256sum.txt из релиза;
|
||
* • Flowseal/zapret-discord-youtube — подобранные сообществом списки доменов и
|
||
* стратегии winws (переносятся в код генератором `parser-strategies.mjs`,
|
||
* здесь просто данные: списки + fake-пакеты). У Flowseal нет sha256sum.txt —
|
||
* версия фиксируется по sha коммита, на который указывает тег.
|
||
* • Flowseal/zapret-mac-discord-youtube — macOS-порт nfqws (`utunws`, работает через
|
||
* utun+BPF вместо netfilter/WinDivert). Тоже без sha256sum.txt — сумму архива
|
||
* релиза фиксируем сами (см. `FLOWSEAL_MAC_ZIP_SHA256`).
|
||
*
|
||
* Каталог resources/zapret в git не хранится: запустите `npm run zapret:fetch`
|
||
* (или просто `npm run build` — он вызывает скрипт сам).
|
||
*/
|
||
|
||
import { createHash } from 'node:crypto'
|
||
import { execFileSync } from 'node:child_process'
|
||
import { mkdir, mkdtemp, copyFile, readFile, writeFile, rm, chmod } from 'node:fs/promises'
|
||
import { existsSync } from 'node:fs'
|
||
import { tmpdir } from 'node:os'
|
||
import { dirname, join, resolve } from 'node:path'
|
||
import { fileURLToPath } from 'node:url'
|
||
|
||
const ZAPRET_VERSION = 'v72.13'
|
||
const WINDIVERT_VERSION = 'v2.2.2'
|
||
|
||
/** Тег Flowseal, зафиксированный по sha коммита (проверено: `git/refs/tags/1.10.2`). */
|
||
const FLOWSEAL_VERSION = '1.10.2'
|
||
const FLOWSEAL_COMMIT = 'dfd8e613b099676cf2aa7b474ee5923801514dec'
|
||
|
||
/**
|
||
* Релиз Flowseal/zapret-mac-discord-youtube, откуда берём `utunws` — форк nfqws под
|
||
* macOS (utun+BPF вместо netfilter). У репозитория нет sha256sum.txt, поэтому вместо
|
||
* суммы отдельного бинарника фиксируем сумму всего архива релиза — она получена один раз
|
||
* командой `shasum -a 256` по скачанному `ZapretMac-macOS-universal.zip` и проверяется
|
||
* при каждой закачке, чтобы подмена ассета в чужом форке или на CDN не прошла незамеченной.
|
||
*/
|
||
const FLOWSEAL_MAC_VERSION = '1.1.2'
|
||
const FLOWSEAL_MAC_ZIP_SHA256 = '9895b0ec5d13ec05676f755cde71107e72d2661538440c3cc4f06e0e6d47844f'
|
||
|
||
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||
const DEST = join(ROOT, 'resources', 'zapret')
|
||
|
||
const RELEASE = `https://github.com/bol-van/zapret/releases/download/${ZAPRET_VERSION}`
|
||
const ARCHIVE = `zapret-${ZAPRET_VERSION}.tar.gz`
|
||
/** Корневой каталог внутри архива — с ним же начинаются пути в sha256sum.txt. */
|
||
const PREFIX = `zapret-${ZAPRET_VERSION}`
|
||
|
||
/** Что забираем из архива bol-van/zapret: путь внутри архива → путь внутри resources/zapret. */
|
||
const FILES = {
|
||
'binaries/linux-x86_64/nfqws': 'linux/x64/nfqws',
|
||
'binaries/windows-x86_64/winws.exe': 'win32/x64/winws.exe',
|
||
'binaries/windows-x86_64/cygwin1.dll': 'win32/x64/cygwin1.dll',
|
||
'binaries/windows-x86_64/WinDivert.dll': 'win32/x64/WinDivert.dll',
|
||
'binaries/windows-x86_64/WinDivert64.sys': 'win32/x64/WinDivert64.sys',
|
||
'docs/LICENSE.txt': 'LICENSE.zapret.txt'
|
||
}
|
||
|
||
/** Исполняемый бит нужен только там, где мы сами запускаем бинарник. */
|
||
const EXECUTABLE = new Set(['darwin/utunws', 'linux/x64/nfqws'])
|
||
|
||
/**
|
||
* Списки доменов Flowseal → наши короткие имена (используются и в `lists.ts`, и
|
||
* в сгенерированных стратегиях). `*-user.txt` не берём — у нас все списки и так
|
||
* редактируемые, отдельного пользовательского слоя не нужно.
|
||
*/
|
||
const FLOWSEAL_LISTS = {
|
||
'lists/list-general.txt': 'lists/general.txt',
|
||
'lists/list-google.txt': 'lists/google.txt',
|
||
'lists/list-exclude.txt': 'lists/exclude.txt',
|
||
'lists/ipset-exclude.txt': 'lists/ipset-exclude.txt',
|
||
'lists/ipset-all.txt': 'lists/ipset-all.txt'
|
||
}
|
||
|
||
/** Fake-пакеты, реально используемые хотя бы одной стратегией general*.bat. */
|
||
const FLOWSEAL_FAKES = [
|
||
'ACTIVE_DISCORD_UDP.bin',
|
||
'ACTIVE_GAME_UDP.bin',
|
||
'quic_initial_4pda_to.bin',
|
||
'quic_initial_www_google_com.bin',
|
||
'stun.bin',
|
||
'stun2.bin',
|
||
'tls_clienthello_4pda_to.bin',
|
||
'tls_clienthello_max_ru.bin',
|
||
'tls_clienthello_sochi_park.bin',
|
||
'tls_clienthello_www_google_com.bin'
|
||
]
|
||
|
||
async function download(url) {
|
||
const response = await fetch(url, { redirect: 'follow' })
|
||
if (!response.ok) throw new Error(`${response.status} ${response.statusText} — ${url}`)
|
||
return Buffer.from(await response.arrayBuffer())
|
||
}
|
||
|
||
function sha256(buffer) {
|
||
return createHash('sha256').update(buffer).digest('hex')
|
||
}
|
||
|
||
/** sha256sum.txt: строки вида "<hex> zapret-v72.13/binaries/mac64/tpws". */
|
||
function parseChecksums(text) {
|
||
const checksums = new Map()
|
||
for (const line of text.split('\n')) {
|
||
const match = /^([0-9a-f]{64})\s+(\S+)$/.exec(line.trim())
|
||
if (match) checksums.set(match[2], match[1])
|
||
}
|
||
return checksums
|
||
}
|
||
|
||
async function fetchZapretEngines(work) {
|
||
console.log(`zapret ${ZAPRET_VERSION}: скачиваю ${ARCHIVE}`)
|
||
const archivePath = join(work, ARCHIVE)
|
||
await writeFile(archivePath, await download(`${RELEASE}/${ARCHIVE}`))
|
||
|
||
const checksums = parseChecksums((await download(`${RELEASE}/sha256sum.txt`)).toString('utf8'))
|
||
|
||
// tar есть на macOS, Linux и в Windows 10 1803+; распаковываем только нужные пути.
|
||
execFileSync(
|
||
'tar',
|
||
['-xzf', archivePath, '-C', work, ...Object.keys(FILES).map((p) => `${PREFIX}/${p}`)],
|
||
{ stdio: ['ignore', 'ignore', 'inherit'] }
|
||
)
|
||
|
||
for (const [source, target] of Object.entries(FILES)) {
|
||
const extracted = join(work, PREFIX, source)
|
||
const content = await readFile(extracted)
|
||
|
||
// Лицензии в sha256sum.txt не попадают — там только binaries/**.
|
||
const expected = checksums.get(`${PREFIX}/${source}`)
|
||
if (expected) {
|
||
const actual = sha256(content)
|
||
if (actual !== expected) {
|
||
throw new Error(
|
||
`не совпала контрольная сумма ${source}\n ожидалось: ${expected}\n получено: ${actual}`
|
||
)
|
||
}
|
||
}
|
||
|
||
const destination = join(DEST, target)
|
||
await mkdir(dirname(destination), { recursive: true })
|
||
await copyFile(extracted, destination)
|
||
if (EXECUTABLE.has(target)) await chmod(destination, 0o755)
|
||
|
||
console.log(` ${target}${expected ? ' ✓ sha256' : ''}`)
|
||
}
|
||
|
||
// Лицензия WinDivert лежит в своём репозитории, в архив zapret не входит.
|
||
const windivertLicense = await download(
|
||
`https://raw.githubusercontent.com/basil00/WinDivert/${WINDIVERT_VERSION}/LICENSE`
|
||
)
|
||
await writeFile(join(DEST, 'LICENSE.WinDivert.txt'), windivertLicense)
|
||
console.log(' LICENSE.WinDivert.txt')
|
||
}
|
||
|
||
async function fetchFlowsealData(work) {
|
||
console.log(
|
||
`Flowseal ${FLOWSEAL_VERSION} (commit ${FLOWSEAL_COMMIT.slice(0, 12)}): скачиваю списки и fake-пакеты`
|
||
)
|
||
|
||
const archivePath = join(work, 'flowseal.tar.gz')
|
||
// Тарбол по sha коммита, а не по имени тега — тег можно переставить, sha неизменен.
|
||
await writeFile(
|
||
archivePath,
|
||
await download(
|
||
`https://codeload.github.com/Flowseal/zapret-discord-youtube/tar.gz/${FLOWSEAL_COMMIT}`
|
||
)
|
||
)
|
||
|
||
const paths = [
|
||
...Object.keys(FLOWSEAL_LISTS),
|
||
...FLOWSEAL_FAKES.map((name) => `bin/${name}`),
|
||
'LICENSE.txt'
|
||
]
|
||
|
||
// GitHub codeload архивирует репозиторий в каталог "<repo>-<sha>/".
|
||
const prefixOut = execFileSync('tar', ['-tzf', archivePath], { encoding: 'utf8' })
|
||
.split('\n')[0]
|
||
.split('/')[0]
|
||
|
||
execFileSync('tar', ['-xzf', archivePath, '-C', work, ...paths.map((p) => `${prefixOut}/${p}`)], {
|
||
stdio: ['ignore', 'ignore', 'inherit']
|
||
})
|
||
|
||
for (const [source, target] of Object.entries(FLOWSEAL_LISTS)) {
|
||
const destination = join(DEST, target)
|
||
await mkdir(dirname(destination), { recursive: true })
|
||
await copyFile(join(work, prefixOut, source), destination)
|
||
console.log(` ${target}`)
|
||
}
|
||
|
||
const fakesDir = join(DEST, 'fakes')
|
||
await mkdir(fakesDir, { recursive: true })
|
||
for (const name of FLOWSEAL_FAKES) {
|
||
await copyFile(join(work, prefixOut, 'bin', name), join(fakesDir, name))
|
||
console.log(` fakes/${name}`)
|
||
}
|
||
|
||
await copyFile(join(work, prefixOut, 'LICENSE.txt'), join(DEST, 'LICENSE.flowseal.txt'))
|
||
console.log(' LICENSE.flowseal.txt')
|
||
}
|
||
|
||
/** Путь внутри ZapretMac-macOS-universal.zip до самого бинарника. */
|
||
const UTUNWS_ZIP_ENTRY = 'ZapretMac.app/Contents/Resources/Payload/bin/utunws'
|
||
|
||
async function fetchUtunwsEngine(work) {
|
||
console.log(`Flowseal-mac ${FLOWSEAL_MAC_VERSION}: скачиваю ZapretMac-macOS-universal.zip`)
|
||
|
||
const zipPath = join(work, 'zapretmac.zip')
|
||
const zipBuffer = await download(
|
||
`https://github.com/Flowseal/zapret-mac-discord-youtube/releases/download/${FLOWSEAL_MAC_VERSION}/ZapretMac-macOS-universal.zip`
|
||
)
|
||
|
||
const actual = sha256(zipBuffer)
|
||
if (actual !== FLOWSEAL_MAC_ZIP_SHA256) {
|
||
throw new Error(
|
||
`не совпала контрольная сумма ZapretMac-macOS-universal.zip\n` +
|
||
` ожидалось: ${FLOWSEAL_MAC_ZIP_SHA256}\n получено: ${actual}`
|
||
)
|
||
}
|
||
await writeFile(zipPath, zipBuffer)
|
||
|
||
// unzip есть из коробки на macOS и в Git for Windows/WSL; в архиве Flowseal нет sha256sum.txt
|
||
// на отдельные файлы — сумму всего zip уже сверили выше, здесь просто достаём бинарник.
|
||
const utunwsDestination = join(DEST, 'darwin', 'utunws')
|
||
await mkdir(dirname(utunwsDestination), { recursive: true })
|
||
const extracted = execFileSync('unzip', ['-p', zipPath, UTUNWS_ZIP_ENTRY])
|
||
await writeFile(utunwsDestination, extracted)
|
||
await chmod(utunwsDestination, 0o755)
|
||
console.log(` darwin/utunws (${extracted.length} байт, universal x86_64+arm64)`)
|
||
|
||
// LICENSE в архив релиза не входит (только ZapretMac.app) — берём из репозитория по тому
|
||
// же тегу, что и версия релиза, тем же приёмом, что LICENSE.WinDivert.txt выше.
|
||
const license = await download(
|
||
`https://raw.githubusercontent.com/Flowseal/zapret-mac-discord-youtube/${FLOWSEAL_MAC_VERSION}/LICENSE`
|
||
)
|
||
await writeFile(join(DEST, 'LICENSE.flowseal-mac.txt'), license)
|
||
console.log(' LICENSE.flowseal-mac.txt')
|
||
}
|
||
|
||
async function main() {
|
||
const work = await mkdtemp(join(tmpdir(), 'zapret-'))
|
||
|
||
try {
|
||
await fetchZapretEngines(work)
|
||
await fetchFlowsealData(work)
|
||
await fetchUtunwsEngine(work)
|
||
console.log(`Готово: ${DEST}`)
|
||
} finally {
|
||
await rm(work, { recursive: true, force: true })
|
||
}
|
||
}
|
||
|
||
// Повторная сборка не должна каждый раз ходить в сеть.
|
||
if (
|
||
process.argv.includes('--force') ||
|
||
!existsSync(join(DEST, 'darwin', 'utunws')) ||
|
||
!existsSync(join(DEST, 'linux', 'x64', 'nfqws')) ||
|
||
!existsSync(join(DEST, 'win32', 'x64', 'winws.exe')) ||
|
||
!existsSync(join(DEST, 'lists', 'general.txt'))
|
||
) {
|
||
await main()
|
||
} else {
|
||
console.log(`zapret уже распакован в ${DEST} (перекачать: npm run zapret:fetch -- --force)`)
|
||
}
|