Skip to content

Commit

Permalink
implement parsing of the ADDRESS_ASSIGN capsule
Browse files Browse the repository at this point in the history
  • Loading branch information
marten-seemann committed Oct 5, 2024
1 parent 595ebd6 commit 20d698f
Show file tree
Hide file tree
Showing 4 changed files with 292 additions and 0 deletions.
80 changes: 80 additions & 0 deletions capsule.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package masque

import (
"errors"
"fmt"
"io"
"net/netip"

"github.com/quic-go/quic-go/http3"
"github.com/quic-go/quic-go/quicvarint"
)

const capsuleTypeAddressAssign http3.CapsuleType = 1

// addressAssignCapsule represents an ADDRESS_ASSIGN capsule
type addressAssignCapsule struct {
AssignedAddresses []AssignedAddress
}

// AssignedAddress represents an Assigned Address within an ADDRESS_ASSIGN capsule
type AssignedAddress struct {
RequestID uint64
IPPrefix netip.Prefix
}

func parseAddressAssignCapsule(r io.Reader) (*addressAssignCapsule, error) {
var assignedAddresses []AssignedAddress
for {
addr, err := parseAssignedAddress(r)
if err != nil {
if err == io.EOF {
break
}
return nil, err
}
assignedAddresses = append(assignedAddresses, addr)
}
return &addressAssignCapsule{AssignedAddresses: assignedAddresses}, nil
}

func parseAssignedAddress(r io.Reader) (AssignedAddress, error) {
vr := quicvarint.NewReader(r)
requestID, err := quicvarint.Read(vr)
if err != nil {
return AssignedAddress{}, err
}
ipVersion, err := vr.ReadByte()
if err != nil {
return AssignedAddress{}, err
}
var ip netip.Addr
switch ipVersion {
case 4:
var ipv4 [4]byte
if _, err := io.ReadFull(r, ipv4[:]); err != nil {
return AssignedAddress{}, err
}
ip = netip.AddrFrom4(ipv4)
case 6:
var ipv6 [16]byte
if _, err := io.ReadFull(r, ipv6[:]); err != nil {
return AssignedAddress{}, err
}
ip = netip.AddrFrom16(ipv6)
default:
return AssignedAddress{}, fmt.Errorf("invalid IP version: %d", ipVersion)
}
prefixLen, err := vr.ReadByte()
if err != nil {
return AssignedAddress{}, err
}
if int(prefixLen) > ip.BitLen() {
return AssignedAddress{}, fmt.Errorf("prefix length %d exceeds IP address length (%d)", prefixLen, ip.BitLen())
}
prefix := netip.PrefixFrom(ip, int(prefixLen))
if prefix != prefix.Masked() {
return AssignedAddress{}, errors.New("lower bits not covered by prefix length are not all zero")
}
return AssignedAddress{RequestID: requestID, IPPrefix: prefix}, nil
}
119 changes: 119 additions & 0 deletions capsule_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package masque

import (
"bytes"
"io"
"net/netip"
"testing"

"github.com/quic-go/quic-go/http3"
"github.com/quic-go/quic-go/quicvarint"

"github.com/stretchr/testify/require"
)

func TestParseAddressAssignCapsule(t *testing.T) {
addr1 := quicvarint.Append(nil, 1337) // Request ID
addr1 = append(addr1, 4) // IPv4
addr1 = append(addr1, netip.AddrFrom4([4]byte{1, 2, 3, 0}).AsSlice()...)
addr1 = append(addr1, 24) // IP Prefix Length
addr2 := quicvarint.Append(nil, 1338) // Request ID
addr2 = append(addr2, 6) // IPv6
addr2 = append(addr2, netip.MustParseAddr("2001:db8::1").AsSlice()...)
addr2 = append(addr2, 128) // IP Prefix Length

data := quicvarint.Append(nil, uint64(capsuleTypeAddressAssign))
data = quicvarint.Append(data, uint64(len(addr1)+len(addr2))) // Length
data = append(data, addr1...)
data = append(data, addr2...)

r := bytes.NewReader(data)
typ, cr, err := http3.ParseCapsule(r)
require.NoError(t, err)
require.Equal(t, capsuleTypeAddressAssign, typ)
capsule, err := parseAddressAssignCapsule(cr)
require.NoError(t, err)
require.Equal(t,
[]AssignedAddress{
{RequestID: 1337, IPPrefix: netip.MustParsePrefix("1.2.3.0/24")},
{RequestID: 1338, IPPrefix: netip.MustParsePrefix("2001:db8::1/128")},
},
capsule.AssignedAddresses,
)
require.Zero(t, r.Len())
}

func TestParseAddressAssignCapsuleInvalid(t *testing.T) {
t.Run("invalid IP version", func(t *testing.T) {
addr1 := quicvarint.Append(nil, 1337) // Request ID
addr1 = append(addr1, 5) // Invalid IP version (not 4 or 6)
addr1 = append(addr1, netip.AddrFrom4([4]byte{1, 2, 3, 4}).AsSlice()...)
addr1 = append(addr1, 32) // IP Prefix Length
data := quicvarint.Append(nil, uint64(capsuleTypeAddressAssign))
data = quicvarint.Append(data, uint64(len(addr1))) // Length
data = append(data, addr1...)

_, cr, err := http3.ParseCapsule(bytes.NewReader(data))
require.NoError(t, err)
_, err = parseAddressAssignCapsule(cr)
require.ErrorContains(t, err, "invalid IP version: 5")
})

t.Run("invalid prefix length", func(t *testing.T) {
addr1 := quicvarint.Append(nil, 1337) // Request ID
addr1 = append(addr1, 4) // IPv4
addr1 = append(addr1, netip.AddrFrom4([4]byte{1, 2, 3, 4}).AsSlice()...)
addr1 = append(addr1, 33) // too long IP Prefix Length
data := quicvarint.Append(nil, uint64(capsuleTypeAddressAssign))
data = quicvarint.Append(data, uint64(len(addr1))) // Length
data = append(data, addr1...)

_, cr, err := http3.ParseCapsule(bytes.NewReader(data))
require.NoError(t, err)
_, err = parseAddressAssignCapsule(cr)
require.ErrorContains(t, err, "prefix length 33 exceeds IP address length (32)")
})

t.Run("lower bits not covered by prefix length are not all zero", func(t *testing.T) {
addr1 := quicvarint.Append(nil, 1337) // Request ID
addr1 = append(addr1, 4) // IPv4
addr1 = append(addr1, netip.AddrFrom4([4]byte{1, 2, 3, 4}).AsSlice()...) // non-zero lower bits
addr1 = append(addr1, 28) // IP Prefix Length
data := quicvarint.Append(nil, uint64(capsuleTypeAddressAssign))
data = quicvarint.Append(data, uint64(len(addr1))) // Length
data = append(data, addr1...)

_, cr, err := http3.ParseCapsule(bytes.NewReader(data))
require.NoError(t, err)
_, err = parseAddressAssignCapsule(cr)
require.ErrorContains(t, err, "lower bits not covered by prefix length are not all zero")
})

t.Run("incomplete capsule", func(t *testing.T) {
addr1 := quicvarint.Append(nil, 1337) // Request ID
addr1 = append(addr1, 4) // IPv4
addr1 = append(addr1, netip.AddrFrom4([4]byte{1, 2, 3, 4}).AsSlice()...)
addr1 = append(addr1, 32) // IP Prefix Length
data := quicvarint.Append(nil, uint64(capsuleTypeAddressAssign))
data = quicvarint.Append(data, uint64(len(addr1))) // Length
data = append(data, addr1...)

_, cr, err := http3.ParseCapsule(bytes.NewReader(data))
require.NoError(t, err)
_, err = parseAddressAssignCapsule(cr)
require.NoError(t, err)
for i := range data {
_, cr, err := http3.ParseCapsule(bytes.NewReader(data[:i]))
if err != nil {
if i == 0 {
require.ErrorIs(t, err, io.EOF)
} else {
require.ErrorIs(t, err, io.ErrUnexpectedEOF)
}
continue
}
_, err = parseAddressAssignCapsule(cr)
require.ErrorIs(t, err, io.ErrUnexpectedEOF)
}
})
}
25 changes: 25 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,28 @@
module github.com/quic-go/connect-ip-go

go 1.22

require (
github.com/quic-go/quic-go v0.47.1-0.20241002141227-b2233591adc7
github.com/stretchr/testify v1.9.0
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/onsi/ginkgo/v2 v2.9.5 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
go.uber.org/mock v0.4.0 // indirect
golang.org/x/crypto v0.26.0 // indirect
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
golang.org/x/mod v0.17.0 // indirect
golang.org/x/net v0.28.0 // indirect
golang.org/x/sys v0.23.0 // indirect
golang.org/x/text v0.17.0 // indirect
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
68 changes: 68 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q=
github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k=
github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.47.1-0.20241002141227-b2233591adc7 h1:/yqMifKM4Z946YBFFUiysDy5jUlCxMNuCVfxArX/eMg=
github.com/quic-go/quic-go v0.47.1-0.20241002141227-b2233591adc7/go.mod h1:yBgs3rWBOADpga7F+jJsb6Ybg1LSYiQvwWlLX+/6HMs=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM=
golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

0 comments on commit 20d698f

Please sign in to comment.