Advertisement
Open Source Projects by Phil Schwartz

Making DenyHosts Work Across Python 2 And Python 3

DenyHosts began as a focused Python utility for protecting SSH services from repeated login attacks. Its job was straightforward: inspect authentication logs, identify abusive addresses, and update host-deny rules before an attacker could continue guessing passwords. Keeping that workflow reliable became more complicated as Linux distributions moved from Python 2 to Python 3.

The migration was less about replacing a few print statements and more about preserving behavior across two runtimes with different ideas about text, bytes, exceptions, file handling, and standard-library organization. I wanted existing DenyHosts installations to keep working while giving newer systems a supported Python 3 path.

The practical solution was an incremental compatibility layer. Instead of rewriting every module at once, I isolated version differences, clarified the data flowing through the program, and used the same test cases against both interpreters.

Finding The Real Compatibility Risks

The first pass involved running DenyHosts under Python 3 and recording failures in dependency order. Syntax errors appeared immediately, but they were only the visible part of the migration. Code that parsed correctly could still produce incorrect results when a log line became a Unicode string instead of a byte sequence.

The most sensitive areas were log readers, configuration parsing, database access, socket-related utilities, and code that wrote generated host files. A security tool must treat small behavioral changes seriously. If a timestamp is parsed differently or an address is normalized inconsistently, the result can be missed attacks or an unnecessary block.

I also separated actual Python 3 problems from assumptions that had become difficult to maintain in Python 2. This distinction kept the port focused. The goal was compatibility, not an unrelated redesign of DenyHosts architecture.

Establishing A Shared Runtime Layer

I used compatibility imports and small helper functions to hide differences between Python versions. Imports that moved or changed names were handled in one place, while commonly used operations such as string types, integer checks, and exception syntax were updated consistently throughout the project.

The key principle was to keep application code readable. Rather than scatter version checks across every log parser, I made the parser operate on a predictable text representation. Files were opened with explicit encoding decisions where appropriate, and byte-oriented interfaces were converted at their boundaries instead of being allowed to leak into the rest of the program.

This approach also made review easier. A maintainer could inspect one compatibility helper and understand why it existed, rather than reconstructing dozens of conditional branches. It reduced the chance that a future change would fix Python 3 while quietly breaking older installations.

Making Log Processing Deterministic

DenyHosts depends on recognizing patterns in SSH authentication messages, and log formats are not perfectly uniform across operating systems. Python 3 migration exposed places where regular expressions, line endings, and string interpolation had relied on Python 2 behavior.

I revised those paths so every input line was decoded before matching and every output operation used an intentional text or byte mode. Regular expression patterns and replacement values now shared the same type. This eliminated confusing errors where a pattern expected text but received bytes.

The same discipline applies to related diagnostic work. When I used Scratchy attack analysis to examine web-server activity, the lesson was similar: reliable parsing begins with clearly defined input boundaries. Whether the source is an Apache access log or an SSH authentication log, ambiguous encoding and inconsistent normalization can obscure the event being investigated.

Comparing The Two Supported Paths

Compatibility meant more than making both interpreters launch the program. Each runtime needed to produce equivalent decisions when given the same configuration and log data. I compared the important stages of processing rather than relying only on successful startup.

Area Python 2 Consideration Python 3 Consideration Compatibility Approach
Text input Implicit string assumptions Clear text and byte separation Decode at file and process boundaries
Exceptions Older syntax and patterns Modern exception syntax Standardize handling without changing behavior
Imports Legacy module locations Renamed or reorganized modules Centralize conditional imports
Dictionary behavior Different view semantics Dynamic view objects Convert explicitly where lists are required
File output Platform-dependent defaults More explicit encoding concerns Set modes and encoding deliberately
Testing Existing historical fixtures New edge-case failures Run identical fixtures under both runtimes

The comparison also helped identify false positives in the port. Some outputs differed in ordering because dictionary behavior changed, even though the blocked-address set was identical. I adjusted tests to verify meaningful results rather than accidental presentation details.

For security-related software, regression tests need representative hostile input. I included repeated failures, malformed lines, IPv4 and IPv6 addresses where supported, comments, empty files, rotated logs, and previously recorded addresses. These cases made the migration measurable instead of subjective.

Preserving Configuration And State

Users should not have to rebuild DenyHosts state simply because the interpreter changed. Configuration files, working directories, synchronized host data, and blocked-address databases therefore received special attention. I treated existing files as an API: their format and meaning needed to remain stable unless a change was unavoidable.

Python 3 also made it easier to notice implicit assumptions about serialized data. Values read from persistent storage were normalized before comparison, and writes were made consistently so a Python 2 process and a Python 3 process would not interpret the same state differently.

I avoided changing security policy during the runtime migration. Thresholds, whitelist behavior, synchronization settings, and blocking decisions should be reviewed independently from interpreter support. Keeping those concerns separate makes it possible to audit the port without wondering whether a policy change caused a new result.

Testing Migration Changes In Layers

I began with fast unit tests for parsers and utility functions, then moved to integration tests that processed complete log fixtures. Finally, I installed the program in clean environments and exercised the command-line workflow, configuration loading, state updates, and generated output.

Running the same suite under Python 2 and Python 3 was essential. It exposed differences that a single-runtime test run could never reveal, including changed return types, altered sorting behavior, and failures that appeared only when a file contained non-ASCII text.

I also tested upgrade scenarios rather than only fresh installations. Existing users often have years of accumulated state and locally customized configuration. A migration is successful when those installations continue operating predictably, not merely when a new checkout passes its tests.

Practical Rules For Future Ports

The DenyHosts migration reinforced a few habits that are useful for maintaining older Python security tools:

A compatibility port is easier to maintain when its compromises are documented. Future contributors should be able to see why a helper exists, which Python versions it supports, and what behavior the tests protect.

The finished work gave DenyHosts a gradual path from Python 2 to Python 3 without forcing users into an abrupt rewrite. If you maintain a Python security utility facing the same transition, start by measuring behavior, isolate the runtime differences, and run both interpreters against the same evidence. That process can turn a risky migration into a controlled maintenance release.