Two years ago, in 2015, Apple announced that they were going to transition to IPv6-only network services the following year. This change forced all apps that were submitted after June 1 to support IPv6-only networking.

Unfortunately, this completely flew under our radar when we chose to use the .net network library “Lidgren” for a Unity-based game project that was started in early may of 2016.

Later during the year, when we submitted the app to App Store, it kept being rejected by the app review team. The only information we got from them was that it was rejected because of a connection issue and that it maybe had to do with IPv6. The official Lidgren source repo on github does not support IPv6, so the solution was basically to modify the source.

If you have the same problem and you are running a server with .net 4.5, there is actually a complete solution to this problem already available for free which you can check out here: UnityLidgrenIPv6. All credits to StevePro7.

Unluckily for us though, the server we used could only run .net 3.5, which means that the dual mode solution that was utilized in the above example was not compatible with the server. The dual mode socket was implemented in .net 4.5, so we had to come up with another solution.

First off is a new class we’re going to need, which contains similar code to the .net 4.5 source code.
We’ll call it NetExtensions.cs.

You can remove the MapToIPv4 function if you want, as it is not actually used for this solution. It can however be useful if you want to extend this or create a more sophisticated solution.

using System;
using System.Net;
using System.Net.Sockets;
using System.Reflection;

namespace Lidgren.Network
{
    /// <summary>
    /// Extension class for .NET 4.5 functions that are not available in earlier versions of .NET
    /// </summary>
    public static class NetExtensions
    {
        /// <summary>
        /// Used to steal private values from a class
        /// </summary>
        internal static object GetInstanceField(Type type, object instance, string fieldName)
        {
            BindingFlags bindFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic
                                     | BindingFlags.Static;
            FieldInfo field = type.GetField(fieldName, bindFlags);
            return field.GetValue(instance);
        }

        /// <summary>
        /// Converts an IPv6 address to IPv4
        /// </summary>
        public static IPAddress MapToIPv4(this IPAddress ipa)
        {
            ushort[] m_Numbers = GetInstanceField(typeof(IPAddress), ipa, "m_Numbers") as ushort[];

            foreach (ushort u in m_Numbers)
            {
                Console.WriteLine(u);
            }

            if (ipa.AddressFamily == AddressFamily.InterNetwork)
                return ipa;
            if (ipa.AddressFamily != AddressFamily.InterNetworkV6)
                throw new Exception("Only AddressFamily.InterNetworkV6 can be converted to IPv4");

            //Test for 0000 0000 0000 0000 0000 FFFF xxxx xxxx
            for (int i = 0; i < 5; i++)
            {
                if (m_Numbers[i] != 0x0000)
                    throw new Exception("Address does not have the ::FFFF prefix");
            }
            if (m_Numbers[5] != 0xFFFF)
                throw new Exception("Address does not have the ::FFFF prefix");

            //We've got an IPv4 address
            byte[] ipv4Bytes = new byte[4];
            Buffer.BlockCopy(m_Numbers, 12, ipv4Bytes, 0, 4);
            return new IPAddress(ipv4Bytes);
        }

        /// <summary>
        /// Converts an IPv6 address to IPv4
        /// </summary>
        public static IPAddress MapToIPv6(this IPAddress ipa)
        {
            if (ipa.AddressFamily == AddressFamily.InterNetworkV6)
                return ipa;
            if (ipa.AddressFamily != AddressFamily.InterNetwork)
                throw new Exception("Only AddressFamily.InterNetworkV4 can be converted to IPv6");

            byte[] ipv4Bytes = ipa.GetAddressBytes();
            byte[] ipv6Bytes = new byte[16] {
                0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xFF, 0xFF,
                ipv4Bytes [0], ipv4Bytes [1], ipv4Bytes [2], ipv4Bytes [3]
            };
            return new IPAddress(ipv6Bytes);
        }
    }
}

The thing we need to do next is to find all occurences of IPv4-only code and replace it.

First let’s implement a custom dual mode solution that is compatible with .net 3.5. To do this we need to change the BindSocket function in NetPeer.Internal.cs. The function is located at row 109. Replace the entire function with this:

private void BindSocket(bool reBind)
{
    double now = NetTime.Now;
    if (now - m_lastSocketBind < 1.0)
    {
        LogDebug("Suppressed socket rebind; last bound " + (now - m_lastSocketBind) + " seconds ago");
        return; // only allow rebind once every second
    }
    m_lastSocketBind = now;

    if (m_socket == null)
        m_socket = new Socket(AddressFamily.InterNetworkV6, SocketType.Dgram, ProtocolType.Udp);

    if (reBind)
        m_socket.SetSocketOption(SocketOptionLevel.IPv6, SocketOptionName.ReuseAddress, (int)1);

    try
    {
        m_socket.SetSocketOption(SocketOptionLevel.IPv6, (SocketOptionName)27, 0);
    }
    catch
    {
        LogDebug("Failed to unset IPv6Only. Linux and Mac have this option off by default.");
    }
 
    m_socket.ReceiveBufferSize = m_configuration.ReceiveBufferSize;
    m_socket.SendBufferSize = m_configuration.SendBufferSize;
    m_socket.Blocking = false;

    var ep = (EndPoint)new NetEndPoint(m_configuration.LocalAddress.MapToIPv6(), reBind ? m_listenPort : m_configuration.Port);

    m_socket.Bind(ep);

    try
    {
        const uint IOC_IN = 0x80000000;
        const uint IOC_VENDOR = 0x18000000;
        uint SIO_UDP_CONNRESET = IOC_IN | IOC_VENDOR | 12;
        m_socket.IOControl((int)SIO_UDP_CONNRESET, new byte[] { Convert.ToByte(false) }, null);
    }
    catch
    {
        // ignore; SIO_UDP_CONNRESET not supported on this platform
    }

    var boundEp = m_socket.LocalEndPoint as NetEndPoint;
    LogDebug("Socket bound to " + boundEp + ": " + m_socket.IsBound);
    m_listenPort = boundEp.Port;
}

Next up is to add an extra line which enables IPv6 mapping for latency simulation.

Find row 137 in NetPeer.LatencySimulation.cs. Between “connectionReset = false;” and “IPAddress ba = default(IPAddress);” add a new line:

connectionReset = false;

target = NetUtility.MapToIPv6(target);

IPAddress ba = default(IPAddress);

Now we open the file NetPeer.cs and find row 124. Replace with:

m_senderRemote = (EndPoint)new NetEndPoint(IPAddress.IPv6Any, 0);

And in the same file, at row 304, we add a line below the ArgumentNullException:

if (remoteEndPoint == null)
    throw new ArgumentNullException("remoteEndPoint");

remoteEndPoint = NetUtility.MapToIPv6(remoteEndPoint);

Next file is NetPeerConfiguration.cs, find row 96. We’re going to replace the IPAddress.Any field with a field that let’s us listen to all protocols, not only IPv4:

m_localAddress = IPAddress.IPv6Any;

Now let’s change and add a few lines in NetUtility.cs. Go to row 99 and replace the if-statement with the following:

if (NetAddress.TryParse(ipOrHost, out ipAddress))
{
    if (ipAddress.AddressFamily == AddressFamily.InterNetwork || ipAddress.AddressFamily == AddressFamily.InterNetworkV6)
    {
        callback(ipAddress);
        return;
    }
    throw new ArgumentException("This method only resolves IPv4 and IPv6 addresses");
}

Next is row 140:

foreach (var ipCurrent in entry.AddressList)
{
    if (ipCurrent.AddressFamily == AddressFamily.InterNetwork || ipAddress.AddressFamily == AddressFamily.InterNetworkV6)
    {
	callback(ipCurrent);
	return;
    }
}

Then row 177:

if (NetAddress.TryParse(ipOrHost, out ipAddress))
{
    if (ipAddress.AddressFamily == AddressFamily.InterNetwork || ipAddress.AddressFamily == AddressFamily.InterNetworkV6)
        return ipAddress;
    throw new ArgumentException("This method only resolves IPv4 and IPv6 addresses");
}

And row 190:

foreach (var address in addresses)
{
    if (address.AddressFamily == AddressFamily.InterNetwork || ipAddress.AddressFamily == AddressFamily.InterNetworkV6)
        return address;
}

Finally, we scroll down to the bottom of the class and add a new function that we’ve referenced earlier in the code modifications which is called MapToIPv6:

/// <summary>
/// Maps the IPEndPoint object to an IPv6 address, if it is currently mapped to an IPv4 address.
/// </summary>
internal static IPEndPoint MapToIPv6(IPEndPoint endPoint)
{
    if (endPoint.AddressFamily == AddressFamily.InterNetwork)
    {
        return new IPEndPoint(endPoint.Address.MapToIPv6(), endPoint.Port);
    }
    return endPoint;
}

That’s it! Now you should be able to receive and set up connections between your clients and your server.

Additional credits to Jens Nolte for some of the code modifications.

En tanke om “Unity + Lidgren: How we solved our IPv6 problems with Apple

  1. Ni Yuxuan says:

    row 140
    ipCurrent.AddressFamily == AddressFamily.InterNetwork || ipAddress.AddressFamily == AddressFamily.InterNetworkV6
    Should each side of || be both ipCurrent?
    row 190

Kommentera

E-postadressen publiceras inte. Obligatoriska fält är märkta *