├── .github └── workflows │ ├── docker.yaml │ └── hydrun.yaml ├── .gitignore ├── Dockerfile ├── Hydrunfile ├── LICENSE ├── Makefile ├── Procfile ├── README.md ├── cmd └── weron │ ├── cmd │ ├── chat.go │ ├── manager_create.go │ ├── manager_delete.go │ ├── manager_list.go │ ├── manager_root.go │ ├── root.go │ ├── signaler.go │ ├── utility_latency.go │ ├── utility_root.go │ ├── utility_throughput.go │ ├── vpn_ethernet.go │ ├── vpn_ip.go │ └── vpn_root.go │ └── main.go ├── configs ├── sql-migrate │ └── communities.yaml └── sqlboiler │ └── communities.yaml ├── db └── psql │ └── migrations │ └── communities │ └── 1646780237.sql ├── docs └── icon.svg ├── examples └── weron-echo │ └── main.go ├── go.mod ├── go.sum ├── internal ├── api │ └── websocket │ │ ├── messages.go │ │ └── types.go ├── brokers │ ├── communities.go │ ├── process │ │ └── communities.go │ └── redis │ │ └── communities.go ├── db │ └── psql │ │ ├── migrations │ │ └── communities │ │ │ └── migrations.go │ │ └── models │ │ └── communities │ │ ├── boil_queries.go │ │ ├── boil_table_names.go │ │ ├── boil_types.go │ │ ├── boil_view_names.go │ │ ├── communities.go │ │ ├── gorp_migrations.go │ │ └── psql_upsert.go ├── encryption │ └── aes.go └── persisters │ ├── communities.go │ ├── memory │ └── communities.go │ └── psql │ └── communities.go └── pkg ├── api └── webrtc │ └── v1 │ ├── exchange.go │ ├── message.go │ └── types.go ├── services └── channels.go ├── wrtcchat └── wrtcchat.go ├── wrtcconn ├── adapter.go └── adapter_named.go ├── wrtceth ├── netns_darwin.go ├── netns_linux.go ├── netns_others.go ├── netns_windows.go └── wrtceth.go ├── wrtcip ├── netns_linux.go ├── netns_others.go ├── netns_windows.go └── wrtcip.go ├── wrtcltc └── wrtcltc.go ├── wrtcmgr └── wrtcmgr.go ├── wrtcsgl └── wrtcsgl.go └── wrtcthr └── wrtcthr.go /.github/workflows/docker.yaml: -------------------------------------------------------------------------------- 1 | name: Docker CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: "0 0 * * 0" 8 | 9 | jobs: 10 | build-oci-images: 11 | runs-on: ${{ matrix.target.runner }} 12 | permissions: 13 | contents: read 14 | packages: write 15 | id-token: write 16 | strategy: 17 | matrix: 18 | target: 19 | - id: weron-linux-amd64 20 | src: . 21 | file: Dockerfile 22 | image: ghcr.io/pojntfx/weron 23 | arch: "linux/amd64,linux/arm/v7,linux/386,linux/s390x" # linux/mips64le,linux/ppc64le,linux/arm/v5 24 | runner: ubuntu-latest 25 | - id: weron-linux-arm64-v8 26 | src: . 27 | file: Dockerfile 28 | image: ghcr.io/pojntfx/weron 29 | arch: "linux/arm64/v8" 30 | runner: ubicloud-standard-4-arm 31 | 32 | steps: 33 | - name: Checkout 34 | uses: actions/checkout@v4 35 | - name: Set up QEMU 36 | uses: docker/setup-qemu-action@v3 37 | - name: Set up Docker Buildx 38 | uses: docker/setup-buildx-action@v3 39 | - name: Login to registry 40 | uses: docker/login-action@v3 41 | with: 42 | registry: ghcr.io 43 | username: ${{ github.actor }} 44 | password: ${{ secrets.GITHUB_TOKEN }} 45 | - name: Set up metadata 46 | id: meta 47 | uses: docker/metadata-action@v5 48 | with: 49 | images: ${{ matrix.target.image }} 50 | - name: Build and push image by digest to registry 51 | id: build 52 | uses: docker/build-push-action@v5 53 | with: 54 | context: ${{ matrix.target.src }} 55 | file: ${{ matrix.target.src }}/${{ matrix.target.file }} 56 | platforms: ${{ matrix.target.arch }} 57 | labels: ${{ steps.meta.outputs.labels }} 58 | outputs: type=image,name=${{ matrix.target.image }},push-by-digest=true,name-canonical=true,push=true 59 | cache-from: type=gha 60 | cache-to: type=gha,mode=max 61 | - name: Export digest 62 | run: | 63 | mkdir -p "/tmp/digests" 64 | export DIGEST="${{ steps.build.outputs.digest }}" 65 | touch "/tmp/digests/${DIGEST#sha256:}" 66 | - name: Upload digest 67 | uses: actions/upload-artifact@v4 68 | with: 69 | name: digests-${{ matrix.target.id }} 70 | path: /tmp/digests/* 71 | if-no-files-found: error 72 | retention-days: 1 73 | 74 | merge-oci-images: 75 | runs-on: ubuntu-latest 76 | permissions: 77 | contents: read 78 | packages: write 79 | id-token: write 80 | needs: build-oci-images 81 | strategy: 82 | matrix: 83 | target: 84 | - idprefix: weron-linux- 85 | image: ghcr.io/pojntfx/weron 86 | 87 | steps: 88 | - name: Checkout 89 | uses: actions/checkout@v4 90 | - name: Set up QEMU 91 | uses: docker/setup-qemu-action@v3 92 | - name: Set up Docker Buildx 93 | uses: docker/setup-buildx-action@v3 94 | - name: Login to registry 95 | uses: docker/login-action@v3 96 | with: 97 | registry: ghcr.io 98 | username: ${{ github.actor }} 99 | password: ${{ secrets.GITHUB_TOKEN }} 100 | - name: Set up metadata 101 | id: meta 102 | uses: docker/metadata-action@v5 103 | with: 104 | images: ${{ matrix.target.image }} 105 | tags: type=semver,pattern={{version}} 106 | - name: Download digests 107 | uses: actions/download-artifact@v4 108 | with: 109 | path: /tmp/digests 110 | pattern: digests-${{ matrix.target.idprefix }}* 111 | merge-multiple: true 112 | - name: Create pre-release manifest list and push to registry 113 | working-directory: /tmp/digests 114 | run: | 115 | docker buildx imagetools create --tag "${{ matrix.target.image }}:${{ github.ref_name }}" $(printf '${{ matrix.target.image }}@sha256:%s ' *) 116 | - name: Create release manifest list and push to registry 117 | if: startsWith(github.ref, 'refs/tags/v') 118 | working-directory: /tmp/digests 119 | run: | 120 | TAGS=$(echo "${{ steps.meta.outputs.tags }}" | tr '\n' ' ') 121 | for TAG in $TAGS; do 122 | docker buildx imagetools create --tag "$TAG" $(printf '${{ matrix.target.image }}@sha256:%s ' *); 123 | done 124 | -------------------------------------------------------------------------------- /.github/workflows/hydrun.yaml: -------------------------------------------------------------------------------- 1 | name: hydrun CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: "0 0 * * 0" 8 | 9 | jobs: 10 | build-linux: 11 | runs-on: ${{ matrix.target.runner }} 12 | permissions: 13 | contents: read 14 | strategy: 15 | matrix: 16 | target: 17 | # Tests 18 | - id: test 19 | src: . 20 | os: golang:bookworm 21 | flags: -e '-v /tmp/ccache:/root/.cache/go-build --privileged -v /var/run/docker.sock:/var/run/docker.sock --net host' 22 | cmd: GOFLAGS="-short" ./Hydrunfile test 23 | dst: out/nonexistent 24 | runner: ubuntu-latest 25 | 26 | # Binaries 27 | - id: go.weron 28 | src: . 29 | os: golang:bookworm 30 | flags: -e '-v /tmp/ccache:/root/.cache/go-build --privileged -v /var/run/docker.sock:/var/run/docker.sock --net host' 31 | cmd: ./Hydrunfile go weron 32 | dst: out/* 33 | runner: ubuntu-latest 34 | 35 | steps: 36 | - name: Checkout 37 | uses: actions/checkout@v4 38 | - name: Restore ccache 39 | uses: actions/cache/restore@v4 40 | with: 41 | path: | 42 | /tmp/ccache 43 | key: cache-ccache-${{ matrix.target.id }} 44 | - name: Set up QEMU 45 | uses: docker/setup-qemu-action@v3 46 | - name: Set up Docker Buildx 47 | uses: docker/setup-buildx-action@v3 48 | - name: Set up hydrun 49 | run: | 50 | curl -L -o /tmp/hydrun "https://github.com/pojntfx/hydrun/releases/latest/download/hydrun.linux-$(uname -m)" 51 | sudo install /tmp/hydrun /usr/local/bin 52 | - name: Build with hydrun 53 | working-directory: ${{ matrix.target.src }} 54 | run: hydrun -o ${{ matrix.target.os }} ${{ matrix.target.flags }} "${{ matrix.target.cmd }}" 55 | - name: Fix permissions for output 56 | run: sudo chown -R $USER . 57 | - name: Save ccache 58 | uses: actions/cache/save@v4 59 | with: 60 | path: | 61 | /tmp/ccache 62 | key: cache-ccache-${{ matrix.target.id }} 63 | - name: Upload output 64 | uses: actions/upload-artifact@v4 65 | with: 66 | name: ${{ matrix.target.id }} 67 | path: ${{ matrix.target.dst }} 68 | 69 | publish-linux: 70 | runs-on: ubuntu-latest 71 | permissions: 72 | contents: write 73 | needs: build-linux 74 | 75 | steps: 76 | - name: Checkout 77 | uses: actions/checkout@v4 78 | - name: Download output 79 | uses: actions/download-artifact@v4 80 | with: 81 | path: /tmp/out 82 | - name: Extract branch name 83 | id: extract_branch 84 | run: echo "##[set-output name=branch;]$(echo ${GITHUB_REF#refs/heads/})" 85 | - name: Publish pre-release to GitHub releases 86 | if: ${{ github.ref == 'refs/heads/main' }} 87 | uses: softprops/action-gh-release@v2 88 | with: 89 | tag_name: release-${{ steps.extract_branch.outputs.branch }} 90 | prerelease: true 91 | files: | 92 | /tmp/out/*/* 93 | - name: Publish release to GitHub releases 94 | if: startsWith(github.ref, 'refs/tags/v') 95 | uses: softprops/action-gh-release@v2 96 | with: 97 | prerelease: false 98 | files: | 99 | /tmp/out/*/* 100 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build container 2 | FROM golang:bookworm AS build 3 | 4 | # Setup environment 5 | RUN mkdir -p /data 6 | WORKDIR /data 7 | 8 | # Build the release 9 | COPY . . 10 | RUN make build/weron 11 | 12 | # Extract the release 13 | RUN mkdir -p /out 14 | RUN cp out/weron /out/weron 15 | 16 | # Release container 17 | FROM debian:bookworm 18 | 19 | # Add certificates 20 | RUN apt update 21 | RUN apt install -y ca-certificates 22 | 23 | # Add the release 24 | COPY --from=build /out/weron /usr/local/bin/weron 25 | 26 | CMD /usr/local/bin/weron 27 | -------------------------------------------------------------------------------- /Hydrunfile: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | # Test 6 | if [ "$1" = "test" ]; then 7 | # Install native dependencies 8 | apt update 9 | apt install -y docker.io 10 | 11 | # Configure Git 12 | git config --global --add safe.directory '*' 13 | 14 | # Generate dependencies 15 | make depend 16 | 17 | # Run tests 18 | make test 19 | 20 | exit 0 21 | fi 22 | 23 | # Go 24 | if [ "$1" = "go" ]; then 25 | # Install native dependencies 26 | apt update 27 | apt install -y curl make docker.io 28 | 29 | # Install bagop 30 | curl -L -o /tmp/bagop "https://github.com/pojntfx/bagop/releases/latest/download/bagop.linux-$(uname -m)" 31 | install /tmp/bagop /usr/local/bin 32 | 33 | # Configure Git 34 | git config --global --add safe.directory '*' 35 | 36 | # Generate dependencies 37 | make depend 38 | 39 | # Build 40 | CGO_ENABLED=0 bagop -j "$(nproc)" -b "$2" -x '(android/*|ios/*|plan9/*|aix/*|linux/loong64|freebsd/riscv64|wasip1/wasm)' -p "make build/$2 DST=\$DST" -d out 41 | 42 | exit 0 43 | fi 44 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Public variables 2 | DESTDIR ?= 3 | PREFIX ?= /usr/local 4 | OUTPUT_DIR ?= out 5 | DST ?= 6 | 7 | # Private variables 8 | obj = weron 9 | all: $(addprefix build/,$(obj)) 10 | 11 | # Build 12 | build: $(addprefix build/,$(obj)) 13 | $(addprefix build/,$(obj)): 14 | ifdef DST 15 | go build -o $(DST) ./cmd/$(subst build/,,$@) 16 | else 17 | go build -o $(OUTPUT_DIR)/$(subst build/,,$@) ./cmd/$(subst build/,,$@) 18 | endif 19 | 20 | # Install 21 | install: $(addprefix install/,$(obj)) 22 | $(addprefix install/,$(obj)): 23 | install -D -m 0755 $(OUTPUT_DIR)/$(subst install/,,$@) $(DESTDIR)$(PREFIX)/bin/$(subst install/,,$@) 24 | 25 | # Uninstall 26 | uninstall: $(addprefix uninstall/,$(obj)) 27 | $(addprefix uninstall/,$(obj)): 28 | rm $(DESTDIR)$(PREFIX)/bin/$(subst uninstall/,,$@) 29 | 30 | # Run 31 | $(addprefix run/,$(obj)): 32 | $(subst run/,,$@) $(ARGS) 33 | 34 | # Test 35 | test: 36 | go test -timeout 3600s -parallel $(shell nproc) ./... 37 | 38 | # Benchmark 39 | benchmark: 40 | go test -timeout 3600s -bench=. ./... 41 | 42 | # Clean 43 | clean: 44 | rm -rf out internal/db 45 | docker rm -f weron-postgres weron-redis 46 | 47 | # Dependencies 48 | depend: 49 | docker run -d --name weron-postgres -p 5432:5432 -e POSTGRES_HOST_AUTH_METHOD=trust -e POSTGRES_DB=weron_communities postgres 50 | docker run -d --name weron-redis -p 6379:6379 valkey/valkey 51 | docker exec weron-postgres bash -c 'until pg_isready; do sleep 1; done' 52 | go install github.com/rubenv/sql-migrate/sql-migrate@latest 53 | go install github.com/volatiletech/sqlboiler/v4@latest 54 | go install github.com/jteeuwen/go-bindata/go-bindata@latest 55 | go install github.com/volatiletech/sqlboiler/v4/drivers/sqlboiler-psql@latest 56 | sql-migrate up -env="psql" -config configs/sql-migrate/communities.yaml 57 | go generate ./internal/persisters/psql/... 58 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: bin/weron signaler -------------------------------------------------------------------------------- /cmd/weron/cmd/chat.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "net/url" 10 | "os" 11 | "os/signal" 12 | "strings" 13 | "syscall" 14 | "time" 15 | 16 | "github.com/rs/zerolog/log" 17 | 18 | "github.com/pojntfx/weron/pkg/services" 19 | "github.com/pojntfx/weron/pkg/wrtcchat" 20 | "github.com/pojntfx/weron/pkg/wrtcconn" 21 | "github.com/spf13/cobra" 22 | "github.com/spf13/viper" 23 | ) 24 | 25 | const ( 26 | timeoutFlag = "timeout" 27 | keyFlag = "key" 28 | namesFlag = "names" 29 | channelsFlag = "channels" 30 | idChannelFlag = "id-channel" 31 | iceFlag = "ice" 32 | forceRelayFlag = "force-relay" 33 | kicksFlag = "kicks" 34 | ) 35 | 36 | var ( 37 | errMissingKey = errors.New("missing key") 38 | errMissingUsernames = errors.New("missing usernames") 39 | ) 40 | 41 | func addInterruptHandler(cancel func(), closer io.Closer, before func()) { 42 | s := make(chan os.Signal) 43 | signal.Notify(s, os.Interrupt, syscall.SIGTERM) 44 | go func() { 45 | <-s 46 | 47 | if before != nil { 48 | before() 49 | } 50 | 51 | log.Debug().Msg("Gracefully shutting down") 52 | 53 | go func() { 54 | <-s 55 | 56 | log.Debug().Msg("Forcing shutdown") 57 | 58 | cancel() 59 | 60 | os.Exit(1) 61 | }() 62 | 63 | if err := closer.Close(); err != nil { 64 | panic(err) 65 | } 66 | 67 | cancel() 68 | }() 69 | } 70 | 71 | var chatCmd = &cobra.Command{ 72 | Use: "chat", 73 | Aliases: []string{"cht", "c"}, 74 | Short: "Chat over the overlay network", 75 | RunE: func(cmd *cobra.Command, args []string) error { 76 | if err := viper.BindPFlags(cmd.PersistentFlags()); err != nil { 77 | return err 78 | } 79 | 80 | ctx, cancel := context.WithCancel(context.Background()) 81 | defer cancel() 82 | 83 | if strings.TrimSpace(viper.GetString(communityFlag)) == "" { 84 | return errMissingCommunity 85 | } 86 | 87 | if strings.TrimSpace(viper.GetString(passwordFlag)) == "" { 88 | return errMissingPassword 89 | } 90 | 91 | if strings.TrimSpace(viper.GetString(keyFlag)) == "" { 92 | return errMissingKey 93 | } 94 | 95 | if len(viper.GetStringSlice(namesFlag)) <= 0 { 96 | return errMissingUsernames 97 | } 98 | 99 | fmt.Printf(".%v", viper.GetString(raddrFlag)) 100 | 101 | u, err := url.Parse(viper.GetString(raddrFlag)) 102 | if err != nil { 103 | return err 104 | } 105 | 106 | q := u.Query() 107 | q.Set("community", viper.GetString(communityFlag)) 108 | q.Set("password", viper.GetString(passwordFlag)) 109 | u.RawQuery = q.Encode() 110 | 111 | id := "" 112 | adapter := wrtcchat.NewAdapter( 113 | u.String(), 114 | viper.GetString(keyFlag), 115 | viper.GetStringSlice(iceFlag), 116 | &wrtcchat.AdapterConfig{ 117 | OnSignalerConnect: func(s string) { 118 | id = s 119 | 120 | fmt.Printf("\n%v!\n", id) 121 | }, 122 | OnPeerConnect: func(peerID, channelID string) { 123 | fmt.Printf("\r\u001b[0K+%v@%v\n", peerID, channelID) 124 | fmt.Printf("\r\u001b[0K%v> ", id) 125 | }, 126 | OnPeerDisconnected: func(peerID, channelID string) { 127 | fmt.Printf("\r\u001b[0K-%v@%v\n", peerID, channelID) 128 | fmt.Printf("\r\u001b[0K%v> ", id) 129 | }, 130 | OnMessage: func(m wrtcchat.Message) { 131 | fmt.Printf("\r\u001b[0K%v@%v: %s\n", m.PeerID, m.ChannelID, m.Body) 132 | fmt.Printf("\r\u001b[0K%v> ", id) 133 | }, 134 | Channels: viper.GetStringSlice(channelsFlag), 135 | NamedAdapterConfig: &wrtcconn.NamedAdapterConfig{ 136 | AdapterConfig: &wrtcconn.AdapterConfig{ 137 | Timeout: viper.GetDuration(timeoutFlag), 138 | ForceRelay: viper.GetBool(forceRelayFlag), 139 | }, 140 | IDChannel: viper.GetString(idChannelFlag), 141 | Names: viper.GetStringSlice(namesFlag), 142 | Kicks: viper.GetDuration(kicksFlag), 143 | }, 144 | }, 145 | ctx, 146 | ) 147 | 148 | if err := adapter.Open(); err != nil { 149 | return err 150 | } 151 | addInterruptHandler(cancel, adapter, nil) 152 | 153 | go func() { 154 | reader := bufio.NewScanner(os.Stdin) 155 | 156 | for reader.Scan() { 157 | adapter.SendMessage([]byte(reader.Text() + "\n")) 158 | fmt.Printf("\r\u001b[0K%v> ", id) 159 | } 160 | }() 161 | 162 | return adapter.Wait() 163 | }, 164 | } 165 | 166 | func init() { 167 | chatCmd.PersistentFlags().String(raddrFlag, "wss://weron.up.railway.app/", "Remote address") 168 | chatCmd.PersistentFlags().Duration(timeoutFlag, time.Second*10, "Time to wait for connections") 169 | chatCmd.PersistentFlags().String(communityFlag, "", "ID of community to join") 170 | chatCmd.PersistentFlags().String(passwordFlag, "", "Password for community") 171 | chatCmd.PersistentFlags().String(keyFlag, "", "Encryption key for community") 172 | chatCmd.PersistentFlags().StringSlice(namesFlag, []string{}, "Comma-separated list of names to try and claim one from") 173 | chatCmd.PersistentFlags().StringSlice(channelsFlag, []string{services.ChatPrimary}, "Comma-separated list of channels in community to join") 174 | chatCmd.PersistentFlags().String(idChannelFlag, services.ChatID, "Channel to use to negotiate names") 175 | chatCmd.PersistentFlags().StringSlice(iceFlag, []string{"stun:stun.l.google.com:19302"}, "Comma-separated list of STUN servers (in format stun:host:port) and TURN servers to use (in format username:credential@turn:host:port) (i.e. username:credential@turn:global.turn.twilio.com:3478?transport=tcp)") 176 | chatCmd.PersistentFlags().Bool(forceRelayFlag, false, "Force usage of TURN servers") 177 | chatCmd.PersistentFlags().Duration(kicksFlag, time.Second*5, "Time to wait for kicks") 178 | 179 | viper.AutomaticEnv() 180 | 181 | rootCmd.AddCommand(chatCmd) 182 | } 183 | -------------------------------------------------------------------------------- /cmd/weron/cmd/manager_create.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "encoding/csv" 6 | "errors" 7 | "fmt" 8 | "os" 9 | "strings" 10 | 11 | "github.com/pojntfx/weron/pkg/wrtcmgr" 12 | "github.com/spf13/cobra" 13 | "github.com/spf13/viper" 14 | ) 15 | 16 | var ( 17 | errMissingCommunity = errors.New("missing community") 18 | errMissingPassword = errors.New("missing password") 19 | ) 20 | 21 | const ( 22 | communityFlag = "community" 23 | passwordFlag = "password" 24 | ) 25 | 26 | var managerCreateCmd = &cobra.Command{ 27 | Use: "create", 28 | Aliases: []string{"ctr", "c", "mk"}, 29 | Short: "Create a persistent community", 30 | PreRunE: validateRemoteFlags, 31 | RunE: func(cmd *cobra.Command, args []string) error { 32 | if err := viper.BindPFlags(cmd.PersistentFlags()); err != nil { 33 | return err 34 | } 35 | 36 | if strings.TrimSpace(viper.GetString(apiPasswordFlag)) == "" { 37 | return errMissingAPIPassword 38 | } 39 | 40 | if strings.TrimSpace(viper.GetString(apiUsernameFlag)) == "" { 41 | return errMissingAPIUsername 42 | } 43 | 44 | if strings.TrimSpace(viper.GetString(communityFlag)) == "" { 45 | return errMissingCommunity 46 | } 47 | 48 | if strings.TrimSpace(viper.GetString(passwordFlag)) == "" { 49 | return errMissingPassword 50 | } 51 | 52 | ctx, cancel := context.WithCancel(context.Background()) 53 | defer cancel() 54 | 55 | manager := wrtcmgr.NewManager( 56 | viper.GetString(raddrFlag), 57 | viper.GetString(apiUsernameFlag), 58 | viper.GetString(apiPasswordFlag), 59 | ctx, 60 | ) 61 | 62 | c, err := manager.CreatePersistentCommunity(viper.GetString(communityFlag), viper.GetString(passwordFlag)) 63 | if err != nil { 64 | return err 65 | } 66 | 67 | w := csv.NewWriter(os.Stdout) 68 | defer w.Flush() 69 | 70 | if err := w.Write([]string{"id", "clients", "persistent"}); err != nil { 71 | return err 72 | } 73 | 74 | return w.Write([]string{c.ID, fmt.Sprintf("%v", c.Clients), fmt.Sprintf("%v", c.Persistent)}) 75 | }, 76 | } 77 | 78 | func init() { 79 | addRemoteFlags(managerCreateCmd.PersistentFlags()) 80 | managerCreateCmd.PersistentFlags().String(communityFlag, "", "ID of community to create") 81 | managerCreateCmd.PersistentFlags().String(passwordFlag, "", "Password for community") 82 | 83 | viper.AutomaticEnv() 84 | 85 | managerCmd.AddCommand(managerCreateCmd) 86 | } 87 | -------------------------------------------------------------------------------- /cmd/weron/cmd/manager_delete.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | 7 | "github.com/pojntfx/weron/pkg/wrtcmgr" 8 | "github.com/spf13/cobra" 9 | "github.com/spf13/viper" 10 | ) 11 | 12 | var managerDeleteCmd = &cobra.Command{ 13 | Use: "delete", 14 | Aliases: []string{"del", "d", "rm"}, 15 | Short: "Delete a persistent or ephemeral community", 16 | PreRunE: validateRemoteFlags, 17 | RunE: func(cmd *cobra.Command, args []string) error { 18 | if err := viper.BindPFlags(cmd.PersistentFlags()); err != nil { 19 | return err 20 | } 21 | 22 | if strings.TrimSpace(viper.GetString(apiPasswordFlag)) == "" { 23 | return errMissingAPIPassword 24 | } 25 | 26 | if strings.TrimSpace(viper.GetString(apiUsernameFlag)) == "" { 27 | return errMissingAPIUsername 28 | } 29 | 30 | if strings.TrimSpace(viper.GetString(communityFlag)) == "" { 31 | return errMissingCommunity 32 | } 33 | 34 | ctx, cancel := context.WithCancel(context.Background()) 35 | defer cancel() 36 | 37 | manager := wrtcmgr.NewManager( 38 | viper.GetString(raddrFlag), 39 | viper.GetString(apiUsernameFlag), 40 | viper.GetString(apiPasswordFlag), 41 | ctx, 42 | ) 43 | 44 | if err := manager.DeleteCommunity(viper.GetString(communityFlag)); err != nil { 45 | return err 46 | } 47 | 48 | return nil 49 | }, 50 | } 51 | 52 | func init() { 53 | addRemoteFlags(managerDeleteCmd.PersistentFlags()) 54 | managerDeleteCmd.PersistentFlags().String(communityFlag, "", "ID of community to create") 55 | 56 | viper.AutomaticEnv() 57 | 58 | managerCmd.AddCommand(managerDeleteCmd) 59 | } 60 | -------------------------------------------------------------------------------- /cmd/weron/cmd/manager_list.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "encoding/csv" 6 | "errors" 7 | "fmt" 8 | "os" 9 | "strings" 10 | 11 | "github.com/rs/zerolog/log" 12 | 13 | "github.com/pojntfx/weron/pkg/wrtcmgr" 14 | "github.com/spf13/cobra" 15 | "github.com/spf13/pflag" 16 | "github.com/spf13/viper" 17 | ) 18 | 19 | const ( 20 | raddrFlag = "raddr" 21 | ) 22 | 23 | var ( 24 | errMissingAPIPassword = errors.New("missing API password") 25 | errMissingAPIUsername = errors.New("missing API username") 26 | ) 27 | 28 | var managerListCmd = &cobra.Command{ 29 | Use: "list", 30 | Aliases: []string{"lis", "l", "ls"}, 31 | Short: "List persistent and ephemeral communities", 32 | PreRunE: validateRemoteFlags, 33 | RunE: func(cmd *cobra.Command, args []string) error { 34 | if err := viper.BindPFlags(cmd.PersistentFlags()); err != nil { 35 | return err 36 | } 37 | 38 | if strings.TrimSpace(viper.GetString(apiPasswordFlag)) == "" { 39 | return errMissingAPIPassword 40 | } 41 | 42 | if strings.TrimSpace(viper.GetString(apiUsernameFlag)) == "" { 43 | return errMissingAPIUsername 44 | } 45 | 46 | ctx, cancel := context.WithCancel(context.Background()) 47 | defer cancel() 48 | 49 | manager := wrtcmgr.NewManager( 50 | viper.GetString(raddrFlag), 51 | viper.GetString(apiUsernameFlag), 52 | viper.GetString(apiPasswordFlag), 53 | ctx, 54 | ) 55 | 56 | c, err := manager.ListCommunities() 57 | if err != nil { 58 | return err 59 | } 60 | 61 | w := csv.NewWriter(os.Stdout) 62 | defer w.Flush() 63 | 64 | if err := w.Write([]string{"id", "clients", "persistent"}); err != nil { 65 | return err 66 | } 67 | 68 | for _, community := range c { 69 | if err := w.Write([]string{community.ID, fmt.Sprintf("%v", community.Clients), fmt.Sprintf("%v", community.Persistent)}); err != nil { 70 | return err 71 | } 72 | } 73 | 74 | return nil 75 | }, 76 | } 77 | 78 | func init() { 79 | addRemoteFlags(managerListCmd.PersistentFlags()) 80 | 81 | viper.AutomaticEnv() 82 | 83 | managerCmd.AddCommand(managerListCmd) 84 | } 85 | 86 | func addRemoteFlags(f *pflag.FlagSet) { 87 | f.String(apiUsernameFlag, "admin", "Username for the management API (can also be set using the API_USERNAME env variable). Ignored if any of the OIDC parameters are set.") 88 | f.String(apiPasswordFlag, "", "Password for the management API (can also be set using the API_PASSWORD env variable). Ignored if any of the OIDC parameters are set.") 89 | f.String(raddrFlag, "https://weron.up.railway.app/", "Remote address") 90 | } 91 | 92 | func validateRemoteFlags(cmd *cobra.Command, args []string) error { 93 | if u := os.Getenv("API_USERNAME"); u != "" { 94 | log.Debug().Msg("Using username from API_USERNAME env variable") 95 | 96 | viper.Set(apiUsernameFlag, u) 97 | } 98 | 99 | if u := os.Getenv("API_PASSWORD"); u != "" { 100 | log.Debug().Msg("Using password from API_PASSWORD env variable") 101 | 102 | viper.Set(apiPasswordFlag, u) 103 | } 104 | 105 | return viper.BindPFlags(cmd.PersistentFlags()) 106 | } 107 | -------------------------------------------------------------------------------- /cmd/weron/cmd/manager_root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | "github.com/spf13/viper" 6 | ) 7 | 8 | var managerCmd = &cobra.Command{ 9 | Use: "manager", 10 | Aliases: []string{"mgr", "m"}, 11 | Short: "Manage a signaling server", 12 | } 13 | 14 | func init() { 15 | viper.AutomaticEnv() 16 | 17 | rootCmd.AddCommand(managerCmd) 18 | } 19 | -------------------------------------------------------------------------------- /cmd/weron/cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/rs/zerolog" 7 | "github.com/spf13/cobra" 8 | "github.com/spf13/viper" 9 | "github.com/volatiletech/sqlboiler/v4/boil" 10 | ) 11 | 12 | const ( 13 | verboseFlag = "verbose" 14 | ) 15 | 16 | var rootCmd = &cobra.Command{ 17 | Use: "weron", 18 | Short: "WebRTC Overlay Networks", 19 | Long: `Lean, fast & secure overlay networks based on WebRTC. 20 | 21 | 22 | Find more information at: 23 | https://github.com/pojntfx/weron`, 24 | PersistentPreRunE: func(cmd *cobra.Command, args []string) error { 25 | viper.SetEnvPrefix("weron") 26 | viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_", ".", "_")) 27 | 28 | if err := viper.BindPFlags(cmd.PersistentFlags()); err != nil { 29 | return err 30 | } 31 | 32 | verbose := viper.GetInt(verboseFlag) 33 | if verbose > 5 { 34 | boil.DebugMode = true 35 | } 36 | 37 | switch verbose { 38 | case 0: 39 | zerolog.SetGlobalLevel(zerolog.Disabled) 40 | case 1: 41 | zerolog.SetGlobalLevel(zerolog.PanicLevel) 42 | case 2: 43 | zerolog.SetGlobalLevel(zerolog.FatalLevel) 44 | case 3: 45 | zerolog.SetGlobalLevel(zerolog.ErrorLevel) 46 | case 4: 47 | zerolog.SetGlobalLevel(zerolog.WarnLevel) 48 | case 5: 49 | zerolog.SetGlobalLevel(zerolog.InfoLevel) 50 | case 6: 51 | zerolog.SetGlobalLevel(zerolog.DebugLevel) 52 | default: 53 | zerolog.SetGlobalLevel(zerolog.TraceLevel) 54 | } 55 | 56 | return nil 57 | }, 58 | } 59 | 60 | func Execute() error { 61 | rootCmd.PersistentFlags().IntP(verboseFlag, "v", 5, "Verbosity level (0 is disabled, default is info, 7 is trace)") 62 | 63 | if err := viper.BindPFlags(rootCmd.PersistentFlags()); err != nil { 64 | return err 65 | } 66 | 67 | viper.AutomaticEnv() 68 | 69 | return rootCmd.Execute() 70 | } 71 | -------------------------------------------------------------------------------- /cmd/weron/cmd/signaler.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "net" 6 | "os" 7 | "strconv" 8 | "time" 9 | 10 | "github.com/rs/zerolog/log" 11 | 12 | "github.com/pojntfx/weron/pkg/wrtcsgl" 13 | "github.com/spf13/cobra" 14 | "github.com/spf13/viper" 15 | ) 16 | 17 | const ( 18 | laddrFlag = "laddr" 19 | heartbeatFlag = "heartbeat" 20 | postgresURLFlag = "postgres-url" 21 | redisURLFlag = "redis-url" 22 | cleanupFlag = "cleanup" 23 | ephemeralCommunitiesFlag = "ephemeral-communities" 24 | apiUsernameFlag = "api-username" 25 | apiPasswordFlag = "api-password" 26 | oidcIssuerFlag = "oidc-issuer" 27 | oidcClientIDFlag = "oidc-client-id" 28 | ) 29 | 30 | var signalerCmd = &cobra.Command{ 31 | Use: "signaler", 32 | Aliases: []string{"sgl", "s"}, 33 | Short: "Start a signaling server", 34 | PreRunE: func(cmd *cobra.Command, args []string) error { 35 | if u := os.Getenv("API_USERNAME"); u != "" { 36 | log.Debug().Msg("Using username from API_USERNAME env variable") 37 | 38 | viper.Set(apiUsernameFlag, u) 39 | } 40 | 41 | if u := os.Getenv("API_PASSWORD"); u != "" { 42 | log.Debug().Msg("Using password from API_PASSWORD env variable") 43 | 44 | viper.Set(apiPasswordFlag, u) 45 | } 46 | 47 | if u := os.Getenv("DATABASE_URL"); u != "" { 48 | log.Debug().Msg("Using database URL from DATABASE_URL env variable") 49 | 50 | viper.Set(postgresURLFlag, u) 51 | } 52 | 53 | if u := os.Getenv("REDIS_URL"); u != "" { 54 | log.Debug().Msg("Using broker URL from REDIS_URL env variable") 55 | 56 | viper.Set(redisURLFlag, u) 57 | } 58 | 59 | if u := os.Getenv("OIDC_ISSUER"); u != "" { 60 | log.Debug().Msg("Using OIDC issuer from OIDC_ISSUER env variable") 61 | 62 | viper.Set(oidcIssuerFlag, u) 63 | } 64 | 65 | if u := os.Getenv("OIDC_CLIENT_ID"); u != "" { 66 | log.Debug().Msg("Using OIDC client ID from OIDC_CLIENT_ID env variable") 67 | 68 | viper.Set(oidcClientIDFlag, u) 69 | } 70 | 71 | return viper.BindPFlags(cmd.PersistentFlags()) 72 | }, 73 | RunE: func(cmd *cobra.Command, args []string) error { 74 | if err := viper.BindPFlags(cmd.PersistentFlags()); err != nil { 75 | return err 76 | } 77 | 78 | ctx, cancel := context.WithCancel(context.Background()) 79 | defer cancel() 80 | 81 | addr, err := net.ResolveTCPAddr("tcp", viper.GetString(laddrFlag)) 82 | if err != nil { 83 | return err 84 | } 85 | 86 | if port := os.Getenv("PORT"); port != "" { 87 | log.Debug().Msg("Using port from PORT env variable") 88 | 89 | p, err := strconv.Atoi(port) 90 | if err != nil { 91 | return err 92 | } 93 | 94 | addr.Port = p 95 | } 96 | 97 | signaler := wrtcsgl.NewSignaler( 98 | addr.String(), 99 | viper.GetString(postgresURLFlag), 100 | viper.GetString(redisURLFlag), 101 | &wrtcsgl.SignalerConfig{ 102 | Heartbeat: viper.GetDuration(heartbeatFlag), 103 | Cleanup: viper.GetBool(cleanupFlag), 104 | EphemeralCommunities: viper.GetBool(ephemeralCommunitiesFlag), 105 | APIUsername: viper.GetString(apiUsernameFlag), 106 | APIPassword: viper.GetString(apiPasswordFlag), 107 | OIDCIssuer: viper.GetString(oidcIssuerFlag), 108 | OIDCClientID: viper.GetString(oidcClientIDFlag), 109 | OnConnect: func(raddr, community string) { 110 | log.Info(). 111 | Str("address", raddr). 112 | Str("community", community). 113 | Msg("Connected to client") 114 | }, 115 | OnDisconnect: func(raddr, community string, err interface{}) { 116 | log.Info(). 117 | Str("address", raddr). 118 | Str("community", community). 119 | Msg("Disconnected from client") 120 | }, 121 | }, 122 | ctx, 123 | ) 124 | 125 | if err := signaler.Open(); err != nil { 126 | return err 127 | } 128 | addInterruptHandler(cancel, signaler, nil) 129 | 130 | log.Info(). 131 | Str("address", addr.String()). 132 | Msg("Listening") 133 | 134 | return signaler.Wait() 135 | }, 136 | } 137 | 138 | func init() { 139 | signalerCmd.PersistentFlags().String(laddrFlag, ":1337", "Listening address (can also be set using the PORT env variable)") 140 | signalerCmd.PersistentFlags().Duration(heartbeatFlag, time.Second*10, "Time to wait for heartbeats") 141 | signalerCmd.PersistentFlags().String(postgresURLFlag, "", "URL of PostgreSQL database to use (i.e. postgres://myuser:mypassword@myhost:myport/mydatabase) (can also be set using the DATABASE_URL env variable). If empty, a in-memory database will be used.") 142 | signalerCmd.PersistentFlags().String(redisURLFlag, "", "URL of Redis database to use (i.e. redis://myuser:mypassword@myhost:myport/1) (can also be set using the REDIS_URL env variable). If empty, a in-process broker will be used.") 143 | signalerCmd.PersistentFlags().Bool(cleanupFlag, false, "(Warning: Only enable this after stopping all other servers accessing the database!) Remove all ephemeral communities from database and reset client counts before starting") 144 | signalerCmd.PersistentFlags().Bool(ephemeralCommunitiesFlag, true, "Enable the creation of ephemeral communities") 145 | signalerCmd.PersistentFlags().String(apiUsernameFlag, "admin", "Username for the management API (can also be set using the API_USERNAME env variable). Ignored if any of the OIDC parameters are set.") 146 | signalerCmd.PersistentFlags().String(apiPasswordFlag, "", "Password for the management API (can also be set using the API_PASSWORD env variable). Ignored if any of the OIDC parameters are set.") 147 | signalerCmd.PersistentFlags().String(oidcIssuerFlag, "", "OIDC Issuer (i.e. https://pojntfx.eu.auth0.com/) (can also be set using the OIDC_ISSUER env variable)") 148 | signalerCmd.PersistentFlags().String(oidcClientIDFlag, "", "OIDC Client ID (i.e. myoidcclientid) (can also be set using the OIDC_CLIENT_ID env variable)") 149 | 150 | viper.AutomaticEnv() 151 | 152 | rootCmd.AddCommand(signalerCmd) 153 | } 154 | -------------------------------------------------------------------------------- /cmd/weron/cmd/utility_latency.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/url" 7 | "strings" 8 | "time" 9 | 10 | "github.com/rs/zerolog/log" 11 | 12 | "github.com/pojntfx/weron/pkg/wrtcconn" 13 | "github.com/pojntfx/weron/pkg/wrtcltc" 14 | "github.com/spf13/cobra" 15 | "github.com/spf13/viper" 16 | "github.com/teivah/broadcast" 17 | ) 18 | 19 | const ( 20 | pauseFlag = "pause" 21 | ) 22 | 23 | var utilityLatencyCommand = &cobra.Command{ 24 | Use: "latency", 25 | Aliases: []string{"ltc", "l"}, 26 | Short: "Measure the latency of the overlay network", 27 | RunE: func(cmd *cobra.Command, args []string) error { 28 | if err := viper.BindPFlags(cmd.PersistentFlags()); err != nil { 29 | return err 30 | } 31 | 32 | ctx, cancel := context.WithCancel(context.Background()) 33 | defer cancel() 34 | 35 | if strings.TrimSpace(viper.GetString(communityFlag)) == "" { 36 | return errMissingCommunity 37 | } 38 | 39 | if strings.TrimSpace(viper.GetString(passwordFlag)) == "" { 40 | return errMissingPassword 41 | } 42 | 43 | if strings.TrimSpace(viper.GetString(keyFlag)) == "" { 44 | return errMissingKey 45 | } 46 | 47 | fmt.Printf("\r\u001b[0K.%v\n", viper.GetString(raddrFlag)) 48 | 49 | u, err := url.Parse(viper.GetString(raddrFlag)) 50 | if err != nil { 51 | return err 52 | } 53 | 54 | q := u.Query() 55 | q.Set("community", viper.GetString(communityFlag)) 56 | q.Set("password", viper.GetString(passwordFlag)) 57 | u.RawQuery = q.Encode() 58 | 59 | adapter := wrtcltc.NewAdapter( 60 | u.String(), 61 | viper.GetString(keyFlag), 62 | viper.GetStringSlice(iceFlag), 63 | &wrtcltc.AdapterConfig{ 64 | OnSignalerConnect: func(s string) { 65 | log.Info(). 66 | Str("id", s). 67 | Msg("Connected to signaler") 68 | }, 69 | OnPeerConnect: func(s string) { 70 | log.Info(). 71 | Str("id", s). 72 | Msg("Connected to peer") 73 | }, 74 | OnPeerDisconnected: func(s string) { 75 | log.Info(). 76 | Str("id", s). 77 | Msg("Disconnected from peer") 78 | }, 79 | AdapterConfig: &wrtcconn.AdapterConfig{ 80 | Timeout: viper.GetDuration(timeoutFlag), 81 | ForceRelay: viper.GetBool(forceRelayFlag), 82 | }, 83 | Server: viper.GetBool(serverFlag), 84 | PacketLength: viper.GetInt(packetLengthFlag), 85 | Pause: viper.GetDuration(pauseFlag), 86 | }, 87 | ctx, 88 | ) 89 | 90 | acked := false 91 | totaled := broadcast.NewRelay[struct{}]() 92 | 93 | go func() { 94 | for { 95 | select { 96 | case <-ctx.Done(): 97 | if err := ctx.Err(); err != context.Canceled { 98 | panic(err) 99 | } 100 | 101 | return 102 | case ack := <-adapter.Acknowledgements(): 103 | fmt.Printf("%v B written and acknowledged in %v\n", ack.BytesWritten, ack.Latency) 104 | 105 | acked = true 106 | case totals := <-adapter.Totals(): 107 | fmt.Printf("Average latency: %v (%v packets written) Min: %v Max: %v\n", totals.LatencyAverage, totals.PacketsWritten, totals.LatencyMin, totals.LatencyMax) 108 | 109 | totaled.Broadcast(struct{}{}) 110 | } 111 | } 112 | }() 113 | 114 | log.Info(). 115 | Str("addr", viper.GetString(raddrFlag)). 116 | Msg("Connecting to signaler") 117 | 118 | if err := adapter.Open(); err != nil { 119 | return err 120 | } 121 | addInterruptHandler( 122 | cancel, 123 | adapter, 124 | func() { 125 | if !viper.GetBool(serverFlag) && acked { 126 | l := totaled.Listener(0) 127 | defer l.Close() 128 | 129 | adapter.GatherTotals() 130 | 131 | <-l.Ch() 132 | } 133 | }, 134 | ) 135 | 136 | return adapter.Wait() 137 | }, 138 | } 139 | 140 | func init() { 141 | utilityLatencyCommand.PersistentFlags().String(raddrFlag, "wss://weron.up.railway.app/", "Remote address") 142 | utilityLatencyCommand.PersistentFlags().Duration(timeoutFlag, time.Second*10, "Time to wait for connections") 143 | utilityLatencyCommand.PersistentFlags().String(communityFlag, "", "ID of community to join") 144 | utilityLatencyCommand.PersistentFlags().String(passwordFlag, "", "Password for community") 145 | utilityLatencyCommand.PersistentFlags().String(keyFlag, "", "Encryption key for community") 146 | utilityLatencyCommand.PersistentFlags().StringSlice(iceFlag, []string{"stun:stun.l.google.com:19302"}, "Comma-separated list of STUN servers (in format stun:host:port) and TURN servers to use (in format username:credential@turn:host:port) (i.e. username:credential@turn:global.turn.twilio.com:3478?transport=tcp)") 147 | utilityLatencyCommand.PersistentFlags().Bool(forceRelayFlag, false, "Force usage of TURN servers") 148 | utilityLatencyCommand.PersistentFlags().Bool(serverFlag, false, "Act as a server") 149 | utilityLatencyCommand.PersistentFlags().Int(packetLengthFlag, 128, "Size of packet to send and acknowledge") 150 | utilityLatencyCommand.PersistentFlags().Duration(pauseFlag, time.Second*1, "Time to wait before sending next packet") 151 | 152 | viper.AutomaticEnv() 153 | 154 | utilityCmd.AddCommand(utilityLatencyCommand) 155 | } 156 | -------------------------------------------------------------------------------- /cmd/weron/cmd/utility_root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | "github.com/spf13/viper" 6 | ) 7 | 8 | var utilityCmd = &cobra.Command{ 9 | Use: "utility", 10 | Aliases: []string{"uti", "u", "util"}, 11 | Short: "Utilities for overlay networks", 12 | } 13 | 14 | func init() { 15 | viper.AutomaticEnv() 16 | 17 | rootCmd.AddCommand(utilityCmd) 18 | } 19 | -------------------------------------------------------------------------------- /cmd/weron/cmd/utility_throughput.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/url" 7 | "strings" 8 | "time" 9 | 10 | "github.com/rs/zerolog/log" 11 | 12 | "github.com/pojntfx/weron/pkg/wrtcconn" 13 | "github.com/pojntfx/weron/pkg/wrtcthr" 14 | "github.com/spf13/cobra" 15 | "github.com/spf13/viper" 16 | "github.com/teivah/broadcast" 17 | ) 18 | 19 | const ( 20 | serverFlag = "server" 21 | packetLengthFlag = "packet-length" 22 | packetCountFlag = "packet-count" 23 | ) 24 | 25 | var utilityThroughputCmd = &cobra.Command{ 26 | Use: "throughput", 27 | Aliases: []string{"thr", "t"}, 28 | Short: "Measure the throughput of the overlay network", 29 | RunE: func(cmd *cobra.Command, args []string) error { 30 | if err := viper.BindPFlags(cmd.PersistentFlags()); err != nil { 31 | return err 32 | } 33 | 34 | ctx, cancel := context.WithCancel(context.Background()) 35 | defer cancel() 36 | 37 | if strings.TrimSpace(viper.GetString(communityFlag)) == "" { 38 | return errMissingCommunity 39 | } 40 | 41 | if strings.TrimSpace(viper.GetString(passwordFlag)) == "" { 42 | return errMissingPassword 43 | } 44 | 45 | if strings.TrimSpace(viper.GetString(keyFlag)) == "" { 46 | return errMissingKey 47 | } 48 | 49 | fmt.Printf("\r\u001b[0K.%v\n", viper.GetString(raddrFlag)) 50 | 51 | u, err := url.Parse(viper.GetString(raddrFlag)) 52 | if err != nil { 53 | return err 54 | } 55 | 56 | q := u.Query() 57 | q.Set("community", viper.GetString(communityFlag)) 58 | q.Set("password", viper.GetString(passwordFlag)) 59 | u.RawQuery = q.Encode() 60 | 61 | adapter := wrtcthr.NewAdapter( 62 | u.String(), 63 | viper.GetString(keyFlag), 64 | viper.GetStringSlice(iceFlag), 65 | &wrtcthr.AdapterConfig{ 66 | OnSignalerConnect: func(s string) { 67 | log.Info(). 68 | Str("id", s). 69 | Msg("Connected to signaler") 70 | }, 71 | OnPeerConnect: func(s string) { 72 | log.Info(). 73 | Str("id", s). 74 | Msg("Connected to peer") 75 | }, 76 | OnPeerDisconnected: func(s string) { 77 | log.Info(). 78 | Str("id", s). 79 | Msg("Disconnected from peer") 80 | }, 81 | AdapterConfig: &wrtcconn.AdapterConfig{ 82 | Timeout: viper.GetDuration(timeoutFlag), 83 | ForceRelay: viper.GetBool(forceRelayFlag), 84 | }, 85 | Server: viper.GetBool(serverFlag), 86 | PacketLength: viper.GetInt(packetLengthFlag), 87 | PacketCount: viper.GetInt(packetCountFlag), 88 | }, 89 | ctx, 90 | ) 91 | 92 | acked := false 93 | totaled := broadcast.NewRelay[struct{}]() 94 | 95 | go func() { 96 | for { 97 | select { 98 | case <-ctx.Done(): 99 | if err := ctx.Err(); err != context.Canceled { 100 | panic(err) 101 | } 102 | 103 | return 104 | case ack := <-adapter.Acknowledgements(): 105 | fmt.Printf( 106 | "%.3f MB/s (%.3f Mb/s) (%v MB read in %v)\n", 107 | ack.ThroughputMB, 108 | ack.ThroughputMb, 109 | ack.TransferredMB, 110 | ack.TransferredDuration, 111 | ) 112 | 113 | acked = true 114 | case totals := <-adapter.Totals(): 115 | fmt.Printf( 116 | "Average throughput: %.3f MB/s (%.3f Mb/s) (%v MB written in %v) Min: %.3f MB/s Max: %.3f MB/s\n", 117 | totals.ThroughputAverageMB, 118 | totals.ThroughputAverageMb, 119 | totals.TransferredMB, 120 | totals.TransferredDuration, 121 | totals.ThroughputMin, 122 | totals.ThroughputMax, 123 | ) 124 | 125 | totaled.Broadcast(struct{}{}) 126 | } 127 | } 128 | }() 129 | 130 | log.Info(). 131 | Str("addr", viper.GetString(raddrFlag)). 132 | Msg("Connecting to signaler") 133 | 134 | if err := adapter.Open(); err != nil { 135 | return err 136 | } 137 | addInterruptHandler( 138 | cancel, 139 | adapter, 140 | func() { 141 | if !viper.GetBool(serverFlag) && acked { 142 | l := totaled.Listener(0) 143 | defer l.Close() 144 | 145 | adapter.GatherTotals() 146 | 147 | <-l.Ch() 148 | } 149 | }, 150 | ) 151 | 152 | return adapter.Wait() 153 | }, 154 | } 155 | 156 | func init() { 157 | utilityThroughputCmd.PersistentFlags().String(raddrFlag, "wss://weron.up.railway.app/", "Remote address") 158 | utilityThroughputCmd.PersistentFlags().Duration(timeoutFlag, time.Second*10, "Time to wait for connections") 159 | utilityThroughputCmd.PersistentFlags().String(communityFlag, "", "ID of community to join") 160 | utilityThroughputCmd.PersistentFlags().String(passwordFlag, "", "Password for community") 161 | utilityThroughputCmd.PersistentFlags().String(keyFlag, "", "Encryption key for community") 162 | utilityThroughputCmd.PersistentFlags().StringSlice(iceFlag, []string{"stun:stun.l.google.com:19302"}, "Comma-separated list of STUN servers (in format stun:host:port) and TURN servers to use (in format username:credential@turn:host:port) (i.e. username:credential@turn:global.turn.twilio.com:3478?transport=tcp)") 163 | utilityThroughputCmd.PersistentFlags().Bool(forceRelayFlag, false, "Force usage of TURN servers") 164 | utilityThroughputCmd.PersistentFlags().Bool(serverFlag, false, "Act as a server") 165 | utilityThroughputCmd.PersistentFlags().Int(packetLengthFlag, 50000, "Size of packet to send") 166 | utilityThroughputCmd.PersistentFlags().Int(packetCountFlag, 1000, "Amount of packets to send before waiting for acknowledgement") 167 | 168 | viper.AutomaticEnv() 169 | 170 | utilityCmd.AddCommand(utilityThroughputCmd) 171 | } 172 | -------------------------------------------------------------------------------- /cmd/weron/cmd/vpn_ethernet.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "net/url" 6 | "runtime" 7 | "strings" 8 | "time" 9 | 10 | "github.com/rs/zerolog/log" 11 | 12 | "github.com/pojntfx/weron/pkg/wrtcconn" 13 | "github.com/pojntfx/weron/pkg/wrtceth" 14 | "github.com/spf13/cobra" 15 | "github.com/spf13/viper" 16 | ) 17 | 18 | const ( 19 | devFlag = "dev" 20 | macFlag = "mac" 21 | parallelFlag = "parallel" 22 | ) 23 | 24 | var vpnEthernetCmd = &cobra.Command{ 25 | Use: "ethernet", 26 | Aliases: []string{"eth", "e"}, 27 | Short: "Join a layer 2 overlay network", 28 | RunE: func(cmd *cobra.Command, args []string) error { 29 | if err := viper.BindPFlags(cmd.PersistentFlags()); err != nil { 30 | return err 31 | } 32 | 33 | ctx, cancel := context.WithCancel(context.Background()) 34 | defer cancel() 35 | 36 | if strings.TrimSpace(viper.GetString(communityFlag)) == "" { 37 | return errMissingCommunity 38 | } 39 | 40 | if strings.TrimSpace(viper.GetString(passwordFlag)) == "" { 41 | return errMissingPassword 42 | } 43 | 44 | if strings.TrimSpace(viper.GetString(keyFlag)) == "" { 45 | return errMissingKey 46 | } 47 | 48 | u, err := url.Parse(viper.GetString(raddrFlag)) 49 | if err != nil { 50 | return err 51 | } 52 | 53 | q := u.Query() 54 | q.Set("community", viper.GetString(communityFlag)) 55 | q.Set("password", viper.GetString(passwordFlag)) 56 | u.RawQuery = q.Encode() 57 | 58 | adapter := wrtceth.NewAdapter( 59 | u.String(), 60 | viper.GetString(keyFlag), 61 | viper.GetStringSlice(iceFlag), 62 | &wrtceth.AdapterConfig{ 63 | Device: viper.GetString(devFlag), 64 | OnSignalerConnect: func(s string) { 65 | log.Info(). 66 | Str("id", s). 67 | Msg("Connected to signaler") 68 | }, 69 | OnPeerConnect: func(s string) { 70 | log.Info(). 71 | Str("id", s). 72 | Msg("Connected to peer") 73 | }, 74 | OnPeerDisconnected: func(s string) { 75 | log.Info(). 76 | Str("id", s). 77 | Msg("Disconnected from peer") 78 | }, 79 | Parallel: viper.GetInt(parallelFlag), 80 | AdapterConfig: &wrtcconn.AdapterConfig{ 81 | Timeout: viper.GetDuration(timeoutFlag), 82 | ID: viper.GetString(macFlag), 83 | ForceRelay: viper.GetBool(forceRelayFlag), 84 | }, 85 | }, 86 | ctx, 87 | ) 88 | 89 | log.Info(). 90 | Str("addr", viper.GetString(raddrFlag)). 91 | Msg("Connecting to signaler") 92 | 93 | if err := adapter.Open(); err != nil { 94 | return err 95 | } 96 | addInterruptHandler(cancel, adapter, nil) 97 | 98 | return adapter.Wait() 99 | }, 100 | } 101 | 102 | func init() { 103 | vpnEthernetCmd.PersistentFlags().String(raddrFlag, "wss://weron.up.railway.app/", "Remote address") 104 | vpnEthernetCmd.PersistentFlags().Duration(timeoutFlag, time.Second*10, "Time to wait for connections") 105 | vpnEthernetCmd.PersistentFlags().String(communityFlag, "", "ID of community to join") 106 | vpnEthernetCmd.PersistentFlags().String(passwordFlag, "", "Password for community") 107 | vpnEthernetCmd.PersistentFlags().String(keyFlag, "", "Encryption key for community") 108 | vpnEthernetCmd.PersistentFlags().StringSlice(iceFlag, []string{"stun:stun.l.google.com:19302"}, "Comma-separated list of STUN servers (in format stun:host:port) and TURN servers to use (in format username:credential@turn:host:port) (i.e. username:credential@turn:global.turn.twilio.com:3478?transport=tcp)") 109 | vpnEthernetCmd.PersistentFlags().Bool(forceRelayFlag, false, "Force usage of TURN servers") 110 | vpnEthernetCmd.PersistentFlags().String(devFlag, "", "Name to give to the TAP device (i.e. weron0) (default is auto-generated; only supported on Linux and macOS)") 111 | vpnEthernetCmd.PersistentFlags().String(macFlag, "", "MAC address to give to the TAP device (i.e. 3a:f8:de:7b:ef:52) (default is auto-generated; only supported on Linux)") 112 | vpnEthernetCmd.PersistentFlags().Int(parallelFlag, runtime.NumCPU(), "Amount of threads to use to decode frames") 113 | 114 | viper.AutomaticEnv() 115 | 116 | vpnCmd.AddCommand(vpnEthernetCmd) 117 | } 118 | -------------------------------------------------------------------------------- /cmd/weron/cmd/vpn_ip.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "net" 7 | "net/url" 8 | "runtime" 9 | "strings" 10 | "time" 11 | 12 | "github.com/rs/zerolog/log" 13 | 14 | "github.com/pojntfx/weron/pkg/services" 15 | "github.com/pojntfx/weron/pkg/wrtcconn" 16 | "github.com/pojntfx/weron/pkg/wrtcip" 17 | "github.com/spf13/cobra" 18 | "github.com/spf13/viper" 19 | ) 20 | 21 | var ( 22 | errInvalidCIDR = errors.New("invalid CIDR notation for IPs") 23 | ) 24 | 25 | const ( 26 | ipsFlag = "ips" 27 | maxRetriesFlag = "max-retries" 28 | staticFlag = "static" 29 | ) 30 | 31 | var vpnIPCmd = &cobra.Command{ 32 | Use: "ip", 33 | Aliases: []string{"i"}, 34 | Short: "Join a layer 3 overlay network", 35 | RunE: func(cmd *cobra.Command, args []string) error { 36 | if err := viper.BindPFlags(cmd.PersistentFlags()); err != nil { 37 | return err 38 | } 39 | 40 | ctx, cancel := context.WithCancel(context.Background()) 41 | defer cancel() 42 | 43 | if strings.TrimSpace(viper.GetString(communityFlag)) == "" { 44 | return errMissingCommunity 45 | } 46 | 47 | if strings.TrimSpace(viper.GetString(passwordFlag)) == "" { 48 | return errMissingPassword 49 | } 50 | 51 | if strings.TrimSpace(viper.GetString(keyFlag)) == "" { 52 | return errMissingKey 53 | } 54 | 55 | if len(viper.GetStringSlice(ipsFlag)) <= 0 { 56 | return wrtcip.ErrMissingIPs 57 | } 58 | 59 | for _, ip := range viper.GetStringSlice(ipsFlag) { 60 | if _, _, err := net.ParseCIDR(ip); err != nil { 61 | return errInvalidCIDR 62 | } 63 | } 64 | 65 | u, err := url.Parse(viper.GetString(raddrFlag)) 66 | if err != nil { 67 | return err 68 | } 69 | 70 | q := u.Query() 71 | q.Set("community", viper.GetString(communityFlag)) 72 | q.Set("password", viper.GetString(passwordFlag)) 73 | u.RawQuery = q.Encode() 74 | 75 | adapter := wrtcip.NewAdapter( 76 | u.String(), 77 | viper.GetString(keyFlag), 78 | viper.GetStringSlice(iceFlag), 79 | &wrtcip.AdapterConfig{ 80 | Device: viper.GetString(devFlag), 81 | OnSignalerConnect: func(s string) { 82 | log.Info(). 83 | Str("id", s). 84 | Msg("Connected to signaler") 85 | }, 86 | OnPeerConnect: func(s string) { 87 | log.Info(). 88 | Str("id", s). 89 | Msg("Connected to peer") 90 | }, 91 | OnPeerDisconnected: func(s string) { 92 | log.Info(). 93 | Str("id", s). 94 | Msg("Disconnected from peer") 95 | }, 96 | CIDRs: viper.GetStringSlice(ipsFlag), 97 | MaxRetries: viper.GetInt(maxRetriesFlag), 98 | Parallel: viper.GetInt(parallelFlag), 99 | NamedAdapterConfig: &wrtcconn.NamedAdapterConfig{ 100 | AdapterConfig: &wrtcconn.AdapterConfig{ 101 | Timeout: viper.GetDuration(timeoutFlag), 102 | ForceRelay: viper.GetBool(forceRelayFlag), 103 | }, 104 | IDChannel: viper.GetString(idChannelFlag), 105 | Kicks: viper.GetDuration(kicksFlag), 106 | }, 107 | Static: viper.GetBool(staticFlag), 108 | }, 109 | ctx, 110 | ) 111 | 112 | log.Info(). 113 | Str("addr", viper.GetString(raddrFlag)). 114 | Msg("Connecting to signaler") 115 | 116 | if err := adapter.Open(); err != nil { 117 | return err 118 | } 119 | addInterruptHandler(cancel, adapter, nil) 120 | 121 | return adapter.Wait() 122 | }, 123 | } 124 | 125 | func init() { 126 | vpnIPCmd.PersistentFlags().String(raddrFlag, "wss://weron.up.railway.app/", "Remote address") 127 | vpnIPCmd.PersistentFlags().Duration(timeoutFlag, time.Second*10, "Time to wait for connections") 128 | vpnIPCmd.PersistentFlags().String(communityFlag, "", "ID of community to join") 129 | vpnIPCmd.PersistentFlags().String(passwordFlag, "", "Password for community") 130 | vpnIPCmd.PersistentFlags().String(keyFlag, "", "Encryption key for community") 131 | vpnIPCmd.PersistentFlags().StringSlice(iceFlag, []string{"stun:stun.l.google.com:19302"}, "Comma-separated list of STUN servers (in format stun:host:port) and TURN servers to use (in format username:credential@turn:host:port) (i.e. username:credential@turn:global.turn.twilio.com:3478?transport=tcp)") 132 | vpnIPCmd.PersistentFlags().Bool(forceRelayFlag, false, "Force usage of TURN servers") 133 | vpnIPCmd.PersistentFlags().String(devFlag, "", "Name to give to the TUN device (i.e. weron0) (default is auto-generated; only supported on Linux)") 134 | vpnIPCmd.PersistentFlags().StringSlice(ipsFlag, []string{""}, "Comma-separated list of IP networks to claim an IP address from and and give to the TUN device (i.e. 2001:db8::1/32,192.0.2.1/24) (on Windows, only one IP network (either IPv4 or IPv6) is supported; on macOS, IPv4 networks are ignored)") 135 | vpnIPCmd.PersistentFlags().Bool(staticFlag, false, "Try to claim the exact IPs specified in the --"+ipsFlag+" flag statically instead of selecting a random one from the specified network") 136 | vpnIPCmd.PersistentFlags().Int(parallelFlag, runtime.NumCPU(), "Amount of threads to use to decode frames") 137 | vpnIPCmd.PersistentFlags().String(idChannelFlag, services.IPID, "Channel to use to negotiate names") 138 | vpnIPCmd.PersistentFlags().Duration(kicksFlag, time.Second*5, "Time to wait for kicks") 139 | vpnIPCmd.PersistentFlags().Int(maxRetriesFlag, 200, "Maximum amount of times to try and claim an IP address") 140 | 141 | viper.AutomaticEnv() 142 | 143 | vpnCmd.AddCommand(vpnIPCmd) 144 | } 145 | -------------------------------------------------------------------------------- /cmd/weron/cmd/vpn_root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | "github.com/spf13/viper" 6 | ) 7 | 8 | var vpnCmd = &cobra.Command{ 9 | Use: "vpn", 10 | Aliases: []string{"vpn", "v"}, 11 | Short: "Join virtual private networks built on overlay networks", 12 | } 13 | 14 | func init() { 15 | viper.AutomaticEnv() 16 | 17 | rootCmd.AddCommand(vpnCmd) 18 | } 19 | -------------------------------------------------------------------------------- /cmd/weron/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/pojntfx/weron/cmd/weron/cmd" 4 | 5 | func main() { 6 | if err := cmd.Execute(); err != nil { 7 | panic(err) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /configs/sql-migrate/communities.yaml: -------------------------------------------------------------------------------- 1 | psql: 2 | dialect: postgres 3 | datasource: dbname=weron_communities user=postgres sslmode=disable 4 | dir: db/psql/migrations/communities/ 5 | -------------------------------------------------------------------------------- /configs/sqlboiler/communities.yaml: -------------------------------------------------------------------------------- 1 | no-tests: true 2 | psql: 3 | dbname: weron_communities 4 | user: postgres 5 | sslmode: disable 6 | host: localhost -------------------------------------------------------------------------------- /db/psql/migrations/communities/1646780237.sql: -------------------------------------------------------------------------------- 1 | -- +migrate Up 2 | create table communities ( 3 | id text primary key not null, 4 | password text not null, 5 | clients integer not null, 6 | persistent boolean not null 7 | ); 8 | -- +migrate Down 9 | drop table communities; -------------------------------------------------------------------------------- /docs/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 304 | -------------------------------------------------------------------------------- /examples/weron-echo/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "errors" 7 | "flag" 8 | "fmt" 9 | "log" 10 | "net/url" 11 | "strings" 12 | "time" 13 | 14 | "github.com/pojntfx/weron/pkg/wrtcconn" 15 | "github.com/rs/zerolog" 16 | ) 17 | 18 | var ( 19 | errMissingCommunity = errors.New("missing community") 20 | errMissingPassword = errors.New("missing password") 21 | errMissingKey = errors.New("missing key") 22 | ) 23 | 24 | func main() { 25 | verboseFlag := flag.Int("verbose", 5, "Verbosity level (0 is disabled, default is info, 7 is trace)") 26 | raddrFlag := flag.String("raddr", "wss://weron.up.railway.app/", "Remote address") 27 | timeoutFlag := flag.Duration("timeout", time.Second*10, "Time to wait for connections") 28 | communityFlag := flag.String("community", "", "ID of community to join") 29 | passwordFlag := flag.String("password", "", "Password for community") 30 | keyFlag := flag.String("key", "", "Encryption key for community") 31 | iceFlag := flag.String("ice", "stun:stun.l.google.com:19302", "Comma-separated list of STUN servers (in format stun:host:port) and TURN servers to use (in format username:credential@turn:host:port) (i.e. username:credential@turn:global.turn.twilio.com:3478?transport=tcp)") 32 | forceRelayFlag := flag.Bool("force-relay", false, "Force usage of TURN servers") 33 | pauseFlag := flag.Duration("pause", time.Second*1, "Time to wait before sending next message") 34 | 35 | flag.Parse() 36 | 37 | switch *verboseFlag { 38 | case 0: 39 | zerolog.SetGlobalLevel(zerolog.Disabled) 40 | case 1: 41 | zerolog.SetGlobalLevel(zerolog.PanicLevel) 42 | case 2: 43 | zerolog.SetGlobalLevel(zerolog.FatalLevel) 44 | case 3: 45 | zerolog.SetGlobalLevel(zerolog.ErrorLevel) 46 | case 4: 47 | zerolog.SetGlobalLevel(zerolog.WarnLevel) 48 | case 5: 49 | zerolog.SetGlobalLevel(zerolog.InfoLevel) 50 | case 6: 51 | zerolog.SetGlobalLevel(zerolog.DebugLevel) 52 | default: 53 | zerolog.SetGlobalLevel(zerolog.TraceLevel) 54 | } 55 | 56 | ctx, cancel := context.WithCancel(context.Background()) 57 | defer cancel() 58 | 59 | if strings.TrimSpace(*communityFlag) == "" { 60 | panic(errMissingCommunity) 61 | } 62 | 63 | if strings.TrimSpace(*passwordFlag) == "" { 64 | panic(errMissingPassword) 65 | } 66 | 67 | if strings.TrimSpace(*keyFlag) == "" { 68 | panic(errMissingKey) 69 | } 70 | 71 | log.Println("Connecting to signaler with address", *raddrFlag) 72 | 73 | u, err := url.Parse(*raddrFlag) 74 | if err != nil { 75 | panic(err) 76 | } 77 | 78 | q := u.Query() 79 | q.Set("community", *communityFlag) 80 | q.Set("password", *passwordFlag) 81 | u.RawQuery = q.Encode() 82 | 83 | adapter := wrtcconn.NewAdapter( 84 | u.String(), 85 | *keyFlag, 86 | strings.Split(*iceFlag, ","), 87 | []string{"weron/example/echo"}, 88 | &wrtcconn.AdapterConfig{ 89 | Timeout: *timeoutFlag, 90 | ForceRelay: *forceRelayFlag, 91 | OnSignalerReconnect: func() { 92 | log.Println("Reconnecting to signaler with address", *raddrFlag) 93 | }, 94 | }, 95 | ctx, 96 | ) 97 | 98 | ids, err := adapter.Open() 99 | if err != nil { 100 | panic(err) 101 | } 102 | defer adapter.Close() 103 | 104 | id := "" 105 | errs := make(chan error) 106 | for { 107 | select { 108 | case <-ctx.Done(): 109 | if err := ctx.Err(); err != context.Canceled { 110 | panic(err) 111 | } 112 | 113 | return 114 | case err := <-errs: 115 | panic(err) 116 | case rid := <-ids: 117 | id = rid 118 | 119 | log.Println("Connected to signaler with address", *raddrFlag, "and ID", rid) 120 | case peer := <-adapter.Accept(): 121 | go func() { 122 | defer func() { 123 | log.Println("Disconnected from peer with ID", peer.PeerID, "and channel", peer.ChannelID) 124 | }() 125 | 126 | log.Println("Connected to peer with ID", peer.PeerID, "and channel", peer.ChannelID) 127 | 128 | go func() { 129 | ticker := time.NewTicker(*pauseFlag) 130 | 131 | for { 132 | select { 133 | case <-ctx.Done(): 134 | ticker.Stop() 135 | 136 | return 137 | case <-ticker.C: 138 | if _, err := peer.Conn.Write([]byte(fmt.Sprintf("Hello from %v! It is currently %v local time.\n", id, time.Now().Local()))); err != nil { 139 | return 140 | } 141 | } 142 | } 143 | }() 144 | 145 | reader := bufio.NewScanner(peer.Conn) 146 | 147 | for reader.Scan() { 148 | fmt.Printf("Got message from peer %v: %v\n", peer.PeerID, reader.Text()) 149 | } 150 | }() 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/pojntfx/weron 2 | 3 | // +heroku goVersion go1.24.2 4 | go 1.24 5 | 6 | toolchain go1.24.2 7 | 8 | require ( 9 | github.com/friendsofgo/errors v0.9.2 10 | github.com/go-redis/redis/v8 v8.11.5 11 | github.com/google/gopacket v1.1.19 12 | github.com/google/uuid v1.6.0 13 | github.com/gorilla/websocket v1.5.3 14 | github.com/json-iterator/go v1.1.12 15 | github.com/lib/pq v1.10.9 16 | github.com/mitchellh/mapstructure v1.5.0 17 | github.com/pion/webrtc/v3 v3.3.5 18 | github.com/pojntfx/go-auth-utils v0.1.0 19 | github.com/rs/zerolog v1.34.0 20 | github.com/rubenv/sql-migrate v1.8.0 21 | github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 22 | github.com/spf13/cobra v1.9.1 23 | github.com/spf13/pflag v1.0.6 24 | github.com/spf13/viper v1.20.1 25 | github.com/teivah/broadcast v0.1.0 26 | github.com/vishvananda/netlink v1.3.0 27 | github.com/volatiletech/null/v8 v8.1.2 28 | github.com/volatiletech/sqlboiler/v4 v4.18.0 29 | github.com/volatiletech/strmangle v0.0.8 30 | golang.org/x/crypto v0.37.0 31 | golang.org/x/sync v0.13.0 32 | ) 33 | 34 | require ( 35 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 36 | github.com/coreos/go-oidc/v3 v3.14.1 // indirect 37 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 38 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 39 | github.com/fsnotify/fsnotify v1.9.0 // indirect 40 | github.com/go-gorp/gorp/v3 v3.1.0 // indirect 41 | github.com/go-jose/go-jose/v4 v4.1.0 // indirect 42 | github.com/go-viper/mapstructure/v2 v2.2.1 // indirect 43 | github.com/gofrs/uuid v4.4.0+incompatible // indirect 44 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 45 | github.com/mattn/go-colorable v0.1.14 // indirect 46 | github.com/mattn/go-isatty v0.0.20 // indirect 47 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 48 | github.com/modern-go/reflect2 v1.0.2 // indirect 49 | github.com/pelletier/go-toml/v2 v2.2.4 // indirect 50 | github.com/pion/datachannel v1.5.10 // indirect 51 | github.com/pion/dtls/v2 v2.2.12 // indirect 52 | github.com/pion/ice/v2 v2.3.37 // indirect 53 | github.com/pion/interceptor v0.1.37 // indirect 54 | github.com/pion/logging v0.2.3 // indirect 55 | github.com/pion/mdns v0.0.12 // indirect 56 | github.com/pion/randutil v0.1.0 // indirect 57 | github.com/pion/rtcp v1.2.15 // indirect 58 | github.com/pion/rtp v1.8.13 // indirect 59 | github.com/pion/sctp v1.8.38 // indirect 60 | github.com/pion/sdp/v3 v3.0.11 // indirect 61 | github.com/pion/srtp/v2 v2.0.20 // indirect 62 | github.com/pion/stun v0.6.1 // indirect 63 | github.com/pion/transport/v2 v2.2.10 // indirect 64 | github.com/pion/transport/v3 v3.0.7 // indirect 65 | github.com/pion/turn/v2 v2.1.6 // indirect 66 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 67 | github.com/sagikazarmark/locafero v0.9.0 // indirect 68 | github.com/sourcegraph/conc v0.3.0 // indirect 69 | github.com/spf13/afero v1.14.0 // indirect 70 | github.com/spf13/cast v1.7.1 // indirect 71 | github.com/stretchr/testify v1.10.0 // indirect 72 | github.com/subosito/gotenv v1.6.0 // indirect 73 | github.com/vishvananda/netns v0.0.5 // indirect 74 | github.com/volatiletech/inflect v0.0.1 // indirect 75 | github.com/volatiletech/randomize v0.0.1 // indirect 76 | github.com/wlynxg/anet v0.0.5 // indirect 77 | go.uber.org/multierr v1.11.0 // indirect 78 | golang.org/x/net v0.39.0 // indirect 79 | golang.org/x/oauth2 v0.29.0 // indirect 80 | golang.org/x/sys v0.32.0 // indirect 81 | golang.org/x/text v0.24.0 // indirect 82 | golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect 83 | gopkg.in/yaml.v3 v3.0.1 // indirect 84 | ) 85 | -------------------------------------------------------------------------------- /internal/api/websocket/messages.go: -------------------------------------------------------------------------------- 1 | package websocket 2 | 3 | type Message struct { 4 | Type string `json:"type"` 5 | } 6 | 7 | type Introduction struct { 8 | *Message 9 | 10 | From string `json:"from"` 11 | } 12 | 13 | type Exchange struct { 14 | *Message 15 | 16 | From string `json:"from"` 17 | To string `json:"to"` 18 | Payload []byte `json:"payload"` 19 | } 20 | 21 | func NewIntroduction(from string) *Introduction { 22 | return &Introduction{ 23 | Message: &Message{ 24 | Type: TypeIntroduction, 25 | }, 26 | From: from, 27 | } 28 | } 29 | 30 | func NewOffer(from string, to string, payload []byte) *Exchange { 31 | return &Exchange{ 32 | Message: &Message{ 33 | Type: TypeOffer, 34 | }, 35 | From: from, 36 | To: to, 37 | Payload: payload, 38 | } 39 | } 40 | 41 | func NewAnswer(from string, to string, payload []byte) *Exchange { 42 | return &Exchange{ 43 | Message: &Message{ 44 | Type: TypeAnswer, 45 | }, 46 | From: from, 47 | To: to, 48 | Payload: payload, 49 | } 50 | } 51 | 52 | func NewCandidate(from string, to string, payload []byte) *Exchange { 53 | return &Exchange{ 54 | Message: &Message{ 55 | Type: TypeCandidate, 56 | }, 57 | From: from, 58 | To: to, 59 | Payload: payload, 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /internal/api/websocket/types.go: -------------------------------------------------------------------------------- 1 | package websocket 2 | 3 | const ( 4 | TypeIntroduction = "introduction" 5 | TypeOffer = "offer" 6 | TypeAnswer = "answer" 7 | TypeCandidate = "candidate" 8 | ) 9 | -------------------------------------------------------------------------------- /internal/brokers/communities.go: -------------------------------------------------------------------------------- 1 | package brokers 2 | 3 | import "context" 4 | 5 | type Kick struct { 6 | Community string `json:"community"` 7 | } 8 | 9 | type Input struct { 10 | Raddr string `json:"raddr"` 11 | MessageType int `json:"messageType"` 12 | P []byte `json:"p"` 13 | } 14 | 15 | type CommunitiesBroker interface { 16 | Open(ctx context.Context, brokerURL string) error 17 | SubscribeToKicks(ctx context.Context, errs chan error) (kicks chan Kick, close func() error) 18 | SubscribeToInputs(ctx context.Context, errs chan error, community string) (kicks chan Input, close func() error) 19 | PublishInput(ctx context.Context, input Input, community string) error 20 | PublishKick(ctx context.Context, kick Kick) error 21 | Close() error 22 | } 23 | -------------------------------------------------------------------------------- /internal/brokers/process/communities.go: -------------------------------------------------------------------------------- 1 | package process 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "github.com/pojntfx/weron/internal/brokers" 8 | "github.com/teivah/broadcast" 9 | ) 10 | 11 | var ( 12 | ErrCouldNotUnmarshalKick = errors.New("could not unmarshal kick") 13 | ErrCouldNotUnmarshalInput = errors.New("could not unmarshal input") 14 | ) 15 | 16 | type CommunitiesBroker struct { 17 | kicks *broadcast.Relay[brokers.Kick] 18 | inputs *broadcast.Relay[brokers.Input] 19 | } 20 | 21 | func NewCommunitiesBroker() *CommunitiesBroker { 22 | return &CommunitiesBroker{ 23 | kicks: broadcast.NewRelay[brokers.Kick](), 24 | inputs: broadcast.NewRelay[brokers.Input](), 25 | } 26 | } 27 | 28 | func (c *CommunitiesBroker) Open(ctx context.Context, brokerURL string) error { 29 | return nil 30 | } 31 | 32 | func (c *CommunitiesBroker) SubscribeToKicks(ctx context.Context, errs chan error) (chan brokers.Kick, func() error) { 33 | kicks := make(chan brokers.Kick) 34 | 35 | l := c.kicks.Listener(0) 36 | rawKicks := l.Ch() 37 | 38 | go func() { 39 | for { 40 | select { 41 | case <-ctx.Done(): 42 | return 43 | case kick := <-rawKicks: 44 | kicks <- kick 45 | } 46 | } 47 | }() 48 | 49 | return kicks, func() error { 50 | l.Close() 51 | 52 | return nil 53 | } 54 | } 55 | 56 | func (c *CommunitiesBroker) SubscribeToInputs(ctx context.Context, errs chan error, community string) (chan brokers.Input, func() error) { 57 | inputs := make(chan brokers.Input) 58 | 59 | l := c.inputs.Listener(0) 60 | rawInputs := l.Ch() 61 | 62 | go func() { 63 | for { 64 | select { 65 | case <-ctx.Done(): 66 | return 67 | case input := <-rawInputs: 68 | inputs <- input 69 | } 70 | } 71 | }() 72 | 73 | return inputs, func() error { 74 | l.Close() 75 | 76 | return nil 77 | } 78 | } 79 | 80 | func (c *CommunitiesBroker) PublishInput(ctx context.Context, input brokers.Input, community string) error { 81 | c.inputs.NotifyCtx(ctx, input) 82 | 83 | return nil 84 | } 85 | 86 | func (c *CommunitiesBroker) PublishKick(ctx context.Context, kick brokers.Kick) error { 87 | c.kicks.NotifyCtx(ctx, kick) 88 | 89 | return nil 90 | } 91 | 92 | func (c *CommunitiesBroker) Close() error { 93 | c.inputs.Close() 94 | c.kicks.Close() 95 | 96 | return nil 97 | } 98 | -------------------------------------------------------------------------------- /internal/brokers/redis/communities.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/go-redis/redis/v8" 7 | jsoniter "github.com/json-iterator/go" 8 | "github.com/pojntfx/weron/internal/brokers" 9 | ) 10 | 11 | const ( 12 | topicKick = "kick" 13 | topicMessagesPrefix = "messages." 14 | ) 15 | 16 | var ( 17 | json = jsoniter.ConfigCompatibleWithStandardLibrary 18 | ) 19 | 20 | type CommunitiesBroker struct { 21 | client *redis.Client 22 | } 23 | 24 | func NewCommunitiesBroker() *CommunitiesBroker { 25 | return &CommunitiesBroker{} 26 | } 27 | 28 | func (c *CommunitiesBroker) Open(ctx context.Context, brokerURL string) error { 29 | u, err := redis.ParseURL(brokerURL) 30 | if err != nil { 31 | return err 32 | } 33 | 34 | c.client = redis.NewClient(u).WithContext(ctx) 35 | 36 | return nil 37 | } 38 | 39 | func (c *CommunitiesBroker) SubscribeToKicks(ctx context.Context, errs chan error) (chan brokers.Kick, func() error) { 40 | kicks := make(chan brokers.Kick) 41 | 42 | kickPubsub := c.client.Subscribe(ctx, topicKick) 43 | rawKicks := kickPubsub.Channel() 44 | 45 | go func() { 46 | for { 47 | select { 48 | case <-ctx.Done(): 49 | return 50 | case rawKick := <-rawKicks: 51 | if rawKick == nil { 52 | close(kicks) 53 | 54 | // Channel closed 55 | return 56 | } 57 | 58 | var kick brokers.Kick 59 | if err := json.Unmarshal([]byte(rawKick.Payload), &kick); err != nil { 60 | errs <- err 61 | 62 | return 63 | } 64 | 65 | kicks <- kick 66 | } 67 | } 68 | }() 69 | 70 | return kicks, kickPubsub.Close 71 | } 72 | 73 | func (c *CommunitiesBroker) SubscribeToInputs(ctx context.Context, errs chan error, community string) (chan brokers.Input, func() error) { 74 | inputs := make(chan brokers.Input) 75 | 76 | inputsPubsub := c.client.Subscribe(ctx, topicMessagesPrefix+community) 77 | rawKicks := inputsPubsub.Channel() 78 | 79 | go func() { 80 | for { 81 | select { 82 | case <-ctx.Done(): 83 | return 84 | case rawInput := <-rawKicks: 85 | if rawInput == nil { 86 | close(inputs) 87 | 88 | // Channel closed 89 | return 90 | } 91 | 92 | var input brokers.Input 93 | if err := json.Unmarshal([]byte(rawInput.Payload), &input); err != nil { 94 | errs <- err 95 | 96 | return 97 | } 98 | 99 | inputs <- input 100 | } 101 | } 102 | }() 103 | 104 | return inputs, inputsPubsub.Close 105 | } 106 | 107 | func (c *CommunitiesBroker) PublishInput(ctx context.Context, input brokers.Input, community string) error { 108 | data, err := json.Marshal(input) 109 | if err != nil { 110 | return err 111 | } 112 | 113 | return c.client.Publish(ctx, topicMessagesPrefix+community, data).Err() 114 | } 115 | 116 | func (c *CommunitiesBroker) PublishKick(ctx context.Context, kick brokers.Kick) error { 117 | data, err := json.Marshal(kick) 118 | if err != nil { 119 | return err 120 | } 121 | 122 | return c.client.Publish(ctx, topicKick, data).Err() 123 | } 124 | 125 | func (c *CommunitiesBroker) Close() error { 126 | return c.client.Close() 127 | } 128 | -------------------------------------------------------------------------------- /internal/db/psql/migrations/communities/migrations.go: -------------------------------------------------------------------------------- 1 | package communities 2 | 3 | import ( 4 | "bytes" 5 | "compress/gzip" 6 | "fmt" 7 | "io" 8 | "strings" 9 | ) 10 | 11 | func bindata_read(data []byte, name string) ([]byte, error) { 12 | gz, err := gzip.NewReader(bytes.NewBuffer(data)) 13 | if err != nil { 14 | return nil, fmt.Errorf("Read %q: %v", name, err) 15 | } 16 | 17 | var buf bytes.Buffer 18 | _, err = io.Copy(&buf, gz) 19 | gz.Close() 20 | 21 | if err != nil { 22 | return nil, fmt.Errorf("Read %q: %v", name, err) 23 | } 24 | 25 | return buf.Bytes(), nil 26 | } 27 | 28 | var _db_psql_migrations_communities_1646780237_sql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x64\xce\x4b\xaa\xc3\x30\x0c\x85\xe1\xb9\x57\x71\x86\xf7\xd2\x66\x05\x99\x76\x0b\x5d\x80\xe3\x88\x20\x6a\x4b\x46\x52\x48\xb3\xfb\xd2\x52\x4a\x1f\x33\x81\xbe\x03\xff\x30\xe0\xd0\x78\xb1\x1c\x84\x73\x4f\xc5\xe8\x7e\x45\x9e\x2a\xa1\x68\x6b\xab\x70\x30\x39\xfe\x12\x00\xf0\x8c\xa0\x6b\xa0\x1b\xb7\x6c\x3b\x2e\xb4\x43\x34\x20\x6b\xad\xc7\x87\xe8\xd9\x7d\x53\x7b\xba\xcf\x5f\xa9\x4c\x12\x0e\x96\xa0\x85\xec\x7b\x49\xe6\xec\x41\x12\x98\x54\x2b\x65\x79\x81\xf4\x3f\xa6\xf7\xd0\x93\x6e\x92\x66\xd3\xfe\x1b\x3a\xde\x02\x00\x00\xff\xff\xea\x26\xc1\x9c\xd1\x00\x00\x00") 29 | 30 | func db_psql_migrations_communities_1646780237_sql() ([]byte, error) { 31 | return bindata_read( 32 | _db_psql_migrations_communities_1646780237_sql, 33 | "../../../db/psql/migrations/communities/1646780237.sql", 34 | ) 35 | } 36 | 37 | // Asset loads and returns the asset for the given name. 38 | // It returns an error if the asset could not be found or 39 | // could not be loaded. 40 | func Asset(name string) ([]byte, error) { 41 | cannonicalName := strings.Replace(name, "\\", "/", -1) 42 | if f, ok := _bindata[cannonicalName]; ok { 43 | return f() 44 | } 45 | return nil, fmt.Errorf("Asset %s not found", name) 46 | } 47 | 48 | // AssetNames returns the names of the assets. 49 | func AssetNames() []string { 50 | names := make([]string, 0, len(_bindata)) 51 | for name := range _bindata { 52 | names = append(names, name) 53 | } 54 | return names 55 | } 56 | 57 | // _bindata is a table, holding each asset generator, mapped to its name. 58 | var _bindata = map[string]func() ([]byte, error){ 59 | "../../../db/psql/migrations/communities/1646780237.sql": db_psql_migrations_communities_1646780237_sql, 60 | } 61 | // AssetDir returns the file names below a certain 62 | // directory embedded in the file by go-bindata. 63 | // For example if you run go-bindata on data/... and data contains the 64 | // following hierarchy: 65 | // data/ 66 | // foo.txt 67 | // img/ 68 | // a.png 69 | // b.png 70 | // then AssetDir("data") would return []string{"foo.txt", "img"} 71 | // AssetDir("data/img") would return []string{"a.png", "b.png"} 72 | // AssetDir("foo.txt") and AssetDir("notexist") would return an error 73 | // AssetDir("") will return []string{"data"}. 74 | func AssetDir(name string) ([]string, error) { 75 | node := _bintree 76 | if len(name) != 0 { 77 | cannonicalName := strings.Replace(name, "\\", "/", -1) 78 | pathList := strings.Split(cannonicalName, "/") 79 | for _, p := range pathList { 80 | node = node.Children[p] 81 | if node == nil { 82 | return nil, fmt.Errorf("Asset %s not found", name) 83 | } 84 | } 85 | } 86 | if node.Func != nil { 87 | return nil, fmt.Errorf("Asset %s not found", name) 88 | } 89 | rv := make([]string, 0, len(node.Children)) 90 | for name := range node.Children { 91 | rv = append(rv, name) 92 | } 93 | return rv, nil 94 | } 95 | 96 | type _bintree_t struct { 97 | Func func() ([]byte, error) 98 | Children map[string]*_bintree_t 99 | } 100 | var _bintree = &_bintree_t{nil, map[string]*_bintree_t{ 101 | "..": &_bintree_t{nil, map[string]*_bintree_t{ 102 | "..": &_bintree_t{nil, map[string]*_bintree_t{ 103 | "..": &_bintree_t{nil, map[string]*_bintree_t{ 104 | "db": &_bintree_t{nil, map[string]*_bintree_t{ 105 | "psql": &_bintree_t{nil, map[string]*_bintree_t{ 106 | "migrations": &_bintree_t{nil, map[string]*_bintree_t{ 107 | "communities": &_bintree_t{nil, map[string]*_bintree_t{ 108 | "1646780237.sql": &_bintree_t{db_psql_migrations_communities_1646780237_sql, map[string]*_bintree_t{ 109 | }}, 110 | }}, 111 | }}, 112 | }}, 113 | }}, 114 | }}, 115 | }}, 116 | }}, 117 | }} 118 | -------------------------------------------------------------------------------- /internal/db/psql/models/communities/boil_queries.go: -------------------------------------------------------------------------------- 1 | // Code generated by SQLBoiler 4.18.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. 2 | // This file is meant to be re-generated in place and/or deleted at any time. 3 | 4 | package models 5 | 6 | import ( 7 | "regexp" 8 | 9 | "github.com/volatiletech/sqlboiler/v4/drivers" 10 | "github.com/volatiletech/sqlboiler/v4/queries" 11 | "github.com/volatiletech/sqlboiler/v4/queries/qm" 12 | ) 13 | 14 | var dialect = drivers.Dialect{ 15 | LQ: 0x22, 16 | RQ: 0x22, 17 | 18 | UseIndexPlaceholders: true, 19 | UseLastInsertID: false, 20 | UseSchema: false, 21 | UseDefaultKeyword: true, 22 | UseAutoColumns: false, 23 | UseTopClause: false, 24 | UseOutputClause: false, 25 | UseCaseWhenExistsClause: false, 26 | } 27 | 28 | // This is a dummy variable to prevent unused regexp import error 29 | var _ = ®exp.Regexp{} 30 | 31 | // NewQuery initializes a new Query using the passed in QueryMods 32 | func NewQuery(mods ...qm.QueryMod) *queries.Query { 33 | q := &queries.Query{} 34 | queries.SetDialect(q, &dialect) 35 | qm.Apply(q, mods...) 36 | 37 | return q 38 | } 39 | -------------------------------------------------------------------------------- /internal/db/psql/models/communities/boil_table_names.go: -------------------------------------------------------------------------------- 1 | // Code generated by SQLBoiler 4.18.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. 2 | // This file is meant to be re-generated in place and/or deleted at any time. 3 | 4 | package models 5 | 6 | var TableNames = struct { 7 | Communities string 8 | GorpMigrations string 9 | }{ 10 | Communities: "communities", 11 | GorpMigrations: "gorp_migrations", 12 | } 13 | -------------------------------------------------------------------------------- /internal/db/psql/models/communities/boil_types.go: -------------------------------------------------------------------------------- 1 | // Code generated by SQLBoiler 4.18.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. 2 | // This file is meant to be re-generated in place and/or deleted at any time. 3 | 4 | package models 5 | 6 | import ( 7 | "strconv" 8 | 9 | "github.com/friendsofgo/errors" 10 | "github.com/volatiletech/sqlboiler/v4/boil" 11 | "github.com/volatiletech/strmangle" 12 | ) 13 | 14 | // M type is for providing columns and column values to UpdateAll. 15 | type M map[string]interface{} 16 | 17 | // ErrSyncFail occurs during insert when the record could not be retrieved in 18 | // order to populate default value information. This usually happens when LastInsertId 19 | // fails or there was a primary key configuration that was not resolvable. 20 | var ErrSyncFail = errors.New("models: failed to synchronize data after insert") 21 | 22 | type insertCache struct { 23 | query string 24 | retQuery string 25 | valueMapping []uint64 26 | retMapping []uint64 27 | } 28 | 29 | type updateCache struct { 30 | query string 31 | valueMapping []uint64 32 | } 33 | 34 | func makeCacheKey(cols boil.Columns, nzDefaults []string) string { 35 | buf := strmangle.GetBuffer() 36 | 37 | buf.WriteString(strconv.Itoa(cols.Kind)) 38 | for _, w := range cols.Cols { 39 | buf.WriteString(w) 40 | } 41 | 42 | if len(nzDefaults) != 0 { 43 | buf.WriteByte('.') 44 | } 45 | for _, nz := range nzDefaults { 46 | buf.WriteString(nz) 47 | } 48 | 49 | str := buf.String() 50 | strmangle.PutBuffer(buf) 51 | return str 52 | } 53 | -------------------------------------------------------------------------------- /internal/db/psql/models/communities/boil_view_names.go: -------------------------------------------------------------------------------- 1 | // Code generated by SQLBoiler 4.18.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. 2 | // This file is meant to be re-generated in place and/or deleted at any time. 3 | 4 | package models 5 | 6 | var ViewNames = struct { 7 | }{} 8 | -------------------------------------------------------------------------------- /internal/db/psql/models/communities/psql_upsert.go: -------------------------------------------------------------------------------- 1 | // Code generated by SQLBoiler 4.18.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. 2 | // This file is meant to be re-generated in place and/or deleted at any time. 3 | 4 | package models 5 | 6 | import ( 7 | "fmt" 8 | "strings" 9 | 10 | "github.com/volatiletech/sqlboiler/v4/drivers" 11 | "github.com/volatiletech/strmangle" 12 | ) 13 | 14 | type UpsertOptions struct { 15 | conflictTarget string 16 | updateSet string 17 | } 18 | 19 | type UpsertOptionFunc func(o *UpsertOptions) 20 | 21 | func UpsertConflictTarget(conflictTarget string) UpsertOptionFunc { 22 | return func(o *UpsertOptions) { 23 | o.conflictTarget = conflictTarget 24 | } 25 | } 26 | 27 | func UpsertUpdateSet(updateSet string) UpsertOptionFunc { 28 | return func(o *UpsertOptions) { 29 | o.updateSet = updateSet 30 | } 31 | } 32 | 33 | // buildUpsertQueryPostgres builds a SQL statement string using the upsertData provided. 34 | func buildUpsertQueryPostgres(dia drivers.Dialect, tableName string, updateOnConflict bool, ret, update, conflict, whitelist []string, opts ...UpsertOptionFunc) string { 35 | conflict = strmangle.IdentQuoteSlice(dia.LQ, dia.RQ, conflict) 36 | whitelist = strmangle.IdentQuoteSlice(dia.LQ, dia.RQ, whitelist) 37 | ret = strmangle.IdentQuoteSlice(dia.LQ, dia.RQ, ret) 38 | 39 | upsertOpts := &UpsertOptions{} 40 | for _, o := range opts { 41 | o(upsertOpts) 42 | } 43 | 44 | buf := strmangle.GetBuffer() 45 | defer strmangle.PutBuffer(buf) 46 | 47 | columns := "DEFAULT VALUES" 48 | if len(whitelist) != 0 { 49 | columns = fmt.Sprintf("(%s) VALUES (%s)", 50 | strings.Join(whitelist, ", "), 51 | strmangle.Placeholders(dia.UseIndexPlaceholders, len(whitelist), 1, 1)) 52 | } 53 | 54 | fmt.Fprintf( 55 | buf, 56 | "INSERT INTO %s %s ON CONFLICT ", 57 | tableName, 58 | columns, 59 | ) 60 | 61 | if upsertOpts.conflictTarget != "" { 62 | buf.WriteString(upsertOpts.conflictTarget) 63 | } else if len(conflict) != 0 { 64 | buf.WriteByte('(') 65 | buf.WriteString(strings.Join(conflict, ", ")) 66 | buf.WriteByte(')') 67 | } 68 | buf.WriteByte(' ') 69 | 70 | if !updateOnConflict || len(update) == 0 { 71 | buf.WriteString("DO NOTHING") 72 | } else { 73 | buf.WriteString("DO UPDATE SET ") 74 | 75 | if upsertOpts.updateSet != "" { 76 | buf.WriteString(upsertOpts.updateSet) 77 | } else { 78 | for i, v := range update { 79 | if len(v) == 0 { 80 | continue 81 | } 82 | if i != 0 { 83 | buf.WriteByte(',') 84 | } 85 | quoted := strmangle.IdentQuote(dia.LQ, dia.RQ, v) 86 | buf.WriteString(quoted) 87 | buf.WriteString(" = EXCLUDED.") 88 | buf.WriteString(quoted) 89 | } 90 | } 91 | } 92 | 93 | if len(ret) != 0 { 94 | buf.WriteString(" RETURNING ") 95 | buf.WriteString(strings.Join(ret, ", ")) 96 | } 97 | 98 | return buf.String() 99 | } 100 | -------------------------------------------------------------------------------- /internal/encryption/aes.go: -------------------------------------------------------------------------------- 1 | package encryption 2 | 3 | import ( 4 | "crypto/aes" 5 | "crypto/cipher" 6 | "crypto/rand" 7 | "crypto/sha256" 8 | ) 9 | 10 | // See https://bruinsslot.jp/post/golang-crypto/ 11 | 12 | func Encrypt(data, password []byte) ([]byte, error) { 13 | key := deriveKey(password) 14 | 15 | blockCipher, err := aes.NewCipher(key) 16 | if err != nil { 17 | return nil, err 18 | } 19 | 20 | gcm, err := cipher.NewGCM(blockCipher) 21 | if err != nil { 22 | return nil, err 23 | } 24 | 25 | nonce := make([]byte, gcm.NonceSize()) 26 | if _, err = rand.Read(nonce); err != nil { 27 | return nil, err 28 | } 29 | 30 | return gcm.Seal(nonce, nonce, data, nil), nil 31 | } 32 | 33 | func Decrypt(data, password []byte) ([]byte, error) { 34 | key := deriveKey(password) 35 | 36 | blockCipher, err := aes.NewCipher(key) 37 | if err != nil { 38 | return nil, err 39 | } 40 | 41 | gcm, err := cipher.NewGCM(blockCipher) 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | nonce, ciphertext := data[:gcm.NonceSize()], data[gcm.NonceSize():] 47 | 48 | plaintext, err := gcm.Open(nil, nonce, ciphertext, nil) 49 | if err != nil { 50 | return nil, err 51 | } 52 | 53 | return plaintext, nil 54 | } 55 | 56 | func deriveKey(password []byte) []byte { 57 | buf := make([]byte, 32) // Will use AES-256 58 | 59 | h := sha256.Sum224(password) 60 | copy(buf, h[:]) // Fill the rest of the hash with zeros (SHA-224 leads to a 28 byte long hash) 61 | 62 | return buf 63 | } 64 | -------------------------------------------------------------------------------- /internal/persisters/communities.go: -------------------------------------------------------------------------------- 1 | package persisters 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | ) 7 | 8 | var ( 9 | ErrEphemeralCommunitiesDisabled = errors.New("creation of ephemeral communites is disabled") 10 | ) 11 | 12 | type Community struct { 13 | ID string `json:"id"` 14 | Clients int `json:"clients"` 15 | Persistent bool `json:"persistent"` 16 | } 17 | 18 | type CommunitiesPersister interface { 19 | Open(dbURL string) error 20 | AddClientsToCommunity( 21 | ctx context.Context, 22 | community string, 23 | password string, 24 | upsert bool, 25 | ) error 26 | RemoveClientFromCommunity( 27 | ctx context.Context, 28 | community string, 29 | ) error 30 | Cleanup( 31 | ctx context.Context, 32 | ) error 33 | GetCommunities( 34 | ctx context.Context, 35 | ) ([]Community, error) 36 | CreatePersistentCommunity( 37 | ctx context.Context, 38 | community string, 39 | password string, 40 | ) (*Community, error) 41 | DeleteCommunity( 42 | ctx context.Context, 43 | community string, 44 | ) error 45 | } 46 | -------------------------------------------------------------------------------- /internal/persisters/memory/communities.go: -------------------------------------------------------------------------------- 1 | package memory 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "errors" 7 | "sync" 8 | 9 | "github.com/pojntfx/go-auth-utils/pkg/authn" 10 | "github.com/pojntfx/weron/internal/persisters" 11 | "golang.org/x/crypto/bcrypt" 12 | ) 13 | 14 | var ( 15 | ErrUniqueConstraintViolation = errors.New("unique constraint violation") 16 | ) 17 | 18 | type Community struct { 19 | *persisters.Community 20 | password string 21 | } 22 | 23 | type CommunitiesPersister struct { 24 | lock sync.Mutex 25 | communities []*Community 26 | } 27 | 28 | func NewCommunitiesPersister() *CommunitiesPersister { 29 | return &CommunitiesPersister{ 30 | communities: []*Community{}, 31 | } 32 | } 33 | 34 | func (p *CommunitiesPersister) Open(dbURL string) error { 35 | return nil 36 | } 37 | 38 | func (p *CommunitiesPersister) AddClientsToCommunity( 39 | ctx context.Context, 40 | community string, 41 | password string, 42 | upsert bool, 43 | ) error { 44 | p.lock.Lock() 45 | defer p.lock.Unlock() 46 | 47 | hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) 48 | if err != nil { 49 | return err 50 | } 51 | 52 | var c *Community 53 | for _, candidate := range p.communities { 54 | if candidate.ID == community { 55 | c = candidate 56 | 57 | break 58 | } 59 | } 60 | 61 | if c == nil { 62 | p.communities = append(p.communities, &Community{ 63 | password: string(hashedPassword), 64 | Community: &persisters.Community{ 65 | ID: community, 66 | Clients: 1, 67 | Persistent: false, 68 | }, 69 | }) 70 | 71 | return nil 72 | } 73 | 74 | if bcrypt.CompareHashAndPassword([]byte(c.password), []byte(password)) != nil { 75 | return authn.ErrWrongPassword 76 | } 77 | 78 | c.Clients += 1 79 | 80 | return nil 81 | } 82 | 83 | func (p *CommunitiesPersister) RemoveClientFromCommunity( 84 | ctx context.Context, 85 | community string, 86 | ) error { 87 | p.lock.Lock() 88 | defer p.lock.Unlock() 89 | 90 | var c *Community 91 | for _, candidate := range p.communities { 92 | if candidate.ID == community { 93 | c = candidate 94 | 95 | break 96 | } 97 | } 98 | 99 | if c == nil { 100 | return sql.ErrNoRows 101 | } 102 | 103 | c.Clients -= 1 104 | if c.Clients <= 0 { 105 | if !c.Persistent { 106 | newCommunities := []*Community{} 107 | for _, candidate := range p.communities { 108 | if candidate.ID != community { 109 | newCommunities = append(newCommunities, candidate) 110 | } 111 | } 112 | 113 | p.communities = newCommunities 114 | 115 | return nil 116 | } 117 | 118 | c.Clients = 0 119 | } 120 | 121 | return nil 122 | } 123 | 124 | func (p *CommunitiesPersister) Cleanup( 125 | ctx context.Context, 126 | ) error { 127 | p.lock.Lock() 128 | defer p.lock.Unlock() 129 | 130 | newCommunities := []*Community{} 131 | for _, candidate := range p.communities { 132 | // Delete all ephemeral communities 133 | if candidate.Persistent { 134 | continue 135 | } 136 | 137 | // Set client count to 0 for all persistent communities 138 | candidate.Clients = 0 139 | 140 | newCommunities = append(newCommunities, candidate) 141 | } 142 | 143 | p.communities = newCommunities 144 | 145 | return nil 146 | } 147 | 148 | func (p *CommunitiesPersister) GetCommunities( 149 | ctx context.Context, 150 | ) ([]persisters.Community, error) { 151 | p.lock.Lock() 152 | defer p.lock.Unlock() 153 | 154 | cc := []persisters.Community{} 155 | for _, community := range p.communities { 156 | cc = append(cc, persisters.Community{ 157 | ID: community.ID, 158 | Clients: community.Clients, 159 | Persistent: community.Persistent, 160 | }) 161 | } 162 | 163 | return cc, nil 164 | } 165 | 166 | func (p *CommunitiesPersister) CreatePersistentCommunity( 167 | ctx context.Context, 168 | community string, 169 | password string, 170 | ) (*persisters.Community, error) { 171 | p.lock.Lock() 172 | defer p.lock.Unlock() 173 | 174 | hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) 175 | if err != nil { 176 | return nil, err 177 | } 178 | 179 | var c *Community 180 | for _, candidate := range p.communities { 181 | if candidate.ID == community { 182 | c = candidate 183 | 184 | break 185 | } 186 | } 187 | 188 | if c != nil { 189 | return nil, ErrUniqueConstraintViolation 190 | } 191 | 192 | c = &Community{ 193 | password: string(hashedPassword), 194 | Community: &persisters.Community{ 195 | ID: community, 196 | Clients: 0, 197 | Persistent: true, 198 | }, 199 | } 200 | 201 | p.communities = append(p.communities, c) 202 | 203 | cc := &persisters.Community{ 204 | ID: c.ID, 205 | Clients: c.Clients, 206 | Persistent: c.Persistent, 207 | } 208 | 209 | return cc, nil 210 | } 211 | 212 | func (p *CommunitiesPersister) DeleteCommunity( 213 | ctx context.Context, 214 | community string, 215 | ) error { 216 | p.lock.Lock() 217 | defer p.lock.Unlock() 218 | 219 | newCommunities := []*Community{} 220 | n := 0 221 | for _, candidate := range p.communities { 222 | if candidate.ID != community { 223 | newCommunities = append(newCommunities, candidate) 224 | 225 | continue 226 | } 227 | 228 | n++ 229 | } 230 | 231 | p.communities = newCommunities 232 | 233 | if n <= 0 { 234 | return sql.ErrNoRows 235 | } 236 | 237 | return nil 238 | } 239 | -------------------------------------------------------------------------------- /internal/persisters/psql/communities.go: -------------------------------------------------------------------------------- 1 | package psql 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | 7 | _ "github.com/lib/pq" 8 | "github.com/pojntfx/go-auth-utils/pkg/authn" 9 | "github.com/pojntfx/weron/internal/db/psql/migrations/communities" 10 | models "github.com/pojntfx/weron/internal/db/psql/models/communities" 11 | "github.com/pojntfx/weron/internal/persisters" 12 | migrate "github.com/rubenv/sql-migrate" 13 | "github.com/volatiletech/sqlboiler/v4/boil" 14 | "github.com/volatiletech/sqlboiler/v4/queries/qm" 15 | "golang.org/x/crypto/bcrypt" 16 | ) 17 | 18 | //go:generate sqlboiler psql -o ../../../internal/db/psql/models/communities -c ../../../configs/sqlboiler/communities.yaml 19 | //go:generate go-bindata -pkg communities -o ../../../internal/db/psql/migrations/communities/migrations.go ../../../db/psql/migrations/communities 20 | 21 | type CommunitiesPersister struct { 22 | db *sql.DB 23 | } 24 | 25 | func NewCommunitiesPersister() *CommunitiesPersister { 26 | return &CommunitiesPersister{} 27 | } 28 | 29 | func (p *CommunitiesPersister) Open(dbURL string) error { 30 | // Connect to the DB 31 | db, err := sql.Open("postgres", dbURL) 32 | if err != nil { 33 | return err 34 | } 35 | 36 | // Configure the db 37 | db.SetMaxOpenConns(1) // Prevent "database locked" errors 38 | 39 | // Run migrations 40 | if _, err := migrate.Exec(db, "postgres", migrate.AssetMigrationSource{ 41 | Asset: communities.Asset, 42 | AssetDir: communities.AssetDir, 43 | Dir: "../../../db/psql/migrations/communities", 44 | }, migrate.Up); err != nil { 45 | return err 46 | } 47 | 48 | p.db = db 49 | 50 | return nil 51 | } 52 | 53 | func (p *CommunitiesPersister) AddClientsToCommunity( 54 | ctx context.Context, 55 | community string, 56 | password string, 57 | upsert bool, 58 | ) error { 59 | tx, err := p.db.BeginTx(ctx, &sql.TxOptions{ 60 | Isolation: sql.LevelSerializable, 61 | }) 62 | if err != nil { 63 | return err 64 | } 65 | 66 | hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) 67 | if err != nil { 68 | if err := tx.Rollback(); err != nil { 69 | return err 70 | } 71 | 72 | return err 73 | } 74 | 75 | c, err := models.FindCommunity(ctx, tx, community) 76 | if err != nil { 77 | if err == sql.ErrNoRows { 78 | if !upsert { 79 | if err := tx.Rollback(); err != nil { 80 | return err 81 | } 82 | 83 | return persisters.ErrEphemeralCommunitiesDisabled 84 | } 85 | 86 | c = &models.Community{ 87 | ID: community, 88 | Password: string(hashedPassword), 89 | Clients: 1, 90 | Persistent: false, 91 | } 92 | 93 | if err := c.Insert(ctx, tx, boil.Infer()); err != nil { 94 | if err := tx.Rollback(); err != nil { 95 | return err 96 | } 97 | 98 | return err 99 | } 100 | 101 | return tx.Commit() 102 | } else { 103 | if err := tx.Rollback(); err != nil { 104 | return err 105 | } 106 | 107 | return err 108 | } 109 | } 110 | 111 | if bcrypt.CompareHashAndPassword([]byte(c.Password), []byte(password)) != nil { 112 | if err := tx.Rollback(); err != nil { 113 | return err 114 | } 115 | 116 | return authn.ErrWrongPassword 117 | } 118 | 119 | c.Clients += 1 120 | 121 | if _, err := c.Update(ctx, tx, boil.Infer()); err != nil { 122 | if err := tx.Rollback(); err != nil { 123 | return err 124 | } 125 | 126 | return err 127 | } 128 | 129 | return tx.Commit() 130 | } 131 | 132 | func (p *CommunitiesPersister) RemoveClientFromCommunity( 133 | ctx context.Context, 134 | community string, 135 | ) error { 136 | tx, err := p.db.BeginTx(ctx, nil) 137 | if err != nil { 138 | return err 139 | } 140 | 141 | c, err := models.FindCommunity(ctx, tx, community) 142 | if err != nil { 143 | if err == sql.ErrNoRows { 144 | if err := tx.Rollback(); err != nil { 145 | return err 146 | } 147 | 148 | return nil // No-op 149 | } 150 | 151 | if err := tx.Rollback(); err != nil { 152 | return err 153 | } 154 | 155 | return err 156 | } 157 | 158 | c.Clients -= 1 159 | if c.Clients <= 0 { 160 | if !c.Persistent { 161 | if _, err := c.Delete(ctx, tx); err != nil { 162 | if err := tx.Rollback(); err != nil { 163 | return err 164 | } 165 | 166 | return err 167 | } 168 | 169 | return tx.Commit() 170 | } 171 | 172 | c.Clients = 0 173 | } 174 | 175 | if _, err := c.Update(ctx, tx, boil.Infer()); err != nil { 176 | if err := tx.Rollback(); err != nil { 177 | return err 178 | } 179 | 180 | return err 181 | } 182 | 183 | return tx.Commit() 184 | } 185 | 186 | func (p *CommunitiesPersister) Cleanup( 187 | ctx context.Context, 188 | ) error { 189 | tx, err := p.db.BeginTx(ctx, nil) 190 | if err != nil { 191 | return err 192 | } 193 | 194 | // Delete all ephemeral communities 195 | if _, err := models.Communities(qm.Where(models.CommunityColumns.Persistent+"= ?", false)).DeleteAll(ctx, tx); err != nil { 196 | if err := tx.Rollback(); err != nil { 197 | return err 198 | } 199 | 200 | return err 201 | } 202 | 203 | // Set client count to 0 for all persistent communities 204 | if _, err := models.Communities(qm.Where(models.CommunityColumns.Persistent+"= ?", true)).UpdateAll(ctx, tx, models.M{models.CommunityColumns.Clients: 0}); err != nil { 205 | if err := tx.Rollback(); err != nil { 206 | return err 207 | } 208 | 209 | return err 210 | } 211 | 212 | return tx.Commit() 213 | } 214 | 215 | func (p *CommunitiesPersister) GetCommunities( 216 | ctx context.Context, 217 | ) ([]persisters.Community, error) { 218 | c, err := models.Communities().All(ctx, p.db) 219 | if err != nil { 220 | return nil, err 221 | } 222 | 223 | cc := []persisters.Community{} 224 | for _, community := range c { 225 | cc = append(cc, persisters.Community{ 226 | ID: community.ID, 227 | Clients: community.Clients, 228 | Persistent: community.Persistent, 229 | }) 230 | } 231 | 232 | return cc, nil 233 | } 234 | 235 | func (p *CommunitiesPersister) CreatePersistentCommunity( 236 | ctx context.Context, 237 | community string, 238 | password string, 239 | ) (*persisters.Community, error) { 240 | hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) 241 | if err != nil { 242 | return nil, err 243 | } 244 | 245 | c := &models.Community{ 246 | ID: community, 247 | Password: string(hashedPassword), 248 | Clients: 0, 249 | Persistent: true, 250 | } 251 | 252 | if err := c.Insert(ctx, p.db, boil.Infer()); err != nil { 253 | return nil, err 254 | } 255 | 256 | cc := &persisters.Community{ 257 | ID: c.ID, 258 | Clients: c.Clients, 259 | Persistent: c.Persistent, 260 | } 261 | 262 | return cc, nil 263 | } 264 | 265 | func (p *CommunitiesPersister) DeleteCommunity( 266 | ctx context.Context, 267 | community string, 268 | ) error { 269 | n, err := models.Communities( 270 | qm.Where(models.CommunityColumns.ID+"= ?", community), 271 | ).DeleteAll(ctx, p.db) 272 | if err != nil { 273 | return err 274 | } 275 | 276 | if n <= 0 { 277 | return sql.ErrNoRows 278 | } 279 | 280 | return nil 281 | } 282 | -------------------------------------------------------------------------------- /pkg/api/webrtc/v1/exchange.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | // Greeting is a claim for a set of IDs 4 | type Greeting struct { 5 | Message 6 | IDs map[string]struct{} `json:"ids"` // IDs to claim one of 7 | Timestamp int64 `json:"timestamp"` // Timestamp to resolve conflicts 8 | } 9 | 10 | func NewGreeting(id map[string]struct{}, timestamp int64) *Greeting { 11 | return &Greeting{ 12 | Message: Message{ 13 | Type: TypeGreeting, 14 | }, 15 | IDs: id, 16 | Timestamp: timestamp, 17 | } 18 | } 19 | 20 | // Kick notifies peers that an ID has already been claimed 21 | type Kick struct { 22 | Message 23 | ID string `json:"id"` // ID which has already been claimed 24 | } 25 | 26 | func NewKick(id string) *Kick { 27 | return &Kick{ 28 | Message: Message{ 29 | Type: TypeKick, 30 | }, 31 | ID: id, 32 | } 33 | } 34 | 35 | // Backoff asks a peer to back off from claiming IDs 36 | type Backoff struct { 37 | Message 38 | } 39 | 40 | func NewBackoff() *Backoff { 41 | return &Backoff{ 42 | Message: Message{ 43 | Type: TypeBackoff, 44 | }, 45 | } 46 | } 47 | 48 | // Claimed notifies a peer that an ID has already been claimed 49 | type Claimed struct { 50 | Message 51 | ID string `json:"id"` // ID which has already been claimed 52 | } 53 | 54 | func NewClaimed(id string) *Claimed { 55 | return &Claimed{ 56 | Message: Message{ 57 | Type: TypeClaimed, 58 | }, 59 | ID: id, 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /pkg/api/webrtc/v1/message.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | // Message is a generic message container 4 | type Message struct { 5 | Type string `json:"type"` // Message type to unmarshal to 6 | } 7 | -------------------------------------------------------------------------------- /pkg/api/webrtc/v1/types.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | const ( 4 | TypeGreeting = "greeting" // Greeting is a claim for a set of IDs 5 | TypeKick = "kick" // Kick notifies peers that an ID has already been claimed 6 | TypeBackoff = "backoff" // Backoff asks a peer to back off from claiming IDs 7 | TypeClaimed = "claimed" // Claimed notifies a peer that an ID has already been claimed 8 | ) 9 | -------------------------------------------------------------------------------- /pkg/services/channels.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | const ( 4 | weronPrefix = "weron/" // General channel prefix 5 | 6 | EthernetPrimary = weronPrefix + "ethernet/primary" // Primary channel for Ethernet 7 | 8 | IPPrimary = weronPrefix + "ip/primary" // Primary channel for IP 9 | IPID = weronPrefix + "ip/id" // ID negotiation channel for IP 10 | 11 | ChatPrimary = weronPrefix + "chat/primary" // Primary channel for chat 12 | ChatID = weronPrefix + "chat/id" // ID negotiation channel for chat 13 | 14 | ThroughputPrimary = weronPrefix + "throughput/primary" // Primary channel for throughput measurements 15 | LatencyPrimary = weronPrefix + "latency/primary" // Primary channel for latency measurements 16 | 17 | IDGeneral = weronPrefix + "id/id" // General channel for ID negotiation 18 | ) 19 | -------------------------------------------------------------------------------- /pkg/wrtcchat/wrtcchat.go: -------------------------------------------------------------------------------- 1 | package wrtcchat 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "strings" 7 | 8 | "github.com/pojntfx/weron/pkg/wrtcconn" 9 | "github.com/rs/zerolog/log" 10 | "github.com/teivah/broadcast" 11 | ) 12 | 13 | // Message is a chat message 14 | type Message struct { 15 | PeerID string // ID of the peer that sent the message 16 | ChannelID string // Channel to which the message has been sent 17 | Body []byte // Content of the message 18 | } 19 | 20 | // AdapterConfig configures the adapter 21 | type AdapterConfig struct { 22 | *wrtcconn.NamedAdapterConfig 23 | OnSignalerConnect func(string) // Handler to be called when the adapter has connected to the signaler 24 | OnPeerConnect func(peerID string, channelID string) // Handler to be called when the adapter has connected to a peer 25 | OnPeerDisconnected func(peerID string, channelID string) // Handler to be called when the adapter has disconnected from a peer 26 | OnMessage func(Message) // Handler to be called when the adapter has received a message 27 | Channels []string // Channels to join 28 | } 29 | 30 | // Adapter provides a chat service 31 | type Adapter struct { 32 | signaler string 33 | key string 34 | ice []string 35 | config *AdapterConfig 36 | ctx context.Context 37 | 38 | cancel context.CancelFunc 39 | adapter *wrtcconn.NamedAdapter 40 | 41 | ids chan string 42 | input *broadcast.Relay[[]byte] 43 | } 44 | 45 | // NewAdapter creates the adapter 46 | func NewAdapter( 47 | signaler string, 48 | key string, 49 | ice []string, 50 | config *AdapterConfig, 51 | ctx context.Context, 52 | ) *Adapter { 53 | ictx, cancel := context.WithCancel(ctx) 54 | 55 | if config == nil { 56 | config = &AdapterConfig{} 57 | } 58 | 59 | return &Adapter{ 60 | signaler: signaler, 61 | key: key, 62 | ice: ice, 63 | config: config, 64 | ctx: ictx, 65 | 66 | cancel: cancel, 67 | 68 | ids: make(chan string), 69 | input: broadcast.NewRelay[[]byte](), 70 | } 71 | } 72 | 73 | // Open connects the adapter to the signaler 74 | func (a *Adapter) Open() error { 75 | log.Trace().Msg("Opening adapter") 76 | 77 | a.adapter = wrtcconn.NewNamedAdapter( 78 | a.signaler, 79 | a.key, 80 | strings.Split(strings.Join(a.ice, ","), ","), 81 | a.config.Channels, 82 | a.config.NamedAdapterConfig, 83 | a.ctx, 84 | ) 85 | 86 | var err error 87 | a.ids, err = a.adapter.Open() 88 | if err != nil { 89 | return err 90 | } 91 | 92 | return err 93 | } 94 | 95 | // Close disconnects the adapter from the signaler 96 | func (a *Adapter) Close() error { 97 | log.Trace().Msg("Closing adapter") 98 | 99 | a.input.Close() 100 | 101 | return a.adapter.Close() 102 | } 103 | 104 | // Wait starts the transmission loop 105 | func (a *Adapter) Wait() error { 106 | for { 107 | select { 108 | case <-a.ctx.Done(): 109 | log.Trace().Err(a.ctx.Err()).Msg("Context cancelled") 110 | 111 | if err := a.ctx.Err(); err != context.Canceled { 112 | return err 113 | } 114 | 115 | return nil 116 | case id := <-a.ids: 117 | log.Debug().Str("id", id).Msg("Connected to signaler") 118 | 119 | if a.config.OnSignalerConnect != nil { 120 | a.config.OnSignalerConnect(id) 121 | } 122 | case peer := <-a.adapter.Accept(): 123 | log.Debug().Str("channelID", peer.ChannelID).Str("peerID", peer.PeerID).Msg("Connected to peer") 124 | 125 | l := a.input.Listener(0) 126 | 127 | if a.config.OnPeerConnect != nil { 128 | a.config.OnPeerConnect(peer.PeerID, peer.ChannelID) 129 | } 130 | 131 | go func() { 132 | defer func() { 133 | log.Debug().Str("channelID", peer.ChannelID).Str("peerID", peer.PeerID).Msg("Disconnected from peer") 134 | 135 | if a.config.OnPeerDisconnected != nil { 136 | a.config.OnPeerDisconnected(peer.PeerID, peer.ChannelID) 137 | } 138 | 139 | l.Close() 140 | }() 141 | 142 | reader := bufio.NewScanner(peer.Conn) 143 | for reader.Scan() { 144 | body := reader.Bytes() 145 | 146 | log.Trace().Bytes("body", body).Msg("Received message") 147 | 148 | a.config.OnMessage( 149 | Message{ 150 | PeerID: peer.PeerID, 151 | ChannelID: peer.ChannelID, 152 | Body: body, 153 | }, 154 | ) 155 | } 156 | }() 157 | 158 | go func() { 159 | for msg := range l.Ch() { 160 | if _, err := peer.Conn.Write(msg); err != nil { 161 | log.Debug(). 162 | Err(err). 163 | Str("channelID", peer.ChannelID). 164 | Str("peerID", peer.PeerID). 165 | Msg("Could not write to peer, stopping") 166 | 167 | return 168 | } 169 | } 170 | }() 171 | } 172 | } 173 | } 174 | 175 | // SendMessage sends a message to all peers 176 | func (a *Adapter) SendMessage(body []byte) { 177 | log.Trace().Bytes("body", body).Msg("Sending message") 178 | 179 | a.input.NotifyCtx(a.ctx, body) 180 | } 181 | -------------------------------------------------------------------------------- /pkg/wrtcconn/adapter.go: -------------------------------------------------------------------------------- 1 | package wrtcconn 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "io" 7 | "net/url" 8 | "strings" 9 | "sync" 10 | "time" 11 | 12 | "github.com/google/uuid" 13 | "github.com/gorilla/websocket" 14 | "github.com/pion/webrtc/v3" 15 | websocketapi "github.com/pojntfx/weron/internal/api/websocket" 16 | "github.com/pojntfx/weron/internal/encryption" 17 | "github.com/rs/zerolog/log" 18 | ) 19 | 20 | var ( 21 | ErrInvalidTURNServerAddr = errors.New("invalid TURN server address") // The specified TURN server address is invalid 22 | ErrMissingTURNCredentials = errors.New("missing TURN server credentials") // The specified TURN server is missing credentials 23 | ErrMissingForcedTURNServer = errors.New("TURN is forced, but no TURN server has been configured") // All connections must use TURN, but no TURN server has been configured 24 | ) 25 | 26 | type peer struct { 27 | conn *webrtc.PeerConnection 28 | candidates chan webrtc.ICECandidateInit 29 | channels map[string]*webrtc.DataChannel 30 | iid string 31 | } 32 | 33 | // Peer is a connected remote adapter 34 | type Peer struct { 35 | PeerID string // ID of the peer 36 | ChannelID string // Channel on which the peer is connected to 37 | Conn io.ReadWriteCloser // Underlying connection to send/receive on 38 | } 39 | 40 | // AdapterConfig configures the adapter 41 | type AdapterConfig struct { 42 | Timeout time.Duration // Time to wait before retrying to connect to the signaler 43 | ID string // ID to claim without conflict resolution (default is UUID) 44 | ForceRelay bool // Whether to block P2P connections 45 | OnSignalerReconnect func() // Handler to be called when the adapter has reconnected to the signaler 46 | } 47 | 48 | // NamedAdapter provides a connection service without name conflict prevention 49 | type Adapter struct { 50 | signaler string 51 | key string 52 | ice []string 53 | channels []string 54 | config *AdapterConfig 55 | ctx context.Context 56 | 57 | cancel context.CancelFunc 58 | done bool 59 | doneSync sync.Mutex 60 | lines chan []byte 61 | 62 | peers chan *Peer 63 | 64 | api *webrtc.API 65 | } 66 | 67 | // NewAdapter creates the adapter 68 | func NewAdapter( 69 | signaler string, 70 | key string, 71 | ice []string, 72 | channels []string, 73 | config *AdapterConfig, 74 | ctx context.Context, 75 | ) *Adapter { 76 | ictx, cancel := context.WithCancel(ctx) 77 | 78 | if config == nil { 79 | config = &AdapterConfig{ 80 | Timeout: time.Second * 10, 81 | ID: "", 82 | ForceRelay: false, 83 | } 84 | } 85 | 86 | return &Adapter{ 87 | signaler: signaler, 88 | key: key, 89 | ice: ice, 90 | channels: channels, 91 | config: config, 92 | ctx: ictx, 93 | 94 | cancel: cancel, 95 | peers: make(chan *Peer), 96 | lines: make(chan []byte), 97 | } 98 | } 99 | 100 | func (a *Adapter) sendLine(line []byte) { 101 | a.doneSync.Lock() 102 | defer a.doneSync.Unlock() 103 | 104 | if a.done { 105 | return 106 | } 107 | 108 | a.lines <- line 109 | } 110 | 111 | // Open connects the adapter to the signaler 112 | func (a *Adapter) Open() (chan string, error) { 113 | settingEngine := webrtc.SettingEngine{} 114 | settingEngine.DetachDataChannels() 115 | a.api = webrtc.NewAPI(webrtc.WithSettingEngine(settingEngine)) 116 | 117 | ids := make(chan string) 118 | 119 | u, err := url.Parse(a.signaler) 120 | if err != nil { 121 | return ids, err 122 | } 123 | 124 | community := u.Query().Get("community") 125 | 126 | iceServers := []webrtc.ICEServer{} 127 | 128 | containsTURN := false 129 | for _, ice := range a.ice { 130 | // Skip empty server configs 131 | if strings.TrimSpace(ice) == "" { 132 | log.Trace().Msg("Skipping empty server config") 133 | 134 | continue 135 | } 136 | 137 | if strings.Contains(ice, "stun:") { 138 | iceServers = append(iceServers, webrtc.ICEServer{ 139 | URLs: []string{ice}, 140 | }) 141 | } else { 142 | addrParts := strings.Split(ice, "@") 143 | if len(addrParts) < 2 { 144 | return ids, ErrInvalidTURNServerAddr 145 | } 146 | 147 | authParts := strings.Split(addrParts[0], ":") 148 | if len(addrParts) < 2 { 149 | return ids, ErrMissingTURNCredentials 150 | } 151 | 152 | iceServers = append(iceServers, webrtc.ICEServer{ 153 | URLs: []string{addrParts[1]}, 154 | Username: authParts[0], 155 | Credential: authParts[1], 156 | CredentialType: webrtc.ICECredentialTypePassword, 157 | }) 158 | 159 | containsTURN = true 160 | } 161 | } 162 | 163 | if a.config.ForceRelay && !containsTURN { 164 | return ids, ErrMissingForcedTURNServer 165 | } 166 | 167 | go func() { 168 | for { 169 | if a.done { 170 | return 171 | } 172 | 173 | peers := map[string]*peer{} 174 | var peerLock sync.Mutex 175 | 176 | func() { 177 | defer func() { 178 | if err := recover(); err != nil { 179 | log.Debug().Str("address", u.String()).Err(err.(error)).Msg("Closed connection to signaler (wrong username or password?)") 180 | } 181 | 182 | log.Debug().Str("address", u.String()).Dur("timeout", a.config.Timeout).Msg("Reconnecting to signaler") 183 | 184 | if a.config.OnSignalerReconnect != nil { 185 | a.config.OnSignalerReconnect() 186 | } 187 | 188 | time.Sleep(a.config.Timeout) 189 | }() 190 | 191 | ctx, cancel := context.WithTimeout(a.ctx, a.config.Timeout) 192 | defer cancel() 193 | 194 | conn, _, err := websocket.DefaultDialer.DialContext(ctx, u.String(), nil) 195 | if err != nil { 196 | panic(err) 197 | } 198 | 199 | defer func() { 200 | log.Debug().Str("address", u.String()).Msg("Disconnected from signaler") 201 | 202 | if err := conn.Close(); err != nil { 203 | panic(err) 204 | } 205 | 206 | peerLock.Lock() 207 | defer peerLock.Unlock() 208 | 209 | for _, peer := range peers { 210 | for _, channel := range peer.channels { 211 | if err := channel.Close(); err != nil { 212 | panic(err) 213 | } 214 | } 215 | 216 | if err := peer.conn.Close(); err != nil { 217 | panic(err) 218 | } 219 | 220 | close(peer.candidates) 221 | } 222 | }() 223 | 224 | if err := conn.SetReadDeadline(time.Now().Add(a.config.Timeout)); err != nil { 225 | panic(err) 226 | } 227 | conn.SetPongHandler(func(string) error { 228 | return conn.SetReadDeadline(time.Now().Add(a.config.Timeout)) 229 | }) 230 | 231 | log.Debug().Str("address", u.String()).Msg("Connected to signaler") 232 | 233 | inputs := make(chan []byte) 234 | errs := make(chan error) 235 | go func() { 236 | defer func() { 237 | close(inputs) 238 | close(errs) 239 | }() 240 | 241 | for { 242 | _, p, err := conn.ReadMessage() 243 | if err != nil { 244 | errs <- err 245 | 246 | return 247 | } 248 | 249 | inputs <- p 250 | } 251 | }() 252 | 253 | id := a.config.ID 254 | if strings.TrimSpace(id) == "" { 255 | id = uuid.New().String() 256 | } 257 | 258 | ids <- id 259 | 260 | go func() { 261 | p, err := json.Marshal(websocketapi.NewIntroduction(id)) 262 | if err != nil { 263 | errs <- err 264 | 265 | return 266 | } 267 | 268 | a.sendLine(p) 269 | 270 | log.Debug().Str("address", u.String()).Str("id", id).Msg("Introduced to signaler") 271 | }() 272 | 273 | pings := time.NewTicker(a.config.Timeout / 2) 274 | defer pings.Stop() 275 | 276 | for { 277 | select { 278 | case err := <-errs: 279 | panic(err) 280 | case input := <-inputs: 281 | input, err = encryption.Decrypt(input, []byte(a.key)) 282 | if err != nil { 283 | log.Debug(). 284 | Str("address", conn.RemoteAddr().String()). 285 | Int("len", len(input)). 286 | Str("community", community). 287 | Str("id", id).Msg("Could not decrypt message from signaler, continuing") 288 | 289 | continue 290 | } 291 | 292 | log.Trace(). 293 | Str("address", conn.RemoteAddr().String()). 294 | Int("len", len(input)). 295 | Str("community", community). 296 | Str("id", id).Msg("Received message from signaler") 297 | 298 | var message websocketapi.Message 299 | if err := json.Unmarshal(input, &message); err != nil { 300 | log.Debug(). 301 | Str("address", conn.RemoteAddr().String()). 302 | Str("community", community). 303 | Str("id", id).Msg("Could not unmarshal message from signaler, continuing") 304 | 305 | continue 306 | } 307 | 308 | switch message.Type { 309 | case websocketapi.TypeIntroduction: 310 | var introduction websocketapi.Introduction 311 | if err := json.Unmarshal(input, &introduction); err != nil { 312 | log.Debug(). 313 | Str("address", conn.RemoteAddr().String()). 314 | Str("community", community). 315 | Str("id", id).Msg("Could not unmarshal introduction from signaler, continuing") 316 | 317 | continue 318 | } 319 | 320 | log.Debug(). 321 | Str("address", conn.RemoteAddr().String()). 322 | Str("community", community). 323 | Str("id", id).Msg("Received introduction from signaler") 324 | 325 | iid := uuid.NewString() 326 | 327 | transportPolicy := webrtc.ICETransportPolicyAll 328 | if a.config.ForceRelay { 329 | transportPolicy = webrtc.ICETransportPolicyRelay 330 | } 331 | 332 | c, err := a.api.NewPeerConnection(webrtc.Configuration{ 333 | ICEServers: iceServers, 334 | ICETransportPolicy: transportPolicy, 335 | }) 336 | if err != nil { 337 | panic(err) 338 | } 339 | 340 | c.OnConnectionStateChange(func(pcs webrtc.PeerConnectionState) { 341 | if pcs == webrtc.PeerConnectionStateDisconnected { 342 | log.Debug().Str("peerID", introduction.From).Msg("Disconnected from peer") 343 | 344 | peerLock.Lock() 345 | defer peerLock.Unlock() 346 | 347 | c, ok := peers[introduction.From] 348 | 349 | if !ok { 350 | log.Debug().Str("peerID", introduction.From).Msg("Could not find connection for peer, continuing") 351 | 352 | return 353 | } 354 | 355 | if c.iid != iid { 356 | log.Debug().Str("peerID", introduction.From).Msg("Peer already rejoined, not disconnecting") 357 | 358 | return 359 | } 360 | 361 | for _, channel := range c.channels { 362 | if err := channel.Close(); err != nil { 363 | panic(err) 364 | } 365 | } 366 | 367 | if err := c.conn.Close(); err != nil { 368 | panic(err) 369 | } 370 | 371 | close(c.candidates) 372 | 373 | delete(peers, introduction.From) 374 | } 375 | }) 376 | 377 | c.OnICECandidate(func(i *webrtc.ICECandidate) { 378 | if i != nil { 379 | log.Trace(). 380 | Str("address", conn.RemoteAddr().String()). 381 | Str("len", i.String()). 382 | Str("community", community). 383 | Str("id", id).Msg("Created ICE candidate") 384 | 385 | p, err := json.Marshal(websocketapi.NewCandidate(id, introduction.From, []byte(i.ToJSON().Candidate))) 386 | if err != nil { 387 | panic(err) 388 | } 389 | 390 | go func() { 391 | a.sendLine(p) 392 | 393 | log.Debug(). 394 | Str("address", conn.RemoteAddr().String()). 395 | Str("community", community). 396 | Str("id", id). 397 | Str("client", introduction.From). 398 | Msg("Sent ICE candidate to signaler") 399 | }() 400 | } 401 | }) 402 | 403 | for i, channelID := range a.channels { 404 | // Skip empty channel IDs 405 | if strings.TrimSpace(channelID) == "" { 406 | continue 407 | } 408 | 409 | dc, err := c.CreateDataChannel(channelID, nil) 410 | if err != nil { 411 | panic(err) 412 | } 413 | 414 | log.Trace(). 415 | Str("address", conn.RemoteAddr().String()). 416 | Str("community", community). 417 | Str("channelID", channelID). 418 | Msg("Created data channel") 419 | 420 | dc.OnOpen(func() { 421 | log.Debug(). 422 | Str("label", dc.Label()). 423 | Str("peer", introduction.From). 424 | Msg("Connected to channel") 425 | 426 | c, err := dc.Detach() 427 | if err != nil { 428 | panic(err) 429 | } 430 | 431 | for _, channel := range a.channels { 432 | if dc.Label() == channel { 433 | peerLock.Lock() 434 | peers[introduction.From].channels[dc.Label()] = dc 435 | a.peers <- &Peer{introduction.From, dc.Label(), c} 436 | peerLock.Unlock() 437 | 438 | break 439 | } 440 | } 441 | }) 442 | 443 | dc.OnClose(func() { 444 | log.Debug(). 445 | Str("label", dc.Label()). 446 | Str("peer", introduction.From). 447 | Msg("Disconnected from channel") 448 | 449 | peerLock.Lock() 450 | defer peerLock.Unlock() 451 | peer, ok := peers[introduction.From] 452 | if !ok { 453 | log.Debug().Str("peerID", introduction.From).Msg("Could not find peer, continuing") 454 | 455 | return 456 | } 457 | 458 | channel, ok := peer.channels[dc.Label()] 459 | if !ok { 460 | log.Debug(). 461 | Str("peerID", introduction.From). 462 | Str("channelID", dc.Label()). 463 | Msg("Could not find channel, continuing") 464 | 465 | return 466 | } 467 | 468 | if err := channel.Close(); err != nil { 469 | panic(err) 470 | } 471 | 472 | delete(peers[introduction.From].channels, dc.Label()) 473 | }) 474 | 475 | if i == 0 { 476 | o, err := c.CreateOffer(nil) 477 | if err != nil { 478 | panic(err) 479 | } 480 | 481 | if err := c.SetLocalDescription(o); err != nil { 482 | panic(err) 483 | } 484 | 485 | oj, err := json.Marshal(o) 486 | if err != nil { 487 | panic(err) 488 | } 489 | 490 | p, err := json.Marshal(websocketapi.NewOffer(id, introduction.From, oj)) 491 | if err != nil { 492 | panic(err) 493 | } 494 | 495 | pr := &peer{c, make(chan webrtc.ICECandidateInit), map[string]*webrtc.DataChannel{ 496 | dc.Label(): dc, 497 | }, iid} 498 | 499 | peerLock.Lock() 500 | old, ok := peers[introduction.From] 501 | if ok { 502 | // Disconnect the old peer 503 | log.Debug().Str("peerID", introduction.From).Msg("Disconnected from peer") 504 | 505 | for _, channel := range old.channels { 506 | if err := channel.Close(); err != nil { 507 | panic(err) 508 | } 509 | } 510 | 511 | if err := old.conn.Close(); err != nil { 512 | panic(err) 513 | } 514 | 515 | close(old.candidates) 516 | } 517 | peers[introduction.From] = pr 518 | peerLock.Unlock() 519 | 520 | go func() { 521 | a.sendLine(p) 522 | 523 | log.Debug(). 524 | Str("address", conn.RemoteAddr().String()). 525 | Str("community", community). 526 | Str("id", id). 527 | Str("client", introduction.From). 528 | Msg("Sent offer to signaler") 529 | }() 530 | } 531 | } 532 | 533 | case websocketapi.TypeOffer: 534 | var offer websocketapi.Exchange 535 | if err := json.Unmarshal(input, &offer); err != nil { 536 | log.Debug(). 537 | Str("address", conn.RemoteAddr().String()). 538 | Str("community", community). 539 | Str("id", id).Msg("Could not unmarshal offer from signaler, continuing") 540 | 541 | continue 542 | } 543 | 544 | if offer.To != id { 545 | log.Trace(). 546 | Str("address", conn.RemoteAddr().String()). 547 | Str("community", community). 548 | Str("id", id).Msg("Discarding offer from signaler because it is not intended for this client") 549 | 550 | continue 551 | } 552 | 553 | log.Debug(). 554 | Str("address", conn.RemoteAddr().String()). 555 | Str("community", community). 556 | Str("id", id).Msg("Received offer from signaler") 557 | 558 | iid := uuid.NewString() 559 | 560 | transportPolicy := webrtc.ICETransportPolicyAll 561 | if a.config.ForceRelay { 562 | transportPolicy = webrtc.ICETransportPolicyRelay 563 | } 564 | 565 | c, err := a.api.NewPeerConnection(webrtc.Configuration{ 566 | ICEServers: iceServers, 567 | ICETransportPolicy: transportPolicy, 568 | }) 569 | if err != nil { 570 | panic(err) 571 | } 572 | 573 | c.OnConnectionStateChange(func(pcs webrtc.PeerConnectionState) { 574 | if pcs == webrtc.PeerConnectionStateDisconnected { 575 | log.Debug().Str("peerID", offer.From).Msg("Disconnected from peer") 576 | 577 | peerLock.Lock() 578 | defer peerLock.Unlock() 579 | 580 | c, ok := peers[offer.From] 581 | if !ok { 582 | log.Debug().Str("peerID", offer.From).Msg("Could not find connection for peer, continuing") 583 | 584 | return 585 | } 586 | 587 | if c.iid != iid { 588 | log.Debug().Str("peerID", offer.From).Msg("Peer already rejoined, not disconnecting") 589 | 590 | return 591 | } 592 | 593 | if err := c.conn.Close(); err != nil { 594 | panic(err) 595 | } 596 | 597 | if err := c.conn.Close(); err != nil { 598 | panic(err) 599 | } 600 | 601 | close(c.candidates) 602 | 603 | delete(peers, offer.From) 604 | } 605 | }) 606 | 607 | c.OnICECandidate(func(i *webrtc.ICECandidate) { 608 | if i != nil { 609 | log.Trace(). 610 | Str("address", conn.RemoteAddr().String()). 611 | Str("len", i.String()). 612 | Str("community", community). 613 | Str("id", id).Msg("Created ICE candidate") 614 | 615 | p, err := json.Marshal(websocketapi.NewCandidate(id, offer.From, []byte(i.ToJSON().Candidate))) 616 | if err != nil { 617 | panic(err) 618 | } 619 | 620 | go func() { 621 | a.sendLine(p) 622 | 623 | log.Debug(). 624 | Str("address", conn.RemoteAddr().String()). 625 | Str("community", community). 626 | Str("id", id). 627 | Str("client", offer.From). 628 | Msg("Sent ICE candidate to signaler") 629 | }() 630 | } 631 | }) 632 | 633 | c.OnDataChannel(func(dc *webrtc.DataChannel) { 634 | dc.OnOpen(func() { 635 | log.Debug(). 636 | Str("label", dc.Label()). 637 | Str("peer", offer.From). 638 | Msg("Connected to channel") 639 | 640 | c, err := dc.Detach() 641 | if err != nil { 642 | panic(err) 643 | } 644 | 645 | for _, channel := range a.channels { 646 | if dc.Label() == channel { 647 | peerLock.Lock() 648 | peers[offer.From].channels[dc.Label()] = dc 649 | a.peers <- &Peer{offer.From, dc.Label(), c} 650 | peerLock.Unlock() 651 | 652 | break 653 | } 654 | } 655 | }) 656 | 657 | dc.OnClose(func() { 658 | log.Debug(). 659 | Str("label", dc.Label()). 660 | Str("peer", offer.From). 661 | Msg("Disconnected from channel") 662 | 663 | peerLock.Lock() 664 | defer peerLock.Unlock() 665 | channel, ok := peers[offer.From].channels[dc.Label()] 666 | if !ok { 667 | log.Debug(). 668 | Str("peerID", offer.From). 669 | Str("channelID", dc.Label()). 670 | Msg("Could not find channel, continuing") 671 | 672 | return 673 | } 674 | 675 | if err := channel.Close(); err != nil { 676 | panic(err) 677 | } 678 | 679 | delete(peers[offer.From].channels, dc.Label()) 680 | }) 681 | }) 682 | 683 | var sdp webrtc.SessionDescription 684 | if err := json.Unmarshal(offer.Payload, &sdp); err != nil { 685 | log.Debug(). 686 | Str("address", conn.RemoteAddr().String()). 687 | Str("community", community). 688 | Str("id", id).Msg("Could not unmarshal SDP from signaler, continuing") 689 | 690 | continue 691 | } 692 | 693 | if err := c.SetRemoteDescription(sdp); err != nil { 694 | panic(err) 695 | } 696 | 697 | ans, err := c.CreateAnswer(nil) 698 | if err != nil { 699 | panic(err) 700 | } 701 | 702 | if err := c.SetLocalDescription(ans); err != nil { 703 | panic(err) 704 | } 705 | 706 | aj, err := json.Marshal(ans) 707 | if err != nil { 708 | panic(err) 709 | } 710 | 711 | p, err := json.Marshal(websocketapi.NewAnswer(id, offer.From, aj)) 712 | if err != nil { 713 | panic(err) 714 | } 715 | 716 | peerLock.Lock() 717 | 718 | candidates := make(chan webrtc.ICECandidateInit) 719 | peers[offer.From] = &peer{c, candidates, map[string]*webrtc.DataChannel{}, iid} 720 | 721 | peerLock.Unlock() 722 | 723 | go func() { 724 | for candidate := range candidates { 725 | if err := c.AddICECandidate(candidate); err != nil { 726 | errs <- err 727 | 728 | return 729 | } 730 | 731 | log.Debug(). 732 | Str("address", conn.RemoteAddr().String()). 733 | Str("community", community). 734 | Str("id", id). 735 | Str("peerID", offer.From). 736 | Msg("Added ICE candidate from signaler") 737 | } 738 | }() 739 | 740 | go func() { 741 | a.sendLine(p) 742 | 743 | log.Debug(). 744 | Str("address", conn.RemoteAddr().String()). 745 | Str("community", community). 746 | Str("id", id). 747 | Str("client", offer.From). 748 | Msg("Sent answer to signaler") 749 | }() 750 | case websocketapi.TypeCandidate: 751 | var candidate websocketapi.Exchange 752 | if err := json.Unmarshal(input, &candidate); err != nil { 753 | log.Debug(). 754 | Str("address", conn.RemoteAddr().String()). 755 | Str("community", community). 756 | Str("id", id).Msg("Could not unmarshal candidate from signaler, continuing") 757 | 758 | continue 759 | } 760 | 761 | if candidate.To != id { 762 | log.Trace(). 763 | Str("address", conn.RemoteAddr().String()). 764 | Str("community", community). 765 | Str("id", id).Msg("Discarding candidate from signaler because it is not intended for this client") 766 | 767 | continue 768 | } 769 | 770 | log.Debug(). 771 | Str("address", conn.RemoteAddr().String()). 772 | Str("community", community). 773 | Str("id", id).Msg("Received candidate from signaler") 774 | 775 | peerLock.Lock() 776 | c, ok := peers[candidate.From] 777 | 778 | if !ok { 779 | log.Debug().Str("peerID", candidate.From).Msg("Could not find connection for peer, continuing") 780 | 781 | peerLock.Unlock() 782 | 783 | continue 784 | } 785 | 786 | go func() { 787 | defer func() { 788 | if err := recover(); err != nil { 789 | log.Debug(). 790 | Str("address", conn.RemoteAddr().String()). 791 | Str("community", community). 792 | Str("id", id). 793 | Msg("Gathering candiates has stopped, continuing candidate") 794 | } 795 | }() 796 | 797 | c.candidates <- webrtc.ICECandidateInit{Candidate: string(candidate.Payload)} 798 | }() 799 | 800 | peerLock.Unlock() 801 | case websocketapi.TypeAnswer: 802 | var answer websocketapi.Exchange 803 | if err := json.Unmarshal(input, &answer); err != nil { 804 | log.Debug(). 805 | Str("address", conn.RemoteAddr().String()). 806 | Str("community", community). 807 | Str("id", id).Msg("Could not unmarshal answer from signaler, continuing") 808 | 809 | continue 810 | } 811 | 812 | if answer.To != id { 813 | log.Trace(). 814 | Str("address", conn.RemoteAddr().String()). 815 | Str("community", community). 816 | Str("id", id).Msg("Discarding answer from signaler because it is not intended for this client") 817 | 818 | continue 819 | } 820 | 821 | log.Debug(). 822 | Str("address", conn.RemoteAddr().String()). 823 | Str("community", community). 824 | Str("id", id).Msg("Received answer from signaler") 825 | 826 | peerLock.Lock() 827 | c, ok := peers[answer.From] 828 | peerLock.Unlock() 829 | 830 | if !ok { 831 | log.Debug().Str("peerID", answer.From).Msg("Could not find connection for peer, continuing") 832 | 833 | continue 834 | } 835 | 836 | var sdp webrtc.SessionDescription 837 | if err := json.Unmarshal(answer.Payload, &sdp); err != nil { 838 | log.Debug(). 839 | Str("address", conn.RemoteAddr().String()). 840 | Str("community", community). 841 | Str("id", id).Msg("Could not unmarshal SDP from signaler, continuing") 842 | 843 | continue 844 | } 845 | 846 | if err := c.conn.SetRemoteDescription(sdp); err != nil { 847 | panic(err) 848 | } 849 | 850 | go func() { 851 | for candidate := range c.candidates { 852 | if err := c.conn.AddICECandidate(candidate); err != nil { 853 | errs <- err 854 | 855 | return 856 | } 857 | 858 | log.Debug(). 859 | Str("address", conn.RemoteAddr().String()). 860 | Str("community", community). 861 | Str("id", id). 862 | Str("peerID", answer.From). 863 | Msg("Added ICE candidate from signaler") 864 | } 865 | }() 866 | 867 | log.Debug(). 868 | Str("address", conn.RemoteAddr().String()). 869 | Str("community", community). 870 | Str("id", id). 871 | Str("peerID", answer.From). 872 | Msg("Added answer from signaler") 873 | default: 874 | log.Debug(). 875 | Str("address", conn.RemoteAddr().String()). 876 | Str("community", community). 877 | Str("id", id). 878 | Str("type", message.Type). 879 | Msg("Got message with unknown type from signaler, continuing") 880 | 881 | continue 882 | } 883 | case line := <-a.lines: 884 | line, err = encryption.Encrypt(line, []byte(a.key)) 885 | if err != nil { 886 | panic(err) 887 | } 888 | 889 | log.Trace(). 890 | Str("address", conn.RemoteAddr().String()). 891 | Str("community", community). 892 | Str("id", id). 893 | Int("len", len(line)). 894 | Msg("Sending message to signaler") 895 | 896 | if err := conn.WriteMessage(websocket.TextMessage, line); err != nil { 897 | panic(err) 898 | } 899 | 900 | if err := conn.SetWriteDeadline(time.Now().Add(a.config.Timeout)); err != nil { 901 | panic(err) 902 | } 903 | case <-pings.C: 904 | log.Trace(). 905 | Str("address", conn.RemoteAddr().String()). 906 | Str("community", community). 907 | Str("id", id). 908 | Msg("Sending ping to signaler") 909 | 910 | if err := conn.SetWriteDeadline(time.Now().Add(a.config.Timeout)); err != nil { 911 | panic(err) 912 | } 913 | 914 | if err := conn.WriteMessage(websocket.PingMessage, nil); err != nil { 915 | panic(err) 916 | } 917 | } 918 | } 919 | }() 920 | } 921 | }() 922 | 923 | return ids, nil 924 | } 925 | 926 | // Close disconnects the adapter from the signaler 927 | func (a *Adapter) Close() error { 928 | log.Trace().Msg("Closing adapter") 929 | 930 | a.done = true 931 | 932 | a.cancel() 933 | 934 | close(a.lines) 935 | 936 | return nil 937 | } 938 | 939 | // Accept returns a channel on which peers will be sent when they connect 940 | func (a *Adapter) Accept() chan *Peer { 941 | return a.peers 942 | } 943 | -------------------------------------------------------------------------------- /pkg/wrtcconn/adapter_named.go: -------------------------------------------------------------------------------- 1 | package wrtcconn 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "strings" 7 | "sync" 8 | "time" 9 | 10 | jsoniter "github.com/json-iterator/go" 11 | "github.com/rs/zerolog/log" 12 | 13 | "github.com/mitchellh/mapstructure" 14 | v1 "github.com/pojntfx/weron/pkg/api/webrtc/v1" 15 | "github.com/pojntfx/weron/pkg/services" 16 | ) 17 | 18 | var ( 19 | ErrAllNamesClaimed = errors.New("all available names have been claimed") // All specified usernames have already been claimed by other peers 20 | 21 | json = jsoniter.ConfigCompatibleWithStandardLibrary 22 | ) 23 | 24 | // NamedAdapterConfig configures the adapter 25 | type NamedAdapterConfig struct { 26 | *AdapterConfig 27 | IDChannel string // Channel to use for ID negotiation 28 | Names []string // Names to try and claim one of 29 | Kicks time.Duration // Time to wait for kicks before claiming names 30 | IsIDClaimed func(theirs map[string]struct{}, ours string) bool // Handler to be called when asked to compare own ID with an incoming greeting 31 | } 32 | 33 | // NamedAdapter provides a connection service with name conflict prevention 34 | type NamedAdapter struct { 35 | signaler string 36 | key string 37 | ice []string 38 | channels []string 39 | config *NamedAdapterConfig 40 | ctx context.Context 41 | 42 | cancel context.CancelFunc 43 | adapter *Adapter 44 | ids chan string 45 | names chan string 46 | errs chan error 47 | acceptedPeers chan *Peer 48 | } 49 | 50 | // NewNamedAdapter creates the adapter 51 | func NewNamedAdapter( 52 | signaler string, 53 | key string, 54 | ice []string, 55 | channels []string, 56 | config *NamedAdapterConfig, 57 | ctx context.Context, 58 | ) *NamedAdapter { 59 | ictx, cancel := context.WithCancel(ctx) 60 | 61 | if config == nil { 62 | config = &NamedAdapterConfig{} 63 | } 64 | 65 | if config.IDChannel == "" { 66 | config.IDChannel = services.IDGeneral 67 | } 68 | 69 | if config.IsIDClaimed == nil { 70 | config.IsIDClaimed = func(ids map[string]struct{}, id string) bool { 71 | _, ok := ids[id] 72 | 73 | return ok 74 | } 75 | } 76 | 77 | return &NamedAdapter{ 78 | signaler: signaler, 79 | key: key, 80 | ice: ice, 81 | channels: channels, 82 | config: config, 83 | ctx: ictx, 84 | 85 | cancel: cancel, 86 | ids: make(chan string), 87 | names: make(chan string), 88 | errs: make(chan error), 89 | acceptedPeers: make(chan *Peer), 90 | } 91 | } 92 | 93 | // Open connects the adapter to the signaler 94 | func (a *NamedAdapter) Open() (chan string, error) { 95 | ready := time.NewTimer(a.config.Timeout + a.config.Kicks) 96 | 97 | a.config.AdapterConfig.OnSignalerReconnect = func() { 98 | ready.Stop() 99 | ready.Reset(a.config.Timeout + a.config.Kicks) 100 | } 101 | 102 | a.adapter = NewAdapter( 103 | a.signaler, 104 | a.key, 105 | strings.Split(strings.Join(a.ice, ","), ","), 106 | append([]string{a.config.IDChannel}, a.channels...), 107 | a.config.AdapterConfig, 108 | a.ctx, 109 | ) 110 | 111 | var err error 112 | a.ids, err = a.adapter.Open() 113 | if err != nil { 114 | return nil, err 115 | } 116 | 117 | var candidatesLock sync.Mutex 118 | candidates := map[string]struct{}{} 119 | id := "" 120 | timestamp := time.Now().UnixNano() 121 | 122 | peers := map[string]map[string]*Peer{} 123 | var peersLock sync.Mutex 124 | 125 | namedPeers := make(chan *Peer) 126 | var namedPeersLock sync.Mutex 127 | namedPeersCond := sync.NewCond(&namedPeersLock) 128 | 129 | go func() { 130 | for { 131 | select { 132 | case <-a.ctx.Done(): 133 | return 134 | case sid := <-a.ids: 135 | candidatesLock.Lock() 136 | candidates = map[string]struct{}{} 137 | for _, username := range a.config.Names { 138 | candidates[username] = struct{}{} 139 | } 140 | id = "" 141 | candidatesLock.Unlock() 142 | 143 | log.Debug().Str("id", sid).Msg("Claimed ID") 144 | 145 | ready.Stop() 146 | ready.Reset(a.config.Kicks) 147 | case <-ready.C: 148 | candidatesLock.Lock() 149 | for username := range candidates { 150 | id = username 151 | 152 | break 153 | } 154 | candidates = map[string]struct{}{} 155 | candidatesLock.Unlock() 156 | 157 | if id == "" { 158 | a.errs <- ErrAllNamesClaimed 159 | 160 | return 161 | } 162 | 163 | a.names <- id 164 | namedPeersCond.Broadcast() 165 | 166 | peersLock.Lock() 167 | for _, peer := range peers { 168 | log.Debug().Str("id", id).Msg("Sending claimed") 169 | 170 | d, err := json.Marshal(v1.NewClaimed(id)) 171 | if err != nil { 172 | log.Debug(). 173 | Str("id", id). 174 | Err(err). 175 | Msg("Could not marshal claimed") 176 | 177 | continue 178 | } 179 | 180 | if _, err := peer[a.config.IDChannel].Conn.Write(d); err != nil { 181 | log.Debug(). 182 | Str("channelID", peer[a.config.IDChannel].ChannelID). 183 | Str("peerID", peer[a.config.IDChannel].PeerID). 184 | Msg("Could not write to peer, stopping") 185 | 186 | continue 187 | } 188 | } 189 | peersLock.Unlock() 190 | case peer := <-namedPeers: 191 | go func() { 192 | if id == "" { 193 | namedPeersCond.L.Lock() 194 | namedPeersCond.Wait() 195 | namedPeersCond.L.Unlock() 196 | } 197 | 198 | a.acceptedPeers <- peer 199 | }() 200 | case peer := <-a.adapter.Accept(): 201 | rid := peer.PeerID 202 | 203 | peersLock.Lock() 204 | for candidate, p := range peers { 205 | for _, c := range p { 206 | if c.PeerID == peer.PeerID { 207 | rid = candidate 208 | 209 | break 210 | } 211 | } 212 | } 213 | if _, ok := peers[rid]; !ok { 214 | peers[rid] = map[string]*Peer{} 215 | } 216 | peers[rid][peer.ChannelID] = peer 217 | if rid != peer.PeerID && peer.ChannelID != a.config.IDChannel { 218 | namedPeers <- &Peer{ 219 | PeerID: rid, 220 | ChannelID: peer.ChannelID, 221 | Conn: peer.Conn, 222 | } 223 | } 224 | peersLock.Unlock() 225 | 226 | if peer.ChannelID == a.config.IDChannel { 227 | go func() { 228 | e := json.NewEncoder(peer.Conn) 229 | d := json.NewDecoder(peer.Conn) 230 | 231 | defer func() { 232 | if err := recover(); err != nil { 233 | log.Debug(). 234 | Err(err.(error)). 235 | Str("channelID", peer.ChannelID). 236 | Str("peerID", rid). 237 | Msg("Could not read/write from peer, stopping") 238 | } 239 | 240 | if rid != peer.PeerID { 241 | log.Debug(). 242 | Str("channelID", peer.ChannelID). 243 | Str("peerID", rid). 244 | Msg("Disconnected from peer") 245 | } 246 | 247 | peersLock.Lock() 248 | if _, ok := peers[rid]; !ok { 249 | delete(peers[rid], peer.ChannelID) 250 | 251 | if len(peers[rid]) <= 0 { 252 | delete(peers, rid) 253 | } 254 | } 255 | peersLock.Unlock() 256 | }() 257 | 258 | greet := func() { 259 | log.Debug(). 260 | Str("channelID", peer.ChannelID). 261 | Str("peerID", rid). 262 | Int("candidates", len(candidates)). 263 | Int64("timestamp", timestamp). 264 | Msg("Sending greeting") 265 | 266 | if id == "" { 267 | if err := e.Encode(v1.NewGreeting(candidates, timestamp)); err != nil { 268 | log.Debug(). 269 | Err(err). 270 | Str("channelID", peer.ChannelID). 271 | Str("peerID", rid). 272 | Msg("Could not write greeting to peer, stopping") 273 | 274 | return 275 | } 276 | } else { 277 | if err := e.Encode(v1.NewGreeting(map[string]struct{}{id: {}}, timestamp)); err != nil { 278 | log.Debug(). 279 | Err(err). 280 | Str("channelID", peer.ChannelID). 281 | Str("peerID", rid). 282 | Msg("Could not write greeting to peer, stopping") 283 | 284 | return 285 | } 286 | 287 | log.Debug(). 288 | Str("channelID", peer.ChannelID). 289 | Str("peerID", rid). 290 | Str("id", id). 291 | Msg("Sending claimed") 292 | 293 | if err := e.Encode(v1.NewClaimed(id)); err != nil { 294 | log.Debug(). 295 | Err(err). 296 | Str("channelID", peer.ChannelID). 297 | Str("peerID", rid). 298 | Msg("Could not write claimed to peer, stopping") 299 | 300 | return 301 | } 302 | } 303 | } 304 | 305 | greet() 306 | 307 | l: 308 | for { 309 | var j interface{} 310 | if err := d.Decode(&j); err != nil { 311 | log.Debug(). 312 | Err(err). 313 | Str("channelID", peer.ChannelID). 314 | Str("peerID", rid). 315 | Msg("Could not read from peer, stopping") 316 | 317 | return 318 | } 319 | 320 | var message v1.Message 321 | if err := mapstructure.Decode(j, &message); err != nil { 322 | log.Debug(). 323 | Err(err). 324 | Str("channelID", peer.ChannelID). 325 | Str("peerID", rid). 326 | Msg("Could not decode message from peer, stopping") 327 | 328 | continue 329 | } 330 | 331 | switch message.Type { 332 | case v1.TypeGreeting: 333 | var gng v1.Greeting 334 | if err := mapstructure.Decode(j, &gng); err != nil { 335 | log.Debug(). 336 | Err(err). 337 | Str("channelID", peer.ChannelID). 338 | Str("peerID", rid). 339 | Msg("Could not decode greeting from peer, stopping") 340 | 341 | continue 342 | } 343 | 344 | log.Debug(). 345 | Err(err). 346 | Str("channelID", peer.ChannelID). 347 | Str("peerID", rid). 348 | Msg("Received greeting") 349 | 350 | for gngID := range gng.IDs { 351 | if _, ok := candidates[gngID]; id == "" && ok && timestamp < gng.Timestamp { 352 | log.Debug(). 353 | Str("channelID", peer.ChannelID). 354 | Str("peerID", rid). 355 | Str("id", gngID). 356 | Msg("Sending backoff") 357 | 358 | if err := e.Encode(v1.NewBackoff()); err != nil { 359 | log.Debug(). 360 | Err(err). 361 | Str("channelID", peer.ChannelID). 362 | Str("peerID", rid). 363 | Msg("Could not write backoff to peer, stopping") 364 | 365 | return 366 | } 367 | 368 | continue l 369 | } 370 | } 371 | 372 | if a.config.IsIDClaimed(gng.IDs, id) { 373 | log.Debug(). 374 | Str("channelID", peer.ChannelID). 375 | Str("peerID", rid). 376 | Str("id", id). 377 | Msg("Sending kick") 378 | 379 | if err := e.Encode(v1.NewKick(id)); err != nil { 380 | log.Debug(). 381 | Err(err). 382 | Str("channelID", peer.ChannelID). 383 | Str("peerID", rid). 384 | Str("id", id). 385 | Msg("Could not send backoff to peer, stopping") 386 | 387 | return 388 | } 389 | } 390 | case v1.TypeKick: 391 | var kck v1.Kick 392 | if err := mapstructure.Decode(j, &kck); err != nil { 393 | log.Debug(). 394 | Err(err). 395 | Str("channelID", peer.ChannelID). 396 | Str("peerID", rid). 397 | Msg("Could not decode kick from peer, stopping") 398 | 399 | continue 400 | } 401 | 402 | log.Debug(). 403 | Err(err). 404 | Str("channelID", peer.ChannelID). 405 | Str("peerID", rid). 406 | Str("id", kck.ID). 407 | Msg("Received kick") 408 | 409 | candidatesLock.Lock() 410 | delete(candidates, kck.ID) 411 | candidatesLock.Unlock() 412 | case v1.TypeBackoff: 413 | log.Debug(). 414 | Err(err). 415 | Str("channelID", peer.ChannelID). 416 | Str("peerID", rid). 417 | Msg("Received backoff") 418 | 419 | ready.Stop() 420 | 421 | time.Sleep(a.config.Kicks) 422 | 423 | greet() 424 | 425 | ready.Reset(a.config.Kicks) 426 | case v1.TypeClaimed: 427 | var clm v1.Claimed 428 | if err := mapstructure.Decode(j, &clm); err != nil { 429 | log.Debug(). 430 | Err(err). 431 | Str("channelID", peer.ChannelID). 432 | Str("peerID", rid). 433 | Msg("Could not decode claimed from peer, stopping") 434 | 435 | continue 436 | } 437 | 438 | log.Debug(). 439 | Err(err). 440 | Str("channelID", peer.ChannelID). 441 | Str("peerID", rid). 442 | Str("id", clm.ID). 443 | Msg("Received kick") 444 | 445 | rid = clm.ID 446 | 447 | if _, ok := peers[rid]; !ok { 448 | log.Debug(). 449 | Err(err). 450 | Str("channelID", peer.ChannelID). 451 | Str("peerID", rid). 452 | Str("id", clm.ID). 453 | Msg("Connected to peer") 454 | } 455 | 456 | peersLock.Lock() 457 | if _, ok := peers[rid]; !ok { 458 | peers[rid] = map[string]*Peer{} 459 | } 460 | for key, value := range peers[peer.PeerID] { 461 | peers[rid][key] = value 462 | 463 | if value.ChannelID != a.config.IDChannel { 464 | namedPeers <- &Peer{ 465 | PeerID: rid, 466 | ChannelID: value.ChannelID, 467 | Conn: value.Conn, 468 | } 469 | } 470 | } 471 | delete(peers, peer.PeerID) 472 | peersLock.Unlock() 473 | default: 474 | log.Debug(). 475 | Str("channelID", peer.ChannelID). 476 | Str("peerID", rid). 477 | Str("type", message.Type). 478 | Msg("Got message with unknown type from peer, continuing") 479 | 480 | continue 481 | } 482 | } 483 | }() 484 | } 485 | } 486 | } 487 | }() 488 | 489 | return a.names, nil 490 | } 491 | 492 | // Close disconnects the adapter from the signaler 493 | func (a *NamedAdapter) Close() error { 494 | log.Trace().Msg("Closing adapter") 495 | 496 | return a.adapter.Close() 497 | } 498 | 499 | // Err returns a channel on which all fatal errors will be sent 500 | func (a *NamedAdapter) Err() chan error { 501 | return a.errs 502 | } 503 | 504 | // Accept returns a channel on which peers will be sent when they connect 505 | func (a *NamedAdapter) Accept() chan *Peer { 506 | return a.acceptedPeers 507 | } 508 | -------------------------------------------------------------------------------- /pkg/wrtceth/netns_darwin.go: -------------------------------------------------------------------------------- 1 | package wrtceth 2 | 3 | import ( 4 | "net" 5 | 6 | "github.com/songgao/water" 7 | ) 8 | 9 | func getPlatformSpecificParams(name string) water.PlatformSpecificParams { 10 | return water.PlatformSpecificParams{ 11 | Name: name, 12 | } 13 | } 14 | 15 | func setMACAddress(linkName string, hwaddr string) (string, error) { 16 | return hwaddr, nil 17 | } 18 | 19 | func getMTU(linkName string) (int, error) { 20 | iface, err := net.InterfaceByName(linkName) 21 | if err != nil { 22 | return -1, err 23 | } 24 | 25 | return iface.MTU, nil 26 | } 27 | 28 | func setLinkUp(linkName string) error { 29 | return nil 30 | } 31 | -------------------------------------------------------------------------------- /pkg/wrtceth/netns_linux.go: -------------------------------------------------------------------------------- 1 | package wrtceth 2 | 3 | import ( 4 | "net" 5 | "strings" 6 | 7 | "github.com/songgao/water" 8 | "github.com/vishvananda/netlink" 9 | ) 10 | 11 | func getPlatformSpecificParams(name string) water.PlatformSpecificParams { 12 | return water.PlatformSpecificParams{ 13 | Name: name, 14 | } 15 | } 16 | 17 | func setMACAddress(linkName string, hwaddr string) (string, error) { 18 | link, err := netlink.LinkByName(linkName) 19 | if err != nil { 20 | return "", err 21 | } 22 | 23 | var mac net.HardwareAddr 24 | if strings.TrimSpace(hwaddr) == "" { 25 | mac = link.Attrs().HardwareAddr 26 | } else { 27 | mac, err = net.ParseMAC(hwaddr) 28 | if err != nil { 29 | return "", err 30 | } 31 | } 32 | 33 | return mac.String(), netlink.LinkSetHardwareAddr(link, mac) 34 | } 35 | 36 | func getMTU(linkName string) (int, error) { 37 | iface, err := net.InterfaceByName(linkName) 38 | if err != nil { 39 | return -1, err 40 | } 41 | 42 | return iface.MTU, nil 43 | } 44 | 45 | func setLinkUp(linkName string) error { 46 | link, err := netlink.LinkByName(linkName) 47 | if err != nil { 48 | return err 49 | } 50 | 51 | return netlink.LinkSetUp(link) 52 | } 53 | -------------------------------------------------------------------------------- /pkg/wrtceth/netns_others.go: -------------------------------------------------------------------------------- 1 | //go:build !(windows || linux || darwin) 2 | // +build !windows,!linux,!darwin 3 | 4 | package wrtceth 5 | 6 | import ( 7 | "net" 8 | 9 | "github.com/songgao/water" 10 | ) 11 | 12 | func getPlatformSpecificParams(name string) water.PlatformSpecificParams { 13 | return water.PlatformSpecificParams{} 14 | } 15 | 16 | func setMACAddress(linkName string, hwaddr string) (string, error) { 17 | return hwaddr, nil 18 | } 19 | 20 | func getMTU(linkName string) (int, error) { 21 | iface, err := net.InterfaceByName(linkName) 22 | if err != nil { 23 | return -1, err 24 | } 25 | 26 | return iface.MTU, nil 27 | } 28 | 29 | func setLinkUp(linkName string) error { 30 | return nil 31 | } 32 | -------------------------------------------------------------------------------- /pkg/wrtceth/netns_windows.go: -------------------------------------------------------------------------------- 1 | package wrtceth 2 | 3 | import ( 4 | "net" 5 | 6 | "github.com/songgao/water" 7 | ) 8 | 9 | func getPlatformSpecificParams(name string) water.PlatformSpecificParams { 10 | return water.PlatformSpecificParams{} 11 | } 12 | 13 | func setMACAddress(linkName string, hwaddr string) (string, error) { 14 | return hwaddr, nil 15 | } 16 | 17 | func getMTU(linkName string) (int, error) { 18 | iface, err := net.InterfaceByName(linkName) 19 | if err != nil { 20 | return -1, err 21 | } 22 | 23 | return iface.MTU, nil 24 | } 25 | 26 | func setLinkUp(linkName string) error { 27 | return nil 28 | } 29 | -------------------------------------------------------------------------------- /pkg/wrtceth/wrtceth.go: -------------------------------------------------------------------------------- 1 | package wrtceth 2 | 3 | import ( 4 | "context" 5 | "runtime" 6 | "strings" 7 | "sync" 8 | 9 | "github.com/rs/zerolog/log" 10 | 11 | "github.com/google/gopacket" 12 | "github.com/google/gopacket/layers" 13 | "github.com/pojntfx/weron/pkg/services" 14 | "github.com/pojntfx/weron/pkg/wrtcconn" 15 | "github.com/songgao/water" 16 | "golang.org/x/sync/semaphore" 17 | ) 18 | 19 | const ( 20 | broadcastMAC = "ff:ff:ff:ff:ff:ff" 21 | ethernetHeaderLength = 14 22 | ) 23 | 24 | // AdapterConfig configures the adapter 25 | type AdapterConfig struct { 26 | *wrtcconn.AdapterConfig 27 | Device string // Name to give to the TAP device 28 | OnSignalerConnect func(string) // Handler to be called when the adapter has connected to the signaler 29 | OnPeerConnect func(string) // Handler to be called when the adapter has connected to a peer 30 | OnPeerDisconnected func(string) // Handler to be called when the adapter has received a message 31 | Parallel int // Maximum amount of goroutines to use to unmarshal ethernet frames 32 | } 33 | 34 | // Adapter provides an ethernet service 35 | type Adapter struct { 36 | signaler string 37 | key string 38 | ice []string 39 | config *AdapterConfig 40 | ctx context.Context 41 | 42 | cancel context.CancelFunc 43 | adapter *wrtcconn.Adapter 44 | tap *water.Interface 45 | mtu int 46 | ids chan string 47 | } 48 | 49 | // NewAdapter creates the adapter 50 | func NewAdapter( 51 | signaler string, 52 | key string, 53 | ice []string, 54 | config *AdapterConfig, 55 | ctx context.Context, 56 | ) *Adapter { 57 | ictx, cancel := context.WithCancel(ctx) 58 | 59 | if config == nil { 60 | config = &AdapterConfig{} 61 | } 62 | 63 | if config.Parallel <= 0 { 64 | config.Parallel = runtime.NumCPU() 65 | } 66 | 67 | return &Adapter{ 68 | signaler: signaler, 69 | key: key, 70 | ice: ice, 71 | config: config, 72 | ctx: ictx, 73 | 74 | cancel: cancel, 75 | ids: make(chan string), 76 | } 77 | } 78 | 79 | // Open connects the adapter to the signaler and creates the TAP device 80 | func (a *Adapter) Open() error { 81 | log.Trace().Msg("Opening adapter") 82 | 83 | var err error 84 | a.tap, err = water.New(water.Config{ 85 | DeviceType: water.TAP, 86 | PlatformSpecificParams: getPlatformSpecificParams(a.config.Device), 87 | }) 88 | if err != nil { 89 | return err 90 | } 91 | 92 | a.config.AdapterConfig.ID, err = setMACAddress(a.tap.Name(), a.config.ID) 93 | if err != nil { 94 | return err 95 | } 96 | 97 | a.adapter = wrtcconn.NewAdapter( 98 | a.signaler, 99 | a.key, 100 | strings.Split(strings.Join(a.ice, ","), ","), 101 | []string{services.EthernetPrimary}, 102 | a.config.AdapterConfig, 103 | a.ctx, 104 | ) 105 | 106 | a.ids, err = a.adapter.Open() 107 | if err != nil { 108 | return err 109 | } 110 | 111 | a.mtu, err = getMTU(a.tap.Name()) 112 | 113 | return err 114 | } 115 | 116 | // Close disconnects the adapter from the signaler and closes the TAP device 117 | func (a *Adapter) Close() error { 118 | log.Trace().Msg("Closing adapter") 119 | 120 | if err := a.tap.Close(); err != nil { 121 | return err 122 | } 123 | 124 | return a.adapter.Close() 125 | } 126 | 127 | // Wait starts the transmission loop 128 | func (a *Adapter) Wait() error { 129 | peers := map[string]*wrtcconn.Peer{} 130 | var peersLock sync.Mutex 131 | 132 | go func() { 133 | sem := semaphore.NewWeighted(int64(a.config.Parallel)) 134 | 135 | for { 136 | buf := make([]byte, a.mtu+ethernetHeaderLength) 137 | 138 | if _, err := a.tap.Read(buf); err != nil { 139 | log.Debug().Err(err).Msg("Could not read from TAP device, continuing") 140 | 141 | continue 142 | } 143 | 144 | go func() { 145 | if err := sem.Acquire(a.ctx, 1); err != nil { 146 | log.Debug().Err(err).Msg("Could not acquire semaphore, stopping") 147 | 148 | return 149 | } 150 | defer sem.Release(1) 151 | 152 | var frame layers.Ethernet 153 | if err := frame.DecodeFromBytes(buf, gopacket.NilDecodeFeedback); err != nil { 154 | log.Debug().Err(err).Msg("Could not unmarshal frame, stopping") 155 | 156 | return 157 | } 158 | 159 | peersLock.Lock() 160 | for _, peer := range peers { 161 | // Send if matching destination, multicast or broadcast MAC 162 | if dst := frame.DstMAC.String(); dst == peer.PeerID || frame.DstMAC[1]&0b01 == 1 || dst == broadcastMAC { 163 | if _, err := peer.Conn.Write(buf); err != nil { 164 | log.Debug(). 165 | Err(err). 166 | Str("channelID", peer.ChannelID). 167 | Str("peerID", peer.PeerID). 168 | Msg("Could not write to peer, continuing") 169 | 170 | continue 171 | } 172 | } 173 | } 174 | peersLock.Unlock() 175 | }() 176 | } 177 | }() 178 | 179 | for { 180 | select { 181 | case <-a.ctx.Done(): 182 | log.Trace().Err(a.ctx.Err()).Msg("Context cancelled") 183 | 184 | if err := a.ctx.Err(); err != context.Canceled { 185 | return err 186 | } 187 | 188 | return nil 189 | case id := <-a.ids: 190 | log.Debug().Str("id", id).Msg("Connected to signaler") 191 | 192 | if a.config.OnSignalerConnect != nil { 193 | a.config.OnSignalerConnect(id) 194 | } 195 | 196 | if err := setLinkUp(a.tap.Name()); err != nil { 197 | return err 198 | } 199 | case peer := <-a.adapter.Accept(): 200 | log.Debug().Str("channelID", peer.ChannelID).Str("peerID", peer.PeerID).Msg("Connected to peer") 201 | 202 | if a.config.OnPeerConnect != nil { 203 | a.config.OnPeerConnect(peer.PeerID) 204 | } 205 | 206 | go func() { 207 | defer func() { 208 | log.Debug().Str("channelID", peer.ChannelID).Str("peerID", peer.PeerID).Msg("Disconnected from peer") 209 | 210 | if a.config.OnPeerDisconnected != nil { 211 | a.config.OnPeerDisconnected(peer.PeerID) 212 | } 213 | 214 | peersLock.Lock() 215 | delete(peers, peer.PeerID) 216 | peersLock.Unlock() 217 | }() 218 | 219 | peersLock.Lock() 220 | peers[peer.PeerID] = peer 221 | peersLock.Unlock() 222 | 223 | for { 224 | buf := make([]byte, a.mtu+ethernetHeaderLength) 225 | 226 | if _, err := peer.Conn.Read(buf); err != nil { 227 | log.Debug(). 228 | Err(err). 229 | Str("channelID", peer.ChannelID). 230 | Str("peerID", peer.PeerID). 231 | Msg("Could not read from peer, stopping") 232 | 233 | return 234 | } 235 | 236 | if _, err := a.tap.Write(buf); err != nil { 237 | log.Debug(). 238 | Err(err). 239 | Str("channelID", peer.ChannelID). 240 | Str("peerID", peer.PeerID). 241 | Msg("Could not write to TAP device, continuing") 242 | 243 | continue 244 | } 245 | } 246 | }() 247 | } 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /pkg/wrtcip/netns_linux.go: -------------------------------------------------------------------------------- 1 | package wrtcip 2 | 3 | import ( 4 | "github.com/songgao/water" 5 | "github.com/vishvananda/netlink" 6 | ) 7 | 8 | func setupTUN(name string, ips []string) (*water.Interface, int, error) { 9 | tun, err := water.New(water.Config{ 10 | DeviceType: water.TUN, 11 | PlatformSpecificParams: water.PlatformSpecificParams{ 12 | Name: name, 13 | }, 14 | }) 15 | if err != nil { 16 | return nil, 0, err 17 | } 18 | 19 | link, err := netlink.LinkByName(tun.Name()) 20 | if err != nil { 21 | return tun, 0, err 22 | } 23 | 24 | for _, rawIP := range ips { 25 | ip, err := netlink.ParseAddr(rawIP) 26 | if err != nil { 27 | return tun, 0, err 28 | } 29 | 30 | if err := netlink.AddrAdd(link, ip); err != nil { 31 | return tun, 0, err 32 | } 33 | } 34 | 35 | if err := netlink.LinkSetUp(link); err != nil { 36 | return tun, 0, err 37 | } 38 | 39 | return tun, link.Attrs().MTU, nil 40 | } 41 | -------------------------------------------------------------------------------- /pkg/wrtcip/netns_others.go: -------------------------------------------------------------------------------- 1 | //go:build !(windows || linux) 2 | // +build !windows,!linux 3 | 4 | package wrtcip 5 | 6 | import ( 7 | "fmt" 8 | "net" 9 | "os/exec" 10 | "runtime" 11 | 12 | "github.com/songgao/water" 13 | ) 14 | 15 | func setupTUN(name string, ips []string) (*water.Interface, int, error) { 16 | tun, err := water.New(water.Config{ 17 | DeviceType: water.TUN, 18 | PlatformSpecificParams: water.PlatformSpecificParams{}, 19 | }) 20 | if err != nil { 21 | return nil, 0, err 22 | } 23 | 24 | for _, rawIP := range ips { 25 | ip, _, err := net.ParseCIDR(rawIP) 26 | if err != nil { 27 | return tun, 0, err 28 | } 29 | 30 | if ip.To4() != nil { 31 | // macOS does not support IPv4 TUN 32 | if runtime.GOOS == "darwin" && ip.To4() != nil { 33 | continue 34 | } 35 | 36 | output, err := exec.Command("ifconfig", tun.Name(), "inet", rawIP).CombinedOutput() 37 | if err != nil { 38 | return tun, 0, fmt.Errorf("could not add IPv4 address to interface: %v: %v", string(output), err) 39 | } 40 | } else { 41 | output, err := exec.Command("ifconfig", tun.Name(), "inet6", "add", rawIP).CombinedOutput() 42 | if err != nil { 43 | return tun, 0, fmt.Errorf("could not add IPv6 address to interface: %v: %v", string(output), err) 44 | } 45 | } 46 | } 47 | 48 | iface, err := net.InterfaceByName(tun.Name()) 49 | if err != nil { 50 | return tun, 0, err 51 | } 52 | 53 | return tun, iface.MTU, nil 54 | } 55 | -------------------------------------------------------------------------------- /pkg/wrtcip/netns_windows.go: -------------------------------------------------------------------------------- 1 | package wrtcip 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "os/exec" 7 | 8 | "github.com/songgao/water" 9 | ) 10 | 11 | func setupTUN(name string, ips []string) (*water.Interface, int, error) { 12 | tun, err := water.New(water.Config{ 13 | DeviceType: water.TUN, 14 | PlatformSpecificParams: water.PlatformSpecificParams{ 15 | ComponentID: "tap0901", 16 | Network: ips[0], 17 | }, 18 | }) 19 | if err != nil { 20 | return nil, 0, err 21 | } 22 | 23 | ip, _, err := net.ParseCIDR(ips[0]) 24 | if err != nil { 25 | return tun, 0, err 26 | } 27 | 28 | if ip.To4() != nil { 29 | output, err := exec.Command("netsh", "interface", "ipv4", "set", "address", tun.Name(), "static", ips[0]).CombinedOutput() 30 | if err != nil { 31 | return tun, 0, fmt.Errorf("could not add IPv4 address to interface: %v: %v", string(output), err) 32 | } 33 | } else { 34 | output, err := exec.Command("netsh", "interface", "ipv6", "set", "address", tun.Name(), ips[0]).CombinedOutput() 35 | if err != nil { 36 | return tun, 0, fmt.Errorf("could not add IPv6 address to interface: %v: %v", string(output), err) 37 | } 38 | } 39 | 40 | iface, err := net.InterfaceByName(tun.Name()) 41 | if err != nil { 42 | return tun, 0, err 43 | } 44 | 45 | return tun, iface.MTU, nil 46 | } 47 | -------------------------------------------------------------------------------- /pkg/wrtcip/wrtcip.go: -------------------------------------------------------------------------------- 1 | package wrtcip 2 | 3 | import ( 4 | "context" 5 | "encoding/binary" 6 | "errors" 7 | "fmt" 8 | "net" 9 | "net/netip" 10 | "runtime" 11 | "strings" 12 | "sync" 13 | 14 | "github.com/rs/zerolog/log" 15 | 16 | "github.com/google/gopacket" 17 | "github.com/google/gopacket/layers" 18 | jsoniter "github.com/json-iterator/go" 19 | "github.com/pojntfx/weron/pkg/services" 20 | "github.com/pojntfx/weron/pkg/wrtcconn" 21 | "github.com/songgao/water" 22 | "golang.org/x/sync/semaphore" 23 | ) 24 | 25 | const ( 26 | headerLength = 22 27 | ) 28 | 29 | var ( 30 | json = jsoniter.ConfigCompatibleWithStandardLibrary 31 | 32 | ErrMissingIPs = errors.New("no IPs provided") 33 | ) 34 | 35 | // AdapterConfig configures the adapter 36 | type AdapterConfig struct { 37 | *wrtcconn.NamedAdapterConfig 38 | Device string // Name to give to the TUN device 39 | OnSignalerConnect func(string) // Handler to be called when the adapter has connected to the signaler 40 | OnPeerConnect func(string) // Handler to be called when the adapter has connected to a peer 41 | OnPeerDisconnected func(string) // Handler to be called when the adapter has received a message 42 | CIDRs []string // IPv4 & IPv6 networks to join 43 | MaxRetries int // Maximum amount of IP address to try and claim before giving up 44 | Parallel int // Maximum amount of goroutines to use to unmarshal IP packets 45 | Static bool // Claim the exact IP specified in the CIDR notation instead of selecting a random one from the networks 46 | } 47 | 48 | // Adapter provides an IP service 49 | type Adapter struct { 50 | signaler string 51 | key string 52 | ice []string 53 | config *AdapterConfig 54 | ctx context.Context 55 | 56 | cancel context.CancelFunc 57 | adapter *wrtcconn.NamedAdapter 58 | tun *water.Interface 59 | ids chan string 60 | 61 | mtu int 62 | mtuCond *sync.Cond 63 | } 64 | 65 | type peerWithIP struct { 66 | *wrtcconn.Peer 67 | ip net.IP 68 | net *net.IPNet 69 | } 70 | 71 | // NewAdapter creates the adapter 72 | func NewAdapter( 73 | signaler string, 74 | key string, 75 | ice []string, 76 | config *AdapterConfig, 77 | ctx context.Context, 78 | ) *Adapter { 79 | ictx, cancel := context.WithCancel(ctx) 80 | 81 | if config == nil { 82 | config = &AdapterConfig{} 83 | } 84 | 85 | if config.Parallel <= 0 { 86 | config.Parallel = runtime.NumCPU() 87 | } 88 | 89 | return &Adapter{ 90 | signaler: signaler, 91 | key: key, 92 | ice: ice, 93 | config: config, 94 | ctx: ictx, 95 | 96 | cancel: cancel, 97 | ids: make(chan string), 98 | 99 | mtuCond: sync.NewCond(&sync.Mutex{}), 100 | } 101 | } 102 | 103 | // Open connects the adapter to the signaler 104 | func (a *Adapter) Open() error { 105 | log.Trace().Msg("Opening adapter") 106 | 107 | for _, rawIP := range a.config.CIDRs { 108 | ip, _, err := net.ParseCIDR(rawIP) 109 | if err != nil { 110 | return err 111 | } 112 | 113 | // macOS does not support IPv4 TUN 114 | if runtime.GOOS == "darwin" && ip.To4() != nil { 115 | continue 116 | } 117 | 118 | } 119 | 120 | names := []string{} 121 | if a.config.Static { 122 | for _, cidr := range a.config.CIDRs { 123 | if _, err := netip.ParsePrefix(cidr); err != nil { 124 | return err 125 | } 126 | } 127 | 128 | name, err := json.Marshal(a.config.CIDRs) 129 | if err != nil { 130 | return err 131 | } 132 | 133 | names = append(names, string(name)) 134 | } else { 135 | rawNames := make([][]string, a.config.MaxRetries) 136 | for _, cidr := range a.config.CIDRs { 137 | prefix, err := netip.ParsePrefix(cidr) 138 | if err != nil { 139 | return err 140 | } 141 | 142 | cidrIPs := []string{} 143 | i := 0 144 | for addr := prefix.Addr(); prefix.Contains(addr); addr = addr.Next() { 145 | if i >= a.config.MaxRetries+2 { 146 | break 147 | } 148 | 149 | cidrIPs = append(cidrIPs, fmt.Sprintf("%v/%v", addr.String(), prefix.Bits())) 150 | 151 | i++ 152 | } 153 | 154 | if prefix.Addr().Is4() && len(cidrIPs) > 2 { 155 | cidrIPs = cidrIPs[1 : len(cidrIPs)-1] 156 | } 157 | 158 | for i, cidrIP := range cidrIPs { 159 | if i >= a.config.MaxRetries { 160 | break 161 | } 162 | 163 | rawNames[i] = append(rawNames[i], cidrIP) 164 | } 165 | } 166 | 167 | for _, rawName := range rawNames { 168 | name, err := json.Marshal(rawName) 169 | if err != nil { 170 | return err 171 | } 172 | 173 | names = append(names, string(name)) 174 | } 175 | } 176 | 177 | a.config.NamedAdapterConfig.Names = names 178 | a.config.NamedAdapterConfig.IsIDClaimed = func(theirRawIPs map[string]struct{}, s string) bool { 179 | ourIPs := []string{} 180 | if err := json.Unmarshal([]byte(s), &ourIPs); err != nil { 181 | return true 182 | } 183 | 184 | for theirRawIP := range theirRawIPs { 185 | for _, ourRawIP := range ourIPs { 186 | theirIP, _, err := net.ParseCIDR(theirRawIP) 187 | if err != nil { 188 | return true 189 | } 190 | 191 | ourIP, _, err := net.ParseCIDR(ourRawIP) 192 | if err != nil { 193 | return true 194 | } 195 | 196 | if theirIP.Equal(ourIP) { 197 | return true 198 | } 199 | } 200 | } 201 | 202 | return false 203 | } 204 | 205 | a.adapter = wrtcconn.NewNamedAdapter( 206 | a.signaler, 207 | a.key, 208 | strings.Split(strings.Join(a.ice, ","), ","), 209 | []string{services.IPPrimary}, 210 | a.config.NamedAdapterConfig, 211 | a.ctx, 212 | ) 213 | 214 | var err error 215 | a.ids, err = a.adapter.Open() 216 | if err != nil { 217 | return err 218 | } 219 | 220 | return err 221 | } 222 | 223 | // Close disconnects the adapter from the signaler and closes the TUN device 224 | func (a *Adapter) Close() error { 225 | log.Trace().Msg("Closing adapter") 226 | 227 | if a.tun != nil { 228 | if err := a.tun.Close(); err != nil { 229 | return err 230 | } 231 | } 232 | 233 | return a.adapter.Close() 234 | } 235 | 236 | // Wait starts the transmission loop 237 | func (a *Adapter) Wait() error { 238 | peers := map[string]*peerWithIP{} 239 | var peersLock sync.Mutex 240 | 241 | for { 242 | select { 243 | case <-a.ctx.Done(): 244 | log.Trace().Err(a.ctx.Err()).Msg("Context cancelled") 245 | 246 | if err := a.ctx.Err(); err != context.Canceled { 247 | return err 248 | } 249 | 250 | return nil 251 | case err := <-a.adapter.Err(): 252 | return err 253 | case id := <-a.ids: 254 | log.Debug().Str("id", id).Msg("Connected to signaler") 255 | 256 | if a.config.OnSignalerConnect != nil { 257 | a.config.OnSignalerConnect(id) 258 | } 259 | 260 | ips := []string{} 261 | if err := json.Unmarshal([]byte(id), &ips); err != nil { 262 | return err 263 | } 264 | 265 | if len(ips) <= 0 { 266 | return ErrMissingIPs 267 | } 268 | 269 | // Close old TUN device if it isn't already closed 270 | if a.tun != nil { 271 | _ = a.tun.Close() 272 | } 273 | 274 | a.mtuCond.L.Lock() 275 | var err error 276 | a.tun, a.mtu, err = setupTUN(a.config.Device, ips) 277 | if err != nil { 278 | a.mtuCond.L.Unlock() 279 | 280 | return err 281 | } 282 | // Signal that the MTU is available/the TUN device is started 283 | a.mtuCond.Broadcast() 284 | a.mtuCond.L.Unlock() 285 | 286 | go func() { 287 | sem := semaphore.NewWeighted(int64(a.config.Parallel)) 288 | 289 | for { 290 | buf := make([]byte, a.mtu+headerLength) // No need for the MTU cond here since its guaranteed to be set 291 | 292 | if _, err := a.tun.Read(buf); err != nil { 293 | log.Debug().Err(err).Msg("Could not read from TUN device, returning") 294 | 295 | return 296 | } 297 | 298 | go func() { 299 | if err := sem.Acquire(a.ctx, 1); err != nil { 300 | log.Debug().Err(err).Msg("Could not acquire semaphore, stopping") 301 | 302 | return 303 | } 304 | defer sem.Release(1) 305 | 306 | var dst net.IP 307 | var packet layers.IPv4 308 | if err := packet.DecodeFromBytes(buf, gopacket.NilDecodeFeedback); err != nil { 309 | var packet layers.IPv6 310 | if err := packet.DecodeFromBytes(buf, gopacket.NilDecodeFeedback); err != nil { 311 | log.Debug().Err(err).Msg("Could not unmarshal packet, stopping") 312 | 313 | return 314 | } else { 315 | dst = packet.DstIP 316 | } 317 | } else { 318 | dst = packet.DstIP 319 | } 320 | 321 | peersLock.Lock() 322 | for _, peer := range peers { 323 | // Send if matching destination, multicast or broadcast IP 324 | if dst.Equal(peer.ip) || ((dst.IsMulticast() || dst.IsInterfaceLocalMulticast() || dst.IsInterfaceLocalMulticast()) && len(dst) == len(peer.ip)) || (peer.ip.To4() != nil && dst.Equal(getBroadcastAddr(peer.net))) { 325 | if _, err := peer.Conn.Write(buf); err != nil { 326 | log.Debug(). 327 | Err(err). 328 | Str("channelID", peer.ChannelID). 329 | Str("peerID", peer.PeerID). 330 | Msg("Could not write to peer, continuing") 331 | 332 | continue 333 | } 334 | } 335 | } 336 | peersLock.Unlock() 337 | }() 338 | } 339 | }() 340 | case peer := <-a.adapter.Accept(): 341 | log.Debug().Str("channelID", peer.ChannelID).Str("peerID", peer.PeerID).Msg("Connected to peer") 342 | 343 | go func() { 344 | if a.config.OnPeerConnect != nil { 345 | a.config.OnPeerConnect(peer.PeerID) 346 | } 347 | 348 | ips := []string{} 349 | if err := json.Unmarshal([]byte(peer.PeerID), &ips); err != nil { 350 | log.Debug(). 351 | Str("channelID", peer.ChannelID). 352 | Str("peerID", peer.PeerID). 353 | Err(err).Msg("Could not parse local IP address, stopping") 354 | 355 | return 356 | } 357 | 358 | valid := false 359 | peersLock.Lock() 360 | for _, rawIP := range ips { 361 | ip, net, err := net.ParseCIDR(rawIP) 362 | if err != nil { 363 | log.Debug(). 364 | Str("channelID", peer.ChannelID). 365 | Str("peerID", peer.PeerID). 366 | Err(err). 367 | Msg("Could not parse local IP address, continuing") 368 | 369 | continue 370 | } 371 | 372 | peers[ip.String()] = &peerWithIP{peer, ip, net} 373 | 374 | valid = true 375 | } 376 | peersLock.Unlock() 377 | 378 | defer func() { 379 | log.Debug().Str("channelID", peer.ChannelID).Str("peerID", peer.PeerID).Msg("Disconnected from peer") 380 | 381 | if a.config.OnPeerDisconnected != nil { 382 | a.config.OnPeerDisconnected(peer.PeerID) 383 | } 384 | 385 | peersLock.Lock() 386 | for _, ip := range ips { 387 | delete(peers, ip) 388 | } 389 | peersLock.Unlock() 390 | }() 391 | 392 | if !valid { 393 | log.Debug(). 394 | Str("channelID", peer.ChannelID). 395 | Str("peerID", peer.PeerID). 396 | Msg("Got peer with invalid IP addresses, stopping") 397 | 398 | return 399 | } 400 | 401 | for { 402 | a.mtuCond.L.Lock() 403 | if a.mtu <= 0 { 404 | a.mtuCond.Wait() 405 | } 406 | buf := make([]byte, a.mtu+headerLength) 407 | a.mtuCond.L.Unlock() 408 | 409 | if _, err := peer.Conn.Read(buf); err != nil { 410 | log.Debug(). 411 | Err(err). 412 | Str("channelID", peer.ChannelID). 413 | Str("peerID", peer.PeerID). 414 | Msg("Could not read from peer, stopping") 415 | 416 | return 417 | } 418 | 419 | if _, err := a.tun.Write(buf); err != nil { 420 | log.Debug(). 421 | Err(err). 422 | Str("channelID", peer.ChannelID). 423 | Str("peerID", peer.PeerID). 424 | Msg("Could not write to TUN device, continuing") 425 | 426 | continue 427 | } 428 | } 429 | }() 430 | } 431 | } 432 | } 433 | 434 | // See https://go.dev/play/p/Igo6Ct3gx_ 435 | func getBroadcastAddr(n *net.IPNet) net.IP { 436 | ip := make(net.IP, len(n.IP.To4())) 437 | 438 | binary.BigEndian.PutUint32(ip, binary.BigEndian.Uint32(n.IP.To4())|^binary.BigEndian.Uint32(net.IP(n.Mask).To4())) 439 | 440 | return ip 441 | } 442 | -------------------------------------------------------------------------------- /pkg/wrtcltc/wrtcltc.go: -------------------------------------------------------------------------------- 1 | package wrtcltc 2 | 3 | import ( 4 | "context" 5 | "crypto/rand" 6 | "math" 7 | "strings" 8 | "time" 9 | 10 | "github.com/rs/zerolog/log" 11 | 12 | "github.com/pojntfx/weron/pkg/services" 13 | "github.com/pojntfx/weron/pkg/wrtcconn" 14 | "github.com/teivah/broadcast" 15 | ) 16 | 17 | // AdapterConfig configures the adapter 18 | type AdapterConfig struct { 19 | *wrtcconn.AdapterConfig 20 | OnSignalerConnect func(string) // Handler to be called when the adapter has connected to the signaler 21 | OnPeerConnect func(string) // Handler to be called when the adapter has connected to a peer 22 | OnPeerDisconnected func(string) // Handler to be called when the adapter has received a message 23 | Server bool // Whether to act as the server 24 | PacketLength int // Length of the packet to measure latency with 25 | Pause time.Duration // Amount of time to wait before measuring next latency datapoint 26 | } 27 | 28 | // Totals are the total statistics 29 | type Totals struct { 30 | LatencyAverage time.Duration // Average total latency 31 | LatencyMin time.Duration // Minimum mesured latency 32 | LatencyMax time.Duration // Maximum measured latency 33 | PacketsWritten int64 // Count of written packets 34 | } 35 | 36 | // Acknowledgement is an individual datapoint 37 | type Acknowledgement struct { 38 | BytesWritten int // Count of written bytes 39 | Latency time.Duration // Latency measured at this datapoint 40 | } 41 | 42 | // Adapter provides a latency measurement service 43 | type Adapter struct { 44 | signaler string 45 | key string 46 | ice []string 47 | config *AdapterConfig 48 | ctx context.Context 49 | 50 | cancel context.CancelFunc 51 | adapter *wrtcconn.Adapter 52 | 53 | ids chan string 54 | totals chan Totals 55 | acknowledgements chan Acknowledgement 56 | 57 | closer *broadcast.Relay[struct{}] 58 | } 59 | 60 | // NewAdapter creates the adapter 61 | func NewAdapter( 62 | signaler string, 63 | key string, 64 | ice []string, 65 | config *AdapterConfig, 66 | ctx context.Context, 67 | ) *Adapter { 68 | ictx, cancel := context.WithCancel(ctx) 69 | 70 | if config == nil { 71 | config = &AdapterConfig{} 72 | } 73 | 74 | return &Adapter{ 75 | signaler: signaler, 76 | key: key, 77 | ice: ice, 78 | config: config, 79 | ctx: ictx, 80 | 81 | cancel: cancel, 82 | 83 | ids: make(chan string), 84 | totals: make(chan Totals), 85 | acknowledgements: make(chan Acknowledgement), 86 | } 87 | } 88 | 89 | // Open connects the adapter to the signaler 90 | func (a *Adapter) Open() error { 91 | log.Trace().Msg("Opening adapter") 92 | 93 | a.adapter = wrtcconn.NewAdapter( 94 | a.signaler, 95 | a.key, 96 | strings.Split(strings.Join(a.ice, ","), ","), 97 | []string{services.LatencyPrimary}, 98 | a.config.AdapterConfig, 99 | a.ctx, 100 | ) 101 | 102 | var err error 103 | a.ids, err = a.adapter.Open() 104 | if err != nil { 105 | return err 106 | } 107 | 108 | a.closer = broadcast.NewRelay[struct{}]() 109 | 110 | return err 111 | } 112 | 113 | // Close disconnects the adapter from the signaler and stops all measurements, resulting in the totals being yielded 114 | func (a *Adapter) Close() error { 115 | log.Trace().Msg("Closing adapter") 116 | 117 | a.closer.Close() 118 | 119 | return a.adapter.Close() 120 | } 121 | 122 | // Wait starts the transmission and measurement loop 123 | func (a *Adapter) Wait() error { 124 | errs := make(chan error) 125 | 126 | for { 127 | select { 128 | case <-a.ctx.Done(): 129 | log.Trace().Err(a.ctx.Err()).Msg("Context cancelled") 130 | 131 | if err := a.ctx.Err(); err != context.Canceled { 132 | return err 133 | } 134 | 135 | return nil 136 | case err := <-errs: 137 | return err 138 | case id := <-a.ids: 139 | log.Debug().Str("id", id).Msg("Connected to signaler") 140 | 141 | if a.config.OnSignalerConnect != nil { 142 | a.config.OnSignalerConnect(id) 143 | } 144 | case peer := <-a.adapter.Accept(): 145 | log.Debug().Str("channelID", peer.ChannelID).Str("peerID", peer.PeerID).Msg("Connected to peer") 146 | 147 | if a.config.Server { 148 | go func() { 149 | defer func() { 150 | log.Debug().Str("channelID", peer.ChannelID).Str("peerID", peer.PeerID).Msg("Disconnected from peer") 151 | 152 | if a.config.OnPeerDisconnected != nil { 153 | a.config.OnPeerDisconnected(peer.PeerID) 154 | } 155 | }() 156 | 157 | if a.config.OnPeerConnect != nil { 158 | a.config.OnPeerConnect(peer.PeerID) 159 | } 160 | 161 | for { 162 | buf := make([]byte, a.config.PacketLength) 163 | if _, err := peer.Conn.Read(buf); err != nil { 164 | log.Debug(). 165 | Err(err). 166 | Str("channelID", peer.ChannelID). 167 | Str("peerID", peer.PeerID). 168 | Msg("Could not read from peer, stopping") 169 | 170 | return 171 | } 172 | 173 | if _, err := peer.Conn.Write(buf); err != nil { 174 | log.Debug(). 175 | Err(err). 176 | Str("channelID", peer.ChannelID). 177 | Str("peerID", peer.PeerID). 178 | Msg("Could not write to peer, stopping") 179 | 180 | return 181 | } 182 | } 183 | }() 184 | } else { 185 | go func() { 186 | if a.config.OnPeerConnect != nil { 187 | a.config.OnPeerConnect(peer.PeerID) 188 | } 189 | 190 | packetsWritten := int64(0) 191 | totalLatency := time.Duration(0) 192 | 193 | minLatency := time.Duration(math.MaxInt64) 194 | maxLatency := time.Duration(0) 195 | 196 | printTotals := func() { 197 | if packetsWritten >= 1 { 198 | averageLatency := totalLatency.Nanoseconds() / packetsWritten 199 | 200 | a.totals <- Totals{ 201 | LatencyAverage: time.Duration(averageLatency), 202 | LatencyMin: minLatency, 203 | LatencyMax: maxLatency, 204 | PacketsWritten: packetsWritten, 205 | } 206 | } 207 | } 208 | 209 | go func() { 210 | c := a.closer.Listener(0) 211 | defer c.Close() 212 | 213 | <-c.Ch() 214 | 215 | printTotals() 216 | }() 217 | 218 | defer func() { 219 | printTotals() 220 | 221 | if a.config.OnPeerDisconnected != nil { 222 | a.config.OnPeerDisconnected(peer.PeerID) 223 | } 224 | }() 225 | 226 | for { 227 | start := time.Now() 228 | 229 | buf := make([]byte, a.config.PacketLength) 230 | if _, err := rand.Read(buf); err != nil { 231 | errs <- err 232 | 233 | return 234 | } 235 | 236 | written, err := peer.Conn.Write(buf) 237 | if err != nil { 238 | log.Debug(). 239 | Err(err). 240 | Str("channelID", peer.ChannelID). 241 | Str("peerID", peer.PeerID). 242 | Msg("Could not write to peer, stopping") 243 | 244 | return 245 | } 246 | 247 | if _, err := peer.Conn.Read(buf); err != nil { 248 | log.Debug(). 249 | Err(err). 250 | Str("channelID", peer.ChannelID). 251 | Str("peerID", peer.PeerID). 252 | Msg("Could not read from peer, stopping") 253 | 254 | return 255 | } 256 | 257 | latency := time.Since(start) 258 | 259 | if latency < minLatency { 260 | minLatency = latency 261 | } 262 | 263 | if latency > maxLatency { 264 | maxLatency = latency 265 | } 266 | 267 | totalLatency += latency 268 | packetsWritten++ 269 | 270 | a.acknowledgements <- Acknowledgement{ 271 | BytesWritten: written, 272 | Latency: latency, 273 | } 274 | 275 | time.Sleep(a.config.Pause) 276 | } 277 | }() 278 | } 279 | } 280 | } 281 | } 282 | 283 | // GatherTotals yields the total statistics 284 | func (a *Adapter) GatherTotals() { 285 | a.closer.NotifyCtx(a.ctx, struct{}{}) 286 | } 287 | 288 | // Totals returns a channel on which all total statistics will be sent 289 | func (a *Adapter) Totals() chan Totals { 290 | return a.totals 291 | } 292 | 293 | // Acknowledgements returns a channel on which all individual datapoints will be sent 294 | func (a *Adapter) Acknowledgements() chan Acknowledgement { 295 | return a.acknowledgements 296 | } 297 | -------------------------------------------------------------------------------- /pkg/wrtcmgr/wrtcmgr.go: -------------------------------------------------------------------------------- 1 | package wrtcmgr 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "io/ioutil" 7 | "net/http" 8 | "net/url" 9 | 10 | jsoniter "github.com/json-iterator/go" 11 | "github.com/pojntfx/weron/internal/persisters" 12 | ) 13 | 14 | var ( 15 | json = jsoniter.ConfigCompatibleWithStandardLibrary 16 | ) 17 | 18 | // Manager manages a signaling server 19 | type Manager struct { 20 | url string 21 | username string 22 | password string 23 | ctx context.Context 24 | } 25 | 26 | // NewManager creates the manager 27 | func NewManager( 28 | url string, 29 | username string, 30 | password string, 31 | ctx context.Context, 32 | ) *Manager { 33 | return &Manager{ 34 | url: url, 35 | username: username, 36 | password: password, 37 | ctx: ctx, 38 | } 39 | } 40 | 41 | // CreatePersistentCommunity creates a persistent community, which will not be automatically deleted after the last peer leaves 42 | func (m *Manager) CreatePersistentCommunity(community string, password string) (*persisters.Community, error) { 43 | hc := &http.Client{} 44 | 45 | u, err := url.Parse(m.url) 46 | if err != nil { 47 | return nil, err 48 | } 49 | 50 | q := u.Query() 51 | q.Set("community", community) 52 | q.Set("password", password) 53 | u.RawQuery = q.Encode() 54 | 55 | req, err := http.NewRequest(http.MethodPost, u.String(), http.NoBody) 56 | if err != nil { 57 | return nil, err 58 | } 59 | req.SetBasicAuth(m.username, m.password) 60 | 61 | res, err := hc.Do(req) 62 | if err != nil { 63 | return nil, err 64 | } 65 | if res.Body != nil { 66 | defer res.Body.Close() 67 | } 68 | if res.StatusCode != http.StatusOK { 69 | return nil, errors.New(res.Status) 70 | } 71 | 72 | body, err := ioutil.ReadAll(res.Body) 73 | if err != nil { 74 | return nil, err 75 | } 76 | 77 | c := persisters.Community{} 78 | if err := json.Unmarshal(body, &c); err != nil { 79 | return nil, err 80 | } 81 | 82 | return &c, nil 83 | } 84 | 85 | // ListCommunities queries all communities 86 | func (m *Manager) ListCommunities() ([]persisters.Community, error) { 87 | u, err := url.Parse(m.url) 88 | if err != nil { 89 | return nil, err 90 | } 91 | 92 | hc := &http.Client{} 93 | 94 | req, err := http.NewRequest(http.MethodGet, u.String(), http.NoBody) 95 | if err != nil { 96 | return nil, err 97 | } 98 | req.SetBasicAuth(m.username, m.password) 99 | 100 | res, err := hc.Do(req) 101 | if err != nil { 102 | return nil, err 103 | } 104 | if res.Body != nil { 105 | defer res.Body.Close() 106 | } 107 | if res.StatusCode != http.StatusOK { 108 | return nil, errors.New(res.Status) 109 | } 110 | 111 | body, err := ioutil.ReadAll(res.Body) 112 | if err != nil { 113 | return nil, err 114 | } 115 | 116 | c := []persisters.Community{} 117 | if err := json.Unmarshal(body, &c); err != nil { 118 | return nil, err 119 | } 120 | 121 | return c, nil 122 | } 123 | 124 | // DeleteCommunity deletes a community and kicks all peers that joined it 125 | func (m *Manager) DeleteCommunity(community string) error { 126 | hc := &http.Client{} 127 | 128 | u, err := url.Parse(m.url) 129 | if err != nil { 130 | return err 131 | } 132 | 133 | q := u.Query() 134 | q.Set("community", community) 135 | u.RawQuery = q.Encode() 136 | 137 | req, err := http.NewRequest(http.MethodDelete, u.String(), http.NoBody) 138 | if err != nil { 139 | return err 140 | } 141 | req.SetBasicAuth(m.username, m.password) 142 | 143 | res, err := hc.Do(req) 144 | if err != nil { 145 | return err 146 | } 147 | if res.Body != nil { 148 | defer res.Body.Close() 149 | } 150 | if res.StatusCode != http.StatusOK { 151 | return errors.New(res.Status) 152 | } 153 | 154 | return nil 155 | } 156 | -------------------------------------------------------------------------------- /pkg/wrtcsgl/wrtcsgl.go: -------------------------------------------------------------------------------- 1 | package wrtcsgl 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "errors" 7 | "fmt" 8 | "net" 9 | "net/http" 10 | "strings" 11 | "sync" 12 | "time" 13 | 14 | "github.com/rs/zerolog/log" 15 | 16 | rediserr "github.com/go-redis/redis/v8" 17 | "github.com/google/uuid" 18 | "github.com/gorilla/websocket" 19 | jsoniter "github.com/json-iterator/go" 20 | "github.com/pojntfx/go-auth-utils/pkg/authn" 21 | "github.com/pojntfx/go-auth-utils/pkg/authn/basic" 22 | "github.com/pojntfx/go-auth-utils/pkg/authn/oidc" 23 | "github.com/pojntfx/weron/internal/brokers" 24 | "github.com/pojntfx/weron/internal/brokers/process" 25 | "github.com/pojntfx/weron/internal/brokers/redis" 26 | "github.com/pojntfx/weron/internal/persisters" 27 | "github.com/pojntfx/weron/internal/persisters/memory" 28 | "github.com/pojntfx/weron/internal/persisters/psql" 29 | ) 30 | 31 | var ( 32 | errMissingCommunity = errors.New("missing community") 33 | errMissingPassword = errors.New("missing password") 34 | 35 | upgrader = websocket.Upgrader{} 36 | 37 | json = jsoniter.ConfigCompatibleWithStandardLibrary 38 | ) 39 | 40 | type connection struct { 41 | conn *websocket.Conn 42 | closer chan struct{} 43 | } 44 | 45 | // SignalerConfig configures the adapter 46 | type SignalerConfig struct { 47 | Heartbeat time.Duration // Duration between heartbeats 48 | Cleanup bool // Whether to cleanup leftover ephemeral communities before starting 49 | EphemeralCommunities bool // Whether to enable ephemeral communities 50 | APIUsername string // Username for the API endpoint 51 | APIPassword string // Password for the API endpoint; ignored if any of the OIDC parameters are set 52 | OIDCIssuer string // OpenID Connect issuer 53 | OIDCClientID string // OpenID Connect client id 54 | 55 | OnConnect func(raddr string, community string) // Handler to be called when a client has connected to the signaler 56 | OnDisconnect func(raddr string, community string, err interface{}) // Handler to be called when a client has disconnected from the signaler 57 | } 58 | 59 | // Signaler provides a WebRTC signaling server 60 | type Signaler struct { 61 | laddr string 62 | postgresURL string 63 | redisURL string 64 | config *SignalerConfig 65 | ctx context.Context 66 | 67 | errs chan error 68 | connectionsLock sync.Mutex 69 | connections map[string]map[string]connection 70 | db persisters.CommunitiesPersister 71 | broker brokers.CommunitiesBroker 72 | srv *http.Server 73 | closeKicks func() error 74 | } 75 | 76 | // NewSignaler creates the signaler 77 | func NewSignaler( 78 | laddr string, 79 | dbURL string, 80 | brokerURL string, 81 | config *SignalerConfig, 82 | ctx context.Context, 83 | ) *Signaler { 84 | if config == nil { 85 | config = &SignalerConfig{} 86 | } 87 | 88 | return &Signaler{ 89 | laddr: laddr, 90 | postgresURL: dbURL, 91 | redisURL: brokerURL, 92 | config: config, 93 | ctx: ctx, 94 | 95 | errs: make(chan error), 96 | } 97 | } 98 | 99 | // Open starts listening and connects to the database and broker 100 | func (s *Signaler) Open() error { 101 | log.Trace().Msg("Opening signaler") 102 | 103 | addr, err := net.ResolveTCPAddr("tcp", s.laddr) 104 | if err != nil { 105 | return err 106 | } 107 | 108 | managementAPIEnabled := true 109 | if (strings.TrimSpace(s.config.OIDCIssuer) == "" && strings.TrimSpace(s.config.OIDCClientID) == "") && strings.TrimSpace(s.config.APIPassword) == "" { 110 | managementAPIEnabled = false 111 | 112 | log.Debug().Msg("API password not set, disabling management API") 113 | } 114 | 115 | if strings.TrimSpace(s.postgresURL) == "" { 116 | s.db = memory.NewCommunitiesPersister() 117 | } else { 118 | s.db = psql.NewCommunitiesPersister() 119 | } 120 | 121 | if err := s.db.Open(s.postgresURL); err != nil { 122 | return err 123 | } 124 | 125 | if s.config.Cleanup { 126 | if err := s.db.Cleanup(s.ctx); err != nil { 127 | return err 128 | } 129 | } 130 | 131 | if strings.TrimSpace(s.redisURL) == "" { 132 | s.broker = process.NewCommunitiesBroker() 133 | } else { 134 | s.broker = redis.NewCommunitiesBroker() 135 | } 136 | 137 | if err := s.broker.Open(s.ctx, s.redisURL); err != nil { 138 | return err 139 | } 140 | 141 | var auth authn.Authn 142 | if strings.TrimSpace(s.config.OIDCIssuer) == "" && strings.TrimSpace(s.config.OIDCClientID) == "" { 143 | auth = basic.NewAuthn(s.config.APIUsername, s.config.APIPassword) 144 | } else { 145 | auth = oidc.NewAuthn(s.config.OIDCIssuer, s.config.OIDCClientID) 146 | } 147 | 148 | if err := auth.Open(s.ctx); err != nil { 149 | return err 150 | } 151 | 152 | s.srv = &http.Server{Addr: addr.String()} 153 | 154 | s.connections = map[string]map[string]connection{} 155 | 156 | kicks, closeKicks := s.broker.SubscribeToKicks(s.ctx, s.errs) 157 | s.closeKicks = closeKicks 158 | 159 | s.srv.Handler = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { 160 | raddr := uuid.New().String() 161 | 162 | defer func() { 163 | err := recover() 164 | 165 | switch err { 166 | case nil: 167 | log.Debug(). 168 | Str("address", raddr). 169 | Msg("Closed connection for client") 170 | case http.StatusUnauthorized: 171 | fallthrough 172 | case http.StatusNotImplemented: 173 | log.Debug(). 174 | Err(err.(error)). 175 | Str("address", raddr). 176 | Msg("Closed connection for client") 177 | case http.StatusNotFound: 178 | log.Debug(). 179 | Err(err.(error)). 180 | Str("address", raddr). 181 | Msg("Closed connection for client") 182 | default: 183 | rw.WriteHeader(http.StatusInternalServerError) 184 | 185 | log.Debug(). 186 | Err(err.(error)). 187 | Str("address", raddr). 188 | Msg("Closed connection for client") 189 | } 190 | }() 191 | 192 | switch r.Method { 193 | case http.MethodGet: 194 | community := r.URL.Query().Get("community") 195 | if strings.TrimSpace(community) == "" { 196 | if !managementAPIEnabled { 197 | rw.WriteHeader(http.StatusNotImplemented) 198 | 199 | panic(fmt.Errorf("%v", http.StatusNotImplemented)) 200 | } 201 | 202 | // List communities 203 | u, p, ok := r.BasicAuth() 204 | if err := auth.Validate(u, p); !ok || err != nil { 205 | rw.WriteHeader(http.StatusUnauthorized) 206 | 207 | panic(fmt.Errorf("%v", http.StatusUnauthorized)) 208 | } 209 | 210 | pc, err := s.db.GetCommunities(s.ctx) 211 | if err != nil { 212 | panic(err) 213 | } 214 | 215 | j, err := json.Marshal(pc) 216 | if err != nil { 217 | panic(err) 218 | } 219 | 220 | if _, err := fmt.Fprint(rw, string(j)); err != nil { 221 | panic(err) 222 | } 223 | 224 | return 225 | } 226 | 227 | // Create ephemeral community 228 | password := r.URL.Query().Get("password") 229 | if strings.TrimSpace(password) == "" { 230 | panic(errMissingPassword) 231 | } 232 | 233 | if err := s.db.AddClientsToCommunity(s.ctx, community, password, s.config.EphemeralCommunities); err != nil { 234 | if err == authn.ErrWrongPassword || err == persisters.ErrEphemeralCommunitiesDisabled { 235 | rw.WriteHeader(http.StatusUnauthorized) 236 | 237 | panic(fmt.Errorf("%v", http.StatusUnauthorized)) 238 | } else { 239 | panic(err) 240 | } 241 | } 242 | 243 | defer func() { 244 | if err := s.db.RemoveClientFromCommunity(s.ctx, community); err != nil { 245 | panic(err) 246 | } 247 | }() 248 | 249 | conn, err := upgrader.Upgrade(rw, r, nil) 250 | if err != nil { 251 | panic(err) 252 | } 253 | 254 | defer func() { 255 | s.connectionsLock.Lock() 256 | delete(s.connections[community], raddr) 257 | if len(s.connections[community]) <= 0 { 258 | delete(s.connections, community) 259 | } 260 | s.connectionsLock.Unlock() 261 | 262 | log.Debug(). 263 | Str("address", raddr). 264 | Str("community", community). 265 | Msg("Disconnected from client") 266 | 267 | if s.config.OnDisconnect != nil { 268 | s.config.OnDisconnect(raddr, community, err) 269 | } 270 | 271 | if err := conn.Close(); err != nil { 272 | panic(err) 273 | } 274 | }() 275 | 276 | s.connectionsLock.Lock() 277 | if _, exists := s.connections[community]; !exists { 278 | s.connections[community] = map[string]connection{} 279 | } 280 | s.connections[community][raddr] = connection{ 281 | conn: conn, 282 | closer: make(chan struct{}), 283 | } 284 | s.connectionsLock.Unlock() 285 | 286 | log.Debug(). 287 | Str("address", raddr). 288 | Str("community", community). 289 | Msg("Connected from client") 290 | 291 | if s.config.OnConnect != nil { 292 | s.config.OnConnect(raddr, community) 293 | } 294 | 295 | if err := conn.SetReadDeadline(time.Now().Add(s.config.Heartbeat)); err != nil { 296 | panic(err) 297 | } 298 | conn.SetPongHandler(func(string) error { 299 | return conn.SetReadDeadline(time.Now().Add(s.config.Heartbeat)) 300 | }) 301 | 302 | pings := time.NewTicker(s.config.Heartbeat / 2) 303 | defer pings.Stop() 304 | 305 | errs := make(chan error) 306 | go func() { 307 | for { 308 | messageType, p, err := conn.ReadMessage() 309 | if err != nil { 310 | if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure, websocket.CloseNoStatusReceived) { 311 | errs <- err 312 | } 313 | 314 | errs <- nil 315 | 316 | return 317 | } 318 | 319 | log.Debug(). 320 | Str("address", raddr). 321 | Str("community", community). 322 | Int("type", messageType). 323 | Msg("Received message") 324 | 325 | if err := s.broker.PublishInput(s.ctx, brokers.Input{ 326 | Raddr: raddr, 327 | MessageType: messageType, 328 | P: p, 329 | }, community); err != nil { 330 | errs <- err 331 | 332 | return 333 | } 334 | } 335 | }() 336 | 337 | inputs, closeInputs := s.broker.SubscribeToInputs(s.ctx, errs, community) 338 | defer func() { 339 | if err := closeInputs(); err != nil { 340 | panic(err) 341 | } 342 | }() 343 | 344 | for { 345 | select { 346 | case <-s.connections[community][raddr].closer: 347 | return 348 | case err := <-errs: 349 | panic(err) 350 | case input := <-inputs: 351 | // Prevent sending message back to sender 352 | if input.Raddr == raddr { 353 | continue 354 | } 355 | 356 | if err := conn.WriteMessage(input.MessageType, input.P); err != nil { 357 | panic(err) 358 | } 359 | 360 | if err := conn.SetWriteDeadline(time.Now().Add(s.config.Heartbeat)); err != nil { 361 | panic(err) 362 | } 363 | case <-pings.C: 364 | log.Debug(). 365 | Str("address", raddr). 366 | Str("community", community). 367 | Msg("Sending ping to client") 368 | 369 | if err := conn.WriteMessage(websocket.PingMessage, nil); err != nil { 370 | panic(err) 371 | } 372 | 373 | if err := conn.SetWriteDeadline(time.Now().Add(s.config.Heartbeat)); err != nil { 374 | panic(err) 375 | } 376 | } 377 | } 378 | case http.MethodPost: 379 | if !managementAPIEnabled { 380 | rw.WriteHeader(http.StatusNotImplemented) 381 | 382 | panic(fmt.Errorf("%v", http.StatusNotImplemented)) 383 | } 384 | 385 | // Create persistent community 386 | u, p, ok := r.BasicAuth() 387 | if err := auth.Validate(u, p); !ok || err != nil { 388 | rw.WriteHeader(http.StatusUnauthorized) 389 | 390 | panic(fmt.Errorf("%v", http.StatusUnauthorized)) 391 | } 392 | 393 | password := r.URL.Query().Get("password") 394 | if strings.TrimSpace(password) == "" { 395 | panic(errMissingPassword) 396 | } 397 | 398 | community := r.URL.Query().Get("community") 399 | if strings.TrimSpace(community) == "" { 400 | panic(errMissingCommunity) 401 | } 402 | 403 | c, err := s.db.CreatePersistentCommunity(s.ctx, community, password) 404 | if err != nil { 405 | panic(err) 406 | } 407 | 408 | cc := persisters.Community{ 409 | ID: c.ID, 410 | Clients: c.Clients, 411 | Persistent: c.Persistent, 412 | } 413 | 414 | j, err := json.Marshal(cc) 415 | if err != nil { 416 | panic(err) 417 | } 418 | 419 | if _, err := fmt.Fprint(rw, string(j)); err != nil { 420 | panic(err) 421 | } 422 | 423 | return 424 | case http.MethodDelete: 425 | if !managementAPIEnabled { 426 | rw.WriteHeader(http.StatusNotImplemented) 427 | 428 | panic(fmt.Errorf("%v", http.StatusNotImplemented)) 429 | } 430 | 431 | // Delete persistent community 432 | u, p, ok := r.BasicAuth() 433 | if err := auth.Validate(u, p); !ok || err != nil { 434 | rw.WriteHeader(http.StatusUnauthorized) 435 | 436 | panic(fmt.Errorf("%v", http.StatusUnauthorized)) 437 | } 438 | 439 | community := r.URL.Query().Get("community") 440 | if strings.TrimSpace(community) == "" { 441 | panic(errMissingCommunity) 442 | } 443 | 444 | if err := s.db.DeleteCommunity(s.ctx, community); err != nil { 445 | if err == sql.ErrNoRows { 446 | rw.WriteHeader(http.StatusNotFound) 447 | 448 | panic(fmt.Errorf("%v", http.StatusNotFound)) 449 | } else { 450 | panic(err) 451 | } 452 | } 453 | 454 | if err := s.broker.PublishKick(s.ctx, brokers.Kick{ 455 | Community: community, 456 | }); err != nil { 457 | panic(err) 458 | } 459 | 460 | return 461 | default: 462 | rw.WriteHeader(http.StatusNotImplemented) 463 | 464 | panic(fmt.Errorf("%v", http.StatusNotImplemented)) 465 | } 466 | }) 467 | 468 | go func() { 469 | for { 470 | kick := <-kicks 471 | 472 | s.connectionsLock.Lock() 473 | c, ok := s.connections[kick.Community] 474 | if !ok { 475 | s.connectionsLock.Unlock() 476 | 477 | continue 478 | } 479 | s.connectionsLock.Unlock() 480 | 481 | for _, conn := range c { 482 | close(conn.closer) 483 | } 484 | } 485 | }() 486 | 487 | go func() { 488 | if err := s.srv.ListenAndServe(); err != nil { 489 | if err == http.ErrServerClosed { 490 | close(s.errs) 491 | 492 | return 493 | } 494 | 495 | s.errs <- err 496 | 497 | return 498 | } 499 | }() 500 | 501 | return nil 502 | } 503 | 504 | // Close stops listening and disconnects from the database and broker 505 | func (s *Signaler) Close() error { 506 | log.Trace().Msg("Closing signaler") 507 | 508 | s.connectionsLock.Lock() 509 | defer s.connectionsLock.Unlock() 510 | for c := range s.connections { 511 | for range s.connections[c] { 512 | if err := s.db.RemoveClientFromCommunity(s.ctx, c); err != nil { 513 | return err 514 | } 515 | } 516 | } 517 | 518 | if err := s.closeKicks(); err != nil { 519 | if err != context.Canceled { 520 | return err 521 | } 522 | } 523 | 524 | if err := s.broker.Close(); err != nil { 525 | if err != context.Canceled && err != rediserr.ErrClosed { 526 | return err 527 | } 528 | } 529 | 530 | if err := s.srv.Shutdown(s.ctx); err != nil { 531 | if err != context.Canceled { 532 | return err 533 | } 534 | } 535 | 536 | return nil 537 | } 538 | 539 | // Wait waits for any errors 540 | func (s *Signaler) Wait() error { 541 | for err := range s.errs { 542 | if err != nil { 543 | return err 544 | } 545 | } 546 | 547 | return nil 548 | } 549 | -------------------------------------------------------------------------------- /pkg/wrtcthr/wrtcthr.go: -------------------------------------------------------------------------------- 1 | package wrtcthr 2 | 3 | import ( 4 | "context" 5 | "crypto/rand" 6 | "math" 7 | "strings" 8 | "time" 9 | 10 | "github.com/rs/zerolog/log" 11 | 12 | "github.com/pojntfx/weron/pkg/services" 13 | "github.com/pojntfx/weron/pkg/wrtcconn" 14 | "github.com/teivah/broadcast" 15 | ) 16 | 17 | const ( 18 | acklen = 100 19 | ) 20 | 21 | // AdapterConfig configures the adapter 22 | type AdapterConfig struct { 23 | *wrtcconn.AdapterConfig 24 | OnSignalerConnect func(string) // Handler to be called when the adapter has connected to the signaler 25 | OnPeerConnect func(string) // Handler to be called when the adapter has connected to a peer 26 | OnPeerDisconnected func(string) // Handler to be called when the adapter has received a message 27 | Server bool // Whether to act as the server 28 | PacketLength int // Length of the packet to measure latency with 29 | PacketCount int // Amount of packets to send before measuring 30 | } 31 | 32 | // Totals are the total statistics 33 | type Totals struct { 34 | ThroughputAverageMB float64 // Average total throughput in megabyte/s 35 | ThroughputAverageMb float64 // Average total throughput in megabit/s 36 | 37 | TransferredMB int // Total transfered amount in megabyte 38 | TransferredDuration time.Duration // Total duration of transfer 39 | 40 | ThroughputMin float64 // Minimum measured throughput 41 | ThroughputMax float64 // Maximum measured throughput 42 | } 43 | 44 | // Acknowledgement is an individual datapoint 45 | type Acknowledgement struct { 46 | ThroughputMB float64 // Average throughput in megabyte/s at this datapoint 47 | ThroughputMb float64 // Average throughput in megabit/s at this datapoint 48 | 49 | TransferredMB int // Transfered amount in megabyte at this datapoint 50 | TransferredDuration time.Duration // Duration of transfer at this datapoint 51 | } 52 | 53 | // Adapter provides a throughput measurement service 54 | type Adapter struct { 55 | signaler string 56 | key string 57 | ice []string 58 | config *AdapterConfig 59 | ctx context.Context 60 | 61 | cancel context.CancelFunc 62 | adapter *wrtcconn.Adapter 63 | 64 | ids chan string 65 | totals chan Totals 66 | acknowledgements chan Acknowledgement 67 | 68 | closer *broadcast.Relay[struct{}] 69 | } 70 | 71 | // NewAdapter creates the adapter 72 | func NewAdapter( 73 | signaler string, 74 | key string, 75 | ice []string, 76 | config *AdapterConfig, 77 | ctx context.Context, 78 | ) *Adapter { 79 | ictx, cancel := context.WithCancel(ctx) 80 | 81 | if config == nil { 82 | config = &AdapterConfig{} 83 | } 84 | 85 | return &Adapter{ 86 | signaler: signaler, 87 | key: key, 88 | ice: ice, 89 | config: config, 90 | ctx: ictx, 91 | 92 | cancel: cancel, 93 | 94 | ids: make(chan string), 95 | totals: make(chan Totals), 96 | acknowledgements: make(chan Acknowledgement), 97 | } 98 | } 99 | 100 | // Open connects the adapter to the signaler 101 | func (a *Adapter) Open() error { 102 | log.Trace().Msg("Opening adapter") 103 | 104 | a.adapter = wrtcconn.NewAdapter( 105 | a.signaler, 106 | a.key, 107 | strings.Split(strings.Join(a.ice, ","), ","), 108 | []string{services.ThroughputPrimary}, 109 | a.config.AdapterConfig, 110 | a.ctx, 111 | ) 112 | 113 | var err error 114 | a.ids, err = a.adapter.Open() 115 | if err != nil { 116 | return err 117 | } 118 | 119 | a.closer = broadcast.NewRelay[struct{}]() 120 | 121 | return err 122 | } 123 | 124 | // Close disconnects the adapter from the signaler and stops all measurements, resulting in the totals being yielded 125 | func (a *Adapter) Close() error { 126 | log.Trace().Msg("Closing adapter") 127 | 128 | a.closer.Close() 129 | 130 | return a.adapter.Close() 131 | } 132 | 133 | // Wait starts the transmission and measurement loop 134 | func (a *Adapter) Wait() error { 135 | errs := make(chan error) 136 | 137 | for { 138 | select { 139 | case <-a.ctx.Done(): 140 | log.Trace().Err(a.ctx.Err()).Msg("Context cancelled") 141 | 142 | if err := a.ctx.Err(); err != context.Canceled { 143 | return err 144 | } 145 | 146 | return nil 147 | case err := <-errs: 148 | return err 149 | case id := <-a.ids: 150 | log.Debug().Str("id", id).Msg("Connected to signaler") 151 | 152 | if a.config.OnSignalerConnect != nil { 153 | a.config.OnSignalerConnect(id) 154 | } 155 | case peer := <-a.adapter.Accept(): 156 | log.Debug().Str("channelID", peer.ChannelID).Str("peerID", peer.PeerID).Msg("Connected to peer") 157 | 158 | if a.config.Server { 159 | go func() { 160 | defer func() { 161 | log.Debug().Str("channelID", peer.ChannelID).Str("peerID", peer.PeerID).Msg("Disconnected from peer") 162 | 163 | if a.config.OnPeerDisconnected != nil { 164 | a.config.OnPeerDisconnected(peer.PeerID) 165 | } 166 | }() 167 | 168 | if a.config.OnPeerConnect != nil { 169 | a.config.OnPeerConnect(peer.PeerID) 170 | } 171 | 172 | for { 173 | read := 0 174 | for i := 0; i < a.config.PacketCount; i++ { 175 | if i == 0 { 176 | log.Debug(). 177 | Str("channelID", peer.ChannelID). 178 | Str("peerID", peer.PeerID). 179 | Msg("Started receiving data") 180 | } 181 | 182 | buf := make([]byte, a.config.PacketLength) 183 | 184 | n, err := peer.Conn.Read(buf) 185 | if err != nil { 186 | log.Debug(). 187 | Err(err). 188 | Str("channelID", peer.ChannelID). 189 | Str("peerID", peer.PeerID). 190 | Msg("Could not read from peer, stopping") 191 | 192 | return 193 | } 194 | 195 | read += n 196 | } 197 | 198 | log.Debug(). 199 | Str("channelID", peer.ChannelID). 200 | Str("peerID", peer.PeerID). 201 | Msg("Acknowledging received data") 202 | 203 | if _, err := peer.Conn.Write(make([]byte, acklen)); err != nil { 204 | log.Debug(). 205 | Err(err). 206 | Str("channelID", peer.ChannelID). 207 | Str("peerID", peer.PeerID). 208 | Msg("Could not write to peer, stopping") 209 | 210 | return 211 | } 212 | } 213 | }() 214 | } else { 215 | go func() { 216 | if a.config.OnPeerConnect != nil { 217 | a.config.OnPeerConnect(peer.PeerID) 218 | } 219 | 220 | totalTransferred := 0 221 | totalStart := time.Now() 222 | 223 | minSpeed := math.MaxFloat64 224 | maxSpeed := float64(0) 225 | 226 | printTotals := func() { 227 | if totalTransferred >= 1 { 228 | totalDuration := time.Since(totalStart) 229 | 230 | totalSpeed := (float64(totalTransferred) / totalDuration.Seconds()) / 1000000 231 | 232 | a.totals <- Totals{ 233 | ThroughputAverageMB: totalSpeed, 234 | ThroughputAverageMb: totalSpeed * 8, 235 | TransferredMB: totalTransferred / 1000000, 236 | TransferredDuration: totalDuration, 237 | ThroughputMin: minSpeed, 238 | ThroughputMax: maxSpeed, 239 | } 240 | } 241 | } 242 | 243 | go func() { 244 | c := a.closer.Listener(0) 245 | defer c.Close() 246 | 247 | <-c.Ch() 248 | 249 | printTotals() 250 | }() 251 | 252 | defer func() { 253 | printTotals() 254 | 255 | if a.config.OnPeerDisconnected != nil { 256 | a.config.OnPeerDisconnected(peer.PeerID) 257 | } 258 | }() 259 | 260 | for { 261 | start := time.Now() 262 | 263 | written := 0 264 | for i := 0; i < a.config.PacketCount; i++ { 265 | buf := make([]byte, a.config.PacketLength) 266 | if _, err := rand.Read(buf); err != nil { 267 | errs <- err 268 | 269 | return 270 | } 271 | 272 | n, err := peer.Conn.Write(buf) 273 | if err != nil { 274 | log.Debug(). 275 | Err(err). 276 | Str("channelID", peer.ChannelID). 277 | Str("peerID", peer.PeerID). 278 | Msg("Could not write to peer, stopping") 279 | 280 | return 281 | } 282 | 283 | written += n 284 | } 285 | 286 | buf := make([]byte, acklen) 287 | if _, err := peer.Conn.Read(buf); err != nil { 288 | log.Debug(). 289 | Err(err). 290 | Str("channelID", peer.ChannelID). 291 | Str("peerID", peer.PeerID). 292 | Msg("Could not read from peer, stopping") 293 | 294 | return 295 | } 296 | 297 | duration := time.Since(start) 298 | 299 | speed := (float64(written) / duration.Seconds()) / 1000000 300 | 301 | if speed < float64(minSpeed) { 302 | minSpeed = speed 303 | } 304 | 305 | if speed > float64(maxSpeed) { 306 | maxSpeed = speed 307 | } 308 | 309 | a.acknowledgements <- Acknowledgement{ 310 | ThroughputMB: speed, 311 | ThroughputMb: speed * 8, 312 | TransferredMB: written / 1000000, 313 | TransferredDuration: duration, 314 | } 315 | 316 | totalTransferred += written 317 | } 318 | }() 319 | } 320 | } 321 | } 322 | } 323 | 324 | // GatherTotals yields the total statistics 325 | func (a *Adapter) GatherTotals() { 326 | a.closer.NotifyCtx(a.ctx, struct{}{}) 327 | } 328 | 329 | // Totals returns a channel on which all total statistics will be sent 330 | func (a *Adapter) Totals() chan Totals { 331 | return a.totals 332 | } 333 | 334 | // Acknowledgements returns a channel on which all individual datapoints will be sent 335 | func (a *Adapter) Acknowledgements() chan Acknowledgement { 336 | return a.acknowledgements 337 | } 338 | --------------------------------------------------------------------------------