diff --git a/.github/workflows/examnet.yml b/.github/workflows/examnet.yml new file mode 100644 index 0000000..9a1ba19 --- /dev/null +++ b/.github/workflows/examnet.yml @@ -0,0 +1,30 @@ +name: 'Examnet' + +on: + pull_request: + push: + branches: + - main + workflow_dispatch: + +jobs: + test-unit: + name: Unit tests + runs-on: ubuntu-latest + defaults: + run: + working-directory: packages/ytl-linux-digabi2-examnet + permissions: + id-token: write + contents: read + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-node@v4 + with: + node-version-file: .nvmrc + cache: npm + cache-dependency-path: packages/ytl-linux-digabi2-examnet/package-lock.json + - name: Install Node.js dependencies + run: npm ci --no-audit --no-fund + - name: Run unit tests + run: npm run test \ No newline at end of file diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..cabf43b --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +24 \ No newline at end of file diff --git a/packages/ytl-linux-digabi2-examnet/.prettierrc b/packages/ytl-linux-digabi2-examnet/.prettierrc new file mode 100644 index 0000000..a01141e --- /dev/null +++ b/packages/ytl-linux-digabi2-examnet/.prettierrc @@ -0,0 +1,7 @@ +{ + "printWidth": 120, + "semi": false, + "singleQuote": true, + "trailingComma": "none", + "arrowParens": "avoid" +} diff --git a/packages/ytl-linux-digabi2-examnet/Makefile b/packages/ytl-linux-digabi2-examnet/Makefile index c83a58e..975144c 100644 --- a/packages/ytl-linux-digabi2-examnet/Makefile +++ b/packages/ytl-linux-digabi2-examnet/Makefile @@ -4,6 +4,9 @@ VERSION := 0.2.1 DEPENDENCIES := \ --depends apt \ --depends dnsmasq \ + --depends dnsutils \ + --depends ipset \ + --depends iptables \ --depends jq \ --depends network-manager \ --depends zenity diff --git a/packages/ytl-linux-digabi2-examnet/package-lock.json b/packages/ytl-linux-digabi2-examnet/package-lock.json new file mode 100644 index 0000000..3435090 --- /dev/null +++ b/packages/ytl-linux-digabi2-examnet/package-lock.json @@ -0,0 +1,905 @@ +{ + "name": "ytl-linux-digabi2-examnet", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ytl-linux-digabi2-examnet", + "devDependencies": { + "@types/node": "^24.10.9", + "execa": "^9.6.1", + "prettier": "^3.8.1", + "tsx": "^4.21.0" + }, + "engines": { + "node": ">=24" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@sec-ant/readable-stream": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", + "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sindresorhus/merge-streams": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", + "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@types/node": { + "version": "24.12.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.2.tgz", + "integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/execa": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-9.6.1.tgz", + "integrity": "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^4.0.0", + "cross-spawn": "^7.0.6", + "figures": "^6.1.0", + "get-stream": "^9.0.0", + "human-signals": "^8.0.1", + "is-plain-obj": "^4.1.0", + "is-stream": "^4.0.1", + "npm-run-path": "^6.0.0", + "pretty-ms": "^9.2.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^4.0.0", + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": "^18.19.0 || >=20.5.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/figures": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", + "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-stream": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.7", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.7.tgz", + "integrity": "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/human-signals": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz", + "integrity": "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/npm-run-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", + "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-ms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", + "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/prettier": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.2.tgz", + "integrity": "sha512-8c3mgTe0ASwWAJK+78dpviD+A8EqhndQPUBpNUIPt6+xWlIigCwfN01lWr9MAede4uqXGTEKeQWTvzb3vjia0Q==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/pretty-ms": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz", + "integrity": "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse-ms": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/strip-final-newline": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz", + "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/yoctocolors": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", + "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/packages/ytl-linux-digabi2-examnet/package.json b/packages/ytl-linux-digabi2-examnet/package.json new file mode 100644 index 0000000..7383ac9 --- /dev/null +++ b/packages/ytl-linux-digabi2-examnet/package.json @@ -0,0 +1,17 @@ +{ + "name": "ytl-linux-digabi2-examnet", + "private": true, + "type": "module", + "engines": { + "node": ">=24" + }, + "scripts": { + "test": "node --import tsx --test \"test/**/*.test.ts\"" + }, + "devDependencies": { + "execa": "^9.6.1", + "prettier": "^3.8.1", + "@types/node": "^24.10.9", + "tsx": "^4.21.0" + } +} \ No newline at end of file diff --git a/packages/ytl-linux-digabi2-examnet/templates/dnsmasq.conf.template b/packages/ytl-linux-digabi2-examnet/templates/dnsmasq.conf.template index 1fd55e0..06d0f0c 100644 --- a/packages/ytl-linux-digabi2-examnet/templates/dnsmasq.conf.template +++ b/packages/ytl-linux-digabi2-examnet/templates/dnsmasq.conf.template @@ -33,8 +33,16 @@ resolv-file=/etc/resolv.conf # need DNS to tell student computers where to go server=/koe.abitti.net/# -# Null-route all other traffic -# This prevents software on the student computer from getting confused by when DNS queries work, but the TCP -# request stalls (since this is not a router) for however long the client timeout is set to; possibly Infinity +# Null-route all other traffic by default. +# +# Note: ytl-linux-digabi2-examnet configures a permanent limited Internet allowlist +# using dnsmasq+ipset+iptables: +# - This line stays enabled to deny DNS answers by default (clients get 0.0.0.0) +# - Allowlisted domains are forwarded to upstream DNS using: +# /etc/dnsmasq.d/ytl-linux-internet-allowlist-server.conf (server=/domain/#) +# - Returned A records for allowlisted domains are added to an ipset using: +# /etc/dnsmasq.d/ytl-linux-internet-allowlist-ipset.conf (ipset=/domain/set) +# - iptables then only forwards/NATs LAN->WAN traffic when the destination IP +# is in that ipset. address=/#/0.0.0.0 address=/#/:: diff --git a/packages/ytl-linux-digabi2-examnet/test/examnet.test.ts b/packages/ytl-linux-digabi2-examnet/test/examnet.test.ts new file mode 100644 index 0000000..629acfc --- /dev/null +++ b/packages/ytl-linux-digabi2-examnet/test/examnet.test.ts @@ -0,0 +1,558 @@ +import assert from 'node:assert/strict' +import { test, describe, beforeEach } from 'node:test' +import { execa } from 'execa' +import { join } from 'node:path' +import { mkdtemp, writeFile, chmod, readFile, mkdir, truncate, access } from 'node:fs/promises' +import { tmpdir } from 'node:os' + +describe('examnet', async () => { + let callsLog + let mockBinDir + let mockConfigDir + let mockTemplatesDir + let mockResolvedDir + let mockDockerDir + let mockDnsmasqDir + let mockNaksu2WorkDir + let mockNaksu2CertsDir + + beforeEach(async () => { + await truncateCallsLog() + ;({ + callsLog, + mockBinDir, + mockConfigDir, + mockTemplatesDir, + mockResolvedDir, + mockDockerDir, + mockDnsmasqDir, + mockNaksu2WorkDir, + mockNaksu2CertsDir + } = await initTempDir()) + }) + + function runExamnet(netDeviceWan: string, netDeviceLan: string, serverNumber: number, serverFriendlyName?: string) { + return execa( + './ytl-linux-digabi2-examnet', + [netDeviceWan, netDeviceLan, `${serverNumber}`, serverFriendlyName, '--not-root'].filter(Boolean), + { + env: { + ...process.env, + PATH: `${mockBinDir}:${process.env.PATH}`, + CALLS_LOG: callsLog, + PATH_EXAMNET_CONFIG: mockConfigDir, + PATH_TEMPLATES: mockTemplatesDir, + PATH_RESOLVED: mockResolvedDir, + PATH_DOCKER: mockDockerDir, + PATH_DNSMASQ: mockDnsmasqDir, + NAKSU2_WORKDIR: mockNaksu2WorkDir + }, + detached: true + } + ) + } + + describe('bouncer', async () => { + test('daemon starts when correct parameters are given', async () => { + await writeToTempDir(mockConfigDir, 'net-device-lan', 'eth0') + await writeToTempDir(mockConfigDir, 'server-friendly-name', 'foobar') + + // do not await runExamnet, as it stays running in daemon mode + const subprocess = runExamnet('eth0', 'eth1', 1, '--daemon') + await waitForLogEntry(callsLog, '"ytl-linux-digabi2-bouncer"') + await assertCalls([callStat(mockNaksu2WorkDir), callIpAddrShow('eth0'), callBouncer(mockNaksu2CertsDir)]) + await killSubprocess(subprocess) + }) + }) + + describe('restart-bouncer', () => { + test('runs when correct parameters are given', async () => { + await runExamnet('eth0', 'eth1', 1, '--restart-daemon') + await assertCalls([ + callStat(mockNaksu2WorkDir), + callSystemctl('is-enabled', 'ytl-linux-digabi2-examnet.service'), + callSystemctl('is-enabled', 'ytl-linux-digabi2-examnet-discovery.timer'), + callSystemctl('is-enabled', 'ytl-linux-digabi2-examnet-discovery.service') + ]) + }) + }) + + describe('discovery', () => { + test('runs when correct parameters are given', async () => { + await runExamnet('eth0', 'eth1', 1, '--discovery') + await assertCalls([callStat(mockNaksu2WorkDir), callDiscovery(mockDnsmasqDir, mockConfigDir)]) + }) + }) + + describe('setup', () => { + test('runs when correct parameters are given', async () => { + await runExamnet('eth0', 'eth1', 1) + await assertCalls([ + callStat(mockNaksu2WorkDir), + callIpLinkShow('eth0'), + callIpLinkShow('eth1'), + callIpAddrShow('eth0'), + callIpAddrShow('eth1'), + callNmicliConnectionDelete('yo-eth1'), + callNmicliConnectionAdd('yo-eth1', '192.168.10.1/16'), + callNmicliConnectionModify('yo-eth1'), + callNmicliConnectionUp('yo-eth1'), + callSystemctl('restart', 'NetworkManager.service'), + callNmonline(), + callIpsetCreate('ytl_internet_allowlist'), + callIptablesNewChain('YTL_LAN_WAN_IPSET_LOG'), + callIptablesFlushChain('YTL_LAN_WAN_IPSET_LOG'), + { + cmd: 'iptables', + argv: [ + '-t', + 'filter', + '-A', + 'YTL_LAN_WAN_IPSET_LOG', + '-m', + 'conntrack', + '--ctstate', + 'NEW', + '-m', + 'set', + '--match-set', + 'ytl_internet_allowlist', + 'dst', + '-m', + 'limit', + '--limit', + '10/second', + '--limit-burst', + '30', + '-j', + 'LOG', + '--log-prefix', + 'YTL ALLOW NEW ', + '--log-level', + '6' + ] + }, + callIptablesAppendRule('YTL_LAN_WAN_IPSET_LOG', 'RETURN'), + callIptablesNewChain('YTL_LAN_WAN_IPSET'), + callIptablesFlushChain('YTL_LAN_WAN_IPSET'), + { + cmd: 'iptables', + argv: [ + '-t', + 'filter', + '-A', + 'YTL_LAN_WAN_IPSET', + '-m', + 'set', + '--match-set', + 'ytl_internet_allowlist', + 'dst', + '-j', + 'ACCEPT' + ] + }, + callIptablesAppendRule('YTL_LAN_WAN_IPSET', 'DROP'), + { + cmd: 'iptables', + argv: [ + '-t', + 'filter', + '-C', + 'FORWARD', + '-i', + 'eth0', + '-o', + 'eth1', + '-m', + 'conntrack', + '--ctstate', + 'RELATED,ESTABLISHED', + '-j', + 'ACCEPT' + ] + }, + callIptablesCheckChain('FORWARD', 'eth1', 'YTL_LAN_WAN_IPSET_LOG'), + callIptablesCheckChain('FORWARD', 'eth1', 'YTL_LAN_WAN_IPSET'), + { + cmd: 'iptables', + argv: [ + '-t', + 'nat', + '-C', + 'POSTROUTING', + '-o', + 'eth0', + '-m', + 'set', + '--match-set', + 'ytl_internet_allowlist', + 'dst', + '-j', + 'MASQUERADE' + ] + }, + callSystemctl('is-enabled', 'dnsmasq.service'), + callDig('endpoint.security.microsoft.com'), + callDig('smartscreen-prod.microsoft.com'), + callDig('smartscreen.microsoft.com'), + callSystemctl('enable', 'ytl-linux-digabi2-examnet.service'), + callSystemctl('enable', 'dnsmasq.service'), + callSystemctl('enable', 'ytl-linux-digabi2-examnet-discovery.service'), + callSystemctl('enable', 'ytl-linux-digabi2-examnet-discovery.timer'), + callSystemctl('restart', 'systemd-resolved'), + callSystemctl('is-enabled', 'dnsmasq.service'), + callSystemctl('is-enabled', 'ytl-linux-digabi2-examnet.service'), + callSystemctl('is-enabled', 'ytl-linux-digabi2-examnet-discovery.timer'), + callSystemctl('is-enabled', 'ytl-linux-digabi2-examnet-discovery.service'), + { cmd: 'ytl-linux-digabi2-docker-configure.sh', argv: ['127.0.0.1', '192.168.10.1'] } + ]) + }) + }) + + describe('destroy', () => { + test('runs when correct parameters are given', async () => { + await runExamnet('eth0', 'eth1', 1) + await assertCalls([ + callStat(mockNaksu2WorkDir), + callIpLinkShow('eth0'), + callIpLinkShow('eth1'), + callIpAddrShow('eth0'), + callIpAddrShow('eth1'), + callNmicliConnectionDelete('yo-eth1'), + callNmicliConnectionAdd('yo-eth1', '192.168.10.1/16'), + callNmicliConnectionModify('yo-eth1'), + callNmicliConnectionUp('yo-eth1'), + callSystemctl('restart', 'NetworkManager.service'), + callNmonline(), + callIpsetCreate('ytl_internet_allowlist'), + callIptablesNewChain('YTL_LAN_WAN_IPSET_LOG'), + callIptablesFlushChain('YTL_LAN_WAN_IPSET_LOG'), + { + cmd: 'iptables', + argv: [ + '-t', + 'filter', + '-A', + 'YTL_LAN_WAN_IPSET_LOG', + '-m', + 'conntrack', + '--ctstate', + 'NEW', + '-m', + 'set', + '--match-set', + 'ytl_internet_allowlist', + 'dst', + '-m', + 'limit', + '--limit', + '10/second', + '--limit-burst', + '30', + '-j', + 'LOG', + '--log-prefix', + 'YTL ALLOW NEW ', + '--log-level', + '6' + ] + }, + callIptablesAppendRule('YTL_LAN_WAN_IPSET_LOG', 'RETURN'), + callIptablesNewChain('YTL_LAN_WAN_IPSET'), + callIptablesFlushChain('YTL_LAN_WAN_IPSET'), + { + cmd: 'iptables', + argv: [ + '-t', + 'filter', + '-A', + 'YTL_LAN_WAN_IPSET', + '-m', + 'set', + '--match-set', + 'ytl_internet_allowlist', + 'dst', + '-j', + 'ACCEPT' + ] + }, + callIptablesAppendRule('YTL_LAN_WAN_IPSET', 'DROP'), + { + cmd: 'iptables', + argv: [ + '-t', + 'filter', + '-C', + 'FORWARD', + '-i', + 'eth0', + '-o', + 'eth1', + '-m', + 'conntrack', + '--ctstate', + 'RELATED,ESTABLISHED', + '-j', + 'ACCEPT' + ] + }, + callIptablesCheckChain('FORWARD', 'eth1', 'YTL_LAN_WAN_IPSET_LOG'), + callIptablesCheckChain('FORWARD', 'eth1', 'YTL_LAN_WAN_IPSET'), + { + cmd: 'iptables', + argv: [ + '-t', + 'nat', + '-C', + 'POSTROUTING', + '-o', + 'eth0', + '-m', + 'set', + '--match-set', + 'ytl_internet_allowlist', + 'dst', + '-j', + 'MASQUERADE' + ] + }, + callSystemctl('is-enabled', 'dnsmasq.service'), + callDig('endpoint.security.microsoft.com'), + callDig('smartscreen-prod.microsoft.com'), + callDig('smartscreen.microsoft.com'), + callSystemctl('enable', 'ytl-linux-digabi2-examnet.service'), + callSystemctl('enable', 'dnsmasq.service'), + callSystemctl('enable', 'ytl-linux-digabi2-examnet-discovery.service'), + callSystemctl('enable', 'ytl-linux-digabi2-examnet-discovery.timer'), + callSystemctl('restart', 'systemd-resolved'), + callSystemctl('is-enabled', 'dnsmasq.service'), + callSystemctl('is-enabled', 'ytl-linux-digabi2-examnet.service'), + callSystemctl('is-enabled', 'ytl-linux-digabi2-examnet-discovery.timer'), + callSystemctl('is-enabled', 'ytl-linux-digabi2-examnet-discovery.service'), + { cmd: 'ytl-linux-digabi2-docker-configure.sh', argv: ['127.0.0.1', '192.168.10.1'] } + ]) + }) + }) + + async function truncateCallsLog() { + try { + await access(callsLog) + await truncate(callsLog) + } catch { + // callsLog doesn't exist yet + } + } + + async function bashWrapMockScript(mockScriptTSPath: string) { + return `#!/usr/bin/env bash + set -euo pipefail + node --import tsx ${shellQuote(mockScriptTSPath)} "$0" "$@" + ` + } + + function shellQuote(str: string) { + return `'${str.replace(/'/g, `'\\''`)}'` + } + + async function initTempDir() { + const root = await mkdtemp(join(tmpdir(), 'just-test-')) + + const callsLog = join(root, 'calls.log') + + const mockBinDir = await makeTempDir(root, 'mock-bin-dir') + const mockConfigDir = await makeTempDir(root, 'mock-config-dir') + const mockTemplatesDir = await makeTempDir(root, 'mock-templates-dir') + const mockResolvedDir = await makeTempDir(root, 'mock-resolved-dir') + const mockDockerDir = await makeTempDir(root, 'mock-docker-dir') + const mockDnsmasqDir = await makeTempDir(root, 'mock-dnsmasq-dir') + const mockNaksu2WorkDir = await makeTempDir(root, 'naksu2-work-dir') + const mockNaksu2CertsDir = await makeTempDir(mockNaksu2WorkDir, 'certs') + + await writeToTempDir(mockNaksu2CertsDir, 'domain.txt', 'kamreeri-kelvokas.koe.abitti.net') + const mockTsPath = join(process.cwd(), 'test', 'mock-script.ts') + const mockScript = await bashWrapMockScript(mockTsPath) + await writeToTempDir(mockBinDir, 'echo', mockScript) + await writeToTempDir(mockBinDir, 'nmcli', mockScript) + await writeToTempDir(mockBinDir, 'systemctl', mockScript) + await writeToTempDir(mockBinDir, 'nm-online', mockScript) + await writeToTempDir(mockBinDir, 'ip', mockScript) + await writeToTempDir(mockBinDir, 'iptables', mockScript) + await writeToTempDir(mockBinDir, 'ipset', mockScript) + await writeToTempDir(mockBinDir, 'dig', mockScript) + await writeToTempDir(mockBinDir, 'stat', mockScript) + await writeToTempDir(mockBinDir, 'ytl-linux-digabi2-bouncer', mockScript) + await writeToTempDir(mockBinDir, 'ytl-linux-digabi2-discovery', mockScript) + await writeToTempDir(mockBinDir, 'ytl-linux-digabi2-docker-configure.sh', mockScript) + + await writeToTempDir(mockConfigDir, 'ncsi-hostnames', 'example.com') + await writeToTempDir(mockConfigDir, 'server-own-ip', '127.0.0.1') + await writeToTempDir(mockTemplatesDir, 'resolved.conf.template', 'foobar') + await writeToTempDir(mockTemplatesDir, 'docker-daemon.json.template', 'foobar') + await writeToTempDir(mockTemplatesDir, 'dnsmasq.conf.template', 'foobar') + await writeToTempDir(mockDnsmasqDir, 'ytl-linux-static-dns-records.conf', 'xyzzy') + return { + callsLog, + mockBinDir, + mockConfigDir, + mockTemplatesDir, + mockResolvedDir, + mockDockerDir, + mockDnsmasqDir, + mockNaksu2WorkDir, + mockNaksu2CertsDir + } + } + + async function assertCalls(expectedCalls: Object[]) { + const calls = (await readFile(callsLog, 'utf8')).trim() + const callsLines = calls.split('\n') + // console.log(`expecting ${expectedCalls.length} calls to external programs`) + const callsArray = callsLines.map(line => { + // console.log(`parsing line ${line}`) + return JSON.parse(line) + }) + assert.deepEqual(callsArray, expectedCalls) + } +}) + +async function writeToTempDir(dir: string, name: string, script: string) { + const file = join(dir, name) + await writeFile(file, script, 'utf8') + await chmod(file, 0o755) + return file +} + +async function makeTempDir(root: string, name: string) { + const dir = join(root, name) + await mkdir(dir, { recursive: true }) + return dir +} + +async function waitForLogEntry(callsLog: string, msg: string) { + const start = Date.now() + while (Date.now() - start < 5000) { + try { + const contents = await readFile(callsLog, 'utf8') + if (contents.includes(msg)) { + break + } + } catch { + // file may not exist yet + } + await new Promise(r => setTimeout(r, 100)) + } +} + +async function killSubprocess(subprocess: any) { + let killTimer + try { + process.kill(-subprocess.pid!, 'SIGTERM') + + killTimer = setTimeout(() => { + try { + process.kill(-subprocess.pid!, 'SIGKILL') + } catch {} + }, 1000) + await Promise.race([subprocess.catch(() => {}), new Promise(r => setTimeout(r, 3000))]) + } finally { + if (killTimer) clearTimeout(killTimer) + } +} + +function callStat(mockNaksu2WorkDir) { + return { cmd: 'stat', argv: ['-c', '%U:%G', mockNaksu2WorkDir] } +} + +function callBouncer(mockNaksu2CertsDir: string) { + return { + cmd: 'ytl-linux-digabi2-bouncer', + argv: [ + ` { "config": { "friendlyName": "foobar", "canonicalHostname": "kamreeri-kelvokas.koe.abitti.net", "ncsiHostnames": ["example.com"], "searchDomain": "internal", "serverOwnIp": "127.0.0.1", "ports": {"discovery": 26464, "bouncer": 80} }, "secrets": {"cert":"${mockNaksu2CertsDir}/fullchain.pem","key":"${mockNaksu2CertsDir}/key.pem"} }` + ] + } +} + +function callDiscovery(mockDnsmasqDir: string, mockConfigDir: string) { + return { + cmd: 'ytl-linux-digabi2-discovery', + argv: [ + ` {\n "config": {\n "isProd": true,\n "ktpDomains": [],\n "dnsmasqConfigOutputFile": "${mockDnsmasqDir}/ytl-linux-ktp-aliases.conf",\n "ports": {"discovery": 26464},\n "dbPath": "${mockConfigDir}/discovery.db"\n }\n }` + ] + } +} + +function callIpLinkShow(networkDevice: string) { + return { cmd: 'ip', argv: ['link', 'show', networkDevice] } +} +function callIpAddrShow(networkDevice: string) { + return { cmd: 'ip', argv: ['-oneline', '-4', 'addr', 'show', 'scope', 'global', networkDevice] } +} + +function callSystemctl(cmd: string, service: string) { + return { cmd: 'systemctl', argv: [cmd, service] } +} + +function callDig(host: string) { + return { cmd: 'dig', argv: ['+time=1', '+tries=1', '@127.0.0.1', host, 'A'] } +} + +function callNmicliConnectionDelete(connectionName: string) { + return { cmd: 'nmcli', argv: ['connection', 'delete', connectionName] } +} + +function callNmicliConnectionModify(connectionName: string) { + return { cmd: 'nmcli', argv: ['connection', 'modify', connectionName, 'ipv6.method', 'disabled'] } +} + +function callNmicliConnectionAdd(connectionName: string, ipRange: string) { + return { + cmd: 'nmcli', + argv: [ + 'connection', + 'add', + 'type', + 'ethernet', + 'ifname', + 'eth1', + 'con-name', + connectionName, + 'ip4', + ipRange, + 'autoconnect', + 'yes', + 'save', + 'yes' + ] + } +} + +function callNmicliConnectionUp(deviceName: string) { + return { cmd: 'nmcli', argv: ['connection', 'up', deviceName] } +} + +function callNmonline() { + return { cmd: 'nm-online', argv: ['-s', '-q', '--timeout=30'] } +} + +function callIpsetCreate(listName: string) { + return { cmd: 'ipset', argv: ['create', listName, 'hash:ip', 'timeout', '3600', '-exist'] } +} + +function callIptablesNewChain(chainName: string) { + return { cmd: 'iptables', argv: ['-t', 'filter', '-N', chainName] } +} + +function callIptablesFlushChain(chainName: string) { + return { cmd: 'iptables', argv: ['-t', 'filter', '-F', chainName] } +} + +function callIptablesCheckChain(chainName: string, networkDevice: string, jumpTarget: string) { + return { + cmd: 'iptables', + argv: ['-t', 'filter', '-C', chainName, '-i', networkDevice, '-o', 'eth0', '-j', jumpTarget] + } +} + +function callIptablesAppendRule(chainName: string, jumpTarget: string) { + return { cmd: 'iptables', argv: ['-t', 'filter', '-A', chainName, '-j', jumpTarget] } +} diff --git a/packages/ytl-linux-digabi2-examnet/test/mock-script.ts b/packages/ytl-linux-digabi2-examnet/test/mock-script.ts new file mode 100644 index 0000000..863e5e5 --- /dev/null +++ b/packages/ytl-linux-digabi2-examnet/test/mock-script.ts @@ -0,0 +1,30 @@ +// +// This script emulates the behavior of external programs called by the examnet script +// + +import fs from 'node:fs' +import path from 'node:path' + +async function main() { + // process.argv[2] is the name of the program being called + const cmd = path.basename(process.argv[2]) + const entry = { + cmd, + argv: process.argv.slice(3) + } + fs.appendFileSync(process.env.CALLS_LOG, JSON.stringify(entry) + '\n') + switch (cmd) { + case 'ip': + console.log(process.argv[9] === 'eth0' ? '127.0.0.1' : '') + break + case 'stat': + console.log('nobody:nobody') + break + case 'ytl-linux-digabi2-bouncer': + // simulate a daemon that stays running by waiting for two minutes + await new Promise(resolve => setTimeout(resolve, 120_000)) + break + } +} + +main() diff --git a/packages/ytl-linux-digabi2-examnet/tsconfig.json b/packages/ytl-linux-digabi2-examnet/tsconfig.json new file mode 100644 index 0000000..a786b93 --- /dev/null +++ b/packages/ytl-linux-digabi2-examnet/tsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "esModuleInterop": true + } +} \ No newline at end of file diff --git a/packages/ytl-linux-digabi2-examnet/ytl-linux-digabi2-examnet b/packages/ytl-linux-digabi2-examnet/ytl-linux-digabi2-examnet index bee2bca..4aef664 100755 --- a/packages/ytl-linux-digabi2-examnet/ytl-linux-digabi2-examnet +++ b/packages/ytl-linux-digabi2-examnet/ytl-linux-digabi2-examnet @@ -1,5 +1,13 @@ #!/usr/bin/env bash +# These domains and their subdomains are allowed from LAN to WAN for Windows Defender cloud verification. +# Keep this list minimal. +readonly INTERNET_ALLOWLIST_DOMAINS=( + "endpoint.security.microsoft.com" + "smartscreen-prod.microsoft.com" + "smartscreen.microsoft.com" +) + # Exit codes readonly EXIT_CODE_MUST_BE_ROOT=1 # The script must be executed by root or sudo readonly EXIT_CODE_NETWORK_DEVICE_NAME_MISSING_WAN=2 # Network device name (internet) is missing @@ -41,51 +49,44 @@ readonly FRIENDLY_NAME_SEARCH_DOMAIN="internal" readonly DISCOVERY_PORT=26464 readonly BOUNCER_PORT=80 -readonly PATH_TEMPLATES=/etc/ytl-linux-digabi2-examnet/templates -readonly PATH_RESOLVED=/etc/systemd/resolved.conf.d +readonly PATH_TEMPLATES=${PATH_TEMPLATES:-/etc/ytl-linux-digabi2-examnet/templates} +readonly PATH_RESOLVED=${PATH_RESOLVED:-/etc/systemd/resolved.conf.d} readonly PATH_RESOLVED_CONF=$PATH_RESOLVED/ytl-linux.conf readonly PATH_RESOLVED_CONF_TEMPLATE=$PATH_TEMPLATES/resolved.conf.template -readonly PATH_DNSMASQ=/etc/dnsmasq.d +readonly PATH_DNSMASQ=${PATH_DNSMASQ:-/etc/dnsmasq.d} readonly PATH_DNSMASQ_CONF=$PATH_DNSMASQ/ytl-linux.conf readonly PATH_DNSMASQ_CONF_TEMPLATE=$PATH_TEMPLATES/dnsmasq.conf.template readonly PATH_DNSMASQ_STATIC_DNS_CONF=$PATH_DNSMASQ/ytl-linux-static-dns-records.conf readonly PATH_DNSMASQ_KTP_ALIASES_CONF=$PATH_DNSMASQ/ytl-linux-ktp-aliases.conf +readonly PATH_DNSMASQ_IPSET_ALLOWLIST_CONF=$PATH_DNSMASQ/ytl-linux-internet-allowlist-ipset.conf +readonly PATH_DNSMASQ_SERVER_ALLOWLIST_CONF=$PATH_DNSMASQ/ytl-linux-internet-allowlist-server.conf +readonly PATH_DOCKER=${PATH_DOCKER:-/etc/docker} +readonly PATH_DOCKER_DAEMON_CONF=$PATH_DOCKER/daemon.json +readonly PATH_DOCKER_DAEMON_CONF_TEMPLATE=$PATH_TEMPLATES/docker-daemon.json.template readonly PATH_NAKSU2_WORKDIR="${NAKSU2_WORKDIR:-/home/school/.local/share/digabi/naksu2}" readonly PATH_NAKSU2_CERTS_DIR="$PATH_NAKSU2_WORKDIR/certs" readonly PATH_NAKSU2_CERT="$PATH_NAKSU2_CERTS_DIR/cert.pem" readonly PATH_NAKSU2_DOMAIN="$PATH_NAKSU2_CERTS_DIR/domain.txt" -readonly PATH_EXAMNET_CONFIG=/etc/ytl-linux-digabi2-examnet/config/ +readonly PATH_EXAMNET_CONFIG="${PATH_EXAMNET_CONFIG:-/etc/ytl-linux-digabi2-examnet/config/}" readonly PATH_SERVER_FRIENDLY_NAME_CONF=$PATH_EXAMNET_CONFIG/server-friendly-name readonly PATH_NET_DEVICE_LAN_CONF=$PATH_EXAMNET_CONFIG/net-device-lan readonly PATH_NET_DEVICE_WAN_CONF=$PATH_EXAMNET_CONFIG/net-device-wan readonly PATH_SERVER_OWN_IP=$PATH_EXAMNET_CONFIG/server-own-ip readonly PATH_DISCOVERY_DB=$PATH_EXAMNET_CONFIG/discovery.db -readonly BIN_ECHO=/usr/bin/echo -readonly BIN_GREP=/usr/bin/grep -readonly BIN_TR=/usr/bin/tr -readonly BIN_CUT=/usr/bin/cut -readonly BIN_IP=/usr/sbin/ip -readonly BIN_MKDIR=/usr/bin/mkdir -readonly BIN_SYSTEMCTL=/usr/bin/systemctl -readonly BIN_NMCLI=/usr/bin/nmcli -readonly BIN_NM_ONLINE=/usr/bin/nm-online -readonly BIN_SED=/usr/bin/sed -readonly BIN_XARGS=/usr/bin/xargs -readonly BIN_OPENSSL=/usr/bin/openssl -readonly BIN_DIGABI2_EXAMNET_BOUNCER=/usr/local/libexec/ytl-linux-digabi2-examnet/bouncer -readonly BIN_DIGABI2_EXAMNET_DISCOVERY=/usr/local/libexec/ytl-linux-digabi2-examnet/discovery -readonly BIN_STAT=/usr/bin/stat -readonly BIN_CHOWN=/usr/bin/chown - readonly CONST_MIN_SERVER_NUMBER=1 readonly CONST_MAX_SERVER_NUMBER=24 readonly CONST_SUBNETS_PER_SERVER=10 +readonly INTERNET_ALLOWLIST_IPSET_NAME="ytl_internet_allowlist" +readonly INTERNET_ALLOWLIST_FILTER_CHAIN="YTL_LAN_WAN_IPSET" +readonly INTERNET_ALLOWLIST_LOG_CHAIN="YTL_LAN_WAN_IPSET_LOG" +readonly INTERNET_ALLOWLIST_IPSET_TIMEOUT_SECONDS=3600 + function get_dir_owner() { _DIR=$1 if [ -d "$_DIR" ]; then - $BIN_STAT -c '%U:%G' "$_DIR" + stat -c '%U:%G' "$_DIR" fi } @@ -93,9 +94,9 @@ readonly NAKSU2_WORKDIR_OWNER=$(get_dir_owner "$PATH_NAKSU2_WORKDIR") function debug_path() { if [ -z "$DEBUG" ]; then - $BIN_ECHO "/dev/null" + echo "/dev/null" else - $BIN_ECHO "$DEBUG" + echo "$DEBUG" fi } @@ -104,29 +105,29 @@ readonly PATH_DEBUG function debug() { _DEBUG_MESSAGE=$1 - $BIN_ECHO "$SCRIPT_PATH DEBUG: $_DEBUG_MESSAGE" >>"$PATH_DEBUG" + echo "$SCRIPT_PATH DEBUG: $_DEBUG_MESSAGE" #>>"$PATH_DEBUG" } function print_error() { _ERROR_MESSAGE=$1 debug "ERROR: $_ERROR_MESSAGE" - $BIN_ECHO "$SCRIPT_PATH error: $_ERROR_MESSAGE" >&2 + echo "$SCRIPT_PATH error: $_ERROR_MESSAGE" >&2 } function print_info() { _INFO_MESSAGE=$1 debug "INFO: $_INFO_MESSAGE" - $BIN_ECHO "$SCRIPT_PATH info: $_INFO_MESSAGE" + echo "$SCRIPT_PATH info: $_INFO_MESSAGE" } function print_usage() { - $BIN_ECHO "usage: $SCRIPT_PATH wan-network-device local-network-device server-number [custom-server-name] [--use-static-local-dns]" >&2 - $BIN_ECHO " $SCRIPT_PATH --daemon" >&2 - $BIN_ECHO " $SCRIPT_PATH --restart-daemon" >&2 - $BIN_ECHO " $SCRIPT_PATH --discover" >&2 - $BIN_ECHO " $SCRIPT_PATH --remove" >&2 - $BIN_ECHO "" >&2 - $BIN_ECHO "example: $SCRIPT_PATH eth0 eth1 1" >&2 + echo "usage: $SCRIPT_PATH wan-network-device local-network-device server-number [custom-server-name] [--use-static-local-dns] [--configure-only-static-local-dns] [--not-root]" >&2 + echo " $SCRIPT_PATH --daemon" >&2 + echo " $SCRIPT_PATH --restart-daemon" >&2 + echo " $SCRIPT_PATH --discover" >&2 + echo " $SCRIPT_PATH --remove" >&2 + echo "" >&2 + echo "example: $SCRIPT_PATH eth0 eth1 1" >&2 } function exit_script() { @@ -164,14 +165,14 @@ function network_enumerate_devices () { function network_device_exists() { _DEVICE=$1 - $BIN_IP link show "$_DEVICE" &> /dev/null && $BIN_ECHO "1" + ip link show "$_DEVICE" &> /dev/null && echo "1" } function get_ipv4_address() { _DEVICE=$1 - _IP=$($BIN_IP -oneline -4 addr show scope global "$_DEVICE" | $BIN_TR -s ' ' | $BIN_TR '/' ' ' | $BIN_CUT -f 4 -d ' ') + _IP=$(ip -oneline -4 addr show scope global "$_DEVICE" | tr -s ' ' | tr '/' ' ' | cut -f 4 -d ' ') if [[ ! "$_IP" =~ "does not exist" ]]; then - $BIN_ECHO "$_IP" + echo "$_IP" fi } @@ -215,7 +216,7 @@ function check_network_device_names() { function server_number_is_valid() { _SERVER_NUMBER=$1 if [[ "$_SERVER_NUMBER" =~ ^[0-9]+$ ]] && [ "$_SERVER_NUMBER" -ge $CONST_MIN_SERVER_NUMBER ] && [ "$_SERVER_NUMBER" -le $CONST_MAX_SERVER_NUMBER ]; then - $BIN_ECHO "1" + echo "1" fi } @@ -239,9 +240,9 @@ function get_lan_ip_prefix() { _IP_WAN=$1 if [[ "$_IP_WAN" =~ ^192\.168\. ]]; then - $BIN_ECHO "10.0." + echo "10.0." else - $BIN_ECHO "192.168." + echo "192.168." fi } @@ -253,21 +254,21 @@ function write_file() { _FILE_PATH=$(dirname "$_FILE_FILENAME") if [ ! -d "$_FILE_PATH" ]; then debug "Path $_FILE_PATH is missing, creating" - $BIN_MKDIR -p "$_FILE_PATH" + mkdir -p "$_FILE_PATH" exit_if_error $? $EXIT_CODE_BAD_FILE_PATH "Failed to create directory $_FILE_PATH" if [ -n "$_FILE_OWNER" ]; then - $BIN_CHOWN "$_FILE_OWNER" "$_FILE_PATH" + chown "$_FILE_OWNER" "$_FILE_PATH" fi else debug "Path $_FILE_PATH exists" fi debug "$_FILE_FILENAME: $_FILE_CONTENT" - $BIN_ECHO -e "$_FILE_CONTENT" >"$_FILE_FILENAME" + echo -e "$_FILE_CONTENT" >"$_FILE_FILENAME" exit_if_error $? $EXIT_CODE_CANNOT_WRITE "Failed to write to $_FILE_FILENAME" if [ -n "$_FILE_OWNER" ]; then - $BIN_CHOWN "$_FILE_OWNER" "$_FILE_FILENAME" + chown "$_FILE_OWNER" "$_FILE_FILENAME" fi } @@ -277,12 +278,12 @@ function configure_networkmanager() { _IP_AND_NETMASK=$3 debug "Deleting existing NetworkManager connection '$_CONNECTION_NAME'" - $BIN_NMCLI connection delete "$_CONNECTION_NAME" >>"$PATH_DEBUG" 2>&1 + nmcli connection delete "$_CONNECTION_NAME" >>"$PATH_DEBUG" 2>&1 # We are not checking the exit code as this command fails if the connection does not exist # This is quite normal if the script has been executed before debug "Adding NetworkManager connection '$_CONNECTION_NAME'" - $BIN_NMCLI connection add \ + nmcli connection add \ type ethernet \ ifname "$_INTERFACE_NAME" \ con-name "$_CONNECTION_NAME" \ @@ -292,11 +293,11 @@ function configure_networkmanager() { exit_if_error $? $EXIT_CODE_CANNOT_RESTART_LAN_DEVICE "NetworkManager CLI: connection add $_CONNECTION_NAME failed" debug "Disabling IPv6 in connection '$_CONNECTION_NAME'" - $BIN_NMCLI connection modify "$_CONNECTION_NAME" ipv6.method "disabled" >>"$PATH_DEBUG" 2>&1 + nmcli connection modify "$_CONNECTION_NAME" ipv6.method "disabled" >>"$PATH_DEBUG" 2>&1 exit_if_error $? $EXIT_CODE_CANNOT_RESTART_LAN_DEVICE "NetworkManager CLI: IPv6 disable failed" debug "Starting connection '$_CONNECTION_NAME'" - $BIN_NMCLI connection up "$_CONNECTION_NAME" >>"$PATH_DEBUG" 2>&1 + nmcli connection up "$_CONNECTION_NAME" >>"$PATH_DEBUG" 2>&1 exit_if_error $? $EXIT_CODE_CANNOT_RESTART_LAN_DEVICE "Failed to start NetworkManager connection '$_CONNECTION_NAME'" } @@ -310,6 +311,101 @@ function remove_dnsmasq_settings() { rm -f $PATH_DNSMASQ_STATIC_DNS_CONF exit_if_error $? $EXIT_CODE_CANNOT_REMOVE_FILES "Failed to remove system file '$PATH_DNSMASQ_STATIC_DNS_CONF'" fi + + if [ -f $PATH_DNSMASQ_KTP_ALIASES_CONF ]; then + rm -f $PATH_DNSMASQ_KTP_ALIASES_CONF + exit_if_error $? $EXIT_CODE_CANNOT_REMOVE_FILES "Failed to remove system file '$PATH_DNSMASQ_KTP_ALIASES_CONF'" + fi + + if [ -f $PATH_DNSMASQ_IPSET_ALLOWLIST_CONF ]; then + rm -f $PATH_DNSMASQ_IPSET_ALLOWLIST_CONF + exit_if_error $? $EXIT_CODE_CANNOT_REMOVE_FILES "Failed to remove system file '$PATH_DNSMASQ_IPSET_ALLOWLIST_CONF'" + fi + + if [ -f $PATH_DNSMASQ_SERVER_ALLOWLIST_CONF ]; then + rm -f $PATH_DNSMASQ_SERVER_ALLOWLIST_CONF + exit_if_error $? $EXIT_CODE_CANNOT_REMOVE_FILES "Failed to remove system file '$PATH_DNSMASQ_SERVER_ALLOWLIST_CONF'" + fi +} + +function remove_internet_allowlist() { + # When running --remove, NET_DEVICE_WAN/LAN might not be set (user might not pass params). + # Prefer values from saved examnet config files if available. + if [ -z "$NET_DEVICE_WAN" ] && [ -f "$PATH_NET_DEVICE_WAN_CONF" ]; then + NET_DEVICE_WAN=$(cat "$PATH_NET_DEVICE_WAN_CONF") + fi + if [ -z "$NET_DEVICE_LAN" ] && [ -f "$PATH_NET_DEVICE_LAN_CONF" ]; then + NET_DEVICE_LAN=$(cat "$PATH_NET_DEVICE_LAN_CONF") + fi + + if [ -z "$NET_DEVICE_WAN" ] || [ -z "$NET_DEVICE_LAN" ]; then + # Can't remove interface-specific rules; still flush/destroy ipset. + if [ -x "ipset" ]; then + ipset flush "$INTERNET_ALLOWLIST_IPSET_NAME" 2>/dev/null || true + ipset destroy "$INTERNET_ALLOWLIST_IPSET_NAME" 2>/dev/null || true + fi + return 0 + fi + + # Remove NAT rule + while iptables -t nat -C POSTROUTING -o "$NET_DEVICE_WAN" -m set --match-set "$INTERNET_ALLOWLIST_IPSET_NAME" dst -j MASQUERADE 2>/dev/null; do + iptables -t nat -D POSTROUTING -o "$NET_DEVICE_WAN" -m set --match-set "$INTERNET_ALLOWLIST_IPSET_NAME" dst -j MASQUERADE + done + + # Remove FORWARD jump rules + while iptables -t filter -C FORWARD -i "$NET_DEVICE_LAN" -o "$NET_DEVICE_WAN" -j "$INTERNET_ALLOWLIST_LOG_CHAIN" 2>/dev/null; do + iptables -t filter -D FORWARD -i "$NET_DEVICE_LAN" -o "$NET_DEVICE_WAN" -j "$INTERNET_ALLOWLIST_LOG_CHAIN" + done + + while iptables -t filter -C FORWARD -i "$NET_DEVICE_LAN" -o "$NET_DEVICE_WAN" -j "$INTERNET_ALLOWLIST_FILTER_CHAIN" 2>/dev/null; do + iptables -t filter -D FORWARD -i "$NET_DEVICE_LAN" -o "$NET_DEVICE_WAN" -j "$INTERNET_ALLOWLIST_FILTER_CHAIN" + done + + # Remove established return rule + while iptables -t filter -C FORWARD -i "$NET_DEVICE_WAN" -o "$NET_DEVICE_LAN" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT 2>/dev/null; do + iptables -t filter -D FORWARD -i "$NET_DEVICE_WAN" -o "$NET_DEVICE_LAN" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT + done + + # Delete chains + iptables -t filter -F "$INTERNET_ALLOWLIST_FILTER_CHAIN" 2>/dev/null || true + iptables -t filter -X "$INTERNET_ALLOWLIST_FILTER_CHAIN" 2>/dev/null || true + iptables -t filter -F "$INTERNET_ALLOWLIST_LOG_CHAIN" 2>/dev/null || true + iptables -t filter -X "$INTERNET_ALLOWLIST_LOG_CHAIN" 2>/dev/null || true + + ipset flush "$INTERNET_ALLOWLIST_IPSET_NAME" 2>/dev/null || true + ipset destroy "$INTERNET_ALLOWLIST_IPSET_NAME" 2>/dev/null || true +} + +function enable_systemd_services() { + systemctl enable ytl-linux-digabi2-examnet.service >>"$PATH_DEBUG" 2>&1 + exit_if_error $? $EXIT_CODE_CANNOT_CHANGE_SERVICE_STATE "Failed to enable ytl-linux-digabi2-examnet.service" + + systemctl enable dnsmasq.service >>"$PATH_DEBUG" 2>&1 + exit_if_error $? $EXIT_CODE_CANNOT_CHANGE_SERVICE_STATE "Failed to enable dnsmasq.service" + + systemctl enable ytl-linux-digabi2-examnet-discovery.service >>"$PATH_DEBUG" 2>&1 + exit_if_error $? $EXIT_CODE_CANNOT_CHANGE_SERVICE_STATE "Failed to enable ytl-linux-digabi2-examnet-discovery.service" + + systemctl enable ytl-linux-digabi2-examnet-discovery.timer >>"$PATH_DEBUG" 2>&1 + exit_if_error $? $EXIT_CODE_CANNOT_CHANGE_SERVICE_STATE "Failed to enable ytl-linux-digabi2-examnet-discovery.timer" +} + +function disable_systemd_services() { + systemctl disable --now ytl-linux-digabi2-examnet.service >>"$PATH_DEBUG" 2>&1 + exit_if_error $? $EXIT_CODE_CANNOT_CHANGE_SERVICE_STATE "Failed to disable ytl-linux-digabi2-examnet.service" + + systemctl disable --now dnsmasq.service >>"$PATH_DEBUG" 2>&1 + exit_if_error $? $EXIT_CODE_CANNOT_CHANGE_SERVICE_STATE "Failed to disable dnsmasq.service" + + systemctl disable --now ytl-linux-digabi2-examnet-discovery.timer >>"$PATH_DEBUG" 2>&1 + exit_if_error $? $EXIT_CODE_CANNOT_CHANGE_SERVICE_STATE "Failed to disable ytl-linux-digabi2-examnet-discovery.timer" + + systemctl disable --now ytl-linux-digabi2-examnet-discovery.service >>"$PATH_DEBUG" 2>&1 + exit_if_error $? $EXIT_CODE_CANNOT_CHANGE_SERVICE_STATE "Failed to disable ytl-linux-digabi2-examnet-discovery.service" +} + +function remove_school_host_entries() { + sed -i '/^# BEGIN SCHOOL DOMAIN ENTRIES$/,/^# END SCHOOL DOMAIN ENTRIES$/d' /etc/hosts } function remove_all_settings() { @@ -351,33 +447,146 @@ function remove_all_settings() { fi remove_dnsmasq_settings + remove_internet_allowlist remove_school_host_entries - $BIN_NMCLI -f UUID,NAME connection show | $BIN_GREP -P -v "^UUID" | $BIN_TR -s ' ' | while read -r line ; do - connection_uuid=$(echo "$line" | $BIN_CUT -d ' ' -f 1) - connection_name=$(echo "$line" | $BIN_CUT -d ' ' -f 2) + nmcli -f UUID,NAME connection show | grep -P -v "^UUID" | tr -s ' ' | while read -r line ; do + connection_uuid=$(echo "$line" | cut -d ' ' -f 1) + connection_name=$(echo "$line" | cut -d ' ' -f 2) if [[ "$connection_name" =~ ^yo- ]]; then debug "Removing connection '$connection_name', uuid: $connection_uuid" - $BIN_NMCLI connection delete "$connection_uuid" + nmcli connection delete "$connection_uuid" exit_if_error $? $EXIT_CODE_CANNOT_REMOVE_CONNECTION "Failed to remove connection '$connection_name'" fi done } function restart_systemd_resolved() { - $BIN_SYSTEMCTL restart systemd-resolved + systemctl restart systemd-resolved exit_if_error $? $EXIT_CODE_CANNOT_RESTART_RESOLVED "Failed to restart systemd-resolved" } function restart_dnsmasq() { if [[ "$(systemctl is-enabled dnsmasq.service)" == "enabled" ]]; then - $BIN_SYSTEMCTL restart dnsmasq.service >>"$PATH_DEBUG" 2>&1 + systemctl restart dnsmasq.service >>"$PATH_DEBUG" 2>&1 exit_if_error $? $EXIT_CODE_CANNOT_RESTART_DNSMASQ "Failed to restart dnsmasq.service" fi } +function configure_internet_allowlist() { + if [ -z "$NET_DEVICE_LAN" ] || [ -z "$NET_DEVICE_WAN" ] || [ -z "$SERVER_OWN_IP" ]; then + print_error "Internet allowlist: missing NET_DEVICE_LAN/NET_DEVICE_WAN/SERVER_OWN_IP" + exit_script $EXIT_CODE_CANNOT_START_DAEMON + fi + + if ! command -v "iptables" >/dev/null 2>&1; then + print_error "Internet allowlist: iptables not found (iptables)" + exit_script $EXIT_CODE_CANNOT_START_DAEMON + fi + + if ! command -v "ipset" >/dev/null 2>&1; then + print_error "Internet allowlist: ipset not found (ipset)" + exit_script $EXIT_CODE_CANNOT_START_DAEMON + fi + + if ! command -v "dig" >/dev/null 2>&1; then + print_error "Internet allowlist: dig not found (dig); install dnsutils" + exit_script $EXIT_CODE_CANNOT_START_DAEMON + fi + + # DNS-level deny: + # Keep address=/#/0.0.0.0 in the main dnsmasq config (deny by default). + # Allowlisted domains are the ONLY ones we forward to upstream resolvers. + # + # This prevents LAN clients from receiving real DNS answers for arbitrary + # Internet domains. + + # 1) Write a dnsmasq snippet that (a) adds resolved IPs for allowed domains into ipset + # and (b) forwards ONLY those domains to upstream resolver(s). + normalize_dnsmasq_domain_suffix() { + # dnsmasq treats /domain/ as a suffix match; providing a leading dot makes intent explicit. + # Example: endpoint.security.microsoft.com -> .endpoint.security.microsoft.com + _d="$1" + if [[ "$_d" == .* ]]; then + echo "$_d" + else + echo ".$_d" + fi + } + + { + echo "# Managed by ytl-linux-digabi2-examnet" + echo "# Internet allowlist (DNS + ipset)" + echo "# - Allowlisted domains are forwarded to upstream resolvers" + echo "# - All other domains are null-routed by address=/#/0.0.0.0 in the main config" + echo "# - Returned A records for allowlisted domains (and their subdomains) are added to ipset '$INTERNET_ALLOWLIST_IPSET_NAME'" + for d in "${INTERNET_ALLOWLIST_DOMAINS[@]}"; do + sd="$(normalize_dnsmasq_domain_suffix "$d")" + echo "ipset=/$sd/$INTERNET_ALLOWLIST_IPSET_NAME" + done + } >"$PATH_DNSMASQ_IPSET_ALLOWLIST_CONF" + + # 2) Write a dnsmasq snippet that forwards allowlisted domains to upstream resolvers. + { + echo "# Managed by ytl-linux-digabi2-examnet" + echo "# Forward ONLY allowlisted domains to upstream DNS (as defined by resolv-file=/etc/resolv.conf)" + for d in "${INTERNET_ALLOWLIST_DOMAINS[@]}"; do + sd="$(normalize_dnsmasq_domain_suffix "$d")" + echo "server=/$sd/#" + done + } >"$PATH_DNSMASQ_SERVER_ALLOWLIST_CONF" + + # 3) Ensure ipset exists (with timeout). + # Note: -exist prevents failure if it already exists. + ipset create "$INTERNET_ALLOWLIST_IPSET_NAME" hash:ip timeout "$INTERNET_ALLOWLIST_IPSET_TIMEOUT_SECONDS" -exist + + # 4) Install iptables rules. + # Strategy: + # - LOG chain logs NEW flows allowed by ipset + # - FILTER chain allows LAN->WAN only if dst in ipset; then drop + # - FORWARD has: + # (a) established return traffic WAN->LAN + # (b) LAN->WAN jump to LOG and FILTER chain + # - NAT POSTROUTING masquerade only when dst in ipset + + iptables -t filter -N "$INTERNET_ALLOWLIST_LOG_CHAIN" 2>/dev/null || true + iptables -t filter -F "$INTERNET_ALLOWLIST_LOG_CHAIN" 2>/dev/null || true + iptables -t filter -A "$INTERNET_ALLOWLIST_LOG_CHAIN" \ + -m conntrack --ctstate NEW \ + -m set --match-set "$INTERNET_ALLOWLIST_IPSET_NAME" dst \ + -m limit --limit 10/second --limit-burst 30 \ + -j LOG --log-prefix "YTL ALLOW NEW " --log-level 6 + iptables -t filter -A "$INTERNET_ALLOWLIST_LOG_CHAIN" -j RETURN + + iptables -t filter -N "$INTERNET_ALLOWLIST_FILTER_CHAIN" 2>/dev/null || true + iptables -t filter -F "$INTERNET_ALLOWLIST_FILTER_CHAIN" 2>/dev/null || true + iptables -t filter -A "$INTERNET_ALLOWLIST_FILTER_CHAIN" -m set --match-set "$INTERNET_ALLOWLIST_IPSET_NAME" dst -j ACCEPT + iptables -t filter -A "$INTERNET_ALLOWLIST_FILTER_CHAIN" -j DROP + + # Established/related return traffic from WAN->LAN + iptables -t filter -C FORWARD -i "$NET_DEVICE_WAN" -o "$NET_DEVICE_LAN" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || \ + iptables -t filter -A FORWARD -i "$NET_DEVICE_WAN" -o "$NET_DEVICE_LAN" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT + + # LAN->WAN logging (NEW allowed only) + enforcement + iptables -t filter -C FORWARD -i "$NET_DEVICE_LAN" -o "$NET_DEVICE_WAN" -j "$INTERNET_ALLOWLIST_LOG_CHAIN" 2>/dev/null || \ + iptables -t filter -I FORWARD 1 -i "$NET_DEVICE_LAN" -o "$NET_DEVICE_WAN" -j "$INTERNET_ALLOWLIST_LOG_CHAIN" + + iptables -t filter -C FORWARD -i "$NET_DEVICE_LAN" -o "$NET_DEVICE_WAN" -j "$INTERNET_ALLOWLIST_FILTER_CHAIN" 2>/dev/null || \ + iptables -t filter -I FORWARD 2 -i "$NET_DEVICE_LAN" -o "$NET_DEVICE_WAN" -j "$INTERNET_ALLOWLIST_FILTER_CHAIN" + + # NAT only for allowed destinations + iptables -t nat -C POSTROUTING -o "$NET_DEVICE_WAN" -m set --match-set "$INTERNET_ALLOWLIST_IPSET_NAME" dst -j MASQUERADE 2>/dev/null || \ + iptables -t nat -A POSTROUTING -o "$NET_DEVICE_WAN" -m set --match-set "$INTERNET_ALLOWLIST_IPSET_NAME" dst -j MASQUERADE + + # 5) Restart dnsmasq to pick up ipset=/server= rules, then proactively populate ipset via local dnsmasq. + restart_dnsmasq + for d in "${INTERNET_ALLOWLIST_DOMAINS[@]}"; do + dig +time=1 +tries=1 @127.0.0.1 "$d" A >/dev/null 2>&1 || true + done +} + function restart_networkmanager() { - $BIN_SYSTEMCTL restart NetworkManager.service >>"$PATH_DEBUG" 2>&1 + systemctl restart NetworkManager.service >>"$PATH_DEBUG" 2>&1 exit_if_error $? $EXIT_CODE_CANNOT_RESTART_NM "Failed to restart NetworkManager.service" } @@ -388,24 +597,24 @@ function wait_for_network_online() { _TIMEOUT=30 fi - $BIN_NM_ONLINE -s -q --timeout="$_TIMEOUT" >>"$PATH_DEBUG" 2>&1 + nm-online -s -q --timeout="$_TIMEOUT" >>"$PATH_DEBUG" 2>&1 exit_if_error $? $EXIT_CODE_NETWORK_NOT_ONLINE "Network did not become online within timeout" } function restart_examnet_daemon() { if [[ "$(systemctl is-enabled ytl-linux-digabi2-examnet.service)" == "enabled" ]]; then - $BIN_SYSTEMCTL restart ytl-linux-digabi2-examnet.service >>"$PATH_DEBUG" 2>&1 + systemctl restart ytl-linux-digabi2-examnet.service >>"$PATH_DEBUG" 2>&1 exit_if_error $? $EXIT_CODE_CANNOT_CHANGE_SERVICE_STATE "Failed to restart ytl-linux-digabi2-examnet.service" fi if [[ "$(systemctl is-enabled ytl-linux-digabi2-examnet-discovery.timer)" == "enabled" ]]; then - $BIN_SYSTEMCTL restart ytl-linux-digabi2-examnet-discovery.timer >>"$PATH_DEBUG" 2>&1 + systemctl restart ytl-linux-digabi2-examnet-discovery.timer >>"$PATH_DEBUG" 2>&1 exit_if_error $? $EXIT_CODE_CANNOT_CHANGE_SERVICE_STATE "Failed to restart ytl-linux-digabi2-examnet-discovery.timer" fi if [[ "$(systemctl is-enabled ytl-linux-digabi2-examnet-discovery.service)" == "enabled" ]]; then - $BIN_SYSTEMCTL restart ytl-linux-digabi2-examnet-discovery.service >>"$PATH_DEBUG" 2>&1 + systemctl restart ytl-linux-digabi2-examnet-discovery.service >>"$PATH_DEBUG" 2>&1 exit_if_error $? $EXIT_CODE_CANNOT_CHANGE_SERVICE_STATE "Failed to restart ytl-linux-digabi2-examnet-discovery.service" fi } @@ -417,36 +626,8 @@ function restart_network_services() { restart_examnet_daemon } -function enable_systemd_services() { - $BIN_SYSTEMCTL enable ytl-linux-digabi2-examnet.service >>"$PATH_DEBUG" 2>&1 - exit_if_error $? $EXIT_CODE_CANNOT_CHANGE_SERVICE_STATE "Failed to enable ytl-linux-digabi2-examnet.service" - - $BIN_SYSTEMCTL enable dnsmasq.service >>"$PATH_DEBUG" 2>&1 - exit_if_error $? $EXIT_CODE_CANNOT_CHANGE_SERVICE_STATE "Failed to enable dnsmasq.service" - - $BIN_SYSTEMCTL enable ytl-linux-digabi2-examnet-discovery.service >>"$PATH_DEBUG" 2>&1 - exit_if_error $? $EXIT_CODE_CANNOT_CHANGE_SERVICE_STATE "Failed to enable ytl-linux-digabi2-examnet-discovery.service" - - $BIN_SYSTEMCTL enable ytl-linux-digabi2-examnet-discovery.timer >>"$PATH_DEBUG" 2>&1 - exit_if_error $? $EXIT_CODE_CANNOT_CHANGE_SERVICE_STATE "Failed to enable ytl-linux-digabi2-examnet-discovery.timer" -} - -function disable_systemd_services() { - $BIN_SYSTEMCTL disable --now ytl-linux-digabi2-examnet.service >>"$PATH_DEBUG" 2>&1 - exit_if_error $? $EXIT_CODE_CANNOT_CHANGE_SERVICE_STATE "Failed to disable ytl-linux-digabi2-examnet.service" - - $BIN_SYSTEMCTL disable --now dnsmasq.service >>"$PATH_DEBUG" 2>&1 - exit_if_error $? $EXIT_CODE_CANNOT_CHANGE_SERVICE_STATE "Failed to disable dnsmasq.service" - - $BIN_SYSTEMCTL disable --now ytl-linux-digabi2-examnet-discovery.timer >>"$PATH_DEBUG" 2>&1 - exit_if_error $? $EXIT_CODE_CANNOT_CHANGE_SERVICE_STATE "Failed to disable ytl-linux-digabi2-examnet-discovery.timer" - - $BIN_SYSTEMCTL disable --now ytl-linux-digabi2-examnet-discovery.service >>"$PATH_DEBUG" 2>&1 - exit_if_error $? $EXIT_CODE_CANNOT_CHANGE_SERVICE_STATE "Failed to disable ytl-linux-digabi2-examnet-discovery.service" -} - function restart_docker() { - $BIN_SYSTEMCTL restart docker + systemctl restart docker exit_if_error $? $EXIT_CODE_CANNOT_RESTART_DOCKER "Failed to restart docker" } @@ -464,12 +645,12 @@ function configure_static_dns_records() { # Parse out SANs from certificate; only include server subdomains (e.g. ktp1.1000.koe.abitti.net), not the "root subdomain" # (e.g. 1000.koe.abitti.net) since we cannot reasonably ascribe DNS names to these DOMAINS="$( - $BIN_OPENSSL x509 -in "$PATH_NAKSU2_CERT" -text -noout |\ - $BIN_GREP DNS: |\ - $BIN_XARGS |\ - $BIN_SED 's/DNS://g' |\ - $BIN_SED 's/, /\n/g' |\ - $BIN_GREP -E '(.+\..+){4}' + openssl x509 -in "$PATH_NAKSU2_CERT" -text -noout |\ + grep DNS: |\ + xargs |\ + sed 's/DNS://g' |\ + sed 's/, /\n/g' |\ + grep -E '(.+\..+){4}' )" debug "Configuring static DNS records for the following domains:" @@ -482,7 +663,7 @@ function configure_static_dns_records() { for DOMAIN in $DOMAINS; do debug "Generating DNS record for domain: $DOMAIN" - SERVER_NUMBER="$($BIN_ECHO "$DOMAIN" | $BIN_CUT -d '.' -f1 | $BIN_TR -dc '[:digit:]')" + SERVER_NUMBER="$(echo "$DOMAIN" | cut -d '.' -f1 | tr -dc '[:digit:]')" if [ "$SERVER_NUMBER" == "$_SERVER_NUMBER" ]; then THIS_SERVER_DOMAIN=$DOMAIN @@ -500,7 +681,7 @@ function configure_static_dns_records() { fi debug "Generated DNS records: ${DNS_RECORDS[*]}" - write_file $PATH_DNSMASQ_STATIC_DNS_CONF "$(IFS=$'\n'; $BIN_ECHO "${DNS_RECORDS[*]}")" + write_file $PATH_DNSMASQ_STATIC_DNS_CONF "$(IFS=$'\n'; echo "${DNS_RECORDS[*]}")" debug "Generated /etc/hosts entries: ${ETC_HOSTS[*]}" @@ -517,10 +698,6 @@ function configure_static_dns_records() { write_file "$PATH_NAKSU2_DOMAIN" "$THIS_SERVER_DOMAIN" "$NAKSU2_WORKDIR_OWNER" } -function remove_school_host_entries() { - $BIN_SED -i '/^# BEGIN SCHOOL DOMAIN ENTRIES$/,/^# END SCHOOL DOMAIN ENTRIES$/d' /etc/hosts -} - # ---------- Main script logic starts here ---------- # if [ -z "$PARAM_NET_DEVICE_WAN" ]; then @@ -528,7 +705,7 @@ if [ -z "$PARAM_NET_DEVICE_WAN" ]; then exit_script $EXIT_CODE_NETWORK_DEVICE_NAME_MISSING_WAN fi -if [ ! "$(current_user_is_root)" ]; then +if [[ $* != *--not-root* && ! "$(current_user_is_root)" ]]; then print_error "You're not root" exit_script $EXIT_CODE_MUST_BE_ROOT fi @@ -554,23 +731,23 @@ if [[ $* == *--daemon* ]]; then fi CONFIG=$(cat </dev/null 2>&1; then + echo "ERROR WAN device '$wan' does not exist" >&2 + exit 1 +fi + +if ! ip link show "$lan" >/dev/null 2>&1; then + echo "ERROR LAN device '$lan' does not exist" >&2 + exit 1 +fi + +wan_cidr="$(get_ipv4_cidr "$wan")" +lan_cidr="$(get_ipv4_cidr "$lan")" +lan_ip="$(get_ipv4_addr "$lan")" + +if [[ -z "$wan_cidr" ]]; then + echo "ERROR WAN device '$wan' has no global IPv4 address" >&2 + exit 1 +fi + +if [[ -z "$lan_cidr" ]]; then + echo "ERROR LAN device '$lan' has no global IPv4 address" >&2 + exit 1 +fi + +echo "INFO WAN device: $wan ($wan_cidr)" +echo "INFO LAN device: $lan ($lan_cidr)" + +echo "INFO Allowing internet access for clients on $lan via $wan" + +sudo iptables -t nat -C POSTROUTING -o "$wan" -j MASQUERADE 2>/dev/null || \ + sudo iptables -t nat -A POSTROUTING -o "$wan" -j MASQUERADE + +sudo iptables -C FORWARD -i "$lan" -o "$wan" -j ACCEPT 2>/dev/null || \ + sudo iptables -A FORWARD -i "$lan" -o "$wan" -j ACCEPT + +sudo iptables -C FORWARD -i "$wan" -o "$lan" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || \ + sudo iptables -A FORWARD -i "$wan" -o "$lan" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT + +echo "INFO Disabling null route in dnsmasq configuration file ${DNSMASQ_CONFIG_FILE}" +sudo sed -i 's/^address=\/#\/0\.0\.0\.0$/# address=\/#\/0.0.0.0/' "${DNSMASQ_CONFIG_FILE}" + +echo "INFO Restarting dnsmasq" +sudo systemctl restart dnsmasq.service + +echo "INFO Done" +echo "INFO Clients on $lan should use $lan_ip as their default gateway" \ No newline at end of file diff --git a/packages/ytl-linux-tasks/scripts/deny-internet-for-lan.sh b/packages/ytl-linux-tasks/scripts/deny-internet-for-lan.sh new file mode 100755 index 0000000..b32aad1 --- /dev/null +++ b/packages/ytl-linux-tasks/scripts/deny-internet-for-lan.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +set -euo pipefail + +DNSMASQ_CONFIG_FILE="/etc/dnsmasq.d/ytl-linux.conf" + +wan="$1" +lan="$2" + +if ! ip link show "$wan" >/dev/null 2>&1; then + echo "ERROR WAN device '$wan' does not exist" >&2 + exit 1 +fi + +if ! ip link show "$lan" >/dev/null 2>&1; then + echo "ERROR LAN device '$lan' does not exist" >&2 + exit 1 +fi + +echo "INFO Removing internet access for clients on $lan via $wan" + +while sudo iptables -t nat -C POSTROUTING -o "$wan" -j MASQUERADE 2>/dev/null; do + sudo iptables -t nat -D POSTROUTING -o "$wan" -j MASQUERADE +done + +while sudo iptables -C FORWARD -i "$lan" -o "$wan" -j ACCEPT 2>/dev/null; do + sudo iptables -D FORWARD -i "$lan" -o "$wan" -j ACCEPT +done + +while sudo iptables -C FORWARD -i "$wan" -o "$lan" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT 2>/dev/null; do + sudo iptables -D FORWARD -i "$wan" -o "$lan" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT +done + +echo "INFO Enabling null route in dnsmasq configuration file ${DNSMASQ_CONFIG_FILE}" +sudo sed -i 's/^# address=\/#\/0\.0\.0\.0$/address=\/#\/0.0.0.0/' "${DNSMASQ_CONFIG_FILE}" + +echo "INFO Restarting dnsmasq" +sudo systemctl restart dnsmasq.service + +echo "INFO Done" \ No newline at end of file