1
votes

I'm trying to make a IP version-agnostic client/server. I've been playing around with this in C++ and came up with something that works using the IN6ADDR_SETV4MAPPED macro (as Microsoft so kindly recommends). I followed the code sample here to accomplish this; my code that converts the address is no different from the example and everything works. I can connect to the server from the client by typing in both an IPv4 and IPv6 address (application does the mapping accordingly).

Now I'm looking for a solution in C# to upgrade a simple chat server I made and I've been unable to find any resources on how to use mapped addresses. I haven't found a function that provides the equivalent of IN6ADDR_SETV4MAPPED or any other facility in .net. My question is: how can I go about using a IPv4-mapped IPv6 address in C# (client-side)?

What I've tried:

  1. Prepend string "::ffff:" to dotted IPv4 notation, call Socket.Connect using this address. Resulting address string looks like ::ffff:127.0.0.1.
  2. Prepend string "::ffff:". Convert each octect from dotted format into hex and separate with colons, call Socket.Connect. Resulting address string looks like ::ffff:7f:0:0:1.

Neither of these approaches have worked so far.

Code snippet for server:

this.m_endPoint = new IPEndPoint(IPAddress.IPv6Any, 1337);
this.m_server.SetSocketOption(SocketOptionLevel.IPv6, SocketOptionName.IPv6Only, 0);
this.m_server.Bind(this.m_endPoint);
this.m_server.Listen(10);

Code snippet for client:

public ClientNet(string host, short port)
{
    IPAddress ip;
    if(IPAddress.TryParse(host, out ip))
    {
        string[] octs = host.Split(new char[] { '.' });
        host = "::ffff:";
        for(int i = 0; i < octs.Length; ++i)
        {
            host += string.Format("{0:x}", int.Parse(octs[i]));
            if(i + 1 != octs.Length)
            {
                host += ":";
            }
        }
    }
    else
    {
        throw new ClientCreateException("[in ClientNet.Constructor] Unable to create client; use IPv4 address");
    }
    Socket client = new Socket(AddressFamily.InterNetworkV6, SocketType.Stream, ProtocolType.Tcp);
    client.Connect(host, port);
    . . . //More initialization
}
1

1 Answers

4
votes

Came back to this today thinking I might be able to figure it out. And I did! The answer is quite easy and I feel like a fool for not getting it for a year.

Two things about the code I posted:

  1. Should have used IPAddress.MaptoIPv6 (see MSDN link) instead of that silly, contrived loop I wrote that's more prone to errors.

    a. I realized later while working in .NET 4.0 that the convenience functions I used in my sample are not available until .NET 4.5. A quick code sample I threw together is at the bottom of this post, in case anyone else is stuck in an earlier version of .NET.

  2. Real solution: Needed to call client.SetSocketOption(SocketOptionLevel.IPv6, SocketOptionName.IPv6Only, 0); prior to calling client.Connect().

Here is a full code example of a sample application I wrote today to test it out. I was able to make a connection using both ::1 and 127.0.0.1 as addresses. Note that the server Socket is created for IPv6, and that the SocketOptionName.IPv6Only option is set to 0 on both the client and the server.

using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
namespace sixsharp
{
    class Program
    {
        static void Main(string[] args)
        {
            if(args.Length <= 0) //run as server
                RunServer();
            else
                RunClient(args);
            Console.WriteLine("Press enter to close.");
            Console.ReadLine();
        }
        static void RunServer()
        {
            using(Socket serv = new Socket(AddressFamily.InterNetworkV6, SocketType.Stream, ProtocolType.Tcp))
            {
                serv.SetSocketOption(SocketOptionLevel.IPv6, SocketOptionName.IPv6Only, 0);
                serv.Bind(new IPEndPoint(IPAddress.IPv6Any, 1337));
                serv.Listen(5);
                Console.Write("Listening for client connection...");
                using(Socket client = serv.Accept())
                {
                    Console.WriteLine("Client connection accepted from {0}", client.RemoteEndPoint.ToString());
                    byte[] buf = new byte[128];
                    client.Receive(buf, 128, SocketFlags.None);
                    Console.WriteLine("Got \'{0}\' from client", Encoding.ASCII.GetString(buf));
                    Console.WriteLine("Echoing response");
                    client.Send(buf);
                    client.Shutdown(SocketShutdown.Both);
                }
            }
            Console.WriteLine("Done.");
        }
        static void RunClient(string[] args)
        {
            using(Socket client = new Socket(AddressFamily.InterNetworkV6, SocketType.Stream, ProtocolType.Tcp))
            {
                client.SetSocketOption(SocketOptionLevel.IPv6, SocketOptionName.IPv6Only, 0);
                Console.WriteLine("Setting up address, input is {0}", args[0]);
                IPEndPoint ep;
                try
                {
                    ep = new IPEndPoint(IPAddress.Parse(args[0]), 1337);
                }
                catch(FormatException fe)
                {
                    Console.WriteLine("IP address was improperly formatted and not parsed.");
                    Console.WriteLine("Detail: {0}", fe.Message);
                    return;
                }
                if(ep.AddressFamily == AddressFamily.InterNetwork)
                {
                    ep = new IPEndPoint(ep.Address.MapToIPv6(), ep.Port);
                    if(!ep.Address.IsIPv4MappedToIPv6 || ep.Address.AddressFamily != AddressFamily.InterNetworkV6)
                    {
                        Console.WriteLine("Error mapping IPv4 address to IPv6");
                        return;
                    }
                }
                Console.WriteLine("Connecting to server {0} ...", ep.ToString());
                try
                {
                    client.Connect(ep);
                }
                catch(Exception ex)
                {
                    Console.WriteLine("Unable to connect.\n Detail: {0}", ex.Message);
                    return;
                }
                client.Send(Encoding.ASCII.GetBytes("This is a test message. Hello!"));
                byte[] buf = new byte[128];
                client.Receive(buf);
                Console.WriteLine("Got back from server: {0}", Encoding.ASCII.GetString(buf));
                client.Shutdown(SocketShutdown.Both);
            }
            Console.WriteLine("Done.");
        }
    }
}

Client output:

Setting up address, input is 10.2.6.179
Connecting to server [::ffff:10.2.6.179]:1337 ...
Got back from server: This is a test message. Hello!

Done.
Press enter to close.

Server output:

Listening for client connection...Client connection accepted from [::ffff:10.2.6.179]:56275
Got 'This is a test message. Hello!
' from client
Echoing response
Done.
Press enter to close.

Sample extension methods providing the missing convenience functions in earlier versions of .NET:

static class IPAddressExtensions
{
    public static IPAddress MapToIPv6(this IPAddress addr)
    {
        if(addr.AddressFamily != AddressFamily.InterNetwork)
            throw new ArgumentException("Must pass an IPv4 address to MapToIPv6");

        string ipv4str = addr.ToString();

        return IPAddress.Parse("::ffff:" + ipv4str);
    }

    public static bool IsIPv4MappedToIPv6(this IPAddress addr)
    {
        bool pass1 = addr.AddressFamily == System.Net.Sockets.AddressFamily.InterNetworkV6, pass2;

        try
        {
            pass2 = (addr.ToString().StartsWith("0000:0000:0000:0000:0000:ffff:") ||
                    addr.ToString().StartsWith("0:0:0:0:0:ffff:") ||
                    addr.ToString().StartsWith("::ffff:")) && 
                    IPAddress.Parse(addr.ToString().Substring(addr.ToString().LastIndexOf(":") + 1)).AddressFamily == AddressFamily.InterNetwork;
        }
        catch
        {
            return false;
        }

        return pass1 && pass2;
    }
}