From 66a938839af405de895f137ff19500efd4b31e8d Mon Sep 17 00:00:00 2001 From: theodorsm Date: Sat, 20 Apr 2024 16:24:57 +0200 Subject: [PATCH] Remove fingerprinting workflow, added srtp mimicry --- .github/workflows/fingerprint.yaml | 146 ----------------- .github/workflows/fingerprinting/go.mod | 7 - .github/workflows/fingerprinting/go.sum | 15 -- .github/workflows/fingerprinting/main.go | 153 ------------------ .github/workflows/fingerprinting/package.json | 10 -- .../test/interop/connection.test.js | 56 ------- .../workflows/fingerprinting/test/steps.js | 44 ----- .../fingerprinting/test/webdriver.js | 108 ------------- .../fingerprinting/test/webrtcclient.js | 138 ---------------- .reuse/dep5 | 5 +- e2e/e2e_test.go | 15 +- ...e-Google_Chrome_124_0_6367_60_unknown.pcap | Bin 2688 -> 0 bytes ...pture-firefox-Mozilla_Firefox_125_0_1.pcap | Bin 2148 -> 0 bytes flight1handler.go | 17 +- flight3handler.go | 17 +- pkg/mimicry/README.md | 28 ++++ pkg/mimicry/fingerprints.go | 2 +- pkg/mimicry/mimic_client_hello.go | 44 ++++- 18 files changed, 100 insertions(+), 705 deletions(-) delete mode 100644 .github/workflows/fingerprint.yaml delete mode 100644 .github/workflows/fingerprinting/go.mod delete mode 100644 .github/workflows/fingerprinting/go.sum delete mode 100644 .github/workflows/fingerprinting/main.go delete mode 100644 .github/workflows/fingerprinting/package.json delete mode 100644 .github/workflows/fingerprinting/test/interop/connection.test.js delete mode 100644 .github/workflows/fingerprinting/test/steps.js delete mode 100644 .github/workflows/fingerprinting/test/webdriver.js delete mode 100644 .github/workflows/fingerprinting/test/webrtcclient.js delete mode 100644 fingerprints/capture-chrome-Google_Chrome_124_0_6367_60_unknown.pcap delete mode 100644 fingerprints/capture-firefox-Mozilla_Firefox_125_0_1.pcap create mode 100644 pkg/mimicry/README.md diff --git a/.github/workflows/fingerprint.yaml b/.github/workflows/fingerprint.yaml deleted file mode 100644 index 922349b0a..000000000 --- a/.github/workflows/fingerprint.yaml +++ /dev/null @@ -1,146 +0,0 @@ -## SPDX-FileCopyrightText: 2023 The Pion community -## SPDX-License-Identifier: MIT - -name: Fingerprinting -on: - pull_request: - branches: - - master - push: - branches: - - master -# on: -# schedule: -# - cron: "30 5 * * *" -# - -jobs: - handshake-capture: - runs-on: ubuntu-latest - timeout-minutes: 5 - strategy: - fail-fast: false - matrix: - browser: [firefox, chrome] - bver: ['stable'] - steps: - - uses: actions/checkout@v3 - - - name: Install tshark - run: sudo apt install -y tshark - - - uses: actions/setup-node@v3 - - - run: npm install - working-directory: .github/workflows/fingerprinting - - - name: Install browser version - run: BROWSER=${{matrix.browser}} BVER=${{matrix.bver}} ./node_modules/travis-multirunner/setup.sh - working-directory: .github/workflows/fingerprinting - - - name: Remove preinstalled github chromedriver/geckodriver from $PATH - run: sudo rm /usr/bin/chromedriver /usr/bin/geckodriver - - - run: Xvfb :99 & - - - name: Get browser version - id: "browser" - run: echo "version=$(./browsers/bin/${{matrix.browser}}-${{matrix.bver}} --version | sed -e 's/ /_/g' -e 's/\./_/g' -e 's/\-/_/g')" >> $GITHUB_OUTPUT - working-directory: .github/workflows/fingerprinting - - - name: Create directory for pcaps - run: | - mkdir ./captures/ - touch ./captures/full-capture-${{matrix.browser}}-${{steps.browser.outputs.version}}.pcap - sudo chown -R root:root ./captures - ls -lga ./captures - - name: Start tshark capture - run: sudo tshark -i any -w ./captures/full-capture-${{matrix.browser}}-${{steps.browser.outputs.version}}.pcap -f "udp" & - - - name: Run webrtc applications with jest/selenium - run: BROWSER_A=${{matrix.browser}} BROWSER_B=${{matrix.browser}} BVER=${{matrix.bver}} DISPLAY=:99.0 node_modules/.bin/jest --retries=3 test/interop - working-directory: .github/workflows/fingerprinting - - - name: Kill tshark capture - run: sudo killall tshark 1> /dev/null 2> /dev/null - continue-on-error: true - - - name: Filter DTLS handshake in pcap - run: sudo tshark -r ./captures/full-capture-${{matrix.browser}}-${{steps.browser.outputs.version}}.pcap -Y "dtls.handshake" -w ./captures/capture-${{matrix.browser}}-${{steps.browser.outputs.version}}.pcap - - - name: Archive pcap - uses: actions/upload-artifact@v4 - with: - name: fingerprint-pcap-${{matrix.browser}}-${{steps.browser.outputs.version}}.pcap - path: ./captures/capture-${{matrix.browser}}-${{steps.browser.outputs.version}}.pcap - - commit-fingerprints: - needs: handshake-capture - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - with: - ref: ${{ github.event.pull_request.head.ref }} - - - name: Create fingerprint directory - run: | - mkdir -p ./fingerprints - mkdir -p ${{ runner.temp }}/fingerprints - - - name: Download all artifacts - uses: actions/download-artifact@v4 - with: - path: ${{ runner.temp }}/fingerprints - pattern: fingerprint-pcap-* - merge-multiple: true - - - name: Install libpcap - run: sudo apt install libpcap-dev - - - name: Setup go - uses: actions/setup-go@v5 - with: - go-version: 'stable' - - - name: Run pcap fingerprint parser - run: | - go get . - go run main.go ${{ runner.temp }}/fingerprints - working-directory: .github/workflows/fingerprinting - - - name: Run gofmt on fingerprints.go - run: gofmt -s -w ./pkg/mimicry/fingerprints.go - - - name: Move new fingerprints to repo - run: | - mv ${{ runner.temp }}/fingerprints/* ./fingerprints/ - ls -R fingerprints - - - name: Run pre lint hook - if: always() - run: | - if [ -f .github/.ci.conf ]; then . .github/.ci.conf; fi - if [ -n "${PRE_LINT_HOOK}" ]; then ${PRE_LINT_HOOK}; fi - - - name: golangci-lint - uses: golangci/golangci-lint-action@v4 - with: - version: v1.56.2 - skip-pkg-cache: true - skip-build-cache: true - args: $GOLANGCI_LINT_EXRA_ARGS - - - name: Run E2E tests - run: | - docker build -t pion-dtls-e2e -f e2e/Dockerfile . - docker run -i --rm pion-dtls-e2e - - - name: Commit fingerprints - run: | - git config user.name github-actions - git config user.email github-actions@github.com - git add ./fingerprints - git add ./pkg/mimicry/fingerprints.go - git commit -m "Add fresh fingerprints" - git push diff --git a/.github/workflows/fingerprinting/go.mod b/.github/workflows/fingerprinting/go.mod deleted file mode 100644 index 68bf0a624..000000000 --- a/.github/workflows/fingerprinting/go.mod +++ /dev/null @@ -1,7 +0,0 @@ -module fingerprinting - -go 1.22.2 - -require github.com/google/gopacket v1.1.19 - -require golang.org/x/sys v0.0.0-20190412213103-97732733099d // indirect diff --git a/.github/workflows/fingerprinting/go.sum b/.github/workflows/fingerprinting/go.sum deleted file mode 100644 index aea2a4afd..000000000 --- a/.github/workflows/fingerprinting/go.sum +++ /dev/null @@ -1,15 +0,0 @@ -github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= -github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/.github/workflows/fingerprinting/main.go b/.github/workflows/fingerprinting/main.go deleted file mode 100644 index c2d25e6c8..000000000 --- a/.github/workflows/fingerprinting/main.go +++ /dev/null @@ -1,153 +0,0 @@ -package main - -import ( - "bufio" - "encoding/hex" - "errors" - "fmt" - "os" - "path/filepath" - "strings" - - "github.com/google/gopacket" - "github.com/google/gopacket/pcap" -) - -const OffsetContentType = 0 -const OffsetHandshakeType = 13 -const OffsetMajorVersion = 25 - -const ClientHelloType = 1 -const ServerHelloType = 2 -const HelloVerifyRequest = 3 -const HandshakeType = 22 - -var fingerprintType string - -func appendFingerprint(fingerprint string, version string) error { - var fileStrings []string - - file := "../../../pkg/mimicry/fingerprints.go" - readFile, err := os.Open(file) - - if err != nil { - return err - } - fileScanner := bufio.NewScanner(readFile) - - fileScanner.Split(bufio.ScanLines) - - var isDone bool - - for fileScanner.Scan() { - line := fileScanner.Text() - - if line == ")" { - fileStrings = append(fileStrings, fmt.Sprintf(" %s ClientHelloFingerprint = \"%s\" //nolint:revive,stylecheck", version, fingerprint)) - fileStrings = append(fileStrings, line) - } else if strings.Contains(line, "}") && !isDone { - fileStrings = append(fileStrings, fmt.Sprintf(" %s, //nolint:revive,stylecheck", version)) - fileStrings = append(fileStrings, line) - isDone = true - } else if !strings.Contains(line, version) { - fileStrings = append(fileStrings, line) - } - } - - readFile.Close() - - f, err := os.Create(file) - if err != nil { - f.Close() - return err - } - - for _, v := range fileStrings { - _, err = fmt.Fprintln(f, v) - if err != nil { - f.Close() - return err - } - } - err = f.Close() - return err -} - -func parsePcap(path string, filename string) error { - fmt.Printf("Parsing %s\n", filename) - - var parsedClientHello bool - - tmp := strings.Split(filename, "-") - version := tmp[len(tmp)-1] - version = strings.Trim(version, ".pcap") - version = strings.Trim(version, "_") - - handle, err := pcap.OpenOffline(path) - if err != nil { - return err - } - defer handle.Close() - - packetSource := gopacket.NewPacketSource(handle, handle.LinkType()) - for packet := range packetSource.Packets() { - - dtls := packet.ApplicationLayer().LayerContents() - - if len(dtls) < OffsetContentType { - return errors.New("parsed packet is empty") - } - if dtls[OffsetContentType] == HandshakeType { - - if len(dtls) < OffsetHandshakeType { - return errors.New("parsed packet does not contain a handshake") - } - handshakeType := uint(dtls[OffsetHandshakeType]) - - switch handshakeType { - case ClientHelloType: - if len(dtls) < OffsetMajorVersion { - return errors.New("parsed client hello does not have any fields") - } - fingerprintRaw := dtls[OffsetMajorVersion:] - fingerprintString := hex.EncodeToString(fingerprintRaw) - - // Only parse one client hello per handshake - if !parsedClientHello { - err = appendFingerprint(fingerprintString, version) - if err != nil { - return err - } - parsedClientHello = true - } - default: - } - - } - } - return nil -} - -func main() { - if len(os.Args) < 1 { - fmt.Println("Please provide pcaps") - os.Exit(1) - } - - err := filepath.Walk(os.Args[1], func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - if !info.IsDir() && strings.Contains(info.Name(), ".pcap") { - err = parsePcap(path, info.Name()) - if err != nil { - return err - } - } - return nil - }) - if err != nil { - fmt.Fprintf(os.Stderr, "failed during parsing of pcap: %v\n", err) - os.Exit(1) - } -} diff --git a/.github/workflows/fingerprinting/package.json b/.github/workflows/fingerprinting/package.json deleted file mode 100644 index b4d5c4f96..000000000 --- a/.github/workflows/fingerprinting/package.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name": "webrtc-fingerprinting", - "version": "1.0.0", - "devDependencies": { - "http-server": "^14.1.0", - "jest": "^29.7.0", - "selenium-webdriver": "^4.12.0", - "travis-multirunner": "^5.0.1" - } -} diff --git a/.github/workflows/fingerprinting/test/interop/connection.test.js b/.github/workflows/fingerprinting/test/interop/connection.test.js deleted file mode 100644 index 553eb9aec..000000000 --- a/.github/workflows/fingerprinting/test/interop/connection.test.js +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright (c) 2022 The WebRTC project authors. All Rights Reserved. - * - * Use of this source code is governed by a BSD-style license - * that can be found in the LICENSE file in the root of the source - * tree. - */ -const { buildDriver } = require('../webdriver'); -const { PeerConnection, MediaDevices } = require('../webrtcclient'); -const steps = require('../steps'); - -const browserA = process.env.BROWSER_A || 'firefox'; -const browserB = process.env.BROWSER_B || 'firefox'; - -describe(`basic interop test ${browserA} => ${browserB}`, function() { - let drivers; - let clients; - beforeAll(async () => { - const options = { - version: process.env.BVER || 'stable', - browserLogging: true, - } - drivers = [ - buildDriver(browserA, options), - buildDriver(browserB, options), - ]; - clients = drivers.map(driver => { - return { - connection: new PeerConnection(driver), - mediaDevices: new MediaDevices(driver), - }; - }); - }); - afterAll(async () => { - await drivers.map(driver => driver.close()); - }); - - it('establishes a connection', async () => { - await Promise.all(drivers); // timeouts in before(Each)? - await steps.step(drivers, (d) => d.get('https://webrtc.github.io/samples/emptypage.html'), 'Empty page loaded'); - await steps.step(clients, (client) => client.connection.create(), 'Created RTCPeerConnection'); - await steps.step(clients, async (client) => { - const stream = await client.mediaDevices.getUserMedia({ audio: true, video: true }); - return Promise.all(stream.getTracks().map(async track => { - return client.connection.addTrack(track, stream); - })); - }, 'Acquired and added audio/video stream'); - const offerWithCandidates = await clients[0].connection.setLocalDescription(); - await clients[1].connection.setRemoteDescription(offerWithCandidates); - const answerWithCandidates = await clients[1].connection.setLocalDescription(); - await clients[0].connection.setRemoteDescription(answerWithCandidates); - - await steps.step(drivers, (d) => steps.waitNVideosExist(d, 1), 'Video elements exist'); - await steps.step(drivers, steps.waitAllVideosHaveEnoughData, 'Video elements have enough data'); - }, 30000); -}, 90000); diff --git a/.github/workflows/fingerprinting/test/steps.js b/.github/workflows/fingerprinting/test/steps.js deleted file mode 100644 index e31e1bd41..000000000 --- a/.github/workflows/fingerprinting/test/steps.js +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright (c) 2022 The WebRTC project authors. All Rights Reserved. - * - * Use of this source code is governed by a BSD-style license - * that can be found in the LICENSE file in the root of the source - * tree. - */ -const TIMEOUT = 10000; - -function step(drivers, cb, logMessage) { - return Promise.all(drivers.map(driver => { - return cb(driver); - })).then(() => { - if (logMessage) { - console.log(logMessage); - } - }); -} -function waitNVideosExist(driver, n) { - return driver.wait(() => { - return driver.executeScript(n => document.querySelectorAll('video').length === n, n); - }, TIMEOUT); -} - -function waitAllVideosHaveEnoughData(driver) { - return driver.wait(() => { - return driver.executeScript(() => { - const videos = document.querySelectorAll('video'); - let ready = 0; - for (let i = 0; i < videos.length; i++) { - if (videos[i].readyState >= videos[i].HAVE_ENOUGH_DATA) { - ready++; - } - } - return ready === videos.length; - }); - }, TIMEOUT); -} - -module.exports = { - step, - waitNVideosExist, - waitAllVideosHaveEnoughData, -}; diff --git a/.github/workflows/fingerprinting/test/webdriver.js b/.github/workflows/fingerprinting/test/webdriver.js deleted file mode 100644 index e28b992cb..000000000 --- a/.github/workflows/fingerprinting/test/webdriver.js +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright (c) 2022 The WebRTC project authors. All Rights Reserved. - * - * Use of this source code is governed by a BSD-style license - * that can be found in the LICENSE file in the root of the source - * tree. - */ -const os = require('os'); - -const webdriver = require('selenium-webdriver'); -const chrome = require('selenium-webdriver/chrome'); -const firefox = require('selenium-webdriver/firefox'); -const safari = require('selenium-webdriver/safari'); - -if (os.platform() === 'win32') { - process.env.PATH += ';' + process.cwd() + '\\node_modules\\chromedriver\\lib\\chromedriver\\'; - process.env.PATH += ';' + process.cwd() + '\\node_modules\\geckodriver'; -} else { - process.env.PATH += ':node_modules/.bin'; -} - -function buildDriver(browser = process.env.BROWSER || 'chrome', options = {version: process.env.BVER}) { - // Chrome options. - // options.headless = true; - const chromeOptions = new chrome.Options() - .addArguments('allow-insecure-localhost') - .addArguments('use-fake-device-for-media-stream') - .addArguments('allow-file-access-from-files'); - // .addArguments('headless=new'); - if (options.chromeFlags) { - options.chromeFlags.forEach((flag) => chromeOptions.addArguments(flag)); - } - - if (options.chromepath) { - chromeOptions.setChromeBinaryPath(options.chromepath); - } else if (os.platform() === 'linux' && options.version) { - chromeOptions.setChromeBinaryPath('browsers/bin/chrome-' + options.version); - } - - if (!options.devices || options.headless) { - // GUM doesn't work in headless mode so we need this. See - // https://bugs.chromium.org/p/chromium/issues/detail?id=776649 - chromeOptions.addArguments('use-fake-ui-for-media-stream'); - } else { - // see https://bugs.chromium.org/p/chromium/issues/detail?id=459532#c22 - const domain = 'https://' + (options.devices.domain || 'localhost') + ':' + (options.devices.port || 443) + ',*'; - const exceptions = { - media_stream_mic: {}, - media_stream_camera: {}, - }; - - exceptions.media_stream_mic[domain] = { - last_used: Date.now(), - setting: options.devices.audio ? 1 : 2 // 0: ask, 1: allow, 2: denied - }; - exceptions.media_stream_camera[domain] = { - last_used: Date.now(), - setting: options.devices.video ? 1 : 2 - }; - - chromeOptions.setUserPreferences({ - profile: { - content_settings: { - exceptions: exceptions - } - } - }); - } - - const safariOptions = new safari.Options(); - safariOptions.setTechnologyPreview(options.version === 'unstable'); - - // Firefox options. - const firefoxOptions = new firefox.Options(); - let firefoxPath = firefox.Channel.RELEASE; - if (options.firefoxpath) { - firefoxPath = options.firefoxpath; - } else if (os.platform() == 'linux' && options.version) { - firefoxPath = 'browsers/bin/firefox - ' + options.version; - } - if (options.headless) { - firefoxOptions.addArguments('-headless'); - } - firefoxOptions.setBinary(firefoxPath); - firefoxOptions.setPreference('media.navigator.streams.fake', true); - firefoxOptions.setPreference('media.navigator.permission.disabled', true); - - const driver = new webdriver.Builder() - .setChromeOptions(chromeOptions) - .setSafariOptions(safariOptions) - .setFirefoxOptions(firefoxOptions) - .forBrowser(browser); - /* - .setChromeService( - new chrome.ServiceBuilder().addArguments('--disable-build-check') - ); - */ - - if (browser === 'firefox') { - driver.getCapabilities().set('marionette', true); - driver.getCapabilities().set('acceptInsecureCerts', true); - } - return driver.build(); -} - -module.exports = { - buildDriver, -}; diff --git a/.github/workflows/fingerprinting/test/webrtcclient.js b/.github/workflows/fingerprinting/test/webrtcclient.js deleted file mode 100644 index e545361e0..000000000 --- a/.github/workflows/fingerprinting/test/webrtcclient.js +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Copyright (c) 2022 The WebRTC project authors. All Rights Reserved. - * - * Use of this source code is governed by a BSD-style license - * that can be found in the LICENSE file in the root of the source - * tree. - */ -// Disable no-undef since this file is a mix of code executed -// in JS and the browser. -/* eslint no-undef: 0 */ -class MediaStream { - constructor(tracks = []) { - this.tracks = tracks; - this.id = 0; - } - - getTracks() { - return this.tracks; - } - - getAudioTracks() { - return this.getTracks().filter(t => t.kind === 'audio'); - } - - getVideoTracks() { - return this.getTracks().filter(t => t.kind === 'video'); - } -} - -class MediaDevices { - constructor(driver) { - this.driver = driver; - } - - getUserMedia(constraints) { - return this.driver.executeAsyncScript((constraints) => { - const callback = arguments[arguments.length - 1]; - if (!window.localStreams) { - window.localStreams = {}; - } - - return navigator.mediaDevices.getUserMedia(constraints) - .then((stream) => { - window.localStreams[stream.id] = stream; - callback({id: stream.id, tracks: stream.getTracks().map((t) => { - return {id: t.id, kind: t.kind}; - })}); - }, (e) => callback(e)); - }, constraints || {audio: true, video: true}) - .then((streamObj) => { - const stream = new MediaStream(streamObj.tracks); - stream.id = streamObj.id; - return stream; - }); - } -} - -class PeerConnection { - constructor(driver) { - this.driver = driver; - } - - create(rtcConfiguration) { - return this.driver.executeScript(rtcConfiguration => { - window.pc = new RTCPeerConnection(rtcConfiguration); - }, rtcConfiguration); - } - - addTrack(track, stream) { - return this.driver.executeScript((track, stream) => { - stream = localStreams[stream.id]; - track = stream.getTracks().find(t => t.id === track.id); - pc.addTrack(track, stream); - }, track, stream); - } - - createOffer(offerOptions) { - return this.driver.executeAsyncScript((offerOptions) => { - const callback = arguments[arguments.length - 1]; - - pc.createOffer(offerOptions) - .then(callback, callback); - }, offerOptions); - } - createAnswer() { - return this.driver.executeAsyncScript(() => { - const callback = arguments[arguments.length - 1]; - - pc.createAnswer() - .then(callback, callback); - }); - } - - // resolves with non-trickle description including candidates. - setLocalDescription(desc) { - return this.driver.executeAsyncScript((desc) => { - const callback = arguments[arguments.length - 1]; - - pc.onicecandidate = (event) => { - console.log('candidate', event.candidate); - if (!event.candidate) { - pc.onicecandidate = null; - callback(pc.localDescription); - } - }; - pc.setLocalDescription(desc) - .catch(callback); - }, desc); - } - - // TODO: this implicitly creates video elements, is that deseriable? - setRemoteDescription(desc) { - return this.driver.executeAsyncScript(function(desc) { - const callback = arguments[arguments.length - 1]; - - pc.ontrack = function(event) { - const id = event.streams[0].id; - if (document.getElementById('video-' + id)) { - return; - } - const video = document.createElement('video'); - video.id = 'video-' + id; - video.autoplay = true; - video.srcObject = event.streams[0]; - document.body.appendChild(video); - }; - pc.setRemoteDescription(new RTCSessionDescription(desc)) - .then(callback, callback); - }, desc); - } -} - -module.exports = { - PeerConnection, - MediaDevices, - MediaStream, -}; - diff --git a/.reuse/dep5 b/.reuse/dep5 index aad80b0bb..eb7fac2fc 100644 --- a/.reuse/dep5 +++ b/.reuse/dep5 @@ -2,11 +2,10 @@ Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Upstream-Name: Pion Source: https://github.com/pion/ -Files: README.md DESIGN.md **/README.md AUTHORS.txt renovate.json go.mod go.sum **/go.mod **/go.sum .eslintrc.json package.json examples.json sfu-ws/flutter/.gitignore sfu-ws/flutter/pubspec.yaml c-data-channels/webrtc.h examples/examples.json .github/workflows/fingerprinting/* +Files: README.md DESIGN.md **/README.md AUTHORS.txt renovate.json go.mod go.sum **/go.mod **/go.sum .eslintrc.json package.json examples.json sfu-ws/flutter/.gitignore sfu-ws/flutter/pubspec.yaml c-data-channels/webrtc.h examples/examples.json Copyright: 2023 The Pion community License: MIT -Files: testdata/fuzz/* **/testdata/fuzz/* api/*.txt fingerprints/*.pcap +Files: testdata/fuzz/* **/testdata/fuzz/* api/*.txt Copyright: 2023 The Pion community License: CC0-1.0 - diff --git a/e2e/e2e_test.go b/e2e/e2e_test.go index 21aa5c094..33aed6124 100644 --- a/e2e/e2e_test.go +++ b/e2e/e2e_test.go @@ -585,17 +585,24 @@ func testPionE2ESimpleMimicry(t *testing.T, server, client func(*comm), opts ... t.Fatal(err) } - cfg := &dtls.Config{ + client_cfg := &dtls.Config{ + Certificates: []tls.Certificate{cert}, + MimicryEnabled: true, + InsecureSkipVerify: true, + } + + server_cfg := &dtls.Config{ Certificates: []tls.Certificate{cert}, SRTPProtectionProfiles: []dtls.SRTPProtectionProfile{dtls.SRTP_AES128_CM_HMAC_SHA1_80, dtls.SRTP_AES128_CM_HMAC_SHA1_32, dtls.SRTP_AEAD_AES_128_GCM, dtls.SRTP_AEAD_AES_256_GCM}, - MimicryEnabled: true, InsecureSkipVerify: true, } + for _, o := range opts { - o(cfg) + o(client_cfg) + o(server_cfg) } serverPort := randomPort(t) - comm := newComm(ctx, cfg, cfg, serverPort, server, client) + comm := newComm(ctx, client_cfg, server_cfg, serverPort, server, client) defer comm.cleanup(t) comm.assert(t) }) diff --git a/fingerprints/capture-chrome-Google_Chrome_124_0_6367_60_unknown.pcap b/fingerprints/capture-chrome-Google_Chrome_124_0_6367_60_unknown.pcap deleted file mode 100644 index c717b92f3c41ddee581bf083caba91fcfd459e81..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2688 zcmd5;dpy+X8h?Mkxin-LH4H+((UDsij6ssyU?L&8<+CyvOA}^CE|FZqx~(kQZ0n-ixKQcNl5#tz{8?g*cSSFT+{G>Rs001J`4Y+YpqENmF)AfrK z3t}VqVxb;pX2dZv#dKXFk7MyGa}hsO$p26fhVf%pb`7P$VSGGKJjeY|KZ1%!Md4TZ z(IwDRo8cOW3lquxPA<479)k>Z5;vgaU+-pQ<*B&zz&WlE4N-bPI;Q|90IrHE z?0}tObD=B(PUBiw(rpE*m2N68ElW?~KSeOX%vCFMX|YoNhfHOx>1a8FGZEpe{Xt6I z-r|qag>rulOfAjz@@K%4%AE$C#=Z5ub)7PuTRUYt>7X1K19KRM3*i1Tf@78i6qt<$ zTu^}!QMj@Lx>5lhY#|aTL?S{WP)H<%LM9_{QyyLg8M;xxCOj1!<9}kE`hM8;cd^z; zv1b2otjHf4Ycq-?U#aW#2{c-Zj17vFa16m>~(bhWv9THs>Bw@oM&~dYdD9DYc-}008y1`mNPW9*`hu z5wI26T@l;!VxgDwU*;V7WHxqaE?W*ZZs^{@&=>nt&7s zk+!3w-j5>z~Z8q=-!bh&6S$U)>ZK|3*(VXTkQ7n>D+I@ z9cTL38$XE;iXGK(K9>H@?eTYdX~Fc6uFa}yO=gXRseee8P&cR44{X-14FySZ07S#f z#3h;lG%!>BXFcGw_||OTvy=thy+@bzhjzq|_f}=V>R3WXDSn2I6w2}ci{VzmeJ@$dHo|Lpi=`>dyWnLb#Pkl`cI%4uD@Hv%xE0l-}46%%Wq z{nWPDYiHaQZ5z{fpVHuyPQRcl{FAS}o_TLd9GAT2Nd$fh0JyDE!40y_h2?}YL7vVR z@~`uLybo|EkRi5E|%UQ=ax1wbCaA@PY0pEN@jZ(f1zvr8@M>;S9*U*!Y z7es&z!1e|OjzAql`aEruBXeygEd^)$#-i_(wOWg{N`cYnvHBl%+i8eevVb}%Lmc+# zbqpZM2wal_3$0Gx(T-D%?LKM7+{I#!M$#tFY0?D;>dL=7D6%E7bVdRtwCS^Pxi<@5 z)#bcXc|LTV>6)REwLjnMT%B{rOUmc3Rb`c4ryOljh@-V=spo6nn|oB6;`{u-?I+9U zSrNTyx|QUShp8Vvz2-|y@VMIPFi-Y=MSAArX(34TCcM-0pOcT*uPrdP;_ms>vgml! zp)%oj1^Hs0`^&trnV`7I>}~Tkp0^FIEmWDejIcksK+hYuWpK*do(@zJ{Gac#42}V= zrMgvT8otUIAcowZ9xTgIT7EG|(@!!Rn)rqK>}21PouyjLbl09HF7cb9d))ay(aJ%i(oM^2M5@U(WLfYs)^q)xAi$Neu@nfA3Pp<-SVuz=6l@HHa40g0ilb5w zJn_JCsP)1L=!{m{G}1a&#gkf5QN)XafZ}MOt;GJD1hHeS5Udv9&+ z?QQh{5I2rD97F3XtKbj-pT{bkOasVWocCa3aRtKy1EU&?Zz5v>Z2XZvIIrXwC*!a zeK96wyQ3=M{LOa#Uwr1>S*=BRhOput9A!=Hlv}s!sz{Cu<5gYSFznliFdt!fea7zm z5&Fhn$IH~<@jr9hBzhwcfHzQ?{%UZ*##)Wn2{KV)HZXt%!@e8@*d#N1%xfHApHa)O zXsV#IjKDD*XTnX53r5+p*rQiYxERJDaEd4JqeiiasSLt}lu#gG7c)0B~#FqB^9Ps86#keC3J6eqoG68)m9eQUikX5Z=UEnxi=v~j)R8895mi)(sL zjfuoP;qH->k%^u*+`kLYR-IZMe>uCExzJ^xDI&=S(ek~)-c8F}c@N`ApK;wM zsi#Wa6EE7|e$C5$T9PtsY-w%fAK}+dc+`rgl+N7t=u%MaIIrA&4V?{jF*WBnvNsd7 z(9#py8#KQ+M?Oe0>`HeR7j)h0E68;IrFP7mZ;Jk$Gwr%d3r=e5E*3{_j(>Wubpw|( z@}Oz%iSomn)~e2|3oS`ipS`0Us5APTIRSWDY3WG@Qy5 zk4>=e)b1Nx3MmN#o&-#H1ejUSLdBD?!03tJivYCZRmCsR?Ysx*BFKSznFKUg8O<>G62KqH=;3Y z7OH>Ox2k(ImQ++f+9zyJ;Nbk$l?KH%y%Jc<+U_j`FN_w{VSptySSwW fUfatN$DO6wp5JkgZdEtRE^j}!x^OwC(~RdIJI72A diff --git a/flight1handler.go b/flight1handler.go index 2860f86cb..ffbf1e5aa 100644 --- a/flight1handler.go +++ b/flight1handler.go @@ -135,6 +135,16 @@ func flight1Generate(c flightConn, state *State, _ *handshakeCache, cfg *handsha } if cfg.mimicryEnabled { + msg := &mimicry.MimickedClientHello{ + Random: state.localRandom, + SessionID: state.SessionID, + Cookie: state.cookie, + } + + msg.LoadFingerprint(cfg.clientHelloFingerprint) + + cfg.localSRTPProtectionProfiles = msg.SRTPProtectionProfiles + return []*packet{ { record: &recordlayer.RecordLayer{ @@ -142,12 +152,7 @@ func flight1Generate(c flightConn, state *State, _ *handshakeCache, cfg *handsha Version: protocol.Version1_2, }, Content: &handshake.Handshake{ - Message: &mimicry.MimickedClientHello{ - ClientHelloFingerprint: cfg.clientHelloFingerprint, - Random: state.localRandom, - SessionID: state.SessionID, - Cookie: state.cookie, - }, + Message: msg, }, }, }, diff --git a/flight3handler.go b/flight3handler.go index 9c356495d..e4cad7d20 100644 --- a/flight3handler.go +++ b/flight3handler.go @@ -289,6 +289,16 @@ func flight3Generate(_ flightConn, state *State, _ *handshakeCache, cfg *handsha } if cfg.mimicryEnabled { + msg := &mimicry.MimickedClientHello{ + Random: state.localRandom, + SessionID: state.SessionID, + Cookie: state.cookie, + } + + msg.LoadFingerprint(cfg.clientHelloFingerprint) + + cfg.localSRTPProtectionProfiles = msg.SRTPProtectionProfiles + return []*packet{ { record: &recordlayer.RecordLayer{ @@ -296,12 +306,7 @@ func flight3Generate(_ flightConn, state *State, _ *handshakeCache, cfg *handsha Version: protocol.Version1_2, }, Content: &handshake.Handshake{ - Message: &mimicry.MimickedClientHello{ - ClientHelloFingerprint: cfg.clientHelloFingerprint, - Random: state.localRandom, - SessionID: state.SessionID, - Cookie: state.cookie, - }, + Message: msg, }, }, }, diff --git a/pkg/mimicry/README.md b/pkg/mimicry/README.md new file mode 100644 index 000000000..aea370dd8 --- /dev/null +++ b/pkg/mimicry/README.md @@ -0,0 +1,28 @@ +### Fingerprint mimicking + +This submodule offers mimicking of the client hello message which is a anti-fingerprint feature for applications that needs censorship-resistance. + +Some sample fingerprints of common browsers are given in `fingerprints.go` (*Note*: the fingerprint is from a webrtc application). Using updated fingerprints is highly recommended, which can be found by an external package such as [dtls-webrtc-fingerprint](https://github.com/theodorsm/dtls-webrtc-fingerprint). + +Example with BYOF (bring your own fingerprints): + +```go +import ( + "github.com/pion/dtls/v2/pkg/mimicry" + "github.com/theodorsm/dtls-webrtc-fingerprint/pkg/fingerprints" +) + + +// Get recent fingerprints +fingerprints := fingerprints.GetClientHelloFingerprints() + +// Choose the freshest fingerprint +fingerprint := fingerprints[len(fingerprints)-1] + +cfg := &dtls.Config{ + Certificates: []tls.Certificate{cert}, + InsecureSkipVerify: true, + MimicryEnabled: true, + ClientHelloFingerprint: mimicry.ClientHelloFingerprint(fingerprint), +} +``` diff --git a/pkg/mimicry/fingerprints.go b/pkg/mimicry/fingerprints.go index 7755d22e0..72b54060f 100644 --- a/pkg/mimicry/fingerprints.go +++ b/pkg/mimicry/fingerprints.go @@ -6,7 +6,7 @@ package mimicry //nolint:revive type ClientHelloFingerprint string -// These fingerprints are added automatically generated and added by the 'fingerprint' workflow +// These fingerprints are sample fingerprints, more can by using the `fingerprints` module at https://github.com/theodorsm/dtls-webrtc-fingerprint // The first byte should correspond to the DTLS version in a handshake message const ( Google_Chrome_124_0_6367_60_unknown ClientHelloFingerprint = "fefda20ee7841620b36a9c1736ea6846255d7da83e9271816b0cc85b7f948951581700000016c02bc02fcca9cca8c009c013c00ac014009c002f00350100004400170000ff01000100000a00080006001d00170018000b0002010000230000000d00140012040308040401050308050501080606010201000e0009000600010008000700" //nolint:revive,stylecheck diff --git a/pkg/mimicry/mimic_client_hello.go b/pkg/mimicry/mimic_client_hello.go index c86afb464..04245295d 100644 --- a/pkg/mimicry/mimic_client_hello.go +++ b/pkg/mimicry/mimic_client_hello.go @@ -8,6 +8,7 @@ import ( "encoding/hex" "errors" + "github.com/pion/dtls/v2/pkg/protocol/extension" "github.com/pion/dtls/v2/pkg/protocol/handshake" ) @@ -15,10 +16,12 @@ var errBufferTooSmall = errors.New("buffer is too small") //nolint:goerr113 //nolint:revive type MimickedClientHello struct { - ClientHelloFingerprint ClientHelloFingerprint + clientHelloFingerprint ClientHelloFingerprint Random handshake.Random SessionID []byte Cookie []byte + Extensions []extension.Extension + SRTPProtectionProfiles []extension.SRTPProtectionProfile } //nolint:revive @@ -26,20 +29,45 @@ func (m MimickedClientHello) Type() handshake.Type { return handshake.TypeClientHello } +func (m *MimickedClientHello) LoadFingerprint(fingerprint ClientHelloFingerprint) error { + m.clientHelloFingerprint = fingerprint + clientHello := handshake.MessageClientHello{} + data, err := hex.DecodeString(string(m.clientHelloFingerprint)) + if err != nil { + return errors.New("mimicry: failed to decode mimicry hexstring") //nolint:goerr113 + } + clientHello.Unmarshal(data) + m.Extensions = clientHello.Extensions + for _, ext := range m.Extensions { + if ext.TypeValue() == extension.UseSRTPTypeValue { + srtp := extension.UseSRTP{} + buf, err := ext.Marshal() + if err != nil { + return err + } + err = srtp.Unmarshal(buf) + if err != nil { + return err + } + m.SRTPProtectionProfiles = srtp.ProtectionProfiles + } + } + return nil +} + //nolint:revive func (m *MimickedClientHello) Marshal() ([]byte, error) { var out []byte - fingerprints := getClientHelloFingerprints() - - if len(fingerprints) < 1 { - return out, errors.New("no fingerprints available") //nolint:goerr113 - } - - fingerprint := m.ClientHelloFingerprint + fingerprint := m.clientHelloFingerprint if string(fingerprint) == "" { + fingerprints := getClientHelloFingerprints() + if len(fingerprints) < 1 { + return out, errors.New("no fingerprints available") //nolint:goerr113 + } fingerprint = fingerprints[len(fingerprints)-1] + m.LoadFingerprint(fingerprint) } data, err := hex.DecodeString(string(fingerprint))