Browse Source

Merge pull request #171 from gravitl/v0.5

V0.5
Alex 4 years ago
parent
commit
7cc85b6fc1
100 changed files with 7235 additions and 1463 deletions
  1. 1 2
      .github/workflows/test.yml
  2. 21 120
      README.md
  3. 26 0
      compose/docker-compose.localserver.yml
  4. 55 0
      compose/docker-compose.noclient.yml
  5. 3 4
      compose/docker-compose.nodns.yml
  6. 85 0
      compose/docker-compose.reference.yml
  7. 42 0
      compose/docker-compose.server-only.yml
  8. 44 0
      compose/docker-compose.slim.yml
  9. 7 5
      compose/docker-compose.yml
  10. 14 2
      config/config.go
  11. 8 0
      config/dnsconfig/Corefile
  12. 1 0
      config/dnsconfig/netmaker.hosts
  13. 10 0
      config/environments/dev.yaml
  14. 122 123
      controllers/common.go
  15. 534 0
      controllers/common_test.go
  16. 8 0
      controllers/config/dnsconfig/Corefile
  17. 1 0
      controllers/config/dnsconfig/netmaker.hosts
  18. 17 0
      controllers/config/environments/dev.yaml
  19. 8 7
      controllers/controller.go
  20. 288 288
      controllers/dnsHttpController.go
  21. 180 0
      controllers/dnsHttpController_test.go
  22. 464 0
      controllers/extClientHttpController.go
  23. 207 0
      controllers/intClientHttpController.go
  24. 331 309
      controllers/networkHttpController.go
  25. 572 0
      controllers/networkHttpController_test.go
  26. 296 219
      controllers/nodeGrpcController.go
  27. 273 193
      controllers/nodeHttpController.go
  28. 194 0
      controllers/nodeHttpController_test.go
  29. 57 0
      controllers/responseHttp_test.go
  30. 42 13
      controllers/serverHttpController.go
  31. 121 178
      controllers/userHttpController.go
  32. 257 0
      controllers/userHttpController_test.go
  33. 2 0
      docs/Dockerfile
  34. 20 0
      docs/Makefile
  35. BIN
      docs/_build/doctrees/about.doctree
  36. BIN
      docs/_build/doctrees/api.doctree
  37. BIN
      docs/_build/doctrees/architecture.doctree
  38. BIN
      docs/_build/doctrees/client-installation.doctree
  39. BIN
      docs/_build/doctrees/conduct.doctree
  40. BIN
      docs/_build/doctrees/contact.doctree
  41. BIN
      docs/_build/doctrees/contribute.doctree
  42. BIN
      docs/_build/doctrees/environment.pickle
  43. BIN
      docs/_build/doctrees/external-clients.doctree
  44. BIN
      docs/_build/doctrees/getting-started.doctree
  45. BIN
      docs/_build/doctrees/index.doctree
  46. BIN
      docs/_build/doctrees/introduction.doctree
  47. BIN
      docs/_build/doctrees/license.doctree
  48. BIN
      docs/_build/doctrees/quick-start.doctree
  49. BIN
      docs/_build/doctrees/server-installation.doctree
  50. BIN
      docs/_build/doctrees/support.doctree
  51. BIN
      docs/_build/doctrees/troubleshoot.doctree
  52. BIN
      docs/_build/doctrees/tutorials.doctree
  53. BIN
      docs/_build/doctrees/usage.doctree
  54. BIN
      docs/_build/doctrees/walkthroughs.doctree
  55. 4 0
      docs/_build/html/.buildinfo
  56. BIN
      docs/_build/html/_images/access-key.png
  57. BIN
      docs/_build/html/_images/create-user.png
  58. BIN
      docs/_build/html/_images/default-net.png
  59. BIN
      docs/_build/html/_images/mesh-diagram.png
  60. BIN
      docs/_build/html/_images/mesh.png
  61. BIN
      docs/_build/html/_images/nc-install-output.png
  62. BIN
      docs/_build/html/_images/netmaker.png
  63. BIN
      docs/_build/html/_images/nm-diagram-2.jpg
  64. BIN
      docs/_build/html/_images/nm-diagram.jpg
  65. BIN
      docs/_build/html/_images/nm-node-success.png
  66. BIN
      docs/_build/html/_images/node-details.png
  67. BIN
      docs/_build/html/_images/nodes.png
  68. BIN
      docs/_build/html/_images/ping-node.png
  69. 46 0
      docs/_build/html/_sources/about.rst.txt
  70. 184 0
      docs/_build/html/_sources/api.rst.txt
  71. 176 0
      docs/_build/html/_sources/architecture.rst.txt
  72. 126 0
      docs/_build/html/_sources/client-installation.rst.txt
  73. 77 0
      docs/_build/html/_sources/conduct.rst.txt
  74. 8 0
      docs/_build/html/_sources/contact.rst.txt
  75. 26 0
      docs/_build/html/_sources/contribute.rst.txt
  76. 19 0
      docs/_build/html/_sources/external-clients.rst.txt
  77. 148 0
      docs/_build/html/_sources/getting-started.rst.txt
  78. 160 0
      docs/_build/html/_sources/index.rst.txt
  79. 47 0
      docs/_build/html/_sources/introduction.rst.txt
  80. 6 0
      docs/_build/html/_sources/license.rst.txt
  81. 137 0
      docs/_build/html/_sources/quick-start.rst.txt
  82. 363 0
      docs/_build/html/_sources/server-installation.rst.txt
  83. 75 0
      docs/_build/html/_sources/support.rst.txt
  84. 19 0
      docs/_build/html/_sources/troubleshoot.rst.txt
  85. 17 0
      docs/_build/html/_sources/tutorials.rst.txt
  86. 39 0
      docs/_build/html/_sources/usage.rst.txt
  87. 37 0
      docs/_build/html/_sources/walkthroughs.rst.txt
  88. 861 0
      docs/_build/html/_static/basic.css
  89. 321 0
      docs/_build/html/_static/doctools.js
  90. 12 0
      docs/_build/html/_static/documentation_options.js
  91. BIN
      docs/_build/html/_static/file.png
  92. 3 0
      docs/_build/html/_static/fonts/font-awesome.css
  93. 13 0
      docs/_build/html/_static/fonts/material-icons.css
  94. BIN
      docs/_build/html/_static/fonts/specimen/FontAwesome.ttf
  95. BIN
      docs/_build/html/_static/fonts/specimen/FontAwesome.woff
  96. BIN
      docs/_build/html/_static/fonts/specimen/FontAwesome.woff2
  97. BIN
      docs/_build/html/_static/fonts/specimen/MaterialIcons-Regular.ttf
  98. BIN
      docs/_build/html/_static/fonts/specimen/MaterialIcons-Regular.woff
  99. BIN
      docs/_build/html/_static/fonts/specimen/MaterialIcons-Regular.woff2
  100. BIN
      docs/_build/html/_static/images/favicon.png

+ 1 - 2
.github/workflows/test.yml

@@ -19,5 +19,4 @@ jobs:
         uses: actions/checkout@v2
       - name: run tests
         run: |
-            cd test
-            go test . -v
+            go test -p 1 ./... -v

+ 21 - 120
README.md

@@ -6,139 +6,40 @@
 <i>Connect any computers together over a secure, fast, private network, and manage multiple networks from a central server.</i> 
 </p>
 
-## What is Netmaker?
-Netmaker is a tool for creating and managing virtual networks. If you have servers spread across multiple locations, data centers, or clouds, they all live on separate networks. This can make life very difficult. Netmaker takes all those machines and puts them on a single, flat network so that they can talk to each other easily and securely.
+## Documentation
 
-Think of it like Tailscale, ZeroTier, or Nebula, but faster, easier, and more dynamic.
+### Netmaker's documentation can be found at:  
+###  [docs.netmaker.org](https://docs.netmaker.org)
+###  [netmaker.readthedocs.io](https://netmaker.readthedocs.io)
+  
+For information about installing and using Netmaker, including the [quick start guide](https://docs.netmaker.org/quick-start.html), please visit one of the above documentation sites, or read on for a short description of Netmaker.
 
-You spin up the Netmaker server and UI, and then install the Netclient (agent) on your computers. Netmaker will do the rest. It will tell all of your computers how to reach each other and will keep them informed of any changes to the network.
+## What is Netmaker?
+Netmaker is a platform for creating and managing networks that are:
 
-Netmaker's handy dandy UI can be found [here](https://github.com/gravitl/netmaker-ui).
+- Fast
+- Secure
+- Dynamic
+- Virtual
+- Zero-Trust
+- WireGuard-based
 
-Under the hood, Netmaker uses WireGuard to create encrypted tunnels between every node in your virtual network, creating a full mesh overlay. Netmaker takes the work out of manually configuring machines with WireGuard and updating them every time you have a change in your network. The netclient agent is self-updating and pulls any necessary changes (such as new peers) from the server. 
+Netmaker manages networks between computers that span data centers, clouds, and edge devices. Networking between environments can be a challenge, so Netmaker is designed to allow developers, DevOps engineers, SRE's, and others to manage virtual networks with as little pain as possible. You create a network with a few clicks, and add machines to that network with a single command.
 
 ## Why Netmaker?
  1. Create a flat, secure network between multiple/hybrid cloud environments
  2. Integrate central and edge services
  3. Secure a home or office network while providing remote connectivity
- 4. Manage cryptocurrency proof-of-stake machines
- 6. Provide an additional layer of security on an existing network
- 7. Encrypt Kubernetes inter-node communications
- 8. Secure site-to-site connections
-
+ 4. Provide an additional layer of security on an existing network
+ 5. Encrypt Kubernetes inter-node communications
+ 6. Secure site-to-site connections
+ 7. Connect to IoT devices
+ 8. Manage an existing WireGuard setup with less manual configuration
 
 <p align="center">
   <img src="mesh-diagram.png">
 </p>
 
-## Compatible Systems
-
-Netmaker is primarily designed for **linux**, specifically **systemd-based linux.** This includes Fedora, Ubuntu, and Raspian. Just make sure you have WireGuard installed. Having a problem? Open an issue or Contact us.
-
-In version 0.3 we have released Private DNS. Nameservers can be configured manually on any system, but to have the Netclient add dns automatically, it requires **resolvectl.**
-
-In future releases, we have plans to support other platforms such as Windows and MacOS. 
-
-
-## Docs
-**For more information, please read the docs, or check out the Quick Start below:**
-
- - [General Usage](docs/USAGE.md)
- - [Troubleshooting](docs/TROUBLESHOOTING.md)
- - [API Documentation](docs/API.md)
- - [Product Roadmap](docs/ROADMAP.md)
- - [Contributing](docs/CONTRIBUTING.md)
-
-
-## Quick Start
-
-[Intro/Overview Video Tutorial](https://youtu.be/PWLPT320Ybo)  
-[Site-to-Site Video Tutorial](https://youtu.be/krCKBJhwwDk)  
-
-### Note about permissions
-The default installation requires special privileges on the server side, because Netmaker will control the local kernel Wireguard. This can be turned off and run in non-privileged mode if necessary (but disables some features). For more details, see the **Usage** docs.
-
-### Prereqs
- 1. A running linux server to host Netmaker, with an IP reachable by your computers (Debian-based preferred but not required).
- 2. Linux installed on the above server (Debian-based preferred but not required).
- 3. Install Docker and Docker Compose if running in Docker Mode (see below).
- 4. System dependencies installed:
-	 - Docker (if running in default Docker mode. DO NOT use snap install for docker.)
-	 - Docker Compose
-	 - Wireguard + Resolvectl (if running in default Client mode)
-
-#### CoreDNS Preparation
-v0.3 introduces CoreDNS as a private nameserver. To run CoreDNS on your server host, you must disable systemd-resolved to open port 53: 
-1. systemctl stop systemd-resolved
-2. systemctl disable systemd-resolved
-3. vim /etc/systemd/resolved.conf
-	 - uncomment **DNS=** and add 8.8.8.8 or whatever is your preference
-	 - uncomment **DNSStubListener=** and set to **"no"**
- 4. sudo ln -sf /run/systemd/resolve/resolv.conf /etc/resolv.conf
-
-
-
-### Launch Netmaker
-Note, this installs Netmaker with CoreDNS and a Netclient (privileged).  If you want to run the server non-privileged or without CoreDNS, see the advanced usage docs. 
-
-1. Clone this repo or just copy contents of "docker-compose.yml" to your Netmaker server (from prereqs).
-2. In docker-compose.yml, change BACKEND_URL to the public IP of your server.
-3. Run `sudo docker-compose up -d`
-4. Navigate to your server's IP in the browser and you should see the Netmaker UI asking to create a new admin user.
-5. Create a new admin user
-6. You are now ready to begin using Netmaker. 
-
-### Create a Network
-You can also just use the "default" network.
-1. Click "CREATE NETWORK" in the upper left of your console
-2. Enter a valid address range, e.g. 10.11.12.0/24
-3. Enter a name such as "homenet"
-4. Additional options:
-	- **Dual Stack**: Machines will recieve a private IPv6 address in addition to their IPv4 address.
-	- **Local:** Will use local address range for endpoints instead of public. Use Case: Home or Office network where most devices do not have public IP's. In this case you can create a gateway into the network after creating the Local Network.
-
-After Network creation, you can edit the network in the NETWORK DETAILS pane, modifying the address range and default options. You can also toggle on **Allow Node Signup Without Keys**, which makes the next step unnecessary, but allows anyone to create a node in your network, which will be cordoned in pending state.
-
-### Create Keys
-1. Click the "ACCESS KEYS" tab
-2. Click "ADD NEW ACCESSS KEY"
-3. Give your key a name and number of uses
-4. Several values will be displayed. Save these somewhere, as they will only be displayed once:
-	- **Access Key:** Use only in special edge cases where server connection string must be modified
-	- **Access Token:** Use on machines that already have the netclient utility
-	- **Install Command:** Use on machines that do not have the netclient utility
-
-### Install Agent:
-For machines **without** netclient, run the install command (from above): `curl -sfL https://raw.githubusercontent.com/gravitl/netmaker/v0.3/netclient-install.sh | KEY=<your access key> sh -`  
-For machines **with** netclient run the following (with access token from above): `sudo netclient -c install -t <access token>`
-For networks with **manual signup** enabled (see above), install using the network name: `sudo netclient -c install -n <network name>`
-
-### Manage Nodes
-Your machines should now be visible in the control pane. 
-**Modify nodes:** Click the pencil icon in the NODES pane to modify details like WireGuard port, address, and node name. You can also **DELETE** nodes here and they will lose network access.
-**Approve nodes:** If a node is in pending state (signed up without key), you can approve it. An icon will appear for pending nodes that need approval.
-
-**Gateway Mode:** Click the Gateway icon to enable gateway mode on a given node. A popup will allow you to choose an existing network, or enter a custom address range.
-*Example: You create a network in netmaker called Homenet. It has several machines on your home server. You create another network called Cloudnet. It has several machines in AWS. You have one server (server X) which is added to both networks. On Cloudnet, you make Server X a gateway to Homenet. Now, the cloudnet machines have access to your homenet machines. via  Server X.*
-
-*On Homenet, you add Server Y, a machine in AWS, and make it a gateway to a custom address range 172.16.0.0/16. The machines on your home network now have access to any AWS machines in that address range via Server Y*
-
-### Manage DNS
-On the DNS tab you can create custom DNS entries for a given network.
-
- 1. All dns entries will be *postfixed* with a private TLD of the network name, for example, ".mynet"
- 2. Default DNS is created for node name + TLD, for instance, node-c42wt.mynet. This is not editable.
- 3. Click ADD ENTRY to add custom DNS
-	 - You can click CHOOSE NODE to direct DNS to a specific node in the network
-	 - You can also specify any custom address you would like, which can be outside the network (for instance, the IP for google.com)
-	 - Add a dns entry name, which will be postfixed with the network TLD. E.g. if you enter "privateapi.com", it will become "privateapi.com.networkname" 
-
-### Uninstalling Client
-To uninstall the client from a network: `sudo netclient -c remove -n < networkname >`
-To uninstall entirely, run the above for each network,  and then run `sudo rm -rf /etc/netclient`
-
-### Uninstralling Netmaker
-To uninstall the netmaker server, simply run `docker-compose down`
 
 #### LICENSE
 
@@ -146,5 +47,5 @@ Netmaker's source code and all artifacts in this repository are freely available
 
 #### CONTACT
 
-Email: alex@gravitl.com  
+Email: info@gravitl.com  
 Discord: https://discord.gg/zRb9Vfhk8A

+ 26 - 0
compose/docker-compose.localserver.yml

@@ -0,0 +1,26 @@
+version: "3.4"
+
+volumes:
+  dnsconfig:
+  driver: local
+services:
+  mongodb:
+    image: mongo:4.2
+    ports:
+      - "27017:27017"
+    container_name: mongodb
+    volumes:
+      - mongovol:/data/db
+    restart: always
+    environment:
+      MONGO_INITDB_ROOT_USERNAME: mongoadmin
+      MONGO_INITDB_ROOT_PASSWORD: mongopass
+  netmaker-ui:
+    container_name: netmaker-ui
+    image: gravitl/netmaker-ui:v0.5
+    ports:
+      - "80:80"
+    environment:
+      BACKEND_URL: "http://HOST_IP:8081"
+volumes:
+  mongovol: {}

+ 55 - 0
compose/docker-compose.noclient.yml

@@ -0,0 +1,55 @@
+version: "3.4"
+
+volumes:
+  dnsconfig:
+  driver: local
+services:
+  mongodb:
+    image: mongo:4.2
+    ports:
+      - "27017:27017"
+    container_name: mongodb
+    volumes:
+      - mongovol:/data/db
+    restart: always
+    environment:
+      MONGO_INITDB_ROOT_USERNAME: mongoadmin
+      MONGO_INITDB_ROOT_PASSWORD: mongopass
+  netmaker:
+    container_name: netmaker
+    ports:
+      - "8081:8081"
+      - "50051:50051"
+    depends_on:
+      - mongodb
+    image: gravitl/netmaker:v0.5
+    restart: always
+    environment:
+      SERVER_HOST: "HOST_IP"
+      CLIENT_MODE: "off"
+      SERVER_GRPC_WIREGUARD: "off"
+  netmaker-ui:
+    container_name: netmaker-ui
+    depends_on:
+      - netmaker
+    image: gravitl/netmaker-ui:v0.5
+    links:
+      - "netmaker:api"
+    ports:
+      - "80:80"
+    environment:
+      BACKEND_URL: "http://HOST_IP:8081"
+  coredns:
+    depends_on:
+      - netmaker 
+    image: coredns/coredns
+    command: -conf /root/dnsconfig/Corefile
+    container_name: coredns
+    restart: always
+    ports:
+      - "53:53/udp"
+    volumes:
+      - dnsconfig:/root/dnsconfig
+volumes:
+  mongovol: {}
+  dnsconfig: {}

+ 3 - 4
docker-compose.nodns.yml → compose/docker-compose.nodns.yml

@@ -20,7 +20,7 @@ services:
     container_name: netmaker
     depends_on:
       - mongodb
-    image: gravitl/netmaker:v0.3
+    image: gravitl/netmaker:v0.5
     volumes:
       - ./:/local
       - /etc/netclient:/etc/netclient
@@ -36,19 +36,18 @@ services:
     restart: always
     network_mode: host
     environment:
-      CLIENT_MODE: "off"
       DNS_MODE: "off"
   netmaker-ui:
     container_name: netmaker-ui
     depends_on:
       - netmaker
-    image: gravitl/netmaker-ui:v0.3
+    image: gravitl/netmaker-ui:v0.5
     links:
       - "netmaker:api"
     ports:
       - "80:80"
     environment:
-      BACKEND_URL: "http://your-backend:8081"
+      BACKEND_URL: "http://HOST_IP:8081"
 volumes:
   mongovol: {}
   dnsconfig: {}

+ 85 - 0
compose/docker-compose.reference.yml

@@ -0,0 +1,85 @@
+version: "3.4"
+
+services:
+  mongodb: # The MongoDB Instance that backs up Netmaker
+    image: mongo:4.2
+    ports:
+      - "27017:27017" # Port Mapping for MongoDB. Can be modified, but be sure to change the MONGO_PORT env var in netmaker
+    container_name: mongodb
+    volumes:
+      - mongovol:/data/db
+    restart: always
+    environment:
+      MONGO_INITDB_ROOT_USERNAME: mongoadmin # Default username. Recommend changing for production installs. You will need to set MONGO_ADMIN netmaker env var.
+      MONGO_INITDB_ROOT_PASSWORD: mongopass # Default password. Recommend changing for production installs. You will need to set MONGO_PASS netmaker env var.
+  netmaker: # The Primary Server for running Netmaker
+    privileged: true # Necessary to run sudo/root level commands on host system. Take out if not running with CLIENT_MODE=on
+    container_name: netmaker
+    depends_on:
+      - mongodb
+    image: gravitl/netmaker:v0.3
+    volumes: # Volume mounts necessary for CLIENT_MODE to control netclient, wireguard, and networking on host (except dnsconfig, which is where dns config files are stored for use by CoreDNS)
+      - ./:/local
+      - /etc/netclient:/etc/netclient
+      - dnsconfig:/root/config/dnsconfig # Netmaker writes Corefile to this location, which gets mounted by CoreDNS for DNS configuration.
+      - /usr/bin/wg:/usr/bin/wg
+      - /var/run/dbus/system_bus_socket:/var/run/dbus/system_bus_socket
+      - /run/systemd/system:/run/systemd/system
+      - /etc/systemd/system:/etc/systemd/system
+      - /sys/fs/cgroup:/sys/fs/cgroup
+    cap_add: # Necessary for CLIENT_MODE. Should be removed if turned off. 
+      - NET_ADMIN
+      - SYS_MODULE
+    restart: always
+    network_mode: host # Necessary for CLIENT_MODE. Should be removed if turned off, but then need to add port mappings
+    environment:
+      SERVER_HOST: "" # All the Docker Compose files pre-populate this with HOST_IP, which you replace as part of the install instructions. This will set both HTTP and GRPC host.
+      SERVER_HTTP_HOST: "127.0.0.1" # Overrides SERVER_HOST if set. Useful for making HTTP and GRPC available via different interfaces/networks.
+      SERVER_GRPC_HOST: "127.0.0.1" # Overrides SERVER_HOST if set. Useful for making HTTP and GRPC available via different interfaces/networks.
+      API_PORT: 8081 # The HTTP API port for Netmaker. Used for API calls / communication from front end. If changed, need to change port of BACKEND_URL for netmaker-ui.
+      GRPC_PORT: 50051 # The GRPC port for Netmaker. Used for communications from nodes.
+      MASTER_KEY: "secretkey" # The admin master key for accessing the API. Change this in any production installation.
+      CORS_ALLOWED_ORIGIN: "*" # The "allowed origin" for API requests. Change to restrict where API requests can come from.
+      REST_BACKEND: "on" # Enables the REST backend (API running on API_PORT at SERVER_HTTP_HOST). Change to "off" to turn off.
+      AGENT_BACKEND: "on" # Enables the AGENT backend (GRPC running on GRPC_PORT at SERVER_GRPC_HOST). Change to "off" to turn off.
+      CLIENT_MODE: "on" # Enables Client Mode, meaning netclient will be deployed on server and will be manageable from UI. Change to "off" to turn off.
+      DNS_MODE: "on" # Enables DNS Mode, meaning config files will be generated for CoreDNS. Note, turning "off" does not remove CoreDNS. You still need to remove CoreDNS from compose file.
+      DISABLE_REMOTE_IP_CHECK: "off" # If turned "on", Server will not set Host based on remote IP check. This is already overridden if SERVER_HOST is set. Turned "off" by default.
+      MONGO_ADMIN: "mongoadmin" # Admin user for MongoDB. Change to match above MongoDB instance
+      MONGO_PASS: "mongopass" # Admin password for MongoDB. Change to match above MongoDB instance
+      MONGO_HOST: "127.0.0.1" # Address of MongoDB. Change if necessary.
+      MONGO_PORT: "27017" # Port of MongoDB. Change if necessary.
+      MONGO_OPTS: "/?authSource=admin" # Opts to enable admin login for Mongo.
+      SERVER_GRPC_WIREGUARD: "on" # Whether to run GRPC over a WireGuard network. On by default. Secures server comms. Switch to "off" to turn off.
+      SERVER_GRPC_WG_INTERFACE: "nm-grpc-wg" # Interface to use for GRPC WireGuard network if enabled
+      SERVER_GRPC_WG_ADDRESS: "10.101.0.1" # Private Address to use for GRPC WireGuard network if enabled
+      SERVER_GRPC_WG_ADDRESS_RANGE: "10.101.0.0/16" # Private Address range to use for GRPC WireGard clients if enabled
+      SERVER_GRPC_WG_PORT: "50555" # Port to use for GRPC WireGuard if enabled
+      SERVER_GRPC_WG_PUBKEY: "SERVER_GRPC_WG_PUBKEY" # PublicKey for GRPC WireGuard interface. Generated if blank.
+      SERVER_GRPC_WG_PRIVKEY: "SERVER_GRPC_WG_PRIVKEY" # PrivateKey for GRPC WireGuard interface. Generated if blank.
+  netmaker-ui: # The Netmaker UI Component
+    container_name: netmaker-ui
+    depends_on:
+      - netmaker
+    image: gravitl/netmaker-ui:v0.3
+    links:
+      - "netmaker:api"
+    ports:
+      - "80:80"
+    environment:
+      BACKEND_URL: "http://HOST_IP:8081" # URL where UI will send API requests. Change based on SERVER_HOST, SERVER_HTTP_HOST, and API_PORT
+      MASTER_KEY: "secretkey" # Master Key for API calls. Will be removed in v0.3.5
+  coredns: # The DNS Server. Remove this section if DNS_MODE="off"
+    depends_on:
+      - netmaker 
+    image: coredns/coredns
+    command: -conf /root/dnsconfig/Corefile # Config location for Corefile. This is the path of file which is also mounted to Netmaker for modification.
+    container_name: coredns
+    restart: always
+    ports:
+      - "53:53/udp" # Likely needs to run at port 53 for adequate nameserver usage.
+    volumes:
+      - dnsconfig:/root/dnsconfig
+volumes:
+  mongovol: {}
+  dnsconfig: {}

+ 42 - 0
compose/docker-compose.server-only.yml

@@ -0,0 +1,42 @@
+version: "3.4"
+
+volumes:
+  dnsconfig:
+  driver: local
+services:
+  mongodb:
+    image: mongo:4.2
+    ports:
+      - "27017:27017"
+    container_name: mongodb
+    volumes:
+      - mongovol:/data/db
+    restart: always
+    environment:
+      MONGO_INITDB_ROOT_USERNAME: mongoadmin
+      MONGO_INITDB_ROOT_PASSWORD: mongopass
+  netmaker:
+    container_name: netmaker
+    depends_on:
+      - mongodb
+    image: gravitl/netmaker:v0.5
+    ports:
+      - "8081:8081"
+      - "50051:50051"
+    volumes:
+      - ./:/local
+      - /etc/netclient:/etc/netclient
+      - dnsconfig:/root/config/dnsconfig
+      - /usr/bin/wg:/usr/bin/wg:ro
+      - /var/run/dbus/system_bus_socket:/var/run/dbus/system_bus_socket
+      - /run/systemd/system:/run/systemd/system
+      - /etc/systemd/system:/etc/systemd/system
+      - /sys/fs/cgroup:/sys/fs/cgroup
+    restart: always
+    environment:
+      CLIENT_MODE: "off"
+      DNS_MODE: "off"
+      SERVER_HOST: "localhost"
+volumes:
+  mongovol: {}
+  dnsconfig: {}

+ 44 - 0
compose/docker-compose.slim.yml

@@ -0,0 +1,44 @@
+version: "3.4"
+
+volumes:
+  dnsconfig:
+  driver: local
+services:
+  mongodb:
+    image: mongo:4.2
+    ports:
+      - "27017:27017"
+    container_name: mongodb
+    volumes:
+      - mongovol:/data/db
+    restart: always
+    environment:
+      MONGO_INITDB_ROOT_USERNAME: mongoadmin
+      MONGO_INITDB_ROOT_PASSWORD: mongopass
+  netmaker:
+    container_name: netmaker
+    ports:
+      - "8081:8081"
+      - "50051:50051"
+    depends_on:
+      - mongodb
+    image: gravitl/netmaker:v0.3
+    restart: always
+    environment:
+      SERVER_HOST: "HOST_IP"
+      DNS_MODE: "off"
+      CLIENT_MODE: "off"
+      SERVER_GRPC_WIREGUARD: "off"
+  netmaker-ui:
+    container_name: netmaker-ui
+    depends_on:
+      - netmaker
+    image: gravitl/netmaker-ui:v0.3
+    links:
+      - "netmaker:api"
+    ports:
+      - "80:80"
+    environment:
+      BACKEND_URL: "http://HOST_IP:8081"
+volumes:
+  mongovol: {}

+ 7 - 5
docker-compose.yml → compose/docker-compose.yml

@@ -17,32 +17,34 @@ services:
     container_name: netmaker
     depends_on:
       - mongodb
-    image: gravitl/netmaker:v0.3
+    image: gravitl/netmaker:v0.5
     volumes:
       - ./:/local
       - /etc/netclient:/etc/netclient
       - dnsconfig:/root/config/dnsconfig
-      - /usr/bin/wg:/usr/bin/wg:ro
+      - /usr/bin/wg:/usr/bin/wg
       - /var/run/dbus/system_bus_socket:/var/run/dbus/system_bus_socket
       - /run/systemd/system:/run/systemd/system
       - /etc/systemd/system:/etc/systemd/system
       - /sys/fs/cgroup:/sys/fs/cgroup
-    cap_add:
+    cap_add: 
       - NET_ADMIN
       - SYS_MODULE
     restart: always
     network_mode: host
+    environment:
+      SERVER_HOST: "HOST_IP"
   netmaker-ui:
     container_name: netmaker-ui
     depends_on:
       - netmaker
-    image: gravitl/netmaker-ui:v0.3
+    image: gravitl/netmaker-ui:v0.5
     links:
       - "netmaker:api"
     ports:
       - "80:80"
     environment:
-      BACKEND_URL: "http://your-ip:8081"
+      BACKEND_URL: "http://HOST_IP:8081"
   coredns:
     depends_on:
       - netmaker 

+ 14 - 2
config/config.go

@@ -31,6 +31,7 @@ var Config *EnvironmentConfig
 type EnvironmentConfig struct {
   Server ServerConfig `yaml:"server"`
   MongoConn MongoConnConfig `yaml:"mongoconn"`
+  WG WG `yaml:"wg"`
 }
 
 // ServerConfig :
@@ -48,6 +49,17 @@ type ServerConfig struct {
   DisableRemoteIPCheck string `yaml:"disableremoteipcheck"`
 }
 
+type WG struct {
+  RegisterKeyRequired  string  `yaml:"keyrequired"`
+  GRPCWireGuard  string  `yaml:"grpcwg"`
+  GRPCWGInterface  string `yaml:"grpciface"`
+  GRPCWGAddress  string `yaml:"grpcaddr"`
+  GRPCWGAddressRange  string `yaml:"grpcaddrrange"`
+  GRPCWGPort  string  `yaml:"grpcport"`
+  GRPCWGPubKey  string  `yaml:"pubkey"`
+  GRPCWGPrivKey  string  `yaml:"privkey"`
+}
+
 type MongoConnConfig struct {
   User   string  `yaml:"user"`
   Pass   string  `yaml:"pass"`
@@ -65,8 +77,8 @@ func readConfig() *EnvironmentConfig {
   if err != nil {
     //log.Fatal(err)
     //os.Exit(2)
-    log.Println("Unable to open config file at config/environments/" + getEnv())
-    log.Println("Will proceed with defaults or enironment variables (no config file).")
+    //log.Println("Unable to open config file at config/environments/" + getEnv())
+    //log.Println("Will proceed with defaults or enironment variables (no config file).")
     return &cfg
   }
   defer f.Close()

+ 8 - 0
config/dnsconfig/Corefile

@@ -0,0 +1,8 @@
+default comms  {
+    reload 15s
+    hosts /root/dnsconfig/netmaker.hosts {
+	fallthrough	
+    }
+    forward . 8.8.8.8 8.8.4.4
+    log
+}

+ 1 - 0
config/dnsconfig/netmaker.hosts

@@ -0,0 +1 @@
+10.10.10.1       netmaker.default

+ 10 - 0
config/environments/dev.yaml

@@ -16,3 +16,13 @@ mongoconn:
   host: "" # defaults to 127.0.0.1 or MONGO_HOST (if set)
   port: "" # defaults to 27017 or MONGO_PORT (if set)
   opts: '' # defaults to '/?authSource=admin' or MONGO_OPTS (if set)
+wg:
+  keyrequired: "" # defaults to "". If set to "yes", a key is required for signing up for the comms network 
+  grpcwg: "" # defaults to "on" or SERVER_GRPC_WIREGUARD if set
+  grpciface: "" # defaults to nm-grpc-wg or SERVER_GRPC_WG_INTERFACE if set
+  grpcaddr: "" # defaults to 10.101.0.1 or SERVER_GRPC_WG_ADDRESS if set
+  grpcaddrrange: "" # defaults to 10.101.0.0/16 or SERVER_GRPC_WG_ADDRESS_RANGE if set
+  grpcendpoint: "" # defaults to SERVER_HOST if unset
+  grpcport: "" # defaults to 50555 or SERVER_GRPC_WG_PORT if set
+  pubkey: "" # defaults to generated value or SERVER_GRPC_WG_PUBKEY if set
+  privkey: "" # defaults to generated value or SERVER_GRPC_WG_PRIVKEY if set

+ 122 - 123
controllers/common.go

@@ -4,17 +4,17 @@ import (
 	"context"
 	"fmt"
 	"log"
-	"net"
 	"time"
 
+	"github.com/go-playground/validator/v10"
 	"github.com/gravitl/netmaker/functions"
-	"github.com/gravitl/netmaker/servercfg"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/mongoconn"
+	"github.com/gravitl/netmaker/servercfg"
+	"github.com/gravitl/netmaker/serverctl"
 	"go.mongodb.org/mongo-driver/bson"
 	"go.mongodb.org/mongo-driver/mongo/options"
 	"golang.org/x/crypto/bcrypt"
-	"gopkg.in/go-playground/validator.v9"
 )
 
 func GetPeersList(networkName string) ([]models.PeersResponse, error) {
@@ -59,62 +59,64 @@ func GetPeersList(networkName string) ([]models.PeersResponse, error) {
 	return peers, err
 }
 
-func ValidateNodeCreate(networkName string, node models.Node) error {
 
-	v := validator.New()
-	_ = v.RegisterValidation("address_check", func(fl validator.FieldLevel) bool {
-		isIpv4 := functions.IsIpNet(node.Address)
-		empty := node.Address == ""
-		return (empty || isIpv4)
-	})
-        _ = v.RegisterValidation("address6_check", func(fl validator.FieldLevel) bool {
-                isIpv6 := functions.IsIpNet(node.Address6)
-                empty := node.Address6 == ""
-                return (empty || isIpv6)
-        })
-	_ = v.RegisterValidation("endpoint_check", func(fl validator.FieldLevel) bool {
-		//var isFieldUnique bool = functions.IsFieldUnique(networkName, "endpoint", node.Endpoint)
-		isIp := functions.IsIpNet(node.Endpoint)
-		notEmptyCheck := node.Endpoint != ""
-		return (notEmptyCheck && isIp)
-	})
-	_ = v.RegisterValidation("localaddress_check", func(fl validator.FieldLevel) bool {
-		//var isFieldUnique bool = functions.IsFieldUnique(networkName, "endpoint", node.Endpoint)
-		isIp := functions.IsIpNet(node.LocalAddress)
-		empty := node.LocalAddress == ""
-		return (empty || isIp)
-	})
+func GetExtPeersList(networkName string, macaddress string) ([]models.ExtPeersResponse, error) {
+
+        var peers []models.ExtPeersResponse
+
+        //Connection mongoDB with mongoconn class
+        collection := mongoconn.Client.Database("netmaker").Collection("extclients")
+
+        ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+
+        //Get all nodes in the relevant network which are NOT in pending state
+	filter := bson.M{"network": networkName, "ingressgatewayid": macaddress}
+        cur, err := collection.Find(ctx, filter)
+
+        if err != nil {
+                return peers, err
+        }
+
+        // Close the cursor once finished and cancel if it takes too long
+        defer cancel()
+
+        for cur.Next(context.TODO()) {
+
+                var peer models.ExtPeersResponse
+                err := cur.Decode(&peer)
+                if err != nil {
+                        log.Fatal(err)
+                }
+
+                // add the node to our node array
+                //maybe better to just return this? But then that's just GetNodes...
+                peers = append(peers, peer)
+        }
+
+        //Uh oh, fatal error! This needs some better error handling
+        //TODO: needs appropriate error handling so the server doesnt shut down.
+        if err := cur.Err(); err != nil {
+                log.Fatal(err)
+        }
+
+        return peers, err
+}
+
 
+func ValidateNodeCreate(networkName string, node models.Node) error {
+	v := validator.New()
 	_ = v.RegisterValidation("macaddress_unique", func(fl validator.FieldLevel) bool {
 		var isFieldUnique bool = functions.IsFieldUnique(networkName, "macaddress", node.MacAddress)
 		return isFieldUnique
 	})
-
-	_ = v.RegisterValidation("macaddress_valid", func(fl validator.FieldLevel) bool {
-		_, err := net.ParseMAC(node.MacAddress)
-		return err == nil
-	})
-
-	_ = v.RegisterValidation("name_valid", func(fl validator.FieldLevel) bool {
-		isvalid := functions.NameInNodeCharSet(node.Name)
-		return isvalid
-	})
-
 	_ = v.RegisterValidation("network_exists", func(fl validator.FieldLevel) bool {
 		_, err := node.GetNetwork()
 		return err == nil
 	})
-	_ = v.RegisterValidation("pubkey_check", func(fl validator.FieldLevel) bool {
-		notEmptyCheck := node.PublicKey != ""
-		isBase64 := functions.IsBase64(node.PublicKey)
-		return (notEmptyCheck && isBase64)
-	})
-	_ = v.RegisterValidation("password_check", func(fl validator.FieldLevel) bool {
-		notEmptyCheck := node.Password != ""
-		goodLength := len(node.Password) > 5
-		return (notEmptyCheck && goodLength)
-	})
-
+        _ = v.RegisterValidation("in_charset", func(fl validator.FieldLevel) bool {
+                isgood := functions.NameInNodeCharSet(node.Name)
+                return isgood
+        })
 	err := v.Struct(node)
 
 	if err != nil {
@@ -125,71 +127,26 @@ func ValidateNodeCreate(networkName string, node models.Node) error {
 	return err
 }
 
-func ValidateNodeUpdate(networkName string, node models.Node) error {
-
-        v := validator.New()
-        _ = v.RegisterValidation("address_check", func(fl validator.FieldLevel) bool {
-                isIpv4 := functions.IsIpNet(node.Address)
-                empty := node.Address == ""
-                return (empty || isIpv4)
-        })
-        _ = v.RegisterValidation("address6_check", func(fl validator.FieldLevel) bool {
-                isIpv6 := functions.IsIpNet(node.Address6)
-                empty := node.Address6 == ""
-                return (empty || isIpv6)
-        })
-        _ = v.RegisterValidation("endpoint_check", func(fl validator.FieldLevel) bool {
-                //var isFieldUnique bool = functions.IsFieldUnique(networkName, "endpoint", node.Endpoint)
-                isIp := functions.IsIpNet(node.Address)
-		empty := node.Endpoint == ""
-                return (empty || isIp)
-        })
-        _ = v.RegisterValidation("localaddress_check", func(fl validator.FieldLevel) bool {
-                //var isFieldUnique bool = functions.IsFieldUnique(networkName, "endpoint", node.Endpoint)
-                isIp := functions.IsIpNet(node.LocalAddress)
-                empty := node.LocalAddress == ""
-                return (empty || isIp )
-        })
-        _ = v.RegisterValidation("macaddress_unique", func(fl validator.FieldLevel) bool {
-                return true
-        })
-
-        _ = v.RegisterValidation("macaddress_valid", func(fl validator.FieldLevel) bool {
-                _, err := net.ParseMAC(node.MacAddress)
-                return err == nil
-        })
-
-        _ = v.RegisterValidation("name_valid", func(fl validator.FieldLevel) bool {
-                isvalid := functions.NameInNodeCharSet(node.Name)
-                return isvalid
-        })
-
-        _ = v.RegisterValidation("network_exists", func(fl validator.FieldLevel) bool {
-                _, err := node.GetNetwork()
-                return err == nil
-        })
-        _ = v.RegisterValidation("pubkey_check", func(fl validator.FieldLevel) bool {
-                empty := node.PublicKey == ""
-                isBase64 := functions.IsBase64(node.PublicKey)
-                return (empty || isBase64)
-        })
-        _ = v.RegisterValidation("password_check", func(fl validator.FieldLevel) bool {
-                empty := node.Password == ""
-                goodLength := len(node.Password) > 5
-                return (empty || goodLength)
+func ValidateNodeUpdate(networkName string, node models.NodeUpdate) error {
+	v := validator.New()
+	_ = v.RegisterValidation("network_exists", func(fl validator.FieldLevel) bool {
+		_, err := node.GetNetwork()
+		return err == nil
+	})
+        _ = v.RegisterValidation("in_charset", func(fl validator.FieldLevel) bool {
+                isgood := functions.NameInNodeCharSet(node.Name)
+                return isgood
         })
-
-        err := v.Struct(node)
-
-        if err != nil {
-                for _, e := range err.(validator.ValidationErrors) {
-                        fmt.Println(e)
-                }
-        }
-        return err
+	err := v.Struct(node)
+	if err != nil {
+		for _, e := range err.(validator.ValidationErrors) {
+			fmt.Println(e)
+		}
+	}
+	return err
 }
 
-func UpdateNode(nodechange models.Node, node models.Node) (models.Node, error) {
+func UpdateNode(nodechange models.NodeUpdate, node models.Node) (models.Node, error) {
 	//Question: Is there a better way  of doing  this than a bunch of "if" statements? probably...
 	//Eventually, lets have a better way to check if any of the fields are filled out...
 	queryMac := node.MacAddress
@@ -200,10 +157,10 @@ func UpdateNode(nodechange models.Node, node models.Node) (models.Node, error) {
 		node.Address = nodechange.Address
 		notifynetwork = true
 	}
-        if nodechange.Address6 != "" {
-                node.Address6 = nodechange.Address6
-                notifynetwork = true
-        }
+	if nodechange.Address6 != "" {
+		node.Address6 = nodechange.Address6
+		notifynetwork = true
+	}
 	if nodechange.Name != "" {
 		node.Name = nodechange.Name
 	}
@@ -307,9 +264,9 @@ func UpdateNode(nodechange models.Node, node models.Node) (models.Node, error) {
 	if notifynetwork {
 		errN = SetNetworkNodesLastModified(queryNetwork)
 	}
-        if servercfg.IsDNSMode() {
+	if servercfg.IsDNSMode() {
 		errN = SetDNS()
-        }
+	}
 
 	return returnnode, errN
 }
@@ -336,13 +293,38 @@ func DeleteNode(macaddress string, network string) (bool, error) {
 
 	err = SetNetworkNodesLastModified(network)
 	fmt.Println("Deleted node " + macaddress + " from network " + network)
-        if servercfg.IsDNSMode() {
-                err = SetDNS()
-        }
+	if servercfg.IsDNSMode() {
+		err = SetDNS()
+	}
 
 	return deleted, err
 }
 
+func DeleteIntClient(clientid string) (bool, error) {
+
+        deleted := false
+
+        collection := mongoconn.Client.Database("netmaker").Collection("intclients")
+
+        filter := bson.M{"clientid": clientid}
+
+        ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+
+        result, err := collection.DeleteOne(ctx, filter)
+
+        deletecount := result.DeletedCount
+
+        if deletecount > 0 {
+                deleted = true
+        }
+
+        defer cancel()
+
+	err = serverctl.ReconfigureServerWireGuard()
+
+        return deleted, err
+}
+
 func GetNode(macaddress string, network string) (models.Node, error) {
 
 	var node models.Node
@@ -359,6 +341,22 @@ func GetNode(macaddress string, network string) (models.Node, error) {
 	return node, err
 }
 
+func GetIntClient(clientid string) (models.IntClient, error) {
+
+        var client models.IntClient
+
+        collection := mongoconn.Client.Database("netmaker").Collection("intclients")
+
+        ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+
+        filter := bson.M{"clientid": clientid}
+        err := collection.FindOne(ctx, filter, options.FindOne().SetProjection(bson.M{"_id": 0})).Decode(&clientid)
+
+        defer cancel()
+
+        return client, err
+}
+
 func CreateNode(node models.Node, networkName string) (models.Node, error) {
 
 	//encrypt that password so we never see it again
@@ -386,10 +384,11 @@ func CreateNode(node models.Node, networkName string) (models.Node, error) {
 		return node, err
 	}
 
-        node.Address6, err = functions.UniqueAddress6(networkName)
+	node.Address6, err = functions.UniqueAddress6(networkName)
+
 	if err != nil {
-                return node, err
-        }
+		return node, err
+	}
 
 	//IDK why these aren't a part of "set defaults. Pretty dumb.
 	//TODO: This is dumb. Consolidate and fix.

+ 534 - 0
controllers/common_test.go

@@ -0,0 +1,534 @@
+package controller
+
+import (
+	"context"
+	"testing"
+	"time"
+
+	"github.com/gravitl/netmaker/models"
+	"github.com/gravitl/netmaker/mongoconn"
+	"github.com/stretchr/testify/assert"
+	"go.mongodb.org/mongo-driver/bson"
+)
+
+type NodeValidationTC struct {
+	testname     string
+	node         models.Node
+	errorMessage string
+}
+
+type NodeValidationUpdateTC struct {
+	testname     string
+	node         models.NodeUpdate
+	errorMessage string
+}
+
+func createTestNode(t *testing.T) models.Node {
+	createnode := models.Node{PublicKey: "DM5qhLAE20PG9BbfBCger+Ac9D2NDOwCtY1rbYDLf34=", Endpoint: "10.0.0.1", MacAddress: "01:02:03:04:05:06", Password: "password", Network: "skynet"}
+	node, err := CreateNode(createnode, "skynet")
+	assert.Nil(t, err)
+	return node
+}
+
+func TestCreateNode(t *testing.T) {
+	deleteNet(t)
+	createNet()
+	createnode := models.Node{PublicKey: "DM5qhLAE20PG9BbfBCger+Ac9D2NDOwCtY1rbYDLf34=", Endpoint: "10.0.0.1", MacAddress: "01:02:03:04:05:06", Password: "password", Network: "skynet"}
+	err := ValidateNodeCreate("skynet", createnode)
+	assert.Nil(t, err)
+	node, err := CreateNode(createnode, "skynet")
+	assert.Nil(t, err)
+	assert.Equal(t, "10.0.0.1", node.Endpoint)
+	assert.Equal(t, "DM5qhLAE20PG9BbfBCger+Ac9D2NDOwCtY1rbYDLf34=", node.PublicKey)
+	assert.Equal(t, "01:02:03:04:05:06", node.MacAddress)
+	assert.Equal(t, int32(51821), node.ListenPort)
+	assert.NotNil(t, node.Name)
+	assert.Equal(t, "skynet", node.Network)
+	assert.Equal(t, "nm-skynet", node.Interface)
+}
+func TestDeleteNode(t *testing.T) {
+	deleteNet(t)
+	createNet()
+	node := createTestNode(t)
+	t.Run("NodeExists", func(t *testing.T) {
+		deleted, err := DeleteNode(node.MacAddress, node.Network)
+		assert.Nil(t, err)
+		assert.True(t, deleted)
+	})
+	t.Run("NonExistantNode", func(t *testing.T) {
+		deleted, err := DeleteNode(node.MacAddress, node.Network)
+		assert.Nil(t, err)
+		assert.False(t, deleted)
+	})
+}
+func TestGetNode(t *testing.T) {
+	deleteNet(t)
+	createNet()
+	node := createTestNode(t)
+	t.Run("NodeExists", func(t *testing.T) {
+		response, err := GetNode(node.MacAddress, node.Network)
+		assert.Nil(t, err)
+		assert.Equal(t, "10.0.0.1", response.Endpoint)
+		assert.Equal(t, "DM5qhLAE20PG9BbfBCger+Ac9D2NDOwCtY1rbYDLf34=", response.PublicKey)
+		assert.Equal(t, "01:02:03:04:05:06", response.MacAddress)
+		assert.Equal(t, int32(51821), response.ListenPort)
+		assert.NotNil(t, response.Name)
+		assert.Equal(t, "skynet", response.Network)
+		assert.Equal(t, "nm-skynet", response.Interface)
+	})
+	t.Run("BadMac", func(t *testing.T) {
+		response, err := GetNode("01:02:03:04:05:07", node.Network)
+		assert.NotNil(t, err)
+		assert.Equal(t, models.Node{}, response)
+		assert.Equal(t, "mongo: no documents in result", err.Error())
+	})
+	t.Run("BadNetwork", func(t *testing.T) {
+		response, err := GetNode(node.MacAddress, "badnet")
+		assert.NotNil(t, err)
+		assert.Equal(t, models.Node{}, response)
+		assert.Equal(t, "mongo: no documents in result", err.Error())
+	})
+	t.Run("NoNode", func(t *testing.T) {
+		_, _ = DeleteNode("01:02:03:04:05:06", "skynet")
+		response, err := GetNode(node.MacAddress, node.Network)
+		assert.NotNil(t, err)
+		assert.Equal(t, models.Node{}, response)
+		assert.Equal(t, "mongo: no documents in result", err.Error())
+	})
+
+}
+func TestGetPeerList(t *testing.T) {
+	deleteNet(t)
+	createNet()
+	_ = createTestNode(t)
+	//createnode := models.Node{PublicKey: "RM5qhLAE20PG9BbfBCger+Ac9D2NDOwCtY1rbYDLf34=", Endpoint: "10.0.0.2", MacAddress: "02:02:03:04:05:06", Password: "password", Network: "skynet"}
+	//_, _ = CreateNode(createnode, "skynet")
+	t.Run("PeerExist", func(t *testing.T) {
+		peers, err := GetPeersList("skynet")
+		assert.Nil(t, err)
+		assert.NotEqual(t, []models.PeersResponse(nil), peers)
+		t.Log(peers)
+	})
+	t.Run("NoNodes", func(t *testing.T) {
+		_, _ = DeleteNode("01:02:03:04:05:06", "skynet")
+		peers, err := GetPeersList("skynet")
+		assert.Nil(t, err)
+		assert.Equal(t, []models.PeersResponse(nil), peers)
+		t.Log(peers)
+	})
+}
+func TestNodeCheckIn(t *testing.T) {
+	deleteNet(t)
+	createNet()
+	node := createTestNode(t)
+	time.Sleep(time.Second * 1)
+	expectedResponse := models.CheckInResponse{false, false, false, false, false, "", false}
+	t.Run("BadNet", func(t *testing.T) {
+		response, err := NodeCheckIn(node, "badnet")
+		assert.NotNil(t, err)
+		assert.Contains(t, err.Error(), "Couldnt retrieve Network badnet: ")
+		assert.Equal(t, expectedResponse, response)
+	})
+	t.Run("BadNode", func(t *testing.T) {
+		badnode := models.Node{PublicKey: "RM5qhLAE20PG9BbfBCger+Ac9D2NDOwCtY1rbYDLf34=", Endpoint: "10.0.0.2", MacAddress: "02:02:03:04:05:06", Password: "password", Network: "skynet"}
+		response, err := NodeCheckIn(badnode, "skynet")
+		assert.NotNil(t, err)
+		assert.Contains(t, err.Error(), "Couldnt Get Node 02:02:03:04:05:06")
+		assert.Equal(t, expectedResponse, response)
+	})
+	t.Run("NoUpdatesNeeded", func(t *testing.T) {
+		expectedResponse := models.CheckInResponse{true, false, false, false, false, "", false}
+		response, err := NodeCheckIn(node, node.Network)
+		assert.Nil(t, err)
+		assert.Equal(t, expectedResponse, response)
+	})
+	t.Run("NodePending", func(t *testing.T) {
+		//		create Pending Node
+		createnode := models.Node{PublicKey: "RM5qhLAE20PG9BbfBCger+Ac9D2NDOwCtY1rbYDLf34=", Endpoint: "10.0.0.2", MacAddress: "01:02:03:04:05:07", Password: "password", Network: "skynet", IsPending: true}
+		pendingNode, _ := CreateNode(createnode, "skynet")
+		expectedResponse.IsPending = true
+		response, err := NodeCheckIn(pendingNode, "skynet")
+		assert.NotNil(t, err)
+		assert.Contains(t, err.Error(), "Node checking in is still pending: 01:02:03:04:05:07")
+		assert.Equal(t, expectedResponse, response)
+	})
+	t.Run("ConfigUpdateRequired", func(t *testing.T) {
+		err := TimestampNode(node, false, false, true)
+		assert.Nil(t, err)
+		expectedResponse.NeedConfigUpdate = true
+		expectedResponse.Success = true
+		response, err := NodeCheckIn(node, "skynet")
+		assert.Nil(t, err)
+		assert.Equal(t, true, response.Success)
+		assert.Equal(t, true, response.NeedConfigUpdate)
+	})
+	t.Run("PeerUpdateRequired", func(t *testing.T) {
+		var nodeUpdate models.NodeUpdate
+		newtime := time.Now().Add(time.Hour * -24).Unix()
+		nodeUpdate.LastPeerUpdate = newtime
+		_, err := UpdateNode(nodeUpdate, node)
+		assert.Nil(t, err)
+		response, err := NodeCheckIn(node, "skynet")
+		assert.Nil(t, err)
+		assert.Equal(t, true, response.Success)
+		assert.Equal(t, true, response.NeedPeerUpdate)
+	})
+	t.Run("KeyUpdateRequired", func(t *testing.T) {
+		var network models.Network
+		newtime := time.Now().Add(time.Hour * 24).Unix()
+		t.Log(newtime, time.Now().Unix())
+		//this is cheating; but can't find away to update timestamp through existing api
+		collection := mongoconn.Client.Database("netmaker").Collection("networks")
+		ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+		filter := bson.M{"netid": "skynet"}
+		update := bson.D{
+			{"$set", bson.D{
+				{"keyupdatetimestamp", newtime},
+			}},
+		}
+		defer cancel()
+		err := collection.FindOneAndUpdate(ctx, filter, update).Decode(&network)
+		assert.Nil(t, err)
+		response, err := NodeCheckIn(node, "skynet")
+		assert.Nil(t, err)
+		assert.Equal(t, true, response.Success)
+		assert.Equal(t, true, response.NeedKeyUpdate)
+	})
+	t.Run("DeleteNeeded", func(t *testing.T) {
+		var nodeUpdate models.NodeUpdate
+		newtime := time.Now().Add(time.Hour * -24).Unix()
+		nodeUpdate.ExpirationDateTime = newtime
+		_, err := UpdateNode(nodeUpdate, node)
+		assert.Nil(t, err)
+		response, err := NodeCheckIn(node, "skynet")
+		assert.Nil(t, err)
+		assert.Equal(t, true, response.Success)
+		assert.Equal(t, true, response.NeedDelete)
+	})
+}
+
+func TestSetNetworkNodesLastModified(t *testing.T) {
+	deleteNet(t)
+	createNet()
+	t.Run("InvalidNetwork", func(t *testing.T) {
+		err := SetNetworkNodesLastModified("badnet")
+		assert.NotNil(t, err)
+		assert.Equal(t, "mongo: no documents in result", err.Error())
+	})
+	t.Run("NetworkExists", func(t *testing.T) {
+		err := SetNetworkNodesLastModified("skynet")
+		assert.Nil(t, err)
+	})
+}
+func TestTimestampNode(t *testing.T) {
+	deleteNet(t)
+	createNet()
+	node := createTestNode(t)
+	time.Sleep(time.Second * 1)
+	before, err := GetNode(node.MacAddress, node.Network)
+	assert.Nil(t, err)
+	t.Run("UpdateCheckIn", func(t *testing.T) {
+		err = TimestampNode(node, true, false, false)
+		assert.Nil(t, err)
+		after, err := GetNode(node.MacAddress, node.Network)
+		assert.Nil(t, err)
+		assert.Greater(t, after.LastCheckIn, before.LastCheckIn)
+	})
+	t.Run("UpdatePeers", func(t *testing.T) {
+		err = TimestampNode(node, false, true, false)
+		assert.Nil(t, err)
+		after, err := GetNode(node.MacAddress, node.Network)
+		assert.Nil(t, err)
+		assert.Greater(t, after.LastPeerUpdate, before.LastPeerUpdate)
+	})
+	t.Run("UpdateLastModified", func(t *testing.T) {
+		err = TimestampNode(node, false, false, true)
+		assert.Nil(t, err)
+		after, err := GetNode(node.MacAddress, node.Network)
+		assert.Nil(t, err)
+		assert.Greater(t, after.LastModified, before.LastModified)
+	})
+	t.Run("InvalidNode", func(t *testing.T) {
+		node.MacAddress = "01:02:03:04:05:08"
+		err = TimestampNode(node, true, true, true)
+		assert.NotNil(t, err)
+		assert.Equal(t, "mongo: no documents in result", err.Error())
+	})
+
+}
+func TestUpdateNode(t *testing.T) {
+	deleteNet(t)
+	createNet()
+	node := createTestNode(t)
+	var update models.NodeUpdate
+	update.MacAddress = "01:02:03:04:05:06"
+	update.Name = "helloworld"
+	newnode, err := UpdateNode(update, node)
+	assert.Nil(t, err)
+	assert.Equal(t, update.Name, newnode.Name)
+}
+
+func TestValidateNodeCreate(t *testing.T) {
+	cases := []NodeValidationTC{
+		//		NodeValidationTC{
+		//			testname: "EmptyAddress",
+		//			node: models.Node{
+		//				Address: "",
+		//			},
+		//			errorMessage: "Field validation for 'Endpoint' failed on the 'address_check' tag",
+		//		},
+		NodeValidationTC{
+			testname: "BadAddress",
+			node: models.Node{
+				Address: "256.0.0.1",
+			},
+			errorMessage: "Field validation for 'Address' failed on the 'ipv4' tag",
+		},
+		NodeValidationTC{
+			testname: "BadAddress6",
+			node: models.Node{
+				Address6: "2607::abcd:efgh::1",
+			},
+			errorMessage: "Field validation for 'Address6' failed on the 'ipv6' tag",
+		},
+		NodeValidationTC{
+			testname: "BadLocalAddress",
+			node: models.Node{
+				LocalAddress: "10.0.200.300",
+			},
+			errorMessage: "Field validation for 'LocalAddress' failed on the 'ip' tag",
+		},
+		NodeValidationTC{
+			testname: "InvalidName",
+			node: models.Node{
+				Name: "mynode*",
+			},
+			errorMessage: "Field validation for 'Name' failed on the 'in_charset' tag",
+		},
+		NodeValidationTC{
+			testname: "NameTooLong",
+			node: models.Node{
+				Name: "mynodexmynode",
+			},
+			errorMessage: "Field validation for 'Name' failed on the 'max' tag",
+		},
+		NodeValidationTC{
+			testname: "ListenPortMin",
+			node: models.Node{
+				ListenPort: 1023,
+			},
+			errorMessage: "Field validation for 'ListenPort' failed on the 'min' tag",
+		},
+		NodeValidationTC{
+			testname: "ListenPortMax",
+			node: models.Node{
+				ListenPort: 65536,
+			},
+			errorMessage: "Field validation for 'ListenPort' failed on the 'max' tag",
+		},
+		NodeValidationTC{
+			testname: "PublicKeyEmpty",
+			node: models.Node{
+				PublicKey: "",
+			},
+			errorMessage: "Field validation for 'PublicKey' failed on the 'required' tag",
+		},
+		NodeValidationTC{
+			testname: "PublicKeyInvalid",
+			node: models.Node{
+				PublicKey: "junk%key",
+			},
+			errorMessage: "Field validation for 'PublicKey' failed on the 'base64' tag",
+		},
+		NodeValidationTC{
+			testname: "EndpointInvalid",
+			node: models.Node{
+				Endpoint: "10.2.0.300",
+			},
+			errorMessage: "Field validation for 'Endpoint' failed on the 'ip' tag",
+		},
+		NodeValidationTC{
+			testname: "EndpointEmpty",
+			node: models.Node{
+				Endpoint: "",
+			},
+			errorMessage: "Field validation for 'Endpoint' failed on the 'required' tag",
+		},
+		NodeValidationTC{
+			testname: "PersistentKeepaliveMax",
+			node: models.Node{
+				PersistentKeepalive: 1001,
+			},
+			errorMessage: "Field validation for 'PersistentKeepalive' failed on the 'max' tag",
+		},
+		NodeValidationTC{
+			testname: "MacAddressInvalid",
+			node: models.Node{
+				MacAddress: "01:02:03:04:05",
+			},
+			errorMessage: "Field validation for 'MacAddress' failed on the 'mac' tag",
+		},
+		NodeValidationTC{
+			testname: "MacAddressMissing",
+			node: models.Node{
+				MacAddress: "",
+			},
+			errorMessage: "Field validation for 'MacAddress' failed on the 'required' tag",
+		},
+		NodeValidationTC{
+			testname: "EmptyPassword",
+			node: models.Node{
+				Password: "",
+			},
+			errorMessage: "Field validation for 'Password' failed on the 'required' tag",
+		},
+		NodeValidationTC{
+			testname: "ShortPassword",
+			node: models.Node{
+				Password: "1234",
+			},
+			errorMessage: "Field validation for 'Password' failed on the 'min' tag",
+		},
+		NodeValidationTC{
+			testname: "NoNetwork",
+			node: models.Node{
+				Network: "badnet",
+			},
+			errorMessage: "Field validation for 'Network' failed on the 'network_exists' tag",
+		},
+	}
+
+	for _, tc := range cases {
+		t.Run(tc.testname, func(t *testing.T) {
+			err := ValidateNodeCreate("skynet", tc.node)
+			assert.NotNil(t, err)
+			assert.Contains(t, err.Error(), tc.errorMessage)
+		})
+	}
+	t.Run("MacAddresUnique", func(t *testing.T) {
+		createNet()
+		node := models.Node{MacAddress: "01:02:03:04:05:06", Network: "skynet"}
+		_, err := CreateNode(node, "skynet")
+		assert.Nil(t, err)
+		err = ValidateNodeCreate("skynet", node)
+		assert.NotNil(t, err)
+		assert.Contains(t, err.Error(), "Field validation for 'MacAddress' failed on the 'macaddress_unique' tag")
+	})
+	t.Run("EmptyAddress", func(t *testing.T) {
+		node := models.Node{Address: ""}
+		err := ValidateNodeCreate("skynet", node)
+		assert.NotNil(t, err)
+		assert.NotContains(t, err.Error(), "Field validation for 'Address' failed on the 'ipv4' tag")
+	})
+}
+func TestValidateNodeUpdate(t *testing.T) {
+	//cases
+	cases := []NodeValidationUpdateTC{
+		NodeValidationUpdateTC{
+			testname: "BadAddress",
+			node: models.NodeUpdate{
+				Address: "256.0.0.1",
+			},
+			errorMessage: "Field validation for 'Address' failed on the 'ip' tag",
+		},
+		NodeValidationUpdateTC{
+			testname: "BadAddress6",
+			node: models.NodeUpdate{
+				Address6: "2607::abcd:efgh::1",
+			},
+			errorMessage: "Field validation for 'Address6' failed on the 'ipv6' tag",
+		},
+		NodeValidationUpdateTC{
+			testname: "BadLocalAddress",
+			node: models.NodeUpdate{
+				LocalAddress: "10.0.200.300",
+			},
+			errorMessage: "Field validation for 'LocalAddress' failed on the 'ip' tag",
+		},
+		NodeValidationUpdateTC{
+			testname: "InvalidName",
+			node: models.NodeUpdate{
+				Name: "mynode*",
+			},
+			errorMessage: "Field validation for 'Name' failed on the 'in_charset' tag",
+		},
+		NodeValidationUpdateTC{
+			testname: "NameTooLong",
+			node: models.NodeUpdate{
+				Name: "mynodexmynode",
+			},
+			errorMessage: "Field validation for 'Name' failed on the 'max' tag",
+		},
+		NodeValidationUpdateTC{
+			testname: "ListenPortMin",
+			node: models.NodeUpdate{
+				ListenPort: 1023,
+			},
+			errorMessage: "Field validation for 'ListenPort' failed on the 'min' tag",
+		},
+		NodeValidationUpdateTC{
+			testname: "ListenPortMax",
+			node: models.NodeUpdate{
+				ListenPort: 65536,
+			},
+			errorMessage: "Field validation for 'ListenPort' failed on the 'max' tag",
+		},
+		NodeValidationUpdateTC{
+			testname: "PublicKeyInvalid",
+			node: models.NodeUpdate{
+				PublicKey: "bad&key",
+			},
+			errorMessage: "Field validation for 'PublicKey' failed on the 'base64' tag",
+		},
+		NodeValidationUpdateTC{
+			testname: "EndpointInvalid",
+			node: models.NodeUpdate{
+				Endpoint: "10.2.0.300",
+			},
+			errorMessage: "Field validation for 'Endpoint' failed on the 'ip' tag",
+		},
+		NodeValidationUpdateTC{
+			testname: "PersistentKeepaliveMax",
+			node: models.NodeUpdate{
+				PersistentKeepalive: 1001,
+			},
+			errorMessage: "Field validation for 'PersistentKeepalive' failed on the 'max' tag",
+		},
+		NodeValidationUpdateTC{
+			testname: "MacAddressInvalid",
+			node: models.NodeUpdate{
+				MacAddress: "01:02:03:04:05",
+			},
+			errorMessage: "Field validation for 'MacAddress' failed on the 'mac' tag",
+		},
+		NodeValidationUpdateTC{
+			testname: "MacAddressMissing",
+			node: models.NodeUpdate{
+				MacAddress: "",
+			},
+			errorMessage: "Field validation for 'MacAddress' failed on the 'required' tag",
+		},
+		NodeValidationUpdateTC{
+			testname: "ShortPassword",
+			node: models.NodeUpdate{
+				Password: "1234",
+			},
+			errorMessage: "Field validation for 'Password' failed on the 'min' tag",
+		},
+		NodeValidationUpdateTC{
+			testname: "NoNetwork",
+			node: models.NodeUpdate{
+				Network: "badnet",
+			},
+			errorMessage: "Field validation for 'Network' failed on the 'network_exists' tag",
+		},
+	}
+	for _, tc := range cases {
+		t.Run(tc.testname, func(t *testing.T) {
+			err := ValidateNodeUpdate("skynet", tc.node)
+			assert.NotNil(t, err)
+			assert.Contains(t, err.Error(), tc.errorMessage)
+		})
+	}
+
+}

+ 8 - 0
controllers/config/dnsconfig/Corefile

@@ -0,0 +1,8 @@
+skynet  {
+    reload 15s
+    hosts /root/dnsconfig/netmaker.hosts {
+	fallthrough	
+    }
+    forward . 8.8.8.8 8.8.4.4
+    log
+}

+ 1 - 0
controllers/config/dnsconfig/netmaker.hosts

@@ -0,0 +1 @@
+10.0.0.1         node-4bukt.skynet

+ 17 - 0
controllers/config/environments/dev.yaml

@@ -0,0 +1,17 @@
+server:
+  host: "localhost"
+  apiport: "8081"
+  grpcport: "50051"
+  masterkey: "secretkey"
+  allowedorigin: "*"
+  restbackend: true            
+  agentbackend: true
+  defaultnetname: "default"
+  defaultnetrange: "10.10.10.0/24"
+  createdefault: true
+mongoconn:
+  user: "mongoadmin"
+  pass: "mongopass"
+  host: "localhost"
+  port: "27017"
+  opts: '/?authSource=admin'

+ 8 - 7
controllers/controller.go

@@ -5,7 +5,7 @@ import (
     "github.com/gravitl/netmaker/servercfg"
     "os/signal"
     "os"
-    "fmt"
+    "log"
     "context"
     "net/http"
     "github.com/gorilla/mux"
@@ -31,19 +31,20 @@ func HandleRESTRequests(wg *sync.WaitGroup) {
     dnsHandlers(r)
     fileHandlers(r)
     serverHandlers(r)
+    extClientHandlers(r)
+    intClientHandlers(r)
 
 		port := servercfg.GetAPIPort()
 
 		srv := &http.Server{Addr: ":" + port, Handler: handlers.CORS(originsOk, headersOk, methodsOk)(r)}
 		go func(){
 		err := srv.ListenAndServe()
-		//err := http.ListenAndServe(":" + port,
-		//handlers.CORS(originsOk, headersOk, methodsOk)(r))
 		if err != nil {
-			fmt.Println(err)
+			log.Println(err)
 		}
 		}()
-		fmt.Println("REST Server succesfully started on port " + port + " (REST)")
+
+		log.Println("REST Server succesfully started on port " + port + " (REST)")
 		c := make(chan os.Signal)
 
 		// Relay os.Interrupt to our channel (os.Interrupt = CTRL+C)
@@ -55,8 +56,8 @@ func HandleRESTRequests(wg *sync.WaitGroup) {
 		<-c
 
 		// After receiving CTRL+C Properly stop the server
-		fmt.Println("Stopping the REST server...")
+		log.Println("Stopping the REST server...")
 		srv.Shutdown(context.TODO())
-                fmt.Println("REST Server closed.")
+                log.Println("REST Server closed.")
 		mongoconn.Client.Disconnect(context.TODO())
 }

+ 288 - 288
controllers/dnsHttpController.go

@@ -7,14 +7,15 @@ import (
 	"fmt"
 	"net/http"
 	"time"
+
+	"github.com/go-playground/validator/v10"
 	"github.com/gorilla/mux"
-	"github.com/txn2/txeh"
 	"github.com/gravitl/netmaker/functions"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/mongoconn"
+	"github.com/txn2/txeh"
 	"go.mongodb.org/mongo-driver/bson"
 	"go.mongodb.org/mongo-driver/mongo/options"
-	"gopkg.in/go-playground/validator.v9"
 )
 
 func dnsHandlers(r *mux.Router) {
@@ -32,85 +33,86 @@ func dnsHandlers(r *mux.Router) {
 //Gets all nodes associated with network, including pending nodes
 func getNodeDNS(w http.ResponseWriter, r *http.Request) {
 
-        w.Header().Set("Content-Type", "application/json")
+	w.Header().Set("Content-Type", "application/json")
 
-        var dns []models.DNSEntry
-        var params = mux.Vars(r)
+	var dns []models.DNSEntry
+	var params = mux.Vars(r)
 
 	dns, err := GetNodeDNS(params["network"])
-        if err != nil {
-                returnErrorResponse(w, r, formatError(err, "internal"))
-                return
-        }
-
-        //Returns all the nodes in JSON format
-        w.WriteHeader(http.StatusOK)
-        json.NewEncoder(w).Encode(dns)
+	if err != nil {
+		returnErrorResponse(w, r, formatError(err, "internal"))
+		return
+	}
+
+	//Returns all the nodes in JSON format
+	w.WriteHeader(http.StatusOK)
+	json.NewEncoder(w).Encode(dns)
 }
 
 //Gets all nodes associated with network, including pending nodes
 func getAllDNS(w http.ResponseWriter, r *http.Request) {
+	w.Header().Set("Content-Type", "application/json")
+	dns, err := GetAllDNS()
+	if err != nil {
+		returnErrorResponse(w, r, formatError(err, "internal"))
+		return
+	}
+	//Returns all the nodes in JSON format
+	w.WriteHeader(http.StatusOK)
+	json.NewEncoder(w).Encode(dns)
+}
 
-        w.Header().Set("Content-Type", "application/json")
-
-        var dns []models.DNSEntry
-
+func GetAllDNS() ([]models.DNSEntry, error) {
+	var dns []models.DNSEntry
 	networks, err := functions.ListNetworks()
-        if err != nil {
-                returnErrorResponse(w, r, formatError(err, "internal"))
-                return
-        }
-
-        for _, net := range networks {
-                netdns, err := GetDNS(net.NetID)
-                if err != nil {
-			returnErrorResponse(w, r, formatError(err, "internal"))
-                        return
-                }
-	        dns = append(dns, netdns...)
-        }
-
-        //Returns all the nodes in JSON format
-        w.WriteHeader(http.StatusOK)
-        json.NewEncoder(w).Encode(dns)
+	if err != nil {
+		return []models.DNSEntry{}, err
+	}
+	for _, net := range networks {
+		netdns, err := GetDNS(net.NetID)
+		if err != nil {
+			return []models.DNSEntry{}, nil
+		}
+		dns = append(dns, netdns...)
+	}
+	return dns, nil
 }
 
+func GetNodeDNS(network string) ([]models.DNSEntry, error) {
 
-func GetNodeDNS(network string) ([]models.DNSEntry, error){
+	var dns []models.DNSEntry
 
-        var dns []models.DNSEntry
+	collection := mongoconn.Client.Database("netmaker").Collection("nodes")
 
-        collection := mongoconn.Client.Database("netmaker").Collection("nodes")
-
-        ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
 
-        filter := bson.M{"network": network}
+	filter := bson.M{"network": network}
 
-        cur, err := collection.Find(ctx, filter, options.Find().SetProjection(bson.M{"_id": 0}))
+	cur, err := collection.Find(ctx, filter, options.Find().SetProjection(bson.M{"_id": 0}))
 
-        if err != nil {
-                return dns, err
-        }
+	if err != nil {
+		return dns, err
+	}
 
-        defer cancel()
+	defer cancel()
 
-        for cur.Next(context.TODO()) {
+	for cur.Next(context.TODO()) {
 
-                var entry models.DNSEntry
+		var entry models.DNSEntry
 
-                err := cur.Decode(&entry)
-                if err != nil {
-                        return dns, err
-                }
+		err := cur.Decode(&entry)
+		if err != nil {
+			return dns, err
+		}
 
-                // add item our array of nodes
-                dns = append(dns, entry)
-        }
+		// add item our array of nodes
+		dns = append(dns, entry)
+	}
 
-        //TODO: Another fatal error we should take care of.
-        if err := cur.Err(); err != nil {
-                return dns, err
-        }
+	//TODO: Another fatal error we should take care of.
+	if err := cur.Err(); err != nil {
+		return dns, err
+	}
 
 	return dns, err
 }
@@ -118,144 +120,144 @@ func GetNodeDNS(network string) ([]models.DNSEntry, error){
 //Gets all nodes associated with network, including pending nodes
 func getCustomDNS(w http.ResponseWriter, r *http.Request) {
 
-        w.Header().Set("Content-Type", "application/json")
+	w.Header().Set("Content-Type", "application/json")
 
-        var dns []models.DNSEntry
-        var params = mux.Vars(r)
+	var dns []models.DNSEntry
+	var params = mux.Vars(r)
 
-        dns, err := GetCustomDNS(params["network"])
-        if err != nil {
-                returnErrorResponse(w, r, formatError(err, "internal"))
-                return
-        }
+	dns, err := GetCustomDNS(params["network"])
+	if err != nil {
+		returnErrorResponse(w, r, formatError(err, "internal"))
+		return
+	}
 
-        //Returns all the nodes in JSON format
-        w.WriteHeader(http.StatusOK)
-        json.NewEncoder(w).Encode(dns)
+	//Returns all the nodes in JSON format
+	w.WriteHeader(http.StatusOK)
+	json.NewEncoder(w).Encode(dns)
 }
 
-func GetCustomDNS(network string) ([]models.DNSEntry, error){
+func GetCustomDNS(network string) ([]models.DNSEntry, error) {
 
-        var dns []models.DNSEntry
+	var dns []models.DNSEntry
 
-        collection := mongoconn.Client.Database("netmaker").Collection("dns")
+	collection := mongoconn.Client.Database("netmaker").Collection("dns")
 
-        ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
 
-        filter := bson.M{"network": network}
+	filter := bson.M{"network": network}
 
-        cur, err := collection.Find(ctx, filter, options.Find().SetProjection(bson.M{"_id": 0}))
+	cur, err := collection.Find(ctx, filter, options.Find().SetProjection(bson.M{"_id": 0}))
 
-        if err != nil {
-                return dns, err
-        }
+	if err != nil {
+		return dns, err
+	}
 
-        defer cancel()
+	defer cancel()
 
-        for cur.Next(context.TODO()) {
+	for cur.Next(context.TODO()) {
 
-                var entry models.DNSEntry
+		var entry models.DNSEntry
 
-                err := cur.Decode(&entry)
-                if err != nil {
-                        return dns, err
-                }
+		err := cur.Decode(&entry)
+		if err != nil {
+			return dns, err
+		}
 
-                // add item our array of nodes
-                dns = append(dns, entry)
-        }
+		// add item our array of nodes
+		dns = append(dns, entry)
+	}
 
-        //TODO: Another fatal error we should take care of.
-        if err := cur.Err(); err != nil {
-                return dns, err
-        }
+	//TODO: Another fatal error we should take care of.
+	if err := cur.Err(); err != nil {
+		return dns, err
+	}
 
-        return dns, err
+	return dns, err
 }
 
 func SetDNS() error {
-        hostfile := txeh.Hosts{}
-        var corefilestring string
-        networks, err := functions.ListNetworks()
-        if err != nil {
-                return err
-        }
-
-        for _, net := range networks {
-                corefilestring = corefilestring + net.NetID + " "
-                dns, err := GetDNS(net.NetID)
-                if err != nil {
-                        return err
-                }
-                for _, entry := range dns {
-                        hostfile.AddHost(entry.Address, entry.Name+"."+entry.Network)
-                        if err != nil {
-                                return err
-                        }
-                }
-        }
-        if corefilestring == "" {
-                corefilestring = "example.com"
-        }
-
-        err = hostfile.SaveAs("./config/dnsconfig/netmaker.hosts")
-        if err != nil {
-                return err
-        }
-        err = functions.SetCorefile(corefilestring)
-
-        return err
+	hostfile := txeh.Hosts{}
+	var corefilestring string
+	networks, err := functions.ListNetworks()
+	if err != nil {
+		return err
+	}
+
+	for _, net := range networks {
+		corefilestring = corefilestring + net.NetID + " "
+		dns, err := GetDNS(net.NetID)
+		if err != nil {
+			return err
+		}
+		for _, entry := range dns {
+			hostfile.AddHost(entry.Address, entry.Name+"."+entry.Network)
+			if err != nil {
+				return err
+			}
+		}
+	}
+	if corefilestring == "" {
+		corefilestring = "example.com"
+	}
+
+	err = hostfile.SaveAs("./config/dnsconfig/netmaker.hosts")
+	if err != nil {
+		return err
+	}
+	err = functions.SetCorefile(corefilestring)
+
+	return err
 }
 
-func GetDNSEntryNum(domain string, network string) (int, error){
+func GetDNSEntryNum(domain string, network string) (int, error) {
 
-        num := 0
+	num := 0
 
-        entries, err := GetDNS(network)
-        if err != nil {
-                return 0, err
-        }
+	entries, err := GetDNS(network)
+	if err != nil {
+		return 0, err
+	}
 
-        for i := 0; i < len(entries); i++ {
+	for i := 0; i < len(entries); i++ {
 
-                if domain == entries[i].Name {
-                        num++
-                }
-        }
+		if domain == entries[i].Name {
+			num++
+		}
+	}
 
-        return num, nil
+	return num, nil
 }
 
 //Gets all nodes associated with network, including pending nodes
 func getDNS(w http.ResponseWriter, r *http.Request) {
 
-        w.Header().Set("Content-Type", "application/json")
+	w.Header().Set("Content-Type", "application/json")
 
-        var dns []models.DNSEntry
-        var params = mux.Vars(r)
+	var dns []models.DNSEntry
+	var params = mux.Vars(r)
 
 	dns, err := GetDNS(params["network"])
-        if err != nil {
-                returnErrorResponse(w, r, formatError(err, "internal"))
-                return
-        }
-        w.WriteHeader(http.StatusOK)
-        json.NewEncoder(w).Encode(dns)
+	if err != nil {
+		returnErrorResponse(w, r, formatError(err, "internal"))
+		return
+	}
+	w.WriteHeader(http.StatusOK)
+	json.NewEncoder(w).Encode(dns)
 }
 
 func GetDNS(network string) ([]models.DNSEntry, error) {
 
-        var dns []models.DNSEntry
-        dns, err := GetNodeDNS(network)
-        if err != nil {
-                return dns, err
-        }
-        customdns, err := GetCustomDNS(network)
-        if err != nil {
-                return dns, err
-        }
-
-        dns = append(dns, customdns...)
+	var dns []models.DNSEntry
+	dns, err := GetNodeDNS(network)
+	if err != nil {
+		return dns, err
+	}
+	customdns, err := GetCustomDNS(network)
+	if err != nil {
+		return dns, err
+	}
+
+	dns = append(dns, customdns...)
 	return dns, err
 }
 
@@ -263,7 +265,7 @@ func createDNS(w http.ResponseWriter, r *http.Request) {
 	w.Header().Set("Content-Type", "application/json")
 
 	var entry models.DNSEntry
-        var params = mux.Vars(r)
+	var params = mux.Vars(r)
 
 	//get node from body of request
 	_ = json.NewDecoder(r.Body).Decode(&entry)
@@ -277,10 +279,10 @@ func createDNS(w http.ResponseWriter, r *http.Request) {
 
 	entry, err = CreateDNS(entry)
 	if err != nil {
-                returnErrorResponse(w, r, formatError(err, "internal"))
+		returnErrorResponse(w, r, formatError(err, "internal"))
 		return
 	}
-        w.WriteHeader(http.StatusOK)
+	w.WriteHeader(http.StatusOK)
 	json.NewEncoder(w).Encode(entry)
 }
 
@@ -292,9 +294,9 @@ func updateDNS(w http.ResponseWriter, r *http.Request) {
 	var entry models.DNSEntry
 
 	//start here
-	entry, err := GetDNSEntry(params["domain"],params["network"])
+	entry, err := GetDNSEntry(params["domain"], params["network"])
 	if err != nil {
-                returnErrorResponse(w, r, formatError(err, "badrequest"))
+		returnErrorResponse(w, r, formatError(err, "badrequest"))
 		return
 	}
 
@@ -303,21 +305,31 @@ func updateDNS(w http.ResponseWriter, r *http.Request) {
 	// we decode our body request params
 	err = json.NewDecoder(r.Body).Decode(&dnschange)
 	if err != nil {
-                returnErrorResponse(w, r, formatError(err, "badrequest"))
+		returnErrorResponse(w, r, formatError(err, "badrequest"))
 		return
 	}
+	//fill in any missing fields
+	if dnschange.Name == "" {
+		dnschange.Name = entry.Name
+	}
+	if dnschange.Network == "" {
+		dnschange.Network = entry.Network
+	}
+	if dnschange.Address == "" {
+		dnschange.Address = entry.Address
+	}
 
 	err = ValidateDNSUpdate(dnschange, entry)
 
 	if err != nil {
-                returnErrorResponse(w, r, formatError(err, "badrequest"))
+		returnErrorResponse(w, r, formatError(err, "badrequest"))
 		return
 	}
 
 	entry, err = UpdateDNS(dnschange, entry)
 
 	if err != nil {
-                returnErrorResponse(w, r, formatError(err, "badrequest"))
+		returnErrorResponse(w, r, formatError(err, "badrequest"))
 		return
 	}
 
@@ -325,101 +337,101 @@ func updateDNS(w http.ResponseWriter, r *http.Request) {
 }
 
 func deleteDNS(w http.ResponseWriter, r *http.Request) {
-        // Set header
-        w.Header().Set("Content-Type", "application/json")
+	// Set header
+	w.Header().Set("Content-Type", "application/json")
 
-        // get params
-        var params = mux.Vars(r)
+	// get params
+	var params = mux.Vars(r)
 
-        success, err := DeleteDNS(params["domain"], params["network"])
+	success, err := DeleteDNS(params["domain"], params["network"])
 
-        if err != nil {
-                returnErrorResponse(w, r, formatError(err, "internal"))
-                return
-        } else if !success {
-                returnErrorResponse(w, r, formatError(errors.New("Delete unsuccessful."), "badrequest"))
-                return
-        }
+	if err != nil {
+		returnErrorResponse(w, r, formatError(err, "internal"))
+		return
+	} else if !success {
+		returnErrorResponse(w, r, formatError(errors.New("Delete unsuccessful."), "badrequest"))
+		return
+	}
 
-        json.NewEncoder(w).Encode(params["domain"] + " deleted.")
+	json.NewEncoder(w).Encode(params["domain"] + " deleted.")
 }
 
 func CreateDNS(entry models.DNSEntry) (models.DNSEntry, error) {
 
-        // connect db
-        collection := mongoconn.Client.Database("netmaker").Collection("dns")
-        ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+	// connect db
+	collection := mongoconn.Client.Database("netmaker").Collection("dns")
+	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
 
-        // insert our node to the node db.
-        _, err := collection.InsertOne(ctx, entry)
+	// insert our node to the node db.
+	_, err := collection.InsertOne(ctx, entry)
 
-        defer cancel()
+	defer cancel()
 
-        return entry, err
+	return entry, err
 }
 
 func GetDNSEntry(domain string, network string) (models.DNSEntry, error) {
-        var entry models.DNSEntry
+	var entry models.DNSEntry
 
-        collection := mongoconn.Client.Database("netmaker").Collection("dns")
+	collection := mongoconn.Client.Database("netmaker").Collection("dns")
 
-        ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
 
-        filter := bson.M{"name": domain, "network": network}
-        err := collection.FindOne(ctx, filter, options.FindOne().SetProjection(bson.M{"_id": 0})).Decode(&entry)
+	filter := bson.M{"name": domain, "network": network}
+	err := collection.FindOne(ctx, filter, options.FindOne().SetProjection(bson.M{"_id": 0})).Decode(&entry)
 
-        defer cancel()
+	defer cancel()
 
-        return entry, err
+	return entry, err
 }
 
 func UpdateDNS(dnschange models.DNSEntry, entry models.DNSEntry) (models.DNSEntry, error) {
 
-        queryDNS := entry.Name
-
-        if dnschange.Name != "" {
-                entry.Name = dnschange.Name
-        }
-        if dnschange.Address != "" {
-                entry.Address = dnschange.Address
-        }
-        //collection := mongoconn.ConnectDB()
-        collection := mongoconn.Client.Database("netmaker").Collection("dns")
-
-        ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
-
-        // Create filter
-        filter := bson.M{"name": queryDNS}
-
-        // prepare update model.
-        update := bson.D{
-                {"$set", bson.D{
-                        {"name", entry.Name},
-                        {"address", entry.Address},
-                }},
-        }
-        var dnsupdate models.DNSEntry
-
-        errN := collection.FindOneAndUpdate(ctx, filter, update).Decode(&dnsupdate)
-        if errN != nil {
-                fmt.Println("Could not update: ")
-                fmt.Println(errN)
-        } else {
-                fmt.Println("DNS Entry updated successfully.")
-        }
-
-        defer cancel()
-
-        return dnsupdate, errN
+	queryDNS := entry.Name
+
+	if dnschange.Name != "" {
+		entry.Name = dnschange.Name
+	}
+	if dnschange.Address != "" {
+		entry.Address = dnschange.Address
+	}
+	//collection := mongoconn.ConnectDB()
+	collection := mongoconn.Client.Database("netmaker").Collection("dns")
+
+	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+
+	// Create filter
+	filter := bson.M{"name": queryDNS}
+
+	// prepare update model.
+	update := bson.D{
+		{"$set", bson.D{
+			{"name", entry.Name},
+			{"address", entry.Address},
+		}},
+	}
+	var dnsupdate models.DNSEntry
+
+	errN := collection.FindOneAndUpdate(ctx, filter, update).Decode(&dnsupdate)
+	if errN != nil {
+		fmt.Println("Could not update: ")
+		fmt.Println(errN)
+	} else {
+		fmt.Println("DNS Entry updated successfully.")
+	}
+
+	defer cancel()
+
+	return dnsupdate, errN
 }
 
 func DeleteDNS(domain string, network string) (bool, error) {
-
+	fmt.Println("delete dns entry ", domain, network)
 	deleted := false
 
 	collection := mongoconn.Client.Database("netmaker").Collection("dns")
 
-	filter := bson.M{"name": domain,  "network": network}
+	filter := bson.M{"name": domain, "network": network}
 
 	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
 
@@ -437,49 +449,36 @@ func DeleteDNS(domain string, network string) (bool, error) {
 }
 
 func pushDNS(w http.ResponseWriter, r *http.Request) {
-        // Set header
-        w.Header().Set("Content-Type", "application/json")
+	// Set header
+	w.Header().Set("Content-Type", "application/json")
 
-        err := SetDNS()
+	err := SetDNS()
 
-        if err != nil {
-                returnErrorResponse(w, r, formatError(err, "internal"))
-                return
+	if err != nil {
+		returnErrorResponse(w, r, formatError(err, "internal"))
+		return
 	}
-
-        json.NewEncoder(w).Encode("DNS Pushed to CoreDNS")
+	json.NewEncoder(w).Encode("DNS Pushed to CoreDNS")
 }
 
 func ValidateDNSCreate(entry models.DNSEntry) error {
 
 	v := validator.New()
-        fmt.Println("Validating DNS: " + entry.Name)
-        fmt.Println("       Address: " + entry.Address)
-        fmt.Println("       Network: " + entry.Network)
+	fmt.Println("Validating DNS: " + entry.Name)
+	fmt.Println("       Address: " + entry.Address)
+	fmt.Println("       Network: " + entry.Network)
 
 	_ = v.RegisterValidation("name_unique", func(fl validator.FieldLevel) bool {
 		num, err := GetDNSEntryNum(entry.Name, entry.Network)
 		return err == nil && num == 0
 	})
 
-	_ = v.RegisterValidation("name_valid", func(fl validator.FieldLevel) bool {
-		isvalid := functions.NameInDNSCharSet(entry.Name)
-                notEmptyCheck := len(entry.Name) > 0
-		return isvalid && notEmptyCheck
-	})
-
-	_ = v.RegisterValidation("address_valid", func(fl validator.FieldLevel) bool {
-		notEmptyCheck := len(entry.Address) > 0
-                isIp := functions.IsIpNet(entry.Address)
-		return notEmptyCheck && isIp
+	_ = v.RegisterValidation("network_exists", func(fl validator.FieldLevel) bool {
+		_, err := functions.GetParentNetwork(entry.Network)
+		return err == nil
 	})
-        _ = v.RegisterValidation("network_exists", func(fl validator.FieldLevel) bool {
-                _, err := functions.GetParentNetwork(entry.Network)
-                return err == nil
-        })
 
 	err := v.Struct(entry)
-
 	if err != nil {
 		for _, e := range err.(validator.ValidationErrors) {
 			fmt.Println(e)
@@ -490,41 +489,42 @@ func ValidateDNSCreate(entry models.DNSEntry) error {
 
 func ValidateDNSUpdate(change models.DNSEntry, entry models.DNSEntry) error {
 
-        v := validator.New()
+	v := validator.New()
 
-        _ = v.RegisterValidation("name_unique", func(fl validator.FieldLevel) bool {
-		goodNum := false
-                num, err := GetDNSEntryNum(entry.Name, entry.Network)
-		if change.Name != entry.Name {
-			goodNum = num == 0
-		} else {
-                        goodNum = num == 1
-		}
-		return err == nil && goodNum
-        })
-
-        _ = v.RegisterValidation("name_valid", func(fl validator.FieldLevel) bool {
-                isvalid := functions.NameInDNSCharSet(entry.Name)
-                notEmptyCheck := entry.Name != ""
-                return isvalid && notEmptyCheck
-        })
-
-        _ = v.RegisterValidation("address_valid", func(fl validator.FieldLevel) bool {
-		isValid := true
-		if entry.Address != "" {
-			isValid = functions.IsIpNet(entry.Address)
+	_ = v.RegisterValidation("name_unique", func(fl validator.FieldLevel) bool {
+		//if name & net not changing name we are good
+		if change.Name == entry.Name && change.Network == entry.Network {
+			return true
 		}
-		return isValid
-        })
+		num, err := GetDNSEntryNum(change.Name, change.Network)
+		return err == nil && num == 0
+	})
+	_ = v.RegisterValidation("network_exists", func(fl validator.FieldLevel) bool {
+		_, err := functions.GetParentNetwork(change.Network)
+		fmt.Println(err, entry.Network)
+		return err == nil
+	})
 
-        err := v.Struct(entry)
+	//	_ = v.RegisterValidation("name_valid", func(fl validator.FieldLevel) bool {
+	//		isvalid := functions.NameInDNSCharSet(entry.Name)
+	//		notEmptyCheck := entry.Name != ""
+	//		return isvalid && notEmptyCheck
+	//	})
+	//
+	//	_ = v.RegisterValidation("address_valid", func(fl validator.FieldLevel) bool {
+	//		isValid := true
+	//		if entry.Address != "" {
+	//			isValid = functions.IsIpNet(entry.Address)
+	//		}
+	//		return isValid
+	//	})
+
+	err := v.Struct(change)
 
-        if err != nil {
-                for _, e := range err.(validator.ValidationErrors) {
-                        fmt.Println(e)
-                }
-        }
-        return err
+	if err != nil {
+		for _, e := range err.(validator.ValidationErrors) {
+			fmt.Println(e)
+		}
+	}
+	return err
 }
-
-

+ 180 - 0
controllers/dnsHttpController_test.go

@@ -0,0 +1,180 @@
+package controller
+
+import (
+	"testing"
+
+	"github.com/gravitl/netmaker/models"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestGetNodeDNS(t *testing.T) {
+	dns, err := GetNodeDNS("skynet")
+	assert.Nil(t, err)
+	t.Log(dns)
+}
+func TestGetCustomDNS(t *testing.T) {
+	dns, err := GetCustomDNS("skynet")
+	assert.Nil(t, err)
+	t.Log(dns)
+}
+func TestGetDNSEntryNum(t *testing.T) {
+	num, err := GetDNSEntryNum("myhost", "skynet")
+	assert.Nil(t, err)
+	t.Log(num)
+}
+func TestGetDNS(t *testing.T) {
+	dns, err := GetDNS("skynet")
+	assert.Nil(t, err)
+	t.Log(dns)
+}
+func TestCreateDNS(t *testing.T) {
+	deleteNet(t)
+	createNet()
+	//dns, err := GetDNS("skynet")
+	//assert.Nil(t, err)
+	//for _, entry := range dns {
+	//	_, _ = DeleteDNS(entry.Name, "skynet")
+	//}
+	entry := models.DNSEntry{"10.0.0.2", "myhost", "skynet"}
+	err := ValidateDNSCreate(entry)
+	assert.Nil(t, err)
+	if err != nil {
+		return
+	}
+	dns, err := CreateDNS(entry)
+	assert.Nil(t, err)
+	t.Log(dns)
+}
+func TestGetDNSEntry(t *testing.T) {
+	entry, err := GetDNSEntry("myhost", "skynet")
+	assert.Nil(t, err)
+	t.Log(entry)
+}
+func TestUpdateDNS(t *testing.T) {
+}
+func TestDeleteDNS(t *testing.T) {
+	t.Run("EntryExists", func(t *testing.T) {
+		success, err := DeleteDNS("myhost", "skynet")
+		assert.Nil(t, err)
+		assert.True(t, success)
+	})
+	t.Run("NoEntry", func(t *testing.T) {
+		success, err := DeleteDNS("myhost", "skynet")
+		assert.Nil(t, err)
+		assert.False(t, success)
+	})
+
+}
+
+func TestValidateDNSUpdate(t *testing.T) {
+	entry := models.DNSEntry{"10.0.0.2", "myhost", "skynet"}
+	_, _ = DeleteDNS("mynode", "skynet")
+	t.Run("BadNetwork", func(t *testing.T) {
+		change := models.DNSEntry{"10.0.0.2", "myhost", "badnet"}
+		err := ValidateDNSUpdate(change, entry)
+		assert.NotNil(t, err)
+		assert.Contains(t, err.Error(), "Field validation for 'Network' failed on the 'network_exists' tag")
+	})
+	t.Run("EmptyNetwork", func(t *testing.T) {
+		//this can't actually happen as change.Network is populated if is blank
+		change := models.DNSEntry{"10.0.0.2", "myhost", ""}
+		err := ValidateDNSUpdate(change, entry)
+		assert.NotNil(t, err)
+		assert.Contains(t, err.Error(), "Field validation for 'Network' failed on the 'network_exists' tag")
+	})
+	t.Run("EmptyAddress", func(t *testing.T) {
+		//this can't actually happen as change.Address is populated if is blank
+		change := models.DNSEntry{"", "myhost", "skynet"}
+		err := ValidateDNSUpdate(change, entry)
+		assert.NotNil(t, err)
+		assert.Contains(t, err.Error(), "Field validation for 'Address' failed on the 'required' tag")
+	})
+	t.Run("BadAddress", func(t *testing.T) {
+		change := models.DNSEntry{"10.0.256.1", "myhost", "skynet"}
+		err := ValidateDNSUpdate(change, entry)
+		assert.NotNil(t, err)
+		assert.Contains(t, err.Error(), "Field validation for 'Address' failed on the 'ip' tag")
+	})
+	t.Run("BadName", func(t *testing.T) {
+		change := models.DNSEntry{"10.0.0.2", "myhostr*", "skynet"}
+		err := ValidateDNSUpdate(change, entry)
+		assert.NotNil(t, err)
+		assert.Contains(t, err.Error(), "Field validation for 'Name' failed on the 'alphanum' tag")
+	})
+	t.Run("EmptyName", func(t *testing.T) {
+		//this can't actually happen as change.Name is populated if is blank
+		change := models.DNSEntry{"10.0.0.2", "", "skynet"}
+		err := ValidateDNSUpdate(change, entry)
+		assert.NotNil(t, err)
+		assert.Contains(t, err.Error(), "Field validation for 'Name' failed on the 'required' tag")
+	})
+	t.Run("NameTooLong", func(t *testing.T) {
+		name := ""
+		for i := 1; i < 122; i++ {
+			name = name + "a"
+		}
+		change := models.DNSEntry{"10.0.0.2", name, "skynet"}
+		err := ValidateDNSUpdate(change, entry)
+		assert.NotNil(t, err)
+		assert.Contains(t, err.Error(), "Field validation for 'Name' failed on the 'max' tag")
+	})
+	t.Run("NameUnique", func(t *testing.T) {
+		change := models.DNSEntry{"10.0.0.2", "myhost", "wirecat"}
+		_, _ = CreateDNS(entry)
+		_, _ = CreateDNS(change)
+		err := ValidateDNSUpdate(change, entry)
+		assert.NotNil(t, err)
+		assert.Contains(t, err.Error(), "Field validation for 'Name' failed on the 'name_unique' tag")
+	})
+
+}
+func TestValidateDNSCreate(t *testing.T) {
+	_, _ = DeleteDNS("mynode", "skynet")
+	t.Run("NoNetwork", func(t *testing.T) {
+		entry := models.DNSEntry{"10.0.0.2", "myhost", "badnet"}
+		err := ValidateDNSCreate(entry)
+		assert.NotNil(t, err)
+		assert.Contains(t, err.Error(), "Field validation for 'Network' failed on the 'network_exists' tag")
+	})
+	t.Run("EmptyAddress", func(t *testing.T) {
+		entry := models.DNSEntry{"", "myhost", "skynet"}
+		err := ValidateDNSCreate(entry)
+		assert.NotNil(t, err)
+		assert.Contains(t, err.Error(), "Field validation for 'Address' failed on the 'required' tag")
+	})
+	t.Run("BadAddress", func(t *testing.T) {
+		entry := models.DNSEntry{"10.0.256.1", "myhost", "skynet"}
+		err := ValidateDNSCreate(entry)
+		assert.NotNil(t, err)
+		assert.Contains(t, err.Error(), "Field validation for 'Address' failed on the 'ip' tag")
+	})
+	t.Run("BadName", func(t *testing.T) {
+		entry := models.DNSEntry{"10.0.0.2", "myhostr*", "skynet"}
+		err := ValidateDNSCreate(entry)
+		assert.NotNil(t, err)
+		assert.Contains(t, err.Error(), "Field validation for 'Name' failed on the 'alphanum' tag")
+	})
+	t.Run("EmptyName", func(t *testing.T) {
+		entry := models.DNSEntry{"10.0.0.2", "", "skynet"}
+		err := ValidateDNSCreate(entry)
+		assert.NotNil(t, err)
+		assert.Contains(t, err.Error(), "Field validation for 'Name' failed on the 'required' tag")
+	})
+	t.Run("NameTooLong", func(t *testing.T) {
+		name := ""
+		for i := 1; i < 122; i++ {
+			name = name + "a"
+		}
+		entry := models.DNSEntry{"10.0.0.2", name, "skynet"}
+		err := ValidateDNSCreate(entry)
+		assert.NotNil(t, err)
+		assert.Contains(t, err.Error(), "Field validation for 'Name' failed on the 'max' tag")
+	})
+	t.Run("NameUnique", func(t *testing.T) {
+		entry := models.DNSEntry{"10.0.0.2", "myhost", "skynet"}
+		_, _ = CreateDNS(entry)
+		err := ValidateDNSCreate(entry)
+		assert.NotNil(t, err)
+		assert.Contains(t, err.Error(), "Field validation for 'Name' failed on the 'name_unique' tag")
+	})
+}

+ 464 - 0
controllers/extClientHttpController.go

@@ -0,0 +1,464 @@
+package controller
+
+import (
+	"context"
+	"encoding/json"
+	"io"
+	"errors"
+	"fmt"
+        "math/rand"
+
+	// "fmt"
+	"net/http"
+	"time"
+	"strconv"
+	"github.com/gorilla/mux"
+	"github.com/gravitl/netmaker/functions"
+	"github.com/gravitl/netmaker/models"
+	"github.com/gravitl/netmaker/mongoconn"
+	"go.mongodb.org/mongo-driver/bson"
+	"go.mongodb.org/mongo-driver/mongo/options"
+	"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
+	"github.com/skip2/go-qrcode"
+)
+
+func extClientHandlers(r *mux.Router) {
+
+	r.HandleFunc("/api/extclients", securityCheck(http.HandlerFunc(getAllExtClients))).Methods("GET")
+	r.HandleFunc("/api/extclients/{network}", securityCheck(http.HandlerFunc(getNetworkExtClients))).Methods("GET")
+	r.HandleFunc("/api/extclients/{network}/{clientid}", securityCheck(http.HandlerFunc(getExtClient))).Methods("GET")
+	r.HandleFunc("/api/extclients/{network}/{clientid}/{type}", securityCheck(http.HandlerFunc(getExtClientConf))).Methods("GET")
+	r.HandleFunc("/api/extclients/{network}/{clientid}", securityCheck(http.HandlerFunc(updateExtClient))).Methods("PUT")
+	r.HandleFunc("/api/extclients/{network}/{clientid}", securityCheck(http.HandlerFunc(deleteExtClient))).Methods("DELETE")
+	r.HandleFunc("/api/extclients/{network}/{macaddress}", securityCheck(http.HandlerFunc(createExtClient))).Methods("POST")
+}
+
+// TODO: Implement Validation
+func ValidateExtClientCreate(networkName string, extclient models.ExtClient) error {
+	// 	v := validator.New()
+	// 	_ = v.RegisterValidation("macaddress_unique", func(fl validator.FieldLevel) bool {
+	// 		var isFieldUnique bool = functions.IsFieldUnique(networkName, "macaddress", extclient.MacAddress)
+	// 		return isFieldUnique
+	// 	})
+	// 	_ = v.RegisterValidation("network_exists", func(fl validator.FieldLevel) bool {
+	// 		_, err := extclient.GetNetwork()
+	// 		return err == nil
+	// 	})
+	// 	err := v.Struct(extclient)
+
+	// 	if err != nil {
+	// 		for _, e := range err.(validator.ValidationErrors) {
+	// 			fmt.Println(e)
+	// 		}
+	// 	}
+	return nil
+}
+
+// TODO: Implement Validation
+func ValidateExtClientUpdate(networkName string, extclient models.ExtClient) error {
+	// v := validator.New()
+	// _ = v.RegisterValidation("network_exists", func(fl validator.FieldLevel) bool {
+	// 	_, err := extclient.GetNetwork()
+	// 	return err == nil
+	// })
+	// err := v.Struct(extclient)
+	// if err != nil {
+	// 	for _, e := range err.(validator.ValidationErrors) {
+	// 		fmt.Println(e)
+	// 	}
+	// }
+	return nil
+}
+
+func checkIngressExists(network string, macaddress string) bool {
+	node, err := functions.GetNodeByMacAddress(network, macaddress)
+	if err != nil {
+		return false
+	}
+	return node.IsIngressGateway
+}
+
+//Gets all extclients associated with network, including pending extclients
+func getNetworkExtClients(w http.ResponseWriter, r *http.Request) {
+
+	w.Header().Set("Content-Type", "application/json")
+
+	var extclients []models.ExtClient
+	var params = mux.Vars(r)
+	extclients, err := GetNetworkExtClients(params["network"])
+	if err != nil {
+		returnErrorResponse(w, r, formatError(err, "internal"))
+		return
+	}
+
+	//Returns all the extclients in JSON format
+	w.WriteHeader(http.StatusOK)
+	json.NewEncoder(w).Encode(extclients)
+}
+
+func GetNetworkExtClients(network string) ([]models.ExtClient, error) {
+	var extclients []models.ExtClient
+	collection := mongoconn.Client.Database("netmaker").Collection("extclients")
+	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+	filter := bson.M{"network": network}
+	//Filtering out the ID field cuz Dillon doesn't like it. May want to filter out other fields in the future
+	cur, err := collection.Find(ctx, filter, options.Find().SetProjection(bson.M{"_id": 0}))
+	if err != nil {
+		return []models.ExtClient{}, err
+	}
+	defer cancel()
+	for cur.Next(context.TODO()) {
+		//Using a different model for the ReturnExtClient (other than regular extclient).
+		//Either we should do this for ALL structs (so Networks and Keys)
+		//OR we should just use the original struct
+		//My preference is to make some new return structs
+		//TODO: Think about this. Not an immediate concern. Just need to get some consistency eventually
+		var extclient models.ExtClient
+		err := cur.Decode(&extclient)
+		if err != nil {
+			return []models.ExtClient{}, err
+		}
+		// add item our array of extclients
+		extclients = append(extclients, extclient)
+	}
+	//TODO: Another fatal error we should take care of.
+	if err := cur.Err(); err != nil {
+		return []models.ExtClient{}, err
+	}
+	return extclients, nil
+}
+
+//A separate function to get all extclients, not just extclients for a particular network.
+//Not quite sure if this is necessary. Probably necessary based on front end but may want to review after iteration 1 if it's being used or not
+func getAllExtClients(w http.ResponseWriter, r *http.Request) {
+	w.Header().Set("Content-Type", "application/json")
+	extclients, err := functions.GetAllExtClients()
+	if err != nil {
+		returnErrorResponse(w, r, formatError(err, "internal"))
+		return
+	}
+	//Return all the extclients in JSON format
+	w.WriteHeader(http.StatusOK)
+	json.NewEncoder(w).Encode(extclients)
+}
+
+//Get an individual extclient. Nothin fancy here folks.
+func getExtClient(w http.ResponseWriter, r *http.Request) {
+	// set header.
+	w.Header().Set("Content-Type", "application/json")
+
+	var params = mux.Vars(r)
+
+	var extclient models.ExtClient
+
+	collection := mongoconn.Client.Database("netmaker").Collection("extclients")
+
+	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+
+	filter := bson.M{"network": params["network"], "clientid": params["clientid"]}
+	err := collection.FindOne(ctx, filter, options.FindOne().SetProjection(bson.M{"_id": 0})).Decode(&extclient)
+	if err != nil {
+		returnErrorResponse(w, r, formatError(err, "internal"))
+		return
+	}
+
+	defer cancel()
+
+	w.WriteHeader(http.StatusOK)
+	json.NewEncoder(w).Encode(extclient)
+}
+
+//Get an individual extclient. Nothin fancy here folks.
+func getExtClientConf(w http.ResponseWriter, r *http.Request) {
+        // set header.
+        w.Header().Set("Content-Type", "application/json")
+
+        var params = mux.Vars(r)
+
+        var extclient models.ExtClient
+
+        collection := mongoconn.Client.Database("netmaker").Collection("extclients")
+
+        ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+
+        filter := bson.M{"network": params["network"], "clientid": params["clientid"]}
+        err := collection.FindOne(ctx, filter, options.FindOne().SetProjection(bson.M{"_id": 0})).Decode(&extclient)
+        if err != nil {
+                returnErrorResponse(w, r, formatError(err, "internal"))
+                return
+        }
+
+        gwnode, err := functions.GetNodeByMacAddress(extclient.Network, extclient.IngressGatewayID)
+        if err != nil {
+		fmt.Println("Could not retrieve Ingress Gateway Node " + extclient.IngressGatewayID)
+		returnErrorResponse(w, r, formatError(err, "internal"))
+                return
+        }
+
+	network, err := functions.GetParentNetwork(extclient.Network)
+        if err != nil {
+                fmt.Println("Could not retrieve Ingress Gateway Network " + extclient.Network)
+                returnErrorResponse(w, r, formatError(err, "internal"))
+                return
+        }
+	keepalive := ""
+	if network.DefaultKeepalive != 0 {
+		keepalive = "PersistentKeepalive = " + strconv.Itoa(int(network.DefaultKeepalive))
+	}
+	gwendpoint := gwnode.Endpoint + ":" + strconv.Itoa(int(gwnode.ListenPort))
+	config := fmt.Sprintf(`[Interface]
+Address = %s
+PrivateKey = %s
+
+[Peer]
+PublicKey = %s
+AllowedIPs = %s
+Endpoint = %s
+%s
+
+`, extclient.Address + "/32",
+   extclient.PrivateKey,
+   gwnode.PublicKey,
+   network.AddressRange,
+   gwendpoint,
+   keepalive)
+
+	if params["type"] == "qr" {
+		bytes, err := qrcode.Encode(config, qrcode.Medium, 220)
+		if err != nil {
+			returnErrorResponse(w, r, formatError(err, "internal"))
+			return
+		}
+		w.Header().Set("Content-Type", "image/png")
+		w.WriteHeader(http.StatusOK)
+		_, err = w.Write(bytes)
+		if err != nil {
+			returnErrorResponse(w, r, formatError(err, "internal"))
+			return
+		}
+		return
+	}
+
+	if params["type"] == "file" {
+		name := extclient.ClientID + ".conf"
+                w.Header().Set("Content-Type", "application/config")
+		w.Header().Set("Content-Disposition", "attachment; filename=\"" + name + "\"")
+		w.WriteHeader(http.StatusOK)
+		_, err := fmt.Fprint(w, config)
+		if err != nil {
+                        returnErrorResponse(w, r, formatError(err, "internal"))
+		}
+		return
+	}
+
+        defer cancel()
+
+        w.WriteHeader(http.StatusOK)
+        json.NewEncoder(w).Encode(extclient)
+}
+
+func CreateExtClient(extclient models.ExtClient) error {
+	if extclient.PrivateKey == "" {
+		privateKey, err := wgtypes.GeneratePrivateKey()
+		if err != nil {
+			return err
+		}
+
+		extclient.PrivateKey = privateKey.String()
+		extclient.PublicKey = privateKey.PublicKey().String()
+	}
+
+	if extclient.Address == "" {
+		newAddress, err := functions.UniqueAddress(extclient.Network)
+		if err != nil {
+			return err
+		}
+		extclient.Address = newAddress
+	}
+
+        if extclient.ClientID == "" {
+                clientid := StringWithCharset(7, charset)
+                clientname := "client-" + clientid
+                extclient.ClientID = clientname
+        }
+
+	extclient.LastModified = time.Now().Unix()
+
+	collection := mongoconn.Client.Database("netmaker").Collection("extclients")
+	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+	// insert our network into the network table
+	_, err := collection.InsertOne(ctx, extclient)
+	defer cancel()
+	if err != nil {
+		return err
+	}
+
+	err = SetNetworkNodesLastModified(extclient.Network)
+
+	return err
+}
+
+//This one's a doozy
+//To create a extclient
+//Must have valid key and be unique
+func createExtClient(w http.ResponseWriter, r *http.Request) {
+	w.Header().Set("Content-Type", "application/json")
+
+	var params = mux.Vars(r)
+
+	networkName := params["network"]
+	macaddress := params["macaddress"]
+	//Check if network exists  first
+	//TODO: This is inefficient. Let's find a better way.
+	//Just a few rows down we grab the network anyway
+	ingressExists := checkIngressExists(networkName, macaddress)
+	if !ingressExists {
+		returnErrorResponse(w, r, formatError(errors.New("ingress does not exist"), "internal"))
+		return
+	}
+
+	var extclient models.ExtClient
+	extclient.Network = networkName
+	extclient.IngressGatewayID = macaddress
+	node, err := functions.GetNodeByMacAddress(networkName, macaddress)
+	if err != nil {
+                returnErrorResponse(w, r, formatError(err, "internal"))
+                return
+        }
+	extclient.IngressGatewayEndpoint = node.Endpoint + ":" + strconv.FormatInt(int64(node.ListenPort), 10)
+
+	err = json.NewDecoder(r.Body).Decode(&extclient)
+	if err != nil && !errors.Is(err, io.EOF) {
+		returnErrorResponse(w, r, formatError(err, "internal"))
+		return
+	}
+	err = ValidateExtClientCreate(params["network"], extclient)
+	if err != nil {
+		returnErrorResponse(w, r, formatError(err, "badrequest"))
+		return
+	}
+	err = CreateExtClient(extclient)
+
+	if err != nil {
+		returnErrorResponse(w, r, formatError(err, "internal"))
+		return
+	}
+	w.WriteHeader(http.StatusOK)
+}
+
+func updateExtClient(w http.ResponseWriter, r *http.Request) {
+	w.Header().Set("Content-Type", "application/json")
+
+	var params = mux.Vars(r)
+
+	var newExtClient models.ExtClient
+	var oldExtClient models.ExtClient
+	// we decode our body request params
+	_ = json.NewDecoder(r.Body).Decode(&newExtClient)
+	// TODO: Validation for update.
+	// err := ValidateExtClientUpdate(params["network"], params["clientid"], newExtClient)
+	// if err != nil {
+	// 	returnErrorResponse(w, r, formatError(err, "badrequest"))
+	// 	return
+	// }
+	collection := mongoconn.Client.Database("netmaker").Collection("extclients")
+	ctx, _ := context.WithTimeout(context.Background(), 10*time.Second)
+	filter := bson.M{"network": params["network"], "clientid": params["clientid"]}
+	err := collection.FindOne(ctx, filter, options.FindOne().SetProjection(bson.M{"_id": 0})).Decode(&oldExtClient)
+	if err != nil {
+		returnErrorResponse(w, r, formatError(err, "internal"))
+		return
+	}
+	newclient, err := UpdateExtClient(newExtClient.ClientID, params["network"], oldExtClient)
+	if err != nil {
+		returnErrorResponse(w, r, formatError(err, "internal"))
+		return
+	}
+
+	w.WriteHeader(http.StatusOK)
+	json.NewEncoder(w).Encode(newclient)
+}
+
+func UpdateExtClient(newclientid string, network string, client models.ExtClient) (models.ExtClient, error) {
+
+        //collection := mongoconn.ConnectDB()
+        collection := mongoconn.Client.Database("netmaker").Collection("extclients")
+
+        ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+
+        // Create filter
+	filter := bson.M{"clientid": client.ClientID, "network": network}
+
+        // prepare update model.
+        update := bson.D{
+                {"$set", bson.D{
+                        {"clientid", newclientid},
+                }},
+        }
+        var clientupdate models.ExtClient
+
+        err := collection.FindOneAndUpdate(ctx, filter, update).Decode(&clientupdate)
+
+        defer cancel()
+
+        return clientupdate, err
+}
+
+func DeleteExtClient(network string, clientid string) (bool, error) {
+
+	deleted := false
+
+	collection := mongoconn.Client.Database("netmaker").Collection("extclients")
+
+	filter := bson.M{"network": network, "clientid": clientid}
+
+	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+
+	result, err := collection.DeleteOne(ctx, filter)
+
+	deletecount := result.DeletedCount
+
+	if deletecount > 0 {
+		deleted = true
+	}
+
+	defer cancel()
+
+	fmt.Println("Deleted extclient client " + clientid + " from network " + network)
+	return deleted, err
+}
+
+//Delete a extclient
+//Pretty straightforward
+func deleteExtClient(w http.ResponseWriter, r *http.Request) {
+	// Set header
+	w.Header().Set("Content-Type", "application/json")
+
+	// get params
+	var params = mux.Vars(r)
+
+	success, err := DeleteExtClient(params["network"], params["clientid"])
+
+	if err != nil {
+		returnErrorResponse(w, r, formatError(err, "internal"))
+		return
+	} else if !success {
+		err = errors.New("Could not delete extclient " + params["clientid"])
+		returnErrorResponse(w, r, formatError(err, "internal"))
+		return
+	}
+	returnSuccessResponse(w, r, params["clientid"]+" deleted.")
+}
+
+func StringWithCharset(length int, charset string) string {
+        b := make([]byte, length)
+        for i := range b {
+                b[i] = charset[seededRand.Intn(len(charset))]
+        }
+        return string(b)
+}
+
+const charset = "abcdefghijklmnopqrstuvwxyz" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
+
+var seededRand *rand.Rand = rand.New(
+        rand.NewSource(time.Now().UnixNano()))

+ 207 - 0
controllers/intClientHttpController.go

@@ -0,0 +1,207 @@
+package controller
+
+import (
+	//	"fmt"
+	// "github.com/davecgh/go-spew/spew"
+	"errors"
+	"context"
+	"encoding/json"
+	"net/http"
+	"time"
+	"github.com/gorilla/mux"
+	"github.com/gravitl/netmaker/functions"
+	"github.com/gravitl/netmaker/serverctl"
+	"github.com/gravitl/netmaker/servercfg"
+	"github.com/gravitl/netmaker/models"
+	"github.com/gravitl/netmaker/mongoconn"
+	"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
+)
+
+func intClientHandlers(r *mux.Router) {
+
+	r.HandleFunc("/api/intclient/{clientid}", securityCheck(http.HandlerFunc(getIntClient))).Methods("GET")
+	r.HandleFunc("/api/intclients", securityCheck(http.HandlerFunc(getAllIntClients))).Methods("GET")
+        r.HandleFunc("/api/intclients/deleteall", securityCheck(http.HandlerFunc(deleteAllIntClients))).Methods("DELETE")
+        r.HandleFunc("/api/intclient/{clientid}", securityCheck(http.HandlerFunc(updateIntClient))).Methods("PUT")
+	r.HandleFunc("/api/intclient/register", http.HandlerFunc(registerIntClient)).Methods("POST")
+	r.HandleFunc("/api/intclient/{clientid}", http.HandlerFunc(deleteIntClient)).Methods("DELETE")
+}
+
+func getAllIntClients(w http.ResponseWriter, r *http.Request) {
+        w.Header().Set("Content-Type", "application/json")
+        clients, err := functions.GetAllIntClients()
+        if err != nil {
+                returnErrorResponse(w, r, formatError(err, "internal"))
+                return
+        }
+        //Return all the extclients in JSON format
+        w.WriteHeader(http.StatusOK)
+        json.NewEncoder(w).Encode(clients)
+}
+
+func deleteAllIntClients(w http.ResponseWriter, r *http.Request) {
+        w.Header().Set("Content-Type", "application/json")
+        err := functions.DeleteAllIntClients()
+        if err != nil {
+                returnErrorResponse(w, r, formatError(err, "internal"))
+                return
+        }
+        w.WriteHeader(http.StatusOK)
+}
+
+func deleteIntClient(w http.ResponseWriter, r *http.Request) {
+        w.Header().Set("Content-Type", "application/json")
+        // get params
+        var params = mux.Vars(r)
+
+        success, err := DeleteIntClient(params["clientid"])
+
+        if err != nil {
+                returnErrorResponse(w, r, formatError(err, "internal"))
+                return
+        } else if !success {
+                err = errors.New("Could not delete intclient " + params["clientid"])
+                returnErrorResponse(w, r, formatError(err, "internal"))
+                return
+        }
+        returnSuccessResponse(w, r, params["clientid"]+" deleted.")
+}
+
+
+func getIntClient(w http.ResponseWriter, r *http.Request) {
+        w.Header().Set("Content-Type", "application/json")
+        var params = mux.Vars(r)
+
+	client, err := GetIntClient(params["clientid"])
+        if err != nil {
+                returnErrorResponse(w, r, formatError(err, "internal"))
+                return
+        }
+        w.WriteHeader(http.StatusOK)
+        json.NewEncoder(w).Encode(client)
+}
+
+func updateIntClient(w http.ResponseWriter, r *http.Request) {
+        w.Header().Set("Content-Type", "application/json")
+
+        var errorResponse = models.ErrorResponse{
+                Code: http.StatusInternalServerError, Message: "W1R3: It's not you it's me.",
+        }
+
+        var clientreq models.IntClient
+
+        //get node from body of request
+        err := json.NewDecoder(r.Body).Decode(&clientreq)
+        if err != nil {
+                returnErrorResponse(w, r, formatError(err, "internal"))
+                return
+        }
+        if servercfg.IsRegisterKeyRequired() {
+                validKey := functions.IsKeyValidGlobal(clientreq.AccessKey)
+                if !validKey {
+                                errorResponse = models.ErrorResponse{
+                                        Code: http.StatusUnauthorized, Message: "W1R3: Key invalid, or none provided.",
+                                }
+                                returnErrorResponse(w, r, errorResponse)
+                                return
+                }
+        }
+        client, err := RegisterIntClient(clientreq)
+
+        if err != nil {
+                returnErrorResponse(w, r, formatError(err, "internal"))
+                return
+        }
+        w.WriteHeader(http.StatusOK)
+        json.NewEncoder(w).Encode(client)
+}
+
+func RegisterIntClient(client models.IntClient) (models.IntClient, error) {
+	if client.PrivateKey == "" {
+		privateKey, err := wgtypes.GeneratePrivateKey()
+		if err != nil {
+			return client, err
+		}
+
+		client.PrivateKey = privateKey.String()
+		client.PublicKey = privateKey.PublicKey().String()
+	}
+
+	if client.Address == "" {
+		newAddress, err := functions.UniqueAddress(client.Network)
+		if err != nil {
+			return client, err
+		}
+		if newAddress == "" {
+			return client, errors.New("Could not find an address.")
+		}
+		client.Address = newAddress
+	}
+        if client.Network == "" { client.Network = "comms" }
+	server, err := serverctl.GetServerWGConf()
+        //spew.Dump(server)
+	if err != nil {
+                return client, err
+        }
+	client.ServerPublicEndpoint = server.ServerPublicEndpoint
+        client.ServerAPIPort = server.ServerAPIPort
+        client.ServerPrivateAddress = server.ServerPrivateAddress
+        client.ServerWGPort = server.ServerWGPort
+        client.ServerGRPCPort = server.ServerGRPCPort
+        client.ServerKey = server.ServerKey
+
+        if client.ClientID == "" {
+                clientid := StringWithCharset(7, charset)
+                clientname := "client-" + clientid
+                client.ClientID = clientname
+        }
+
+	//spew.Dump(client)
+	collection := mongoconn.Client.Database("netmaker").Collection("intclients")
+	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+	// insert our network into the network table
+	_, err = collection.InsertOne(ctx, client)
+	defer cancel()
+
+	if err != nil {
+		return client, err
+	}
+
+	err = serverctl.ReconfigureServerWireGuard()
+
+	return client, err
+}
+func registerIntClient(w http.ResponseWriter, r *http.Request) {
+        w.Header().Set("Content-Type", "application/json")
+
+        var errorResponse = models.ErrorResponse{
+                Code: http.StatusInternalServerError, Message: "W1R3: It's not you it's me.",
+        }
+
+        var clientreq models.IntClient
+
+        //get node from body of request
+        err := json.NewDecoder(r.Body).Decode(&clientreq)
+        if err != nil {
+                returnErrorResponse(w, r, formatError(err, "internal"))
+                return
+        }
+        if servercfg.IsRegisterKeyRequired() {
+                validKey := functions.IsKeyValidGlobal(clientreq.AccessKey)
+                if !validKey {
+                                errorResponse = models.ErrorResponse{
+                                        Code: http.StatusUnauthorized, Message: "W1R3: Key invalid, or none provided.",
+                                }
+                                returnErrorResponse(w, r, errorResponse)
+                                return
+                }
+        }
+        client, err := RegisterIntClient(clientreq)
+
+        if err != nil {
+                returnErrorResponse(w, r, formatError(err, "internal"))
+                return
+        }
+        w.WriteHeader(http.StatusOK)
+        json.NewEncoder(w).Encode(client)
+}

+ 331 - 309
controllers/networkHttpController.go

@@ -9,14 +9,16 @@ import (
 	"net/http"
 	"strings"
 	"time"
+
+	"github.com/go-playground/validator/v10"
 	"github.com/gorilla/mux"
-	"github.com/gravitl/netmaker/servercfg"
 	"github.com/gravitl/netmaker/functions"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/mongoconn"
+	"github.com/gravitl/netmaker/servercfg"
 	"go.mongodb.org/mongo-driver/bson"
+	"go.mongodb.org/mongo-driver/mongo"
 	"go.mongodb.org/mongo-driver/mongo/options"
-	"gopkg.in/go-playground/validator.v9"
 )
 
 func networkHandlers(r *mux.Router) {
@@ -28,6 +30,7 @@ func networkHandlers(r *mux.Router) {
 	r.HandleFunc("/api/networks/{networkname}/keyupdate", securityCheck(http.HandlerFunc(keyUpdate))).Methods("POST")
 	r.HandleFunc("/api/networks/{networkname}/keys", securityCheck(http.HandlerFunc(createAccessKey))).Methods("POST")
 	r.HandleFunc("/api/networks/{networkname}/keys", securityCheck(http.HandlerFunc(getAccessKeys))).Methods("GET")
+        r.HandleFunc("/api/networks/{networkname}/signuptoken", securityCheck(http.HandlerFunc(getSignupToken))).Methods("GET")
 	r.HandleFunc("/api/networks/{networkname}/keys/{name}", securityCheck(http.HandlerFunc(deleteAccessKey))).Methods("DELETE")
 }
 
@@ -37,47 +40,50 @@ func networkHandlers(r *mux.Router) {
 func securityCheck(next http.Handler) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 		var errorResponse = models.ErrorResponse{
-			Code: http.StatusInternalServerError, Message: "W1R3: It's not you it's me.",
+			Code: http.StatusUnauthorized, Message: "W1R3: It's not you it's me.",
 		}
 
 		var params = mux.Vars(r)
-		hasnetwork := params["networkname"] != ""
-		networkexists, err := functions.NetworkExists(params["networkname"])
+		bearerToken := r.Header.Get("Authorization")
+		err := SecurityCheck(params["networkname"], bearerToken)
 		if err != nil {
-			returnErrorResponse(w, r, formatError(err, "internal"))
-			return
-		} else if hasnetwork && !networkexists {
-			errorResponse = models.ErrorResponse{
-				Code: http.StatusNotFound, Message: "W1R3: This network does not exist.",
+			if strings.Contains(err.Error(), "does not exist") {
+				errorResponse.Code = http.StatusNotFound
 			}
+			errorResponse.Message = err.Error()
 			returnErrorResponse(w, r, errorResponse)
 			return
-		} else {
-
-			bearerToken := r.Header.Get("Authorization")
+		}
+		next.ServeHTTP(w, r)
+	}
+}
+func SecurityCheck(netname, token string) error {
+	hasnetwork := netname != ""
+	networkexists, err := functions.NetworkExists(netname)
+	if err != nil {
+		return err
+	}
+	if hasnetwork && !networkexists {
+		return errors.New("This network does not exist")
+	}
 
-			var hasBearer = true
-			var tokenSplit = strings.Split(bearerToken, " ")
-			var authToken = ""
+	var hasBearer = true
+	var tokenSplit = strings.Split(token, " ")
+	var authToken = ""
 
-			if len(tokenSplit) < 2 {
-				hasBearer = false
-			} else {
-				authToken = tokenSplit[1]
-			}
-			//all endpoints here require master so not as complicated
-			//still might not be a good  way of doing this
-			if !hasBearer || !authenticateMaster(authToken) {
-				errorResponse = models.ErrorResponse{
-					Code: http.StatusUnauthorized, Message: "W1R3: You are unauthorized to access this endpoint.",
-				}
-				returnErrorResponse(w, r, errorResponse)
-				return
-			} else {
-				next.ServeHTTP(w, r)
-			}
+	if len(tokenSplit) < 2 {
+		hasBearer = false
+	} else {
+		authToken = tokenSplit[1]
+	}
+	//all endpoints here require master so not as complicated
+	if !hasBearer || !authenticateMaster(authToken) {
+		_, isadmin, err := functions.VerifyUserToken(authToken)
+		if err != nil || !isadmin {
+			return errors.New("You are unauthorized to access this endpoint")
 		}
 	}
+	return nil
 }
 
 //Consider a more secure way of setting master key
@@ -91,43 +97,57 @@ func authenticateMaster(tokenString string) bool {
 //simple get all networks function
 func getNetworks(w http.ResponseWriter, r *http.Request) {
 
-	networks, err := functions.ListNetworks()
-
+	allnetworks, err := functions.ListNetworks()
 	if err != nil {
 		returnErrorResponse(w, r, formatError(err, "internal"))
 		return
-	} else {
-		w.WriteHeader(http.StatusOK)
-		json.NewEncoder(w).Encode(networks)
-		return
 	}
+	networks := RemoveComms(allnetworks)
+
+	w.WriteHeader(http.StatusOK)
+	json.NewEncoder(w).Encode(networks)
+	return
 }
 
-func validateNetworkUpdate(network models.Network) error {
+func RemoveComms(networks []models.Network) []models.Network {
+	var index int = 100000001
+	for ind, net := range networks {
+		if net.NetID == "comms" {
+			index = ind
+		}
+	}
+	if index == 100000001 {
+		return networks
+	}
+	returnable := make([]models.Network, 0)
+	returnable = append(returnable, networks[:index]...)
+	return append(returnable, networks[index+1:]...)
+}
 
+func ValidateNetworkUpdate(network models.NetworkUpdate) error {
 	v := validator.New()
 
-	_ = v.RegisterValidation("addressrange_valid", func(fl validator.FieldLevel) bool {
-		isvalid := fl.Field().String() == "" || functions.IsIpCIDR(fl.Field().String())
-		return isvalid
-	})
-        _ = v.RegisterValidation("addressrange6_valid", func(fl validator.FieldLevel) bool {
-                isvalid := fl.Field().String() == "" || functions.IsIpCIDR(fl.Field().String())
-                return isvalid
-        })
+	//	_ = v.RegisterValidation("addressrange_valid", func(fl validator.FieldLevel) bool {
+	//		isvalid := fl.Field().String() == "" || functions.IsIpCIDR(fl.Field().String())
+	//		return isvalid
+	//	})
+	//_ = v.RegisterValidation("addressrange6_valid", func(fl validator.FieldLevel) bool {
+	//		isvalid := fl.Field().String() == "" || functions.IsIpCIDR(fl.Field().String())
+	//		return isvalid
+	//	})
 
-	_ = v.RegisterValidation("localrange_valid", func(fl validator.FieldLevel) bool {
-		isvalid := fl.Field().String() == "" || functions.IsIpCIDR(fl.Field().String())
-		return isvalid
-	})
+	//	_ = v.RegisterValidation("localrange_valid", func(fl validator.FieldLevel) bool {
+	//		isvalid := fl.Field().String() == "" || functions.IsIpCIDR(fl.Field().String())
+	//		return isvalid
+	//	})
 
-	_ = v.RegisterValidation("netid_valid", func(fl validator.FieldLevel) bool {
-		return true
-	})
+	//	_ = v.RegisterValidation("netid_valid", func(fl validator.FieldLevel) bool {
+	//		return true
+	//	})
 
-	_ = v.RegisterValidation("displayname_unique", func(fl validator.FieldLevel) bool {
-		return true
-	})
+	//	_ = v.RegisterValidation("displayname_unique", func(fl validator.FieldLevel) bool {
+	//		return true
+	//	})
 
 	err := v.Struct(network)
 
@@ -139,34 +159,33 @@ func validateNetworkUpdate(network models.Network) error {
 	return err
 }
 
-func validateNetworkCreate(network models.Network) error {
+func ValidateNetworkCreate(network models.Network) error {
 
 	v := validator.New()
 
-	_ = v.RegisterValidation("addressrange_valid", func(fl validator.FieldLevel) bool {
-		isvalid := functions.IsIpCIDR(fl.Field().String())
-		return isvalid
-	})
-        _ = v.RegisterValidation("addressrange6_valid", func(fl validator.FieldLevel) bool {
+	//	_ = v.RegisterValidation("addressrange_valid", func(fl validator.FieldLevel) bool {
+	//		isvalid := functions.IsIpCIDR(fl.Field().String())
+	//		return isvalid
+	//	})
+	_ = v.RegisterValidation("addressrange6_valid", func(fl validator.FieldLevel) bool {
 		isvalid := true
 		if *network.IsDualStack {
 			isvalid = functions.IsIpCIDR(fl.Field().String())
 		}
 		return isvalid
-        })
-
-
-	_ = v.RegisterValidation("localrange_valid", func(fl validator.FieldLevel) bool {
-		isvalid := fl.Field().String() == "" || functions.IsIpCIDR(fl.Field().String())
-		return isvalid
 	})
-
+	//
+	//	_ = v.RegisterValidation("localrange_valid", func(fl validator.FieldLevel) bool {
+	//		isvalid := fl.Field().String() == "" || functions.IsIpCIDR(fl.Field().String())
+	//		return isvalid
+	//	})
+	//
 	_ = v.RegisterValidation("netid_valid", func(fl validator.FieldLevel) bool {
 		isFieldUnique, _ := functions.IsNetworkNameUnique(fl.Field().String())
-		inCharSet := functions.NameInNetworkCharSet(fl.Field().String())
-		return isFieldUnique && inCharSet
+		//		inCharSet := functions.NameInNetworkCharSet(fl.Field().String())
+		return isFieldUnique
 	})
-
+	//
 	_ = v.RegisterValidation("displayname_unique", func(fl validator.FieldLevel) bool {
 		isFieldUnique, _ := functions.IsNetworkDisplayNameUnique(fl.Field().String())
 		return isFieldUnique
@@ -184,61 +203,63 @@ func validateNetworkCreate(network models.Network) error {
 
 //Simple get network function
 func getNetwork(w http.ResponseWriter, r *http.Request) {
-
 	// set header.
 	w.Header().Set("Content-Type", "application/json")
-
 	var params = mux.Vars(r)
+	netname := params["networkname"]
+	network, err := GetNetwork(netname)
+	if err != nil {
+		returnErrorResponse(w, r, formatError(err, "internal"))
+		return
+	}
+	w.WriteHeader(http.StatusOK)
+	json.NewEncoder(w).Encode(network)
+}
 
+func GetNetwork(name string) (models.Network, error) {
 	var network models.Network
-
 	collection := mongoconn.Client.Database("netmaker").Collection("networks")
-
 	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
-
-	filter := bson.M{"netid": params["networkname"]}
+	filter := bson.M{"netid": name}
 	err := collection.FindOne(ctx, filter, options.FindOne().SetProjection(bson.M{"_id": 0})).Decode(&network)
-
 	defer cancel()
-
 	if err != nil {
-		returnErrorResponse(w, r, formatError(err, "internal"))
-		return
+		return models.Network{}, err
 	}
-	w.WriteHeader(http.StatusOK)
-	json.NewEncoder(w).Encode(network)
+	return network, nil
 }
 
 func keyUpdate(w http.ResponseWriter, r *http.Request) {
-
 	w.Header().Set("Content-Type", "application/json")
-
 	var params = mux.Vars(r)
-
-	var network models.Network
-
-	network, err := functions.GetParentNetwork(params["networkname"])
+	netname := params["networkname"]
+	network, err := KeyUpdate(netname)
 	if err != nil {
 		returnErrorResponse(w, r, formatError(err, "internal"))
 		return
 	}
+	w.WriteHeader(http.StatusOK)
+	json.NewEncoder(w).Encode(network)
+}
 
+func KeyUpdate(netname string) (models.Network, error) {
+	network, err := functions.GetParentNetwork(netname)
+	if err != nil {
+		return models.Network{}, err
+	}
 	network.KeyUpdateTimeStamp = time.Now().Unix()
-
 	collection := mongoconn.Client.Database("netmaker").Collection("networks")
-
 	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
-
-        filter := bson.M{"netid": params["networkname"]}
-        // prepare update model.
-        update := bson.D{
-                {"$set", bson.D{
-                        {"addressrange", network.AddressRange},
-                        {"addressrange6", network.AddressRange6},
-                        {"displayname", network.DisplayName},
-                        {"defaultlistenport", network.DefaultListenPort},
-                        {"defaultpostup", network.DefaultPostUp},
-                        {"defaultpostdown", network.DefaultPostDown},
+	filter := bson.M{"netid": netname}
+	// prepare update model.
+	update := bson.D{
+		{"$set", bson.D{
+			{"addressrange", network.AddressRange},
+			{"addressrange6", network.AddressRange6},
+			{"displayname", network.DisplayName},
+			{"defaultlistenport", network.DefaultListenPort},
+			{"defaultpostup", network.DefaultPostUp},
+			{"defaultpostdown", network.DefaultPostDown},
 			{"defaultkeepalive", network.DefaultKeepalive},
 			{"keyupdatetimestamp", network.KeyUpdateTimeStamp},
 			{"defaultsaveconfig", network.DefaultSaveConfig},
@@ -247,20 +268,14 @@ func keyUpdate(w http.ResponseWriter, r *http.Request) {
 			{"networklastmodified", network.NetworkLastModified},
 			{"allowmanualsignup", network.AllowManualSignUp},
 			{"checkininterval", network.DefaultCheckInInterval},
-		        }},
-	      }
-
+		}},
+	}
 	err = collection.FindOneAndUpdate(ctx, filter, update).Decode(&network)
-
 	defer cancel()
-
 	if err != nil {
-		returnErrorResponse(w, r, formatError(err, "internal"))
-		return
+		return models.Network{}, err
 	}
-
-	w.WriteHeader(http.StatusOK)
-	json.NewEncoder(w).Encode(network)
+	return network, nil
 }
 
 //Update a network
@@ -292,82 +307,70 @@ func AlertNetwork(netid string) error {
 
 //Update a network
 func updateNetwork(w http.ResponseWriter, r *http.Request) {
-
 	w.Header().Set("Content-Type", "application/json")
-
 	var params = mux.Vars(r)
-
 	var network models.Network
-
 	network, err := functions.GetParentNetwork(params["networkname"])
 	if err != nil {
 		returnErrorResponse(w, r, formatError(err, "internal"))
 		return
 	}
 
-	var networkChange models.Network
-
-	haschange := false
-	hasrangeupdate := false
-	haslocalrangeupdate := false
+	var networkChange models.NetworkUpdate
 
 	_ = json.NewDecoder(r.Body).Decode(&networkChange)
-
 	if networkChange.AddressRange == "" {
 		networkChange.AddressRange = network.AddressRange
 	}
-        if networkChange.AddressRange6 == "" {
-                networkChange.AddressRange6 = network.AddressRange6
-        }
+	if networkChange.AddressRange6 == "" {
+		networkChange.AddressRange6 = network.AddressRange6
+	}
 	if networkChange.NetID == "" {
 		networkChange.NetID = network.NetID
 	}
 
-	err = validateNetworkUpdate(networkChange)
+	err = ValidateNetworkUpdate(networkChange)
+	if err != nil {
+		returnErrorResponse(w, r, formatError(err, "badrequest"))
+		return
+	}
+	returnednetwork, err := UpdateNetwork(networkChange, network)
 	if err != nil {
 		returnErrorResponse(w, r, formatError(err, "badrequest"))
 		return
 	}
 
+	w.WriteHeader(http.StatusOK)
+	json.NewEncoder(w).Encode(returnednetwork)
+}
+
+func UpdateNetwork(networkChange models.NetworkUpdate, network models.Network) (models.Network, error) {
 	//NOTE: Network.NetID is intentionally NOT editable. It acts as a static ID for the network.
 	//DisplayName can be changed instead, which is what shows on the front end
 	if networkChange.NetID != network.NetID {
-		returnErrorResponse(w, r, formatError(errors.New("NetID is not editable"), "badrequest"))
-		return
+		return models.Network{}, errors.New("NetID is not editable")
 	}
-	//MRK:  I think this code block is redundant.  valdiateNetworkUpdate(networkChange) covers this
-	if networkChange.AddressRange != "" {
 
-		network.AddressRange = networkChange.AddressRange
+	haschange := false
+	hasrangeupdate := false
+	haslocalrangeupdate := false
 
-		var isAddressOK bool = functions.IsIpCIDR(networkChange.AddressRange)
-		if !isAddressOK {
-			err := errors.New("Invalid Range of " + networkChange.AddressRange + " for addresses.")
-			returnErrorResponse(w, r, formatError(err, "internal"))
-			return
-		}
+	if networkChange.AddressRange != "" {
 		haschange = true
 		hasrangeupdate = true
-
+		network.AddressRange = networkChange.AddressRange
 	}
 	if networkChange.LocalRange != "" {
-		network.LocalRange = networkChange.LocalRange
-
-		var isAddressOK bool = functions.IsIpCIDR(networkChange.LocalRange)
-		if !isAddressOK {
-			err := errors.New("Invalid Range of " + networkChange.LocalRange + " for internal addresses.")
-			returnErrorResponse(w, r, formatError(err, "internal"))
-			return
-		}
 		haschange = true
 		haslocalrangeupdate = true
+		network.LocalRange = networkChange.LocalRange
 	}
 	if networkChange.IsLocal != nil {
 		network.IsLocal = networkChange.IsLocal
 	}
-        if networkChange.IsDualStack != nil {
-                network.IsDualStack = networkChange.IsDualStack
-        }
+	if networkChange.IsDualStack != nil {
+		network.IsDualStack = networkChange.IsDualStack
+	}
 	if networkChange.DefaultListenPort != 0 {
 		network.DefaultListenPort = networkChange.DefaultListenPort
 		haschange = true
@@ -403,65 +406,59 @@ func updateNetwork(w http.ResponseWriter, r *http.Request) {
 
 	collection := mongoconn.Client.Database("netmaker").Collection("networks")
 	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
-	filter := bson.M{"netid": params["networkname"]}
+	filter := bson.M{"netid": network.NetID}
 
 	if haschange {
 		network.SetNetworkLastModified()
 	}
-        // prepare update model.
-        update := bson.D{
-                {"$set", bson.D{
-                        {"addressrange", network.AddressRange},
-                        {"addressrange6", network.AddressRange6},
-                        {"displayname", network.DisplayName},
-                        {"defaultlistenport", network.DefaultListenPort},
-                        {"defaultpostup", network.DefaultPostUp},
-                        {"defaultpostdown", network.DefaultPostDown},
-                        {"defaultkeepalive", network.DefaultKeepalive},
-                        {"defaultsaveconfig", network.DefaultSaveConfig},
-                        {"defaultinterface", network.DefaultInterface},
-                        {"nodeslastmodified", network.NodesLastModified},
-                        {"networklastmodified", network.NetworkLastModified},
-                        {"allowmanualsignup", network.AllowManualSignUp},
-                        {"localrange", network.LocalRange},
-                        {"islocal", network.IsLocal},
-                        {"isdualstack", network.IsDualStack},
-                        {"checkininterval", network.DefaultCheckInInterval},
-		              }},
+	// prepare update model.
+	update := bson.D{
+		{"$set", bson.D{
+			{"addressrange", network.AddressRange},
+			{"addressrange6", network.AddressRange6},
+			{"displayname", network.DisplayName},
+			{"defaultlistenport", network.DefaultListenPort},
+			{"defaultpostup", network.DefaultPostUp},
+			{"defaultpostdown", network.DefaultPostDown},
+			{"defaultkeepalive", network.DefaultKeepalive},
+			{"defaultsaveconfig", network.DefaultSaveConfig},
+			{"defaultinterface", network.DefaultInterface},
+			{"nodeslastmodified", network.NodesLastModified},
+			{"networklastmodified", network.NetworkLastModified},
+			{"allowmanualsignup", network.AllowManualSignUp},
+			{"localrange", network.LocalRange},
+			{"islocal", network.IsLocal},
+			{"isdualstack", network.IsDualStack},
+			{"checkininterval", network.DefaultCheckInInterval},
+		}},
 	}
 
-	err = collection.FindOneAndUpdate(ctx, filter, update).Decode(&network)
+	err := collection.FindOneAndUpdate(ctx, filter, update).Decode(&network)
 	defer cancel()
 
 	if err != nil {
-		returnErrorResponse(w, r, formatError(err, "internal"))
-		return
+		return models.Network{}, err
 	}
 
 	//Cycles through nodes and gives them new IP's based on the new range
 	//Pretty cool, but also pretty inefficient currently
 	if hasrangeupdate {
-		err = functions.UpdateNetworkNodeAddresses(params["networkname"])
+		err = functions.UpdateNetworkNodeAddresses(network.NetID)
 		if err != nil {
-			returnErrorResponse(w, r, formatError(err, "internal"))
-			return
+			return models.Network{}, err
 		}
 	}
 	if haslocalrangeupdate {
-		err = functions.UpdateNetworkPrivateAddresses(params["networkname"])
+		err = functions.UpdateNetworkPrivateAddresses(network.NetID)
 		if err != nil {
-			returnErrorResponse(w, r, formatError(err, "internal"))
-			return
+			return models.Network{}, err
 		}
 	}
 	returnnetwork, err := functions.GetParentNetwork(network.NetID)
 	if err != nil {
-		returnErrorResponse(w, r, formatError(err, "internal"))
-		return
+		return models.Network{}, err
 	}
-
-	w.WriteHeader(http.StatusOK)
-	json.NewEncoder(w).Encode(returnnetwork)
+	return returnnetwork, nil
 }
 
 //Delete a network
@@ -471,36 +468,46 @@ func deleteNetwork(w http.ResponseWriter, r *http.Request) {
 	w.Header().Set("Content-Type", "application/json")
 
 	var params = mux.Vars(r)
+	network := params["networkname"]
+	count, err := DeleteNetwork(network)
 
-	nodecount, err := functions.GetNetworkNodeNumber(params["networkname"])
 	if err != nil {
-		returnErrorResponse(w, r, formatError(err, "internal"))
-		return
-	} else if nodecount > 0 {
-		errorResponse := models.ErrorResponse{
-			Code: http.StatusForbidden, Message: "W1R3: Node check failed. All nodes must be deleted before deleting network.",
+		errtype := "badrequest"
+		if strings.Contains(err.Error(), "Node check failed"){
+			errtype = "forbidden"
 		}
-		returnErrorResponse(w, r, errorResponse)
+		returnErrorResponse(w, r, formatError(err, errtype))
 		return
 	}
+	w.WriteHeader(http.StatusOK)
+	json.NewEncoder(w).Encode(count)
+}
 
-	collection := mongoconn.Client.Database("netmaker").Collection("networks")
+func DeleteNetwork(network string) (*mongo.DeleteResult, error) {
+	none := &mongo.DeleteResult{}
 
-	filter := bson.M{"netid": params["networkname"]}
+	nodecount, err := functions.GetNetworkNodeNumber(network)
+	if err != nil {
+		//returnErrorResponse(w, r, formatError(err, "internal"))
+		return none, err
+	} else if nodecount > 0 {
+		//errorResponse := models.ErrorResponse{
+		//	Code: http.StatusForbidden, Message: "W1R3: Node check failed. All nodes must be deleted before deleting network.",
+		//}
+		//returnErrorResponse(w, r, errorResponse)
+		return none, errors.New("Node check failed. All nodes must be deleted before deleting network")
+	}
 
+	collection := mongoconn.Client.Database("netmaker").Collection("networks")
+	filter := bson.M{"netid": network}
 	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
-
 	deleteResult, err := collection.DeleteOne(ctx, filter)
-
 	defer cancel()
-
 	if err != nil {
-		returnErrorResponse(w, r, formatError(err, "internal"))
-		return
+		//returnErrorResponse(w, r, formatError(err, "internal"))
+		return none, err
 	}
-
-	w.WriteHeader(http.StatusOK)
-	json.NewEncoder(w).Encode(deleteResult)
+	return deleteResult, nil
 }
 
 //Create a network
@@ -518,6 +525,16 @@ func createNetwork(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	err = CreateNetwork(network)
+	if err != nil {
+		returnErrorResponse(w, r, formatError(err, "badrequest"))
+		return
+	}
+	w.WriteHeader(http.StatusOK)
+	//json.NewEncoder(w).Encode(result)
+}
+
+func CreateNetwork(network models.Network) error {
 	//TODO: Not really doing good validation here. Same as createNode, updateNode, and updateNetwork
 	//Need to implement some better validation across the board
 
@@ -525,15 +542,15 @@ func createNetwork(w http.ResponseWriter, r *http.Request) {
 		falsevar := false
 		network.IsLocal = &falsevar
 	}
-        if network.IsDualStack == nil {
-                falsevar := false
-                network.IsDualStack = &falsevar
-        }
+	if network.IsDualStack == nil {
+		falsevar := false
+		network.IsDualStack = &falsevar
+	}
 
-	err = validateNetworkCreate(network)
+	err := ValidateNetworkCreate(network)
 	if err != nil {
-		returnErrorResponse(w, r, formatError(err, "badrequest"))
-		return
+		//returnErrorResponse(w, r, formatError(err, "badrequest"))
+		return err
 	}
 	network.SetDefaults()
 	network.SetNodesLastModified()
@@ -544,16 +561,12 @@ func createNetwork(w http.ResponseWriter, r *http.Request) {
 	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
 
 	// insert our network into the network table
-	result, err := collection.InsertOne(ctx, network)
-
+	_, err = collection.InsertOne(ctx, network)
 	defer cancel()
-
 	if err != nil {
-		returnErrorResponse(w, r, formatError(err, "internal"))
-		return
+		return err
 	}
-	w.WriteHeader(http.StatusOK)
-	json.NewEncoder(w).Encode(result)
+	return nil
 }
 
 // BEGIN KEY MANAGEMENT SECTION
@@ -561,187 +574,196 @@ func createNetwork(w http.ResponseWriter, r *http.Request) {
 //TODO: Very little error handling
 //accesskey is created as a json string inside the Network collection item in mongo
 func createAccessKey(w http.ResponseWriter, r *http.Request) {
-
 	w.Header().Set("Content-Type", "application/json")
-
 	var params = mux.Vars(r)
-
-	var network models.Network
 	var accesskey models.AccessKey
-
 	//start here
 	network, err := functions.GetParentNetwork(params["networkname"])
 	if err != nil {
 		returnErrorResponse(w, r, formatError(err, "internal"))
 		return
 	}
-
 	err = json.NewDecoder(r.Body).Decode(&accesskey)
 	if err != nil {
 		returnErrorResponse(w, r, formatError(err, "internal"))
 		return
 	}
+	key, err := CreateAccessKey(accesskey, network)
+	if err != nil {
+		returnErrorResponse(w, r, formatError(err, "badrequest"))
+		return
+	}
+	w.WriteHeader(http.StatusOK)
+	json.NewEncoder(w).Encode(key)
+	//w.Write([]byte(accesskey.AccessString))
+}
+
+func CreateAccessKey(accesskey models.AccessKey, network models.Network) (models.AccessKey, error) {
 
 	if accesskey.Name == "" {
 		accesskey.Name = functions.GenKeyName()
 	}
+
 	if accesskey.Value == "" {
 		accesskey.Value = functions.GenKey()
 	}
-
 	if accesskey.Uses == 0 {
 		accesskey.Uses = 1
 	}
-	if err != nil {
-		returnErrorResponse(w, r, formatError(err, "internal"))
-		return
-	}
 
+        checkkeys, err := GetKeys(network.NetID)
+        if err != nil {
+                return models.AccessKey{}, errors.New("could not retrieve network keys")
+        }
+
+	for _, key := range checkkeys {
+		if key.Name == accesskey.Name {
+			return models.AccessKey{}, errors.New("Duplicate AccessKey Name")
+		}
+	}
 	privAddr := ""
-	if *network.IsLocal {
-		privAddr = network.LocalRange
+	if network.IsLocal != nil {
+		if *network.IsLocal {
+			privAddr = network.LocalRange
+		}
 	}
 
-	netID := params["networkname"]
-	address := servercfg.GetGRPCHost() + ":" + servercfg.GetGRPCPort()
+	netID := network.NetID
+	grpcaddress := servercfg.GetGRPCHost() + ":" + servercfg.GetGRPCPort()
+	apiaddress := servercfg.GetAPIHost() + ":" + servercfg.GetAPIPort()
+	wgport := servercfg.GetGRPCWGPort()
 
-	accessstringdec := address + "|" + netID + "|" + accesskey.Value + "|" + privAddr
+	accessstringdec := wgport + "|" +grpcaddress + "|" + apiaddress + "|" + netID + "|" + accesskey.Value + "|" + privAddr
 	accesskey.AccessString = base64.StdEncoding.EncodeToString([]byte(accessstringdec))
-
+	//validate accesskey
+	v := validator.New()
+	err = v.Struct(accesskey)
+	if err != nil {
+		for _, e := range err.(validator.ValidationErrors) {
+			fmt.Println(e)
+		}
+		return models.AccessKey{}, err
+	}
 	network.AccessKeys = append(network.AccessKeys, accesskey)
-
 	collection := mongoconn.Client.Database("netmaker").Collection("networks")
-
 	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
-
 	// Create filter
-	filter := bson.M{"netid": params["networkname"]}
-
+	filter := bson.M{"netid": network.NetID}
 	// Read update model from body request
-	fmt.Println("Adding key to " + network.NetID)
-
 	// prepare update model.
 	update := bson.D{
 		{"$set", bson.D{
 			{"accesskeys", network.AccessKeys},
 		}},
 	}
-
 	err = collection.FindOneAndUpdate(ctx, filter, update).Decode(&network)
-
 	defer cancel()
-
 	if err != nil {
-		returnErrorResponse(w, r, formatError(err, "internal"))
-		return
+		//returnErrorResponse(w, r, formatError(err, "internal"))
+		return models.AccessKey{}, err
 	}
-	w.WriteHeader(http.StatusOK)
-	json.NewEncoder(w).Encode(accesskey)
-	//w.Write([]byte(accesskey.AccessString))
+	return accesskey, nil
 }
 
-//pretty simple get
-func getAccessKeys(w http.ResponseWriter, r *http.Request) {
-
-	// set header.
-	w.Header().Set("Content-Type", "application/json")
-
-	var params = mux.Vars(r)
+func GetSignupToken(netID string) (models.AccessKey, error) {
 
-	var network models.Network
-	//var keys []models.DisplayKey
-	var keys []models.AccessKey
-	collection := mongoconn.Client.Database("netmaker").Collection("networks")
+	var accesskey models.AccessKey
+        address := servercfg.GetGRPCHost() + ":" + servercfg.GetGRPCPort()
 
-	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+        accessstringdec := address + "|" + netID + "|" + "" + "|"
+        accesskey.AccessString = base64.StdEncoding.EncodeToString([]byte(accessstringdec))
+        return accesskey, nil
+}
+func getSignupToken(w http.ResponseWriter, r *http.Request) {
+        w.Header().Set("Content-Type", "application/json")
+        var params = mux.Vars(r)
+        netID := params["networkname"]
+
+	token, err := GetSignupToken(netID)
+        if err != nil {
+                returnErrorResponse(w, r, formatError(err, "internal"))
+                return
+        }
+        w.WriteHeader(http.StatusOK)
+        json.NewEncoder(w).Encode(token)
+}
 
-	filter := bson.M{"netid": params["networkname"]}
-	err := collection.FindOne(ctx, filter, options.FindOne().SetProjection(bson.M{"_id": 0})).Decode(&network)
 
-	defer cancel()
 
+//pretty simple get
+func getAccessKeys(w http.ResponseWriter, r *http.Request) {
+	w.Header().Set("Content-Type", "application/json")
+	var params = mux.Vars(r)
+	network := params["networkname"]
+	keys, err := GetKeys(network)
 	if err != nil {
 		returnErrorResponse(w, r, formatError(err, "internal"))
 		return
 	}
-	keydata, err := json.Marshal(network.AccessKeys)
+	w.WriteHeader(http.StatusOK)
+	json.NewEncoder(w).Encode(keys)
+}
+func GetKeys(net string) ([]models.AccessKey, error) {
 
+	var network models.Network
+	collection := mongoconn.Client.Database("netmaker").Collection("networks")
+	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+	filter := bson.M{"netid": net}
+	err := collection.FindOne(ctx, filter, options.FindOne().SetProjection(bson.M{"_id": 0})).Decode(&network)
+	defer cancel()
 	if err != nil {
-		returnErrorResponse(w, r, formatError(err, "internal"))
-		return
+		return []models.AccessKey{}, err
 	}
-
-	json.Unmarshal(keydata, &keys)
-
-	w.WriteHeader(http.StatusOK)
-	json.NewEncoder(w).Encode(keys)
+	return network.AccessKeys, nil
 }
 
 //delete key. Has to do a little funky logic since it's not a collection item
 func deleteAccessKey(w http.ResponseWriter, r *http.Request) {
-
 	w.Header().Set("Content-Type", "application/json")
-
 	var params = mux.Vars(r)
-
-	var network models.Network
 	keyname := params["name"]
-
-	//start here
-	network, err := functions.GetParentNetwork(params["networkname"])
+	netname := params["networkname"]
+	err := DeleteKey(keyname, netname)
 	if err != nil {
-		returnErrorResponse(w, r, formatError(err, "internal"))
+		returnErrorResponse(w, r, formatError(err, "badrequest"))
 		return
 	}
+	w.WriteHeader(http.StatusOK)
+}
+func DeleteKey(keyname, netname string) error {
+	network, err := functions.GetParentNetwork(netname)
+	if err != nil {
+		return err
+	}
 	//basically, turn the list of access keys into the list of access keys before and after the item
 	//have not done any error handling for if there's like...1 item. I think it works? need to test.
 	found := false
-	for i := len(network.AccessKeys) - 1; i >= 0; i-- {
-
-		currentkey := network.AccessKeys[i]
+	var updatedKeys []models.AccessKey
+	for _, currentkey := range network.AccessKeys {
 		if currentkey.Name == keyname {
-			network.AccessKeys = append(network.AccessKeys[:i],
-				network.AccessKeys[i+1:]...)
 			found = true
+		} else {
+			updatedKeys = append(updatedKeys, currentkey)
 		}
 	}
 	if !found {
-		err = errors.New("key " + keyname + " does not exist")
-		returnErrorResponse(w, r, formatError(err, "badrequest"))
-		return
+		return errors.New("key " + keyname + " does not exist")
 	}
 
 	collection := mongoconn.Client.Database("netmaker").Collection("networks")
-
 	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
-
 	// Create filter
-	filter := bson.M{"netid": params["networkname"]}
-
+	filter := bson.M{"netid": netname}
 	// prepare update model.
 	update := bson.D{
 		{"$set", bson.D{
-			{"accesskeys", network.AccessKeys},
+			{"accesskeys", updatedKeys},
 		}},
 	}
-
 	err = collection.FindOneAndUpdate(ctx, filter, update).Decode(&network)
-
 	defer cancel()
-
-	if err != nil {
-		returnErrorResponse(w, r, formatError(err, "internal"))
-		return
-	}
-	var keys []models.AccessKey
-	keydata, err := json.Marshal(network.AccessKeys)
 	if err != nil {
-		returnErrorResponse(w, r, formatError(err, "internal"))
-		return
+		return err
 	}
-
-	json.Unmarshal(keydata, &keys)
-
-	w.WriteHeader(http.StatusOK)
-	json.NewEncoder(w).Encode(keys)
+	return nil
 }

+ 572 - 0
controllers/networkHttpController_test.go

@@ -0,0 +1,572 @@
+package controller
+
+import (
+	"testing"
+	"time"
+
+	"github.com/gravitl/netmaker/functions"
+	"github.com/gravitl/netmaker/models"
+	"github.com/stretchr/testify/assert"
+)
+
+type NetworkValidationTestCase struct {
+	testname   string
+	network    models.Network
+	errMessage string
+}
+
+func deleteNet(t *testing.T) {
+	nodes, err := functions.GetAllNodes()
+	assert.Nil(t, err)
+	for _, node := range nodes {
+		t.Log("deleting node", node.Name)
+		result, err := DeleteNode(node.MacAddress, node.Network)
+		assert.Nil(t, err)
+		assert.True(t, result)
+	}
+	dns, err := GetAllDNS()
+	assert.Nil(t, err)
+	for _, entry := range dns {
+		t.Log("deleting dns enty", entry.Name, entry.Network)
+		success, err := DeleteDNS(entry.Name, entry.Network)
+		assert.Nil(t, err)
+		assert.True(t, success)
+	}
+	networks, _ := functions.ListNetworks()
+	for _, network := range networks {
+		t.Log("deleting network", network.NetID)
+		success, err := DeleteNetwork(network.NetID)
+		t.Log(success, err)
+	}
+}
+
+func createNet() {
+	var network models.Network
+	network.NetID = "skynet"
+	network.AddressRange = "10.0.0.1/24"
+	network.DisplayName = "mynetwork"
+	_, err := GetNetwork("skynet")
+	if err != nil {
+		CreateNetwork(network)
+	}
+}
+func getNet() models.Network {
+	network, _ := GetNetwork("skynet")
+	return network
+}
+
+func TestGetNetworks(t *testing.T) {
+	//calls functions.ListNetworks --- nothing to be done
+}
+func TestCreateNetwork(t *testing.T) {
+	deleteNet(t)
+	var network models.Network
+	network.NetID = "skynet"
+	network.AddressRange = "10.0.0.1/24"
+	network.DisplayName = "mynetwork"
+	err := CreateNetwork(network)
+	assert.Nil(t, err)
+}
+func TestGetDeleteNetwork(t *testing.T) {
+	createNet()
+	//create nodes
+	t.Run("NetworkwithNodes", func(t *testing.T) {
+	})
+	t.Run("GetExistingNetwork", func(t *testing.T) {
+		network, err := GetNetwork("skynet")
+		assert.Nil(t, err)
+		assert.Equal(t, "skynet", network.NetID)
+	})
+	t.Run("DeleteExistingNetwork", func(t *testing.T) {
+		result, err := DeleteNetwork("skynet")
+		assert.Nil(t, err)
+		assert.Equal(t, int64(1), result.DeletedCount)
+		t.Log(result.DeletedCount)
+	})
+	t.Run("GetNonExistantNetwork", func(t *testing.T) {
+		network, err := GetNetwork("skynet")
+		assert.NotNil(t, err)
+		assert.Equal(t, "mongo: no documents in result", err.Error())
+		assert.Equal(t, "", network.NetID)
+	})
+	t.Run("NonExistantNetwork", func(t *testing.T) {
+		result, err := DeleteNetwork("skynet")
+		assert.Nil(t, err)
+		assert.Equal(t, int64(0), result.DeletedCount)
+		t.Log(result.DeletedCount)
+	})
+}
+func TestGetNetwork(t *testing.T) {
+	createNet()
+	t.Run("NoNetwork", func(t *testing.T) {
+		network, err := GetNetwork("badnet")
+		assert.NotNil(t, err)
+		assert.Equal(t, "mongo: no documents in result", err.Error())
+		assert.Equal(t, models.Network{}, network)
+	})
+	t.Run("Valid", func(t *testing.T) {
+		network, err := GetNetwork("skynet")
+		assert.Nil(t, err)
+		assert.Equal(t, "skynet", network.NetID)
+	})
+}
+func TestUpdateNetwork(t *testing.T) {
+	createNet()
+	network := getNet()
+	t.Run("NetID", func(t *testing.T) {
+		var networkupdate models.NetworkUpdate
+		networkupdate.NetID = "wirecat"
+		_, err := UpdateNetwork(networkupdate, network)
+		assert.NotNil(t, err)
+		assert.Equal(t, "NetID is not editable", err.Error())
+	})
+	t.Run("LocalRange", func(t *testing.T) {
+		var networkupdate models.NetworkUpdate
+		//NetID needs to be set as it will be in updateNetwork
+		networkupdate.NetID = "skynet"
+		networkupdate.LocalRange = "192.168.0.1/24"
+		update, err := UpdateNetwork(networkupdate, network)
+		assert.Nil(t, err)
+		t.Log(err, update)
+	})
+}
+
+func TestKeyUpdate(t *testing.T) {
+	createNet()
+	existing, err := GetNetwork("skynet")
+	assert.Nil(t, err)
+	time.Sleep(time.Second * 1)
+	network, err := KeyUpdate("skynet")
+	assert.Nil(t, err)
+	network, err = GetNetwork("skynet")
+	assert.Nil(t, err)
+	assert.Greater(t, network.KeyUpdateTimeStamp, existing.KeyUpdateTimeStamp)
+}
+
+func TestCreateKey(t *testing.T) {
+	createNet()
+	var accesskey models.AccessKey
+	var network models.Network
+	network.NetID = "skynet"
+	t.Run("InvalidName", func(t *testing.T) {
+		network, err := GetNetwork("skynet")
+		assert.Nil(t, err)
+		accesskey.Name = "bad-name"
+		_, err = CreateAccessKey(accesskey, network)
+		assert.NotNil(t, err)
+		assert.Contains(t, err.Error(), "Field validation for 'Name' failed on the 'alphanum' tag")
+	})
+	t.Run("NameTooLong", func(t *testing.T) {
+		network, err := GetNetwork("skynet")
+		assert.Nil(t, err)
+		accesskey.Name = "Thisisareallylongkeynamethatwillfail"
+		_, err = CreateAccessKey(accesskey, network)
+		assert.NotNil(t, err)
+		assert.Contains(t, err.Error(), "Field validation for 'Name' failed on the 'max' tag")
+	})
+	t.Run("BlankName", func(t *testing.T) {
+		network, err := GetNetwork("skynet")
+		assert.Nil(t, err)
+		accesskey.Name = ""
+		key, err := CreateAccessKey(accesskey, network)
+		assert.Nil(t, err)
+		assert.NotEqual(t, "", key.Name)
+	})
+	t.Run("InvalidValue", func(t *testing.T) {
+		network, err := GetNetwork("skynet")
+		assert.Nil(t, err)
+		accesskey.Value = "bad-value"
+		_, err = CreateAccessKey(accesskey, network)
+		assert.NotNil(t, err)
+		assert.Contains(t, err.Error(), "Field validation for 'Value' failed on the 'alphanum' tag")
+	})
+	t.Run("BlankValue", func(t *testing.T) {
+		network, err := GetNetwork("skynet")
+		assert.Nil(t, err)
+		accesskey.Name = "mykey"
+		accesskey.Value = ""
+		key, err := CreateAccessKey(accesskey, network)
+		assert.Nil(t, err)
+		assert.NotEqual(t, "", key.Value)
+		assert.Equal(t, accesskey.Name, key.Name)
+	})
+	t.Run("ValueTooLong", func(t *testing.T) {
+		network, err := GetNetwork("skynet")
+		assert.Nil(t, err)
+		accesskey.Name = "keyname"
+		accesskey.Value = "AccessKeyValuethatistoolong"
+		_, err = CreateAccessKey(accesskey, network)
+		assert.NotNil(t, err)
+		assert.Contains(t, err.Error(), "Field validation for 'Value' failed on the 'max' tag")
+	})
+	t.Run("BlankUses", func(t *testing.T) {
+		network, err := GetNetwork("skynet")
+		assert.Nil(t, err)
+		accesskey.Uses = 0
+		accesskey.Value = ""
+		key, err := CreateAccessKey(accesskey, network)
+		assert.Nil(t, err)
+		assert.Equal(t, 1, key.Uses)
+	})
+	t.Run("DuplicateKey", func(t *testing.T) {
+		network, err := GetNetwork("skynet")
+		assert.Nil(t, err)
+		accesskey.Name = "mykey"
+		_, err = CreateAccessKey(accesskey, network)
+		assert.NotNil(t, err)
+		assert.Equal(t, "Duplicate AccessKey Name", err.Error())
+	})
+}
+func TestGetKeys(t *testing.T) {
+	deleteNet(t)
+	createNet()
+	network, err := GetNetwork("skynet")
+	assert.Nil(t, err)
+	var key models.AccessKey
+	key.Name = "mykey"
+	_, err = CreateAccessKey(key, network)
+	assert.Nil(t, err)
+	t.Run("KeyExists", func(t *testing.T) {
+		keys, err := GetKeys(network.NetID)
+		assert.Nil(t, err)
+		assert.NotEqual(t, models.AccessKey{}, keys)
+	})
+	t.Run("NonExistantKey", func(t *testing.T) {
+		err := DeleteKey("mykey", "skynet")
+		assert.Nil(t, err)
+		keys, err := GetKeys(network.NetID)
+		assert.Nil(t, err)
+		assert.Equal(t, []models.AccessKey(nil), keys)
+	})
+}
+func TestDeleteKey(t *testing.T) {
+	createNet()
+	network, err := GetNetwork("skynet")
+	assert.Nil(t, err)
+	var key models.AccessKey
+	key.Name = "mykey"
+	_, err = CreateAccessKey(key, network)
+	assert.Nil(t, err)
+	t.Run("ExistingKey", func(t *testing.T) {
+		err := DeleteKey("mykey", "skynet")
+		assert.Nil(t, err)
+	})
+	t.Run("NonExistantKey", func(t *testing.T) {
+		err := DeleteKey("mykey", "skynet")
+		assert.NotNil(t, err)
+		assert.Equal(t, "key mykey does not exist", err.Error())
+	})
+}
+func TestSecurityCheck(t *testing.T) {
+	t.Run("NoNetwork", func(t *testing.T) {
+		err := SecurityCheck("", "Bearer secretkey")
+		assert.Nil(t, err)
+	})
+	t.Run("WithNetwork", func(t *testing.T) {
+		err := SecurityCheck("skynet", "Bearer secretkey")
+		assert.Nil(t, err)
+	})
+	t.Run("BadNet", func(t *testing.T) {
+		err := SecurityCheck("badnet", "Bearer secretkey")
+		assert.NotNil(t, err)
+		t.Log(err)
+	})
+	t.Run("BadToken", func(t *testing.T) {
+		err := SecurityCheck("skynet", "Bearer badkey")
+		assert.NotNil(t, err)
+		t.Log(err)
+	})
+}
+func TestValidateNetworkUpdate(t *testing.T) {
+	//yes := true
+	//no := false
+	deleteNet(t)
+	//DeleteNetworks
+	cases := []NetworkValidationTestCase{
+		NetworkValidationTestCase{
+			testname: "InvalidAddress",
+			network: models.Network{
+				AddressRange: "10.0.0.256",
+			},
+			errMessage: "Field validation for 'AddressRange' failed on the 'cidr' tag",
+		},
+		NetworkValidationTestCase{
+			testname: "InvalidAddress6",
+			network: models.Network{
+				AddressRange6: "2607::ag",
+			},
+			errMessage: "Field validation for 'AddressRange6' failed on the 'cidr' tag",
+		},
+
+		NetworkValidationTestCase{
+			testname: "BadDisplayName",
+			network: models.Network{
+				DisplayName: "skynet*",
+			},
+			errMessage: "Field validation for 'DisplayName' failed on the 'alphanum' tag",
+		},
+		NetworkValidationTestCase{
+			testname: "DisplayNameTooLong",
+			network: models.Network{
+				DisplayName: "Thisisareallylongdisplaynamethatistoolong",
+			},
+			errMessage: "Field validation for 'DisplayName' failed on the 'max' tag",
+		},
+		NetworkValidationTestCase{
+			testname: "DisplayNameTooShort",
+			network: models.Network{
+				DisplayName: "1",
+			},
+			errMessage: "Field validation for 'DisplayName' failed on the 'min' tag",
+		},
+		NetworkValidationTestCase{
+			testname: "InvalidNetID",
+			network: models.Network{
+				NetID: "contains spaces",
+			},
+			errMessage: "Field validation for 'NetID' failed on the 'alphanum' tag",
+		},
+		NetworkValidationTestCase{
+			testname: "NetIDTooLong",
+			network: models.Network{
+				NetID: "LongNetIDName",
+			},
+			errMessage: "Field validation for 'NetID' failed on the 'max' tag",
+		},
+		NetworkValidationTestCase{
+			testname: "ListenPortTooLow",
+			network: models.Network{
+				DefaultListenPort: 1023,
+			},
+			errMessage: "Field validation for 'DefaultListenPort' failed on the 'min' tag",
+		},
+		NetworkValidationTestCase{
+			testname: "ListenPortTooHigh",
+			network: models.Network{
+				DefaultListenPort: 65536,
+			},
+			errMessage: "Field validation for 'DefaultListenPort' failed on the 'max' tag",
+		},
+		NetworkValidationTestCase{
+			testname: "KeepAliveTooBig",
+			network: models.Network{
+				DefaultKeepalive: 1010,
+			},
+			errMessage: "Field validation for 'DefaultKeepalive' failed on the 'max' tag",
+		},
+		NetworkValidationTestCase{
+			testname: "InvalidLocalRange",
+			network: models.Network{
+				LocalRange: "192.168.0.1",
+			},
+			errMessage: "Field validation for 'LocalRange' failed on the 'cidr' tag",
+		},
+		NetworkValidationTestCase{
+			testname: "CheckInIntervalTooBig",
+			network: models.Network{
+				DefaultCheckInInterval: 100001,
+			},
+			errMessage: "Field validation for 'DefaultCheckInInterval' failed on the 'max' tag",
+		},
+		NetworkValidationTestCase{
+			testname: "CheckInIntervalTooSmall",
+			network: models.Network{
+				DefaultCheckInInterval: 1,
+			},
+			errMessage: "Field validation for 'DefaultCheckInInterval' failed on the 'min' tag",
+		},
+	}
+	for _, tc := range cases {
+		t.Run(tc.testname, func(t *testing.T) {
+			network := models.NetworkUpdate(tc.network)
+			err := ValidateNetworkUpdate(network)
+			assert.NotNil(t, err)
+			assert.Contains(t, err.Error(), tc.errMessage)
+		})
+	}
+}
+func TestValidateNetworkCreate(t *testing.T) {
+	yes := true
+	no := false
+	deleteNet(t)
+	//DeleteNetworks
+	cases := []NetworkValidationTestCase{
+		NetworkValidationTestCase{
+			testname: "InvalidAddress",
+			network: models.Network{
+				AddressRange: "10.0.0.256",
+				NetID:        "skynet",
+				IsDualStack:  &no,
+			},
+			errMessage: "Field validation for 'AddressRange' failed on the 'cidr' tag",
+		},
+		NetworkValidationTestCase{
+			testname: "BadDisplayName",
+			network: models.Network{
+				AddressRange: "10.0.0.1/24",
+				NetID:        "skynet",
+				DisplayName:  "skynet*",
+				IsDualStack:  &no,
+			},
+			errMessage: "Field validation for 'DisplayName' failed on the 'alphanum' tag",
+		},
+		NetworkValidationTestCase{
+			testname: "DisplayNameTooLong",
+			network: models.Network{
+				AddressRange: "10.0.0.1/24",
+				NetID:        "skynet",
+				DisplayName:  "Thisisareallylongdisplaynamethatistoolong",
+				IsDualStack:  &no,
+			},
+			errMessage: "Field validation for 'DisplayName' failed on the 'max' tag",
+		},
+		NetworkValidationTestCase{
+			testname: "DisplayNameTooShort",
+			network: models.Network{
+				AddressRange: "10.0.0.1/24",
+				NetID:        "skynet",
+				DisplayName:  "1",
+				IsDualStack:  &no,
+			},
+			errMessage: "Field validation for 'DisplayName' failed on the 'min' tag",
+		},
+		NetworkValidationTestCase{
+			testname: "NetIDMissing",
+			network: models.Network{
+				AddressRange: "10.0.0.1/24",
+				IsDualStack:  &no,
+			},
+			errMessage: "Field validation for 'NetID' failed on the 'required' tag",
+		},
+		NetworkValidationTestCase{
+			testname: "InvalidNetID",
+			network: models.Network{
+				AddressRange: "10.0.0.1/24",
+				NetID:        "contains spaces",
+				IsDualStack:  &no,
+			},
+			errMessage: "Field validation for 'NetID' failed on the 'alphanum' tag",
+		},
+		NetworkValidationTestCase{
+			testname: "NetIDTooShort",
+			network: models.Network{
+				AddressRange: "10.0.0.1/24",
+				NetID:        "",
+				IsDualStack:  &no,
+			},
+			errMessage: "Field validation for 'NetID' failed on the 'required' tag",
+		},
+		NetworkValidationTestCase{
+			testname: "NetIDTooLong",
+			network: models.Network{
+				AddressRange: "10.0.0.1/24",
+				NetID:        "LongNetIDName",
+				IsDualStack:  &no,
+			},
+			errMessage: "Field validation for 'NetID' failed on the 'max' tag",
+		},
+		NetworkValidationTestCase{
+			testname: "ListenPortTooLow",
+			network: models.Network{
+				AddressRange:      "10.0.0.1/24",
+				NetID:             "skynet",
+				DefaultListenPort: 1023,
+				IsDualStack:       &no,
+			},
+			errMessage: "Field validation for 'DefaultListenPort' failed on the 'min' tag",
+		},
+		NetworkValidationTestCase{
+			testname: "ListenPortTooHigh",
+			network: models.Network{
+				AddressRange:      "10.0.0.1/24",
+				NetID:             "skynet",
+				DefaultListenPort: 65536,
+				IsDualStack:       &no,
+			},
+			errMessage: "Field validation for 'DefaultListenPort' failed on the 'max' tag",
+		},
+		NetworkValidationTestCase{
+			testname: "KeepAliveTooBig",
+			network: models.Network{
+				AddressRange:     "10.0.0.1/24",
+				NetID:            "skynet",
+				DefaultKeepalive: 1010,
+				IsDualStack:      &no,
+			},
+			errMessage: "Field validation for 'DefaultKeepalive' failed on the 'max' tag",
+		},
+		NetworkValidationTestCase{
+			testname: "InvalidLocalRange",
+			network: models.Network{
+				AddressRange: "10.0.0.1/24",
+				NetID:        "skynet",
+				LocalRange:   "192.168.0.1",
+				IsDualStack:  &no,
+			},
+			errMessage: "Field validation for 'LocalRange' failed on the 'cidr' tag",
+		},
+		NetworkValidationTestCase{
+			testname: "DualStackWithoutIPv6",
+			network: models.Network{
+				AddressRange: "10.0.0.1/24",
+				NetID:        "skynet",
+				IsDualStack:  &yes,
+			},
+			errMessage: "Field validation for 'AddressRange6' failed on the 'addressrange6_valid' tag",
+		},
+		NetworkValidationTestCase{
+			testname: "CheckInIntervalTooBig",
+			network: models.Network{
+				AddressRange:           "10.0.0.1/24",
+				NetID:                  "skynet",
+				IsDualStack:            &no,
+				DefaultCheckInInterval: 100001,
+			},
+			errMessage: "Field validation for 'DefaultCheckInInterval' failed on the 'max' tag",
+		},
+		NetworkValidationTestCase{
+			testname: "CheckInIntervalTooSmall",
+			network: models.Network{
+				AddressRange:           "10.0.0.1/24",
+				NetID:                  "skynet",
+				IsDualStack:            &no,
+				DefaultCheckInInterval: 1,
+			},
+			errMessage: "Field validation for 'DefaultCheckInInterval' failed on the 'min' tag",
+		},
+	}
+	for _, tc := range cases {
+		t.Run(tc.testname, func(t *testing.T) {
+			err := ValidateNetworkCreate(tc.network)
+			assert.NotNil(t, err)
+			assert.Contains(t, err.Error(), tc.errMessage)
+		})
+	}
+	t.Run("DuplicateNetID", func(t *testing.T) {
+		deleteNet(t)
+		var net1, net2 models.Network
+		net1.NetID = "skynet"
+		net1.AddressRange = "10.0.0.1/24"
+		net1.DisplayName = "mynetwork"
+		net2.NetID = "skynet"
+		net2.AddressRange = "10.0.1.1/24"
+		net2.IsDualStack = &no
+
+		err := CreateNetwork(net1)
+		assert.Nil(t, err)
+		err = ValidateNetworkCreate(net2)
+		assert.NotNil(t, err)
+		assert.Contains(t, err.Error(), "Field validation for 'NetID' failed on the 'netid_valid' tag")
+	})
+	t.Run("DuplicateDisplayName", func(t *testing.T) {
+		var network models.Network
+		network.NetID = "wirecat"
+		network.AddressRange = "10.0.100.1/24"
+		network.IsDualStack = &no
+		network.DisplayName = "mynetwork"
+		err := ValidateNetworkCreate(network)
+		assert.NotNil(t, err)
+		assert.Contains(t, err.Error(), "Field validation for 'DisplayName' failed on the 'displayname_unique' tag")
+	})
+
+}

+ 296 - 219
controllers/nodeGrpcController.go

@@ -1,11 +1,11 @@
 package controller
 
 import (
-        "context"
+	"context"
 	"fmt"
+	"github.com/gravitl/netmaker/functions"
 	nodepb "github.com/gravitl/netmaker/grpc"
 	"github.com/gravitl/netmaker/models"
-	"github.com/gravitl/netmaker/functions"
 	"github.com/gravitl/netmaker/servercfg"
 	"go.mongodb.org/mongo-driver/mongo"
 	"google.golang.org/grpc/codes"
@@ -15,12 +15,12 @@ import (
 type NodeServiceServer struct {
 	NodeDB *mongo.Collection
 	nodepb.UnimplementedNodeServiceServer
-
 }
+
 func (s *NodeServiceServer) ReadNode(ctx context.Context, req *nodepb.ReadNodeReq) (*nodepb.ReadNodeRes, error) {
 	// convert string id (from proto) to mongoDB ObjectId
 	macaddress := req.GetMacaddress()
-        networkName := req.GetNetwork()
+	networkName := req.GetNetwork()
 	network, _ := functions.GetParentNetwork(networkName)
 
 	node, err := GetNode(macaddress, networkName)
@@ -30,50 +30,100 @@ func (s *NodeServiceServer) ReadNode(ctx context.Context, req *nodepb.ReadNodeRe
 	}
 
 	/*
-	if node == nil {
-		return nil, status.Errorf(codes.NotFound, fmt.Sprintf("Could not find node with Mac Address %s: %v", req.GetMacaddress(), err))
-	}
+		if node == nil {
+			return nil, status.Errorf(codes.NotFound, fmt.Sprintf("Could not find node with Mac Address %s: %v", req.GetMacaddress(), err))
+		}
 	*/
 	// Cast to ReadNodeRes type
 	dualvar := false
-        if network.IsDualStack != nil {
-                dualvar = *network.IsDualStack
-        }
+	if network.IsDualStack != nil {
+		dualvar = *network.IsDualStack
+	}
 	localvar := false
-        if network.IsLocal != nil {
-                localvar = *network.IsLocal
-        }
-
-
+	if network.IsLocal != nil {
+		localvar = *network.IsLocal
+	}
 
 	response := &nodepb.ReadNodeRes{
 		Node: &nodepb.Node{
-			Macaddress: node.MacAddress,
-			Name:    node.Name,
-			Address:  node.Address,
-			Address6:  node.Address6,
-			Endpoint:  node.Endpoint,
-			Password:  node.Password,
-			Nodenetwork:  node.Network,
-			Interface:  node.Interface,
-			Localaddress:  node.LocalAddress,
-			Postdown:  node.PostDown,
-			Postup:  node.PostUp,
-			Checkininterval:  node.CheckInInterval,
-			Dnsoff:  !servercfg.IsDNSMode(),
-			Ispending:  node.IsPending,
-			Publickey:  node.PublicKey,
-			Listenport:  node.ListenPort,
-			Keepalive:  node.PersistentKeepalive,
-                        Islocal:  localvar,
-                        Isdualstack:  dualvar,
-                        Localrange:  network.LocalRange,
-
+			Macaddress:      node.MacAddress,
+			Name:            node.Name,
+			Address:         node.Address,
+			Address6:        node.Address6,
+			Endpoint:        node.Endpoint,
+			Password:        node.Password,
+			Nodenetwork:     node.Network,
+			Interface:       node.Interface,
+			Localaddress:    node.LocalAddress,
+			Postdown:        node.PostDown,
+			Postup:          node.PostUp,
+			Checkininterval: node.CheckInInterval,
+			Dnsoff:          !servercfg.IsDNSMode(),
+			Ispending:       node.IsPending,
+			Isingressgateway:       node.IsIngressGateway,
+			Ingressgatewayrange:       node.IngressGatewayRange,
+			Publickey:       node.PublicKey,
+			Listenport:      node.ListenPort,
+			Keepalive:       node.PersistentKeepalive,
+			Islocal:         localvar,
+			Isdualstack:     dualvar,
+			Localrange:      network.LocalRange,
 		},
 	}
 	return response, nil
 }
+/*
+func (s *NodeServiceServer) GetConn(ctx context.Context, data *nodepb.Client) (*nodepb.Client, error) {
+        // Get the protobuf node type from the protobuf request type
+        // Essentially doing req.Node to access the struct with a nil check
+        // Now we have to convert this into a NodeItem type to convert into BSON
+        clientreq := models.IntClient{
+                // ID:       primitive.NilObjectID,
+                Address:             data.GetAddress(),
+                Address6:            data.GetAddress6(),
+                AccessKey:           data.GetAccesskey(),
+                PublicKey:           data.GetPublickey(),
+                PrivateKey:           data.GetPrivatekey(),
+                ServerPort:          data.GetServerport(),
+                ServerKey:          data.GetServerkey(),
+                ServerWGEndpoint:          data.GetServerwgendpoint(),
+        }
 
+        //Check to see if key is valid
+        //TODO: Triple inefficient!!! This is the third call to the DB we make for networks
+        if servercfg.IsRegisterKeyRequired() {
+		validKey := functions.IsKeyValidGlobal(clientreq.AccessKey)
+		if !validKey {
+			return nil, status.Errorf(
+                                codes.Internal,
+                                fmt.Sprintf("Invalid key, and server does not allow no-key signups"),
+                        )
+		}
+	}
+	client, err := RegisterIntClient(clientreq)
+
+        if err != nil {
+                // return internal gRPC error to be handled later
+                return nil, status.Errorf(
+                        codes.Internal,
+                        fmt.Sprintf("Internal error: %v", err),
+                )
+        }
+        // return the node in a CreateNodeRes type
+        response := &nodepb.Client{
+                        Privatekey:   client.PrivateKey,
+                        Publickey: client.PublicKey,
+                        Accesskey:         client.AccessKey,
+                        Address:      client.Address,
+                        Address6:     client.Address6,
+                        Serverwgendpoint:     client.ServerWGEndpoint,
+                        Serverport:     client.ServerPort,
+                        Serverkey:    client.ServerKey,
+        }
+
+        return response, nil
+}
+*/
 func (s *NodeServiceServer) CreateNode(ctx context.Context, req *nodepb.CreateNodeReq) (*nodepb.CreateNodeRes, error) {
 	// Get the protobuf node type from the protobuf request type
 	// Essentially doing req.Node to access the struct with a nil check
@@ -81,53 +131,49 @@ func (s *NodeServiceServer) CreateNode(ctx context.Context, req *nodepb.CreateNo
 	// Now we have to convert this into a NodeItem type to convert into BSON
 	node := models.Node{
 		// ID:       primitive.NilObjectID,
-                        MacAddress: data.GetMacaddress(),
-                        LocalAddress: data.GetLocaladdress(),
-                        Name:    data.GetName(),
-                        Address:  data.GetAddress(),
-                        Address6:  data.GetAddress6(),
-                        AccessKey:  data.GetAccesskey(),
-                        Endpoint:  data.GetEndpoint(),
-                        PersistentKeepalive:  data.GetKeepalive(),
-                        Password:  data.GetPassword(),
-                        Interface:  data.GetInterface(),
-                        Network:  data.GetNodenetwork(),
-                        IsPending:  data.GetIspending(),
-                        PublicKey:  data.GetPublickey(),
-                        ListenPort:  data.GetListenport(),
+		MacAddress:          data.GetMacaddress(),
+		LocalAddress:        data.GetLocaladdress(),
+		Name:                data.GetName(),
+		Address:             data.GetAddress(),
+		Address6:            data.GetAddress6(),
+		AccessKey:           data.GetAccesskey(),
+		Endpoint:            data.GetEndpoint(),
+		PersistentKeepalive: data.GetKeepalive(),
+		Password:            data.GetPassword(),
+		Interface:           data.GetInterface(),
+		Network:             data.GetNodenetwork(),
+		IsPending:           data.GetIspending(),
+		PublicKey:           data.GetPublickey(),
+		ListenPort:          data.GetListenport(),
 	}
 
-        err := ValidateNodeCreate(node.Network, node)
+	err := ValidateNodeCreate(node.Network, node)
 
-        if err != nil {
-                // return internal gRPC error to be handled later
-                return nil, err
-        }
-
-        //Check to see if key is valid
-        //TODO: Triple inefficient!!! This is the third call to the DB we make for networks
-        validKey := functions.IsKeyValid(node.Network, node.AccessKey)
-        network, err := functions.GetParentNetwork(node.Network)
-        if err != nil {
-                return nil, status.Errorf(codes.NotFound, fmt.Sprintf("Could not find network: %v", err))
-        } else {
-		fmt.Println("Creating node in network " + network.NetID)
+	if err != nil {
+		// return internal gRPC error to be handled later
+		return nil, err
 	}
 
+	//Check to see if key is valid
+	//TODO: Triple inefficient!!! This is the third call to the DB we make for networks
+	validKey := functions.IsKeyValid(node.Network, node.AccessKey)
+	network, err := functions.GetParentNetwork(node.Network)
+	if err != nil {
+		return nil, status.Errorf(codes.NotFound, fmt.Sprintf("Could not find network: %v", err))
+	}
 
-
-        if !validKey {
-                //Check to see if network will allow manual sign up
-                //may want to switch this up with the valid key check and avoid a DB call that way.
-                if *network.AllowManualSignUp {
-                        node.IsPending = true
-                } else  {
-	                return nil, status.Errorf(
-		                codes.Internal,
+	if !validKey {
+		//Check to see if network will allow manual sign up
+		//may want to switch this up with the valid key check and avoid a DB call that way.
+		if *network.AllowManualSignUp {
+			node.IsPending = true
+		} else {
+			return nil, status.Errorf(
+				codes.Internal,
 				fmt.Sprintf("Invalid key, and network does not allow no-key signups"),
 			)
-                }
-        }
+		}
+	}
 
 	node, err = CreateNode(node, node.Network)
 
@@ -138,135 +184,131 @@ func (s *NodeServiceServer) CreateNode(ctx context.Context, req *nodepb.CreateNo
 			fmt.Sprintf("Internal error: %v", err),
 		)
 	}
-        dualvar := false
-        if network.IsDualStack != nil {
-                dualvar = *network.IsDualStack
-        }
-        localvar := false
-        if network.IsLocal != nil {
-                localvar = *network.IsLocal
-        }
+	dualvar := false
+	if network.IsDualStack != nil {
+		dualvar = *network.IsDualStack
+	}
+	localvar := false
+	if network.IsLocal != nil {
+		localvar = *network.IsLocal
+	}
 
 	// return the node in a CreateNodeRes type
 	response := &nodepb.CreateNodeRes{
 		Node: &nodepb.Node{
-                        Macaddress: node.MacAddress,
-                        Localaddress: node.LocalAddress,
-                        Name:    node.Name,
-                        Address:  node.Address,
-                        Address6:  node.Address6,
-                        Endpoint:  node.Endpoint,
-                        Password:  node.Password,
-                        Interface:  node.Interface,
-                        Nodenetwork:  node.Network,
-			Dnsoff:  !servercfg.IsDNSMode(),
-                        Ispending:  node.IsPending,
-                        Publickey:  node.PublicKey,
-                        Listenport:  node.ListenPort,
-                        Keepalive:  node.PersistentKeepalive,
-                        Islocal:  localvar,
-                        Isdualstack:  dualvar,
-                        Localrange:  network.LocalRange,
+			Macaddress:   node.MacAddress,
+			Localaddress: node.LocalAddress,
+			Name:         node.Name,
+			Address:      node.Address,
+			Address6:     node.Address6,
+			Endpoint:     node.Endpoint,
+			Password:     node.Password,
+			Interface:    node.Interface,
+			Nodenetwork:  node.Network,
+			Dnsoff:       !servercfg.IsDNSMode(),
+			Ispending:    node.IsPending,
+			Publickey:    node.PublicKey,
+			Listenport:   node.ListenPort,
+			Keepalive:    node.PersistentKeepalive,
+			Islocal:      localvar,
+			Isdualstack:  dualvar,
+			Localrange:   network.LocalRange,
 		},
 	}
-        err = SetNetworkNodesLastModified(node.Network)
-        if err != nil {
-                return nil, status.Errorf(codes.NotFound, fmt.Sprintf("Could not update network last modified date: %v", err))
-        }
+	err = SetNetworkNodesLastModified(node.Network)
+	if err != nil {
+		return nil, status.Errorf(codes.NotFound, fmt.Sprintf("Could not update network last modified date: %v", err))
+	}
 
 	return response, nil
 }
 
 func (s *NodeServiceServer) CheckIn(ctx context.Context, req *nodepb.CheckInReq) (*nodepb.CheckInRes, error) {
 	// Get the protobuf node type from the protobuf request type
-        // Essentially doing req.Node to access the struct with a nil check
+	// Essentially doing req.Node to access the struct with a nil check
 	data := req.GetNode()
 	//postchanges := req.GetPostchanges()
 	// Now we have to convert this into a NodeItem type to convert into BSON
-        node := models.Node{
-                // ID:       primitive.NilObjectID,
-                        MacAddress: data.GetMacaddress(),
-                        Address:  data.GetAddress(),
-                        Address6:  data.GetAddress6(),
-                        Endpoint:  data.GetEndpoint(),
-                        Network:  data.GetNodenetwork(),
-                        Password:  data.GetPassword(),
-                        LocalAddress:  data.GetLocaladdress(),
-                        ListenPort:  data.GetListenport(),
-                        PersistentKeepalive:  data.GetKeepalive(),
-                        PublicKey:  data.GetPublickey(),
-        }
+	node := models.Node{
+		// ID:       primitive.NilObjectID,
+		MacAddress:          data.GetMacaddress(),
+		Address:             data.GetAddress(),
+		Address6:            data.GetAddress6(),
+		Endpoint:            data.GetEndpoint(),
+		Network:             data.GetNodenetwork(),
+		Password:            data.GetPassword(),
+		LocalAddress:        data.GetLocaladdress(),
+		ListenPort:          data.GetListenport(),
+		PersistentKeepalive: data.GetKeepalive(),
+		PublicKey:           data.GetPublickey(),
+	}
 
 	checkinresponse, err := NodeCheckIn(node, node.Network)
 
-        if err != nil {
-                // return internal gRPC error to be handled later
+	if err != nil {
+		// return internal gRPC error to be handled later
 		if checkinresponse == (models.CheckInResponse{}) || !checkinresponse.IsPending {
-                return nil, status.Errorf(
-                        codes.Internal,
-                        fmt.Sprintf("Internal error: %v", err),
-                )
+			return nil, status.Errorf(
+				codes.Internal,
+				fmt.Sprintf("Internal error: %v", err),
+			)
 		}
-        }
-        // return the node in a CreateNodeRes type
-        response := &nodepb.CheckInRes{
-                Checkinresponse: &nodepb.CheckInResponse{
-                        Success:  checkinresponse.Success,
-                        Needpeerupdate:  checkinresponse.NeedPeerUpdate,
-                        Needdelete:  checkinresponse.NeedDelete,
-                        Needconfigupdate:  checkinresponse.NeedConfigUpdate,
-                        Needkeyupdate:  checkinresponse.NeedKeyUpdate,
-                        Nodemessage:  checkinresponse.NodeMessage,
-                        Ispending:  checkinresponse.IsPending,
-                },
-        }
-        return response, nil
+	}
+	// return the node in a CreateNodeRes type
+	response := &nodepb.CheckInRes{
+		Checkinresponse: &nodepb.CheckInResponse{
+			Success:          checkinresponse.Success,
+			Needpeerupdate:   checkinresponse.NeedPeerUpdate,
+			Needdelete:       checkinresponse.NeedDelete,
+			Needconfigupdate: checkinresponse.NeedConfigUpdate,
+			Needkeyupdate:    checkinresponse.NeedKeyUpdate,
+			Nodemessage:      checkinresponse.NodeMessage,
+			Ispending:        checkinresponse.IsPending,
+		},
+	}
+	return response, nil
 }
 
-
 func (s *NodeServiceServer) UpdateNode(ctx context.Context, req *nodepb.UpdateNodeReq) (*nodepb.UpdateNodeRes, error) {
 	// Get the node data from the request
-        data := req.GetNode()
-        // Now we have to convert this into a NodeItem type to convert into BSON
-        nodechange := models.Node{
-                // ID:       primitive.NilObjectID,
-                        MacAddress: data.GetMacaddress(),
-                        Name:    data.GetName(),
-                        Address:  data.GetAddress(),
-                        Address6:  data.GetAddress6(),
-                        LocalAddress:  data.GetLocaladdress(),
-                        Endpoint:  data.GetEndpoint(),
-                        Password:  data.GetPassword(),
-                        PersistentKeepalive:  data.GetKeepalive(),
-                        Network:  data.GetNodenetwork(),
-                        Interface:  data.GetInterface(),
-                        PostDown:  data.GetPostdown(),
-                        PostUp:  data.GetPostup(),
-                        IsPending:  data.GetIspending(),
-                        PublicKey:  data.GetPublickey(),
-                        ListenPort:  data.GetListenport(),
-        }
-
+	data := req.GetNode()
+	// Now we have to convert this into a NodeItem type to convert into BSON
+	nodechange := models.NodeUpdate{
+		// ID:       primitive.NilObjectID,
+		MacAddress:          data.GetMacaddress(),
+		Name:                data.GetName(),
+		Address:             data.GetAddress(),
+		Address6:            data.GetAddress6(),
+		LocalAddress:        data.GetLocaladdress(),
+		Endpoint:            data.GetEndpoint(),
+		Password:            data.GetPassword(),
+		PersistentKeepalive: data.GetKeepalive(),
+		Network:             data.GetNodenetwork(),
+		Interface:           data.GetInterface(),
+		PostDown:            data.GetPostdown(),
+		PostUp:              data.GetPostup(),
+		IsPending:           data.GetIspending(),
+		PublicKey:           data.GetPublickey(),
+		ListenPort:          data.GetListenport(),
+	}
 
 	// Convert the Id string to a MongoDB ObjectId
 	macaddress := nodechange.MacAddress
 	networkName := nodechange.Network
-        network, _ := functions.GetParentNetwork(networkName)
-
+	network, _ := functions.GetParentNetwork(networkName)
 
 	err := ValidateNodeUpdate(networkName, nodechange)
-        if err != nil {
-                return nil, err
-        }
-
-        node, err := functions.GetNodeByMacAddress(networkName, macaddress)
-        if err != nil {
-               return nil, status.Errorf(
-                        codes.NotFound,
-                        fmt.Sprintf("Could not find node with supplied Mac Address: %v", err),
-                )
+	if err != nil {
+		return nil, err
 	}
 
+	node, err := functions.GetNodeByMacAddress(networkName, macaddress)
+	if err != nil {
+		return nil, status.Errorf(
+			codes.NotFound,
+			fmt.Sprintf("Could not find node with supplied Mac Address: %v", err),
+		)
+	}
 
 	newnode, err := UpdateNode(nodechange, node)
 
@@ -276,43 +318,41 @@ func (s *NodeServiceServer) UpdateNode(ctx context.Context, req *nodepb.UpdateNo
 			fmt.Sprintf("Could not find node with supplied Mac Address: %v", err),
 		)
 	}
-        dualvar := false
-        if network.IsDualStack != nil {
-                dualvar = *network.IsDualStack
-        }
-        localvar := false
-        if network.IsLocal != nil {
-                localvar = *network.IsLocal
-        }
+	dualvar := false
+	if network.IsDualStack != nil {
+		dualvar = *network.IsDualStack
+	}
+	localvar := false
+	if network.IsLocal != nil {
+		localvar = *network.IsLocal
+	}
 
 	return &nodepb.UpdateNodeRes{
 		Node: &nodepb.Node{
-                        Macaddress: newnode.MacAddress,
-                        Localaddress: newnode.LocalAddress,
-                        Name:    newnode.Name,
-                        Address:  newnode.Address,
-                        Address6:  newnode.Address6,
-                        Endpoint:  newnode.Endpoint,
-                        Password:  newnode.Password,
-                        Interface:  newnode.Interface,
-                        Postdown:  newnode.PostDown,
-                        Postup:  newnode.PostUp,
-                        Nodenetwork:  newnode.Network,
-                        Ispending:  newnode.IsPending,
-                        Publickey:  newnode.PublicKey,
-			Dnsoff:  !servercfg.IsDNSMode(),
-                        Listenport:  newnode.ListenPort,
-                        Keepalive:  newnode.PersistentKeepalive,
-                        Islocal:  localvar,
-                        Isdualstack: dualvar,
-                        Localrange:  network.LocalRange,
-
+			Macaddress:   newnode.MacAddress,
+			Localaddress: newnode.LocalAddress,
+			Name:         newnode.Name,
+			Address:      newnode.Address,
+			Address6:     newnode.Address6,
+			Endpoint:     newnode.Endpoint,
+			Password:     newnode.Password,
+			Interface:    newnode.Interface,
+			Postdown:     newnode.PostDown,
+			Postup:       newnode.PostUp,
+			Nodenetwork:  newnode.Network,
+			Ispending:    newnode.IsPending,
+			Publickey:    newnode.PublicKey,
+			Dnsoff:       !servercfg.IsDNSMode(),
+			Listenport:   newnode.ListenPort,
+			Keepalive:    newnode.PersistentKeepalive,
+			Islocal:      localvar,
+			Isdualstack:  dualvar,
+			Localrange:   network.LocalRange,
 		},
 	}, nil
 }
 
 func (s *NodeServiceServer) DeleteNode(ctx context.Context, req *nodepb.DeleteNodeReq) (*nodepb.DeleteNodeRes, error) {
-	fmt.Println("beginning node delete")
 	macaddress := req.GetMacaddress()
 	network := req.GetNetworkName()
 
@@ -326,12 +366,11 @@ func (s *NodeServiceServer) DeleteNode(ctx context.Context, req *nodepb.DeleteNo
 
 	fmt.Println("updating network last modified of " + req.GetNetworkName())
 	err = SetNetworkNodesLastModified(req.GetNetworkName())
-        if err != nil {
+	if err != nil {
 		fmt.Println("Error updating Network")
 		fmt.Println(err)
 		return nil, status.Errorf(codes.NotFound, fmt.Sprintf("Could not update network last modified date: %v", err))
-        }
-
+	}
 
 	return &nodepb.DeleteNodeRes{
 		Success: true,
@@ -349,35 +388,73 @@ func (s *NodeServiceServer) GetPeers(req *nodepb.GetPeersReq, stream nodepb.Node
 		return status.Errorf(codes.Internal, fmt.Sprintf("Unknown internal error: %v", err))
 	}
 	// cursor.Next() returns a boolean, if false there are no more items and loop will break
-        for i := 0; i < len(peers); i++ {
+	for i := 0; i < len(peers); i++ {
 
 		// If no error is found send node over stream
 		stream.Send(&nodepb.GetPeersRes{
 			Peers: &nodepb.PeersResponse{
-                            Address:  peers[i].Address,
-                            Address6:  peers[i].Address6,
-                            Endpoint:  peers[i].Endpoint,
-                            Gatewayrange:  peers[i].GatewayRange,
-                            Isgateway:  peers[i].IsGateway,
-                            Publickey:  peers[i].PublicKey,
-                            Keepalive:  peers[i].KeepAlive,
-                            Listenport:  peers[i].ListenPort,
-                            Localaddress:  peers[i].LocalAddress,
+				Address:      peers[i].Address,
+				Address6:     peers[i].Address6,
+				Endpoint:     peers[i].Endpoint,
+				Egressgatewayrange: peers[i].EgressGatewayRange,
+				Isegressgateway:    peers[i].IsEgressGateway,
+				Publickey:    peers[i].PublicKey,
+				Keepalive:    peers[i].KeepAlive,
+				Listenport:   peers[i].ListenPort,
+				Localaddress: peers[i].LocalAddress,
 			},
 		})
 	}
 
 	node, err := functions.GetNodeByMacAddress(req.GetNetwork(), req.GetMacaddress())
-       if err != nil {
-                return status.Errorf(codes.Internal, fmt.Sprintf("Could not get node: %v", err))
+	if err != nil {
+		return status.Errorf(codes.Internal, fmt.Sprintf("Could not get node: %v", err))
+	}
+
+	err = TimestampNode(node, false, true, false)
+	if err != nil {
+		return status.Errorf(codes.Internal, fmt.Sprintf("Internal error occurred: %v", err))
+	}
+
+	return nil
+}
+
+func (s *NodeServiceServer) GetExtPeers(req *nodepb.GetExtPeersReq, stream nodepb.NodeService_GetExtPeersServer) error {
+        // Initiate a NodeItem type to write decoded data to
+        //data := &models.PeersResponse{}
+        // collection.Find returns a cursor for our (empty) query
+        //cursor, err := s.NodeDB.Find(context.Background(), bson.M{})
+        peers, err := GetExtPeersList(req.GetNetwork(), req.GetMacaddress())
+
+        if err != nil {
+                return status.Errorf(codes.Internal, fmt.Sprintf("Unknown internal error: %v", err))
         }
+        // cursor.Next() returns a boolean, if false there are no more items and loop will break
+        for i := 0; i < len(peers); i++ {
 
+                // If no error is found send node over stream
+                stream.Send(&nodepb.GetExtPeersRes{
+                        Extpeers: &nodepb.ExtPeersResponse{
+                                Address:      peers[i].Address,
+                                Address6:     peers[i].Address6,
+                                Endpoint:     peers[i].Endpoint,
+                                Publickey:    peers[i].PublicKey,
+                                Keepalive:    peers[i].KeepAlive,
+                                Listenport:   peers[i].ListenPort,
+                                Localaddress: peers[i].LocalAddress,
+                        },
+                })
+        }
 
-	err = TimestampNode(node, false, true, false)
+        node, err := functions.GetNodeByMacAddress(req.GetNetwork(), req.GetMacaddress())
         if err != nil {
-                return status.Errorf(codes.Internal, fmt.Sprintf("Internal error occurred: %v", err))
+                return status.Errorf(codes.Internal, fmt.Sprintf("Could not get node: %v", err))
         }
 
+        err = TimestampNode(node, false, true, false)
+        if err != nil {
+                return status.Errorf(codes.Internal, fmt.Sprintf("Internal error occurred: %v", err))
+        }
 
-	return nil
+        return nil
 }

+ 273 - 193
controllers/nodeHttpController.go

@@ -8,7 +8,7 @@ import (
 	"net/http"
 	"strings"
 	"time"
-
+	"log"
 	"github.com/gorilla/mux"
 	"github.com/gravitl/netmaker/functions"
 	"github.com/gravitl/netmaker/models"
@@ -26,8 +26,10 @@ func nodeHandlers(r *mux.Router) {
 	r.HandleFunc("/api/nodes/{network}/{macaddress}", authorize(true, "node", http.HandlerFunc(updateNode))).Methods("PUT")
 	r.HandleFunc("/api/nodes/{network}/{macaddress}", authorize(true, "node", http.HandlerFunc(deleteNode))).Methods("DELETE")
 	r.HandleFunc("/api/nodes/{network}/{macaddress}/checkin", authorize(true, "node", http.HandlerFunc(checkIn))).Methods("POST")
-	r.HandleFunc("/api/nodes/{network}/{macaddress}/creategateway", authorize(true, "master", http.HandlerFunc(createGateway))).Methods("POST")
-	r.HandleFunc("/api/nodes/{network}/{macaddress}/deletegateway", authorize(true, "master", http.HandlerFunc(deleteGateway))).Methods("DELETE")
+	r.HandleFunc("/api/nodes/{network}/{macaddress}/creategateway", authorize(true, "master", http.HandlerFunc(createEgressGateway))).Methods("POST")
+	r.HandleFunc("/api/nodes/{network}/{macaddress}/deletegateway", authorize(true, "master", http.HandlerFunc(deleteEgressGateway))).Methods("DELETE")
+	r.HandleFunc("/api/nodes/{network}/{macaddress}/createingress", securityCheck(http.HandlerFunc(createIngressGateway))).Methods("POST")
+	r.HandleFunc("/api/nodes/{network}/{macaddress}/deleteingress", securityCheck(http.HandlerFunc(deleteIngressGateway))).Methods("DELETE")
 	r.HandleFunc("/api/nodes/{network}/{macaddress}/approve", authorize(true, "master", http.HandlerFunc(uncordonNode))).Methods("POST")
 	r.HandleFunc("/api/nodes/{network}", createNode).Methods("POST")
 	r.HandleFunc("/api/nodes/adm/{network}/lastmodified", authorize(true, "network", http.HandlerFunc(getLastModified))).Methods("GET")
@@ -181,17 +183,24 @@ func authorize(networkCheck bool, authNetwork string, next http.Handler) http.Ha
 			//A: the token is the master password
 			//B: the token corresponds to a mac address, and if so, which one
 			//TODO: There's probably a better way of dealing with the "master token"/master password. Plz Halp.
-			macaddress, _, err := functions.VerifyToken(authToken)
-			if err != nil {
-				errorResponse = models.ErrorResponse{
-					Code: http.StatusUnauthorized, Message: "W1R3: Error Verifying Auth Token.",
+
+                        var isAuthorized = false
+			var macaddress = ""
+                        _, isadmin, errN := functions.VerifyUserToken(authToken)
+                        if errN == nil && isadmin {
+	                        macaddress = "mastermac"
+                                isAuthorized = true
+			} else {
+				mac, _, err := functions.VerifyToken(authToken)
+				if err != nil {
+					errorResponse = models.ErrorResponse{
+						Code: http.StatusUnauthorized, Message: "W1R3: Error Verifying Auth Token.",
+					}
+					returnErrorResponse(w, r, errorResponse)
+					return
 				}
-				returnErrorResponse(w, r, errorResponse)
-				return
+				macaddress = mac
 			}
-
-			var isAuthorized = false
-
 			//The mastermac (login with masterkey from config) can do everything!! May be dangerous.
 			if macaddress == "mastermac" {
 				isAuthorized = true
@@ -241,99 +250,63 @@ func getNetworkNodes(w http.ResponseWriter, r *http.Request) {
 
 	w.Header().Set("Content-Type", "application/json")
 
-	var nodes []models.ReturnNode
+	var nodes []models.Node
 	var params = mux.Vars(r)
+	nodes, err := GetNetworkNodes(params["network"])
+	if err != nil {
+		returnErrorResponse(w, r, formatError(err, "internal"))
+		return
+	}
 
-	collection := mongoconn.Client.Database("netmaker").Collection("nodes")
+	//Returns all the nodes in JSON format
+	w.WriteHeader(http.StatusOK)
+	json.NewEncoder(w).Encode(nodes)
+}
 
+func GetNetworkNodes(network string) ([]models.Node, error) {
+	var nodes []models.Node
+	collection := mongoconn.Client.Database("netmaker").Collection("nodes")
 	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
-
-	filter := bson.M{"network": params["network"]}
-
+	filter := bson.M{"network": network}
 	//Filtering out the ID field cuz Dillon doesn't like it. May want to filter out other fields in the future
 	cur, err := collection.Find(ctx, filter, options.Find().SetProjection(bson.M{"_id": 0}))
-
 	if err != nil {
-		returnErrorResponse(w, r, formatError(err, "internal"))
-		return
+		return []models.Node{}, err
 	}
-
 	defer cancel()
-
 	for cur.Next(context.TODO()) {
-
-		//Using a different model for the ReturnNode (other than regular node).
+		//Using a different model for the Node (other than regular node).
 		//Either we should do this for ALL structs (so Networks and Keys)
 		//OR we should just use the original struct
 		//My preference is to make some new return structs
 		//TODO: Think about this. Not an immediate concern. Just need to get some consistency eventually
-		var node models.ReturnNode
-
+		var node models.Node
 		err := cur.Decode(&node)
 		if err != nil {
-			returnErrorResponse(w, r, formatError(err, "internal"))
-			return
+			return []models.Node{}, err
 		}
-
 		// add item our array of nodes
 		nodes = append(nodes, node)
 	}
-
 	//TODO: Another fatal error we should take care of.
 	if err := cur.Err(); err != nil {
-		returnErrorResponse(w, r, formatError(err, "internal"))
-		return
+		return []models.Node{}, err
 	}
-
-	//Returns all the nodes in JSON format
-	w.WriteHeader(http.StatusOK)
-	json.NewEncoder(w).Encode(nodes)
-
+	return nodes, nil
 }
 
 //A separate function to get all nodes, not just nodes for a particular network.
 //Not quite sure if this is necessary. Probably necessary based on front end but may want to review after iteration 1 if it's being used or not
 func getAllNodes(w http.ResponseWriter, r *http.Request) {
-
 	w.Header().Set("Content-Type", "application/json")
-
-	var nodes []models.ReturnNode
-
-	collection := mongoconn.Client.Database("netmaker").Collection("nodes")
-
-	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
-
-	// Filter out them ID's again
-	cur, err := collection.Find(ctx, bson.M{}, options.Find().SetProjection(bson.M{"_id": 0}))
+	nodes, err := functions.GetAllNodes()
 	if err != nil {
 		returnErrorResponse(w, r, formatError(err, "internal"))
 		return
 	}
-
-	defer cancel()
-
-	for cur.Next(context.TODO()) {
-
-		var node models.ReturnNode
-		err := cur.Decode(&node)
-		if err != nil {
-			returnErrorResponse(w, r, formatError(err, "internal"))
-			return
-		}
-		// add node to our array
-		nodes = append(nodes, node)
-	}
-
-	//TODO: Fatal error
-	if err := cur.Err(); err != nil {
-		returnErrorResponse(w, r, formatError(err, "internal"))
-		return
-	}
-
 	//Return all the nodes in JSON format
 	w.WriteHeader(http.StatusOK)
 	json.NewEncoder(w).Encode(nodes)
-
 }
 
 //This function get's called when a node "checks in" at check in interval
@@ -353,49 +326,43 @@ func checkIn(w http.ResponseWriter, r *http.Request) {
 	w.Header().Set("Content-Type", "application/json")
 
 	var params = mux.Vars(r)
-
+	node, err := CheckIn(params["network"], params["macaddress"])
+	if err != nil {
+		returnErrorResponse(w, r, formatError(err, "internal"))
+		return
+	}
+	w.WriteHeader(http.StatusOK)
+	json.NewEncoder(w).Encode(node)
+}
+func CheckIn(network, macaddress string) (models.Node, error) {
 	var node models.Node
 
 	//Retrieves node with DB Call which is inefficient. Let's just get the time and set it.
 	//node = functions.GetNodeByMacAddress(params["network"], params["macaddress"])
-
 	collection := mongoconn.Client.Database("netmaker").Collection("nodes")
-
 	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
-
-	filter := bson.M{"macaddress": params["macaddress"], "network": params["network"]}
-
+	filter := bson.M{"macaddress": macaddress, "network": network}
 	//old code was inefficient, this is all we need.
 	time := time.Now().Unix()
-
 	//node.SetLastCheckIn()
-
 	// prepare update model with new time
 	update := bson.D{
 		{"$set", bson.D{
 			{"lastcheckin", time},
 		}},
 	}
-
 	err := collection.FindOneAndUpdate(ctx, filter, update).Decode(&node)
-
 	defer cancel()
-
 	if err != nil {
-		returnErrorResponse(w, r, formatError(err, "internal"))
-		return
+		return models.Node{}, err
 	}
-
 	//TODO: check node last modified vs network last modified
 	//Get Updated node to return
-	node, err = GetNode(params["macaddress"], params["network"])
+	node, err = GetNode(macaddress, network)
 	if err != nil {
-		returnErrorResponse(w, r, formatError(err, "internal"))
-		return
+		return models.Node{}, err
 	}
-	w.WriteHeader(http.StatusOK)
-	json.NewEncoder(w).Encode(node)
-
+	return node, nil
 }
 
 //Get an individual node. Nothin fancy here folks.
@@ -422,26 +389,28 @@ func getLastModified(w http.ResponseWriter, r *http.Request) {
 	// set header.
 	w.Header().Set("Content-Type", "application/json")
 
-	var network models.Network
 	var params = mux.Vars(r)
-
-	collection := mongoconn.Client.Database("netmaker").Collection("networks")
-
-	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
-
-	filter := bson.M{"netid": params["network"]}
-	err := collection.FindOne(ctx, filter).Decode(&network)
-
-	defer cancel()
-
+	network, err := GetLastModified(params["network"])
 	if err != nil {
 		returnErrorResponse(w, r, formatError(err, "internal"))
 		return
 	}
-
 	w.WriteHeader(http.StatusOK)
-	w.Write([]byte(string(network.NodesLastModified)))
+	json.NewEncoder(w).Encode(network.NodesLastModified)
+}
 
+func GetLastModified(network string) (models.Network, error) {
+	var net models.Network
+	collection := mongoconn.Client.Database("netmaker").Collection("networks")
+	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+	filter := bson.M{"netid": network}
+	err := collection.FindOne(ctx, filter).Decode(&net)
+	defer cancel()
+	if err != nil {
+		fmt.Println(err)
+		return models.Network{}, err
+	}
+	return net, nil
 }
 
 //This one's a doozy
@@ -527,57 +496,47 @@ func createNode(w http.ResponseWriter, r *http.Request) {
 //Takes node out of pending state
 //TODO: May want to use cordon/uncordon terminology instead of "ispending".
 func uncordonNode(w http.ResponseWriter, r *http.Request) {
-	w.Header().Set("Content-Type", "application/json")
-
 	var params = mux.Vars(r)
-
-	var node models.Node
-
-	node, err := functions.GetNodeByMacAddress(params["network"], params["macaddress"])
+	w.Header().Set("Content-Type", "application/json")
+	node, err := UncordonNode(params["network"], params["macaddress"])
 	if err != nil {
 		returnErrorResponse(w, r, formatError(err, "internal"))
 		return
 	}
+	fmt.Println("Node " + node.Name + " uncordoned.")
+	w.WriteHeader(http.StatusOK)
+	json.NewEncoder(w).Encode("SUCCESS")
+}
 
+func UncordonNode(network, macaddress string) (models.Node, error) {
+	node, err := functions.GetNodeByMacAddress(network, macaddress)
+	if err != nil {
+		return models.Node{}, err
+	}
 	collection := mongoconn.Client.Database("netmaker").Collection("nodes")
-
 	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
-
 	// Create filter
-	filter := bson.M{"macaddress": params["macaddress"], "network": params["network"]}
-
+	filter := bson.M{"macaddress": macaddress, "network": network}
 	node.SetLastModified()
-
 	fmt.Println("Uncordoning node " + node.Name)
-
 	// prepare update model.
 	update := bson.D{
 		{"$set", bson.D{
 			{"ispending", false},
 		}},
 	}
-
 	err = collection.FindOneAndUpdate(ctx, filter, update).Decode(&node)
-
 	defer cancel()
-
 	if err != nil {
-		returnErrorResponse(w, r, formatError(err, "internal"))
-		return
+		return models.Node{}, err
 	}
-
-	fmt.Println("Node " + node.Name + " uncordoned.")
-	w.WriteHeader(http.StatusOK)
-	json.NewEncoder(w).Encode("SUCCESS")
+	return node, nil
 }
 
-func createGateway(w http.ResponseWriter, r *http.Request) {
-	w.Header().Set("Content-Type", "application/json")
-
+func createEgressGateway(w http.ResponseWriter, r *http.Request) {
+	var gateway models.EgressGatewayRequest
 	var params = mux.Vars(r)
-
-	var gateway models.GatewayRequest
-
+	w.Header().Set("Content-Type", "application/json")
 	err := json.NewDecoder(r.Body).Decode(&gateway)
 	if err != nil {
 		returnErrorResponse(w, r, formatError(err, "internal"))
@@ -585,81 +544,83 @@ func createGateway(w http.ResponseWriter, r *http.Request) {
 	}
 	gateway.NetID = params["network"]
 	gateway.NodeID = params["macaddress"]
-
-	node, err := functions.GetNodeByMacAddress(params["network"], params["macaddress"])
+	node, err := CreateEgressGateway(gateway)
 	if err != nil {
 		returnErrorResponse(w, r, formatError(err, "internal"))
 		return
 	}
+	w.WriteHeader(http.StatusOK)
+	json.NewEncoder(w).Encode(node)
+}
 
-	err = validateGateway(gateway)
+func CreateEgressGateway(gateway models.EgressGatewayRequest) (models.Node, error) {
+	node, err := functions.GetNodeByMacAddress(gateway.NetID, gateway.NodeID)
 	if err != nil {
-		returnErrorResponse(w, r, formatError(err, "internal"))
-		return
+		return models.Node{}, err
+	}
+	err = ValidateEgressGateway(gateway)
+	if err != nil {
+		return models.Node{}, err
 	}
-
 	var nodechange models.Node
-
-	nodechange.IsGateway = true
-	nodechange.GatewayRange = gateway.RangeString
-	if gateway.PostUp == "" {
-		nodechange.PostUp = "iptables -A FORWARD -i " + node.Interface + " -j ACCEPT; iptables -t nat -A POSTROUTING -o " + gateway.Interface + " -j MASQUERADE"
-	} else {
+	nodechange.IsEgressGateway = true
+	nodechange.EgressGatewayRange = gateway.RangeString
+	nodechange.PostUp = "iptables -A FORWARD -i " + node.Interface + " -j ACCEPT; iptables -t nat -A POSTROUTING -o " + gateway.Interface + " -j MASQUERADE"
+	nodechange.PostDown = "iptables -D FORWARD -i " + node.Interface + " -j ACCEPT; iptables -t nat -D POSTROUTING -o " + gateway.Interface + " -j MASQUERADE"
+	if gateway.PostUp != "" {
 		nodechange.PostUp = gateway.PostUp
 	}
-	if gateway.PostDown == "" {
-		nodechange.PostDown = "iptables -D FORWARD -i " + node.Interface + " -j ACCEPT; iptables -t nat -D POSTROUTING -o " + gateway.Interface + " -j MASQUERADE"
-	} else {
+	if gateway.PostDown != "" {
 		nodechange.PostDown = gateway.PostDown
 	}
-
+	if node.PostUp != "" {
+		if !strings.Contains(node.PostUp, nodechange.PostUp) {
+			nodechange.PostUp = node.PostUp + "; " + nodechange.PostUp
+		} else {
+			nodechange.PostUp = node.PostUp
+		}
+	}
+        if node.PostDown != "" {
+                if !strings.Contains(node.PostDown, nodechange.PostDown) {
+                        nodechange.PostDown = node.PostDown + "; " + nodechange.PostDown
+                } else {
+                        nodechange.PostDown = node.PostDown
+                }
+        }
 	collection := mongoconn.Client.Database("netmaker").Collection("nodes")
-
 	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
-
 	// Create filter
-	filter := bson.M{"macaddress": params["macaddress"], "network": params["network"]}
-
+	filter := bson.M{"macaddress": gateway.NodeID, "network": gateway.NetID}
 	nodechange.SetLastModified()
-
 	// prepare update model.
 	update := bson.D{
 		{"$set", bson.D{
 			{"postup", nodechange.PostUp},
 			{"postdown", nodechange.PostDown},
-			{"isgateway", nodechange.IsGateway},
-			{"gatewayrange", nodechange.GatewayRange},
+			{"isegressgateway", nodechange.IsEgressGateway},
+			{"egressgatewayrange", nodechange.EgressGatewayRange},
 			{"lastmodified", nodechange.LastModified},
 		}},
 	}
 	var nodeupdate models.Node
-
 	err = collection.FindOneAndUpdate(ctx, filter, update).Decode(&nodeupdate)
-
 	defer cancel()
-
 	if err != nil {
-		returnErrorResponse(w, r, formatError(err, "internal"))
-		return
+		return models.Node{}, err
 	}
-
-	err = SetNetworkNodesLastModified(params["network"])
+	err = SetNetworkNodesLastModified(gateway.NetID)
 	if err != nil {
-		returnErrorResponse(w, r, formatError(err, "internal"))
-		return
+		return models.Node{}, err
 	}
-
 	//Get updated values to return
-	node, err = functions.GetNodeByMacAddress(params["network"], params["macaddress"])
+	node, err = functions.GetNodeByMacAddress(gateway.NetID, gateway.NodeID)
 	if err != nil {
-		returnErrorResponse(w, r, formatError(err, "internal"))
-		return
+		return models.Node{}, err
 	}
-	w.WriteHeader(http.StatusOK)
-	json.NewEncoder(w).Encode(node)
+	return node, nil
 }
 
-func validateGateway(gateway models.GatewayRequest) error {
+func ValidateEgressGateway(gateway models.EgressGatewayRequest) error {
 	var err error
 	isIp := functions.IsIpCIDR(gateway.RangeString)
 	empty := gateway.RangeString == ""
@@ -673,71 +634,190 @@ func validateGateway(gateway models.GatewayRequest) error {
 	return err
 }
 
-func deleteGateway(w http.ResponseWriter, r *http.Request) {
+func deleteEgressGateway(w http.ResponseWriter, r *http.Request) {
 	w.Header().Set("Content-Type", "application/json")
-
 	var params = mux.Vars(r)
-
-	node, err := functions.GetNodeByMacAddress(params["network"], params["macaddress"])
+	node, err := DeleteEgressGateway(params["network"], params["macaddress"])
 	if err != nil {
 		returnErrorResponse(w, r, formatError(err, "internal"))
 		return
 	}
+	w.WriteHeader(http.StatusOK)
+	json.NewEncoder(w).Encode(node)
+}
 
+func DeleteEgressGateway(network, macaddress string) (models.Node, error) {
+
+	var nodeupdate models.Node
 	var nodechange models.Node
+	node, err := functions.GetNodeByMacAddress(network, macaddress)
+	if err != nil {
+		return models.Node{}, err
+	}
 
-	nodechange.IsGateway = false
-	nodechange.GatewayRange = ""
+	nodechange.IsEgressGateway = false
+	nodechange.EgressGatewayRange = ""
 	nodechange.PostUp = ""
 	nodechange.PostDown = ""
 
 	collection := mongoconn.Client.Database("netmaker").Collection("nodes")
-
 	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
-
 	// Create filter
-	filter := bson.M{"macaddress": params["macaddress"], "network": params["network"]}
-
+	filter := bson.M{"macaddress": macaddress, "network": network}
 	nodechange.SetLastModified()
-
 	// prepare update model.
 	update := bson.D{
 		{"$set", bson.D{
 			{"postup", nodechange.PostUp},
 			{"postdown", nodechange.PostDown},
-			{"isgateway", nodechange.IsGateway},
-			{"gatewayrange", nodechange.GatewayRange},
+			{"isegressgateway", nodechange.IsEgressGateway},
+			{"egressgatewayrange", nodechange.EgressGatewayRange},
 			{"lastmodified", nodechange.LastModified},
 		}},
 	}
-	var nodeupdate models.Node
-
 	err = collection.FindOneAndUpdate(ctx, filter, update).Decode(&nodeupdate)
-
 	defer cancel()
-
 	if err != nil {
-		returnErrorResponse(w, r, formatError(err, "internal"))
-		return
+		return models.Node{}, err
 	}
-
-	err = SetNetworkNodesLastModified(params["network"])
+	err = SetNetworkNodesLastModified(network)
+	if err != nil {
+		return models.Node{}, err
+	}
+	//Get updated values to return
+	node, err = functions.GetNodeByMacAddress(network, macaddress)
+	if err != nil {
+		return models.Node{}, err
+	}
+	return node, nil
+}
+// == INGRESS ==
+func createIngressGateway(w http.ResponseWriter, r *http.Request) {
+	var params = mux.Vars(r)
+	w.Header().Set("Content-Type", "application/json")
+	node, err := CreateIngressGateway(params["network"], params["macaddress"])
 	if err != nil {
 		returnErrorResponse(w, r, formatError(err, "internal"))
 		return
 	}
+	w.WriteHeader(http.StatusOK)
+	json.NewEncoder(w).Encode(node)
+}
 
-	//Get updated values to return
-	node, err = functions.GetNodeByMacAddress(params["network"], params["macaddress"])
+func CreateIngressGateway(netid string, macaddress string) (models.Node, error) {
+
+	node, err := functions.GetNodeByMacAddress(netid, macaddress)
+        if err != nil {
+                return models.Node{}, err
+        }
+
+        network, err := functions.GetParentNetwork(netid)
+        if err != nil {
+		log.Println("Could not find network.")
+                return models.Node{}, err
+        }
+        var nodechange models.Node
+	nodechange.IngressGatewayRange = network.AddressRange
+        nodechange.PostUp = "iptables -A FORWARD -i " + node.Interface + " -j ACCEPT; iptables -t nat -A POSTROUTING -o " + node.Interface + " -j MASQUERADE"
+        nodechange.PostDown = "iptables -D FORWARD -i " + node.Interface + " -j ACCEPT; iptables -t nat -D POSTROUTING -o " + node.Interface + " -j MASQUERADE"
+        if node.PostUp != "" {
+                if !strings.Contains(node.PostUp, nodechange.PostUp) {
+                        nodechange.PostUp = node.PostUp + "; " + nodechange.PostUp
+                } else {
+                        nodechange.PostUp = node.PostUp
+                }
+        }
+        if node.PostDown != "" {
+                if !strings.Contains(node.PostDown, nodechange.PostDown) {
+                        nodechange.PostDown = node.PostDown + "; " + nodechange.PostDown
+                } else {
+                        nodechange.PostDown = node.PostDown
+                }
+        }
+
+	collection := mongoconn.Client.Database("netmaker").Collection("nodes")
+        ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+        // Create filter
+        filter := bson.M{"macaddress": macaddress, "network": netid}
+        node.SetLastModified()
+        // prepare update model.
+        update := bson.D{
+                {"$set", bson.D{
+                        {"postup", nodechange.PostUp},
+                        {"postdown", nodechange.PostDown},
+                        {"isingressgateway", true},
+                        {"ingressgatewayrange", nodechange.IngressGatewayRange},
+                        {"lastmodified", node.LastModified},
+                }},
+        }
+        var nodeupdate models.Node
+        err = collection.FindOneAndUpdate(ctx, filter, update).Decode(&nodeupdate)
+        defer cancel()
+        if err != nil {
+		log.Println("error updating node to gateway")
+                return models.Node{}, err
+        }
+        err = SetNetworkNodesLastModified(netid)
+        if err != nil {
+                return node, err
+        }
+        //Get updated values to return
+        node, err = functions.GetNodeByMacAddress(netid, macaddress)
+        if err != nil {
+		log.Println("error finding node after update")
+                return node, err
+        }
+        return node, nil
+}
+
+func deleteIngressGateway(w http.ResponseWriter, r *http.Request) {
+	w.Header().Set("Content-Type", "application/json")
+	var params = mux.Vars(r)
+	node, err := DeleteIngressGateway(params["network"], params["macaddress"])
 	if err != nil {
 		returnErrorResponse(w, r, formatError(err, "internal"))
 		return
 	}
-
 	w.WriteHeader(http.StatusOK)
 	json.NewEncoder(w).Encode(node)
 }
 
+func DeleteIngressGateway(network, macaddress string) (models.Node, error) {
+
+	var nodeupdate models.Node
+	node, err := functions.GetNodeByMacAddress(network, macaddress)
+	if err != nil {
+		return models.Node{}, err
+	}
+
+	collection := mongoconn.Client.Database("netmaker").Collection("nodes")
+	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+	// Create filter
+	filter := bson.M{"macaddress": macaddress, "network": network}
+	// prepare update model.
+	update := bson.D{
+		{"$set", bson.D{
+			{"lastmodified", time.Now().Unix()},
+			{"isingressgateway", false},
+		}},
+	}
+	err = collection.FindOneAndUpdate(ctx, filter, update).Decode(&nodeupdate)
+	defer cancel()
+	if err != nil {
+		return models.Node{}, err
+	}
+	err = SetNetworkNodesLastModified(network)
+	if err != nil {
+		return models.Node{}, err
+	}
+	//Get updated values to return
+	node, err = functions.GetNodeByMacAddress(network, macaddress)
+	if err != nil {
+		return models.Node{}, err
+	}
+	return node, nil
+}
+
 func updateNode(w http.ResponseWriter, r *http.Request) {
 	w.Header().Set("Content-Type", "application/json")
 
@@ -755,7 +835,7 @@ func updateNode(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	var nodechange models.Node
+	var nodechange models.NodeUpdate
 
 	// we decode our body request params
 	_ = json.NewDecoder(r.Body).Decode(&nodechange)

+ 194 - 0
controllers/nodeHttpController_test.go

@@ -0,0 +1,194 @@
+package controller
+
+import (
+	"testing"
+	"time"
+
+	"github.com/gravitl/netmaker/models"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestCheckIn(t *testing.T) {
+	deleteNet(t)
+	createNet()
+	node := createTestNode(t)
+	time.Sleep(time.Second * 1)
+	t.Run("BadNet", func(t *testing.T) {
+		resp, err := CheckIn("badnet", node.MacAddress)
+		assert.NotNil(t, err)
+		assert.Equal(t, models.Node{}, resp)
+		assert.Equal(t, "mongo: no documents in result", err.Error())
+	})
+	t.Run("BadMac", func(t *testing.T) {
+		resp, err := CheckIn("skynet", "01:02:03")
+		assert.NotNil(t, err)
+		assert.Equal(t, models.Node{}, resp)
+		assert.Equal(t, "mongo: no documents in result", err.Error())
+	})
+	t.Run("Success", func(t *testing.T) {
+		resp, err := CheckIn("skynet", node.MacAddress)
+		assert.Nil(t, err)
+		assert.Greater(t, resp.LastCheckIn, node.LastCheckIn)
+	})
+}
+func TestCreateEgressGateway(t *testing.T) {
+	var gateway models.EgressGatewayRequest
+	gateway.Interface = "eth0"
+	gateway.RangeString = "10.100.100.0/24"
+	deleteNet(t)
+	createNet()
+	t.Run("NoNodes", func(t *testing.T) {
+		node, err := CreateEgressGateway(gateway)
+		assert.NotNil(t, err)
+		assert.Equal(t, models.Node{}, node)
+		assert.Equal(t, "mongo: no documents in result", err.Error())
+	})
+	t.Run("Success", func(t *testing.T) {
+		testnode := createTestNode(t)
+		gateway.NetID = "skynet"
+		gateway.NodeID = testnode.MacAddress
+
+		node, err := CreateEgressGateway(gateway)
+		assert.Nil(t, err)
+		assert.Equal(t, true, node.IsEgressGateway)
+		assert.Equal(t, "10.100.100.0/24", node.EgressGatewayRange)
+	})
+
+}
+func TestDeleteEgressGateway(t *testing.T) {
+	var gateway models.EgressGatewayRequest
+	deleteNet(t)
+	createNet()
+	createTestNode(t)
+	testnode := createTestNode(t)
+	gateway.Interface = "eth0"
+	gateway.RangeString = "10.100.100.0/24"
+	gateway.NetID = "skynet"
+	gateway.NodeID = testnode.MacAddress
+	t.Run("Success", func(t *testing.T) {
+		node, err := CreateEgressGateway(gateway)
+		assert.Nil(t, err)
+		assert.Equal(t, true, node.IsEgressGateway)
+		assert.Equal(t, "10.100.100.0/24", node.EgressGatewayRange)
+		node, err = DeleteEgressGateway(gateway.NetID, gateway.NodeID)
+		assert.Nil(t, err)
+		assert.Equal(t, false, node.IsEgressGateway)
+		assert.Equal(t, "", node.EgressGatewayRange)
+		assert.Equal(t, "", node.PostUp)
+		assert.Equal(t, "", node.PostDown)
+	})
+	t.Run("NotGateway", func(t *testing.T) {
+		node, err := DeleteEgressGateway(gateway.NetID, gateway.NodeID)
+		assert.Nil(t, err)
+		assert.Equal(t, false, node.IsEgressGateway)
+		assert.Equal(t, "", node.EgressGatewayRange)
+		assert.Equal(t, "", node.PostUp)
+		assert.Equal(t, "", node.PostDown)
+	})
+	t.Run("BadNode", func(t *testing.T) {
+		node, err := DeleteEgressGateway(gateway.NetID, "01:02:03")
+		assert.NotNil(t, err)
+		assert.Equal(t, "mongo: no documents in result", err.Error())
+		assert.Equal(t, models.Node{}, node)
+	})
+	t.Run("BadNet", func(t *testing.T) {
+		node, err := DeleteEgressGateway("badnet", gateway.NodeID)
+		assert.NotNil(t, err)
+		assert.Equal(t, "mongo: no documents in result", err.Error())
+		assert.Equal(t, models.Node{}, node)
+	})
+
+}
+func TestGetLastModified(t *testing.T) {
+	deleteNet(t)
+	createNet()
+	createTestNode(t)
+	t.Run("BadNet", func(t *testing.T) {
+		network, err := GetLastModified("badnet")
+		assert.NotNil(t, err)
+		assert.Equal(t, models.Network{}, network)
+		assert.Equal(t, "mongo: no documents in result", err.Error())
+	})
+	t.Run("Success", func(t *testing.T) {
+		network, err := GetLastModified("skynet")
+		assert.Nil(t, err)
+		assert.NotEqual(t, models.Network{}, network)
+	})
+}
+func TestGetNetworkNodes(t *testing.T) {
+	deleteNet(t)
+	createNet()
+	t.Run("BadNet", func(t *testing.T) {
+		node, err := GetNetworkNodes("badnet")
+		assert.Nil(t, err)
+		assert.Equal(t, []models.Node(nil), node)
+		//assert.Equal(t, "mongo: no documents in result", err.Error())
+	})
+	t.Run("NoNodes", func(t *testing.T) {
+		node, err := GetNetworkNodes("skynet")
+		assert.Nil(t, err)
+		assert.Equal(t, []models.Node(nil), node)
+	})
+	t.Run("Success", func(t *testing.T) {
+		createTestNode(t)
+		node, err := GetNetworkNodes("skynet")
+		assert.Nil(t, err)
+		assert.NotEqual(t, []models.Node(nil), node)
+	})
+
+}
+func TestUncordonNode(t *testing.T) {
+	deleteNet(t)
+	createNet()
+	node := createTestNode(t)
+	t.Run("BadNet", func(t *testing.T) {
+		resp, err := UncordonNode("badnet", node.MacAddress)
+		assert.NotNil(t, err)
+		assert.Equal(t, models.Node{}, resp)
+		assert.Equal(t, "mongo: no documents in result", err.Error())
+	})
+	t.Run("BadMac", func(t *testing.T) {
+		resp, err := UncordonNode("skynet", "01:02:03")
+		assert.NotNil(t, err)
+		assert.Equal(t, models.Node{}, resp)
+		assert.Equal(t, "mongo: no documents in result", err.Error())
+	})
+	t.Run("Success", func(t *testing.T) {
+		resp, err := CheckIn("skynet", node.MacAddress)
+		assert.Nil(t, err)
+		assert.Equal(t, false, resp.IsPending)
+	})
+
+}
+func TestValidateEgressGateway(t *testing.T) {
+	var gateway models.EgressGatewayRequest
+	t.Run("InvalidRange", func(t *testing.T) {
+		gateway.Interface = "eth0"
+		gateway.RangeString = "helloworld"
+		err := ValidateEgressGateway(gateway)
+		assert.NotNil(t, err)
+		assert.Equal(t, "IP Range Not Valid", err.Error())
+	})
+	t.Run("EmptyRange", func(t *testing.T) {
+		gateway.Interface = "eth0"
+		gateway.RangeString = ""
+		err := ValidateEgressGateway(gateway)
+		assert.NotNil(t, err)
+		assert.Equal(t, "IP Range Not Valid", err.Error())
+	})
+	t.Run("EmptyInterface", func(t *testing.T) {
+		gateway.Interface = ""
+		err := ValidateEgressGateway(gateway)
+		assert.NotNil(t, err)
+		assert.Equal(t, "Interface cannot be empty", err.Error())
+	})
+	t.Run("Success", func(t *testing.T) {
+		gateway.Interface = "eth0"
+		gateway.RangeString = "10.100.100.0/24"
+		err := ValidateEgressGateway(gateway)
+		assert.Nil(t, err)
+	})
+}
+
+//func TestUpdateNode(t *testing.T) {
+//}

+ 57 - 0
controllers/responseHttp_test.go

@@ -0,0 +1,57 @@
+package controller
+
+import (
+	"encoding/json"
+	"errors"
+	"net/http"
+	"net/http/httptest"
+	"testing"
+
+	"github.com/gravitl/netmaker/models"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestFormatError(t *testing.T) {
+	response := formatError(errors.New("this is a sample error"), "badrequest")
+	assert.Equal(t, http.StatusBadRequest, response.Code)
+	assert.Equal(t, "this is a sample error", response.Message)
+}
+
+func TestReturnSuccessResponse(t *testing.T) {
+	var response models.SuccessResponse
+	handler := func(rw http.ResponseWriter, r *http.Request) {
+		returnSuccessResponse(rw, r, "This is a test message")
+	}
+	req := httptest.NewRequest(http.MethodGet, "http://example.com", nil)
+	w := httptest.NewRecorder()
+	handler(w, req)
+	resp := w.Result()
+	assert.Equal(t, http.StatusOK, resp.StatusCode)
+	assert.Equal(t, "application/json", resp.Header.Get("Content-Type"))
+	//body, err := ioutil.ReadAll(resp.Body)
+	//assert.Nil(t, err)
+	//t.Log(body, string(body))
+	err := json.NewDecoder(resp.Body).Decode(&response)
+	assert.Nil(t, err)
+	assert.Equal(t, http.StatusOK, response.Code)
+	assert.Equal(t, "This is a test message", response.Message)
+}
+
+func testReturnErrorResponse(t *testing.T) {
+	var response, errMessage models.ErrorResponse
+	errMessage.Code = http.StatusUnauthorized
+	errMessage.Message = "You are not authorized to access this endpoint"
+	handler := func(rw http.ResponseWriter, r *http.Request) {
+		returnErrorResponse(rw, r, errMessage)
+	}
+	req := httptest.NewRequest(http.MethodGet, "http://example.com", nil)
+	w := httptest.NewRecorder()
+	handler(w, req)
+	resp := w.Result()
+	assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
+	assert.Equal(t, "application/json", resp.Header.Get("Content-Type"))
+	err := json.NewDecoder(resp.Body).Decode(&response)
+	assert.Nil(t, err)
+	assert.Equal(t, http.StatusUnauthorized, response.Code)
+	assert.Equal(t, "You are not authorized to access this endpoint", response.Message)
+}

+ 42 - 13
controllers/serverHttpController.go

@@ -2,6 +2,7 @@ package controller
 
 import (
     "github.com/gravitl/netmaker/models"
+    "github.com/gravitl/netmaker/functions"
     "github.com/gravitl/netmaker/serverctl"
     "github.com/gravitl/netmaker/servercfg"
     "encoding/json"
@@ -13,6 +14,7 @@ import (
 func serverHandlers(r *mux.Router) {
     r.HandleFunc("/api/server/addnetwork/{network}", securityCheckServer(http.HandlerFunc(addNetwork))).Methods("POST")
     r.HandleFunc("/api/server/getconfig", securityCheckServer(http.HandlerFunc(getConfig))).Methods("GET")
+    r.HandleFunc("/api/server/getwgconfig", securityCheckServer(http.HandlerFunc(getWGConfig))).Methods("GET")
     r.HandleFunc("/api/server/removenetwork/{network}", securityCheckServer(http.HandlerFunc(removeNetwork))).Methods("DELETE")
 }
 
@@ -27,25 +29,29 @@ func securityCheckServer(next http.Handler) http.HandlerFunc {
 
 		bearerToken := r.Header.Get("Authorization")
 
-		var hasBearer = true
 		var tokenSplit = strings.Split(bearerToken, " ")
 		var  authToken = ""
-
 		if len(tokenSplit) < 2 {
-			hasBearer = false
-		} else {
+                      errorResponse = models.ErrorResponse{
+                                Code: http.StatusUnauthorized, Message: "W1R3: You are unauthorized to access this endpoint.",
+                      }
+                      returnErrorResponse(w, r, errorResponse)
+			return 
+	        } else {
 			authToken = tokenSplit[1]
 		}
 		//all endpoints here require master so not as complicated
 		//still might not be a good  way of doing this
-		if !hasBearer || !authenticateMasterServer(authToken) {
-			errorResponse = models.ErrorResponse{
-				Code: http.StatusUnauthorized, Message: "W1R3: You are unauthorized to access this endpoint.",
-			}
-			returnErrorResponse(w, r, errorResponse)
-		} else {
-			next.ServeHTTP(w, r)
+                _, isadmin, _ := functions.VerifyUserToken(authToken)
+
+		if !isadmin && !authenticateMasterServer(authToken) {
+				errorResponse = models.ErrorResponse{
+					Code: http.StatusUnauthorized, Message: "W1R3: You are unauthorized to access this endpoint.",
+				}
+				returnErrorResponse(w, r, errorResponse)
+				return
 		}
+		next.ServeHTTP(w, r)
 	}
 }
 //Consider a more secure way of setting master key
@@ -74,16 +80,39 @@ func removeNetwork(w http.ResponseWriter, r *http.Request) {
 }
 
 func getConfig(w http.ResponseWriter, r *http.Request) {
+	// Set header
+        w.Header().Set("Content-Type", "application/json")
+
+        // get params
+
+        scfg := servercfg.GetServerConfig()
+        w.WriteHeader(http.StatusOK)
+        json.NewEncoder(w).Encode(scfg)
+}
+
+func getWGConfig(w http.ResponseWriter, r *http.Request) {
         // Set header
         w.Header().Set("Content-Type", "application/json")
 
         // get params
 
-        scfg := servercfg.GetConfig()
+        wgcfg := servercfg.GetWGConfig()
+        w.WriteHeader(http.StatusOK)
+        json.NewEncoder(w).Encode(wgcfg)
+}
+
+/*
+func getMongoConfig(w http.ResponseWriter, r *http.Request) {
+        // Set header
+        w.Header().Set("Content-Type", "application/json")
+
+        // get params
 
+        mcfg := servercfg.GetMongoConfig()
         w.WriteHeader(http.StatusOK)
-        json.NewEncoder(w).Encode(scfg)
+        json.NewEncoder(w).Encode(mcfg)
 }
+*/
 
 func addNetwork(w http.ResponseWriter, r *http.Request) {
         // Set header

+ 121 - 178
controllers/userHttpController.go

@@ -9,6 +9,7 @@ import (
 	"strings"
 	"time"
 
+	"github.com/go-playground/validator/v10"
 	"github.com/gorilla/mux"
 	"github.com/gravitl/netmaker/functions"
 	"github.com/gravitl/netmaker/models"
@@ -17,7 +18,6 @@ import (
 	"go.mongodb.org/mongo-driver/mongo"
 	"go.mongodb.org/mongo-driver/mongo/options"
 	"golang.org/x/crypto/bcrypt"
-	"gopkg.in/go-playground/validator.v9"
 )
 
 func userHandlers(r *mux.Router) {
@@ -36,7 +36,6 @@ func authenticateUser(response http.ResponseWriter, request *http.Request) {
 	//Auth request consists of Mac Address and Password (from node that is authorizing
 	//in case of Master, auth is ignored and mac is set to "mastermac"
 	var authRequest models.UserAuthParams
-	var result models.User
 	var errorResponse = models.ErrorResponse{
 		Code: http.StatusInternalServerError, Message: "W1R3: It's not you it's me.",
 	}
@@ -44,74 +43,74 @@ func authenticateUser(response http.ResponseWriter, request *http.Request) {
 	decoder := json.NewDecoder(request.Body)
 	decoderErr := decoder.Decode(&authRequest)
 	defer request.Body.Close()
-
 	if decoderErr != nil {
 		returnErrorResponse(response, request, errorResponse)
 		return
-	} else {
-		errorResponse.Code = http.StatusBadRequest
-		if authRequest.UserName == "" {
-			errorResponse.Message = "W1R3: Username can't be empty"
-			returnErrorResponse(response, request, errorResponse)
-			return
-		} else if authRequest.Password == "" {
-			errorResponse.Message = "W1R3: Password can't be empty"
-			returnErrorResponse(response, request, errorResponse)
-			return
-		} else {
-
-			//Search DB for node with Mac Address. Ignore pending nodes (they should not be able to authenticate with API untill approved).
-			collection := mongoconn.Client.Database("netmaker").Collection("users")
-			ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
-			var err = collection.FindOne(ctx, bson.M{"username": authRequest.UserName}).Decode(&result)
-
-			defer cancel()
-
-			if err != nil {
-				errorResponse.Message = "W1R3: User " + authRequest.UserName + " not found."
-				returnErrorResponse(response, request, errorResponse)
-				return
-			}
-
-			//compare password from request to stored password in database
-			//might be able to have a common hash (certificates?) and compare those so that a password isn't passed in in plain text...
-			//TODO: Consider a way of hashing the password client side before sending, or using certificates
-			err = bcrypt.CompareHashAndPassword([]byte(result.Password), []byte(authRequest.Password))
-			if err != nil {
-				errorResponse = models.ErrorResponse{
-					Code: http.StatusUnauthorized, Message: "W1R3: Wrong Password.",
-				}
-				returnErrorResponse(response, request, errorResponse)
-				return
-			} else {
-				//Create a new JWT for the node
-				tokenString, _ := functions.CreateUserJWT(authRequest.UserName, result.IsAdmin)
-
-				if tokenString == "" {
-					returnErrorResponse(response, request, errorResponse)
-					return
-				}
-
-				var successResponse = models.SuccessResponse{
-					Code:    http.StatusOK,
-					Message: "W1R3: Device " + authRequest.UserName + " Authorized",
-					Response: models.SuccessfulUserLoginResponse{
-						AuthToken: tokenString,
-						UserName:  authRequest.UserName,
-					},
-				}
-				//Send back the JWT
-				successJSONResponse, jsonError := json.Marshal(successResponse)
-
-				if jsonError != nil {
-					returnErrorResponse(response, request, errorResponse)
-					return
-				}
-				response.Header().Set("Content-Type", "application/json")
-				response.Write(successJSONResponse)
-			}
-		}
 	}
+
+	jwt, err := VerifyAuthRequest(authRequest)
+	if err != nil {
+		returnErrorResponse(response, request, formatError(err, "badrequest"))
+		return
+	}
+
+	if jwt == "" {
+		//very unlikely that err is !nil and no jwt returned, but handle it anyways.
+		returnErrorResponse(response, request, formatError(errors.New("No token returned"), "internal"))
+		return
+	}
+
+	var successResponse = models.SuccessResponse{
+		Code:    http.StatusOK,
+		Message: "W1R3: Device " + authRequest.UserName + " Authorized",
+		Response: models.SuccessfulUserLoginResponse{
+			AuthToken: jwt,
+			UserName:  authRequest.UserName,
+		},
+	}
+	//Send back the JWT
+	successJSONResponse, jsonError := json.Marshal(successResponse)
+
+	if jsonError != nil {
+		returnErrorResponse(response, request, errorResponse)
+		return
+	}
+	response.Header().Set("Content-Type", "application/json")
+	response.Write(successJSONResponse)
+}
+
+func VerifyAuthRequest(authRequest models.UserAuthParams) (string, error) {
+	var result models.User
+	if authRequest.UserName == "" {
+		return "", errors.New("Username can't be empty")
+	} else if authRequest.Password == "" {
+		return "", errors.New("Password can't be empty")
+	}
+	//Search DB for node with Mac Address. Ignore pending nodes (they should not be able to authenticate with API untill approved).
+	collection := mongoconn.Client.Database("netmaker").Collection("users")
+	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+	var err = collection.FindOne(ctx, bson.M{"username": authRequest.UserName}).Decode(&result)
+
+	defer cancel()
+	if err != nil {
+		return "", errors.New("User " + authRequest.UserName + " not found")
+	}
+	// This is a a useless test as cannot create user that is not an an admin
+	if !result.IsAdmin {
+		return "", errors.New("User is not an admin")
+	}
+
+	//compare password from request to stored password in database
+	//might be able to have a common hash (certificates?) and compare those so that a password isn't passed in in plain text...
+	//TODO: Consider a way of hashing the password client side before sending, or using certificates
+	err = bcrypt.CompareHashAndPassword([]byte(result.Password), []byte(authRequest.Password))
+	if err != nil {
+		return "", errors.New("Wrong Password")
+	}
+
+	//Create a new JWT for the node
+	tokenString, _ := functions.CreateUserJWT(authRequest.UserName, true)
+	return tokenString, nil
 }
 
 //The middleware for most requests to the API
@@ -123,56 +122,43 @@ func authenticateUser(response http.ResponseWriter, request *http.Request) {
 //TODO: Consider better RBAC implementations
 func authorizeUser(next http.Handler) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
-
-		var errorResponse = models.ErrorResponse{
-			Code: http.StatusInternalServerError, Message: "W1R3: It's not you it's me.",
-		}
-
 		w.Header().Set("Content-Type", "application/json")
 
 		//get the auth token
 		bearerToken := r.Header.Get("Authorization")
-
-		var tokenSplit = strings.Split(bearerToken, " ")
-
-		//I put this in in case the user doesn't put in a token at all (in which case it's empty)
-		//There's probably a smarter way of handling this.
-		var authToken = "928rt238tghgwe@TY@$Y@#WQAEGB2FC#@HG#@$Hddd"
-
-		if len(tokenSplit) > 1 {
-			authToken = tokenSplit[1]
-		} else {
-			errorResponse = models.ErrorResponse{
-				Code: http.StatusUnauthorized, Message: "W1R3: Missing Auth Token.",
-			}
-			returnErrorResponse(w, r, errorResponse)
+		err := ValidateUserToken(bearerToken)
+		if err != nil {
+			returnErrorResponse(w, r, formatError(err, "unauthorized"))
 			return
 		}
+		next.ServeHTTP(w, r)
+	}
+}
 
-		//This checks if
-		//A: the token is the master password
-		//B: the token corresponds to a mac address, and if so, which one
-		//TODO: There's probably a better way of dealing with the "master token"/master password. Plz Halp.
-		username, _, err := functions.VerifyUserToken(authToken)
+func ValidateUserToken(token string) error {
+	var tokenSplit = strings.Split(token, " ")
 
-		if err != nil {
-			returnErrorResponse(w, r, formatError(err, "internal"))
-			return
-		}
+	//I put this in in case the user doesn't put in a token at all (in which case it's empty)
+	//There's probably a smarter way of handling this.
+	var authToken = "928rt238tghgwe@TY@$Y@#WQAEGB2FC#@HG#@$Hddd"
 
-		isAuthorized := username != ""
+	if len(tokenSplit) > 1 {
+		authToken = tokenSplit[1]
+	} else {
+		return errors.New("Missing Auth Token.")
+	}
 
-		if !isAuthorized {
-			errorResponse = models.ErrorResponse{
-				Code: http.StatusUnauthorized, Message: "W1R3: You are unauthorized to access this endpoint.",
-			}
-			returnErrorResponse(w, r, errorResponse)
-			return
-		} else {
-			//If authorized, this function passes along it's request and output to the appropriate route function.
-			next.ServeHTTP(w, r)
-		}
+	username, _, err := functions.VerifyUserToken(authToken)
+	if err != nil {
+		return errors.New("Error Verifying Auth Token")
+	}
+
+	isAuthorized := username != ""
+	if !isAuthorized {
+		return errors.New("You are unauthorized to access this endpoint.")
 	}
+
+	return nil
 }
 
 func HasAdmin() (bool, error) {
@@ -192,8 +178,9 @@ func HasAdmin() (bool, error) {
 
 	if err != nil {
 		if err == mongo.ErrNoDocuments {
-			return false, err
+			return false, nil
 		}
+		return false, err
 		fmt.Println(err)
 	}
 	return true, err
@@ -237,18 +224,27 @@ func getUser(w http.ResponseWriter, r *http.Request) {
 	user, err := GetUser(params["username"])
 
 	if err != nil {
-                returnErrorResponse(w, r, formatError(err, "internal"))
-                return
+		returnErrorResponse(w, r, formatError(err, "internal"))
+		return
 	}
 
 	json.NewEncoder(w).Encode(user)
 }
 
 func CreateUser(user models.User) (models.User, error) {
+	hasadmin, err := HasAdmin()
+	if hasadmin {
+		return models.User{}, errors.New("Admin already Exists")
+	}
+
+	user.IsAdmin = true
+	err = ValidateUser("create", user)
+	if err != nil {
+		return models.User{}, err
+	}
 
 	//encrypt that password so we never see it again
 	hash, err := bcrypt.GenerateFromPassword([]byte(user.Password), 5)
-
 	if err != nil {
 		return user, err
 	}
@@ -267,9 +263,7 @@ func CreateUser(user models.User) (models.User, error) {
 	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
 
 	// insert our node to the node db.
-	result, err := collection.InsertOne(ctx, user)
-	_ = result
-
+	_, err = collection.InsertOne(ctx, user)
 	defer cancel()
 
 	return user, err
@@ -278,37 +272,15 @@ func CreateUser(user models.User) (models.User, error) {
 func createAdmin(w http.ResponseWriter, r *http.Request) {
 	w.Header().Set("Content-Type", "application/json")
 
-	var errorResponse = models.ErrorResponse{
-		Code: http.StatusInternalServerError, Message: "W1R3: It's not you it's me.",
-	}
-
-	hasadmin, err := HasAdmin()
-
-	if hasadmin {
-		errorResponse = models.ErrorResponse{
-			Code: http.StatusUnauthorized, Message: "W1R3: Admin already exists! ",
-		}
-		returnErrorResponse(w, r, errorResponse)
-		return
-	}
-
 	var admin models.User
-
 	//get node from body of request
 	_ = json.NewDecoder(r.Body).Decode(&admin)
 
-	admin.IsAdmin = true
-
-	err = ValidateUser("create", admin)
-	if err != nil {
-                returnErrorResponse(w, r, formatError(err, "internal"))
-                return
-	}
+	admin, err := CreateUser(admin)
 
-	admin, err = CreateUser(admin)
 	if err != nil {
-                returnErrorResponse(w, r, formatError(err, "internal"))
-                return
+		returnErrorResponse(w, r, formatError(err, "badrequest"))
+		return
 	}
 
 	json.NewEncoder(w).Encode(admin)
@@ -316,6 +288,11 @@ func createAdmin(w http.ResponseWriter, r *http.Request) {
 
 func UpdateUser(userchange models.User, user models.User) (models.User, error) {
 
+	err := ValidateUser("update", userchange)
+	if err != nil {
+		return models.User{}, err
+	}
+
 	queryUser := user.UserName
 
 	if userchange.UserName != "" {
@@ -363,48 +340,31 @@ func UpdateUser(userchange models.User, user models.User) (models.User, error) {
 
 	defer cancel()
 
-	return userupdate, errN
+	return user, errN
 }
 
 func updateUser(w http.ResponseWriter, r *http.Request) {
 	w.Header().Set("Content-Type", "application/json")
-
 	var params = mux.Vars(r)
-
 	var user models.User
-
 	//start here
 	user, err := GetUser(params["username"])
 	if err != nil {
-                returnErrorResponse(w, r, formatError(err, "internal"))
-                return
+		returnErrorResponse(w, r, formatError(err, "internal"))
+		return
 	}
-
 	var userchange models.User
-
 	// we decode our body request params
 	err = json.NewDecoder(r.Body).Decode(&userchange)
 	if err != nil {
-                returnErrorResponse(w, r, formatError(err, "internal"))
-                return
-	}
-
-	userchange.IsAdmin = true
-
-	err = ValidateUser("update", userchange)
-
-	if err != nil {
-                returnErrorResponse(w, r, formatError(err, "internal"))
-                return
+		returnErrorResponse(w, r, formatError(err, "internal"))
+		return
 	}
-
 	user, err = UpdateUser(userchange, user)
-
 	if err != nil {
-                returnErrorResponse(w, r, formatError(err, "internal"))
-                return
+		returnErrorResponse(w, r, formatError(err, "badrequest"))
+		return
 	}
-
 	json.NewEncoder(w).Encode(user)
 }
 
@@ -454,23 +414,6 @@ func deleteUser(w http.ResponseWriter, r *http.Request) {
 func ValidateUser(operation string, user models.User) error {
 
 	v := validator.New()
-
-	_ = v.RegisterValidation("username_unique", func(fl validator.FieldLevel) bool {
-		_, err := GetUser(user.UserName)
-		return err == nil || operation == "update"
-	})
-
-	_ = v.RegisterValidation("username_valid", func(fl validator.FieldLevel) bool {
-		isvalid := functions.NameInNodeCharSet(user.UserName)
-		return isvalid
-	})
-
-	_ = v.RegisterValidation("password_check", func(fl validator.FieldLevel) bool {
-		notEmptyCheck := user.Password != ""
-		goodLength := len(user.Password) > 5
-		return (notEmptyCheck && goodLength) || operation == "update"
-	})
-
 	err := v.Struct(user)
 
 	if err != nil {

+ 257 - 0
controllers/userHttpController_test.go

@@ -0,0 +1,257 @@
+package controller
+
+import (
+	"context"
+	"os"
+	"testing"
+	"time"
+
+	"github.com/gravitl/netmaker/models"
+	"github.com/gravitl/netmaker/mongoconn"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestMain(m *testing.M) {
+	mongoconn.ConnectDatabase()
+	var gconf models.GlobalConfig
+	gconf.ServerGRPC = "localhost:8081"
+	gconf.PortGRPC = "50051"
+	//err := SetGlobalConfig(gconf)
+	collection := mongoconn.Client.Database("netmaker").Collection("config")
+	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+	defer cancel()
+	//create, _, err := functions.GetGlobalConfig()
+	_, err := collection.InsertOne(ctx, gconf)
+	if err != nil {
+		panic("could not create config store")
+	}
+	//drop network, nodes, and user collections
+	var collections = []string{"networks", "nodes", "users", "dns"}
+	for _, table := range collections {
+		collection := mongoconn.Client.Database("netmaker").Collection(table)
+		ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+		defer cancel()
+		err := collection.Drop(ctx)
+		if err != nil {
+			panic("could not drop collection")
+		}
+	}
+	os.Exit(m.Run())
+}
+
+func TestHasAdmin(t *testing.T) {
+	_, err := DeleteUser("admin")
+	assert.Nil(t, err)
+	user := models.User{"admin", "password", true}
+	_, err = CreateUser(user)
+	assert.Nil(t, err)
+	t.Run("AdminExists", func(t *testing.T) {
+		found, err := HasAdmin()
+		assert.Nil(t, err)
+		assert.True(t, found)
+	})
+	t.Run("NoUser", func(t *testing.T) {
+		_, err := DeleteUser("admin")
+		assert.Nil(t, err)
+		found, err := HasAdmin()
+		assert.Nil(t, err)
+		assert.False(t, found)
+	})
+}
+
+func TestCreateUser(t *testing.T) {
+	user := models.User{"admin", "password", true}
+	t.Run("NoUser", func(t *testing.T) {
+		_, err := DeleteUser("admin")
+		assert.Nil(t, err)
+		admin, err := CreateUser(user)
+		assert.Nil(t, err)
+		assert.Equal(t, user.UserName, admin.UserName)
+	})
+	t.Run("AdminExists", func(t *testing.T) {
+		_, err := CreateUser(user)
+		assert.NotNil(t, err)
+		assert.Equal(t, "Admin already Exists", err.Error())
+	})
+}
+
+func TestDeleteUser(t *testing.T) {
+	hasadmin, err := HasAdmin()
+	assert.Nil(t, err)
+	if !hasadmin {
+		user := models.User{"admin", "pasword", true}
+		_, err := CreateUser(user)
+		assert.Nil(t, err)
+	}
+	t.Run("ExistingUser", func(t *testing.T) {
+		deleted, err := DeleteUser("admin")
+		assert.Nil(t, err)
+		assert.True(t, deleted)
+		t.Log(deleted, err)
+	})
+	t.Run("NonExistantUser", func(t *testing.T) {
+		deleted, err := DeleteUser("admin")
+		assert.Nil(t, err)
+		assert.False(t, deleted)
+	})
+}
+
+func TestValidateUser(t *testing.T) {
+	var user models.User
+	t.Run("ValidCreate", func(t *testing.T) {
+		user.UserName = "admin"
+		user.Password = "validpass"
+		err := ValidateUser("create", user)
+		assert.Nil(t, err)
+	})
+	t.Run("ValidUpdate", func(t *testing.T) {
+		user.UserName = "admin"
+		user.Password = "password"
+		err := ValidateUser("update", user)
+		assert.Nil(t, err)
+	})
+	t.Run("InvalidUserName", func(t *testing.T) {
+		user.UserName = "invalid*"
+		err := ValidateUser("update", user)
+		assert.NotNil(t, err)
+		assert.Contains(t, err.Error(), "Field validation for 'UserName' failed")
+	})
+	t.Run("ShortUserName", func(t *testing.T) {
+		user.UserName = "12"
+		err := ValidateUser("create", user)
+		assert.NotNil(t, err)
+		assert.Contains(t, err.Error(), "Field validation for 'UserName' failed")
+	})
+	t.Run("EmptyPassword", func(t *testing.T) {
+		user.Password = ""
+		err := ValidateUser("create", user)
+		assert.NotNil(t, err)
+		assert.Contains(t, err.Error(), "Field validation for 'Password' failed")
+	})
+	t.Run("ShortPassword", func(t *testing.T) {
+		user.Password = "123"
+		err := ValidateUser("create", user)
+		assert.NotNil(t, err)
+		assert.Contains(t, err.Error(), "Field validation for 'Password' failed")
+	})
+}
+
+func TestGetUser(t *testing.T) {
+	t.Run("UserExisits", func(t *testing.T) {
+		user := models.User{"admin", "password", true}
+		hasadmin, err := HasAdmin()
+		assert.Nil(t, err)
+		if !hasadmin {
+			_, err := CreateUser(user)
+			assert.Nil(t, err)
+		}
+		admin, err := GetUser("admin")
+		assert.Nil(t, err)
+		assert.Equal(t, user.UserName, admin.UserName)
+	})
+	t.Run("NonExistantUser", func(t *testing.T) {
+		_, err := DeleteUser("admin")
+		assert.Nil(t, err)
+		admin, err := GetUser("admin")
+		assert.Equal(t, "mongo: no documents in result", err.Error())
+		assert.Equal(t, "", admin.UserName)
+	})
+}
+
+func TestUpdateUser(t *testing.T) {
+	user := models.User{"admin", "password", true}
+	newuser := models.User{"hello", "world", true}
+	t.Run("UserExisits", func(t *testing.T) {
+		_, err := DeleteUser("admin")
+		_, err = CreateUser(user)
+		assert.Nil(t, err)
+		admin, err := UpdateUser(newuser, user)
+		assert.Nil(t, err)
+		assert.Equal(t, newuser.UserName, admin.UserName)
+	})
+	t.Run("NonExistantUser", func(t *testing.T) {
+		_, err := DeleteUser("hello")
+		assert.Nil(t, err)
+		_, err = UpdateUser(newuser, user)
+		assert.Equal(t, "mongo: no documents in result", err.Error())
+	})
+}
+
+func TestValidateUserToken(t *testing.T) {
+	t.Run("EmptyToken", func(t *testing.T) {
+		err := ValidateUserToken("")
+		assert.NotNil(t, err)
+		assert.Equal(t, "Missing Auth Token.", err.Error())
+	})
+	t.Run("InvalidToken", func(t *testing.T) {
+		err := ValidateUserToken("Bearer: badtoken")
+		assert.NotNil(t, err)
+		assert.Equal(t, "Error Verifying Auth Token", err.Error())
+	})
+	t.Run("InvalidUser", func(t *testing.T) {
+		t.Skip()
+		//need authorization
+	})
+	t.Run("ValidToken", func(t *testing.T) {
+		err := ValidateUserToken("Bearer: secretkey")
+		assert.Nil(t, err)
+	})
+}
+
+func TestVerifyAuthRequest(t *testing.T) {
+	var authRequest models.UserAuthParams
+	t.Run("EmptyUserName", func(t *testing.T) {
+		authRequest.UserName = ""
+		authRequest.Password = "Password"
+		jwt, err := VerifyAuthRequest(authRequest)
+		assert.NotNil(t, err)
+		assert.Equal(t, "", jwt)
+		assert.Equal(t, "Username can't be empty", err.Error())
+	})
+	t.Run("EmptyPassword", func(t *testing.T) {
+		authRequest.UserName = "admin"
+		authRequest.Password = ""
+		jwt, err := VerifyAuthRequest(authRequest)
+		assert.NotNil(t, err)
+		assert.Equal(t, "", jwt)
+		assert.Equal(t, "Password can't be empty", err.Error())
+	})
+	t.Run("NonExistantUser", func(t *testing.T) {
+		_, err := DeleteUser("admin")
+		authRequest.UserName = "admin"
+		authRequest.Password = "password"
+		jwt, err := VerifyAuthRequest(authRequest)
+		assert.NotNil(t, err)
+		assert.Equal(t, "", jwt)
+		assert.Equal(t, "User admin not found", err.Error())
+	})
+	t.Run("Non-Admin", func(t *testing.T) {
+		//can't create a user that is not a an admin
+		t.Skip()
+		user := models.User{"admin", "admin", false}
+		_, err := CreateUser(user)
+		assert.Nil(t, err)
+		authRequest := models.UserAuthParams{"admin", "admin"}
+		jwt, err := VerifyAuthRequest(authRequest)
+		assert.NotNil(t, err)
+		assert.Equal(t, "", jwt)
+		assert.Equal(t, "User is not an admin", err.Error())
+	})
+	t.Run("WrongPassword", func(t *testing.T) {
+		_, err := DeleteUser("admin")
+		user := models.User{"admin", "password", true}
+		_, err = CreateUser(user)
+		assert.Nil(t, err)
+		authRequest := models.UserAuthParams{"admin", "badpass"}
+		jwt, err := VerifyAuthRequest(authRequest)
+		assert.NotNil(t, err)
+		assert.Equal(t, "", jwt)
+		assert.Equal(t, "Wrong Password", err.Error())
+	})
+	t.Run("Success", func(t *testing.T) {
+		authRequest := models.UserAuthParams{"admin", "password"}
+		jwt, err := VerifyAuthRequest(authRequest)
+		assert.Nil(t, err)
+		assert.NotNil(t, jwt)
+	})
+}

+ 2 - 0
docs/Dockerfile

@@ -0,0 +1,2 @@
+FROM nginx:1.19
+COPY _build/html /usr/share/nginx/html

+ 20 - 0
docs/Makefile

@@ -0,0 +1,20 @@
+# Minimal makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line, and also
+# from the environment for the first two.
+SPHINXOPTS    ?=
+SPHINXBUILD   ?= sphinx-build
+SOURCEDIR     = .
+BUILDDIR      = _build
+
+# Put it first so that "make" without argument is like "make help".
+help:
+	@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
+
+.PHONY: help Makefile
+
+# Catch-all target: route all unknown targets to Sphinx using the new
+# "make mode" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).
+%: Makefile
+	@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

BIN
docs/_build/doctrees/about.doctree


BIN
docs/_build/doctrees/api.doctree


BIN
docs/_build/doctrees/architecture.doctree


BIN
docs/_build/doctrees/client-installation.doctree


BIN
docs/_build/doctrees/conduct.doctree


BIN
docs/_build/doctrees/contact.doctree


BIN
docs/_build/doctrees/contribute.doctree


BIN
docs/_build/doctrees/environment.pickle


BIN
docs/_build/doctrees/external-clients.doctree


BIN
docs/_build/doctrees/getting-started.doctree


BIN
docs/_build/doctrees/index.doctree


BIN
docs/_build/doctrees/introduction.doctree


BIN
docs/_build/doctrees/license.doctree


BIN
docs/_build/doctrees/quick-start.doctree


BIN
docs/_build/doctrees/server-installation.doctree


BIN
docs/_build/doctrees/support.doctree


BIN
docs/_build/doctrees/troubleshoot.doctree


BIN
docs/_build/doctrees/tutorials.doctree


BIN
docs/_build/doctrees/usage.doctree


BIN
docs/_build/doctrees/walkthroughs.doctree


+ 4 - 0
docs/_build/html/.buildinfo

@@ -0,0 +1,4 @@
+# Sphinx build info version 1
+# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done.
+config: ef1c483d6ecb673b2f9ea9eedb1cedaa
+tags: 645f666f9bcd5a90fca523b33c5a78b7

BIN
docs/_build/html/_images/access-key.png


BIN
docs/_build/html/_images/create-user.png


BIN
docs/_build/html/_images/default-net.png


BIN
docs/_build/html/_images/mesh-diagram.png


BIN
docs/_build/html/_images/mesh.png


BIN
docs/_build/html/_images/nc-install-output.png


BIN
docs/_build/html/_images/netmaker.png


BIN
docs/_build/html/_images/nm-diagram-2.jpg


BIN
docs/_build/html/_images/nm-diagram.jpg


BIN
docs/_build/html/_images/nm-node-success.png


BIN
docs/_build/html/_images/node-details.png


BIN
docs/_build/html/_images/nodes.png


BIN
docs/_build/html/_images/ping-node.png


+ 46 - 0
docs/_build/html/_sources/about.rst.txt

@@ -0,0 +1,46 @@
+===============
+About
+===============
+
+What is Netmaker?
+==================
+
+Netmaker is a tool for creating and managing virtual overlay networks. If you have at least two machines with internet access which you need to connect with a secure tunnel, Netmaker is for you. If you have thousands of servers spread across multiple locations, data centers, or clouds, Netmaker is also for you. Netmaker connects machines securely, wherever they are.
+
+.. image:: images/mesh-diagram.png
+   :width: 50%
+   :alt: WireGuard Mesh
+   :align: center
+
+Netmaker takes those machines and creates a flat network so that they can all talk to each other easily and securely. 
+If you're familiar with AWS, it's like a VPC but made up of arbitrary computers. From the machine's perspective, all these other machines are in the same neighborhood, even if they're spread all over the world.
+
+Netmaker has many similarities to Tailscale, ZeroTier, and Nebula. What makes Netmaker different is its speed and flexibility. Netmaker is faster because it uses kernel WireGuard. It is more dynamic because the server and agents are fully configurable, which lets you handle all sorts of different use cases.
+
+How Does Netmaker Work?
+=======================
+
+Netmaker relies on WireGuard to create tunnels between machines. At its core, Netmaker is managing WireGuard across machines to create sensible networks. Technically, Netmaker is two things:
+
+- the admin server, called Netmaker
+- the agent, called Netclient
+
+As the network manager, you interact with the server to create and manage networks and devices. The server holds configurations for these networks and devices, which are retrieved by the netclients (agent). 
+
+The netclient is installed on any machine you would like to add to a given network, whether that machine is a VM, Server, or IoT device. The netclient reaches out to the server, and the server tells it how it should configure the network. By doing this across many machines simultaneously, we create a dynamic, fully configurable virtual networks.
+
+The Netmaker server does not typically route traffic. Otherwise, this would be a hub-and-spoke model, which is very slow. Instead, Netmaker just tells the machines on the network how they can reach each other directly. This is called a *full mesh* network and is much faster. Even if the server goes down, as long as none of the existing machines change substantially, your network will still run just fine.
+
+Use Cases for Netmaker
+=============================
+
+There are many use cases for Netmaker. In fact, you could probably be using it right now. This list is not all-encompassing, but provides a sample of how you might want to use Netmaker. Guided setup for many of these use cases can be found in the :doc:`Using Netmaker <./usage>` documentation. 
+
+ 0. Automate creation of a WireGuard mesh network
+ 1. Create a flat, secure network between cloud environments and data centers
+ 2. Provide secure access to IoT devices, remote servers, and client sites.
+ 3. Secure a home or office network
+ 4. Add a layer of encryption to an existing network 
+ 5. Secure site-to-site connections
+ 6. Manage cryptocurrency proof-of-stake machines 
+ 7. Create a dynamic and secure Kubernetes underlay network

+ 184 - 0
docs/_build/html/_sources/api.rst.txt

@@ -0,0 +1,184 @@
+=============================================
+API Reference
+=============================================
+
+API Usage
+==========================
+
+Most actions that can be performed via API can be performed via UI. We recommend managing your networks using the official netmaker-ui project. However, Netmaker can also be run without the UI, and all functions can be achieved via API calls. If your use case requires using Netmaker without the UI or you need to do some troubleshooting/advanced configuration, using the API directly may help.
+
+
+Authentication
+==============
+API calls must be authenticated via a header of  the format  `-H "Authorization: Bearer <YOUR_SECRET_KEY>"` There are two methods to obtain YOUR_SECRET_KEY:
+1. Using the masterkey. By default, this value is "secret key," but you should change this on your instance and keep it secure. This value can be set via env var at startup or in a config file (config/environments/< env >.yaml). See the [general usage](./USAGE.md) documentation for more details.
+2. Using a JWT recieved for a node. This  can be retrieved by calling the `/api/nodes/<network>/authenticate` endpoint, as documented below.
+
+
+Format of Calls for Curl
+========================
+Requests take the format of `curl -H "Authorization: Bearer <YOUR_SECRET_KEY>" -H 'Content-Type: application/json' localhost:8081/api/path/to/endpoint`
+
+
+API Documentation
+=================
+
+Networks API
+------------
+
+**Get All Networks:** `/api/networks`, `GET` 
+  
+**Create Network:** `/api/network`, `POST` 
+  
+**Get Network:** `/api/networks/{network id}`, `GET`  
+  
+**Update Network:** `/api/networks/{network id}`, `PUT`  
+  
+**Delete Network:** `/api/networks/{network id}`, `DELETE`  
+  
+**Cycle PublicKeys on all Nodes:** `/api/networks/{network id}/keyupdate`, `POST`  
+  
+  
+Networks API Call Examples
+--------------------------  
+  
+**Get All Networks:** `curl -H "Authorization: Bearer YOUR_SECRET_KEY" localhost:8081/api/networks | jq`
+
+**Create Network:** `curl -d '{"addressrange":"10.70.0.0/16","netid":"skynet"}' -H "Authorization: Bearer YOUR_SECRET_KEY" -H 'Content-Type: application/json' localhost:8081/api/networks`
+
+**Get Network:** `curl -H "Authorization: Bearer YOUR_SECRET_KEY" localhost:8081/api/networks/skynet | jq`
+
+**Update Network:** `curl -X PUT -d '{"displayname":"my-house"}' -H "Authorization: Bearer YOUR_SECRET_KEY" -H 'Content-Type: application/json' localhost:8081/api/networks/skynet`
+
+**Delete Network:** `curl -X DELETE -H "Authorization: Bearer YOUR_SECRET_KEY" localhost:8081/api/networks/skynet`
+
+**Cycle PublicKeys on all Nodes:** `curl -X POST -H "Authorization: Bearer YOUR_SECRET_KEY" localhost:8081/api/networks/skynet/keyupdate`
+
+Access Keys API
+---------------
+
+**Get All Keys:** `/api/networks/{network id}/keys`, `GET` 
+  
+**Create Key:** `/api/networks/{network id}/keys`, `GET` 
+  
+**Delete Key:** `/api/networks/{network id}/keys/{keyname}`, `DELETE` 
+  
+  
+Access Keys API Call Examples
+-----------------------------
+   
+**Get All Keys:** `curl -H "Authorization: Bearer YOUR_SECRET_KEY" localhost:8081/api/networks/skynet/keys | jq`
+  
+**Create Key:** `curl -d '{"uses":10,"name":"mykey"}' -H "Authorization: Bearer YOUR_SECRET_KEY" -H 'Content-Type: application/json' localhost:8081/api/networks/skynet/keys`
+  
+**Delete Key:** `curl -X DELETE -H "Authorization: Bearer YOUR_SECRET_KEY" localhost:8081/api/networks/skynet/keys/mykey`
+  
+    
+Nodes API
+---------
+  
+**Get All Nodes:** `/api/nodes`, `GET` 
+  
+**Get Network Nodes:** `/api/nodes/{network id}`, `GET` 
+  
+**Create Node:** `/api/nodes/{network id}`, `POST`  
+  
+**Get Node:** `/api/nodes/{network id}/{macaddress}`, `GET`  
+  
+**Update Node:** `/api/nodes/{network id}/{macaddress}`, `PUT`  
+  
+**Delete Node:** `/api/nodes/{network id}/{macaddress}`, `DELETE`  
+  
+**Check In Node:** `/api/nodes/{network id}/{macaddress}/checkin`, `POST`  
+  
+**Create a Gateway:** `/api/nodes/{network id}/{macaddress}/creategateway`, `POST`  
+  
+**Delete a Gateway:** `/api/nodes/{network id}/{macaddress}/deletegateway`, `DELETE`  
+  
+**Uncordon (Approve) a Pending Node:** `/api/nodes/{network id}/{macaddress}/uncordon`, `POST`  
+  
+**Get Last Modified Date (Last Modified Node in Network):** `/api/nodes/adm/{network id}/lastmodified`, `GET`  
+  
+**Authenticate:** `/api/nodes/adm/{network id}/authenticate`, `POST`  
+  
+  
+Nodes API Call Examples
+----------------------- 
+  
+**Get All Nodes:** `curl -H "Authorization: Bearer YOUR_SECRET_KEY" http://localhost:8081/api/nodes | jq`
+  
+**Get Network Nodes:** `curl -H "Authorization: Bearer YOUR_SECRET_KEY" http://localhost:8081/api/nodes/skynet | jq`
+    
+**Create Node:** `curl  -d  '{ "endpoint": 100.200.100.200, "publickey": aorijqalrik3ajflaqrdajhkr,"macaddress": "8c:90:b5:06:f1:d9","password": "reallysecret","localaddress": "172.16.16.1","accesskey": "aA3bVG0rnItIRXDx","listenport": 6400}' -H 'Content-Type: application/json' -H "authorization: Bearer YOUR_SECRET_KEY" localhost:8081/api/nodes/skynet`
+    
+**Get Node:** `curl -H "Authorization: Bearer YOUR_SECRET_KEY" http://localhost:8081/api/nodes/skynet/{macaddress} | jq`  
+  
+**Update Node:** `curl -X PUT -d '{"name":"laptop1"}' -H 'Content-Type: application/json' -H "authorization: Bearer YOUR_SECRET_KEY" localhost:8081/api/nodes/skynet/8c:90:b5:06:f1:d9`
+  
+**Delete Node:** `curl -X DELETE -H "authorization: Bearer YOUR_SECRET_KEY" localhost:8081/api/skynet/nodes/8c:90:b5:06:f1:d9`
+  
+**Create a Gateway:** `curl  -d  '{ "rangestring": "172.31.0.0/16", "interface": "eth0"}' -H 'Content-Type: application/json' -H "authorization: Bearer YOUR_SECRET_KEY" localhost:8081/api/nodes/skynet/8c:90:b5:06:f1:d9/creategateway`
+  
+**Delete a Gateway:** `curl -X DELETE -H "authorization: Bearer YOUR_SECRET_KEY" localhost:8081/api/nodes/skynet/8c:90:b5:06:f1:d9/deletegateway`
+  
+**Approve a Pending Node:** `curl -X POST -H "authorization: Bearer YOUR_SECRET_KEY" localhost:8081/api/nodes/skynet/8c:90:b5:06:f1:d9/approve`
+  
+**Get Last Modified Date (Last Modified Node in Network):** `curl -H "authorization: Bearer YOUR_SECRET_KEY" localhost:8081/api/nodes/adm/skynet/lastmodified`
+
+**Authenticate:** `curl -d  '{"macaddress": "8c:90:b5:06:f1:d9", "password": "YOUR_PASSWORD"}' -H 'Content-Type: application/json' localhost:8081/api/nodes/adm/skynet/authenticate`
+  
+
+Users API
+-----------------------
+  
+**Note:** Only able to create Admin user at this time. The "user" is only used by the `user interface <https://github.com/gravitl/netmaker-ui>`_ to authenticate the  single  admin user.
+
+**Get User:** `/api/users/{username}`, `GET`  
+  
+**Update User:** `/api/users/{username}`, `PUT`  
+  
+**Delete User:** `/api/users/{username}`, `DELETE`  
+  
+**Check for Admin User:** `/api/users/adm/hasadmin`, `GET` 
+  
+**Create Admin User:** `/api/users/adm/createadmin`, `POST` 
+  
+**Authenticate:** `/api/users/adm/authenticate`, `POST` 
+  
+  
+Users API Calls Examples
+------------------------
+  
+**Get User:** `curl -H "Authorization: Bearer YOUR_SECRET_KEY" http://localhost:8081/api/users/{username} | jq`
+
+**Update User:** `curl -X PUT -d '{"password":"noonewillguessthis"}' -H 'Content-Type: application/json' -H "authorization: Bearer YOUR_SECRET_KEY" localhost:8081/api/users/{username}`
+  
+**Delete User:** `curl -X DELETE -H "authorization: Bearer YOUR_SECRET_KEY" localhost:8081/api/users/{username}`
+  
+**Check for Admin User:** `curl -H "Authorization: Bearer YOUR_SECRET_KEY" http://localhost:8081/api/users/adm/hasadmin`
+  
+**Create Admin User:** `curl -d '{ "username": "smartguy", "password": "YOUR_PASS"}' -H 'Content-Type: application/json' -H "authorization: Bearer YOUR_SECRET_KEY" localhost:8081/api/users/adm/createadmin`
+   
+**Authenticate:** `curl -d  '{"username": "smartguy", "password": "YOUR_PASS"}' -H 'Content-Type: application/json' localhost:8081/api/nodes/adm/skynet/authenticate`
+  
+
+Server Management API
+---------------------
+
+The Server Mgmt. API allows you to add and remove the server from networks.
+
+**Add to Network:** `/api/server/addnetwork/{network id}`, `POST`  
+  
+**Remove from Network:** `/api/server/removenetwork/{network id}`, `DELETE`  
+
+**Add to Network:**  `curl -X POST -H "authorization: Bearer YOUR_SECRET_KEY" localhost:8081/api/server/addnetwork/{network id}`
+
+**Remove from Network:** `curl -X DELETE -H "authorization: Bearer YOUR_SECRET_KEY" localhost:8081/api/server/removenetwork/{network id}`
+
+
+File Server API
+---------------
+  
+**Get File:** `/meshclient/files/{filename}`, `GET`
+  
+**Example:**  `curl localhost:8081/meshclient/files/meshclient`

+ 176 - 0
docs/_build/html/_sources/architecture.rst.txt

@@ -0,0 +1,176 @@
+===============
+Architecture
+===============
+
+.. image:: images/nm-diagram-2.jpg
+   :width: 45%
+   :alt: Netmaker Architecture Diagram
+   :align: center
+    
+
+*Pictured Above: A diagram of Netmaker's Architecture.*
+
+
+Core Concepts
+==============
+
+Familiarity with several core concepts will help when you encounter them later on in the documentation.
+
+WireGuard
+----------
+
+WireGuard is a relatively new but very important technology which was recently added to the Linux kernel. WireGuard creates very fast but simple encrypted tunnels between devices. From the `WireGuard <https://www.wireguard.com/>`_ website, "it might be regarded as the most secure, easiest to use, and simplest VPN solution in the industry."
+
+Previous solutions like OpenVPN and IPSec are considerably more heavy and complex, while being less performant. All existing VPN tunnelling solutions will cause a significant increase in your network latency. WireGuard is the first to achieve near over-the-line network speeds, meaning you see no signigifant performance impact.  With the release of WireGuard, there is little reason to use any other existing tunnel encryption technology.
+
+Mesh Network
+-------------
+
+When we refer to a mesh network in these documents we are typically referring to a "full mesh."
+
+.. image:: images/mesh.png
+   :width: 33%
+   :alt: Full Mesh Network Diagram
+   :align: center
+
+
+A full `mesh network <https://www.bbc.co.uk/bitesize/guides/zr3yb82/revision/2>`_ exists where each machine is able to directly talk to every other machine on the network. For example, on your home network, behind your router, all the computers are likely given private addresses and can reach each other directly.
+
+This is in contrast to a hub-and-spoke network, where each machine must first pass its traffic through a relay server before it can reach other machines.
+
+In certain situations you may either want or need a *partial mesh* network, where only some devices can reach each other directly, and other devices must route their traffic through a relay/gateway. Netmaker can use this model in some use cases where it makes sense. In the diagram at the top of this page, the setup is a partial mesh, because the servers (nodes A-D) are meshed, but then external clients come in via a gateway, and are not meshed.
+
+Mesh networks are generally faster than other topologies, but are also more complicated to set up. WireGuard on its own gives you the means to create encrypted tunnels between devices, but it does not provide a method for setting up a full network. This is where Netmaker comes in.
+
+Netmaker
+---------
+
+Netmaker is a platform built off of WireGuard which enables users to create mesh networks between their devices. Netmaker can create both full and partial mesh networks depending on the use case.
+
+When we refer to Netmaker in aggregate, we are typically referring to Netmaker and the netclient, as well as other supporting services such as CoreDNS, MongoDB, and UI webserver.
+
+From an end user perspective, they typically interact with the Netmaker UI, or even just run the install script for the netclient on their devices. The other components run in the background invisibly. 
+
+Netmaker does a lot of work to set configurations for you, so that you don't have to. This includes things like WireGuard ports, endpoints, public IPs, keys, and peers. Netmaker works to abstract away as much of the network management as possible, so that you can just click to create a network, and click to add a machine to a network. That said, every machine (node) is different, and may require special configuration. That is why, while Netmaker sets practical default settings, everything within Netmaker is fully configurable.
+
+Node
+------
+
+A machine in a Netmaker network, which is managed by the Netclient, is referred to as a Node, as you will see in the UI. A Node can be a VM, a bare metal server, a desktop computer, an IoT device, or any other number of internet-connected machines on which the netclient is installed. A node is simply an endpoint in the network, which can send traffic to all the other nodes, and recieve traffic from all of the other nodes.
+
+SystemD
+-------
+
+SystemD is a system service manager for a wide array of Linux operating systems. Not all Linux distributions have adopted systemd, but, for better or worse, it has become a fairly common standard in the Linux world. That said, any non-Linux operating system will not have systemd, and many Linux/Unix distributionshave alternative system service managers.
+
+Netmaker's netclient, the agent which controls networking on all nodes, relies heavily on systemd as of version 0.3. This reliance is being reduced but is currently a core dependency, causing most of the limitations and incompatibilities. As Netmaker evolves, systemd will become just one of the possible service management options, allowing the netclient to be run on a wider array of devices.
+
+
+Components
+===========
+
+Netmaker consists of several core components, which are explained in high-level technical detail below.
+
+Netmaker Server
+------------------
+
+The Netmaker server is, at its core, a golang binary. Source code can be found `on GitHub <https://github.com/gravitl/netmaker>`_. The binary, by itself can be compiled for most systems. If you need to run the Netmaker server on a particular system, it likely can be made to work. In typical deployments, it is run as a Docker container. It can also be run as a systemd service as outlined in the non-docker install guide.
+
+The Netmaker server acts as an API to the front end, and as a GRPC server to the machines in the network. GRPC is much faster and more efficient than standard API calls, which increases the speed of transactions. For this reason, the Netmaker server exposes two ports: The default for the API is 8081, and the default for GRPC is 50051. Either the API or the GRPC server can be disabled on any given Netmaker instance can be disabled, allowing you to deploy two different servers for managing the API (which is largely for the admin's use) and GRPC (which is largely for the nodes' use).
+
+Most server settings are configurable via a config file, or by environment variables (which take precedence). If the server finds neither of these, it sets sensible defaults, including things like the server's reachable IP, ports, and which "modes" to run in.
+
+These modes include client mode and dns mode. Either of these can be disabled but are enabled by default. Client mode allows you to treat the Netmaker host machine (operating system) as a network Node, installing the netclient and controlling the host network. DNS mode has the server write config settings for CoreDNS, a separate component and nameserver, which picks up the config settings to manage node DNS.
+
+The Netmaker server interacts with (as of v0.3) a MongoDB instance, which holds information about nodes, networks, users, and other important data. This data is configuration data. For the most part, Netmaker serves configuration data to Nodes, telling them how they should configure themselves. The Netclient is the agent that actually does that configuration.
+
+
+Netclient
+----------------
+
+The netclient is, at its core, a golang binary. Source code can be found in the netclient folder of the Netmaker `GitHub Repository <https://github.com/gravitl/netmaker/tree/master/netclient>`_. The binary, by itself, can be compiled for most systems. However, this binary is designed to manage a certain number of Operating Systems. As of version 0.3, it requires systemd in order to manage the host system appropriately. It may be installable, and it may even make the machine a part of the mesh network, but it will not function in entirely (see Compatible Systems for more info) without systemd.
+
+The netclient is installed via a simple bash script, which pulls the latest binary and runs install command.
+
+The install command registers the machine with the Netmaker server using sensible defaults, which can be overridden with a config file or environment variables. Assuming the netclient has a valid key (or the network allows manual node signup), it will be registered in the Netmaker network, which will return configuration details about how to set up the local network. 
+
+The netclient then sets itself up in systemd, and configures WireGuard. At this point it should be part of the network.
+
+On a periodic basis (systemd timer), the netclient performs a "check in." It will authenticate with the server, and check to see if anything has changed in the network. It will also post changes about its own local configuration if there. If there has been a change, the server will return new configurations and the netclient will reconfigure the network.
+
+The check in process is what allows Netmaker to create dynamic mesh networks. As nodes are added to, removed from, and modified on the network, other nodes are notified, and make appropriate changes.
+
+
+MongoDB
+--------
+
+As of v0.3, Netmaker uses MongoDB as its database, and interacts with a MongoDB instance to store and retrieve information about nodes, networks, and users. Netmaker is rapidly evolving, and MongoDB provides a flexible database structure that accelerates development. However, MongoDB is also the heaviest component of Netmaker (high cpu/memory consumption), and is set to be replaced by a lighter-weight, SQL-based database in the future.
+
+Netmaker UI
+---------------
+
+The Netmaker UI is a ReactJS-based static website which can be run on top of standard webservers such as Apache and Nginx. Source code can be found `here <https://github.com/gravitl/netmaker-ui>`_. In a typical configuration, the Netmaker UI is run on Nginx as a Docker container.
+
+Netmaker can be used in its entirety without the UI, but the UI makes things a lot easier for most users. It has a sensible flow and layout for managing Networks, Nodes, Access Keys, and DNS.
+
+
+CoreDNS
+--------
+
+v0.3 introduced the concept of private DNS management for nodes. This requires a nameserver, and CoreDNS is the chosen nameserver. CoreDNS is lightweight and extensible. CoreDNS loads dns settings from a simple file, managed by Netmaker, and serves out DNS info for managed nodes. DNS can be tricky, and DNS management is currently only supported on a small set of devices, specifically those running systemd-resolved. However, the Netmaker CoreDNS instance can be added manually as a nameserver to other devices. DNS mode can also be turned off.
+
+Worth considering is that CoreDNS requires port 53 on the Netmaker host system, which may cause conflicts depending on your operating system. This is explained in the :doc:`Server Installation <./server-installation>` guide.
+
+
+Technical Process
+====================
+
+Below is a high level, step-by-step overview of the flow of communications within Netmaker (assuming Netmaker has already been installed):
+
+1. Admin creates a new network with a subnet, for instance 10.10.10.0/24
+2. Admin creates an access key for signing up new nodes
+3. Both of the above requests are routed to the server via an API call from the front end
+4. Admin runs the netclient install script on any given node (machine).
+5. Netclient decodes key, which contains the GRPC server location and port
+6. Netclient retrieves/sets local information, including open ports for WireGuard, public IP, and generating key pairs for peers
+7. Netclient reaches out to GRPC server with this information, authenticating via access key.
+8. Netmaker server verifies information and creates the node, setting default values for any missing information. 
+9. Timestamp is set for the network (see #16). 
+10. Netmaker returns settings as response to netclient. Some settings may be added or modified based on the network.
+11. Netclient recieves response. If successful, it takes any additional info returned from Netmaker and configures the local system/WireGuard
+12. Netclient sends another request to Netmaker's GRPC server, this time to retrieve the peers list (all other clients in the network).
+13. Netmaker sends back peers list, including current known configurations of all nodes in network.
+14. Netclient configures WireGuard with this information. At this point, the node is fully configured as a part of the network and should be able to reach the other nodes via private address.
+15. Netclient begins daemon (system timer) to run check in's with the server. It awaits changes, reporting local changes, and retrieving changes from any other nodes in the network.
+16. Other netclients on the network, upon checking in with the Netmaker server, will see that the timestamp has updated, and they will retrieve a new peers list, completing the update cycle.
+
+
+Compatible Systems for Netclient
+==================================
+
+To manage a node automatically, the Netmaker client (netclient) requires **systemd-based linux.** Compatible systems include:
+        - Fedora
+        - Ubuntu
+        - Debian
+        - Mint
+        - SUSE
+        - RHEL
+        - Raspian.
+        - Arch
+        - CentOS
+        - CoreOS
+      
+To manage DNS (optional), the node must have systemd-resolved. Systems that have this enabled include:
+        - Arch
+        - Debian
+        - Ubuntu
+        - SUSE
+
+Limitations
+===========
+
+Install limitations mostly include platform-specific limitations, such as needing systemd or systemd-resolved (see above). In addition the Netmaker platform has some additional limitations:
+
+- **Double NAT**: Netmaker is currently unable to route traffic for devices behind a "double NAT".
+- **CGNAT**: Netmaker is currently unable to route traffic for for devices behind a "carrier-grade NAT".
+- **Windows/iPhone/Android**: To reiterate the systemd limitation, Netmaker is not currently configured to support "end user" devices such as Windows desktops and phones generally. In v0.4, Netmaker will introduce external device gateways to allow this traffic (and many other sorts of devices).

+ 126 - 0
docs/_build/html/_sources/client-installation.rst.txt

@@ -0,0 +1,126 @@
+====================
+Client Installation
+====================
+
+This document tells you how to install the netclient on machines that will be a part of your Netmaker network, as well as non-compatible systems.
+
+These steps should be run after the Netmaker server has been created and a network has been designated within Netmaker.
+
+Introduction to Netclient
+===============================
+
+At its heart, the netclient is a simple CLI for managing access to various WireGuard-based networks. It manages WireGuard on the host system, so that you don't have to. Why is this necessary?
+
+If you are setting up a WireGuard-based virtual network, you must configure each machine with very specific settings, so that every machine can reach it, and it can reach every machine. Any changes to the settings of any one of these machines can break those connections. Any machine that is added, removed, or modified on the network requires reconfiguring every peer in the network. This can be very time consuming.
+
+The netmaker server holds configuration details about every machine in your network and how other machines should connect to it.
+
+The netclient agent connects to the server, pushing and pulling information when the network (or its local configuration) changes. 
+
+The netclient agent then configures WireGuard (and other network properties) locally, so that the network stays intact.
+
+Modes and System Compatibility
+==================================
+
+**Note: If you would like to connect non-Linux/Unix machines to your network such as phones and Windows desktops, please see the documentation on External Clients**
+
+The netclient can be run in a few "modes". System compatibility depends on which modes you intend to use. These modes can be mixed and matched across a network, meaning all machines do not have to run with the same "mode."
+
+CLI
+------------
+
+In its simplest form, the netclient can be treated as just a simple, manual, CLI tool, which a user can call to configure the machine. The cli can be compiled from source code to run on most systems, and has already been compiled for x86 and ARM devices.
+
+As a CLI, the netclient should function on any Linux or Unix based system that has the wireguard utility (callable with **wg**) installed.
+
+Daemon
+----------
+
+The netclient is intended to be run as a system daemon. This allows it to automatically retrieve and send updates. To do this, the netclient can install itself as a systemd service.
+
+This requires a systemd-based linux operating system.
+
+If running the netclient on a non-systemd system, it is recommended to manually configure the netclient as a daemon using whatever method is acceptable on the chosen operating system.
+
+Private DNS Management
+-----------------------
+
+To manage private DNS, the netclient relies on systemd-resolved (resolvectl). Absent this, it cannot set private DNS for the machine.
+
+A user may choose to manually set a private DNS nameserver of <netmaker server>:53. However, beware, as netmaker sets split dns, and the system must be configured properly. Otherwise, this nameserver may break your local DNS.
+
+Prerequisites
+=============
+
+To obtain the netclient, go to the GitHub releases: https://github.com/gravitl/netmaker/releases
+
+**For netclient cli:** Linux/Unix with WireGuard installed (wg command available)
+
+**For netclient daemon:** Systemd Linux + WireGuard
+
+**For Private DNS management:** Resolvectl (systemd-resolved)
+
+Configuration
+===============
+
+The CLI has information about all commands and variables. This section shows the "help" output for these commands as well as some additional reference.
+
+CLI Reference
+--------------------
+``sudo netclient --help``
+
+.. literalinclude:: ./examplecode/netclient-help.txt
+  :language: YAML
+
+
+``sudo netclient join --help``
+
+.. literalinclude:: ./examplecode/netclient-join.txt
+  :language: YAML
+
+
+Config File Reference
+------------------------
+
+There is a config file for each node under /etc/netconfig-<network name>. You can change these values and then set "postchanges" to "true", or go to the CLI and run ``netclient push -n <network>``
+
+
+.. literalinclude:: ./examplecode/netconfig-example.yml
+  :language: YAML
+
+
+Installation
+======================
+
+
+To install netmaker, you need a server token for a particular network, unless you're joining a network that allows manual signup, in which case you can join without a token, but the server will quarantine the machine until the admin approves it.
+
+An admin creates a token in the ACCESS KEYS section of the UI. Upon creating a token, it generates 3 values:
+
+**Access Key:** The secret key to authenticate as a node in the network
+
+**Access Token:** The secret key plus information about how to access the server (addresses, ports), all decoded by the netclient to register with the server
+
+**Install Command:** A short script that will obtain the netclient binary, register with the server, and join the network, all in one
+
+For first time installations, you can run the Install Command. For additional networks, simply run ``netclient join -t <access token>``. The raw access key will not be needed unless there are special circumstances, mostly troubleshooting incorrect information in the token (you can instead manually specify the server location).
+
+
+Managing Netclient
+=====================
+
+Viewing Logs
+---------------
+
+Making Updates
+----------------
+
+Adding/Removing Networks
+---------------------------
+
+Uninstalling
+---------------
+
+Troubleshooting
+-----------------
+

+ 77 - 0
docs/_build/html/_sources/conduct.rst.txt

@@ -0,0 +1,77 @@
+===============
+Code of Conduct
+===============
+
+Our Pledge
+==========
+
+In the interest of fostering an open and welcoming environment, we as
+contributors and maintainers pledge to making participation in our project and
+our community a harassment-free experience for everyone, regardless of age, body
+size, disability, ethnicity, gender identity and expression, level of experience,
+nationality, personal appearance, race, religion, or sexual identity and
+orientation.
+
+Our Standards
+=============
+
+Examples of behavior that contributes to creating a positive environment
+include:
+
+* Using welcoming and inclusive language
+* Being respectful of differing viewpoints and experiences
+* Gracefully accepting constructive criticism
+* Focusing on what is best for the community
+* Showing empathy towards other community members
+
+Examples of unacceptable behavior by participants include:
+
+* The use of sexualized language or imagery and unwelcome sexual attention or advances
+* Trolling, insulting/derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or electronic address, without explicit permission
+* Other conduct which could reasonably be considered inappropriate in a professional setting
+
+Our Responsibilities
+====================
+
+Project maintainers are responsible for clarifying the standards of acceptable
+behavior and are expected to take appropriate and fair corrective action in
+response to any instances of unacceptable behavior.
+
+Project maintainers have the right and responsibility to remove, edit, or
+reject comments, commits, code, wiki edits, issues, and other contributions
+that are not aligned to this Code of Conduct, or to ban temporarily or
+permanently any contributor for other behaviors that they deem inappropriate,
+threatening, offensive, or harmful.
+
+Scope
+=====
+
+This Code of Conduct applies both within project spaces and in public spaces
+when an individual is representing the project or its community. Examples of
+representing a project or community include using an official project e-mail
+address, posting via an official social media account, or acting as an appointed
+representative at an online or offline event. Representation of a project may be
+further defined and clarified by project maintainers.
+
+Enforcement
+===========
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported by contacting the project team at [email protected]. All
+complaints will be reviewed and investigated and will result in a response that
+is deemed necessary and appropriate to the circumstances. The project team is
+obligated to maintain confidentiality with regard to the reporter of an incident.
+Further details of specific enforcement policies may be posted separately.
+
+Project maintainers who do not follow or enforce the Code of Conduct in good
+faith may face temporary or permanent repercussions as determined by other
+members of the project's leadership.
+
+Attribution
+===========
+
+This Code of Conduct is adapted from the `Contributor Covenant <https://contributor-covenant.org>`_, version 1.4,
+available `here <https://contributor-covenant.org/version/1/4>`_.
+

+ 8 - 0
docs/_build/html/_sources/contact.rst.txt

@@ -0,0 +1,8 @@
+=======
+Contact
+=======
+
+If you need help, try the discord or open a GitHub ticket.
+
+Email: [email protected]
+Discord: https://discord.gg/zRb9Vfhk8A

+ 26 - 0
docs/_build/html/_sources/contribute.rst.txt

@@ -0,0 +1,26 @@
+===========
+Contribute
+===========
+
+Submitting an Issue
+====================
+
+Submitting an Enhancement
+==========================
+
+Contributing Code
+==================
+
+Forking
+----------
+
+Building
+-----------
+
+Testing
+----------
+
+Submitting a PR
+-----------------
+
+

+ 19 - 0
docs/_build/html/_sources/external-clients.rst.txt

@@ -0,0 +1,19 @@
+================
+External Clients
+================
+
+Introduction
+===============
+
+Netmaker allows for "external clients" to reach into a network and access services via an Ingress Gateway. So what is an "external client"? An external client is any machine which cannot or should not be meshed. This can include:
+        - Phones
+        - Laptops
+        - Desktops
+
+An external client is not "managed," meaning it does not automatically pull the latest network configuration, or push changes to its configuration. Instead, it uses a generated WireGuard config file to access the designated **Ingress Gateway**, which **is** a managed server (running netclient). This server then forwards traffic to the appropriate endpoint, acting as a middle-man/relay.
+
+By using this method, you can hook any machine into a netmaker network that can run WireGuard.
+
+It is recommended to run the netclient where compatible, but for all other cases, a machine can be configured as an external client.
+
+Important to note, an external client is not **reachable** by the network, meaning the client can establish connections to other machines, but those machines cannot independently establish a connection back. The External Client method should only be used in use cases where one wishes to access resource runnin on the virtual network, and **not** for use cases where one wishes to make a resource accessible on the network. For that, use netclient.

+ 148 - 0
docs/_build/html/_sources/getting-started.rst.txt

@@ -0,0 +1,148 @@
+===============
+Getting Started
+===============
+
+Netmaker is a tool for creating and managing virtual overlay networks. If you have servers spread across multiple locations, data centers, or clouds, this platform can make life easier. Netmaker takes all those machines and puts them on a single, secure, flat network so that they can all talk to each other easily and securely. It's like a VPC but of arbitrary computers.
+
+Netmaker can be compared to and covers use cases similar to Tailscale, ZeroTier, or Nebula, but Netmaker does more than that, while being faster, more dynamic and more flexible.
+
+Netmaker uses kernel WireGuard to create encrypted tunnels between every node in your virtual network. Netmaker's `netclient` agent is self-updating and pulls any necessary changes (such as new peers) from the main server. 
+
+Use Cases
+=========
+ 1. Create a flat, secure network between multiple/hybrid cloud environments
+ 2. Integrate central and edge services
+ 3. Secure a home or office network while providing remote connectivity
+ 4. Manage cryptocurrency proof-of-stake machines
+ 6. Provide an additional layer of security on an existing network
+ 7. Encrypt Kubernetes inter-node communications
+ 8. Secure site-to-site connections
+
+
+Compatible Systems
+==================
+
+To manage a server automatically, Netmaker requires **systemd-based linux.** Compatible systems include:
+        - Fedora
+        - Ubuntu
+        - Debian
+        - Mint
+        - SUSE
+        - RHEL
+        - Raspian.
+        - Arch
+        - CentOS
+        - CoreOS
+      
+To manage DNS (optional), the server must have systemd-resolved. Systems that have this enabled include:
+        - Arch
+        - Debian
+        - Ubuntu
+        - SUSE
+
+
+In future releases, we will support other platforms such as Windows, MacOS, iOS, Android, and more. 
+
+Video Tutorials and Articles:
+
+
+
+Quick Start
+===========
+
+[Intro/Overview Video Tutorial](https://youtu.be/PWLPT320Ybo)  
+[Site-to-Site Video Tutorial](https://youtu.be/krCKBJhwwDk)  
+
+### Note about permissions
+The default installation requires special privileges on the server side, because Netmaker will control the local kernel Wireguard. This can be turned off and run in non-privileged mode if necessary (but disables some features). For more details, see the **Usage** docs.
+
+### Prereqs
+ 1. A running linux server to host Netmaker, with an IP reachable by your computers (Debian-based preferred but not required).
+ 2. Linux installed on the above server (Debian-based preferred but not required).
+ 3. Install Docker and Docker Compose if running in Docker Mode (see below).
+ 4. System dependencies installed:
+	 - Docker (if running in default Docker mode. DO NOT use snap install for docker.)
+	 - Docker Compose
+	 - Wireguard + Resolvectl (if running in default Client mode)
+
+#### CoreDNS Preparation
+v0.3 introduces CoreDNS as a private nameserver. To run CoreDNS on your server host, you must disable systemd-resolved to open port 53: 
+1. systemctl stop systemd-resolved
+2. systemctl disable systemd-resolved
+3. vim /etc/systemd/resolved.conf
+	 - uncomment **DNS=** and add 8.8.8.8 or whatever is your preference
+	 - uncomment **DNSStubListener=** and set to **"no"**
+ 4. sudo ln -sf /run/systemd/resolve/resolv.conf /etc/resolv.conf
+
+
+
+### Launch Netmaker
+Note, this installs Netmaker with CoreDNS and a Netclient (privileged).  If you want to run the server non-privileged or without CoreDNS, see the advanced usage docs. 
+
+1. Clone this repo or just copy contents of "docker-compose.yml" to your Netmaker server (from prereqs).
+2. In docker-compose.yml, change BACKEND_URL to the public IP of your server.
+3. Run `sudo docker-compose up -d`
+4. Navigate to your server's IP in the browser and you should see the Netmaker UI asking to create a new admin user.
+5. Create a new admin user
+6. You are now ready to begin using Netmaker. 
+
+### Create a Network
+You can also just use the "default" network.
+1. Click "CREATE NETWORK" in the upper left of your console
+2. Enter a valid address range, e.g. 10.11.12.0/24
+3. Enter a name such as "homenet"
+4. Additional options:
+	- **Dual Stack**: Machines will recieve a private IPv6 address in addition to their IPv4 address.
+	- **Local:** Will use local address range for endpoints instead of public. Use Case: Home or Office network where most devices do not have public IP's. In this case you can create a gateway into the network after creating the Local Network.
+
+After Network creation, you can edit the network in the NETWORK DETAILS pane, modifying the address range and default options. You can also toggle on **Allow Node Signup Without Keys**, which makes the next step unnecessary, but allows anyone to create a node in your network, which will be cordoned in pending state.
+
+### Create Keys
+1. Click the "ACCESS KEYS" tab
+2. Click "ADD NEW ACCESSS KEY"
+3. Give your key a name and number of uses
+4. Several values will be displayed. Save these somewhere, as they will only be displayed once:
+	- **Access Key:** Use only in special edge cases where server connection string must be modified
+	- **Access Token:** Use on machines that already have the netclient utility
+	- **Install Command:** Use on machines that do not have the netclient utility
+
+### Install Agent:
+For machines **without** netclient, run the install command (from above): `curl -sfL https://raw.githubusercontent.com/gravitl/netmaker/v0.3/netclient-install.sh | KEY=<your access key> sh -`  
+For machines **with** netclient run the following (with access token from above): `sudo netclient -c install -t <access token>`
+For networks with **manual signup** enabled (see above), install using the network name: `sudo netclient -c install -n <network name>`
+
+### Manage Nodes
+Your machines should now be visible in the control pane. 
+**Modify nodes:** Click the pencil icon in the NODES pane to modify details like WireGuard port, address, and node name. You can also **DELETE** nodes here and they will lose network access.
+**Approve nodes:** If a node is in pending state (signed up without key), you can approve it. An icon will appear for pending nodes that need approval.
+
+**Gateway Mode:** Click the Gateway icon to enable gateway mode on a given node. A popup will allow you to choose an existing network, or enter a custom address range.
+*Example: You create a network in netmaker called Homenet. It has several machines on your home server. You create another network called Cloudnet. It has several machines in AWS. You have one server (server X) which is added to both networks. On Cloudnet, you make Server X a gateway to Homenet. Now, the cloudnet machines have access to your homenet machines. via  Server X.*
+
+*On Homenet, you add Server Y, a machine in AWS, and make it a gateway to a custom address range 172.16.0.0/16. The machines on your home network now have access to any AWS machines in that address range via Server Y*
+
+### Manage DNS
+On the DNS tab you can create custom DNS entries for a given network.
+
+ 1. All dns entries will be *postfixed* with a private TLD of the network name, for example, ".mynet"
+ 2. Default DNS is created for node name + TLD, for instance, node-c42wt.mynet. This is not editable.
+ 3. Click ADD ENTRY to add custom DNS
+	 - You can click CHOOSE NODE to direct DNS to a specific node in the network
+	 - You can also specify any custom address you would like, which can be outside the network (for instance, the IP for google.com)
+	 - Add a dns entry name, which will be postfixed with the network TLD. E.g. if you enter "privateapi.com", it will become "privateapi.com.networkname" 
+
+### Uninstalling Client
+To uninstall the client from a network: `sudo netclient -c remove -n < networkname >`
+To uninstall entirely, run the above for each network,  and then run `sudo rm -rf /etc/netclient`
+
+### Uninstralling Netmaker
+To uninstall the netmaker server, simply run `docker-compose down`
+
+#### LICENSE
+
+Netmaker's source code and all artifacts in this repository are freely available. All versions are published under the Server Side Public License (SSPL), version 1, which can be found here: [LICENSE.txt](./LICENSE.txt).
+
+#### CONTACT
+
+Email: [email protected]  
+Discord: https://discord.gg/zRb9Vfhk8A

+ 160 - 0
docs/_build/html/_sources/index.rst.txt

@@ -0,0 +1,160 @@
+.. Netmaker documentation master file, created by
+   sphinx-quickstart on Fri May 14 08:51:40 2021.
+   You can adapt this file completely to your liking, but it should at least
+   contain the root `toctree` directive.
+
+
+.. image:: images/netmaker.png
+   :width: 100%
+   :alt: Netmaker WireGuard
+   :align: center
+
+.. role:: raw-html(raw)
+    :format: html
+
+:raw-html:`<br />`
+
+=======================================
+Welcome to the Netmaker Documentation
+=======================================
+
+
+Netmaker is a platform for creating and managing fast, secure, and dynamic virtual overlay networks using WireGuard.
+
+This documentation covers Netmaker's :doc:`installation <./server-installation>`, :doc:`usage <./usage>`, :doc:`troubleshooting <./support>`, and customization, as well as reference documents for the :doc:`API <./api>`, UI and Agent configuration. All of the `source code <https://github.com/gravitl/netmaker>`_ for Netmaker is on GitHub.
+
+
+.. :raw-html:`<br />`
+
+.. .. raw:: html
+..   :file: youtube-1.html
+
+About
+------
+A quick overview of Netmaker, explaining what it is, how it works, and why you should be using it.
+
+.. toctree::
+   :maxdepth: 2
+   
+   about
+
+Architecture
+---------------
+
+A technical overview of Netmaker, including design decisions and limitations.
+
+.. toctree::
+   :maxdepth: 2
+   
+   architecture
+
+Quick Start
+---------------
+
+A quick start guide to getting up and running with Netmaker and WireGuard as quickly as possible.
+
+.. toctree::
+   :maxdepth: 2
+
+   quick-start
+
+Server Installation
+--------------------
+
+A detailed guide to installing the Netmaker server (API, DB, UI, DNS), and configuration options.
+
+.. toctree::
+   :maxdepth: 2
+   
+   server-installation
+
+Client Installation
+--------------------
+
+A detailed guide to installing the Netmaker agent (netclient) on devices and configuration options.
+
+.. toctree::
+   :maxdepth: 2
+   
+   client-installation
+
+External Clients
+--------------------
+
+A detailed guide to give clients outside of the Netmaker network access to network resources.
+
+.. toctree::
+   :maxdepth: 2
+   
+   external-clients
+
+Guides
+----------------
+
+A handful of guides for use cases including site-to-site, Kubernetes, private DNS, and more.
+
+.. toctree::
+   :maxdepth: 2
+   
+   usage
+
+API Reference
+---------------
+
+A reference document for the Netmaker Server API, and example API calls for various use cases.
+
+**Coming Soon:** Swagger Documentation
+
+.. toctree::
+   :maxdepth: 1
+
+   api
+
+Troubleshooting
+----------------
+
+Help with common Netmaker/netclient issues.
+
+.. toctree::
+   :maxdepth: 2
+
+   troubleshoot
+
+
+Support
+----------------
+
+Where to go for help, and a FAQ.
+
+.. toctree::
+   :maxdepth: 2
+
+   support
+
+
+Contributing
+-----------------
+
+A guide on how to contribute to the Netmaker project.
+
+.. toctree:: 
+
+        contribute.rst
+
+Code of Conduct
+-----------------
+
+A statement on our expectations and pledge to the community.
+
+.. toctree:: 
+
+        conduct.rst
+
+Licensing
+---------------
+
+A link to the Netmaker license.
+
+.. toctree:: 
+
+        license.rst

+ 47 - 0
docs/_build/html/_sources/introduction.rst.txt

@@ -0,0 +1,47 @@
+===============
+Introduction
+===============
+
+Netmaker is a tool for creating and managing virtual overlay networks. If you have servers spread across multiple locations, data centers, or clouds, this platform can make life easier. Netmaker takes all those machines and puts them on a single, secure, flat network so that they can all talk to each other easily and securely. It's like a VPC but of arbitrary computers.
+
+Netmaker can be compared to and covers use cases similar to Tailscale, ZeroTier, or Nebula, but Netmaker does more than that, while being faster, more dynamic and more flexible.
+
+Netmaker uses kernel WireGuard to create encrypted tunnels between every node in your virtual network. Netmaker's `netclient` agent is self-updating and pulls any necessary changes (such as new peers) from the main server. 
+
+Use Cases
+=========
+ 1. Create a flat, secure network between multiple/hybrid cloud environments
+ 2. Integrate central and edge services
+ 3. Secure a home or office network while providing remote connectivity
+ 4. Manage cryptocurrency proof-of-stake machines
+ 6. Provide an additional layer of security on an existing network
+ 7. Encrypt Kubernetes inter-node communications
+ 8. Secure site-to-site connections
+
+
+Compatible Systems
+==================
+
+To manage a server automatically, Netmaker requires **systemd-based linux.** Compatible systems include:
+        - Fedora
+        - Ubuntu
+        - Debian
+        - Mint
+        - SUSE
+        - RHEL
+        - Raspian.
+        - Arch
+        - CentOS
+        - CoreOS
+      
+To manage DNS (optional), the server must have systemd-resolved. Systems that have this enabled include:
+        - Arch
+        - Debian
+        - Ubuntu
+        - SUSE
+
+
+In future releases, we will support other platforms such as Windows, MacOS, iOS, Android, and more. 
+
+Limitations
+===========

+ 6 - 0
docs/_build/html/_sources/license.rst.txt

@@ -0,0 +1,6 @@
+=======
+License
+=======
+
+Netmaker's source code and all artifacts in this repository are freely available. All versions are published under the Server Side Public License (SSPL), version 1, which can be found `here <https://raw.githubusercontent.com/gravitl/netmaker/master/LICENSE.txt>`_.
+

+ 137 - 0
docs/_build/html/_sources/quick-start.rst.txt

@@ -0,0 +1,137 @@
+===========
+Quick Start
+===========
+
+Introduction
+==============
+
+This is a guide to getting up and running with Netmaker as quickly as possible. 
+
+By default, Netmaker ships with DNS Mode, Client Mode, and Secure GRPC enabled. However, these features require special permissions and are not necessary for a simple setup, so we are going to deploy without them. To learn more about enabling these features, check out the :doc:`installation docs <./server-installation>`.
+
+Prerequisites
+==================
+ #. A Linux server to host Netmaker, with an external IP reachable by your nodes (will be referred to as **your-host** in  document).
+ #. Docker and Docker Compose installed on the above server. Follow the official `Docker instructions <https://docs.docker.com/engine/install/>`_ for installing Docker and Docker Compose on your system.
+ #. All network nodes should be systemd-based (see Compatibility under :doc:`Architecture <./architecture>` docs)
+
+Install
+==============
+#. ``ssh root@your-host``
+#. ``wget -O docker-compose.yml https://raw.githubusercontent.com/gravitl/netmaker/master/scripts/docker-compose.slim.yml``
+#. ``sed -i ‘s/HOST_IP/< Insert your-host IP Address Here >/g’ docker-compose.yml``
+#. ``docker-compose up -d``
+
+Navigate to the IP address of your host in the browser. You should see the below screen. If not, please see the Quick Start section of the :doc:`troubleshooting <./support>` docs.
+
+.. image:: images/create-user.png
+   :width: 80%
+   :alt: Create User Screen
+   :align: center
+
+Setup
+=================
+
+#. Create your admin user, with a username and password.
+#. Login with your new user
+#. Examine the **default** network. Click on DEFAULT under NETWORK DETAILS
+
+.. image:: images/default-net.png
+   :width: 80%
+   :alt: Create User Screen
+   :align: center
+
+This displays information about the **default** network, which is created on server startup. You can delete this network if you do not need it, but for standard use cases this network should be enough to get started. Nodes will get an address from the network address range (ADDRESSRANGE). If the range conflicts with a pre-existing private network on your devices, you may want to change this, or make a new network instead. Nodes will also get default settings from here for unset configurations. For instance, the DEFAULTKEEPALIVE field will set the PersistenKeepAlive for nodes.
+
+To get started quickly, we can just use the existing default network.
+
+Create Key
+------------
+
+#. Click on the ACCESS KEYS tab and select the DEFAULT network.
+#. Click ADD NEW ACCESS KEY
+#. Give it a name (ex: "mykey") and a number of uses (ex: 25)
+#. Click CREATE KEY (**Important:** Do not click out of the following screen until you have saved your key details. It will appear only once.)
+#. Copy the bottom command under "Your agent install command with access token" and save it somewhere locally. E.x: ``curl -sfL https://raw.githubusercontent.com/gravitl/netmaker/v0.3/scripts/netclient-install.sh | KEY=vm3ow4thatogiwnsla3thsl3894ths sh -``
+
+.. image:: images/access-key.png
+   :width: 80%
+   :alt: Access Key Screen
+   :align: center
+
+You will use this command to install the netclient on your nodes. There are three different values for three different scenarios: 
+
+* The **Access Key** value is the secret string that will allow your node to authenticate with the Netmaker network. This can be used with existing netclient installations where additional configurations (such as setting the server IP manually) may be required. This is not typical. E.g. ``netclient -c install -k <access key> -s 1.2.3.4 -p 50052``
+* The **Access Token** value is a base64 encoded string that contains the server IP and grpc port, as well as the access key. This is decoded by the netclient and can be used with existing netclient installations like this: ``netclient -c install -t <access token>``. You should use this method for adding a network to a node that is already on a network. For instance, Node A is in the **mynet** network and now you are adding it to **default**.
+* The **install command** value is a curl command that can be run on Linux systems. It is a simple script that downloads the netclient binary and runs the install command all in one.
+  
+Networks can also be enabled to allow nodes to sign up without keys at all. In this scenario, nodes enter a "pending state" and are not permitted to join the network until an admin approves them.
+
+Deploy Nodes
+=================
+
+1. SSH to each machine 
+2. ``sudo su -``
+3. **Prerequisite Check:** Every Linux machine on which you run the netclient must have WireGuard and systemd installed
+
+  * ``which wg`` (should show wg binary present)
+  * ``pidof systemd && echo "systemd found" || echo "systemd not found"``
+
+4. Run the install command, Ex: ``curl -sfL https://raw.githubusercontent.com/gravitl/netmaker/v0.5/scripts/netclient-install.sh | KEY=vm3ow4thatogiwnsla3thsl3894ths sh -``
+
+You should get output similar to the below. The netclient retrieves local settings, submits them to the server for processing, and retrieves updated settings. Then it sets the local network configuration. For more information about this process, see the :doc:`client installation <./client-installation>` documentation. If this process failed and you do not see your node in the console (see below), then reference the :doc:`troubleshooting <./troubleshoot>` documentation.
+
+.. image:: images/nc-install-output.png
+   :width: 80%
+   :alt: Output from Netclient Install
+   :align: center
+
+
+.. image:: images/nm-node-success.png
+   :width: 80%
+   :alt: Node Success
+   :align: center
+
+
+Repeat the above steps for every machine you would like to add to your network. You can re-use the same install command so long as you do not run out of uses on your access key (after which it will be invalidated and deleted).
+
+Once installed on all nodes, you can test the connection by pinging the private address of any node from any other node.
+
+
+.. image:: images/ping-node.png
+   :width: 80%
+   :alt: Node Success
+   :align: center
+
+Manage Nodes
+===============
+
+Your machines should now be visible in the control pane. 
+
+.. image:: images/nodes.png
+   :width: 80%
+   :alt: Node Success
+   :align: center
+
+You can view/modify/delete any node by selecting it in the NODES tab. For instance, you can change the name to something more sensible like "workstation" or "api server". You can also modify network settings here, such as keys or the WireGuard port. These settings will be picked up by the node on its next check in. For more information, see Advanced Configuration in the :doc:`Using Netmaker <./usage>` docs.
+
+.. image:: images/node-details.png
+   :width: 80%
+   :alt: Node Success
+   :align: center
+
+
+
+Nodes can be added/removed/modified on the network at any time. Nodes can also be added to multiple Netmaker networks. Any changes will get picked up by any nodes on a given network, and will take aboue ~30 seconds to take effect.
+
+Uninstalling the netclient
+=============================
+
+1. To remove your nodes from the default network, run the following on each node: ``sudo netclient leave -n default``
+2. To remove the netclient entirely from each node, run ``sudo rm -rf /etc/netclient`` (after running the first step)
+
+Uninstralling Netmaker
+===========================
+
+To uninstall Netmaker from the server, simply run ``docker-compose down`` or ``docker-compose down --volumes`` to remove the docker volumes for a future installation.
+

+ 363 - 0
docs/_build/html/_sources/server-installation.rst.txt

@@ -0,0 +1,363 @@
+====================
+Server Installation
+====================
+
+This section outlines installing the Netmaker server, including Netmaker, Netmaker UI, MongoDB, and CoreDNS
+
+Notes on Optional Features
+============================
+
+There are a few key options to keep in mind when deploying Netmaker. All of the following options are enabled by default but can be disabled with a single flag at runtime (see Customization). In addition to these options, there are many more Customizable components which will be discussed later on and help to solve for special challenges and use cases.
+
+**Client Mode:** Client Mode enables Netmaker to control the underlying host server's Network. This can make management a bit easier, because Netmaker can be added into networks via a button click in the UI. This is especially useful for things like Gateways, and will open up additional options in future versions, for instance, allowing Netmaker to easily become a relay server.
+
+Client Mode requires many additional privileges on the host machine, since Netmaker needs to control kernel WireGuard. Because of this, if running in Client Mode, you must run with root privileges and mount many system directories to the Netmaker container. Running without Client Mode allows you to install without privilege escalation and increases the number of compatible systems substantially.
+
+**DNS Mode:** DNS Mode enables Netmaker to write configuration files for CoreDNS, which can be set as a DNS Server for nodes. DNS Mode, paired with a CoreDNS deployment, requires use of port 53. On many linux systems (such as Ubuntu), port 53 is already in use to support local DNS, via systemd-resolved. Running in DNS Mode may require making modifications on the host machine.
+
+**Secure GRPC**: Secure GRPC ensures all communications between nodes and the server are encrypted. Netmaker sets up a default "comms" network that exists only for nodes to connect to the server. It acts as a hub-and-spoke WireGuard network. In the below installation instructions, when port 50555 needs to be open, this is referring to the WireGuard port for Netmaker's GRPC comms. When it is port 50051, secure comms is not enabled. 
+
+When Secure GRPC is enabled, before any nodes can join a Netmaker network, they request to join the comms network, and are given the appropriate WireGuard configs to connect to the server. Then they are able to make requests against the private netmaker endpoint specified for the comms network (10.101.0.1 by default). If switched off, communications are not secure between the hub and nodes over GRPC (it is like http vs https), and likewise, certificates must be added to gain secure communications.
+
+**Agent Backend:** The Agent Backend is the GRPC server (by default running on port 50051). This port is not needed for the admin server. If your use case requires special access configuration, you can run two Netmaker instances, one for the admin server, and one for node access.
+
+**REST Backend:** Similar to the above, the REST backend runs by default on port 8081, and is used for admin API and UI access. By enabling the REST backend while disabling the Agent backend, you can separate the two functions for more restricted environments.
+
+
+System Compatibility
+====================
+
+Both **Client Mode** and **Secure GRPC** require WireGuard to be installed on the host system, and will require elevated privileges to perform network operations..
+
+When both of these features are **disabled**, Netmaker can be run on any system that supports Docker, including Windows, Mac, and Linux, and other systems. With these features disabled, no special privileges are required. Netmaker will only need ports for GRPC (50051 by default), the API (8081 by default), and CoreDNS (53, if enabled).
+
+With Client Mode and/or Secure GRPC **enabled** (the default), Netmaker has the same limitations as the :doc:`netclient <./client-installation>` (client networking agent), because client mode just means that the Netmaker server is also running a netclient. 
+
+These modes require privileged (root) access to the host machine. In addition, Client Mode requires multiple host directory mounts. WireGuard must be installed, the system must be systemd Linux (see :doc:`compatible systems <./architecture>` for more details).
+
+To run a non-docker installation, you must run the Netmaker binary, CoreDNS binary, MongoDB, and a web server directly on the host. This requires all the requirements for those individual components. Our guided install assumes systemd-based linux, but there are many other ways to install Netmaker's individual components onto machines that do not support Docker. 
+
+DNS Mode Prereqisite Setup
+====================================
+
+If you plan on running the server in DNS Mode, know that a `CoreDNS Server <https://coredns.io/manual/toc/>`_ will be installed. CoreDNS is a light-weight, fast, and easy-to-configure DNS server. It is recommended to bind CoreDNS to port 53 of the host system, and it will do so by default. The clients will expect the nameserver to be on port 53, and many systems have issues resolving a different port.
+
+However, on your host system (for Netmaker), this may conflict with an existing process. On linux systems running systemd-resolved, there is likely a service consuming port 53. The below steps will disable systemd-resolved, and replace it with a generic (e.g. Google) nameserver. Be warned that this may have consequences for any existing private DNS configuration. The following was tested on Ubuntu 20.04 and should be run prior to deploying the docker containers.
+
+1. ``systemctl stop systemd-resolved`` 
+2. ``systemctl disable systemd-resolved`` 
+3. ``vim /etc/systemd/resolved.conf``
+    * uncomment DNS and add 8.8.8.8 or whatever reachable nameserver is your preference
+    * uncomment DNSStubListener and set to "no"
+4. ``ln -sf /run/systemd/resolve/resolv.conf /etc/resolv.conf``
+
+Port 53 should now be available for CoreDNS to use.
+
+Docker Compose Install
+=======================
+
+The most simple (and recommended) way of installing Netmaker is to use one of the provided `Docker Compose files <https://github.com/gravitl/netmaker/tree/feature_v0.3.5_docs/compose>`_. Below are instructions for several different options to install Netmaker via Docker Compose, followed by an annotated reference Docker Compose in case your use case requires additional customization.
+
+Slim Install - No DNS, No Client Mode, No Secure GRPC
+--------------------------------------------------------
+
+This is the same docker compose covered in the :doc:`quick start <./quick-start>`. It requires no special privileges and can run on any system with Docker and Docker Compose. However, it also does not have the full feature set, and lacks Client Mode and DNS Mode.
+
+**Prerequisites:**
+  * ports 80, 8081, and 50051 are not blocked by firewall
+  * ports 80, 8081, 50051, and 27017 are not in use 
+
+**Notes:** 
+  * You can still run the netclient on the host system even if Client Mode is not enabled. It will just be managed like the netclient on any other nodes, and will not be automatically managed by thhe server/UI.
+  * You can change the port mappings in the Docker Compose if the listed ports are already in use.
+
+Assuming you have Docker and Docker Compose installed, you can just run the following, replacing **< Insert your-host IP Address Here >** with your host IP (or domain):
+
+#. ``wget -O docker-compose.yml https://raw.githubusercontent.com/gravitl/netmaker/master/scripts/docker-compose.slim.yml``
+#. ``sed -i ‘s/HOST_IP/< Insert your-host IP Address Here >/g’ docker-compose.yml``
+#. ``docker-compose up -d``
+
+Full Install - DNS, Client Mode, and Secure GRPC Enabled
+----------------------------------------------------------
+
+This installation gives you the fully-featured product with Client Mode and DNS Mode. 
+
+**Prerequisites:**
+  * systemd linux (Debian or Ubuntu reccommended)
+  * sudo privileges
+  * DNS Mode Prerequisite Setup (see above)
+  * WireGuard installed
+  * ports 80, 8081, 53, and 50555 are not blocked by firewall
+  * ports 80, 8081, 53, 50555, and 27017 are not in use
+
+**Notes:** 
+  * You can change the port mappings in the Docker Compose if the listed ports are already in use.
+  * You can run CoreDNS on a non-53 port, but this likely will cause issues on the client side (DNS on non-standard port). We do not recommend this and do not cover how to manage running CoreDNS on a different port for clients, which will likely have problems resolving a nameserver on a non-53 port.
+
+Assuming you have Docker and Docker Compose installed, you can just run the following, replacing **< Insert your-host IP Address Here >** with your host IP (or domain):
+
+#. ``sudo su -``
+#. ``wget -O docker-compose.yml https://raw.githubusercontent.com/gravitl/netmaker/master/scripts/docker-compose.yml``
+#. ``sed -i ‘s/HOST_IP/< Insert your-host IP Address Here >/g’ docker-compose.yml``
+#. ``docker-compose up -d``
+
+
+Server Only Install - UI, DNS, Client Disabled
+------------------------------------------------
+
+A "Server Only" install can be helpful for scenarios in which you do not want to run the UI. the UI is not mandatory for running a Netmaker network, but it makes the process easier. This mode also diables DNS and Client Modes, though you can add those back in if needed. There is no UI dependency on Client Mode or DNS Mode.
+
+**Prerequisites:**
+  * ports 8081 and 50051 are not blocked by firewall
+  * ports 8081, 50051, and 27017 are not in use
+
+**Notes:**
+  * You can still run the netclient on the host system even if Client Mode is not enabled. It will just be managed like the netclient on any other nodes, and will not be automatically managed by thhe server/UI.
+  * You can change the port mappings in the Docker Compose if the listed ports are already in use.
+
+Assuming you have Docker and Docker Compose installed, you can just run the following, replacing **< Insert your-host IP Address Here >** with your host IP (or domain):
+
+#. ``wget -O docker-compose.yml https://raw.githubusercontent.com/gravitl/netmaker/master/scripts/docker-compose.server-only.yml``
+#. ``sed -i ‘s/HOST_IP/< Insert your-host IP Address Here >/g’ docker-compose.yml``
+
+No DNS - CoreDNS Disabled, Client Enabled
+----------------------------------------------
+
+DNS Mode is currently limited to clients that can run resolvectl (systemd-resolved, see :doc:`Architecture docs <./architecture>` for more info). You may wish to disable DNS mode for various reasons. This installation option gives you the full feature set minus CoreDNS.
+
+**Prerequisites:**
+  * systemd linux (Debian or Ubuntu reccommended)
+  * sudo privileges
+  * WireGuard installed
+  * ports 80, 8081, and 50555 are not blocked by firewall
+  * ports 80, 8081, 50555, and 27017 are not in use
+
+**Notes:** 
+  * You can change the port mappings in the Docker Compose if the listed ports are already in use.
+  * If you would like to run DNS Mode, but disable it on some clients, this is also an option. See the :doc:`client installation <./client-installation>` documentation for more details.
+
+Assuming you have Docker and Docker Compose installed, you can just run the following, replacing **< Insert your-host IP Address Here >** with your host IP (or domain):
+
+#. ``wget -O docker-compose.yml https://raw.githubusercontent.com/gravitl/netmaker/master/scripts/docker-compose.nodns.yml``
+#. ``sed -i ‘s/HOST_IP/< Insert your-host IP Address Here >/g’ docker-compose.yml``
+
+No DNS - CoreDNS Disabled, Client Enabled
+
+No Client - DNS Enabled, Client Disabled
+---------------------------------------------
+
+You may want to provide DNS, but do not want to run the server with special privileges, in which case you can run with just Client Mode disabled. It requires no special privileges and can run on any system with Docker and Docker Compose. 
+
+**Prerequisites:**
+  * ports 80, 8081, 53, and 50051 are not blocked by firewall
+  * ports 80, 8081, 53, 50051, and 27017 are not in use
+  * DNS Mode Prerequisite Setup (see above)
+
+**Notes:** 
+  * You can still run the netclient on the host system even if Client Mode is not enabled. It will just be managed like the netclient on any other nodes, and will not be automatically managed by thhe server/UI.
+  * You can change the port mappings in the Docker Compose if the listed ports are already in use.
+
+Assuming you have Docker and Docker Compose installed, you can just run the following, replacing **< Insert your-host IP Address Here >** with your host IP (or domain):
+
+#. ``wget -O docker-compose.yml https://raw.githubusercontent.com/gravitl/netmaker/master/scripts/docker-compose.noclient.yml``
+#. ``sed -i ‘s/HOST_IP/< Insert your-host IP Address Here >/g’ docker-compose.yml``
+#. ``docker-compose up -d``
+
+
+Reference Compose File - Annotated
+--------------------------------------
+
+All environment variables and options are enabled in this file. It is the equivalent to running the "full install" from the above section. However, all environment variables are included, and are set to the default values provided by Netmaker (if the environment variable was left unset, it would not change the installation). Comments are added to each option to show how you might use it to modify your installation.
+
+.. literalinclude:: ../compose/docker-compose.reference.yml
+  :language: YAML
+
+
+Linux Install without Docker
+=============================
+
+Most systems support Docker, but some, such as LXC, do not. In such environments, there are many options for installing Netmaker. Netmaker is available as a binary file, and there is a zip file of the Netmaker UI static HTML on GitHub. Beyond the UI and Server, you need to install MongoDB and CoreDNS (optional). 
+
+Below is a guided set of instructions for installing without Docker on Ubuntu 20.04. Depending on your system, the steps may vary.
+
+MongoDB Setup
+----------------
+1. Install MongoDB on your server:
+    * For Ubuntu: `sudo apt install -y mongodb`
+    * For more advanced installation or other operating systems, see  the `MongoDB documentation <https://docs.mongodb.com/manual/administration/install-community/>`_.
+
+2. Create a user:
+    * ``mongo admin``  
+    * > `db.createUser({ user: "mongoadmin" , pwd: "mongopass", roles: ["userAdminAnyDatabase", "dbAdminAnyDatabase", "readWriteAnyDatabase"]})`
+
+Server Setup
+-------------
+1. **Run the install script:** ``sudo curl -sfL https://raw.githubusercontent.com/gravitl/netmaker/v0.3.5/scripts/netmaker-server.sh | sh -``
+2. Check status:  ``sudo journalctl -u netmaker``
+3. If any settings are incorrect such as host or mongo credentials, change them under /etc/netmaker/config/environments/< your env >.yaml and then run ``sudo systemctl restart netmaker``
+
+UI Setup
+-----------
+
+The following uses NGinx as an http server. You may alternatively use Apache or any other web server that serves static web files.
+
+1. **Download UI asset files:** ``sudo wget -O /usr/share/nginx/html/netmaker-ui.zip https://github.com/gravitl/netmaker-ui/releases/download/latest/netmaker-ui.zip``
+2. **Unzip:** ``sudo unzip /usr/share/nginx/html/netmaker-ui.zip -d /usr/share/nginx/html``
+3. **Copy Config to Nginx:** ``sudo cp /usr/share/nginx/html/nginx.conf /etc/nginx/conf.d/default.conf``
+4. **Modify Default Config Path:** ``sudo sed -i 's/root \/var\/www\/html/root \/usr\/share\/nginx\/html/g' /etc/nginx/sites-available/default``
+5. **Change Backend URL:** ``sudo sh -c 'BACKEND_URL=http://<YOUR BACKEND API URL>:PORT /usr/share/nginx/html/generate_config_js.sh >/usr/share/nginx/html/config.js'``
+6. **Start Nginx:** ``sudo systemctl start nginx``
+
+CoreDNS Setup
+----------------
+
+Kubernetes Install
+=======================
+
+**This configuration is coming soon.** It will allow you to deploy Netmaker on a Kubernetes cluster.
+
+Configuration Reference
+=========================
+
+The "Reference Compose File" (above) explains many of these options. However, it is important to understand fundamentally how Netmaker sets its configuration:
+
+1. Defaults
+2. Config File
+3. Environment Variables
+
+Variable Description
+----------------------
+
+SERVER_HOST: 
+    **Default:** Server will perform an IP check and set automatically unless explicitly set, or DISABLE_REMOTE_IP_CHECK is set to true, in which case it defaults to 127.0.0.1
+
+    **Description:** Sets the SERVER_HTTP_HOST and SERVER_GRPC_HOST variables if they are unset. The address where traffic comes in. 
+
+SERVER_HTTP_HOST: 
+    **Default:** Equals SERVER_HOST if set, "127.0.0.1" if SERVER_HOST is unset.
+    
+    **Description:** Set to make the HTTP and GRPC functions available via different interfaces/networks.
+
+SERVER_GRPC_HOST: 
+    **Default:** Equals SERVER_HOST if set, "127.0.0.1" if SERVER_HOST is unset.
+
+    **Description:** Set to make the HTTP and GRPC functions available via different interfaces/networks.
+
+API_PORT:  
+    **Default:** 8081 
+
+    **Description:** The HTTP API port for Netmaker. Used for API calls / communication from front end.
+
+GRPC_PORT:  
+    **Default:** 50051
+
+    **Description:** The GRPC port for Netmaker. Used for communications from nodes.
+
+MASTER_KEY:  
+    **Default:** "secretkey" 
+
+    **Description:** The admin master key for accessing the API. Change this in any production installation.
+
+CORS_ALLOWED_ORIGIN:  
+    **Default:** "*"
+
+    **Description:** The "allowed origin" for API requests. Change to restrict where API requests can come from.
+
+REST_BACKEND:  
+    **Default:** "on" 
+
+    **Description:** Enables the REST backend (API running on API_PORT at SERVER_HTTP_HOST). Change to "off" to turn off.
+
+AGENT_BACKEND:  
+    **Default:** "on" 
+
+    **Description:** Enables the AGENT backend (GRPC running on GRPC_PORT at SERVER_GRPC_HOST). Change to "off" to turn off.
+
+CLIENT_MODE:  
+    **Default:** "on" 
+
+    **Description:** Enables Client Mode, meaning netclient will be deployed on server and will be manageable from UI. Change to "off" to turn off.
+
+DNS_MODE:  
+    **Default:** "on"
+
+    **Description:** Enables DNS Mode, meaning config files will be generated for CoreDNS.
+
+DISABLE_REMOTE_IP_CHECK:  
+    **Default:** "off" 
+
+    **Description:** If turned "on", Server will not set Host based on remote IP check. This is already overridden if SERVER_HOST is set. Turned "off" by default.
+
+MONGO_ADMIN:  
+    **Default:** "mongoadmin" 
+
+    **Description:** Admin user for MongoDB.
+
+MONGO_PASS:  
+    **Default:** "mongopass" 
+
+    **Description:** Admin password for MongoDB.
+
+MONGO_HOST:  
+    **Default:** "127.0.0.1"
+
+    **Description:** Address of MongoDB.
+
+MONGO_PORT:  
+    **Default:** "27017"
+
+    **Description:** Port of MongoDB.
+
+MONGO_OPTS:  
+    **Default:** "/?authSource=admin"
+
+    **Description:** Opts to enable admin login for Mongo.
+
+SERVER_GRPC_WIREGUARD: 
+    **Default:** "on"
+
+    **Description:** Whether to run GRPC over a WireGuard network. On by default. Secures the server comms. Switch to "off" to turn off. If off and running in production, make sure to have certificates installed to secure GRPC communications. 
+
+SERVER_GRPC_WG_INTERFACE: 
+    **Default:** "nm-grpc-wg"
+
+    **Description:** Interface to use for GRPC WireGuard network if enabled
+
+SERVER_GRPC_WG_ADDRESS:
+    **Default:** "10.101.0.1"
+
+    **Description:** Private Address to use for GRPC WireGuard network if enabled
+
+SERVER_GRPC_WG_ADDRESS_RANGE:
+    **Default:** "10.101.0.0/16"
+
+    **Description:** Private Address range to use for GRPC WireGard clients if enabled. Gives 65,534 total addresses for all of netmaker. If running a larger network, will need to configure addresses differently, for instance using ipv6, or use certificates instead.
+
+SERVER_GRPC_WG_PORT:
+    **Default:** 50555
+
+    **Description:** Port to use for GRPC WireGuard if enabled
+
+SERVER_GRPC_WG_PUBKEY:
+    **Default:** < generated at startup >
+
+    **Description:** PublicKey for GRPC WireGuard interface. Generated if left blank.
+
+SERVER_GRPC_WG_PRIVKEY:
+    **Default:** < generated at startup >
+
+    **Description:** PrivateKey for GRPC WireGuard interface. Generated if left blank.
+
+SERVER_GRPC_WG_KEYREQUIRED
+    **Default:** ""
+
+    **Description:** Determines if an Access Key is required to join the Comms network. Blank (meaning 'no') by default. Set to "yes" to turn on.
+
+
+Config File Reference
+----------------------
+A config file may be placed under config/environments/<env-name>.yml. To read this file at runtime, provide the environment variable ENV at runtime. For instance, dev.yml paired with ENV=dev. Netmaker will load the specified Config file. This allows you to store and manage configurations for different environments. Below is a reference Config File you may use.
+
+.. literalinclude:: ../config/environments/dev.yaml
+  :language: YAML
+

+ 75 - 0
docs/_build/html/_sources/support.rst.txt

@@ -0,0 +1,75 @@
+=========
+Support
+=========
+
+FAQ
+======
+
+Does/Will Netmaker Support X Operating System?
+--------------------------------------------------
+
+Netmaker is initially available on a limited number of operating systems for good reason: Every operating system is designed differently. With a small team, we can either focus on making Netmaker do a lot on a few number of operating systems, or a little on a bunch of operating systems. We chose the first option. You can view the System Compatibility docs for more info, but in general, you should only be using Netmaker on systemd linux right now.
+
+However, as of v0.4, we will technically be able to bring any operating system into the network. This is a bit of a hack. v0.4 introduces Ingress Gateways. Think of it this way. You set up a private network. You want devices to access it. You set up a single node as an "Ingress Gateway" and generate config files for "external clients." These clients are unmanaged and unmeshed, meaning they can access the network but only via the gateway. It also means they will not automatically account for changes to the network, and the user will have to update the configs manually.
+
+This lets us immediately "support" any device which can run WireGuard, which includes most operating systems at this point including phones and Windows.
+
+As we stabilize the design and feature set of Netmaker, we will expand the operating system support for Netclient which configures dynamic, fully-meshed devices. Expect to see updates about new OS support every few weeks, until eventually the Ingress Gateway becomes unnecessary (though you will still want it for certain use cases).
+
+How do I install the Netclient on X?
+---------------------------------------
+
+As per the above, there are many unsupported operating systems. You are still welcome to try, it is just an executable binary file after all. If the system is unix-based and has kernel WireGuard installed, netclient may very well mesh the device into the network. However, the service likely will encounter problems retrieving updates.
+
+Why MongoDB? SQL is better and smaller.
+----------------------------------------
+
+We are in a phase of rapid iteration. Every week the database schema changes. MongoDB makes our development process much more flexible. That said, we agree! SQL will be better for production, and indeed, we plan to switch over to SQL before v1.0, so please be patient with the resource consumption of MongoDB for the time being; it is helping to support a rapid pace of development. 
+
+
+Is Netmaker a VPN like NordNPN?
+--------------------------------
+
+No. Netmaker makes Virtual Networks, which are technically VPNs, but different. It's more like a corporate VPN, or a VPC (if you're familiar with AWS).
+
+If you're looking to achieve self-hosted web browsing, with functionality similar to NordVPN, ExpressVPN, Surfshark, Tunnelbear, or Private Internet Access, this is probably not the project for you. Technically, you can accomplish this with Netmaker, but it would be a little like using a all-terrain vehicle for stock car racing.
+
+There are many good projects out there that support general internet privacy using WireGuard. Here are just a few of them:
+
+https://github.com/trailofbits/algo
+https://github.com/pivpn/pivpn
+https://github.com/subspacecloud/subspace
+https://github.com/mullvad/mullvadvpn-app
+
+Do you offer any paid support?
+---------------------------------
+
+Not at this time, but eventually we will. If you are interested, or if you are interested in sponsoring the project generally, please contact Alex Feiszli ([email protected]).
+
+Why the SSPL License?
+----------------------
+
+We thought long and hard about the license. Ultimately, we think this is the best way to support and ensure the health of the project long term. The community deserves something that is well-maintained, and in order to do that, eventually we need some financial support. We won't do that by limiting the project, but we will offer some additional support, and hosted options for things people would end up paying for anyway (relay servers, load balancing support, backups). 
+
+While SSPL is not an OSI-approved open source license, it let's people generally run the project however they want, both for private use and business use, without running into the issue of someone else monetizing the project and making it financially untenable. We are working on making the guidelines clear, and will make sure that the license does not impact the communities ability to use and modify the project.
+
+If you have concerns about the license leading to project restrictions down the road, just know that there are other paid, closed-source/closed-core options out there, so beyond not wanting to follow that path, we also don't think it's a good idea economically either. We firmly believe that having the project open is not only right, but the best option.
+
+All that said, we will re-evaluate the license on a regular basis and determine if an OSI-approved license makes more sense. It's just easier to move from SSPL to another license than vice-versa.
+
+Issues, Bugs, and Feature Requests
+=====================================
+
+Issues / Bugs
+----------------
+
+Feature Requests
+-------------------
+
+Contact
+===========
+If you need help, try the discord or open a GitHub ticket.
+
+Email: [email protected]
+
+Discord: https://discord.gg/zRb9Vfhk8A

+ 19 - 0
docs/_build/html/_sources/troubleshoot.rst.txt

@@ -0,0 +1,19 @@
+=================
+Troubleshooting
+=================
+
+Common Issues
+---------------
+
+Server
+-------
+
+UI
+----
+
+Agent
+-------
+
+CoreDNS
+--------
+

+ 17 - 0
docs/_build/html/_sources/tutorials.rst.txt

@@ -0,0 +1,17 @@
+===========
+Tutorials
+===========
+
+Members of the community have created helpful tutorials for getting started with Netmaker. Below are some selected tutorials on different topics.
+
+Video Tutorials
+===============
+* `Intro/Overview <https://youtu.be/PWLPT320Ybo>`_: Tutorial on first-time usage, setting up a mesh network.
+* `Site-to-Site Gateway <https://youtu.be/krCKBJhwwDk>`_: Tutorial on setting up site-to-site connections, allowing peers to access external networks via gateways.
+* `IPv6 and Private DNS <https://youtu.be/b4diaKWUcXI>`_: Tutorial on dual-stack IPv6 in Netmaker and Private DNS management (separate topics).
+* `Kubernetes Networking <https://youtu.be/z2jvlFVU3dw>`_: Tutorial on setting up cross-cloud Kubernetes clusters using Netmaker.
+
+
+Written Tutorials
+=================
+* `Kubernetes Networking <https://itnext.io/how-to-deploy-a-single-kubernetes-cluster-across-multiple-clouds-using-k3s-and-wireguard-a5ae176a6e81>`_: Tutorial on setting up cross-cloud Kubernetes clusters using Netmaker.

+ 39 - 0
docs/_build/html/_sources/usage.rst.txt

@@ -0,0 +1,39 @@
+==============
+Using Netmaker
+==============
+
+Netmaker has many different use cases, from a basic virtual network to an office gateway VPN to a Kubernetes underlay. It can be a bit overwhelming to figure out where to start. If you don't find your use case here, but think Netmaker is a good fit, let us know!
+
+External Tutorials
+==================
+
+Members of the community have created helpful tutorials for getting started with Netmaker. Below are some selected tutorials on different topics.
+
+Video Tutorials
+---------------
+* `Intro/Overview <https://youtu.be/PWLPT320Ybo>`_: Tutorial on first-time usage, setting up a mesh network.
+* `Site-to-Site Gateway <https://youtu.be/krCKBJhwwDk>`_: Tutorial on setting up site-to-site connections, allowing peers to access external networks via gateways.
+* `IPv6 and Private DNS <https://youtu.be/b4diaKWUcXI>`_: Tutorial on dual-stack IPv6 in Netmaker and Private DNS management (separate topics).
+* `Kubernetes Networking <https://youtu.be/z2jvlFVU3dw>`_: Tutorial on setting up cross-cloud Kubernetes clusters using Netmaker.
+
+
+Written Tutorials
+-----------------
+* `Kubernetes Cross-cloud cluster <https://itnext.io/how-to-deploy-a-single-kubernetes-cluster-across-multiple-clouds-using-k3s-and-wireguard-a5ae176a6e81>`_: Tutorial on setting up cross-cloud Kubernetes clusters using Netmaker.
+
+Basic
+=====
+
+Local Network
+=============
+  
+Site-to-Site
+============
+
+Dual Stack with IPv6
+====================
+
+Kubernetes Node Network
+========================
+
+

+ 37 - 0
docs/_build/html/_sources/walkthroughs.rst.txt

@@ -0,0 +1,37 @@
+===========
+Walkthroughs
+===========
+
+External Tutorials
+==================
+
+Members of the community have created helpful tutorials for getting started with Netmaker. Below are some selected tutorials on different topics.
+
+Video Tutorials
+---------------
+* `Intro/Overview <https://youtu.be/PWLPT320Ybo>`_: Tutorial on first-time usage, setting up a mesh network.
+* `Site-to-Site Gateway <https://youtu.be/krCKBJhwwDk>`_: Tutorial on setting up site-to-site connections, allowing peers to access external networks via gateways.
+* `IPv6 and Private DNS <https://youtu.be/b4diaKWUcXI>`_: Tutorial on dual-stack IPv6 in Netmaker and Private DNS management (separate topics).
+* `Kubernetes Networking <https://youtu.be/z2jvlFVU3dw>`_: Tutorial on setting up cross-cloud Kubernetes clusters using Netmaker.
+
+
+Written Tutorials
+-----------------
+* `Kubernetes Networking <https://itnext.io/how-to-deploy-a-single-kubernetes-cluster-across-multiple-clouds-using-k3s-and-wireguard-a5ae176a6e81>`_: Tutorial on setting up cross-cloud Kubernetes clusters using Netmaker.
+
+Basic
+=====
+
+Local Network
+=============
+  
+Site-to-Site
+============
+
+Dual Stack with IPv6
+====================
+
+Kubernetes Node Network
+========================
+
+

+ 861 - 0
docs/_build/html/_static/basic.css

@@ -0,0 +1,861 @@
+/*
+ * basic.css
+ * ~~~~~~~~~
+ *
+ * Sphinx stylesheet -- basic theme.
+ *
+ * :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS.
+ * :license: BSD, see LICENSE for details.
+ *
+ */
+
+/* -- main layout ----------------------------------------------------------- */
+
+div.clearer {
+    clear: both;
+}
+
+div.section::after {
+    display: block;
+    content: '';
+    clear: left;
+}
+
+/* -- relbar ---------------------------------------------------------------- */
+
+div.related {
+    width: 100%;
+    font-size: 90%;
+}
+
+div.related h3 {
+    display: none;
+}
+
+div.related ul {
+    margin: 0;
+    padding: 0 0 0 10px;
+    list-style: none;
+}
+
+div.related li {
+    display: inline;
+}
+
+div.related li.right {
+    float: right;
+    margin-right: 5px;
+}
+
+/* -- sidebar --------------------------------------------------------------- */
+
+div.sphinxsidebarwrapper {
+    padding: 10px 5px 0 10px;
+}
+
+div.sphinxsidebar {
+    float: left;
+    width: 230px;
+    margin-left: -100%;
+    font-size: 90%;
+    word-wrap: break-word;
+    overflow-wrap : break-word;
+}
+
+div.sphinxsidebar ul {
+    list-style: none;
+}
+
+div.sphinxsidebar ul ul,
+div.sphinxsidebar ul.want-points {
+    margin-left: 20px;
+    list-style: square;
+}
+
+div.sphinxsidebar ul ul {
+    margin-top: 0;
+    margin-bottom: 0;
+}
+
+div.sphinxsidebar form {
+    margin-top: 10px;
+}
+
+div.sphinxsidebar input {
+    border: 1px solid #98dbcc;
+    font-family: sans-serif;
+    font-size: 1em;
+}
+
+div.sphinxsidebar #searchbox form.search {
+    overflow: hidden;
+}
+
+div.sphinxsidebar #searchbox input[type="text"] {
+    float: left;
+    width: 80%;
+    padding: 0.25em;
+    box-sizing: border-box;
+}
+
+div.sphinxsidebar #searchbox input[type="submit"] {
+    float: left;
+    width: 20%;
+    border-left: none;
+    padding: 0.25em;
+    box-sizing: border-box;
+}
+
+
+img {
+    border: 0;
+    max-width: 100%;
+}
+
+/* -- search page ----------------------------------------------------------- */
+
+ul.search {
+    margin: 10px 0 0 20px;
+    padding: 0;
+}
+
+ul.search li {
+    padding: 5px 0 5px 20px;
+    background-image: url(file.png);
+    background-repeat: no-repeat;
+    background-position: 0 7px;
+}
+
+ul.search li a {
+    font-weight: bold;
+}
+
+ul.search li div.context {
+    color: #888;
+    margin: 2px 0 0 30px;
+    text-align: left;
+}
+
+ul.keywordmatches li.goodmatch a {
+    font-weight: bold;
+}
+
+/* -- index page ------------------------------------------------------------ */
+
+table.contentstable {
+    width: 90%;
+    margin-left: auto;
+    margin-right: auto;
+}
+
+table.contentstable p.biglink {
+    line-height: 150%;
+}
+
+a.biglink {
+    font-size: 1.3em;
+}
+
+span.linkdescr {
+    font-style: italic;
+    padding-top: 5px;
+    font-size: 90%;
+}
+
+/* -- general index --------------------------------------------------------- */
+
+table.indextable {
+    width: 100%;
+}
+
+table.indextable td {
+    text-align: left;
+    vertical-align: top;
+}
+
+table.indextable ul {
+    margin-top: 0;
+    margin-bottom: 0;
+    list-style-type: none;
+}
+
+table.indextable > tbody > tr > td > ul {
+    padding-left: 0em;
+}
+
+table.indextable tr.pcap {
+    height: 10px;
+}
+
+table.indextable tr.cap {
+    margin-top: 10px;
+    background-color: #f2f2f2;
+}
+
+img.toggler {
+    margin-right: 3px;
+    margin-top: 3px;
+    cursor: pointer;
+}
+
+div.modindex-jumpbox {
+    border-top: 1px solid #ddd;
+    border-bottom: 1px solid #ddd;
+    margin: 1em 0 1em 0;
+    padding: 0.4em;
+}
+
+div.genindex-jumpbox {
+    border-top: 1px solid #ddd;
+    border-bottom: 1px solid #ddd;
+    margin: 1em 0 1em 0;
+    padding: 0.4em;
+}
+
+/* -- domain module index --------------------------------------------------- */
+
+table.modindextable td {
+    padding: 2px;
+    border-collapse: collapse;
+}
+
+/* -- general body styles --------------------------------------------------- */
+
+div.body {
+    min-width: 450px;
+    max-width: 800px;
+}
+
+div.body p, div.body dd, div.body li, div.body blockquote {
+    -moz-hyphens: auto;
+    -ms-hyphens: auto;
+    -webkit-hyphens: auto;
+    hyphens: auto;
+}
+
+a.headerlink {
+    visibility: hidden;
+}
+
+a.brackets:before,
+span.brackets > a:before{
+    content: "[";
+}
+
+a.brackets:after,
+span.brackets > a:after {
+    content: "]";
+}
+
+h1:hover > a.headerlink,
+h2:hover > a.headerlink,
+h3:hover > a.headerlink,
+h4:hover > a.headerlink,
+h5:hover > a.headerlink,
+h6:hover > a.headerlink,
+dt:hover > a.headerlink,
+caption:hover > a.headerlink,
+p.caption:hover > a.headerlink,
+div.code-block-caption:hover > a.headerlink {
+    visibility: visible;
+}
+
+div.body p.caption {
+    text-align: inherit;
+}
+
+div.body td {
+    text-align: left;
+}
+
+.first {
+    margin-top: 0 !important;
+}
+
+p.rubric {
+    margin-top: 30px;
+    font-weight: bold;
+}
+
+img.align-left, figure.align-left, .figure.align-left, object.align-left {
+    clear: left;
+    float: left;
+    margin-right: 1em;
+}
+
+img.align-right, figure.align-right, .figure.align-right, object.align-right {
+    clear: right;
+    float: right;
+    margin-left: 1em;
+}
+
+img.align-center, figure.align-center, .figure.align-center, object.align-center {
+  display: block;
+  margin-left: auto;
+  margin-right: auto;
+}
+
+img.align-default, figure.align-default, .figure.align-default {
+  display: block;
+  margin-left: auto;
+  margin-right: auto;
+}
+
+.align-left {
+    text-align: left;
+}
+
+.align-center {
+    text-align: center;
+}
+
+.align-default {
+    text-align: center;
+}
+
+.align-right {
+    text-align: right;
+}
+
+/* -- sidebars -------------------------------------------------------------- */
+
+div.sidebar,
+aside.sidebar {
+    margin: 0 0 0.5em 1em;
+    border: 1px solid #ddb;
+    padding: 7px;
+    background-color: #ffe;
+    width: 40%;
+    float: right;
+    clear: right;
+    overflow-x: auto;
+}
+
+p.sidebar-title {
+    font-weight: bold;
+}
+
+div.admonition, div.topic, blockquote {
+    clear: left;
+}
+
+/* -- topics ---------------------------------------------------------------- */
+
+div.topic {
+    border: 1px solid #ccc;
+    padding: 7px;
+    margin: 10px 0 10px 0;
+}
+
+p.topic-title {
+    font-size: 1.1em;
+    font-weight: bold;
+    margin-top: 10px;
+}
+
+/* -- admonitions ----------------------------------------------------------- */
+
+div.admonition {
+    margin-top: 10px;
+    margin-bottom: 10px;
+    padding: 7px;
+}
+
+div.admonition dt {
+    font-weight: bold;
+}
+
+p.admonition-title {
+    margin: 0px 10px 5px 0px;
+    font-weight: bold;
+}
+
+div.body p.centered {
+    text-align: center;
+    margin-top: 25px;
+}
+
+/* -- content of sidebars/topics/admonitions -------------------------------- */
+
+div.sidebar > :last-child,
+aside.sidebar > :last-child,
+div.topic > :last-child,
+div.admonition > :last-child {
+    margin-bottom: 0;
+}
+
+div.sidebar::after,
+aside.sidebar::after,
+div.topic::after,
+div.admonition::after,
+blockquote::after {
+    display: block;
+    content: '';
+    clear: both;
+}
+
+/* -- tables ---------------------------------------------------------------- */
+
+table.docutils {
+    margin-top: 10px;
+    margin-bottom: 10px;
+    border: 0;
+    border-collapse: collapse;
+}
+
+table.align-center {
+    margin-left: auto;
+    margin-right: auto;
+}
+
+table.align-default {
+    margin-left: auto;
+    margin-right: auto;
+}
+
+table caption span.caption-number {
+    font-style: italic;
+}
+
+table caption span.caption-text {
+}
+
+table.docutils td, table.docutils th {
+    padding: 1px 8px 1px 5px;
+    border-top: 0;
+    border-left: 0;
+    border-right: 0;
+    border-bottom: 1px solid #aaa;
+}
+
+table.footnote td, table.footnote th {
+    border: 0 !important;
+}
+
+th {
+    text-align: left;
+    padding-right: 5px;
+}
+
+table.citation {
+    border-left: solid 1px gray;
+    margin-left: 1px;
+}
+
+table.citation td {
+    border-bottom: none;
+}
+
+th > :first-child,
+td > :first-child {
+    margin-top: 0px;
+}
+
+th > :last-child,
+td > :last-child {
+    margin-bottom: 0px;
+}
+
+/* -- figures --------------------------------------------------------------- */
+
+div.figure, figure {
+    margin: 0.5em;
+    padding: 0.5em;
+}
+
+div.figure p.caption, figcaption {
+    padding: 0.3em;
+}
+
+div.figure p.caption span.caption-number,
+figcaption span.caption-number {
+    font-style: italic;
+}
+
+div.figure p.caption span.caption-text,
+figcaption span.caption-text {
+}
+
+/* -- field list styles ----------------------------------------------------- */
+
+table.field-list td, table.field-list th {
+    border: 0 !important;
+}
+
+.field-list ul {
+    margin: 0;
+    padding-left: 1em;
+}
+
+.field-list p {
+    margin: 0;
+}
+
+.field-name {
+    -moz-hyphens: manual;
+    -ms-hyphens: manual;
+    -webkit-hyphens: manual;
+    hyphens: manual;
+}
+
+/* -- hlist styles ---------------------------------------------------------- */
+
+table.hlist {
+    margin: 1em 0;
+}
+
+table.hlist td {
+    vertical-align: top;
+}
+
+
+/* -- other body styles ----------------------------------------------------- */
+
+ol.arabic {
+    list-style: decimal;
+}
+
+ol.loweralpha {
+    list-style: lower-alpha;
+}
+
+ol.upperalpha {
+    list-style: upper-alpha;
+}
+
+ol.lowerroman {
+    list-style: lower-roman;
+}
+
+ol.upperroman {
+    list-style: upper-roman;
+}
+
+:not(li) > ol > li:first-child > :first-child,
+:not(li) > ul > li:first-child > :first-child {
+    margin-top: 0px;
+}
+
+:not(li) > ol > li:last-child > :last-child,
+:not(li) > ul > li:last-child > :last-child {
+    margin-bottom: 0px;
+}
+
+ol.simple ol p,
+ol.simple ul p,
+ul.simple ol p,
+ul.simple ul p {
+    margin-top: 0;
+}
+
+ol.simple > li:not(:first-child) > p,
+ul.simple > li:not(:first-child) > p {
+    margin-top: 0;
+}
+
+ol.simple p,
+ul.simple p {
+    margin-bottom: 0;
+}
+
+dl.footnote > dt,
+dl.citation > dt {
+    float: left;
+    margin-right: 0.5em;
+}
+
+dl.footnote > dd,
+dl.citation > dd {
+    margin-bottom: 0em;
+}
+
+dl.footnote > dd:after,
+dl.citation > dd:after {
+    content: "";
+    clear: both;
+}
+
+dl.field-list {
+    display: grid;
+    grid-template-columns: fit-content(30%) auto;
+}
+
+dl.field-list > dt {
+    font-weight: bold;
+    word-break: break-word;
+    padding-left: 0.5em;
+    padding-right: 5px;
+}
+
+dl.field-list > dt:after {
+    content: ":";
+}
+
+dl.field-list > dd {
+    padding-left: 0.5em;
+    margin-top: 0em;
+    margin-left: 0em;
+    margin-bottom: 0em;
+}
+
+dl {
+    margin-bottom: 15px;
+}
+
+dd > :first-child {
+    margin-top: 0px;
+}
+
+dd ul, dd table {
+    margin-bottom: 10px;
+}
+
+dd {
+    margin-top: 3px;
+    margin-bottom: 10px;
+    margin-left: 30px;
+}
+
+dl > dd:last-child,
+dl > dd:last-child > :last-child {
+    margin-bottom: 0;
+}
+
+dt:target, span.highlighted {
+    background-color: #fbe54e;
+}
+
+rect.highlighted {
+    fill: #fbe54e;
+}
+
+dl.glossary dt {
+    font-weight: bold;
+    font-size: 1.1em;
+}
+
+.optional {
+    font-size: 1.3em;
+}
+
+.sig-paren {
+    font-size: larger;
+}
+
+.versionmodified {
+    font-style: italic;
+}
+
+.system-message {
+    background-color: #fda;
+    padding: 5px;
+    border: 3px solid red;
+}
+
+.footnote:target  {
+    background-color: #ffa;
+}
+
+.line-block {
+    display: block;
+    margin-top: 1em;
+    margin-bottom: 1em;
+}
+
+.line-block .line-block {
+    margin-top: 0;
+    margin-bottom: 0;
+    margin-left: 1.5em;
+}
+
+.guilabel, .menuselection {
+    font-family: sans-serif;
+}
+
+.accelerator {
+    text-decoration: underline;
+}
+
+.classifier {
+    font-style: oblique;
+}
+
+.classifier:before {
+    font-style: normal;
+    margin: 0.5em;
+    content: ":";
+}
+
+abbr, acronym {
+    border-bottom: dotted 1px;
+    cursor: help;
+}
+
+/* -- code displays --------------------------------------------------------- */
+
+pre {
+    overflow: auto;
+    overflow-y: hidden;  /* fixes display issues on Chrome browsers */
+}
+
+pre, div[class*="highlight-"] {
+    clear: both;
+}
+
+span.pre {
+    -moz-hyphens: none;
+    -ms-hyphens: none;
+    -webkit-hyphens: none;
+    hyphens: none;
+}
+
+div[class*="highlight-"] {
+    margin: 1em 0;
+}
+
+td.linenos pre {
+    border: 0;
+    background-color: transparent;
+    color: #aaa;
+}
+
+table.highlighttable {
+    display: block;
+}
+
+table.highlighttable tbody {
+    display: block;
+}
+
+table.highlighttable tr {
+    display: flex;
+}
+
+table.highlighttable td {
+    margin: 0;
+    padding: 0;
+}
+
+table.highlighttable td.linenos {
+    padding-right: 0.5em;
+}
+
+table.highlighttable td.code {
+    flex: 1;
+    overflow: hidden;
+}
+
+.highlight .hll {
+    display: block;
+}
+
+div.highlight pre,
+table.highlighttable pre {
+    margin: 0;
+}
+
+div.code-block-caption + div {
+    margin-top: 0;
+}
+
+div.code-block-caption {
+    margin-top: 1em;
+    padding: 2px 5px;
+    font-size: small;
+}
+
+div.code-block-caption code {
+    background-color: transparent;
+}
+
+table.highlighttable td.linenos,
+span.linenos,
+div.doctest > div.highlight span.gp {  /* gp: Generic.Prompt */
+    user-select: none;
+}
+
+div.code-block-caption span.caption-number {
+    padding: 0.1em 0.3em;
+    font-style: italic;
+}
+
+div.code-block-caption span.caption-text {
+}
+
+div.literal-block-wrapper {
+    margin: 1em 0;
+}
+
+code.descname {
+    background-color: transparent;
+    font-weight: bold;
+    font-size: 1.2em;
+}
+
+code.descclassname {
+    background-color: transparent;
+}
+
+code.xref, a code {
+    background-color: transparent;
+    font-weight: bold;
+}
+
+h1 code, h2 code, h3 code, h4 code, h5 code, h6 code {
+    background-color: transparent;
+}
+
+.viewcode-link {
+    float: right;
+}
+
+.viewcode-back {
+    float: right;
+    font-family: sans-serif;
+}
+
+div.viewcode-block:target {
+    margin: -1px -10px;
+    padding: 0 10px;
+}
+
+/* -- math display ---------------------------------------------------------- */
+
+img.math {
+    vertical-align: middle;
+}
+
+div.body div.math p {
+    text-align: center;
+}
+
+span.eqno {
+    float: right;
+}
+
+span.eqno a.headerlink {
+    position: absolute;
+    z-index: 1;
+}
+
+div.math:hover a.headerlink {
+    visibility: visible;
+}
+
+/* -- printout stylesheet --------------------------------------------------- */
+
+@media print {
+    div.document,
+    div.documentwrapper,
+    div.bodywrapper {
+        margin: 0 !important;
+        width: 100%;
+    }
+
+    div.sphinxsidebar,
+    div.related,
+    div.footer,
+    #top-link {
+        display: none;
+    }
+}

+ 321 - 0
docs/_build/html/_static/doctools.js

@@ -0,0 +1,321 @@
+/*
+ * doctools.js
+ * ~~~~~~~~~~~
+ *
+ * Sphinx JavaScript utilities for all documentation.
+ *
+ * :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS.
+ * :license: BSD, see LICENSE for details.
+ *
+ */
+
+/**
+ * select a different prefix for underscore
+ */
+$u = _.noConflict();
+
+/**
+ * make the code below compatible with browsers without
+ * an installed firebug like debugger
+if (!window.console || !console.firebug) {
+  var names = ["log", "debug", "info", "warn", "error", "assert", "dir",
+    "dirxml", "group", "groupEnd", "time", "timeEnd", "count", "trace",
+    "profile", "profileEnd"];
+  window.console = {};
+  for (var i = 0; i < names.length; ++i)
+    window.console[names[i]] = function() {};
+}
+ */
+
+/**
+ * small helper function to urldecode strings
+ *
+ * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent#Decoding_query_parameters_from_a_URL
+ */
+jQuery.urldecode = function(x) {
+  if (!x) {
+    return x
+  }
+  return decodeURIComponent(x.replace(/\+/g, ' '));
+};
+
+/**
+ * small helper function to urlencode strings
+ */
+jQuery.urlencode = encodeURIComponent;
+
+/**
+ * This function returns the parsed url parameters of the
+ * current request. Multiple values per key are supported,
+ * it will always return arrays of strings for the value parts.
+ */
+jQuery.getQueryParameters = function(s) {
+  if (typeof s === 'undefined')
+    s = document.location.search;
+  var parts = s.substr(s.indexOf('?') + 1).split('&');
+  var result = {};
+  for (var i = 0; i < parts.length; i++) {
+    var tmp = parts[i].split('=', 2);
+    var key = jQuery.urldecode(tmp[0]);
+    var value = jQuery.urldecode(tmp[1]);
+    if (key in result)
+      result[key].push(value);
+    else
+      result[key] = [value];
+  }
+  return result;
+};
+
+/**
+ * highlight a given string on a jquery object by wrapping it in
+ * span elements with the given class name.
+ */
+jQuery.fn.highlightText = function(text, className) {
+  function highlight(node, addItems) {
+    if (node.nodeType === 3) {
+      var val = node.nodeValue;
+      var pos = val.toLowerCase().indexOf(text);
+      if (pos >= 0 &&
+          !jQuery(node.parentNode).hasClass(className) &&
+          !jQuery(node.parentNode).hasClass("nohighlight")) {
+        var span;
+        var isInSVG = jQuery(node).closest("body, svg, foreignObject").is("svg");
+        if (isInSVG) {
+          span = document.createElementNS("http://www.w3.org/2000/svg", "tspan");
+        } else {
+          span = document.createElement("span");
+          span.className = className;
+        }
+        span.appendChild(document.createTextNode(val.substr(pos, text.length)));
+        node.parentNode.insertBefore(span, node.parentNode.insertBefore(
+          document.createTextNode(val.substr(pos + text.length)),
+          node.nextSibling));
+        node.nodeValue = val.substr(0, pos);
+        if (isInSVG) {
+          var rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
+          var bbox = node.parentElement.getBBox();
+          rect.x.baseVal.value = bbox.x;
+          rect.y.baseVal.value = bbox.y;
+          rect.width.baseVal.value = bbox.width;
+          rect.height.baseVal.value = bbox.height;
+          rect.setAttribute('class', className);
+          addItems.push({
+              "parent": node.parentNode,
+              "target": rect});
+        }
+      }
+    }
+    else if (!jQuery(node).is("button, select, textarea")) {
+      jQuery.each(node.childNodes, function() {
+        highlight(this, addItems);
+      });
+    }
+  }
+  var addItems = [];
+  var result = this.each(function() {
+    highlight(this, addItems);
+  });
+  for (var i = 0; i < addItems.length; ++i) {
+    jQuery(addItems[i].parent).before(addItems[i].target);
+  }
+  return result;
+};
+
+/*
+ * backward compatibility for jQuery.browser
+ * This will be supported until firefox bug is fixed.
+ */
+if (!jQuery.browser) {
+  jQuery.uaMatch = function(ua) {
+    ua = ua.toLowerCase();
+
+    var match = /(chrome)[ \/]([\w.]+)/.exec(ua) ||
+      /(webkit)[ \/]([\w.]+)/.exec(ua) ||
+      /(opera)(?:.*version|)[ \/]([\w.]+)/.exec(ua) ||
+      /(msie) ([\w.]+)/.exec(ua) ||
+      ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec(ua) ||
+      [];
+
+    return {
+      browser: match[ 1 ] || "",
+      version: match[ 2 ] || "0"
+    };
+  };
+  jQuery.browser = {};
+  jQuery.browser[jQuery.uaMatch(navigator.userAgent).browser] = true;
+}
+
+/**
+ * Small JavaScript module for the documentation.
+ */
+var Documentation = {
+
+  init : function() {
+    this.fixFirefoxAnchorBug();
+    this.highlightSearchWords();
+    this.initIndexTable();
+    if (DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) {
+      this.initOnKeyListeners();
+    }
+  },
+
+  /**
+   * i18n support
+   */
+  TRANSLATIONS : {},
+  PLURAL_EXPR : function(n) { return n === 1 ? 0 : 1; },
+  LOCALE : 'unknown',
+
+  // gettext and ngettext don't access this so that the functions
+  // can safely bound to a different name (_ = Documentation.gettext)
+  gettext : function(string) {
+    var translated = Documentation.TRANSLATIONS[string];
+    if (typeof translated === 'undefined')
+      return string;
+    return (typeof translated === 'string') ? translated : translated[0];
+  },
+
+  ngettext : function(singular, plural, n) {
+    var translated = Documentation.TRANSLATIONS[singular];
+    if (typeof translated === 'undefined')
+      return (n == 1) ? singular : plural;
+    return translated[Documentation.PLURALEXPR(n)];
+  },
+
+  addTranslations : function(catalog) {
+    for (var key in catalog.messages)
+      this.TRANSLATIONS[key] = catalog.messages[key];
+    this.PLURAL_EXPR = new Function('n', 'return +(' + catalog.plural_expr + ')');
+    this.LOCALE = catalog.locale;
+  },
+
+  /**
+   * add context elements like header anchor links
+   */
+  addContextElements : function() {
+    $('div[id] > :header:first').each(function() {
+      $('<a class="headerlink">\u00B6</a>').
+      attr('href', '#' + this.id).
+      attr('title', _('Permalink to this headline')).
+      appendTo(this);
+    });
+    $('dt[id]').each(function() {
+      $('<a class="headerlink">\u00B6</a>').
+      attr('href', '#' + this.id).
+      attr('title', _('Permalink to this definition')).
+      appendTo(this);
+    });
+  },
+
+  /**
+   * workaround a firefox stupidity
+   * see: https://bugzilla.mozilla.org/show_bug.cgi?id=645075
+   */
+  fixFirefoxAnchorBug : function() {
+    if (document.location.hash && $.browser.mozilla)
+      window.setTimeout(function() {
+        document.location.href += '';
+      }, 10);
+  },
+
+  /**
+   * highlight the search words provided in the url in the text
+   */
+  highlightSearchWords : function() {
+    var params = $.getQueryParameters();
+    var terms = (params.highlight) ? params.highlight[0].split(/\s+/) : [];
+    if (terms.length) {
+      var body = $('div.body');
+      if (!body.length) {
+        body = $('body');
+      }
+      window.setTimeout(function() {
+        $.each(terms, function() {
+          body.highlightText(this.toLowerCase(), 'highlighted');
+        });
+      }, 10);
+      $('<p class="highlight-link"><a href="javascript:Documentation.' +
+        'hideSearchWords()">' + _('Hide Search Matches') + '</a></p>')
+          .appendTo($('#searchbox'));
+    }
+  },
+
+  /**
+   * init the domain index toggle buttons
+   */
+  initIndexTable : function() {
+    var togglers = $('img.toggler').click(function() {
+      var src = $(this).attr('src');
+      var idnum = $(this).attr('id').substr(7);
+      $('tr.cg-' + idnum).toggle();
+      if (src.substr(-9) === 'minus.png')
+        $(this).attr('src', src.substr(0, src.length-9) + 'plus.png');
+      else
+        $(this).attr('src', src.substr(0, src.length-8) + 'minus.png');
+    }).css('display', '');
+    if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) {
+        togglers.click();
+    }
+  },
+
+  /**
+   * helper function to hide the search marks again
+   */
+  hideSearchWords : function() {
+    $('#searchbox .highlight-link').fadeOut(300);
+    $('span.highlighted').removeClass('highlighted');
+  },
+
+  /**
+   * make the url absolute
+   */
+  makeURL : function(relativeURL) {
+    return DOCUMENTATION_OPTIONS.URL_ROOT + '/' + relativeURL;
+  },
+
+  /**
+   * get the current relative url
+   */
+  getCurrentURL : function() {
+    var path = document.location.pathname;
+    var parts = path.split(/\//);
+    $.each(DOCUMENTATION_OPTIONS.URL_ROOT.split(/\//), function() {
+      if (this === '..')
+        parts.pop();
+    });
+    var url = parts.join('/');
+    return path.substring(url.lastIndexOf('/') + 1, path.length - 1);
+  },
+
+  initOnKeyListeners: function() {
+    $(document).keydown(function(event) {
+      var activeElementType = document.activeElement.tagName;
+      // don't navigate when in search box, textarea, dropdown or button
+      if (activeElementType !== 'TEXTAREA' && activeElementType !== 'INPUT' && activeElementType !== 'SELECT'
+          && activeElementType !== 'BUTTON' && !event.altKey && !event.ctrlKey && !event.metaKey
+          && !event.shiftKey) {
+        switch (event.keyCode) {
+          case 37: // left
+            var prevHref = $('link[rel="prev"]').prop('href');
+            if (prevHref) {
+              window.location.href = prevHref;
+              return false;
+            }
+          case 39: // right
+            var nextHref = $('link[rel="next"]').prop('href');
+            if (nextHref) {
+              window.location.href = nextHref;
+              return false;
+            }
+        }
+      }
+    });
+  }
+};
+
+// quick alias for translations
+_ = Documentation.gettext;
+
+$(document).ready(function() {
+  Documentation.init();
+});

+ 12 - 0
docs/_build/html/_static/documentation_options.js

@@ -0,0 +1,12 @@
+var DOCUMENTATION_OPTIONS = {
+    URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'),
+    VERSION: '0.3.5',
+    LANGUAGE: 'None',
+    COLLAPSE_INDEX: false,
+    BUILDER: 'html',
+    FILE_SUFFIX: '.html',
+    LINK_SUFFIX: '.html',
+    HAS_SOURCE: true,
+    SOURCELINK_SUFFIX: '.txt',
+    NAVIGATION_WITH_KEYS: false
+};

BIN
docs/_build/html/_static/file.png


File diff suppressed because it is too large
+ 3 - 0
docs/_build/html/_static/fonts/font-awesome.css


+ 13 - 0
docs/_build/html/_static/fonts/material-icons.css

@@ -0,0 +1,13 @@
+/*!
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy
+ * of the License at:
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING, SOFTWARE
+ * DISTRIBUTED UNDER THE LICENSE IS DISTRIBUTED ON AN "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED.
+ * SEE THE LICENSE FOR THE SPECIFIC LANGUAGE GOVERNING PERMISSIONS AND
+ * LIMITATIONS UNDER THE LICENSE.
+ */@font-face{font-display:swap;font-family:"Material Icons";font-style:normal;font-weight:400;src:local("Material Icons"),local("MaterialIcons-Regular"),url("specimen/MaterialIcons-Regular.woff2") format("woff2"),url("specimen/MaterialIcons-Regular.woff") format("woff"),url("specimen/MaterialIcons-Regular.ttf") format("truetype")}

BIN
docs/_build/html/_static/fonts/specimen/FontAwesome.ttf


BIN
docs/_build/html/_static/fonts/specimen/FontAwesome.woff


BIN
docs/_build/html/_static/fonts/specimen/FontAwesome.woff2


BIN
docs/_build/html/_static/fonts/specimen/MaterialIcons-Regular.ttf


BIN
docs/_build/html/_static/fonts/specimen/MaterialIcons-Regular.woff


BIN
docs/_build/html/_static/fonts/specimen/MaterialIcons-Regular.woff2


BIN
docs/_build/html/_static/images/favicon.png


Some files were not shown because too many files changed in this diff