diff --git a/.gitignore b/.gitignore index e070cbc..ebf9ce6 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,5 @@ examples/http-dialer/http-dialer examples/pairchat/pairchat examples/simple-pipe/simple-pipe examples/timeserve/timeserve +examples/timeserve-close/timeserve iroh-go/target/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..4b28717 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,12 @@ +# Contributing + +Please read the [README](./README.md) before diving in. Please [raise an issue +before starting on large +changes](https://git.coopcloud.tech/decentral1se/iroh-go/issues/new). + +## Conventions + +* API wrappers (such as `endpoint.go`) should return upstream errors even + though it would be more convenient to use `iroh.As` internally. This is to + maintain consistency in error handling across the entire API boundary + (bindings or custom). The API consumer uses `iroh.As`, not the producer. diff --git a/README.md b/README.md index 79a0360..16e0e4b 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,10 @@ in [`libs`](./libs). Apache 2.0 & MIT, following the upstream licensing choices in [`iroh-ffi`](https://github.com/n0-computer/iroh-ffi). +## Contributing + +[`CONTRIBUTING.md`](./CONTRIBUTING.md). + ## LLM Policy We adopt the [Strict No LLM / No AI diff --git a/alpns.go b/alpns.go new file mode 100644 index 0000000..ebff344 --- /dev/null +++ b/alpns.go @@ -0,0 +1,13 @@ +package iroh_ffi + +type Alpn []byte + +func Alpns(alpns ...Alpn) [][]byte { + var a [][]byte + + for _, alpn := range alpns { + a = append(a, []byte(alpn)) + } + + return a +} diff --git a/endpoint.go b/endpoint.go new file mode 100644 index 0000000..745354e --- /dev/null +++ b/endpoint.go @@ -0,0 +1,55 @@ +package iroh_ffi + +import ( + "context" + + "golang.org/x/sync/errgroup" +) + +func (e *Endpoint) Accept(ctx context.Context, a Accepter) error { + g, gCtx := errgroup.WithContext(ctx) + + g.Go(func() error { + for { + incomingConn := *e.AcceptNext() + if incomingConn == nil { + return nil + } + + acceptedConn, err := incomingConn.Accept() + if err != nil { + return err + } + + conn, err := acceptedConn.Connect() + if err != nil { + return err + } + + Log.Debug("incoming connection established") + + if err := a.Accept(gCtx, conn); err != nil { + return err + } + + return nil + } + }) + + return g.Wait() +} + +func (e *Endpoint) Open(ctx context.Context, addr *EndpointAddr, alpn Alpn, o Opener) error { + conn, err := e.Connect(addr, alpn) + if err != nil { + return err + } + + Log.Debug("remote connection established") + + if err := o.Open(ctx, conn); err != nil { + return err + } + + return nil +} diff --git a/endpoint/endpoint.go b/endpoint/endpoint.go new file mode 100644 index 0000000..8b54ad9 --- /dev/null +++ b/endpoint/endpoint.go @@ -0,0 +1,78 @@ +package endpoint + +import ( + "errors" + "log/slog" + "os" + + iroh "git.coopcloud.tech/decentral1se/iroh-go" +) + +type Option func(*iroh.EndpointBuilder) error + +func New(options ...Option) (*iroh.Endpoint, error) { + var err error + + endpointBuilder := iroh.NewEndpointBuilder() + + for _, opt := range options { + if optErr := opt(endpointBuilder); optErr != nil { + err = errors.Join(err, optErr) + } + } + + // TODO(d1): implement DNS customisation when it lands + // https://docs.iroh.computer/connecting/dns-address-lookup#use-your-own-server + // https://github.com/n0-computer/iroh-ffi/issues/289 + + endpoint, err := endpointBuilder.Bind() + if bindErr := iroh.As(err); err != nil { + err = errors.Join(err, bindErr) + } + + return endpoint, err +} + +func WithAlpns(alpn ...iroh.Alpn) Option { + return func(endpointBuilder *iroh.EndpointBuilder) error { + endpointBuilder.Alpns(iroh.Alpns(alpn...)) + return nil + } +} + +func WithApplyN0() Option { + return func(endpointBuilder *iroh.EndpointBuilder) error { + endpointBuilder.ApplyN0() + return nil + } +} + +func WithApplyN0DisableRelay() Option { + return func(endpointBuilder *iroh.EndpointBuilder) error { + endpointBuilder.ApplyN0DisableRelay() + return nil + } +} + +func WithApplyMinimal() Option { + return func(endpointBuilder *iroh.EndpointBuilder) error { + endpointBuilder.ApplyMinimal() + return nil + } +} + +func WithRelayMode(rMode *iroh.RelayMode) Option { + return func(endpointBuilder *iroh.EndpointBuilder) error { + endpointBuilder.RelayMode(rMode) + return nil + } +} + +func WithDebug() Option { + return func(endpointBuilder *iroh.EndpointBuilder) error { + lvl := new(slog.LevelVar) + lvl.Set(slog.LevelDebug) + iroh.Log = slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: lvl})) + return nil + } +} diff --git a/examples/allowlist/go.mod b/examples/allowlist/go.mod index 18e07d6..f3f1d75 100644 --- a/examples/allowlist/go.mod +++ b/examples/allowlist/go.mod @@ -4,4 +4,6 @@ go 1.26.1 require git.coopcloud.tech/decentral1se/iroh-go v0.0.0-20260717110820-68ad06fe2a25 +require golang.org/x/sync v0.24.0 // indirect + replace git.coopcloud.tech/decentral1se/iroh-go => ../../ diff --git a/examples/allowlist/go.sum b/examples/allowlist/go.sum new file mode 100644 index 0000000..76e6342 --- /dev/null +++ b/examples/allowlist/go.sum @@ -0,0 +1,2 @@ +golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA= +golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= diff --git a/examples/connect-desktop/go.mod b/examples/connect-desktop/go.mod index a519804..6c3e29d 100644 --- a/examples/connect-desktop/go.mod +++ b/examples/connect-desktop/go.mod @@ -33,6 +33,7 @@ require ( github.com/wailsapp/mimetype v1.4.1 // indirect golang.org/x/crypto v0.33.0 // indirect golang.org/x/net v0.35.0 // indirect + golang.org/x/sync v0.24.0 // indirect golang.org/x/sys v0.30.0 // indirect golang.org/x/text v0.22.0 // indirect ) diff --git a/examples/connect-desktop/go.sum b/examples/connect-desktop/go.sum index b1e0229..6022f51 100644 --- a/examples/connect-desktop/go.sum +++ b/examples/connect-desktop/go.sum @@ -64,6 +64,8 @@ golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5 golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= +golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA= +golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/examples/connect/go.mod b/examples/connect/go.mod index 9397db9..6b646ea 100644 --- a/examples/connect/go.mod +++ b/examples/connect/go.mod @@ -4,4 +4,6 @@ go 1.26.1 require git.coopcloud.tech/decentral1se/iroh-go v0.0.0-20260717110820-68ad06fe2a25 +require golang.org/x/sync v0.24.0 // indirect + replace git.coopcloud.tech/decentral1se/iroh-go => ../../ diff --git a/examples/connect/go.sum b/examples/connect/go.sum index e69de29..76e6342 100644 --- a/examples/connect/go.sum +++ b/examples/connect/go.sum @@ -0,0 +1,2 @@ +golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA= +golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= diff --git a/examples/echo-endpoint/go.mod b/examples/echo-endpoint/go.mod index 5b64f43..ed45e4a 100644 --- a/examples/echo-endpoint/go.mod +++ b/examples/echo-endpoint/go.mod @@ -4,6 +4,9 @@ go 1.26.1 require git.coopcloud.tech/decentral1se/iroh-go v0.0.0-20260717110820-68ad06fe2a25 -require gomod.cblgh.org/cerca v0.3.3 // indirect +require ( + golang.org/x/sync v0.24.0 // indirect + gomod.cblgh.org/cerca v0.3.3 +) replace git.coopcloud.tech/decentral1se/iroh-go => ../../ diff --git a/examples/echo-endpoint/go.sum b/examples/echo-endpoint/go.sum index a350675..b32c282 100644 --- a/examples/echo-endpoint/go.sum +++ b/examples/echo-endpoint/go.sum @@ -1,2 +1,4 @@ +golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA= +golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= gomod.cblgh.org/cerca v0.3.3 h1:NaDO+J/cHn5nIsmLhjBUfWb/4RkNg6Dn3frrbX7TvHc= gomod.cblgh.org/cerca v0.3.3/go.mod h1:ZLFyHtI0Dd9GohYRn2Y+HpY3vpHQHa21efOTeSXmWvo= diff --git a/examples/echosrv/go.mod b/examples/echosrv/go.mod index 576b2d1..f41d13b 100644 --- a/examples/echosrv/go.mod +++ b/examples/echosrv/go.mod @@ -4,6 +4,9 @@ go 1.26.1 require git.coopcloud.tech/decentral1se/iroh-go v0.0.0-20260717110820-68ad06fe2a25 -require gomod.cblgh.org/cerca v0.3.3 // indirect +require ( + golang.org/x/sync v0.24.0 // indirect + gomod.cblgh.org/cerca v0.3.3 +) replace git.coopcloud.tech/decentral1se/iroh-go => ../../ diff --git a/examples/echosrv/go.sum b/examples/echosrv/go.sum index a350675..b32c282 100644 --- a/examples/echosrv/go.sum +++ b/examples/echosrv/go.sum @@ -1,2 +1,4 @@ +golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA= +golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= gomod.cblgh.org/cerca v0.3.3 h1:NaDO+J/cHn5nIsmLhjBUfWb/4RkNg6Dn3frrbX7TvHc= gomod.cblgh.org/cerca v0.3.3/go.mod h1:ZLFyHtI0Dd9GohYRn2Y+HpY3vpHQHa21efOTeSXmWvo= diff --git a/examples/errors/go.mod b/examples/errors/go.mod index 811614a..08cc479 100644 --- a/examples/errors/go.mod +++ b/examples/errors/go.mod @@ -4,4 +4,6 @@ go 1.26.1 require git.coopcloud.tech/decentral1se/iroh-go v0.0.0-20260717110820-68ad06fe2a25 +require golang.org/x/sync v0.24.0 // indirect + replace git.coopcloud.tech/decentral1se/iroh-go => ../../ diff --git a/examples/errors/go.sum b/examples/errors/go.sum index e69de29..76e6342 100644 --- a/examples/errors/go.sum +++ b/examples/errors/go.sum @@ -0,0 +1,2 @@ +golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA= +golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= diff --git a/examples/http-dialer/go.mod b/examples/http-dialer/go.mod index 25d0d58..0c78ac4 100644 --- a/examples/http-dialer/go.mod +++ b/examples/http-dialer/go.mod @@ -4,6 +4,9 @@ go 1.26.1 require git.coopcloud.tech/decentral1se/iroh-go v0.0.0-20260717110820-68ad06fe2a25 -require gomod.cblgh.org/cerca v0.3.3 // indirect +require ( + golang.org/x/sync v0.24.0 // indirect + gomod.cblgh.org/cerca v0.3.3 +) replace git.coopcloud.tech/decentral1se/iroh-go => ../../ diff --git a/examples/http-dialer/go.sum b/examples/http-dialer/go.sum index a350675..b32c282 100644 --- a/examples/http-dialer/go.sum +++ b/examples/http-dialer/go.sum @@ -1,2 +1,4 @@ +golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA= +golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= gomod.cblgh.org/cerca v0.3.3 h1:NaDO+J/cHn5nIsmLhjBUfWb/4RkNg6Dn3frrbX7TvHc= gomod.cblgh.org/cerca v0.3.3/go.mod h1:ZLFyHtI0Dd9GohYRn2Y+HpY3vpHQHa21efOTeSXmWvo= diff --git a/examples/pairchat/go.mod b/examples/pairchat/go.mod index 3c80def..e18f254 100644 --- a/examples/pairchat/go.mod +++ b/examples/pairchat/go.mod @@ -4,4 +4,6 @@ go 1.26.1 require git.coopcloud.tech/decentral1se/iroh-go v0.0.0-20260717110820-68ad06fe2a25 +require golang.org/x/sync v0.24.0 // indirect + replace git.coopcloud.tech/decentral1se/iroh-go => ../../ diff --git a/examples/pairchat/go.sum b/examples/pairchat/go.sum new file mode 100644 index 0000000..76e6342 --- /dev/null +++ b/examples/pairchat/go.sum @@ -0,0 +1,2 @@ +golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA= +golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= diff --git a/examples/simple-pipe/go.mod b/examples/simple-pipe/go.mod index 9bb865f..7d880bf 100644 --- a/examples/simple-pipe/go.mod +++ b/examples/simple-pipe/go.mod @@ -4,4 +4,6 @@ go 1.26.1 require git.coopcloud.tech/decentral1se/iroh-go v0.0.0-20260717110820-68ad06fe2a25 +require golang.org/x/sync v0.24.0 // indirect + replace git.coopcloud.tech/decentral1se/iroh-go => ../../ diff --git a/examples/simple-pipe/go.sum b/examples/simple-pipe/go.sum index e69de29..76e6342 100644 --- a/examples/simple-pipe/go.sum +++ b/examples/simple-pipe/go.sum @@ -0,0 +1,2 @@ +golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA= +golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= diff --git a/examples/timeserve-close/README.md b/examples/timeserve-close/README.md new file mode 100644 index 0000000..bbeef4e --- /dev/null +++ b/examples/timeserve-close/README.md @@ -0,0 +1,3 @@ +# timeserve-close + +> **WIP** diff --git a/examples/timeserve-close/go.mod b/examples/timeserve-close/go.mod new file mode 100644 index 0000000..da1d030 --- /dev/null +++ b/examples/timeserve-close/go.mod @@ -0,0 +1,9 @@ +module git.coopcloud.tech/decentral1se/iroh-go/examples/timeserve + +go 1.26.1 + +require git.coopcloud.tech/decentral1se/iroh-go v0.0.0-20260717110820-68ad06fe2a25 + +require golang.org/x/sync v0.24.0 // indirect + +replace git.coopcloud.tech/decentral1se/iroh-go => ../../ diff --git a/examples/timeserve-close/go.sum b/examples/timeserve-close/go.sum new file mode 100644 index 0000000..76e6342 --- /dev/null +++ b/examples/timeserve-close/go.sum @@ -0,0 +1,2 @@ +golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA= +golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= diff --git a/examples/timeserve-close/makefile b/examples/timeserve-close/makefile new file mode 100644 index 0000000..aa931af --- /dev/null +++ b/examples/timeserve-close/makefile @@ -0,0 +1,2 @@ +default: + @go build -v ./... diff --git a/examples/timeserve-close/timeserve.go b/examples/timeserve-close/timeserve.go new file mode 100644 index 0000000..29fdb75 --- /dev/null +++ b/examples/timeserve-close/timeserve.go @@ -0,0 +1,108 @@ +package main + +import ( + "context" + "flag" + "fmt" + "time" + + iroh "git.coopcloud.tech/decentral1se/iroh-go" + "git.coopcloud.tech/decentral1se/iroh-go/endpoint" +) + +var ( + alpn = iroh.Alpn("iroh-go-timeserve/0") + frameSize uint32 = 256 + endpointPeer *string + numMessages = 5 +) + +type Timeserve struct{} + +func (t Timeserve) Open(ctx context.Context, conn *iroh.Connection) error { + stream, err := conn.OpenBi() + if err := iroh.As(err); err != nil { + return err + } + + err = stream.Ready() + if err := iroh.As(err); err != nil { + panic(err) + } + + recv := stream.Recv() + for range numMessages { + frame, err := recv.Read(frameSize) + if err := iroh.As(err); err != nil { + return err + } + + fmt.Printf("recv: %s: %s\n", recv.Id(), string(frame)) + } + + return nil +} + +func (t Timeserve) Accept(ctx context.Context, conn *iroh.Connection) error { + stream, err := conn.AcceptBi() + if err := iroh.As(err); err != nil { + return err + } + + send := stream.Send() + for range numMessages { + now := time.Now().Format(time.DateTime) + fmt.Printf("send: %s: %s\n", send.Id(), now) + + err := send.WriteAll([]byte(now)) + if err := iroh.As(err); err != nil { + return err + } + + <-time.After(1 * time.Second) + } + + return nil +} + +func main() { + endpointPeer = flag.String("endpoint", "", "endpoint id of peer") + flag.Parse() + + endpoint, err := endpoint.New( + endpoint.WithAlpns(alpn), + endpoint.WithApplyN0(), + endpoint.WithDebug(), + ) + if err := iroh.As(err); err != nil { + panic(err) + } + + endpoint.Online() + + timeserve := Timeserve{} + ctx := context.Background() + + if *endpointPeer == "" { + fmt.Println("endpoint online! use id to connect peer:") + fmt.Printf("timeserve -endpoint %s\n", endpoint.Id()) + + err := endpoint.Accept(ctx, timeserve) + if err := iroh.As(err); err != nil { + panic(err) + } + + return + } + + remote, err := iroh.EndpointIdFromString(*endpointPeer) + if err := iroh.As(err); err != nil { + panic(err) + } + addr := iroh.NewEndpointAddr(remote, nil, nil) + + err = endpoint.Open(ctx, addr, alpn, timeserve) + if err := iroh.As(err); err != nil { + panic(err) + } +} diff --git a/examples/timeserve/go.mod b/examples/timeserve/go.mod index 0fd40c3..da1d030 100644 --- a/examples/timeserve/go.mod +++ b/examples/timeserve/go.mod @@ -4,4 +4,6 @@ go 1.26.1 require git.coopcloud.tech/decentral1se/iroh-go v0.0.0-20260717110820-68ad06fe2a25 +require golang.org/x/sync v0.24.0 // indirect + replace git.coopcloud.tech/decentral1se/iroh-go => ../../ diff --git a/examples/timeserve/go.sum b/examples/timeserve/go.sum new file mode 100644 index 0000000..76e6342 --- /dev/null +++ b/examples/timeserve/go.sum @@ -0,0 +1,2 @@ +golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA= +golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= diff --git a/go.mod b/go.mod index c3bb5cf..cf3e744 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module git.coopcloud.tech/decentral1se/iroh-go go 1.26.1 + +require golang.org/x/sync v0.24.0 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..76e6342 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA= +golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= diff --git a/interfaces.go b/interfaces.go new file mode 100644 index 0000000..cc6f57c --- /dev/null +++ b/interfaces.go @@ -0,0 +1,16 @@ +package iroh_ffi + +import "context" + +type Accepter interface { + Accept(context.Context, *Connection) error +} + +type Opener interface { + Open(context.Context, *Connection) error +} + +type AcceptOpener interface { + Accepter + Opener +} diff --git a/logger.go b/logger.go new file mode 100644 index 0000000..52e1e49 --- /dev/null +++ b/logger.go @@ -0,0 +1,8 @@ +package iroh_ffi + +import ( + "log/slog" + "os" +) + +var Log = slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{})) diff --git a/stream.go b/stream.go new file mode 100644 index 0000000..31628ae --- /dev/null +++ b/stream.go @@ -0,0 +1,8 @@ +package iroh_ffi + +func (s *BiStream) Ready() error { + if err := s.Send().WriteAll([]byte{1}); err != nil { + return err + } + return nil +}