mirror of
https://github.com/shtorm-7/sing-box-extended.git
synced 2026-08-07 14:25:17 +03:00
Add call protocol, Rmux. Update AmneziaWG. Fixes and improvements
This commit is contained in:
8
transport/call/common/buffers.go
Normal file
8
transport/call/common/buffers.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package common
|
||||
|
||||
const (
|
||||
UDPBufSize = 4096
|
||||
RTPBufSize = 65536
|
||||
VP8BufSize = 1126
|
||||
DCBufSize = 32768
|
||||
)
|
||||
15
transport/call/common/deps.go
Normal file
15
transport/call/common/deps.go
Normal 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)
|
||||
87
transport/call/common/http.go
Normal file
87
transport/call/common/http.go
Normal 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, "; ")
|
||||
}
|
||||
58
transport/call/common/ice.go
Normal file
58
transport/call/common/ice.go
Normal 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
|
||||
}
|
||||
40
transport/call/common/jitter.go
Normal file
40
transport/call/common/jitter.go
Normal 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)
|
||||
}
|
||||
68
transport/call/common/mask.go
Normal file
68
transport/call/common/mask.go
Normal 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]) + "***"
|
||||
}
|
||||
89
transport/call/common/signaling.go
Normal file
89
transport/call/common/signaling.go
Normal 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
|
||||
}
|
||||
}
|
||||
17
transport/call/common/ws.go
Normal file
17
transport/call/common/ws.go
Normal 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()
|
||||
}
|
||||
Reference in New Issue
Block a user