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

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

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

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

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