ospc

package
v0.0.0-...-b38deca Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 29, 2026 License: MIT Imports: 32 Imported by: 0

README

OpenScreen Network
Network Protocol

OpenScreen Protocol network layer implementation

W3C Specification


Features

  • QUIC transport with ALPN negotiation
  • mDNS discovery and advertisement
  • PSK authentication (SPAKE2)
  • Agent fingerprint verification
  • CBOR message encoding

Authentication

SPAKE2 follows network.bs "Authentication with SPAKE2": identities A/B are the client's and server's agent fingerprints, the password is the base-10 PSK of Appendix B with dashes and leading zeros removed (ParsePSK, FormatPSK), hashed with SHA-512, and either the PSK presenter (psk-shown) or the PSK consumer (psk-needs-presentation) may initiate.

One deviation remains: the spec's edwards25519 suite is not implemented. Agents must opt in to the RFC 9382 P-256 suite with InteropQuirks.Spake2SuiteP256 (Chromium's osp_demo --spake2_suite=p256); without it AuthenticatePSK fails with ErrSpake2SuiteUnsupported.

LP2P sessions

The LP2P session extension (local-peer-to-peer index.bs "Session extension", spec commit 4761890) is opt-in: AgentConfig.SessionExtension on a listening agent, DiscoveredAgent.DialSession on the dialing one. Plain OSP agents are unaffected, which keeps the osp_demo and lp2p_demo interop working.

  • Both agents exchange agent-info and auth-capabilities as in OSP; only the client ever starts SPAKE2. As the PSK consumer it asks the presenter to show the PSK (StartAuthentication, see the spec gap below).
  • The client verifies the server first (AuthenticatePSK, or TrustPeer for a trusted fingerprint), then sends lp2p-session-request with the origin (RequestSession, only to an agent listing data-channels or quic-transport, else ErrSessionUnsupported).
  • A server that hasn't verified the client answers the provisional authentication-required (RequireAuthentication) without looking at the origin; the client then authenticates. Once the client is verified the origin is available (RequestingOrigin) and the server accepts (CompleteSession, success) or refuses (RefuseSession, permanent-error, then the connection is closed once the client ended the stream). Agents that trust each other run no ceremony.
  • AwaitSession reports the cues on both sides: authenticate, requested (server), accepted (client). Neither side hands the connection to the application protocol before the session is accepted; streams that arrive earlier are held, and discarded if the connection closes first.
  • A second lp2p-session-request, or one received by an agent without the extension, is refused and closes the connection.

Spec gap

Under 4761890 a client meeting a server it hasn't verified must start a trust ceremony, and only the client starts one. But network.bs has a PSK consumer initiate with "psk-needs-presentation and the public-value field set to pA", which needs the PSK the presenter hasn't shown yet, and with equal ease of input the server presents. go-lp2p assumes the OSP fix the LP2P editors proposed: a consumer's psk-needs-presentation carries no public-value; the presenter shows the PSK and initiates with psk-shown and pA, and the consumer answers psk-input with pB. The old form, with pA, is only sent with InteropQuirks.ConsumerInitiatesWithPublicValue, for Chromium's openscreen when it presents (it never sends psk-needs-presentation itself and reads public-value as pA). Received psk-needs-presentation with pA is still answered.

WebTransport

The WebTransport extension (index.bs "WebTransport extension") pools WebTransportSessions on the connection: OpenWebTransport and AcceptWebTransport (accept or refuse), bidirectional and unidirectional streams starting with data-transport-stream, datagrams as QUIC DATAGRAM frames prefixed with the exchange-id (QUIC datagrams are enabled on every connection), Close with code and reason (data-transport-close then FIN; it waits for the peer's FIN so a connection close right after doesn't lose it), Drain, and ExportKeyingMaterial with the exporter context of WebTransport over HTTP/3. Stream error codes 0..2^32-1 map to QUIC application error codes; 2^32 is session-gone, used for streams of unknown or closed sessions (held for webTransportHoldTimeout while the session may still be opening). Sessions aren't opened towards a peer that doesn't list quic-transport. Dedicated (q2q) connections aren't implemented.

Listeners

Listener.Close doesn't close the connections it accepted. quic-go closes a server's connections with its listener, so the QUIC listener and its UDP socket are closed once the last accepted connection ends; handshakes that complete meanwhile are closed right away.

Data channels

The LP2P data channel extension (local-peer-to-peer index.bs "Data channel extension") follows the spec and interoperates with openscreen's lp2p_demo (test/lp2p-demo-interop). Where the spec is silent, ospc does what openscreen does:

  • The acceptor answers a data-channel-open-request with a data-channel-open-response on the request's bidirectional stream, before any data-frame. OpenDataChannel returns only after a success response; any other result fails it with ErrDataChannelRejected, and no response within DataChannelOpenTimeout (or the context) fails it too.
  • A channel-id above 65,534 is answered with permanent-error.
  • Without an explicit ID, the QUIC client (requester side) allocates even IDs and the QUIC server (receiver side) odd IDs: the lowest ID of its parity not used by an open channel of the connection. An explicit ID that is in use fails with ErrDataChannelIDInUse, one of the peer's parity with ErrDataChannelIDParity. IDs from the peer are accepted whatever their parity; channels are told apart by their stream.
  • Closing a channel closes the sending side of its stream (there is no close message).

License

MIT License

Documentation

Index

Examples

Constants

View Source
const (
	// DefaultIdleTimeout is the recommended QUIC idle_timeout transport
	// parameter.
	DefaultIdleTimeout = 25 * time.Second
	// DefaultKeepAliveInterval is the default time without sending any message
	// after which an agent-status-request is sent. It has to be less than the
	// idle timeout; half of it leaves room for loss and round trips.
	DefaultKeepAliveInterval = DefaultIdleTimeout / 2
)
View Source
const (
	AgentCapabilityDataChannels   msgAgentCapability = 1100
	AgentCapabilityQuickTransport msgAgentCapability = 1200
)
View Source
const (
	DataChannelEncodingIdEncodingIdBlob        msgDataChannelEncodingId = 0
	DataChannelEncodingIdEncodingIdString      msgDataChannelEncodingId = 1
	DataChannelEncodingIdEncodingIdArrayBuffer msgDataChannelEncodingId = 2
)
View Source
const (
	DataEncodingBinary = iota
	DataEncodingString
	DataEncodingArrayBuffer
)
View Source
const (
	// MaxExchangeID is the largest exchange-id of a WebTransport session:
	// `exchange-id = uint .lt 4611686018427387904` (below 2^62).
	MaxExchangeID = 1<<62 - 1
	// MaxWebTransportCloseCode is the largest close-code of
	// data-transport-close: `uint .size 4`.
	MaxWebTransportCloseCode = 1<<32 - 1
	// MaxWebTransportReasonLen is the longest reason of data-transport-close
	// in bytes: `text .size (0..1024)`.
	MaxWebTransportReasonLen = 1024
)
View Source
const (
	LP2PSessionResultSuccess                                     = msgLp2pSessionResult(ResultSuccess)
	LP2PSessionResultPermanentError                              = msgLp2pSessionResult(ResultPermanentError)
	LP2PSessionResultAuthenticationRequired msgLp2pSessionResult = 1110
)
View Source
const (
	AgentCapabilityReceiveAudio          msgAgentCapability = 1
	AgentCapabilityReceiveVideo          msgAgentCapability = 2
	AgentCapabilityReceivePresentation   msgAgentCapability = 3
	AgentCapabilityControlPresentation   msgAgentCapability = 4
	AgentCapabilityReceiveRemotePlayback msgAgentCapability = 5
	AgentCapabilityControlRemotePlayback msgAgentCapability = 6
	AgentCapabilityReceiveStreaming      msgAgentCapability = 7
	AgentCapabilitySendStreaming         msgAgentCapability = 8
)
View Source
const (
	PskInputMethodNumeric msgPskInputMethod = 0
	PskInputMethodQrCode  msgPskInputMethod = 1
)
View Source
const (
	AuthSpake2PskStatusPskNeedsPresentation msgAuthSpake2PskStatus = 0
	AuthSpake2PskStatusPskShown             msgAuthSpake2PskStatus = 1
	AuthSpake2PskStatusPskInput             msgAuthSpake2PskStatus = 2
)
View Source
const (
	AuthStatusResultAuthenticated         msgAuthStatusResult = 0
	AuthStatusResultUnknownError          msgAuthStatusResult = 1
	AuthStatusResultTimeout               msgAuthStatusResult = 2
	AuthStatusResultSecretUnknown         msgAuthStatusResult = 3
	AuthStatusResultValidationTookTooLong msgAuthStatusResult = 4
	AuthStatusResultProofInvalid          msgAuthStatusResult = 5
)
View Source
const (
	UrlAvailabilityAvailable   msgUrlAvailability = 0
	UrlAvailabilityUnavailable msgUrlAvailability = 1
	UrlAvailabilityInvalid     msgUrlAvailability = 10
)
View Source
const (
	PresentationTerminationSourceController msgPresentationTerminationSource = 1
	PresentationTerminationSourceReceiver   msgPresentationTerminationSource = 2
	PresentationTerminationSourceUnknown    msgPresentationTerminationSource = 255
)
View Source
const (
	PresentationTerminationReasonApplicationRequest           msgPresentationTerminationReason = 1
	PresentationTerminationReasonUserRequest                  msgPresentationTerminationReason = 2
	PresentationTerminationReasonReceiverReplacedPresentation msgPresentationTerminationReason = 20
	PresentationTerminationReasonReceiverIdleTooLong          msgPresentationTerminationReason = 30
	PresentationTerminationReasonReceiverAttemptedToNavigate  msgPresentationTerminationReason = 31
	PresentationTerminationReasonReceiverPoweringDown         msgPresentationTerminationReason = 100
	PresentationTerminationReasonReceiverError                msgPresentationTerminationReason = 101
	PresentationTerminationReasonUnknown                      msgPresentationTerminationReason = 255
)
View Source
const (
	PresentationConnectionCloseEventReasonCloseMethodCalled                                msgPresentationConnectionCloseEventReason = 1
	PresentationConnectionCloseEventReasonConnectionObjectDiscarded                        msgPresentationConnectionCloseEventReason = 10
	PresentationConnectionCloseEventReasonUnrecoverableErrorWhileSendingOrReceivingMessage msgPresentationConnectionCloseEventReason = 100
)
View Source
const (
	ResultSuccess               msgResult = 1
	ResultInvalidUrl            msgResult = 10
	ResultInvalidPresentationId msgResult = 11
	ResultTimeout               msgResult = 100
	ResultTransientError        msgResult = 101
	ResultPermanentError        msgResult = 102
	ResultTerminating           msgResult = 103
	ResultUnknownError          msgResult = 199
)
View Source
const (
	RemotePlaybackTerminationRequestReasonUserTerminatedViaController msgRemotePlaybackTerminationRequestReason = 11
	RemotePlaybackTerminationRequestReasonUnknown                     msgRemotePlaybackTerminationRequestReason = 255
)
View Source
const (
	RemotePlaybackTerminationEventReasonReceiverCalledTerminate   msgRemotePlaybackTerminationEventReason = 1
	RemotePlaybackTerminationEventReasonUserTerminatedViaReceiver msgRemotePlaybackTerminationEventReason = 2
	RemotePlaybackTerminationEventReasonReceiverIdleTooLong       msgRemotePlaybackTerminationEventReason = 30
	RemotePlaybackTerminationEventReasonReceiverPoweringDown      msgRemotePlaybackTerminationEventReason = 100
	RemotePlaybackTerminationEventReasonReceiverCrashed           msgRemotePlaybackTerminationEventReason = 101
	RemotePlaybackTerminationEventReasonUnknown                   msgRemotePlaybackTerminationEventReason = 255
)
View Source
const (
	RemotePlaybackControlsPreloadNone     msgRemotePlaybackControlsPreload = 0
	RemotePlaybackControlsPreloadMetadata msgRemotePlaybackControlsPreload = 1
	RemotePlaybackControlsPreloadAuto     msgRemotePlaybackControlsPreload = 2
)
View Source
const (
	RemotePlaybackStateLoadingEmpty    msgRemotePlaybackStateLoading = 0
	RemotePlaybackStateLoadingIdle     msgRemotePlaybackStateLoading = 1
	RemotePlaybackStateLoadingLoading  msgRemotePlaybackStateLoading = 2
	RemotePlaybackStateLoadingNoSource msgRemotePlaybackStateLoading = 3
)
View Source
const (
	RemotePlaybackStateLoadedNothing  msgRemotePlaybackStateLoaded = 0
	RemotePlaybackStateLoadedMetadata msgRemotePlaybackStateLoaded = 1
	RemotePlaybackStateLoadedCurrent  msgRemotePlaybackStateLoaded = 2
	RemotePlaybackStateLoadedFuture   msgRemotePlaybackStateLoaded = 3
	RemotePlaybackStateLoadedEnough   msgRemotePlaybackStateLoaded = 4
)
View Source
const (
	AddedTextTrackKindSubtitles    msgAddedTextTrackKind = 1
	AddedTextTrackKindCaptions     msgAddedTextTrackKind = 2
	AddedTextTrackKindDescriptions msgAddedTextTrackKind = 3
	AddedTextTrackKindChapters     msgAddedTextTrackKind = 4
	AddedTextTrackKindMetadata     msgAddedTextTrackKind = 5
)
View Source
const (
	TextTrackModeDisabled msgTextTrackMode = 1
	TextTrackModeShowing  msgTextTrackMode = 2
	TextTrackModeHidden   msgTextTrackMode = 3
)
View Source
const (
	MediaErrorMsgCodeUserAborted        msgMediaErrorMsgCode = 1
	MediaErrorMsgCodeNetworkError       msgMediaErrorMsgCode = 2
	MediaErrorMsgCodeDecodeError        msgMediaErrorMsgCode = 3
	MediaErrorMsgCodeSourceNotSupported msgMediaErrorMsgCode = 4
	MediaErrorMsgCodeUnknownError       msgMediaErrorMsgCode = 5
)
View Source
const (
	VideoRotationVideoRotation0   msgVideoRotation = 0
	VideoRotationVideoRotation90  msgVideoRotation = 1
	VideoRotationVideoRotation180 msgVideoRotation = 2
	VideoRotationVideoRotation270 msgVideoRotation = 3
)
View Source
const (
	StreamingBufferStatusEnoughData       msgStreamingBufferStatus = 0
	StreamingBufferStatusInsufficientData msgStreamingBufferStatus = 1
	StreamingBufferStatusTooMuchData      msgStreamingBufferStatus = 2
)
View Source
const (
	CapabilityReceiveAudio          = AgentCapabilityReceiveAudio
	CapabilityReceiveVideo          = AgentCapabilityReceiveVideo
	CapabilityReceivePresentation   = AgentCapabilityReceivePresentation
	CapabilityControlPresentation   = AgentCapabilityControlPresentation
	CapabilityReceiveRemotePlayback = AgentCapabilityReceiveRemotePlayback
	CapabilityControlRemotePlayback = AgentCapabilityControlRemotePlayback
	CapabilityReceiveStreaming      = AgentCapabilityReceiveStreaming
	CapabilitySendStreaming         = AgentCapabilitySendStreaming
)

Exported capability constants

View Source
const (
	MdnsServiceType = "_openscreen._udp"
	MdnsDomain      = "local"
	ALPN_OSP        = "osp"
)
View Source
const (
	// WebTransportSessionGone is the RESET_STREAM and STOP_SENDING code for
	// a stream whose session is closed or unknown (2^32). Codes below it are
	// WebTransport stream error codes; unknown codes from 2^32 are treated
	// as session-gone.
	WebTransportSessionGone uint64 = 1 << 32
	// MaxWebTransportStreamErrorCode is the largest WebTransport stream
	// error code.
	MaxWebTransportStreamErrorCode = 1<<32 - 1
)
View Source
const MTU = 8192
View Source
const MaxDataChannelID = 65534

MaxDataChannelID is the largest channel-id (LP2P "Data channel extension": "between 0 and 65,534").

View Source
const (
	// QuicErrorCodeNoLongerNeeded is used when neither side needs the
	// connection anymore.
	QuicErrorCodeNoLongerNeeded quic.ApplicationErrorCode = 5139
)

QUIC application error codes used when closing a connection.

Variables

View Source
var (
	// ErrDataChannelIDOutOfRange is returned when an explicit channel ID is
	// above MaxDataChannelID, or no ID is free.
	ErrDataChannelIDOutOfRange = errors.New("data channel ID out of range")
	// ErrDataChannelIDInUse is returned when an explicit channel ID is used
	// by another open data channel of the connection.
	ErrDataChannelIDInUse = errors.New("data channel ID in use")
	// ErrDataChannelIDParity is returned when an explicit channel ID has
	// the parity the peer allocates from.
	ErrDataChannelIDParity = errors.New("data channel ID has the peer's parity")
	// ErrDataChannelRejected is returned when the peer answers the
	// data-channel-open-request with a result other than success.
	ErrDataChannelRejected = errors.New("data channel rejected by the peer")
)
View Source
var (
	// ErrSessionRefused is the cause of a connection closed because the
	// server refused the session with lp2p-session-response permanent-error,
	// or because RefuseSession was called.
	ErrSessionRefused = errors.New("session refused")
	// ErrSessionUnsupported is returned by RequestSession when the peer
	// lists neither data-channels nor quic-transport in its agent-info, or
	// the transport can't carry the session stream.
	ErrSessionUnsupported = errors.New("peer does not support LP2P sessions")
	// ErrSessionProtocol is the cause of a connection closed for a violation
	// of the session extension, e.g. a second lp2p-session-request.
	ErrSessionProtocol = errors.New("session extension protocol violation")
	// ErrSessionNotVerified is returned by RequestSession, CompleteSession
	// and RequestingOrigin when the peer's certificate wasn't verified first.
	ErrSessionNotVerified = errors.New("peer certificate not verified")
)
View Source
var (
	// ErrWebTransportUnsupported is returned by OpenWebTransport when the
	// peer doesn't list quic-transport in its agent-info: the extension's
	// messages must not be sent to it.
	ErrWebTransportUnsupported = errors.New("peer does not support the WebTransport extension")
	// ErrWebTransportRefused is returned by OpenWebTransport when the peer
	// answers with a result other than success.
	ErrWebTransportRefused = errors.New("WebTransport session refused by the peer")
	// ErrWebTransportClosed is returned when using a closed session, and by
	// Closed for a session that was closed abruptly without a close code.
	ErrWebTransportClosed = errors.New("WebTransport session closed")
	// ErrWebTransportSessionGone is returned by stream operations when the
	// peer reset or stopped the stream with session-gone. It carries no
	// stream error code.
	ErrWebTransportSessionGone = errors.New("WebTransport stream reset: session gone")
	// ErrWebTransportNoDatagrams is returned by SendDatagram when QUIC
	// datagrams weren't negotiated on the connection.
	ErrWebTransportNoDatagrams = errors.New("QUIC datagrams not supported on this connection")
	// ErrWebTransportDatagramTooLarge is returned by SendDatagram for a
	// datagram that doesn't fit in a QUIC DATAGRAM frame.
	ErrWebTransportDatagramTooLarge = errors.New("datagram too large")
)
View Source
var DataChannelOpenTimeout = 10 * time.Second

DataChannelOpenTimeout bounds how long OpenDataChannel waits for the data-channel-open-response when the context has no earlier deadline.

View Source
var ErrAuthenticationFailed = errors.New("authentication failed")

ErrAuthenticationFailed is the cause of a connection closed because authentication failed, locally or as reported by the remote agent.

View Source
var ErrConnectionClosed = errors.New("connection closed")
View Source
var ErrDiscovererClosed = errors.New("discoverer closed")
View Source
var ErrFingerprintMismatch = errors.New("agent certificate doesn't match advertised fingerprint")

ErrFingerprintMismatch is returned when the certificate presented by an advertising agent doesn't match its advertised fp.

View Source
var ErrInvalidMessage = errors.New("invalid message")

ErrInvalidMessage is the cause of encode and decode errors for messages whose values are outside the limits their CDDL defines.

View Source
var ErrInvalidPSK = errors.New("invalid PSK")

ErrInvalidPSK is returned for a PSK that is not a base-10 numeric PSK.

View Source
var ErrListenerClosed = errors.New("listener closed")
View Source
var ErrNoAgentInfo = errors.New("peer did not complete metadata discovery")

ErrNoAgentInfo is returned by Dial and the listener when the peer doesn't complete metadata discovery in time: it didn't answer agent-info-request or didn't send auth-capabilities. Peers that don't implement network.bs "Metadata Discovery" and "Authentication" fail this way, see InteropQuirks.TolerateMissingAgentInfo.

View Source
var ErrQuirkDisabled = errors.New("interop quirk not enabled")

ErrQuirkDisabled is returned when calling an API that requires an interop quirk that is not enabled in the AgentConfig.

View Source
var ErrSpake2SuiteUnsupported = errors.New("SPAKE2 edwards25519 suite (network.bs) is not implemented; enable InteropQuirks.Spake2SuiteP256 to use the P-256 suite")

ErrSpake2SuiteUnsupported is returned when authenticating without InteropQuirks.Spake2SuiteP256: the spec's edwards25519 suite is not implemented.

View Source
var ErrStreamDirection = errors.New("unidirectional stream: operation not supported in this direction")

ErrStreamDirection is returned when reading from a stream that can only be written or writing to a stream that can only be read: one half of a unidirectional stream.

View Source
var ErrTransportClosed = errors.New("transport closed")
View Source
var ErrTransportHandedOff = errors.New("transport handed off")
View Source
var WebTransportOpenTimeout = 10 * time.Second

WebTransportOpenTimeout bounds how long OpenWebTransport waits for the data-transport-start-response when the context has no earlier deadline.

Functions

func DecodeCBOR

func DecodeCBOR(r io.Reader, v interface{}) error

DecodeCBOR decodes a CBOR-encoded value from the reader. Exactly one CBOR data item is consumed, so it is safe to call repeatedly on a stream carrying multiple messages.

func EncodeCBOR

func EncodeCBOR(v interface{}) ([]byte, error)

EncodeCBOR encodes v as CBOR with deterministically ordered map keys.

func FormatPSK

func FormatPSK(psk string) (string, error)

FormatPSK encodes a numeric PSK for display to the user (network.bs Appendix B "Base-10 Numeric"): groups of three digits separated by dashes below 9 digits, groups of four otherwise, zero-padded on the left to fill the first group. The input may be in any form ParsePSK accepts.

The spec's padding rule ("Zero-pad N on the left with 3 - len(N) mod 3 digits") would add a whole group of zeros when len(N) is already a multiple of the group size, and it doesn't cover exactly 9 digits; like Chromium's openscreen, padding is only added when needed and 9 digits use groups of four. Leading zeros are dropped when decoding, so this doesn't affect interoperability.

func ParsePSK

func ParsePSK(s string) (string, error)

ParsePSK decodes a base-10 numeric PSK as typed or displayed (network.bs Appendix B "Base-10 Numeric"): "Remove dashes and leading zeros from N. Parse N as a base-10 decimal number to obtain P." Whitespace is ignored as well. It returns the canonical decimal text of P, which is the SPAKE2 password pw: e.g. "0614-8854-8833" and "61488548833" both yield "61488548833". This is the encoding Chromium's openscreen uses too.

func ReadTypedMessage

func ReadTypedMessage(r io.Reader, typeKey TypeKey, v interface{}) error

ReadTypedMessage reads a message that must have the given type key and decodes its body into v.

func WriteTypedMessage

func WriteTypedMessage(w io.Writer, typeKey TypeKey, v interface{}) error

WriteTypedMessage writes v, CBOR-encoded with map keys in ascending order, prefixed with the type key. The message is emitted with a single Write call.

Types

type ALPNListener

type ALPNListener struct {
	// contains filtered or unexported fields
}

ALPNListener allows listening for application protocols on the same port as OSP.

func (*ALPNListener) Accept

func (l *ALPNListener) Accept(ctx context.Context) (quic.Connection, error)

func (*ALPNListener) Close

func (l *ALPNListener) Close() error

Close stops accepting connections for the ALPN. It is idempotent.

type ALPNListenerConfig

type ALPNListenerConfig struct {
	VerifyConnection func(cs tls.ConnectionState) error
}

type Agent

type Agent struct {
	PeerID PeerID

	Certificate          *tls.Certificate
	CertificateSNBase    uint32
	CertificateSNCounter uint32
	// contains filtered or unexported fields
}

func NewAgent

func NewAgent(c AgentConfig) (*Agent, error)

func (*Agent) AgentInfoMessage

func (a *Agent) AgentInfoMessage() MsgAgentInfo

AgentInfoMessage returns the agent-info a local agent sends in an agent-info-response: its display and model name, the capabilities ospc implements plus AgentConfig.Capabilities, its state token and locales.

func (*Agent) Authenticated

func (a *Agent) Authenticated() bool

Authenticated reports whether a connection to this remote agent completed authentication.

func (*Agent) AuthenticationInfo

func (a *Agent) AuthenticationInfo() *AgentAuthenticationInfo

func (*Agent) CertificateFingerPrint

func (a *Agent) CertificateFingerPrint() (string, error)

func (*Agent) DiscoveredInstanceName

func (a *Agent) DiscoveredInstanceName() (InstanceName, bool)

DiscoveredInstanceName returns the DNS-SD Instance Name a remote agent was discovered with, if it was discovered.

func (*Agent) DisplayName

func (a *Agent) DisplayName() (string, DisplayNameStatus)

DisplayName returns the name to present for a remote agent and its status:

  • Once authenticated, the display name from agent-info is verified if the Instance Name the agent was discovered with is a prefix of it, or if the agent wasn't discovered (incoming connection).
  • Otherwise the discovered Instance Name is returned, unverified or truncated (should not be shown).
  • An agent that wasn't discovered and isn't authenticated yet returns the agent-info display name as unverified, or "" if it has none.

A mismatch between Instance Name and agent-info display name leaves the Instance Name unverified; the agent-info is still available through Info.

Example
a := &Agent{}
a.setDiscovery(parseInstanceName(`Living\ Ro\000`), 0, false)
name, status := a.DisplayName()
fmt.Println(name, status, status.Displayable())

a.setInfo(AgentInfo{DisplayName: "Living Room TV"})
a.setAuthenticated()
name, status = a.DisplayName()
fmt.Println(name, status, status.Displayable())
Output:
Living Ro truncated false
Living Room TV verified true

func (*Agent) HasAuthenticationInfo

func (a *Agent) HasAuthenticationInfo() bool

func (*Agent) HasCapability

func (a *Agent) HasCapability(cap AgentCapability) bool

HasCapability reports whether a remote agent listed cap in its agent-info. It is false until the agent's info is known.

func (*Agent) HasInfo

func (a *Agent) HasInfo() bool

func (*Agent) Info

func (a *Agent) Info() *AgentInfo

func (*Agent) Metadata

func (a *Agent) Metadata() (AgentMetadata, bool)

Metadata returns a snapshot of a remote agent's metadata, to compare later discoveries against with DiscoveredAgent.MetadataChangedSince. It returns false if no agent-info was received yet.

func (*Agent) MetadataVersion

func (a *Agent) MetadataVersion() uint64

MetadataVersion returns the mv value advertised by this (local) agent.

func (*Agent) NewRemoteAgent

func (a *Agent) NewRemoteAgent(nc NetworkConnection) (*Agent, error)

func (*Agent) NextRequestID

func (a *Agent) NextRequestID(peer PeerID) uint64

NextRequestID returns the next request (or watch) ID to use for a message sent by this (local) agent to the given peer.

func (*Agent) RequestIDs

func (a *Agent) RequestIDs() *RequestIDAllocator

RequestIDs returns the allocator used for request and watch IDs sent by this (local) agent. It is shared by all connections of the agent, so IDs continue to increase when reconnecting to the same peer.

func (*Agent) ResetState

func (a *Agent) ResetState()

ResetState signals that this (local) agent lost its state: it picks a new state token and resets the request ID counters of all peers to 1.

func (*Agent) StateToken

func (a *Agent) StateToken() string

StateToken returns the state token of this (local) agent: 8 characters in the range [0-9A-Za-z].

type AgentAuthenticationInfo

type AgentAuthenticationInfo struct {
	PSKConfig PSKConfig
}

type AgentCapability

type AgentCapability = msgAgentCapability

AgentCapability represents an agent capability

type AgentConfig

type AgentConfig struct {
	// ID
	PeerID            PeerID
	Certificate       *tls.Certificate
	CertificateSNBase uint32

	// Info
	DisplayName string
	ModelName   string
	Locales     []string

	// Capabilities are advertised in agent-info in addition to the ones ospc
	// itself implements (data channels, QUIC transport). application.bs
	// "Presentation API": a controlling user agent must support
	// control-presentation (CapabilityControlPresentation), a receiving user
	// agent receive-presentation (CapabilityReceivePresentation).
	Capabilities []AgentCapability

	// AuthInfo
	PSKConfig PSKConfig

	SupportedTransports []AgentTransport

	// KeepAlive configures the connection keep-alive.
	KeepAlive KeepAliveConfig

	// Logger receives the logs of the agent and its connections. Per-message
	// logs are at debug level. Nil discards all logs.
	Logger *slog.Logger

	// InteropQuirks enables deviations from the spec for interop with
	// non-conforming peers. All are off by default, see InteropQuirks.
	InteropQuirks InteropQuirks

	// SessionExtension enables the LP2P session extension
	// (local-peer-to-peer index.bs "Session extension") on connections
	// accepted by this agent's listeners: every client must send an
	// lp2p-session-request, reported by RequestingOrigin, and the connection
	// is refused (RefuseSession) or accepted (CompleteSession) by the caller;
	// auth-capabilities is only sent on RequestAuthentication and SPAKE2 is
	// only ever started by the client. Off by default: plain OSP peers don't
	// send lp2p-session-request. Dialing with it is per connection, see
	// DiscoveredAgent.DialSession.
	SessionExtension bool
}

func NewAgentConfig

func NewAgentConfig(nickname string) AgentConfig

func (*AgentConfig) WithCertificateSNBase

func (c *AgentConfig) WithCertificateSNBase(snBase uint32)

func (*AgentConfig) WithLogger

func (c *AgentConfig) WithLogger(logger *slog.Logger)

WithLogger sets the logger of the agent, see AgentConfig.Logger.

func (*AgentConfig) WithModelName

func (c *AgentConfig) WithModelName(modelName string)

func (*AgentConfig) WithPSKConfig

func (c *AgentConfig) WithPSKConfig(easeOfInput, entropy int)

type AgentInfo

type AgentInfo struct {
	DisplayName string
	ModelName   string
	// Capabilities as listed in agent-info. For a remote agent they are
	// what it advertised; for a local agent see AgentInfoMessage.
	Capabilities []AgentCapability
	StateToken   string // 8 characters in the range [0-9A-Za-z]
	Locales      []string
}

type AgentMetadata

type AgentMetadata struct {
	PeerID PeerID
	// MetadataVersion is the mv value seen at discovery, before Info was
	// retrieved. Only meaningful if HasMetadataVersion is set.
	MetadataVersion    uint64
	HasMetadataVersion bool
	// Info is the agent-info received from the agent.
	Info AgentInfo
}

AgentMetadata is a snapshot of what is known about a remote agent after connecting to it. Callers can keep it (e.g. keyed by PeerID) and compare a later discovery against it to avoid needless connections; see DiscoveredAgent.MetadataChangedSince.

func (AgentMetadata) StateReset

func (m AgentMetadata) StateReset(info AgentInfo) bool

StateReset reports whether the agent advertised a different state-token in info than in the snapshot, meaning it lost its state. Per application.bs, the agent "should discard any requests, responses, watches and events for that agent".

type AgentRole

type AgentRole int
const (
	AgentRoleClient AgentRole = 0
	AgentRoleServer AgentRole = 1
)

type AgentTransport

type AgentTransport int
const (
	AgentTransportQUIC AgentTransport = iota + 1
	AgentTransportWebRTC
)

type ApplicationConnection

type ApplicationConnection interface {
	AcceptStream(context.Context) (ApplicationStream, error)
	OpenStreamSync(context.Context) (ApplicationStream, error)
	Close() error
}

Abstract connection for the application protocol.

type ApplicationStream

type ApplicationStream interface {
	io.ReadWriteCloser
}

Abstract stream for the application protocol.

type AuthenticationRole

type AuthenticationRole int
const (
	AuthenticationRolePresenter AuthenticationRole = 0
	AuthenticationRoleConsumer  AuthenticationRole = 1
)

func (AuthenticationRole) String

func (t AuthenticationRole) String() string

type Connection

type Connection struct {
	// contains filtered or unexported fields
}

Connection is an authenticated connection to a remote agent.

func (*Connection) AcceptDataChannel

func (c *Connection) AcceptDataChannel(ctx context.Context) (*DataChannel, error)

AcceptDataChannel accepts a data channel opened by the peer.

func (*Connection) AcceptStream

func (c *Connection) AcceptStream(ctx context.Context) (ApplicationStream, error)

AcceptStream accepts an incoming application stream for use by custom protocols. Streams opened by the peer as unidirectional streams can only be read: Write fails.

The connection owns accepting streams: it reads the type key of the first message on every incoming stream. Streams for protocols handled by the connection itself (data channels, pooled WebTransport) are consumed internally, all others are returned here unchanged, i.e. starting with their type key.

func (*Connection) AcceptWebTransport

func (c *Connection) AcceptWebTransport(ctx context.Context) (*IncomingWebTransport, error)

AcceptWebTransport returns the next WebTransport session the peer opens, to be accepted or refused.

func (*Connection) Authenticated

func (c *Connection) Authenticated() bool

Authenticated reports whether the remote agent was authenticated. It is only false for connections handed over by UnauthenticatedConnection.SkipAuthentication.

func (*Connection) Close

func (c *Connection) Close() error

Close the connection and all associated streams. It waits for all goroutines of the connection to exit. Close is idempotent.

func (*Connection) Done

func (c *Connection) Done() <-chan struct{}

Done is closed when the connection is closed, by either side.

func (*Connection) Err

func (c *Connection) Err() error

Err returns why the connection was closed, or nil if it is still open.

func (*Connection) LocalAgent

func (c *Connection) LocalAgent() *Agent

func (*Connection) OpenDataChannel

func (c *Connection) OpenDataChannel(ctx context.Context, init DataChannelInit) (*DataChannel, error)

OpenDataChannel opens a data channel. It returns once the peer accepted it with a data-channel-open-response.

func (*Connection) OpenStream

func (c *Connection) OpenStream(ctx context.Context) (ApplicationStream, error)

OpenStream opens a new application stream for use by custom protocols. The stream is a raw bidirectional byte stream. The first bytes written should be the type key of an OSP message, as the receiving agent routes incoming streams by it, see AcceptStream.

func (*Connection) OpenUniStream

func (c *Connection) OpenUniStream(ctx context.Context) (ApplicationStream, error)

OpenUniStream opens a new unidirectional application stream, which can only be written to. Per application.bs, OSP messages are sent on unidirectional streams: write the type key and CBOR body of each message (see WriteTypedMessage), messages that must be processed in order on the same stream, and close it when done. Transports without unidirectional streams fall back to a bidirectional stream.

func (*Connection) OpenWebTransport

func (c *Connection) OpenWebTransport(ctx context.Context) (*WebTransportSession, error)

OpenWebTransport opens a WebTransport session on the connection. It returns once the peer accepted it, or fails with ErrWebTransportRefused. It fails with ErrWebTransportUnsupported if the peer doesn't list quic-transport in its agent-info.

func (*Connection) RemoteAgent

func (c *Connection) RemoteAgent() *Agent

func (*Connection) SupportsDatagrams

func (c *Connection) SupportsDatagrams() bool

SupportsDatagrams reports whether QUIC datagrams were negotiated on the connection, so WebTransport sessions on it are "supports-unreliable".

type DTLSTransport

type DTLSTransport struct{}

func NewDTLSTransport

func NewDTLSTransport() *DTLSTransport

func (*DTLSTransport) DialAddr

func (t *DTLSTransport) DialAddr(ctx context.Context, addr string, tlsConf *tls.Config) (NetworkConnection, error)

func (*DTLSTransport) ListenAddr

func (t *DTLSTransport) ListenAddr(addr string, tlsConf *tls.Config) (NetworkListener, error)

type DataChannel

type DataChannel struct {
	DataChannelParameters
	// contains filtered or unexported fields
}

DataChannel is an open data channel.

func (*DataChannel) Close

func (c *DataChannel) Close() error

Close closes the DataChannel and the underlying Quic stream. The channel's ID is free for reuse from then on.

func (*DataChannel) Read

func (c *DataChannel) Read(p []byte) (int, error)

Read reads a packet of len(p) bytes as binary data

func (*DataChannel) ReadDataChannel

func (c *DataChannel) ReadDataChannel(p []byte) (int, DataEncoding, error)

ReadDataChannel reads a packet of len(p) bytes

func (*DataChannel) ReceiveMessage

func (c *DataChannel) ReceiveMessage() ([]byte, error)

ReceiveMessage

func (*DataChannel) ReceiveMessageWithEncoding

func (c *DataChannel) ReceiveMessageWithEncoding() ([]byte, DataEncoding, error)

ReceiveMessageWithEncoding

func (*DataChannel) SendMessage

func (c *DataChannel) SendMessage(payload []byte) error

SendMessage

func (*DataChannel) SendMessageWithEncoding

func (c *DataChannel) SendMessageWithEncoding(payload []byte, enc DataEncoding) error

SendMessageWithEncoding

func (*DataChannel) Write

func (c *DataChannel) Write(p []byte) (n int, err error)

Write writes len(p) bytes from p as binary data

func (*DataChannel) WriteDataChannel

func (c *DataChannel) WriteDataChannel(p []byte, enc DataEncoding) (n int, err error)

WriteDataChannel writes len(p) bytes from p

type DataChannelInit

type DataChannelInit struct {
	Label    string
	Protocol string
	// ID is the channel-id to use. If nil, one is allocated: the QUIC
	// client allocates even IDs and the QUIC server odd IDs, taking the
	// lowest one of its parity not used by an open data channel of the
	// connection. An explicit ID must be free and of the local parity, so
	// the two sides never pick the same ID. IDs only label channels;
	// channels are told apart by their QUIC stream, so IDs from the peer
	// are accepted whatever their parity, duplicates included.
	ID *uint64
}

DataChannelInit configures a data channel to open.

type DataChannelParameters

type DataChannelParameters struct {
	Label    string
	Protocol string
	ID       uint64
}

DataChannelParameters describes an open data channel.

type DataEncoding

type DataEncoding uint64

DataEncoding represents pre-agreed encoding IDs used in data-frame messages of a data channel.

type DiscoveredAgent

type DiscoveredAgent struct {
	PeerID PeerID
	// TXT holds the unescaped TXT records.
	TXT TXTRecordSet
	// contains filtered or unexported fields
}

DiscoveredAgent represents a discovered remote agent that has not been contacted yet.

func DirectAgent

func DirectAgent(instanceName string, addr *net.UDPAddr, txt TXTRecordSet) (*DiscoveredAgent, error)

DirectAgent returns a DiscoveredAgent for an agent known without mDNS discovery: listening on addr, with the given DNS-SD Instance Name (unescaped) and unescaped TXT records. It can be dialed like a discovered agent.

func (*DiscoveredAgent) AuthToken

func (a *DiscoveredAgent) AuthToken() (string, error)

AuthToken returns the advertised at value: the auth-initiation-token to use in the first authentication message to this agent.

func (DiscoveredAgent) Dial

func (ra DiscoveredAgent) Dial(ctx context.Context, transportType AgentTransport, la *Agent) (*UnauthenticatedConnection, error)

Dial opens a connection to the remote agent.

The advertising agent is verified as network.bs "TLS 1.3" requires: the agent fingerprint of the certificate it presents must match its advertised fp TXT record. Nothing else about the certificate is checked, its identity is established by authentication.

The non-standard sn TXT key, if present, is used to compute the agent hostname sent as TLS server_name. Without it, which is all the spec defines (see w3c/openscreenprotocol#293), the hostname can't be computed and the server_name is the encoded Instance Name and domain only.

Dial returns once the peer completed metadata discovery (agent-info and auth-capabilities), see ErrNoAgentInfo.

func (DiscoveredAgent) DialSession

func (ra DiscoveredAgent) DialSession(ctx context.Context, transportType AgentTransport, la *Agent, origin string) (*UnauthenticatedConnection, error)

DialSession opens a connection to the remote agent with the LP2P session extension, for a session requested by origin. After the agent-info exchange the caller verifies the server (StartAuthentication and AuthenticatePSK, or TrustPeer), sends the lp2p-session-request with RequestSession, follows the server's cues with AwaitSession, and finishes with CompleteSession.

func (*DiscoveredAgent) DisplayName

func (a *DiscoveredAgent) DisplayName() (string, DisplayNameStatus)

DisplayName returns the name to present for the agent before connecting, and its status. The status is DisplayNameTruncated (don't show) or DisplayNameUnverified. After authenticating, use Agent.DisplayName on the connection's remote agent to get a verified display name.

func (*DiscoveredAgent) InstanceName

func (a *DiscoveredAgent) InstanceName() InstanceName

InstanceName returns the parsed DNS-SD Instance Name of the agent.

func (*DiscoveredAgent) MetadataChangedSince

func (a *DiscoveredAgent) MetadataChangedSince(prev AgentMetadata) bool

MetadataChangedSince reports whether the agent's metadata may have changed since prev was retrieved, i.e. whether connecting to the agent to discover updated metadata is warranted. It is conservative: it returns true unless both prev and the advertisement carry the same mv for the same PeerID.

The spec requires mv to only increase; any different value is treated as changed since a lower value likely indicates the agent lost its state.

func (*DiscoveredAgent) MetadataVersion

func (a *DiscoveredAgent) MetadataVersion() (uint64, bool)

MetadataVersion returns the advertised mv value, if present.

func (*DiscoveredAgent) Nickname

func (a *DiscoveredAgent) Nickname() string

Nickname of the remote agent: its unescaped DNS-SD Instance Name, without truncation marker. It is unverified and may be truncated; prefer DisplayName for presenting the agent to users.

type Discoverer

type Discoverer struct {
	// contains filtered or unexported fields
}

Discoverer is used to discover agents.

func Discover

func Discover() (*Discoverer, error)

Discover agents

func NewDiscoverer

func NewDiscoverer() *Discoverer

NewDiscoverer creates a new Discoverer

func (*Discoverer) Accept

func (d *Discoverer) Accept(ctx context.Context) (*DiscoveredAgent, error)

Accept returns an a discovered agent. It should be called in a loop.

func (*Discoverer) Close

func (d *Discoverer) Close() error

Close closes the discoverer. Any blocked Accept operations will be unblocked and return errors.

func (*Discoverer) Start

func (d *Discoverer) Start() error

Start discovering agents

func (*Discoverer) WithNickname

func (d *Discoverer) WithNickname(nickname string)

WithNickname defines a nickname of agents to discover. Agents match if their DNS-SD Instance Name equals the nickname or, if the Instance Name is truncated, is a prefix of it. The match is unverified, see Agent.DisplayName for the verified display name after authentication.

type DisplayNameStatus

type DisplayNameStatus int

DisplayNameStatus describes how far a remote agent's name can be trusted. It models the three categories of application.bs #instance-names.

const (
	// DisplayNameTruncated is a truncated and unverified DNS-SD Instance Name.
	// It should not be shown to the user.
	DisplayNameTruncated DisplayNameStatus = iota
	// DisplayNameUnverified is a complete but unverified DNS-SD Instance
	// Name. It can be shown as unverified prior to authentication.
	DisplayNameUnverified
	// DisplayNameVerified is the display name from agent-info, received over
	// an authenticated connection, of which the Instance Name is a prefix.
	DisplayNameVerified
)

func (DisplayNameStatus) Displayable

func (s DisplayNameStatus) Displayable() bool

Displayable reports whether a name with this status may be shown to the user. Truncated, unverified names should not be shown.

func (DisplayNameStatus) String

func (s DisplayNameStatus) String() string

func (DisplayNameStatus) Verified

func (s DisplayNameStatus) Verified() bool

Verified reports whether the name is a verified display name.

type IncomingWebTransport

type IncomingWebTransport struct {
	// contains filtered or unexported fields
}

IncomingWebTransport is a WebTransport session the peer opened, waiting to be accepted or refused.

func (*IncomingWebTransport) Accept

Accept accepts the session: it answers success and returns the open session. The peer may already have opened streams on it.

func (*IncomingWebTransport) ExchangeID

func (i *IncomingWebTransport) ExchangeID() uint64

ExchangeID is the session's exchange-id.

func (*IncomingWebTransport) Refuse

func (i *IncomingWebTransport) Refuse() error

Refuse refuses the session: it answers permanent-error and ends the control stream.

type InstanceName

type InstanceName struct {
	// Name is the unescaped Instance Name without the truncation marker.
	Name string
	// Truncated is set when the advertiser terminated the Instance Name with
	// a null character, signalling it is not the complete display name.
	Truncated bool
}

InstanceName is a parsed (unescaped) DNS-SD Instance Name.

func (InstanceName) MatchesNickname

func (n InstanceName) MatchesNickname(nickname string) bool

MatchesNickname reports whether the Instance Name could belong to an agent with the given display name: an exact match for complete names, or a prefix match for truncated names. This is a discovery-time filter only, the result is unverified.

func (InstanceName) Status

func (n InstanceName) Status() DisplayNameStatus

Status returns the unverified status of the Instance Name.

func (InstanceName) Verify

func (n InstanceName) Verify(displayName string) DisplayNameStatus

Verify checks the Instance Name against the display name from agent-info. The caller must only pass a display name received over an authenticated connection. If the Instance Name is not a prefix of the display name, the name stays unverified.

type InteropQuirks

type InteropQuirks struct {
	// Spake2SuiteP256 authenticates with the RFC 9382
	// SPAKE2-P256-SHA256-HKDF-HMAC ciphersuite: group P-256 instead of
	// edwards25519. Everything else follows network.bs (SHA-256 transcript
	// hash, HKDF, HMAC, SHA-512 password hash, client/server fingerprints as
	// the identities A/B). Both agents must enable it.
	//
	// Violates network.bs "Authentication with SPAKE2": "Elliptic curve is
	// edwards25519". The edwards25519 suite is not implemented (neither here
	// nor in Chromium's openscreen, where BoringSSL offers no general
	// edwards25519 group operations), so without this quirk AuthenticatePSK
	// fails with ErrSpake2SuiteUnsupported. Chromium's osp_demo enables the
	// same suite with --spake2_suite=p256.
	Spake2SuiteP256 bool

	// AcceptCertificateFingerprint accepts an "fp" TXT value computed as the
	// base64 SHA-256 of the whole DER-encoded certificate, in addition to the
	// spec's SPKI fingerprint, when dialing.
	//
	// Violates network.bs "Computing the Agent Fingerprint" (fp is the
	// RFC 7469 SPKI fingerprint) and "TLS 1.3" (the fingerprint calculated for
	// the received certificate must match fp). Older osp_demo builds advertise
	// the whole-certificate hash.
	AcceptCertificateFingerprint bool

	// AdvertiseCertificateFingerprint advertises "fp" as the base64 SHA-256
	// of the whole DER-encoded certificate instead of the SPKI fingerprint,
	// so peers that pin the whole-certificate hash (older osp_demo
	// controllers) accept the listener's certificate. Spec-compliant peers
	// will reject the listener.
	//
	// Violates network.bs "Computing the Agent Fingerprint". The PeerID and
	// the SPAKE2 identities are still the SPKI fingerprint.
	AdvertiseCertificateFingerprint bool

	// TolerateMissingAgentInfo lets Dial and Accept proceed when the peer
	// doesn't send agent-info-response and auth-capabilities within a short
	// grace period. The remote agent's Info is then nil and its
	// authentication capabilities are assumed equal to the local ones.
	//
	// Violates network.bs "Metadata Discovery" and "Authentication": agents
	// must answer agent-info-request, and authentication starts from the
	// exchanged auth-capabilities. Older osp_demo builds implement neither.
	TolerateMissingAgentInfo bool

	// ConsumerInitiatesWithPublicValue makes a PSK consumer that initiates
	// SPAKE2 send auth-spake2-handshake psk-needs-presentation with pA, as
	// the current network.bs text says, which requires the consumer to know
	// the PSK before the presenter showed it. Without it, the consumer's
	// psk-needs-presentation carries no public-value: it asks the presenter
	// to show the PSK and initiate with psk-shown. Chromium's openscreen
	// never sends psk-needs-presentation but, when it presents, fails on one
	// without pA unless it initiated first; enable this when osp_demo is the
	// presenter (the osp-demo interop harness does with
	// OSP_DEMO_EASE_OF_INPUT). Received psk-needs-presentation with pA is
	// always answered.
	//
	// Violates the OSP fix local-peer-to-peer assumes: a consumer can't
	// compute pA before it has the PSK, so network.bs "the PSK consumer
	// sends ... psk-needs-presentation and the public-value field set to pA"
	// is read as carrying no public-value.
	ConsumerInitiatesWithPublicValue bool

	// AllowSkipAuthentication enables
	// UnauthenticatedConnection.SkipAuthentication, which hands the
	// connection to the application protocol WITHOUT authenticating the
	// peer. Application messages the peer sent before the hand-off are
	// delivered rather than discarded. Every skipped authentication is logged
	// at warning level.
	//
	// INSECURE. Violates network.bs "Authentication": agents must
	// authenticate with SPAKE2 before exchanging application messages. Only
	// for debugging against peers without authentication.
	AllowSkipAuthentication bool
}

InteropQuirks relaxes the protocol to interoperate with implementations that deviate from the Open Screen Network Protocol (network.bs), such as older builds of Chromium's osp_demo. Every quirk breaks a rule of the spec and is off by default. Enable them only for interop testing and debugging, never in production: several of them remove security guarantees.

Only behavior that violates the spec is gated here. Spec-compatible leniency (e.g. dialing an agent that doesn't advertise the non-standard "sn" TXT key, or discovering an agent that omits "mv") is always on.

func (InteropQuirks) Any

func (q InteropQuirks) Any() bool

Any reports whether any quirk is enabled.

type KeepAliveConfig

type KeepAliveConfig struct {
	// Disabled stops this agent from sending agent-status-request messages.
	// Incoming agent-status-request messages are still answered.
	Disabled bool
	// Interval overrides DefaultKeepAliveInterval when non-zero.
	Interval time.Duration
}

KeepAliveConfig configures the OSP keep-alive mechanism: an agent behaves as though a timer, shorter than the idle timeout, is reset every time a message is sent. When the timer expires an agent-status-request is sent. QUIC PING frames are not used.

type Listener

type Listener struct {
	// contains filtered or unexported fields
}

Listener acts as an advertising OSP agent and listens for incoming connections.

func Listen

func Listen(transportType AgentTransport, a *Agent) (*Listener, error)

Listen starts an advertising agent and listens for incoming connections.

func NewListener

func NewListener(a *Agent, transportType AgentTransport) *Listener

NewListener creates a new Listener

func (*Listener) Accept

Accept returns the next incoming connection. It should be called in a loop.

func (*Listener) Addr

func (l *Listener) Addr() net.Addr

Addr returns the local address the listener listens on, or nil if it is not started.

func (*Listener) AnnounceMetadataChange

func (l *Listener) AnnounceMetadataChange() error

AnnounceMetadataChange signals listening agents that the metadata of the local agent changed by advertising a greater mv value, so they know to reconnect to discover the updated metadata.

func (*Listener) Close

func (l *Listener) Close() error

Close closes the listener and the connections it didn't hand out yet. Connections returned by Accept are not closed. Any blocked Accept operations are unblocked and return ErrListenerClosed. Close is idempotent.

func (*Listener) ListenApplication

func (l *Listener) ListenApplication(alpn string, config *ALPNListenerConfig) *ALPNListener

ListenApplication allows you to listen for quic connections on the same port but with a different ALPN. Only one per ALPN is allowed. Needs to be registered before starting the Listener.

func (*Listener) Start

func (l *Listener) Start() error

Start listening and advertising.

func (*Listener) StartWithoutAdvertising

func (l *Listener) StartWithoutAdvertising(addr string) error

StartWithoutAdvertising listens on addr (e.g. "127.0.0.1:0") without advertising the agent with mDNS. Peers connect with a DiscoveredAgent from DirectAgent, using Addr and TXT.

func (*Listener) TXT

func (l *Listener) TXT() TXTRecordSet

TXT returns a copy of the TXT records the listener advertises, or would advertise when started without advertising.

type MsgAgentInfo

type MsgAgentInfo struct {
	DisplayName  string            `cbor:"0,keyasint"`
	ModelName    string            `cbor:"1,keyasint,omitempty"`
	Capabilities []AgentCapability `cbor:"2,keyasint"`
	StateToken   string            `cbor:"3,keyasint"`
	Locales      []string          `cbor:"4,keyasint"`
}

MsgAgentInfo is the exported agent-info structure

type MsgAgentInfoEvent

type MsgAgentInfoEvent struct {
	AgentInfo MsgAgentInfo `cbor:"0,keyasint"`
}

MsgAgentInfoEvent is the exported agent-info-event message

type MsgAgentInfoRequest

type MsgAgentInfoRequest struct {
	RequestID RequestID `cbor:"0,keyasint"`
}

MsgAgentInfoRequest is the exported agent-info-request message

type MsgAgentInfoResponse

type MsgAgentInfoResponse struct {
	RequestID RequestID    `cbor:"0,keyasint"`
	AgentInfo MsgAgentInfo `cbor:"1,keyasint"`
}

MsgAgentInfoResponse is the exported agent-info-response message

type MsgAgentStatusRequest

type MsgAgentStatusRequest struct {
	RequestID RequestID `cbor:"0,keyasint"`
	Status    *string   `cbor:"1,keyasint,omitempty"`
}

MsgAgentStatusRequest is the exported agent-status-request message

type MsgAgentStatusResponse

type MsgAgentStatusResponse struct {
	RequestID RequestID `cbor:"0,keyasint"`
	Status    *string   `cbor:"1,keyasint,omitempty"`
}

MsgAgentStatusResponse is the exported agent-status-response message

type NetworkConnection

type NetworkConnection interface {
	// Write sends a single serialized message.
	io.WriteCloser
	// ReadMessage returns the next received message. Transports with multiple
	// streams read them independently: ordering is only preserved for
	// messages within the same stream.
	ReadMessage() (interface{}, error)
	// Determines if this connection supports reliable delivery. If not, the
	// network protocol agent needs to perform timeout & retransmission.
	IsReliable() bool
	// Upgrades the connection to an application connection. Any proceeding
	// calls to the network connection will fail.
	IntoApplicationConnection() (ApplicationConnection, error)

	ConnectionState() tls.ConnectionState
}

Abstract connection for the network protocol, responsible for getting extended agent capabilities and performing the authentication ceremony.

type NetworkListener

type NetworkListener interface {
	Accept(ctx context.Context) (NetworkConnection, error)
	Addr() net.Addr
	// Close stops listening. Accepted connections are not closed.
	Close() error
}

type NetworkTransport

type NetworkTransport interface {
	DialAddr(ctx context.Context, addr string, tlsConf *tls.Config) (NetworkConnection, error)
	ListenAddr(addr string, tlsConf *tls.Config) (NetworkListener, error)
}

func NewNetworkTransport

func NewNetworkTransport(typ AgentTransport) (NetworkTransport, error)

type PSKConfig

type PSKConfig struct {
	EaseOfInput int // 0-100
	Entropy     int // 20-60 bits
}

type PeerID

type PeerID string

PeerID is a unique identifier for a peer, it is equal tot the OSP agent fingerprint (FP) of the peer.

type QuicTransport

type QuicTransport struct {
	// IdleTimeout is the QUIC idle_timeout transport parameter. Zero means
	// DefaultIdleTimeout.
	IdleTimeout time.Duration
	// KeepAlive configures sending agent-status-request messages.
	KeepAlive KeepAliveConfig
	// Logger receives transport logs. Nil discards them.
	Logger *slog.Logger
}

func NewQuicTransport

func NewQuicTransport() *QuicTransport

func (*QuicTransport) DialAddr

func (t *QuicTransport) DialAddr(ctx context.Context, addr string, tlsConf *tls.Config) (NetworkConnection, error)

func (*QuicTransport) ListenAddr

func (t *QuicTransport) ListenAddr(addr string, tlsConf *tls.Config) (NetworkListener, error)

type RequestID

type RequestID = msgRequestId

RequestID is the request identifier type

type RequestIDAllocator

type RequestIDAllocator struct {
	// contains filtered or unexported fields
}

RequestIDAllocator allocates request and watch IDs per remote agent.

Per the OpenScreen Application Protocol, request-id and watch-id values come from a counter that starts at 1 and increments by 1 for each ID. IDs are not tied to a particular connection: closing and reopening a connection to the same peer must not reset the counter. Counters are therefore keyed by the remote agent's PeerID (its certificate fingerprint), which identifies the peer across reconnects.

The counters are only reset when the local agent changes its state token (see Agent.ResetState).

A RequestIDAllocator is safe for concurrent use.

func NewRequestIDAllocator

func NewRequestIDAllocator() *RequestIDAllocator

NewRequestIDAllocator returns an empty allocator.

func (*RequestIDAllocator) Next

func (a *RequestIDAllocator) Next(peer PeerID) uint64

Next returns the next request ID for the given peer. The first ID for a peer is 1.

func (*RequestIDAllocator) Reset

func (a *RequestIDAllocator) Reset()

Reset resets the counters of all peers to 1.

type SessionCue

type SessionCue int

SessionCue is what AwaitSession reports.

const (
	// SessionCueAuthenticate: a trust ceremony is due. On the client, the
	// server answered authentication-required; on the server, the client
	// started SPAKE2. Run AcceptAuthenticate and AuthenticatePSK (the
	// client first StartAuthentication), then AwaitSession again.
	SessionCueAuthenticate SessionCue = iota + 1
	// SessionCueAccepted (client): the server accepted the session; call
	// CompleteSession.
	SessionCueAccepted
	// SessionCueRequested (server): the lp2p-session-request arrived. If
	// the client isn't verified yet, call RequireAuthentication and wait for
	// SessionCueAuthenticate; otherwise RequestingOrigin is available and
	// the session is accepted with CompleteSession or refused with
	// RefuseSession.
	SessionCueRequested
)

func (SessionCue) String

func (c SessionCue) String() string

type TXTRecordSet

type TXTRecordSet map[string][]string

func (TXTRecordSet) Add

func (r TXTRecordSet) Add(key, value string)

func (TXTRecordSet) FromSlice

func (r TXTRecordSet) FromSlice(in []string) error

func (TXTRecordSet) Get

func (r TXTRecordSet) Get(key string) []string

func (TXTRecordSet) GetOne

func (r TXTRecordSet) GetOne(key string) (string, error)

func (TXTRecordSet) Set

func (r TXTRecordSet) Set(key, value string)

func (TXTRecordSet) ToSlice

func (r TXTRecordSet) ToSlice() []string

type TypeKey

type TypeKey uint64
const (
	TypeKeyAgentInfoRequest    TypeKey = 10
	TypeKeyAgentInfoResponse   TypeKey = 11
	TypeKeyAgentStatusRequest  TypeKey = 12
	TypeKeyAgentStatusResponse TypeKey = 13
	TypeKeyAgentInfoEvent      TypeKey = 120
)

Exported type keys for application protocol use

func ReadTypeKey

func ReadTypeKey(r io.Reader) (TypeKey, error)

ReadTypeKey reads a variable-length type key from the reader.

type UnauthenticatedConnection

type UnauthenticatedConnection struct {
	// contains filtered or unexported fields
}

UnauthenticatedConnection represents an OSPC connection that didn't pass authentication yet.

func (*UnauthenticatedConnection) AcceptAuthenticate

func (c *UnauthenticatedConnection) AcceptAuthenticate(ctx context.Context) (role AuthenticationRole, err error)

AcceptAuthenticate waits until the authentication role is known, after the auth-capabilities exchange. The presenter then shows a PSK to the user (GeneratePSK, FormatPSK), the consumer asks the user for it; both pass it to AuthenticatePSK.

func (*UnauthenticatedConnection) AuthenticatePSK

func (c *UnauthenticatedConnection) AuthenticatePSK(ctx context.Context, psk []byte) (*Connection, error)

AuthenticatePSK authenticates the peer with SPAKE2 (network.bs "Authentication with SPAKE2") using psk, a base-10 numeric PSK in any form ParsePSK accepts. It blocks until authentication completes or ctx is done.

If the peer already initiated, the local agent answers it. Otherwise it initiates: the PSK presenter with psk-status psk-shown, the PSK consumer with psk-needs-presentation. A consumer that doesn't know the PSK yet asks the user for it after AcceptAuthenticate; the presenter's handshake is answered once AuthenticatePSK is called.

The spec's edwards25519 suite is not implemented: without InteropQuirks.Spake2SuiteP256 it fails with ErrSpake2SuiteUnsupported and closes the connection. A malformed psk returns ErrInvalidPSK and leaves the connection open.

On the client of a connection made with DialSession, the returned Connection must not be used before CompleteSession returns it: until the server accepted the session, streams it opens are held.

func (*UnauthenticatedConnection) AwaitSession

func (c *UnauthenticatedConnection) AwaitSession(ctx context.Context) (SessionCue, error)

AwaitSession waits for the next cue of the session exchange.

On the client, after RequestSession: SessionCueAuthenticate when the server answers authentication-required, SessionCueAccepted when it accepts. On the server, after Accept: SessionCueAuthenticate when the client starts SPAKE2, SessionCueRequested when its lp2p-session-request arrives. Each cue is reported once; it fails with ErrSessionRefused if the server refuses, or with the close reason.

func (*UnauthenticatedConnection) Close

func (c *UnauthenticatedConnection) Close() error

Close the unauthenticated connection. If the connection has progressed to authenticated, it is not closed but an error is returned. This allows for defer closing regardless.

func (*UnauthenticatedConnection) CompleteSession

func (c *UnauthenticatedConnection) CompleteSession(ctx context.Context) (*Connection, error)

CompleteSession finishes the session exchange and returns the connection.

On the server it accepts the session: it sends the success lp2p-session-response, once the client's certificate is verified (by AuthenticatePSK, or because the caller trusts its fingerprint, see TrustPeer) and the user granted the session.

On the client it waits for the success response, if not received yet.

Until it returns, neither agent sends messages of the data channel or WebTransport extensions.

func (*UnauthenticatedConnection) GeneratePSK

func (c *UnauthenticatedConnection) GeneratePSK() ([]byte, error)

GeneratePSK creates a random PSK for the PSK presenter, with the entropy both agents require. It is canonical decimal text (see ParsePSK): show it to the user with FormatPSK and pass it to AuthenticatePSK.

func (*UnauthenticatedConnection) GetAuthenticationRole

func (c *UnauthenticatedConnection) GetAuthenticationRole() AuthenticationRole

GetAuthenticationRole determines if the agent should act as presenter or consumer of the PSK.

func (*UnauthenticatedConnection) LocalAgent

func (c *UnauthenticatedConnection) LocalAgent() *Agent

LocalAgent provides info about local agent

func (*UnauthenticatedConnection) RefuseSession

func (c *UnauthenticatedConnection) RefuseSession() error

RefuseSession refuses the session on the server: it answers the lp2p-session-request with permanent-error and closes the connection once the client ended its side of the stream, or after a short wait. Call it when no receiver allows the requesting origin, or when the user declines.

func (*UnauthenticatedConnection) RemoteAgent

func (c *UnauthenticatedConnection) RemoteAgent() *Agent

RemoteAgent provides info about remote agent

func (*UnauthenticatedConnection) RequestSession

func (c *UnauthenticatedConnection) RequestSession(ctx context.Context) error

RequestSession sends the lp2p-session-request with the origin given to DialSession, on the client. The server's certificate must be verified first, by AuthenticatePSK or TrustPeer (ErrSessionNotVerified otherwise). It fails with ErrSessionUnsupported if the agent lists neither data-channels nor quic-transport. Then follow the server's cues with AwaitSession.

func (*UnauthenticatedConnection) RequestingOrigin

func (c *UnauthenticatedConnection) RequestingOrigin() (origin string, ok bool)

RequestingOrigin returns the requesting origin the client sent in its lp2p-session-request, on the server, once the client's certificate is verified: before, it isn't looked at. It is the ASCII serialization of an origin as asserted by the client's user agent: parse it, and treat a value that doesn't parse as an opaque origin, which matches nothing.

func (*UnauthenticatedConnection) RequireAuthentication

func (c *UnauthenticatedConnection) RequireAuthentication() error

RequireAuthentication answers the lp2p-session-request with the provisional authentication-required, on a server that hasn't verified the client's certificate. The client then starts SPAKE2: wait for SessionCueAuthenticate with AwaitSession and run AcceptAuthenticate and AuthenticatePSK.

func (*UnauthenticatedConnection) SkipAuthentication

func (c *UnauthenticatedConnection) SkipAuthentication() (*Connection, error)

SkipAuthentication hands the connection to the application protocol without authenticating the peer. INSECURE, for interop debugging only: it returns ErrQuirkDisabled unless InteropQuirks.AllowSkipAuthentication is set on the local agent, and violates network.bs, which requires authentication before exchanging application messages. The returned Connection reports Authenticated() == false.

func (*UnauthenticatedConnection) StartAuthentication

func (c *UnauthenticatedConnection) StartAuthentication() error

StartAuthentication asks the PSK presenter to show the PSK when this agent is the consumer, see baseConnection.StartAuthentication. Call it before asking the user for the PSK.

func (*UnauthenticatedConnection) TrustPeer

func (c *UnauthenticatedConnection) TrustPeer() error

TrustPeer marks the peer's certificate as verified without SPAKE2, because its fingerprint (RemoteAgent().PeerID) is a trust anchor of the caller.

type UniStreamOpener

type UniStreamOpener interface {
	OpenUniStreamSync(context.Context) (ApplicationStream, error)
}

UniStreamOpener is implemented by application connections that support unidirectional streams, as used for OSP messages (application.bs). The returned stream can only be written to.

type WebTransportCloseInfo

type WebTransportCloseInfo struct {
	CloseCode uint32
	Reason    string
}

WebTransportCloseInfo is the close code and reason of a cleanly closed session.

type WebTransportSession

type WebTransportSession struct {
	// contains filtered or unexported fields
}

WebTransportSession is an open WebTransport session pooled on an OSP connection.

func (*WebTransportSession) AcceptStream

func (s *WebTransportSession) AcceptStream(ctx context.Context) (*WebTransportStream, error)

AcceptStream returns the next bidirectional stream the peer opened.

func (*WebTransportSession) AcceptUniStream

func (s *WebTransportSession) AcceptUniStream(ctx context.Context) (*WebTransportStream, error)

AcceptUniStream returns the next unidirectional stream the peer opened, which can only be read.

func (*WebTransportSession) Close

Close closes the session with info: it sends data-transport-close, ends the control stream and resets all streams of the session with session-gone. The reason must be at most 1024 bytes. It returns once the peer ended its side of the control stream, so that closing the connection right after doesn't discard the close, or after a short wait.

func (*WebTransportSession) Closed

Closed waits until the session is closed. It returns the close info of a clean close (data-transport-close, or FIN on the control stream), or the error of an abrupt one (control stream reset, connection closed).

func (*WebTransportSession) Done

func (s *WebTransportSession) Done() <-chan struct{}

Done is closed once the session is closed, cleanly or not.

func (*WebTransportSession) Drain

func (s *WebTransportSession) Drain() error

Drain asks the peer to finish using the session and close it.

func (*WebTransportSession) Draining

func (s *WebTransportSession) Draining() <-chan struct{}

Draining is closed once the peer asked to finish using the session.

func (*WebTransportSession) ExchangeID

func (s *WebTransportSession) ExchangeID() uint64

ExchangeID is the session's exchange-id.

func (*WebTransportSession) ExportKeyingMaterial

func (s *WebTransportSession) ExportKeyingMaterial(label string, context []byte, length int) ([]byte, error)

ExportKeyingMaterial derives keying material of the given length that both sides of the session derive identically and no other session yields: the TLS exporter of the QUIC connection with the label "EXPORTER-WebTransport" and the WebTransport Exporter Context of draft-ietf-webtrans-http3 section 4.8, with the exchange-id as session ID, label and context inside it. Label and context are at most 255 bytes.

func (*WebTransportSession) OpenStreamSync

func (s *WebTransportSession) OpenStreamSync(ctx context.Context) (*WebTransportStream, error)

OpenStreamSync opens a bidirectional stream on the session.

func (*WebTransportSession) OpenUniStreamSync

func (s *WebTransportSession) OpenUniStreamSync(ctx context.Context) (*WebTransportStream, error)

OpenUniStreamSync opens a unidirectional stream on the session, which can only be written.

func (*WebTransportSession) ReceiveDatagram

func (s *WebTransportSession) ReceiveDatagram(ctx context.Context) ([]byte, error)

ReceiveDatagram returns the next datagram received for the session.

func (*WebTransportSession) SendDatagram

func (s *WebTransportSession) SendDatagram(p []byte) error

SendDatagram sends p as a datagram of the session: a QUIC DATAGRAM frame with the exchange-id as a varint before p.

func (*WebTransportSession) SupportsDatagrams

func (s *WebTransportSession) SupportsDatagrams() bool

SupportsDatagrams reports whether the session delivers datagrams: it is "supports-unreliable" if so and "reliable-only" otherwise.

type WebTransportStream

type WebTransportStream struct {
	// contains filtered or unexported fields
}

WebTransportStream is a stream of a WebTransportSession: a QUIC stream of the same directionality. Reads and writes fail with a *WebTransportStreamError when the stream was reset or stopped with a WebTransport error code, and with ErrWebTransportSessionGone when it was reset or stopped because the session is gone.

func (*WebTransportStream) CancelRead

func (st *WebTransportStream) CancelRead(code uint32)

CancelRead stops receiving with a WebTransport error code (STOP_SENDING).

func (*WebTransportStream) CancelWrite

func (st *WebTransportStream) CancelWrite(code uint32)

CancelWrite aborts sending with a WebTransport error code (RESET_STREAM).

func (*WebTransportStream) Close

func (st *WebTransportStream) Close() error

Close ends the sending side of the stream with FIN.

func (*WebTransportStream) Read

func (st *WebTransportStream) Read(p []byte) (int, error)

func (*WebTransportStream) StreamID

func (st *WebTransportStream) StreamID() int64

StreamID is the QUIC stream ID.

func (*WebTransportStream) Unidirectional

func (st *WebTransportStream) Unidirectional() bool

Unidirectional reports whether the stream has only one direction: it can only be written if opened locally, only read if opened by the peer.

func (*WebTransportStream) Write

func (st *WebTransportStream) Write(p []byte) (int, error)

type WebTransportStreamError

type WebTransportStreamError struct {
	Code   uint32
	Remote bool // The peer reset or stopped the stream.
}

WebTransportStreamError is returned by stream operations when the stream was reset or stopped with a WebTransport stream error code.

func (*WebTransportStreamError) Error

func (e *WebTransportStreamError) Error() string

Directories

Path Synopsis
examples
data-channel command
protocol command

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL
⌂ 换网址