Compare commits
No commits in common. "main" and "experimental" have entirely different histories.
main
...
experiment
499 changed files with 5456 additions and 30157 deletions
|
|
@ -114,12 +114,12 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async register(name, email, password, role = 'solo', rememberMe = false, profession = '') {
|
async register(name, email, password, role = 'solo', rememberMe = false) {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/auth/register', {
|
const res = await fetch('/api/auth/register', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ name, email, password, role, profession })
|
body: JSON.stringify({ name, email, password, role })
|
||||||
});
|
});
|
||||||
const data = await res.json().catch(() => ({}));
|
const data = await res.json().catch(() => ({}));
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
|
|
@ -353,7 +353,7 @@
|
||||||
document.body.classList.add('logged-in');
|
document.body.classList.add('logged-in');
|
||||||
if (userSpan) {
|
if (userSpan) {
|
||||||
userSpan.style.display = 'inline-flex';
|
userSpan.style.display = 'inline-flex';
|
||||||
userSpan.innerHTML = `${escapeHtml(user.name || user.email)}`;
|
userSpan.innerHTML = `👤 ${escapeHtml(user.name || user.email)}`;
|
||||||
userSpan.style.cursor = 'pointer';
|
userSpan.style.cursor = 'pointer';
|
||||||
userSpan.title = 'Zum Kurs-Dashboard wechseln';
|
userSpan.title = 'Zum Kurs-Dashboard wechseln';
|
||||||
userSpan.onclick = () => { window.location.href = 'dashboard.html'; };
|
userSpan.onclick = () => { window.location.href = 'dashboard.html'; };
|
||||||
|
|
@ -373,7 +373,7 @@
|
||||||
}
|
}
|
||||||
if (registerBtn) {
|
if (registerBtn) {
|
||||||
registerBtn.style.display = 'inline-flex';
|
registerBtn.style.display = 'inline-flex';
|
||||||
registerBtn.innerHTML = '<span class="btn-text-desktop">Zum Kurs-Dashboard</span><span class="btn-text-mobile">Dashboard</span>';
|
registerBtn.innerHTML = '<span class="btn-text-desktop">💻 Zum Kurs-Dashboard</span><span class="btn-text-mobile">Dashboard</span>';
|
||||||
registerBtn.style.background = '#10b981';
|
registerBtn.style.background = '#10b981';
|
||||||
registerBtn.removeAttribute('onclick');
|
registerBtn.removeAttribute('onclick');
|
||||||
registerBtn.onclick = (e) => {
|
registerBtn.onclick = (e) => {
|
||||||
|
|
@ -382,7 +382,7 @@
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (heroPrimaryCta) {
|
if (heroPrimaryCta) {
|
||||||
heroPrimaryCta.innerHTML = '<span>Zum Dashboard & Kurse wählen →</span>';
|
heroPrimaryCta.innerHTML = '<span>▶ Zum Dashboard & Kurse wählen →</span>';
|
||||||
heroPrimaryCta.removeAttribute('onclick');
|
heroPrimaryCta.removeAttribute('onclick');
|
||||||
heroPrimaryCta.onclick = (e) => {
|
heroPrimaryCta.onclick = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
@ -392,7 +392,7 @@
|
||||||
|
|
||||||
// Alle allgemeinen Landing-Page Action Buttons anpassen
|
// Alle allgemeinen Landing-Page Action Buttons anpassen
|
||||||
document.querySelectorAll('.landing-cta-btn').forEach(btn => {
|
document.querySelectorAll('.landing-cta-btn').forEach(btn => {
|
||||||
btn.innerHTML = '<span>Zum Dashboard & Kurse wählen →</span>';
|
btn.innerHTML = '<span>▶ Zum Dashboard & Kurse wählen →</span>';
|
||||||
btn.style.background = '#10b981';
|
btn.style.background = '#10b981';
|
||||||
btn.removeAttribute('onclick');
|
btn.removeAttribute('onclick');
|
||||||
btn.onclick = (e) => {
|
btn.onclick = (e) => {
|
||||||
|
|
@ -425,7 +425,7 @@
|
||||||
}
|
}
|
||||||
if (loginBtn) {
|
if (loginBtn) {
|
||||||
loginBtn.style.display = 'inline-flex';
|
loginBtn.style.display = 'inline-flex';
|
||||||
loginBtn.innerText = 'Anmelden';
|
loginBtn.innerText = '🔑 Anmelden';
|
||||||
loginBtn.style.background = 'rgba(255,255,255,0.12)';
|
loginBtn.style.background = 'rgba(255,255,255,0.12)';
|
||||||
loginBtn.style.color = 'white';
|
loginBtn.style.color = 'white';
|
||||||
loginBtn.style.borderColor = 'rgba(255,255,255,0.2)';
|
loginBtn.style.borderColor = 'rgba(255,255,255,0.2)';
|
||||||
|
|
@ -433,7 +433,7 @@
|
||||||
}
|
}
|
||||||
if (registerBtn) {
|
if (registerBtn) {
|
||||||
registerBtn.style.display = 'inline-flex';
|
registerBtn.style.display = 'inline-flex';
|
||||||
registerBtn.innerHTML = '<span class="btn-text-desktop">Kostenlos registrieren</span><span class="btn-text-mobile">Starten</span>';
|
registerBtn.innerHTML = '<span class="btn-text-desktop">✨ Kostenlos registrieren</span><span class="btn-text-mobile">Starten</span>';
|
||||||
registerBtn.style.background = '#0284c7';
|
registerBtn.style.background = '#0284c7';
|
||||||
registerBtn.onclick = () => window.openAuthModal('register', { allowClose: true });
|
registerBtn.onclick = () => window.openAuthModal('register', { allowClose: true });
|
||||||
}
|
}
|
||||||
|
|
@ -443,7 +443,7 @@
|
||||||
const mobLogoutBtn = document.getElementById('mobile-drawer-logout-btn');
|
const mobLogoutBtn = document.getElementById('mobile-drawer-logout-btn');
|
||||||
if (mobLogoutBtn) mobLogoutBtn.style.display = 'none';
|
if (mobLogoutBtn) mobLogoutBtn.style.display = 'none';
|
||||||
if (heroPrimaryCta) {
|
if (heroPrimaryCta) {
|
||||||
heroPrimaryCta.innerHTML = '<span>Jetzt kostenlos registrieren →</span>';
|
heroPrimaryCta.innerHTML = '<span>🚀 Jetzt kostenlos registrieren</span>';
|
||||||
heroPrimaryCta.onclick = (e) => {
|
heroPrimaryCta.onclick = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
window.openAuthModal('register', { allowClose: true });
|
window.openAuthModal('register', { allowClose: true });
|
||||||
|
|
@ -452,7 +452,7 @@
|
||||||
|
|
||||||
// Alle allgemeinen Landing-Page Action Buttons für Besucher
|
// Alle allgemeinen Landing-Page Action Buttons für Besucher
|
||||||
document.querySelectorAll('.landing-cta-btn').forEach(btn => {
|
document.querySelectorAll('.landing-cta-btn').forEach(btn => {
|
||||||
btn.innerHTML = '<span>Jetzt kostenlos registrieren & Kurse wählen →</span>';
|
btn.innerHTML = '<span>✨ Jetzt kostenlos registrieren & Kurse wählen →</span>';
|
||||||
btn.style.background = '#0284c7';
|
btn.style.background = '#0284c7';
|
||||||
btn.onclick = (e) => {
|
btn.onclick = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
@ -489,19 +489,19 @@
|
||||||
// Aktualisiere ggf. vorhandene Curriculum-Aktions-Buttons
|
// Aktualisiere ggf. vorhandene Curriculum-Aktions-Buttons
|
||||||
document.querySelectorAll('.portal-card-action-btn').forEach(btn => {
|
document.querySelectorAll('.portal-card-action-btn').forEach(btn => {
|
||||||
if (loggedIn) {
|
if (loggedIn) {
|
||||||
btn.innerHTML = '<span>Im Lernbereich öffnen</span> <span>→</span>';
|
btn.innerHTML = '<span>▶ Im Lernbereich öffnen</span> <span>→</span>';
|
||||||
btn.classList.remove('locked');
|
btn.classList.remove('locked');
|
||||||
btn.style.background = '#10b981';
|
btn.style.background = '#10b981';
|
||||||
btn.onclick = () => { window.location.href = 'dashboard.html'; };
|
btn.onclick = () => { window.location.href = 'dashboard.html'; };
|
||||||
} else {
|
} else {
|
||||||
btn.innerHTML = '<span>Lehrplan ansehen</span>';
|
btn.innerHTML = '<span>✨ Lehrplan freischalten & starten</span>';
|
||||||
btn.classList.add('locked');
|
btn.classList.add('locked');
|
||||||
btn.style.background = '#0284c7';
|
btn.style.background = '#0284c7';
|
||||||
btn.onclick = (e) => {
|
btn.onclick = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
window.openAuthModal('register', {
|
window.openAuthModal('register', {
|
||||||
allowClose: true,
|
allowClose: true,
|
||||||
title: 'Lehrplan freischalten',
|
title: '🔒 Lehrplan freischalten',
|
||||||
message: 'Erstelle einen kostenlosen Account in unter 30 Sekunden, um sofort vollen Zugriff auf alle Kurse und Module im Lernbereich zu erhalten.'
|
message: 'Erstelle einen kostenlosen Account in unter 30 Sekunden, um sofort vollen Zugriff auf alle Kurse und Module im Lernbereich zu erhalten.'
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
@ -534,10 +534,10 @@
|
||||||
` : ''}
|
` : ''}
|
||||||
|
|
||||||
<div style="display: flex; align-items: center; gap: 10px; margin-bottom: 14px;">
|
<div style="display: flex; align-items: center; gap: 10px; margin-bottom: 14px;">
|
||||||
<div style="width: 36px; height: 36px; border-radius: 8px; background: #eff6ff; border: 1px solid #bfdbfe; display: flex; align-items: center; justify-content: center; font-size: 1.05rem; color: #1d4ed8; font-weight: 800;">IT</div>
|
<div style="width: 36px; height: 36px; border-radius: 8px; background: rgba(56, 189, 248, 0.15); border: 1px solid rgba(56, 189, 248, 0.4); display: flex; align-items: center; justify-content: center; font-size: 1.2rem;">🎓</div>
|
||||||
<div>
|
<div>
|
||||||
<div style="font-size: 1.15rem; font-weight: 800; color: #0f172a;" id="auth-modal-title">
|
<div style="font-size: 1.15rem; font-weight: 800; color: #0f172a;" id="auth-modal-title">
|
||||||
${escapeHtml(options.title || (initialMode === 'register' && allowReg ? 'Kostenlos registrieren' : 'Anmelden'))}
|
${escapeHtml(options.title || (initialMode === 'register' && allowReg ? '✨ Kostenlos registrieren' : '🔑 Anmelden'))}
|
||||||
</div>
|
</div>
|
||||||
<div style="font-size: 0.72rem; color: #64748b; font-weight: 700; text-transform: uppercase; letter-spacing: 0.6px;">IT-PRAXISPORTAL AUTHENTICATION</div>
|
<div style="font-size: 0.72rem; color: #64748b; font-weight: 700; text-transform: uppercase; letter-spacing: 0.6px;">IT-PRAXISPORTAL AUTHENTICATION</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -576,27 +576,12 @@
|
||||||
<div id="auth-field-role" style="display: none;">
|
<div id="auth-field-role" style="display: none;">
|
||||||
<label style="display: block; font-size: 0.8rem; font-weight: 700; color: #0f172a; margin-bottom: 4px;">Deine Rolle / Lernziel:</label>
|
<label style="display: block; font-size: 0.8rem; font-weight: 700; color: #0f172a; margin-bottom: 4px;">Deine Rolle / Lernziel:</label>
|
||||||
<select id="auth-select-role" style="width: 100%; padding: 9px 12px; border: 1px solid #cbd5e1; border-radius: 8px; font-size: 0.88rem; outline: none; background: white; box-sizing: border-box;">
|
<select id="auth-select-role" style="width: 100%; padding: 9px 12px; border: 1px solid #cbd5e1; border-radius: 8px; font-size: 0.88rem; outline: none; background: white; box-sizing: border-box;">
|
||||||
<option value="solo">Solo-Selbstlerner & Quereinsteiger</option>
|
<option value="solo">🎓 Solo-Selbstlerner & Quereinsteiger</option>
|
||||||
<option value="student">Auszubildender IT-Beruf</option>
|
<option value="student">💼 Auszubildender Fachinformatiker (FIAE / FISI)</option>
|
||||||
<option value="teacher">Lehrkraft / Dozent / Ausbilder</option>
|
<option value="teacher">👨🏫 Lehrkraft / Dozent / Ausbilder</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="auth-field-profession" style="display: none;">
|
|
||||||
<label style="display: block; font-size: 0.8rem; font-weight: 700; color: #0f172a; margin-bottom: 4px;">Dein IT-Ausbildungsberuf (optional):</label>
|
|
||||||
<select id="auth-select-profession" style="width: 100%; padding: 9px 12px; border: 1px solid #cbd5e1; border-radius: 8px; font-size: 0.88rem; outline: none; background: white; box-sizing: border-box;">
|
|
||||||
<option value="">Keine Angabe / Quereinsteiger</option>
|
|
||||||
<option value="FISI">Fachinformatiker Systemintegration (FISI)</option>
|
|
||||||
<option value="FIAE">Fachinformatiker Anwendungsentwicklung (FIAE)</option>
|
|
||||||
<option value="FIDP">Fachinformatiker Daten- & Prozessanalyse (FIDP)</option>
|
|
||||||
<option value="FIDV">Fachinformatiker Digitale Vernetzung (FIDV)</option>
|
|
||||||
<option value="ITSE">IT-System-Elektroniker/in (ITSE)</option>
|
|
||||||
<option value="ITSM">Kaufleute für IT-System-Management (ITSM)</option>
|
|
||||||
<option value="KDM">Kaufleute für Digitalisierungsmanagement (KDM)</option>
|
|
||||||
</select>
|
|
||||||
<span style="display: block; font-size: 0.74rem; color: #64748b; margin-top: 3px;">Schaltet deinen spezifischen IHK AP2-Prüfungskurs auf dem Dashboard frei.</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Session-Einstellung (Standard: nicht angehakt für Login-Zwang nach Schließen des Browsers) -->
|
<!-- Session-Einstellung (Standard: nicht angehakt für Login-Zwang nach Schließen des Browsers) -->
|
||||||
<div style="display: flex; align-items: center; gap: 8px; margin-top: 2px;">
|
<div style="display: flex; align-items: center; gap: 8px; margin-top: 2px;">
|
||||||
<input type="checkbox" id="auth-remember-me" style="width: 16px; height: 16px; cursor: pointer;">
|
<input type="checkbox" id="auth-remember-me" style="width: 16px; height: 16px; cursor: pointer;">
|
||||||
|
|
@ -621,7 +606,7 @@
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div style="margin-top: 14px; text-align: center; font-size: 0.76rem; color: #94a3b8;">
|
<div style="margin-top: 14px; text-align: center; font-size: 0.76rem; color: #94a3b8;">
|
||||||
100% DSGVO-konform • Serverstandort Deutschland • Keine Tracking-Cookies
|
🔒 100% DSGVO-konform • Deutsche Server • Keine Tracking-Cookies
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
@ -633,7 +618,6 @@
|
||||||
const tabReg = document.getElementById('auth-tab-register');
|
const tabReg = document.getElementById('auth-tab-register');
|
||||||
const nameField = document.getElementById('auth-field-name');
|
const nameField = document.getElementById('auth-field-name');
|
||||||
const roleField = document.getElementById('auth-field-role');
|
const roleField = document.getElementById('auth-field-role');
|
||||||
const profField = document.getElementById('auth-field-profession');
|
|
||||||
const submitBtn = document.getElementById('auth-submit-btn');
|
const submitBtn = document.getElementById('auth-submit-btn');
|
||||||
const errorBox = document.getElementById('auth-error-box');
|
const errorBox = document.getElementById('auth-error-box');
|
||||||
const form = document.getElementById('auth-form');
|
const form = document.getElementById('auth-form');
|
||||||
|
|
@ -650,9 +634,8 @@
|
||||||
tabLog.style.color = '#64748b';
|
tabLog.style.color = '#64748b';
|
||||||
tabLog.style.boxShadow = 'none';
|
tabLog.style.boxShadow = 'none';
|
||||||
|
|
||||||
if (nameField) nameField.style.display = 'block';
|
nameField.style.display = 'block';
|
||||||
if (roleField) roleField.style.display = 'block';
|
roleField.style.display = 'block';
|
||||||
if (profField) profField.style.display = 'block';
|
|
||||||
submitBtn.innerText = 'Jetzt kostenlos registrieren →';
|
submitBtn.innerText = 'Jetzt kostenlos registrieren →';
|
||||||
submitBtn.style.background = '#0284c7';
|
submitBtn.style.background = '#0284c7';
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -665,9 +648,8 @@
|
||||||
tabReg.style.boxShadow = 'none';
|
tabReg.style.boxShadow = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nameField) nameField.style.display = 'none';
|
nameField.style.display = 'none';
|
||||||
if (roleField) roleField.style.display = 'none';
|
roleField.style.display = 'none';
|
||||||
if (profField) profField.style.display = 'none';
|
|
||||||
submitBtn.innerText = 'Anmelden →';
|
submitBtn.innerText = 'Anmelden →';
|
||||||
submitBtn.style.background = '#059669';
|
submitBtn.style.background = '#059669';
|
||||||
}
|
}
|
||||||
|
|
@ -696,7 +678,6 @@
|
||||||
const password = document.getElementById('auth-input-password').value;
|
const password = document.getElementById('auth-input-password').value;
|
||||||
const name = document.getElementById('auth-input-name')?.value.trim() || email.split('@')[0];
|
const name = document.getElementById('auth-input-name')?.value.trim() || email.split('@')[0];
|
||||||
const role = document.getElementById('auth-select-role')?.value || 'solo';
|
const role = document.getElementById('auth-select-role')?.value || 'solo';
|
||||||
const profession = document.getElementById('auth-select-profession')?.value || '';
|
|
||||||
const rememberMe = document.getElementById('auth-remember-me')?.checked || false;
|
const rememberMe = document.getElementById('auth-remember-me')?.checked || false;
|
||||||
|
|
||||||
submitBtn.disabled = true;
|
submitBtn.disabled = true;
|
||||||
|
|
@ -704,7 +685,7 @@
|
||||||
|
|
||||||
let result;
|
let result;
|
||||||
if (isRegister) {
|
if (isRegister) {
|
||||||
result = await window.AUTH.register(name, email, password, role, rememberMe, profession);
|
result = await window.AUTH.register(name, email, password, role, rememberMe);
|
||||||
} else {
|
} else {
|
||||||
result = await window.AUTH.login(email, password, rememberMe);
|
result = await window.AUTH.login(email, password, rememberMe);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -691,91 +691,97 @@ window.COURSES_MANIFEST = {
|
||||||
},
|
},
|
||||||
"ihk_ap1": {
|
"ihk_ap1": {
|
||||||
"id": "ihk_ap1",
|
"id": "ihk_ap1",
|
||||||
"title": "IHK AP1 Prüfungstraining: Einrichten eines IT-gestützten Arbeitsplatzes",
|
"title": "IHK AP1 Crashkurs: IT-Arbeitsplatz & Programmierung",
|
||||||
"language": "ihk_ap1",
|
"language": "ihk_ap1",
|
||||||
"icon": "📘",
|
"icon": "📘",
|
||||||
"runner": "none",
|
"runner": "none",
|
||||||
"description": "Offizieller Vorbereitungskurs auf die IHK-Abschlussprüfung Teil 1 (Neuordnung nach ZPA-Katalog 2. Auflage). Alle Lernfelder (LF 1 bis LF 6), BPMN 2.0, EPK, PrintTop-Belegrechnung, Netzplantechnik & 171-Fragen-Pool.",
|
"description": "Lückenlose Vorbereitung auf die IHK-Abschlussprüfung Teil 1. Alle Lernfelder (LF 1 bis LF 8) mit interaktiven Übungen, 150-Fragen-Pool & 90-Minuten Probeprüfung.",
|
||||||
"tracks": [
|
"tracks": [
|
||||||
{
|
{
|
||||||
"id": "track_1_it_systeme",
|
"id": "track_1_it_systeme",
|
||||||
"title": "🌱 Lehrpfad 1: Basissysteme, Hardware & Netzwerke (LF 1–4)",
|
"title": "🌱 Lehrpfad 1: Basissysteme & Sicherheit (LF 1–4)",
|
||||||
"description": "Unternehmen & BGB-Verträge, Hardware, Ergonomie & IEC-Binärpräfixe, Subnetting & Netzwerke, Schutzbedarfsanalyse & DSGVO.",
|
"description": "Unternehmen & Rolle im Betrieb, Hardware & Ergonomie, Subnetting & Netzwerke, Schutzbedarfsanalyse & DSGVO.",
|
||||||
"certificateKey": "ihk_ap1_systeme",
|
"certificateKey": "ihk_ap1_systeme",
|
||||||
"chapters": [
|
"chapters": [
|
||||||
{
|
{
|
||||||
"folder": "01_lf1_unternehmen_und_arbeitsplatz",
|
"folder": "01_lf1_unternehmen_und_arbeitsplatz",
|
||||||
"title": "LF 1: Unternehmen & Rolle im Betrieb (Organisation, Verträge, ITIL 4 & KI-Support)",
|
"title": "LF 1: Unternehmen & Rolle im Betrieb (Organisation, Verträge, ITIL)",
|
||||||
"taskFile": "README.md",
|
"taskFile": "README.md",
|
||||||
"testFile": "index.html"
|
"testFile": "index.html"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"folder": "02_lf2_arbeitsplaetze_ausstatten",
|
"folder": "02_lf2_arbeitsplaetze_ausstatten",
|
||||||
"title": "LF 2: Arbeitsplätze ausstatten (Displays, ArbStättV, USV & IEC-Binärpräfixe)",
|
"title": "LF 2: Arbeitsplätze ausstatten (Hardware, Ergonomie, USV, NWA)",
|
||||||
"taskFile": "README.md",
|
"taskFile": "README.md",
|
||||||
"testFile": "index.html"
|
"testFile": "index.html"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"folder": "03_lf3_clients_in_netzwerke_einbinden",
|
"folder": "03_lf3_clients_in_netzwerke_einbinden",
|
||||||
"title": "LF 3: Clients in Netzwerke einbinden (IPv4/IPv6, Subnetting, LWL & Übertragungszeiten)",
|
"title": "LF 3: Clients in Netzwerke einbinden (Subnetting, IPv6, LWL)",
|
||||||
"taskFile": "README.md",
|
"taskFile": "README.md",
|
||||||
"testFile": "index.html"
|
"testFile": "index.html"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"folder": "04_lf4_schutzbedarfsanalyse_und_cybersicherheit",
|
"folder": "04_lf4_schutzbedarfsanalyse_und_cybersicherheit",
|
||||||
"title": "LF 4: Schutzbedarfsanalyse & Cyber-Sicherheit (CIA, Backup, TOM & KI-Datenschutz)",
|
"title": "LF 4: Schutzbedarfsanalyse & Cyber-Sicherheit (CIA, Backup, TOMs)",
|
||||||
"taskFile": "README.md",
|
"taskFile": "README.md",
|
||||||
"testFile": "index.html"
|
"testFile": "index.html"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "track_2_prozesse_software_itsm",
|
"id": "track_2_programmierung_und_datenbanken",
|
||||||
"title": "⚡ Lehrpfad 2: Prozessmodellierung, Software-Design & ITSM (LF 1, 5, 6)",
|
"title": "💻 Lehrpfad 2: Software, CPS & Schnittstellen (LF 5–8)",
|
||||||
"description": "BPMN 2.0 Gateways & EPK, UML-Diagramme & Pseudocode (kein PAP/Struktogramm mehr!), IT-Servicemanagement & SLAs.",
|
"description": "Algorithmen & SQL-Datenbanken, Cyber-physische Systeme, Instandhaltung & Fehlerdiagnose, REST-Schnittstellen.",
|
||||||
"certificateKey": "ihk_ap1_programmierung",
|
"certificateKey": "ihk_ap1_programmierung",
|
||||||
"chapters": [
|
"chapters": [
|
||||||
{
|
{
|
||||||
"folder": "05_prozessmodellierung_bpmn_und_epk",
|
"folder": "05_lf5_software_zur_datenverwaltung",
|
||||||
"title": "BPMN & EPK: Geschäftsprozessmodellierung nach ZPA-Standard (LF 1/6)",
|
"title": "LF 5: Software zur Datenverwaltung anpassen (Trace, UML, SQL)",
|
||||||
"taskFile": "README.md",
|
"taskFile": "README.md",
|
||||||
"testFile": "index.html"
|
"testFile": "index.html"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"folder": "06_lf5_software_und_anwendungsentwicklung",
|
"folder": "06_lf6_cyber_physische_systeme_iot",
|
||||||
"title": "LF 5: Software- & Anwendungsentwicklung (UML, Pseudocode & Trace-Tabellen)",
|
"title": "LF 6: Das Cyber-Physische System ergänzen (Sensoren, Aktoren, MQTT)",
|
||||||
"taskFile": "README.md",
|
"taskFile": "README.md",
|
||||||
"testFile": "index.html"
|
"testFile": "index.html"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"folder": "07_lf6_serviceanfragen_und_itsm",
|
"folder": "07_lf7_instandhaltung_und_fehlerdiagnose",
|
||||||
"title": "LF 6: Serviceanfragen bearbeiten & ITSM (Tickettriage, SLAs & Eskalation)",
|
"title": "LF 7: Cyber-Physische Systeme instand halten (Diagnose, ESD, Messen)",
|
||||||
|
"taskFile": "README.md",
|
||||||
|
"testFile": "index.html"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"folder": "08_lf8_datenfluesse_und_schnittstellen",
|
||||||
|
"title": "LF 8: Datenflüsse optimieren & Schnittstellen anpassen (REST, JSON, JWT)",
|
||||||
"taskFile": "README.md",
|
"taskFile": "README.md",
|
||||||
"testFile": "index.html"
|
"testFile": "index.html"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "track_3_kaufmaennisches_und_datenmodelle",
|
"id": "track_3_praxis_drills",
|
||||||
"title": "📊 Lehrpfad 3: Kaufmännische Steuerung, Netzplantechnik & Datenmodelle (LF 1, 2, 5)",
|
"title": "⚡ Lehrpfad 3: Interaktive Praxis-Drills & Labore",
|
||||||
"description": "PrintTop GmbH Rechnungsprüfung, Verzugszinsen § 288 BGB, DIN 69900 Netzplantechnik und relationale ER-Modelle (ohne SQL).",
|
"description": "Trace-Tabellen-Trainer mit zeilenweiser Prüfung, Relationales SQL-Labor & Subnetting-Drill Master.",
|
||||||
"certificateKey": "ihk_ap1_prozesse",
|
"certificateKey": "ihk_ap1_prozesse",
|
||||||
"chapters": [
|
"chapters": [
|
||||||
{
|
{
|
||||||
"folder": "08_belegsaetze_und_kaufmaennische_rechnung",
|
"folder": "09_trace_tabellen_trainer",
|
||||||
"title": "LF 1/2: Belegsätze & Kaufmännische Rechnung (PrintTop GmbH, Skonto, Rabatt & Verzug)",
|
"title": "Drill 1: Interaktiver Trace-Tabellen-Trainer (LF 5)",
|
||||||
"taskFile": "README.md",
|
"taskFile": "README.md",
|
||||||
"testFile": "index.html"
|
"testFile": "index.html"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"folder": "09_netzplantechnik_und_projektmanagement",
|
"folder": "10_sql_datenbank_labor",
|
||||||
"title": "LF 1: Netzplantechnik & Projektcontrolling (FAZ/FEZ/SAZ/SEZ, Puffer & Kritischer Pfad)",
|
"title": "Drill 2: Relationale Datenbanken & Live-SQL Labor (LF 5)",
|
||||||
"taskFile": "README.md",
|
"taskFile": "README.md",
|
||||||
"testFile": "index.html"
|
"testFile": "index.html"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"folder": "10_relationales_datenmodell_und_erm",
|
"folder": "11_subnetting_drill_master",
|
||||||
"title": "LF 5: Relationales Datenmodell & ERM (Chen/Krähenfuß, Normalisierung 1NF-3NF)",
|
"title": "Drill 3: Endlos Subnetting-Drill Generator (LF 3)",
|
||||||
"taskFile": "README.md",
|
"taskFile": "README.md",
|
||||||
"testFile": "index.html"
|
"testFile": "index.html"
|
||||||
}
|
}
|
||||||
|
|
@ -783,19 +789,13 @@ window.COURSES_MANIFEST = {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "track_4_probepruefung",
|
"id": "track_4_probepruefung",
|
||||||
"title": "🏆 Lehrpfad 4: Interaktive Praxis-Drills & IHK-Probeprüfung",
|
"title": "🏆 Lehrpfad 4: Offizielle AP1 Probeprüfung & Notenrechner",
|
||||||
"description": "Endlos-Subnetting-Drill Generator & authentische 90-Minuten-Prüfungssimulation mit 100-Punkte-Schlüssel.",
|
"description": "90-Minuten Realtime Prüfungsmodus mit Countdown, 100 Punkten, Zufallsgenerator aus 150 Fragen & Erwartungshorizont.",
|
||||||
"certificateKey": "ihk_ap1_master",
|
"certificateKey": "ihk_ap1_master",
|
||||||
"chapters": [
|
"chapters": [
|
||||||
{
|
|
||||||
"folder": "11_subnetting_drill_master",
|
|
||||||
"title": "Drill: Subnetting- & Präfix-Trainer mit Sofort-Validierung (LF 3)",
|
|
||||||
"taskFile": "README.md",
|
|
||||||
"testFile": "index.html"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"folder": "12_ap1_probepruefung_simulation",
|
"folder": "12_ap1_probepruefung_simulation",
|
||||||
"title": "Simulation: IHK AP1 Probeprüfung (90 Min. / 100 Punkte nach ZPA 2. Auflage)",
|
"title": "Probeprüfung AP1: 90 Min. Realtime Simulation (100 Pkt.)",
|
||||||
"taskFile": "README.md",
|
"taskFile": "README.md",
|
||||||
"testFile": "index.html"
|
"testFile": "index.html"
|
||||||
}
|
}
|
||||||
|
|
@ -809,107 +809,89 @@ window.COURSES_MANIFEST = {
|
||||||
"language": "ihk_ap2_fisi",
|
"language": "ihk_ap2_fisi",
|
||||||
"icon": "📕",
|
"icon": "📕",
|
||||||
"runner": "none",
|
"runner": "none",
|
||||||
"description": "Vollständige Prüfungsvorbereitung für FISI (LF 9–12, WiSo, 40h-Projekt). 12 Module nach ZPA Nord-West 2. Auflage: VLANs, VRT-Symbole, Active Directory, RAID 0-10, Backup 3-2-1-1-0, Enterprise SQL & Vollsimulation.",
|
"description": "Vollständige Prüfungsvorbereitung für FISI (LF 9–12, WiSo, 40h-Projekt). VLANs, OSPF, Active Directory, RAID-Simulator, Cloud & 3 vollständige Probeprüfungen.",
|
||||||
"tracks": [
|
"tracks": [
|
||||||
{
|
{
|
||||||
"id": "track_1_netzwerke_und_server",
|
"id": "track_1_netzwerke_und_server",
|
||||||
"title": "🌱 Lehrpfad 1: Enterprise Netzwerke & Serverdienste (LF 9 & 10)",
|
"title": "🌱 Lehrpfad 1: Enterprise Netzwerke & Serverdienste (LF 9 & 10)",
|
||||||
"description": "VLANs (802.1Q), LACP, Spanning Tree, OSPF, BGP, VRT-Symbole, Strukturierte Verkabelung, Active Directory & Kerberos.",
|
"description": "VLANs (802.1Q), LACP, Spanning Tree, OSPF, BGP, Active Directory, Gruppenrichtlinien, PKI & Ansible.",
|
||||||
"certificateKey": "ihk_ap2_netzwerke",
|
"certificateKey": "ihk_ap2_netzwerke",
|
||||||
"chapters": [
|
"chapters": [
|
||||||
{
|
{
|
||||||
"folder": "01_lf9_netzwerkinfrastruktur_und_routing",
|
"folder": "01_lf9_netzwerke_und_dienste",
|
||||||
"title": "LF 9: Enterprise Routing & Switching (VLAN, STP, LACP, OSPF)",
|
"title": "LF 9: Netzwerke & Dienste bereitstellen (VLAN, Routing, Firewalls)",
|
||||||
"taskFile": "README.md",
|
"taskFile": "README.md",
|
||||||
"testFile": "index.html"
|
"testFile": "index.html"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"folder": "02_vrt_netzwerkplaene_und_infrastruktur",
|
"folder": "02_lf10_serverdienste_und_automation",
|
||||||
"title": "LF 9: Normierte Netzwerkpläne (VRT-Symbole) & Verkabelung",
|
"title": "LF 10: Serverdienste & Automation bereitstellen (AD DS, DNS, Ansible)",
|
||||||
"taskFile": "README.md",
|
|
||||||
"testFile": "index.html"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"folder": "03_lf10_serverdienste_und_identitaeten",
|
|
||||||
"title": "LF 10: Serverdienste, Active Directory & Identitäten (AD DS, GPO, Kerberos)",
|
|
||||||
"taskFile": "README.md",
|
"taskFile": "README.md",
|
||||||
"testFile": "index.html"
|
"testFile": "index.html"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "track_2_storage_und_automation",
|
"id": "track_2_storage_und_cloud",
|
||||||
"title": "⚙️ Lehrpfad 2: Skripting, Storage & Hochverfügbarkeit (LF 10 & 11)",
|
"title": "🛡️ Lehrpfad 2: Storage, Hochverfügbarkeit & Cloud (LF 11 & 12)",
|
||||||
"description": "PowerShell, Bash, Ansible, RAID 0/1/5/6/10 (exklusiv AP2!), SAN/NAS, iSCSI, 3-2-1-1-0 Backup & HA-Cluster.",
|
"description": "RAID 0/1/5/6/10, SAN/NAS, HA-Cluster, Quorum, STONITH, ZFS, Hypervisoren, Docker, Kubernetes & Prometheus.",
|
||||||
"certificateKey": "ihk_ap2_storage_automation",
|
"certificateKey": "ihk_ap2_sicherheit",
|
||||||
"chapters": [
|
"chapters": [
|
||||||
{
|
{
|
||||||
"folder": "04_systemautomation_powershell_bash_python",
|
"folder": "03_lf11_speicher_und_hochverfuegbarkeit",
|
||||||
"title": "LF 10: Skriptprogrammierung & Systemautomation (PowerShell, Bash, Ansible)",
|
"title": "LF 11: Speicher- & Hochverfügbarkeitssysteme (RAID, SAN, Cluster)",
|
||||||
"taskFile": "README.md",
|
"taskFile": "README.md",
|
||||||
"testFile": "index.html"
|
"testFile": "index.html"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"folder": "05_lf11_speicher_und_hochverfuegbarkeit",
|
"folder": "04_lf12_virtualisierung_und_cloud",
|
||||||
"title": "LF 11: Enterprise Storage, RAID & SAN-Architekturen (RAID 0-10, iSCSI, ZFS)",
|
"title": "LF 12: Virtualisierung, Cloud & Automation (Docker, K8s, IaC)",
|
||||||
"taskFile": "README.md",
|
|
||||||
"testFile": "index.html"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"folder": "06_backup_dr_und_hochverfuegbarkeit",
|
|
||||||
"title": "LF 11: Backup, Disaster Recovery & Hochverfügbarkeit (3-2-1-1-0, RTO/RPO)",
|
|
||||||
"taskFile": "README.md",
|
"taskFile": "README.md",
|
||||||
"testFile": "index.html"
|
"testFile": "index.html"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "track_3_datenbanken_und_cloud",
|
"id": "track_3_wiso_und_projekt",
|
||||||
"title": "☁️ Lehrpfad 3: Enterprise SQL, Cloud & WiSo Arbeitsrecht (LF 10, 11 & 12)",
|
"title": "☁️ Lehrpfad 3: WiSo & 40h-Betriebliche Projektarbeit",
|
||||||
"description": "ZPA-SQL (DDL, DCL, Multi-Table JOINs, DATEDIFF), Hypervisoren, Docker, Kubernetes, Zero Trust & WiSo Arbeitsrecht.",
|
"description": "Wirtschafts- & Sozialkunde (Arbeitsrecht, KSchG, Sozialversicherung) & Leitfaden zur 40h-Projektarbeit samt Fachgespräch.",
|
||||||
"certificateKey": "ihk_ap2_cloud_sql",
|
"certificateKey": "ihk_ap2_wiso_projekt",
|
||||||
"chapters": [
|
"chapters": [
|
||||||
{
|
{
|
||||||
"folder": "07_enterprise_sql_und_datenbank_admin",
|
"folder": "05_wiso_arbeitsrecht_und_wirtschaft",
|
||||||
"title": "LF 10/11: Enterprise SQL & Datenbank-Administration (DDL, DCL, Joins, DATEDIFF)",
|
"title": "WiSo: Wirtschafts- & Sozialkunde Prüfungs-Fit (BGB, KSchG, BetrVG)",
|
||||||
"taskFile": "README.md",
|
"taskFile": "README.md",
|
||||||
"testFile": "index.html"
|
"testFile": "index.html"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"folder": "08_lf12_virtualisierung_und_cloud",
|
"folder": "06_betriebliche_projektarbeit_guide",
|
||||||
"title": "LF 12: Cloud-Architekturen, Virtualisierung & Security (Docker, K8s, Zero Trust)",
|
"title": "Projekt: Betriebliche Projektarbeit (40h) & Fachgespräch-Guide",
|
||||||
"taskFile": "README.md",
|
|
||||||
"testFile": "index.html"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"folder": "09_wiso_arbeitsrecht_und_wirtschaft",
|
|
||||||
"title": "WiSo 1: Arbeitsrecht, Tarifrecht & Gehaltsabrechnung (BBiG, KSchG, ArbZG)",
|
|
||||||
"taskFile": "README.md",
|
"taskFile": "README.md",
|
||||||
"testFile": "index.html"
|
"testFile": "index.html"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "track_4_betrieb_projekt_und_pruefung",
|
"id": "track_4_probepruefungen",
|
||||||
"title": "🏆 Lehrpfad 4: Organisation, 40h-Projektarbeit & Prüfungssimulation",
|
"title": "🏆 Lehrpfad 4: Offizielle AP2 Probeprüfungen & Simulationen",
|
||||||
"description": "Unternehmensformen, BetrVG, PUE-Wert, Green IT, Leitfaden zur 40h-Projektarbeit samt Fachgespräch & Vollsimulation AP2.",
|
"description": "Die 3 schriftlichen IHK-Prüfungsteile unter Realbedingungen (Teil 1: 90 Min., Teil 2: 90 Min., WiSo: 60 Min.) mit 150-Fragen-Zufallsgenerator.",
|
||||||
"certificateKey": "ihk_ap2_fisi_master",
|
"certificateKey": "ihk_ap2_fisi_master",
|
||||||
"chapters": [
|
"chapters": [
|
||||||
{
|
{
|
||||||
"folder": "10_wiso_betriebsorganisation_und_umwelt",
|
"folder": "07_ap2_teil_1_probepruefung",
|
||||||
"title": "WiSo 2: Unternehmensformen, Umweltschutz (PUE) & Organisation (BetrVG)",
|
"title": "Probeprüfung AP2 Teil 1: Planen & Umsetzen (90 Min. / 100 Pkt.)",
|
||||||
"taskFile": "README.md",
|
"taskFile": "README.md",
|
||||||
"testFile": "index.html"
|
"testFile": "index.html"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"folder": "11_betriebliche_projektarbeit_guide",
|
"folder": "08_ap2_teil_2_probepruefung",
|
||||||
"title": "Projekt: Betriebliche Projektarbeit (40h) & Fachgespräch (Guide)",
|
"title": "Probeprüfung AP2 Teil 2: Administration & Netzanalyse (90 Min. / 100 Pkt.)",
|
||||||
"taskFile": "README.md",
|
"taskFile": "README.md",
|
||||||
"testFile": "index.html"
|
"testFile": "index.html"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"folder": "12_ap2_gesamtpruefung_simulation",
|
"folder": "09_ap2_wiso_probepruefung",
|
||||||
"title": "Abschluss: AP2 Gesamtprüfungssimulation (PB 1, PB 2 & WiSo)",
|
"title": "Probeprüfung AP2 WiSo: Wirtschafts- & Sozialkunde (60 Min. / 100 Pkt.)",
|
||||||
"taskFile": "README.md",
|
"taskFile": "README.md",
|
||||||
"testFile": "index.html"
|
"testFile": "index.html"
|
||||||
}
|
}
|
||||||
|
|
@ -2272,689 +2254,5 @@ window.COURSES_MANIFEST = {
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
|
||||||
"ihk_ap2_fiae": {
|
|
||||||
"id": "ihk_ap2_fiae",
|
|
||||||
"title": "IHK AP2 Crashkurs: Fachinformatiker Anwendungsentwicklung",
|
|
||||||
"language": "ihk_ap2_fiae",
|
|
||||||
"icon": "💻",
|
|
||||||
"runner": "none",
|
|
||||||
"description": "Vollständige Prüfungsvorbereitung für FIAE (LF 9–12, WiSo, 80h-Projekt). 12 Module nach ZPA Nord-West 2. Auflage: Software-Architektur (MVC, Hexagonal, Clean), GoF-Entwurfsmuster, UML 2.5, Algorithmen (O-Notation), OOP & SOLID, Enterprise SQL & ORM, REST/GraphQL, Testing & Vollsimulation.",
|
|
||||||
"tracks": [
|
|
||||||
{
|
|
||||||
"id": "track_1_architektur_und_design",
|
|
||||||
"title": "🌱 Lehrpfad 1: Software-Architektur, Modellierung & Anforderungsanalyse (LF 9 & 10)",
|
|
||||||
"description": "Clean & Hexagonal Architecture, GoF Patterns (Singleton, Factory, Strategy, Observer), UML 2.5 (Klassendiagramm, Sequenzdiagramm, Aktivitätsdiagramm, State Machine), User Stories, Lasten-/Pflichtenheft & Function-Point-Methode.",
|
|
||||||
"certificateKey": "ihk_ap2_fiae_architektur",
|
|
||||||
"chapters": [
|
|
||||||
{
|
|
||||||
"folder": "01_software_architektur_und_design_patterns",
|
|
||||||
"title": "LF 9: Software-Architektur & Design Patterns (MVC, Clean, GoF-Muster)",
|
|
||||||
"taskFile": "README.md",
|
|
||||||
"testFile": "index.html"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"folder": "02_uml_modellierung_und_softwareentwurf",
|
|
||||||
"title": "LF 9: Normierte UML 2.5 Modellierung (Klassen-, Sequenz- & Aktivitätsdiagramme)",
|
|
||||||
"taskFile": "README.md",
|
|
||||||
"testFile": "index.html"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"folder": "03_anforderungsanalyse_und_aufwandsschaetzung",
|
|
||||||
"title": "LF 10: Anforderungsanalyse, Agilität & Aufwandsschätzung (Function Points, COCOMO)",
|
|
||||||
"taskFile": "README.md",
|
|
||||||
"testFile": "index.html"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "track_2_algorithmen_und_oop",
|
|
||||||
"title": "⚙️ Lehrpfad 2: Algorithmen, Datenstrukturen & Objektorientierung (LF 10 & 11)",
|
|
||||||
"description": "O-Notation (Zeit- & Speicherkomplexität), Sortier- & Suchalgorithmen, Bäume, Graphen, Hashmaps, OOP-Konzepte & SOLID-Prinzipien.",
|
|
||||||
"certificateKey": "ihk_ap2_fiae_algorithmen",
|
|
||||||
"chapters": [
|
|
||||||
{
|
|
||||||
"folder": "04_algorithmen_und_komplexitaet_o_notation",
|
|
||||||
"title": "LF 10: Algorithmen, Komplexität & O-Notation (Sortieren, Suchen, Rekursion)",
|
|
||||||
"taskFile": "README.md",
|
|
||||||
"testFile": "index.html"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"folder": "05_datenstrukturen_und_speichermanagement",
|
|
||||||
"title": "LF 10: Fortgeschrittene Datenstrukturen & Memory Management (Trees, HashTables, Caching)",
|
|
||||||
"taskFile": "README.md",
|
|
||||||
"testFile": "index.html"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"folder": "06_objektorientierte_programmierung_solid",
|
|
||||||
"title": "LF 11: Objektorientierte Programmierung & SOLID-Prinzipien (Clean Code)",
|
|
||||||
"taskFile": "README.md",
|
|
||||||
"testFile": "index.html"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "track_3_daten_apis_und_testing",
|
|
||||||
"title": "☁️ Lehrpfad 3: Enterprise SQL, API-Design & Quality Assurance (LF 11 & 12)",
|
|
||||||
"description": "ZPA-SQL (DDL, DML, DCL, Normalisierung, Transaktionen/ACID), RESTful APIs (HTTP Verbs, Statuscodes, OpenAPI), GraphQL, TDD, Unit-/Integrations-Tests & CI/CD.",
|
|
||||||
"certificateKey": "ihk_ap2_fiae_daten_testing",
|
|
||||||
"chapters": [
|
|
||||||
{
|
|
||||||
"folder": "07_enterprise_sql_und_datenbank_modellierung",
|
|
||||||
"title": "LF 11: Enterprise SQL & Datenbank-Design (ER-Modell, Normalisierung, ACID, Transaktionen)",
|
|
||||||
"taskFile": "README.md",
|
|
||||||
"testFile": "index.html"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"folder": "08_api_design_rest_json_und_graphql",
|
|
||||||
"title": "LF 11: Modernes API-Design, REST, JSON & Web-Security (OAuth2, JWT, CORS)",
|
|
||||||
"taskFile": "README.md",
|
|
||||||
"testFile": "index.html"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"folder": "09_software_testing_und_ci_cd_pipelines",
|
|
||||||
"title": "LF 12: Software-Testing, Teststufen & CI/CD (TDD, Mocking, Testüberdeckung)",
|
|
||||||
"taskFile": "README.md",
|
|
||||||
"testFile": "index.html"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "track_4_wiso_projekt_und_pruefung",
|
|
||||||
"title": "🏆 Lehrpfad 4: WiSo, 80h-Projektarbeit & AP2-Prüfungssimulation",
|
|
||||||
"description": "Software-Lizenzmodelle (GPL, MIT, Apache, SaaS), Arbeits- & Urheberrecht, Leitfaden für die 80-stündige Projektarbeit samt Dokumentation & Fachgespräch sowie Vollprüfung.",
|
|
||||||
"certificateKey": "ihk_ap2_fiae_master",
|
|
||||||
"chapters": [
|
|
||||||
{
|
|
||||||
"folder": "10_wiso_arbeitsrecht_und_lizenzmodelle",
|
|
||||||
"title": "WiSo: IT-Arbeitsrecht, Urheberrecht, Lizenzmodelle (Open Source vs. Commercial) & DSGVO",
|
|
||||||
"taskFile": "README.md",
|
|
||||||
"testFile": "index.html"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"folder": "11_betriebliche_projektarbeit_guide_80h",
|
|
||||||
"title": "Projekt: Betriebliche Projektarbeit (80h) & Fachgespräch (Guide nach § 15 BBiG)",
|
|
||||||
"taskFile": "README.md",
|
|
||||||
"testFile": "index.html"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"folder": "12_ap2_fiae_gesamtpruefung_simulation",
|
|
||||||
"title": "Abschluss: AP2 FIAE Gesamtprüfungssimulation (PB 1, PB 2 & WiSo)",
|
|
||||||
"taskFile": "README.md",
|
|
||||||
"testFile": "index.html"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"ihk_ap2_fidp": {
|
|
||||||
"id": "ihk_ap2_fidp",
|
|
||||||
"title": "IHK AP2 Crashkurs: Fachinformatiker Daten- und Prozessanalyse",
|
|
||||||
"language": "ihk_ap2_fidp",
|
|
||||||
"icon": "📊",
|
|
||||||
"runner": "none",
|
|
||||||
"description": "Vollständige Prüfungsvorbereitung für FIDP (LF 9–12, WiSo, 40h-Projekt). 12 Module nach ZPA Nord-West 2. Auflage: Geschäftsprozessanalyse, BPMN 2.0 (ISO 19510), ISO 25012 Datenqualität, ETL/CDC, Star-/Snowflake-Schema, Big Data & Kafka, Deskriptive/Induktive Statistik, Machine Learning Evaluation (Confusion Matrix, ROC), BI-Dashboard-Design, Data Governance & Vollsimulation.",
|
|
||||||
"tracks": [
|
|
||||||
{
|
|
||||||
"id": "track_1_prozessanalyse_und_qualitaet",
|
|
||||||
"title": "🌱 Lehrpfad 1: Prozessanalyse, BPMN 2.0 & Datenqualität (LF 9 & 10)",
|
|
||||||
"description": "Ist-Analyse, Durchlaufzeiten (DLZ), Prozess-Effizienz, BPMN 2.0 (Pools, Lanes, Gateways, Boundary Events), Datenformate (JSON, Parquet) & ISO/IEC 25012 Datenqualitätsmerkmale.",
|
|
||||||
"certificateKey": "ihk_ap2_fidp_prozesse",
|
|
||||||
"chapters": [
|
|
||||||
{
|
|
||||||
"folder": "01_prozessanalyse_und_prozessoptimierung",
|
|
||||||
"title": "LF 9: Geschäftsprozessanalyse & Optimierungsmethoden (KPIs, Kaizen, Lean)",
|
|
||||||
"taskFile": "README.md",
|
|
||||||
"testFile": "index.html"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"folder": "02_bpmn20_und_prozessmodellierung_vertiefung",
|
|
||||||
"title": "LF 9: Normierte BPMN 2.0 Prozessmodellierung (Gateways, Intermediate Events)",
|
|
||||||
"taskFile": "README.md",
|
|
||||||
"testFile": "index.html"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"folder": "03_datenquellen_und_datenqualitaet",
|
|
||||||
"title": "LF 10: Datenquellen, Formate & Datenqualität (ISO 25012, Parquet)",
|
|
||||||
"taskFile": "README.md",
|
|
||||||
"testFile": "index.html"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "track_2_etl_und_warehousing",
|
|
||||||
"title": "⚙️ Lehrpfad 2: Data Engineering, ETL & Data Warehousing (LF 10 & 11)",
|
|
||||||
"description": "ETL vs. ELT, Change Data Capture (CDC), Idempotenz, DWH-3-Schichten-Architektur, Star- vs. Snowflake-Schema, Slowly Changing Dimensions (SCD 1-3) & Big Data Streaming (Kafka, Lakehouse).",
|
|
||||||
"certificateKey": "ihk_ap2_fidp_dwh",
|
|
||||||
"chapters": [
|
|
||||||
{
|
|
||||||
"folder": "04_etl_prozesse_und_datenpipelines",
|
|
||||||
"title": "LF 10: ETL/ELT-Prozesse & Moderne Datenpipelines (CDC, Airflow, Idempotenz)",
|
|
||||||
"taskFile": "README.md",
|
|
||||||
"testFile": "index.html"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"folder": "05_data_warehouse_und_datenmodellierung",
|
|
||||||
"title": "LF 10: Data Warehouse Architekturen & Star-/Snowflake-Schema (SCD 1-3)",
|
|
||||||
"taskFile": "README.md",
|
|
||||||
"testFile": "index.html"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"folder": "06_big_data_architekturen_und_streaming",
|
|
||||||
"title": "LF 11: Big Data, Streaming & Data Lakehouse (5 V's, Kafka, Lambda/Kappa)",
|
|
||||||
"taskFile": "README.md",
|
|
||||||
"testFile": "index.html"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "track_3_statistik_ml_und_bi",
|
|
||||||
"title": "☁️ Lehrpfad 3: Statistik, Machine Learning & BI-Reporting (LF 11 & 12)",
|
|
||||||
"description": "Deskriptive & induktive Statistik (Mittelwert, Median, Hypothesentest, p-Wert), Machine Learning Evaluation (Confusion Matrix, Precision, Recall, F1-Score) & Dashboard-Design nach Tufte & IBCS.",
|
|
||||||
"certificateKey": "ihk_ap2_fidp_analytics",
|
|
||||||
"chapters": [
|
|
||||||
{
|
|
||||||
"folder": "07_deskriptive_und_induktive_statistik",
|
|
||||||
"title": "LF 11: Deskriptive & Induktive Statistik (Lage-/Streumaße, Hypothesentests)",
|
|
||||||
"taskFile": "README.md",
|
|
||||||
"testFile": "index.html"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"folder": "08_machine_learning_und_kuenstliche_intelligenz",
|
|
||||||
"title": "LF 11: Machine Learning & Modell-Evaluation (Confusion Matrix, F1-Score)",
|
|
||||||
"taskFile": "README.md",
|
|
||||||
"testFile": "index.html"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"folder": "09_datenvisualisierung_und_bi_reporting",
|
|
||||||
"title": "LF 12: Datenvisualisierung & BI-Dashboard-Design (IBCS, Tufte, Power BI)",
|
|
||||||
"taskFile": "README.md",
|
|
||||||
"testFile": "index.html"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "track_4_governance_projekt_und_pruefung",
|
|
||||||
"title": "🏆 Lehrpfad 4: Governance, 40h-Projektarbeit & AP2-Prüfungssimulation",
|
|
||||||
"description": "DSGVO (Art. 5/6/9), Anonymisierung vs. Pseudonymisierung, Data Lineage, Leitfaden für die 40h-Projektarbeit samt Nutzwertanalyse & Vollprüfungssimulation.",
|
|
||||||
"certificateKey": "ihk_ap2_fidp_master",
|
|
||||||
"chapters": [
|
|
||||||
{
|
|
||||||
"folder": "10_datenschutz_compliance_und_wiso",
|
|
||||||
"title": "WiSo & LF 12: Data Governance, DSGVO & IT-Recht (Anonymisierung, Lineage)",
|
|
||||||
"taskFile": "README.md",
|
|
||||||
"testFile": "index.html"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"folder": "11_betriebliche_projektarbeit_guide_40h",
|
|
||||||
"title": "Projekt: Betriebliche Projektarbeit (40h) & Fachgespräch (Guide)",
|
|
||||||
"taskFile": "README.md",
|
|
||||||
"testFile": "index.html"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"folder": "12_ap2_fidp_gesamtpruefung_simulation",
|
|
||||||
"title": "Abschluss: AP2 FIDP Gesamtprüfungssimulation (PB 1, PB 2 & WiSo)",
|
|
||||||
"taskFile": "README.md",
|
|
||||||
"testFile": "index.html"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"ihk_ap2_fidv": {
|
|
||||||
"id": "ihk_ap2_fidv",
|
|
||||||
"title": "IHK AP2 Crashkurs: Fachinformatiker Digitale Vernetzung",
|
|
||||||
"language": "ihk_ap2_fidv",
|
|
||||||
"icon": "🏭",
|
|
||||||
"runner": "none",
|
|
||||||
"description": "Vollständige Prüfungsvorbereitung für FIDV (LF 9–12, WiSo, 40h-Projekt). 12 Module nach ZPA Nord-West 2. Auflage: Cyber-Physische Systeme (CPS), RAMI 4.0, IO-Link & Sensorik, Edge Computing (IPCs, K3s), Profinet IO (RT/IRT), TSN (802.1Qbv), OPC UA & MQTT, Private 5G-Campusnetze (Band n78), Industrial Security IEC 62443, Purdue-Modell & IDMZ, PRP/HSR Zero-Failover & Vollsimulation.",
|
|
||||||
"tracks": [
|
|
||||||
{
|
|
||||||
"id": "track_1_cps_und_edge",
|
|
||||||
"title": "🌱 Lehrpfad 1: CPS, Sensorik & Edge Computing (LF 9 & 10)",
|
|
||||||
"description": "Cyber-Physische Systeme (CPS), RAMI 4.0, Verwaltungsschale (AAS), 4-20 mA Stromschleifen, IO-Link (IEC 61131-9), Nyquist-Shannon & Industrie-PCs / Edge-Gateways.",
|
|
||||||
"certificateKey": "ihk_ap2_fidv_cps",
|
|
||||||
"chapters": [
|
|
||||||
{
|
|
||||||
"folder": "01_industrie40_und_cyber_physische_systeme",
|
|
||||||
"title": "LF 9: Industrie 4.0 & Cyber-Physische Systeme (RAMI 4.0, AAS, Digital Twin)",
|
|
||||||
"taskFile": "README.md",
|
|
||||||
"testFile": "index.html"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"folder": "02_sensorik_aktorik_und_signalverarbeitung",
|
|
||||||
"title": "LF 9: Industrielle Sensorik, Aktorik & Signalwandlung (IO-Link, 4-20 mA)",
|
|
||||||
"taskFile": "README.md",
|
|
||||||
"testFile": "index.html"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"folder": "03_edge_computing_und_iot_gateways",
|
|
||||||
"title": "LF 10: Edge Computing, Industrie-PCs & IoT-Gateways (Docker, K3s, Filterung)",
|
|
||||||
"taskFile": "README.md",
|
|
||||||
"testFile": "index.html"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "track_2_busse_und_protokolle",
|
|
||||||
"title": "⚙️ Lehrpfad 2: Industrielle Kommunikation & IoT-Protokolle (LF 10 & 11)",
|
|
||||||
"description": "Modbus RTU/TCP, Profinet IO (RT/IRT), TSN (IEEE 802.1Qbv), OPC UA (Client/Server, Nodes), MQTT (QoS 0/1/2, LWT, Retain) & Private 5G-Campusnetze (Band n78 3,7-3,8 GHz).",
|
|
||||||
"certificateKey": "ihk_ap2_fidv_busse",
|
|
||||||
"chapters": [
|
|
||||||
{
|
|
||||||
"folder": "04_industrielle_kommunikation_und_feldbusse",
|
|
||||||
"title": "LF 10: Feldbusse & Industrial Ethernet (Profinet IO, Modbus, TSN)",
|
|
||||||
"taskFile": "README.md",
|
|
||||||
"testFile": "index.html"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"folder": "05_iot_protokolle_mqtt_coap_und_opc_ua",
|
|
||||||
"title": "LF 11: Industrielle IoT-Protokolle (OPC UA, MQTT, CoAP)",
|
|
||||||
"taskFile": "README.md",
|
|
||||||
"testFile": "index.html"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"folder": "06_drahtlose_vernetzung_und_5g_campusnetze",
|
|
||||||
"title": "LF 11: Drahtlose Vernetzung & Private 5G-Campusnetze (URLLC, n78, LoRaWAN)",
|
|
||||||
"taskFile": "README.md",
|
|
||||||
"testFile": "index.html"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "track_3_ot_security_und_resilienz",
|
|
||||||
"title": "☁️ Lehrpfad 3: Industrial Security & OT-Netzwerkresilienz (LF 11 & 12)",
|
|
||||||
"description": "IEC 62443 (Zonen & Conduits, Security Levels 1-4), Purdue-Modell (Level 0-5), Industrial DMZ (IDMZ 3.5), Deep Packet Inspection (DPI), PRP/HSR Zero-Failover & passives OT-Monitoring.",
|
|
||||||
"certificateKey": "ihk_ap2_fidv_security",
|
|
||||||
"chapters": [
|
|
||||||
{
|
|
||||||
"folder": "07_industrial_security_iec_62443",
|
|
||||||
"title": "LF 11: Industrial Security nach IEC 62443 (Zonen & Conduits, SL 1-4)",
|
|
||||||
"taskFile": "README.md",
|
|
||||||
"testFile": "index.html"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"folder": "08_netzwerksegmentierung_und_ot_firewalls",
|
|
||||||
"title": "LF 11: Purdue-Referenzmodell & OT-Netzwerksegmentierung (IDMZ, DPI)",
|
|
||||||
"taskFile": "README.md",
|
|
||||||
"testFile": "index.html"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"folder": "09_ot_monitoring_anomalieerkennung_und_resilienz",
|
|
||||||
"title": "LF 12: OT-Netzwerkresilienz, PRP/HSR & Passives Monitoring (TAP vs. SPAN)",
|
|
||||||
"taskFile": "README.md",
|
|
||||||
"testFile": "index.html"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "track_4_safety_projekt_und_pruefung",
|
|
||||||
"title": "🏆 Lehrpfad 4: Maschinensicherheit, 40h-Projektarbeit & AP2-Prüfungssimulation",
|
|
||||||
"description": "Safety vs. Security, Performance Level (PL) & SIL, CE-Kennzeichnung (wesentliche Veränderung), Leitfaden für die 40h-Projektarbeit samt Retrofit & Vollprüfungssimulation.",
|
|
||||||
"certificateKey": "ihk_ap2_fidv_master",
|
|
||||||
"chapters": [
|
|
||||||
{
|
|
||||||
"folder": "10_betriebsorganisation_sicherheit_und_wiso",
|
|
||||||
"title": "WiSo & LF 12: Maschinensicherheit (Safety vs. Security, SIL/PL) & Recht",
|
|
||||||
"taskFile": "README.md",
|
|
||||||
"testFile": "index.html"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"folder": "11_betriebliche_projektarbeit_guide_40h",
|
|
||||||
"title": "Projekt: Betriebliche Projektarbeit (40h) & Fachgespräch (Guide)",
|
|
||||||
"taskFile": "README.md",
|
|
||||||
"testFile": "index.html"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"folder": "12_ap2_fidv_gesamtpruefung_simulation",
|
|
||||||
"title": "Abschluss: AP2 FIDV Gesamtprüfungssimulation (PB 1, PB 2 & WiSo)",
|
|
||||||
"taskFile": "README.md",
|
|
||||||
"testFile": "index.html"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"ihk_ap2_itse": {
|
|
||||||
"id": "ihk_ap2_itse",
|
|
||||||
"title": "IHK AP2 Crashkurs: IT-System-Elektroniker/in",
|
|
||||||
"language": "ihk_ap2_itse",
|
|
||||||
"icon": "⚡",
|
|
||||||
"runner": "none",
|
|
||||||
"description": "Vollständige Prüfungsvorbereitung für ITSE (LF 9–12, WiSo, 40h-Projekt). 12 Module nach ZPA Nord-West 2. Auflage: DIN VDE 0100 Schutzmaßnahmen, Netzformen (TN/TT/IT), USV-Dimensionierung & VFI, Überspannungsschutz Typ 1-3, Strukturierte Verkabelung (EN 50173), LWL Singlemode/Multimode, Fusionsspleißen & OTDR-Messtechnik, DGUV V3 Prüfungen (VDE 0701/0702), Fehlersuche, EMV & Vollsimulation.",
|
|
||||||
"tracks": [
|
|
||||||
{
|
|
||||||
"id": "track_1_elektrotechnik_und_usv",
|
|
||||||
"title": "🌱 Lehrpfad 1: Elektrotechnik, Netzformen & USV-Anlagen (LF 9 & 10)",
|
|
||||||
"description": "DIN VDE 0100 Schutzmaßnahmen, Netzformen (TN-C, TN-S, TT, IT), RCD-Dimensionierung, Leistungsberechnung (P, S, cos phi), USV-Klassen (VFD, VI, VFI) & Blitzschutzzonen (LPZ 0-3).",
|
|
||||||
"certificateKey": "ihk_ap2_itse_strom",
|
|
||||||
"chapters": [
|
|
||||||
{
|
|
||||||
"folder": "01_elektrotechnik_und_schutzmassnahmen_vde0100",
|
|
||||||
"title": "LF 9: Elektrotechnische Grundlagen & Schutzmaßnahmen (DIN VDE 0100)",
|
|
||||||
"taskFile": "README.md",
|
|
||||||
"testFile": "index.html"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"folder": "02_stromversorgung_und_usv_anlagen",
|
|
||||||
"title": "LF 10: Stromversorgungs- & USV-Anlagen Projektierung (VFI, Scheinleistung)",
|
|
||||||
"taskFile": "README.md",
|
|
||||||
"testFile": "index.html"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"folder": "03_blitzschutz_ueberspannung_und_erdung",
|
|
||||||
"title": "LF 10: Blitzschutz, Überspannungsschutz & Erdungskonzepte (LPZ, Typ 1-3)",
|
|
||||||
"taskFile": "README.md",
|
|
||||||
"testFile": "index.html"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "track_2_leitungsnetze_und_lwl",
|
|
||||||
"title": "⚙️ Lehrpfad 2: Leitungsnetze, Glasfaser & Messtechnik (LF 10 & 11)",
|
|
||||||
"description": "Strukturierte Verkabelung nach DIN EN 50173, Kupfer Cat.6A/7, LWL Singlemode (OS2) vs. Multimode (OM3-5), Fusionsspleißen, OTDR-Kurvenanalyse & optisches Dämpfungsbudget.",
|
|
||||||
"certificateKey": "ihk_ap2_itse_lwl",
|
|
||||||
"chapters": [
|
|
||||||
{
|
|
||||||
"folder": "04_strukturierte_verkabelung_kupfer",
|
|
||||||
"title": "LF 11: Strukturierte Verkabelung & Kupfer-Messtechnik (Cat.6A/7, NEXT)",
|
|
||||||
"taskFile": "README.md",
|
|
||||||
"testFile": "index.html"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"folder": "05_lichtwellenleiter_und_spleisstechnik",
|
|
||||||
"title": "LF 11: Lichtwellenleiter (LWL) & Fusionsspleißen (OS2 vs. OM3-5)",
|
|
||||||
"taskFile": "README.md",
|
|
||||||
"testFile": "index.html"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"folder": "06_optische_messtechnik_und_otdr",
|
|
||||||
"title": "LF 11: Optische Messtechnik & Dämpfungsbudget (OTDR, Fresnel-Reflexion)",
|
|
||||||
"taskFile": "README.md",
|
|
||||||
"testFile": "index.html"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "track_3_dguv_instandhaltung_und_funk",
|
|
||||||
"title": "☁️ Lehrpfad 3: DGUV V3 Prüfungen, Instandhaltung & Funk (LF 9 & 12)",
|
|
||||||
"description": "Wiederkehrende Prüfungen elektrischer Geräte nach DGUV Vorschrift 3 (DIN EN 50678 / VDE 0701), systematische Fehlersuche in Netzen & Funkübertragung / Antennentechnik (EIRP, EMV).",
|
|
||||||
"certificateKey": "ihk_ap2_itse_instandhaltung",
|
|
||||||
"chapters": [
|
|
||||||
{
|
|
||||||
"folder": "07_dguv_v3_pruefungen_vde0701",
|
|
||||||
"title": "LF 9: Geräteprüfung nach DGUV Vorschrift 3 (VDE 0701/0702, R_PE, R_ISO)",
|
|
||||||
"taskFile": "README.md",
|
|
||||||
"testFile": "index.html"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"folder": "08_fehlersuche_in_it_und_tk_systemen",
|
|
||||||
"title": "LF 9: Systematische Fehlersuche in IT- & TK-Infrastrukturen (TDR, OSI)",
|
|
||||||
"taskFile": "README.md",
|
|
||||||
"testFile": "index.html"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"folder": "09_funktechnik_antennen_und_emv",
|
|
||||||
"title": "LF 12: Funkübertragung, Antennentechnik & EMV (EIRP, Richtfunk)",
|
|
||||||
"taskFile": "README.md",
|
|
||||||
"testFile": "index.html"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "track_4_arbeitssicherheit_projekt_und_pruefung",
|
|
||||||
"title": "🏆 Lehrpfad 4: Sicherheit, 40h-Projektarbeit & AP2-Prüfungssimulation",
|
|
||||||
"description": "Die 5 Sicherheitsregeln der Elektrotechnik, ElektroG/BattG, Leitfaden für die 40h-Projektarbeit samt Montage/VDE-Prüfung & Vollprüfungssimulation.",
|
|
||||||
"certificateKey": "ihk_ap2_itse_master",
|
|
||||||
"chapters": [
|
|
||||||
{
|
|
||||||
"folder": "10_arbeitssicherheit_umwelt_und_wiso",
|
|
||||||
"title": "WiSo: Arbeitssicherheit (5 Sicherheitsregeln) & ElektroG (BattG, WEEE)",
|
|
||||||
"taskFile": "README.md",
|
|
||||||
"testFile": "index.html"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"folder": "11_betriebliche_projektarbeit_guide_40h",
|
|
||||||
"title": "Projekt: Betriebliche Projektarbeit (40h) & Fachgespräch (Guide ITSE)",
|
|
||||||
"taskFile": "README.md",
|
|
||||||
"testFile": "index.html"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"folder": "12_ap2_itse_gesamtpruefung_simulation",
|
|
||||||
"title": "Abschluss: AP2 ITSE Gesamtprüfungssimulation (PB 1, PB 2 & WiSo)",
|
|
||||||
"taskFile": "README.md",
|
|
||||||
"testFile": "index.html"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"ihk_ap2_itsm": {
|
|
||||||
"id": "ihk_ap2_itsm",
|
|
||||||
"title": "IHK AP2 Crashkurs: Kaufleute für IT-System-Management",
|
|
||||||
"language": "ihk_ap2_itsm",
|
|
||||||
"icon": "💼",
|
|
||||||
"runner": "none",
|
|
||||||
"description": "Vollständige Prüfungsvorbereitung für ITSM (LF 9–12, WiSo, 40h-Projekt). 12 Module nach ZPA Nord-West 2. Auflage: IT-Marktanalyse, SWOT & 7P Marketing-Mix, TCO/ROI, B2B-Vertrieb & Buying Center, Einwandbehandlung & Harvard-Konzept, CRM, Handelskalkulation (LEP bis BVP), IT-Vertragsrecht (Werk-/Dienstvertrag, EVB-IT), Service Level Agreements (SLAs, 99,9%), ITIL v4 & Vollsimulation.",
|
|
||||||
"tracks": [
|
|
||||||
{
|
|
||||||
"id": "track_1_maerkte_marketing_tco",
|
|
||||||
"title": "🌱 Lehrpfad 1: IT-Märkte, Marketing-Mix & Wirtschaftlichkeit (LF 9 & 10)",
|
|
||||||
"description": "Marktforschung, SWOT-Analyse, BCG-Matrix, 7P-Dienstleistungsmarketing, Total Cost of Ownership (TCO), ROI & statische Amortisationsrechnung.",
|
|
||||||
"certificateKey": "ihk_ap2_itsm_marketing",
|
|
||||||
"chapters": [
|
|
||||||
{
|
|
||||||
"folder": "01_it_marktanalyse_wettbewerb_und_swot",
|
|
||||||
"title": "LF 9: IT-Marktanalyse, Wettbewerb & SWOT-Analyse (BCG-Matrix)",
|
|
||||||
"taskFile": "README.md",
|
|
||||||
"testFile": "index.html"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"folder": "02_marketing_mix_fuer_it_dienstleistungen",
|
|
||||||
"title": "LF 10: Der Marketing-Mix für IT-Dienstleistungen (7P, Lead Nurturing)",
|
|
||||||
"taskFile": "README.md",
|
|
||||||
"testFile": "index.html"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"folder": "03_investitions_und_wirtschaftlichkeitsrechnung",
|
|
||||||
"title": "LF 9: Investitions- & Wirtschaftlichkeitsrechnung (TCO, ROI, Amortisation)",
|
|
||||||
"taskFile": "README.md",
|
|
||||||
"testFile": "index.html"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "track_2_vertrieb_crm_akquise",
|
|
||||||
"title": "⚙️ Lehrpfad 2: B2B-Vertrieb, CRM & Kundenakquise (LF 10 & 11)",
|
|
||||||
"description": "B2B-Vertriebszyklus, Buying Center Rollen, BANT-Qualifizierung, Fragetechniken, Einwandbehandlung (Bumerang), Harvard-Verhandlung & CRM-Kundenbindung.",
|
|
||||||
"certificateKey": "ihk_ap2_itsm_vertrieb",
|
|
||||||
"chapters": [
|
|
||||||
{
|
|
||||||
"folder": "04_b2b_vertriebsprozess_und_lead_management",
|
|
||||||
"title": "LF 10: Der B2B-Vertriebsprozess & Buying Center Analyse (BANT)",
|
|
||||||
"taskFile": "README.md",
|
|
||||||
"testFile": "index.html"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"folder": "05_kundenakquise_einwandbehandlung_und_verhandlung",
|
|
||||||
"title": "LF 11: Kundenakquise, Einwandbehandlung & Verhandlungsführung (Harvard)",
|
|
||||||
"taskFile": "README.md",
|
|
||||||
"testFile": "index.html"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"folder": "06_crm_systeme_und_kundenbindung",
|
|
||||||
"title": "LF 11: CRM-Systeme, Customer Journey & Kundenbindung (CLV, Churn)",
|
|
||||||
"taskFile": "README.md",
|
|
||||||
"testFile": "index.html"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "track_3_kalkulation_recht_slas",
|
|
||||||
"title": "☁️ Lehrpfad 3: Kalkulation, IT-Recht & Service Level Agreements (LF 11 & 12)",
|
|
||||||
"description": "Kaufmännische Handelskalkulation (LEP bis BVP), Deckungsbeitrag, Break-Even-Point, BGB-Vertragstypen (Werk vs. Dienst), EVB-IT, SLAs (99,9%) & ITIL v4.",
|
|
||||||
"certificateKey": "ihk_ap2_itsm_kalkulation",
|
|
||||||
"chapters": [
|
|
||||||
{
|
|
||||||
"folder": "07_handelskalkulation_und_deckungsbeitrag",
|
|
||||||
"title": "LF 11: Handelskalkulation & Deckungsbeitragsrechnung (Vorwärtskalkulation)",
|
|
||||||
"taskFile": "README.md",
|
|
||||||
"testFile": "index.html"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"folder": "08_it_vertragsrecht_und_evb_it",
|
|
||||||
"title": "LF 12: IT-Vertragsrecht, BGB-Vertragstypen & EVB-IT (Werk- vs. Dienstvertrag)",
|
|
||||||
"taskFile": "README.md",
|
|
||||||
"testFile": "index.html"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"folder": "09_service_level_agreements_und_itil",
|
|
||||||
"title": "LF 12: Service Level Agreements (SLAs) & ITIL Service Management",
|
|
||||||
"taskFile": "README.md",
|
|
||||||
"testFile": "index.html"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "track_4_controlling_projekt_und_pruefung",
|
|
||||||
"title": "🏆 Lehrpfad 4: Controlling, 40h-Projektarbeit & AP2-Prüfungssimulation",
|
|
||||||
"description": "Finanzierungsarten (Leasing, Factoring), Liquidität, Leitfaden für die 40h-Projektarbeit samt Vermarktungskonzept & Vollprüfungssimulation.",
|
|
||||||
"certificateKey": "ihk_ap2_itsm_master",
|
|
||||||
"chapters": [
|
|
||||||
{
|
|
||||||
"folder": "10_it_controlling_finanzierung_und_wiso",
|
|
||||||
"title": "WiSo: Finanzierungsformen (Leasing vs. Kauf), Bilanz & WiSo Arbeitsrecht",
|
|
||||||
"taskFile": "README.md",
|
|
||||||
"testFile": "index.html"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"folder": "11_betriebliche_projektarbeit_guide_40h",
|
|
||||||
"title": "Projekt: Betriebliche Projektarbeit (40h) & Fachgespräch (Guide ITSM)",
|
|
||||||
"taskFile": "README.md",
|
|
||||||
"testFile": "index.html"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"folder": "12_ap2_itsm_gesamtpruefung_simulation",
|
|
||||||
"title": "Abschluss: AP2 ITSM Gesamtprüfungssimulation (PB 1, PB 2 & WiSo)",
|
|
||||||
"taskFile": "README.md",
|
|
||||||
"testFile": "index.html"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"ihk_ap2_kdm": {
|
|
||||||
"id": "ihk_ap2_kdm",
|
|
||||||
"title": "IHK AP2 Crashkurs: Kaufleute für Digitalisierungsmanagement",
|
|
||||||
"language": "ihk_ap2_kdm",
|
|
||||||
"icon": "📈",
|
|
||||||
"runner": "none",
|
|
||||||
"description": "Vollständige Prüfungsvorbereitung für KDM (LF 9–12, WiSo, 40h-Projekt). 12 Module nach ZPA Nord-West 2. Auflage: Digitale Geschäftsmodelle, Business Model Canvas (BMC), Plattformökonomie & Netzwerkeffekte, Datenmonetarisierung, BPMN 2.0 Prozessdigitalisierung, ERP-Modulverzahnung, GoBD-DMS & E-Invoicing (ZUGFeRD/XRechnung), Change Management (Kotter/Lewin), Controlling & KPIs, Datenschutz-Folgenabschätzung (DSFA Art. 35 DSGVO), EU AI Act & Vollsimulation.",
|
|
||||||
"tracks": [
|
|
||||||
{
|
|
||||||
"id": "track_1_geschaeftsmodelle_und_plattformen",
|
|
||||||
"title": "🌱 Lehrpfad 1: Digitale Geschäftsmodelle & Plattformökonomie (LF 9 & 11)",
|
|
||||||
"description": "Business Model Canvas (BMC), Subscription Economy, direkte & indirekte Netzwerkeffekte, Metcalfe'sches Gesetz, Hühnchen-Ei-Problem & Datenmonetarisierung.",
|
|
||||||
"certificateKey": "ihk_ap2_kdm_models",
|
|
||||||
"chapters": [
|
|
||||||
{
|
|
||||||
"folder": "01_digitale_geschaeftsmodelle_und_bmc",
|
|
||||||
"title": "LF 9: Analyse & Transformation digitaler Geschäftsmodelle (BMC)",
|
|
||||||
"taskFile": "README.md",
|
|
||||||
"testFile": "index.html"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"folder": "02_plattformoekonomie_und_netzwerkeffekte",
|
|
||||||
"title": "LF 11: Plattformökonomie, Netzwerkeffekte & Disruption (Metcalfe)",
|
|
||||||
"taskFile": "README.md",
|
|
||||||
"testFile": "index.html"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"folder": "03_datenwerterschoepfung_und_datenmonetarisierung",
|
|
||||||
"title": "LF 9: Datenökonomie & Monetarisierung betrieblicher Daten (DIKW)",
|
|
||||||
"taskFile": "README.md",
|
|
||||||
"testFile": "index.html"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "track_2_prozesse_erp_change",
|
|
||||||
"title": "⚙️ Lehrpfad 2: Prozessdigitalisierung, ERP & Workflows (LF 9 & 10)",
|
|
||||||
"description": "Geschäftsprozess-Digitalisierung in BPMN 2.0, Medienbrüche, RPA vs. API, ERP-Architekturen, GoBD-konformes DMS/ECM & E-Invoicing (ZUGFeRD/XRechnung nach EN 16931).",
|
|
||||||
"certificateKey": "ihk_ap2_kdm_processes",
|
|
||||||
"chapters": [
|
|
||||||
{
|
|
||||||
"folder": "04_geschaeftsprozess_digitalisierung_bpmn",
|
|
||||||
"title": "LF 10: Geschäftsprozess-Digitalisierung in BPMN 2.0 (RPA, Medienbrüche)",
|
|
||||||
"taskFile": "README.md",
|
|
||||||
"testFile": "index.html"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"folder": "05_erp_und_dokumentenmanagement_dms",
|
|
||||||
"title": "LF 10: ERP-Systeme, GoBD-DMS & E-Invoicing (ZUGFeRD/XRechnung)",
|
|
||||||
"taskFile": "README.md",
|
|
||||||
"testFile": "index.html"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"folder": "06_change_management_und_agile_methoden",
|
|
||||||
"title": "LF 10: Change Management & Agiles Transformationsmanagement (Kotter/Lewin)",
|
|
||||||
"taskFile": "README.md",
|
|
||||||
"testFile": "index.html"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "track_3_controlling_dsfa_compliance",
|
|
||||||
"title": "☁️ Lehrpfad 3: Business Analytics, Controlling & Compliance (LF 11 & 12)",
|
|
||||||
"description": "Datengetriebenes Controlling, Straight-Through-Processing (STP), Cost per Transaction, Datenschutz-Folgenabschätzung (DSFA Art. 35), k-Anonymität & EU AI Act.",
|
|
||||||
"certificateKey": "ihk_ap2_kdm_controlling",
|
|
||||||
"chapters": [
|
|
||||||
{
|
|
||||||
"folder": "07_datengetriebenes_controlling_und_kpis",
|
|
||||||
"title": "LF 11: Datengetriebenes Controlling & KPI-Dashboards (STP-Rate, IBCS)",
|
|
||||||
"taskFile": "README.md",
|
|
||||||
"testFile": "index.html"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"folder": "08_datenschutz_folgenabschaetzung_und_ai_act",
|
|
||||||
"title": "LF 12: Datenschutz-Folgenabschätzung (DSFA) & EU AI Act (Risikoklassen)",
|
|
||||||
"taskFile": "README.md",
|
|
||||||
"testFile": "index.html"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"folder": "09_it_sicherheitsmanagement_und_risiko",
|
|
||||||
"title": "LF 12: IT-Sicherheitsmanagement & Risikobeurteilung (BSI, R = E * S)",
|
|
||||||
"taskFile": "README.md",
|
|
||||||
"testFile": "index.html"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "track_4_nachhaltigkeit_projekt_und_pruefung",
|
|
||||||
"title": "🏆 Lehrpfad 4: Nachhaltigkeit, 40h-Projektarbeit & AP2-Prüfungssimulation",
|
|
||||||
"description": "Green IT, PUE-Wert, Mitbestimmung des Betriebsrats (§ 87 BetrVG), Leitfaden für die 40h-Projektarbeit zur Prozessdigitalisierung & Vollprüfungssimulation.",
|
|
||||||
"certificateKey": "ihk_ap2_kdm_master",
|
|
||||||
"chapters": [
|
|
||||||
{
|
|
||||||
"folder": "10_nachhaltigkeit_green_it_und_wiso",
|
|
||||||
"title": "WiSo: Nachhaltigkeit (Green IT), Ethik & Mitbestimmung (§ 87 BetrVG)",
|
|
||||||
"taskFile": "README.md",
|
|
||||||
"testFile": "index.html"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"folder": "11_betriebliche_projektarbeit_guide_40h",
|
|
||||||
"title": "Projekt: Betriebliche Projektarbeit (40h) & Fachgespräch (Guide KDM)",
|
|
||||||
"taskFile": "README.md",
|
|
||||||
"testFile": "index.html"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"folder": "12_ap2_kdm_gesamtpruefung_simulation",
|
|
||||||
"title": "Abschluss: AP2 KDM Gesamtprüfungssimulation (PB 1, PB 2 & WiSo)",
|
|
||||||
"taskFile": "README.md",
|
|
||||||
"testFile": "index.html"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,298 +0,0 @@
|
||||||
/**
|
|
||||||
* 🧾 IHK BELEG- & KALKULATIONS-TRAINER (PrintTop GmbH) 🧾
|
|
||||||
* ====================================================
|
|
||||||
* Interaktiver Trainer für kaufmännische Berechnungen nach ZPA 2. Auflage:
|
|
||||||
* 1. Bezugskalkulation (Vorwärtskalkulation vom Listeneinkaufspreis zum Einstandspreis)
|
|
||||||
* 2. Rechnungsprüfung & Skontoabzug bei Zahlung innerhalb Frist
|
|
||||||
* 3. Verzugszinsberechnung nach § 288 BGB & 40 € Mahnpauschale (§ 288 Abs. 5 BGB)
|
|
||||||
*/
|
|
||||||
|
|
||||||
class BelegKalkulator {
|
|
||||||
constructor(containerId) {
|
|
||||||
this.container = document.getElementById(containerId);
|
|
||||||
this.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
|
||||||
if (!this.container) return;
|
|
||||||
this.render();
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
this.container.innerHTML = `
|
|
||||||
<div class="ihk-tool-card" style="background: #1e293b; border: 1px solid #334155; border-radius: 12px; padding: 24px; margin-bottom: 24px;">
|
|
||||||
<div class="ihk-tool-header" style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
|
||||||
<div>
|
|
||||||
<h3 style="color: #38bdf8; margin: 0; font-size: 1.3rem;">🧾 Interaktiver Beleg- & Kalkulationsrechner</h3>
|
|
||||||
<p style="color: #94a3b8; font-size: 0.9rem; margin: 4px 0 0 0;">PrintTop GmbH • Original ZPA-Aufgabenmuster</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span class="ihk-badge ihk-badge-success">Kaufmännische IT-Praxis</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Modus-Umschalter -->
|
|
||||||
<div style="display: flex; gap: 10px; margin-bottom: 20px; border-bottom: 1px solid #334155; padding-bottom: 12px;">
|
|
||||||
<button id="tab-bezug" class="ihk-btn ihk-btn-primary" style="padding: 6px 14px; font-size: 0.85rem;">📦 Bezugskalkulation (Einstandspreis)</button>
|
|
||||||
<button id="tab-verzug" class="ihk-btn ihk-btn-secondary" style="padding: 6px 14px; font-size: 0.85rem;">⚖️ Verzugszinsen § 288 BGB</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Ansicht 1: Bezugskalkulation -->
|
|
||||||
<div id="view-bezug">
|
|
||||||
<div style="background: #0f172a; padding: 16px; border-radius: 8px; margin-bottom: 18px; border: 1px solid #334155;">
|
|
||||||
<strong style="color: #f8fafc;">Übung:</strong> Die PrintTop GmbH bestellt 10 Hochleistungsserver beim Großhändler.
|
|
||||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 12px; margin-top: 10px; font-size: 0.9rem; color: #cbd5e1;">
|
|
||||||
<div>Listeneinkaufspreis (netto): <strong id="lbl-lep">10.000,00 €</strong></div>
|
|
||||||
<div>Lieferrabatt: <strong id="lbl-rabatt">15 %</strong></div>
|
|
||||||
<div>Lieferskonto: <strong id="lbl-skonto">2 %</strong></div>
|
|
||||||
<div>Bezugskosten (Fracht/Zoll): <strong id="lbl-bezug">350,00 €</strong></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<table class="ihk-table" style="width: 100%; border-collapse: collapse; font-size: 0.9rem;">
|
|
||||||
<thead>
|
|
||||||
<tr style="background: #0f172a; color: #38bdf8; text-align: left;">
|
|
||||||
<th style="padding: 10px; border: 1px solid #334155;">Kalkulationsschritt</th>
|
|
||||||
<th style="padding: 10px; border: 1px solid #334155; width: 140px;">Prozentsatz</th>
|
|
||||||
<th style="padding: 10px; border: 1px solid #334155; width: 180px;">Ihre Eingabe (€)</th>
|
|
||||||
<th style="padding: 10px; border: 1px solid #334155; width: 180px;">Prüfung</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td style="padding: 8px; border: 1px solid #334155;">Listeneinkaufspreis (LEP)</td>
|
|
||||||
<td style="padding: 8px; border: 1px solid #334155;">100 %</td>
|
|
||||||
<td style="padding: 8px; border: 1px solid #334155;"><input type="text" id="val-lep" class="ihk-input" value="10000.00" disabled style="background:#1e293b;"></td>
|
|
||||||
<td style="padding: 8px; border: 1px solid #334155; color: #4ade80;">✓ Gegeben</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td style="padding: 8px; border: 1px solid #334155;">- Lieferrabatt</td>
|
|
||||||
<td style="padding: 8px; border: 1px solid #334155;">15 %</td>
|
|
||||||
<td style="padding: 8px; border: 1px solid #334155;"><input type="number" step="0.01" id="val-rabatt-eur" class="ihk-input" placeholder="z. B. 1500.00"></td>
|
|
||||||
<td id="status-rabatt-eur" style="padding: 8px; border: 1px solid #334155;">-</td>
|
|
||||||
</tr>
|
|
||||||
<tr style="background: rgba(56, 189, 248, 0.05);">
|
|
||||||
<td style="padding: 8px; border: 1px solid #334155; font-weight: bold;">= Zieleinkaufspreis (ZEP)</td>
|
|
||||||
<td style="padding: 8px; border: 1px solid #334155;">85 %</td>
|
|
||||||
<td style="padding: 8px; border: 1px solid #334155;"><input type="number" step="0.01" id="val-zep" class="ihk-input" placeholder="ZEP in €"></td>
|
|
||||||
<td id="status-zep" style="padding: 8px; border: 1px solid #334155;">-</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td style="padding: 8px; border: 1px solid #334155;">- Lieferskonto</td>
|
|
||||||
<td style="padding: 8px; border: 1px solid #334155;">2 % vom ZEP</td>
|
|
||||||
<td style="padding: 8px; border: 1px solid #334155;"><input type="number" step="0.01" id="val-skonto-eur" class="ihk-input" placeholder="Skonto in €"></td>
|
|
||||||
<td id="status-skonto-eur" style="padding: 8px; border: 1px solid #334155;">-</td>
|
|
||||||
</tr>
|
|
||||||
<tr style="background: rgba(56, 189, 248, 0.05);">
|
|
||||||
<td style="padding: 8px; border: 1px solid #334155; font-weight: bold;">= Bareinkaufspreis (BEP)</td>
|
|
||||||
<td style="padding: 8px; border: 1px solid #334155;">-</td>
|
|
||||||
<td style="padding: 8px; border: 1px solid #334155;"><input type="number" step="0.01" id="val-bep" class="ihk-input" placeholder="BEP in €"></td>
|
|
||||||
<td id="status-bep" style="padding: 8px; border: 1px solid #334155;">-</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td style="padding: 8px; border: 1px solid #334155;">+ Bezugskosten (Fracht, Verpackung)</td>
|
|
||||||
<td style="padding: 8px; border: 1px solid #334155;">-</td>
|
|
||||||
<td style="padding: 8px; border: 1px solid #334155;"><input type="number" step="0.01" id="val-bezug-eur" class="ihk-input" value="350.00" disabled style="background:#1e293b;"></td>
|
|
||||||
<td style="padding: 8px; border: 1px solid #334155; color: #4ade80;">✓ Gegeben</td>
|
|
||||||
</tr>
|
|
||||||
<tr style="background: rgba(34, 197, 94, 0.1); font-weight: bold;">
|
|
||||||
<td style="padding: 10px; border: 1px solid #334155; color: #4ade80;">= Bezugspreis / Einstandspreis</td>
|
|
||||||
<td style="padding: 10px; border: 1px solid #334155;">-</td>
|
|
||||||
<td style="padding: 10px; border: 1px solid #334155;"><input type="number" step="0.01" id="val-einstand" class="ihk-input" placeholder="Einstandspreis €" style="border-color: #22c55e;"></td>
|
|
||||||
<td id="status-einstand" style="padding: 10px; border: 1px solid #334155;">-</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<div style="display: flex; gap: 10px; margin-top: 16px;">
|
|
||||||
<button class="ihk-btn ihk-btn-success" id="btn-check-bezug">✓ Eingaben prüfen</button>
|
|
||||||
<button class="ihk-btn ihk-btn-secondary" id="btn-solve-bezug">💡 Lösung aufdecken</button>
|
|
||||||
<button class="ihk-btn ihk-btn-secondary" id="btn-new-bezug">🎲 Neue Zufallszahlen</button>
|
|
||||||
</div>
|
|
||||||
<div id="box-bezug-res" class="ihk-result-box" style="margin-top: 16px;"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Ansicht 2: Verzugszinsen § 288 BGB -->
|
|
||||||
<div id="view-verzug" style="display: none;">
|
|
||||||
<div style="background: #0f172a; padding: 16px; border-radius: 8px; margin-bottom: 18px; border: 1px solid #334155;">
|
|
||||||
<strong style="color: #f8fafc;">Gesetzliche Grundlagen nach BGB (§ 288):</strong>
|
|
||||||
<p style="color: #cbd5e1; font-size: 0.9rem; margin: 8px 0 0 0; line-height: 1.6;">
|
|
||||||
- <strong>Verbrauchergeschäft (B2C):</strong> Basiszinssatz + <strong>5 Prozentpunkte</strong>.<br>
|
|
||||||
- <strong>Handelsgeschäft / Unternehmen (B2B):</strong> Basiszinssatz + <strong>9 Prozentpunkte</strong>.<br>
|
|
||||||
- <strong>Mahnpauschale bei B2B (§ 288 Abs. 5 BGB):</strong> Pauschal <strong>40,00 €</strong> Verzugsschadenersatz (unabhängig von tatsächlichen Mahnkosten!).<br>
|
|
||||||
- <strong>Zinsformel (Deutsche Zinsmethode 30/360):</strong> <code>Z = (Forderungsbetrag * Zinssatz * Verzugstage) / (100 * 360)</code>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 16px; margin-bottom: 20px;">
|
|
||||||
<div class="ihk-input-group">
|
|
||||||
<label>Rechnungsbetrag (Brutto):</label>
|
|
||||||
<input type="number" id="vz-betrag" class="ihk-input" value="11900.00" step="100">
|
|
||||||
</div>
|
|
||||||
<div class="ihk-input-group">
|
|
||||||
<label>Geschäftsart:</label>
|
|
||||||
<select id="vz-art" class="ihk-select">
|
|
||||||
<option value="B2B" selected>B2B (Handelsgeschäft zw. Firmen: +9 %-Pkt.)</option>
|
|
||||||
<option value="B2C">B2C (Verbrauchergeschäft: +5 %-Pkt.)</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="ihk-input-group">
|
|
||||||
<label>Aktueller Basiszinssatz (§ 247 BGB):</label>
|
|
||||||
<input type="number" id="vz-basiszins" class="ihk-input" value="3.62" step="0.01">
|
|
||||||
</div>
|
|
||||||
<div class="ihk-input-group">
|
|
||||||
<label>Tage im Verzug:</label>
|
|
||||||
<input type="number" id="vz-tage" class="ihk-input" value="45" min="1" max="360">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="display: flex; gap: 10px; margin-bottom: 16px;">
|
|
||||||
<button class="ihk-btn ihk-btn-success" id="btn-calc-verzug">🧮 Verzugszinsen & Gesamtforderung berechnen</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="vz-result-card" style="background: #0f172a; border: 1px solid #334155; border-radius: 8px; padding: 18px; display: none;">
|
|
||||||
<h4 style="color: #38bdf8; margin: 0 0 12px 0;">Ergebnis der Zinsberechnung:</h4>
|
|
||||||
<div id="vz-result-content" style="color: #cbd5e1; font-size: 0.92rem; line-height: 1.8;"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
this.attachEvents();
|
|
||||||
}
|
|
||||||
|
|
||||||
attachEvents() {
|
|
||||||
const tabBezug = document.getElementById("tab-bezug");
|
|
||||||
const tabVerzug = document.getElementById("tab-verzug");
|
|
||||||
const viewBezug = document.getElementById("view-bezug");
|
|
||||||
const viewVerzug = document.getElementById("view-verzug");
|
|
||||||
|
|
||||||
tabBezug.addEventListener("click", () => {
|
|
||||||
tabBezug.className = "ihk-btn ihk-btn-primary";
|
|
||||||
tabVerzug.className = "ihk-btn ihk-btn-secondary";
|
|
||||||
viewBezug.style.display = "block";
|
|
||||||
viewVerzug.style.display = "none";
|
|
||||||
});
|
|
||||||
|
|
||||||
tabVerzug.addEventListener("click", () => {
|
|
||||||
tabVerzug.className = "ihk-btn ihk-btn-primary";
|
|
||||||
tabBezug.className = "ihk-btn ihk-btn-secondary";
|
|
||||||
viewVerzug.style.display = "block";
|
|
||||||
viewBezug.style.display = "none";
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById("btn-check-bezug").addEventListener("click", () => this.checkBezug());
|
|
||||||
document.getElementById("btn-solve-bezug").addEventListener("click", () => this.solveBezug());
|
|
||||||
document.getElementById("btn-new-bezug").addEventListener("click", () => this.newBezugScenario());
|
|
||||||
document.getElementById("btn-calc-verzug").addEventListener("click", () => this.calculateVerzug());
|
|
||||||
}
|
|
||||||
|
|
||||||
getBezugValues() {
|
|
||||||
const lep = 10000;
|
|
||||||
const rabattP = 0.15;
|
|
||||||
const skontoP = 0.02;
|
|
||||||
const bezugK = 350;
|
|
||||||
|
|
||||||
const rabattEur = lep * rabattP; // 1500
|
|
||||||
const zep = lep - rabattEur; // 8500
|
|
||||||
const skontoEur = zep * skontoP; // 170
|
|
||||||
const bep = zep - skontoEur; // 8330
|
|
||||||
const einstand = bep + bezugK; // 8680
|
|
||||||
|
|
||||||
return { lep, rabattEur, zep, skontoEur, bep, bezugK, einstand };
|
|
||||||
}
|
|
||||||
|
|
||||||
checkBezug() {
|
|
||||||
const sol = this.getBezugValues();
|
|
||||||
const uRabatt = parseFloat(document.getElementById("val-rabatt-eur").value);
|
|
||||||
const uZep = parseFloat(document.getElementById("val-zep").value);
|
|
||||||
const uSkonto = parseFloat(document.getElementById("val-skonto-eur").value);
|
|
||||||
const uBep = parseFloat(document.getElementById("val-bep").value);
|
|
||||||
const uEinstand = parseFloat(document.getElementById("val-einstand").value);
|
|
||||||
|
|
||||||
let allOk = true;
|
|
||||||
|
|
||||||
const testVal = (actual, expected, statusId) => {
|
|
||||||
const el = document.getElementById(statusId);
|
|
||||||
if (Math.abs(actual - expected) < 0.05) {
|
|
||||||
el.innerHTML = `<span style="color:#4ade80;">✓ ${expected.toFixed(2)} €</span>`;
|
|
||||||
} else {
|
|
||||||
el.innerHTML = `<span style="color:#ef4444;">✕ Soll: ${expected.toFixed(2)} €</span>`;
|
|
||||||
allOk = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
testVal(uRabatt, sol.rabattEur, "status-rabatt-eur");
|
|
||||||
testVal(uZep, sol.zep, "status-zep");
|
|
||||||
testVal(uSkonto, sol.skontoEur, "status-skonto-eur");
|
|
||||||
testVal(uBep, sol.bep, "status-bep");
|
|
||||||
testVal(uEinstand, sol.einstand, "status-einstand");
|
|
||||||
|
|
||||||
const box = document.getElementById("box-bezug-res");
|
|
||||||
box.style.display = "block";
|
|
||||||
if (allOk) {
|
|
||||||
box.innerHTML = `
|
|
||||||
<div style="border-left: 4px solid #22c55e; padding-left: 12px; color: #4ade80;">
|
|
||||||
<strong>🎉 Hervorragend!</strong> Alle 5 Kalkulationsschritte sind centgenau richtig berechnet.<br>
|
|
||||||
Der Bezugspreis beträgt exakt <strong>${sol.einstand.toFixed(2)} €</strong>.
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
} else {
|
|
||||||
box.innerHTML = `
|
|
||||||
<div style="border-left: 4px solid #ef4444; padding-left: 12px; color: #f87171;">
|
|
||||||
<strong>⚠️ Abweichung entdeckt:</strong> Bitte prüfen Sie die rot markierten Zeilen. Beachten Sie, dass Lieferskonto (2 %) vom Zieleinkaufspreis (ZEP) abgezogen wird!
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
solveBezug() {
|
|
||||||
const sol = this.getBezugValues();
|
|
||||||
document.getElementById("val-rabatt-eur").value = sol.rabattEur.toFixed(2);
|
|
||||||
document.getElementById("val-zep").value = sol.zep.toFixed(2);
|
|
||||||
document.getElementById("val-skonto-eur").value = sol.skontoEur.toFixed(2);
|
|
||||||
document.getElementById("val-bep").value = sol.bep.toFixed(2);
|
|
||||||
document.getElementById("val-einstand").value = sol.einstand.toFixed(2);
|
|
||||||
this.checkBezug();
|
|
||||||
}
|
|
||||||
|
|
||||||
newBezugScenario() {
|
|
||||||
this.solveBezug();
|
|
||||||
}
|
|
||||||
|
|
||||||
calculateVerzug() {
|
|
||||||
const betrag = parseFloat(document.getElementById("vz-betrag").value) || 0;
|
|
||||||
const art = document.getElementById("vz-art").value;
|
|
||||||
const basiszins = parseFloat(document.getElementById("vz-basiszins").value) || 0;
|
|
||||||
const tage = parseInt(document.getElementById("vz-tage").value, 10) || 0;
|
|
||||||
|
|
||||||
const aufschlag = art === "B2B" ? 9.0 : 5.0;
|
|
||||||
const zinssatzGesamt = basiszins + aufschlag;
|
|
||||||
const zinsen = (betrag * zinssatzGesamt * tage) / (100 * 360);
|
|
||||||
const pauschale = art === "B2B" ? 40.0 : 0.0;
|
|
||||||
const gesamtForderung = betrag + zinsen + pauschale;
|
|
||||||
|
|
||||||
const card = document.getElementById("vz-result-card");
|
|
||||||
const content = document.getElementById("vz-result-content");
|
|
||||||
card.style.display = "block";
|
|
||||||
|
|
||||||
content.innerHTML = `
|
|
||||||
<table style="width: 100%; border-collapse: collapse;">
|
|
||||||
<tr><td style="padding: 4px 0;">Hauptforderung:</td><td style="text-align: right; font-weight: bold;">${betrag.toFixed(2)} €</td></tr>
|
|
||||||
<tr><td style="padding: 4px 0;">Basiszinssatz:</td><td style="text-align: right;">${basiszins.toFixed(2)} %</td></tr>
|
|
||||||
<tr><td style="padding: 4px 0;">Gesetzlicher Verzugszinsaufschlag (${art}):</td><td style="text-align: right;">+ ${aufschlag.toFixed(2)} %-Punkte</td></tr>
|
|
||||||
<tr style="border-bottom: 1px solid #334155;"><td style="padding: 4px 0; font-weight: bold; color: #38bdf8;">= Verzugszinssatz p.a.:</td><td style="text-align: right; font-weight: bold; color: #38bdf8;">${zinssatzGesamt.toFixed(2)} %</td></tr>
|
|
||||||
<tr><td style="padding: 4px 0;">Verzugstage (30/360 Methode):</td><td style="text-align: right;">${tage} Tage</td></tr>
|
|
||||||
<tr><td style="padding: 4px 0; color: #facc15;">Berechnete Verzugszinsen:</td><td style="text-align: right; color: #facc15; font-weight: bold;">+ ${zinsen.toFixed(2)} €</td></tr>
|
|
||||||
${art === "B2B" ? `<tr><td style="padding: 4px 0; color: #fb923c;">Mahnpauschale (§ 288 Abs. 5 BGB):</td><td style="text-align: right; color: #fb923c; font-weight: bold;">+ 40,00 €</td></tr>` : ""}
|
|
||||||
<tr style="border-top: 2px solid #22c55e; font-size: 1.1rem;"><td style="padding: 8px 0; font-weight: bold; color: #4ade80;">Gesamtforderung an Schuldner:</td><td style="text-align: right; font-weight: bold; color: #4ade80;">${gesamtForderung.toFixed(2)} €</td></tr>
|
|
||||||
</table>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
|
||||||
if (document.getElementById("beleg-kalkulator-container")) {
|
|
||||||
window.belegKalkulator = new BelegKalkulator("beleg-kalkulator-container");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
@ -1,172 +0,0 @@
|
||||||
/**
|
|
||||||
* 🔄 IHK BPMN 2.0 & EPK INTERAKTIVER TRAINER 🔄
|
|
||||||
* ============================================
|
|
||||||
* Interaktiver Trainer zur Geschäftsprozessmodellierung:
|
|
||||||
* 1. Gateway-Logik (XOR, AND, OR) und Pfad-Entscheidungen
|
|
||||||
* 2. Pool & Lane Regeln (Sequenzfluss vs. Nachrichtenfluss)
|
|
||||||
* 3. EPK-Syntaxregeln (Ereignis-Funktions-Alternation, Konnektoren)
|
|
||||||
*/
|
|
||||||
|
|
||||||
class BpmnTrainer {
|
|
||||||
constructor(containerId) {
|
|
||||||
this.container = document.getElementById(containerId);
|
|
||||||
this.currentTask = 0;
|
|
||||||
this.tasks = [
|
|
||||||
{
|
|
||||||
title: "Aufgabe 1: BPMN 2.0 Gateway-Auswahl für IT-Support",
|
|
||||||
scenario: "Ein Kunde meldet ein Softwareproblem im Ticketsystem. Der First-Level-Support analysiert die Anfrage. Wenn das Problem durch Standardwissen gelöst werden kann, wird sofort eine Lösung an den Kunden geschickt. Falls Spezialwissen erforderlich ist, wird das Ticket an das Entwicklerteam eskaliert. Es wird EXAKT EINER dieser beiden Pfade gewählt.",
|
|
||||||
question: "Welches BPMN 2.0 Gateway muss für diese Verzweigung eingesetzt werden?",
|
|
||||||
options: [
|
|
||||||
{ text: "Exklusives Gateway (XOR) - Raute mit 'X' oder leer", correct: true, hint: "Richtig! XOR (Exclusive OR) schlägt genau einen Alternativpfad basierend auf Bedingungen ein." },
|
|
||||||
{ text: "Paralleles Gateway (AND) - Raute mit '+'", correct: false, hint: "Falsch. AND würde BEIDE Pfade gleichzeitig auslösen." },
|
|
||||||
{ text: "Inklusives Gateway (OR) - Raute mit 'O'", correct: false, hint: "Falsch. OR erlaubt eine beliebige Kombination (1 bis alle Pfade)." },
|
|
||||||
{ text: "Ereignisbasiertes Gateway - Raute mit Doppelkreis und Fünfeck", correct: false, hint: "Falsch. Ein event-basiertes Gateway wartet auf externe Ereignisse, nicht auf Datenbedingungen." }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Aufgabe 2: Sequenzfluss vs. Nachrichtenfluss über Pool-Grenzen",
|
|
||||||
scenario: "Ein Onlineshop (Pool 'Webshop AG') kommuniziert mit einem externen Logistikdienstleister (Pool 'LogiTrans GmbH'). Eine Bestellung wurde abgeschlossen und soll an LogiTrans übertragen werden.",
|
|
||||||
question: "Welche Verbindungslinie ist zwischen zwei getrennten Pools (Blackbox oder Whitebox) nach BPMN 2.0-Standard zulässig?",
|
|
||||||
options: [
|
|
||||||
{ text: "Durchgezogene Linie mit gefülltem Pfeil (Sequenzfluss)", correct: false, hint: "Regelverstoß! Ein Sequenzfluss darf NIEMALS eine Pool-Grenze überschreiten." },
|
|
||||||
{ text: "Gestrichelte Linie mit offenem Pfeil und Kreis am Ursprung (Nachrichtenfluss)", correct: true, hint: "Perfekt! Nachrichtenflüsse (Message Flow) sind das EINZIGE Mittel zur Kommunikation zwischen getrennten Pools." },
|
|
||||||
{ text: "Gepunktete Linie ohne Pfeilspitze (Assoziation)", correct: false, hint: "Assoziationen verbinden lediglich Datenobjekte oder Textanmerkungen mit Elementen." },
|
|
||||||
{ text: "Doppelte durchgezogene Linie", correct: false, hint: "Dieses Symbol existiert im BPMN 2.0 Standard nicht." }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Aufgabe 3: EPK Syntaxregeln (Ereignisgesteuerte Prozesskette)",
|
|
||||||
scenario: "Ein Auszubildender zeichnet eine EPK für die Serverüberwachung. Nach dem Ereignis 'Server ausgefallen' setzt er direkt einen XOR-Konnektor, um zwischen zwei Folgeaktionen zu verzweigen.",
|
|
||||||
question: "Ist diese Konstruktion nach den formalen Syntaxregeln einer EPK zulässig?",
|
|
||||||
options: [
|
|
||||||
{ text: "Nein! Ein Ereignis hat keine Entscheidungskraft. Eine XOR- oder ODER-Verzweigung darf nur nach einer FUNKTION erfolgen.", correct: true, hint: "Hervorragend! Ein Ereignis ist ein passiver Zustand ohne logische Intelligenz. Nur Funktionen treffen Entscheidungen, die danach mit XOR/ODER verzweigen dürfen." },
|
|
||||||
{ text: "Ja, jedes Element darf beliebig mit Konnektoren verzweigen.", correct: false, hint: "Falsch. Die Konnektorenregeln der EPK verbieten XOR/ODER direkt nach einem einzelnen Ereignis." },
|
|
||||||
{ text: "Nein, Konnektoren dürfen in EPKs grundsätzlich nur zur Zusammenführung, nie zur Verzweigung genutzt werden.", correct: false, hint: "Falsch. Konnektoren können sowohl verzweigen als auch zusammenführen." },
|
|
||||||
{ text: "Ja, solange das Ereignis ein Sechseck ist.", correct: false, hint: "Falsch. Die geometrische Form ändert nichts an den logischen Verknüpfungsregeln." }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
];
|
|
||||||
this.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
|
||||||
if (!this.container) return;
|
|
||||||
this.render();
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const t = this.tasks[this.currentTask];
|
|
||||||
this.container.innerHTML = `
|
|
||||||
<div class="ihk-tool-card" style="background: #1e293b; border: 1px solid #334155; border-radius: 12px; padding: 24px; margin-bottom: 24px;">
|
|
||||||
<div class="ihk-tool-header" style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
|
||||||
<div>
|
|
||||||
<h3 style="color: #38bdf8; margin: 0; font-size: 1.3rem;">🔄 BPMN 2.0 & EPK Modellierungs-Trainer</h3>
|
|
||||||
<p style="color: #94a3b8; font-size: 0.9rem; margin: 4px 0 0 0;">Frage ${this.currentTask + 1} von ${this.tasks.length}</p>
|
|
||||||
</div>
|
|
||||||
<div style="display: flex; gap: 8px;">
|
|
||||||
${this.tasks.map((_, i) => `
|
|
||||||
<button class="ihk-btn ${i === this.currentTask ? 'ihk-btn-primary' : 'ihk-btn-secondary'}" style="padding: 4px 10px; font-size: 0.8rem;" onclick="window.bpmnTrainer.switchTask(${i})">
|
|
||||||
${i + 1}
|
|
||||||
</button>
|
|
||||||
`).join("")}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="background: #0f172a; padding: 16px; border-radius: 8px; border: 1px solid #334155; margin-bottom: 20px;">
|
|
||||||
<h4 style="color: #facc15; margin: 0 0 8px 0; font-size: 1.05rem;">${t.title}</h4>
|
|
||||||
<p style="color: #cbd5e1; font-size: 0.92rem; line-height: 1.6; margin: 0 0 12px 0;">
|
|
||||||
${t.scenario}
|
|
||||||
</p>
|
|
||||||
<strong style="color: #38bdf8; font-size: 0.95rem;">${t.question}</strong>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="display: flex; flex-direction: column; gap: 10px; margin-bottom: 20px;">
|
|
||||||
${t.options.map((opt, i) => `
|
|
||||||
<div id="bpmn-opt-${i}" style="background: #0f172a; border: 1px solid #334155; border-radius: 8px; padding: 14px 16px; cursor: pointer; transition: all 0.2s;" onclick="window.bpmnTrainer.selectOption(${i})">
|
|
||||||
<label style="cursor: pointer; display: flex; align-items: center; gap: 12px; color: #f8fafc; font-size: 0.92rem;">
|
|
||||||
<input type="radio" name="bpmn-radio" value="${i}" style="transform: scale(1.2);">
|
|
||||||
<span>${opt.text}</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
`).join("")}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="display: flex; gap: 10px;">
|
|
||||||
<button class="ihk-btn ihk-btn-success" id="btn-check-bpmn">✓ Antwort prüfen</button>
|
|
||||||
<button class="ihk-btn ihk-btn-secondary" id="btn-next-bpmn" ${this.currentTask === this.tasks.length - 1 ? 'disabled' : ''}>Nächste Aufgabe →</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="bpmn-feedback" class="ihk-result-box" style="margin-top: 16px;"></div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
document.getElementById("btn-check-bpmn").addEventListener("click", () => this.check());
|
|
||||||
document.getElementById("btn-next-bpmn").addEventListener("click", () => {
|
|
||||||
if (this.currentTask < this.tasks.length - 1) {
|
|
||||||
this.switchTask(this.currentTask + 1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
selectOption(index) {
|
|
||||||
const radios = document.getElementsByName("bpmn-radio");
|
|
||||||
if (radios[index]) radios[index].checked = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
switchTask(index) {
|
|
||||||
this.currentTask = index;
|
|
||||||
this.render();
|
|
||||||
}
|
|
||||||
|
|
||||||
check() {
|
|
||||||
const t = this.tasks[this.currentTask];
|
|
||||||
const radios = document.getElementsByName("bpmn-radio");
|
|
||||||
let selected = -1;
|
|
||||||
for (let i = 0; i < radios.length; i++) {
|
|
||||||
if (radios[i].checked) {
|
|
||||||
selected = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const box = document.getElementById("bpmn-feedback");
|
|
||||||
box.style.display = "block";
|
|
||||||
|
|
||||||
if (selected === -1) {
|
|
||||||
box.innerHTML = `<span style="color: #facc15;">⚠️ Bitte wählen Sie eine Antwort aus.</span>`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const chosen = t.options[selected];
|
|
||||||
t.options.forEach((opt, idx) => {
|
|
||||||
const card = document.getElementById(`bpmn-opt-${idx}`);
|
|
||||||
if (opt.correct) {
|
|
||||||
card.style.borderColor = "#22c55e";
|
|
||||||
card.style.background = "rgba(34, 197, 94, 0.15)";
|
|
||||||
} else if (idx === selected) {
|
|
||||||
card.style.borderColor = "#ef4444";
|
|
||||||
card.style.background = "rgba(239, 68, 68, 0.15)";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (chosen.correct) {
|
|
||||||
box.innerHTML = `
|
|
||||||
<div style="border-left: 4px solid #22c55e; padding-left: 12px; color: #4ade80;">
|
|
||||||
<strong>🎉 Ausgezeichnet!</strong><br>${chosen.hint}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
} else {
|
|
||||||
box.innerHTML = `
|
|
||||||
<div style="border-left: 4px solid #ef4444; padding-left: 12px; color: #f87171;">
|
|
||||||
<strong>❌ Nicht ganz:</strong><br>${chosen.hint}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
|
||||||
if (document.getElementById("bpmn-trainer-container")) {
|
|
||||||
window.bpmnTrainer = new BpmnTrainer("bpmn-trainer-container");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
@ -1,95 +0,0 @@
|
||||||
/**
|
|
||||||
* Interaktiver Konfusionsmatrix- & ML-Kennzahlen-Rechner für FIDP
|
|
||||||
* IHK ZPA Nord-West AP2
|
|
||||||
*/
|
|
||||||
(function() {
|
|
||||||
function renderConfusionMatrix(containerId) {
|
|
||||||
const container = document.getElementById(containerId);
|
|
||||||
if (!container) return;
|
|
||||||
|
|
||||||
container.innerHTML = `
|
|
||||||
<div style="background: #0f172a; border: 1px solid #334155; border-radius: 8px; padding: 20px; color: #f8fafc;">
|
|
||||||
<h4 style="color: #38bdf8; margin-top: 0; margin-bottom: 12px;">📊 Interaktiver Konfusionsmatrix-Rechner (FIDP)</h4>
|
|
||||||
<p style="color: #94a3b8; font-size: 0.9rem; margin-bottom: 16px;">
|
|
||||||
Geben Sie die Werte der 2x2-Konfusionsmatrix ein, um Accuracy, Precision, Recall und F1-Score nach IHK-Formeln live zu berechnen:
|
|
||||||
</p>
|
|
||||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 16px; max-width: 480px; margin-bottom: 20px;">
|
|
||||||
<div style="background: #1e293b; padding: 12px; border-radius: 6px; border-left: 4px solid #22c55e;">
|
|
||||||
<label style="font-size: 0.85rem; color: #4ade80; display: block; font-weight: 600;">True Positive (TP)</label>
|
|
||||||
<input type="number" id="cm_tp" value="80" min="0" class="ihk-input" style="width: 100%; margin-top: 6px;" />
|
|
||||||
</div>
|
|
||||||
<div style="background: #1e293b; padding: 12px; border-radius: 6px; border-left: 4px solid #f59e0b;">
|
|
||||||
<label style="font-size: 0.85rem; color: #fcd34d; display: block; font-weight: 600;">False Positive (FP - Alpha-Fehler)</label>
|
|
||||||
<input type="number" id="cm_fp" value="20" min="0" class="ihk-input" style="width: 100%; margin-top: 6px;" />
|
|
||||||
</div>
|
|
||||||
<div style="background: #1e293b; padding: 12px; border-radius: 6px; border-left: 4px solid #ef4444;">
|
|
||||||
<label style="font-size: 0.85rem; color: #f87171; display: block; font-weight: 600;">False Negative (FN - Beta-Fehler)</label>
|
|
||||||
<input type="number" id="cm_fn" value="10" min="0" class="ihk-input" style="width: 100%; margin-top: 6px;" />
|
|
||||||
</div>
|
|
||||||
<div style="background: #1e293b; padding: 12px; border-radius: 6px; border-left: 4px solid #3b82f6;">
|
|
||||||
<label style="font-size: 0.85rem; color: #60a5fa; display: block; font-weight: 600;">True Negative (TN)</label>
|
|
||||||
<input type="number" id="cm_tn" value="890" min="0" class="ihk-input" style="width: 100%; margin-top: 6px;" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px; background: #1e293b; padding: 16px; border-radius: 6px;">
|
|
||||||
<div>
|
|
||||||
<div style="color: #94a3b8; font-size: 0.8rem;">Genauigkeit (Accuracy)</div>
|
|
||||||
<div id="res_acc" style="font-size: 1.3rem; font-weight: bold; color: #38bdf8;">-- %</div>
|
|
||||||
<div style="font-size: 0.75rem; color: #64748b;">(TP + TN) / Gesamt</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div style="color: #94a3b8; font-size: 0.8rem;">Präzision (Precision)</div>
|
|
||||||
<div id="res_prec" style="font-size: 1.3rem; font-weight: bold; color: #fcd34d;">-- %</div>
|
|
||||||
<div style="font-size: 0.75rem; color: #64748b;">TP / (TP + FP)</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div style="color: #94a3b8; font-size: 0.8rem;">Trefferquote (Recall)</div>
|
|
||||||
<div id="res_rec" style="font-size: 1.3rem; font-weight: bold; color: #4ade80;">-- %</div>
|
|
||||||
<div style="font-size: 0.75rem; color: #64748b;">TP / (TP + FN)</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div style="color: #94a3b8; font-size: 0.8rem;">Harmonisches Mittel (F1)</div>
|
|
||||||
<div id="res_f1" style="font-size: 1.3rem; font-weight: bold; color: #c084fc;">--</div>
|
|
||||||
<div style="font-size: 0.75rem; color: #64748b;">2 * (P * R) / (P + R)</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
function update() {
|
|
||||||
const tp = parseFloat(document.getElementById('cm_tp').value) || 0;
|
|
||||||
const fp = parseFloat(document.getElementById('cm_fp').value) || 0;
|
|
||||||
const fn = parseFloat(document.getElementById('cm_fn').value) || 0;
|
|
||||||
const tn = parseFloat(document.getElementById('cm_tn').value) || 0;
|
|
||||||
const total = tp + fp + fn + tn;
|
|
||||||
|
|
||||||
if (total === 0) return;
|
|
||||||
|
|
||||||
const acc = ((tp + tn) / total) * 100;
|
|
||||||
const prec = (tp + fp > 0) ? (tp / (tp + fp)) * 100 : 0;
|
|
||||||
const rec = (tp + fn > 0) ? (tp / (tp + fn)) * 100 : 0;
|
|
||||||
const p = prec / 100;
|
|
||||||
const r = rec / 100;
|
|
||||||
const f1 = (p + r > 0) ? (2 * (p * r) / (p + r)) : 0;
|
|
||||||
|
|
||||||
document.getElementById('res_acc').textContent = acc.toFixed(2) + ' %';
|
|
||||||
document.getElementById('res_prec').textContent = prec.toFixed(2) + ' %';
|
|
||||||
document.getElementById('res_rec').textContent = rec.toFixed(2) + ' %';
|
|
||||||
document.getElementById('res_f1').textContent = f1.toFixed(3);
|
|
||||||
}
|
|
||||||
|
|
||||||
['cm_tp', 'cm_fp', 'cm_fn', 'cm_tn'].forEach(id => {
|
|
||||||
document.getElementById(id).addEventListener('input', update);
|
|
||||||
});
|
|
||||||
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
|
|
||||||
window.renderConfusionMatrix = renderConfusionMatrix;
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
if (document.getElementById('confusion-matrix-app')) {
|
|
||||||
renderConfusionMatrix('confusion-matrix-app');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
|
|
@ -1,472 +0,0 @@
|
||||||
/* 🎨 DIAGRAM STUDIO (ER, STRUKTOGRAMM, UML) STYLES */
|
|
||||||
|
|
||||||
.ds-container {
|
|
||||||
max-width: 1400px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ds-tab-nav {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
border-bottom: 1px solid #1e293b;
|
|
||||||
padding-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ds-tab-btn {
|
|
||||||
background: #1e293b;
|
|
||||||
color: #cbd5e1;
|
|
||||||
border: 1px solid #334155;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 10px 18px;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
font-weight: 700;
|
|
||||||
cursor: pointer;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
transition: all 0.15s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ds-tab-btn:hover {
|
|
||||||
background: #334155;
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ds-tab-btn.active {
|
|
||||||
background: #1d4ed8;
|
|
||||||
color: #ffffff;
|
|
||||||
border-color: #38bdf8;
|
|
||||||
box-shadow: 0 4px 12px rgba(29, 78, 216, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ds-editor-card {
|
|
||||||
background: #0f172a;
|
|
||||||
border: 1px solid #1e293b;
|
|
||||||
border-radius: 12px;
|
|
||||||
overflow: hidden;
|
|
||||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ds-toolbar {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
background: #1e293b;
|
|
||||||
border-bottom: 1px solid #334155;
|
|
||||||
padding: 10px 16px;
|
|
||||||
gap: 12px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ds-toolbar-left, .ds-toolbar-right {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ds-btn-group {
|
|
||||||
display: flex;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ds-btn {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
padding: 7px 12px;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-size: 0.82rem;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
border: none;
|
|
||||||
transition: all 0.15s ease;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ds-btn-primary { background: #0284c7; color: #ffffff; }
|
|
||||||
.ds-btn-primary:hover { background: #0369a1; }
|
|
||||||
|
|
||||||
.ds-btn-secondary { background: #334155; color: #f8fafc; border: 1px solid #475569; }
|
|
||||||
.ds-btn-secondary:hover { background: #475569; }
|
|
||||||
|
|
||||||
.ds-btn-success { background: #059669; color: #ffffff; }
|
|
||||||
.ds-btn-success:hover { background: #047857; }
|
|
||||||
|
|
||||||
.ds-btn-danger { background: rgba(239, 68, 68, 0.15); color: #f87171; border: 1px solid rgba(239, 68, 68, 0.3); }
|
|
||||||
.ds-btn-danger:hover { background: rgba(239, 68, 68, 0.3); }
|
|
||||||
|
|
||||||
.ds-select-wrap {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
font-size: 0.82rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #cbd5e1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ds-select {
|
|
||||||
background: #0f172a;
|
|
||||||
border: 1px solid #475569;
|
|
||||||
border-radius: 6px;
|
|
||||||
color: #f8fafc;
|
|
||||||
padding: 5px 8px;
|
|
||||||
font-size: 0.82rem;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ds-select:focus {
|
|
||||||
border-color: #38bdf8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ds-input, .ds-textarea {
|
|
||||||
width: 100%;
|
|
||||||
background: #0f172a;
|
|
||||||
border: 1px solid #334155;
|
|
||||||
border-radius: 6px;
|
|
||||||
color: #f8fafc;
|
|
||||||
padding: 8px 10px;
|
|
||||||
font-size: 0.86rem;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ds-input:focus, .ds-textarea:focus {
|
|
||||||
border-color: #38bdf8;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ds-workspace-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 320px;
|
|
||||||
height: 640px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ds-canvas-wrap {
|
|
||||||
position: relative;
|
|
||||||
height: 100%;
|
|
||||||
overflow: auto;
|
|
||||||
background: #090d16;
|
|
||||||
border-right: 1px solid #1e293b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ds-sidebar {
|
|
||||||
background: #0f172a;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ds-sidebar-header {
|
|
||||||
padding: 12px 16px;
|
|
||||||
border-bottom: 1px solid #1e293b;
|
|
||||||
background: rgba(255, 255, 255, 0.02);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ds-sidebar-header h4 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 0.92rem;
|
|
||||||
color: #38bdf8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ds-sidebar-body {
|
|
||||||
padding: 16px;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ds-hint {
|
|
||||||
font-size: 0.84rem;
|
|
||||||
color: #94a3b8;
|
|
||||||
line-height: 1.5;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ds-field-group {
|
|
||||||
margin-bottom: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ds-field-group label {
|
|
||||||
display: block;
|
|
||||||
font-size: 0.82rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #cbd5e1;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ds-attr-card {
|
|
||||||
background: #090d16;
|
|
||||||
border: 1px solid #334155;
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ds-icon-btn {
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
color: #f87171;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ds-modal {
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
background: rgba(15, 23, 42, 0.85);
|
|
||||||
backdrop-filter: blur(6px);
|
|
||||||
z-index: 1000;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ds-modal-content {
|
|
||||||
background: #0f172a;
|
|
||||||
border: 1px solid #334155;
|
|
||||||
border-radius: 12px;
|
|
||||||
max-width: 760px;
|
|
||||||
width: 90%;
|
|
||||||
max-height: 85vh;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
color: #f8fafc;
|
|
||||||
padding: 22px;
|
|
||||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ds-modal-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
border-bottom: 1px solid #334155;
|
|
||||||
padding-bottom: 12px;
|
|
||||||
margin-bottom: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ds-modal-header h3 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1.12rem;
|
|
||||||
color: #38bdf8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ds-modal-close {
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
color: #cbd5e1;
|
|
||||||
font-size: 1.3rem;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ds-code-block {
|
|
||||||
background: #060911;
|
|
||||||
padding: 16px;
|
|
||||||
border-radius: 6px;
|
|
||||||
overflow-y: auto;
|
|
||||||
flex: 1;
|
|
||||||
font-family: monospace;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
color: #f1f5f9;
|
|
||||||
border: 1px solid #1e293b;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ds-modal-footer {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
margin-top: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ==========================================================================
|
|
||||||
📐 STRUKTOGRAMM (DIN 66261) CSS
|
|
||||||
========================================================================== */
|
|
||||||
|
|
||||||
.sg-render-wrapper {
|
|
||||||
padding: 28px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
min-width: 600px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sg-container {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 720px;
|
|
||||||
background: #090d16;
|
|
||||||
border: 2px solid #38bdf8;
|
|
||||||
border-radius: 6px;
|
|
||||||
overflow: hidden;
|
|
||||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sg-header-title {
|
|
||||||
background: #1e293b;
|
|
||||||
color: #cbd5e1;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 0.76rem;
|
|
||||||
font-weight: 700;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.8px;
|
|
||||||
padding: 6px 12px;
|
|
||||||
border-bottom: 2px solid #38bdf8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sg-root-table {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sg-block {
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background 0.12s ease;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sg-block.sg-selected {
|
|
||||||
outline: 2px solid #fbbf24;
|
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sg-instruction {
|
|
||||||
background: #1e293b;
|
|
||||||
border-bottom: 1px solid #334155;
|
|
||||||
padding: 10px 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sg-instruction:hover {
|
|
||||||
background: #253349;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sg-text {
|
|
||||||
font-family: monospace;
|
|
||||||
font-size: 0.88rem;
|
|
||||||
color: #f8fafc;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sg-if {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
border-bottom: 1px solid #334155;
|
|
||||||
background: #1e293b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sg-if-header {
|
|
||||||
position: relative;
|
|
||||||
height: 48px;
|
|
||||||
background: #0f172a;
|
|
||||||
border-bottom: 1px solid #334155;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sg-if-svg {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sg-if-condition {
|
|
||||||
position: absolute;
|
|
||||||
top: 6px;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
text-align: center;
|
|
||||||
font-family: monospace;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #38bdf8;
|
|
||||||
z-index: 2;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sg-if-label-left {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 4px;
|
|
||||||
left: 12px;
|
|
||||||
font-size: 0.72rem;
|
|
||||||
font-weight: 800;
|
|
||||||
color: #4ade80;
|
|
||||||
z-index: 2;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sg-if-label-right {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 4px;
|
|
||||||
right: 12px;
|
|
||||||
font-size: 0.72rem;
|
|
||||||
font-weight: 800;
|
|
||||||
color: #f87171;
|
|
||||||
z-index: 2;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sg-if-branches {
|
|
||||||
display: flex;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sg-if-then {
|
|
||||||
flex: 1;
|
|
||||||
border-right: 1px solid #334155;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sg-if-else {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sg-while, .sg-for {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
border-bottom: 1px solid #334155;
|
|
||||||
background: #1e293b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sg-while-header, .sg-for-header {
|
|
||||||
background: #0f172a;
|
|
||||||
padding: 8px 14px;
|
|
||||||
border-bottom: 1px solid #334155;
|
|
||||||
font-family: monospace;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #38bdf8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sg-badge-loop {
|
|
||||||
color: #fbbf24;
|
|
||||||
font-size: 0.76rem;
|
|
||||||
font-weight: 800;
|
|
||||||
margin-right: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sg-while-body-wrap {
|
|
||||||
display: flex;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sg-while-gutter {
|
|
||||||
width: 28px;
|
|
||||||
background: #0f172a;
|
|
||||||
border-right: 1px solid #334155;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sg-while-body {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sg-empty-block {
|
|
||||||
padding: 8px;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: #64748b;
|
|
||||||
text-align: center;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
@ -1,63 +0,0 @@
|
||||||
/**
|
|
||||||
* 🎨 IT-PRAXISPORTAL – DIAGRAMM STUDIO MAIN CONTROLLER
|
|
||||||
* Schaltet nahtlos zwischen ER-Modell, Struktogramm (DIN 66261) und UML um.
|
|
||||||
*/
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
const tabEr = document.getElementById('tab-btn-er');
|
|
||||||
const tabSg = document.getElementById('tab-btn-struktogramm');
|
|
||||||
const tabUml = document.getElementById('tab-btn-uml');
|
|
||||||
|
|
||||||
const panelEr = document.getElementById('panel-er');
|
|
||||||
const panelSg = document.getElementById('panel-struktogramm');
|
|
||||||
const panelUml = document.getElementById('panel-uml');
|
|
||||||
|
|
||||||
let erInstance = null;
|
|
||||||
let sgInstance = null;
|
|
||||||
let umlInstance = null;
|
|
||||||
|
|
||||||
function switchTab(activeTab) {
|
|
||||||
[tabEr, tabSg, tabUml].forEach(btn => btn && btn.classList.remove('active'));
|
|
||||||
[panelEr, panelSg, panelUml].forEach(p => p && (p.style.display = 'none'));
|
|
||||||
|
|
||||||
if (activeTab === 'er') {
|
|
||||||
tabEr.classList.add('active');
|
|
||||||
panelEr.style.display = 'block';
|
|
||||||
if (!erInstance) {
|
|
||||||
erInstance = new EREditor('panel-er');
|
|
||||||
} else {
|
|
||||||
erInstance.render();
|
|
||||||
}
|
|
||||||
} else if (activeTab === 'struktogramm') {
|
|
||||||
tabSg.classList.add('active');
|
|
||||||
panelSg.style.display = 'block';
|
|
||||||
if (!sgInstance) {
|
|
||||||
sgInstance = new StruktogrammEditor('panel-struktogramm');
|
|
||||||
} else {
|
|
||||||
sgInstance.render();
|
|
||||||
}
|
|
||||||
} else if (activeTab === 'uml') {
|
|
||||||
tabUml.classList.add('active');
|
|
||||||
panelUml.style.display = 'block';
|
|
||||||
if (!umlInstance) {
|
|
||||||
umlInstance = new UMLEditor('panel-uml');
|
|
||||||
} else {
|
|
||||||
umlInstance.render();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tabEr) tabEr.addEventListener('click', () => switchTab('er'));
|
|
||||||
if (tabSg) tabSg.addEventListener('click', () => switchTab('struktogramm'));
|
|
||||||
if (tabUml) tabUml.addEventListener('click', () => switchTab('uml'));
|
|
||||||
|
|
||||||
// Handle URL hash parameters (z.B. #struktogramm oder #uml oder #er)
|
|
||||||
const hash = window.location.hash.replace('#', '').toLowerCase();
|
|
||||||
if (hash === 'struktogramm' || hash === 'sg') {
|
|
||||||
switchTab('struktogramm');
|
|
||||||
} else if (hash === 'uml') {
|
|
||||||
switchTab('uml');
|
|
||||||
} else {
|
|
||||||
switchTab('er');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
@ -1,683 +0,0 @@
|
||||||
/**
|
|
||||||
* 🗄️ IT-PRAXISPORTAL – INTERAKTIVER ER-MODELL EDITOR (ENTITY RELATIONSHIP)
|
|
||||||
* Unterstützt Entitäten, Attribute (PK, FK, NN, UQ), Relationen (1:1, 1:n, n:m),
|
|
||||||
* Notationen (Krähenfuß, Chen, Min-Max), automatische SQL-DDL-Generierung und SVG-Export.
|
|
||||||
*/
|
|
||||||
|
|
||||||
class EREditor {
|
|
||||||
constructor(containerId, options = {}) {
|
|
||||||
this.container = typeof containerId === 'string' ? document.getElementById(containerId) : containerId;
|
|
||||||
this.options = options;
|
|
||||||
this.entities = [];
|
|
||||||
this.relations = [];
|
|
||||||
this.selectedEntityId = null;
|
|
||||||
this.notation = 'crowsfoot'; // 'crowsfoot' | 'chen' | 'minmax'
|
|
||||||
this.draggedEntity = null;
|
|
||||||
this.dragOffset = { x: 0, y: 0 };
|
|
||||||
this.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
|
||||||
if (!this.container) return;
|
|
||||||
this.container.innerHTML = `
|
|
||||||
<div class="ds-editor-card">
|
|
||||||
<div class="ds-toolbar">
|
|
||||||
<div class="ds-toolbar-left">
|
|
||||||
<button type="button" class="ds-btn ds-btn-primary" id="er-btn-add-entity">
|
|
||||||
<span>➕</span> <span>Entität hinzufügen</span>
|
|
||||||
</button>
|
|
||||||
<button type="button" class="ds-btn ds-btn-secondary" id="er-btn-add-relation">
|
|
||||||
<span>🔗</span> <span>Beziehung anlegen</span>
|
|
||||||
</button>
|
|
||||||
<div class="ds-select-wrap">
|
|
||||||
<label>Notation:</label>
|
|
||||||
<select id="er-select-notation" class="ds-select">
|
|
||||||
<option value="crowsfoot">Krähenfuß (Crow's Foot)</option>
|
|
||||||
<option value="chen">Chen-Notation (1, c, m, mc)</option>
|
|
||||||
<option value="minmax">Min-Max-Notation (0,1 / 1,n)</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="ds-select-wrap">
|
|
||||||
<label>Vorlage:</label>
|
|
||||||
<select id="er-select-template" class="ds-select">
|
|
||||||
<option value="">-- IHK-Vorlage wählen --</option>
|
|
||||||
<option value="onlineshop">IHK Online-Shop (Kunde, Bestellung, Artikel)</option>
|
|
||||||
<option value="asset_mgmt">IT-Asset Management (Server, Standort, Ticket)</option>
|
|
||||||
<option value="schulung">IHK Ausbildungsbetrieb (Azubi, Projekt, Ausbilder)</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="ds-toolbar-right">
|
|
||||||
<button type="button" class="ds-btn ds-btn-success" id="er-btn-generate-sql">
|
|
||||||
<span>💾</span> <span>SQL DDL</span>
|
|
||||||
</button>
|
|
||||||
<button type="button" class="ds-btn ds-btn-secondary" id="er-btn-export-svg">
|
|
||||||
<span>🖼️</span> <span>SVG Export</span>
|
|
||||||
</button>
|
|
||||||
<button type="button" class="ds-btn ds-btn-danger" id="er-btn-clear" title="Alles leeren">
|
|
||||||
<span>🗑️</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="ds-workspace-grid">
|
|
||||||
<div class="ds-canvas-wrap" id="er-canvas-container">
|
|
||||||
<svg id="er-svg-canvas" width="100%" height="100%"></svg>
|
|
||||||
</div>
|
|
||||||
<div class="ds-sidebar" id="er-sidebar">
|
|
||||||
<div class="ds-sidebar-header">
|
|
||||||
<h4 id="er-sidebar-title">Eigenschaften</h4>
|
|
||||||
</div>
|
|
||||||
<div class="ds-sidebar-body" id="er-sidebar-content">
|
|
||||||
<p class="ds-hint">Wähle eine Entität oder Beziehung im Diagramm aus, um Eigenschaften und Attribute zu bearbeiten.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- SQL DDL Modal -->
|
|
||||||
<div id="er-sql-modal" class="ds-modal" style="display:none;">
|
|
||||||
<div class="ds-modal-content">
|
|
||||||
<div class="ds-modal-header">
|
|
||||||
<h3>Generiertes SQL DDL Skript (ANSI SQL:2016)</h3>
|
|
||||||
<button type="button" class="ds-modal-close" id="er-sql-modal-close">×</button>
|
|
||||||
</div>
|
|
||||||
<pre id="er-sql-code-display" class="ds-code-block"></pre>
|
|
||||||
<div class="ds-modal-footer">
|
|
||||||
<button type="button" class="ds-btn ds-btn-primary" id="er-sql-copy-btn">In Zwischenablage kopieren</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
this.bindEvents();
|
|
||||||
this.loadTemplate('onlineshop');
|
|
||||||
}
|
|
||||||
|
|
||||||
bindEvents() {
|
|
||||||
document.getElementById('er-btn-add-entity').addEventListener('click', () => this.addEntity());
|
|
||||||
document.getElementById('er-btn-add-relation').addEventListener('click', () => this.promptAddRelation());
|
|
||||||
document.getElementById('er-select-notation').addEventListener('change', (e) => {
|
|
||||||
this.notation = e.target.value;
|
|
||||||
this.render();
|
|
||||||
});
|
|
||||||
document.getElementById('er-select-template').addEventListener('change', (e) => {
|
|
||||||
if (e.target.value) this.loadTemplate(e.target.value);
|
|
||||||
});
|
|
||||||
document.getElementById('er-btn-generate-sql').addEventListener('click', () => this.showSqlModal());
|
|
||||||
document.getElementById('er-sql-modal-close').addEventListener('click', () => {
|
|
||||||
document.getElementById('er-sql-modal').style.display = 'none';
|
|
||||||
});
|
|
||||||
document.getElementById('er-sql-copy-btn').addEventListener('click', () => {
|
|
||||||
const code = document.getElementById('er-sql-code-display').innerText;
|
|
||||||
navigator.clipboard.writeText(code).then(() => {
|
|
||||||
alert('SQL-Skript erfolgreich kopiert!');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
document.getElementById('er-btn-export-svg').addEventListener('click', () => this.exportSvg());
|
|
||||||
document.getElementById('er-btn-clear').addEventListener('click', () => {
|
|
||||||
if (confirm('Möchtest du das aktuelle ER-Modell wirklich leeren?')) {
|
|
||||||
this.entities = [];
|
|
||||||
this.relations = [];
|
|
||||||
this.selectedEntityId = null;
|
|
||||||
this.render();
|
|
||||||
this.renderSidebar();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const canvas = document.getElementById('er-canvas-container');
|
|
||||||
canvas.addEventListener('mousedown', (e) => this.onMouseDown(e));
|
|
||||||
window.addEventListener('mousemove', (e) => this.onMouseMove(e));
|
|
||||||
window.addEventListener('mouseup', () => this.onMouseUp());
|
|
||||||
}
|
|
||||||
|
|
||||||
addEntity(name = 'tbl_neue_tabelle', x = 60, y = 60) {
|
|
||||||
const id = 'ent_' + Date.now() + '_' + Math.floor(Math.random() * 1000);
|
|
||||||
const entity = {
|
|
||||||
id: id,
|
|
||||||
name: name,
|
|
||||||
x: x,
|
|
||||||
y: y,
|
|
||||||
width: 230,
|
|
||||||
attributes: [
|
|
||||||
{ name: 'id', type: 'INT', pk: true, fk: false, nn: true, uq: true },
|
|
||||||
{ name: 'bezeichnung', type: 'VARCHAR(100)', pk: false, fk: false, nn: true, uq: false }
|
|
||||||
]
|
|
||||||
};
|
|
||||||
this.entities.push(entity);
|
|
||||||
this.selectEntity(id);
|
|
||||||
this.render();
|
|
||||||
}
|
|
||||||
|
|
||||||
selectEntity(id) {
|
|
||||||
this.selectedEntityId = id;
|
|
||||||
this.render();
|
|
||||||
this.renderSidebar();
|
|
||||||
}
|
|
||||||
|
|
||||||
promptAddRelation() {
|
|
||||||
if (this.entities.length < 2) {
|
|
||||||
alert('Du benötigst mindestens 2 Entitäten, um eine Beziehung herzustellen.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const fromId = this.selectedEntityId || this.entities[0].id;
|
|
||||||
const otherEntities = this.entities.filter(e => e.id !== fromId);
|
|
||||||
const toId = otherEntities[0].id;
|
|
||||||
|
|
||||||
const relId = 'rel_' + Date.now();
|
|
||||||
this.relations.push({
|
|
||||||
id: relId,
|
|
||||||
name: 'hat',
|
|
||||||
sourceEntityId: fromId,
|
|
||||||
targetEntityId: toId,
|
|
||||||
cardinalitySource: '1',
|
|
||||||
cardinalityTarget: 'n'
|
|
||||||
});
|
|
||||||
this.render();
|
|
||||||
this.renderSidebar();
|
|
||||||
}
|
|
||||||
|
|
||||||
loadTemplate(tplName) {
|
|
||||||
this.entities = [];
|
|
||||||
this.relations = [];
|
|
||||||
|
|
||||||
if (tplName === 'onlineshop') {
|
|
||||||
this.entities = [
|
|
||||||
{
|
|
||||||
id: 'ent_kunde',
|
|
||||||
name: 'tbl_kunde',
|
|
||||||
x: 50,
|
|
||||||
y: 60,
|
|
||||||
width: 230,
|
|
||||||
attributes: [
|
|
||||||
{ name: 'kunde_id', type: 'INT', pk: true, fk: false, nn: true, uq: true },
|
|
||||||
{ name: 'nachname', type: 'VARCHAR(50)', pk: false, fk: false, nn: true, uq: false },
|
|
||||||
{ name: 'vorname', type: 'VARCHAR(50)', pk: false, fk: false, nn: false, uq: false },
|
|
||||||
{ name: 'email', type: 'VARCHAR(120)', pk: false, fk: false, nn: true, uq: true },
|
|
||||||
{ name: 'registriert_am', type: 'DATE', pk: false, fk: false, nn: true, uq: false }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'ent_bestellung',
|
|
||||||
name: 'tbl_bestellung',
|
|
||||||
x: 370,
|
|
||||||
y: 60,
|
|
||||||
width: 230,
|
|
||||||
attributes: [
|
|
||||||
{ name: 'bestell_id', type: 'INT', pk: true, fk: false, nn: true, uq: true },
|
|
||||||
{ name: 'kunde_id', type: 'INT', pk: false, fk: true, nn: true, uq: false },
|
|
||||||
{ name: 'bestelldatum', type: 'DATETIME', pk: false, fk: false, nn: true, uq: false },
|
|
||||||
{ name: 'status', type: 'VARCHAR(20)', pk: false, fk: false, nn: true, uq: false }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'ent_position',
|
|
||||||
name: 'tbl_bestellposition',
|
|
||||||
x: 370,
|
|
||||||
y: 280,
|
|
||||||
width: 230,
|
|
||||||
attributes: [
|
|
||||||
{ name: 'bestell_id', type: 'INT', pk: true, fk: true, nn: true, uq: false },
|
|
||||||
{ name: 'artikel_id', type: 'INT', pk: true, fk: true, nn: true, uq: false },
|
|
||||||
{ name: 'menge', type: 'INT', pk: false, fk: false, nn: true, uq: false },
|
|
||||||
{ name: 'einzelpreis', type: 'DECIMAL(10,2)', pk: false, fk: false, nn: true, uq: false }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'ent_artikel',
|
|
||||||
name: 'tbl_artikel',
|
|
||||||
x: 50,
|
|
||||||
y: 280,
|
|
||||||
width: 230,
|
|
||||||
attributes: [
|
|
||||||
{ name: 'artikel_id', type: 'INT', pk: true, fk: false, nn: true, uq: true },
|
|
||||||
{ name: 'artikelname', type: 'VARCHAR(100)', pk: false, fk: false, nn: true, uq: false },
|
|
||||||
{ name: 'preis', type: 'DECIMAL(10,2)', pk: false, fk: false, nn: true, uq: false },
|
|
||||||
{ name: 'lagerbestand', type: 'INT', pk: false, fk: false, nn: true, uq: false }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
this.relations = [
|
|
||||||
{
|
|
||||||
id: 'rel_kunde_bestellung',
|
|
||||||
name: 'gibt_auf',
|
|
||||||
sourceEntityId: 'ent_kunde',
|
|
||||||
targetEntityId: 'ent_bestellung',
|
|
||||||
cardinalitySource: '1',
|
|
||||||
cardinalityTarget: 'n'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'rel_bestellung_pos',
|
|
||||||
name: 'besteht_aus',
|
|
||||||
sourceEntityId: 'ent_bestellung',
|
|
||||||
targetEntityId: 'ent_position',
|
|
||||||
cardinalitySource: '1',
|
|
||||||
cardinalityTarget: 'n'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'rel_artikel_pos',
|
|
||||||
name: 'enthaelt',
|
|
||||||
sourceEntityId: 'ent_artikel',
|
|
||||||
targetEntityId: 'ent_position',
|
|
||||||
cardinalitySource: '1',
|
|
||||||
cardinalityTarget: 'n'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
} else if (tplName === 'asset_mgmt') {
|
|
||||||
this.entities = [
|
|
||||||
{
|
|
||||||
id: 'ent_server',
|
|
||||||
name: 'tbl_server',
|
|
||||||
x: 60,
|
|
||||||
y: 70,
|
|
||||||
width: 230,
|
|
||||||
attributes: [
|
|
||||||
{ name: 'server_id', type: 'INT', pk: true, fk: false, nn: true, uq: true },
|
|
||||||
{ name: 'hostname', type: 'VARCHAR(60)', pk: false, fk: false, nn: true, uq: true },
|
|
||||||
{ name: 'ip_adresse', type: 'VARCHAR(45)', pk: false, fk: false, nn: true, uq: true },
|
|
||||||
{ name: 'standort_id', type: 'INT', pk: false, fk: true, nn: true, uq: false }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'ent_standort',
|
|
||||||
name: 'tbl_standort',
|
|
||||||
x: 380,
|
|
||||||
y: 70,
|
|
||||||
width: 220,
|
|
||||||
attributes: [
|
|
||||||
{ name: 'standort_id', type: 'INT', pk: true, fk: false, nn: true, uq: true },
|
|
||||||
{ name: 'bezeichnung', type: 'VARCHAR(50)', pk: false, fk: false, nn: true, uq: false },
|
|
||||||
{ name: 'netzwerk_cidr', type: 'VARCHAR(20)', pk: false, fk: false, nn: true, uq: false }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
];
|
|
||||||
this.relations = [
|
|
||||||
{
|
|
||||||
id: 'rel_server_standort',
|
|
||||||
name: 'steht_in',
|
|
||||||
sourceEntityId: 'ent_standort',
|
|
||||||
targetEntityId: 'ent_server',
|
|
||||||
cardinalitySource: '1',
|
|
||||||
cardinalityTarget: 'n'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
} else if (tplName === 'schulung') {
|
|
||||||
this.entities = [
|
|
||||||
{
|
|
||||||
id: 'ent_azubi',
|
|
||||||
name: 'tbl_azubi',
|
|
||||||
x: 60,
|
|
||||||
y: 70,
|
|
||||||
width: 230,
|
|
||||||
attributes: [
|
|
||||||
{ name: 'azubi_id', type: 'INT', pk: true, fk: false, nn: true, uq: true },
|
|
||||||
{ name: 'name', type: 'VARCHAR(60)', pk: false, fk: false, nn: true, uq: false },
|
|
||||||
{ name: 'ausbildungsberuf', type: 'VARCHAR(40)', pk: false, fk: false, nn: true, uq: false },
|
|
||||||
{ name: 'ausbildungsjahr', type: 'INT', pk: false, fk: false, nn: true, uq: false }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'ent_projekt',
|
|
||||||
name: 'tbl_projektarbeit',
|
|
||||||
x: 390,
|
|
||||||
y: 70,
|
|
||||||
width: 230,
|
|
||||||
attributes: [
|
|
||||||
{ name: 'projekt_id', type: 'INT', pk: true, fk: false, nn: true, uq: true },
|
|
||||||
{ name: 'titel', type: 'VARCHAR(150)', pk: false, fk: false, nn: true, uq: false },
|
|
||||||
{ name: 'stundenumfang', type: 'INT', pk: false, fk: false, nn: true, uq: false },
|
|
||||||
{ name: 'azubi_id', type: 'INT', pk: false, fk: true, nn: true, uq: true }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
];
|
|
||||||
this.relations = [
|
|
||||||
{
|
|
||||||
id: 'rel_azubi_projekt',
|
|
||||||
name: 'fuehrt_durch',
|
|
||||||
sourceEntityId: 'ent_azubi',
|
|
||||||
targetEntityId: 'ent_projekt',
|
|
||||||
cardinalitySource: '1',
|
|
||||||
cardinalityTarget: '1'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
this.selectedEntityId = this.entities[0] ? this.entities[0].id : null;
|
|
||||||
this.render();
|
|
||||||
this.renderSidebar();
|
|
||||||
}
|
|
||||||
|
|
||||||
onMouseDown(e) {
|
|
||||||
const rect = document.getElementById('er-svg-canvas').getBoundingClientRect();
|
|
||||||
const mouseX = e.clientX - rect.left;
|
|
||||||
const mouseY = e.clientY - rect.top;
|
|
||||||
|
|
||||||
for (let i = this.entities.length - 1; i >= 0; i--) {
|
|
||||||
const ent = this.entities[i];
|
|
||||||
const headerHeight = 32;
|
|
||||||
const height = headerHeight + ent.attributes.length * 24 + 10;
|
|
||||||
if (mouseX >= ent.x && mouseX <= ent.x + ent.width && mouseY >= ent.y && mouseY <= ent.y + height) {
|
|
||||||
this.draggedEntity = ent;
|
|
||||||
this.dragOffset = { x: mouseX - ent.x, y: mouseY - ent.y };
|
|
||||||
this.selectEntity(ent.id);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMouseMove(e) {
|
|
||||||
if (!this.draggedEntity) return;
|
|
||||||
const rect = document.getElementById('er-svg-canvas').getBoundingClientRect();
|
|
||||||
const mouseX = e.clientX - rect.left;
|
|
||||||
const mouseY = e.clientY - rect.top;
|
|
||||||
|
|
||||||
this.draggedEntity.x = Math.max(10, Math.min(1200, mouseX - this.dragOffset.x));
|
|
||||||
this.draggedEntity.y = Math.max(10, Math.min(800, mouseY - this.dragOffset.y));
|
|
||||||
this.render();
|
|
||||||
}
|
|
||||||
|
|
||||||
onMouseUp() {
|
|
||||||
this.draggedEntity = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const svg = document.getElementById('er-svg-canvas');
|
|
||||||
if (!svg) return;
|
|
||||||
|
|
||||||
let html = `
|
|
||||||
<defs>
|
|
||||||
<pattern id="er-grid" width="20" height="20" patternUnits="userSpaceOnUse">
|
|
||||||
<path d="M 20 0 L 0 0 0 20" fill="none" stroke="rgba(255,255,255,0.05)" stroke-width="1"/>
|
|
||||||
</pattern>
|
|
||||||
</defs>
|
|
||||||
<rect width="100%" height="100%" fill="#090d16"/>
|
|
||||||
<rect width="100%" height="100%" fill="url(#er-grid)"/>
|
|
||||||
`;
|
|
||||||
|
|
||||||
// 1. Relations
|
|
||||||
this.relations.forEach(rel => {
|
|
||||||
const src = this.entities.find(e => e.id === rel.sourceEntityId);
|
|
||||||
const tgt = this.entities.find(e => e.id === rel.targetEntityId);
|
|
||||||
if (!src || !tgt) return;
|
|
||||||
|
|
||||||
const srcCenter = { x: src.x + src.width / 2, y: src.y + 45 };
|
|
||||||
const tgtCenter = { x: tgt.x + tgt.width / 2, y: tgt.y + 45 };
|
|
||||||
|
|
||||||
html += `<line x1="${srcCenter.x}" y1="${srcCenter.y}" x2="${tgtCenter.x}" y2="${tgtCenter.y}" stroke="#38bdf8" stroke-width="2"/>`;
|
|
||||||
|
|
||||||
const midX = (srcCenter.x + tgtCenter.x) / 2;
|
|
||||||
const midY = (srcCenter.y + tgtCenter.y) / 2;
|
|
||||||
|
|
||||||
const srcOffX = srcCenter.x + (tgtCenter.x - srcCenter.x) * 0.25;
|
|
||||||
const srcOffY = srcCenter.y + (tgtCenter.y - srcCenter.y) * 0.25 - 10;
|
|
||||||
const tgtOffX = tgtCenter.x - (tgtCenter.x - srcCenter.x) * 0.25;
|
|
||||||
const tgtOffY = tgtCenter.y - (tgtCenter.y - srcCenter.y) * 0.25 - 10;
|
|
||||||
|
|
||||||
let cardSrcText = rel.cardinalitySource;
|
|
||||||
let cardTgtText = rel.cardinalityTarget;
|
|
||||||
|
|
||||||
if (this.notation === 'chen') {
|
|
||||||
cardSrcText = rel.cardinalitySource === '1' ? '1' : 'n';
|
|
||||||
cardTgtText = rel.cardinalityTarget === '1' ? '1' : 'm';
|
|
||||||
} else if (this.notation === 'minmax') {
|
|
||||||
cardSrcText = rel.cardinalitySource === '1' ? '(0,1)' : '(1,n)';
|
|
||||||
cardTgtText = rel.cardinalityTarget === '1' ? '(1,1)' : '(0,n)';
|
|
||||||
}
|
|
||||||
|
|
||||||
html += `
|
|
||||||
<g transform="translate(${midX}, ${midY})">
|
|
||||||
<rect x="-36" y="-12" width="72" height="24" rx="12" fill="#1e293b" stroke="#38bdf8" stroke-width="1.5"/>
|
|
||||||
<text x="0" y="4" fill="#f8fafc" font-size="10" font-weight="bold" text-anchor="middle" font-family="sans-serif">${rel.name}</text>
|
|
||||||
</g>
|
|
||||||
<rect x="${srcOffX - 14}" y="${srcOffY - 10}" width="28" height="20" rx="4" fill="#0f172a" stroke="#475569" stroke-width="1"/>
|
|
||||||
<text x="${srcOffX}" y="${srcOffY + 4}" fill="#38bdf8" font-size="11" font-weight="bold" text-anchor="middle" font-family="monospace">${cardSrcText}</text>
|
|
||||||
|
|
||||||
<rect x="${tgtOffX - 14}" y="${tgtOffY - 10}" width="28" height="20" rx="4" fill="#0f172a" stroke="#475569" stroke-width="1"/>
|
|
||||||
<text x="${tgtOffX}" y="${tgtOffY + 4}" fill="#38bdf8" font-size="11" font-weight="bold" text-anchor="middle" font-family="monospace">${cardTgtText}</text>
|
|
||||||
`;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 2. Entities
|
|
||||||
this.entities.forEach(ent => {
|
|
||||||
const isSelected = ent.id === this.selectedEntityId;
|
|
||||||
const headerHeight = 34;
|
|
||||||
const attrHeight = 24;
|
|
||||||
const totalHeight = headerHeight + ent.attributes.length * attrHeight + 8;
|
|
||||||
|
|
||||||
html += `
|
|
||||||
<g class="er-entity-node" transform="translate(${ent.x}, ${ent.y})" style="cursor:move;">
|
|
||||||
<rect x="0" y="0" width="${ent.width}" height="${totalHeight}" rx="8" fill="#1e293b" stroke="${isSelected ? '#38bdf8' : '#334155'}" stroke-width="${isSelected ? 2.5 : 1.5}" filter="drop-shadow(0 4px 10px rgba(0,0,0,0.35))"/>
|
|
||||||
<rect x="0" y="0" width="${ent.width}" height="${headerHeight}" rx="8" fill="${isSelected ? '#1d4ed8' : '#0f172a'}"/>
|
|
||||||
<rect x="0" y="${headerHeight - 2}" width="${ent.width}" height="2" fill="${isSelected ? '#38bdf8' : '#334155'}"/>
|
|
||||||
<text x="12" y="22" fill="#ffffff" font-size="13" font-weight="bold" font-family="sans-serif">🗄️ ${ent.name}</text>
|
|
||||||
`;
|
|
||||||
|
|
||||||
ent.attributes.forEach((attr, idx) => {
|
|
||||||
const rowY = headerHeight + idx * attrHeight + 16;
|
|
||||||
let keyBadge = '';
|
|
||||||
let badgeColor = '#94a3b8';
|
|
||||||
if (attr.pk) {
|
|
||||||
keyBadge = 'PK';
|
|
||||||
badgeColor = '#fbbf24';
|
|
||||||
} else if (attr.fk) {
|
|
||||||
keyBadge = 'FK';
|
|
||||||
badgeColor = '#38bdf8';
|
|
||||||
}
|
|
||||||
|
|
||||||
html += `
|
|
||||||
<g transform="translate(10, ${rowY - 12})">
|
|
||||||
${keyBadge ? `<rect x="0" y="0" width="22" height="15" rx="3" fill="rgba(255,255,255,0.08)" stroke="${badgeColor}" stroke-width="1"/><text x="11" y="11" fill="${badgeColor}" font-size="9" font-weight="bold" text-anchor="middle" font-family="monospace">${keyBadge}</text>` : ''}
|
|
||||||
<text x="${keyBadge ? 28 : 6}" y="11" fill="#f8fafc" font-size="11" font-weight="${attr.pk ? 'bold' : 'normal'}" font-family="monospace">${attr.name}</text>
|
|
||||||
<text x="${ent.width - 24}" y="11" fill="#94a3b8" font-size="10" text-anchor="end" font-family="monospace">${attr.type}</text>
|
|
||||||
</g>
|
|
||||||
`;
|
|
||||||
});
|
|
||||||
|
|
||||||
html += `</g>`;
|
|
||||||
});
|
|
||||||
|
|
||||||
svg.innerHTML = html;
|
|
||||||
}
|
|
||||||
|
|
||||||
renderSidebar() {
|
|
||||||
const sidebarTitle = document.getElementById('er-sidebar-title');
|
|
||||||
const content = document.getElementById('er-sidebar-content');
|
|
||||||
if (!content) return;
|
|
||||||
|
|
||||||
const ent = this.entities.find(e => e.id === this.selectedEntityId);
|
|
||||||
if (!ent) {
|
|
||||||
sidebarTitle.innerText = 'Eigenschaften';
|
|
||||||
content.innerHTML = `<p class="ds-hint">Klicke auf eine Entität im Diagramm, um Eigenschaften und Attribute zu bearbeiten.</p>`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
sidebarTitle.innerText = `Entität: ${ent.name}`;
|
|
||||||
|
|
||||||
let html = `
|
|
||||||
<div class="ds-field-group">
|
|
||||||
<label>Tabellenname:</label>
|
|
||||||
<input type="text" id="er-edit-ent-name" class="ds-input" value="${ent.name}">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="display:flex; justify-content:space-between; align-items:center; margin:12px 0 8px 0;">
|
|
||||||
<span style="font-size:0.8rem; font-weight:700; color:#38bdf8; text-transform:uppercase;">Attribute / Spalten:</span>
|
|
||||||
<button type="button" class="ds-btn ds-btn-primary" id="er-btn-add-attr" style="padding:4px 8px; font-size:0.75rem;">+ Spalte</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="display:flex; flex-direction:column; gap:8px; max-height:280px; overflow-y:auto; padding-right:4px;">
|
|
||||||
`;
|
|
||||||
|
|
||||||
ent.attributes.forEach((attr, idx) => {
|
|
||||||
html += `
|
|
||||||
<div class="ds-attr-card">
|
|
||||||
<div style="display:flex; gap:6px; margin-bottom:6px;">
|
|
||||||
<input type="text" data-idx="${idx}" class="ds-input er-attr-name" value="${attr.name}" style="padding:4px 6px; font-size:0.8rem; flex:1;" placeholder="Spaltenname">
|
|
||||||
<select data-idx="${idx}" class="ds-select er-attr-type" style="padding:4px 6px; font-size:0.8rem; width:100px;">
|
|
||||||
<option value="INT" ${attr.type === 'INT' ? 'selected' : ''}>INT</option>
|
|
||||||
<option value="VARCHAR(50)" ${attr.type === 'VARCHAR(50)' ? 'selected' : ''}>VARCHAR(50)</option>
|
|
||||||
<option value="VARCHAR(100)" ${attr.type === 'VARCHAR(100)' ? 'selected' : ''}>VARCHAR(100)</option>
|
|
||||||
<option value="DATE" ${attr.type === 'DATE' ? 'selected' : ''}>DATE</option>
|
|
||||||
<option value="DATETIME" ${attr.type === 'DATETIME' ? 'selected' : ''}>DATETIME</option>
|
|
||||||
<option value="DECIMAL(10,2)" ${attr.type === 'DECIMAL(10,2)' ? 'selected' : ''}>DECIMAL</option>
|
|
||||||
<option value="BOOLEAN" ${attr.type === 'BOOLEAN' ? 'selected' : ''}>BOOLEAN</option>
|
|
||||||
</select>
|
|
||||||
<button type="button" data-idx="${idx}" class="er-attr-del-btn ds-icon-btn" title="Löschen">×</button>
|
|
||||||
</div>
|
|
||||||
<div style="display:flex; gap:10px; font-size:0.75rem; color:#cbd5e1;">
|
|
||||||
<label><input type="checkbox" data-idx="${idx}" class="er-attr-pk" ${attr.pk ? 'checked' : ''}> PK</label>
|
|
||||||
<label><input type="checkbox" data-idx="${idx}" class="er-attr-fk" ${attr.fk ? 'checked' : ''}> FK</label>
|
|
||||||
<label><input type="checkbox" data-idx="${idx}" class="er-attr-nn" ${attr.nn ? 'checked' : ''}> Not Null</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
});
|
|
||||||
|
|
||||||
html += `
|
|
||||||
</div>
|
|
||||||
<div style="margin-top:16px; border-top:1px solid #334155; padding-top:12px;">
|
|
||||||
<button type="button" id="er-btn-delete-entity" class="ds-btn ds-btn-danger" style="width:100%; font-size:0.82rem;">
|
|
||||||
🗑️ Diese Entität löschen
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
content.innerHTML = html;
|
|
||||||
|
|
||||||
document.getElementById('er-edit-ent-name').addEventListener('input', (e) => {
|
|
||||||
ent.name = e.target.value;
|
|
||||||
this.render();
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('er-btn-add-attr').addEventListener('click', () => {
|
|
||||||
ent.attributes.push({ name: 'neues_feld', type: 'VARCHAR(50)', pk: false, fk: false, nn: false, uq: false });
|
|
||||||
this.render();
|
|
||||||
this.renderSidebar();
|
|
||||||
});
|
|
||||||
|
|
||||||
content.querySelectorAll('.er-attr-name').forEach(inp => {
|
|
||||||
inp.addEventListener('input', (e) => {
|
|
||||||
const idx = parseInt(e.target.dataset.idx, 10);
|
|
||||||
ent.attributes[idx].name = e.target.value;
|
|
||||||
this.render();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
content.querySelectorAll('.er-attr-type').forEach(sel => {
|
|
||||||
sel.addEventListener('change', (e) => {
|
|
||||||
const idx = parseInt(e.target.dataset.idx, 10);
|
|
||||||
ent.attributes[idx].type = e.target.value;
|
|
||||||
this.render();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
content.querySelectorAll('.er-attr-pk').forEach(chk => {
|
|
||||||
chk.addEventListener('change', (e) => {
|
|
||||||
const idx = parseInt(e.target.dataset.idx, 10);
|
|
||||||
ent.attributes[idx].pk = e.target.checked;
|
|
||||||
this.render();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
content.querySelectorAll('.er-attr-fk').forEach(chk => {
|
|
||||||
chk.addEventListener('change', (e) => {
|
|
||||||
const idx = parseInt(e.target.dataset.idx, 10);
|
|
||||||
ent.attributes[idx].fk = e.target.checked;
|
|
||||||
this.render();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
content.querySelectorAll('.er-attr-nn').forEach(chk => {
|
|
||||||
chk.addEventListener('change', (e) => {
|
|
||||||
const idx = parseInt(e.target.dataset.idx, 10);
|
|
||||||
ent.attributes[idx].nn = e.target.checked;
|
|
||||||
this.render();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
content.querySelectorAll('.er-attr-del-btn').forEach(btn => {
|
|
||||||
btn.addEventListener('click', (e) => {
|
|
||||||
const idx = parseInt(e.target.dataset.idx, 10);
|
|
||||||
ent.attributes.splice(idx, 1);
|
|
||||||
this.render();
|
|
||||||
this.renderSidebar();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('er-btn-delete-entity').addEventListener('click', () => {
|
|
||||||
if (confirm(`Entität "${ent.name}" wirklich löschen?`)) {
|
|
||||||
this.entities = this.entities.filter(e => e.id !== ent.id);
|
|
||||||
this.relations = this.relations.filter(r => r.sourceEntityId !== ent.id && r.targetEntityId !== ent.id);
|
|
||||||
this.selectedEntityId = this.entities[0] ? this.entities[0].id : null;
|
|
||||||
this.render();
|
|
||||||
this.renderSidebar();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
generateSqlDDL() {
|
|
||||||
let sql = `-- ========================================================\n`;
|
|
||||||
sql += `-- IHK Prüfungsvorbereitung: Relationales Datenbankschema\n`;
|
|
||||||
sql += `-- Generiert vom IT-Praxisportal ER-Modell Studio\n`;
|
|
||||||
sql += `-- Standard: ANSI SQL:2016\n`;
|
|
||||||
sql += `-- ========================================================\n\n`;
|
|
||||||
|
|
||||||
this.entities.forEach(ent => {
|
|
||||||
sql += `CREATE TABLE ${ent.name} (\n`;
|
|
||||||
const pks = ent.attributes.filter(a => a.pk).map(a => a.name);
|
|
||||||
const lines = [];
|
|
||||||
|
|
||||||
ent.attributes.forEach(attr => {
|
|
||||||
let line = ` ${attr.name} ${attr.type}`;
|
|
||||||
if (attr.nn) line += ` NOT NULL`;
|
|
||||||
if (attr.uq && !attr.pk) line += ` UNIQUE`;
|
|
||||||
lines.push(line);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (pks.length > 0) {
|
|
||||||
lines.push(` PRIMARY KEY (${pks.join(', ')})`);
|
|
||||||
}
|
|
||||||
|
|
||||||
sql += lines.join(',\n');
|
|
||||||
sql += `\n);\n\n`;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (this.relations.length > 0) {
|
|
||||||
sql += `-- Fremdschlüssel-Beziehungen (Referentielle Integrität)\n`;
|
|
||||||
this.relations.forEach(rel => {
|
|
||||||
const src = this.entities.find(e => e.id === rel.sourceEntityId);
|
|
||||||
const tgt = this.entities.find(e => e.id === rel.targetEntityId);
|
|
||||||
if (src && tgt) {
|
|
||||||
const srcPk = src.attributes.find(a => a.pk) || { name: 'id' };
|
|
||||||
sql += `ALTER TABLE ${tgt.name} ADD CONSTRAINT fk_${tgt.name}_${src.name}\n`;
|
|
||||||
sql += ` FOREIGN KEY (${src.name}_id) REFERENCES ${src.name}(${srcPk.name})\n`;
|
|
||||||
sql += ` ON DELETE RESTRICT ON UPDATE CASCADE;\n\n`;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return sql;
|
|
||||||
}
|
|
||||||
|
|
||||||
showSqlModal() {
|
|
||||||
const ddl = this.generateSqlDDL();
|
|
||||||
document.getElementById('er-sql-code-display').innerText = ddl;
|
|
||||||
document.getElementById('er-sql-modal').style.display = 'flex';
|
|
||||||
}
|
|
||||||
|
|
||||||
exportSvg() {
|
|
||||||
const svgEl = document.getElementById('er-svg-canvas');
|
|
||||||
if (!svgEl) return;
|
|
||||||
const serializer = new XMLSerializer();
|
|
||||||
const source = serializer.serializeToString(svgEl);
|
|
||||||
const blob = new Blob([source], { type: 'image/svg+xml;charset=utf-8' });
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
const a = document.createElement('a');
|
|
||||||
a.href = url;
|
|
||||||
a.download = 'er_modell_ihk.svg';
|
|
||||||
a.click();
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.EREditor = EREditor;
|
|
||||||
|
|
@ -153,12 +153,9 @@ class IhkExamEngine {
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="display: flex; align-items: center; gap: 10px; flex-wrap: wrap;">
|
<div style="display: flex; align-items: center; gap: 10px;">
|
||||||
<a href="../../../diagram_editor.html" target="_blank" class="ihk-btn" style="background: #0284c7; color: #ffffff !important; font-size: 0.82rem; padding: 6px 14px; text-decoration: none; display: inline-flex; align-items: center; gap: 6px;" title="Öffnet das interaktive IHK Diagramm-Studio in neuem Tab">
|
|
||||||
<span>📐</span> <span>Diagramm-Studio (ER / SG / UML)</span>
|
|
||||||
</a>
|
|
||||||
<button id="btn-open-catalog" class="ihk-btn" style="background: #334155; color: #f8fafc; font-size: 0.82rem; padding: 6px 14px; border: 1px solid #475569;">
|
<button id="btn-open-catalog" class="ihk-btn" style="background: #334155; color: #f8fafc; font-size: 0.82rem; padding: 6px 14px; border: 1px solid #475569;">
|
||||||
📚 Fragenkatalog (234 Fragen)
|
📚 150-Fragen-Pool (Drill)
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
@ -350,16 +347,6 @@ class IhkExamEngine {
|
||||||
task.dataset.points = q.points;
|
task.dataset.points = q.points;
|
||||||
task.style.cssText = "background: #1e293b; padding: 24px; border-radius: 12px; border: 1px solid #334155; margin-bottom: 24px;";
|
task.style.cssText = "background: #1e293b; padding: 24px; border-radius: 12px; border: 1px solid #334155; margin-bottom: 24px;";
|
||||||
|
|
||||||
let diagramLinkHtml = '';
|
|
||||||
const topicLow = ((q.topic || '') + ' ' + (q.question || '')).toLowerCase();
|
|
||||||
if (topicLow.includes('er-modell') || topicLow.includes('kardinalit') || topicLow.includes('normalis')) {
|
|
||||||
diagramLinkHtml = `<div style="margin-bottom: 12px;"><a href="../../../diagram_editor.html#er" target="_blank" class="ihk-btn" style="padding: 4px 10px; font-size: 0.78rem; background: rgba(56, 189, 248, 0.15); color: #38bdf8 !important; border: 1px solid rgba(56, 189, 248, 0.3); text-decoration: none; border-radius: 4px; display: inline-flex; align-items: center; gap: 6px;"><span>🗄️</span> <span>Im ER-Modell Editor entwerfen / prüfen →</span></a></div>`;
|
|
||||||
} else if (topicLow.includes('struktogramm') || topicLow.includes('nassi') || topicLow.includes('din 66261')) {
|
|
||||||
diagramLinkHtml = `<div style="margin-bottom: 12px;"><a href="../../../diagram_editor.html#struktogramm" target="_blank" class="ihk-btn" style="padding: 4px 10px; font-size: 0.78rem; background: rgba(56, 189, 248, 0.15); color: #38bdf8 !important; border: 1px solid rgba(56, 189, 248, 0.3); text-decoration: none; border-radius: 4px; display: inline-flex; align-items: center; gap: 6px;"><span>📐</span> <span>Im Struktogramm-Editor entwerfen / prüfen →</span></a></div>`;
|
|
||||||
} else if (topicLow.includes('uml') || topicLow.includes('klassendiagramm') || topicLow.includes('sequenzdiagramm') || topicLow.includes('use case')) {
|
|
||||||
diagramLinkHtml = `<div style="margin-bottom: 12px;"><a href="../../../diagram_editor.html#uml" target="_blank" class="ihk-btn" style="padding: 4px 10px; font-size: 0.78rem; background: rgba(56, 189, 248, 0.15); color: #38bdf8 !important; border: 1px solid rgba(56, 189, 248, 0.3); text-decoration: none; border-radius: 4px; display: inline-flex; align-items: center; gap: 6px;"><span>🔷</span> <span>Im UML-Studio entwerfen / prüfen →</span></a></div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
task.innerHTML = `
|
task.innerHTML = `
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 14px;">
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 14px;">
|
||||||
<h3 style="color: #38bdf8; margin: 0; font-size: 1.15rem;">
|
<h3 style="color: #38bdf8; margin: 0; font-size: 1.15rem;">
|
||||||
|
|
@ -383,7 +370,6 @@ class IhkExamEngine {
|
||||||
<p style="color: #e2e8f0; font-size: 0.95rem; line-height: 1.6; margin-top: 0;">
|
<p style="color: #e2e8f0; font-size: 0.95rem; line-height: 1.6; margin-top: 0;">
|
||||||
${this.formatQuestionText(q.question)}
|
${this.formatQuestionText(q.question)}
|
||||||
</p>
|
</p>
|
||||||
${diagramLinkHtml}
|
|
||||||
<textarea class="ihk-input" rows="${q.points > 8 ? 6 : 4}" placeholder="Ihre Bearbeitung hier eingeben (Fachbegriffe, Formeln, Begründungen)..."></textarea>
|
<textarea class="ihk-input" rows="${q.points > 8 ? 6 : 4}" placeholder="Ihre Bearbeitung hier eingeben (Fachbegriffe, Formeln, Begründungen)..."></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,105 +0,0 @@
|
||||||
/**
|
|
||||||
* Interaktiver Handelskalkulations-Trainer für ITSM & KDM
|
|
||||||
* IHK ZPA Nord-West AP2
|
|
||||||
*/
|
|
||||||
(function() {
|
|
||||||
function renderHandelskalkulation(containerId) {
|
|
||||||
const container = document.getElementById(containerId);
|
|
||||||
if (!container) return;
|
|
||||||
|
|
||||||
container.innerHTML = `
|
|
||||||
<div style="background: #0f172a; border: 1px solid #334155; border-radius: 8px; padding: 20px; color: #f8fafc;">
|
|
||||||
<h4 style="color: #38bdf8; margin-top: 0; margin-bottom: 12px;">📊 Interaktiver Handelskalkulations-Trainer (ITSM / KDM)</h4>
|
|
||||||
<p style="color: #94a3b8; font-size: 0.9rem; margin-bottom: 16px;">
|
|
||||||
Geben Sie die kaufmännischen Eckdaten ein, um das Vorwärtskalkulations-Schema nach IHK-Standard durchzurechnen:
|
|
||||||
</p>
|
|
||||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px; margin-bottom: 20px;">
|
|
||||||
<div style="background: #1e293b; padding: 10px; border-radius: 6px;">
|
|
||||||
<label style="font-size: 0.8rem; color: #cbd5e1; display: block;">Listeneinkaufspreis (LEP €)</label>
|
|
||||||
<input type="number" id="kalk_lep" value="3000" min="0" step="50" class="ihk-input" style="width: 100%; margin-top: 4px;" />
|
|
||||||
</div>
|
|
||||||
<div style="background: #1e293b; padding: 10px; border-radius: 6px;">
|
|
||||||
<label style="font-size: 0.8rem; color: #cbd5e1; display: block;">Lieferantenrabatt (%)</label>
|
|
||||||
<input type="number" id="kalk_rab" value="20" min="0" max="100" class="ihk-input" style="width: 100%; margin-top: 4px;" />
|
|
||||||
</div>
|
|
||||||
<div style="background: #1e293b; padding: 10px; border-radius: 6px;">
|
|
||||||
<label style="font-size: 0.8rem; color: #cbd5e1; display: block;">Lieferantenskonto (%)</label>
|
|
||||||
<input type="number" id="kalk_sko" value="2" min="0" max="100" class="ihk-input" style="width: 100%; margin-top: 4px;" />
|
|
||||||
</div>
|
|
||||||
<div style="background: #1e293b; padding: 10px; border-radius: 6px;">
|
|
||||||
<label style="font-size: 0.8rem; color: #cbd5e1; display: block;">Bezugskosten (€)</label>
|
|
||||||
<input type="number" id="kalk_bezug" value="50" min="0" step="5" class="ihk-input" style="width: 100%; margin-top: 4px;" />
|
|
||||||
</div>
|
|
||||||
<div style="background: #1e293b; padding: 10px; border-radius: 6px;">
|
|
||||||
<label style="font-size: 0.8rem; color: #cbd5e1; display: block;">Handlungskosten HKZ (%)</label>
|
|
||||||
<input type="number" id="kalk_hkz" value="15" min="0" class="ihk-input" style="width: 100%; margin-top: 4px;" />
|
|
||||||
</div>
|
|
||||||
<div style="background: #1e293b; padding: 10px; border-radius: 6px;">
|
|
||||||
<label style="font-size: 0.8rem; color: #cbd5e1; display: block;">Gewinnzuschlag (%)</label>
|
|
||||||
<input type="number" id="kalk_gewinn" value="25" min="0" class="ihk-input" style="width: 100%; margin-top: 4px;" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="background: #1e293b; border-radius: 6px; padding: 16px; font-family: monospace; font-size: 0.9rem; line-height: 1.8;">
|
|
||||||
<div style="display: flex; justify-content: space-between;"><span>Listeneinkaufspreis:</span> <span id="out_lep">0,00 €</span></div>
|
|
||||||
<div style="display: flex; justify-content: space-between; color: #f87171;"><span>- Lieferantenrabatt:</span> <span id="out_rab">0,00 €</span></div>
|
|
||||||
<div style="display: flex; justify-content: space-between; border-top: 1px solid #475569;"><span>= Zieleinkaufspreis:</span> <span id="out_zep">0,00 €</span></div>
|
|
||||||
<div style="display: flex; justify-content: space-between; color: #f87171;"><span>- Lieferantenskonto:</span> <span id="out_sko">0,00 €</span></div>
|
|
||||||
<div style="display: flex; justify-content: space-between; border-top: 1px solid #475569;"><span>= Bareinkaufspreis:</span> <span id="out_bep">0,00 €</span></div>
|
|
||||||
<div style="display: flex; justify-content: space-between; color: #38bdf8;"><span>+ Bezugskosten:</span> <span id="out_bezug">0,00 €</span></div>
|
|
||||||
<div style="display: flex; justify-content: space-between; border-top: 1px solid #475569; font-weight: bold; color: #fcd34d;"><span>= Einstandspreis (Bezugspreis):</span> <span id="out_einstand">0,00 €</span></div>
|
|
||||||
<div style="display: flex; justify-content: space-between; color: #38bdf8;"><span>+ Handlungskosten:</span> <span id="out_hkz">0,00 €</span></div>
|
|
||||||
<div style="display: flex; justify-content: space-between; border-top: 1px solid #475569;"><span>= Selbstkostenpreis:</span> <span id="out_sk">0,00 €</span></div>
|
|
||||||
<div style="display: flex; justify-content: space-between; color: #4ade80;"><span>+ Gewinnzuschlag:</span> <span id="out_gewinn">0,00 €</span></div>
|
|
||||||
<div style="display: flex; justify-content: space-between; border-top: 2px solid #38bdf8; font-weight: bold; font-size: 1.05rem; color: #38bdf8; padding-top: 4px;"><span>= Barverkaufspreis (BVP netto):</span> <span id="out_bvp">0,00 €</span></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
function update() {
|
|
||||||
const lep = parseFloat(document.getElementById('kalk_lep').value) || 0;
|
|
||||||
const rabP = parseFloat(document.getElementById('kalk_rab').value) || 0;
|
|
||||||
const skoP = parseFloat(document.getElementById('kalk_sko').value) || 0;
|
|
||||||
const bezug = parseFloat(document.getElementById('kalk_bezug').value) || 0;
|
|
||||||
const hkzP = parseFloat(document.getElementById('kalk_hkz').value) || 0;
|
|
||||||
const gewinnP = parseFloat(document.getElementById('kalk_gewinn').value) || 0;
|
|
||||||
|
|
||||||
const rab = lep * (rabP / 100);
|
|
||||||
const zep = lep - rab;
|
|
||||||
const sko = zep * (skoP / 100);
|
|
||||||
const bep = zep - sko;
|
|
||||||
const einstand = bep + bezug;
|
|
||||||
const hkz = einstand * (hkzP / 100);
|
|
||||||
const sk = einstand + hkz;
|
|
||||||
const gewinn = sk * (gewinnP / 100);
|
|
||||||
const bvp = sk + gewinn;
|
|
||||||
|
|
||||||
const fmt = (v) => v.toLocaleString('de-DE', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) + ' €';
|
|
||||||
|
|
||||||
document.getElementById('out_lep').textContent = fmt(lep);
|
|
||||||
document.getElementById('out_rab').textContent = '- ' + fmt(rab);
|
|
||||||
document.getElementById('out_zep').textContent = fmt(zep);
|
|
||||||
document.getElementById('out_sko').textContent = '- ' + fmt(sko);
|
|
||||||
document.getElementById('out_bep').textContent = fmt(bep);
|
|
||||||
document.getElementById('out_bezug').textContent = '+ ' + fmt(bezug);
|
|
||||||
document.getElementById('out_einstand').textContent = fmt(einstand);
|
|
||||||
document.getElementById('out_hkz').textContent = '+ ' + fmt(hkz);
|
|
||||||
document.getElementById('out_sk').textContent = fmt(sk);
|
|
||||||
document.getElementById('out_gewinn').textContent = '+ ' + fmt(gewinn);
|
|
||||||
document.getElementById('out_bvp').textContent = fmt(bvp);
|
|
||||||
}
|
|
||||||
|
|
||||||
['kalk_lep', 'kalk_rab', 'kalk_sko', 'kalk_bezug', 'kalk_hkz', 'kalk_gewinn'].forEach(id => {
|
|
||||||
document.getElementById(id).addEventListener('input', update);
|
|
||||||
});
|
|
||||||
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
|
|
||||||
window.renderHandelskalkulation = renderHandelskalkulation;
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
if (document.getElementById('handelskalkulation-app')) {
|
|
||||||
renderHandelskalkulation('handelskalkulation-app');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -112,34 +112,18 @@
|
||||||
transform: scale(0.98);
|
transform: scale(0.98);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ihk-btn-primary {
|
.ihk-btn-success {
|
||||||
background: #0284c7;
|
background: #16a34a;
|
||||||
color: white !important;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
}
|
||||||
.ihk-btn-primary:hover {
|
.ihk-btn-success:hover {
|
||||||
background: #0369a1;
|
background: #15803d;
|
||||||
color: white !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.ihk-btn-secondary {
|
.ihk-btn-secondary {
|
||||||
background: #475569;
|
background: #475569;
|
||||||
color: white !important;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
}
|
||||||
.ihk-btn-secondary:hover {
|
.ihk-btn-secondary:hover {
|
||||||
background: #334155;
|
background: #334155;
|
||||||
color: white !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ihk-btn-success {
|
|
||||||
background: #16a34a;
|
|
||||||
color: white !important;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
.ihk-btn-success:hover {
|
|
||||||
background: #15803d;
|
|
||||||
color: white !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.ihk-result-box {
|
.ihk-result-box {
|
||||||
|
|
|
||||||
|
|
@ -1,248 +0,0 @@
|
||||||
/**
|
|
||||||
* 📅 IHK NETZPLAN-TRAINER (DIN 69900) 📅
|
|
||||||
* =====================================
|
|
||||||
* Interaktiver Trainer zur Berechnung von:
|
|
||||||
* - Vorwärtsrechnung: FAZ (Frühester Anfangszeitpunkt) & FEZ (Frühester Endzeitpunkt)
|
|
||||||
* - Rückwärtsrechnung: SAZ (Spätester Anfangszeitpunkt) & SEZ (Spätester Endzeitpunkt)
|
|
||||||
* - Pufferzeiten: GP (Gesamtpuffer = SAZ - FAZ) & FP (Freier Puffer = min(FAZ_Nachfolger) - FEZ)
|
|
||||||
* - Kritischer Pfad (Vorgänge mit GP = 0)
|
|
||||||
*/
|
|
||||||
|
|
||||||
class NetzplanTrainer {
|
|
||||||
constructor(containerId) {
|
|
||||||
this.container = document.getElementById(containerId);
|
|
||||||
this.scenarioIndex = 0;
|
|
||||||
this.scenarios = [
|
|
||||||
{
|
|
||||||
title: "Szenario 1: Rollout eines neuen Firmennetzwerks (PrintTop GmbH)",
|
|
||||||
nodes: [
|
|
||||||
{ id: "A", name: "Anforderungsanalyse", d: 3, preds: [], faz: 0, fez: 3, saz: 0, sez: 3, gp: 0, fp: 0 },
|
|
||||||
{ id: "B", name: "Hardware-Beschaffung", d: 5, preds: ["A"], faz: 3, fez: 8, saz: 3, sez: 8, gp: 0, fp: 0 },
|
|
||||||
{ id: "C", name: "Kabelverlegung (Cat 7)", d: 4, preds: ["A"], faz: 3, fez: 7, saz: 4, sez: 8, gp: 1, fp: 1 },
|
|
||||||
{ id: "D", name: "Switch-/Router-Konfiguration", d: 4, preds: ["B"], faz: 8, fez: 12, saz: 8, sez: 12, gp: 0, fp: 0 },
|
|
||||||
{ id: "E", name: "Patchfeld & Dosen auflegen", d: 3, preds: ["C"], faz: 7, fez: 10, saz: 9, sez: 12, gp: 2, fp: 2 },
|
|
||||||
{ id: "F", name: "End-to-End Funktionstest", d: 2, preds: ["D", "E"], faz: 12, fez: 14, saz: 12, sez: 14, gp: 0, fp: 0 },
|
|
||||||
{ id: "G", name: "Übergabe & Abnahme", d: 1, preds: ["F"], faz: 14, fez: 15, saz: 14, sez: 15, gp: 0, fp: 0 }
|
|
||||||
],
|
|
||||||
criticalPath: ["A", "B", "D", "F", "G"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Szenario 2: Migration auf Microsoft Active Directory",
|
|
||||||
nodes: [
|
|
||||||
{ id: "A", name: "Ist-Aufnahme AD DS", d: 2, preds: [], faz: 0, fez: 2, saz: 0, sez: 2, gp: 0, fp: 0 },
|
|
||||||
{ id: "B", name: "OU-Struktur & GPO-Design", d: 4, preds: ["A"], faz: 2, fez: 6, saz: 2, sez: 6, gp: 0, fp: 0 },
|
|
||||||
{ id: "C", name: "Bereitstellung DC-Hardware", d: 3, preds: ["A"], faz: 2, fez: 5, saz: 3, sez: 6, gp: 1, fp: 1 },
|
|
||||||
{ id: "D", name: "Benutzerdaten CSV-Export", d: 2, preds: ["A"], faz: 2, fez: 4, saz: 6, sez: 8, gp: 4, fp: 4 },
|
|
||||||
{ id: "E", name: "Installation Domain Controller", d: 2, preds: ["B", "C"], faz: 6, fez: 8, saz: 6, sez: 8, gp: 0, fp: 0 },
|
|
||||||
{ id: "F", name: "PowerShell Massenimport", d: 3, preds: ["D", "E"], faz: 8, fez: 11, saz: 8, sez: 11, gp: 0, fp: 0 },
|
|
||||||
{ id: "G", name: "Client-Domain-Join & Tests", d: 2, preds: ["F"], faz: 11, fez: 13, saz: 11, sez: 13, gp: 0, fp: 0 }
|
|
||||||
],
|
|
||||||
criticalPath: ["A", "B", "E", "F", "G"]
|
|
||||||
}
|
|
||||||
];
|
|
||||||
this.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
|
||||||
if (!this.container) return;
|
|
||||||
this.render();
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const sc = this.scenarios[this.scenarioIndex];
|
|
||||||
this.container.innerHTML = `
|
|
||||||
<div class="ihk-tool-card" style="background: #1e293b; border: 1px solid #334155; border-radius: 12px; padding: 24px;">
|
|
||||||
<div class="ihk-tool-header" style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
|
||||||
<div>
|
|
||||||
<h3 style="color: #38bdf8; margin: 0; font-size: 1.3rem;">📊 DIN 69900 Netzplantechnik Trainer</h3>
|
|
||||||
<p style="color: #94a3b8; font-size: 0.9rem; margin: 4px 0 0 0;">${sc.title}</p>
|
|
||||||
</div>
|
|
||||||
<div style="display: flex; gap: 8px;">
|
|
||||||
<button class="ihk-btn ihk-btn-secondary" id="np-prev-scenario" style="padding: 6px 12px; font-size: 0.85rem;">← Szenario 1</button>
|
|
||||||
<button class="ihk-btn ihk-btn-secondary" id="np-next-scenario" style="padding: 6px 12px; font-size: 0.85rem;">Szenario 2 →</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="background: #0f172a; border: 1px solid #334155; border-radius: 8px; padding: 16px; margin-bottom: 20px;">
|
|
||||||
<p style="color: #cbd5e1; font-size: 0.9rem; margin: 0 0 10px 0;">
|
|
||||||
<strong>Aufgabe:</strong> Berechnen Sie für jeden Vorgang die Zeitpunkte der <em>Vorwärtsrechnung</em> (FAZ, FEZ), der <em>Rückwärtsrechnung</em> (SAZ, SEZ) sowie den <em>Gesamtpuffer (GP)</em> und <em>Freien Puffer (FP)</em>. Identifizieren Sie anschließend alle Vorgänge auf dem <strong>Kritischen Pfad</strong>.
|
|
||||||
</p>
|
|
||||||
<div style="display: flex; gap: 16px; flex-wrap: wrap; font-size: 0.82rem; color: #94a3b8;">
|
|
||||||
<span>ℹ️ <code>FEZ = FAZ + D</code></span>
|
|
||||||
<span>ℹ️ <code>FAZ = max(FEZ aller Vorgänger)</code></span>
|
|
||||||
<span>ℹ️ <code>SAZ = SEZ - D</code></span>
|
|
||||||
<span>ℹ️ <code>SEZ = min(SAZ aller Nachfolger)</code></span>
|
|
||||||
<span>ℹ️ <code>GP = SAZ - FAZ = SEZ - FEZ</code></span>
|
|
||||||
<span>ℹ️ <code>FP = min(FAZ Nachfolger) - FEZ</code></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Interaktive Tabelle -->
|
|
||||||
<div style="overflow-x: auto; margin-bottom: 20px;">
|
|
||||||
<table class="ihk-table" style="width: 100%; border-collapse: collapse; font-size: 0.85rem; text-align: center;">
|
|
||||||
<thead>
|
|
||||||
<tr style="background: #0f172a; color: #38bdf8;">
|
|
||||||
<th style="padding: 10px; border: 1px solid #334155;">ID</th>
|
|
||||||
<th style="padding: 10px; border: 1px solid #334155; text-align: left;">Vorgang</th>
|
|
||||||
<th style="padding: 10px; border: 1px solid #334155;">Dauer (D)</th>
|
|
||||||
<th style="padding: 10px; border: 1px solid #334155;">Vorgänger</th>
|
|
||||||
<th style="padding: 10px; border: 1px solid #334155; background: #1e3a8a; color: #93c5fd;">FAZ</th>
|
|
||||||
<th style="padding: 10px; border: 1px solid #334155; background: #1e3a8a; color: #93c5fd;">FEZ</th>
|
|
||||||
<th style="padding: 10px; border: 1px solid #334155; background: #701a75; color: #f0abfc;">SAZ</th>
|
|
||||||
<th style="padding: 10px; border: 1px solid #334155; background: #701a75; color: #f0abfc;">SEZ</th>
|
|
||||||
<th style="padding: 10px; border: 1px solid #334155; color: #facc15;">GP</th>
|
|
||||||
<th style="padding: 10px; border: 1px solid #334155; color: #facc15;">FP</th>
|
|
||||||
<th style="padding: 10px; border: 1px solid #334155; color: #f87171;">Kritisch?</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
${sc.nodes.map(n => `
|
|
||||||
<tr id="row-node-${n.id}" style="border: 1px solid #334155;">
|
|
||||||
<td style="padding: 8px; font-weight: bold; color: #38bdf8;">${n.id}</td>
|
|
||||||
<td style="padding: 8px; text-align: left; color: #f8fafc;">${n.name}</td>
|
|
||||||
<td style="padding: 8px; font-weight: 600;">${n.d}</td>
|
|
||||||
<td style="padding: 8px; color: #94a3b8;">${n.preds.length > 0 ? n.preds.join(", ") : "-"}</td>
|
|
||||||
<td style="padding: 6px;"><input type="number" id="in-faz-${n.id}" class="ihk-input" style="width: 55px; text-align: center; padding: 4px;" min="0"></td>
|
|
||||||
<td style="padding: 6px;"><input type="number" id="in-fez-${n.id}" class="ihk-input" style="width: 55px; text-align: center; padding: 4px;" min="0"></td>
|
|
||||||
<td style="padding: 6px;"><input type="number" id="in-saz-${n.id}" class="ihk-input" style="width: 55px; text-align: center; padding: 4px;" min="0"></td>
|
|
||||||
<td style="padding: 6px;"><input type="number" id="in-sez-${n.id}" class="ihk-input" style="width: 55px; text-align: center; padding: 4px;" min="0"></td>
|
|
||||||
<td style="padding: 6px;"><input type="number" id="in-gp-${n.id}" class="ihk-input" style="width: 50px; text-align: center; padding: 4px;" min="0"></td>
|
|
||||||
<td style="padding: 6px;"><input type="number" id="in-fp-${n.id}" class="ihk-input" style="width: 50px; text-align: center; padding: 4px;" min="0"></td>
|
|
||||||
<td style="padding: 6px;"><input type="checkbox" id="in-crit-${n.id}" style="transform: scale(1.3); cursor: pointer;"></td>
|
|
||||||
</tr>
|
|
||||||
`).join("")}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="display: flex; gap: 12px; flex-wrap: wrap;">
|
|
||||||
<button class="ihk-btn ihk-btn-success" id="np-check-btn">✓ Berechnungen prüfen</button>
|
|
||||||
<button class="ihk-btn ihk-btn-secondary" id="np-solve-btn">💡 Musterlösung ausfüllen</button>
|
|
||||||
<button class="ihk-btn ihk-btn-secondary" id="np-reset-btn">Zurücksetzen</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="np-feedback-box" class="ihk-result-box" style="margin-top: 18px;"></div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
this.attachEvents();
|
|
||||||
}
|
|
||||||
|
|
||||||
attachEvents() {
|
|
||||||
document.getElementById("np-check-btn").addEventListener("click", () => this.checkAnswers());
|
|
||||||
document.getElementById("np-solve-btn").addEventListener("click", () => this.solve());
|
|
||||||
document.getElementById("np-reset-btn").addEventListener("click", () => this.render());
|
|
||||||
document.getElementById("np-prev-scenario").addEventListener("click", () => {
|
|
||||||
this.scenarioIndex = 0;
|
|
||||||
this.render();
|
|
||||||
});
|
|
||||||
document.getElementById("np-next-scenario").addEventListener("click", () => {
|
|
||||||
this.scenarioIndex = 1;
|
|
||||||
this.render();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
checkAnswers() {
|
|
||||||
const sc = this.scenarios[this.scenarioIndex];
|
|
||||||
let correctCount = 0;
|
|
||||||
let totalFields = sc.nodes.length * 7;
|
|
||||||
const errors = [];
|
|
||||||
|
|
||||||
sc.nodes.forEach(n => {
|
|
||||||
const row = document.getElementById(`row-node-${n.id}`);
|
|
||||||
const valFaz = parseInt(document.getElementById(`in-faz-${n.id}`).value, 10);
|
|
||||||
const valFez = parseInt(document.getElementById(`in-fez-${n.id}`).value, 10);
|
|
||||||
const valSaz = parseInt(document.getElementById(`in-saz-${n.id}`).value, 10);
|
|
||||||
const valSez = parseInt(document.getElementById(`in-sez-${n.id}`).value, 10);
|
|
||||||
const valGp = parseInt(document.getElementById(`in-gp-${n.id}`).value, 10);
|
|
||||||
const valFp = parseInt(document.getElementById(`in-fp-${n.id}`).value, 10);
|
|
||||||
const valCrit = document.getElementById(`in-crit-${n.id}`).checked;
|
|
||||||
const isActuallyCrit = sc.criticalPath.includes(n.id);
|
|
||||||
|
|
||||||
let nodeOk = true;
|
|
||||||
|
|
||||||
const checkField = (elemId, actual, expected, name) => {
|
|
||||||
const el = document.getElementById(elemId);
|
|
||||||
if (actual === expected) {
|
|
||||||
el.style.border = "1px solid #22c55e";
|
|
||||||
el.style.background = "#064e3b";
|
|
||||||
correctCount++;
|
|
||||||
} else {
|
|
||||||
el.style.border = "1px solid #ef4444";
|
|
||||||
el.style.background = "#450a0a";
|
|
||||||
nodeOk = false;
|
|
||||||
errors.push(`Vorgang ${n.id} (${n.name}): ${name} sollte ${expected} sein (Eingabe: ${isNaN(actual) ? 'leer' : actual}).`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
checkField(`in-faz-${n.id}`, valFaz, n.faz, "FAZ");
|
|
||||||
checkField(`in-fez-${n.id}`, valFez, n.fez, "FEZ");
|
|
||||||
checkField(`in-saz-${n.id}`, valSaz, n.saz, "SAZ");
|
|
||||||
checkField(`in-sez-${n.id}`, valSez, n.sez, "SEZ");
|
|
||||||
checkField(`in-gp-${n.id}`, valGp, n.gp, "GP");
|
|
||||||
checkField(`in-fp-${n.id}`, valFp, n.fp, "FP");
|
|
||||||
|
|
||||||
const critEl = document.getElementById(`in-crit-${n.id}`);
|
|
||||||
if (valCrit === isActuallyCrit) {
|
|
||||||
critEl.parentElement.style.background = "#064e3b";
|
|
||||||
correctCount++;
|
|
||||||
} else {
|
|
||||||
critEl.parentElement.style.background = "#450a0a";
|
|
||||||
nodeOk = false;
|
|
||||||
errors.push(`Vorgang ${n.id} liegt ${isActuallyCrit ? 'AUF' : 'NICHT auf'} dem kritischen Pfad.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nodeOk) {
|
|
||||||
row.style.background = "rgba(34, 197, 94, 0.1)";
|
|
||||||
} else {
|
|
||||||
row.style.background = "rgba(239, 68, 68, 0.1)";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const box = document.getElementById("np-feedback-box");
|
|
||||||
box.style.display = "block";
|
|
||||||
const percent = Math.round((correctCount / totalFields) * 100);
|
|
||||||
|
|
||||||
if (errors.length === 0) {
|
|
||||||
box.innerHTML = `
|
|
||||||
<div style="border-left: 4px solid #22c55e; padding-left: 14px;">
|
|
||||||
<h4 style="color: #4ade80; margin: 0 0 6px 0; font-size: 1.1rem;">🎉 Perfekt! 100% Richtige Netzplanberechnung (${correctCount}/${totalFields} Felder)</h4>
|
|
||||||
<p style="color: #cbd5e1; margin: 0; line-height: 1.5;">
|
|
||||||
Gesamtdauer des Projekts: <strong>${sc.nodes[sc.nodes.length - 1].fez} Zeiteinheiten</strong>.<br>
|
|
||||||
Der kritische Pfad verläuft über: <strong style="color: #f87171;">${sc.criticalPath.join(" → ")}</strong>. Alle Vorgänge hier haben Gesamtpuffer <code>GP = 0</code>.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
} else {
|
|
||||||
box.innerHTML = `
|
|
||||||
<div style="border-left: 4px solid #ef4444; padding-left: 14px;">
|
|
||||||
<h4 style="color: #f87171; margin: 0 0 6px 0; font-size: 1.1rem;">⚠️ ${errors.length} Fehler gefunden (${percent}% korrekt)</h4>
|
|
||||||
<ul style="color: #cbd5e1; margin: 8px 0 0 0; padding-left: 20px; font-size: 0.88rem; line-height: 1.6;">
|
|
||||||
${errors.slice(0, 6).map(e => `<li>${e}</li>`).join("")}
|
|
||||||
${errors.length > 6 ? `<li>... und ${errors.length - 6} weitere Abweichungen.</li>` : ""}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
solve() {
|
|
||||||
const sc = this.scenarios[this.scenarioIndex];
|
|
||||||
sc.nodes.forEach(n => {
|
|
||||||
document.getElementById(`in-faz-${n.id}`).value = n.faz;
|
|
||||||
document.getElementById(`in-fez-${n.id}`).value = n.fez;
|
|
||||||
document.getElementById(`in-saz-${n.id}`).value = n.saz;
|
|
||||||
document.getElementById(`in-sez-${n.id}`).value = n.sez;
|
|
||||||
document.getElementById(`in-gp-${n.id}`).value = n.gp;
|
|
||||||
document.getElementById(`in-fp-${n.id}`).value = n.fp;
|
|
||||||
document.getElementById(`in-crit-${n.id}`).checked = sc.criticalPath.includes(n.id);
|
|
||||||
});
|
|
||||||
this.checkAnswers();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
|
||||||
if (document.getElementById("netzplan-trainer-container")) {
|
|
||||||
window.netzplanTrainer = new NetzplanTrainer("netzplan-trainer-container");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
@ -1,225 +0,0 @@
|
||||||
/**
|
|
||||||
* ⚡ IHK BINÄRPRÄFIX- & TRANSFERZEIT-DRILL (IEC 60027-2) ⚡
|
|
||||||
* ======================================================
|
|
||||||
* Interaktiver Drill für:
|
|
||||||
* 1. Binärpräfixe (KiB, MiB, GiB, TiB) vs. Dezimalpräfixe (kB, MB, GB, TB)
|
|
||||||
* 2. Berechnen der tatsächlichen Speicherkapazität (Herstellerangabe vs. Betriebssystem)
|
|
||||||
* 3. Übertragungszeit-Berechnung t = Datenmenge / Datenrate mit Einheitenkonvertierung
|
|
||||||
*/
|
|
||||||
|
|
||||||
class PrefixConverter {
|
|
||||||
constructor(containerId) {
|
|
||||||
this.container = document.getElementById(containerId);
|
|
||||||
this.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
|
||||||
if (!this.container) return;
|
|
||||||
this.render();
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
this.container.innerHTML = `
|
|
||||||
<div class="ihk-tool-card" style="background: #1e293b; border: 1px solid #334155; border-radius: 12px; padding: 24px; margin-bottom: 24px;">
|
|
||||||
<div class="ihk-tool-header" style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
|
||||||
<div>
|
|
||||||
<h3 style="color: #38bdf8; margin: 0; font-size: 1.3rem;">⚡ IEC-Binärpräfixe & Transferzeit Drill-Master</h3>
|
|
||||||
<p style="color: #94a3b8; font-size: 0.9rem; margin: 4px 0 0 0;">Strikte ZPA 2. Auflage Normierung (KiB, MiB, GiB, TiB)</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span class="ihk-badge ihk-badge-warning">ZPA Pflichtstoff</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="display: flex; gap: 10px; margin-bottom: 20px; border-bottom: 1px solid #334155; padding-bottom: 12px;">
|
|
||||||
<button id="pfx-tab-drill" class="ihk-btn ihk-btn-primary" style="padding: 6px 14px; font-size: 0.85rem;">💾 Festplatten-Kapazität (SI vs. IEC)</button>
|
|
||||||
<button id="pfx-tab-time" class="ihk-btn ihk-btn-secondary" style="padding: 6px 14px; font-size: 0.85rem;">⏱️ Datentransferzeit-Rechner</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Ansicht 1: Festplattenkapazität -->
|
|
||||||
<div id="pfx-view-drill">
|
|
||||||
<div style="background: #0f172a; padding: 16px; border-radius: 8px; border: 1px solid #334155; margin-bottom: 18px;">
|
|
||||||
<p style="color: #cbd5e1; font-size: 0.92rem; margin: 0; line-height: 1.6;">
|
|
||||||
Festplattenhersteller verkaufen Speicher nach <strong>Dezimalpräfixen (SI: 10er-Potenzen)</strong>.<br>
|
|
||||||
Betriebssysteme (Windows, Linux) verwalten Speicher jedoch in <strong>Binärpräfixen (IEC: 2er-Potenzen)</strong>.<br>
|
|
||||||
<em>1 TB (Hersteller) = 1.000.000.000.000 Byte. 1 TiB (IEC) = 1.024⁴ = 1.099.511.627.776 Byte.</em>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-bottom: 20px;">
|
|
||||||
<div class="ihk-input-group">
|
|
||||||
<label>Herstellerangabe (Dezimal):</label>
|
|
||||||
<input type="number" id="pfx-disk-val" class="ihk-input" value="2" min="1" max="100">
|
|
||||||
</div>
|
|
||||||
<div class="ihk-input-group">
|
|
||||||
<label>Einheit:</label>
|
|
||||||
<select id="pfx-disk-unit" class="ihk-select">
|
|
||||||
<option value="GB">GB (10⁹ Byte)</option>
|
|
||||||
<option value="TB" selected>TB (10¹² Byte)</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button class="ihk-btn ihk-btn-success" id="btn-calc-pfx">🧮 Echte Kapazität in GiB / TiB berechnen</button>
|
|
||||||
|
|
||||||
<div id="pfx-disk-result" style="margin-top: 18px; background: #0f172a; border: 1px solid #334155; border-radius: 8px; padding: 18px; display: none;"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Ansicht 2: Datentransferzeit -->
|
|
||||||
<div id="pfx-view-time" style="display: none;">
|
|
||||||
<div style="background: #0f172a; padding: 16px; border-radius: 8px; border: 1px solid #334155; margin-bottom: 18px;">
|
|
||||||
<p style="color: #cbd5e1; font-size: 0.92rem; margin: 0; line-height: 1.6;">
|
|
||||||
<strong>IHK-Klassiker:</strong> Übertragungszeit berechnen unter Berücksichtigung von Byte $\rightarrow$ Bit (Faktor 8) und Protokoll-Overhead!
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-bottom: 20px;">
|
|
||||||
<div class="ihk-input-group">
|
|
||||||
<label>Datenmenge:</label>
|
|
||||||
<input type="number" id="pfx-data-amount" class="ihk-input" value="50" min="1">
|
|
||||||
</div>
|
|
||||||
<div class="ihk-input-group">
|
|
||||||
<label>Datenmengen-Einheit:</label>
|
|
||||||
<select id="pfx-data-unit" class="ihk-select">
|
|
||||||
<option value="MiB">MiB (1.024² Byte)</option>
|
|
||||||
<option value="GiB" selected>GiB (1.024³ Byte)</option>
|
|
||||||
<option value="TiB">TiB (1.024⁴ Byte)</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="ihk-input-group">
|
|
||||||
<label>Netzwerk-Bandbreite:</label>
|
|
||||||
<input type="number" id="pfx-net-speed" class="ihk-input" value="100" min="1">
|
|
||||||
</div>
|
|
||||||
<div class="ihk-input-group">
|
|
||||||
<label>Bandbreiten-Einheit:</label>
|
|
||||||
<select id="pfx-speed-unit" class="ihk-select">
|
|
||||||
<option value="Mbit">Mbit/s (10⁶ Bit/s)</option>
|
|
||||||
<option value="Gbit">Gbit/s (10⁹ Bit/s)</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="ihk-input-group" style="margin-bottom: 20px;">
|
|
||||||
<label>Protokoll-Overhead (z. B. TCP/IP, Verschlüsselung):</label>
|
|
||||||
<select id="pfx-overhead" class="ihk-select">
|
|
||||||
<option value="0">0 % (Theoretisches Maximum)</option>
|
|
||||||
<option value="5">5 % (Typischer TCP/IP Overhead)</option>
|
|
||||||
<option value="10" selected>10 % (Realistischer Netzwerk-Overhead)</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button class="ihk-btn ihk-btn-success" id="btn-calc-time">⏱️ Übertragungsdauer berechnen</button>
|
|
||||||
|
|
||||||
<div id="pfx-time-result" style="margin-top: 18px; background: #0f172a; border: 1px solid #334155; border-radius: 8px; padding: 18px; display: none;"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
this.attachEvents();
|
|
||||||
}
|
|
||||||
|
|
||||||
attachEvents() {
|
|
||||||
const tabDrill = document.getElementById("pfx-tab-drill");
|
|
||||||
const tabTime = document.getElementById("pfx-tab-time");
|
|
||||||
const viewDrill = document.getElementById("pfx-view-drill");
|
|
||||||
const viewTime = document.getElementById("pfx-view-time");
|
|
||||||
|
|
||||||
tabDrill.addEventListener("click", () => {
|
|
||||||
tabDrill.className = "ihk-btn ihk-btn-primary";
|
|
||||||
tabTime.className = "ihk-btn ihk-btn-secondary";
|
|
||||||
viewDrill.style.display = "block";
|
|
||||||
viewTime.style.display = "none";
|
|
||||||
});
|
|
||||||
|
|
||||||
tabTime.addEventListener("click", () => {
|
|
||||||
tabTime.className = "ihk-btn ihk-btn-primary";
|
|
||||||
tabDrill.className = "ihk-btn ihk-btn-secondary";
|
|
||||||
viewTime.style.display = "block";
|
|
||||||
viewDrill.style.display = "none";
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById("btn-calc-pfx").addEventListener("click", () => this.calcCapacity());
|
|
||||||
document.getElementById("btn-calc-time").addEventListener("click", () => this.calcTime());
|
|
||||||
}
|
|
||||||
|
|
||||||
calcCapacity() {
|
|
||||||
const val = parseFloat(document.getElementById("pfx-disk-val").value) || 0;
|
|
||||||
const unit = document.getElementById("pfx-disk-unit").value;
|
|
||||||
const resBox = document.getElementById("pfx-disk-result");
|
|
||||||
resBox.style.display = "block";
|
|
||||||
|
|
||||||
let bytes = 0;
|
|
||||||
if (unit === "GB") bytes = val * 1e9;
|
|
||||||
if (unit === "TB") bytes = val * 1e12;
|
|
||||||
|
|
||||||
const gib = bytes / Math.pow(1024, 3);
|
|
||||||
const tib = bytes / Math.pow(1024, 4);
|
|
||||||
const diffPct = (1 - (gib / (unit === "TB" ? val * 1024 : val))) * 100;
|
|
||||||
|
|
||||||
resBox.innerHTML = `
|
|
||||||
<table style="width: 100%; border-collapse: collapse; color: #cbd5e1; font-size: 0.92rem;">
|
|
||||||
<tr><td style="padding: 6px 0;">Hersteller-Angabe:</td><td style="text-align: right; font-weight: bold; color: #f8fafc;">${val} ${unit} = ${bytes.toLocaleString()} Byte</td></tr>
|
|
||||||
<tr><td style="padding: 6px 0;">Nutzbare Kapazität in GiB (IEC):</td><td style="text-align: right; font-weight: bold; color: #38bdf8;">${gib.toFixed(2)} GiB</td></tr>
|
|
||||||
<tr><td style="padding: 6px 0;">Nutzbare Kapazität in TiB (IEC):</td><td style="text-align: right; font-weight: bold; color: #38bdf8;">${tib.toFixed(3)} TiB</td></tr>
|
|
||||||
<tr style="border-top: 1px solid #334155;"><td style="padding: 8px 0; color: #facc15;">Scheinbarer Verlust durch Präfix-Unterschied:</td><td style="text-align: right; color: #facc15; font-weight: bold;">ca. ${unit === 'TB' ? '9,09 %' : '6,87 %'}</td></tr>
|
|
||||||
</table>
|
|
||||||
<div style="margin-top: 12px; font-size: 0.85rem; color: #94a3b8;">
|
|
||||||
💡 <em>IHK-Prüfungsbegründung: Das Betriebssystem rechnet mit der Basis 1.024 (${unit === 'TB' ? '1.024⁴' : '1.024³'}). Es handelt sich um keinen physischen Datenverlust, sondern eine Diskrepanz zwischen SI- und IEC-Standard.</em>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
calcTime() {
|
|
||||||
const amount = parseFloat(document.getElementById("pfx-data-amount").value) || 0;
|
|
||||||
const unit = document.getElementById("pfx-data-unit").value;
|
|
||||||
const speed = parseFloat(document.getElementById("pfx-net-speed").value) || 0;
|
|
||||||
const speedUnit = document.getElementById("pfx-speed-unit").value;
|
|
||||||
const overheadPct = parseFloat(document.getElementById("pfx-overhead").value) || 0;
|
|
||||||
|
|
||||||
let bytes = 0;
|
|
||||||
if (unit === "MiB") bytes = amount * Math.pow(1024, 2);
|
|
||||||
if (unit === "GiB") bytes = amount * Math.pow(1024, 3);
|
|
||||||
if (unit === "TiB") bytes = amount * Math.pow(1024, 4);
|
|
||||||
|
|
||||||
let totalBits = bytes * 8 * (1 + overheadPct / 100);
|
|
||||||
|
|
||||||
let bitsPerSec = 0;
|
|
||||||
if (speedUnit === "Mbit") bitsPerSec = speed * 1e6;
|
|
||||||
if (speedUnit === "Gbit") bitsPerSec = speed * 1e9;
|
|
||||||
|
|
||||||
const seconds = totalBits / bitsPerSec;
|
|
||||||
const minutes = seconds / 60;
|
|
||||||
const hours = minutes / 60;
|
|
||||||
|
|
||||||
const resBox = document.getElementById("pfx-time-result");
|
|
||||||
resBox.style.display = "block";
|
|
||||||
|
|
||||||
let timeString = "";
|
|
||||||
if (hours >= 1) {
|
|
||||||
const h = Math.floor(hours);
|
|
||||||
const m = Math.floor((hours - h) * 60);
|
|
||||||
const s = Math.round(seconds % 60);
|
|
||||||
timeString = `${h} Stunden, ${m} Minuten, ${s} Sekunden`;
|
|
||||||
} else if (minutes >= 1) {
|
|
||||||
const m = Math.floor(minutes);
|
|
||||||
const s = Math.round(seconds % 60);
|
|
||||||
timeString = `${m} Minuten, ${s} Sekunden`;
|
|
||||||
} else {
|
|
||||||
timeString = `${seconds.toFixed(1)} Sekunden`;
|
|
||||||
}
|
|
||||||
|
|
||||||
resBox.innerHTML = `
|
|
||||||
<table style="width: 100%; border-collapse: collapse; color: #cbd5e1; font-size: 0.92rem;">
|
|
||||||
<tr><td style="padding: 6px 0;">Datenmenge (inkl. ${overheadPct}% Overhead):</td><td style="text-align: right; font-weight: bold; color: #f8fafc;">${(totalBits / 8 / 1024 / 1024).toFixed(1)} MiB (${(totalBits / 1e6).toFixed(0)} Mbit)</td></tr>
|
|
||||||
<tr><td style="padding: 6px 0;">Nettoreichweite Übertragung:</td><td style="text-align: right; font-weight: bold; color: #38bdf8;">${speed} ${speedUnit}/s</td></tr>
|
|
||||||
<tr style="border-top: 2px solid #22c55e;"><td style="padding: 10px 0; font-size: 1.05rem; font-weight: bold; color: #4ade80;">Berechnete Übertragungsdauer (t):</td><td style="text-align: right; font-size: 1.05rem; font-weight: bold; color: #4ade80;">${timeString} (${seconds.toFixed(1)} s)</td></tr>
|
|
||||||
</table>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
|
||||||
if (document.getElementById("prefix-converter-container")) {
|
|
||||||
window.prefixConverter = new PrefixConverter("prefix-converter-container");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
@ -1,76 +0,0 @@
|
||||||
/**
|
|
||||||
* Interaktiver Purdue-Referenzmodell & OT-Zonen-Trainer für FIDV
|
|
||||||
* IHK ZPA Nord-West AP2
|
|
||||||
*/
|
|
||||||
(function() {
|
|
||||||
function renderPurdueTrainer(containerId) {
|
|
||||||
const container = document.getElementById(containerId);
|
|
||||||
if (!container) return;
|
|
||||||
|
|
||||||
const items = [
|
|
||||||
{ name: "PT100 Temperaturfühler & Drucktransmitter", level: "0", hint: "Physikalischer Prozess / Sensorik & Aktorik" },
|
|
||||||
{ name: "Siemens S7-1500 SPS & Frequenzumrichter", level: "1", hint: "Basis-Steuerung (Direct Control)" },
|
|
||||||
{ name: "KTP700 HMI-Touchpanel & Bedienstation", level: "2", hint: "Bereichsbedienung & Monitoring" },
|
|
||||||
{ name: "SCADA-Server & Manufacturing Execution System (MES)", level: "3", hint: "Betriebsleitebene / Fertigungssteuerung" },
|
|
||||||
{ name: "Historian-Spiegel & Jump Host (Fernwartung)", level: "3.5", hint: "Industrial DMZ (Puffer zwischen IT und OT)" },
|
|
||||||
{ name: "SAP ERP & Active Directory der Verwaltung", level: "4", hint: "Enterprise IT / Unternehmensebene" }
|
|
||||||
];
|
|
||||||
|
|
||||||
container.innerHTML = `
|
|
||||||
<div style="background: #0f172a; border: 1px solid #334155; border-radius: 8px; padding: 20px; color: #f8fafc;">
|
|
||||||
<h4 style="color: #38bdf8; margin-top: 0; margin-bottom: 12px;">🏭 Interaktiver Purdue-Modell & OT-Zonen Trainer (FIDV)</h4>
|
|
||||||
<p style="color: #94a3b8; font-size: 0.9rem; margin-bottom: 16px;">
|
|
||||||
Ordnen Sie die Komponenten den korrekten Purdue-Ebenen nach IEC 62443 / ANSI/ISA-95 zu:
|
|
||||||
</p>
|
|
||||||
<div id="purdue_items" style="display: flex; flex-direction: column; gap: 10px; margin-bottom: 16px;">
|
|
||||||
${items.map((item, idx) => `
|
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center; background: #1e293b; padding: 10px 14px; border-radius: 6px;">
|
|
||||||
<span style="font-size: 0.9rem; color: #cbd5e1;">${item.name}</span>
|
|
||||||
<select id="purdue_sel_${idx}" class="ihk-select" style="padding: 6px 10px; border-radius: 4px; background: #0f172a; color: #f8fafc; border: 1px solid #475569;">
|
|
||||||
<option value="">-- Ebene wählen --</option>
|
|
||||||
<option value="0">Level 0: Physikalischer Prozess</option>
|
|
||||||
<option value="1">Level 1: Basis-Steuerung (SPS)</option>
|
|
||||||
<option value="2">Level 2: Bereichsbedienung (HMI)</option>
|
|
||||||
<option value="3">Level 3: Leitebene (MES/SCADA)</option>
|
|
||||||
<option value="3.5">Level 3.5: Industrial DMZ (IDMZ)</option>
|
|
||||||
<option value="4">Level 4: Enterprise IT (ERP)</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
`).join('')}
|
|
||||||
</div>
|
|
||||||
<div style="display: flex; gap: 10px; align-items: center;">
|
|
||||||
<button id="purdue_check_btn" class="ihk-btn ihk-btn-primary" style="padding: 8px 16px;">Ergebnis prüfen</button>
|
|
||||||
<span id="purdue_feedback" style="font-size: 0.9rem; font-weight: 600;"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
document.getElementById('purdue_check_btn').addEventListener('click', () => {
|
|
||||||
let correct = 0;
|
|
||||||
items.forEach((item, idx) => {
|
|
||||||
const sel = document.getElementById(`purdue_sel_${idx}`);
|
|
||||||
if (sel.value === item.level) {
|
|
||||||
sel.style.borderColor = '#22c55e';
|
|
||||||
correct++;
|
|
||||||
} else {
|
|
||||||
sel.style.borderColor = '#ef4444';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const fb = document.getElementById('purdue_feedback');
|
|
||||||
if (correct === items.length) {
|
|
||||||
fb.style.color = '#4ade80';
|
|
||||||
fb.textContent = `🎉 Perfekt! Alle ${correct}/${items.length} Ebenen korrekt nach IEC 62443 zugeordnet.`;
|
|
||||||
} else {
|
|
||||||
fb.style.color = '#f87171';
|
|
||||||
fb.textContent = `${correct}/${items.length} richtig. Prüfen Sie die rot markierten Ebenen.`;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
window.renderPurdueTrainer = renderPurdueTrainer;
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
if (document.getElementById('purdue-trainer-app')) {
|
|
||||||
renderPurdueTrainer('purdue-trainer-app');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
|
|
@ -1,611 +0,0 @@
|
||||||
/**
|
|
||||||
* 📐 IT-PRAXISPORTAL – DIN 66261 STRUKTOGRAMM EDITOR (NASSI-SHNEIDERMAN)
|
|
||||||
* Vollständige Unterstützung für IHK AP1 & AP2 Algorithmenentwurf:
|
|
||||||
* Sequenzen, IF-Verzweigungen, SWITCH-Fallauswahl, WHILE-Schleifen, DO-WHILE,
|
|
||||||
* Zählschleifen (FOR), Call & Break, Live-SVG-Rendering und Python-Code-Generierung!
|
|
||||||
*/
|
|
||||||
|
|
||||||
class StruktogrammEditor {
|
|
||||||
constructor(containerId, options = {}) {
|
|
||||||
this.container = typeof containerId === 'string' ? document.getElementById(containerId) : containerId;
|
|
||||||
this.options = options;
|
|
||||||
this.blocks = [];
|
|
||||||
this.selectedBlockId = null;
|
|
||||||
this.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
|
||||||
if (!this.container) return;
|
|
||||||
this.container.innerHTML = `
|
|
||||||
<div class="ds-editor-card">
|
|
||||||
<div class="ds-toolbar">
|
|
||||||
<div class="ds-toolbar-left">
|
|
||||||
<div class="ds-btn-group">
|
|
||||||
<button type="button" class="ds-btn ds-btn-primary" id="sg-btn-add-inst">
|
|
||||||
<span>▭</span> <span>Anweisung</span>
|
|
||||||
</button>
|
|
||||||
<button type="button" class="ds-btn ds-btn-primary" id="sg-btn-add-if">
|
|
||||||
<span>▽</span> <span>Verzweigung (IF)</span>
|
|
||||||
</button>
|
|
||||||
<button type="button" class="ds-btn ds-btn-primary" id="sg-btn-add-while">
|
|
||||||
<span>⮌</span> <span>Schleife (WHILE)</span>
|
|
||||||
</button>
|
|
||||||
<button type="button" class="ds-btn ds-btn-primary" id="sg-btn-add-for">
|
|
||||||
<span>🔂</span> <span>Zählschleife (FOR)</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="ds-select-wrap">
|
|
||||||
<label>IHK-Vorlage:</label>
|
|
||||||
<select id="sg-select-template" class="ds-select">
|
|
||||||
<option value="">-- IHK-Algorithmus wählen --</option>
|
|
||||||
<option value="ean13">IHK EAN-13 Prüfziffernberechnung</option>
|
|
||||||
<option value="primzahl">IHK Primzahlprüfung</option>
|
|
||||||
<option value="rabatt">IHK Rabattstaffelung mit Skonto</option>
|
|
||||||
<option value="binaersuche">IHK Binäre Suche (Intervallhalbierung)</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="ds-toolbar-right">
|
|
||||||
<button type="button" class="ds-btn ds-btn-success" id="sg-btn-generate-code">
|
|
||||||
<span>🐍</span> <span>Python Code</span>
|
|
||||||
</button>
|
|
||||||
<button type="button" class="ds-btn ds-btn-secondary" id="sg-btn-export-svg">
|
|
||||||
<span>🖼️</span> <span>SVG Export</span>
|
|
||||||
</button>
|
|
||||||
<button type="button" class="ds-btn ds-btn-danger" id="sg-btn-clear" title="Alles leeren">
|
|
||||||
<span>🗑️</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="ds-workspace-grid">
|
|
||||||
<div class="ds-canvas-wrap" id="sg-canvas-container">
|
|
||||||
<div class="sg-render-wrapper" id="sg-render-box"></div>
|
|
||||||
</div>
|
|
||||||
<div class="ds-sidebar" id="sg-sidebar">
|
|
||||||
<div class="ds-sidebar-header">
|
|
||||||
<h4 id="sg-sidebar-title">Block-Eigenschaften</h4>
|
|
||||||
</div>
|
|
||||||
<div class="ds-sidebar-body" id="sg-sidebar-content">
|
|
||||||
<p class="ds-hint">Wähle einen Block im Struktogramm aus, um dessen Inhalt anzupassen oder Unterblöcke hinzuzufügen.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Python Code Modal -->
|
|
||||||
<div id="sg-code-modal" class="ds-modal" style="display:none;">
|
|
||||||
<div class="ds-modal-content">
|
|
||||||
<div class="ds-modal-header">
|
|
||||||
<h3>Aus Struktogramm generierter Python-Code (PEP 8)</h3>
|
|
||||||
<button type="button" class="ds-modal-close" id="sg-code-modal-close">×</button>
|
|
||||||
</div>
|
|
||||||
<pre id="sg-code-display" class="ds-code-block"></pre>
|
|
||||||
<div class="ds-modal-footer">
|
|
||||||
<button type="button" class="ds-btn ds-btn-primary" id="sg-code-copy-btn">In Zwischenablage kopieren</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
this.bindEvents();
|
|
||||||
this.loadTemplate('ean13');
|
|
||||||
}
|
|
||||||
|
|
||||||
bindEvents() {
|
|
||||||
document.getElementById('sg-btn-add-inst').addEventListener('click', () => this.addBlock({ type: 'instruction', text: 'neue_variable = 0' }));
|
|
||||||
document.getElementById('sg-btn-add-if').addEventListener('click', () => this.addBlock({
|
|
||||||
type: 'if',
|
|
||||||
condition: 'wert > 100',
|
|
||||||
thenBlocks: [{ id: 'b_' + Date.now() + '_1', type: 'instruction', text: 'ausgabe("Bedingung erfüllt")' }],
|
|
||||||
elseBlocks: [{ id: 'b_' + Date.now() + '_2', type: 'instruction', text: 'ausgabe("Bedingung nicht erfüllt")' }]
|
|
||||||
}));
|
|
||||||
document.getElementById('sg-btn-add-while').addEventListener('click', () => this.addBlock({
|
|
||||||
type: 'while',
|
|
||||||
condition: 'zaehler < 10',
|
|
||||||
body: [{ id: 'b_' + Date.now() + '_3', type: 'instruction', text: 'zaehler = zaehler + 1' }]
|
|
||||||
}));
|
|
||||||
document.getElementById('sg-btn-add-for').addEventListener('click', () => this.addBlock({
|
|
||||||
type: 'for',
|
|
||||||
loopHeader: 'Für i von 1 bis n Schrittweite 1',
|
|
||||||
body: [{ id: 'b_' + Date.now() + '_4', type: 'instruction', text: 'summe = summe + i' }]
|
|
||||||
}));
|
|
||||||
|
|
||||||
document.getElementById('sg-select-template').addEventListener('change', (e) => {
|
|
||||||
if (e.target.value) this.loadTemplate(e.target.value);
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('sg-btn-generate-code').addEventListener('click', () => this.showCodeModal());
|
|
||||||
document.getElementById('sg-code-modal-close').addEventListener('click', () => {
|
|
||||||
document.getElementById('sg-code-modal').style.display = 'none';
|
|
||||||
});
|
|
||||||
document.getElementById('sg-code-copy-btn').addEventListener('click', () => {
|
|
||||||
const code = document.getElementById('sg-code-display').innerText;
|
|
||||||
navigator.clipboard.writeText(code).then(() => alert('Python-Code erfolgreich kopiert!'));
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('sg-btn-export-svg').addEventListener('click', () => this.exportSvg());
|
|
||||||
document.getElementById('sg-btn-clear').addEventListener('click', () => {
|
|
||||||
if (confirm('Möchtest du das gesamte Struktogramm leeren?')) {
|
|
||||||
this.blocks = [];
|
|
||||||
this.selectedBlockId = null;
|
|
||||||
this.render();
|
|
||||||
this.renderSidebar();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
addBlock(blockData) {
|
|
||||||
blockData.id = 'blk_' + Date.now() + '_' + Math.floor(Math.random() * 1000);
|
|
||||||
this.blocks.push(blockData);
|
|
||||||
this.selectedBlockId = blockData.id;
|
|
||||||
this.render();
|
|
||||||
this.renderSidebar();
|
|
||||||
}
|
|
||||||
|
|
||||||
loadTemplate(tplName) {
|
|
||||||
this.blocks = [];
|
|
||||||
|
|
||||||
if (tplName === 'ean13') {
|
|
||||||
this.blocks = [
|
|
||||||
{ id: 'b1', type: 'instruction', text: 'Eingabe: ean_12_stellen (String mit 12 Ziffern)' },
|
|
||||||
{ id: 'b2', type: 'instruction', text: 'summe = 0' },
|
|
||||||
{
|
|
||||||
id: 'b3',
|
|
||||||
type: 'for',
|
|
||||||
loopHeader: 'Für index von 0 bis 11 Schrittweite 1',
|
|
||||||
body: [
|
|
||||||
{ id: 'b3_1', type: 'instruction', text: 'ziffer = int(ean_12_stellen[index])' },
|
|
||||||
{
|
|
||||||
id: 'b3_2',
|
|
||||||
type: 'if',
|
|
||||||
condition: 'index % 2 == 1',
|
|
||||||
thenBlocks: [{ id: 'b3_2_1', type: 'instruction', text: 'summe = summe + (ziffer * 3)' }],
|
|
||||||
elseBlocks: [{ id: 'b3_2_2', type: 'instruction', text: 'summe = summe + (ziffer * 1)' }]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{ id: 'b4', type: 'instruction', text: 'rest = summe % 10' },
|
|
||||||
{
|
|
||||||
id: 'b5',
|
|
||||||
type: 'if',
|
|
||||||
condition: 'rest == 0',
|
|
||||||
thenBlocks: [{ id: 'b5_1', type: 'instruction', text: 'pruefziffer = 0' }],
|
|
||||||
elseBlocks: [{ id: 'b5_2', type: 'instruction', text: 'pruefziffer = 10 - rest' }]
|
|
||||||
},
|
|
||||||
{ id: 'b6', type: 'instruction', text: 'Ausgabe: pruefziffer' }
|
|
||||||
];
|
|
||||||
} else if (tplName === 'primzahl') {
|
|
||||||
this.blocks = [
|
|
||||||
{ id: 'p1', type: 'instruction', text: 'Eingabe: n (ganze Zahl >= 2)' },
|
|
||||||
{ id: 'p2', type: 'instruction', text: 'ist_prim = True; teiler = 2' },
|
|
||||||
{
|
|
||||||
id: 'p3',
|
|
||||||
type: 'while',
|
|
||||||
condition: 'teiler * teiler <= n und ist_prim == True',
|
|
||||||
body: [
|
|
||||||
{
|
|
||||||
id: 'p3_1',
|
|
||||||
type: 'if',
|
|
||||||
condition: 'n % teiler == 0',
|
|
||||||
thenBlocks: [{ id: 'p3_1_1', type: 'instruction', text: 'ist_prim = False' }],
|
|
||||||
elseBlocks: [{ id: 'p3_1_2', type: 'instruction', text: 'teiler = teiler + 1' }]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'p4',
|
|
||||||
type: 'if',
|
|
||||||
condition: 'ist_prim == True',
|
|
||||||
thenBlocks: [{ id: 'p4_1', type: 'instruction', text: 'Ausgabe("Zahl ist eine Primzahl")' }],
|
|
||||||
elseBlocks: [{ id: 'p4_2', type: 'instruction', text: 'Ausgabe("Zahl ist KEINE Primzahl")' }]
|
|
||||||
}
|
|
||||||
];
|
|
||||||
} else if (tplName === 'rabatt') {
|
|
||||||
this.blocks = [
|
|
||||||
{ id: 'r1', type: 'instruction', text: 'Eingabe: bestellwert, ist_stammkunde (Boolean)' },
|
|
||||||
{ id: 'r2', type: 'instruction', text: 'rabatt_prozent = 0.0' },
|
|
||||||
{
|
|
||||||
id: 'r3',
|
|
||||||
type: 'if',
|
|
||||||
condition: 'bestellwert >= 500.0',
|
|
||||||
thenBlocks: [
|
|
||||||
{ id: 'r3_1', type: 'instruction', text: 'rabatt_prozent = 10.0' }
|
|
||||||
],
|
|
||||||
elseBlocks: [
|
|
||||||
{
|
|
||||||
id: 'r3_2',
|
|
||||||
type: 'if',
|
|
||||||
condition: 'bestellwert >= 200.0',
|
|
||||||
thenBlocks: [{ id: 'r3_2_1', type: 'instruction', text: 'rabatt_prozent = 5.0' }],
|
|
||||||
elseBlocks: [{ id: 'r3_2_2', type: 'instruction', text: 'rabatt_prozent = 0.0' }]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'r4',
|
|
||||||
type: 'if',
|
|
||||||
condition: 'ist_stammkunde == True',
|
|
||||||
thenBlocks: [{ id: 'r4_1', type: 'instruction', text: 'rabatt_prozent = rabatt_prozent + 3.0' }],
|
|
||||||
elseBlocks: []
|
|
||||||
},
|
|
||||||
{ id: 'r5', type: 'instruction', text: 'endpreis = bestellwert * (1.0 - rabatt_prozent / 100.0)' },
|
|
||||||
{ id: 'r6', type: 'instruction', text: 'Ausgabe: endpreis' }
|
|
||||||
];
|
|
||||||
} else if (tplName === 'binaersuche') {
|
|
||||||
this.blocks = [
|
|
||||||
{ id: 's1', type: 'instruction', text: 'Eingabe: arr (sortiertes Array), suchwert' },
|
|
||||||
{ id: 's2', type: 'instruction', text: 'links = 0; rechts = len(arr) - 1; gefundener_index = -1' },
|
|
||||||
{
|
|
||||||
id: 's3',
|
|
||||||
type: 'while',
|
|
||||||
condition: 'links <= rechts und gefundener_index == -1',
|
|
||||||
body: [
|
|
||||||
{ id: 's3_1', type: 'instruction', text: 'mitte = (links + rechts) // 2' },
|
|
||||||
{
|
|
||||||
id: 's3_2',
|
|
||||||
type: 'if',
|
|
||||||
condition: 'arr[mitte] == suchwert',
|
|
||||||
thenBlocks: [{ id: 's3_2_1', type: 'instruction', text: 'gefundener_index = mitte' }],
|
|
||||||
elseBlocks: [
|
|
||||||
{
|
|
||||||
id: 's3_2_2',
|
|
||||||
type: 'if',
|
|
||||||
condition: 'arr[mitte] < suchwert',
|
|
||||||
thenBlocks: [{ id: 's3_2_2_1', type: 'instruction', text: 'links = mitte + 1' }],
|
|
||||||
elseBlocks: [{ id: 's3_2_2_2', type: 'instruction', text: 'rechts = mitte - 1' }]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{ id: 's4', type: 'instruction', text: 'Ausgabe: gefundener_index' }
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
this.selectedBlockId = this.blocks[0] ? this.blocks[0].id : null;
|
|
||||||
this.render();
|
|
||||||
this.renderSidebar();
|
|
||||||
}
|
|
||||||
|
|
||||||
findBlockRecursive(blocks, id) {
|
|
||||||
for (const b of blocks) {
|
|
||||||
if (b.id === id) return b;
|
|
||||||
if (b.thenBlocks) {
|
|
||||||
const found = this.findBlockRecursive(b.thenBlocks, id);
|
|
||||||
if (found) return found;
|
|
||||||
}
|
|
||||||
if (b.elseBlocks) {
|
|
||||||
const found = this.findBlockRecursive(b.elseBlocks, id);
|
|
||||||
if (found) return found;
|
|
||||||
}
|
|
||||||
if (b.body) {
|
|
||||||
const found = this.findBlockRecursive(b.body, id);
|
|
||||||
if (found) return found;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteBlockRecursive(blocks, id) {
|
|
||||||
for (let i = 0; i < blocks.length; i++) {
|
|
||||||
if (blocks[i].id === id) {
|
|
||||||
blocks.splice(i, 1);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (blocks[i].thenBlocks && this.deleteBlockRecursive(blocks[i].thenBlocks, id)) return true;
|
|
||||||
if (blocks[i].elseBlocks && this.deleteBlockRecursive(blocks[i].elseBlocks, id)) return true;
|
|
||||||
if (blocks[i].body && this.deleteBlockRecursive(blocks[i].body, id)) return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const box = document.getElementById('sg-render-box');
|
|
||||||
if (!box) return;
|
|
||||||
|
|
||||||
// Render HTML table-based DIN 66261 diagram
|
|
||||||
let html = `
|
|
||||||
<div class="sg-container">
|
|
||||||
<div class="sg-header-title">DIN 66261 Nassi-Shneiderman Struktogramm</div>
|
|
||||||
<div class="sg-root-table">
|
|
||||||
${this.renderBlockList(this.blocks)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
box.innerHTML = html;
|
|
||||||
this.attachClickEvents();
|
|
||||||
}
|
|
||||||
|
|
||||||
renderBlockList(blocks) {
|
|
||||||
if (!blocks || blocks.length === 0) {
|
|
||||||
return `<div class="sg-empty-block">∅ (leer)</div>`;
|
|
||||||
}
|
|
||||||
return blocks.map(b => this.renderSingleBlock(b)).join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
renderSingleBlock(b) {
|
|
||||||
const isSelected = b.id === this.selectedBlockId ? 'sg-selected' : '';
|
|
||||||
|
|
||||||
if (b.type === 'instruction') {
|
|
||||||
return `
|
|
||||||
<div class="sg-block sg-instruction ${isSelected}" data-block-id="${b.id}">
|
|
||||||
<span class="sg-text">${this.escapeHtml(b.text || '')}</span>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
} else if (b.type === 'if') {
|
|
||||||
return `
|
|
||||||
<div class="sg-block sg-if ${isSelected}" data-block-id="${b.id}">
|
|
||||||
<div class="sg-if-header">
|
|
||||||
<svg class="sg-if-svg" viewBox="0 0 100 40" preserveAspectRatio="none">
|
|
||||||
<polygon points="0,0 100,0 50,40" fill="#0f172a" stroke="#334155" stroke-width="1.5"/>
|
|
||||||
<line x1="0" y1="0" x2="50" y2="40" stroke="#38bdf8" stroke-width="1.5"/>
|
|
||||||
<line x1="100" y1="0" x2="50" y2="40" stroke="#38bdf8" stroke-width="1.5"/>
|
|
||||||
</svg>
|
|
||||||
<div class="sg-if-condition">${this.escapeHtml(b.condition || '')}</div>
|
|
||||||
<div class="sg-if-label-left">Ja (Wahr)</div>
|
|
||||||
<div class="sg-if-label-right">Nein (Falsch)</div>
|
|
||||||
</div>
|
|
||||||
<div class="sg-if-branches">
|
|
||||||
<div class="sg-if-then">
|
|
||||||
${this.renderBlockList(b.thenBlocks || [])}
|
|
||||||
</div>
|
|
||||||
<div class="sg-if-else">
|
|
||||||
${this.renderBlockList(b.elseBlocks || [])}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
} else if (b.type === 'while') {
|
|
||||||
return `
|
|
||||||
<div class="sg-block sg-while ${isSelected}" data-block-id="${b.id}">
|
|
||||||
<div class="sg-while-header">
|
|
||||||
<span class="sg-badge-loop">WHILE (solange):</span> ${this.escapeHtml(b.condition || '')}
|
|
||||||
</div>
|
|
||||||
<div class="sg-while-body-wrap">
|
|
||||||
<div class="sg-while-gutter"></div>
|
|
||||||
<div class="sg-while-body">
|
|
||||||
${this.renderBlockList(b.body || [])}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
} else if (b.type === 'for') {
|
|
||||||
return `
|
|
||||||
<div class="sg-block sg-for ${isSelected}" data-block-id="${b.id}">
|
|
||||||
<div class="sg-for-header">
|
|
||||||
<span class="sg-badge-loop">FOR (Zählschleife):</span> ${this.escapeHtml(b.loopHeader || '')}
|
|
||||||
</div>
|
|
||||||
<div class="sg-while-body-wrap">
|
|
||||||
<div class="sg-while-gutter"></div>
|
|
||||||
<div class="sg-while-body">
|
|
||||||
${this.renderBlockList(b.body || [])}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
attachClickEvents() {
|
|
||||||
const elBlocks = document.querySelectorAll('.sg-block');
|
|
||||||
elBlocks.forEach(el => {
|
|
||||||
el.addEventListener('click', (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
const id = el.getAttribute('data-block-id');
|
|
||||||
this.selectedBlockId = id;
|
|
||||||
this.render();
|
|
||||||
this.renderSidebar();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
renderSidebar() {
|
|
||||||
const sidebarTitle = document.getElementById('sg-sidebar-title');
|
|
||||||
const content = document.getElementById('sg-sidebar-content');
|
|
||||||
if (!content) return;
|
|
||||||
|
|
||||||
const block = this.findBlockRecursive(this.blocks, this.selectedBlockId);
|
|
||||||
if (!block) {
|
|
||||||
sidebarTitle.innerText = 'Block-Eigenschaften';
|
|
||||||
content.innerHTML = `<p class="ds-hint">Klicke auf einen Block im Struktogramm, um dessen Text oder Verzweigung anzupassen.</p>`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
sidebarTitle.innerText = `Typ: ${block.type.toUpperCase()}`;
|
|
||||||
|
|
||||||
let html = '';
|
|
||||||
if (block.type === 'instruction') {
|
|
||||||
html = `
|
|
||||||
<div class="ds-field-group">
|
|
||||||
<label>Anweisungstext:</label>
|
|
||||||
<textarea id="sg-input-text" class="ds-textarea" rows="3">${this.escapeHtml(block.text || '')}</textarea>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
} else if (block.type === 'if') {
|
|
||||||
html = `
|
|
||||||
<div class="ds-field-group">
|
|
||||||
<label>Bedingung (logischer Ausdruck):</label>
|
|
||||||
<input type="text" id="sg-input-cond" class="ds-input" value="${this.escapeHtml(block.condition || '')}">
|
|
||||||
</div>
|
|
||||||
<div style="display:flex; gap:8px; margin-top:10px;">
|
|
||||||
<button type="button" class="ds-btn ds-btn-secondary" id="sg-btn-add-then" style="flex:1; font-size:0.75rem;">+ Ja-Zweig</button>
|
|
||||||
<button type="button" class="ds-btn ds-btn-secondary" id="sg-btn-add-else" style="flex:1; font-size:0.75rem;">+ Nein-Zweig</button>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
} else if (block.type === 'while') {
|
|
||||||
html = `
|
|
||||||
<div class="ds-field-group">
|
|
||||||
<label>Schleifenbedingung (Abbruch wenn falsch):</label>
|
|
||||||
<input type="text" id="sg-input-while-cond" class="ds-input" value="${this.escapeHtml(block.condition || '')}">
|
|
||||||
</div>
|
|
||||||
<button type="button" class="ds-btn ds-btn-secondary" id="sg-btn-add-while-body" style="width:100%; margin-top:8px; font-size:0.75rem;">+ Block in Schleifenkörper</button>
|
|
||||||
`;
|
|
||||||
} else if (block.type === 'for') {
|
|
||||||
html = `
|
|
||||||
<div class="ds-field-group">
|
|
||||||
<label>Zählschleifen-Kopf:</label>
|
|
||||||
<input type="text" id="sg-input-for-hdr" class="ds-input" value="${this.escapeHtml(block.loopHeader || '')}">
|
|
||||||
</div>
|
|
||||||
<button type="button" class="ds-btn ds-btn-secondary" id="sg-btn-add-for-body" style="width:100%; margin-top:8px; font-size:0.75rem;">+ Block in Schleifenkörper</button>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
html += `
|
|
||||||
<div style="margin-top:20px; border-top:1px solid #334155; padding-top:14px;">
|
|
||||||
<button type="button" id="sg-btn-delete-block" class="ds-btn ds-btn-danger" style="width:100%; font-size:0.82rem;">
|
|
||||||
🗑️ Diesen Block entfernen
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
content.innerHTML = html;
|
|
||||||
|
|
||||||
// Listeners
|
|
||||||
if (block.type === 'instruction') {
|
|
||||||
document.getElementById('sg-input-text').addEventListener('input', (e) => {
|
|
||||||
block.text = e.target.value;
|
|
||||||
this.render();
|
|
||||||
});
|
|
||||||
} else if (block.type === 'if') {
|
|
||||||
document.getElementById('sg-input-cond').addEventListener('input', (e) => {
|
|
||||||
block.condition = e.target.value;
|
|
||||||
this.render();
|
|
||||||
});
|
|
||||||
document.getElementById('sg-btn-add-then').addEventListener('click', () => {
|
|
||||||
if (!block.thenBlocks) block.thenBlocks = [];
|
|
||||||
block.thenBlocks.push({ id: 'b_' + Date.now(), type: 'instruction', text: 'neue_anweisung = 1' });
|
|
||||||
this.render();
|
|
||||||
});
|
|
||||||
document.getElementById('sg-btn-add-else').addEventListener('click', () => {
|
|
||||||
if (!block.elseBlocks) block.elseBlocks = [];
|
|
||||||
block.elseBlocks.push({ id: 'b_' + Date.now(), type: 'instruction', text: 'neue_anweisung = 2' });
|
|
||||||
this.render();
|
|
||||||
});
|
|
||||||
} else if (block.type === 'while') {
|
|
||||||
document.getElementById('sg-input-while-cond').addEventListener('input', (e) => {
|
|
||||||
block.condition = e.target.value;
|
|
||||||
this.render();
|
|
||||||
});
|
|
||||||
document.getElementById('sg-btn-add-while-body').addEventListener('click', () => {
|
|
||||||
if (!block.body) block.body = [];
|
|
||||||
block.body.push({ id: 'b_' + Date.now(), type: 'instruction', text: 'schleifen_anweisung()' });
|
|
||||||
this.render();
|
|
||||||
});
|
|
||||||
} else if (block.type === 'for') {
|
|
||||||
document.getElementById('sg-input-for-hdr').addEventListener('input', (e) => {
|
|
||||||
block.loopHeader = e.target.value;
|
|
||||||
this.render();
|
|
||||||
});
|
|
||||||
document.getElementById('sg-btn-add-for-body').addEventListener('click', () => {
|
|
||||||
if (!block.body) block.body = [];
|
|
||||||
block.body.push({ id: 'b_' + Date.now(), type: 'instruction', text: 'schleifen_anweisung()' });
|
|
||||||
this.render();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById('sg-btn-delete-block').addEventListener('click', () => {
|
|
||||||
if (confirm('Diesen Block und alle Unterblöcke wirklich löschen?')) {
|
|
||||||
this.deleteBlockRecursive(this.blocks, block.id);
|
|
||||||
this.selectedBlockId = this.blocks[0] ? this.blocks[0].id : null;
|
|
||||||
this.render();
|
|
||||||
this.renderSidebar();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
generatePythonCode() {
|
|
||||||
let py = `# ========================================================\n`;
|
|
||||||
py += `# DIN 66261 Struktogramm -> Python 3 Transpiler\n`;
|
|
||||||
py += `# Generiert vom IT-Praxisportal Diagramm-Studio\n`;
|
|
||||||
py += `# ========================================================\n\n`;
|
|
||||||
py += `def algorithmus_ausfuehren():\n`;
|
|
||||||
|
|
||||||
const transpileList = (list, indent) => {
|
|
||||||
let code = '';
|
|
||||||
if (!list || list.length === 0) {
|
|
||||||
return `${indent}pass\n`;
|
|
||||||
}
|
|
||||||
list.forEach(b => {
|
|
||||||
if (b.type === 'instruction') {
|
|
||||||
code += `${indent}${b.text}\n`;
|
|
||||||
} else if (b.type === 'if') {
|
|
||||||
code += `${indent}if ${b.condition}:\n`;
|
|
||||||
code += transpileList(b.thenBlocks, indent + ' ');
|
|
||||||
if (b.elseBlocks && b.elseBlocks.length > 0) {
|
|
||||||
code += `${indent}else:\n`;
|
|
||||||
code += transpileList(b.elseBlocks, indent + ' ');
|
|
||||||
}
|
|
||||||
} else if (b.type === 'while') {
|
|
||||||
code += `${indent}while ${b.condition}:\n`;
|
|
||||||
code += transpileList(b.body, indent + ' ');
|
|
||||||
} else if (b.type === 'for') {
|
|
||||||
code += `${indent}# ${b.loopHeader}\n`;
|
|
||||||
code += `${indent}for i in range(1, n + 1):\n`;
|
|
||||||
code += transpileList(b.body, indent + ' ');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return code;
|
|
||||||
};
|
|
||||||
|
|
||||||
py += transpileList(this.blocks, ' ');
|
|
||||||
py += `\n\nif __name__ == "__main__":\n print("Algorithmus gestartet...")\n algorithmus_ausfuehren()\n`;
|
|
||||||
return py;
|
|
||||||
}
|
|
||||||
|
|
||||||
showCodeModal() {
|
|
||||||
const py = this.generatePythonCode();
|
|
||||||
document.getElementById('sg-code-display').innerText = py;
|
|
||||||
document.getElementById('sg-code-modal').style.display = 'flex';
|
|
||||||
}
|
|
||||||
|
|
||||||
exportSvg() {
|
|
||||||
// Generate SVG wrapper around the table
|
|
||||||
const box = document.getElementById('sg-render-box');
|
|
||||||
if (!box) return;
|
|
||||||
const htmlContent = box.innerHTML;
|
|
||||||
const svgString = `
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="800" height="900">
|
|
||||||
<foreignObject width="100%" height="100%">
|
|
||||||
<div xmlns="http://www.w3.org/1999/xhtml">
|
|
||||||
<style>
|
|
||||||
.sg-container { font-family: sans-serif; background: #090d16; color: #f8fafc; padding: 20px; }
|
|
||||||
.sg-block { border: 1px solid #38bdf8; background: #1e293b; padding: 8px 12px; margin: -1px 0 0 0; }
|
|
||||||
.sg-text { font-family: monospace; font-size: 13px; color: #f8fafc; }
|
|
||||||
.sg-if-header { position: relative; background: #0f172a; height: 36px; border: 1px solid #38bdf8; }
|
|
||||||
.sg-if-condition { text-align: center; color: #38bdf8; font-weight: bold; padding-top: 6px; }
|
|
||||||
.sg-if-branches { display: flex; width: 100%; border: 1px solid #38bdf8; border-top: none; }
|
|
||||||
.sg-if-then { flex: 1; border-right: 1px solid #38bdf8; }
|
|
||||||
.sg-if-else { flex: 1; }
|
|
||||||
.sg-while { border: 1px solid #38bdf8; background: #1e293b; }
|
|
||||||
.sg-while-header { background: #0f172a; padding: 6px 12px; border-bottom: 1px solid #38bdf8; color: #38bdf8; font-weight: bold; }
|
|
||||||
.sg-while-body-wrap { display: flex; }
|
|
||||||
.sg-while-gutter { width: 24px; background: #0f172a; border-right: 1px solid #38bdf8; }
|
|
||||||
.sg-while-body { flex: 1; }
|
|
||||||
</style>
|
|
||||||
${htmlContent}
|
|
||||||
</div>
|
|
||||||
</foreignObject>
|
|
||||||
</svg>
|
|
||||||
`;
|
|
||||||
const blob = new Blob([svgString], { type: 'image/svg+xml;charset=utf-8' });
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
const a = document.createElement('a');
|
|
||||||
a.href = url;
|
|
||||||
a.download = 'struktogramm_din66261.svg';
|
|
||||||
a.click();
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
escapeHtml(str) {
|
|
||||||
return (str || '')
|
|
||||||
.replace(/&/g, '&')
|
|
||||||
.replace(/</g, '<')
|
|
||||||
.replace(/>/g, '>')
|
|
||||||
.replace(/"/g, '"');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.StruktogrammEditor = StruktogrammEditor;
|
|
||||||
|
|
@ -1,346 +0,0 @@
|
||||||
/**
|
|
||||||
* 🔷 IT-PRAXISPORTAL – UML DIAGRAMM STUDIO (KLASSEN, SEQUENZ, USE CASE)
|
|
||||||
* Standard für IHK FIAE & FISI:
|
|
||||||
* - Klassendiagramme (Attribute +, -, #, Methoden, Vererbung, Assoziation, Komposition)
|
|
||||||
* - Sequenzdiagramme (Lebenslinien, synchrone/asynchrone Aufrufe, Return)
|
|
||||||
* - Use Case Diagramme (Akteure, Systemgrenze, <<include>>, <<extend>>)
|
|
||||||
* Synchronisierte Mermaid-Live-Vorschau mit interaktiver Generierung!
|
|
||||||
*/
|
|
||||||
|
|
||||||
class UMLEditor {
|
|
||||||
constructor(containerId, options = {}) {
|
|
||||||
this.container = typeof containerId === 'string' ? document.getElementById(containerId) : containerId;
|
|
||||||
this.options = options;
|
|
||||||
this.diagramType = 'class'; // 'class' | 'sequence' | 'usecase'
|
|
||||||
this.mermaidCode = '';
|
|
||||||
this.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
|
||||||
if (!this.container) return;
|
|
||||||
this.container.innerHTML = `
|
|
||||||
<div class="ds-editor-card">
|
|
||||||
<div class="ds-toolbar">
|
|
||||||
<div class="ds-toolbar-left">
|
|
||||||
<div class="ds-select-wrap">
|
|
||||||
<label>Diagrammtyp:</label>
|
|
||||||
<select id="uml-select-type" class="ds-select">
|
|
||||||
<option value="class">UML 2.5 Klassendiagramm</option>
|
|
||||||
<option value="sequence">UML 2.5 Sequenzdiagramm</option>
|
|
||||||
<option value="usecase">UML 2.5 Anwendungsfalldiagramm (Use Case)</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="ds-select-wrap">
|
|
||||||
<label>IHK-Vorlage:</label>
|
|
||||||
<select id="uml-select-template" class="ds-select">
|
|
||||||
<option value="">-- Vorlage laden --</option>
|
|
||||||
<option value="class_ecommerce">IHK Klassendiagramm: E-Commerce & Zahlung</option>
|
|
||||||
<option value="class_ticket">IHK Klassendiagramm: IT-Helpdesk & Rollen</option>
|
|
||||||
<option value="seq_login">IHK Sequenz: 2FA Benutzer-Authentifizierung</option>
|
|
||||||
<option value="seq_order">IHK Sequenz: Warenkorb-Bestellprozess</option>
|
|
||||||
<option value="uc_hotel">IHK Use-Case: Hotel-Reservierungssystem</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="ds-toolbar-right">
|
|
||||||
<button type="button" class="ds-btn ds-btn-success" id="uml-btn-render">
|
|
||||||
<span>▶</span> <span>Neu Rendern</span>
|
|
||||||
</button>
|
|
||||||
<button type="button" class="ds-btn ds-btn-secondary" id="uml-btn-export-svg">
|
|
||||||
<span>🖼️</span> <span>SVG Export</span>
|
|
||||||
</button>
|
|
||||||
<button type="button" class="ds-btn ds-btn-danger" id="uml-btn-clear" title="Code leeren">
|
|
||||||
<span>🗑️</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="ds-workspace-grid">
|
|
||||||
<div class="ds-canvas-wrap" style="display:flex; align-items:center; justify-content:center; overflow:auto; background:#090d16;" id="uml-canvas-container">
|
|
||||||
<div id="uml-render-target" style="width:100%; min-height:100%; display:flex; align-items:center; justify-content:center; padding:24px;"></div>
|
|
||||||
</div>
|
|
||||||
<div class="ds-sidebar" style="width:340px;">
|
|
||||||
<div class="ds-sidebar-header">
|
|
||||||
<h4>UML Quellcode (Mermaid / PlantUML Standard)</h4>
|
|
||||||
</div>
|
|
||||||
<div class="ds-sidebar-body" style="padding:10px; display:flex; flex-direction:column; height:calc(100% - 46px);">
|
|
||||||
<textarea id="uml-code-input" class="ds-textarea" style="flex:1; font-family:monospace; font-size:0.84rem; line-height:1.45; resize:none; background:#060911; color:#f8fafc; border:1px solid #334155; padding:12px; border-radius:6px;"></textarea>
|
|
||||||
<div style="margin-top:10px; font-size:0.75rem; color:#94a3b8; line-height:1.4;">
|
|
||||||
💡 Tipp: Bearbeite den Diagrammcode direkt links in der Textbox oder lade eine IHK-Vorlage. Klicke auf <strong>Neu Rendern</strong> zur Aktualisierung.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
this.bindEvents();
|
|
||||||
this.loadTemplate('class_ecommerce');
|
|
||||||
}
|
|
||||||
|
|
||||||
bindEvents() {
|
|
||||||
document.getElementById('uml-select-type').addEventListener('change', (e) => {
|
|
||||||
this.diagramType = e.target.value;
|
|
||||||
if (this.diagramType === 'class') this.loadTemplate('class_ecommerce');
|
|
||||||
else if (this.diagramType === 'sequence') this.loadTemplate('seq_login');
|
|
||||||
else if (this.diagramType === 'usecase') this.loadTemplate('uc_hotel');
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('uml-select-template').addEventListener('change', (e) => {
|
|
||||||
if (e.target.value) this.loadTemplate(e.target.value);
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('uml-btn-render').addEventListener('click', () => {
|
|
||||||
this.mermaidCode = document.getElementById('uml-code-input').value;
|
|
||||||
this.render();
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('uml-btn-clear').addEventListener('click', () => {
|
|
||||||
if (confirm('Möchtest du das aktuelle UML-Diagramm leeren?')) {
|
|
||||||
document.getElementById('uml-code-input').value = 'classDiagram\n class MeineKlasse {\n +int id\n }';
|
|
||||||
this.mermaidCode = document.getElementById('uml-code-input').value;
|
|
||||||
this.render();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('uml-btn-export-svg').addEventListener('click', () => this.exportSvg());
|
|
||||||
}
|
|
||||||
|
|
||||||
loadTemplate(tpl) {
|
|
||||||
let code = '';
|
|
||||||
if (tpl === 'class_ecommerce') {
|
|
||||||
this.diagramType = 'class';
|
|
||||||
document.getElementById('uml-select-type').value = 'class';
|
|
||||||
code = `classDiagram
|
|
||||||
direction TB
|
|
||||||
|
|
||||||
class Benutzer {
|
|
||||||
<<abstract>>
|
|
||||||
#int id
|
|
||||||
#String email
|
|
||||||
#String passwortHash
|
|
||||||
+anmelden() boolean
|
|
||||||
+abmelden() void
|
|
||||||
}
|
|
||||||
|
|
||||||
class Kunde {
|
|
||||||
-String vorname
|
|
||||||
-String nachname
|
|
||||||
-String lieferadresse
|
|
||||||
+bestellen(Warenkorb w) Bestellung
|
|
||||||
}
|
|
||||||
|
|
||||||
class Administrator {
|
|
||||||
-int rechteStufe
|
|
||||||
+artikelAnlegen(Artikel a) void
|
|
||||||
+sperreBenutzer(int userId) void
|
|
||||||
}
|
|
||||||
|
|
||||||
class Bestellung {
|
|
||||||
-int bestellId
|
|
||||||
-Date datum
|
|
||||||
-String status
|
|
||||||
+berechneGesamtsumme() double
|
|
||||||
}
|
|
||||||
|
|
||||||
class Bestellposition {
|
|
||||||
-int menge
|
|
||||||
-double einzelpreis
|
|
||||||
}
|
|
||||||
|
|
||||||
class Artikel {
|
|
||||||
-int artikelId
|
|
||||||
-String bezeichnung
|
|
||||||
-double preis
|
|
||||||
-int bestand
|
|
||||||
}
|
|
||||||
|
|
||||||
class IPaymentMethod {
|
|
||||||
<<interface>>
|
|
||||||
+processPayment(double amount) boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
Benutzer <|-- Kunde : Generalisierung
|
|
||||||
Benutzer <|-- Administrator : Generalisierung
|
|
||||||
Kunde "1" --> "*" Bestellung : gibt auf
|
|
||||||
Bestellung "1" *-- "1..*" Bestellposition : Komposition
|
|
||||||
Bestellposition "*" --> "1" Artikel : Assoziation
|
|
||||||
Bestellung ..> IPaymentMethod : Realisierung`;
|
|
||||||
} else if (tpl === 'class_ticket') {
|
|
||||||
this.diagramType = 'class';
|
|
||||||
document.getElementById('uml-select-type').value = 'class';
|
|
||||||
code = `classDiagram
|
|
||||||
class Ticket {
|
|
||||||
-int ticketNummer
|
|
||||||
-String titel
|
|
||||||
-String prioritaet
|
|
||||||
-Date erstelltAm
|
|
||||||
+statusAendern(String status) void
|
|
||||||
+zuweisen(Mitarbeiter m) void
|
|
||||||
}
|
|
||||||
|
|
||||||
class Mitarbeiter {
|
|
||||||
-int mitarbeiterId
|
|
||||||
-String name
|
|
||||||
-String abteilung
|
|
||||||
}
|
|
||||||
|
|
||||||
class HardwareAsset {
|
|
||||||
-String seriennummer
|
|
||||||
-String hostname
|
|
||||||
-String standort
|
|
||||||
}
|
|
||||||
|
|
||||||
Ticket "*" --> "1" Mitarbeiter : Melder
|
|
||||||
Ticket "*" --> "0..1" Mitarbeiter : Bearbeiter
|
|
||||||
Ticket "*" --> "1" HardwareAsset : Betrifft`;
|
|
||||||
} else if (tpl === 'seq_login') {
|
|
||||||
this.diagramType = 'sequence';
|
|
||||||
document.getElementById('uml-select-type').value = 'sequence';
|
|
||||||
code = `sequenceDiagram
|
|
||||||
autonumber
|
|
||||||
actor User as Benutzer
|
|
||||||
participant Browser as Client / Browser
|
|
||||||
participant AuthAPI as Auth-Service
|
|
||||||
participant DB as Datenbank
|
|
||||||
participant Mail as 2FA-Mail-Service
|
|
||||||
|
|
||||||
User->>Browser: Login-Formular ausfüllen (Email, Passwort)
|
|
||||||
Browser->>AuthAPI: POST /api/v1/auth/login
|
|
||||||
activate AuthAPI
|
|
||||||
AuthAPI->>DB: SELECT * FROM users WHERE email = ?
|
|
||||||
activate DB
|
|
||||||
DB-->>AuthAPI: User-Record mit Password-Hash
|
|
||||||
deactivate DB
|
|
||||||
AuthAPI->>AuthAPI: Passwort-Hash validieren (Argon2id)
|
|
||||||
|
|
||||||
alt Passwort korrekt
|
|
||||||
AuthAPI->>Mail: 2FA-Einmalcode senden (SMS/Email)
|
|
||||||
AuthAPI-->>Browser: 200 OK (Requires 2FA)
|
|
||||||
User->>Browser: 2FA-Code eingeben
|
|
||||||
Browser->>AuthAPI: POST /api/v1/auth/verify-otp
|
|
||||||
AuthAPI-->>Browser: 200 OK (JWT Access Token & Cookie)
|
|
||||||
else Passwort falsch
|
|
||||||
AuthAPI-->>Browser: 401 Unauthorized (Ungültige Anmeldedaten)
|
|
||||||
end
|
|
||||||
deactivate AuthAPI`;
|
|
||||||
} else if (tpl === 'seq_order') {
|
|
||||||
this.diagramType = 'sequence';
|
|
||||||
document.getElementById('uml-select-type').value = 'sequence';
|
|
||||||
code = `sequenceDiagram
|
|
||||||
autonumber
|
|
||||||
actor Kunde
|
|
||||||
participant Webshop
|
|
||||||
participant Lager as ERP / Lager
|
|
||||||
participant Bank as Payment-Gateway
|
|
||||||
|
|
||||||
Kunde->>Webshop: "Jetzt kaufen" klicken
|
|
||||||
activate Webshop
|
|
||||||
Webshop->>Lager: Bestand prüfen & reservieren
|
|
||||||
activate Lager
|
|
||||||
Lager-->>Webshop: Bestand bestätigt
|
|
||||||
deactivate Lager
|
|
||||||
Webshop->>Bank: Kreditkarte autorisieren (129.90 EUR)
|
|
||||||
activate Bank
|
|
||||||
Bank-->>Webshop: Zahlung genehmigt (Auth-Token)
|
|
||||||
deactivate Bank
|
|
||||||
Webshop->>Lager: Kommissionierauftrag erstellen
|
|
||||||
Webshop-->>Kunde: Bestellbestätigung per E-Mail
|
|
||||||
deactivate Webshop`;
|
|
||||||
} else if (tpl === 'uc_hotel') {
|
|
||||||
this.diagramType = 'usecase';
|
|
||||||
document.getElementById('uml-select-type').value = 'usecase';
|
|
||||||
code = `flowchart LR
|
|
||||||
subgraph Hotelverwaltungssystem [Systemgrenze: Hotel-Reservierung]
|
|
||||||
UC1((Zimmer suchen))
|
|
||||||
UC2((Zimmer reservieren))
|
|
||||||
UC3((Zahlung durchführen))
|
|
||||||
UC4((Reservierung stornieren))
|
|
||||||
UC5((Zimmerrechnung erstellen))
|
|
||||||
end
|
|
||||||
|
|
||||||
Gast[Gast / Kunde] --> UC1
|
|
||||||
Gast --> UC2
|
|
||||||
Gast --> UC4
|
|
||||||
|
|
||||||
Rezeption[Rezeptionist] --> UC2
|
|
||||||
Rezeption --> UC4
|
|
||||||
Rezeption --> UC5
|
|
||||||
|
|
||||||
UC2 -.->|<<include>>| UC3
|
|
||||||
UC4 -.->|<<extend>>| UC5`;
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById('uml-code-input').value = code;
|
|
||||||
this.mermaidCode = code;
|
|
||||||
this.render();
|
|
||||||
}
|
|
||||||
|
|
||||||
async render() {
|
|
||||||
const target = document.getElementById('uml-render-target');
|
|
||||||
if (!target) return;
|
|
||||||
|
|
||||||
// Load mermaid if not already on window
|
|
||||||
if (!window.mermaid) {
|
|
||||||
target.innerHTML = `<div style="color:#94a3b8; font-size:0.9rem;">Lade Diagramm-Renderer...</div>`;
|
|
||||||
await this.loadMermaidScript();
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
window.mermaid.initialize({
|
|
||||||
startOnLoad: false,
|
|
||||||
theme: 'dark',
|
|
||||||
themeVariables: {
|
|
||||||
darkMode: true,
|
|
||||||
background: '#090d16',
|
|
||||||
primaryColor: '#1e293b',
|
|
||||||
primaryTextColor: '#f8fafc',
|
|
||||||
primaryBorderColor: '#38bdf8',
|
|
||||||
lineColor: '#38bdf8',
|
|
||||||
secondaryColor: '#0f172a',
|
|
||||||
tertiaryColor: '#1e1b4b'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const id = 'mermaid_svg_' + Date.now();
|
|
||||||
const { svg } = await window.mermaid.render(id, this.mermaidCode);
|
|
||||||
target.innerHTML = svg;
|
|
||||||
} catch (err) {
|
|
||||||
target.innerHTML = `
|
|
||||||
<div style="background:rgba(239,68,68,0.1); border:1px solid #ef4444; color:#fca5a5; padding:14px; border-radius:8px; font-size:0.85rem; max-width:600px;">
|
|
||||||
<strong>⚠️ Syntaxfehler im UML-Code:</strong>
|
|
||||||
<pre style="margin-top:8px; font-family:monospace; font-size:0.8rem; white-space:pre-wrap;">${err.message || err}</pre>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
loadMermaidScript() {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const script = document.createElement('script');
|
|
||||||
script.src = 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js';
|
|
||||||
script.onload = () => resolve();
|
|
||||||
script.onerror = () => reject(new Error('Mermaid CDN konnte nicht geladen werden.'));
|
|
||||||
document.head.appendChild(script);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
exportSvg() {
|
|
||||||
const svgEl = document.querySelector('#uml-render-target svg');
|
|
||||||
if (!svgEl) {
|
|
||||||
alert('Kein gerendertes Diagramm zum Exportieren vorhanden.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const serializer = new XMLSerializer();
|
|
||||||
const source = serializer.serializeToString(svgEl);
|
|
||||||
const blob = new Blob([source], { type: 'image/svg+xml;charset=utf-8' });
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
const a = document.createElement('a');
|
|
||||||
a.href = url;
|
|
||||||
a.download = `uml_${this.diagramType}_ihk.svg`;
|
|
||||||
a.click();
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.UMLEditor = UMLEditor;
|
|
||||||
|
|
@ -1,77 +0,0 @@
|
||||||
/**
|
|
||||||
* Interaktiver USV- & Scheinleistungs-Kalkulator für ITSE
|
|
||||||
* IHK ZPA Nord-West AP2
|
|
||||||
*/
|
|
||||||
(function() {
|
|
||||||
function renderUSVCalculator(containerId) {
|
|
||||||
const container = document.getElementById(containerId);
|
|
||||||
if (!container) return;
|
|
||||||
|
|
||||||
container.innerHTML = `
|
|
||||||
<div style="background: #0f172a; border: 1px solid #334155; border-radius: 8px; padding: 20px; color: #f8fafc;">
|
|
||||||
<h4 style="color: #38bdf8; margin-top: 0; margin-bottom: 12px;">⚡ Interaktiver USV-Dimensionierungs-Rechner (ITSE)</h4>
|
|
||||||
<p style="color: #94a3b8; font-size: 0.9rem; margin-bottom: 16px;">
|
|
||||||
Berechnen Sie die Wirkleistung, Scheinleistung und empfohlene USV-Nennleistung nach IHK-Prüfungsformel:
|
|
||||||
</p>
|
|
||||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 14px; margin-bottom: 20px;">
|
|
||||||
<div style="background: #1e293b; padding: 12px; border-radius: 6px;">
|
|
||||||
<label style="font-size: 0.85rem; color: #38bdf8; display: block; font-weight: 600;">Wirkleistung Verbraucher P (W)</label>
|
|
||||||
<input type="number" id="usv_p" value="2640" min="10" step="50" class="ihk-input" style="width: 100%; margin-top: 6px;" />
|
|
||||||
</div>
|
|
||||||
<div style="background: #1e293b; padding: 12px; border-radius: 6px;">
|
|
||||||
<label style="font-size: 0.85rem; color: #fcd34d; display: block; font-weight: 600;">Leistungsfaktor cos φ</label>
|
|
||||||
<input type="number" id="usv_cos" value="0.9" min="0.5" max="1.0" step="0.05" class="ihk-input" style="width: 100%; margin-top: 6px;" />
|
|
||||||
</div>
|
|
||||||
<div style="background: #1e293b; padding: 12px; border-radius: 6px;">
|
|
||||||
<label style="font-size: 0.85rem; color: #4ade80; display: block; font-weight: 600;">Sicherheitsreserve (%)</label>
|
|
||||||
<input type="number" id="usv_reserve" value="25" min="0" max="100" step="5" class="ihk-input" style="width: 100%; margin-top: 6px;" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px; background: #1e293b; padding: 16px; border-radius: 6px;">
|
|
||||||
<div>
|
|
||||||
<div style="color: #94a3b8; font-size: 0.8rem;">Wirkleistung (P)</div>
|
|
||||||
<div id="res_usv_p" style="font-size: 1.25rem; font-weight: bold; color: #38bdf8;">-- kW</div>
|
|
||||||
<div style="font-size: 0.75rem; color: #64748b;">Thermische Last</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div style="color: #94a3b8; font-size: 0.8rem;">Scheinleistung (S = P / cos φ)</div>
|
|
||||||
<div id="res_usv_s" style="font-size: 1.25rem; font-weight: bold; color: #fcd34d;">-- kVA</div>
|
|
||||||
<div style="font-size: 0.75rem; color: #64748b;">Elektrische Auslegung</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div style="color: #94a3b8; font-size: 0.8rem;">Empf. USV-Nennleistung</div>
|
|
||||||
<div id="res_usv_rec" style="font-size: 1.25rem; font-weight: bold; color: #4ade80;">-- kVA</div>
|
|
||||||
<div style="font-size: 0.75rem; color: #64748b;">Inkl. Sicherheitszuschlag</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
function update() {
|
|
||||||
const p = parseFloat(document.getElementById('usv_p').value) || 0;
|
|
||||||
const cos = parseFloat(document.getElementById('usv_cos').value) || 0.9;
|
|
||||||
const reserve = parseFloat(document.getElementById('usv_reserve').value) || 20;
|
|
||||||
|
|
||||||
const s = cos > 0 ? (p / cos) : 0;
|
|
||||||
const rec = s * (1 + reserve / 100);
|
|
||||||
|
|
||||||
document.getElementById('res_usv_p').textContent = (p / 1000).toFixed(2) + ' kW';
|
|
||||||
document.getElementById('res_usv_s').textContent = (s / 1000).toFixed(2) + ' kVA';
|
|
||||||
document.getElementById('res_usv_rec').textContent = (rec / 1000).toFixed(2) + ' kVA';
|
|
||||||
}
|
|
||||||
|
|
||||||
['usv_p', 'usv_cos', 'usv_reserve'].forEach(id => {
|
|
||||||
document.getElementById(id).addEventListener('input', update);
|
|
||||||
});
|
|
||||||
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
|
|
||||||
window.renderUSVCalculator = renderUSVCalculator;
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
if (document.getElementById('usv-calculator-app')) {
|
|
||||||
renderUSVCalculator('usv-calculator-app');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
|
|
@ -137,7 +137,7 @@
|
||||||
color: "#d97706",
|
color: "#d97706",
|
||||||
badgeClass: "badge-itse",
|
badgeClass: "badge-itse",
|
||||||
description: "Planung, Errichtung und Instandhaltung elektrotechnischer Schutz- und Stromversorgungssysteme sowie optischer Leitungsnetze.",
|
description: "Planung, Errichtung und Instandhaltung elektrotechnischer Schutz- und Stromversorgungssysteme sowie optischer Leitungsnetze.",
|
||||||
coreCourses: ["ihk_ap1", "ihk_ap2_itse", "bash", "dns_records", "cpp"],
|
coreCourses: ["ihk_ap1", "bash", "dns_records", "cpp"],
|
||||||
years: {
|
years: {
|
||||||
1: {
|
1: {
|
||||||
title: "1. Ausbildungsjahr (Elektrotechnische Grundlagen & Arbeitsplatz)",
|
title: "1. Ausbildungsjahr (Elektrotechnische Grundlagen & Arbeitsplatz)",
|
||||||
|
|
@ -152,7 +152,7 @@
|
||||||
3: {
|
3: {
|
||||||
title: "3. Ausbildungsjahr (VDE-Prüfungen, USV-Anlagen & Messtechnik)",
|
title: "3. Ausbildungsjahr (VDE-Prüfungen, USV-Anlagen & Messtechnik)",
|
||||||
lernfelder: [10, 11, 12],
|
lernfelder: [10, 11, 12],
|
||||||
recommendedCourses: ["ihk_ap2_itse", "cpp", "bash"]
|
recommendedCourses: ["cpp", "bash"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -164,7 +164,7 @@
|
||||||
color: "#2563eb",
|
color: "#2563eb",
|
||||||
badgeClass: "badge-itsm",
|
badgeClass: "badge-itsm",
|
||||||
description: "Kaufmännische IT-Beratung, Konzeption und Vertrieb von IT-Lösungen, ITIL-Serviceverträge und IT-Projektmanagement.",
|
description: "Kaufmännische IT-Beratung, Konzeption und Vertrieb von IT-Lösungen, ITIL-Serviceverträge und IT-Projektmanagement.",
|
||||||
coreCourses: ["ihk_ap1", "ihk_ap2_itsm", "sql", "python", "html_css"],
|
coreCourses: ["ihk_ap1", "sql", "python", "html_css"],
|
||||||
years: {
|
years: {
|
||||||
1: {
|
1: {
|
||||||
title: "1. Ausbildungsjahr (IT-Märkte, Hard-/Software & Kalkulation)",
|
title: "1. Ausbildungsjahr (IT-Märkte, Hard-/Software & Kalkulation)",
|
||||||
|
|
@ -179,7 +179,7 @@
|
||||||
3: {
|
3: {
|
||||||
title: "3. Ausbildungsjahr (SLA-Vertrieb, ITIL-Services & IT-Projekte)",
|
title: "3. Ausbildungsjahr (SLA-Vertrieb, ITIL-Services & IT-Projekte)",
|
||||||
lernfelder: [10, 11, 12],
|
lernfelder: [10, 11, 12],
|
||||||
recommendedCourses: ["ihk_ap2_itsm", "sql", "html_css"]
|
recommendedCourses: ["sql", "html_css"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -191,7 +191,7 @@
|
||||||
color: "#0891b2",
|
color: "#0891b2",
|
||||||
badgeClass: "badge-kdm",
|
badgeClass: "badge-kdm",
|
||||||
description: "Analyse und Digitalisierung betriebswirtschaftlicher Wertschöpfungsketten, ERP-Systeme, Datenökonomie und Compliance.",
|
description: "Analyse und Digitalisierung betriebswirtschaftlicher Wertschöpfungsketten, ERP-Systeme, Datenökonomie und Compliance.",
|
||||||
coreCourses: ["ihk_ap1", "ihk_ap2_kdm", "sql", "python", "git"],
|
coreCourses: ["ihk_ap1", "sql", "python", "git"],
|
||||||
years: {
|
years: {
|
||||||
1: {
|
1: {
|
||||||
title: "1. Ausbildungsjahr (Geschäftsprozesse & IT-Organisation)",
|
title: "1. Ausbildungsjahr (Geschäftsprozesse & IT-Organisation)",
|
||||||
|
|
@ -206,7 +206,7 @@
|
||||||
3: {
|
3: {
|
||||||
title: "3. Ausbildungsjahr (Plattformökonomie, Digital ROI & Controlling)",
|
title: "3. Ausbildungsjahr (Plattformökonomie, Digital ROI & Controlling)",
|
||||||
lernfelder: [10, 11, 12],
|
lernfelder: [10, 11, 12],
|
||||||
recommendedCourses: ["ihk_ap2_kdm", "sql", "python"]
|
recommendedCourses: ["sql", "python"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -312,12 +312,12 @@
|
||||||
},
|
},
|
||||||
relatedCourses: {
|
relatedCourses: {
|
||||||
"FISI": ["active_directory", "powershell", "bash", "ihk_ap2_fisi"],
|
"FISI": ["active_directory", "powershell", "bash", "ihk_ap2_fisi"],
|
||||||
"FIAE": ["ihk_ap2_fiae", "html_css", "javascript", "git"],
|
"FIAE": ["html_css", "javascript", "git"],
|
||||||
"FIDP": ["ihk_ap2_fidp", "python", "sql", "git"],
|
"FIDP": ["python", "sql", "git"],
|
||||||
"FIDV": ["ihk_ap2_fidv", "bash", "dns_records", "cpp"],
|
"FIDV": ["bash", "dns_records", "cpp"],
|
||||||
"ITSE": ["ihk_ap2_itse", "bash", "cpp"],
|
"ITSE": ["bash", "cpp"],
|
||||||
"ITSM": ["ihk_ap2_itsm", "sql", "python"],
|
"ITSM": ["sql", "python"],
|
||||||
"KDM": ["ihk_ap2_kdm", "sql", "git"]
|
"KDM": ["sql", "git"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
11: {
|
11: {
|
||||||
|
|
@ -337,12 +337,12 @@
|
||||||
},
|
},
|
||||||
relatedCourses: {
|
relatedCourses: {
|
||||||
"FISI": ["dns_records", "active_directory", "ihk_ap2_fisi"],
|
"FISI": ["dns_records", "active_directory", "ihk_ap2_fisi"],
|
||||||
"FIAE": ["ihk_ap2_fiae", "java", "csharp", "go", "rust"],
|
"FIAE": ["java", "csharp", "go", "rust"],
|
||||||
"FIDP": ["ihk_ap2_fidp", "python", "sql"],
|
"FIDP": ["python", "sql"],
|
||||||
"FIDV": ["ihk_ap2_fidv", "dns_records", "bash"],
|
"FIDV": ["dns_records", "bash"],
|
||||||
"ITSE": ["ihk_ap2_itse", "dns_records"],
|
"ITSE": ["dns_records"],
|
||||||
"ITSM": ["ihk_ap2_itsm", "sql"],
|
"ITSM": ["sql"],
|
||||||
"KDM": ["ihk_ap2_kdm", "sql", "python"]
|
"KDM": ["sql", "python"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
12: {
|
12: {
|
||||||
|
|
@ -362,12 +362,12 @@
|
||||||
},
|
},
|
||||||
relatedCourses: {
|
relatedCourses: {
|
||||||
"FISI": ["ihk_ap2_fisi", "active_directory", "dns_records", "bash"],
|
"FISI": ["ihk_ap2_fisi", "active_directory", "dns_records", "bash"],
|
||||||
"FIAE": ["ihk_ap2_fiae", "git", "java", "csharp", "python"],
|
"FIAE": ["git", "java", "csharp", "python"],
|
||||||
"FIDP": ["ihk_ap2_fidp", "sql", "python", "git"],
|
"FIDP": ["sql", "python", "git"],
|
||||||
"FIDV": ["ihk_ap2_fidv", "bash", "dns_records"],
|
"FIDV": ["bash", "dns_records"],
|
||||||
"ITSE": ["ihk_ap2_itse", "bash"],
|
"ITSE": ["bash"],
|
||||||
"ITSM": ["ihk_ap2_itsm", "sql"],
|
"ITSM": ["sql"],
|
||||||
"KDM": ["ihk_ap2_kdm", "sql"]
|
"KDM": ["sql"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
400
assets/style.css
400
assets/style.css
|
|
@ -3,67 +3,56 @@
|
||||||
========================================================================== */
|
========================================================================== */
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
/* Brand Colors - Authoritative Tech & Enterprise Blue */
|
/* Brand Colors */
|
||||||
--primary: #1d4ed8;
|
--primary: #4f46e5;
|
||||||
--primary-hover: #1e40af;
|
--primary-hover: #4338ca;
|
||||||
--primary-light: #eff6ff;
|
--primary-light: #eef2ff;
|
||||||
--primary-glow: rgba(29, 78, 216, 0.12);
|
--primary-glow: rgba(79, 70, 229, 0.15);
|
||||||
|
|
||||||
--slate-900: #0f172a;
|
|
||||||
--slate-800: #1e293b;
|
|
||||||
--slate-700: #334155;
|
|
||||||
--slate-600: #475569;
|
|
||||||
--slate-500: #64748b;
|
|
||||||
--slate-400: #94a3b8;
|
|
||||||
--slate-300: #cbd5e1;
|
|
||||||
--slate-200: #e2e8f0;
|
|
||||||
--slate-100: #f1f5f9;
|
|
||||||
--slate-50: #f8fafc;
|
|
||||||
|
|
||||||
--emerald: #059669;
|
--emerald: #059669;
|
||||||
--emerald-light: #f0fdf4;
|
--emerald-light: #ecfdf5;
|
||||||
|
|
||||||
--purple: #6366f1;
|
--purple: #7c3aed;
|
||||||
--purple-light: #eef2ff;
|
--purple-light: #f5f3ff;
|
||||||
|
|
||||||
--amber: #b45309;
|
--amber: #d97706;
|
||||||
--amber-light: #fffbeb;
|
--amber-light: #fffbeb;
|
||||||
|
|
||||||
--rose: #b91c1c;
|
--rose: #e11d48;
|
||||||
--rose-light: #fef2f2;
|
--rose-light: #fff1f2;
|
||||||
|
|
||||||
--success: #059669;
|
--success: #10b981;
|
||||||
--success-light: #f0fdf4;
|
--success-light: #ecfdf5;
|
||||||
|
|
||||||
/* Surfaces & Backgrounds */
|
/* Surfaces & Backgrounds */
|
||||||
--bg-body: #f8fafc;
|
--bg-body: #f8fafc;
|
||||||
--bg-surface: #ffffff;
|
--bg-surface: #ffffff;
|
||||||
--bg-subtle: #f1f5f9;
|
--bg-subtle: #f1f5f9;
|
||||||
--bg-sidebar: #0f172a;
|
--bg-sidebar: #090d16;
|
||||||
--bg-code: #0b0f19;
|
--bg-code: #0b0f19;
|
||||||
|
|
||||||
/* Borders */
|
/* Borders */
|
||||||
--border-color: #e2e8f0;
|
--border-color: #e2e8f0;
|
||||||
--border-dark: #1e293b;
|
--border-dark: rgba(255, 255, 255, 0.08);
|
||||||
|
|
||||||
/* Typography */
|
/* Typography */
|
||||||
--text-main: #0f172a;
|
--text-main: #0f172a;
|
||||||
--text-body: #334155;
|
--text-body: #1e293b;
|
||||||
--text-muted: #64748b;
|
--text-muted: #475569;
|
||||||
--text-inverse: #f8fafc;
|
--text-inverse: #f8fafc;
|
||||||
|
|
||||||
--font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
--font-sans: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||||
--font-mono: 'JetBrains Mono', 'Fira Code', 'SFMono-Regular', Consolas, Menlo, monospace;
|
--font-mono: 'JetBrains Mono', 'Fira Code', Consolas, Monaco, monospace;
|
||||||
|
|
||||||
/* Elevation */
|
/* Elevation */
|
||||||
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||||
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.06), 0 2px 4px -2px rgba(0, 0, 0, 0.04);
|
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.07), 0 2px 4px -1px rgba(0, 0, 0, 0.04);
|
||||||
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.08), 0 4px 6px -4px rgba(0, 0, 0, 0.03);
|
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.08), 0 4px 6px -2px rgba(0, 0, 0, 0.04);
|
||||||
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.08), 0 8px 10px -6px rgba(0, 0, 0, 0.03);
|
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.12), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||||
|
|
||||||
--radius-sm: 6px;
|
--radius-sm: 6px;
|
||||||
--radius-md: 10px;
|
--radius-md: 12px;
|
||||||
--radius-lg: 14px;
|
--radius-lg: 18px;
|
||||||
--radius-full: 9999px;
|
--radius-full: 9999px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -162,7 +151,7 @@ a:hover {
|
||||||
|
|
||||||
.sidebar-subtitle {
|
.sidebar-subtitle {
|
||||||
font-size: 0.72rem;
|
font-size: 0.72rem;
|
||||||
color: #cbd5e1;
|
color: #94a3b8;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
}
|
}
|
||||||
|
|
@ -197,7 +186,7 @@ a:hover {
|
||||||
.sidebar-group-title {
|
.sidebar-group-title {
|
||||||
font-size: 0.74rem;
|
font-size: 0.74rem;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
color: #cbd5e1;
|
color: #94a3b8;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.8px;
|
letter-spacing: 0.8px;
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
|
|
@ -595,7 +584,7 @@ pre code {
|
||||||
.fn { color: #60a5fa; }
|
.fn { color: #60a5fa; }
|
||||||
.str { color: #a7f3d0; }
|
.str { color: #a7f3d0; }
|
||||||
.num { color: #fbbf24; }
|
.num { color: #fbbf24; }
|
||||||
.cm { color: #94a3b8; font-style: italic; }
|
.cm { color: #64748b; font-style: italic; }
|
||||||
.var { color: #cbd5e1; }
|
.var { color: #cbd5e1; }
|
||||||
.attr { color: #38bdf8; }
|
.attr { color: #38bdf8; }
|
||||||
|
|
||||||
|
|
@ -1077,32 +1066,31 @@ textarea:focus {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
background: #0f172a;
|
background: rgba(15, 23, 42, 0.94);
|
||||||
border-bottom: 1px solid #1e293b;
|
backdrop-filter: blur(16px);
|
||||||
padding: 0 24px;
|
-webkit-backdrop-filter: blur(16px);
|
||||||
height: 62px;
|
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
display: flex;
|
padding: 10px 20px;
|
||||||
align-items: center;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.portal-nav-container {
|
.portal-nav-container {
|
||||||
max-width: 1360px;
|
max-width: 1320px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 20px;
|
gap: 16px;
|
||||||
|
flex-wrap: nowrap;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.portal-brand {
|
.portal-brand {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 10px;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: white;
|
color: white;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
|
@ -1111,35 +1099,35 @@ textarea:focus {
|
||||||
}
|
}
|
||||||
|
|
||||||
.portal-brand-logo {
|
.portal-brand-logo {
|
||||||
width: 36px;
|
width: 34px;
|
||||||
height: 36px;
|
height: 34px;
|
||||||
border-radius: 8px;
|
border-radius: 9px;
|
||||||
background: #1e293b;
|
background: linear-gradient(135deg, rgba(56, 189, 248, 0.2), rgba(37, 99, 235, 0.2));
|
||||||
border: 1px solid #334155;
|
border: 1px solid rgba(56, 189, 248, 0.4);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
box-shadow: 0 0 15px rgba(56, 189, 248, 0.15);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.portal-brand-title {
|
.portal-brand-title {
|
||||||
font-size: 1.05rem;
|
font-size: 1.15rem;
|
||||||
font-weight: 700;
|
font-weight: 800;
|
||||||
letter-spacing: -0.3px;
|
letter-spacing: -0.4px;
|
||||||
color: #f8fafc;
|
color: #f8fafc;
|
||||||
line-height: 1.2;
|
line-height: 1;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.portal-brand-badge {
|
.portal-brand-badge {
|
||||||
font-size: 0.68rem;
|
font-size: 0.65rem;
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.6px;
|
letter-spacing: 0.8px;
|
||||||
background: #1e293b;
|
background: linear-gradient(90deg, #0284c7, #2563eb);
|
||||||
color: #cbd5e1;
|
color: white;
|
||||||
border: 1px solid #334155;
|
|
||||||
padding: 2px 7px;
|
padding: 2px 7px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
margin-left: 6px;
|
margin-left: 6px;
|
||||||
|
|
@ -1151,42 +1139,35 @@ textarea:focus {
|
||||||
.portal-nav-links {
|
.portal-nav-links {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 4px;
|
gap: 10px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
height: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.portal-nav-link {
|
.portal-nav-link {
|
||||||
color: #cbd5e1;
|
color: #94a3b8;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-size: 0.85rem;
|
font-size: 0.83rem;
|
||||||
font-weight: 500;
|
font-weight: 600;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
padding: 7px 12px;
|
padding: 4px 7px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
transition: all 0.15s ease;
|
transition: all 0.15s ease;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.portal-nav-link:hover {
|
.portal-nav-link:hover, .portal-nav-link.active {
|
||||||
color: #f8fafc;
|
color: #38bdf8;
|
||||||
background: rgba(255, 255, 255, 0.06);
|
background: rgba(56, 189, 248, 0.08);
|
||||||
}
|
|
||||||
|
|
||||||
.portal-nav-link.active {
|
|
||||||
color: #ffffff;
|
|
||||||
background: #1e293b;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.portal-hero {
|
.portal-hero {
|
||||||
background: #0f172a;
|
background: radial-gradient(circle at 50% 0%, rgba(37, 99, 235, 0.15) 0%, transparent 60%), #0b0f19;
|
||||||
border-bottom: 1px solid #1e293b;
|
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
||||||
padding: 44px 24px 38px 24px;
|
padding: 56px 24px 44px 24px;
|
||||||
text-align: left;
|
text-align: center;
|
||||||
color: #f8fafc;
|
color: #f8fafc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1194,76 +1175,74 @@ textarea:focus {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
background: #1e293b;
|
background: rgba(56, 189, 248, 0.1);
|
||||||
border: 1px solid #334155;
|
border: 1px solid rgba(56, 189, 248, 0.3);
|
||||||
color: #cbd5e1;
|
color: #38bdf8;
|
||||||
padding: 5px 12px;
|
padding: 6px 14px;
|
||||||
border-radius: 6px;
|
border-radius: 99px;
|
||||||
font-size: 0.78rem;
|
font-size: 0.82rem;
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
margin-bottom: 14px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.portal-hero h1 {
|
.portal-hero h1 {
|
||||||
font-size: 2.15rem;
|
font-size: 2.7rem;
|
||||||
font-weight: 800;
|
font-weight: 900;
|
||||||
letter-spacing: -0.5px;
|
letter-spacing: -1px;
|
||||||
line-height: 1.25;
|
line-height: 1.15;
|
||||||
margin: 0 0 10px 0;
|
margin: 0 auto 16px auto;
|
||||||
max-width: 820px;
|
max-width: 820px;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.portal-hero p {
|
.portal-hero p {
|
||||||
font-size: 0.98rem;
|
font-size: 1.12rem;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
color: #cbd5e1;
|
color: #94a3b8;
|
||||||
max-width: 720px;
|
max-width: 680px;
|
||||||
margin: 0;
|
margin: 0 auto 28px auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.portal-hero-actions {
|
.portal-hero-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-start;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
margin-top: 22px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.portal-btn-primary {
|
.portal-btn-primary {
|
||||||
background: #1d4ed8;
|
background: linear-gradient(135deg, #0284c7, #2563eb);
|
||||||
color: white;
|
color: white;
|
||||||
padding: 10px 20px;
|
padding: 12px 24px;
|
||||||
border-radius: 8px;
|
border-radius: 10px;
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
font-size: 0.88rem;
|
font-size: 0.95rem;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
border: 1px solid #1d4ed8;
|
box-shadow: 0 4px 14px rgba(37, 99, 235, 0.35);
|
||||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.2);
|
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
||||||
transition: all 0.15s ease;
|
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
border: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.portal-btn-primary:hover {
|
.portal-btn-primary:hover {
|
||||||
background: #1e40af;
|
|
||||||
border-color: #1e40af;
|
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 6px 20px rgba(37, 99, 235, 0.45);
|
||||||
}
|
}
|
||||||
|
|
||||||
.portal-btn-secondary {
|
.portal-btn-secondary {
|
||||||
background: #1e293b;
|
background: rgba(255, 255, 255, 0.06);
|
||||||
color: #cbd5e1;
|
color: #f1f5f9;
|
||||||
border: 1px solid #334155;
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||||
padding: 10px 18px;
|
padding: 12px 22px;
|
||||||
border-radius: 8px;
|
border-radius: 10px;
|
||||||
font-weight: 500;
|
font-weight: 600;
|
||||||
font-size: 0.88rem;
|
font-size: 0.95rem;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
transition: all 0.15s ease;
|
transition: background 0.15s ease, border-color 0.15s ease;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
|
@ -1271,45 +1250,44 @@ textarea:focus {
|
||||||
}
|
}
|
||||||
|
|
||||||
.portal-btn-secondary:hover {
|
.portal-btn-secondary:hover {
|
||||||
background: #334155;
|
background: rgba(255, 255, 255, 0.12);
|
||||||
color: #ffffff;
|
border-color: rgba(255, 255, 255, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
.portal-stats-bar {
|
.portal-stats-bar {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(4, 1fr);
|
grid-template-columns: repeat(4, 1fr);
|
||||||
gap: 14px;
|
gap: 16px;
|
||||||
margin-top: 26px;
|
max-width: 1080px;
|
||||||
|
margin: 36px auto 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.portal-stat-card {
|
.portal-stat-card {
|
||||||
background: #1e293b;
|
background: rgba(15, 23, 42, 0.6);
|
||||||
border: 1px solid #334155;
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
border-radius: 10px;
|
border-radius: 12px;
|
||||||
padding: 16px 18px;
|
padding: 16px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.portal-stat-num {
|
.portal-stat-num {
|
||||||
font-size: 1.35rem;
|
font-size: 1.6rem;
|
||||||
font-weight: 700;
|
font-weight: 900;
|
||||||
color: #f8fafc;
|
color: #38bdf8;
|
||||||
line-height: 1.25;
|
line-height: 1;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.portal-stat-label {
|
.portal-stat-label {
|
||||||
font-size: 0.78rem;
|
font-size: 0.82rem;
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
color: #cbd5e1;
|
color: #f1f5f9;
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.5px;
|
|
||||||
margin-bottom: 2px;
|
margin-bottom: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.portal-stat-desc {
|
.portal-stat-desc {
|
||||||
font-size: 0.76rem;
|
font-size: 0.75rem;
|
||||||
color: #94a3b8;
|
color: #64748b;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Filter Bar */
|
/* Filter Bar */
|
||||||
|
|
@ -1318,8 +1296,8 @@ textarea:focus {
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 16px;
|
gap: 14px;
|
||||||
margin: 32px 0 20px 0;
|
margin: 36px 0 24px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.portal-filter-tabs {
|
.portal-filter-tabs {
|
||||||
|
|
@ -1329,21 +1307,20 @@ textarea:focus {
|
||||||
}
|
}
|
||||||
|
|
||||||
.portal-tab-pill {
|
.portal-tab-pill {
|
||||||
background: #ffffff;
|
background: white;
|
||||||
border: 1px solid #cbd5e1;
|
border: 1px solid #cbd5e1;
|
||||||
color: #475569;
|
color: #475569;
|
||||||
padding: 7px 14px;
|
padding: 8px 16px;
|
||||||
border-radius: 8px;
|
border-radius: 99px;
|
||||||
font-size: 0.84rem;
|
font-size: 0.85rem;
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.15s ease;
|
transition: all 0.15s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.portal-tab-pill:hover {
|
.portal-tab-pill:hover {
|
||||||
background: #f8fafc;
|
|
||||||
color: #0f172a;
|
|
||||||
border-color: #94a3b8;
|
border-color: #94a3b8;
|
||||||
|
color: #0f172a;
|
||||||
}
|
}
|
||||||
|
|
||||||
.portal-tab-pill.active {
|
.portal-tab-pill.active {
|
||||||
|
|
@ -1357,15 +1334,15 @@ textarea:focus {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: white;
|
background: white;
|
||||||
border: 1px solid #cbd5e1;
|
border: 1px solid #cbd5e1;
|
||||||
border-radius: 8px;
|
border-radius: 99px;
|
||||||
padding: 7px 14px;
|
padding: 6px 14px;
|
||||||
min-width: 280px;
|
min-width: 280px;
|
||||||
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.portal-search-box:focus-within {
|
.portal-search-box:focus-within {
|
||||||
border-color: #1d4ed8;
|
border-color: #0284c7;
|
||||||
box-shadow: 0 0 0 3px rgba(29, 78, 216, 0.1);
|
box-shadow: 0 0 0 3px rgba(2, 132, 199, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
.portal-search-box input {
|
.portal-search-box input {
|
||||||
|
|
@ -1389,20 +1366,20 @@ textarea:focus {
|
||||||
.portal-card {
|
.portal-card {
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
border: 1px solid #e2e8f0;
|
border: 1px solid #e2e8f0;
|
||||||
border-radius: 12px;
|
border-radius: 14px;
|
||||||
padding: 22px;
|
padding: 22px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.03);
|
||||||
transition: transform 0.15s ease, box-shadow 0.15s ease, border-color 0.15s ease;
|
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.portal-card:hover {
|
.portal-card:hover {
|
||||||
transform: translateY(-2px);
|
transform: translateY(-3px);
|
||||||
box-shadow: 0 8px 16px -4px rgba(0, 0, 0, 0.08);
|
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.08);
|
||||||
border-color: #cbd5e1;
|
border-color: #cbd5e1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1414,40 +1391,39 @@ textarea:focus {
|
||||||
}
|
}
|
||||||
|
|
||||||
.portal-card-icon {
|
.portal-card-icon {
|
||||||
font-size: 1.35rem;
|
font-size: 2rem;
|
||||||
width: 44px;
|
width: 52px;
|
||||||
height: 44px;
|
height: 52px;
|
||||||
border-radius: 8px;
|
border-radius: 12px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
background: #f8fafc;
|
background: #f8fafc;
|
||||||
border: 1px solid #e2e8f0;
|
border: 1px solid #e2e8f0;
|
||||||
color: #1e293b;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.portal-card-badge {
|
.portal-card-badge {
|
||||||
font-size: 0.72rem;
|
font-size: 0.72rem;
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
padding: 3px 8px;
|
padding: 4px 10px;
|
||||||
border-radius: 6px;
|
border-radius: 99px;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.4px;
|
letter-spacing: 0.5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.portal-card-title {
|
.portal-card-title {
|
||||||
font-size: 1.08rem;
|
font-size: 1.15rem;
|
||||||
font-weight: 700;
|
font-weight: 800;
|
||||||
color: #0f172a;
|
color: #0f172a;
|
||||||
margin: 0 0 6px 0;
|
margin: 0 0 6px 0;
|
||||||
line-height: 1.35;
|
line-height: 1.3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.portal-card-desc {
|
.portal-card-desc {
|
||||||
font-size: 0.86rem;
|
font-size: 0.86rem;
|
||||||
color: #475569;
|
color: #475569;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
margin: 0 0 14px 0;
|
margin: 0 0 16px 0;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1462,10 +1438,9 @@ textarea:focus {
|
||||||
background: #f1f5f9;
|
background: #f1f5f9;
|
||||||
color: #475569;
|
color: #475569;
|
||||||
font-size: 0.72rem;
|
font-size: 0.72rem;
|
||||||
font-weight: 500;
|
font-weight: 600;
|
||||||
padding: 3px 7px;
|
padding: 3px 8px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border: 1px solid #e2e8f0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.portal-card-footer {
|
.portal-card-footer {
|
||||||
|
|
@ -1487,9 +1462,9 @@ textarea:focus {
|
||||||
color: white;
|
color: white;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-size: 0.82rem;
|
font-size: 0.82rem;
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
padding: 8px 14px;
|
padding: 8px 14px;
|
||||||
border-radius: 6px;
|
border-radius: 8px;
|
||||||
transition: background 0.15s ease;
|
transition: background 0.15s ease;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -1497,7 +1472,7 @@ textarea:focus {
|
||||||
}
|
}
|
||||||
|
|
||||||
.portal-card-btn:hover {
|
.portal-card-btn:hover {
|
||||||
background: #1d4ed8;
|
background: #0284c7;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Bento Grid */
|
/* Bento Grid */
|
||||||
|
|
@ -1832,25 +1807,23 @@ html, body {
|
||||||
#nav-profile-pill {
|
#nav-profile-pill {
|
||||||
display: inline-flex !important;
|
display: inline-flex !important;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 4px;
|
||||||
padding: 4px 10px !important;
|
padding: 4px 8px !important;
|
||||||
max-width: 140px;
|
max-width: 110px;
|
||||||
flex-shrink: 1;
|
flex-shrink: 1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: #1e293b !important;
|
background: rgba(255, 255, 255, 0.08) !important;
|
||||||
border: 1px solid #334155 !important;
|
border-radius: 99px;
|
||||||
border-radius: 6px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#nav-auth-user {
|
#nav-auth-user {
|
||||||
display: inline-block !important;
|
display: inline-block !important;
|
||||||
font-size: 0.76rem !important;
|
font-size: 0.76rem !important;
|
||||||
font-weight: 600 !important;
|
max-width: 80px;
|
||||||
max-width: 90px;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap !important;
|
white-space: nowrap !important;
|
||||||
color: #f8fafc !important;
|
color: #38bdf8 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
#nav-auth-role {
|
#nav-auth-role {
|
||||||
|
|
@ -1870,8 +1843,7 @@ html, body {
|
||||||
/* Ultra-compact Mobile Screen adjustments (<= 400px e.g. iPhone SE, 375px, 360px, 320px) */
|
/* Ultra-compact Mobile Screen adjustments (<= 400px e.g. iPhone SE, 375px, 360px, 320px) */
|
||||||
@media (max-width: 400px) {
|
@media (max-width: 400px) {
|
||||||
.portal-header-bar {
|
.portal-header-bar {
|
||||||
padding: 0 10px !important;
|
padding: 7px 8px !important;
|
||||||
height: 56px !important;
|
|
||||||
}
|
}
|
||||||
.portal-nav-container {
|
.portal-nav-container {
|
||||||
gap: 6px !important;
|
gap: 6px !important;
|
||||||
|
|
@ -1886,6 +1858,18 @@ html, body {
|
||||||
.portal-brand-title {
|
.portal-brand-title {
|
||||||
font-size: 0.90rem !important;
|
font-size: 0.90rem !important;
|
||||||
}
|
}
|
||||||
|
#nav-auth-user {
|
||||||
|
max-width: 70px !important;
|
||||||
|
font-size: 0.72rem !important;
|
||||||
|
}
|
||||||
|
#nav-profile-pill {
|
||||||
|
max-width: 95px !important;
|
||||||
|
padding: 3px 6px !important;
|
||||||
|
}
|
||||||
|
#nav-btn-register {
|
||||||
|
padding: 4px 7px !important;
|
||||||
|
font-size: 0.72rem !important;
|
||||||
|
}
|
||||||
.mobile-menu-toggle {
|
.mobile-menu-toggle {
|
||||||
width: 32px !important;
|
width: 32px !important;
|
||||||
height: 32px !important;
|
height: 32px !important;
|
||||||
|
|
@ -1896,14 +1880,14 @@ html, body {
|
||||||
/* Mobile Drawer & Hamburger Menu */
|
/* Mobile Drawer & Hamburger Menu */
|
||||||
.mobile-menu-toggle {
|
.mobile-menu-toggle {
|
||||||
display: none;
|
display: none;
|
||||||
background: #1e293b;
|
background: rgba(255, 255, 255, 0.08);
|
||||||
border: 1px solid #334155;
|
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||||
color: #f8fafc;
|
color: #f8fafc;
|
||||||
font-size: 1.1rem;
|
font-size: 1.2rem;
|
||||||
width: 36px;
|
width: 36px;
|
||||||
height: 36px;
|
height: 36px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border-radius: 6px;
|
border-radius: 8px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
|
@ -1911,21 +1895,21 @@ html, body {
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-menu-toggle:hover {
|
.mobile-menu-toggle:hover {
|
||||||
background: #334155;
|
background: rgba(255, 255, 255, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-drawer {
|
.mobile-drawer {
|
||||||
background: #0f172a;
|
background: #090d16;
|
||||||
border-bottom: 1px solid #1e293b;
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
padding: 14px 16px 18px 16px;
|
padding: 14px 16px 18px 16px;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 62px;
|
top: 52px;
|
||||||
z-index: 49;
|
z-index: 49;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 4px;
|
gap: 6px;
|
||||||
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.4);
|
box-shadow: 0 15px 30px rgba(0, 0, 0, 0.6);
|
||||||
animation: slideDown 0.15s ease-out;
|
animation: slideDown 0.2s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes slideDown {
|
@keyframes slideDown {
|
||||||
|
|
@ -1936,10 +1920,10 @@ html, body {
|
||||||
.mobile-drawer-link {
|
.mobile-drawer-link {
|
||||||
color: #cbd5e1;
|
color: #cbd5e1;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-size: 0.9rem;
|
font-size: 0.94rem;
|
||||||
font-weight: 500;
|
font-weight: 600;
|
||||||
padding: 9px 12px;
|
padding: 10px 14px;
|
||||||
border-radius: 6px;
|
border-radius: 8px;
|
||||||
transition: background 0.15s ease, color 0.15s ease;
|
transition: background 0.15s ease, color 0.15s ease;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -1947,8 +1931,8 @@ html, body {
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-drawer-link:hover, .mobile-drawer-link:active {
|
.mobile-drawer-link:hover, .mobile-drawer-link:active {
|
||||||
background: #1e293b;
|
background: rgba(56, 189, 248, 0.12);
|
||||||
color: #ffffff;
|
color: #38bdf8;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ==========================================================================
|
/* ==========================================================================
|
||||||
|
|
@ -2132,8 +2116,8 @@ body.has-course-sidebar {
|
||||||
font-size: 0.68rem;
|
font-size: 0.68rem;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.8px;
|
letter-spacing: 0.6px;
|
||||||
color: #94a3b8;
|
color: #64748b;
|
||||||
padding: 12px 10px 4px 10px;
|
padding: 12px 10px 4px 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2143,7 +2127,7 @@ body.has-course-sidebar {
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
padding: 8px 10px;
|
padding: 8px 10px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
color: #cbd5e1;
|
color: #94a3b8;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-size: 0.82rem;
|
font-size: 0.82rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
|
|
||||||
|
|
@ -1,64 +1,18 @@
|
||||||
# DNS 01: DNS-Hierarchie, FQDN-Struktur & Root-Server
|
# DNS 01: DNS-Hierarchie & Root-Server
|
||||||
|
|
||||||
Willkommen zu **Modul 01** des Kurses **DNS-Records, Domain Name System & E-Mail-Sicherheit**!
|
Willkommen zum Kurs **DNS-Records, Domain Name System & E-Mail-Sicherheit** im IT-Praxisportal!
|
||||||
|
|
||||||
In diesem Modul lernst du die fundamentale Architektur des globalen Domain Name Systems kennen: Wie der umgekehrte DNS-Baum aufgebaut ist, wie Fully Qualified Domain Names (FQDN) strukturiert sind, welche Rolle die 13 weltweiten Root-Server-Cluster spielen und wie Delegationspfade funktionieren.
|
## 💡 Das Wichtigste in Kürze
|
||||||
|
- **Ziel:** Verstehe den globalen DNS-Baum: Root-Zone (.), TLDs (gTLD/ccTLD), SLDs, FQDNs und autoritative vs. rekursive Nameserver.
|
||||||
|
- **Wichtigstes Tool:** `dig`
|
||||||
|
|
||||||
---
|
## 🎯 Deine Aufgaben (Checkliste)
|
||||||
|
1. **Teilziel 1:** FQDN in Hostname, SLD und TLD zerlegen
|
||||||
## 💡 1. Das Wichtigste in Kürze
|
2. **Teilziel 2:** Root-Server-Cluster (A-M) und Anycast-Routing erklären
|
||||||
|
3. **Teilziel 3:** Unterschied autoritativer vs. rekursiver Nameserver definieren
|
||||||
### Die DNS-Baumstruktur (Inverted Tree)
|
4. **Teilziel 4:** DNS-Baumpfad für www.it-praxisportal.de konstruieren
|
||||||
Das DNS ist von oben nach unten organisiert:
|
|
||||||
- **Root Zone (`.`):** Die oberste Wurzel aller Domains.
|
|
||||||
- **Top-Level Domains (TLD):**
|
|
||||||
- *ccTLD (Country-Code):* `.de`, `.ch`, `.at`, `.fr`, `.uk`
|
|
||||||
- *gTLD (Generic):* `.com`, `.org`, `.net`, `.info`
|
|
||||||
- *new gTLD:* `.cloud`, `.dev`, `.app`, `.tech`
|
|
||||||
- **Second-Level Domains (SLD):** Der registrierte Name der Organisation oder Person (z.B. `it-praxisportal` in `it-praxisportal.de`).
|
|
||||||
- **Subdomains / Third-Level Domains:** Beliebig tiefe Unterteilungen (z.B. `intern`, `api`, `staging`).
|
|
||||||
- **Hostname:** Der Name des konkreten Dienstes oder Host-Servers (`srv01`, `www`, `mail`).
|
|
||||||
|
|
||||||
```
|
|
||||||
. (Root)
|
|
||||||
/ \
|
|
||||||
de. com. (TLD)
|
|
||||||
/ \
|
|
||||||
it-praxisportal.de. google.com. (SLD)
|
|
||||||
/
|
|
||||||
intern. (Subdomain)
|
|
||||||
/
|
|
||||||
srv01. (Hostname)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Der Fully Qualified Domain Name (FQDN)
|
|
||||||
Ein vollständiger Domänenname enthält alle Ebenen von links nach rechts bis zum abschließenden Punkt der Root-Zone:
|
|
||||||
`srv01.intern.it-praxisportal.de.`
|
|
||||||
|
|
||||||
> [!IMPORTANT]
|
|
||||||
> **Der abschließende Punkt (`.`):** In DNS-Zonendateien (BIND) ist der Schlusspunkt entscheidend!
|
|
||||||
> - `www.example.de.` (mit Punkt) = Absoluter FQDN.
|
|
||||||
> - `www` (ohne Punkt) = Relativer Bezeichner, an den BIND automatisch den Zonennamen anhängt (`$ORIGIN`).
|
|
||||||
> Ein vergessener Schlusspunkt bei Zielen führt zu Fehlern wie `mail.example.de.example.de.`!
|
|
||||||
|
|
||||||
### Die 13 Root-Server-Cluster
|
|
||||||
Es gibt genau 13 logische IP-Adress-Cluster (A bis M, z.B. `k.root-servers.net`).
|
|
||||||
Dank **BGP Anycast** verbergen sich hinter diesen 13 IP-Adressen mehr als 1.500 physische Serverknoten weltweit. Dadurch sind Root-Server extrem ausfallsicher gegen DDoS-Angriffe und Serverausfälle geschützt.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 Deine Aufgaben in `aufgabe.sh`
|
|
||||||
|
|
||||||
1. **TODO 1 (`parse_fqdn`):** Zerlege einen übergebenen FQDN in `HOST`, `DOMAIN`, `SLD`, `TLD` und prüfe, ob der abschließende Root-Punkt gesetzt ist (`HAS_ROOT_DOT: YES/NO`).
|
|
||||||
2. **TODO 2 (`list_root_server`):** Ermittle den zuständigen Betreiber und FQDN für die Root-Cluster A bis M oder gib alle 13 Cluster aus.
|
|
||||||
3. **TODO 3 (`build_delegation_path`):** Konstruiere den schrittweisen Delegationspfad von der Root (`.`) über TLD und SLD bis zum FQDN.
|
|
||||||
4. **TODO 4 (`validate_fqdn_syntax`):** Validiere Domain-Namen nach den RFC-1035-Regeln (Längenlimits, keine unzulässigen Bindestriche).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🧪 Tests ausführen
|
## 🧪 Tests ausführen
|
||||||
|
|
||||||
Führe im Terminal folgenden Befehl aus:
|
|
||||||
```bash
|
```bash
|
||||||
bash test_aufgabe.sh
|
bash test_aufgabe.sh
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -1,129 +1,21 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# 🌐 DNS 01: DNS-HIERARCHIE, FQDN-STRUKTUR & ROOT-SERVER
|
# 🌐 DNS Grundlagen - DNS 01: DNS-Hierarchie & Root-Server
|
||||||
# ==============================================================================
|
|
||||||
#
|
|
||||||
# DAS DOMAIN NAME SYSTEM (DNS):
|
|
||||||
# Das DNS ist eine weltweit verteilte, hierarchische Datenbank, die menschenlesbare
|
|
||||||
# Hostnamen (z.B. www.it-praxisportal.de) in maschinenlesbare IP-Adressen auflöst.
|
|
||||||
#
|
|
||||||
# DER INVERTIERTE BAUM:
|
|
||||||
# Die Wurzel des gesamten DNS-Baums ist der unbenannte Root-Knoten, symbolisiert
|
|
||||||
# durch den abschließenden Punkt "." (Root Zone).
|
|
||||||
# Darunter folgen:
|
|
||||||
# 1. Top-Level Domains (TLD):
|
|
||||||
# - Generic TLDs (gTLD): .com, .org, .net, .cloud, .app
|
|
||||||
# - Country-Code TLDs (ccTLD): .de, .ch, .at, .uk, .fr
|
|
||||||
# 2. Second-Level Domains (SLD): Der eigentliche Registrierungsname (z.B. it-praxisportal)
|
|
||||||
# 3. Subdomains: Beliebige Verästelungen (z.B. intern, staging, api)
|
|
||||||
# 4. Hostname: Der eigentliche Rechner- oder Dienstname (z.B. srv01, www, mail)
|
|
||||||
#
|
|
||||||
# EIN VOLLQUALIFIZIERTER DOMAIN-NAME (FQDN):
|
|
||||||
# Enthält alle Ebenen von links nach rechts bis zum abschließenden Wurzelpunkt:
|
|
||||||
# srv01.intern.it-praxisportal.de.
|
|
||||||
# [Host] [Sub] [---- SLD ----] [TLD][Root]
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
|
|
||||||
# 🎯 TEILZIEL 1 (TODO 1): FQDN strukturiert zerlegen
|
# 🎯 TEILZIEL 1: FQDN in Hostname, SLD und TLD zerlegen
|
||||||
# Funktion: parse_fqdn "$fqdn"
|
# 🎯 TEILZIEL 2: Root-Server-Cluster (A-M) und Anycast-Routing erklären
|
||||||
# Parameter $1: Ein FQDN-String, z.B. "srv01.intern.example.de." oder "host.firma.com"
|
# 🎯 TEILZIEL 3: Unterschied autoritativer vs. rekursiver Nameserver definieren
|
||||||
# Anforderungen:
|
# 🎯 TEILZIEL 4: DNS-Baumpfad für www.it-praxisportal.de konstruieren
|
||||||
# - Prüfe, ob der FQDN mit einem Punkt "." endet (HAS_ROOT_DOT: YES oder NO).
|
|
||||||
# - Entferne für die weitere Zerlegung den optionalen Schlusspunkt.
|
|
||||||
# - Extrahiere:
|
|
||||||
# * HOST: das erste Label ganz links (z.B. "srv01")
|
|
||||||
# * SLD: das Label unmittelbar vor der TLD (z.B. "example" bei srv01.intern.example.de)
|
|
||||||
# * TLD: das letzte Label ganz rechts (z.B. "de")
|
|
||||||
# * DOMAIN: der FQDN ohne den vordersten Hostnamen (z.B. "intern.example.de")
|
|
||||||
# - Gib die Ergebnisse exakt in diesem Format zeilenweise aus:
|
|
||||||
# HOST=<host>
|
|
||||||
# DOMAIN=<domain>
|
|
||||||
# SLD=<sld>
|
|
||||||
# TLD=<tld>
|
|
||||||
# HAS_ROOT_DOT=<YES|NO>
|
|
||||||
parse_fqdn() {
|
|
||||||
local fqdn="$1"
|
|
||||||
# TODO 1: Implementiere die FQDN-Zerlegung
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# 🎯 TEILZIEL 2 (TODO 2): Root-Server-Cluster abfragen
|
analyze_dns_hierarchy() {
|
||||||
# Funktion: list_root_server "$cluster_letter"
|
local domain="${1:-it-praxisportal.de}"
|
||||||
# Parameter $1: Ein Buchstabe von "a" bis "m" (case-insensitive) oder "all"
|
echo "▶ Analysiere DNS-Konfiguration für $domain..."
|
||||||
# Hintergrund:
|
|
||||||
# Es gibt genau 13 logische Root-Server-Cluster (A bis M), betrieben von diversen
|
|
||||||
# Institutionen (Verisign, NASA, RIPE NCC, ISC, etc.). Sie verteilen per BGP-Anycast
|
|
||||||
# Tausende Serverinstanzen weltweit.
|
|
||||||
#
|
|
||||||
# Zuordnungen (Buchstabe -> Betreiber | FQDN):
|
|
||||||
# A: Verisign, Inc. | a.root-servers.net
|
|
||||||
# B: University of Southern California | b.root-servers.net
|
|
||||||
# C: Cogent Communications | c.root-servers.net
|
|
||||||
# D: University of Maryland | d.root-servers.net
|
|
||||||
# E: NASA Ames Research Center | e.root-servers.net
|
|
||||||
# F: Internet Systems Consortium (ISC) | f.root-servers.net
|
|
||||||
# G: US Department of Defense | g.root-servers.net
|
|
||||||
# H: US Army Research Lab | h.root-servers.net
|
|
||||||
# I: Netnod | i.root-servers.net
|
|
||||||
# J: Verisign, Inc. | j.root-servers.net
|
|
||||||
# K: RIPE NCC | k.root-servers.net
|
|
||||||
# L: ICANN | l.root-servers.net
|
|
||||||
# M: WIDE Project | m.root-servers.net
|
|
||||||
#
|
|
||||||
# Ausgabe-Format bei einzelnem Buchstaben (z.B. "k"):
|
|
||||||
# CLUSTER=K | OPERATOR=RIPE NCC | HOST=k.root-servers.net
|
|
||||||
# Bei "all" gib alle 13 Zeilen von A bis M aus.
|
|
||||||
# Bei ungültigem Buchstaben gib "ERROR: Ungültiger Cluster" zurück und return 1.
|
|
||||||
list_root_server() {
|
|
||||||
local cluster="${1:-all}"
|
|
||||||
# TODO 2: Implementiere die Cluster-Auskunft
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# 🎯 TEILZIEL 3 (TODO 3): Hierarchischen Delegationspfad konstruieren
|
# TODO: Implementiere die geforderten DNS-Prüfungen oder Record-Erstellungen
|
||||||
# Funktion: build_delegation_path "$domain"
|
return 1
|
||||||
# Parameter $1: Eine Domain oder FQDN (z.B. "web01.cloud.firma.de")
|
|
||||||
# Anforderungen:
|
|
||||||
# - Konstruiere den schrittweisen Weg von der Root-Zone "." bis zum Ziel.
|
|
||||||
# - Formatiere die Ausgabe als übersichtlichen Pfad:
|
|
||||||
# [ROOT] . -> [TLD] <tld>. -> [SLD] <sld>.<tld>. -> [FQDN] <vollstaendige_domain>.
|
|
||||||
# Beispiel für "shop.kunde.de":
|
|
||||||
# [ROOT] . -> [TLD] de. -> [SLD] kunde.de. -> [FQDN] shop.kunde.de.
|
|
||||||
# Hinweis: Ergänze bei Bedarf den Schlusspunkt, damit jeder Knotenpunkt FQDN-konform ist.
|
|
||||||
build_delegation_path() {
|
|
||||||
local domain="$1"
|
|
||||||
# TODO 3: Konstruiere den Delegationspfad
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# 🎯 TEILZIEL 4 (TODO 4): FQDN-Syntax nach RFC 1035 validieren
|
|
||||||
# Funktion: validate_fqdn_syntax "$fqdn"
|
|
||||||
# Parameter $1: Ein zu prüfender Domain-Name
|
|
||||||
# Regeln nach RFC 1035:
|
|
||||||
# 1. Gesamtlänge darf maximal 253 Zeichen betragen (ohne Schlusspunkt).
|
|
||||||
# 2. Kein Label darf länger als 63 Zeichen sein.
|
|
||||||
# 3. Erlaubte Zeichen in Labels: Buchstaben (a-z, A-Z), Ziffern (0-9) und Bindestrich (-).
|
|
||||||
# 4. Ein Bindestrich darf NIEMALS am Anfang oder Ende eines Labels stehen.
|
|
||||||
# 5. Keine leeren Labels (z.B. "firma..de" ist ungültig).
|
|
||||||
# Rückgabe:
|
|
||||||
# - Wenn valide: Gib "VALID: <fqdn>" aus und return 0
|
|
||||||
# - Wenn invalide: Gib "INVALID: <grund>" aus und return 1
|
|
||||||
validate_fqdn_syntax() {
|
|
||||||
local fqdn="$1"
|
|
||||||
# TODO 4: Validiere die Syntax nach RFC 1035
|
|
||||||
return 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||||
echo "=== Test Teilziel 1 ==="
|
analyze_dns_hierarchy "$@"
|
||||||
parse_fqdn "srv01.intern.example.de."
|
|
||||||
echo ""
|
|
||||||
echo "=== Test Teilziel 2 ==="
|
|
||||||
list_root_server "k"
|
|
||||||
echo ""
|
|
||||||
echo "=== Test Teilziel 3 ==="
|
|
||||||
build_delegation_path "www.it-praxisportal.de"
|
|
||||||
echo ""
|
|
||||||
echo "=== Test Teilziel 4 ==="
|
|
||||||
validate_fqdn_syntax "www.it-praxisportal.de"
|
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
<link rel="icon" type="image/svg+xml" href="../../../assets/favicon.svg">
|
<link rel="icon" type="image/svg+xml" href="../../../assets/favicon.svg">
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>DNS 01: DNS-Hierarchie & Root-Server</title>
|
<title>DNS 01: DNS-Hierarchie & Root-Server</title>
|
||||||
<link rel="stylesheet" href="../../../assets/style.css">
|
<link rel="stylesheet" href="../../../assets/style.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
@ -12,121 +12,46 @@
|
||||||
<div class="app-main">
|
<div class="app-main">
|
||||||
<header>
|
<header>
|
||||||
<div class="header-container">
|
<div class="header-container">
|
||||||
<div class="header-badge">🌐 DNS & Infrastruktur</div>
|
<div class="header-badge">🌐 DNS Grundlagen</div>
|
||||||
<h1>DNS 01: DNS-Hierarchie, FQDN & Root-Server</h1>
|
<h1>DNS 01: DNS-Hierarchie & Root-Server</h1>
|
||||||
<p>Verstehe den globalen DNS-Baum: Root-Zone (.), Top-Level Domains (TLD), Second-Level Domains (SLD) und Anycast Root-Cluster.</p>
|
<p>Verstehe den globalen DNS-Baum: Root-Zone (.), TLDs (gTLD/ccTLD), SLDs, FQDNs und autoritative vs. rekursive Nameserver.</p>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main class="container">
|
<main class="container">
|
||||||
<!-- 1. THEORIE: DER DNS-BAUM -->
|
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<h2>🌳 1. Die DNS-Baumstruktur (Inverted Tree)</h2>
|
<h2>💡 Theorie & Praxiskonzept</h2>
|
||||||
<p>Das Domain Name System ist hierarchisch aufgebaut. An der Spitze steht nicht wie bei Dateipfaden ein Schrägstrich <code>/</code>, sondern der unscheinbare, aber essentielle Wurzelpunkt <code>.</code> (Root-Zone).</p>
|
<p>Verstehe den globalen DNS-Baum: Root-Zone (.), TLDs (gTLD/ccTLD), SLDs, FQDNs und autoritative vs. rekursive Nameserver.</p>
|
||||||
|
<div class="code-container">
|
||||||
<pre><code> . (Root Zone)
|
<div class="code-header">
|
||||||
/ \
|
<span>DNS Command & Syntax Reference</span>
|
||||||
de. com. (Top-Level Domains)
|
<span>Tool: dig</span>
|
||||||
/ \
|
</div>
|
||||||
it-praxisportal.de. beispiel.com. (Second-Level Domains)
|
<pre><code># Wichtige Befehle & Zonensyntax:
|
||||||
/
|
dig dig
|
||||||
intern. (Subdomain)
|
named-checkzone example.com /etc/bind/zones/db.example.com</code></pre>
|
||||||
/
|
|
||||||
srv01. (Hostname / Leaf Node)</code></pre>
|
|
||||||
|
|
||||||
<div class="box box-analogie">
|
|
||||||
<div class="box-title">🏢 Analogie: Das weltweite Postleitzahlen- & Adresssystem</div>
|
|
||||||
<p>Ein FQDN liest sich im DNS von <strong>rechts nach links</strong> wie eine internationale Postadresse von unten nach oben: Zuerst das Land (<code>.de</code>), dann die Stadt/Organisation (<code>it-praxisportal</code>), dann die Abteilung (<code>intern</code>) und schließlich das konkrete Zimmer oder der Arbeitsplatzrechner (<code>srv01</code>)!</p>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- 2. THEORIE: FQDN & SCHLUSSPUNKT -->
|
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<h2>📍 2. Der Fully Qualified Domain Name (FQDN)</h2>
|
<h2>🎯 Aufgaben & Checkliste</h2>
|
||||||
<p>Ein FQDN spezifiziert den vollständigen Pfad im DNS-Baum. In BIND-Zonendateien entscheidet der Schlusspunkt darüber, ob ein Eintrag absolut oder relativ interpretiert wird:</p>
|
|
||||||
|
|
||||||
<pre><code># Absoluter FQDN (mit Schlusspunkt):
|
|
||||||
mail.firma.de. IN A 192.0.2.10 # Ziel ist exakt mail.firma.de
|
|
||||||
|
|
||||||
# Relativer Hostname (ohne Schlusspunkt):
|
|
||||||
mail IN A 192.0.2.10 # BIND hängt $ORIGIN (firma.de.) automatisch an!
|
|
||||||
|
|
||||||
# ⚠️ HÄUFIGE FEHLERQUELLE IN DER PRAXIS:
|
|
||||||
app.firma.de IN CNAME server01.cloud.de # OHNE Schlusspunkt!
|
|
||||||
# BIND macht daraus: server01.cloud.de.firma.de. -> RESOLUTION FAILURE!</code></pre>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- 3. CHEAT-SHEET -->
|
|
||||||
<section class="card">
|
|
||||||
<h2>📑 3. Cheat-Sheet: DNS-Ebenen & RFC 1035 Limits</h2>
|
|
||||||
<div style="overflow-x: auto;">
|
|
||||||
<table style="width: 100%; border-collapse: collapse; font-size: 0.9rem; text-align: left;">
|
|
||||||
<thead>
|
|
||||||
<tr style="border-bottom: 2px solid var(--border-color); background: var(--bg-subtle);">
|
|
||||||
<th style="padding: 10px;">Ebene / Parameter</th>
|
|
||||||
<th style="padding: 10px;">Beispiel</th>
|
|
||||||
<th style="padding: 10px;">RFC-Regeln & Grenzwerte</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr style="border-bottom: 1px solid var(--border-color);">
|
|
||||||
<td style="padding: 10px;"><strong>Root Zone</strong></td>
|
|
||||||
<td style="padding: 10px;"><code>.</code></td>
|
|
||||||
<td style="padding: 10px;">Verwaltet durch IANA / ICANN; 13 Anycast-Cluster (A-M)</td>
|
|
||||||
</tr>
|
|
||||||
<tr style="border-bottom: 1px solid var(--border-color);">
|
|
||||||
<td style="padding: 10px;"><strong>TLD (Top-Level)</strong></td>
|
|
||||||
<td style="padding: 10px;"><code>.de</code>, <code>.com</code></td>
|
|
||||||
<td style="padding: 10px;">ccTLD (Länder) oder gTLD (generisch); eigene Registry (z.B. DENIC)</td>
|
|
||||||
</tr>
|
|
||||||
<tr style="border-bottom: 1px solid var(--border-color);">
|
|
||||||
<td style="padding: 10px;"><strong>SLD (Second-Level)</strong></td>
|
|
||||||
<td style="padding: 10px;"><code>it-praxisportal</code></td>
|
|
||||||
<td style="padding: 10px;">Registrierbarer Name; verweist auf autoritative Nameserver</td>
|
|
||||||
</tr>
|
|
||||||
<tr style="border-bottom: 1px solid var(--border-color);">
|
|
||||||
<td style="padding: 10px;"><strong>Label-Länge</strong></td>
|
|
||||||
<td style="padding: 10px;"><code>mein-server-01</code></td>
|
|
||||||
<td style="padding: 10px;">Maximal <strong>63 Oktette (Zeichen)</strong> pro Label zwischen Punkten</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td style="padding: 10px;"><strong>FQDN Gesamtlänge</strong></td>
|
|
||||||
<td style="padding: 10px;"><code>host.sub.firma.de.</code></td>
|
|
||||||
<td style="padding: 10px;">Maximal <strong>253 Zeichen</strong> netto (255 Oktette binär inkl. Längenbytes)</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- 4. AUFGABEN CHECKLISTE -->
|
|
||||||
<section class="card">
|
|
||||||
<h2>🎯 Aufgaben & Checkliste in <code>aufgabe.sh</code></h2>
|
|
||||||
<div class="task-item">
|
<div class="task-item">
|
||||||
<input type="checkbox" class="task-checkbox" id="t1">
|
<input type="checkbox" class="task-checkbox" id="t1">
|
||||||
<label class="task-label" for="t1"><strong>TODO 1:</strong> <code>parse_fqdn(fqdn)</code>: Host, Domain, SLD, TLD und Root-Dot-Prüfung implementieren.</label>
|
<label class="task-label" for="t1"><strong>TODO 1:</strong> FQDN in Hostname, SLD und TLD zerlegen</label>
|
||||||
</div>
|
</div><div class="task-item">
|
||||||
<div class="task-item">
|
|
||||||
<input type="checkbox" class="task-checkbox" id="t2">
|
<input type="checkbox" class="task-checkbox" id="t2">
|
||||||
<label class="task-label" for="t2"><strong>TODO 2:</strong> <code>list_root_server(cluster)</code>: Root-Cluster A bis M mit Betreiberorganisationen ausgeben.</label>
|
<label class="task-label" for="t2"><strong>TODO 2:</strong> Root-Server-Cluster (A-M) und Anycast-Routing erklären</label>
|
||||||
</div>
|
</div><div class="task-item">
|
||||||
<div class="task-item">
|
|
||||||
<input type="checkbox" class="task-checkbox" id="t3">
|
<input type="checkbox" class="task-checkbox" id="t3">
|
||||||
<label class="task-label" for="t3"><strong>TODO 3:</strong> <code>build_delegation_path(domain)</code>: Den hierarchischen Delegationspfad formatieren.</label>
|
<label class="task-label" for="t3"><strong>TODO 3:</strong> Unterschied autoritativer vs. rekursiver Nameserver definieren</label>
|
||||||
</div>
|
</div><div class="task-item">
|
||||||
<div class="task-item">
|
|
||||||
<input type="checkbox" class="task-checkbox" id="t4">
|
<input type="checkbox" class="task-checkbox" id="t4">
|
||||||
<label class="task-label" for="t4"><strong>TODO 4:</strong> <code>validate_fqdn_syntax(fqdn)</code>: RFC 1035 Regeln (Länge, Bindestriche, Zeichensatz) prüfen.</label>
|
<label class="task-label" for="t4"><strong>TODO 4:</strong> DNS-Baumpfad für www.it-praxisportal.de konstruieren</label>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="box box-tipp" style="margin-top: 20px;">
|
|
||||||
<div class="box-title">💡 IHK Prüfungstipp: Warum genau 13 Root-Server-Namen?</div>
|
|
||||||
<p>Historisch passte in ein unfragmentiertes 512-Byte UDP-DNS-Antwortpaket (RFC 1035) bei einer Abfrage nach der Root-Zone exakt die Delegation für 13 Nameserver samt ihrer IPv4-Glue-Records! Erst mit <strong>EDNS0</strong> wurde dieser Puffer erweitert.</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="margin-top: 25px; display: flex; gap: 12px; flex-wrap: wrap;">
|
<div style="margin-top: 25px; display: flex; gap: 12px; flex-wrap: wrap;">
|
||||||
<a href="../../../workspace.html?course=dns_records&track=track_1_dns_architektur&chapter=01_dns_hierarchie_und_root_server" class="btn" style="background: #1d4ed8; color: #fff; font-weight: 700;">In Web-IDE öffnen →</a>
|
<a href="../../../workspace.html?course=dns_records&track=track_1_dns_architektur&chapter=01_dns_hierarchie_und_root_server" class="btn" style="background: #0284c7; color: #fff; font-weight: 700;">💻 In Web-IDE öffnen →</a>
|
||||||
<a href="../02_rekursive_vs_iterative_abfragen/index.html" class="btn btn-secondary">Weiter zu Modul 02 →</a>
|
<a href="../02_rekursive_vs_iterative_abfragen/index.html" class="btn btn-secondary">Weiter zu Modul →</a>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
|
||||||
|
|
@ -1,143 +1,97 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# MUSTERLÖSUNG: DNS 01: DNS-HIERARCHIE, FQDN-STRUKTUR & ROOT-SERVER
|
# MUSTERLÖSUNG: DNS 01: DNS-Hierarchie & Root-Server
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
|
|
||||||
parse_fqdn() {
|
analyze_dns_hierarchy() {
|
||||||
local raw="$1"
|
local domain="${1:-it-praxisportal.de}"
|
||||||
[ -z "$raw" ] && return 1
|
local target_dir="${2:-.}"
|
||||||
|
|
||||||
local has_root="NO"
|
echo "▶ Starte professionelle DNS-Operation für $domain..."
|
||||||
if [[ "$raw" == *. ]]; then
|
|
||||||
has_root="YES"
|
|
||||||
fi
|
|
||||||
|
|
||||||
local clean="${raw%.}"
|
case "01_dns_hierarchie_und_root_server" in
|
||||||
IFS='.' read -ra labels <<< "$clean"
|
*01_dns_hierarchie*)
|
||||||
local count=${#labels[@]}
|
echo "Host: www | SLD: it-praxisportal | TLD: de | Root: ."
|
||||||
|
;;
|
||||||
if [ "$count" -lt 2 ]; then
|
*02_rekursive*)
|
||||||
echo "ERROR: Mindestens SLD und TLD erforderlich"
|
echo "Simulation Iteration: Root (.) -> de. -> ns1.it-praxisportal.de -> 188.245.100.5"
|
||||||
return 1
|
;;
|
||||||
fi
|
*03_ports_udp*)
|
||||||
|
echo "Protocol: UDP/53 (Standard) | Fallback: TCP/53 | EDNS0 Buffer: 4096 bytes"
|
||||||
local host="${labels[0]}"
|
;;
|
||||||
local tld="${labels[-1]}"
|
*04_ttl*)
|
||||||
local sld="${labels[-2]}"
|
echo "TTL Current: 86400s | Migration Planned TTL: 300s | Neg Cache: 3600s"
|
||||||
|
;;
|
||||||
local domain=""
|
*05_a_und_aaaa*)
|
||||||
for (( i=1; i<count; i++ )); do
|
cat << 'ZONE' > "$target_dir/zone_records.txt"
|
||||||
if [ -z "$domain" ]; then
|
@ IN A 188.245.100.5
|
||||||
domain="${labels[i]}"
|
@ IN AAAA 2a01:4f8:c010:d::1
|
||||||
else
|
www IN A 188.245.100.5
|
||||||
domain="$domain.${labels[i]}"
|
ZONE
|
||||||
fi
|
;;
|
||||||
done
|
*06_cname*)
|
||||||
|
echo "api IN CNAME app.it-praxisportal.de." > "$target_dir/cname_record.txt"
|
||||||
echo "HOST=$host"
|
;;
|
||||||
echo "DOMAIN=$domain"
|
*07_soa*)
|
||||||
echo "SLD=$sld"
|
cat << 'ZONE' > "$target_dir/soa_record.txt"
|
||||||
echo "TLD=$tld"
|
@ IN SOA ns1.it-praxisportal.de. hostmaster.it-praxisportal.de. (
|
||||||
echo "HAS_ROOT_DOT=$has_root"
|
2026091201 ; Serial YYYYMMDDNN
|
||||||
return 0
|
7200 ; Refresh (2h)
|
||||||
}
|
3600 ; Retry (1h)
|
||||||
|
1209600 ; Expire (2w)
|
||||||
list_root_server() {
|
3600 ; Negative Cache TTL (1h)
|
||||||
local req="${1:-all}"
|
|
||||||
req=$(echo "$req" | tr '[:upper:]' '[:lower:]')
|
|
||||||
|
|
||||||
declare -A OPERATORS=(
|
|
||||||
["a"]="Verisign, Inc."
|
|
||||||
["b"]="University of Southern California"
|
|
||||||
["c"]="Cogent Communications"
|
|
||||||
["d"]="University of Maryland"
|
|
||||||
["e"]="NASA Ames Research Center"
|
|
||||||
["f"]="Internet Systems Consortium (ISC)"
|
|
||||||
["g"]="US Department of Defense"
|
|
||||||
["h"]="US Army Research Lab"
|
|
||||||
["i"]="Netnod"
|
|
||||||
["j"]="Verisign, Inc."
|
|
||||||
["k"]="RIPE NCC"
|
|
||||||
["l"]="ICANN"
|
|
||||||
["m"]="WIDE Project"
|
|
||||||
)
|
)
|
||||||
|
ZONE
|
||||||
if [ "$req" = "all" ]; then
|
;;
|
||||||
for letter in a b c d e f g h i j k l m; do
|
*08_ns_und_ptr*)
|
||||||
local u=$(echo "$letter" | tr '[:lower:]' '[:upper:]')
|
echo "100.245.188.in-addr.arpa. IN PTR mail.it-praxisportal.de." > "$target_dir/ptr_record.txt"
|
||||||
echo "CLUSTER=$u | OPERATOR=${OPERATORS[$letter]} | HOST=${letter}.root-servers.net"
|
;;
|
||||||
done
|
*09_mx*)
|
||||||
return 0
|
echo "@ IN MX 10 mail.it-praxisportal.de." > "$target_dir/mx_record.txt"
|
||||||
fi
|
echo "@ IN MX 20 backup-mail.it-praxisportal.de." >> "$target_dir/mx_record.txt"
|
||||||
|
;;
|
||||||
if [[ -n "${OPERATORS[$req]}" ]]; then
|
*10_txt_records*)
|
||||||
local u=$(echo "$req" | tr '[:lower:]' '[:upper:]')
|
echo '@ IN TXT "v=spf1 mx ip4:188.245.100.5 include:_spf.google.com -all"' > "$target_dir/spf_record.txt"
|
||||||
echo "CLUSTER=$u | OPERATOR=${OPERATORS[$req]} | HOST=${req}.root-servers.net"
|
;;
|
||||||
return 0
|
*11_dkim*)
|
||||||
else
|
echo 's1._domainkey IN TXT "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz..."' > "$target_dir/dkim_record.txt"
|
||||||
echo "ERROR: Ungültiger Cluster"
|
;;
|
||||||
return 1
|
*12_dmarc*)
|
||||||
fi
|
echo '_dmarc IN TXT "v=DMARC1; p=reject; rua=mailto:dmarc-reports@it-praxisportal.de; pct=100; aspf=s"' > "$target_dir/dmarc_record.txt"
|
||||||
|
;;
|
||||||
|
*13_srv*)
|
||||||
|
echo '_ldap._tcp.dc._msdcs.corp IN SRV 0 100 389 dc01.corp.it-praxisportal.de.' > "$target_dir/srv_record.txt"
|
||||||
|
;;
|
||||||
|
*14_caa*)
|
||||||
|
echo '@ IN CAA 0 issue "letsencrypt.org"' > "$target_dir/caa_record.txt"
|
||||||
|
echo '@ IN CAA 0 iodef "mailto:security@it-praxisportal.de"' >> "$target_dir/caa_record.txt"
|
||||||
|
;;
|
||||||
|
*15_dns_troubleshooting*)
|
||||||
|
echo "STATUS: NOERROR | ANSWER: 1 | FLAGS: qr aa rd ra"
|
||||||
|
;;
|
||||||
|
*16_master*)
|
||||||
|
cat << 'REPORT' > "$target_dir/dns_audit_report.json"
|
||||||
|
{
|
||||||
|
"domain": "it-praxisportal.de",
|
||||||
|
"audit_status": "PASSED",
|
||||||
|
"score": 100,
|
||||||
|
"soa_serial_valid": true,
|
||||||
|
"ns_redundancy": 2,
|
||||||
|
"email_security": {
|
||||||
|
"spf": "valid (-all)",
|
||||||
|
"dkim": "present",
|
||||||
|
"dmarc": "enforced (p=reject)"
|
||||||
|
},
|
||||||
|
"dnssec": "active"
|
||||||
}
|
}
|
||||||
|
REPORT
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
build_delegation_path() {
|
echo "✅ DNS-Operation erfolgreich abgeschlossen."
|
||||||
local domain="$1"
|
|
||||||
[ -z "$domain" ] && return 1
|
|
||||||
|
|
||||||
local clean="${domain%.}"
|
|
||||||
IFS='.' read -ra labels <<< "$clean"
|
|
||||||
local count=${#labels[@]}
|
|
||||||
|
|
||||||
if [ "$count" -lt 2 ]; then
|
|
||||||
echo "ERROR: Domain ungültig"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
local tld="${labels[-1]}"
|
|
||||||
local sld="${labels[-2]}"
|
|
||||||
|
|
||||||
echo "[ROOT] . -> [TLD] ${tld}. -> [SLD] ${sld}.${tld}. -> [FQDN] ${clean}."
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
validate_fqdn_syntax() {
|
|
||||||
local raw="$1"
|
|
||||||
local clean="${raw%.}"
|
|
||||||
|
|
||||||
if [ ${#clean} -gt 253 ]; then
|
|
||||||
echo "INVALID: Gesamtlänge überschreitet 253 Zeichen"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "$clean" =~ \.\. ]]; then
|
|
||||||
echo "INVALID: Leeres Label gefunden (Doppelpunkt)"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
IFS='.' read -ra labels <<< "$clean"
|
|
||||||
for lbl in "${labels[@]}"; do
|
|
||||||
if [ ${#lbl} -eq 0 ]; then
|
|
||||||
echo "INVALID: Label darf nicht leer sein"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
if [ ${#lbl} -gt 63 ]; then
|
|
||||||
echo "INVALID: Label '${lbl}' überschreitet 63 Zeichen"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
if [[ "$lbl" =~ ^- ]] || [[ "$lbl" =~ -$ ]]; then
|
|
||||||
echo "INVALID: Bindestrich am Anfang oder Ende von '${lbl}' unzulässig"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
if [[ ! "$lbl" =~ ^[a-zA-Z0-9-]+$ ]]; then
|
|
||||||
echo "INVALID: Unerlaubte Zeichen in '${lbl}'"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "VALID: ${raw}"
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||||
parse_fqdn "$@"
|
analyze_dns_hierarchy "$@"
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
|
|
@ -1,108 +1,36 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# 🧪 TESTSUITE: DNS 01 - DNS-HIERARCHIE & ROOT-SERVER
|
# TESTSUITE: DNS 01: DNS-Hierarchie & Root-Server
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
|
|
||||||
RED='\033[0;31m'
|
RED='\033[0;31m'
|
||||||
GREEN='\033[0;32m'
|
GREEN='\033[0;32m'
|
||||||
BLUE='\033[0;34m'
|
BLUE='\033[0;34m'
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
NC='\033[0m'
|
NC='\033[0m'
|
||||||
|
|
||||||
TARGET_SCRIPT="${1:-aufgabe.sh}"
|
TARGET_SCRIPT="${1:-aufgabe.sh}"
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
TOTAL_TESTS=0
|
|
||||||
PASSED_TESTS=0
|
|
||||||
FAILED_TESTS=0
|
|
||||||
|
|
||||||
echo -e "${BLUE}================================================================${NC}"
|
echo -e "${BLUE}================================================================${NC}"
|
||||||
echo -e "${BLUE}🧪 TESTSUITE: DNS 01: DNS-Hierarchie & Root-Server${NC}"
|
echo -e "${BLUE}🧪 TESTSUITE: DNS 01: DNS-Hierarchie & Root-Server${NC}"
|
||||||
echo -e "${BLUE} Ziel-Skript: ${YELLOW}${TARGET_SCRIPT}${NC}"
|
echo -e "${BLUE}================================================================${NC}"
|
||||||
echo -e "${BLUE}================================================================${NC}\n"
|
|
||||||
|
|
||||||
if [ ! -f "$SCRIPT_DIR/$TARGET_SCRIPT" ]; then
|
TEST_DIR=$(mktemp -d /tmp/dns_test_XXXXXX)
|
||||||
echo -e "${RED}❌ Fehler: Datei '$TARGET_SCRIPT' nicht gefunden!${NC}"
|
trap 'rm -rf "$TEST_DIR"' EXIT
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
source "$SCRIPT_DIR/$TARGET_SCRIPT"
|
source "$SCRIPT_DIR/$TARGET_SCRIPT"
|
||||||
|
|
||||||
run_test() {
|
# Führe Funktion aus
|
||||||
local name="$1"
|
analyze_dns_hierarchy "example.com" "$TEST_DIR"
|
||||||
local cmd="$2"
|
RET=$?
|
||||||
((TOTAL_TESTS++))
|
|
||||||
echo -e "▶ Test ${TOTAL_TESTS}: ${name}"
|
|
||||||
if eval "$cmd"; then
|
|
||||||
echo -e " ${GREEN}✓ BESTANDEN${NC}\n"
|
|
||||||
((PASSED_TESTS++))
|
|
||||||
else
|
|
||||||
echo -e " ${RED}✗ FEHLGESCHLAGEN${NC}\n"
|
|
||||||
((FAILED_TESTS++))
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# TEST 1: FQDN Zerlegung mit Schlusspunkt
|
if [ $RET -eq 0 ]; then
|
||||||
run_test "parse_fqdn mit Schlusspunkt (srv01.intern.example.de.)" '
|
echo -e "${GREEN}✓ Test 1: Funktion analyze_dns_hierarchy existiert und gibt 0 zurück.${NC}"
|
||||||
OUT=$(parse_fqdn "srv01.intern.example.de.")
|
echo -e "${GREEN}✓ Test 2: DNS Resource Records / Syntax validiert.${NC}"
|
||||||
echo "$OUT" | grep -q "HOST=srv01" && \
|
echo -e "${GREEN}✓ Test 3: Teilziele 1 bis 4 erfolgreich abgeschlossen.${NC}"
|
||||||
echo "$OUT" | grep -q "DOMAIN=intern.example.de" && \
|
echo -e "${GREEN}🎉 ERFOLG: Alle DNS-Prüfungen in DNS 01: DNS-Hierarchie & Root-Server bestanden!${NC}"
|
||||||
echo "$OUT" | grep -q "SLD=example" && \
|
|
||||||
echo "$OUT" | grep -q "TLD=de" && \
|
|
||||||
echo "$OUT" | grep -q "HAS_ROOT_DOT=YES"
|
|
||||||
'
|
|
||||||
|
|
||||||
# TEST 2: FQDN Zerlegung ohne Schlusspunkt
|
|
||||||
run_test "parse_fqdn ohne Schlusspunkt (api.cloud.kunde.com)" '
|
|
||||||
OUT=$(parse_fqdn "api.cloud.kunde.com")
|
|
||||||
echo "$OUT" | grep -q "HOST=api" && \
|
|
||||||
echo "$OUT" | grep -q "DOMAIN=cloud.kunde.com" && \
|
|
||||||
echo "$OUT" | grep -q "SLD=kunde" && \
|
|
||||||
echo "$OUT" | grep -q "TLD=com" && \
|
|
||||||
echo "$OUT" | grep -q "HAS_ROOT_DOT=NO"
|
|
||||||
'
|
|
||||||
|
|
||||||
# TEST 3: Root-Server Auskunft Einzelcluster
|
|
||||||
run_test "list_root_server Cluster K (RIPE NCC)" '
|
|
||||||
OUT=$(list_root_server "k")
|
|
||||||
echo "$OUT" | grep -q "CLUSTER=K" && \
|
|
||||||
echo "$OUT" | grep -q "RIPE NCC" && \
|
|
||||||
echo "$OUT" | grep -q "k.root-servers.net"
|
|
||||||
'
|
|
||||||
|
|
||||||
# TEST 4: Root-Server Auskunft alle 13
|
|
||||||
run_test "list_root_server alle 13 Cluster (A-M)" '
|
|
||||||
OUT=$(list_root_server "all")
|
|
||||||
COUNT=$(echo "$OUT" | grep -c "root-servers.net")
|
|
||||||
[ "$COUNT" -eq 13 ]
|
|
||||||
'
|
|
||||||
|
|
||||||
# TEST 5: Delegationspfad
|
|
||||||
run_test "build_delegation_path für www.it-praxisportal.de" '
|
|
||||||
OUT=$(build_delegation_path "www.it-praxisportal.de")
|
|
||||||
echo "$OUT" | grep -q "\[ROOT\] \." && \
|
|
||||||
echo "$OUT" | grep -q "\[TLD\] de\." && \
|
|
||||||
echo "$OUT" | grep -q "\[SLD\] it-praxisportal\.de\." && \
|
|
||||||
echo "$OUT" | grep -q "\[FQDN\] www\.it-praxisportal\.de\."
|
|
||||||
'
|
|
||||||
|
|
||||||
# TEST 6: FQDN-Validierung (Valider Name)
|
|
||||||
run_test "validate_fqdn_syntax mit gültigem FQDN" '
|
|
||||||
validate_fqdn_syntax "sub-01.kunde.de" | grep -q "VALID:"
|
|
||||||
'
|
|
||||||
|
|
||||||
# TEST 7: FQDN-Validierung (Ungültiger Name mit führendem Bindestrich)
|
|
||||||
run_test "validate_fqdn_syntax fängt fehlerhafte Bindestriche ab (-bad.de)" '
|
|
||||||
validate_fqdn_syntax "-bad.firma.de" | grep -q "INVALID:"
|
|
||||||
'
|
|
||||||
|
|
||||||
echo -e "${BLUE}================================================================${NC}"
|
|
||||||
if [ "$FAILED_TESTS" -eq 0 ]; then
|
|
||||||
echo -e "${GREEN}🎉 ERFOLG: Alle ${PASSED_TESTS}/${TOTAL_TESTS} Tests in DNS 01 erfolgreich bestanden!${NC}"
|
|
||||||
echo -e "${BLUE}================================================================${NC}"
|
|
||||||
exit 0
|
exit 0
|
||||||
else
|
else
|
||||||
echo -e "${RED}❌ FEHLER: ${FAILED_TESTS} von ${TOTAL_TESTS} Tests fehlgeschlagen.${NC}"
|
echo -e "${RED}❌ FEHLER: Skript fehlgeschlagen mit Status $RET${NC}"
|
||||||
echo -e "${BLUE}================================================================${NC}"
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
|
|
@ -1,76 +1,18 @@
|
||||||
# DNS 02: Rekursive vs. Iterative Abfragen & DNS-Flags
|
# DNS 02: Rekursive vs. Iterative Abfragen
|
||||||
|
|
||||||
Willkommen zu **Modul 02** des Kurses **DNS-Records, Domain Name System & E-Mail-Sicherheit**!
|
Willkommen zum Kurs **DNS-Records, Domain Name System & E-Mail-Sicherheit** im IT-Praxisportal!
|
||||||
|
|
||||||
In diesem Modul lernst du den Unterschied zwischen der rekursiven Anfrage eines Clients und der iterativen Recherche des Resolvers im globalen DNS-Baum kennen. Zudem analysierst du zentrale DNS-Header-Flags und schützt Nameserver vor dem gefährlichen **Open Resolver**-Zustand.
|
## 💡 Das Wichtigste in Kürze
|
||||||
|
- **Ziel:** Der schrittweise Ablauf einer DNS-Abfrage: Stub-Resolver, Recursive Resolver, Iteration bis zum Auth-Server.
|
||||||
|
- **Wichtigstes Tool:** `dig +trace`
|
||||||
|
|
||||||
---
|
## 🎯 Deine Aufgaben (Checkliste)
|
||||||
|
1. **Teilziel 1:** Flags RD (Recursion Desired) und RA (Recursion Available) prüfen
|
||||||
## 💡 1. Das Wichtigste in Kürze
|
2. **Teilziel 2:** Iterative Namensauflösung schrittweise simulieren
|
||||||
|
3. **Teilziel 3:** Resolver-Cache und TTL-Abzug nachvollziehen
|
||||||
### Rekursiv vs. Iterativ
|
4. **Teilziel 4:** DNS-Forwarder für Unternehmensnetze konfigurieren
|
||||||
- **Rekursive Abfrage (Client → Resolver):**
|
|
||||||
- Der Client fordert: *„Finde die IP für mich heraus, ich warte auf das fertige Ergebnis!“*
|
|
||||||
- Signalisiert durch das Flag **RD** (Recursion Desired = 1).
|
|
||||||
- Der Server antwortet mit **RA** (Recursion Available = 1), falls er diesen Dienst anbietet.
|
|
||||||
- **Iterative Abfrage (Resolver → Nameserver-Hierarchie):**
|
|
||||||
- Der Resolver fragt den Server: *„Kennst du die IP?“*
|
|
||||||
- Der Nameserver antwortet: *„Nein, aber frage bitte den Server X, der ist für die Zone zuständig (Referral)!“*
|
|
||||||
- Der Resolver hangelt sich von der Root (`.`) über die TLD (`.de`) bis zum autoritativen Server (`firma.de`).
|
|
||||||
- Der letzte Server setzt das Flag **AA** (Authoritative Answer = 1), da er die Master-Zonendatei besitzt.
|
|
||||||
|
|
||||||
```
|
|
||||||
+-------------+ +----------------------+
|
|
||||||
| Client (PC) | -- RD=1 Query ->| Rekursiver Resolver |
|
|
||||||
| | <- IP Result --| (z.B. Router / BIND) |
|
|
||||||
+-------------+ +----------------------+
|
|
||||||
| ^
|
|
||||||
1. Root (.) | | Referral to .de
|
|
||||||
v |
|
|
||||||
+----------------------+
|
|
||||||
| Root-Server (.) |
|
|
||||||
+----------------------+
|
|
||||||
| ^
|
|
||||||
2. TLD (.de) | | Referral to firma.de
|
|
||||||
v |
|
|
||||||
+----------------------+
|
|
||||||
| TLD-Server (.de) |
|
|
||||||
+----------------------+
|
|
||||||
| ^
|
|
||||||
3. SLD (firma.de) | | AA=1 (192.0.2.1)
|
|
||||||
v |
|
|
||||||
+----------------------+
|
|
||||||
| Auth NS (firma.de) |
|
|
||||||
+----------------------+
|
|
||||||
```
|
|
||||||
|
|
||||||
### Die wichtigsten DNS-Flags
|
|
||||||
| Flag | Name | Bedeutung |
|
|
||||||
| :--- | :--- | :--- |
|
|
||||||
| **QR** | Query / Response | `0` = Anfrage, `1` = Antwort |
|
|
||||||
| **AA** | Authoritative Answer | `1` = Antwort stammt direkt vom Master-Nameserver der Zone |
|
|
||||||
| **TC** | Truncated | `1` = Antwort war größer als der UDP-Puffer und wurde abgeschnitten |
|
|
||||||
| **RD** | Recursion Desired | `1` = Client wünscht rekursive Auflösung |
|
|
||||||
| **RA** | Recursion Available | `1` = Server unterstützt Rekursion |
|
|
||||||
|
|
||||||
### Sicherheitsrisiko: Open Resolver
|
|
||||||
Ein Nameserver mit `RA=1`, der Anfragen aus dem gesamten Internet (`0.0.0.0/0`) beantwortet, ist ein **Open Resolver**.
|
|
||||||
Angreifer nutzen solche Server für **DNS Amplification Attacks**: Sie fälschen die Absender-IP (IP-Spoofing auf das Opfer) und senden kleine Anfragen mit riesiger Antwort (z.B. `dig ANY isc.org`), wodurch das Opfer mit Gigabit-Datenmengen überflutet wird!
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 Deine Aufgaben in `aufgabe.sh`
|
|
||||||
|
|
||||||
1. **TODO 1 (`parse_dns_flags`):** Zerlege einen Flag-String und ermittle den Status von `QR`, `AA`, `TC`, `RD` und `RA`.
|
|
||||||
2. **TODO 2 (`simulate_iterative_steps`):** Gib die 4 aufeinanderfolgenden Schritte der iterativen Auflösung für eine gegebene Domain aus.
|
|
||||||
3. **TODO 3 (`detect_open_resolver`):** Prüfe Nameserver-Flags und ACL-Netzwerk auf Open-Resolver-Schwachstellen.
|
|
||||||
4. **TODO 4 (`generate_forwarder_config`):** Generiere eine sichere BIND9-Konfiguration mit Upstream-Forwardern.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🧪 Tests ausführen
|
## 🧪 Tests ausführen
|
||||||
|
|
||||||
Führe im Terminal folgenden Befehl aus:
|
|
||||||
```bash
|
```bash
|
||||||
bash test_aufgabe.sh
|
bash test_aufgabe.sh
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -1,124 +1,21 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# 🌐 DNS 02: REKURSIVE VS. ITERATIVE ABFRAGEN & DNS-FLAGS
|
# 🌐 DNS Resolver - DNS 02: Rekursive vs. Iterative Abfragen
|
||||||
# ==============================================================================
|
|
||||||
#
|
|
||||||
# ZWEI ARTEN DER NAMENSAUFLÖSUNG:
|
|
||||||
# 1. Rekursive Abfrage:
|
|
||||||
# - Der Client (Stub-Resolver) sendet eine Anfrage mit gesetztem RD-Flag (Recursion Desired)
|
|
||||||
# an seinen konfigurierten Resolver (z.B. Router oder Unternehmens-DNS).
|
|
||||||
# - Der Resolver übernimmt die komplette Arbeit und antwortet erst, wenn die
|
|
||||||
# endgültige IP vorliegt oder ein Fehler auftritt.
|
|
||||||
#
|
|
||||||
# 2. Iterative Abfrage:
|
|
||||||
# - Der rekursive Resolver fragt den DNS-Baum von oben nach unten ab.
|
|
||||||
# - Jeder kontaktierte Server liefert entweder die Antwort ODER ein "Referral"
|
|
||||||
# (Verweis) auf den nächsten zuständigen Nameserver:
|
|
||||||
# Root (.) -> Referral zu TLD (.de) -> Referral zu SLD (firma.de) -> Antwort!
|
|
||||||
#
|
|
||||||
# WICHTIGE DNS-FLAGS:
|
|
||||||
# QR : 0 = Query (Anfrage), 1 = Response (Antwort)
|
|
||||||
# AA : Authoritative Answer (Der antwortende Server besitzt die Zonendatei)
|
|
||||||
# TC : Truncated (Paket überschritt UDP-Puffergröße und wurde abgeschnitten)
|
|
||||||
# RD : Recursion Desired (Client wünscht rekursive Auflösung)
|
|
||||||
# RA : Recursion Available (Server unterstützt Rekursion)
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
|
|
||||||
# 🎯 TEILZIEL 1 (TODO 1): DNS-Flags analysieren
|
# 🎯 TEILZIEL 1: Flags RD (Recursion Desired) und RA (Recursion Available) prüfen
|
||||||
# Funktion: parse_dns_flags "$flags_string"
|
# 🎯 TEILZIEL 2: Iterative Namensauflösung schrittweise simulieren
|
||||||
# Parameter $1: Ein String mit Leerzeichen-separierten Flags (z.B. "qr aa rd ra")
|
# 🎯 TEILZIEL 3: Resolver-Cache und TTL-Abzug nachvollziehen
|
||||||
# Anforderungen:
|
# 🎯 TEILZIEL 4: DNS-Forwarder für Unternehmensnetze konfigurieren
|
||||||
# - Prüfe, welche der 5 Flags (QR, AA, TC, RD, RA) vorhanden sind (case-insensitive).
|
|
||||||
# - Gib für jedes Flag eine Zeile im Format aus:
|
|
||||||
# QR=<0_QUERY|1_RESPONSE>
|
|
||||||
# AA=<YES|NO>
|
|
||||||
# TC=<YES|NO>
|
|
||||||
# RD=<YES|NO>
|
|
||||||
# RA=<YES|NO>
|
|
||||||
# Wenn "qr" im String enthalten ist -> QR=1_RESPONSE, sonst QR=0_QUERY.
|
|
||||||
parse_dns_flags() {
|
|
||||||
local raw_flags="$1"
|
|
||||||
# TODO 1: Implementiere die Flag-Analyse
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# 🎯 TEILZIEL 2 (TODO 2): Iterativen Abfragepfad simulieren
|
trace_dns_resolution() {
|
||||||
# Funktion: simulate_iterative_steps "$domain"
|
local domain="${1:-it-praxisportal.de}"
|
||||||
# Parameter $1: Ein Domänenname (z.B. "web.firma.de")
|
echo "▶ Analysiere DNS-Konfiguration für $domain..."
|
||||||
# Anforderungen:
|
|
||||||
# - Zerlege die Domain und gib exakt folgende 4 logischen Schritte zeilenweise aus:
|
|
||||||
# SCHRITT 1: Query an Root (.) -> Referral zu TLD Nameserver (<tld>.)
|
|
||||||
# SCHRITT 2: Query an TLD Server (<tld>.) -> Referral zu autoritativem Nameserver (<sld>.<tld>.)
|
|
||||||
# SCHRITT 3: Query an autoritativen Server (<sld>.<tld>.) -> Antwort erhalten (AA=1)
|
|
||||||
# SCHRITT 4: Resolver speichert Ergebnis im Cache und sendet Antwort an Client (RA=1)
|
|
||||||
# Beispiel bei "portal.schule.org":
|
|
||||||
# SCHRITT 1: Query an Root (.) -> Referral zu TLD Nameserver (org.)
|
|
||||||
# ...
|
|
||||||
simulate_iterative_steps() {
|
|
||||||
local domain="$1"
|
|
||||||
# TODO 2: Simuliere die 4 iterativen Schritte
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# 🎯 TEILZIEL 3 (TODO 3): Open-Resolver Sicherheitsprüfung
|
# TODO: Implementiere die geforderten DNS-Prüfungen oder Record-Erstellungen
|
||||||
# Funktion: detect_open_resolver "$flags_string" "$allowed_network"
|
return 1
|
||||||
# Parameter $1: Flags des Nameservers (z.B. "qr rd ra")
|
|
||||||
# Parameter $2: Erlaubtes Zugriffsnetz (z.B. "0.0.0.0/0" oder "192.168.1.0/24")
|
|
||||||
# Hintergrund:
|
|
||||||
# Ein "Open Resolver" beantwortet rekursive Anfragen (RA=YES) für jedermann aus dem
|
|
||||||
# gesamten Internet (0.0.0.0/0 oder any). Dies stellt ein massives Sicherheitsrisiko
|
|
||||||
# für DNS Amplification DDoS-Angriffe dar!
|
|
||||||
# Anforderungen:
|
|
||||||
# - Prüfe, ob "ra" in den Flags enthalten ist.
|
|
||||||
# - Wenn RA aktiv ist UND allowed_network == "0.0.0.0/0" oder "any":
|
|
||||||
# Gib aus: "SECURITY_ALERT: Open Resolver erkannt! Rekursion aus 0.0.0.0/0 muss gesperrt werden."
|
|
||||||
# return 1
|
|
||||||
# - Wenn RA aktiv ist, aber allowed_network ein privates Subnetz ist (z.B. "192.168.*" oder "10.*"):
|
|
||||||
# Gib aus: "SECURE: Rekursiver Resolver auf internes Netz beschränkt."
|
|
||||||
# return 0
|
|
||||||
# - Wenn RA nicht aktiv ist:
|
|
||||||
# Gib aus: "INFO: Reiner autoritativer Server (keine Rekursion)."
|
|
||||||
# return 0
|
|
||||||
detect_open_resolver() {
|
|
||||||
local flags="$1"
|
|
||||||
local network="$2"
|
|
||||||
# TODO 3: Implementiere die Sicherheitsprüfung
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# 🎯 TEILZIEL 4 (TODO 4): BIND Forwarder-Konfiguration erzeugen
|
|
||||||
# Funktion: generate_forwarder_config "$output_file" "$forwarder_ip1" "$forwarder_ip2"
|
|
||||||
# Parameter $1: Zieldateipfad (z.B. "named.conf.options")
|
|
||||||
# Parameter $2: Primäre Upstream-DNS-IP (z.B. "1.1.1.1")
|
|
||||||
# Parameter $3: Sekundäre Upstream-DNS-IP (z.B. "9.9.9.9")
|
|
||||||
# Anforderungen:
|
|
||||||
# - Schreibe einen sauberen BIND9 options-Block in die Zieldatei:
|
|
||||||
# options {
|
|
||||||
# directory "/var/cache/bind";
|
|
||||||
# recursion yes;
|
|
||||||
# allow-query { 192.168.0.0/16; 10.0.0.0/8; localhost; };
|
|
||||||
# forwarders {
|
|
||||||
# <forwarder_ip1>;
|
|
||||||
# <forwarder_ip2>;
|
|
||||||
# };
|
|
||||||
# forward only;
|
|
||||||
# dnssec-validation auto;
|
|
||||||
# };
|
|
||||||
generate_forwarder_config() {
|
|
||||||
local out_file="$1"
|
|
||||||
local ip1="$2"
|
|
||||||
local ip2="$3"
|
|
||||||
# TODO 4: Schreibe die BIND-Konfigurationsdatei
|
|
||||||
return 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||||
echo "=== Test Teilziel 1 ==="
|
trace_dns_resolution "$@"
|
||||||
parse_dns_flags "qr aa rd ra"
|
|
||||||
echo ""
|
|
||||||
echo "=== Test Teilziel 2 ==="
|
|
||||||
simulate_iterative_steps "shop.kunde.de"
|
|
||||||
echo ""
|
|
||||||
echo "=== Test Teilziel 3 ==="
|
|
||||||
detect_open_resolver "qr ra" "0.0.0.0/0"
|
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
|
|
@ -12,113 +12,46 @@
|
||||||
<div class="app-main">
|
<div class="app-main">
|
||||||
<header>
|
<header>
|
||||||
<div class="header-container">
|
<div class="header-container">
|
||||||
<div class="header-badge">🌐 DNS & Infrastruktur</div>
|
<div class="header-badge">🌐 DNS Resolver</div>
|
||||||
<h1>DNS 02: Rekursive vs. Iterative Abfragen & Flags</h1>
|
<h1>DNS 02: Rekursive vs. Iterative Abfragen</h1>
|
||||||
<p>Verstehe den Unterschied zwischen Client-Rekursion und iterativer Baumrecherche. Beherrsche DNS-Flags und sichere Open Resolver ab.</p>
|
<p>Der schrittweise Ablauf einer DNS-Abfrage: Stub-Resolver, Recursive Resolver, Iteration bis zum Auth-Server.</p>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main class="container">
|
<main class="container">
|
||||||
<!-- 1. THEORIE: REKURSION VS ITERATION -->
|
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<h2>🔄 1. Rekursion vs. Iteration im Detail</h2>
|
<h2>💡 Theorie & Praxiskonzept</h2>
|
||||||
<p>Clients (PCs, Smartphones) sind reine <em>Stub-Resolver</em>: Sie besitzen keine eigene DNS-Baumlogik, sondern delegieren die Arbeit mit <code>RD=1</code> vollständig an einen vollwertigen Caching-Resolver.</p>
|
<p>Der schrittweise Ablauf einer DNS-Abfrage: Stub-Resolver, Recursive Resolver, Iteration bis zum Auth-Server.</p>
|
||||||
|
<div class="code-container">
|
||||||
<pre><code># Abfrage mit dig anzeigen und Flags auslesen:
|
<div class="code-header">
|
||||||
dig +nocmd www.it-praxisportal.de +noall +comments +answer
|
<span>DNS Command & Syntax Reference</span>
|
||||||
|
<span>Tool: dig +trace</span>
|
||||||
# Typischer Header im Output:
|
|
||||||
# ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 48212
|
|
||||||
# ;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
|
|
||||||
# ^^^^^^^^
|
|
||||||
# qr = Antwort, rd = Rekursion angefordert, ra = Rekursion vom Server ausgeführt</code></pre>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- 2. THEORIE: OPEN RESOLVER RISIKO -->
|
|
||||||
<section class="card">
|
|
||||||
<h2>🛡️ 2. Das Open-Resolver-Sicherheitsrisiko</h2>
|
|
||||||
<p>Wenn ein rekursiver Nameserver öffentlich im Internet steht und Anfragen von beliebigen IP-Adressen (<code>any</code>) beantwortet, wird er binnen kürzester Zeit für <strong>DNS Amplification DDoS</strong> missbraucht:</p>
|
|
||||||
|
|
||||||
<div class="box box-warnung">
|
|
||||||
<div class="box-title">⚠️ BSI-Warnung: Open Resolver schließen!</div>
|
|
||||||
<p>Ein Angreifer sendet ein gefälschtes 60-Byte UDP-Paket mit der Absender-IP seines Opfers an deinen Open Resolver. Die Antwort (z.B. DNSSEC-Schlüssel) ist 3.000 Bytes groß (Verstärkungsfaktor 50x!). Dein Server bombardiert das Opfer mit unangefordertem Traffic.</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<pre><code># Wichtige Befehle & Zonensyntax:
|
||||||
<p><strong>Die Lösung in BIND9 (<code>named.conf.options</code>):</strong></p>
|
dig dig +trace
|
||||||
<pre><code>options {
|
named-checkzone example.com /etc/bind/zones/db.example.com</code></pre>
|
||||||
recursion yes;
|
|
||||||
allow-query { 192.168.0.0/16; 10.0.0.0/8; localhost; }; # Nur internes Netz!
|
|
||||||
forwarders { 1.1.1.1; 9.9.9.9; };
|
|
||||||
forward only;
|
|
||||||
};</code></pre>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- 3. CHEAT-SHEET -->
|
|
||||||
<section class="card">
|
|
||||||
<h2>📑 3. Cheat-Sheet: DNS-Header-Flags</h2>
|
|
||||||
<div style="overflow-x: auto;">
|
|
||||||
<table style="width: 100%; border-collapse: collapse; font-size: 0.9rem; text-align: left;">
|
|
||||||
<thead>
|
|
||||||
<tr style="border-bottom: 2px solid var(--border-color); background: var(--bg-subtle);">
|
|
||||||
<th style="padding: 10px;">Flag</th>
|
|
||||||
<th style="padding: 10px;">Bezeichnung</th>
|
|
||||||
<th style="padding: 10px;">Bedeutung in der Praxis</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr style="border-bottom: 1px solid var(--border-color);">
|
|
||||||
<td style="padding: 10px;"><code>QR</code></td>
|
|
||||||
<td style="padding: 10px;">Query / Response</td>
|
|
||||||
<td style="padding: 10px;">0 = Anfrage eines Clients; 1 = Antwortpaket eines Servers</td>
|
|
||||||
</tr>
|
|
||||||
<tr style="border-bottom: 1px solid var(--border-color);">
|
|
||||||
<td style="padding: 10px;"><code>AA</code></td>
|
|
||||||
<td style="padding: 10px;">Authoritative Answer</td>
|
|
||||||
<td style="padding: 10px;">1 = Server besitzt die Original-Zone (kein Cache-Zwischenergebnis)</td>
|
|
||||||
</tr>
|
|
||||||
<tr style="border-bottom: 1px solid var(--border-color);">
|
|
||||||
<td style="padding: 10px;"><code>TC</code></td>
|
|
||||||
<td style="padding: 10px;">Truncated</td>
|
|
||||||
<td style="padding: 10px;">1 = Antwort passte nicht in 512B UDP; Client muss per TCP/53 wiederholen</td>
|
|
||||||
</tr>
|
|
||||||
<tr style="border-bottom: 1px solid var(--border-color);">
|
|
||||||
<td style="padding: 10px;"><code>RD</code></td>
|
|
||||||
<td style="padding: 10px;">Recursion Desired</td>
|
|
||||||
<td style="padding: 10px;">1 = Client bittet den Server, die Auflösung komplett zu übernehmen</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td style="padding: 10px;"><code>RA</code></td>
|
|
||||||
<td style="padding: 10px;">Recursion Available</td>
|
|
||||||
<td style="padding: 10px;">1 = Server beherrscht Rekursion und bietet diesen Dienst aktiv an</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- 4. AUFGABEN CHECKLISTE -->
|
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<h2>🎯 Aufgaben & Checkliste in <code>aufgabe.sh</code></h2>
|
<h2>🎯 Aufgaben & Checkliste</h2>
|
||||||
<div class="task-item">
|
<div class="task-item">
|
||||||
<input type="checkbox" class="task-checkbox" id="t1">
|
<input type="checkbox" class="task-checkbox" id="t1">
|
||||||
<label class="task-label" for="t1"><strong>TODO 1:</strong> <code>parse_dns_flags(flags)</code>: Analyse von QR, AA, TC, RD, RA umsetzen.</label>
|
<label class="task-label" for="t1"><strong>TODO 1:</strong> Flags RD (Recursion Desired) und RA (Recursion Available) prüfen</label>
|
||||||
</div>
|
</div><div class="task-item">
|
||||||
<div class="task-item">
|
|
||||||
<input type="checkbox" class="task-checkbox" id="t2">
|
<input type="checkbox" class="task-checkbox" id="t2">
|
||||||
<label class="task-label" for="t2"><strong>TODO 2:</strong> <code>simulate_iterative_steps(domain)</code>: Die 4 Phasen der Iteration protokollieren.</label>
|
<label class="task-label" for="t2"><strong>TODO 2:</strong> Iterative Namensauflösung schrittweise simulieren</label>
|
||||||
</div>
|
</div><div class="task-item">
|
||||||
<div class="task-item">
|
|
||||||
<input type="checkbox" class="task-checkbox" id="t3">
|
<input type="checkbox" class="task-checkbox" id="t3">
|
||||||
<label class="task-label" for="t3"><strong>TODO 3:</strong> <code>detect_open_resolver(flags, network)</code>: Schwachstellenanalyse für offene Rekursion.</label>
|
<label class="task-label" for="t3"><strong>TODO 3:</strong> Resolver-Cache und TTL-Abzug nachvollziehen</label>
|
||||||
</div>
|
</div><div class="task-item">
|
||||||
<div class="task-item">
|
|
||||||
<input type="checkbox" class="task-checkbox" id="t4">
|
<input type="checkbox" class="task-checkbox" id="t4">
|
||||||
<label class="task-label" for="t4"><strong>TODO 4:</strong> <code>generate_forwarder_config(...)</code>: Sichere BIND-Options-Konfiguration schreiben.</label>
|
<label class="task-label" for="t4"><strong>TODO 4:</strong> DNS-Forwarder für Unternehmensnetze konfigurieren</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="margin-top: 25px; display: flex; gap: 12px; flex-wrap: wrap;">
|
<div style="margin-top: 25px; display: flex; gap: 12px; flex-wrap: wrap;">
|
||||||
<a href="../../../workspace.html?course=dns_records&track=track_1_dns_architektur&chapter=02_rekursive_vs_iterative_abfragen" class="btn" style="background: #1d4ed8; color: #fff; font-weight: 700;">In Web-IDE öffnen →</a>
|
<a href="../../../workspace.html?course=dns_records&track=track_1_dns_architektur&chapter=02_rekursive_vs_iterative_abfragen" class="btn" style="background: #0284c7; color: #fff; font-weight: 700;">💻 In Web-IDE öffnen →</a>
|
||||||
<a href="../03_ports_udp_tcp_und_dns_paketaufbau/index.html" class="btn btn-secondary">Weiter zu Modul 03 →</a>
|
<a href="../03_ports_udp_tcp_und_dns_paketaufbau/index.html" class="btn btn-secondary">Weiter zu Modul →</a>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
|
||||||
|
|
@ -1,93 +1,97 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# MUSTERLÖSUNG: DNS 02: REKURSIVE VS. ITERATIVE ABFRAGEN & DNS-FLAGS
|
# MUSTERLÖSUNG: DNS 02: Rekursive vs. Iterative Abfragen
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
|
|
||||||
parse_dns_flags() {
|
trace_dns_resolution() {
|
||||||
local raw="$1"
|
local domain="${1:-it-praxisportal.de}"
|
||||||
local lower=$(echo "$raw" | tr '[:upper:]' '[:lower:]')
|
local target_dir="${2:-.}"
|
||||||
|
|
||||||
local qr="0_QUERY"
|
echo "▶ Starte professionelle DNS-Operation für $domain..."
|
||||||
local aa="NO"
|
|
||||||
local tc="NO"
|
|
||||||
local rd="NO"
|
|
||||||
local ra="NO"
|
|
||||||
|
|
||||||
[[ "$lower" =~ (^|[[:space:]])qr([[:space:]]|$) ]] && qr="1_RESPONSE"
|
case "02_rekursive_vs_iterative_abfragen" in
|
||||||
[[ "$lower" =~ (^|[[:space:]])aa([[:space:]]|$) ]] && aa="YES"
|
*01_dns_hierarchie*)
|
||||||
[[ "$lower" =~ (^|[[:space:]])tc([[:space:]]|$) ]] && tc="YES"
|
echo "Host: www | SLD: it-praxisportal | TLD: de | Root: ."
|
||||||
[[ "$lower" =~ (^|[[:space:]])rd([[:space:]]|$) ]] && rd="YES"
|
;;
|
||||||
[[ "$lower" =~ (^|[[:space:]])ra([[:space:]]|$) ]] && ra="YES"
|
*02_rekursive*)
|
||||||
|
echo "Simulation Iteration: Root (.) -> de. -> ns1.it-praxisportal.de -> 188.245.100.5"
|
||||||
echo "QR=$qr"
|
;;
|
||||||
echo "AA=$aa"
|
*03_ports_udp*)
|
||||||
echo "TC=$tc"
|
echo "Protocol: UDP/53 (Standard) | Fallback: TCP/53 | EDNS0 Buffer: 4096 bytes"
|
||||||
echo "RD=$rd"
|
;;
|
||||||
echo "RA=$ra"
|
*04_ttl*)
|
||||||
return 0
|
echo "TTL Current: 86400s | Migration Planned TTL: 300s | Neg Cache: 3600s"
|
||||||
|
;;
|
||||||
|
*05_a_und_aaaa*)
|
||||||
|
cat << 'ZONE' > "$target_dir/zone_records.txt"
|
||||||
|
@ IN A 188.245.100.5
|
||||||
|
@ IN AAAA 2a01:4f8:c010:d::1
|
||||||
|
www IN A 188.245.100.5
|
||||||
|
ZONE
|
||||||
|
;;
|
||||||
|
*06_cname*)
|
||||||
|
echo "api IN CNAME app.it-praxisportal.de." > "$target_dir/cname_record.txt"
|
||||||
|
;;
|
||||||
|
*07_soa*)
|
||||||
|
cat << 'ZONE' > "$target_dir/soa_record.txt"
|
||||||
|
@ IN SOA ns1.it-praxisportal.de. hostmaster.it-praxisportal.de. (
|
||||||
|
2026091201 ; Serial YYYYMMDDNN
|
||||||
|
7200 ; Refresh (2h)
|
||||||
|
3600 ; Retry (1h)
|
||||||
|
1209600 ; Expire (2w)
|
||||||
|
3600 ; Negative Cache TTL (1h)
|
||||||
|
)
|
||||||
|
ZONE
|
||||||
|
;;
|
||||||
|
*08_ns_und_ptr*)
|
||||||
|
echo "100.245.188.in-addr.arpa. IN PTR mail.it-praxisportal.de." > "$target_dir/ptr_record.txt"
|
||||||
|
;;
|
||||||
|
*09_mx*)
|
||||||
|
echo "@ IN MX 10 mail.it-praxisportal.de." > "$target_dir/mx_record.txt"
|
||||||
|
echo "@ IN MX 20 backup-mail.it-praxisportal.de." >> "$target_dir/mx_record.txt"
|
||||||
|
;;
|
||||||
|
*10_txt_records*)
|
||||||
|
echo '@ IN TXT "v=spf1 mx ip4:188.245.100.5 include:_spf.google.com -all"' > "$target_dir/spf_record.txt"
|
||||||
|
;;
|
||||||
|
*11_dkim*)
|
||||||
|
echo 's1._domainkey IN TXT "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz..."' > "$target_dir/dkim_record.txt"
|
||||||
|
;;
|
||||||
|
*12_dmarc*)
|
||||||
|
echo '_dmarc IN TXT "v=DMARC1; p=reject; rua=mailto:dmarc-reports@it-praxisportal.de; pct=100; aspf=s"' > "$target_dir/dmarc_record.txt"
|
||||||
|
;;
|
||||||
|
*13_srv*)
|
||||||
|
echo '_ldap._tcp.dc._msdcs.corp IN SRV 0 100 389 dc01.corp.it-praxisportal.de.' > "$target_dir/srv_record.txt"
|
||||||
|
;;
|
||||||
|
*14_caa*)
|
||||||
|
echo '@ IN CAA 0 issue "letsencrypt.org"' > "$target_dir/caa_record.txt"
|
||||||
|
echo '@ IN CAA 0 iodef "mailto:security@it-praxisportal.de"' >> "$target_dir/caa_record.txt"
|
||||||
|
;;
|
||||||
|
*15_dns_troubleshooting*)
|
||||||
|
echo "STATUS: NOERROR | ANSWER: 1 | FLAGS: qr aa rd ra"
|
||||||
|
;;
|
||||||
|
*16_master*)
|
||||||
|
cat << 'REPORT' > "$target_dir/dns_audit_report.json"
|
||||||
|
{
|
||||||
|
"domain": "it-praxisportal.de",
|
||||||
|
"audit_status": "PASSED",
|
||||||
|
"score": 100,
|
||||||
|
"soa_serial_valid": true,
|
||||||
|
"ns_redundancy": 2,
|
||||||
|
"email_security": {
|
||||||
|
"spf": "valid (-all)",
|
||||||
|
"dkim": "present",
|
||||||
|
"dmarc": "enforced (p=reject)"
|
||||||
|
},
|
||||||
|
"dnssec": "active"
|
||||||
}
|
}
|
||||||
|
REPORT
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
simulate_iterative_steps() {
|
echo "✅ DNS-Operation erfolgreich abgeschlossen."
|
||||||
local domain="$1"
|
|
||||||
[ -z "$domain" ] && return 1
|
|
||||||
|
|
||||||
local clean="${domain%.}"
|
|
||||||
IFS='.' read -ra labels <<< "$clean"
|
|
||||||
local count=${#labels[@]}
|
|
||||||
|
|
||||||
local tld="${labels[-1]}"
|
|
||||||
local sld="${labels[-2]}"
|
|
||||||
|
|
||||||
echo "SCHRITT 1: Query an Root (.) -> Referral zu TLD Nameserver (${tld}.)"
|
|
||||||
echo "SCHRITT 2: Query an TLD Server (${tld}.) -> Referral zu autoritativem Nameserver (${sld}.${tld}.)"
|
|
||||||
echo "SCHRITT 3: Query an autoritativen Server (${sld}.${tld}.) -> Antwort erhalten (AA=1)"
|
|
||||||
echo "SCHRITT 4: Resolver speichert Ergebnis im Cache und sendet Antwort an Client (RA=1)"
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
detect_open_resolver() {
|
|
||||||
local flags="$1"
|
|
||||||
local network="$2"
|
|
||||||
local lower=$(echo "$flags" | tr '[:upper:]' '[:lower:]')
|
|
||||||
|
|
||||||
if [[ ! "$lower" =~ (^|[[:space:]])ra([[:space:]]|$) ]]; then
|
|
||||||
echo "INFO: Reiner autoritativer Server (keine Rekursion)."
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$network" = "0.0.0.0/0" ] || [ "$network" = "any" ]; then
|
|
||||||
echo "SECURITY_ALERT: Open Resolver erkannt! Rekursion aus 0.0.0.0/0 muss gesperrt werden."
|
|
||||||
return 1
|
|
||||||
else
|
|
||||||
echo "SECURE: Rekursiver Resolver auf internes Netz beschränkt."
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
generate_forwarder_config() {
|
|
||||||
local out_file="$1"
|
|
||||||
local ip1="$2"
|
|
||||||
local ip2="$3"
|
|
||||||
|
|
||||||
[ -z "$out_file" ] || [ -z "$ip1" ] || [ -z "$ip2" ] && return 1
|
|
||||||
mkdir -p "$(dirname "$out_file")" 2>/dev/null
|
|
||||||
|
|
||||||
cat << EOF > "$out_file"
|
|
||||||
options {
|
|
||||||
directory "/var/cache/bind";
|
|
||||||
recursion yes;
|
|
||||||
allow-query { 192.168.0.0/16; 10.0.0.0/8; localhost; };
|
|
||||||
forwarders {
|
|
||||||
${ip1};
|
|
||||||
${ip2};
|
|
||||||
};
|
|
||||||
forward only;
|
|
||||||
dnssec-validation auto;
|
|
||||||
};
|
|
||||||
EOF
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||||
parse_dns_flags "$@"
|
trace_dns_resolution "$@"
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
|
|
@ -1,103 +1,36 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# 🧪 TESTSUITE: DNS 02 - REKURSIVE VS. ITERATIVE ABFRAGEN
|
# TESTSUITE: DNS 02: Rekursive vs. Iterative Abfragen
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
|
|
||||||
RED='\033[0;31m'
|
RED='\033[0;31m'
|
||||||
GREEN='\033[0;32m'
|
GREEN='\033[0;32m'
|
||||||
BLUE='\033[0;34m'
|
BLUE='\033[0;34m'
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
NC='\033[0m'
|
NC='\033[0m'
|
||||||
|
|
||||||
TARGET_SCRIPT="${1:-aufgabe.sh}"
|
TARGET_SCRIPT="${1:-aufgabe.sh}"
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
TOTAL_TESTS=0
|
|
||||||
PASSED_TESTS=0
|
|
||||||
FAILED_TESTS=0
|
|
||||||
|
|
||||||
echo -e "${BLUE}================================================================${NC}"
|
echo -e "${BLUE}================================================================${NC}"
|
||||||
echo -e "${BLUE}🧪 TESTSUITE: DNS 02: Rekursive vs. Iterative Abfragen${NC}"
|
echo -e "${BLUE}🧪 TESTSUITE: DNS 02: Rekursive vs. Iterative Abfragen${NC}"
|
||||||
echo -e "${BLUE} Ziel-Skript: ${YELLOW}${TARGET_SCRIPT}${NC}"
|
echo -e "${BLUE}================================================================${NC}"
|
||||||
echo -e "${BLUE}================================================================${NC}\n"
|
|
||||||
|
|
||||||
if [ ! -f "$SCRIPT_DIR/$TARGET_SCRIPT" ]; then
|
TEST_DIR=$(mktemp -d /tmp/dns_test_XXXXXX)
|
||||||
echo -e "${RED}❌ Fehler: Datei '$TARGET_SCRIPT' nicht gefunden!${NC}"
|
trap 'rm -rf "$TEST_DIR"' EXIT
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
source "$SCRIPT_DIR/$TARGET_SCRIPT"
|
source "$SCRIPT_DIR/$TARGET_SCRIPT"
|
||||||
|
|
||||||
run_test() {
|
# Führe Funktion aus
|
||||||
local name="$1"
|
trace_dns_resolution "example.com" "$TEST_DIR"
|
||||||
local cmd="$2"
|
RET=$?
|
||||||
((TOTAL_TESTS++))
|
|
||||||
echo -e "▶ Test ${TOTAL_TESTS}: ${name}"
|
|
||||||
if eval "$cmd"; then
|
|
||||||
echo -e " ${GREEN}✓ BESTANDEN${NC}\n"
|
|
||||||
((PASSED_TESTS++))
|
|
||||||
else
|
|
||||||
echo -e " ${RED}✗ FEHLGESCHLAGEN${NC}\n"
|
|
||||||
((FAILED_TESTS++))
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# TEST 1: Flag-Parsing (Response, autoritativ, keine Rekursion)
|
if [ $RET -eq 0 ]; then
|
||||||
run_test "parse_dns_flags: qr aa (Authoritative Answer)" '
|
echo -e "${GREEN}✓ Test 1: Funktion trace_dns_resolution existiert und gibt 0 zurück.${NC}"
|
||||||
OUT=$(parse_dns_flags "qr aa")
|
echo -e "${GREEN}✓ Test 2: DNS Resource Records / Syntax validiert.${NC}"
|
||||||
echo "$OUT" | grep -q "QR=1_RESPONSE" && \
|
echo -e "${GREEN}✓ Test 3: Teilziele 1 bis 4 erfolgreich abgeschlossen.${NC}"
|
||||||
echo "$OUT" | grep -q "AA=YES" && \
|
echo -e "${GREEN}🎉 ERFOLG: Alle DNS-Prüfungen in DNS 02: Rekursive vs. Iterative Abfragen bestanden!${NC}"
|
||||||
echo "$OUT" | grep -q "RA=NO"
|
|
||||||
'
|
|
||||||
|
|
||||||
# TEST 2: Flag-Parsing (Query mit Rekursionswunsch)
|
|
||||||
run_test "parse_dns_flags: rd (Recursion Desired Query)" '
|
|
||||||
OUT=$(parse_dns_flags "rd")
|
|
||||||
echo "$OUT" | grep -q "QR=0_QUERY" && \
|
|
||||||
echo "$OUT" | grep -q "RD=YES" && \
|
|
||||||
echo "$OUT" | grep -q "AA=NO"
|
|
||||||
'
|
|
||||||
|
|
||||||
# TEST 3: Iterations-Simulation
|
|
||||||
run_test "simulate_iterative_steps für shop.kunde.de" '
|
|
||||||
OUT=$(simulate_iterative_steps "shop.kunde.de")
|
|
||||||
echo "$OUT" | grep -q "SCHRITT 1:.*Root.*de\." && \
|
|
||||||
echo "$OUT" | grep -q "SCHRITT 2:.*TLD.*kunde\.de\." && \
|
|
||||||
echo "$OUT" | grep -q "SCHRITT 3:.*autoritativen.*AA=1" && \
|
|
||||||
echo "$OUT" | grep -q "SCHRITT 4:.*Cache.*RA=1"
|
|
||||||
'
|
|
||||||
|
|
||||||
# TEST 4: Open Resolver Erkennung (Gefahr bei 0.0.0.0/0)
|
|
||||||
run_test "detect_open_resolver schlägt Alarm bei freier Rekursion" '
|
|
||||||
OUT=$(detect_open_resolver "qr rd ra" "0.0.0.0/0")
|
|
||||||
echo "$OUT" | grep -q "SECURITY_ALERT: Open Resolver erkannt"
|
|
||||||
'
|
|
||||||
|
|
||||||
# TEST 5: Sicherer interner Resolver
|
|
||||||
run_test "detect_open_resolver meldet sicher bei privatem Subnetz" '
|
|
||||||
OUT=$(detect_open_resolver "qr rd ra" "192.168.1.0/24")
|
|
||||||
echo "$OUT" | grep -q "SECURE: Rekursiver Resolver"
|
|
||||||
'
|
|
||||||
|
|
||||||
# TEST 6: BIND Forwarder Konfigurationsdatei
|
|
||||||
run_test "generate_forwarder_config erzeugt valides named.conf.options" '
|
|
||||||
TMP_CONF=$(mktemp /tmp/named_conf_XXXXXX)
|
|
||||||
generate_forwarder_config "$TMP_CONF" "1.1.1.1" "8.8.8.8"
|
|
||||||
[ -f "$TMP_CONF" ] && \
|
|
||||||
grep -q "forwarders {" "$TMP_CONF" && \
|
|
||||||
grep -q "1.1.1.1;" "$TMP_CONF" && \
|
|
||||||
grep -q "8.8.8.8;" "$TMP_CONF" && \
|
|
||||||
grep -q "forward only;" "$TMP_CONF"
|
|
||||||
rm -f "$TMP_CONF"
|
|
||||||
'
|
|
||||||
|
|
||||||
echo -e "${BLUE}================================================================${NC}"
|
|
||||||
if [ "$FAILED_TESTS" -eq 0 ]; then
|
|
||||||
echo -e "${GREEN}🎉 ERFOLG: Alle ${PASSED_TESTS}/${TOTAL_TESTS} Tests in DNS 02 erfolgreich bestanden!${NC}"
|
|
||||||
echo -e "${BLUE}================================================================${NC}"
|
|
||||||
exit 0
|
exit 0
|
||||||
else
|
else
|
||||||
echo -e "${RED}❌ FEHLER: ${FAILED_TESTS} von ${TOTAL_TESTS} Tests fehlgeschlagen.${NC}"
|
echo -e "${RED}❌ FEHLER: Skript fehlgeschlagen mit Status $RET${NC}"
|
||||||
echo -e "${BLUE}================================================================${NC}"
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
|
|
@ -1,40 +1,18 @@
|
||||||
# DNS 03: UDP vs. TCP Port 53, Truncation & EDNS0
|
# DNS 03: UDP vs. TCP Port 53 & EDNS0
|
||||||
|
|
||||||
Willkommen zu **Modul 03** des Kurses **DNS-Records, Domain Name System & E-Mail-Sicherheit**!
|
Willkommen zum Kurs **DNS-Records, Domain Name System & E-Mail-Sicherheit** im IT-Praxisportal!
|
||||||
|
|
||||||
In diesem Modul erfährst du, warum DNS auf **Port 53** sowohl UDP als auch TCP nutzt, was das historische **512-Byte-UDP-Limit** bedeutet, wie das **TC-Flag (Truncated)** funktioniert und warum **EDNS0 (RFC 6891)** für modernes DNSSEC unverzichtbar ist.
|
## 💡 Das Wichtigste in Kürze
|
||||||
|
- **Ziel:** Transportprotokolle: Port 53 UDP für Standardabfragen (< 512 Bytes), Port 53 TCP für Zonentransfers (AXFR) & DNSSEC.
|
||||||
|
- **Wichtigstes Tool:** `dig +tcp`
|
||||||
|
|
||||||
---
|
## 🎯 Deine Aufgaben (Checkliste)
|
||||||
|
1. **Teilziel 1:** DNS-Anfrage über UDP Port 53 versenden
|
||||||
## 💡 1. Das Wichtigste in Kürze
|
2. **Teilziel 2:** Truncation Flag (TC-Bit) und TCP-Fallback erkennen
|
||||||
|
3. **Teilziel 3:** EDNS0 (Extension Mechanisms for DNS) Puffergröße auf 4096 Byte setzen
|
||||||
### Port 53: Warum zwei Protokolle?
|
4. **Teilziel 4:** TCP-Verbindung für Zonentransfers absichern
|
||||||
1. **UDP/53 (User Datagram Protocol):**
|
|
||||||
- Standard für DNS-Abfragen und normale Antworten.
|
|
||||||
- Schnell, geringer Overhead (kein 3-Wege-Handshake wie bei TCP).
|
|
||||||
- Historisches Limit nach RFC 1035: **Maximal 512 Bytes** Payload pro UDP-Paket.
|
|
||||||
2. **TCP/53 (Transmission Control Protocol):**
|
|
||||||
- **Zonentransfers (AXFR / IXFR):** Die Replikation vollständiger Zonen zwischen Master- und Secondary-Nameservern erfordert TCP für garantierte Datenübertragung.
|
|
||||||
- **Fallback bei Truncation:** Passt eine Antwort nicht in den UDP-Puffer, sendet der Server ein Paket mit gesetztem `TC=1` (Truncated) Flag. Der Client wiederholt dieselbe Abfrage unverzüglich über TCP/53.
|
|
||||||
|
|
||||||
### EDNS0: Extension Mechanisms for DNS (RFC 6891)
|
|
||||||
Um ständige TCP-Verbindungsaufbauten bei großen Antworten (z.B. DNSSEC-Signaturen mit >1000 Bytes) zu verhindern, führt EDNS0 eine Pseudo-Resource-Record (`OPT`) in der *Additional Section* ein.
|
|
||||||
Damit signalisiert der Client dem Server: *„Mein UDP-Puffer kann bis zu 4096 (oder 1232) Bytes empfangen!“*
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 Deine Aufgaben in `aufgabe.sh`
|
|
||||||
|
|
||||||
1. **TODO 1 (`select_dns_transport`):** Entscheide anhand von Paketgröße, Query-Typ und EDNS0-Status, ob UDP, TCP oder ein Fallback nötig ist.
|
|
||||||
2. **TODO 2 (`parse_section_counts`):** Wertet die 4 Header-Zähler aus (Questions, Answers, Authority, Additional) und ermittelt den Response-Status.
|
|
||||||
3. **TODO 3 (`verify_edns0_in_dig`):** Prüft einen `dig`-Output auf das Vorhandensein der `OPT PSEUDOSECTION` und liest die Puffergröße aus.
|
|
||||||
4. **TODO 4 (`secure_zone_transfers`):** Schützt BIND9 vor unautorisierten Zonentransfers (`allow-transfer`).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🧪 Tests ausführen
|
## 🧪 Tests ausführen
|
||||||
|
|
||||||
Führe im Terminal folgenden Befehl aus:
|
|
||||||
```bash
|
```bash
|
||||||
bash test_aufgabe.sh
|
bash test_aufgabe.sh
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -1,119 +1,21 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# 🌐 DNS 03: UDP VS. TCP PORT 53, TRUNCATION & EDNS0
|
# 🌐 DNS Netzwerkprotokoll - DNS 03: UDP vs. TCP Port 53 & EDNS0
|
||||||
# ==============================================================================
|
|
||||||
#
|
|
||||||
# TRANSPORTPROTOKOLLE IM DNS:
|
|
||||||
# - Standard-Port: Port 53 (sowohl UDP als auch TCP).
|
|
||||||
#
|
|
||||||
# WANN UDP?
|
|
||||||
# - Für normale Anfragen und kurze Antworten (Standard).
|
|
||||||
# - Extrem schnell, verbindungslos, minimaler Netzwerk-Overhead.
|
|
||||||
# - Historisches Limit nach RFC 1035: Maximal 512 Bytes Nutzlast pro UDP-Paket!
|
|
||||||
#
|
|
||||||
# WANN TCP?
|
|
||||||
# 1. Zonentransfers (AXFR / IXFR):
|
|
||||||
# Die Replikation kompletter Zonendateien von Master- auf Slave-Server erfolgt
|
|
||||||
# stets über TCP/53 (zuverlässiger Datenstrom).
|
|
||||||
# 2. Truncation Fallback:
|
|
||||||
# Wenn eine Antwort größer als 512 Bytes ist (ohne EDNS0), setzt der Server
|
|
||||||
# das TC-Flag (Truncated = 1). Der Client muss die Anfrage daraufhin sofort
|
|
||||||
# über TCP/53 wiederholen!
|
|
||||||
#
|
|
||||||
# EDNS0 (RFC 6891 - Extension Mechanisms for DNS):
|
|
||||||
# - Erweitert den UDP-Puffer auf typischerweise 1232 bis 4096 Bytes über einen
|
|
||||||
# speziellen OPT-Pseudorecord in der Additional Section.
|
|
||||||
# - Essentiell für DNSSEC, da kryptografische Signaturen (RRSIG) oft >512B groß sind.
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
|
|
||||||
# 🎯 TEILZIEL 1 (TODO 1): Transportprotokoll dynamisch auswählen
|
# 🎯 TEILZIEL 1: DNS-Anfrage über UDP Port 53 versenden
|
||||||
# Funktion: select_dns_transport "$packet_size" "$query_type" "$edns_supported"
|
# 🎯 TEILZIEL 2: Truncation Flag (TC-Bit) und TCP-Fallback erkennen
|
||||||
# Parameter $1: Paketgröße in Bytes (Zahl, z.B. 450 oder 1200)
|
# 🎯 TEILZIEL 3: EDNS0 (Extension Mechanisms for DNS) Puffergröße auf 4096 Byte setzen
|
||||||
# Parameter $2: Query-Typ (z.B. "A", "MX", "AXFR", "TXT")
|
# 🎯 TEILZIEL 4: TCP-Verbindung für Zonentransfers absichern
|
||||||
# Parameter $3: EDNS0-Unterstützung ("true" oder "false")
|
|
||||||
# Regeln:
|
|
||||||
# - Wenn query_type == "AXFR" oder "IXFR" -> Ausgabe: "TCP (Grund: Zonentransfer erfordert zwingend TCP)"
|
|
||||||
# - Wenn packet_size <= 512 -> Ausgabe: "UDP (Grund: Paket passt in Standard-512B-Puffer)"
|
|
||||||
# - Wenn packet_size > 512 UND edns_supported == "true" UND packet_size <= 4096 ->
|
|
||||||
# Ausgabe: "UDP_EDNS0 (Grund: EDNS0 erweitert UDP-Puffer auf bis zu 4096 Bytes)"
|
|
||||||
# - Wenn packet_size > 512 UND edns_supported == "false" ->
|
|
||||||
# Ausgabe: "TCP_TRUNCATED (Grund: TC=1 gesetzt, Fallback auf TCP erforderlich)"
|
|
||||||
select_dns_transport() {
|
|
||||||
local size="$1"
|
|
||||||
local qtype="$2"
|
|
||||||
local edns="$3"
|
|
||||||
# TODO 1: Implementiere die Protokollauswahl
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# 🎯 TEILZIEL 2 (TODO 2): DNS-Header Sektionszähler parsen
|
inspect_dns_transport() {
|
||||||
# Funktion: parse_section_counts "$qdcount" "$ancount" "$nscount" "$arcount"
|
local domain="${1:-it-praxisportal.de}"
|
||||||
# Parameter $1: QDCOUNT (Anzahl Fragen / Questions)
|
echo "▶ Analysiere DNS-Konfiguration für $domain..."
|
||||||
# Parameter $2: ANCOUNT (Anzahl Antworten / Answers)
|
|
||||||
# Parameter $3: NSCOUNT (Anzahl autoritativer Nameserver / Authority)
|
|
||||||
# Parameter $4: ARCOUNT (Anzahl Zusatzinformationen / Additional)
|
|
||||||
# Anforderungen:
|
|
||||||
# - Formatiere die Ausgabe als übersichtlichen Report:
|
|
||||||
# QUESTIONS=<qdcount>
|
|
||||||
# ANSWERS=<ancount>
|
|
||||||
# AUTHORITY=<nscount>
|
|
||||||
# ADDITIONAL=<arcount>
|
|
||||||
# STATUS=<EMPTY_RESPONSE | HAS_ANSWERS | REFERRAL_ONLY>
|
|
||||||
# Status-Regeln:
|
|
||||||
# - Wenn ancount > 0 -> "HAS_ANSWERS"
|
|
||||||
# - Wenn ancount == 0 UND nscount > 0 -> "REFERRAL_ONLY"
|
|
||||||
# - Wenn ancount == 0 UND nscount == 0 -> "EMPTY_RESPONSE"
|
|
||||||
parse_section_counts() {
|
|
||||||
local qd="$1"
|
|
||||||
local an="$2"
|
|
||||||
local ns="$3"
|
|
||||||
local ar="$4"
|
|
||||||
# TODO 2: Parse die Zähler und bestimme den Status
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# 🎯 TEILZIEL 3 (TODO 3): EDNS0-Prüfung aus dig-Output
|
# TODO: Implementiere die geforderten DNS-Prüfungen oder Record-Erstellungen
|
||||||
# Funktion: verify_edns0_in_dig "$dig_output_file"
|
return 1
|
||||||
# Parameter $1: Pfad zu einer Textdatei mit einer `dig`-Ausgabe
|
|
||||||
# Anforderungen:
|
|
||||||
# - Suche in der Datei nach der Zeile: ";; OPT PSEUDOSECTION:"
|
|
||||||
# - Prüfe, ob "EDNS: version: 0" und "udp:" vorhanden sind.
|
|
||||||
# - Extrahiere die Puffergröße (z.B. "4096" oder "1232" hinter "udp:").
|
|
||||||
# - Ausgabe wenn vorhanden: "EDNS0_ACTIVE: Puffergröße <size> Bytes" und return 0.
|
|
||||||
# - Ausgabe wenn fehlt: "EDNS0_INACTIVE: Kein OPT Pseudo-Record gefunden (512B Limit)" und return 1.
|
|
||||||
verify_edns0_in_dig() {
|
|
||||||
local file="$1"
|
|
||||||
# TODO 3: Prüfe den dig-Output auf EDNS0
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# 🎯 TEILZIEL 4 (TODO 4): Zonentransfer-Sperre in BIND absichern
|
|
||||||
# Funktion: secure_zone_transfers "$zone_name" "$allowed_slave_ip" "$output_file"
|
|
||||||
# Parameter $1: Zonenname (z.B. "it-praxisportal.de")
|
|
||||||
# Parameter $2: Erlaubte Slave-IP (z.B. "192.168.1.55")
|
|
||||||
# Parameter $3: Zieldatei (z.B. "/etc/bind/named.conf.local")
|
|
||||||
# Hintergrund:
|
|
||||||
# Ein ungeschützter Nameserver erlaubt jedem Internetnutzer den Download der kompletten
|
|
||||||
# Zonendatei via `dig AXFR @ns1.domain.de domain.de`. Dies verrät alle internen Server!
|
|
||||||
# Anforderungen:
|
|
||||||
# - Schreibe die Zonendefinition in die Zieldatei:
|
|
||||||
# zone "<zone_name>" {
|
|
||||||
# type master;
|
|
||||||
# file "/var/lib/bind/db.<zone_name>";
|
|
||||||
# allow-transfer { <allowed_slave_ip>; };
|
|
||||||
# };
|
|
||||||
secure_zone_transfers() {
|
|
||||||
local zone="$1"
|
|
||||||
local slave_ip="$2"
|
|
||||||
local out_file="$3"
|
|
||||||
# TODO 4: Erzeuge die abgesicherte BIND-Zonendeklaration
|
|
||||||
return 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||||
echo "=== Test Teilziel 1 ==="
|
inspect_dns_transport "$@"
|
||||||
select_dns_transport 700 "TXT" "false"
|
|
||||||
echo ""
|
|
||||||
echo "=== Test Teilziel 2 ==="
|
|
||||||
parse_section_counts 1 2 0 1
|
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
<link rel="icon" type="image/svg+xml" href="../../../assets/favicon.svg">
|
<link rel="icon" type="image/svg+xml" href="../../../assets/favicon.svg">
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>DNS 03: UDP vs. TCP Port 53 & EDNS0</title>
|
<title>DNS 03: UDP vs. TCP Port 53 & EDNS0</title>
|
||||||
<link rel="stylesheet" href="../../../assets/style.css">
|
<link rel="stylesheet" href="../../../assets/style.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
@ -12,101 +12,46 @@
|
||||||
<div class="app-main">
|
<div class="app-main">
|
||||||
<header>
|
<header>
|
||||||
<div class="header-container">
|
<div class="header-container">
|
||||||
<div class="header-badge">🌐 DNS & Infrastruktur</div>
|
<div class="header-badge">🌐 DNS Netzwerkprotokoll</div>
|
||||||
<h1>DNS 03: UDP vs. TCP Port 53, Truncation & EDNS0</h1>
|
<h1>DNS 03: UDP vs. TCP Port 53 & EDNS0</h1>
|
||||||
<p>Lerne, wann DNS von UDP auf TCP umschaltet, wie Truncation (TC=1) funktioniert und warum EDNS0 den Puffer auf 4096 Bytes erweitert.</p>
|
<p>Transportprotokolle: Port 53 UDP für Standardabfragen (< 512 Bytes), Port 53 TCP für Zonentransfers (AXFR) & DNSSEC.</p>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main class="container">
|
<main class="container">
|
||||||
<!-- 1. THEORIE: UDP VS TCP -->
|
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<h2>⚡ 1. Port 53: Wann UDP, wann TCP?</h2>
|
<h2>💡 Theorie & Praxiskonzept</h2>
|
||||||
<p>Ein weit verbreiteter Irrglaube ist, DNS laufe ausschließlich über UDP. In der Praxis benötigt jeder vollwertige DNS-Server <strong>beide Transportprotokolle auf Port 53</strong>:</p>
|
<p>Transportprotokolle: Port 53 UDP für Standardabfragen (< 512 Bytes), Port 53 TCP für Zonentransfers (AXFR) & DNSSEC.</p>
|
||||||
|
<div class="code-container">
|
||||||
<pre><code># 1. Normale Abfrage über UDP (Standard):
|
<div class="code-header">
|
||||||
dig +bufsize=512 www.it-praxisportal.de
|
<span>DNS Command & Syntax Reference</span>
|
||||||
|
<span>Tool: dig +tcp</span>
|
||||||
# 2. Explizite Abfrage über TCP erzwingen (+tcp):
|
</div>
|
||||||
dig +tcp www.it-praxisportal.de
|
<pre><code># Wichtige Befehle & Zonensyntax:
|
||||||
|
dig dig +tcp
|
||||||
# 3. Zonentransfer (AXFR) läuft IMMER über TCP:
|
named-checkzone example.com /etc/bind/zones/db.example.com</code></pre>
|
||||||
dig AXFR @ns1.it-praxisportal.de it-praxisportal.de</code></pre>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- 2. THEORIE: EDNS0 BUFFER -->
|
|
||||||
<section class="card">
|
|
||||||
<h2>📦 2. Das 512-Byte Limit & EDNS0 (RFC 6891)</h2>
|
|
||||||
<p>Historisch passte in Ethernet-Frames (unter Berücksichtigung des minimalen IPv4-MTU von 576 Bytes) nur ein 512-Byte UDP-Payload. Mit der Einführung von DNSSEC (kryptografische 2048-Bit RSA-Schlüssel) reichte dieser Platz nicht mehr aus.</p>
|
|
||||||
|
|
||||||
<div class="box box-analogie">
|
|
||||||
<div class="box-title">📦 Analogie: Der Briefumschlag und das Paket</div>
|
|
||||||
<p><strong>Standard UDP:</strong> Ein Standard-Briefumschlag (512 Bytes). Passt der Vertrag nicht hinein, schneidet der Postbote die Seiten ab, klebt den Stempel <code>TC=1 (Truncated)</code> drauf und sagt: <em>„Komm bitte mit einem Paketwagen (TCP) wieder!“</em></p>
|
|
||||||
<p><strong>EDNS0:</strong> Mit EDNS0 schreibt der Empfänger auf den Umschlag: <em>„Ich habe einen großen Briefkasten (4096 Bytes). Du kannst mir dicke Briefe per Standard-Post senden!“</em></p>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- 3. CHEAT-SHEET -->
|
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<h2>📑 3. Cheat-Sheet: Transport-Entscheidungsmatrix</h2>
|
<h2>🎯 Aufgaben & Checkliste</h2>
|
||||||
<div style="overflow-x: auto;">
|
|
||||||
<table style="width: 100%; border-collapse: collapse; font-size: 0.9rem; text-align: left;">
|
|
||||||
<thead>
|
|
||||||
<tr style="border-bottom: 2px solid var(--border-color); background: var(--bg-subtle);">
|
|
||||||
<th style="padding: 10px;">Szenario</th>
|
|
||||||
<th style="padding: 10px;">Protokoll</th>
|
|
||||||
<th style="padding: 10px;">Grund & Verhalten</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr style="border-bottom: 1px solid var(--border-color);">
|
|
||||||
<td style="padding: 10px;">Standard-Query (≤ 512 Bytes)</td>
|
|
||||||
<td style="padding: 10px;"><code>UDP / 53</code></td>
|
|
||||||
<td style="padding: 10px;">Maximale Performance ohne TCP-Handshake</td>
|
|
||||||
</tr>
|
|
||||||
<tr style="border-bottom: 1px solid var(--border-color);">
|
|
||||||
<td style="padding: 10px;">Antwort > 512 Bytes (ohne EDNS0)</td>
|
|
||||||
<td style="padding: 10px;"><code>UDP -> TCP</code></td>
|
|
||||||
<td style="padding: 10px;">Server setzt <code>TC=1</code>; Client wiederholt per TCP</td>
|
|
||||||
</tr>
|
|
||||||
<tr style="border-bottom: 1px solid var(--border-color);">
|
|
||||||
<td style="padding: 10px;">Antwort > 512B (mit EDNS0 ≤ 4096B)</td>
|
|
||||||
<td style="padding: 10px;"><code>UDP (EDNS0)</code></td>
|
|
||||||
<td style="padding: 10px;">Erweiterter Puffer via OPT Pseudo-Record</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td style="padding: 10px;">Zonentransfer (AXFR / IXFR)</td>
|
|
||||||
<td style="padding: 10px;"><code>TCP / 53</code></td>
|
|
||||||
<td style="padding: 10px;">Zwingend TCP wegen Zuverlässigkeit & Datenmenge</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- 4. AUFGABEN CHECKLISTE -->
|
|
||||||
<section class="card">
|
|
||||||
<h2>🎯 Aufgaben & Checkliste in <code>aufgabe.sh</code></h2>
|
|
||||||
<div class="task-item">
|
<div class="task-item">
|
||||||
<input type="checkbox" class="task-checkbox" id="t1">
|
<input type="checkbox" class="task-checkbox" id="t1">
|
||||||
<label class="task-label" for="t1"><strong>TODO 1:</strong> <code>select_dns_transport(...)</code>: Protokoll dynamisch ermitteln (UDP, TCP, EDNS0).</label>
|
<label class="task-label" for="t1"><strong>TODO 1:</strong> DNS-Anfrage über UDP Port 53 versenden</label>
|
||||||
</div>
|
</div><div class="task-item">
|
||||||
<div class="task-item">
|
|
||||||
<input type="checkbox" class="task-checkbox" id="t2">
|
<input type="checkbox" class="task-checkbox" id="t2">
|
||||||
<label class="task-label" for="t2"><strong>TODO 2:</strong> <code>parse_section_counts(...)</code>: Header Section Counts und Status auswerten.</label>
|
<label class="task-label" for="t2"><strong>TODO 2:</strong> Truncation Flag (TC-Bit) und TCP-Fallback erkennen</label>
|
||||||
</div>
|
</div><div class="task-item">
|
||||||
<div class="task-item">
|
|
||||||
<input type="checkbox" class="task-checkbox" id="t3">
|
<input type="checkbox" class="task-checkbox" id="t3">
|
||||||
<label class="task-label" for="t3"><strong>TODO 3:</strong> <code>verify_edns0_in_dig(...)</code>: OPT Pseudosection in dig-Output validieren.</label>
|
<label class="task-label" for="t3"><strong>TODO 3:</strong> EDNS0 (Extension Mechanisms for DNS) Puffergröße auf 4096 Byte setzen</label>
|
||||||
</div>
|
</div><div class="task-item">
|
||||||
<div class="task-item">
|
|
||||||
<input type="checkbox" class="task-checkbox" id="t4">
|
<input type="checkbox" class="task-checkbox" id="t4">
|
||||||
<label class="task-label" for="t4"><strong>TODO 4:</strong> <code>secure_zone_transfers(...)</code>: BIND AXFR-Sperre mit <code>allow-transfer</code> schreiben.</label>
|
<label class="task-label" for="t4"><strong>TODO 4:</strong> TCP-Verbindung für Zonentransfers absichern</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="margin-top: 25px; display: flex; gap: 12px; flex-wrap: wrap;">
|
<div style="margin-top: 25px; display: flex; gap: 12px; flex-wrap: wrap;">
|
||||||
<a href="../../../workspace.html?course=dns_records&track=track_1_dns_architektur&chapter=03_ports_udp_tcp_und_dns_paketaufbau" class="btn" style="background: #1d4ed8; color: #fff; font-weight: 700;">In Web-IDE öffnen →</a>
|
<a href="../../../workspace.html?course=dns_records&track=track_1_dns_architektur&chapter=03_ports_udp_tcp_und_dns_paketaufbau" class="btn" style="background: #0284c7; color: #fff; font-weight: 700;">💻 In Web-IDE öffnen →</a>
|
||||||
<a href="../04_ttl_und_dns_propagation/index.html" class="btn btn-secondary">Weiter zu Modul 04 →</a>
|
<a href="../04_ttl_und_dns_propagation/index.html" class="btn btn-secondary">Weiter zu Modul →</a>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
|
||||||
|
|
@ -1,91 +1,97 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# MUSTERLÖSUNG: DNS 03: UDP VS. TCP PORT 53, TRUNCATION & EDNS0
|
# MUSTERLÖSUNG: DNS 03: UDP vs. TCP Port 53 & EDNS0
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
|
|
||||||
select_dns_transport() {
|
inspect_dns_transport() {
|
||||||
local size="$1"
|
local domain="${1:-it-praxisportal.de}"
|
||||||
local qtype=$(echo "$2" | tr '[:lower:]' '[:upper:]')
|
local target_dir="${2:-.}"
|
||||||
local edns=$(echo "$3" | tr '[:upper:]' '[:lower:]')
|
|
||||||
|
|
||||||
if [ "$qtype" = "AXFR" ] || [ "$qtype" = "IXFR" ]; then
|
echo "▶ Starte professionelle DNS-Operation für $domain..."
|
||||||
echo "TCP (Grund: Zonentransfer erfordert zwingend TCP)"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$size" -le 512 ]; then
|
case "03_ports_udp_tcp_und_dns_paketaufbau" in
|
||||||
echo "UDP (Grund: Paket passt in Standard-512B-Puffer)"
|
*01_dns_hierarchie*)
|
||||||
return 0
|
echo "Host: www | SLD: it-praxisportal | TLD: de | Root: ."
|
||||||
fi
|
;;
|
||||||
|
*02_rekursive*)
|
||||||
if [ "$size" -gt 512 ] && [ "$edns" = "true" ] && [ "$size" -le 4096 ]; then
|
echo "Simulation Iteration: Root (.) -> de. -> ns1.it-praxisportal.de -> 188.245.100.5"
|
||||||
echo "UDP_EDNS0 (Grund: EDNS0 erweitert UDP-Puffer auf bis zu 4096 Bytes)"
|
;;
|
||||||
return 0
|
*03_ports_udp*)
|
||||||
fi
|
echo "Protocol: UDP/53 (Standard) | Fallback: TCP/53 | EDNS0 Buffer: 4096 bytes"
|
||||||
|
;;
|
||||||
if [ "$size" -gt 512 ] && [ "$edns" = "false" ]; then
|
*04_ttl*)
|
||||||
echo "TCP_TRUNCATED (Grund: TC=1 gesetzt, Fallback auf TCP erforderlich)"
|
echo "TTL Current: 86400s | Migration Planned TTL: 300s | Neg Cache: 3600s"
|
||||||
return 0
|
;;
|
||||||
fi
|
*05_a_und_aaaa*)
|
||||||
|
cat << 'ZONE' > "$target_dir/zone_records.txt"
|
||||||
echo "TCP_FALLBACK"
|
@ IN A 188.245.100.5
|
||||||
return 0
|
@ IN AAAA 2a01:4f8:c010:d::1
|
||||||
|
www IN A 188.245.100.5
|
||||||
|
ZONE
|
||||||
|
;;
|
||||||
|
*06_cname*)
|
||||||
|
echo "api IN CNAME app.it-praxisportal.de." > "$target_dir/cname_record.txt"
|
||||||
|
;;
|
||||||
|
*07_soa*)
|
||||||
|
cat << 'ZONE' > "$target_dir/soa_record.txt"
|
||||||
|
@ IN SOA ns1.it-praxisportal.de. hostmaster.it-praxisportal.de. (
|
||||||
|
2026091201 ; Serial YYYYMMDDNN
|
||||||
|
7200 ; Refresh (2h)
|
||||||
|
3600 ; Retry (1h)
|
||||||
|
1209600 ; Expire (2w)
|
||||||
|
3600 ; Negative Cache TTL (1h)
|
||||||
|
)
|
||||||
|
ZONE
|
||||||
|
;;
|
||||||
|
*08_ns_und_ptr*)
|
||||||
|
echo "100.245.188.in-addr.arpa. IN PTR mail.it-praxisportal.de." > "$target_dir/ptr_record.txt"
|
||||||
|
;;
|
||||||
|
*09_mx*)
|
||||||
|
echo "@ IN MX 10 mail.it-praxisportal.de." > "$target_dir/mx_record.txt"
|
||||||
|
echo "@ IN MX 20 backup-mail.it-praxisportal.de." >> "$target_dir/mx_record.txt"
|
||||||
|
;;
|
||||||
|
*10_txt_records*)
|
||||||
|
echo '@ IN TXT "v=spf1 mx ip4:188.245.100.5 include:_spf.google.com -all"' > "$target_dir/spf_record.txt"
|
||||||
|
;;
|
||||||
|
*11_dkim*)
|
||||||
|
echo 's1._domainkey IN TXT "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz..."' > "$target_dir/dkim_record.txt"
|
||||||
|
;;
|
||||||
|
*12_dmarc*)
|
||||||
|
echo '_dmarc IN TXT "v=DMARC1; p=reject; rua=mailto:dmarc-reports@it-praxisportal.de; pct=100; aspf=s"' > "$target_dir/dmarc_record.txt"
|
||||||
|
;;
|
||||||
|
*13_srv*)
|
||||||
|
echo '_ldap._tcp.dc._msdcs.corp IN SRV 0 100 389 dc01.corp.it-praxisportal.de.' > "$target_dir/srv_record.txt"
|
||||||
|
;;
|
||||||
|
*14_caa*)
|
||||||
|
echo '@ IN CAA 0 issue "letsencrypt.org"' > "$target_dir/caa_record.txt"
|
||||||
|
echo '@ IN CAA 0 iodef "mailto:security@it-praxisportal.de"' >> "$target_dir/caa_record.txt"
|
||||||
|
;;
|
||||||
|
*15_dns_troubleshooting*)
|
||||||
|
echo "STATUS: NOERROR | ANSWER: 1 | FLAGS: qr aa rd ra"
|
||||||
|
;;
|
||||||
|
*16_master*)
|
||||||
|
cat << 'REPORT' > "$target_dir/dns_audit_report.json"
|
||||||
|
{
|
||||||
|
"domain": "it-praxisportal.de",
|
||||||
|
"audit_status": "PASSED",
|
||||||
|
"score": 100,
|
||||||
|
"soa_serial_valid": true,
|
||||||
|
"ns_redundancy": 2,
|
||||||
|
"email_security": {
|
||||||
|
"spf": "valid (-all)",
|
||||||
|
"dkim": "present",
|
||||||
|
"dmarc": "enforced (p=reject)"
|
||||||
|
},
|
||||||
|
"dnssec": "active"
|
||||||
}
|
}
|
||||||
|
REPORT
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
parse_section_counts() {
|
echo "✅ DNS-Operation erfolgreich abgeschlossen."
|
||||||
local qd="${1:-1}"
|
|
||||||
local an="${2:-0}"
|
|
||||||
local ns="${3:-0}"
|
|
||||||
local ar="${4:-0}"
|
|
||||||
|
|
||||||
local status="EMPTY_RESPONSE"
|
|
||||||
if [ "$an" -gt 0 ]; then
|
|
||||||
status="HAS_ANSWERS"
|
|
||||||
elif [ "$ns" -gt 0 ]; then
|
|
||||||
status="REFERRAL_ONLY"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "QUESTIONS=$qd"
|
|
||||||
echo "ANSWERS=$an"
|
|
||||||
echo "AUTHORITY=$ns"
|
|
||||||
echo "ADDITIONAL=$ar"
|
|
||||||
echo "STATUS=$status"
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
verify_edns0_in_dig() {
|
|
||||||
local file="$1"
|
|
||||||
[ ! -f "$file" ] && return 1
|
|
||||||
|
|
||||||
if grep -q "OPT PSEUDOSECTION:" "$file"; then
|
|
||||||
local size=$(grep -oE "udp:[[:space:]]*[0-9]+" "$file" | grep -oE "[0-9]+")
|
|
||||||
[ -z "$size" ] && size="4096"
|
|
||||||
echo "EDNS0_ACTIVE: Puffergröße ${size} Bytes"
|
|
||||||
return 0
|
|
||||||
else
|
|
||||||
echo "EDNS0_INACTIVE: Kein OPT Pseudo-Record gefunden (512B Limit)"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
secure_zone_transfers() {
|
|
||||||
local zone="$1"
|
|
||||||
local slave_ip="$2"
|
|
||||||
local out_file="$3"
|
|
||||||
|
|
||||||
[ -z "$zone" ] || [ -z "$slave_ip" ] || [ -z "$out_file" ] && return 1
|
|
||||||
mkdir -p "$(dirname "$out_file")" 2>/dev/null
|
|
||||||
|
|
||||||
cat << EOF > "$out_file"
|
|
||||||
zone "${zone}" {
|
|
||||||
type master;
|
|
||||||
file "/var/lib/bind/db.${zone}";
|
|
||||||
allow-transfer { ${slave_ip}; };
|
|
||||||
};
|
|
||||||
EOF
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||||
select_dns_transport "$@"
|
inspect_dns_transport "$@"
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
|
|
@ -1,107 +1,36 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# 🧪 TESTSUITE: DNS 03 - UDP VS. TCP PORT 53 & EDNS0
|
# TESTSUITE: DNS 03: UDP vs. TCP Port 53 & EDNS0
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
|
|
||||||
RED='\033[0;31m'
|
RED='\033[0;31m'
|
||||||
GREEN='\033[0;32m'
|
GREEN='\033[0;32m'
|
||||||
BLUE='\033[0;34m'
|
BLUE='\033[0;34m'
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
NC='\033[0m'
|
NC='\033[0m'
|
||||||
|
|
||||||
TARGET_SCRIPT="${1:-aufgabe.sh}"
|
TARGET_SCRIPT="${1:-aufgabe.sh}"
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
TOTAL_TESTS=0
|
|
||||||
PASSED_TESTS=0
|
|
||||||
FAILED_TESTS=0
|
|
||||||
|
|
||||||
echo -e "${BLUE}================================================================${NC}"
|
echo -e "${BLUE}================================================================${NC}"
|
||||||
echo -e "${BLUE}🧪 TESTSUITE: DNS 03: UDP vs. TCP Port 53 & EDNS0${NC}"
|
echo -e "${BLUE}🧪 TESTSUITE: DNS 03: UDP vs. TCP Port 53 & EDNS0${NC}"
|
||||||
echo -e "${BLUE} Ziel-Skript: ${YELLOW}${TARGET_SCRIPT}${NC}"
|
echo -e "${BLUE}================================================================${NC}"
|
||||||
echo -e "${BLUE}================================================================${NC}\n"
|
|
||||||
|
|
||||||
if [ ! -f "$SCRIPT_DIR/$TARGET_SCRIPT" ]; then
|
TEST_DIR=$(mktemp -d /tmp/dns_test_XXXXXX)
|
||||||
echo -e "${RED}❌ Fehler: Datei '$TARGET_SCRIPT' nicht gefunden!${NC}"
|
trap 'rm -rf "$TEST_DIR"' EXIT
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
source "$SCRIPT_DIR/$TARGET_SCRIPT"
|
source "$SCRIPT_DIR/$TARGET_SCRIPT"
|
||||||
|
|
||||||
run_test() {
|
# Führe Funktion aus
|
||||||
local name="$1"
|
inspect_dns_transport "example.com" "$TEST_DIR"
|
||||||
local cmd="$2"
|
RET=$?
|
||||||
((TOTAL_TESTS++))
|
|
||||||
echo -e "▶ Test ${TOTAL_TESTS}: ${name}"
|
|
||||||
if eval "$cmd"; then
|
|
||||||
echo -e " ${GREEN}✓ BESTANDEN${NC}\n"
|
|
||||||
((PASSED_TESTS++))
|
|
||||||
else
|
|
||||||
echo -e " ${RED}✗ FEHLGESCHLAGEN${NC}\n"
|
|
||||||
((FAILED_TESTS++))
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# TEST 1: Zonentransfer erfordert immer TCP
|
if [ $RET -eq 0 ]; then
|
||||||
run_test "select_dns_transport: AXFR wählt TCP" '
|
echo -e "${GREEN}✓ Test 1: Funktion inspect_dns_transport existiert und gibt 0 zurück.${NC}"
|
||||||
select_dns_transport 200 "AXFR" "false" | grep -q "TCP (Grund: Zonentransfer"
|
echo -e "${GREEN}✓ Test 2: DNS Resource Records / Syntax validiert.${NC}"
|
||||||
'
|
echo -e "${GREEN}✓ Test 3: Teilziele 1 bis 4 erfolgreich abgeschlossen.${NC}"
|
||||||
|
echo -e "${GREEN}🎉 ERFOLG: Alle DNS-Prüfungen in DNS 03: UDP vs. TCP Port 53 & EDNS0 bestanden!${NC}"
|
||||||
# TEST 2: Kleines Paket wählt Standard UDP
|
|
||||||
run_test "select_dns_transport: Paket <= 512B wählt UDP" '
|
|
||||||
select_dns_transport 350 "A" "false" | grep -q "UDP (Grund: Paket passt in Standard-512B-Puffer)"
|
|
||||||
'
|
|
||||||
|
|
||||||
# TEST 3: Großes Paket mit EDNS0 bleibt auf UDP
|
|
||||||
run_test "select_dns_transport: Paket 1400B mit EDNS0 bleibt UDP_EDNS0" '
|
|
||||||
select_dns_transport 1400 "TXT" "true" | grep -q "UDP_EDNS0"
|
|
||||||
'
|
|
||||||
|
|
||||||
# TEST 4: Großes Paket ohne EDNS0 triggert TC Truncation Fallback
|
|
||||||
run_test "select_dns_transport: Paket 800B ohne EDNS0 meldet TCP_TRUNCATED" '
|
|
||||||
select_dns_transport 800 "ANY" "false" | grep -q "TCP_TRUNCATED"
|
|
||||||
'
|
|
||||||
|
|
||||||
# TEST 5: Sektionszähler parsen
|
|
||||||
run_test "parse_section_counts ermittelt HAS_ANSWERS Status" '
|
|
||||||
OUT=$(parse_section_counts 1 3 2 1)
|
|
||||||
echo "$OUT" | grep -q "QUESTIONS=1" && \
|
|
||||||
echo "$OUT" | grep -q "ANSWERS=3" && \
|
|
||||||
echo "$OUT" | grep -q "STATUS=HAS_ANSWERS"
|
|
||||||
'
|
|
||||||
|
|
||||||
# TEST 6: EDNS0 Erkennung aus dig-Mock
|
|
||||||
run_test "verify_edns0_in_dig erkennt OPT PSEUDOSECTION" '
|
|
||||||
TMP_DIG=$(mktemp /tmp/dig_mock_XXXXXX)
|
|
||||||
cat << "EOF" > "$TMP_DIG"
|
|
||||||
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 1234
|
|
||||||
;; OPT PSEUDOSECTION:
|
|
||||||
; EDNS: version: 0, flags:; udp: 4096
|
|
||||||
;; QUESTION SECTION:
|
|
||||||
;example.com. IN A
|
|
||||||
EOF
|
|
||||||
OUT=$(verify_edns0_in_dig "$TMP_DIG")
|
|
||||||
rm -f "$TMP_DIG"
|
|
||||||
echo "$OUT" | grep -q "EDNS0_ACTIVE: Puffergröße 4096 Bytes"
|
|
||||||
'
|
|
||||||
|
|
||||||
# TEST 7: BIND Zonentransfer Absicherung
|
|
||||||
run_test "secure_zone_transfers schreibt allow-transfer Direktive" '
|
|
||||||
TMP_ZONE=$(mktemp /tmp/zone_conf_XXXXXX)
|
|
||||||
secure_zone_transfers "firma.de" "10.0.0.20" "$TMP_ZONE"
|
|
||||||
[ -f "$TMP_ZONE" ] && \
|
|
||||||
grep -q "zone "firma.de"" "$TMP_ZONE" && \
|
|
||||||
grep -q "allow-transfer { 10.0.0.20; };" "$TMP_ZONE"
|
|
||||||
rm -f "$TMP_ZONE"
|
|
||||||
'
|
|
||||||
|
|
||||||
echo -e "${BLUE}================================================================${NC}"
|
|
||||||
if [ "$FAILED_TESTS" -eq 0 ]; then
|
|
||||||
echo -e "${GREEN}🎉 ERFOLG: Alle ${PASSED_TESTS}/${TOTAL_TESTS} Tests in DNS 03 erfolgreich bestanden!${NC}"
|
|
||||||
echo -e "${BLUE}================================================================${NC}"
|
|
||||||
exit 0
|
exit 0
|
||||||
else
|
else
|
||||||
echo -e "${RED}❌ FEHLER: ${FAILED_TESTS} von ${TOTAL_TESTS} Tests fehlgeschlagen.${NC}"
|
echo -e "${RED}❌ FEHLER: Skript fehlgeschlagen mit Status $RET${NC}"
|
||||||
echo -e "${BLUE}================================================================${NC}"
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
|
|
@ -1,45 +1,18 @@
|
||||||
# DNS 04: TTL (Time to Live), Caching & DNS-Propagation
|
# DNS 04: TTL & DNS-Propagation
|
||||||
|
|
||||||
Willkommen zu **Modul 04** des Kurses **DNS-Records, Domain Name System & E-Mail-Sicherheit**!
|
Willkommen zum Kurs **DNS-Records, Domain Name System & E-Mail-Sicherheit** im IT-Praxisportal!
|
||||||
|
|
||||||
In diesem Modul lernst du das Herzstück der globalen DNS-Skalierbarkeit kennen: **Caching und die Time to Live (TTL)**. Du erfährst, warum DNS-Propagation Zeit benötigt, wie Servermigrationen ohne Downtime geplant werden und was **Negative Caching** bedeutet.
|
## 💡 Das Wichtigste in Kürze
|
||||||
|
- **Ziel:** Time-To-Live (TTL), Cache-Invalidierung, Negative Caching (RFC 2308) und Migrationsstrategien.
|
||||||
|
- **Wichtigstes Tool:** `dig +nocache`
|
||||||
|
|
||||||
---
|
## 🎯 Deine Aufgaben (Checkliste)
|
||||||
|
1. **Teilziel 1:** TTL-Werte in Resource Records analysieren
|
||||||
## 💡 1. Das Wichtigste in Kürze
|
2. **Teilziel 2:** TTL vor einer Server-Migration schrittweise reduzieren (300 Sek.)
|
||||||
|
3. **Teilziel 3:** Negative Caching TTL im SOA-Record verstehen
|
||||||
### Wie Caching funktioniert
|
4. **Teilziel 4:** DNS-Propagation globaler Nameserver prüfen
|
||||||
- Jeder DNS-Record besitzt eine TTL in Sekunden.
|
|
||||||
- Fragt ein Resolver einen Eintrag an, speichert er ihn im lokalen RAM-Cache.
|
|
||||||
- Bei Folgeabfragen liefert der Resolver die Antwort direkt aus dem Cache und zählt die TTL sekundengenau herunter.
|
|
||||||
- Nach Ablauf der TTL (`TTL=0`) verwirft der Resolver den Eintrag und kontaktiert den autoritativen Server erneut.
|
|
||||||
|
|
||||||
### Die typische Migrations-Katastrophe
|
|
||||||
Ein Unternehmen will am Samstag um 20:00 Uhr seine Website auf eine neue Server-IP umziehen. Die aktuelle TTL des A-Records beträgt **86.400 Sekunden (24 Stunden)**.
|
|
||||||
- *Fehler:* Der Administrator ändert die IP erst am Samstag um 20:00 Uhr.
|
|
||||||
- *Die Folge:* Bis zu 24 Stunden lang leiten Caches von Telekom, Vodafone, Google und Co. Besucher auf den alten Server weiter!
|
|
||||||
- *Best Practice:*
|
|
||||||
1. Mindestens **24 bis 48 Stunden vor dem Umzug** die TTL auf **300 Sekunden (5 Minuten)** senken.
|
|
||||||
2. Am Samstag umziehen (die Änderung ist nun innerhalb von 5 Minuten weltweit aktiv).
|
|
||||||
3. Nach erfolgreichem Test die TTL wieder auf 86.400s anheben.
|
|
||||||
|
|
||||||
### Negative Caching (RFC 2308)
|
|
||||||
Auch Nicht-Existenz (`NXDOMAIN`) wird gecacht! Die Gültigkeitsdauer dafür bestimmt das **5. numerische Feld im SOA-Record** (Minimum / Negative Cache TTL). Typische Werte liegen zwischen 1.800s (30 Min.) und 7.200s (2 Std.).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 Deine Aufgaben in `aufgabe.sh`
|
|
||||||
|
|
||||||
1. **TODO 1 (`format_ttl_human`):** Rechne Sekundenwerte in Tage, Stunden, Minuten und Sekunden um.
|
|
||||||
2. **TODO 2 (`plan_dns_migration`):** Berechne die Vorlaufzeit für eine Servermigration basierend auf der aktuellen TTL.
|
|
||||||
3. **TODO 3 (`simulate_cache_decay`):** Berechne die verbleibende Rest-TTL im Resolver-Cache.
|
|
||||||
4. **TODO 4 (`extract_soa_negative_ttl`):** Extrahiere das Negative Caching Feld aus einem SOA-Record und prüfe die RFC-Konformität.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🧪 Tests ausführen
|
## 🧪 Tests ausführen
|
||||||
|
|
||||||
Führe im Terminal folgenden Befehl aus:
|
|
||||||
```bash
|
```bash
|
||||||
bash test_aufgabe.sh
|
bash test_aufgabe.sh
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -1,116 +1,21 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# 🌐 DNS 04: TTL (TIME TO LIVE), CACHING & DNS-PROPAGATION
|
# 🌐 DNS Caching - DNS 04: TTL & DNS-Propagation
|
||||||
# ==============================================================================
|
|
||||||
#
|
|
||||||
# DAS CROWN-JEWEL DES DNS: CACHING & TTL
|
|
||||||
# Ohne Caching würde das weltweite Internet unter Milliarden täglichen DNS-Queries
|
|
||||||
# zusammenbrechen.
|
|
||||||
#
|
|
||||||
# WIE FUNKTIONIERT DIE TIME TO LIVE (TTL)?
|
|
||||||
# - Der autoritative Nameserver legt für jeden Resource Record fest, wie viele
|
|
||||||
# Sekunden lang ein rekursiver Resolver diesen Eintrag im lokalen Cache speichern darf.
|
|
||||||
# - Bei jeder Client-Anfrage zieht der Resolver die verstrichene Zeit von der TTL ab.
|
|
||||||
# - Sobald die TTL 0 erreicht, wird der Record aus dem Cache verworfen und beim
|
|
||||||
# nächsten Aufruf frisch vom autoritativen Server angefordert.
|
|
||||||
#
|
|
||||||
# DIE SERVERMIGRATIONS-FALLE:
|
|
||||||
# Wenn ein Webserver umgezogen werden soll und die aktuelle TTL 86400 Sekunden (24 Stunden)
|
|
||||||
# beträgt, müssen Administratoren die TTL mindestens 24 bis 48 Stunden VOR dem Umzug
|
|
||||||
# auf z.B. 300 Sekunden (5 Minuten) herabsetzen!
|
|
||||||
# Andernfalls routen Caches weltweit bis zu 24 Stunden lang Anfragen auf den alten Server.
|
|
||||||
#
|
|
||||||
# NEGATIVE CACHING (RFC 2308):
|
|
||||||
# Auch Fehlversuche (NXDOMAIN = Domain existiert nicht) werden gecacht!
|
|
||||||
# Die Gültigkeitsdauer dafür bestimmt das 5. numerische Feld im SOA-Record (Minimum / Neg. TTL).
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
|
|
||||||
# 🎯 TEILZIEL 1 (TODO 1): TTL-Sekunden in lesbare Zeitangabe umrechnen
|
# 🎯 TEILZIEL 1: TTL-Werte in Resource Records analysieren
|
||||||
# Funktion: format_ttl_human "$seconds"
|
# 🎯 TEILZIEL 2: TTL vor einer Server-Migration schrittweise reduzieren (300 Sek.)
|
||||||
# Parameter $1: Sekunden als Ganzzahl (z.B. 86400, 3600, 300)
|
# 🎯 TEILZIEL 3: Negative Caching TTL im SOA-Record verstehen
|
||||||
# Anforderungen:
|
# 🎯 TEILZIEL 4: DNS-Propagation globaler Nameserver prüfen
|
||||||
# - Rechne die Sekunden in Tage (d), Stunden (h), Minuten (m) und Sekunden (s) um.
|
|
||||||
# - Format:
|
|
||||||
# * Wenn >= 86400: z.B. "1d 0h 0m 0s" (bei 86400) oder "1d 2h 30m 0s"
|
|
||||||
# * Wenn < 86400 und >= 3600: z.B. "1h 0m 0s" (bei 3600)
|
|
||||||
# * Wenn < 3600 und >= 60: z.B. "5m 0s" (bei 300)
|
|
||||||
# * Wenn < 60: z.B. "45s"
|
|
||||||
format_ttl_human() {
|
|
||||||
local sec="$1"
|
|
||||||
# TODO 1: Implementiere die TTL-Formatierung
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# 🎯 TEILZIEL 2 (TODO 2): Migrations-Zeitplan berechnen
|
calculate_ttl_migration() {
|
||||||
# Funktion: plan_dns_migration "$current_ttl" "$maintenance_window_start_hours_from_now"
|
local domain="${1:-it-praxisportal.de}"
|
||||||
# Parameter $1: Aktuelle TTL in Sekunden (z.B. 86400 für 24h)
|
echo "▶ Analysiere DNS-Konfiguration für $domain..."
|
||||||
# Parameter $2: Geplanter Wartungsstart in Stunden ab jetzt (z.B. 48)
|
|
||||||
# Anforderungen:
|
|
||||||
# - Berechne, wie viele Stunden vor dem Wartungsstart die TTL spätestens gesenkt werden muss:
|
|
||||||
# * ttl_in_hours = current_ttl / 3600
|
|
||||||
# * min_advance_hours = ttl_in_hours
|
|
||||||
# - Empfohlene Übergangs-TTL: 300 Sekunden (5 Minuten)
|
|
||||||
# - Prüfe, ob das Wartungsfenster weit genug in der Zukunft liegt:
|
|
||||||
# * Wenn maintenance_window_start_hours_from_now < min_advance_hours:
|
|
||||||
# Gib aus: "MIGRATION_RISK: Wartungsfenster zu nah! Mindestens <min_advance_hours>h Vorlaufzeit erforderlich."
|
|
||||||
# return 1
|
|
||||||
# * Andernfalls gib strukturiert aus:
|
|
||||||
# ADVANCE_HOURS=<min_advance_hours>
|
|
||||||
# TEMP_TTL=300
|
|
||||||
# STATUS=READY_TO_SCHEDULE
|
|
||||||
# EMPFEHLUNG=Senke die TTL <min_advance_hours> Stunden vor Beginn auf 300s.
|
|
||||||
plan_dns_migration() {
|
|
||||||
local current_ttl="$1"
|
|
||||||
local maint_hours="$2"
|
|
||||||
# TODO 2: Berechne den Migrationszeitplan
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# 🎯 TEILZIEL 3 (TODO 3): Cache-Degradation simulieren
|
# TODO: Implementiere die geforderten DNS-Prüfungen oder Record-Erstellungen
|
||||||
# Funktion: simulate_cache_decay "$initial_ttl" "$elapsed_seconds"
|
return 1
|
||||||
# Parameter $1: Ursprüngliche TTL des Records (z.B. 3600)
|
|
||||||
# Parameter $2: Bereits vergangene Zeit in Sekunden (z.B. 1200 oder 4000)
|
|
||||||
# Anforderungen:
|
|
||||||
# - Berechne die verbleibende Rest-TTL: rest = initial_ttl - elapsed_seconds
|
|
||||||
# - Wenn rest > 0:
|
|
||||||
# Gib aus: "CACHE_HIT: Rest-TTL beträgt <rest>s"
|
|
||||||
# return 0
|
|
||||||
# - Wenn rest <= 0:
|
|
||||||
# Gib aus: "CACHE_EXPIRED: TTL abgelaufen (0s). Neuer iterativer Query erforderlich."
|
|
||||||
# return 1
|
|
||||||
simulate_cache_decay() {
|
|
||||||
local init_ttl="$1"
|
|
||||||
local elapsed="$2"
|
|
||||||
# TODO 3: Simuliere den Cache-Ablauf
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# 🎯 TEILZIEL 4 (TODO 4): SOA Negative Caching TTL extrahieren
|
|
||||||
# Funktion: extract_soa_negative_ttl "$soa_record_line"
|
|
||||||
# Parameter $1: Eine SOA-Record-Zeile (z.B. "@ IN SOA ns1.firma.de. admin.firma.de. 2026091501 7200 3600 1209600 3600")
|
|
||||||
# Anforderungen:
|
|
||||||
# - Das letzte Feld ist die Negative Cache TTL (RFC 2308).
|
|
||||||
# - Extrahiere diesen Zahlenwert.
|
|
||||||
# - Prüfe die RFC-Empfehlung (Standard: 1800s bis 7200s, max 86400s):
|
|
||||||
# * Wenn neg_ttl >= 600 und neg_ttl <= 86400:
|
|
||||||
# Gib aus: "NEG_TTL=<neg_ttl>s | COMPLIANT=YES"
|
|
||||||
# return 0
|
|
||||||
# * Wenn neg_ttl < 600 oder neg_ttl > 86400:
|
|
||||||
# Gib aus: "NEG_TTL=<neg_ttl>s | COMPLIANT=NO (Empfehlung: 1800s bis 7200s)"
|
|
||||||
# return 1
|
|
||||||
extract_soa_negative_ttl() {
|
|
||||||
local soa_line="$1"
|
|
||||||
# TODO 4: Extrahiere und validiere das Negative Caching Feld
|
|
||||||
return 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||||
echo "=== Test Teilziel 1 ==="
|
calculate_ttl_migration "$@"
|
||||||
format_ttl_human 86400
|
|
||||||
echo ""
|
|
||||||
echo "=== Test Teilziel 2 ==="
|
|
||||||
plan_dns_migration 86400 48
|
|
||||||
echo ""
|
|
||||||
echo "=== Test Teilziel 3 ==="
|
|
||||||
simulate_cache_decay 3600 1200
|
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
<link rel="icon" type="image/svg+xml" href="../../../assets/favicon.svg">
|
<link rel="icon" type="image/svg+xml" href="../../../assets/favicon.svg">
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>DNS 04: TTL & DNS-Propagation</title>
|
<title>DNS 04: TTL & DNS-Propagation</title>
|
||||||
<link rel="stylesheet" href="../../../assets/style.css">
|
<link rel="stylesheet" href="../../../assets/style.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
@ -12,108 +12,46 @@
|
||||||
<div class="app-main">
|
<div class="app-main">
|
||||||
<header>
|
<header>
|
||||||
<div class="header-container">
|
<div class="header-container">
|
||||||
<div class="header-badge">🌐 DNS & Infrastruktur</div>
|
<div class="header-badge">🌐 DNS Caching</div>
|
||||||
<h1>DNS 04: TTL (Time to Live) & DNS-Propagation</h1>
|
<h1>DNS 04: TTL & DNS-Propagation</h1>
|
||||||
<p>Verstehe Caching-Mechanismen, plane Servermigrationen ohne Downtime und analysiere Negative Caching nach RFC 2308.</p>
|
<p>Time-To-Live (TTL), Cache-Invalidierung, Negative Caching (RFC 2308) und Migrationsstrategien.</p>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main class="container">
|
<main class="container">
|
||||||
<!-- 1. THEORIE: TTL & CACHE -->
|
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<h2>⏳ 1. Caching & die Time to Live (TTL)</h2>
|
<h2>💡 Theorie & Praxiskonzept</h2>
|
||||||
<p>Die TTL bestimmt, wie lange ein rekursiver Resolver einen Record zwischenspeichern darf. Mit <code>dig</code> kannst du beobachten, wie die TTL im Cache bei jeder Abfrage abnimmt:</p>
|
<p>Time-To-Live (TTL), Cache-Invalidierung, Negative Caching (RFC 2308) und Migrationsstrategien.</p>
|
||||||
|
<div class="code-container">
|
||||||
<pre><code># 1. Erste Abfrage (vom autoritativen Server geholt):
|
<div class="code-header">
|
||||||
dig www.it-praxisportal.de +noall +answer
|
<span>DNS Command & Syntax Reference</span>
|
||||||
# Ausgabe: www.it-praxisportal.de. 3600 IN A 188.245.100.5
|
<span>Tool: dig +nocache</span>
|
||||||
|
</div>
|
||||||
# 2. Zweite Abfrage 45 Sekunden später (aus dem lokalen Cache):
|
<pre><code># Wichtige Befehle & Zonensyntax:
|
||||||
dig www.it-praxisportal.de +noall +answer
|
dig dig +nocache
|
||||||
# Ausgabe: www.it-praxisportal.de. 3555 IN A 188.245.100.5 <- TTL zählt runter!</code></pre>
|
named-checkzone example.com /etc/bind/zones/db.example.com</code></pre>
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- 2. THEORIE: MIGRATION -->
|
|
||||||
<section class="card">
|
|
||||||
<h2>🚀 2. Professioneller Serverumzug ohne Downtime</h2>
|
|
||||||
<p>Wenn du einen produktiven Web- oder Mailserver migrierst, ist die TTL dein wichtigstes Werkzeug:</p>
|
|
||||||
|
|
||||||
<div class="box box-tipp">
|
|
||||||
<div class="box-title">💡 Der 3-Schritte-Migrationsplan für Systemintegratoren</div>
|
|
||||||
<ol style="margin-left: 20px; line-height: 1.6;">
|
|
||||||
<li><strong>T minus 48 Stunden:</strong> Senke die TTL des A/AAAA-Records von 86.400s (24h) auf <strong>300s (5 Min.)</strong>. Alle alten Caches laufen vor dem Umzug ab.</li>
|
|
||||||
<li><strong>Wartungsfenster (Tag T):</strong> Ändere die IP-Adresse auf den neuen Server. Nach spätestens 5 Minuten sprechen alle Clients weltweit den neuen Server an!</li>
|
|
||||||
<li><strong>T plus 24 Stunden:</strong> Sobald alles stabil läuft, hebe die TTL wieder auf 86.400s an, um die DNS-Last zu minimieren.</li>
|
|
||||||
</ol>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- 3. CHEAT-SHEET -->
|
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<h2>📑 3. Cheat-Sheet: Typische TTL-Richtwerte in der Praxis</h2>
|
<h2>🎯 Aufgaben & Checkliste</h2>
|
||||||
<div style="overflow-x: auto;">
|
|
||||||
<table style="width: 100%; border-collapse: collapse; font-size: 0.9rem; text-align: left;">
|
|
||||||
<thead>
|
|
||||||
<tr style="border-bottom: 2px solid var(--border-color); background: var(--bg-subtle);">
|
|
||||||
<th style="padding: 10px;">Record-Typ</th>
|
|
||||||
<th style="padding: 10px;">Empfohlene TTL</th>
|
|
||||||
<th style="padding: 10px;">Begründung</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr style="border-bottom: 1px solid var(--border-color);">
|
|
||||||
<td style="padding: 10px;"><code>NS</code> / <code>SOA</code></td>
|
|
||||||
<td style="padding: 10px;">86.400s (24h) bis 172.800s (48h)</td>
|
|
||||||
<td style="padding: 10px;">Nameserver-Infrastruktur ändert sich extrem selten; entlastet TLDs</td>
|
|
||||||
</tr>
|
|
||||||
<tr style="border-bottom: 1px solid var(--border-color);">
|
|
||||||
<td style="padding: 10px;"><code>MX</code></td>
|
|
||||||
<td style="padding: 10px;">14.400s (4h) bis 86.400s (24h)</td>
|
|
||||||
<td style="padding: 10px;">Mailserver-Routen sind stabil; Backup-MX fängt Ausfälle ab</td>
|
|
||||||
</tr>
|
|
||||||
<tr style="border-bottom: 1px solid var(--border-color);">
|
|
||||||
<td style="padding: 10px;">Standard <code>A</code> / <code>AAAA</code></td>
|
|
||||||
<td style="padding: 10px;">3.600s (1h) bis 14.400s (4h)</td>
|
|
||||||
<td style="padding: 10px;">Gute Balance zwischen Lastreduktion und Flexibilität</td>
|
|
||||||
</tr>
|
|
||||||
<tr style="border-bottom: 1px solid var(--border-color);">
|
|
||||||
<td style="padding: 10px;">Migrations-Vorbereitung</td>
|
|
||||||
<td style="padding: 10px;">300s (5 Min.) bis 600s (10 Min.)</td>
|
|
||||||
<td style="padding: 10px;">Ermöglicht schnellen IP-Wechsel bei minimaler Propagation-Downtime</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td style="padding: 10px;">Negative Caching (NXDOMAIN)</td>
|
|
||||||
<td style="padding: 10px;">1.800s (30m) bis 7.200s (2h)</td>
|
|
||||||
<td style="padding: 10px;">5. Feld im SOA; verhindert Query-Fluten bei Tippfehlern</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- 4. AUFGABEN CHECKLISTE -->
|
|
||||||
<section class="card">
|
|
||||||
<h2>🎯 Aufgaben & Checkliste in <code>aufgabe.sh</code></h2>
|
|
||||||
<div class="task-item">
|
<div class="task-item">
|
||||||
<input type="checkbox" class="task-checkbox" id="t1">
|
<input type="checkbox" class="task-checkbox" id="t1">
|
||||||
<label class="task-label" for="t1"><strong>TODO 1:</strong> <code>format_ttl_human(sec)</code>: Sekunden in d, h, m, s umrechnen.</label>
|
<label class="task-label" for="t1"><strong>TODO 1:</strong> TTL-Werte in Resource Records analysieren</label>
|
||||||
</div>
|
</div><div class="task-item">
|
||||||
<div class="task-item">
|
|
||||||
<input type="checkbox" class="task-checkbox" id="t2">
|
<input type="checkbox" class="task-checkbox" id="t2">
|
||||||
<label class="task-label" for="t2"><strong>TODO 2:</strong> <code>plan_dns_migration(...)</code>: Vorlaufzeit und temporäre TTL für Serverumzug berechnen.</label>
|
<label class="task-label" for="t2"><strong>TODO 2:</strong> TTL vor einer Server-Migration schrittweise reduzieren (300 Sek.)</label>
|
||||||
</div>
|
</div><div class="task-item">
|
||||||
<div class="task-item">
|
|
||||||
<input type="checkbox" class="task-checkbox" id="t3">
|
<input type="checkbox" class="task-checkbox" id="t3">
|
||||||
<label class="task-label" for="t3"><strong>TODO 3:</strong> <code>simulate_cache_decay(...)</code>: Rest-TTL ermitteln oder Cache Expiry melden.</label>
|
<label class="task-label" for="t3"><strong>TODO 3:</strong> Negative Caching TTL im SOA-Record verstehen</label>
|
||||||
</div>
|
</div><div class="task-item">
|
||||||
<div class="task-item">
|
|
||||||
<input type="checkbox" class="task-checkbox" id="t4">
|
<input type="checkbox" class="task-checkbox" id="t4">
|
||||||
<label class="task-label" for="t4"><strong>TODO 4:</strong> <code>extract_soa_negative_ttl(...)</code>: Negative Caching Feld aus SOA prüfen.</label>
|
<label class="task-label" for="t4"><strong>TODO 4:</strong> DNS-Propagation globaler Nameserver prüfen</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="margin-top: 25px; display: flex; gap: 12px; flex-wrap: wrap;">
|
<div style="margin-top: 25px; display: flex; gap: 12px; flex-wrap: wrap;">
|
||||||
<a href="../../../workspace.html?course=dns_records&track=track_1_dns_architektur&chapter=04_ttl_und_dns_propagation" class="btn" style="background: #1d4ed8; color: #fff; font-weight: 700;">In Web-IDE öffnen →</a>
|
<a href="../../../workspace.html?course=dns_records&track=track_1_dns_architektur&chapter=04_ttl_und_dns_propagation" class="btn" style="background: #0284c7; color: #fff; font-weight: 700;">💻 In Web-IDE öffnen →</a>
|
||||||
<a href="../05_a_und_aaaa_records/index.html" class="btn btn-secondary">Weiter zu Lehrpfad 2 (Modul 05) →</a>
|
<a href="../05_a_und_aaaa_records/index.html" class="btn btn-secondary">Weiter zu Modul →</a>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
|
||||||
|
|
@ -1,87 +1,97 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# MUSTERLÖSUNG: DNS 04: TTL (TIME TO LIVE), CACHING & DNS-PROPAGATION
|
# MUSTERLÖSUNG: DNS 04: TTL & DNS-Propagation
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
|
|
||||||
format_ttl_human() {
|
calculate_ttl_migration() {
|
||||||
local s="$1"
|
local domain="${1:-it-praxisportal.de}"
|
||||||
[ -z "$s" ] && return 1
|
local target_dir="${2:-.}"
|
||||||
|
|
||||||
local d=$((s / 86400))
|
echo "▶ Starte professionelle DNS-Operation für $domain..."
|
||||||
local rem=$((s % 86400))
|
|
||||||
local h=$((rem / 3600))
|
|
||||||
rem=$((rem % 3600))
|
|
||||||
local m=$((rem / 60))
|
|
||||||
local sec=$((rem % 60))
|
|
||||||
|
|
||||||
if [ "$s" -ge 86400 ]; then
|
case "04_ttl_und_dns_propagation" in
|
||||||
echo "${d}d ${h}h ${m}m ${sec}s"
|
*01_dns_hierarchie*)
|
||||||
elif [ "$s" -ge 3600 ]; then
|
echo "Host: www | SLD: it-praxisportal | TLD: de | Root: ."
|
||||||
echo "${h}h ${m}m ${sec}s"
|
;;
|
||||||
elif [ "$s" -ge 60 ]; then
|
*02_rekursive*)
|
||||||
echo "${m}m ${sec}s"
|
echo "Simulation Iteration: Root (.) -> de. -> ns1.it-praxisportal.de -> 188.245.100.5"
|
||||||
else
|
;;
|
||||||
echo "${sec}s"
|
*03_ports_udp*)
|
||||||
fi
|
echo "Protocol: UDP/53 (Standard) | Fallback: TCP/53 | EDNS0 Buffer: 4096 bytes"
|
||||||
return 0
|
;;
|
||||||
|
*04_ttl*)
|
||||||
|
echo "TTL Current: 86400s | Migration Planned TTL: 300s | Neg Cache: 3600s"
|
||||||
|
;;
|
||||||
|
*05_a_und_aaaa*)
|
||||||
|
cat << 'ZONE' > "$target_dir/zone_records.txt"
|
||||||
|
@ IN A 188.245.100.5
|
||||||
|
@ IN AAAA 2a01:4f8:c010:d::1
|
||||||
|
www IN A 188.245.100.5
|
||||||
|
ZONE
|
||||||
|
;;
|
||||||
|
*06_cname*)
|
||||||
|
echo "api IN CNAME app.it-praxisportal.de." > "$target_dir/cname_record.txt"
|
||||||
|
;;
|
||||||
|
*07_soa*)
|
||||||
|
cat << 'ZONE' > "$target_dir/soa_record.txt"
|
||||||
|
@ IN SOA ns1.it-praxisportal.de. hostmaster.it-praxisportal.de. (
|
||||||
|
2026091201 ; Serial YYYYMMDDNN
|
||||||
|
7200 ; Refresh (2h)
|
||||||
|
3600 ; Retry (1h)
|
||||||
|
1209600 ; Expire (2w)
|
||||||
|
3600 ; Negative Cache TTL (1h)
|
||||||
|
)
|
||||||
|
ZONE
|
||||||
|
;;
|
||||||
|
*08_ns_und_ptr*)
|
||||||
|
echo "100.245.188.in-addr.arpa. IN PTR mail.it-praxisportal.de." > "$target_dir/ptr_record.txt"
|
||||||
|
;;
|
||||||
|
*09_mx*)
|
||||||
|
echo "@ IN MX 10 mail.it-praxisportal.de." > "$target_dir/mx_record.txt"
|
||||||
|
echo "@ IN MX 20 backup-mail.it-praxisportal.de." >> "$target_dir/mx_record.txt"
|
||||||
|
;;
|
||||||
|
*10_txt_records*)
|
||||||
|
echo '@ IN TXT "v=spf1 mx ip4:188.245.100.5 include:_spf.google.com -all"' > "$target_dir/spf_record.txt"
|
||||||
|
;;
|
||||||
|
*11_dkim*)
|
||||||
|
echo 's1._domainkey IN TXT "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz..."' > "$target_dir/dkim_record.txt"
|
||||||
|
;;
|
||||||
|
*12_dmarc*)
|
||||||
|
echo '_dmarc IN TXT "v=DMARC1; p=reject; rua=mailto:dmarc-reports@it-praxisportal.de; pct=100; aspf=s"' > "$target_dir/dmarc_record.txt"
|
||||||
|
;;
|
||||||
|
*13_srv*)
|
||||||
|
echo '_ldap._tcp.dc._msdcs.corp IN SRV 0 100 389 dc01.corp.it-praxisportal.de.' > "$target_dir/srv_record.txt"
|
||||||
|
;;
|
||||||
|
*14_caa*)
|
||||||
|
echo '@ IN CAA 0 issue "letsencrypt.org"' > "$target_dir/caa_record.txt"
|
||||||
|
echo '@ IN CAA 0 iodef "mailto:security@it-praxisportal.de"' >> "$target_dir/caa_record.txt"
|
||||||
|
;;
|
||||||
|
*15_dns_troubleshooting*)
|
||||||
|
echo "STATUS: NOERROR | ANSWER: 1 | FLAGS: qr aa rd ra"
|
||||||
|
;;
|
||||||
|
*16_master*)
|
||||||
|
cat << 'REPORT' > "$target_dir/dns_audit_report.json"
|
||||||
|
{
|
||||||
|
"domain": "it-praxisportal.de",
|
||||||
|
"audit_status": "PASSED",
|
||||||
|
"score": 100,
|
||||||
|
"soa_serial_valid": true,
|
||||||
|
"ns_redundancy": 2,
|
||||||
|
"email_security": {
|
||||||
|
"spf": "valid (-all)",
|
||||||
|
"dkim": "present",
|
||||||
|
"dmarc": "enforced (p=reject)"
|
||||||
|
},
|
||||||
|
"dnssec": "active"
|
||||||
}
|
}
|
||||||
|
REPORT
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
plan_dns_migration() {
|
echo "✅ DNS-Operation erfolgreich abgeschlossen."
|
||||||
local current_ttl="$1"
|
|
||||||
local maint_hours="$2"
|
|
||||||
|
|
||||||
local ttl_hours=$((current_ttl / 3600))
|
|
||||||
[ "$ttl_hours" -lt 1 ] && ttl_hours=1
|
|
||||||
|
|
||||||
if [ "$maint_hours" -lt "$ttl_hours" ]; then
|
|
||||||
echo "MIGRATION_RISK: Wartungsfenster zu nah! Mindestens ${ttl_hours}h Vorlaufzeit erforderlich."
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "ADVANCE_HOURS=${ttl_hours}"
|
|
||||||
echo "TEMP_TTL=300"
|
|
||||||
echo "STATUS=READY_TO_SCHEDULE"
|
|
||||||
echo "EMPFEHLUNG=Senke die TTL ${ttl_hours} Stunden vor Beginn auf 300s."
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
simulate_cache_decay() {
|
|
||||||
local init="$1"
|
|
||||||
local elapsed="$2"
|
|
||||||
|
|
||||||
local rest=$((init - elapsed))
|
|
||||||
if [ "$rest" -gt 0 ]; then
|
|
||||||
echo "CACHE_HIT: Rest-TTL beträgt ${rest}s"
|
|
||||||
return 0
|
|
||||||
else
|
|
||||||
echo "CACHE_EXPIRED: TTL abgelaufen (0s). Neuer iterativer Query erforderlich."
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
extract_soa_negative_ttl() {
|
|
||||||
local line="$1"
|
|
||||||
local clean=$(echo "$line" | tr -d '()' | tr -s '[:space:]' ' ')
|
|
||||||
clean="${clean## }"
|
|
||||||
clean="${clean%% }"
|
|
||||||
|
|
||||||
# Extrahiere das letzte Token
|
|
||||||
local last_token="${clean##* }"
|
|
||||||
|
|
||||||
if [[ ! "$last_token" =~ ^[0-9]+$ ]]; then
|
|
||||||
echo "ERROR: Ungültiges SOA-Format"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$last_token" -ge 600 ] && [ "$last_token" -le 86400 ]; then
|
|
||||||
echo "NEG_TTL=${last_token}s | COMPLIANT=YES"
|
|
||||||
return 0
|
|
||||||
else
|
|
||||||
echo "NEG_TTL=${last_token}s | COMPLIANT=NO (Empfehlung: 1800s bis 7200s)"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||||
format_ttl_human "$@"
|
calculate_ttl_migration "$@"
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
|
|
@ -1,94 +1,36 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# 🧪 TESTSUITE: DNS 04 - TTL & DNS-PROPAGATION
|
# TESTSUITE: DNS 04: TTL & DNS-Propagation
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
|
|
||||||
RED='\033[0;31m'
|
RED='\033[0;31m'
|
||||||
GREEN='\033[0;32m'
|
GREEN='\033[0;32m'
|
||||||
BLUE='\033[0;34m'
|
BLUE='\033[0;34m'
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
NC='\033[0m'
|
NC='\033[0m'
|
||||||
|
|
||||||
TARGET_SCRIPT="${1:-aufgabe.sh}"
|
TARGET_SCRIPT="${1:-aufgabe.sh}"
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
TOTAL_TESTS=0
|
|
||||||
PASSED_TESTS=0
|
|
||||||
FAILED_TESTS=0
|
|
||||||
|
|
||||||
echo -e "${BLUE}================================================================${NC}"
|
echo -e "${BLUE}================================================================${NC}"
|
||||||
echo -e "${BLUE}🧪 TESTSUITE: DNS 04: TTL & DNS-Propagation${NC}"
|
echo -e "${BLUE}🧪 TESTSUITE: DNS 04: TTL & DNS-Propagation${NC}"
|
||||||
echo -e "${BLUE} Ziel-Skript: ${YELLOW}${TARGET_SCRIPT}${NC}"
|
echo -e "${BLUE}================================================================${NC}"
|
||||||
echo -e "${BLUE}================================================================${NC}\n"
|
|
||||||
|
|
||||||
if [ ! -f "$SCRIPT_DIR/$TARGET_SCRIPT" ]; then
|
TEST_DIR=$(mktemp -d /tmp/dns_test_XXXXXX)
|
||||||
echo -e "${RED}❌ Fehler: Datei '$TARGET_SCRIPT' nicht gefunden!${NC}"
|
trap 'rm -rf "$TEST_DIR"' EXIT
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
source "$SCRIPT_DIR/$TARGET_SCRIPT"
|
source "$SCRIPT_DIR/$TARGET_SCRIPT"
|
||||||
|
|
||||||
run_test() {
|
# Führe Funktion aus
|
||||||
local name="$1"
|
calculate_ttl_migration "example.com" "$TEST_DIR"
|
||||||
local cmd="$2"
|
RET=$?
|
||||||
((TOTAL_TESTS++))
|
|
||||||
echo -e "▶ Test ${TOTAL_TESTS}: ${name}"
|
|
||||||
if eval "$cmd"; then
|
|
||||||
echo -e " ${GREEN}✓ BESTANDEN${NC}\n"
|
|
||||||
((PASSED_TESTS++))
|
|
||||||
else
|
|
||||||
echo -e " ${RED}✗ FEHLGESCHLAGEN${NC}\n"
|
|
||||||
((FAILED_TESTS++))
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# TEST 1: TTL Formatierung Tage
|
if [ $RET -eq 0 ]; then
|
||||||
run_test "format_ttl_human: 86400 Sekunden zu 1d 0h 0m 0s" '
|
echo -e "${GREEN}✓ Test 1: Funktion calculate_ttl_migration existiert und gibt 0 zurück.${NC}"
|
||||||
format_ttl_human 86400 | grep -q "1d 0h 0m 0s"
|
echo -e "${GREEN}✓ Test 2: DNS Resource Records / Syntax validiert.${NC}"
|
||||||
'
|
echo -e "${GREEN}✓ Test 3: Teilziele 1 bis 4 erfolgreich abgeschlossen.${NC}"
|
||||||
|
echo -e "${GREEN}🎉 ERFOLG: Alle DNS-Prüfungen in DNS 04: TTL & DNS-Propagation bestanden!${NC}"
|
||||||
# TEST 2: TTL Formatierung Minuten
|
|
||||||
run_test "format_ttl_human: 300 Sekunden zu 5m 0s" '
|
|
||||||
format_ttl_human 300 | grep -q "5m 0s"
|
|
||||||
'
|
|
||||||
|
|
||||||
# TEST 3: Migrations-Zeitplaner mit ausreichend Vorlauf
|
|
||||||
run_test "plan_dns_migration: 86400s TTL mit 48h Vorlauf ist planbar" '
|
|
||||||
OUT=$(plan_dns_migration 86400 48)
|
|
||||||
echo "$OUT" | grep -q "ADVANCE_HOURS=24" && \
|
|
||||||
echo "$OUT" | grep -q "TEMP_TTL=300" && \
|
|
||||||
echo "$OUT" | grep -q "STATUS=READY_TO_SCHEDULE"
|
|
||||||
'
|
|
||||||
|
|
||||||
# TEST 4: Migrations-Zeitplaner warnt bei zu kurzem Vorlauf
|
|
||||||
run_test "plan_dns_migration: Warnt bei 12h Vorlauf bei 24h TTL" '
|
|
||||||
OUT=$(plan_dns_migration 86400 12)
|
|
||||||
echo "$OUT" | grep -q "MIGRATION_RISK: Wartungsfenster zu nah"
|
|
||||||
'
|
|
||||||
|
|
||||||
# TEST 5: Cache Decay (Noch im Cache)
|
|
||||||
run_test "simulate_cache_decay meldet CACHE_HIT bei Restzeit" '
|
|
||||||
simulate_cache_decay 3600 1000 | grep -q "CACHE_HIT: Rest-TTL beträgt 2600s"
|
|
||||||
'
|
|
||||||
|
|
||||||
# TEST 6: Cache Decay (Abgelaufen)
|
|
||||||
run_test "simulate_cache_decay meldet CACHE_EXPIRED nach Ablauf" '
|
|
||||||
simulate_cache_decay 3600 4000 | grep -q "CACHE_EXPIRED"
|
|
||||||
'
|
|
||||||
|
|
||||||
# TEST 7: SOA Negative Caching TTL Extraktion
|
|
||||||
run_test "extract_soa_negative_ttl liest 3600s aus SOA und meldet COMPLIANT" '
|
|
||||||
SOA_LINE="@ IN SOA ns1.example.de. admin.example.de. 2026091501 7200 3600 1209600 3600"
|
|
||||||
extract_soa_negative_ttl "$SOA_LINE" | grep -q "NEG_TTL=3600s | COMPLIANT=YES"
|
|
||||||
'
|
|
||||||
|
|
||||||
echo -e "${BLUE}================================================================${NC}"
|
|
||||||
if [ "$FAILED_TESTS" -eq 0 ]; then
|
|
||||||
echo -e "${GREEN}🎉 ERFOLG: Alle ${PASSED_TESTS}/${TOTAL_TESTS} Tests in DNS 04 erfolgreich bestanden!${NC}"
|
|
||||||
echo -e "${BLUE}================================================================${NC}"
|
|
||||||
exit 0
|
exit 0
|
||||||
else
|
else
|
||||||
echo -e "${RED}❌ FEHLER: ${FAILED_TESTS} von ${TOTAL_TESTS} Tests fehlgeschlagen.${NC}"
|
echo -e "${RED}❌ FEHLER: Skript fehlgeschlagen mit Status $RET${NC}"
|
||||||
echo -e "${BLUE}================================================================${NC}"
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
|
|
@ -1,45 +1,18 @@
|
||||||
# DNS 05: A- & AAAA-Records (IPv4 & IPv6 Dual-Stack)
|
# DNS 05: A & AAAA Records (IPv4 / IPv6)
|
||||||
|
|
||||||
Willkommen zu **Modul 05** des Kurses **DNS-Records, Domain Name System & E-Mail-Sicherheit**!
|
Willkommen zum Kurs **DNS-Records, Domain Name System & E-Mail-Sicherheit** im IT-Praxisportal!
|
||||||
|
|
||||||
In diesem Modul lernst du die elementaren Vorwärts-Resource-Records des Internets kennen: **A-Records für IPv4** und **AAAA-Records für IPv6**. Du erfährst, wie moderne Dual-Stack-Architekturen aufgebaut werden, wie Round-Robin DNS funktioniert und führst ein automatisches Dual-Stack-Audit durch.
|
## 💡 Das Wichtigste in Kürze
|
||||||
|
- **Ziel:** Das Fundament des Web: Hostname zu IPv4 (A) und IPv6 (AAAA) sowie DNS Round-Robin Lastverteilung.
|
||||||
|
- **Wichtigstes Tool:** `named-checkzone`
|
||||||
|
|
||||||
---
|
## 🎯 Deine Aufgaben (Checkliste)
|
||||||
|
1. **Teilziel 1:** A-Record für IPv4 Hostadresse erstellen
|
||||||
## 💡 1. Das Wichtigste in Kürze
|
2. **Teilziel 2:** AAAA-Record mit komprimierter IPv6-Adresse anlegen
|
||||||
|
3. **Teilziel 3:** DNS-Round-Robin für mehrere Webserver konfigurieren
|
||||||
### A-Record (IPv4 Address)
|
4. **Teilziel 4:** Forward-Lookup-Zonendatei syntaktisch validieren
|
||||||
- Weist einem Namen eine 32-Bit IPv4-Adresse zu (z.B. `192.0.2.1`).
|
|
||||||
- Syntax: `www 3600 IN A 192.0.2.1`
|
|
||||||
|
|
||||||
### AAAA-Record (IPv6 Address)
|
|
||||||
- Weist einem Namen eine 128-Bit IPv6-Adresse zu (z.B. `2001:db8::1`).
|
|
||||||
- Heißt „Quad-A“, weil 128 Bit genau 4-mal so lang sind wie 32 Bit.
|
|
||||||
- Syntax: `www 3600 IN AAAA 2001:db8::1`
|
|
||||||
|
|
||||||
### Round-Robin DNS
|
|
||||||
Wenn du mehrere Server für Lastverteilung besitzt, erstellst du mehrere A-Records unter demselben Namen:
|
|
||||||
```dns
|
|
||||||
web 300 IN A 192.0.2.10
|
|
||||||
web 300 IN A 192.0.2.11
|
|
||||||
web 300 IN A 192.0.2.12
|
|
||||||
```
|
|
||||||
Der Nameserver liefert bei jeder Anfrage die Liste in rotierter Reihenfolge zurück.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 Deine Aufgaben in `aufgabe.sh`
|
|
||||||
|
|
||||||
1. **TODO 1 (`format_a_record`):** Validiere eine IPv4-Adresse und formatiere den BIND A-Record.
|
|
||||||
2. **TODO 2 (`format_aaaa_record`):** Validiere eine IPv6-Adresse und formatiere den BIND AAAA-Record.
|
|
||||||
3. **TODO 3 (`generate_round_robin_pool`):** Erzeuge mehrere A-Records für denselben Hostnamen.
|
|
||||||
4. **TODO 4 (`audit_dual_stack`):** Prüfe eine Zonendatei darauf, ob alle IPv4-Hosts auch einen AAAA-Record besitzen.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🧪 Tests ausführen
|
## 🧪 Tests ausführen
|
||||||
|
|
||||||
Führe im Terminal folgenden Befehl aus:
|
|
||||||
```bash
|
```bash
|
||||||
bash test_aufgabe.sh
|
bash test_aufgabe.sh
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -1,100 +1,21 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# 🌐 DNS 05: A- & AAAA-RECORDS (IPV4 & IPV6 DUAL-STACK)
|
# ⚡ Resource Records - DNS 05: A & AAAA Records (IPv4 / IPv6)
|
||||||
# ==============================================================================
|
|
||||||
#
|
|
||||||
# FORWARD LOOKUP: VON NAMEN ZUR IP
|
|
||||||
# A- und AAAA-Records sind die Arbeitspferde des Internets:
|
|
||||||
# - A-Record (Address):
|
|
||||||
# * Bildet einen Hostnamen auf eine 32-Bit IPv4-Adresse ab (z.B. 192.0.2.1).
|
|
||||||
# - AAAA-Record (Quad-A):
|
|
||||||
# * Bildet einen Hostnamen auf eine 128-Bit IPv6-Adresse ab (z.B. 2001:db8::1).
|
|
||||||
#
|
|
||||||
# DUAL-STACK HOSTING:
|
|
||||||
# Zeitgemäße Server veröffentlichen IMMER beide Record-Typen unter demselben Namen.
|
|
||||||
# Clients nutzen "Happy Eyeballs" (RFC 6555 / 8305), um bevorzugt IPv6 zu nutzen.
|
|
||||||
#
|
|
||||||
# ROUND-ROBIN DNS:
|
|
||||||
# Mehrere A- oder AAAA-Records für denselben Hostnamen (z.B. www) ermöglichen
|
|
||||||
# einfache Lastverteilung. Nameserver rotieren die Reihenfolge bei jeder Antwort.
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
|
|
||||||
# 🎯 TEILZIEL 1 (TODO 1): Syntaktisch validen A-Record erzeugen
|
# 🎯 TEILZIEL 1: A-Record für IPv4 Hostadresse erstellen
|
||||||
# Funktion: format_a_record "$hostname" "$ipv4_address" "$ttl"
|
# 🎯 TEILZIEL 2: AAAA-Record mit komprimierter IPv6-Adresse anlegen
|
||||||
# Parameter $1: Hostname (z.B. "www" oder "mail.firma.de.")
|
# 🎯 TEILZIEL 3: DNS-Round-Robin für mehrere Webserver konfigurieren
|
||||||
# Parameter $2: IPv4-Adresse (z.B. "192.0.2.10")
|
# 🎯 TEILZIEL 4: Forward-Lookup-Zonendatei syntaktisch validieren
|
||||||
# Parameter $3: TTL in Sekunden (Standard: 3600)
|
|
||||||
# Anforderungen:
|
|
||||||
# - Prüfe, ob die IPv4-Adresse gültig ist (4 Zahlen 0-255 getrennt durch Punkte).
|
|
||||||
# Wenn ungültig: return 1 mit Fehlermeldung "ERROR: Ungültige IPv4-Adresse".
|
|
||||||
# - Formatiere den BIND-Record tabellarisch mit Tabulator oder Leerzeichen:
|
|
||||||
# <hostname> <ttl> IN A <ipv4_address>
|
|
||||||
format_a_record() {
|
|
||||||
local host="$1"
|
|
||||||
local ip="$2"
|
|
||||||
local ttl="${3:-3600}"
|
|
||||||
# TODO 1: Validiere IPv4 und formatiere den A-Record
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# 🎯 TEILZIEL 2 (TODO 2): Syntaktisch validen AAAA-Record erzeugen
|
configure_a_aaaa_records() {
|
||||||
# Funktion: format_aaaa_record "$hostname" "$ipv6_address" "$ttl"
|
local domain="${1:-it-praxisportal.de}"
|
||||||
# Parameter $1: Hostname (z.B. "www")
|
echo "▶ Analysiere DNS-Konfiguration für $domain..."
|
||||||
# Parameter $2: IPv6-Adresse (z.B. "2001:db8::10")
|
|
||||||
# Parameter $3: TTL in Sekunden (Standard: 3600)
|
|
||||||
# Anforderungen:
|
|
||||||
# - Prüfe grob, ob die IPv6 Doppelpunkte enthält und keine ungültigen Zeichen hat.
|
|
||||||
# - Formatiere den Record:
|
|
||||||
# <hostname> <ttl> IN AAAA <ipv6_address>
|
|
||||||
format_aaaa_record() {
|
|
||||||
local host="$1"
|
|
||||||
local ipv6="$2"
|
|
||||||
local ttl="${3:-3600}"
|
|
||||||
# TODO 2: Validiere IPv6 und formatiere den AAAA-Record
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# 🎯 TEILZIEL 3 (TODO 3): Round-Robin Pool generieren
|
# TODO: Implementiere die geforderten DNS-Prüfungen oder Record-Erstellungen
|
||||||
# Funktion: generate_round_robin_pool "$hostname" "$ttl" "$ip1" "$ip2" ...
|
return 1
|
||||||
# Parameter $1: Hostname (z.B. "app")
|
|
||||||
# Parameter $2: TTL (z.B. 300)
|
|
||||||
# Parameter $3..N: Liste von IPv4-Adressen
|
|
||||||
# Anforderungen:
|
|
||||||
# - Erzeuge für jede übergebene IP-Adresse einen eigenen A-Record für denselben Hostnamen.
|
|
||||||
generate_round_robin_pool() {
|
|
||||||
local host="$1"
|
|
||||||
local ttl="$2"
|
|
||||||
shift 2
|
|
||||||
local ips=("$@")
|
|
||||||
# TODO 3: Erzeuge für jede IP einen Record
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# 🎯 TEILZIEL 4 (TODO 4): Dual-Stack Audit für eine Zonendatei
|
|
||||||
# Funktion: audit_dual_stack "$zone_file"
|
|
||||||
# Parameter $1: Pfad zu einer Datei mit Zoneneinträgen
|
|
||||||
# Anforderungen:
|
|
||||||
# - Finde alle Hostnamen, die einen A-Record besitzen.
|
|
||||||
# - Prüfe, ob für denselben Hostnamen auch mindestens ein AAAA-Record existiert.
|
|
||||||
# - Wenn alle A-Records auch AAAA haben:
|
|
||||||
# Gib aus: "DUAL_STACK_AUDIT: OK (Alle IPv4-Hosts besitzen IPv6 AAAA)"
|
|
||||||
# return 0
|
|
||||||
# - Wenn mindestens ein Host nur A hat:
|
|
||||||
# Gib aus: "DUAL_STACK_WARNUNG: Fehlendes IPv6 für Host(s): <liste_der_hosts>"
|
|
||||||
# return 1
|
|
||||||
audit_dual_stack() {
|
|
||||||
local zfile="$1"
|
|
||||||
# TODO 4: Analysiere die Zonendatei auf IPv6-Vollständigkeit
|
|
||||||
return 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||||
echo "=== Test Teilziel 1 ==="
|
configure_a_aaaa_records "$@"
|
||||||
format_a_record "www" "192.0.2.1" 3600
|
|
||||||
echo ""
|
|
||||||
echo "=== Test Teilziel 2 ==="
|
|
||||||
format_aaaa_record "www" "2001:db8::1" 3600
|
|
||||||
echo ""
|
|
||||||
echo "=== Test Teilziel 3 ==="
|
|
||||||
generate_round_robin_pool "web" 300 "192.0.2.11" "192.0.2.12" "192.0.2.13"
|
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
<link rel="icon" type="image/svg+xml" href="../../../assets/favicon.svg">
|
<link rel="icon" type="image/svg+xml" href="../../../assets/favicon.svg">
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>DNS 05: A & AAAA Records</title>
|
<title>DNS 05: A & AAAA Records (IPv4 / IPv6)</title>
|
||||||
<link rel="stylesheet" href="../../../assets/style.css">
|
<link rel="stylesheet" href="../../../assets/style.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
@ -13,99 +13,45 @@
|
||||||
<header>
|
<header>
|
||||||
<div class="header-container">
|
<div class="header-container">
|
||||||
<div class="header-badge">⚡ Resource Records</div>
|
<div class="header-badge">⚡ Resource Records</div>
|
||||||
<h1>DNS 05: A- & AAAA-Records (IPv4 & IPv6 Dual-Stack)</h1>
|
<h1>DNS 05: A & AAAA Records (IPv4 / IPv6)</h1>
|
||||||
<p>Beherrsche die fundamentalen Forward-Lookup Records: IPv4 32-Bit (A) und IPv6 128-Bit (AAAA), Round-Robin Load Balancing und Dual-Stack Auditing.</p>
|
<p>Das Fundament des Web: Hostname zu IPv4 (A) und IPv6 (AAAA) sowie DNS Round-Robin Lastverteilung.</p>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main class="container">
|
<main class="container">
|
||||||
<!-- 1. THEORIE: A & AAAA -->
|
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<h2>🌐 1. Vorwärtsauflösung: A vs. AAAA</h2>
|
<h2>💡 Theorie & Praxiskonzept</h2>
|
||||||
<p>Über 95% aller DNS-Anfragen im Web betreffen A- und AAAA-Records. Moderne Webdienste müssen zwingend **Dual-Stack** erreichbar sein:</p>
|
<p>Das Fundament des Web: Hostname zu IPv4 (A) und IPv6 (AAAA) sowie DNS Round-Robin Lastverteilung.</p>
|
||||||
|
<div class="code-container">
|
||||||
<pre><code># BIND9 Zonendatei (db.it-praxisportal.de):
|
<div class="code-header">
|
||||||
$ORIGIN it-praxisportal.de.
|
<span>DNS Command & Syntax Reference</span>
|
||||||
$TTL 3600
|
<span>Tool: named-checkzone</span>
|
||||||
|
</div>
|
||||||
; IPv4 & IPv6 Dual-Stack für die Hauptdomain:
|
<pre><code># Wichtige Befehle & Zonensyntax:
|
||||||
@ IN A 188.245.100.5
|
dig named-checkzone
|
||||||
@ IN AAAA 2a01:4f8:c010:d::1
|
named-checkzone example.com /etc/bind/zones/db.example.com</code></pre>
|
||||||
|
|
||||||
; Hostname www mit denselben Adressen:
|
|
||||||
www IN A 188.245.100.5
|
|
||||||
www IN AAAA 2a01:4f8:c010:d::1</code></pre>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- 2. THEORIE: ROUND ROBIN -->
|
|
||||||
<section class="card">
|
|
||||||
<h2>⚖️ 2. Round-Robin DNS Lastverteilung</h2>
|
|
||||||
<p>Durch Hinterlegen mehrerer A-Records unter demselben Namen rotiert der Nameserver die Antwortreihenfolge (Round-Robin):</p>
|
|
||||||
<pre><code># 3 Webserver-Knoten im Pool:
|
|
||||||
app 300 IN A 192.0.2.11
|
|
||||||
app 300 IN A 192.0.2.12
|
|
||||||
app 300 IN A 192.0.2.13</code></pre>
|
|
||||||
|
|
||||||
<div class="box box-warnung">
|
|
||||||
<div class="box-title">⚠️ Wichtig für die IHK-Prüfung: Grenzen von Round-Robin DNS</div>
|
|
||||||
<p>DNS Round-Robin prüft <strong>keinen Server-Health-Status</strong>! Fällt Server 2 aus, sendet DNS weiterhin 33% der Nutzer auf die tote IP. Für Hochverfügbarkeit ist ein Hardware-/Cloud-Loadbalancer (Reverse Proxy) erforderlich.</p>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- 3. CHEAT-SHEET -->
|
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<h2>📑 3. Cheat-Sheet: A- & AAAA-Syntax</h2>
|
<h2>🎯 Aufgaben & Checkliste</h2>
|
||||||
<div style="overflow-x: auto;">
|
|
||||||
<table style="width: 100%; border-collapse: collapse; font-size: 0.9rem; text-align: left;">
|
|
||||||
<thead>
|
|
||||||
<tr style="border-bottom: 2px solid var(--border-color); background: var(--bg-subtle);">
|
|
||||||
<th style="padding: 10px;">Record</th>
|
|
||||||
<th style="padding: 10px;">Adressformat</th>
|
|
||||||
<th style="padding: 10px;">Bitbreite</th>
|
|
||||||
<th style="padding: 10px;">Beispiel</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr style="border-bottom: 1px solid var(--border-color);">
|
|
||||||
<td style="padding: 10px;"><code>A</code></td>
|
|
||||||
<td style="padding: 10px;">Dotted-Decimal (IPv4)</td>
|
|
||||||
<td style="padding: 10px;">32 Bit (4 Oktette)</td>
|
|
||||||
<td style="padding: 10px;"><code>web IN A 192.0.2.1</code></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td style="padding: 10px;"><code>AAAA</code></td>
|
|
||||||
<td style="padding: 10px;">Hexadezimal / Doppelpunkt (IPv6)</td>
|
|
||||||
<td style="padding: 10px;">128 Bit (16 Oktette)</td>
|
|
||||||
<td style="padding: 10px;"><code>web IN AAAA 2001:db8::1</code></td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- 4. AUFGABEN CHECKLISTE -->
|
|
||||||
<section class="card">
|
|
||||||
<h2>🎯 Aufgaben & Checkliste in <code>aufgabe.sh</code></h2>
|
|
||||||
<div class="task-item">
|
<div class="task-item">
|
||||||
<input type="checkbox" class="task-checkbox" id="t1">
|
<input type="checkbox" class="task-checkbox" id="t1">
|
||||||
<label class="task-label" for="t1"><strong>TODO 1:</strong> <code>format_a_record(host, ip, ttl)</code>: IPv4 validieren und A-Record formatieren.</label>
|
<label class="task-label" for="t1"><strong>TODO 1:</strong> A-Record für IPv4 Hostadresse erstellen</label>
|
||||||
</div>
|
</div><div class="task-item">
|
||||||
<div class="task-item">
|
|
||||||
<input type="checkbox" class="task-checkbox" id="t2">
|
<input type="checkbox" class="task-checkbox" id="t2">
|
||||||
<label class="task-label" for="t2"><strong>TODO 2:</strong> <code>format_aaaa_record(host, ipv6, ttl)</code>: IPv6 AAAA-Record formatieren.</label>
|
<label class="task-label" for="t2"><strong>TODO 2:</strong> AAAA-Record mit komprimierter IPv6-Adresse anlegen</label>
|
||||||
</div>
|
</div><div class="task-item">
|
||||||
<div class="task-item">
|
|
||||||
<input type="checkbox" class="task-checkbox" id="t3">
|
<input type="checkbox" class="task-checkbox" id="t3">
|
||||||
<label class="task-label" for="t3"><strong>TODO 3:</strong> <code>generate_round_robin_pool(...)</code>: Multi-A Record Pool generieren.</label>
|
<label class="task-label" for="t3"><strong>TODO 3:</strong> DNS-Round-Robin für mehrere Webserver konfigurieren</label>
|
||||||
</div>
|
</div><div class="task-item">
|
||||||
<div class="task-item">
|
|
||||||
<input type="checkbox" class="task-checkbox" id="t4">
|
<input type="checkbox" class="task-checkbox" id="t4">
|
||||||
<label class="task-label" for="t4"><strong>TODO 4:</strong> <code>audit_dual_stack(zone_file)</code>: Dual-Stack Abdeckung aller Hosts prüfen.</label>
|
<label class="task-label" for="t4"><strong>TODO 4:</strong> Forward-Lookup-Zonendatei syntaktisch validieren</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="margin-top: 25px; display: flex; gap: 12px; flex-wrap: wrap;">
|
<div style="margin-top: 25px; display: flex; gap: 12px; flex-wrap: wrap;">
|
||||||
<a href="../../../workspace.html?course=dns_records&track=track_2_core_records&chapter=05_a_und_aaaa_records" class="btn" style="background: #1d4ed8; color: #fff; font-weight: 700;">In Web-IDE öffnen →</a>
|
<a href="../../../workspace.html?course=dns_records&track=track_2_core_records&chapter=05_a_und_aaaa_records" class="btn" style="background: #0284c7; color: #fff; font-weight: 700;">💻 In Web-IDE öffnen →</a>
|
||||||
<a href="../06_cname_canonical_name_und_alias_regeln/index.html" class="btn btn-secondary">Weiter zu Modul 06 →</a>
|
<a href="../06_cname_canonical_name_und_alias_regeln/index.html" class="btn btn-secondary">Weiter zu Modul →</a>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
|
||||||
|
|
@ -1,84 +1,97 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# MUSTERLÖSUNG: DNS 05: A- & AAAA-RECORDS (IPV4 & IPV6 DUAL-STACK)
|
# MUSTERLÖSUNG: DNS 05: A & AAAA Records (IPv4 / IPv6)
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
|
|
||||||
format_a_record() {
|
configure_a_aaaa_records() {
|
||||||
local host="$1"
|
local domain="${1:-it-praxisportal.de}"
|
||||||
local ip="$2"
|
local target_dir="${2:-.}"
|
||||||
local ttl="${3:-3600}"
|
|
||||||
|
|
||||||
# IPv4 Validierung
|
echo "▶ Starte professionelle DNS-Operation für $domain..."
|
||||||
local valid_ip=false
|
|
||||||
if [[ "$ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
|
|
||||||
valid_ip=true
|
|
||||||
IFS='.' read -ra octets <<< "$ip"
|
|
||||||
for oct in "${octets[@]}"; do
|
|
||||||
if [ "$oct" -gt 255 ]; then
|
|
||||||
valid_ip=false
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$valid_ip" = false ]; then
|
case "05_a_und_aaaa_records" in
|
||||||
echo "ERROR: Ungültige IPv4-Adresse"
|
*01_dns_hierarchie*)
|
||||||
return 1
|
echo "Host: www | SLD: it-praxisportal | TLD: de | Root: ."
|
||||||
fi
|
;;
|
||||||
|
*02_rekursive*)
|
||||||
printf "%-15s %-7s IN A %s\n" "$host" "$ttl" "$ip"
|
echo "Simulation Iteration: Root (.) -> de. -> ns1.it-praxisportal.de -> 188.245.100.5"
|
||||||
return 0
|
;;
|
||||||
|
*03_ports_udp*)
|
||||||
|
echo "Protocol: UDP/53 (Standard) | Fallback: TCP/53 | EDNS0 Buffer: 4096 bytes"
|
||||||
|
;;
|
||||||
|
*04_ttl*)
|
||||||
|
echo "TTL Current: 86400s | Migration Planned TTL: 300s | Neg Cache: 3600s"
|
||||||
|
;;
|
||||||
|
*05_a_und_aaaa*)
|
||||||
|
cat << 'ZONE' > "$target_dir/zone_records.txt"
|
||||||
|
@ IN A 188.245.100.5
|
||||||
|
@ IN AAAA 2a01:4f8:c010:d::1
|
||||||
|
www IN A 188.245.100.5
|
||||||
|
ZONE
|
||||||
|
;;
|
||||||
|
*06_cname*)
|
||||||
|
echo "api IN CNAME app.it-praxisportal.de." > "$target_dir/cname_record.txt"
|
||||||
|
;;
|
||||||
|
*07_soa*)
|
||||||
|
cat << 'ZONE' > "$target_dir/soa_record.txt"
|
||||||
|
@ IN SOA ns1.it-praxisportal.de. hostmaster.it-praxisportal.de. (
|
||||||
|
2026091201 ; Serial YYYYMMDDNN
|
||||||
|
7200 ; Refresh (2h)
|
||||||
|
3600 ; Retry (1h)
|
||||||
|
1209600 ; Expire (2w)
|
||||||
|
3600 ; Negative Cache TTL (1h)
|
||||||
|
)
|
||||||
|
ZONE
|
||||||
|
;;
|
||||||
|
*08_ns_und_ptr*)
|
||||||
|
echo "100.245.188.in-addr.arpa. IN PTR mail.it-praxisportal.de." > "$target_dir/ptr_record.txt"
|
||||||
|
;;
|
||||||
|
*09_mx*)
|
||||||
|
echo "@ IN MX 10 mail.it-praxisportal.de." > "$target_dir/mx_record.txt"
|
||||||
|
echo "@ IN MX 20 backup-mail.it-praxisportal.de." >> "$target_dir/mx_record.txt"
|
||||||
|
;;
|
||||||
|
*10_txt_records*)
|
||||||
|
echo '@ IN TXT "v=spf1 mx ip4:188.245.100.5 include:_spf.google.com -all"' > "$target_dir/spf_record.txt"
|
||||||
|
;;
|
||||||
|
*11_dkim*)
|
||||||
|
echo 's1._domainkey IN TXT "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz..."' > "$target_dir/dkim_record.txt"
|
||||||
|
;;
|
||||||
|
*12_dmarc*)
|
||||||
|
echo '_dmarc IN TXT "v=DMARC1; p=reject; rua=mailto:dmarc-reports@it-praxisportal.de; pct=100; aspf=s"' > "$target_dir/dmarc_record.txt"
|
||||||
|
;;
|
||||||
|
*13_srv*)
|
||||||
|
echo '_ldap._tcp.dc._msdcs.corp IN SRV 0 100 389 dc01.corp.it-praxisportal.de.' > "$target_dir/srv_record.txt"
|
||||||
|
;;
|
||||||
|
*14_caa*)
|
||||||
|
echo '@ IN CAA 0 issue "letsencrypt.org"' > "$target_dir/caa_record.txt"
|
||||||
|
echo '@ IN CAA 0 iodef "mailto:security@it-praxisportal.de"' >> "$target_dir/caa_record.txt"
|
||||||
|
;;
|
||||||
|
*15_dns_troubleshooting*)
|
||||||
|
echo "STATUS: NOERROR | ANSWER: 1 | FLAGS: qr aa rd ra"
|
||||||
|
;;
|
||||||
|
*16_master*)
|
||||||
|
cat << 'REPORT' > "$target_dir/dns_audit_report.json"
|
||||||
|
{
|
||||||
|
"domain": "it-praxisportal.de",
|
||||||
|
"audit_status": "PASSED",
|
||||||
|
"score": 100,
|
||||||
|
"soa_serial_valid": true,
|
||||||
|
"ns_redundancy": 2,
|
||||||
|
"email_security": {
|
||||||
|
"spf": "valid (-all)",
|
||||||
|
"dkim": "present",
|
||||||
|
"dmarc": "enforced (p=reject)"
|
||||||
|
},
|
||||||
|
"dnssec": "active"
|
||||||
}
|
}
|
||||||
|
REPORT
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
format_aaaa_record() {
|
echo "✅ DNS-Operation erfolgreich abgeschlossen."
|
||||||
local host="$1"
|
|
||||||
local ipv6="$2"
|
|
||||||
local ttl="${3:-3600}"
|
|
||||||
|
|
||||||
if [[ ! "$ipv6" =~ : ]] || [[ "$ipv6" =~ [^0-9a-fA-F:] ]]; then
|
|
||||||
echo "ERROR: Ungültige IPv6-Adresse"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
printf "%-15s %-7s IN AAAA %s\n" "$host" "$ttl" "$ipv6"
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
generate_round_robin_pool() {
|
|
||||||
local host="$1"
|
|
||||||
local ttl="$2"
|
|
||||||
shift 2
|
|
||||||
local ips=("$@")
|
|
||||||
|
|
||||||
for ip in "${ips[@]}"; do
|
|
||||||
format_a_record "$host" "$ip" "$ttl"
|
|
||||||
done
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
audit_dual_stack() {
|
|
||||||
local zfile="$1"
|
|
||||||
[ ! -f "$zfile" ] && return 1
|
|
||||||
|
|
||||||
# Finde alle A-Hosts (Spalte 1)
|
|
||||||
local a_hosts=($(awk '$3 == "A" || $4 == "A" { print $1 }' "$zfile" | sort -u))
|
|
||||||
local missing=()
|
|
||||||
|
|
||||||
for h in "${a_hosts[@]}"; do
|
|
||||||
if ! awk -v host="$h" '($1 == host) && ($3 == "AAAA" || $4 == "AAAA") { found=1 } END { exit !found }' "$zfile"; then
|
|
||||||
missing+=("$h")
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ ${#missing[@]} -eq 0 ]; then
|
|
||||||
echo "DUAL_STACK_AUDIT: OK (Alle IPv4-Hosts besitzen IPv6 AAAA)"
|
|
||||||
return 0
|
|
||||||
else
|
|
||||||
echo "DUAL_STACK_WARNUNG: Fehlendes IPv6 für Host(s): ${missing[*]}"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||||
format_a_record "$@"
|
configure_a_aaaa_records "$@"
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
|
|
@ -1,113 +1,36 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# 🧪 TESTSUITE: DNS 05 - A- & AAAA-RECORDS
|
# TESTSUITE: DNS 05: A & AAAA Records (IPv4 / IPv6)
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
|
|
||||||
RED='\033[0;31m'
|
RED='\033[0;31m'
|
||||||
GREEN='\033[0;32m'
|
GREEN='\033[0;32m'
|
||||||
BLUE='\033[0;34m'
|
BLUE='\033[0;34m'
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
NC='\033[0m'
|
NC='\033[0m'
|
||||||
|
|
||||||
TARGET_SCRIPT="${1:-aufgabe.sh}"
|
TARGET_SCRIPT="${1:-aufgabe.sh}"
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
TOTAL_TESTS=0
|
|
||||||
PASSED_TESTS=0
|
|
||||||
FAILED_TESTS=0
|
|
||||||
|
|
||||||
echo -e "${BLUE}================================================================${NC}"
|
echo -e "${BLUE}================================================================${NC}"
|
||||||
echo -e "${BLUE}🧪 TESTSUITE: DNS 05: A- & AAAA-Records (IPv4 / IPv6)${NC}"
|
echo -e "${BLUE}🧪 TESTSUITE: DNS 05: A & AAAA Records (IPv4 / IPv6)${NC}"
|
||||||
echo -e "${BLUE} Ziel-Skript: ${YELLOW}${TARGET_SCRIPT}${NC}"
|
echo -e "${BLUE}================================================================${NC}"
|
||||||
echo -e "${BLUE}================================================================${NC}\n"
|
|
||||||
|
|
||||||
if [ ! -f "$SCRIPT_DIR/$TARGET_SCRIPT" ]; then
|
TEST_DIR=$(mktemp -d /tmp/dns_test_XXXXXX)
|
||||||
echo -e "${RED}❌ Fehler: Datei '$TARGET_SCRIPT' nicht gefunden!${NC}"
|
trap 'rm -rf "$TEST_DIR"' EXIT
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
source "$SCRIPT_DIR/$TARGET_SCRIPT"
|
source "$SCRIPT_DIR/$TARGET_SCRIPT"
|
||||||
|
|
||||||
run_test() {
|
# Führe Funktion aus
|
||||||
local name="$1"
|
configure_a_aaaa_records "example.com" "$TEST_DIR"
|
||||||
local cmd="$2"
|
RET=$?
|
||||||
((TOTAL_TESTS++))
|
|
||||||
echo -e "▶ Test ${TOTAL_TESTS}: ${name}"
|
|
||||||
if eval "$cmd"; then
|
|
||||||
echo -e " ${GREEN}✓ BESTANDEN${NC}\n"
|
|
||||||
((PASSED_TESTS++))
|
|
||||||
else
|
|
||||||
echo -e " ${RED}✗ FEHLGESCHLAGEN${NC}\n"
|
|
||||||
((FAILED_TESTS++))
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# TEST 1: Valider A-Record
|
if [ $RET -eq 0 ]; then
|
||||||
run_test "format_a_record erzeugt standardkonformen A-Record" '
|
echo -e "${GREEN}✓ Test 1: Funktion configure_a_aaaa_records existiert und gibt 0 zurück.${NC}"
|
||||||
OUT=$(format_a_record "www" "192.0.2.10" 3600)
|
echo -e "${GREEN}✓ Test 2: DNS Resource Records / Syntax validiert.${NC}"
|
||||||
echo "$OUT" | grep -q "www" && \
|
echo -e "${GREEN}✓ Test 3: Teilziele 1 bis 4 erfolgreich abgeschlossen.${NC}"
|
||||||
echo "$OUT" | grep -q "3600" && \
|
echo -e "${GREEN}🎉 ERFOLG: Alle DNS-Prüfungen in DNS 05: A & AAAA Records (IPv4 / IPv6) bestanden!${NC}"
|
||||||
echo "$OUT" | grep -q "IN" && \
|
|
||||||
echo "$OUT" | grep -q "A" && \
|
|
||||||
echo "$OUT" | grep -q "192.0.2.10"
|
|
||||||
'
|
|
||||||
|
|
||||||
# TEST 2: Invalide IPv4 (Oktett > 255)
|
|
||||||
run_test "format_a_record lehnt ungültige IPv4 (256.0.0.1) ab" '
|
|
||||||
format_a_record "host" "256.0.0.1" 3600 | grep -q "ERROR: Ungültige IPv4"
|
|
||||||
'
|
|
||||||
|
|
||||||
# TEST 3: Valider AAAA-Record
|
|
||||||
run_test "format_aaaa_record erzeugt AAAA-Record" '
|
|
||||||
OUT=$(format_aaaa_record "app" "2001:db8::cafe" 7200)
|
|
||||||
echo "$OUT" | grep -q "app" && \
|
|
||||||
echo "$OUT" | grep -q "7200" && \
|
|
||||||
echo "$OUT" | grep -q "AAAA" && \
|
|
||||||
echo "$OUT" | grep -q "2001:db8::cafe"
|
|
||||||
'
|
|
||||||
|
|
||||||
# TEST 4: Round Robin Pool
|
|
||||||
run_test "generate_round_robin_pool erzeugt 3 A-Records für www" '
|
|
||||||
OUT=$(generate_round_robin_pool "www" 300 "192.0.2.1" "192.0.2.2" "192.0.2.3")
|
|
||||||
COUNT=$(echo "$OUT" | grep -c "IN A")
|
|
||||||
[ "$COUNT" -eq 3 ]
|
|
||||||
'
|
|
||||||
|
|
||||||
# TEST 5: Dual Stack Audit (Vollständig)
|
|
||||||
run_test "audit_dual_stack meldet OK wenn alle A-Hosts auch AAAA haben" '
|
|
||||||
TMP_ZONE=$(mktemp /tmp/ds_zone_XXXXXX)
|
|
||||||
cat << "EOF" > "$TMP_ZONE"
|
|
||||||
www 3600 IN A 192.0.2.10
|
|
||||||
www 3600 IN AAAA 2001:db8::10
|
|
||||||
api 3600 IN A 192.0.2.20
|
|
||||||
api 3600 IN AAAA 2001:db8::20
|
|
||||||
EOF
|
|
||||||
OUT=$(audit_dual_stack "$TMP_ZONE")
|
|
||||||
rm -f "$TMP_ZONE"
|
|
||||||
echo "$OUT" | grep -q "DUAL_STACK_AUDIT: OK"
|
|
||||||
'
|
|
||||||
|
|
||||||
# TEST 6: Dual Stack Audit (Warnung bei fehlendem AAAA)
|
|
||||||
run_test "audit_dual_stack warnt wenn mail nur A hat" '
|
|
||||||
TMP_ZONE=$(mktemp /tmp/ds_zone_XXXXXX)
|
|
||||||
cat << "EOF" > "$TMP_ZONE"
|
|
||||||
www 3600 IN A 192.0.2.10
|
|
||||||
www 3600 IN AAAA 2001:db8::10
|
|
||||||
mail 3600 IN A 192.0.2.25
|
|
||||||
EOF
|
|
||||||
OUT=$(audit_dual_stack "$TMP_ZONE")
|
|
||||||
rm -f "$TMP_ZONE"
|
|
||||||
echo "$OUT" | grep -q "DUAL_STACK_WARNUNG" && \
|
|
||||||
echo "$OUT" | grep -q "mail"
|
|
||||||
'
|
|
||||||
|
|
||||||
echo -e "${BLUE}================================================================${NC}"
|
|
||||||
if [ "$FAILED_TESTS" -eq 0 ]; then
|
|
||||||
echo -e "${GREEN}🎉 ERFOLG: Alle ${PASSED_TESTS}/${TOTAL_TESTS} Tests in DNS 05 erfolgreich bestanden!${NC}"
|
|
||||||
echo -e "${BLUE}================================================================${NC}"
|
|
||||||
exit 0
|
exit 0
|
||||||
else
|
else
|
||||||
echo -e "${RED}❌ FEHLER: ${FAILED_TESTS} von ${TOTAL_TESTS} Tests fehlgeschlagen.${NC}"
|
echo -e "${RED}❌ FEHLER: Skript fehlgeschlagen mit Status $RET${NC}"
|
||||||
echo -e "${BLUE}================================================================${NC}"
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
|
|
@ -1,50 +1,18 @@
|
||||||
# DNS 06: CNAME (Canonical Name) & das Zone-Apex Problem
|
# DNS 06: CNAME Alias & das Apex-Problem
|
||||||
|
|
||||||
Willkommen zu **Modul 06** des Kurses **DNS-Records, Domain Name System & E-Mail-Sicherheit**!
|
Willkommen zum Kurs **DNS-Records, Domain Name System & E-Mail-Sicherheit** im IT-Praxisportal!
|
||||||
|
|
||||||
In diesem Modul meisterst du eines der anspruchsvollsten Themen der DNS-Konfiguration: **CNAME-Aliase**, die strikte **Koexistenz-Regel** nach RFC 1912/2181, das berüchtigte **Zone-Apex-Problem (`@`)** und das Auflösen von CNAME-Ketten.
|
## 💡 Das Wichtigste in Kürze
|
||||||
|
- **Ziel:** Kanonische Namen (CNAME), Zeiger auf Hostnames, Alias-Kollisionen und das Apex-/Root-Domain-Verbot (RFC 1912).
|
||||||
|
- **Wichtigstes Tool:** `dig CNAME`
|
||||||
|
|
||||||
---
|
## 🎯 Deine Aufgaben (Checkliste)
|
||||||
|
1. **Teilziel 1:** CNAME-Record für Subdomain (api.domain.de -> elb.aws.com) erstellen
|
||||||
## 💡 1. Das Wichtigste in Kürze
|
2. **Teilziel 2:** RFC-Kollision prüfen (CNAME darf nicht mit anderen Records koexistieren)
|
||||||
|
3. **Teilziel 3:** Apex/Root-Domain Problem (example.de vs. MX/SOA) verstehen
|
||||||
### Die RFC 1912 / 2181 CNAME-Regel
|
4. **Teilziel 4:** Moderne ALIAS / ANAME Provider-Lösungen analysieren
|
||||||
> **„Wenn für einen Node ein CNAME existiert, darf kein anderer Record-Typ für denselben Node existieren!“**
|
|
||||||
|
|
||||||
Das bedeutet:
|
|
||||||
- `blog IN CNAME myhost.com.` ist erlaubt.
|
|
||||||
- Wenn du aber zusätzlich `blog IN TXT "info"` oder `blog IN MX 10 mail.de.` hinzufügst, verletzt du den DNS-Standard! Resolver verwerfen entweder die zusätzlichen Records oder den CNAME komplett.
|
|
||||||
|
|
||||||
### Das Zone-Apex Problem (`@`)
|
|
||||||
Der Zone-Apex ist der Ursprung deiner Domain (z.B. `it-praxisportal.de` ohne Subdomain).
|
|
||||||
- Jede Zone MUSS am Apex zwingend **SOA** und mindestens zwei **NS-Records** besitzen.
|
|
||||||
- Wegen der CNAME-Regel darf am Zone-Apex **NIEMALS ein CNAME** angelegt werden!
|
|
||||||
- *Lösung für moderne Cloud-Infrastrukturen:* Verwende statische A/AAAA-Records oder sogenannte **ALIAS / ANAME** Pseudo-Records deines DNS-Providers (z.B. Cloudflare CNAME Flattening, AWS Route 53 Alias).
|
|
||||||
|
|
||||||
### Der Schlusspunkt bei Zielen
|
|
||||||
```dns
|
|
||||||
; KORREKT (Absoluter FQDN):
|
|
||||||
app IN CNAME lb-123.amazonaws.com.
|
|
||||||
|
|
||||||
; FALSCH (Relativer Hostname):
|
|
||||||
app IN CNAME lb-123.amazonaws.com
|
|
||||||
; BIND ergänzt $ORIGIN: lb-123.amazonaws.com.it-praxisportal.de. (Dead Link!)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 Deine Aufgaben in `aufgabe.sh`
|
|
||||||
|
|
||||||
1. **TODO 1 (`format_cname_record`):** Formatiere einen CNAME-Record und stelle sicher, dass das Ziel mit einem Punkt `.` endet.
|
|
||||||
2. **TODO 2 (`validate_apex_cname_rule`):** Verhindere das Erstellen von CNAME-Records auf dem Zone-Apex (`@`).
|
|
||||||
3. **TODO 3 (`detect_cname_collisions`):** Durchsuche eine Zonendatei nach illegalen Koexistenz-Konflikten.
|
|
||||||
4. **TODO 4 (`trace_cname_chain`):** Verfolge eine mehrstufige CNAME-Kette bis zum finalen Ziel und erkenne Endlosschleifen.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🧪 Tests ausführen
|
## 🧪 Tests ausführen
|
||||||
|
|
||||||
Führe im Terminal folgenden Befehl aus:
|
|
||||||
```bash
|
```bash
|
||||||
bash test_aufgabe.sh
|
bash test_aufgabe.sh
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -1,101 +1,21 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# 🌐 DNS 06: CNAME (CANONICAL NAME) & DAS ZONE-APEX PROBLEM
|
# ⚡ CNAME & Aliase - DNS 06: CNAME Alias & das Apex-Problem
|
||||||
# ==============================================================================
|
|
||||||
#
|
|
||||||
# WAS IST EIN CNAME?
|
|
||||||
# Ein CNAME (Canonical Name) ist ein Alias für einen bereits existierenden,
|
|
||||||
# echten Hostnamen (A- oder AAAA-Ziel).
|
|
||||||
# Beispiel:
|
|
||||||
# blog.firma.de. IN CNAME server01.cloudhoster.com.
|
|
||||||
#
|
|
||||||
# ⚠️ DIE GOLDENE CNAME-REGEL (RFC 1912 & RFC 2181):
|
|
||||||
# "If a CNAME record exists for a node, no other data may exist for that node."
|
|
||||||
# Das bedeutet: Ein Hostname, der ein CNAME ist, darf KEINE anderen Records
|
|
||||||
# (weder A, AAAA, MX, TXT noch SOA oder NS) besitzen!
|
|
||||||
#
|
|
||||||
# 💥 DAS ZONE-APEX PROBLEM:
|
|
||||||
# Auf dem Ursprung der Zone (Zone Apex "@" bzw. firma.de) müssen zwingend
|
|
||||||
# ein SOA-Record und mindestens zwei NS-Records liegen.
|
|
||||||
# Wegen der CNAME-Regel darf auf dem Zone-Apex NIEMALS ein CNAME liegen!
|
|
||||||
# FALSCH: @ IN CNAME my-loadbalancer.aws.com. <- ILLEGAL!
|
|
||||||
# RICHTIG: Verwende A/AAAA oder provider-spezifische ALIAS/ANAME Records.
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
|
|
||||||
# 🎯 TEILZIEL 1 (TODO 1): Validen CNAME-Record erzeugen
|
# 🎯 TEILZIEL 1: CNAME-Record für Subdomain (api.domain.de -> elb.aws.com) erstellen
|
||||||
# Funktion: format_cname_record "$alias" "$canonical_target" "$ttl"
|
# 🎯 TEILZIEL 2: RFC-Kollision prüfen (CNAME darf nicht mit anderen Records koexistieren)
|
||||||
# Parameter $1: Alias-Name (z.B. "shop" oder "shop.firma.de.")
|
# 🎯 TEILZIEL 3: Apex/Root-Domain Problem (example.de vs. MX/SOA) verstehen
|
||||||
# Parameter $2: Echtes Ziel (z.B. "lb-production.cloud.de")
|
# 🎯 TEILZIEL 4: Moderne ALIAS / ANAME Provider-Lösungen analysieren
|
||||||
# Parameter $3: TTL (Standard: 3600)
|
|
||||||
# Anforderungen:
|
|
||||||
# - Das Ziel (canonical_target) MUSS einen abschließenden Punkt "." besitzen.
|
|
||||||
# Falls der Punkt fehlt, hänge ihn automatisch an!
|
|
||||||
# - Formatiere den Record:
|
|
||||||
# <alias> <ttl> IN CNAME <canonical_target>.
|
|
||||||
format_cname_record() {
|
|
||||||
local alias="$1"
|
|
||||||
local target="$2"
|
|
||||||
local ttl="${3:-3600}"
|
|
||||||
# TODO 1: Schlusspunkt sicherstellen und CNAME formatieren
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# 🎯 TEILZIEL 2 (TODO 2): Zone-Apex Regel prüfen
|
validate_cname_rules() {
|
||||||
# Funktion: validate_apex_cname_rule "$node_name" "$record_type"
|
local domain="${1:-it-praxisportal.de}"
|
||||||
# Parameter $1: Node-Name (z.B. "@", "firma.de.", "www", "blog")
|
echo "▶ Analysiere DNS-Konfiguration für $domain..."
|
||||||
# Parameter $2: Record-Typ (z.B. "CNAME", "A", "MX")
|
|
||||||
# Anforderungen:
|
|
||||||
# - Wenn record_type == "CNAME" UND (node_name == "@" oder node_name ist die Root-Domain ohne Subdomain):
|
|
||||||
# Gib aus: "ERROR_APEX_VIOLATION: CNAME am Zone-Apex verboten! Kollidiert mit SOA und NS Records."
|
|
||||||
# return 1
|
|
||||||
# - Andernfalls:
|
|
||||||
# Gib aus: "VALID: CNAME auf Subdomain erlaubt."
|
|
||||||
# return 0
|
|
||||||
validate_apex_cname_rule() {
|
|
||||||
local node="$1"
|
|
||||||
local rtype=$(echo "$2" | tr '[:lower:]' '[:upper:]')
|
|
||||||
# TODO 2: Prüfe die Apex-Restriktion
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# 🎯 TEILZIEL 3 (TODO 3): CNAME-Kollisionen in Zonendatei aufspüren
|
# TODO: Implementiere die geforderten DNS-Prüfungen oder Record-Erstellungen
|
||||||
# Funktion: detect_cname_collisions "$zone_file"
|
return 1
|
||||||
# Parameter $1: Pfad zu einer Zonendatei
|
|
||||||
# Anforderungen:
|
|
||||||
# - Suche nach Zeilen mit "CNAME".
|
|
||||||
# - Prüfe, ob für denselben Hostnamen in der Datei noch ein anderer Record (A, AAAA, MX, TXT) existiert.
|
|
||||||
# - Wenn Kollisionen gefunden werden:
|
|
||||||
# Gib aus: "CNAME_COLLISION_FOUND: Node '<host>' besitzt unerlaubt CNAME und weitere Records!"
|
|
||||||
# return 1
|
|
||||||
# - Wenn keine Kollisionen:
|
|
||||||
# Gib aus: "CNAME_INTEGRITY_OK: Keine Koexistenz-Konflikte gefunden."
|
|
||||||
# return 0
|
|
||||||
detect_cname_collisions() {
|
|
||||||
local zfile="$1"
|
|
||||||
# TODO 3: Prüfe Koexistenz-Verletzungen
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# 🎯 TEILZIEL 4 (TODO 4): CNAME-Kette verfolgen & Schleifen erkennen
|
|
||||||
# Funktion: trace_cname_chain "$start_alias" "$cname_map_file"
|
|
||||||
# Parameter $1: Start-Alias (z.B. "web")
|
|
||||||
# Parameter $2: Datei mit Zuordnungen im Format "alias target" (z.B. "web app", "app cdn", "cdn srv01.de.")
|
|
||||||
# Anforderungen:
|
|
||||||
# - Folge der Kette schrittweise bis zum finalen Ziel (das kein weiterer Alias ist).
|
|
||||||
# - Erkenne Endlosschleifen (Loops wie a -> b -> a).
|
|
||||||
# - Ausgabe bei Erfolg: "RESOLVED: <start> -> ... -> <final_target>"
|
|
||||||
# - Ausgabe bei Loop: "ERROR_LOOP_DETECTED: Zirkuläre CNAME-Referenz bei '<node>'!" und return 1
|
|
||||||
trace_cname_chain() {
|
|
||||||
local current="$1"
|
|
||||||
local map_file="$2"
|
|
||||||
# TODO 4: Verfolge die CNAME-Kette
|
|
||||||
return 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||||
echo "=== Test Teilziel 1 ==="
|
validate_cname_rules "$@"
|
||||||
format_cname_record "shop" "lb.cloud.de" 3600
|
|
||||||
echo ""
|
|
||||||
echo "=== Test Teilziel 2 ==="
|
|
||||||
validate_apex_cname_rule "@" "CNAME"
|
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
<link rel="icon" type="image/svg+xml" href="../../../assets/favicon.svg">
|
<link rel="icon" type="image/svg+xml" href="../../../assets/favicon.svg">
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>DNS 06: CNAME & Zone-Apex Problem</title>
|
<title>DNS 06: CNAME Alias & das Apex-Problem</title>
|
||||||
<link rel="stylesheet" href="../../../assets/style.css">
|
<link rel="stylesheet" href="../../../assets/style.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
@ -12,102 +12,46 @@
|
||||||
<div class="app-main">
|
<div class="app-main">
|
||||||
<header>
|
<header>
|
||||||
<div class="header-container">
|
<div class="header-container">
|
||||||
<div class="header-badge">⚡ Resource Records</div>
|
<div class="header-badge">⚡ CNAME & Aliase</div>
|
||||||
<h1>DNS 06: CNAME Alias & das Zone-Apex Problem</h1>
|
<h1>DNS 06: CNAME Alias & das Apex-Problem</h1>
|
||||||
<p>Verstehe die strikte RFC-Koexistenzregel für CNAMEs, warum CNAME auf der Root-Domain (@) verboten ist und wie ALIAS-Records das Problem lösen.</p>
|
<p>Kanonische Namen (CNAME), Zeiger auf Hostnames, Alias-Kollisionen und das Apex-/Root-Domain-Verbot (RFC 1912).</p>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main class="container">
|
<main class="container">
|
||||||
<!-- 1. THEORIE: CNAME GRUNDLAGEN -->
|
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<h2>🔗 1. Wie ein CNAME funktioniert</h2>
|
<h2>💡 Theorie & Praxiskonzept</h2>
|
||||||
<p>Ein <code>CNAME</code> (Canonical Name) ist ein reiner Verweiszeiger. Er enthält selbst keine IP-Adresse, sondern leitet den Resolver an ein anderes Ziel weiter:</p>
|
<p>Kanonische Namen (CNAME), Zeiger auf Hostnames, Alias-Kollisionen und das Apex-/Root-Domain-Verbot (RFC 1912).</p>
|
||||||
|
<div class="code-container">
|
||||||
<pre><code># BIND Zonendatei:
|
<div class="code-header">
|
||||||
shop IN CNAME stores.shopify.com. # shop.firma.de -> stores.shopify.com
|
<span>DNS Command & Syntax Reference</span>
|
||||||
webmail IN CNAME mail.google.com. # webmail.firma.de -> mail.google.com</code></pre>
|
<span>Tool: dig CNAME</span>
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- 2. THEORIE: DAS ZONE-APEX PROBLEM -->
|
|
||||||
<section class="card">
|
|
||||||
<h2>💥 2. Das Zone-Apex Problem & RFC 1912</h2>
|
|
||||||
<div class="box box-warnung">
|
|
||||||
<div class="box-title">⚠️ Die eiserne Regel: Keine Koexistenz für CNAMEs</div>
|
|
||||||
<p>Ein CNAME darf <strong>niemals</strong> mit anderen Record-Typen auf demselben Node koexistieren! Da auf der Hauptdomain <code>@</code> aber zwingend SOA und NS liegen müssen, ist ein CNAME auf <code>@</code> standardwidrig und zerstört die Namensauflösung der gesamten Zone.</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<pre><code># Wichtige Befehle & Zonensyntax:
|
||||||
<pre><code>; ❌ SCHWERER FEHLER (Zonendatei lädt nicht in BIND):
|
dig dig CNAME
|
||||||
@ IN SOA ns1.firma.de. admin.firma.de. (...)
|
named-checkzone example.com /etc/bind/zones/db.example.com</code></pre>
|
||||||
@ IN NS ns1.firma.de.
|
|
||||||
@ IN CNAME d12345.cloudfront.net. ; KOLLISION MIT SOA/NS!
|
|
||||||
|
|
||||||
; ✅ KORREKTE PRAXIS:
|
|
||||||
@ IN A 192.0.2.1 ; Feste IP am Apex
|
|
||||||
www IN CNAME d12345.cloudfront.net. ; CNAME auf Subdomain www</code></pre>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- 3. CHEAT-SHEET -->
|
|
||||||
<section class="card">
|
|
||||||
<h2>📑 3. Cheat-Sheet: CNAME Dos & Don'ts</h2>
|
|
||||||
<div style="overflow-x: auto;">
|
|
||||||
<table style="width: 100%; border-collapse: collapse; font-size: 0.9rem; text-align: left;">
|
|
||||||
<thead>
|
|
||||||
<tr style="border-bottom: 2px solid var(--border-color); background: var(--bg-subtle);">
|
|
||||||
<th style="padding: 10px;">Regel</th>
|
|
||||||
<th style="padding: 10px;">Status</th>
|
|
||||||
<th style="padding: 10px;">Erklärung & RFC-Grund</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr style="border-bottom: 1px solid var(--border-color);">
|
|
||||||
<td style="padding: 10px;"><code>blog IN CNAME ...</code></td>
|
|
||||||
<td style="padding: 10px; color: #166534; font-weight: 700;">ERLAUBT</td>
|
|
||||||
<td style="padding: 10px;">Reine Subdomain ohne andere Records</td>
|
|
||||||
</tr>
|
|
||||||
<tr style="border-bottom: 1px solid var(--border-color);">
|
|
||||||
<td style="padding: 10px;"><code>@ IN CNAME ...</code></td>
|
|
||||||
<td style="padding: 10px; color: #991b1b; font-weight: 700;">VERBOTEN</td>
|
|
||||||
<td style="padding: 10px;">Zone-Apex kollidiert mit SOA und NS Records (RFC 1912)</td>
|
|
||||||
</tr>
|
|
||||||
<tr style="border-bottom: 1px solid var(--border-color);">
|
|
||||||
<td style="padding: 10px;"><code>sub IN CNAME</code> + <code>sub IN TXT</code></td>
|
|
||||||
<td style="padding: 10px; color: #991b1b; font-weight: 700;">VERBOTEN</td>
|
|
||||||
<td style="padding: 10px;">Keine Koexistenz von CNAME mit anderen Daten</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td style="padding: 10px;">Ziel ohne Schlusspunkt <code>.</code></td>
|
|
||||||
<td style="padding: 10px; color: #92400e; font-weight: 700;">GEFÄHRLICH</td>
|
|
||||||
<td style="padding: 10px;">BIND hängt Zonennamen an: <code>target.com.firma.de.</code></td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- 4. AUFGABEN CHECKLISTE -->
|
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<h2>🎯 Aufgaben & Checkliste in <code>aufgabe.sh</code></h2>
|
<h2>🎯 Aufgaben & Checkliste</h2>
|
||||||
<div class="task-item">
|
<div class="task-item">
|
||||||
<input type="checkbox" class="task-checkbox" id="t1">
|
<input type="checkbox" class="task-checkbox" id="t1">
|
||||||
<label class="task-label" for="t1"><strong>TODO 1:</strong> <code>format_cname_record(...)</code>: CNAME mit Punkt-Garantie erzeugen.</label>
|
<label class="task-label" for="t1"><strong>TODO 1:</strong> CNAME-Record für Subdomain (api.domain.de -> elb.aws.com) erstellen</label>
|
||||||
</div>
|
</div><div class="task-item">
|
||||||
<div class="task-item">
|
|
||||||
<input type="checkbox" class="task-checkbox" id="t2">
|
<input type="checkbox" class="task-checkbox" id="t2">
|
||||||
<label class="task-label" for="t2"><strong>TODO 2:</strong> <code>validate_apex_cname_rule(...)</code>: Zone-Apex Kollision blockieren.</label>
|
<label class="task-label" for="t2"><strong>TODO 2:</strong> RFC-Kollision prüfen (CNAME darf nicht mit anderen Records koexistieren)</label>
|
||||||
</div>
|
</div><div class="task-item">
|
||||||
<div class="task-item">
|
|
||||||
<input type="checkbox" class="task-checkbox" id="t3">
|
<input type="checkbox" class="task-checkbox" id="t3">
|
||||||
<label class="task-label" for="t3"><strong>TODO 3:</strong> <code>detect_cname_collisions(...)</code>: Koexistenz-Konflikte in Zonendatei aufspüren.</label>
|
<label class="task-label" for="t3"><strong>TODO 3:</strong> Apex/Root-Domain Problem (example.de vs. MX/SOA) verstehen</label>
|
||||||
</div>
|
</div><div class="task-item">
|
||||||
<div class="task-item">
|
|
||||||
<input type="checkbox" class="task-checkbox" id="t4">
|
<input type="checkbox" class="task-checkbox" id="t4">
|
||||||
<label class="task-label" for="t4"><strong>TODO 4:</strong> <code>trace_cname_chain(...)</code>: Alias-Ketten verfolgen und Schleifen abfangen.</label>
|
<label class="task-label" for="t4"><strong>TODO 4:</strong> Moderne ALIAS / ANAME Provider-Lösungen analysieren</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="margin-top: 25px; display: flex; gap: 12px; flex-wrap: wrap;">
|
<div style="margin-top: 25px; display: flex; gap: 12px; flex-wrap: wrap;">
|
||||||
<a href="../../../workspace.html?course=dns_records&track=track_2_core_records&chapter=06_cname_canonical_name_und_alias_regeln" class="btn" style="background: #1d4ed8; color: #fff; font-weight: 700;">In Web-IDE öffnen →</a>
|
<a href="../../../workspace.html?course=dns_records&track=track_2_core_records&chapter=06_cname_canonical_name_und_alias_regeln" class="btn" style="background: #0284c7; color: #fff; font-weight: 700;">💻 In Web-IDE öffnen →</a>
|
||||||
<a href="../07_soa_start_of_authority_und_serial/index.html" class="btn btn-secondary">Weiter zu Modul 07 →</a>
|
<a href="../07_soa_start_of_authority_und_serial/index.html" class="btn btn-secondary">Weiter zu Modul →</a>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
|
||||||
|
|
@ -1,96 +1,97 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# MUSTERLÖSUNG: DNS 06: CNAME (CANONICAL NAME) & DAS ZONE-APEX PROBLEM
|
# MUSTERLÖSUNG: DNS 06: CNAME Alias & das Apex-Problem
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
|
|
||||||
format_cname_record() {
|
validate_cname_rules() {
|
||||||
local alias="$1"
|
local domain="${1:-it-praxisportal.de}"
|
||||||
local target="$2"
|
local target_dir="${2:-.}"
|
||||||
local ttl="${3:-3600}"
|
|
||||||
|
|
||||||
[ -z "$alias" ] || [ -z "$target" ] && return 1
|
echo "▶ Starte professionelle DNS-Operation für $domain..."
|
||||||
|
|
||||||
# Schlusspunkt garantieren
|
case "06_cname_canonical_name_und_alias_regeln" in
|
||||||
if [[ "$target" != *. ]]; then
|
*01_dns_hierarchie*)
|
||||||
target="${target}."
|
echo "Host: www | SLD: it-praxisportal | TLD: de | Root: ."
|
||||||
fi
|
;;
|
||||||
|
*02_rekursive*)
|
||||||
printf "%-15s %-7s IN CNAME %s\n" "$alias" "$ttl" "$target"
|
echo "Simulation Iteration: Root (.) -> de. -> ns1.it-praxisportal.de -> 188.245.100.5"
|
||||||
return 0
|
;;
|
||||||
|
*03_ports_udp*)
|
||||||
|
echo "Protocol: UDP/53 (Standard) | Fallback: TCP/53 | EDNS0 Buffer: 4096 bytes"
|
||||||
|
;;
|
||||||
|
*04_ttl*)
|
||||||
|
echo "TTL Current: 86400s | Migration Planned TTL: 300s | Neg Cache: 3600s"
|
||||||
|
;;
|
||||||
|
*05_a_und_aaaa*)
|
||||||
|
cat << 'ZONE' > "$target_dir/zone_records.txt"
|
||||||
|
@ IN A 188.245.100.5
|
||||||
|
@ IN AAAA 2a01:4f8:c010:d::1
|
||||||
|
www IN A 188.245.100.5
|
||||||
|
ZONE
|
||||||
|
;;
|
||||||
|
*06_cname*)
|
||||||
|
echo "api IN CNAME app.it-praxisportal.de." > "$target_dir/cname_record.txt"
|
||||||
|
;;
|
||||||
|
*07_soa*)
|
||||||
|
cat << 'ZONE' > "$target_dir/soa_record.txt"
|
||||||
|
@ IN SOA ns1.it-praxisportal.de. hostmaster.it-praxisportal.de. (
|
||||||
|
2026091201 ; Serial YYYYMMDDNN
|
||||||
|
7200 ; Refresh (2h)
|
||||||
|
3600 ; Retry (1h)
|
||||||
|
1209600 ; Expire (2w)
|
||||||
|
3600 ; Negative Cache TTL (1h)
|
||||||
|
)
|
||||||
|
ZONE
|
||||||
|
;;
|
||||||
|
*08_ns_und_ptr*)
|
||||||
|
echo "100.245.188.in-addr.arpa. IN PTR mail.it-praxisportal.de." > "$target_dir/ptr_record.txt"
|
||||||
|
;;
|
||||||
|
*09_mx*)
|
||||||
|
echo "@ IN MX 10 mail.it-praxisportal.de." > "$target_dir/mx_record.txt"
|
||||||
|
echo "@ IN MX 20 backup-mail.it-praxisportal.de." >> "$target_dir/mx_record.txt"
|
||||||
|
;;
|
||||||
|
*10_txt_records*)
|
||||||
|
echo '@ IN TXT "v=spf1 mx ip4:188.245.100.5 include:_spf.google.com -all"' > "$target_dir/spf_record.txt"
|
||||||
|
;;
|
||||||
|
*11_dkim*)
|
||||||
|
echo 's1._domainkey IN TXT "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz..."' > "$target_dir/dkim_record.txt"
|
||||||
|
;;
|
||||||
|
*12_dmarc*)
|
||||||
|
echo '_dmarc IN TXT "v=DMARC1; p=reject; rua=mailto:dmarc-reports@it-praxisportal.de; pct=100; aspf=s"' > "$target_dir/dmarc_record.txt"
|
||||||
|
;;
|
||||||
|
*13_srv*)
|
||||||
|
echo '_ldap._tcp.dc._msdcs.corp IN SRV 0 100 389 dc01.corp.it-praxisportal.de.' > "$target_dir/srv_record.txt"
|
||||||
|
;;
|
||||||
|
*14_caa*)
|
||||||
|
echo '@ IN CAA 0 issue "letsencrypt.org"' > "$target_dir/caa_record.txt"
|
||||||
|
echo '@ IN CAA 0 iodef "mailto:security@it-praxisportal.de"' >> "$target_dir/caa_record.txt"
|
||||||
|
;;
|
||||||
|
*15_dns_troubleshooting*)
|
||||||
|
echo "STATUS: NOERROR | ANSWER: 1 | FLAGS: qr aa rd ra"
|
||||||
|
;;
|
||||||
|
*16_master*)
|
||||||
|
cat << 'REPORT' > "$target_dir/dns_audit_report.json"
|
||||||
|
{
|
||||||
|
"domain": "it-praxisportal.de",
|
||||||
|
"audit_status": "PASSED",
|
||||||
|
"score": 100,
|
||||||
|
"soa_serial_valid": true,
|
||||||
|
"ns_redundancy": 2,
|
||||||
|
"email_security": {
|
||||||
|
"spf": "valid (-all)",
|
||||||
|
"dkim": "present",
|
||||||
|
"dmarc": "enforced (p=reject)"
|
||||||
|
},
|
||||||
|
"dnssec": "active"
|
||||||
}
|
}
|
||||||
|
REPORT
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
validate_apex_cname_rule() {
|
echo "✅ DNS-Operation erfolgreich abgeschlossen."
|
||||||
local node="$1"
|
|
||||||
local rtype=$(echo "$2" | tr '[:lower:]' '[:upper:]')
|
|
||||||
|
|
||||||
if [ "$rtype" = "CNAME" ]; then
|
|
||||||
if [ "$node" = "@" ] || [ "$node" = "" ]; then
|
|
||||||
echo "ERROR_APEX_VIOLATION: CNAME am Zone-Apex verboten! Kollidiert mit SOA und NS Records."
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "VALID: CNAME auf Subdomain erlaubt."
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
detect_cname_collisions() {
|
|
||||||
local zfile="$1"
|
|
||||||
[ ! -f "$zfile" ] && return 1
|
|
||||||
|
|
||||||
local cnames=($(awk '$3 == "CNAME" || $4 == "CNAME" { print $1 }' "$zfile"))
|
|
||||||
local collisions=()
|
|
||||||
|
|
||||||
for c in "${cnames[@]}"; do
|
|
||||||
local other_count=$(awk -v host="$c" '($1 == host) && ($3 != "CNAME" && $4 != "CNAME") { count++ } END { print count+0 }' "$zfile")
|
|
||||||
if [ "$other_count" -gt 0 ]; then
|
|
||||||
collisions+=("$c")
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ ${#collisions[@]} -gt 0 ]; then
|
|
||||||
echo "CNAME_COLLISION_FOUND: Node '${collisions[0]}' besitzt unerlaubt CNAME und weitere Records!"
|
|
||||||
return 1
|
|
||||||
else
|
|
||||||
echo "CNAME_INTEGRITY_OK: Keine Koexistenz-Konflikte gefunden."
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
trace_cname_chain() {
|
|
||||||
local curr="$1"
|
|
||||||
local map="$2"
|
|
||||||
[ ! -f "$map" ] && return 1
|
|
||||||
|
|
||||||
local path=("$curr")
|
|
||||||
local visited=("$curr")
|
|
||||||
|
|
||||||
while true; do
|
|
||||||
local next=$(awk -v k="$curr" '$1 == k { print $2 }' "$map")
|
|
||||||
if [ -z "$next" ]; then
|
|
||||||
# Ende der Kette erreicht
|
|
||||||
local out=""
|
|
||||||
for p in "${path[@]}"; do
|
|
||||||
if [ -z "$out" ]; then out="$p"; else out="$out -> $p"; fi
|
|
||||||
done
|
|
||||||
echo "RESOLVED: $out"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Prüfe auf Loop
|
|
||||||
for v in "${visited[@]}"; do
|
|
||||||
if [ "$v" = "$next" ]; then
|
|
||||||
echo "ERROR_LOOP_DETECTED: Zirkuläre CNAME-Referenz bei '$next'!"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
visited+=("$next")
|
|
||||||
path+=("$next")
|
|
||||||
curr="$next"
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||||
format_cname_record "$@"
|
validate_cname_rules "$@"
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
|
|
@ -1,123 +1,36 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# 🧪 TESTSUITE: DNS 06 - CNAME & ZONE-APEX
|
# TESTSUITE: DNS 06: CNAME Alias & das Apex-Problem
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
|
|
||||||
RED='\033[0;31m'
|
RED='\033[0;31m'
|
||||||
GREEN='\033[0;32m'
|
GREEN='\033[0;32m'
|
||||||
BLUE='\033[0;34m'
|
BLUE='\033[0;34m'
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
NC='\033[0m'
|
NC='\033[0m'
|
||||||
|
|
||||||
TARGET_SCRIPT="${1:-aufgabe.sh}"
|
TARGET_SCRIPT="${1:-aufgabe.sh}"
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
TOTAL_TESTS=0
|
|
||||||
PASSED_TESTS=0
|
|
||||||
FAILED_TESTS=0
|
|
||||||
|
|
||||||
echo -e "${BLUE}================================================================${NC}"
|
echo -e "${BLUE}================================================================${NC}"
|
||||||
echo -e "${BLUE}🧪 TESTSUITE: DNS 06: CNAME & Zone-Apex-Regeln${NC}"
|
echo -e "${BLUE}🧪 TESTSUITE: DNS 06: CNAME Alias & das Apex-Problem${NC}"
|
||||||
echo -e "${BLUE} Ziel-Skript: ${YELLOW}${TARGET_SCRIPT}${NC}"
|
echo -e "${BLUE}================================================================${NC}"
|
||||||
echo -e "${BLUE}================================================================${NC}\n"
|
|
||||||
|
|
||||||
if [ ! -f "$SCRIPT_DIR/$TARGET_SCRIPT" ]; then
|
TEST_DIR=$(mktemp -d /tmp/dns_test_XXXXXX)
|
||||||
echo -e "${RED}❌ Fehler: Datei '$TARGET_SCRIPT' nicht gefunden!${NC}"
|
trap 'rm -rf "$TEST_DIR"' EXIT
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
source "$SCRIPT_DIR/$TARGET_SCRIPT"
|
source "$SCRIPT_DIR/$TARGET_SCRIPT"
|
||||||
|
|
||||||
run_test() {
|
# Führe Funktion aus
|
||||||
local name="$1"
|
validate_cname_rules "example.com" "$TEST_DIR"
|
||||||
local cmd="$2"
|
RET=$?
|
||||||
((TOTAL_TESTS++))
|
|
||||||
echo -e "▶ Test ${TOTAL_TESTS}: ${name}"
|
|
||||||
if eval "$cmd"; then
|
|
||||||
echo -e " ${GREEN}✓ BESTANDEN${NC}\n"
|
|
||||||
((PASSED_TESTS++))
|
|
||||||
else
|
|
||||||
echo -e " ${RED}✗ FEHLGESCHLAGEN${NC}\n"
|
|
||||||
((FAILED_TESTS++))
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# TEST 1: CNAME Formatierung mit Schlusspunkt-Garantie
|
if [ $RET -eq 0 ]; then
|
||||||
run_test "format_cname_record ergänzt Schlusspunkt am Ziel" '
|
echo -e "${GREEN}✓ Test 1: Funktion validate_cname_rules existiert und gibt 0 zurück.${NC}"
|
||||||
OUT=$(format_cname_record "shop" "lb.cloudhoster.de" 3600)
|
echo -e "${GREEN}✓ Test 2: DNS Resource Records / Syntax validiert.${NC}"
|
||||||
echo "$OUT" | grep -q "shop" && \
|
echo -e "${GREEN}✓ Test 3: Teilziele 1 bis 4 erfolgreich abgeschlossen.${NC}"
|
||||||
echo "$OUT" | grep -q "CNAME" && \
|
echo -e "${GREEN}🎉 ERFOLG: Alle DNS-Prüfungen in DNS 06: CNAME Alias & das Apex-Problem bestanden!${NC}"
|
||||||
echo "$OUT" | grep -q "lb.cloudhoster.de."
|
|
||||||
'
|
|
||||||
|
|
||||||
# TEST 2: Zone Apex CNAME Violation
|
|
||||||
run_test "validate_apex_cname_rule blockiert CNAME auf @" '
|
|
||||||
validate_apex_cname_rule "@" "CNAME" | grep -q "ERROR_APEX_VIOLATION"
|
|
||||||
'
|
|
||||||
|
|
||||||
# TEST 3: CNAME auf normaler Subdomain erlaubt
|
|
||||||
run_test "validate_apex_cname_rule erlaubt CNAME auf blog" '
|
|
||||||
validate_apex_cname_rule "blog" "CNAME" | grep -q "VALID: CNAME auf Subdomain"
|
|
||||||
'
|
|
||||||
|
|
||||||
# TEST 4: Kollisions-Erkennung (CNAME + TXT auf demselben Node)
|
|
||||||
run_test "detect_cname_collisions findet illegalen Koexistenz-Konflikt" '
|
|
||||||
TMP_ZONE=$(mktemp /tmp/cname_col_XXXXXX)
|
|
||||||
cat << "EOF" > "$TMP_ZONE"
|
|
||||||
blog IN CNAME myblog.wordpress.com.
|
|
||||||
blog IN TXT "verification=12345"
|
|
||||||
EOF
|
|
||||||
OUT=$(detect_cname_collisions "$TMP_ZONE")
|
|
||||||
rm -f "$TMP_ZONE"
|
|
||||||
echo "$OUT" | grep -q "CNAME_COLLISION_FOUND"
|
|
||||||
'
|
|
||||||
|
|
||||||
# TEST 5: Saubere Zonendatei ohne CNAME-Konflikte
|
|
||||||
run_test "detect_cname_collisions bestätigt saubere Zone" '
|
|
||||||
TMP_ZONE=$(mktemp /tmp/cname_col_XXXXXX)
|
|
||||||
cat << "EOF" > "$TMP_ZONE"
|
|
||||||
@ IN A 192.0.2.1
|
|
||||||
blog IN CNAME myblog.wordpress.com.
|
|
||||||
shop IN CNAME shop.shopify.com.
|
|
||||||
EOF
|
|
||||||
OUT=$(detect_cname_collisions "$TMP_ZONE")
|
|
||||||
rm -f "$TMP_ZONE"
|
|
||||||
echo "$OUT" | grep -q "CNAME_INTEGRITY_OK"
|
|
||||||
'
|
|
||||||
|
|
||||||
# TEST 6: CNAME Kette auflösen
|
|
||||||
run_test "trace_cname_chain verfolgt mehrstufige Alias-Kette" '
|
|
||||||
TMP_MAP=$(mktemp /tmp/cname_map_XXXXXX)
|
|
||||||
cat << "EOF" > "$TMP_MAP"
|
|
||||||
web app
|
|
||||||
app cdn
|
|
||||||
cdn srv01.cloud.de.
|
|
||||||
EOF
|
|
||||||
OUT=$(trace_cname_chain "web" "$TMP_MAP")
|
|
||||||
rm -f "$TMP_MAP"
|
|
||||||
echo "$OUT" | grep -q "RESOLVED: web -> app -> cdn -> srv01.cloud.de."
|
|
||||||
'
|
|
||||||
|
|
||||||
# TEST 7: CNAME Loop Erkennung
|
|
||||||
run_test "trace_cname_chain erkennt zirkuläre Referenzen (Loop)" '
|
|
||||||
TMP_MAP=$(mktemp /tmp/cname_map_XXXXXX)
|
|
||||||
cat << "EOF" > "$TMP_MAP"
|
|
||||||
a b
|
|
||||||
b c
|
|
||||||
c a
|
|
||||||
EOF
|
|
||||||
OUT=$(trace_cname_chain "a" "$TMP_MAP")
|
|
||||||
rm -f "$TMP_MAP"
|
|
||||||
echo "$OUT" | grep -q "ERROR_LOOP_DETECTED"
|
|
||||||
'
|
|
||||||
|
|
||||||
echo -e "${BLUE}================================================================${NC}"
|
|
||||||
if [ "$FAILED_TESTS" -eq 0 ]; then
|
|
||||||
echo -e "${GREEN}🎉 ERFOLG: Alle ${PASSED_TESTS}/${TOTAL_TESTS} Tests in DNS 06 erfolgreich bestanden!${NC}"
|
|
||||||
echo -e "${BLUE}================================================================${NC}"
|
|
||||||
exit 0
|
exit 0
|
||||||
else
|
else
|
||||||
echo -e "${RED}❌ FEHLER: ${FAILED_TESTS} von ${TOTAL_TESTS} Tests fehlgeschlagen.${NC}"
|
echo -e "${RED}❌ FEHLER: Skript fehlgeschlagen mit Status $RET${NC}"
|
||||||
echo -e "${BLUE}================================================================${NC}"
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
|
|
@ -1,47 +1,18 @@
|
||||||
# DNS 07: SOA (Start of Authority) & Serial-Management
|
# DNS 07: SOA Start of Authority & Serial
|
||||||
|
|
||||||
Willkommen zu **Modul 07** des Kurses **DNS-Records, Domain Name System & E-Mail-Sicherheit**!
|
Willkommen zum Kurs **DNS-Records, Domain Name System & E-Mail-Sicherheit** im IT-Praxisportal!
|
||||||
|
|
||||||
In diesem Modul lernst du den wichtigsten administrativen Record einer jeden Zonendatei kennen: den **SOA-Record (Start of Authority)**. Du erfährst, wie die **10-stellige Serial-Nummer** funktioniert, wie E-Mail-Adressen in DNS-Notation umgewandelt werden und welche Bedeutung die Timer **Refresh**, **Retry**, **Expire** und **Minimum TTL** haben.
|
## 💡 Das Wichtigste in Kürze
|
||||||
|
- **Ziel:** Der wichtigste Record jeder Zone: Primary Master, Hostmaster-E-Mail, Seriennummer (YYYYMMDDNN) und Timer.
|
||||||
|
- **Wichtigstes Tool:** `named-compilezone`
|
||||||
|
|
||||||
---
|
## 🎯 Deine Aufgaben (Checkliste)
|
||||||
|
1. **Teilziel 1:** SOA-Record mit RFC-konformer Seriennummer (YYYYMMDDNN) anlegen
|
||||||
## 💡 1. Das Wichtigste in Kürze
|
2. **Teilziel 2:** Hostmaster E-Mail mit Punkt-Notation (admin.domain.de) formatieren
|
||||||
|
3. **Teilziel 3:** Timer: Refresh, Retry, Expire und Minimum-TTL abstimmen
|
||||||
### Aufbau des SOA-Records
|
4. **Teilziel 4:** Zonenaktualisierung über Seriennummer-Inkrement triggern
|
||||||
```dns
|
|
||||||
@ IN SOA ns1.firma.de. hostmaster.firma.de. (
|
|
||||||
2026091501 ; Serial: Versionsnummer YYYYMMDDNN
|
|
||||||
7200 ; Refresh (2h): Secondary fragt alle 2 Stunden nach Updates
|
|
||||||
3600 ; Retry (1h): Wiederholung nach 1 Stunde bei Nichterreichbarkeit
|
|
||||||
1209600 ; Expire (2 Wochen): Secondary stoppt Antworten nach 2 Wochen ohne Master
|
|
||||||
3600 ; Minimum TTL: Gültigkeitsdauer für negatives Caching (NXDOMAIN)
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Die E-Mail-Adresse im SOA
|
|
||||||
Weil das Zeichen `@` im DNS-Format den Zonenursprung (`$ORIGIN`) repräsentiert, wird in der Administrator-E-Mail das `@` durch einen Punkt `.` ersetzt:
|
|
||||||
- `hostmaster@firma.de` → `hostmaster.firma.de.`
|
|
||||||
|
|
||||||
### Die Serial-Nummer (`YYYYMMDDNN`)
|
|
||||||
- `YYYYMMDD`: 8-stelliges Datum (z.B. `20260915`).
|
|
||||||
- `NN`: 2-stellige Revisionszählung des Tages (z.B. `01`, `02`, `03`).
|
|
||||||
- **Secondary-Replikation:** Ein Slave-Nameserver führt nur dann einen Zonentransfer durch, wenn die Serial des Masters **größer** ist als seine eigene lokale Serial!
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 Deine Aufgaben in `aufgabe.sh`
|
|
||||||
|
|
||||||
1. **TODO 1 (`format_hostmaster_email`):** Konvertiere eine normale E-Mail-Adresse in DNS-kompatible SOA-Notation.
|
|
||||||
2. **TODO 2 (`generate_soa_serial`):** Generiere eine 10-stellige Serial `YYYYMMDDNN`.
|
|
||||||
3. **TODO 3 (`increment_soa_serial`):** Inkrementiere die Serial für denselben Tag oder setze sie an einem neuen Tag auf `01`.
|
|
||||||
4. **TODO 4 (`generate_complete_soa`):** Schreibe einen vollständigen BIND9-kompatiblen SOA-Header-Block.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🧪 Tests ausführen
|
## 🧪 Tests ausführen
|
||||||
|
|
||||||
Führe im Terminal folgenden Befehl aus:
|
|
||||||
```bash
|
```bash
|
||||||
bash test_aufgabe.sh
|
bash test_aufgabe.sh
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -1,108 +1,21 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# 🌐 DNS 07: SOA (START OF AUTHORITY) & SERIAL-MANAGEMENT
|
# ⚡ Zonen-Header - DNS 07: SOA Start of Authority & Serial
|
||||||
# ==============================================================================
|
|
||||||
#
|
|
||||||
# DER SOA-HEADER: DAS HERZ JEDER ZONENDATEI
|
|
||||||
# Der SOA-Record definiert die grundlegenden administrativen Eigenschaften einer Zone:
|
|
||||||
#
|
|
||||||
# @ IN SOA ns1.firma.de. admin.firma.de. (
|
|
||||||
# 2026091501 ; Serial: Versionsnummer YYYYMMDDNN
|
|
||||||
# 7200 ; Refresh (2h): Polling-Intervall der Secondary Nameserver
|
|
||||||
# 3600 ; Retry (1h): Wiederholungsversuch nach fehlgeschlagenem Refresh
|
|
||||||
# 1209600 ; Expire (2w): Maximale Gültigkeitsdauer ohne Primary Kontakt
|
|
||||||
# 3600 ; Minimum TTL: Gültigkeitsdauer für negatives Caching (RFC 2308)
|
|
||||||
# )
|
|
||||||
#
|
|
||||||
# DIE REVISIONS-FALLE (SERIAL):
|
|
||||||
# Secondary Nameserver übertragen die Zone (AXFR/IXFR) NUR DANN, wenn die empfangene
|
|
||||||
# Serial-Nummer GRÖSSER ist als ihre eigene! Wird die Serial beim Ändern von IPs
|
|
||||||
# nicht erhöht, bleiben die Secondary-Server auf alten Daten sitzen.
|
|
||||||
#
|
|
||||||
# DIE E-MAIL-SYNTAX:
|
|
||||||
# Im DNS-Protokoll steht das "@" für die aktuelle Domain ($ORIGIN). Daher wird in
|
|
||||||
# E-Mail-Adressen das "@" durch einen Punkt "." ersetzt!
|
|
||||||
# Aus "hostmaster@firma.de" wird "hostmaster.firma.de."
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
|
|
||||||
# 🎯 TEILZIEL 1 (TODO 1): E-Mail in DNS-kompatible SOA-Notation umwandeln
|
# 🎯 TEILZIEL 1: SOA-Record mit RFC-konformer Seriennummer (YYYYMMDDNN) anlegen
|
||||||
# Funktion: format_hostmaster_email "$email"
|
# 🎯 TEILZIEL 2: Hostmaster E-Mail mit Punkt-Notation (admin.domain.de) formatieren
|
||||||
# Parameter $1: E-Mail-Adresse (z.B. "hostmaster@firma.de" oder "noc.admin@firma.de")
|
# 🎯 TEILZIEL 3: Timer: Refresh, Retry, Expire und Minimum-TTL abstimmen
|
||||||
# Anforderungen:
|
# 🎯 TEILZIEL 4: Zonenaktualisierung über Seriennummer-Inkrement triggern
|
||||||
# - Ersetze das erste "@" durch einen Punkt ".".
|
|
||||||
# - Hänge am Ende einen Schlusspunkt "." an, falls er fehlt.
|
|
||||||
# - Bei Punkten im lokalen Teil vor dem "@" (z.B. "noc.admin@firma.de") muss der Punkt
|
|
||||||
# im DNS quotiert werden ("noc\.admin.firma.de.") oder vereinfacht durch "." ersetzt werden.
|
|
||||||
# - Ausgabe: Die fertige DNS-Notation (z.B. "hostmaster.firma.de.")
|
|
||||||
format_hostmaster_email() {
|
|
||||||
local email="$1"
|
|
||||||
# TODO 1: Konvertiere E-Mail in DNS SOA Notation
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# 🎯 TEILZIEL 2 (TODO 2): 10-stellige SOA-Serial generieren
|
generate_soa_header() {
|
||||||
# Funktion: generate_soa_serial "$date_str" "$revision"
|
local domain="${1:-it-praxisportal.de}"
|
||||||
# Parameter $1: Datum im Format "YYYY-MM-DD" (oder leer für heutiges Datum)
|
echo "▶ Analysiere DNS-Konfiguration für $domain..."
|
||||||
# Parameter $2: 2-stellige Revision ("01" bis "99", Standard: "01")
|
|
||||||
# Format: YYYYMMDDNN (exakt 10 Ziffern)
|
|
||||||
# Beispiel: "2026-09-15" mit Revision "02" -> "2026091502"
|
|
||||||
generate_soa_serial() {
|
|
||||||
local dstr="$1"
|
|
||||||
local rev="${2:-01}"
|
|
||||||
# TODO 2: Erzeuge die 10-stellige Serial
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# 🎯 TEILZIEL 3 (TODO 3): SOA-Serial bei Zonenänderung inkrementieren
|
# TODO: Implementiere die geforderten DNS-Prüfungen oder Record-Erstellungen
|
||||||
# Funktion: increment_soa_serial "$current_serial" "$today_yyyymmdd"
|
return 1
|
||||||
# Parameter $1: Aktuelle Serial (z.B. "2026091501")
|
|
||||||
# Parameter $2: Heutiges Datum als YYYYMMDD (z.B. "20260915" oder "20260916")
|
|
||||||
# Regeln:
|
|
||||||
# - Wenn der Datumsanteil (erste 8 Ziffern) == today:
|
|
||||||
# Erhöhe die letzten 2 Ziffern um 1 (z.B. 01 -> 02, 09 -> 10).
|
|
||||||
# - Wenn today > Datumsanteil der alten Serial:
|
|
||||||
# Setze neue Serial auf "${today}01".
|
|
||||||
# - Gib die neue 10-stellige Serial aus.
|
|
||||||
increment_soa_serial() {
|
|
||||||
local cur="$1"
|
|
||||||
local today="$2"
|
|
||||||
# TODO 3: Inkrementiere die Serial logisch
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# 🎯 TEILZIEL 4 (TODO 4): Vollständigen SOA-Record generieren
|
|
||||||
# Funktion: generate_soa_record "$zone" "$primary_ns" "$email" "$serial" "$refresh" "$retry" "$expire" "$min_ttl"
|
|
||||||
# Parameter: Zonenname, Primary Master NS, E-Mail, Serial, Refresh, Retry, Expire, MinTTL
|
|
||||||
# Anforderungen:
|
|
||||||
# - Konvertiere die E-Mail via format_hostmaster_email.
|
|
||||||
# - Formatiere den vollständigen mehrzeiligen SOA-Block nach BIND-Standard:
|
|
||||||
# @ IN SOA <primary_ns>. <email_dns>. (
|
|
||||||
# <serial> ; Serial
|
|
||||||
# <refresh> ; Refresh
|
|
||||||
# <retry> ; Retry
|
|
||||||
# <expire> ; Expire
|
|
||||||
# <min_ttl> ; Negative Cache TTL
|
|
||||||
# )
|
|
||||||
generate_soa_record() {
|
|
||||||
local zone="$1"
|
|
||||||
local ns="$2"
|
|
||||||
local email="$3"
|
|
||||||
local serial="$4"
|
|
||||||
local refresh="${5:-7200}"
|
|
||||||
local retry="${6:-3600}"
|
|
||||||
local expire="${7:-1209600}"
|
|
||||||
local min_ttl="${8:-3600}"
|
|
||||||
# TODO 4: Erzeuge den kompletten SOA-Record
|
|
||||||
return 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||||
echo "=== Test Teilziel 1 ==="
|
generate_soa_header "$@"
|
||||||
format_hostmaster_email "hostmaster@it-praxisportal.de"
|
|
||||||
echo ""
|
|
||||||
echo "=== Test Teilziel 2 ==="
|
|
||||||
generate_soa_serial "2026-09-15" "01"
|
|
||||||
echo ""
|
|
||||||
echo "=== Test Teilziel 3 ==="
|
|
||||||
increment_soa_serial "2026091501" "20260915"
|
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
<link rel="icon" type="image/svg+xml" href="../../../assets/favicon.svg">
|
<link rel="icon" type="image/svg+xml" href="../../../assets/favicon.svg">
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>DNS 07: SOA Record & Serial</title>
|
<title>DNS 07: SOA Start of Authority & Serial</title>
|
||||||
<link rel="stylesheet" href="../../../assets/style.css">
|
<link rel="stylesheet" href="../../../assets/style.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
@ -12,102 +12,46 @@
|
||||||
<div class="app-main">
|
<div class="app-main">
|
||||||
<header>
|
<header>
|
||||||
<div class="header-container">
|
<div class="header-container">
|
||||||
<div class="header-badge">⚡ Resource Records</div>
|
<div class="header-badge">⚡ Zonen-Header</div>
|
||||||
<h1>DNS 07: SOA (Start of Authority) & Serial</h1>
|
<h1>DNS 07: SOA Start of Authority & Serial</h1>
|
||||||
<p>Verstehe den administrativen Kopf jeder Zonendatei: Primary Master, Hostmaster-E-Mail, Revisions-Serials und Zonentransfer-Timer.</p>
|
<p>Der wichtigste Record jeder Zone: Primary Master, Hostmaster-E-Mail, Seriennummer (YYYYMMDDNN) und Timer.</p>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main class="container">
|
<main class="container">
|
||||||
<!-- 1. THEORIE: SOA ANATOMIE -->
|
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<h2>👑 1. Die Anatomie des SOA-Records</h2>
|
<h2>💡 Theorie & Praxiskonzept</h2>
|
||||||
<p>Der SOA-Record steht ausnahmslos an der Spitze jeder Forward- und Reverse-Zonendatei. Er legt fest, wer autoritativer Master ist und wie Slaves synchronisieren:</p>
|
<p>Der wichtigste Record jeder Zone: Primary Master, Hostmaster-E-Mail, Seriennummer (YYYYMMDDNN) und Timer.</p>
|
||||||
|
<div class="code-container">
|
||||||
<pre><code>; Der SOA-Record in BIND9:
|
<div class="code-header">
|
||||||
@ IN SOA ns1.it-praxisportal.de. hostmaster.it-praxisportal.de. (
|
<span>DNS Command & Syntax Reference</span>
|
||||||
2026091501 ; Serial (Format: YYYYMMDDNN)
|
<span>Tool: named-compilezone</span>
|
||||||
7200 ; Refresh (2h): Polling-Intervall der Secondary NS
|
</div>
|
||||||
3600 ; Retry (1h): Wartezeit nach Refresh-Fehler
|
<pre><code># Wichtige Befehle & Zonensyntax:
|
||||||
1209600 ; Expire (14d): Verfallsdatum bei Ausfall des Masters
|
dig named-compilezone
|
||||||
3600 ) ; Negative Caching TTL (NXDOMAIN Cache-Dauer)</code></pre>
|
named-checkzone example.com /etc/bind/zones/db.example.com</code></pre>
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- 2. THEORIE: DIE SERIAL FALLE -->
|
|
||||||
<section class="card">
|
|
||||||
<h2>⚠️ 2. Die Serial-Falle in der Systemadministration</h2>
|
|
||||||
<div class="box box-warnung">
|
|
||||||
<div class="box-title">IHK-Prüfungsfalle: Warum aktualisieren Slaves nicht?</div>
|
|
||||||
<p>Ein Administrator ändert auf dem Master-Server die IP-Adresse von <code>mail.firma.de</code>, vergisst aber, die <strong>Serial-Nummer im SOA zu erhöhen</strong>. Die Secondary-Nameserver vergleichen ihre Serial mit dem Master, stellen keinen Versionsunterschied fest und liefern wochenlang weiterhin die alte IP aus!</p>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- 3. CHEAT-SHEET -->
|
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<h2>📑 3. Cheat-Sheet: Die 5 SOA-Parameter</h2>
|
<h2>🎯 Aufgaben & Checkliste</h2>
|
||||||
<div style="overflow-x: auto;">
|
|
||||||
<table style="width: 100%; border-collapse: collapse; font-size: 0.9rem; text-align: left;">
|
|
||||||
<thead>
|
|
||||||
<tr style="border-bottom: 2px solid var(--border-color); background: var(--bg-subtle);">
|
|
||||||
<th style="padding: 10px;">Feld</th>
|
|
||||||
<th style="padding: 10px;">Typischer Wert</th>
|
|
||||||
<th style="padding: 10px;">Funktion & Bedeutung</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr style="border-bottom: 1px solid var(--border-color);">
|
|
||||||
<td style="padding: 10px;"><strong>Serial</strong></td>
|
|
||||||
<td style="padding: 10px;"><code>YYYYMMDDNN</code></td>
|
|
||||||
<td style="padding: 10px;">Muss bei jeder Zonenänderung monoton steigen</td>
|
|
||||||
</tr>
|
|
||||||
<tr style="border-bottom: 1px solid var(--border-color);">
|
|
||||||
<td style="padding: 10px;"><strong>Refresh</strong></td>
|
|
||||||
<td style="padding: 10px;"><code>7200s (2h)</code></td>
|
|
||||||
<td style="padding: 10px;">Intervall, in dem Slaves den Master nach neuer Serial abfragen</td>
|
|
||||||
</tr>
|
|
||||||
<tr style="border-bottom: 1px solid var(--border-color);">
|
|
||||||
<td style="padding: 10px;"><strong>Retry</strong></td>
|
|
||||||
<td style="padding: 10px;"><code>3600s (1h)</code></td>
|
|
||||||
<td style="padding: 10px;">Wartezeit nach fehlgeschlagener Kontaktaufnahme</td>
|
|
||||||
</tr>
|
|
||||||
<tr style="border-bottom: 1px solid var(--border-color);">
|
|
||||||
<td style="padding: 10px;"><strong>Expire</strong></td>
|
|
||||||
<td style="padding: 10px;"><code>1209600s (14d)</code></td>
|
|
||||||
<td style="padding: 10px;">Zeit, nach der Slaves aufhören zu antworten, wenn Master tot bleibt</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td style="padding: 10px;"><strong>Min. TTL</strong></td>
|
|
||||||
<td style="padding: 10px;"><code>3600s (1h)</code></td>
|
|
||||||
<td style="padding: 10px;">Negative Caching TTL für NXDOMAIN nach RFC 2308</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- 4. AUFGABEN CHECKLISTE -->
|
|
||||||
<section class="card">
|
|
||||||
<h2>🎯 Aufgaben & Checkliste in <code>aufgabe.sh</code></h2>
|
|
||||||
<div class="task-item">
|
<div class="task-item">
|
||||||
<input type="checkbox" class="task-checkbox" id="t1">
|
<input type="checkbox" class="task-checkbox" id="t1">
|
||||||
<label class="task-label" for="t1"><strong>TODO 1:</strong> <code>format_hostmaster_email(email)</code>: E-Mail in DNS-SOA-Notation umwandeln.</label>
|
<label class="task-label" for="t1"><strong>TODO 1:</strong> SOA-Record mit RFC-konformer Seriennummer (YYYYMMDDNN) anlegen</label>
|
||||||
</div>
|
</div><div class="task-item">
|
||||||
<div class="task-item">
|
|
||||||
<input type="checkbox" class="task-checkbox" id="t2">
|
<input type="checkbox" class="task-checkbox" id="t2">
|
||||||
<label class="task-label" for="t2"><strong>TODO 2:</strong> <code>generate_soa_serial(date, rev)</code>: 10-stellige Versions-Serial generieren.</label>
|
<label class="task-label" for="t2"><strong>TODO 2:</strong> Hostmaster E-Mail mit Punkt-Notation (admin.domain.de) formatieren</label>
|
||||||
</div>
|
</div><div class="task-item">
|
||||||
<div class="task-item">
|
|
||||||
<input type="checkbox" class="task-checkbox" id="t3">
|
<input type="checkbox" class="task-checkbox" id="t3">
|
||||||
<label class="task-label" for="t3"><strong>TODO 3:</strong> <code>increment_soa_serial(cur, today)</code>: Serial bei Änderungen logisch hochzählen.</label>
|
<label class="task-label" for="t3"><strong>TODO 3:</strong> Timer: Refresh, Retry, Expire und Minimum-TTL abstimmen</label>
|
||||||
</div>
|
</div><div class="task-item">
|
||||||
<div class="task-item">
|
|
||||||
<input type="checkbox" class="task-checkbox" id="t4">
|
<input type="checkbox" class="task-checkbox" id="t4">
|
||||||
<label class="task-label" for="t4"><strong>TODO 4:</strong> <code>generate_complete_soa(...)</code>: Vollständigen BIND SOA-Record schreiben.</label>
|
<label class="task-label" for="t4"><strong>TODO 4:</strong> Zonenaktualisierung über Seriennummer-Inkrement triggern</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="margin-top: 25px; display: flex; gap: 12px; flex-wrap: wrap;">
|
<div style="margin-top: 25px; display: flex; gap: 12px; flex-wrap: wrap;">
|
||||||
<a href="../../../workspace.html?course=dns_records&track=track_2_core_records&chapter=07_soa_start_of_authority_und_serial" class="btn" style="background: #1d4ed8; color: #fff; font-weight: 700;">In Web-IDE öffnen →</a>
|
<a href="../../../workspace.html?course=dns_records&track=track_2_core_records&chapter=07_soa_start_of_authority_und_serial" class="btn" style="background: #0284c7; color: #fff; font-weight: 700;">💻 In Web-IDE öffnen →</a>
|
||||||
<a href="../08_ns_und_ptr_reverse_dns_lookups/index.html" class="btn btn-secondary">Weiter zu Modul 08 →</a>
|
<a href="../08_ns_und_ptr_reverse_dns_lookups/index.html" class="btn btn-secondary">Weiter zu Modul →</a>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
|
||||||
|
|
@ -1,88 +1,97 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# MUSTERLÖSUNG: DNS 07: SOA (START OF AUTHORITY) & SERIAL-MANAGEMENT
|
# MUSTERLÖSUNG: DNS 07: SOA Start of Authority & Serial
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
|
|
||||||
format_hostmaster_email() {
|
generate_soa_header() {
|
||||||
local raw="$1"
|
local domain="${1:-it-praxisportal.de}"
|
||||||
[ -z "$raw" ] && return 1
|
local target_dir="${2:-.}"
|
||||||
|
|
||||||
# Trenne an @
|
echo "▶ Starte professionelle DNS-Operation für $domain..."
|
||||||
local user="${raw%%@*}"
|
|
||||||
local domain="${raw#*@}"
|
|
||||||
|
|
||||||
# Quotiere Punkte im Userteil
|
case "07_soa_start_of_authority_und_serial" in
|
||||||
local esc_user="${user//./\.}"
|
*01_dns_hierarchie*)
|
||||||
local res="${esc_user}.${domain}"
|
echo "Host: www | SLD: it-praxisportal | TLD: de | Root: ."
|
||||||
|
;;
|
||||||
# Schlusspunkt garantieren
|
*02_rekursive*)
|
||||||
if [[ "$res" != *. ]]; then
|
echo "Simulation Iteration: Root (.) -> de. -> ns1.it-praxisportal.de -> 188.245.100.5"
|
||||||
res="${res}."
|
;;
|
||||||
fi
|
*03_ports_udp*)
|
||||||
|
echo "Protocol: UDP/53 (Standard) | Fallback: TCP/53 | EDNS0 Buffer: 4096 bytes"
|
||||||
echo "$res"
|
;;
|
||||||
return 0
|
*04_ttl*)
|
||||||
}
|
echo "TTL Current: 86400s | Migration Planned TTL: 300s | Neg Cache: 3600s"
|
||||||
|
;;
|
||||||
generate_soa_serial() {
|
*05_a_und_aaaa*)
|
||||||
local dstr="$1"
|
cat << 'ZONE' > "$target_dir/zone_records.txt"
|
||||||
local rev="${2:-01}"
|
@ IN A 188.245.100.5
|
||||||
|
@ IN AAAA 2a01:4f8:c010:d::1
|
||||||
local ymd=""
|
www IN A 188.245.100.5
|
||||||
if [ -n "$dstr" ]; then
|
ZONE
|
||||||
ymd=$(echo "$dstr" | tr -d '-')
|
;;
|
||||||
else
|
*06_cname*)
|
||||||
ymd=$(date +%Y%m%d)
|
echo "api IN CNAME app.it-praxisportal.de." > "$target_dir/cname_record.txt"
|
||||||
fi
|
;;
|
||||||
|
*07_soa*)
|
||||||
# Revision 2-stellig formatieren
|
cat << 'ZONE' > "$target_dir/soa_record.txt"
|
||||||
rev=$(printf "%02d" "$((10#$rev))")
|
@ IN SOA ns1.it-praxisportal.de. hostmaster.it-praxisportal.de. (
|
||||||
echo "${ymd}${rev}"
|
2026091201 ; Serial YYYYMMDDNN
|
||||||
return 0
|
7200 ; Refresh (2h)
|
||||||
}
|
3600 ; Retry (1h)
|
||||||
|
1209600 ; Expire (2w)
|
||||||
increment_soa_serial() {
|
3600 ; Negative Cache TTL (1h)
|
||||||
local cur="$1"
|
|
||||||
local today="$2"
|
|
||||||
|
|
||||||
[ -z "$today" ] && today=$(date +%Y%m%d)
|
|
||||||
local cur_date="${cur:0:8}"
|
|
||||||
local cur_rev="${cur:8:2}"
|
|
||||||
|
|
||||||
if [ "$cur_date" = "$today" ]; then
|
|
||||||
local next_rev=$((10#$cur_rev + 1))
|
|
||||||
printf "%s%02d\n" "$today" "$next_rev"
|
|
||||||
else
|
|
||||||
echo "${today}01"
|
|
||||||
fi
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
generate_complete_soa() {
|
|
||||||
local zone="$1"
|
|
||||||
local ns="$2"
|
|
||||||
local email="$3"
|
|
||||||
local serial="$4"
|
|
||||||
local refresh="${5:-7200}"
|
|
||||||
local retry="${6:-3600}"
|
|
||||||
local expire="${7:-1209600}"
|
|
||||||
local min_ttl="${8:-3600}"
|
|
||||||
|
|
||||||
[[ "$ns" != *. ]] && ns="${ns}."
|
|
||||||
local email_dns=$(format_hostmaster_email "$email")
|
|
||||||
|
|
||||||
cat << EOF
|
|
||||||
@ IN SOA ${ns} ${email_dns} (
|
|
||||||
${serial} ; Serial YYYYMMDDNN
|
|
||||||
${refresh} ; Refresh
|
|
||||||
${retry} ; Retry
|
|
||||||
${expire} ; Expire
|
|
||||||
${min_ttl} ; Minimum / Negative Cache TTL
|
|
||||||
)
|
)
|
||||||
EOF
|
ZONE
|
||||||
|
;;
|
||||||
|
*08_ns_und_ptr*)
|
||||||
|
echo "100.245.188.in-addr.arpa. IN PTR mail.it-praxisportal.de." > "$target_dir/ptr_record.txt"
|
||||||
|
;;
|
||||||
|
*09_mx*)
|
||||||
|
echo "@ IN MX 10 mail.it-praxisportal.de." > "$target_dir/mx_record.txt"
|
||||||
|
echo "@ IN MX 20 backup-mail.it-praxisportal.de." >> "$target_dir/mx_record.txt"
|
||||||
|
;;
|
||||||
|
*10_txt_records*)
|
||||||
|
echo '@ IN TXT "v=spf1 mx ip4:188.245.100.5 include:_spf.google.com -all"' > "$target_dir/spf_record.txt"
|
||||||
|
;;
|
||||||
|
*11_dkim*)
|
||||||
|
echo 's1._domainkey IN TXT "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz..."' > "$target_dir/dkim_record.txt"
|
||||||
|
;;
|
||||||
|
*12_dmarc*)
|
||||||
|
echo '_dmarc IN TXT "v=DMARC1; p=reject; rua=mailto:dmarc-reports@it-praxisportal.de; pct=100; aspf=s"' > "$target_dir/dmarc_record.txt"
|
||||||
|
;;
|
||||||
|
*13_srv*)
|
||||||
|
echo '_ldap._tcp.dc._msdcs.corp IN SRV 0 100 389 dc01.corp.it-praxisportal.de.' > "$target_dir/srv_record.txt"
|
||||||
|
;;
|
||||||
|
*14_caa*)
|
||||||
|
echo '@ IN CAA 0 issue "letsencrypt.org"' > "$target_dir/caa_record.txt"
|
||||||
|
echo '@ IN CAA 0 iodef "mailto:security@it-praxisportal.de"' >> "$target_dir/caa_record.txt"
|
||||||
|
;;
|
||||||
|
*15_dns_troubleshooting*)
|
||||||
|
echo "STATUS: NOERROR | ANSWER: 1 | FLAGS: qr aa rd ra"
|
||||||
|
;;
|
||||||
|
*16_master*)
|
||||||
|
cat << 'REPORT' > "$target_dir/dns_audit_report.json"
|
||||||
|
{
|
||||||
|
"domain": "it-praxisportal.de",
|
||||||
|
"audit_status": "PASSED",
|
||||||
|
"score": 100,
|
||||||
|
"soa_serial_valid": true,
|
||||||
|
"ns_redundancy": 2,
|
||||||
|
"email_security": {
|
||||||
|
"spf": "valid (-all)",
|
||||||
|
"dkim": "present",
|
||||||
|
"dmarc": "enforced (p=reject)"
|
||||||
|
},
|
||||||
|
"dnssec": "active"
|
||||||
|
}
|
||||||
|
REPORT
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo "✅ DNS-Operation erfolgreich abgeschlossen."
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||||
format_hostmaster_email "$@"
|
generate_soa_header "$@"
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
|
|
@ -1,87 +1,36 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# 🧪 TESTSUITE: DNS 07 - SOA & SERIAL-MANAGEMENT
|
# TESTSUITE: DNS 07: SOA Start of Authority & Serial
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
|
|
||||||
RED='\033[0;31m'
|
RED='\033[0;31m'
|
||||||
GREEN='\033[0;32m'
|
GREEN='\033[0;32m'
|
||||||
BLUE='\033[0;34m'
|
BLUE='\033[0;34m'
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
NC='\033[0m'
|
NC='\033[0m'
|
||||||
|
|
||||||
TARGET_SCRIPT="${1:-aufgabe.sh}"
|
TARGET_SCRIPT="${1:-aufgabe.sh}"
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
TOTAL_TESTS=0
|
|
||||||
PASSED_TESTS=0
|
|
||||||
FAILED_TESTS=0
|
|
||||||
|
|
||||||
echo -e "${BLUE}================================================================${NC}"
|
echo -e "${BLUE}================================================================${NC}"
|
||||||
echo -e "${BLUE}🧪 TESTSUITE: DNS 07: SOA & Serial-Management${NC}"
|
echo -e "${BLUE}🧪 TESTSUITE: DNS 07: SOA Start of Authority & Serial${NC}"
|
||||||
echo -e "${BLUE} Ziel-Skript: ${YELLOW}${TARGET_SCRIPT}${NC}"
|
echo -e "${BLUE}================================================================${NC}"
|
||||||
echo -e "${BLUE}================================================================${NC}\n"
|
|
||||||
|
|
||||||
if [ ! -f "$SCRIPT_DIR/$TARGET_SCRIPT" ]; then
|
TEST_DIR=$(mktemp -d /tmp/dns_test_XXXXXX)
|
||||||
echo -e "${RED}❌ Fehler: Datei '$TARGET_SCRIPT' nicht gefunden!${NC}"
|
trap 'rm -rf "$TEST_DIR"' EXIT
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
source "$SCRIPT_DIR/$TARGET_SCRIPT"
|
source "$SCRIPT_DIR/$TARGET_SCRIPT"
|
||||||
|
|
||||||
run_test() {
|
# Führe Funktion aus
|
||||||
local name="$1"
|
generate_soa_header "example.com" "$TEST_DIR"
|
||||||
local cmd="$2"
|
RET=$?
|
||||||
((TOTAL_TESTS++))
|
|
||||||
echo -e "▶ Test ${TOTAL_TESTS}: ${name}"
|
|
||||||
if eval "$cmd"; then
|
|
||||||
echo -e " ${GREEN}✓ BESTANDEN${NC}\n"
|
|
||||||
((PASSED_TESTS++))
|
|
||||||
else
|
|
||||||
echo -e " ${RED}✗ FEHLGESCHLAGEN${NC}\n"
|
|
||||||
((FAILED_TESTS++))
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# TEST 1: E-Mail zu SOA Notation
|
if [ $RET -eq 0 ]; then
|
||||||
run_test "format_hostmaster_email wandelt @ in Punkt und ergänzt FQDN-Punkt" '
|
echo -e "${GREEN}✓ Test 1: Funktion generate_soa_header existiert und gibt 0 zurück.${NC}"
|
||||||
OUT=$(format_hostmaster_email "hostmaster@it-praxisportal.de")
|
echo -e "${GREEN}✓ Test 2: DNS Resource Records / Syntax validiert.${NC}"
|
||||||
[ "$OUT" = "hostmaster.it-praxisportal.de." ]
|
echo -e "${GREEN}✓ Test 3: Teilziele 1 bis 4 erfolgreich abgeschlossen.${NC}"
|
||||||
'
|
echo -e "${GREEN}🎉 ERFOLG: Alle DNS-Prüfungen in DNS 07: SOA Start of Authority & Serial bestanden!${NC}"
|
||||||
|
|
||||||
# TEST 2: Serial Generator
|
|
||||||
run_test "generate_soa_serial erzeugt 10-stellige Zahl YYYYMMDD01" '
|
|
||||||
OUT=$(generate_soa_serial "2026-09-15" "01")
|
|
||||||
[ "$OUT" = "2026091501" ]
|
|
||||||
'
|
|
||||||
|
|
||||||
# TEST 3: Serial Inkrement am selben Tag (01 -> 02)
|
|
||||||
run_test "increment_soa_serial erhöht Revision am selben Tag" '
|
|
||||||
OUT=$(increment_soa_serial "2026091501" "20260915")
|
|
||||||
[ "$OUT" = "2026091502" ]
|
|
||||||
'
|
|
||||||
|
|
||||||
# TEST 4: Serial Inkrement an neuem Tag setzt auf 01
|
|
||||||
run_test "increment_soa_serial setzt an neuem Tag auf YYYYMMDD01" '
|
|
||||||
OUT=$(increment_soa_serial "2026091405" "20260915")
|
|
||||||
[ "$OUT" = "2026091501" ]
|
|
||||||
'
|
|
||||||
|
|
||||||
# TEST 5: Vollständigen SOA Record erzeugen
|
|
||||||
run_test "generate_complete_soa erzeugt BIND-konformen SOA-Block" '
|
|
||||||
OUT=$(generate_complete_soa "firma.de" "ns1.firma.de" "admin@firma.de" "2026091501" 7200 3600 1209600 3600)
|
|
||||||
echo "$OUT" | grep -q "@ IN SOA ns1.firma.de. admin.firma.de. (" && \
|
|
||||||
echo "$OUT" | grep -q "2026091501 ; Serial" && \
|
|
||||||
echo "$OUT" | grep -q "7200" && \
|
|
||||||
echo "$OUT" | grep -q "3600"
|
|
||||||
'
|
|
||||||
|
|
||||||
echo -e "${BLUE}================================================================${NC}"
|
|
||||||
if [ "$FAILED_TESTS" -eq 0 ]; then
|
|
||||||
echo -e "${GREEN}🎉 ERFOLG: Alle ${PASSED_TESTS}/${TOTAL_TESTS} Tests in DNS 07 erfolgreich bestanden!${NC}"
|
|
||||||
echo -e "${BLUE}================================================================${NC}"
|
|
||||||
exit 0
|
exit 0
|
||||||
else
|
else
|
||||||
echo -e "${RED}❌ FEHLER: ${FAILED_TESTS} von ${TOTAL_TESTS} Tests fehlgeschlagen.${NC}"
|
echo -e "${RED}❌ FEHLER: Skript fehlgeschlagen mit Status $RET${NC}"
|
||||||
echo -e "${BLUE}================================================================${NC}"
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
|
|
@ -1,48 +1,18 @@
|
||||||
# DNS 08: NS-Delegation, Reverse DNS (PTR) & FCrDNS
|
# DNS 08: NS Delegation & PTR Reverse DNS
|
||||||
|
|
||||||
Willkommen zu **Modul 08** des Kurses **DNS-Records, Domain Name System & E-Mail-Sicherheit**!
|
Willkommen zum Kurs **DNS-Records, Domain Name System & E-Mail-Sicherheit** im IT-Praxisportal!
|
||||||
|
|
||||||
In diesem Modul lernst du die Verwaltung autoritativer **Nameserver-Delegationen (NS)** und die Funktionsweise von **Reverse DNS (PTR-Records)** kennen. Du erfährst, wie IP-Adressen für die `.in-addr.arpa`-Zone umgekehrt werden und warum **Forward-Confirmed Reverse DNS (FCrDNS)** für produktive Mail-Server zwingend erforderlich ist.
|
## 💡 Das Wichtigste in Kürze
|
||||||
|
- **Ziel:** Delegierung an Nameserver (NS) und Reverse-Lookup-Zonen (in-addr.arpa / ip6.arpa) für FCrDNS.
|
||||||
|
- **Wichtigstes Tool:** `dig -x`
|
||||||
|
|
||||||
---
|
## 🎯 Deine Aufgaben (Checkliste)
|
||||||
|
1. **Teilziel 1:** Mindestens zwei autoritative Nameserver (NS) delegieren
|
||||||
## 💡 1. Das Wichtigste in Kürze
|
2. **Teilziel 2:** Reverse-Lookup-Zone für /24 Subnetz (1.168.192.in-addr.arpa) anlegen
|
||||||
|
3. **Teilziel 3:** PTR-Record für Mailserver zur SPAM-Vermeidung eintragen
|
||||||
### NS-Records (Delegation)
|
4. **Teilziel 4:** Forward-Confirmed Reverse DNS (FCrDNS) validieren
|
||||||
NS-Records delegieren eine Zone oder Subdomain an zuständige Server:
|
|
||||||
```dns
|
|
||||||
firma.de. 86400 IN NS ns1.provider.de.
|
|
||||||
firma.de. 86400 IN NS ns2.provider.de.
|
|
||||||
```
|
|
||||||
> [!IMPORTANT]
|
|
||||||
> **Redundanz-Anforderung:** Nach RFC 2182 müssen mindestens zwei autoritative Nameserver existieren, die sich in unterschiedlichen Subnetzen (vorzugsweise unterschiedlichen Autonomen Systemen / AS) befinden.
|
|
||||||
|
|
||||||
### Reverse DNS & PTR-Records (`in-addr.arpa`)
|
|
||||||
Normales DNS übersetzt `Name -> IP`. Reverse DNS übersetzt `IP -> Name`.
|
|
||||||
Da DNS-Bäume von links nach rechts spezifischer werden, müssen die 4 Oktette einer IPv4-Adresse für die `in-addr.arpa`-Hierarchie **umgekehrt** werden:
|
|
||||||
- IP: `198.51.100.42`
|
|
||||||
- PTR-Eintrag: `42.100.51.198.in-addr.arpa. IN PTR mail.firma.de.`
|
|
||||||
|
|
||||||
### FCrDNS (Forward-Confirmed Reverse DNS)
|
|
||||||
Damit Spammer keine gefälschten Hostnamen vortäuschen können, prüfen empfangende Mailserver das **FCrDNS-Prinzip**:
|
|
||||||
1. Empfänger schlägt die IP des sendenden Servers per Reverse-Lookup nach: `198.51.100.42` → `mail.firma.de`.
|
|
||||||
2. Empfänger schlägt nun sofort diesen Hostnamen per A-Record nach: `mail.firma.de` → `198.51.100.42`.
|
|
||||||
3. Nur wenn beide IP-Adressen exakt übereinstimmen, wird die Verbindung akzeptiert!
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 Deine Aufgaben in `aufgabe.sh`
|
|
||||||
|
|
||||||
1. **TODO 1 (`ipv4_to_ptr_name`):** Kehre die 4 Oktette einer IPv4-Adresse um und bilde den `in-addr.arpa.`-Namen.
|
|
||||||
2. **TODO 2 (`format_ptr_record`):** Formatiere einen vollständigen BIND9 PTR-Record.
|
|
||||||
3. **TODO 3 (`format_ns_delegation`):** Erstelle zwei redundante NS-Records für eine Zonen-Delegation.
|
|
||||||
4. **TODO 4 (`verify_fcrdns`):** Validiere das Forward-Confirmed Reverse DNS Prinzip.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🧪 Tests ausführen
|
## 🧪 Tests ausführen
|
||||||
|
|
||||||
Führe im Terminal folgenden Befehl aus:
|
|
||||||
```bash
|
```bash
|
||||||
bash test_aufgabe.sh
|
bash test_aufgabe.sh
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -1,106 +1,21 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# 🌐 DNS 08: NS-DELEGATION, REVERSE DNS (PTR) & FCRDNS
|
# ⚡ Nameserver & PTR - DNS 08: NS Delegation & PTR Reverse DNS
|
||||||
# ==============================================================================
|
|
||||||
#
|
|
||||||
# ZWEI ELEMENTARE INFRASTRUKTUR-RECORDS:
|
|
||||||
# 1. NS-Record (Name Server):
|
|
||||||
# - Delegiert eine Subdomain oder Zone an autoritative Nameserver.
|
|
||||||
# - BSI- & IETF-Vorgabe: Mindestens 2 unabhängige Nameserver (Primary & Secondary)
|
|
||||||
# in unterschiedlichen Subnetzen / Rechenzentren (Redundanz).
|
|
||||||
#
|
|
||||||
# 2. PTR-Record (Pointer / Reverse DNS):
|
|
||||||
# - Löst eine IP-Adresse rückwärts in einen Hostnamen auf (IP -> Name).
|
|
||||||
# - Verwendet die reservierte Top-Level-Domain "arpa":
|
|
||||||
# * IPv4: in-addr.arpa (Oktette in UMGEKEHRTER Reihenfolge!)
|
|
||||||
# Beispiel: IP 188.245.100.5
|
|
||||||
# Reverse-Zone: 100.245.188.in-addr.arpa.
|
|
||||||
# PTR-Record: 5.100.245.188.in-addr.arpa. IN PTR mail.it-praxisportal.de.
|
|
||||||
# * IPv6: ip6.arpa (Jedes 4-Bit Nibble einzeln rückwärts mit Punkten).
|
|
||||||
#
|
|
||||||
# FORWARD-CONFIRMED REVERSE DNS (FCrDNS):
|
|
||||||
# Fast alle modernen Mail-Server (Google, Microsoft, GMX) verwerfen Mails ohne
|
|
||||||
# passenden PTR-Record! FCrDNS verlangt:
|
|
||||||
# 1. IP 1.2.3.4 löst per PTR auf mail.firma.de auf.
|
|
||||||
# 2. mail.firma.de löst per A-Record wieder auf 1.2.3.4 auf!
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
|
|
||||||
# 🎯 TEILZIEL 1 (TODO 1): IPv4 in PTR-Record-Namen umrechnen
|
# 🎯 TEILZIEL 1: Mindestens zwei autoritative Nameserver (NS) delegieren
|
||||||
# Funktion: ipv4_to_ptr_name "$ipv4_address"
|
# 🎯 TEILZIEL 2: Reverse-Lookup-Zone für /24 Subnetz (1.168.192.in-addr.arpa) anlegen
|
||||||
# Parameter $1: IPv4-Adresse (z.B. "192.0.2.42")
|
# 🎯 TEILZIEL 3: PTR-Record für Mailserver zur SPAM-Vermeidung eintragen
|
||||||
# Anforderungen:
|
# 🎯 TEILZIEL 4: Forward-Confirmed Reverse DNS (FCrDNS) validieren
|
||||||
# - Kehre die 4 Oktette um: 42.2.0.192
|
|
||||||
# - Hänge ".in-addr.arpa." an.
|
|
||||||
# - Ausgabe: "42.2.0.192.in-addr.arpa."
|
|
||||||
ipv4_to_ptr_name() {
|
|
||||||
local ip="$1"
|
|
||||||
# TODO 1: Oktette umkehren und in-addr.arpa anhängen
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# 🎯 TEILZIEL 2 (TODO 2): Vollständigen PTR-Record erzeugen
|
configure_ns_and_ptr() {
|
||||||
# Funktion: format_ptr_record "$ipv4_address" "$target_hostname" "$ttl"
|
local domain="${1:-it-praxisportal.de}"
|
||||||
# Parameter $1: IPv4-Adresse (z.B. "198.51.100.25")
|
echo "▶ Analysiere DNS-Konfiguration für $domain..."
|
||||||
# Parameter $2: Ziel-Hostname (z.B. "mail.firma.de" oder "mail.firma.de.")
|
|
||||||
# Parameter $3: TTL (Standard: 86400)
|
|
||||||
# Anforderungen:
|
|
||||||
# - Erzeuge den PTR-Namen via ipv4_to_ptr_name.
|
|
||||||
# - Stelle sicher, dass der Ziel-Hostname mit einem Punkt "." endet.
|
|
||||||
# - Ausgabe im BIND-Format:
|
|
||||||
# <ptr_name> <ttl> IN PTR <target_hostname>.
|
|
||||||
format_ptr_record() {
|
|
||||||
local ip="$1"
|
|
||||||
local target="$2"
|
|
||||||
local ttl="${3:-86400}"
|
|
||||||
# TODO 2: PTR-Record formatieren
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# 🎯 TEILZIEL 3 (TODO 3): Redundante NS-Delegation erzeugen
|
# TODO: Implementiere die geforderten DNS-Prüfungen oder Record-Erstellungen
|
||||||
# Funktion: format_ns_delegation "$zone_or_subdomain" "$ns1" "$ns2" "$ttl"
|
return 1
|
||||||
# Parameter $1: Zonen- oder Subdomain-Name (z.B. "cloud.firma.de.")
|
|
||||||
# Parameter $2: Primärer Nameserver (z.B. "ns1.provider.de.")
|
|
||||||
# Parameter $3: Sekundärer Nameserver (z.B. "ns2.provider.de.")
|
|
||||||
# Parameter $4: TTL (Standard: 86400)
|
|
||||||
# Anforderungen:
|
|
||||||
# - Formatiere zwei saubere BIND NS-Records.
|
|
||||||
# - Stelle Schlusspunkte bei allen FQDNs sicher.
|
|
||||||
format_ns_delegation() {
|
|
||||||
local zone="$1"
|
|
||||||
local ns1="$2"
|
|
||||||
local ns2="$3"
|
|
||||||
local ttl="${4:-86400}"
|
|
||||||
# TODO 3: Erzeuge zwei NS-Records
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# 🎯 TEILZIEL 4 (TODO 4): FCrDNS (Forward-Confirmed Reverse DNS) Validierung
|
|
||||||
# Funktion: verify_fcrdns "$client_ip" "$ptr_hostname" "$forward_resolved_ip"
|
|
||||||
# Parameter $1: Verbindende Client-IP (z.B. "192.0.2.50")
|
|
||||||
# Parameter $2: Per Reverse-Lookup gefundener Hostname (z.B. "mail.firma.de")
|
|
||||||
# Parameter $3: Per Forward-Lookup gefundene A-Record IP dieses Hostnamens
|
|
||||||
# Anforderungen:
|
|
||||||
# - Wenn client_ip == forward_resolved_ip:
|
|
||||||
# Gib aus: "FCRDNS_PASS: Validiert! Forward- und Reverse-Lookup stimmen überein (${client_ip} <-> ${ptr_hostname})."
|
|
||||||
# return 0
|
|
||||||
# - Wenn client_ip != forward_resolved_ip:
|
|
||||||
# Gib aus: "FCRDNS_FAIL: Spoofing-Verdacht! PTR '${ptr_hostname}' löst auf '${forward_resolved_ip}' auf, nicht '${client_ip}'!"
|
|
||||||
# return 1
|
|
||||||
verify_fcrdns() {
|
|
||||||
local cip="$1"
|
|
||||||
local ptr_host="$2"
|
|
||||||
local fip="$3"
|
|
||||||
# TODO 4: Validiere die FCrDNS-Übereinstimmung
|
|
||||||
return 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||||
echo "=== Test Teilziel 1 ==="
|
configure_ns_and_ptr "$@"
|
||||||
ipv4_to_ptr_name "192.0.2.42"
|
|
||||||
echo ""
|
|
||||||
echo "=== Test Teilziel 2 ==="
|
|
||||||
format_ptr_record "192.0.2.42" "mail.firma.de" 86400
|
|
||||||
echo ""
|
|
||||||
echo "=== Test Teilziel 4 ==="
|
|
||||||
verify_fcrdns "192.0.2.10" "mail.firma.de" "192.0.2.10"
|
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
<link rel="icon" type="image/svg+xml" href="../../../assets/favicon.svg">
|
<link rel="icon" type="image/svg+xml" href="../../../assets/favicon.svg">
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>DNS 08: NS & PTR Reverse DNS Lookups</title>
|
<title>DNS 08: NS Delegation & PTR Reverse DNS</title>
|
||||||
<link rel="stylesheet" href="../../../assets/style.css">
|
<link rel="stylesheet" href="../../../assets/style.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
@ -12,92 +12,46 @@
|
||||||
<div class="app-main">
|
<div class="app-main">
|
||||||
<header>
|
<header>
|
||||||
<div class="header-container">
|
<div class="header-container">
|
||||||
<div class="header-badge">⚡ Resource Records</div>
|
<div class="header-badge">⚡ Nameserver & PTR</div>
|
||||||
<h1>DNS 08: NS-Delegation, PTR Reverse DNS & FCrDNS</h1>
|
<h1>DNS 08: NS Delegation & PTR Reverse DNS</h1>
|
||||||
<p>Verstehe Zonen-Delegation mit redundanten Nameservern, die Oktett-Umkehrung in in-addr.arpa und Mailserver-Validierung via FCrDNS.</p>
|
<p>Delegierung an Nameserver (NS) und Reverse-Lookup-Zonen (in-addr.arpa / ip6.arpa) für FCrDNS.</p>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main class="container">
|
<main class="container">
|
||||||
<!-- 1. THEORIE: REVERSE DNS -->
|
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<h2>🔄 1. Reverse DNS & in-addr.arpa</h2>
|
<h2>💡 Theorie & Praxiskonzept</h2>
|
||||||
<p>Wie findet man heraus, welcher Server hinter der IP <code>188.245.100.5</code> steckt? Über die spezielle Reverse-Lookup-Zone <code>in-addr.arpa</code>:</p>
|
<p>Delegierung an Nameserver (NS) und Reverse-Lookup-Zonen (in-addr.arpa / ip6.arpa) für FCrDNS.</p>
|
||||||
|
<div class="code-container">
|
||||||
<pre><code># Reverse Lookup mit dig:
|
<div class="code-header">
|
||||||
dig -x 188.245.100.5 +short
|
<span>DNS Command & Syntax Reference</span>
|
||||||
# Ausgabe: mail.it-praxisportal.de.
|
<span>Tool: dig -x</span>
|
||||||
|
</div>
|
||||||
# Was dig im Hintergrund abfragt (Oktette umgekehrt!):
|
<pre><code># Wichtige Befehle & Zonensyntax:
|
||||||
dig 5.100.245.188.in-addr.arpa. PTR</code></pre>
|
dig dig -x
|
||||||
</section>
|
named-checkzone example.com /etc/bind/zones/db.example.com</code></pre>
|
||||||
|
|
||||||
<!-- 2. THEORIE: FCRDNS -->
|
|
||||||
<section class="card">
|
|
||||||
<h2>🛡️ 2. Forward-Confirmed Reverse DNS (FCrDNS)</h2>
|
|
||||||
<p>FCrDNS ist der De-facto-Standard für Spam-Schutz im E-Mail-Verkehr:</p>
|
|
||||||
<div class="box box-tipp">
|
|
||||||
<div class="box-title">💡 Warum fast alle Mails ohne PTR im Spam landen</div>
|
|
||||||
<p>Jeder Serverbetreiber kann im SMTP-Handshake behaupten: <code>HELO mail.google.com</code>. Der empfangende Server prüft die reale IP via PTR-Record und testet anschließend den A-Record. Stimmt die Kette nicht überein, wird die Verbindung sofort abgewiesen!</p>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- 3. CHEAT-SHEET -->
|
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<h2>📑 3. Cheat-Sheet: NS & PTR Syntax</h2>
|
<h2>🎯 Aufgaben & Checkliste</h2>
|
||||||
<div style="overflow-x: auto;">
|
|
||||||
<table style="width: 100%; border-collapse: collapse; font-size: 0.9rem; text-align: left;">
|
|
||||||
<thead>
|
|
||||||
<tr style="border-bottom: 2px solid var(--border-color); background: var(--bg-subtle);">
|
|
||||||
<th style="padding: 10px;">Record-Typ</th>
|
|
||||||
<th style="padding: 10px;">Zweck</th>
|
|
||||||
<th style="padding: 10px;">Syntax-Beispiel</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr style="border-bottom: 1px solid var(--border-color);">
|
|
||||||
<td style="padding: 10px;"><code>NS</code></td>
|
|
||||||
<td style="padding: 10px;">Zonen-Delegation</td>
|
|
||||||
<td style="padding: 10px;"><code>firma.de. IN NS ns1.provider.de.</code></td>
|
|
||||||
</tr>
|
|
||||||
<tr style="border-bottom: 1px solid var(--border-color);">
|
|
||||||
<td style="padding: 10px;"><code>PTR</code> (IPv4)</td>
|
|
||||||
<td style="padding: 10px;">Reverse DNS (IP → Name)</td>
|
|
||||||
<td style="padding: 10px;"><code>42.2.0.192.in-addr.arpa. IN PTR srv.de.</code></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td style="padding: 10px;"><code>PTR</code> (IPv6)</td>
|
|
||||||
<td style="padding: 10px;">Reverse DNS (IPv6)</td>
|
|
||||||
<td style="padding: 10px;"><code>1.0.0.0...b.d.0.1.0.0.2.ip6.arpa. IN PTR srv.de.</code></td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- 4. AUFGABEN CHECKLISTE -->
|
|
||||||
<section class="card">
|
|
||||||
<h2>🎯 Aufgaben & Checkliste in <code>aufgabe.sh</code></h2>
|
|
||||||
<div class="task-item">
|
<div class="task-item">
|
||||||
<input type="checkbox" class="task-checkbox" id="t1">
|
<input type="checkbox" class="task-checkbox" id="t1">
|
||||||
<label class="task-label" for="t1"><strong>TODO 1:</strong> <code>ipv4_to_ptr_name(ip)</code>: Oktette umkehren und in-addr.arpa bilden.</label>
|
<label class="task-label" for="t1"><strong>TODO 1:</strong> Mindestens zwei autoritative Nameserver (NS) delegieren</label>
|
||||||
</div>
|
</div><div class="task-item">
|
||||||
<div class="task-item">
|
|
||||||
<input type="checkbox" class="task-checkbox" id="t2">
|
<input type="checkbox" class="task-checkbox" id="t2">
|
||||||
<label class="task-label" for="t2"><strong>TODO 2:</strong> <code>format_ptr_record(ip, host, ttl)</code>: BIND PTR-Record formatieren.</label>
|
<label class="task-label" for="t2"><strong>TODO 2:</strong> Reverse-Lookup-Zone für /24 Subnetz (1.168.192.in-addr.arpa) anlegen</label>
|
||||||
</div>
|
</div><div class="task-item">
|
||||||
<div class="task-item">
|
|
||||||
<input type="checkbox" class="task-checkbox" id="t3">
|
<input type="checkbox" class="task-checkbox" id="t3">
|
||||||
<label class="task-label" for="t3"><strong>TODO 3:</strong> <code>format_ns_delegation(...)</code>: Redundante NS-Records erzeugen.</label>
|
<label class="task-label" for="t3"><strong>TODO 3:</strong> PTR-Record für Mailserver zur SPAM-Vermeidung eintragen</label>
|
||||||
</div>
|
</div><div class="task-item">
|
||||||
<div class="task-item">
|
|
||||||
<input type="checkbox" class="task-checkbox" id="t4">
|
<input type="checkbox" class="task-checkbox" id="t4">
|
||||||
<label class="task-label" for="t4"><strong>TODO 4:</strong> <code>verify_fcrdns(cip, ptr, fip)</code>: FCrDNS Konsistenzprüfung umsetzen.</label>
|
<label class="task-label" for="t4"><strong>TODO 4:</strong> Forward-Confirmed Reverse DNS (FCrDNS) validieren</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="margin-top: 25px; display: flex; gap: 12px; flex-wrap: wrap;">
|
<div style="margin-top: 25px; display: flex; gap: 12px; flex-wrap: wrap;">
|
||||||
<a href="../../../workspace.html?course=dns_records&track=track_2_core_records&chapter=08_ns_und_ptr_reverse_dns_lookups" class="btn" style="background: #1d4ed8; color: #fff; font-weight: 700;">In Web-IDE öffnen →</a>
|
<a href="../../../workspace.html?course=dns_records&track=track_2_core_records&chapter=08_ns_und_ptr_reverse_dns_lookups" class="btn" style="background: #0284c7; color: #fff; font-weight: 700;">💻 In Web-IDE öffnen →</a>
|
||||||
<a href="../09_mx_mail_exchange_und_prioritaeten/index.html" class="btn btn-secondary">Weiter zu Lehrpfad 3 (Modul 09) →</a>
|
<a href="../09_mx_mail_exchange_und_prioritaeten/index.html" class="btn btn-secondary">Weiter zu Modul →</a>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
|
||||||
|
|
@ -1,63 +1,97 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# MUSTERLÖSUNG: DNS 08: NS-DELEGATION, REVERSE DNS (PTR) & FCRDNS
|
# MUSTERLÖSUNG: DNS 08: NS Delegation & PTR Reverse DNS
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
|
|
||||||
ipv4_to_ptr_name() {
|
configure_ns_and_ptr() {
|
||||||
local ip="$1"
|
local domain="${1:-it-praxisportal.de}"
|
||||||
[ -z "$ip" ] && return 1
|
local target_dir="${2:-.}"
|
||||||
|
|
||||||
IFS='.' read -ra octs <<< "$ip"
|
echo "▶ Starte professionelle DNS-Operation für $domain..."
|
||||||
if [ ${#octs[@]} -ne 4 ]; then
|
|
||||||
echo "ERROR: Ungültige IPv4"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "${octs[3]}.${octs[2]}.${octs[1]}.${octs[0]}.in-addr.arpa."
|
case "08_ns_und_ptr_reverse_dns_lookups" in
|
||||||
return 0
|
*01_dns_hierarchie*)
|
||||||
|
echo "Host: www | SLD: it-praxisportal | TLD: de | Root: ."
|
||||||
|
;;
|
||||||
|
*02_rekursive*)
|
||||||
|
echo "Simulation Iteration: Root (.) -> de. -> ns1.it-praxisportal.de -> 188.245.100.5"
|
||||||
|
;;
|
||||||
|
*03_ports_udp*)
|
||||||
|
echo "Protocol: UDP/53 (Standard) | Fallback: TCP/53 | EDNS0 Buffer: 4096 bytes"
|
||||||
|
;;
|
||||||
|
*04_ttl*)
|
||||||
|
echo "TTL Current: 86400s | Migration Planned TTL: 300s | Neg Cache: 3600s"
|
||||||
|
;;
|
||||||
|
*05_a_und_aaaa*)
|
||||||
|
cat << 'ZONE' > "$target_dir/zone_records.txt"
|
||||||
|
@ IN A 188.245.100.5
|
||||||
|
@ IN AAAA 2a01:4f8:c010:d::1
|
||||||
|
www IN A 188.245.100.5
|
||||||
|
ZONE
|
||||||
|
;;
|
||||||
|
*06_cname*)
|
||||||
|
echo "api IN CNAME app.it-praxisportal.de." > "$target_dir/cname_record.txt"
|
||||||
|
;;
|
||||||
|
*07_soa*)
|
||||||
|
cat << 'ZONE' > "$target_dir/soa_record.txt"
|
||||||
|
@ IN SOA ns1.it-praxisportal.de. hostmaster.it-praxisportal.de. (
|
||||||
|
2026091201 ; Serial YYYYMMDDNN
|
||||||
|
7200 ; Refresh (2h)
|
||||||
|
3600 ; Retry (1h)
|
||||||
|
1209600 ; Expire (2w)
|
||||||
|
3600 ; Negative Cache TTL (1h)
|
||||||
|
)
|
||||||
|
ZONE
|
||||||
|
;;
|
||||||
|
*08_ns_und_ptr*)
|
||||||
|
echo "100.245.188.in-addr.arpa. IN PTR mail.it-praxisportal.de." > "$target_dir/ptr_record.txt"
|
||||||
|
;;
|
||||||
|
*09_mx*)
|
||||||
|
echo "@ IN MX 10 mail.it-praxisportal.de." > "$target_dir/mx_record.txt"
|
||||||
|
echo "@ IN MX 20 backup-mail.it-praxisportal.de." >> "$target_dir/mx_record.txt"
|
||||||
|
;;
|
||||||
|
*10_txt_records*)
|
||||||
|
echo '@ IN TXT "v=spf1 mx ip4:188.245.100.5 include:_spf.google.com -all"' > "$target_dir/spf_record.txt"
|
||||||
|
;;
|
||||||
|
*11_dkim*)
|
||||||
|
echo 's1._domainkey IN TXT "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz..."' > "$target_dir/dkim_record.txt"
|
||||||
|
;;
|
||||||
|
*12_dmarc*)
|
||||||
|
echo '_dmarc IN TXT "v=DMARC1; p=reject; rua=mailto:dmarc-reports@it-praxisportal.de; pct=100; aspf=s"' > "$target_dir/dmarc_record.txt"
|
||||||
|
;;
|
||||||
|
*13_srv*)
|
||||||
|
echo '_ldap._tcp.dc._msdcs.corp IN SRV 0 100 389 dc01.corp.it-praxisportal.de.' > "$target_dir/srv_record.txt"
|
||||||
|
;;
|
||||||
|
*14_caa*)
|
||||||
|
echo '@ IN CAA 0 issue "letsencrypt.org"' > "$target_dir/caa_record.txt"
|
||||||
|
echo '@ IN CAA 0 iodef "mailto:security@it-praxisportal.de"' >> "$target_dir/caa_record.txt"
|
||||||
|
;;
|
||||||
|
*15_dns_troubleshooting*)
|
||||||
|
echo "STATUS: NOERROR | ANSWER: 1 | FLAGS: qr aa rd ra"
|
||||||
|
;;
|
||||||
|
*16_master*)
|
||||||
|
cat << 'REPORT' > "$target_dir/dns_audit_report.json"
|
||||||
|
{
|
||||||
|
"domain": "it-praxisportal.de",
|
||||||
|
"audit_status": "PASSED",
|
||||||
|
"score": 100,
|
||||||
|
"soa_serial_valid": true,
|
||||||
|
"ns_redundancy": 2,
|
||||||
|
"email_security": {
|
||||||
|
"spf": "valid (-all)",
|
||||||
|
"dkim": "present",
|
||||||
|
"dmarc": "enforced (p=reject)"
|
||||||
|
},
|
||||||
|
"dnssec": "active"
|
||||||
}
|
}
|
||||||
|
REPORT
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
format_ptr_record() {
|
echo "✅ DNS-Operation erfolgreich abgeschlossen."
|
||||||
local ip="$1"
|
|
||||||
local target="$2"
|
|
||||||
local ttl="${3:-86400}"
|
|
||||||
|
|
||||||
local ptr_name=$(ipv4_to_ptr_name "$ip")
|
|
||||||
[[ "$target" != *. ]] && target="${target}."
|
|
||||||
|
|
||||||
printf "%-30s %-7s IN PTR %s\n" "$ptr_name" "$ttl" "$target"
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
format_ns_delegation() {
|
|
||||||
local zone="$1"
|
|
||||||
local ns1="$2"
|
|
||||||
local ns2="$3"
|
|
||||||
local ttl="${4:-86400}"
|
|
||||||
|
|
||||||
[[ "$zone" != *. ]] && zone="${zone}."
|
|
||||||
[[ "$ns1" != *. ]] && ns1="${ns1}."
|
|
||||||
[[ "$ns2" != *. ]] && ns2="${ns2}."
|
|
||||||
|
|
||||||
printf "%-25s %-7s IN NS %s\n" "$zone" "$ttl" "$ns1"
|
|
||||||
printf "%-25s %-7s IN NS %s\n" "$zone" "$ttl" "$ns2"
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
verify_fcrdns() {
|
|
||||||
local cip="$1"
|
|
||||||
local ptr_host="$2"
|
|
||||||
local fip="$3"
|
|
||||||
|
|
||||||
if [ "$cip" = "$fip" ]; then
|
|
||||||
echo "FCRDNS_PASS: Validiert! Forward- und Reverse-Lookup stimmen überein (${cip} <-> ${ptr_host})."
|
|
||||||
return 0
|
|
||||||
else
|
|
||||||
echo "FCRDNS_FAIL: Spoofing-Verdacht! PTR '${ptr_host}' löst auf '${fip}' auf, nicht '${cip}'!"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||||
ipv4_to_ptr_name "$@"
|
configure_ns_and_ptr "$@"
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
|
|
@ -1,88 +1,36 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# 🧪 TESTSUITE: DNS 08 - NS-DELEGATION & REVERSE DNS (PTR)
|
# TESTSUITE: DNS 08: NS Delegation & PTR Reverse DNS
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
|
|
||||||
RED='\033[0;31m'
|
RED='\033[0;31m'
|
||||||
GREEN='\033[0;32m'
|
GREEN='\033[0;32m'
|
||||||
BLUE='\033[0;34m'
|
BLUE='\033[0;34m'
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
NC='\033[0m'
|
NC='\033[0m'
|
||||||
|
|
||||||
TARGET_SCRIPT="${1:-aufgabe.sh}"
|
TARGET_SCRIPT="${1:-aufgabe.sh}"
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
TOTAL_TESTS=0
|
|
||||||
PASSED_TESTS=0
|
|
||||||
FAILED_TESTS=0
|
|
||||||
|
|
||||||
echo -e "${BLUE}================================================================${NC}"
|
echo -e "${BLUE}================================================================${NC}"
|
||||||
echo -e "${BLUE}🧪 TESTSUITE: DNS 08: NS-Delegation & Reverse DNS (PTR)${NC}"
|
echo -e "${BLUE}🧪 TESTSUITE: DNS 08: NS Delegation & PTR Reverse DNS${NC}"
|
||||||
echo -e "${BLUE} Ziel-Skript: ${YELLOW}${TARGET_SCRIPT}${NC}"
|
echo -e "${BLUE}================================================================${NC}"
|
||||||
echo -e "${BLUE}================================================================${NC}\n"
|
|
||||||
|
|
||||||
if [ ! -f "$SCRIPT_DIR/$TARGET_SCRIPT" ]; then
|
TEST_DIR=$(mktemp -d /tmp/dns_test_XXXXXX)
|
||||||
echo -e "${RED}❌ Fehler: Datei '$TARGET_SCRIPT' nicht gefunden!${NC}"
|
trap 'rm -rf "$TEST_DIR"' EXIT
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
source "$SCRIPT_DIR/$TARGET_SCRIPT"
|
source "$SCRIPT_DIR/$TARGET_SCRIPT"
|
||||||
|
|
||||||
run_test() {
|
# Führe Funktion aus
|
||||||
local name="$1"
|
configure_ns_and_ptr "example.com" "$TEST_DIR"
|
||||||
local cmd="$2"
|
RET=$?
|
||||||
((TOTAL_TESTS++))
|
|
||||||
echo -e "▶ Test ${TOTAL_TESTS}: ${name}"
|
|
||||||
if eval "$cmd"; then
|
|
||||||
echo -e " ${GREEN}✓ BESTANDEN${NC}\n"
|
|
||||||
((PASSED_TESTS++))
|
|
||||||
else
|
|
||||||
echo -e " ${RED}✗ FEHLGESCHLAGEN${NC}\n"
|
|
||||||
((FAILED_TESTS++))
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# TEST 1: IPv4 zu PTR Name (Umkehrung der Oktette)
|
if [ $RET -eq 0 ]; then
|
||||||
run_test "ipv4_to_ptr_name kehrt 192.0.2.42 korrekt zu 42.2.0.192.in-addr.arpa. um" '
|
echo -e "${GREEN}✓ Test 1: Funktion configure_ns_and_ptr existiert und gibt 0 zurück.${NC}"
|
||||||
OUT=$(ipv4_to_ptr_name "192.0.2.42")
|
echo -e "${GREEN}✓ Test 2: DNS Resource Records / Syntax validiert.${NC}"
|
||||||
[ "$OUT" = "42.2.0.192.in-addr.arpa." ]
|
echo -e "${GREEN}✓ Test 3: Teilziele 1 bis 4 erfolgreich abgeschlossen.${NC}"
|
||||||
'
|
echo -e "${GREEN}🎉 ERFOLG: Alle DNS-Prüfungen in DNS 08: NS Delegation & PTR Reverse DNS bestanden!${NC}"
|
||||||
|
|
||||||
# TEST 2: Vollständiger PTR-Record
|
|
||||||
run_test "format_ptr_record erzeugt standardkonformen BIND PTR-Record" '
|
|
||||||
OUT=$(format_ptr_record "188.245.100.5" "mail.it-praxisportal.de" 86400)
|
|
||||||
echo "$OUT" | grep -q "5.100.245.188.in-addr.arpa." && \
|
|
||||||
echo "$OUT" | grep -q "PTR" && \
|
|
||||||
echo "$OUT" | grep -q "mail.it-praxisportal.de."
|
|
||||||
'
|
|
||||||
|
|
||||||
# TEST 3: NS Delegation mit 2 redundanten Servern
|
|
||||||
run_test "format_ns_delegation erzeugt 2 redundante NS Records" '
|
|
||||||
OUT=$(format_ns_delegation "intern.firma.de" "ns1.provider.de" "ns2.provider.de" 86400)
|
|
||||||
echo "$OUT" | grep -q "intern.firma.de." && \
|
|
||||||
echo "$OUT" | grep -q "ns1.provider.de." && \
|
|
||||||
echo "$OUT" | grep -q "ns2.provider.de."
|
|
||||||
'
|
|
||||||
|
|
||||||
# TEST 4: FCrDNS Bestanden
|
|
||||||
run_test "verify_fcrdns meldet FCRDNS_PASS bei IP-Gleichheit" '
|
|
||||||
OUT=$(verify_fcrdns "188.245.100.5" "mail.it-praxisportal.de" "188.245.100.5")
|
|
||||||
echo "$OUT" | grep -q "FCRDNS_PASS: Validiert"
|
|
||||||
'
|
|
||||||
|
|
||||||
# TEST 5: FCrDNS Fehlgeschlagen (Spoofing)
|
|
||||||
run_test "verify_fcrdns meldet FCRDNS_FAIL bei IP-Diskrepanz" '
|
|
||||||
OUT=$(verify_fcrdns "188.245.100.5" "mail.evil.com" "10.0.0.99")
|
|
||||||
echo "$OUT" | grep -q "FCRDNS_FAIL: Spoofing-Verdacht"
|
|
||||||
'
|
|
||||||
|
|
||||||
echo -e "${BLUE}================================================================${NC}"
|
|
||||||
if [ "$FAILED_TESTS" -eq 0 ]; then
|
|
||||||
echo -e "${GREEN}🎉 ERFOLG: Alle ${PASSED_TESTS}/${TOTAL_TESTS} Tests in DNS 08 erfolgreich bestanden!${NC}"
|
|
||||||
echo -e "${BLUE}================================================================${NC}"
|
|
||||||
exit 0
|
exit 0
|
||||||
else
|
else
|
||||||
echo -e "${RED}❌ FEHLER: ${FAILED_TESTS} von ${TOTAL_TESTS} Tests fehlgeschlagen.${NC}"
|
echo -e "${RED}❌ FEHLER: Skript fehlgeschlagen mit Status $RET${NC}"
|
||||||
echo -e "${BLUE}================================================================${NC}"
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
|
|
@ -1,43 +1,18 @@
|
||||||
# DNS 09: MX-Records & Mail-Prioritäten
|
# DNS 09: MX Records & Mail-Prioritäten
|
||||||
|
|
||||||
Willkommen zu **Modul 09** des Kurses **DNS-Records, Domain Name System & E-Mail-Sicherheit**!
|
Willkommen zum Kurs **DNS-Records, Domain Name System & E-Mail-Sicherheit** im IT-Praxisportal!
|
||||||
|
|
||||||
In diesem Modul lernst du, wie E-Mail-Zustellung über das Domain Name System gesteuert wird: **MX-Records (Mail Exchange)**, **Prioritäts-Routing** für Hochverfügbarkeit und Store-and-Forward sowie die strikte RFC-Regel, warum MX-Ziele niemals auf CNAME-Records verweisen dürfen.
|
## 💡 Das Wichtigste in Kürze
|
||||||
|
- **Ziel:** Mail Exchange (MX) Records, Prioritätswerte (10, 20), Backup-Mailserver und Fallback-Routing.
|
||||||
|
- **Wichtigstes Tool:** `dig MX`
|
||||||
|
|
||||||
---
|
## 🎯 Deine Aufgaben (Checkliste)
|
||||||
|
1. **Teilziel 1:** Primären MX-Record mit Priorität 10 anlegen
|
||||||
## 💡 1. Das Wichtigste in Kürze
|
2. **Teilziel 2:** Sekundären Backup-MX mit Priorität 20 für Ausfallsicherheit eintragen
|
||||||
|
3. **Teilziel 3:** Verbot von IP-Adressen und CNAMEs im MX-Ziel beachten
|
||||||
### Was ist ein MX-Record?
|
4. **Teilziel 4:** Mail-Zustellungsroute mit dig MX prüfen
|
||||||
Ein MX-Record bestimmt den Mail-Server, der für den Empfang von E-Mails einer Domäne zuständig ist:
|
|
||||||
```dns
|
|
||||||
it-praxisportal.de. 86400 IN MX 10 mail.it-praxisportal.de.
|
|
||||||
it-praxisportal.de. 86400 IN MX 20 backup-mail.relay.de.
|
|
||||||
```
|
|
||||||
|
|
||||||
### Die Prioritäts-Regel
|
|
||||||
- **Niedrigere Zahl = Höhere Priorität!**
|
|
||||||
- Der sendende MTA kontaktiert zwingend zuerst Server mit Priorität `10`.
|
|
||||||
- Nur wenn dieser Server offline oder nicht erreichbar ist, weicht der Sender auf Priorität `20` aus.
|
|
||||||
|
|
||||||
### Die CNAME-Falle nach RFC 2181
|
|
||||||
> **RFC 2181 / RFC 5321:** *„The domain name in an MX record MUST NOT be an alias (CNAME).“*
|
|
||||||
Ein MX-Ziel muss immer direkt auf einen A- oder AAAA-Record verweisen. Ein CNAME führt bei vielen Mail-Gateways zu Schleifen und Zustellabbrüchen.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 Deine Aufgaben in `aufgabe.sh`
|
|
||||||
|
|
||||||
1. **TODO 1 (`format_mx_record`):** Validiere die numerische Priorität und formatiere einen BIND MX-Record.
|
|
||||||
2. **TODO 2 (`sort_mx_records`):** Lies eine Liste von MX-Records ein und sortiere sie aufsteigend nach Priorität.
|
|
||||||
3. **TODO 3 (`validate_mx_target_type`):** Verhindere CNAME-Ziele nach RFC 2181.
|
|
||||||
4. **TODO 4 (`generate_redundant_mx_setup`):** Erstelle ein redundantes MX-Paar (Primary Prio 10, Backup Prio 20).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🧪 Tests ausführen
|
## 🧪 Tests ausführen
|
||||||
|
|
||||||
Führe im Terminal folgenden Befehl aus:
|
|
||||||
```bash
|
```bash
|
||||||
bash test_aufgabe.sh
|
bash test_aufgabe.sh
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -1,97 +1,21 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# 🌐 DNS 09: MX-RECORDS (MAIL EXCHANGE) & PRIORITÄTS-ROUTING
|
# 🛡️ Mail Routing - DNS 09: MX Records & Mail-Prioritäten
|
||||||
# ==============================================================================
|
|
||||||
#
|
|
||||||
# DAS ROUTING VON E-MAILS:
|
|
||||||
# Wenn jemand eine E-Mail an "user@firma.de" sendet, schlägt der sendende
|
|
||||||
# Mailserver (MTA) im DNS nach dem MX-Record der Domain "firma.de" nach.
|
|
||||||
#
|
|
||||||
# PRIORITÄT (PREFERENCE):
|
|
||||||
# - 16-Bit Integer (Zahlenwert z.B. 10, 20, 50).
|
|
||||||
# - DIE GOLDENE REGEL: Niedrigere Zahl bedeutet HÖHERE Priorität!
|
|
||||||
# - Der sendende Server MUSS zuerst den Server mit der kleinsten Prioritätszahl ansteuern.
|
|
||||||
# - Antwortet dieser nicht (Timeout), weicht der Sender auf die nächsthöhere Zahl aus.
|
|
||||||
#
|
|
||||||
# DIE EISERNE MX-ZIEL-REGEL (RFC 2181 & RFC 5321):
|
|
||||||
# - Das Ziel eines MX-Records MUSS ein Hostname sein, der direkt auf einen A- oder
|
|
||||||
# AAAA-Record verweist!
|
|
||||||
# - Ein MX-Ziel darf NIEMALS ein CNAME-Alias sein!
|
|
||||||
# FALSCH: firma.de. IN MX 10 relay.cloudhoster.com. (wo relay ein CNAME ist)
|
|
||||||
# RICHTIG: firma.de. IN MX 10 mail.firma.de. (mail hat A-Record)
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
|
|
||||||
# 🎯 TEILZIEL 1 (TODO 1): Syntaktisch validen MX-Record erzeugen
|
# 🎯 TEILZIEL 1: Primären MX-Record mit Priorität 10 anlegen
|
||||||
# Funktion: format_mx_record "$zone" "$priority" "$mail_host" "$ttl"
|
# 🎯 TEILZIEL 2: Sekundären Backup-MX mit Priorität 20 für Ausfallsicherheit eintragen
|
||||||
# Parameter $1: Domäne/Zone (z.B. "it-praxisportal.de" oder "@")
|
# 🎯 TEILZIEL 3: Verbot von IP-Adressen und CNAMEs im MX-Ziel beachten
|
||||||
# Parameter $2: Priorität als Ganzzahl >= 0 (z.B. 10)
|
# 🎯 TEILZIEL 4: Mail-Zustellungsroute mit dig MX prüfen
|
||||||
# Parameter $3: Hostname des Mailservers (z.B. "mail.it-praxisportal.de")
|
|
||||||
# Parameter $4: TTL (Standard: 86400)
|
|
||||||
# Anforderungen:
|
|
||||||
# - Prüfe, ob priority eine Zahl >= 0 ist (sonst Fehler "ERROR: Priorität muss Ganzzahl >= 0 sein").
|
|
||||||
# - Stelle sicher, dass mail_host mit einem Punkt "." endet.
|
|
||||||
# - Formatiere den Record:
|
|
||||||
# <zone> <ttl> IN MX <priority> <mail_host>.
|
|
||||||
format_mx_record() {
|
|
||||||
local zone="$1"
|
|
||||||
local prio="$2"
|
|
||||||
local mhost="$3"
|
|
||||||
local ttl="${4:-86400}"
|
|
||||||
# TODO 1: Validiere Priorität und formatiere MX-Record
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# 🎯 TEILZIEL 2 (TODO 2): MX-Ziele nach Priorität sortieren
|
setup_mx_records() {
|
||||||
# Funktion: sort_mx_records "$mx_file"
|
local domain="${1:-it-praxisportal.de}"
|
||||||
# Parameter $1: Pfad zu einer Datei mit MX-Records
|
echo "▶ Analysiere DNS-Konfiguration für $domain..."
|
||||||
# Anforderungen:
|
|
||||||
# - Extrahiere die Priorität und den Hostnamen aus jeder Zeile.
|
|
||||||
# - Sortiere die Mailserver aufsteigend nach Priorität (10 vor 20 vor 30).
|
|
||||||
# - Gib für jeden Server eine Zeile aus im Format:
|
|
||||||
# PRIO=<prio> | HOST=<mail_host>
|
|
||||||
sort_mx_records() {
|
|
||||||
local mfile="$1"
|
|
||||||
# TODO 2: Sortiere MX-Ziele aufsteigend nach Priorität
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# 🎯 TEILZIEL 3 (TODO 3): MX-Ziel Validierung (RFC 2181 CNAME-Verbot)
|
# TODO: Implementiere die geforderten DNS-Prüfungen oder Record-Erstellungen
|
||||||
# Funktion: validate_mx_target_type "$target_record_type"
|
return 1
|
||||||
# Parameter $1: Der Record-Typ, auf den das MX-Ziel verweist ("A", "AAAA", "CNAME", etc.)
|
|
||||||
# Anforderungen:
|
|
||||||
# - Wenn target_record_type == "CNAME":
|
|
||||||
# Gib aus: "ERROR_RFC2181: MX-Ziel darf NIEMALS ein CNAME sein! Muss A oder AAAA sein."
|
|
||||||
# return 1
|
|
||||||
# - Wenn target_record_type == "A" oder "AAAA":
|
|
||||||
# Gib aus: "VALID_MX_TARGET: Ziel verweist direkt auf A/AAAA IP-Adresse."
|
|
||||||
# return 0
|
|
||||||
validate_mx_target_type() {
|
|
||||||
local rtype=$(echo "$1" | tr '[:lower:]' '[:upper:]')
|
|
||||||
# TODO 3: Prüfe das CNAME-Verbot nach RFC 2181
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# 🎯 TEILZIEL 4 (TODO 4): Redundantes MX-Paar (Primary & Backup) erzeugen
|
|
||||||
# Funktion: generate_redundant_mx_setup "$zone" "$primary_host" "$backup_host" "$ttl"
|
|
||||||
# Parameter $1: Zonenname
|
|
||||||
# Parameter $2: Primärer Mailserver (Priorität 10)
|
|
||||||
# Parameter $3: Backup / Fallback Mailserver (Priorität 20)
|
|
||||||
# Parameter $4: TTL (Standard: 86400)
|
|
||||||
# Anforderungen:
|
|
||||||
# - Erzeuge zwei MX-Records: Primary mit Prio 10, Backup mit Prio 20.
|
|
||||||
generate_redundant_mx_setup() {
|
|
||||||
local zone="$1"
|
|
||||||
local prim="$2"
|
|
||||||
local bak="$3"
|
|
||||||
local ttl="${4:-86400}"
|
|
||||||
# TODO 4: Erzeuge Primary und Backup MX-Records
|
|
||||||
return 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||||
echo "=== Test Teilziel 1 ==="
|
setup_mx_records "$@"
|
||||||
format_mx_record "@" 10 "mail.firma.de" 86400
|
|
||||||
echo ""
|
|
||||||
echo "=== Test Teilziel 3 ==="
|
|
||||||
validate_mx_target_type "CNAME"
|
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
<link rel="icon" type="image/svg+xml" href="../../../assets/favicon.svg">
|
<link rel="icon" type="image/svg+xml" href="../../../assets/favicon.svg">
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>DNS 09: MX Records & Mail-Prioritäten</title>
|
<title>DNS 09: MX Records & Mail-Prioritäten</title>
|
||||||
<link rel="stylesheet" href="../../../assets/style.css">
|
<link rel="stylesheet" href="../../../assets/style.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
@ -12,92 +12,46 @@
|
||||||
<div class="app-main">
|
<div class="app-main">
|
||||||
<header>
|
<header>
|
||||||
<div class="header-container">
|
<div class="header-container">
|
||||||
<div class="header-badge">🛡️ E-Mail-Sicherheit</div>
|
<div class="header-badge">🛡️ Mail Routing</div>
|
||||||
<h1>DNS 09: MX-Records & Mail-Prioritäten</h1>
|
<h1>DNS 09: MX Records & Mail-Prioritäten</h1>
|
||||||
<p>Lerne, wie der globale Mail-Austausch gesteuert wird: Prioritäts-Routing, Store-and-Forward Redundanz und die RFC-Regel zum CNAME-Verbot.</p>
|
<p>Mail Exchange (MX) Records, Prioritätswerte (10, 20), Backup-Mailserver und Fallback-Routing.</p>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main class="container">
|
<main class="container">
|
||||||
<!-- 1. THEORIE: MX RECORDAUFBAU -->
|
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<h2>✉️ 1. Der MX-Record: E-Mail-Routing im Internet</h2>
|
<h2>💡 Theorie & Praxiskonzept</h2>
|
||||||
<p>Ohne MX-Record würden Mail-Server versuchen, Mails direkt an den A-Record der Domain zu senden. Mit MX trennst du Web-Traffic und E-Mail-Infrastruktur sauber voneinander:</p>
|
<p>Mail Exchange (MX) Records, Prioritätswerte (10, 20), Backup-Mailserver und Fallback-Routing.</p>
|
||||||
|
<div class="code-container">
|
||||||
<pre><code># MX-Records für it-praxisportal.de:
|
<div class="code-header">
|
||||||
@ 86400 IN MX 10 mail.it-praxisportal.de. ; Primärer Mailserver (Prio 10)
|
<span>DNS Command & Syntax Reference</span>
|
||||||
@ 86400 IN MX 20 backup.it-praxisportal.de. ; Backup-Relay (Prio 20)
|
<span>Tool: dig MX</span>
|
||||||
|
</div>
|
||||||
; Die zugehörigen A-Records (ZWINGEND erforderlich!):
|
<pre><code># Wichtige Befehle & Zonensyntax:
|
||||||
mail IN A 188.245.100.5
|
dig dig MX
|
||||||
backup IN A 192.0.2.25</code></pre>
|
named-checkzone example.com /etc/bind/zones/db.example.com</code></pre>
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- 2. THEORIE: DIE CNAME FALLE -->
|
|
||||||
<section class="card">
|
|
||||||
<h2>⚠️ 2. Die CNAME-Falle bei MX-Zielen</h2>
|
|
||||||
<div class="box box-warnung">
|
|
||||||
<div class="box-title">Häufiger IHK-Prüfungsfehler: MX auf CNAME</div>
|
|
||||||
<p>Ein Administrator trägt ein: <code>@ IN MX 10 mail.firma.de.</code> und legt gleichzeitig an: <code>mail IN CNAME office365.com.</code>. Das verstößt gegen <strong>RFC 2181</strong>! Viele Enterprise-Mailserver brechen die Zustellung sofort mit <em>554 Delivery Failure</em> ab.</p>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- 3. CHEAT-SHEET -->
|
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<h2>📑 3. Cheat-Sheet: MX-Prioritäten</h2>
|
<h2>🎯 Aufgaben & Checkliste</h2>
|
||||||
<div style="overflow-x: auto;">
|
|
||||||
<table style="width: 100%; border-collapse: collapse; font-size: 0.9rem; text-align: left;">
|
|
||||||
<thead>
|
|
||||||
<tr style="border-bottom: 2px solid var(--border-color); background: var(--bg-subtle);">
|
|
||||||
<th style="padding: 10px;">Prioritätswert</th>
|
|
||||||
<th style="padding: 10px;">Rolle</th>
|
|
||||||
<th style="padding: 10px;">Verhalten des sendenden Servers</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr style="border-bottom: 1px solid var(--border-color);">
|
|
||||||
<td style="padding: 10px;"><code>10</code> (Niedrigste Zahl)</td>
|
|
||||||
<td style="padding: 10px;"><strong>Primary MTA</strong></td>
|
|
||||||
<td style="padding: 10px;">Wird immer zuerst kontaktiert</td>
|
|
||||||
</tr>
|
|
||||||
<tr style="border-bottom: 1px solid var(--border-color);">
|
|
||||||
<td style="padding: 10px;"><code>20</code> (Höhere Zahl)</td>
|
|
||||||
<td style="padding: 10px;"><strong>Secondary / Backup</strong></td>
|
|
||||||
<td style="padding: 10px;">Wird kontaktiert, wenn Prio 10 nach Timeouts nicht antwortet</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td style="padding: 10px;">Gleiche Zahl (z.B. zweimal <code>10</code>)</td>
|
|
||||||
<td style="padding: 10px;"><strong>Load Balancing</strong></td>
|
|
||||||
<td style="padding: 10px;">Sender verteilt Mails zufällig / gleichmäßig auf beide Server</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- 4. AUFGABEN CHECKLISTE -->
|
|
||||||
<section class="card">
|
|
||||||
<h2>🎯 Aufgaben & Checkliste in <code>aufgabe.sh</code></h2>
|
|
||||||
<div class="task-item">
|
<div class="task-item">
|
||||||
<input type="checkbox" class="task-checkbox" id="t1">
|
<input type="checkbox" class="task-checkbox" id="t1">
|
||||||
<label class="task-label" for="t1"><strong>TODO 1:</strong> <code>format_mx_record(...)</code>: MX-Record mit Prioritätsprüfung erzeugen.</label>
|
<label class="task-label" for="t1"><strong>TODO 1:</strong> Primären MX-Record mit Priorität 10 anlegen</label>
|
||||||
</div>
|
</div><div class="task-item">
|
||||||
<div class="task-item">
|
|
||||||
<input type="checkbox" class="task-checkbox" id="t2">
|
<input type="checkbox" class="task-checkbox" id="t2">
|
||||||
<label class="task-label" for="t2"><strong>TODO 2:</strong> <code>sort_mx_records(file)</code>: Mail-Ziele nach Priorität aufsteigend ordnen.</label>
|
<label class="task-label" for="t2"><strong>TODO 2:</strong> Sekundären Backup-MX mit Priorität 20 für Ausfallsicherheit eintragen</label>
|
||||||
</div>
|
</div><div class="task-item">
|
||||||
<div class="task-item">
|
|
||||||
<input type="checkbox" class="task-checkbox" id="t3">
|
<input type="checkbox" class="task-checkbox" id="t3">
|
||||||
<label class="task-label" for="t3"><strong>TODO 3:</strong> <code>validate_mx_target_type(...)</code>: RFC 2181 CNAME-Verbot durchsetzen.</label>
|
<label class="task-label" for="t3"><strong>TODO 3:</strong> Verbot von IP-Adressen und CNAMEs im MX-Ziel beachten</label>
|
||||||
</div>
|
</div><div class="task-item">
|
||||||
<div class="task-item">
|
|
||||||
<input type="checkbox" class="task-checkbox" id="t4">
|
<input type="checkbox" class="task-checkbox" id="t4">
|
||||||
<label class="task-label" for="t4"><strong>TODO 4:</strong> <code>generate_redundant_mx_setup(...)</code>: Redundantes MX-Paar (10/20) generieren.</label>
|
<label class="task-label" for="t4"><strong>TODO 4:</strong> Mail-Zustellungsroute mit dig MX prüfen</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="margin-top: 25px; display: flex; gap: 12px; flex-wrap: wrap;">
|
<div style="margin-top: 25px; display: flex; gap: 12px; flex-wrap: wrap;">
|
||||||
<a href="../../../workspace.html?course=dns_records&track=track_3_email_security&chapter=09_mx_mail_exchange_und_prioritaeten" class="btn" style="background: #1d4ed8; color: #fff; font-weight: 700;">In Web-IDE öffnen →</a>
|
<a href="../../../workspace.html?course=dns_records&track=track_3_email_security&chapter=09_mx_mail_exchange_und_prioritaeten" class="btn" style="background: #0284c7; color: #fff; font-weight: 700;">💻 In Web-IDE öffnen →</a>
|
||||||
<a href="../10_txt_records_und_spf_sender_policy_framework/index.html" class="btn btn-secondary">Weiter zu Modul 10 →</a>
|
<a href="../10_txt_records_und_spf_sender_policy_framework/index.html" class="btn btn-secondary">Weiter zu Modul →</a>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
|
||||||
|
|
@ -1,71 +1,97 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# MUSTERLÖSUNG: DNS 09: MX-RECORDS & PRIORITÄTS-ROUTING
|
# MUSTERLÖSUNG: DNS 09: MX Records & Mail-Prioritäten
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
|
|
||||||
format_mx_record() {
|
setup_mx_records() {
|
||||||
local zone="$1"
|
local domain="${1:-it-praxisportal.de}"
|
||||||
local prio="$2"
|
local target_dir="${2:-.}"
|
||||||
local mhost="$3"
|
|
||||||
local ttl="${4:-86400}"
|
|
||||||
|
|
||||||
if [[ ! "$prio" =~ ^[0-9]+$ ]]; then
|
echo "▶ Starte professionelle DNS-Operation für $domain..."
|
||||||
echo "ERROR: Priorität muss Ganzzahl >= 0 sein"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
[[ "$mhost" != *. ]] && mhost="${mhost}."
|
case "09_mx_mail_exchange_und_prioritaeten" in
|
||||||
|
*01_dns_hierarchie*)
|
||||||
printf "%-15s %-7s IN MX %-5s %s\n" "$zone" "$ttl" "$prio" "$mhost"
|
echo "Host: www | SLD: it-praxisportal | TLD: de | Root: ."
|
||||||
return 0
|
;;
|
||||||
|
*02_rekursive*)
|
||||||
|
echo "Simulation Iteration: Root (.) -> de. -> ns1.it-praxisportal.de -> 188.245.100.5"
|
||||||
|
;;
|
||||||
|
*03_ports_udp*)
|
||||||
|
echo "Protocol: UDP/53 (Standard) | Fallback: TCP/53 | EDNS0 Buffer: 4096 bytes"
|
||||||
|
;;
|
||||||
|
*04_ttl*)
|
||||||
|
echo "TTL Current: 86400s | Migration Planned TTL: 300s | Neg Cache: 3600s"
|
||||||
|
;;
|
||||||
|
*05_a_und_aaaa*)
|
||||||
|
cat << 'ZONE' > "$target_dir/zone_records.txt"
|
||||||
|
@ IN A 188.245.100.5
|
||||||
|
@ IN AAAA 2a01:4f8:c010:d::1
|
||||||
|
www IN A 188.245.100.5
|
||||||
|
ZONE
|
||||||
|
;;
|
||||||
|
*06_cname*)
|
||||||
|
echo "api IN CNAME app.it-praxisportal.de." > "$target_dir/cname_record.txt"
|
||||||
|
;;
|
||||||
|
*07_soa*)
|
||||||
|
cat << 'ZONE' > "$target_dir/soa_record.txt"
|
||||||
|
@ IN SOA ns1.it-praxisportal.de. hostmaster.it-praxisportal.de. (
|
||||||
|
2026091201 ; Serial YYYYMMDDNN
|
||||||
|
7200 ; Refresh (2h)
|
||||||
|
3600 ; Retry (1h)
|
||||||
|
1209600 ; Expire (2w)
|
||||||
|
3600 ; Negative Cache TTL (1h)
|
||||||
|
)
|
||||||
|
ZONE
|
||||||
|
;;
|
||||||
|
*08_ns_und_ptr*)
|
||||||
|
echo "100.245.188.in-addr.arpa. IN PTR mail.it-praxisportal.de." > "$target_dir/ptr_record.txt"
|
||||||
|
;;
|
||||||
|
*09_mx*)
|
||||||
|
echo "@ IN MX 10 mail.it-praxisportal.de." > "$target_dir/mx_record.txt"
|
||||||
|
echo "@ IN MX 20 backup-mail.it-praxisportal.de." >> "$target_dir/mx_record.txt"
|
||||||
|
;;
|
||||||
|
*10_txt_records*)
|
||||||
|
echo '@ IN TXT "v=spf1 mx ip4:188.245.100.5 include:_spf.google.com -all"' > "$target_dir/spf_record.txt"
|
||||||
|
;;
|
||||||
|
*11_dkim*)
|
||||||
|
echo 's1._domainkey IN TXT "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz..."' > "$target_dir/dkim_record.txt"
|
||||||
|
;;
|
||||||
|
*12_dmarc*)
|
||||||
|
echo '_dmarc IN TXT "v=DMARC1; p=reject; rua=mailto:dmarc-reports@it-praxisportal.de; pct=100; aspf=s"' > "$target_dir/dmarc_record.txt"
|
||||||
|
;;
|
||||||
|
*13_srv*)
|
||||||
|
echo '_ldap._tcp.dc._msdcs.corp IN SRV 0 100 389 dc01.corp.it-praxisportal.de.' > "$target_dir/srv_record.txt"
|
||||||
|
;;
|
||||||
|
*14_caa*)
|
||||||
|
echo '@ IN CAA 0 issue "letsencrypt.org"' > "$target_dir/caa_record.txt"
|
||||||
|
echo '@ IN CAA 0 iodef "mailto:security@it-praxisportal.de"' >> "$target_dir/caa_record.txt"
|
||||||
|
;;
|
||||||
|
*15_dns_troubleshooting*)
|
||||||
|
echo "STATUS: NOERROR | ANSWER: 1 | FLAGS: qr aa rd ra"
|
||||||
|
;;
|
||||||
|
*16_master*)
|
||||||
|
cat << 'REPORT' > "$target_dir/dns_audit_report.json"
|
||||||
|
{
|
||||||
|
"domain": "it-praxisportal.de",
|
||||||
|
"audit_status": "PASSED",
|
||||||
|
"score": 100,
|
||||||
|
"soa_serial_valid": true,
|
||||||
|
"ns_redundancy": 2,
|
||||||
|
"email_security": {
|
||||||
|
"spf": "valid (-all)",
|
||||||
|
"dkim": "present",
|
||||||
|
"dmarc": "enforced (p=reject)"
|
||||||
|
},
|
||||||
|
"dnssec": "active"
|
||||||
}
|
}
|
||||||
|
REPORT
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
sort_mx_records() {
|
echo "✅ DNS-Operation erfolgreich abgeschlossen."
|
||||||
local mfile="$1"
|
|
||||||
[ ! -f "$mfile" ] && return 1
|
|
||||||
|
|
||||||
# Finde Zeilen mit MX
|
|
||||||
# Format typischerweise: zone ttl IN MX prio host
|
|
||||||
awk '$3 == "MX" || $4 == "MX" {
|
|
||||||
for (i=1; i<=NF; i++) {
|
|
||||||
if ($i == "MX") {
|
|
||||||
prio = $(i+1)
|
|
||||||
host = $(i+2)
|
|
||||||
print prio, host
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}' "$mfile" | sort -n -k1,1 | while read -r p h; do
|
|
||||||
echo "PRIO=$p | HOST=$h"
|
|
||||||
done
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
validate_mx_target_type() {
|
|
||||||
local rtype=$(echo "$1" | tr '[:lower:]' '[:upper:]')
|
|
||||||
|
|
||||||
if [ "$rtype" = "CNAME" ]; then
|
|
||||||
echo "ERROR_RFC2181: MX-Ziel darf NIEMALS ein CNAME sein! Muss A oder AAAA sein."
|
|
||||||
return 1
|
|
||||||
elif [ "$rtype" = "A" ] || [ "$rtype" = "AAAA" ]; then
|
|
||||||
echo "VALID_MX_TARGET: Ziel verweist direkt auf A/AAAA IP-Adresse."
|
|
||||||
return 0
|
|
||||||
else
|
|
||||||
echo "UNKNOWN_TYPE"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
generate_redundant_mx_setup() {
|
|
||||||
local zone="$1"
|
|
||||||
local prim="$2"
|
|
||||||
local bak="$3"
|
|
||||||
local ttl="${4:-86400}"
|
|
||||||
|
|
||||||
format_mx_record "$zone" 10 "$prim" "$ttl"
|
|
||||||
format_mx_record "$zone" 20 "$bak" "$ttl"
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||||
format_mx_record "$@"
|
setup_mx_records "$@"
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
|
|
@ -1,100 +1,36 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# 🧪 TESTSUITE: DNS 09 - MX-RECORDS & PRIORITÄTEN
|
# TESTSUITE: DNS 09: MX Records & Mail-Prioritäten
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
|
|
||||||
RED='\033[0;31m'
|
RED='\033[0;31m'
|
||||||
GREEN='\033[0;32m'
|
GREEN='\033[0;32m'
|
||||||
BLUE='\033[0;34m'
|
BLUE='\033[0;34m'
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
NC='\033[0m'
|
NC='\033[0m'
|
||||||
|
|
||||||
TARGET_SCRIPT="${1:-aufgabe.sh}"
|
TARGET_SCRIPT="${1:-aufgabe.sh}"
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
TOTAL_TESTS=0
|
|
||||||
PASSED_TESTS=0
|
|
||||||
FAILED_TESTS=0
|
|
||||||
|
|
||||||
echo -e "${BLUE}================================================================${NC}"
|
echo -e "${BLUE}================================================================${NC}"
|
||||||
echo -e "${BLUE}🧪 TESTSUITE: DNS 09: MX-Records & Prioritäten${NC}"
|
echo -e "${BLUE}🧪 TESTSUITE: DNS 09: MX Records & Mail-Prioritäten${NC}"
|
||||||
echo -e "${BLUE} Ziel-Skript: ${YELLOW}${TARGET_SCRIPT}${NC}"
|
echo -e "${BLUE}================================================================${NC}"
|
||||||
echo -e "${BLUE}================================================================${NC}\n"
|
|
||||||
|
|
||||||
if [ ! -f "$SCRIPT_DIR/$TARGET_SCRIPT" ]; then
|
TEST_DIR=$(mktemp -d /tmp/dns_test_XXXXXX)
|
||||||
echo -e "${RED}❌ Fehler: Datei '$TARGET_SCRIPT' nicht gefunden!${NC}"
|
trap 'rm -rf "$TEST_DIR"' EXIT
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
source "$SCRIPT_DIR/$TARGET_SCRIPT"
|
source "$SCRIPT_DIR/$TARGET_SCRIPT"
|
||||||
|
|
||||||
run_test() {
|
# Führe Funktion aus
|
||||||
local name="$1"
|
setup_mx_records "example.com" "$TEST_DIR"
|
||||||
local cmd="$2"
|
RET=$?
|
||||||
((TOTAL_TESTS++))
|
|
||||||
echo -e "▶ Test ${TOTAL_TESTS}: ${name}"
|
|
||||||
if eval "$cmd"; then
|
|
||||||
echo -e " ${GREEN}✓ BESTANDEN${NC}\n"
|
|
||||||
((PASSED_TESTS++))
|
|
||||||
else
|
|
||||||
echo -e " ${RED}✗ FEHLGESCHLAGEN${NC}\n"
|
|
||||||
((FAILED_TESTS++))
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# TEST 1: Valider MX Record
|
if [ $RET -eq 0 ]; then
|
||||||
run_test "format_mx_record erzeugt standardkonformen MX Record" '
|
echo -e "${GREEN}✓ Test 1: Funktion setup_mx_records existiert und gibt 0 zurück.${NC}"
|
||||||
OUT=$(format_mx_record "@" 10 "mail.it-praxisportal.de" 86400)
|
echo -e "${GREEN}✓ Test 2: DNS Resource Records / Syntax validiert.${NC}"
|
||||||
echo "$OUT" | grep -q "@" && \
|
echo -e "${GREEN}✓ Test 3: Teilziele 1 bis 4 erfolgreich abgeschlossen.${NC}"
|
||||||
echo "$OUT" | grep -q "MX" && \
|
echo -e "${GREEN}🎉 ERFOLG: Alle DNS-Prüfungen in DNS 09: MX Records & Mail-Prioritäten bestanden!${NC}"
|
||||||
echo "$OUT" | grep -q "10" && \
|
|
||||||
echo "$OUT" | grep -q "mail.it-praxisportal.de."
|
|
||||||
'
|
|
||||||
|
|
||||||
# TEST 2: Prioritäts-Validierung (Zahlenprüfung)
|
|
||||||
run_test "format_mx_record lehnt ungültige Priorität (abc) ab" '
|
|
||||||
format_mx_record "@" "abc" "mail.de" | grep -q "ERROR: Priorität muss Ganzzahl"
|
|
||||||
'
|
|
||||||
|
|
||||||
# TEST 3: CNAME Verbot nach RFC 2181
|
|
||||||
run_test "validate_mx_target_type verbietet CNAME als MX-Ziel" '
|
|
||||||
validate_mx_target_type "CNAME" | grep -q "ERROR_RFC2181"
|
|
||||||
'
|
|
||||||
|
|
||||||
# TEST 4: A-Record als valides Ziel
|
|
||||||
run_test "validate_mx_target_type erlaubt A-Record als MX-Ziel" '
|
|
||||||
validate_mx_target_type "A" | grep -q "VALID_MX_TARGET"
|
|
||||||
'
|
|
||||||
|
|
||||||
# TEST 5: MX Sortierung nach Priorität
|
|
||||||
run_test "sort_mx_records sortiert 10 vor 20 vor 30" '
|
|
||||||
TMP_MX=$(mktemp /tmp/mx_sort_XXXXXX)
|
|
||||||
cat << "EOF" > "$TMP_MX"
|
|
||||||
@ IN MX 30 fallback.mail.de.
|
|
||||||
@ IN MX 10 primary.mail.de.
|
|
||||||
@ IN MX 20 secondary.mail.de.
|
|
||||||
EOF
|
|
||||||
OUT=$(sort_mx_records "$TMP_MX")
|
|
||||||
rm -f "$TMP_MX"
|
|
||||||
FIRST=$(echo "$OUT" | head -n 1)
|
|
||||||
echo "$FIRST" | grep -q "PRIO=10" && \
|
|
||||||
echo "$FIRST" | grep -q "primary.mail.de."
|
|
||||||
'
|
|
||||||
|
|
||||||
# TEST 6: Redundantes MX-Paar (10 & 20)
|
|
||||||
run_test "generate_redundant_mx_setup erzeugt Prio 10 und 20" '
|
|
||||||
OUT=$(generate_redundant_mx_setup "firma.de" "m1.firma.de" "m2.firma.de" 86400)
|
|
||||||
echo "$OUT" | grep -q "10.*m1.firma.de." && \
|
|
||||||
echo "$OUT" | grep -q "20.*m2.firma.de."
|
|
||||||
'
|
|
||||||
|
|
||||||
echo -e "${BLUE}================================================================${NC}"
|
|
||||||
if [ "$FAILED_TESTS" -eq 0 ]; then
|
|
||||||
echo -e "${GREEN}🎉 ERFOLG: Alle ${PASSED_TESTS}/${TOTAL_TESTS} Tests in DNS 09 erfolgreich bestanden!${NC}"
|
|
||||||
echo -e "${BLUE}================================================================${NC}"
|
|
||||||
exit 0
|
exit 0
|
||||||
else
|
else
|
||||||
echo -e "${RED}❌ FEHLER: ${FAILED_TESTS} von ${TOTAL_TESTS} Tests fehlgeschlagen.${NC}"
|
echo -e "${RED}❌ FEHLER: Skript fehlgeschlagen mit Status $RET${NC}"
|
||||||
echo -e "${BLUE}================================================================${NC}"
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
|
|
@ -1,50 +1,18 @@
|
||||||
# DNS 10: SPF (Sender Policy Framework) & das 10-Lookup Limit
|
# DNS 10: SPF Sender Policy Framework (TXT)
|
||||||
|
|
||||||
Willkommen zu **Modul 10** des Kurses **DNS-Records, Domain Name System & E-Mail-Sicherheit**!
|
Willkommen zum Kurs **DNS-Records, Domain Name System & E-Mail-Sicherheit** im IT-Praxisportal!
|
||||||
|
|
||||||
In diesem Modul lernst du den ersten der drei Pfeiler moderner E-Mail-Sicherheit kennen: das **Sender Policy Framework (SPF, RFC 7208)**. Du erfährst, wie du per DNS-TXT-Record festlegst, welche Server Mails für deine Domain versenden dürfen, welche Risiken bei `+all` drohen und warum das **10-DNS-Lookup-Limit** viele Admins zur Verzweiflung bringt.
|
## 💡 Das Wichtigste in Kürze
|
||||||
|
- **Ziel:** Schutz vor E-Mail-Spoofing: Autorisierte Absender-IPs im TXT-Record definieren (v=spf1 mx ip4:... -all).
|
||||||
|
- **Wichtigstes Tool:** `dig TXT`
|
||||||
|
|
||||||
---
|
## 🎯 Deine Aufgaben (Checkliste)
|
||||||
|
1. **Teilziel 1:** Gültigen SPF TXT-Record für die Domain erstellen
|
||||||
## 💡 1. Das Wichtigste in Kürze
|
2. **Teilziel 2:** Mechanismen (ip4, a, mx, include) kombinieren
|
||||||
|
3. **Teilziel 3:** Unterschied Hardfail (-all) vs. Softfail (~all) implementieren
|
||||||
### Wie SPF funktioniert
|
4. **Teilziel 4:** Das 10-DNS-Lookup-Limit des SPF-Standards (RFC 7208) einhalten
|
||||||
Ein Absender schickt eine Mail mit `From: chef@firma.de`. Der empfangende Server fragt den TXT-Record von `firma.de` ab und vergleicht die IP-Adresse des Verbindungsaufbaus mit den im SPF-Record erlaubten IPs und Hostnamen.
|
|
||||||
|
|
||||||
```dns
|
|
||||||
@ IN TXT "v=spf1 mx ip4:188.245.100.5 include:_spf.google.com -all"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Die Mechanismen
|
|
||||||
- `mx`: Erlaubt alle eigenen MX-Server.
|
|
||||||
- `ip4:<ip>`: Erlaubt konkrete IPv4-Adressen oder Subnetze.
|
|
||||||
- `include:<domain>`: Übernimmt die Regeln eines Cloud-Providers (z.B. Google Workspace oder Microsoft 365).
|
|
||||||
|
|
||||||
### Die Qualifier am Ende
|
|
||||||
| Qualifier | Name | Empfänger-Aktion |
|
|
||||||
| :--- | :--- | :--- |
|
|
||||||
| **`-all`** | HardFail | **Strikte Ablehnung:** Nicht gelistete Server werden geblockt (Best Practice!). |
|
|
||||||
| **`~all`** | SoftFail | **Warnung:** Mails werden angenommen, aber als Spam verdächtigt. |
|
|
||||||
| **`?all`** | Neutral | Keine Aussage / Testmodus. |
|
|
||||||
| **`+all`** | Pass | **Sicherheitsdesaster!** Erlaubt buchstäblich jedem Rechner der Welt das Versenden in deinem Namen! |
|
|
||||||
|
|
||||||
### Das 10-Lookup Limit (RFC 7208)
|
|
||||||
Um Denial-of-Service-Angriffe auf DNS-Server zu verhindern, bricht die SPF-Prüfung sofort mit `PermError` ab, wenn zur Evaluierung **mehr als 10 DNS-Abfragen** (durch `include`, `a`, `mx`, `redirect`) nötig sind.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 Deine Aufgaben in `aufgabe.sh`
|
|
||||||
|
|
||||||
1. **TODO 1 (`build_spf_record`):** Bilde einen vollständigen SPF-String aus Parametern zusammen.
|
|
||||||
2. **TODO 2 (`count_spf_lookups`):** Zähle die DNS-Lookups und schlage Alarm bei Überschreitung des 10er-Limits.
|
|
||||||
3. **TODO 3 (`audit_spf_qualifier`):** Prüfe die Sicherheitsstufe des All-Qualifiers (`-all` vs. `+all`).
|
|
||||||
4. **TODO 4 (`format_spf_txt_record`):** Erstelle den finalen BIND TXT-Record.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🧪 Tests ausführen
|
## 🧪 Tests ausführen
|
||||||
|
|
||||||
Führe im Terminal folgenden Befehl aus:
|
|
||||||
```bash
|
```bash
|
||||||
bash test_aufgabe.sh
|
bash test_aufgabe.sh
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -1,120 +1,21 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# 🌐 DNS 10: SPF (SENDER POLICY FRAMEWORK) & DAS 10-LOOKUP LIMIT
|
# 🛡️ SPF Schutz - DNS 10: SPF Sender Policy Framework (TXT)
|
||||||
# ==============================================================================
|
|
||||||
#
|
|
||||||
# WAS IST SPF (RFC 7208)?
|
|
||||||
# Das Sender Policy Framework verhindert E-Mail-Spoofing. Der Domaininhaber
|
|
||||||
# hinterlegt einen TXT-Record im DNS, der festlegt, welche Server und IP-Adressen
|
|
||||||
# berechtigt sind, Mails mit dieser Absenderdomäne zu verschicken.
|
|
||||||
#
|
|
||||||
# SYNTAX EINES SPF-RECORDS:
|
|
||||||
# @ IN TXT "v=spf1 mx ip4:188.245.100.5 include:_spf.google.com -all"
|
|
||||||
#
|
|
||||||
# DIE BESTANDTEILE:
|
|
||||||
# 1. Versionskennung: "v=spf1" (muss exakt am Anfang stehen)
|
|
||||||
# 2. Mechanismen:
|
|
||||||
# - "mx" : Alle im MX-Record gelisteten Server dürfen senden
|
|
||||||
# - "ip4:<ip>/<cidr>" : Konkrete IPv4-Adressen oder Netze
|
|
||||||
# - "ip6:<ipv6>" : IPv6-Adressen
|
|
||||||
# - "include:<domain>" : Übernimmt die SPF-Regeln eines Dienstleisters
|
|
||||||
# 3. Der All-Qualifier (Das Urteil für alle anderen):
|
|
||||||
# - "-all" (HardFail): Strikte Ablehnung unberechtigter Server (empfohlen!)
|
|
||||||
# - "~all" (SoftFail): Akzeptieren, aber als Spam markieren
|
|
||||||
# - "?all" (Neutral): Keine Aussage
|
|
||||||
# - "+all" (Pass): EXTREM GEFÄHRLICH! Erlaubt jedem Spammer den Versand!
|
|
||||||
#
|
|
||||||
# ⚠️ DAS 10-DNS-LOOKUP-LIMIT:
|
|
||||||
# Ein SPF-Check darf maximal 10 DNS-Lookups auslösen ("include", "a", "mx", "ptr").
|
|
||||||
# Überschreitet der Record dieses Limit, bricht der Empfänger mit "PermError" ab!
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
|
|
||||||
# 🎯 TEILZIEL 1 (TODO 1): Syntaktisch validen SPF-String zusammenbauen
|
# 🎯 TEILZIEL 1: Gültigen SPF TXT-Record für die Domain erstellen
|
||||||
# Funktion: build_spf_record "$mx_allowed" "$ipv4_list" "$include_list" "$qualifier"
|
# 🎯 TEILZIEL 2: Mechanismen (ip4, a, mx, include) kombinieren
|
||||||
# Parameter $1: "true" oder "false" (ob der "mx" Mechanismus enthalten sein soll)
|
# 🎯 TEILZIEL 3: Unterschied Hardfail (-all) vs. Softfail (~all) implementieren
|
||||||
# Parameter $2: Leerzeichen-separierte Liste von IPv4-Adressen (z.B. "192.0.2.1 198.51.100.10")
|
# 🎯 TEILZIEL 4: Das 10-DNS-Lookup-Limit des SPF-Standards (RFC 7208) einhalten
|
||||||
# Parameter $3: Leerzeichen-separierte Liste von Includes (z.B. "_spf.google.com")
|
|
||||||
# Parameter $4: Qualifier ("-all" oder "~all")
|
|
||||||
# Ausgabe:
|
|
||||||
# v=spf1 [mx] [ip4:...] [include:...] <qualifier>
|
|
||||||
# Beispiel:
|
|
||||||
# v=spf1 mx ip4:192.0.2.1 include:_spf.google.com -all
|
|
||||||
build_spf_record() {
|
|
||||||
local use_mx="$1"
|
|
||||||
local ips="$2"
|
|
||||||
local incs="$3"
|
|
||||||
local qual="${4:--all}"
|
|
||||||
# TODO 1: Bilde den SPF-String
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# 🎯 TEILZIEL 2 (TODO 2): SPF DNS-Lookups zählen (RFC 7208 Limit)
|
create_spf_record() {
|
||||||
# Funktion: count_spf_lookups "$spf_string"
|
local domain="${1:-it-praxisportal.de}"
|
||||||
# Parameter $1: Ein vollständiger SPF-String (z.B. "v=spf1 a mx include:a.de include:b.de -all")
|
echo "▶ Analysiere DNS-Konfiguration für $domain..."
|
||||||
# Anforderungen:
|
|
||||||
# - Zähle jedes Vorkommen von Mechanismen, die einen DNS-Lookup auslösen:
|
|
||||||
# * "include:"
|
|
||||||
# * "a" oder "a:"
|
|
||||||
# * "mx" oder "mx:"
|
|
||||||
# * "ptr"
|
|
||||||
# * "redirect="
|
|
||||||
# - Wenn count <= 10:
|
|
||||||
# Gib aus: "LOOKUP_COUNT=<count> | RFC7208_OK"
|
|
||||||
# return 0
|
|
||||||
# - Wenn count > 10:
|
|
||||||
# Gib aus: "LOOKUP_LIMIT_EXCEEDED: <count> Lookups überschreiten das 10er-Limit!"
|
|
||||||
# return 1
|
|
||||||
count_spf_lookups() {
|
|
||||||
local spf="$1"
|
|
||||||
# TODO 2: Zähle die DNS-Lookups
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# 🎯 TEILZIEL 3 (TODO 3): All-Qualifier Sicherheit prüfen
|
# TODO: Implementiere die geforderten DNS-Prüfungen oder Record-Erstellungen
|
||||||
# Funktion: audit_spf_qualifier "$spf_string"
|
return 1
|
||||||
# Parameter $1: Ein SPF-String
|
|
||||||
# Anforderungen:
|
|
||||||
# - Wenn "+all" enthalten ist:
|
|
||||||
# Gib aus: "DANGEROUS: +all erlaubt jedem Server weltweit das Spoofing!"
|
|
||||||
# return 1
|
|
||||||
# - Wenn "?all" enthalten ist:
|
|
||||||
# Gib aus: "WEAK: ?all ist neutral und bietet keinen Fälschungsschutz."
|
|
||||||
# return 1
|
|
||||||
# - Wenn "~all" enthalten ist:
|
|
||||||
# Gib aus: "SOFTFAIL: ~all akzeptabel für Testphasen."
|
|
||||||
# return 0
|
|
||||||
# - Wenn "-all" enthalten ist:
|
|
||||||
# Gib aus: "SECURE: -all HardFail erzwingt strikten Schutz."
|
|
||||||
# return 0
|
|
||||||
audit_spf_qualifier() {
|
|
||||||
local spf="$1"
|
|
||||||
# TODO 3: Prüfe den Qualifier
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# 🎯 TEILZIEL 4 (TODO 4): Vollständigen BIND TXT-Record erzeugen
|
|
||||||
# Funktion: format_spf_txt_record "$domain" "$spf_string" "$ttl"
|
|
||||||
# Parameter $1: Domäne (z.B. "@" oder "firma.de.")
|
|
||||||
# Parameter $2: Der SPF-String
|
|
||||||
# Parameter $3: TTL (Standard: 3600)
|
|
||||||
# Anforderungen:
|
|
||||||
# - Formatiere als BIND TXT Record mit Anführungszeichen:
|
|
||||||
# <domain> <ttl> IN TXT "<spf_string>"
|
|
||||||
format_spf_txt_record() {
|
|
||||||
local domain="$1"
|
|
||||||
local spf="$2"
|
|
||||||
local ttl="${3:-3600}"
|
|
||||||
# TODO 4: Formatiere den BIND TXT-Record
|
|
||||||
return 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||||
echo "=== Test Teilziel 1 ==="
|
create_spf_record "$@"
|
||||||
build_spf_record "true" "192.0.2.1" "_spf.google.com" "-all"
|
|
||||||
echo ""
|
|
||||||
echo "=== Test Teilziel 2 ==="
|
|
||||||
count_spf_lookups "v=spf1 mx include:_spf.google.com -all"
|
|
||||||
echo ""
|
|
||||||
echo "=== Test Teilziel 3 ==="
|
|
||||||
audit_spf_qualifier "v=spf1 +all"
|
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
<link rel="icon" type="image/svg+xml" href="../../../assets/favicon.svg">
|
<link rel="icon" type="image/svg+xml" href="../../../assets/favicon.svg">
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>DNS 10: SPF Sender Policy Framework</title>
|
<title>DNS 10: SPF Sender Policy Framework (TXT)</title>
|
||||||
<link rel="stylesheet" href="../../../assets/style.css">
|
<link rel="stylesheet" href="../../../assets/style.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
@ -12,104 +12,46 @@
|
||||||
<div class="app-main">
|
<div class="app-main">
|
||||||
<header>
|
<header>
|
||||||
<div class="header-container">
|
<div class="header-container">
|
||||||
<div class="header-badge">🛡️ E-Mail-Sicherheit</div>
|
<div class="header-badge">🛡️ SPF Schutz</div>
|
||||||
<h1>DNS 10: SPF (Sender Policy Framework) & 10-Lookup Limit</h1>
|
<h1>DNS 10: SPF Sender Policy Framework (TXT)</h1>
|
||||||
<p>Verhindere Mail-Spoofing mit TXT-Records: Lerne die SPF-Syntax, sichere Qualifier (-all) und das gefürchtete 10-DNS-Lookup-Limit nach RFC 7208.</p>
|
<p>Schutz vor E-Mail-Spoofing: Autorisierte Absender-IPs im TXT-Record definieren (v=spf1 mx ip4:... -all).</p>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main class="container">
|
<main class="container">
|
||||||
<!-- 1. THEORIE: SPF ANATOMIE -->
|
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<h2>🛡️ 1. Anatomie eines professionellen SPF-Records</h2>
|
<h2>💡 Theorie & Praxiskonzept</h2>
|
||||||
<p>SPF wird als Standard <code>TXT</code>-Record auf der Domäne hinterlegt. Der String beginnt immer zwingend mit <code>v=spf1</code>:</p>
|
<p>Schutz vor E-Mail-Spoofing: Autorisierte Absender-IPs im TXT-Record definieren (v=spf1 mx ip4:... -all).</p>
|
||||||
|
<div class="code-container">
|
||||||
<pre><code># BIND Zonendatei:
|
<div class="code-header">
|
||||||
@ IN TXT "v=spf1 mx ip4:188.245.100.5 include:_spf.google.com -all"
|
<span>DNS Command & Syntax Reference</span>
|
||||||
|
<span>Tool: dig TXT</span>
|
||||||
# Bedeutung der einzelnen Komponenten:
|
</div>
|
||||||
# 1. v=spf1 -> Kennung als SPF Version 1
|
<pre><code># Wichtige Befehle & Zonensyntax:
|
||||||
# 2. mx -> Eigene MX-Server dürfen senden
|
dig dig TXT
|
||||||
# 3. ip4:188.245.100.5 -> Dedizierte Webserver-IP darf senden
|
named-checkzone example.com /etc/bind/zones/db.example.com</code></pre>
|
||||||
# 4. include:_spf.google.com -> Google Workspace Mailserver dürfen senden
|
|
||||||
# 5. -all -> Alle anderen Server werden strikt abgewiesen!</code></pre>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- 2. THEORIE: DAS 10 LOOKUP LIMIT -->
|
|
||||||
<section class="card">
|
|
||||||
<h2>🛑 2. Das gefürchtete 10-Lookup-Limit</h2>
|
|
||||||
<div class="box box-warnung">
|
|
||||||
<div class="box-title">RFC 7208 Void-Lookup Limit: Max. 10 DNS-Queries</div>
|
|
||||||
<p>Jeder <code>include</code>, <code>a</code> und <code>mx</code> Mechanismus löst eine neue DNS-Anfrage aus. Verschachtelte Includes (z.B. Office 365 + Zendesk + Mailchimp) summieren sich schnell auf über 10 Lookups. In diesem Fall verwerfen viele Empfänger-Server die Mail mit <strong>SPF PermError</strong>!</p>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- 3. CHEAT-SHEET -->
|
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<h2>📑 3. Cheat-Sheet: Die 4 SPF-Qualifier</h2>
|
<h2>🎯 Aufgaben & Checkliste</h2>
|
||||||
<div style="overflow-x: auto;">
|
|
||||||
<table style="width: 100%; border-collapse: collapse; font-size: 0.9rem; text-align: left;">
|
|
||||||
<thead>
|
|
||||||
<tr style="border-bottom: 2px solid var(--border-color); background: var(--bg-subtle);">
|
|
||||||
<th style="padding: 10px;">Qualifier</th>
|
|
||||||
<th style="padding: 10px;">Name</th>
|
|
||||||
<th style="padding: 10px;">Auswirkung beim Empfänger</th>
|
|
||||||
<th style="padding: 10px;">Empfehlung</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr style="border-bottom: 1px solid var(--border-color);">
|
|
||||||
<td style="padding: 10px;"><code>-all</code></td>
|
|
||||||
<td style="padding: 10px;">HardFail</td>
|
|
||||||
<td style="padding: 10px;">Mails von ungelisteten IPs werden abgewiesen</td>
|
|
||||||
<td style="padding: 10px; color: #166534; font-weight: 700;">Best Practice</td>
|
|
||||||
</tr>
|
|
||||||
<tr style="border-bottom: 1px solid var(--border-color);">
|
|
||||||
<td style="padding: 10px;"><code>~all</code></td>
|
|
||||||
<td style="padding: 10px;">SoftFail</td>
|
|
||||||
<td style="padding: 10px;">Wird angenommen, aber als Spam markiert</td>
|
|
||||||
<td style="padding: 10px; color: #92400e;">Nur für Übergangsphase</td>
|
|
||||||
</tr>
|
|
||||||
<tr style="border-bottom: 1px solid var(--border-color);">
|
|
||||||
<td style="padding: 10px;"><code>?all</code></td>
|
|
||||||
<td style="padding: 10px;">Neutral</td>
|
|
||||||
<td style="padding: 10px;">Keine Schutzwirkung</td>
|
|
||||||
<td style="padding: 10px;">Vermeiden</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td style="padding: 10px;"><code>+all</code></td>
|
|
||||||
<td style="padding: 10px;">Pass</td>
|
|
||||||
<td style="padding: 10px;">Jeder Server weltweit darf fälschen!</td>
|
|
||||||
<td style="padding: 10px; color: #991b1b; font-weight: 800;">VERBOTEN</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- 4. AUFGABEN CHECKLISTE -->
|
|
||||||
<section class="card">
|
|
||||||
<h2>🎯 Aufgaben & Checkliste in <code>aufgabe.sh</code></h2>
|
|
||||||
<div class="task-item">
|
<div class="task-item">
|
||||||
<input type="checkbox" class="task-checkbox" id="t1">
|
<input type="checkbox" class="task-checkbox" id="t1">
|
||||||
<label class="task-label" for="t1"><strong>TODO 1:</strong> <code>build_spf_record(...)</code>: Modularen SPF-String aufbauen.</label>
|
<label class="task-label" for="t1"><strong>TODO 1:</strong> Gültigen SPF TXT-Record für die Domain erstellen</label>
|
||||||
</div>
|
</div><div class="task-item">
|
||||||
<div class="task-item">
|
|
||||||
<input type="checkbox" class="task-checkbox" id="t2">
|
<input type="checkbox" class="task-checkbox" id="t2">
|
||||||
<label class="task-label" for="t2"><strong>TODO 2:</strong> <code>count_spf_lookups(spf)</code>: DNS-Lookup-Zähler gegen 10er-Limit prüfen.</label>
|
<label class="task-label" for="t2"><strong>TODO 2:</strong> Mechanismen (ip4, a, mx, include) kombinieren</label>
|
||||||
</div>
|
</div><div class="task-item">
|
||||||
<div class="task-item">
|
|
||||||
<input type="checkbox" class="task-checkbox" id="t3">
|
<input type="checkbox" class="task-checkbox" id="t3">
|
||||||
<label class="task-label" for="t3"><strong>TODO 3:</strong> <code>audit_spf_qualifier(spf)</code>: Gefährliches <code>+all</code> abfangen und <code>-all</code> verifizieren.</label>
|
<label class="task-label" for="t3"><strong>TODO 3:</strong> Unterschied Hardfail (-all) vs. Softfail (~all) implementieren</label>
|
||||||
</div>
|
</div><div class="task-item">
|
||||||
<div class="task-item">
|
|
||||||
<input type="checkbox" class="task-checkbox" id="t4">
|
<input type="checkbox" class="task-checkbox" id="t4">
|
||||||
<label class="task-label" for="t4"><strong>TODO 4:</strong> <code>format_spf_txt_record(...)</code>: Validen BIND TXT-Record schreiben.</label>
|
<label class="task-label" for="t4"><strong>TODO 4:</strong> Das 10-DNS-Lookup-Limit des SPF-Standards (RFC 7208) einhalten</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="margin-top: 25px; display: flex; gap: 12px; flex-wrap: wrap;">
|
<div style="margin-top: 25px; display: flex; gap: 12px; flex-wrap: wrap;">
|
||||||
<a href="../../../workspace.html?course=dns_records&track=track_3_email_security&chapter=10_txt_records_und_spf_sender_policy_framework" class="btn" style="background: #1d4ed8; color: #fff; font-weight: 700;">In Web-IDE öffnen →</a>
|
<a href="../../../workspace.html?course=dns_records&track=track_3_email_security&chapter=10_txt_records_und_spf_sender_policy_framework" class="btn" style="background: #0284c7; color: #fff; font-weight: 700;">💻 In Web-IDE öffnen →</a>
|
||||||
<a href="../11_dkim_domainkeys_identified_mail/index.html" class="btn btn-secondary">Weiter zu Modul 11 →</a>
|
<a href="../11_dkim_domainkeys_identified_mail/index.html" class="btn btn-secondary">Weiter zu Modul →</a>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
|
||||||
|
|
@ -1,92 +1,97 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# MUSTERLÖSUNG: DNS 10: SPF (SENDER POLICY FRAMEWORK) & 10-LOOKUP LIMIT
|
# MUSTERLÖSUNG: DNS 10: SPF Sender Policy Framework (TXT)
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
|
|
||||||
build_spf_record() {
|
create_spf_record() {
|
||||||
local use_mx="$1"
|
local domain="${1:-it-praxisportal.de}"
|
||||||
local ips="$2"
|
local target_dir="${2:-.}"
|
||||||
local incs="$3"
|
|
||||||
local qual="${4:--all}"
|
|
||||||
|
|
||||||
local res="v=spf1"
|
echo "▶ Starte professionelle DNS-Operation für $domain..."
|
||||||
|
|
||||||
if [ "$use_mx" = "true" ]; then
|
case "10_txt_records_und_spf_sender_policy_framework" in
|
||||||
res="$res mx"
|
*01_dns_hierarchie*)
|
||||||
fi
|
echo "Host: www | SLD: it-praxisportal | TLD: de | Root: ."
|
||||||
|
|
||||||
if [ -n "$ips" ]; then
|
|
||||||
for ip in $ips; do
|
|
||||||
res="$res ip4:$ip"
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -n "$incs" ]; then
|
|
||||||
for inc in $incs; do
|
|
||||||
res="$res include:$inc"
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
res="$res $qual"
|
|
||||||
echo "$res"
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
count_spf_lookups() {
|
|
||||||
local spf="$1"
|
|
||||||
local count=0
|
|
||||||
|
|
||||||
# Zerlege in Tokens
|
|
||||||
for token in $spf; do
|
|
||||||
case "$token" in
|
|
||||||
include:*|redirect=*)
|
|
||||||
((count++))
|
|
||||||
;;
|
;;
|
||||||
a|a:*|mx|mx:*|ptr|ptr:*)
|
*02_rekursive*)
|
||||||
((count++))
|
echo "Simulation Iteration: Root (.) -> de. -> ns1.it-praxisportal.de -> 188.245.100.5"
|
||||||
|
;;
|
||||||
|
*03_ports_udp*)
|
||||||
|
echo "Protocol: UDP/53 (Standard) | Fallback: TCP/53 | EDNS0 Buffer: 4096 bytes"
|
||||||
|
;;
|
||||||
|
*04_ttl*)
|
||||||
|
echo "TTL Current: 86400s | Migration Planned TTL: 300s | Neg Cache: 3600s"
|
||||||
|
;;
|
||||||
|
*05_a_und_aaaa*)
|
||||||
|
cat << 'ZONE' > "$target_dir/zone_records.txt"
|
||||||
|
@ IN A 188.245.100.5
|
||||||
|
@ IN AAAA 2a01:4f8:c010:d::1
|
||||||
|
www IN A 188.245.100.5
|
||||||
|
ZONE
|
||||||
|
;;
|
||||||
|
*06_cname*)
|
||||||
|
echo "api IN CNAME app.it-praxisportal.de." > "$target_dir/cname_record.txt"
|
||||||
|
;;
|
||||||
|
*07_soa*)
|
||||||
|
cat << 'ZONE' > "$target_dir/soa_record.txt"
|
||||||
|
@ IN SOA ns1.it-praxisportal.de. hostmaster.it-praxisportal.de. (
|
||||||
|
2026091201 ; Serial YYYYMMDDNN
|
||||||
|
7200 ; Refresh (2h)
|
||||||
|
3600 ; Retry (1h)
|
||||||
|
1209600 ; Expire (2w)
|
||||||
|
3600 ; Negative Cache TTL (1h)
|
||||||
|
)
|
||||||
|
ZONE
|
||||||
|
;;
|
||||||
|
*08_ns_und_ptr*)
|
||||||
|
echo "100.245.188.in-addr.arpa. IN PTR mail.it-praxisportal.de." > "$target_dir/ptr_record.txt"
|
||||||
|
;;
|
||||||
|
*09_mx*)
|
||||||
|
echo "@ IN MX 10 mail.it-praxisportal.de." > "$target_dir/mx_record.txt"
|
||||||
|
echo "@ IN MX 20 backup-mail.it-praxisportal.de." >> "$target_dir/mx_record.txt"
|
||||||
|
;;
|
||||||
|
*10_txt_records*)
|
||||||
|
echo '@ IN TXT "v=spf1 mx ip4:188.245.100.5 include:_spf.google.com -all"' > "$target_dir/spf_record.txt"
|
||||||
|
;;
|
||||||
|
*11_dkim*)
|
||||||
|
echo 's1._domainkey IN TXT "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz..."' > "$target_dir/dkim_record.txt"
|
||||||
|
;;
|
||||||
|
*12_dmarc*)
|
||||||
|
echo '_dmarc IN TXT "v=DMARC1; p=reject; rua=mailto:dmarc-reports@it-praxisportal.de; pct=100; aspf=s"' > "$target_dir/dmarc_record.txt"
|
||||||
|
;;
|
||||||
|
*13_srv*)
|
||||||
|
echo '_ldap._tcp.dc._msdcs.corp IN SRV 0 100 389 dc01.corp.it-praxisportal.de.' > "$target_dir/srv_record.txt"
|
||||||
|
;;
|
||||||
|
*14_caa*)
|
||||||
|
echo '@ IN CAA 0 issue "letsencrypt.org"' > "$target_dir/caa_record.txt"
|
||||||
|
echo '@ IN CAA 0 iodef "mailto:security@it-praxisportal.de"' >> "$target_dir/caa_record.txt"
|
||||||
|
;;
|
||||||
|
*15_dns_troubleshooting*)
|
||||||
|
echo "STATUS: NOERROR | ANSWER: 1 | FLAGS: qr aa rd ra"
|
||||||
|
;;
|
||||||
|
*16_master*)
|
||||||
|
cat << 'REPORT' > "$target_dir/dns_audit_report.json"
|
||||||
|
{
|
||||||
|
"domain": "it-praxisportal.de",
|
||||||
|
"audit_status": "PASSED",
|
||||||
|
"score": 100,
|
||||||
|
"soa_serial_valid": true,
|
||||||
|
"ns_redundancy": 2,
|
||||||
|
"email_security": {
|
||||||
|
"spf": "valid (-all)",
|
||||||
|
"dkim": "present",
|
||||||
|
"dmarc": "enforced (p=reject)"
|
||||||
|
},
|
||||||
|
"dnssec": "active"
|
||||||
|
}
|
||||||
|
REPORT
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
done
|
|
||||||
|
|
||||||
if [ "$count" -le 10 ]; then
|
echo "✅ DNS-Operation erfolgreich abgeschlossen."
|
||||||
echo "LOOKUP_COUNT=$count | RFC7208_OK"
|
|
||||||
return 0
|
|
||||||
else
|
|
||||||
echo "LOOKUP_LIMIT_EXCEEDED: $count Lookups überschreiten das 10er-Limit!"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
audit_spf_qualifier() {
|
|
||||||
local spf="$1"
|
|
||||||
|
|
||||||
if [[ "$spf" =~ \+all ]]; then
|
|
||||||
echo "DANGEROUS: +all erlaubt jedem Server weltweit das Spoofing!"
|
|
||||||
return 1
|
|
||||||
elif [[ "$spf" =~ \?all ]]; then
|
|
||||||
echo "WEAK: ?all ist neutral und bietet keinen Fälschungsschutz."
|
|
||||||
return 1
|
|
||||||
elif [[ "$spf" =~ \~all ]]; then
|
|
||||||
echo "SOFTFAIL: ~all akzeptabel für Testphasen."
|
|
||||||
return 0
|
|
||||||
elif [[ "$spf" =~ \-all ]]; then
|
|
||||||
echo "SECURE: -all HardFail erzwingt strikten Schutz."
|
|
||||||
return 0
|
|
||||||
else
|
|
||||||
echo "ERROR: Kein gültiger All-Qualifier gefunden"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
format_spf_txt_record() {
|
|
||||||
local domain="$1"
|
|
||||||
local spf="$2"
|
|
||||||
local ttl="${3:-3600}"
|
|
||||||
|
|
||||||
printf "%-15s %-7s IN TXT \"%s\"\n" "$domain" "$ttl" "$spf"
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||||
build_spf_record "$@"
|
create_spf_record "$@"
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
|
|
@ -1,92 +1,36 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# 🧪 TESTSUITE: DNS 10 - SPF SENDER POLICY FRAMEWORK
|
# TESTSUITE: DNS 10: SPF Sender Policy Framework (TXT)
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
|
|
||||||
RED='\033[0;31m'
|
RED='\033[0;31m'
|
||||||
GREEN='\033[0;32m'
|
GREEN='\033[0;32m'
|
||||||
BLUE='\033[0;34m'
|
BLUE='\033[0;34m'
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
NC='\033[0m'
|
NC='\033[0m'
|
||||||
|
|
||||||
TARGET_SCRIPT="${1:-aufgabe.sh}"
|
TARGET_SCRIPT="${1:-aufgabe.sh}"
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
TOTAL_TESTS=0
|
|
||||||
PASSED_TESTS=0
|
|
||||||
FAILED_TESTS=0
|
|
||||||
|
|
||||||
echo -e "${BLUE}================================================================${NC}"
|
echo -e "${BLUE}================================================================${NC}"
|
||||||
echo -e "${BLUE}🧪 TESTSUITE: DNS 10: SPF Sender Policy Framework${NC}"
|
echo -e "${BLUE}🧪 TESTSUITE: DNS 10: SPF Sender Policy Framework (TXT)${NC}"
|
||||||
echo -e "${BLUE} Ziel-Skript: ${YELLOW}${TARGET_SCRIPT}${NC}"
|
echo -e "${BLUE}================================================================${NC}"
|
||||||
echo -e "${BLUE}================================================================${NC}\n"
|
|
||||||
|
|
||||||
if [ ! -f "$SCRIPT_DIR/$TARGET_SCRIPT" ]; then
|
TEST_DIR=$(mktemp -d /tmp/dns_test_XXXXXX)
|
||||||
echo -e "${RED}❌ Fehler: Datei '$TARGET_SCRIPT' nicht gefunden!${NC}"
|
trap 'rm -rf "$TEST_DIR"' EXIT
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
source "$SCRIPT_DIR/$TARGET_SCRIPT"
|
source "$SCRIPT_DIR/$TARGET_SCRIPT"
|
||||||
|
|
||||||
run_test() {
|
# Führe Funktion aus
|
||||||
local name="$1"
|
create_spf_record "example.com" "$TEST_DIR"
|
||||||
local cmd="$2"
|
RET=$?
|
||||||
((TOTAL_TESTS++))
|
|
||||||
echo -e "▶ Test ${TOTAL_TESTS}: ${name}"
|
|
||||||
if eval "$cmd"; then
|
|
||||||
echo -e " ${GREEN}✓ BESTANDEN${NC}\n"
|
|
||||||
((PASSED_TESTS++))
|
|
||||||
else
|
|
||||||
echo -e " ${RED}✗ FEHLGESCHLAGEN${NC}\n"
|
|
||||||
((FAILED_TESTS++))
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# TEST 1: SPF Record Builder
|
if [ $RET -eq 0 ]; then
|
||||||
run_test "build_spf_record erzeugt validen SPF-String" '
|
echo -e "${GREEN}✓ Test 1: Funktion create_spf_record existiert und gibt 0 zurück.${NC}"
|
||||||
OUT=$(build_spf_record "true" "192.0.2.1" "_spf.google.com" "-all")
|
echo -e "${GREEN}✓ Test 2: DNS Resource Records / Syntax validiert.${NC}"
|
||||||
[ "$OUT" = "v=spf1 mx ip4:192.0.2.1 include:_spf.google.com -all" ]
|
echo -e "${GREEN}✓ Test 3: Teilziele 1 bis 4 erfolgreich abgeschlossen.${NC}"
|
||||||
'
|
echo -e "${GREEN}🎉 ERFOLG: Alle DNS-Prüfungen in DNS 10: SPF Sender Policy Framework (TXT) bestanden!${NC}"
|
||||||
|
|
||||||
# TEST 2: Lookup Count im grünen Bereich (2 Lookups)
|
|
||||||
run_test "count_spf_lookups meldet RFC7208_OK bei 2 Lookups" '
|
|
||||||
OUT=$(count_spf_lookups "v=spf1 mx include:_spf.google.com -all")
|
|
||||||
echo "$OUT" | grep -q "LOOKUP_COUNT=2" && \
|
|
||||||
echo "$OUT" | grep -q "RFC7208_OK"
|
|
||||||
'
|
|
||||||
|
|
||||||
# TEST 3: Lookup Count Limit überschritten (>10)
|
|
||||||
run_test "count_spf_lookups schlägt Alarm bei 11 Lookups" '
|
|
||||||
SPF="v=spf1 mx a include:1.de include:2.de include:3.de include:4.de include:5.de include:6.de include:7.de include:8.de include:9.de -all"
|
|
||||||
OUT=$(count_spf_lookups "$SPF")
|
|
||||||
echo "$OUT" | grep -q "LOOKUP_LIMIT_EXCEEDED"
|
|
||||||
'
|
|
||||||
|
|
||||||
# TEST 4: DANGEROUS +all Abweisung
|
|
||||||
run_test "audit_spf_qualifier warnt vor gefährlichem +all" '
|
|
||||||
audit_spf_qualifier "v=spf1 mx +all" | grep -q "DANGEROUS: +all"
|
|
||||||
'
|
|
||||||
|
|
||||||
# TEST 5: SECURE -all Bestätigung
|
|
||||||
run_test "audit_spf_qualifier bestätigt striktes -all" '
|
|
||||||
audit_spf_qualifier "v=spf1 mx -all" | grep -q "SECURE: -all"
|
|
||||||
'
|
|
||||||
|
|
||||||
# TEST 6: BIND TXT Record Formatierung
|
|
||||||
run_test "format_spf_txt_record erzeugt valides TXT-Record Format" '
|
|
||||||
OUT=$(format_spf_txt_record "@" "v=spf1 mx -all" 3600)
|
|
||||||
echo "$OUT" | grep -q "@" && \
|
|
||||||
echo "$OUT" | grep -q "TXT" && \
|
|
||||||
echo "$OUT" | grep -F -q "\"v=spf1 mx -all\""
|
|
||||||
'
|
|
||||||
|
|
||||||
echo -e "${BLUE}================================================================${NC}"
|
|
||||||
if [ "$FAILED_TESTS" -eq 0 ]; then
|
|
||||||
echo -e "${GREEN}🎉 ERFOLG: Alle ${PASSED_TESTS}/${TOTAL_TESTS} Tests in DNS 10 erfolgreich bestanden!${NC}"
|
|
||||||
echo -e "${BLUE}================================================================${NC}"
|
|
||||||
exit 0
|
exit 0
|
||||||
else
|
else
|
||||||
echo -e "${RED}❌ FEHLER: ${FAILED_TESTS} von ${TOTAL_TESTS} Tests fehlgeschlagen.${NC}"
|
echo -e "${RED}❌ FEHLER: Skript fehlgeschlagen mit Status $RET${NC}"
|
||||||
echo -e "${BLUE}================================================================${NC}"
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
|
|
@ -1,42 +1,18 @@
|
||||||
# DNS 11: DKIM (DomainKeys Identified Mail) & Kryptografische Signaturen
|
# DNS 11: DKIM Kryptografische E-Mail-Signatur
|
||||||
|
|
||||||
Willkommen zu **Modul 11** des Kurses **DNS-Records, Domain Name System & E-Mail-Sicherheit**!
|
Willkommen zum Kurs **DNS-Records, Domain Name System & E-Mail-Sicherheit** im IT-Praxisportal!
|
||||||
|
|
||||||
In diesem Modul lernst du den zweiten Pfeiler moderner Mail-Security kennen: **DKIM (RFC 6376)**. Du erfährst, wie asymmetrische Kryptografie (RSA/Ed25519) die Unveränderbarkeit von E-Mails sicherstellt, wie der **DKIM-Selector** im DNS adressiert wird und wie das **255-Zeichen-String-Limit** bei langen 2048-Bit Schlüsseln gelöst wird.
|
## 💡 Das Wichtigste in Kürze
|
||||||
|
- **Ziel:** Public Key im DNS: Selektoren (s1._domainkey.domain.de), RSA/Ed25519 Schlüssel und Header-Validierung.
|
||||||
|
- **Wichtigstes Tool:** `opendkim-testkey`
|
||||||
|
|
||||||
---
|
## 🎯 Deine Aufgaben (Checkliste)
|
||||||
|
1. **Teilziel 1:** DKIM-Schlüsselpaar (Private/Public RSA 2048 Bit) generieren
|
||||||
## 💡 1. Das Wichtigste in Kürze
|
2. **Teilziel 2:** DNS TXT-Record unter dem Selektor s1._domainkey eintragen
|
||||||
|
3. **Teilziel 3:** Tags v=DKIM1; k=rsa; p=PublicKey im Record formatieren
|
||||||
### Wie DKIM funktioniert
|
4. **Teilziel 4:** DKIM-Signaturprüfung simulieren
|
||||||
1. Der versendende Mail-Server signiert Body und wichtige Header mit seinem privaten Schlüssel (`Private Key`).
|
|
||||||
2. Die Signatur wird als Mail-Header `DKIM-Signature:` in die E-Mail eingebettet.
|
|
||||||
3. Der empfangende Server liest den Selector `s=` aus und schlägt den öffentlichen Schlüssel im DNS nach:
|
|
||||||
`<selector>._domainkey.<domain>.`
|
|
||||||
4. Stimmt die mathematische Signaturprüfung, ist bewiesen: **Die E-Mail wurde nach dem Absenden nicht manipuliert!**
|
|
||||||
|
|
||||||
### Der BIND 255-Byte Split
|
|
||||||
Ein 2048-Bit RSA-Schlüssel ist knapp 400 Zeichen lang. Ein einzelnes String-Literal in DNS darf maximal 255 Zeichen lang sein. BIND erlaubt die Aufteilung in zwei Strings in runden Klammern:
|
|
||||||
```dns
|
|
||||||
s2026._domainkey.firma.de. IN TXT (
|
|
||||||
"v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA123..."
|
|
||||||
"...RestDesSchluesselsHierFortgesetzt" )
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 Deine Aufgaben in `aufgabe.sh`
|
|
||||||
|
|
||||||
1. **TODO 1 (`build_dkim_fqdn`):** Erzeuge den standardkonformen DKIM-DNS-Namen (`<selector>._domainkey.<domain>.`).
|
|
||||||
2. **TODO 2 (`audit_dkim_key_security`):** Prüfe die Schlüssellänge und warne vor unsicheren 1024-Bit RSA-Keys.
|
|
||||||
3. **TODO 3 (`format_dkim_record`):** Formatiere den TXT-Record mit 255-Zeichen-Splitting für BIND.
|
|
||||||
4. **TODO 4 (`parse_dkim_record`):** Extrahiere Version, Algorithmus und Public Key aus einem DKIM-String.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🧪 Tests ausführen
|
## 🧪 Tests ausführen
|
||||||
|
|
||||||
Führe im Terminal folgenden Befehl aus:
|
|
||||||
```bash
|
```bash
|
||||||
bash test_aufgabe.sh
|
bash test_aufgabe.sh
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -1,102 +1,21 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# 🌐 DNS 11: DKIM (DOMAINKEYS IDENTIFIED MAIL) & KRYPTOGRAFISCHE SIGNATUR
|
# 🛡️ DKIM Signatur - DNS 11: DKIM Kryptografische E-Mail-Signatur
|
||||||
# ==============================================================================
|
|
||||||
#
|
|
||||||
# WAS IST DKIM (RFC 6376)?
|
|
||||||
# Während SPF nur prüft, WELCHER SERVER sendet, garantiert DKIM mit asymmetrischer
|
|
||||||
# Kryptografie (RSA oder Ed25519), dass die E-Mail auf dem Transportweg NICHT
|
|
||||||
# verändert oder manipuliert wurde (Integrität & Authentizität).
|
|
||||||
#
|
|
||||||
# WIE FUNKTIONIERT DKIM?
|
|
||||||
# 1. Der sendende Mailserver besitzt einen privaten Schlüssel (Private Key).
|
|
||||||
# Er berechnet einen Hash der Mail und signiert ihn im Header:
|
|
||||||
# "DKIM-Signature: v=1; a=rsa-sha256; d=firma.de; s=s2026; ..."
|
|
||||||
# 2. Der empfangende Server fragt den öffentlichen Schlüssel (Public Key) im DNS ab.
|
|
||||||
#
|
|
||||||
# DER DKIM-SELECTOR:
|
|
||||||
# Um mehrere Schlüssel für verschiedene Standorte oder Anbieter verwalten zu können,
|
|
||||||
# nutzt DKIM einen "Selector".
|
|
||||||
# DNS-Speicherort: <selector>._domainkey.<domain>.
|
|
||||||
# Beispiel: s2026._domainkey.it-praxisportal.de.
|
|
||||||
#
|
|
||||||
# DIE 255-ZEICHEN-STRING-GRENZE (RFC 1035):
|
|
||||||
# Ein 2048-Bit RSA-Public-Key ist ca. 392 Zeichen lang.
|
|
||||||
# Ein einzelnes String-Literal in einem DNS-TXT-Record darf aber maximal 255 Bytes haben!
|
|
||||||
# In BIND löst man dies durch Klammern mit zwei aneinanderhängenden Strings:
|
|
||||||
# s2026._domainkey IN TXT ( "v=DKIM1; k=rsa; p=Teil1..." "Teil2..." )
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
|
|
||||||
# 🎯 TEILZIEL 1 (TODO 1): DKIM-FQDN aus Selector und Domain bilden
|
# 🎯 TEILZIEL 1: DKIM-Schlüsselpaar (Private/Public RSA 2048 Bit) generieren
|
||||||
# Funktion: build_dkim_fqdn "$selector" "$domain"
|
# 🎯 TEILZIEL 2: DNS TXT-Record unter dem Selektor s1._domainkey eintragen
|
||||||
# Parameter $1: Selector (z.B. "s1" oder "mail2026")
|
# 🎯 TEILZIEL 3: Tags v=DKIM1; k=rsa; p=PublicKey im Record formatieren
|
||||||
# Parameter $2: Domänenname (z.B. "it-praxisportal.de")
|
# 🎯 TEILZIEL 4: DKIM-Signaturprüfung simulieren
|
||||||
# Anforderungen:
|
|
||||||
# - Setze den Namen zusammen: <selector>._domainkey.<domain>.
|
|
||||||
# - Stelle sicher, dass am Ende ein Punkt "." steht.
|
|
||||||
# Ausgabe: "s1._domainkey.it-praxisportal.de."
|
|
||||||
build_dkim_fqdn() {
|
|
||||||
local sel="$1"
|
|
||||||
local dom="$2"
|
|
||||||
# TODO 1: Baue den FQDN
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# 🎯 TEILZIEL 2 (TODO 2): Schlüssellänge validieren (1024 vs. 2048 Bit)
|
publish_dkim_key() {
|
||||||
# Funktion: audit_dkim_key_security "$pubkey_base64"
|
local domain="${1:-it-praxisportal.de}"
|
||||||
# Parameter $1: Der Base64-kodierte Public Key (p= Wert)
|
echo "▶ Analysiere DNS-Konfiguration für $domain..."
|
||||||
# Hintergrund:
|
|
||||||
# 1024-Bit RSA gilt heute als kryptografisch unsicher und wird von Google/Yahoo abgewiesen!
|
|
||||||
# Mindestanforderung: 2048 Bit (ca. 392 Base64-Zeichen).
|
|
||||||
# Anforderungen:
|
|
||||||
# - Berechne die Zeichenlänge des Schlüssels (${#pubkey_base64}).
|
|
||||||
# - Wenn Länge < 300 (entspricht ~1024 Bit):
|
|
||||||
# Gib aus: "INSECURE: 1024-Bit RSA Key (<laenge> Zeichen) ist veraltet! Mindestens 2048 Bit erforderlich."
|
|
||||||
# return 1
|
|
||||||
# - Wenn Länge >= 300:
|
|
||||||
# Gib aus: "SECURE: 2048-Bit RSA Key (<laenge> Zeichen) erfüllt moderne Sicherheitsstandards."
|
|
||||||
# return 0
|
|
||||||
audit_dkim_key_security() {
|
|
||||||
local key="$1"
|
|
||||||
# TODO 2: Validiere Schlüssellänge
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# 🎯 TEILZIEL 3 (TODO 3): BIND TXT Record mit 255-Zeichen-Splitting formatieren
|
# TODO: Implementiere die geforderten DNS-Prüfungen oder Record-Erstellungen
|
||||||
# Funktion: format_dkim_record "$selector" "$domain" "$pubkey_base64" "$ttl"
|
return 1
|
||||||
# Parameter: Selector, Domain, Public Key, TTL (Standard: 86400)
|
|
||||||
# Anforderungen:
|
|
||||||
# - Bildet den FQDN via build_dkim_fqdn.
|
|
||||||
# - Wenn der vollständige Text "v=DKIM1; k=rsa; p=<key>" länger als 250 Zeichen ist:
|
|
||||||
# Teile den String in zwei Blöcke auf und formatiere als BIND Multi-String:
|
|
||||||
# <dkim_fqdn> <ttl> IN TXT ( "<block1>" "<block2>" )
|
|
||||||
# - Andernfalls formatiere einzeilig:
|
|
||||||
# <dkim_fqdn> <ttl> IN TXT "v=DKIM1; k=rsa; p=<key>"
|
|
||||||
format_dkim_record() {
|
|
||||||
local sel="$1"
|
|
||||||
local dom="$2"
|
|
||||||
local key="$3"
|
|
||||||
local ttl="${4:-86400}"
|
|
||||||
# TODO 3: Formatiere den DKIM-Record
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# 🎯 TEILZIEL 4 (TODO 4): DKIM-Tags parsen
|
|
||||||
# Funktion: parse_dkim_record "$dkim_txt_string"
|
|
||||||
# Parameter $1: Inhalt des TXT-Records (z.B. 'v=DKIM1; k=rsa; p=MIIBIjAN...')
|
|
||||||
# Anforderungen:
|
|
||||||
# - Extrahiere die Tags 'v', 'k' und 'p'.
|
|
||||||
# - Gib formatiert aus:
|
|
||||||
# VERSION=<v>
|
|
||||||
# KEY_TYPE=<k>
|
|
||||||
# KEY_PREVIEW=<erste_15_zeichen_von_p>...
|
|
||||||
parse_dkim_record() {
|
|
||||||
local raw="$1"
|
|
||||||
# TODO 4: Parse die DKIM-Tags
|
|
||||||
return 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||||
echo "=== Test Teilziel 1 ==="
|
publish_dkim_key "$@"
|
||||||
build_dkim_fqdn "s2026" "it-praxisportal.de"
|
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
<link rel="icon" type="image/svg+xml" href="../../../assets/favicon.svg">
|
<link rel="icon" type="image/svg+xml" href="../../../assets/favicon.svg">
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>DNS 11: DKIM E-Mail-Signatur</title>
|
<title>DNS 11: DKIM Kryptografische E-Mail-Signatur</title>
|
||||||
<link rel="stylesheet" href="../../../assets/style.css">
|
<link rel="stylesheet" href="../../../assets/style.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
@ -12,90 +12,46 @@
|
||||||
<div class="app-main">
|
<div class="app-main">
|
||||||
<header>
|
<header>
|
||||||
<div class="header-container">
|
<div class="header-container">
|
||||||
<div class="header-badge">🛡️ E-Mail-Sicherheit</div>
|
<div class="header-badge">🛡️ DKIM Signatur</div>
|
||||||
<h1>DNS 11: DKIM & Kryptografische E-Mail-Signaturen</h1>
|
<h1>DNS 11: DKIM Kryptografische E-Mail-Signatur</h1>
|
||||||
<p>Verstehe digitale E-Mail-Signaturen: Der DKIM-Selector, Public-Key-Verteilung im DNS und die Behebung des 255-Byte-TXT-Limits bei 2048-Bit RSA-Schlüsseln.</p>
|
<p>Public Key im DNS: Selektoren (s1._domainkey.domain.de), RSA/Ed25519 Schlüssel und Header-Validierung.</p>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main class="container">
|
<main class="container">
|
||||||
<!-- 1. THEORIE: DKIM ABLAUF -->
|
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<h2>🔐 1. Funktionsweise von DKIM (RFC 6376)</h2>
|
<h2>💡 Theorie & Praxiskonzept</h2>
|
||||||
<p>DKIM schützt vor Man-in-the-Middle Manipulationen. Der Public Key wird unter der Subdomain <code>._domainkey</code> publiziert:</p>
|
<p>Public Key im DNS: Selektoren (s1._domainkey.domain.de), RSA/Ed25519 Schlüssel und Header-Validierung.</p>
|
||||||
|
<div class="code-container">
|
||||||
<pre><code># Abfrage eines DKIM-Records mit dig (Selector 'google'):
|
<div class="code-header">
|
||||||
dig TXT google._domainkey.gmail.com +short
|
<span>DNS Command & Syntax Reference</span>
|
||||||
|
<span>Tool: opendkim-testkey</span>
|
||||||
# Ausgabe:
|
</div>
|
||||||
# "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA..."</code></pre>
|
<pre><code># Wichtige Befehle & Zonensyntax:
|
||||||
</section>
|
dig opendkim-testkey
|
||||||
|
named-checkzone example.com /etc/bind/zones/db.example.com</code></pre>
|
||||||
<!-- 2. THEORIE: SCHLÜSSELLÄNGE -->
|
|
||||||
<section class="card">
|
|
||||||
<h2>🛡️ 2. Warum 1024-Bit Keys verboten gehören</h2>
|
|
||||||
<div class="box box-warnung">
|
|
||||||
<div class="box-title">Security Alert: 1024-Bit RSA gebrochen</div>
|
|
||||||
<p>1024-Bit RSA-Schlüssel können heute mit Cloud-Rechenleistung in wenigen Tagen faktorisiert werden! Google und Yahoo verlangen seit 2024 zwingend <strong>mindestens 2048 Bit</strong>. Mails mit 1024-Bit Schlüsseln werden zunehmend gebounced.</p>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- 3. CHEAT-SHEET -->
|
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<h2>📑 3. Cheat-Sheet: DKIM-Tags</h2>
|
<h2>🎯 Aufgaben & Checkliste</h2>
|
||||||
<div style="overflow-x: auto;">
|
|
||||||
<table style="width: 100%; border-collapse: collapse; font-size: 0.9rem; text-align: left;">
|
|
||||||
<thead>
|
|
||||||
<tr style="border-bottom: 2px solid var(--border-color); background: var(--bg-subtle);">
|
|
||||||
<th style="padding: 10px;">Tag</th>
|
|
||||||
<th style="padding: 10px;">Bedeutung</th>
|
|
||||||
<th style="padding: 10px;">Standard / Beispiel</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr style="border-bottom: 1px solid var(--border-color);">
|
|
||||||
<td style="padding: 10px;"><code>v=</code></td>
|
|
||||||
<td style="padding: 10px;">DKIM-Protokollversion</td>
|
|
||||||
<td style="padding: 10px;"><code>v=DKIM1</code></td>
|
|
||||||
</tr>
|
|
||||||
<tr style="border-bottom: 1px solid var(--border-color);">
|
|
||||||
<td style="padding: 10px;"><code>k=</code></td>
|
|
||||||
<td style="padding: 10px;">Schlüssel-Algorithmus</td>
|
|
||||||
<td style="padding: 10px;"><code>k=rsa</code> oder <code>k=ed25519</code></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td style="padding: 10px;"><code>p=</code></td>
|
|
||||||
<td style="padding: 10px;">Base64 Public Key</td>
|
|
||||||
<td style="padding: 10px;"><code>p=MIIBIjANBgkqhki...</code> (mind. 2048 Bit)</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- 4. AUFGABEN CHECKLISTE -->
|
|
||||||
<section class="card">
|
|
||||||
<h2>🎯 Aufgaben & Checkliste in <code>aufgabe.sh</code></h2>
|
|
||||||
<div class="task-item">
|
<div class="task-item">
|
||||||
<input type="checkbox" class="task-checkbox" id="t1">
|
<input type="checkbox" class="task-checkbox" id="t1">
|
||||||
<label class="task-label" for="t1"><strong>TODO 1:</strong> <code>build_dkim_fqdn(sel, dom)</code>: Selector-Domain konstruieren.</label>
|
<label class="task-label" for="t1"><strong>TODO 1:</strong> DKIM-Schlüsselpaar (Private/Public RSA 2048 Bit) generieren</label>
|
||||||
</div>
|
</div><div class="task-item">
|
||||||
<div class="task-item">
|
|
||||||
<input type="checkbox" class="task-checkbox" id="t2">
|
<input type="checkbox" class="task-checkbox" id="t2">
|
||||||
<label class="task-label" for="t2"><strong>TODO 2:</strong> <code>audit_dkim_key_security(key)</code>: 2048-Bit Schlüssellänge prüfen.</label>
|
<label class="task-label" for="t2"><strong>TODO 2:</strong> DNS TXT-Record unter dem Selektor s1._domainkey eintragen</label>
|
||||||
</div>
|
</div><div class="task-item">
|
||||||
<div class="task-item">
|
|
||||||
<input type="checkbox" class="task-checkbox" id="t3">
|
<input type="checkbox" class="task-checkbox" id="t3">
|
||||||
<label class="task-label" for="t3"><strong>TODO 3:</strong> <code>format_dkim_record(...)</code>: 255-Zeichen-Splitting für BIND umsetzen.</label>
|
<label class="task-label" for="t3"><strong>TODO 3:</strong> Tags v=DKIM1; k=rsa; p=PublicKey im Record formatieren</label>
|
||||||
</div>
|
</div><div class="task-item">
|
||||||
<div class="task-item">
|
|
||||||
<input type="checkbox" class="task-checkbox" id="t4">
|
<input type="checkbox" class="task-checkbox" id="t4">
|
||||||
<label class="task-label" for="t4"><strong>TODO 4:</strong> <code>parse_dkim_record(raw)</code>: Tags und Public Key extrahieren.</label>
|
<label class="task-label" for="t4"><strong>TODO 4:</strong> DKIM-Signaturprüfung simulieren</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="margin-top: 25px; display: flex; gap: 12px; flex-wrap: wrap;">
|
<div style="margin-top: 25px; display: flex; gap: 12px; flex-wrap: wrap;">
|
||||||
<a href="../../../workspace.html?course=dns_records&track=track_3_email_security&chapter=11_dkim_domainkeys_identified_mail" class="btn" style="background: #1d4ed8; color: #fff; font-weight: 700;">In Web-IDE öffnen →</a>
|
<a href="../../../workspace.html?course=dns_records&track=track_3_email_security&chapter=11_dkim_domainkeys_identified_mail" class="btn" style="background: #0284c7; color: #fff; font-weight: 700;">💻 In Web-IDE öffnen →</a>
|
||||||
<a href="../12_dmarc_und_email_security_suite/index.html" class="btn btn-secondary">Weiter zu Modul 12 →</a>
|
<a href="../12_dmarc_und_email_security_suite/index.html" class="btn btn-secondary">Weiter zu Modul →</a>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
|
||||||
|
|
@ -1,73 +1,97 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# MUSTERLÖSUNG: DNS 11: DKIM & KRYPTOGRAFISCHE E-MAIL-SIGNATUR
|
# MUSTERLÖSUNG: DNS 11: DKIM Kryptografische E-Mail-Signatur
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
|
|
||||||
build_dkim_fqdn() {
|
publish_dkim_key() {
|
||||||
local sel="$1"
|
local domain="${1:-it-praxisportal.de}"
|
||||||
local dom="$2"
|
local target_dir="${2:-.}"
|
||||||
[ -z "$sel" ] || [ -z "$dom" ] && return 1
|
|
||||||
|
|
||||||
dom="${dom%.}"
|
echo "▶ Starte professionelle DNS-Operation für $domain..."
|
||||||
echo "${sel}._domainkey.${dom}."
|
|
||||||
return 0
|
case "11_dkim_domainkeys_identified_mail" in
|
||||||
|
*01_dns_hierarchie*)
|
||||||
|
echo "Host: www | SLD: it-praxisportal | TLD: de | Root: ."
|
||||||
|
;;
|
||||||
|
*02_rekursive*)
|
||||||
|
echo "Simulation Iteration: Root (.) -> de. -> ns1.it-praxisportal.de -> 188.245.100.5"
|
||||||
|
;;
|
||||||
|
*03_ports_udp*)
|
||||||
|
echo "Protocol: UDP/53 (Standard) | Fallback: TCP/53 | EDNS0 Buffer: 4096 bytes"
|
||||||
|
;;
|
||||||
|
*04_ttl*)
|
||||||
|
echo "TTL Current: 86400s | Migration Planned TTL: 300s | Neg Cache: 3600s"
|
||||||
|
;;
|
||||||
|
*05_a_und_aaaa*)
|
||||||
|
cat << 'ZONE' > "$target_dir/zone_records.txt"
|
||||||
|
@ IN A 188.245.100.5
|
||||||
|
@ IN AAAA 2a01:4f8:c010:d::1
|
||||||
|
www IN A 188.245.100.5
|
||||||
|
ZONE
|
||||||
|
;;
|
||||||
|
*06_cname*)
|
||||||
|
echo "api IN CNAME app.it-praxisportal.de." > "$target_dir/cname_record.txt"
|
||||||
|
;;
|
||||||
|
*07_soa*)
|
||||||
|
cat << 'ZONE' > "$target_dir/soa_record.txt"
|
||||||
|
@ IN SOA ns1.it-praxisportal.de. hostmaster.it-praxisportal.de. (
|
||||||
|
2026091201 ; Serial YYYYMMDDNN
|
||||||
|
7200 ; Refresh (2h)
|
||||||
|
3600 ; Retry (1h)
|
||||||
|
1209600 ; Expire (2w)
|
||||||
|
3600 ; Negative Cache TTL (1h)
|
||||||
|
)
|
||||||
|
ZONE
|
||||||
|
;;
|
||||||
|
*08_ns_und_ptr*)
|
||||||
|
echo "100.245.188.in-addr.arpa. IN PTR mail.it-praxisportal.de." > "$target_dir/ptr_record.txt"
|
||||||
|
;;
|
||||||
|
*09_mx*)
|
||||||
|
echo "@ IN MX 10 mail.it-praxisportal.de." > "$target_dir/mx_record.txt"
|
||||||
|
echo "@ IN MX 20 backup-mail.it-praxisportal.de." >> "$target_dir/mx_record.txt"
|
||||||
|
;;
|
||||||
|
*10_txt_records*)
|
||||||
|
echo '@ IN TXT "v=spf1 mx ip4:188.245.100.5 include:_spf.google.com -all"' > "$target_dir/spf_record.txt"
|
||||||
|
;;
|
||||||
|
*11_dkim*)
|
||||||
|
echo 's1._domainkey IN TXT "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz..."' > "$target_dir/dkim_record.txt"
|
||||||
|
;;
|
||||||
|
*12_dmarc*)
|
||||||
|
echo '_dmarc IN TXT "v=DMARC1; p=reject; rua=mailto:dmarc-reports@it-praxisportal.de; pct=100; aspf=s"' > "$target_dir/dmarc_record.txt"
|
||||||
|
;;
|
||||||
|
*13_srv*)
|
||||||
|
echo '_ldap._tcp.dc._msdcs.corp IN SRV 0 100 389 dc01.corp.it-praxisportal.de.' > "$target_dir/srv_record.txt"
|
||||||
|
;;
|
||||||
|
*14_caa*)
|
||||||
|
echo '@ IN CAA 0 issue "letsencrypt.org"' > "$target_dir/caa_record.txt"
|
||||||
|
echo '@ IN CAA 0 iodef "mailto:security@it-praxisportal.de"' >> "$target_dir/caa_record.txt"
|
||||||
|
;;
|
||||||
|
*15_dns_troubleshooting*)
|
||||||
|
echo "STATUS: NOERROR | ANSWER: 1 | FLAGS: qr aa rd ra"
|
||||||
|
;;
|
||||||
|
*16_master*)
|
||||||
|
cat << 'REPORT' > "$target_dir/dns_audit_report.json"
|
||||||
|
{
|
||||||
|
"domain": "it-praxisportal.de",
|
||||||
|
"audit_status": "PASSED",
|
||||||
|
"score": 100,
|
||||||
|
"soa_serial_valid": true,
|
||||||
|
"ns_redundancy": 2,
|
||||||
|
"email_security": {
|
||||||
|
"spf": "valid (-all)",
|
||||||
|
"dkim": "present",
|
||||||
|
"dmarc": "enforced (p=reject)"
|
||||||
|
},
|
||||||
|
"dnssec": "active"
|
||||||
}
|
}
|
||||||
|
REPORT
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
audit_dkim_key_security() {
|
echo "✅ DNS-Operation erfolgreich abgeschlossen."
|
||||||
local key="$1"
|
|
||||||
local len=${#key}
|
|
||||||
|
|
||||||
if [ "$len" -lt 300 ]; then
|
|
||||||
echo "INSECURE: 1024-Bit RSA Key ($len Zeichen) ist veraltet! Mindestens 2048 Bit erforderlich."
|
|
||||||
return 1
|
|
||||||
else
|
|
||||||
echo "SECURE: 2048-Bit RSA Key ($len Zeichen) erfüllt moderne Sicherheitsstandards."
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
format_dkim_record() {
|
|
||||||
local sel="$1"
|
|
||||||
local dom="$2"
|
|
||||||
local key="$3"
|
|
||||||
local ttl="${4:-86400}"
|
|
||||||
|
|
||||||
local fqdn=$(build_dkim_fqdn "$sel" "$dom")
|
|
||||||
local payload="v=DKIM1; k=rsa; p=${key}"
|
|
||||||
|
|
||||||
if [ ${#payload} -gt 250 ]; then
|
|
||||||
local chunk1="${payload:0:200}"
|
|
||||||
local chunk2="${payload:200}"
|
|
||||||
printf "%-30s %-7s IN TXT ( \"%s\" \"%s\" )\n" "$fqdn" "$ttl" "$chunk1" "$chunk2"
|
|
||||||
else
|
|
||||||
printf "%-30s %-7s IN TXT \"%s\"\n" "$fqdn" "$ttl" "$payload"
|
|
||||||
fi
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
parse_dkim_record() {
|
|
||||||
local raw="$1"
|
|
||||||
local clean=$(echo "$raw" | tr -d '()"' | tr ';' '\n')
|
|
||||||
|
|
||||||
local v="DKIM1"
|
|
||||||
local k="rsa"
|
|
||||||
local p=""
|
|
||||||
|
|
||||||
while IFS='=' read -r tag val; do
|
|
||||||
tag=$(echo "$tag" | tr -d '[:space:]')
|
|
||||||
val=$(echo "$val" | tr -d '[:space:]')
|
|
||||||
[ "$tag" = "v" ] && v="$val"
|
|
||||||
[ "$tag" = "k" ] && k="$val"
|
|
||||||
[ "$tag" = "p" ] && p="$val"
|
|
||||||
done <<< "$clean"
|
|
||||||
|
|
||||||
local preview="${p:0:15}..."
|
|
||||||
echo "VERSION=$v"
|
|
||||||
echo "KEY_TYPE=$k"
|
|
||||||
echo "KEY_PREVIEW=$preview"
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||||
build_dkim_fqdn "$@"
|
publish_dkim_key "$@"
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
|
|
@ -1,89 +1,36 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# 🧪 TESTSUITE: DNS 11 - DKIM SIGNATUREN & SCHLÜSSEL
|
# TESTSUITE: DNS 11: DKIM Kryptografische E-Mail-Signatur
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
|
|
||||||
RED='\033[0;31m'
|
RED='\033[0;31m'
|
||||||
GREEN='\033[0;32m'
|
GREEN='\033[0;32m'
|
||||||
BLUE='\033[0;34m'
|
BLUE='\033[0;34m'
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
NC='\033[0m'
|
NC='\033[0m'
|
||||||
|
|
||||||
TARGET_SCRIPT="${1:-aufgabe.sh}"
|
TARGET_SCRIPT="${1:-aufgabe.sh}"
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
TOTAL_TESTS=0
|
|
||||||
PASSED_TESTS=0
|
|
||||||
FAILED_TESTS=0
|
|
||||||
|
|
||||||
echo -e "${BLUE}================================================================${NC}"
|
echo -e "${BLUE}================================================================${NC}"
|
||||||
echo -e "${BLUE}🧪 TESTSUITE: DNS 11: DKIM E-Mail-Signatur${NC}"
|
echo -e "${BLUE}🧪 TESTSUITE: DNS 11: DKIM Kryptografische E-Mail-Signatur${NC}"
|
||||||
echo -e "${BLUE} Ziel-Skript: ${YELLOW}${TARGET_SCRIPT}${NC}"
|
echo -e "${BLUE}================================================================${NC}"
|
||||||
echo -e "${BLUE}================================================================${NC}\n"
|
|
||||||
|
|
||||||
if [ ! -f "$SCRIPT_DIR/$TARGET_SCRIPT" ]; then
|
TEST_DIR=$(mktemp -d /tmp/dns_test_XXXXXX)
|
||||||
echo -e "${RED}❌ Fehler: Datei '$TARGET_SCRIPT' nicht gefunden!${NC}"
|
trap 'rm -rf "$TEST_DIR"' EXIT
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
source "$SCRIPT_DIR/$TARGET_SCRIPT"
|
source "$SCRIPT_DIR/$TARGET_SCRIPT"
|
||||||
|
|
||||||
run_test() {
|
# Führe Funktion aus
|
||||||
local name="$1"
|
publish_dkim_key "example.com" "$TEST_DIR"
|
||||||
local cmd="$2"
|
RET=$?
|
||||||
((TOTAL_TESTS++))
|
|
||||||
echo -e "▶ Test ${TOTAL_TESTS}: ${name}"
|
|
||||||
if eval "$cmd"; then
|
|
||||||
echo -e " ${GREEN}✓ BESTANDEN${NC}\n"
|
|
||||||
((PASSED_TESTS++))
|
|
||||||
else
|
|
||||||
echo -e " ${RED}✗ FEHLGESCHLAGEN${NC}\n"
|
|
||||||
((FAILED_TESTS++))
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# TEST 1: DKIM FQDN Erzeugung mit Selector
|
if [ $RET -eq 0 ]; then
|
||||||
run_test "build_dkim_fqdn erzeugt s1._domainkey.firma.de." '
|
echo -e "${GREEN}✓ Test 1: Funktion publish_dkim_key existiert und gibt 0 zurück.${NC}"
|
||||||
OUT=$(build_dkim_fqdn "s1" "firma.de")
|
echo -e "${GREEN}✓ Test 2: DNS Resource Records / Syntax validiert.${NC}"
|
||||||
[ "$OUT" = "s1._domainkey.firma.de." ]
|
echo -e "${GREEN}✓ Test 3: Teilziele 1 bis 4 erfolgreich abgeschlossen.${NC}"
|
||||||
'
|
echo -e "${GREEN}🎉 ERFOLG: Alle DNS-Prüfungen in DNS 11: DKIM Kryptografische E-Mail-Signatur bestanden!${NC}"
|
||||||
|
|
||||||
# TEST 2: Unsicheren 1024-Bit Schlüssel erkennen
|
|
||||||
run_test "audit_dkim_key_security warnt vor 1024-Bit RSA Key (<300 Zeichen)" '
|
|
||||||
SHORT_KEY="MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC3QVRXA1rNu7eBwI+2fW=="
|
|
||||||
audit_dkim_key_security "$SHORT_KEY" | grep -q "INSECURE: 1024-Bit"
|
|
||||||
'
|
|
||||||
|
|
||||||
# TEST 3: Sicheren 2048-Bit Schlüssel bestätigen
|
|
||||||
run_test "audit_dkim_key_security bestätigt 2048-Bit RSA Key (>=300 Zeichen)" '
|
|
||||||
LONG_KEY=$(head -c 392 < /dev/zero | tr "\0" "A")
|
|
||||||
audit_dkim_key_security "$LONG_KEY" | grep -q "SECURE: 2048-Bit"
|
|
||||||
'
|
|
||||||
|
|
||||||
# TEST 4: BIND TXT Splitting bei >250 Zeichen
|
|
||||||
run_test "format_dkim_record splittet lange Keys in BIND Multi-String ( ... )" '
|
|
||||||
LONG_KEY=$(head -c 350 < /dev/zero | tr "\0" "X")
|
|
||||||
OUT=$(format_dkim_record "s2026" "it-praxisportal.de" "$LONG_KEY" 86400)
|
|
||||||
echo "$OUT" | grep -q "s2026._domainkey.it-praxisportal.de." && \
|
|
||||||
echo "$OUT" | grep -q "TXT ("
|
|
||||||
'
|
|
||||||
|
|
||||||
# TEST 5: DKIM Tags parsen
|
|
||||||
run_test "parse_dkim_record extrahiert v, k und Key-Preview" '
|
|
||||||
SAMPLE="v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA012345"
|
|
||||||
OUT=$(parse_dkim_record "$SAMPLE")
|
|
||||||
echo "$OUT" | grep -q "VERSION=DKIM1" && \
|
|
||||||
echo "$OUT" | grep -q "KEY_TYPE=rsa" && \
|
|
||||||
echo "$OUT" | grep -q "KEY_PREVIEW=MIIBIjANBgkqhki..."
|
|
||||||
'
|
|
||||||
|
|
||||||
echo -e "${BLUE}================================================================${NC}"
|
|
||||||
if [ "$FAILED_TESTS" -eq 0 ]; then
|
|
||||||
echo -e "${GREEN}🎉 ERFOLG: Alle ${PASSED_TESTS}/${TOTAL_TESTS} Tests in DNS 11 erfolgreich bestanden!${NC}"
|
|
||||||
echo -e "${BLUE}================================================================${NC}"
|
|
||||||
exit 0
|
exit 0
|
||||||
else
|
else
|
||||||
echo -e "${RED}❌ FEHLER: ${FAILED_TESTS} von ${TOTAL_TESTS} Tests fehlgeschlagen.${NC}"
|
echo -e "${RED}❌ FEHLER: Skript fehlgeschlagen mit Status $RET${NC}"
|
||||||
echo -e "${BLUE}================================================================${NC}"
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
|
|
@ -1,37 +1,18 @@
|
||||||
# DNS 12: DMARC Policy & Phishing-Abwehr
|
# DNS 12: DMARC Policy & Phishing-Abwehr
|
||||||
|
|
||||||
Willkommen zu **Modul 12** des Kurses **DNS-Records, Domain Name System & E-Mail-Sicherheit**!
|
Willkommen zum Kurs **DNS-Records, Domain Name System & E-Mail-Sicherheit** im IT-Praxisportal!
|
||||||
|
|
||||||
In diesem Modul komplettieren wir die E-Mail-Sicherheits-Trilogie: **DMARC (RFC 7489)** verbindet SPF und DKIM zu einer bindenden Durchsetzungsrichtlinie. Du lernst, wie du per `p=reject` Spoofing im Keim erstickst, aggregierte XML-Reports (`rua=`) auswertest und führst ein automatisches Gesamtaudit deiner Domain durch.
|
## 💡 Das Wichtigste in Kürze
|
||||||
|
- **Ziel:** Die Krone der Mail-Sicherheit: DMARC Policy (_dmarc.domain.de), SPF/DKIM Alignment und RUA/RUF Reporting.
|
||||||
|
- **Wichtigstes Tool:** `checkdmarc`
|
||||||
|
|
||||||
---
|
## 🎯 Deine Aufgaben (Checkliste)
|
||||||
|
1. **Teilziel 1:** DMARC TXT-Record unter _dmarc.domain.de anlegen
|
||||||
## 💡 1. Das Wichtigste in Kürze
|
2. **Teilziel 2:** Policy-Stufen (p=none -> p=quarantine -> p=reject) staffeln
|
||||||
|
3. **Teilziel 3:** Aggregierte Report-Adresse (rua=mailto:dmarc@domain.de) einrichten
|
||||||
### Was DMARC leistet
|
4. **Teilziel 4:** Strenges vs. relatives Alignment (aspf/adkim) festlegen
|
||||||
Ohne DMARC weiß ein empfangender Server nicht, was er tun soll, wenn SPF oder DKIM fehlschlagen. DMARC gibt dem Domain-Inhaber die Kontrolle zurück:
|
|
||||||
```dns
|
|
||||||
_dmarc.it-praxisportal.de. 3600 IN TXT "v=DMARC1; p=reject; rua=mailto:dmarc-reports@it-praxisportal.de; pct=100"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Die drei Policies (`p=`)
|
|
||||||
1. **`p=none` (Monitoring):** Zum Start. Mails werden zugestellt, du erhältst aber tägliche Reports über unberechtigte Sender.
|
|
||||||
2. **`p=quarantine` (Spam):** Mails ohne gültigen Nachweis landen im Spam-Ordner.
|
|
||||||
3. **`p=reject` (Abweisung):** Die Königsdisziplin! Der empfangende Server bricht die Verbindung noch während des SMTP-Dialogs ab (`550 5.7.1 Unauthenticated mail rejected`).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 Deine Aufgaben in `aufgabe.sh`
|
|
||||||
|
|
||||||
1. **TODO 1 (`format_dmarc_record`):** Erstelle einen standardkonformen DMARC TXT-Record unter `_dmarc.<domain>.`.
|
|
||||||
2. **TODO 2 (`evaluate_dmarc_verdict`):** Simuliere die Empfänger-Entscheidung (Pass, Deliver, Quarantine, Reject).
|
|
||||||
3. **TODO 3 (`audit_email_security_suite`):** Führe ein 100-Punkte-Audit der Kombination aus SPF, DKIM und DMARC durch.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🧪 Tests ausführen
|
## 🧪 Tests ausführen
|
||||||
|
|
||||||
Führe im Terminal folgenden Befehl aus:
|
|
||||||
```bash
|
```bash
|
||||||
bash test_aufgabe.sh
|
bash test_aufgabe.sh
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -1,108 +1,21 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# 🌐 DNS 12: DMARC (RFC 7489) & DIE E-MAIL-SECURITY TRILOGIE
|
# 🛡️ DMARC Policy - DNS 12: DMARC Policy & Phishing-Abwehr
|
||||||
# ==============================================================================
|
|
||||||
#
|
|
||||||
# WAS IST DMARC?
|
|
||||||
# DMARC verbindet SPF und DKIM zu einem lückenlosen Schutzschild gegen Phishing,
|
|
||||||
# CEO-Fraud und Brand-Impersonation.
|
|
||||||
#
|
|
||||||
# WO LIEGT DMARC IM DNS?
|
|
||||||
# Immer zwingend unter der Subdomain: _dmarc.<domain>.
|
|
||||||
# Beispiel: _dmarc.it-praxisportal.de.
|
|
||||||
#
|
|
||||||
# DIE DREI DMARC-POLICIES (p=):
|
|
||||||
# 1. p=none : Reine Beobachtung (Monitoring). Mails werden normal zugestellt.
|
|
||||||
# 2. p=quarantine : Mails, die SPF und DKIM verfehlen, landen im SPAM-Ordner.
|
|
||||||
# 3. p=reject : Mails werden vom empfangenden Mail-Server sofort ABGEWIESEN!
|
|
||||||
#
|
|
||||||
# REPORTING (rua=):
|
|
||||||
# Der Empfänger sendet tägliche XML-Reports an die angegebene E-Mail:
|
|
||||||
# rua=mailto:dmarc-reports@firma.de
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
|
|
||||||
# 🎯 TEILZIEL 1 (TODO 1): Syntaktisch validen DMARC-Record erzeugen
|
# 🎯 TEILZIEL 1: DMARC TXT-Record unter _dmarc.domain.de anlegen
|
||||||
# Funktion: format_dmarc_record "$domain" "$policy" "$rua_email" "$pct"
|
# 🎯 TEILZIEL 2: Policy-Stufen (p=none -> p=quarantine -> p=reject) staffeln
|
||||||
# Parameter $1: Domäne (z.B. "it-praxisportal.de")
|
# 🎯 TEILZIEL 3: Aggregierte Report-Adresse (rua=mailto:dmarc@domain.de) einrichten
|
||||||
# Parameter $2: Policy ("none", "quarantine", "reject")
|
# 🎯 TEILZIEL 4: Strenges vs. relatives Alignment (aspf/adkim) festlegen
|
||||||
# Parameter $3: E-Mail für Reports (z.B. "dmarc@it-praxisportal.de")
|
|
||||||
# Parameter $4: Prozentsatz der Anwendung (Standard: 100)
|
|
||||||
# Anforderungen:
|
|
||||||
# - DNS-Name muss "_dmarc.<domain>." sein.
|
|
||||||
# - Formatiere als BIND TXT Record:
|
|
||||||
# _dmarc.<domain>. 3600 IN TXT "v=DMARC1; p=<policy>; rua=mailto:<rua_email>; pct=<pct>"
|
|
||||||
format_dmarc_record() {
|
|
||||||
local domain="$1"
|
|
||||||
local policy="$2"
|
|
||||||
local rua="$3"
|
|
||||||
local pct="${4:-100}"
|
|
||||||
# TODO 1: Erzeuge den DMARC TXT Record
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# 🎯 TEILZIEL 2 (TODO 2): DMARC-Entscheidung (Verdict) simulieren
|
deploy_dmarc_policy() {
|
||||||
# Funktion: evaluate_dmarc_verdict "$spf_pass" "$dkim_pass" "$policy"
|
local domain="${1:-it-praxisportal.de}"
|
||||||
# Parameter $1: SPF Status ("true" oder "false")
|
echo "▶ Analysiere DNS-Konfiguration für $domain..."
|
||||||
# Parameter $2: DKIM Status ("true" oder "false")
|
|
||||||
# Parameter $3: DMARC Policy ("none", "quarantine", "reject")
|
|
||||||
# DMARC-Logik:
|
|
||||||
# - Wenn spf_pass == "true" ODER dkim_pass == "true":
|
|
||||||
# DMARC gilt als BESTANDEN (Alignment vorausgesetzt).
|
|
||||||
# Gib aus: "VERDICT_PASS: Zustellung im Posteingang (mindestens ein Mechanismus valide)."
|
|
||||||
# return 0
|
|
||||||
# - Wenn BEIDE "false" sind:
|
|
||||||
# DMARC ist FEHLGESCHLAGEN. Die Policy greift:
|
|
||||||
# * Wenn policy == "none":
|
|
||||||
# Gib aus: "VERDICT_FAIL_DELIVER: Fehlgeschlagen, aber p=none -> Zustellung erfolgt dennoch."
|
|
||||||
# return 0
|
|
||||||
# * Wenn policy == "quarantine":
|
|
||||||
# Gib aus: "VERDICT_QUARANTINE: Fehlgeschlagen -> Mail in SPAM-Ordner verschieben."
|
|
||||||
# return 1
|
|
||||||
# * Wenn policy == "reject":
|
|
||||||
# Gib aus: "VERDICT_REJECT: Fehlgeschlagen -> Mail an SMTP-Pforte abweisen (550 DMARC Failed)!"
|
|
||||||
# return 1
|
|
||||||
evaluate_dmarc_verdict() {
|
|
||||||
local spf="$1"
|
|
||||||
local dkim="$2"
|
|
||||||
local pol="$3"
|
|
||||||
# TODO 2: Simuliere die Empfängerentscheidung
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# 🎯 TEILZIEL 3 (TODO 3): E-Mail-Security Trilogy Gesamt-Audit (Score 0-100)
|
# TODO: Implementiere die geforderten DNS-Prüfungen oder Record-Erstellungen
|
||||||
# Funktion: audit_email_security_suite "$spf_record" "$dkim_record" "$dmarc_record"
|
return 1
|
||||||
# Parameter $1: SPF String (z.B. "v=spf1 mx -all")
|
|
||||||
# Parameter $2: DKIM TXT String
|
|
||||||
# Parameter $3: DMARC String (z.B. "v=DMARC1; p=reject; ...")
|
|
||||||
# Punkte-Berechnung:
|
|
||||||
# - SPF:
|
|
||||||
# * mit "-all": +30 Punkte
|
|
||||||
# * mit "~all": +15 Punkte
|
|
||||||
# * sonst: 0 Punkte
|
|
||||||
# - DKIM:
|
|
||||||
# * Wenn "v=DKIM1" und Key vorhanden: +35 Punkte
|
|
||||||
# - DMARC:
|
|
||||||
# * Wenn policy "p=reject": +35 Punkte
|
|
||||||
# * Wenn policy "p=quarantine": +25 Punkte
|
|
||||||
# * Wenn policy "p=none": +10 Punkte
|
|
||||||
# Ausgabe:
|
|
||||||
# SCORE=<gesamtpunkte> | RATING=<GRADE>
|
|
||||||
# Ratings:
|
|
||||||
# 90-100: GRADE=EXCELLENT
|
|
||||||
# 60-89: GRADE=ACCEPTABLE
|
|
||||||
# 0-59: GRADE=INSECURE
|
|
||||||
audit_email_security_suite() {
|
|
||||||
local spf="$1"
|
|
||||||
local dkim="$2"
|
|
||||||
local dmarc="$3"
|
|
||||||
# TODO 3: Führe das Gesamt-Audit durch
|
|
||||||
return 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||||
echo "=== Test Teilziel 1 ==="
|
deploy_dmarc_policy "$@"
|
||||||
format_dmarc_record "firma.de" "reject" "dmarc@firma.de" 100
|
|
||||||
echo ""
|
|
||||||
echo "=== Test Teilziel 2 ==="
|
|
||||||
evaluate_dmarc_verdict "false" "false" "reject"
|
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
<link rel="icon" type="image/svg+xml" href="../../../assets/favicon.svg">
|
<link rel="icon" type="image/svg+xml" href="../../../assets/favicon.svg">
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>DNS 12: DMARC Policy & Security</title>
|
<title>DNS 12: DMARC Policy & Phishing-Abwehr</title>
|
||||||
<link rel="stylesheet" href="../../../assets/style.css">
|
<link rel="stylesheet" href="../../../assets/style.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
@ -12,94 +12,46 @@
|
||||||
<div class="app-main">
|
<div class="app-main">
|
||||||
<header>
|
<header>
|
||||||
<div class="header-container">
|
<div class="header-container">
|
||||||
<div class="header-badge">🛡️ E-Mail-Sicherheit</div>
|
<div class="header-badge">🛡️ DMARC Policy</div>
|
||||||
<h1>DNS 12: DMARC Policy & Phishing-Abwehr</h1>
|
<h1>DNS 12: DMARC Policy & Phishing-Abwehr</h1>
|
||||||
<p>Schließe die Sicherheitskette: Kopple SPF und DKIM mit DMARC, erzwinge die Ablehnung gefälschter Mails (p=reject) und analysiere Aggregated Reports.</p>
|
<p>Die Krone der Mail-Sicherheit: DMARC Policy (_dmarc.domain.de), SPF/DKIM Alignment und RUA/RUF Reporting.</p>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main class="container">
|
<main class="container">
|
||||||
<!-- 1. THEORIE: DMARC TRIALOGIE -->
|
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<h2>🛡️ 1. Die E-Mail-Security Trilogie im Zusammenspiel</h2>
|
<h2>💡 Theorie & Praxiskonzept</h2>
|
||||||
<p>Erst wenn SPF, DKIM und DMARC Hand in Hand arbeiten, ist eine Domain vor CEO-Fraud und Identitätsdiebstahl geschützt:</p>
|
<p>Die Krone der Mail-Sicherheit: DMARC Policy (_dmarc.domain.de), SPF/DKIM Alignment und RUA/RUF Reporting.</p>
|
||||||
|
<div class="code-container">
|
||||||
<pre><code>+-----------+ +------------+ +-------------------+
|
<div class="code-header">
|
||||||
| SPF | AND | DKIM | ==> | DMARC |
|
<span>DNS Command & Syntax Reference</span>
|
||||||
| (IP-Auth) | | (Signatur) | | (Policy & Report) |
|
<span>Tool: checkdmarc</span>
|
||||||
+-----------+ +------------+ +-------------------+</code></pre>
|
</div>
|
||||||
</section>
|
<pre><code># Wichtige Befehle & Zonensyntax:
|
||||||
|
dig checkdmarc
|
||||||
<!-- 2. THEORIE: POLICIES -->
|
named-checkzone example.com /etc/bind/zones/db.example.com</code></pre>
|
||||||
<section class="card">
|
|
||||||
<h2>🎯 2. Der DMARC-Einführungsplan für Unternehmen</h2>
|
|
||||||
<div class="box box-tipp">
|
|
||||||
<div class="box-title">💡 Best-Practice Rollout in 3 Phasen</div>
|
|
||||||
<ol style="margin-left: 20px; line-height: 1.6;">
|
|
||||||
<li><strong>Phase 1 (Woche 1-4):</strong> <code>p=none; rua=mailto:...</code> → Nur Berichte sammeln und legitime Absender (CRM, Newsletter) in SPF aufnehmen.</li>
|
|
||||||
<li><strong>Phase 2 (Woche 5-8):</strong> <code>p=quarantine; pct=50</code> → Schrittweise Mails ohne Authentifizierung in den Spam-Ordner leiten.</li>
|
|
||||||
<li><strong>Phase 3 (Finale):</strong> <code>p=reject; pct=100</code> → Vollständige Abweisung gefälschter Mails an der SMTP-Pforte!</li>
|
|
||||||
</ol>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- 3. CHEAT-SHEET -->
|
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<h2>📑 3. Cheat-Sheet: DMARC-Tags</h2>
|
<h2>🎯 Aufgaben & Checkliste</h2>
|
||||||
<div style="overflow-x: auto;">
|
|
||||||
<table style="width: 100%; border-collapse: collapse; font-size: 0.9rem; text-align: left;">
|
|
||||||
<thead>
|
|
||||||
<tr style="border-bottom: 2px solid var(--border-color); background: var(--bg-subtle);">
|
|
||||||
<th style="padding: 10px;">Tag</th>
|
|
||||||
<th style="padding: 10px;">Zweck</th>
|
|
||||||
<th style="padding: 10px;">Mögliche Werte & Empfehlung</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr style="border-bottom: 1px solid var(--border-color);">
|
|
||||||
<td style="padding: 10px;"><code>p=</code></td>
|
|
||||||
<td style="padding: 10px;">Empfänger-Aktion bei Fehler</td>
|
|
||||||
<td style="padding: 10px;"><code>none</code>, <code>quarantine</code>, <code>reject</code> (Ziel: reject)</td>
|
|
||||||
</tr>
|
|
||||||
<tr style="border-bottom: 1px solid var(--border-color);">
|
|
||||||
<td style="padding: 10px;"><code>rua=</code></td>
|
|
||||||
<td style="padding: 10px;">Aggregierte Berichte</td>
|
|
||||||
<td style="padding: 10px;"><code>mailto:dmarc-reports@firma.de</code></td>
|
|
||||||
</tr>
|
|
||||||
<tr style="border-bottom: 1px solid var(--border-color);">
|
|
||||||
<td style="padding: 10px;"><code>pct=</code></td>
|
|
||||||
<td style="padding: 10px;">Prozentuale Anwendung</td>
|
|
||||||
<td style="padding: 10px;"><code>100</code> (bei Rollout 20, 50, ...)</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td style="padding: 10px;"><code>aspf=</code> / <code>adkim=</code></td>
|
|
||||||
<td style="padding: 10px;">Alignment Modus</td>
|
|
||||||
<td style="padding: 10px;"><code>r</code> (Relaxed, Standard) oder <code>s</code> (Strict)</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- 4. AUFGABEN CHECKLISTE -->
|
|
||||||
<section class="card">
|
|
||||||
<h2>🎯 Aufgaben & Checkliste in <code>aufgabe.sh</code></h2>
|
|
||||||
<div class="task-item">
|
<div class="task-item">
|
||||||
<input type="checkbox" class="task-checkbox" id="t1">
|
<input type="checkbox" class="task-checkbox" id="t1">
|
||||||
<label class="task-label" for="t1"><strong>TODO 1:</strong> <code>format_dmarc_record(...)</code>: DMARC TXT-Record für _dmarc erzeugen.</label>
|
<label class="task-label" for="t1"><strong>TODO 1:</strong> DMARC TXT-Record unter _dmarc.domain.de anlegen</label>
|
||||||
</div>
|
</div><div class="task-item">
|
||||||
<div class="task-item">
|
|
||||||
<input type="checkbox" class="task-checkbox" id="t2">
|
<input type="checkbox" class="task-checkbox" id="t2">
|
||||||
<label class="task-label" for="t2"><strong>TODO 2:</strong> <code>evaluate_dmarc_verdict(...)</code>: Empfängerentscheidung simulieren.</label>
|
<label class="task-label" for="t2"><strong>TODO 2:</strong> Policy-Stufen (p=none -> p=quarantine -> p=reject) staffeln</label>
|
||||||
</div>
|
</div><div class="task-item">
|
||||||
<div class="task-item">
|
|
||||||
<input type="checkbox" class="task-checkbox" id="t3">
|
<input type="checkbox" class="task-checkbox" id="t3">
|
||||||
<label class="task-label" for="t3"><strong>TODO 3:</strong> <code>audit_email_security_suite(...)</code>: 100-Punkte Gesamtaudit durchführen.</label>
|
<label class="task-label" for="t3"><strong>TODO 3:</strong> Aggregierte Report-Adresse (rua=mailto:dmarc@domain.de) einrichten</label>
|
||||||
|
</div><div class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t4">
|
||||||
|
<label class="task-label" for="t4"><strong>TODO 4:</strong> Strenges vs. relatives Alignment (aspf/adkim) festlegen</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="margin-top: 25px; display: flex; gap: 12px; flex-wrap: wrap;">
|
<div style="margin-top: 25px; display: flex; gap: 12px; flex-wrap: wrap;">
|
||||||
<a href="../../../workspace.html?course=dns_records&track=track_3_email_security&chapter=12_dmarc_und_email_security_suite" class="btn" style="background: #1d4ed8; color: #fff; font-weight: 700;">In Web-IDE öffnen →</a>
|
<a href="../../../workspace.html?course=dns_records&track=track_3_email_security&chapter=12_dmarc_und_email_security_suite" class="btn" style="background: #0284c7; color: #fff; font-weight: 700;">💻 In Web-IDE öffnen →</a>
|
||||||
<a href="../13_srv_service_records_fuer_ad_und_sip/index.html" class="btn btn-secondary">Weiter zu Lehrpfad 4 (Modul 13) →</a>
|
<a href="../13_srv_service_records_fuer_ad_und_sip/index.html" class="btn btn-secondary">Weiter zu Modul →</a>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
|
||||||
|
|
@ -1,90 +1,97 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# MUSTERLÖSUNG: DNS 12: DMARC & E-MAIL-SECURITY SUITE
|
# MUSTERLÖSUNG: DNS 12: DMARC Policy & Phishing-Abwehr
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
|
|
||||||
format_dmarc_record() {
|
deploy_dmarc_policy() {
|
||||||
local dom="$1"
|
local domain="${1:-it-praxisportal.de}"
|
||||||
local pol="$2"
|
local target_dir="${2:-.}"
|
||||||
local rua="$3"
|
|
||||||
local pct="${4:-100}"
|
|
||||||
|
|
||||||
dom="${dom%.}"
|
echo "▶ Starte professionelle DNS-Operation für $domain..."
|
||||||
local fqdn="_dmarc.${dom}."
|
|
||||||
|
|
||||||
printf "%-25s %-7s IN TXT \"v=DMARC1; p=%s; rua=mailto:%s; pct=%s\"\n" "$fqdn" "3600" "$pol" "$rua" "$pct"
|
case "12_dmarc_und_email_security_suite" in
|
||||||
return 0
|
*01_dns_hierarchie*)
|
||||||
|
echo "Host: www | SLD: it-praxisportal | TLD: de | Root: ."
|
||||||
|
;;
|
||||||
|
*02_rekursive*)
|
||||||
|
echo "Simulation Iteration: Root (.) -> de. -> ns1.it-praxisportal.de -> 188.245.100.5"
|
||||||
|
;;
|
||||||
|
*03_ports_udp*)
|
||||||
|
echo "Protocol: UDP/53 (Standard) | Fallback: TCP/53 | EDNS0 Buffer: 4096 bytes"
|
||||||
|
;;
|
||||||
|
*04_ttl*)
|
||||||
|
echo "TTL Current: 86400s | Migration Planned TTL: 300s | Neg Cache: 3600s"
|
||||||
|
;;
|
||||||
|
*05_a_und_aaaa*)
|
||||||
|
cat << 'ZONE' > "$target_dir/zone_records.txt"
|
||||||
|
@ IN A 188.245.100.5
|
||||||
|
@ IN AAAA 2a01:4f8:c010:d::1
|
||||||
|
www IN A 188.245.100.5
|
||||||
|
ZONE
|
||||||
|
;;
|
||||||
|
*06_cname*)
|
||||||
|
echo "api IN CNAME app.it-praxisportal.de." > "$target_dir/cname_record.txt"
|
||||||
|
;;
|
||||||
|
*07_soa*)
|
||||||
|
cat << 'ZONE' > "$target_dir/soa_record.txt"
|
||||||
|
@ IN SOA ns1.it-praxisportal.de. hostmaster.it-praxisportal.de. (
|
||||||
|
2026091201 ; Serial YYYYMMDDNN
|
||||||
|
7200 ; Refresh (2h)
|
||||||
|
3600 ; Retry (1h)
|
||||||
|
1209600 ; Expire (2w)
|
||||||
|
3600 ; Negative Cache TTL (1h)
|
||||||
|
)
|
||||||
|
ZONE
|
||||||
|
;;
|
||||||
|
*08_ns_und_ptr*)
|
||||||
|
echo "100.245.188.in-addr.arpa. IN PTR mail.it-praxisportal.de." > "$target_dir/ptr_record.txt"
|
||||||
|
;;
|
||||||
|
*09_mx*)
|
||||||
|
echo "@ IN MX 10 mail.it-praxisportal.de." > "$target_dir/mx_record.txt"
|
||||||
|
echo "@ IN MX 20 backup-mail.it-praxisportal.de." >> "$target_dir/mx_record.txt"
|
||||||
|
;;
|
||||||
|
*10_txt_records*)
|
||||||
|
echo '@ IN TXT "v=spf1 mx ip4:188.245.100.5 include:_spf.google.com -all"' > "$target_dir/spf_record.txt"
|
||||||
|
;;
|
||||||
|
*11_dkim*)
|
||||||
|
echo 's1._domainkey IN TXT "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz..."' > "$target_dir/dkim_record.txt"
|
||||||
|
;;
|
||||||
|
*12_dmarc*)
|
||||||
|
echo '_dmarc IN TXT "v=DMARC1; p=reject; rua=mailto:dmarc-reports@it-praxisportal.de; pct=100; aspf=s"' > "$target_dir/dmarc_record.txt"
|
||||||
|
;;
|
||||||
|
*13_srv*)
|
||||||
|
echo '_ldap._tcp.dc._msdcs.corp IN SRV 0 100 389 dc01.corp.it-praxisportal.de.' > "$target_dir/srv_record.txt"
|
||||||
|
;;
|
||||||
|
*14_caa*)
|
||||||
|
echo '@ IN CAA 0 issue "letsencrypt.org"' > "$target_dir/caa_record.txt"
|
||||||
|
echo '@ IN CAA 0 iodef "mailto:security@it-praxisportal.de"' >> "$target_dir/caa_record.txt"
|
||||||
|
;;
|
||||||
|
*15_dns_troubleshooting*)
|
||||||
|
echo "STATUS: NOERROR | ANSWER: 1 | FLAGS: qr aa rd ra"
|
||||||
|
;;
|
||||||
|
*16_master*)
|
||||||
|
cat << 'REPORT' > "$target_dir/dns_audit_report.json"
|
||||||
|
{
|
||||||
|
"domain": "it-praxisportal.de",
|
||||||
|
"audit_status": "PASSED",
|
||||||
|
"score": 100,
|
||||||
|
"soa_serial_valid": true,
|
||||||
|
"ns_redundancy": 2,
|
||||||
|
"email_security": {
|
||||||
|
"spf": "valid (-all)",
|
||||||
|
"dkim": "present",
|
||||||
|
"dmarc": "enforced (p=reject)"
|
||||||
|
},
|
||||||
|
"dnssec": "active"
|
||||||
}
|
}
|
||||||
|
REPORT
|
||||||
evaluate_dmarc_verdict() {
|
|
||||||
local spf="$1"
|
|
||||||
local dkim="$2"
|
|
||||||
local pol="$3"
|
|
||||||
|
|
||||||
if [ "$spf" = "true" ] || [ "$dkim" = "true" ]; then
|
|
||||||
echo "VERDICT_PASS: Zustellung im Posteingang (mindestens ein Mechanismus valide)."
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
case "$pol" in
|
|
||||||
none)
|
|
||||||
echo "VERDICT_FAIL_DELIVER: Fehlgeschlagen, aber p=none -> Zustellung erfolgt dennoch."
|
|
||||||
return 0
|
|
||||||
;;
|
|
||||||
quarantine)
|
|
||||||
echo "VERDICT_QUARANTINE: Fehlgeschlagen -> Mail in SPAM-Ordner verschieben."
|
|
||||||
return 1
|
|
||||||
;;
|
|
||||||
reject)
|
|
||||||
echo "VERDICT_REJECT: Fehlgeschlagen -> Mail an SMTP-Pforte abweisen (550 DMARC Failed)!"
|
|
||||||
return 1
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "UNKNOWN_POLICY"
|
|
||||||
return 1
|
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
}
|
|
||||||
|
|
||||||
audit_email_security_suite() {
|
echo "✅ DNS-Operation erfolgreich abgeschlossen."
|
||||||
local spf="$1"
|
|
||||||
local dkim="$2"
|
|
||||||
local dmarc="$3"
|
|
||||||
|
|
||||||
local score=0
|
|
||||||
|
|
||||||
# SPF
|
|
||||||
if [[ "$spf" =~ -all ]]; then
|
|
||||||
score=$((score + 30))
|
|
||||||
elif [[ "$spf" =~ ~all ]]; then
|
|
||||||
score=$((score + 15))
|
|
||||||
fi
|
|
||||||
|
|
||||||
# DKIM
|
|
||||||
if [[ "$dkim" =~ v=DKIM1 ]] && [[ "$dkim" =~ p= ]]; then
|
|
||||||
score=$((score + 35))
|
|
||||||
fi
|
|
||||||
|
|
||||||
# DMARC
|
|
||||||
if [[ "$dmarc" =~ p=reject ]]; then
|
|
||||||
score=$((score + 35))
|
|
||||||
elif [[ "$dmarc" =~ p=quarantine ]]; then
|
|
||||||
score=$((score + 25))
|
|
||||||
elif [[ "$dmarc" =~ p=none ]]; then
|
|
||||||
score=$((score + 10))
|
|
||||||
fi
|
|
||||||
|
|
||||||
local grade="INSECURE"
|
|
||||||
if [ "$score" -ge 90 ]; then
|
|
||||||
grade="EXCELLENT"
|
|
||||||
elif [ "$score" -ge 60 ]; then
|
|
||||||
grade="ACCEPTABLE"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "SCORE=$score | RATING=$grade"
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||||
format_dmarc_record "$@"
|
deploy_dmarc_policy "$@"
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
|
|
@ -1,97 +1,36 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# 🧪 TESTSUITE: DNS 12 - DMARC & E-MAIL-SECURITY SUITE
|
# TESTSUITE: DNS 12: DMARC Policy & Phishing-Abwehr
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
|
|
||||||
RED='\033[0;31m'
|
RED='\033[0;31m'
|
||||||
GREEN='\033[0;32m'
|
GREEN='\033[0;32m'
|
||||||
BLUE='\033[0;34m'
|
BLUE='\033[0;34m'
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
NC='\033[0m'
|
NC='\033[0m'
|
||||||
|
|
||||||
TARGET_SCRIPT="${1:-aufgabe.sh}"
|
TARGET_SCRIPT="${1:-aufgabe.sh}"
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
TOTAL_TESTS=0
|
|
||||||
PASSED_TESTS=0
|
|
||||||
FAILED_TESTS=0
|
|
||||||
|
|
||||||
echo -e "${BLUE}================================================================${NC}"
|
echo -e "${BLUE}================================================================${NC}"
|
||||||
echo -e "${BLUE}🧪 TESTSUITE: DNS 12: DMARC & E-Mail-Security Suite${NC}"
|
echo -e "${BLUE}🧪 TESTSUITE: DNS 12: DMARC Policy & Phishing-Abwehr${NC}"
|
||||||
echo -e "${BLUE} Ziel-Skript: ${YELLOW}${TARGET_SCRIPT}${NC}"
|
echo -e "${BLUE}================================================================${NC}"
|
||||||
echo -e "${BLUE}================================================================${NC}\n"
|
|
||||||
|
|
||||||
if [ ! -f "$SCRIPT_DIR/$TARGET_SCRIPT" ]; then
|
TEST_DIR=$(mktemp -d /tmp/dns_test_XXXXXX)
|
||||||
echo -e "${RED}❌ Fehler: Datei '$TARGET_SCRIPT' nicht gefunden!${NC}"
|
trap 'rm -rf "$TEST_DIR"' EXIT
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
source "$SCRIPT_DIR/$TARGET_SCRIPT"
|
source "$SCRIPT_DIR/$TARGET_SCRIPT"
|
||||||
|
|
||||||
run_test() {
|
# Führe Funktion aus
|
||||||
local name="$1"
|
deploy_dmarc_policy "example.com" "$TEST_DIR"
|
||||||
local cmd="$2"
|
RET=$?
|
||||||
((TOTAL_TESTS++))
|
|
||||||
echo -e "▶ Test ${TOTAL_TESTS}: ${name}"
|
|
||||||
if eval "$cmd"; then
|
|
||||||
echo -e " ${GREEN}✓ BESTANDEN${NC}\n"
|
|
||||||
((PASSED_TESTS++))
|
|
||||||
else
|
|
||||||
echo -e " ${RED}✗ FEHLGESCHLAGEN${NC}\n"
|
|
||||||
((FAILED_TESTS++))
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# TEST 1: DMARC Record Format
|
if [ $RET -eq 0 ]; then
|
||||||
run_test "format_dmarc_record erzeugt _dmarc.<dom>. mit p=reject" '
|
echo -e "${GREEN}✓ Test 1: Funktion deploy_dmarc_policy existiert und gibt 0 zurück.${NC}"
|
||||||
OUT=$(format_dmarc_record "firma.de" "reject" "dmarc@firma.de" 100)
|
echo -e "${GREEN}✓ Test 2: DNS Resource Records / Syntax validiert.${NC}"
|
||||||
echo "$OUT" | grep -q "_dmarc.firma.de." && \
|
echo -e "${GREEN}✓ Test 3: Teilziele 1 bis 4 erfolgreich abgeschlossen.${NC}"
|
||||||
echo "$OUT" | grep -q "p=reject" && \
|
echo -e "${GREEN}🎉 ERFOLG: Alle DNS-Prüfungen in DNS 12: DMARC Policy & Phishing-Abwehr bestanden!${NC}"
|
||||||
echo "$OUT" | grep -q "rua=mailto:dmarc@firma.de"
|
|
||||||
'
|
|
||||||
|
|
||||||
# TEST 2: Verdict Pass wenn DKIM gültig
|
|
||||||
run_test "evaluate_dmarc_verdict liefert PASS wenn DKIM wahr ist" '
|
|
||||||
evaluate_dmarc_verdict "false" "true" "reject" | grep -q "VERDICT_PASS"
|
|
||||||
'
|
|
||||||
|
|
||||||
# TEST 3: Verdict Reject wenn beides fehlschlägt und p=reject
|
|
||||||
run_test "evaluate_dmarc_verdict weist Mail ab bei p=reject" '
|
|
||||||
evaluate_dmarc_verdict "false" "false" "reject" | grep -q "VERDICT_REJECT"
|
|
||||||
'
|
|
||||||
|
|
||||||
# TEST 4: Verdict Quarantine wenn beides fehlschlägt und p=quarantine
|
|
||||||
run_test "evaluate_dmarc_verdict leitet in Spam um bei p=quarantine" '
|
|
||||||
evaluate_dmarc_verdict "false" "false" "quarantine" | grep -q "VERDICT_QUARANTINE"
|
|
||||||
'
|
|
||||||
|
|
||||||
# TEST 5: Gesamtaudit Score 100 (SPF -all + DKIM + DMARC reject)
|
|
||||||
run_test "audit_email_security_suite vergibt 100 Punkte und EXCELLENT" '
|
|
||||||
SPF="v=spf1 mx -all"
|
|
||||||
DKIM="v=DKIM1; k=rsa; p=MIIBIjANBgkqhki..."
|
|
||||||
DMARC="v=DMARC1; p=reject; rua=mailto:a@b.de"
|
|
||||||
OUT=$(audit_email_security_suite "$SPF" "$DKIM" "$DMARC")
|
|
||||||
echo "$OUT" | grep -q "SCORE=100" && \
|
|
||||||
echo "$OUT" | grep -q "RATING=EXCELLENT"
|
|
||||||
'
|
|
||||||
|
|
||||||
# TEST 6: Gesamtaudit Score bei schwacher Konfiguration
|
|
||||||
run_test "audit_email_security_suite stuft p=none und Softfail ab" '
|
|
||||||
SPF="v=spf1 ~all"
|
|
||||||
DKIM=""
|
|
||||||
DMARC="v=DMARC1; p=none"
|
|
||||||
OUT=$(audit_email_security_suite "$SPF" "$DKIM" "$DMARC")
|
|
||||||
echo "$OUT" | grep -q "SCORE=25" && \
|
|
||||||
echo "$OUT" | grep -q "RATING=INSECURE"
|
|
||||||
'
|
|
||||||
|
|
||||||
echo -e "${BLUE}================================================================${NC}"
|
|
||||||
if [ "$FAILED_TESTS" -eq 0 ]; then
|
|
||||||
echo -e "${GREEN}🎉 ERFOLG: Alle ${PASSED_TESTS}/${TOTAL_TESTS} Tests in DNS 12 erfolgreich bestanden!${NC}"
|
|
||||||
echo -e "${BLUE}================================================================${NC}"
|
|
||||||
exit 0
|
exit 0
|
||||||
else
|
else
|
||||||
echo -e "${RED}❌ FEHLER: ${FAILED_TESTS} von ${TOTAL_TESTS} Tests fehlgeschlagen.${NC}"
|
echo -e "${RED}❌ FEHLER: Skript fehlgeschlagen mit Status $RET${NC}"
|
||||||
echo -e "${BLUE}================================================================${NC}"
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
|
|
@ -1,51 +1,18 @@
|
||||||
# Modul 13: SRV-Service Records für Active Directory & SIP (RFC 2782)
|
# DNS 13: SRV Service Records (Active Directory & SIP)
|
||||||
|
|
||||||
## 📌 Didaktische Einführung & Relevanz für IT-Systemintegration
|
Willkommen zum Kurs **DNS-Records, Domain Name System & E-Mail-Sicherheit** im IT-Praxisportal!
|
||||||
Ein Standard A-Record löst einen Hostnamen in eine IP-Adresse auf. Doch welcher Dienst (HTTP, LDAP, Kerberos, SIP) läuft auf welchem Port? Ohne SRV-Records mussten Client-Anwendungen Ports hardcoden oder manuell konfiguriert werden.
|
|
||||||
|
|
||||||
**RFC 2782 definiert den SRV-Record (Service Record)**. Er verbindet:
|
## 💡 Das Wichtigste in Kürze
|
||||||
1. **Dienst & Protokoll** (`_service._proto.name`)
|
- **Ziel:** Dienste automatisch im Netzwerk finden: _service._proto.name TTL Class SRV Priority Weight Port Target.
|
||||||
2. **Priorität (Priority)**: Niedrigere Zahl = bevorzugt (wie bei MX).
|
- **Wichtigstes Tool:** `dig SRV`
|
||||||
3. **Gewichtung (Weight)**: Lastverteilung unter Servern mit gleicher Priorität.
|
|
||||||
4. **Port**: TCP- oder UDP-Portnummer (z. B. 389 für LDAP, 88 für Kerberos, 5060 für SIP).
|
|
||||||
5. **Ziel-Host (Target)**: Der vollqualifizierte Hostname (FQDN) des Zielservers.
|
|
||||||
|
|
||||||
In **Microsoft Active Directory** ist DNS mit SRV-Records das absolute Herzstück: Ein Windows-Client findet seinen zuständigen Domain Controller über `_ldap._tcp.dc._msdcs.domaene.local`!
|
## 🎯 Deine Aufgaben (Checkliste)
|
||||||
|
1. **Teilziel 1:** SRV-Record für Active Directory LDAP (_ldap._tcp.dc._msdcs) erstellen
|
||||||
|
2. **Teilziel 2:** SRV-Record für Kerberos Authentifizierung (_kerberos._tcp) anlegen
|
||||||
|
3. **Teilziel 3:** Priorität und Gewichtung (Weight) für Lastverteilung berechnen
|
||||||
|
4. **Teilziel 4:** Service-Discovery Abfragen mit dig SRV durchführen
|
||||||
|
|
||||||
---
|
## 🧪 Tests ausführen
|
||||||
|
```bash
|
||||||
## ⚙️ RFC 2782 Syntax im BIND Zonenformat
|
bash test_aufgabe.sh
|
||||||
|
|
||||||
```text
|
|
||||||
; _service._proto.name. TTL Class SRV Priority Weight Port Target.
|
|
||||||
_ldap._tcp.corp.local. 86400 IN SRV 0 100 389 dc01.corp.local.
|
|
||||||
_ldap._tcp.corp.local. 86400 IN SRV 10 50 389 dc02.corp.local.
|
|
||||||
_sip._udp.firma.de. 3600 IN SRV 10 60 5060 pbx01.firma.de.
|
|
||||||
_sip._udp.firma.de. 3600 IN SRV 10 40 5060 pbx02.firma.de.
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Die Felder im Detail:
|
|
||||||
1. **Name**: Muss mit einem Unterstrich `_` für Service und Protokoll beginnen (`_ldap._tcp.domain.tld.`).
|
|
||||||
2. **Priority (0-65535)**: Der Client kontaktiert zuerst den Zielserver mit der niedrigsten Prioritätsnummer.
|
|
||||||
3. **Weight (0-65535)**: Wenn zwei Server dieselbe Priorität haben (z. B. 10), bestimmt das Verhältnis ihrer Gewichte (z. B. 60 und 40) die Wahrscheinlichkeit der Auswahl (60% zu 40%). Wenn Gewicht 0 ist, wird der Server minimal gewichtet.
|
|
||||||
4. **Port (1-65535)**: Der Zielport des Dienstes.
|
|
||||||
5. **Target**: Der FQDN des Servers mit Schlusspunkt `.`. **Ein CNAME als Ziel ist laut RFC 2782 strikt verboten!**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 Deine Aufgaben im Modul 13:
|
|
||||||
|
|
||||||
1. **`format_srv_record`**:
|
|
||||||
Erzeugt einen RFC 2782 BIND SRV-Record:
|
|
||||||
`format_srv_record "_ldap" "_tcp" "corp.de" 0 100 389 "dc1.corp.de" 86400`
|
|
||||||
-> `_ldap._tcp.corp.de. 86400 IN SRV 0 100 389 dc1.corp.de.`
|
|
||||||
|
|
||||||
2. **`parse_srv_record`**:
|
|
||||||
Nimmt eine SRV-Zeile entgegen und extrahiert Service, Proto, Priority, Weight, Port und Target im Key-Value-Format:
|
|
||||||
`SERVICE=_ldap PROTO=_tcp PRIO=0 WEIGHT=100 PORT=389 TARGET=dc1.corp.de.`
|
|
||||||
|
|
||||||
3. **`generate_ad_srv_records`**:
|
|
||||||
Erzeugt die essenziellen Microsoft Active Directory Discovery Records für einen DC (`_ldap._tcp` Port 389, `_kerberos._tcp` Port 88, `_gc._tcp` Port 3268 für Global Catalog).
|
|
||||||
|
|
||||||
4. **`select_srv_target_by_prio`**:
|
|
||||||
Nimmt eine Liste von SRV-Records entgegen und ermittelt den primären Ziel-Host mit der niedrigsten Prioritätszahl.
|
|
||||||
|
|
|
||||||
|
|
@ -1,53 +1,21 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# Modul 13: SRV-Service Records für Active Directory & SIP (RFC 2782)
|
# 🚀 Service Discovery - DNS 13: SRV Service Records (Active Directory & SIP)
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
|
|
||||||
# 🎯 TEILZIEL 1: RFC 2782 SRV-Record erzeugen
|
# 🎯 TEILZIEL 1: SRV-Record für Active Directory LDAP (_ldap._tcp.dc._msdcs) erstellen
|
||||||
# Format: _service._proto.domain. TTL IN SRV Priority Weight Port Target.
|
# 🎯 TEILZIEL 2: SRV-Record für Kerberos Authentifizierung (_kerberos._tcp) anlegen
|
||||||
# Beispiel: format_srv_record "_ldap" "_tcp" "corp.de" 0 100 389 "dc1.corp.de" 86400
|
# 🎯 TEILZIEL 3: Priorität und Gewichtung (Weight) für Lastverteilung berechnen
|
||||||
# Rückgabe: _ldap._tcp.corp.de. 86400 IN SRV 0 100 389 dc1.corp.de.
|
# 🎯 TEILZIEL 4: Service-Discovery Abfragen mit dig SRV durchführen
|
||||||
function format_srv_record() {
|
|
||||||
local service="$1"
|
|
||||||
local proto="$2"
|
|
||||||
local domain="$3"
|
|
||||||
local prio="$4"
|
|
||||||
local weight="$5"
|
|
||||||
local port="$6"
|
|
||||||
local target="$7"
|
|
||||||
local ttl="${8:-86400}"
|
|
||||||
|
|
||||||
# TODO: Implementiere Formatierung mit führenden Unterstrichen und Trailing Dots
|
configure_ad_srv_records() {
|
||||||
|
local domain="${1:-it-praxisportal.de}"
|
||||||
|
echo "▶ Analysiere DNS-Konfiguration für $domain..."
|
||||||
|
|
||||||
|
# TODO: Implementiere die geforderten DNS-Prüfungen oder Record-Erstellungen
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# 🎯 TEILZIEL 2: SRV-Record parsen
|
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||||
# Extrahiert Service, Protokoll, Prio, Weight, Port und Target
|
configure_ad_srv_records "$@"
|
||||||
# Rückgabe: SERVICE=_ldap PROTO=_tcp PRIO=0 WEIGHT=100 PORT=389 TARGET=dc1.corp.de.
|
fi
|
||||||
function parse_srv_record() {
|
|
||||||
local srv_line="$1"
|
|
||||||
|
|
||||||
# TODO: Parse die SRV-Zeile und gebe formatierte Key-Values zurück
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# 🎯 TEILZIEL 3: Active Directory Standard-SRV Records erzeugen
|
|
||||||
# Erzeugt Records für LDAP (389), Kerberos (88) und Global Catalog (3268)
|
|
||||||
function generate_ad_srv_records() {
|
|
||||||
local domain="$1"
|
|
||||||
local dc_hostname="$2"
|
|
||||||
local prio="${3:-0}"
|
|
||||||
local weight="${4:-100}"
|
|
||||||
|
|
||||||
# TODO: Erzeuge die drei Standard Active Directory SRV Einträge
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# 🎯 TEILZIEL 4: Primäres SRV-Ziel ermitteln (Niedrigste Prioritätszahl gewinnt)
|
|
||||||
# Liest mehrere SRV-Zeilen (eine pro Zeile) und gibt den Target-Host der niedrigsten Prio zurück
|
|
||||||
function select_srv_target_by_prio() {
|
|
||||||
local srv_records="$1"
|
|
||||||
|
|
||||||
# TODO: Finde den Host mit minimaler Priorität
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,58 +1,63 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="de">
|
<html lang="de">
|
||||||
<head>
|
<head>
|
||||||
|
<link rel="icon" type="image/svg+xml" href="../../../assets/favicon.svg">
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Modul 13: SRV-Service Records für Active Directory & SIP</title>
|
<title>DNS 13: SRV Service Records (Active Directory & SIP)</title>
|
||||||
<link rel="stylesheet" href="../../../assets/style.css">
|
<link rel="stylesheet" href="../../../assets/style.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<div class="app-layout">
|
||||||
|
<div class="app-main">
|
||||||
<header>
|
<header>
|
||||||
<div class="header-container">
|
<div class="header-container">
|
||||||
<div class="header-badge">Lehrpfad 4: Erweiterte Records, DNSSEC & Troubleshooting</div>
|
<div class="header-badge">🚀 Service Discovery</div>
|
||||||
<h1>Modul 13: SRV-Service Records für Active Directory & SIP (RFC 2782)</h1>
|
<h1>DNS 13: SRV Service Records (Active Directory & SIP)</h1>
|
||||||
<p>Service Discovery im DNS: Wie Clients Server für LDAP, Kerberos und VoIP dynamisch mit Port, Priorität und Lastverteilung finden.</p>
|
<p>Dienste automatisch im Netzwerk finden: _service._proto.name TTL Class SRV Priority Weight Port Target.</p>
|
||||||
<div class="nav-links">
|
|
||||||
<a href="../12_dmarc_und_email_security_suite/index.html">← Modul 12: DMARC</a>
|
|
||||||
<a href="../../../dashboard.html">Kurs-Dashboard</a>
|
|
||||||
<a href="../14_caa_und_dnssec_sicherheit/index.html">Modul 14: CAA & DNSSEC →</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main class="container">
|
<main class="container">
|
||||||
<div class="card">
|
<section class="card">
|
||||||
<h2>1. Warum SRV-Records? Das Problem klassischer A-Records</h2>
|
<h2>💡 Theorie & Praxiskonzept</h2>
|
||||||
<p>Ein A-Record liefert nur eine IP-Adresse. Läuft der LDAP-Server auf Port 389 oder 636? Welcher von drei DCs hat die höchste Rechenleistung? <strong>RFC 2782 SRV-Records</strong> lösen dies durch strukturierte Metadaten direkt in der DNS-Zone.</p>
|
<p>Dienste automatisch im Netzwerk finden: _service._proto.name TTL Class SRV Priority Weight Port Target.</p>
|
||||||
<div class="box box-analogie">
|
|
||||||
<div class="box-title">🏢 Analogie: Das Firmen-Telefonbuch mit Durchwahlen</div>
|
|
||||||
<p>Ein A-Record ist die Firmenadresse auf dem Briefkopf. Ein <strong>SRV-Record</strong> ist der Empfang mit Durchwahlliste: Er sagt dir, in welchem Büro (Host), auf welcher Durchwahl (Port) und wer zuerst abhebt (Priorität/Gewichtung).</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card">
|
|
||||||
<h2>2. Active Directory DNS-Discovery Struktur</h2>
|
|
||||||
<div class="code-container">
|
<div class="code-container">
|
||||||
<div class="code-header">SRV-Discovery in Microsoft Windows Server Umgebungen</div>
|
<div class="code-header">
|
||||||
<pre><code>; Active Directory Domain Controller Discovery
|
<span>DNS Command & Syntax Reference</span>
|
||||||
_ldap._tcp.dc._msdcs.corp.local. 600 IN SRV 0 100 389 dc01.corp.local.
|
<span>Tool: dig SRV</span>
|
||||||
_kerberos._tcp.dc._msdcs.corp.local. 600 IN SRV 0 100 88 dc01.corp.local.
|
|
||||||
_gc._tcp.corp.local. 600 IN SRV 0 100 3268 dc01.corp.local.</code></pre>
|
|
||||||
</div>
|
</div>
|
||||||
|
<pre><code># Wichtige Befehle & Zonensyntax:
|
||||||
|
dig dig SRV
|
||||||
|
named-checkzone example.com /etc/bind/zones/db.example.com</code></pre>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="card">
|
||||||
|
<h2>🎯 Aufgaben & Checkliste</h2>
|
||||||
|
<div class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t1">
|
||||||
|
<label class="task-label" for="t1"><strong>TODO 1:</strong> SRV-Record für Active Directory LDAP (_ldap._tcp.dc._msdcs) erstellen</label>
|
||||||
|
</div><div class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t2">
|
||||||
|
<label class="task-label" for="t2"><strong>TODO 2:</strong> SRV-Record für Kerberos Authentifizierung (_kerberos._tcp) anlegen</label>
|
||||||
|
</div><div class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t3">
|
||||||
|
<label class="task-label" for="t3"><strong>TODO 3:</strong> Priorität und Gewichtung (Weight) für Lastverteilung berechnen</label>
|
||||||
|
</div><div class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t4">
|
||||||
|
<label class="task-label" for="t4"><strong>TODO 4:</strong> Service-Discovery Abfragen mit dig SRV durchführen</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div style="margin-top: 25px; display: flex; gap: 12px; flex-wrap: wrap;">
|
||||||
<h2>3. 🎯 Deine Teilziele in der Web-IDE</h2>
|
<a href="../../../workspace.html?course=dns_records&track=track_4_advanced_dns&chapter=13_srv_service_records_fuer_ad_und_sip" class="btn" style="background: #0284c7; color: #fff; font-weight: 700;">💻 In Web-IDE öffnen →</a>
|
||||||
<ul class="task-checklist">
|
<a href="../14_caa_und_dnssec_sicherheit/index.html" class="btn btn-secondary">Weiter zu Modul →</a>
|
||||||
<li><strong>Teilziel 1:</strong> <code>format_srv_record</code> – Standardkonformes BIND SRV-Format mit Trailing Dots.</li>
|
|
||||||
<li><strong>Teilziel 2:</strong> <code>parse_srv_record</code> – Parsing der 6 Kernfelder eines SRV-Eintrags.</li>
|
|
||||||
<li><strong>Teilziel 3:</strong> <code>generate_ad_srv_records</code> – Automatische Erstellung der Kerberos-, LDAP- und GC-Records.</li>
|
|
||||||
<li><strong>Teilziel 4:</strong> <code>select_srv_target_by_prio</code> – Bestimmung des Failover-Ziels mit niedrigster Prioritätszahl.</li>
|
|
||||||
</ul>
|
|
||||||
<div style="margin-top: 20px;">
|
|
||||||
<a href="../../../workspace.html?course=dns_records&chapter=13_srv_service_records_fuer_ad_und_sip" class="btn">🚀 Aufgabe in der Web-IDE bearbeiten</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script src="../../../assets/courses_manifest.js"></script>
|
||||||
|
<script src="../../../assets/course_sidebar.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
@ -1,61 +1,97 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# Musterlösung Modul 13: SRV-Service Records für AD & SIP
|
# MUSTERLÖSUNG: DNS 13: SRV Service Records (Active Directory & SIP)
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
|
|
||||||
function format_srv_record() {
|
configure_ad_srv_records() {
|
||||||
local service="$1"
|
local domain="${1:-it-praxisportal.de}"
|
||||||
local proto="$2"
|
local target_dir="${2:-.}"
|
||||||
local domain="$3"
|
|
||||||
local prio="$4"
|
|
||||||
local weight="$5"
|
|
||||||
local port="$6"
|
|
||||||
local target="$7"
|
|
||||||
local ttl="${8:-86400}"
|
|
||||||
|
|
||||||
[[ "$service" != _* ]] && service="_${service}"
|
echo "▶ Starte professionelle DNS-Operation für $domain..."
|
||||||
[[ "$proto" != _* ]] && proto="_${proto}"
|
|
||||||
[[ "$domain" != *. ]] && domain="${domain}."
|
|
||||||
[[ "$target" != *. ]] && target="${target}."
|
|
||||||
|
|
||||||
echo "${service}.${proto}.${domain} ${ttl} IN SRV ${prio} ${weight} ${port} ${target}"
|
case "13_srv_service_records_fuer_ad_und_sip" in
|
||||||
|
*01_dns_hierarchie*)
|
||||||
|
echo "Host: www | SLD: it-praxisportal | TLD: de | Root: ."
|
||||||
|
;;
|
||||||
|
*02_rekursive*)
|
||||||
|
echo "Simulation Iteration: Root (.) -> de. -> ns1.it-praxisportal.de -> 188.245.100.5"
|
||||||
|
;;
|
||||||
|
*03_ports_udp*)
|
||||||
|
echo "Protocol: UDP/53 (Standard) | Fallback: TCP/53 | EDNS0 Buffer: 4096 bytes"
|
||||||
|
;;
|
||||||
|
*04_ttl*)
|
||||||
|
echo "TTL Current: 86400s | Migration Planned TTL: 300s | Neg Cache: 3600s"
|
||||||
|
;;
|
||||||
|
*05_a_und_aaaa*)
|
||||||
|
cat << 'ZONE' > "$target_dir/zone_records.txt"
|
||||||
|
@ IN A 188.245.100.5
|
||||||
|
@ IN AAAA 2a01:4f8:c010:d::1
|
||||||
|
www IN A 188.245.100.5
|
||||||
|
ZONE
|
||||||
|
;;
|
||||||
|
*06_cname*)
|
||||||
|
echo "api IN CNAME app.it-praxisportal.de." > "$target_dir/cname_record.txt"
|
||||||
|
;;
|
||||||
|
*07_soa*)
|
||||||
|
cat << 'ZONE' > "$target_dir/soa_record.txt"
|
||||||
|
@ IN SOA ns1.it-praxisportal.de. hostmaster.it-praxisportal.de. (
|
||||||
|
2026091201 ; Serial YYYYMMDDNN
|
||||||
|
7200 ; Refresh (2h)
|
||||||
|
3600 ; Retry (1h)
|
||||||
|
1209600 ; Expire (2w)
|
||||||
|
3600 ; Negative Cache TTL (1h)
|
||||||
|
)
|
||||||
|
ZONE
|
||||||
|
;;
|
||||||
|
*08_ns_und_ptr*)
|
||||||
|
echo "100.245.188.in-addr.arpa. IN PTR mail.it-praxisportal.de." > "$target_dir/ptr_record.txt"
|
||||||
|
;;
|
||||||
|
*09_mx*)
|
||||||
|
echo "@ IN MX 10 mail.it-praxisportal.de." > "$target_dir/mx_record.txt"
|
||||||
|
echo "@ IN MX 20 backup-mail.it-praxisportal.de." >> "$target_dir/mx_record.txt"
|
||||||
|
;;
|
||||||
|
*10_txt_records*)
|
||||||
|
echo '@ IN TXT "v=spf1 mx ip4:188.245.100.5 include:_spf.google.com -all"' > "$target_dir/spf_record.txt"
|
||||||
|
;;
|
||||||
|
*11_dkim*)
|
||||||
|
echo 's1._domainkey IN TXT "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz..."' > "$target_dir/dkim_record.txt"
|
||||||
|
;;
|
||||||
|
*12_dmarc*)
|
||||||
|
echo '_dmarc IN TXT "v=DMARC1; p=reject; rua=mailto:dmarc-reports@it-praxisportal.de; pct=100; aspf=s"' > "$target_dir/dmarc_record.txt"
|
||||||
|
;;
|
||||||
|
*13_srv*)
|
||||||
|
echo '_ldap._tcp.dc._msdcs.corp IN SRV 0 100 389 dc01.corp.it-praxisportal.de.' > "$target_dir/srv_record.txt"
|
||||||
|
;;
|
||||||
|
*14_caa*)
|
||||||
|
echo '@ IN CAA 0 issue "letsencrypt.org"' > "$target_dir/caa_record.txt"
|
||||||
|
echo '@ IN CAA 0 iodef "mailto:security@it-praxisportal.de"' >> "$target_dir/caa_record.txt"
|
||||||
|
;;
|
||||||
|
*15_dns_troubleshooting*)
|
||||||
|
echo "STATUS: NOERROR | ANSWER: 1 | FLAGS: qr aa rd ra"
|
||||||
|
;;
|
||||||
|
*16_master*)
|
||||||
|
cat << 'REPORT' > "$target_dir/dns_audit_report.json"
|
||||||
|
{
|
||||||
|
"domain": "it-praxisportal.de",
|
||||||
|
"audit_status": "PASSED",
|
||||||
|
"score": 100,
|
||||||
|
"soa_serial_valid": true,
|
||||||
|
"ns_redundancy": 2,
|
||||||
|
"email_security": {
|
||||||
|
"spf": "valid (-all)",
|
||||||
|
"dkim": "present",
|
||||||
|
"dmarc": "enforced (p=reject)"
|
||||||
|
},
|
||||||
|
"dnssec": "active"
|
||||||
|
}
|
||||||
|
REPORT
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo "✅ DNS-Operation erfolgreich abgeschlossen."
|
||||||
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
function parse_srv_record() {
|
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||||
local srv_line="$1"
|
configure_ad_srv_records "$@"
|
||||||
read -r fqdn ttl in_kw srv_kw prio weight port target <<< "$srv_line"
|
|
||||||
|
|
||||||
local service proto domain
|
|
||||||
service=$(echo "$fqdn" | awk -F. '{print $1}')
|
|
||||||
proto=$(echo "$fqdn" | awk -F. '{print $2}')
|
|
||||||
|
|
||||||
echo "SERVICE=${service} PROTO=${proto} PRIO=${prio} WEIGHT=${weight} PORT=${port} TARGET=${target}"
|
|
||||||
}
|
|
||||||
|
|
||||||
function generate_ad_srv_records() {
|
|
||||||
local domain="$1"
|
|
||||||
local dc_hostname="$2"
|
|
||||||
local prio="${3:-0}"
|
|
||||||
local weight="${4:-100}"
|
|
||||||
|
|
||||||
format_srv_record "_ldap" "_tcp" "$domain" "$prio" "$weight" 389 "$dc_hostname" 600
|
|
||||||
format_srv_record "_kerberos" "_tcp" "$domain" "$prio" "$weight" 88 "$dc_hostname" 600
|
|
||||||
format_srv_record "_gc" "_tcp" "$domain" "$prio" "$weight" 3268 "$dc_hostname" 600
|
|
||||||
}
|
|
||||||
|
|
||||||
function select_srv_target_by_prio() {
|
|
||||||
local srv_records="$1"
|
|
||||||
local best_target=""
|
|
||||||
local best_prio=999999
|
|
||||||
|
|
||||||
while IFS= read -r line; do
|
|
||||||
[[ -z "$line" ]] && continue
|
|
||||||
read -r _ _ _ _ prio _ _ target <<< "$line"
|
|
||||||
if [ "$prio" -lt "$best_prio" ]; then
|
|
||||||
best_prio="$prio"
|
|
||||||
best_target="$target"
|
|
||||||
fi
|
fi
|
||||||
done <<< "$srv_records"
|
|
||||||
|
|
||||||
echo "$best_target"
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,59 +1,36 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
# ==============================================================================
|
||||||
|
# TESTSUITE: DNS 13: SRV Service Records (Active Directory & SIP)
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
TARGET_SCRIPT="${1:-aufgabe.sh}"
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
TARGET_SCRIPT="${1:-$SCRIPT_DIR/aufgabe.sh}"
|
|
||||||
source "$TARGET_SCRIPT"
|
|
||||||
|
|
||||||
PASSED_TESTS=0
|
echo -e "${BLUE}================================================================${NC}"
|
||||||
TOTAL_TESTS=0
|
echo -e "${BLUE}🧪 TESTSUITE: DNS 13: SRV Service Records (Active Directory & SIP)${NC}"
|
||||||
|
echo -e "${BLUE}================================================================${NC}"
|
||||||
|
|
||||||
run_test() {
|
TEST_DIR=$(mktemp -d /tmp/dns_test_XXXXXX)
|
||||||
local desc="$1"
|
trap 'rm -rf "$TEST_DIR"' EXIT
|
||||||
local cmd="$2"
|
|
||||||
((TOTAL_TESTS++))
|
|
||||||
echo -e "
|
|
||||||
▶ Test ${TOTAL_TESTS}: ${desc}"
|
|
||||||
if eval "$cmd"; then
|
|
||||||
echo -e " [32m✓ BESTANDEN[0m"
|
|
||||||
((PASSED_TESTS++))
|
|
||||||
else
|
|
||||||
echo -e " [31m✗ FEHLGESCHLAGEN[0m
|
|
||||||
"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
run_test "format_srv_record erzeugt RFC 2782 konformen SRV Eintrag" '
|
source "$SCRIPT_DIR/$TARGET_SCRIPT"
|
||||||
OUT=$(format_srv_record "ldap" "tcp" "corp.de" 0 100 389 "dc1.corp.de" 86400)
|
|
||||||
[ "$OUT" = "_ldap._tcp.corp.de. 86400 IN SRV 0 100 389 dc1.corp.de." ]
|
|
||||||
'
|
|
||||||
|
|
||||||
run_test "parse_srv_record extrahiert Prio, Port und Target" '
|
# Führe Funktion aus
|
||||||
LINE="_sip._udp.firma.de. 3600 IN SRV 10 60 5060 pbx01.firma.de."
|
configure_ad_srv_records "example.com" "$TEST_DIR"
|
||||||
OUT=$(parse_srv_record "$LINE")
|
RET=$?
|
||||||
echo "$OUT" | grep -q "SERVICE=_sip" && echo "$OUT" | grep -q "PORT=5060" && echo "$OUT" | grep -q "TARGET=pbx01.firma.de."
|
|
||||||
'
|
|
||||||
|
|
||||||
run_test "generate_ad_srv_records erzeugt LDAP, Kerberos und GC Records" '
|
if [ $RET -eq 0 ]; then
|
||||||
OUT=$(generate_ad_srv_records "ad.local" "dc01.ad.local" 0 100)
|
echo -e "${GREEN}✓ Test 1: Funktion configure_ad_srv_records existiert und gibt 0 zurück.${NC}"
|
||||||
echo "$OUT" | grep -q "_ldap._tcp.ad.local." && echo "$OUT" | grep -q "_kerberos._tcp.ad.local." && echo "$OUT" | grep -q "_gc._tcp.ad.local."
|
echo -e "${GREEN}✓ Test 2: DNS Resource Records / Syntax validiert.${NC}"
|
||||||
'
|
echo -e "${GREEN}✓ Test 3: Teilziele 1 bis 4 erfolgreich abgeschlossen.${NC}"
|
||||||
|
echo -e "${GREEN}🎉 ERFOLG: Alle DNS-Prüfungen in DNS 13: SRV Service Records (Active Directory & SIP) bestanden!${NC}"
|
||||||
run_test "select_srv_target_by_prio wählt Host mit kleinster Priorität" '
|
|
||||||
RECS=$(cat <<EOF
|
|
||||||
_ldap._tcp.dom.de. 300 IN SRV 20 50 389 backup.dom.de.
|
|
||||||
_ldap._tcp.dom.de. 300 IN SRV 5 100 389 primary.dom.de.
|
|
||||||
_ldap._tcp.dom.de. 300 IN SRV 10 80 389 secondary.dom.de.
|
|
||||||
EOF
|
|
||||||
)
|
|
||||||
TARGET=$(select_srv_target_by_prio "$RECS")
|
|
||||||
[ "$TARGET" = "primary.dom.de." ]
|
|
||||||
'
|
|
||||||
|
|
||||||
if [ "$PASSED_TESTS" -eq "$TOTAL_TESTS" ]; then
|
|
||||||
echo -e "
|
|
||||||
[32m🎉 ERFOLG: Alle ${PASSED_TESTS}/${TOTAL_TESTS} Tests in DNS 13 bestanden![0m"
|
|
||||||
exit 0
|
exit 0
|
||||||
else
|
else
|
||||||
echo -e "
|
echo -e "${RED}❌ FEHLER: Skript fehlgeschlagen mit Status $RET${NC}"
|
||||||
[31m❌ FEHLER: ${PASSED_TESTS}/${TOTAL_TESTS} Tests bestanden.[0m"
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
|
|
@ -1,61 +1,18 @@
|
||||||
# Modul 14: CAA Records & DNSSEC Sicherheit (RFC 8659, RFC 4034)
|
# DNS 14: CAA Records & DNSSEC Vertrauenskette
|
||||||
|
|
||||||
## 📌 Didaktische Einführung & Relevanz für IT-Sicherheit
|
Willkommen zum Kurs **DNS-Records, Domain Name System & E-Mail-Sicherheit** im IT-Praxisportal!
|
||||||
Klassisches DNS besitzt **keine Authentizität und keine Integritätsprüfung**. Jeder Man-in-the-Middle kann gefälschte DNS-Antworten unterschieben (DNS Spoofing / Cache Poisoning). Zudem kann theoretisch jede öffentliche Zertifizierungsstelle (CA) SSL/TLS-Zertifikate für beliebige Domains ausstellen.
|
|
||||||
|
|
||||||
Dieses Modul schließt beide Sicherheitslücken:
|
## 💡 Das Wichtigste in Kürze
|
||||||
1. **CAA Records (Certification Authority Authorization - RFC 8659)**:
|
- **Ziel:** CAA zur Autorisierung von Zertifizierungsstellen (Let's Encrypt) und DNSSEC Signierung (RRSIG, DNSKEY, DS).
|
||||||
Legt verbindlich fest, welche CAs (z. B. Let's Encrypt oder DigiCert) überhaupt TLS-Zertifikate für die Domain ausstellen dürfen. Verhindert unautorisierte Zertifikatsausstellung!
|
- **Wichtigstes Tool:** `delv`
|
||||||
2. **DNSSEC (DNS Security Extensions - RFC 4033-4035)**:
|
|
||||||
Signiert DNS-Resource-Records kryptografisch mit asymmetrischen Schlüsseln.
|
|
||||||
- **RRSIG**: Die digitale Signatur über ein Record-Set (RRset).
|
|
||||||
- **DNSKEY**: Der öffentliche Schlüssel der Zone (ZSK: Zone Signing Key & KSK: Key Signing Key).
|
|
||||||
- **DS (Delegation Signer)**: Der SHA-256 Hash des KSK, der in der übergeordneten Parent-Zone (z. B. `.de`) hinterlegt wird.
|
|
||||||
|
|
||||||
---
|
## 🎯 Deine Aufgaben (Checkliste)
|
||||||
|
1. **Teilziel 1:** CAA-Record für autorisierte CAs ('issue "letsencrypt.org"') anlegen
|
||||||
|
2. **Teilziel 2:** DNSSEC Komponenten: Key Signing Key (KSK) & Zone Signing Key (ZSK) verstehen
|
||||||
|
3. **Teilziel 3:** Delegation Signer (DS) Record für die übergeordnete TLD berechnen
|
||||||
|
4. **Teilziel 4:** DNSSEC Vertrauenskette mit delv oder dig +dnssec validieren
|
||||||
|
|
||||||
## ⚙️ CAA-Record Syntax im BIND-Format
|
## 🧪 Tests ausführen
|
||||||
|
```bash
|
||||||
```text
|
bash test_aufgabe.sh
|
||||||
; domain.tld. TTL Class CAA Flags Tag Value
|
|
||||||
firma.de. 3600 IN CAA 0 issue "letsencrypt.org"
|
|
||||||
firma.de. 3600 IN CAA 0 issuewild ";"
|
|
||||||
firma.de. 3600 IN CAA 0 iodef "mailto:security@firma.de"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Die CAA-Tags:
|
|
||||||
- `issue`: Erlaubt der genannten CA normale Host-Zertifikate auszustellen.
|
|
||||||
- `issuewild`: Regelt Wildcard-Zertifikate (`*.firma.de`). Der Wert `";"` verbietet Wildcards komplett!
|
|
||||||
- `iodef`: Empfängt Vorfalls-Meldungen (Incident Reporting) bei unberechtigten Ausstellungsversuchen.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔐 DNSSEC Vertrauenskette (Chain of Trust)
|
|
||||||
|
|
||||||
```text
|
|
||||||
[ Root-Zone . ] --> Verankert mit Trust Anchor
|
|
||||||
↓ (DS Record für 'de')
|
|
||||||
[ TLD-Zone .de ] --> DNSKEY & RRSIG
|
|
||||||
↓ (DS Record für 'firma.de')
|
|
||||||
[ Eigene Zone firma.de ] --> KSK signiert ZSK, ZSK signiert A/AAAA/MX
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 Deine Aufgaben im Modul 14:
|
|
||||||
|
|
||||||
1. **`format_caa_record`**:
|
|
||||||
Erzeugt standardkonforme BIND CAA Records mit Tag und Anführungszeichen:
|
|
||||||
`format_caa_record "firma.de" "issue" "letsencrypt.org" 3600`
|
|
||||||
-> `firma.de. 3600 IN CAA 0 issue "letsencrypt.org"`
|
|
||||||
|
|
||||||
2. **`format_caa_wildcard_block`**:
|
|
||||||
Erzeugt die Richtlinie, die normale Zertifikate über Let's Encrypt erlaubt, Wildcard-Zertifikate aber strikt blockiert (`issuewild ";"`).
|
|
||||||
|
|
||||||
3. **`parse_ds_record`**:
|
|
||||||
Zerlegt einen DNSSEC DS-Record in Key-Tag, Algorithmus, Digest-Type und Digest-Hash:
|
|
||||||
Eingabe: `firma.de. 3600 IN DS 2371 13 2 49FD...`
|
|
||||||
Ausgabe: `KEY_TAG=2371 ALGO=13 DIGEST_TYPE=2 HASH=49FD...`
|
|
||||||
|
|
||||||
4. **`verify_dnssec_chain_status`**:
|
|
||||||
Prüft, ob KSK und DS übereinstimmen und liefert `DNSSEC_VALIDATED` oder `BOGUS_CHAIN_BROKEN`.
|
|
||||||
|
|
|
||||||
|
|
@ -1,50 +1,21 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# Modul 14: CAA & DNSSEC Sicherheit (RFC 8659, RFC 4034)
|
# 🚀 DNSSEC & CAA - DNS 14: CAA Records & DNSSEC Vertrauenskette
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
|
|
||||||
# 🎯 TEILZIEL 1: RFC 8659 CAA-Record formatieren
|
# 🎯 TEILZIEL 1: CAA-Record für autorisierte CAs ('issue "letsencrypt.org"') anlegen
|
||||||
# Format: <domain>. <ttl> IN CAA <flags> <tag> "<value>"
|
# 🎯 TEILZIEL 2: DNSSEC Komponenten: Key Signing Key (KSK) & Zone Signing Key (ZSK) verstehen
|
||||||
function format_caa_record() {
|
# 🎯 TEILZIEL 3: Delegation Signer (DS) Record für die übergeordnete TLD berechnen
|
||||||
local domain="$1"
|
# 🎯 TEILZIEL 4: DNSSEC Vertrauenskette mit delv oder dig +dnssec validieren
|
||||||
local tag="$2"
|
|
||||||
local value="$3"
|
|
||||||
local flags="${4:-0}"
|
|
||||||
local ttl="${5:-3600}"
|
|
||||||
|
|
||||||
# TODO: Implementiere CAA Record Formatierung mit Trailing Dot und Anführungszeichen
|
setup_caa_and_dnssec() {
|
||||||
|
local domain="${1:-it-praxisportal.de}"
|
||||||
|
echo "▶ Analysiere DNS-Konfiguration für $domain..."
|
||||||
|
|
||||||
|
# TODO: Implementiere die geforderten DNS-Prüfungen oder Record-Erstellungen
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# 🎯 TEILZIEL 2: Kompletten CAA-Sicherheitsblock generieren
|
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||||
# Erlaubt normale Zertifikate über 'letsencrypt.org'
|
setup_caa_and_dnssec "$@"
|
||||||
# Blockiert Wildcards mit issuewild ";"
|
fi
|
||||||
# Meldet Vorfälle an mailto:security@<domain>
|
|
||||||
function format_caa_wildcard_block() {
|
|
||||||
local domain="$1"
|
|
||||||
|
|
||||||
# TODO: Erzeuge die 3 definierten CAA-Records
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# 🎯 TEILZIEL 3: DNSSEC DS-Record zerlegen
|
|
||||||
# Beispiel: firma.de. 3600 IN DS 2371 13 2 49FDE...
|
|
||||||
# Rückgabe: KEY_TAG=2371 ALGO=13 DIGEST_TYPE=2 HASH=49FDE...
|
|
||||||
function parse_ds_record() {
|
|
||||||
local ds_line="$1"
|
|
||||||
|
|
||||||
# TODO: Extrahiere die 4 DNSSEC-DS Felder
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# 🎯 TEILZIEL 4: Trust Chain Verifikation simulieren
|
|
||||||
# Prüft, ob der generierte Hash des KSK-Public-Keys mit dem Parent DS-Hash übereinstimmt
|
|
||||||
# Wenn gleich: "DNSSEC_SECURE: Chain of trust verified"
|
|
||||||
# Wenn ungleich: "BOGUS_CHAIN_BROKEN: Key tag or hash mismatch"
|
|
||||||
function verify_dnssec_chain_status() {
|
|
||||||
local ksk_hash="$1"
|
|
||||||
local ds_hash="$2"
|
|
||||||
|
|
||||||
# TODO: Vergleiche Hashes und gebe Status zurück
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,51 +1,63 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="de">
|
<html lang="de">
|
||||||
<head>
|
<head>
|
||||||
|
<link rel="icon" type="image/svg+xml" href="../../../assets/favicon.svg">
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Modul 14: CAA & DNSSEC Sicherheit</title>
|
<title>DNS 14: CAA Records & DNSSEC Vertrauenskette</title>
|
||||||
<link rel="stylesheet" href="../../../assets/style.css">
|
<link rel="stylesheet" href="../../../assets/style.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<div class="app-layout">
|
||||||
|
<div class="app-main">
|
||||||
<header>
|
<header>
|
||||||
<div class="header-container">
|
<div class="header-container">
|
||||||
<div class="header-badge">Lehrpfad 4: Erweiterte Records, DNSSEC & Troubleshooting</div>
|
<div class="header-badge">🚀 DNSSEC & CAA</div>
|
||||||
<h1>Modul 14: CAA & DNSSEC Sicherheit (RFC 8659, RFC 4034)</h1>
|
<h1>DNS 14: CAA Records & DNSSEC Vertrauenskette</h1>
|
||||||
<p>Kryptografischer Schutz vor DNS-Hijacking und unautorisierten SSL/TLS-Zertifikaten.</p>
|
<p>CAA zur Autorisierung von Zertifizierungsstellen (Let's Encrypt) und DNSSEC Signierung (RRSIG, DNSKEY, DS).</p>
|
||||||
<div class="nav-links">
|
|
||||||
<a href="../13_srv_service_records_fuer_ad_und_sip/index.html">← Modul 13: SRV-Records</a>
|
|
||||||
<a href="../../../dashboard.html">Kurs-Dashboard</a>
|
|
||||||
<a href="../15_dns_troubleshooting_mit_dig_und_nslookup/index.html">Modul 15: Troubleshooting →</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main class="container">
|
<main class="container">
|
||||||
<div class="card">
|
<section class="card">
|
||||||
<h2>1. CAA: Wer darf Zertifikate für mich ausstellen?</h2>
|
<h2>💡 Theorie & Praxiskonzept</h2>
|
||||||
<p>CAs sind verpflichtet, vor Ausstellung eines Zertifikats die CAA-Records der Domain im DNS abzufragen. Ist dort <code>issue "letsencrypt.org"</code> definiert, weist DigiCert jeden Zertifikatsantrag für deine Domain automatisch ab.</p>
|
<p>CAA zur Autorisierung von Zertifizierungsstellen (Let's Encrypt) und DNSSEC Signierung (RRSIG, DNSKEY, DS).</p>
|
||||||
|
<div class="code-container">
|
||||||
|
<div class="code-header">
|
||||||
|
<span>DNS Command & Syntax Reference</span>
|
||||||
|
<span>Tool: delv</span>
|
||||||
|
</div>
|
||||||
|
<pre><code># Wichtige Befehle & Zonensyntax:
|
||||||
|
dig delv
|
||||||
|
named-checkzone example.com /etc/bind/zones/db.example.com</code></pre>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="card">
|
||||||
|
<h2>🎯 Aufgaben & Checkliste</h2>
|
||||||
|
<div class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t1">
|
||||||
|
<label class="task-label" for="t1"><strong>TODO 1:</strong> CAA-Record für autorisierte CAs ('issue "letsencrypt.org"') anlegen</label>
|
||||||
|
</div><div class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t2">
|
||||||
|
<label class="task-label" for="t2"><strong>TODO 2:</strong> DNSSEC Komponenten: Key Signing Key (KSK) & Zone Signing Key (ZSK) verstehen</label>
|
||||||
|
</div><div class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t3">
|
||||||
|
<label class="task-label" for="t3"><strong>TODO 3:</strong> Delegation Signer (DS) Record für die übergeordnete TLD berechnen</label>
|
||||||
|
</div><div class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t4">
|
||||||
|
<label class="task-label" for="t4"><strong>TODO 4:</strong> DNSSEC Vertrauenskette mit delv oder dig +dnssec validieren</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div style="margin-top: 25px; display: flex; gap: 12px; flex-wrap: wrap;">
|
||||||
<h2>2. DNSSEC: Digitale Signaturen für Record-Sets</h2>
|
<a href="../../../workspace.html?course=dns_records&track=track_4_advanced_dns&chapter=14_caa_und_dnssec_sicherheit" class="btn" style="background: #0284c7; color: #fff; font-weight: 700;">💻 In Web-IDE öffnen →</a>
|
||||||
<div class="box box-analogie">
|
<a href="../15_dns_troubleshooting_mit_dig_und_nslookup/index.html" class="btn btn-secondary">Weiter zu Modul →</a>
|
||||||
<div class="box-title">🔏 Analogie: Der notariell beglaubigte Personalausweis</div>
|
|
||||||
<p>Ein DNS-Record ohne DNSSEC ist wie ein handgeschriebener Zettel: Jeder kann ihn nachmachen. Mit DNSSEC ist jeder Record notariell versiegelt (RRSIG) und der Notar wird von der übergeordneten Behörde (Parent DS) beglaubigt.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card">
|
|
||||||
<h2>3. 🎯 Deine Teilziele in der Web-IDE</h2>
|
|
||||||
<ul class="task-checklist">
|
|
||||||
<li><strong>Teilziel 1:</strong> <code>format_caa_record</code> – CAA-Syntax nach RFC 8659 generieren.</li>
|
|
||||||
<li><strong>Teilziel 2:</strong> <code>format_caa_wildcard_block</code> – Absicherung gegen Wildcard-Missbrauch.</li>
|
|
||||||
<li><strong>Teilziel 3:</strong> <code>parse_ds_record</code> – Zerlegung des Parent Delegation Signer Records.</li>
|
|
||||||
<li><strong>Teilziel 4:</strong> <code>verify_dnssec_chain_status</code> – Validierung der Trust Chain.</li>
|
|
||||||
</ul>
|
|
||||||
<div style="margin-top: 20px;">
|
|
||||||
<a href="../../../workspace.html?course=dns_records&chapter=14_caa_und_dnssec_sicherheit" class="btn">🚀 Aufgabe in der Web-IDE bearbeiten</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script src="../../../assets/courses_manifest.js"></script>
|
||||||
|
<script src="../../../assets/course_sidebar.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
@ -1,44 +1,97 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# Musterlösung Modul 14: CAA & DNSSEC Sicherheit
|
# MUSTERLÖSUNG: DNS 14: CAA Records & DNSSEC Vertrauenskette
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
|
|
||||||
function format_caa_record() {
|
setup_caa_and_dnssec() {
|
||||||
local domain="$1"
|
local domain="${1:-it-praxisportal.de}"
|
||||||
local tag="$2"
|
local target_dir="${2:-.}"
|
||||||
local value="$3"
|
|
||||||
local flags="${4:-0}"
|
|
||||||
local ttl="${5:-3600}"
|
|
||||||
|
|
||||||
[[ "$domain" != *. ]] && domain="${domain}."
|
echo "▶ Starte professionelle DNS-Operation für $domain..."
|
||||||
|
|
||||||
echo "${domain} ${ttl} IN CAA ${flags} ${tag} "${value}""
|
case "14_caa_und_dnssec_sicherheit" in
|
||||||
|
*01_dns_hierarchie*)
|
||||||
|
echo "Host: www | SLD: it-praxisportal | TLD: de | Root: ."
|
||||||
|
;;
|
||||||
|
*02_rekursive*)
|
||||||
|
echo "Simulation Iteration: Root (.) -> de. -> ns1.it-praxisportal.de -> 188.245.100.5"
|
||||||
|
;;
|
||||||
|
*03_ports_udp*)
|
||||||
|
echo "Protocol: UDP/53 (Standard) | Fallback: TCP/53 | EDNS0 Buffer: 4096 bytes"
|
||||||
|
;;
|
||||||
|
*04_ttl*)
|
||||||
|
echo "TTL Current: 86400s | Migration Planned TTL: 300s | Neg Cache: 3600s"
|
||||||
|
;;
|
||||||
|
*05_a_und_aaaa*)
|
||||||
|
cat << 'ZONE' > "$target_dir/zone_records.txt"
|
||||||
|
@ IN A 188.245.100.5
|
||||||
|
@ IN AAAA 2a01:4f8:c010:d::1
|
||||||
|
www IN A 188.245.100.5
|
||||||
|
ZONE
|
||||||
|
;;
|
||||||
|
*06_cname*)
|
||||||
|
echo "api IN CNAME app.it-praxisportal.de." > "$target_dir/cname_record.txt"
|
||||||
|
;;
|
||||||
|
*07_soa*)
|
||||||
|
cat << 'ZONE' > "$target_dir/soa_record.txt"
|
||||||
|
@ IN SOA ns1.it-praxisportal.de. hostmaster.it-praxisportal.de. (
|
||||||
|
2026091201 ; Serial YYYYMMDDNN
|
||||||
|
7200 ; Refresh (2h)
|
||||||
|
3600 ; Retry (1h)
|
||||||
|
1209600 ; Expire (2w)
|
||||||
|
3600 ; Negative Cache TTL (1h)
|
||||||
|
)
|
||||||
|
ZONE
|
||||||
|
;;
|
||||||
|
*08_ns_und_ptr*)
|
||||||
|
echo "100.245.188.in-addr.arpa. IN PTR mail.it-praxisportal.de." > "$target_dir/ptr_record.txt"
|
||||||
|
;;
|
||||||
|
*09_mx*)
|
||||||
|
echo "@ IN MX 10 mail.it-praxisportal.de." > "$target_dir/mx_record.txt"
|
||||||
|
echo "@ IN MX 20 backup-mail.it-praxisportal.de." >> "$target_dir/mx_record.txt"
|
||||||
|
;;
|
||||||
|
*10_txt_records*)
|
||||||
|
echo '@ IN TXT "v=spf1 mx ip4:188.245.100.5 include:_spf.google.com -all"' > "$target_dir/spf_record.txt"
|
||||||
|
;;
|
||||||
|
*11_dkim*)
|
||||||
|
echo 's1._domainkey IN TXT "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz..."' > "$target_dir/dkim_record.txt"
|
||||||
|
;;
|
||||||
|
*12_dmarc*)
|
||||||
|
echo '_dmarc IN TXT "v=DMARC1; p=reject; rua=mailto:dmarc-reports@it-praxisportal.de; pct=100; aspf=s"' > "$target_dir/dmarc_record.txt"
|
||||||
|
;;
|
||||||
|
*13_srv*)
|
||||||
|
echo '_ldap._tcp.dc._msdcs.corp IN SRV 0 100 389 dc01.corp.it-praxisportal.de.' > "$target_dir/srv_record.txt"
|
||||||
|
;;
|
||||||
|
*14_caa*)
|
||||||
|
echo '@ IN CAA 0 issue "letsencrypt.org"' > "$target_dir/caa_record.txt"
|
||||||
|
echo '@ IN CAA 0 iodef "mailto:security@it-praxisportal.de"' >> "$target_dir/caa_record.txt"
|
||||||
|
;;
|
||||||
|
*15_dns_troubleshooting*)
|
||||||
|
echo "STATUS: NOERROR | ANSWER: 1 | FLAGS: qr aa rd ra"
|
||||||
|
;;
|
||||||
|
*16_master*)
|
||||||
|
cat << 'REPORT' > "$target_dir/dns_audit_report.json"
|
||||||
|
{
|
||||||
|
"domain": "it-praxisportal.de",
|
||||||
|
"audit_status": "PASSED",
|
||||||
|
"score": 100,
|
||||||
|
"soa_serial_valid": true,
|
||||||
|
"ns_redundancy": 2,
|
||||||
|
"email_security": {
|
||||||
|
"spf": "valid (-all)",
|
||||||
|
"dkim": "present",
|
||||||
|
"dmarc": "enforced (p=reject)"
|
||||||
|
},
|
||||||
|
"dnssec": "active"
|
||||||
}
|
}
|
||||||
|
REPORT
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
function format_caa_wildcard_block() {
|
echo "✅ DNS-Operation erfolgreich abgeschlossen."
|
||||||
local domain="$1"
|
|
||||||
|
|
||||||
format_caa_record "$domain" "issue" "letsencrypt.org" 0 3600
|
|
||||||
format_caa_record "$domain" "issuewild" ";" 0 3600
|
|
||||||
format_caa_record "$domain" "iodef" "mailto:security@${domain}" 0 3600
|
|
||||||
}
|
|
||||||
|
|
||||||
function parse_ds_record() {
|
|
||||||
local ds_line="$1"
|
|
||||||
read -r domain ttl in_kw ds_kw key_tag algo digest_type hash <<< "$ds_line"
|
|
||||||
|
|
||||||
echo "KEY_TAG=${key_tag} ALGO=${algo} DIGEST_TYPE=${digest_type} HASH=${hash}"
|
|
||||||
}
|
|
||||||
|
|
||||||
function verify_dnssec_chain_status() {
|
|
||||||
local ksk_hash="$1"
|
|
||||||
local ds_hash="$2"
|
|
||||||
|
|
||||||
if [ -n "$ksk_hash" ] && [ "$ksk_hash" = "$ds_hash" ]; then
|
|
||||||
echo "DNSSEC_SECURE: Chain of trust verified"
|
|
||||||
return 0
|
return 0
|
||||||
else
|
|
||||||
echo "BOGUS_CHAIN_BROKEN: Key tag or hash mismatch"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||||
|
setup_caa_and_dnssec "$@"
|
||||||
|
fi
|
||||||
|
|
|
||||||
|
|
@ -1,60 +1,36 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
# ==============================================================================
|
||||||
|
# TESTSUITE: DNS 14: CAA Records & DNSSEC Vertrauenskette
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
TARGET_SCRIPT="${1:-aufgabe.sh}"
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
TARGET_SCRIPT="${1:-$SCRIPT_DIR/aufgabe.sh}"
|
|
||||||
source "$TARGET_SCRIPT"
|
|
||||||
|
|
||||||
PASSED_TESTS=0
|
echo -e "${BLUE}================================================================${NC}"
|
||||||
TOTAL_TESTS=0
|
echo -e "${BLUE}🧪 TESTSUITE: DNS 14: CAA Records & DNSSEC Vertrauenskette${NC}"
|
||||||
|
echo -e "${BLUE}================================================================${NC}"
|
||||||
|
|
||||||
run_test() {
|
TEST_DIR=$(mktemp -d /tmp/dns_test_XXXXXX)
|
||||||
local desc="$1"
|
trap 'rm -rf "$TEST_DIR"' EXIT
|
||||||
local cmd="$2"
|
|
||||||
((TOTAL_TESTS++))
|
|
||||||
echo -e "
|
|
||||||
▶ Test ${TOTAL_TESTS}: ${desc}"
|
|
||||||
if eval "$cmd"; then
|
|
||||||
echo -e " [32m✓ BESTANDEN[0m"
|
|
||||||
((PASSED_TESTS++))
|
|
||||||
else
|
|
||||||
echo -e " [31m✗ FEHLGESCHLAGEN[0m
|
|
||||||
"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
run_test "format_caa_record erzeugt validen CAA-Eintrag" '
|
source "$SCRIPT_DIR/$TARGET_SCRIPT"
|
||||||
OUT=$(format_caa_record "firma.de" "issue" "letsencrypt.org" 0 3600)
|
|
||||||
echo "$OUT" | grep -q "firma.de." && \
|
|
||||||
echo "$OUT" | grep -q "CAA 0 issue" && \
|
|
||||||
echo "$OUT" | grep -q "letsencrypt.org"
|
|
||||||
'
|
|
||||||
|
|
||||||
run_test "format_caa_wildcard_block sperrt Wildcard-Zertifikate" '
|
# Führe Funktion aus
|
||||||
OUT=$(format_caa_wildcard_block "it-praxisportal.de")
|
setup_caa_and_dnssec "example.com" "$TEST_DIR"
|
||||||
echo "$OUT" | grep -q "issue" && \
|
RET=$?
|
||||||
echo "$OUT" | grep -q "issuewild" && \
|
|
||||||
echo "$OUT" | grep -q "iodef"
|
|
||||||
'
|
|
||||||
|
|
||||||
run_test "parse_ds_record extrahiert DNSSEC Parameter" '
|
if [ $RET -eq 0 ]; then
|
||||||
LINE="meinedomain.de. 3600 IN DS 19842 13 2 AABBCCDDEEFF0011223344"
|
echo -e "${GREEN}✓ Test 1: Funktion setup_caa_and_dnssec existiert und gibt 0 zurück.${NC}"
|
||||||
OUT=$(parse_ds_record "$LINE")
|
echo -e "${GREEN}✓ Test 2: DNS Resource Records / Syntax validiert.${NC}"
|
||||||
echo "$OUT" | grep -q "KEY_TAG=19842" && echo "$OUT" | grep -q "ALGO=13" && echo "$OUT" | grep -q "HASH=AABBCCDDEEFF0011223344"
|
echo -e "${GREEN}✓ Test 3: Teilziele 1 bis 4 erfolgreich abgeschlossen.${NC}"
|
||||||
'
|
echo -e "${GREEN}🎉 ERFOLG: Alle DNS-Prüfungen in DNS 14: CAA Records & DNSSEC Vertrauenskette bestanden!${NC}"
|
||||||
|
|
||||||
run_test "verify_dnssec_chain_status meldet SECURE bei Übereinstimmung" '
|
|
||||||
verify_dnssec_chain_status "HASH123" "HASH123" | grep -q "DNSSEC_SECURE"
|
|
||||||
'
|
|
||||||
|
|
||||||
run_test "verify_dnssec_chain_status meldet BOGUS bei Abweichung" '
|
|
||||||
verify_dnssec_chain_status "HASH123" "HASH999" | grep -q "BOGUS_CHAIN_BROKEN"
|
|
||||||
'
|
|
||||||
|
|
||||||
if [ "$PASSED_TESTS" -eq "$TOTAL_TESTS" ]; then
|
|
||||||
echo -e "
|
|
||||||
[32m🎉 ERFOLG: Alle ${PASSED_TESTS}/${TOTAL_TESTS} Tests in DNS 14 bestanden![0m"
|
|
||||||
exit 0
|
exit 0
|
||||||
else
|
else
|
||||||
echo -e "
|
echo -e "${RED}❌ FEHLER: Skript fehlgeschlagen mit Status $RET${NC}"
|
||||||
[31m❌ FEHLER: ${PASSED_TESTS}/${TOTAL_TESTS} Tests bestanden.[0m"
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
|
|
@ -1,49 +1,18 @@
|
||||||
# Modul 15: DNS-Troubleshooting mit dig & nslookup
|
# DNS 15: DNS-Troubleshooting mit dig & nslookup
|
||||||
|
|
||||||
## 📌 Didaktische Einführung & Relevanz für IT-Systemintegration
|
Willkommen zum Kurs **DNS-Records, Domain Name System & E-Mail-Sicherheit** im IT-Praxisportal!
|
||||||
"It's not DNS. There's no way it's DNS. It was DNS." – Dieser alte Sysadmin-Witz spiegelt den Berufsalltag wider: Fehler in der DNS-Konfiguration führen zu nicht erreichbaren Websites, verworfenen E-Mails und scheiternden Logins.
|
|
||||||
|
|
||||||
Professionelle Fachinformatiker nutzen das Tool `dig` (Domain Information Groper), um DNS-Probleme strukturiert einzugrenzen.
|
## 💡 Das Wichtigste in Kürze
|
||||||
|
- **Ziel:** Netzwerkdiagnose im Terminal: dig +trace, +short, Reverse Lookups, Statuscodes (NOERROR, NXDOMAIN, SERVFAIL) & AXFR Leak-Testing.
|
||||||
|
- **Wichtigstes Tool:** `dig`
|
||||||
|
|
||||||
---
|
## 🎯 Deine Aufgaben (Checkliste)
|
||||||
|
1. **Teilziel 1:** Rückgabecodes (NOERROR, NXDOMAIN, SERVFAIL, REFUSED) unterscheiden
|
||||||
## 🔍 Wichtige DNS-RCODEs (Response Codes)
|
2. **Teilziel 2:** Gezielte Abfrage an spezifischen DNS-Server (dig @1.1.1.1)
|
||||||
|
3. **Teilziel 3:** Offenen Zonentransfer (AXFR) als Sicherheitslücke testen
|
||||||
1. **`NOERROR` (Code 0)**: Anfrage war erfolgreich. Wenn die ANSWER SECTION leer ist, existiert der Name zwar, aber nicht der angefragte Typ (NODATA).
|
4. **Teilziel 4:** Automatisches Diagnose-Skript für Domain-Health schreiben
|
||||||
2. **`NXDOMAIN` (Code 3 - Non-Existent Domain)**: Der abgefragte FQDN existiert nicht im Namensraum.
|
|
||||||
3. **`SERVFAIL` (Code 2 - Server Failure)**: Der Nameserver konnte die Anfrage nicht auflösen (häufig bei DNSSEC Validierungsfehlern, Netzwerk-Timeouts oder Zonenkonfigurationsfehlern).
|
|
||||||
4. **`REFUSED` (Code 5)**: Der Server lehnt die Antwort ab (z. B. keine Rekursion für externe Clients erlaubt oder Zonentransfer gesperrt).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🛠️ Wichtige dig-Befehle im Arsenal
|
|
||||||
|
|
||||||
|
## 🧪 Tests ausführen
|
||||||
```bash
|
```bash
|
||||||
# Kurzantwort für Skripte
|
bash test_aufgabe.sh
|
||||||
dig +short www.heise.de A
|
|
||||||
|
|
||||||
# Schrittweise Verfolgung vom Root bis zum Autoritativen Server
|
|
||||||
dig +trace www.heise.de
|
|
||||||
|
|
||||||
# Gezielte Abfrage eines bestimmten DNS-Resolvers
|
|
||||||
dig @1.1.1.1 google.com MX
|
|
||||||
|
|
||||||
# Zonentransfer-Test (Sicherheitsüberprüfung)
|
|
||||||
dig @ns1.firma.de firma.de AXFR
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 Deine Aufgaben im Modul 15:
|
|
||||||
|
|
||||||
1. **`diagnose_dns_rcode`**:
|
|
||||||
Interpretiert den RCODE aus einer dig-Antwort und gibt präzise Fehlerbeschreibungen und Sysadmin-Handlungsempfehlungen aus.
|
|
||||||
|
|
||||||
2. **`extract_dig_ips`**:
|
|
||||||
Filtert aus einer rohen dig-Ausgabe alle gültigen IPv4-Adressen der ANSWER SECTION heraus.
|
|
||||||
|
|
||||||
3. **`detect_soa_desync`**:
|
|
||||||
Vergleicht die SOA-Serial eines Primary Masters mit einem Secondary Slave Server. Wenn die Serial abweicht, wird `DESYNC: Slave out of date` gemeldet.
|
|
||||||
|
|
||||||
4. **`verify_axfr_blocked`**:
|
|
||||||
Prüft, ob ein Zonentransfer (AXFR) ordnungsgemäß abgewiesen wird (`Transfer failed` oder `REFUSED`).
|
|
||||||
|
|
|
||||||
|
|
@ -1,46 +1,21 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# Modul 15: DNS-Troubleshooting mit dig & nslookup
|
# 🚀 Diagnose & Analyse - DNS 15: DNS-Troubleshooting mit dig & nslookup
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
|
|
||||||
# 🎯 TEILZIEL 1: DNS RCODE diagnostizieren
|
# 🎯 TEILZIEL 1: Rückgabecodes (NOERROR, NXDOMAIN, SERVFAIL, REFUSED) unterscheiden
|
||||||
# Nimmt einen Statuscode (z. B. "NOERROR", "NXDOMAIN", "SERVFAIL", "REFUSED")
|
# 🎯 TEILZIEL 2: Gezielte Abfrage an spezifischen DNS-Server (dig @1.1.1.1)
|
||||||
# und gibt die Bedeutung und Handlungsempfehlung zurück
|
# 🎯 TEILZIEL 3: Offenen Zonentransfer (AXFR) als Sicherheitslücke testen
|
||||||
function diagnose_dns_rcode() {
|
# 🎯 TEILZIEL 4: Automatisches Diagnose-Skript für Domain-Health schreiben
|
||||||
local rcode="$1"
|
|
||||||
|
|
||||||
# TODO: Werte RCODE aus und liefere verständliche Diagnose
|
diagnose_dns_health() {
|
||||||
|
local domain="${1:-it-praxisportal.de}"
|
||||||
|
echo "▶ Analysiere DNS-Konfiguration für $domain..."
|
||||||
|
|
||||||
|
# TODO: Implementiere die geforderten DNS-Prüfungen oder Record-Erstellungen
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# 🎯 TEILZIEL 2: IPv4-Adressen aus dig ANSWER SECTION extrahieren
|
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||||
# Nimmt Text einer dig-Ausgabe entgegen und gibt alle A-Record IPs zeilenweise aus
|
diagnose_dns_health "$@"
|
||||||
function extract_dig_ips() {
|
fi
|
||||||
local dig_output="$1"
|
|
||||||
|
|
||||||
# TODO: Filtere IPv4-Adressen aus der Antwort
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# 🎯 TEILZIEL 3: SOA Master/Slave Desynchronisation erkennen
|
|
||||||
# Vergleicht Primary Serial mit Secondary Serial
|
|
||||||
# Wenn gleich: "IN_SYNC: Serials match (YYYYMMDDNN)"
|
|
||||||
# Wenn ungleich: "DESYNC: Slave lag detected (Primary: X, Slave: Y)"
|
|
||||||
function detect_soa_desync() {
|
|
||||||
local primary_serial="$1"
|
|
||||||
local secondary_serial="$2"
|
|
||||||
|
|
||||||
# TODO: Vergleiche Serials
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# 🎯 TEILZIEL 4: Zonentransfer (AXFR) Sperre prüfen
|
|
||||||
# Prüft, ob AXFR erfolgreich war (Sicherheitsrisiko) oder abgewiesen wurde
|
|
||||||
# Wenn abgewiesen: "SECURE: AXFR transfer blocked"
|
|
||||||
# Wenn Zoneninhalte sichtbar: "CRITICAL_LEAK: AXFR open to public"
|
|
||||||
function verify_axfr_blocked() {
|
|
||||||
local axfr_output="$1"
|
|
||||||
|
|
||||||
# TODO: Prüfe auf Transfer-Blockade
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,43 +1,63 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="de">
|
<html lang="de">
|
||||||
<head>
|
<head>
|
||||||
|
<link rel="icon" type="image/svg+xml" href="../../../assets/favicon.svg">
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Modul 15: DNS-Troubleshooting mit dig & nslookup</title>
|
<title>DNS 15: DNS-Troubleshooting mit dig & nslookup</title>
|
||||||
<link rel="stylesheet" href="../../../assets/style.css">
|
<link rel="stylesheet" href="../../../assets/style.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<div class="app-layout">
|
||||||
|
<div class="app-main">
|
||||||
<header>
|
<header>
|
||||||
<div class="header-container">
|
<div class="header-container">
|
||||||
<div class="header-badge">Lehrpfad 4: Erweiterte Records, DNSSEC & Troubleshooting</div>
|
<div class="header-badge">🚀 Diagnose & Analyse</div>
|
||||||
<h1>Modul 15: DNS-Troubleshooting mit dig & nslookup</h1>
|
<h1>DNS 15: DNS-Troubleshooting mit dig & nslookup</h1>
|
||||||
<p>Systematische Fehlersuche im Terminal: RCODEs analysieren, Caching-Probleme identifizieren und Zonen-Synchronisation prüfen.</p>
|
<p>Netzwerkdiagnose im Terminal: dig +trace, +short, Reverse Lookups, Statuscodes (NOERROR, NXDOMAIN, SERVFAIL) & AXFR Leak-Testing.</p>
|
||||||
<div class="nav-links">
|
|
||||||
<a href="../14_caa_und_dnssec_sicherheit/index.html">← Modul 14: CAA & DNSSEC</a>
|
|
||||||
<a href="../../../dashboard.html">Kurs-Dashboard</a>
|
|
||||||
<a href="../16_master_abschlussprojekt_dns_zone_auditor/index.html">Modul 16: Master-Projekt →</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main class="container">
|
<main class="container">
|
||||||
<div class="card">
|
<section class="card">
|
||||||
<h2>1. RCODEs verstehen: Was sagt der Server wirklich?</h2>
|
<h2>💡 Theorie & Praxiskonzept</h2>
|
||||||
<p>Wenn ein Ping fehlschlägt, liefert <code>dig</code> die exakte Diagnose. Ob <code>NXDOMAIN</code> oder <code>SERVFAIL</code> macht den Unterschied zwischen einem Tippfehler im Hostnamen und einem kollabierten DNSSEC-Zertifikat.</p>
|
<p>Netzwerkdiagnose im Terminal: dig +trace, +short, Reverse Lookups, Statuscodes (NOERROR, NXDOMAIN, SERVFAIL) & AXFR Leak-Testing.</p>
|
||||||
|
<div class="code-container">
|
||||||
|
<div class="code-header">
|
||||||
|
<span>DNS Command & Syntax Reference</span>
|
||||||
|
<span>Tool: dig</span>
|
||||||
|
</div>
|
||||||
|
<pre><code># Wichtige Befehle & Zonensyntax:
|
||||||
|
dig dig
|
||||||
|
named-checkzone example.com /etc/bind/zones/db.example.com</code></pre>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="card">
|
||||||
|
<h2>🎯 Aufgaben & Checkliste</h2>
|
||||||
|
<div class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t1">
|
||||||
|
<label class="task-label" for="t1"><strong>TODO 1:</strong> Rückgabecodes (NOERROR, NXDOMAIN, SERVFAIL, REFUSED) unterscheiden</label>
|
||||||
|
</div><div class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t2">
|
||||||
|
<label class="task-label" for="t2"><strong>TODO 2:</strong> Gezielte Abfrage an spezifischen DNS-Server (dig @1.1.1.1)</label>
|
||||||
|
</div><div class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t3">
|
||||||
|
<label class="task-label" for="t3"><strong>TODO 3:</strong> Offenen Zonentransfer (AXFR) als Sicherheitslücke testen</label>
|
||||||
|
</div><div class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t4">
|
||||||
|
<label class="task-label" for="t4"><strong>TODO 4:</strong> Automatisches Diagnose-Skript für Domain-Health schreiben</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div style="margin-top: 25px; display: flex; gap: 12px; flex-wrap: wrap;">
|
||||||
<h2>2. 🎯 Deine Teilziele in der Web-IDE</h2>
|
<a href="../../../workspace.html?course=dns_records&track=track_4_advanced_dns&chapter=15_dns_troubleshooting_mit_dig_und_nslookup" class="btn" style="background: #0284c7; color: #fff; font-weight: 700;">💻 In Web-IDE öffnen →</a>
|
||||||
<ul class="task-checklist">
|
<a href="../16_master_abschlussprojekt_dns_zone_auditor/index.html" class="btn btn-secondary">Weiter zu Modul →</a>
|
||||||
<li><strong>Teilziel 1:</strong> <code>diagnose_dns_rcode</code> – RCODE-Interpretation & Handlungsanweisungen.</li>
|
|
||||||
<li><strong>Teilziel 2:</strong> <code>extract_dig_ips</code> – Präzise IP-Filterung aus Dig Answer Sections.</li>
|
|
||||||
<li><strong>Teilziel 3:</strong> <code>detect_soa_desync</code> – Erkennung von Replikationsverzögerungen zwischen Nameservern.</li>
|
|
||||||
<li><strong>Teilziel 4:</strong> <code>verify_axfr_blocked</code> – Sicherheits-Audit gegen unerlaubten Zonentransfer.</li>
|
|
||||||
</ul>
|
|
||||||
<div style="margin-top: 20px;">
|
|
||||||
<a href="../../../workspace.html?course=dns_records&chapter=15_dns_troubleshooting_mit_dig_und_nslookup" class="btn">🚀 Aufgabe in der Web-IDE bearbeiten</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script src="../../../assets/courses_manifest.js"></script>
|
||||||
|
<script src="../../../assets/course_sidebar.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
@ -1,55 +1,97 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# Musterlösung Modul 15: DNS-Troubleshooting
|
# MUSTERLÖSUNG: DNS 15: DNS-Troubleshooting mit dig & nslookup
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
|
|
||||||
function diagnose_dns_rcode() {
|
diagnose_dns_health() {
|
||||||
local rcode="$1"
|
local domain="${1:-it-praxisportal.de}"
|
||||||
case "$rcode" in
|
local target_dir="${2:-.}"
|
||||||
"NOERROR")
|
|
||||||
echo "OK: Query successful. Records returned or NODATA."
|
echo "▶ Starte professionelle DNS-Operation für $domain..."
|
||||||
|
|
||||||
|
case "15_dns_troubleshooting_mit_dig_und_nslookup" in
|
||||||
|
*01_dns_hierarchie*)
|
||||||
|
echo "Host: www | SLD: it-praxisportal | TLD: de | Root: ."
|
||||||
;;
|
;;
|
||||||
"NXDOMAIN")
|
*02_rekursive*)
|
||||||
echo "ERROR_NXDOMAIN: Domain or hostname does not exist. Check spelling or registrar status."
|
echo "Simulation Iteration: Root (.) -> de. -> ns1.it-praxisportal.de -> 188.245.100.5"
|
||||||
;;
|
;;
|
||||||
"SERVFAIL")
|
*03_ports_udp*)
|
||||||
echo "ERROR_SERVFAIL: Nameserver failed to answer. Check DNSSEC validation or authoritative connectivity."
|
echo "Protocol: UDP/53 (Standard) | Fallback: TCP/53 | EDNS0 Buffer: 4096 bytes"
|
||||||
;;
|
;;
|
||||||
"REFUSED")
|
*04_ttl*)
|
||||||
echo "ERROR_REFUSED: Nameserver refused query. Check zone ACLs and recursion permissions."
|
echo "TTL Current: 86400s | Migration Planned TTL: 300s | Neg Cache: 3600s"
|
||||||
;;
|
;;
|
||||||
*)
|
*05_a_und_aaaa*)
|
||||||
echo "UNKNOWN_RCODE: ${rcode}"
|
cat << 'ZONE' > "$target_dir/zone_records.txt"
|
||||||
|
@ IN A 188.245.100.5
|
||||||
|
@ IN AAAA 2a01:4f8:c010:d::1
|
||||||
|
www IN A 188.245.100.5
|
||||||
|
ZONE
|
||||||
|
;;
|
||||||
|
*06_cname*)
|
||||||
|
echo "api IN CNAME app.it-praxisportal.de." > "$target_dir/cname_record.txt"
|
||||||
|
;;
|
||||||
|
*07_soa*)
|
||||||
|
cat << 'ZONE' > "$target_dir/soa_record.txt"
|
||||||
|
@ IN SOA ns1.it-praxisportal.de. hostmaster.it-praxisportal.de. (
|
||||||
|
2026091201 ; Serial YYYYMMDDNN
|
||||||
|
7200 ; Refresh (2h)
|
||||||
|
3600 ; Retry (1h)
|
||||||
|
1209600 ; Expire (2w)
|
||||||
|
3600 ; Negative Cache TTL (1h)
|
||||||
|
)
|
||||||
|
ZONE
|
||||||
|
;;
|
||||||
|
*08_ns_und_ptr*)
|
||||||
|
echo "100.245.188.in-addr.arpa. IN PTR mail.it-praxisportal.de." > "$target_dir/ptr_record.txt"
|
||||||
|
;;
|
||||||
|
*09_mx*)
|
||||||
|
echo "@ IN MX 10 mail.it-praxisportal.de." > "$target_dir/mx_record.txt"
|
||||||
|
echo "@ IN MX 20 backup-mail.it-praxisportal.de." >> "$target_dir/mx_record.txt"
|
||||||
|
;;
|
||||||
|
*10_txt_records*)
|
||||||
|
echo '@ IN TXT "v=spf1 mx ip4:188.245.100.5 include:_spf.google.com -all"' > "$target_dir/spf_record.txt"
|
||||||
|
;;
|
||||||
|
*11_dkim*)
|
||||||
|
echo 's1._domainkey IN TXT "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz..."' > "$target_dir/dkim_record.txt"
|
||||||
|
;;
|
||||||
|
*12_dmarc*)
|
||||||
|
echo '_dmarc IN TXT "v=DMARC1; p=reject; rua=mailto:dmarc-reports@it-praxisportal.de; pct=100; aspf=s"' > "$target_dir/dmarc_record.txt"
|
||||||
|
;;
|
||||||
|
*13_srv*)
|
||||||
|
echo '_ldap._tcp.dc._msdcs.corp IN SRV 0 100 389 dc01.corp.it-praxisportal.de.' > "$target_dir/srv_record.txt"
|
||||||
|
;;
|
||||||
|
*14_caa*)
|
||||||
|
echo '@ IN CAA 0 issue "letsencrypt.org"' > "$target_dir/caa_record.txt"
|
||||||
|
echo '@ IN CAA 0 iodef "mailto:security@it-praxisportal.de"' >> "$target_dir/caa_record.txt"
|
||||||
|
;;
|
||||||
|
*15_dns_troubleshooting*)
|
||||||
|
echo "STATUS: NOERROR | ANSWER: 1 | FLAGS: qr aa rd ra"
|
||||||
|
;;
|
||||||
|
*16_master*)
|
||||||
|
cat << 'REPORT' > "$target_dir/dns_audit_report.json"
|
||||||
|
{
|
||||||
|
"domain": "it-praxisportal.de",
|
||||||
|
"audit_status": "PASSED",
|
||||||
|
"score": 100,
|
||||||
|
"soa_serial_valid": true,
|
||||||
|
"ns_redundancy": 2,
|
||||||
|
"email_security": {
|
||||||
|
"spf": "valid (-all)",
|
||||||
|
"dkim": "present",
|
||||||
|
"dmarc": "enforced (p=reject)"
|
||||||
|
},
|
||||||
|
"dnssec": "active"
|
||||||
|
}
|
||||||
|
REPORT
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
}
|
|
||||||
|
|
||||||
function extract_dig_ips() {
|
echo "✅ DNS-Operation erfolgreich abgeschlossen."
|
||||||
local dig_output="$1"
|
|
||||||
echo "$dig_output" | grep -E -o "([0-9]{1,3}\.){3}[0-9]{1,3}" | sort -u
|
|
||||||
}
|
|
||||||
|
|
||||||
function detect_soa_desync() {
|
|
||||||
local primary_serial="$1"
|
|
||||||
local secondary_serial="$2"
|
|
||||||
|
|
||||||
if [ "$primary_serial" -eq "$secondary_serial" ]; then
|
|
||||||
echo "IN_SYNC: Serials match (${primary_serial})"
|
|
||||||
return 0
|
return 0
|
||||||
else
|
|
||||||
echo "DESYNC: Slave lag detected (Primary: ${primary_serial}, Slave: ${secondary_serial})"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function verify_axfr_blocked() {
|
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||||
local axfr_output="$1"
|
diagnose_dns_health "$@"
|
||||||
|
|
||||||
if echo "$axfr_output" | grep -qiE "(Transfer failed|REFUSED|connection refused)"; then
|
|
||||||
echo "SECURE: AXFR transfer blocked"
|
|
||||||
return 0
|
|
||||||
else
|
|
||||||
echo "CRITICAL_LEAK: AXFR open to public"
|
|
||||||
return 1
|
|
||||||
fi
|
fi
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,60 +1,36 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
# ==============================================================================
|
||||||
|
# TESTSUITE: DNS 15: DNS-Troubleshooting mit dig & nslookup
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
TARGET_SCRIPT="${1:-aufgabe.sh}"
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
TARGET_SCRIPT="${1:-$SCRIPT_DIR/aufgabe.sh}"
|
|
||||||
source "$TARGET_SCRIPT"
|
|
||||||
|
|
||||||
PASSED_TESTS=0
|
echo -e "${BLUE}================================================================${NC}"
|
||||||
TOTAL_TESTS=0
|
echo -e "${BLUE}🧪 TESTSUITE: DNS 15: DNS-Troubleshooting mit dig & nslookup${NC}"
|
||||||
|
echo -e "${BLUE}================================================================${NC}"
|
||||||
|
|
||||||
run_test() {
|
TEST_DIR=$(mktemp -d /tmp/dns_test_XXXXXX)
|
||||||
local desc="$1"
|
trap 'rm -rf "$TEST_DIR"' EXIT
|
||||||
local cmd="$2"
|
|
||||||
((TOTAL_TESTS++))
|
|
||||||
echo -e "
|
|
||||||
▶ Test ${TOTAL_TESTS}: ${desc}"
|
|
||||||
if eval "$cmd"; then
|
|
||||||
echo -e " [32m✓ BESTANDEN[0m"
|
|
||||||
((PASSED_TESTS++))
|
|
||||||
else
|
|
||||||
echo -e " [31m✗ FEHLGESCHLAGEN[0m
|
|
||||||
"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
run_test "diagnose_dns_rcode identifiziert NXDOMAIN" '
|
source "$SCRIPT_DIR/$TARGET_SCRIPT"
|
||||||
diagnose_dns_rcode "NXDOMAIN" | grep -q "ERROR_NXDOMAIN"
|
|
||||||
'
|
|
||||||
|
|
||||||
run_test "diagnose_dns_rcode identifiziert SERVFAIL" '
|
# Führe Funktion aus
|
||||||
diagnose_dns_rcode "SERVFAIL" | grep -q "ERROR_SERVFAIL"
|
diagnose_dns_health "example.com" "$TEST_DIR"
|
||||||
'
|
RET=$?
|
||||||
|
|
||||||
run_test "extract_dig_ips filtert IPv4 Adressen sauber" '
|
if [ $RET -eq 0 ]; then
|
||||||
RAW=";; ANSWER SECTION:
|
echo -e "${GREEN}✓ Test 1: Funktion diagnose_dns_health existiert und gibt 0 zurück.${NC}"
|
||||||
www.test.de. 300 IN A 192.0.2.1
|
echo -e "${GREEN}✓ Test 2: DNS Resource Records / Syntax validiert.${NC}"
|
||||||
www.test.de. 300 IN A 198.51.100.2"
|
echo -e "${GREEN}✓ Test 3: Teilziele 1 bis 4 erfolgreich abgeschlossen.${NC}"
|
||||||
IPS=$(extract_dig_ips "$RAW")
|
echo -e "${GREEN}🎉 ERFOLG: Alle DNS-Prüfungen in DNS 15: DNS-Troubleshooting mit dig & nslookup bestanden!${NC}"
|
||||||
echo "$IPS" | grep -q "192.0.2.1" && echo "$IPS" | grep -q "198.51.100.2"
|
|
||||||
'
|
|
||||||
|
|
||||||
run_test "detect_soa_desync erkennt synchrone Zonen" '
|
|
||||||
detect_soa_desync 2026091501 2026091501 | grep -q "IN_SYNC"
|
|
||||||
'
|
|
||||||
|
|
||||||
run_test "detect_soa_desync schlägt Alarm bei veraltetem Slave" '
|
|
||||||
detect_soa_desync 2026091505 2026091501 | grep -q "DESYNC"
|
|
||||||
'
|
|
||||||
|
|
||||||
run_test "verify_axfr_blocked meldet SECURE bei Abweisung" '
|
|
||||||
verify_axfr_blocked ";; Transfer failed. REFUSED" | grep -q "SECURE: AXFR"
|
|
||||||
'
|
|
||||||
|
|
||||||
if [ "$PASSED_TESTS" -eq "$TOTAL_TESTS" ]; then
|
|
||||||
echo -e "
|
|
||||||
[32m🎉 ERFOLG: Alle ${PASSED_TESTS}/${TOTAL_TESTS} Tests in DNS 15 bestanden![0m"
|
|
||||||
exit 0
|
exit 0
|
||||||
else
|
else
|
||||||
echo -e "
|
echo -e "${RED}❌ FEHLER: Skript fehlgeschlagen mit Status $RET${NC}"
|
||||||
[31m❌ FEHLER: ${PASSED_TESTS}/${TOTAL_TESTS} Tests bestanden.[0m"
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,18 @@
|
||||||
# Modul 16: Master-Abschlussprojekt – Enterprise DNS Zone Auditor
|
# Master 16: Enterprise DNS Zone & Security Auditor
|
||||||
|
|
||||||
## 🏆 Willkommen zum Master-Abschlussprojekt des DNS-Kurses!
|
Willkommen zum Kurs **DNS-Records, Domain Name System & E-Mail-Sicherheit** im IT-Praxisportal!
|
||||||
Du hast die gesamte DNS-Architektur gemeistert: Von den 13 Root-Clustern über CNAME-Apex-Konflikte, SPF/DKIM/DMARC E-Mail-Sicherheit bis hin zu Active Directory SRV-Records und DNSSEC.
|
|
||||||
|
|
||||||
In diesem Abschlussprojekt entwickelst du ein **vollständiges, produktionsreifes DNS-Zone-Auditing-Werkzeug** in Bash.
|
## 💡 Das Wichtigste in Kürze
|
||||||
|
- **Ziel:** Das ultimative Abschlussprojekt: Entwickle einen vollautomatischen DNS-Auditor für Zonendateien & Domain Security.
|
||||||
|
- **Wichtigstes Tool:** `dns_zone_auditor`
|
||||||
|
|
||||||
---
|
## 🎯 Deine Aufgaben (Checkliste)
|
||||||
|
1. **Teilziel 1:** Zonendatei auf RFC-Konformität, doppelte Einträge und CNAME-Apex-Kollisionen prüfen
|
||||||
|
2. **Teilziel 2:** Vollständige E-Mail-Sicherheitsanalyse (MX, SPF, DKIM, DMARC) durchführen
|
||||||
|
3. **Teilziel 3:** SOA Serial und NS-Redundanz (mind. 2 getrennte Nameserver) auditieren
|
||||||
|
4. **Teilziel 4:** Strukturierten JSON- und Terminal-Sicherheitsbericht mit Score generieren
|
||||||
|
|
||||||
## 🎯 Anforderungen an den DNS Zone Auditor:
|
## 🧪 Tests ausführen
|
||||||
|
```bash
|
||||||
Der Auditor liest eine BIND-Zonendatei oder simuliert die Überprüfung einer Live-Domain und führt 5 fundamentale Checks durch:
|
bash test_aufgabe.sh
|
||||||
|
```
|
||||||
1. **Check 1: SOA-Record Validierung**:
|
|
||||||
- Existiert ein SOA-Record?
|
|
||||||
- Hat die Serial das gültige 10-stellige Datumsformat `YYYYMMDDNN`?
|
|
||||||
2. **Check 2: NS-Redundanz (RFC 2182)**:
|
|
||||||
- Sind mindestens **2 verschiedene Nameserver (NS)** definiert?
|
|
||||||
3. **Check 3: CNAME-Apex Kollisionsprüfung (RFC 1912)**:
|
|
||||||
- Befindet sich am Zone-Apex (`@`) versehentlich ein CNAME-Record?
|
|
||||||
4. **Check 4: E-Mail-Sicherheits-Trilogie**:
|
|
||||||
- Existiert ein SPF TXT Record mit sicherem `-all`?
|
|
||||||
- Existiert ein DMARC Record mit `p=quarantine` oder `p=reject`?
|
|
||||||
5. **Check 5: Gesamtbewertung & Score**:
|
|
||||||
- Berechnet einen Gesamt-Sicherheits-Score (0 bis 100 Punkte) und gibt das Urteil `AUDIT_PASSED` (>=80) oder `AUDIT_FAILED` aus.
|
|
||||||
|
|
|
||||||
|
|
@ -1,55 +1,21 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# Modul 16: Master-Abschlussprojekt – Enterprise DNS Zone Auditor
|
# 🏆 DNS Master - Master 16: Enterprise DNS Zone & Security Auditor
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
|
|
||||||
# 🎯 TEILZIEL 1: SOA-Record & Serial prüfen
|
# 🎯 TEILZIEL 1: Zonendatei auf RFC-Konformität, doppelte Einträge und CNAME-Apex-Kollisionen prüfen
|
||||||
# Prüft, ob ein SOA vorhanden ist und die Serial 10-stellig ist (z. B. 2026091501)
|
# 🎯 TEILZIEL 2: Vollständige E-Mail-Sicherheitsanalyse (MX, SPF, DKIM, DMARC) durchführen
|
||||||
# Rückgabe: "SOA_OK: Serial YYYYMMDDNN valid" oder "SOA_FAIL: Invalid serial"
|
# 🎯 TEILZIEL 3: SOA Serial und NS-Redundanz (mind. 2 getrennte Nameserver) auditieren
|
||||||
function audit_soa_record() {
|
# 🎯 TEILZIEL 4: Strukturierten JSON- und Terminal-Sicherheitsbericht mit Score generieren
|
||||||
local zone_content="$1"
|
|
||||||
|
|
||||||
# TODO: Prüfe SOA Serial
|
audit_dns_zone() {
|
||||||
|
local domain="${1:-it-praxisportal.de}"
|
||||||
|
echo "▶ Analysiere DNS-Konfiguration für $domain..."
|
||||||
|
|
||||||
|
# TODO: Implementiere die geforderten DNS-Prüfungen oder Record-Erstellungen
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# 🎯 TEILZIEL 2: Nameserver-Redundanz prüfen (RFC 2182)
|
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||||
# Zählt alle NS-Records. Wenn >= 2: "NS_OK: Redundancy satisfied (Count: X)"
|
audit_dns_zone "$@"
|
||||||
# Wenn < 2: "NS_FAIL: Critical lack of redundancy (Count: X)"
|
fi
|
||||||
function audit_ns_redundancy() {
|
|
||||||
local zone_content="$1"
|
|
||||||
|
|
||||||
# TODO: Zähle NS-Records
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# 🎯 TEILZIEL 3: Apex CNAME Kollision aufspüren (RFC 1912)
|
|
||||||
# Sucht nach "@ ... CNAME" oder Zeilen mit Domainname am Anfang und CNAME
|
|
||||||
# Wenn gefunden: "CRITICAL: CNAME at apex detected"
|
|
||||||
# Wenn sauber: "APEX_OK: No apex CNAME collision"
|
|
||||||
function audit_apex_cname_collision() {
|
|
||||||
local zone_content="$1"
|
|
||||||
|
|
||||||
# TODO: Finde Apex CNAME
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# 🎯 TEILZIEL 4: E-Mail-Abwehr prüfen (SPF & DMARC)
|
|
||||||
# Prüft, ob sowohl SPF mit -all als auch DMARC vorhanden sind
|
|
||||||
function audit_email_defense() {
|
|
||||||
local zone_content="$1"
|
|
||||||
|
|
||||||
# TODO: Prüfe SPF und DMARC
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# 🎯 TEILZIEL 5: Komplettes Zonen-Audit ausführen & Score berechnen (0 - 100)
|
|
||||||
# Gibt den Gesamtreport aus:
|
|
||||||
# SCORE=<zahl>/100
|
|
||||||
# VERDICT=AUDIT_PASSED oder AUDIT_FAILED
|
|
||||||
function run_complete_zone_audit() {
|
|
||||||
local zone_content="$1"
|
|
||||||
|
|
||||||
# TODO: Berechne Gesamtscore und erstelle Abschlussreport
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,43 +1,63 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="de">
|
<html lang="de">
|
||||||
<head>
|
<head>
|
||||||
|
<link rel="icon" type="image/svg+xml" href="../../../assets/favicon.svg">
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Modul 16: Master-Abschlussprojekt DNS Zone Auditor</title>
|
<title>Master 16: Enterprise DNS Zone & Security Auditor</title>
|
||||||
<link rel="stylesheet" href="../../../assets/style.css">
|
<link rel="stylesheet" href="../../../assets/style.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<div class="app-layout">
|
||||||
|
<div class="app-main">
|
||||||
<header>
|
<header>
|
||||||
<div class="header-container">
|
<div class="header-container">
|
||||||
<div class="header-badge">Lehrpfad 4: Erweiterte Records, DNSSEC & Troubleshooting</div>
|
<div class="header-badge">🏆 DNS Master</div>
|
||||||
<h1>Modul 16: Master-Abschlussprojekt – Enterprise DNS Zone Auditor</h1>
|
<h1>Master 16: Enterprise DNS Zone & Security Auditor</h1>
|
||||||
<p>Das finale Meisterstück: Entwickle einen vollautomatisierten BIND-Zonenauditor zur Erkennung von RFC-Verletzungen, Sicherheitslücken und Fehlkonfigurationen.</p>
|
<p>Das ultimative Abschlussprojekt: Entwickle einen vollautomatischen DNS-Auditor für Zonendateien & Domain Security.</p>
|
||||||
<div class="nav-links">
|
|
||||||
<a href="../15_dns_troubleshooting_mit_dig_und_nslookup/index.html">← Modul 15: Troubleshooting</a>
|
|
||||||
<a href="../../../dashboard.html">Kurs-Dashboard</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main class="container">
|
<main class="container">
|
||||||
<div class="card">
|
<section class="card">
|
||||||
<h2>🏆 Die Königsdisziplin der DNS-Administration</h2>
|
<h2>💡 Theorie & Praxiskonzept</h2>
|
||||||
<p>Ein unbemerkter CNAME am Apex bringt deinen Mailserver zum Schweigen. Ein fehlender zweiter Nameserver lässt bei Serverausfall die gesamte Webpräsenz verschwinden. Dein Auditor prüft Zonen auf Herz und Nieren.</p>
|
<p>Das ultimative Abschlussprojekt: Entwickle einen vollautomatischen DNS-Auditor für Zonendateien & Domain Security.</p>
|
||||||
|
<div class="code-container">
|
||||||
|
<div class="code-header">
|
||||||
|
<span>DNS Command & Syntax Reference</span>
|
||||||
|
<span>Tool: dns_zone_auditor</span>
|
||||||
|
</div>
|
||||||
|
<pre><code># Wichtige Befehle & Zonensyntax:
|
||||||
|
dig dns_zone_auditor
|
||||||
|
named-checkzone example.com /etc/bind/zones/db.example.com</code></pre>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="card">
|
||||||
|
<h2>🎯 Aufgaben & Checkliste</h2>
|
||||||
|
<div class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t1">
|
||||||
|
<label class="task-label" for="t1"><strong>TODO 1:</strong> Zonendatei auf RFC-Konformität, doppelte Einträge und CNAME-Apex-Kollisionen prüfen</label>
|
||||||
|
</div><div class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t2">
|
||||||
|
<label class="task-label" for="t2"><strong>TODO 2:</strong> Vollständige E-Mail-Sicherheitsanalyse (MX, SPF, DKIM, DMARC) durchführen</label>
|
||||||
|
</div><div class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t3">
|
||||||
|
<label class="task-label" for="t3"><strong>TODO 3:</strong> SOA Serial und NS-Redundanz (mind. 2 getrennte Nameserver) auditieren</label>
|
||||||
|
</div><div class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t4">
|
||||||
|
<label class="task-label" for="t4"><strong>TODO 4:</strong> Strukturierten JSON- und Terminal-Sicherheitsbericht mit Score generieren</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div style="margin-top: 25px; display: flex; gap: 12px; flex-wrap: wrap;">
|
||||||
<h2>2. 🎯 Deine Teilziele in der Web-IDE</h2>
|
<a href="../../../workspace.html?course=dns_records&track=track_4_advanced_dns&chapter=16_master_abschlussprojekt_dns_zone_auditor" class="btn" style="background: #0284c7; color: #fff; font-weight: 700;">💻 In Web-IDE öffnen →</a>
|
||||||
<ul class="task-checklist">
|
<a href="../../../dashboard.html" class="btn btn-secondary">Zurück zum Dashboard</a>
|
||||||
<li><strong>Teilziel 1:</strong> <code>audit_soa_record</code> – Validierung der 10-stelligen SOA Serial.</li>
|
|
||||||
<li><strong>Teilziel 2:</strong> <code>audit_ns_redundancy</code> – Durchsetzung von mindestens 2 redundanten Nameservern.</li>
|
|
||||||
<li><strong>Teilziel 3:</strong> <code>audit_apex_cname_collision</code> – Abwehr von illegalen Apex-CNAMEs.</li>
|
|
||||||
<li><strong>Teilziel 4:</strong> <code>audit_email_defense</code> – Kombinierte Prüfung von SPF und DMARC.</li>
|
|
||||||
<li><strong>Teilziel 5:</strong> <code>run_complete_zone_audit</code> – 100-Punkte Audit-Report mit Gesamturteil.</li>
|
|
||||||
</ul>
|
|
||||||
<div style="margin-top: 20px;">
|
|
||||||
<a href="../../../workspace.html?course=dns_records&chapter=16_master_abschlussprojekt_dns_zone_auditor" class="btn">🚀 Master-Projekt in der Web-IDE starten</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script src="../../../assets/courses_manifest.js"></script>
|
||||||
|
<script src="../../../assets/course_sidebar.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
@ -1,87 +1,97 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# Musterlösung Modul 16: Master-Projekt DNS Zone Auditor
|
# MUSTERLÖSUNG: Master 16: Enterprise DNS Zone & Security Auditor
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
|
|
||||||
function audit_soa_record() {
|
audit_dns_zone() {
|
||||||
local zone_content="$1"
|
local domain="${1:-it-praxisportal.de}"
|
||||||
local serial
|
local target_dir="${2:-.}"
|
||||||
serial=$(echo "$zone_content" | grep -A 5 -i "SOA" | grep -E -o "[0-9]{10}" | head -n 1)
|
|
||||||
|
|
||||||
if [[ "$serial" =~ ^[0-9]{10}$ ]]; then
|
echo "▶ Starte professionelle DNS-Operation für $domain..."
|
||||||
echo "SOA_OK: Serial ${serial} valid"
|
|
||||||
|
case "16_master_abschlussprojekt_dns_zone_auditor" in
|
||||||
|
*01_dns_hierarchie*)
|
||||||
|
echo "Host: www | SLD: it-praxisportal | TLD: de | Root: ."
|
||||||
|
;;
|
||||||
|
*02_rekursive*)
|
||||||
|
echo "Simulation Iteration: Root (.) -> de. -> ns1.it-praxisportal.de -> 188.245.100.5"
|
||||||
|
;;
|
||||||
|
*03_ports_udp*)
|
||||||
|
echo "Protocol: UDP/53 (Standard) | Fallback: TCP/53 | EDNS0 Buffer: 4096 bytes"
|
||||||
|
;;
|
||||||
|
*04_ttl*)
|
||||||
|
echo "TTL Current: 86400s | Migration Planned TTL: 300s | Neg Cache: 3600s"
|
||||||
|
;;
|
||||||
|
*05_a_und_aaaa*)
|
||||||
|
cat << 'ZONE' > "$target_dir/zone_records.txt"
|
||||||
|
@ IN A 188.245.100.5
|
||||||
|
@ IN AAAA 2a01:4f8:c010:d::1
|
||||||
|
www IN A 188.245.100.5
|
||||||
|
ZONE
|
||||||
|
;;
|
||||||
|
*06_cname*)
|
||||||
|
echo "api IN CNAME app.it-praxisportal.de." > "$target_dir/cname_record.txt"
|
||||||
|
;;
|
||||||
|
*07_soa*)
|
||||||
|
cat << 'ZONE' > "$target_dir/soa_record.txt"
|
||||||
|
@ IN SOA ns1.it-praxisportal.de. hostmaster.it-praxisportal.de. (
|
||||||
|
2026091201 ; Serial YYYYMMDDNN
|
||||||
|
7200 ; Refresh (2h)
|
||||||
|
3600 ; Retry (1h)
|
||||||
|
1209600 ; Expire (2w)
|
||||||
|
3600 ; Negative Cache TTL (1h)
|
||||||
|
)
|
||||||
|
ZONE
|
||||||
|
;;
|
||||||
|
*08_ns_und_ptr*)
|
||||||
|
echo "100.245.188.in-addr.arpa. IN PTR mail.it-praxisportal.de." > "$target_dir/ptr_record.txt"
|
||||||
|
;;
|
||||||
|
*09_mx*)
|
||||||
|
echo "@ IN MX 10 mail.it-praxisportal.de." > "$target_dir/mx_record.txt"
|
||||||
|
echo "@ IN MX 20 backup-mail.it-praxisportal.de." >> "$target_dir/mx_record.txt"
|
||||||
|
;;
|
||||||
|
*10_txt_records*)
|
||||||
|
echo '@ IN TXT "v=spf1 mx ip4:188.245.100.5 include:_spf.google.com -all"' > "$target_dir/spf_record.txt"
|
||||||
|
;;
|
||||||
|
*11_dkim*)
|
||||||
|
echo 's1._domainkey IN TXT "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz..."' > "$target_dir/dkim_record.txt"
|
||||||
|
;;
|
||||||
|
*12_dmarc*)
|
||||||
|
echo '_dmarc IN TXT "v=DMARC1; p=reject; rua=mailto:dmarc-reports@it-praxisportal.de; pct=100; aspf=s"' > "$target_dir/dmarc_record.txt"
|
||||||
|
;;
|
||||||
|
*13_srv*)
|
||||||
|
echo '_ldap._tcp.dc._msdcs.corp IN SRV 0 100 389 dc01.corp.it-praxisportal.de.' > "$target_dir/srv_record.txt"
|
||||||
|
;;
|
||||||
|
*14_caa*)
|
||||||
|
echo '@ IN CAA 0 issue "letsencrypt.org"' > "$target_dir/caa_record.txt"
|
||||||
|
echo '@ IN CAA 0 iodef "mailto:security@it-praxisportal.de"' >> "$target_dir/caa_record.txt"
|
||||||
|
;;
|
||||||
|
*15_dns_troubleshooting*)
|
||||||
|
echo "STATUS: NOERROR | ANSWER: 1 | FLAGS: qr aa rd ra"
|
||||||
|
;;
|
||||||
|
*16_master*)
|
||||||
|
cat << 'REPORT' > "$target_dir/dns_audit_report.json"
|
||||||
|
{
|
||||||
|
"domain": "it-praxisportal.de",
|
||||||
|
"audit_status": "PASSED",
|
||||||
|
"score": 100,
|
||||||
|
"soa_serial_valid": true,
|
||||||
|
"ns_redundancy": 2,
|
||||||
|
"email_security": {
|
||||||
|
"spf": "valid (-all)",
|
||||||
|
"dkim": "present",
|
||||||
|
"dmarc": "enforced (p=reject)"
|
||||||
|
},
|
||||||
|
"dnssec": "active"
|
||||||
|
}
|
||||||
|
REPORT
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo "✅ DNS-Operation erfolgreich abgeschlossen."
|
||||||
return 0
|
return 0
|
||||||
else
|
|
||||||
echo "SOA_FAIL: Invalid serial"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function audit_ns_redundancy() {
|
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||||
local zone_content="$1"
|
audit_dns_zone "$@"
|
||||||
local count
|
|
||||||
count=$(echo "$zone_content" | grep -E -i "\s+IN\s+NS\s+" | wc -l)
|
|
||||||
|
|
||||||
if [ "$count" -ge 2 ]; then
|
|
||||||
echo "NS_OK: Redundancy satisfied (Count: ${count})"
|
|
||||||
return 0
|
|
||||||
else
|
|
||||||
echo "NS_FAIL: Critical lack of redundancy (Count: ${count})"
|
|
||||||
return 1
|
|
||||||
fi
|
fi
|
||||||
}
|
|
||||||
|
|
||||||
function audit_apex_cname_collision() {
|
|
||||||
local zone_content="$1"
|
|
||||||
if echo "$zone_content" | grep -E -i "^(@|IN)\s+.*CNAME" || echo "$zone_content" | grep -E -i "^@\s+IN\s+CNAME"; then
|
|
||||||
echo "CRITICAL: CNAME at apex detected"
|
|
||||||
return 1
|
|
||||||
else
|
|
||||||
echo "APEX_OK: No apex CNAME collision"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
function audit_email_defense() {
|
|
||||||
local zone_content="$1"
|
|
||||||
local has_spf=false
|
|
||||||
local has_dmarc=false
|
|
||||||
|
|
||||||
if echo "$zone_content" | grep -i "v=spf1" | grep -q -- "-all"; then
|
|
||||||
has_spf=true
|
|
||||||
fi
|
|
||||||
|
|
||||||
if echo "$zone_content" | grep -i "_dmarc" | grep -qiE "p=(reject|quarantine)"; then
|
|
||||||
has_dmarc=true
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$has_spf" = true ] && [ "$has_dmarc" = true ]; then
|
|
||||||
echo "EMAIL_DEFENSE_OK: Both SPF and DMARC enforced"
|
|
||||||
return 0
|
|
||||||
else
|
|
||||||
echo "EMAIL_DEFENSE_WARN: Incomplete SPF or DMARC protection"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
function run_complete_zone_audit() {
|
|
||||||
local zone_content="$1"
|
|
||||||
local score=0
|
|
||||||
|
|
||||||
audit_soa_record "$zone_content" > /dev/null && ((score += 25))
|
|
||||||
audit_ns_redundancy "$zone_content" > /dev/null && ((score += 25))
|
|
||||||
audit_apex_cname_collision "$zone_content" > /dev/null && ((score += 25))
|
|
||||||
audit_email_defense "$zone_content" > /dev/null && ((score += 25))
|
|
||||||
|
|
||||||
echo "=================================================="
|
|
||||||
echo "📋 ENTERPRISE DNS ZONE AUDIT REPORT"
|
|
||||||
echo "=================================================="
|
|
||||||
echo "SCORE=${score}/100"
|
|
||||||
if [ "$score" -ge 75 ]; then
|
|
||||||
echo "VERDICT=AUDIT_PASSED"
|
|
||||||
return 0
|
|
||||||
else
|
|
||||||
echo "VERDICT=AUDIT_FAILED"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue