├── .gitignore ├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── Makefile ├── go.mod ├── README.md ├── LICENSE ├── netdrop_test.go ├── go.sum └── netdrop.go /.gitignore: -------------------------------------------------------------------------------- 1 | /netdrop 2 | /netdrop.pi 3 | /netdrop.mac 4 | /netdrop.exe 5 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | strategy: 8 | matrix: 9 | go-version: [1.16.x, 1.17.x] 10 | platform: [ubuntu-latest, macos-latest, windows-latest] 11 | runs-on: ${{ matrix.platform }} 12 | steps: 13 | - name: Install Go 14 | uses: actions/setup-go@v2 15 | with: 16 | go-version: ${{ matrix.go-version }} 17 | - name: Checkout code 18 | uses: actions/checkout@v2 19 | - name: Test 20 | run: | 21 | go test -race ./... 22 | go build . 23 | 24 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean test 2 | 3 | VERSION:=$(shell git describe --always --tags) 4 | GO_FILES:=$(wildcard *.go) 5 | 6 | cross: $(GO_FILES) netdrop 7 | GOOS=windows go build -ldflags "-X main.Version=$(VERSION)" . 8 | GOOS=darwin go build -o netdrop.mac -ldflags "-X main.Version=$(VERSION)" . 9 | GOOS=linux GOARCH=arm go build -o netdrop.pi -ldflags "-X main.Version=$(VERSION)" . 10 | 11 | netdrop: test $(GO_FILES) 12 | go build -o $@ -ldflags "-X main.Version=$(VERSION)" . 13 | 14 | install: netdrop 15 | install -Dm 0755 netdrop ~/.local/bin/netdrop 16 | 17 | test: 18 | go test . 19 | 20 | clean: 21 | git clean -fd 22 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/klingtnet/netdrop 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect 7 | github.com/dustinkirkland/golang-petname v0.0.0-20191129215211-8e5a1ed0cff0 8 | github.com/grandcat/zeroconf v1.0.0 9 | github.com/miekg/dns v1.1.42 // indirect 10 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 11 | github.com/secure-io/sio-go v0.3.1 12 | github.com/stretchr/testify v1.6.1 13 | github.com/urfave/cli/v2 v2.3.0 14 | golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a // indirect 15 | golang.org/x/net v0.0.0-20210525063256-abc453219eb5 // indirect 16 | golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea // indirect 17 | ) 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # netdrop 2 | 3 | ![CI](https://github.com/klingtnet/netdrop/workflows/CI/badge.svg) 4 | 5 | [Releases](https://github.com/klingtnet/netdrop/releases) 6 | 7 | With netdrop you can send files or stream data from one peer to another inside a local network. 8 | Data is encrypted on transport and there is zero configuration necessary, just share the password with the receiver. 9 | 10 | netdrop uses zeroconf to detect peers which saves you from entering IP addresses. 11 | 12 | ## Installation 13 | 14 | Just run `make install` or call `go run .` to run the application. 15 | 16 | ## Usage 17 | 18 | In the following there are examples for some typical use cases of the tool. 19 | 20 | ### Send a File 21 | 22 | ```sh 23 | # on the server side 24 | $ netdrop send /path/to/my.file 25 | password: usable-barnacle 26 | waiting for connection... 27 | # on the receiver side 28 | $ netdrop receive usable-barnacle > /destination/my.file 29 | ``` 30 | 31 | ### Pipe Through the Network 32 | 33 | One example is to share a folder by piping the tar output. 34 | 35 | ```sh 36 | # on the server side 37 | $ tar -c /path/to/dir | netdrop send 38 | tar -c ~/Downloads/wallpapers | netdrop send 39 | password: climbing-crow 40 | # on the receiver side 41 | $ netdrop receive climbing-crow | tar -xC /destination/dir 42 | ``` -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2020, Andreas Linz 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | 31 | -------------------------------------------------------------------------------- /netdrop_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "net" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestSendAndReceive(t *testing.T) { 13 | listener, err := net.Listen("tcp", listenAddr) 14 | require.NoError(t, err, "net.Listen") 15 | tcpListener, ok := listener.(*net.TCPListener) 16 | require.NoError(t, err, "listener not a TCPListener") 17 | defer tcpListener.Close() 18 | 19 | go func() { 20 | conn, err := tcpListener.AcceptTCP() 21 | require.NoError(t, err, "listener.Accept") 22 | defer conn.Close() 23 | err = handleSend(conn, bytes.NewBufferString("my-test-string"), "test-password") 24 | require.NoError(t, err, "handleSend") 25 | }() 26 | 27 | outBuf, errBuf := bytes.NewBuffer(nil), bytes.NewBuffer(nil) 28 | addr, ok := tcpListener.Addr().(*net.TCPAddr) 29 | require.True(t, ok, "listener addr not a TCP Addr") 30 | err = receiveFrom(context.TODO(), addr.String(), "test-password", outBuf, errBuf) 31 | require.NoError(t, err, "receiveFrom") 32 | require.Equal(t, "my-test-string", outBuf.String(), "outBuf") 33 | require.Empty(t, errBuf.String(), "errBuf") 34 | } 35 | 36 | func TestWrongPassword(t *testing.T) { 37 | listener, err := net.Listen("tcp", listenAddr) 38 | require.NoError(t, err, "net.Listen") 39 | tcpListener, ok := listener.(*net.TCPListener) 40 | require.NoError(t, err, "listener not a TCPListener") 41 | defer tcpListener.Close() 42 | 43 | go func() { 44 | conn, err := tcpListener.AcceptTCP() 45 | require.NoError(t, err, "listener.Accept") 46 | defer conn.Close() 47 | err = handleSend(conn, bytes.NewBufferString("my-test-string"), "test-password") 48 | require.EqualError(t, err, ErrWrongPassword.Error(), "handleSend") 49 | }() 50 | 51 | outBuf, errBuf := bytes.NewBuffer(nil), bytes.NewBuffer(nil) 52 | addr, ok := tcpListener.Addr().(*net.TCPAddr) 53 | require.True(t, ok, "listener addr not a TCP Addr") 54 | err = receiveFrom(context.TODO(), addr.String(), "not-the-test-password", outBuf, errBuf) 55 | require.EqualError(t, err, ErrWrongPassword.Error(), "handleSend") 56 | require.Empty(t, outBuf.String(), "outBuf") 57 | require.Empty(t, errBuf.String(), "errBuf") 58 | } 59 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | tags: "v*" 5 | jobs: 6 | build: 7 | name: Create Release 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout code 11 | uses: actions/checkout@v2 12 | - name: Install Go 13 | uses: actions/setup-go@v2 14 | with: 15 | go-version: 1.17.x 16 | - name: Build 17 | run: | 18 | make cross 19 | - name: Create Release 20 | id: create_release 21 | uses: actions/create-release@v1 22 | env: 23 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 24 | with: 25 | tag_name: ${{ github.ref }} 26 | release_name: Release ${{ github.ref }} 27 | draft: false 28 | prerelease: false 29 | - name: Upload Linux Build 30 | id: upload-linux-build 31 | uses: actions/upload-release-asset@v1 32 | env: 33 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 34 | with: 35 | upload_url: ${{ steps.create_release.outputs.upload_url }} 36 | asset_path: ./netdrop 37 | asset_name: netdrop 38 | asset_content_type: application/octet-stream 39 | - name: Upload Windows Build 40 | id: upload-windows-build 41 | uses: actions/upload-release-asset@v1 42 | env: 43 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 44 | with: 45 | upload_url: ${{ steps.create_release.outputs.upload_url }} 46 | asset_path: ./netdrop.exe 47 | asset_name: netdrop.exe 48 | asset_content_type: application/octet-stream 49 | - name: Upload Mac Build 50 | id: upload-mac-build 51 | uses: actions/upload-release-asset@v1 52 | env: 53 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 54 | with: 55 | upload_url: ${{ steps.create_release.outputs.upload_url }} 56 | asset_path: ./netdrop.mac 57 | asset_name: netdrop.mac 58 | asset_content_type: application/octet-stream 59 | - name: Upload Raspberry Pi Build 60 | id: upload-raspberry-pi-build 61 | uses: actions/upload-release-asset@v1 62 | env: 63 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 64 | with: 65 | upload_url: ${{ steps.create_release.outputs.upload_url }} 66 | asset_path: ./netdrop.pi 67 | asset_name: netdrop.pi 68 | asset_content_type: application/octet-stream -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 2 | github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= 3 | github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= 4 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 5 | github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM= 6 | github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 7 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 8 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/dustinkirkland/golang-petname v0.0.0-20191129215211-8e5a1ed0cff0 h1:90Ly+6UfUypEF6vvvW5rQIv9opIL8CbmW9FT20LDQoY= 10 | github.com/dustinkirkland/golang-petname v0.0.0-20191129215211-8e5a1ed0cff0/go.mod h1:V+Qd57rJe8gd4eiGzZyg4h54VLHmYVVw54iMnlAMrF8= 11 | github.com/grandcat/zeroconf v1.0.0 h1:uHhahLBKqwWBV6WZUDAT71044vwOTL+McW0mBJvo6kE= 12 | github.com/grandcat/zeroconf v1.0.0/go.mod h1:lTKmG1zh86XyCoUeIHSA4FJMBwCJiQmGfcP2PdzytEs= 13 | github.com/miekg/dns v1.1.27/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= 14 | github.com/miekg/dns v1.1.42 h1:gWGe42RGaIqXQZ+r3WUGEKBEtvPHY2SXo4dqixDNxuY= 15 | github.com/miekg/dns v1.1.42/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4= 16 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 17 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 18 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 19 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 20 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 21 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 22 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 23 | github.com/secure-io/sio-go v0.3.1 h1:dNvY9awjabXTYGsTF1PiCySl9Ltofk9GA3VdWlo7rRc= 24 | github.com/secure-io/sio-go v0.3.1/go.mod h1:+xbkjDzPjwh4Axd07pRKSNriS9SCiYksWnZqdnfpQxs= 25 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 26 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 27 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 28 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 29 | github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M= 30 | github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= 31 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 32 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 33 | golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 34 | golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a h1:kr2P4QFmQr29mSLA43kwrOcgcReGTfbE9N577tCTuBc= 35 | golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= 36 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 37 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 38 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 39 | golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 40 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 41 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 42 | golang.org/x/net v0.0.0-20210525063256-abc453219eb5 h1:wjuX4b5yYQnEQHzd+CBcrcC6OVR2J1CN6mUy0oSxIPo= 43 | golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 44 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 45 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= 46 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 47 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 48 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 49 | golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 50 | golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 51 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 52 | golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 53 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 54 | golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea h1:+WiDlPBBaO+h9vPNZi8uJ3k4BkKQB7Iow3aqwHVA5hI= 55 | golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 56 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 57 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 58 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 59 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 60 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 61 | golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 62 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 63 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 64 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 65 | gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 66 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 67 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 68 | -------------------------------------------------------------------------------- /netdrop.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "crypto/aes" 6 | "crypto/cipher" 7 | "crypto/sha256" 8 | "encoding/hex" 9 | "errors" 10 | "fmt" 11 | "io" 12 | "log" 13 | "net" 14 | "os" 15 | "strings" 16 | "time" 17 | 18 | petname "github.com/dustinkirkland/golang-petname" 19 | "github.com/grandcat/zeroconf" 20 | "github.com/secure-io/sio-go" 21 | "github.com/urfave/cli/v2" 22 | ) 23 | 24 | var ( 25 | // Version is the applications build version. 26 | Version = "unset" 27 | ) 28 | 29 | func main() { 30 | app := &cli.App{ 31 | Name: "netdrop", 32 | Usage: "share data encrypted between peers in a network", 33 | Description: `With netdrop you can send files or stream data from one peer to another inside a local network. Data is encrypted on transport and there is zero configuration necessary, just share the password with the receiver.`, 34 | Version: Version, 35 | Commands: []*cli.Command{ 36 | { 37 | Name: "send", 38 | Aliases: []string{"s"}, 39 | Action: sendAction, 40 | ArgsUsage: "/path/to/file", 41 | Description: `send a file or a stream of data 42 | 43 | EXAMPLES: 44 | 45 | netdrop send my-picture.png 46 | tar -cz /some/directory | netdrop send`, 47 | }, 48 | { 49 | Name: "receive", 50 | Aliases: []string{"r", "recv"}, 51 | Flags: []cli.Flag{ 52 | &cli.StringFlag{ 53 | Name: "output", 54 | Usage: "/path/to/output/file", 55 | }, 56 | }, 57 | Action: receiveAction, 58 | ArgsUsage: "password", 59 | Description: `receive a file or folder 60 | 61 | EXAMPLES: 62 | 63 | netdrop receive --output my-picture.png 64 | netdrop send | tar -xf-`, 65 | }, 66 | }, 67 | } 68 | err := app.Run(os.Args) 69 | if err != nil { 70 | log.Fatal(err) 71 | } 72 | } 73 | 74 | func receiveAction(c *cli.Context) error { 75 | password := c.Args().First() 76 | filepath := c.String("output") 77 | if filepath == "" { 78 | return receive(c.Context, password, os.Stdout, os.Stderr) 79 | } 80 | 81 | f, err := os.OpenFile(filepath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600) 82 | if err != nil { 83 | return fmt.Errorf("failed to open %q: %w", filepath, err) 84 | } 85 | defer f.Close() 86 | return receive(c.Context, password, f, os.Stderr) 87 | } 88 | 89 | func resolveServices(ctx context.Context) ([]*zeroconf.ServiceEntry, error) { 90 | resolver, err := zeroconf.NewResolver() 91 | if err != nil { 92 | return nil, err 93 | } 94 | serviceCh := make(chan *zeroconf.ServiceEntry) 95 | err = resolver.Lookup(ctx, zeroconfInstance, zeroconfService, zeroconfDomain, serviceCh) 96 | if err != nil { 97 | return nil, err 98 | } 99 | 100 | var entries []*zeroconf.ServiceEntry 101 | select { 102 | case entry := <-serviceCh: 103 | entries = append(entries, entry) 104 | case <-time.After(500 * time.Millisecond): 105 | break 106 | } 107 | return entries, nil 108 | } 109 | 110 | func receive(ctx context.Context, password string, output, stderr io.Writer) error { 111 | entries, err := resolveServices(ctx) 112 | if err != nil { 113 | return fmt.Errorf("failed to resolve netdrop services: %w", err) 114 | } 115 | 116 | for _, entry := range entries { 117 | err = receiveFrom(ctx, fmt.Sprintf("%s:%d", entry.AddrIPv4[0].String(), entry.Port), password, output, stderr) 118 | if err != nil { 119 | if errors.Is(err, ErrWrongPassword) { 120 | continue 121 | } 122 | return err 123 | } 124 | return nil 125 | } 126 | 127 | return fmt.Errorf("nothing to receive") 128 | } 129 | 130 | func receiveFrom(ctx context.Context, addr, password string, output, stderr io.Writer) error { 131 | conn, err := net.Dial("tcp", addr) 132 | if err != nil { 133 | return fmt.Errorf("dial %q failed: %w", addr, err) 134 | } 135 | defer conn.Close() 136 | hash, err := hashPassword(password) 137 | if err != nil { 138 | return fmt.Errorf("password hash failed: %w", err) 139 | } 140 | n, err := conn.Write(hash) 141 | if err != nil { 142 | return fmt.Errorf("sending password hash failed: %w", err) 143 | } 144 | if n != 32 { 145 | return fmt.Errorf("expected 32 bytes to be send but was %d", n) 146 | } 147 | 148 | stream, err := newStreamCipher(password) 149 | if err != nil { 150 | return fmt.Errorf("stream cipher failed: %w", err) 151 | } 152 | zeroNonce := make([]byte, stream.NonceSize()) 153 | decR := stream.DecryptReader(conn, zeroNonce, nil) 154 | 155 | n64, err := io.Copy(output, decR) 156 | if err != nil { 157 | if n64 == 0 { 158 | // EOF 159 | // sio intentionally hides the error cause to prevent side-channel attacks: https://github.com/secure-io/sio-go/pull/58 160 | return ErrWrongPassword 161 | } 162 | return fmt.Errorf("copy failed after %d bytes: %w", n64, err) 163 | } 164 | return nil 165 | } 166 | 167 | const ( 168 | zeroconfService = "_netdrop._tcp" 169 | zeroconfDomain = "local." 170 | zeroconfInstance = "netdrop" 171 | ) 172 | 173 | func announce(port int) (shutdown func(), err error) { 174 | server, err := zeroconf.Register("netdrop", zeroconfService, zeroconfDomain, port, []string{"github.com/klingtnet/netdrop server"}, nil) 175 | if err != nil { 176 | return nil, fmt.Errorf("failed to register zerconf service") 177 | } 178 | 179 | return server.Shutdown, nil 180 | } 181 | 182 | const listenAddr = ":0" 183 | 184 | func sendAction(c *cli.Context) error { 185 | filepath := c.Args().First() 186 | if filepath == "" { 187 | inf, err := os.Stdin.Stat() 188 | if err != nil { 189 | return fmt.Errorf("could not open stdin: %w", err) 190 | } 191 | if inf.Mode()&os.ModeNamedPipe == 0 { 192 | return fmt.Errorf("input is not a pipe") 193 | } 194 | return send(c.Context, os.Stdin, os.Stderr) 195 | } 196 | var f *os.File 197 | f, err := os.Open(filepath) 198 | if err != nil { 199 | return fmt.Errorf("failed to open %q for reading: %w", filepath, err) 200 | } 201 | defer f.Close() 202 | return send(c.Context, f, os.Stderr) 203 | } 204 | 205 | func hashPassword(password string) ([]byte, error) { 206 | hasher := sha256.New() 207 | _, err := hasher.Write([]byte(password)) 208 | if err != nil { 209 | return nil, err 210 | } 211 | return hasher.Sum(nil), nil 212 | } 213 | 214 | func newStreamCipher(password string) (*sio.Stream, error) { 215 | key := make([]byte, 32) 216 | copy(key, []byte(password)) 217 | block, err := aes.NewCipher(key) 218 | if err != nil { 219 | return nil, fmt.Errorf("failed to create encryption cipher: %w", err) 220 | } 221 | gcm, err := cipher.NewGCM(block) 222 | if err != nil { 223 | return nil, fmt.Errorf("failed to create 'authenticated encryption with associated data': %w", err) 224 | } 225 | return sio.NewStream(gcm, sio.BufSize), nil 226 | } 227 | 228 | type Error string 229 | 230 | func (e Error) Error() string { return string(e) } 231 | 232 | const ErrWrongPassword = Error("client sent wrong password") 233 | 234 | func handleSend(conn *net.TCPConn, in io.Reader, password string) error { 235 | actualHash := make([]byte, 32) 236 | nR, err := conn.Read(actualHash) 237 | if err != nil { 238 | return fmt.Errorf("failed to read password hash from client connection: %w", err) 239 | } 240 | if nR != 32 { 241 | return fmt.Errorf("expected 32 bytes but read %d", nR) 242 | } 243 | 244 | expectedHash, err := hashPassword(password) 245 | if err != nil { 246 | return fmt.Errorf("failed to hash password: %w", err) 247 | } 248 | if hex.EncodeToString(expectedHash) != hex.EncodeToString(actualHash) { 249 | return ErrWrongPassword 250 | } 251 | 252 | stream, err := newStreamCipher(password) 253 | if err != nil { 254 | return fmt.Errorf("failed to create stream cipher: %w", err) 255 | } 256 | zeroNonce := make([]byte, stream.NonceSize()) 257 | encW := stream.EncryptWriter(conn, zeroNonce, nil) 258 | defer encW.Close() 259 | 260 | n, err := io.Copy(encW, in) 261 | if err != nil { 262 | return fmt.Errorf("send failed: %w", err) 263 | } 264 | if n == 0 { 265 | return fmt.Errorf("send nothing") 266 | } 267 | return nil 268 | } 269 | 270 | func send(ctx context.Context, in io.Reader, stderr io.Writer) error { 271 | listener, err := net.Listen("tcp", listenAddr) 272 | if err != nil { 273 | return fmt.Errorf("failed to listen on %q: %w", listenAddr, err) 274 | } 275 | tcpListener, ok := listener.(*net.TCPListener) 276 | if !ok { 277 | return fmt.Errorf("not a TCP listener: %#v", listener.Addr()) 278 | } 279 | defer tcpListener.Close() 280 | addr, ok := listener.Addr().(*net.TCPAddr) 281 | if !ok { 282 | return fmt.Errorf("not a TCP addr: %w", err) 283 | } 284 | 285 | shutdownFn, err := announce(addr.Port) 286 | if err != nil { 287 | return fmt.Errorf("zeroconf announcement failed: %w", err) 288 | } 289 | // TODO: catch SIGINT and clean exit 290 | defer shutdownFn() 291 | 292 | petname.NonDeterministicMode() 293 | password := petname.Generate(2, "-") 294 | fmt.Fprintln(stderr, "password:", password) 295 | 296 | for { 297 | fmt.Fprintln(stderr, "waiting for connection...") 298 | conn, err := tcpListener.AcceptTCP() 299 | if err != nil { 300 | return fmt.Errorf("accept failed: %w", err) 301 | } 302 | 303 | err = handleSend(conn, in, password) 304 | if err != nil { 305 | conn.Close() 306 | if errors.Is(err, ErrWrongPassword) { 307 | fmt.Fprintf(stderr, "%q: %s\n", conn.RemoteAddr(), err.Error()) 308 | continue 309 | } 310 | return err 311 | } 312 | // shutdown server 313 | err = conn.Close() 314 | if err != nil { 315 | // ErrNetClosing is not exposed: https://github.com/golang/go/issues/4373 316 | if strings.HasSuffix(err.Error(), "use of closed network connection") { 317 | return nil 318 | } 319 | return fmt.Errorf("close failed: %w", err) 320 | } 321 | return nil 322 | } 323 | } 324 | --------------------------------------------------------------------------------