Skip to content

Instantly share code, notes, and snippets.

@elico
Created February 20, 2025 20:05
Show Gist options
  • Save elico/de5096d5d46632212f9385f7b1641e5b to your computer and use it in GitHub Desktop.
Save elico/de5096d5d46632212f9385f7b1641e5b to your computer and use it in GitHub Desktop.
<!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