dns-is-reverse 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Eugene Yaacobi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,194 @@
1
+ Metadata-Version: 2.4
2
+ Name: dns-is-reverse
3
+ Version: 1.0.0
4
+ Summary: Python reimplementation of all-knowing-dns: authoritative DNS server for IPv6 reverse DNS synthesis
5
+ Author: DNS Reverse Team
6
+ License: MIT
7
+ Requires-Python: >=3.11
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: dnslib>=0.9.23
11
+ Provides-Extra: dev
12
+ Requires-Dist: pytest>=7.0; extra == "dev"
13
+ Requires-Dist: mypy>=1.0; extra == "dev"
14
+ Dynamic: license-file
15
+
16
+ # DNS-is-reverse
17
+
18
+ A Python reimplementation of [all-knowing-dns](https://github.com/raumzeitlabor/all-knowing-dns): a tiny authoritative DNS server that synthesizes IPv6 reverse DNS (PTR) and matching forward AAAA records on the fly for SLAAC-style networks, avoiding gigantic zone files.
19
+
20
+ ## What it does
21
+
22
+ DNS-is-reverse answers DNS queries for IPv6 networks by synthesizing responses based on templates:
23
+
24
+ - **PTR queries**: For reverse DNS lookups (ip6.arpa), it extracts the host portion of the IPv6 address and generates a hostname using a configurable template
25
+ - **AAAA queries**: For forward DNS lookups, it parses hostnames matching the template and returns the corresponding IPv6 address within the configured network
26
+ - **Upstream fallback**: For PTR queries, it can optionally query an upstream DNS server first before synthesizing locally
27
+
28
+ ## Configuration Format
29
+
30
+ The configuration file uses a simple line-based format:
31
+
32
+ ```
33
+ listen <address>
34
+ network <CIDR>
35
+ resolves to <template>
36
+ with upstream <address> # optional
37
+ ```
38
+
39
+ ### Example Configuration
40
+
41
+ ```
42
+ # Listen on IPv6 and IPv4
43
+ listen ::1
44
+ listen 127.0.0.1
45
+
46
+ # Configure a /64 network
47
+ network 2001:4d88:100e:ccc0::/64
48
+ resolves to ipv6-%DIGITS%.nutzer.raumzeitlabor.de
49
+ with upstream 2001:4860:4860::8888
50
+
51
+ # Configure a /56 network without upstream
52
+ network 2001:db8:100::/56
53
+ resolves to host-%DIGITS%.example.com
54
+ ```
55
+
56
+ ## How %DIGITS% Works
57
+
58
+ The `%DIGITS%` placeholder in templates represents the host portion of IPv6 addresses as hexadecimal digits:
59
+
60
+ - For a `/64` network: 64 host bits = 16 hex digits
61
+ - For a `/56` network: 72 host bits = 18 hex digits
62
+ - For a `/80` network: 48 host bits = 12 hex digits
63
+
64
+ ### Example for /64 network `2001:4d88:100e:ccc0::/64`:
65
+
66
+ - IPv6 address: `2001:4d88:100e:ccc0:216:eaff:fecb:826`
67
+ - Host portion: `0216eafffecb0826` (16 hex digits, zero-padded)
68
+ - Template: `ipv6-%DIGITS%.example.com`
69
+ - Generated hostname: `ipv6-0216eafffecb0826.example.com`
70
+
71
+ ## Upstream Fallback
72
+
73
+ When a network has `with upstream <address>` configured:
74
+
75
+ 1. For PTR queries, DNS-is-reverse first queries the upstream server for `<original_ptr_qname>.upstream`
76
+ 2. If the upstream returns a PTR answer, that answer is relayed to the client
77
+ 3. If the upstream returns NXDOMAIN/timeout/no-answer, DNS-is-reverse synthesizes the response locally
78
+ 4. AAAA queries are always synthesized locally (no upstream fallback)
79
+
80
+ ## Installation
81
+
82
+ ### Native Installation
83
+
84
+ ```bash
85
+ pip install -e .
86
+ ```
87
+
88
+ ### Docker Installation
89
+
90
+ ```bash
91
+ # Using docker-compose (recommended)
92
+ docker-compose up --build
93
+
94
+ # Or build and run manually
95
+ docker build -t dns-is-reverse .
96
+ docker run -p 53:53/udp -v ./test.conf:/etc/dns-is-reverse.conf:ro dns-is-reverse
97
+ ```
98
+
99
+ ## Usage
100
+
101
+ ### Command Line Options
102
+
103
+ ```bash
104
+ dns-is-reverse [options]
105
+
106
+ Options:
107
+ --configfile PATH Configuration file path (default: /etc/all-knowing-dns.conf)
108
+ --listen ADDRESS Additional listen address (can be used multiple times)
109
+ --port PORT Listen port (default: 53)
110
+ --querylog Enable query logging to stdout
111
+ ```
112
+
113
+ ### Running on High Port (Non-root)
114
+
115
+ For development or non-root usage:
116
+
117
+ ```bash
118
+ dns-is-reverse --configfile ./test.conf --port 5353 --querylog
119
+ ```
120
+
121
+ ### Running with Docker
122
+
123
+ The Docker container runs on port 53 by default:
124
+
125
+ ```bash
126
+ # Start with docker-compose
127
+ docker-compose up
128
+
129
+ # Or run directly with custom config
130
+ docker run -p 53:53/udp -v /path/to/config.conf:/etc/dns-is-reverse.conf:ro dns-is-reverse
131
+ ```
132
+
133
+ ### Example Configuration File
134
+
135
+ Create `test.conf`:
136
+
137
+ ```
138
+ listen ::1
139
+ listen 127.0.0.1
140
+
141
+ network 2001:db8::/64
142
+ resolves to test-%DIGITS%.local
143
+
144
+ network 2001:db8:100::/56
145
+ resolves to server-%DIGITS%.example.com
146
+ with upstream 8.8.8.8
147
+ ```
148
+
149
+ ### Testing with dig
150
+
151
+ ```bash
152
+ # Test AAAA query
153
+ dig @::1 -p 5353 test-1234567890abcdef.local AAAA
154
+
155
+ # Test PTR query
156
+ dig @::1 -p 5353 -x 2001:db8::1234:5678:9abc:def0
157
+ ```
158
+
159
+ ## Supported Network Sizes
160
+
161
+ DNS-is-reverse works with any IPv6 network size where the host portion is a multiple of 4 bits:
162
+
163
+ - `/64` networks: 16 hex digits (most common for SLAAC)
164
+ - `/56` networks: 18 hex digits
165
+ - `/48` networks: 20 hex digits
166
+ - `/80` networks: 12 hex digits
167
+ - etc.
168
+
169
+ ## DNS Behavior
170
+
171
+ - **Authoritative**: All synthesized responses have the AA (Authoritative Answer) flag set
172
+ - **TTL**: All responses use a 60-second TTL
173
+ - **Error handling**: Returns NXDOMAIN for out-of-network queries or template mismatches
174
+ - **Malformed queries**: Returns FORMERR for unparseable DNS requests
175
+ - **Query types**: Only PTR and AAAA queries are supported; all others return NXDOMAIN
176
+
177
+ ## Development
178
+
179
+ ### Running Tests
180
+
181
+ ```bash
182
+ pip install -e ".[dev]"
183
+ pytest
184
+ ```
185
+
186
+ ### Type Checking
187
+
188
+ ```bash
189
+ mypy dns_is_reverse/
190
+ ```
191
+
192
+ ## License
193
+
194
+ MIT License
@@ -0,0 +1,179 @@
1
+ # DNS-is-reverse
2
+
3
+ A Python reimplementation of [all-knowing-dns](https://github.com/raumzeitlabor/all-knowing-dns): a tiny authoritative DNS server that synthesizes IPv6 reverse DNS (PTR) and matching forward AAAA records on the fly for SLAAC-style networks, avoiding gigantic zone files.
4
+
5
+ ## What it does
6
+
7
+ DNS-is-reverse answers DNS queries for IPv6 networks by synthesizing responses based on templates:
8
+
9
+ - **PTR queries**: For reverse DNS lookups (ip6.arpa), it extracts the host portion of the IPv6 address and generates a hostname using a configurable template
10
+ - **AAAA queries**: For forward DNS lookups, it parses hostnames matching the template and returns the corresponding IPv6 address within the configured network
11
+ - **Upstream fallback**: For PTR queries, it can optionally query an upstream DNS server first before synthesizing locally
12
+
13
+ ## Configuration Format
14
+
15
+ The configuration file uses a simple line-based format:
16
+
17
+ ```
18
+ listen <address>
19
+ network <CIDR>
20
+ resolves to <template>
21
+ with upstream <address> # optional
22
+ ```
23
+
24
+ ### Example Configuration
25
+
26
+ ```
27
+ # Listen on IPv6 and IPv4
28
+ listen ::1
29
+ listen 127.0.0.1
30
+
31
+ # Configure a /64 network
32
+ network 2001:4d88:100e:ccc0::/64
33
+ resolves to ipv6-%DIGITS%.nutzer.raumzeitlabor.de
34
+ with upstream 2001:4860:4860::8888
35
+
36
+ # Configure a /56 network without upstream
37
+ network 2001:db8:100::/56
38
+ resolves to host-%DIGITS%.example.com
39
+ ```
40
+
41
+ ## How %DIGITS% Works
42
+
43
+ The `%DIGITS%` placeholder in templates represents the host portion of IPv6 addresses as hexadecimal digits:
44
+
45
+ - For a `/64` network: 64 host bits = 16 hex digits
46
+ - For a `/56` network: 72 host bits = 18 hex digits
47
+ - For a `/80` network: 48 host bits = 12 hex digits
48
+
49
+ ### Example for /64 network `2001:4d88:100e:ccc0::/64`:
50
+
51
+ - IPv6 address: `2001:4d88:100e:ccc0:216:eaff:fecb:826`
52
+ - Host portion: `0216eafffecb0826` (16 hex digits, zero-padded)
53
+ - Template: `ipv6-%DIGITS%.example.com`
54
+ - Generated hostname: `ipv6-0216eafffecb0826.example.com`
55
+
56
+ ## Upstream Fallback
57
+
58
+ When a network has `with upstream <address>` configured:
59
+
60
+ 1. For PTR queries, DNS-is-reverse first queries the upstream server for `<original_ptr_qname>.upstream`
61
+ 2. If the upstream returns a PTR answer, that answer is relayed to the client
62
+ 3. If the upstream returns NXDOMAIN/timeout/no-answer, DNS-is-reverse synthesizes the response locally
63
+ 4. AAAA queries are always synthesized locally (no upstream fallback)
64
+
65
+ ## Installation
66
+
67
+ ### Native Installation
68
+
69
+ ```bash
70
+ pip install -e .
71
+ ```
72
+
73
+ ### Docker Installation
74
+
75
+ ```bash
76
+ # Using docker-compose (recommended)
77
+ docker-compose up --build
78
+
79
+ # Or build and run manually
80
+ docker build -t dns-is-reverse .
81
+ docker run -p 53:53/udp -v ./test.conf:/etc/dns-is-reverse.conf:ro dns-is-reverse
82
+ ```
83
+
84
+ ## Usage
85
+
86
+ ### Command Line Options
87
+
88
+ ```bash
89
+ dns-is-reverse [options]
90
+
91
+ Options:
92
+ --configfile PATH Configuration file path (default: /etc/all-knowing-dns.conf)
93
+ --listen ADDRESS Additional listen address (can be used multiple times)
94
+ --port PORT Listen port (default: 53)
95
+ --querylog Enable query logging to stdout
96
+ ```
97
+
98
+ ### Running on High Port (Non-root)
99
+
100
+ For development or non-root usage:
101
+
102
+ ```bash
103
+ dns-is-reverse --configfile ./test.conf --port 5353 --querylog
104
+ ```
105
+
106
+ ### Running with Docker
107
+
108
+ The Docker container runs on port 53 by default:
109
+
110
+ ```bash
111
+ # Start with docker-compose
112
+ docker-compose up
113
+
114
+ # Or run directly with custom config
115
+ docker run -p 53:53/udp -v /path/to/config.conf:/etc/dns-is-reverse.conf:ro dns-is-reverse
116
+ ```
117
+
118
+ ### Example Configuration File
119
+
120
+ Create `test.conf`:
121
+
122
+ ```
123
+ listen ::1
124
+ listen 127.0.0.1
125
+
126
+ network 2001:db8::/64
127
+ resolves to test-%DIGITS%.local
128
+
129
+ network 2001:db8:100::/56
130
+ resolves to server-%DIGITS%.example.com
131
+ with upstream 8.8.8.8
132
+ ```
133
+
134
+ ### Testing with dig
135
+
136
+ ```bash
137
+ # Test AAAA query
138
+ dig @::1 -p 5353 test-1234567890abcdef.local AAAA
139
+
140
+ # Test PTR query
141
+ dig @::1 -p 5353 -x 2001:db8::1234:5678:9abc:def0
142
+ ```
143
+
144
+ ## Supported Network Sizes
145
+
146
+ DNS-is-reverse works with any IPv6 network size where the host portion is a multiple of 4 bits:
147
+
148
+ - `/64` networks: 16 hex digits (most common for SLAAC)
149
+ - `/56` networks: 18 hex digits
150
+ - `/48` networks: 20 hex digits
151
+ - `/80` networks: 12 hex digits
152
+ - etc.
153
+
154
+ ## DNS Behavior
155
+
156
+ - **Authoritative**: All synthesized responses have the AA (Authoritative Answer) flag set
157
+ - **TTL**: All responses use a 60-second TTL
158
+ - **Error handling**: Returns NXDOMAIN for out-of-network queries or template mismatches
159
+ - **Malformed queries**: Returns FORMERR for unparseable DNS requests
160
+ - **Query types**: Only PTR and AAAA queries are supported; all others return NXDOMAIN
161
+
162
+ ## Development
163
+
164
+ ### Running Tests
165
+
166
+ ```bash
167
+ pip install -e ".[dev]"
168
+ pytest
169
+ ```
170
+
171
+ ### Type Checking
172
+
173
+ ```bash
174
+ mypy dns_is_reverse/
175
+ ```
176
+
177
+ ## License
178
+
179
+ MIT License
@@ -0,0 +1,3 @@
1
+ """dns-is-reverse - Python reimplementation of all-knowing-dns."""
2
+
3
+ __version__ = "1.0.0"
@@ -0,0 +1,66 @@
1
+ """Command-line interface for DNS-is-reverse."""
2
+
3
+ import argparse
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ from .dns_server import DNSServer
8
+ from .parser import parse_config
9
+
10
+
11
+ def main() -> None:
12
+ """Main entry point."""
13
+ parser = argparse.ArgumentParser(description="DNS-is-reverse - IPv6 reverse DNS synthesizer")
14
+ parser.add_argument(
15
+ "--configfile",
16
+ default="/etc/dns-is-reverse.conf",
17
+ help="Configuration file path (default: /etc/dns-is-reverse.conf)"
18
+ )
19
+ parser.add_argument(
20
+ "--listen",
21
+ action="append",
22
+ help="Additional listen address (can be used multiple times)"
23
+ )
24
+ parser.add_argument(
25
+ "--port",
26
+ type=int,
27
+ default=53,
28
+ help="Listen port (default: 53)"
29
+ )
30
+ parser.add_argument(
31
+ "--querylog",
32
+ action="store_true",
33
+ help="Enable query logging to stdout"
34
+ )
35
+
36
+ args = parser.parse_args()
37
+
38
+ # Read config file
39
+ config_path = Path(args.configfile)
40
+ if not config_path.exists():
41
+ print(f"Error: Config file not found: {config_path}", file=sys.stderr)
42
+ sys.exit(1)
43
+
44
+ try:
45
+ config_text = config_path.read_text()
46
+ config = parse_config(config_text)
47
+ except Exception as e:
48
+ print(f"Error parsing config: {e}", file=sys.stderr)
49
+ sys.exit(1)
50
+
51
+ # Override config with CLI args
52
+ if args.listen:
53
+ config.listen_addresses.extend(args.listen)
54
+ config.port = args.port
55
+ config.query_log = args.querylog
56
+
57
+ # Start server
58
+ server = DNSServer(config)
59
+ try:
60
+ server.start()
61
+ except KeyboardInterrupt:
62
+ print("\nShutting down...")
63
+
64
+
65
+ if __name__ == "__main__":
66
+ main()
@@ -0,0 +1,27 @@
1
+ """Configuration data structures for dns-is-reverse."""
2
+
3
+ from dataclasses import dataclass
4
+ from ipaddress import IPv6Network
5
+ from typing import Optional
6
+
7
+
8
+ @dataclass
9
+ class NetworkConfig:
10
+ """Configuration for a single network."""
11
+ network: IPv6Network
12
+ template: str
13
+ upstream: Optional[str] = None
14
+
15
+ def __post_init__(self) -> None:
16
+ """Validate template contains exactly one %DIGITS%."""
17
+ if self.template.count('%DIGITS%') != 1:
18
+ raise ValueError(f"Template must contain exactly one %DIGITS%, got: {self.template}")
19
+
20
+
21
+ @dataclass
22
+ class Config:
23
+ """Main configuration."""
24
+ listen_addresses: list[str]
25
+ networks: list[NetworkConfig]
26
+ port: int = 53
27
+ query_log: bool = False
@@ -0,0 +1,155 @@
1
+ """DNS server implementation."""
2
+
3
+ import socket
4
+ import threading
5
+ from typing import Optional
6
+
7
+ import dnslib # type: ignore
8
+
9
+ from .config import Config
10
+ from .reverse import ptr_qname_to_ipv6
11
+ from .synth import find_matching_network, find_matching_template, synthesize_ptr_hostname, synthesize_aaaa_address
12
+ from .upstream import query_upstream
13
+
14
+
15
+ class DNSServer:
16
+ """UDP DNS server."""
17
+
18
+ def __init__(self, config: Config):
19
+ self.config = config
20
+ self.running = False
21
+
22
+ def handle_request(self, data: bytes, addr: tuple[str, int]) -> bytes:
23
+ """Handle a single DNS request."""
24
+ try:
25
+ request = dnslib.DNSRecord.parse(data)
26
+ except Exception:
27
+ # Malformed request
28
+ response = dnslib.DNSRecord()
29
+ response.header.rcode = dnslib.RCODE.FORMERR
30
+ return response.pack() # type: ignore
31
+
32
+ if self.config.query_log:
33
+ print(f"Query from {addr[0]}: {request.q.qname} {dnslib.QTYPE[request.q.qtype]}")
34
+
35
+ response = dnslib.DNSRecord()
36
+ response.header.id = request.header.id
37
+ response.header.qr = 1 # Response
38
+ response.header.aa = 1 # Authoritative
39
+ response.header.rcode = dnslib.RCODE.NOERROR # Default to success
40
+ response.add_question(request.q)
41
+
42
+ qname = str(request.q.qname).rstrip('.')
43
+ qtype = request.q.qtype
44
+
45
+ if qtype == dnslib.QTYPE.PTR:
46
+ self._handle_ptr(qname, response)
47
+ elif qtype == dnslib.QTYPE.AAAA:
48
+ self._handle_aaaa(qname, response)
49
+ else:
50
+ response.header.rcode = dnslib.RCODE.NXDOMAIN
51
+
52
+ return response.pack() # type: ignore
53
+
54
+ def _handle_ptr(self, qname: str, response: dnslib.DNSRecord) -> None:
55
+ """Handle PTR query."""
56
+ # Convert PTR qname to IPv6
57
+ addr = ptr_qname_to_ipv6(qname)
58
+ if addr is None:
59
+ response.header.rcode = dnslib.RCODE.NXDOMAIN
60
+ return
61
+
62
+ # Find matching network
63
+ network_config = find_matching_network(addr, self.config.networks)
64
+ if network_config is None:
65
+ response.header.rcode = dnslib.RCODE.NXDOMAIN
66
+ return
67
+
68
+ # Try upstream first if configured
69
+ if network_config.upstream:
70
+ upstream_qname = f"{qname}.upstream"
71
+ ptr_values = query_upstream(network_config.upstream, upstream_qname)
72
+ if ptr_values:
73
+ for ptr_value in ptr_values:
74
+ # Strip trailing dot from upstream response
75
+ ptr_value = ptr_value.rstrip('.')
76
+ rr = dnslib.RR(
77
+ rname=qname,
78
+ rtype=dnslib.QTYPE.PTR,
79
+ rdata=dnslib.PTR(ptr_value),
80
+ ttl=60
81
+ )
82
+ response.add_answer(rr)
83
+ return
84
+
85
+ # Synthesize locally
86
+ hostname = synthesize_ptr_hostname(addr, network_config)
87
+ rr = dnslib.RR(
88
+ rname=qname,
89
+ rtype=dnslib.QTYPE.PTR,
90
+ rdata=dnslib.PTR(hostname),
91
+ ttl=60
92
+ )
93
+ response.add_answer(rr)
94
+
95
+ def _handle_aaaa(self, qname: str, response: dnslib.DNSRecord) -> None:
96
+ """Handle AAAA query."""
97
+ # Find matching template
98
+ network_config = find_matching_template(qname, self.config.networks)
99
+ if network_config is None:
100
+ response.header.rcode = dnslib.RCODE.NXDOMAIN
101
+ return
102
+
103
+ # Synthesize address
104
+ addr = synthesize_aaaa_address(qname, network_config)
105
+ if addr is None:
106
+ response.header.rcode = dnslib.RCODE.NXDOMAIN
107
+ return
108
+
109
+ rr = dnslib.RR(
110
+ rname=qname,
111
+ rtype=dnslib.QTYPE.AAAA,
112
+ rdata=dnslib.AAAA(str(addr)),
113
+ ttl=60
114
+ )
115
+ response.add_answer(rr)
116
+
117
+ def start(self) -> None:
118
+ """Start the DNS server."""
119
+ self.running = True
120
+ threads = []
121
+
122
+ for listen_addr in self.config.listen_addresses:
123
+ thread = threading.Thread(target=self._serve_address, args=(listen_addr,))
124
+ thread.daemon = True
125
+ thread.start()
126
+ threads.append(thread)
127
+
128
+ try:
129
+ for thread in threads:
130
+ thread.join()
131
+ except KeyboardInterrupt:
132
+ self.running = False
133
+
134
+ def _serve_address(self, listen_addr: str) -> None:
135
+ """Serve DNS requests on a single address."""
136
+ family = socket.AF_INET6 if ':' in listen_addr else socket.AF_INET
137
+ sock = socket.socket(family, socket.SOCK_DGRAM)
138
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
139
+
140
+ try:
141
+ sock.bind((listen_addr, self.config.port))
142
+ print(f"Listening on {listen_addr}:{self.config.port}")
143
+
144
+ while self.running:
145
+ try:
146
+ data, addr = sock.recvfrom(4096)
147
+ response_data = self.handle_request(data, addr)
148
+ sock.sendto(response_data, addr)
149
+ except socket.timeout:
150
+ continue
151
+ except Exception as e:
152
+ if self.running:
153
+ print(f"Error handling request: {e}")
154
+ finally:
155
+ sock.close()
@@ -0,0 +1,60 @@
1
+ """Configuration file parser."""
2
+
3
+ import re
4
+ from ipaddress import IPv6Network
5
+ from typing import Iterator
6
+
7
+ from .config import Config, NetworkConfig
8
+
9
+
10
+ def parse_config(text: str) -> Config:
11
+ """Parse configuration from text."""
12
+ lines = [line.rstrip() for line in text.splitlines()]
13
+ listen_addresses: list[str] = []
14
+ networks: list[NetworkConfig] = []
15
+
16
+ i = 0
17
+ while i < len(lines):
18
+ line = lines[i].strip()
19
+ if not line or line.startswith('#'):
20
+ i += 1
21
+ continue
22
+
23
+ if line.startswith('listen '):
24
+ address = line[7:].strip()
25
+ listen_addresses.append(address)
26
+ i += 1
27
+ elif line.startswith('network '):
28
+ network_str = line[8:].strip()
29
+ network = IPv6Network(network_str)
30
+ i += 1
31
+
32
+ # Parse indented block
33
+ template = None
34
+ upstream = None
35
+
36
+ while i < len(lines):
37
+ if not lines[i] or lines[i].startswith('#'):
38
+ i += 1
39
+ continue
40
+ if not lines[i].startswith((' ', '\t')):
41
+ break
42
+
43
+ subline = lines[i].strip()
44
+ if subline.startswith('resolves to '):
45
+ template = subline[12:].strip()
46
+ elif subline.startswith('with upstream '):
47
+ upstream = subline[14:].strip()
48
+ i += 1
49
+
50
+ if template is None:
51
+ raise ValueError(f"Network {network_str} missing 'resolves to' directive")
52
+
53
+ networks.append(NetworkConfig(network, template, upstream))
54
+ else:
55
+ raise ValueError(f"Unknown directive: {line}")
56
+
57
+ if not listen_addresses:
58
+ listen_addresses = ["::", "0.0.0.0"]
59
+
60
+ return Config(listen_addresses, networks)