Skip to content

Commit

Permalink
update: final v0 (plus) API and spec (#7)
Browse files Browse the repository at this point in the history
* new: moving runtime towards v0+

* new: v0+ spec

* fix: SetReadDeadline otherwise may block too long

* update: v0 finalized spec with namespace

* style: format code with Gofumpt

This commit fixes the style issues introduced in 70401cf according to the output
from Gofumpt.

Details: #7

* fix: remove benchmark result

benchmark result should be ignored.

* update: .gitignore ignore bench.txt

* fix: UnixConn and TCPConn failing on macOS

* fix: deepsource detected bugs and issues

* docs: update README

* update: ci and readme

- Update CI to fail fast. Add `golangci-lint` to CI.
- Update full name WebAssembly Transport Executables Runtime. Update badges.

* satisfy the linting gods

* style: format code with Gofumpt

This commit fixes the style issues introduced in 0422575 according to the output
from Gofumpt.

Details: #7

* readme pass

---------

Co-authored-by: deepsource-autofix[bot] <62050782+deepsource-autofix[bot]@users.noreply.github.com>
Co-authored-by: Jack Wampler <[email protected]>
  • Loading branch information
3 people authored Oct 24, 2023
1 parent baa9b44 commit ec4b9f1
Show file tree
Hide file tree
Showing 54 changed files with 2,421 additions and 1,803 deletions.
14 changes: 14 additions & 0 deletions .github/assets/logo_v0.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
43 changes: 39 additions & 4 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
# This workflow will build a golang project
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go

name: "Go"

on:
Expand All @@ -12,11 +9,12 @@ on:
jobs:
build:
strategy:
fail-fast: false
fail-fast: true
matrix:
# os: [ "ubuntu-latest", "windows-latest", "macos-latest" ] # Windows is not supported until net library implements Fd() for Windows
os: [ "ubuntu-latest", "macos-latest" ]
go: [ "1.20.x", "1.21.x" ]
name: "build and test"
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
Expand All @@ -28,3 +26,40 @@ jobs:
run: go build -v ./...
- name: Test
run: go test -failfast ./...

golangci-lint:
strategy:
matrix:
go: [ "1.20.x", "1.21.x" ]
os: [ "ubuntu-latest", "macos-latest" ]
name: lint
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
with:
go-version: ${{ matrix.go }}
cache: false
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
with:
# Require: The version of golangci-lint to use.
# When `install-mode` is `binary` (default) the value can be v1.2 or v1.2.3 or `latest` to use the latest version.
# When `install-mode` is `goinstall` the value can be v1.2.3, `latest`, or the hash of a commit.
version: v1.54
args: -v --disable structcheck,govet --timeout 5m

# Optional: working directory, useful for monorepos
# working-directory: somedir

# Optional: golangci-lint command line arguments.
#
# Note: by default the `.golangci.yml` file should be at the root of the repository.
# The location of the configuration file can be changed by using `--config=`
# args: --timeout=30m --config=/my/path/.golangci.yml --issues-exit-code=0

# Optional: show only new issues if it's a pull request. The default value is `false`.
# only-new-issues: true

# Optional:The mode to install golangci-lint. It can be 'binary' or 'goinstall'.
# install-mode: "goinstall"
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,7 @@ Cargo.lock
# vendor/

# Go workspace file
go.work
go.work

# Benchmark file
bench.txt
111 changes: 98 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,25 +1,110 @@
# W.A.T.E.R.: WebAssembly Transport Executable Reactor
[![License](https://img.shields.io/badge/License-Apache_2.0-yellowgreen.svg)](https://opensource.org/licenses/Apache-2.0) [![Build Status](https://github.com/gaukas/water/actions/workflows/go.yml/badge.svg?branch=master)](https://github.com/gaukas/water/actions/workflows/go.yml)
# W.A.T.E.R.: WebAssembly Transport Executables Runtime
[![License](https://img.shields.io/badge/License-Apache_2.0-yellowgreen.svg)](https://opensource.org/licenses/Apache-2.0)
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fgaukas%2Fwater.svg?type=shield&issueType=license)](https://app.fossa.com/projects/git%2Bgithub.com%2Fgaukas%2Fwater?ref=badge_shield&issueType=license)
[![Build Status](https://github.com/gaukas/water/actions/workflows/go.yml/badge.svg?branch=master)](https://github.com/gaukas/water/actions/workflows/go.yml)
[![Go Reference](https://pkg.go.dev/badge/github.com/gaukas/water.svg)](https://pkg.go.dev/github.com/gaukas/water)
[![DeepSource](https://app.deepsource.com/gh/gaukas/water.svg/?label=resolved+issues&show_trend=true&token=SonUOOtyjJHnPuIdEBGZp4zx)](https://app.deepsource.com/gh/gaukas/water/)

W.A.T.E.R. provides a runtime environment for WebAssembly modules to run in and work as a application-layer transport protocol. It is designed to be highly portable and lightweight, and can be used as a replacement for pluggable transports.
<div style="width: 100%; height = 160px">
<div style="width: 75%; height: 150px; float: left;">
WATER-go provides a golang runtime for WebAssembly Transport Modules(WATM) as a pluggable
application-layer transport protocol provider. It is designed to be highly portable and
lightweight, allowing for rapidly deployable pluggable transports. While other pluggable
transport implementations require a fresh client deployment (and app-store review) to update
their protocol WATER allows <b><u>dynamic delivery of new transports</u></b> in real time
over the network.<br />
<br />
</div>
<div style="margin-left: 80%; height: 150px;">
<img src=".github/assets/logo_v0.svg" alt="WATER wasm transport" align="right">
</div>
</div>

## API
Information about writing, buiding, and sharing WebAssembly Transport Modules can be found in the
[water-rs](https://github.com/erikziyunchi/water-rs/tree/main/crates/wasm) library.

Currently, W.A.T.E.R. provides a set of APIs based on **WASI Preview 1 (wasip1)** snapshot.
# Usage

### Config
A `Config` is a struct that contains the configuration for a WASI instance. It is used to configure the WASI reactor before starting it.
<!-- ## API -->
Based on **WASI Preview 1 (wasip1)** snapshot, currently W.A.T.E.R. provides a set of
`net`-like APIs, including `Dialer`, `Listener` and `Relay`.

### Dialer
### Dialer

A `Dialer` could be used to dial a remote address upon `Dial()` and return a `net.Conn` back to the caller once the connection is established. Caller could use the `net.Conn` to read and write data to the remote address and the data will be processed by a WebAssembly instance.
A `Dialer` connects to a remote address and returns a `net.Conn` to the caller if the connection can
be successfully established. The `net.Conn` then provides tunnelled read and write to the remote
endpoint with the WebAssembly module wrapping / encrypting / transforming the traffic.

`Dialer` is used to encapsulate the caller's connection into an outbound, transport-wrapped
connection.

```go
wasm, err := os.ReadFile("./examples/v0plus/plain/plain.wasm")

config := &water.Config{
TMBin: wasm,
}

dialer, err := water.NewDialer(config)
conn, err := dialer.Dial("tcp", remoteAddr)
// ...
```

### Listener

A `Listener` could be used to listen on a local address. Upon `Accept()`, it returns a `net.Conn` back once an incoming connection is accepted from the wrapped listener. Caller could use the `net.Conn` to read and write data to the remote address and the data will be processed by a WebAssembly instance.
A `Listener` listens on a local address for incoming connections which it `Accept()`s, returning
a `net.Conn` for the caller to handle. The WebAssembly Module is responsible for the initial
accpt allowing it to implement both the server side handshake as well as any unwrap / decrypt
/reform operations required to validate and re-assemble the stream. The `net.Conn` returned provides
the normalized stream, and allows the caller to write back to the client with the WebAssembly module
encoding / encrypting / transforming traffic to be obfuscated on the wire on the way to the remote
client.


`Listener` is used to decapsulate incoming transport-wrapped connections for the caller to handle,
managing the tunnel obfuscation once a connection is established.

```go
wasm, err := os.ReadFile("./examples/v0plus/plain/plain.wasm")

config := &water.Config{
TMBin: wasm,
}

lis, err := config.Listen("tcp", localAddr)
defer lis.Close()
log.Infof("Listening on %s", lis.Addr().String())

clientCntr := 0
for {
conn, err := lis.Accept()
// ...
}
```

### Relay

A `Relay` combines the role of `Dialer` and `Listener`. It listens on a local address `Accept()`-ing
incoming connections and `Dial()`-ing the remote endpoints on establishment. The connecions are
tunneled through the WebAssembly Transport Module allowing the module to handshake, encode,
transform, pad, etc. without any caller interaction. The internal `Relay` manages the incoming
connections as well as the associated outgoing connectons.

`Relay` is a managed many-to-many handler for incoming connections that uses the WebAssemble module
to tunnel traffic.

```go
wasm, err := os.ReadFile("./examples/v0plus/plain/plain.wasm")

config := &water.Config{
TMBin: wasm,
}

relay, err := water.NewRelay(config)

### Server
err = relay.ListenAndRelayTo("tcp", localAddr, "tcp", remoteAddr)
```

A `Server` somewhat combines the role of `Dialer` and `Listener`. It could be used to listen on a local address and dial a remote address and automatically `Accept()` the incoming connections, feed them into the WebAssembly instance and `Dial()` the pre-defined remote address. Without any caller interaction, the `Server` will automatically* handle the data transmission between the two ends.
## Example

***TODO: Server could not be realistic until WASI multi-threading or blocking mainloop is supported**
See [examples](./examples) for example usecase of W.A.T.E.R. API, including `Dialer`, `Listener` and `Relay`.
79 changes: 33 additions & 46 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,45 +2,34 @@ package water

import (
"net"
"os"

"github.com/gaukas/water/internal/log"
"github.com/gaukas/water/internal/wasm"
)

// Config defines the configuration for the WATER Dialer/Config interface.
type Config struct {
// WATMBin contains the binary format of the WebAssembly Transport Module.
// TMBin contains the binary format of the WebAssembly Transport Module.
// In a typical use case, this mandatory field is populated by loading
// from a .wasm file, downloaded from a remote target, or generated from
// a .wat (WebAssembly Text Format) file.
WATMBin []byte
TMBin []byte

// DialerFunc specifies a func that dials the specified address on the
// NetworkDialerFunc specifies a func that dials the specified address on the
// named network. This optional field can be set to override the Go
// default dialer func:
// net.Dial(network, address)
DialerFunc func(network, address string) (net.Conn, error)
NetworkDialerFunc func(network, address string) (net.Conn, error)

// NetworkListener specifies a net.listener implementation that listens
// on the specified address on the named network. This optional field
// will be used to provide (incoming) network connections from a
// presumably remote source to the WASM instance. Required by
// ListenConfig().
// water.WrapListener().
NetworkListener net.Listener

// Feature specifies a series of experimental features for the WASM
// runtime.
//
// Each feature flag is bit-masked and version-dependent, and flags
// are independent of each other. This means that a particular
// feature flag may be supported in one version of the runtime but
// not in another. If a feature flag is not supported or not recognized
// by the runtime, it will be silently ignored.
Feature Feature

// WATMConfig optionally provides a configuration file to be pushed into
// TMConfig optionally provides a configuration file to be pushed into
// the WASM Transport Module.
WATMConfig WATMConfig
TMConfig TMConfig

// wasiConfigFactory is used to replicate the WASI config for each WASM
// instance created. This field is for advanced use cases and/or debugging
Expand All @@ -52,32 +41,36 @@ type Config struct {
wasiConfigFactory *wasm.WASIConfigFactory
}

// Clone creates a deep copy of the Config.
func (c *Config) Clone() *Config {
if c == nil {
return nil
}

wasmClone := make([]byte, len(c.WATMBin))
copy(wasmClone, c.WATMBin)
wasmClone := make([]byte, len(c.TMBin))
copy(wasmClone, c.TMBin)

return &Config{
WATMBin: c.WATMBin,
DialerFunc: c.DialerFunc,
TMBin: c.TMBin,
NetworkDialerFunc: c.NetworkDialerFunc,
NetworkListener: c.NetworkListener,
Feature: c.Feature,
WATMConfig: c.WATMConfig,
TMConfig: c.TMConfig,
wasiConfigFactory: c.wasiConfigFactory.Clone(),
}
}

func (c *Config) DialerFuncOrDefault() func(network, address string) (net.Conn, error) {
if c.DialerFunc == nil {
// NetworkDialerFuncOrDefault returns the DialerFunc if it is not nil, otherwise
// returns the default net.Dial function.
func (c *Config) NetworkDialerFuncOrDefault() func(network, address string) (net.Conn, error) {
if c.NetworkDialerFunc == nil {
return net.Dial
}

return c.DialerFunc
return c.NetworkDialerFunc
}

// NetworkListenerOrDefault returns the NetworkListener if it is not nil,
// otherwise it panics.
func (c *Config) NetworkListenerOrPanic() net.Listener {
if c.NetworkListener == nil {
panic("water: network listener is not provided in config")
Expand All @@ -86,14 +79,17 @@ func (c *Config) NetworkListenerOrPanic() net.Listener {
return c.NetworkListener
}

// WATMBinOrDefault returns the WATMBin if it is not nil, otherwise it panics.
func (c *Config) WATMBinOrPanic() []byte {
if len(c.WATMBin) == 0 {
if len(c.TMBin) == 0 {
panic("water: WebAssembly Transport Module binary is not provided in config")
}

return c.WATMBin
return c.TMBin
}

// WASIConfig returns the WASIConfigFactory. If the pointer is
// nil, a new WASIConfigFactory will be created and returned.
func (c *Config) WASIConfig() *wasm.WASIConfigFactory {
if c.wasiConfigFactory == nil {
c.wasiConfigFactory = wasm.NewWasiConfigFactory()
Expand All @@ -102,23 +98,14 @@ func (c *Config) WASIConfig() *wasm.WASIConfigFactory {
return c.wasiConfigFactory
}

// WATMConfig defines the configuration file used by the WebAssembly Transport Module.
type WATMConfig struct {
FilePath string // Path to the config file.
}

// File opens the config file and returns the file descriptor.
func (c *WATMConfig) File() *os.File {
if c.FilePath == "" {
log.Errorf("water: WASM config file path is not provided in config")
return nil
}

f, err := os.Open(c.FilePath)
func (c *Config) Listen(network, address string) (net.Listener, error) {
lis, err := net.Listen(network, address)
if err != nil {
log.Errorf("water: failed to open WATM config file: %v", err)
return nil
return nil, err
}

return f
config := c.Clone()
config.NetworkListener = lis

return NewListener(config)
}
32 changes: 32 additions & 0 deletions config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package water_test

import (
"crypto/rand"
"net"
"reflect"
"testing"

"github.com/gaukas/water"
)

func TestConfigClone(t *testing.T) {
c := &water.Config{
TMBin: make([]byte, 256),
NetworkDialerFunc: nil, // functions aren't deeply equal unless nil
NetworkListener: &net.TCPListener{},
TMConfig: water.TMConfig{
FilePath: "/tmp/watm.toml",
},
}

_, err := rand.Read(c.TMBin)
if err != nil {
t.Fatalf("rand.Read error: %v", err)
}

ccloned := c.Clone()

if !reflect.DeepEqual(c, ccloned) {
t.Errorf("Clone() = %v, want %v", ccloned, c)
}
}
Loading

0 comments on commit ec4b9f1

Please sign in to comment.