(20250211) Completed documentation for MikroTik's PPPoE system as per my current understanding.

This commit is contained in:
2025-02-11 17:37:56 +05:30
parent 382c6c991d
commit cf6520d119
3 changed files with 556 additions and 54 deletions
+162 -19
View File
@@ -21,8 +21,7 @@
N/A N/A
""" """
import datetime
# ***************************************************************************************************************** # *****************************************************************************************************************
# ***** **** # ***** ****
# *** IMPORT *** # *** IMPORT ***
@@ -36,6 +35,8 @@ sys.path.append(".")
sys.path.append("..") sys.path.append("..")
# My async utils: # My async utils:
from utils_v2.string import json
from utils_v2.date_time import date_time
from utils_v2.database.async_mysql_v2 import AsyncMySQL from utils_v2.database.async_mysql_v2 import AsyncMySQL
from utils_v2.database.async_mongo_v2 import AsyncMongo from utils_v2.database.async_mongo_v2 import AsyncMongo
from utils_v2.cache.async_redis_cache_v2 import AsyncRedisCache from utils_v2.cache.async_redis_cache_v2 import AsyncRedisCache
@@ -185,7 +186,7 @@ class MikroTikController(CoreSoftwareController, ABC):
@staticmethod @staticmethod
def get_mikrotik_url( def get_mikrotik_url(
nas_ip: str, mikrotik_ip: str,
path: str, path: str,
port_no: int | str = None, port_no: int | str = None,
use_https: bool = True use_https: bool = True
@@ -193,7 +194,7 @@ class MikroTikController(CoreSoftwareController, ABC):
""" """
Simply creates the base URL for hitting the MikroTik server. Simply creates the base URL for hitting the MikroTik server.
:param nas_ip: The IP address of the MikroTik device. :param mikrotik_ip: The IP address of the MikroTik device.
:param path: The path of the REST API to hit. :param path: The path of the REST API to hit.
:param port_no: The port no. to hit the MikroTik device on. :param port_no: The port no. to hit the MikroTik device on.
:param use_https: Whether to use HTTPS, or HTTP. :param use_https: Whether to use HTTPS, or HTTP.
@@ -201,46 +202,46 @@ class MikroTikController(CoreSoftwareController, ABC):
""" """
base_url = r"https://" if use_https else r"http://" base_url = r"https://" if use_https else r"http://"
base_url += nas_ip base_url += mikrotik_ip
if port_no is not None: base_url += f":{port_no}" if port_no is not None: base_url += f":{port_no}"
base_url += "/rest" base_url += "/rest"
if not path.startswith("/"): path = "/" + path if not path.startswith("/"): path = "/" + path
return base_url + path return base_url + path
@staticmethod @staticmethod
def split_ipv4_range_among_vlans( def split_ipv4_range_among_targets(
start_ip: ipaddress.IPv4Address | str, start_ip: ipaddress.IPv4Address | str,
end_ip: ipaddress.IPv4Address | str, end_ip: ipaddress.IPv4Address | str,
vlan_ids: List[int] targets: List
) -> List[dict]: ) -> List[dict]:
""" """
Divides the IPv4 range over the list of VLAN ids. Divides the IPv4 range over the list of VLAN ids.
:param start_ip: The first IP in the full pool (range). :param start_ip: The first IP in the full pool (range).
:param end_ip: The last IP in the full pool (range). :param end_ip: The last IP in the full pool (range).
:param vlan_ids: The list of VLAN ids (need not be sequential or ordered). :param targets: The list of targets (need not be sequential or ordered) like VLAN ids.
:return: A list of dicts that describes each VLAN. :return: A list of dicts that describes the subnet for each target.
""" """
# Parse the inputs: # Parse the inputs:
start_ip_obj = start_ip if isinstance(start_ip, ipaddress.IPv4Address) else ipaddress.IPv4Address(start_ip) start_ip_obj = start_ip if isinstance(start_ip, ipaddress.IPv4Address) else ipaddress.IPv4Address(start_ip)
end_ip_obj = end_ip if isinstance(end_ip, ipaddress.IPv4Address) else ipaddress.IPv4Address(end_ip) end_ip_obj = end_ip if isinstance(end_ip, ipaddress.IPv4Address) else ipaddress.IPv4Address(end_ip)
vlan_count = len(vlan_ids) target_count = len(targets)
# Calculate the total number of IPs in the range: # Calculate the total number of IPs in the range:
total_ips = int(end_ip_obj) - int(start_ip_obj) + 1 total_ips = int(end_ip_obj) - int(start_ip_obj) + 1
# Calculate the no. of IPs each VLAN gets, # Calculate the no. of IPs each VLAN gets,
# and add three because we need IPs for network, gateway and broadcast: # and add three because we need IPs for network, gateway and broadcast:
ips_per_vlan = math.ceil(total_ips / vlan_count) + 3 ips_per_target = math.ceil(total_ips / target_count) + 3
# Generate subnets for each VLAN: # Generate subnets for each VLAN:
subnets = [] subnets = []
current_ip = start_ip_obj current_ip = start_ip_obj
for vlan_id in vlan_ids: for target in targets:
# Calculate the network address for the current VLAN: # Calculate the network address for the current VLAN:
subnet_network = ipaddress.IPv4Network(f"{current_ip}/{32 - (ips_per_vlan).bit_length()}", strict = False) subnet_network = ipaddress.IPv4Network(f"{current_ip}/{32 - ips_per_target.bit_length()}", strict = False)
# Check for overlap with the parent network: # Check for overlap with the parent network:
subnet_start_ip_obj = subnet_network.network_address subnet_start_ip_obj = subnet_network.network_address
@@ -249,7 +250,7 @@ class MikroTikController(CoreSoftwareController, ABC):
# Add the calculated subnet to the list: # Add the calculated subnet to the list:
subnets.append({ subnets.append({
"vlanId": vlan_id, "target": target,
"network": str(subnet_network), "network": str(subnet_network),
"size": int(2 ** (32 - subnet_network.prefixlen)), "size": int(2 ** (32 - subnet_network.prefixlen)),
"startIp": str(subnet_start_ip_obj), "startIp": str(subnet_start_ip_obj),
@@ -262,6 +263,51 @@ class MikroTikController(CoreSoftwareController, ABC):
# Done here: # Done here:
return subnets return subnets
@staticmethod
def create_comment_json(
created_by: str,
created_ts: datetime.datetime = None,
roll_back_config: dict = None
) -> str:
"""
To create a JSON string that can be put as a comment in any step to later identify the work that was done and
have some hint about how to roll it back if needed.
:param created_by: A hint to put to identify which process/tool created this comment/config/process.
:param created_ts: The datetime (preferably UTC) when this comment was created.
:param roll_back_config: The original config that can be used when rolling back.
:return: The comment string to be used.
"""
# Create the JSON:
comment_json = {
"createdBy": created_by,
"createdTs": (created_ts or date_time.get_current_utc_date_time(as_string = False)).timestamp(),
"rollbackConfig": roll_back_config
}
# Return a string:
return json.to_string(comment_json, no_space = True)
@staticmethod
def parse_comment_json(comment: str) -> dict | list | None:
"""
Tries to parse a comment as if it were a JSON string.
:param comment: The comment string.
:return: The dict/list parsed form the JSON, or a null value of the input was not a valid JSON string.
"""
# Start with a null value:
parsed = None
# Try to parse the comment as if it were a JSON string:
try: parsed = json.from_string(comment)
except: parsed = None
# Done here:
return parsed
# ┏┓ # ┏┓
# ┗┓┓┏┏╋┏┓┏┳┓ # ┗┓┓┏┏╋┏┓┏┳┓
# ┗┛┗┫┛┗┗ ┛┗┗ # ┗┛┗┫┛┗┗ ┛┗┗
@@ -269,7 +315,7 @@ class MikroTikController(CoreSoftwareController, ABC):
async def get_system_resource( async def get_system_resource(
self, self,
nas_ip: str, mikrotik_ip: str,
username: str, username: str,
password: str, password: str,
port_no: int | str = None, port_no: int | str = None,
@@ -279,7 +325,7 @@ class MikroTikController(CoreSoftwareController, ABC):
""" """
To get a summary of the hardware resources available in the MikroTik device. This also becomes a great way to To get a summary of the hardware resources available in the MikroTik device. This also becomes a great way to
quickly check if any given device is valid, and up and running. quickly check if any given device is valid, and up and running.
:param nas_ip: The IP address of the MikroTik device. :param mikrotik_ip: The IP address of the MikroTik device.
:param username: The username to get access to the MikroTik device. :param username: The username to get access to the MikroTik device.
:param password: The password to get access to the MikroTik device. :param password: The password to get access to the MikroTik device.
:param port_no: The port no. to hit the MikroTik device on. :param port_no: The port no. to hit the MikroTik device on.
@@ -287,10 +333,10 @@ class MikroTikController(CoreSoftwareController, ABC):
:return: A structured API response. :return: A structured API response.
""" """
# Make the API call and return the response: # Make the API call:
return await self._rest.get( api_response = await self._rest.get(
url = self.get_mikrotik_url( url = self.get_mikrotik_url(
nas_ip = nas_ip, mikrotik_ip = mikrotik_ip,
path = r"/system/resource", path = r"/system/resource",
port_no = port_no, port_no = port_no,
use_https = use_https use_https = use_https
@@ -301,6 +347,103 @@ class MikroTikController(CoreSoftwareController, ABC):
) )
) )
# Done here:
return api_response
# ┳ ┏
# ┃┏┓╋┏┓┏┓╋┏┓┏┏┓┏
# ┻┛┗┗┗ ┛ ┛┗┻┗┗ ┛
async def list_interfaces(
self,
mikrotik_ip: str,
username: str,
password: str,
port_no: int | str = None,
use_https: bool = True
):
"""
To enlist all the physical connectivity interfaces available on the MikroTik device.
:param mikrotik_ip: The IP address of the MikroTik device.
:param username: The username to get access to the MikroTik device.
:param password: The password to get access to the MikroTik device.
:param port_no: The port no. to hit the MikroTik device on.
:param use_https: Whether to use HTTPS, or HTTP.
:return: A structured API response.
"""
# Make the API call:
api_response = await self._rest.get(
url = self.get_mikrotik_url(
mikrotik_ip = mikrotik_ip,
path = r"/interface",
port_no = port_no,
use_https = use_https
),
auth = httpx.BasicAuth(
username = username,
password = password
)
)
# Done here:
return api_response
async def update_interface(
self,
mikrotik_ip: str,
username: str,
password: str,
interface_id: str,
port_no: int | str = None,
use_https: bool = True,
name: str = None,
disabled: bool = False,
comment: str = None
):
"""
To update an interface's configuration.
:param mikrotik_ip: The IP address of the MikroTik device.
:param username: The username to get access to the MikroTik device.
:param password: The password to get access to the MikroTik device.
:param interface_id: The id of the interface that you'd like to modify.
:param port_no: The port no. to hit the MikroTik device on.
:param use_https: Whether to use HTTPS, or HTTP.
:param name: The new name for the interface.
:param disabled: Whether, or not, you'd like to disable the interface.
:param comment: A note that you'd like to attach to the entry. It'll then be available in the listing API.
:return: A structured API response.
"""
# Prepare the JSON to give to the API:
input_json = {
k: v for k, v in {
"name": name,
"disabled": disabled,
"comment": comment
}.items() if v is not None
}
# Make the API call:
api_response = await self._rest.patch(
url = self.get_mikrotik_url(
mikrotik_ip = mikrotik_ip,
path = f"/interface/{interface_id}",
port_no = port_no,
use_https = use_https
),
auth = httpx.BasicAuth(
username = username,
password = password
),
json = input_json
)
# Done here:
return api_response
# ┏┓ ┓ # ┏┓ ┓
# ┣┫┓┏╋┣┓ # ┣┫┓┏╋┣┓
# ┛┗┗┻┗┛┗ # ┛┗┗┻┗┛┗
@@ -21,8 +21,7 @@
N/A N/A
""" """
import asyncio
# ***************************************************************************************************************** # *****************************************************************************************************************
# ***** **** # ***** ****
# *** IMPORT *** # *** IMPORT ***
@@ -37,6 +36,7 @@ sys.path.append("..")
# My async utils: # My async utils:
from utils_v2.string import json from utils_v2.string import json
from utils_v2.date_time import date_time
from utils_v2.database.async_mysql_v2 import AsyncMySQL from utils_v2.database.async_mysql_v2 import AsyncMySQL
from utils_v2.database.async_mongo_v2 import AsyncMongo from utils_v2.database.async_mongo_v2 import AsyncMongo
from utils_v2.cache.async_redis_cache_v2 import AsyncRedisCache from utils_v2.cache.async_redis_cache_v2 import AsyncRedisCache
@@ -117,6 +117,11 @@ class MikroTikPPPoE1000Controller(MikroTikController):
CLIENT_NAME = "mikrotikPPPoE1000" CLIENT_NAME = "mikrotikPPPoE1000"
# For automated configuration, and identification of automated configuration:
CREATED_BY_NAME = "easyfi"
HW_INTERFACE_NAME = "easyfi-pppoe"
VLAN_NAME = "easyfi-vlan-{}" # ... Substitute the VLAN's id here.
# ┏┓ # ┏┓
# ┃ ┏┓┏┓┏╋┏┓┓┏┏╋┏┓┏┓ # ┃ ┏┓┏┓┏╋┏┓┓┏┏╋┏┓┏┓
# ┗┛┗┛┛┗┛┗┛ ┗┻┗┗┗┛┛ # ┗┛┗┛┛┗┛┗┛ ┗┻┗┗┗┛┛
@@ -157,6 +162,158 @@ class MikroTikPPPoE1000Controller(MikroTikController):
# Init a variable in a parent: # Init a variable in a parent:
self._client = self.CLIENT_NAME self._client = self.CLIENT_NAME
# ┳ ┏
# ┃┏┓╋┏┓┏┓╋┏┓┏┏┓┏
# ┻┛┗┗┗ ┛ ┛┗┻┗┗ ┛
async def interface_setup(
self,
mikrotik_auth: MikroTikPPPoE1000Auth,
use_https: bool = True
) -> MikroTikConfigAttemptResponse:
"""
To find the first available interface.
:param mikrotik_auth: The set of credentials as received from the UI/API.
:param use_https: Whether to use HTTPS, or HTTP.
:return: A structured response to indicate what happened during the process.
"""
# Start by assuming failure:
step_response = MikroTikConfigAttemptResponse()
# Enlist all the interfaces:
api_response = await self.list_interfaces(
mikrotik_ip = mikrotik_auth.nasIp,
port_no = mikrotik_auth.nasPort,
username = mikrotik_auth.username,
password = mikrotik_auth.password,
use_https = use_https
)
# If the listing failed:
if not api_response.success:
step_response.success = False
step_response.message = "Failed to enlist h/w interfaces during configuration."
step_response.exception = step_response.exception
return step_response
# We find the first available interface:
unused_if_json = None
for if_json in await api_response.get_json():
if str(if_json.get("running")).lower() == "false":
unused_if_json = if_json
break
# If we found no available interface:
if unused_if_json is None:
step_response.success = False
step_response.message = "Failed to find an idle h/w interface."
step_response.exception = None
return step_response
# Reconfigure the idle interface:
api_response = await self.update_interface(
mikrotik_ip = mikrotik_auth.nasIp,
username = mikrotik_auth.username,
password = mikrotik_auth.password,
interface_id = unused_if_json.get(".id"),
port_no = mikrotik_auth.nasPort,
use_https = use_https,
name = self.HW_INTERFACE_NAME,
disabled = False,
comment = self.create_comment_json(
created_by = self.CREATED_BY_NAME,
created_ts = date_time.get_current_utc_date_time(as_string = False),
roll_back_config = unused_if_json
)
)
# Check if the attempt was successful:
if api_response.success:
step_response.success = True
step_response.message = f"H/w interface ('{unused_if_json.get('.id')}') configured."
step_response.exception = None
else:
step_response.success = False
step_response.message = f"H/w interface ('{unused_if_json.get('.id')}') found, but could NOT be configured."
step_response.exception = api_response.exception
# Done here:
return step_response
async def interface_roll_back(
self,
mikrotik_auth: MikroTikPPPoE1000Auth,
use_https: bool = True
) -> MikroTikRollBackAttemptResponse:
"""
To find the first available interface.
:param mikrotik_auth: The set of credentials as received from the UI/API.
:param use_https: Whether to use HTTPS, or HTTP.
:return: A structured response to indicate what happened during the process.
"""
# Start by assuming failure:
total_count = 0
rolled_back_count = 0
step_response = MikroTikConfigAttemptResponse()
# Enlist all the interfaces:
api_response = await self.list_interfaces(
mikrotik_ip = mikrotik_auth.nasIp,
port_no = mikrotik_auth.nasPort,
username = mikrotik_auth.username,
password = mikrotik_auth.password,
use_https = use_https
)
# If the listing failed:
if not api_response.success:
step_response.success = False
step_response.message = "Failed to enlist h/w interfaces during rollback."
step_response.exception = step_response.exception
return step_response
# Loop through all the interfaces,
# identify which ones were configured by this class,
# and perform rollback on those:
for if_json in await api_response.get_json():
# Check if the configuration was made by this class:
created_by = None
original_config = None
comment_json = self.parse_comment_json(if_json.get("comment"))
if comment_json is not None:
created_by = comment_json.get("createdBy")
original_config = comment_json.get("rollbackConfig")
# If this was indeed set up by this class,
# and if rollback data is available:
if created_by == self.CREATED_BY_NAME and original_config is not None:
api_response = await self.update_interface(
mikrotik_ip = mikrotik_auth.nasIp,
username = mikrotik_auth.username,
password = mikrotik_auth.password,
interface_id = if_json.get(".id"),
port_no = mikrotik_auth.nasPort,
use_https = use_https,
name = original_config.get("name", self.HW_INTERFACE_NAME),
disabled = False,
comment = original_config.get("comment", "")
)
total_count += 1
if api_response.success: rolled_back_count += 1
print("RB API RESP:", api_response)
# Construct the final response:
step_response.message = f"Rolled back {rolled_back_count}/{total_count} h/w interface configuration(s)."
if rolled_back_count == total_count: step_response.success = True
# Done here:
return step_response
# ┏┓ ┓ # ┏┓ ┓
# ┣┫┓┏╋┣┓ # ┣┫┓┏╋┣┓
# ┛┗┗┻┗┛┗ # ┛┗┗┻┗┛┗
@@ -179,10 +336,10 @@ class MikroTikPPPoE1000Controller(MikroTikController):
# print("IN-AUTH:", json.to_string(mikrotik_auth.model_dump(), default = str)) # print("IN-AUTH:", json.to_string(mikrotik_auth.model_dump(), default = str))
print("VLAN SUBNETS:", json.to_string( print("VLAN SUBNETS:", json.to_string(
self.split_ipv4_range_among_vlans( self.split_ipv4_range_among_targets(
start_ip = mikrotik_auth.firstPrivateIp, start_ip = mikrotik_auth.firstPrivateIp,
end_ip = mikrotik_auth.lastPrivateIp, end_ip = mikrotik_auth.lastPrivateIp,
vlan_ids = mikrotik_auth.vlanRange targets = mikrotik_auth.vlanRange
), ),
default = str default = str
)) ))
@@ -190,24 +347,34 @@ class MikroTikPPPoE1000Controller(MikroTikController):
# Start with a blank response: # Start with a blank response:
auth_response = MikroTikAuthResponse() auth_response = MikroTikAuthResponse()
# Try connecting to the server to check if the credentials are valid, or not: # # Try connecting to the server to check if the credentials are valid, or not:
api_response = await self.get_system_resource( # api_response = await self.get_system_resource(
nas_ip = mikrotik_auth.nasIp, # mikrotik_ip = mikrotik_auth.nasIp,
port_no = mikrotik_auth.nasPort, # port_no = mikrotik_auth.nasPort,
username = mikrotik_auth.username, # username = mikrotik_auth.username,
password = mikrotik_auth.password, # password = mikrotik_auth.password,
use_https = False # use_https = False
) # )
#
# # If the connection attempt failed:
# if not api_response.success:
# auth_response.exception = api_response.exception
# if api_response.httpCode is None:
# auth_response.message = "Exception: " + api_response.exception.__class__.__name__
# if exception_str := str(auth_response.exception): auth_response.message += f" ({exception_str})"
# elif api_response.httpCode in [401]: auth_response.message = "Invalid credentials passed."
# elif api_response.httpCode in [502]: auth_response.message = "Could not ."
# else: auth_response.message = "Unknown error."
# If the connection attempt failed: # # Try to configure the system:
if not api_response.success: # config_response = await self.configure(mikrotik_auth)
auth_response.exception = api_response.exception # print("CONFIG RESPONSE:", config_response)
if api_response.httpCode is None: #
auth_response.message = "Exception: " + api_response.exception.__class__.__name__ # await asyncio.sleep(5.0)
if exception_str := str(auth_response.exception): auth_response.message += f" ({exception_str})"
elif api_response.httpCode in [401]: auth_response.message = "Invalid credentials passed." # Try to roll all configuration back:
elif api_response.httpCode in [502]: auth_response.message = "Could not ." roll_back_response = await self.roll_back(mikrotik_auth)
else: auth_response.message = "Unknown error." print("ROLL-BACK RESPONSE:", roll_back_response)
# Done here: # Done here:
return auth_response return auth_response
@@ -229,7 +396,21 @@ class MikroTikPPPoE1000Controller(MikroTikController):
:return: A structured response to indicate what happened during the configuration attempt. :return: A structured response to indicate what happened during the configuration attempt.
""" """
raise NotImplementedError # Start with some variables:
keep_going = True
all_messages = []
roll_back_response = MikroTikConfigAttemptResponse()
# First, we arrange an interface:
if keep_going:
step_response = await self.interface_roll_back(mikrotik_auth, use_https = False)
all_messages.append(step_response.message)
keep_going = step_response.success
# Done here:
roll_back_response.success = keep_going
roll_back_response.message = " -> ".join(all_messages)
return roll_back_response
async def configure( async def configure(
self, self,
@@ -242,7 +423,21 @@ class MikroTikPPPoE1000Controller(MikroTikController):
:return: A structured response to indicate what happened during the configuration attempt. :return: A structured response to indicate what happened during the configuration attempt.
""" """
raise NotImplementedError # Start with some variables:
keep_going = True
all_messages = []
config_response = MikroTikConfigAttemptResponse()
# First, we arrange an interface:
if keep_going:
step_response = await self.interface_setup(mikrotik_auth, use_https = False)
all_messages.append(step_response.message)
keep_going = step_response.success
# Done here:
config_response.success = keep_going
config_response.message = " -> ".join(all_messages)
return config_response
# ***************************************************************************************************************** # *****************************************************************************************************************
+176 -12
View File
@@ -1,7 +1,7 @@
# MiktoTik Configuration Steps # MikroTik (PPPoE) Configuration Steps for 1,000 Clients
#### 20250210 / Khushal P S #### 20250210 / Khushal P S
We need to configure MikroTik servers for the end use case of "PPPoE"Here are the steps to achieve it through MikroTik's We need to configure MikroTik servers for the end use case of "PPPoE". Here are the steps to achieve it through
REST API facility. Use the `MiktoTik (EasyFi)` Postman collection for this. MikroTik's REST API facility. Use the `MiktoTik (EasyFi)` Postman collection for this.
--- ---
@@ -10,6 +10,24 @@ REST API facility. Use the `MiktoTik (EasyFi)` Postman collection for this.
MikroTik allows access via REST API using Basic-Auth headers which take a `username` and a `password`. The base path MikroTik allows access via REST API using Basic-Auth headers which take a `username` and a `password`. The base path
would look something like `http://<ip-addr>/rest`. You may need to mention a port no. if the default has been changed. would look something like `http://<ip-addr>/rest`. You may need to mention a port no. if the default has been changed.
You must also know what are "Private" and "Public" IP addresses in the context of an ISP. For an ISP, every IP that he
gives to his client is "Private" to his network, and every IP that he uses to connect his client to the broader internet
is a "Public" IP. Typically, many private IPs use the same public IP to connect to the internet. You will configure this
ratio in the NAT-ing step.
### Broad Steps
- We first select a physical interface.
- Then we create all the needed VLANs and point them to the physical interface. Let's say this no. is 'n'.
- Then we cut up the full private IP block into 'n' pools.
- Then we create 'n' no. of PPPoE profiles such that each profile gets one private IP pool.
- Then we create 'n' no. of PPPoE servers such that each server gets one IP pool (through a profile), and one VLAN interface.
- Set up AAA such that we use RADIUS.
- Then we add the given RADIUS server's config.
- Then we allow incoming traffic from RADIUS servers.
- Then we populate the NAT table with rules.
- Then we add a new SNMP community.
--- ---
## Steps for PPPoE ## Steps for PPPoE
@@ -30,43 +48,189 @@ is an actual physical connectivity interface (typically ethernet) on the MikroTi
You will be given a range of VLAN interfaces like `"2001-2010"`. Create on VLAN for each id such that the value of the You will be given a range of VLAN interfaces like `"2001-2010"`. Create on VLAN for each id such that the value of the
name of the `"interface"` that you set for it is set to `"easyfi-pppoe"` (the name of the physical interface). name of the `"interface"` that you set for it is set to `"easyfi-pppoe"` (the name of the physical interface).
Use `PUT` method on the path `/interface/vlan`. Consider to the following example JSON: Use `PUT` or `PATCH` method on the path `/interface/vlan`. Consider to the following example JSON:
```json ```json
{ {
"name": "easyfi-vlan-2001", "name": "easyfi-vlan-2001", // .... Use the VLAN id in the name.
"interface": "easyfi-pppoe", "interface": "easyfi-pppoe", // ... The h/w interface that you prepared in the prev. step.
"vlan-id": "2001", "vlan-id": "2001", // ............. The VLAN id.
"disabled": "false", "disabled": "false", // ........... To ensure it is enabled by default.
"comment": "easyfi" "comment": "{...}" // ............. The JSON string to indicate automated config.
} }
``` ```
**NOTE:** Looping needed. **NOTE:** Looping needed.
**ROLL-BACK:** Save the original configuration as a JSON string in the `comment` field. If there was no original **ROLL-BACK:** Save the original configuration as a JSON string in the `comment` field. If there was no original
configuration, simply delete your record. configuration, simply delete your record.
### 3. Assign Private IP Subnets to the VLANs ### 3. Create Private IP Subnets (a.k.a. Pools/Ranges)
You will receive the Private IP range in either CIDR notation or as a hyphen-separated string. You will receive the Private IP range in either CIDR notation or as a hyphen-separated string. You need to cut up this
larger IP pool into subnets that you will eventually assign to each VLAN (by way of creating PPPoE servers).
Use `PUT` or `PATCH` method on the path `/ip/pool`. Consider to the following example JSON:
```json
{
"name": "easyfi-pppoe-pool-2001", // ... Use the VLAN id in the name.
"ranges": "100.64.0.0/24", // .......... The subnet for the associated VLAN id.
"comment": "{...}" // .................. The JSON string to indicate automated config.
}
```
**NOTE:** Looping needed.
**ROLL-BACK:** Save the original configuration as a JSON string in the `comment` field. If there was no original
configuration, simply delete your record.
### 4. Create PPPoE Profiles
Now we are ready to create PPPoE profiles. A profile is like a blueprint for the servers we will create later. It
defines the rules that the server must follow. For now, our major focus is on telling the server which private IP pool
to use. We create the same no. of PPPoE profiles as we have created VLANs.
Use `PUT` or `PATCH` method on the path `/ppp/profile`. Consider to the following example JSON:
```json
{
"name": "easyfi-pppoe-prf-2001", // .............. Use the VLAN id in the name.
"local-address": "192.168.10.1", // .............. The IP address of the MikroTik device itself (NAS IP).
"remote-address": "easyfi-pppoe-pool-2001", // ... Use the VLAN id in the name.
"comment": "{...}" // ............................ The JSON string to indicate automated config.
}
```
**NOTE:** Looping needed.
**ROLL-BACK:** Save the original configuration as a JSON string in the `comment` field. If there was no original
configuration, simply delete your record.
### 5. Create PPPoE Servers
Here's where we tie up ALL the steps we've done so far!
In this step we spin up actual server instances (software) that handle the PPPoE traffic. On one hand we have virtual
sub-blocks of our private IP pool, and on the other we have VLANs that point to the physical interfaces. Here we spin
up servers that use one VLAN and one private IP sub-pool to actually handle the traffic.
Use `PUT` or `PATCH` method on the path `/interface/pppoe-server/server`. Consider to the following example JSON:
```json
{
"interface": "vlan10", // ..................... The VLAN interface you had created earlier.
"profile": "easyfi-pppoe-prf-2001", // ........ The PPPoE profile you created earlier.
"service-name": "easyfi-pppoe-srv-2001", // ... Use the VLAN id in the name.
"comment": "{...}" // ......................... The JSON string to indicate automated config.
}
```
**NOTE:** Looping needed.
**ROLL-BACK:** Save the original configuration as a JSON string in the `comment` field. If there was no original
configuration, simply delete your record.
### 6. Set-up AAA to use RADIUS
Now we need to tell the MikroTik device to use a RADIUS server for AAA.
Use `POST` method on the path `/ppp/aaa/set`. Use the following JSON as is:
```json
{
"accounting": "true",
"interim-update": "3m",
"use-circuit-id-in-nas-port-id": "false",
"use-radius": "true"
}
```
**ROLL-BACK:** Not known.
### 7. Set up the given RADIUS Server
We configure the MikroTik to use the given Radius Server for its AAA activities.
Use `PUT` or `PATCH` method on the path `/radius`. Consider to the following example JSON:
```json
{
"name": "easyfi-radius", // ............... Use this value.
"address": "<radius_server_ip>", // ....... The IP address of the RADIUS server.
"secret": "<radius_server_secret>", // .... The "password" of the RADIUS server.
"accounting-port": "1813", // ............. Port of the RADIUS server.
"authentication-port": "1812", // ......... Port of the RADIUS server.
"disabled": "false", // ................... Use this value.
"protocol": "udp", // ..................... Use this value.
"service": "ppp,login,hotspot,dhcp", // ... Use this value.
"timeout": "300ms", // .................... Use this value.
"comment": "{...}" // ..................... The JSON string to indicate automated config.
}
```
**ROLL-BACK:** Enlist the RADIUS servers in the system and remove the one with your name (`easyfi-radius`) in it. You
may also use some other identifier like the IP address of the RADIUS server or the contents of the `comment` field.
### 8. Allow Incoming Traffic from RADIUS Servers
In some cases the RADIUS server needs to be able to initiate the communication. We allow that in this step.
Use `POST` method on the path `/radius/incoming/set`. Consider to the following example JSON:
```json
{
"accept": "yes", // ... Enable incoming RADIUS handling
"port": "3799" // .... Port of the MikroTik server for RADIUS authentication.
}
```
**ROLL-BACK:** Not known.
### 9. NAT Table Setup
We now add IP mapping rules to the NAT table. Any ISP is expected to have more private IPs than public IPs. The ISP will
assign private IPs to his clients such that multiple private IPs will use the same public IP to connect to the internet.
In this step we are effectively assigning one slice of the ISP's private IP to one of his public IPs. A known good
sharing ratio is 16:1, but we split it equally.
In the following example, all the traffic from all the IPs in the `src-address` will be sent out to the open internet as
if it were being sent from the IP mentioned in the `to-addresses`.
Use `PUT` method on the path `/ip/firewall/nat`. Consider to the following example JSON:
```json
{
"name": "easyfi-pppoe-nat-0000", // ..... A name to later identify the rules created by the automated script.
"action": "src-nat", // ................. To indicate that we are translating a private IP to a public IP.
"chain": "srcnat", // ................... To indicate that we are translating a private IP to a public IP.
"disabled": "false", // ................. To enable the rule immediately.
"src-address": "100.64.0.16/28", // ..... One of your private IP subnets.
"to-addresses": "111.222.111.111", // ... One of your public IPs.
"comment": "{...}" // ................... The JSON string to indicate automated config.
}
```
**ROLL-BACK:** Identify entries from their `name` field (contains 'easyfi') or from the contents of the `comment` field
and remove them.
### 10. Add the SNMP Community
This is needed by the Network Management System (NMS). I don't know much about it, but it is a very straight-forward
step.
Use `PUT` or `PATCH` method on the path `/snmp/community`. Consider to the following example JSON:
```json
{
"name": "easyfi-snmp", // .............. A name to later identify the entry created by the automated script.
"addresses": "::/0", // ................ Use this value.
"authentication-password": "", // ...... Use this value.
"authentication-protocol": "MD5", // ... Use this value.
"disabled": "false", // ................ Use this value.
"encryption-password": "", // .......... Use this value.
"encryption-protocol": "DES", // ....... Use this value.
"read-access": "true", // .............. Use this value.
"security": "none", // ................. Use this value.
"write-access": "false", // ............ Use this value.
"comment": "{...}" // .................. The JSON string to indicate automated config.
}
```
**ROLL-BACK:** Identify your entry from the `name` field (contains 'easyfi') or from the contents of the `comment` field
and remove it.
---
*End of Document.*