Update v1.2.0

This commit is contained in:
amurcanov
2026-05-26 22:48:52 +03:00
parent 63ba2cf1d9
commit bc0c8f5fc9
33 changed files with 1689 additions and 546 deletions
+240 -56
View File
@@ -27,6 +27,8 @@ import (
"syscall"
"time"
"crypto/cipher"
"github.com/pion/dtls/v3"
"github.com/pion/dtls/v3/pkg/crypto/selfsign"
"golang.org/x/crypto/chacha20poly1305"
@@ -45,8 +47,6 @@ import (
const (
wgIfaceName = "wdtt0"
wgServerAddr = "10.66.66.1"
wgClientAddr = "10.66.66.2"
wgClientCIDR = wgClientAddr + "/32"
wgServerCIDR = wgServerAddr + "/24"
defaultInternalWGPort = 56001
dns = "1.1.1.1"
@@ -64,23 +64,16 @@ type ClientDevice struct {
}
type PasswordEntry struct {
DeviceID string `json:"device_id"` // пусто = ещё не привязан
ExpiresAt int64 `json:"expires_at"` // unix timestamp
DownBytes int64 `json:"down_bytes"` // скачано клиентом
UpBytes int64 `json:"up_bytes"` // отдано клиентом
DeviceID string `json:"device_id"` // пусто = ещё не привязан
ExpiresAt int64 `json:"expires_at"` // unix timestamp
DownBytes int64 `json:"down_bytes"` // скачано клиентом
UpBytes int64 `json:"up_bytes"` // отдано клиентом
VkHash string `json:"vk_hash,omitempty"`
Ports string `json:"ports,omitempty"` // "dtls,wg,tun"
IsDeactivated bool `json:"is_deactivated,omitempty"`
}
// Трафик главного пароля (владельца)
var (
mainPassDown int64
mainPassUp int64
)
// Онлайн-статус устройств
var (
activeDevices = make(map[string]int32) // deviceID -> кол-во активных коннектов
activeDevicesMu sync.Mutex
)
type Database struct {
MainPassword string `json:"main_password"`
@@ -120,6 +113,37 @@ func generatePassword() string {
return string(b)
}
var publicIP string = ""
func getPublicIP() string {
if publicIP != "" {
return publicIP
}
client := &http.Client{Timeout: 5 * time.Second}
resp, err := client.Get("https://api.ipify.org")
if err != nil {
return "YOUR_SERVER_IP"
}
defer resp.Body.Close()
ipBytes, err := io.ReadAll(resp.Body)
if err != nil {
return "YOUR_SERVER_IP"
}
publicIP = string(bytes.TrimSpace(ipBytes))
return publicIP
}
func stripVkUrl(url string) string {
url = strings.TrimSpace(url)
if idx := strings.LastIndex(url, "/"); idx != -1 {
url = url[idx+1:]
}
if idx := strings.Index(url, "?"); idx != -1 {
url = url[:idx]
}
return strings.TrimSpace(url)
}
type wrapKeyEntry struct {
id string
key []byte
@@ -340,7 +364,7 @@ func botLoop(token string, adminIDstr string, wgDev *device.Device) {
// Устанавливаем команды для синей кнопки Menu
go func() {
cmds := `{"commands":[{"command":"new","description":"Создать временный пароль"},{"command":"list","description":"Управление доступами"}]}`
cmds := `{"commands":[{"command":"start","description":"Главное меню"},{"command":"new","description":"Создать временный пароль"},{"command":"list","description":"Управление доступами"}]}`
resp, err := http.Post(fmt.Sprintf("https://api.telegram.org/bot%s/setMyCommands", token), "application/json", strings.NewReader(cmds))
if err == nil {
resp.Body.Close()
@@ -350,8 +374,14 @@ func botLoop(token string, adminIDstr string, wgDev *device.Device) {
offset := 0
client := &http.Client{Timeout: 65 * time.Second}
// Состояние ожидания ввода дней
// Состояние ожидания ввода
var waitingForDays bool
var waitingForPorts bool
var waitingForHash bool
var targetPassword string
var tempDays int
var tempPorts string // "dtls,wg,tun"
for {
url := fmt.Sprintf("https://api.telegram.org/bot%s/getUpdates?timeout=60&offset=%d", token, offset)
@@ -410,6 +440,22 @@ func botLoop(token string, adminIDstr string, wgDev *device.Device) {
continue
}
txt := fmt.Sprintf("🔑 *Пароль:* `%s`\n", pass)
if entry.VkHash != "" {
ports := entry.Ports
if ports == "" {
ports = "56000,56001,9000"
}
pts := strings.Split(ports, ",")
srvIP := getPublicIP()
link := fmt.Sprintf("wdtt://%s:%s:%s:%s:%s:%s", srvIP, pts[0], pts[1], pts[2], pass, entry.VkHash)
txt += fmt.Sprintf("🔗 *Быстрая ссылка:* `%s`\n", link)
}
if entry.IsDeactivated {
txt += "🔴 Статус: *ДЕАКТИВИРОВАН*\n"
} else {
txt += "🟢 Статус: *АКТИВЕН*\n"
}
if entry.ExpiresAt > 0 {
expireTime := time.Unix(entry.ExpiresAt, 0)
remaining := time.Until(expireTime)
@@ -421,6 +467,8 @@ func botLoop(token string, adminIDstr string, wgDev *device.Device) {
} else {
txt += "⏰ Бессрочный ♾\n"
}
txt += fmt.Sprintf("\n📊 *Трафик:*\n• Скачано: %.2f MB\n• Отдано: %.2f MB\n", float64(entry.DownBytes)/(1024*1024), float64(entry.UpBytes)/(1024*1024))
txt += "\n📱 *Привязанное устройство:*\n"
var kb []map[string]interface{}
if entry.DeviceID == "" {
@@ -438,6 +486,17 @@ func botLoop(token string, adminIDstr string, wgDev *device.Device) {
})
}
dbMutex.Unlock()
if entry.IsDeactivated {
kb = append(kb, map[string]interface{}{
"text": "✅ Активировать",
"callback_data": "react_" + pass,
})
} else {
kb = append(kb, map[string]interface{}{
"text": "⏸ Деактивировать",
"callback_data": "deact_" + pass,
})
}
kb = append(kb, map[string]interface{}{
"text": "❌ Удалить пароль",
"callback_data": "delpass_" + pass,
@@ -452,6 +511,44 @@ func botLoop(token string, adminIDstr string, wgDev *device.Device) {
}
sendTelegram(token, adminID, txt, map[string]interface{}{"inline_keyboard": keyboard})
} else if strings.HasPrefix(data, "deact_") {
pass := strings.TrimPrefix(data, "deact_")
dbMutex.Lock()
entry, exists := db.Passwords[pass]
if exists && entry != nil {
entry.IsDeactivated = true
// Отключаем активное устройство от WG если нужно
if entry.DeviceID != "" {
if dev, devExists := db.Devices[entry.DeviceID]; devExists {
pubHex, _ := b64ToHex(dev.PubKey)
wgDev.IpcSet(fmt.Sprintf("public_key=%s\nremove=true\n", pubHex))
}
}
saveDB()
}
dbMutex.Unlock()
sendTelegram(token, adminID, fmt.Sprintf("⏸ Пароль `%s` деактивирован", pass), nil)
} else if strings.HasPrefix(data, "react_") {
pass := strings.TrimPrefix(data, "react_")
dbMutex.Lock()
entry, exists := db.Passwords[pass]
if exists && entry != nil {
entry.IsDeactivated = false
saveDB()
}
dbMutex.Unlock()
sendTelegram(token, adminID, fmt.Sprintf("✅ Пароль `%s` активирован", pass), nil)
} else if data == "mainlink" {
targetPassword = "main"
var keyboard [][]map[string]interface{}
keyboard = append(keyboard, []map[string]interface{}{
{"text": "Да", "callback_data": "ports_def"},
{"text": "Нет", "callback_data": "ports_custom"},
})
sendTelegram(token, adminID, "⚙️ Использовать стандартные порты для главного пароля (56000, 56001, 9000)?", map[string]interface{}{"inline_keyboard": keyboard})
} else if strings.HasPrefix(data, "unbind_") {
pass := strings.TrimPrefix(data, "unbind_")
dbMutex.Lock()
@@ -509,6 +606,13 @@ func botLoop(token string, adminIDstr string, wgDev *device.Device) {
} else if data == "backlist" {
sendPasswordList(token, adminID, wgDev)
} else if data == "ports_def" {
tempPorts = "56000,56001,9000"
waitingForHash = true
sendTelegram(token, adminID, "🔑 Укажите VK хеш (или несколько через запятую):", nil)
} else if data == "ports_custom" {
waitingForPorts = true
sendTelegram(token, adminID, "⚙️ Укажите через запятую 3 порта (DTLS,WG,TUN):\nНапример: 56000,56001,9000", nil)
}
}
@@ -528,7 +632,68 @@ func botLoop(token string, adminIDstr string, wgDev *device.Device) {
sendTelegram(token, adminID, "❌ Неверное значение. Укажите число от 1 до 365, или отправьте /new заново.", nil)
continue
}
expiresAt := time.Now().Add(time.Duration(days) * 24 * time.Hour).Unix()
tempDays = days
var keyboard [][]map[string]interface{}
keyboard = append(keyboard, []map[string]interface{}{
{"text": "Да", "callback_data": "ports_def"},
{"text": "Нет", "callback_data": "ports_custom"},
})
sendTelegram(token, adminID, "⚙️ Использовать стандартные порты (56000, 56001, 9000)?", map[string]interface{}{"inline_keyboard": keyboard})
continue
}
if waitingForPorts {
parts := strings.Split(cmd, ",")
if len(parts) != 3 {
sendTelegram(token, adminID, "❌ Неверный формат. Укажите 3 порта через запятую (например: 56000,56001,9000):", nil)
continue
}
p1 := strings.TrimSpace(parts[0])
p2 := strings.TrimSpace(parts[1])
p3 := strings.TrimSpace(parts[2])
if _, err := strconv.Atoi(p1); err != nil {
sendTelegram(token, adminID, "❌ Неверный порт. Повторите ввод:", nil)
continue
}
if _, err := strconv.Atoi(p2); err != nil {
sendTelegram(token, adminID, "❌ Неверный порт. Повторите ввод:", nil)
continue
}
if _, err := strconv.Atoi(p3); err != nil {
sendTelegram(token, adminID, "❌ Неверный порт. Повторите ввод:", nil)
continue
}
waitingForPorts = false
tempPorts = fmt.Sprintf("%s,%s,%s", p1, p2, p3)
waitingForHash = true
sendTelegram(token, adminID, "🔑 Укажите VK хеш (или несколько через запятую):", nil)
continue
}
if waitingForHash {
hash := strings.ReplaceAll(cmd, " ", "")
if strings.Contains(hash, "http") || strings.Contains(hash, "/") {
sendTelegram(token, adminID, "❌ Пожалуйста, отправьте только хеш (или несколько хешей через запятую). Ссылки не поддерживаются.", nil)
continue
}
if hash == "" {
sendTelegram(token, adminID, "❌ Хеш не должен быть пустым.", nil)
continue
}
waitingForHash = false
if targetPassword == "main" {
targetPassword = ""
srvIP := getPublicIP()
pts := strings.Split(tempPorts, ",")
link := fmt.Sprintf("wdtt://%s:%s:%s:%s:%s:%s", srvIP, pts[0], pts[1], pts[2], db.MainPassword, hash)
sendTelegram(token, adminID, fmt.Sprintf("🔗 *Ссылка для главного пароля:*\n`%s`", link), nil)
continue
}
dbMutex.Lock()
if cleanupExpiredPasswordsLocked(wgDev) > 0 {
saveDB()
@@ -556,11 +721,21 @@ func botLoop(token string, adminIDstr string, wgDev *device.Device) {
sendTelegram(token, adminID, "❌ Не удалось создать WRAP-ключ для пароля. Повторите /new.", nil)
continue
}
db.Passwords[newPass] = &PasswordEntry{ExpiresAt: expiresAt}
expiresAt := time.Now().Add(time.Duration(tempDays) * 24 * time.Hour).Unix()
db.Passwords[newPass] = &PasswordEntry{
ExpiresAt: expiresAt,
VkHash: hash,
Ports: tempPorts,
}
saveDB()
dbMutex.Unlock()
expDate := time.Unix(expiresAt, 0).Format("02.01.2006")
sendTelegram(token, adminID, fmt.Sprintf("🔑 Новый пароль:\n`%s`\n\n⏰ Действует %d дн. (до %s)\n📱 Ожидает первого подключения", newPass, days, expDate), nil)
srvIP := getPublicIP()
pts := strings.Split(tempPorts, ",")
link := fmt.Sprintf("wdtt://%s:%s:%s:%s:%s:%s", srvIP, pts[0], pts[1], pts[2], newPass, hash)
sendTelegram(token, adminID, fmt.Sprintf("🔑 Новый пароль:\n`%s`\n\n⏰ Действует %d дн. (до %s)\n📱 Ожидает первого подключения\n\n🔗 *Быстрая ссылка:* `%s`", newPass, tempDays, expDate, link), nil)
continue
}
@@ -677,6 +852,10 @@ func sendPasswordList(token string, adminID int64, wgDev *device.Device) {
txt += fmt.Sprintf("🔒 Главный: `%s` (владелец)\n\n", db.MainPassword)
var inlineKb []map[string]interface{}
inlineKb = append(inlineKb, map[string]interface{}{
"text": "🔗 Ссылка на главный пароль",
"callback_data": "mainlink",
})
if len(db.Passwords) == 0 {
txt += "_Нет сгенерированных паролей._\n"
@@ -1224,7 +1403,6 @@ func main() {
func handleConn(ctx context.Context, clientConn net.Conn, wgEndpoint string, wgDev *device.Device, keys *wgKeys) {
atomic.AddInt64(&totalConns, 1)
var connDeviceID string
var connPassword string
var connIsMainPass bool
@@ -1276,14 +1454,16 @@ func handleConn(ctx context.Context, clientConn net.Conn, wgEndpoint string, wgD
entry, isGenPass := db.Passwords[password]
valid := isMainPass || (isGenPass && !isPasswordExpired(entry))
// Для сгенерированных паролей — проверяем привязку к устройству
if valid && isGenPass && entry.DeviceID != "" && entry.DeviceID != deviceID {
if valid && isGenPass && entry.IsDeactivated {
clientConn.Write([]byte("DENIED:deactivated"))
log.Printf("[WG] Отказ: пароль %s деактивирован, запрос от %s", maskPassword(password), deviceID)
dbMutex.Unlock()
} else if valid && isGenPass && entry.DeviceID != "" && entry.DeviceID != deviceID {
// Пароль уже привязан к другому устройству
clientConn.Write([]byte("DENIED:device_mismatch"))
log.Printf("[WG] Отказ: пароль %s привязан к %s, запрос от %s", maskPassword(password), entry.DeviceID, deviceID)
dbMutex.Unlock()
} else if valid {
connDeviceID = deviceID
connPassword = password
connIsMainPass = isMainPass
@@ -1364,20 +1544,7 @@ func handleConn(ctx context.Context, clientConn net.Conn, wgEndpoint string, wgD
}
atomic.AddInt64(&totalBytesFromClient, int64(len(firstPacket)))
// Трекинг онлайн-статуса
if connDeviceID != "" {
activeDevicesMu.Lock()
activeDevices[connDeviceID]++
activeDevicesMu.Unlock()
defer func() {
activeDevicesMu.Lock()
activeDevices[connDeviceID]--
if activeDevices[connDeviceID] <= 0 {
delete(activeDevices, connDeviceID)
}
activeDevicesMu.Unlock()
}()
}
pctx, pcancel := context.WithCancel(ctx)
defer pcancel()
@@ -1413,9 +1580,7 @@ func handleConn(ctx context.Context, clientConn net.Conn, wgEndpoint string, wgD
}
atomic.AddInt64(&totalBytesFromClient, int64(nn))
// Per-password upload tracking
if connIsMainPass {
atomic.AddInt64(&mainPassUp, int64(nn))
} else if connPassword != "" {
if connPassword != "" && !connIsMainPass {
dbMutex.Lock()
e, ok := db.Passwords[connPassword]
if !ok || e == nil || isPasswordExpired(e) {
@@ -1456,9 +1621,7 @@ func handleConn(ctx context.Context, clientConn net.Conn, wgEndpoint string, wgD
}
atomic.AddInt64(&totalBytesToClient, int64(nn))
// Per-password download tracking
if connIsMainPass {
atomic.AddInt64(&mainPassDown, int64(nn))
} else if connPassword != "" {
if connPassword != "" && !connIsMainPass {
dbMutex.Lock()
e, ok := db.Passwords[connPassword]
if !ok || e == nil || isPasswordExpired(e) {
@@ -1482,6 +1645,24 @@ const (
wrapKeyLen = 32
)
var aeadCache sync.Map
func getAEAD(key []byte) (cipher.AEAD, error) {
if len(key) != wrapKeyLen {
return nil, fmt.Errorf("obfs: key must be %d bytes", wrapKeyLen)
}
keyStr := string(key)
if val, ok := aeadCache.Load(keyStr); ok {
return val.(cipher.AEAD), nil
}
aead, err := chacha20poly1305.New(key)
if err != nil {
return nil, err
}
aeadCache.Store(keyStr, aead)
return aead, nil
}
// ==================== RTP Обфускация ====================
type ObfsConfig struct {
@@ -1491,9 +1672,10 @@ type ObfsConfig struct {
}
type ObfsState struct {
mu sync.Mutex
seq uint16
ts uint32
mu sync.Mutex
initSeq uint16
initTs uint32
count uint64
}
func NewObfsConfig() *ObfsConfig {
@@ -1510,8 +1692,9 @@ func NewObfsState() *ObfsState {
var buf [6]byte
rand.Read(buf[:])
return &ObfsState{
seq: binary.BigEndian.Uint16(buf[0:2]),
ts: binary.BigEndian.Uint32(buf[2:6]),
initSeq: binary.BigEndian.Uint16(buf[0:2]),
initTs: binary.BigEndian.Uint32(buf[2:6]),
count: 0,
}
}
@@ -1531,12 +1714,13 @@ func obfsWrapPacket(key, payload []byte, cfg *ObfsConfig, state *ObfsState) ([]b
return nil, errors.New("obfs: empty payload")
}
state.mu.Lock()
seq := state.seq
ts := state.ts
state.seq++
state.ts += 960
c := state.count
state.count++
state.mu.Unlock()
seq := state.initSeq + uint16(c)
ts := state.initTs + uint32(c)*960 + uint32(c>>16)
nonce := obfsBuildNonce(cfg.SSRC, seq, ts)
padRand := 0
if cfg.PaddingMax > 0 {
@@ -1554,7 +1738,7 @@ func obfsWrapPacket(key, payload []byte, cfg *ObfsConfig, state *ObfsState) ([]b
binary.BigEndian.PutUint32(out[4:8], ts)
binary.BigEndian.PutUint32(out[8:12], cfg.SSRC)
aead, err := chacha20poly1305.New(key)
aead, err := getAEAD(key)
if err != nil {
return nil, fmt.Errorf("obfs: cipher init: %w", err)
}
@@ -1597,7 +1781,7 @@ func obfsUnwrapPacket(key, wire, dst []byte) (int, error) {
return 0, errors.New("obfs: dst buffer too small")
}
nonce := obfsBuildNonce(ssrc, seq, ts)
aead, err := chacha20poly1305.New(key)
aead, err := getAEAD(key)
if err != nil {
return 0, fmt.Errorf("obfs: cipher init: %w", err)
}