Skip to content

Conversation

@penhauer
Copy link

Description

For supporting ECN, it was needed to change the PacketBuffer so it can contain attributes like ECN or TOS. The new PacketBuffer can store packets with additional variable length attributes.

For the rest of pion, it was needed to add a ReadWithAttributes/WriteWithAttributes method to support reading the said packet "features". Many structs involved in the packet path from the socket to the ReceiveRTP method implement either PacketConn or NetConn. These interfaces however, cannot accommodate the needed ReadWithAttributes method. It is exactly the same problem for having a []byte as a packet, as it prevents further feature development. For the said reasons, Two interfaces were introduced, one for PacketConn and one for NetConn.

The names are not the best names, so any suggestions would be appreciated.

@codecov
Copy link

codecov bot commented Nov 13, 2025

Codecov Report

❌ Patch coverage is 71.81818% with 31 lines in your changes missing coverage. Please review.
✅ Project coverage is 83.99%. Comparing base (6c2e335) to head (1b3f0a8).
⚠️ Report is 7 commits behind head on master.

Files with missing lines Patch % Lines
packetattributes.go 0.00% 17 Missing ⚠️
connsocket.go 0.00% 13 Missing ⚠️
packetio/buffer.go 98.75% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #365      +/-   ##
==========================================
+ Coverage   81.67%   83.99%   +2.31%     
==========================================
  Files          37       40       +3     
  Lines        3564     3174     -390     
==========================================
- Hits         2911     2666     -245     
+ Misses        521      374     -147     
- Partials      132      134       +2     
Flag Coverage Δ
go 83.99% <71.81%> (+2.31%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Member

@JoTurk JoTurk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is exciting, thank you! And sorry for all the comments, I tried to avoid anything super nitpicky. adding new APIs is always a bit hard .
It'd be great to also have tests covering the attributes path.

Comment on lines +173 to +175
func (b *Buffer) Write(buff []byte) (n int, err error) { //nolint:cyclop
return b.WriteWithAttributes(buff, nil)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we shouldn't change the normal write path, this is a behavior change, and it will break applications that depend on this logic.

Copy link
Author

@penhauer penhauer Nov 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi Joe, I will address the comments one by one. Thank you for your great feedback.

No it will not break it. Whether you write the a packet with attributes or no attributes (nil in this case), an additional byte segment will be stored in the buffer. The buffer saves the packet length as two bytes first, and then the packet itself. With my changes, every written packet will have two segments. The first segment is like before, so two bytes for length and the rest for the packet. The second segment stores the attributes, first the attributes length (two bytes for that) and the attributes payload. When reading from buffer, regardless of method, both segments are consumed from the buffer (the packet and the payload itself). When writing into the buffer, regardless of having attributes present or not, a segment of size 2+ (2 for the segment length) will be stored in the buffer. Now if the attribtues are nil, only the empty segment's length (=2) will be stored.

Also, If it were to break anything, I couldn't have my sender and receiver working. However, they work fine and all the ICE candidate exchange and RTP/RTCP exchange work properly.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're not the only users of pion/transport, even if receive and send works for us. If a user writes 2 bytes using Write and now it writes 4 bytes. this is a breaking behavior, even if the API itself didn't change.
We can add like a private flag or something hasAttributes.

Copy link
Author

@penhauer penhauer Nov 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No you're not on the right track. What does a user of PacketBuffer see? They are concerned with writing a packet properly to the buffer, and reading one packet properly from the buffer. The internals of the buffer are not the user's business.

In your case, we write 2 bytes (2 bytes only for the payload of the packet) to the buffer but:

  • We return 2 to the user (only the length of payload's bytes written to the packet buffer) so the user does not even understand more bytes are stored actually
  • When we read from the buffer, we consume all the 4 bytes written and still tel the user we read 2 bytes so no behavior change here either.

P.S. Is your concern the method Size? I didn't pay attention, yes the behavior of Size shouldn't change you're right. I thought it's internal to the buffer. I will fix this.

Copy link
Member

@JoTurk JoTurk Nov 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not correct, because in packetio.Buffer the internal byte count directly affects observable behavior through:

  1. Size()
  2. SetLimitSize()
  3. SetLimitCount()
  4. backpressure via Write() returning ErrFull when the buffer is full, now we have extra bytes.
  5. how grow() decides the allocation size
  6. how many packets fit before being dropped/rejected

So even if Write() returns the same n and Read() returns the same n, the extra internal bytes materially change the buffer's behavior, it's not just about size.

Copy link
Author

@penhauer penhauer Nov 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now I see your point. Thanks for your patience. Yes you're right. I checked the methods that call Size() in other repos. It seems like they are not using it so it's almost internal method. For SetLimitSize(), it seems like the callers are not that exact in the set size as they set the size to 100*1000 for example, so the additional 2 bytes practically change nothing in the behavior. Now If you say we should maintain the exact exact same behavior I will do this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you, and again i'm sorry, introducing / changing APIs is always hard, and pion/transport touches every library we have.

Comment on lines 11 to 15
// for serializing
Marshal() []byte

// for de-serializing. The bytes will be copied into the returned buffer
GetBuffer() []byte
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe marshal/Unmarshal or Bytes/Scratch or any common serialize/de-serialize naming convention that is common in Go.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes yes I'm familiar with that. My idea was, GetBuffer returns the attributes buffer so bytes can be directly copied into the PacketAttributes' buffer instead of copying first into a temp buffer and then calling UnMarshall. The reason is, through a packet's journey, a packet is written/read to the packet buffer multiple times.

If you think there's a better option or UnMarshall is a good enough name, I'm fine with that.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like marshal/unmarshall personally, but generally it's better to follow the naming conventions. as long as you also have marshal.


const attrMaxLen = 1

type ECN uint8
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be better if transport's packet-attributes stayed generic, leaving ECN and other metadata details to higher-level application logic.

Because the way i see it is that ECN is a semantics not mechanism, and transport is just a thin layer / helper library, and it shouldn't care about details.

this comment also extends to ECN and WithECN..

}

type PacketAttributes struct {
buffer [attrMaxLen]byte
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should make the attribute size dynamic and add a byte to determine how many attributes are there so we don't have to make attributesv2 in the future.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No we don't really need to make attributesv2 in the future. If we were to add more fields to the PacketAttributes we can just do it. The packet buffer is agnostic of PacketAttributes' buffer length and it can work with any variable sized PacketAttributes.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My comment was more about that we don't need to touch pion/transport to add new attributes, we should make it generic, IMO: no attrMaxLen, no ECN aware. just generic attributes reader and writer.


func NewPacketAttributes() *PacketAttributes {
p := &PacketAttributes{}
p.Reset()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to call reset here?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No. You're right!

type NetConnSocket interface {
net.Conn

ReadWithAttributes(p []byte, attr *PacketAttributes) (n int, err error)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be better if we unify attribute types in the APIs.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think having a generic packet attributes type with variable buffer length would address this comment right?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah a generic packet attributes will be cool, good idea.

assert.NoError(err)
assert.Equal(2, n)
assert.Equal(4, buffer.Size())
assert.Equal(6, buffer.Size())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is related to the comment about buffer.Write. we shouldn't change the test, and size shouldn't count attributes imo (not 100% sure).

Copy link
Author

@penhauer penhauer Nov 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I explained that buffer's write writes two segments in the buffer per packet. So this has to change. Also if you're not confident that it works, I have added a test at the end where I write some packets without attributes and read the same packets in order with attributes. It doesn't break any functionality.

P.S. yes Size is a public facing method so its behavior shouldn't change. I will update this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not about that it's working :) we shouldn't change how Size works for the users that didn't use attributes :)

Comment on lines 298 to 307
b.advanceHead(count)

// read the attributes segment
aLen := b.getSegmentLength()
if aLen > 0 {
if attr != nil {
b.writeToInputBuffer(attr.GetBuffer(), aLen)
}
b.advanceHead(aLen)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there is no guarantee len(GetBuffer()) >= aLen, if someone implements their own attributes interface in the future or something breaks down the line, this code will be unsafe.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes you're right. I missed this one.


if attr != nil {
// store the attributes buffer itself
b.writeFromInputBuffer(attr.Marshal())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make it so we call attr.Marshal once?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes

connsocket.go Outdated
Comment on lines 31 to 60
func NewNetConnToNetConnSocket(conn net.Conn) *NetConnToNetConnSocket {
return &NetConnToNetConnSocket{conn: conn}
}

// ReadWithAttributes reads from the underlying connection and ignores attributes.
func (p *NetConnToNetConnSocket) ReadWithAttributes(b []byte, _ *PacketAttributes) (int, error) {
return p.conn.Read(b)
}

// Delegate net.Conn methods to the underlying connection.
func (p *NetConnToNetConnSocket) Read(b []byte) (int, error) { return p.conn.Read(b) }
func (p *NetConnToNetConnSocket) Write(b []byte) (int, error) { return p.conn.Write(b) }
func (p *NetConnToNetConnSocket) Close() error { return p.conn.Close() }
func (p *NetConnToNetConnSocket) LocalAddr() net.Addr { return p.conn.LocalAddr() }
func (p *NetConnToNetConnSocket) RemoteAddr() net.Addr { return p.conn.RemoteAddr() }
func (p *NetConnToNetConnSocket) SetDeadline(t time.Time) error { return p.conn.SetDeadline(t) }
func (p *NetConnToNetConnSocket) SetReadDeadline(t time.Time) error { return p.conn.SetReadDeadline(t) }
func (p *NetConnToNetConnSocket) SetWriteDeadline(t time.Time) error {
return p.conn.SetWriteDeadline(t)
}

type PacketConnToPacketConnSocket struct {
conn net.PacketConn
}

func NewPacketConnToPacketConnSocket(conn net.PacketConn) *PacketConnToPacketConnSocket {
return &PacketConnToPacketConnSocket{
conn: conn,
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you please explain why we need these two structs and why we need to make them public?
I also think the name of PacketConnToPacketConnSocket can be better.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The answer is not in transport but in other repos like srtp and webrtc. The issue is, the socket is abstracted with either net.Conn or net.PacketConn in the code. For example take mux.

mux implements net.Conn so it has a method Read or Write (just like a socket) but I needed to tell my mux to ReadWithAttributes. This is why these two structs are defined.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't this just be a helper-cast function that simply returns an interface, err without exposing a new public struct?

Copy link
Author

@penhauer penhauer Nov 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean something like?

var conn net.Conn

if s, ok := conn.(*NetConnSocket); ok != nil {
   // some code
}

yes this is doable. I'm not much experienced in Go, but I just learned this for the changes I made, so initially I didn't think of this.

@penhauer
Copy link
Author

Thanks @joeturki for your comments. I will work on a revised version and will come back soon.

@penhauer
Copy link
Author

@joeturki What do you think about the new changes?

You mentioned the problem with methods

  • Size()
  • SetLimitSize()
  • SetLimitCount()

For SetLimitCount there's no behavior change. No behavior change for the third one either but for Size I believe we're fine with the changes. The whole thing is random and depends on the length of the packets received, the order that we push in the buffer (so affected by network jitter) and ... . So about your comment about back pressure, all of the calculations for setting the buffer size are approximate, and the additional 2 bytes wouldn't kill anybody.

@JoTurk
Copy link
Member

JoTurk commented Nov 14, 2025

@penhauer Hey, first of all, we're all just trying to have fun here, so please let's just avoid non-technical phrasing, this is the second time you did this :)

Our only requirement in Pion is that we never change existing behavior (API or logic) unless we have a major upgrade, other than that everything is a fair game. and as I said, we can meet that easily by adding a private flag for buffers constructed with attributes, or by keeping an extra buffer for attributes (nil by default). This keeps everything fully backward-compatible.

This isn't an internal library so the network behavior doesn't matter here, because again, this is a public facing library, and with non-breaking changes we typically don't have to change unit tests for public facing functions.

@penhauer
Copy link
Author

@joeturki

Sorry I didn't mean that in a bad way but your comment is fair.

How about introducing a new PacketBuffer that keeps the attributes, and leave the original one intact and then change the type of PacketBuffers in other places like mux and endpoint and ... . I think this would be cleaner.

@JoTurk
Copy link
Member

JoTurk commented Dec 2, 2025

@penhauer Sorry i got sick (fever) and lost track of this, I'm just back as active now, I think we can keep the same PacketBuffer and add a flag if it's constructed with attributes or not. New PacketBuffer is also fine if it's the only way, we just try to:

  1. Not break old APIs (so we can't change the tests, because we added more bytes).
  2. If we add a new API it should be extendable and isn't just hard-coded to fix a single issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants