Tuesday, November 15, 2016

Unity3D and IPv6 Networking

At WWDC 2015, Apple announced the transition to IPv6-only network services in iOS 9. Starting June 1st 2016 all apps submitted to the App Store must support IPv6-only networking. Therefore, Unity3D posted Unity and IPv6 Support in response to this since many developers publish Unity games to the App Store.
Let's check it out!

IPv6
IPv6 (Internet Protocol version 6) is the most recent version of the Internet Protocol communications protocol that provides identification for computers on networks and routes traffic across the Internet.

Therefore, Apple's new requirement means that apps must be able to operate on an IPv6-only network!

Example
As an example, suppose that you have a typical client / server networked game that uses the Lidgren library as the networking layer to communicate messages from the client to the server and vice versa.

Lidgren
Initially, the Lidgren library uses IPv4-only specific APIs to communicate messages from the client to the server. Therefore, here is a short list of tasks to transition the code to support IPv6-only networking now:
  • Look for IPv4 addresses (e.g. 127.0.0.1, 8.8.4.4). Any hardcoded addresses should be removed. Prefer host names: use to look up IPv4 or IPv6 address of device for the proper type of network.

  • Look for the use of the IPAddress.AddressFamily property. Any code that branches based on the value of the AddressFamily must ensure the code handles IPv6-only networking option properly.

  • Look for the use of IPAddress.Any and IPAddress.Loopback fields. These fields work with IPv4 addresses not IPv6. Use IPAddress.IPv6Any and IPAddress.IPv6Loopback for IPv6 compatibility.

Fortunately, a Lidgren pull request has been submitted by jens-nolte to implement IPv6 Dual Mode, that is, upgrade Lidgren library to support both IPv4 and IPv6 network connections in client and server code.

However, all C#/.NET code in pull request is based on .NET Framework 4.5 whereas Unity uses Mono 3.5! Therefore, implement the server as Dual Mode but the client must detect at runtime either IPv4 or IPv6.

Server
Let's implement the server first [Dual Mode] as is easier + simply requires .NET Framework 4.5 (above). Channel all network connectivity through IPv6. If IPv6 then leave otherwise map IPv4 address to IPv6.

NetPeer.cs
public NetPeer(NetPeerConfiguration config)
{
  m_senderRemote = (EndPoint)new IPEndPoint(IPAddress.IPv6Any, 0);
}
public virtual NetConnection Connect(IPEndPoint remoteEndPoint, NetOutgoingMessage hailMessage)
{
  remoteEndPoint = NetUtility.MapToIPv6(remoteEndPoint);
}
NetPeer.Internal.cs
private void BindSocket(bool reBind)
{
  m_socket = new Socket(AddressFamily.InterNetworkV6, SocketType.Dgram, ProtocolType.Udp);
  m_socket.DualMode = true;

  var addr = m_configuration.LocalAddress.MapToIPv6();
  var port = reBind ? m_listenPort : m_configuration.Port;

  var ep = (EndPoint)new IPEndPoint(addr, port);
  m_socket.Bind(ep);
}
NetPeer.LatencySimulation.cs
internal bool ActuallySendPacket(byte[] data, int numBytes, IPEndPoint target, out bool connectionReset)
{
  target = NetUtility.MapToIPv6(target);
}
NetPeerConfiguration.cs
public NetPeerConfiguration(string appIdentifier)
{
  m_localAddress = IPAddress.IPv6Any;
}
NetUtility.cs
public static void ResolveAsync(string ipOrHost, ResolveAddressCallback callback)
{
  if (ipAddress.AddressFamily == AddressFamily.InterNetwork || 
      ipAddress.AddressFamily == AddressFamily.InterNetworkV6) {}
}
internal static IPEndPoint MapToIPv6(IPEndPoint endPoint)
{
  if (endPoint.AddressFamily == AddressFamily.InterNetwork)
  {
    return new IPEndPoint(endPoint.Address.MapToIPv6(), endPoint.Port);
  }
  return endPoint;
}

Client
Let's implement the client: detect network connectivity from IPv4 or IPv6 at runtime and inject either AddressFamily.InterNetwork | IPAddress.Any into Lidgren for IPv4 or AddressFamily.InterNetworkV6 | IPAddress.IPv6Any for IPv6.

Create custom class to store the IPAddress and AddressFamily from IPv4 or IPv6 and inject at runtime.
public struct NetGameConfig
{
  public NetGameConfig(IPAddress netIPAddress, AddressFamily netAddressFamily)
    : this()
  {
    NetIPAddress = netIPAddress;
    NetAddressFamily = netAddressFamily;
  }

  public IPAddress NetIPAddress { get; private set; }
  public AddressFamily NetAddressFamily { get; private set; }
}

public enum IPProtocol { IPv4, IPv6 }

Unity4
Note: if you are on an older version of Unity e.g. Unity4 then you must upgrade to Unity 4.7.2f1. If you use .NET / IL2CPP libraries, e.g. to support 64-bit architecture on iOS, then upgrade to Unity 4.7.2p1; Especially if ReceiveFrom() method is used on the Socket otherwise sender remote becomes corrupt!

Finally, remove any code referencing the LocalEndPoint property on the Socket otherwise an exception will be thrown. Again this is if you use IL2CPP scripting backend to support 64-bit architecture on iOS.

IMPORTANT
The corresponding code will not work on Android platform with Unity4: a SocketException will occur as Unity Technologies explicitly decided not to back port these changes. More information is available here.

NetClient.cs
public NetClient(NetPeerConfiguration config, NetGameConfig netGameConfig)
  : base(config, netGameConfig) {}
NetPeer.cs
public NetGameConfig NetGameConfig { get; protected set; }

public NetPeer(NetPeerConfiguration config, NetGameConfig netGameConfig)
{
  NetGameConfig = netGameConfig;
  m_senderRemote = (EndPoint)new IPEndPoint(netGameConfig.NetIPAddress, 0);
}
public NetConnection Connect(string host, int port)
{
  var addr = NetUtility.Resolve(host, NetGameConfiguration.NetAddressFamily);
  return Connect(new IPEndPoint(addr, port), null);
}
NetPeer.Discovery.cs
public bool DiscoverKnownPeer(string host, int serverPort)
{
  IPAddress address = NetUtility.Resolve(host, NetGameConfig.NetAddressFamily);
}
NetPeer.Internal.cs
private void BindSocket(bool reBind)
{
  m_socket = new Socket(NetGameConfig.NetAddressFamily, SocketType.Dgram, ProtocolType.Udp);
}
NetPeer.Send.cs
public void SendUnconnectedMessage(NetOutgoingMessage msg, string host, int port)
{
  IPAddress adr = NetUtility.Resolve(host, NetGameConfig.NetAddressFamily);
}
NetPeerConfiguration.cs
private NetGameConfig m_NetGameConfig;

public NetPeerConfiguration(string appIdentifier, NetGameConfig netGameConfig)
{
  m_NetGameConfig = netGameConfig;
  m_localAddress = m_NetGameConfig.NetIPAddress;
}
NetServer.cs
public NetServer(NetPeerConfiguration config, NetGameConfig netGameConfig)
  : base(config, netGameConfig) {}
NetUtility.cs
public static void ResolveAsync(string ipOrHost, AddressFamily addressFamily, ResolveAddressCallback callback)
{
  if (ipAddress.AddressFamily == addressFamily) {}
}
Finally, create an IPNetworkManager to get the host address for the IPProtocol type and resolve the DNS.


Testing
Apple recommend the easiest way to test for IPv6 DNS64 / NAT64 compatibility, which is the type of network most cellular carriers deploy, is to set up a local IPv6 DNS64 / NAT64 network with your Mac.

Hosts
If you would like to setup hosts files then assume example has 1x Game Server and 2x Game Clients:
 Environment  Platform  Mode  Address  Host
 Game Server  Windows PC  Dual  fc00:992b:a::1  GameServer.v6net
 Game Client  Windows PC  IPv4  fc00:992b:a::4  StevePro-PC.v6net
 Game Client  Mac OS/X  IPv6  fc00:992b:a::6  SteveProIMac.v6net

Game Server
Edit the hosts file located at C:\Windows\System32\drivers\etc
# localhost name resolution is handled within DNS itself.
127.0.0.1        localhost 
::1              localhost
fc00:992b:a::1   GameServer.v6net
fc00:992b:a::4   StevePro-PC.v6net
fc00:992b:a::6   SteveProIMac.v6net

Game Client [PC]
Edit the hosts file located at C:\Windows\System32\drivers\etc
# localhost name resolution is handled within DNS itself.
127.0.0.1        localhost 
::1              localhost
fc00:992b:a::1   GameServer.v6net
fc00:992b:a::4   StevePro-PC.v6net
Disable IPv4 networking to ensure IPv6-only connection. Local Area Connection | Double click (TCP/IPv6)

Game Client [Mac]
Launch terminal as root. Edit the hosts file located at /etc/hosts
# localhost is used to configure the loopback interface
# when the system is booting.  Do not change this entry.
127.0.0.1        localhost
255.255.255.255  broadcasthost
::1              localhost 
fc00:992b:a::1   GameServer.v6net
fc00:992b:a::6   SteveProIMac.v6net
Disable IPv4 networking to ensure IPv6-only connection. System Preferences | Network. Click Advanced
Download code sample here.

Summary
At the time of this writing, Lidgren uses IPv4-only specific APIs to communicate all networked messages. These APIs are built into .NET Framework 3.5 and therefore can currently be accessed from within Unity.

However, to access IPv6-only specific APIs from within Unity [Dual Mode] requires .NET Framework 4.5. The .NET Profile Upgrade is currently on the Unity roadmap for this functionality although there's no ETA!

No comments:

Post a Comment