Skip to content

Commit

Permalink
feat: stun protocol & stun connection
Browse files Browse the repository at this point in the history
  • Loading branch information
lchenut committed Mar 8, 2024
1 parent a0f6c77 commit a53c04f
Show file tree
Hide file tree
Showing 7 changed files with 482 additions and 3 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,6 @@ jobs:
run: |
nim --version
nimble --version
# nimble test
nimble test
# nim c examples/ping.nim
# 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
36 changes: 36 additions & 0 deletions tests/teststun.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import options
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 ]
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 "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)
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
rmFile "tests/" & filename.toExe

# task test, "Run test":
# runTest("runalltests")
task test, "Run test":
runTest("runalltests")
151 changes: 151 additions & 0 deletions webrtc/stun/stun.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
# 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
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

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 getResponse*(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 new*(T: typedesc[Stun]): T =
result = T()
Loading

0 comments on commit a53c04f

Please sign in to comment.