클라우드 Mac에서 검증하는 iOS IP 주소 파싱과 정규화

클라우드 Mac에서 검증하는 iOS IP 주소 파싱과 정규화

Windows에서 연결 설정을 수정한 뒤 클라우드 Mac에 검증 작업을 맡겼는데, 같은 장치의 IPv6 주소가 서로 다른 두 대상으로 처리되는 경우가 있습니다. 한쪽은 0으로 된 구간을 모두 표기하고, 다른 쪽은 이중 콜론으로 줄여 썼기 때문입니다. 연결 목록, 중복 제거 로직, 접근 규칙이 각각 원본 문자열을 비교하면 결과가 달라질 수 있습니다. 여기서는 Swift와 시스템 파싱 함수를 사용해 오프라인 검증을 구성합니다. 실제 서버가 필요하지 않으며, 네트워크 연결 상태에도 영향을 받지 않습니다.

먼저 입력 범위를 정의하기

이 예제는 IP 주소 리터럴만 받으며 도메인 이름, 포트, 전체 URL은 받지 않습니다. IPv4는 점으로 구분된 네 개의 십진수 형식을 사용하고, IPv6는 시스템이 지원하는 표준 표기법을 허용합니다. 앞뒤에 공백이 있으면 자동으로 제거하지 않고 거부합니다. 화면에 표시되는 값과 실제 연결에 쓰이는 값이 달라지는 일을 막기 위해서입니다.

IPv6의 대괄호는 URL에서 호스트를 표현할 때 사용하는 구문이지, 주소 자체의 일부가 아닙니다. %en0과 같은 영역 식별자도 이 예제의 파서에서는 처리하지 않습니다. 링크 로컬 주소에 연결해야 한다면 별도 계층에서 주소와 인터페이스 정보를 명시적으로 받아야 합니다. 퍼센트 기호 뒤를 임의로 잘라 낸 뒤 그대로 연결해서는 안 됩니다.

파싱에 성공했다는 것은 해당 텍스트가 주소를 표현할 수 있다는 뜻일 뿐입니다. 그 주소에 접근해도 된다거나 해당 장치가 온라인 상태라는 뜻은 아닙니다.

예제에 등장하는 문서용 주소는 로컬 파싱에만 사용합니다. 스크립트는 이 주소로 연결을 시도하지 않습니다.

주소 패밀리와 바이트로 결과 저장하기

클라우드 Mac의 프로젝트 디렉터리에 ip-contract.swift를 만듭니다. 먼저 xcrun swiftc --version으로 컴파일러 정보가 출력되는지 확인한 다음 아래 코드를 작성합니다. 호스트 이름을 해석할 가능성이 있는 API는 사용하지 않습니다. 리터럴 검증에 DNS 동작이 섞이지 않도록 하기 위해서입니다.

import Darwin
import Foundation

struct IPAddress: Equatable {
    let family: Int32
    let bytes: [UInt8]
}

func parseIP(_ text: String) -> IPAddress? {
    guard !text.isEmpty,
          text.utf8.count <= 45,
          !text.utf8.contains(0) else {
        return nil
    }

    var v4 = in_addr()
    let result4 = text.withCString {
        inet_pton(AF_INET, $0, &v4)
    }
    if result4 == 1 {
        return IPAddress(
            family: AF_INET,
            bytes: withUnsafeBytes(of: &v4) { Array($0) }
        )
    }

    var v6 = in6_addr()
    let result6 = text.withCString {
        inet_pton(AF_INET6, $0, &v6)
    }
    if result6 == 1 {
        return IPAddress(
            family: AF_INET6,
            bytes: withUnsafeBytes(of: &v6) { Array($0) }
        )
    }

    return nil
}

func require(_ condition: Bool, _ message: String) {
    if !condition {
        fatalError(message)
    }
}

let good = [
    "192.0.2.1",
    "2001:db8::1",
    "2001:0DB8:0000:0000:0000:0000:0000:0001",
    "::",
    "::1",
    "::ffff:192.0.2.1"
]

for text in good {
    require(parseIP(text) != nil, "Rejected valid literal")
}

let bad = [
    "",
    " 192.0.2.1",
    "192.0.2.1\n",
    "192.0.2.256",
    "192.0.2",
    "192.0.02.1",
    "0xC0000201",
    "[2001:db8::1]",
    "fe80::1%en0",
    "2001:::1",
    "192.0.2.1\u{0}ignored"
]

for text in bad {
    require(parseIP(text) == nil, "Accepted invalid literal")
}

let short = parseIP("2001:db8::1")!
let expanded = parseIP(
    "2001:0DB8:0000:0000:0000:0000:0000:0001"
)!
require(short == expanded, "Equivalent IPv6 mismatch")
require(short.bytes.count == 16, "Invalid IPv6 length")

let v4 = parseIP("192.0.2.1")!
let mapped = parseIP("::ffff:192.0.2.1")!
require(v4.bytes.count == 4, "Invalid IPv4 length")
require(v4 != mapped, "Address families collapsed")

길이 상한은 이 파싱 계약에서 정한 바이트 한도입니다. 영역 식별자가 포함된 모든 주소에 적용되는 일반적인 제한은 아닙니다. 주소 바이트는 시스템 구조체에서 직접 가져오므로 네트워크 바이트 순서를 유지합니다. 이 바이트 묶음을 다시 호스트 정수형으로 해석해 비교해서는 안 됩니다.

컴파일하고 실행한 뒤 실패 동작 확인하기

스크립트를 독립 실행 프로그램으로 컴파일합니다. 이렇게 하면 빌드 작업에 포함하기 쉽고, 실행 방식의 차이가 인터프리터 실행에 영향을 주는 것도 피할 수 있습니다.

set -eu
mkdir -p .ip-check
xcrun swiftc ip-contract.swift -o .ip-check/ip-contract
.ip-check/ip-contract

모든 검사를 통과하면 아무것도 출력하지 않고 종료 코드 0을 반환합니다. 계약 조건 중 하나라도 충족하지 못하면 0이 아닌 상태로 종료합니다. 여기서는 assert 대신 명시적인 검사를 사용합니다. 최적화 설정에 따라 어설션이 제거될 수 있기 때문입니다.

먼저 동등 비교를 잠시 부등 비교로 바꿔 작업이 실제로 실패하는지 확인한 다음 원래대로 되돌립니다. 이런 실패 유도 테스트를 통해 파이프라인이 종료 코드를 무시하거나, 로그만 저장한 채 성공으로 표시하는 문제를 찾아낼 수 있습니다. 스크립트가 “한 번 실행됐다”는 사실만 확인하고 끝내지 마세요.

정규화와 접근 정책 분리하기

동등성의 범위를 임의로 넓히지 않기

같은 IPv6 주소의 축약 표기와 전체 표기는 동등하게 취급해야 합니다. 다만 이 모델에서 IPv4 주소와 IPv4 매핑 IPv6 주소는 여전히 서로 다른 주소 패밀리에 속합니다. 서비스 요구사항상 둘을 하나의 논리적 대상으로 묶어야 한다면 명시적인 변환을 추가하고, 변환 전후의 접근 규칙을 테스트해야 합니다. 문자열 접두사를 삭제하는 방식으로 처리해서는 안 됩니다.

화면에 표시할 때는 inet_ntop으로 일관된 텍스트를 생성할 수 있습니다. 중복 제거와 접근 권한 판단에는 계속 파싱된 모델을 사용해야 합니다. 특정 시스템 버전이 출력하는 문자열 형식을 버전이 바뀌어도 영구히 유지되는 저장 규약으로 간주해서는 안 됩니다.

도메인 이름은 별도로 검토하기

도메인 이름이 이 함수를 통과하지 못한다고 해서 유효하지 않은 도메인이라는 뜻은 아닙니다. 단지 이 함수가 받는 입력 유형에 속하지 않을 뿐입니다. 제품에서 도메인 이름도 지원한다면 별도의 입력 경로를 제공하고, 이름 해석 후 실제로 연결하려는 주소 집합을 검사해야 합니다. 사용자가 입력한 호스트 문자열만 확인해서는 이름 해석 결과가 바뀌는 상황에 대응할 수 없습니다.

루프백, 사설, 링크 로컬 주소 등을 어디까지 허용할지는 제품 용도에 따라 결정해야 합니다. 장치 디버깅 도구와 공개 인터넷 리소스를 가져오는 서비스에는 서로 다른 규칙이 필요합니다. 의미가 모호한 “안전한 주소” 불리언 하나를 공통으로 사용하는 것은 적절하지 않습니다.

실패 사례를 회귀 테스트 자산으로 남기기

파서를 공유 소스 코드에 넣은 다음, macOS 명령줄 검사와 iOS 테스트 타깃에서 동일한 테스트 벡터를 실행합니다. 전자는 원격 환경에서 오류를 빠르게 걸러 내는 데 적합하고, 후자는 대상 시스템의 동작을 확인하는 역할을 합니다. 호스트에서 통과했다는 결과로 대상 플랫폼의 검증을 대신할 수는 없습니다.

문제를 수정할 때마다 원본 입력, 예상 주소 패밀리, 예상 결과를 테스트에 추가합니다. 특히 널 바이트가 삽입된 입력은 원본 바이트를 반드시 보존해야 합니다. 로그에 표시할 때 널 바이트가 드러나지 않을 수 있기 때문입니다. C 문자열은 널 바이트에서 끝나므로 입력 단계의 검사가 빠지면 앞부분의 유효한 문자열만 검증하게 될 수 있습니다.

마지막으로 세 가지를 확인합니다. 서로 다른 텍스트가 같은 주소로 인식되는지, 서로 다른 주소 패밀리의 경계가 유지되는지, 잘못된 입력이 연결 전에 거부되는지입니다. 이 계약이 안정적으로 유지되면 이후 UI 검증이나 연결 정책을 추가할 때도 반복 실행할 수 있는 검증 기준을 갖추게 됩니다.

자주 묻는 질문

IPv6 문자열이 다르면 서로 다른 주소인가요?

아닙니다. 0 축약, 선행 0, 16진수 대소문자로 표기가 달라집니다. 파싱한 주소 계열과 네트워크 바이트 순서의 데이터를 비교해야 합니다.

inet_pton에 통과한 주소는 바로 연결해도 되나요?

아닙니다. 루프백, 사설, 링크 로컬, 매핑 주소에 대한 접근 정책이 따로 필요합니다. DNS로 얻은 주소도 별도로 검사해야 합니다.

전용 클라우드 Mac에 작업을 맡기세요

작업 일정에 맞춰 위치와 요금제를 선택하고 전용 물리 Mac mini를 대여하세요. 개통 후 콘솔에서 접속 정보를 확인할 수 있으며, 이용 중 문제가 생기면 콘솔에서 지원 티켓을 제출하세요.

클라우드 Mac 요금제 선택

작업 일정에 맞는 위치 선택

사양과 이용 기간을 확인한 뒤 나만의 전용 물리 Mac mini를 개통하세요.