본문 바로가기
Java

ip 범위 구하고 범위안에 포함되는지(with subnetmask)

by ybs 2021. 8. 24.
반응형

내부 로직을 다 이해해도 조금만 시간 지나면 까먹어서 정리했다. 코드는 IPv4 만 구현되어 있다.

서브넷 마스크, 넷마스크 용어를 엄격하게 구분져서 쓰지 않았다. 엄격히 구분할 필요가 없다고 판단했다.

 

먼저 ip 가 서로 같은지 다른지 비교를 해보자. 192.168.254.252와 192.168.254.253 은 당연히 다르다. string equal 을 이용해 바로 확인이 가능하다.

 

그런데 192.168.254.252/13 와 같이 subnetmask 가 같이 있는 경우, 192.175.254.253 이 포함되는지 여부는 바로 알기 어렵다(답은 포함됨).

 

코드를 보면서 하나씩 살펴보자. 먼저 IpAddressMatcher 생성자로 ip 와 subnetmask 문자열을 / 구분자로 함께 전달한다.

IpAddressMatcher ipAddressMatcher = new IpAddressMatcher("192.168.254.252/13");

다음 코드는 생성자 내부 로직이다. / 구분자를 기준으로 ipAddressString 은 192.168.254.252 가 되고 netMaskString 는 13 가 된다. / 가 없으면 netmask 는 -1이 된다(이 경우 단순 ip 문자 비교가 이뤄진다).

public IpAddressMatcher(String ipAddress) throws NumberFormatException {
	if (ipAddress.indexOf('/') > 0) {
		String[] addressAndMask = StringUtils.split(ipAddress, "/");
		if (addressAndMask.length != 2) {
			throw new NumberFormatException("Invalid CIDR format");
		}

		String ipAddressString = addressAndMask[0];
		String netMaskString = addressAndMask[1];

		makeIpAddress(ipAddressString);
		makeNetMask(netMaskString);
	} else {
		makeIpAddress(ipAddress);
		this.netmask = -1;
	}
}

다음으로 makeIpAddress 메서드를 통해서 int 타입의 ipAddress를 얻는다.

private void makeIpAddress(String ipAddressString) {

	String[] octets = ipAddressString.split("\\.");
	if (octets.length != 4) {
		throw new NumberFormatException("Invalid IP address: " + ipAddressString);
	}

	int i = 24;
	this.ipAddress = 0;

	for (int n = 0; n < octets.length; n++) {

		int value = Integer.parseInt(octets[n]);
		if (value != (value & 0xff)) { // 0xff는 255
			throw new NumberFormatException("Invalid IP address: " + ipAddressString);
		}

		this.ipAddress += value << i;
		i -= 8;
	}
}

 

192.168.254.252 문자열을 . 구분자로 분리해서 octets 배열에 담고 for문을 돌리면서 각각의 int 값을 얻어 ipAddress에 더한다.

아래에 과정별 로그가 있는데 먼저 192는 Integer.parseInt를 통해 11000000 가 된다. 그리고 i는 24이므로 24만큼 왼쪽으로 shift 되서 11000000000000000000000000000000 가 된다(빈자리는 0으로 채워짐)

value : 11000000
i : 24
ipAddress : 11000000000000000000000000000000
value : 10101000
i : 16
ipAddress : 11000000101010000000000000000000
value : 11111110
i : 8
ipAddress : 11000000101010001111111000000000
value : 11111100
i : 0
ipAddress : 11000000101010001111111011111100

 

다음번은 168에 해당하는 10101000 가 16만큼 shift 된다. 그리고 마지막 for문까지 마치면 ipAddress 는 이진수로 표현된 값을 얻는다(11000000101010001111111011111100).

 

다음은 subnet mask 를 int 타입으로 얻는 과정이다. netMaskString 13이라는 숫자는 1이 13개 있다는 뜻이다(왼쪽부터).

private void makeNetMask(String netMaskString) {
	int netMaskInteger = Integer.parseInt(netMaskString);
	if (netMaskInteger < 1 || netMaskInteger > 32) {
		throw new NumberFormatException("netMaskInteger value is wrong");
	}

	// 최소단위 A 클래스 255.0.0.0
	if (netMaskInteger < 8) {
		throw new NumberFormatException("Netmask CIDR can not be less than 8");
	}

	this.netmask = 0xffffffff; // 11111111.11111111.11111111.11111111
	this.netmask = this.netmask << (32 - netMaskInteger);
}

netmask 변수는 먼저 1로 다 채우고 32 - 13 인 19개를 shift 함으로써 0으로 채우고 13개가 1이 되게 한다.

즉 11111111111110000000000000000000 이 된다. 

 

이제 192.168.254.252/13 범위를 구해보자.

먼저 netmask 가 -1이면(subnetmask가 없으면) 범위가 없으니 그냥 ip를 리턴한다.

그 후 numberOfBits 변수를 얻는데 netmask 가 11111111111110000000000000000000 다. 0이 나올때까지 for문 돌려서 값을 증가시키므로 13이 된다. 특별한건 없고 아까 subnetmask 문자열을 다시 얻고 싶어서 있는 로직이다.

public String getHostAddressRange() {
	if (this.netmask == -1) {
		return convertIpToString(this.ipAddress);
	}

	int numberOfBits;
	for (numberOfBits = 0; numberOfBits < 32; numberOfBits++) {
		if ((this.netmask << numberOfBits) == 0) {
			break;
		}
	}

	int numberOfIps = 0;
	for (int n = 0; n < (32 - numberOfBits); n++) {
		numberOfIps = numberOfIps << 1;
		numberOfIps = numberOfIps | 0x01;
	}

	// 192.168.254.252/13
	// 11000000.10101000.11111110.11111100 (ipAddress)
	// 11111111.11111000.00000000.00000000 (netmask)
	// 11000000.10101000.00000000.00000000 (&연산. networkIp)
	int networkIp = this.ipAddress & this.netmask;

	// 11000000.10101000.00000000.00000000 (networkIp)
	// 00000000.00000111.11111111.11111111 (numberOfIps) netmask를 8보다 제한하므로 더해서 단위 초과는 발생안함
	// 11000000.10101111.11111111.11111111 (networkIp + numberOfIps)
	String firstIP = convertIpToString(networkIp);
	String lastIP = convertIpToString(networkIp + numberOfIps);
	return firstIP + " - " + lastIP;
}

private String convertIpToString(int ip) {
	StringBuffer sb = new StringBuffer(15);

	for (int shift = 24; shift > 0; shift -= 8) {

		// process 3 bytes, from high order byte down.
		sb.append(((ip >>> shift) & 0xff)); // >>> 연산자는  오른쪽으로 이동시킬 때 부호에 상관없이 항상 0으로 채움
		sb.append('.');
	}

	sb.append((ip & 0xff));
	return sb.toString();
}

다음 numberOfIps 변수 얻는 과정을 보자. 32 - 13인 19번 for문을 돌면서 numberOfIps 왼쪽 shift 하고 1을 추가한다(or 연산자로).

그 과정이 아래 로그로 표현하면 이해가 빠르다.

numberOfIps : 0
numberOfIps : 1
numberOfIps : 10
numberOfIps : 11
numberOfIps : 110
numberOfIps : 111
numberOfIps : 1110
numberOfIps : 1111
numberOfIps : 11110
numberOfIps : 11111
numberOfIps : 111110
numberOfIps : 111111
numberOfIps : 1111110
numberOfIps : 1111111
numberOfIps : 11111110
numberOfIps : 11111111
numberOfIps : 111111110
numberOfIps : 111111111
numberOfIps : 1111111110
numberOfIps : 1111111111
numberOfIps : 11111111110
numberOfIps : 11111111111
numberOfIps : 111111111110
numberOfIps : 111111111111
numberOfIps : 1111111111110
numberOfIps : 1111111111111
numberOfIps : 11111111111110
numberOfIps : 11111111111111
numberOfIps : 111111111111110
numberOfIps : 111111111111111
numberOfIps : 1111111111111110
numberOfIps : 1111111111111111
numberOfIps : 11111111111111110
numberOfIps : 11111111111111111
numberOfIps : 111111111111111110
numberOfIps : 111111111111111111
numberOfIps : 1111111111111111110
numberOfIps : 1111111111111111111

즉 numberOfIps 는 1이 19개인 값이다. 

이제 범위의 출발점인 networkIp 를 먼저 구한다.

// 192.168.254.252/13
// 11000000.10101000.11111110.11111100 (ipAddress)
// 11111111.11111000.00000000.00000000 (netmask)
// 11000000.10101000.00000000.00000000 (networkIp)
int networkIp = this.ipAddress & this.netmask;

아까 구한 ipAddress 와 netmask 를 & 연산를 하면 왼쪽부터 13개 까지만 ipAddress와 일치하게 된다. 나머지는 0이다.

 

범위의 마지막인 lastIp를 구한다. 위에서 구한 networkIp (11000000.10101000.00000000.00000000) 와 1이 19개인 numberOfIps 를 더한다. 

// 11000000.10101000.00000000.00000000 (networkIp)
// 00000000.00000111.11111111.11111111 (numberOfIps) netmask를 8보다 제한하므로 더해서 단위 초과는 발생안함
// 11000000.10101111.11111111.11111111 (networkIp + numberOfIps)
String lastIP = convertIpToString(networkIp + numberOfIps);
String lastIP2 = convertIpToString(networkIp | numberOfIps); // OR 연산해도 결과는 같음

결국 범위는 192.168.0.0 - 192.175.255.255 이고 192.175.254.253 은 포함된다.

IpAddressMatcher ipAddressMatcher = new IpAddressMatcher("192.168.254.252/13");
assertThat(ipAddressMatcher.contains("192.175.254.253"), is(true));

그런데 networkIp와 numberOfIps 를 더하는 것보다는 OR 연산자를 쓰는게 더 좋아보인다. networkIp는 ipAddress 와 subnet mask 로 AND 연산자가 수행되서 앞의 13개만 빼고 나머지는 다 0이 된다. numberOfIps 는 뒤 19개가 1이니 OR 연산자를 통해서도 쉽게 얻을 수 있다.

 

마지막으로 위 contains 메서드로 어떻게 포함되는지 안되는지 알수 있는지 확인해보자.

먼저 netmask 값이 없다면 단순 string equal 비교가 된다.

그다음은 포함되는지 여부를 알고 싶은 checkingIpAddressString 을 int 타입으로 변환하는 과정이다. 아까와 과정은 똑같으므로 설명은 생략한다. 

public boolean contains(String checkingIpAddressString) {
	if (this.netmask == -1) {
		return StringUtils.equals(convertIpToString(this.ipAddress), checkingIpAddressString);
	}

	int checkingIpAddress = 0;
	String[] octets = checkingIpAddressString.split("\\.");

	if (octets.length != 4) {
		throw new NumberFormatException("Invalid IP address: " + checkingIpAddressString);
	}

	int i = 24;
	for (int n = 0; n < octets.length; n++) {
		int value = Integer.parseInt(octets[n]);
		if (value != (value & 0xff)) {
			throw new NumberFormatException("Invalid IP address: " + checkingIpAddressString);
		}

		checkingIpAddress += value << i;
		i -= 8;
	}

	// IpAddressMatcher ipAddressMatcher = new IpAddressMatcher("192.168.254.252/13");
	// assertThat(ipAddressMatcher.contains("192.175.254.253"), is(true));
	// 11000000.10101000.11111110.11111100
	// 11111111.11111000.00000000.00000000
	// 11000000.10101000.00000000.00000000 (this.ipAddress & this.netmask 결과)

	// 11000000.10101111.11111110.11111101
	// 11111111.11111000.00000000.00000000
	// 11000000.10101000.00000000.00000000 (checkingIpAddress & this.netmask 결과)
	return (this.ipAddress & this.netmask) == (checkingIpAddress & this.netmask);

	// 결국 ipAddress 와 checkingIpAddress 는 netmask 부분까지만 같으면 된다.
}

ipAddress(11000000.10101000.11111110.11111100) 와 checkingIpAddress(11000000.10101111.11111110.11111101) 에서 netmask 부분까지만 같으면 포함된것이다.

반응형