Blog

  • First Post: (Actually) Evaluating Per-ASN Traffic

    First Post: (Actually) Evaluating Per-ASN Traffic

    As part of our effort to make management more efficient, we’ve been working on meaningfully monitoring what’s going on with our infrastructure.

    Key word: meaningful. It’s one thing to just keep logs of everything. It’s another to make actionable insights.

    And what’s more actionable than knowing what our clients need? As an Internet Service Provider, a core part of this is traffic.

    To study what upstreams we need to optimize, we need to monitor where our traffic comes from and where it goes.

    There are several tools out there such as pmacct, pNRG, FastNetMon, SolarWinds, as-stats — but after evaluating options, we chose as-stats.

    Primarily because it’s free.

    It’s also self-contained – just one component to deal with (as-stats) rather than linking multiple tools together (Grafana + InfluxDB + pmacct for instance)

    Finally, it’s PHP-based. Our infrastructure is already mostly centered on Debian and PHP, so as-stats was right at home and in line with our technical competencies.

    Oh, and APNIC has an existing blog post on the matter here. However, it’s outdated and lacks a lot of steps that took us several hours to deal with.

    Without further ado,

    Getting started with AS-Stats
    (on a modern system)

    In order to make sure what I’m writing here actually works, I’m setting up as-stats again and documenting what I do in this post. I’m setting up a new VM in Proxmox — I’m going with

    • a Virtual Machine (I have not done this in a container, YMMV)
    • These settings, as they’re our standard for VMs:
      • Ubuntu 24.04.2 LTS from a Live Server ISO
      • q35 machine type (under System -> Machine)
      • Large SCSI disk with SSD emulation and Discard enabled
      • “Host” CPU type
      • And a network interface in the same LAN as our NetFlow source, for ease of setup.
    We’re doing it from scratch.

    Setting up a VM and installing an OS is out of this article’s scope.

    Installing Dependencies

    Usual update right after system install.

    sudo apt-get update
    sudo apt-get upgrade

    I’m trying to go with the latest versions available in apt repositories. This means no PHP 8.4 as of writing.

    sudo apt install librrds-perl librrd-dev rrdtool apache2 php8.3 make gcc git libapache2-mod-php8.3
    
    
    # Required by Perl's Net:Patricia during compilation
    sudo apt install libnsl-dev 
    
    
    # Required by as-stats-gui
    sudo apt install php8.3-sqlite3
    
    
    # For debugging
    sudo apt install net-tools

    Onto the Perl modules required by as-stats.

    During the first module install, I got asked if I wanted to configure as much as possible automatically. I simply said yes.

    I think this is because I’m using cpan for the first time?

    Further, some of the modules take a while to set up due to compilation and such. I think it would be wise to run these commands individually.

    sudo cpan install File::Find::Rule
    sudo cpan install Net::sFlow     # Long install
    sudo cpan install IO::Select
    sudo cpan install IO::Socket     # Installed quickly
    sudo cpan install Scalar::Util
    
    
    # Not in APNIC guide, required by asstatd.pl
    sudo cpan install JSON::XS 
    sudo cpan install Net::Patricia
    
    
    # Not in APNIC guide, required by rrd-extractstats.pl
    sudo cpan install DBI          # Installed in ~2 min
    sudo cpan install TryCatch     # 26m 10.260s
    sudo cpan install DBD::SQLite  # I stopped counting

    There is a Perl module, ip2as, that needs to be installed manually.

    First, I checked which version of Perl I had installed.

    perl -v
    
    This is perl 5, version 38, subversion 2 (v5.38.2) built for x86_64-linux-gnu-thread-multi
    (with 44 registered patches, see perl -V for more detail)

    Thanks. Now I went into what I assume is Perl’s plugin directory. I then downloaded ip2as by JackSlateur on Github.

    # Required by asstatd.pl
    
    # Install ip2as module
    cd /usr/share/perl/5.38.2
    sudo wget https://raw.githubusercontent.com/JackSlateur/perl-ip2as/master/ip2as.pm
    
    # Get the current IP-ASN mapping. RRD files are not generated without this.
    sudo wget -O /opt/AS-Stats/bin/ip.json https://asndb.network/get/latest/ip.json

    Setting up as-stats

    As per the APNIC guide, I went ahead and placed as-stats in /opt.

    cd /opt/
    sudo git clone https://github.com/manuelkasper/AS-Stats.git
    cd /opt/AS-Stats
    
    
    # Also create the directory for RRD storage
    sudo mkdir /opt/AS-Stats/rrd
    sudo chmod 0777 /opt/AS-Stats/rrd

    I then modified the knownlinks file to reflect my current setup.

    # nano, or your choice of text editor (vi?)
    sudo nano /opt/AS-Stats/conf/knownlinks

    I commented out all the 192.0 examples and put in the interface I was looking to monitor.

    Now, here’s the thing with knownlinks:

    The entries need to be delimited by tabs, not spaces. Take care when you copy your config from somewhere else.

    • For example, if you highlight text in putty, tabs may be copied as multiple spaces.
    • as-stats will throw an error if this is incorrect. It happened in my case. A quick way to check is by pasting your config into Notepad++ and showing all characters
      • View -> Show Symbol -> Show All Characters
    After “17”, what seemed like a tab was actually a space.

    ifindex is in decimal, and must reflect IDs as if collecting data via SNMP. In MikroTik, this ID can be acquired via the command

    /interface/print show-ids
    
    
    # Expected output:
    
    [admin@Border-PHLGES] > /interface/print show-ids 
    Flags: R - RUNNING
    Columns: NAME, TYPE, ACTUAL-MTU
    *     NAME                  TYPE      ACTUAL-MTU
    *6    ether1                ether           1500
    *7    ether2                ether           1500
    *8    ether3                ether           1500
    *9    ether4                ether           1500
    *A    ether5                ether           1500
    *B    ether6                ether           1500
    *C    ether7                ether           1500
    *D    ether8                ether           1500
    *E    ether9                ether           1500
    *F    ether10               ether           1500
    *10   ether11               ether           1500
    *11 R ether12               ether           1500
    *1    ether13               ether           1500
    *2  R sfp-sfpplus1          ether           1500
    *3    sfp-sfpplus2          ether           1500
    *4  R sfp-sfpplus3          ether           1500
    *5    sfp-sfpplus4          ether           1500
    *15 R lo                    loopback       65536
    
    
    # Irrelevant details removed (e.g., MAC address)

    On the leftmost column is the interface ID (thanks to show-ids) in hexadecimal.

    I will be monitoring ether12 and sfp-sfpplus1.

    After converting to decimal, I will use ifindex 17 and 2 respectively in my knownlinks file.

    # Router IP, ifindex, tag, description, graph color, sampling rate
    
    198.51.100.1	17	trans1	Transit 1	A6CEE3	1
    198.51.100.1	2	trans2	Transit 2	FB9A99	1

    As for the router IP, specify your router’s public IP address. However during testing, I did not find this too important so long as

    • It’s assigned to an interface
    • It matches NetFlow’s source address.

    It’s good to verify that we’re actually receiving NetFlow data. I used tcpdump:

    # You may need to change the interface "-i"
    sudo tcpdump -i enp6s18 -nn port 9000
    
    
    # Expected output:
    $ sudo tcpdump -i enp6s18 -nn port 9000
    
    tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
    listening on enp6s18, link-type EN10MB (Ethernet), snapshot length 262144 bytes
    15:32:39.929515 IP 198.51.100.1.9000 > 10.0.0.251.9000: UDP, length 236
    15:32:39.929756 IP 198.51.100.1.9000 > 10.0.0.251.9000: UDP, length 1436
    15:32:39.929756 IP 198.51.100.1.9000 > 10.0.0.251.9000: UDP, length 1436
    15:32:39.929756 IP 198.51.100.1.9000 > 10.0.0.251.9000: UDP, length 1436
    15:32:39.929756 IP 198.51.100.1.9000 > 10.0.0.251.9000: UDP, length 1436

    We’re receiving packets. Great! We should be ready to test as-stats.

    /opt/AS-Stats/bin/asstatd.pl -P 0 -p 9000 -r /opt/AS-Stats/rrd -k /opt/AS-Stats/conf/knownlinks -m /opt/AS-Stats/bin/ip.json

    This command holds the terminal until exit with Ctrl+C.

    At this point, I opened a new terminal and did checks.

    First, I checked if Perl is listening with netstat:

    $ sudo netstat -tulpn
    
    Active Internet connections (only servers)
    Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name
    tcp        0      0 127.0.0.54:53           0.0.0.0:*               LISTEN      7160/systemd-resolv
    tcp        0      0 127.0.0.53:53           0.0.0.0:*               LISTEN      7160/systemd-resolv
    tcp6       0      0 :::22                   :::*                    LISTEN      1/systemd
    tcp6       0      0 :::80                   :::*                    LISTEN      20724/apache2
    udp        0      0 127.0.0.54:53           0.0.0.0:*                           7160/systemd-resolv
    udp        0      0 127.0.0.53:53           0.0.0.0:*                           7160/systemd-resolv
    udp        0      0 10.0.0.251:68           0.0.0.0:*                           6346/systemd-networ
    udp        0      0 0.0.0.0:9000            0.0.0.0:*                           27450/perl

    We see that Perl is listening on port 9000. It should be running!

    Now, we kept checking /opt/AS-Stats/rrd. After a minute or so, it filled up with data.

    I kept checking every 30 seconds or so.

    Data Processing and Visualization

    Processing the RRDs

    This command processes the RRD files into something that can be graphed by the GUI.

    sudo /opt/AS-Stats/bin/rrd-extractstats.pl /opt/AS-Stats/rrd /opt/AS-Stats/conf/knownlinks /opt/AS-Stats/asstats_day

    Running this manually gave the initial asstats_day file, which is important for the GUI later on.

    APNIC suggests running this every hour, however I opted for every 10 minutes. I placed the schedule on the root crontab.

    sudo crontab -e
    
    
    # Then I placed the schedule every 10 minutes
    #           */10 * * * * [command]
    
    
    */10 * * * * /opt/AS-Stats/bin/rrd-extractstats.pl /opt/AS-Stats/rrd /opt/AS-Stats/conf/knownlinks /opt/AS-Stats/asstats_day

    Installing the Web GUI

    I kept things as barebones as possible as this is really an internal tool. Simply put, I cloned nidebr/as-stats-gui into Apache’s root (located in /var/www/html).

    cd /var/www/html/
    
    
    sudo git clone https://github.com/nidebr/as-stats-gui
    
    
    # To be safe
    sudo rm -rf as-stats-gui/.git  
    
    
    sudo chown -R www-data:www-data as-stats-gui/

    I then edited as-stats-gui/config.inc

    # The important ones to edit are
    
    # $rrdpath - Per ASN graphs are generated from here (weekly, monthly, etc.), I think
    
    # $daystatsfile - Breaks the general 24 hour "home" page (but per-ASN view still works)
    
    # $knownlinksfile - Everything breaks without this
    
    
    $rrdpath = "/opt/AS-Stats/rrd";
    $daystatsfile = "/opt/AS-Stats/asstats_day";
    $knownlinksfile = "/opt/AS-Stats/conf/knownlinks";

    And that’s it, really.

    Accessing the IP address, plus the as-stats-gui path, gave pretty graphs. Now it took a while to have any data since this is a 24 hour view by default.

    http://your-ip-here/as-stats-gui/

    Clicking on one of the graphs opens up that ASN’s other timeframes (weekly, monthly, and yearly).

    Setting up for headless mode

    Running asstatd.pl in an always-open SSH console may prove to be inconvenient. For starters, the instance will die if the console is disconnected.

    Therefore I set it up as a service.

    I created a new file /etc/systemd/system/as-stats.service and placed our command earlier as a service:

    [Unit]
    Description=AS-Status Collector
    After=network.target
    StartLimitIntervalSec=0
    [Service]
    Type=simple
    Restart=always
    RestartSec=1
    User=root
    ExecStart=/usr/bin/perl /opt/AS-Stats/bin/asstatd.pl -P 0 -p 9000 -r /opt/AS-Stats/rrd -k /opt/AS-Stats/conf/knownlinks -m /opt/AS-Stats/bin/ip.json
    
    [Install]
    WantedBy=multi-user.target

    I then started the service

    sudo service as-stats start

    And set it up to run on boot

    sudo systemctl enable as-stats

    Then I checked that it’s actually running

    sudo systemctl --no-pager status as-stats
    
    
    # Expected output:
    alohaspark@as-stats-blog:~$ sudo systemctl --no-pager status as-stats
    ● as-stats.service - AS-Status Collector
         Loaded: loaded (/etc/systemd/system/as-stats.service; enabled; preset: enabled)
         Active: active (running) since Sat 2025-03-15 03:07:11 UTC; 4min 18s ago
       Main PID: 164524 (perl)
          Tasks: 1 (limit: 4609)
         Memory: 470.8M (peak: 701.8M)
            CPU: 1min 11.374s
         CGroup: /system.slice/as-stats.service
                 └─164524 /usr/bin/perl /opt/AS-Stats/bin/asstatd.pl -P 0 -p 9000 -r /opt/AS-Stats/rrd -k /opt/AS-Stats/conf/knownlinks -m /opt/AS-Stats/bin/ip.…
    
    Mar 15 03:07:11 as-stats-blog systemd[1]: Started as-stats.service - AS-Status Collector.
    alohaspark@as-stats-blog:~$

    The “enabled”, in bold, signifies that this will run on boot. It would say “disabled” otherwise.

    Disabled otherwise.

    Summary and Conclusion

    With as-stats now providing clear insights into our traffic flows, we are well positioned to optimize our upstreams, such as selecting a provider with a better path to our heaviest destinations.

    Or better, we can strategically decide where and who to peer with.

    As datacenter and transport costs continue to fall in the Philippines, it’s now practical to establish direct peering with content providers such as Facebook (Meta) and Google, significantly reducing transit costs by lowering usage.

    GLHF.

  • Hello world!

    Welcome to WordPress. This is your first post. Edit or delete it, then start writing!