All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.datakernel.dns.DnsProtocol Maven / Gradle / Ivy

/*
 * Copyright (C) 2015-2018 SoftIndex LLC.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.datakernel.dns;

import io.datakernel.bytebuf.ByteBuf;
import io.datakernel.bytebuf.ByteBufPool;
import io.datakernel.common.parse.InvalidSizeException;
import io.datakernel.common.parse.ParseException;
import io.datakernel.common.parse.UnknownFormatException;
import org.jetbrains.annotations.Nullable;

import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

import static io.datakernel.dns.DnsProtocol.ResponseErrorCode.NO_DATA;
import static java.nio.charset.StandardCharsets.US_ASCII;

/**
 * This class allows to use a simple subset of the Domain Name System (or DNS) protocol
 */
public final class DnsProtocol {
	public static final ParseException QUESTION_COUNT_NOT_ONE = new ParseException(DnsProtocol.class, "Received DNS response has question count not equal to one");

	private static final int MAX_SIZE = 512;

	private static final byte[] STANDARD_QUERY_HEADER = {
			0x01, 0x00, // flags: 0x0100 - standard query
			0x00, 0x01, // nubmer of questions      : 1
			0x00, 0x00, // nubmer of answer RRs     : 0
			0x00, 0x00, // nubmer of authority RRs  : 0
			0x00, 0x00, // nubmer of additional RRs : 0
	};

	private static final AtomicInteger xorshiftState = new AtomicInteger(1);

	/* atomic 16-bit xorshift LSFR, cycling through all 16-bit values except zero */
	public static short generateTransactionId() {
		return (short) xorshiftState.updateAndGet(old -> {
			short x = (short) old;
			x ^= (x & 0xffff) << 7;
			x ^= (x & 0xffff) >>> 9;
			x ^= (x & 0xffff) << 8;
			assert x != 0 : "Xorshift LFSR can never produce zero";
			return x;
		});
	}

//	public static void main(String[] args) {
//		long start = System.currentTimeMillis();
//		boolean[] set = new boolean[1 << 16];
//		for (int i = 0; i < 1 << 16 - 1; i++) {
//			int idx = generateTransactionId() & 0xFFFF;
//			if (set[idx]) {
//				System.out.println(i + " - FAIL " + idx);
//				continue;
//			}
//			set[idx] = true;
//		}
//		System.out.println("took " + (System.currentTimeMillis() - start) + " ms");
//	}

	/**
	 * Creates a bytebuf with a DNS query payload
	 *
	 * @param transaction DNS transaction to encode
	 * @return ByteBuf with DNS message payload data
	 */
	public static ByteBuf createDnsQueryPayload(DnsTransaction transaction) {
		ByteBuf byteBuf = ByteBufPool.allocate(MAX_SIZE);

		byteBuf.writeShort(transaction.getId());
		// standard query flags, 1 question and 0 of other stuff
		byteBuf.write(STANDARD_QUERY_HEADER);

		// query domain name
		byte componentSize = 0;
		DnsQuery query = transaction.getQuery();
		byte[] domainBytes = query.getDomainName().getBytes(US_ASCII);

		int pos = -1;
		while (++pos < domainBytes.length) {
			if (domainBytes[pos] != '.') {
				componentSize++;
				continue;
			}
			byteBuf.writeByte(componentSize);
			byteBuf.write(domainBytes, pos - componentSize, componentSize);
			componentSize = 0;
		}
		byteBuf.writeByte(componentSize);
		byteBuf.write(domainBytes, pos - componentSize, componentSize);
		byteBuf.writeByte((byte) 0x0); // terminator byte

		// query record type
		byteBuf.writeShort(query.getRecordType().getCode());
		// query class: IN
		byteBuf.writeShort(QueryClass.INTERNET.getCode());
		return byteBuf;
	}

	/**
	 * Reads a DNS query response from payload
	 *
	 * @param payload byte buffer with response payload
	 * @return DNS query response parsed from the payload
	 * @throws ParseException when parsing fails
	 */
	public static DnsResponse readDnsResponse(ByteBuf payload) throws ParseException {
		try {
			short transactionId = payload.readShort();
			payload.moveHead(1); // skip first flags byte

			//                                                                    last 4 flag bits are error code
			ResponseErrorCode errorCode = ResponseErrorCode.fromBits(payload.readByte() & 0b00001111);

			short questionCount = payload.readShort();
			short answerCount = payload.readShort();
			payload.moveHead(4); // skip authority and additional counts (2 bytes each)

			if (questionCount != 1) {
				// malformed response, we are always sending only one question
				throw QUESTION_COUNT_NOT_ONE;
			}

			// read domain name from first query
			StringBuilder sb = new StringBuilder();
			byte componentSize = payload.readByte();
			while (componentSize != 0) {
				sb.append(new String(payload.array(), payload.head(), componentSize, US_ASCII));
				payload.moveHead(componentSize);
				componentSize = payload.readByte();
				if (componentSize != 0) {
					sb.append('.');
				}
			}
			String domainName = sb.toString();

			// read query record type
			short recordTypeCode = payload.readShort();
			RecordType recordType = RecordType.fromCode(recordTypeCode);
			if (recordType == null) {
				// malformed response, we are sending query only with existing RecordType's
				throw new UnknownFormatException(DnsProtocol.class, "Received DNS response with unknown query record type (" +
						Integer.toHexString(recordTypeCode & 0xFFFF) + ")");
			}

			// read query class (only for sanity check)
			short queryClassCode = payload.readShort();
			QueryClass queryClass = QueryClass.fromCode(queryClassCode);
			if (queryClass != QueryClass.INTERNET) {
				throw new UnknownFormatException(DnsProtocol.class, "Received DNS response with unknown query class (" +
						Integer.toHexString(queryClassCode & 0xFFFF) + ")");
			}

			// at this point, we know the query of this response
			DnsQuery query = DnsQuery.of(domainName, recordType);

			// and so we have all the data to fail if error code is not zero
			DnsTransaction transaction = DnsTransaction.of(transactionId, query);
			if (errorCode != ResponseErrorCode.NO_ERROR) {
				return DnsResponse.ofFailure(transaction, errorCode);
			}

//			// skip other queries if any (we are sending only one and checking for it above, so this is dead code)
//			for (int i = 0; i < questionCount - 1; i++) {
//				// skip domain name (bytes until zero-terminator)
//				while ((componentSize = payload.readByte()) != 0) {
//					payload.moveHead(componentSize);
//				}
//				payload.moveHead(4); // skip query record type and class (2 bytes each)
//			}

			List ips = new ArrayList<>();
			int minTtl = Integer.MAX_VALUE;
			for (int i = 0; i < answerCount; i++) {
				payload.moveHead(2); // skip answer name (2 bytes)
				RecordType currentRecordType = RecordType.fromCode(payload.readShort());
				payload.moveHead(2); // skip answer class (2 bytes)
				if (currentRecordType != recordType) { // this is some other record
					payload.moveHead(4); // skip ttl
					payload.moveHead(payload.readShort()); // and skip data
					continue;
				}
				minTtl = Math.min(payload.readInt(), minTtl);
				short length = payload.readShort();
				if (length != recordType.dataLength) {
					throw new InvalidSizeException(DnsProtocol.class, "Bad record length received. " + recordType +
							"-record length should be " + recordType.dataLength + " bytes, it was " + length);
				}
				byte[] bytes = new byte[length];
				payload.read(bytes);
				try {
					ips.add(InetAddress.getByAddress(domainName, bytes));
				} catch (UnknownHostException ignored) {
					// never happens, we have explicit check for length
				}
			}
			// fail if no IPs were parsed (or answer count was zero)
			if (ips.isEmpty()) {
				return DnsResponse.ofFailure(transaction, NO_DATA);
			}
			return DnsResponse.of(transaction, DnsResourceRecord.of(ips.toArray(new InetAddress[0]), minTtl));
		} catch (IndexOutOfBoundsException e) {
			throw new ParseException(DnsProtocol.class, "Failed parsing DNS response", e);
		}
	}

	// used DNS enum types:

	public enum RecordType {
		A(0x0001, 4),
		AAAA(0x001C, 16);

		private final short code;
		private final short dataLength;

		RecordType(int code, int dataLength) {
			this.code = (short) code;
			this.dataLength = (short) dataLength;
		}

		public short getCode() {
			return code;
		}

		public short getDataLength() {
			return dataLength;
		}

		/**
		 * Returns record type based on its code or null if code is invalid.
		 */
		@Nullable
		static RecordType fromCode(short code) {
			switch (code) {
				case 0x0001:
					return A;
				case 0x001C:
					return AAAA;
				default:
					return null;
			}
		}
	}

	public enum QueryClass {
		INTERNET(0x0001);

		private final short code;

		QueryClass(int code) {
			this.code = (short) code;
		}

		public short getCode() {
			return code;
		}

		/**
		 * Returns query class based on its code or null if code is invalid.
		 */
		@Nullable
		public static QueryClass fromCode(short code) {
			if (code == 0x0001) return INTERNET;
			return null;
		}
	}

	/**
	 * Handled error codes:
	 * 
	 * 
	 * 
	 * 
	 * 
	 * 
	 * 
	 * 
	 * 
	 * ...
	 * 
RFC6895 - DNS, page 5:
RCODE:NAME:Description:Reference:
0NoErrorNo ErrorRFC1035
1FormErrFormat ErrorRFC1035
2ServFailServer FailureRFC1035
3NXDomainNon-Existent DomainRFC1035
4NotImpNot ImplementedRFC1035
5RefusedQuery RefusedRFC1035
*/ public enum ResponseErrorCode { NO_ERROR, FORMAT_ERROR, SERVER_FAILURE, NAME_ERROR, NOT_IMPLEMENTED, REFUSED, // custom error codes NO_DATA, TIMED_OUT, UNKNOWN; static ResponseErrorCode fromBits(int rcodeBits) { if (rcodeBits < 0 || rcodeBits > 5) { return UNKNOWN; } return values()[rcodeBits]; } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy