Repository: jotyGill/openpyn-nordvpn (language: python)

Issue:
Several changes including fix for new API rate limiting
Hi @jotyGill ,

Hope you're doing well, I had some spare time and made several changes.
New Features
Now connect to servers in any country!
'-ls' to list all countries, '-d' shows openvpn (udp-tcp) support
Failed to set DNS configuration: Could not activate remote peer.
This is a fresh install on fedora 30.

`openpyn uk
According to NordVPN, Least Busy 10 Servers in UK With 'Load' Less Than 70 Which Support OPENVPN-UDP Are: [['uk1321', 5], ['uk1081', 6], ['uk1074', 7], ['uk1172', 7], ['uk1212', 7], ['uk1235', 7], ['uk1322', 7], ['uk1359', 7], ['uk1404', 7], ['uk1466', 7]]

Pinging Server uk1321 min/avg/max/mdev = [54, 88, 155, 47] 

Pinging Server uk1081 min/avg/max/mdev = [56, 58, 59, 0] 

Pinging Server uk1074 min/avg/max/mdev = [57, 59, 64, 3] 

Pinging Server uk1172 min/avg/max/mdev = [54, 62, 66, 5] 

Pinging Server uk1212 min/avg/max/mdev = [53, 55, 57, 1] 

Pinging Server uk1235 min/avg/max/mdev = [56, 58, 60, 1] 

Pinging Server uk1322 min/avg/max/mdev = [56, 61, 64, 3] 

Pinging Server uk1359 min/avg/max/mdev = [51, 67, 91, 17] 

Pinging Server uk1404 min/avg/max/mdev = [58, 63, 72, 6] 

Pinging Server uk1466 min/avg/max/mdev = [54, 73, 99, 19] 

Top 10 Servers with Best Ping Are: ['uk1212', 'uk1081', 'uk1235', 'uk1074', 'uk1322', 'uk1172', 'uk1404', 'uk1359', 'uk1466', 'uk1321']

Out of the Best Available Servers, Chose uk1212

2019-08-10 20:17:05 [SUCCESS] CONNECTING TO SERVER uk1212 ON PORT udp
2019-08-10 20:17:05 [SUCCESS] Your OS 'linux' Does have '/sbin/resolvconf', using it to update DNS Resolver Entries
Sat Aug 10 20:17:05 2019 WARNING: Using --management on a TCP port WITHOUT passwords is STRONGLY discouraged and considered insecure
Sat Aug 10 20:17:05 2019 OpenVPN 2.4.7 x86_64-redhat-linux-gnu [SSL (OpenSSL)] [LZO] [LZ4] [EPOLL] [PKCS11] [MH/PKTINFO] [AEAD] built on Feb 20 2019
Sat Aug 10 20:17:05 2019 library versions: OpenSSL 1.1.1c FIPS  28 May 2019, LZO 2.08
Sat Aug 10 20:17:05 2019 MANAGEMENT: TCP Socket listening on [AF_INET]127.0.0.1:7015
Sat Aug 10 20:17:05 2019 WARNING: --ping should normally be used with --ping-restart or --ping-exit
Sat Aug 10 20:17:05 2019 NOTE: the current --script-security setting may allow this configuration to call user-defined scripts
Sat Aug 10 20:17:05 2019 Outgoing Control Channel Authentication: Using 512 bit message hash 'SHA512' for HMAC authentication
Sat Aug 10 20:17:05 2019 Incoming Control Channel Authentication: Using 512 bit message hash 'SHA512' for HMAC authentication
Sat Aug 10 20:17:05 2019 TCP/UDP: Preserving recently used remote address: [AF_INET]217.146.82.216:1194
Sat Aug 10 20:17:05 2019 Socket Buffers: R=[212992->212992] S=[212992->212992]
Sat Aug 10 20:17:05 2019 UDP link local: (not bound)
Sat Aug 10 20:17:05 2019 UDP link remote: [AF_INET]217.146.82.216:1194
Sat Aug 10 20:17:05 2019 TLS: Initial packet from [AF_INET]217.146.82.216:1194, sid=67483799 bcc1fa3b
Sat Aug 10 20:17:05 2019 WARNING: this configuration may cache passwords in memory -- use the auth-nocache option to prevent this
Sat Aug 10 20:17:05 2019 VERIFY OK: depth=2, C=PA, O=NordVPN, CN=NordVPN Root CA
Sat Aug 10 20:17:05 2019 VERIFY OK: depth=1, C=PA, O=NordVPN, CN=NordVPN CA3
Sat Aug 10 20:17:05 2019 VERIFY KU OK
Sat Aug 10 20:17:05 2019 Validating certificate extended key usage
Sat Aug 10 20:17:05 2019 ++ Certificate has EKU (str) TLS Web Server Authentication, expects TLS Web Server Authentication
Sat Aug 10 20:17:05 2019 VERIFY EKU OK
Sat Aug 10 20:17:05 2019 VERIFY OK: depth=0, CN=uk1212.nordvpn.com
Sat Aug 10 20:17:06 2019 Control Channel: TLSv1.2, cipher TLSv1.2 ECDHE-RSA-AES256-GCM-SHA384, 4096 bit RSA
Sat Aug 10 20:17:06 2019 [uk1212.nordvpn.com] Peer Connection Initiated with [AF_INET]217.146.82.216:1194
Sat Aug 10 20:17:06 2019 MANAGEMENT: Client connected from [AF_INET]127.0.0.1:7015
Sat Aug 10 20:17:07 2019 SENT CONTROL [uk1212.nordvpn.com]: 'PUSH_REQUEST' (status=1)
Sat Aug 10 20:17:07 2019 PUSH: Received control message: 'PUSH_REPLY,redirect-gateway def1,dhcp-option DNS 103.86.96.100,dhcp-option DNS 103.86.99.100,sndbuf 524288,rcvbuf 524288,explicit-exit-notify,comp-lzo no,route-gateway 10.8.1.1,topology subnet,ping 60,ping-restart 180,ifconfig 10.8.1.5 255.255.255.0,peer-id 3,cipher AES-256-GCM'
Sat Aug 10 20:17:07 2019 OPTIONS IMPORT: timers and/or timeouts modified
Sat Aug 10 20:17:07 2019 OPTIONS IMPORT: explicit notify parm(s) modified
Sat Aug 10 20:17:07 2019 OPTIONS IMPORT: compression parms modified
Sat Aug 10 20:17:07 2019 OPTIONS IMPORT: --sndbuf/--rcvbuf options modified
Sat Aug 10 20:17:07 2019 Socket Buffers: R=[212992->425984] S=[212992->425984]
Sat Aug 10 20:17:07 2019 OPTIONS IMPORT: --ifconfig/up options modified
Sat Aug 10 20:17:07 2019 OPTIONS IMPORT: route options modified
Sat Aug 10 20:17:07 2019 OPTIONS IMPORT: route-related options modified
Sat Aug 10 20:17:07 2019 OPTIONS IMPORT: --ip-win32 and/or --dhcp-option options modified
Sat Aug 10 20:17:07 2019 OPTIONS IMPORT: peer-id set
Sat Aug 10 20:17:07 2019 OPTIONS IMPORT: adjusting link_mtu to 1657
Sat Aug 10 20:17:07 2019 OPTIONS IMPORT: data channel crypto options modified
Sat Aug 10 20:17:07 2019 Data Channel: using negotiated cipher 'AES-256-GCM'
Sat Aug 10 20:17:07 2019 Outgoing Data Channel: Cipher 'AES-256-GCM' initialized with 256 bit key
Sat Aug 10 20:17:07 2019 Incoming Data Channel: Cipher 'AES-256-GCM' initialized with 256 bit key
Sat Aug 10 20:17:07 2019 ROUTE_GATEWAY 192.168.43.190/255.255.255.0 IFACE=wlp58s0 HWADDR=9c:b6:d0:f3:39:77
Sat Aug 10 20:17:07 2019 TUN/TAP device tun0 opened
Sat Aug 10 20:17:07 2019 TUN/TAP TX queue length set to 100
Sat Aug 10 20:17:07 2019 /sbin/ip link set dev tun0 up mtu 1500
Sat Aug 10 20:17:07 2019 /sbin/ip addr add dev tun0 10.8.1.5/24 broadcast 10.8.1.255
Sat Aug 10 20:17:07 2019 /usr/local/lib/python3.7/site-packages/openpyn/scripts/update-resolv-conf.sh tun0 1500 1585 10.8.1.5 255.255.255.0 init
dhcp-option DNS 103.86.99.100
dhcp-option DNS 103.86.96.100
dhcp-option DNS 208.67.222.220
Failed to set DNS configuration: Could not activate remote peer.
Sat Aug 10 20:17:07 2019 MANAGEMENT: Client disconnected
Sat Aug 10 20:17:07 2019 WARNING: Failed running command (--up/--down): external program exited with error status: 1
Sat Aug 10 20:17:07 2019 Exiting due to fatal error
`
any ideas?
no killswitch during start and restart
Thanks for this great tool. I just installed it and did some tests. The killswitch works when no vpn connection is established. But during start or restart there is no blocking and all traffic goes through standard connection. Is there any possibillity to solve this issue?

Thanks a lot!
no killswitch during start and restart
Thanks for this great tool. I just installed it and did some tests. The killswitch works when no vpn connection is established. But during start or restart there is no blocking and all traffic goes through standard connection. Is there any possibillity to solve this issue?

Thanks a lot!
parrallel pings
### Parrallel pings

When I am travelling I often find that I am connecting repeatedly to different wifi's or more likely after putting my laptop into suspend.

Because of this I find waiting for the pings to 10 servers takes slightly longer than feels right to my flow.

I would like to implement sending the pings to the servers in parrallel with each other.

#### implementation design decisions 
* change: `def ping_servers(better_servers_list: List, pings: str, stats: bool) -> List`:
  * [x] - Implement sending pings to servers in parrallel
  * [x] - ~~default to send pings in parallel add option to send in series~~
    * series pings removed completely

#### questions / discussion
* are there any reasons not to send the pings in parrallell?
  * perhaps may use more ram than is suitable for openpyn running on router hardware
* is there any case where a "fallback" to sending pings in series is required?

#### state of implementation
* [x] - ~~ implement parallel pings with tests ~~
parrallel pings
### Parrallel pings

When I am travelling I often find that I am connecting repeatedly to different wifi's or more likely after putting my laptop into suspend.

Because of this I find waiting for the pings to 10 servers takes slightly longer than feels right to my flow.

I would like to implement sending the pings to the servers in parrallel with each other.

#### implementation design decisions 
* change: `def ping_servers(better_servers_list: List, pings: str, stats: bool) -> List`:
  * [x] - Implement sending pings to servers in parrallel
  * [x] - ~~default to send pings in parallel add option to send in series~~
    * series pings removed completely

#### questions / discussion
* are there any reasons not to send the pings in parrallell?
  * perhaps may use more ram than is suitable for openpyn running on router hardware
* is there any case where a "fallback" to sending pings in series is required?

#### state of implementation
* [x] - ~~ implement parallel pings with tests ~~
Not working on macos
Hi,
I tried to homebrew openpyn on my wife's mac. The brewing works (but I had to manually install pip3 - it doesn't install automatically with python3)
When I try to execute openpyn uk, however, this is what I get:

> caterina@caterinas-imac ~> sudo openpyn uk
> 
> According to NordVPN, Least Busy 10 Servers in UK With 'Load' Less Than 70 Which Support OPENVPN-UDP Are: [['uk1175', 5], ['uk1483', 5], ['uk805', 6], ['uk829', 6], ['uk1009', 6], ['uk1037', 6], ['uk1054', 6], ['uk1213', 6], ['uk1217', 6], ['uk1292', 6]]
> 
> Pinging Server uk1175 min/avg/max/mdev = [20, 21, 21, 0] 
> 
> Pinging Server uk1483 min/avg/max/mdev = [20, 21, 22, 0] 
> 
> Pinging Server uk805 min/avg/max/mdev = [22, 24, 26, 1] 
> 
> Pinging Server uk829 min/avg/max/mdev = [20, 21, 22, 0] 
> 
> Pinging Server uk1009 min/avg/max/mdev = [20, 21, 21, 0] 
> 
> Pinging Server uk1037 min/avg/max/mdev = [21, 22, 23, 0] 
> 
> Pinging Server uk1054 min/avg/max/mdev = [20, 21, 22, 0] 
> 
> Pinging Server uk1213 min/avg/max/mdev = [20, 22, 23, 1] 
> 
> Pinging Server uk1217 min/avg/max/mdev = [21, 22, 22, 0] 
> 
> Pinging Server uk1292 min/avg/max/mdev = [20, 20, 21, 0] 
> 
> Top 10 Servers with Best Ping Are: ['uk1292', 'uk1175', 'uk1483', 'uk829', 'uk1009', 'uk1054', 'uk1037', 'uk1217', 'uk1213', 'uk805']
> 
> Out of the Best Available Servers, Chose uk1292
> 
> 2019-09-24 11:42:32 [SUCCESS] CONNECTING TO SERVER uk1292 ON PORT udp
> 2019-09-24 11:42:32 [WARNING] Not modifying '/etc/resolv.conf', DNS traffic likely won't go through the encrypted tunnel
> sudo: openvpn: command not found
> caterina@caterinas-imac ~> brew install openvpn
> Warning: openvpn 2.4.7_1 is already installed and up-to-date
> To reinstall 2.4.7_1, run `brew reinstall openvpn`
> caterina@caterinas-imac ~> brew link openvpn
> Warning: Already linked: /usr/local/Cellar/openvpn/2.4.7_1
> To relink: brew unlink openvpn && brew link openvpn
> 

As you can see, I also checked whether openvpn was both installed and linked. It was, so I'm not to sure what causes the error.
Auto manage ip routes to allow ssh or (any other service) to external IP; VPS support
Ubuntu 18.04.3 LTS
Python 3.6.9
openpyn 2.7.5

Hi, awesome program. I'm just having trouble running it on my Ubuntu 18.04 VPS.

I've installed it fine using "sudo python3 -m pip install --upgrade openpyn", but when I run the app, it gets to a point and just hangs. The VPS becomes unresponsive and after a few minutes disconnects me, and I have to reboot manually.

I've attached the terminal output when I run "openpyn us"

Any help would be appreciated.

Thanks.



[openpyn ubuntu18_04 error log 20200203.txt](https://github.com/jotyGill/openpyn-nordvpn/files/4145737/openpyn.ubuntu18_04.error.log.20200203.txt)
Ignoring redirect-gateway
Is it possible to add support for this to prevent the redirection of all internet traffic over VPN incase if custom routes are used? 
I tried passing it using -o OPENVPN_OPTIONS but it did not seem to work.


Discussion / hints:

Fix patch:
diff --git a/.gitignore b/.gitignore
index 2214c27..5c4de37 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,6 +4,7 @@ build
 dist
 deb_dist
 .*
+!.pylintrc
 .python-version
 openpyn/test.py
 openpyn/credentials
diff --git a/.pylintrc b/.pylintrc
new file mode 100644
index 0000000..c7b9320
--- /dev/null
+++ b/.pylintrc
@@ -0,0 +1,2 @@
+[MESSAGES CONTROL]
+disable = C0103,C0114,C0115,C0116,C0301,R0902,R0911,R0912,R0913,R0914,R0915,R0916
diff --git a/README.md b/README.md
index b9582f5..d33595c 100644
--- a/README.md
+++ b/README.md
@@ -1,283 +1,410 @@
 # openpyn
+
 <p align="center">
 <a href="https://pypi.python.org/pypi/openpyn"><img alt="Downloads" src="https://img.shields.io/pypi/v/openpyn.svg"></a>
 <a href="https://pepy.tech/project/openpyn"><img alt="Downloads" src="https://pepy.tech/badge/openpyn"></a> </p>
-A python3 script (systemd service as well) to manage openvpn connections. Created to easily connect to and switch between, OpenVPN servers hosted by NordVPN. Quickly Connect to the least busy servers with lowest latency from you (using current data from Nordvpn's API). Find servers in a specific country or even a city. It Tunnels DNS traffic through the VPN which normally (when using OpenVPN) goes through your ISP's DNS (unencrypted) and compromises Privacy!
+A python3 script (systemd service as well) to manage OpenVPN connections. Created to easily connect to and switch between, OpenVPN servers hosted by NordVPN. Quickly Connect to the least busy servers with lowest latency from you (using current data from NordVPN’s API). Find servers in a specific country or even a city. It Tunnels DNS traffic through the VPN which normally (when using OpenVPN) goes through your ISP’s DNS (unencrypted) and compromises Privacy!
 
 ## Features
-* Automatically connect to least busy, low latency servers in a given country.
-* Systemd integration, easy to check VPN status, autostart at startup.
-* Find and connect to servers in a specific city or state.
-* Uses NordVPN's DNS servers and tunnels DNS queries through the VPN Tunnel.
-* Use Iptables rules to prevent IP leakage if tunnel breaks (Experimental), ie KILL SWITCH.
-* Quickly Connect to any specific server. i.e au10 or us20.
-* Downloads and Updates (modifications) the latest config files from NordVPN.
-* Option to run the script in background (as a systemd service).
-* Options to finetune server selection based on "Server Load" or "Ping Latency".
-* Auto excludes the servers if a ping to them fails or some packets drops when pinging \
-or if they don't support OpenVPN \ (TCP or UDP depending upon which one you are trying to use).
-* Finds and displays nord vpn servers (with extra info) in a given country.
-* Now list and connect to servers with "Netflix" --netflix, "Peer To Peer" --p2p, "Dedicated IP" --dedicated, \
-"Tor Over VPN" --tor, "Double VPN" --double, "Anti DDos" --anti-ddos support.
-* Desktop notification are shown when VPN connects and disconnects. (needs to run without sudo)
-* Auto retry if [soft,auth-failure] received, auto failover to next best server if connection dies.
-* NVRAM write support for Asuswrt-merlin
-* Pass through openvpn options, e.g. openpyn uk -o '--status /var/log/status.log --log /var/log/log.log'
-* Logs stored in '/var/log/openpyn/' for information and troubleshooting.
-* Temporarily disable ipv6 to prevent leakage (when using -f).
+
+-   Automatically connect to least busy, low latency servers in a given country.
+-   Systemd integration, easy to check VPN status, autostart at startup.
+-   Find and connect to servers in a specific city or state.
+-   Uses NordVPN’s DNS servers and tunnels DNS queries through the VPN Tunnel.
+-   Use Iptables rules to prevent IP leakage if tunnel breaks (Experimental), ie KILL SWITCH.
+-   Quickly Connect to any specific server. i.e au10 or us20.
+-   Downloads and Updates (modifications) the latest config files from NordVPN.
+-   Option to run the script in background (as a systemd service).
+-   Options to finetune server selection based on "Server Load" or "Ping Latency".
+-   Auto excludes the servers if a ping to them fails or some packets drops when pinging
+    or if they don’t support OpenVPN (TCP or UDP depending upon which one you are trying to use).
+-   Finds and displays NordVPN servers (with extra info) in a given country.
+-   Now list and connect to servers with "Netflix" --netflix, "Peer To Peer" --p2p, "Dedicated IP" --dedicated,
+    "Tor Over VPN" --tor, "Double VPN" --double, "Anti DDos" --anti-ddos support.
+-   Desktop notification are shown when VPN connects and disconnects. (needs to run without sudo)
+-   Auto retry if \[soft,auth-failure\] received, auto failover to next best server if connection dies.
+-   NVRAM support for Asuswrt-merlin
+-   Pass through OpenVPN options, e.g. openpyn uk -o '--status /var/log/status.log --log /var/log/log.log'
+-   Logs are stored in '/var/log/openpyn/' for information and troubleshooting.
+-   Temporarily disable IPv6 to prevent leakage (when using -f).
 
 ## Demo
+
 ![connection](https://user-images.githubusercontent.com/8462091/29347697-0798a52a-823e-11e7-818f-4dad1582e173.gif)
 
 ## Instructions
-1. Install dependencies if they are not already present.
-``` bash
-# common dependencies
-sudo apt install openvpn unzip wget python3-setuptools python3-pip
-```
-2. The following python dependencies are needed and will be installed when using pip.
-``` bash
-requests colorama coloredlogs verboselogs
-```
+
+1.  Install dependencies if they are not already present.
+
+    ```bash
+    # common dependencies
+    sudo apt install openvpn python3-setuptools python3-pip
+    ```
+
+2.  The following Python dependencies are needed and will be installed when using pip.
+
+    ```bash
+    requests colorama coloredlogs verboselogs tqdm jsonschema
+    ```
+
 ### Installation Methods
-1. Install openpyn with pip3 (Python=>3.5)
-**Recommended method to get the latest version and receive frequent updates.**
-``` bash
-sudo python3 -m pip install --upgrade openpyn
-```
-2. Alternatively clone and install.
-``` bash
-git clone https://github.com/jotyGill/openpyn-nordvpn.git
-cd openpyn-nordvpn && sudo python3 -m pip install --upgrade .
-```
-For the latest/ in development features, try the 'test' branch instead
-```bash
- git clone --branch test https://github.com/jotyGill/openpyn-nordvpn.git
- cd openpyn-nordvpn && sudo python3 -m pip install --upgrade -e .
-```
-3. For macOS with Python=>3.5 (credit: [1951FDG](https://github.com/1951FDG))
-``` bash
-# common dependencies
-xcode-select --install
-/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
-echo 'export PATH="/usr/local/sbin:$PATH"' >> ~/.bash_profile
-brew install python3 wget openvpn
-sudo brew services start openvpn
-```
-``` bash
-git clone https://github.com/jotyGill/openpyn-nordvpn.git
-cd openpyn-nordvpn
-git pull
-sudo pip3 install --upgrade .
-```
-4. On Asuswrt-merlin, install [Entware-ng-3x](https://gist.github.com/1951FDG/3cada1211df8a59a95a8a71db6310299#file-asuswrt-merlin-md) (credit: [1951FDG](https://github.com/1951FDG))
-``` bash
-# common dependencies
-opkg install git git-http iputils-ping procps-ng-pgrep python3 python3-pip sudo unzip wget
 
-```
-``` bash
-cd /tmp/share/
-git clone https://github.com/jotyGill/openpyn-nordvpn.git
-cd openpyn-nordvpn/
-git pull
-pip3 install --upgrade setuptools
-pip3 install --upgrade .
-```
+1.  Install openpyn with pip3 (Python=>3.5)
+    **Recommended method to get the latest version and receive frequent updates.**
+    Do not install with --user switch, as OpenVPN needs to run as sudo and sudo won’t be able to locate openpyn.
+
+    ```bash
+    sudo python3 -m pip install --upgrade openpyn
+    ```
+
+2.  Alternatively clone and install.
+
+    ```bash
+    git clone https://github.com/jotyGill/openpyn-nordvpn.git
+    cd openpyn-nordvpn/
+    sudo python3 -m pip install --upgrade .
+    ```
+
+    For the latest in development features, try the 'test' branch instead
+
+    ```bash
+    git clone --branch test https://github.com/jotyGill/openpyn-nordvpn.git
+    cd openpyn-nordvpn/
+    sudo python3 -m pip install --upgrade .
+    ```
+
+3.  On macOS (credit: [1951FDG](https://github.com/1951FDG))
+
+    ```bash
+    # install Xcode command line tools
+    xcode-select --install
+    ```
+
+    ```bash
+    # install Homebrew
+    curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh
+    echo 'export PATH="/usr/local/sbin:$PATH"' >> ~/.bash_profile
+    ```
+
+    ```bash
+    # common dependencies
+    brew install python3 openvpn
+    sudo brew services start openvpn
+    ```
+
+    ```bash
+    git clone --branch test https://github.com/jotyGill/openpyn-nordvpn.git
+    cd openpyn-nordvpn/
+    sudo python3 -m pip install --upgrade pip
+    sudo python3 -m pip install --upgrade .
+    ```
+
+4.  On Asuswrt-merlin, standard installation [Entware](https://gist.github.com/1951FDG/3cada1211df8a59a95a8a71db6310299#file-asuswrt-merlin-md) (credit: [1951FDG](https://github.com/1951FDG))
+
+    Steps below **may** also work for OpenWrt
+
+    ```bash
+    # common dependencies
+    opkg install git git-http iputils-ping procps-ng-pgrep python3 python3-pip sudo unzip
+    ```
+
+    ```bash
+    # allow admin user to run sudo
+    sed -i 's~root ALL=(ALL) ALL~admin ALL=(ALL) ALL~g' /opt/etc/sudoers
+    ```
+
+    ```bash
+    cd /tmp/share/
+    git clone --branch test https://github.com/jotyGill/openpyn-nordvpn.git
+    cd openpyn-nordvpn/
+    python3 -m pip install --upgrade pip
+    python3 -m pip install --upgrade .
+    ```
+
+    Steps below **not** required if only using --nvram option
+
+    ```bash
+    # steps need to be done once after every device reboot
+    modprobe tun
+    iptables -A FORWARD -i eth0 -o tun0 -s 192.168.1.0/24 -m conntrack --ctstate NEW -j ACCEPT
+    iptables -A FORWARD -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
+    iptables -A POSTROUTING -t nat -o tun0 -j MASQUERADE
+    ```
+
+    Steps below **may** be required if **not** using --nvram option
+
+    ```bash
+    # change the DNS, either in LAN or WAN settings
+    **DNS Server1** 103.86.96.100
+    **DNS Server2** 103.86.99.100
+    ```
 
 ## Setup
-Initialise the script with "--init" (store credentials, install Systemd service, update/install vpn config files)
-``` bash
+
+Initialise the script with "--init" (store credentials, install Systemd service, update/install VPN config files)
+
+```bash
 sudo openpyn --init
 ```
+
 Note: if you get ' openpyn: command not found' when using sudo on Fedora, create a symbolic link.
 `sudo ln -s /usr/local/bin/openpyn /bin/openpyn`
 
-That's it, run the script! when done with it, press "Ctr + C" to exit.
+That’s it, run the script! when done with it, press "Ctr + C" to exit.
 
 ## Basic Usage
-* At minimum, you only need to specify the country-code, default port is UDP-1194, If you want to use
-TCP-443 instead, use "--tcp" switch.
-``` bash
+
+-   At minimum, you only need to specify the country-code, default port is UDP-1194, If you want to use
+    TCP-443 instead, use "--tcp" switch.
+
+```bash
 openpyn us
 ```
-* Now, you can also specify a city or state, useful when companies (like Google) lock your
-account if you try to login from an IP that resides in a different physical location.
-``` bash
+
+-   Now, you can also specify a city or state, useful when companies (like Google) lock your
+    account if you try to login from an IP that resides in a different physical location.
+
+```bash
 openpyn us -a ny
 openpyn us --area "new york"
 ```
-* To enforce Firewall rules to prevent dns leakage, also from ip leakage if tunnel breaks. i.e KILL SWITCH
-also temporarily disables ipv6 by running "sudo sysctl -w net.ipv6.conf.all.disable_ipv6=1"
-``` bash
-openpyn us -f # Experimental!, Warning, clears IPtables rules!
+
+-   To enforce firewall rules to prevent DNS leakage, also from IP leakage if tunnel breaks. i.e KILL SWITCH
+    also temporarily disables IPv6 by running "sudo sysctl -w net.ipv6.conf.all.disable_ipv6=1"
+
+```bash
+openpyn us -f # Experimental!, Warning, clears iptables rules!
               # (changes are non persistent, simply reboot if having networking issues)
 ```
-* When using "-f", To allow custom ports (from internal ip ranges, i.e 192.168 or 10.) through the firewall.
-``` bash
-openpyn us -f --allow 22 80 443  #only accessible from local network
+
+-   When using "-f", To allow custom ports (from internal IP ranges, i.e 192.168 or 10.) through the firewall.
+
+```bash
+openpyn us -f --allow 22 80 443 # only accessible from local network
 ```
-* To quickly connect to a specific server.
-``` bash
+
+-   To allow ports from other ranges use the `--allow-config` or `--allow-config-json` options. More details can be found [here](./docs/allowed-ports-config.md)
+
+-   To quickly connect to a specific server.
+
+```bash
 openpyn -s au10
 ```
-* To list all the Countries and their Country Codes where NordVPN hosts servers.
-``` bash
+
+-   To list all the Countries and their Country Codes where NordVPN hosts servers.
+
+```bash
 openpyn -l
 ```
-* To find detailed information about the available servers in a given country.
-``` bash
+
+-   To find detailed information about the available servers in a given country.
+
+```bash
 openpyn -l uk
 ```
-* To find servers with features like "peer-to-peer", "netflix", "tor over vpn",
-  "double vpn" in all countries or a given country.
-``` bash
+
+-   To find servers with features like "peer-to-peer", "netflix", "tor over vpn",
+    "double vpn" in all countries or a given country.
+
+```bash
 openpyn -l uk --p2p
 openpyn --list uk --dedicated
-openpyn -l --tor  # tor over vpn in all countries
+openpyn -l --tor # Tor over VPN in all countries
 ```
-* To find the least loaded 10 NordVPN servers in US that support "peer-to-peer",
-sort them by the lowest latency from you, connect to the best one, if connection fails
-try the next one and so on.
-``` bash
+
+-   To find the least loaded 10 NordVPN servers in US that support "peer-to-peer",
+    sort them by the lowest latency from you, connect to the best one, if connection fails
+    try the next one and so on.
+
+```bash
 openpyn us -t 10 --p2p
 ```
-* To update and run the systemd openpyn.service, use "-d" or "--daemon"
-``` bash
+
+-   To update and run the systemd openpyn.service, use "-d" or "--daemon"
+
+```bash
 sudo openpyn us -d
 ```
-* To check the status of the systemd openpyn.service.
-``` bash
+
+-   To check the status of the systemd openpyn.service.
+
+```bash
 systemctl status openpyn
 ```
-* To kill a running openvpn connection.
-``` bash
+
+-   To kill a running OpenVPN connection.
+
+```bash
 sudo openpyn -k
 ```
-* To Flush the iptables and kill any running openvpn connections.
-``` bash
-sudo openpyn -x   #optionally --allow 22 if using as ssh server
+
+-   To flush the iptables and kill any running OpenVPN connections.
+
+```bash
+sudo openpyn -x # optionally --allow 22 if using as SSH server
 ```
-* To Download/Update the latest vpn config files from NordVPN by:
-``` bash
+
+-   To download/update the latest VPN config files from NordVPN.
+
+```bash
 openpyn --update
 ```
 
-* To quickly save best NordVPN server in US to NVRAM for "OpenVPN Client 5"
-(ASUSWRT-Merlin):
-``` bash
+-   To quickly connect to United States "OpenVPN Client 5" (Asuswrt-Merlin).
+
+```bash
 openpyn us --nvram 5
 ```
 
+-   To kill "OpenVPN Client 5" (Asuswrt-Merlin).
+
+```bash
+openpyn -k --nvram 5
+```
+
 ## Usage Options
-``` bash
-usage: openpyn.py [-h] [-v] [-s SERVER] [-u] [-c COUNTRY_CODE] [-a AREA] [-d]
-                  [-m MAX_LOAD] [-t TOP_SERVERS] [-p PINGS]
-                  [-k] [-x] [--update] [-f]
-                  [-l [LIST_SERVERS]] [--p2p] [--dedicated] [--tor] [--double] [--anti-ddos] [--test]]
-                  [country]
-
-A python3 script to easily connect to and switch between, OpenVPN servers
-hosted by NordVPN. Quickly Connect to the least busy servers (using current
-data from Nordvpn website) with lowest latency from you. Tunnels DNS traffic
+
+```bash
+usage: openpyn [-h] [-v] [--init] [-d] [-k] [-x] [--update] [--skip-dns-patch]
+               [--silent] [--test] [-n NVRAM] [--no-redirect-gateway]
+               [-o OPENVPN_OPTIONS] [-s SERVER] [-c COUNTRY_CODE] [--tcp]
+               [-a AREA] [-m MAX_LOAD] [-t TOP_SERVERS] [--p2p] [--dedicated]
+               [--tor] [--double] [--anti-ddos] [--netflix]
+               [-l [LIST_SERVERS]] [--status] [--stats] [-f] [-r]
+               [--allow-locally]
+               [--allow INTERNALLY_ALLOWED [INTERNALLY_ALLOWED ...]]
+               [--allow-config INTERNALLY_ALLOWED_CONFIG]
+               [--allow-config-json INTERNALLY_ALLOWED_CONFIG_JSON]
+               [country]
+
+A python3 script/systemd service (GPLv3+) to easily connect to and switch
+between, OpenVPN servers hosted by NordVPN. Quickly Connect to the least busy
+servers (using current data from NordVPN website) with lowest latency from
+you. Find NordVPN servers in a given country or city. Tunnels DNS traffic
 through the VPN which normally (when using OpenVPN with NordVPN) goes through
-your ISP''s DNS (still unencrypted, even if you use a thirdparty) and
-completely compromises Privacy!
+your ISP’s DNS (still unencrypted, even if you use a third-party DNS servers)
 
 positional arguments:
-  country               Country Code can also be specified without "-c," i.e
+  country               Country code can also be specified without "-c,"i.e.
                         "openpyn au"
 
 optional arguments:
   -h, --help            show this help message and exit
+  -v, --version         show program’s version number and exit
+  --init                Initialise, store/change credentials, download/update
+                        VPN config files, needs root "sudo" access.
+  -d, --daemon          Update and start systemd service openpyn.service,
+                        running it as a background process, to check status
+                        "systemctl status openpyn"
+  -k, --kill            Kill any running OpenVPN process, very useful to kill
+                        openpyn process running in background with "-d" switch
+  -x, --kill-flush      Kill any running OpenVPN process, and flush iptables
+  --update              Fetch the latest config files from NordVPN’s site
+  --skip-dns-patch      Skips DNS patching, leaves /etc/resolv.conf untouched.
+                        (Not recommended)
+  --silent              Do not try to send notifications. Use if "libnotify"
+                        or "gi" are not available. Automatically used in
+                        systemd service file
+  --test                Simulation only, do not actually connect to the VPN
+                        server
+  -n NVRAM, --nvram NVRAM
+                        Specify client to save configuration to NVRAM
+                        (Asuswrt-Merlin)
 
-  -v, --version         show program''s version number and exit
+OpenVPN Options:
+  Configurable Options Being Passed Downed To OpenVPN
 
-  --init                Initialise, store/change credentials, download/update
-                        vpn config files, needs root "sudo" access.
+  --no-redirect-gateway
+                        Don’t set --redirect-gateway
+  -o OPENVPN_OPTIONS, --openvpn-options OPENVPN_OPTIONS
+                        Pass through OpenVPN options, e.g. openpyn uk -o '--
+                        status /var/log/status.log --log /var/log/log.log'
+
+Connect Options:
+  Connect To A Specific Server Or Any In A Country; TCP or UDP
 
   -s SERVER, --server SERVER
                         server name, i.e. ca64 or au10
-
-  --tcp                 use port TCP-443 instead of the default UDP-1194
-
   -c COUNTRY_CODE, --country-code COUNTRY_CODE
-                        Specify Country Code with 2 letters, i.e au,
-
-  -a AREA, --area AREA  Specify area: city name or state e.g "openpyn au -a victoria"
-                        or "openpyn au -a 'sydney'"
+                        Specify country code with 2 letters, i.e. au
+  --tcp                 use port TCP-443 instead of the default UDP-1194
 
-  -d, --daemon          Update and start Systemd service openpyn.service,
-                        running it as a background process, to check status
-                        "systemctl status openpyn",
+Filter Options:
+  Find Specific Types Of Servers
 
+  -a AREA, --area AREA  Specify area, city name or state e.g "openpyn au -a
+                        victoria" or "openpyn au -a 'sydney'"
   -m MAX_LOAD, --max-load MAX_LOAD
-                        Specify load threshold, rejects servers with more
-                        load than this, DEFAULT=70
-
+                        Specify load threshold, rejects servers with more load
+                        than this, DEFAULT=70
   -t TOP_SERVERS, --top-servers TOP_SERVERS
-                        Specify the number of Top Servers to choose from the
-                        NordVPN''s Sever list for the given Country, These will
-                        be Pinged. DEFAULT=4
-
-  -p PINGS, --pings PINGS
-                        Specify number of pings to be sent to each server to
-                        determine quality, DEFAULT=5
-
-  -k, --kill            Kill any running Openvnp process, very useful to kill
-                        openpyn process running in background with "-d" switch
-
-  -x, --kill-flush      Kill any running Openvnp process, AND Flush Iptables
-
-  -f, --force-fw-rules  Enforce Firewall rules to drop traffic when tunnel
-                        breaks , Force disable DNS traffic going to any other
-                        interface
-
-  --allow INTERNALLY_ALLOWED [INTERNALLY_ALLOWED ...]
-                        To be used with "f" to allow ports but ONLY to
-                        INTERNAL IP RANGE. e.g, you can use your PC as
-                        SSH, HTTP server for local devices (e.g 192.168.1.*
-                        range) by using "openpyn us -f --allow 22 80"
+                        Specify the number of top servers to choose from the
+                        NordVPN’s server list for the given country, these
+                        will be pinged, DEFAULT=10
+  --p2p                 Only look for servers with "Peer To Peer" support
+  --dedicated           Only look for servers with "Dedicated IP" support
+  --tor                 Only look for servers with "Tor Over VPN" support
+  --double              Only look for servers with "Double VPN" support
+  --anti-ddos           Only look for servers with "Obfuscated" support
+  --netflix             Only look for servers that are optimised for "Netflix"
 
-  --update              Fetch the latest config files from nord''s site
+Display Options:
+  These Only Display Information
 
-  -l [L_LIST], --list [L_LIST]
+  -l [LIST_SERVERS], --list [LIST_SERVERS]
                         If no argument given prints all Country Names and
                         Country Codes; If country code supplied ("-l us"):
                         Displays all servers in that given country with their
-                        current load and openvpn support status. Works in
-                        conjunction with (-a | --area, and server types (--p2p,
-                        --tor) e.g "openpyn -l it --p2p --area milano"
+                        current load and OpenVPN support status. Works in
+                        conjunction with (-a | --area, and server types
+                        (--p2p, --tor) e.g "openpyn -l it --p2p --area milano"
+  --status              Show last change in connection status
+  --stats               Show OpenVPN connection stats
 
-  --p2p                 Only look for servers with "Peer To Peer" support
-  --dedicated           Only look for servers with "Dedicated IP" support
-  --tor                 Only look for servers with "Tor Over VPN" support
-  --double              Only look for servers with "Double VPN" support
-  --anti-ddos           Only look for servers with "Anti DDos" support
-  --netflix             Only look for servers that are optimised for "Netflix"
-  --test                Simulation only, do not actually connect to the vpn
-                        server
-  -n NVRAM, --nvram NVRAM
-                        Specify client to save configuration to NVRAM
-                        (ASUSWRT-Merlin)
-  -o OPENVPN_OPTIONS, --openvpn-options OPENVPN_OPTIONS
-                        Pass through openvpn options, e.g. openpyn uk -o '--
-                        status /var/log/status.log --log /var/log/log.log'
+Firewall Options:
+  Firewall, KillSwitch and Route Options
+
+  -f, --force-fw-rules  Enforce firewall rules to drop traffic when tunnel
+                        breaks , force disable DNS traffic going to any other
+                        interface
+  -r, --add-route       Add route to default-gateway; Needed to continue
+                        serving any service including SSH. Required on VPSs.
+                        To ensure it doesn’t leak traffic use it with -f and
+                        --allow
+  --allow-locally       To be used with "-f" to allow input traffic on all
+                        ports from locally connected / INTERNAL IP SUBNET. for
+                        example 192.168.1.* range
+  --allow INTERNALLY_ALLOWED [INTERNALLY_ALLOWED ...]
+                        To be used with "-f" to allow TCP connections to given
+                        ports but ONLY to INTERNAL IP RANGE. for example: you
+                        can use your PC as SSH, HTTP server for local devices
+                        (i.e. 192.168.1.* range) by "openpyn us -f --allow 22
+                        80"
+  --allow-config INTERNALLY_ALLOWED_CONFIG
+                        To be used with "-f" to allow a complex set of allow
+                        port rules. This option requires a path to a JSON file
+                        that contains the relevant config
+  --allow-config-json INTERNALLY_ALLOWED_CONFIG_JSON
+                        To be used with "-f" to allow a complex set of allow
+                        port rules. This option works the same as "--allow-
+                        config" option but accepts a JSON object as a string
+                        instead
+```
 
-  ```
 ## Todo
-- [x] find servers with P2P support, Dedicated ips, Anti DDoS, Double VPN, Onion over VPN
-- [x] utilise the frequently updated api at "api.nordvpn.com/server"
-- [x] clean exit, handle exceptions
-- [x] store credentials from user input, if "credentials" file exists use that instead
-- [x] sane command-line options following the POSIX guidelines
-- [ ] ability to store profiles (sort of works as the systemd service file stores last state)
-- [x] find and display server's locations (cities)
-- [x] accept full country names
-- [x] colourise output
-- [x] modularize
-- [x] create a combined config of multiple servers (on the fly) for auto failover
-- [x] uninstall.sh   #sudo pip3 uninstall openpyn
-- [x] view status of the connection after launching in --daemon mode
-- [x] desktop notifications
-- [x] initd script for Asuswrt-merlin: "/opt/etc/init.d/S23openpyn start"
+
+-   [x] find servers with P2P support, Dedicated IPs, Anti DDoS, Double VPN, Onion over VPN
+-   [x] utilise the frequently updated api at "api.nordvpn.com/server"
+-   [x] clean exit, handle exceptions
+-   [x] store credentials from user input, if "credentials" file exists use that instead
+-   [x] sane command-line options following the POSIX guidelines
+-   [ ] ability to store profiles (sort of works as the systemd service file stores last state)
+-   [x] find and display server’s locations (cities)
+-   [x] accept full country names
+-   [x] colourise output
+-   [x] modularize
+-   [x] create a combined config of multiple servers (on the fly) for auto failover
+-   [x] uninstall.sh # sudo pip3 uninstall openpyn
+-   [x] view status of the connection after launching in --daemon mode
+-   [x] desktop notifications
+-   [x] initd script for Asuswrt-merlin: "/opt/etc/init.d/S23openpyn start"
diff --git a/docs/allowed-ports-config.md b/docs/allowed-ports-config.md
new file mode 100644
index 0000000..7a08f5a
--- /dev/null
+++ b/docs/allowed-ports-config.md
@@ -0,0 +1,70 @@
+# Allowed ports config
+
+## Intro
+The allowed ports config is a json object used to specify exclutions before the the firewall is created.
+
+Note: Incorrectly applying these rules will result in the firewall not blocking all outgoing traffic. Only disable the `internal` option if you know what you are doing!
+## Setup
+This config can be loaded through two options
+
+- `--allow-config [string]` Specifies a path to a .json file containing the config.
+- `--allow-config-json [string]` Allows for the json string to be passed directly as an argument.
+
+## Schema
+```json
+[
+    {
+        "port": int || string,
+        "protocol": (optional) string,
+        "internal": (optional) bool
+        "allowed_ip_range": (optional) str[] || str
+    },
+    ...
+]
+```
+
+## Details
+
+### Port
+This option specifies what port(s) should be excluded by this rule. This option is required.
+
+Can either be passed as an integer between `1` and `65535`; a string or a port range in the form of `{port_lower}-{port_upper}`
+
+### Protocol
+This option defines the protocols the ports will be opened on. This defaults to `tcp`.
+
+This can be either be set to `tcp`, `udp` or `both`.
+
+### Internal
+This option specifies if ports should be only allowed on internal IP ranges. When set to `true` this will only allow ports to be exposed in the local network
+
+Note: this option is overridden if extra IPs are set in
+
+### Allowed IP Range
+This option specifies the ip(s) that the specified port(s) should be exposed to. This can either be a specific IP or an IP block.
+
+This can either be passed as a single IP (as a string) or an array of ips (as an array of srings). This can also be used 
+
+This defaults to null.
+
+## Examples
+```json
+[
+    {
+        "port": 22,
+        "protocol": "both"
+    }
+]
+```
+Expose on the external network port `22` on both `tcp` and `udp` protocols.
+
+```json
+[
+    {
+        "port": "137-139",
+        "protocol": "udp",
+        "internal": false
+    }
+]
+```
+Expose ports 137, 138 and 139 over UDP to the clear net while the firewall is up
diff --git a/openpyn/__init__.py b/openpyn/__init__.py
index e5d466b..24fa104 100644
--- a/openpyn/__init__.py
+++ b/openpyn/__init__.py
@@ -1,38 +1,41 @@
-import sys
 import os.path
 import subprocess
+import sys
 
-
-__version__ = "2.7.5"
+__version__ = "3.0.0"
 __license__ = "GNU General Public License v3 or later (GPLv3+)"
 __data_files__ = []
 __basefilepath__ = os.path.dirname(os.path.abspath(__file__)) + "/"
 
-log_format = '%(asctime)s [%(levelname)s] %(message)s'
-log_folder = "/var/log/openpyn"     # logs will be saved here
+log_format = "%(asctime)s [%(levelname)s] %(message)s"
+
+ovpn_folder = os.path.join(__basefilepath__, "files")  # .ovpn config files location
+log_folder = "/var/log/openpyn"  # logs will be saved here
+credentials_file_path = os.path.join(__basefilepath__, "credentials")  # nordvpn username/password
 
+sudo_user = "root"
 
 if sys.platform == "linux":
-    if subprocess.check_output(['/bin/uname', '-o']).decode(sys.stdout.encoding).strip() == "ASUSWRT-Merlin":
-        __data_files__ = [('/opt/etc/init.d', ['./openpyn/S23openpyn'])]
+    if subprocess.check_output(["/bin/uname", "-o"]).decode(sys.stdout.encoding).strip() == "ASUSWRT-Merlin":
+        __data_files__ = [("/opt/etc/init.d", ["./openpyn/S23openpyn"])]
+        # admin is Asuswrt's UID 1. It's root, but with a different name
+        # standard installation Entware
+        sudo_user = "admin"
+        # alternative installation Entware (not recommended)
+        # sudo_user = "root"
     elif os.path.exists("/etc/openwrt_release"):
-        __data_files__ = [('/opt/etc/init.d', ['./openpyn/S23openpyn'])]
-
-
-# print("\n".join(sorted({attrname for item in gc.get_objects() for attrname in dir(item) if attrname.startswith("__")})))
-#
-# print(__basefilepath__)
-# print(__build_class__)
-# print(__builtins__)
-# print(__data_files__)
-# print(__debug__)
-# print(__doc__)
-# print(__file__)
-# print(__import__)
-# print(__license__)
-# print(__loader__)
-# print(__name__)
-# print(__package__)
-# print(__path__)
-# print(__spec__)
-# print(__version__)
+        __data_files__ = [("/opt/etc/init.d", ["./openpyn/S23openpyn"])]
+
+
+# locally modify the PATH variable, to get around issues on some distros
+def add_to_path(bin_path):
+    # add pathseperator i.e ":"
+    bin_path_str = os.pathsep + bin_path
+    if bin_path_str not in os.environ["PATH"]:
+        os.environ["PATH"] += bin_path_str
+
+
+add_to_path("/usr/sbin")        # for Fedora/Debian
+add_to_path("/sbin")            # for Debain Buster
+add_to_path("/usr/local/bin")   # for openpyn-management on Fedora
+add_to_path("/usr/local/sbin")  # for openpyn-management on MacOS
diff --git a/openpyn/api.py b/openpyn/api.py
index b31c4d8..cacb5a5 100644
--- a/openpyn/api.py
+++ b/openpyn/api.py
@@ -1,48 +1,87 @@
+import json
 import logging
-import sys
-from typing import Dict, List
+import os
+from datetime import datetime
+from datetime import timedelta
+from typing import Dict
+from typing import List
 
 import requests
 import verboselogs
 
 from openpyn import filters
+from openpyn import ovpn_folder
 
 logger = logging.getLogger(__package__)
 verboselogs.install()
 
 
-# Using requests, GETs and returns json from a url.
+def get_json_cached(url) -> Dict:
+    json_data = None
+    json_path = os.path.join(ovpn_folder, "nordvpn.json")
+
+    if os.path.exists(json_path):
+        t = os.path.getmtime(json_path)
+        last_updated = datetime.utcfromtimestamp(t)
+
+        if (datetime.utcnow() - last_updated) > timedelta(minutes=5):
+            try:
+                json_data = get_json(url)
+            except RuntimeError:
+                logger.error("Error occurred while updating nordvpn.json, rate limit exceeded?")
+    else:
+        json_data = get_json(url)
+
+    if json_data is not None:
+        with open(json_path, "w") as json_file:
+            json.dump(json_data, json_file)
+    else:
+        with open(json_path, "r") as json_file:
+            json_data = json.load(json_file)
+
+    return json_data
+
+
+# Using requests, GETs and returns JSON from a url.
 def get_json(url) -> Dict:
-    headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) \
-    AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.90 Safari/537.36'}
+    headers = {
+        "User-Agent": (
+            "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.90 Safari/537.36"
+        )
+    }
 
     try:
-        json_response = requests.get(url, headers=headers).json()
-    except requests.exceptions.HTTPError:
-        logger.error("Cannot GET the JSON from nordvpn.com, Manually Specify a Server \
-using '-s' for example '-s au10'")
-        sys.exit(1)
-    except requests.exceptions.RequestException:
-        logger.error("There was an ambiguous exception, Check Your Network Connection. \
-forgot to flush iptables? (openpyn -x)")
-        sys.exit(1)
-    return json_response
-
-
-# Gets json data, from api.nordvpn.com. filter servers by type, country, area.
+        json_response = requests.get(url, headers=headers)
+        json_response.raise_for_status()
+        json_data = json_response.json()
+    except requests.exceptions.HTTPError as e:
+        raise RuntimeError(
+            "Cannot GET the JSON from nordvpn.com, Manually Specify a Server using '-s' for example '-s au10'"
+        ) from e
+    except requests.exceptions.RequestException as e:
+        raise RuntimeError(
+            "Error while connecting to {}, Check Your Network Connection. forgot to flush iptables? (openpyn -x)".format(url)
+        ) from e
+    return json_data
+
+
+# Gets JSON data, from api.nordvpn.com. filter servers by type, country, area.
 def get_data_from_api(
         country_code: str, area: str, p2p: bool, dedicated: bool, double_vpn: bool,
         tor_over_vpn: bool, anti_ddos: bool, netflix: bool, location: float) -> List:
-
+    country_code = country_code.lower()
     url = "https://api.nordvpn.com/server"
-    json_response = get_json(url)
+    json_response = get_json_cached(url)
 
-    type_filtered_servers = filters.filter_by_type(
-        json_response, p2p, dedicated, double_vpn, tor_over_vpn, anti_ddos, netflix)
+    type_filtered_servers = []
+    if netflix:
+        type_filtered_servers = filters.filter_by_netflix(json_response, country_code)
+    else:
+        type_filtered_servers = filters.filter_by_type(json_response, p2p, dedicated, double_vpn, tor_over_vpn, anti_ddos)
     if location:
         type_location_filtered = filters.filter_by_location(location, type_filtered_servers)
         return type_location_filtered
-    if country_code != "all":       # if "-l" had country code with it. e.g "-l au"
+    if country_code != "all":  # if "-l" had country code with it. e.g "-l au"
         type_country_filtered = filters.filter_by_country(country_code, type_filtered_servers)
         if area is None:
             return type_country_filtered
@@ -54,20 +93,31 @@ def get_data_from_api(
 def list_all_countries() -> None:
     countries_mapping = {}
     url = "https://api.nordvpn.com/server"
-    json_response = get_json(url)
+    json_response = get_json_cached(url)
     for res in json_response:
         if res["domain"][:2] not in countries_mapping:
             countries_mapping.update({res["domain"][:2]: res["country"]})
     for key, val in countries_mapping.items():
-        print("Full Name : " + val + "      Country Code : " + key)
+        print("Full Name : " + val + "\t\tCountry Code : " + key)
 
 
 def get_country_code(full_name: str) -> str:
+    full_name = full_name.lower()
     url = "https://api.nordvpn.com/server"
-    json_response = get_json(url)
+    json_response = get_json_cached(url)
     for res in json_response:
-        if res["country"].lower() == full_name.lower():
-            code = res["domain"][:2].lower()
+        if res["country"].lower() == full_name:
+            code = res["domain"][:2]
             return code
-    logger.error("Country Name Not Correct")
-    sys.exit(1)
+    raise RuntimeError("Country Name Not Correct")
+
+
+def get_country_name(iso_code: str) -> str:
+    iso_code = iso_code.lower()
+    url = "https://api.nordvpn.com/server"
+    json_response = get_json_cached(url)
+    for res in json_response:
+        if res["domain"][:2] == iso_code:
+            name = res["country"]
+            return name
+    raise RuntimeError("Country Code Not Correct")
diff --git a/openpyn/asus.py b/openpyn/asus.py
index aa7717b..2e293fb 100644
--- a/openpyn/asus.py
+++ b/openpyn/asus.py
@@ -1,39 +1,44 @@
 import logging
+import os
 import subprocess
 
 import verboselogs
 
-from openpyn import __basefilepath__, api
-from openpyn.converter import T_CLIENT, Converter
+from openpyn import api
+from openpyn import credentials_file_path
+from openpyn import ovpn_folder
+from openpyn import sudo_user
+from openpyn.converter import T_CLIENT
+from openpyn.converter import Converter
 
 verboselogs.install()
 logger = logging.getLogger(__package__)
 
 
-def run(server, c_code, client, rgw=None, comp=None, adns=None, tcp=False, test=False, debug=False):
-    with open(__basefilepath__ + "credentials", 'r') as f:
-        lines = f.read().splitlines()
-        f.close()
+def run(server, client, options=None, rgw=None, comp=None, adns=None, tcp=False, test=False, debug=False):
+    country_name = api.get_country_name(server[:2])
 
-    url = "https://api.nordvpn.com/server"
-    json_response = api.get_json(url)
-    for res in json_response:
-        if res["domain"][:2].lower() == c_code.lower():
-            country_name = res["country"]
-            break
+    with open(credentials_file_path, "r") as f:
+        lines = f.read().splitlines()
 
     port = "udp"
     port_name = "1194"
     protocol_name = "udp"
-    folder = "ovpn_udp/"
+    folder = "/ovpn_udp/"
     if tcp:
         port = "tcp"
         port_name = "443"
         protocol_name = "tcp-client"
-        folder = "ovpn_tcp/"
+        folder = "/ovpn_tcp/"
 
     vpn_config_file = server + ".nordvpn.com." + port + ".ovpn"
 
+    certs_folder = "/jffs/openvpn/"
+
+    if not os.path.exists(certs_folder):
+        os.mkdir(certs_folder, 0o700)
+        os.chmod(certs_folder, 0o700)
+
     c = Converter(debug)
     c.set_username(lines[0])
     c.set_password(lines[1])
@@ -42,20 +47,21 @@ def run(server, c_code, client, rgw=None, comp=None, adns=None, tcp=False, test=
     c.set_protocol(protocol_name)
 
     c.set_name(server)
-    c.set_source_folder(__basefilepath__ + "files/" + folder)
-    c.set_certs_folder("/jffs/openvpn/")
+    c.set_source_folder(ovpn_folder + folder)
+    c.set_certs_folder(certs_folder)
 
     c.set_accept_dns_configuration(adns)
     c.set_compression(comp)
     c.set_redirect_gateway(rgw)
     c.set_client(client)
 
+    if options:
+        c.set_openvpn_options("\n".join(filter(None, options.split("--"))) + "\n")
+
     extracted_info = c.extract_information(vpn_config_file)
     if not test:
         c.write_certificates(client)
 
-    c.pprint(extracted_info)
-
     # 'vpn_client_unit'
     key = ""
     value = ""
@@ -63,14 +69,12 @@ def run(server, c_code, client, rgw=None, comp=None, adns=None, tcp=False, test=
     service = "client"
 
     for key, value in extracted_info.items():
-        write(c, key, value, unit, service, test)
+        write(key, value, unit, service, test)
 
     extracted_info = dict(extracted_info)
     if T_CLIENT in extracted_info:
         del extracted_info[T_CLIENT]
 
-    c.pprint(extracted_info)
-
     # 'vpn_client_unit$'
     key = ""
     value = ""
@@ -78,7 +82,7 @@ def run(server, c_code, client, rgw=None, comp=None, adns=None, tcp=False, test=
     service = "client"
 
     for key, value in extracted_info.items():
-        write(c, key, value, unit, service, test)
+        write(key, value, unit, service, test)
 
     # 'vpn_upload_unit'
     key = T_CLIENT
@@ -86,21 +90,101 @@ def run(server, c_code, client, rgw=None, comp=None, adns=None, tcp=False, test=
     unit = ""
     service = "upload"
 
-    write(c, key, value, unit, service, test)
+    write(key, value, unit, service, test)
 
 
-def write(c, key, value, unit, service, test=False):
+def write(key, value, unit, service, test=False):
     argument1 = "vpn" + "_" + service + unit + "_" + key
     argument2 = argument1 + "=" + value
     try:
-        c.pprint("/bin/nvram" + " " + "get" + " " + argument1)
+        pprint("/bin/nvram" + " " + "get" + " " + argument1)
         if not test:
-            current = subprocess.run(["/bin/nvram", "get", argument1],
-                                     check=True, stdout=subprocess.PIPE).stdout
-            if current.decode('utf-8').strip() == value:
+            process = subprocess.run(["/bin/nvram", "get", argument1], check=True, stdout=subprocess.PIPE)
+            if process.stdout.decode("utf-8").strip() == value:
                 return
-        c.pprint("/bin/nvram" + " " + "set" + " " + argument2)
+        pprint("/bin/nvram" + " " + "set" + " " + argument2)
         if not test:
-            subprocess.run(["sudo", "/bin/nvram", "set", argument2], check=True)
+            subprocess.run(["sudo", "-u", sudo_user, "/bin/nvram", "set", argument2], check=True)
     except subprocess.CalledProcessError as e:
-        logger.error(e.output)
+        raise RuntimeError(e.output) from e
+
+
+def connect(unit, test=False):
+    argument1 = "vpnclient" + unit
+    argument2 = "start" + "_" + "vpnclient" + unit
+    try:
+        pprint("/bin/pidof" + " " + argument1)
+        if not test:
+            process = subprocess.run(["/bin/pidof", argument1], stdout=subprocess.DEVNULL, check=False)
+            if process.returncode == 0:  # Connected
+                return
+        pprint("/sbin/service" + " " + argument2)
+        if not test:
+            subprocess.run(["sudo", "-u", sudo_user, "/sbin/service", argument2], check=True, stdout=subprocess.DEVNULL)
+    except subprocess.CalledProcessError as e:
+        raise RuntimeError(e.output) from e
+
+
+def disconnect(unit, test=False):
+    argument1 = "vpnclient" + unit
+    argument2 = "stop" + "_" + "vpnclient" + unit
+    try:
+        pprint("/bin/pidof" + " " + argument1)
+        if not test:
+            process = subprocess.run(["/bin/pidof", argument1], stdout=subprocess.DEVNULL, check=False)
+            if process.returncode == 1:  # Disconnected
+                return
+        pprint("/sbin/service" + " " + argument2)
+        if not test:
+            subprocess.run(["sudo", "-u", sudo_user, "/sbin/service", argument2], check=True, stdout=subprocess.DEVNULL)
+    except subprocess.CalledProcessError as e:
+        raise RuntimeError(e.output) from e
+
+
+def state(unit, test=False) -> bool:
+    argument1 = "vpn" + "_" + "client" + unit + "_" + "state"
+    try:
+        pprint("/bin/nvram" + " " + "get" + " " + argument1)
+        if not test:
+            client_state = subprocess.run(["/bin/nvram", "get", argument1], check=True, stdout=subprocess.PIPE).stdout
+            code = client_state.decode("utf-8").strip()
+            if code == "1":
+                logger.success("Connecting...")
+            elif code == "2":
+                logger.success("Connected")
+            elif code == "-1":
+                return errno(unit, test)
+
+        return 0
+    except subprocess.CalledProcessError as e:
+        raise RuntimeError(e.output) from e
+
+
+def errno(unit, test=False) -> bool:
+    argument1 = "vpn" + "_" + "client" + unit + "_" + "errno"
+    try:
+        pprint("/bin/nvram" + " " + "get" + " " + argument1)
+        if not test:
+            client_errno = subprocess.run(["/bin/nvram", "get", argument1], check=True, stdout=subprocess.PIPE).stdout
+            code = client_errno.decode("utf-8").strip()
+            if code == "1":
+                logger.error("Error - IP conflict!")
+            elif code == "2":
+                logger.error("Error - Routing conflict!")
+            elif code == "4":
+                logger.error("Error - SSL/TLS issue!")
+            elif code == "5":
+                logger.error("Error - DH issue!")
+            elif code == "6":
+                logger.error("Error - Authentication failure!")
+            else:
+                logger.error("Error - check configuration!")
+
+        return 1
+    except subprocess.CalledProcessError as e:
+        raise RuntimeError(e.output) from e
+
+
+def pprint(msg, debug=False):
+    if debug:
+        logger.debug(msg)
diff --git a/openpyn/config.json b/openpyn/config.json
deleted file mode 100644
index 998741f..0000000
--- a/openpyn/config.json
+++ /dev/null
@@ -1,15 +0,0 @@
-{
-  "vpn_provider": "nordvpn"{
-    "settings":{
-      "files_location": "/usr/share/openpyn/files/",
-      "download_url": "https://nordvpn.com/api/files/zip"
-    },
-  },
-
-  "vpn_provider": "ipvanish"{
-    "settings":{
-      "files_location": "/usr/share/openpyn/ipvanish/files/",
-      "download_url": "https://www.ipvanish.com/software/configs/configs.zip"
-    },
-  },
-}
diff --git a/openpyn/converter.py b/openpyn/converter.py
index 1b2a118..ee6e35e 100644
--- a/openpyn/converter.py
+++ b/openpyn/converter.py
@@ -10,40 +10,45 @@
 verboselogs.install()
 logger = logging.getLogger(__package__)
 
+# pylint: disable=R1732
+
+# ASUSWRT-Merlin
+# github.com/RMerl/asuswrt-merlin.ng/blob/5491c35d99160fefd3c8c89e7de5a635215009a2/release/src/router/shared/defaults.c
+
 # CONFIGURATION PLACEHOLDERS
-# spaces are intentionally let by the end of the word
-SERVER_ADDRESS = 'remote '
-CIPHER = 'cipher '
-AUTH_DIGEST = 'auth '
-TLS_CONTROL_CHANNEL_SECURITY = 'key-direction '
-TLS_RENEGOTIATION_TIME = 'reneg-sec '
-CONNECTION_RETRY = 'resolv-retry '
-LOG_VERBOSITY = 'verb '
+# spaces are intentionally left at the end of the word
+SERVER_ADDRESS = "remote "
+CIPHER = "cipher "
+AUTH_DIGEST = "auth "
+TLS_CONTROL_CHANNEL_SECURITY = "key-direction "
+TLS_RENEGOTIATION_TIME = "reneg-sec "
+CONNECTION_RETRY = "resolv-retry "
+LOG_VERBOSITY = "verb "
 
 # TEMPLATE PLACEHOLDERS
-T_SERVER_ADDRESS = 'addr'
-T_ACCEPT_DNS_CONFIGURATION = 'adns'
-T_CIPHER = 'cipher'
-T_COMPRESSION = 'comp'
-T_CUSTOM_CONFIGURATION = 'custom2'
-T_DESCRIPTION = 'desc'
-T_AUTH_DIGEST = 'digest'
-T_TLS_CONTROL_CHANNEL_SECURITY = 'hmac'
-T_INTERFACE_TYPE = 'if'
-T_PASSWORD = 'password'
-T_PORT = 'port'
-T_PROTOCOL = 'proto'
-T_TLS_RENEGOTIATION_TIME = 'reneg'
-T_CONNECTION_RETRY = 'retry'
-T_REDIRECT_GATEWAY = 'rgw'
-T_CLIENT = 'unit'
-T_USERAUTH = 'userauth'
-T_USERNAME = 'username'
-T_USERONLY = 'useronly'
-T_LOG_VERBOSITY = 'verb'
-
-
-class Converter(object):
+T_SERVER_ADDRESS = "addr"
+T_ACCEPT_DNS_CONFIGURATION = "adns"
+T_CIPHER = "cipher"
+T_COMPRESSION = "comp"
+T_CUSTOM_CONFIGURATION = "cust2"
+T_DESCRIPTION = "desc"
+T_AUTH_DIGEST = "digest"
+T_TLS_CONTROL_CHANNEL_SECURITY = "hmac"
+T_INTERFACE_TYPE = "if"
+T_PASSWORD = "password"
+T_PORT = "port"
+T_PROTOCOL = "proto"
+T_TLS_RENEGOTIATION_TIME = "reneg"
+T_CONNECTION_RETRY = "connretry"
+T_REDIRECT_GATEWAY = "rgw"
+T_CLIENT = "unit"
+T_USERAUTH = "userauth"
+T_USERNAME = "username"
+T_USERONLY = "useronly"
+T_LOG_VERBOSITY = "verb"
+
+
+class Converter:
     # NordVPN username
     _username = None
     # NordVPN password
@@ -95,7 +100,7 @@ def __init__(self, debug_mode=False):
         self._extracted_data[T_PORT] = "1194"
         self._extracted_data[T_PROTOCOL] = "udp"
         self._extracted_data[T_TLS_RENEGOTIATION_TIME] = "0"
-        self._extracted_data[T_CONNECTION_RETRY] = "-1"
+        self._extracted_data[T_CONNECTION_RETRY] = "15"
         self._extracted_data[T_REDIRECT_GATEWAY] = "1"
         self._extracted_data[T_CLIENT] = "5"
         self._extracted_data[T_USERAUTH] = "1"
@@ -106,56 +111,56 @@ def __init__(self, debug_mode=False):
     def set_name(self, name):
         """Name for the VPN connection"""
         if not name:
-            raise Exception("Please specify an name.")
+            raise RuntimeError("Please specify an name.")
 
         self._name = name
 
     def set_source_folder(self, source_input):
         """Sets the source folder for the configuration files"""
         if not source_input or not os.path.isdir(source_input):
-            raise Exception("Please specify a valid path for the configuration files.")
+            raise RuntimeError("Please specify a valid path for the configuration files.")
 
         self._source_folder = source_input
 
     def set_certs_folder(self, certs_output):
         """Sets the destination folder for the certificates"""
         if not certs_output or not os.path.isdir(certs_output):
-            raise Exception("Please specify a valid path for the certificates.")
+            raise RuntimeError("Please specify a valid path for the certificates.")
 
         self._certs_folder = certs_output
 
     def set_description(self, description):
         """Description for the VPN connection"""
         if not description:
-            raise Exception("Please specify an description.")
+            raise RuntimeError("Please specify an description.")
 
         self._description = description
 
     def set_password(self, password):
         """Password for the VPN connection"""
         if not password:
-            raise Exception("Please specify an password.")
+            raise RuntimeError("Please specify an password.")
 
         self._password = password
 
     def set_port(self, port):
         """Port for the VPN connection"""
         if not port:
-            raise Exception("Please specify an port.")
+            raise RuntimeError("Please specify an port.")
 
         self._port = port
 
     def set_protocol(self, protocol):
         """Protocol for the VPN connection"""
         if not protocol:
-            raise Exception("Please specify an protocol.")
+            raise RuntimeError("Please specify an protocol.")
 
         self._protocol = protocol
 
     def set_username(self, username):
         """Username for the VPN connection"""
         if not username:
-            raise Exception("Please specify an username.")
+            raise RuntimeError("Please specify an username.")
 
         self._username = username
 
@@ -177,13 +182,20 @@ def set_compression(self, compression):
         """Compression for the VPN connection"""
         if compression is None:
             return
-        values = ("-1", "no", "yes", "adaptive", "lz4")
+        # values = ("-1", "no", "yes", "adaptive", "lz4")
         values = ("yes", "adaptive")
         if compression not in values:
             raise ValueError("Value must be one of {0}".format(values))
 
         self._comp = compression
 
+    def set_openvpn_options(self, options):
+        """Options for the VPN connection"""
+        if options is None:
+            return
+
+        self._extracted_data[T_CUSTOM_CONFIGURATION] = options
+
     def set_redirect_gateway(self, rgw):
         """Redirect Internet traffic for the VPN connection"""
         if rgw is None:
@@ -213,7 +225,7 @@ def extract_information(self, input_file):
         self.pprint("Processing file {}".format(input_file), True)
 
         input_file_full = os.path.join(self._source_folder, input_file)
-        with open(input_file_full, 'r') as lines:
+        with open(input_file_full, "r") as lines:
             data = ""
             # for line in islice(lines, 10):
             # pass
@@ -251,7 +263,6 @@ def extract_information(self, input_file):
                     self._extract_tls_control_channel_security(line)
                     continue
                 data = data + line
-            lines.close()
 
         # print(data, end="")
 
@@ -269,21 +280,23 @@ def extract_information(self, input_file):
         # self._extract_name(input_file)
         self._extract_certificates(data)
 
-        self.pprint(self._ca)
-        self.pprint(self._static)
+        self.pprint("\n" + self._ca)
+        self.pprint("\n" + self._static)
         self.pprint(self._extracted_data)
 
         line = self._extracted_data[T_CUSTOM_CONFIGURATION]
         if line[-1:] == "\n":
             line = line[:-1]
 
-        self.pprint(line)
+        self.pprint("\n" + line)
 
-        line = self.stringToBase64(line)
-        line = line.decode('utf-8')
+        line = self.string_to_b64(line)
+        line = line.decode("utf-8")
         self._extracted_data[T_CUSTOM_CONFIGURATION] = line
 
-        self.pprint(line)
+        self.pprint("\n" + line)
+
+        self.pprint(self._extracted_data)
 
         return self._extracted_data
 
@@ -349,7 +362,7 @@ def _extract_log_verbosity(self, line):
     def _extract_custom_configuration(self, line):
         """Specific extractor for Custom Configuration"""
         value = line.strip()
-        # These are already added by OpenVPN
+        # these are already added by OpenVPN
         if value == "tun-mtu 1500":
             pass
         elif value == "tun-mtu-extra 0":
@@ -358,7 +371,7 @@ def _extract_custom_configuration(self, line):
             pass
         elif value == "mssfix 1450":
             pass
-        # These are already added by ASUSWRT-Merlin
+        # these are already added by ASUSWRT-Merlin
         elif value == "client":
             pass
         elif value == "nobind":
@@ -367,10 +380,10 @@ def _extract_custom_configuration(self, line):
             pass
         elif value == "persist-tun":
             pass
-        # Pull is implied by "client"
+        # 'pull' is implied by "client"
         elif value == "pull":
             pass
-        # These are already added by us
+        # these are already added by us
         elif value.startswith("auth-user-pass"):
             pass
         elif value.startswith("comp-lzo"):
@@ -387,12 +400,12 @@ def _extract_custom_configuration(self, line):
         return False
 
     @staticmethod
-    def stringToBase64(s):
-        return base64.b64encode(bytes(s, 'utf-8'))
+    def string_to_b64(s):
+        return base64.b64encode(bytes(s, "utf-8"))
 
     @staticmethod
-    def base64ToString(b):
-        return base64.b64decode(b).decode('utf-8')
+    def b64_to_string(b):
+        return base64.b64decode(b).decode("utf-8")
 
     def _extract_vpn_description(self):
         """Description for the VPN connection"""
@@ -439,13 +452,13 @@ def write_certificates(self, client):
         # write keys
         if self._ca is not None:
             cert_name = "vpn_crt_client" + client + "_ca"
-            cert_file = open(os.path.join(self._certs_folder, cert_name), 'w')
+            cert_file = open(os.path.join(self._certs_folder, cert_name), "w")
             cert_file.write(self._ca)
             cert_file.close()
 
         if self._static is not None:
             cert_name = "vpn_crt_client" + client + "_static"
-            cert_file = open(os.path.join(self._certs_folder, cert_name), 'w')
+            cert_file = open(os.path.join(self._certs_folder, cert_name), "w")
             cert_file.write(self._static)
             cert_file.close()
 
diff --git a/openpyn/credentials.py b/openpyn/credentials.py
index 969a6b8..ac4d17c 100644
--- a/openpyn/credentials.py
+++ b/openpyn/credentials.py
@@ -1,18 +1,16 @@
 import getpass
 import logging
 import os
-import subprocess
 import sys
-from typing import List
 
 import verboselogs
-from openpyn import __basefilepath__, root
+
+from openpyn import credentials_file_path
+from openpyn import root
 
 verboselogs.install()
 logger = logging.getLogger(__package__)
 
-credentials_file_path = __basefilepath__ + "credentials"
-
 
 def check_credentials() -> bool:
     return os.path.exists(credentials_file_path)
@@ -20,28 +18,25 @@ def check_credentials() -> bool:
 
 def save_credentials() -> None:
     if not sys.__stdin__.isatty():
-        logger.critical("Please run %s in interactive mode", __name__)
-        sys.exit(1)
+        raise RuntimeError("Please run %s in interactive mode" % __name__)
 
     if root.verify_running_as_root() is False:
-        logger.error("Please run as 'sudo openpyn --init' the first time. \
-Root access is needed to store credentials in '%s'.", credentials_file_path)
-        sys.exit(1)
-    else:
-        logger.info("Storing credentials in '%s' with openvpn \
-compatible 'auth-user-pass' file format", credentials_file_path)
-
-        username = input("Enter your username for NordVPN, i.e youremail@yourmail.com: ")
-        password = getpass.getpass("Enter the password for NordVPN: ")
-        try:
-            with open(credentials_file_path, 'w') as creds:
-                creds.write(username + "\n")
-                creds.write(password + "\n")
-            creds.close()
-            # Change file permission to 600
-            subprocess.check_call(["sudo", "chmod", "600", credentials_file_path])
-
-            logger.info("Awesome, the credentials have been saved in '%s'", credentials_file_path)
-        except (IOError, OSError):
-            logger.error("IOError while creating 'credentials' file.")
-    return
+        raise RuntimeError(
+            "Please run as 'sudo openpyn --init' the first time. Root access is needed to store credentials in '%s'."
+            % credentials_file_path
+        )
+
+    logger.info("Storing credentials in '%s' with openvpn compatible 'auth-user-pass' file format", credentials_file_path)
+
+    username = input("Enter your username for NordVPN, i.e youremail@yourmail.com: ")
+    password = getpass.getpass("Enter the password for NordVPN: ")
+    try:
+        with open(credentials_file_path, "w") as creds:
+            creds.write(username + "\n")
+            creds.write(password + "\n")
+        # change file permission to 600
+        os.chmod(credentials_file_path, 0o600)
+
+        logger.info("Awesome, the credentials have been saved in '%s'", credentials_file_path)
+    except (IOError, OSError):
+        raise RuntimeError("IOError while creating 'credentials' file.") from None
diff --git a/openpyn/filters.py b/openpyn/filters.py
index ef8a2d3..88c1339 100644
--- a/openpyn/filters.py
+++ b/openpyn/filters.py
@@ -1,6 +1,5 @@
 import logging
 import operator
-import sys
 from typing import List
 
 import verboselogs
@@ -21,15 +20,15 @@ def filter_by_area(area: str, type_country_filtered: List) -> List:
                     aServer["location"]["long"] == item[1]["long"] and \
                     area.lower() in lower_case_areas:
                 aServer["location_names"] = item[2]  # add location info to server
-                # logger.debug(aServer)
                 remaining_servers.append(aServer)
+                # logger.debug(aServer)
     return remaining_servers
 
 
 def filter_by_country(country_code: str, type_filtered_servers: List) -> List:
     remaining_servers = []
     for aServer in type_filtered_servers:
-        if aServer["domain"][:2].lower() == country_code.lower():
+        if aServer["domain"][:2] == country_code:
             remaining_servers.append(aServer)
             # logger.debug(aServer["domain"])
     return remaining_servers
@@ -38,54 +37,118 @@ def filter_by_country(country_code: str, type_filtered_servers: List) -> List:
 def filter_by_location(location: float, type_filtered_servers: List) -> List:
     remaining_servers = []
     for aServer in type_filtered_servers:
-        if aServer["location"]["lat"] == location[0] and \
-                aServer["location"]["long"] == location[1]:
-            # logger.debug(aServer)
+        if aServer["location"]["lat"] == location[0] and aServer["location"]["long"] == location[1]:
             remaining_servers.append(aServer)
+            # logger.debug(aServer)
     return remaining_servers
 
 
-def filter_by_type(json_response, p2p: bool, dedicated: bool, double_vpn: bool,
-                   tor_over_vpn: bool, anti_ddos: bool, netflix: bool) -> List:
+def filter_by_netflix(json_response, country_code: str) -> List:
     remaining_servers = []
-    serverCount = 0
-    netflix_srv = [[707, 710, "us"], [722, 733, "us"], [868, 875, "us"], [884, 887, "us"],
-                   [940, 947, "us"], [952, 963, "us"], [980, 987, "us"], [1074, 1153, "us"],
-                   [1154, 1195, "us"], [1196, 1287, "us"], [1289, 1292, "us"], [1297, 1312, "us"],
-                   [1314, 1317, "us"], [1322, 1353, "us"], [1358, 1365, "us"], [1370, 1385, "us"],
-                   [1394, 1421, "us"], [1422, 1457, "us"], [15, 20, "uk"], [30, 34, "uk"],
-                   [36, 41, "uk"], [47, 51, "uk"], [53, 101, "uk"], [190, 192, "uk"],
-                   [196, 207, "uk"], [228, 239, "uk"], [248, 251, "uk"], [256, 279, "uk"],
-                   [300, 323, "uk"], [328, 331, "uk"], [348, 351, "uk"],
-                   [30, 44, "fr"], [45, 63, "fr"], [65, 76, "fr"], [77, 80, "fr"],
-                   [92, 95, "nl"], [165, 170, "ca"]]
+    server_count = 0
+    netflix_us = [[585, 592, "us"], [603, 604, "us"], [609, 617, "us"], [625, 632, "us"], [645, 680, "us"],
+                  [690, 690, "us"], [707, 710, "us"], [722, 737, "us"], [777, 780, "us"], [797, 804, "us"],
+                  [833, 843, "us"], [856, 853, "us"], [872, 879, "us"], [896, 903, "us"], [908, 919, "us"],
+                  [936, 939, "us"], [972, 987, "us"], [1016, 1020, "us"], [1033, 1041, "us"], [1046, 1049, "us"],
+                  [1054, 1086, "us"], [1102, 1130, "us"], [1138, 1141, "us"], [1150, 1157, "us"], [1162, 1195, "us"],
+                  [1236, 1239, "us"], [1248, 1255, "us"], [1260, 1279, "us"], [1284, 1287, "us"], [1297, 1312, "us"],
+                  [1322, 1341, "us"], [1346, 1357, "us"], [1362, 1365, "us"], [1370, 1373, "us"], [1418, 1421, "us"],
+                  [1426, 1429, "us"], [1442, 1457, "us"], [1470, 1479, "us"], [1484, 1495, "us"], [1500, 1515, "us"],
+                  [1520, 1539, "us"], [1560, 1599, "us"], [1604, 1607, "us"], [1612, 1671, "us"], [1676, 1691, "us"],
+                  [1696, 1707, "us"], [1709, 1737, "us"], [1742, 1745, "us"], [1750, 1793, "us"], [1798, 1801, "us"],
+                  [1806, 2029, "us"], [2034, 2045, "us"], [2048, 2139, "us"], [2144, 2203, "us"], [2216, 2223, "us"],
+                  [2228, 2392, "us"], [2396, 2411, "us"], [2428, 2451, "us"], [2460, 2488, "us"], [2509, 2524, "us"],
+                  [2533, 2536, "us"], [2545, 2548, "us"], [2561, 2564, "us"], [2573, 2600, "us"], [2609, 2616, "us"],
+                  [2621, 2632, "us"], [2642, 2645, "us"], [2674, 2677, "us"], [2686, 2705, "us"], [2710, 2721, "us"],
+                  [2730, 2749, "us"], [2758, 2765, "us"], [2778, 2789, "us"], [2794, 2805, "us"], [2810, 2825, "us"],
+                  [2834, 2843, "us"], [2848, 2851, "us"], [2853, 2853, "us"], [2862, 2870, "us"], [2872, 2872, "us"],
+                  [2877, 2893, "us"], [2895, 2895, "us"], [2897, 2899, "us"], [2951, 2953, "us"]]
+
+    netflix_ca = [[18, 21, "ca"], [27, 30, "ca"], [37, 40, "ca"], [49, 56, "ca"], [61, 64, "ca"], [69, 76, "ca"],
+                  [81, 90, "ca"], [103, 104, "ca"], [117, 124, "ca"], [149, 343, "ca"], [367, 453, "ca"], [456, 457, "ca"],
+                  [461, 462, "ca"]]
+
+    netflix_nl = [[21, 24, "nl"], [39, 42, "nl"], [44, 47, "nl"], [52, 67, "nl"], [156, 264, "nl"], [269, 287, "nl"],
+                  [292, 331, "nl"], [360, 360, "nl"], [373, 373, "nl"], [375, 377, "nl"], [382, 387, "nl"], [21, 24, "nl"],
+                  [39, 42, "nl"], [44, 47, "nl"], [52, 67, "nl"], [156, 264, "nl"], [269, 287, "nl"], [292, 331, "nl"],
+                  [360, 360, "nl"], [373, 373, "nl"], [375, 377, "nl"], [382, 387, "nl"]]
+
+    netflix_jp = [[15, 16, "jp"], [26, 29, "jp"], [67, 69, "jp"], [71, 74, "jp"], [115, 126, "jp"], [175, 175, "jp"],
+                  [181, 181, "jp"]]
+
+    netflix_uk = [[69, 72, "uk"], [89, 89, "uk"], [154, 156, "uk"], [178, 181, "uk"], [195, 199, "uk"], [228, 231, "uk"],
+                  [264, 275, "uk"], [280, 283, "uk"], [300, 303, "uk"], [336, 339, "uk"], [344, 347, "uk"], [372, 375, "uk"],
+                  [380, 383, "uk"], [393, 400, "uk"], [417, 420, "uk"], [425, 428, "uk"], [433, 436, "uk"], [457, 460, "uk"],
+                  [465, 468, "uk"], [484, 487, "uk"], [496, 496, "uk"], [565, 568, "uk"], [593, 596, "uk"], [609, 612, "uk"],
+                  [617, 620, "uk"], [637, 640, "uk"], [649, 656, "uk"], [673, 676, "uk"], [701, 704, "uk"], [705, 708, "uk"],
+                  [725, 728, "uk"], [765, 766, "uk"]]
+
+    netflix_gr = [[3, 3, "gr"]]
+
+    netflix_mx = [[3, 9, "mx"], [11, 11, "mx"]]
+
+    netflix_srv = []
+
+    # Google Sheets Formula
+    # =CONCATENATE("[",IF(ISERR(FIND("-", A1)),CONCATENATE(A1,", ",A1),SUBSTITUTE(A1,"-",", ")), ", ""us""", "]")
+
+    if country_code == "us":
+        netflix_srv = netflix_us
+    elif country_code == "ca":
+        netflix_srv = netflix_ca
+    elif country_code == "nl":
+        netflix_srv = netflix_nl
+    elif country_code == "jp":
+        netflix_srv = netflix_jp
+    elif country_code == "uk":
+        netflix_srv = netflix_uk
+    elif country_code == "gr":
+        netflix_srv = netflix_gr
+    elif country_code == "mx":
+        netflix_srv = netflix_mx
+    elif country_code == "all":
+        netflix_srv = netflix_us + netflix_ca + netflix_nl + netflix_jp + netflix_uk + netflix_gr + netflix_mx
 
     for eachServer in json_response:
-        serverCount += 1
-        if netflix:
-            for server in netflix_srv:
-                for number in range(server[0], server[1] + 1):
-                    # logger.debug(server[2]+str(number)+".")
+        server_count += 1
+        for server in netflix_srv:
+            for number in range(server[0], server[1] + 1):
+                if server[2] + str(number) + "." in eachServer["domain"]:
+                    remaining_servers.append(eachServer)
                     # logger.debug(eachServer["domain"])
-                    if server[2] + str(number) + "." in eachServer["domain"]:
-                        remaining_servers.append(eachServer)
-                        # logger.debug(eachServer["domain"])
+    # logger.debug("Total available servers = ", serverCount)
+    return remaining_servers
+
+
+def filter_by_type(json_response, p2p: bool, dedicated: bool, double_vpn: bool, tor_over_vpn: bool, anti_ddos: bool) -> List:
+    remaining_servers = []
+    server_count = 0
+    standard_vpn = False
+
+    if p2p is False and dedicated is False and double_vpn is False and tor_over_vpn is False and anti_ddos is False:
+        standard_vpn = True
+
+    for eachServer in json_response:
+        server_count += 1
         for ServerType in eachServer["categories"]:
-            # logger.debug(eachServer["categories"])
             if p2p and ServerType["name"] == "P2P":
                 remaining_servers.append(eachServer)
-            if dedicated and ServerType["name"] == "Dedicated IP servers":
+                break
+            if dedicated and ServerType["name"] == "Dedicated IP":
                 remaining_servers.append(eachServer)
+                break
             if double_vpn and ServerType["name"] == "Double VPN":
                 remaining_servers.append(eachServer)
+                break
             if tor_over_vpn and ServerType["name"] == "Onion Over VPN":
                 remaining_servers.append(eachServer)
-            if anti_ddos and ServerType["name"] == "Anti DDoS":
+                break
+            if anti_ddos and ServerType["name"] == "Obfuscated Servers":
                 remaining_servers.append(eachServer)
-            if p2p is False and dedicated is False and double_vpn is False and \
-                    tor_over_vpn is False and anti_ddos is False and netflix is False:
-                if ServerType["name"] == "Standard VPN servers":
-                    remaining_servers.append(eachServer)
+                break
+            if standard_vpn and ServerType["name"] == "Standard VPN servers":
+                remaining_servers.append(eachServer)
+                break
     # logger.debug("Total available servers = ", serverCount)
     return remaining_servers
 
@@ -111,11 +174,8 @@ def filter_by_load(server_list: List, max_load: int, top_servers: int) -> List:
     # only choose servers with < 70% load then top 10 of them
     for server in server_list:
         server_load = int(server[1])
-        # skip if server_load < 4, sometimes they don't work
-        if server_load < max_load and len(remaining_servers) < top_servers and server_load > 3:
-            remaining_servers.append(server)
-
-    if len(remaining_servers) < 1:    # if no servers under search criteria
-        logger.error("There are no servers that satisfy your criteria, please broaden your search.")
-        sys.exit(1)
+        # skip if server_load < 6, sometimes they don't work
+        if max_load >= server_load > 5:
+            if len(remaining_servers) < top_servers:
+                remaining_servers.append(server)
     return remaining_servers
diff --git a/openpyn/firewall.py b/openpyn/firewall.py
index c5a8a29..d6dc013 100644
--- a/openpyn/firewall.py
+++ b/openpyn/firewall.py
@@ -1,10 +1,16 @@
+import itertools
+import json
 import logging
+import os
 import subprocess
+from typing import Dict
 from typing import List
 
 import verboselogs
+from jsonschema import Draft4Validator
 
 from openpyn import root
+from openpyn import sudo_user
 
 verboselogs.install()
 logger = logging.getLogger(__package__)
@@ -14,9 +20,10 @@ def manage_ipv6(disable: bool) -> None:
     value = 1 if disable else 0
     try:
         subprocess.check_call(
-            ["sudo", "sysctl", "-w", "net.ipv6.conf.all.disable_ipv6={}".format(value)],
-            stdout=subprocess.DEVNULL)
-    except subprocess.SubprocessError:      # in case systemd is not used
+            ["sudo", "-u", sudo_user, "sysctl", "-w", "net.ipv6.conf.all.disable_ipv6={}".format(value)],
+            stdout=subprocess.DEVNULL,
+        )
+    except subprocess.SubprocessError:  # in case systemd is not used
         logger.warning("Cant disable/enable ipv6 using sysctl, are you even using systemd?")
 
 
@@ -24,22 +31,31 @@ def manage_ipv6(disable: bool) -> None:
 def clear_fw_rules() -> None:
     root.verify_root_access("Root access needed to modify 'iptables' rules")
     logger.info("Flushing iptables INPUT and OUTPUT chains AND Applying default Rules")
-    subprocess.call(["sudo", "iptables", "-F", "OUTPUT"])
+
+    subprocess.call(["sudo", "-u", sudo_user, "iptables", "-F", "OUTPUT"])
+
     # allow all outgoing traffic
-    subprocess.call("sudo iptables -P OUTPUT ACCEPT".split())
+    subprocess.call(["sudo", "-u", sudo_user, "iptables", "-P", "OUTPUT", "ACCEPT"])
+
+    subprocess.call(["sudo", "-u", sudo_user, "iptables", "-F", "INPUT"])
+    subprocess.call(["sudo", "-u", sudo_user, "iptables", "-A", "INPUT", "-i", "lo", "-j", "ACCEPT"])
+    subprocess.call(["sudo", "-u", sudo_user, "iptables", "-A", "OUTPUT", "-o", "lo", "-j", "ACCEPT"])
+    subprocess.call([
+        "sudo", "-u", sudo_user, "iptables",
+        "-A", "INPUT",
+        "-m", "conntrack",
+        "--ctstate", "ESTABLISHED,RELATED",
+        "-j", "ACCEPT"
+    ])
 
-    subprocess.call(["sudo", "iptables", "-F", "INPUT"])
-    subprocess.call(["sudo", "iptables", "-A", "INPUT", "-i", "lo", "-j", "ACCEPT"])
-    subprocess.call(["sudo", "iptables", "-A", "OUTPUT", "-o", "lo", "-j", "ACCEPT"])
-    subprocess.call(
-        "sudo iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT".split())
     # allow ICMP traffic
-    subprocess.call("sudo iptables -A INPUT -p icmp --icmp-type any -j ACCEPT".split())
+    subprocess.call(["sudo", "-u", sudo_user, "iptables", "-A", "INPUT", "-p", "icmp", "--icmp-type", "any", "-j", "ACCEPT"])
+
     # best practice, stops spoofing
-    subprocess.call("sudo iptables -A INPUT -s 127.0.0.0/8 -j DROP".split())
+    subprocess.call(["sudo", "-u", sudo_user, "iptables", "-A", "INPUT", "-s", "127.0.0.0/8", "-j", "DROP"])
+
     # drop anything else incoming
-    subprocess.call("sudo iptables -P INPUT DROP".split())
-    return
+    subprocess.call(["sudo", "-u", sudo_user, "iptables", "-P", "INPUT", "DROP"])
 
 
 NORDVPN_DNS = [
@@ -48,23 +64,30 @@ def clear_fw_rules() -> None:
 ]
 
 
+# flush input and output iptables rules.
+def flush_input_output() -> None:
+    root.verify_root_access("Root access needed to modify 'iptables' rules")
+    logger.info("Flushing ALL INPUT and OUTPUT Rules")
+    subprocess.call(["sudo", "-u", sudo_user, "iptables", "-F", "OUTPUT"])
+    subprocess.call(["sudo", "-u", sudo_user, "iptables", "-F", "INPUT"])
+
+
 def do_dns(iface: str, dest: str, what: str) -> None:
     # for pp in ("udp", "tcp"):
     pp = "udp"
-    cmd = ["sudo",
-           "iptables",
-           "-A", "OUTPUT",
-           "-p", pp,
-           "-d", dest, "--destination-port", "53",
-           "-j", what,
-           ]
+    cmd = [
+        "sudo", "-u", sudo_user, "iptables",
+        "-A", "OUTPUT",
+        "-p", pp,
+        "-d", dest, "--destination-port", "53",
+        "-j", what
+    ]
     if iface is not None:
         cmd.extend(["-o", iface])
     subprocess.check_call(cmd)
 
-# responsibility of update-systemd-resolved script now...
-
 
+# responsibility of update-systemd-resolved script now...
 def apply_dns_rules():
     root.verify_root_access("Root access needed to modify 'iptables' rules")
     for ndns in NORDVPN_DNS:
@@ -73,83 +96,293 @@ def apply_dns_rules():
     # do_dns(None, "0.0.0.0/0", "DROP")
 
 
-def apply_fw_rules(interfaces_details: List, vpn_server_ip: str, skip_dns_patch: bool) -> None:
+def apply_fw_rules(interfaces_details: List, vpn_server_ips: List, skip_dns_patch: bool) -> None:
     root.verify_root_access("Root access needed to modify 'iptables' rules")
 
-    # Empty the INPUT and OUTPUT chain of any current rules
-    subprocess.check_call(["sudo", "iptables", "-F", "OUTPUT"])
-    subprocess.check_call(["sudo", "iptables", "-F", "INPUT"])
-
     apply_dns_rules()
     logger.notice("Temporarily disabling ipv6 to prevent leakage")
     manage_ipv6(disable=True)
 
-    # Allow all traffic out over the vpn tunnel
+    # allow all traffic out over the VPN tunnel
     # except for DNS, which is handled by systemd-resolved script
-    # NOTE: that def helped with leaky DNS queries, nothing in wireshark too
+    # NOTE: that def helped with leaky DNS queries, nothing in Wireshark too
     # weird that ping ya.ru was showing "operation not permitted"
     subprocess.check_call([
-        "sudo", "iptables",
+        "sudo", "-u", sudo_user, "iptables",
         "-A", "OUTPUT",
         "-o", "tun+",
         # "-p", "all", "-d", "0.0.0.0/0", "!", "--dport", "53",
         "-p", "all", "-d", "0.0.0.0/0",
         "-j", "ACCEPT"
     ])
+
     # accept traffic that comes through tun that you connect to
-    subprocess.check_call(
-        "sudo iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED\
-         -i tun+ -j ACCEPT".split())
+    subprocess.check_call([
+        "sudo", "-u", sudo_user, "iptables",
+        "-A", "INPUT",
+        "-m", "conntrack",
+        "--ctstate", "ESTABLISHED,RELATED",
+        "-i", "tun+",
+        "-j", "ACCEPT"
+    ])
 
     for interface in interfaces_details:
         if len(interface) != 3:
             continue  # TODO what does that mean?
         iname = interface[0]
 
-        if len(iname) == 0:
+        if not iname:
             print("WARNING: empty {}".format(interface))
             continue
 
-        # allow access to vpn_server_ip
-        subprocess.check_call(
-            ["sudo", "iptables", "-A", "OUTPUT", "-o", iname,
-                "-d", vpn_server_ip, "-j", "ACCEPT"])
-        # talk to the vpnServer ip to connect to it
-        subprocess.check_call(
-            ["sudo", "iptables", "-A", "INPUT", "-m", "conntrack",
-                "--ctstate", "ESTABLISHED,RELATED", "-i", iname,
-                "-s", vpn_server_ip, "-j", "ACCEPT"])
+        # Allow currently chosen vpn_server_ips
+        for vpn_server_ip in vpn_server_ips:
+            # allow access to vpn_server_ip
+            subprocess.check_call([
+                "sudo", "-u", sudo_user, "iptables",
+                "-A", "OUTPUT",
+                "-o", iname,
+                "-d", vpn_server_ip,
+                "-j", "ACCEPT"
+            ])
 
-        # allow access to internal ip range
-        # print("internal ip with range", interface[2])
-        subprocess.check_call(
-            ["sudo", "iptables", "-A", "OUTPUT", "-o", iname, "-d",
-                interface[2], "-j", "ACCEPT"])
-        subprocess.check_call(
-            ["sudo", "iptables", "-A", "INPUT", "-m", "conntrack",
-                "--ctstate", "ESTABLISHED,RELATED", "-i", iname,
-                "-s", interface[2], "-j", "ACCEPT"])
+            # talk to the vpn_server_ip to connect to it
+            subprocess.check_call([
+                "sudo", "-u", sudo_user, "iptables",
+                "-A", "INPUT",
+                "-m", "conntrack",
+                "--ctstate", "ESTABLISHED,RELATED",
+                "-i", iname,
+                "-s", vpn_server_ip,
+                "-j", "ACCEPT"
+            ])
+
+        # allow access to internal IP range
+        # print("internal IP with range", interface[2])
+        subprocess.check_call([
+            "sudo", "-u", sudo_user, "iptables",
+            "-A", "OUTPUT",
+            "-o", iname,
+            "-d", interface[2],
+            "-j", "ACCEPT"
+        ])
+
+        subprocess.check_call([
+            "sudo", "-u", sudo_user, "iptables",
+            "-A", "INPUT",
+            "-m", "conntrack",
+            "--ctstate", "ESTABLISHED,RELATED",
+            "-i", iname,
+            "-s", interface[2],
+            "-j", "ACCEPT"
+        ])
 
-    # Allow loopback traffic
-    subprocess.check_call("sudo iptables -A INPUT -i lo -j ACCEPT".split())
-    subprocess.check_call("sudo iptables -A OUTPUT -o lo -j ACCEPT".split())
+    # allow loopback traffic
+    subprocess.check_call(["sudo", "-u", sudo_user, "iptables", "-A", "INPUT", "-i", "lo", "-j", "ACCEPT"])
+    subprocess.check_call(["sudo", "-u", sudo_user, "iptables", "-A", "OUTPUT", "-o", "lo", "-j", "ACCEPT"])
 
     # best practice, stops spoofing
-    subprocess.check_call("sudo iptables -A INPUT -s 127.0.0.0/8 -j DROP".split())
+    subprocess.check_call(["sudo", "-u", sudo_user, "iptables", "-A", "INPUT", "-s", "127.0.0.0/8", "-j", "DROP"])
 
-    # Default action if no other rules match
-    subprocess.check_call("sudo iptables -P OUTPUT DROP".split())
-    subprocess.check_call("sudo iptables -P INPUT DROP".split())
-    return
+    # default action if no other rules match
+    subprocess.check_call(["sudo", "-u", sudo_user, "iptables", "-P", "OUTPUT", "DROP"])
+    subprocess.check_call(["sudo", "-u", sudo_user, "iptables", "-P", "INPUT", "DROP"])
 
 
-# open sepecified ports for devices in the local network
+# Open specified ports for devices in the local network
 def internally_allow_ports(interfaces_details: List, internally_allowed: List) -> None:
     for interface in interfaces_details:
         # if interface is active with an IP in it, and not "tun*"
         if len(interface) == 3 and "tun" not in interface[0]:
-            # Allow the specified ports on internal network
+            # allow the specified ports on internal network
             for port in internally_allowed:
-                subprocess.call(
-                    ("sudo iptables -A INPUT -p tcp --dport " + port + " -i " +
-                        interface[0] + " -s " + interface[2] + " -j ACCEPT").split())
+                subprocess.call([
+                    "sudo", "-u", sudo_user, "iptables",
+                    "-A", "INPUT",
+                    "-p", "tcp",
+                    "--dport", port,
+                    "-i", interface[0],
+                    "-s", interface[2],
+                    "-j", "ACCEPT"
+                ])
+
+
+# Open all ports for devices in the local network
+def internally_allow_all(interfaces_details: List) -> None:
+    for interface in interfaces_details:
+        # if interface is active with an IP in it, and not "tun*"
+        if len(interface) == 3 and "tun" not in interface[0]:
+            subprocess.call([
+                "sudo", "-u", sudo_user, "iptables",
+                "-A", "INPUT",
+                "-i", interface[0],
+                "-s", interface[2],
+                "-j", "ACCEPT"
+            ])
+
+
+# Converts the allwed ports config to a series of iptable rules and applies them
+def apply_allowed_port_rules(interfaces_details: List, allowed_ports_config: List) -> bool:
+
+    if not validate_allowed_ports_json(allowed_ports_config):
+        return False
+
+    root.verify_root_access("Root access needed to pre load fire wall rules")
+
+    DEFAULT_PORT_CONFIG = {"internal": True, "protocol": "tcp", "allowed_ip_range": None}
+
+    # Merge default config with existing config
+    allowed_ports_config = [{**DEFAULT_PORT_CONFIG, **port_config} for port_config in allowed_ports_config]
+
+    ip_table_rules = []
+
+    for port_config in allowed_ports_config:
+        # Get perms for the connection type
+        port_protocol_permiatations = []
+
+        if port_config["protocol"] in ["tcp", "both"]:
+            port_protocol_permiatations.append("tcp")
+
+        if port_config["protocol"] in ["udp", "both"]:
+            port_protocol_permiatations.append("udp")
+
+        # Create the flags for the port range / port
+        if "-" in str(port_config["port"]):
+            port_range = port_config["port"].split("-")
+            port_flag = "--match multiport --dports {0}:{1}".format(*port_range)
+        else:
+            port_flag = "--dport {0}".format(port_config["port"])
+
+        for interface, port_type in itertools.product(interfaces_details, port_protocol_permiatations):
+            # Skip any tunnel interfaces that might be invalid
+            if len(interface) != 3 or "tun" in interface[0]:
+                continue
+
+            ip_flag = ""
+            ip_ranges = []
+
+            if port_config["internal"]:
+                ip_ranges.append(interface[2])
+
+            if port_config["allowed_ip_range"] is not None:
+                if isinstance(port_config["allowed_ip_range"], list):
+                    ip_ranges += port_config["allowed_ip_range"]
+                else:
+                    ip_ranges.append(port_config["allowed_ip_range"])
+
+            if ip_ranges != []:
+                ip_flag = " -s " + ",".join(ip_ranges)
+
+            ip_table_rules.append(
+                "sudo -u {user} iptables -A INPUT -p {port_type} {port_flag} -i {interface}{ip_flag} -j ACCEPT".format(
+                    user=sudo_user, port_type=port_type, port_flag=port_flag, interface=interface[0], ip_flag=ip_flag
+                )
+            )
+
+    for rule in ip_table_rules:
+        subprocess.call(rule.split(" "))
+
+    return True
+
+
+# Load allowed ports config from path (does not include validation)
+def load_allowed_ports(path_to_allowed_ports: str) -> bool:
+    # Ensure paths are resolved to real paths
+    path_to_allowed_ports = os.path.realpath(path_to_allowed_ports)
+
+    if not os.path.isfile(path_to_allowed_ports):
+        logger.warn("Cannot preload allowed ports: file {0} does not exist".format(path_to_allowed_ports))
+        return False
+
+    try:
+        with open(path_to_allowed_ports, "rt") as file_handle:
+            try:
+                allowed_ports_config = json.load(file_handle)
+            except json.JSONDecodeError as json_decode_error:
+                logger.error(
+                    "Failed to decode allowed ports JSON Error at line {line}:{col} {msg} ".format(
+                        line=json_decode_error.lineno, col=json_decode_error.colno, msg=json_decode_error.msg
+                    )
+                )
+                return False
+
+    except EnvironmentError as file_read_error:
+        logger.error(
+            'Cannot preload allowed ports: failed to load "{filename}" {strerr}'.format(
+                filename=file_read_error.filename, strerr=file_read_error.strerror
+            )
+        )
+
+    return allowed_ports_config
+
+
+# Validates if the allowed ports JSON is valid before loading it
+def validate_allowed_ports_json(allowed_ports_config: Dict) -> bool:
+    validation_schema = {
+        "description": "Root config node",
+        "type": "array",
+        "items": {
+            "type": "object",
+            "properties": {
+                "port": {
+                    "anyOf": [
+                        {
+                            "name": "Port number",
+                            "type": "integer",
+                            "minimum": 0,
+                            "maximum": 65535
+                        },
+                        {
+                            "type": "string",
+                            "pattern": "^\\d{1,5}(-\\d{1,5})?$"
+                        }
+                    ]
+                },
+                "protocol": {
+                    "type": "string",
+                    "pattern": "^(tcp|ip|both)$"
+                },
+                "internal": {
+                    "type": "boolean",
+                },
+                "allowed_ip_range": {
+                    "anyOf": [
+                        {
+                            "$ref": "#/definitions/ip_address_block"
+                        },
+                        {
+                            "items": {"$ref": "#/definitions/ip_address_block"},
+                            "uniqueItems": True
+                        }
+                    ]
+
+                }
+            },
+            "required": ["port"]
+        },
+        "definitions": {
+            "ip_address_block": {
+                "type": "string",
+                "pattern": "^([0-9]{1,3}\.){3}[0-9]{1,3}(\/([0-9]|[1-2][0-9]|3[0-2]))?$"
+            }
+        }
+    }
+
+    # Create the config validator
+    allowed_ports_config_validator = Draft4Validator(validation_schema)
+
+    # If the passed config is not valid enumerate errors and print them in human readable form
+    if not allowed_ports_config_validator.is_valid(allowed_ports_config):
+
+        error_message = "Errors were raise when validating the allowed ports config:"
+        # Retrieve all validation errors yielded in schema
+        for validation_error in allowed_ports_config_validator.iter_errors(allowed_ports_config):
+            error_message += "\n\nError at root.{0} in config: {1}".format(
+                ".".join([str(part) for part in validation_error.absolute_path]), validation_error.message
+            )
+
+        logger.error(error_message)
+
+        return False
+
+    return True
diff --git a/openpyn/initd.py b/openpyn/initd.py
index a2135c2..38fbc02 100644
--- a/openpyn/initd.py
+++ b/openpyn/initd.py
@@ -13,59 +13,69 @@
 logger = logging.getLogger(__package__)
 
 
-def install_service() -> bool:
+def install_service() -> None:
     if not sys.__stdin__.isatty():
-        logger.critical("Please run %s in interactive mode", __name__)
-        return 1
+        raise RuntimeError("Please run %s in interactive mode" % __name__)
 
-    openpyn_options = input("Enter Openpyn options to be stored in initd \
-service file (/opt/etc/init.d/S23openpyn, \
-Default(Just Press Enter) is, uk : ") or "uk"
+    openpyn_options = (
+        input(
+            "\nEnter Openpyn options to be stored in initd service file (/opt/etc/init.d/S23openpyn, Default(Just Press"
+            " Enter) is, uk : "
+        )
+        or "uk"
+    )
 
     # regex used
-    # (, help).+' --> “”
-    # (\n).+       ' --> “'”
-    # (\n).+       ac --> “ ac”
-
-    parser = argparse.ArgumentParser(add_help=False)
-    # parser.add_argument('--init')
-    parser.add_argument('-s', '--server')
-    parser.add_argument('--tcp', action='store_true')
-    parser.add_argument('-c', '--country-code', type=str)
-    parser.add_argument('country', nargs='?')
-    parser.add_argument('-a', '--area', type=str)
-    parser.add_argument('-d', '--daemon', action='store_true')
-    parser.add_argument('-m', '--max-load', type=int, default=70)
-    parser.add_argument('-t', '--top-servers', type=int, default=10)
-    parser.add_argument('-p', '--pings', type=str, default="5")
-    # parser.add_argument('-k', '--kill', action='store_true')
-    # parser.add_argument('-x', '--kill-flush', action='store_true')
-    # parser.add_argument('--update', action='store_true')
-    parser.add_argument('--skip-dns-patch', dest='skip_dns_patch')
-    parser.add_argument('-f', '--force-fw-rules')
-    parser.add_argument('--allow', dest='internally_allowed')
-    # parser.add_argument('-l', '--list', dest="list_servers", type=str, nargs='?', default="nope")
-    parser.add_argument('--silent')
-    parser.add_argument('--p2p')
+    # .*add_argument_group.*\n --> ""
+    # \, help='(.|\\\n)+', --> ","
+    # \, help='(.|\\\n)+' --> ""
+    # (\n).+       ' --> "'"
+    # (\n).+       ac --> " ac"
+
+    parser = argparse.ArgumentParser(add_help=False, allow_abbrev=False)
+    parser.add_argument('--allow', dest='internally_allowed', nargs='+')
+    # parser.add_argument('--allow-config', dest='internally_allowed_config')
+    parser.add_argument('--allow-config-json', dest='internally_allowed_config_json')
+    parser.add_argument('--allow-locally', dest='allow_locally', action='store_true')
+    parser.add_argument('--anti-ddos', dest='anti_ddos', action='store_true')
     parser.add_argument('--dedicated', action='store_true')
-    parser.add_argument('--tor', dest='tor_over_vpn', action='store_true')
     parser.add_argument('--double', dest='double_vpn', action='store_true')
-    parser.add_argument('--anti-ddos', dest='anti_ddos', action='store_true')
     parser.add_argument('--netflix', dest='netflix', action='store_true')
+    parser.add_argument('--p2p', action='store_true')
+    parser.add_argument('--silent', action='store_true')
+    parser.add_argument('--skip-dns-patch', dest='skip_dns_patch', action='store_true')
+    parser.add_argument('--tcp', action='store_true')
     parser.add_argument('--test', action='store_true')
-    parser.add_argument('-n', '--nvram', type=str, default="5")
-    parser.add_argument('-o', '--openvpn-options', dest='openvpn_options', type=str)
+    parser.add_argument('--tor', dest='tor_over_vpn', action='store_true')
+    parser.add_argument('--update', action='store_true')
+    parser.add_argument('-a', '--area', type=str)
+    parser.add_argument('-c', '--country-code', type=str)
+    parser.add_argument('-f', '--force-fw-rules', action='store_true')
     parser.add_argument('-loc', '--location', nargs=2, type=float)
+    parser.add_argument('-m', '--max-load', type=int, default=70)
+    parser.add_argument('-n', '--nvram', type=str)
+    parser.add_argument('-o', '--openvpn-options', dest='openvpn_options', type=str)
+    parser.add_argument('-s', '--server', type=str)
+    parser.add_argument('-t', '--top-servers', type=int, default=10)
+    parser.add_argument('country', nargs='?')
 
     try:
         args = parser.parse_args(openpyn_options.split())
     except SystemExit as e:
         if e.code == 2:
-            openpyn_options = input("Enter Openpyn options to be stored in initd \
-service file (/opt/etc/init.d/S23openpyn, \
-Default(Just Press Enter) is, uk : ") or "uk"
+            openpyn_options = (
+                input(
+                    "\nEnter Openpyn options to be stored in initd service file (/opt/etc/init.d/S23openpyn, Default(Just"
+                    " Press Enter) is, uk : "
+                )
+                or "uk"
+            )
             args = parser.parse_args(openpyn_options.split())
 
+    if args.update:
+        update_service("--update")
+        return
+
     server = args.server
     country_code = args.country_code
     country = args.country
@@ -73,7 +83,6 @@ def install_service() -> bool:
     tcp = args.tcp
     max_load = args.max_load
     top_servers = args.top_servers
-    pings = args.pings
     force_fw_rules = args.force_fw_rules
     p2p = args.p2p
     dedicated = args.dedicated
@@ -83,6 +92,7 @@ def install_service() -> bool:
     netflix = args.netflix
     test = args.test
     internally_allowed = args.internally_allowed
+    internally_allowed_config_json = args.internally_allowed_config_json
     skip_dns_patch = args.skip_dns_patch
     silent = args.silent
     nvram = args.nvram
@@ -92,22 +102,30 @@ def install_service() -> bool:
     detected_os = sys.platform
     if detected_os == "linux":
         if subprocess.check_output(["/bin/uname", "-o"]).decode(sys.stdout.encoding).strip() == "ASUSWRT-Merlin":
+            force_fw_rules = False
+            internally_allowed = None
             silent = True
             skip_dns_patch = True
         elif os.path.exists("/etc/openwrt_release"):
+            force_fw_rules = False
+            internally_allowed = None
             silent = True
             skip_dns_patch = True
+            nvram = None
+        else:
+            nvram = None
 
     openpyn_options = ""
 
     # if only positional argument used
     if country_code is None and server is None:
-        country_code = country      # consider the positional arg e.g "us" same as "-c us"
-    # if either "-c" or positional arg f.e "au" is present
+        # consider the positional arg e.g "us" same as "-c us"
+        country_code = country
 
+    # if either "-c" or positional arg f.e "au" is present
     if country_code:
-        if len(country_code) > 2:   # full country name
-            # get the country_code from the full name
+        # if full name of the country supplied get country_code
+        if len(country_code) > 2:
             country_code = api.get_country_code(full_name=country_code)
         country_code = country_code.lower()
         openpyn_options += country_code
@@ -123,8 +141,6 @@ def install_service() -> bool:
         openpyn_options += " --max-load " + str(max_load)
     if top_servers:
         openpyn_options += " --top-servers " + str(top_servers)
-    if pings:
-        openpyn_options += " --pings " + str(pings)
     if force_fw_rules:
         openpyn_options += " --force-fw-rules"
     if p2p:
@@ -146,37 +162,42 @@ def install_service() -> bool:
         for port_number in internally_allowed:
             open_ports += " " + port_number
         openpyn_options += " --allow" + open_ports
+    if internally_allowed_config_json:
+        # Assume at this stage the JSON has been passed as string so it can be directly loaded
+        openpyn_options += " --allow-config-json=" + internally_allowed_config_json
     if skip_dns_patch:
         openpyn_options += " --skip-dns-patch"
     if silent:
         openpyn_options += " --silent"
     if nvram:
-        openpyn_options += " --nvram " + str(nvram)
+        openpyn_options += " --nvram " + nvram
     if openvpn_options:
         openpyn_options += " --openvpn-options '" + openvpn_options + "'"
     if location:
         openpyn_options += " --location " + str(location[1]) + " " + str(location[2])
 
     update_service(openpyn_options)
-    return 0
 
 
 def update_service(openpyn_options: str, run=False) -> None:
-    logger.debug(openpyn_options)
+    # logger.debug(openpyn_options)
 
     os.chmod("/opt/etc/init.d/S23openpyn", 0o755)
     for line in fileinput.FileInput("/opt/etc/init.d/S23openpyn", inplace=1):
         sline = line.strip().split("=")
         if sline[0].startswith("ARGS"):
-            sline[1] = "\"" + openpyn_options + "\""
-        line = '='.join(sline)
-        logger.debug(line)
+            sline[1] = '"' + openpyn_options + '"'
+        line = "=".join(sline)
+        print(line)
 
-    logger.notice("The Following config has been saved in S23openpyn. \
-You can Start it or/and Stop it with: '/opt/etc/init.d/S23openpyn start', \
-'/opt/etc/init.d/S23openpyn stop' \n")
+    logger.notice(
+        "The Following config has been saved in S23openpyn. You can Start it or/and Stop it with:"
+        " '/opt/etc/init.d/S23openpyn start', '/opt/etc/init.d/S23openpyn stop' \n"
+    )
 
     if run:
-        logger.notice("Started Openpyn by running '/opt/etc/init.d/S23openpyn start'\n\
-To check VPN status, run '/opt/etc/init.d/S23openpyn check'")
-        subprocess.run(["/opt/etc/init.d/S23openpyn", "start"])
+        logger.notice(
+            "Started Openpyn by running '/opt/etc/init.d/S23openpyn start'\nTo check VPN status, run"
+            " '/opt/etc/init.d/S23openpyn check'"
+        )
+        subprocess.run(["/opt/etc/init.d/S23openpyn", "start"], check=False)
diff --git a/openpyn/locations.py b/openpyn/locations.py
index b8b95db..9cdd74c 100644
--- a/openpyn/locations.py
+++ b/openpyn/locations.py
@@ -1,8 +1,8 @@
 import logging
 import random
-import sys
 import time
-from typing import Dict, List, Set
+from typing import Dict
+from typing import List
 
 import requests
 import verboselogs
@@ -10,12 +10,14 @@
 verboselogs.install()
 logger = logging.getLogger(__package__)
 
-user_agents = ['Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:58.0) Gecko/20100101 Firefox/58.0',
-               'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:59.0) Gecko/20100101 Firefox/59.0',
-               'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:59.0) Gecko/20100101 Firefox/59.0',
-               'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:58.0) Gecko/20100101 Firefox/58.0',
-               'Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:59.0) Gecko/20100101 Firefox/59.0',
-               'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:59.0) Gecko/20100101 Firefox/59.0']
+user_agents = [
+    "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:58.0) Gecko/20100101 Firefox/58.0",
+    "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:59.0) Gecko/20100101 Firefox/59.0",
+    "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:59.0) Gecko/20100101 Firefox/59.0",
+    "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:58.0) Gecko/20100101 Firefox/58.0",
+    "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:59.0) Gecko/20100101 Firefox/59.0",
+    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:59.0) Gecko/20100101 Firefox/59.0",
+]
 
 
 # takes server list outputs locations (each only once) the servers are in.
@@ -25,13 +27,15 @@ def get_unique_locations(list_of_servers: List) -> List:
     locations_count = 0
 
     for aServer in list_of_servers:
-        latLongDic = {"lat": aServer["location"]["lat"], "long": aServer["location"]["long"]}
-        if latLongDic not in unique_locations:
-            unique_locations.append(latLongDic)
+        lat_long_dic = {"lat": aServer["location"]["lat"], "long": aServer["location"]["long"]}
+        if lat_long_dic not in unique_locations:
+            unique_locations.append(lat_long_dic)
     # logger.debug(unique_locations)
     for eachLocation in unique_locations:
-        user_agent = {'User-Agent': user_agents[locations_count % 6],
-                      'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8'}
+        user_agent = {
+            "User-Agent": user_agents[locations_count % 6],
+            "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
+        }
         geo_address_list = get_locations(eachLocation, user_agent)
         time.sleep(random.randrange(1, 5, 1) * 0.1)
         resolved_locations.append(geo_address_list)
@@ -44,11 +48,8 @@ def get_locations(location_dic: Dict, req_headers: str) -> List:
     latitude = location_dic["lat"]
     longitude = location_dic["long"]
 
-    url = 'https://nominatim.openstreetmap.org/reverse?format=jsonv2'
-    params = "&lat={lat}&lon={lon}".format(
-        lat=latitude,
-        lon=longitude
-    )
+    url = "https://nominatim.openstreetmap.org/reverse?format=jsonv2"
+    params = "&lat={lat}&lon={lon}".format(lat=latitude, lon=longitude)
     final_url = url + params
     # logger.debug("req_headers %s", req_headers)
     r = requests.get(final_url, headers=req_headers)
@@ -56,12 +57,13 @@ def get_locations(location_dic: Dict, req_headers: str) -> List:
     name_list = []
     try:
         response = r.json()
-        results = response['address']
+        results = response["address"]
         # logger.debug(results)
     except IndexError:
-        logger.error("IndexError: Looks like you have reached API's daily request limit. \
-No location data for you :( you could restart your router to get a new IP.")
-        sys.exit()
+        raise RuntimeError(
+            "IndexError: Looks like you have reached APIs daily request limit. No location data for you :( you could restart"
+            " your router to get a new IP."
+        ) from None
 
     geo_address_list.append(location_dic)
 
@@ -82,5 +84,5 @@ def get_locations(location_dic: Dict, req_headers: str) -> List:
         if key == "state_district":
             name_list.append(results["state_district"])
     geo_address_list.insert(2, name_list)
-    logger.debug(geo_address_list)
+    # logger.debug(geo_address_list)
     return geo_address_list
diff --git a/openpyn/management/management.py b/openpyn/management/management.py
index 586681c..565c71c 100644
--- a/openpyn/management/management.py
+++ b/openpyn/management/management.py
@@ -2,7 +2,6 @@
 
 import argparse
 import logging
-import logging.handlers
 import os
 import socket
 import sys
@@ -11,16 +10,16 @@
 
 import verboselogs
 
-from openpyn import log_folder, log_format
+from openpyn import log_folder
+from openpyn import log_format
 
 verboselogs.install()
 logger = logging.getLogger(__package__)
 
-logger.setLevel(logging.VERBOSE)
+logger.setLevel(logging.INFO)
 
 # Add another rotating handler to log to .log files
-file_handler = logging.handlers.TimedRotatingFileHandler(
-    log_folder + '/openpyn-notifications.log', when='W0', interval=4)
+file_handler = logging.FileHandler(log_folder + "/openpyn-notifications.log")
 file_handler_formatter = logging.Formatter(log_format)
 file_handler.setFormatter(file_handler_formatter)
 logger.addHandler(file_handler)
@@ -32,7 +31,8 @@ def socket_connect(server, port):
     return s
 
 
-def show(do_notify):
+# pylint: disable=C0415,E0401
+def show(do_notify) -> bool:
     detected_os = sys.platform
     sleep(1)
     if do_notify:
@@ -40,38 +40,38 @@ def show(do_notify):
             import gi
         except ImportError:
             logger.error("Python3-gi not found, expected on a non-gui os")
-            sys.exit()
+            return 1
         try:
-            gi.require_version('Notify', '0.7')
+            gi.require_version("Notify", "0.7")
             from gi.repository import Notify
         except ValueError:
             logger.error("Notify 0.7 not found, expected on a non-gui os")
-            sys.exit()
+            return 1
 
         if detected_os == "linux":
             Notify.init("openpyn")
 
     while True:
         try:
-            s = socket_connect('localhost', 7015)
+            s = socket_connect("localhost", 7015)
         except ConnectionRefusedError:
             sleep(3)
             continue
         break
+
     try:
-        # Create the notification object and show once
+        # create the notification object and show once
         summary = "Openpyn"
         body = "Initiating connection (If stuck here, try again)"
         if detected_os == "linux":
             if do_notify:
                 notification = Notify.Notification.new(summary, body)
                 notification.show()
-            logger.warning("{} {}".format(summary, body))
+            logger.warning("%s %s", summary, body)
         elif detected_os == "darwin":
             if do_notify:
-                notification = "\"{}\" with title \"{}\"".format(body, summary)
+                notification = '"{}" with title "{}"'.format(body, summary)
                 os.system("""osascript -e 'display notification {}'""".format(notification))
-        server_name = ""
         last_status_UP = False
         # s.send(str.encode("state on"))
         while True:
@@ -79,24 +79,24 @@ def show(do_notify):
             data_str = repr(data)
             # logger.debug(data_str)
             # if 'UPDOWN:DOWN' or 'UPDOWN:UP' or 'INFO' in data_str:
-            if 'UPDOWN:UP' in data_str:
+            if "UPDOWN:UP" in data_str:
                 last_status_UP = True
-                # logger.debug('Received AN UP')
+                # logger.debug("Received AN UP")
 
-            if 'UPDOWN:DOWN' in data_str:
+            if "UPDOWN:DOWN" in data_str:
                 last_status_UP = False
 
-                # logger.debug('Received A DOWN' + data_str)
+                # logger.debug("Received A DOWN" + data_str)
                 body = "Connection Down, Disconnected."
                 if detected_os == "linux":
                     if do_notify:
                         notification.update(summary, body)
-                        # Show again
+                        # show again
                         notification.show()
-                    logger.info("{} {}".format(summary, body))
+                    logger.info("%s %s", summary, body)
                 elif detected_os == "darwin":
                     if do_notify:
-                        notification = "\"{}\" with title \"{}\"".format(body, summary)
+                        notification = '"{}" with title "{}"'.format(body, summary)
                         os.system("""osascript -e 'display notification {}'""".format(notification))
 
             server_name_location = data_str.find("common_name=")
@@ -110,10 +110,10 @@ def show(do_notify):
                     if do_notify:
                         notification.update(summary, body)
                         notification.show()
-                    logger.info("{} {}".format(summary, body))
+                    logger.info("%s %s", summary, body)
                 elif detected_os == "darwin":
                     if do_notify:
-                        notification = "\"{}\" with title \"{}\"".format(body, summary)
+                        notification = '"{}" with title "{}"'.format(body, summary)
                         os.system("""osascript -e 'display notification {}'""".format(notification))
 
             # break of data stream is empty
@@ -122,49 +122,46 @@ def show(do_notify):
 
     except KeyboardInterrupt:
         body = "Disconnected, Bye."
+        logger.info("%s %s", summary, body)
         if detected_os == "linux":
             if do_notify:
                 notification.update(summary, body)
                 notification.show()
-            logger.info("{} {}".format(summary, body))
         elif detected_os == "darwin":
             if do_notify:
-                notification = "\"{}\" with title \"{}\"".format(body, summary)
+                notification = '"{}" with title "{}"'.format(body, summary)
                 os.system("""osascript -e 'display notification {}'""".format(notification))
-        logger.info('Shutting down safely, please wait until process exits')
     except ConnectionResetError:
         body = "Disconnected, Bye. (ConnectionReset)"
+        logger.info("%s %s", summary, body)
         if detected_os == "linux":
             if do_notify:
                 notification.update(summary, body)
                 notification.show()
-            logger.info("{} {}".format(summary, body))
         elif detected_os == "darwin":
             if do_notify:
-                notification = "\"{}\" with title \"{}\"".format(body, summary)
+                notification = '"{}" with title "{}"'.format(body, summary)
                 os.system("""osascript -e 'display notification {}'""".format(notification))
-        sys.exit()
 
     s.close()
-    return
+    return 0
 
 
 def parse_args(argv: List[str]) -> argparse.Namespace:
     parser = argparse.ArgumentParser(
         description="Management interface for openpyn to display notifications and log"
-        "them to /var/log/openpyn/openpyn-notifications.log ")
-    parser.add_argument(
-        '--do-notify', dest='do_notify', help='try to display desktop notifications.',
-        action='store_true')
+        " them to {}/openpyn-notifications.log".format(log_folder),
+        allow_abbrev=False,
+    )
+    parser.add_argument("--do-notify", dest="do_notify", help="try to display desktop notifications.", action="store_true")
 
     return parser.parse_args(argv[1:])
 
 
 def main() -> bool:
-
     args = parse_args(sys.argv)
     return show(args.do_notify)
 
 
-if __name__ == '__main__':
+if __name__ == "__main__":
     sys.exit(main())
diff --git a/openpyn/openpyn.py b/openpyn/openpyn.py
old mode 100755
new mode 100644
index 86a3ef2..40dee23
--- a/openpyn/openpyn.py
+++ b/openpyn/openpyn.py
@@ -1,19 +1,35 @@
 #!/usr/bin/env python3
 
 import argparse
+import io
+import json
+import locale
 import logging
 import logging.handlers
 import os
+import shlex
 import shutil
+import signal
 import subprocess
 import sys
+import tempfile
 import time
-from typing import List, Set
+import zipfile
+from email.utils import parsedate
+from pathlib import Path
+from time import sleep
+from typing import List
+from typing import Set
 
 import coloredlogs
+import requests
 import verboselogs
-from colorama import Fore, Style
+from colorama import Fore
+from colorama import Style
+from tqdm import tqdm
 
+from openpyn import __basefilepath__
+from openpyn import __version__
 from openpyn import api
 from openpyn import asus
 from openpyn import credentials
@@ -21,9 +37,15 @@
 from openpyn import firewall
 from openpyn import initd
 from openpyn import locations
+from openpyn import log_folder
+from openpyn import log_format
+from openpyn import ovpn_folder
 from openpyn import root
+from openpyn import routes
+from openpyn import sudo_user
 from openpyn import systemd
-from openpyn import __basefilepath__, __version__, log_folder, log_format    # variables
+
+locale.setlocale(locale.LC_ALL, "")
 
 verboselogs.install()
 logger = logging.getLogger(__package__)
@@ -36,199 +58,221 @@ def parse_args(argv: List[str]) -> argparse.Namespace:
         (using current data from NordVPN website) with lowest latency from you. Find NordVPN \
         servers in a given country or city. Tunnels DNS traffic through the VPN which normally \
         (when using OpenVPN with NordVPN) goes through your ISP's DNS (still unencrypted, even if \
-        you use a third-party DNS servers) and completely compromises Privacy!")
+        you use a third-party DNS servers)",
+        allow_abbrev=False)
     parser.add_argument(
         '-v', '--version', action='version', version="openpyn " + __version__)
-    parser.add_argument(
-        '--init', help='Initialise, store/change credentials, download/update VPN config files,\
-        needs root "sudo" access.', action='store_true')
-    parser.add_argument(
-        '-s', '--server', type=str, help='server name, i.e. ca64 or au10')
-    parser.add_argument(
-        '--tcp', help='use port TCP-443 instead of the default UDP-1194',
-        action='store_true')
-    parser.add_argument(
-        '-c', '--country-code', type=str, help='Specify country code with 2 letters, i.e. au')
     # use nargs='?' to make a positional arg optional
     parser.add_argument(
-        'country', nargs='?', help='Country code can also be specified without "-c,"\
-         i.e. "openpyn au"')
+        'country', nargs='?', help='Country code can also be specified without "-c,"i.e. "openpyn au"')
     parser.add_argument(
-        '-a', '--area', type=str, help='Specify area, city name or state e.g \
-        "openpyn au -a victoria" or "openpyn au -a \'sydney\'"')
+        '--init', help='Initialise, store/change credentials, download/update VPN config files,\
+        needs root "sudo" access.', action='store_true')
     parser.add_argument(
         '-d', '--daemon', help='Update and start systemd service openpyn.service,\
         running it as a background process, to check status "systemctl status openpyn"',
         action='store_true')
-    parser.add_argument(
-        '-m', '--max-load', type=int, default=70, help='Specify load threshold, \
-        rejects servers with more load than this, DEFAULT=70')
-    parser.add_argument(
-        '-t', '--top-servers', type=int, default=10, help='Specify the number of top \
-         servers to choose from the NordVPN\'s server list for the given country, these will be \
-         pinged, DEFAULT=10')
-    parser.add_argument(
-        '-p', '--pings', type=str, default="3", help='Specify number of pings \
-        to be sent to each server to determine quality, DEFAULT=3')
     parser.add_argument(
         '-k', '--kill', help='Kill any running OpenVPN process, very useful \
-        to kill openpyn process running in background with "-d" switch',
-        action='store_true')
+        to kill openpyn process running in background with "-d" switch', action='store_true')
     parser.add_argument(
-        '-x', '--kill-flush', help='Kill any running OpenVPN process, and flush iptables',
-        action='store_true')
+        '-x', '--kill-flush', help='Kill any running OpenVPN process, and flush iptables', action='store_true')
     parser.add_argument(
-        '--update', help='Fetch the latest config files from NordVPN\'s site',
-        action='store_true')
+        '--update', help='Fetch the latest config files from NordVPN\'s site', action='store_true')
     parser.add_argument(
         '--skip-dns-patch', dest='skip_dns_patch', help='Skips DNS patching,\
         leaves /etc/resolv.conf untouched. (Not recommended)', action='store_true')
-    parser.add_argument(
-        '-f', '--force-fw-rules', help='Enforce firewall rules to drop traffic when tunnel breaks\
-        , force disable DNS traffic going to any other interface', action='store_true')
-    parser.add_argument(
-        '--allow', dest='internally_allowed', help='To be used with "f" to allow ports \
-        but ONLY to INTERNAL IP RANGE. for example: you can use your PC as SSH, HTTP server \
-        for local devices (i.e. 192.168.1.* range) by "openpyn us --allow 22 80"', nargs='+')
-    parser.add_argument(
-        '-l', '--list', dest="list_servers", type=str, nargs='?', default="nope",
-        help='If no argument given prints all Country Names and Country Codes; \
-        If country code supplied ("-l us"): Displays all servers in that given\
-        country with their current load and OpenVPN support status. Works in \
-        conjunction with (-a | --area, and server types (--p2p, --tor) \
-        e.g "openpyn -l it --p2p --area milano"')
     parser.add_argument(
         '--silent', help='Do not try to send notifications. Use if "libnotify" or "gi"\
         are not available. Automatically used in systemd service file', action='store_true')
     parser.add_argument(
-        '--p2p', help='Only look for servers with "Peer To Peer" support', action='store_true')
-    parser.add_argument(
-        '--dedicated', help='Only look for servers with "Dedicated IP" support',
-        action='store_true')
-    parser.add_argument(
-        '--tor', dest='tor_over_vpn', help='Only look for servers with "Tor Over VPN" support',
-        action='store_true')
-    parser.add_argument(
-        '--double', dest='double_vpn', help='Only look for servers with "Double VPN" support',
-        action='store_true')
-    parser.add_argument(
-        '--anti-ddos', dest='anti_ddos', help='Only look for servers with "Anti DDoS" support',
-        action='store_true')
+        '--test', help='Simulation only, do not actually connect to the VPN server', action='store_true')
     parser.add_argument(
-        '--netflix', dest='netflix', help='Only look for servers that are optimised for "Netflix"',
-        action='store_true')
-    parser.add_argument(
-        '--test', help='Simulation only, do not actually connect to the VPN server',
-        action='store_true')
+        '-n', '--nvram', type=str, help='Specify client to save configuration to NVRAM (ASUSWRT-Merlin)')
     parser.add_argument(
-        '-n', '--nvram', type=str, help='Specify client to save configuration to \
-        NVRAM (ASUSWRT-Merlin)')
+        '-app', '--application', dest='app', help=argparse.SUPPRESS, action='store_true')
     parser.add_argument(
+        '-loc', '--location', nargs=2, type=float, metavar=('latitude', 'longitude'), help=argparse.SUPPRESS)
+
+    openvpn_internal_options = parser.add_argument_group(
+        "OpenVPN Options", "Configurable Options Being Passed Downed To OpenVPN")
+    openvpn_internal_options.add_argument(
+        '--no-redirect-gateway', dest='no_redirect', help='Don\'t set --redirect-gateway', action='store_true')
+    openvpn_internal_options.add_argument(
         '-o', '--openvpn-options', dest='openvpn_options', type=str, help='Pass through OpenVPN \
         options, e.g. openpyn uk -o \'--status /var/log/status.log --log /var/log/log.log\'')
-    parser.add_argument(
-        '-loc', '--location', nargs=2, type=float, metavar=('latitude', 'longitude'))
+
+    connect_options = parser.add_argument_group("Connect Options",
+                                                "Connect To A Specific Server Or Any In A Country; TCP or UDP")
+    connect_options.add_argument(
+        '-s', '--server', type=str, help='server name, i.e. ca64 or au10')
+    connect_options.add_argument(
+        '-c', '--country-code', type=str, help='Specify country code with 2 letters, i.e. au')
+    connect_options.add_argument(
+        '--tcp', help='use port TCP-443 instead of the default UDP-1194', action='store_true')
+
+    filter_options = parser.add_argument_group("Filter Options", "Find Specific Types Of Servers")
+    filter_options.add_argument(
+        '-a', '--area', type=str, help='Specify area, city name or state e.g \
+        "openpyn au -a victoria" or "openpyn au -a \'sydney\'"')
+    filter_options.add_argument(
+        '-m', '--max-load', type=int, default=70, help='Specify load threshold, \
+        rejects servers with more load than this, DEFAULT=%(default)s')
+    filter_options.add_argument(
+        '-t', '--top-servers', type=int, default=10, help='Specify the number of top \
+         servers to choose from the NordVPN\'s server list for the given country, these will be \
+         pinged, DEFAULT=%(default)s')
+    filter_options.add_argument(
+        '--p2p', help='Only look for servers with "Peer To Peer" support', action='store_true')
+    filter_options.add_argument(
+        '--dedicated', help='Only look for servers with "Dedicated IP" support', action='store_true')
+    filter_options.add_argument(
+        '--tor', dest='tor_over_vpn', help='Only look for servers with "Tor Over VPN" support', action='store_true')
+    filter_options.add_argument(
+        '--double', dest='double_vpn', help='Only look for servers with "Double VPN" support', action='store_true')
+    filter_options.add_argument(
+        '--anti-ddos', dest='anti_ddos', help='Only look for servers with "Obfuscated" support', action='store_true')
+    filter_options.add_argument(
+        '--netflix', dest='netflix', help='Only look for servers that are optimised for "Netflix"', action='store_true')
+
+    display_options = parser.add_argument_group("Display Options", "These Only Display Information")
+    display_options.add_argument(
+        '-l', '--list', dest='list_servers', type=str, nargs='?', default="nope",
+        help='If no argument given prints all Country Names and Country Codes; \
+        If country code supplied ("-l us"): Displays all servers in that given\
+        country with their current load and OpenVPN support status. Works in \
+        conjunction with (-a | --area, and server types (--p2p, --tor) \
+        e.g "openpyn -l it --p2p --area milano"')
+    display_options.add_argument(
+        '--status', dest='show_status', help='Show last change in connection status', action='store_true')
+    display_options.add_argument(
+        '--stats', dest='show_stats', help='Show OpenVPN connection stats', action='store_true')
+
+    fw_options = parser.add_argument_group("Firewall Options", "Firewall, KillSwitch and Route Options")
+    fw_options.add_argument(
+        '-f', '--force-fw-rules', help='Enforce firewall rules to drop traffic when tunnel breaks\
+        , force disable DNS traffic going to any other interface', action='store_true')
+    fw_options.add_argument('-r', '--add-route', dest="add_route", help='Add route to default-gateway; Needed to \
+        continue serving any service including SSH. Required on VPSs. To ensure it doesn\'t leak traffic use it with \
+        -f and --allow', action='store_true')
+    fw_options.add_argument(
+        '--allow-locally', dest='allow_locally', help='To be used with "-f" to allow input traffic \
+        on all ports from locally connected / INTERNAL IP SUBNET. for example 192.168.1.* range', action='store_true')
+    fw_options.add_argument(
+        '--allow', dest='internally_allowed', help='To be used with "-f" to allow TCP connections to given ports \
+        but ONLY to INTERNAL IP RANGE. for example: you can use your PC as SSH, HTTP server \
+        for local devices (i.e. 192.168.1.* range) by "openpyn us -f --allow 22 80"', nargs='+')
+    fw_options.add_argument(
+        '--allow-config', dest='internally_allowed_config', help='To be used with "-f" to allow a complex \
+        set of allow port rules. This option requires a path to a JSON file that contains the \
+        relevant config')
+    fw_options.add_argument(
+        '--allow-config-json', dest='internally_allowed_config_json', help='To be used with "-f" to allow a complex \
+        set of allow port rules. This option works the same as "--allow-config" option \
+        but accepts a JSON object as a string instead')
 
     return parser.parse_args(argv[1:])
 
 
 def main() -> bool:
-
     args = parse_args(sys.argv)
     return_code = run(
         args.init, args.server, args.country_code, args.country, args.area, args.tcp,
-        args.daemon, args.max_load, args.top_servers, args.pings,
+        args.daemon, args.max_load, args.top_servers,
         args.kill, args.kill_flush, args.update, args.list_servers,
-        args.force_fw_rules, args.p2p, args.dedicated, args.double_vpn,
+        args.force_fw_rules, args.add_route, args.allow_locally, args.p2p, args.dedicated, args.double_vpn,
         args.tor_over_vpn, args.anti_ddos, args.netflix, args.test, args.internally_allowed,
-        args.skip_dns_patch, args.silent, args.nvram, args.openvpn_options, args.location)
+        args.internally_allowed_config, args.internally_allowed_config_json, args.skip_dns_patch,
+        args.silent, args.nvram, args.app, args.location, args.no_redirect, args.openvpn_options,
+        args.show_status, args.show_stats)
     return return_code
 
 
 # run openpyn
-def run(init: bool, server: str, country_code: str, country: str, area: str, tcp: bool, daemon: bool,
-        max_load: int, top_servers: int, pings: str, kill: bool, kill_flush: bool, update: bool, list_servers: bool,
-        force_fw_rules: bool, p2p: bool, dedicated: bool, double_vpn: bool, tor_over_vpn: bool, anti_ddos: bool,
-        netflix: bool, test: bool, internally_allowed: List, skip_dns_patch: bool, silent: bool, nvram: str,
-        openvpn_options: str, location: float) -> bool:
-
-    if init:
-        initialise(log_folder)
-
+def run(init: bool, server: str, country_code: str, country: str, area: str, tcp: bool,
+        daemon: bool, max_load: int, top_servers: int,
+        kill: bool, kill_flush: bool, update: bool, list_servers: bool,
+        force_fw_rules: bool, add_route: bool, allow_locally: bool, p2p: bool, dedicated: bool, double_vpn: bool,
+        tor_over_vpn: bool, anti_ddos: bool, netflix: bool, test: bool, internally_allowed: List,
+        internally_allowed_config: str, internally_allowed_config_json: dict, skip_dns_patch: bool,
+        silent: bool, nvram: str, app: bool, location: float, no_redirect: bool, openvpn_options: str,
+        show_status: bool, show_stats: bool) -> bool:
     fieldstyles = {
-        'asctime': {'color': 'green'},
-        'hostname': {'color': 'magenta'},
-        'levelname': {'color': 'black', 'bold': True},
-        'name': {'color': 'blue'},
-        'programname': {'color': 'cyan'},
+        "asctime": {"color": "green"},
+        "hostname": {"color": "magenta"},
+        "levelname": {"color": "black", "bold": True},
+        "name": {"color": "blue"},
+        "programname": {"color": "cyan"},
     }
     levelstyles = {
-        'spam': {'color': 'green', 'faint': True},
-        'debug': {'color': 'green', 'bold': True},
-        'verbose': {'color': 'blue', 'bold': True},
-        'info': {},
-        'notice': {'color': 'magenta', 'bold': True},
-        'warning': {'color': 'yellow', 'bold': True},
-        'success': {'color': 'green', 'bold': True},
-        'error': {'color': 'red', 'bold': True},
-        'critical': {'color': 'white', 'background': 'red', 'bold': True}
+        "spam": {"color": "green", "faint": True},
+        "debug": {"color": "green", "bold": True},
+        "verbose": {"color": "blue", "bold": True},
+        "info": {},
+        "notice": {"color": "magenta", "bold": True},
+        "warning": {"color": "yellow", "bold": True},
+        "success": {"color": "green", "bold": True},
+        "error": {"color": "red", "bold": True},
+        "critical": {"color": "white", "background": "red", "bold": True},
     }
 
     logger.addHandler(logging.StreamHandler())
 
-    # if log folder doesnt exist, exit, "--init" creates it
-    if not os.path.exists(log_folder):
-        logger.error(
-            "Please initialise first by running 'sudo openpyn --init', then start using 'openpyn' without sudo")
-        return 1
-
-    # Add another rotating handler to log to .log files
-    # fix permissions if needed
-    for attempt in range(2):
-        try:
-            file_handler = logging.handlers.TimedRotatingFileHandler(
-                log_folder + '/openpyn.log', when='W0', interval=4)
-            file_handler_formatter = logging.Formatter(log_format)
-            file_handler.setFormatter(file_handler_formatter)
-            logger.addHandler(file_handler)
-        except PermissionError:
-            root.verify_root_access(
-                "Root access needed to set permissions of {}/openpyn.log".format(log_folder))
-            subprocess.run("sudo chmod 777 {}/openpyn.log".format(log_folder).split())
-            subprocess.run("sudo chmod 777 {}/openpyn-notifications.log".format(log_folder).split())
-        else:
-            break
-
-    # In this case only log messages originating from this logger will show up on the terminal.
-    coloredlogs.install(level="verbose", logger=logger, fmt=log_format,
-                        level_styles=levelstyles, field_styles=fieldstyles)
-
+    # in this case only log messages originating from this logger will show up on the terminal.
+    coloredlogs.install(level="DEBUG", logger=logger, fmt=log_format, level_styles=levelstyles, field_styles=fieldstyles)
     stats = True
-    if sys.__stdin__.isatty():
-        logger.debug("Interactive")
-    else:
-        logger.addHandler(logging.StreamHandler(sys.stdout))
-        logger.setLevel(logging.WARNING)
-        logger.debug("Non-Interactive")
+    # if non-interactive shell
+    if not sys.__stdin__.isatty():
+        # special handler and formatter for JuiceSSH plugin
+        stdout_handler = logging.StreamHandler(sys.stdout)
+        stdout_handler_formatter = logging.Formatter("%(message)s:%(levelname)s")
+        stdout_handler.setFormatter(stdout_handler_formatter)
+        logger.addHandler(stdout_handler)
         stats = False
 
-    port = "udp"
-    if tcp:
-        port = "tcp"
+    # if only positional argument used
+    if country_code is None and server is None:
+        # consider the positional arg e.g "us" same as "-c us"
+        country_code = country
+
+    port = "tcp" if tcp else "udp"
+
+    # Always decode internally JSON config when passed
+    if internally_allowed_config_json:
+        try:
+            internally_allowed_config_json = json.loads(internally_allowed_config_json)
+        except json.JSONDecodeError as err:
+            logger.error(
+                "Failed to decode JSON passed in '----allow-config-json' Error at line {line}:{col} {msg} ".format(
+                    line=err.lineno, col=err.colno, msg=err.msg
+                )
+            )
+            internally_allowed_config_json = None
 
     detected_os = sys.platform
+    asuswrt_os = False
+    openwrt_os = False
     if detected_os == "linux":
         if subprocess.check_output(["/bin/uname", "-o"]).decode(sys.stdout.encoding).strip() == "ASUSWRT-Merlin":
+            os.makedirs(log_folder, exist_ok=True)
+            os.chmod(log_folder, 0o777)
+            asuswrt_os = True
             force_fw_rules = False
+            internally_allowed = None
             silent = True
             skip_dns_patch = True
-            if openvpn_options:
-                openvpn_options += " " + "--syslog openpyn"
-            else:
-                openvpn_options = "--syslog openpyn"
-            logger.debug(openvpn_options)
+            if not nvram:
+                if openvpn_options:
+                    openvpn_options += " " + "--syslog openpyn"
+                else:
+                    openvpn_options = "--syslog openpyn"
+            # logger.debug(openvpn_options)
         elif os.path.exists("/etc/openwrt_release"):
+            os.makedirs(log_folder, exist_ok=True)
+            os.chmod(log_folder, 0o777)
+            openwrt_os = True
             force_fw_rules = False
+            internally_allowed = None
             silent = True
             skip_dns_patch = True
             nvram = None
@@ -237,23 +281,50 @@ def run(init: bool, server: str, country_code: str, country: str, area: str, tcp
     elif detected_os == "win32":
         logger.error("Are you even a l33t mate? Try GNU/Linux")
         return 1
+    else:
+        force_fw_rules = False
+        internally_allowed = None
+        nvram = None
 
     # check if dependencies are installed
-    if shutil.which("openvpn") is None or shutil.which("wget") is None or shutil.which("unzip") is None:
-        # In case of Debian Sid where "openvpn" is only in root's PATH, don't error out
+    if shutil.which("openvpn") is None:
+        logger.error("Please install 'openvpn' first")
+        return 1
+
+    if init:
+        if not root.verify_running_as_root():
+            logger.error("Option '--init' requires sudo access. run 'sudo openpyn --init' instead.")
+            return 1
         try:
-            root_access = root.verify_root_access(
-                "Sudo credentials required to check if 'openvpn' is available in root's PATH")
-            if root_access is False:
-                root.obtain_root_access()
-            subprocess.check_output(["sudo", "which", "wget"])
-            subprocess.check_output(["sudo", "which", "unzip"])
-            # subprocess.check_output(["sudo", "which", "openvpn"])
-        except subprocess.CalledProcessError:
-            logger.error("Please Install 'openvpn' 'wget' 'unzip' first")
+            initialise(detected_os, asuswrt_os, openwrt_os)
+            return 0
+        except RuntimeError as e:
+            logger.critical(e)
             return 1
 
-    elif daemon:
+    # if log folder doesn't exist, exit, "--init" creates it
+    if not os.path.exists(log_folder):
+        raise RuntimeError(
+            "Please initialise first by running 'sudo openpyn --init', then start using 'openpyn' without sudo"
+        )
+
+    # Add another rotating handler to log to .log files
+    # fix permissions if needed
+    for _ in range(2):
+        try:
+            file_handler = logging.handlers.TimedRotatingFileHandler(log_folder + "/openpyn.log", when="W0", interval=4)
+            file_handler_formatter = logging.Formatter(log_format)
+            file_handler.setFormatter(file_handler_formatter)
+            logger.addHandler(file_handler)
+        except PermissionError:
+            root.verify_root_access("Root access needed to set permissions of {}/openpyn.log".format(log_folder))
+            subprocess.run(["sudo", "-u", sudo_user, "chmod", "777", log_folder], check=False)
+            subprocess.run(["sudo", "-u", sudo_user, "chmod", "666", log_folder + "/openpyn.log"], check=False)
+            subprocess.run(["sudo", "-u", sudo_user, "chmod", "666", log_folder + "/openpyn-notifications.log"], check=False)
+        else:
+            break
+
+    if daemon:
         if detected_os != "linux":
             logger.error("Daemon mode is only available in GNU/Linux distros")
             return 1
@@ -261,17 +332,19 @@ def run(init: bool, server: str, country_code: str, country: str, area: str, tcp
         if not root.verify_running_as_root():
             logger.error("Please run '--daemon' or '-d' mode with sudo")
             return 1
+
         openpyn_options = ""
 
-        # if only positional argument used
-        if country_code is None and server is None:
-            country_code = country      # consider the positional arg e.g "us" same as "-c us"
         # if either "-c" or positional arg f.e "au" is present
-
         if country_code:
-            if len(country_code) > 2:   # full country name
-                # get the country_code from the full name
-                country_code = api.get_country_code(full_name=country_code)
+            # if full name of the country supplied get country_code
+            if len(country_code) > 2:
+                try:
+                    country_code = api.get_country_code(full_name=country_code)
+                except RuntimeError as e:
+                    logger.critical(e)
+                    return 1
+
             country_code = country_code.lower()
             openpyn_options += country_code
 
@@ -286,10 +359,12 @@ def run(init: bool, server: str, country_code: str, country: str, area: str, tcp
             openpyn_options += " --max-load " + str(max_load)
         if top_servers:
             openpyn_options += " --top-servers " + str(top_servers)
-        if pings:
-            openpyn_options += " --pings " + str(pings)
         if force_fw_rules:
             openpyn_options += " --force-fw-rules"
+        if add_route:
+            openpyn_options += " --add-route"
+        if allow_locally:
+            openpyn_options += " --allow-locally"
         if p2p:
             openpyn_options += " --p2p"
         if dedicated:
@@ -304,6 +379,15 @@ def run(init: bool, server: str, country_code: str, country: str, area: str, tcp
             openpyn_options += " --netflix"
         if test:
             openpyn_options += " --test"
+        if internally_allowed_config_json or internally_allowed_config:
+            # Override passed config is file is specified
+            if internally_allowed_config:
+                internally_allowed_config_json = firewall.load_allowed_ports(internally_allowed_config)
+            if firewall.validate_allowed_ports_json(internally_allowed_config_json):
+                openpyn_options += " --allow-config-json=" + shlex.quote(
+                    json.dumps(internally_allowed_config_json, separators=(",", ":"))
+                )
+            logger.error(openpyn_options)
         if internally_allowed:
             open_ports = ""
             for port_number in internally_allowed:
@@ -312,149 +396,362 @@ def run(init: bool, server: str, country_code: str, country: str, area: str, tcp
         if skip_dns_patch:
             openpyn_options += " --skip-dns-patch"
         if nvram:
-            openpyn_options += " --nvram " + str(nvram)
+            openpyn_options += " --nvram " + nvram
+        if no_redirect:
+            openpyn_options += " --no-redirect-gateway"
         if openvpn_options:
             openpyn_options += " --openvpn-options '" + openvpn_options + "'"
         # logger.debug(openpyn_options)
-        if subprocess.check_output(["/bin/uname", "-o"]).decode(sys.stdout.encoding).strip() == "ASUSWRT-Merlin":
+        if asuswrt_os:
             initd.update_service(openpyn_options, run=True)
-        elif os.path.exists("/etc/openwrt_release"):
+        elif openwrt_os:
             initd.update_service(openpyn_options, run=True)
         else:
             systemd.update_service(openpyn_options, run=True)
-        return 0
 
     elif kill:
-        logger.warning("Killing the running processes")
-        kill_management_client()
-        kill_vpn_processes()  # don't touch iptable rules
-        kill_openpyn_process()
+        if detected_os == "linux":
+            if asuswrt_os:
+                if nvram:
+                    asus.disconnect(nvram, test)
+                    sleep(2)
+                    return asus.state(nvram, test)
+        kill_all()
 
     elif kill_flush:
-        firewall.clear_fw_rules()      # also clear iptable rules
-        # if --allow present, allow those ports internally
-        logger.info("Re-enabling ipv6")
-        firewall.manage_ipv6(disable=False)
-        if internally_allowed:
-            network_interfaces = get_network_interfaces()
-            firewall.internally_allow_ports(network_interfaces, internally_allowed)
-        kill_management_client()
-        kill_vpn_processes()
-        kill_openpyn_process()
+        if detected_os == "linux":
+            if asuswrt_os:
+                pass
+            elif openwrt_os:
+                pass
+            else:
+                # also clear iptable rules
+                firewall.clear_fw_rules()
+                # if --allow present, allow those ports internally
+                logger.notice("Re-enabling ipv6")
+                firewall.manage_ipv6(disable=False)
+                if internally_allowed:
+                    network_interfaces = get_network_interfaces()
+                    firewall.internally_allow_ports(network_interfaces, internally_allowed)
+        kill_all()
 
     elif update:
-        update_config_files()
+        try:
+            update_config_files()
+        except RuntimeError as e:
+            logger.critical(e)
+            return 1
+
+    elif show_status:
+        try:
+            print_status()
+        except RuntimeError as e:
+            logger.critical(e)
+            return 1
+
+    elif show_stats:
+        try:
+            print_stats()
+        except RuntimeError as e:
+            logger.critical(e)
+            return 1
 
     # a hack to list all countries and their codes when no arg supplied with "-l"
-    elif list_servers != 'nope':      # means "-l" supplied
-        if list_servers is None:      # no arg given with "-l"
-            if p2p or dedicated or double_vpn or tor_over_vpn or anti_ddos or netflix:
-                # show the special servers in all countries
+    elif list_servers != "nope":  # means "-l" supplied
+        try:
+            if list_servers is None:  # no arg given with "-l"
+                if p2p or dedicated or double_vpn or tor_over_vpn or anti_ddos or netflix:
+                    # show the special servers in all countries
+                    display_servers(
+                        list_servers="all", port=port, area=area, p2p=p2p, dedicated=dedicated,
+                        double_vpn=double_vpn, tor_over_vpn=tor_over_vpn, anti_ddos=anti_ddos,
+                        netflix=netflix, location=location)
+                else:
+                    api.list_all_countries()
+            # if a country code is supplied give details about that country only.
+            else:
+                # if full name of the country supplied get country_code
+                if len(list_servers) > 2:
+                    list_servers = api.get_country_code(full_name=list_servers)
+
+                list_servers = list_servers.lower()
+
                 display_servers(
-                    list_servers="all", port=port, area=area, p2p=p2p, dedicated=dedicated,
+                    list_servers=list_servers, port=port, area=area, p2p=p2p, dedicated=dedicated,
                     double_vpn=double_vpn, tor_over_vpn=tor_over_vpn, anti_ddos=anti_ddos,
                     netflix=netflix, location=location)
-            else:
-                api.list_all_countries()
-        # if a country code is supplied give details about that country only.
-        else:
+        except RuntimeError as e:
+            logger.critical(e)
+            return 1
+
+    # if either "-c" or positional arg f.e "au" is present
+    elif country_code:
+        try:
+            if not test:
+                # ask for and store credentials if not present, skip if "--test"
+                if credentials.check_credentials() is False:
+                    logger.error("Credentials not found: Please run 'sudo openpyn --init' first")
+
+                # check if OpenVPN config files exist if not download them, skip if "--test"
+                check_config_files()
+
+            use_systemd_resolved = False
+            use_resolvconf = False
+            if detected_os == "linux":
+                if asuswrt_os or openwrt_os:
+                    if not nvram:
+                        # make sure module is loaded
+                        load_tun_module()
+                else:
+                    use_systemd_resolved = uses_systemd_resolved()
+                    use_resolvconf = os.path.isfile("/sbin/resolvconf")
+
             # if full name of the country supplied get country_code
-            if len(list_servers) > 2:
-                list_servers = api.get_country_code(full_name=list_servers)
-            display_servers(
-                list_servers=list_servers, port=port, area=area, p2p=p2p, dedicated=dedicated,
-                double_vpn=double_vpn, tor_over_vpn=tor_over_vpn, anti_ddos=anti_ddos,
-                netflix=netflix, location=location)
+            if len(country_code) > 2:
+                country_code = api.get_country_code(full_name=country_code)
 
-    # only clear/touch FW Rules if "-f" used
-    elif force_fw_rules:
-        firewall.clear_fw_rules()
+            country_code = country_code.lower()
 
-    # check if OpenVPN config files exist if not download them.
-    check_config_files()
+            # if '-f' supplied, clear_fw_rules first
+            if force_fw_rules and not test:
+                firewall.clear_fw_rules()
 
-    # if only positional argument used
-    if country_code is None and server is None:
-        country_code = country      # consider the positional arg e.g "us" same as "-c us"
-    # if either "-c" or positional arg f.e "au" is present
-    if country_code:
-        # ask for and store credentials if not present, skip if "--test"
-        if not test:
-            if credentials.check_credentials() is False:
-                credentials.save_credentials()
-
-        if len(country_code) > 2:   # full country name
-            # get the country_code from the full name
-            country_code = api.get_country_code(full_name=country_code)
-        country_code = country_code.lower()
-
-        # keep trying to connect to new servers
-        for tries in range(3):  # pylint: disable=W0612
             better_servers_list = find_better_servers(
                 country_code, area, max_load, top_servers, tcp, p2p,
                 dedicated, double_vpn, tor_over_vpn, anti_ddos, netflix, location, stats)
-            pinged_servers_list = ping_servers(better_servers_list, pings, stats)
+            # if no servers under search criteria
+            if not better_servers_list:
+                logger.critical("There are no servers that satisfy your criteria, please broaden your search.")
+                return 1
+            pinged_servers_list = ping_servers(better_servers_list, stats)
             chosen_servers = choose_best_servers(pinged_servers_list, stats)
 
+            # only clear/touch FW Rules if "-f" used, skip if "--test"
+            if force_fw_rules and not test:
+                touch_iptables_rules(chosen_servers, port, skip_dns_patch, allow_locally,
+                                     internally_allowed, internally_allowed_config, internally_allowed_config_json)
             # connect to chosen_servers, if one fails go to next
             for aserver in chosen_servers:
                 if stats:
-                    print(Style.BRIGHT + Fore.BLUE + "Out of the Best Available Servers, Chose",
+                    print(Style.BRIGHT + Fore.BLUE + "\nOut of the Best Available Servers, Chose",
                           (Fore.GREEN + aserver + Fore.BLUE) + "\n")
-                # if "-f" used apply firewall rules
-                if force_fw_rules:
-                    network_interfaces = get_network_interfaces()
-                    vpn_server_ip = get_vpn_server_ip(aserver, port)
-                    firewall.apply_fw_rules(network_interfaces, vpn_server_ip, skip_dns_patch)
-                    if internally_allowed:
-                        firewall.internally_allow_ports(network_interfaces, internally_allowed)
+
                 if nvram:
-                    # TODO return 0 on success else 1 in asus.run()
-                    asus.run(aserver, country_code, nvram, "All", "adaptive", "Strict", tcp, test)
-                    logger.success("SAVED SERVER " + aserver + " ON PORT " + port + " TO NVRAM")
-                return(connect(aserver, port, silent, test, skip_dns_patch, openvpn_options))
+                    check_config_files()
+                    asus.run(aserver, nvram, openvpn_options, "All", "adaptive", "Strict", tcp, test)
+                    logger.success("SAVED SERVER " + aserver + " ON PORT " + port + " TO NVRAM " + nvram)
+                    asus.connect(nvram, test)
+                    sleep(2)
+                    return asus.state(nvram, test)
+
+                if test:
+                    logger.success(
+                        "Simulation end reached, openpyn would have connected to server: "
+                        + aserver
+                        + " on port: "
+                        + port
+                        + " with 'silent' mode: "
+                        + str(silent).lower()
+                    )
+                    continue
+
+                connect(
+                    aserver,
+                    port,
+                    silent,
+                    skip_dns_patch,
+                    app,
+                    no_redirect,
+                    openvpn_options,
+                    add_route,
+                    use_systemd_resolved,
+                    use_resolvconf,
+                )
+        except RuntimeError as e:
+            logger.critical(e)
+            return 1
+        except SystemExit:
+            logger.info("Shutting down safely, please wait until process exits")
+        finally:
+            # Run revert-manual-dns-patch.sh, if this is not Debian Based OS and manual-dns-patch was applied
+            if (use_systemd_resolved or use_resolvconf) is False and skip_dns_patch is False and test is False:
+                # Darwin: Revert /etc/resolv.conf back to the original if it was modified
+                subprocess.call(["sudo", "-u", sudo_user, __basefilepath__ + "scripts/revert-manual-dns-patch.sh"])
+
     elif server:
-        # ask for and store credentials if not present, skip if "--test"
-        if not test:
-            if credentials.check_credentials() is False:
-                credentials.save_credentials()
+        try:
+            if not test:
+                # ask to store credentials if not present, skip if "--test"
+                if credentials.check_credentials() is False:
+                    logger.error("Credentials not found: Please run 'sudo openpyn --init' first")
+
+                # check if OpenVPN config files exist if not download them, skip if "--test"
+                check_config_files()
+
+            use_systemd_resolved = False
+            use_resolvconf = False
+            if detected_os == "linux":
+                if asuswrt_os or openwrt_os:
+                    if not nvram:
+                        # make sure module is loaded
+                        load_tun_module()
+                else:
+                    use_systemd_resolved = uses_systemd_resolved()
+                    use_resolvconf = os.path.isfile("/sbin/resolvconf")
+
+            server = server.lower()
+
+            # only clear/touch FW Rules if "-f" used, skip if "--test"
+            if force_fw_rules and not test:
+                touch_iptables_rules([server], port, skip_dns_patch, allow_locally,
+                                     internally_allowed, internally_allowed_config, internally_allowed_config_json)
+
+            if nvram:
+                check_config_files()
+                asus.run(server, nvram, openvpn_options, "All", "adaptive", "Strict", tcp, test)
+                logger.success("SAVED SERVER " + server + " ON PORT " + port + " TO NVRAM " + nvram)
+                asus.connect(nvram, test)
+                sleep(2)
+                return asus.state(nvram, test)
+
+            if test:
+                logger.success(
+                    "Simulation end reached, openpyn would have connected to server: "
+                    + server
+                    + " on port: "
+                    + port
+                    + " with 'silent' mode: "
+                    + str(silent).lower()
+                )
+                return 0
+
+            # keep trying to connect to same server
+            for _ in range(3 * top_servers):
+                connect(
+                    server,
+                    port,
+                    silent,
+                    skip_dns_patch,
+                    app,
+                    no_redirect,
+                    openvpn_options,
+                    add_route,
+                    use_systemd_resolved,
+                    use_resolvconf,
+                )
+        except RuntimeError as e:
+            logger.critical(e)
+            return 1
+        except SystemExit:
+            logger.info("Shutting down safely, please wait until process exits")
+        finally:
+            # Run revert-manual-dns-patch.sh, if this is not Debian Based OS and manual-dns-patch was applied
+            if (use_systemd_resolved or use_resolvconf) is False and skip_dns_patch is False and test is False:
+                # Darwin: Revert /etc/resolv.conf back to the original if it was modified
+                subprocess.call(["sudo", "-u", sudo_user, __basefilepath__ + "scripts/revert-manual-dns-patch.sh"])
 
-        server = server.lower()
-        # if "-f" used apply firewall rules
-        if force_fw_rules:
-            network_interfaces = get_network_interfaces()
-            vpn_server_ip = get_vpn_server_ip(server, port)
-            firewall.apply_fw_rules(network_interfaces, vpn_server_ip, skip_dns_patch)
-            if internally_allowed:
-                firewall.internally_allow_ports(network_interfaces, internally_allowed)
-        if nvram:
-            asus.run(server, country_code, nvram, "All", "adaptive", "Strict", tcp, test)
-            logger.success("SAVED SERVER " + server + " ON PORT " + port + " TO NVRAM")
-            return 0
-        for i in range(20):  # pylint: disable=W0612
-            return(connect(server, port, silent, test, skip_dns_patch, openvpn_options))
     else:
-        logger.info('To see usage options type: "openpyn -h" or "openpyn --help"')
-    return 0        # if everything went ok
+        logger.info("To see usage options type: 'openpyn -h' or 'openpyn --help'")
 
+    # if everything went ok
+    return 0
+
+
+def initialise(detected_os: str, asuswrt_os: bool, openwrt_os: bool) -> None:
+    if os.path.exists(ovpn_folder):
+        shutil.rmtree(ovpn_folder)
+    os.mkdir(ovpn_folder)
+    os.chmod(ovpn_folder, 0o777)
+
+    os.makedirs(log_folder, exist_ok=True)
+    os.chmod(log_folder, 0o777)
 
-def initialise(log_folder: str) -> bool:
-    credentials.save_credentials()
     update_config_files()
-    if not os.path.exists(log_folder):
-        os.mkdir(log_folder)
-        os.chmod(log_folder, mode=0o777)
-        open(log_folder + "/openpyn.log", "a").close()      # touch the log file
-        os.chmod(log_folder + "/openpyn.log", mode=0o777)
-    if sys.platform == "linux":
-        if subprocess.check_output(["/bin/uname", "-o"]).decode(sys.stdout.encoding).strip() == "ASUSWRT-Merlin":
-            return initd.install_service()
-        elif os.path.exists("/etc/openwrt_release"):
-            return initd.install_service()
-        elif subprocess.check_output(["cat", "/proc/1/comm"]).decode(sys.stdout.encoding).strip() == "systemd":
-            return systemd.install_service()
-        logger.warning("systemd not found, skipping systemd integration")
-        return 1
+    credentials.save_credentials()
+    if detected_os == "linux":
+        if asuswrt_os:
+            initd.install_service()
+        elif openwrt_os:
+            initd.install_service()
+        elif os.path.exists("/sbin/init") and os.readlink("/sbin/init").rsplit("/", maxsplit=1)[-1] == "systemd":
+            systemd.install_service()
+        else:
+            logger.warning("systemd not found, skipping systemd integration")
+
+
+def print_status():
+    try:
+        ps = (
+            subprocess.check_output(["pgrep", "openpyn"], stderr=subprocess.DEVNULL)
+            .decode(sys.stdout.encoding)
+            .strip()
+            .split()
+        )
+        if len(ps) > 1:  # first is the current process
+            # when it returns "0", proceed
+            with open("{}/status".format(log_folder), "r") as status_file:
+                print(status_file.readline().rstrip())
+        else:
+            raise RuntimeError("'openpyn' is not running")
+    except subprocess.CalledProcessError:
+        # when check_output issued non 0 result, "not found"
+        raise RuntimeError("command 'pgrep' not found") from None
+    except FileNotFoundError:
+        raise RuntimeError("{}/status not found".format(log_folder)) from None
+
+
+def print_stats():
+    try:
+        ps = (
+            subprocess.check_output(["pgrep", "openpyn"], stderr=subprocess.DEVNULL)
+            .decode(sys.stdout.encoding)
+            .strip()
+            .split()
+        )
+        if len(ps) > 1:  # first is the current process
+            # when it returns "0", proceed
+            with open("{}/openvpn-status".format(log_folder), "r") as status_file:
+                print(status_file.read())
+        else:
+            raise RuntimeError("'openpyn' is not running")
+    except subprocess.CalledProcessError:
+        # when check_output issued non 0 result, "not found"
+        raise RuntimeError("command 'pgrep' not found") from None
+    except FileNotFoundError:
+        raise RuntimeError("{}/openvpn-status not found".format(log_folder)) from None
+    except PermissionError:
+        raise RuntimeError("Option '--stats' requires sudo access. run 'sudo openpyn --stats' instead.") from None
+
+
+def load_tun_module():
+    if not Path("/dev/net/tun").is_char_device():
+        subprocess.call("modprobe tun", shell=True)
+        if not Path("/dev/net/tun").is_char_device():
+            raise RuntimeError("Cannot open TUN/TAP dev /dev/net/tun: No such file or directory")
+
+
+def touch_iptables_rules(chosen_servers: List, port: str, skip_dns_patch: bool, allow_locally: bool,
+                         internally_allowed: List, internally_allowed_config: str, internally_allowed_config_json: dict):
+    network_interfaces = get_network_interfaces()
+    vpn_server_ips = []
+    firewall.flush_input_output()
+
+    if internally_allowed_config or internally_allowed_config_json:
+        if internally_allowed_config:
+            internally_allowed_config_json = firewall.load_allowed_ports(internally_allowed_config)
+
+        if firewall.validate_allowed_ports_json(internally_allowed_config_json):
+            firewall.apply_allowed_port_rules(network_interfaces, internally_allowed_config_json)
+    elif internally_allowed:
+        firewall.internally_allow_ports(network_interfaces, internally_allowed)
+
+    if allow_locally:
+        firewall.internally_allow_all(network_interfaces)
+
+    for server in chosen_servers:
+        vpn_server_ips.append(get_vpn_server_ip(server, port))
+
+    firewall.apply_fw_rules(network_interfaces, vpn_server_ips, skip_dns_patch)
 
 
 # Filters servers based on the specified criteria.
@@ -468,15 +765,22 @@ def find_better_servers(country_code: str, area: str, max_load: int, top_servers
 
     # use api.nordvpn.com
     json_res_list = api.get_data_from_api(
-        country_code=country_code, area=area, p2p=p2p, dedicated=dedicated,
-        double_vpn=double_vpn, tor_over_vpn=tor_over_vpn, anti_ddos=anti_ddos,
-        netflix=netflix, location=location)
+        country_code=country_code,
+        area=area,
+        p2p=p2p,
+        dedicated=dedicated,
+        double_vpn=double_vpn,
+        tor_over_vpn=tor_over_vpn,
+        anti_ddos=anti_ddos,
+        netflix=netflix,
+        location=location,
+    )
 
     server_list = filters.filter_by_protocol(json_res_list=json_res_list, tcp=tcp)
 
     better_servers_list = filters.filter_by_load(server_list, max_load, top_servers)
 
-    if stats:
+    if better_servers_list and stats:
         print(Style.BRIGHT + Fore.BLUE + "According to NordVPN, \
 Least Busy " + Fore.GREEN + str(len(better_servers_list)) + Fore.BLUE + " Servers in \
 " + Fore.GREEN + country_code.upper() + Fore.BLUE, end=" ")
@@ -503,60 +807,79 @@ def find_better_servers(country_code: str, area: str, max_load: int, top_servers
 
 
 # Pings servers with the specified no of "ping",
-# returns a sorted list by Ping Avg and Median Deviation
-def ping_servers(better_servers_list: List, pings: str, stats: bool) -> List:
+# Returns a sorted list by ping median average deviation
+def ping_servers(better_servers_list: List, stats: bool) -> List:
     pinged_servers_list = []
-    ping_supports_option_i = True       # older ping command doesn't support "-i"
+    ping_supports_option_i = True  # older ping command doesn't support "-i"
 
     try:
-        subprocess.check_output(["ping", "-n", "-i", ".2", "-c", "2", "8.8.8.8"],
-                                stderr=subprocess.DEVNULL)
+        subprocess.check_output(["ping", "-n", "-i", locale.str(0.2), "-c", "2", "8.8.8.8"], stderr=subprocess.DEVNULL)
     except subprocess.CalledProcessError:
         # when Exception, the processes issued error, "option is not supported"
         ping_supports_option_i = False
-        logger.warning("Your 'ping' command doesn't support '-i' or '-n', \
-falling back to wait of 1 second between pings, pings will be slow")
-    for i in better_servers_list:
-        # ping_result to append 2  lists into it
-        ping_result = []
+        logger.warning(
+            "Your 'ping' command doesn't support '-i' or '-n', falling back to wait of 1 second between pings, pings will be"
+            " slow"
+        )
+    if ping_supports_option_i is True:
+        ping_subprocess_command = ["ping", "-n", "-i", locale.str(0.2), "-c", "5", "dns_placeholder"]
+    else:
+        ping_subprocess_command = ["ping", "-c", "5", "dns_placeholder"]
+
+    ping_subprocess_list = []
+
+    if stats:
+        print(Style.BRIGHT + Fore.BLUE + "Sending Pings To Servers\n")
+
+    for server_spec in better_servers_list:
+        ping_subprocess_command[-1] = server_spec[0] + ".nordvpn.com"
+
         try:
-            if ping_supports_option_i:
-                ping_proc = subprocess.Popen(
-                    ["ping", "-n", "-i", ".2", "-c", pings, i[0] + ".nordvpn.com"],
-                    stdout=subprocess.PIPE)
-            else:
-                ping_proc = subprocess.Popen(
-                    ["ping", "-c", pings, i[0] + ".nordvpn.com"],
-                    stdout=subprocess.PIPE)
-            # pipe the output of ping to grep.
-            ping_output = subprocess.check_output(
-                ["grep", "-B", "1", "min/avg/max"], stdin=ping_proc.stdout)
-
-            ping_string = str(ping_output)
-            # logger.debug(ping_string)
-            if "0%" not in ping_string:
-                logger.warning("Some packet loss while pinging %s, skipping it", i[0])
-                continue
+            ping_process = subprocess.Popen(ping_subprocess_command, stdout=subprocess.PIPE)
+            grep_process = subprocess.Popen(
+                ["grep", "-B", "1", "min/avg/max"],
+                stdin=ping_process.stdout,
+                stdout=subprocess.PIPE,
+            )
+
+            ping_subprocess = [server_spec, grep_process]
+            time.sleep(0.02)  # needs to spawn Popen process
+            ping_subprocess_list.append(ping_subprocess)
+
         except subprocess.CalledProcessError:
-            logger.warning("Ping Failed to: %s, excluding it from the list", i[0])
+            logger.warning("Ping Failed to: %s, excluding it from the list", server_spec[0])
             continue
         except KeyboardInterrupt:
-            logger.info('KeyboardInterrupt; Shutting down')
-            sys.exit()
-        ping_string = ping_string[ping_string.find("= ") + 2:]
-        ping_string = ping_string[:ping_string.find(" ")]
-        ping_list = ping_string.split("/")
-        # change str values in ping_list to ints
-        ping_list = list(map(float, ping_list))
-        ping_list = list(map(int, ping_list))
-
-        if stats:
-            print(Style.BRIGHT + Fore.BLUE + "Pinging Server " + i[0] + " min/avg/max/mdev = \
-" + Fore.GREEN + str(ping_list), Fore.BLUE + "\n")
-        ping_result.append(i)
-        ping_result.append(ping_list)
-        # logger.debug(ping_result)
-        pinged_servers_list.append(ping_result)
+            raise SystemExit from None
+
+    for ping_subprocess in ping_subprocess_list:
+        ping_subprocess.append(ping_subprocess[1].communicate())
+
+        ping_output = ping_subprocess[2][0]
+
+        # logger.info("openpyn: ping output for %s\n%s", ping_subprocess[0][0], ping_output)
+
+        ping_string = str(ping_output)
+        ping_result = []
+        # logger.debug(ping_string)
+        if "0%" not in ping_string:
+            logger.warning("Some packet loss while pinging %s, skipping it", ping_subprocess[0][0])
+        else:
+            ping_string = ping_string[ping_string.find("= ") + 2:]
+            ping_string = ping_string[: ping_string.find(" ")]
+            ping_list = ping_string.split("/")
+            # change str values in ping_list to ints
+            ping_list = list(map(float, ping_list))
+            ping_list = list(map(int, ping_list))
+
+            if stats:
+                print(Style.BRIGHT + Fore.BLUE + "Ping Resonse From " + ping_subprocess[0][0].ljust(7) +
+                      " min/avg/max/mdev = " + Fore.GREEN + str(ping_list), Fore.BLUE + "")
+            ping_result.append(ping_subprocess[0])
+            ping_result.append(ping_list)
+            # logger.debug(ping_result)
+            pinged_servers_list.append(ping_result)
+    # sort by ping median average deviation
     if len(pinged_servers_list[0][1]) >= 4:
         # sort by Ping Avg and Median Deviation
         pinged_servers_list = sorted(pinged_servers_list, key=lambda item: (item[1][1], item[1][3]))
@@ -575,116 +898,204 @@ def choose_best_servers(best_servers: List, stats: bool) -> List:
         best_servers_names.append(i[0][0])
 
     if stats:
-        print("Top " + Fore.GREEN + str(len(best_servers)) + Fore.BLUE + " Servers with Best Ping Are: \
-" + Fore.GREEN + str(best_servers_names) + Fore.BLUE)
+        print("\nTop " + Fore.GREEN + str(len(best_servers)) + Fore.BLUE + " Servers with Best Ping Are: "
+              + Fore.GREEN + str(best_servers_names) + Fore.BLUE)
         print(Style.RESET_ALL)
     return best_servers_names
 
 
+def kill_all() -> None:
+    logger.notice("Killing the running processes")
+
+    root_access = root.verify_root_access("Root access needed to kill 'openvpn', 'openpyn', 'openpyn-management' processes")
+    if root_access is False:
+        root.obtain_root_access()
+
+    # ignore interrupt from keyboard (CTRL + C)
+    signal.signal(signal.SIGINT, signal.SIG_IGN)
+    # ignore termination signal (143)
+    signal.signal(signal.SIGTERM, signal.SIG_IGN)
+
+    kill_management_client()
+    kill_vpn_processes()
+
+    kill_openpyn_process()
+
+
 def kill_vpn_processes() -> None:
     try:
-        subprocess.check_output(["pgrep", "openvpn"])
-        # When it returns "0", proceed
-        root.verify_root_access("Root access needed to kill openvpn process")
-        subprocess.call(["sudo", "killall", "openvpn"])
+        subprocess.check_output(["pgrep", "openvpn"], stderr=subprocess.DEVNULL)
+        # when it returns "0", proceed
         logger.notice("Killing the running openvpn process")
+        subprocess.check_output(["sudo", "-u", sudo_user, "killall", "openvpn"], stderr=subprocess.DEVNULL)
         time.sleep(1)
     except subprocess.CalledProcessError:
         # when Exception, the openvpn_processes issued non 0 result, "not found"
         pass
-    return
 
 
 def kill_openpyn_process() -> None:
     try:
-        root.verify_root_access("Root access needed to kill openpyn process")
-        subprocess.call(["sudo", "killall", "openpyn"])
+        subprocess.check_output(["pgrep", "openpyn"], stderr=subprocess.DEVNULL)
+        # when it returns "0", proceed
+        logger.notice("Killing the running openpyn process")
+        subprocess.check_output(["sudo", "-u", sudo_user, "killall", "openpyn"], stderr=subprocess.DEVNULL)
     except subprocess.CalledProcessError:
-        # when Exception, the openvpn_processes issued non 0 result, "not found"
+        # when Exception, the openpyn_processes issued non 0 result, "not found"
         pass
-    return
 
 
 def kill_management_client() -> None:
     # kill the management client if it is for some reason still alive
     try:
-        root.verify_root_access("Root access needed to kill 'openpyn-management' process")
-        subprocess.check_output(["sudo", "killall", "openpyn-management"],
-                                stderr=subprocess.DEVNULL)
-        logger.notice("Killing the running openvpn-management client")
+        subprocess.check_output(["pgrep", "openpyn-management"], stderr=subprocess.DEVNULL)
+        # when it returns "0", proceed
+        logger.notice("Killing the running openvpn-management process")
+        subprocess.check_output(["sudo", "-u", sudo_user, "killall", "openpyn-management"], stderr=subprocess.DEVNULL)
         time.sleep(3)
     except subprocess.CalledProcessError:
-        # when Exception, the openvpn_processes issued non 0 result, "not found"
+        # when Exception, the openpyn-management_processes issued non 0 result, "not found"
         pass
-    return
 
 
 def update_config_files() -> None:
-    root.verify_root_access("Root access needed to write files in " +
-                            "'" + __basefilepath__ + "files/" + "'")
-    try:
-        zip_archive = __basefilepath__ + "ovpn.zip"
-        if os.path.exists(zip_archive):
-            print(Fore.BLUE + "Previous update file already exists, deleting..." + Style.RESET_ALL)
-            os.remove(zip_archive)
+    # temporary folder to download files in and change permissions to 666
+    temp_folder = tempfile.mkdtemp()
+
+    url = "https://downloads.nordcdn.com/configs/archives/servers/ovpn.zip"
+    _, filename = url.rsplit("/", maxsplit=1)
 
-        subprocess.check_call(
-            ["sudo", "wget", "https://downloads.nordcdn.com/configs/archives/servers/ovpn.zip", "-P", __basefilepath__])
-    except subprocess.CalledProcessError:
-        logger.error("Exception occurred while wgetting zip, is the internet working? \
-is nordcdn.com blocked by your ISP or Country?, If so use Privoxy \
-[https://github.com/jotyGill/openpyn-nordvpn/issues/109]")
-        sys.exit()
     try:
-        subprocess.check_call(
-            ["sudo", "unzip", "-q", "-u", "-o", __basefilepath__ +
-                "ovpn", "-d", __basefilepath__ + "files/"],
-            stderr=subprocess.DEVNULL)
-        subprocess.check_call(
-            ["sudo", "rm", __basefilepath__ + "ovpn.zip"])
-    except subprocess.CalledProcessError:
-        try:
-            subprocess.check_call(
-                ["sudo", "rm", "-rf", __basefilepath__ + "files/ovpn_udp"])
-            subprocess.check_call(
-                ["sudo", "rm", "-rf", __basefilepath__ + "files/ovpn_tcp"])
-            subprocess.check_call(
-                ["sudo", "unzip", __basefilepath__ + "ovpn", "-d", __basefilepath__ + "files/"])
-            subprocess.check_call(
-                ["sudo", "rm", __basefilepath__ + "ovpn.zip"])
-        except subprocess.CalledProcessError:
-            logger.error("Exception occured while unzipping ovpn.zip, is unzip installed?")
-            sys.exit()
+        r = requests.head(url, stream=True)
+        total = int(r.headers["content-length"])
+    except requests.exceptions.RequestException:
+        raise RuntimeError(
+            "Error while connecting to {}, Check Your Network Connection. Forgot to flush iptables? (openpyn -x)".format(url)
+        ) from None
+
+    last_modified = r.headers["last-modified"]
+    last_update_path = os.path.join(ovpn_folder, "last_update")
+    if os.path.exists(last_update_path):
+        with open(last_update_path, "r") as fp:
+            last_update = parsedate(fp.read())
+
+        if last_update >= parsedate(last_modified):
+            logger.info("Configuration files are up-to-date, skipping...")
+            return
+
+    r = requests.get(url, stream=True)
+    f = io.BytesIO()
+    chunk_size = 512
+
+    with tqdm(total=total, unit="B", unit_scale=True, desc="Downloading {}".format(filename)) as pbar:
+        for chunk in r.iter_content(chunk_size=chunk_size):
+            if chunk:
+                f.write(chunk)
+                pbar.update(len(chunk))
+
+    z = zipfile.ZipFile(f)
+    total = sum(f.file_size for f in z.infolist())
+
+    with tqdm(total=total, unit="B", unit_scale=True, desc="Extracting {}".format(filename)) as pbar:
+        for file in z.infolist():
+            z.extract(file, path=temp_folder)
+            pbar.update(file.file_size)
+
+    z.close()
+
+    # pylint: disable=W0612
+    # change dir permissions so non root can delete/write them
+    for dirpath, dirnames, filenames in os.walk(temp_folder):
+        for name in dirnames:
+            os.chmod(os.path.join(dirpath, name), 0o777)
+        # for name in filenames:
+            # os.chmod(os.path.join(dirpath, name), 0o666)
+
+    # remove dirs, because non-root can't chmod if files/dirs were created by root
+    shutil.rmtree(os.path.join(ovpn_folder, "ovpn_tcp"), True)
+    shutil.rmtree(os.path.join(ovpn_folder, "ovpn_udp"), True)
+
+    recusive_copy(temp_folder, ovpn_folder, 0o777)
+
+    with open(os.path.join(ovpn_folder, "last_update"), "w") as fp:
+        fp.write(last_modified)
+    os.chmod(os.path.join(ovpn_folder, "last_update"), 0o666)
+
+    shutil.rmtree(temp_folder)
+
+
+# Implements recursive copy in Python
+def recusive_copy(source_path, destination_path, folder_permission):
+    for dirpath, dirnames, filenames in os.walk(source_path):
+        for _ in dirnames:
+            pass
+            # src_folder_path = os.path.join(dirpath, dirname)
+            # dst_path = os.path.join(prof, dirname)
+        for filename in filenames:
+            src_file_path = os.path.join(dirpath, filename)
+            # src_list = list(Path(src_file_path).parts)
+            src_list = src_file_path.split(os.path.sep)
+            # remove first element '/' from the list
+            src_list.pop(0)
+            # find index of base folder in order to extract subfolder paths
+            # these subfolder paths will be created in dest location then appended to
+            # the full path of files ~/.mozilla/firefox/TEST/"extensions/uBlock0@raymondhill.net.xpi"
+            # base_folder_ends = len(list(Path(source_path).parts)) - 1
+            base_folder_ends = len(source_path.split(os.path.sep)) - 1
+
+            # extract section after 'profile' out of '/home/user/privacy-fighter/profile/extensions/ext.xpi'
+            src_list = src_list[base_folder_ends:]
+
+            # now src_file would be e.g extensions/ext.xpi
+            # src_file = Path(*src_list)
+
+            dst_file_path = os.path.join(destination_path, *src_list)  # str(src_file))
+            # print("file : ", src_file_path, dst_file_path)
+            # print("Copying: ", src_file)
+            # create parent directory
+            if not os.path.exists(os.path.dirname(dst_file_path)):
+                os.makedirs(os.path.dirname(dst_file_path))
+                os.chmod(os.path.dirname(dst_file_path), folder_permission)
+            shutil.copy(src_file_path, dst_file_path)
 
 
 # Lists information about servers under the given criteria.
-def display_servers(list_servers: List, port: str, area: str, p2p: bool, dedicated: bool, double_vpn: bool,
+def display_servers(list_servers: str, port: str, area: str, p2p: bool, dedicated: bool, double_vpn: bool,
                     tor_over_vpn: bool, anti_ddos: bool, netflix: bool, location: float) -> None:
-    servers_on_web = set()      # servers shown on the website
+    servers_on_web = set()  # servers shown on the website
 
     # if list_servers was not a specific country it would be "all"
     json_res_list = api.get_data_from_api(
-        country_code=list_servers, area=area, p2p=p2p, dedicated=dedicated,
-        double_vpn=double_vpn, tor_over_vpn=tor_over_vpn, anti_ddos=anti_ddos,
-        netflix=netflix, location=location)
+        country_code=list_servers,
+        area=area,
+        p2p=p2p,
+        dedicated=dedicated,
+        double_vpn=double_vpn,
+        tor_over_vpn=tor_over_vpn,
+        anti_ddos=anti_ddos,
+        netflix=netflix,
+        location=location,
+    )
     # logger.debug(json_res_list)
 
-    print(Style.BRIGHT + Fore.BLUE + "The NordVPN Servers In", Fore.GREEN +
-          list_servers.upper() + Fore.BLUE, end=" ")
+    if not json_res_list:
+        raise RuntimeError("There are no servers that satisfy your criteria, please broaden your search.")
+
+    print(Style.BRIGHT + Fore.BLUE + "The NordVPN Servers in", Fore.GREEN + list_servers.upper() + Fore.BLUE, end=" ")
     if area:
-        print("Area ", Fore.GREEN + area + Fore.BLUE, end=" ")
+        print("Area", Fore.GREEN + area + Fore.BLUE, end=" ")
     if p2p:
-        print("with " + Fore.GREEN + "p2p" + Fore.BLUE + " support", end=" ")
+        print("with " + Fore.GREEN + "p2p" + Fore.BLUE + " Support", end=" ")
     if dedicated:
-        print("with " + Fore.GREEN + "dedicated" + Fore.BLUE + " support", end=" ")
+        print("with " + Fore.GREEN + "dedicated" + Fore.BLUE + " Support", end=" ")
     if double_vpn:
-        print("with " + Fore.GREEN + "double_vpn" + Fore.BLUE + " support", end=" ")
+        print("with " + Fore.GREEN + "double_vpn" + Fore.BLUE + " Support", end=" ")
     if tor_over_vpn:
-        print("with " + Fore.GREEN + "tor_over_vpn" + Fore.BLUE + " support", end=" ")
+        print("with " + Fore.GREEN + "tor_over_vpn" + Fore.BLUE + " Support", end=" ")
     if anti_ddos:
-        print("with " + Fore.GREEN + "anti_ddos" + Fore.BLUE + " support", end=" ")
+        print("with " + Fore.GREEN + "anti_ddos" + Fore.BLUE + " Support", end=" ")
     if netflix:
-        print("with " + Fore.GREEN + "netflix" + Fore.BLUE + " support", end=" ")
+        print("with " + Fore.GREEN + "netflix" + Fore.BLUE + " Support", end=" ")
     print("Are:\n" + Style.RESET_ALL)
 
     # add server names to "servers_on_web" set
@@ -702,69 +1113,52 @@ def display_servers(list_servers: List, port: str, area: str, p2p: bool, dedicat
 
     if list_servers != "all" and not p2p and not dedicated and not double_vpn \
             and not tor_over_vpn and not anti_ddos and not netflix and not area:
-            # else not applicable.
+        # else not applicable.
         print_latest_servers(list_servers=list_servers, port=port, server_set=servers_on_web)
 
 
-def print_latest_servers(list_servers: List, port: str, server_set: Set) -> None:
-    if port == "tcp":
-        folder = "ovpn_tcp/"
-    else:
-        folder = "ovpn_udp/"
+def print_latest_servers(list_servers: str, port: str, server_set: Set) -> None:
+    folder = "ovpn_{}".format(port)
 
-    servers_in_files = set()      # servers from .ovpn files
-    new_servers = set()   # new Servers, not published on website yet, or taken down
+    servers_in_files = set()  # servers from .ovpn files
+    new_servers = set()  # new servers, not published on website yet, or taken down
     try:
-        serverFiles = subprocess.check_output(
-            "ls " + __basefilepath__ + "files/" + folder + list_servers + "*", shell=True)
+        server_files_path = os.path.join(ovpn_folder, folder, list_servers)
+        server_files = subprocess.check_output("ls " + server_files_path + "*", shell=True)
     except subprocess.CalledProcessError:
-        logger.error("The supplied Country Code is likely wrong or you just don't have \
-its config files (In which case run 'sudo openpyn --update')")
-        sys.exit()
-    openvpn_files_str = str(serverFiles)
+        raise RuntimeError(
+            "The supplied Country Code is likely wrong or you just don't have its config files (In which case run 'sudo"
+            " openpyn --update')"
+        ) from None
+    openvpn_files_str = str(server_files)
     openvpn_files_str = openvpn_files_str[2:-3]
     openvpn_files_list = openvpn_files_str.split("\\n")
 
     for server in openvpn_files_list:
-        server_name = server[server.find("files/" + folder) + 15:server.find(".")]
-        servers_in_files.add(server_name)
+        server_name = os.path.basename(server)
+        servers_in_files.add(server_name.split(".")[0])
 
     for server in servers_in_files:
         if server not in server_set:
             new_servers.add(server)
     if new_servers:
-        print("The following servers have not been listed on the nord's site yet, they usually are the fastest or dead.")
+        print("The following servers have not been listed on the Nord's site yet, they usually are the fastest or dead.")
         print(new_servers)
-    return
 
 
 def check_config_files() -> None:
-    try:
-        serverFiles = subprocess.check_output(
-            "ls " + __basefilepath__ + "files", shell=True, stderr=subprocess.DEVNULL)
-        openvpn_files_str = str(serverFiles)
-    except subprocess.CalledProcessError:
-        subprocess.call(["sudo", "mkdir", "-p", __basefilepath__ + "files"])
-        serverFiles = subprocess.check_output(
-            "ls " + __basefilepath__ + "files", shell=True, stderr=subprocess.DEVNULL)
-        openvpn_files_str = str(serverFiles)
-
-    if len(openvpn_files_str) < 4:  # 3 is of Empty str (b'')
-        logger.notice("Running openpyn for the first time? running 'openpyn --update' for you :)")
-        time.sleep(5)
-        # download the config files
-        update_config_files()
+    if not os.path.exists(ovpn_folder):
+        raise RuntimeError("please run 'sudo openpyn --init' first. {} not found".format(ovpn_folder))
 
 
 def get_network_interfaces() -> List:
     # find the network interfaces present on the system
-    interfaces = []
     interfaces_details = []
 
     interfaces = subprocess.check_output("ls /sys/class/net", shell=True)
-    interfaceString = str(interfaces)
-    interfaceString = interfaceString[2:-3]
-    interfaces = interfaceString.split('\\n')
+    interface_string = str(interfaces)
+    interface_string = interface_string[2:-3]
+    interfaces = interface_string.split("\\n")
 
     for interface in interfaces:
         interface_out = subprocess.check_output(["ip", "addr", "show", interface])
@@ -783,32 +1177,37 @@ def get_network_interfaces() -> List:
 
 
 def get_vpn_server_ip(server: str, port: str) -> str:
-    # grab the ip address of vpnserver from the config file
-    if port == "tcp":
-        folder = "ovpn_tcp/"
-    else:
-        folder = "ovpn_udp/"
-
-    vpn_config_file = __basefilepath__ + "files/" + folder + server + ".nordvpn.com." + port + ".ovpn"
-    with open(vpn_config_file, 'r') as openvpn_file:
-        for line in openvpn_file:
-            if "remote " in line:
-                vpn_server_ip = line[7:]
-                vpn_server_ip = vpn_server_ip[:vpn_server_ip.find(" ")]
-        openvpn_file.close()
-        return vpn_server_ip
+    # grab the IP address of VPN server from the config file
+    vpn_config_file = os.path.join(ovpn_folder, "ovpn_{}".format(port), "{}.nordvpn.com.{}.ovpn").format(server, port)
+    for _ in range(2):
+        try:
+            with open(vpn_config_file, "r") as openvpn_file:
+                for line in openvpn_file:
+                    if "remote " in line:
+                        vpn_server_ip = line[7:]
+                        vpn_server_ip = vpn_server_ip[:vpn_server_ip.find(" ")]
+            return vpn_server_ip
+        except FileNotFoundError:
+            logger.notice("VPN configuration file for '{}' doesn't exist. auto downloading config files".format(server))
+            update_config_files()
+            continue
+        else:
+            raise RuntimeError("FileNotFoundError: Get the latest config files by running 'sudo openpyn --update'")
 
 
+# pylint: disable=C0415
 def uses_systemd_resolved() -> bool:
     # see https://www.freedesktop.org/software/systemd/man/systemd-resolved.service.html
-
     try:
-        systemd_resolved_running = subprocess.call(
-            ["systemctl", "is-active", "systemd-resolved"],
-            stdout=subprocess.DEVNULL,
-            stderr=subprocess.DEVNULL,
-        ) == 0
-    except FileNotFoundError:   # When OS doesn't find systemctl
+        systemd_resolved_running = (
+            subprocess.call(
+                ["systemctl", "is-active", "systemd-resolved"],
+                stdout=subprocess.DEVNULL,
+                stderr=subprocess.DEVNULL,
+            )
+            == 0
+        )
+    except FileNotFoundError:  # when OS doesn't find systemctl
         return False
 
     if not systemd_resolved_running:
@@ -820,8 +1219,9 @@ def uses_systemd_resolved() -> bool:
 
     stub_systemd_resolver = "127.0.0.53"  # seems to be hardcoded in systemd
     dns_servers = []
-    with open('/etc/resolv.conf', 'r') as f:
+    with open("/etc/resolv.conf", "r") as f:
         import re
+
         ns_rgx = re.compile("nameserver (.*)")
         for line in f:
             m = ns_rgx.match(line)
@@ -832,102 +1232,95 @@ def uses_systemd_resolved() -> bool:
     if resolv_conf_managed:
         return True
 
-    # otherwise, something must be broken.. why is systemd-resolved running yet resolv.conf still pointing somewhere else?
+    # otherwise, something must be broken.. why is systemd-resolved running, yet resolv.conf still pointing somewhere else?
     # TODO test implications
-    logger.warning(
-        "systemd-resolved is running, but resolv.conf contains {}, test if DNS leaks!".format(dns_servers))
+    logger.warning("systemd-resolved is running, but resolv.conf contains %s, test if DNS leaks!", dns_servers)
     return True
 
 
-def connect(server: str, port: str, silent: bool, test: bool, skip_dns_patch: bool,
-            openvpn_options: str, server_provider="nordvpn") -> bool:
+def connect(server: str, port: str, silent: bool, skip_dns_patch: bool, app: bool,
+            no_redirect: bool, openvpn_options: str, add_route: str, use_systemd_resolved: bool, use_resolvconf: bool,
+            server_provider="nordvpn") -> None:
     detected_os = sys.platform
     if server_provider == "nordvpn":
-        if port == "tcp":
-            folder = "ovpn_tcp/"
-        else:
-            folder = "ovpn_udp/"
-
-        vpn_config_file = __basefilepath__ + "files/" + folder + server +\
-            ".nordvpn.com." + port + ".ovpn"
+        vpn_config_file = os.path.join(ovpn_folder, "ovpn_{}".format(port), "{}.nordvpn.com.{}.ovpn").format(server, port)
         # logger.debug("CONFIG FILE %s", vpn_config_file)
         if os.path.isfile(vpn_config_file) is False:
-            logger.notice("VPN configuration file %s doesn't exist, \
-don't worry running 'openpyn --update' for you :)", vpn_config_file)
+            logger.notice(
+                "VPN configuration file %s doesn't exist, don't worry running 'openpyn --update' for you :)",
+                vpn_config_file,
+            )
             time.sleep(6)
             update_config_files()
-    elif server_provider == "ipvanish":
-        vpn_config_file = __basefilepath__ + "files/" + "ipvanish/" + server
-        # logger.debug("ipvanish")
-
-    if test:
-        logger.success("Simulation end reached, \
-openpyn would have connected to server: " + server + " on port: " + port + " with 'silent' mode: " + str(silent).lower())
-        return 0
-
-    kill_vpn_processes()   # kill existing OpenVPN processes
-    kill_management_client()
-    logger.success("CONNECTING TO SERVER " + server + " ON PORT " + port)
 
-    root_access = root.verify_root_access("Sudo credentials required to run 'openvpn'")
+    root_access = root.verify_root_access("Root access needed to run 'openvpn'")
     if root_access is False:
         root.obtain_root_access()
 
+    kill_management_client()
+    kill_vpn_processes()  # kill existing OpenVPN processes
+
     if not silent:
-        # notifications Don't work with 'sudo'
+        # notifications don't work with 'sudo'
         if detected_os == "linux" and root.running_with_sudo():
-            logger.warning("Desktop notifications don't work when using 'sudo', run without it, \
-when asked, provide the sudo credentials")
+            logger.warning(
+                "Desktop notifications don't work when using 'sudo', run without it, when asked, provide the sudo"
+                " credentials"
+            )
             subprocess.Popen("openpyn-management".split())
         else:
             subprocess.Popen("openpyn-management --do-notify".split())
-    use_systemd_resolved = False
-    use_resolvconf = False
-    if detected_os == "linux":
-        if subprocess.check_output(["/bin/uname", "-o"]).decode(sys.stdout.encoding).strip() == "ASUSWRT-Merlin":
-            skip_dns_patch = True
-        elif os.path.exists("/etc/openwrt_release"):
-            skip_dns_patch = True
-        else:
-            use_systemd_resolved = uses_systemd_resolved()
-            use_resolvconf = os.path.isfile("/sbin/resolvconf")
-    else:
-        use_systemd_resolved = False
-        use_resolvconf = False
-        skip_dns_patch = True
+
     if not openvpn_options:
         openvpn_options = ""
+
+    # if add_route used, allow incoming traffic to go back through default interface,
+    if add_route:
+        routes.add_route()
+        no_redirect = True  # otherwise, routing won't work
+    # if not specified, force --redirect-gateway
+    if not no_redirect:
+        openvpn_options += " --redirect-gateway"
+
+    logger.success("CONNECTING TO SERVER " + server + " ON PORT " + port)
+
     if (use_systemd_resolved or use_resolvconf) and skip_dns_patch is False:  # Debian Based OS + do DNS patching
         try:
             if use_systemd_resolved:
                 openvpn_options += " " + "--dhcp-option DOMAIN-ROUTE ."
                 up_down_script = __basefilepath__ + "scripts/update-systemd-resolved.sh"
-                logger.success("Your OS '%s' has systemd-resolve running, \
-using it to update DNS Resolver Entries", detected_os)
+                logger.success(
+                    "Your OS '%s' has systemd-resolve running, using it to update DNS Resolver Entries", detected_os
+                )
             elif use_resolvconf:
                 # tunnel DNS through VPN by changing /etc/resolv.conf using
                 # "update-resolv-conf.sh" to change the DNS servers to NordVPN's.
 
                 up_down_script = __basefilepath__ + "scripts/update-resolv-conf.sh"
-                logger.success("Your OS '%s' Does have '/sbin/resolvconf', \
-using it to update DNS Resolver Entries", detected_os)
+                logger.success("Your OS '%s' has '/sbin/resolvconf', using it to update DNS Resolver Entries", detected_os)
             else:
                 raise RuntimeError("Should not happen")
 
             def run_openvpn(*args):
                 cmdline = [
-                    "sudo", "openvpn",
-                    "--redirect-gateway",
+                    "sudo", "-u", sudo_user, "openvpn",
+                    "--status", "{}/openvpn-status".format(log_folder), "30",
                     "--auth-retry", "nointeract",
                     "--config", vpn_config_file,
                     "--auth-user-pass", __basefilepath__ + "credentials",
                     "--script-security", "2",
                     "--up", up_down_script,
                     "--down", up_down_script,
-                    "--down-pre",
+                    # "--down-pre",
                     *args,
                 ] + openvpn_options.split()
-                subprocess.run(cmdline, check=True)
+                completed = subprocess.run(cmdline, check=True)
+
+                # "killall openvpn" - the default signal sent is SIGTERM
+                # SIGTERM signal causes OpenVPN to exit gracefully - OpenVPN exits with 0 status
+                # logger.debug("RETURN CODE, {}".format(completed.returncode))
+                if completed.returncode == 0:
+                    raise SystemExit
 
             if silent:
                 run_openvpn()
@@ -939,60 +1332,70 @@ def run_openvpn(*args):
         except subprocess.CalledProcessError as openvpn_err:
             # logger.debug(openvpn_err.output)
             if "Error opening configuration file" in str(openvpn_err.output):
-                logger.error(
-                    "Error opening config file %s, make sure it exists, run 'openpyn --update'", vpn_config_file)
-                return 1
+                raise RuntimeError(
+                    "Error opening config %s, make sure it exists, run 'openpyn --update'" % vpn_config_file
+                ) from openvpn_err
         except KeyboardInterrupt:
-            logger.info("Shutting down safely, please wait until process exits")
-            return 0
-        except PermissionError:     # needed cause complains when killing sudo process
-            return 0
-
-    else:       # If not Debian Based or skip_dns_patch
+            raise SystemExit from None
+        except PermissionError:  # needed cause complains when killing sudo process
+            raise SystemExit from None
+    else:  # if not Debian Based or skip_dns_patch
         # if skip_dns_patch, do not touch etc/resolv.conf
         if skip_dns_patch is False:
             logger.warning("Your OS '%s' Does not have '/sbin/resolvconf'", detected_os)
-            logger.notice(
-                "Manually applying patch to tunnel DNS through the VPN tunnel by modifying '/etc/resolv.conf'")
-            subprocess.call(["sudo", __basefilepath__ + "scripts/manual-dns-patch.sh"])
+            logger.notice("Manually applying patch to tunnel DNS through the VPN tunnel by modifying '/etc/resolv.conf'")
+            subprocess.call(["sudo", "-u", sudo_user, __basefilepath__ + "scripts/manual-dns-patch.sh"])
         else:
-            logger.warning(
-                "Not modifying '/etc/resolv.conf', DNS traffic likely won't go through the encrypted tunnel")
-        try:   # pylint: disable=R1702
-            if silent:
-                if detected_os == "linux":
-                    if subprocess.check_output(["/bin/uname", "-o"]).decode(sys.stdout.encoding).strip() == "ASUSWRT-Merlin":
-                        # make sure module is loaded
-                        if os.popen("test ! -c /dev/net/tun && echo 0 || echo 1").read()[0:-1] == '0':
-                            subprocess.call("modprobe tun", shell=True)
-                            if os.popen("test ! -c /dev/net/tun && echo 0 || echo 1").read()[0:-1] == '0':
-                                logger.error(
-                                    "Cannot open TUN/TAP dev /dev/net/tun: No such file or directory")
-                                return 1
-                subprocess.run(
-                    ["sudo", "openvpn", "--redirect-gateway", "--auth-retry",
-                     "nointeract", "--config", vpn_config_file, "--auth-user-pass",
-                     __basefilepath__ + "credentials"]
-                    + openvpn_options.split(), check=True)
+            logger.warning("Not modifying '/etc/resolv.conf', DNS traffic likely won't go through the encrypted tunnel")
+
+        try:
+
+            def run_openvpn(*args):
+                cmdline = [
+                    "sudo", "-u", sudo_user, "openvpn",
+                    "--status", "{}/openvpn-status".format(log_folder), "30",
+                    "--config", vpn_config_file,
+                    *args,
+                ] + openvpn_options.split()
+                completed = subprocess.run(cmdline, check=True)
+
+                # "killall openvpn" - the default signal sent is SIGTERM
+                # SIGTERM signal causes OpenVPN to exit gracefully - OpenVPN exits with 0 status
+                # logger.debug("RETURN CODE, {}".format(completed.returncode))
+                if completed.returncode == 0:
+                    raise SystemExit
+
+            if app:
+                # an app can allow username and password to be requested and entered via the management socket
+                # the app in question would need to pass the following OpenVPN options to --openvpn-options
+                # --auth-retry nointeract --management 127.0.0.1 7015 --management-hold --management-query-passwords
+                # "--auth-retry" can be either "nointeract" or "interact" depending on the functionality within the app
+                # user auth is done via the app not via the router, hence credentials file may contain invalid credentials
+                # Android app by 1951FDG will use this when both username and password are provided in settings
+                run_openvpn()
+            elif silent:
+                run_openvpn(
+                    "--auth-retry", "nointeract",
+                    "--auth-user-pass", __basefilepath__ + "credentials",
+                )
             else:
-                subprocess.run(
-                    ["sudo", "openvpn", "--redirect-gateway", "--auth-retry",
-                     "nointeract", "--config", vpn_config_file, "--auth-user-pass",
-                     __basefilepath__ + "credentials",
-                     "--management", "127.0.0.1", "7015", "--management-up-down"]
-                    + openvpn_options.split(), check=True)
+                run_openvpn(
+                    "--auth-retry", "nointeract",
+                    "--auth-user-pass", __basefilepath__ + "credentials",
+                    "--management", "127.0.0.1", "7015",
+                    "--management-up-down",
+                )
         except subprocess.CalledProcessError as openvpn_err:
             # logger.debug(openvpn_err.output)
-            if 'Error opening configuration file' in str(openvpn_err.output):
-                logger.error(
-                    "Error opening config file %s, make sure it exists, run 'openpyn --update'", vpn_config_file)
-                return 1
+            if "Error opening configuration file" in str(openvpn_err.output):
+                raise RuntimeError(
+                    "Error opening config %s, make sure it exists, run 'openpyn --update'" % vpn_config_file
+                ) from openvpn_err
         except KeyboardInterrupt:
-            logger.info('Shutting down safely, please wait until process exits')
-            return 0
-        except PermissionError:     # needed cause complains when killing sudo process
-            return 0
+            raise SystemExit from None
+        except PermissionError:  # needed cause complains when killing sudo process
+            raise SystemExit from None
 
 
-if __name__ == '__main__':
+if __name__ == "__main__":
     sys.exit(main())
diff --git a/openpyn/root.py b/openpyn/root.py
index 7548261..be06b52 100644
--- a/openpyn/root.py
+++ b/openpyn/root.py
@@ -2,16 +2,17 @@
 import os
 import pwd
 import subprocess
-import sys
 
 import verboselogs
 
+from openpyn import sudo_user
+
 verboselogs.install()
 logger = logging.getLogger(__package__)
 
 
+# Check that user has root privileges, if not print message
 def verify_root_access(message: str) -> bool:
-    # Check that user has root privileges. if not print message
     # in a case when starting openpyn without sudo then providing sudo privileges when asked,
     # sudo privileges get cached, os.getuid would say user not root and prints "root needed"
     # messages, but it would work
@@ -21,8 +22,7 @@ def verify_root_access(message: str) -> bool:
     #        return False
 
     try:
-        subprocess.check_output(
-            ["sudo", "-n", "cat", "/etc/resolv.conf"], stderr=subprocess.DEVNULL)
+        subprocess.check_output(["sudo", "-u", sudo_user, "-n", "cat", "/etc/resolv.conf"], stderr=subprocess.DEVNULL)
     # -n 'non-interactive' mode used to, not prompt for password (if user not sudo) but throw err.
     except subprocess.CalledProcessError:
         logger.notice(message)
@@ -30,7 +30,7 @@ def verify_root_access(message: str) -> bool:
     return True
 
 
-# check if openpyn itself has been started with root access.
+# Check if openpyn itself has been started with root access.
 def verify_running_as_root() -> bool:
     if os.getuid() == 0:
         # logger.debug(message)
@@ -40,15 +40,16 @@ def verify_running_as_root() -> bool:
 
 def obtain_root_access() -> None:
     # asks for sudo password to be cached
-    try:    # try accessing root read only file "600" permission, ask for sudo pass
+    try:  # try accessing root read only file "600" permission, ask for sudo pass
         subprocess.call(
-            ["sudo", "cat", "/etc/resolv.conf"],
-            stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL)
-    except subprocess.CalledProcessError:
-        logger.error("except occurred while running obtain_root_access() 'sudo ls' command")
+            ["sudo", "-u", sudo_user, "cat", "/etc/resolv.conf"],
+            stderr=subprocess.DEVNULL,
+            stdout=subprocess.DEVNULL,
+        )
+    except subprocess.CalledProcessError as e:
+        raise RuntimeError("except occurred while running obtain_root_access() 'sudo cat /etc/resolv.conf' command") from e
     except KeyboardInterrupt:
-        logger.info('(KeyboardInterrupt) Ctrl+C received, Bye')
-        sys.exit()
+        raise RuntimeError("Ctrl+C received, Bye") from None
 
 
 def logged_in_user_is_root(username: str) -> bool:
@@ -65,15 +66,13 @@ def running_with_sudo() -> bool:
         try:
             logged_in_user = os.getlogin()
             if logged_in_user_is_root(logged_in_user):
-                return False    # when logged in as 'root' user notifications will work.
-            return True     # 'sudo' is used notification won't work.
+                return False  # when logged in as 'root' user notifications will work.
+            return True  # 'sudo' is used notification won't work.
         except FileNotFoundError:
-            logger.verbose(
-                "os.getlogin(), returned FileNotFoundError, assuming 'openpyn' is running with 'SUDO'")
+            logger.verbose("os.getlogin(), returned FileNotFoundError, assuming 'openpyn' is running with 'SUDO'")
             return True
         except OSError:
-            logger.verbose(
-                "os.getlogin(), returned error, assuming 'openpyn' is running with 'SUDO'")
+            logger.verbose("os.getlogin(), returned error, assuming 'openpyn' is running with 'SUDO'")
             return True
 
-    return False    # regular user without 'sudo'
+    return False  # regular user without 'sudo'
diff --git a/openpyn/routes.py b/openpyn/routes.py
new file mode 100644
index 0000000..62bd60d
--- /dev/null
+++ b/openpyn/routes.py
@@ -0,0 +1,36 @@
+import subprocess
+import sys
+
+from openpyn import sudo_user
+
+
+# Add route to allow traffic to go out of the public ip
+# For opening services like ssh/http on anything on public net,
+# Needed on VPSs.
+def add_route():
+    table_number = "175"
+
+    default_route = subprocess.check_output(["ip", "route", "show", "default"]).decode(sys.stdout.encoding).strip().split()
+    default_gateway_ip = default_route[2]
+    # default_interface = default_route[4]
+
+    # Get the first IP address reported by 'hostname --all-ip-addresses'
+    ip_addr = subprocess.check_output(["hostname", "--all-ip-addresses"]).decode(sys.stdout.encoding).strip().split()[0]
+
+    route_rule = "from {} lookup {}".format(ip_addr, table_number)
+    # print(route_rule)
+    ip_rules = str(subprocess.check_output(["ip", "rule", "list"]))
+    if route_rule in ip_rules:
+        print("IP route rule already exists, skipping")
+    else:
+        # Add ip rule and route to send traffic through the default gateway.
+        subprocess.call(
+            ["sudo", "-u", sudo_user, "ip", "rule", "add", "from", ip_addr, "table", table_number]
+        )
+        # subprocess.call(
+        #     ["sudo", "-u", sudo_user, "ip", "route", "add", "table", table_number, "to", ip_addr + "/32", "dev", default_interface]
+        # )
+
+        subprocess.call(
+            ["sudo", "-u", sudo_user, "ip", "route", "add", "table", table_number, "default", "via", default_gateway_ip]
+        )
diff --git a/openpyn/scripts/manual-dns-patch.sh b/openpyn/scripts/manual-dns-patch.sh
index a668eec..0491c97 100755
--- a/openpyn/scripts/manual-dns-patch.sh
+++ b/openpyn/scripts/manual-dns-patch.sh
@@ -9,6 +9,10 @@ echo "nameserver nordDNS1 = $nordDNS1"
 echo "nameserver nordDNS2 = $nordDNS2"
 echo "nameserver openDNS3 = $openDNS3"
 
+# try to move current resolv.conf to a backup file before overwriting
+# if backup already exists, do not overwrite backup
+mv -n /etc/resolv.conf /etc/resolv.conf.backup
+
 echo "nameserver $nordDNS1" >  /etc/resolv.conf
 echo "nameserver $nordDNS2" >> /etc/resolv.conf
 echo "nameserver $openDNS3" >> /etc/resolv.conf
diff --git a/openpyn/scripts/revert-manual-dns-patch.sh b/openpyn/scripts/revert-manual-dns-patch.sh
new file mode 100755
index 0000000..f58fc79
--- /dev/null
+++ b/openpyn/scripts/revert-manual-dns-patch.sh
@@ -0,0 +1,6 @@
+#!/usr/bin/env sh
+
+echo "Reverting /etc/resolv.conf back to original"
+
+# force overwrites resolv.conf with the backup
+mv -f /etc/resolv.conf.backup /etc/resolv.conf
diff --git a/openpyn/scripts/update-systemd-resolved.sh b/openpyn/scripts/update-systemd-resolved.sh
index 6706de3..1109281 100755
--- a/openpyn/scripts/update-systemd-resolved.sh
+++ b/openpyn/scripts/update-systemd-resolved.sh
@@ -28,7 +28,6 @@
 # Define what needs to be called via DBus
 DBUS_DEST="org.freedesktop.resolve1"
 DBUS_NODE="/org/freedesktop/resolve1"
-PATH="$PATH:/usr/sbin"
 
 SCRIPT_NAME="${BASH_SOURCE[0]##*/}"
 
@@ -74,6 +73,8 @@ dhcp_settings() {
 }
 
 up() {
+  # echo UP with date but strip timezone info
+  echo "UP $(date '+%T %F')" > /var/log/openpyn/status
   local link="$1"
   shift
   local if_index="$1"
@@ -138,6 +139,8 @@ up() {
 }
 
 down() {
+  # echo DOWN with date but strip timezone info
+  echo "DOWN $(date '+%T %F')" > /var/log/openpyn/status
   local link="$1"
   shift
   local if_index="$1"
diff --git a/openpyn/systemd.py b/openpyn/systemd.py
index 8c36331..85526f7 100644
--- a/openpyn/systemd.py
+++ b/openpyn/systemd.py
@@ -1,4 +1,6 @@
 import logging
+import os
+import shutil
 import subprocess
 import sys
 
@@ -10,55 +12,65 @@
 logger = logging.getLogger(__package__)
 
 
-def install_service() -> int:
+def install_service() -> None:
     if not sys.__stdin__.isatty():
-        logger.critical("Please run %s in interactive mode", __name__)
-        return 1
+        raise RuntimeError("Please run %s in interactive mode" % __name__)
 
-    openpyn_options = input("\nEnter Openpyn options to be stored in systemd \
-service file (/etc/systemd/system/openpyn.service, \
-Default(Just Press Enter) is, uk : ") or "uk"
+    openpyn_options = (
+        input(
+            "\nEnter Openpyn options to be stored in systemd service file (/etc/systemd/system/openpyn.service, Default(Just"
+            " Press Enter) is, uk : "
+        )
+        or "uk"
+    )
     update_service(openpyn_options)
-    return 0
 
 
 def update_service(openpyn_options: str, run=False) -> None:
-    if "-f" in openpyn_options or "--force-fw-rules" in openpyn_options:
-        kill_option = " --kill-flush"
-    else:
-        kill_option = " --kill"
+    # if '-f' is used, you'll need to manually clear the killswitch with 'openpyn -x'
+    kill_option = " --kill"
     openpyn_options = openpyn_options.replace("-d ", "")
     openpyn_options = openpyn_options.replace("--daemon", "")
-    openpyn_location = str(subprocess.check_output(["which", "openpyn"]))[2:-3]
-    sleep_location = str(subprocess.check_output(["which", "sleep"]))[2:-3]
+    openpyn_location = shutil.which("openpyn")
+    sleep_location = shutil.which("sleep")
 
-    service_text = "[Unit]\nDescription=NordVPN connection manager\nWants=network-online.target\n" + \
-        "After=network-online.target\nAfter=multi-user.target\n[Service]\nType=simple\nUser=root\n" + \
-        "WorkingDirectory=" + __basefilepath__ + "\nExecStartPre=" + sleep_location + " 5\nExecStart=" + \
-        openpyn_location + " " + openpyn_options + "\nExecStop=" + openpyn_location + kill_option + \
-        "\nStandardOutput=syslog\nStandardError=syslog\n[Install]\nWantedBy=multi-user.target\n"
+    service_text = (
+        "[Unit]\nDescription=NordVPN connection manager\nWants=network-online.target\n"
+        + "After=network-online.target\nAfter=multi-user.target\n[Service]\nType=simple\nUser=root\n"
+        + "WorkingDirectory="
+        + __basefilepath__
+        + "\nExecStartPre="
+        + sleep_location
+        + " 5\nExecStart="
+        + openpyn_location
+        + " "
+        + openpyn_options
+        + "\nExecStop="
+        + openpyn_location
+        + kill_option
+        + "\nStandardOutput=syslog\nStandardError=syslog\n[Install]\nWantedBy=multi-user.target\n"
+    )
 
-    with open("/etc/systemd/system/openpyn.service", "w+") as service_file:
+    systemd_service_path = os.path.join("/", "etc", "systemd", "system")
+    systemd_service_file = os.path.join(systemd_service_path, "openpyn.service")
+
+    if not os.path.exists(systemd_service_path):
+        os.makedirs(systemd_service_path)
+        os.chmod(systemd_service_path, 0o777)
+    with open(systemd_service_file, "w+") as service_file:
         service_file.write(service_text)
-        service_file.close()
 
-    logger.notice("The Following config has been saved in openpyn.service. \
-You can Run it or/and Enable it with: 'sudo systemctl start openpyn', \
-'sudo systemctl enable openpyn' \n" + service_text)
+    logger.notice(
+        "The Following config has been saved in openpyn.service. You can Run it or/and Enable it with: 'sudo systemctl start"
+        " openpyn', 'sudo systemctl enable openpyn' \n\n"
+        + service_text
+    )
 
     subprocess.run(["systemctl", "daemon-reload"])
-    if run:
-        daemon_running = subprocess.call(  # subprocess.run behaves differently
-            ["systemctl", "is-active", "openpyn"],
-            stdout=subprocess.DEVNULL,
-            stderr=subprocess.DEVNULL,
-        ) == 0
 
-        if daemon_running:
-            logger.notice("Restarting Openpyn by running 'systemctl restart openpyn'\n\
-To check VPN status, run 'systemctl status openpyn'")
-            subprocess.Popen(["systemctl", "restart", "openpyn"])
-        else:
-            logger.notice("Starting Openpyn by running 'systemctl start openpyn'\n\
-To check VPN status, run 'systemctl status openpyn'")
-            subprocess.Popen(["systemctl", "start", "openpyn"])
+    if run:
+        logger.notice(
+            "Starting/Restarting Openpyn by running 'systemctl restart openpyn'\nTo check VPN status, run 'systemctl status"
+            " openpyn'"
+        )
+        subprocess.Popen(["systemctl", "restart", "openpyn"])
diff --git a/pylama.ini b/pylama.ini
index 7ca40ef..48ae48f 100644
--- a/pylama.ini
+++ b/pylama.ini
@@ -11,4 +11,4 @@ max_line_length = 125
 max-line-length = 125
 
 
-disable = C0103,C0111,R0916,R0914,C901,W0406
+disable = C0103,C0114,C0115,C0116,C0301,R0902,R0911,R0912,R0913,R0914,R0915,R0916
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..700d3c2
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,5 @@
+# Example configuration for Black.
+
+[tool.black]
+line-length = 125
+target-version = ['py35']
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..11adc70
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,7 @@
+verboselogs
+requests
+coloredlogs
+colorama
+tqdm
+jsonschema
+pytest
diff --git a/setup.cfg b/setup.cfg
index a8dfae3..b0c4a28 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -3,3 +3,7 @@
 [metadata]
 description-file = README.md
 license_file = LICENSE.md
+
+[flake8]
+ignore = N806
+max-line-length = 125
diff --git a/setup.py b/setup.py
old mode 100755
new mode 100644
index 746cedb..698eda8
--- a/setup.py
+++ b/setup.py
@@ -1,16 +1,15 @@
 import sys
 
 import setuptools
+
 from openpyn import __data_files__, __version__
 
 if sys.version_info < (3, 5):
-    sys.stderr.write("ERROR: openpyn requires Python 3.5 or above." +
-                     "Install using 'pip3' instead of just 'pip' \n")
+    sys.stderr.write("ERROR: openpyn requires Python 3.5 or above." + "Install using 'pip3' instead of just 'pip' \n")
     sys.exit(1)
 
-with open('README.md', encoding='utf-8') as readme_file:
+with open("README.md", encoding="utf-8") as readme_file:
     full_description = readme_file.read()
-    readme_file.close()
 
 setuptools.setup(
     name='openpyn',
@@ -21,17 +20,15 @@
     zip_safe=False,
     author_email='joty@mygnu.org',
     url='https://github.com/jotyGill/openpyn-nordvpn/',
-    keywords=[
-        'openvpn wrapper', 'nordvpn', 'nordvpn client', 'secure vpn',
-        'vpn wrapper', 'private vpn', 'privacy'],
-    install_requires=['requests', 'colorama', 'coloredlogs', 'verboselogs'],
+    keywords=['openvpn wrapper', 'nordvpn', 'nordvpn client', 'secure vpn', 'vpn wrapper', 'private vpn', 'privacy'],
+    python_requires='>=3.5',
+    install_requires=['colorama', 'coloredlogs', 'jsonschema', 'requests', 'tqdm', 'verboselogs'],
     tests_require=['pytest', 'mock'],
     platforms=['GNU/Linux', 'Ubuntu', 'Debian', 'Kali', 'CentOS', 'Arch', 'Fedora'],
     packages=setuptools.find_packages(),
     entry_points={
-        'console_scripts': [
-            'openpyn = openpyn.openpyn:main',
-            'openpyn-management = openpyn.management.management:main']},
+        'console_scripts': ['openpyn = openpyn.openpyn:main', 'openpyn-management = openpyn.management.management:main']
+    },
     data_files=__data_files__,
     include_package_data=True,
     exclude_package_data={'openpyn': ['creds', 'credentials', 'install.sh', '.gitignore']},
@@ -48,5 +45,8 @@
         'Programming Language :: Python :: 3 :: Only',
         'Programming Language :: Python :: 3.5',
         'Programming Language :: Python :: 3.6',
+        'Programming Language :: Python :: 3.7',
+        'Programming Language :: Python :: 3.8',
+        'Programming Language :: Python :: 3.9',
     ],
 )


Test patch:
diff --git a/tests/test_openpyn.py b/tests/test_openpyn.py
index 0e751f7..859c372 100644
--- a/tests/test_openpyn.py
+++ b/tests/test_openpyn.py
@@ -1,29 +1,20 @@
 #!/usr/bin/env python3
-
-import argparse
-import datetime
-import json
-import logging
-import logging.handlers
-import sys
-import time
-
-import mock
-import pytest
+from unittest import mock
 
 from openpyn import openpyn
 
+
 # def test_version():
 #     assert openpyn.__version__ == "2.7.0"
 
 
 def test_list():
-    with mock.patch('sys.argv', ['openpyn', '-l']):
+    with mock.patch("sys.argv", ["openpyn", "-l"]):
         assert openpyn.main() == 0
 
 
 def test_list_tor():
-    with mock.patch('sys.argv', ['openpyn', '-l', '--tor']):
+    with mock.patch("sys.argv", ["openpyn", "-l", "--tor"]):
         assert openpyn.main() == 0
 
 
@@ -38,40 +29,45 @@ def test_list_tor():
 #
 #
 # def test_list_country_area_state():
-#     with mock.patch('sys.argv', ['openpyn', '-l', 'au', '--area', 'sa']):
+#     with mock.patch('sys.argv', ['openpyn', '-l', 'au', '--area', 'south australia']):
 #         assert openpyn.main() == 0
 
 
 def test_list_country_area_state_p2p():
-    with mock.patch('sys.argv', ['openpyn', '-l', 'au', '--area', 'sa', '--p2p']):
+    with mock.patch("sys.argv", ["openpyn", "-l", "au", "--area", "south australia", "--p2p"]):
         assert openpyn.main() == 0
 
 
 def test_connect_country_code():
-    with mock.patch('sys.argv', ['openpyn', 'au', '--test']):
+    with mock.patch("sys.argv", ["openpyn", "au", "--test"]):
+        assert openpyn.main() == 0
+
+
+def test_connect_country_location():
+    with mock.patch("sys.argv", ["openpyn", "au", "--location", "-37.813938", "144.963425", "--test"]):
         assert openpyn.main() == 0
 
 
 def test_connect_country_name():
-    with mock.patch('sys.argv', ['openpyn', 'australia', '--test']):
+    with mock.patch("sys.argv", ["openpyn", "australia", "--test"]):
         assert openpyn.main() == 0
 
 
 def test_connect_country_code_arg():
-    with mock.patch('sys.argv', ['openpyn', '-c', 'au', '--test']):
+    with mock.patch("sys.argv", ["openpyn", "-c", "au", "--test"]):
         assert openpyn.main() == 0
 
 
 def test_connect_max_load():
-    with mock.patch('sys.argv', ['openpyn', 'au', '-m', '50', '--test']):
+    with mock.patch("sys.argv", ["openpyn", "au", "-m", "50", "--test"]):
         assert openpyn.main() == 0
 
 
 def test_connect_top_servers():
-    with mock.patch('sys.argv', ['openpyn', 'au', '-t', '5', '--test']):
+    with mock.patch("sys.argv", ["openpyn", "au", "-t", "5", "--test"]):
         assert openpyn.main() == 0
 
 
-def test_connect_pings():
-    with mock.patch('sys.argv', ['openpyn', 'au', '-p', '5', '--test']):
+def test_connect_parallel():
+    with mock.patch("sys.argv", ["openpyn", "de", "--test"]):
         assert openpyn.main() == 0