@@ -26,6 +26,7 @@ import (
2626 "time"
2727
2828 "github.com/cacggghp/vk-turn-proxy/internal/controlpath"
29+ "github.com/cacggghp/vk-turn-proxy/internal/wrap"
2930 "github.com/cacggghp/vk-turn-proxy/sessionproto"
3031 "github.com/cbeuw/connutil"
3132 "github.com/google/uuid"
@@ -1467,6 +1468,16 @@ type turnParams struct {
14671468 getCreds getCredsFunc
14681469 resolver * protectedResolver
14691470 credsManager * groupedCredsManager
1471+ // WRAP per-packet obfuscation. wrapCipher == WRAP_CIPHER_NONE /
1472+ // _UNSPECIFIED disables WRAP regardless of wrapKey.
1473+ wrapCipher sessionproto.WrapCipher
1474+ wrapKey []byte
1475+ // wrapMode controls fallback semantics when WRAP is configured:
1476+ // "off" — never wrap (wrapCipher should already be NONE)
1477+ // "preferred" — try WRAP, fall back to raw if no successfully unwrapped
1478+ // inbound packet arrives within wrapFallbackInboundTimeout
1479+ // "required" — fail hard if WRAP unwrap doesn't succeed; never fall back
1480+ wrapMode string
14701481}
14711482
14721483func oneTurnConnection (
@@ -1632,6 +1643,53 @@ func oneTurnConnection(
16321643 }
16331644 }
16341645 })
1646+ wrapCipher , err1 := wrap .New (turnParams .wrapCipher , turnParams .wrapKey )
1647+ if err1 != nil {
1648+ err = fmt .Errorf ("WRAP cipher init failed: %s" , err1 )
1649+ return
1650+ }
1651+ wrapModeForAttempt := turnParams .wrapMode
1652+ // Fallback key is the peer (our server) address: WRAP support is a
1653+ // property of the peer, not of the VK TURN relay we route through.
1654+ fallbackKey := peer .String ()
1655+ if wrapCipher != nil && wrapModeForAttempt == "preferred" && wrapDisabledForAddr (fallbackKey ) {
1656+ log .Printf ("[STREAM %d] WRAP marked unsupported for peer %s recently; using raw this attempt" , streamID , fallbackKey )
1657+ wrapCipher = nil
1658+ }
1659+ if wrapCipher != nil {
1660+ log .Printf ("[STREAM %d] WRAP active: cipher=%s mode=%s" , streamID , turnParams .wrapCipher , wrapModeForAttempt )
1661+ }
1662+ var anyWrapInboundSuccess atomic.Bool
1663+ wrapActiveThisAttempt := wrapCipher != nil
1664+ // On return: if WRAP was active and no successful inbound unwrap
1665+ // happened (worker exited due to DTLS handshake timeout, peer
1666+ // closure, ctx cancellation, etc.), record peer as no-wrap so the
1667+ // maintain loop's next attempt goes raw within the TTL window.
1668+ defer func () {
1669+ if wrapActiveThisAttempt && wrapModeForAttempt == "preferred" && ! anyWrapInboundSuccess .Load () {
1670+ markWrapDisabledForAddr (fallbackKey )
1671+ log .Printf ("[STREAM %d] WRAP exit with no decoded inbound; disabling WRAP for peer %s" , streamID , fallbackKey )
1672+ }
1673+ }()
1674+ if wrapActiveThisAttempt && wrapModeForAttempt == "preferred" {
1675+ go func () {
1676+ timer := time .NewTimer (wrapFallbackInboundTimeout )
1677+ defer timer .Stop ()
1678+ select {
1679+ case <- turnctx .Done ():
1680+ return
1681+ case <- timer .C :
1682+ if ! anyWrapInboundSuccess .Load () {
1683+ markWrapDisabledForAddr (fallbackKey )
1684+ log .Printf (
1685+ "[STREAM %d] no WRAP-decoded inbound from peer %s in %s — disabling WRAP and reconnecting raw" ,
1686+ streamID , fallbackKey , wrapFallbackInboundTimeout ,
1687+ )
1688+ turncancel ()
1689+ }
1690+ }
1691+ }()
1692+ }
16351693 var addr atomic.Value
16361694 // Start read-loop on conn2 (output of DTLS)
16371695 go func () {
@@ -1654,7 +1712,16 @@ func oneTurnConnection(
16541712
16551713 addr .Store (addr1 ) // store peer
16561714
1657- _ , err1 = relayConn .WriteTo (buf [:n ], peer )
1715+ payload := buf [:n ]
1716+ if wrapCipher != nil {
1717+ sealed , sealErr := wrapCipher .Seal (payload )
1718+ if sealErr != nil {
1719+ log .Printf ("[STREAM %d] WRAP seal failed: %s" , streamID , sealErr )
1720+ return
1721+ }
1722+ payload = sealed
1723+ }
1724+ _ , err1 = relayConn .WriteTo (payload , peer )
16581725 if err1 != nil {
16591726 if ! shouldSuppressWorkerError (turnctx , err1 ) {
16601727 log .Printf ("Failed: %s" , err1 )
@@ -1668,7 +1735,11 @@ func oneTurnConnection(
16681735 go func () {
16691736 defer wg .Done ()
16701737 defer turncancel ()
1671- buf := make ([]byte , 1600 )
1738+ readBufLen := 1600
1739+ if wrapCipher != nil {
1740+ readBufLen += wrapCipher .Overhead ()
1741+ }
1742+ buf := make ([]byte , readBufLen )
16721743 for {
16731744 select {
16741745 case <- turnctx .Done ():
@@ -1687,7 +1758,17 @@ func oneTurnConnection(
16871758 continue
16881759 }
16891760
1690- _ , err1 = conn2 .WriteTo (buf [:n ], addr1 )
1761+ payload := buf [:n ]
1762+ if wrapCipher != nil {
1763+ plain , openErr := wrapCipher .Open (payload )
1764+ if openErr != nil {
1765+ log .Printf ("[STREAM %d] WRAP unwrap failed (%d bytes): %s" , streamID , n , openErr )
1766+ continue
1767+ }
1768+ anyWrapInboundSuccess .Store (true )
1769+ payload = plain
1770+ }
1771+ _ , err1 = conn2 .WriteTo (payload , addr1 )
16911772 if err1 != nil {
16921773 if ! shouldSuppressWorkerError (turnctx , err1 ) {
16931774 log .Printf ("Failed: %s" , err1 )
@@ -2016,6 +2097,10 @@ func main() { //nolint:cyclop
20162097 setStrategy := func (_ sessionproto.Mode , _ uint32 , _ int ) {}
20172098 return unifiedGetCreds , setStrategy
20182099 }
2100+ wrapCipherSel , wrapKey , wrapMode , err := resolveWrapConfig (opts .wrapMode , opts .wrapCipher , opts .wrapKeyHex )
2101+ if err != nil {
2102+ log .Panicf ("WRAP config: %v" , err )
2103+ }
20192104 params := & turnParams {
20202105 host : opts .host ,
20212106 port : opts .port ,
@@ -2024,6 +2109,9 @@ func main() { //nolint:cyclop
20242109 getCreds : nil ,
20252110 resolver : peerResolver ,
20262111 credsManager : vkLinkManager ,
2112+ wrapCipher : wrapCipherSel ,
2113+ wrapKey : wrapKey ,
2114+ wrapMode : wrapMode ,
20272115 }
20282116 sessionID := []byte (nil )
20292117
@@ -2271,10 +2359,24 @@ func main() { //nolint:cyclop
22712359 false ,
22722360 )
22732361 if ! waitForReady (ctx , okchan , mainlineBootstrapTimeout ) {
2362+ // If WRAP was attempted, the watchdog in each worker has
2363+ // by now marked the peer as no-wrap; pre-emptively mark
2364+ // it here too so any racing worker also goes raw. Give
2365+ // bootstrap one more shot before tearing the process
2366+ // down so Android does not enter a respawn loop that
2367+ // wipes the wrap-disabled cache each time.
2368+ if params .wrapMode == "preferred" {
2369+ markWrapDisabledForAddr (peer .String ())
2370+ log .Printf ("bootstrap timed out; forcing WRAP fallback for peer %s and retrying" , peer .String ())
2371+ if waitForReady (ctx , okchan , mainlineBootstrapTimeout ) {
2372+ goto mainlineBootstrapDone
2373+ }
2374+ }
22742375 runtimeCancel ()
22752376 runtimeWG .Wait ()
22762377 log .Fatalf ("failed to bootstrap mainline session" )
22772378 }
2379+ mainlineBootstrapDone:
22782380
22792381 supportedVersion := waitForProbeVersion (ctx , probeResult , muProbeTimeout )
22802382 activeMainlineControl := waitForMainlineControlHandle (ctx , mainlineControl , muProbeTimeout )
0 commit comments