Advertisement
Open Source Projects by Phil Schwartz

How I Added IPv6 Support to DenyHosts Safely

Adding IPv6 support to DenyHosts looked straightforward at first. SSH logs contain addresses, DenyHosts records suspicious activity, and the operating system ultimately needs a reliable address to block. The difficult part was discovering how many parts of the program quietly assumed every address would fit the familiar IPv4 pattern.

DenyHosts had been running for years, so compatibility mattered as much as the new feature. Existing configuration files, database entries, reports, hostnames, and firewall integrations all represented established behavior. I wanted IPv6 protection without forcing users to rebuild their installations or risk rejecting valid IPv4 data.

The work became an exercise in making address handling explicit. Instead of scattering special cases through the parser, I traced an address from the SSH log to the storage layer and finally to the generated blocklist. That made it possible to add IPv6 while preserving the behavior users already depended on.

Finding The Hidden IPv4 Assumptions

The first assumptions were easy to spot: regular expressions that matched four decimal octets, code that split an address on periods, and validation routines that expected values between zero and 255. IPv6 immediately breaks each of those shortcuts because it uses hexadecimal groups, compressed notation, and optional scope information.

Less obvious problems appeared in reporting and persistence. An IPv6 address can contain colons, so any code that used a colon as a delimiter needed review. A textual address can also have multiple valid representations. For example, expanded and compressed forms may identify the same host while appearing completely different in a text file or database.

I treated this as a data-model change rather than a parser patch. The program needed one consistent concept of an IP address, with formatting decisions made only at the edges. That approach reduced the chance that one component would recognize an address while another silently discarded it.

Parsing Logs Without Losing Evidence

SSH log formats vary across Linux distributions, OpenSSH versions, and authentication methods. Some lines include a raw address, while others contain a hostname that later resolves to an address. The parser therefore had to remain conservative: identify the candidate field using the surrounding log structure, then validate the candidate as an IPv4 or IPv6 address.

Python's address utilities provided a useful foundation for this validation. They understand compressed IPv6 notation, reject malformed values, and distinguish individual addresses from networks. I kept the log parser responsible for extracting text, while a separate address layer handled interpretation and normalization.

That separation also protected existing behavior. A malformed token was still ignored rather than becoming a blocking entry, and an unresolvable hostname did not cause the monitor to fail. IPv6 support expanded the set of accepted addresses without lowering the standard for trustworthy input.

Choosing A Stable Representation

Normalization was the most important design decision. If DenyHosts stored every spelling exactly as it appeared in a log, the same IPv6 host could occupy several entries. That would weaken duplicate detection and make statistics misleading. I chose canonical textual output for individual addresses while retaining the original evidence where it was useful for diagnostics.

The storage format also had to remain readable by older installations. Existing IPv4 records could be loaded unchanged, and new IPv6 records used the same logical collections rather than a parallel database with different rules. This avoided a migration that would have been difficult for long-running servers and package maintainers.

The distinction between an address and a network was kept clear. DenyHosts identifies hostile sources discovered in logs; it should not accidentally interpret an IPv6 address as a range simply because both use colon-based notation. Any future CIDR feature would need its own explicit validation and policy.

Area Existing IPv4 Behavior IPv6-Safe Behavior
Log extraction Recognizes dotted-decimal addresses Recognizes dotted-decimal and colon-based addresses
Validation Checks four octets Uses address-aware validation for both families
Storage Keeps one address per record Stores canonical values without splitting families
Duplicate detection Compares IPv4 strings Compares normalized address objects or text
Output Writes entries for blocking tools Preserves valid syntax for the selected backend
Compatibility Reads existing files directly Continues reading old IPv4 data unchanged

The table summarizes the guiding principle: IPv6 was added as another address family, not as a second application. That made the change easier to test and reduced surprises for scripts that consume DenyHosts output.

Updating Blocking And Reporting

Recognizing an IPv6 address is only useful if the generated response can handle it. DenyHosts has historically worked with mechanisms such as hosts.deny and external firewall commands, but those mechanisms do not all treat IPv6 identically. I avoided assuming that an IPv4-oriented backend could accept an IPv6 value without additional configuration.

The integration boundary therefore became explicit. A backend receives a validated address and decides whether it supports that address family. Unsupported combinations produce a clear diagnostic instead of silently claiming success. This is safer than writing a syntactically plausible rule that the system never enforces.

Reports received the same attention. Counts by address, denied-host listings, and diagnostic messages needed to display compressed IPv6 addresses without truncation or confusing punctuation. Every output path was tested with short and long forms, loopback addresses, and addresses containing embedded IPv4 notation.

Maintaining a mature security tool also means documenting operational details. My broader notes on maintaining DenyHosts reflect why small compatibility decisions deserve the same care as the central detection algorithm.

Testing The Boundaries

Unit tests covered valid and invalid examples from both address families. For IPv6, I included compressed addresses, fully expanded addresses, uppercase hexadecimal digits, loopback values, IPv4-mapped addresses, malformed group counts, and illegal characters. The goal was to test the address grammar rather than a handful of convenient samples.

Regression tests replayed older SSH log fixtures and compared the resulting records with known output. These tests were essential because a change to tokenization can break IPv4 parsing even when every new IPv6 test passes. I also checked empty files, partially written logs, duplicate attacks, and records created by previous DenyHosts releases.

Integration tests examined the complete path: read a log, identify repeated failures, write a denied-host entry, reload the database, and generate output for the configured backend. Running the same scenario with IPv4 and IPv6 exposed assumptions that isolated unit tests could not see.

Keeping The Change Maintainable

A safe feature should be understandable to the next person who edits it. I kept address-family checks close to the code that needs them and avoided embedding IPv6 syntax directly into unrelated modules. Comments explain why normalization and backend capability checks exist, rather than restating what each line does.

The change also benefited from conservative release practices. Existing users could upgrade without manually converting their IPv4 data, while new installations gained IPv6 recognition automatically. Clear release notes described supported output paths and any platform-specific requirements, giving administrators enough information to verify their deployment.

For developers extending similar security software, these practices are useful:

The result was a change that felt deliberately uneventful. DenyHosts continued to detect and block IPv4 attacks as before, while IPv6 sources could move through the same monitoring pipeline without being discarded. That is the standard I wanted: broader protection with no unnecessary disruption.

If you maintain a DenyHosts deployment, review the configured blocking backend, enable IPv6-aware logging where appropriate, and test a controlled authentication failure before relying on the new path. If you work on an open-source parser, follow the same trail from input to storage to enforcement, and make each boundary explicit before adding another address family.