mirror of
https://github.com/MengMengCode/VoCat.git
synced 2026-08-20 23:03:44 +08:00
748 lines
22 KiB
Go
748 lines
22 KiB
Go
package ims
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/binary"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
type SecurityMode string
|
|
|
|
const (
|
|
// SecurityRequired is the production default. A 401 response without a
|
|
// supported ipsec-3gpp Security-Server offer fails closed.
|
|
SecurityRequired SecurityMode = "required"
|
|
// SecurityOptional advertises ipsec-3gpp but permits a carrier that
|
|
// explicitly omits Security-Server to continue on the tunnel in plain SIP.
|
|
SecurityOptional SecurityMode = "optional"
|
|
// SecurityDisabled is intended for controlled interoperability testing.
|
|
SecurityDisabled SecurityMode = "disabled"
|
|
)
|
|
|
|
var (
|
|
ErrIPSecAgreementRequired = errors.New("ims: a supported ipsec-3gpp security agreement is required")
|
|
ErrIPSecInstall = errors.New("ims: install ipsec-3gpp security associations")
|
|
)
|
|
|
|
// IPSecSAConfig is the complete, evidence-derived 3GPP transport-mode SA set.
|
|
// The two UE SPIs identify inbound SAs; the two P-CSCF SPIs identify outbound
|
|
// SAs. EncryptionKey and IntegrityKey must be discarded after Install returns.
|
|
type IPSecSAConfig struct {
|
|
LocalIP net.IP
|
|
RemoteIP net.IP
|
|
|
|
UEClientSPI uint32
|
|
UEServerSPI uint32
|
|
PCSCFClientSPI uint32
|
|
PCSCFServerSPI uint32
|
|
|
|
UEClientPort int
|
|
UEServerPort int
|
|
PCSCFClientPort int
|
|
PCSCFServerPort int
|
|
|
|
EncryptionKey []byte
|
|
IntegrityKey []byte
|
|
}
|
|
|
|
type IPSecSAHandle interface {
|
|
Close(context.Context) error
|
|
}
|
|
|
|
type IPSecSAInstaller interface {
|
|
Install(context.Context, IPSecSAConfig) (IPSecSAHandle, error)
|
|
}
|
|
|
|
type securityProposal struct {
|
|
spiClient uint32
|
|
spiServer uint32
|
|
portClient int
|
|
portServer int
|
|
}
|
|
|
|
func newSecurityProposal(localIP net.IP, configuredClientPort int, configuredServerPort int) (securityProposal, error) {
|
|
spiClient, err := randomSPI(0)
|
|
if err != nil {
|
|
return securityProposal{}, err
|
|
}
|
|
spiServer, err := randomSPI(spiClient)
|
|
if err != nil {
|
|
return securityProposal{}, err
|
|
}
|
|
portClient := configuredClientPort
|
|
if portClient == 0 {
|
|
portClient, err = availableProtectedPort(localIP, 0)
|
|
if err != nil {
|
|
return securityProposal{}, err
|
|
}
|
|
}
|
|
portServer := configuredServerPort
|
|
if portServer == 0 {
|
|
portServer, err = availableProtectedPort(localIP, portClient)
|
|
if err != nil {
|
|
return securityProposal{}, err
|
|
}
|
|
}
|
|
if !validProtectedPort(portClient) || !validProtectedPort(portServer) || portClient == portServer {
|
|
return securityProposal{}, errors.New("ims: protected UE ports must be distinct non-standard SIP ports")
|
|
}
|
|
return securityProposal{
|
|
spiClient: spiClient,
|
|
spiServer: spiServer,
|
|
portClient: portClient,
|
|
portServer: portServer,
|
|
}, nil
|
|
}
|
|
|
|
func (proposal securityProposal) headerValue() string {
|
|
return fmt.Sprintf(
|
|
"ipsec-3gpp;q=1.000;alg=hmac-sha-1-96;prot=esp;mod=trans;ealg=aes-cbc;spi-c=%010d;spi-s=%010d;port-c=%d;port-s=%d",
|
|
proposal.spiClient,
|
|
proposal.spiServer,
|
|
proposal.portClient,
|
|
proposal.portServer,
|
|
)
|
|
}
|
|
|
|
func randomSPI(exclude uint32) (uint32, error) {
|
|
for attempts := 0; attempts < 16; attempts++ {
|
|
var value [4]byte
|
|
if _, err := rand.Read(value[:]); err != nil {
|
|
return 0, fmt.Errorf("ims: create protected SPI: %w", err)
|
|
}
|
|
spi := binary.BigEndian.Uint32(value[:])
|
|
if spi >= 256 && spi != exclude {
|
|
return spi, nil
|
|
}
|
|
}
|
|
return 0, errors.New("ims: could not allocate a protected SPI")
|
|
}
|
|
|
|
func availableProtectedPort(localIP net.IP, exclude int) (int, error) {
|
|
for attempts := 0; attempts < 32; attempts++ {
|
|
var value [2]byte
|
|
if _, err := rand.Read(value[:]); err != nil {
|
|
return 0, fmt.Errorf("ims: create protected port: %w", err)
|
|
}
|
|
port := 20000 + int(binary.BigEndian.Uint16(value[:]))%44000
|
|
if port == exclude || !validProtectedPort(port) {
|
|
continue
|
|
}
|
|
address := &net.TCPAddr{IP: append(net.IP(nil), localIP...), Port: port}
|
|
listener, err := net.ListenTCP("tcp", address)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
_ = listener.Close()
|
|
packet, err := net.ListenUDP("udp", &net.UDPAddr{IP: append(net.IP(nil), localIP...), Port: port})
|
|
if err != nil {
|
|
continue
|
|
}
|
|
_ = packet.Close()
|
|
return port, nil
|
|
}
|
|
return 0, errors.New("ims: no protected local port is available")
|
|
}
|
|
|
|
func validProtectedPort(port int) bool {
|
|
return port > 1024 && port <= 65535 && port != 5060 && port != 5061
|
|
}
|
|
|
|
type securityMechanism struct {
|
|
raw string
|
|
name string
|
|
algorithm string
|
|
protocol string
|
|
mode string
|
|
encryption string
|
|
spiClient uint32
|
|
spiServer uint32
|
|
portClient int
|
|
portServer int
|
|
preference int
|
|
}
|
|
|
|
type securityAgreement struct {
|
|
selected securityMechanism
|
|
verifyValue string
|
|
}
|
|
|
|
func parseSecurityAgreement(values []string, proposal securityProposal) (securityAgreement, error) {
|
|
items := splitHeaderValues(values)
|
|
if len(items) == 0 {
|
|
return securityAgreement{}, ErrIPSecAgreementRequired
|
|
}
|
|
candidates := make([]securityMechanism, 0, len(items))
|
|
for _, item := range items {
|
|
mechanism, err := parseSecurityMechanism(item)
|
|
if err != nil {
|
|
name := strings.ToLower(strings.TrimSpace(strings.SplitN(item, ";", 2)[0]))
|
|
if name == "ipsec-3gpp" {
|
|
return securityAgreement{}, fmt.Errorf(
|
|
"ims: malformed ipsec-3gpp Security-Server: %w",
|
|
err,
|
|
)
|
|
}
|
|
continue
|
|
}
|
|
if !strings.EqualFold(mechanism.name, "ipsec-3gpp") ||
|
|
!strings.EqualFold(mechanism.algorithm, "hmac-sha-1-96") ||
|
|
!strings.EqualFold(mechanism.protocol, "esp") ||
|
|
!strings.EqualFold(mechanism.mode, "trans") ||
|
|
!strings.EqualFold(mechanism.encryption, "aes-cbc") {
|
|
continue
|
|
}
|
|
if mechanism.spiClient == 0 || mechanism.spiServer == 0 ||
|
|
mechanism.spiClient == mechanism.spiServer ||
|
|
mechanism.spiClient == proposal.spiClient ||
|
|
mechanism.spiClient == proposal.spiServer ||
|
|
mechanism.spiServer == proposal.spiClient ||
|
|
mechanism.spiServer == proposal.spiServer ||
|
|
!validProtectedPort(mechanism.portClient) ||
|
|
!validProtectedPort(mechanism.portServer) ||
|
|
mechanism.portClient == mechanism.portServer {
|
|
continue
|
|
}
|
|
candidates = append(candidates, mechanism)
|
|
}
|
|
if len(candidates) == 0 {
|
|
return securityAgreement{}, ErrIPSecAgreementRequired
|
|
}
|
|
sort.SliceStable(candidates, func(left int, right int) bool {
|
|
return candidates[left].preference > candidates[right].preference
|
|
})
|
|
return securityAgreement{
|
|
selected: candidates[0],
|
|
verifyValue: strings.Join(items, ", "),
|
|
}, nil
|
|
}
|
|
|
|
func parseSecurityMechanism(value string) (securityMechanism, error) {
|
|
parts := strings.Split(value, ";")
|
|
if len(parts) == 0 {
|
|
return securityMechanism{}, errors.New("ims: empty Security-Server mechanism")
|
|
}
|
|
mechanism := securityMechanism{
|
|
raw: strings.TrimSpace(value),
|
|
name: strings.ToLower(strings.TrimSpace(parts[0])),
|
|
protocol: "esp",
|
|
mode: "trans",
|
|
encryption: "null",
|
|
}
|
|
parameters := make(map[string]string)
|
|
for _, raw := range parts[1:] {
|
|
key, parameterValue, found := strings.Cut(strings.TrimSpace(raw), "=")
|
|
if !found {
|
|
return securityMechanism{}, errors.New("ims: malformed Security-Server parameter")
|
|
}
|
|
key = strings.ToLower(strings.TrimSpace(key))
|
|
parameterValue = strings.Trim(strings.TrimSpace(parameterValue), `"`)
|
|
if key == "" || parameterValue == "" {
|
|
return securityMechanism{}, errors.New("ims: empty Security-Server parameter")
|
|
}
|
|
if _, duplicate := parameters[key]; duplicate {
|
|
return securityMechanism{}, errors.New("ims: duplicate Security-Server parameter")
|
|
}
|
|
parameters[key] = parameterValue
|
|
}
|
|
mechanism.algorithm = strings.ToLower(parameters["alg"])
|
|
if value := parameters["prot"]; value != "" {
|
|
mechanism.protocol = strings.ToLower(value)
|
|
}
|
|
if value := parameters["mod"]; value != "" {
|
|
mechanism.mode = strings.ToLower(value)
|
|
}
|
|
if value := parameters["ealg"]; value != "" {
|
|
mechanism.encryption = strings.ToLower(value)
|
|
}
|
|
var err error
|
|
if mechanism.spiClient, err = decimalUint32(parameters["spi-c"]); err != nil {
|
|
return securityMechanism{}, err
|
|
}
|
|
if mechanism.spiServer, err = decimalUint32(parameters["spi-s"]); err != nil {
|
|
return securityMechanism{}, err
|
|
}
|
|
if mechanism.portClient, err = decimalPort(parameters["port-c"]); err != nil {
|
|
return securityMechanism{}, err
|
|
}
|
|
if mechanism.portServer, err = decimalPort(parameters["port-s"]); err != nil {
|
|
return securityMechanism{}, err
|
|
}
|
|
mechanism.preference, err = preferenceValue(parameters["q"])
|
|
if err != nil {
|
|
return securityMechanism{}, err
|
|
}
|
|
return mechanism, nil
|
|
}
|
|
|
|
func decimalUint32(value string) (uint32, error) {
|
|
if value == "" || len(value) > 10 {
|
|
return 0, errors.New("ims: invalid Security-Server SPI")
|
|
}
|
|
parsed, err := strconv.ParseUint(value, 10, 32)
|
|
if err != nil {
|
|
return 0, errors.New("ims: invalid Security-Server SPI")
|
|
}
|
|
return uint32(parsed), nil
|
|
}
|
|
|
|
func decimalPort(value string) (int, error) {
|
|
parsed, err := strconv.Atoi(value)
|
|
if err != nil || parsed < 1 || parsed > 65535 {
|
|
return 0, errors.New("ims: invalid Security-Server port")
|
|
}
|
|
return parsed, nil
|
|
}
|
|
|
|
func preferenceValue(value string) (int, error) {
|
|
if value == "" {
|
|
return 0, nil
|
|
}
|
|
whole, fraction, found := strings.Cut(value, ".")
|
|
if whole != "0" && whole != "1" {
|
|
return 0, errors.New("ims: invalid Security-Server preference")
|
|
}
|
|
if !found {
|
|
if whole == "1" {
|
|
return 1000, nil
|
|
}
|
|
return 0, nil
|
|
}
|
|
if len(fraction) > 3 {
|
|
return 0, errors.New("ims: invalid Security-Server preference")
|
|
}
|
|
for len(fraction) < 3 {
|
|
fraction += "0"
|
|
}
|
|
numeric, err := strconv.Atoi(fraction)
|
|
if err != nil || (whole == "1" && numeric != 0) {
|
|
return 0, errors.New("ims: invalid Security-Server preference")
|
|
}
|
|
if whole == "1" {
|
|
return 1000, nil
|
|
}
|
|
return numeric, nil
|
|
}
|
|
|
|
func expandIPSecKeys(ck []byte, ik []byte) (encryption []byte, integrity []byte, err error) {
|
|
if len(ck) != 16 || len(ik) != 16 {
|
|
return nil, nil, errors.New("ims: AKA did not return 16-byte CK and IK")
|
|
}
|
|
encryption = append([]byte(nil), ck...)
|
|
integrity = make([]byte, 20)
|
|
copy(integrity, ik)
|
|
return encryption, integrity, nil
|
|
}
|
|
|
|
type xfrmOperation struct {
|
|
description string
|
|
arguments []string
|
|
}
|
|
|
|
func buildXFRMInstallPlan(config IPSecSAConfig) ([]xfrmOperation, error) {
|
|
if err := validateIPSecSAConfig(config); err != nil {
|
|
return nil, err
|
|
}
|
|
var operations []xfrmOperation
|
|
states := []struct {
|
|
description string
|
|
source net.IP
|
|
destination net.IP
|
|
spi uint32
|
|
reqid uint32
|
|
}{
|
|
{"outbound UE-client to P-CSCF-server state", config.LocalIP, config.RemoteIP, config.PCSCFServerSPI, clientPairReqID(config)},
|
|
{"inbound P-CSCF-server to UE-client state", config.RemoteIP, config.LocalIP, config.UEClientSPI, clientPairReqID(config)},
|
|
{"inbound P-CSCF-client to UE-server state", config.RemoteIP, config.LocalIP, config.UEServerSPI, serverPairReqID(config)},
|
|
{"outbound UE-server to P-CSCF-client state", config.LocalIP, config.RemoteIP, config.PCSCFClientSPI, serverPairReqID(config)},
|
|
}
|
|
for _, state := range states {
|
|
operations = append(operations, xfrmOperation{
|
|
description: state.description,
|
|
arguments: []string{
|
|
"xfrm", "state", "add",
|
|
"src", state.source.String(),
|
|
"dst", state.destination.String(),
|
|
"proto", "esp",
|
|
"spi", fmt.Sprintf("0x%08x", state.spi),
|
|
"reqid", strconv.FormatUint(uint64(state.reqid), 10),
|
|
"mode", "transport",
|
|
"replay-window", "32",
|
|
"auth-trunc", "hmac(sha1)", "0x" + hex.EncodeToString(config.IntegrityKey), "96",
|
|
"enc", "cbc(aes)", "0x" + hex.EncodeToString(config.EncryptionKey),
|
|
},
|
|
})
|
|
}
|
|
for _, flow := range xfrmFlows(config) {
|
|
for _, protocol := range flow.protocols {
|
|
operations = append(operations, xfrmOperation{
|
|
description: flow.description + " " + protocol + " policy",
|
|
arguments: []string{
|
|
flow.family,
|
|
"xfrm", "policy", "add",
|
|
"src", flow.sourcePrefix,
|
|
"dst", flow.destinationPrefix,
|
|
"proto", protocol,
|
|
"sport", strconv.Itoa(flow.sourcePort),
|
|
"dport", strconv.Itoa(flow.destinationPort),
|
|
"dir", flow.direction,
|
|
"priority", "100",
|
|
"tmpl",
|
|
"src", flow.templateSource.String(),
|
|
"dst", flow.templateDestination.String(),
|
|
"proto", "esp",
|
|
"spi", fmt.Sprintf("0x%08x", flow.spi),
|
|
"reqid", strconv.FormatUint(uint64(flow.reqid), 10),
|
|
"mode", "transport",
|
|
"level", "required",
|
|
},
|
|
})
|
|
}
|
|
}
|
|
return operations, nil
|
|
}
|
|
|
|
func buildXFRMCleanupPlan(config IPSecSAConfig) []xfrmOperation {
|
|
var operations []xfrmOperation
|
|
flows := xfrmFlows(config)
|
|
for flowIndex := len(flows) - 1; flowIndex >= 0; flowIndex-- {
|
|
flow := flows[flowIndex]
|
|
for protocolIndex := len(flow.protocols) - 1; protocolIndex >= 0; protocolIndex-- {
|
|
protocol := flow.protocols[protocolIndex]
|
|
operations = append(operations, xfrmOperation{
|
|
description: "delete " + flow.description + " " + protocol + " policy",
|
|
arguments: []string{
|
|
flow.family,
|
|
"xfrm", "policy", "delete",
|
|
"src", flow.sourcePrefix,
|
|
"dst", flow.destinationPrefix,
|
|
"proto", protocol,
|
|
"sport", strconv.Itoa(flow.sourcePort),
|
|
"dport", strconv.Itoa(flow.destinationPort),
|
|
"dir", flow.direction,
|
|
},
|
|
})
|
|
}
|
|
}
|
|
states := []struct {
|
|
source net.IP
|
|
destination net.IP
|
|
spi uint32
|
|
}{
|
|
{config.LocalIP, config.RemoteIP, config.PCSCFClientSPI},
|
|
{config.RemoteIP, config.LocalIP, config.UEServerSPI},
|
|
{config.RemoteIP, config.LocalIP, config.UEClientSPI},
|
|
{config.LocalIP, config.RemoteIP, config.PCSCFServerSPI},
|
|
}
|
|
for _, state := range states {
|
|
operations = append(operations, xfrmOperation{
|
|
description: "delete ipsec-3gpp state",
|
|
arguments: []string{
|
|
"xfrm", "state", "delete",
|
|
"src", state.source.String(),
|
|
"dst", state.destination.String(),
|
|
"proto", "esp",
|
|
"spi", fmt.Sprintf("0x%08x", state.spi),
|
|
},
|
|
})
|
|
}
|
|
return operations
|
|
}
|
|
|
|
type xfrmFlow struct {
|
|
description string
|
|
family string
|
|
sourcePrefix string
|
|
destinationPrefix string
|
|
sourcePort int
|
|
destinationPort int
|
|
direction string
|
|
templateSource net.IP
|
|
templateDestination net.IP
|
|
spi uint32
|
|
reqid uint32
|
|
protocols []string
|
|
}
|
|
|
|
func xfrmFlows(config IPSecSAConfig) []xfrmFlow {
|
|
family := "-4"
|
|
prefix := "/32"
|
|
if config.LocalIP.To4() == nil {
|
|
family = "-6"
|
|
prefix = "/128"
|
|
}
|
|
localPrefix := config.LocalIP.String() + prefix
|
|
remotePrefix := config.RemoteIP.String() + prefix
|
|
return []xfrmFlow{
|
|
{
|
|
description: "UE-client to P-CSCF-server", family: family,
|
|
sourcePrefix: localPrefix, destinationPrefix: remotePrefix,
|
|
sourcePort: config.UEClientPort, destinationPort: config.PCSCFServerPort,
|
|
direction: "out", templateSource: config.LocalIP, templateDestination: config.RemoteIP,
|
|
spi: config.PCSCFServerSPI, reqid: clientPairReqID(config),
|
|
protocols: []string{"tcp", "udp"},
|
|
},
|
|
{
|
|
description: "P-CSCF-server to UE-client", family: family,
|
|
sourcePrefix: remotePrefix, destinationPrefix: localPrefix,
|
|
sourcePort: config.PCSCFServerPort, destinationPort: config.UEClientPort,
|
|
direction: "in", templateSource: config.RemoteIP, templateDestination: config.LocalIP,
|
|
spi: config.UEClientSPI, reqid: clientPairReqID(config),
|
|
protocols: []string{"tcp"},
|
|
},
|
|
{
|
|
description: "P-CSCF-client to UE-server", family: family,
|
|
sourcePrefix: remotePrefix, destinationPrefix: localPrefix,
|
|
sourcePort: config.PCSCFClientPort, destinationPort: config.UEServerPort,
|
|
direction: "in", templateSource: config.RemoteIP, templateDestination: config.LocalIP,
|
|
spi: config.UEServerSPI, reqid: serverPairReqID(config),
|
|
protocols: []string{"tcp", "udp"},
|
|
},
|
|
{
|
|
description: "UE-server to P-CSCF-client", family: family,
|
|
sourcePrefix: localPrefix, destinationPrefix: remotePrefix,
|
|
sourcePort: config.UEServerPort, destinationPort: config.PCSCFClientPort,
|
|
direction: "out", templateSource: config.LocalIP, templateDestination: config.RemoteIP,
|
|
spi: config.PCSCFClientSPI, reqid: serverPairReqID(config),
|
|
protocols: []string{"tcp"},
|
|
},
|
|
}
|
|
}
|
|
|
|
func clientPairReqID(config IPSecSAConfig) uint32 {
|
|
reqid := (config.UEClientSPI ^ config.PCSCFServerSPI) & 0x7fffffff
|
|
if reqid == 0 {
|
|
return 1
|
|
}
|
|
return reqid
|
|
}
|
|
|
|
func serverPairReqID(config IPSecSAConfig) uint32 {
|
|
reqid := (config.UEServerSPI ^ config.PCSCFClientSPI) & 0x7fffffff
|
|
if reqid == 0 {
|
|
reqid = 2
|
|
}
|
|
if reqid == clientPairReqID(config) {
|
|
reqid ^= 0x40000000
|
|
if reqid == 0 {
|
|
reqid = 2
|
|
}
|
|
}
|
|
return reqid
|
|
}
|
|
|
|
func validateIPSecSAConfig(config IPSecSAConfig) error {
|
|
local := config.LocalIP
|
|
remote := config.RemoteIP
|
|
if local == nil || remote == nil || local.IsUnspecified() || remote.IsUnspecified() ||
|
|
(local.To4() == nil) != (remote.To4() == nil) {
|
|
return errors.New("ims: ipsec-3gpp endpoints are invalid or use different IP families")
|
|
}
|
|
spis := []uint32{
|
|
config.UEClientSPI, config.UEServerSPI, config.PCSCFClientSPI, config.PCSCFServerSPI,
|
|
}
|
|
seen := make(map[uint32]struct{}, len(spis))
|
|
for _, spi := range spis {
|
|
if spi == 0 {
|
|
return errors.New("ims: ipsec-3gpp SPI is zero")
|
|
}
|
|
if _, duplicate := seen[spi]; duplicate {
|
|
return errors.New("ims: ipsec-3gpp SPIs must be unique")
|
|
}
|
|
seen[spi] = struct{}{}
|
|
}
|
|
ports := []int{
|
|
config.UEClientPort, config.UEServerPort, config.PCSCFClientPort, config.PCSCFServerPort,
|
|
}
|
|
for _, port := range ports {
|
|
if !validProtectedPort(port) {
|
|
return errors.New("ims: ipsec-3gpp protected port is invalid")
|
|
}
|
|
}
|
|
if config.UEClientPort == config.UEServerPort ||
|
|
config.PCSCFClientPort == config.PCSCFServerPort {
|
|
return errors.New("ims: client and server protected ports must differ")
|
|
}
|
|
if len(config.EncryptionKey) != 16 || len(config.IntegrityKey) != 20 {
|
|
return errors.New("ims: ipsec-3gpp key length is invalid")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func cloneIPSecSAConfig(config IPSecSAConfig) IPSecSAConfig {
|
|
config.LocalIP = append(net.IP(nil), config.LocalIP...)
|
|
config.RemoteIP = append(net.IP(nil), config.RemoteIP...)
|
|
config.EncryptionKey = append([]byte(nil), config.EncryptionKey...)
|
|
config.IntegrityKey = append([]byte(nil), config.IntegrityKey...)
|
|
return config
|
|
}
|
|
|
|
func zeroBytes(value []byte) {
|
|
for index := range value {
|
|
value[index] = 0
|
|
}
|
|
}
|
|
|
|
func (session *Session) securityOffered() bool {
|
|
return session.provider.config.SecurityMode != SecurityDisabled && !session.securityDeclined
|
|
}
|
|
|
|
func (session *Session) securityFromResponse(response *sipResponse) (securityAgreement, bool, error) {
|
|
if !session.securityOffered() {
|
|
return securityAgreement{}, false, nil
|
|
}
|
|
values := response.values("Security-Server")
|
|
if len(splitHeaderValues(values)) == 0 {
|
|
if session.provider.config.SecurityMode == SecurityRequired {
|
|
return securityAgreement{}, false, ErrIPSecAgreementRequired
|
|
}
|
|
session.declineSecurity()
|
|
return securityAgreement{}, false, nil
|
|
}
|
|
agreement, err := parseSecurityAgreement(values, session.securityProposal)
|
|
if err != nil {
|
|
return securityAgreement{}, false, err
|
|
}
|
|
return agreement, true, nil
|
|
}
|
|
|
|
func (session *Session) declineSecurity() {
|
|
session.securityDeclined = true
|
|
session.endpoint = session.initialEndpoint
|
|
if session.protectedTCP != nil {
|
|
_ = session.protectedTCP.Close()
|
|
session.protectedTCP = nil
|
|
}
|
|
if session.protectedUDP != nil {
|
|
_ = session.protectedUDP.Close()
|
|
session.protectedUDP = nil
|
|
}
|
|
session.securityProposal = securityProposal{}
|
|
}
|
|
|
|
func (session *Session) activateIPSec(
|
|
ctx context.Context,
|
|
agreement securityAgreement,
|
|
ck []byte,
|
|
ik []byte,
|
|
) error {
|
|
if session.securityActive {
|
|
return errors.New("ims: ipsec-3gpp is already active")
|
|
}
|
|
if !session.securityOffered() {
|
|
return ErrIPSecAgreementRequired
|
|
}
|
|
encryptionKey, integrityKey, err := expandIPSecKeys(ck, ik)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer zeroBytes(encryptionKey)
|
|
defer zeroBytes(integrityKey)
|
|
|
|
localIP := addressIP(session.conn.LocalAddr())
|
|
remoteIP := addressIP(session.conn.RemoteAddr())
|
|
if localIP == nil || remoteIP == nil {
|
|
return errors.New("ims: protected SIP endpoints are unavailable")
|
|
}
|
|
selected := agreement.selected
|
|
config := IPSecSAConfig{
|
|
LocalIP: localIP,
|
|
RemoteIP: remoteIP,
|
|
UEClientSPI: session.securityProposal.spiClient,
|
|
UEServerSPI: session.securityProposal.spiServer,
|
|
PCSCFClientSPI: selected.spiClient,
|
|
PCSCFServerSPI: selected.spiServer,
|
|
UEClientPort: session.securityProposal.portClient,
|
|
UEServerPort: session.securityProposal.portServer,
|
|
PCSCFClientPort: selected.portClient,
|
|
PCSCFServerPort: selected.portServer,
|
|
EncryptionKey: encryptionKey,
|
|
IntegrityKey: integrityKey,
|
|
}
|
|
handle, err := session.provider.installer.Install(ctx, config)
|
|
if err != nil {
|
|
return fmt.Errorf("%w: %v", ErrIPSecInstall, err)
|
|
}
|
|
if handle == nil {
|
|
return fmt.Errorf("%w: installer returned no handle", ErrIPSecInstall)
|
|
}
|
|
|
|
remoteAddress := net.JoinHostPort(remoteIP.String(), strconv.Itoa(selected.portServer))
|
|
_ = session.conn.Close()
|
|
connection, dialErr := dialSIP(
|
|
ctx,
|
|
session.transport,
|
|
localIP.String(),
|
|
session.securityProposal.portClient,
|
|
remoteAddress,
|
|
)
|
|
if dialErr != nil {
|
|
cleanupErr := handle.Close(context.Background())
|
|
if cleanupErr != nil {
|
|
return errors.Join(
|
|
fmt.Errorf("ims: connect protected P-CSCF: %w", dialErr),
|
|
fmt.Errorf("ims: roll back ipsec-3gpp: %w", cleanupErr),
|
|
)
|
|
}
|
|
return fmt.Errorf("ims: connect protected P-CSCF: %w", dialErr)
|
|
}
|
|
|
|
session.conn = connection
|
|
if session.transport == "tcp" {
|
|
session.reader = bufio.NewReader(connection)
|
|
} else {
|
|
session.reader = nil
|
|
}
|
|
session.endpoint.port = selected.portServer
|
|
session.securityAgreement = agreement
|
|
session.securityActive = true
|
|
session.ipsecHandle = handle
|
|
return nil
|
|
}
|
|
|
|
func (session *Session) contactAddress() string {
|
|
if session.securityOffered() {
|
|
host := addressHost(session.conn.LocalAddr())
|
|
return net.JoinHostPort(host, strconv.Itoa(session.securityProposal.portServer))
|
|
}
|
|
return session.conn.LocalAddr().String()
|
|
}
|
|
|
|
func (session *Session) emptyDigestAuthorization() string {
|
|
uri := "sip:" + session.identity.domain
|
|
return "Digest " + strings.Join([]string{
|
|
`username="` + quoteDigest(session.identity.private) + `"`,
|
|
`realm="` + quoteDigest(session.identity.domain) + `"`,
|
|
`nonce=""`,
|
|
`uri="` + quoteDigest(uri) + `"`,
|
|
`response=""`,
|
|
"algorithm=AKAv1-MD5",
|
|
"integrity-protected=no",
|
|
}, ", ")
|
|
}
|
|
|
|
func (session *Session) validProtectedUDPSource(remote *net.UDPAddr) bool {
|
|
if remote == nil || !session.securityActive {
|
|
return false
|
|
}
|
|
expectedIP := addressIP(session.conn.RemoteAddr())
|
|
return expectedIP != nil &&
|
|
expectedIP.Equal(remote.IP) &&
|
|
remote.Port == session.securityAgreement.selected.portClient
|
|
}
|
|
|
|
func (session *Session) effectiveSecurityMode() string {
|
|
if session.securityActive {
|
|
return "ipsec-3gpp"
|
|
}
|
|
return "none"
|
|
}
|