Browse Source

Merge pull request #105 from alexmac6574/impr-cap

feat: Improve stability and performance for automatic captcha solver, fallback to manual captcha
pull/120/head v1.5.0
cacggghp 2 months ago
committed by GitHub
parent
commit
72f051ed2c
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 28
      .github/workflows/issues.yml
  2. 20
      .github/workflows/release.yml
  3. 116
      README.md
  4. 1114
      client/main.go
  5. 578
      client/manual_captcha.go
  6. 189
      client/namegen.go
  7. 105
      client/profiles.go
  8. 20
      go.mod
  9. 44
      go.sum
  10. 10
      server/main.go

28
.github/workflows/issues.yml

@ -0,0 +1,28 @@
name: "Close Stale Issues"
on:
schedule:
- cron: "0 0 * * 3"
workflow_dispatch:
jobs:
stale:
permissions:
issues: write
runs-on: ubuntu-latest
steps:
- uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
stale-issue-message: >
Привет! 👋
<br><br>
Данный issue давно не обновлялся. Если в течение недели здесь не будет больше активности, issue будет закрыт.
<br><br>
Спасибо!
close-issue-message: "Issue был закрыт из-за отсутствия активности."
days-before-stale: 120
days-before-close: 7
operations-per-run: 1000
ascending: true
enable-statistics: true
stale-issue-label: "Stale"

20
.github/workflows/release.yml

@ -43,7 +43,7 @@ jobs:
cgo: 0
- goos: linux
goarch: riscv64
cgo: 0
cgo: 0
- goos: darwin
goarch: amd64
cgo: 0
@ -135,7 +135,7 @@ jobs:
go build -ldflags "-s -w -checklinkname=0" -trimpath \
-o "dist/client-${GOOS}-${GOARCH}${EXT}" \
./client
GOOS=$GOOS GOARCH=$GOARCH GOARM=$GOARM GOMIPS=$GOMIPS \
go build -ldflags "-s -w -checklinkname=0" -trimpath \
-o "dist/server-${GOOS}-${GOARCH}${EXT}" \
@ -243,21 +243,21 @@ jobs:
{
if [ -n "$BREAKING" ]; then
echo "### Несовместимые изменения"
echo "### Breaking Changes"
echo
printf '%s\n' "$BREAKING" | sed 's/^/- /'
echo
fi
if [ -n "$FEATURES" ]; then
echo "### Новые функции"
echo "### Features"
echo
printf '%s\n' "$FEATURES" | sed 's/^/- /'
echo
fi
if [ -n "$FIXES" ]; then
echo "### Исправление багов"
echo "### Bug Fixes"
echo
printf '%s\n' "$FIXES" | sed 's/^/- /'
echo
@ -298,10 +298,10 @@ jobs:
uses: actions/checkout@v6
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4
- name: Prepare image name
id: image
@ -311,7 +311,7 @@ jobs:
echo "name=ghcr.io/$(echo "$REPOSITORY" | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_OUTPUT"
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
@ -319,7 +319,7 @@ jobs:
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v5
uses: docker/metadata-action@v6
with:
images: ${{ steps.image.outputs.name }}
tags: |
@ -327,7 +327,7 @@ jobs:
type=raw,value=${{ needs.release.outputs.tag_name }}
- name: Build and push Docker image
uses: docker/build-push-action@v6
uses: docker/build-push-action@v7
with:
context: .
file: ./Dockerfile

116
README.md

File diff suppressed because one or more lines are too long

1114
client/main.go

File diff suppressed because it is too large

578
client/manual_captcha.go

@ -0,0 +1,578 @@
package main
import (
"bytes"
"compress/gzip"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"net"
"net/http"
"net/http/httputil"
neturl "net/url"
"os/exec"
"runtime"
"strings"
"time"
"github.com/bschaatsbergen/dnsdialer"
)
const captchaListenPort = "8765"
type browserCommand struct {
name string
args []string
}
func localCaptchaOrigin() string {
return "http://localhost:" + captchaListenPort
}
func localCaptchaListenAddrs() []string {
return []string{
"127.0.0.1:" + captchaListenPort,
"[::1]:" + captchaListenPort,
}
}
func localCaptchaHosts() []string {
return []string{
"localhost:" + captchaListenPort,
"127.0.0.1:" + captchaListenPort,
"[::1]:" + captchaListenPort,
}
}
func isLocalCaptchaHost(host string) bool {
for _, localHost := range localCaptchaHosts() {
if strings.EqualFold(host, localHost) {
return true
}
}
return false
}
func localCaptchaURLForTarget(targetURL *neturl.URL) string {
localURL := &neturl.URL{
Scheme: "http",
Host: "localhost:" + captchaListenPort,
Path: targetURL.Path,
RawPath: targetURL.RawPath,
RawQuery: targetURL.RawQuery,
}
if localURL.Path == "" {
localURL.Path = "/"
}
return localURL.String()
}
func targetOrigin(targetURL *neturl.URL) string {
return targetURL.Scheme + "://" + targetURL.Host
}
func rewriteProxyHeaderURL(raw string, targetURL *neturl.URL) string {
if raw == "" {
return raw
}
parsed, err := neturl.Parse(raw)
if err != nil {
return raw
}
if parsed.Scheme != "http" || !isLocalCaptchaHost(parsed.Host) {
return raw
}
parsed.Scheme = targetURL.Scheme
parsed.Host = targetURL.Host
return parsed.String()
}
func rewriteProxyRequest(req *http.Request, targetURL *neturl.URL) {
req.URL.Scheme = targetURL.Scheme
req.URL.Host = targetURL.Host
if req.URL.Path == "" {
req.URL.Path = targetURL.Path
}
req.Host = targetURL.Host
req.Header.Del("Accept-Encoding")
for _, headerName := range []string{"Origin", "Referer"} {
if rewritten := rewriteProxyHeaderURL(req.Header.Get(headerName), targetURL); rewritten != "" {
req.Header.Set(headerName, rewritten)
} else {
req.Header.Del(headerName)
}
}
}
func extractSuccessToken(body []byte) string {
var payload struct {
Response struct {
SuccessToken string `json:"success_token"`
} `json:"response"`
}
if err := json.Unmarshal(body, &payload); err != nil {
return ""
}
return payload.Response.SuccessToken
}
func rewriteProxyCookies(header http.Header) {
cookies := (&http.Response{Header: header}).Cookies()
if len(cookies) == 0 {
return
}
header.Del("Set-Cookie")
for _, cookie := range cookies {
cookie.Domain = ""
cookie.Secure = false
cookie.Partitioned = false
if cookie.SameSite == http.SameSiteNoneMode || cookie.SameSite == http.SameSiteStrictMode {
cookie.SameSite = http.SameSiteLaxMode
}
header.Add("Set-Cookie", cookie.String())
}
}
func rewriteCaptchaHTML(html string, targetURL *neturl.URL) string {
localOrigin := localCaptchaOrigin()
upstreamOrigin := targetOrigin(targetURL)
html = strings.ReplaceAll(html, upstreamOrigin, localOrigin)
script := fmt.Sprintf(`
<script>
(function() {
var localOrigin = %q;
var upstreamOrigin = %q;
function rewriteUrl(urlStr) {
if (!urlStr || typeof urlStr !== 'string') return urlStr;
if (urlStr.indexOf(localOrigin) === 0) return urlStr;
if (urlStr.indexOf(upstreamOrigin) === 0) return localOrigin + urlStr.slice(upstreamOrigin.length);
if (urlStr.indexOf('//') === 0) {
return '/generic_proxy?proxy_url=' + encodeURIComponent(window.location.protocol + urlStr);
}
if (urlStr.indexOf('http://') === 0 || urlStr.indexOf('https://') === 0) {
return '/generic_proxy?proxy_url=' + encodeURIComponent(urlStr);
}
return urlStr;
}
function rewriteElementAttr(el, attr) {
if (!el || !el.getAttribute) return;
var value = el.getAttribute(attr);
if (!value) return;
var rewritten = rewriteUrl(value);
if (rewritten !== value) {
el.setAttribute(attr, rewritten);
}
}
function rewriteDocument(root) {
if (!root || !root.querySelectorAll) return;
root.querySelectorAll('[href]').forEach(function(el) { rewriteElementAttr(el, 'href'); });
root.querySelectorAll('[src]').forEach(function(el) { rewriteElementAttr(el, 'src'); });
root.querySelectorAll('form[action]').forEach(function(el) { rewriteElementAttr(el, 'action'); });
}
function handleSuccessToken(token) {
if (!token) return;
fetch('/local-captcha-result', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'token=' + encodeURIComponent(token)
}).then(function() {
document.body.innerHTML = '<h2 style="text-align:center;margin-top:20vh">Done! You can close the page.</h2>';
setTimeout(function() { window.close(); }, 300);
}).catch(function() {});
}
var origOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function() {
if (arguments[1] && typeof arguments[1] === 'string') {
this._origUrl = arguments[1];
arguments[1] = rewriteUrl(arguments[1]);
}
return origOpen.apply(this, arguments);
};
var origSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.send = function() {
var xhr = this;
if (this._origUrl && this._origUrl.indexOf('captchaNotRobot.check') !== -1) {
xhr.addEventListener('load', function() {
try {
var data = JSON.parse(xhr.responseText);
if (data.response && data.response.success_token) {
handleSuccessToken(data.response.success_token);
}
} catch (e) {}
});
}
return origSend.apply(this, arguments);
};
var origFetch = window.fetch;
if (origFetch) {
window.fetch = function() {
var url = arguments[0];
var isObj = (typeof url === 'object' && url && url.url);
var urlStr = isObj ? url.url : url;
var origUrlStr = urlStr;
if (typeof urlStr === 'string') {
urlStr = rewriteUrl(urlStr);
arguments[0] = urlStr;
}
var p = origFetch.apply(this, arguments);
if (typeof origUrlStr === 'string' && origUrlStr.indexOf('captchaNotRobot.check') !== -1) {
p.then(function(response) {
return response.clone().json();
}).then(function(data) {
if (data.response && data.response.success_token) {
handleSuccessToken(data.response.success_token);
}
}).catch(function() {});
}
return p;
};
}
document.addEventListener('submit', function(event) {
if (event.target && event.target.action) {
event.target.action = rewriteUrl(event.target.action);
}
}, true);
document.addEventListener('click', function(event) {
var target = event.target && event.target.closest ? event.target.closest('a[href]') : null;
if (target && target.href) {
target.href = rewriteUrl(target.href);
}
}, true);
var origFormSubmit = HTMLFormElement.prototype.submit;
HTMLFormElement.prototype.submit = function() {
if (this.action) {
this.action = rewriteUrl(this.action);
}
return origFormSubmit.apply(this, arguments);
};
var origWindowOpen = window.open;
if (origWindowOpen) {
window.open = function(url) {
if (typeof url === 'string') {
arguments[0] = rewriteUrl(url);
}
return origWindowOpen.apply(this, arguments);
};
}
rewriteDocument(document);
if (document.documentElement && window.MutationObserver) {
new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.type === 'attributes' && mutation.target) {
rewriteElementAttr(mutation.target, mutation.attributeName);
return;
}
mutation.addedNodes.forEach(function(node) {
if (node.nodeType === 1) {
rewriteDocument(node);
}
});
});
}).observe(document.documentElement, {
subtree: true,
childList: true,
attributes: true,
attributeFilter: ['href', 'src', 'action']
});
}
})();
</script>
`, localOrigin, upstreamOrigin)
switch {
case strings.Contains(html, "</head>"):
return strings.Replace(html, "</head>", script+"</head>", 1)
case strings.Contains(html, "</body>"):
return strings.Replace(html, "</body>", script+"</body>", 1)
default:
return html + script
}
}
func newCaptchaProxyTransport(dialer *dnsdialer.Dialer) *http.Transport {
transport := &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
ForceAttemptHTTP2: true,
}
if dialer != nil {
transport.DialContext = dialer.DialContext
}
return transport
}
func startCaptchaServer(srv *http.Server, logPrefix string) error {
var listenErrs []string
var listening bool
for _, addr := range localCaptchaListenAddrs() {
listener, err := net.Listen("tcp", addr)
if err != nil {
listenErrs = append(listenErrs, fmt.Sprintf("%s (%v)", addr, err))
continue
}
listening = true
go func(listener net.Listener) {
if err := srv.Serve(listener); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Printf("%s: %s", logPrefix, err)
}
}(listener)
}
if listening {
return nil
}
return fmt.Errorf("captcha listeners failed: %s", strings.Join(listenErrs, "; "))
}
// runCaptchaServerAndWait triggers the browser, and waiting gracefully for the solution token.
func runCaptchaServerAndWait(handler http.Handler, captchaURL string, keyCh <-chan string, logPrefix string) (string, error) {
srv := &http.Server{Handler: handler}
if err := startCaptchaServer(srv, logPrefix); err != nil {
return "", err
}
fmt.Println("\n==============================================")
fmt.Println("ACTION REQUIRED: MANUAL CAPTCHA SOLVING NEEDED")
fmt.Println("Open this URL in your browser: " + captchaURL)
fmt.Println("==============================================")
fmt.Println()
openBrowser(captchaURL)
key := <-keyCh
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
return "", err
}
return key, nil
}
// notifyKey pushes the key string to the given channel without blocking
func notifyKey(keyCh chan<- string, key string) {
if key != "" {
select {
case keyCh <- key:
default:
}
}
}
func solveCaptchaViaHTTP(captchaImg string) (string, error) {
keyCh := make(chan string, 1)
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
_, _ = fmt.Fprintf(w, `<!DOCTYPE html>
<html><head>
<meta name="viewport" content="width=device-width,initial-scale=1">
<style>body{font-family:sans-serif;text-align:center;padding:20px}
img{max-width:100%%;margin:16px 0}
input{font-size:24px;padding:12px;width:80%%;box-sizing:border-box}
button{font-size:24px;padding:12px 32px;margin-top:12px;cursor:pointer}</style>
</head><body>
<h2>Solve the Captcha</h2>
<img src="%s" alt="captcha"/>
<form onsubmit="fetch('/solve?key='+encodeURIComponent(document.getElementById('k').value)).then(()=>{document.body.innerHTML='<h2>Done!</h2>';setTimeout(function(){window.close();}, 300);});return false;">
<br><input id="k" type="text" autofocus placeholder="Text from image"/>
<br><button type="submit">Submit</button>
</form></body></html>`, captchaImg)
})
mux.HandleFunc("/solve", func(w http.ResponseWriter, r *http.Request) {
notifyKey(keyCh, r.URL.Query().Get("key"))
w.Header().Set("Content-Type", "text/html; charset=utf-8")
_, _ = fmt.Fprint(w, `<!DOCTYPE html><html><body><h2>Done!</h2></body></html>`)
})
return runCaptchaServerAndWait(mux, localCaptchaOrigin(), keyCh, "captcha HTTP server error")
}
func solveCaptchaViaProxy(redirectURI string, dialer *dnsdialer.Dialer) (string, error) {
keyCh := make(chan string, 1)
targetURL, err := neturl.Parse(redirectURI)
if err != nil {
return "", fmt.Errorf("invalid redirect URI: %v", err)
}
transport := newCaptchaProxyTransport(dialer)
proxy := &httputil.ReverseProxy{
Transport: transport,
Rewrite: func(req *httputil.ProxyRequest) {
rewriteProxyRequest(req.Out, targetURL)
},
ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) {
log.Printf("captcha proxy error for %s: %v", r.URL.String(), err)
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusBadGateway)
_, _ = fmt.Fprintf(w, `<!DOCTYPE html><html><body style="font-family:sans-serif;padding:20px"><h2>Captcha proxy error</h2><p>%v</p></body></html>`, err)
},
ModifyResponse: func(res *http.Response) error {
rewriteProxyCookies(res.Header)
if res.StatusCode >= 300 && res.StatusCode < 400 {
if loc := res.Header.Get("Location"); loc != "" {
if strings.HasPrefix(loc, "/") {
res.Header.Set("Location", loc)
} else if strings.HasPrefix(loc, targetOrigin(targetURL)) {
res.Header.Set("Location", strings.Replace(loc, targetOrigin(targetURL), localCaptchaOrigin(), 1))
}
}
}
contentType := res.Header.Get("Content-Type")
shouldInspectBody := strings.Contains(contentType, "text/html") || strings.Contains(res.Request.URL.Path, "captchaNotRobot.check")
if !shouldInspectBody {
return nil
}
reader := res.Body
if res.Header.Get("Content-Encoding") == "gzip" {
gzReader, err := gzip.NewReader(res.Body)
if err == nil {
reader = gzReader
defer func() {
if err := gzReader.Close(); err != nil {
log.Printf("failed to close gzip reader: %v", err)
}
}()
}
}
bodyBytes, err := io.ReadAll(reader)
if err != nil {
return err
}
if err := res.Body.Close(); err != nil {
return err
}
if strings.Contains(res.Request.URL.Path, "captchaNotRobot.check") {
notifyKey(keyCh, extractSuccessToken(bodyBytes))
}
if strings.Contains(contentType, "text/html") {
for _, headerName := range []string{
"Content-Security-Policy",
"Content-Security-Policy-Report-Only",
"X-Content-Security-Policy",
"X-WebKit-CSP",
"Cross-Origin-Opener-Policy",
"Cross-Origin-Embedder-Policy",
"Cross-Origin-Resource-Policy",
"X-Frame-Options",
} {
res.Header.Del(headerName)
}
bodyBytes = []byte(rewriteCaptchaHTML(string(bodyBytes), targetURL))
res.Header.Del("Content-Encoding")
}
res.Body = io.NopCloser(bytes.NewReader(bodyBytes))
res.ContentLength = int64(len(bodyBytes))
res.Header.Set("Content-Length", fmt.Sprint(len(bodyBytes)))
return nil
},
}
mux := http.NewServeMux()
mux.HandleFunc("/local-captcha-result", func(w http.ResponseWriter, r *http.Request) {
notifyKey(keyCh, r.FormValue("token")) // r.FormValue automatically parses the form
w.Header().Set("Access-Control-Allow-Origin", "*")
_, _ = fmt.Fprint(w, "ok")
})
mux.HandleFunc("/generic_proxy", func(w http.ResponseWriter, r *http.Request) {
targetAuthUrl := r.URL.Query().Get("proxy_url")
targetParsed, err := neturl.Parse(targetAuthUrl)
if err != nil || targetParsed.Host == "" {
http.Error(w, "Bad URL", http.StatusBadRequest)
return
}
genericReverse := &httputil.ReverseProxy{
Transport: transport,
Rewrite: func(req *httputil.ProxyRequest) {
req.Out.URL.Path = targetParsed.Path
req.Out.URL.RawQuery = targetParsed.RawQuery
rewriteProxyRequest(req.Out, targetParsed)
},
}
genericReverse.ServeHTTP(w, r)
})
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/" && targetURL.Path != "" && targetURL.Path != "/" && r.URL.RawQuery == "" {
http.Redirect(w, r, localCaptchaURLForTarget(targetURL), http.StatusTemporaryRedirect)
return
}
proxy.ServeHTTP(w, r)
})
return runCaptchaServerAndWait(mux, localCaptchaURLForTarget(targetURL), keyCh, "proxy HTTP server error")
}
func openBrowser(url string) {
for _, cmd := range browserOpenCommands(runtime.GOOS, url) {
if err := exec.Command(cmd.name, cmd.args...).Start(); err == nil {
return
}
}
}
func browserOpenCommands(goos string, url string) []browserCommand {
switch goos {
case "windows":
return []browserCommand{{name: "cmd", args: []string{"/c", "start", url}}}
case "darwin":
return []browserCommand{{name: "open", args: []string{url}}}
case "linux":
return []browserCommand{
{name: "xdg-open", args: []string{url}},
{name: "gio", args: []string{"open", url}},
}
case "android":
return []browserCommand{
{name: "termux-open-url", args: []string{url}},
{name: "/system/bin/am", args: []string{"start", "-a", "android.intent.action.VIEW", "-d", url}},
{name: "am", args: []string{"start", "-a", "android.intent.action.VIEW", "-d", url}},
{name: "xdg-open", args: []string{url}},
}
case "ios":
return []browserCommand{
{name: "open", args: []string{url}},
{name: "uiopen", args: []string{url}},
}
}
return nil
}

189
client/namegen.go

@ -3,40 +3,185 @@ package main
import (
"fmt"
"math/rand"
"strings"
)
// firstNames contains Russian first names. Add or remove names as needed.
var firstNames = []string{
"Александр", "Дмитрий", "Максим", "Сергей", "Андрей", "Алексей", "Артём", "Илья",
"Кирилл", "Михаил", "Никита", "Матвей", "Роман", "Егор", "Арсений", "Иван",
"Денис", "Даниил", "Тимофей", "Владислав", "Игорь", "Павел", "Руслан", "Марк",
"Анна", "Мария", "Елена", "Дарья", "Анастасия", "Екатерина", "Виктория", "Ольга",
"Наталья", "Юлия", "Татьяна", "Светлана", "Ирина", "Ксения", "Алина", "Елизавета",
var maleFirstNames = []string{
"Александр",
"Алексей",
"Андрей",
"Антон",
"Арсений",
"Артур",
"Артём",
"Богдан",
"Валерий",
"Василий",
"Виктор",
"Владислав",
"Глеб",
"Григорий",
"Даниил",
"Денис",
"Дмитрий",
"Евгений",
"Егор",
"Иван",
"Игорь",
"Илья",
"Кирилл",
"Леонид",
"Максим",
"Марк",
"Матвей",
"Михаил",
"Никита",
"Николай",
"Олег",
"Павел",
"Пётр",
"Роман",
"Руслан",
"Сергей",
"Станислав",
"Тимофей",
"Фёдор",
}
var femaleFirstNames = []string{
"Алина",
"Алёна",
"Анастасия",
"Ангелина",
"Анна",
"Вера",
"Вероника",
"Виктория",
"Дарья",
"Ева",
"Екатерина",
"Елена",
"Елизавета",
"Ирина",
"Кира",
"Кристина",
"Ксения",
"Любовь",
"Маргарита",
"Марина",
"Мария",
"Милана",
"Надежда",
"Наталья",
"Ольга",
"Полина",
"Светлана",
"София",
"Татьяна",
"Юлия",
"Яна",
}
// lastNames contains Russian last names. Add or remove names as needed.
var lastNames = []string{
"Иванов", "Смирнов", "Кузнецов", "Попов", "Васильев", "Петров", "Соколов", "Михайлов",
"Новиков", "Федоров", "Морозов", "Волков", "Алексеев", "Лебедев", "Семенов", "Егоров",
"Павлов", "Козлов", "Степанов", "Николаев", "Орлов", "Андреев", "Макаров", "Никитин",
"Захаров", "Зайцев", "Соловьев", "Борисов", "Яковлев", "Григорьев", "Романов", "Воробьев",
"Алексеев",
"Андреев",
"Антонов",
"Баранов",
"Белов",
"Белый",
"Бельский",
"Беляев",
"Борисов",
"Васильев",
"Великий",
"Волков",
"Воробьёв",
"Григорьев",
"Давыдов",
"Егоров",
"Жуков",
"Зайцев",
"Захаров",
"Иванов",
"Калинин",
"Ковалёв",
"Козлов",
"Комаров",
"Крамской",
"Кузнецов",
"Кузьмин",
"Лебедев",
"Макаров",
"Медведев",
"Михайлов",
"Морозов",
"Никитин",
"Николаев",
"Новиков",
"Орлов",
"Островский",
"Павлов",
"Петров",
"Покровский",
"Попов",
"Раевский",
"Романов",
"Семёнов",
"Сергеев",
"Смирнов",
"Соколов",
"Соловьёв",
"Степанов",
"Тарасов",
"Титов",
"Толстой",
"Трубецкой",
"Филиппов",
"Фролов",
"Фёдоров",
"Чайковский",
"Черный",
"Яковлев",
}
// convertToFemaleSurname handles Russian suffix rules
func convertToFemaleSurname(surname string) string {
// Handle adjective-style surnames:
if strings.HasSuffix(surname, "ий") || strings.HasSuffix(surname, "ый") || strings.HasSuffix(surname, "ой") {
return surname[:len(surname)-4] + "ая"
}
// Handle standard possessive surnames:
if strings.HasSuffix(surname, "ов") || strings.HasSuffix(surname, "ев") ||
strings.HasSuffix(surname, "ин") || strings.HasSuffix(surname, "ын") ||
strings.HasSuffix(surname, "ёв") {
return surname + "а"
}
// Foreign or unchangeable
return surname
}
// generateName generates a random Russian name.
// 30% chance to generate only first name, 70% chance first + last name.
// For female names (ending in 'а' or 'я'), adds 'а' to the last name.
func generateName() string {
// Decide gender first
isFemale := rand.Intn(2) == 0
var fn string
if isFemale {
fn = femaleFirstNames[rand.Intn(len(femaleFirstNames))]
} else {
fn = maleFirstNames[rand.Intn(len(maleFirstNames))]
}
// 70% chance to have a last name
if rand.Float32() < 0.3 {
return firstNames[rand.Intn(len(firstNames))]
return fn
}
fn := firstNames[rand.Intn(len(firstNames))]
ln := lastNames[rand.Intn(len(lastNames))]
// add 'a' to the last name for females
lastChar := fn[len(fn)-2:] // 2 bytes for cyrillic
if lastChar == "а" || lastChar == "я" {
return fmt.Sprintf("%s %sа", fn, ln)
if isFemale {
ln = convertToFemaleSurname(ln)
}
return fmt.Sprintf("%s %s", fn, ln)
}

105
client/profiles.go

@ -5,51 +5,78 @@ import (
)
type Profile struct {
UserAgent string
UserAgent string
SecChUa string
SecChUaMobile string
SecChUaPlatform string
}
// profiles contains realistic user-agent strings for different browsers and platforms.
// Add or remove profiles as needed.
var profiles = []Profile{
{"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"},
{"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36"},
{"Mozilla/5.0 (Windows NT 11.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"},
{"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36"},
{"Mozilla/5.0 (Windows NT 11.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36"},
// profiles contain paired User-Agent and Client Hints strings to harden bot detection.
var profile = []Profile{
// Windows Chrome
{
UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36",
SecChUa: `"Chromium";v="146", "Not-A.Brand";v="24", "Google Chrome";v="146"`,
SecChUaMobile: "?0",
SecChUaPlatform: `"Windows"`,
},
{
UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36",
SecChUa: `"Chromium";v="145", "Not-A.Brand";v="99", "Google Chrome";v="145"`,
SecChUaMobile: "?0",
SecChUaPlatform: `"Windows"`,
},
{
UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36",
SecChUa: `"Chromium";v="144", "Not-A.Brand";v="8", "Google Chrome";v="144"`,
SecChUaMobile: "?0",
SecChUaPlatform: `"Windows"`,
},
{"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 Edg/146.0.0.0"},
{"Mozilla/5.0 (Windows NT 11.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36 Edg/145.0.0.0"},
{"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36 Edg/144.0.0.0"},
{"Mozilla/5.0 (Windows NT 11.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 Edg/146.0.0.0"},
// Windows Edge
{
UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 Edg/146.0.0.0",
SecChUa: `"Chromium";v="146", "Not-A.Brand";v="24", "Microsoft Edge";v="146"`,
SecChUaMobile: "?0",
SecChUaPlatform: `"Windows"`,
},
{
UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36 Edg/145.0.0.0",
SecChUa: `"Chromium";v="145", "Not-A.Brand";v="99", "Microsoft Edge";v="145"`,
SecChUaMobile: "?0",
SecChUaPlatform: `"Windows"`,
},
{"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 YaBrowser/24.1.0.0 Yowser/2.5 Safari/537.36"},
{"Mozilla/5.0 (Windows NT 11.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 YaBrowser/24.1.2.0 Yowser/2.5 Safari/537.36"},
{"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 YaBrowser/23.12.0.0 Yowser/2.5 Safari/537.36"},
// macOS Chrome
{
UserAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36",
SecChUa: `"Chromium";v="146", "Not-A.Brand";v="24", "Google Chrome";v="146"`,
SecChUaMobile: "?0",
SecChUaPlatform: `"macOS"`,
},
{
UserAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36",
SecChUa: `"Chromium";v="145", "Not-A.Brand";v="99", "Google Chrome";v="145"`,
SecChUaMobile: "?0",
SecChUaPlatform: `"macOS"`,
},
{"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 OPR/112.0.0.0"},
{"Mozilla/5.0 (Windows NT 11.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36 OPR/111.0.0.0"},
{"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"},
{"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36"},
{"Mozilla/5.0 (Macintosh; Intel Mac OS X 11_0_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"},
{"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 Edg/146.0.0.0"},
{"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"},
{"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36"},
{"Mozilla/5.0 (X11; Ubuntu; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36"},
{"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"},
{"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"},
{"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"},
{"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"},
{"Mozilla/5.0 (Windows NT 11.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"},
{"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36"},
{"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36"},
{"Mozilla/5.0 (Windows NT 11.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36"},
{"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36"},
// Linux Chrome
{
UserAgent: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36",
SecChUa: `"Chromium";v="146", "Not-A.Brand";v="24", "Google Chrome";v="146"`,
SecChUaMobile: "?0",
SecChUaPlatform: `"Linux"`,
},
{
UserAgent: "Mozilla/5.0 (X11; Ubuntu; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36",
SecChUa: `"Chromium";v="144", "Not-A.Brand";v="8", "Google Chrome";v="144"`,
SecChUaMobile: "?0",
SecChUaPlatform: `"Linux"`,
},
}
// getRandomProfile returns a random user-agent profile.
// getRandomProfile returns a paired User-Agent and Client Hints profile.
func getRandomProfile() Profile {
return profiles[rand.Intn(len(profiles))]
return profile[rand.Intn(len(profile))]
}

20
go.mod

@ -3,26 +3,38 @@ module github.com/cacggghp/vk-turn-proxy
go 1.25.5
require (
github.com/bogdanfinn/fhttp v0.6.8
github.com/bogdanfinn/tls-client v1.14.0
github.com/bschaatsbergen/dnsdialer v0.0.0-20251225104348-3e7610e8ea45
github.com/cbeuw/connutil v1.0.1
github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.3
github.com/pion/dtls/v3 v3.0.11
github.com/pion/logging v0.2.4
github.com/pion/transport/v4 v4.0.1
github.com/pion/turn/v5 v5.0.2
)
require (
github.com/andybalholm/brotli v1.2.0 // indirect
github.com/bdandy/go-errors v1.2.2 // indirect
github.com/bdandy/go-socks4 v1.2.3 // indirect
github.com/bogdanfinn/quic-go-utls v1.0.9-utls // indirect
github.com/bogdanfinn/utls v1.7.7-barnius // indirect
github.com/bogdanfinn/websocket v1.5.5-barnius // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/klauspost/compress v1.18.2 // indirect
github.com/miekg/dns v1.1.69 // indirect
github.com/pion/randutil v0.1.0 // indirect
github.com/pion/stun/v3 v3.1.1 // indirect
github.com/pion/transport/v4 v4.0.1 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/tam7t/hpkp v0.0.0-20160821193359-2b70b4024ed5 // indirect
github.com/wlynxg/anet v0.0.5 // indirect
golang.org/x/crypto v0.47.0 // indirect
golang.org/x/mod v0.30.0 // indirect
golang.org/x/mod v0.31.0 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/sync v0.18.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.40.0 // indirect
golang.org/x/tools v0.39.0 // indirect
golang.org/x/text v0.33.0 // indirect
golang.org/x/tools v0.40.0 // indirect
)

44
go.sum

@ -1,3 +1,19 @@
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/bdandy/go-errors v1.2.2 h1:WdFv/oukjTJCLa79UfkGmwX7ZxONAihKu4V0mLIs11Q=
github.com/bdandy/go-errors v1.2.2/go.mod h1:NkYHl4Fey9oRRdbB1CoC6e84tuqQHiqrOcZpqFEkBxM=
github.com/bdandy/go-socks4 v1.2.3 h1:Q6Y2heY1GRjCtHbmlKfnwrKVU/k81LS8mRGLRlmDlic=
github.com/bdandy/go-socks4 v1.2.3/go.mod h1:98kiVFgpdogR8aIGLWLvjDVZ8XcKPsSI/ypGrO+bqHI=
github.com/bogdanfinn/fhttp v0.6.8 h1:LiQyHOY3i0QoxxNB7nq27/nGNNbtPj0fuBPozhR7Ws4=
github.com/bogdanfinn/fhttp v0.6.8/go.mod h1:A+EKDzMx2hb4IUbMx4TlkoHnaJEiLl8r/1Ss1Y+5e5M=
github.com/bogdanfinn/quic-go-utls v1.0.9-utls h1:tV6eDEiRbRCcepALSzxR94JUVD3N3ACIiRLgyc2Ep8s=
github.com/bogdanfinn/quic-go-utls v1.0.9-utls/go.mod h1:aHph9B9H9yPOt5xnhWKSOum27DJAqpiHzwX+gjvaXcg=
github.com/bogdanfinn/tls-client v1.14.0 h1:vyk7Cn4BIvLAGVuMfb0tP22OqogfO1lYamquQNEZU1A=
github.com/bogdanfinn/tls-client v1.14.0/go.mod h1:LsU6mXVn8MOFDwTkyRfI7V1BZM1p0wf2ZfZsICW/1fM=
github.com/bogdanfinn/utls v1.7.7-barnius h1:OuJ497cc7F3yKNVHRsYPQdGggmk5x6+V5ZlrCR7fOLU=
github.com/bogdanfinn/utls v1.7.7-barnius/go.mod h1:aAK1VZQlpKZClF1WEQeq6kyclbkPq4hz6xTbB5xSlmg=
github.com/bogdanfinn/websocket v1.5.5-barnius h1:bY+qnxpai1qe7Jmjx+Sds/cmOSpuuLoR8x61rWltjOI=
github.com/bogdanfinn/websocket v1.5.5-barnius/go.mod h1:gvvEw6pTKHb7yOiFvIfAFTStQWyrm25BMVCTj5wRSsI=
github.com/bschaatsbergen/dnsdialer v0.0.0-20251225104348-3e7610e8ea45 h1:0b2i5TvZm8FVcuHP1288k+DEu1XM26DtRjcidOxpGXs=
github.com/bschaatsbergen/dnsdialer v0.0.0-20251225104348-3e7610e8ea45/go.mod h1:NU7MdmhQD8Ounc0760w90fL6nxI2lxjlnIaN6qWzNIU=
github.com/cbeuw/connutil v1.0.1 h1:LWuNYjwm7JEDYG/ISAO1TfU4G+q2dA5NhR97eq2roCA=
@ -12,6 +28,8 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/miekg/dns v1.1.69 h1:Kb7Y/1Jo+SG+a2GtfoFUfDkG//csdRPwRLkCsxDG9Sc=
github.com/miekg/dns v1.1.69/go.mod h1:7OyjD9nEba5OkqQ/hB4fy3PIoxafSZJtducccIelz3g=
github.com/pion/dtls/v3 v3.0.11 h1:zqn8YhoAU7d9whsWLhNiQlbB8QdpJj8XQVSc5ImUons=
@ -28,26 +46,40 @@ github.com/pion/turn/v5 v5.0.2 h1:GHlDk+fiegz+yibb3ch+tK+iPFokoVWiM+aVJakySqA=
github.com/pion/turn/v5 v5.0.2/go.mod h1:cumcsSEF2ytAtDhDwkYgYhv1uJ3AOP7a4pFt0NL/snY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tam7t/hpkp v0.0.0-20160821193359-2b70b4024ed5 h1:YqAladjX7xpA6BM04leXMWAEjS0mTZ5kUU9KRBriQJc=
github.com/tam7t/hpkp v0.0.0-20160821193359-2b70b4024ed5/go.mod h1:2JjD2zLQYH5HO74y5+aE3remJQvl6q4Sn6aWA2wD1Ng=
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
golang.org/x/net v0.0.0-20211104170005-ce137452f963/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4=
golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda h1:i/Q+bfisr7gq6feoJnS/DlpdwEL4ihp41fvRiM3Ork0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=

10
server/main.go

@ -17,6 +17,8 @@ import (
"github.com/pion/dtls/v3/pkg/crypto/selfsign"
)
const idleTimeout = 2 * time.Minute
func main() {
listen := flag.String("listen", "0.0.0.0:56000", "listen on ip:port")
connect := flag.String("connect", "", "connect to ip:port")
@ -150,7 +152,7 @@ func main() {
return
default:
}
if err1 := conn.SetReadDeadline(time.Now().Add(time.Minute * 30)); err1 != nil {
if err1 := conn.SetReadDeadline(time.Now().Add(idleTimeout)); err1 != nil {
log.Printf("Failed: %s", err1)
return
}
@ -160,7 +162,7 @@ func main() {
return
}
if err1 := serverConn.SetWriteDeadline(time.Now().Add(time.Minute * 30)); err1 != nil {
if err1 := serverConn.SetWriteDeadline(time.Now().Add(idleTimeout)); err1 != nil {
log.Printf("Failed: %s", err1)
return
}
@ -181,7 +183,7 @@ func main() {
return
default:
}
if err1 := serverConn.SetReadDeadline(time.Now().Add(time.Minute * 30)); err1 != nil {
if err1 := serverConn.SetReadDeadline(time.Now().Add(idleTimeout)); err1 != nil {
log.Printf("Failed: %s", err1)
return
}
@ -191,7 +193,7 @@ func main() {
return
}
if err1 := conn.SetWriteDeadline(time.Now().Add(time.Minute * 30)); err1 != nil {
if err1 := conn.SetWriteDeadline(time.Now().Add(idleTimeout)); err1 != nil {
log.Printf("Failed: %s", err1)
return
}

Loading…
Cancel
Save