fix
This commit is contained in:
parent
89496b3558
commit
406a8805d8
5 changed files with 139 additions and 9 deletions
|
|
@ -644,6 +644,7 @@ func run(logger *zap.Logger) error {
|
||||||
t, err := turnsrv.New(turnsrv.Config{
|
t, err := turnsrv.New(turnsrv.Config{
|
||||||
UDPPort: cfg.TURNUDPPort,
|
UDPPort: cfg.TURNUDPPort,
|
||||||
AdvertiseIP: turnAdvertise,
|
AdvertiseIP: turnAdvertise,
|
||||||
|
ExtraIPs: cfg.TURNExtraIPs,
|
||||||
SharedSecret: cfg.TURNSecret,
|
SharedSecret: cfg.TURNSecret,
|
||||||
RelayMinPort: cfg.TURNRelayMinPort,
|
RelayMinPort: cfg.TURNRelayMinPort,
|
||||||
RelayMaxPort: cfg.TURNRelayMaxPort,
|
RelayMaxPort: cfg.TURNRelayMaxPort,
|
||||||
|
|
|
||||||
|
|
@ -353,6 +353,10 @@ type Config struct {
|
||||||
// TURNAdvertiseIP 是写进 phoneConnectionWebrtc 与 relay 分配的客户端可达
|
// TURNAdvertiseIP 是写进 phoneConnectionWebrtc 与 relay 分配的客户端可达
|
||||||
// 地址,默认回落 SFUAdvertiseIP → AdvertiseIP。
|
// 地址,默认回落 SFUAdvertiseIP → AdvertiseIP。
|
||||||
TURNAdvertiseIP string
|
TURNAdvertiseIP string
|
||||||
|
// TURNExtraIPs 是额外下发的 TURN/STUN 候选 IP(逗号分隔)。当客户端与服务器
|
||||||
|
// 同处一个局域网、AdvertiseIP 又是公网 IP 时,把 LAN IP(如 192.168.x.x)列进
|
||||||
|
// 这里,可修复 LAN 端发起通话「Failed to connect」(NAT hairpin)。详见 turnsrv.Config.ExtraIPs。
|
||||||
|
TURNExtraIPs []string
|
||||||
// TURNSecret 是 TURN REST 凭据 HMAC 密钥;为空则进程级随机(单实例自洽,
|
// TURNSecret 是 TURN REST 凭据 HMAC 密钥;为空则进程级随机(单实例自洽,
|
||||||
// 多实例/外部 coturn 必须显式配置同一值)。
|
// 多实例/外部 coturn 必须显式配置同一值)。
|
||||||
TURNSecret string
|
TURNSecret string
|
||||||
|
|
@ -607,6 +611,7 @@ func Load() (Config, error) {
|
||||||
TURNEnable: envBoolOr("TELESRV_TURN_ENABLE", true),
|
TURNEnable: envBoolOr("TELESRV_TURN_ENABLE", true),
|
||||||
TURNUDPPort: envIntOr("TELESRV_TURN_UDP_PORT", 12400),
|
TURNUDPPort: envIntOr("TELESRV_TURN_UDP_PORT", 12400),
|
||||||
TURNAdvertiseIP: envOr("TELESRV_TURN_ADVERTISE_IP", ""),
|
TURNAdvertiseIP: envOr("TELESRV_TURN_ADVERTISE_IP", ""),
|
||||||
|
TURNExtraIPs: envListOr("TELESRV_TURN_EXTRA_IPS", nil),
|
||||||
TURNSecret: envOr("TELESRV_TURN_SECRET", ""),
|
TURNSecret: envOr("TELESRV_TURN_SECRET", ""),
|
||||||
TURNRelayMinPort: envIntOr("TELESRV_TURN_RELAY_MIN_PORT", 12500),
|
TURNRelayMinPort: envIntOr("TELESRV_TURN_RELAY_MIN_PORT", 12500),
|
||||||
TURNRelayMaxPort: envIntOr("TELESRV_TURN_RELAY_MAX_PORT", 12999),
|
TURNRelayMaxPort: envIntOr("TELESRV_TURN_RELAY_MAX_PORT", 12999),
|
||||||
|
|
|
||||||
|
|
@ -50,8 +50,22 @@ func (r *Router) phoneCallConnections(callerID int64) []domain.PhoneCallConnecti
|
||||||
// stun flag——单条目 stun+turn 在 Android 上只会产出 TURN server、丢失 STUN
|
// stun flag——单条目 stun+turn 在 Android 上只会产出 TURN server、丢失 STUN
|
||||||
//(org_telegram_messenger_voip_Instance.cpp:848-884)。TDesktop 两种写法都认。
|
//(org_telegram_messenger_voip_Instance.cpp:848-884)。TDesktop 两种写法都认。
|
||||||
// TURN username 是 REST 格式 "<expiry>:<uid>",天然避开 "reflector" 劫持禁区。
|
// TURN username 是 REST 格式 "<expiry>:<uid>",天然避开 "reflector" 劫持禁区。
|
||||||
return []domain.PhoneCallConnection{
|
//
|
||||||
{ID: 1, IP: t.IP(), Port: t.Port(), Stun: true},
|
// 每个可达 IP(AdvertiseIP + ExtraIPs)都下发一对 STUN/TURN 候选,ICE 逐一
|
||||||
{ID: 2, IP: t.IP(), Port: t.Port(), Username: username, Password: password, Turn: true},
|
// 尝试并选可达者:LAN 客户端走 LAN IP、外网客户端走公网 IP。id 必须全局唯一
|
||||||
|
// 且从 1 递增(DrKLO 用 id 做 reflector 映射)。凭据与 IP 无关(TURN REST 只
|
||||||
|
// 校验 HMAC),同一份 username/password 对所有 IP 有效。
|
||||||
|
ips := append([]string{t.IP()}, t.ExtraIPs()...)
|
||||||
|
conns := make([]domain.PhoneCallConnection, 0, len(ips)*2)
|
||||||
|
id := int64(1)
|
||||||
|
for _, ip := range ips {
|
||||||
|
if ip == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
conns = append(conns, domain.PhoneCallConnection{ID: id, IP: ip, Port: t.Port(), Stun: true})
|
||||||
|
id++
|
||||||
|
conns = append(conns, domain.PhoneCallConnection{ID: id, IP: ip, Port: t.Port(), Username: username, Password: password, Turn: true})
|
||||||
|
id++
|
||||||
}
|
}
|
||||||
|
return conns
|
||||||
}
|
}
|
||||||
|
|
|
||||||
98
internal/rpc/phone_turn_test.go
Normal file
98
internal/rpc/phone_turn_test.go
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
package rpc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/iamxvbaba/td/clock"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// fakeTURN is a minimal turnsrv.Service for exercising phoneCallConnections.
|
||||||
|
type fakeTURN struct {
|
||||||
|
ip string
|
||||||
|
port int
|
||||||
|
extra []string
|
||||||
|
enabled bool
|
||||||
|
credUser string
|
||||||
|
credPass string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeTURN) Enabled() bool { return f.enabled }
|
||||||
|
func (f *fakeTURN) Credentials(string) (string, string, error) {
|
||||||
|
return f.credUser, f.credPass, nil
|
||||||
|
}
|
||||||
|
func (f *fakeTURN) IP() string { return f.ip }
|
||||||
|
func (f *fakeTURN) Port() int { return f.port }
|
||||||
|
func (f *fakeTURN) ExtraIPs() []string { return f.extra }
|
||||||
|
func (f *fakeTURN) Close() error { return nil }
|
||||||
|
|
||||||
|
func newTURNRouter(t *testing.T, turn *fakeTURN) *Router {
|
||||||
|
t.Helper()
|
||||||
|
return New(Config{}, Deps{TURN: turn}, zap.NewNop(), clock.System)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPhoneCallConnectionsDisabledTURNReturnsNil(t *testing.T) {
|
||||||
|
r := newTURNRouter(t, &fakeTURN{enabled: false})
|
||||||
|
if conns := r.phoneCallConnections(1); conns != nil {
|
||||||
|
t.Fatalf("disabled TURN: want nil, got %+v", conns)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPhoneCallConnectionsSingleIP(t *testing.T) {
|
||||||
|
r := newTURNRouter(t, &fakeTURN{
|
||||||
|
enabled: true, ip: "89.28.58.29", port: 12400,
|
||||||
|
credUser: "u", credPass: "p",
|
||||||
|
})
|
||||||
|
conns := r.phoneCallConnections(1)
|
||||||
|
if len(conns) != 2 {
|
||||||
|
t.Fatalf("single IP: want 2 conns (stun+turn), got %d: %+v", len(conns), conns)
|
||||||
|
}
|
||||||
|
if !conns[0].Stun || conns[0].Turn || conns[0].ID != 1 || conns[0].IP != "89.28.58.29" {
|
||||||
|
t.Fatalf("conn[0] should be STUN id1 on public IP, got %+v", conns[0])
|
||||||
|
}
|
||||||
|
if !conns[1].Turn || conns[1].Stun || conns[1].ID != 2 || conns[1].Username != "u" || conns[1].Password != "p" {
|
||||||
|
t.Fatalf("conn[1] should be TURN id2 with creds, got %+v", conns[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPhoneCallConnectionsExtraIPsAddCandidatesWithUniqueIDs(t *testing.T) {
|
||||||
|
r := newTURNRouter(t, &fakeTURN{
|
||||||
|
enabled: true, ip: "89.28.58.29", port: 12400,
|
||||||
|
extra: []string{"192.168.0.20", ""}, // empty entry must be skipped
|
||||||
|
credUser: "u", credPass: "p",
|
||||||
|
})
|
||||||
|
conns := r.phoneCallConnections(1)
|
||||||
|
// public (stun+turn) + LAN (stun+turn) = 4; the empty extra IP is skipped.
|
||||||
|
if len(conns) != 4 {
|
||||||
|
t.Fatalf("want 4 conns, got %d: %+v", len(conns), conns)
|
||||||
|
}
|
||||||
|
seenIDs := map[int64]bool{}
|
||||||
|
for _, c := range conns {
|
||||||
|
if seenIDs[c.ID] {
|
||||||
|
t.Fatalf("duplicate connection id %d: %+v", c.ID, conns)
|
||||||
|
}
|
||||||
|
seenIDs[c.ID] = true
|
||||||
|
if c.IP == "" {
|
||||||
|
t.Fatalf("empty IP leaked into connections: %+v", conns)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// IDs must be a contiguous 1..4 run (DrKLO maps reflectors by id).
|
||||||
|
for id := int64(1); id <= 4; id++ {
|
||||||
|
if !seenIDs[id] {
|
||||||
|
t.Fatalf("missing contiguous id %d: %+v", id, conns)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// The LAN IP must appear as both a STUN and a TURN candidate.
|
||||||
|
var lanStun, lanTurn bool
|
||||||
|
for _, c := range conns {
|
||||||
|
if c.IP == "192.168.0.20" && c.Stun {
|
||||||
|
lanStun = true
|
||||||
|
}
|
||||||
|
if c.IP == "192.168.0.20" && c.Turn {
|
||||||
|
lanTurn = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !lanStun || !lanTurn {
|
||||||
|
t.Fatalf("LAN IP must yield both STUN and TURN candidates, got %+v", conns)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -29,6 +29,14 @@ type Config struct {
|
||||||
// AdvertiseIP 是写进 phoneConnectionWebrtc 与 relay 分配地址的客户端可达
|
// AdvertiseIP 是写进 phoneConnectionWebrtc 与 relay 分配地址的客户端可达
|
||||||
// 地址。⚠ 127.0.0.1 时真机拿到的 relay candidate 不可达(媒体面静默失败)。
|
// 地址。⚠ 127.0.0.1 时真机拿到的 relay candidate 不可达(媒体面静默失败)。
|
||||||
AdvertiseIP string
|
AdvertiseIP string
|
||||||
|
// ExtraIPs 是额外下发的 TURN/STUN 候选地址(除 AdvertiseIP 外)。典型用途:
|
||||||
|
// 当客户端与服务器同处一个局域网、而 AdvertiseIP 是公网 IP 时,路由器多半不
|
||||||
|
// 支持 NAT 回环(hairpin),LAN 内的客户端无法经公网 IP 触达 TURN 控制通道
|
||||||
|
//(现象:LAN 端发起的通话「Failed to connect」,外网端发起的却正常)。把 LAN
|
||||||
|
// IP(如 192.168.x.x)列进这里,ICE 会一并尝试,LAN 客户端走 LAN 候选、外网
|
||||||
|
// 客户端走公网候选。relay 分配地址仍是 AdvertiseIP(LAN-LAN 通话用 host
|
||||||
|
// 候选直连、不经 relay,故公网 relay 地址不成问题)。
|
||||||
|
ExtraIPs []string
|
||||||
// Realm 是 TURN long-term credential 的 realm(任意稳定串即可)。
|
// Realm 是 TURN long-term credential 的 realm(任意稳定串即可)。
|
||||||
Realm string
|
Realm string
|
||||||
// SharedSecret 是 REST 凭据的 HMAC 密钥;为空则进程级随机生成
|
// SharedSecret 是 REST 凭据的 HMAC 密钥;为空则进程级随机生成
|
||||||
|
|
@ -54,6 +62,8 @@ type Service interface {
|
||||||
// IP/Port 返回客户端可达的服务地址。
|
// IP/Port 返回客户端可达的服务地址。
|
||||||
IP() string
|
IP() string
|
||||||
Port() int
|
Port() int
|
||||||
|
// ExtraIPs 返回除 IP() 外额外下发的候选地址(可空)。
|
||||||
|
ExtraIPs() []string
|
||||||
Close() error
|
Close() error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -66,9 +76,10 @@ func (disabled) Enabled() bool { return false }
|
||||||
func (disabled) Credentials(string) (string, string, error) {
|
func (disabled) Credentials(string) (string, string, error) {
|
||||||
return "", "", fmt.Errorf("turnsrv: disabled")
|
return "", "", fmt.Errorf("turnsrv: disabled")
|
||||||
}
|
}
|
||||||
func (disabled) IP() string { return "" }
|
func (disabled) IP() string { return "" }
|
||||||
func (disabled) Port() int { return 0 }
|
func (disabled) Port() int { return 0 }
|
||||||
func (disabled) Close() error { return nil }
|
func (disabled) ExtraIPs() []string { return nil }
|
||||||
|
func (disabled) Close() error { return nil }
|
||||||
|
|
||||||
type pionTURN struct {
|
type pionTURN struct {
|
||||||
cfg Config
|
cfg Config
|
||||||
|
|
@ -150,9 +161,10 @@ func (t *pionTURN) Credentials(user string) (string, string, error) {
|
||||||
return username, password, nil
|
return username, password, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *pionTURN) IP() string { return t.cfg.AdvertiseIP }
|
func (t *pionTURN) IP() string { return t.cfg.AdvertiseIP }
|
||||||
func (t *pionTURN) Port() int { return t.cfg.UDPPort }
|
func (t *pionTURN) Port() int { return t.cfg.UDPPort }
|
||||||
func (t *pionTURN) Close() error { return t.server.Close() }
|
func (t *pionTURN) ExtraIPs() []string { return t.cfg.ExtraIPs }
|
||||||
|
func (t *pionTURN) Close() error { return t.server.Close() }
|
||||||
|
|
||||||
func randomSecret() (string, error) {
|
func randomSecret() (string, error) {
|
||||||
buf := make([]byte, 24)
|
buf := make([]byte, 24)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue