golang

Как я воевал с китайским умным туалетом для котов

  • суббота, 10 мая 2025 г. в 00:00:10
https://habr.com/ru/articles/908086/

В очередной раз, листая озон, я наткнулся на девайс, который привлек мое внимание. Самоочищающийся лоток для котов Petkit Pura Max, вещь весьма интересная, особенно, если у тебя три кота. Пушистые бандиты у меня крупные, потребляют много калорий и соответственно часто ходят в лоток.

слева-направо Максвелл, Ульмо и Эйнштейн
слева-направо Максвелл, Ульмо и Эйнштейн

Одна уборка за ними — это целый квест, а если хочешь уехать из дома на день‑другой, это обязательно нужно кого‑то просить убирать за ними. Умную кормушку и фонтан для воды я купил давно, остался только умный лоток. Но жаба знатно начала меня душить, когда я увидел цену этого девайса в районе 40 тысяч рублей, и в тот раз покупать его не стал.

Про эту мысль я уже забыл, пока однажды не зашел на китайский Poizon и не увидел там этот же лоток, но уже за какие‑то смешные 10 тысяч рублей, с доставкой в РФ 12 тысяч.


Недолго думая, я заказываю этот лоток через байера, и примерно через месяц становлюсь счастливым обладателем этого чудного девайса, пора бы радоваться, но нет?

Распаковав, я начал настраивать лоток и подключать к оригинальному приложению, но тут меня ждал сюрприз, данный девайс был заблокирован в моем регионе, мне рекомендовали вернуть его в Китай, либо связаться с продавцом. Продавца тут винить не в чем, он свою работу по доставке выполнил, кто же виноват, что не почитал регион действия лотка? Но у меня, честно говоря, даже в мыслях не было, что кто-то заморочится такой блокировкой.



Умный горшок 1:0 Инженер

Первым делом я пошел читать форумы, где нашел еще много таких же коллег по несчастью, они обсуждали разные варианты, как разблокировать этот горшок, там были и варианты с китайским впн, и установки старых версий apk приложений, кто‑то даже припаял кастомную плату которая через какой‑то период времени замыкает нужные контакты и вызывает отчистку.

Ничего из выше описанного мне не помогло.

Что ж поделать, придется проявить инженерную смекалку.

Подход №1

В заблокированном туалете не работала функция самоочистки и не было коннекта к родному приложению, но при нажатии кнопок руками туалет отчищался и моей первой мыслью стало каким-то образом имитировать ручное нажатие кнопок в нужной последовательности. Сказано, сделано: приобрел хаб от Tuya, к нему датчик движения и 2 кнопки fingerbot, которые имитируют нажатие пальцем. Немного поколдовав с настройками и выбрав оптимальные значения задержек, эта схема заработала, я приклеил все эти датчики к туалету и через пять минут после того, как кот сходил в лоток, кнопки нажимали нужную последовательность. И вуаля, туалет отчищался сам.

Снизу ИК датчик движения и 2 фингербота
Снизу ИК датчик движения и 2 фингербота

Умный горшок 1:1 Инженер

Тут я сравнял счет, теперь можно было уехать на день-другой, а лоток работал сам.

Немного порадовавшись, я обнаружил несколько неприятных нюансов такой работы.

  1. Однажды мы уехали на сутки из дома, и жена, имея привычку выключать все лишнее из розеток, случайно выключила умный хаб Tuya из розетки, вся конструкция перестала работать и пришлось срочно звонить знакомым, чтобы выручили.

  2. Излишние срабатывание, иногда один из котов просто проходил мимо и вызывал срабатывание датчика, при этом он мог в сам лоток даже не сходить.

  3. Коты отгрызали датчики и кнопки, приклеенные на лоток, когда им было скучно.

Подход №2

Когда коты в очередной раз отгрызли датчики, я понял, что моя схема работы далека от идеала, и нужно с этим что-то делать. На работе закончится контест по CTF и окрыленный 66м местом в общем зачете, я решил, что пора по-настоящему взяться за решение этого вопроса.

Для начала я подумал, что раз лоток просит подключение к вай фаю, значит он общается через интернет с серверами Petkit и, возможно, если там не https, у меня получится прочитать этот трафик и найти там какую-то подсказку. Но встал вопрос: как именно завернуть трафик от лотка на прокси? В случае с проксированием трафика телефона все просто, мы в настройках вай фая на девайсе вводим адрес прокси. Но что делать, если это IOT девайс? Тут мне в голову пришла мысль подменить айпи адрес сервера Petkit через DNS.

Наливаем кружку горячего чая и начинаем работу.

Первое, что нужно сделать - подменить резолв домена на наш айпи адрес, для этого я арендовал простенький linux сервер, и на нем установим свой dns прокси.

Для начала устанавливаем bind9

sudo apt update
sudo apt install bind9

После установки проверяем что bind9 работает

nslookup google.com 127.0.0.1

Ответ должен быть похожим на такой:

Server:		127.0.0.1
Address:	127.0.0.1#53

Non-authoritative answer:
Name:	google.com
Address: 216.58.207.238
Name:	google.com
Address: 2a00:1450:400f:80d::200e

Дальше настраиваем BIND9

Разрешаем BIND9 работать через брандмауэр

sudo ufw allow Bind9

Дальше редактируем конфиг

sudo vim /etc/bind/named.conf.options

По дефолту BIND9 работает только для локальной сети, нам необходимо разрешить все запросы к нему

allow-query { any; };

Так же поскольку наш DNS сервер это просто прокси, то нужно настроить DNS сервера куда будут перенаправляться запросы в случае если наш сервер не будет содержать необходимых данных, я пропишу DNS от Cloudflare

  forwarders {
    1.1.1.1;
    1.0.0.1;
  };

Итоговый конфиг у меня получился такой

options {
	directory "/var/cache/bind";
	forwarders {
	 	1.1.1.1;
	 	1.0.0.1;
	};
    allow-query { any; };
	dnssec-validation auto;
	listen-on-v6 { any; };
};

Дальше создаем файл новой зоны /etc/bind/db.eu-pet.comи прописываем туда ip адрес нашего сервера

;
; BIND data file for api.eu-pet.com
;
$TTL    86400
@       IN      SOA     ns1.eu-pet.com. admin.eu-pet.com. (
                        2024050701      ; Serial
                        3600            ; Refresh
                        1800            ; Retry
                        604800          ; Expire
                        86400 )         ; Minimum TTL

; Name servers
@       IN      NS      ns1.eu-pet.com.

; A records
@       IN      A       <your_server_ip_address>
api     IN      A       <your_server_ip_address>
ns1     IN      A       <your_server_ip_address>

; Wildcard
;*       IN      A       <your_server_ip_address>

Добавляем наш конфиг в BIND9 /etc/bind/named.conf.local

zone "eu-pet.com" {
    type master;
    file "/etc/bind/db.eu-pet.com";
};

Проверяем что все настройки корректные и перезапускаем BIND9

sudo named-checkconf
sudo named-checkzone eu-pet.com /etc/bind/db.eu-pet.com
sudo systemctl restart bind9

Теперь выходим с удаленного сервера и проверяем сработал ли DNS

nslookup api.eu-pet.com <your_ip_address>

Должны увидеть такой ответ

Server:		192.168.0.1
Address:	192.168.0.1#53

Name:	api.eu-pet.com
Address: <your_ip_address>

Значит наш DNS корректно перенаправляет трафик и мы можем взяться за написание нашей прокси.

Я написал простой прокси на golang, который поднял в докере на том же арендованном сервере, он ловил входящие запросы от лотка, отправлял их дальше на сервера petkit, получал оттуда ответы и затем подменял в ответе нужное поле и уже измененный ответ возвращал в ответе лотку.

Все это можно было бы сделать так же через Charles Proxy, но я хотел сделать решение, которое можно будет легко и просто переиспользовать любому.

Код получился довольно простым и без использования сторонних библиотек

package main

import (
	"bytes"
	"encoding/json"
	"io"
	"log"
	"net/http"
	"net/http/httputil"
	"net/url"
	"strconv"
)

const (
	targetURL   = "http://api.eu-pet.com"
	proxyPort   = ":8080"
	specialPath = "/6/t4/dev_device_info"
)

func modifyResponse(resp *http.Response) error {
	if resp.Request.URL.Path != specialPath {
		return nil
	}

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return err
	}
	bodyErr := resp.Body.Close()
	if bodyErr != nil {
		return bodyErr
	}

	var data map[string]interface{}
	if err := json.Unmarshal(body, &data); err != nil {
		log.Printf("JSON parse error: %v", err)
		return nil
	}

	if result, ok := data["result"].(map[string]interface{}); ok {
		if settings, ok := result["settings"].(map[string]interface{}); ok {
			if autowork, exists := settings["autoWork"].(float64); exists {
				log.Printf("Modifying autowork from %.0f to 1", autowork)
				settings["autoWork"] = 1
			}
		}
	}

	modifiedBody, err := json.Marshal(data)
	if err != nil {
		return err
	}

	resp.Body = io.NopCloser(bytes.NewBuffer(modifiedBody))
	resp.ContentLength = int64(len(modifiedBody))
	resp.Header.Set("Content-Length", strconv.Itoa(len(modifiedBody)))

	return nil
}

func NewReverseProxy(target *url.URL) *httputil.ReverseProxy {
	proxy := httputil.NewSingleHostReverseProxy(target)

	proxy.Director = func(req *http.Request) {
		req.URL.Scheme = target.Scheme
		req.URL.Host = target.Host
		req.Host = target.Host
	}

	proxy.ModifyResponse = func(resp *http.Response) error {
		return modifyResponse(resp)
	}

	proxy.ErrorHandler = func(rw http.ResponseWriter, req *http.Request, err error) {
		log.Printf("Proxy error: %v", err)
		rw.WriteHeader(http.StatusBadGateway)
		_, _ = rw.Write([]byte("Proxy error: " + err.Error()))
	}

	return proxy
}

func proxyHandler(proxy http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		log.Printf("Proxying request for host: %s", r.Host)
		proxy.ServeHTTP(w, r)
		return
	})
}

func main() {
	target, err := url.Parse(targetURL)
	if err != nil {
		log.Fatal(err)
	}
	proxy := NewReverseProxy(target)
	handler := proxyHandler(proxy)
	log.Printf("Starting proxy server on %s", proxyPort)
	log.Fatal(http.ListenAndServe(proxyPort, handler))
}

Исходники прокси если вы хотите развернуть у себя лежат тут https://github.com/dzaytsev91/petkit-proxy/


Теперь, когда dns настроен и прокси готово, осталось дело техники, прописываем наш dns в роутер. Дальше я несколько раз подключал лоток через приложение, посмотрел какие запросы/ответы он шлет, и оказалось, что все запросы идут через http, значит наш прокси легко осуществит подмену.
Дальше у меня ушло некоторое время на ковыряние ручек api petkit, и я довольно быстро нашел нужный урл и нужное поле.


Дальше нужно было заставить лоток дернуть именно этот урл, с чем я повозился еще примерно час, я пробовал выключать лоток из розетки, переподключать к приложению, менять wifi сеть и тд. Но ничего не заставляло лоток дернуть нужный мне урл, но в конце концов лоток сдался и совершил запрос (я так и не разобрался когда и почему он это делает), в ответе которого наш прокси успешно подменил поле autoWork=1 . После этого ждем, пока кот сходит по своим делам (благо их трое и они не заставили себя ждать). И вуаля, все работает!

Теперь просто меняем пароль на Wifi сети, к которой подключен лоток, и без связи с интернетом он навсегда запоминает настройки и работает в автоматическом режиме.
Конечно, говорить о полноценной работе лотка не приходится, но самая важная и нужная функция в нем была активирована.

В напряженной схватке я выгрызаю победу у Китайского чуда.

Умный горшок 1:2 Инженер