Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: stun protocol & stun connection #9

Merged
merged 44 commits into from
May 24, 2024
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
a53c04f
feat: stun protocol & stun connection
lchenut Mar 8, 2024
6778672
rename getResponse into getPong and test it
lchenut Mar 15, 2024
7be3d0a
add Username attribute
lchenut Mar 15, 2024
f4ca113
genUfrag procedure
lchenut Mar 15, 2024
81a2a51
Add generateRandomSeq to generate a transaction id
lchenut Mar 21, 2024
52296d1
First draft of getPing
lchenut Mar 21, 2024
2b70ad7
Merge remote-tracking branch 'origin/master' into stun-protocol
lchenut Apr 2, 2024
50fde87
Use UdpPacketInfo tuple
lchenut Apr 2, 2024
82203f2
Change closing debug message
lchenut Apr 2, 2024
fa5d674
Add proper exception tracking
lchenut Apr 2, 2024
3d070b5
Change StunConn init behavior
lchenut Apr 2, 2024
762acd8
Add a last UdpPacketInfo
lchenut Apr 2, 2024
5ce6796
Add comments
lchenut Apr 2, 2024
5e9a335
refactor: change connection management
lchenut Apr 5, 2024
832a343
Add a lot of comments/Finish refactor
lchenut Apr 11, 2024
18e302b
Add copyright headers on test files
lchenut Apr 15, 2024
7eb2940
simplify newRng proc for testing
lchenut Apr 15, 2024
f075c40
add exception tracking for stun transport asynchronous proc
lchenut Apr 15, 2024
5100b40
remove ping/pong example building in the ci
lchenut Apr 15, 2024
5c3afe1
rename getPong test
lchenut Apr 15, 2024
4660ac0
remove maximum connections
lchenut Apr 17, 2024
7c36e98
Add ICE stun attributes
lchenut Apr 23, 2024
cf84596
Stun rework
lchenut Apr 26, 2024
b22d6a5
feat: getBindingRequest
lchenut Apr 26, 2024
aada110
fix oversight & add comments
lchenut Apr 26, 2024
9e95f56
Test rework
lchenut Apr 26, 2024
bdf32d9
Adds continue in stunMessageHandler loop
lchenut Apr 29, 2024
6f2dae5
remove `doAssert(false)` from Stun.connect()
lchenut Apr 29, 2024
bccc27f
Update TODO
lchenut Apr 29, 2024
47319e4
fix comment typo
lchenut Apr 29, 2024
9508b79
fix: test lacking precision
lchenut Apr 30, 2024
65bbfba
docs: add StunConn.init() comments
lchenut Apr 30, 2024
d0c4013
chore: make teststun more readable
lchenut Apr 30, 2024
559c857
feat: use withValue instead of getOrDefault in Stun.connect()
lchenut Apr 30, 2024
735cde8
feat: add check if Fingerprint is valid
lchenut Apr 30, 2024
11b4d42
refactor: getAttribute and username/password provider
lchenut May 21, 2024
3a5b206
chore: removes genUfrag, should be in libp2p instead
lchenut May 23, 2024
955b1d2
chore: remove redundant test
lchenut May 24, 2024
285d7fb
chore: change warn log to debug
lchenut May 24, 2024
ea2e56a
docs: update getBindingResponse/Request comments
lchenut May 24, 2024
a1a1acc
chore: renames init into new
lchenut May 24, 2024
0aab612
fix: compilation warnings
lchenut May 24, 2024
f3af915
chore: change closed line to be at the end of the close procedure
lchenut May 24, 2024
96749ab
feat: limit queues size
lchenut May 24, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,4 @@ jobs:
run: |
nim --version
nimble --version
# nimble test
# nim c examples/ping.nim
# nim c examples/pong.nim
nimble test
12 changes: 12 additions & 0 deletions tests/runalltests.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Nim-WebRTC
# Copyright (c) 2024 Status Research & Development GmbH
# Licensed under either of
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
# * MIT license ([LICENSE-MIT](LICENSE-MIT))
# at your option.
# This file may not be copied, modified, or distributed except according to
# those terms.

{.used.}

import teststun
83 changes: 83 additions & 0 deletions tests/teststun.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Nim-WebRTC
# Copyright (c) 2024 Status Research & Development GmbH
# Licensed under either of
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
# * MIT license ([LICENSE-MIT](LICENSE-MIT))
# at your option.
# This file may not be copied, modified, or distributed except according to
# those terms.

{.used.}
diegomrsantos marked this conversation as resolved.
Show resolved Hide resolved

import options, strutils
import bearssl
import ../webrtc/stun/stun_protocol
import ../webrtc/stun/stun_attributes
import ../webrtc/stun/stun_utils
import ./asyncunit

proc newRng(): ref HmacDrbgContext =
diegomrsantos marked this conversation as resolved.
Show resolved Hide resolved
HmacDrbgContext.new()

suite "Stun message encoding/decoding":
test "Stun decoding":
let msg = @[ 0x00'u8, 0x01, 0x00, 0xa4, 0x21, 0x12, 0xa4, 0x42, 0x75, 0x6a, 0x58, 0x46, 0x42, 0x58, 0x4e, 0x72, 0x6a, 0x50, 0x4d, 0x2b, 0x00, 0x06, 0x00, 0x63, 0x6c, 0x69, 0x62, 0x70, 0x32, 0x70, 0x2b, 0x77, 0x65, 0x62, 0x72, 0x74, 0x63, 0x2b, 0x76, 0x31, 0x2f, 0x62, 0x71, 0x36, 0x67, 0x69, 0x43, 0x75, 0x4a, 0x38, 0x6e, 0x78, 0x59, 0x46, 0x4a, 0x36, 0x43, 0x63, 0x67, 0x45, 0x59, 0x58, 0x58, 0x2f, 0x78, 0x51, 0x58, 0x56, 0x4c, 0x74, 0x39, 0x71, 0x7a, 0x3a, 0x6c, 0x69, 0x62, 0x70, 0x32, 0x70, 0x2b, 0x77, 0x65, 0x62, 0x72, 0x74, 0x63, 0x2b, 0x76, 0x31, 0x2f, 0x62, 0x71, 0x36, 0x67, 0x69, 0x43, 0x75, 0x4a, 0x38, 0x6e, 0x78, 0x59, 0x46, 0x4a, 0x36, 0x43, 0x63, 0x67, 0x45, 0x59, 0x58, 0x58, 0x2f, 0x78, 0x51, 0x58, 0x56, 0x4c, 0x74, 0x39, 0x71, 0x7a, 0x00, 0xc0, 0x57, 0x00, 0x04, 0x00, 0x00, 0x03, 0xe7, 0x80, 0x2a, 0x00, 0x08, 0x86, 0x63, 0xfd, 0x45, 0xa9, 0xe5, 0x4c, 0xdb, 0x00, 0x24, 0x00, 0x04, 0x6e, 0x00, 0x1e, 0xff, 0x00, 0x08, 0x00, 0x14, 0x16, 0xff, 0x70, 0x8d, 0x97, 0x0b, 0xd6, 0xa3, 0x5b, 0xac, 0x8f, 0x4c, 0x85, 0xe6, 0xa6, 0xac, 0xaa, 0x7a, 0x68, 0x27, 0x80, 0x28, 0x00, 0x04, 0x79, 0x5e, 0x03, 0xd8 ]
diegomrsantos marked this conversation as resolved.
Show resolved Hide resolved
let stunmsg = StunMessage.decode(msg)
check:
stunmsg.msgType == 1
stunmsg.transactionId.len() == 12
stunmsg.attributes.len() == 6
stunmsg.attributes[0].attributeType == 6 # AttrUsername
stunmsg.attributes[^1].attributeType == 0x8028 # AttrFingerprint

test "Stun encoding":
let transactionId: array[12, byte] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
var msg = StunMessage(msgType: 0x0001'u16, transactionId: transactionId)
msg.attributes.add(ErrorCode.encode(ECUnknownAttribute))
let encoded = msg.encode()
let decoded = StunMessage.decode(encoded)
# cannot do `check msg == decoded` because encode add a Fingerprint
# attribute at the end
check:
decoded.msgType == 1
decoded.transactionId == transactionId
decoded.attributes.len() == 2
decoded.attributes[0].attributeType == 9 # AttrErrorCode
decoded.attributes[^1].attributeType == 0x8028 # AttrFingerprint

test "getBindingResponse":
diegomrsantos marked this conversation as resolved.
Show resolved Hide resolved
let msg = @[0x00'u8, 0x01, 0x00, 0xa4, 0x21, 0x12, 0xa4, 0x42, 0x55, 0x77,
0x4b, 0x71, 0x52, 0x47, 0x31, 0x6d, 0x50, 0x77, 0x67, 0x52,
0x00, 0x06, 0x00, 0x63, 0x6c, 0x69, 0x62, 0x70, 0x32, 0x70,
0x2b, 0x77, 0x65, 0x62, 0x72, 0x74, 0x63, 0x2b, 0x76, 0x31,
0x2f, 0x71, 0x51, 0x45, 0x45, 0x71, 0x44, 0x68, 0x51, 0x47,
0x58, 0x44, 0x7a, 0x71, 0x57, 0x53, 0x38, 0x6e, 0x66, 0x6c,
0x46, 0x74, 0x36, 0x6b, 0x4f, 0x67, 0x4c, 0x48, 0x38, 0x32,
0x51, 0x78, 0x35, 0x3a, 0x6c, 0x69, 0x62, 0x70, 0x32, 0x70,
0x2b, 0x77, 0x65, 0x62, 0x72, 0x74, 0x63, 0x2b, 0x76, 0x31,
0x2f, 0x71, 0x51, 0x45, 0x45, 0x71, 0x44, 0x68, 0x51, 0x47,
0x58, 0x44, 0x7a, 0x71, 0x57, 0x53, 0x38, 0x6e, 0x66, 0x6c,
0x46, 0x74, 0x36, 0x6b, 0x4f, 0x67, 0x4c, 0x48, 0x38, 0x32,
0x51, 0x78, 0x35, 0x00, 0xc0, 0x57, 0x00, 0x04, 0x00, 0x00,
0x03, 0xe7, 0x80, 0x2a, 0x00, 0x08, 0xf3, 0x61, 0x52, 0xaf,
0x6d, 0x50, 0xec, 0x63, 0x00, 0x24, 0x00, 0x04, 0x6e, 0x00,
0x1e, 0xff, 0x00, 0x08, 0x00, 0x14, 0xd4, 0x30, 0x90, 0x22,
0x36, 0xd8, 0x32, 0x44, 0x9b, 0x02, 0x38, 0xa7, 0x81, 0x64,
0x4d, 0xf1, 0xa7, 0x0e, 0x7b, 0xa0, 0x80, 0x28, 0x00, 0x04,
0x21, 0x9b, 0x7b, 0xac]
let response = getBindingResponse(msg, TransportAddress(initTAddress("127.0.0.1:4242")))
check:
response.isSome()
response.get() == @[1'u8, 1, 0, 44, 33, 18, 164, 66, 85, 119, 75, 113, 82, 71, 49, 109, 80, 119, 103, 82, 0, 32, 0, 8, 0, 1, 49, 128, 94, 18, 164, 67, 0, 8, 0, 20, 108, 97, 248, 191, 152, 3, 6, 204, 50, 118, 190, 144, 193, 207, 113, 115, 248, 252, 186, 239, 128, 40, 0, 4, 86, 114, 133, 42]

test "Error while decoding":
let msgLengthFailed = @[ 0x00'u8, 0x01, 0x00, 0xa4, 0x21, 0x12, 0xa4, 0x42, 0x75, 0x6a, 0x58, 0x46, 0x42, 0x58, 0x4e, 0x72, 0x6a, 0x50, 0x4d ]
expect AssertionDefect: discard StunMessage.decode(msgLengthFailed)
let msgAttrFailed = @[ 0x00'u8, 0x01, 0x00, 0x08, 0x21, 0x12, 0xa4, 0x42, 0x75, 0x6a, 0x58, 0x46, 0x42, 0x58, 0x4e, 0x72, 0x6a, 0x50, 0x4d, 0x2b, 0x28, 0x00, 0x05, 0x79, 0x5e, 0x03, 0xd8 ]
expect AssertionDefect: discard StunMessage.decode(msgAttrFailed)

test "genUfrag":
let s = genUfrag(newRng(), 20)
check s.len() == 20
for c in s:
check isAlphaNumeric(c.chr())
4 changes: 2 additions & 2 deletions webrtc.nimble
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,5 @@ proc runTest(filename: string) =
exec excstr & " -r " & " tests/" & filename
diegomrsantos marked this conversation as resolved.
Show resolved Hide resolved
rmFile "tests/" & filename.toExe

# task test, "Run test":
# runTest("runalltests")
task test, "Run test":
runTest("runalltests")
206 changes: 206 additions & 0 deletions webrtc/stun/stun_attributes.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
# Nim-WebRTC
# Copyright (c) 2024 Status Research & Development GmbH
# Licensed under either of
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
# * MIT license ([LICENSE-MIT](LICENSE-MIT))
# at your option.
# This file may not be copied, modified, or distributed except according to
# those terms.

import sequtils, system, typetraits
import binary_serialization,
stew/byteutils,
chronos
import stun_utils

# -- Attributes --
# There are obviously some attributes implementation that are missing,
# it might be something to do eventually if we want to make this
# repository work for other project than nim-libp2p
#
# Stun Attribute
# 0 1 2 3
# 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
# | Type | Length |
# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
# | Value (variable) ....
# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

type
StunAttributeEncodingError* = object of CatchableError

RawStunAttribute* = object
attributeType*: uint16
length* {.bin_value: it.value.len.}: uint16
value* {.bin_len: it.length.}: seq[byte]

proc decode*(T: typedesc[RawStunAttribute], cnt: seq[byte]): seq[RawStunAttribute] =
const pad = @[0, 3, 2, 1]
var padding = 0
while padding < cnt.len():
let attr = Binary.decode(cnt[padding ..^ 1], RawStunAttribute)
result.add(attr)
padding += 4 + attr.value.len()
padding += pad[padding mod 4]

type
StunAttributeEnum* = enum
AttrMappedAddress = 0x0001
AttrChangeRequest = 0x0003 # RFC5780 Nat Behavior Discovery
AttrSourceAddress = 0x0004 # Deprecated
AttrChangedAddress = 0x0005 # Deprecated
AttrUsername = 0x0006
AttrMessageIntegrity = 0x0008
AttrErrorCode = 0x0009
AttrUnknownAttributes = 0x000A
AttrChannelNumber = 0x000C # RFC5766 TURN
AttrLifetime = 0x000D # RFC5766 TURN
AttrXORPeerAddress = 0x0012 # RFC5766 TURN
AttrData = 0x0013 # RFC5766 TURN
AttrRealm = 0x0014
AttrNonce = 0x0015
AttrXORRelayedAddress = 0x0016 # RFC5766 TURN
AttrRequestedAddressFamily = 0x0017 # RFC6156
AttrEvenPort = 0x0018 # RFC5766 TURN
AttrRequestedTransport = 0x0019 # RFC5766 TURN
AttrDontFragment = 0x001A # RFC5766 TURN
AttrMessageIntegritySHA256 = 0x001C # RFC8489 STUN (v2)
AttrPasswordAlgorithm = 0x001D # RFC8489 STUN (v2)
AttrUserhash = 0x001E # RFC8489 STUN (v2)
AttrXORMappedAddress = 0x0020
AttrReservationToken = 0x0022 # RFC5766 TURN
AttrPriority = 0x0024 # RFC5245 ICE
AttrUseCandidate = 0x0025 # RFC5245 ICE
AttrPadding = 0x0026 # RFC5780 Nat Behavior Discovery
AttrResponsePort = 0x0027 # RFC5780 Nat Behavior Discovery
AttrConnectionID = 0x002a # RFC6062 TURN Extensions
AttrPasswordAlgorithms = 0x8002 # RFC8489 STUN (v2)
AttrAlternateDomain = 0x8003 # RFC8489 STUN (v2)
AttrSoftware = 0x8022
AttrAlternateServer = 0x8023
AttrCacheTimeout = 0x8027 # RFC5780 Nat Behavior Discovery
AttrFingerprint = 0x8028
AttrICEControlled = 0x8029 # RFC5245 ICE
AttrICEControlling = 0x802A # RFC5245 ICE
AttrResponseOrigin = 0x802b # RFC5780 Nat Behavior Discovery
AttrOtherAddress = 0x802C # RFC5780 Nat Behavior Discovery
AttrOrigin = 0x802F

proc isRequired*(typ: uint16): bool = typ <= 0x7FFF'u16
proc isOptional*(typ: uint16): bool = typ >= 0x8000'u16

# Username
# https://datatracker.ietf.org/doc/html/rfc5389#section-15.3

type
UsernameAttribute* = object

proc encode(T: typedesc[UsernameAttribute], username: seq[byte]): RawStunAttribute =
result = RawStunAttribute(attributeType: AttrUsername.uint16,
length: username.len().uint16,
value: username)

# Error Code
# https://datatracker.ietf.org/doc/html/rfc5389#section-15.6

type
ErrorCodeEnum* = enum
ECTryAlternate = 300
ECBadRequest = 400
ECUnauthenticated = 401
ECUnknownAttribute = 420
ECStaleNonce = 438
ECServerError = 500
ErrorCode* = object
reserved1: uint16 # should be 0
reserved2 {.bin_bitsize: 5.}: uint8 # should be 0
class {.bin_bitsize: 3.}: uint8
number: uint8
reason: seq[byte]

proc encode*(T: typedesc[ErrorCode], code: ErrorCodeEnum, reason: string = ""): RawStunAttribute =
let
ec = T(class: (code.uint16 div 100'u16).uint8,
number: (code.uint16 mod 100'u16).uint8,
reason: reason.toBytes())
value = Binary.encode(ec)
result = RawStunAttribute(attributeType: AttrErrorCode.uint16,
length: value.len().uint16,
value: value)

# Unknown Attribute
# https://datatracker.ietf.org/doc/html/rfc5389#section-15.9

type
UnknownAttribute* = object
unknownAttr: seq[uint16]

proc encode*(T: typedesc[UnknownAttribute], unknownAttr: seq[uint16]): RawStunAttribute =
let
ua = T(unknownAttr: unknownAttr)
value = Binary.encode(ua)
result = RawStunAttribute(attributeType: AttrUnknownAttributes.uint16,
length: value.len().uint16,
value: value)

# Fingerprint
# https://datatracker.ietf.org/doc/html/rfc5389#section-15.5

type
Fingerprint* = object
crc32: uint32

proc encode*(T: typedesc[Fingerprint], msg: seq[byte]): RawStunAttribute =
let value = Binary.encode(T(crc32: crc32(msg) xor 0x5354554e'u32))
result = RawStunAttribute(attributeType: AttrFingerprint.uint16,
length: value.len().uint16,
value: value)

# Xor Mapped Address
# https://datatracker.ietf.org/doc/html/rfc5389#section-15.2

type
MappedAddressFamily {.size: 1.} = enum
MAFIPv4 = 0x01
MAFIPv6 = 0x02

XorMappedAddress* = object
reserved: uint8 # should be 0
family: MappedAddressFamily
port: uint16
address: seq[byte]

proc encode*(T: typedesc[XorMappedAddress], ta: TransportAddress,
tid: array[12, byte]): RawStunAttribute =
const magicCookie = @[ 0x21'u8, 0x12, 0xa4, 0x42 ]
let
(address, family) =
if ta.family == AddressFamily.IPv4:
var s = newSeq[uint8](4)
for i in 0..3:
s[i] = ta.address_v4[i] xor magicCookie[i]
(s, MAFIPv4)
else:
let magicCookieTid = magicCookie.concat(@tid)
var s = newSeq[uint8](16)
for i in 0..15:
s[i] = ta.address_v6[i] xor magicCookieTid[i]
(s, MAFIPv6)
xma = T(family: family, port: ta.port.distinctBase xor 0x2112'u16, address: address)
value = Binary.encode(xma)
result = RawStunAttribute(attributeType: AttrXORMappedAddress.uint16,
length: value.len().uint16,
value: value)

# Message Integrity
# https://datatracker.ietf.org/doc/html/rfc5389#section-15.4

type
MessageIntegrity* = object
msgInt: seq[byte]

proc encode*(T: typedesc[MessageIntegrity], msg: seq[byte], key: seq[byte]): RawStunAttribute =
let value = Binary.encode(T(msgInt: hmacSha1(key, msg)))
result = RawStunAttribute(attributeType: AttrMessageIntegrity.uint16,
length: value.len().uint16, value: value)
Loading
Loading