IP 가 특정 네트워크에 속해있는지 확인이 필요한 기능이 있다. 예를 들다면, 특정 IP 가 amazone ip ranges 에 속해있는지 확인이 필요한 경우다.
IP 와 네트워크 계산 방식에 대해 간단히 알아보고, 특정 IP 가 IP RANGES 에 속하는지 판단하는 코드를 구현해보자.
IP 주소는 두 부분으로 구성된다.
클래스 A IPv4 주소는 네트워크 접두사 비트가 8개이다. 예를 들어 44.0.0.1에서 44는 네트워크 주소이고 0.0.1은 호스트 주소이다.
클래스 B IPv4 주소는 네트워크 접두사 비트가 16개이다. 예를 들어 128.16.0.2에서 128.16은 네트워크 주소이고 0.2는 호스트 주소이다.
클래스 C IPv4 주소는 네트워크 접두사 비트가 24개이다. 예를 들어 192.168.1.100에서 192.168.1은 네트워크 주소이고 100은 호스트 주소이다.
클래스 없는 주소 또는 Classless Inter-Domain Routing(CIDR) 주소는 가변 길이 서브넷 마스킹(VLSM)을 사용하여 IP 주소의 네트워크와 호스트 주소 비트 간의 비율을 변경한다. 서브넷 마스크는 호스트 주소를 0으로 변환하여 IP 주소의 네트워크 주소 값을 반환하는 식별자 집합이다.
CIDR 블록은 A.B.C.D/N과 같은 형태를 띠고 있다. '/' 뒤에 N은 prefix 길이이며 주소의 왼쪽으로부터 비트의 수를 가리키고 있다.
IPv4는 주소의 길이가 32비트입니다. 따라서 N비트의 CIDR prefix는 32-N 비트의 나머지를 남기며, 남은 비트로로 만들 수 있는 경우의 수는 2^(32-N)라고 할 수 있다. 짧은 CIDR prefix는 더 많은 IP 주소를 가지게 되며, 긴 CIDR prefix는 더 적은 IP 주소를 가지게 된다.
또한 CIDR는 IPv6 주소에서도 사용될 수 있는데, prefix의 길이는 0에서 128까지의 범위를 가지고 있다.(0 < N < 128) IPv6도 IPv4처럼 동일한 방식이 적용된다.
192.168.10.0/24라는 CIDR 블록이 있다. prefix길이가 24이므로 192.168.10 부분은 고정이다.
. 을 기준으로 구간을 나눈다면, 한 구간에는 8비트를 차지하기 때문이다.
따라서 Host로 사용할 수 있는 IP 주소는 192.168.10.0 ~ 192.168.10.255이다.
하지만 여기서 네트워크와 브로트캐스트 주소를 뺀 192.168.1 ~ 192.168.254 구간을 실질적으로 사용할 수 있다.
예를 들어 192.168.10.10/24의 네트워크 주소를 알고 싶을 때 다음과 같이 구할 수 있다.
public class TrustedProxiesChecker {
private static final Set<TrustIP> IP_RANGES = Set.of(
new TrustIP("3.2.34.0/26"),
new TrustIP("3.5.140.0/2"),
...
);
@Getter
private static class TrustIP {
private static final Pattern CIDR_PATTERN = Pattern.compile("^\\d+\\.\\d+\\.\\d+\\.\\d+/\\d+$");
private final String ip;
private final long baseIpToLong;
private final int prefixLength;
private TrustIP(String ip) {
String[] cidrParts = ip.split("/");
if (!CIDR_PATTERN.matcher(ip).matches()) {
throw new IllegalArgumentException("유효하지 않은 CIDR 형식입니다.");
}
this.ip = ip;
this.baseIpToLong = ipToLong(cidrParts[0]);
this.prefixLength = Integer.parseInt(cidrParts[1]);
}
private boolean isInRange(long ip) {
// 서브넷 마스크 계산
long mask = (0xFFFFFFFFL << (32 - prefixLength));
// 입력 IP 주소에 서브넷 마스크를 적용한 결과
long maskedInputIP = ip & mask;
// 두 IP 주소가 동일한 네트워크인지 판별
return maskedInputIP == baseIpToLong;
}
/**
* IP 문자열을 long 값으로 변환
*
* @param ip 변환할 IP
* @return long 으로 변환된 IP
*/
private static long ipToLong(String ip) {
try {
InetAddress inetAddress = InetAddress.getByName(ip);
byte[] addressBytes = inetAddress.getAddress();
long result = 0;
for (byte b : addressBytes) {
result = result << 8 | (b & 0xFF);
}
return result;
} catch (UnknownHostException e) {
log.error("[ipToLong] ip : {}", ip);
// IP 의 long 표현 또는 유효하지 않은 경우 0을 반환
return 0;
}
}
}
/**
* IP 지정된 프록시 범위 중 하나에 있는지 확인
*
* @param ip 확인할 IP 를 확인할 대상
* @return 주어진 IP 가 지정된 범위 중 하나에 있으면 true, 그렇지 않으면 false 를 반환
*/
public static boolean isInRange(@NonNull String ip) {
long ipToLong = TrustIP.ipToLong(ip);
for (var trustIP : IP_RANGES) {
if (trustIP.isInRange(ipToLong)) {
return true;
}
}
return false;
}
}
현재 IP 가 IP_RANGES 에 속하는지 판단하는 메서드는 다음과 같이 구현하면 된다.
@ParameterizedTest
@ValueSource(strings = {"127.0.0.0", "x.x.x.x""})
@DisplayName("IP 주소 범위에 해당되는 경우, true 를 리턴하라.")
void isInRangeTest(String ip) {
Assertions.assertThat(TrustedProxiesChecker.isInRange(ip)).isTrue();
}
쉽고 깔끔하게 잘 설명해주셔서 오늘 하루 또 배워 갑니다. 🙇♂️
덤으로 코드까지...👏