How to Set Up a PPPoE Billing Software with RouterOS on Ubuntu 26.04
04 May, 2026
Introduction
Point-to-Point Protocol over Ethernet (PPPoE) is a network protocol that Internet Service Providers (ISPs) use to establish and manage customer connections. RouterOS, the operating system running on MikroTik network devices, includes a built‑in PPPoE server that authenticates users through local secrets stored on the router. When your Ubuntu VPS cannot access the MikroTik router directly over a local network, WireGuard creates a secure encrypted tunnel between the VPS and the router. A Python FastAPI application then communicates through this tunnel using the RouterOS API library to create, enable, or disable PPPoE secrets programmatically.
This guide teaches you how to build a PPPoE billing API on an Ubuntu VPS that connects to a remote MikroTik router through WireGuard and manages PPPoE secrets using the RouterOS API library.
Prerequisites
Before you start:
- Deploy an Ubuntu 26.04 VPS with at least 1 GB RAM and 10 GB storage.
- Connect to your VPS through SSH using PuTTY for Windows or Open SSH for Linux and Mac OS.
- Create a non‑root user with sudo privileges.
- Deploy a MikroTik router running RouterOS v6 or v7 with PPPoE server enabled on a local network or another cloud location.
- Obtain the MikroTik router IP address, API port (default 8728 for plain text or 8729 for SSL), username, and password.
- Configure WireGuard on both the MikroTik router and Ubuntu VPS to establish a secure tunnel between them.
Install System Dependencies
Your Ubuntu VPS needs Python 3, PostgreSQL, and WireGuard tools to run the FastAPI billing application.
-
Refresh your system package list to get the latest available versions.
console$ sudo apt update -
Install Python 3, pip, PostgreSQL, WireGuard tools, and development libraries.
console$ sudo apt install -y python3 python3-pip python3-venv postgresql postgresql-contrib wireguard build-essentialOutput:
Reading package lists... Done Building dependency tree... Done Reading state information... Done The following additional packages will be installed: libpq-dev postgresql-client wireguard-tools 0 upgraded, 18 newly installed, 0 to remove and 0 not upgraded. -
Verify the Python installation.
console$ python3 --versionOutput:
Python 3.12.3 -
Verify the WireGuard installation.
console$ wg --versionOutput:
wireguard-tools v1.0.20210914
Configure WireGuard Tunnel Between VPS and MikroTik Router
WireGuard creates a secure virtual network interface that allows your Ubuntu VPS to communicate with the MikroTik router as if they were on the same local network.
-
Generate a private and public key pair for the Ubuntu VPS.
console$ wg genkey | tee vps_private.key | wg pubkey > vps_public.keyOutput:
(No output, keys saved to files) -
Display the VPS private key.
console$ cat vps_private.keyOutput:
mJ7fG8dK9eL2pQ3rS4tU5vW6xY7zA8bC9dE0fG1hI2jK= -
Display the VPS public key to share with the MikroTik router.
console$ cat vps_public.keyOutput:
aB3cD4eF5gH6iJ7kL8mN9oP0qR1sT2uV3wX4yZ5A6bC7= -
Create a new WireGuard configuration file using the nano text editor.
console$ sudo nano /etc/wireguard/wg0.conf -
Add the VPS side configuration to the new
wg0.conffile. Replace192.168.200.2/24with your chosen tunnel IP andPublicKeywith the MikroTik router public key.INI[Interface] Address = 192.168.200.2/24 PrivateKey = mJ7fG8dK9eL2pQ3rS4tU5vW6xY7zA8bC9dE0fG1hI2jK= ListenPort = 51820 [Peer] PublicKey = tX6yZ7aB8cC9dD0eE1fF2gG3hH4iI5jJ6kK7lL8mM9nN0= AllowedIPs = 192.168.200.1/32, 10.10.10.0/24 Endpoint = mikrotik_public_ip:51820 PersistentKeepalive = 25 -
Save and close the
/etc/wireguard/wg0.conffile by pressing Ctrl + X, then Y, then Enter. -
Enable and start the WireGuard interface.
console$ sudo systemctl enable wg-quick@wg0Output:
Created symlink /etc/systemd/system/multi-user.target.wants/wg-quick@wg0.service → /lib/systemd/system/wg-quick@.service. -
Start the WireGuard tunnel.
console$ sudo systemctl start wg-quick@wg0 -
Verify the WireGuard tunnel status.
console$ sudo wg showOutput:
interface: wg0 public key: aB3cD4eF5gH6iJ7kL8mN9oP0qR1sT2uV3wX4yZ5A6bC7= private key: (hidden) listening port: 51820 peer: tX6yZ7aB8cC9dD0eE1fF2gG3hH4iI5jJ6kK7lL8mM9nN0= endpoint: 203.0.113.5:51820 allowed ips: 192.168.200.1/32, 10.10.10.0/24 latest handshake: 5 seconds ago transfer: 1.23 KiB received, 2.34 KiB sent persistent keepalive: every 25 seconds -
Test the connection to the MikroTik router over the WireGuard tunnel.
console$ ping -c 3 192.168.200.1Output:
PING 192.168.200.1 (192.168.200.1) 56(84) bytes of data. 64 bytes from 192.168.200.1: icmp_seq=1 ttl=64 time=45.2 ms 64 bytes from 192.168.200.1: icmp_seq=2 ttl=64 time=42.8 ms 64 bytes from 192.168.200.1: icmp_seq=3 ttl=64 time=44.1 ms --- 192.168.200.1 ping statistics --- 3 packets transmitted, 3 received, 0% packet loss, time 2003ms
Configure PostgreSQL Database
The billing system stores customer information in a PostgreSQL database on the Ubuntu VPS.
-
Log in to the PostgreSQL server as the
postgresuser.console$ sudo -u postgres psqlOutput:
psql (18.3 (Ubuntu 18.3-1)) Type "help" for help. postgres=# -
Create a database for the billing application.
SQLpostgres=# CREATE DATABASE pppoe_billing;Output:
CREATE DATABASE -
Create a database user for the application.
SQLpostgres=# CREATE USER billing_user WITH PASSWORD 'secure_password_here';Output:
CREATE ROLE -
Grant all privileges on the database to the new user.
SQLpostgres=# GRANT ALL PRIVILEGES ON DATABASE pppoe_billing TO billing_user;Output:
GRANT -
Exit from the PostgreSQL prompt.
SQLpostgres=# \qOutput:
$ -
Connect to the database as
billing_userto create the customer table.console$ psql -U billing_user -d pppoe_billingOutput:
Password for user billing_user: psql (18.3 (Ubuntu 18.3-1)) Type "help" for help. pppoe_billing=> -
Create the
customerstable to store PPPoE account information.SQLpppoe_billing=> CREATE TABLE customers ( id SERIAL PRIMARY KEY, username VARCHAR(50) UNIQUE NOT NULL, password VARCHAR(100) NOT NULL, service_name VARCHAR(50) DEFAULT 'pppoe', is_active BOOLEAN DEFAULT true, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );Output:
CREATE TABLE -
Create an index on the username column for faster lookups.
SQLpppoe_billing=> CREATE INDEX idx_customers_username ON customers(username);Output:
CREATE INDEX -
Exit from the PostgreSQL prompt.
SQLpppoe_billing=> \qOutput:
$
Create Project Directory and Virtual Environment
Isolating your billing project dependencies prevents conflicts with other Python applications on the VPS.
-
Create a directory for the billing application.
console$ mkdir ~/pppoe-billing -
Navigate into the new directory.
console$ cd ~/pppoe-billing -
Create a Python virtual environment.
console$ python3 -m venv billing-env -
Activate the virtual environment.
console$ source billing-env/bin/activate -
Verify your active environment shows the virtual environment name in the shell prompt.
console(billing-env) $ which python3Output:
/home/username/pppoe-billing/billing-env/bin/python3
Install Required Python Packages
The billing system requires FastAPI for the web framework, the RouterOS API library for MikroTik communication, psycopg2 for PostgreSQL connectivity, and Uvicorn as the ASGI server.
-
Install the core Python packages.
console(billing-env) $ pip install fastapi uvicorn psycopg2-binary routeros-apiOutput:
Collecting fastapi Downloading fastapi-0.115.6-py3-none-any.whl (95 kB) Collecting uvicorn Downloading uvicorn-0.34.0-py3-none-any.whl (62 kB) Collecting psycopg2-binary Downloading psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_x86_64.whl (3.1 MB) Collecting routeros-api Downloading routeros_api-1.8.1-py3-none-any.whl (15 kB) Successfully installed fastapi-0.115.6 uvicorn-0.34.0 psycopg2-binary-2.9.10 routeros-api-1.8.1
Configure MikroTik Router API Access
The MikroTik router must have the API service enabled and a user with appropriate permissions.
-
Log in to your MikroTik router using WinBox, SSH, or the web interface.
-
Enable the API service. Navigate to IP → Services, find api, and enable it on port 8728.
console/ip service enable api -
Verify the API service status.
console/ip service print where name=apiOutput:
Flags: X - disabled, I - invalid # NAME PORT ADDRESS CERTIFICATE 0 api 8728 0.0.0.0/0 -
Create an API user with write permissions for PPPoE management.
console/user add name=api-user password=router_api_password group=write
Create the FastAPI Application
The FastAPI application provides REST endpoints to create customers and enable or disable PPPoE secrets on the remote MikroTik router through the WireGuard tunnel.
-
Create a new
main.pyfile using the nano text editor.console(billing-env) $ nano main.py -
Add the complete FastAPI application code to the new
main.pyfile.Pythonfrom fastapi import FastAPI, HTTPException from pydantic import BaseModel from datetime import datetime import psycopg2 import psycopg2.extras from routeros_api import RouterOsApiPool from routeros_api.exceptions import RouterOsApiConnectionError from typing import Optional app = FastAPI(title="PPPoE Billing API", description="Manage PPPoE secrets on MikroTik RouterOS via WireGuard") # PostgreSQL connection configuration DB_CONFIG = { "dbname": "pppoe_billing", "user": "billing_user", "password": "secure_password_here", "host": "localhost", "port": 5432 } # MikroTik RouterOS connection configuration (use WireGuard tunnel IP) ROUTEROS_CONFIG = { "host": "192.168.200.1", "port": 8728, "username": "api-user", "password": "router_api_password" } # Pydantic models for request validation class CustomerCreate(BaseModel): username: str password: str service_name: str = "pppoe" class SecretToggle(BaseModel): username: str enable: bool def get_db_connection(): """Create and return a PostgreSQL database connection.""" conn = psycopg2.connect(**DB_CONFIG) return conn def get_routeros_connection(): """Create and return a RouterOS API connection pool through WireGuard tunnel.""" try: pool = RouterOsApiPool( host=ROUTEROS_CONFIG["host"], port=ROUTEROS_CONFIG["port"], username=ROUTEROS_CONFIG["username"], password=ROUTEROS_CONFIG["password"], use_ssl=False, plaintext_login=True ) api = pool.get_api() return api, pool except RouterOsApiConnectionError as e: raise HTTPException(status_code=500, detail=f"RouterOS connection failed: {str(e)}") def add_pppoe_secret_to_router(username: str, password: str, service_name: str): """Add a PPPoE secret to MikroTik RouterOS using the API library.""" api, pool = get_routeros_connection() try: ppp_secret_resource = api.get_resource("/ppp/secret") # Create the new PPPoE secret secret = ppp_secret_resource.add( name=username, password=password, service=service_name, disabled="no" ) pool.disconnect() return True, secret except Exception as e: pool.disconnect() return False, str(e) def disable_pppoe_secret_on_router(username: str): """Disable a PPPoE secret on MikroTik RouterOS.""" api, pool = get_routeros_connection() try: ppp_secret_resource = api.get_resource("/ppp/secret") # Find the secret by username secrets = ppp_secret_resource.get(name=username) if not secrets: pool.disconnect() return False, f"PPPoE secret {username} not found on router" # Disable the secret secret_id = secrets[0]['id'] ppp_secret_resource.set(id=secret_id, disabled="yes") pool.disconnect() return True, {"message": f"Secret {username} disabled", "id": secret_id} except Exception as e: pool.disconnect() return False, str(e) def enable_pppoe_secret_on_router(username: str): """Enable a PPPoE secret on MikroTik RouterOS.""" api, pool = get_routeros_connection() try: ppp_secret_resource = api.get_resource("/ppp/secret") # Find the secret by username secrets = ppp_secret_resource.get(name=username) if not secrets: pool.disconnect() return False, f"PPPoE secret {username} not found on router" # Enable the secret secret_id = secrets[0]['id'] ppp_secret_resource.set(id=secret_id, disabled="no") pool.disconnect() return True, {"message": f"Secret {username} enabled", "id": secret_id} except Exception as e: pool.disconnect() return False, str(e) @app.post("/customers") def create_customer(customer: CustomerCreate): """ Create a new customer in the database and add PPPoE secret to MikroTik RouterOS. """ conn = get_db_connection() cursor = conn.cursor(cursor_factory=psycopg2.extras.DictCursor) try: # Check if customer already exists in database cursor.execute("SELECT id FROM customers WHERE username = %s", (customer.username,)) existing = cursor.fetchone() if existing: raise HTTPException(status_code=400, detail="Customer username already exists") # Add PPPoE secret to RouterOS through WireGuard tunnel success, result = add_pppoe_secret_to_router( customer.username, customer.password, customer.service_name ) if not success: raise HTTPException(status_code=500, detail=f"RouterOS error: {result}") # Insert customer into PostgreSQL database cursor.execute( """ INSERT INTO customers (username, password, service_name, is_active) VALUES (%s, %s, %s, %s) RETURNING id, username, service_name, is_active, created_at """, (customer.username, customer.password, customer.service_name, True) ) conn.commit() new_customer = cursor.fetchone() return { "message": "Customer created and PPPoE secret added to router", "customer": dict(new_customer), "router_response": result } except HTTPException: raise except Exception as e: conn.rollback() raise HTTPException(status_code=500, detail=str(e)) finally: cursor.close() conn.close() @app.patch("/secrets/toggle") def toggle_pppoe_secret(data: SecretToggle): """ Enable or disable a PPPoE secret on MikroTik RouterOS and update the database status. """ conn = get_db_connection() cursor = conn.cursor(cursor_factory=psycopg2.extras.DictCursor) try: # Check if customer exists in database cursor.execute( "SELECT id, username, is_active FROM customers WHERE username = %s", (data.username,) ) customer = cursor.fetchone() if not customer: raise HTTPException(status_code=404, detail="Customer not found in database") # Enable or disable on RouterOS if data.enable: success, result = enable_pppoe_secret_on_router(data.username) action = "enabled" new_status = True else: success, result = disable_pppoe_secret_on_router(data.username) action = "disabled" new_status = False if not success: raise HTTPException(status_code=500, detail=f"RouterOS error: {result}") # Update database status cursor.execute( """ UPDATE customers SET is_active = %s, updated_at = CURRENT_TIMESTAMP WHERE username = %s """, (new_status, data.username) ) conn.commit() return { "message": f"PPPoE secret {action} successfully", "username": data.username, "is_active": new_status, "router_response": result } except HTTPException: raise except Exception as e: conn.rollback() raise HTTPException(status_code=500, detail=str(e)) finally: cursor.close() conn.close() @app.get("/secrets/{username}") def get_pppoe_secret_status(username: str): """ Retrieve the current status of a PPPoE secret from MikroTik RouterOS. """ api, pool = get_routeros_connection() try: ppp_secret_resource = api.get_resource("/ppp/secret") secrets = ppp_secret_resource.get(name=username) pool.disconnect() if not secrets: raise HTTPException(status_code=404, detail=f"PPPoE secret {username} not found on router") return { "username": username, "disabled": secrets[0].get('disabled', 'true'), "service": secrets[0].get('service', 'pppoe'), "router_comment": secrets[0].get('comment', '') } except RouterOsApiConnectionError as e: pool.disconnect() raise HTTPException(status_code=500, detail=f"RouterOS connection error: {str(e)}") except Exception as e: pool.disconnect() raise HTTPException(status_code=500, detail=str(e)) -
Save and close the
main.pyfile by pressing Ctrl + X, then Y, then Enter.
Run the FastAPI Application
Start the Uvicorn server to expose the PPPoE billing API endpoints.
-
Run the FastAPI application using Uvicorn.
console(billing-env) $ uvicorn main:app --host 0.0.0.0 --port 8000 --reloadOutput:
INFO: Started server process [12345] INFO: Waiting for application startup. INFO: Application startup complete. INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
Press Ctrl + C to stop the server when needed.
Configure Firewall for API Access
Allow external access to the FastAPI application on port 8000.
-
Allow port 8000 through the Ubuntu firewall.
console$ sudo ufw allow 8000/tcp -
Verify the firewall rules.
console$ sudo ufw statusOutput:
Status: active To Action From -- ------ ---- 22/tcp ALLOW Anywhere 8000/tcp ALLOW Anywhere 22/tcp (v6) ALLOW Anywhere (v6) 8000/tcp (v6) ALLOW Anywhere (v6)
Test the PPPoE Billing API Endpoints
Test the API endpoints using curl commands from another terminal or your local machine.
-
Create a new customer with a PPPoE secret.
console$ curl -X POST "http://your_vps_ip:8000/customers" \ -H "Content-Type: application/json" \ -d '{"username": "john_customer", "password": "customer123", "service_name": "pppoe"}'Output:
JSON{ "message": "Customer created and PPPoE secret added to router", "customer": { "id": 1, "username": "john_customer", "service_name": "pppoe", "is_active": true, "created_at": "2026-05-04T10:30:45.123456" }, "router_response": { ".id": "*1", "name": "john_customer", "password": "customer123", "service": "pppoe", "disabled": "false" } } -
Check the status of a PPPoE secret on the router.
console$ curl -X GET "http://your_vps_ip:8000/secrets/john_customer"Output:
JSON{ "username": "john_customer", "disabled": "false", "service": "pppoe", "router_comment": "" } -
Disable the PPPoE secret to suspend a customer.
console$ curl -X PATCH "http://your_vps_ip:8000/secrets/toggle" \ -H "Content-Type: application/json" \ -d '{"username": "john_customer", "enable": false}'Output:
JSON{ "message": "PPPoE secret disabled successfully", "username": "john_customer", "is_active": false, "router_response": { "message": "Secret john_customer disabled", "id": "*1" } } -
Enable the PPPoE secret to reactivate a customer.
console$ curl -X PATCH "http://your_vps_ip:8000/secrets/toggle" \ -H "Content-Type: application/json" \ -d '{"username": "john_customer", "enable": true}'Output:
JSON{ "message": "PPPoE secret enabled successfully", "username": "john_customer", "is_active": true, "router_response": { "message": "Secret john_customer enabled", "id": "*1" } }
Verify PPPoE Secret on MikroTik Router
Log in to your MikroTik router to confirm the PPPoE secret exists and reflects the changes made through the API.
-
Check the PPPoE secrets list on the router.
console/ppp secret printOutput:
Flags: X - disabled # NAME SERVICE CALLER-ID ADDRESS REMOTE-ADDRESS PROFILE ROUTE LIMIT-BYTES-IN LIMIT-BYTES-OUT 0 john_customer pppoe local 0 0 -
Verify the secret is enabled (no X flag).
-
Disable the secret through the API and check again.
console/ppp secret printOutput:
Flags: X - disabled # NAME SERVICE CALLER-ID ADDRESS REMOTE-ADDRESS PROFILE ROUTE LIMIT-BYTES-IN LIMIT-BYTES-OUT 0 X john_customer pppoe local 0 0
The X flag confirms the secret is disabled.
Understand the API Workflow for Billing Integration
Your billing system can now integrate these endpoints into a complete customer management workflow.
-
Customer registration: Call
POST /customersto create a new PPPoE account. The API adds the secret to RouterOS and stores the record in PostgreSQL. -
Customer suspension: Call
PATCH /secrets/togglewithenable: falseto disable the PPPoE secret when a customer misses a payment. -
Customer reactivation: Call
PATCH /secrets/togglewithenable: trueto enable the PPPoE secret after payment confirmation. -
Account verification: Call
GET /secrets/{username}to check the current status before applying changes.
Later, you can extend this system by merging customer records with payment history, usage tracking, and invoice generation.
Conclusion
In this guide, you have set up a PPPoE billing API on an Ubuntu VPS that connects to a remote MikroTik router through a WireGuard tunnel. You installed PostgreSQL to store customer records, built a FastAPI application using the RouterOS API library, and created endpoints to add, enable, and disable PPPoE secrets. You tested the API by creating a customer, toggling the secret status, and verifying changes on the router. Now that your billing API runs correctly, consider extending it with payment tracking, usage monitoring through RADIUS accounting, or building a customer self‑service portal that calls these same endpoints for plan upgrades and account management.