provider.go 37 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228
  1. package ims
  2. import (
  3. "bufio"
  4. "context"
  5. "crypto/rand"
  6. "encoding/base64"
  7. "encoding/hex"
  8. "errors"
  9. "fmt"
  10. "net"
  11. "strconv"
  12. "strings"
  13. "sync"
  14. "time"
  15. "vocat/internal/vowifi"
  16. )
  17. const (
  18. defaultSIPPort = 5060
  19. defaultRegistrationExpiry = 3600 * time.Second
  20. defaultTransactionTimeout = 12 * time.Second
  21. maxAuthenticationChallenges = 3
  22. )
  23. var (
  24. ErrRegistrationRejected = errors.New("ims: SIP registration was rejected")
  25. ErrSessionClosed = errors.New("ims: session is closed")
  26. ErrRegistrationExpired = errors.New("ims: registration has expired")
  27. ErrSMSCapabilityNotConfirmed = errors.New("ims: registrar did not confirm the +g.3gpp.smsip contact")
  28. )
  29. // Config defines only deployment-specific SIP details. If PCSCF or
  30. // LocalAddress is empty, Provider uses the corresponding value proven by the
  31. // TunnelSession. The default transport is TCP and the default port is 5060.
  32. type Config struct {
  33. PCSCF string
  34. LocalAddress string
  35. Transport string
  36. Port int
  37. RegistrationExpiry time.Duration
  38. TransactionTimeout time.Duration
  39. PrivateIdentity string
  40. PublicIdentity string
  41. UserAgent string
  42. SecurityMode SecurityMode
  43. IPSecInstaller IPSecSAInstaller
  44. ProtectedClientPort int
  45. ProtectedServerPort int
  46. // SMSCenter is an operator-provided fallback when the SIM leaves EF_SMSP
  47. // and AT+CSCA empty. It must be an international or national digit string.
  48. SMSCenter string
  49. // OnSMS is invoked after a valid inbound RP-DATA/SMS-DELIVER has been
  50. // decoded. Returning an error causes an RP-ERROR delivery report.
  51. OnSMS func(context.Context, ReceivedSMS) error
  52. // OnSMSStatus is invoked for an SMS-STATUS-REPORT received after a
  53. // submission that requested a delivery report.
  54. OnSMSStatus func(context.Context, ReceivedSMSStatus) error
  55. }
  56. // Provider implements vowifi.IMSProvider using a small RFC 3261 REGISTER
  57. // transaction and 3GPP AKAv1-MD5 authentication. It has no SIP stack or
  58. // runtime dependency outside the Go standard library.
  59. type Provider struct {
  60. aka vowifi.AKAProvider
  61. config Config
  62. installer IPSecSAInstaller
  63. }
  64. func NewProvider(aka vowifi.AKAProvider, config Config) (*Provider, error) {
  65. if aka == nil {
  66. return nil, errors.New("ims: AKA provider is required")
  67. }
  68. normalized, err := normalizeConfig(config)
  69. if err != nil {
  70. return nil, err
  71. }
  72. installer := normalized.IPSecInstaller
  73. if installer == nil {
  74. installer = defaultIPSecInstaller()
  75. }
  76. return &Provider{aka: aka, config: normalized, installer: installer}, nil
  77. }
  78. func normalizeConfig(config Config) (Config, error) {
  79. if config.Port == 0 {
  80. config.Port = defaultSIPPort
  81. }
  82. if config.Port < 1 || config.Port > 65535 {
  83. return Config{}, errors.New("ims: SIP port is out of range")
  84. }
  85. if config.RegistrationExpiry == 0 {
  86. config.RegistrationExpiry = defaultRegistrationExpiry
  87. }
  88. if config.RegistrationExpiry < time.Minute || config.RegistrationExpiry > 24*time.Hour {
  89. return Config{}, errors.New("ims: registration expiry must be between one minute and 24 hours")
  90. }
  91. if config.TransactionTimeout == 0 {
  92. config.TransactionTimeout = defaultTransactionTimeout
  93. }
  94. if config.TransactionTimeout < time.Second || config.TransactionTimeout > time.Minute {
  95. return Config{}, errors.New("ims: transaction timeout must be between one second and one minute")
  96. }
  97. config.Transport = strings.ToLower(strings.TrimSpace(config.Transport))
  98. if config.Transport != "" && config.Transport != "udp" && config.Transport != "tcp" {
  99. return Config{}, fmt.Errorf("ims: unsupported SIP transport %q", config.Transport)
  100. }
  101. if strings.TrimSpace(config.UserAgent) == "" {
  102. config.UserAgent = "vocat/1"
  103. }
  104. if config.SecurityMode == "" {
  105. config.SecurityMode = SecurityRequired
  106. }
  107. switch config.SecurityMode {
  108. case SecurityRequired, SecurityOptional, SecurityDisabled:
  109. default:
  110. return Config{}, fmt.Errorf("ims: unsupported security mode %q", config.SecurityMode)
  111. }
  112. if config.ProtectedClientPort != 0 && !validProtectedPort(config.ProtectedClientPort) {
  113. return Config{}, errors.New("ims: protected client port is invalid")
  114. }
  115. if config.ProtectedServerPort != 0 && !validProtectedPort(config.ProtectedServerPort) {
  116. return Config{}, errors.New("ims: protected server port is invalid")
  117. }
  118. if config.ProtectedClientPort != 0 &&
  119. config.ProtectedClientPort == config.ProtectedServerPort {
  120. return Config{}, errors.New("ims: protected client and server ports must differ")
  121. }
  122. for name, value := range map[string]string{
  123. "PCSCF": config.PCSCF, "local address": config.LocalAddress,
  124. "private identity": config.PrivateIdentity, "public identity": config.PublicIdentity,
  125. "user agent": config.UserAgent,
  126. } {
  127. if strings.ContainsAny(value, "\r\n") {
  128. return Config{}, fmt.Errorf("ims: %s contains a line break", name)
  129. }
  130. }
  131. config.PCSCF = strings.TrimSpace(config.PCSCF)
  132. config.LocalAddress = strings.TrimSpace(config.LocalAddress)
  133. config.PrivateIdentity = strings.TrimSpace(config.PrivateIdentity)
  134. config.PublicIdentity = strings.TrimSpace(config.PublicIdentity)
  135. config.UserAgent = strings.TrimSpace(config.UserAgent)
  136. config.SMSCenter = strings.TrimSpace(config.SMSCenter)
  137. if config.SMSCenter != "" {
  138. digits := strings.TrimPrefix(config.SMSCenter, "+")
  139. if !digitsBetween(digits, 3, 20) {
  140. return Config{}, errors.New("ims: configured SMS service-centre address is invalid")
  141. }
  142. }
  143. return config, nil
  144. }
  145. func (provider *Provider) Start(ctx context.Context, request vowifi.IMSRequest) (vowifi.IMSSession, error) {
  146. if ctx == nil {
  147. ctx = context.Background()
  148. }
  149. if request.Tunnel == nil {
  150. return nil, errors.New("ims: tunnel session is required")
  151. }
  152. tunnel := request.Tunnel.Evidence()
  153. if !tunnel.Established {
  154. return nil, vowifi.ErrTunnelNotEstablished
  155. }
  156. identities, err := deriveIdentities(request.Identity, provider.config)
  157. if err != nil {
  158. return nil, err
  159. }
  160. pcscf := provider.config.PCSCF
  161. if pcscf == "" {
  162. for _, candidate := range tunnel.PCSCF {
  163. if strings.TrimSpace(candidate) != "" {
  164. pcscf = candidate
  165. break
  166. }
  167. }
  168. }
  169. if pcscf == "" {
  170. return nil, errors.New("ims: tunnel did not provide a P-CSCF")
  171. }
  172. endpoint, transportHint, err := parsePCSCF(pcscf, provider.config.Port)
  173. if err != nil {
  174. return nil, err
  175. }
  176. if provider.config.PCSCF != "" && !pcscfProvenByTunnel(endpoint, tunnel.PCSCF, provider.config.Port) {
  177. return nil, errors.New("ims: configured P-CSCF is not proven by the SWu tunnel")
  178. }
  179. transport := provider.config.Transport
  180. if transport == "" {
  181. transport = transportHint
  182. }
  183. if transport == "" {
  184. transport = "tcp"
  185. }
  186. localAddress := provider.config.LocalAddress
  187. if localAddress == "" {
  188. if endpointIP := net.ParseIP(endpoint.host); endpointIP != nil && endpointIP.To4() == nil {
  189. localAddress = tunnel.LocalIPv6
  190. } else {
  191. localAddress = tunnel.LocalIPv4
  192. if strings.TrimSpace(localAddress) == "" {
  193. localAddress = tunnel.LocalIPv6
  194. }
  195. }
  196. }
  197. localAddress = strings.TrimSpace(strings.Split(localAddress, "/")[0])
  198. if localAddress == "" {
  199. return nil, errors.New("ims: tunnel did not provide a local address")
  200. }
  201. if !localAddressProvenByTunnel(localAddress, tunnel) {
  202. return nil, errors.New("ims: configured local address is not assigned by the SWu tunnel")
  203. }
  204. connection, err := dialSIP(ctx, transport, localAddress, 0, endpoint.address())
  205. if err != nil {
  206. return nil, fmt.Errorf("ims: connect to P-CSCF: %w", err)
  207. }
  208. session, err := newSession(provider, request, identities, endpoint, transport, connection)
  209. if err != nil {
  210. _ = connection.Close()
  211. return nil, err
  212. }
  213. if err := session.establish(ctx); err != nil {
  214. session.abort()
  215. return nil, err
  216. }
  217. return session, nil
  218. }
  219. type identitySet struct {
  220. domain string
  221. private string
  222. public string
  223. user string
  224. }
  225. func deriveIdentities(identity vowifi.SIMIdentity, config Config) (identitySet, error) {
  226. imsi := strings.TrimSpace(identity.IMSI)
  227. if !digitsBetween(imsi, 5, 16) {
  228. return identitySet{}, errors.New("ims: SIM IMSI is unavailable or invalid")
  229. }
  230. mcc := strings.TrimSpace(identity.HomeMCC)
  231. mnc := strings.TrimSpace(identity.HomeMNC)
  232. if !digitsBetween(mcc, 3, 3) || !digitsBetween(mnc, 2, 3) {
  233. return identitySet{}, errors.New("ims: home PLMN is unavailable or invalid")
  234. }
  235. for len(mnc) < 3 {
  236. mnc = "0" + mnc
  237. }
  238. domain := fmt.Sprintf("ims.mnc%s.mcc%s.3gppnetwork.org", mnc, mcc)
  239. privateIdentity := config.PrivateIdentity
  240. if privateIdentity == "" {
  241. privateIdentity = imsi + "@" + domain
  242. }
  243. publicIdentity := config.PublicIdentity
  244. if publicIdentity == "" {
  245. publicIdentity = "sip:" + imsi + "@" + domain
  246. }
  247. if strings.ContainsAny(privateIdentity+publicIdentity, "\r\n") ||
  248. !strings.Contains(privateIdentity, "@") ||
  249. (!strings.HasPrefix(strings.ToLower(publicIdentity), "sip:") &&
  250. !strings.HasPrefix(strings.ToLower(publicIdentity), "sips:")) {
  251. return identitySet{}, errors.New("ims: configured IMS identity is invalid")
  252. }
  253. user := strings.TrimPrefix(strings.TrimPrefix(publicIdentity, "sip:"), "sips:")
  254. if at := strings.IndexByte(user, '@'); at >= 0 {
  255. user = user[:at]
  256. }
  257. if user == "" || strings.ContainsAny(user, "<>\" \t;") {
  258. return identitySet{}, errors.New("ims: public identity user is invalid")
  259. }
  260. return identitySet{domain: domain, private: privateIdentity, public: publicIdentity, user: user}, nil
  261. }
  262. type pcscfEndpoint struct {
  263. host string
  264. port int
  265. }
  266. func (endpoint pcscfEndpoint) address() string {
  267. return net.JoinHostPort(endpoint.host, strconv.Itoa(endpoint.port))
  268. }
  269. func parsePCSCF(raw string, defaultPort int) (pcscfEndpoint, string, error) {
  270. value := strings.Trim(strings.TrimSpace(raw), "<>")
  271. lower := strings.ToLower(value)
  272. if strings.HasPrefix(lower, "sip:") {
  273. value = value[4:]
  274. } else if strings.HasPrefix(lower, "sips:") {
  275. value = value[5:]
  276. }
  277. transport := ""
  278. if separator := strings.IndexAny(value, ";?"); separator >= 0 {
  279. parameters := value[separator+1:]
  280. value = value[:separator]
  281. for _, parameter := range strings.FieldsFunc(parameters, func(character rune) bool {
  282. return character == ';' || character == '&'
  283. }) {
  284. key, parameterValue, found := strings.Cut(parameter, "=")
  285. if found && strings.EqualFold(strings.TrimSpace(key), "transport") {
  286. transport = strings.ToLower(strings.TrimSpace(parameterValue))
  287. }
  288. }
  289. }
  290. if at := strings.LastIndexByte(value, '@'); at >= 0 {
  291. value = value[at+1:]
  292. }
  293. value = strings.TrimSpace(value)
  294. if value == "" || strings.ContainsAny(value, "\r\n/") {
  295. return pcscfEndpoint{}, "", errors.New("ims: invalid P-CSCF address")
  296. }
  297. host := value
  298. port := defaultPort
  299. if parsedHost, parsedPort, err := net.SplitHostPort(value); err == nil {
  300. host = parsedHost
  301. numericPort, parseErr := strconv.Atoi(parsedPort)
  302. if parseErr != nil || numericPort < 1 || numericPort > 65535 {
  303. return pcscfEndpoint{}, "", errors.New("ims: invalid P-CSCF port")
  304. }
  305. port = numericPort
  306. } else if strings.HasPrefix(value, "[") && strings.HasSuffix(value, "]") {
  307. host = strings.Trim(value, "[]")
  308. } else if strings.Count(value, ":") == 1 {
  309. candidateHost, candidatePort, found := strings.Cut(value, ":")
  310. if found {
  311. numericPort, parseErr := strconv.Atoi(candidatePort)
  312. if parseErr != nil || numericPort < 1 || numericPort > 65535 {
  313. return pcscfEndpoint{}, "", errors.New("ims: invalid P-CSCF port")
  314. }
  315. host = candidateHost
  316. port = numericPort
  317. }
  318. }
  319. host = strings.Trim(strings.TrimSpace(host), "[]")
  320. if host == "" || strings.ContainsAny(host, " \t<>\"") {
  321. return pcscfEndpoint{}, "", errors.New("ims: invalid P-CSCF host")
  322. }
  323. if transport != "" && transport != "udp" && transport != "tcp" {
  324. return pcscfEndpoint{}, "", fmt.Errorf("ims: unsupported P-CSCF transport %q", transport)
  325. }
  326. return pcscfEndpoint{host: host, port: port}, transport, nil
  327. }
  328. func pcscfProvenByTunnel(endpoint pcscfEndpoint, candidates []string, defaultPort int) bool {
  329. for _, candidate := range candidates {
  330. proven, _, err := parsePCSCF(candidate, defaultPort)
  331. if err != nil {
  332. continue
  333. }
  334. if proven.port == endpoint.port && equalHost(proven.host, endpoint.host) {
  335. return true
  336. }
  337. }
  338. return false
  339. }
  340. func equalHost(left string, right string) bool {
  341. leftIP := net.ParseIP(strings.Trim(left, "[]"))
  342. rightIP := net.ParseIP(strings.Trim(right, "[]"))
  343. if leftIP != nil || rightIP != nil {
  344. return leftIP != nil && rightIP != nil && leftIP.Equal(rightIP)
  345. }
  346. return strings.EqualFold(strings.TrimSpace(left), strings.TrimSpace(right))
  347. }
  348. func localAddressProvenByTunnel(localAddress string, tunnel vowifi.TunnelEvidence) bool {
  349. localIP := net.ParseIP(strings.Trim(localAddress, "[]"))
  350. if localIP == nil {
  351. return false
  352. }
  353. for _, candidate := range []string{tunnel.LocalIPv4, tunnel.LocalIPv6} {
  354. candidate = strings.TrimSpace(strings.Split(candidate, "/")[0])
  355. candidateIP := net.ParseIP(strings.Trim(candidate, "[]"))
  356. if candidateIP != nil && localIP.Equal(candidateIP) {
  357. return true
  358. }
  359. }
  360. return false
  361. }
  362. func dialSIP(
  363. ctx context.Context,
  364. transport string,
  365. localAddress string,
  366. localPort int,
  367. remoteAddress string,
  368. ) (net.Conn, error) {
  369. var local net.Addr
  370. var err error
  371. switch transport {
  372. case "udp":
  373. local, err = net.ResolveUDPAddr("udp", net.JoinHostPort(localAddress, strconv.Itoa(localPort)))
  374. case "tcp":
  375. local, err = net.ResolveTCPAddr("tcp", net.JoinHostPort(localAddress, strconv.Itoa(localPort)))
  376. default:
  377. return nil, fmt.Errorf("ims: unsupported SIP transport %q", transport)
  378. }
  379. if err != nil {
  380. return nil, fmt.Errorf("resolve tunnel local address: %w", err)
  381. }
  382. dialer := net.Dialer{LocalAddr: local}
  383. return dialer.DialContext(ctx, transport, remoteAddress)
  384. }
  385. type authenticationState struct {
  386. challenge digestChallenge
  387. password []byte
  388. auts string
  389. cnonce string
  390. nc uint32
  391. }
  392. type Session struct {
  393. provider *Provider
  394. request vowifi.IMSRequest
  395. identity identitySet
  396. endpoint pcscfEndpoint
  397. transport string
  398. conn net.Conn
  399. reader *bufio.Reader
  400. initialEndpoint pcscfEndpoint
  401. securityDeclined bool
  402. callID string
  403. fromTag string
  404. instanceID string
  405. cseq uint32
  406. auth *authenticationState
  407. securityProposal securityProposal
  408. securityAgreement securityAgreement
  409. securityActive bool
  410. ipsecHandle IPSecSAHandle
  411. protectedTCP *net.TCPListener
  412. protectedUDP *net.UDPConn
  413. failures chan error
  414. failureOnce sync.Once
  415. writeMu sync.Mutex
  416. transactionsMu sync.Mutex
  417. transactions map[sipTransactionKey]chan *sipResponse
  418. runtimeStarted bool
  419. receiveDone sync.WaitGroup
  420. inboundMu sync.Mutex
  421. inboundConnections map[net.Conn]struct{}
  422. smsMu sync.Mutex
  423. nextRPReference byte
  424. mu sync.Mutex
  425. closed bool
  426. evidence vowifi.IMSEvidence
  427. smsContactConfirmed bool
  428. expiresAt time.Time
  429. refreshContext context.Context
  430. refreshCancel context.CancelFunc
  431. refreshDone chan struct{}
  432. }
  433. func newSession(
  434. provider *Provider,
  435. request vowifi.IMSRequest,
  436. identity identitySet,
  437. endpoint pcscfEndpoint,
  438. transport string,
  439. connection net.Conn,
  440. ) (*Session, error) {
  441. callToken, err := randomHex(18)
  442. if err != nil {
  443. return nil, err
  444. }
  445. fromTag, err := randomHex(8)
  446. if err != nil {
  447. return nil, err
  448. }
  449. instanceID, err := randomUUID()
  450. if err != nil {
  451. return nil, err
  452. }
  453. refreshContext, refreshCancel := context.WithCancel(context.Background())
  454. session := &Session{
  455. provider: provider,
  456. request: request,
  457. identity: identity,
  458. endpoint: endpoint,
  459. initialEndpoint: endpoint,
  460. transport: transport,
  461. conn: connection,
  462. callID: callToken + "@" + addressHost(connection.LocalAddr()),
  463. fromTag: fromTag,
  464. instanceID: "urn:uuid:" + instanceID,
  465. cseq: 1,
  466. refreshContext: refreshContext,
  467. refreshCancel: refreshCancel,
  468. refreshDone: make(chan struct{}),
  469. failures: make(chan error, 1),
  470. transactions: make(map[sipTransactionKey]chan *sipResponse),
  471. inboundConnections: make(map[net.Conn]struct{}),
  472. evidence: vowifi.IMSEvidence{
  473. RegistrationState: "registering",
  474. Transport: transport,
  475. },
  476. }
  477. if transport == "tcp" {
  478. session.reader = bufio.NewReader(connection)
  479. }
  480. if provider.config.SecurityMode != SecurityDisabled {
  481. localIP := addressIP(connection.LocalAddr())
  482. if localIP == nil {
  483. refreshCancel()
  484. return nil, errors.New("ims: protected local IP address is unavailable")
  485. }
  486. proposal, err := newSecurityProposal(
  487. localIP,
  488. provider.config.ProtectedClientPort,
  489. provider.config.ProtectedServerPort,
  490. )
  491. if err != nil {
  492. refreshCancel()
  493. return nil, err
  494. }
  495. session.securityProposal = proposal
  496. protectedTCP, err := net.ListenTCP(
  497. "tcp",
  498. &net.TCPAddr{IP: append(net.IP(nil), localIP...), Port: proposal.portServer},
  499. )
  500. if err != nil {
  501. refreshCancel()
  502. return nil, fmt.Errorf("ims: reserve protected TCP server port: %w", err)
  503. }
  504. protectedUDP, err := net.ListenUDP(
  505. "udp",
  506. &net.UDPAddr{IP: append(net.IP(nil), localIP...), Port: proposal.portServer},
  507. )
  508. if err != nil {
  509. _ = protectedTCP.Close()
  510. refreshCancel()
  511. return nil, fmt.Errorf("ims: reserve protected UDP server port: %w", err)
  512. }
  513. session.protectedTCP = protectedTCP
  514. session.protectedUDP = protectedUDP
  515. }
  516. return session, nil
  517. }
  518. func (session *Session) abort() {
  519. session.refreshCancel()
  520. _ = session.conn.Close()
  521. if session.protectedTCP != nil {
  522. _ = session.protectedTCP.Close()
  523. }
  524. if session.protectedUDP != nil {
  525. _ = session.protectedUDP.Close()
  526. }
  527. if session.ipsecHandle != nil {
  528. _ = session.ipsecHandle.Close(context.Background())
  529. }
  530. session.clearAuthentication()
  531. }
  532. func (session *Session) establish(ctx context.Context) error {
  533. session.mu.Lock()
  534. defer session.mu.Unlock()
  535. response, err := session.register(ctx, int(session.provider.config.RegistrationExpiry/time.Second))
  536. if err != nil {
  537. session.evidence.RegistrationState = "failed"
  538. return err
  539. }
  540. if response.StatusCode != 200 {
  541. session.evidence.RegistrationState = "rejected"
  542. phase := "initial"
  543. if session.auth != nil {
  544. phase = "authenticated"
  545. }
  546. return registrationRejectionError(response, phase)
  547. }
  548. if err := session.applyRegistrationEvidence(response); err != nil {
  549. return err
  550. }
  551. if err := session.startRuntimeReceivers(); err != nil {
  552. return err
  553. }
  554. go session.refreshLoop()
  555. return nil
  556. }
  557. // registrationRejectionError preserves the registrar's safe diagnostic text.
  558. // Operators commonly use the SIP reason phrase, Reason, or Warning header to
  559. // distinguish an unprovisioned subscriber from a malformed REGISTER. Do not
  560. // include authentication headers here because they contain AKA material.
  561. func registrationRejectionError(response *sipResponse, phase string) error {
  562. if response == nil {
  563. return fmt.Errorf("%w: empty SIP response", ErrRegistrationRejected)
  564. }
  565. phase = strings.TrimSpace(phase)
  566. if phase == "" {
  567. phase = "unknown"
  568. }
  569. message := fmt.Sprintf(
  570. "%s: SIP %d",
  571. phase+" REGISTER was rejected",
  572. response.StatusCode,
  573. )
  574. if reason := safeSIPDiagnostic(response.Reason); reason != "" {
  575. message += " " + reason
  576. }
  577. for _, header := range []string{"Reason", "Warning"} {
  578. for _, value := range response.values(header) {
  579. if value = safeSIPDiagnostic(value); value != "" {
  580. message += fmt.Sprintf("; %s: %s", header, value)
  581. }
  582. }
  583. }
  584. return fmt.Errorf("%w: %s", ErrRegistrationRejected, message)
  585. }
  586. func safeSIPDiagnostic(value string) string {
  587. value = strings.Join(strings.Fields(value), " ")
  588. const maximum = 256
  589. if len(value) > maximum {
  590. value = value[:maximum] + "..."
  591. }
  592. return value
  593. }
  594. func (session *Session) register(ctx context.Context, expires int) (*sipResponse, error) {
  595. for challenges := 0; challenges <= maxAuthenticationChallenges; challenges++ {
  596. cseq := session.cseq
  597. session.cseq++
  598. authorization := ""
  599. authorizationHeader := ""
  600. if session.auth != nil {
  601. session.auth.nc++
  602. credentials := digestCredentials{
  603. Username: session.identity.private,
  604. Password: session.auth.password,
  605. AUTS: session.auth.auts,
  606. URI: "sip:" + session.identity.domain,
  607. Method: "REGISTER",
  608. CNonce: session.auth.cnonce,
  609. NC: session.auth.nc,
  610. }
  611. authorization = buildDigestAuthorization(session.auth.challenge, credentials)
  612. if session.auth.challenge.Proxy {
  613. authorizationHeader = "Proxy-Authorization"
  614. } else {
  615. authorizationHeader = "Authorization"
  616. }
  617. }
  618. request, err := session.buildRegister(cseq, expires, authorizationHeader, authorization)
  619. if err != nil {
  620. return nil, err
  621. }
  622. response, err := session.exchange(ctx, request, cseq)
  623. if err != nil {
  624. return nil, err
  625. }
  626. session.evidence.LastSIPCode = response.StatusCode
  627. if response.StatusCode != 401 && response.StatusCode != 407 {
  628. return response, nil
  629. }
  630. if challenges == maxAuthenticationChallenges {
  631. break
  632. }
  633. if session.securityActive {
  634. return nil, errors.New("ims: protected registration was challenged again")
  635. }
  636. challenge, err := challengeFromResponse(response)
  637. if err != nil {
  638. return nil, err
  639. }
  640. agreement, useSecurity, err := session.securityFromResponse(response)
  641. if err != nil {
  642. return nil, err
  643. }
  644. material, err := authenticateAKA(ctx, session.provider.aka, session.request.Identity, challenge)
  645. if err != nil {
  646. return nil, err
  647. }
  648. if len(material.auts) == 0 && useSecurity {
  649. if err := session.activateIPSec(ctx, agreement, material.ck, material.ik); err != nil {
  650. clearAKAMaterial(&material)
  651. return nil, err
  652. }
  653. }
  654. credentials, err := newDigestCredentials(
  655. session.identity.private,
  656. material.password,
  657. "sip:"+session.identity.domain,
  658. "REGISTER",
  659. 1,
  660. )
  661. if err != nil {
  662. clearAKAMaterial(&material)
  663. return nil, err
  664. }
  665. auts := base64.StdEncoding.EncodeToString(material.auts)
  666. session.auth = &authenticationState{
  667. challenge: challenge,
  668. password: append([]byte(nil), material.password...),
  669. auts: auts,
  670. cnonce: credentials.CNonce,
  671. }
  672. clearAKAMaterial(&material)
  673. }
  674. return nil, errors.New("ims: too many SIP authentication challenges")
  675. }
  676. func challengeFromResponse(response *sipResponse) (digestChallenge, error) {
  677. header := "WWW-Authenticate"
  678. proxy := false
  679. if response.StatusCode == 407 {
  680. header = "Proxy-Authenticate"
  681. proxy = true
  682. }
  683. var lastError error
  684. for _, value := range response.values(header) {
  685. challenge, err := parseDigestChallenge(value, proxy)
  686. if err == nil {
  687. return challenge, nil
  688. }
  689. lastError = err
  690. }
  691. if lastError == nil {
  692. lastError = errors.New("ims: authentication response omitted a digest challenge")
  693. }
  694. return digestChallenge{}, lastError
  695. }
  696. func (session *Session) buildRegister(
  697. cseq uint32,
  698. expires int,
  699. authorizationHeader string,
  700. authorization string,
  701. ) ([]byte, error) {
  702. branch, err := randomHex(12)
  703. if err != nil {
  704. return nil, err
  705. }
  706. local := session.conn.LocalAddr().String()
  707. contactAddress := session.contactAddress()
  708. transportUpper := strings.ToUpper(session.transport)
  709. requestURI := "sip:" + session.identity.domain
  710. routeURI := "sip:" + session.endpoint.address() + ";transport=" + session.transport + ";lr"
  711. contact := fmt.Sprintf(
  712. "<sip:%s@%s;transport=%s>;+sip.instance=\"<%s>\";+g.3gpp.smsip",
  713. session.identity.user,
  714. contactAddress,
  715. session.transport,
  716. session.instanceID,
  717. )
  718. lines := []string{
  719. "REGISTER " + requestURI + " SIP/2.0",
  720. fmt.Sprintf("Via: SIP/2.0/%s %s;branch=z9hG4bK%s;rport", transportUpper, local, branch),
  721. "Max-Forwards: 70",
  722. "Route: <" + routeURI + ">",
  723. "From: <" + session.identity.public + ">;tag=" + session.fromTag,
  724. "To: <" + session.identity.public + ">",
  725. "Call-ID: " + session.callID,
  726. fmt.Sprintf("CSeq: %d REGISTER", cseq),
  727. "Contact: " + contact,
  728. fmt.Sprintf("Expires: %d", expires),
  729. "Supported: path, gruu",
  730. "Allow: REGISTER, OPTIONS",
  731. "User-Agent: " + session.provider.config.UserAgent,
  732. }
  733. if session.securityOffered() {
  734. lines = append(
  735. lines,
  736. "Security-Client: "+session.securityProposal.headerValue(),
  737. "Require: sec-agree",
  738. "Proxy-Require: sec-agree",
  739. )
  740. if session.securityActive {
  741. lines = append(lines, "Security-Verify: "+session.securityAgreement.verifyValue)
  742. }
  743. }
  744. if authorization != "" {
  745. if session.securityOffered() {
  746. integrity := "no"
  747. if session.securityActive {
  748. integrity = "yes"
  749. }
  750. authorization += ", integrity-protected=" + integrity
  751. }
  752. lines = append(lines, authorizationHeader+": "+authorization)
  753. } else if cseq == 1 && session.securityOffered() {
  754. lines = append(lines, "Authorization: "+session.emptyDigestAuthorization())
  755. }
  756. lines = append(lines, "Content-Length: 0", "", "")
  757. return []byte(strings.Join(lines, "\r\n")), nil
  758. }
  759. func (session *Session) exchange(ctx context.Context, request []byte, cseq uint32) (*sipResponse, error) {
  760. if err := ctx.Err(); err != nil {
  761. return nil, err
  762. }
  763. if session.runtimeStarted {
  764. return session.exchangeRuntime(ctx, request, sipTransactionKey{
  765. callID: session.callID,
  766. cseq: cseq,
  767. method: "REGISTER",
  768. })
  769. }
  770. deadline := time.Now().Add(session.provider.config.TransactionTimeout)
  771. if contextDeadline, ok := ctx.Deadline(); ok && contextDeadline.Before(deadline) {
  772. deadline = contextDeadline
  773. }
  774. readUDP := session.protectedUDP
  775. protectedUDP := session.securityActive && session.transport == "udp" && readUDP != nil
  776. if err := session.conn.SetDeadline(deadline); err != nil {
  777. return nil, fmt.Errorf("ims: set SIP transaction deadline: %w", err)
  778. }
  779. if protectedUDP {
  780. if err := readUDP.SetReadDeadline(deadline); err != nil {
  781. return nil, fmt.Errorf("ims: set protected SIP receive deadline: %w", err)
  782. }
  783. }
  784. stopCancellation := context.AfterFunc(ctx, func() {
  785. _ = session.conn.SetDeadline(time.Now())
  786. if protectedUDP {
  787. _ = readUDP.SetReadDeadline(time.Now())
  788. }
  789. })
  790. defer stopCancellation()
  791. if _, err := session.conn.Write(request); err != nil {
  792. return nil, fmt.Errorf("ims: send SIP REGISTER: %w", err)
  793. }
  794. for {
  795. var response *sipResponse
  796. var err error
  797. if session.transport == "tcp" {
  798. response, err = readSIPResponse(session.reader)
  799. } else if protectedUDP {
  800. packet := make([]byte, 65535)
  801. var count int
  802. var remote *net.UDPAddr
  803. count, remote, err = readUDP.ReadFromUDP(packet)
  804. if err == nil && !session.validProtectedUDPSource(remote) {
  805. continue
  806. }
  807. if err == nil {
  808. response, err = parseSIPResponse(packet[:count])
  809. }
  810. } else {
  811. packet := make([]byte, 65535)
  812. var count int
  813. count, err = session.conn.Read(packet)
  814. if err == nil {
  815. response, err = parseSIPResponse(packet[:count])
  816. }
  817. }
  818. if err != nil {
  819. if contextErr := ctx.Err(); contextErr != nil {
  820. return nil, contextErr
  821. }
  822. return nil, fmt.Errorf("ims: receive SIP REGISTER response: %w", err)
  823. }
  824. if !strings.EqualFold(strings.TrimSpace(response.value("Call-ID")), session.callID) {
  825. continue
  826. }
  827. responseCSeq, method, err := cseqNumber(response.value("CSeq"))
  828. if err != nil || responseCSeq != cseq || method != "REGISTER" {
  829. continue
  830. }
  831. if response.StatusCode >= 100 && response.StatusCode < 200 {
  832. continue
  833. }
  834. return response, nil
  835. }
  836. }
  837. func (session *Session) applyRegistrationEvidence(response *sipResponse) error {
  838. if session.provider.config.SecurityMode == SecurityRequired && !session.securityActive {
  839. session.evidence.Registered = false
  840. session.evidence.RegistrationState = "security_failed"
  841. return ErrIPSecAgreementRequired
  842. }
  843. associated := splitHeaderValues(response.values("P-Associated-URI"))
  844. contacts := splitHeaderValues(response.values("Contact"))
  845. serviceRoutes := splitHeaderValues(response.values("Service-Route"))
  846. registeredContact := ""
  847. smsConfirmed := false
  848. instanceLower := strings.ToLower(session.instanceID)
  849. contactURILower := strings.ToLower(fmt.Sprintf(
  850. "sip:%s@%s;transport=%s",
  851. session.identity.user,
  852. session.contactAddress(),
  853. session.transport,
  854. ))
  855. for _, contact := range contacts {
  856. lower := strings.ToLower(contact)
  857. matchesThisSession := strings.Contains(lower, instanceLower) ||
  858. strings.Contains(lower, contactURILower)
  859. if matchesThisSession {
  860. registeredContact = contact
  861. smsConfirmed = strings.Contains(lower, "+g.3gpp.smsip")
  862. if strings.Contains(lower, instanceLower) {
  863. break
  864. }
  865. }
  866. }
  867. expiry := registrationExpiry(response, contacts, session.provider.config.RegistrationExpiry)
  868. if expiry <= 0 {
  869. session.evidence.Registered = false
  870. session.evidence.RegistrationState = "rejected_zero_expiry"
  871. session.evidence.LastSIPCode = response.StatusCode
  872. session.clearAuthentication()
  873. return fmt.Errorf("%w: registrar granted zero expiry", ErrRegistrationRejected)
  874. }
  875. session.expiresAt = time.Now().Add(expiry)
  876. session.smsContactConfirmed = smsConfirmed
  877. session.evidence = vowifi.IMSEvidence{
  878. Registered: true,
  879. RegistrationState: "registered",
  880. PAssociatedURI: append([]string(nil), associated...),
  881. AssociatedIdentities: append([]string(nil), associated...),
  882. RegisteredContact: registeredContact,
  883. ServiceRoute: append([]string(nil), serviceRoutes...),
  884. Transport: session.transport,
  885. LastSIPCode: response.StatusCode,
  886. SecurityMode: session.effectiveSecurityMode(),
  887. SecurityVerified: session.securityActive,
  888. }
  889. session.clearAuthentication()
  890. return nil
  891. }
  892. func (session *Session) refreshLoop() {
  893. defer close(session.refreshDone)
  894. for {
  895. session.mu.Lock()
  896. if session.closed || !session.evidence.Registered {
  897. session.mu.Unlock()
  898. return
  899. }
  900. delay := refreshDelay(time.Until(session.expiresAt))
  901. session.mu.Unlock()
  902. timer := time.NewTimer(delay)
  903. select {
  904. case <-session.refreshContext.Done():
  905. if !timer.Stop() {
  906. <-timer.C
  907. }
  908. return
  909. case <-timer.C:
  910. }
  911. if err := session.refreshOnce(session.refreshContext); err != nil {
  912. session.publishFailure(err)
  913. return
  914. }
  915. }
  916. }
  917. func refreshDelay(remaining time.Duration) time.Duration {
  918. if remaining <= 0 {
  919. return 0
  920. }
  921. delay := remaining * 4 / 5
  922. if remaining > time.Minute && delay > remaining-30*time.Second {
  923. delay = remaining - 30*time.Second
  924. }
  925. if delay < 100*time.Millisecond {
  926. delay = 100 * time.Millisecond
  927. }
  928. return delay
  929. }
  930. func (session *Session) refreshOnce(ctx context.Context) error {
  931. session.mu.Lock()
  932. defer session.mu.Unlock()
  933. switch {
  934. case session.closed:
  935. return ErrSessionClosed
  936. case !session.evidence.Registered:
  937. return vowifi.ErrIMSNotRegistered
  938. }
  939. response, err := session.register(ctx, int(session.provider.config.RegistrationExpiry/time.Second))
  940. if err != nil {
  941. session.failRefresh()
  942. return fmt.Errorf("ims: refresh registration: %w", err)
  943. }
  944. if response.StatusCode != 200 {
  945. session.failRefresh()
  946. return fmt.Errorf("ims: refresh registration returned SIP %d", response.StatusCode)
  947. }
  948. if err := session.applyRegistrationEvidence(response); err != nil {
  949. session.failRefresh()
  950. return err
  951. }
  952. return nil
  953. }
  954. func (session *Session) failRefresh() {
  955. session.evidence.Registered = false
  956. session.evidence.RegistrationState = "refresh_failed"
  957. session.smsContactConfirmed = false
  958. session.expiresAt = time.Time{}
  959. session.clearAuthentication()
  960. }
  961. func (session *Session) publishFailure(err error) {
  962. if err == nil {
  963. return
  964. }
  965. session.failureOnce.Do(func() {
  966. session.failures <- err
  967. })
  968. }
  969. func (session *Session) Failures() <-chan error {
  970. return session.failures
  971. }
  972. func (session *Session) clearAuthentication() {
  973. if session.auth == nil {
  974. return
  975. }
  976. for index := range session.auth.password {
  977. session.auth.password[index] = 0
  978. }
  979. session.auth = nil
  980. }
  981. func registrationExpiry(response *sipResponse, contacts []string, fallback time.Duration) time.Duration {
  982. for _, contact := range contacts {
  983. if seconds, ok := parameterSeconds(contact, "expires"); ok {
  984. return boundedExpiry(seconds)
  985. }
  986. }
  987. if seconds, err := strconv.Atoi(strings.TrimSpace(response.value("Expires"))); err == nil {
  988. return boundedExpiry(seconds)
  989. }
  990. return fallback
  991. }
  992. func parameterSeconds(value string, name string) (int, bool) {
  993. lower := strings.ToLower(value)
  994. needle := strings.ToLower(name) + "="
  995. index := strings.Index(lower, needle)
  996. if index < 0 {
  997. return 0, false
  998. }
  999. start := index + len(needle)
  1000. end := start
  1001. for end < len(value) && value[end] >= '0' && value[end] <= '9' {
  1002. end++
  1003. }
  1004. if end == start {
  1005. return 0, false
  1006. }
  1007. seconds, err := strconv.Atoi(value[start:end])
  1008. return seconds, err == nil
  1009. }
  1010. func boundedExpiry(seconds int) time.Duration {
  1011. if seconds <= 0 {
  1012. return 0
  1013. }
  1014. expiry := time.Duration(seconds) * time.Second
  1015. if expiry > 24*time.Hour {
  1016. return 24 * time.Hour
  1017. }
  1018. return expiry
  1019. }
  1020. func (session *Session) Evidence() vowifi.IMSEvidence {
  1021. session.mu.Lock()
  1022. defer session.mu.Unlock()
  1023. evidence := cloneEvidence(session.evidence)
  1024. if session.closed {
  1025. evidence.Registered = false
  1026. evidence.RegistrationState = "closed"
  1027. } else if evidence.Registered && !session.expiresAt.IsZero() && !time.Now().Before(session.expiresAt) {
  1028. evidence.Registered = false
  1029. evidence.RegistrationState = "expired"
  1030. }
  1031. return evidence
  1032. }
  1033. func (session *Session) EnableSMS(ctx context.Context) (vowifi.SMSEvidence, error) {
  1034. if ctx == nil {
  1035. ctx = context.Background()
  1036. }
  1037. if err := ctx.Err(); err != nil {
  1038. return vowifi.SMSEvidence{}, err
  1039. }
  1040. session.mu.Lock()
  1041. defer session.mu.Unlock()
  1042. // This method is deliberately evidence-only. It never sends SIP MESSAGE,
  1043. // modem CMGS commands, or any dial request. A registrar-confirmed feature
  1044. // tag on this session's own Contact is the minimum readiness proof.
  1045. switch {
  1046. case session.closed:
  1047. return vowifi.SMSEvidence{}, ErrSessionClosed
  1048. case !session.evidence.Registered:
  1049. return vowifi.SMSEvidence{}, vowifi.ErrIMSNotRegistered
  1050. case !session.expiresAt.IsZero() && !time.Now().Before(session.expiresAt):
  1051. return vowifi.SMSEvidence{}, ErrRegistrationExpired
  1052. case !session.smsContactConfirmed:
  1053. return vowifi.SMSEvidence{Ready: false}, ErrSMSCapabilityNotConfirmed
  1054. default:
  1055. return vowifi.SMSEvidence{Ready: true}, nil
  1056. }
  1057. }
  1058. func (session *Session) Close(ctx context.Context) error {
  1059. if ctx == nil {
  1060. ctx = context.Background()
  1061. }
  1062. session.refreshCancel()
  1063. select {
  1064. case <-session.refreshDone:
  1065. case <-ctx.Done():
  1066. _ = session.conn.Close()
  1067. <-session.refreshDone
  1068. }
  1069. session.mu.Lock()
  1070. if session.closed {
  1071. session.mu.Unlock()
  1072. return nil
  1073. }
  1074. var unregisterErr error
  1075. if session.evidence.Registered && ctx.Err() == nil {
  1076. response, err := session.register(ctx, 0)
  1077. if err != nil {
  1078. unregisterErr = err
  1079. } else if response.StatusCode != 200 {
  1080. unregisterErr = fmt.Errorf("ims: SIP deregistration returned %d", response.StatusCode)
  1081. }
  1082. }
  1083. session.closed = true
  1084. session.evidence.Registered = false
  1085. session.evidence.RegistrationState = "closed"
  1086. session.smsContactConfirmed = false
  1087. session.clearAuthentication()
  1088. session.mu.Unlock()
  1089. var cleanupErrors []error
  1090. if unregisterErr != nil {
  1091. cleanupErrors = append(cleanupErrors, unregisterErr)
  1092. }
  1093. if err := session.conn.Close(); err != nil {
  1094. cleanupErrors = append(cleanupErrors, err)
  1095. }
  1096. if session.protectedTCP != nil {
  1097. if err := session.protectedTCP.Close(); err != nil && !errors.Is(err, net.ErrClosed) {
  1098. cleanupErrors = append(cleanupErrors, err)
  1099. }
  1100. }
  1101. if session.protectedUDP != nil {
  1102. if err := session.protectedUDP.Close(); err != nil && !errors.Is(err, net.ErrClosed) {
  1103. cleanupErrors = append(cleanupErrors, err)
  1104. }
  1105. }
  1106. session.closeInboundConnections()
  1107. session.receiveDone.Wait()
  1108. if session.ipsecHandle != nil {
  1109. if err := session.ipsecHandle.Close(ctx); err != nil {
  1110. cleanupErrors = append(cleanupErrors, err)
  1111. }
  1112. }
  1113. return errors.Join(cleanupErrors...)
  1114. }
  1115. func cloneEvidence(evidence vowifi.IMSEvidence) vowifi.IMSEvidence {
  1116. evidence.PAssociatedURI = append([]string(nil), evidence.PAssociatedURI...)
  1117. evidence.AssociatedIdentities = append([]string(nil), evidence.AssociatedIdentities...)
  1118. evidence.ServiceRoute = append([]string(nil), evidence.ServiceRoute...)
  1119. return evidence
  1120. }
  1121. func randomHex(size int) (string, error) {
  1122. value := make([]byte, size)
  1123. if _, err := rand.Read(value); err != nil {
  1124. return "", fmt.Errorf("ims: create random SIP identifier: %w", err)
  1125. }
  1126. return hex.EncodeToString(value), nil
  1127. }
  1128. func randomUUID() (string, error) {
  1129. value := make([]byte, 16)
  1130. if _, err := rand.Read(value); err != nil {
  1131. return "", fmt.Errorf("ims: create SIP instance identifier: %w", err)
  1132. }
  1133. value[6] = (value[6] & 0x0f) | 0x40
  1134. value[8] = (value[8] & 0x3f) | 0x80
  1135. return fmt.Sprintf(
  1136. "%x-%x-%x-%x-%x",
  1137. value[0:4], value[4:6], value[6:8], value[8:10], value[10:16],
  1138. ), nil
  1139. }
  1140. func addressHost(address net.Addr) string {
  1141. if address == nil {
  1142. return "localhost"
  1143. }
  1144. host, _, err := net.SplitHostPort(address.String())
  1145. if err == nil {
  1146. return strings.Trim(host, "[]")
  1147. }
  1148. return strings.Trim(address.String(), "[]")
  1149. }
  1150. func addressIP(address net.Addr) net.IP {
  1151. host := addressHost(address)
  1152. ip := net.ParseIP(strings.Trim(host, "[]"))
  1153. if ip == nil {
  1154. return nil
  1155. }
  1156. return append(net.IP(nil), ip...)
  1157. }
  1158. func digitsBetween(value string, minimum int, maximum int) bool {
  1159. if len(value) < minimum || len(value) > maximum {
  1160. return false
  1161. }
  1162. for _, character := range value {
  1163. if character < '0' || character > '9' {
  1164. return false
  1165. }
  1166. }
  1167. return true
  1168. }
  1169. var _ vowifi.IMSProvider = (*Provider)(nil)
  1170. var _ vowifi.IMSSession = (*Session)(nil)
  1171. var _ vowifi.RuntimeFailureNotifier = (*Session)(nil)