Add call protocol, Rmux. Update AmneziaWG. Fixes and improvements

This commit is contained in:
Shtorm
2026-08-06 14:19:34 +03:00
parent 051e928b01
commit d6b9f693c4
89 changed files with 15671 additions and 132 deletions

View File

@@ -0,0 +1,8 @@
package common
const (
UDPBufSize = 4096
RTPBufSize = 65536
VP8BufSize = 1126
DCBufSize = 32768
)

View File

@@ -0,0 +1,15 @@
package common
import (
"github.com/pion/webrtc/v4"
"github.com/sagernet/sing/common/logger"
)
type ResolveFunc func(hostname string) (string, error)
type PeerConnectionConfigurer interface {
ConfigureSettingEngine(settingEngine *webrtc.SettingEngine)
}
type AddTunnelTracksFunc func(pc *webrtc.PeerConnection, logger logger.ContextLogger, prefix string) *webrtc.TrackLocalStaticSample
type ReadTrackFunc func(track *webrtc.TrackRemote, handler func([]byte), logger logger.ContextLogger, prefix string)

View File

@@ -0,0 +1,87 @@
package common
import (
"context"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"os"
"strings"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
)
const UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36"
func LoadCookies(path string) (string, error) {
data, err := os.ReadFile(path)
if err != nil {
return "", fmt.Errorf("cannot read cookies: %w", err)
}
var cookies []struct {
Name string `json:"name"`
Value string `json:"value"`
}
if err := json.Unmarshal(data, &cookies); err != nil {
return "", fmt.Errorf("cannot parse cookies: %w", err)
}
parts := make([]string, len(cookies))
for i, c := range cookies {
parts[i] = c.Name + "=" + c.Value
}
return strings.Join(parts, "; "), nil
}
func HttpClient(dialer N.Dialer) *http.Client {
return &http.Client{
Transport: &http.Transport{
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return dialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
},
},
}
}
func HttpGet(dialer N.Dialer, endpoint string) ([]byte, error) {
req, _ := http.NewRequest("GET", endpoint, nil)
req.Header.Set("User-Agent", UserAgent)
resp, err := HttpClient(dialer).Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
return io.ReadAll(resp.Body)
}
func CookieValue(cookieHeader, name string) string {
for _, part := range strings.Split(cookieHeader, ";") {
part = strings.TrimSpace(part)
eq := strings.IndexByte(part, '=')
if eq != -1 && part[:eq] == name {
return part[eq+1:]
}
}
return ""
}
func FilterCookies(cookieHeader string, allow []string) string {
allowed := make(map[string]struct{}, len(allow))
for _, n := range allow {
allowed[n] = struct{}{}
}
var out []string
for _, part := range strings.Split(cookieHeader, ";") {
trimmed := strings.TrimSpace(part)
eq := strings.IndexByte(trimmed, '=')
if eq == -1 {
continue
}
if _, ok := allowed[trimmed[:eq]]; ok {
out = append(out, trimmed)
}
}
return strings.Join(out, "; ")
}

View File

@@ -0,0 +1,58 @@
package common
import (
"net"
"strings"
)
func FixICEURL(iceURL string) string {
idx := strings.Index(iceURL, ":")
if idx < 0 {
return iceURL
}
scheme := iceURL[:idx]
if scheme != "turn" && scheme != "stun" && scheme != "turns" && scheme != "stuns" {
return iceURL
}
rest := iceURL[idx+1:]
if strings.HasPrefix(rest, "[") {
return iceURL
}
if strings.Count(rest, ":") <= 1 {
return iceURL
}
params := ""
if qm := strings.Index(rest, "?"); qm >= 0 {
params = rest[qm:]
rest = rest[:qm]
}
lastColon := strings.LastIndex(rest, ":")
if lastColon > 0 {
host := rest[:lastColon]
port := rest[lastColon+1:]
if net.ParseIP(host) != nil {
return scheme + ":[" + host + "]:" + port + params
}
}
if net.ParseIP(rest) != nil {
return scheme + ":[" + rest + "]" + params
}
return iceURL
}
func ExtractICEHost(iceURL string) string {
idx := strings.Index(iceURL, ":")
if idx < 0 {
return ""
}
rest := iceURL[idx+1:]
params := strings.Index(rest, "?")
if params >= 0 {
rest = rest[:params]
}
host, _, err := net.SplitHostPort(rest)
if err != nil {
return rest
}
return host
}

View File

@@ -0,0 +1,40 @@
package common
import (
"math/rand/v2"
"time"
)
const backoffJitterFloorDivisor = 4
func BackoffWithJitter(attempt int, initialDelay, maxDelay time.Duration) time.Duration {
if initialDelay <= 0 {
return 0
}
if maxDelay < initialDelay {
maxDelay = initialDelay
}
if attempt < 0 {
attempt = 0
}
ceiling := maxDelay
if shifted := initialDelay << uint(attempt); shifted > 0 && shifted < maxDelay {
ceiling = shifted
}
floor := ceiling / backoffJitterFloorDivisor
return floor + time.Duration(rand.Int64N(int64(ceiling-floor)+1))
}
func DurationInRange(minDuration, maxDuration time.Duration) time.Duration {
if maxDuration <= minDuration {
return minDuration
}
return minDuration + time.Duration(rand.Int64N(int64(maxDuration-minDuration)+1))
}
func IntInRange(minValue, maxValue int) int {
if maxValue <= minValue {
return minValue
}
return minValue + rand.IntN(maxValue-minValue+1)
}

View File

@@ -0,0 +1,68 @@
package common
import (
"fmt"
"net"
)
func MaskError(err error) string {
if err == nil {
return ""
}
if !MaskingEnabled {
return err.Error()
}
if opErr, ok := err.(*net.OpError); ok {
msg := opErr.Op
if opErr.Net != "" {
msg += " " + opErr.Net
}
if opErr.Source != nil {
msg += " " + MaskAddr(opErr.Source.String())
}
if opErr.Source != nil && opErr.Addr != nil {
msg += "->"
}
if opErr.Addr != nil {
msg += MaskAddr(opErr.Addr.String())
}
msg += ": " + opErr.Err.Error()
return msg
}
return err.Error()
}
const MaskingEnabled = true
func MaskAddr(addr string) string {
if !MaskingEnabled {
return addr
}
host, port, err := net.SplitHostPort(addr)
if err != nil {
host = addr
port = ""
}
masked := maskHost(host)
if port != "" {
return net.JoinHostPort(masked, port)
}
return masked
}
func maskHost(host string) string {
if host == "" {
return ""
}
ip := net.ParseIP(host)
if ip != nil {
if ip4 := ip.To4(); ip4 != nil {
return fmt.Sprintf("%d.%d.x.x", ip4[0], ip4[1])
}
return "x::x"
}
if len(host) <= 1 {
return "*"
}
return string(host[0]) + "***"
}

View File

@@ -0,0 +1,89 @@
package common
import (
"fmt"
"github.com/pion/rtp"
"github.com/pion/rtp/codecs"
"github.com/pion/webrtc/v4"
"github.com/sagernet/sing/common/logger"
)
func AddTunnelTracks(pc *webrtc.PeerConnection, logger logger.ContextLogger, prefix string) *webrtc.TrackLocalStaticSample {
sampleTrack, _ := webrtc.NewTrackLocalStaticSample(
webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeVP8},
"video", "tunnel-video",
)
audioTrack, _ := webrtc.NewTrackLocalStaticRTP(
webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeOpus},
"audio", "tunnel-audio",
)
audioSender, audioErr := pc.AddTrack(audioTrack)
videoSender, videoErr := pc.AddTrack(sampleTrack)
logger.Debug(fmt.Sprintf("%s: AddTrack audio: sender=%v err=%v", prefix, audioSender != nil, audioErr))
logger.Debug(fmt.Sprintf("%s: AddTrack video: sender=%v err=%v", prefix, videoSender != nil, videoErr))
logger.Debug(fmt.Sprintf("%s: senders count: %d", prefix, len(pc.GetSenders())))
return sampleTrack
}
func ReadTrack(track *webrtc.TrackRemote, handler func([]byte), logger logger.ContextLogger, prefix string) {
if track.Codec().MimeType != webrtc.MimeTypeVP8 {
buf := make([]byte, UDPBufSize)
for {
if _, _, err := track.Read(buf); err != nil {
return
}
}
}
var vp8Pkt codecs.VP8Packet
var pkt rtp.Packet
var frameBuf []byte
var lastSeq uint16
var haveLastSeq bool
frameValid := false
recvCount := 0
buf := make([]byte, RTPBufSize)
for {
n, _, err := track.Read(buf)
if err != nil {
return
}
if pkt.Unmarshal(buf[:n]) != nil {
continue
}
if haveLastSeq && pkt.SequenceNumber != lastSeq+1 {
frameValid = false
frameBuf = frameBuf[:0]
}
lastSeq = pkt.SequenceNumber
haveLastSeq = true
vp8Payload, err := vp8Pkt.Unmarshal(pkt.Payload)
if err != nil {
frameValid = false
frameBuf = frameBuf[:0]
continue
}
if vp8Pkt.S == 1 {
frameBuf = frameBuf[:0]
frameValid = true
}
if !frameValid {
continue
}
frameBuf = append(frameBuf, vp8Payload...)
if !pkt.Marker {
continue
}
recvCount++
if recvCount <= 3 || recvCount%200 == 0 {
logger.Debug(fmt.Sprintf("%s: recv vp8 frame #%d %d bytes", prefix, recvCount, len(frameBuf)))
}
if handler != nil {
frame := make([]byte, len(frameBuf))
copy(frame, frameBuf)
handler(frame)
}
frameBuf = frameBuf[:0]
frameValid = false
}
}

View File

@@ -0,0 +1,17 @@
package common
import (
"time"
"github.com/gorilla/websocket"
)
func CloseWS(ws *websocket.Conn) {
if ws == nil {
return
}
ws.WriteControl(websocket.CloseMessage,
websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""),
time.Now().Add(time.Second))
ws.Close()
}