mirror of
https://github.com/shtorm-7/sing-box-extended.git
synced 2026-08-07 14:25:17 +03:00
263 lines
8.6 KiB
Go
263 lines
8.6 KiB
Go
package dion
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"net/netip"
|
|
"strings"
|
|
|
|
"github.com/pion/webrtc/v4"
|
|
"github.com/sagernet/sing-box/adapter"
|
|
"github.com/sagernet/sing-box/common/dialer"
|
|
"github.com/sagernet/sing/common/logger"
|
|
N "github.com/sagernet/sing/common/network"
|
|
)
|
|
|
|
type TransceiverPlan struct {
|
|
Mid int
|
|
Direction webrtc.RTPTransceiverDirection
|
|
Kind webrtc.RTPCodecType
|
|
Ctype string
|
|
}
|
|
|
|
type DataChannelPlan struct {
|
|
ID uint16
|
|
Label string
|
|
}
|
|
|
|
var DionTransceiverLayout = []TransceiverPlan{
|
|
{Mid: 0, Direction: webrtc.RTPTransceiverDirectionRecvonly, Kind: webrtc.RTPCodecTypeVideo, Ctype: "Video"},
|
|
{Mid: 1, Direction: webrtc.RTPTransceiverDirectionRecvonly, Kind: webrtc.RTPCodecTypeVideo, Ctype: "Video"},
|
|
{Mid: 2, Direction: webrtc.RTPTransceiverDirectionRecvonly, Kind: webrtc.RTPCodecTypeVideo, Ctype: "Video"},
|
|
{Mid: 3, Direction: webrtc.RTPTransceiverDirectionRecvonly, Kind: webrtc.RTPCodecTypeVideo, Ctype: "Video"},
|
|
{Mid: 4, Direction: webrtc.RTPTransceiverDirectionRecvonly, Kind: webrtc.RTPCodecTypeVideo, Ctype: "Video"},
|
|
{Mid: 5, Direction: webrtc.RTPTransceiverDirectionRecvonly, Kind: webrtc.RTPCodecTypeVideo, Ctype: "Video"},
|
|
{Mid: 6, Direction: webrtc.RTPTransceiverDirectionRecvonly, Kind: webrtc.RTPCodecTypeVideo, Ctype: "Video"},
|
|
{Mid: 7, Direction: webrtc.RTPTransceiverDirectionRecvonly, Kind: webrtc.RTPCodecTypeVideo, Ctype: "Video"},
|
|
{Mid: 8, Direction: webrtc.RTPTransceiverDirectionRecvonly, Kind: webrtc.RTPCodecTypeVideo, Ctype: "Video"},
|
|
{Mid: 9, Direction: webrtc.RTPTransceiverDirectionRecvonly, Kind: webrtc.RTPCodecTypeAudio, Ctype: "Audio"},
|
|
{Mid: 10, Direction: webrtc.RTPTransceiverDirectionSendonly, Kind: webrtc.RTPCodecTypeAudio, Ctype: "Audio"},
|
|
{Mid: 11, Direction: webrtc.RTPTransceiverDirectionSendonly, Kind: webrtc.RTPCodecTypeAudio, Ctype: "AudioScreenSharing"},
|
|
{Mid: 12, Direction: webrtc.RTPTransceiverDirectionSendonly, Kind: webrtc.RTPCodecTypeVideo, Ctype: "Video"},
|
|
{Mid: 13, Direction: webrtc.RTPTransceiverDirectionSendonly, Kind: webrtc.RTPCodecTypeVideo, Ctype: "ScreenSharing"},
|
|
{Mid: 14, Direction: webrtc.RTPTransceiverDirectionRecvonly, Kind: webrtc.RTPCodecTypeVideo, Ctype: "ScreenSharing"},
|
|
{Mid: 15, Direction: webrtc.RTPTransceiverDirectionRecvonly, Kind: webrtc.RTPCodecTypeVideo, Ctype: "Video"},
|
|
{Mid: 16, Direction: webrtc.RTPTransceiverDirectionRecvonly, Kind: webrtc.RTPCodecTypeVideo, Ctype: "Padding"},
|
|
}
|
|
|
|
var DionDataChannels = []DataChannelPlan{
|
|
{ID: 0, Label: "vad"},
|
|
{ID: 1, Label: "stats"},
|
|
{ID: 2, Label: "speed"},
|
|
{ID: 3, Label: "video_quality"},
|
|
{ID: 4, Label: "media_messages"},
|
|
}
|
|
|
|
type PionPeer struct {
|
|
PC *webrtc.PeerConnection
|
|
Transceivers []*webrtc.RTPTransceiver
|
|
DataChannels map[string]*webrtc.DataChannel
|
|
TransceiverDescs []TransceiverDesc
|
|
DatachannelDescs []DataChannelDesc
|
|
}
|
|
|
|
func NewPionAPI(customEngine ...*webrtc.SettingEngine) *webrtc.API {
|
|
mediaEngine := &webrtc.MediaEngine{}
|
|
if err := mediaEngine.RegisterDefaultCodecs(); err != nil {
|
|
panic(fmt.Errorf("dion: register default codecs: %w", err))
|
|
}
|
|
engine := webrtc.SettingEngine{}
|
|
if len(customEngine) > 0 && customEngine[0] != nil {
|
|
engine = *customEngine[0]
|
|
}
|
|
return webrtc.NewAPI(
|
|
webrtc.WithMediaEngine(mediaEngine),
|
|
webrtc.WithSettingEngine(engine),
|
|
)
|
|
}
|
|
|
|
func ResolveICEServerHosts(entries []ICEServerEntry, dnsRouter adapter.DNSRouter, d N.Dialer, logger logger.ContextLogger) []ICEServerEntry {
|
|
if dnsRouter == nil {
|
|
return entries
|
|
}
|
|
resolved := make(map[string]string)
|
|
out := make([]ICEServerEntry, 0, len(entries))
|
|
for _, entry := range entries {
|
|
urls := make([]string, len(entry.URLs))
|
|
copy(urls, entry.URLs)
|
|
for k, raw := range urls {
|
|
host := extractICEHost(raw)
|
|
if host == "" {
|
|
continue
|
|
}
|
|
ip, ok := resolved[host]
|
|
if !ok {
|
|
var addrs []netip.Addr
|
|
var err error
|
|
addrs, err = dnsRouter.Lookup(context.Background(), host, d.(dialer.ResolveDialer).QueryOptions())
|
|
if err != nil {
|
|
logger.Warn(fmt.Sprintf("[dion] resolve ICE host %s failed: %v", host, err))
|
|
continue
|
|
}
|
|
ip = addrs[0].String()
|
|
resolved[host] = ip
|
|
logger.Debug(fmt.Sprintf("[dion] resolved ICE host %s -> %s", host, addrs[0]))
|
|
}
|
|
urls[k] = strings.Replace(raw, host, ip, 1)
|
|
}
|
|
out = append(out, ICEServerEntry{URLs: urls, Username: entry.Username, Credential: entry.Credential})
|
|
}
|
|
return out
|
|
}
|
|
|
|
func IceServerEntriesToWebRTC(entries []ICEServerEntry) []webrtc.ICEServer {
|
|
out := make([]webrtc.ICEServer, 0, len(entries))
|
|
for _, entry := range entries {
|
|
out = append(out, webrtc.ICEServer{
|
|
URLs: entry.URLs,
|
|
Username: entry.Username,
|
|
Credential: entry.Credential,
|
|
})
|
|
}
|
|
return out
|
|
}
|
|
|
|
func BuildPionPeer(api *webrtc.API, iceServers []ICEServerEntry) (*PionPeer, error) {
|
|
pc, err := api.NewPeerConnection(webrtc.Configuration{
|
|
ICEServers: IceServerEntriesToWebRTC(iceServers),
|
|
BundlePolicy: webrtc.BundlePolicyMaxBundle,
|
|
RTCPMuxPolicy: webrtc.RTCPMuxPolicyRequire,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("new peer connection: %w", err)
|
|
}
|
|
transceivers := make([]*webrtc.RTPTransceiver, 0, len(DionTransceiverLayout))
|
|
for _, plan := range DionTransceiverLayout {
|
|
transceiver, err := pc.AddTransceiverFromKind(plan.Kind, webrtc.RTPTransceiverInit{
|
|
Direction: plan.Direction,
|
|
})
|
|
if err != nil {
|
|
pc.Close()
|
|
return nil, fmt.Errorf("add transceiver mid=%d kind=%s dir=%s: %w", plan.Mid, plan.Kind, plan.Direction, err)
|
|
}
|
|
transceivers = append(transceivers, transceiver)
|
|
}
|
|
dataChannels := make(map[string]*webrtc.DataChannel, len(DionDataChannels))
|
|
for _, plan := range DionDataChannels {
|
|
negotiated := true
|
|
id := plan.ID
|
|
dc, err := pc.CreateDataChannel(plan.Label, &webrtc.DataChannelInit{
|
|
Negotiated: &negotiated,
|
|
ID: &id,
|
|
})
|
|
if err != nil {
|
|
pc.Close()
|
|
return nil, fmt.Errorf("create datachannel %s id=%d: %w", plan.Label, plan.ID, err)
|
|
}
|
|
dataChannels[plan.Label] = dc
|
|
}
|
|
dcDescs := make([]DataChannelDesc, 0, len(DionDataChannels))
|
|
for _, plan := range DionDataChannels {
|
|
dcDescs = append(dcDescs, DataChannelDesc{ID: int(plan.ID), Label: plan.Label})
|
|
}
|
|
return &PionPeer{
|
|
PC: pc,
|
|
Transceivers: transceivers,
|
|
DataChannels: dataChannels,
|
|
DatachannelDescs: dcDescs,
|
|
}, nil
|
|
}
|
|
|
|
func (p *PionPeer) BuildOfferDescriptors() error {
|
|
if len(p.Transceivers) != len(DionTransceiverLayout) {
|
|
return fmt.Errorf("transceiver count drift: have %d want %d", len(p.Transceivers), len(DionTransceiverLayout))
|
|
}
|
|
descs := make([]TransceiverDesc, 0, len(p.Transceivers))
|
|
for index, transceiver := range p.Transceivers {
|
|
mid := transceiver.Mid()
|
|
if mid == "" {
|
|
return fmt.Errorf("transceiver index=%d has empty mid; call SetLocalDescription first", index)
|
|
}
|
|
plan := DionTransceiverLayout[index]
|
|
descs = append(descs, TransceiverDesc{
|
|
TransceiverID: mid,
|
|
SessionID: "",
|
|
Direction: directionToDion(plan.Direction),
|
|
Ctype: plan.Ctype,
|
|
})
|
|
}
|
|
p.TransceiverDescs = descs
|
|
return nil
|
|
}
|
|
|
|
func (p *PionPeer) CreateAndSetOffer() (offerEnvelope string, sdpOffer string, err error) {
|
|
offer, err := p.PC.CreateOffer(nil)
|
|
if err != nil {
|
|
return "", "", fmt.Errorf("create offer: %w", err)
|
|
}
|
|
if err := p.PC.SetLocalDescription(offer); err != nil {
|
|
return "", "", fmt.Errorf("set local description: %w", err)
|
|
}
|
|
if err := p.BuildOfferDescriptors(); err != nil {
|
|
return "", "", err
|
|
}
|
|
envelope, err := BuildSDPOfferEnvelope(offer.SDP)
|
|
if err != nil {
|
|
return "", "", fmt.Errorf("build envelope: %w", err)
|
|
}
|
|
return envelope, offer.SDP, nil
|
|
}
|
|
|
|
func (p *PionPeer) ApplyAnswerEnvelope(answerEnvelope string) error {
|
|
sdp, err := DecodeSDPAnswerInner(answerEnvelope)
|
|
if err != nil {
|
|
return fmt.Errorf("decode answer envelope: %w", err)
|
|
}
|
|
return p.PC.SetRemoteDescription(webrtc.SessionDescription{
|
|
Type: webrtc.SDPTypeAnswer,
|
|
SDP: sdp,
|
|
})
|
|
}
|
|
|
|
func (p *PionPeer) Close() error {
|
|
if p.PC == nil {
|
|
return nil
|
|
}
|
|
return p.PC.Close()
|
|
}
|
|
|
|
func directionToDion(direction webrtc.RTPTransceiverDirection) string {
|
|
switch direction {
|
|
case webrtc.RTPTransceiverDirectionSendonly:
|
|
return "SendOnly"
|
|
case webrtc.RTPTransceiverDirectionRecvonly:
|
|
return "RecvOnly"
|
|
case webrtc.RTPTransceiverDirectionSendrecv:
|
|
return "SendRecv"
|
|
case webrtc.RTPTransceiverDirectionInactive:
|
|
return "Inactive"
|
|
}
|
|
return "Unknown"
|
|
}
|
|
|
|
func extractICEHost(raw string) string {
|
|
value := raw
|
|
for _, prefix := range []string{"stun:", "turn:", "turns:"} {
|
|
value = strings.TrimPrefix(value, prefix)
|
|
}
|
|
if idx := strings.Index(value, "?"); idx >= 0 {
|
|
value = value[:idx]
|
|
}
|
|
if idx := strings.LastIndex(value, ":"); idx >= 0 {
|
|
value = value[:idx]
|
|
}
|
|
if value == "" {
|
|
return ""
|
|
}
|
|
if net.ParseIP(value) != nil {
|
|
return ""
|
|
}
|
|
return value
|
|
}
|