내 컴퓨터의 외부 IP 주소 획득하기

컴퓨터의 외부 IP 주소 (외부 인터넷 호스트가 내 컴퓨터를 볼 때 어떤 IP 주소로 인식하는지)를 얻어올 수 있는 라이브러리가 있으면 좋겠다는 생각에서 출발하여 만든 간단한 라이브러리 NuGet 패키지를 올려봅니다.

하고자 하는 일은 단순하지만 의외로 생각해볼 점이 많았던 흥미로운 구현 작업이었습니다.

  1. 시중에 ipify, ip6.me 등 무료로 IP 주소를 보여주는 서비스들이 많이 있습니다만, 수익 모델이 분명하지 않은 무료 서비스들이라 서비스 지속 가능성이 보장되지 않는다고 판단했습니다. 그래서 찾아보니 비슷한 서비스들이 시중에 이미 많이 나와있었고, 이런 서비스들을 동시에 쿼리하도록 라이브러리를 설계하는 것이 안정성에 도움이 되겠다고 생각했습니다.

  2. HttpClient를 그냥 사용하면, 현재 컴퓨터가 듀얼 스택 네트워크 (IPv4와 IPv6를 동시에 쓰는 상태)에서는 외부에서 보여지는 IP 주소 버전을 명시적으로 선택할 수 없다는 문제가 있었습니다. 이를 해결하기 위해서 SocketsHttpHandler를 사용하면 되겠지만, 안타깝게도 이 API는 .NET Standard에 포함된 스펙이 아니었고, .NET Standard 환경에서 쓸 수 있는 범용 NuGet 패키지를 만들고 싶었기 때문에, HTTP 클라이언트 대신 과감하게 System.Net.Sockets.Socket 클래스를 사용하도록 바꾸었고, SSL 지원도 같이 고려할 수 있도록 만들었습니다.

  3. 듀얼 스택 네트워크를 테스트하기 위해, 개인적으로 보유하고 있는 Azure VM에 듀얼 스택 네트워크를 붙여 이 라이브러리가 IPv6 환경에서 잘 동작하는지 테스트해볼 수 있었습니다.

  4. 와일드카드 도메인 서비스는 IP 주소를 이용하여 퍼블릭 DNS 주소를 만들어주는 유용한 서비스입니다. 이 중에서도 sslip.io의 경우 기본 서비스 외에도 GitHub에 sslip DNS 서버 (Go로 만듦)를 오픈 소스로 공개하고 있어서, IP 주소를 와일드카드 도메인 주소로 변환해주는 도우미 메서드를 간단히 추가하면 유용하겠다는 생각이 들어 해당 구현을 추가했습니다.

이러한 배경 맥락에서 다음과 같이 프로젝트와 NuGet 패키지를 릴리즈해보았습니다.

코드 사용 방법은 대략 다음과 같습니다.

using ReflectionIPAddress;

...

var services = new PublicAddressReflectionServices()
	.AddService<IpifyService>()
	.AddService<SeeIPService>()
	.AddService<IP6MeService>()
	.AddService<CurlMyIPService>()
	.AddService<ICanHazIPService>()
	.AddService<IFConfigService>();

// Returns the IP address by checking for the fastest successful response among the specified services.
var ipv4Address = await services.ReflectIPv4Async();
var sslipDomain = ipv4Address.ToSSLIPDomain();

//var ipv6Address = await services.ReflectIPv6Async();

Console.Out.WriteLine($"IPv4 Address: {ipv4Address}, SSLIP Domain: {sslipDomain}");")

프로젝트에 관심있으신 분들의 많은 활용과 피드백을 부탁드립니다!

10개의 좋아요

덧. TCP 소켓을 사용하기로 결정한 것은 제가 생각하기에 당분간은 문제가 없을 것으로 본 것이, HTTP 버전이 올라가더라도 Backward Compatibility를 보장해야 한다는 암묵적인 약속이 있는 것으로 보이고, 라이브러리에서 참조하는 서비스들 모두는 당장 HTTP/1.1 사양 지원을 곧바로 drop하지는 않을 것으로 보여 Socket으로 80이나 443 TCP 포트로 직접 연결하는 구현을 시도했습니다.

좀 더 범용적인 구현 방법 (HTTP 3.0의 QUIC/UDP까지 커버하는 구현 방법)을 쓰면서도 .NET Standard 범주 안에 속하도록 만들 방법은 개선 과제로 두어야 할 것 같습니다. :smiley:

1개의 좋아요

webrtc의 signaling이나 기타 p2p계열에서 사용하기 좋겠네요.
webrtc에서 이런 방식으로 IP를 얻어서 사용하자고 하면 물론 반발이 많겠지만요.
실제로 겪어봤거든요. ㅎㅎ

1개의 좋아요

.net framework 에서 못쓰나요 ? CORE 부터에여 ?
지금 프리 뛰는 곳이 워낙 레거시 해서
.net framework 4.0 인디 설치가 안되요 ㅠㅠ

아…
.NET Framework 4.6.1+ 부터 군요…

4.8로 바꾸고 도전

2개의 좋아요
using System.Buffers.Binary;
using System.Net;
using System.Net.Sockets;

internal class Program
{
    private static void Main(string[] args)
    {
        var ep = FetchGoogleStunAsync().Result;
        Console.WriteLine(ep.Port);
        Console.WriteLine(ep.Address);
        Console.WriteLine(ep);
    }

    
    private static Task<IPEndPoint> FetchGoogleStunAsync(CancellationToken cancellationToken = default)
    {
        return FetchStunAsync("stun.l.google.com", 19302, cancellationToken);
    }


    private static async Task<IPEndPoint> FetchStunAsync(string host, int port, CancellationToken cancellationToken)
    {
        const short TYPE_REQUEST = 0x0001;
        const short TYPE_RESPONSE = 0x0101;
        const int STUN_MAGIC = 0x2112A442;
        using UdpClient client = new(host, port);
        byte[] req = new byte[20];
        BinaryPrimitives.WriteInt16BigEndian(req, TYPE_REQUEST);
        BinaryPrimitives.WriteInt32LittleEndian(req.AsSpan(4), STUN_MAGIC);
        Random.Shared.NextBytes(req.AsSpan(8, 12));
        await client.SendAsync(req, cancellationToken);
        UdpReceiveResult res = await client.ReceiveAsync(cancellationToken);
        if (BinaryPrimitives.ReadInt16BigEndian(res.Buffer) != TYPE_RESPONSE)
            throw new InvalidDataException();
        if (BinaryPrimitives.ReadInt32LittleEndian(res.Buffer.AsSpan(4)) != STUN_MAGIC)
            throw new InvalidDataException();
        return Parse(res.Buffer.AsSpan(20));

        static IPEndPoint Parse(Span<byte> buffer)
        {
            while(0 < buffer.Length)
            {
                if(ReadUInt16(ref buffer) == 0x0001)
                {
                    ReadUInt16(ref buffer); // size
                    ushort familly = ReadUInt16(ref buffer);
                    int port = ReadUInt16(ref buffer);
                    return familly switch {
                        0x01 => new(new IPAddress(buffer[..4]), port),  // IPV4;
                        0x02 => new(new IPAddress(buffer[..16]), port), // IPV6;
                        _ => throw new NotSupportedException() };
                }
                int size = ReadUInt16(ref buffer);
                buffer = buffer[size..];
            }
            throw new InvalidDataException();
        }

        static ushort ReadUInt16(ref Span<byte> buffer)
        {
            ushort value = BinaryPrimitives.ReadUInt16BigEndian(buffer);
            buffer = buffer[sizeof(ushort)..];
            return value;
        }
    }
}

이거 보면서 생각난게 있어서
구글 서버를 통해 IP를 가져오는 코드 한번 작성해봤습니다.

3개의 좋아요

저도 오늘 알게되었는데, STUN 서버를 이용할 수도 있겠습니다.

다만 UDP가 안되는 환경에서도 같이 쓸 수 있게 한다는 차원에서, 지금 구현체에 STUN 지원을 추가해서 동시에 query하도록 보완해보려 합니다.

3개의 좋아요

아마존 AWS 서비스와 클라우드 플레어 서비스 URL도 첨부드립니다.
https://checkip.amazonaws.com
https://www.cloudflare.com/cdn-cgi/trace

2개의 좋아요

구현해주신 코드를 인용해서 STUN 지원을 추가했습니다. 다시 한 번 감사드립니다! (커밋 로그와 소스 코드 상에 @tkm 님의 코드를 인용했다고 명시해두었습니다.)

https://www.nuget.org/packages/ReflectionIPAddress/0.6.1

AWS checkip는 살펴보니 AAAA 레코드가 아쉽게도 등록되어있지 않아서 라이브러리에서는 쓸 수 없을 것 같습니다 :sob: (참고: https://www.nslookup.io/domains/checkip.amazonaws.com/dns-records/aaaa/)

Cloudflare 쪽도 반영했습니다.!

1개의 좋아요