diff --git a/DB/__init__.py b/DB/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/DB/fetchDNSRecords.py b/DB/fetchDNSRecords.py new file mode 100644 index 0000000..5935c57 --- /dev/null +++ b/DB/fetchDNSRecords.py @@ -0,0 +1,19 @@ +import json + +class FetchDNSRecords: + def __init__(self, domain): + self.domain = domain + self.stripTrailingDot() + + def stripTrailingDot(self): + if self.domain.endswith('.'): + self.domain = self.domain[:-1] + + def fetchRecords(self): + with open('DB/registry.json', 'r') as file: + data = json.load(file) + if self.domain in data: + return data[self.domain] + else: + return None + return None \ No newline at end of file diff --git a/DB/registry.json b/DB/registry.json new file mode 100644 index 0000000..2c37025 --- /dev/null +++ b/DB/registry.json @@ -0,0 +1,6 @@ +{ + "test.ks":{ + "test.ks":["A","20.207.73.82",0,"Sample A Record pointing to github.com"], + "sub.test.ks":["CNAME","test.ks",0,"Sample CNAME Record"] + } +} \ No newline at end of file diff --git a/config/__init__.py b/config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/config/config.py b/config/config.py new file mode 100644 index 0000000..174cbbc --- /dev/null +++ b/config/config.py @@ -0,0 +1,40 @@ +class Config: + def __init__(self): + self.maxWorkers = 10 + self.host = "0.0.0.0" + self.port = 53 + self.bufferSize = 512 + self.supporterdRRTypes = ["A","CNAME"] + self.googleDNShost = "1.1.1.1" + self.gooogleDNSport = 53 + self.googleUpstreamTimeout = 10 + self.authTLD = "ks" + + + def getMaxWorkers(self): + return self.maxWorkers + + def getHost(self): + return self.host + + def getPort(self): + return self.port + + def getBufferSize(self): + return self.bufferSize + + def getSupportedRRTypes(self): + return self.supporterdRRTypes + + def getGoogleDNShost(self): + return self.googleDNShost + + def getGoogleDNSport(self): + return self.gooogleDNSport + + def getAuthTLD(self): + return self.authTLD + + def getGoogleUpstreamTimeout(self): + return self.googleUpstreamTimeout + \ No newline at end of file diff --git a/core/DNSMessageHandler.py b/core/DNSMessageHandler.py new file mode 100644 index 0000000..ad9ea40 --- /dev/null +++ b/core/DNSMessageHandler.py @@ -0,0 +1,69 @@ +from DB.fetchDNSRecords import FetchDNSRecords +from config.config import Config +from core.support.domainParser import DomainParser +from core.support.DNSParser import DNSParser +from core.support.DNSRespBuilder import DNSResponseBuilder + + +class DNSMessageHandler: + def __init__(self,dnsMsg): + self.config = Config() + self.respBuilder = DNSResponseBuilder(dnsMsg) + self.dnsParser = DNSParser(dnsMsg) + self.domainParser = DomainParser(self.dnsParser.getQueryDomain()) + self.dnsMsg = dnsMsg + + def isAuthoritative(self): + domain = self.dnsParser.getQueryDomain() + if domain.endswith("." + self.config.getAuthTLD()) or domain.endswith("." + self.config.getAuthTLD() + "."): + return True + else: + return False + + def fetchDomainDNSRecords(self): + domain = self.domainParser.extractDomain() + return FetchDNSRecords(domain).fetchRecords() + + def __handleARecord__(self,dnsRecord): + if dnsRecord[0] == "A": + self.respBuilder.RR_A(dnsRecord) + elif dnsRecord[0] == "CNAME": + self.respBuilder.RR_CNAME(dnsRecord) + else: + self.respBuilder.emptyResponse() + + def __handleCNAMERecord__(self,dnsRecord): + if dnsRecord[0] == "CNAME": + self.respBuilder.RR_CNAME(dnsRecord) + else: + self.respBuilder.emptyResponse() + + def __isSupportedRRType__(self): + if self.dnsParser.getQueryTypeName() not in self.config.getSupportedRRTypes(): + return False + return True + + def handleQuery(self): + if not self.isAuthoritative() and not self.respBuilder.upstreamResp(): + return None + if not self.__isSupportedRRType__(): + self.respBuilder.notImplemented() + return + dnsRecords = self.fetchDomainDNSRecords() + if not dnsRecords: + self.respBuilder.emptyResponse() + return + cleanedDomain = self.domainParser.handleFQDN() + particularDNSRecord = dnsRecords.get(cleanedDomain) + if not particularDNSRecord: + self.respBuilder.emptyResponse() + return + if self.dnsParser.getQueryTypeName() == "A": + self.__handleARecord__(particularDNSRecord) + return + elif self.dnsParser.getQueryTypeName() == "CNAME": + self.__handleCNAMERecord__(particularDNSRecord) + return + + def getResponse(self): + return self.respBuilder.packResponse() \ No newline at end of file diff --git a/core/DNSServer.py b/core/DNSServer.py new file mode 100644 index 0000000..2c264ed --- /dev/null +++ b/core/DNSServer.py @@ -0,0 +1,33 @@ +import socket +from config.config import Config + +class DNSServer: + def __init__(self): + self.config = Config() + self.host = self.config.getHost() + self.port = self.config.getPort() + self.bufferSize = self.config.getBufferSize() + self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + + def start(self): + server = (self.host, self.port) + self.sock.bind(server) + print("Listening on " + self.host + ":" + str(self.port)) + + def stop(self): + self.sock.close() + print("Server stopped.") + + def getRequest(self): + try: + data, addr = self.sock.recvfrom(self.bufferSize) + return data, addr + except socket.error as e: + print("Socket error: " + str(e)) + return None, None + + def sendResponse(self, data, addr): + try: + self.sock.sendto(data, addr) + except socket.error as e: + print("Socket error: " + str(e)) \ No newline at end of file diff --git a/core/__init__.py b/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/support/DNSParser.py b/core/support/DNSParser.py new file mode 100644 index 0000000..a53c171 --- /dev/null +++ b/core/support/DNSParser.py @@ -0,0 +1,25 @@ +from dnslib.dns import DNSRecord,QTYPE + +class DNSParser: + def __init__(self, dnsMsg): + self.dnsMsg = dnsMsg + self.parsedMsg = self.dnsReqMsgParse() + self.DNSRecordTypes = QTYPE.forward + + def dnsReqMsgParse(self): + return DNSRecord.parse(self.dnsMsg) + + def getQueryDomain(self): + return str(self.parsedMsg.q.qname) + + def getQueryType(self): + return str(self.parsedMsg.q.qtype) + + def getQueryTypeName(self): + return self.DNSRecordTypes.get(self.parsedMsg.q.qtype) + + def isQueryTypeValid(self): + if str(self.parsedMsg.q.qtype) in self.DNSRecordTypes: + return True + else: + return False \ No newline at end of file diff --git a/core/support/DNSRespBuilder.py b/core/support/DNSRespBuilder.py new file mode 100644 index 0000000..4c345a2 --- /dev/null +++ b/core/support/DNSRespBuilder.py @@ -0,0 +1,69 @@ +from core.upstreamResolver import UpstreamResolver +from core.support.DNSParser import DNSParser +from dnslib.dns import DNSRecord,DNSHeader,DNSQuestion,RR,CNAME,A,RCODE,QTYPE + + +class DNSResponseBuilder: + def __init__(self, dnsMsg): + self.dnsMsg = dnsMsg + self.upstreamResolver = UpstreamResolver() + self.parsedMsg = DNSParser(dnsMsg).dnsReqMsgParse() + self.DNSRecordTypes = QTYPE.forward + self.dnsResp = None + self.packedDNSResp = None + + def createResponseDNSRecord(self, rcode, rdata, ttl, atype=None, aa=1): + ra = 0 # 1 Recursion Available 0 for not available + qname = str(self.parsedMsg.q.qname) + qclass = self.parsedMsg.q.qclass + dnsHeader = DNSHeader(id=self.parsedMsg.header.id, qr=1, aa=aa, ra=ra, rcode=rcode) + dnsQuestion = DNSQuestion(qname=qname, qtype=self.parsedMsg.q.qtype, qclass=qclass) + dnsAnswer = None + if rcode == RCODE.NOERROR and rdata is not None and atype is not None: + dnsAnswer = RR(str(qname), rtype=atype, rclass=qclass, ttl=ttl, + rdata=rdata,) + dnsRecord = DNSRecord(dnsHeader, + q=dnsQuestion, + a=dnsAnswer) + return dnsRecord + + def notImplemented(self): + self.dnsResp = self.createResponseDNSRecord(RCODE.NOTIMP,None,0) + return self + + def nxDomain(self): + self.dnsResp = self.createResponseDNSRecord(RCODE.NXDOMAIN,None,0) + + def serverFailure(self): + self.dnsResp = self.createResponseDNSRecord(RCODE.SERVFAIL,None,0) + + def emptyResponse(self): + self.dnsResp = self.createResponseDNSRecord(RCODE.NOERROR,None,0) + + def RR_A(self,value): + if not value or not isinstance(value, list): + self.emptyResponse() + return + self.dnsResp = self.createResponseDNSRecord(RCODE.NOERROR,A(value[1]),0,QTYPE.A) + + def RR_CNAME(self,value): + if not value or not isinstance(value, list): + self.emptyResponse() + return + self.dnsResp = self.createResponseDNSRecord(RCODE.NOERROR,CNAME(value[1]),0,QTYPE.CNAME) + + def upstreamResp(self): + upstreamResp = self.upstreamResolver.sendQuery(self.dnsMsg) + if not upstreamResp: + self.serverFailure() + return None + self.packedDNSResp = upstreamResp + self.dnsResp = True #TODO: Temp bypass fix later + + def packResponse(self): + if self.dnsResp is None: + self.serverFailure() + elif hasattr(self.dnsResp, "pack"): + self.packedDNSResp = self.dnsResp.pack() + return self.packedDNSResp + \ No newline at end of file diff --git a/core/support/__init__.py b/core/support/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/support/domainParser.py b/core/support/domainParser.py new file mode 100644 index 0000000..5941a8c --- /dev/null +++ b/core/support/domainParser.py @@ -0,0 +1,19 @@ +import re + +class DomainParser(): + def __init__(self, domain): + self.domain = domain + + def isFQDN(self): + return self.domain.endswith('.') and '.' in self.domain[:-1] + + def handleFQDN(self): + if self.isFQDN() and self.domain.endswith('.'): + return self.domain[:-1] + return self.domain + + def extractDomain(self): + match = re.search(r'([a-zA-Z0-9-]+\.[a-zA-Z]{2,})$', self.handleFQDN()) + if match: + return match.group(1) + return None \ No newline at end of file diff --git a/core/upstreamResolver.py b/core/upstreamResolver.py new file mode 100644 index 0000000..ee73475 --- /dev/null +++ b/core/upstreamResolver.py @@ -0,0 +1,25 @@ +import socket +from config.config import Config + +class UpstreamResolver: + + def __init__(self): + self.config = Config() + self.upstreamHost = self.config.getGoogleDNShost() + self.upstreamPort = self.config.getGoogleDNSport() + self.upstreamDNS = (self.upstreamHost, self.upstreamPort) + self.bufferSize = self.config.getBufferSize() + self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self.sock.settimeout(self.config.getGoogleUpstreamTimeout()) + + def sendQuery(self, data): + try: + self.sock.sendto(data, self.upstreamDNS) + response_data, _ = self.sock.recvfrom(512) + return response_data + except socket.timeout: + print("Upstream DNS query timed out.") + return None + except Exception as e: + print(f"Error occured Upstream Server : {e}") + return None diff --git a/readme.md b/readme.md index bd4ee5e..421562e 100644 --- a/readme.md +++ b/readme.md @@ -1 +1,78 @@ -## Writing my own dns server in python \ No newline at end of file +# DNS Server in Python + +This is a Python-based DNS server that resolves DNS queries for `.ks` domains. The `.ks` TLD (Top-Level Domain) is a friendly nod to [*Kush S.*](https://github.com/krshrimali) a good friend, guide, and the kind of person you want to be or look up to. Cheers, Kush! πŸŽ‰ (And hey, you can always change `.ks` to anything you like from config if you don’t like him. 😜) + +## Features + +- **Custom `.ks` Domain Support**: Resolve `.ks` domains with custom records to make your own little corner of the internet. +- **Support for A and CNAME Records**: Currently, the server supports only `A` and `CNAME` record types for `.ks` domains. +- **Upstream Resolution**: For domains other than `.ks` (e.g., `.com`, `.in`), the server gracefully forwards queries to Google's DNS server. +- **JSON-based Database**: The server uses a JSON file (`DB/registry.json`) as a database for `.ks` domain records. + +## Prerequisites + +- Python 3.12.0 installed on your system. + - *Note*: This project was developed and tested on Python 3.12.0. It has not been tested on other Python versions, and compatibility cannot be guaranteed. +- Required Python packages (installable via `requirements.txt`). + +## Installation & Setup + +1. Clone the repository: + ```bash + git clone https://github.com/jaythorat/dns_server-python.git + cd dns_server-python + ``` + +2. Install the required Python dependencies: + ```bash + pip install -r requirements.txt + ``` + +3. Start the DNS server: + ```bash + python server.py + ``` + +4. Update your system or browser's DNS settings to point to this server. + +5. Add `.ks` domain records by updating the `DB/registry.json` file. + +## Testing + +1. Add `.ks` domain records to the `DB/registry.json` file (e.g., A or CNAME records). +2. Use a DNS query tool (like `dig`) or configure your browser/system to use the DNS server. +3. Examples of DNS queries using `dig`: + - Query a custom `.ks` domain (e.g., `test.ks`): + ```bash + dig @ test.ks A + ``` + ```bash + dig @ test.ks CNAME + ``` + - Query a regular domain (e.g., `google.com`): + ```bash + dig @ google.com + ``` + Replace `` with the IP address of your DNS server. + +4. Verify `.ks` domains for proper resolution. +5. Test non-`.ks` domains (e.g., `.com`, `.in`) to ensure upstream resolution works properly. + +## Limitations + +- Currently supports only `A` and `CNAME` record types for `.ks` domains. +- Uses a JSON file as a database, which may not scale for large datasets. +- Requires users to manually change their system or browser's DNS server settings to use this custom DNS server. + +## Future Enhancements + +- Support for additional record types (e.g., `MX`, `TXT`) to expand functionality. +- Integration with a scalable database instead of JSON for better performance. +- Improved logging and error handling for easier debugging. +- Develop a complete registry service similar to services like GoDaddy or Cloudflare, where users can register and manage domains and DNS records. + +## License + +This project is open-source and available under the [MIT License](LICENSE). + +--- \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..d660d17 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +dnslib==0.9.26 +setuptools==78.1.1 +wheel==0.45.1 diff --git a/server.py b/server.py new file mode 100644 index 0000000..a9db3f9 --- /dev/null +++ b/server.py @@ -0,0 +1,19 @@ +from concurrent.futures import ThreadPoolExecutor +from core.DNSMessageHandler import DNSMessageHandler +from core.DNSServer import DNSServer +from config.config import Config + +server = DNSServer() +server.start() + + +def handleRequest(data, addr, server): + msgHandler = DNSMessageHandler(data) + msgHandler.handleQuery() + response = msgHandler.getResponse() + server.sendResponse(response, addr) + +with ThreadPoolExecutor(max_workers=Config().getMaxWorkers()) as executor: + while True: + data, addr = server.getRequest() + executor.submit(handleRequest, data, addr, server) \ No newline at end of file