<![CDATA[Rodrigo Fernández]]>https://fdezromero.com/https://fdezromero.com/favicon.pngRodrigo Fernándezhttps://fdezromero.com/Ghost 2.12Thu, 14 Feb 2019 16:24:00 GMT60<![CDATA[CORS Errors in Ionic Apps]]>https://fdezromero.com/cors-errors-in-ionic-apps/5c59c7b823af6334c2751b47Tue, 05 Feb 2019 17:30:00 GMTWhat is CORS? CORS Errors in Ionic Apps

Cross-Origin Resource Sharing (CORS) is a mechanism that browsers and webviews — like the ones powering Capacitor and Cordova — use to restrict HTTP and HTTPS requests made from scripts to resources in a different origin for security reasons, mainly to protect your user's data and prevent attacks that would compromise your app.

In order to know if an external origin supports CORS, the server has to send some special headers for the browser to allow the requests.

An origin is the combination of the protocol, domain, and port from which your Ionic app or the external resource is served. For example, apps running in Capacitor have capacitor://localhost (iOS) or http://localhost (Android) as their origin.

When the origin where your app is served (e.g. http://localhost:8100 with ionic serve) and the origin of the resource being requested (e.g. https://api.example.com) don't match, the browser's Same Origin Policy takes effect and CORS is required for the request to be made.

CORS errors are common in web apps when a cross-origin request is made but the server doesn't return the required headers in the response (is not CORS-enabled):

XMLHttpRequest cannot load https://api.example.com. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:8100' is therefore not allowed access.

How does CORS work

Request with preflight

By default, when a web app tries to make a cross-origin request the browser sends a preflight request before the actual request. This preflight request is needed in order to know if the external resource supports CORS and if the actual request can be sent safely, since it may impact user data.

A preflight request is sent by the browser if:

  • The method is:
    • PUT
    • DELETE
    • CONNECT
    • OPTIONS
    • TRACE
    • PATCH
  • Or if it has a header other than:
    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type
    • DPR
    • Downlink
    • Save-Data
    • Viewport-Width
    • Width
  • Or if it has a Content-Type header other than:
    • application/x-www-form-urlencoded
    • multipart/form-data
    • text/plain
  • Or if a ReadableStream or event listeners in XMLHttpRequestUpload are used.

If any of the conditions above are met, a preflight request with the OPTIONS method is sent to the resource URL.

Let's suppose we are making a POST request to a fictional JSON API at https://api.example.com with a Content-Type of application/json. The preflight request would be like this (some default headers omitted for clarity):

OPTIONS / HTTP/1.1
Host: api.example.com
Origin: http://localhost:8100
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type

If the server is CORS enabled, it will parse the Access-Control-Request-* headers and understand that a POST request is trying to be made from http://localhost:8100 with a custom Content-Type.

The server will then respond to this preflight with which origins, methods, and headers are allowed by using the Access-Control-Allow-* headers:

HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://localhost:8100
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: Content-Type

If the returned origin and method don't match the ones from the actual request, or any of the headers used are not allowed, the request will be blocked by the browser and an error will be shown in the console. Otherwise, the request will be made after the preflight.

In our example, since the API expects JSON, all POST requests will have a Content-Type: application/json header and always be preflighted.

Simple requests

Some requests are always considered safe to send and don't need a preflight if they meet all of the following conditions:

  • The method is:
    • GET
    • HEAD
    • POST
  • Have only these headers:
    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type
    • DPR
    • Downlink
    • Save-Data
    • Viewport-Width
    • Width
  • The Content-Type header is:
    • application/x-www-form-urlencoded
    • multipart/form-data
    • text/plain
  • No ReadableStream or event listeners in XMLHttpRequestUpload are used.

In our example API, GET requests don't need to be preflighted because no JSON data is being sent, and so the app doesn't need to use the Content-Type: application/json header. They will always be simple requests.

CORS Headers

Server Headers (Response)

Header Value Description
Access-Control-Allow-Origin origin or * Specifies the origin to be allowed, like http://localhost:8100 or * to allow all origins.
Access-Control-Allow-Methods methods Which methods are allowed when accessing the resource: GET, HEAD, POST, PUT, DELETE, CONNECT, OPTIONS, TRACE, PATCH.
Access-Control-Allow-Headers headers Used in response to a preflight request to indicate which headers can be used when making the actual request, aside from the simple headers, which are always allowed.
Access-Control-Allow-Credentials true or false Whether or not the request can be made with credentials.
Access-Control-Expose-Headers headers Specifies the headers that the browser is allowed to access.
Access-Control-Max-Age seconds Indicates how long the results of a preflight request can be cached.

Browser Headers (Request)

The browser automatically sends the appropriate headers for CORS in every request to the server, including the preflight requests. Please note that the headers below are for reference only, and should not be set in your app code (the browser will ignore them).

All Requests

Header Value Description
Origin origin Indicates the origin of the request.

Preflight Requests

Header Value Description
Access-Control-Request-Method method Used to let the server know what method will be used when the actual request is made.
Access-Control-Request-Headers headers Used to let the server know what non-simple headers will be used when the actual request is made.

Solutions for CORS Errors

A. Enabling CORS in a server you control

The correct and easiest solution is to enable CORS by returning the right response headers from the web server or backend and responding to preflight requests, as it allows to keep using XMLHttpRequest, fetch, or abstractions like HttpClient in Angular.

Ionic apps may be run from different origins, but only one origin can be specified in the Access-Control-Allow-Origin header. Therefore we recommend checking the value of the Origin header from the request and reflecting it in the Access-Control-Allow-Origin header in the response.

Please note that all of the Access-Control-Allow-* headers have to be sent from the server, and don't belong in your app code.

Here are some of the origins your Ionic app may be served from:

Capacitor

Platform Origin
iOS capacitor://localhost
Android http://localhost

Replace localhost with your own hostname if you have changed the default in the Capacitor config.

Ionic WebView 3.x plugin on Cordova

Platform Origin
iOS ionic://localhost
Android http://localhost

Replace localhost with your own hostname if you have changed the default in the plugin config.

Ionic WebView 2.x plugin on Cordova

Platform Origin
iOS http://localhost:8080
Android http://localhost:8080

Replace port 8080 with your own if you have changed the default in the plugin config.

Local development in the browser

Command Origin
ionic serve http://localhost:8100 or http://YOUR_MACHINE_IP:8100
npm run start or ng serve http://localhost:4200 for Ionic Angular apps.

Port numbers can be higher if you are serving multiple apps at the same time.


Allowing any origin with Access-Control-Allow-Origin: * is guaranteed to work in all scenarios but may have security implications — like some CSRF attacks — depending on how the server controls access to resources and use sessions and cookies.

For more information on how to enable CORS in different web and app servers, please check enable-cors.org

CORS can be easily enabled in Express/Connect apps with the cors middleware:

const express = require('express');
const cors = require('cors');
const app = express();

const allowedOrigins = [
  'capacitor://localhost',
  'ionic://localhost',
  'http://localhost',
  'http://localhost:8080',
  'http://localhost:8100'
];

// Reflect the origin if it's in the allowed list or not defined (cURL, Postman, etc.)
const corsOptions = {
  origin: (origin, callback) => {
    if (allowedOrigins.includes(origin) || !origin) {
      callback(null, true);
    } else {
      callback(new Error('Origin not allowed by CORS'));
    }
  }
}

// Enable preflight requests for all routes
app.options('*', cors(corsOptions));

app.get('/', cors(corsOptions), (req, res, next) => {
  res.json({ message: 'This route is CORS-enabled for an allowed origin.' });
})

app.listen(3000, () => {
  console.log('CORS-enabled web server listening on port 3000');
});

B. Working around CORS in a server you can't control

Don't leak your keys!

If you are trying to connect to a 3rd-party API, first check in its documentation that is safe to use it directly from the app (client-side) and that it won't leak any secret/private keys or credentials, as it's easy to see them in clear text in Javascript code. Many APIs don't support CORS on purpose, in order to force developers to use them in the server and protect important information or keys.

1. Native-only apps (iOS/Android)

Use the HTTP plugin from Ionic Native to make the requests natively from outside the webview. Please note that this plugin doesn't work in the browser, so the development and testing of the app must always be done in a device or simulator going forward.

Usage in Ionic Angular 4

import { Component } from '@angular/core';
import { HTTP } from '@ionic-native/http/ngx';

@Component({
  selector: 'app-home',
  templateUrl: './home.page.html',
  styleUrls: ['./home.page.scss'],
})
export class HomePage {
  constructor(private http: HTTP) {}

  async getData() {
    try {
      const url = 'https://api.example.com';
      const params = {};
      const headers = {};

      const response = await this.http.get(url, params, headers);

      console.log(response.status);
      console.log(JSON.parse(response.data)); // JSON data returned by server
      console.log(response.headers);

    } catch (error) {
      console.error(error.status);
      console.error(error.error); // Error message as string
      console.error(error.headers);
    }
  }
}

2. Native + PWAs

Send the requests through an HTTP/HTTPS proxy that bypasses them to the external resources and adds the necessary CORS headers to the responses. This proxy must be trusted or under your control, as it will be intercepting most traffic made by the app.

Also, keep in mind that the browser or webview will not receive the original HTTPS certificates but the one being sent from the proxy if it's provided. URLs may need to be rewritten in your code in order to use the proxy.

Check cors-anywhere for a Node.js CORS proxy that can be deployed in your own server. Using free hosted CORS proxies in production is not recommended.

C. Disabling CORS or browser web security

Please be aware that CORS exists for a reason (security of user data and to prevent attacks against your app). It's not possible or advisable to try to disable CORS.

Older webviews like UIWebView on iOS don't enforce CORS but are deprecated and are very likely to disappear soon. Modern webviews like iOS WKWebView or Android WebView (both used by Capacitor) do enforce CORS and provide huge security and performance improvements.

If you are developing a PWA or testing in the browser, using the --disable-web-security flag in Google Chrome or an extension to disable CORS is a really bad idea. You will be exposed to all kind of attacks, you can't ask your users to take the risk, and your app won't work once in production.

Sources

]]>
<![CDATA[How I Host this Static Ghost Blog on Github Pages with wget]]>https://fdezromero.com/how-i-host-this-static-ghost-blog-on-github-pages-with-wget/5bf2dae9d5546074beaea791Mon, 19 Nov 2018 17:19:17 GMT

Lately I've been wanting to go back to writing more on my personal blog, so I'm currently in the process of gathering articles, slides, videos and code samples I wrote on various platforms.

Initially I wanted to write articles and pages in Markdown, generate some static blog pages from them, push to a repository, and host them in GitHub Pages. I tried several approaches, including building my blog off the great Stencil site generator. But then decided that I didn't want to deal too much with templates and design, or I would spend more time coding and I'd never get to writing 😙

Coming from Medium, I liked Ghost's design and simplicity the most. Keeping another Node.js server with NGNIX and MySQL, backups and so on, not so much 😅. I found some middle ground installing Ghost locally (with the database in a SQLite file I can commit to a repo) and generating a static site with all the articles and pages.

I found many how-to's using Buster, a Python tool that automates the process of grabbing every page generating the site. The problem is that I soon noticed that the tool is no longer maintained and many things have changed in Ghost after it was last updated, so the result is essentially broken. I spent the rest of the day trying all the other forks with different results: some fixed some things and others fixed some other things. Either links were still pointing to localhost or pages were missing.

Then I remembered that the fantastic wget command has some backup and mirroring features, which Buster were also using under the hood. So I wrote a very simple bash script to automate the static generation process.

Here are the steps I followed to set up this blog. If you have a local Ghost installation, you can skip ahead to the script.


Installing Ghost locally

To install Ghost locally you will need the following:

  • A supported version of Node.js
  • npm or yarn to manage packages
  • A clean, empty directory on your machine

Ghost has a nice CLI tool we can install globally to handle multiple blogs:

npm install -g ghost-cli

Then we need to create the empty directory were we want to store the blog, cd into it and run the local install command:

ghost install local

After that, the blog will be installed in this directory and served at https://fdezromero.com, if you don't have previous Ghost installations. The admin is accessible from https://fdezromero.com/ghost.

The database file is located at content/data, in case you want to keep a backup in a private repo.

Generating the static site

Once we have set up our Ghost blog, edited our user, created some articles and pages or installed a different theme, let's generate the static site so it can be served by GitHub Pages (or Firebase, Netlify, etc).

First, if you're using macOS like me, let's install wget via Homebrew. It's like the apt-get of macOS. If you use Linux you should have it already installed.

brew install wget

And then to the final part, the bash script. I saved it at the root directory of the blog, and called it generate.sh:

#!/bin/bash

# Copy blog content
wget --recursive --no-host-directories --directory-prefix=static --adjust-extension --timeout=30 --no-parent --convert-links https://fdezromero.com/

# Copy 404 page
wget --no-host-directories --directory-prefix=static --adjust-extension --timeout=30 --no-parent --convert-links --content-on-error --timestamping https://fdezromero.com/404.html

# Copy sitemaps
wget --recursive --no-host-directories --directory-prefix=static --adjust-extension --timeout=30 --no-parent --convert-links https://fdezromero.com/sitemap.xsl
wget --recursive --no-host-directories --directory-prefix=static --adjust-extension --timeout=30 --no-parent --convert-links https://fdezromero.com/sitemap.xml
wget --recursive --no-host-directories --directory-prefix=static --adjust-extension --timeout=30 --no-parent --convert-links https://fdezromero.com/sitemap-pages.xml
wget --recursive --no-host-directories --directory-prefix=static --adjust-extension --timeout=30 --no-parent --convert-links https://fdezromero.com/sitemap-posts.xml
wget --recursive --no-host-directories --directory-prefix=static --adjust-extension --timeout=30 --no-parent --convert-links https://fdezromero.com/sitemap-authors.xml
wget --recursive --no-host-directories --directory-prefix=static --adjust-extension --timeout=30 --no-parent --convert-links https://fdezromero.com/sitemap-tags.xml

# Replace localhost with domain
LC_ALL=C find ./static -type f -not -wholename *.git* -exec sed -i '' -e 's/http:\/\/fdezromero.com/https:\/\/fdezromero.com/g' {} +
LC_ALL=C find ./static -type f -not -wholename *.git* -exec sed -i '' -e 's/fdezromero.com/fdezromero.com/g' {} +
LC_ALL=C find ./static -type f -not -wholename *.git* -exec sed -i '' -e 's/http:\/\/www.gravatar.com/https:\/\/www.gravatar.com/g' {} +

# Set up Github Pages CNAME
echo "fdezromero.com" > static/CNAME

In order to adapt it to your blog, replace all the occurrences of fdezromero.com with your port number if different, and fdezromero.com with your own domain or subdomain. You can also use the subdomain provided by GitHub Pages, like username.github.io.

Notice that all the script does is to start crawling all links from the home page and converting them to relative when possible. Then the same with the sitemaps for better SEO. At the end, it replaces any broken link still pointing to the local installation and creates the CNAME file needed by GitHub Pages to use your own custom domain.

Once we have the script configured, we just need to give it execution permissions and run it:

chmod +x generate.sh
./generate.sh

If the blog is up, the script will scrape its contents and you'll have a static copy of your blog in the static directory. From here, you can initialize a git repo and push the site to GitHub:

cd static
git init
git remote add origin https://github.com/user/repo.git
git push master origin

And finish configuring your custom domain or subdomain to point to your blog with this guide.

]]>
<![CDATA[Tips & Tricks for Ionic on Desktop]]>https://fdezromero.com/tips-and-tricks-for-ionic-on-desktop/5bf44b2b00458904fdb03c87Thu, 15 Feb 2018 11:00:00 GMT

Originally published in The Official Ionic Blog.

We have been using Ionic at Savelist since the first alpha versions, and always thought that one of its strongest benefits is the ability to code an app for iOS, Android and the Web with a single codebase. Heck, we even use Ionic to build our browser extensions!

When we went through the process of launching the Savelist Progressive Web App in January (2017), we noticed that even though it looked as expected on mobile – as we started with a “mobile first” approach – it didn’t look quite right on the larger displays of laptops and desktops. Navigation bars and content were too wide and since most content was aligned to the left, there was too much white space on the right of almost all our screens. It felt more like the maximized version of our Android app than a website.

In the process of customizing Savelist for larger screens, we collected some tips & tricks that we wanted to share that could help you adapt your app for the desktop, without compromising the mobile experience we already had.

This guide is based on the latest release of ionic-angular at the time (3.9.2). If you’re using an older version of Ionic, I strongly recommend you to update to the latest release.

Setting up the fixed grid

Ionic has a great grid system based on flexbox that we can use to adapt content and make it responsive. It’s incredibly customizable, and we can define custom grid widths and breakpoints for every resolution.

First, we limited the grid width in order to make the content easier to navigate. For our needs, all we had to do was removed the xl breakpoint. Ionic handles this without any issues.

In our src/theme/variables.scss:

$grid-breakpoints: (
 xs: 0, // Phone (0px - 575px)
 sm: 576px, // Phablet (576px - 767px)
 md: 768px, // Tablet vertical (768px - 991px)
 lg: 992px // Tablet horizontal, Desktop (992px and above)
);

Next we modified the $grid-max-widths variable and made our app use 100% of the width until it reaches 992px:

$grid-max-widths: (
 lg: 992px
);

You could use any other size you’d want for this, as long as the value lgfrom $grid-breakpoints matches.

Using the fixed grid on pages

In order to apply a max-width to our pages, we just wrap the markup inside <ion-content> with <ion-grid fixed>. The fixed attribute tells Ionic’s grid to use the max-width corresponding to the current breakpoint, or 100% if not defined.

So this code:

<ion-content>
 <!-- Some code -->
</ion-content>

Becomes:

<ion-content>
 <ion-grid fixed>
   <!-- Some code -->
 </ion-grid>
</ion-content>

We don’t need to apply this to pages that are displayed inside modals or popovers, as they already have a fixed width.

We have better looking pages now, but the toolbars inside <ion-header> and <ion-footer> are not horizontally aligned. We can easily adjust them to match of our fixed-width pages by adding these rules at the end of app.scss:

// Desktop

ion-header .toolbar {
 margin: auto;
}

@media (min-width: map-get($grid-breakpoints, lg)) {
 ion-navbar.toolbar,
 ion-header .toolbar[fixed],
 ion-footer .toolbar[fixed],
 ion-tabs .tabbar {
   margin: 0;
   padding-right: calc((100% - #{map-get($grid-max-widths, lg)}) / 2);
   padding-left: calc((100% - #{map-get($grid-max-widths, lg)}) / 2);
 }
}

If there is any <ion-toolbar> inside <ion-header> or <ion-footer>, you may want to add the fixed attribute to center them, like <ion-toolbar fixed>.

Showing tabs at the top

While users are used to having tabs at the bottom in mobile apps, desktop webs usually show them at the top for better usability and navigation. Well known apps like Twitter, Pinterest and Instagram all use tabs at the top for their desktop experience.

Fortunately, Ionic Framework allows us to easily customize the position and layout of the tabs, so we can have bottom tabs with icons on top in our mobile experience:

Tips & Tricks for Ionic on Desktop

And top tabs with icons on the left for our desktop experience:

Tips & Tricks for Ionic on Desktop

To achieve this we can bind class properties to the tabsPlacement and tabsLayout attributes of our <ion-tabs>:

<ion-tabs [tabsPlacement]="tabsPlacement" [tabsLayout]="tabsLayout">
    <!-- Tabs here -->
<ion-tabs>

And set the values depending on the platform:

@Component({
  templateUrl: 'app.html'
})
export class AppComponent {
  tabsPlacement: string = 'bottom';
  tabsLayout: string = 'icon-top';

  constructor(
    public platform: Platform,
  ) {
    if (!this.platform.is('mobile')) {
      this.tabsPlacement = 'top';
      this.tabsLayout = 'icon-left';
    }
  }
}

You can check the different placement and layout options in the Tabs docs.

Using the responsive grid and breakpoints

One of Savelist’s key features is the ability to show all the products you have saved from the web in a clear and organized layout, so cards in a responsive grid was the best way to go in displaying that information.

This option lets you have one or two cards per row on mobile, and more cards as the screen gets bigger. Cards don’t have a fixed width and make the best use of the available space.

Tips & Tricks for Ionic on Desktop
Mobile
Tips & Tricks for Ionic on Desktop
Tablet
Tips & Tricks for Ionic on Desktop
Desktop

It’s easy to achieve this layout with the help of the grid breakpoints we set up before and col-{breakpoint}-{#}:

<ion-grid fixed>
 <ion-row>
   <ion-card ion-col *ngFor="let product of products" col-6 col-md-4 col-lg-3></ion-card>
 </ion-row>
</ion-grid>

Rows will start with 2 cards on xs and sm, 3 cards on md and 4 cards on lg and above.

Adapting the experience

We have addressed the biggest points of adapting the PWA “app shell” to big screens. However, you may still notice some items that you want to show differently, like font size, images, UI, etc.

There is no silver bullet for this, but you can achieve almost anything you want starting with this media query, so if you ever change the lg breakpoint the media query will update as well:

@media (min-width: map-get($grid-breakpoints, lg)) {
 // Your rules here
}

Another way is to show or hide elements according to the platform type instead of the screen size with showWhen and hideWhen. You can use the value core to target desktops.


Share your tricks!

This was just a few of the changes we made to Savelist in order to look better on larger screens, but I’m sure there’s more ideas out there. I’d love to know what tricks you have in your bag with working with Ionic, so leave a comment below!

]]>
<![CDATA[Injecting Providers into Async Custom Validators in Angular]]>https://fdezromero.com/injecting-providers-into-async-custom-validators-in-angular/5bf1fb12e38f017290dd07aaThu, 09 Feb 2017 11:00:00 GMT

One of the first things developers do when we are building a new app is a signup form, that checks that every field is okay and shows an appropriate error message if it’s not.

It’s also a common practice to check if the typed email is already registered or if the username is taken. Angular has some basic validators built in, but in order to check our API and return the status we need to write a custom async validator. Please read this great guide about Advanced Forms & Validation in Ionic 2 by Josh Morony first.

The only part missing in the guide is to actually inject the provider in charge of connecting to our API and getting the result. This is a bit tricky since async validators need to have static properties and we can’t inject a provider directly, so we will have to pass it to the validator class by using its constructor. This will work from Angular 2 onwards, I’m using Ionic 2.

Here is how I solved it. If you know any way to improve this code, please leave a comment with a link to your code!

import { FormControl } from '@angular/forms';
import { UserService } from '../../providers/user-service/user-service';

export class UserValidator {
  static userService: UserService;
  static EMAIL_REGEXP = /.+@.+\..+/;

  constructor(userService: UserService) {
    UserValidator.userService = userService;
  }

  // Async validator: `new UserValidator(userService).checkEmailAvailable`

  checkEmailAvailable(input: FormControl) {
    if (!input.value || !UserValidator.EMAIL_REGEXP.test(input.value)) {
      return Promise.resolve({ invalid: true });
    }

    return UserValidator.userService.checkEmail(input.value)
    .then(data => data && data.available ? null : data)
    .catch(err => console.error(err));
  }
}

We start making our class properties static, then we inject the provider UserService into the constructor. Because we have defined userService as static, we can assign the reference to the provider by using the Class.property syntax.

The validator function is actually very simple: it returns null if there aren’t any errors, or an object with every error set to true. My API actually returns { available: true } if everything is okay and then I convert that to null, but you may handle this differently.

Now every time we want to use this validator, we need to call it from our parent class like new UserValidator(userService).checkEmailAvailable .

With this snippet, it would be fairly simple to create another validator function inside the UserValidator class to check if a username is taken or not.

]]>
<![CDATA[How To Connect Virtual Networks through a VPN in the New Azure Portal (Resource Manager)]]>https://fdezromero.com/how-to-connect-virtual-networks-through-a-vpn-in-the-new-azure-portal-resource-manager/5bf1d4f63511de70fc804a35Fri, 01 Apr 2016 10:00:00 GMT

This article covers a very common use case for startups using Microsoft Azure and BizSpark: how to connect virtual machines from two virtual networks in different subscriptions (or regions) with the new Azure Portal (Resource Manager or ARM).

The BizSpark program gives startups like Savelist free Azure credits for up to 3 years. The catch is that these credits are divided in several subscriptions with $150/€130 each (one per team member). In order to get the most out of the BizSpark credits, you’ll want to use more than one subscription, but all the resources you create are bound to that subscription. The easiest (note I didn’t write easy) way is to connect the virtual networks through a VPN at the infrastructure level, using what Azure calls Virtual Network Gateways.

So to be clear, this article supposes that:

  • Your VMs and virtual networks were created in the new Azure Portal(Resource Manager mode or ARM), not the old Classic Portal (Service Manager mode or ASM) .
  • These VMs are in different virtual networks. It doesn’t matter if they are in the same subscription, in other subscription or in another Azure location. We’ll connect them with a secure VPN that will route the traffic over the Internet (but inside the Azure backbone). If they are in the same virtual network, the VMs will just see each other and you don’t need any of this.
  • You want to use the Azure Portal instead of the Azure CLI or PowerShell. Because we are cool dev hipsters, not SysAdmins from the 90s (although the Azure CLI is wrote in NodeJS and that’s also cool).

Requirements

Before we can connect the virtual networks, there are some requirements and considerations:

  • The address spaces of the virtual networks cannot collide. If they did, when a VM wanted to connect to a local IP, the virtual network wouldn’t know how to route this request.
  • We will need to create two Virtual Network Gateways, one for each network. These cost around $27/€23 a piece (in March 2016).
  • You may also have to pay for outbound data traffic sent between the networks if they are in different regions or outside of Azure, as the VPN connection will be established using public IPs, and therefore routed through the Internet. You won’t be charged twice, as inbound traffic is always free.

Deployment Overview

These are all the resources we will use. I’ll be using the letters a and b to refer to each side of the connection. You can name them whatever you want but I recommend writing your equivalent in a cheat sheet:

  • 2 existing Virtual Networks: network-a and network-b.
  • 2 or more existing Subnets: subnet-a and subnet-b, inside network-a and network-b, respectively.
  • 2 new Virtual Network Gateways: vpn-gateway-a and vpn-gateway-b.
  • 2 new Public IP addresses for the Virtual Network Gateways: vpn-ip-a and vpn-ip-b.
  • 2 new Local Network Gateways: local-gateway-a and local-gateway-b.
  • 2 new Connections: vpn-connection-a and vpn-connection-b.

In the example I will be also considering that you have 2 different subscriptions, resource groups and locations.

If you have your virtual networks under the same subscription, resource group or location it’s okay, just consider that the following are the same in your case.
  • iptions: subscription-a and subscription-b (can be the same).
  • 2 Resource Groups: resource-group-a and resource-group-b (can be the same).
  • 2 Locations: location-a and location-b (can be the same).

Network Specs

These are the specs of the two networks. Notice that the IP address spaces are different, so they don’t collide. If they do now, you will have to change the address space in one of them before you continue.

network-a
  Address space: 10.0.0.0/16
  Subnets:
    subnet-a: 10.0.0.0/24
network-b
  Address space: 10.1.0.0/16
  Subnets:
    subnet-b: 10.1.0.0/24

1. Create the Gateway Subnets

The Virtual Network Gateways that we will create in the next step need that the networks have a special subnet inside exactly named GatewaySubnet. If you already have them just skip this step.

Go to Virtual networks > Settings > Subnets and click the add Gateway subnet button. Choose an address space inside network-a that is bigger or equal to /29 and that doesn’t collide with the other subnets. For simplicity, I’m using /24. Repeat the steps with network-b so you end up with something like this:

network-a
  Address space: 10.0.0.0/16
  Subnets:
    subnet-a: 10.0.0.0/24
    GatewaySubnet: 10.0.1.0/24
network-b
  Address space: 10.1.0.0/16
  Subnets:
    subnet-b: 10.1.0.0/24
    GatewaySubnet: 10.1.1.0/24

2. Create the Virtual Network Gateways

Go to Virtual network gateways > Add and select the following:

Name: vpn-gateway-a
Virtual network: network-a
Public IP address: vpn-ip-a (new)
Gateway type: VPN
VPN type: Route-based
Subscription: subscription-a
Resource group: resource-group-a
Location: location-a

Repeat the steps for the other subscription/network:

Name: vpn-gateway-b
Virtual network: network-b
Public IP address: vpn-ip-b (new)
Gateway type: VPN
VPN type: Route-based
Subscription: subscription-b
Resource group: resource-group-b
Location: location-b

The deploy can take up to 45 minutes, but we can set some other things up meanwhile.

3. Find the IP addresses

When the deploy of the previous step starts, you will have 2 new public IP addresses, vpn-ip-a and vpn-ip-b. It’s okay if the Virtual Network gateways are still deploying because the IPs are created at the beginning. If there aren’t there just wait a couple of minutes and refresh the list.

Go to Public IP addresses, select vpn-ip-a and write down its number because we will need it later on. Do the same for vpn-ip-b. Let’s say these are my public IPs (yours will be different):

vpn-ip-a: 40.84.0.1
vpn-ip-b: 40.84.0.2

4. Create the Local Network Gateways

This is the tricky part that took me a couple of hours to figure out. In each side of the VPN, we have a virtual network with a virtual gateway (probably still deploying) that acts as a door to the outside Internet that sends and receives traffic to the other end. But we need something to tell our virtual network “hey, for these local IP address outside your address space, you will have to get them through the door at 40.84.0.X”.

So network-a needs to know that the 10.1.0.0/16 range, that is outside its address space (10.0.0.0/16), is reachable through vpn-ip-b (40.84.0.2). And network-b (10.1.0.0/16) needs to know that the outside space 10.0.0.0/16 is reachable through vpn-ip-a (40.84.0.1). This something is a Local Network Gateway.

Go to Local network gateways > Add and create one for network-a:

Name: local-gateway-b
IP address: 40.84.0.2 (vpn-ip-b)
Address space: 10.1.0.0/16
Subscription: subscription-a
Resource group: resource-group-a
Location: location-a

See what happened here? We created the local-gateway-b in the same subscription and resource group as network-a, with the public IP and address space of network-b. This is how network-a knows how to get to an IP from network-b.

Let’s now create the one for network-b:

Name: local-gateway-a
IP address: 40.84.0.1 (vpn-ip-a)
Address space: 10.0.0.0/16
Subscription: subscription-b
Resource group: resource-group-b
Location: location-b

5. Create the Connections

From this point forward you need both Virtual Network Gateways to have finished deploying. If they haven’t, just chill out or have a look at what we do at Savelist.

With the Virtual Network Gateways deployed, we now need to create a connection between local-gateway-b and vpn-gateway-a (for network-a) and another between local-gateway-a and vpn-gateway-b (for network-b). This will complete the traffic routing.

   network-a
local-gateway-b
 vpn-gateway-a
      ||
   Internet
      ||
 vpn-network-b
local-gateway-a
  network-b

Go to Local network gateways in subscription-a and select local-gateway-band then Settings > Connections > Add. The Connection type will already have Site-to-site (IPsec) selected and greyed out.

You can choose any mix of letters and numbers as your shared key (PSK), as long as you put the same in both vpn-connection-a and vpn-connection-b. It acts as a password to secure the connection.

Pro tip: The GRC site generates ultra secure passwords just for you through HTTPS. I copied and pasted the one in the 3rd box.
Name: vpn-connection-b
Connection type: Site-to-site (IPsec)
Virtual network gateway: vpn-gateway-a
Local network gateway: local-gateway-b (locked)
Shared key (PSK): (choose a mix of letters and numbers)
Subscription: subscription-a
Location: location-a

Repeat the process for local-gateway-a in subscription-b:

Name: vpn-connection-a
Connection type: Site-to-site (IPsec)
Virtual network gateway: vpn-gateway-b
Local network gateway: local-gateway-a (locked)
Shared key (PSK): (paste the same key as before)
Subscription: subscription-b
Location: location-b

If everything went well, after a few minutes the Virtual Networks Gateways will establish a connection and show with the Connected status. If it doesn’t work after 10 minutes, check that the IPs and address spaces in the Local Network Gateways are correct and from the other network.

6. Test the Connection

You can just now log into a machine in network-a and try to access a machine in network-b, and viceversa. You will see the traffic increase in the connection details. If it doesn’t work, check the network security groups used by your virtual machines and/or virtual networks. If the VPN connection shows Connected, then is working fine and something else in your networks is blocking the communication.


Special thanks to Jesús Huerta, who helped me translate the old and confusing articles from Microsoft to the newer Azure Portal and ARM.
]]>