📦 build: WebAssembly Transport Module

Modify slightly to make uTLS compatible with TinyGo and WATER.

Signed-off-by: Gaukas Wang <i@gaukas.wang>
This commit is contained in:
Gaukas Wang 2024-01-29 14:26:51 -07:00
parent 925bfb39ab
commit 513679377a
No known key found for this signature in database
GPG key ID: 9E2F8986D76F8B5D
7 changed files with 405 additions and 211 deletions

100
conn.go
View file

@ -1503,106 +1503,6 @@ func (c *Conn) HandshakeContext(ctx context.Context) error {
return c.handshakeContext(ctx)
}
func (c *Conn) handshakeContext(ctx context.Context) (ret error) {
// Fast sync/atomic-based exit if there is no handshake in flight and the
// last one succeeded without an error. Avoids the expensive context setup
// and mutex for most Read and Write calls.
if c.isHandshakeComplete.Load() {
return nil
}
handshakeCtx, cancel := context.WithCancel(ctx)
// Note: defer this before starting the "interrupter" goroutine
// so that we can tell the difference between the input being canceled and
// this cancellation. In the former case, we need to close the connection.
defer cancel()
if c.quic != nil {
c.quic.cancelc = handshakeCtx.Done()
c.quic.cancel = cancel
} else if ctx.Done() != nil {
// Start the "interrupter" goroutine, if this context might be canceled.
// (The background context cannot).
//
// The interrupter goroutine waits for the input context to be done and
// closes the connection if this happens before the function returns.
done := make(chan struct{})
interruptRes := make(chan error, 1)
defer func() {
close(done)
if ctxErr := <-interruptRes; ctxErr != nil {
// Return context error to user.
ret = ctxErr
}
}()
go func() {
select {
case <-handshakeCtx.Done():
// Close the connection, discarding the error
_ = c.conn.Close()
interruptRes <- handshakeCtx.Err()
case <-done:
interruptRes <- nil
}
}()
}
c.handshakeMutex.Lock()
defer c.handshakeMutex.Unlock()
if err := c.handshakeErr; err != nil {
return err
}
if c.isHandshakeComplete.Load() {
return nil
}
c.in.Lock()
defer c.in.Unlock()
c.handshakeErr = c.handshakeFn(handshakeCtx)
if c.handshakeErr == nil {
c.handshakes++
} else {
// If an error occurred during the handshake try to flush the
// alert that might be left in the buffer.
c.flush()
}
if c.handshakeErr == nil && !c.isHandshakeComplete.Load() {
c.handshakeErr = errors.New("tls: internal error: handshake should have had a result")
}
if c.handshakeErr != nil && c.isHandshakeComplete.Load() {
panic("tls: internal error: handshake returned an error but is marked successful")
}
if c.quic != nil {
if c.handshakeErr == nil {
c.quicHandshakeComplete()
// Provide the 1-RTT read secret now that the handshake is complete.
// The QUIC layer MUST NOT decrypt 1-RTT packets prior to completing
// the handshake (RFC 9001, Section 5.7).
c.quicSetReadSecret(QUICEncryptionLevelApplication, c.cipherSuite, c.in.trafficSecret)
} else {
var a alert
c.out.Lock()
if !errors.As(c.out.err, &a) {
a = alertInternalError
}
c.out.Unlock()
// Return an error which wraps both the handshake error and
// any alert error we may have sent, or alertInternalError
// if we didn't send an alert.
// Truncate the text of the alert to 0 characters.
c.handshakeErr = fmt.Errorf("%w%.0w", c.handshakeErr, AlertError(a))
}
close(c.quic.blockedc)
close(c.quic.signalc)
}
return c.handshakeErr
}
// ConnectionState returns basic TLS details about the connection.
func (c *Conn) ConnectionState() ConnectionState {
c.handshakeMutex.Lock()

109
conn_nowasi.go Normal file
View file

@ -0,0 +1,109 @@
//go:build !wasm && !wasi && !wasip1 && !wasip2
package tls
import (
"context"
"errors"
"fmt"
)
func (c *Conn) handshakeContext(ctx context.Context) (ret error) {
// Fast sync/atomic-based exit if there is no handshake in flight and the
// last one succeeded without an error. Avoids the expensive context setup
// and mutex for most Read and Write calls.
if c.isHandshakeComplete.Load() {
return nil
}
handshakeCtx, cancel := context.WithCancel(ctx)
// Note: defer this before starting the "interrupter" goroutine
// so that we can tell the difference between the input being canceled and
// this cancellation. In the former case, we need to close the connection.
defer cancel()
if c.quic != nil {
c.quic.cancelc = handshakeCtx.Done()
c.quic.cancel = cancel
} else if ctx.Done() != nil {
// Start the "interrupter" goroutine, if this context might be canceled.
// (The background context cannot).
//
// The interrupter goroutine waits for the input context to be done and
// closes the connection if this happens before the function returns.
done := make(chan struct{})
interruptRes := make(chan error, 1)
defer func() {
close(done)
if ctxErr := <-interruptRes; ctxErr != nil {
// Return context error to user.
ret = ctxErr
}
}()
go func() {
select {
case <-handshakeCtx.Done():
// Close the connection, discarding the error
_ = c.conn.Close()
interruptRes <- handshakeCtx.Err()
case <-done:
interruptRes <- nil
}
}()
}
c.handshakeMutex.Lock()
defer c.handshakeMutex.Unlock()
if err := c.handshakeErr; err != nil {
return err
}
if c.isHandshakeComplete.Load() {
return nil
}
c.in.Lock()
defer c.in.Unlock()
c.handshakeErr = c.handshakeFn(handshakeCtx)
if c.handshakeErr == nil {
c.handshakes++
} else {
// If an error occurred during the handshake try to flush the
// alert that might be left in the buffer.
c.flush()
}
if c.handshakeErr == nil && !c.isHandshakeComplete.Load() {
c.handshakeErr = errors.New("tls: internal error: handshake should have had a result")
}
if c.handshakeErr != nil && c.isHandshakeComplete.Load() {
panic("tls: internal error: handshake returned an error but is marked successful")
}
if c.quic != nil {
if c.handshakeErr == nil {
c.quicHandshakeComplete()
// Provide the 1-RTT read secret now that the handshake is complete.
// The QUIC layer MUST NOT decrypt 1-RTT packets prior to completing
// the handshake (RFC 9001, Section 5.7).
c.quicSetReadSecret(QUICEncryptionLevelApplication, c.cipherSuite, c.in.trafficSecret)
} else {
var a alert
c.out.Lock()
if !errors.As(c.out.err, &a) {
a = alertInternalError
}
c.out.Unlock()
// Return an error which wraps both the handshake error and
// any alert error we may have sent, or alertInternalError
// if we didn't send an alert.
// Truncate the text of the alert to 0 characters.
c.handshakeErr = fmt.Errorf("%w%.0w", c.handshakeErr, AlertError(a))
}
close(c.quic.blockedc)
close(c.quic.signalc)
}
return c.handshakeErr
}

83
conn_wasi.go Normal file
View file

@ -0,0 +1,83 @@
//go:build wasm || wasi || wasip1 || wasip2
package tls
import (
"context"
"errors"
"fmt"
)
func (c *Conn) handshakeContext(ctx context.Context) (ret error) {
// Fast sync/atomic-based exit if there is no handshake in flight and the
// last one succeeded without an error. Avoids the expensive context setup
// and mutex for most Read and Write calls.
if c.isHandshakeComplete.Load() {
return nil
}
// check if ctx has expired ONLY ONCE, since we cannot use goroutines
if ctxErr := ctx.Err(); ctxErr != nil {
return ctxErr
}
if c.quic != nil {
// c.quic.cancelc = handshakeCtx.Done()
// c.quic.cancel = cancel
}
c.handshakeMutex.Lock()
defer c.handshakeMutex.Unlock()
if err := c.handshakeErr; err != nil {
return err
}
if c.isHandshakeComplete.Load() {
return nil
}
c.in.Lock()
defer c.in.Unlock()
c.handshakeErr = c.handshakeFn(ctx)
if c.handshakeErr == nil {
c.handshakes++
} else {
// If an error occurred during the handshake try to flush the
// alert that might be left in the buffer.
c.flush()
}
if c.handshakeErr == nil && !c.isHandshakeComplete.Load() {
c.handshakeErr = errors.New("tls: internal error: handshake should have had a result")
}
if c.handshakeErr != nil && c.isHandshakeComplete.Load() {
panic("tls: internal error: handshake returned an error but is marked successful")
}
if c.quic != nil {
if c.handshakeErr == nil {
c.quicHandshakeComplete()
// Provide the 1-RTT read secret now that the handshake is complete.
// The QUIC layer MUST NOT decrypt 1-RTT packets prior to completing
// the handshake (RFC 9001, Section 5.7).
c.quicSetReadSecret(QUICEncryptionLevelApplication, c.cipherSuite, c.in.trafficSecret)
} else {
var a alert
c.out.Lock()
if !errors.As(c.out.err, &a) {
a = alertInternalError
}
c.out.Unlock()
// Return an error which wraps both the handshake error and
// any alert error we may have sent, or alertInternalError
// if we didn't send an alert.
// Truncate the text of the alert to 0 characters.
c.handshakeErr = fmt.Errorf("%w%.0w", c.handshakeErr, AlertError(a))
}
close(c.quic.blockedc)
close(c.quic.signalc)
}
return c.handshakeErr
}

108
u_conn.go
View file

@ -313,114 +313,6 @@ func (c *UConn) HandshakeContext(ctx context.Context) error {
return c.handshakeContext(ctx)
}
func (c *UConn) handshakeContext(ctx context.Context) (ret error) {
// Fast sync/atomic-based exit if there is no handshake in flight and the
// last one succeeded without an error. Avoids the expensive context setup
// and mutex for most Read and Write calls.
if c.isHandshakeComplete.Load() {
return nil
}
handshakeCtx, cancel := context.WithCancel(ctx)
// Note: defer this before starting the "interrupter" goroutine
// so that we can tell the difference between the input being canceled and
// this cancellation. In the former case, we need to close the connection.
defer cancel()
// Start the "interrupter" goroutine, if this context might be canceled.
// (The background context cannot).
//
// The interrupter goroutine waits for the input context to be done and
// closes the connection if this happens before the function returns.
if c.quic != nil {
c.quic.cancelc = handshakeCtx.Done()
c.quic.cancel = cancel
} else if ctx.Done() != nil {
done := make(chan struct{})
interruptRes := make(chan error, 1)
defer func() {
close(done)
if ctxErr := <-interruptRes; ctxErr != nil {
// Return context error to user.
ret = ctxErr
}
}()
go func() {
select {
case <-handshakeCtx.Done():
// Close the connection, discarding the error
_ = c.conn.Close()
interruptRes <- handshakeCtx.Err()
case <-done:
interruptRes <- nil
}
}()
}
c.handshakeMutex.Lock()
defer c.handshakeMutex.Unlock()
if err := c.handshakeErr; err != nil {
return err
}
if c.isHandshakeComplete.Load() {
return nil
}
c.in.Lock()
defer c.in.Unlock()
// [uTLS section begins]
if c.isClient {
err := c.BuildHandshakeState()
if err != nil {
return err
}
}
// [uTLS section ends]
c.handshakeErr = c.handshakeFn(handshakeCtx)
if c.handshakeErr == nil {
c.handshakes++
} else {
// If an error occurred during the hadshake try to flush the
// alert that might be left in the buffer.
c.flush()
}
if c.handshakeErr == nil && !c.isHandshakeComplete.Load() {
c.handshakeErr = errors.New("tls: internal error: handshake should have had a result")
}
if c.handshakeErr != nil && c.isHandshakeComplete.Load() {
panic("tls: internal error: handshake returned an error but is marked successful")
}
if c.quic != nil {
if c.handshakeErr == nil {
c.quicHandshakeComplete()
// Provide the 1-RTT read secret now that the handshake is complete.
// The QUIC layer MUST NOT decrypt 1-RTT packets prior to completing
// the handshake (RFC 9001, Section 5.7).
c.quicSetReadSecret(QUICEncryptionLevelApplication, c.cipherSuite, c.in.trafficSecret)
} else {
var a alert
c.out.Lock()
if !errors.As(c.out.err, &a) {
a = alertInternalError
}
c.out.Unlock()
// Return an error which wraps both the handshake error and
// any alert error we may have sent, or alertInternalError
// if we didn't send an alert.
// Truncate the text of the alert to 0 characters.
c.handshakeErr = fmt.Errorf("%w%.0w", c.handshakeErr, AlertError(a))
}
close(c.quic.blockedc)
close(c.quic.signalc)
}
return c.handshakeErr
}
// Copy-pasted from tls.Conn in its entirety. But c.Handshake() is now utls' one, not tls.
// Write writes data to the connection.
func (c *UConn) Write(b []byte) (int, error) {

117
u_conn_nowasi.go Normal file
View file

@ -0,0 +1,117 @@
//go:build !wasm && !wasi && !wasip1 && !wasip2
package tls
import (
"context"
"errors"
"fmt"
)
func (c *UConn) handshakeContext(ctx context.Context) (ret error) {
// Fast sync/atomic-based exit if there is no handshake in flight and the
// last one succeeded without an error. Avoids the expensive context setup
// and mutex for most Read and Write calls.
if c.isHandshakeComplete.Load() {
return nil
}
handshakeCtx, cancel := context.WithCancel(ctx)
// Note: defer this before starting the "interrupter" goroutine
// so that we can tell the difference between the input being canceled and
// this cancellation. In the former case, we need to close the connection.
defer cancel()
// Start the "interrupter" goroutine, if this context might be canceled.
// (The background context cannot).
//
// The interrupter goroutine waits for the input context to be done and
// closes the connection if this happens before the function returns.
if c.quic != nil {
c.quic.cancelc = handshakeCtx.Done()
c.quic.cancel = cancel
} else if ctx.Done() != nil {
done := make(chan struct{})
interruptRes := make(chan error, 1)
defer func() {
close(done)
if ctxErr := <-interruptRes; ctxErr != nil {
// Return context error to user.
ret = ctxErr
}
}()
go func() {
select {
case <-handshakeCtx.Done():
// Close the connection, discarding the error
_ = c.conn.Close()
interruptRes <- handshakeCtx.Err()
case <-done:
interruptRes <- nil
}
}()
}
c.handshakeMutex.Lock()
defer c.handshakeMutex.Unlock()
if err := c.handshakeErr; err != nil {
return err
}
if c.isHandshakeComplete.Load() {
return nil
}
c.in.Lock()
defer c.in.Unlock()
// [uTLS section begins]
if c.isClient {
err := c.BuildHandshakeState()
if err != nil {
return err
}
}
// [uTLS section ends]
c.handshakeErr = c.handshakeFn(handshakeCtx)
if c.handshakeErr == nil {
c.handshakes++
} else {
// If an error occurred during the hadshake try to flush the
// alert that might be left in the buffer.
c.flush()
}
if c.handshakeErr == nil && !c.isHandshakeComplete.Load() {
c.handshakeErr = errors.New("tls: internal error: handshake should have had a result")
}
if c.handshakeErr != nil && c.isHandshakeComplete.Load() {
panic("tls: internal error: handshake returned an error but is marked successful")
}
if c.quic != nil {
if c.handshakeErr == nil {
c.quicHandshakeComplete()
// Provide the 1-RTT read secret now that the handshake is complete.
// The QUIC layer MUST NOT decrypt 1-RTT packets prior to completing
// the handshake (RFC 9001, Section 5.7).
c.quicSetReadSecret(QUICEncryptionLevelApplication, c.cipherSuite, c.in.trafficSecret)
} else {
var a alert
c.out.Lock()
if !errors.As(c.out.err, &a) {
a = alertInternalError
}
c.out.Unlock()
// Return an error which wraps both the handshake error and
// any alert error we may have sent, or alertInternalError
// if we didn't send an alert.
// Truncate the text of the alert to 0 characters.
c.handshakeErr = fmt.Errorf("%w%.0w", c.handshakeErr, AlertError(a))
}
close(c.quic.blockedc)
close(c.quic.signalc)
}
return c.handshakeErr
}

91
u_conn_wasi.go Normal file
View file

@ -0,0 +1,91 @@
//go:build wasm || wasi || wasip1 || wasip2
package tls
import (
"context"
"errors"
"fmt"
)
func (c *UConn) handshakeContext(ctx context.Context) (ret error) {
// Fast sync/atomic-based exit if there is no handshake in flight and the
// last one succeeded without an error. Avoids the expensive context setup
// and mutex for most Read and Write calls.
if c.isHandshakeComplete.Load() {
return nil
}
// check if ctx has expired ONLY ONCE, since we cannot use goroutines
if ctxErr := ctx.Err(); ctxErr != nil {
return ctxErr
}
if c.quic != nil {
// c.quic.cancelc = handshakeCtx.Done()
// c.quic.cancel = cancel
}
c.handshakeMutex.Lock()
defer c.handshakeMutex.Unlock()
if err := c.handshakeErr; err != nil {
return err
}
if c.isHandshakeComplete.Load() {
return nil
}
c.in.Lock()
defer c.in.Unlock()
// [uTLS section begins]
if c.isClient {
err := c.BuildHandshakeState()
if err != nil {
return err
}
}
// [uTLS section ends]
c.handshakeErr = c.handshakeFn(ctx)
if c.handshakeErr == nil {
c.handshakes++
} else {
// If an error occurred during the hadshake try to flush the
// alert that might be left in the buffer.
c.flush()
}
if c.handshakeErr == nil && !c.isHandshakeComplete.Load() {
c.handshakeErr = errors.New("tls: internal error: handshake should have had a result")
}
if c.handshakeErr != nil && c.isHandshakeComplete.Load() {
panic("tls: internal error: handshake returned an error but is marked successful")
}
if c.quic != nil {
if c.handshakeErr == nil {
c.quicHandshakeComplete()
// Provide the 1-RTT read secret now that the handshake is complete.
// The QUIC layer MUST NOT decrypt 1-RTT packets prior to completing
// the handshake (RFC 9001, Section 5.7).
c.quicSetReadSecret(QUICEncryptionLevelApplication, c.cipherSuite, c.in.trafficSecret)
} else {
var a alert
c.out.Lock()
if !errors.As(c.out.err, &a) {
a = alertInternalError
}
c.out.Unlock()
// Return an error which wraps both the handshake error and
// any alert error we may have sent, or alertInternalError
// if we didn't send an alert.
// Truncate the text of the alert to 0 characters.
c.handshakeErr = fmt.Errorf("%w%.0w", c.handshakeErr, AlertError(a))
}
close(c.quic.blockedc)
close(c.quic.signalc)
}
return c.handshakeErr
}

View file

@ -47,8 +47,9 @@ func NewRoller() (*Roller, error) {
// If tcp connection fails or all HelloIDs are tried, returns with last error.
//
// Usage examples:
// Dial("tcp4", "google.com:443", "google.com")
// Dial("tcp", "10.23.144.22:443", "mywebserver.org")
//
// Dial("tcp4", "google.com:443", "google.com")
// Dial("tcp", "10.23.144.22:443", "mywebserver.org")
func (c *Roller) Dial(network, addr, serverName string) (*UConn, error) {
helloIDs := make([]ClientHelloID, len(c.HelloIDs))
copy(helloIDs, c.HelloIDs)
@ -77,7 +78,8 @@ func (c *Roller) Dial(network, addr, serverName string) (*UConn, error) {
var tcpConn net.Conn
var err error
for _, helloID := range helloIDs {
tcpConn, err = net.DialTimeout(network, addr, c.TcpDialTimeout)
// tcpConn, err = net.DialTimeout(network, addr, c.TcpDialTimeout)
tcpConn, err = net.Dial(network, addr)
if err != nil {
return nil, err // on tcp Dial failure return with error right away
}