diff --git a/.github/docker/Dockerfile.ssut b/.github/docker/Dockerfile.ssut new file mode 100644 index 0000000..cc40367 --- /dev/null +++ b/.github/docker/Dockerfile.ssut @@ -0,0 +1,21 @@ +# syntax=docker/dockerfile:1 + +ARG BASE_IMAGE=ghcr.io/sippy/rtpproxy:latest +FROM ${BASE_IMAGE} + +ARG MM_TYPE +ARG MM_BRANCH + +ENV MM_TYPE=${MM_TYPE} \ + MM_BRANCH=${MM_BRANCH} \ + RTPP_BRANCH=DOCKER \ + BASEDIR=/opt/voiptests \ + BUILDDIR=/opt/voiptests \ + PYTHON_CMD=python3 \ + DEBIAN_FRONTEND=noninteractive + +WORKDIR /opt/voiptests + +COPY . /opt/voiptests + +RUN /bin/sh -eux ./cibits/build_ssut_image.sh diff --git a/.github/workflows/.main.yml b/.github/workflows/.main.yml index e3c8ea9..a0c5fd5 100644 --- a/.github/workflows/.main.yml +++ b/.github/workflows/.main.yml @@ -3,10 +3,6 @@ name: Reusable Workflow on: workflow_call: inputs: - python-version: - required: false - type: string - default: '3.12' mm-type: required: true type: string @@ -14,13 +10,21 @@ on: required: false type: string default: 'master' - mm-auth: + mm-auths: required: false type: string - default: '' - rtppc-type: + default: '[""]' + python-versions: + required: false + type: string + default: '["3.12"]' + rtppc-types: required: true type: string + use-local-image: + required: false + type: boolean + default: false rtpp-image: required: false type: string @@ -35,63 +39,246 @@ on: default: '' jobs: - test: + build_ssut_image: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + outputs: + image-ref: ${{ steps.meta.outputs.image-ref }} + image-artifact: ${{ steps.meta.outputs.image-artifact }} + env: + DOCKER_BUILD_SUMMARY: false + DOCKER_BUILD_RECORD_UPLOAD: false + steps: + - uses: actions/checkout@v5 + + - name: Compute image metadata + id: meta + run: | + MM_BRANCH_SAFE="$(echo '${{ inputs.mm-branch }}' | sed 's|[^a-zA-Z0-9_.-]|-|g')" + SHA_SHORT="$(printf '%s' "$GITHUB_SHA" | cut -c1-12)" + IMAGE_REF="ghcr.io/${{ github.repository_owner }}/voiptests-${{ inputs.mm-type }}:ssut-${MM_BRANCH_SAFE}-${SHA_SHORT}" + IMAGE_ARTIFACT="ssut-image-${{ inputs.mm-type }}-${MM_BRANCH_SAFE}-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" + echo "image-ref=${IMAGE_REF}" >> "$GITHUB_OUTPUT" + echo "image-artifact=${IMAGE_ARTIFACT}" >> "$GITHUB_OUTPUT" + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to GHCR + if: ${{ !inputs.use-local-image }} + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push SSUT image + if: ${{ !inputs.use-local-image }} + uses: docker/build-push-action@v6 + with: + context: . + file: ./.github/docker/Dockerfile.ssut + push: true + tags: ${{ steps.meta.outputs.image-ref }} + build-args: | + BASE_IMAGE=${{ inputs.rtpp-image }}:${{ inputs.rtpp-image-tag }} + MM_TYPE=${{ inputs.mm-type }} + MM_BRANCH=${{ inputs.mm-branch }} + + - name: Build SSUT image tarball + if: ${{ inputs.use-local-image }} + uses: docker/build-push-action@v6 + with: + context: . + file: ./.github/docker/Dockerfile.ssut + outputs: type=docker,dest=/tmp/ssut-image.tar + tags: ${{ steps.meta.outputs.image-ref }} + build-args: | + BASE_IMAGE=${{ inputs.rtpp-image }}:${{ inputs.rtpp-image-tag }} + MM_TYPE=${{ inputs.mm-type }} + MM_BRANCH=${{ inputs.mm-branch }} + + - name: Upload SSUT image artifact + if: ${{ inputs.use-local-image }} + uses: actions/upload-artifact@v4 + with: + name: ${{ steps.meta.outputs.image-artifact }} + path: /tmp/ssut-image.tar + + test_ghcr: + if: ${{ !inputs.use-local-image }} runs-on: ubuntu-latest + needs: [build_ssut_image] container: - image: ${{ inputs.rtpp-image }}:${{ inputs.rtpp-image-tag }} + image: ${{ needs.build_ssut_image.outputs.image-ref }} options: --privileged --sysctl net.ipv6.conf.all.disable_ipv6=0 env: MM_TYPE: ${{ inputs.mm-type }} MM_BRANCH: ${{ inputs.mm-branch }} RTPP_BRANCH: DOCKER - RTPPC_TYPE: ${{ inputs.rtppc-type }} + RTPPC_TYPE: ${{ matrix.rtppc-type }} + MM_AUTH: ${{ matrix.mm-auth }} + ARTIFACT_FILES: ${{ inputs.artifact-files }} + strategy: + fail-fast: false + matrix: + rtppc-type: ${{ fromJSON(inputs.rtppc-types) }} + mm-auth: ${{ fromJSON(inputs.mm-auths) }} + python-version: ${{ fromJSON(inputs.python-versions) }} steps: - - uses: actions/checkout@v4 - - - name: Set up Python ${{ inputs.python-version }} + - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: - python-version: ${{ inputs.python-version }} + python-version: ${{ matrix.python-version }} - name: Define PYTHON_CMD - run: | - PYTHON_VER="`echo ${{ matrix.python-version }} | sed 's|-dev$||'`" - echo "PYTHON_CMD=python${PYTHON_VER}" >> $GITHUB_ENV + run: echo "PYTHON_CMD=${pythonLocation}/bin/python" >> "$GITHUB_ENV" - - name: before_install - run: sh -x cibits/before_install.sh + - name: Install Python dependencies + run: | + cd /opt/voiptests + ${PYTHON_CMD} -m pip install --upgrade pip + ${PYTHON_CMD} -m pip install -r requirements.txt + if [ "${MM_TYPE}" = "b2bua" ] + then + ${PYTHON_CMD} -m pip install -U -r dist/b2bua/requirements.txt + fi - - name: transform_var - id: transform_var + - name: Build artifact name + id: artifact_name run: | - ARTNAME="test-logs_${{ inputs.mm-type }}_${{ inputs.mm-branch }}_${{ inputs.rtppc-type }}" - MM_AUTH="`echo "${{ inputs.mm-auth }}" | sed 's|/|.|g'`" - if [ "${MM_AUTH}" != "" ] + ARTNAME="test-logs_${{ inputs.mm-type }}_${{ inputs.mm-branch }}_${{ matrix.rtppc-type }}" + MM_AUTH_SAFE="$(echo "${{ matrix.mm-auth }}" | sed 's|/|.|g')" + if [ -n "${MM_AUTH_SAFE}" ] then - ARTNAME="${ARTNAME}_${MM_AUTH}" - echo "MM_AUTH=${{ inputs.mm-auth }}" >> $GITHUB_ENV + ARTNAME="${ARTNAME}_${MM_AUTH_SAFE}" fi if [ "${{ inputs.mm-type }}" = "b2bua" ] then - ARTNAME="${ARTNAME}_${{ inputs.python-version }}" + ARTNAME="${ARTNAME}_${{ matrix.python-version }}" fi - echo "ARTNAME=${ARTNAME}" >> $GITHUB_OUTPUT + echo "artname=${ARTNAME}" >> "$GITHUB_OUTPUT" - - name: Test (debug) + - name: Run tests run: | + cd /opt/voiptests + rm -rf test.logs + LOG_FILES="bob.log alice.log rtpproxy.log" + if [ -n "${ARTIFACT_FILES}" ] + then + LOG_FILES="${LOG_FILES} ${ARTIFACT_FILES}" + fi + RTPP_VERSION=debug sh -x ./test_run.sh mkdir -p test.logs/debug - mv bob.log alice.log rtpproxy.log ${{ inputs.artifact-files }} test.logs/debug + mv ${LOG_FILES} test.logs/debug - - name: Test (production) - run: | RTPP_VERSION=production sh -x ./test_run.sh mkdir -p test.logs/production - mv bob.log alice.log rtpproxy.log ${{ inputs.artifact-files }} test.logs/production + mv ${LOG_FILES} test.logs/production + + - name: Upload test logs + uses: actions/upload-artifact@v4 + with: + name: ${{ steps.artifact_name.outputs.artname }} + path: /opt/voiptests/test.logs + continue-on-error: true + + test_local: + if: ${{ inputs.use-local-image }} + runs-on: ubuntu-latest + needs: [build_ssut_image] + strategy: + fail-fast: false + matrix: + rtppc-type: ${{ fromJSON(inputs.rtppc-types) }} + mm-auth: ${{ fromJSON(inputs.mm-auths) }} + python-version: ${{ fromJSON(inputs.python-versions) }} + steps: + - name: Download SSUT image artifact + uses: actions/download-artifact@v4 + with: + name: ${{ needs.build_ssut_image.outputs.image-artifact }} + path: /tmp + + - name: Load SSUT image + run: docker load --input /tmp/ssut-image.tar + + - name: Build artifact name + id: artifact_name + run: | + ARTNAME="test-logs_${{ inputs.mm-type }}_${{ inputs.mm-branch }}_${{ matrix.rtppc-type }}" + MM_AUTH_SAFE="$(echo "${{ matrix.mm-auth }}" | sed 's|/|.|g')" + if [ -n "${MM_AUTH_SAFE}" ] + then + ARTNAME="${ARTNAME}_${MM_AUTH_SAFE}" + fi + if [ "${{ inputs.mm-type }}" = "b2bua" ] + then + ARTNAME="${ARTNAME}_${{ matrix.python-version }}" + fi + echo "artname=${ARTNAME}" >> "$GITHUB_OUTPUT" + + - name: Run tests in local image + run: | + PYTHON_VER="$(echo '${{ matrix.python-version }}' | sed 's|-dev$||')" + CTR_NAME="voiptests-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-${{ strategy.job-index }}" + trap 'docker rm -f "${CTR_NAME}" >/dev/null 2>&1 || true' EXIT + + docker create \ + --name "${CTR_NAME}" \ + --privileged \ + --sysctl net.ipv6.conf.all.disable_ipv6=0 \ + -e MM_TYPE='${{ inputs.mm-type }}' \ + -e MM_BRANCH='${{ inputs.mm-branch }}' \ + -e MM_AUTH='${{ matrix.mm-auth }}' \ + -e RTPP_BRANCH='DOCKER' \ + -e RTPPC_TYPE='${{ matrix.rtppc-type }}' \ + -e ARTIFACT_FILES='${{ inputs.artifact-files }}' \ + -e PYTHON_CMD="python${PYTHON_VER}" \ + '${{ needs.build_ssut_image.outputs.image-ref }}' \ + /bin/sh -lc ' + set -e + cd /opt/voiptests + if ! command -v "${PYTHON_CMD}" >/dev/null 2>&1 + then + PYTHON_CMD=python3 + fi + export PYTHON_CMD + export PIP_BREAK_SYSTEM_PACKAGES=1 + + ${PYTHON_CMD} -m pip install --upgrade pip + ${PYTHON_CMD} -m pip install -r requirements.txt + if [ "${MM_TYPE}" = "b2bua" ] + then + ${PYTHON_CMD} -m pip install -U -r dist/b2bua/requirements.txt + fi + + rm -rf test.logs + LOG_FILES="bob.log alice.log rtpproxy.log" + if [ -n "${ARTIFACT_FILES}" ] + then + LOG_FILES="${LOG_FILES} ${ARTIFACT_FILES}" + fi + + RTPP_VERSION=debug sh -x ./test_run.sh + mkdir -p test.logs/debug + mv ${LOG_FILES} test.logs/debug + + RTPP_VERSION=production sh -x ./test_run.sh + mkdir -p test.logs/production + mv ${LOG_FILES} test.logs/production + ' + + docker start -a "${CTR_NAME}" + rm -rf test.logs + docker cp "${CTR_NAME}:/opt/voiptests/test.logs" ./test.logs - - name: Test logs + - name: Upload test logs uses: actions/upload-artifact@v4 with: - name: ${{ steps.transform_var.outputs.ARTNAME }} + name: ${{ steps.artifact_name.outputs.artname }} path: test.logs continue-on-error: true diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 58d33a1..9ceb513 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,118 +1,98 @@ -# This is a basic workflow to help you get started with Actions - name: Alice->SSUT->Bob -# Controls when the action will run. on: - # Triggers the workflow on push or pull request events but only for the master branch push: -# branches: [ master ] pull_request: -# branches: [ master ] - release: types: [created] - - # Allows you to run this workflow manually from the Actions tab workflow_dispatch: - schedule: - cron: "30 0 * * 0" -# A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: # Alice->OpenSIPS->Bob opensips: + name: OpenSIPS (${{ matrix.mm-branch }}) uses: ./.github/workflows/.main.yml with: mm-type: 'opensips' mm-branch: ${{ matrix.mm-branch }} - rtppc-type: ${{ matrix.rtppc-type }} + mm-auths: ${{ matrix.mm-auths }} + rtppc-types: ${{ matrix.rtppc-types }} + use-local-image: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork }} artifact-files: 'opensips.cfg' strategy: fail-fast: false matrix: - mm-branch: ['master', '3.5', '3.4', '3.3', '3.2'] - rtppc-type: ['unix', 'udp', 'udp6', 'tcp', 'tcp6'] include: - - mm-branch: '2.4' - rtppc-type: 'unix' - - mm-branch: '2.4' - rtppc-type: 'udp' - - mm-branch: '2.4' - rtppc-type: 'udp6' - - mm-branch: '3.0' - rtppc-type: 'unix' - - mm-branch: '3.0' - rtppc-type: 'udp' - - mm-branch: '3.0' - rtppc-type: 'udp6' + - mm-branch: 'master' + mm-auths: '["", "passtr", "UAC", "UAS/auth", "UAS/auth_db/calculate_ha1", "UAS/auth_db/ha1"]' + rtppc-types: '["unix", "udp", "udp6", "tcp", "tcp6"]' + - mm-branch: '3.5' + mm-auths: '[""]' + rtppc-types: '["unix", "udp", "udp6", "tcp", "tcp6"]' + - mm-branch: '3.4' + mm-auths: '[""]' + rtppc-types: '["unix", "udp", "udp6", "tcp", "tcp6"]' + - mm-branch: '3.3' + mm-auths: '[""]' + rtppc-types: '["unix", "udp", "udp6", "tcp", "tcp6"]' + - mm-branch: '3.2' + mm-auths: '[""]' + rtppc-types: '["unix", "udp", "udp6", "tcp", "tcp6"]' - mm-branch: '3.1' - rtppc-type: 'unix' - - mm-branch: '3.1' - rtppc-type: 'udp' - - mm-branch: '3.1' - rtppc-type: 'udp6' - - # Alice->OpenSIPS(RFC8760)->Bob - opensips_rfc8760: - uses: ./.github/workflows/.main.yml - with: - mm-type: 'opensips' - mm-auth: ${{ matrix.mm-auth }} - rtppc-type: ${{ matrix.rtppc-type }} - artifact-files: 'opensips.cfg' - - strategy: - fail-fast: false - matrix: - rtppc-type: ['unix', 'udp', 'udp6', 'tcp', 'tcp6'] - mm-auth: ['passtr', 'UAC', 'UAS/auth', 'UAS/auth_db/calculate_ha1', 'UAS/auth_db/ha1'] + mm-auths: '[""]' + rtppc-types: '["unix", "udp", "udp6"]' + - mm-branch: '3.0' + mm-auths: '[""]' + rtppc-types: '["unix", "udp", "udp6"]' + - mm-branch: '2.4' + mm-auths: '[""]' + rtppc-types: '["unix", "udp", "udp6"]' # Alice->Sippy Python B2B->Bob sippy_py_b2bua: + name: Sippy Py-B2BUA (${{ matrix.mm-branch }}) uses: ./.github/workflows/.main.yml with: mm-type: 'b2bua' mm-branch: ${{ matrix.mm-branch }} - rtppc-type: ${{ matrix.rtppc-type }} - python-version: ${{ matrix.python-version }} + python-versions: '["3.9", "3.10", "3.11", "3.12", "3.13-dev"]' + rtppc-types: '["unix", "cunix", "udp", "udp6", "tcp", "tcp6"]' + use-local-image: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork }} artifact-files: 'b2bua.log' strategy: fail-fast: false matrix: mm-branch: ['master', 'PRACK'] - rtppc-type: ['unix', 'cunix', 'udp', 'udp6', 'tcp', 'tcp6'] - python-version: ['3.9', '3.10', '3.11', '3.12', '3.13-dev'] # Alice->Sippy GO B2B->Bob sippy_go_b2bua: + name: Sippy GO-B2BUA (${{ matrix.mm-branch }}) uses: ./.github/workflows/.main.yml with: mm-type: 'go-b2bua' - rtppc-type: ${{ matrix.rtppc-type }} + rtppc-types: '["unix", "cunix", "udp", "udp6", "tcp", "tcp6"]' + use-local-image: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork }} artifact-files: 'b2bua.log' - strategy: - fail-fast: false - matrix: - rtppc-type: ['unix', 'cunix', 'udp', 'udp6', 'tcp', 'tcp6'] # Alice->Kamailio->Bob kamailio: + name: Kamailio (${{ matrix.mm-branch }}) uses: ./.github/workflows/.main.yml with: mm-type: 'kamailio' mm-branch: ${{ matrix.mm-branch }} - rtppc-type: ${{ matrix.rtppc-type }} + rtppc-types: '["unix", "udp", "udp6"]' + use-local-image: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork }} artifact-files: 'kamailio.cfg' strategy: fail-fast: false matrix: mm-branch: ['master', '6.0', '5.8', '5.7', '5.6', '5.5', '5.4', '5.3', '5.2', '5.1', '4.4'] - rtppc-type: ['unix', 'udp', 'udp6'] publish: - needs: [opensips, opensips_rfc8760, sippy_py_b2bua, sippy_go_b2bua, kamailio] + needs: [opensips, sippy_py_b2bua, sippy_go_b2bua, kamailio] if: github.event_name == 'release' && github.event.action == 'created' runs-on: ubuntu-latest diff --git a/alice_main.py b/alice_main.py index 3187e84..2d0a98c 100644 --- a/alice_main.py +++ b/alice_main.py @@ -26,6 +26,9 @@ # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import sys, getopt +import math + +from .contrib.objgraph import show_most_common_types from sippy.MsgBody import MsgBody from sippy.SipLogger import SipLogger @@ -85,6 +88,14 @@ def main_func(): if o == '-s': tcfg.signalling_only = True continue + if o == '-C': + if not a.startswith('='): + cpsval = float(a) + tcfg.cps = lambda now: cpsval + else: + cpsfunc = eval(F'lambda now: {a[1:]}') + tcfg.cps = cpsfunc + continue if len(ttype) > 0: tcfg.ttype = tuple(ttype) @@ -101,4 +112,7 @@ def main_func(): if not dry_run: ED2.loop() + print('\n-- Types Summary: --') + show_most_common_types(50) + sys.exit(acore.rval) diff --git a/bob_main.py b/bob_main.py index be9f8bf..177586d 100644 --- a/bob_main.py +++ b/bob_main.py @@ -61,6 +61,9 @@ BODIES_ALL = (body_audio, body_fax) +def usage(args): + printf("bob.py -p xx -l yy -P zz -T tt -n aa -N bb -w cc -s") + def main_func(): global_config = {} diff --git a/cibits/build_ssut_image.sh b/cibits/build_ssut_image.sh new file mode 100755 index 0000000..f02c4d6 --- /dev/null +++ b/cibits/build_ssut_image.sh @@ -0,0 +1,23 @@ +#!/bin/sh + +set -eux + +apt-get update +apt-get install -y --no-install-recommends \ + python3 python3-pip \ + gpp gdb tcpdump gcc libc6-dev git make ca-certificates \ + libssl-dev flex bison + +if [ "${MM_TYPE}" = "go-b2bua" ] +then + apt-get install -y --no-install-recommends golang-go +fi + +if [ "${MM_TYPE}" = "kamailio" ] +then + apt-get install -y --no-install-recommends libcurl4-gnutls-dev +fi + +SKIP_PY_DEPS=1 /bin/sh -x ./install_depends/opensips.sh + +rm -rf /var/lib/apt/lists/* diff --git a/functions b/functions index 47e9f0d..f3c42bb 100755 --- a/functions +++ b/functions @@ -101,6 +101,7 @@ then BOB_TIMEOUT=90 TEST_SET="${TEST_SET},11,12,13,14,reinv_brkn1,reinv_brkn2,reinv_bad_ack" fi +TEST_SET="${TEST_SET},early_cancel_lost100" MM_ROOT="${MM_ROOT:-"${BUILDDIR}/dist/${MM_TYPE}"}" @@ -109,10 +110,10 @@ then TEST_SET="${TEST_SET},inv_brkn1" fi -#if [ "${MM_TYPE}" != "opensips" -o "${MM_BRANCH}" = "1.11" ] -#then -TEST_SET="${TEST_SET},early_cancel_lost100" -#fi +if [ "${MM_TYPE}" = "opensips" -a "${MM_BRANCH}" = "master" ] +then + TEST_SET="${TEST_SET},nated_contact" +fi #if [ "${MM_TYPE}" = "kamailio" ] #then diff --git a/install_depends/opensips.sh b/install_depends/opensips.sh index d44d645..897e720 100755 --- a/install_depends/opensips.sh +++ b/install_depends/opensips.sh @@ -72,7 +72,10 @@ fi if [ "${MM_TYPE}" = "b2bua" ] then - ${PYTHON_CMD} -m pip install -U -r b2bua/requirements.txt + if [ "${SKIP_PY_DEPS}" != "1" ] + then + ${PYTHON_CMD} -m pip install -U -r b2bua/requirements.txt + fi fi if [ "${MM_TYPE}" = "kamailio" ] diff --git a/lib/alice_testcore.py b/lib/alice_testcore.py index e9d5fd0..921be50 100644 --- a/lib/alice_testcore.py +++ b/lib/alice_testcore.py @@ -23,13 +23,15 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -##import sys +import sys +from signal import SIGINT ##sys.path.insert(0, 'dist/b2bua') from sippy.SipTransactionManager import SipTransactionManager -from sippy.Time.Timeout import Timeout +from sippy.Signal import Signal +from sippy.Time.Timeout import Timeout, TimeoutAbsMono from sippy.Core.EventDispatcher import ED2 -from random import shuffle +from random import shuffle, choice from .testcore_base import testcore_base @@ -58,13 +60,14 @@ from ..test_cases.reinv_frombob import a_test_reinv_frombob from ..test_cases.reinv_bad_ack import a_test_reinv_bad_ack from ..test_cases.inv_brkn1 import a_test_inv_brkn1 +from ..test_cases.nated_contact import a_test_nated_contact ALL_TESTS = (a_test1, a_test2, a_test3, a_test4, a_test5, a_test6, a_test7, \ a_test8, a_test9, a_test10, a_test11, a_test12, a_test13, a_test14, \ a_test_early_cancel, a_test_early_cancel_lost100, a_test_reinvite, \ a_test_reinv_fail, a_test_reinv_brkn1, a_test_reinv_brkn2, \ a_test_reinv_onhold, a_test_reinv_adelay, a_test_reinv_frombob, \ - a_test_reinv_bad_ack, a_test_inv_brkn1) + a_test_reinv_bad_ack, a_test_inv_brkn1, a_test_nated_contact) class a_cfg(object): test_class = None @@ -83,6 +86,9 @@ class a_test(testcore_base): stats_name = 'Alice' nsubtests_running = 0 rval = 1 + tcfg = None + last_ulen = 0 + nextr = None def __init__(self, tcfg): self.setup_stm(tcfg) @@ -103,10 +109,22 @@ def __init__(self, tcfg): subtest_cfg.tcfg = tcfg.gen_tccfg(atype, signalling_only, \ self.subtest_done, cli) atests.append(subtest_cfg) - mode = 'signalling-only' if signalling_only else 'rtp-enabled' - print(f'Scheduling test: {subtest_class.name} [{atype}, {mode}, {cli=}]') + if tcfg.continuous: + subtest_class.debug_lvl = -1 + subtest_class.godead_timeout = 1.0 + else: + mode = 'signalling-only' if signalling_only else 'rtp-enabled' + print(f'Scheduling test: {subtest_class.name} [{atype}, {mode}, {cli=}]') atests = [x for x in atests if not x.disabled] super().__init__(tcfg) + if tcfg.continuous: + self.atests = atests + self.ntime = tcfg.ntime.getCopy() + #Timeout(self.runnext, 0.1, -1) + self.setup_continuous_stats() + self.runnext() + Signal(SIGINT, self.deorbit) + return shuffle(atests) for subtest_cfg in atests: subtest = subtest_cfg.init_test() @@ -118,3 +136,27 @@ def __init__(self, tcfg): def _on_no_subtests_left(self): super()._on_no_subtests_left() ED2.breakLoop() + + def deorbit(self, signum = None): + self.nextr.cancel() + Timeout(self.slowexit, 0.1, -1) + Timeout(self.timeout, 140.0) + + def slowexit(self): + self.update_stats() + if self.nsubtests_running > 0: + return + sys.stdout.write('\n') + sys.stdout.flush() + ED2.breakLoop() + + def runnext(self): + subtest_cfg = choice(self.atests) + subtest = subtest_cfg.init_test() + self.nsubtests_running += 1 + if self.tcfg.cps != None: + self.ntime.offset(1.0 / self.tcfg.cps(self.ntime - self.tcfg.ntime)) + self.nextr = TimeoutAbsMono(self.runnext, self.ntime) + else: + self.runnext() + self.update_stats() diff --git a/lib/bob_testcore.py b/lib/bob_testcore.py index 0ed00ee..5e6bc16 100644 --- a/lib/bob_testcore.py +++ b/lib/bob_testcore.py @@ -23,11 +23,14 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -##import sys +import sys +from signal import SIGUSR1 ##sys.path.insert(0, 'dist/b2bua') from sippy.Time.Timeout import Timeout from sippy.SipTransactionManager import SipTransactionManager +from sippy.Signal import Signal + from random import random from .test_config import fillhostport, SIPPY_RTP_OUSER @@ -58,13 +61,14 @@ from ..test_cases.reinv_frombob import b_test_reinv_frombob from ..test_cases.reinv_bad_ack import b_test_reinv_bad_ack from ..test_cases.inv_brkn1 import b_test_inv_brkn1 +from ..test_cases.nated_contact import b_test_nated_contact ALL_TESTS = (b_test1, b_test2, b_test3, b_test4, b_test5, b_test6, b_test7, \ b_test8, b_test9, b_test10, b_test11, b_test12, b_test13, b_test14, \ b_test_early_cancel, b_test_early_cancel_lost100, b_test_reinvite, \ b_test_reinv_fail, b_test_reinv_brkn1, b_test_reinv_brkn2, \ b_test_reinv_onhold, b_test_reinv_adelay, b_test_reinv_frombob, \ - b_test_reinv_bad_ack, b_test_inv_brkn1) + b_test_reinv_bad_ack, b_test_inv_brkn1, b_test_nated_contact) class STMHooks(object): lossemul = 0 @@ -97,13 +101,30 @@ class b_test(testcore_base): rval = 1 nsubtests_running = 0 tcfg: 'test_config' = None + debug = False + spinor = None + last_ulen = 0 + active_subtests = None def __init__(self, tcfg:'test_config'): self.setup_stm(tcfg) super().__init__(tcfg) - Timeout(self.timeout, tcfg.test_timeout, 1) + if not tcfg.continuous: + Timeout(self.timeout, tcfg.test_timeout, 1) + else: + self.active_subtests = [] + self.setup_continuous_stats() + Signal(SIGUSR1, self.dumpcalls) + + def dumpcalls(self): + sys.stderr.write('BOB: Active tests:\n') + for c in self.active_subtests: + sys.stderr.write('\t%d %s\n' % (id(c), str(c))) + sys.stderr.flush() def recvRequest(self, req, sip_t): + if self.debug: + print('recvRequest') if req.getHFBody('to').getTag() is not None: # Request within dialog, but no such dialog return (req.genResponse(481, 'Call Leg/Transaction Does Not Exist'), None, None) @@ -131,11 +152,17 @@ def recvRequest(self, req, sip_t): tccfg = self.tcfg.gen_tccfg(atype, signalling_only, self.subtest_done) + if self.tcfg.continuous: + tclass.debug_lvl = -1 + tclass.godead_timeout = 1.0 subtest = tclass(tccfg) test_id = req.getHFBody('to').getUrl().username subtest.mightfail = test_id in self.tcfg.tests_mightfail self.nsubtests_running += 1 + if self.tcfg.continuous: + self.active_subtests.append(subtest) + self.update_stats() self.rval += 1 bidx = 0 if not tccfg.signalling_only or random() < 0.5 else 1 @@ -149,6 +176,9 @@ def recvRequest(self, req, sip_t): resp.appendHeaders(ce.challenges) self.nsubtests_running -= 1 self.rval -= 1 + if self.tcfg.continuous: + self.active_subtests.remove(subtest) + self.update_stats() resp.lossemul = STMHooks() resp.lossemul.dupemul = 0.001 return (resp, None, None) @@ -161,3 +191,6 @@ def timeout(self): if self.nsubtests_running == 0 and self.rval == 1: self.rval = 0 super().timeout() + + def _subtest_done_continuous(self, subtest): + self.active_subtests.remove(subtest) diff --git a/lib/spinor.py b/lib/spinor.py new file mode 100644 index 0000000..206e627 --- /dev/null +++ b/lib/spinor.py @@ -0,0 +1,15 @@ +class spinor(object): + i = 0 + busy_states = '-\\|/' + idle_states = '.oOo' + idle = False + + def tick(self): + if self.idle: + st = self.idle_states + else: + st = self.busy_states + ri = self.i % len(st) + rv = st[ri] + self.i += 1 + return rv diff --git a/lib/test_config.py b/lib/test_config.py index 2b2fedb..6faf60b 100644 --- a/lib/test_config.py +++ b/lib/test_config.py @@ -65,6 +65,7 @@ class test_case_config(object): check_media_ips = None cli = None signalling_only = False + continuous = False def __init__(self, nh_address): self.nh_address = nh_address @@ -93,7 +94,7 @@ def checkhostport(self, sdp_body): return (True, 'all good') class test_config(object): - getopts = 'p:l:P:T:n:N:w:m:' + getopts = 'p:l:P:T:n:N:w:m:c:' global_config = None ttype = ('IP4', 'IP6') body = None @@ -107,8 +108,12 @@ class test_config(object): tests_mightfail = tuple() signalling_only = False pre_wait = None + continuous = False + cps = None + ntime = None def gen_tccfg(self, atype, signalling_only, done_cb, cli=None): + self.ntime = MonoTime() if atype == 'IP4': nh_address = self.nh_address4 else: @@ -127,6 +132,7 @@ def gen_tccfg(self, atype, signalling_only, done_cb, cli=None): tccfg.atype = atype tccfg.portrange = self.portrange tccfg.signalling_only = signalling_only + tccfg.continuous = self.continuous return tccfg def _parse_common_opt(self, o, a): @@ -161,6 +167,9 @@ def _parse_common_opt(self, o, a): if o == '-m': self.tests_mightfail = tuple(a.split(',')) return True + if o == '-c': + self.continuous = True + return True return False def __init__(self, global_config, opts = None): diff --git a/lib/testcore_base.py b/lib/testcore_base.py index 2662f67..84e9517 100644 --- a/lib/testcore_base.py +++ b/lib/testcore_base.py @@ -26,8 +26,11 @@ import sys from sippy.Core.EventDispatcher import ED2 +from sippy.Math.recfilter import recfilter +from sippy.Time.Timeout import Timeout from .sleep_abs_mono import sleep_abs_mono +from .spinor import spinor class testcore_base(object): stm_class = None @@ -44,15 +47,42 @@ def setup_stm(self, tcfg): self.stm_class.model_udp_server[1].nworkers = 1 tcfg.global_config['_sip_tm'] = self.stm_class(tcfg.global_config, self.recvRequest) + def setup_continuous_stats(self): + self.spinor = spinor() + self.rcf = recfilter(0.999, 1.0) + self.update_stats() + Timeout(self.idle_update, 1.0, -1) + def recvRequest(self, req, sip_t): return (req.genResponse(501, 'Not Implemented'), None, None) def timeout(self): ED2.breakLoop() + def update_stats(self): + if self.nsubtests_running == 0: + self.spinor.idle = True + else: + self.spinor.idle = False + omsg = '\r%s: %d tests running %s, %f' % (self.stats_name, \ + self.nsubtests_running, self.spinor.tick(), self.rcf.lastval) + sys.stdout.write(omsg) + if len(omsg) < self.last_ulen: + pad = ' ' * (self.last_ulen - len(omsg)) + sys.stdout.write(pad) + sys.stdout.flush() + self.last_ulen = len(omsg) + + def idle_update(self): + if self.spinor.idle: + self.update_stats() + def _subtest_done_common(self, subtest): pass + def _subtest_done_continuous(self, subtest): + pass + def _on_no_subtests_left(self): pass @@ -61,5 +91,12 @@ def subtest_done(self, subtest): self._subtest_done_common(subtest) if subtest.rval == 0: self.rval -= 1 - if self.nsubtests_running == 0: + if self.tcfg.continuous: + self._subtest_done_continuous(subtest) + if subtest.rval == 0: + self.rcf.apply(1.0) + else: + self.rcf.apply(0.0) + self.update_stats() + elif self.nsubtests_running == 0: self._on_no_subtests_left() diff --git a/requirements.txt b/requirements.txt index 7619d8f..60ddd59 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -sippy +git+https://github.com/sippy/b2bua.git@master diff --git a/scenarios/simple/opensips.cfg.in b/scenarios/simple/opensips.cfg.in index 39552b3..c604cd5 100644 --- a/scenarios/simple/opensips.cfg.in +++ b/scenarios/simple/opensips.cfg.in @@ -15,6 +15,7 @@ loadmodule "sipmsgops/sipmsgops.so" loadmodule "sl/sl.so" loadmodule "tm/tm.so" loadmodule "rr/rr.so" +loadmodule "nathelper/nathelper.so" loadmodule "maxfwd/maxfwd.so" #if RTPPC_TYPE == rtp.io loadmodule "rtp.io/rtp.io.so" @@ -38,6 +39,12 @@ modparam("rtp.io", "rtpproxy_args", $$"-m 12000 -M 15000 RTPP_LISTEN"$$) modparam("rtpproxy", "rtpproxy_sock", RTPP_SOCK_TEST) #endif +#if OPENSIPS_VER_FULL == master || OPENSIPS_VER > 33 +#define NAT_PCONTACT "private-contact" +#else +#define NAT_PCONTACT INT(1) +#endif + #if OPENSIPS_VER != 24 && OPENSIPS_VER != 30 socket=udp:127.0.0.1:5060 socket=udp:[::1]:5060 @@ -88,13 +95,27 @@ route { }; if (is_method("BYE")) { - xlog(" calling rtpproxy_unforce()\n"); + xlog(" calling rtpproxy_unforce()\n"); rtpproxy_unforce(); }; record_route(); + if (is_method("INVITE") && !has_totag() && nat_uac_test(NAT_PCONTACT)) { + xlog(" NAT contact detected\n"); +#if OPENSIPS_VER_FULL == master + if (add_contact_alias()) { + xlog(" adding alias\n"); + }; +#endif + }; + if (loose_route()) { +#if OPENSIPS_VER_FULL == master + if (handle_ruri_alias()) { + xlog(" alias removed from the R-URI\n"); + }; +#endif t_relay(); exit; }; @@ -114,6 +135,14 @@ onreply_route[1] { xlog("OpenSIPS received a reply $rs/$rm from $si\n"); if (STATUS =~ "(183)|2[0-9][0-9]") { + if (nat_uac_test(NAT_PCONTACT)) { + xlog(" NAT contact detected\n"); +#if OPENSIPS_VER_FULL == master + if (add_contact_alias()) { + xlog(" adding alias\n"); + }; +#endif + }; xlog(" calling search()\n"); if(!search("^Content-Length:[ ]*0")) { xlog(" calling rtpproxy_answer()\n"); @@ -126,6 +155,6 @@ onreply_route[1] failure_route[1] { xlog("OpenSIPS handling $rm failure in from $si in failure_route[1]\n"); - xlog(" calling rtpproxy_unforce()\n"); + xlog(" calling rtpproxy_unforce()\n"); rtpproxy_unforce(); } diff --git a/test_cases/nated_contact.py b/test_cases/nated_contact.py new file mode 100644 index 0000000..bf1a1f2 --- /dev/null +++ b/test_cases/nated_contact.py @@ -0,0 +1,74 @@ +# Copyright (c) 2025 Sippy Software, Inc. All rights reserved. +# +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation and/or +# other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +from .reinv_frombob import a_test_reinv_frombob, b_test_reinv_frombob +from .TExceptions import ScenarioFailure + +from sippy.SipAddress import SipAddress +from sippy.SipContact import SipContact +from sippy.SipURL import SipURL +from sippy.UA import UA + + +class _ReinviteTrackingUA(UA): + """UA variant that keeps the R-URI of inbound in-dialog INVITEs.""" + + def recvRequest(self, req, *args, **kwargs): + if req.getMethod() == 'INVITE': + self.last_in_dialog_invite_ruri = str(req.getRURI()) + return super(_ReinviteTrackingUA, self).recvRequest(req, *args, **kwargs) + +class a_test_nated_contact(a_test_reinv_frombob): + cld = 'bob_nated_contact' + cli = 'alice_nated_contact' + name = f'{a_test_reinv_frombob.name}: NATed Contact preserved on re-INVITE' + compact_sip = False + nat_contact_ip = '192.168.255.10' + nat_contact_port = 5090 + + def build_ua(self, tccfg): + uaO = super(a_test_nated_contact, self).build_ua(tccfg, UA_class=_ReinviteTrackingUA) + nat_contact = self._build_nat_contact() + uaO.lContact = nat_contact + self.expected_contact_uri = str(nat_contact.getUrl()) + return uaO + + def _build_nat_contact(self): + nat_url = SipURL(username = self.cli, host = self.nat_contact_ip, + port = self.nat_contact_port) + nat_address = SipAddress(url = nat_url) + return SipContact(address = nat_address) + + def on_reinvite_connected(self, ua): + super(a_test_nated_contact, self).on_reinvite_connected(ua) + ruri = getattr(ua, 'last_in_dialog_invite_ruri', None) + if ruri != self.expected_contact_uri: + self.nerrs += 1 + print(ScenarioFailure('%s: expected re-INVITE R-URI %s, got %s' % + (self.failed_msg(), self.expected_contact_uri, ruri))) + +class b_test_nated_contact(b_test_reinv_frombob): + cli = a_test_nated_contact.cld + name = a_test_nated_contact.name diff --git a/test_cases/t1.py b/test_cases/t1.py index 4bb0bf9..c740af2 100644 --- a/test_cases/t1.py +++ b/test_cases/t1.py @@ -256,6 +256,8 @@ def report_rtp(self): stats = self._rtp_shutdown_stats() if self._rtp_error is not None and self.debug_lvl > -1: print('%s: RTP error: %s' % (self.my_name(), self._rtp_error)) + if self.tccfg.continuous: + return if self._rtp_target is None: print('%s: RTP stats: no session' % self.my_name()) return diff --git a/test_run.sh b/test_run.sh index 2d5cf14..92945b2 100755 --- a/test_run.sh +++ b/test_run.sh @@ -332,6 +332,8 @@ then fi fi +${SUDO} find /tmp/ -type f -name core\* + report_rc_log "${ALICE_RC}" "${MM_CFG} alice.log bob.log rtpproxy.log ${MM_LOG}" "Checking if Alice is happy" report_rc_log "${BOB_RC}" "${MM_CFG} bob.log alice.log rtpproxy.log ${MM_LOG}" "Checking if Bob is happy"