diff --git a/backend/name-server/package.json b/backend/name-server/package.json index 952986d0..32bd79fc 100644 --- a/backend/name-server/package.json +++ b/backend/name-server/package.json @@ -4,7 +4,7 @@ "description": "", "main": "src/index.ts", "scripts": { - "dev": "tsx watch src/index.ts", + "start:dev": "NODE_ENV=development tsx watch src/index.ts", "start": "tsx src/index.ts", "build": "tsc -p tsconfig.json && tsc-alias -p tsconfig.json", "tmp": "tsx src/index.ts", diff --git a/backend/name-server/src/app.ts b/backend/name-server/src/app.ts index 12e95354..d346b5bc 100644 --- a/backend/name-server/src/app.ts +++ b/backend/name-server/src/app.ts @@ -1,10 +1,10 @@ import { config } from 'dotenv'; -import type { ServerConfig } from './common/utils/validator/configuration.validator'; -import { ConfigurationValidator } from './common/utils/validator/configuration.validator'; -import { Server } from './server/server'; -import { db } from './database/mysql/mysql-database'; -import { logger } from './common/utils/logger/console.logger'; -import { ProjectQuery } from './database/query/project.query'; +import type { ServerConfig } from 'common/utils/validator/configuration.validator'; +import { ConfigurationValidator } from 'common/utils/validator/configuration.validator'; +import { Server } from 'server/server'; +import { db } from 'database/mysql/mysql-database'; +import { logger } from 'common/utils/logger/console.logger'; +import { ProjectQuery } from 'database/query/project.query'; import { DAURecorder } from 'database/query/dau-recorder'; config(); diff --git a/backend/name-server/src/database/clickhouse/config/clickhouse.config.ts b/backend/name-server/src/database/clickhouse/config/clickhouse.config.ts index 4406760a..26022575 100644 --- a/backend/name-server/src/database/clickhouse/config/clickhouse.config.ts +++ b/backend/name-server/src/database/clickhouse/config/clickhouse.config.ts @@ -1,8 +1,16 @@ import { ClickHouseClientConfigOptions } from '@clickhouse/client'; -export const clickhouseConfig: ClickHouseClientConfigOptions = { - url: process.env.CLICKHOUSE_URL || 'http://localhost:8123', - username: process.env.CLICKHOUSE_USERNAME || 'default', - password: process.env.CLICKHOUSE_PASSWORD || '', - database: process.env.CLICKHOUSE_DATABASE, -}; +export const clickhouseConfig: ClickHouseClientConfigOptions = + process.env.NODE_ENV === 'development' + ? { + url: process.env.DEV_CLICKHOUSE_URL, + username: process.env.DEV_CLICKHOUSE_USERNAME, + password: process.env.DEV_CLICKHOUSE_PASSWORD, + database: process.env.DEV_CLICKHOUSE_DATABASE, + } + : { + url: process.env.CLICKHOUSE_URL || 'http://localhost:8123', + username: process.env.CLICKHOUSE_USERNAME || 'default', + password: process.env.CLICKHOUSE_PASSWORD || '', + database: process.env.CLICKHOUSE_DATABASE, + }; diff --git a/backend/name-server/src/database/mysql/config.ts b/backend/name-server/src/database/mysql/config.ts index e7c455bf..56816b2d 100644 --- a/backend/name-server/src/database/mysql/config.ts +++ b/backend/name-server/src/database/mysql/config.ts @@ -3,10 +3,19 @@ import type { PoolOptions } from 'mysql2/promise'; dotenv.config(); -export const poolConfig: PoolOptions = { - host: process.env.DB_HOST, - port: Number(process.env.DB_PORT), - user: process.env.DB_USERNAME, - password: process.env.DB_PASSWORD, - database: process.env.DB_NAME, -}; +export const poolConfig: PoolOptions = + process.env.NODE_ENV === 'development' + ? { + host: process.env.DEV_DB_HOST, + port: Number(process.env.DEV_DB_PORT), + user: process.env.DEV_DB_USERNAME, + password: process.env.DEV_DB_PASSWORD, + database: process.env.DEV_DB_NAME, + } + : { + host: process.env.DB_HOST, + port: Number(process.env.DB_PORT), + user: process.env.DB_USERNAME, + password: process.env.DB_PASSWORD, + database: process.env.DB_NAME, + }; diff --git a/backend/name-server/src/database/query/dau-recorder.ts b/backend/name-server/src/database/query/dau-recorder.ts index e5f1c27e..a5616c5f 100644 --- a/backend/name-server/src/database/query/dau-recorder.ts +++ b/backend/name-server/src/database/query/dau-recorder.ts @@ -6,9 +6,10 @@ export interface DAURecorderInterface { export class DAURecorder implements DAURecorderInterface { private clickhouseClient = ClickhouseDatabase.getInstance(); + public async recordAccess(domain: string): Promise { const date = new Date().toISOString().slice(0, 10); - const values = [{ domain, date, access: 1 }]; + const values = [{ domain: domain.toLowerCase(), date, access: 1 }]; try { await this.clickhouseClient.insert({ table: 'dau', diff --git a/backend/name-server/src/index.ts b/backend/name-server/src/index.ts index 79eaca25..26ff9b91 100644 --- a/backend/name-server/src/index.ts +++ b/backend/name-server/src/index.ts @@ -1,5 +1,5 @@ -import { Application } from './app'; -import { logger } from './common/utils/logger/console.logger'; +import { Application } from 'app'; +import { logger } from 'common/utils/logger/console.logger'; async function main(): Promise { const initializer = new Application(); diff --git a/backend/name-server/src/server/constant/dns-packet.constant.ts b/backend/name-server/src/server/constant/dns-packet.constant.ts index b971a053..70c4561c 100644 --- a/backend/name-server/src/server/constant/dns-packet.constant.ts +++ b/backend/name-server/src/server/constant/dns-packet.constant.ts @@ -1,4 +1,4 @@ -export const DNSFlags = { +export const DNS_FLAGS = { AUTHORITATIVE_ANSWER: 0x0400, // 권한 있는 응답 (네임서버가 해당 도메인의 공식 서버일 때) TRUNCATED_RESPONSE: 0x0200, // 응답이 잘린 경우 (UDP 크기 제한 초과) RECURSION_DESIRED: 0x0100, // 재귀적 쿼리 요청 (클라이언트가 설정) @@ -7,10 +7,10 @@ export const DNSFlags = { CHECKING_DISABLED: 0x0010, // DNSSEC 검증 비활성화 } as const; -export const ResponseCode = { +export const RESPONSE_CODE = { NOERROR: 0, // 정상 응답 NXDOMAIN: 3, // 도메인이 존재하지 않음 SERVFAIL: 2, // 서버 에러 } as const; -export type ResponseCodeType = (typeof ResponseCode)[keyof typeof ResponseCode]; +export type ResponseCodeType = (typeof RESPONSE_CODE)[keyof typeof RESPONSE_CODE]; diff --git a/backend/name-server/src/server/constant/message-type.constants.ts b/backend/name-server/src/server/constant/message-type.constants.ts new file mode 100644 index 00000000..94d2ac47 --- /dev/null +++ b/backend/name-server/src/server/constant/message-type.constants.ts @@ -0,0 +1,8 @@ +export const MESSAGE_TYPE = { + DNS: 'DNS', + HEALTH_CHECK: 'HEALTH_CHECK', +} as const; + +export const MIN_DNS_MESSAGE_LENGTH = 12; + +export type MessageType = (typeof MESSAGE_TYPE)[keyof typeof MESSAGE_TYPE]; diff --git a/backend/name-server/src/server/server.ts b/backend/name-server/src/server/server.ts index d2d0806f..4f5e00ac 100644 --- a/backend/name-server/src/server/server.ts +++ b/backend/name-server/src/server/server.ts @@ -2,14 +2,15 @@ import type { Socket } from 'dgram'; import { createSocket, type RemoteInfo } from 'dgram'; import { decode, encode } from 'dns-packet'; import type { DecodedPacket, Question } from 'dns-packet'; -import type { ServerConfig } from '../common/utils/validator/configuration.validator'; +import type { ServerConfig } from 'common/utils/validator/configuration.validator'; import { PacketValidator } from './utils/packet.validator'; import { DNSResponseBuilder } from './utils/dns-response-builder'; -import { ResponseCode } from './constant/dns-packet.constant'; -import { logger } from '../common/utils/logger/console.logger'; +import { RESPONSE_CODE } from './constant/dns-packet.constant'; +import { logger } from 'common/utils/logger/console.logger'; import { ServerError } from './error/server.error'; -import type { ProjectQueryInterface } from '../database/query/project.query.interface'; +import type { ProjectQueryInterface } from 'database/query/project.query.interface'; import type { DAURecorderInterface } from 'database/query/dau-recorder'; +import { MESSAGE_TYPE } from 'server/constant/message-type.constants'; export class Server { private server: Socket; @@ -31,16 +32,25 @@ export class Server { private async handleMessage(msg: Buffer, remoteInfo: RemoteInfo): Promise { try { + const messageType = PacketValidator.validateMessageType(msg); + + if (messageType === MESSAGE_TYPE.HEALTH_CHECK) { + await this.handleHealthCheck(remoteInfo); + return; + } + const query = decode(msg); const question = this.parseQuery(query); + logger.logQuery(question.name, remoteInfo); + await this.validateRequest(question.name); this.dauRecorder.recordAccess(question.name).catch((err) => { logger.error(`DAU recording failed for ${question.name}: ${err.message}`); }); const response = new DNSResponseBuilder(this.config, query) - .addAnswer(ResponseCode.NOERROR, question) + .addAnswer(RESPONSE_CODE.NOERROR, question) .build(); const responseMsg = encode(response); @@ -50,6 +60,16 @@ export class Server { } } + private async handleHealthCheck(remoteInfo: RemoteInfo): Promise { + try { + const healthCheckResponse = Buffer.from([]); + + await this.sendResponse(healthCheckResponse, remoteInfo); + } catch (error) { + logger.error(`Health check response failed: ${(error as Error).message}`); + } + } + private async validateRequest(name: string): Promise { if (await this.projectQuery.existsByDomain(name)) { return; @@ -89,7 +109,7 @@ export class Server { const errorMessage = `Failed to process DNS query from ${remoteInfo.address}:${remoteInfo.port}`; const response = new DNSResponseBuilder(this.config, query) - .addAnswer(ResponseCode.NXDOMAIN) + .addAnswer(RESPONSE_CODE.NXDOMAIN) .build(); const responseMsg = encode(response); diff --git a/backend/name-server/src/server/utils/dns-response-builder.ts b/backend/name-server/src/server/utils/dns-response-builder.ts index 42e4a945..e444a585 100644 --- a/backend/name-server/src/server/utils/dns-response-builder.ts +++ b/backend/name-server/src/server/utils/dns-response-builder.ts @@ -2,7 +2,7 @@ import type { Packet, Question, RecordClass } from 'dns-packet'; import type { ServerConfig } from '../../common/utils/validator/configuration.validator'; import { PacketValidator } from './packet.validator'; import type { ResponseCodeType } from '../constant/dns-packet.constant'; -import { DNSFlags, ResponseCode } from '../constant/dns-packet.constant'; +import { DNS_FLAGS, RESPONSE_CODE } from '../constant/dns-packet.constant'; interface DNSResponse extends Packet { answers: Array<{ @@ -30,10 +30,10 @@ export class DNSResponseBuilder { } private createFlags(query: Packet): number { - const flags = DNSFlags.AUTHORITATIVE_ANSWER; + const flags = DNS_FLAGS.AUTHORITATIVE_ANSWER; - if (PacketValidator.hasFlags(query) && query.flags & DNSFlags.RECURSION_DESIRED) { - return flags | DNSFlags.RECURSION_DESIRED; + if (PacketValidator.hasFlags(query) && query.flags & DNS_FLAGS.RECURSION_DESIRED) { + return flags | DNS_FLAGS.RECURSION_DESIRED; } return flags; @@ -42,11 +42,11 @@ export class DNSResponseBuilder { addAnswer(rcode: ResponseCodeType, question?: Question): this { this.response.flags = 0x8000; - if (this.response.flags && rcode === ResponseCode.NXDOMAIN) { - this.response.flags |= ResponseCode.NXDOMAIN; + if (this.response.flags && rcode === RESPONSE_CODE.NXDOMAIN) { + this.response.flags |= RESPONSE_CODE.NXDOMAIN; } - if (rcode === ResponseCode.NOERROR && question) { + if (rcode === RESPONSE_CODE.NOERROR && question) { this.response.answers = [ { name: question.name, diff --git a/backend/name-server/src/server/utils/packet.validator.ts b/backend/name-server/src/server/utils/packet.validator.ts index 7642daed..268cd507 100644 --- a/backend/name-server/src/server/utils/packet.validator.ts +++ b/backend/name-server/src/server/utils/packet.validator.ts @@ -1,4 +1,9 @@ import type { Packet, Question } from 'dns-packet'; +import { + MESSAGE_TYPE, + MessageType, + MIN_DNS_MESSAGE_LENGTH, +} from 'server/constant/message-type.constants'; type TypeGuardResult = T extends Packet ? T & { questions: Question[] } : never; @@ -14,4 +19,20 @@ export class PacketValidator { static validatePacket(packet: Packet): boolean { return this.hasQuestions(packet) && this.hasFlags(packet); } + + static validateMessageType(msg: Buffer): MessageType { + if (msg.length >= MIN_DNS_MESSAGE_LENGTH) { + const flags = msg.readUInt16BE(2); + const isQuery = (flags & 0x8000) === 0; + + if (isQuery) { + const questionCount = msg.readUInt16BE(4); + if (questionCount > 0) { + return MESSAGE_TYPE.DNS; + } + } + } + + return MESSAGE_TYPE.HEALTH_CHECK; + } } diff --git a/backend/name-server/test/dns-response-builder.test.ts b/backend/name-server/test/dns-response-builder.test.ts index 10997c1e..afab81b3 100644 --- a/backend/name-server/test/dns-response-builder.test.ts +++ b/backend/name-server/test/dns-response-builder.test.ts @@ -1,7 +1,7 @@ import { DNSResponseBuilder } from '../src/server/utils/dns-response-builder'; import type { Packet } from 'dns-packet'; import { PacketValidator } from '../src/server/utils/packet.validator'; -import { DNSFlags, ResponseCode } from '../src/server/constant/dns-packet.constant'; +import { DNS_FLAGS, RESPONSE_CODE } from '../src/server/constant/dns-packet.constant'; describe('DNSResponseBuilder의', () => { const mockConfig = { @@ -12,7 +12,7 @@ describe('DNSResponseBuilder의', () => { const mockQuery: Packet = { type: 'query', id: 1234, - flags: DNSFlags.RECURSION_DESIRED, + flags: DNS_FLAGS.RECURSION_DESIRED, questions: [ { name: 'example.com', @@ -29,14 +29,14 @@ describe('DNSResponseBuilder의', () => { expect(response.id).toBe(mockQuery.id); expect(response.type).toBe('response'); - expect(response.flags).toBe(DNSFlags.AUTHORITATIVE_ANSWER | DNSFlags.RECURSION_DESIRED); + expect(response.flags).toBe(DNS_FLAGS.AUTHORITATIVE_ANSWER | DNS_FLAGS.RECURSION_DESIRED); }); test('addAnswer()는 올바른 정보를 담은 answer를 추가해야 합니다.', () => { const builder = new DNSResponseBuilder(mockConfig, mockQuery); if (!PacketValidator.hasQuestions(mockQuery)) return; - builder.addAnswer(ResponseCode.NOERROR, mockQuery.questions[0]); + builder.addAnswer(RESPONSE_CODE.NOERROR, mockQuery.questions[0]); const response = builder.build(); expect(response.answers).toHaveLength(1); @@ -44,7 +44,7 @@ describe('DNSResponseBuilder의', () => { name: 'example.com', type: 'A', class: 'IN', - ttl: 300, + ttl: 86400, data: '127.0.0.1', }); }); diff --git a/backend/name-server/test/server.test.ts b/backend/name-server/test/server.test.ts index 56ad71cb..3dc1c58d 100644 --- a/backend/name-server/test/server.test.ts +++ b/backend/name-server/test/server.test.ts @@ -1,8 +1,9 @@ -import dgram from 'dgram'; +import * as dgram from 'dgram'; import { encode, decode, Packet } from 'dns-packet'; import { Server } from '../src/server/server'; import { ServerConfig } from '../src/common/utils/validator/configuration.validator'; import { NORMAL_PACKET, NOT_EXIST_DOMAIN_PACKET } from './constant/packet'; +import { DAURecorder } from '../src/database/query/dau-recorder'; import { TestProjectQuery } from './database/test-project.query'; describe('DNS 서버는 ', () => { @@ -21,7 +22,7 @@ describe('DNS 서버는 ', () => { beforeAll(async () => { client = dgram.createSocket('udp4'); - server = new Server(config, new TestProjectQuery()); + server = new Server(config, new DAURecorder(), new TestProjectQuery()); server.start(); await new Promise((resolve) => setTimeout(resolve, 100));