Strange Cert. error with some NordVPN connections

I have a multiple connections with NordVPN and just saw that I had errors in the log about the certificate.
NordVPN-failCert.JPG
The certificate is running till 1 January 2036 so that could not be the problem.
Other connections to NordVPN are working fine and reconnect.

The only thing I can think of is that the server is having a different certificate than the local certificate store provides.

Any ideas how to test that?

I’ve seen this myself… Just switched to another server that is currently recommended.

Thanks and was already testing that and I have now chosen a new range. You can see the current servers and load of those servers on this JSON page.

nordvpn.com/api/server/stats

I have this stored as a script on my devices:

:put ([ / tool fetch http-header-field="User-Agent: Mozilla/4.0" "https://api.nordvpn.com/v1/servers/recommendations\?limit=3" output=user as-value ]->"data");

Then from a linux host:

 % ssh mikrotik / system script run nordvpn-recommendations | jq --raw-output '.[].hostname'
de861.nordvpn.com
de877.nordvpn.com
de856.nordvpn.com

Et voilà, three hosts to choose from.

Sadly there is no json parser in RouterOS… Mikrotik, please add one, I could use it for several things!
(I know there are scripts parsing json, but these are really huge.)

hmmm

This might filter out the needed domains and this a part of the script that could do that.

   :while ([:len $data]!=0) do={
      :if ([:pick $data 0 [:find $data "\n"]]~"^nl[0-9]{1,3}\\.nordvpn\\.com") do={
      :do {add list=$blacklist address=([:pick $data 0 [:find $data $delimiter]].$cidr) timeout=35w3d13:13:56} on-error={}
      }

You can replace “nl” by “de” if and this is taken from adding a address-list but the RegEX can also be used to filter out the domain names. It has to be adapted and I will have a look at it later today.

Script source: http://forum.mikrotik.com/t/address-lists-downloader-dshield-spamhaus-drop-edrop-etc/133640/65

Yes, spend gobs of money on code for useful stuff so people using questionable vpn sites can have easy lives… hint to MT - dont bother as there are much higher priorities to work on than this little distraction.

This will return the first active domain in the list:

:local data ([ / tool fetch url="https://api.nordvpn.com/v1/servers/recommendations\?limit=1" output=user as-value ]->"data"); :local position [:find $data nordvpn.com]; put [:pick $data ($position-6) ($position+11)];

And a working script writing the active domains to the log:

# Written by Shumkov
# Adapted by blacklister
# rewritten to list VPN servers
# version 20200703-01
}
 :do {
:local data ([ / tool fetch url="https://api.nordvpn.com/v1/servers/recommendations\?limit=20" output=user as-value ]->"data")
   :while ([:len $data]!=0) do={
      :if ([:pick $data 0 [:find $data "load"]]~"[a-z][a-z][0-9]{1,3}\\.nordvpn\\.com") do={
         :local position [:find $data nordvpn.com]
         :do {log info [:pick $data ($position-6) ($position+11)] } on-error={}
         :put [:pick $data ($position-6) ($position+11)]
       }
       :set $data [:pick $data ([:find $data "}}]}"]+1) [:len $data]]
     }
 } on-error={:log warning "Limit set to high. Data does not fit in the variable and update has failed."}
}

You can adapt it to write it to a array, put it directly in a list/dns or in a file.

Many thanks eworm for providing the download location of the file. I can extract up to 20 domains in one, this due to the length limitation in RouterOS for variables.

I can run it yet as a script and there must be something wrong which I don’t discover at this early hour. Copied and past in Terminal works fine.

If I use a put instead of a log:

 snip
{...  } on-error={:log warning "Limit set to high. Data does not fit in the variable and update has failed."}
nl388.nordvpn.com
nl636.nordvpn.com
nl628.nordvpn.com
nl682.nordvpn.com
nl742.nordvpn.com
nl698.nordvpn.com
nl699.nordvpn.com
nl691.nordvpn.com
nl679.nordvpn.com
nl597.nordvpn.com
nl599.nordvpn.com
nl674.nordvpn.com
nl592.nordvpn.com
nl626.nordvpn.com
nl720.nordvpn.com
nl693.nordvpn.com
nl605.nordvpn.com
nl726.nordvpn.com
nl686.nordvpn.com
nl623.nordvpn.com

Having thought more about it, it looks that there could be two approaches to this in reading a JSON file like this.

Your could go for only one value or just convert the JSON file up to 64KB in size to a array containing the values that is direct accessible by RouterOS.

The last one is think the best solution and the most flexible.

And I have shorted it and made it more flexible. You can now state the name of the field you want to be returned and it adapt the the length of that field. It is still basic but a good start to read from JSON files.

# Written by Shumkov
# Adapted by blacklister
# rewritten to list VPN servers
# 20200704-11
{
 :local recordname "hostname"
 :set $fieldname ($recordname."\":\"")
 :local data ([ / tool fetch url="https://api.nordvpn.com/v1/servers/recommendations\?limit=2" output=user as-value ]->"data")
   :while ([:len $data]!=0) do={
         :local position ([:find $data "$fieldname"]+[len $fieldname])
         :put [:pick $data $position (position + ([:find ( [:pick $data $position ($position+[len $data])]) "\"" ]))]
      :set $data [:pick $data ([:find $data "}}]}"]+5) [:len $data]]
    }
}

nl388.nordvpn.com
nl736.nordvpn.com

I am pleased with myself, this time. Scripting is not something I do often and Mikrotik gives us tools that can be used very flexible…if you persist. :wink:

And then you can extract extra information as IP address and the current load on the server. Notice that the “load” is not a string and then a comma is the end of the field.

{
 :local recordname "hostname"
 :local valname1 "ip"
 :local valname2 "load"
 :local data ([ / tool fetch url="https://api.nordvpn.com/v1/servers/recommendations\?limit=20" output=user as-value ]->"data")
   :while ([:len $data]!=0) do={
 :set $fieldname ($recordname."\":\"")
         :local position ([:find $data "$fieldname"]+[len $fieldname])
         :put ("$recordname: ".[:pick $data $position (position + ([:find ( [:pick $data $position ([find $data "}}]}"])]) "\"" ]))] )
 :set $fieldname ($valname1."\":\"")
         :local position ([:find $data "$fieldname"]+[len $fieldname])
         :put ("$valname1: ".[:pick $data $position (position + ([:find ( [:pick $data $position ([find $data "}}]}"])]) "\"" ]))] )
 :set $fieldname ($valname2."\":")
         :local position ([:find $data "$fieldname"]+[len $fieldname])
         :put ("$valname2: ".[:pick $data $position (position + ([:find ( [:pick $data $position ([find $data "}}]}"])]) "," ]))] )
      :set $data [:pick $data ([:find $data "}}]}"]+5) [:len $data]]
    }
}

hostname: nl388.nordvpn.com
ip: 190.2.149.20
load: 0
hostname: nl622.nordvpn.com
ip: 194.127.172.76
load: 5
hostname: nl693.nordvpn.com
ip: 213.232.87.103
load: 5

This can be made even more flexible by adding a extra while and you can even think about selecting the server with the lowest load.

And back to a singe field and using a Function so that it is easier to rewrite to also read fields can be read and displayed.

# Written by Shumkov
# Adapted by blacklister
# rewritten to list VPN servers
# 20200704-21
{
:local update do={
 :do {
 :local result [/tool fetch url=$url as-value output=user]; :if ($result->"downloaded" != "63") do={ :local data ($result->"data")
 :put "Reading and displaying from the JSON file, the values for the $valname field:"
   :while ([:len $data]!=0) do={
         :set $fieldname ($valname."\":\"")
         :local position ([:find $data "$fieldname"]+[len $fieldname])
         :put [:pick $data $position (position + ([:find ( [:pick $data $position ([find $data "}}]}"])]) $delimiter ]))]
     :set $data [:pick $data ([:find $data "}}]}"]+5) [:len $data]]
   } ;  :put "Read field $valname from file: $url"
   } 
 } on-error={:put "Failure: no information found or file is to big"}
}
$update url=$update url=("https://api.nordvpn.com/v1/servers/recommendations"."\?limit=20") valname="hostname" delimiter=("\"") 
}

This version reads the file multiple times but can now be instructed to treat the wanted field, being a string or a number.

{
:local update do={
 :do {
 :local result [/tool fetch url=$url as-value output=user]; :if ($result->"downloaded" != "63") do={ :local data ($result->"data")
 :put "Reading and displaying from the JSON file, the values for the $valname field:"
   :while ([:len $data]!=0) do={
         :set $fieldname ($valname."\":\"")
         :set $delimiter "\""
         if ($numval = "yes")  do={:set $fieldname ($valname."\":"); :set $delimiter ","}
         :local position ([:find $data "$fieldname"]+[len $fieldname])
         :put [:pick $data $position (position + ([:find ( [:pick $data $position ([find $data "}}]}"])]) $delimiter ]))]
     :set $data [:pick $data ([:find $data "}}]}"]+5) [:len $data]]
   } ;  :put "Read field $valname from file: $url"
   } 
 } on-error={:put "Failure: no information found or file is to big"}
}
$update url=$update url=("https://api.nordvpn.com/v1/servers/recommendations"."\?limit=2") valname="hostname"
$update url=$update url=("https://api.nordvpn.com/v1/servers/recommendations"."\?limit=2") valname="load" numval="yes"
$update url=$update url=("https://api.nordvpn.com/v1/servers/recommendations"."\?limit=2") valname="station"
}

If I may… the $update url= beginning of the last three lines is there twice (copy-paste went wrong?), but more important, there is a risk that the response will be different each time you read it. So as you read it into a variable anyway, you can just parse the data from the same response for each parameter instead of downloading it again.

You’re welcome. It more a bit of playtime for me doing this. And at the same time improve my scripting.

There can be a difference in matching the data due to time. My next step would be to loop through different requested fields. This will limits the returned values to only strings or numbers.
Putting $data in a global variable and move the read of the file to outside function would enable that.

A thing I am looking for a way to select to only return values if the the encrypting protocol is supported by that server. So filtering on Wireguard or OpenVPN.

Personally I would only be interested in the station/IP addresses to feed my NordVPN connections.

This version will download the dataset once and store it in a global (Environment) variable and erase it again when ready.

{
:local update do={
:global dataNordVPN
:local data $dataNordVPN
  :do {
  :put "Reading and displaying from the JSON file, the values for the $valname field:"
   :while ([:len $data]!=0) do={
         :set $fieldname ($valname."\":\"")
         :set $delimiter "\""
         if ($numval = "yes") do={:set $fieldname ($valname."\":"); :set $delimiter ","}
         :local position ([:find $data "$fieldname"]+[len $fieldname])
         :put [:pick $data $position (position + ([:find ( [:pick $data $position ([find $data "}}]}"])]) $delimiter ]))]
     :set $data [:pick $data ([:find $data "}}]}"]+5) [:len $data]]
   } ;  :put "Read field $valname from file: $url"
   } 
 }
:global dataNordVPN ([/ tool fetch http-header-field="User-Agent: Mozilla/4.0" "https://api.nordvpn.com/v1/servers/recommendations\?limit=3" output=user as-value ]->"data")
$update valname="hostname"
$update valname="load" numval="yes"
$update valname="station"
# erase and remove global variable $dataNordVPN
:set $dataNordVPN  
}

Unfortunately only a full json parser would make you bullet-proof in this case, because the order of the sub-items in the same branch is not guaranteed, plus the availability status of the individual technologies (IKEv2, OpenVPN, Wireguard) on the servers may not be the same, and you need the complete information (server IP, IKEv2 support in general, IKEv2 current availability).

So you have to split the complete answer into the branches representing the individual servers, and within each branch, find the branch of “technologies” which contains “identifier”:“ikev2”, and within “pivot” of that branch, check that the value of “status” is “online”.

So it all boils down to counting { and } :slight_smile:

This is a bit quick and dirty but it shows the the servers supporting Wireguard:

{
:local update do={
:global dataNordVPN
:local data $dataNordVPN
  :do {
  :put "Reading and displaying from the JSON file, the values for the $valname field:"
   :while ([:len $data]!=0) do={
         :set $fieldname ($valname."\":\"")
         :set $delimiter "\""
:if (([:find ([:pick $data 0 ([find $data "}}]}"])]) wireguard_udp ])) do={
         if ($numval = "yes") do={:set $fieldname ($valname."\":"); :set $delimiter ","}
         :local position ([:find $data "$fieldname"]+[len $fieldname])
         :put [:pick $data $position (position + ([:find ( [:pick $data $position ([find $data "}}]}"])]) $delimiter ]))]
}
     :set $data [:pick $data ([:find $data "}}]}"]+5) [:len $data]]
   } ;  :put "Read field $valname from file: $url"
   } 
 }
:global dataNordVPN ([/ tool fetch http-header-field="User-Agent: Mozilla/4.0" "https://api.nordvpn.com/v1/servers/recommendations\?limit=3" output=user as-value ]->"data")
$update valname="hostname"
$update valname="load" numval="yes"
$update valname="station"
# erase and remove global variable $dataset
:set $dataNordVPN  
}