Skip to content

Commit a8443e8

Browse files
feat: cnames
1 parent 89268b7 commit a8443e8

4 files changed

Lines changed: 128 additions & 39 deletions

File tree

README.md

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,9 @@ This project consists of two components that work together:
5252

5353
- ✅ Automatic DNS record management for Kubernetes resources
5454
- ✅ Support for A records (IPv4 addresses)
55+
- ✅ Support for CNAME records (domain aliases)
5556
- ✅ Support for TXT records (for external-dns ownership tracking)
56-
- ✅ Multiple IP addresses per domain name
57+
- ✅ Multiple targets per domain name
5758
- ✅ Configurable domain filters
5859
- ✅ Safe concurrent request handling
5960
- ✅ Systemd service management
@@ -553,7 +554,13 @@ address=/example.home.local/192.168.1.100
553554
address=/example.home.local/192.168.1.101
554555
```
555556

556-
#### TXT Records
557+
#### CNAME Records
558+
File: `~/.firewalla/config/dnsmasq_local/api.home.local`
559+
```
560+
cname=api.home.local,service.home.local
561+
```
562+
563+
#### TXT Records
557564
File: `~/.firewalla/config/dnsmasq_local/external-dns-a-example.home.local.txt`
558565
```
559566
txt-record=external-dns-a-example.home.local,"heritage=external-dns,external-dns/owner=my-cluster"
@@ -776,6 +783,13 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
776783

777784
## Changelog
778785

786+
### v1.1.0 (CNAME Support)
787+
788+
- Added support for CNAME records (domain aliases)
789+
- Enhanced validation for CNAME targets (must be valid DNS names)
790+
- Updated dnsmasq configuration format for CNAME records
791+
- Multiple targets per CNAME record supported
792+
779793
### v1.0.0 (Initial Release)
780794

781795
- External-DNS webhook provider implementation

src/services/dnsmasq.js

Lines changed: 51 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -76,31 +76,44 @@ async function getRecords() {
7676
});
7777
}
7878
} else {
79-
// Parse A records
80-
const targets = [];
81-
let dnsName = null;
82-
83-
for (const line of lines) {
84-
// Format: address=/domain/ip
85-
const match = line.match(/^address=\/([^\/]+)\/(.+)$/);
86-
if (match) {
87-
dnsName = match[1];
88-
const ip = match[2];
89-
if (validator.isValidIPv4(ip)) {
90-
targets.push(ip);
91-
}
92-
}
93-
}
94-
95-
if (dnsName && targets.length > 0) {
96-
records.push({
97-
dnsName: dnsName,
98-
targets: targets,
99-
recordType: 'A',
100-
recordTTL: config.dnsTTL
101-
});
102-
}
103-
}
79+
// Parse A and CNAME records
80+
const targets = [];
81+
let dnsName = null;
82+
let recordType = 'A';
83+
84+
for (const line of lines) {
85+
// Format: address=/domain/ip
86+
const addressMatch = line.match(/^address=\/([^\/]+)\/(.+)$/);
87+
if (addressMatch) {
88+
dnsName = addressMatch[1];
89+
const ip = addressMatch[2];
90+
if (validator.isValidIPv4(ip)) {
91+
targets.push(ip);
92+
recordType = 'A';
93+
}
94+
}
95+
96+
// Format: cname=domain,target
97+
const cnameMatch = line.match(/^cname=([^,]+),(.+)$/);
98+
if (cnameMatch) {
99+
dnsName = cnameMatch[1];
100+
const target = cnameMatch[2];
101+
if (validator.isValidDnsName(target)) {
102+
targets.push(target);
103+
recordType = 'CNAME';
104+
}
105+
}
106+
}
107+
108+
if (dnsName && targets.length > 0) {
109+
records.push({
110+
dnsName: dnsName,
111+
targets: targets,
112+
recordType: recordType,
113+
recordTTL: config.dnsTTL
114+
});
115+
}
116+
}
104117
} catch (err) {
105118
logger.warn('Failed to parse file', { file, error: err.message });
106119
}
@@ -129,16 +142,19 @@ async function writeRecord(endpoint) {
129142

130143
const filePath = getRecordFilePath(dnsName, recordType);
131144

132-
let content;
133-
if (recordType === 'A') {
134-
// Generate dnsmasq A record format
135-
content = targets.map(ip => `address=/${dnsName}/${ip}`).join('\n') + '\n';
136-
} else if (recordType === 'TXT') {
137-
// Generate dnsmasq TXT record format
138-
content = targets.map(txt => `txt-record=${dnsName},"${txt}"`).join('\n') + '\n';
139-
} else {
140-
throw new Error(`Unsupported record type: ${recordType}`);
141-
}
145+
let content;
146+
if (recordType === 'A') {
147+
// Generate dnsmasq A record format
148+
content = targets.map(ip => `address=/${dnsName}/${ip}`).join('\n') + '\n';
149+
} else if (recordType === 'TXT') {
150+
// Generate dnsmasq TXT record format
151+
content = targets.map(txt => `txt-record=${dnsName},"${txt}"`).join('\n') + '\n';
152+
} else if (recordType === 'CNAME') {
153+
// Generate dnsmasq CNAME record format
154+
content = targets.map(target => `cname=${dnsName},${target}`).join('\n') + '\n';
155+
} else {
156+
throw new Error(`Unsupported record type: ${recordType}`);
157+
}
142158

143159
if (config.dryRun) {
144160
logger.info('[DRY RUN] Would write file', { filePath, content: content.trim() });

src/utils/validator.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ function isValidIPv4(ip) {
4545
* Validate record type
4646
*/
4747
function isValidRecordType(type) {
48-
const allowedTypes = ['A', 'TXT'];
48+
const allowedTypes = ['A', 'TXT', 'CNAME'];
4949
return allowedTypes.includes(type);
5050
}
5151

@@ -84,7 +84,12 @@ function isValidEndpoint(endpoint) {
8484
if (endpoint.recordType === 'TXT') {
8585
return endpoint.targets.every(t => typeof t === 'string' && t.length > 0);
8686
}
87-
87+
88+
// For CNAME records, targets must be valid DNS names
89+
if (endpoint.recordType === 'CNAME') {
90+
return endpoint.targets.every(isValidDnsName);
91+
}
92+
8893
return true;
8994
}
9095

test-cname.js

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Test script for CNAME record functionality
4+
*/
5+
6+
// Mock config for testing
7+
const config = {
8+
dnsTTL: 300,
9+
dryRun: true,
10+
dnsmasqDir: '/tmp/test-dnsmasq'
11+
};
12+
13+
// Mock logger
14+
const logger = {
15+
debug: console.log,
16+
info: console.log,
17+
warn: console.warn,
18+
error: console.error
19+
};
20+
21+
// Import validator
22+
const validator = require('./src/utils/validator');
23+
24+
// Test CNAME validation
25+
console.log('Testing CNAME validation...');
26+
27+
// Test valid CNAME endpoint
28+
const validCnameEndpoint = {
29+
dnsName: 'api.example.com',
30+
targets: ['service.example.com'],
31+
recordType: 'CNAME'
32+
};
33+
34+
console.log('Valid CNAME endpoint:', validator.isValidEndpoint(validCnameEndpoint));
35+
36+
// Test invalid CNAME endpoint (invalid target)
37+
const invalidCnameEndpoint = {
38+
dnsName: 'api.example.com',
39+
targets: ['invalid..domain'],
40+
recordType: 'CNAME'
41+
};
42+
43+
console.log('Invalid CNAME endpoint:', validator.isValidEndpoint(invalidCnameEndpoint));
44+
45+
// Test multiple CNAME targets
46+
const multiCnameEndpoint = {
47+
dnsName: 'api.example.com',
48+
targets: ['service1.example.com', 'service2.example.com'],
49+
recordType: 'CNAME'
50+
};
51+
52+
console.log('Multiple CNAME targets:', validator.isValidEndpoint(multiCnameEndpoint));
53+
54+
console.log('CNAME validation tests completed.');

0 commit comments

Comments
 (0)