diff --git a/README.md b/README.md index a764a23..877f624 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # vk-turn-proxy -Проброс трафика WireGuard через TURN сервера VK звонков. Пакеты шифруются DTLS 1.2, затем параллельными потоками через TCP или UDP отправляются на TURN сервер по протоколу STUN ChannelData. Оттуда по UDP отправляются на ваш сервер, где расшифровываются и передаются в WireGuard. Логин/пароль от TURN генерируются из ссылки на звонок. +Проброс трафика WireGuard через TURN сервера VK звонков. Пакеты шифруются DTLS 1.2, затем параллельными потоками через TCP или UDP отправляются на TURN сервер по протоколу STUN Datachannel. Оттуда по UDP отправляются на ваш сервер, где расшифровываются и передаются в WireGuard. Логин/пароль от TURN генерируются из ссылки на звонок. Только для учебных целей! ## Настройка @@ -40,4 +40,4 @@ Если не работает TCP, попробуйте добавить флаг `-udp`. -Добавьте флаг `-n 1` для более стабильного подключения в 1 поток (ограничение 5 Мбит/с) +Добавьте флаг `-n 1` для более стабильного подключения в 1 поток (ограничение 5 Мбит/с) \ No newline at end of file diff --git a/client/main.go b/client/main.go new file mode 100644 index 0000000..2884efa --- /dev/null +++ b/client/main.go @@ -0,0 +1,546 @@ +// SPDX-FileCopyrightText: 2023 The Pion community +// SPDX-License-Identifier: MIT + +package main + +import ( + "bytes" + "context" + "crypto/tls" + "encoding/json" + "flag" + "fmt" + "io" + "log" + "net" + "net/http" + "os" + "os/signal" + "sync" + "sync/atomic" + "syscall" + "time" + + "github.com/cbeuw/connutil" + "github.com/google/uuid" + "github.com/pion/dtls/v3" + "github.com/pion/dtls/v3/pkg/crypto/selfsign" + "github.com/pion/logging" + "github.com/pion/turn/v4" +) + +func doRequest(data string, url string) (resp map[string]interface{}, err error) { + client := &http.Client{ + Timeout: 20 * time.Second, + Transport: &http.Transport{ + MaxIdleConns: 100, + MaxIdleConnsPerHost: 100, + IdleConnTimeout: 90 * time.Second, + }, + } + req, err := http.NewRequest("POST", url, bytes.NewBuffer([]byte(data))) + if err != nil { + return nil, err + } + + req.Header.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:144.0) Gecko/20100101 Firefox/144.0") + req.Header.Add("Content-Type", "application/x-www-form-urlencoded") + + httpResp, err := client.Do(req) + if err != nil { + return nil, err + } + defer httpResp.Body.Close() + + body, err := io.ReadAll(httpResp.Body) + if err != nil { + return nil, err + } + + err = json.Unmarshal(body, &resp) + if err != nil { + return nil, err + } + + return resp, nil +} + +func getCreds(link string) (string, string, string, error) { + var resp map[string]interface{} + defer func() { + if r := recover(); r != nil { + log.Panicf("get TURN creds error: %v\n\n", resp) + } + }() + + data := "client_secret=QbYic1K3lEV5kTGiqlq2&client_id=6287487&scopes=audio_anonymous%2Cvideo_anonymous%2Cphotos_anonymous%2Cprofile_anonymous&isApiOauthAnonymEnabled=false&version=1&app_id=6287487" + url := "https://login.vk.ru/?act=get_anonym_token" + + resp, err := doRequest(data, url) + if err != nil { + return "", "", "", fmt.Errorf("request error:%s", err) + } + + token1 := resp["data"].(map[string]interface{})["access_token"].(string) + + data = fmt.Sprintf("access_token=%s", token1) + url = "https://api.vk.ru/method/calls.getAnonymousAccessTokenPayload?v=5.264&client_id=6287487" + + resp, err = doRequest(data, url) + if err != nil { + return "", "", "", fmt.Errorf("request error:%s", err) + } + + token2 := resp["response"].(map[string]interface{})["payload"].(string) + + data = fmt.Sprintf("client_id=6287487&token_type=messages&payload=%s&client_secret=QbYic1K3lEV5kTGiqlq2&version=1&app_id=6287487", token2) + url = "https://login.vk.ru/?act=get_anonym_token" + + resp, err = doRequest(data, url) + if err != nil { + return "", "", "", fmt.Errorf("request error:%s", err) + } + + token3 := resp["data"].(map[string]interface{})["access_token"].(string) + + data = fmt.Sprintf("vk_join_link=https://vk.com/call/join/%s&name=123&access_token=%s", link, token3) + url = "https://api.vk.ru/method/calls.getAnonymousToken?v=5.264" + + resp, err = doRequest(data, url) + if err != nil { + return "", "", "", fmt.Errorf("request error:%s", err) + } + + token4 := resp["response"].(map[string]interface{})["token"].(string) + + data = fmt.Sprintf("%s%s%s", "session_data=%7B%22version%22%3A2%2C%22device_id%22%3A%22", uuid.New(), "%22%2C%22client_version%22%3A1.1%2C%22client_type%22%3A%22SDK_JS%22%7D&method=auth.anonymLogin&format=JSON&application_key=CGMMEJLGDIHBABABA") + url = "https://calls.okcdn.ru/fb.do" + + resp, err = doRequest(data, url) + if err != nil { + return "", "", "", fmt.Errorf("request error:%s", err) + } + + token5 := resp["session_key"].(string) + + data = fmt.Sprintf("joinLink=%s&isVideo=false&protocolVersion=5&anonymToken=%s&method=vchat.joinConversationByLink&format=JSON&application_key=CGMMEJLGDIHBABABA&session_key=%s", link, token4, token5) + url = "https://calls.okcdn.ru/fb.do" + + resp, err = doRequest(data, url) + if err != nil { + return "", "", "", fmt.Errorf("request error:%s", err) + } + + user := resp["turn_server"].(map[string]interface{})["username"].(string) + pass := resp["turn_server"].(map[string]interface{})["credential"].(string) + turn := resp["turn_server"].(map[string]interface{})["urls"].([]interface{})[0].(string) + + return user, pass, turn, nil +} + +func dtlsFunc(ctx context.Context, conn net.PacketConn, peer *net.UDPAddr) (net.Conn, error) { + certificate, err := selfsign.GenerateSelfSigned() + if err != nil { + return nil, err + } + config := &dtls.Config{ + Certificates: []tls.Certificate{certificate}, + InsecureSkipVerify: true, + ExtendedMasterSecret: dtls.RequireExtendedMasterSecret, + CipherSuites: []dtls.CipherSuiteID{dtls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256}, + ConnectionIDGenerator: dtls.OnlySendCIDGenerator(), + } + ctx1, cancel := context.WithTimeout(ctx, 30*time.Second) + defer cancel() + dtlsConn, err := dtls.Client(conn, peer, config) + if err != nil { + return nil, err + } + + if err := dtlsConn.HandshakeContext(ctx1); err != nil { + return nil, err + } + return dtlsConn, nil +} + +func oneDtlsConnection(ctx context.Context, peer *net.UDPAddr, listenConn net.PacketConn, connchan chan<- net.PacketConn, okchan chan<- struct{}, c1 chan<- error) { + var err error = nil + defer func() { c1 <- err }() + dtlsctx, dtlscancel := context.WithCancel(ctx) + defer dtlscancel() + var conn1, conn2 net.PacketConn + conn1, conn2 = connutil.AsyncPacketPipe() + go func() { + for { + select { + case <-dtlsctx.Done(): + return + case connchan <- conn2: + } + } + }() + dtlsConn, err1 := dtlsFunc(dtlsctx, conn1, peer) + if err1 != nil { + err = fmt.Errorf("failed to connect DTLS: %s", err1) + return + } + defer func() { + if closeErr := dtlsConn.Close(); closeErr != nil { + err = fmt.Errorf("failed to close DTLS connection: %s", closeErr) + return + } + log.Printf("Closed DTLS connection\n") + }() + log.Printf("Established DTLS connection!\n") + go func() { + for { + select { + case <-dtlsctx.Done(): + return + case okchan <- struct{}{}: + } + } + }() + + wg := sync.WaitGroup{} + wg.Add(2) + context.AfterFunc(dtlsctx, func() { + listenConn.SetDeadline(time.Now()) + dtlsConn.SetDeadline(time.Now()) + }) + var addr atomic.Value + // Start read-loop on listenConn + go func() { + defer wg.Done() + defer dtlscancel() + buf := make([]byte, 1600) + for { + select { + case <-dtlsctx.Done(): + return + default: + } + n, addr1, err1 := listenConn.ReadFrom(buf) + if err1 != nil { + log.Printf("Failed: %s", err1) + return + } + + addr.Store(addr1) // store peer + + _, err1 = dtlsConn.Write(buf[:n]) + if err1 != nil { + log.Printf("Failed: %s", err1) + return + } + } + }() + + // Start read-loop on dtlsConn + go func() { + defer wg.Done() + defer dtlscancel() + buf := make([]byte, 1600) + for { + select { + case <-dtlsctx.Done(): + return + default: + } + n, err1 := dtlsConn.Read(buf) + if err1 != nil { + log.Printf("Failed: %s", err1) + return + } + addr1, ok := addr.Load().(net.Addr) + if !ok { + log.Printf("Failed: no listener ip") + return + } + + _, err1 = listenConn.WriteTo(buf[:n], addr1) + if err1 != nil { + log.Printf("Failed: %s", err1) + return + } + } + }() + + wg.Wait() + listenConn.SetDeadline(time.Time{}) + dtlsConn.SetDeadline(time.Time{}) +} + +func oneTurnConnection(ctx context.Context, host string, port string, link string, udp bool, realm string, peer net.Addr, conn2 net.PacketConn, c chan<- error) { + var err error = nil + defer func() { c <- err }() + user, pass, url, err1 := getCreds(link) + if err1 != nil { + err = fmt.Errorf("failed to get TURN credentials: %s", err1) + return + } + var turnServerAddr string + if host == "" || port == "" { + turnServerAddr = url[5:] + } else { + turnServerAddr = net.JoinHostPort(host, port) + } + turnServerUdpAddr, err1 := net.ResolveUDPAddr("udp", turnServerAddr) + if err1 != nil { + err = fmt.Errorf("failed to resolve TURN server address: %s", err1) + return + } + turnServerAddr = turnServerUdpAddr.String() + fmt.Println(turnServerUdpAddr.IP) + // Dial TURN Server + var cfg *turn.ClientConfig + var turnConn net.PacketConn + var d net.Dialer + ctx1, cancel := context.WithTimeout(ctx, 5*time.Second) + defer cancel() + if udp { + turnConn, err1 = net.ListenPacket("udp", "") // nolint: noctx + if err1 != nil { + err = fmt.Errorf("failed to connect to TURN server: %s", err1) + return + } + defer func() { + if err1 = turnConn.Close(); err1 != nil { + err = fmt.Errorf("failed to close TURN server connection: %s", err1) + return + } + }() + } else { + conn, err2 := d.DialContext(ctx1, "tcp", turnServerAddr) // nolint: noctx + if err2 != nil { + err = fmt.Errorf("failed to connect to TURN server: %s", err2) + return + } + defer func() { + if err1 = conn.Close(); err1 != nil { + err = fmt.Errorf("failed to close TURN server connection: %s", err1) + return + } + }() + turnConn = turn.NewSTUNConn(conn) + } + // Start a new TURN Client and wrap our net.Conn in a STUNConn + // This allows us to simulate datagram based communication over a net.Conn + cfg = &turn.ClientConfig{ + STUNServerAddr: turnServerAddr, + TURNServerAddr: turnServerAddr, + Conn: turnConn, + Username: user, + Password: pass, + Realm: realm, + LoggerFactory: logging.NewDefaultLoggerFactory(), + } + + client, err1 := turn.NewClient(cfg) + if err1 != nil { + err = fmt.Errorf("failed to create TURN client: %s", err1) + return + } + defer client.Close() + + // Start listening on the conn provided. + err1 = client.Listen() + if err1 != nil { + err = fmt.Errorf("failed to listen: %s", err1) + return + } + + // Allocate a relay socket on the TURN server. On success, it + // will return a net.PacketConn which represents the remote + // socket. + relayConn, err1 := client.Allocate() + if err1 != nil { + err = fmt.Errorf("failed to allocate: %s", err1) + return + } + defer func() { + if err1 := relayConn.Close(); err1 != nil { + err = fmt.Errorf("failed to close TURN allocated connection: %s", err1) + } + }() + + // The relayConn's local address is actually the transport + // address assigned on the TURN server. + log.Printf("relayed-address=%s", relayConn.LocalAddr().String()) + + wg := sync.WaitGroup{} + wg.Add(2) + turnctx, turncancel := context.WithCancel(context.Background()) + context.AfterFunc(turnctx, func() { + relayConn.SetDeadline(time.Now()) + conn2.SetDeadline(time.Now()) + }) + // Start read-loop on conn2 (output of DTLS) + go func() { + defer wg.Done() + defer turncancel() + buf := make([]byte, 1600) + for { + select { + case <-turnctx.Done(): + return + default: + } + n, _, err1 := conn2.ReadFrom(buf) + if err1 != nil { + log.Printf("Failed: %s", err1) + return + } + + _, err1 = relayConn.WriteTo(buf[:n], peer) + if err1 != nil { + log.Printf("Failed: %s", err1) + return + } + } + }() + + // Start read-loop on relayConn + go func() { + defer wg.Done() + defer turncancel() + buf := make([]byte, 1600) + for { + select { + case <-turnctx.Done(): + return + default: + } + n, _, err1 := relayConn.ReadFrom(buf) + if err1 != nil { + log.Printf("Failed: %s", err1) + return + } + + _, err1 = conn2.WriteTo(buf[:n], nil) + if err1 != nil { + log.Printf("Failed: %s", err1) + return + } + } + }() + + wg.Wait() + relayConn.SetDeadline(time.Time{}) + conn2.SetDeadline(time.Time{}) +} + +func main() { //nolint:cyclop + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + signalChan := make(chan os.Signal, 1) + signal.Notify(signalChan, syscall.SIGTERM, syscall.SIGINT) + go func() { + <-signalChan + log.Printf("Terminating...\n") + cancel() + select { + case <-signalChan: + case <-time.After(5 * time.Second): + } + log.Fatalf("Exit...\n") + }() + + host := flag.String("turn", "", "override TURN server ip") + port := flag.String("port", "19302", "override TURN port") + listen := flag.String("listen", "127.0.0.1:9000", "listen on ip:port") + realm := flag.String("realm", "call6-7.vkuser.net", "TURN Realm") + link := flag.String("link", "", "VK calls invite link \"https://vk.com/call/join/...\"") + peerAddr := flag.String("peer", "", "peer server address (host:port)") + n := flag.Int("n", 16, "connections to TURN") + udp := flag.Bool("udp", false, "connect to TURN with UDP") + flag.Parse() + if *peerAddr == "" { + log.Panicf("Need peer address!") + } + if *link == "" { + log.Panicf("Need invite link!") + } + peer, err := net.ResolveUDPAddr("udp", *peerAddr) + if err != nil { + panic(err) + } + + *link = (*link)[len(*link)-43:] + + listenConn, err := net.ListenPacket("udp", *listen) // nolint: noctx + if err != nil { + log.Panicf("Failed to listen: %s", err) + } + defer func() { + if closeErr := listenConn.Close(); closeErr != nil { + log.Panicf("Failed to close local connection: %s", closeErr) + } + }() + + okchan := make(chan struct{}) + connchan := make(chan net.PacketConn) + + wg1 := sync.WaitGroup{} + wg1.Go(func() { + for { + c1 := make(chan error) + go oneDtlsConnection(ctx, peer, listenConn, connchan, okchan, c1) + if err := <-c1; err != nil { + log.Printf("%s", err) + } + select { + case <-ctx.Done(): + return + default: + } + } + }) + + t := time.Tick(100 * time.Millisecond) + wg1.Go(func() { + for { + select { + case <-ctx.Done(): + return + case conn2 := <-connchan: + select { + case <-t: + c := make(chan error) + go oneTurnConnection(ctx, *host, *port, *link, *udp, *realm, peer, conn2, c) + if err := <-c; err != nil { + log.Printf("%s", err) + } + default: + } + } + } + }) + + for i := 0; i < *n-1; i++ { + wg1.Go(func() { + for { + select { + case <-ctx.Done(): + return + case <-okchan: + select { + case conn2 := <-connchan: + select { + case <-t: + c2 := make(chan error) + go oneTurnConnection(ctx, *host, *port, *link, *udp, *realm, peer, conn2, c2) + if err := <-c2; err != nil { + log.Printf("%s", err) + } + default: + } + default: + } + } + } + }) + } + + wg1.Wait() +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..e271175 --- /dev/null +++ b/go.mod @@ -0,0 +1,20 @@ +module github.com/cacggghp/vk-turn-proxy + +go 1.25.5 + +require ( + github.com/cbeuw/connutil v1.0.1 + github.com/google/uuid v1.6.0 + github.com/pion/dtls/v3 v3.0.10 + github.com/pion/logging v0.2.4 + github.com/pion/turn/v4 v4.1.4 +) + +require ( + 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/wlynxg/anet v0.0.5 // indirect + golang.org/x/crypto v0.46.0 // indirect + golang.org/x/sys v0.40.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..d77591a --- /dev/null +++ b/go.sum @@ -0,0 +1,34 @@ +github.com/cbeuw/connutil v1.0.1 h1:LWuNYjwm7JEDYG/ISAO1TfU4G+q2dA5NhR97eq2roCA= +github.com/cbeuw/connutil v1.0.1/go.mod h1:lKofNtrW7Atmosgp1eNnTt2j2NjA2IkifapgLVI1QtA= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/pion/dtls/v3 v3.0.10 h1:k9ekkq1kaZoxnNEbyLKI8DI37j/Nbk1HWmMuywpQJgg= +github.com/pion/dtls/v3 v3.0.10/go.mod h1:YEmmBYIoBsY3jmG56dsziTv/Lca9y4Om83370CXfqJ8= +github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8= +github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so= +github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= +github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= +github.com/pion/stun/v3 v3.1.1 h1:CkQxveJ4xGQjulGSROXbXq94TAWu8gIX2dT+ePhUkqw= +github.com/pion/stun/v3 v3.1.1/go.mod h1:qC1DfmcCTQjl9PBaMa5wSn3x9IPmKxSdcCsxBcDBndM= +github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o= +github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM= +github.com/pion/turn/v4 v4.1.4 h1:EU11yMXKIsK43FhcUnjLlrhE4nboHZq+TXBIi3QpcxQ= +github.com/pion/turn/v4 v4.1.4/go.mod h1:ES1DXVFKnOhuDkqn9hn5VJlSWmZPaRJLyBXoOeO/BmQ= +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/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= +github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4= +golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/routes.ps1 b/routes.ps1 new file mode 100644 index 0000000..2c2464e --- /dev/null +++ b/routes.ps1 @@ -0,0 +1,27 @@ +# Получаем default gateway (IPv4) +$gateway = Get-NetRoute ` + -DestinationPrefix "0.0.0.0/0" ` + | Sort-Object RouteMetric ` + | Select-Object -First 1 -ExpandProperty NextHop + +if (-not $gateway) { + Write-Error "Не удалось определить default gateway" + exit 1 +} + +Write-Host "Default gateway: $gateway" + +# Читаем адреса из stdin +$input | ForEach-Object { + $addr = $_.Trim() + if ($addr -eq "") { return } + + Write-Host "Добавляем маршрут к $addr через $gateway" + + New-NetRoute ` + -DestinationPrefix "$addr/32" ` + -NextHop $gateway ` + -PolicyStore ActiveStore ` + -ErrorAction Stop +} + diff --git a/routes.sh b/routes.sh new file mode 100644 index 0000000..fa84ad9 --- /dev/null +++ b/routes.sh @@ -0,0 +1,5 @@ +#!/bin/bash +gateway="$(ip -o -4 route show to default | awk '/via/ {print $3}' | head -1)" +while read -r remote; do + sudo ip r add $remote via $gateway +done diff --git a/server/main.go b/server/main.go new file mode 100644 index 0000000..0136b39 --- /dev/null +++ b/server/main.go @@ -0,0 +1,189 @@ +package main + +import ( + "context" + "crypto/tls" + "flag" + "fmt" + "log" + "net" + "os" + "os/signal" + "sync" + "syscall" + "time" + + "github.com/pion/dtls/v3" + "github.com/pion/dtls/v3/pkg/crypto/selfsign" +) + +func main() { + listen := flag.String("listen", "0.0.0.0:56000", "listen on ip:port") + connect := flag.String("connect", "", "connect to ip:port") + flag.Parse() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + signalChan := make(chan os.Signal, 1) + signal.Notify(signalChan, syscall.SIGTERM, syscall.SIGINT) + go func() { + <-signalChan + log.Printf("Terminating...\n") + cancel() + <-signalChan + log.Fatalf("Exit...\n") + }() + + addr, err := net.ResolveUDPAddr("udp", *listen) + if err != nil { + panic(err) + } + if len(*connect) == 0 { + log.Panicf("server address is required") + } + // Generate a certificate and private key to secure the connection + certificate, genErr := selfsign.GenerateSelfSigned() + if genErr != nil { + panic(err) + } + + // + // Everything below is the pion-DTLS API! Thanks for using it ❤️. + // + + // Prepare the configuration of the DTLS connection + config := &dtls.Config{ + Certificates: []tls.Certificate{certificate}, + ExtendedMasterSecret: dtls.RequireExtendedMasterSecret, + CipherSuites: []dtls.CipherSuiteID{dtls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256}, + ConnectionIDGenerator: dtls.RandomCIDGenerator(8), + } + + // Connect to a DTLS server + listener, err := dtls.Listen("udp", addr, config) + if err != nil { + panic(err) + } + context.AfterFunc(ctx, func() { + if err = listener.Close(); err != nil { + panic(err) + } + }) + + fmt.Println("Listening") + + wg1 := sync.WaitGroup{} + for { + select { + case <-ctx.Done(): + wg1.Wait() + return + default: + } + // Wait for a connection. + conn, err := listener.Accept() + if err != nil { + log.Println(err) + continue + } + wg1.Add(1) + go func(conn net.Conn) { + defer wg1.Done() + defer conn.Close() // graceful shutdown + var err error = nil + log.Printf("Connection from %s\n", conn.RemoteAddr()) + // `conn` is of type `net.Conn` but may be casted to `dtls.Conn` + // using `dtlsConn := conn.(*dtls.Conn)` in order to to expose + // functions like `ConnectionState` etc. + + // Perform the handshake with a 30-second timeout + ctx1, cancel1 := context.WithTimeout(ctx, 30*time.Second) + dtlsConn, ok := conn.(*dtls.Conn) + if !ok { + log.Println("Type error") + cancel1() + return + } + log.Println("Start handshake") + if err = dtlsConn.HandshakeContext(ctx1); err != nil { + log.Println(err) + cancel1() + return + } + cancel1() + log.Println("Handshake done") + + serverConn, err := net.Dial("udp", *connect) + if err != nil { + log.Println(err) + return + } + defer func() { + if err = serverConn.Close(); err != nil { + log.Printf("failed to close outgoing connection: %s", err) + return + } + }() + + var wg sync.WaitGroup + wg.Add(2) + ctx2, cancel2 := context.WithCancel(ctx) + context.AfterFunc(ctx2, func() { + conn.SetDeadline(time.Now()) + serverConn.SetDeadline(time.Now()) + }) + go func() { + defer wg.Done() + defer cancel2() + buf := make([]byte, 1600) + for { + select { + case <-ctx2.Done(): + return + default: + } + conn.SetReadDeadline(time.Now().Add(time.Minute * 30)) + n, err1 := conn.Read(buf) + if err1 != nil { + log.Printf("Failed: %s", err1) + return + } + + serverConn.SetWriteDeadline(time.Now().Add(time.Minute * 30)) + _, err1 = serverConn.Write(buf[:n]) + if err1 != nil { + log.Printf("Failed: %s", err1) + return + } + } + }() + go func() { + defer wg.Done() + defer cancel2() + buf := make([]byte, 1600) + for { + select { + case <-ctx2.Done(): + return + default: + } + serverConn.SetReadDeadline(time.Now().Add(time.Minute * 30)) + n, err1 := serverConn.Read(buf) + if err1 != nil { + log.Printf("Failed: %s", err1) + return + } + + conn.SetWriteDeadline(time.Now().Add(time.Minute * 30)) + _, err1 = conn.Write(buf[:n]) + if err1 != nil { + log.Printf("Failed: %s", err1) + return + } + } + }() + wg.Wait() + log.Printf("Connection closed: %s\n", conn.RemoteAddr()) + }(conn) + } +}