Compare commits

..

8 Commits

Author SHA1 Message Date
MrUnknownDE 8deca43f20 add internal dual-stack check 2026-05-17 21:46:40 +02:00
MrUnknownDE 1cdec3c54a update dependencies 2026-05-17 21:32:34 +02:00
MrUnknownDE 27d9e7b154 fix: ip whois lookup forword 2026-05-17 21:25:57 +02:00
MrUnknownDE 014d1704de add dual-stack check 2026-05-17 21:24:10 +02:00
MrUnknownDE ea0d192365 update nginx on Dockerfile 2026-05-17 21:11:25 +02:00
MrUnknownDE 413810e298 add ipv6, vpn and browser check 2026-05-17 21:09:48 +02:00
github-actions[bot] 972741b2fd Update MaxMind GeoLite2 databases (LFS) (2026-05-17) 2026-05-17 18:51:06 +00:00
MrUnknownDE 97ccc32832 add beginer guid on subnet calculator 2026-05-17 20:42:54 +02:00
12 changed files with 1184 additions and 419 deletions
+2 -2
View File
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c80be1dbf2b11aa6b2f0f2d6a9288ddcc590a941ccd94c423093f799d2c3ad00
size 11970193
oid sha256:be75726475eaa093155243a8743f461633ca1b5cbbfbd3fc2a4707d08636447e
size 12178607
+2 -2
View File
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:961bc818cb943a0a858ae61017fbeda9306819360b77ba9ea28fa6832fad1a1d
size 65775230
oid sha256:06f0aca0b9062cecc3a4f6ebe19028ba321b1b4942534c870fd49874fa292fee
size 65982658
+61 -118
View File
@@ -21,6 +21,9 @@
"pino-pretty": "^13.0.0",
"qs": "^6.14.2",
"whois-json": "^2.0.4"
},
"engines": {
"node": ">=24"
}
},
"node_modules/@fastify/otel": {
@@ -120,9 +123,9 @@
}
},
"node_modules/@opentelemetry/core": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.7.0.tgz",
"integrity": "sha512-DT12SXVwV2eoJrGf4nnsvZojxxeQo+LlNAsoYGRRObPWTeN6APiqZ2+nqDCQDvQX40eLi1AePONS0onoASp3yQ==",
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.7.1.tgz",
"integrity": "sha512-QAqIj32AtK6+pEVNG7EOVxHdE06RP+FM5qpiEJ4RtDcFIqKUZHYhl7/7UY5efhwmwNAg7j8QbJVBLxMerc0+gw==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/semantic-conventions": "^1.29.0"
@@ -297,23 +300,6 @@
"@opentelemetry/api": ">=1.0.0 <1.10.0"
}
},
"node_modules/@opentelemetry/instrumentation-ioredis": {
"version": "0.62.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-ioredis/-/instrumentation-ioredis-0.62.0.tgz",
"integrity": "sha512-ZYt//zcPve8qklaZX+5Z4MkU7UpEkFRrxsf2cnaKYBitqDnsCN69CPAuuMOX6NYdW2rG9sFy7V/QWtBlP5XiNQ==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/instrumentation": "^0.214.0",
"@opentelemetry/redis-common": "^0.38.2",
"@opentelemetry/semantic-conventions": "^1.33.0"
},
"engines": {
"node": "^18.19.0 || >=20.6.0"
},
"peerDependencies": {
"@opentelemetry/api": "^1.3.0"
}
},
"node_modules/@opentelemetry/instrumentation-kafkajs": {
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-kafkajs/-/instrumentation-kafkajs-0.23.0.tgz",
@@ -465,23 +451,6 @@
"@opentelemetry/api": "^1.3.0"
}
},
"node_modules/@opentelemetry/instrumentation-redis": {
"version": "0.62.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-redis/-/instrumentation-redis-0.62.0.tgz",
"integrity": "sha512-y3pPpot7WzR/8JtHcYlTYsyY8g+pbFhAqbwAuG5bLPnR6v6pt1rQc0DpH0OlGP/9CZbWBP+Zhwp9yFoygf/ZXQ==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/instrumentation": "^0.214.0",
"@opentelemetry/redis-common": "^0.38.2",
"@opentelemetry/semantic-conventions": "^1.27.0"
},
"engines": {
"node": "^18.19.0 || >=20.6.0"
},
"peerDependencies": {
"@opentelemetry/api": "^1.3.0"
}
},
"node_modules/@opentelemetry/instrumentation-tedious": {
"version": "0.33.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-tedious/-/instrumentation-tedious-0.33.0.tgz",
@@ -499,22 +468,13 @@
"@opentelemetry/api": "^1.3.0"
}
},
"node_modules/@opentelemetry/redis-common": {
"version": "0.38.3",
"resolved": "https://registry.npmjs.org/@opentelemetry/redis-common/-/redis-common-0.38.3.tgz",
"integrity": "sha512-VCghU1JYs/4gP6Gqf/xro9MEsZ7LrMv2uONVsaESKL38ZOB9BqnI98FfS23wjMnHlpuE+TTaWSoAVNpTwYXzjw==",
"license": "Apache-2.0",
"engines": {
"node": "^18.19.0 || >=20.6.0"
}
},
"node_modules/@opentelemetry/resources": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.7.0.tgz",
"integrity": "sha512-K+oi0hNMv94EpZbnW3eyu2X6SGVpD3O5DhG2NIp65Hc7lhAj9brRXTAVzh3wB82+q3ThakEf7Zd7RsFUqcTc7A==",
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.7.1.tgz",
"integrity": "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/core": "2.7.0",
"@opentelemetry/core": "2.7.1",
"@opentelemetry/semantic-conventions": "^1.29.0"
},
"engines": {
@@ -525,13 +485,13 @@
}
},
"node_modules/@opentelemetry/sdk-trace-base": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.7.0.tgz",
"integrity": "sha512-Yg9zEXJB50DLVLpsKPk7NmNqlPlS+OvqhJGh0A8oawIOTPOwlm4eXs9BMJV7L79lvEwI+dWtAj+YjTyddV336A==",
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.7.1.tgz",
"integrity": "sha512-NAYIlsF8MPUsKqJMiDQJTMPOmlbawC1Iz/omMLygZ1C9am8fTKYjTaI+OZM+WTY3t3Glo0wnOg/6/pac6RGPPw==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/core": "2.7.0",
"@opentelemetry/resources": "2.7.0",
"@opentelemetry/core": "2.7.1",
"@opentelemetry/resources": "2.7.1",
"@opentelemetry/semantic-conventions": "^1.29.0"
},
"engines": {
@@ -542,9 +502,9 @@
}
},
"node_modules/@opentelemetry/semantic-conventions": {
"version": "1.40.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.40.0.tgz",
"integrity": "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw==",
"version": "1.41.1",
"resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.41.1.tgz",
"integrity": "sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA==",
"license": "Apache-2.0",
"engines": {
"node": ">=14"
@@ -625,18 +585,18 @@
}
},
"node_modules/@sentry/core": {
"version": "10.50.0",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.50.0.tgz",
"integrity": "sha512-J4A+vzUO3adl0TkFCjaN1+4miamrjHiEIYuLHiuu1lmAjq5WIVw32ObvAh4yMwNtxyaEMosTrrh5M6f12XSJFg==",
"version": "10.53.1",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.53.1.tgz",
"integrity": "sha512-XG4ezlkyuAPjBC5+9kXC94rXXuqYTw9NRhfaDHssbTFaGnqBR8vQX2UUgZfY7ucbeelRDGfBu1sywoU+mB04uA==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/@sentry/node": {
"version": "10.50.0",
"resolved": "https://registry.npmjs.org/@sentry/node/-/node-10.50.0.tgz",
"integrity": "sha512-TvwzFQu8MGKzMQ2/tqxcNzFA8UG2kKTB+GDmA4uOzx3+GT849YZRRSJzEXCmYhk1teVd2fbmgqyYY2nyLF5a+Q==",
"version": "10.53.1",
"resolved": "https://registry.npmjs.org/@sentry/node/-/node-10.53.1.tgz",
"integrity": "sha512-rxHVil0tJAmz+keFcZCj1LaUdgdkK66E/l0gqh2p1209PNCGoD3lnClFr6vusy1aF3zF8O9JPtuMEJzXOKhs+w==",
"license": "MIT",
"dependencies": {
"@fastify/otel": "0.18.0",
@@ -651,7 +611,6 @@
"@opentelemetry/instrumentation-graphql": "0.62.0",
"@opentelemetry/instrumentation-hapi": "0.60.0",
"@opentelemetry/instrumentation-http": "0.214.0",
"@opentelemetry/instrumentation-ioredis": "0.62.0",
"@opentelemetry/instrumentation-kafkajs": "0.23.0",
"@opentelemetry/instrumentation-knex": "0.58.0",
"@opentelemetry/instrumentation-koa": "0.62.0",
@@ -661,14 +620,13 @@
"@opentelemetry/instrumentation-mysql": "0.60.0",
"@opentelemetry/instrumentation-mysql2": "0.60.0",
"@opentelemetry/instrumentation-pg": "0.66.0",
"@opentelemetry/instrumentation-redis": "0.62.0",
"@opentelemetry/instrumentation-tedious": "0.33.0",
"@opentelemetry/sdk-trace-base": "^2.6.1",
"@opentelemetry/semantic-conventions": "^1.40.0",
"@prisma/instrumentation": "7.6.0",
"@sentry/core": "10.50.0",
"@sentry/node-core": "10.50.0",
"@sentry/opentelemetry": "10.50.0",
"@sentry/core": "10.53.1",
"@sentry/node-core": "10.53.1",
"@sentry/opentelemetry": "10.53.1",
"import-in-the-middle": "^3.0.0"
},
"engines": {
@@ -676,13 +634,13 @@
}
},
"node_modules/@sentry/node-core": {
"version": "10.50.0",
"resolved": "https://registry.npmjs.org/@sentry/node-core/-/node-core-10.50.0.tgz",
"integrity": "sha512-Eb1BYf4Lc7ZYmdX3acKP6SgyGikrBA370gbGHaWI5jRu7G7vig8sIu1ghPmY5AlvqBPOetado7GniXr6fAXbTw==",
"version": "10.53.1",
"resolved": "https://registry.npmjs.org/@sentry/node-core/-/node-core-10.53.1.tgz",
"integrity": "sha512-iH7SMcM/7jPbN+t7+7ussQOiIqI4BMOGt4VYWlV71/z7k0pY+YPaSvlfVkNXfISiDzFAKv0ecCY3BmsLMu1xDQ==",
"license": "MIT",
"dependencies": {
"@sentry/core": "10.50.0",
"@sentry/opentelemetry": "10.50.0",
"@sentry/core": "10.53.1",
"@sentry/opentelemetry": "10.53.1",
"import-in-the-middle": "^3.0.0"
},
"engines": {
@@ -718,12 +676,12 @@
}
},
"node_modules/@sentry/opentelemetry": {
"version": "10.50.0",
"resolved": "https://registry.npmjs.org/@sentry/opentelemetry/-/opentelemetry-10.50.0.tgz",
"integrity": "sha512-axn3pgDPveGdaMUC0abMCmFN7ux2pA5ebPufCef4lMIsyg7BBQvaEJ+vE19wjstMaBCAJGsdZlL3eeP2rtgRMw==",
"version": "10.53.1",
"resolved": "https://registry.npmjs.org/@sentry/opentelemetry/-/opentelemetry-10.53.1.tgz",
"integrity": "sha512-Zok6UXla0mFOjd1YnVb1TZtQNOry9v93fXUqx8jmDaygwWM2BwvP8rBQabLz0/OZXo8+35oge+Vmw+QY5aesnA==",
"license": "MIT",
"dependencies": {
"@sentry/core": "10.50.0"
"@sentry/core": "10.53.1"
},
"engines": {
"node": ">=18"
@@ -754,12 +712,12 @@
}
},
"node_modules/@types/node": {
"version": "25.6.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz",
"integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==",
"version": "25.8.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.8.0.tgz",
"integrity": "sha512-TCFSk8IZh+iLX1xtksoBVtdmgL+1IX0fC9BeU4QqFSuNdN/K+HUlhqOzEmSYYpZUVsLYcPqc9KX+60iDuninSQ==",
"license": "MIT",
"dependencies": {
"undici-types": "~7.19.0"
"undici-types": ">=7.24.0 <7.24.7"
}
},
"node_modules/@types/pg": {
@@ -898,9 +856,9 @@
}
},
"node_modules/brace-expansion": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"
@@ -1259,14 +1217,14 @@
}
},
"node_modules/express": {
"version": "4.22.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
"integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
"version": "4.22.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz",
"integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==",
"license": "MIT",
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
"body-parser": "~1.20.3",
"body-parser": "~1.20.5",
"content-disposition": "~0.5.4",
"content-type": "~1.0.4",
"cookie": "~0.7.1",
@@ -1285,7 +1243,7 @@
"parseurl": "~1.3.3",
"path-to-regexp": "~0.1.12",
"proxy-addr": "~2.0.7",
"qs": "~6.14.0",
"qs": "~6.15.1",
"range-parser": "~1.2.1",
"safe-buffer": "5.2.1",
"send": "~0.19.0",
@@ -1319,21 +1277,6 @@
"express": ">= 4.11"
}
},
"node_modules/express/node_modules/qs": {
"version": "6.14.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz",
"integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==",
"license": "BSD-3-Clause",
"dependencies": {
"side-channel": "^1.1.0"
},
"engines": {
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/fast-copy": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-4.0.3.tgz",
@@ -1568,9 +1511,9 @@
"license": "ISC"
},
"node_modules/ip-address": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
"integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==",
"version": "10.2.0",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz",
"integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==",
"license": "MIT",
"engines": {
"node": ">= 12"
@@ -2126,9 +2069,9 @@
}
},
"node_modules/qs": {
"version": "6.15.1",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz",
"integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==",
"version": "6.15.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz",
"integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==",
"license": "BSD-3-Clause",
"dependencies": {
"side-channel": "^1.1.0"
@@ -2440,12 +2383,12 @@
}
},
"node_modules/socks": {
"version": "2.8.7",
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz",
"integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==",
"version": "2.8.9",
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz",
"integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==",
"license": "MIT",
"dependencies": {
"ip-address": "^10.0.1",
"ip-address": "^10.1.1",
"smart-buffer": "^4.2.0"
},
"engines": {
@@ -2585,9 +2528,9 @@
"license": "MIT"
},
"node_modules/undici-types": {
"version": "7.19.2",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz",
"integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==",
"version": "7.24.6",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz",
"integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==",
"license": "MIT"
},
"node_modules/unpipe": {
+2 -1
View File
@@ -27,6 +27,7 @@
"whois-json": "^2.0.4"
},
"overrides": {
"underscore": "1.13.8"
"underscore": ">=1.13.8",
"ip-address": ">=10.2.0"
}
}
+10
View File
@@ -0,0 +1,10 @@
const express = require('express');
const router = express.Router();
const { getCleanIp } = require('../utils');
router.get('/', (req, res) => {
const ip = getCleanIp(req.ip || req.socket.remoteAddress);
res.json({ ip: ip || null });
});
module.exports = router;
+75
View File
@@ -0,0 +1,75 @@
const express = require('express');
const router = express.Router();
const dns = require('dns').promises;
const pino = require('pino');
const { getMaxMindReaders } = require('../maxmind');
const logger = pino({ level: process.env.LOG_LEVEL || 'info' });
// ASN org-name patterns that strongly suggest a VPN service
const VPN_PATTERNS = [
/\bvpn\b/i, /nordvpn/i, /expressvpn/i, /mullvad/i, /31173\s+services/i,
/owl\s+limited/i, /surfshark/i, /protonvpn/i, /cyberghost/i, /ipvanish/i,
/purevpn/i, /tunnelbear/i, /private.?internet.?access/i, /\bpia\b/i,
/hide\.?my\.?ip/i, /hidemyass/i, /windscribe/i, /perfect.?privacy/i,
];
// ASN org-name patterns that suggest cloud/datacenter/hosting (but not necessarily VPN)
const DATACENTER_PATTERNS = [
/amazon/i, /\baws\b/i, /google.*cloud/i, /microsoft/i, /\bazure\b/i,
/cloudflare/i, /digitalocean/i, /linode/i, /vultr/i, /hetzner/i,
/\bovh\b/i, /leaseweb/i, /rackspace/i, /choopa/i, /equinix/i,
/hostinger/i, /\bhosting\b/i, /data.?cent(?:er|re)/i, /\bcloud\b/i,
/\bcoloc\b/i, /\bvps\b/i, /akamai/i, /fastly/i, /\bcdn\b/i,
];
// Tor DNS exit-node check via torproject.org DNSBL
async function isTorExit(ip) {
if (!ip || ip.includes(':')) return false; // IPv6 not supported by this DNSBL
try {
const reversed = ip.split('.').reverse().join('.');
await dns.resolve4(`${reversed}.dnsel.torproject.org`);
return true; // resolves → known Tor exit node
} catch {
return false; // NXDOMAIN → not a Tor exit node
}
}
router.get('/:ip', async (req, res, next) => {
const { ip } = req.params;
const flags = [];
try {
// ASN-based classification (synchronous, no network call)
try {
const { asnReader } = getMaxMindReaders();
const asnData = asnReader.asn(ip);
const org = asnData?.autonomousSystemOrganization || '';
if (VPN_PATTERNS.some(p => p.test(org))) {
flags.push({ id: 'vpn', label: 'VPN', color: 'yellow' });
} else if (DATACENTER_PATTERNS.some(p => p.test(org))) {
flags.push({ id: 'datacenter', label: 'Datacenter / Hosting', color: 'orange' });
} else if (org) {
flags.push({ id: 'residential', label: 'Residential / ISP', color: 'green' });
}
} catch (e) {
logger.debug({ ip, err: e.message }, 'ASN lookup skipped for privacy check');
}
// Tor exit-node check (async DNS, ~100300 ms)
const tor = await isTorExit(ip);
if (tor) {
flags.length = 0; // Tor supersedes all network-type flags — showing "Residential" alongside Tor is misleading
flags.push({ id: 'tor', label: 'Tor Exit Node', color: 'red' });
}
logger.info({ ip, flags: flags.map(f => f.id) }, 'Privacy check complete');
res.json({ success: true, ip, flags });
} catch (err) {
logger.error({ ip, err: err.message }, 'Privacy check failed');
next(err);
}
});
module.exports = router;
+4
View File
@@ -32,6 +32,8 @@ const versionRoutes = require('./routes/version');
const portScanRoutes = require('./routes/portScan');
const macLookupRoutes = require('./routes/macLookup');
const asnLookupRoutes = require('./routes/asnLookup');
const privacyRoutes = require('./routes/privacy');
const myipRoutes = require('./routes/myip');
// --- Logger Initialisierung ---
const logger = pino({
@@ -88,6 +90,8 @@ app.use('/api/version', versionRoutes);
app.use('/api/port-scan', portScanRoutes);
app.use('/api/mac-lookup', macLookupRoutes);
app.use('/api/asn-lookup', asnLookupRoutes);
app.use('/api/privacy', privacyRoutes);
app.use('/api/myip', myipRoutes);
// Sentry error handler — must be after routes, before custom error handler
+1 -1
View File
@@ -2,7 +2,7 @@
# Aktuell nicht nötig, da wir CDN/statische Dateien haben.
# Stage 2: Production Environment using Nginx
FROM nginx:1.27-alpine
FROM nginx:1.31.0-alpine
# Arbeitsverzeichnis im Container (optional, aber gute Praxis)
WORKDIR /usr/share/nginx/html
+477 -120
View File
@@ -25,6 +25,27 @@ export const page = {
</a>
<button id="copy-ip-btn" class="copy-btn hidden">copy</button>
</div>
<!-- IPv6 row — shown only when dual-stack is detected -->
<div id="ipv6-row" class="hidden mt-2 flex items-center gap-2">
<span class="text-xs font-bold text-blue-400/60 uppercase tracking-wider w-10 shrink-0">IPv6</span>
<span id="ipv6-address" class="font-mono text-blue-300 text-sm break-all"></span>
<button id="copy-ipv6-btn" class="copy-btn hidden">copy</button>
</div>
<!-- Privacy / risk flags — dual-stack: always show both rows -->
<div id="privacy-checks" class="mt-3 space-y-1.5">
<div class="flex items-center gap-2 min-h-[22px]">
<span class="text-xs font-bold text-gray-500 uppercase tracking-wider w-10 shrink-0">IPv4</span>
<div id="privacy-flags-v4" class="flex flex-wrap gap-1.5 items-center">
<div id="privacy-loader-v4" class="loader" style="width:12px;height:12px;border-width:2px"></div>
</div>
</div>
<div class="flex items-center gap-2 min-h-[22px]">
<span class="text-xs font-bold text-blue-400/60 uppercase tracking-wider w-10 shrink-0">IPv6</span>
<div id="privacy-flags-v6" class="flex flex-wrap gap-1.5 items-center">
<span class="text-xs text-gray-600 italic">detecting…</span>
</div>
</div>
</div>
</div>
<div class="glass-card rounded-lg p-5 space-y-4">
@@ -68,23 +89,71 @@ export const page = {
</div>
</div>
<!-- Right column: map -->
<div class="space-y-4 fade-in" style="animation-delay:.2s">
<h2 class="text-lg font-semibold text-gray-200 flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-purple-400" viewBox="0 0 20 20" fill="currentColor">
<!-- Right column: map — container is h-[420px] so the Leaflet map can fill it -->
<div class="fade-in" style="animation-delay:.2s">
<h2 class="text-sm font-bold text-gray-400 uppercase tracking-widest mb-3 flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-purple-400" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z" clip-rule="evenodd"/>
</svg>
Location Visualization
Location
</h2>
<div id="map-container" class="bg-gray-800/50 rounded-lg min-h-[400px] h-full flex items-center justify-center relative border border-gray-700/50 shadow-inner overflow-hidden">
<div id="map-loader" class="loader absolute z-10"></div>
<div id="map" class="w-full h-full rounded-lg hidden z-0 opacity-80 hover:opacity-100 transition-opacity duration-700"></div>
<p id="map-message" class="text-gray-400 hidden absolute text-sm">Could not load map.</p>
<div class="absolute inset-0 pointer-events-none rounded-lg ring-1 ring-inset ring-white/10"></div>
<div id="map-container" class="rounded-xl h-[420px] relative border border-gray-700/50 overflow-hidden bg-gray-900/60 shadow-inner">
<div id="map-loader" class="loader absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 z-10"></div>
<div id="map" class="w-full hidden z-0 transition-opacity duration-700 rounded-xl"></div>
<p id="map-message" class="text-gray-400 hidden absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-sm">Could not load map.</p>
<div class="absolute inset-0 pointer-events-none rounded-xl ring-1 ring-inset ring-white/10"></div>
</div>
</div>
</div>
<!-- Browser Fingerprint -->
<div class="mt-6 glass-card rounded-xl p-5 fade-in" style="animation-delay:.3s">
<h2 class="text-xs font-bold text-gray-400 uppercase tracking-widest mb-4 border-b border-gray-700 pb-2 flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-purple-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
</svg>
Browser Fingerprint
</h2>
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-x-8 gap-y-4 text-sm">
<div>
<p class="text-xs text-gray-500 uppercase tracking-wider mb-0.5">Browser</p>
<p id="fp-browser" class="text-gray-200 font-medium font-mono">-</p>
</div>
<div>
<p class="text-xs text-gray-500 uppercase tracking-wider mb-0.5">Operating System</p>
<p id="fp-os" class="text-gray-200 font-medium">-</p>
</div>
<div>
<p class="text-xs text-gray-500 uppercase tracking-wider mb-0.5">Screen</p>
<p id="fp-screen" class="text-gray-200 font-medium font-mono">-</p>
</div>
<div>
<p class="text-xs text-gray-500 uppercase tracking-wider mb-0.5">Viewport</p>
<p id="fp-viewport" class="text-gray-200 font-medium font-mono">-</p>
</div>
<div>
<p class="text-xs text-gray-500 uppercase tracking-wider mb-0.5">Language</p>
<p id="fp-language" class="text-gray-200 font-medium">-</p>
</div>
<div>
<p class="text-xs text-gray-500 uppercase tracking-wider mb-0.5">Timezone</p>
<p id="fp-timezone" class="text-gray-200 font-medium">-</p>
</div>
<div>
<p class="text-xs text-gray-500 uppercase tracking-wider mb-0.5">Color Depth</p>
<p id="fp-color" class="text-gray-200 font-medium">-</p>
</div>
<div>
<p class="text-xs text-gray-500 uppercase tracking-wider mb-0.5">Privacy</p>
<p id="fp-privacy" class="text-gray-200 font-medium text-xs leading-relaxed">-</p>
</div>
</div>
<details class="mt-4 border-t border-gray-700/50 pt-3">
<summary class="text-xs text-gray-500 cursor-pointer hover:text-gray-300 select-none list-none transition-colors">User Agent string</summary>
<p id="fp-ua" class="font-mono text-xs text-gray-400 break-all mt-2 leading-relaxed"></p>
</details>
</div>
<!-- IP Lookup -->
<div class="mt-8 p-6 glass-card rounded-xl">
<h2 class="text-xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-purple-400 to-pink-500 mb-4 flex items-center gap-2">
@@ -104,12 +173,14 @@ export const page = {
<div id="lookup-error" class="text-red-400 mb-4 hidden p-3 bg-red-900/20 border border-red-500/30 rounded text-sm"></div>
<div id="lookup-results-section" class="hidden grid grid-cols-1 md:grid-cols-2 gap-8 mt-6 border-t border-gray-700/50 pt-6 fade-in">
<div class="space-y-6">
<!-- Left: info -->
<div class="space-y-5">
<h3 class="text-lg font-semibold text-gray-200">Result for: <span id="lookup-ip-address" class="font-mono text-purple-400 bg-purple-500/10 px-2 py-0.5 rounded"></span>
<button id="copy-lookup-ip-btn" class="copy-btn ml-2">copy</button>
</h3>
<div id="lookup-result-loader" class="loader hidden"></div>
<div id="lookup-geo-info" class="space-y-1 text-sm text-gray-300">
<div id="lookup-geo-info" class="text-sm text-gray-300">
<h4 class="font-bold text-gray-500 uppercase text-xs tracking-wider mb-2">Geolocation</h4>
<div class="grid grid-cols-2 gap-x-2 gap-y-1">
<p><span class="text-gray-500">Country:</span> <span id="lookup-country" class="text-white">-</span></p>
@@ -118,69 +189,145 @@ export const page = {
<p><span class="text-gray-500">Zip:</span> <span id="lookup-postal" class="text-white">-</span></p>
<p class="col-span-2"><span class="text-gray-500">Coords:</span> <span id="lookup-coords" class="font-mono text-purple-300">-</span></p>
<p class="col-span-2"><span class="text-gray-500">Time:</span> <span id="lookup-timezone" class="text-white">-</span></p>
<p id="lookup-geo-error" class="text-red-400 col-span-2"></p>
<p id="lookup-geo-error" class="text-red-400 col-span-2 text-xs"></p>
</div>
</div>
<div id="lookup-asn-info" class="space-y-1 text-sm text-gray-300">
<h4 class="font-bold text-gray-500 uppercase text-xs tracking-wider mb-2 mt-4">ASN</h4>
<div id="lookup-asn-info" class="text-sm text-gray-300">
<h4 class="font-bold text-gray-500 uppercase text-xs tracking-wider mb-2">ASN</h4>
<p><span class="text-gray-500">Number:</span> <span id="lookup-asn-number" class="text-white font-mono">-</span></p>
<p><span class="text-gray-500">Org:</span> <span id="lookup-asn-org" class="text-white">-</span></p>
<p id="lookup-asn-error" class="text-red-400"></p>
<p id="lookup-asn-error" class="text-red-400 text-xs"></p>
</div>
<div id="lookup-rdns-info" class="space-y-1 text-sm text-gray-300">
<h4 class="font-bold text-gray-500 uppercase text-xs tracking-wider mb-2 mt-4">Reverse DNS</h4>
<div id="lookup-rdns-info" class="text-sm text-gray-300">
<h4 class="font-bold text-gray-500 uppercase text-xs tracking-wider mb-2">Reverse DNS</h4>
<ul id="lookup-rdns-list" class="list-none space-y-1 font-mono text-xs text-green-400"><li>-</li></ul>
<p id="lookup-rdns-error" class="text-red-400"></p>
<p id="lookup-rdns-error" class="text-red-400 text-xs"></p>
</div>
</div>
<div class="space-y-6">
<h4 class="font-bold text-gray-500 uppercase text-xs tracking-wider mb-2">Location Map</h4>
<div id="lookup-map-container" class="glass-panel rounded-lg min-h-[250px] flex items-center justify-center relative overflow-hidden">
<div id="lookup-map-loader" class="loader hidden absolute z-10"></div>
<div id="lookup-map" class="w-full rounded hidden opacity-90"></div>
<p id="lookup-map-message" class="text-gray-400 hidden absolute">Could not load map.</p>
<div class="absolute inset-0 pointer-events-none ring-1 ring-inset ring-white/10 rounded-lg"></div>
<!-- Right: map + action buttons -->
<div class="space-y-4">
<h4 class="font-bold text-gray-500 uppercase text-xs tracking-wider">Location Map</h4>
<div id="lookup-map-container" class="rounded-xl h-[260px] relative overflow-hidden bg-gray-900/60 border border-gray-700/50">
<div id="lookup-map-loader" class="loader hidden absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 z-10"></div>
<div id="lookup-map" class="w-full hidden rounded-xl"></div>
<p id="lookup-map-message" class="text-gray-400 hidden absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-sm text-center px-4">Could not load map.</p>
<div class="absolute inset-0 pointer-events-none ring-1 ring-inset ring-white/10 rounded-xl"></div>
</div>
<div class="flex flex-wrap gap-2 pt-2">
<button id="lookup-ping-button" disabled class="flex-1 bg-gray-700 hover:bg-gray-600 text-white font-bold py-2 px-4 rounded-lg text-sm transition-all disabled:opacity-50 disabled:cursor-not-allowed border border-gray-600 hover:border-gray-500 shadow-md">Ping</button>
<button id="lookup-trace-button" disabled class="flex-1 bg-gray-700 hover:bg-gray-600 text-white font-bold py-2 px-4 rounded-lg text-sm transition-all disabled:opacity-50 disabled:cursor-not-allowed border border-gray-600 hover:border-gray-500 shadow-md">Trace</button>
<button id="lookup-scan-button" disabled class="flex-1 bg-gray-700 hover:bg-gray-600 text-white font-bold py-2 px-4 rounded-lg text-sm transition-all disabled:opacity-50 disabled:cursor-not-allowed border border-gray-600 hover:border-gray-500 shadow-md">Port Scan</button>
</div>
<div id="lookup-ping-results" class="mt-4 text-sm hidden fade-in">
<h4 class="font-bold text-purple-400 mb-2 flex items-center gap-2">
<div class="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div> Ping Results
</h4>
<div id="lookup-ping-loader" class="loader hidden"></div>
<pre id="lookup-ping-output" class="result-pre mt-1"></pre>
<p id="lookup-ping-error" class="text-red-400 mt-2"></p>
<!-- Action buttons -->
<div class="grid grid-cols-3 gap-2">
<button id="lookup-ping-button" disabled title="Send ICMP ping"
class="action-tool-btn flex flex-col items-center gap-1.5 py-3 px-2 rounded-lg transition-all disabled:opacity-40 disabled:cursor-not-allowed border border-gray-700/50 bg-gray-800/50 hover:bg-purple-900/30 hover:border-purple-500/40 text-gray-400 hover:text-white">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M22 12h-4l-3 9L9 3l-3 9H2"/>
</svg>
<span class="text-xs font-semibold">Ping</span>
</button>
<button id="lookup-trace-button" disabled title="Run traceroute"
class="action-tool-btn flex flex-col items-center gap-1.5 py-3 px-2 rounded-lg transition-all disabled:opacity-40 disabled:cursor-not-allowed border border-gray-700/50 bg-gray-800/50 hover:bg-purple-900/30 hover:border-purple-500/40 text-gray-400 hover:text-white">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"/>
</svg>
<span class="text-xs font-semibold">Traceroute</span>
</button>
<button id="lookup-scan-button" disabled title="Scan common ports"
class="action-tool-btn flex flex-col items-center gap-1.5 py-3 px-2 rounded-lg transition-all disabled:opacity-40 disabled:cursor-not-allowed border border-gray-700/50 bg-gray-800/50 hover:bg-purple-900/30 hover:border-purple-500/40 text-gray-400 hover:text-white">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 3H5a2 2 0 00-2 2v4m6-6h10a2 2 0 012 2v4M9 3v18m0 0h10a2 2 0 002-2V9M9 21H5a2 2 0 01-2-2V9m0 0h18"/>
</svg>
<span class="text-xs font-semibold">Port Scan</span>
</button>
</div>
</div>
</div>
</div>
<!-- Ping Results — dedicated section, consistent with traceroute/port-scan -->
<div id="ping-section" class="mt-8 p-6 glass-card rounded-xl hidden fade-in">
<h2 class="text-xl font-bold text-purple-300 border-b border-purple-500/30 pb-2 mb-4 flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M22 12h-4l-3 9L9 3l-3 9H2"/>
</svg>
Ping &mdash; <span id="ping-target" class="font-mono text-white text-base font-normal ml-1"></span>
</h2>
<div class="flex items-center gap-3 mb-4 text-sm">
<div id="ping-section-loader" class="loader hidden"></div>
<span id="ping-section-message" class="text-gray-400"></span>
<span id="ping-section-error" class="text-red-400"></span>
</div>
<!-- Stat cards -->
<div id="ping-stats-grid" class="grid grid-cols-2 sm:grid-cols-4 gap-3 mb-4 hidden">
<div class="bg-gray-900/50 rounded-lg p-4 text-center border border-gray-700/30">
<p class="text-xs text-gray-500 uppercase tracking-wider mb-1">Sent</p>
<p id="ping-stat-sent" class="text-3xl font-bold font-mono text-white">-</p>
</div>
<div class="bg-gray-900/50 rounded-lg p-4 text-center border border-gray-700/30">
<p class="text-xs text-gray-500 uppercase tracking-wider mb-1">Received</p>
<p id="ping-stat-recv" class="text-3xl font-bold font-mono text-green-400">-</p>
</div>
<div class="bg-gray-900/50 rounded-lg p-4 text-center border border-gray-700/30">
<p class="text-xs text-gray-500 uppercase tracking-wider mb-1">Packet Loss</p>
<p id="ping-stat-loss" class="text-3xl font-bold font-mono text-red-400">-</p>
</div>
<div class="bg-gray-900/50 rounded-lg p-4 text-center border border-gray-700/30">
<p class="text-xs text-gray-500 uppercase tracking-wider mb-1">Avg RTT</p>
<p id="ping-stat-rtt" class="text-3xl font-bold font-mono text-blue-300">-</p>
</div>
</div>
<!-- RTT range bar -->
<div id="ping-rtt-range" class="hidden mb-4 px-4 py-3 bg-gray-900/40 rounded-lg border border-gray-700/30 flex flex-wrap gap-6 text-xs font-mono text-gray-500">
<span>min &nbsp;<span id="ping-rtt-min" class="text-gray-200 font-semibold">-</span> ms</span>
<span>avg &nbsp;<span id="ping-rtt-avg" class="text-gray-200 font-semibold">-</span> ms</span>
<span>max &nbsp;<span id="ping-rtt-max" class="text-gray-200 font-semibold">-</span> ms</span>
</div>
<!-- Raw output -->
<details>
<summary class="text-xs text-gray-500 hover:text-gray-300 cursor-pointer select-none list-none transition-colors">Raw output</summary>
<pre id="ping-raw-output" class="result-pre mt-2 text-xs"></pre>
</details>
</div>
<!-- Traceroute -->
<div id="traceroute-section" class="mt-8 p-6 glass-card rounded-xl hidden fade-in">
<h2 class="text-xl font-bold text-purple-300 border-b border-purple-500/30 pb-2 mb-4">Traceroute Results</h2>
<div class="flex items-center justify-between mb-4">
<h2 class="text-xl font-bold text-purple-300 border-b border-purple-500/30 pb-2 mb-4 flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"/>
</svg>
Traceroute &mdash; <span id="traceroute-target" class="font-mono text-white text-base font-normal ml-1"></span>
</h2>
<div class="flex items-center justify-between mb-3">
<div class="flex items-center gap-3 text-sm">
<div id="traceroute-loader" class="loader hidden"></div>
<span id="traceroute-message" class="text-gray-300"></span>
<span id="traceroute-message" class="text-gray-400"></span>
</div>
<button id="traceroute-stop-btn" class="stop-btn hidden"> Stop</button>
<button id="traceroute-stop-btn" class="stop-btn hidden">&#9632; Stop</button>
</div>
<div id="traceroute-output" class="rounded-lg overflow-hidden"><pre class="m-0"></pre></div>
<!-- Hop table header -->
<div class="hidden sm:grid traceroute-header text-xs text-gray-600 uppercase tracking-wider px-2 mb-1" style="grid-template-columns:2rem 1fr auto">
<span class="text-right pr-3">#</span>
<span>IP / Hostname</span>
<span class="text-right">RTT</span>
</div>
<div id="traceroute-output" class="font-mono text-sm rounded-lg border border-gray-700/30 bg-black/20 overflow-y-auto max-h-[420px] p-2 space-y-0.5"></div>
</div>
<!-- Port Scan -->
<div id="port-scan-section" class="mt-8 p-6 glass-card rounded-xl hidden fade-in">
<h2 class="text-xl font-bold text-purple-300 border-b border-purple-500/30 pb-2 mb-4">Port Scan Results</h2>
<h2 class="text-xl font-bold text-purple-300 border-b border-purple-500/30 pb-2 mb-4 flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 3H5a2 2 0 00-2 2v4m6-6h10a2 2 0 012 2v4M9 3v18m0 0h10a2 2 0 002-2V9M9 21H5a2 2 0 01-2-2V9m0 0h18"/>
</svg>
Port Scan Results
</h2>
<div class="flex items-center justify-between mb-4">
<div class="flex items-center gap-3 text-sm">
<div id="port-scan-loader" class="loader hidden"></div>
<span id="port-scan-message" class="text-gray-300"></span>
</div>
<button id="port-scan-stop-btn" class="stop-btn hidden"> Stop</button>
<button id="port-scan-stop-btn" class="stop-btn hidden">&#9632; Stop</button>
</div>
<div id="port-scan-output" class="text-sm font-mono bg-gray-900/50 p-4 rounded-lg border border-gray-700/50 max-h-[300px] overflow-y-auto"></div>
</div>
@@ -215,13 +362,20 @@ export const page = {
const lookupPingBtn = document.getElementById('lookup-ping-button');
const lookupTraceBtn = document.getElementById('lookup-trace-button');
const lookupScanBtn = document.getElementById('lookup-scan-button');
const lookupPingRes = document.getElementById('lookup-ping-results');
const lookupPingLoader= document.getElementById('lookup-ping-loader');
const lookupPingOutput= document.getElementById('lookup-ping-output');
const lookupPingError = document.getElementById('lookup-ping-error');
// Ping section
const pingSect = document.getElementById('ping-section');
const pingTarget = document.getElementById('ping-target');
const pingSectLoader = document.getElementById('ping-section-loader');
const pingSectMsg = document.getElementById('ping-section-message');
const pingSectErr = document.getElementById('ping-section-error');
const pingStatsGrid = document.getElementById('ping-stats-grid');
const pingRttRange = document.getElementById('ping-rtt-range');
const pingRawOutput = document.getElementById('ping-raw-output');
const tracerouteSection = document.getElementById('traceroute-section');
const tracerouteOutput = document.querySelector('#traceroute-output pre');
const tracerouteTarget = document.getElementById('traceroute-target');
const tracerouteOutput = document.getElementById('traceroute-output');
const tracerouteLoader = document.getElementById('traceroute-loader');
const tracerouteMessage = document.getElementById('traceroute-message');
const tracerouteStopBtn = document.getElementById('traceroute-stop-btn');
@@ -233,7 +387,7 @@ export const page = {
const portScanStopBtn = document.getElementById('port-scan-stop-btn');
// ── State ────────────────────────────────────────────────────
let map = null, lookupMap = null, currentIp = null, currentLookupIp = null;
let currentIp = null, currentLookupIp = null;
let eventSource = null, portScanEventSource = null;
// ── Helpers ──────────────────────────────────────────────────
@@ -321,13 +475,90 @@ export const page = {
}
// ── Copy buttons ─────────────────────────────────────────────
setupCopyBtn(copyIpBtn, () => ipAddressSpan.textContent);
setupCopyBtn(copyIpBtn, () => ipAddressSpan.textContent);
setupCopyBtn(copyLookupIpBtn, () => lookupIpEl.textContent);
const copyIpv6Btn = document.getElementById('copy-ipv6-btn');
setupCopyBtn(copyIpv6Btn, () => document.getElementById('ipv6-address').textContent);
// ── Lookup button enable/disable ─────────────────────────────
const syncLookupBtn = () => { lookupBtn.disabled = !lookupInput.value.trim(); };
lookupInput.addEventListener('input', syncLookupBtn);
// ── Privacy / risk flags ──────────────────────────────────────
const FLAG_COLORS = {
green: 'bg-green-900/40 text-green-300 border border-green-700/50',
orange: 'bg-orange-900/40 text-orange-300 border border-orange-700/50',
yellow: 'bg-yellow-900/40 text-yellow-300 border border-yellow-700/50',
red: 'bg-red-900/40 text-red-300 border border-red-700/50',
};
async function fetchPrivacyFlags(ip, version = 4) {
const container = document.getElementById(`privacy-flags-v${version}`);
const loader = document.getElementById(`privacy-loader-v${version}`);
if (!container) return;
try {
const r = await fetch(`${API}/privacy/${encodeURIComponent(ip)}`);
const data = await r.json();
loader?.remove();
if (!data.flags?.length) {
container.innerHTML = '<span class="text-xs text-gray-600">—</span>';
return;
}
container.innerHTML = data.flags.map(f =>
`<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-semibold ${FLAG_COLORS[f.color] || FLAG_COLORS.green}">${f.label}</span>`
).join('');
} catch {
loader?.remove();
container.innerHTML = '<span class="text-xs text-gray-600">N/A</span>';
}
}
// ── Browser fingerprint ───────────────────────────────────────
function populateBrowserFingerprint() {
const ua = navigator.userAgent;
function getBrowser() {
if (/Edg\/(\d+)/.test(ua)) return `Edge ${RegExp.$1}`;
if (/OPR\/(\d+)/.test(ua)) return `Opera ${RegExp.$1}`;
if (/Chrome\/(\d+)/.test(ua)) return `Chrome ${RegExp.$1}`;
if (/Firefox\/(\d+)/.test(ua)) return `Firefox ${RegExp.$1}`;
if (/Version\/([\d.]+).*Safari/.test(ua)) return `Safari ${RegExp.$1}`;
return 'Unknown';
}
function getOS() {
if (/Windows NT 10\.0/.test(ua)) return 'Windows 10 / 11';
if (/Windows NT 6\.3/.test(ua)) return 'Windows 8.1';
if (/Windows NT 6\.1/.test(ua)) return 'Windows 7';
if (/Mac OS X ([\d_]+)/.test(ua)) return `macOS ${RegExp.$1.replace(/_/g, '.')}`;
if (/Android ([\d.]+)/.test(ua)) return `Android ${RegExp.$1}`;
if (/iPhone OS ([\d_]+)/.test(ua)) return `iOS ${RegExp.$1.replace(/_/g, '.')}`;
if (/iPad.*OS ([\d_]+)/.test(ua)) return `iPadOS ${RegExp.$1.replace(/_/g, '.')}`;
if (/Linux/.test(ua)) return 'Linux';
return 'Unknown';
}
document.getElementById('fp-browser').textContent = getBrowser();
document.getElementById('fp-os').textContent = getOS();
document.getElementById('fp-screen').textContent =
`${screen.width} × ${screen.height}` + (window.devicePixelRatio !== 1 ? ` @ ${window.devicePixelRatio}×` : '');
document.getElementById('fp-viewport').textContent = `${window.innerWidth} × ${window.innerHeight} px`;
document.getElementById('fp-language').textContent =
navigator.language + (navigator.languages?.length > 1 ? ` (+${navigator.languages.length - 1} more)` : '');
document.getElementById('fp-timezone').textContent = Intl.DateTimeFormat().resolvedOptions().timeZone;
document.getElementById('fp-color').textContent = `${screen.colorDepth}-bit`;
const bits = [];
if (navigator.cookieEnabled) bits.push('Cookies on');
else bits.push('Cookies off');
if (navigator.doNotTrack === '1') bits.push('DNT enabled');
const conn = navigator.connection;
if (conn?.effectiveType) bits.push(conn.effectiveType.toUpperCase());
document.getElementById('fp-privacy').textContent = bits.join(' · ');
document.getElementById('fp-ua').textContent = ua;
}
// ── Own IP fetch ─────────────────────────────────────────────
async function fetchIpInfo() {
globalError.classList.add('hidden');
@@ -337,46 +568,112 @@ export const page = {
mapEl.classList.add('hidden');
mapMessage.classList.add('hidden');
// Force-detect via protocol-specific subdomains (ipv4./ipv6. have A-only / AAAA-only DNS)
async function forceDetect(url) {
const ctrl = new AbortController();
const timer = setTimeout(() => ctrl.abort(), 4000);
try {
const r = await fetch(url, { signal: ctrl.signal });
if (!r.ok) return null;
const d = await r.json();
return d.ip || null;
} catch { return null; }
finally { clearTimeout(timer); }
}
function setPrivacyNA(version) {
const el = document.getElementById(`privacy-flags-v${version}`);
if (el) { document.getElementById(`privacy-loader-v${version}`)?.remove(); el.innerHTML = '<span class="text-xs text-gray-600">N/A</span>'; }
}
// Use current hostname so it works for any deployment (utools.mrunk.de → ipv4.utools.mrunk.de)
const host = location.hostname;
const [ipv4, ipv6] = await Promise.all([
forceDetect(`https://ipv4.${host}/api/myip`),
forceDetect(`https://ipv6.${host}/api/myip`),
]);
const hasSeperateIPv6 = !!(ipv4 && ipv6 && ipv4 !== ipv6);
const primaryIp = ipv4 || ipv6;
// Fetch geo / ASN / rDNS — use /api/lookup for detected IP, /api/ipinfo as fallback
let data;
try {
const r = await fetch(`${API}/ipinfo`);
if (!r.ok) throw new Error(`${r.statusText} (${r.status})`);
const data = await r.json();
currentIp = data.ip;
ipAddressSpan.textContent = data.ip;
ipAddressLink.classList.remove('hidden');
copyIpBtn.classList.remove('hidden');
ipLoader.classList.add('hidden');
ipAddressLink.addEventListener('click', e => {
e.preventDefault();
if (currentIp) window._router.navigate('/whois', { query: currentIp });
});
updateField(document.getElementById('country'), data.geo?.countryName ? `${data.geo.countryName} (${data.geo.country})` : null, null, document.getElementById('geo-error'));
updateField(document.getElementById('region'), data.geo?.region);
updateField(document.getElementById('city'), data.geo?.city);
updateField(document.getElementById('postal'), data.geo?.postalCode);
updateField(document.getElementById('coords'), data.geo?.latitude ? `${data.geo.latitude}, ${data.geo.longitude}` : null);
updateField(document.getElementById('timezone'), data.geo?.timezone, geoLoader);
const asnNum = (data.asn && !data.asn.error) ? data.asn.number : null;
if (asnNum) {
const asnContainer = document.getElementById('asn-number')?.closest('div:not(.loader)');
if (asnContainer) asnContainer.classList.remove('hidden');
document.getElementById('asn-number').innerHTML =
`<a href="/asn?asn=${asnNum}" class="hover:text-purple-200 underline decoration-dotted transition-colors" title="Open ASN Lookup">AS${asnNum}</a>`;
if (primaryIp) {
const r = await fetch(`${API}/lookup?targetIp=${encodeURIComponent(primaryIp)}`);
if (!r.ok) throw new Error(`${r.statusText} (${r.status})`);
data = await r.json();
data.ip = primaryIp;
} else {
updateField(document.getElementById('asn-number'), null, null, document.getElementById('asn-error'), data.asn?.error || '-');
// Local dev / subdomains not reachable — fall back to auto-detect
const r = await fetch(`${API}/ipinfo`);
if (!r.ok) throw new Error(`${r.statusText} (${r.status})`);
data = await r.json();
}
updateField(document.getElementById('asn-org'), data.asn?.organization, asnLoader);
updateRdns(document.getElementById('rdns-list'), data.rdns, rdnsLoader, document.getElementById('rdns-error'));
map = initOrUpdateMap('map', data.geo?.latitude, data.geo?.longitude, mapEl, mapLoader, mapMessage);
} catch (err) {
console.error('Failed to fetch IP info:', err);
showGlobalErr(`Could not load IP information. ${err.message}`);
[ipLoader, geoLoader, asnLoader, rdnsLoader, mapLoader].forEach(l => l?.classList.add('hidden'));
return;
}
currentIp = data.ip;
// Show primary IP
ipAddressSpan.textContent = data.ip;
ipAddressLink.classList.remove('hidden');
copyIpBtn.classList.remove('hidden');
ipLoader.classList.add('hidden');
ipAddressLink.onclick = e => {
e.preventDefault();
window._router.navigate('/whois', { query: currentIp });
};
// IPv6 row — only when a separate IPv6 was detected alongside IPv4
if (hasSeperateIPv6) {
document.getElementById('ipv6-address').textContent = ipv6;
document.getElementById('ipv6-row').classList.remove('hidden');
copyIpv6Btn.classList.remove('hidden');
}
// Privacy checks — each slot gets the correct IP or N/A
const v4ForPrivacy = ipv4 || (!data.ip.includes(':') ? data.ip : null);
const v6ForPrivacy = hasSeperateIPv6 ? ipv6 : (data.ip.includes(':') ? data.ip : null);
if (v4ForPrivacy) {
fetchPrivacyFlags(v4ForPrivacy, 4);
} else {
setPrivacyNA(4);
}
if (v6ForPrivacy) {
const v6El = document.getElementById('privacy-flags-v6');
if (v6El) v6El.innerHTML = '<div id="privacy-loader-v6" class="loader" style="width:12px;height:12px;border-width:2px"></div>';
fetchPrivacyFlags(v6ForPrivacy, 6);
} else {
setPrivacyNA(6);
}
// Populate geo / ASN / rDNS / map
updateField(document.getElementById('country'), data.geo?.countryName ? `${data.geo.countryName} (${data.geo.country})` : null, null, document.getElementById('geo-error'));
updateField(document.getElementById('region'), data.geo?.region);
updateField(document.getElementById('city'), data.geo?.city);
updateField(document.getElementById('postal'), data.geo?.postalCode);
updateField(document.getElementById('coords'), data.geo?.latitude ? `${data.geo.latitude}, ${data.geo.longitude}` : null);
updateField(document.getElementById('timezone'), data.geo?.timezone, geoLoader);
const asnNum = (data.asn && !data.asn.error) ? data.asn.number : null;
if (asnNum) {
const asnContainer = document.getElementById('asn-number')?.closest('div:not(.loader)');
if (asnContainer) asnContainer.classList.remove('hidden');
document.getElementById('asn-number').innerHTML =
`<a href="/asn?asn=${asnNum}" class="hover:text-purple-200 underline decoration-dotted transition-colors" title="Open ASN Lookup">AS${asnNum}</a>`;
} else {
updateField(document.getElementById('asn-number'), null, null, document.getElementById('asn-error'), data.asn?.error || '-');
}
updateField(document.getElementById('asn-org'), data.asn?.organization, asnLoader);
updateRdns(document.getElementById('rdns-list'), data.rdns, rdnsLoader, document.getElementById('rdns-error'));
initOrUpdateMap('map', data.geo?.latitude, data.geo?.longitude, mapEl, mapLoader, mapMessage);
}
// ── Lookup ───────────────────────────────────────────────────
@@ -386,8 +683,7 @@ export const page = {
lookupMapLoader.classList.add('hidden');
lookupMapEl.classList.add('hidden');
lookupMapMsg.classList.add('hidden');
lookupPingRes.classList.add('hidden');
lookupPingLoader.classList.add('hidden');
pingSect.classList.add('hidden');
portScanSection.classList.add('hidden');
portScanOutput.innerHTML = '';
[lookupIpEl, document.getElementById('lookup-country'), document.getElementById('lookup-region'),
@@ -397,8 +693,6 @@ export const page = {
document.getElementById('lookup-geo-error'), document.getElementById('lookup-asn-error'),
document.getElementById('lookup-rdns-error')].forEach(el => { if (el) el.textContent = ''; });
document.getElementById('lookup-rdns-list').innerHTML = '<li>-</li>';
lookupPingOutput.textContent = '';
lookupPingError.textContent = '';
[lookupPingBtn, lookupTraceBtn, lookupScanBtn].forEach(b => { if (b) b.disabled = true; });
currentLookupIp = null;
if (window['lookup-map_instance']) { window['lookup-map_instance'].remove(); window['lookup-map_instance'] = null; }
@@ -460,7 +754,7 @@ export const page = {
}
updateField(document.getElementById('lookup-asn-org'), data.asn?.organization);
updateRdns(document.getElementById('lookup-rdns-list'), data.rdns, null, document.getElementById('lookup-rdns-error'));
lookupMap = initOrUpdateMap('lookup-map', data.geo?.latitude, data.geo?.longitude, lookupMapEl, lookupMapLoader, lookupMapMsg);
initOrUpdateMap('lookup-map', data.geo?.latitude, data.geo?.longitude, lookupMapEl, lookupMapLoader, lookupMapMsg);
[lookupPingBtn, lookupTraceBtn, lookupScanBtn].forEach(b => { if (b) b.disabled = false; });
} catch (err) {
@@ -478,25 +772,44 @@ export const page = {
// ── Ping ─────────────────────────────────────────────────────
async function runPing(ip) {
lookupPingRes.classList.remove('hidden');
lookupPingLoader.classList.remove('hidden');
lookupPingOutput.textContent = '';
lookupPingError.textContent = '';
pingSect.classList.remove('hidden');
pingTarget.textContent = ip;
pingSectLoader.classList.remove('hidden');
pingSectMsg.textContent = `Pinging ${ip}`;
pingSectErr.textContent = '';
pingStatsGrid.classList.add('hidden');
pingRttRange.classList.add('hidden');
pingRawOutput.textContent = '';
pingSect.scrollIntoView({ behavior: 'smooth', block: 'start' });
try {
const r = await fetch(`${API}/ping?targetIp=${encodeURIComponent(ip)}`);
const data = await r.json();
if (!r.ok || !data.success) throw new Error(data.error || `HTTP ${r.status}`);
let out = `--- Ping Statistics for ${ip} ---\n`;
if (data.stats) {
out += `Packets: ${data.stats.packets.transmitted} sent, ${data.stats.packets.received} received, ${data.stats.packets.lossPercent}% loss\n`;
if (data.stats.rtt) out += `RTT (ms): min=${data.stats.rtt.min} avg=${data.stats.rtt.avg} max=${data.stats.rtt.max}\n`;
if (data.stats?.packets) {
const loss = data.stats.packets.lossPercent;
document.getElementById('ping-stat-sent').textContent = data.stats.packets.transmitted;
document.getElementById('ping-stat-recv').textContent = data.stats.packets.received;
document.getElementById('ping-stat-loss').textContent = `${loss}%`;
document.getElementById('ping-stat-loss').className =
`text-3xl font-bold font-mono ${loss === 0 || loss === '0' ? 'text-green-400' : loss >= 50 ? 'text-red-400' : 'text-yellow-400'}`;
document.getElementById('ping-stat-rtt').textContent = data.stats.rtt ? `${data.stats.rtt.avg} ms` : '-';
pingStatsGrid.classList.remove('hidden');
}
out += `\n--- Raw Output ---\n${data.rawOutput || ''}`;
lookupPingOutput.textContent = out;
if (data.stats?.rtt) {
document.getElementById('ping-rtt-min').textContent = data.stats.rtt.min;
document.getElementById('ping-rtt-avg').textContent = data.stats.rtt.avg;
document.getElementById('ping-rtt-max').textContent = data.stats.rtt.max;
pingRttRange.classList.remove('hidden');
}
pingRawOutput.textContent = data.rawOutput || '';
pingSectMsg.textContent = `Ping to ${ip} complete.`;
} catch (err) {
lookupPingError.textContent = `Ping Error: ${err.message}`;
pingSectErr.textContent = `Ping failed: ${err.message}`;
pingSectMsg.textContent = '';
} finally {
lookupPingLoader.classList.add('hidden');
pingSectLoader.classList.add('hidden');
}
}
@@ -504,11 +817,13 @@ export const page = {
function startTraceroute(ip) {
if (eventSource) { eventSource.close(); eventSource = null; }
tracerouteSection.classList.remove('hidden');
tracerouteOutput.textContent = '';
if (tracerouteTarget) tracerouteTarget.textContent = ip;
tracerouteOutput.innerHTML = '';
tracerouteLoader.classList.remove('hidden');
tracerouteStopBtn.classList.remove('hidden');
tracerouteMessage.textContent = `Starting traceroute to ${ip}`;
globalError.classList.add('hidden');
tracerouteSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
eventSource = new EventSource(`${API}/traceroute?targetIp=${encodeURIComponent(ip)}`);
@@ -531,7 +846,7 @@ export const page = {
});
eventSource.addEventListener('end', e => {
try {
const d = JSON.parse(e.data);
const d = JSON.parse(e.data);
const msg = `Traceroute finished${d.exitCode === 0 ? ' successfully' : ` (exit code ${d.exitCode})`}.`;
displayTraceLine(msg, 'end-line');
tracerouteMessage.textContent = msg;
@@ -552,34 +867,74 @@ export const page = {
function displayTraceLine(text, cls = '') {
const div = document.createElement('div');
div.classList.add('px-2', 'py-0.5', 'fade-in');
if (cls) div.classList.add(cls);
div.classList.add('fade-in');
div.textContent = text;
tracerouteOutput.appendChild(div);
tracerouteOutput.scrollTop = tracerouteOutput.scrollHeight;
}
function displayHop(hop) {
const div = document.createElement('div');
div.classList.add('hop-line', 'fade-in');
const num = document.createElement('span'); num.classList.add('hop-number'); num.textContent = hop.hop || '?'; div.appendChild(num);
const row = document.createElement('div');
row.classList.add('hop-row', 'fade-in');
// Hop number
const numEl = document.createElement('span');
numEl.classList.add('hop-number');
numEl.textContent = hop.hop ?? '?';
row.appendChild(numEl);
// Body: IP line + optional RDNS line below
const body = document.createElement('div');
body.classList.add('hop-body');
if (hop.ip) {
const ip = document.createElement('span'); ip.classList.add('hop-ip'); ip.textContent = hop.ip; div.appendChild(ip);
if (hop.hostname) { const h = document.createElement('span'); h.classList.add('hop-hostname'); h.textContent = ` (${hop.hostname})`; div.appendChild(h); }
const ipLine = document.createElement('div');
ipLine.classList.add('hop-ip-line');
const ipEl = document.createElement('span');
ipEl.classList.add('hop-ip');
ipEl.textContent = hop.ip;
ipLine.appendChild(ipEl);
// RTTs — right-aligned via flex margin-left auto on the rtt group
if (Array.isArray(hop.rtt)) {
const rttsEl = document.createElement('span');
rttsEl.classList.add('hop-rtts');
hop.rtt.forEach(r => {
const s = document.createElement('span');
s.classList.add(r === '*' ? 'hop-timeout' : 'hop-rtt');
s.textContent = r === '*' ? '*' : `${r} ms`;
rttsEl.appendChild(s);
});
ipLine.appendChild(rttsEl);
}
body.appendChild(ipLine);
// RDNS — own line, only when it differs from the IP
if (hop.hostname && hop.hostname !== hop.ip) {
const rdnsEl = document.createElement('div');
rdnsEl.classList.add('hop-rdns');
rdnsEl.textContent = hop.hostname;
body.appendChild(rdnsEl);
}
} else if (hop.rtt?.every(r => r === '*')) {
const t = document.createElement('span'); t.classList.add('hop-timeout'); t.textContent = '* * *'; div.appendChild(t);
const line = document.createElement('div');
line.classList.add('hop-ip-line');
const t = document.createElement('span');
t.classList.add('hop-timeout');
t.textContent = '* * *';
line.appendChild(t);
body.appendChild(line);
} else {
div.appendChild(document.createTextNode(hop.rawLine || 'Unknown hop'));
const line = document.createElement('div');
line.classList.add('hop-ip-line', 'text-gray-400');
line.textContent = hop.rawLine || 'Unknown hop';
body.appendChild(line);
}
if (Array.isArray(hop.rtt)) {
hop.rtt.forEach(r => {
const s = document.createElement('span');
s.classList.add(r === '*' ? 'hop-timeout' : 'hop-rtt');
s.textContent = r === '*' ? ' *' : ` ${r} ms`;
div.appendChild(s);
});
}
tracerouteOutput.appendChild(div);
row.appendChild(body);
tracerouteOutput.appendChild(row);
tracerouteOutput.scrollTop = tracerouteOutput.scrollHeight;
}
@@ -591,6 +946,7 @@ export const page = {
portScanLoader.classList.remove('hidden');
portScanStopBtn.classList.remove('hidden');
portScanMessage.textContent = `Starting port scan for ${ip}`;
portScanSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
portScanEventSource = new EventSource(`${API}/port-scan?targetIp=${encodeURIComponent(ip)}`);
portScanEventSource.onopen = () => {};
@@ -650,6 +1006,7 @@ export const page = {
portScanStopBtn.addEventListener('click', stopPortScan);
// ── Bootstrap ────────────────────────────────────────────────
populateBrowserFingerprint();
fetchIpInfo();
const params = new URLSearchParams(search);
+508 -148
View File
@@ -1,111 +1,493 @@
export const page = {
title: 'Subnetz Rechner',
title: 'Subnet Calculator',
template: () => `
<div class="container mx-auto max-w-5xl glass-panel rounded-xl shadow-2xl p-6 md:p-8 backdrop-blur-xl border border-gray-800/50">
<h2 class="text-3xl font-bold mb-8 text-center text-gradient">IP Subnetz Rechner</h2>
<h2 class="text-3xl font-bold mb-4 text-center text-gradient">IP Subnet Calculator</h2>
<form id="subnet-form" class="mb-8 glass-card p-6 rounded-xl">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-4">
<div>
<label for="ip-address" class="block text-gray-400 text-sm font-bold mb-2 uppercase tracking-wide">IP Adresse:</label>
<input type="text" id="ip-address" name="ip-address" placeholder="z.B. 192.168.1.1" required
class="w-full px-4 py-3 bg-gray-900/50 border border-gray-700/50 rounded-lg text-gray-200 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent font-mono transition-all">
</div>
<div>
<label for="cidr" class="block text-gray-400 text-sm font-bold mb-2 uppercase tracking-wide">CIDR / Maske:</label>
<input type="text" id="cidr" name="cidr" placeholder="z.B. 24 oder 255.255.255.0" required
class="w-full px-4 py-3 bg-gray-900/50 border border-gray-700/50 rounded-lg text-gray-200 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent font-mono transition-all">
</div>
</div>
<div id="subnet-error" class="hidden mb-4 p-3 bg-red-900/20 border border-red-500/30 rounded text-red-400 text-sm"></div>
<button type="submit"
class="w-full bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-500 hover:to-pink-500 text-white font-bold py-3 px-6 rounded-lg shadow-lg hover:shadow-purple-500/25 transition-all duration-200 ease-in-out transform hover:-translate-y-0.5">
Berechnen
</button>
</form>
<div id="results" class="glass-card rounded-xl p-6 hidden fade-in">
<h3 class="text-xl font-bold text-purple-300 border-b border-purple-500/30 pb-2 mb-4 flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
Ergebnisse:
</h3>
<div class="space-y-3 text-sm">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2 border-b border-gray-700/50 pb-2">
<span class="text-gray-400">Netzwerkadresse:</span>
<span id="network-address" class="font-mono text-white font-semibold">-</span>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2 border-b border-gray-700/50 pb-2">
<span class="text-gray-400">Broadcast-Adresse:</span>
<span id="broadcast-address" class="font-mono text-purple-400 font-semibold">-</span>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2 border-b border-gray-700/50 pb-2">
<span class="text-gray-400">Subnetzmaske:</span>
<span id="subnet-mask" class="font-mono text-gray-300">-</span>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2 border-b border-gray-700/50 pb-2">
<span class="text-gray-400">Anzahl der Hosts:</span>
<span id="host-count" class="font-mono text-green-400 font-bold">-</span>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2 border-b border-gray-700/50 pb-2">
<span class="text-gray-400">Erste Host-Adresse:</span>
<span id="first-host" class="font-mono text-blue-300">-</span>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
<span class="text-gray-400">Letzte Host-Adresse:</span>
<span id="last-host" class="font-mono text-blue-300">-</span>
</div>
<!-- Mode Toggle -->
<div class="flex justify-center mb-8">
<div class="flex bg-gray-900/70 border border-gray-700/50 rounded-xl p-1 gap-1">
<button id="btn-beginner" class="px-6 py-2 rounded-lg text-sm font-semibold transition-all duration-200 bg-gradient-to-r from-purple-600 to-pink-600 text-white shadow-lg">
Beginner
</button>
<button id="btn-pro" class="px-6 py-2 rounded-lg text-sm font-semibold transition-all duration-200 text-gray-400 hover:text-gray-200">
Expert
</button>
</div>
</div>
<!-- Example subnets -->
<div class="glass-card rounded-xl p-6 mt-8">
<h3 class="text-lg font-bold text-gray-400 uppercase tracking-wider border-b border-gray-700/50 pb-2 mb-4">
Beispiel-Subnetze (Private Adressbereiche)
</h3>
<div class="overflow-x-auto">
<table class="min-w-full text-sm text-left text-gray-400">
<thead class="text-xs uppercase bg-gray-800/50 text-gray-200">
<tr>
<th class="px-6 py-3">Bereich</th>
<th class="px-6 py-3">CIDR</th>
<th class="px-6 py-3">Subnetzmaske</th>
<th class="px-6 py-3">Beschreibung</th>
<th class="px-6 py-3">Aktion</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-700/50">
<tr class="hover:bg-gray-700/30 transition-colors">
<td class="px-6 py-4 font-mono text-white">192.168.0.0 192.168.255.255</td>
<td class="px-6 py-4 font-mono">/16 (Gesamt)</td>
<td class="px-6 py-4 font-mono">255.255.0.0</td>
<td class="px-6 py-4">Klasse C (oft als /24 genutzt)</td>
<td class="px-6 py-4"><span class="example-link text-purple-400 hover:text-purple-300 cursor-pointer underline" data-ip="192.168.1.1" data-cidr="24">Beispiel /24</span></td>
</tr>
<tr class="hover:bg-gray-700/30 transition-colors">
<td class="px-6 py-4 font-mono text-white">172.16.0.0 172.31.255.255</td>
<td class="px-6 py-4 font-mono">/12 (Gesamt)</td>
<td class="px-6 py-4 font-mono">255.240.0.0</td>
<td class="px-6 py-4">Klasse B</td>
<td class="px-6 py-4"><span class="example-link text-purple-400 hover:text-purple-300 cursor-pointer underline" data-ip="172.16.10.5" data-cidr="16">Beispiel /16</span></td>
</tr>
<tr class="hover:bg-gray-700/30 transition-colors">
<td class="px-6 py-4 font-mono text-white">10.0.0.0 10.255.255.255</td>
<td class="px-6 py-4 font-mono">/8 (Gesamt)</td>
<td class="px-6 py-4 font-mono">255.0.0.0</td>
<td class="px-6 py-4">Klasse A</td>
<td class="px-6 py-4"><span class="example-link text-purple-400 hover:text-purple-300 cursor-pointer underline" data-ip="10.0.50.100" data-cidr="8">Beispiel /8</span></td>
</tr>
</tbody>
</table>
<!-- BEGINNER MODE -->
<div id="beginner-mode">
<div class="glass-card p-6 rounded-xl mb-6">
<div class="mb-6">
<label class="block text-gray-400 text-sm font-bold mb-2 uppercase tracking-wide">IP Address</label>
<input type="text" id="beg-ip" value="192.168.1.0" placeholder="e.g. 192.168.1.0"
class="w-full px-4 py-3 bg-gray-900/50 border border-gray-700/50 rounded-lg text-gray-200 focus:outline-none focus:ring-2 focus:ring-purple-500 font-mono transition-all">
<p class="text-xs text-gray-500 mt-1">Enter an IPv4 address, e.g. 192.168.1.0</p>
</div>
<div class="mb-6">
<div class="flex justify-between items-center mb-3">
<label class="text-gray-400 text-sm font-bold uppercase tracking-wide">Network Size</label>
<span id="beg-cidr-label" class="text-3xl font-mono font-bold text-purple-300">/24</span>
</div>
<input type="range" id="beg-slider" min="1" max="30" value="24" list="cidr-marks"
class="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer accent-purple-500">
<datalist id="cidr-marks">
<option value="8"></option>
<option value="16"></option>
<option value="24"></option>
</datalist>
<div class="flex justify-between text-xs text-gray-500 mt-1">
<span>/1 &mdash; huge</span>
<span>/8</span>
<span>/16</span>
<span>/24</span>
<span>/30 &mdash; tiny</span>
</div>
</div>
<!-- Host count highlight -->
<div class="mb-4 p-4 bg-gray-900/60 rounded-xl border border-purple-500/20 flex items-center justify-between">
<div>
<p class="text-xs text-gray-500 uppercase tracking-wider mb-1">Usable Hosts</p>
<p id="beg-hosts-count" class="text-4xl font-bold font-mono text-green-400">254</p>
</div>
<div class="text-right">
<p class="text-xs text-gray-500 uppercase tracking-wider mb-1">Subnet Mask</p>
<p id="beg-mask-display" class="text-sm font-mono text-gray-300">255.255.255.0</p>
</div>
</div>
<!-- Visual bar -->
<div class="mb-6">
<span class="text-xs text-gray-500 uppercase tracking-wider">Relative Network Size (logarithmic)</span>
<div class="h-3 bg-gray-800 rounded-full overflow-hidden border border-gray-700/50 mt-1">
<div id="beg-bar" class="h-full bg-gradient-to-r from-purple-600 to-pink-500 transition-all duration-500 rounded-full" style="width:26%"></div>
</div>
<div class="flex justify-between text-xs text-gray-600 mt-1">
<span>2 hosts (/30)</span>
<span>2 billion hosts (/1)</span>
</div>
</div>
<!-- Results grid -->
<div class="grid grid-cols-2 sm:grid-cols-4 gap-3">
<div class="bg-gray-900/50 rounded-lg p-3 text-center border border-gray-700/30">
<p class="text-xs text-gray-500 mb-1">Network ID</p>
<p id="beg-network" class="font-mono text-white text-sm font-bold">-</p>
<p class="text-xs text-gray-600 mt-1">network address</p>
</div>
<div class="bg-gray-900/50 rounded-lg p-3 text-center border border-gray-700/30">
<p class="text-xs text-gray-500 mb-1">First Host</p>
<p id="beg-first" class="font-mono text-blue-300 text-sm font-bold">-</p>
<p class="text-xs text-gray-600 mt-1">1st usable address</p>
</div>
<div class="bg-gray-900/50 rounded-lg p-3 text-center border border-gray-700/30">
<p class="text-xs text-gray-500 mb-1">Last Host</p>
<p id="beg-last" class="font-mono text-blue-300 text-sm font-bold">-</p>
<p class="text-xs text-gray-600 mt-1">last usable address</p>
</div>
<div class="bg-gray-900/50 rounded-lg p-3 text-center border border-gray-700/30">
<p class="text-xs text-gray-500 mb-1">Broadcast</p>
<p id="beg-broadcast" class="font-mono text-purple-400 text-sm font-bold">-</p>
<p class="text-xs text-gray-600 mt-1">send to all devices</p>
</div>
</div>
</div>
<!-- Explanation card -->
<div class="glass-card p-6 rounded-xl border border-purple-500/20 mb-6">
<h3 class="text-base font-bold text-purple-300 mb-3 flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
What does this mean?
</h3>
<p id="beg-explain-text" class="text-sm text-gray-300 leading-relaxed mb-4"></p>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3 text-xs">
<div class="p-3 bg-gray-900/40 rounded-lg border border-gray-700/30">
<p class="text-white font-semibold mb-1">Network Address</p>
<p class="text-gray-400">The first address &mdash; identifies the network itself. No device can use this address.</p>
</div>
<div class="p-3 bg-gray-900/40 rounded-lg border border-gray-700/30">
<p class="text-white font-semibold mb-1">Host Addresses</p>
<p class="text-gray-400">All addresses in between &mdash; assignable to devices like PCs, servers, or printers.</p>
</div>
<div class="p-3 bg-gray-900/40 rounded-lg border border-gray-700/30">
<p class="text-white font-semibold mb-1">Broadcast Address</p>
<p class="text-gray-400">The last address &mdash; packets sent here are delivered to every device in the network.</p>
</div>
</div>
</div>
<!-- Quick examples -->
<div class="glass-card p-4 rounded-xl mb-6">
<p class="text-xs text-gray-500 uppercase tracking-wider mb-3 font-bold">Typical Networks &mdash; click to try</p>
<div class="flex flex-wrap gap-2">
<button class="beg-example px-3 py-1.5 text-xs bg-gray-800 hover:bg-purple-900/50 border border-gray-700/50 hover:border-purple-500/50 rounded-lg font-mono text-gray-300 hover:text-white transition-all" data-ip="192.168.1.0" data-cidr="24">192.168.1.0/24 &mdash; home network (254 hosts)</button>
<button class="beg-example px-3 py-1.5 text-xs bg-gray-800 hover:bg-purple-900/50 border border-gray-700/50 hover:border-purple-500/50 rounded-lg font-mono text-gray-300 hover:text-white transition-all" data-ip="192.168.0.0" data-cidr="16">192.168.0.0/16 &mdash; large (65k hosts)</button>
<button class="beg-example px-3 py-1.5 text-xs bg-gray-800 hover:bg-purple-900/50 border border-gray-700/50 hover:border-purple-500/50 rounded-lg font-mono text-gray-300 hover:text-white transition-all" data-ip="10.0.0.0" data-cidr="8">10.0.0.0/8 &mdash; huge (16M hosts)</button>
<button class="beg-example px-3 py-1.5 text-xs bg-gray-800 hover:bg-purple-900/50 border border-gray-700/50 hover:border-purple-500/50 rounded-lg font-mono text-gray-300 hover:text-white transition-all" data-ip="192.168.1.0" data-cidr="28">192.168.1.0/28 &mdash; small (14 hosts)</button>
<button class="beg-example px-3 py-1.5 text-xs bg-gray-800 hover:bg-purple-900/50 border border-gray-700/50 hover:border-purple-500/50 rounded-lg font-mono text-gray-300 hover:text-white transition-all" data-ip="10.0.0.0" data-cidr="30">10.0.0.0/30 &mdash; P2P link (2 hosts)</button>
</div>
</div>
<!-- Subnet explainer -->
<details class="glass-card rounded-xl border border-gray-700/30 group">
<summary class="flex items-center justify-between p-5 cursor-pointer select-none list-none">
<span class="text-sm font-bold text-gray-300 flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-purple-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"/>
</svg>
What is a subnet, exactly?
</span>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-gray-500 transition-transform duration-200 group-open:rotate-180" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
</svg>
</summary>
<div class="px-5 pb-6 space-y-6 border-t border-gray-700/30 pt-5">
<!-- Analogy -->
<div>
<h4 class="text-sm font-bold text-purple-300 mb-2">The neighbourhood analogy</h4>
<p class="text-sm text-gray-400 leading-relaxed mb-3">
Think of a city. Every house has a full address: <span class="font-mono text-gray-200">district + house number</span>.
A subnet works the same way &mdash; every IP address is split into two parts.
</p>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div class="p-3 bg-purple-900/20 border border-purple-500/30 rounded-lg">
<p class="text-xs font-bold text-purple-300 uppercase tracking-wider mb-1">Network part (district)</p>
<p class="font-mono text-white text-sm mb-1">192.168.1.<span class="text-gray-500">___</span></p>
<p class="text-xs text-gray-400">All devices in the same subnet share this part &mdash; like neighbours on the same street.</p>
</div>
<div class="p-3 bg-blue-900/20 border border-blue-500/30 rounded-lg">
<p class="text-xs font-bold text-blue-300 uppercase tracking-wider mb-1">Host part (house number)</p>
<p class="font-mono text-white text-sm mb-1"><span class="text-gray-500">___</span>.42</p>
<p class="text-xs text-gray-400">Each device gets a unique number within the network.</p>
</div>
</div>
</div>
<!-- What does /24 mean -->
<div>
<h4 class="text-sm font-bold text-purple-300 mb-2">What does the slash mean?</h4>
<p class="text-sm text-gray-400 leading-relaxed mb-3">
An IP address is made up of exactly <strong class="text-white">32 bits</strong> (ones and zeros) under the hood.
The <span class="font-mono text-purple-300">/24</span> says: &ldquo;the first 24 bits belong to the network, the remaining 8 bits are the host number.&rdquo;
</p>
<div class="p-3 bg-gray-900/60 rounded-lg border border-gray-700/30 font-mono text-xs overflow-x-auto">
<div class="flex items-center gap-2 mb-1 min-w-max">
<span class="text-gray-500 w-20 shrink-0">192.168.1.42</span>
<span class="text-gray-600">=</span>
<span class="text-purple-300">11000000.10101000.00000001</span><span class="text-gray-600">.</span><span class="text-blue-300">00101010</span>
</div>
<div class="flex items-center gap-2 min-w-max">
<span class="text-gray-500 w-20 shrink-0">/24 mask</span>
<span class="text-gray-600">=</span>
<span class="text-purple-300">11111111.11111111.11111111</span><span class="text-gray-600">.</span><span class="text-blue-300">00000000</span>
</div>
<div class="flex gap-2 mt-2 min-w-max">
<span class="w-20 shrink-0"></span>
<span class="text-gray-600 ml-1">&nbsp;&nbsp;&nbsp;</span>
<span class="text-purple-400 text-xs">&larr;&mdash;&mdash;&mdash; network (24 bits) &mdash;&mdash;&mdash;&rarr;</span>
<span class="text-blue-400 text-xs">&larr; host (8 bits) &rarr;</span>
</div>
</div>
<p class="text-xs text-gray-500 mt-2">8 host bits = 2<sup>8</sup> = 256 addresses, 254 usable (minus network ID and broadcast).</p>
</div>
<!-- Why subnets -->
<div>
<h4 class="text-sm font-bold text-purple-300 mb-2">Why do subnets exist?</h4>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3 text-xs">
<div class="p-3 bg-gray-900/40 rounded-lg border border-gray-700/30">
<p class="font-semibold text-white mb-1">Organisation</p>
<p class="text-gray-400">Group devices logically &mdash; e.g. keep office PCs separate from servers or guest Wi-Fi.</p>
</div>
<div class="p-3 bg-gray-900/40 rounded-lg border border-gray-700/30">
<p class="font-semibold text-white mb-1">Security</p>
<p class="text-gray-400">Isolate networks from each other &mdash; malware on the guest network can't reach corporate systems.</p>
</div>
<div class="p-3 bg-gray-900/40 rounded-lg border border-gray-700/30">
<p class="font-semibold text-white mb-1">Efficiency</p>
<p class="text-gray-400">Broadcast traffic stays within the subnet &mdash; no unnecessary noise for the rest of the network.</p>
</div>
</div>
</div>
</div>
</details>
</div>
<!-- EXPERT MODE -->
<div id="pro-mode" class="hidden">
<form id="subnet-form" class="mb-8 glass-card p-6 rounded-xl">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-4">
<div>
<label for="ip-address" class="block text-gray-400 text-sm font-bold mb-2 uppercase tracking-wide">IP Address:</label>
<input type="text" id="ip-address" name="ip-address" placeholder="e.g. 192.168.1.1" required
class="w-full px-4 py-3 bg-gray-900/50 border border-gray-700/50 rounded-lg text-gray-200 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent font-mono transition-all">
<p class="text-xs text-gray-500 mt-1">IPv4 in dotted-decimal notation</p>
</div>
<div>
<label for="cidr" class="block text-gray-400 text-sm font-bold mb-2 uppercase tracking-wide">CIDR / Mask:</label>
<input type="text" id="cidr" name="cidr" placeholder="e.g. 24 or 255.255.255.0" required
class="w-full px-4 py-3 bg-gray-900/50 border border-gray-700/50 rounded-lg text-gray-200 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent font-mono transition-all">
<p class="text-xs text-gray-500 mt-1">CIDR (0&ndash;32) or subnet mask (e.g. 255.255.255.0)</p>
</div>
</div>
<div id="subnet-error" class="hidden mb-4 p-3 bg-red-900/20 border border-red-500/30 rounded text-red-400 text-sm"></div>
<button type="submit"
class="w-full bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-500 hover:to-pink-500 text-white font-bold py-3 px-6 rounded-lg shadow-lg hover:shadow-purple-500/25 transition-all duration-200 ease-in-out transform hover:-translate-y-0.5">
Calculate
</button>
</form>
<div id="results" class="glass-card rounded-xl p-6 hidden fade-in">
<h3 class="text-xl font-bold text-purple-300 border-b border-purple-500/30 pb-2 mb-4 flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
Results:
</h3>
<div class="space-y-2 text-sm mb-6">
<div class="grid grid-cols-1 sm:grid-cols-3 gap-x-4 gap-y-1 border-b border-gray-700/50 pb-2 items-start">
<span class="text-gray-400 font-semibold">Network Address:</span>
<span id="network-address" class="font-mono text-white font-semibold">-</span>
<span class="text-xs text-gray-500 italic">First address of the network &mdash; not assignable to hosts</span>
</div>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-x-4 gap-y-1 border-b border-gray-700/50 pb-2 items-start">
<span class="text-gray-400 font-semibold">Broadcast Address:</span>
<span id="broadcast-address" class="font-mono text-purple-400 font-semibold">-</span>
<span class="text-xs text-gray-500 italic">Last address &mdash; delivers packets to all hosts simultaneously</span>
</div>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-x-4 gap-y-1 border-b border-gray-700/50 pb-2 items-start">
<span class="text-gray-400 font-semibold">Subnet Mask:</span>
<span id="subnet-mask" class="font-mono text-gray-300">-</span>
<span class="text-xs text-gray-500 italic">Separates the network and host portions of the IP address</span>
</div>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-x-4 gap-y-1 border-b border-gray-700/50 pb-2 items-start">
<span class="text-gray-400 font-semibold">Usable Hosts:</span>
<span id="host-count" class="font-mono text-green-400 font-bold">-</span>
<span class="text-xs text-gray-500 italic">Usable IPs = 2<sup>host bits</sup> &minus; 2</span>
</div>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-x-4 gap-y-1 border-b border-gray-700/50 pb-2 items-start">
<span class="text-gray-400 font-semibold">First Host:</span>
<span id="first-host" class="font-mono text-blue-300">-</span>
<span class="text-xs text-gray-500 italic">Network address + 1</span>
</div>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-x-4 gap-y-1 items-start">
<span class="text-gray-400 font-semibold">Last Host:</span>
<span id="last-host" class="font-mono text-blue-300">-</span>
<span class="text-xs text-gray-500 italic">Broadcast &minus; 1</span>
</div>
</div>
<!-- Binary visualization -->
<div class="mt-4 p-4 bg-gray-900/60 rounded-lg border border-gray-700/30">
<h4 class="text-xs text-gray-400 uppercase tracking-wider font-bold mb-3">Binary Representation</h4>
<div class="overflow-x-auto">
<div class="font-mono text-xs space-y-2 min-w-max">
<div class="flex items-center gap-3">
<span class="text-gray-500 w-16 text-right shrink-0 text-xs">IP:</span>
<span id="bin-ip" class="tracking-wide"></span>
</div>
<div class="flex items-center gap-3">
<span class="text-gray-500 w-16 text-right shrink-0 text-xs">Mask:</span>
<span id="bin-mask" class="tracking-wide"></span>
</div>
<div class="flex items-center gap-3">
<span class="text-gray-500 w-16 text-right shrink-0 text-xs">Network:</span>
<span id="bin-net" class="tracking-wide"></span>
</div>
<div class="flex items-center gap-3 pt-1 border-t border-gray-700/30">
<span class="w-16 shrink-0"></span>
<span id="bin-legend" class="text-xs text-gray-500"></span>
</div>
</div>
</div>
</div>
</div>
<!-- Example subnets -->
<div class="glass-card rounded-xl p-6 mt-8">
<h3 class="text-lg font-bold text-gray-400 uppercase tracking-wider border-b border-gray-700/50 pb-2 mb-4">
Example Subnets (Private Address Ranges)
</h3>
<div class="overflow-x-auto">
<table class="min-w-full text-sm text-left text-gray-400">
<thead class="text-xs uppercase bg-gray-800/50 text-gray-200">
<tr>
<th class="px-6 py-3">Range</th>
<th class="px-6 py-3">CIDR</th>
<th class="px-6 py-3">Subnet Mask</th>
<th class="px-6 py-3">Description</th>
<th class="px-6 py-3">Action</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-700/50">
<tr class="hover:bg-gray-700/30 transition-colors">
<td class="px-6 py-4 font-mono text-white">192.168.0.0 &ndash; 192.168.255.255</td>
<td class="px-6 py-4 font-mono">/16 (total)</td>
<td class="px-6 py-4 font-mono">255.255.0.0</td>
<td class="px-6 py-4">Class C (commonly used as /24)</td>
<td class="px-6 py-4"><span class="example-link text-purple-400 hover:text-purple-300 cursor-pointer underline" data-ip="192.168.1.1" data-cidr="24">Example /24</span></td>
</tr>
<tr class="hover:bg-gray-700/30 transition-colors">
<td class="px-6 py-4 font-mono text-white">172.16.0.0 &ndash; 172.31.255.255</td>
<td class="px-6 py-4 font-mono">/12 (total)</td>
<td class="px-6 py-4 font-mono">255.240.0.0</td>
<td class="px-6 py-4">Class B</td>
<td class="px-6 py-4"><span class="example-link text-purple-400 hover:text-purple-300 cursor-pointer underline" data-ip="172.16.10.5" data-cidr="16">Example /16</span></td>
</tr>
<tr class="hover:bg-gray-700/30 transition-colors">
<td class="px-6 py-4 font-mono text-white">10.0.0.0 &ndash; 10.255.255.255</td>
<td class="px-6 py-4 font-mono">/8 (total)</td>
<td class="px-6 py-4 font-mono">255.0.0.0</td>
<td class="px-6 py-4">Class A</td>
<td class="px-6 py-4"><span class="example-link text-purple-400 hover:text-purple-300 cursor-pointer underline" data-ip="10.0.50.100" data-cidr="8">Example /8</span></td>
</tr>
</tbody>
</table>
</div>
<p class="mt-4 text-xs text-gray-500 italic">Click an example to populate the fields and run the calculation.</p>
</div>
<p class="mt-4 text-xs text-gray-500 italic">Klicken Sie auf "Beispiel", um die Felder auszufüllen und die Berechnung zu starten.</p>
</div>
</div>`,
init() {
// ─── Shared helpers ────────────────────────────────────────────────────────
function ipToBinary(ip) {
return ip.split('.').map(o => parseInt(o, 10).toString(2).padStart(8, '0')).join('');
}
function binaryToIp(b) {
const parts = [];
for (let i = 0; i < 32; i += 8) parts.push(parseInt(b.slice(i, i + 8), 2));
return parts.join('.');
}
function cidrToMask(cidr) {
return binaryToIp('1'.repeat(cidr) + '0'.repeat(32 - cidr));
}
function maskToCidr(mask) {
const b = ipToBinary(mask);
if (/^1*0*$/.test(b)) return b.replace(/0+$/, '').length;
return null;
}
function isValidIP(ip) {
return /^(25[0-5]|2[0-4]\d|[01]?\d\d?)\.(25[0-5]|2[0-4]\d|[01]?\d\d?)\.(25[0-5]|2[0-4]\d|[01]?\d\d?)\.(25[0-5]|2[0-4]\d|[01]?\d\d?)$/.test(ip);
}
function calcSubnet(ip, cidr) {
const ipBin = ipToBinary(ip);
const maskBin = '1'.repeat(cidr) + '0'.repeat(32 - cidr);
let netBin = '';
for (let i = 0; i < 32; i++) netBin += (parseInt(ipBin[i]) & parseInt(maskBin[i])).toString();
const hostBits = 32 - cidr;
const bcBin = netBin.slice(0, cidr) + '1'.repeat(hostBits);
const netNum = parseInt(netBin, 2);
const bcNum = parseInt(bcBin, 2);
let hosts, first, last;
if (hostBits >= 2) {
hosts = Math.pow(2, hostBits) - 2;
first = binaryToIp((netNum + 1).toString(2).padStart(32, '0'));
last = binaryToIp((bcNum - 1).toString(2).padStart(32, '0'));
} else if (cidr === 31) {
hosts = 2; first = binaryToIp(netBin); last = binaryToIp(bcBin);
} else {
hosts = 1; first = binaryToIp(netBin); last = binaryToIp(netBin);
}
return { network: binaryToIp(netBin), broadcast: binaryToIp(bcBin), mask: cidrToMask(cidr), hosts, first, last, netBin, ipBin, maskBin, cidr, hostBits };
}
// ─── Mode toggle ──────────────────────────────────────────────────────────
const beginnerEl = document.getElementById('beginner-mode');
const proEl = document.getElementById('pro-mode');
const btnBeg = document.getElementById('btn-beginner');
const btnPro = document.getElementById('btn-pro');
const activeClass = 'px-6 py-2 rounded-lg text-sm font-semibold transition-all duration-200 bg-gradient-to-r from-purple-600 to-pink-600 text-white shadow-lg';
const inactiveClass = 'px-6 py-2 rounded-lg text-sm font-semibold transition-all duration-200 text-gray-400 hover:text-gray-200';
function setMode(mode) {
const isBeg = mode === 'beginner';
beginnerEl.classList.toggle('hidden', !isBeg);
proEl.classList.toggle('hidden', isBeg);
btnBeg.className = isBeg ? activeClass : inactiveClass;
btnPro.className = isBeg ? inactiveClass : activeClass;
}
btnBeg.addEventListener('click', () => setMode('beginner'));
btnPro.addEventListener('click', () => setMode('pro'));
// ─── Beginner mode ────────────────────────────────────────────────────────
const begIpInput = document.getElementById('beg-ip');
const begSlider = document.getElementById('beg-slider');
const begCidrLabel = document.getElementById('beg-cidr-label');
const begBar = document.getElementById('beg-bar');
const begHostsCount = document.getElementById('beg-hosts-count');
const begMaskDisplay = document.getElementById('beg-mask-display');
const begNetwork = document.getElementById('beg-network');
const begFirst = document.getElementById('beg-first');
const begLast = document.getElementById('beg-last');
const begBroadcast = document.getElementById('beg-broadcast');
const begExplain = document.getElementById('beg-explain-text');
function updateBeginner() {
const ip = begIpInput.value.trim();
const cidr = parseInt(begSlider.value, 10);
begCidrLabel.textContent = `/${cidr}`;
// bar: log scale via host-bit count
const barPct = Math.max(3, ((32 - cidr) / 31) * 100);
begBar.style.width = barPct + '%';
if (!isValidIP(ip)) return;
const r = calcSubnet(ip, cidr);
begHostsCount.textContent = r.hosts.toLocaleString('en');
begMaskDisplay.textContent = r.mask;
begNetwork.textContent = r.network;
begFirst.textContent = r.first;
begLast.textContent = r.last;
begBroadcast.textContent = r.broadcast;
let sizeDesc;
if (cidr <= 8) sizeDesc = 'This is a massive network &mdash; only found in large data centres or at internet service providers.';
else if (cidr <= 12) sizeDesc = 'This is a very large network, typical for big enterprises or campus environments.';
else if (cidr <= 16) sizeDesc = 'This is a large network, common in mid-sized companies or universities.';
else if (cidr <= 20) sizeDesc = 'This is a medium-sized network, e.g. for a large office building or campus.';
else if (cidr <= 24) sizeDesc = 'This is a typical home or small office network &mdash; the default on most routers.';
else if (cidr <= 27) sizeDesc = 'This is a small network, e.g. for a single department or server cluster.';
else sizeDesc = 'This is a very small network, usually used for direct point-to-point links.';
begExplain.innerHTML = `
A <strong class="text-purple-300">/${cidr}</strong> network reserves
<strong class="text-purple-300">${cidr} bits for the network address</strong> and leaves
<strong class="text-purple-300">${r.hostBits} bits for hosts</strong>.
That gives <strong class="text-green-400">${r.hosts.toLocaleString('en')} usable IP addresses</strong>
(2<sup>${r.hostBits}</sup>&minus;2, since the network ID and broadcast are reserved).
<br><br>${sizeDesc}
`;
}
begSlider.addEventListener('input', updateBeginner);
begIpInput.addEventListener('input', updateBeginner);
document.querySelectorAll('.beg-example').forEach(btn => {
btn.addEventListener('click', () => {
begIpInput.value = btn.dataset.ip;
begSlider.value = btn.dataset.cidr;
updateBeginner();
});
});
updateBeginner();
// ─── Expert mode ──────────────────────────────────────────────────────────
const form = document.getElementById('subnet-form');
const ipInput = document.getElementById('ip-address');
const cidrInput = document.getElementById('cidr');
@@ -117,28 +499,26 @@ export const page = {
errorEl.classList.toggle('hidden', !msg);
}
function isValidIP(ip) {
return /^(25[0-5]|2[0-4]\d|[01]?\d\d?)\.(25[0-5]|2[0-4]\d|[01]?\d\d?)\.(25[0-5]|2[0-4]\d|[01]?\d\d?)\.(25[0-5]|2[0-4]\d|[01]?\d\d?)$/.test(ip);
function coloredBits(bits, cidr, hostChar) {
let html = '';
for (let i = 0; i < 32; i++) {
if (i > 0 && i % 8 === 0) html += '<span class="text-gray-600 select-none">.</span>';
const isNet = i < cidr;
const ch = hostChar && !isNet ? hostChar : bits[i];
html += `<span class="${isNet ? 'text-purple-300' : 'text-gray-500'}">${ch}</span>`;
}
return html;
}
function ipToBinary(ip) {
return ip.split('.').map(o => parseInt(o, 10).toString(2).padStart(8, '0')).join('');
}
function binaryToIp(b) {
const parts = [];
for (let i = 0; i < 32; i += 8) parts.push(parseInt(b.slice(i, i + 8), 2));
return parts.join('.');
}
function cidrToMask(cidr) {
return binaryToIp('1'.repeat(cidr) + '0'.repeat(32 - cidr));
}
function maskToCidr(mask) {
const b = ipToBinary(mask);
if (/^1*0*$/.test(b)) return b.replace(/0+$/, '').length;
return null;
function renderBinary(r) {
const maskBits = '1'.repeat(r.cidr) + '0'.repeat(r.hostBits);
document.getElementById('bin-ip').innerHTML = coloredBits(r.ipBin, r.cidr, null);
document.getElementById('bin-mask').innerHTML = coloredBits(maskBits, r.cidr, null);
document.getElementById('bin-net').innerHTML = coloredBits(r.netBin, r.cidr, 'x');
document.getElementById('bin-legend').innerHTML =
`<span class="text-purple-400">&#x25A0;</span> network bit (${r.cidr}) &nbsp;&nbsp; ` +
`<span class="text-gray-600">&#x25A1;</span> host bit (${r.hostBits}) &nbsp;&mdash;&nbsp; ` +
`x = any host address within this network`;
}
function calculate() {
@@ -146,48 +526,28 @@ export const page = {
const ip = ipInput.value.trim();
const cidrRaw = cidrInput.value.trim();
if (!isValidIP(ip)) { showInlineError('Bitte eine gültige IPv4-Adresse eingeben.'); return; }
if (!isValidIP(ip)) { showInlineError('Please enter a valid IPv4 address.'); return; }
let cidr, mask;
let cidr;
if (cidrRaw.includes('.')) {
if (!isValidIP(cidrRaw)) { showInlineError('Bitte eine gültige Subnetzmaske eingeben.'); return; }
if (!isValidIP(cidrRaw)) { showInlineError('Please enter a valid subnet mask.'); return; }
cidr = maskToCidr(cidrRaw);
if (cidr === null) { showInlineError('Ungültige Subnetzmaske — muss eine kontinuierliche Folge von Einsen sein (z.B. 255.255.255.0).'); return; }
mask = cidrRaw;
if (cidr === null) { showInlineError('Invalid subnet mask — must be a contiguous sequence of ones (e.g. 255.255.255.0).'); return; }
} else {
cidr = parseInt(cidrRaw, 10);
if (isNaN(cidr) || cidr < 0 || cidr > 32) { showInlineError('Bitte einen gültigen CIDR-Wert (032) eingeben.'); return; }
mask = cidrToMask(cidr);
if (isNaN(cidr) || cidr < 0 || cidr > 32) { showInlineError('Please enter a valid CIDR value (032).'); return; }
}
const ipBin = ipToBinary(ip);
const maskBin = '1'.repeat(cidr) + '0'.repeat(32 - cidr);
let netBin = '';
for (let i = 0; i < 32; i++) netBin += (parseInt(ipBin[i]) & parseInt(maskBin[i])).toString();
const r = calcSubnet(ip, cidr);
const hostBits = 32 - cidr;
const bcBin = netBin.slice(0, cidr) + '1'.repeat(hostBits);
const netNum = parseInt(netBin, 2);
const bcNum = parseInt(bcBin, 2);
let hosts, first, last;
if (hostBits >= 2) {
hosts = Math.pow(2, hostBits) - 2;
first = binaryToIp((netNum + 1).toString(2).padStart(32, '0'));
last = binaryToIp((bcNum - 1).toString(2).padStart(32, '0'));
} else if (cidr === 31) {
hosts = 2; first = binaryToIp(netBin); last = binaryToIp(bcBin);
} else {
hosts = 1; first = binaryToIp(netBin); last = binaryToIp(netBin);
}
document.getElementById('network-address').textContent = binaryToIp(netBin);
document.getElementById('broadcast-address').textContent = binaryToIp(bcBin);
document.getElementById('subnet-mask').textContent = mask;
document.getElementById('host-count').textContent = hosts.toLocaleString();
document.getElementById('first-host').textContent = first;
document.getElementById('last-host').textContent = last;
document.getElementById('network-address').textContent = r.network;
document.getElementById('broadcast-address').textContent = r.broadcast;
document.getElementById('subnet-mask').textContent = r.mask;
document.getElementById('host-count').textContent = r.hosts.toLocaleString('en');
document.getElementById('first-host').textContent = r.first;
document.getElementById('last-host').textContent = r.last;
renderBinary(r);
resultsEl.classList.remove('hidden');
}
+1
View File
@@ -67,6 +67,7 @@ async function navigate(path, { push = true, search = '' } = {}) {
// ── Intercept same-origin link clicks ───────────────────────────
document.addEventListener('click', e => {
if (e.defaultPrevented) return;
const a = e.target.closest('a[href]');
if (!a) return;
let url;
+41 -27
View File
@@ -62,13 +62,16 @@
color: #e5e7eb;
padding: 1rem;
border-radius: 0.375rem;
max-height: 500px;
max-height: 400px;
overflow-y: auto;
font-size: 0.875rem;
font-size: 0.8rem;
border: 1px solid rgba(255, 255, 255, 0.05);
box-shadow: inset 0 2px 4px 0 rgba(0, 0, 0, 0.3);
}
/* ── Tool action buttons (Ping / Traceroute / Port Scan) ──────── */
.action-tool-btn { text-align: center; }
/* ── Scrollbar ─────────────────────────────────────────────────── */
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-track { background: rgba(31, 41, 55, 0.5); }
@@ -230,34 +233,45 @@ header.nav-open #main-nav { display: block; }
#ip-address-link:hover::after { transform: scaleX(1); transform-origin: bottom left; }
/* ── Home page — Traceroute output ─────────────────────────────── */
#traceroute-output pre, .result-pre {
white-space: pre-wrap;
word-break: break-all;
font-family: 'Courier New', Courier, monospace;
background-color: rgba(0,0,0,.3);
color: #e5e7eb;
padding: 1rem;
border-radius: 0.375rem;
max-height: 400px;
overflow-y: auto;
font-size: 0.875rem;
border: 1px solid rgba(255,255,255,.05);
box-shadow: inset 0 2px 4px 0 rgba(0,0,0,.3);
/* Hop rows — structured grid layout with RDNS on its own line */
#traceroute-output .hop-row {
display: grid;
grid-template-columns: 2rem 1fr;
align-items: start;
gap: 0 0.5rem;
padding: 3px 4px;
border-radius: 4px;
border-left: 2px solid transparent;
transition: border-left-color .2s, background .2s;
}
#traceroute-output .hop-line { margin-bottom: .25rem; padding-left: .5rem; border-left: 2px solid transparent; transition: border-left-color .3s; }
#traceroute-output .hop-line:hover { border-left-color: #a855f7; background: rgba(255,255,255,.02); }
#traceroute-output .hop-number { display: inline-block; width: 30px; text-align: right; margin-right: 15px; color: #6b7280; font-weight: bold; }
#traceroute-output .hop-ip { color: #60a5fa; font-weight: 500; }
#traceroute-output .hop-hostname { color: #c084fc; }
#traceroute-output .hop-rtt { color: #34d399; margin-left: 8px; font-size: .85em; }
#traceroute-output .hop-timeout { color: #f87171; }
#traceroute-output .info-line { color: #fbbf24; font-style: italic; }
#traceroute-output .error-line { color: #f87171; font-weight: bold; border-left: 3px solid #f87171; padding-left: 10px; }
#traceroute-output .end-line { color: #d8b4fe; font-weight: bold; margin-top: 15px; text-transform: uppercase; letter-spacing: .05em; border-top: 1px solid rgba(255,255,255,.1); padding-top: 10px; }
#traceroute-output .hop-row:hover { border-left-color: #a855f7; background: rgba(255,255,255,.025); }
#traceroute-output .hop-number { text-align: right; color: #4b5563; font-weight: 700; font-size: .8em; padding-top: 2px; }
#traceroute-output .hop-body { min-width: 0; }
#traceroute-output .hop-ip-line { display: flex; align-items: baseline; gap: 0.5rem; flex-wrap: wrap; }
#traceroute-output .hop-ip { color: #60a5fa; font-weight: 500; flex-shrink: 0; }
#traceroute-output .hop-rtts { display: flex; gap: 0.4rem; margin-left: auto; }
#traceroute-output .hop-rtt { color: #34d399; font-size: .8em; }
#traceroute-output .hop-timeout { color: #f87171; font-size: .85em; }
/* RDNS hostname on its own line — clearly distinguishable from the IP */
#traceroute-output .hop-rdns {
font-size: .75em;
color: #c084fc;
opacity: .85;
margin-top: 1px;
padding-left: 1px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* Non-hop lines (info, error, end) */
#traceroute-output .info-line { color: #fbbf24; font-style: italic; font-size: .85em; }
#traceroute-output .error-line { color: #f87171; font-weight: bold; border-left: 3px solid #f87171; padding-left: 8px; }
#traceroute-output .end-line { color: #d8b4fe; font-weight: bold; margin-top: 8px; text-transform: uppercase; letter-spacing: .05em; border-top: 1px solid rgba(255,255,255,.08); padding-top: 8px; }
/* ── Home page — Maps ───────────────────────────────────────────── */
#map { height: 300px; }
#lookup-map { height: 250px; }
/* Containers use h-[420px] / h-[260px] in HTML; maps fill 100% via ID selector (higher specificity than Tailwind) */
#map { height: 420px; }
#lookup-map { height: 260px; }
/* ── ASN page — Graph ───────────────────────────────────────────── */
#graph-container { width: 100%; height: 600px; background: rgba(0,0,0,.3); border-radius: .75rem; border: 1px solid rgba(255,255,255,.06); overflow: hidden; position: relative; }