Home → Articles → How to Set Up a PPPoE Billing Software with RouterOS on Ubuntu 26.04

How to Set Up a PPPoE Billing Software with RouterOS on Ubuntu 26.04

Published: Last updated:

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-essential
    

    Output:

    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 --version
    

    Output:

    Python 3.12.3
    
  • Verify the WireGuard installation.

    console
    $ wg --version
    

    Output:

    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.key
    

    Output:

    (No output, keys saved to files)
    
  • Display the VPS private key.

    console
    $ cat vps_private.key
    

    Output:

    mJ7fG8dK9eL2pQ3rS4tU5vW6xY7zA8bC9dE0fG1hI2jK=
    
  • Display the VPS public key to share with the MikroTik router.

    console
    $ cat vps_public.key
    

    Output:

    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.conf file. Replace 192.168.200.2/24 with your chosen tunnel IP and PublicKey with 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.conf file by pressing Ctrl + X, then Y, then Enter.

  • Enable and start the WireGuard interface.

    console
    $ sudo systemctl enable wg-quick@wg0
    

    Output:

    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 show
    

    Output:

    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.1
    

    Output:

    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 postgres user.

    console
    $ sudo -u postgres psql
    

    Output:

    psql (18.3 (Ubuntu 18.3-1))
    Type "help" for help.
    
    postgres=#
    
  • Create a database for the billing application.

    SQL
    postgres=# CREATE DATABASE pppoe_billing;
    

    Output:

    CREATE DATABASE
    
  • Create a database user for the application.

    SQL
    postgres=# CREATE USER billing_user WITH PASSWORD 'secure_password_here';
    

    Output:

    CREATE ROLE
    
  • Grant all privileges on the database to the new user.

    SQL
    postgres=# GRANT ALL PRIVILEGES ON DATABASE pppoe_billing TO billing_user;
    

    Output:

    GRANT
    
  • Exit from the PostgreSQL prompt.

    SQL
    postgres=# \q
    

    Output:

    $
    
  • Connect to the database as billing_user to create the customer table.

    console
    $ psql -U billing_user -d pppoe_billing
    

    Output:

    Password for user billing_user: 
    psql (18.3 (Ubuntu 18.3-1))
    Type "help" for help.
    
    pppoe_billing=>
    
  • Create the customers table to store PPPoE account information.

    SQL
    pppoe_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.

    SQL
    pppoe_billing=> CREATE INDEX idx_customers_username ON customers(username);
    

    Output:

    CREATE INDEX
    
  • Exit from the PostgreSQL prompt.

    SQL
    pppoe_billing=> \q
    

    Output:

    $
    

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 python3
    

    Output:

    /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-api
    

    Output:

    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=api
    

    Output:

    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.py file using the nano text editor.

    console
    (billing-env) $ nano main.py
    
  • Add the complete FastAPI application code to the new main.py file.

    Python
    from 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.py file 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 --reload
    

    Output:

    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 status
    

    Output:

    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 print
    

    Output:

    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 print
    

    Output:

    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 /customers to create a new PPPoE account. The API adds the secret to RouterOS and stores the record in PostgreSQL.

  • Customer suspension: Call PATCH /secrets/toggle with enable: false to disable the PPPoE secret when a customer misses a payment.

  • Customer reactivation: Call PATCH /secrets/toggle with enable: true to 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.