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 7 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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,6 @@ jobs:
run: |
nim --version
nimble --version
# nimble test
nimble test
# nim c examples/ping.nim
diegomrsantos marked this conversation as resolved.
Show resolved Hide resolved
# nim c examples/pong.nim
3 changes: 3 additions & 0 deletions tests/runalltests.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{.used.}

import teststun
67 changes: 67 additions & 0 deletions tests/teststun.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import options, strutils
import ../webrtc/stun/stun
import ../webrtc/stun/stun_attributes
import ./asyncunit

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 "getPong":
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 = Stun.getPong(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(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")
171 changes: 171 additions & 0 deletions webrtc/stun/stun.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
# 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 bitops, strutils, random
import chronos,
chronicles,
binary_serialization,
stew/objects,
stew/byteutils
import stun_attributes

export binary_serialization

logScope:
topics = "webrtc stun"

const
msgHeaderSize = 20
magicCookieSeq = @[ 0x21'u8, 0x12, 0xa4, 0x42 ]
magicCookie = 0x2112a442
BindingRequest = 0x0001'u16
BindingResponse = 0x0101'u16

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
# Stun Header
# 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
# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
# |0 0| STUN Message Type | Message Length |
# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
# | Magic Cookie |
# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
# | |
# | Transaction ID (96 bits) |
# | |
# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
# Message type:
# 0x0001: Binding Request
# 0x0101: Binding Response
# 0x0111: Binding Error Response
# 0x0002: Shared Secret Request
# 0x0102: Shared Secret Response
# 0x0112: Shared Secret Error Response

RawStunMessage = object
msgType: uint16
length* {.bin_value: it.content.len().}: uint16
magicCookie: uint32
transactionId: array[12, byte] # Down from 16 to 12 bytes in RFC5389
content* {.bin_len: it.length.}: seq[byte]

StunMessage* = object
msgType*: uint16
transactionId*: array[12, byte]
attributes*: seq[RawStunAttribute]

Stun* = object
iceTiebreaker: uint64

proc generateRandomSeq(size: int): seq[byte] =
result = newSeq[byte](size)
for i in 0..<size:
result[i] = rand(255).uint8
diegomrsantos marked this conversation as resolved.
Show resolved Hide resolved

proc getAttribute(attrs: seq[RawStunAttribute], typ: uint16): Option[seq[byte]] =
for attr in attrs:
if attr.attributeType == typ:
return some(attr.value)
return none(seq[byte])

proc isMessage*(T: typedesc[Stun], msg: seq[byte]): bool =
msg.len >= msgHeaderSize and msg[4..<8] == magicCookieSeq and bitand(0xC0'u8, msg[0]) == 0'u8

proc addLength(msgEncoded: var seq[byte], length: uint16) =
let
hi = (length div 256'u16).uint8
lo = (length mod 256'u16).uint8
msgEncoded[2] = msgEncoded[2] + hi
if msgEncoded[3].int + lo.int >= 256:
msgEncoded[2] = msgEncoded[2] + 1
msgEncoded[3] = ((msgEncoded[3].int + lo.int) mod 256).uint8
else:
msgEncoded[3] = msgEncoded[3] + lo

proc decode*(T: typedesc[StunMessage], msg: seq[byte]): StunMessage =
let smi = Binary.decode(msg, RawStunMessage)
return T(msgType: smi.msgType,
transactionId: smi.transactionId,
attributes: RawStunAttribute.decode(smi.content))

proc encode*(msg: StunMessage, userOpt: Option[seq[byte]] = none(seq[byte])): seq[byte] =
const pad = @[0, 3, 2, 1]
var smi = RawStunMessage(msgType: msg.msgType,
magicCookie: magicCookie,
transactionId: msg.transactionId)
for attr in msg.attributes:
smi.content.add(Binary.encode(attr))
smi.content.add(newSeq[byte](pad[smi.content.len() mod 4]))

result = Binary.encode(smi)

if userOpt.isSome():
let username = string.fromBytes(userOpt.get())
let usersplit = username.split(":")
if usersplit.len() == 2 and usersplit[0].startsWith("libp2p+webrtc+v1/"):
result.addLength(24)
result.add(Binary.encode(MessageIntegrity.encode(result, toBytes(usersplit[0]))))

result.addLength(8)
result.add(Binary.encode(Fingerprint.encode(result)))

proc getPong*(
T: typedesc[Stun],
msg: seq[byte],
ta: TransportAddress
): Option[seq[byte]] =
if ta.family != AddressFamily.IPv4 and ta.family != AddressFamily.IPv6:
return none(seq[byte])
let sm =
try:
StunMessage.decode(msg)
except CatchableError as exc:
return none(seq[byte])

if sm.msgType != BindingRequest:
return none(seq[byte])

var res = StunMessage(msgType: BindingResponse,
transactionId: sm.transactionId)

var unknownAttr: seq[uint16]
for attr in sm.attributes:
let typ = attr.attributeType
if typ.isRequired() and typ notin StunAttributeEnum:
unknownAttr.add(typ)
if unknownAttr.len() > 0:
res.attributes.add(ErrorCode.encode(ECUnknownAttribute))
res.attributes.add(UnknownAttribute.encode(unknownAttr))
return some(res.encode(sm.attributes.getAttribute(AttrUsername.uint16)))

res.attributes.add(XorMappedAddress.encode(ta, sm.transactionId))
return some(res.encode(sm.attributes.getAttribute(AttrUsername.uint16)))

proc getPing*(
T: typedesc[Stun],
ta: TransportAddress,
username: seq[byte] = @[],
iceControlling: bool = true
): seq[byte] =
var res = StunMessage(msgType: BindingRequest,
transactionId: generateRandomSeq(12))
if username != @[]:
res.attributes.add(UsernameAttribute.encode(username))

proc new*(T: typedesc[Stun]): T =
result = T(iceTiebreaker: rand(uint64))
Loading
Loading