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

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

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

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

Умный горшок 1:1 Инженер
Тут я сравнял счет, теперь можно было уехать на день-другой, а лоток работал сам.
Немного порадовавшись, я обнаружил несколько неприятных нюансов такой работы.
Однажды мы уехали на сутки из дома, и жена, имея привычку выключать все лишнее из розеток, случайно выключила умный хаб Tuya из розетки, вся конструкция перестала работать и пришлось срочно звонить знакомым, чтобы выручили.
Излишние срабатывание, иногда один из котов просто проходил мимо и вызывал срабатывание датчика, при этом он мог в сам лоток даже не сходить.
Коты отгрызали датчики и кнопки, приклеенные на лоток, когда им было скучно.
Когда коты в очередной раз отгрызли датчики, я понял, что моя схема работы далека от идеала, и нужно с этим что-то делать. На работе закончится контест по 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 Инженер