Created
February 20, 2025 20:05
-
-
Save elico/de5096d5d46632212f9385f7b1641e5b to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>MikroTik Port Forward Generator</title> | |
<style> | |
body { | |
font-family: Arial, sans-serif; | |
margin: 20px; | |
line-height: 1.6; | |
} | |
label, select, input, button { | |
display: block; | |
margin: 10px 0; | |
padding: 8px; | |
box-sizing: border-box; | |
border-radius: 4px; | |
border: 1px solid #ccc; | |
} | |
input[type="number"], | |
input[type="text"] { | |
width: 200px; | |
} | |
button { | |
background-color: #4CAF50; | |
color: white; | |
cursor: pointer; | |
transition: background-color 0.3s; | |
border: none; | |
} | |
button:hover { | |
background-color: #367c39; | |
} | |
pre { | |
background: #f4f4f4; | |
padding: 10px; | |
border-radius: 5px; | |
white-space: pre-wrap; | |
overflow-x: auto; | |
} | |
.rule-list { | |
margin-top: 20px; | |
list-style-type: none; | |
padding: 0; | |
} | |
.rule-item { | |
display: flex; | |
align-items: center; | |
margin-bottom: 5px; | |
padding: 10px; | |
border: 1px solid #ccc; | |
cursor: move; | |
background-color: #fff; | |
border-radius: 4px; | |
/* touch-action: none; Prevent scrolling on the element */ | |
} | |
.rule-item button { | |
margin-left: 10px; | |
background-color: #f44336; | |
color: white; | |
border: none; | |
padding: 5px 10px; | |
border-radius: 4px; | |
cursor: pointer; | |
transition: background-color 0.3s; | |
} | |
.rule-item button:hover { | |
background-color: #d32f2f; | |
} | |
#output { | |
font-size: 14px; | |
} | |
.container { | |
max-width: 800px; | |
margin: 0 auto; | |
padding: 20px; | |
background-color: #f9f9f9; | |
border-radius: 8px; | |
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); | |
} | |
/* Style for drag and drop feedback */ | |
.rule-item.dragging { | |
opacity: 0.5; | |
} | |
.rule-item.drag-over { | |
background-color: #e0e0e0; | |
} | |
</style> | |
</head> | |
<body> | |
<div class="container"> | |
<h2>MikroTik Port Forward Generator</h2> | |
<label for="protocol">Protocol:</label> | |
<select id="protocol"> | |
<option value="tcp">TCP</option> | |
<option value="udp">UDP</option> | |
<option value="both">TCP & UDP</option> | |
</select> | |
<label for="dstPort">External Port (dst-port):</label> | |
<input type="number" id="dstPort" min="1" max="65535" required> | |
<label for="toAddress">Internal IP (to-addresses):</label> | |
<input type="text" id="toAddress" placeholder="10.10.10.10" required> | |
<label for="toPort">Internal Port (to-ports):</label> | |
<input type="number" id="toPort" min="1" max="65535" required> | |
<label for="srcAddressList">Source Address List (Optional):</label> | |
<input type="text" id="srcAddressList" placeholder="Enter source address list"> | |
<button onclick="addRule()">Add Rule</button> | |
<h3>Rules List:</h3> | |
<ul id="rulesList" class="rule-list"></ul> | |
<label for="commandType">Command Type:</label> | |
<select id="commandType"> | |
<option value="partial" selected>Partial</option> | |
<option value="full">Full</option> | |
</select> | |
<button onclick="generateCommands()">Generate Commands</button> | |
<h3>Generated Commands:</h3> | |
<pre id="output"></pre> | |
<button onclick="copyToClipboard()">Copy to Clipboard</button> | |
<h3>Import Rules from JSON:</h3> | |
<input type="file" id="importFile" accept="application/json"> | |
<button onclick="importRules()">Import JSON</button> | |
<h3>Export Rules to JSON:</h3> | |
<button onclick="exportRules()">Export JSON</button> | |
</div> | |
<script> | |
let rules = []; | |
let draggedItem = null; | |
function addRule() { | |
const protocol = document.getElementById('protocol').value; | |
const dstPort = document.getElementById('dstPort').value; | |
const toAddress = document.getElementById('toAddress').value; | |
const toPort = document.getElementById('toPort').value; | |
const srcAddressList = document.getElementById('srcAddressList').value.trim(); | |
// Validation: Check for valid IP address format | |
if (!isValidIPAddress(toAddress)) { | |
alert("Please enter a valid Internal IP Address."); | |
return; | |
} | |
if (protocol === 'both') { | |
rules.push({ protocol: 'tcp', dstPort, toAddress, toPort, srcAddressList }); | |
rules.push({ protocol: 'udp', dstPort, toAddress, toPort, srcAddressList }); | |
} else { | |
rules.push({ protocol, dstPort, toAddress, toPort, srcAddressList }); | |
} | |
updateRulesList(); | |
clearInputFields(); | |
} | |
function isValidIPAddress(ip) { | |
const ipRegex = /^(\d{1,3}\.){3}\d{1,3}$/; | |
if (!ipRegex.test(ip)) return false; | |
const parts = ip.split('.').map(Number); | |
return parts.every(part => part >= 0 && part <= 255); | |
} | |
function clearInputFields() { | |
document.getElementById('dstPort').value = ''; | |
document.getElementById('toAddress').value = ''; | |
document.getElementById('toPort').value = ''; | |
document.getElementById('srcAddressList').value = ''; | |
} | |
function updateRulesList() { | |
const listElement = document.getElementById('rulesList'); | |
listElement.innerHTML = ''; | |
rules.forEach((rule, index) => { | |
const listItem = document.createElement('li'); | |
listItem.className = 'rule-item'; | |
listItem.draggable = true; | |
listItem.dataset.index = index; | |
listItem.textContent = `Protocol: ${rule.protocol}, External Port: ${rule.dstPort}, Internal IP: ${rule.toAddress}, Internal Port: ${rule.toPort}, Source Address List: ${rule.srcAddressList || 'None'}`; | |
const removeButton = document.createElement('button'); | |
removeButton.textContent = 'Remove'; | |
removeButton.onclick = () => removeRule(index); | |
listItem.appendChild(removeButton); | |
listItem.addEventListener('dragstart', handleDragStart); | |
listItem.addEventListener('dragover', handleDragOver); | |
listItem.addEventListener('drop', handleDrop); | |
listItem.addEventListener('touchstart', handleTouchStart); // Add touch event listeners | |
listItem.addEventListener('touchmove', handleTouchMove); | |
listItem.addEventListener('touchend', handleTouchEnd); | |
listItem.addEventListener('touchcancel', handleTouchCancel); | |
listElement.appendChild(listItem); | |
}); | |
} | |
function removeRule(index) { | |
rules.splice(index, 1); | |
updateRulesList(); | |
} | |
function generateCommands() { | |
const commandType = document.getElementById('commandType').value; | |
if (commandType === 'partial') { | |
generatePartialCommands(); | |
} else if (commandType === 'full') { | |
generateFullCommands(); | |
} else { | |
document.getElementById('output').textContent = 'Invalid command type selected.'; | |
} | |
} | |
function generatePartialCommands() { | |
if (rules.length === 0) { | |
document.getElementById('output').textContent = 'No rules added yet.'; | |
return; | |
} | |
const commands = "/ip firewall nat\n" + rules.map(rule => { | |
return `add action=dst-nat chain=PORT_FORWARDING dst-port=${rule.dstPort} in-interface-list=WAN protocol=${rule.protocol}` + | |
(rule.srcAddressList ? ` src-address-list=${rule.srcAddressList}` : '') + | |
` to-addresses=${rule.toAddress} to-ports=${rule.toPort} comment="Port Forwarding Rule"`; | |
}).join('\n'); | |
document.getElementById('output').textContent = commands; | |
} | |
function generateFullCommands() { | |
let fullCommands = 'do {\n' + | |
' :log info "Stating Port Forwarding Script"\n' + | |
' :log info "Port Forwarding Script step 1"\n' + | |
' /ip firewall nat\n' + | |
' remove [find where chain=PORT_FORWARDING_TMP];\n'; | |
rules.forEach(rule => { | |
fullCommands += `\n add action=dst-nat chain=PORT_FORWARDING_TMP dst-port=${rule.dstPort} in-interface-list=WAN protocol=${rule.protocol} to-addresses=${rule.toAddress} to-ports=${rule.toPort}`; | |
if (rule.srcAddressList) { | |
fullCommands += ` src-address-list=${rule.srcAddressList}`; | |
} | |
fullCommands += '\n'; | |
}); | |
fullCommands += '\n' + | |
' :log info "Port Forwarding Script step 2"\n' + | |
' /ip firewall nat\n' + | |
' print;\n' + | |
' remove [find where chain=PORT_FORWARDING];\n' + | |
' :log info "Port Forwarding Script step 3"\n' + | |
' /ip firewall nat\n' + | |
' /ip firewall nat print\n'; | |
rules.forEach(rule => { | |
fullCommands += ` set [find where chain=PORT_FORWARDING_TMP and dst-port=${rule.dstPort} and protocol=${rule.protocol} and to-addresses=${rule.toAddress} and to-ports=${rule.toPort}] chain=PORT_FORWARDING;\n`; | |
}); | |
fullCommands += `\n`+ | |
' :local natTableSize [:len [/ip/firewall/nat find]];\n' + | |
' :local mngmntJumpExists [:len [ /ip/firewall/nat/find where chain=dstnat and in-interface-list="WAN" and action=jump and jump-target=MNGMNT_PF ]];\n' + | |
' :local portForwadingJumpExists [:len [ /ip/firewall/nat/find where chain=dstnat and in-interface-list="WAN" and action=jump and jump-target=PORT_FORWARDING ]];\n' + | |
' :if ($mngmntJumpExists > 0) do={\n' + | |
' /ip/firewall/nat/remove [find where chain=dstnat and in-interface-list="WAN" and action=jump and jump-target=MNGMNT_PF ];\n' + | |
' /ip/firewall/nat/print;\n' + | |
' }\n' + | |
' :if ($portForwadingJumpExists > 0) do={\n' + | |
' /ip/firewall/nat/remove [find where chain=dstnat and in-interface-list="WAN" and action=jump and jump-target=PORT_FORWARDING ];\n' + | |
' /ip/firewall/nat/print;\n' + | |
' }\n' + | |
' :log info "Port Forwarding Script step 4"\n' + | |
' /ip/firewall/nat\n' + | |
' /ip/firewall/nat/print without-paging\n' + | |
' add action=jump chain=dstnat comment=PORT_FORWARDING in-interface-list=WAN jump-target=PORT_FORWARDING place-before=0\n' + | |
' /ip/firewall/nat/print without-paging\n' + | |
' add action=jump chain=dstnat comment=MNGMNT_PF in-interface-list=WAN jump-target=MNGMNT_PF src-address-list=NGTECH place-before=0\n' + | |
' /ip/firewall/nat/print without-paging\n' + | |
' add action=jump chain=dstnat comment=MNGMNT_PF in-interface-list=WAN jump-target=MNGMNT_PF src-address-list=IPCOM place-before=0\n' + | |
' :log info "Port Forwarding Script step Finished"\n' + | |
'} on-error={\n' + | |
' :log info "NAT MNGMNT rules Initialization script error";\n' + | |
'}'; | |
document.getElementById('output').textContent = fullCommands; | |
} | |
function copyToClipboard() { | |
const outputElement = document.getElementById('output'); | |
const commands = outputElement.textContent; | |
if (!commands) { | |
alert("No commands to copy!"); | |
return; | |
} | |
navigator.clipboard.writeText(commands) | |
.then(() => { | |
alert("Commands copied to clipboard!"); | |
}) | |
.catch(err => { | |
console.error("Failed to copy: ", err); | |
alert("Failed to copy commands to clipboard."); // User-friendly fallback | |
}); | |
} | |
function importRules() { | |
const fileInput = document.getElementById('importFile'); | |
if (fileInput.files.length === 0) { | |
alert('Please select a JSON file.'); | |
return; | |
} | |
const file = fileInput.files[0]; | |
const reader = new FileReader(); | |
reader.onload = function (event) { | |
try { | |
const importedRules = JSON.parse(event.target.result); | |
if (!Array.isArray(importedRules)) throw new Error('Invalid JSON format'); | |
rules = importedRules; | |
updateRulesList(); | |
} catch (error) { | |
alert('Error parsing JSON file. Please ensure it is in the correct format.'); | |
} | |
}; | |
reader.readAsText(file); | |
} | |
function exportRules() { | |
const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(rules, null, 2)); | |
const downloadAnchor = document.createElement('a'); | |
downloadAnchor.setAttribute("href", dataStr); | |
downloadAnchor.setAttribute("download", "mikrotik_rules.json"); | |
document.body.appendChild(downloadAnchor); | |
document.body.removeChild(downloadAnchor); | |
} | |
function handleDragStart(e) { | |
draggedItem = e.target; | |
e.dataTransfer.setData('text/plain', e.target.dataset.index); | |
e.target.classList.add('dragging'); | |
} | |
function handleDragOver(e) { | |
e.preventDefault(); | |
if (e.target !== draggedItem && e.target.classList.contains('rule-item')) { | |
e.target.classList.add('drag-over'); | |
} | |
} | |
function handleDrop(e) { | |
e.preventDefault(); | |
if (e.target !== draggedItem && e.target.classList.contains('rule-item')) { | |
const droppedIndex = parseInt(e.dataTransfer.getData('text/plain'), 10); | |
const targetIndex = parseInt(e.target.dataset.index, 10); | |
const movedRule = rules.splice(droppedIndex, 1)[0]; | |
rules.splice(targetIndex, 0, movedRule); | |
updateRulesList(); | |
} | |
e.target.classList.remove('drag-over'); | |
draggedItem.classList.remove('dragging'); | |
draggedItem = null; | |
} | |
// Touch event handlers - these are stubs, you'll need to implement actual touch-based drag logic | |
function handleTouchStart(e) { | |
// TODO: Implement touch start logic. Store initial touch position. | |
console.log("Touch start"); | |
} | |
function handleTouchMove(e) { | |
// TODO: Implement touch move logic. Calculate movement and potentially scroll the element. | |
e.preventDefault(); // Prevent scrolling while dragging. | |
console.log("Touch move"); | |
} | |
function handleTouchEnd(e) { | |
// TODO: Implement touch end logic. Determine if a drop occurred. | |
console.log("Touch end"); | |
} | |
function handleTouchCancel(e) { | |
// TODO: Handle touch cancel event. | |
console.log("Touch cancel"); | |
} | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment