--- /dev/null
+#!/usr/bin/env python3
+
+'''A program for manipulating tplink2022 images.
+
+A tplink2022 is an image format encountered on TP-Link devices around the year
+2022. This was seen at least on the EAP610-Outdoor. The format is a container
+for a rootfs, and has optional fields for the "software" version. It also
+ requires a "support" string that describes the list of compatible devices.
+
+This module is intended for creating such images with an OpenWRT UBI image, but
+also supports analysis and extraction of vendor images. Altough tplink2022
+images can be signed, this program does not support signing image.
+
+To get an explanation of the commandline arguments, run this program with the
+"--help" argument.
+'''
+
+import argparse
+import hashlib
+import os
+import pprint
+import struct
+
+def decode_header(datafile):
+ '''Read the tplink2022 image header anbd decode it into a dictionary'''
+ header = {}
+ fmt = '>2I'
+
+ datafile.seek(0x1014)
+ raw_header = datafile.read(8)
+ fields = struct.unpack(fmt, raw_header)
+
+ header['rootfs_size'] = fields[0]
+ header['num_items'] = fields[1]
+ header['items'] = []
+
+ rootfs = {}
+ rootfs['name'] = 'rootfs.ubi'
+ rootfs['offset'] = 0
+ rootfs['size'] = header['rootfs_size']
+ header['items'].append(rootfs)
+
+ for _ in range(header['num_items']):
+ entry = datafile.read(0x2c)
+ fmt = '>I32s2I'
+ fields = struct.unpack(fmt, entry)
+
+ section = {}
+ section['name'] = fields[1].decode("utf-8").rstrip('\0')
+ section['type'] = fields[0]
+ section['offset'] = fields[2]
+ section['size'] = fields[3]
+ header['items'].append(section)
+ return header
+
+def extract(datafile):
+ '''Extract the sections of the tplink2022 image to separate files'''
+ header = decode_header(datafile)
+
+ pretty = pprint.PrettyPrinter(indent=4, sort_dicts=False)
+ pretty.pprint(header)
+
+ for section in header['items']:
+ datafile.seek(0x1814 + section['offset'])
+ section_contents = datafile.read(section['size'])
+
+ with open(f"{section['name']}.bin", 'wb') as section_file:
+ section_file.write(section_contents)
+
+ with open('leftover.bin', 'wb') as extras_file:
+ extras_file.write(datafile.read())
+
+def get_section_contents(section):
+ '''I don't remember what this does. It's been a year since I wrote this'''
+ if section.get('data'):
+ data = section['data']
+ elif section.get('file'):
+ with open(section['file'], 'rb') as section_file:
+ data = section_file.read()
+ else:
+ data = bytes()
+
+ if section['size'] != len(data):
+ raise ValueError("Wrong section size", len(data))
+
+ return data
+
+def write_image(output_image, header):
+ '''Write a tplink2022 image with the contents in the "header" dictionary'''
+ with open(output_image, 'w+b') as out_file:
+ # header MD5
+ salt = [ 0x7a, 0x2b, 0x15, 0xed,
+ 0x9b, 0x98, 0x59, 0x6d,
+ 0xe5, 0x04, 0xab, 0x44,
+ 0xac, 0x2a, 0x9f, 0x4e
+ ]
+
+ out_file.seek(4)
+ out_file.write(bytes(salt))
+
+ # unknown section
+ out_file.write(bytes([0xff] * 0x1000))
+
+ # Table of contents
+ raw_header = struct.pack('>2I', header['rootfs_size'],
+ header['num_items'])
+ out_file.write(raw_header)
+
+ for section in header['items']:
+ if section['name'] == 'rootfs.ubi':
+ continue
+
+ hdr = struct.pack('>I32s2I',
+ section.get('type', 0),
+ section['name'].encode('utf-8'),
+ section['offset'],
+ section['size']
+ )
+
+ out_file.write(hdr)
+
+ for section in header['items']:
+ out_file.seek(0x1814 + section['offset'])
+ out_file.write(get_section_contents(section))
+
+ size = out_file.tell()
+
+ out_file.seek(4)
+ md5_sum = hashlib.md5(out_file.read())
+
+ out_file.seek(0)
+ out_file.write(struct.pack('>I16s', size, md5_sum.digest()))
+
+def encode_soft_verson():
+ '''Not sure of the meaning of version. Also doesn't appear to be needed.'''
+ return struct.pack('>4B1I2I', 0xff, 1, 0 ,0, 0x2020202, 30000, 1)
+
+def create_image(output_image, root, support):
+ '''Create an image with a ubi "root" and a "support" string.'''
+ header = {}
+
+ header['rootfs_size'] = os.path.getsize(root)
+ header['items'] = []
+
+ rootfs = {}
+ rootfs['name'] = 'rootfs.ubi'
+ rootfs['file'] = root
+ rootfs['offset'] = 0
+ rootfs['size'] = header['rootfs_size']
+ header['items'].append(rootfs)
+
+ support_list = {}
+ support_list['name'] = 'support-list'
+ support_list['data'] = support.replace(" ", "\r\n").encode('utf-8')
+ support_list['offset'] = header['rootfs_size']
+ support_list['size'] = len(support_list['data'])
+ header['items'].append(support_list)
+
+ sw_version = {}
+ sw_version['name'] = 'soft-version'
+ sw_version['type'] = 1
+ sw_version['data'] = encode_soft_verson()
+ sw_version['offset'] = support_list['offset'] + support_list['size']
+ sw_version['size'] = len(sw_version['data'])
+ header['items'].append(sw_version)
+
+ header['num_items'] = len(header['items']) - 1
+ write_image(output_image, header)
+
+def main(args):
+ '''We support image analysis,extraction, and creation'''
+ if args.extract:
+ with open(args.image, 'rb') as image:
+ extract(image)
+ elif args.create:
+ if not args.rootfs or not args.support:
+ raise ValueError('To create an image, specify rootfs and support list')
+ create_image(args.image, args.rootfs, args.support)
+ else:
+ with open(args.image, 'rb') as image:
+ header = decode_header(image)
+
+ pretty = pprint.PrettyPrinter(indent=4, sort_dicts=False)
+ pretty.pprint(header)
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description='EAP extractor')
+ parser.add_argument('--info', action='store_true')
+ parser.add_argument('--extract', action='store_true')
+ parser.add_argument('--create', action='store_true')
+ parser.add_argument('image', type=str,
+ help='Name of image to create or decode')
+ parser.add_argument('--rootfs', type=str,
+ help='When creating an EAP image, UBI image with rootfs and kernel')
+ parser.add_argument('--support', type=str,
+ help='String for the "support-list" section')
+
+ main(parser.parse_args())
--- /dev/null
+// SPDX-License-Identifier: GPL-2.0-or-later OR MIT
+
+/dts-v1/;
+
+#include "ipq6018.dtsi"
+#include "ipq6018-cp-cpu.dtsi"
+#include "ipq6018-ess.dtsi"
+
+#include <dt-bindings/gpio/gpio.h>
+#include <dt-bindings/input/input.h>
+#include <dt-bindings/leds/common.h>
+
+/ {
+ model = "TP-Link EAP610-Outdoor";
+ compatible = "tplink,eap610-outdoor", "qcom,ipq6018";
+
+ aliases {
+ serial0 = &blsp1_uart3;
+ led-boot = &led_sys_green;
+ led-failsafe = &led_sys_amber;
+ led-running = &led_sys_green;
+ led-upgrade = &led_sys_amber;
+ };
+
+ chosen {
+ stdout-path = "serial0:115200n8";
+ bootargs-append = " ubi.block=0,rootfs root=/dev/ubiblock0_1";
+ };
+
+ keys {
+ compatible = "gpio-keys";
+
+ reset {
+ label = "reset";
+ gpios = <&tlmm 9 GPIO_ACTIVE_LOW>;
+ linux,code = <KEY_RESTART>;
+ };
+ };
+
+ leds {
+ compatible = "gpio-leds";
+
+ led_sys_amber: led-0 {
+ function = "system";
+ color = <LED_COLOR_ID_AMBER>;
+ gpios = <&tlmm 35 GPIO_ACTIVE_HIGH>;
+ };
+
+ led_sys_green: led-1 {
+ function = "system";
+ color = <LED_COLOR_ID_GREEN>;
+ gpios = <&tlmm 37 GPIO_ACTIVE_HIGH>;
+ };
+ };
+
+ gpio-restart {
+ compatible = "gpio-restart";
+ gpios = <&tlmm 61 GPIO_ACTIVE_LOW>;
+ open-source;
+ };
+};
+
+&blsp1_uart3 {
+ pinctrl-0 = <&serial_3_pins>;
+ pinctrl-names = "default";
+ status = "okay";
+};
+
+&tlmm {
+ mdio_pins: mdio-pins {
+ mdc {
+ pins = "gpio64";
+ function = "mdc";
+ drive-strength = <8>;
+ bias-pull-up;
+ };
+
+ mdio {
+ pins = "gpio65";
+ function = "mdio";
+ drive-strength = <8>;
+ bias-pull-up;
+ };
+ };
+
+ led_enable {
+ gpio-hog;
+ output-high;
+ gpios = <36 GPIO_ACTIVE_HIGH>;
+ line-name = "enable-leds";
+ };
+};
+
+&dp5 {
+ phy-handle = <&rtl8211f_4>;
+ phy-mode = "sgmii";
+ label = "lan";
+ status = "okay";
+};
+
+&edma {
+ status = "okay";
+};
+
+&mdio {
+ pinctrl-0 = <&mdio_pins>;
+ pinctrl-names = "default";
+ reset-gpios = <&tlmm 77 GPIO_ACTIVE_LOW>;
+ reset-delay-us = <10000>;
+ reset-post-delay-us = <50000>;
+ status = "okay";
+
+ rtl8211f_4: ethernet-phy@4 {
+ reg = <4>;
+ };
+};
+
+&switch {
+ switch_lan_bmp = <ESS_PORT5>;
+ switch_mac_mode1 = <MAC_MODE_SGMII_CHANNEL0>;
+ status = "okay";
+
+ qcom,port_phyinfo {
+ port@4 {
+ port_id = <5>;
+ phy_address = <4>;
+ };
+ };
+};
+
+&qpic_bam {
+ status = "okay";
+};
+
+&qpic_nand {
+ status = "okay";
+
+ nand@0 {
+ reg = <0>;
+
+ nand-ecc-strength = <4>;
+ nand-ecc-step-size = <512>;
+ nand-bus-width = <8>;
+ };
+};
+
+&wifi {
+ ieee80211-freq-limit = <2402000 5835000>;
+ qcom,ath11k-calibration-variant = "TP-Link-EAP610-Outdoor";
+ status = "okay";
+};