Skip to main content

Build Packet Tracer labs — without Packet Tracer

Create Packet Tracer .pkt labs and scored .pka activities entirely offline, with no Packet Tracer installed. This repo reverse-engineers the Packet Tracer file format — both container families, PT 4.1 through 9.0.1 — and ships a byte-exact codec plus a generator that writes devices, modules, cabling, IOS config, PC IPs, instructions, the activity-wizard password, and the scoring tree — straight to a file PT opens.

PyPI Docs License: GPL v3 Ko-fi Python 3.10+ No Packet Tracer needed Cisco Packet Tracer

Languages: English | 简体中文

Install

$ pip install packet-tracer-pkt-format

Python 3.10 or newer. The only runtime dependency is pydantic. Reading, inspecting, diffing, exporting and re-writing files needs nothing else — no Packet Tracer, no compiler, no system crypto library. libgcrypt, if present, and the optional Rust extension are picked up automatically as accelerators; the pure-Python ciphers are always available as the reference path.

Quickstart

Point it at any .pkt or .pka you already have:

$ pkt info my_lab.pkt                          # container metadata, size, root tag
$ pkt export my_lab.pkt --format graphviz -o topology.dot
$ pkt unpack my_lab.pkt -o out/                # XML + per-device IOS configs + topology
$ pkt pack out/my_lab.xml -o rebuilt.pkt       # byte-exact reconstruction
from pkt_core import decode
from pkt_core.models import Lab

lab = Lab.from_bytes(decode("my_lab.pkt").xml)
print(len(lab.devices), "devices,", len(lab.links), "links")
for dev in lab.devices:
    print(dev.name, dev.kind, dev.model)

Create a Packet Tracer, without Packet Tracer

Packet Tracer is only needed to open the result — the lab itself is authored here. The Lab builder ships with the package, templates included, so this works after a plain pip install with no checkout:

from pkt_core import Lab

lab = Lab()                       # bundled seed document and 137-device template library
lab.prune_seed_devices()          # start from a clean canvas
r  = lab.add_device("2911", "R1", 250, 350, install="HWIC-2T",
                    ios_config=["interface GigabitEthernet0/0",
                                " ip address 192.168.1.1 255.255.255.0", " no shutdown", "end"])
sw = lab.add_device("2960-24TT", "SW1", 450, 350)
pc = lab.add_device("PC-PT", "PC1", 650, 350,
                    pc_ip=("192.168.1.10", "255.255.255.0", "192.168.1.1"))
lab.link(r, "GigabitEthernet0/0", sw, "GigabitEthernet0/1")
lab.link(sw, "FastEthernet0/1", pc, "FastEthernet0")
lab.save("my_lab.pkt")            # open in Packet Tracer — no Packet Tracer used to build it

...or one-shot from a topology spec (devices + links → a scored, password-protected activity). These generators live in tools/ and run from a checkout:

$ python3 tools/make_scored_lab.py --out mylab.pka --spec topology.json --password hunter2

Modern Packet Tracer files have no magic header and are fully encrypted, so file, strings, and hex dumps show nothing but noise. This project documents the container and ships a small reader that turns a .pkt/.pka back into the plain XML Packet Tracer stores internally.

TL;DR

.pkt  ..  [ scramble ] -> [ Twofish-EAX, key=0x89*16, iv=0x10*16 ] ->
          [ XOR (len-i) ] -> [ qCompress/zlib ] -> "<?xml ... <PACKETTRACER5>"
  • Cipher: Twofish in EAX (AEAD) mode, 128-bit key = 0x89 repeated, IV = 0x10 repeated, 16-byte tag appended.
  • Pre-encryption wrapper: byte reversal + position XOR.
  • Post-decryption wrapper: XOR every byte with (len - i).
  • Payload: Qt qCompress (zlib) of a UTF-8 XML document.

Full details, evidence and the per-format key table are in FORMAT.md.

The pkt_core library

pt9.py is a self-contained reference codec. Alongside it, src/pkt_core/ is the type-safe library (Python 3.10+) intended for embedding — the CLI is a thin consumer of it and is never imported back into the package. Everything is checked under mypy --strict except pkt_core.builder, which is a port of the authoring code and is not yet annotated internally; its public signatures are typed.

from pkt_core import decode, encode_bytes

container = decode("lab.pkt")          # format is detected by the AEAD tag,
                                       # not by a magic prefix — there isn't one
print(container.root_tag)              # 'PACKETTRACER5'
print(container.is_activity)           # False  (.pka -> PACKETTRACER5_ACTIVITY)
xml = container.xml                    # the inner XML document

rebuilt = encode_bytes(xml, container.format_name)
from pkt_core import decode
from pkt_core.models import Lab

lab = Lab.from_bytes(decode("lab.pkt").xml)
for dev in lab.devices:
    print(dev.name, dev.kind, dev.model, dev.position)
for link in lab.links:
    print(link.port_names, link.cable_type)
Module Responsibility
pkt_core.crypto.primitives CMAC (RFC 4493), big-endian CTR, and the Crypto++ EAX variant
pkt_core.crypto.keys Per-format key schedule (twofish/serpent128/cast256, key & IV bytes)
pkt_core.crypto.stream The two positional-XOR obfuscation layers
pkt_core.crypto.ciphers_pure Pure-Python Twofish, Serpent and CAST-256
pkt_core.codec.qcompress Qt qCompress framing
pkt_core.codec.container Detect / decode / encode the whole container
pkt_core.xml Lossless byte-level XML layer (lazy, HTML-tolerant)
pkt_core.models Typed views: Lab, Device, Port, Link, Cluster, RunningConfig
pkt_core.builder Authoring: Lab (place, power, module, cable, configure), Layout
pkt_core.resources The bundled template library and seed document
pkt_core.diff Semantic device/link/config comparison
pkt_core.export graphviz, netbox, containerlab topology export
pkt_core.errors Typed exceptions — never a bare Exception

The models are views over the lossless XML tree, not a parallel data structure: writing a document out again emits the original source bytes for anything you did not touch, so data loss is impossible by construction. Unrecognised elements are surfaced in each model's extra_fields rather than dropped. IOS <LINE> config is exposed as bytes on purpose — it carries raw ^C banner delimiters that decoding would normalise away.

The AEAD is deterministic: key and IV are fixed per format, so the keystream is file-independent and identical XML always produces identical bytes. That is also what makes the EAX tag usable as a format-detection oracle (false-positive rate 2^-128) instead of a magic number.

Sample containers generated by this project are committed under corpus/samples/ so the tests run out of the box. The 1,026 third-party files used for the reverse engineering are not committed — see corpus/README.md for the policy and corpus/fetch_corpus.py to reproduce them.

Usage

pt9.py is the original single-file reader/writer and needs no install; pkt is the full CLI. Neither requires libgcrypt — install it (apt install libgcrypt20) only if you want the accelerated cipher path.

$ python3 pt9.py info   my_lab.pkt
root tag  : PACKETTRACER5
xml bytes : 1045755

$ python3 pt9.py decode my_lab.pkt -o my_lab.xml
$ python3 pt9.py decode course_activity.pka -o activity.xml      # .pka uses the same container
$ python3 pt9.py encode my_lab.xml -o my_lab.pkt                 # write it back
$ python3 -m unittest discover -s tests -v                       # self-tests

The pkt entry point adds inspection, diffing, export and auditing:

$ pkt info    my_lab.pkt                 # container metadata, size, root tag
$ pkt unpack  my_lab.pkt -o out/         # XML + per-device IOS configs + topology
$ pkt pack    out/my_lab.xml -o rebuilt.pkt   # byte-exact reconstruction
$ pkt diff    before.pkt after.pkt       # structural diff
$ pkt export  my_lab.pkt --format graphviz    # graphviz / netbox / containerlab
$ pkt audit   my_lab.pkt --templates tools/device_templates.json

pkt audit — why it exists

A count is only meaningful if the counted things are distinct. Early versions of this toolkit shipped a 290-device model whose service totals were inflated by template leftovers — 202 "AAA clients" of which only 140 were distinct (the other 63 were one phantom 192.168.10.1 copied off a harvested device template onto every device), and 259 "DHCP pools" of which 131 were distinct. The validator asserted presence, so it printed PASSED (0 warnings) while reporting both wrong numbers.

pkt audit prints raw and distinct counts for every service family, flags records that came from a harvested template rather than from the author, and exits non-zero on any duplicate. Real output for the enterprise model before it was sanitised:

$ pkt audit JASDF_AEWC_JADGE.pkt --templates tools/device_templates.json
device_names: 290 raw, 290 distinct
config lines  : min=78 median=143 max=250 total=22,648 (143 IOS/ASA devices,
                by model); 22,896 <LINE> elements in 205 RUNNINGCONFIG blocks
services      : 496 DNS records (248 distinct x 2 servers)
  aaa_clients: 202 raw, 140 distinct, 62 duplicate instances
               (e.g. 192.168.10.1 x63), 63 template leftovers -> 139 authored
  dhcp_pools : 259 raw, 133 distinct, 126 duplicate instances
               (e.g. serverPool / 0.0.0.0 / 0.0.0.0 x65), 128 template leftovers
  aaa_users  :  65 raw,   3 distinct, 62 duplicate instances (e.g. user x63)
  ftp_accounts: 64 raw,   3 distinct, 61 duplicate instances (e.g. cisco x62)
  FAIL duplicate-dhcp-pool-name: pool name 'serverPool' is used by 128 pools
  FAIL duplicate-dhcp-pool-name: pool name 'IRUMA-VLAN10' is used by 2 pools
  ...
RESULT: FAILED (15 failures, 0 warnings)

Note the config-line row: it prints both figures, with the method for each (IOS/ASA devices selected by model, versus raw <LINE> elements in all 205 blocks). The earlier documentation published one number without its method, which made it unreproducible. The numbers have since moved once — from 22,579/22,827 to 22,648/22,896 — when a device that was shipping a harvested template's factory-default config was found and properly configured. That is the point of printing the method: the figure is checkable, so it can be seen to change for a reason.

The rule this encodes, and which the library now follows everywhere: if a validator prints a count, it must prove those items are distinct.

Example decrypted root elements:

<PACKETTRACER5>
  <VERSION>9.0.1.0858</VERSION>
  <PIXMAPBANK>…</PIXMAPBANK>
  <DEVICES>…</DEVICES>
  <LINKS>…</LINKS>
  …

Status / scope

Both container families are fully decoded, both directions. encode(decode(f)) == f byte-for-byte on 1058 of 1058 real containers — see docs/CORPUS_VERIFICATION.md.

Family Written by Transform Files verified
modern PT 5.3.3 → 9.0.1 layer A → Crypto++ EAX (Twofish) → layer B → qCompress 865
legacy PT 4.1 → 5.3.2 layer B → qCompress (no AEAD, no reversal) 193
  • The AEAD authentication tag is reproduced exactly and verified on load.
  • Verified against Packet Tracer 9.0.1.0858 (Linux x86-64) and 1,058 real files spanning PT 4.1 through PT 9.0.1.
  • Neither family has a magic header. Detection is verifier-based: the EAX tag for the modern family (false-positive rate 2⁻¹²⁸), a structural qCompress probe for the legacy one. The container switchover is exactly 5.3.2 → 5.3.3.
  • Documents are not always well-formed XML — <LINE> config text carries raw control bytes (the Cisco banner motd ^C…^C delimiter is a literal \x03) and PT 4.1 documents end with a trailing NUL. Do not use a validating parser.
  • Three root elements exist: PACKETTRACER5, PACKETTRACER5_ACTIVITY and PACKETTRACER_ACTIVITY (PT 4.x). Root element is not a container discriminator.
  • .pkt/.pka are fully supported. The .pta/.pkc (Serpent) and internal sm/userapp (CAST-256) flavours are specified but need a crypto backend with Serpent/CAST-256 (stock libgcrypt has neither).

How it was found

Static analysis of the fully-symbolized PacketTracer binary located CNetworkFile::openFile, the Util::encrypt*/decrypt* family, the generic Util::cipher/decipher<Cipher> templates, and Crypto++'s EAX_Base / AuthenticatedDecryptionFilter. Packet Tracer was then run headless under Xvfb with gdb breakpoints to capture the exact ciphertext and plaintext, which pinned the CTR counter and both obfuscation layers with byte-accurate certainty.

Generating files

Because encoding is byte-exact, the same codec writes .pkt files. Tools in tools/ and notes in docs/SCENE_GRAPH.md:

  • tools/harvest_merge.py — harvest one complete device of every model from a tree of existing .pkt files and merge them into one canvas, handling the physical-workspace remapping PT requires.
  • tools/build_wired_lab.py — the same, plus auto-wiring: cables every ethernet-capable device to a backbone of core switches and chains the cores.
  • tools/build_configured_lab.py — a full working, configured lab (PC1-SW1-R1=R2-SW2-PC2 with IPs, gateways, static routes, and an optional long running-config). IOS config is written as <RUNNINGCONFIG><LINE>… commands and applied on load; PCs get <IP>/<SUBNET>/<PORT_GATEWAY>. See docs/CONFIGURATION.md.
  • tools/activity.py + tools/scoring.py — author activity files (.pka) with the Activity Wizard: instructions (HTML), the activity-wizard password, permissions, feedback, the initial / answer networks, and the scoring tree. See docs/ACTIVITY.md.
  • tools/make_scored_lab.py — one-shot offline generator: from a topology spec (--spec) it builds the initial + answer networks, derives scoring, and writes a scored .pka. No Packet Tracer needed. Ships with a library of 137 device templates, 75 module templates and 14 scoring shapes (tools/device_templates.json, tools/device_shapes.json).
  • tools/harvest_library.py — (re)build those libraries from your own .pkt/.pka files to extend model/shape coverage.
  • tools/lab_api.py — a small library of the verified authoring operations (place device, power on/off, install a module, cable copper, write IOS RUNNINGCONFIG, set PC IP/DHCP). Capability matrix + details in docs/CAPABILITIES.md.
  • Editing: decode → change device names/config/positions → encode → PT opens it.

Result of running the wired builder here: 147 devices, 140 distinct models, 71 cables (routers, switches, ASAs, WLCs, APs, IP phones, PCs/laptops/tablets, servers, and a large IoT/industrial set) in one file that Packet Tracer opens and draws connected.

Key facts for anyone generating files (details in the scene-graph notes):

  • Devices carry a physical scene placement that must be remapped when merging.
  • A cable binds by <FROM>/<TO> ids and <PORT> names; the device needs an <ENGINE>/<SAVE_REF_ID> and the port must exist on that exact instance. The *_MEM_ADDR fields are not validated — PT rebinds.
  • Interoperability research on a file format, using a locally licensed copy of Packet Tracer and the author's own files.
  • No DRM or licence protection is bypassed; there is no encryption password.
  • Intended for reading your own files, format documentation, tooling and archival.
  • Do not use this to obtain, alter, or submit assessment/competition answer keys. That is cheating and is out of scope.

License

GNU General Public License v3.0 or later — see LICENSE.

You may use, modify and redistribute this software under those terms, including commercially. There is no separate commercial licence to negotiate: the GPL already permits commercial use.

Earlier releases of this project were distributed under a non-commercial licence. That is no longer the case, and the GPL is the only licence that applies from the commit that added it onwards.

Metadata

Release files for packet-tracer-pkt-format 0.4.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for packet-tracer-pkt-format 0.4.0
File Size Uploaded Provenance
packet_tracer_pkt_format-0.4.0.tar.gz 890.7 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for packet-tracer-pkt-format 0.4.0
File Interpreter ABI Platform
packet_tracer_pkt_format-0.4.0-py3-none-any.whl Python 3 none any Details

Total release size: 1.7 MB

Release files / packet_tracer_pkt_format-0.4.0.tar.gz

Download URL packet_tracer_pkt_format-0.4.0.tar.gz
Size 890.7 kB
Tags Source
SHA-256 checksum
How to use checksums
faf7c22c2efebb3de50a821a4b92a6b07fa61bbf3e9987232afcadbbe3d22814
BLAKE2b-256 checksum
How to use checksums
fadc2b47b1a5470aad2d6743bd88258440d4a21ffbc7b611f84ef0af75d6f387
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Release files / packet_tracer_pkt_format-0.4.0-py3-none-any.whl

Download URL packet_tracer_pkt_format-0.4.0-py3-none-any.whl
Size 839.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
210313bd34198e719800580baa60cc22e863ecb40dc365ba9a4ecb779e3a8bf3
BLAKE2b-256 checksum
How to use checksums
a0c9193d5fc973a2d199d5e87d52b9903eb8f08fb55bf871a4a352a120612740
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Security for packet-tracer-pkt-format 0.4.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.4.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page