package sctp import ( "fmt" "math" "math/rand" "time" "github.com/pkg/errors" ) // AssociationState is an enum for the states that an Association will transition // through while connecting // https://tools.ietf.org/html/rfc4960#section-13.2 type AssociationState uint8 // AssociationState enums const ( Open AssociationState = iota + 1 CookieEchoed CookieWait Established ShutdownAckSent ShutdownPending ShutdownReceived ShutdownSent ) func (a AssociationState) String() string { switch a { case Open: return "Open" case CookieEchoed: return "CookieEchoed" case CookieWait: return "CookieWait" case Established: return "Established" case ShutdownPending: return "ShutdownPending" case ShutdownSent: return "ShutdownSent" case ShutdownReceived: return "ShutdownReceived" case ShutdownAckSent: return "ShutdownAckSent" default: return fmt.Sprintf("Invalid AssociationState %d", a) } } // Association represents an SCTP assocation // 13.2. Parameters Necessary per Association (i.e., the TCB) // Peer : Tag value to be sent in every packet and is received // Verification: in the INIT or INIT ACK chunk. // Tag : // // My : Tag expected in every inbound packet and sent in the // Verification: INIT or INIT ACK chunk. // // Tag : // State : A state variable indicating what state the association // : is in, i.e., COOKIE-WAIT, COOKIE-ECHOED, ESTABLISHED, // : SHUTDOWN-PENDING, SHUTDOWN-SENT, SHUTDOWN-RECEIVED, // : SHUTDOWN-ACK-SENT. // // Note: No "CLOSED" state is illustrated since if a // association is "CLOSED" its TCB SHOULD be removed. type Association struct { peerVerificationTag uint32 myVerificationTag uint32 state AssociationState //peerTransportList //primaryPath //overallErrorCount //overallErrorThreshold //peerReceiverWindow (peerRwnd) myNextTSN uint32 // nextTSN peerLastTSN uint32 // lastRcvdTSN //peerMissingTSN (MappingArray) //ackState //inboundStreams //outboundStreams //reassemblyQueue //localTransportAddressList //associationPTMU // Non-RFC internal data sourcePort uint16 destinationPort uint16 myMaxNumInboundStreams uint16 myMaxNumOutboundStreams uint16 myRreceiverWindowCredit uint32 // TODO are these better as channels // Put a blocking goroutine in port-recieve (vs callbacks) outboundHandler func(*Packet) dataHandler func([]byte) } // PushPacket pushes a SCTP packet onto the assocation func (a *Association) PushPacket(p *Packet) error { if err := checkPacket(p); err != nil { return errors.Wrap(err, "Failed validating packet") } for _, c := range p.Chunks { if err := a.handleChunk(p, c); err != nil { return errors.Wrap(err, "Failed handling chunk") } } return nil } // Close ends the SCTP Association and cleans up any state func (a *Association) Close() error { return nil } // NewAssocation creates a new Association and the state needed to manage it func NewAssocation(outboundHandler func(*Packet), dataHandler func([]byte)) *Association { rs := rand.NewSource(time.Now().UnixNano()) r := rand.New(rs) return &Association{ myVerificationTag: r.Uint32(), myNextTSN: r.Uint32(), outboundHandler: outboundHandler, dataHandler: dataHandler, state: Open, myMaxNumOutboundStreams: math.MaxUint16, myMaxNumInboundStreams: math.MaxUint16, } } func checkPacket(p *Packet) error { // All packets must adhere to these rules // This is the SCTP sender's port number. It can be used by the // receiver in combination with the source IP address, the SCTP // destination port, and possibly the destination IP address to // identify the association to which this packet belongs. The port // number 0 MUST NOT be used. if p.SourcePort == 0 { return errors.New("SCTP Packet must not have a source port of 0") } // This is the SCTP port number to which this packet is destined. // The receiving host will use this port number to de-multiplex the // SCTP packet to the correct receiving endpoint/application. The // port number 0 MUST NOT be used. if p.DestinationPort == 0 { return errors.New("SCTP Packet must not have a destination port of 0") } for _, c := range p.Chunks { switch c.(type) { case *Init: // An INIT or INIT ACK chunk MUST NOT be bundled with any other chunk. // They MUST be the only chunks present in the SCTP packets that carry // them. if len(p.Chunks) != 1 { return errors.New("INIT chunk must not be bundled with any other chunk") } // A packet containing an INIT chunk MUST have a zero Verification // Tag. if p.VerificationTag != 0 { return errors.Errorf("INIT chunk expects a verification tag of 0 on the packet when out-of-the-blue") } } } return nil } func min(a, b uint16) uint16 { if a < b { return a } return b } func (a *Association) handleInit(p *Packet, i *Init) (*Packet, error) { // Should we be setting any of these permanently until we've ACKed further? a.myMaxNumInboundStreams = min(i.numInboundStreams, a.myMaxNumInboundStreams) a.myMaxNumOutboundStreams = min(i.numOutboundStreams, a.myMaxNumOutboundStreams) a.peerVerificationTag = i.initiateTag a.sourcePort = p.DestinationPort a.destinationPort = p.SourcePort // 13.2 This is the last TSN received in sequence. This value // is set initially by taking the peer's initial TSN, // received in the INIT or INIT ACK chunk, and // subtracting one from it. a.peerLastTSN = i.initialTSN - 1 outbound := &Packet{} outbound.VerificationTag = a.peerVerificationTag outbound.SourcePort = a.sourcePort outbound.DestinationPort = a.destinationPort initAck := &InitAck{} initAck.initialTSN = a.myNextTSN initAck.numOutboundStreams = a.myMaxNumOutboundStreams initAck.numInboundStreams = a.myMaxNumInboundStreams initAck.initiateTag = a.myVerificationTag initAck.advertisedReceiverWindowCredit = a.myRreceiverWindowCredit initAck.params = []Param{NewRandomStateCookie(), NewEmptySupportedExtensions()} outbound.Chunks = []Chunk{initAck} return outbound, nil } func (a *Association) handleChunk(p *Packet, c Chunk) error { if _, err := c.Check(); err != nil { errors.Wrap(err, "Failed validating chunk") // TODO: Create ABORT } switch ct := c.(type) { case *Init: switch a.state { case Open: p, err := a.handleInit(p, ct) if err != nil { return errors.Wrap(err, "Failure handling INIT") } a.outboundHandler(p) return nil case CookieEchoed: // https://tools.ietf.org/html/rfc4960#section-5.2.1 // Upon receipt of an INIT in the COOKIE-ECHOED state, an endpoint MUST // respond with an INIT ACK using the same parameters it sent in its // original INIT chunk (including its Initiate Tag, unchanged) return errors.Errorf("TODO respond with original cookie %s", a.state) default: // 5.2.2. Unexpected INIT in States Other than CLOSED, COOKIE-ECHOED, // COOKIE-WAIT, and SHUTDOWN-ACK-SENT return errors.Errorf("TODO Handle Init when in state %s", a.state) } } return nil }