├── .travis.yml
├── LICENSE
├── README.md
├── android
├── .gitignore
├── README.md
├── app
│ ├── .gitignore
│ ├── build.gradle
│ ├── proguard-rules.pro
│ └── src
│ │ ├── androidTest
│ │ └── java
│ │ │ └── com
│ │ │ └── decoyrouting
│ │ │ └── tapdanceclient
│ │ │ └── ExampleInstrumentedTest.java
│ │ ├── main
│ │ ├── AndroidManifest.xml
│ │ ├── assets
│ │ │ ├── pubkey.dev
│ │ │ └── root.pem
│ │ ├── java
│ │ │ └── com
│ │ │ │ └── decoyrouting
│ │ │ │ └── tapdanceclient
│ │ │ │ └── MainActivity.java
│ │ └── res
│ │ │ ├── drawable
│ │ │ ├── border_stdout.xml
│ │ │ ├── button.xml
│ │ │ └── button_normal.xml
│ │ │ ├── layout
│ │ │ ├── activity_main.xml
│ │ │ └── content_main.xml
│ │ │ ├── menu
│ │ │ └── menu_main.xml
│ │ │ ├── mipmap-hdpi
│ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-mdpi
│ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-xhdpi
│ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-xxhdpi
│ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-xxxhdpi
│ │ │ └── ic_launcher.png
│ │ │ ├── values-v21
│ │ │ └── styles.xml
│ │ │ ├── values-w820dp
│ │ │ └── dimens.xml
│ │ │ └── values
│ │ │ ├── colors.xml
│ │ │ ├── dimens.xml
│ │ │ ├── strings.xml
│ │ │ └── styles.xml
│ │ └── test
│ │ └── java
│ │ └── com
│ │ └── decoyrouting
│ │ └── tapdanceclient
│ │ └── ExampleUnitTest.java
├── build.gradle
├── gradle.properties
├── gradle
│ └── wrapper
│ │ ├── gradle-wrapper.jar
│ │ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── proxybind
│ └── build.gradle
└── settings.gradle
├── assets
├── ClientConf
├── ClientConf.dev
├── roots
└── station_pubkey_test
├── build
├── EmbeddedValues.java.enc
└── TunnelManager.java.patch
├── cli
├── README.md
├── assets
└── main.go
├── gobind
├── README.md
└── gobind.go
├── protobuf
├── README.md
├── extensions.go
├── signalling.pb.go
└── signalling.proto
├── tapdance
├── TODO
├── assets
├── assets.go
├── assets_test.go
├── common.go
├── conn_dual.go
├── conn_flow.go
├── conn_raw.go
├── counter.go
├── dialer.go
├── dialer_test.go
├── logger.go
├── utils.go
└── utils_test.go
├── tdproxy
├── README.md
├── flow.go
├── gengodoc.sh
├── tapdance.go
└── tapdance_test.go
├── test_scripts
├── README.md
├── go-1.7.4_wget.sh
├── ip.sh
├── nc_send.sh
├── seq.py
├── ssh-td.sh
└── twitter_wget.sh
└── tools
└── clientconf.go
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: go
2 | go:
3 | - "1.12.x"
4 | - "1.x"
5 |
6 | os: linux
7 | sudo: required
8 | dist: xenial
9 |
10 | services:
11 | - docker
12 |
13 | addons:
14 | artifacts: true
15 |
16 | notifications:
17 | slack:
18 | secure: ZzIEqFE4XRdE9U2p3aeE32DMtoC8RgjoEavhEQ1oLrWFgUpLktqmp9UVY/U+W6iElilLpDbFpry51+Sv9MWpxJMxr+Q/JJuq/3Bj5KjF/wEtil7qvBYhQ1sM/qUQFG6wRkrMNjZGMiaTmnkWF0rZB8lf7+nbnGFaPW3AVVbD+8gVDWTHI4Hcvvgs0UbrJzoPfpvH0dprOchswc1BBKTgo5c44rvS2fquEMVcqMMiNJ5JQqphuRWLTfzLgOzImSf0/xJJyVp/YTkSnVSg8BcWmDCJ4iB9fJkVyZM9WxcgY/J4T5VzFxfMah9zv2j8UTfzHSMeCJDRL647hdnkmr/Qum/LN91Ey2DJw5KUH743CsAbyGhQML6wZ3NCeEP06hnMDphalU5+BYhtAPyc5CB84g6eLIUQ2EqptuPZpjFQohFnapCTnfB5XKTcW+PjxJsoJzk8x+85Xid+H1nnNxeyf10tLv6Pwy4ZGmEEbsa4SYWXibpIEu3fPJXEdtrht0vM40pDLeUYL6Axmh7hNjmDQOXJG41saF+Rk4AArRhKhMQTmlYCc0e1H2/hIDXUMPbqjHeCpEkaA5W8BFBKynhlJa0JX+rtHDFaK82Di8rXT0NO2ACyG8ZQqk87qePyBYPyfR8hRwhrkmQHlYYOZzV6LBz+ynJuWl9ktcC2irJlHZs=
19 |
20 | ## Golang Test & CLI build
21 | # Go versions: all specified
22 | install:
23 | - go get -t ./...
24 | - go get golang.org/x/lint/golint
25 | - go get github.com/alecthomas/gometalinter
26 |
27 | script:
28 | - go test -race -v ./tapdance
29 | - go test -race -v ./tdproxy
30 | - gometalinter --install
31 | - gometalinter --disable-all -E vet -E gofmt -E misspell -E ineffassign -E deadcode --tests ./tapdance
32 |
33 | ## Build job for Psiphon ConsoleClient and Android App
34 | # Go versions: first value in array
35 | jobs:
36 | include:
37 | - &build # YAML anchor/alias
38 | stage: build
39 | name: "Build cli and Psiphon ConsoleClient on Linux"
40 | install:
41 | - go get ./...
42 | # Substitute build string
43 | - sed -i.bak "s/buildInfo = \"\"/buildInfo = \"$TRAVIS_BRANCH-$TRAVIS_COMMIT\"/" tapdance/logger.go
44 | - mkdir -p $GOPATH/src/github.com/Psiphon-Labs
45 | - git clone https://github.com/Psiphon-Labs/psiphon-tunnel-core.git $GOPATH/src/github.com/Psiphon-Labs/psiphon-tunnel-core
46 | # Remove gotapdance from vendored packages
47 | - go get github.com/kardianos/govendor
48 | - (cd $GOPATH/src/github.com/Psiphon-Labs/psiphon-tunnel-core && govendor remove github.com/sergeyfrolov/gotapdance/...)
49 | # Enable TapDance logging
50 | - sed -i.bak 's/refraction_networking_tapdance.Logger().Out = ioutil.Discard//' $GOPATH/src/github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/tapdance/tapdance.go
51 | script:
52 | - go build -race -o build/cli-$TRAVIS_OS_NAME ./cli
53 | - go build -race -o build/ConsoleClient-$TRAVIS_OS_NAME -tags 'TAPDANCE' github.com/Psiphon-Labs/psiphon-tunnel-core/ConsoleClient
54 | after_success:
55 | # Upload built binaries to S3
56 | - sudo pip install awscli
57 | - if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then aws s3 sync build s3://$ARTIFACTS_BUCKET/$TRAVIS_REPO_SLUG/$TRAVIS_BRANCH/; fi
58 |
59 | - <<: *build # Same build on OS X
60 | name: "Build cli and Psiphon ConsoleClient on OS X"
61 | os: osx
62 |
63 | - <<: *build # Reuse setup steps from build
64 | name: "Build Psiphon Android Library and App"
65 | before_script:
66 | # Get Android build environment
67 | - docker pull refraction/psiandroid
68 | - mkdir -p $GOPATH/src/bitbucket.org/psiphon
69 | - hg clone https://bitbucket.org/psiphon/psiphon-circumvention-system $GOPATH/src/bitbucket.org/psiphon/psiphon-circumvention-system
70 | # Use modified EmbeddedValues.java for TapDance
71 | - openssl aes-256-cbc -d -K $encrypted_8a9748c534c1_key -iv $encrypted_8a9748c534c1_iv -in build/EmbeddedValues.java.enc -out $GOPATH/src/bitbucket.org/psiphon/psiphon-circumvention-system/Android/app/src/main/java/com/psiphon3/psiphonlibrary/EmbeddedValues.java
72 | # Patched tunneling protocol for TapDance
73 | - patch $GOPATH/src/bitbucket.org/psiphon/psiphon-circumvention-system/Android/app/src/main/java/com/psiphon3/psiphonlibrary/TunnelManager.java build/TunnelManager.java.patch
74 | script:
75 | # Build Psiphon Android Library ca.psiphon.aar
76 | - docker run -v $TRAVIS_BUILD_DIR:/go/src/github.com/sergeyfrolov/gotapdance -v $GOPATH/src/github.com/Psiphon-Labs/psiphon-tunnel-core:/go/src/github.com/Psiphon-Labs/psiphon-tunnel-core refraction/psiandroid /bin/bash -c 'cd /go/src/github.com/Psiphon-Labs/psiphon-tunnel-core/MobileLibrary/Android && ./make.bash "TAPDANCE"'
77 | - mv $GOPATH/src/github.com/Psiphon-Labs/psiphon-tunnel-core/MobileLibrary/Android/ca.psiphon.aar build/
78 | # Build Psiphon Android App PsiphonAndroid-debug.apk
79 | - cp build/ca.psiphon.aar $GOPATH/src/bitbucket.org/psiphon/psiphon-circumvention-system/Android/app/libs/
80 | - docker run -v $TRAVIS_BUILD_DIR:/go/src/github.com/sergeyfrolov/gotapdance -v $GOPATH/src/bitbucket.org/psiphon/psiphon-circumvention-system/Android:/go/src/bitbucket.org/psiphon/psiphon-circumvention-system/Android refraction/psiandroid /bin/bash -c 'yes | /android-sdk-linux/tools/bin/sdkmanager --update && cd /go/src/bitbucket.org/psiphon/psiphon-circumvention-system/Android && ./gradlew assembleDebug'
81 | - sudo mv $GOPATH/src/bitbucket.org/psiphon/psiphon-circumvention-system/Android/app/build/outputs/apk/debug/PsiphonAndroid-debug.apk build/
82 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
178 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | TapDance is a free-to-use anti-censorship technology, protected from enumeration attacks.
6 |
7 |
8 |
9 |
10 |
11 |
12 | # Build
13 | ## Download Golang and TapDance and dependencies
14 | 0. Install [Golang](https://golang.org/dl/) (currently tested against version 1.10 and latest).
15 |
16 | 1. Get source code for Go TapDance and all dependencies:
17 |
18 | ```bash
19 | go get -d -u -t github.com/sergeyfrolov/gotapdance/...
20 | ```
21 | Ignore the "no buildable Go source files" warning.
22 |
23 | If you have outdated versions of libraries used, you might want to do `go get -u all`.
24 |
25 | ## Usage
26 |
27 | There are 3 supported ways to use TapDance:
28 |
29 | * [Command Line Interface client](cli)
30 |
31 | * [Psiphon](https://psiphon.ca/) Android app integrated TapDance as one of their transports.
32 |
33 | * Use tapdance directly from other Golang program:
34 |
35 | ```Golang
36 | package main
37 |
38 | import (
39 | "github.com/sergeyfrolov/gotapdance/tapdance"
40 | "fmt"
41 | )
42 |
43 | func main() {
44 | // first, copy ClientConf and roots files into assets directory
45 | // make sure assets directory is writable (only) by the td process
46 | tapdance.AssetsSetDir("./path/to/assets/dir/")
47 |
48 | tdConn, err := tapdance.Dial("tcp", "censoredsite.com:80")
49 | if err != nil {
50 | fmt.Printf("tapdance.Dial() failed: %+v\n", err)
51 | return
52 | }
53 | // tdConn implements standard net.Conn, allowing to use it like any other Golang conn with
54 | // Write(), Read(), Close() etc. It also allows to pass tdConn to functions that expect
55 | // net.Conn, such as tls.Client() making it easy to do tls handshake over TapDance conn.
56 | _, err = tdConn.Write([]byte("GET / HTTP/1.1\nHost: censoredsite.com\n\n"))
57 | if err != nil {
58 | fmt.Printf("tdConn.Write() failed: %+v\n", err)
59 | return
60 | }
61 | buf := make([]byte, 16384)
62 | _, err = tdConn.Read(buf)
63 | // ...
64 | }
65 | ```
66 |
67 | * [CURRENTLY NOT MAINTAINED] Standalone TapDance mobile applications that use [Golang Bindings](gobind) as a shared library.
68 |
69 | * [Android application in Java](android)
70 |
71 |
72 | # Links
73 |
74 | [Refraction Networking](https://refraction.network) is an umberlla term for the family of similarly working technnologies.
75 |
76 | TapDance station code released for FOCI'17 on github: [refraction-networking/tapdance](https://github.com/refraction-networking/tapdance)
77 |
78 | Original 2014 paper: ["TapDance: End-to-Middle Anticensorship without Flow Blocking"](https://ericw.us/trow/tapdance-sec14.pdf)
79 |
80 | Newer(2017) paper that shows TapDance working at high-scale: ["An ISP-Scale Deployment of TapDance"](https://sfrolov.io/papers/foci17-paper-frolov_0.pdf)
81 |
--------------------------------------------------------------------------------
/android/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/workspace.xml
5 | /.idea/libraries
6 | .DS_Store
7 | /build
8 | /captures
9 | .externalNativeBuild
10 | .idea
--------------------------------------------------------------------------------
/android/README.md:
--------------------------------------------------------------------------------
1 | # Build
2 | The project is built with Gradle.
3 |
4 | ### Command line
5 | Assemble app.
6 | ````
7 | bash$ cd gotapdance/android/app
8 | bash$ ../gradlew assemble
9 | ````
10 | Install app on device or emulator.
11 | ````
12 | bash$ cd gotapdance/android/app
13 | bash$ ../gradlew install
14 | ````
15 | Developer mode have to be enabled on device.
16 |
17 | ### IDE
18 | Tapdance also can be built from Android Studio or Intellij Idea via standard interface.
19 |
20 | # Screenshot
21 | One of the latest versions looks like this:
22 |
23 |
24 |
--------------------------------------------------------------------------------
/android/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/android/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 |
3 | buildDir = 'build'
4 |
5 | android {
6 | compileSdkVersion 25
7 | buildToolsVersion "25.0.0"
8 | defaultConfig {
9 | applicationId "com.decoyrouting.tapdanceclient"
10 | minSdkVersion 15
11 | targetSdkVersion 25
12 | versionCode 1
13 | versionName "1.0"
14 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
15 | ndk {
16 | abiFilters "armeabi-v7a", "x86"
17 | }
18 | }
19 | buildTypes {
20 | release {
21 | minifyEnabled false
22 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
23 | }
24 | }
25 | }
26 |
27 |
28 | dependencies {
29 | compile fileTree(include: ['*.jar'], dir: 'libs')
30 | androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
31 | exclude group: 'com.android.support', module: 'support-annotations'
32 | })
33 | compile 'com.android.support:appcompat-v7:25.0.0'
34 | compile 'com.android.support:design:25.0.0'
35 | testCompile 'junit:junit:4.12'
36 | compile project(':proxybind')
37 | }
38 |
--------------------------------------------------------------------------------
/android/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in /home/sfrolov/Android/Sdk/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
12 | # If your project uses WebView with JS, uncomment the following
13 | # and specify the fully qualified class name to the JavaScript interface
14 | # class:
15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
16 | # public *;
17 | #}
18 |
--------------------------------------------------------------------------------
/android/app/src/androidTest/java/com/decoyrouting/tapdanceclient/ExampleInstrumentedTest.java:
--------------------------------------------------------------------------------
1 | package com.decoyrouting.tapdanceclient;
2 |
3 | import android.content.Context;
4 | import android.support.test.InstrumentationRegistry;
5 | import android.support.test.runner.AndroidJUnit4;
6 |
7 | import org.junit.Test;
8 | import org.junit.runner.RunWith;
9 |
10 | import static org.junit.Assert.*;
11 |
12 | /**
13 | * Instrumentation test, which will execute on an Android device.
14 | *
15 | * @see Testing documentation
16 | */
17 | @RunWith(AndroidJUnit4.class)
18 | public class ExampleInstrumentedTest {
19 | @Test
20 | public void useAppContext() throws Exception {
21 | // Context of the app under test.
22 | Context appContext = InstrumentationRegistry.getTargetContext();
23 |
24 | assertEquals("com.decoyrouting.tapdanceclient", appContext.getPackageName());
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/android/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
11 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/android/app/src/main/assets/pubkey.dev:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sergeyfrolov/gotapdance/ca457d7d62c306bec43e0545967910d8af03a84a/android/app/src/main/assets/pubkey.dev
--------------------------------------------------------------------------------
/android/app/src/main/java/com/decoyrouting/tapdanceclient/MainActivity.java:
--------------------------------------------------------------------------------
1 | package com.decoyrouting.tapdanceclient;
2 |
3 | import android.os.Bundle;
4 | import android.support.v7.app.AppCompatActivity;
5 | import android.support.v7.widget.Toolbar;
6 | import android.text.method.ScrollingMovementMethod;
7 | import android.view.View;
8 | import android.view.Menu;
9 | import android.view.MenuItem;
10 | import android.widget.Button;
11 | import android.widget.TextView;
12 |
13 | import java.io.IOException;
14 | import java.io.InputStream;
15 |
16 | import go.gobind.*;
17 |
18 | public class MainActivity extends AppCompatActivity {
19 |
20 | @Override
21 | protected void onCreate(Bundle savedInstanceState) {
22 | super.onCreate(savedInstanceState);
23 |
24 | setContentView(R.layout.activity_main);
25 | Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
26 | setSupportActionBar(toolbar);
27 |
28 | final TextView stdout_tv = (TextView) findViewById(R.id.et_stdout);
29 | stdout_tv.setMovementMethod(ScrollingMovementMethod.getInstance());
30 |
31 | final TextView tvState = (TextView) findViewById(R.id.state);
32 | final Button launchButton = (Button) findViewById(R.id.launchButton);
33 |
34 | new Thread() {
35 | @Override
36 | public void run() {
37 | try {
38 | while (!isInterrupted()) {
39 | Thread.sleep(100);
40 | runOnUiThread(new Runnable() {
41 | @Override
42 | public void run() {
43 | String stats = gobind.Gobind.getStats();
44 | Boolean isListening = gobind.Gobind.isListening();
45 | tvState.setText(stats);
46 | if (isListening) {
47 | launchButton.setText("Stop");
48 | } else {
49 | launchButton.setText("Launch");
50 | }
51 | stdout_tv.append(gobind.Gobind.getLog().toString());
52 | }
53 | });
54 | }
55 | } catch (InterruptedException e) {
56 | }
57 | }
58 | }.start();
59 |
60 | launchButton.setOnClickListener(new View.OnClickListener() {
61 | @Override
62 | public void onClick(View v) {
63 | new Thread(new Runnable() {
64 | public void run() {
65 | try {
66 | if (gobind.Gobind.isListening()) {
67 | gobind.Gobind.stop();
68 | } else {
69 | gobind.Gobind.listen();
70 | }
71 | } catch (final Exception ex) {
72 | runOnUiThread(new Runnable() {
73 | @Override
74 | public void run() {
75 | try {
76 | stdout_tv.append(ex.toString() + "\n");
77 | } catch (Exception e) {
78 | }
79 | }
80 | });
81 | }
82 | }
83 | }).start();
84 | }
85 | });
86 |
87 | try {
88 | gobind.Gobind.newDecoyProxy(10500);
89 | } catch (Exception ex) {
90 | System.out.println(ex);
91 | }
92 | }
93 |
94 | @Override
95 | public boolean onCreateOptionsMenu(Menu menu) {
96 | // Inflate the menu; this adds items to the action bar if it is present.
97 | getMenuInflater().inflate(R.menu.menu_main, menu);
98 | return true;
99 | }
100 |
101 | @Override
102 | public boolean onOptionsItemSelected(MenuItem item) {
103 | // Handle action bar item clicks here. The action bar will
104 | // automatically handle clicks on the Home/Up button, so long
105 | // as you specify a parent activity in AndroidManifest.xml.
106 | int id = item.getItemId();
107 |
108 | //noinspection SimplifiableIfStatement
109 | if (id == R.id.action_settings) {
110 | return true;
111 | }
112 |
113 | return super.onOptionsItemSelected(item);
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable/border_stdout.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
8 |
9 |
10 |
11 |
14 |
15 |
16 |
17 |
19 |
20 |
21 |
26 |
27 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable/button.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable/button_normal.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
14 |
--------------------------------------------------------------------------------
/android/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
14 |
15 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/android/app/src/main/res/layout/content_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
16 |
29 |
41 |
58 |
66 |
67 |
--------------------------------------------------------------------------------
/android/app/src/main/res/menu/menu_main.xml:
--------------------------------------------------------------------------------
1 |
5 |
10 |
11 |
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sergeyfrolov/gotapdance/ca457d7d62c306bec43e0545967910d8af03a84a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sergeyfrolov/gotapdance/ca457d7d62c306bec43e0545967910d8af03a84a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sergeyfrolov/gotapdance/ca457d7d62c306bec43e0545967910d8af03a84a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sergeyfrolov/gotapdance/ca457d7d62c306bec43e0545967910d8af03a84a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sergeyfrolov/gotapdance/ca457d7d62c306bec43e0545967910d8af03a84a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/values-v21/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
14 |
15 |
18 |
19 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/android/app/src/main/res/values-w820dp/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 64dp
6 |
7 |
--------------------------------------------------------------------------------
/android/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #7e57c2
4 | #5e35b1
5 | #ede7f6
6 | #311b92
7 |
8 |
--------------------------------------------------------------------------------
/android/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 16dp
4 | 16dp
5 | 16dp
6 |
7 |
--------------------------------------------------------------------------------
/android/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Tapdance Client
3 | Settings
4 |
5 |
--------------------------------------------------------------------------------
/android/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
14 |
15 |
18 |
19 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/android/app/src/test/java/com/decoyrouting/tapdanceclient/ExampleUnitTest.java:
--------------------------------------------------------------------------------
1 | package com.decoyrouting.tapdanceclient;
2 |
3 | import org.junit.Test;
4 |
5 | import static org.junit.Assert.*;
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * @see Testing documentation
11 | */
12 | public class ExampleUnitTest {
13 | @Test
14 | public void addition_isCorrect() throws Exception {
15 | assertEquals(4, 2 + 2);
16 | }
17 | }
--------------------------------------------------------------------------------
/android/build.gradle:
--------------------------------------------------------------------------------
1 | buildDir = 'build'
2 |
3 | buildscript {
4 | repositories {
5 | jcenter()
6 | }
7 | dependencies {
8 | classpath 'com.android.tools.build:gradle:2.2.2'
9 | // NOTE: Do not place your application dependencies here; they belong
10 | // in the individual module build.gradle files
11 | }
12 | }
13 |
14 | allprojects {
15 | repositories {
16 | jcenter()
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/android/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # IDE (e.g. Android Studio) users:
4 | # Gradle settings configured through the IDE *will override*
5 | # any settings specified in this file.
6 |
7 | # For more details on how to configure your build environment visit
8 | # http://www.gradle.org/docs/current/userguide/build_environment.html
9 |
10 | # Specifies the JVM arguments used for the daemon process.
11 | # The setting is particularly useful for tweaking memory settings.
12 | org.gradle.jvmargs=-Xmx1536m
13 |
14 | # When configured, Gradle will run in incubating parallel mode.
15 | # This option should only be used with decoupled projects. More details, visit
16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
17 | # org.gradle.parallel=true
18 |
19 | android.useDeprecatedNdk=true
--------------------------------------------------------------------------------
/android/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sergeyfrolov/gotapdance/ca457d7d62c306bec43e0545967910d8af03a84a/android/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/android/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sun Dec 04 01:24:29 MSK 2016
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-3.2.1-bin.zip
7 |
--------------------------------------------------------------------------------
/android/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn ( ) {
37 | echo "$*"
38 | }
39 |
40 | die ( ) {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save ( ) {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/android/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/android/proxybind/build.gradle:
--------------------------------------------------------------------------------
1 | //configurations.maybeCreate("default")
2 | //artifacts.add("default", file('proxybind.aar'))
3 |
4 | plugins {
5 | id "org.golang.mobile.bind" version "0.2.7"
6 | }
7 |
8 | buildDir = 'build'
9 |
10 | def goPath = System.env.GOPATH
11 | if (!goPath) {
12 | // If GOPATH is unset, try default one
13 | goPath = System.env.HOME + "/go/"
14 | }
15 |
16 | gobind {
17 | pkg = "github.com/sergeyfrolov/gotapdance/gobind"
18 | GOPATH = goPath
19 | GOMOBILE = "$goPath/bin/gomobile"
20 | }
21 |
--------------------------------------------------------------------------------
/android/settings.gradle:
--------------------------------------------------------------------------------
1 | rootProject.name = 'Tapdance'
2 |
3 | include ':app', ':proxybind'
4 |
--------------------------------------------------------------------------------
/assets/ClientConf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sergeyfrolov/gotapdance/ca457d7d62c306bec43e0545967910d8af03a84a/assets/ClientConf
--------------------------------------------------------------------------------
/assets/ClientConf.dev:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sergeyfrolov/gotapdance/ca457d7d62c306bec43e0545967910d8af03a84a/assets/ClientConf.dev
--------------------------------------------------------------------------------
/assets/station_pubkey_test:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sergeyfrolov/gotapdance/ca457d7d62c306bec43e0545967910d8af03a84a/assets/station_pubkey_test
--------------------------------------------------------------------------------
/build/EmbeddedValues.java.enc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sergeyfrolov/gotapdance/ca457d7d62c306bec43e0545967910d8af03a84a/build/EmbeddedValues.java.enc
--------------------------------------------------------------------------------
/build/TunnelManager.java.patch:
--------------------------------------------------------------------------------
1 | @@ -732,6 +732,9 @@
2 | json.put("EgressRegion", egressRegion);
3 | }
4 |
5 | + json.put("TunnelProtocol", "TAPDANCE-OSSH");
6 | + json.put("DisableTactics", true);
7 | +
8 | if (tunnelConfig.disableTimeouts) {
9 | //disable timeouts
10 | MyLog.g("DisableTimeouts", "disableTimeouts", true);
11 |
--------------------------------------------------------------------------------
/cli/README.md:
--------------------------------------------------------------------------------
1 | # Gotapdance CLI version
2 |
3 | # Build
4 | After [downloading Golang, TD and dependencies:](../README.md)
5 | ```
6 | cd ${GOPATH:-~/go}/src/github.com/sergeyfrolov/gotapdance/cli # works even if GOPATH is not set
7 | go build -a .
8 | ```
9 |
10 | # Usage
11 |
12 | Simply run
13 | ```
14 | ./cli
15 | ```
16 | to listen to local connections on default 10500 port.
17 |
18 | Then, you'll have a few options:
19 | ## Configure HTTP proxy
20 | You will need to ask your particular application(e.g. browser) to use 127.0.0.1:10500 as HTTP proxy.
21 | In Firefox (both mobile and desktop) I prefer to type ```about:config``` into address line and set the following:
22 |
23 | ```
24 | network.proxy.http_port = 10500
25 | network.proxy.http = 127.0.0.1
26 | network.proxy.ssl_port = 10500
27 | network.proxy.ssl = 127.0.0.1
28 | network.proxy.type = 1
29 | ```
30 |
31 | To disable proxying you may simply set ```network.proxy.type``` back to ```5``` or ```0```.
32 |
33 | The same settings are available in Firefox GUI: Preferences->Advanced->Network->Settings
34 | ## Configure ssh SOCKS proxy
35 | If you have access to some ssh server, say `socksserver`, you can set up ssh SOCKS tunnel.
36 | First, modify and add the following to `.ssh/config`:
37 | ```ssh
38 | Host socksserver-td
39 | Hostname 123.456.789.012
40 | User cookiemonster
41 | ProxyCommand nc -X connect -x 127.0.0.1:10500 %h %p
42 | ```
43 | then run `ssh -D1234 socksserver-td -4`
44 |
45 | Now in Firefox you could just go to Preferences->Advanced->Network->Settings and set SOCKSv5 host to localhost:1234.
46 |
47 | ## Some utilities use following enivoronment variables:
48 |
49 | ```bash
50 | export https_proxy=127.0.0.1:10500
51 | export http_proxy=127.0.0.1:10500
52 | wget https://twitter.com
53 | ```
54 | Most of the popular utilities also have a flag to specify a proxy.
55 |
--------------------------------------------------------------------------------
/cli/assets:
--------------------------------------------------------------------------------
1 | ../assets/
--------------------------------------------------------------------------------
/cli/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "errors"
5 | "flag"
6 | "fmt"
7 | "io"
8 | "net"
9 | "os"
10 | "strings"
11 | "sync"
12 |
13 | "github.com/pkg/profile"
14 | pb "github.com/sergeyfrolov/gotapdance/protobuf"
15 | "github.com/sergeyfrolov/gotapdance/tapdance"
16 | "github.com/sergeyfrolov/gotapdance/tdproxy"
17 | "github.com/sirupsen/logrus"
18 | )
19 |
20 | func main() {
21 | defer profile.Start().Stop()
22 |
23 | var port = flag.Int("port", 10500, "TapDance will listen for connections on this port.")
24 | var decoy = flag.String("decoy", "", "Sets single decoy. ClientConf won't be requested. " +
25 | "Accepts \"SNI,IP\" or simply \"SNI\" — IP will be resolved. " +
26 | "Examples: \"site.io,1.2.3.4\", \"site.io\"")
27 | var assets_location = flag.String("assetsdir", "./assets/", "Folder to read assets from.")
28 | var proxyProtocol = flag.Bool("proxyproto", false, "Enable PROXY protocol, requesting TapDance station to send client's IP to destination.")
29 | var debug = flag.Bool("debug", false, "Enable debug logs")
30 | var tlsLog = flag.String("tlslog", "", "Filename to write SSL secrets to (allows Wireshark to decrypt TLS connections)")
31 | var connect_target = flag.String("connect-addr", "", "If set, tapdance will transparently connect to provided address, which must be either hostname:port or ip:port. " +
32 | "Default(unset): connects client to forwardproxy, to which CONNECT request is yet to be written.")
33 | flag.Parse()
34 |
35 | if *debug {
36 | tapdance.Logger().Level = logrus.DebugLevel
37 | }
38 | tapdance.Logger().Debug("Debug logging enabled")
39 |
40 | tapdance.AssetsSetDir(*assets_location)
41 | if *decoy != "" {
42 | err := setSingleDecoyHost(*decoy)
43 | if err != nil {
44 | fmt.Fprintf(os.Stderr, "Failed to set single decoy host: %s\n", err)
45 | flag.Usage()
46 | os.Exit(255)
47 | }
48 | }
49 | if *proxyProtocol {
50 | tapdance.EnableProxyProtocol()
51 | }
52 |
53 | if *tlsLog != "" {
54 | err := tapdance.SetTlsLogFilename(*tlsLog)
55 | if err != nil {
56 | tapdance.Logger().Fatal(err)
57 | }
58 | }
59 |
60 | if *connect_target != "" {
61 | err := connectDirect(*connect_target, *port)
62 | if err != nil {
63 | tapdance.Logger().Println(err)
64 | os.Exit(1)
65 | }
66 | return
67 | }
68 |
69 | tapdanceProxy := tdproxy.NewTapDanceProxy(*port)
70 | err := tapdanceProxy.ListenAndServe()
71 | if err != nil {
72 | tdproxy.Logger.Errorf("Failed to ListenAndServe(): %v\n", err)
73 | os.Exit(1)
74 | }
75 | }
76 |
77 | func connectDirect(connect_target string, localPort int) error {
78 | if _, _, err := net.SplitHostPort(connect_target); err != nil {
79 | return fmt.Errorf("Failed to parse host and port from connect_target %s: %v",
80 | connect_target, err)
81 | os.Exit(1)
82 | }
83 | tdConn, err := tapdance.Dial("tcp", connect_target)
84 | if err != nil {
85 | return fmt.Errorf("Failed to dial %s: %v", connect_target, err)
86 | }
87 | l, err := net.ListenTCP("tcp", &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: localPort})
88 | if err != nil {
89 | return fmt.Errorf("Error listening on port %s: %v", localPort, err)
90 | }
91 | clientConn, err := l.AcceptTCP()
92 | if err != nil {
93 | return fmt.Errorf("Error accepting client connection %v: ", err)
94 | }
95 | wg := sync.WaitGroup{}
96 | wg.Add(2)
97 | go func() {
98 | io.Copy(tdConn, clientConn)
99 | tdConn.Close()
100 | wg.Done()
101 | }()
102 | go func() {
103 | io.Copy(clientConn, tdConn)
104 | clientConn.CloseWrite()
105 | wg.Done()
106 | }()
107 | wg.Wait()
108 | return nil
109 | }
110 |
111 | func setSingleDecoyHost(decoy string) error {
112 | splitDecoy := strings.Split(decoy, ",")
113 |
114 | var ip string
115 | switch len(splitDecoy) {
116 | case 1:
117 | ips, err := net.LookupHost(decoy)
118 | if err != nil {
119 | return err
120 | }
121 | ip = ips[0]
122 | case 2:
123 | ip = splitDecoy[1]
124 | if net.ParseIP(ip) == nil {
125 | return errors.New("provided IP address \"" + ip + "\" is invalid")
126 | }
127 | default:
128 | return errors.New("\"" + decoy + "\" contains too many commas")
129 | }
130 |
131 | sni := splitDecoy[0]
132 |
133 | decoySpec := pb.InitTLSDecoySpec(ip, sni)
134 | tapdance.Assets().GetClientConfPtr().DecoyList =
135 | &pb.DecoyList{
136 | TlsDecoys: []*pb.TLSDecoySpec{
137 | decoySpec,
138 | },
139 | }
140 | maxUint32 := ^uint32(0) // max generation: station won't send ClientConf
141 | tapdance.Assets().GetClientConfPtr().Generation = &maxUint32
142 | tapdance.Logger().Infof("Single decoy parsed. SNI: %s, IP: %s", sni, ip)
143 | return nil
144 | }
145 |
--------------------------------------------------------------------------------
/gobind/README.md:
--------------------------------------------------------------------------------
1 | ## Note
2 | This code is for previous version of TapDance Android app, which doesn't create VPN for the whole phone, but merely binds proxy to a certain port. It still could be used in this mode by configuring apps(e.g. Firefox) to use local proxy on said port, however usability is quite limited and we consider this app to be Proof of Concept.
3 |
4 | There is a plan to develop a new version of app, that would actually proxy all traffic.
5 |
6 | ## Get gomobile
7 | You'd need [gomobile](https://godoc.org/golang.org/x/mobile/cmd/gomobile) to compile GUI version:
8 | ```bash
9 | go get golang.org/x/mobile/cmd/gomobile
10 | gomobile init
11 | ```
12 |
13 | ## Wrapper
14 | ### To simply build proxybind.aar library for Android:
15 | ```
16 | cd ${GOPATH}/src/github.com/SergeyFrolov/gotapdance/gobind
17 | gomobile bind -target=android
18 | ```
19 | ### Gradle Plugin
20 | For convinience it is recommended to use [Gobind gradle plugin](https://godoc.org/golang.org/x/mobile/cmd/gomobile#hdr-Gobind_gradle_plugin), compatible with Android Studio.
21 |
--------------------------------------------------------------------------------
/gobind/gobind.go:
--------------------------------------------------------------------------------
1 | package gobind
2 |
3 | import (
4 | "bytes"
5 | "errors"
6 | "github.com/sirupsen/logrus"
7 | "io"
8 |
9 | "github.com/sergeyfrolov/gotapdance/tapdance"
10 | "github.com/sergeyfrolov/gotapdance/tdproxy"
11 | )
12 |
13 | var td_proxy *tdproxy.TapDanceProxy
14 | var buffer bytes.Buffer
15 | var b = make([]byte, 1048576)
16 |
17 | func NewDecoyProxy(listenPort int) (err error) {
18 |
19 | tapdance.Logger().Out = &buffer
20 | tapdance.Logger().Level = logrus.InfoLevel
21 | tapdance.Logger().Formatter = new(logrus.JSONFormatter)
22 | td_proxy = tdproxy.NewTapDanceProxy(listenPort)
23 | if td_proxy == nil {
24 | err = errors.New("Unable to initialize Proxy")
25 | }
26 | return
27 | }
28 |
29 | func GetLog() (out string) {
30 | n, err := buffer.Read(b)
31 | if err == io.EOF {
32 | out = ""
33 | } else if err != nil {
34 | out = err.Error()
35 | } else {
36 | out = string(b[:n])
37 | }
38 | return
39 | }
40 |
41 | func Listen() (err error) {
42 | if td_proxy == nil {
43 | err = errors.New("Proxy is not initialized")
44 | } else {
45 | err = td_proxy.ListenAndServe()
46 | }
47 | return
48 | }
49 |
50 | func Stop() (err error) {
51 | if td_proxy == nil {
52 | err = errors.New("Proxy is not initialized")
53 | } else {
54 | err = td_proxy.Stop()
55 | }
56 | return
57 | }
58 |
59 | func GetStats() (stats string) {
60 | if td_proxy == nil {
61 | stats = "State: Not initialized."
62 | } else {
63 | stats = "State: " + td_proxy.GetStats()
64 | }
65 | return
66 | }
67 |
68 | func IsListening() (listening bool) {
69 | if td_proxy == nil {
70 | listening = false
71 | } else {
72 | if td_proxy.State == tdproxy.ProxyStateListening {
73 | listening = true
74 | } else {
75 | listening = false
76 | }
77 | }
78 | return
79 | }
80 |
--------------------------------------------------------------------------------
/protobuf/README.md:
--------------------------------------------------------------------------------
1 | # Protobuf messages for TapDance
2 | [](https://godoc.org/github.com/sergeyfrolov/gotapdance/protobuf)
3 | ---
4 |
5 | ### Rebuild
6 | protoc --go_out=import_path=tdproto:. signalling.proto
7 |
8 |
--------------------------------------------------------------------------------
/protobuf/extensions.go:
--------------------------------------------------------------------------------
1 | package tdproto
2 |
3 | // Package tdproto, in addition to generated functions, has some manual extensions.
4 |
5 | import (
6 | "encoding/binary"
7 | "net"
8 | )
9 |
10 | // InitTLSDecoySpec creates TLSDecoySpec from ip address and server name.
11 | // Other feilds, such as Pubkey, Timeout and Tcpwin are left unset.
12 |
13 | // InitTLSDecoySpec creates TLSDecoySpec from ip address and server name.
14 | // Other feilds, such as Pubkey, Timeout and Tcpwin are left unset.
15 | func InitTLSDecoySpec(ip string, sni string) *TLSDecoySpec {
16 | _ip := net.ParseIP(ip)
17 | var ipUint32 *uint32
18 | var ipv6Bytes []byte
19 | if _ip.To4() != nil {
20 | ipUint32 = new(uint32)
21 | *ipUint32 = binary.BigEndian.Uint32(net.ParseIP(ip).To4())
22 | } else if _ip.To16() != nil {
23 | ipv6Bytes = _ip
24 | }
25 | tlsDecoy := TLSDecoySpec{Hostname: &sni, Ipv4Addr: ipUint32, Ipv6Addr: ipv6Bytes}
26 | return &tlsDecoy
27 | }
28 |
29 | // GetIpAddrStr returns IP address of TLSDecoySpec as a string.
30 | func (ds *TLSDecoySpec) GetIpAddrStr() string {
31 | if ds.Ipv4Addr != nil {
32 | _ip := make(net.IP, 4)
33 | binary.BigEndian.PutUint32(_ip, ds.GetIpv4Addr())
34 | return net.JoinHostPort(_ip.To4().String(), "443")
35 | }
36 | if ds.Ipv6Addr != nil {
37 | return net.JoinHostPort(net.IP(ds.Ipv6Addr).String(), "443")
38 | }
39 | return ""
40 | }
41 |
--------------------------------------------------------------------------------
/protobuf/signalling.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto2";
2 |
3 | // TODO: We're using proto2 because it's the default on Ubuntu 16.04.
4 | // At some point we will want to migrate to proto3, but we are not
5 | // using any proto3 features yet.
6 |
7 | package tapdance;
8 |
9 | enum KeyType {
10 | AES_GCM_128 = 90;
11 | AES_GCM_256 = 91; // not supported atm
12 | }
13 |
14 | message PubKey {
15 | // A public key, as used by the station.
16 | optional bytes key = 1;
17 |
18 | optional KeyType type = 2;
19 | }
20 |
21 | message TLSDecoySpec {
22 | // The hostname/SNI to use for this host
23 | //
24 | // The hostname is the only required field, although other
25 | // fields are expected to be present in most cases.
26 | optional string hostname = 1;
27 |
28 | // The 32-bit ipv4 address, in network byte order
29 | //
30 | // If the IPv4 address is absent, then it may be resolved via
31 | // DNS by the client, or the client may discard this decoy spec
32 | // if local DNS is untrusted, or the service may be multihomed.
33 | optional fixed32 ipv4addr = 2;
34 |
35 | // The 128-bit ipv6 address, in network byte order
36 | optional bytes ipv6addr = 6;
37 |
38 | // The Tapdance station public key to use when contacting this
39 | // decoy
40 | //
41 | // If omitted, the default station public key (if any) is used.
42 | optional PubKey pubkey = 3;
43 |
44 | // The maximum duration, in milliseconds, to maintain an open
45 | // connection to this decoy (because the decoy may close the
46 | // connection itself after this length of time)
47 | //
48 | // If omitted, a default of 30,000 milliseconds is assumed.
49 | optional uint32 timeout = 4;
50 |
51 | // The maximum TCP window size to attempt to use for this decoy.
52 | //
53 | // If omitted, a default of 15360 is assumed.
54 | //
55 | // TODO: the default is based on the current heuristic of only
56 | // using decoys that permit windows of 15KB or larger. If this
57 | // heuristic changes, then this default doesn't make sense.
58 | optional uint32 tcpwin = 5;
59 | }
60 |
61 | // In version 1, the request is very simple: when
62 | // the client sends a MSG_PROTO to the station, if the
63 | // generation number is present, then this request includes
64 | // (in addition to whatever other operations are part of the
65 | // request) a request for the station to send a copy of
66 | // the current decoy set that has a generation number greater
67 | // than the generation number in its request.
68 | //
69 | // If the response contains a DecoyListUpdate with a generation number equal
70 | // to that which the client sent, then the client is "caught up" with
71 | // the station and the response contains no new information
72 | // (and all other fields may be omitted or empty). Otherwise,
73 | // the station will send the latest configuration information,
74 | // along with its generation number.
75 | //
76 | // The station can also send ClientConf messages
77 | // (as part of Station2Client messages) whenever it wants.
78 | // The client is expected to react as if it had requested
79 | // such messages -- possibly by ignoring them, if the client
80 | // is already up-to-date according to the generation number.
81 |
82 | message ClientConf {
83 | optional DecoyList decoy_list = 1;
84 | optional uint32 generation = 2;
85 | optional PubKey default_pubkey = 3;
86 | }
87 |
88 | message DecoyList {
89 | repeated TLSDecoySpec tls_decoys = 1;
90 | }
91 |
92 | // State transitions of the client
93 | enum C2S_Transition {
94 | C2S_NO_CHANGE = 0;
95 | C2S_SESSION_INIT = 1; // connect me to squid
96 | C2S_SESSION_COVERT_INIT = 11; // connect me to provided covert
97 | C2S_EXPECT_RECONNECT = 2;
98 | C2S_SESSION_CLOSE = 3;
99 | C2S_YIELD_UPLOAD = 4;
100 | C2S_ACQUIRE_UPLOAD = 5;
101 | C2S_EXPECT_UPLOADONLY_RECONN = 6;
102 | C2S_ERROR = 255;
103 | }
104 |
105 | // State transitions of the server
106 | enum S2C_Transition {
107 | S2C_NO_CHANGE = 0;
108 | S2C_SESSION_INIT = 1; // connected to squid
109 | S2C_SESSION_COVERT_INIT = 11; // connected to covert host
110 | S2C_CONFIRM_RECONNECT = 2;
111 | S2C_SESSION_CLOSE = 3;
112 | // TODO should probably also allow EXPECT_RECONNECT here, for DittoTap
113 | S2C_ERROR = 255;
114 | }
115 |
116 | // Should accompany all S2C_ERROR messages.
117 | enum ErrorReasonS2C {
118 | NO_ERROR = 0;
119 | COVERT_STREAM = 1; // Squid TCP connection broke
120 | CLIENT_REPORTED = 2; // You told me something was wrong, client
121 | CLIENT_PROTOCOL = 3; // You messed up, client (e.g. sent a bad protobuf)
122 | STATION_INTERNAL = 4; // I broke
123 | DECOY_OVERLOAD = 5; // Everything's fine, but don't use this decoy right now
124 |
125 | CLIENT_STREAM = 100; // My stream to you broke. (This is impossible to send)
126 | CLIENT_TIMEOUT = 101; // You never came back. (This is impossible to send)
127 | }
128 |
129 | message StationToClient {
130 | // Should accompany (at least) SESSION_INIT and CONFIRM_RECONNECT.
131 | optional uint32 protocol_version = 1;
132 |
133 | // There might be a state transition. May be absent; absence should be
134 | // treated identically to NO_CHANGE.
135 | optional S2C_Transition state_transition = 2;
136 |
137 | // The station can send client config info piggybacked
138 | // on any message, as it sees fit
139 | optional ClientConf config_info = 3;
140 |
141 | // If state_transition == S2C_ERROR, this field is the explanation.
142 | optional ErrorReasonS2C err_reason = 4;
143 |
144 | // Signals client to stop connecting for following amount of seconds
145 | optional uint32 tmp_backoff = 5;
146 |
147 | // Sent in SESSION_INIT, identifies the station that picked up
148 | optional string station_id = 6;
149 |
150 | // Random-sized junk to defeat packet size fingerprinting.
151 | optional bytes padding = 100;
152 | }
153 |
154 | message ClientToStation {
155 | optional uint32 protocol_version = 1;
156 |
157 | // The client reports its decoy list's version number here, which the
158 | // station can use to decide whether to send an updated one. The station
159 | // should always send a list if this field is set to 0.
160 | optional uint32 decoy_list_generation = 2;
161 |
162 | optional C2S_Transition state_transition = 3;
163 |
164 | // The position in the overall session's upload sequence where the current
165 | // YIELD=>ACQUIRE switchover is happening.
166 | optional uint64 upload_sync = 4;
167 |
168 |
169 | // List of decoys that client have unsuccessfully tried in current session.
170 | // Could be sent in chunks
171 | repeated string failed_decoys = 10;
172 |
173 | optional SessionStats stats = 11;
174 |
175 | // Station is only required to check this variable during session initialization.
176 | // If set, station must facilitate connection to said target by itself, i.e. write into squid
177 | // socket an HTTP/SOCKS/any other connection request.
178 | // covert_address must have exactly one ':' colon, that separates host (literal IP address or
179 | // resolvable hostname) and port
180 | // TODO: make it required for initialization, and stop connecting any client straight to squid?
181 | optional string covert_address = 20;
182 |
183 | // Random-sized junk to defeat packet size fingerprinting.
184 | optional bytes padding = 100;
185 | }
186 |
187 | message SessionStats {
188 | optional uint32 failed_decoys_amount = 20; // how many decoys were tried before success
189 |
190 | // Timings below are in milliseconds
191 |
192 | // Applicable to whole session:
193 | optional uint32 total_time_to_connect = 31; // includes failed attempts
194 |
195 | // Last (i.e. successful) decoy:
196 | optional uint32 rtt_to_station = 33; // measured during initial handshake
197 | optional uint32 tls_to_decoy = 38; // includes tcp to decoy
198 | optional uint32 tcp_to_decoy = 39; // measured when establishing tcp connection to decot
199 | }
200 |
201 |
--------------------------------------------------------------------------------
/tapdance/TODO:
--------------------------------------------------------------------------------
1 | MED-LONG-NEVER TERM:
2 | redo bandwidth tracking
3 | redo tmp backoff
4 | UDP
5 |
--------------------------------------------------------------------------------
/tapdance/assets:
--------------------------------------------------------------------------------
1 | ../assets/
--------------------------------------------------------------------------------
/tapdance/assets.go:
--------------------------------------------------------------------------------
1 | package tapdance
2 |
3 | import (
4 | "crypto/x509"
5 | "encoding/binary"
6 | "errors"
7 | "io/ioutil"
8 | "net"
9 | "os"
10 | "path"
11 | "strconv"
12 | "strings"
13 | "sync"
14 |
15 | "github.com/golang/protobuf/proto"
16 | pb "github.com/sergeyfrolov/gotapdance/protobuf"
17 | )
18 |
19 | type assets struct {
20 | sync.RWMutex
21 | path string
22 |
23 | config pb.ClientConf
24 |
25 | roots *x509.CertPool
26 |
27 | filenameStationPubkey string
28 | filenameRoots string
29 | filenameClientConf string
30 | }
31 |
32 | // could reset this internally to refresh assets and avoid woes of singleton testing
33 | var assetsInstance *assets
34 | var assetsOnce sync.Once
35 |
36 | // Assets is an access point to asset managing singleton.
37 | // First access to singleton sets path. Assets(), if called
38 | // before SetAssetsDir() sets path to "./assets/"
39 | func Assets() *assets {
40 | _initAssets := func() { initAssets("./assets/") }
41 | assetsOnce.Do(_initAssets)
42 | return assetsInstance
43 | }
44 |
45 | // AssetsSetDir sets the directory to read assets from.
46 | // Functionally equivalent to Assets() after initialization, unless dir changes.
47 | func AssetsSetDir(dir string) *assets {
48 | _initAssets := func() { initAssets(dir) }
49 | if assetsInstance != nil {
50 | assetsInstance.Lock()
51 | if dir != assetsInstance.path {
52 | Logger().Warnf("Assets path changed %s->%s. (Re)initializing.\n",
53 | assetsInstance.path, dir)
54 | assetsInstance.path = dir
55 | assetsInstance.readConfigs()
56 | assetsInstance.Unlock()
57 | return assetsInstance
58 | }
59 | }
60 | assetsOnce.Do(_initAssets)
61 | return assetsInstance
62 | }
63 |
64 | func initAssets(path string) {
65 | var defaultDecoys = []*pb.TLSDecoySpec{
66 | pb.InitTLSDecoySpec("192.122.190.104", "tapdance1.freeaeskey.xyz"),
67 | pb.InitTLSDecoySpec("192.122.190.105", "tapdance2.freeaeskey.xyz"),
68 | pb.InitTLSDecoySpec("192.122.190.106", "tapdance3.freeaeskey.xyz"),
69 | }
70 |
71 | defaultKey := []byte{81, 88, 104, 190, 127, 69, 171, 111, 49, 10, 254, 212, 178, 41, 183,
72 | 164, 121, 252, 159, 222, 85, 61, 234, 76, 205, 179, 105, 171, 24, 153, 231, 12}
73 |
74 | defualtKeyType := pb.KeyType_AES_GCM_128
75 | defaultPubKey := pb.PubKey{Key: defaultKey, Type: &defualtKeyType}
76 | defaultGeneration := uint32(0)
77 | defaultDecoyList := pb.DecoyList{TlsDecoys: defaultDecoys}
78 | defaultClientConf := pb.ClientConf{DecoyList: &defaultDecoyList,
79 | DefaultPubkey: &defaultPubKey,
80 | Generation: &defaultGeneration}
81 |
82 | assetsInstance = &assets{
83 | path: path,
84 | config: defaultClientConf,
85 | filenameRoots: "roots",
86 | filenameClientConf: "ClientConf",
87 | filenameStationPubkey: "station_pubkey",
88 | }
89 | assetsInstance.readConfigs()
90 | }
91 |
92 | func (a *assets) GetAssetsDir() string {
93 | a.RLock()
94 | defer a.RUnlock()
95 | return a.path
96 | }
97 |
98 | func (a *assets) readConfigs() {
99 | readRoots := func(filename string) error {
100 | rootCerts, err := ioutil.ReadFile(filename)
101 | if err != nil {
102 | return err
103 | }
104 | roots := x509.NewCertPool()
105 | ok := roots.AppendCertsFromPEM(rootCerts)
106 | if !ok {
107 | return errors.New("Failed to parse root certificates")
108 | }
109 | a.roots = roots
110 | return nil
111 | }
112 |
113 | readClientConf := func(filename string) error {
114 | buf, err := ioutil.ReadFile(filename)
115 | if err != nil {
116 | return err
117 | }
118 | clientConf := pb.ClientConf{}
119 | err = proto.Unmarshal(buf, &clientConf)
120 | if err != nil {
121 | return err
122 | }
123 | a.config = clientConf
124 | return nil
125 | }
126 |
127 | readPubkey := func(filename string) error {
128 | staionPubkey, err := ioutil.ReadFile(filename)
129 | if err != nil {
130 | return err
131 | }
132 | if len(staionPubkey) != 32 {
133 | return errors.New("Unexpected keyfile length! Expected: 32. Got: " +
134 | strconv.Itoa(len(staionPubkey)))
135 | }
136 | copy(a.config.DefaultPubkey.Key[:], staionPubkey[0:32])
137 | return nil
138 | }
139 |
140 | var err error
141 | Logger().Infoln("Assets: reading from folder " + a.path)
142 |
143 | rootsFilename := path.Join(a.path, a.filenameRoots)
144 | err = readRoots(rootsFilename)
145 | if err != nil {
146 | Logger().Warningln("Assets: failed to read root ca file: " + err.Error())
147 | } else {
148 | Logger().Infoln("X.509 root CAs successfully read from " + rootsFilename)
149 | }
150 |
151 | clientConfFilename := path.Join(a.path, a.filenameClientConf)
152 | err = readClientConf(clientConfFilename)
153 | if err != nil {
154 | Logger().Warningln("Assets: failed to read ClientConf file: " + err.Error())
155 | } else {
156 | Logger().Infoln("Client config successfully read from " + clientConfFilename)
157 | }
158 |
159 | pubkeyFilename := path.Join(a.path, a.filenameStationPubkey)
160 | err = readPubkey(pubkeyFilename)
161 | if err != nil {
162 | Logger().Debugln("Assets: failed to read pubkey file: " + err.Error())
163 | } else {
164 | Logger().Infoln("Pubkey successfully read from " + pubkeyFilename)
165 | }
166 | }
167 |
168 | // Picks random decoy, returns Server Name Indication and addr in format ipv4:port
169 | func (a *assets) GetDecoyAddress() (sni string, addr string) {
170 | a.RLock()
171 | defer a.RUnlock()
172 |
173 | decoys := a.config.GetDecoyList().GetTlsDecoys()
174 | if len(decoys) == 0 {
175 | return "", ""
176 | }
177 | decoyIndex := getRandInt(0, len(decoys)-1)
178 | ip := make(net.IP, 4)
179 | binary.BigEndian.PutUint32(ip, decoys[decoyIndex].GetIpv4Addr())
180 | // TODO: what checks need to be done, and what's guaranteed?
181 | addr = ip.To4().String() + ":443"
182 | sni = decoys[decoyIndex].GetHostname()
183 | return
184 | }
185 |
186 | // Gets random DecoySpec.
187 | func (a *assets) GetDecoy() pb.TLSDecoySpec {
188 | a.RLock()
189 | defer a.RUnlock()
190 |
191 | decoys := a.config.GetDecoyList().GetTlsDecoys()
192 | chosenDecoy := pb.TLSDecoySpec{}
193 | if len(decoys) == 0 {
194 | return chosenDecoy
195 | }
196 | decoyIndex := getRandInt(0, len(decoys)-1)
197 | chosenDecoy = *decoys[decoyIndex]
198 |
199 | // TODO: stop enforcing values >= defaults.
200 | // Fix ackhole instead
201 | if chosenDecoy.GetTimeout() < timeoutMin {
202 | timeout := uint32(timeoutMax)
203 | chosenDecoy.Timeout = &timeout
204 | }
205 | if chosenDecoy.GetTcpwin() < sendLimitMin {
206 | tcpWin := uint32(sendLimitMax)
207 | chosenDecoy.Tcpwin = &tcpWin
208 | }
209 | return chosenDecoy
210 | }
211 |
212 | func (a *assets) GetRoots() *x509.CertPool {
213 | a.RLock()
214 | defer a.RUnlock()
215 |
216 | return a.roots
217 | }
218 |
219 | func (a *assets) GetPubkey() *[32]byte {
220 | a.RLock()
221 | defer a.RUnlock()
222 |
223 | var pKey [32]byte
224 | copy(pKey[:], a.config.GetDefaultPubkey().GetKey()[:])
225 | return &pKey
226 | }
227 |
228 | func (a *assets) GetGeneration() uint32 {
229 | a.RLock()
230 | defer a.RUnlock()
231 |
232 | return a.config.GetGeneration()
233 | }
234 |
235 | // Set ClientConf generation and store config to disk
236 | func (a *assets) SetGeneration(gen uint32) (err error) {
237 | a.Lock()
238 | defer a.Unlock()
239 |
240 | copyGen := gen
241 | a.config.Generation = ©Gen
242 | err = a.saveClientConf()
243 | return
244 | }
245 |
246 | // Set Public key and store config to disk
247 | func (a *assets) SetPubkey(pubkey pb.PubKey) (err error) {
248 | a.Lock()
249 | defer a.Unlock()
250 |
251 | copyPubkey := pubkey
252 | a.config.DefaultPubkey = ©Pubkey
253 | err = a.saveClientConf()
254 | return
255 | }
256 |
257 | // Set ClientConf and store config to disk
258 | func (a *assets) SetClientConf(conf *pb.ClientConf) (err error) {
259 | a.Lock()
260 | defer a.Unlock()
261 |
262 | a.config = *conf
263 | err = a.saveClientConf()
264 | return
265 | }
266 |
267 | // Not goroutine-safe, use at your own risk
268 | func (a *assets) GetClientConfPtr() *pb.ClientConf {
269 | return &a.config
270 | }
271 |
272 | // Overwrite currently used decoys and store config to disk
273 | func (a *assets) SetDecoys(decoys []*pb.TLSDecoySpec) (err error) {
274 | a.Lock()
275 | defer a.Unlock()
276 |
277 | if a.config.DecoyList == nil {
278 | a.config.DecoyList = &pb.DecoyList{}
279 | }
280 | a.config.DecoyList.TlsDecoys = decoys
281 | err = a.saveClientConf()
282 | return
283 | }
284 |
285 | // Checks if decoy is in currently used ClientConf decoys list
286 | func (a *assets) IsDecoyInList(decoy pb.TLSDecoySpec) bool {
287 | ipv4str := decoy.GetIpAddrStr()
288 | hostname := decoy.GetHostname()
289 | a.RLock()
290 | defer a.RUnlock()
291 | for _, d := range a.config.GetDecoyList().GetTlsDecoys() {
292 | if strings.Compare(d.GetHostname(), hostname) == 0 &&
293 | strings.Compare(d.GetIpAddrStr(), ipv4str) == 0 {
294 | return true
295 | }
296 | }
297 | return false
298 | }
299 |
300 | func (a *assets) saveClientConf() error {
301 | buf, err := proto.Marshal(&a.config)
302 | if err != nil {
303 | return err
304 | }
305 | filename := path.Join(a.path, a.filenameClientConf)
306 | tmpFilename := path.Join(a.path, "."+a.filenameClientConf+"."+getRandString(5)+".tmp")
307 | err = ioutil.WriteFile(tmpFilename, buf[:], 0644)
308 | if err != nil {
309 | return err
310 | }
311 |
312 | return os.Rename(tmpFilename, filename)
313 | }
314 |
--------------------------------------------------------------------------------
/tapdance/assets_test.go:
--------------------------------------------------------------------------------
1 | package tapdance
2 |
3 | import (
4 | "bufio"
5 | "bytes"
6 | "fmt"
7 | "github.com/golang/protobuf/proto"
8 | pb "github.com/sergeyfrolov/gotapdance/protobuf"
9 | "io/ioutil"
10 | "net"
11 | "os"
12 | "path"
13 | "testing"
14 | )
15 |
16 | func TestAssets_Decoys(t *testing.T) {
17 | var b bytes.Buffer
18 | logHolder := bufio.NewWriter(&b)
19 | oldLoggerOut := Logger().Out
20 | Logger().Out = logHolder
21 | defer func() {
22 | Logger().Out = oldLoggerOut
23 | if t.Failed() {
24 | logHolder.Flush()
25 | fmt.Printf("TapDance log was:\n%s\n", b.String())
26 | }
27 | }()
28 | oldpath := Assets().path
29 | Assets().saveClientConf()
30 | dir1, err := ioutil.TempDir("/tmp/", "decoy1")
31 | if err != nil {
32 | fmt.Println(err.Error())
33 | t.Fail()
34 | }
35 | dir2, err := ioutil.TempDir("/tmp/", "decoy2")
36 | if err != nil {
37 | t.Fatal(err)
38 | }
39 |
40 | var testDecoys1 = []*pb.TLSDecoySpec{
41 | pb.InitTLSDecoySpec("4.8.15.16", "ericw.us"),
42 | pb.InitTLSDecoySpec("19.21.23.42", "blahblahbl.ah"),
43 | }
44 |
45 | var testDecoys2 = []*pb.TLSDecoySpec{
46 | pb.InitTLSDecoySpec("0.1.2.3", "whatever.cn"),
47 | pb.InitTLSDecoySpec("255.254.253.252", "particular.ir"),
48 | pb.InitTLSDecoySpec("11.22.33.44", "what.is.up"),
49 | pb.InitTLSDecoySpec("8.255.255.8", "heh.meh"),
50 | }
51 |
52 | AssetsSetDir(dir1)
53 | err = Assets().SetDecoys(testDecoys1)
54 | if err != nil {
55 | t.Fatal(err)
56 | }
57 | if !Assets().IsDecoyInList(*pb.InitTLSDecoySpec("19.21.23.42", "blahblahbl.ah")) {
58 | t.Fatal("Decoy 19.21.23.42(blahblahbl.ah) is NOT in Decoy List!")
59 | }
60 | AssetsSetDir(dir2)
61 | err = Assets().SetDecoys(testDecoys2)
62 | if err != nil {
63 | t.Fatal(err)
64 | }
65 | if Assets().IsDecoyInList(*pb.InitTLSDecoySpec("19.21.23.42", "blahblahbl.ah")) {
66 | t.Fatal("Decoy 19.21.23.42(blahblahbl.ah) is in Decoy List!")
67 | }
68 | if !Assets().IsDecoyInList(*pb.InitTLSDecoySpec("11.22.33.44", "what.is.up")) {
69 | t.Fatal("Decoy 11.22.33.44(what.is.up) is NOT in Decoy List!")
70 | }
71 |
72 | decoyInList := func(d *pb.TLSDecoySpec, decoyList []*pb.TLSDecoySpec) bool {
73 | for _, elem := range decoyList {
74 | if proto.Equal(elem, d) {
75 | return true
76 | }
77 | }
78 | return false
79 | }
80 |
81 | for i := 0; i < 10; i++ {
82 | _sni, addr := Assets().GetDecoyAddress()
83 | hostAddr, _, err := net.SplitHostPort(addr)
84 | if err != nil {
85 | t.Fatal("Corrupted addr:", addr, ". Error:", err.Error())
86 | }
87 | decoyServ := pb.InitTLSDecoySpec(hostAddr, _sni)
88 | if !decoyInList(decoyServ, Assets().config.DecoyList.TlsDecoys) {
89 | fmt.Println("decoyServ not in List!")
90 | fmt.Println("decoyServ:", decoyServ)
91 | fmt.Println("Assets().decoys:", Assets().config.DecoyList.TlsDecoys)
92 | t.Fail()
93 | }
94 | }
95 | AssetsSetDir(dir1)
96 |
97 | if !Assets().IsDecoyInList(*pb.InitTLSDecoySpec("19.21.23.42", "blahblahbl.ah")) {
98 | t.Fatal("Decoy 19.21.23.42(blahblahbl.ah) is NOT in Decoy List!")
99 | }
100 | if Assets().IsDecoyInList(*pb.InitTLSDecoySpec("11.22.33.44", "what.is.up")) {
101 | t.Fatal("Decoy 11.22.33.44(what.is.up) is in Decoy List!")
102 | }
103 | for i := 0; i < 10; i++ {
104 | _sni, addr := Assets().GetDecoyAddress()
105 | hostAddr, _, err := net.SplitHostPort(addr)
106 | if err != nil {
107 | t.Fatal("Corrupted addr:", addr, ". Error:", err.Error())
108 | }
109 | decoyServ := pb.InitTLSDecoySpec(hostAddr, _sni)
110 | if !decoyInList(decoyServ, Assets().config.DecoyList.TlsDecoys) {
111 | fmt.Println("decoyServ not in List!")
112 | fmt.Println("decoyServ:", decoyServ)
113 | fmt.Println("Assets().decoys:", Assets().config.DecoyList.TlsDecoys)
114 | t.Fail()
115 | }
116 | }
117 | os.Remove(path.Join(dir1, Assets().filenameClientConf))
118 | os.Remove(path.Join(dir2, Assets().filenameClientConf))
119 | os.Remove(dir1)
120 | os.Remove(dir2)
121 | AssetsSetDir(oldpath)
122 | }
123 |
124 | func TestAssets_Pubkey(t *testing.T) {
125 | var b bytes.Buffer
126 | logHolder := bufio.NewWriter(&b)
127 | oldLoggerOut := Logger().Out
128 | Logger().Out = logHolder
129 | defer func() {
130 | Logger().Out = oldLoggerOut
131 | if t.Failed() {
132 | logHolder.Flush()
133 | fmt.Printf("TapDance log was:\n%s\n", b.String())
134 | }
135 | }()
136 | initPubKey := func(defaultKey []byte) pb.PubKey {
137 | defualtKeyType := pb.KeyType_AES_GCM_128
138 | return pb.PubKey{Key: defaultKey, Type: &defualtKeyType}
139 | }
140 |
141 | oldpath := Assets().path
142 | Assets().saveClientConf()
143 | dir1, err := ioutil.TempDir("/tmp/", "pubkey1")
144 | if err != nil {
145 | t.Fatal(err)
146 | }
147 | dir2, err := ioutil.TempDir("/tmp/", "pubkey2")
148 | if err != nil {
149 | t.Fatal(err)
150 | }
151 |
152 | var pubkey1 = initPubKey([]byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11,
153 | 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26,
154 | 27, 28, 29, 30, 31})
155 | var pubkey2 = initPubKey([]byte{200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211,
156 | 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226,
157 | 227, 228, 229, 230, 231})
158 |
159 | AssetsSetDir(dir1)
160 | err = Assets().SetPubkey(pubkey1)
161 | if err != nil {
162 | t.Fatal(err)
163 | }
164 | AssetsSetDir(dir2)
165 | err = Assets().SetPubkey(pubkey2)
166 | if err != nil {
167 | t.Fatal(err)
168 | }
169 | if !bytes.Equal(Assets().config.DefaultPubkey.Key[:], pubkey2.Key[:]) {
170 | fmt.Println("Pubkeys are not equal!")
171 | fmt.Println("Assets().stationPubkey:", Assets().config.DefaultPubkey.Key[:])
172 | fmt.Println("pubkey2:", pubkey2)
173 | t.Fail()
174 | }
175 |
176 | AssetsSetDir(dir1)
177 | if !bytes.Equal(Assets().config.DefaultPubkey.Key[:], pubkey1.Key[:]) {
178 | fmt.Println("Pubkeys are not equal!")
179 | fmt.Println("Assets().stationPubkey:", Assets().config.DefaultPubkey.Key[:])
180 | fmt.Println("pubkey1:", pubkey1)
181 | t.Fail()
182 | }
183 | os.Remove(path.Join(dir1, Assets().filenameStationPubkey))
184 | os.Remove(path.Join(dir2, Assets().filenameStationPubkey))
185 | os.Remove(dir1)
186 | os.Remove(dir2)
187 | AssetsSetDir(oldpath)
188 | }
189 |
--------------------------------------------------------------------------------
/tapdance/common.go:
--------------------------------------------------------------------------------
1 | package tapdance
2 |
3 | import (
4 | "encoding/hex"
5 | "errors"
6 | "fmt"
7 | "github.com/refraction-networking/utls"
8 | "os"
9 | "strconv"
10 | "time"
11 | )
12 |
13 | const timeoutMax = 30000
14 | const timeoutMin = 20000
15 |
16 | const sendLimitMax = 15614
17 | const sendLimitMin = 14400
18 |
19 | // timeout for sending TD request and getting a response
20 | const deadlineConnectTDStationMin = 11175
21 | const deadlineConnectTDStationMax = 14231
22 |
23 | // deadline to establish TCP connection to decoy
24 | const deadlineTCPtoDecoyMin = deadlineConnectTDStationMin
25 | const deadlineTCPtoDecoyMax = deadlineConnectTDStationMax
26 |
27 | // during reconnects we send FIN to server and wait until we get FIN back
28 | const waitForFINDieMin = 2 * deadlineConnectTDStationMin
29 | const waitForFINDieMax = 2 * deadlineConnectTDStationMax
30 |
31 | const maxInt16 = int16(^uint16(0) >> 1) // max msg size -> might have to chunk
32 | //const minInt16 = int16(-maxInt16 - 1)
33 |
34 | type flowType int8
35 |
36 | const (
37 | flowUpload flowType = 0x1
38 | flowReadOnly flowType = 0x2
39 | flowBidirectional flowType = 0x4
40 | )
41 |
42 | func (m *flowType) Str() string {
43 | switch *m {
44 | case flowUpload:
45 | return "FlowUpload"
46 | case flowReadOnly:
47 | return "FlowReadOnly"
48 | case flowBidirectional:
49 | return "FlowBidirectional"
50 | default:
51 | return strconv.Itoa(int(*m))
52 | }
53 | }
54 |
55 | type msgType int8
56 |
57 | const (
58 | msgRawData msgType = 1
59 | msgProtobuf msgType = 2
60 | )
61 |
62 | func (m *msgType) Str() string {
63 | switch *m {
64 | case msgRawData:
65 | return "msg raw_data"
66 | case msgProtobuf:
67 | return "msg protobuf"
68 | default:
69 | return strconv.Itoa(int(*m))
70 | }
71 | }
72 |
73 | var errMsgClose = errors.New("MSG CLOSE")
74 | var errNotImplemented = errors.New("Not implemented")
75 |
76 | type tdTagType int8
77 |
78 | const (
79 | tagHttpGetIncomplete tdTagType = 0
80 | tagHttpGetComplete tdTagType = 1
81 | tagHttpPostIncomplete tdTagType = 2
82 | )
83 |
84 | func (m *tdTagType) Str() string {
85 | switch *m {
86 | case tagHttpGetIncomplete:
87 | return "HTTP GET Incomplete"
88 | case tagHttpGetComplete:
89 | return "HTTP GET Complete"
90 | case tagHttpPostIncomplete:
91 | return "HTTP POST Incomplete"
92 | default:
93 | return strconv.Itoa(int(*m))
94 | }
95 | }
96 |
97 | // First byte of tag is for FLAGS
98 | // bit 0 (1 << 7) determines if flow is bidirectional(0) or upload-only(1)
99 | // bits 1-5 are unassigned
100 | // bit 6 determines whether PROXY-protocol-formatted string will be sent
101 | // bit 7 (1 << 0) signals to use TypeLen outer proto
102 | var (
103 | tdFlagUploadOnly = uint8(1 << 7)
104 | tdFlagProxyHeader = uint8(1 << 1)
105 | tdFlagUseTIL = uint8(1 << 0)
106 | )
107 |
108 | var default_flags = tdFlagUseTIL
109 |
110 | // Requests station to proxy client IP to upstream in following form:
111 | // CONNECT 1.2.3.4:443 HTTP/1.1\r\n
112 | // Host: 1.2.3.4\r\n
113 | // \r\n
114 | // PROXY TCP4 x.x.x.x 127.0.0.1 1111 1234\r\n
115 | // ^__^ ^_____^ ^_________________^
116 | // proto clientIP garbage
117 | func EnableProxyProtocol() {
118 | default_flags |= tdFlagProxyHeader
119 | }
120 |
121 | var tlsSecretLog string
122 |
123 | func SetTlsLogFilename(filename string) error {
124 | tlsSecretLog = filename
125 | // Truncate file
126 | f, err := os.Create(filename)
127 | if err != nil {
128 | return err
129 | }
130 | return f.Close()
131 | }
132 |
133 | func WriteTlsLog(clientRandom, masterSecret []byte) error {
134 | if tlsSecretLog != "" {
135 | f, err := os.OpenFile(tlsSecretLog, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
136 | if err != nil {
137 | return err
138 | }
139 |
140 | _, err = fmt.Fprintf(f, "CLIENT_RANDOM %s %s\n",
141 | hex.EncodeToString(clientRandom),
142 | hex.EncodeToString(masterSecret))
143 | if err != nil {
144 | return err
145 | }
146 |
147 | return f.Close()
148 | }
149 | return nil
150 | }
151 |
152 | // List of actually supported ciphers(not a list of offered ciphers!)
153 | // Essentially all working AES_GCM_128 ciphers
154 | var tapDanceSupportedCiphers = []uint16{
155 | tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
156 | tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
157 | tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
158 | tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
159 | tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
160 | tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
161 | }
162 |
163 | // How much time to sleep on trying to connect to decoys to prevent overwhelming them
164 | func sleepBeforeConnect(attempt int) (waitTime <-chan time.Time) {
165 | if attempt >= 6 { // return nil for first 6 attempts
166 | waitTime = time.After(time.Second * 1)
167 | }
168 | return
169 | }
170 |
--------------------------------------------------------------------------------
/tapdance/conn_dual.go:
--------------------------------------------------------------------------------
1 | package tapdance
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "net"
7 | "strconv"
8 | )
9 |
10 | // DualConn is composed of 2 separate TapdanceFlowConn.
11 | // Allows to achieve substantially higher upload speed
12 | // and slightly higher download speed.
13 | type DualConn struct {
14 | net.Conn
15 | writerConn *TapdanceFlowConn
16 | readerConn *TapdanceFlowConn
17 |
18 | sessionId uint64 // constant for logging
19 | }
20 |
21 | // returns TapDance connection that utilizes 2 flows underneath: reader and writer
22 | func dialSplitFlow(ctx context.Context, customDialer func(context.Context, string, string) (net.Conn, error),
23 | covert string) (net.Conn, error) {
24 | dualConn := DualConn{sessionId: sessionsTotal.GetAndInc()}
25 | stationPubkey := Assets().GetPubkey()
26 |
27 | rawRConn := makeTdRaw(tagHttpGetIncomplete, stationPubkey[:])
28 | if customDialer != nil {
29 | rawRConn.TcpDialer = customDialer
30 | }
31 | rawRConn.sessionId = dualConn.sessionId
32 | rawRConn.strIdSuffix = "R"
33 |
34 | var err error
35 | dualConn.readerConn, err = makeTdFlow(flowReadOnly, rawRConn, covert)
36 | if err != nil {
37 | return nil, err
38 | }
39 | err = dualConn.readerConn.DialContext(ctx)
40 | if err != nil {
41 | return nil, err
42 | }
43 |
44 | // net.Conn functions that are not explicitly declared will be performed by readerConn
45 | dualConn.Conn = dualConn.readerConn
46 |
47 | // TODO: traffic fingerprinting issue
48 | // TODO: fundamental issue of observable dependency between 2 flows
49 | err = dualConn.readerConn.yieldUpload()
50 | if err != nil {
51 | dualConn.readerConn.closeWithErrorOnce(err)
52 | return nil, err
53 | }
54 |
55 | rawWConn := makeTdRaw(tagHttpPostIncomplete,
56 | stationPubkey[:])
57 | if customDialer != nil {
58 | rawRConn.TcpDialer = customDialer
59 | }
60 | rawWConn.sessionId = dualConn.sessionId
61 | rawWConn.strIdSuffix = "W"
62 | rawWConn.decoySpec = rawRConn.decoySpec
63 | rawWConn.pinDecoySpec = true
64 |
65 | dualConn.writerConn, err = makeTdFlow(flowUpload, rawWConn, covert)
66 | if err != nil {
67 | dualConn.readerConn.closeWithErrorOnce(err)
68 | return nil, err
69 | }
70 | err = dualConn.writerConn.DialContext(ctx)
71 | if err != nil {
72 | dualConn.readerConn.closeWithErrorOnce(err)
73 | return nil, err
74 | }
75 |
76 | err = dualConn.writerConn.acquireUpload()
77 | if err != nil {
78 | dualConn.readerConn.closeWithErrorOnce(err)
79 | dualConn.writerConn.closeWithErrorOnce(err)
80 | return nil, err
81 | }
82 | /* // TODO: yield confirmation
83 | writerConn.yieldConfirmed = make(chan struct{})
84 | go func() {
85 | time.Sleep(time.Duration(getRandInt(1234, 5432)) * time.Millisecond)
86 | Logger().Infoln(dualConn.idStr() + " faking yield confirmation!")
87 | writerConn.yieldConfirmed <- struct{}{}
88 | }()
89 | err = writerConn.WaitForYieldConfirmation()
90 | if err != nil {
91 | dualConn.readerConn.Close()
92 | writerConn.Close()
93 | return nil, err
94 | }
95 | */
96 | go func() {
97 | select {
98 | case <-dualConn.readerConn.closed:
99 | dualConn.writerConn.closeWithErrorOnce(errors.New("in paired readerConn: " +
100 | dualConn.readerConn.closeErr.Error()))
101 | case <-dualConn.writerConn.closed:
102 | dualConn.readerConn.closeWithErrorOnce(errors.New("in paired writerConn: " +
103 | dualConn.writerConn.closeErr.Error()))
104 | }
105 | }()
106 | return &dualConn, nil
107 | }
108 |
109 | // Write writes data to the connection.
110 | // Write can be made to time out and return an Error with Timeout() == true
111 | // after a fixed time limit; see SetDeadline and SetWriteDeadline.
112 | func (tdConn *DualConn) Write(b []byte) (int, error) {
113 | return tdConn.writerConn.Write(b)
114 | }
115 |
116 | func (tdConn *DualConn) idStr() string {
117 | return "[Session " + strconv.FormatUint(tdConn.sessionId, 10) + "]"
118 | }
119 |
--------------------------------------------------------------------------------
/tapdance/conn_flow.go:
--------------------------------------------------------------------------------
1 | /*
2 | TODO: It probably should have read flow that reads messages and says STAAAHP to channel when read
3 | TODO: here we actually can avoid reconnecting if idle for too long
4 | TODO: confirm that all writes are recorded towards data limit
5 | */
6 |
7 | package tapdance
8 |
9 | import (
10 | "context"
11 | "crypto/rand"
12 | "encoding/binary"
13 | "encoding/hex"
14 | "errors"
15 | "io"
16 | "net"
17 | "sync"
18 | "time"
19 |
20 | "github.com/golang/protobuf/proto"
21 | "github.com/sergeyfrolov/bsbuffer"
22 | pb "github.com/sergeyfrolov/gotapdance/protobuf"
23 | )
24 |
25 | // TapdanceFlowConn represents single TapDance flow.
26 | type TapdanceFlowConn struct {
27 | tdRaw *tdRawConn
28 |
29 | bsbuf *bsbuffer.BSBuffer
30 | recvbuf []byte
31 | headerBuf [6]byte
32 |
33 | writeSliceChan chan []byte
34 | writeResultChan chan ioOpResult
35 | writtenBytesTotal int
36 |
37 | yieldConfirmed chan struct{} // used by flowConn to signal that flow was picked up
38 |
39 | readOnly bool // if readOnly -- we don't need to wait for write engine to stop
40 | reconnectSuccess chan bool
41 | reconnectStarted chan struct{}
42 |
43 | finSent bool // used only by reader to know if it has already scheduled reconnect
44 |
45 | closed chan struct{}
46 | closeOnce sync.Once
47 | closeErr error
48 |
49 | flowType flowType
50 | }
51 |
52 | /*______________________TapdanceFlowConn Mode Chart _____________________________
53 | |FlowType |Default Tag|Diff from old-school bidirectional | Engines spawned|
54 | |-------------|-----------|------------------------------------|----------------|
55 | |Bidirectional| HTTP GET | | Writer, Reader |
56 | |Upload | HTTP POST |acquires upload | Writer, Reader |
57 | |ReadOnly | HTTP GET |yields upload, writer sync ignored | Reader |
58 | \_____________|___________|____________________________________|_______________*/
59 |
60 | // NewTapDanceConn returns TapDance connection, that is ready to be Dial'd
61 | func NewTapDanceConn() (net.Conn, error) {
62 | return makeTdFlow(flowBidirectional, nil, "")
63 | }
64 |
65 | // Prepares TD flow: does not make any network calls nor sets up engines
66 | func makeTdFlow(flow flowType, tdRaw *tdRawConn, covert string) (*TapdanceFlowConn, error) {
67 | if tdRaw == nil {
68 | // raw TapDance connection is not given, make a new one
69 | stationPubkey := Assets().GetPubkey()
70 | remoteConnId := make([]byte, 16)
71 | rand.Read(remoteConnId[:])
72 | tdRaw = makeTdRaw(tagHttpGetIncomplete,
73 | stationPubkey[:])
74 | tdRaw.covert = covert
75 | tdRaw.sessionId = sessionsTotal.GetAndInc()
76 | }
77 |
78 | flowConn := &TapdanceFlowConn{tdRaw: tdRaw}
79 | flowConn.bsbuf = bsbuffer.NewBSBuffer()
80 | flowConn.closed = make(chan struct{})
81 | flowConn.flowType = flow
82 | return flowConn, nil
83 | }
84 |
85 | // Dial establishes direct connection to TapDance station proxy.
86 | // Users are expected to send HTTP CONNECT request next.
87 | func (flowConn *TapdanceFlowConn) DialContext(ctx context.Context) error {
88 | if flowConn.tdRaw.tlsConn == nil {
89 | // if still hasn't dialed
90 | err := flowConn.tdRaw.DialContext(ctx)
91 | if err != nil {
92 | return err
93 | }
94 | }
95 | // don't lose initial msg from station
96 | // strip off state transition and push protobuf up for processing
97 | flowConn.tdRaw.initialMsg.StateTransition = nil
98 | err := flowConn.processProto(flowConn.tdRaw.initialMsg)
99 | if err != nil {
100 | flowConn.closeWithErrorOnce(err)
101 | return err
102 | }
103 |
104 | switch flowConn.flowType {
105 | case flowUpload:
106 | fallthrough
107 | case flowBidirectional:
108 | go flowConn.spawnReaderEngine()
109 | flowConn.reconnectSuccess = make(chan bool, 1)
110 | flowConn.reconnectStarted = make(chan struct{})
111 | flowConn.writeSliceChan = make(chan []byte)
112 | flowConn.writeResultChan = make(chan ioOpResult)
113 | go flowConn.spawnWriterEngine()
114 | return nil
115 | case flowReadOnly:
116 | go flowConn.spawnReaderEngine()
117 | return nil
118 | default:
119 | panic("Not implemented")
120 | }
121 | }
122 |
123 | type ioOpResult struct {
124 | err error
125 | n int
126 | }
127 |
128 | func (flowConn *TapdanceFlowConn) schedReconnectNow() {
129 | flowConn.tdRaw.tlsConn.SetReadDeadline(time.Now())
130 | }
131 |
132 | // returns bool indicating success of reconnect
133 | func (flowConn *TapdanceFlowConn) awaitReconnect() bool {
134 | defer func() { flowConn.writtenBytesTotal = 0 }()
135 | for {
136 | select {
137 | case <-flowConn.reconnectStarted:
138 | case <-flowConn.closed:
139 | return false
140 | case reconnectOk := <-flowConn.reconnectSuccess:
141 | return reconnectOk
142 | }
143 | }
144 | }
145 |
146 | // Write writes data to the connection.
147 | // Write can be made to time out and return an Error with Timeout() == true
148 | // after a fixed time limit; see SetDeadline and SetWriteDeadline.
149 | func (flowConn *TapdanceFlowConn) spawnWriterEngine() {
150 | defer close(flowConn.writeResultChan)
151 | for {
152 | select {
153 | case <-flowConn.reconnectStarted:
154 | if !flowConn.awaitReconnect() {
155 | return
156 | }
157 | case <-flowConn.closed:
158 | return
159 | case b := <-flowConn.writeSliceChan:
160 | ioResult := ioOpResult{}
161 | bytesSent := 0
162 |
163 | canSend := func() int {
164 | // checks the upload limit
165 | // 6 is max header size (protobufs aren't sent here though)
166 | // 1024 is max transition message size
167 | return flowConn.tdRaw.UploadLimit -
168 | flowConn.writtenBytesTotal - 6 - 1024
169 | }
170 | for bytesSent < len(b) {
171 | idxToSend := len(b)
172 | if idxToSend-bytesSent > canSend() {
173 | Logger().Infof("%s reconnecting due to upload limit: "+
174 | "idxToSend (%d) - bytesSent(%d) > UploadLimit(%d) - "+
175 | "writtenBytesTotal(%d) - 6 - 1024 \n",
176 | flowConn.idStr(), idxToSend, bytesSent,
177 | flowConn.tdRaw.UploadLimit, flowConn.writtenBytesTotal)
178 | flowConn.schedReconnectNow()
179 | if !flowConn.awaitReconnect() {
180 | return
181 | }
182 | }
183 | Logger().Debugf("%s WriterEngine: writing\n%s", flowConn.idStr(), hex.Dump(b))
184 |
185 | if cs := minInt(canSend(), int(maxInt16)); idxToSend-bytesSent > cs {
186 | // just reconnected and still can't send: time to chunk
187 | idxToSend = bytesSent + cs
188 | }
189 |
190 | // TODO: outerProto limit on data size
191 | bufToSend := b[bytesSent:idxToSend]
192 | bufToSendWithHeader := getMsgWithHeader(msgRawData, bufToSend) // TODO: optimize!
193 | headerSize := len(bufToSendWithHeader) - len(bufToSend)
194 |
195 | n, err := flowConn.tdRaw.tlsConn.Write(bufToSendWithHeader)
196 | if n >= headerSize {
197 | // TODO: that's kinda hacky
198 | n -= headerSize
199 | }
200 | ioResult.n += n
201 | bytesSent += n
202 | flowConn.writtenBytesTotal += len(bufToSendWithHeader)
203 | if err != nil {
204 | ioResult.err = err
205 | break
206 | }
207 | }
208 | select {
209 | case flowConn.writeResultChan <- ioResult:
210 | case <-flowConn.closed:
211 | return
212 | }
213 | }
214 | }
215 | }
216 |
217 | func (flowConn *TapdanceFlowConn) spawnReaderEngine() {
218 | flowConn.updateReadDeadline()
219 | flowConn.recvbuf = make([]byte, 1500)
220 | for {
221 | msgType, msgLen, err := flowConn.readHeader()
222 | if err != nil {
223 | flowConn.closeWithErrorOnce(err)
224 | return
225 | }
226 | if msgLen == 0 {
227 | continue // wtf?
228 | }
229 | switch msgType {
230 | case msgRawData:
231 | buf, err := flowConn.readRawData(msgLen)
232 | if err != nil {
233 | flowConn.closeWithErrorOnce(err)
234 | return
235 | }
236 | Logger().Debugf("%s ReaderEngine: read\n%s",
237 | flowConn.idStr(), hex.Dump(buf))
238 | _, err = flowConn.bsbuf.Write(buf)
239 | if err != nil {
240 | flowConn.closeWithErrorOnce(err)
241 | return
242 | }
243 | case msgProtobuf:
244 | msg, err := flowConn.readProtobuf(msgLen)
245 | if err != nil {
246 | flowConn.closeWithErrorOnce(err)
247 | return
248 | }
249 | err = flowConn.processProto(msg)
250 | if err != nil {
251 | flowConn.closeWithErrorOnce(err)
252 | return
253 | }
254 | default:
255 | flowConn.closeWithErrorOnce(errors.New("Corrupted outer protocol header: " +
256 | msgType.Str()))
257 | return
258 | }
259 | }
260 | }
261 |
262 | // Write writes data to the connection.
263 | // Write can be made to time out and return an Error with Timeout() == true
264 | // after a fixed time limit; see SetDeadline and SetWriteDeadline.
265 | func (flowConn *TapdanceFlowConn) Write(b []byte) (int, error) {
266 | select {
267 | case flowConn.writeSliceChan <- b:
268 | case <-flowConn.closed:
269 | return 0, flowConn.closeErr
270 | }
271 | select {
272 | case r := <-flowConn.writeResultChan:
273 | return r.n, r.err
274 | case <-flowConn.closed:
275 | return 0, flowConn.closeErr
276 | }
277 | }
278 |
279 | func (flowConn *TapdanceFlowConn) Read(b []byte) (int, error) {
280 | return flowConn.bsbuf.Read(b)
281 | }
282 |
283 | func (flowConn *TapdanceFlowConn) readRawData(msgLen int) ([]byte, error) {
284 | if cap(flowConn.recvbuf) < msgLen {
285 | flowConn.recvbuf = make([]byte, msgLen)
286 | }
287 | var err error
288 | var readBytes int
289 | var readBytesTotal int // both header and body
290 | // Get the message itself
291 | for readBytesTotal < msgLen {
292 | readBytes, err = flowConn.tdRaw.tlsConn.Read(flowConn.recvbuf[readBytesTotal:])
293 | readBytesTotal += int(readBytes)
294 | if err != nil {
295 | err = flowConn.actOnReadError(err)
296 | if err != nil {
297 | return flowConn.recvbuf[:readBytesTotal], err
298 | }
299 | }
300 | }
301 | return flowConn.recvbuf[:readBytesTotal], err
302 | }
303 |
304 | func (flowConn *TapdanceFlowConn) readProtobuf(msgLen int) (msg pb.StationToClient, err error) {
305 | rbuf := make([]byte, msgLen)
306 | var readBytes int
307 | var readBytesTotal int // both header and body
308 | // Get the message itself
309 | for readBytesTotal < msgLen {
310 | readBytes, err = flowConn.tdRaw.tlsConn.Read(rbuf[readBytesTotal:])
311 | readBytesTotal += readBytes
312 | if err != nil {
313 | err = flowConn.actOnReadError(err)
314 | if err != nil {
315 | return
316 | }
317 | }
318 | }
319 | err = proto.Unmarshal(rbuf[:], &msg)
320 | return
321 | }
322 |
323 | func (flowConn *TapdanceFlowConn) readHeader() (msgType msgType, msgLen int, err error) {
324 | // For each message we first read outer protocol header to see if it's protobuf or data
325 |
326 | var readBytes int
327 | var readBytesTotal uint32 // both header and body
328 | headerSize := uint32(2)
329 |
330 | //TODO: check FIN+last data case
331 | for readBytesTotal < headerSize {
332 | readBytes, err = flowConn.tdRaw.tlsConn.Read(flowConn.headerBuf[readBytesTotal:headerSize])
333 | readBytesTotal += uint32(readBytes)
334 | if err != nil {
335 | err = flowConn.actOnReadError(err)
336 | if err != nil {
337 | return
338 | }
339 | }
340 | }
341 |
342 | // Get TIL
343 | typeLen := uint16toInt16(binary.BigEndian.Uint16(flowConn.headerBuf[0:2]))
344 | if typeLen < 0 {
345 | msgType = msgRawData
346 | msgLen = int(-typeLen)
347 | } else if typeLen > 0 {
348 | msgType = msgProtobuf
349 | msgLen = int(typeLen)
350 | } else {
351 | // protobuf with size over 32KB, not fitting into 2-byte TL
352 | msgType = msgProtobuf
353 | headerSize += 4
354 | for readBytesTotal < headerSize {
355 | readBytes, err = flowConn.tdRaw.tlsConn.Read(flowConn.headerBuf[readBytesTotal:headerSize])
356 | readBytesTotal += uint32(readBytes)
357 | if err != nil {
358 | err = flowConn.actOnReadError(err)
359 | if err != nil {
360 | return
361 | }
362 | }
363 | }
364 | msgLen = int(binary.BigEndian.Uint32(flowConn.headerBuf[2:6]))
365 | }
366 | return
367 | }
368 |
369 | // Allows scheduling/doing reconnects in the middle of reads
370 | func (flowConn *TapdanceFlowConn) actOnReadError(err error) error {
371 | if err == nil {
372 | return nil
373 | }
374 |
375 | willScheduleReconnect := false
376 | if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
377 | // Timeout is used as a signal to schedule reconnect, as reconnect is indeed time dependent.
378 | // One can also SetDeadline(NOW) to schedule deadline NOW.
379 | // After EXPECT_RECONNECT and FIN are sent, deadline is used to signal that flow timed out
380 | // waiting for FIN back.
381 | willScheduleReconnect = true
382 | }
383 |
384 | // "EOF is the error returned by Read when no more input is available. Functions should
385 | // return EOF only to signal a graceful end of input." (e.g. FIN was received)
386 | // "ErrUnexpectedEOF means that EOF was encountered in the middle of reading a fixed-size
387 | // block or data structure."
388 | willReconnect := (err == io.EOF || err == io.ErrUnexpectedEOF)
389 |
390 | if willScheduleReconnect {
391 | Logger().Infoln(flowConn.tdRaw.idStr() + " scheduling reconnect")
392 | if flowConn.finSent {
393 | // timeout is hit another time before reconnect
394 | return errors.New("reconnect scheduling: timed out waiting for FIN back")
395 | }
396 | if flowConn.flowType != flowReadOnly {
397 | // notify writer, if there is a writer
398 | select {
399 | case <-flowConn.closed:
400 | return errors.New("reconnect scheduling: closed while notifiyng writer")
401 | case flowConn.reconnectStarted <- struct{}{}:
402 | }
403 | }
404 |
405 | transition := pb.C2S_Transition_C2S_EXPECT_RECONNECT
406 | if flowConn.flowType == flowUpload {
407 | transition = pb.C2S_Transition_C2S_EXPECT_UPLOADONLY_RECONN
408 | }
409 | _, err = flowConn.tdRaw.writeTransition(transition)
410 | if err != nil {
411 | return errors.New("reconnect scheduling: failed to send " +
412 | transition.String() + ": " + err.Error())
413 | }
414 |
415 | if flowConn.flowType == flowUpload {
416 | // for upload-only flows we reconnect right away
417 | willReconnect = true
418 | } else {
419 | flowConn.tdRaw.tlsConn.SetReadDeadline(time.Now().Add(
420 | getRandomDuration(waitForFINDieMin, waitForFINDieMax)))
421 | err = flowConn.tdRaw.closeWrite()
422 | if err != nil {
423 | Logger().Infoln(flowConn.tdRaw.idStr() + " reconnect scheduling:" +
424 | "failed to send FIN: " + err.Error() +
425 | ". Closing roughly and moving on.")
426 | flowConn.tdRaw.Close()
427 | }
428 | flowConn.finSent = true
429 | return nil
430 | }
431 | }
432 |
433 | if willReconnect {
434 | if flowConn.flowType != flowReadOnly {
435 | // notify writer, if there is a writer
436 | select {
437 | case <-flowConn.closed:
438 | return errors.New("reconnect scheduling: closed while notifiyng writer")
439 | case flowConn.reconnectStarted <- struct{}{}:
440 | }
441 | }
442 | if (flowConn.flowType != flowUpload && !flowConn.finSent) ||
443 | err == io.ErrUnexpectedEOF {
444 | Logger().Infoln(flowConn.tdRaw.idStr() + " reconnect: FIN is unexpected")
445 | }
446 | err = flowConn.tdRaw.RedialContext(context.Background())
447 | if flowConn.flowType != flowReadOnly {
448 | // wake up writer engine
449 | select {
450 | case <-flowConn.closed:
451 | case flowConn.reconnectSuccess <- (err == nil):
452 | }
453 | }
454 | if err != nil {
455 | return errors.New("reconnect: failed to Redial: " + err.Error())
456 | }
457 | flowConn.finSent = false
458 | // strip off state transition and push protobuf up for processing
459 | flowConn.tdRaw.initialMsg.StateTransition = nil
460 | err = flowConn.processProto(flowConn.tdRaw.initialMsg)
461 | if err == nil {
462 | flowConn.updateReadDeadline()
463 | return nil
464 | } else if err == errMsgClose {
465 | // errMsgClose actually won't show up here
466 | Logger().Infoln(flowConn.tdRaw.idStr() + " closing cleanly with MSG_CLOSE")
467 | return io.EOF
468 | } // else: proceed and exit as a crash
469 | }
470 |
471 | return flowConn.closeWithErrorOnce(err)
472 | }
473 |
474 | // Sets read deadline to {when raw connection was establihsed} + {timeout} - {small random value}
475 | func (flowConn *TapdanceFlowConn) updateReadDeadline() {
476 | amortizationVal := 0.9
477 | const minSubtrahend = 50
478 | const maxSubtrahend = 9500
479 | deadline := flowConn.tdRaw.establishedAt.Add(time.Millisecond *
480 | time.Duration(int(float64(flowConn.tdRaw.decoySpec.GetTimeout())*amortizationVal)-
481 | getRandInt(minSubtrahend, maxSubtrahend)))
482 | flowConn.tdRaw.tlsConn.SetReadDeadline(deadline)
483 | }
484 |
485 | func (flowConn *TapdanceFlowConn) acquireUpload() error {
486 | _, err := flowConn.tdRaw.writeTransition(pb.C2S_Transition_C2S_ACQUIRE_UPLOAD)
487 | if err != nil {
488 | Logger().Infoln(flowConn.idStr() + " Failed attempt to acquire upload:" + err.Error())
489 | } else {
490 | Logger().Infoln(flowConn.idStr() + " Sent acquire upload request")
491 | }
492 | return err
493 | }
494 |
495 | func (flowConn *TapdanceFlowConn) yieldUpload() error {
496 | _, err := flowConn.tdRaw.writeTransition(pb.C2S_Transition_C2S_YIELD_UPLOAD)
497 | if err != nil {
498 | Logger().Infoln(flowConn.idStr() + " Failed attempt to yield upload:" + err.Error())
499 | } else {
500 | Logger().Infoln(flowConn.idStr() + " Sent yield upload request")
501 | }
502 | return err
503 | }
504 |
505 | // TODO: implement on station, currently unused
506 | // wait for flowConn to confirm that flow was noticed
507 | func (flowConn *TapdanceFlowConn) waitForYieldConfirmation() error {
508 | // camouflage issue
509 | timeout := time.After(20 * time.Second)
510 | select {
511 | case <-timeout:
512 | return errors.New("yield confirmation timeout")
513 | case <-flowConn.yieldConfirmed:
514 | Logger().Infoln(flowConn.idStr() +
515 | " Successfully received yield confirmation from reader flow!")
516 | return nil
517 | case <-flowConn.closed:
518 | return flowConn.closeErr
519 | }
520 | }
521 |
522 | // Closes connection, channel and sets error ONCE, e.g. error won't be overwritten
523 | func (flowConn *TapdanceFlowConn) closeWithErrorOnce(err error) error {
524 | if err == nil {
525 | // safeguard, shouldn't happen
526 | err = errors.New("closed with nil error!")
527 | }
528 | flowConn.closeOnce.Do(func() {
529 | flowConn.closeErr = errors.New(flowConn.idStr() + " " + err.Error())
530 | flowConn.bsbuf.Unblock()
531 | close(flowConn.closed)
532 | flowConn.tdRaw.Close()
533 | })
534 | return flowConn.closeErr
535 | }
536 |
537 | // Close closes the connection.
538 | // Any blocked Read or Write operations will be unblocked and return errors.
539 | func (flowConn *TapdanceFlowConn) Close() error {
540 | return flowConn.closeWithErrorOnce(errors.New("closed by application layer"))
541 | }
542 |
543 | func (flowConn *TapdanceFlowConn) idStr() string {
544 | return flowConn.tdRaw.idStr()
545 | }
546 |
547 | func (flowConn *TapdanceFlowConn) processProto(msg pb.StationToClient) error {
548 | handleConfigInfo := func(conf *pb.ClientConf) {
549 | currGen := Assets().GetGeneration()
550 | if conf.GetGeneration() < currGen {
551 | Logger().Infoln(flowConn.idStr()+" not appliying new config due"+
552 | " to lower generation: ", conf.GetGeneration(), " "+
553 | "(have:", currGen, ")")
554 | return
555 | } else if conf.GetGeneration() < currGen {
556 | Logger().Infoln(flowConn.idStr()+" not appliying new config due"+
557 | " to currently having same generation: ", currGen)
558 | return
559 | }
560 |
561 | _err := Assets().SetClientConf(conf)
562 | if _err != nil {
563 | Logger().Warningln(flowConn.idStr() +
564 | " could not persistently set ClientConf: " + _err.Error())
565 | }
566 | }
567 | Logger().Debugln(flowConn.idStr() + " processing incoming protobuf: " + msg.String())
568 | // handle ConfigInfo
569 | if confInfo := msg.ConfigInfo; confInfo != nil {
570 | handleConfigInfo(confInfo)
571 | // TODO: if we ever get a ``safe'' decoy rotation - code below has to be rewritten
572 | if !Assets().IsDecoyInList(flowConn.tdRaw.decoySpec) {
573 | Logger().Warningln(flowConn.idStr() + " current decoy is no " +
574 | "longer in the list, changing it! Read flow probably will break!")
575 | // if current decoy is no longer in the list
576 | flowConn.tdRaw.decoySpec = Assets().GetDecoy()
577 | }
578 | if !Assets().IsDecoyInList(flowConn.tdRaw.decoySpec) {
579 | Logger().Warningln(flowConn.idStr() + " current decoy is no " +
580 | "longer in the list, changing it! Write flow probably will break!")
581 | // if current decoy is no longer in the list
582 | flowConn.tdRaw.decoySpec = Assets().GetDecoy()
583 | }
584 | }
585 |
586 | // note that flowConn don't see first-message transitions, such as INIT or RECONNECT
587 | stateTransition := msg.GetStateTransition()
588 | switch stateTransition {
589 | case pb.S2C_Transition_S2C_NO_CHANGE:
590 | // carry on
591 | case pb.S2C_Transition_S2C_SESSION_CLOSE:
592 | Logger().Infof(flowConn.idStr() + " received MSG_CLOSE")
593 | return errMsgClose
594 | case pb.S2C_Transition_S2C_ERROR:
595 | err := errors.New("message from station:" +
596 | msg.GetErrReason().String())
597 | Logger().Errorln(flowConn.idStr() + " " + err.Error())
598 | flowConn.closeWithErrorOnce(err)
599 | return err
600 | case pb.S2C_Transition_S2C_CONFIRM_RECONNECT:
601 | fallthrough
602 | case pb.S2C_Transition_S2C_SESSION_INIT:
603 | fallthrough
604 | default:
605 | err := errors.New("Unexpected StateTransition " +
606 | "in initialized Conn:" + stateTransition.String())
607 | Logger().Errorln(flowConn.idStr() + " " + err.Error())
608 | flowConn.closeWithErrorOnce(err)
609 | return err
610 | }
611 | return nil
612 | }
613 |
614 | // LocalAddr returns the local network address.
615 | func (flowConn *TapdanceFlowConn) LocalAddr() net.Addr {
616 | return flowConn.tdRaw.tlsConn.LocalAddr()
617 | }
618 |
619 | // RemoteAddr returns the address of current decoy.
620 | // Not goroutine-safe, mostly here to satisfy net.Conn
621 | func (flowConn *TapdanceFlowConn) RemoteAddr() net.Addr {
622 | return flowConn.tdRaw.tlsConn.RemoteAddr()
623 | }
624 |
625 | // SetDeadline is supposed to set the read and write deadlines
626 | // associated with the connection. It is equivalent to calling
627 | // both SetReadDeadline and SetWriteDeadline.
628 | //
629 | // TODO: In reality, SetDeadline doesn't do that yet, but
630 | // existence of this function is mandatory to implement net.Conn
631 | //
632 | // A deadline is an absolute time after which I/O operations
633 | // fail with a timeout (see type Error) instead of
634 | // blocking. The deadline applies to all future I/O, not just
635 | // the immediately following call to Read or Write.
636 | //
637 | // An idle timeout can be implemented by repeatedly extending
638 | // the deadline after successful Read or Write calls.
639 | //
640 | // A zero value for t means I/O operations will not time out.
641 | //
642 | func (flowConn *TapdanceFlowConn) SetDeadline(t time.Time) error {
643 | return errNotImplemented
644 | }
645 |
646 | // SetReadDeadline sets the deadline for future Read calls.
647 | // A zero value for t means Read will not time out.
648 | func (flowConn *TapdanceFlowConn) SetReadDeadline(t time.Time) error {
649 | return errNotImplemented
650 | }
651 |
652 | // SetWriteDeadline sets the deadline for future Write calls.
653 | // Even if write times out, it may return n > 0, indicating that
654 | // some of the data was successfully written.
655 | // A zero value for t means Write will not time out.
656 | func (flowConn *TapdanceFlowConn) SetWriteDeadline(t time.Time) error {
657 | return errNotImplemented
658 | }
659 |
--------------------------------------------------------------------------------
/tapdance/conn_raw.go:
--------------------------------------------------------------------------------
1 | package tapdance
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "crypto/rand"
7 | "encoding/base64"
8 | "encoding/binary"
9 | "encoding/hex"
10 | "errors"
11 | "fmt"
12 | "io"
13 | "net"
14 | "strconv"
15 | "strings"
16 | "sync"
17 | "time"
18 |
19 | "github.com/golang/protobuf/proto"
20 | "github.com/refraction-networking/utls"
21 | pb "github.com/sergeyfrolov/gotapdance/protobuf"
22 | )
23 |
24 | // Simply establishes TLS and TapDance connection.
25 | // Both reader and writer flows shall have this underlying raw connection.
26 | // Knows about but doesn't keep track of timeout and upload limit
27 | type tdRawConn struct {
28 | tcpConn closeWriterConn // underlying TCP connection with CloseWrite() function that sends FIN
29 | tlsConn *tls.UConn // TLS connection to decoy (and station)
30 |
31 | covert string // hostname that tapdance station will connect client to
32 |
33 | TcpDialer func(context.Context, string, string) (net.Conn, error)
34 |
35 | decoySpec pb.TLSDecoySpec
36 | pinDecoySpec bool // don't ever change decoy (still changeable from outside)
37 | initialMsg pb.StationToClient
38 | stationPubkey []byte
39 | tagType tdTagType
40 |
41 | remoteConnId []byte // 32 byte ID of the connection to station, used for reconnection
42 |
43 | establishedAt time.Time // right after TLS connection to decoy is established, but not to station
44 | UploadLimit int // used only in POST-based tags
45 |
46 | closed chan struct{}
47 | closeOnce sync.Once
48 |
49 | // stats to report
50 | sessionStats pb.SessionStats
51 | failedDecoys []string
52 |
53 | // purely for logging and stats reporting purposes:
54 | flowId CounterUint64 // id of the flow within the session (=how many times reconnected)
55 | sessionId uint64 // id of the local session
56 | strIdSuffix string // suffix for every log string (e.g. to mark upload-only flows)
57 | }
58 |
59 | func makeTdRaw(handshakeType tdTagType, stationPubkey []byte) *tdRawConn {
60 | tdRaw := &tdRawConn{tagType: handshakeType,
61 | stationPubkey: stationPubkey,
62 | }
63 | tdRaw.closed = make(chan struct{})
64 | return tdRaw
65 | }
66 |
67 | func (tdRaw *tdRawConn) DialContext(ctx context.Context) error {
68 | return tdRaw.dial(ctx, false)
69 | }
70 |
71 | func (tdRaw *tdRawConn) RedialContext(ctx context.Context) error {
72 | tdRaw.flowId.Inc()
73 | return tdRaw.dial(ctx, true)
74 | }
75 |
76 | func (tdRaw *tdRawConn) dial(ctx context.Context, reconnect bool) error {
77 | var maxConnectionAttempts int
78 | var err error
79 |
80 | dialStartTs := time.Now()
81 | var expectedTransition pb.S2C_Transition
82 | if reconnect {
83 | maxConnectionAttempts = 5
84 | expectedTransition = pb.S2C_Transition_S2C_CONFIRM_RECONNECT
85 | tdRaw.tlsConn.Close()
86 | } else {
87 | maxConnectionAttempts = 20
88 | expectedTransition = pb.S2C_Transition_S2C_SESSION_INIT
89 | if len(tdRaw.covert) > 0 {
90 | expectedTransition = pb.S2C_Transition_S2C_SESSION_COVERT_INIT
91 | }
92 | }
93 |
94 | for i := 0; i < maxConnectionAttempts; i++ {
95 | if tdRaw.IsClosed() {
96 | return errors.New("Closed")
97 | }
98 | // sleep to prevent overwhelming decoy servers
99 | if waitTime := sleepBeforeConnect(i); waitTime != nil {
100 | select {
101 | case <-waitTime:
102 | case <-ctx.Done():
103 | return context.Canceled
104 | case <-tdRaw.closed:
105 | return errors.New("Closed")
106 | }
107 | }
108 | if tdRaw.pinDecoySpec {
109 | if tdRaw.decoySpec.Ipv4Addr == nil {
110 | return errors.New("decoySpec is pinned, but empty!")
111 | }
112 | } else {
113 | if !reconnect {
114 | tdRaw.decoySpec = Assets().GetDecoy()
115 | if tdRaw.decoySpec.GetIpAddrStr() == "" {
116 | return errors.New("tdConn.decoyAddr is empty!")
117 | }
118 | }
119 | }
120 |
121 | if !reconnect {
122 | // generate a new remove conn ID for each attempt to dial
123 | // keep same remote conn ID for reconnect, since that's what it is for
124 | tdRaw.remoteConnId = make([]byte, 16)
125 | rand.Read(tdRaw.remoteConnId[:])
126 | }
127 |
128 | err = tdRaw.tryDialOnce(ctx, expectedTransition)
129 | if err == nil {
130 | tdRaw.sessionStats.TotalTimeToConnect = durationToU32ptrMs(time.Since(dialStartTs))
131 | return nil
132 | }
133 | tdRaw.failedDecoys = append(tdRaw.failedDecoys,
134 | tdRaw.decoySpec.GetHostname()+" "+tdRaw.decoySpec.GetIpAddrStr())
135 | if tdRaw.sessionStats.FailedDecoysAmount == nil {
136 | tdRaw.sessionStats.FailedDecoysAmount = new(uint32)
137 | }
138 | *tdRaw.sessionStats.FailedDecoysAmount += uint32(1)
139 | }
140 | return err
141 | }
142 |
143 | func (tdRaw *tdRawConn) tryDialOnce(ctx context.Context, expectedTransition pb.S2C_Transition) (err error) {
144 | Logger().Infoln(tdRaw.idStr() + " Attempting to connect to decoy " +
145 | tdRaw.decoySpec.GetHostname() + " (" + tdRaw.decoySpec.GetIpAddrStr() + ")")
146 |
147 | tlsToDecoyStartTs := time.Now()
148 | err = tdRaw.establishTLStoDecoy(ctx)
149 | tlsToDecoyTotalTs := time.Since(tlsToDecoyStartTs)
150 | if err != nil {
151 | Logger().Errorf(tdRaw.idStr() + " establishTLStoDecoy(" +
152 | tdRaw.decoySpec.GetHostname() + "," + tdRaw.decoySpec.GetIpAddrStr() +
153 | ") failed with " + err.Error())
154 | return err
155 | }
156 | tdRaw.sessionStats.TlsToDecoy = durationToU32ptrMs(tlsToDecoyTotalTs)
157 | Logger().Infof("%s Connected to decoy %s(%s) in %s", tdRaw.idStr(), tdRaw.decoySpec.GetHostname(),
158 | tdRaw.decoySpec.GetIpAddrStr(), tlsToDecoyTotalTs.String())
159 |
160 | if tdRaw.IsClosed() {
161 | // if connection was closed externally while in establishTLStoDecoy()
162 | tdRaw.tlsConn.Close()
163 | return errors.New("Closed")
164 | }
165 |
166 | // Check if cipher is supported
167 | cipherIsSupported := func(id uint16) bool {
168 | for _, c := range tapDanceSupportedCiphers {
169 | if c == id {
170 | return true
171 | }
172 | }
173 | return false
174 | }
175 | if !cipherIsSupported(tdRaw.tlsConn.ConnectionState().CipherSuite) {
176 | Logger().Errorf("%s decoy %s offered unsupported cipher %d\n Client ciphers: %#v\n",
177 | tdRaw.idStr(), tdRaw.decoySpec.GetHostname(),
178 | tdRaw.tlsConn.ConnectionState().CipherSuite,
179 | tdRaw.tlsConn.HandshakeState.Hello.CipherSuites)
180 | err = errors.New("Unsupported cipher.")
181 | tdRaw.tlsConn.Close()
182 | return err
183 | }
184 |
185 | var tdRequest string
186 | tdRequest, err = tdRaw.prepareTDRequest(tdRaw.tagType)
187 | if err != nil {
188 | Logger().Errorf(tdRaw.idStr() +
189 | " Preparation of initial TD request failed with " + err.Error())
190 | tdRaw.tlsConn.Close()
191 | return
192 | }
193 | tdRaw.establishedAt = time.Now() // TODO: recheck how ClientConf's timeout is calculated and move, if needed
194 |
195 | Logger().Infoln(tdRaw.idStr() + " Attempting to connect to TapDance Station" +
196 | " with connection ID: " + hex.EncodeToString(tdRaw.remoteConnId[:]) + ", method: " +
197 | tdRaw.tagType.Str())
198 | rttToStationStartTs := time.Now()
199 | _, err = tdRaw.tlsConn.Write([]byte(tdRequest))
200 | if err != nil {
201 | Logger().Errorf(tdRaw.idStr() +
202 | " Could not send initial TD request, error: " + err.Error())
203 | tdRaw.tlsConn.Close()
204 | return
205 | }
206 |
207 | // Give up waiting for the station pretty quickly (2x handshake time == ~4RTT)
208 | tdRaw.tlsConn.SetDeadline(time.Now().Add(tlsToDecoyTotalTs * 2))
209 |
210 | switch tdRaw.tagType {
211 | case tagHttpGetIncomplete:
212 | tdRaw.initialMsg, err = tdRaw.readProto()
213 | rttToStationTotalTs := time.Since(rttToStationStartTs)
214 | tdRaw.sessionStats.RttToStation = durationToU32ptrMs(rttToStationTotalTs)
215 | if err != nil {
216 | if errIsTimeout(err) {
217 | Logger().Errorf("%s %s: %v", tdRaw.idStr(),
218 | "TapDance station didn't pick up the request", err)
219 |
220 | // lame fix for issue #38 with abrupt drop of not picked up flows
221 | tdRaw.tlsConn.SetDeadline(time.Now().Add(
222 | getRandomDuration(deadlineTCPtoDecoyMin,
223 | deadlineTCPtoDecoyMax)))
224 | tdRaw.tlsConn.Write([]byte(getRandPadding(456, 789, 5) + "\r\n" +
225 | "Connection: close\r\n\r\n"))
226 | go readAndClose(tdRaw.tlsConn,
227 | getRandomDuration(deadlineTCPtoDecoyMin,
228 | deadlineTCPtoDecoyMax))
229 | } else {
230 | // any other error will be fatal
231 | Logger().Errorf(tdRaw.idStr() +
232 | " fatal error reading from TapDance station: " +
233 | err.Error())
234 | tdRaw.tlsConn.Close()
235 | return
236 | }
237 | return
238 | }
239 | if tdRaw.initialMsg.GetStateTransition() != expectedTransition {
240 | err = errors.New("Init error: state transition mismatch!" +
241 | " Received: " + tdRaw.initialMsg.GetStateTransition().String() +
242 | " Expected: " + expectedTransition.String())
243 | Logger().Infof("%s Failed to connect to TapDance Station [%s]: %s",
244 | tdRaw.idStr(), tdRaw.initialMsg.GetStationId(), err.Error())
245 | // this exceptional error implies that station has lost state, thus is fatal
246 | return err
247 | }
248 | Logger().Infoln(tdRaw.idStr() + " Successfully connected to TapDance Station [" + tdRaw.initialMsg.GetStationId() + "]")
249 | case tagHttpPostIncomplete:
250 | // don't wait for response
251 | default:
252 | panic("Unsupported td handshake type:" + tdRaw.tagType.Str())
253 | }
254 |
255 | // TapDance should NOT have a timeout, timeouts have to be handled by client and server
256 | tdRaw.tlsConn.SetDeadline(time.Time{}) // unsets timeout
257 | return nil
258 | }
259 |
260 | func (tdRaw *tdRawConn) establishTLStoDecoy(ctx context.Context) error {
261 | deadline, deadlineAlreadySet := ctx.Deadline()
262 | if !deadlineAlreadySet {
263 | deadline = time.Now().Add(getRandomDuration(deadlineTCPtoDecoyMin, deadlineTCPtoDecoyMax))
264 | }
265 | childCtx, childCancelFunc := context.WithDeadline(ctx, deadline)
266 | defer childCancelFunc()
267 |
268 | tcpDialer := tdRaw.TcpDialer
269 | if tcpDialer == nil {
270 | // custom dialer is not set, use default
271 | d := net.Dialer{}
272 | tcpDialer = d.DialContext
273 | }
274 |
275 | tcpToDecoyStartTs := time.Now()
276 | dialConn, err := tcpDialer(childCtx, "tcp", tdRaw.decoySpec.GetIpAddrStr())
277 | tcpToDecoyTotalTs := time.Since(tcpToDecoyStartTs)
278 | if err != nil {
279 | return err
280 | }
281 | tdRaw.sessionStats.TcpToDecoy = durationToU32ptrMs(tcpToDecoyTotalTs)
282 |
283 | config := tls.Config{ServerName: tdRaw.decoySpec.GetHostname()}
284 | if config.ServerName == "" {
285 | // if SNI is unset -- try IP
286 | config.ServerName, _, err = net.SplitHostPort(tdRaw.decoySpec.GetIpAddrStr())
287 | if err != nil {
288 | dialConn.Close()
289 | return err
290 | }
291 | Logger().Infoln(tdRaw.idStr() + ": SNI was nil. Setting it to" +
292 | config.ServerName)
293 | }
294 | // parrot Chrome 62 ClientHello
295 | tdRaw.tlsConn = tls.UClient(dialConn, &config, tls.HelloChrome_62)
296 | err = tdRaw.tlsConn.BuildHandshakeState()
297 | if err != nil {
298 | dialConn.Close()
299 | return err
300 | }
301 | err = tdRaw.tlsConn.MarshalClientHello()
302 | if err != nil {
303 | dialConn.Close()
304 | return err
305 | }
306 | tdRaw.tlsConn.SetDeadline(deadline)
307 | err = tdRaw.tlsConn.Handshake()
308 | if err != nil {
309 | dialConn.Close()
310 | return err
311 | }
312 | closeWriter, ok := dialConn.(closeWriterConn)
313 | if !ok {
314 | return errors.New("dialConn is not a closeWriter")
315 | }
316 | tdRaw.tcpConn = closeWriter
317 | return nil
318 | }
319 |
320 | func (tdRaw *tdRawConn) Close() error {
321 | var err error
322 | tdRaw.closeOnce.Do(func() {
323 | close(tdRaw.closed)
324 | if tdRaw.tlsConn != nil {
325 | err = tdRaw.tlsConn.Close()
326 | }
327 | })
328 | return err
329 | }
330 |
331 | type closeWriterConn interface {
332 | net.Conn
333 | CloseWrite() error
334 | }
335 |
336 | func (tdRaw *tdRawConn) closeWrite() error {
337 | return tdRaw.tcpConn.CloseWrite()
338 | }
339 |
340 | func (tdRaw *tdRawConn) prepareTDRequest(handshakeType tdTagType) (string, error) {
341 | // Generate tag for the initial TapDance request
342 | buf := new(bytes.Buffer) // What we have to encrypt with the shared secret using AES
343 |
344 | masterKey := tdRaw.tlsConn.HandshakeState.MasterSecret
345 |
346 | // write flags
347 | flags := default_flags
348 | if tdRaw.tagType == tagHttpPostIncomplete {
349 | flags |= tdFlagUploadOnly
350 | }
351 | if err := binary.Write(buf, binary.BigEndian, flags); err != nil {
352 | return "", err
353 | }
354 | buf.Write([]byte{0}) // Unassigned byte
355 | negotiatedCipher := tdRaw.tlsConn.HandshakeState.State12.Suite.Id
356 | if tdRaw.tlsConn.HandshakeState.ServerHello.Vers == tls.VersionTLS13 {
357 | negotiatedCipher = tdRaw.tlsConn.HandshakeState.State13.Suite.Id
358 | }
359 | buf.Write([]byte{byte(negotiatedCipher >> 8),
360 | byte(negotiatedCipher & 0xff)})
361 | buf.Write(masterKey[:])
362 | buf.Write(tdRaw.tlsConn.HandshakeState.ServerHello.Random)
363 | buf.Write(tdRaw.tlsConn.HandshakeState.Hello.Random)
364 | buf.Write(tdRaw.remoteConnId[:]) // connection id for persistence
365 |
366 | err := WriteTlsLog(tdRaw.tlsConn.HandshakeState.Hello.Random,
367 | tdRaw.tlsConn.HandshakeState.MasterSecret)
368 | if err != nil {
369 | Logger().Warningf("Failed to write TLS secret log: %s", err)
370 | }
371 |
372 | // Generate and marshal protobuf
373 | transition := pb.C2S_Transition_C2S_SESSION_INIT
374 | var covert *string
375 | if len(tdRaw.covert) > 0 {
376 | transition = pb.C2S_Transition_C2S_SESSION_COVERT_INIT
377 | covert = &tdRaw.covert
378 | }
379 | currGen := Assets().GetGeneration()
380 | initProto := &pb.ClientToStation{
381 | CovertAddress: covert,
382 | StateTransition: &transition,
383 | DecoyListGeneration: &currGen,
384 | }
385 | initProtoBytes, err := proto.Marshal(initProto)
386 | if err != nil {
387 | return "", err
388 | }
389 | Logger().Debugln(tdRaw.idStr()+" Initial protobuf", initProto)
390 |
391 | // Obfuscate/encrypt tag and protobuf
392 | tag, encryptedProtoMsg, err := obfuscateTagAndProtobuf(buf.Bytes(), initProtoBytes, tdRaw.stationPubkey)
393 | if err != nil {
394 | return "", err
395 | }
396 | return tdRaw.genHTTP1Tag(tag, encryptedProtoMsg)
397 | }
398 |
399 | // mutates tdRaw: sets tdRaw.UploadLimit
400 | func (tdRaw *tdRawConn) genHTTP1Tag(tag, encryptedProtoMsg []byte) (string, error) {
401 | sharedHeaders := `Host: ` + tdRaw.decoySpec.GetHostname() +
402 | "\nUser-Agent: TapDance/1.2 (+https://refraction.network/info)"
403 | if len(encryptedProtoMsg) > 0 {
404 | sharedHeaders += "\nX-Proto: " + base64.StdEncoding.EncodeToString(encryptedProtoMsg)
405 | }
406 | var httpTag string
407 | switch tdRaw.tagType {
408 | // for complete copy http generator of golang
409 | case tagHttpGetComplete:
410 | fallthrough
411 | case tagHttpGetIncomplete:
412 | tdRaw.UploadLimit = int(tdRaw.decoySpec.GetTcpwin()) - getRandInt(1, 1045)
413 | httpTag = fmt.Sprintf(`GET / HTTP/1.1
414 | %s
415 | X-Ignore: %s`, sharedHeaders, getRandPadding(7, maxInt(612-len(sharedHeaders), 7), 10))
416 | httpTag = strings.Replace(httpTag, "\n", "\r\n", -1)
417 | case tagHttpPostIncomplete:
418 | ContentLength := getRandInt(900000, 1045000)
419 | tdRaw.UploadLimit = ContentLength - 1
420 | httpTag = fmt.Sprintf(`POST / HTTP/1.1
421 | %s
422 | Accept-Encoding: None
423 | X-Padding: %s
424 | Content-Type: application/zip; boundary=----WebKitFormBoundaryaym16ehT29q60rUx
425 | Content-Length: %s
426 | ----WebKitFormBoundaryaym16ehT29q60rUx
427 | Content-Disposition: form-data; name=\"td.zip\"
428 | `, sharedHeaders, getRandPadding(1, maxInt(461-len(sharedHeaders), 1), 10), strconv.Itoa(ContentLength))
429 | httpTag = strings.Replace(httpTag, "\n", "\r\n", -1)
430 | }
431 |
432 | keystreamOffset := len(httpTag)
433 | keystreamSize := (len(tag)/3+1)*4 + keystreamOffset // we can't use first 2 bits of every byte
434 | wholeKeystream, err := tdRaw.tlsConn.GetOutKeystream(keystreamSize)
435 | if err != nil {
436 | return httpTag, err
437 | }
438 | keystreamAtTag := wholeKeystream[keystreamOffset:]
439 |
440 | httpTag += reverseEncrypt(tag, keystreamAtTag)
441 | if tdRaw.tagType == tagHttpGetComplete {
442 | httpTag += "\r\n\r\n"
443 | }
444 | Logger().Debugf("Generated HTTP TAG:\n%s\n", httpTag)
445 | return httpTag, nil
446 | }
447 |
448 | func (tdRaw *tdRawConn) idStr() string {
449 | return "[Session " + strconv.FormatUint(tdRaw.sessionId, 10) + ", " +
450 | "Flow " + strconv.FormatUint(tdRaw.flowId.Get(), 10) + tdRaw.strIdSuffix + "]"
451 | }
452 |
453 | // Simply reads and returns protobuf
454 | // Returns error if it's not a protobuf
455 | // TODO: redesign it pb, data, err
456 | func (tdRaw *tdRawConn) readProto() (msg pb.StationToClient, err error) {
457 | var readBuffer bytes.Buffer
458 |
459 | var outerProtoMsgType msgType
460 | var msgLen int64 // just the body (e.g. raw data or protobuf)
461 |
462 | // Get TIL
463 | _, err = io.CopyN(&readBuffer, tdRaw.tlsConn, 2)
464 | if err != nil {
465 | return
466 | }
467 |
468 | typeLen := uint16toInt16(binary.BigEndian.Uint16(readBuffer.Next(2)))
469 | if typeLen < 0 {
470 | outerProtoMsgType = msgRawData
471 | msgLen = int64(-typeLen)
472 | } else if typeLen > 0 {
473 | outerProtoMsgType = msgProtobuf
474 | msgLen = int64(typeLen)
475 | } else {
476 | // protobuf with size over 32KB, not fitting into 2-byte TL
477 | outerProtoMsgType = msgProtobuf
478 | _, err = io.CopyN(&readBuffer, tdRaw.tlsConn, 4)
479 | if err != nil {
480 | return
481 | }
482 | msgLen = int64(binary.BigEndian.Uint32(readBuffer.Next(4)))
483 | }
484 |
485 | if outerProtoMsgType == msgRawData {
486 | err = errors.New("Received data message in uninitialized flow")
487 | return
488 | }
489 |
490 | // Get the message itself
491 | _, err = io.CopyN(&readBuffer, tdRaw.tlsConn, msgLen)
492 | if err != nil {
493 | return
494 | }
495 |
496 | err = proto.Unmarshal(readBuffer.Bytes(), &msg)
497 | if err != nil {
498 | return
499 | }
500 |
501 | Logger().Debugln(tdRaw.idStr() + " INIT: received protobuf: " + msg.String())
502 | return
503 | }
504 |
505 | // Generates padding and stuff
506 | // Currently guaranteed to be less than 1024 bytes long
507 | func (tdRaw *tdRawConn) writeTransition(transition pb.C2S_Transition) (n int, err error) {
508 | const paddingMinSize = 250
509 | const paddingMaxSize = 800
510 | const paddingSmoothness = 5
511 | paddingDecrement := 0 // reduce potential padding size by this value
512 |
513 | currGen := Assets().GetGeneration()
514 | msg := pb.ClientToStation{
515 | DecoyListGeneration: &currGen,
516 | StateTransition: &transition,
517 | UploadSync: new(uint64)} // TODO: remove
518 | if tdRaw.flowId.Get() == 0 {
519 | // we have stats for each reconnect, but only send stats for the initial connection
520 | msg.Stats = &tdRaw.sessionStats
521 | }
522 |
523 | if len(tdRaw.failedDecoys) > 0 {
524 | failedDecoysIdx := 0 // how many failed decoys to report now
525 | for failedDecoysIdx < len(tdRaw.failedDecoys) {
526 | if paddingMinSize < proto.Size(&pb.ClientToStation{
527 | FailedDecoys: tdRaw.failedDecoys[:failedDecoysIdx+1]}) {
528 | // if failedDecoys list is too big to fit in place of min padding
529 | // then send the rest on the next reconnect
530 | break
531 | }
532 | failedDecoysIdx += 1
533 | }
534 | paddingDecrement = proto.Size(&pb.ClientToStation{
535 | FailedDecoys: tdRaw.failedDecoys[:failedDecoysIdx]})
536 |
537 | msg.FailedDecoys = tdRaw.failedDecoys[:failedDecoysIdx]
538 | tdRaw.failedDecoys = tdRaw.failedDecoys[failedDecoysIdx:]
539 | }
540 | msg.Padding = []byte(getRandPadding(paddingMinSize-paddingDecrement,
541 | paddingMaxSize-paddingDecrement, paddingSmoothness))
542 |
543 | msgBytes, err := proto.Marshal(&msg)
544 | if err != nil {
545 | return
546 | }
547 |
548 | Logger().Infoln(tdRaw.idStr()+" sending transition: ", msg.String())
549 | b := getMsgWithHeader(msgProtobuf, msgBytes)
550 | n, err = tdRaw.tlsConn.Write(b)
551 | return
552 | }
553 |
554 | func (tdRaw *tdRawConn) IsClosed() bool {
555 | select {
556 | case <-tdRaw.closed:
557 | return true
558 | default:
559 | return false
560 | }
561 | }
562 |
--------------------------------------------------------------------------------
/tapdance/counter.go:
--------------------------------------------------------------------------------
1 | package tapdance
2 |
3 | import "sync"
4 |
5 | // CounterUint64 is a goroutine-safe uint64 counter.
6 | // Wraps, if underflows/overflows.
7 | type CounterUint64 struct {
8 | sync.RWMutex
9 | value uint64
10 | }
11 |
12 | // Inc increases the counter and returns resulting value
13 | func (c *CounterUint64) Inc() uint64 {
14 | c.Lock()
15 | defer c.Unlock()
16 | if c.value == ^uint64(0) {
17 | // if max
18 | c.value = 0
19 | } else {
20 | c.value++
21 | }
22 | return c.value
23 | }
24 |
25 | // GetAndInc returns current value and then increases the counter
26 | func (c *CounterUint64) GetAndInc() uint64 {
27 | c.Lock()
28 | retVal := c.value
29 | if c.value == ^uint64(0) {
30 | // if max
31 | c.value = 0
32 | } else {
33 | c.value++
34 | }
35 | c.Unlock()
36 | return retVal
37 | }
38 |
39 | // Dec decrements the counter and returns resulting value
40 | func (c *CounterUint64) Dec() uint64 {
41 | c.Lock()
42 | defer c.Unlock()
43 | if c.value == 0 {
44 | c.value = ^uint64(0)
45 | } else {
46 | c.value--
47 | }
48 | return c.value
49 | }
50 |
51 | // Get returns current counter value
52 | func (c *CounterUint64) Get() (value uint64) {
53 | c.RLock()
54 | value = c.value
55 | c.RUnlock()
56 | return
57 | }
58 |
59 | // Set assigns current counter value
60 | func (c *CounterUint64) Set(value uint64) {
61 | c.Lock()
62 | c.value = value
63 | c.Unlock()
64 | return
65 | }
66 |
--------------------------------------------------------------------------------
/tapdance/dialer.go:
--------------------------------------------------------------------------------
1 | package tapdance
2 |
3 | import (
4 | "context"
5 | "net"
6 | )
7 |
8 | var sessionsTotal CounterUint64
9 |
10 | // Dialer contains options and implements advanced functions for establishing TapDance connection.
11 | type Dialer struct {
12 | SplitFlows bool
13 | TcpDialer func(context.Context, string, string) (net.Conn, error)
14 | }
15 |
16 | // Dial connects to the address on the named network.
17 | //
18 | // The only supported network at this time: "tcp".
19 | // The address has the form "host:port".
20 | // The host must be a literal IP address, or a host name that can be
21 | // resolved to IP addresses.
22 | // To avoid abuse, only certain whitelisted ports are allowed.
23 | //
24 | // Example: Dial("tcp", "golang.org:80")
25 | func Dial(network, address string) (net.Conn, error) {
26 | var d Dialer
27 | return d.Dial(network, address)
28 | }
29 |
30 | // Dial connects to the address on the named network.
31 | func (d *Dialer) Dial(network, address string) (net.Conn, error) {
32 | return d.DialContext(context.Background(), network, address)
33 | }
34 |
35 | // DialContext connects to the address on the named network using the provided context.
36 | // Long deadline is strongly advised, since tapdance will try multiple decoys.
37 | //
38 | // The only supported network at this time: "tcp".
39 | // The address has the form "host:port".
40 | // The host must be a literal IP address, or a host name that can be
41 | // resolved to IP addresses.
42 | // To avoid abuse, only certain whitelisted ports are allowed.
43 | //
44 | // Example: Dial("tcp", "golang.org:80")
45 | func (d *Dialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
46 | if network != "tcp" {
47 | return nil, &net.OpError{Op: "dial", Net: network, Err: net.UnknownNetworkError(network)}
48 | }
49 | if len(address) > 0 {
50 | _, _, err := net.SplitHostPort(address)
51 | if err != nil {
52 | return nil, err
53 | }
54 | }
55 |
56 | if !d.SplitFlows {
57 | flow, err := makeTdFlow(flowBidirectional, nil, address)
58 | if err != nil {
59 | return nil, err
60 | }
61 | flow.tdRaw.TcpDialer = d.TcpDialer
62 | return flow, flow.DialContext(ctx)
63 | }
64 | return dialSplitFlow(ctx, d.TcpDialer, address)
65 | }
66 |
67 | // DialProxy establishes direct connection to TapDance station proxy.
68 | // Users are expected to send HTTP CONNECT request next.
69 | func (d *Dialer) DialProxy() (net.Conn, error) {
70 | return d.DialProxyContext(context.Background())
71 | }
72 |
73 | // DialProxy establishes direct connection to TapDance station proxy using the provided context.
74 | // Users are expected to send HTTP CONNECT request next.
75 | func (d *Dialer) DialProxyContext(ctx context.Context) (net.Conn, error) {
76 | return d.DialContext(ctx, "tcp", "")
77 | }
78 |
--------------------------------------------------------------------------------
/tapdance/dialer_test.go:
--------------------------------------------------------------------------------
1 | package tapdance
2 |
3 | import (
4 | "bufio"
5 | "crypto/tls"
6 | "fmt"
7 | pb "github.com/sergeyfrolov/gotapdance/protobuf"
8 | "io/ioutil"
9 | "net"
10 | "net/http"
11 | "net/url"
12 | "os"
13 | "testing"
14 | )
15 |
16 | func setupTestAssets() error {
17 | tmpDir, err := ioutil.TempDir("/tmp/", "td-test-")
18 | if err != nil {
19 | return err
20 | }
21 | AssetsSetDir(tmpDir)
22 | // make sure station won't send new ClientConf
23 | err = Assets().SetGeneration(100500)
24 | if err != nil {
25 | return err
26 | }
27 |
28 | // use testing public key
29 | keyType := pb.KeyType_AES_GCM_128
30 | stationTestPubkey, err := ioutil.ReadFile("../assets/station_pubkey_test")
31 | if err != nil {
32 | return err
33 | }
34 |
35 | pubKey := pb.PubKey{
36 | Key: stationTestPubkey,
37 | Type: &keyType,
38 | }
39 | if err != nil {
40 | return err
41 | }
42 | Assets().SetPubkey(pubKey)
43 |
44 | // use correct decoy
45 | tapdance1Decoy := pb.InitTLSDecoySpec("192.122.190.104", "tapdance1.freeaeskey.xyz")
46 | err = Assets().SetDecoys([]*pb.TLSDecoySpec{tapdance1Decoy})
47 | if err != nil {
48 | return err
49 | }
50 | return nil
51 | }
52 |
53 | func TestMain(m *testing.M) {
54 | err := setupTestAssets()
55 | if err != nil {
56 | panic(err)
57 | }
58 | retCode := m.Run()
59 | os.Exit(retCode)
60 | }
61 |
62 | func TestTapDanceDial(t *testing.T) {
63 | urlParse := func(urlStr string) url.URL {
64 | _url, err := url.Parse(urlStr)
65 | if err != nil {
66 | panic(err)
67 | }
68 | return *_url
69 | }
70 | testUrls := []url.URL{
71 | // TODO: uncomment when/if :80 is allowed on all stations
72 | // urlParse("http://detectportal.firefox.com:80/success.txt"),
73 | urlParse("https://tapdance1.freeaeskey.xyz:443/"),
74 | }
75 |
76 | getResponseString := func(url url.URL,
77 | dial func(network, address string) (net.Conn, error)) (string, error) {
78 | conn, err := dial("tcp", url.Hostname()+":"+url.Port())
79 | if err != nil {
80 | return "", fmt.Errorf("dial failed: %v", err)
81 | }
82 | if url.Scheme == "https" {
83 | conn = tls.Client(conn, &tls.Config{ServerName: url.Hostname()})
84 | }
85 | defer conn.Close()
86 |
87 | req, err := http.NewRequest("GET", url.String(), nil)
88 | req.Host = url.Hostname()
89 | if err != nil {
90 | return "", fmt.Errorf("http.NewRequest failed: %v", err)
91 | }
92 |
93 | err = req.Write(conn)
94 | if err != nil {
95 | return "", fmt.Errorf("Write failed: %v", err)
96 | }
97 |
98 | resp, err := http.ReadResponse(bufio.NewReader(conn), req)
99 | if err != nil {
100 | return "", fmt.Errorf("http.ReadResponse failed: %v", err)
101 | }
102 |
103 | responseBody, err := ioutil.ReadAll(resp.Body)
104 | if err != nil {
105 | return "", fmt.Errorf("ioutil.ReadAll failed: %v", err)
106 | }
107 | return string(responseBody), nil
108 | }
109 |
110 | for _, testUrl := range testUrls {
111 | referenceResponse, err := getResponseString(testUrl, net.Dial)
112 | if err != nil {
113 | t.Fatalf("Failed to get reference response from %v : %v. Check your connection",
114 | testUrl, err)
115 | }
116 | tdResponse, err := getResponseString(testUrl, Dial)
117 | if err != nil {
118 | t.Fatalf("Failed to get response from %v via TapDance: %v.", testUrl.String(), err)
119 | }
120 | if string(referenceResponse) != string(tdResponse) {
121 | t.Fatalf("Unexpected response from %s\nExpected: %s\nGot: %s",
122 | testUrl.String(), string(referenceResponse), string(tdResponse))
123 | }
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/tapdance/logger.go:
--------------------------------------------------------------------------------
1 | package tapdance
2 |
3 | import (
4 | "fmt"
5 | "github.com/sirupsen/logrus"
6 | "sync"
7 | )
8 |
9 | // implements interface logrus.Formatter
10 | type formatter struct {
11 | }
12 |
13 | func (f *formatter) Format(entry *logrus.Entry) ([]byte, error) {
14 | return []byte(fmt.Sprintf("[%s] %s\n", entry.Time.Format("15:04:05"), entry.Message)), nil
15 | }
16 |
17 | var logrusLogger *logrus.Logger
18 | var initLoggerOnce sync.Once
19 |
20 | // Logger is an access point for TapDance-wide logger
21 | func Logger() *logrus.Logger {
22 | initLoggerOnce.Do(func() {
23 | logrusLogger = logrus.New()
24 | logrusLogger.Formatter = new(formatter)
25 | logrusLogger.Level = logrus.InfoLevel
26 | // logrusLogger.Level = logrus.DebugLevel
27 |
28 | // buildInfo const will be overwritten by CI with `sed` for test builds
29 | // if not overwritten -- this is a NO-OP
30 | const buildInfo = ""
31 | if len(buildInfo) > 0 {
32 | logrusLogger.Infof("Running gotapdance build %s", buildInfo)
33 | }
34 | })
35 | return logrusLogger
36 | }
37 |
--------------------------------------------------------------------------------
/tapdance/utils.go:
--------------------------------------------------------------------------------
1 | package tapdance
2 |
3 | import (
4 | "bytes"
5 | "crypto/aes"
6 | "crypto/cipher"
7 | "crypto/rand"
8 | "crypto/sha256"
9 | "encoding/binary"
10 | "errors"
11 | mrand "math/rand"
12 | "net"
13 | "strconv"
14 | "strings"
15 | "time"
16 |
17 | "github.com/agl/ed25519/extra25519"
18 | "golang.org/x/crypto/curve25519"
19 | )
20 |
21 | // The key argument should be the AES key, either 16 or 32 bytes
22 | // to select AES-128 or AES-256.
23 | func aesGcmEncrypt(plaintext []byte, key []byte, iv []byte) ([]byte, error) {
24 | block, err := aes.NewCipher(key)
25 | if err != nil {
26 | return nil, err
27 | }
28 |
29 | aesGcmCipher, err := cipher.NewGCM(block)
30 | if err != nil {
31 | return nil, err
32 | }
33 | return aesGcmCipher.Seal(nil, iv, plaintext, nil), nil
34 | }
35 |
36 | // Tries to get crypto random int in range [min, max]
37 | // In case of crypto failure -- return insecure pseudorandom
38 | func getRandInt(min int, max int) int {
39 | // I can't believe Golang is making me do that
40 | // Flashback to awful C/C++ libraries
41 | diff := max - min
42 | if diff < 0 {
43 | Logger().Warningf("getRandInt(): max is less than min")
44 | min = max
45 | diff *= -1
46 | } else if diff == 0 {
47 | return min
48 | }
49 | var v int64
50 | err := binary.Read(rand.Reader, binary.LittleEndian, &v)
51 | if v < 0 {
52 | v *= -1
53 | }
54 | if err != nil {
55 | Logger().Warningf("Unable to securely get getRandInt(): " + err.Error())
56 | v = mrand.Int63()
57 | }
58 | return min + int(v%int64(diff+1))
59 | }
60 |
61 | // returns random duration between min and max in milliseconds
62 | func getRandomDuration(min int, max int) time.Duration {
63 | return time.Millisecond * time.Duration(getRandInt(min, max))
64 | }
65 |
66 | // Get padding of length [minLen, maxLen).
67 | // Distributed in pseudogaussian style.
68 | // Padded using symbol '#'. Known plaintext attacks, anyone?
69 | func getRandPadding(minLen int, maxLen int, smoothness int) string {
70 | paddingLen := 0
71 | for j := 0; j < smoothness; j++ {
72 | paddingLen += getRandInt(minLen, maxLen)
73 | }
74 | paddingLen = paddingLen / smoothness
75 |
76 | return strings.Repeat("#", paddingLen)
77 | }
78 |
79 | func getRandString(length int) string {
80 | const alphabet = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
81 | randString := make([]byte, length)
82 | for i := range randString {
83 | randString[i] = alphabet[getRandInt(0, len(alphabet)-1)]
84 | }
85 | return string(randString)
86 | }
87 |
88 | // obfuscateTagAndProtobuf() generates key-pair and combines it /w stationPubkey to generate
89 | // sharedSecret. Client will use Eligator to find and send uniformly random representative for its
90 | // public key (and avoid sending it directly over the wire, as points on ellyptic curve are
91 | // distinguishable)
92 | // Then the sharedSecret will be used to encrypt stegoPayload and protobuf slices:
93 | // - stegoPayload is encrypted with AES-GCM KEY=sharedSecret[0:16], IV=sharedSecret[16:28]
94 | // - protobuf is encrypted with AES-GCM KEY=sharedSecret[0:16], IV={new random IV}, that will be
95 | // prepended to encryptedProtobuf and eventually sent out together
96 | // Returns
97 | // - tag(concatenated representative and encrypted stegoPayload),
98 | // - encryptedProtobuf(concatenated 12 byte IV + encrypted protobuf)
99 | // - error
100 | func obfuscateTagAndProtobuf(stegoPayload []byte, protobuf []byte, stationPubkey []byte) ([]byte, []byte, error) {
101 | if len(stationPubkey) != 32 {
102 | return nil, nil, errors.New("Unexpected station pubkey length. Expected: 32." +
103 | " Received: " + strconv.Itoa(len(stationPubkey)) + ".")
104 | }
105 | var sharedSecret, clientPrivate, clientPublic, representative [32]byte
106 | for ok := false; ok != true; {
107 | var sliceKeyPrivate []byte = clientPrivate[:]
108 | _, err := rand.Read(sliceKeyPrivate)
109 | if err != nil {
110 | return nil, nil, err
111 | }
112 |
113 | ok = extra25519.ScalarBaseMult(&clientPublic, &representative, &clientPrivate)
114 | }
115 | var stationPubkeyByte32 [32]byte
116 | copy(stationPubkeyByte32[:], stationPubkey)
117 | curve25519.ScalarMult(&sharedSecret, &clientPrivate, &stationPubkeyByte32)
118 |
119 | // extra25519.ScalarBaseMult does not randomize most significant bit(sign of y_coord?)
120 | // Other implementations of elligator may have up to 2 non-random bits.
121 | // Here we randomize the bit, expecting it to be flipped back to 0 on station
122 | randByte := make([]byte, 1)
123 | _, err := rand.Read(randByte)
124 | if err != nil {
125 | return nil, nil, err
126 | }
127 | representative[31] |= (0x80 & randByte[0])
128 |
129 | tagBuf := new(bytes.Buffer) // What we have to encrypt with the shared secret using AES
130 | tagBuf.Write(representative[:])
131 |
132 | stationPubkeyHash := sha256.Sum256(sharedSecret[:])
133 | aesKey := stationPubkeyHash[:16]
134 | aesIvTag := stationPubkeyHash[16:28] // 12 bytes for stegoPayload nonce
135 |
136 | encryptedStegoPayload, err := aesGcmEncrypt(stegoPayload, aesKey, aesIvTag)
137 | if err != nil {
138 | return nil, nil, err
139 | }
140 |
141 | tagBuf.Write(encryptedStegoPayload)
142 | tag := tagBuf.Bytes()
143 |
144 | if len(protobuf) == 0 {
145 | return tag, nil, err
146 | }
147 |
148 | // probably could have used all zeros as IV here, but better to err on safe side
149 | aesIvProtobuf := make([]byte, 12)
150 | _, err = rand.Read(aesIvProtobuf)
151 | if err != nil {
152 | return nil, nil, err
153 | }
154 |
155 | encryptedProtobuf, err := aesGcmEncrypt(protobuf, aesKey, aesIvProtobuf)
156 | return tag, append(aesIvProtobuf, encryptedProtobuf...), err
157 | }
158 |
159 | func getMsgWithHeader(msgType msgType, msgBytes []byte) []byte {
160 | if len(msgBytes) == 0 {
161 | return nil
162 | }
163 | bufSend := new(bytes.Buffer)
164 | var err error
165 | switch msgType {
166 | case msgProtobuf:
167 | if len(msgBytes) <= int(maxInt16) {
168 | bufSend.Grow(2 + len(msgBytes)) // to avoid double allocation
169 | err = binary.Write(bufSend, binary.BigEndian, int16(len(msgBytes)))
170 |
171 | } else {
172 | bufSend.Grow(2 + 4 + len(msgBytes)) // to avoid double allocation
173 | bufSend.Write([]byte{0, 0})
174 | err = binary.Write(bufSend, binary.BigEndian, int32(len(msgBytes)))
175 | }
176 | case msgRawData:
177 | err = binary.Write(bufSend, binary.BigEndian, int16(-len(msgBytes)))
178 | default:
179 | panic("getMsgWithHeader() called with msgType: " + strconv.Itoa(int(msgType)))
180 | }
181 | if err != nil {
182 | // shouldn't ever happen
183 | Logger().Errorln("getMsgWithHeader() failed with error: ", err)
184 | Logger().Errorln("msgType ", msgType)
185 | Logger().Errorln("msgBytes ", msgBytes)
186 | }
187 | bufSend.Write(msgBytes)
188 | return bufSend.Bytes()
189 | }
190 |
191 | func uint16toInt16(i uint16) int16 {
192 | pos := int16(i & 32767)
193 | neg := int16(0)
194 | if i&32768 != 0 {
195 | neg = int16(-32768)
196 | }
197 | return pos + neg
198 | }
199 |
200 | func reverseEncrypt(ciphertext []byte, keyStream []byte) (plaintext string) {
201 | // our plaintext can be antyhing where x & 0xc0 == 0x40
202 | // i.e. 64-127 in ascii (@, A-Z, [\]^_`, a-z, {|}~ DEL)
203 | // This means that we are allowed to choose the last 6 bits
204 | // of each byte in the ciphertext arbitrarily; the upper 2
205 | // bits will have to be 01, so that our plaintext ends up
206 | // in the desired range.
207 | var ka, kb, kc, kd byte // key stream bytes
208 | var ca, cb, cc, cd byte // ciphertext bytes
209 | var pa, pb, pc, pd byte // plaintext bytes
210 | var sa, sb, sc byte // secret bytes
211 |
212 | var tagIdx, keystreamIdx int
213 |
214 | for tagIdx < len(ciphertext) {
215 | ka = keyStream[keystreamIdx]
216 | kb = keyStream[keystreamIdx+1]
217 | kc = keyStream[keystreamIdx+2]
218 | kd = keyStream[keystreamIdx+3]
219 | keystreamIdx += 4
220 |
221 | // read 3 bytes
222 | sa = ciphertext[tagIdx]
223 | sb = ciphertext[tagIdx+1]
224 | sc = ciphertext[tagIdx+2]
225 | tagIdx += 3
226 |
227 | // figure out what plaintext needs to be in base64 encode
228 | ca = (ka & 0xc0) | ((sa & 0xfc) >> 2) // 6 bits sa
229 | cb = (kb & 0xc0) | (((sa & 0x03) << 4) | ((sb & 0xf0) >> 4)) // 2 bits sa, 4 bits sb
230 | cc = (kc & 0xc0) | (((sb & 0x0f) << 2) | ((sc & 0xc0) >> 6)) // 4 bits sb, 2 bits sc
231 | cd = (kd & 0xc0) | (sc & 0x3f) // 6 bits sc
232 |
233 | // Xor with key_stream, and add on 0x40 so it's in range of allowed
234 | pa = (ca ^ ka) + 0x40
235 | pb = (cb ^ kb) + 0x40
236 | pc = (cc ^ kc) + 0x40
237 | pd = (cd ^ kd) + 0x40
238 |
239 | plaintext += string(pa)
240 | plaintext += string(pb)
241 | plaintext += string(pc)
242 | plaintext += string(pd)
243 | }
244 | return
245 | }
246 |
247 | func minInt(a, b int) int {
248 | if a > b {
249 | return b
250 | }
251 | return a
252 | }
253 |
254 | func maxInt(a, b int) int {
255 | if a > b {
256 | return a
257 | }
258 | return b
259 | }
260 |
261 | // Converts provided duration to raw milliseconds.
262 | // Returns a pointer to u32, because protobuf wants pointers.
263 | // Max valid input duration (that fits into uint32): 49.71 days.
264 | func durationToU32ptrMs(d time.Duration) *uint32 {
265 | i := uint32(d.Nanoseconds() / int64(time.Millisecond))
266 | return &i
267 | }
268 |
269 | func readAndClose(c net.Conn, readDeadline time.Duration) {
270 | tinyBuf := []byte{0}
271 | c.SetReadDeadline(time.Now().Add(readDeadline))
272 | c.Read(tinyBuf)
273 | c.Close()
274 | }
275 |
276 | func errIsTimeout(err error) bool {
277 | if err != nil {
278 | if strings.Contains(err.Error(), ": i/o timeout") || // client timed out
279 | err.Error() == "EOF" { // decoy timed out
280 | return true
281 | }
282 | }
283 | return false
284 | }
285 |
--------------------------------------------------------------------------------
/tapdance/utils_test.go:
--------------------------------------------------------------------------------
1 | package tapdance
2 |
3 | import (
4 | "crypto/aes"
5 | "crypto/cipher"
6 | "crypto/rand"
7 | "encoding/hex"
8 | "fmt"
9 | "github.com/pkg/errors"
10 | "strings"
11 | "testing"
12 | )
13 |
14 | type TestRandReader struct{}
15 |
16 | func (z TestRandReader) Read(b []byte) (n int, err error) {
17 | for i := range b {
18 | b[i] = 4 // chosen by fair dice roll
19 | }
20 |
21 | return len(b), nil
22 | }
23 |
24 | var testRandReader TestRandReader
25 |
26 | func TestObfuscate(t *testing.T) {
27 | tag := []byte{247, 20, 77, 35, 144, 14, 9, 150, 39, 129, 27, 186, 173, 52, 160, 245, 185,
28 | 104, 186, 237, 127, 61, 217, 235, 40, 126, 189, 122, 132, 22, 194, 228, 104, 246,
29 | 6, 227, 87, 154, 209, 142, 128, 28, 104, 119, 203, 156, 239, 202, 63, 158, 98, 72,
30 | 223, 122, 113, 220, 252, 30, 125, 11, 38, 240, 244, 39, 209, 221, 33, 42, 100, 50,
31 | 225, 8, 150, 249, 192, 189, 65, 52, 200, 217, 250, 134, 72, 94, 189, 14, 159, 222,
32 | 94, 91, 179, 98, 131, 228, 227, 86, 213, 43, 203, 11, 114, 9, 162, 33, 32, 242, 82,
33 | 216, 167, 113, 216, 200, 117, 178, 135, 208, 209, 205, 120, 67, 131, 125, 171, 54,
34 | 29, 90, 96, 52, 45, 202, 140, 64, 45, 130, 227, 56, 70, 131, 25, 169, 41, 101, 70,
35 | 120, 171, 130, 187, 108, 140, 250, 71, 179, 178, 189, 122, 165, 138, 12, 146, 112,
36 | 66, 71, 204, 45, 160, 115, 255, 249, 40, 123, 48, 96, 0, 202, 172, 248, 208, 229,
37 | 210, 91, 245, 125, 47, 38, 124, 8}
38 | obfuscatedRef := []byte{3, 40, 135, 68, 129, 74, 207, 209, 133, 234, 68, 208, 180, 131, 4,
39 | 158, 228, 29, 180, 150, 39, 71, 210, 214, 44, 197, 31, 194, 58, 84, 103, 28, 165,
40 | 94, 112, 81, 194, 103, 215, 121, 19, 150, 133, 43, 212, 227, 153, 16, 29, 108, 102,
41 | 48, 131, 95, 104, 151, 38, 59, 84, 18, 59, 198, 35, 159, 181, 149, 139, 249, 237,
42 | 89, 205, 85, 109, 156, 130, 69, 164, 145, 184, 85, 61, 9, 30, 73, 186, 52, 201, 179,
43 | 170, 117, 225, 48, 122, 225, 121, 3, 71, 133, 40, 76, 17, 56, 41, 21, 173, 56, 134,
44 | 117, 25, 139, 123, 47, 182, 138, 252, 243, 239, 143, 127, 218, 203, 73, 75, 6, 79,
45 | 113, 217, 0, 44, 248, 226, 110, 81, 34, 69, 66, 15, 112, 133, 130, 118, 64, 217, 44,
46 | 19, 14, 34, 84, 124, 154, 65, 13, 118, 117, 160, 66, 56, 147, 18, 116, 153, 101, 90,
47 | 126, 107, 105, 5, 7, 109, 46, 127, 154, 255, 232, 97, 96, 192, 207, 94, 193, 56,
48 | 232, 66, 199, 189, 30, 51, 66, 133, 187, 173, 75, 216, 194, 59, 189, 78, 123, 79,
49 | 182, 177, 67, 92, 210, 96, 177, 142, 54, 182, 16, 58, 12, 106, 224, 28, 232, 241,
50 | 241, 228, 163, 211, 99, 83, 233, 176, 50, 166, 173, 106, 129, 52, 148, 188, 240}
51 | pubkey := []byte{180, 112, 102, 188, 57, 13, 38, 5, 204, 19, 88, 28, 73, 110, 169, 149, 203,
52 | 140, 250, 223, 0, 166, 73, 5, 37, 9, 239, 74, 200, 165, 26, 7}
53 |
54 | oldReader := rand.Reader
55 | defer func() { rand.Reader = oldReader }()
56 | rand.Reader = testRandReader
57 | obfuscated, _, err := obfuscateTagAndProtobuf(tag, nil, pubkey)
58 | if err != nil {
59 | t.Fatalf("Error: %v\n", err)
60 | }
61 | for i := range obfuscated {
62 | if obfuscatedRef[i] != obfuscated[i] {
63 | t.Fatalf("Obfuscated tag expected: %s. Got: %s\n", obfuscatedRef, obfuscated)
64 | }
65 | }
66 | }
67 |
68 | // this function is only currently used in testing
69 | func aesGcmDecrypt(ciphertext []byte, key []byte, iv []byte) (plaintext []byte, err error) {
70 | block, err := aes.NewCipher(key)
71 | if err != nil {
72 | return
73 | }
74 |
75 | aesGcmCipher, err := cipher.NewGCM(block)
76 | if err != nil {
77 | return
78 | }
79 |
80 | plaintext, err = aesGcmCipher.Open(nil, iv, ciphertext, nil)
81 | if err != nil {
82 | return
83 | }
84 | return
85 | }
86 |
87 | func TestAES_GCM_EncryptDecrypt(t *testing.T) {
88 | iv, _ := hex.DecodeString("156738805e207a6f2c50413a")
89 | key, _ := hex.DecodeString("13c8ff335b01aaf970cbc7b7e3072249")
90 | plaintext, _ := hex.DecodeString("560b81b86b1f30da6d66a982310be6af471f2e9de248a58aa2731bd9b746532c98666d9e9963b1d02ec1d759c228f599411229cd98f2bfd71ad6007f71e4d6bc20a3e2a00322df06159536534480ec97288929dc87cbd658c49894d40b1997292bfa720625e18661fa66999cf4e7030c8bf4cfbe15d77d47c13d5236a8c797e95e80df9d7af6730d35f9a7aa9f5e478b739516bd6e0e5e64dcbc6cda669fdc5f0efbac5e23b25a3ad91e005e276d39438285bfe00c3b53b33f7127becc49ff9825d78f3cab06d315e22aea83a12cb69547d40a5d36c1d5cd288efc678a627cab2583c80f1d81bc3e3d27a4bd")
91 | expectedCryptotext, _ := hex.DecodeString("8ba27bd00b04ec8c8448d517d444b732e8e7179153eaaa9ffdddc8733d88e97dd86fee5eedf96395ba6bfbf98e0e9c74e72baa90ad0271fb621500eb9e15a0c984aa9c886db3f4cb1aab16b42aad4be78b477e9b57ada945fe7e3eb063bf0aff1800d9ec5a9c3be895ef0b785165a592f18fdf3184d167db2be93cd4b6e5e8dd533ee3bca05e19abea75d50aa68fa1ffd2da37090f6e73e94b1372ea2585eabd8f9c388d1cb4e058bbc72e2cd2d286135665944d0bd99bfea1ec06213ffd451252cf16b13828eee7e5688ad78d959d6447c841d4f52e58eac03baab1f2ba86f6fd0a9e68ac0e4d375a764507229a075d0e87661d84887a6d74d71297")
92 |
93 | cryptotext, err := aesGcmEncrypt(plaintext, key, iv)
94 | if err != nil {
95 | t.Fatalf(err.Error())
96 | }
97 | rePlaintext, err := aesGcmDecrypt(cryptotext, key, iv)
98 |
99 | if err != nil {
100 | t.Fatalf(err.Error())
101 | }
102 |
103 | if strings.Compare(string(plaintext), string(rePlaintext)) != 0 {
104 | t.Fatalf("Decrypted text differs from original!\nDecrypt(Encrypt(text)): %s\ntext: %s\n",
105 | hex.Dump(rePlaintext), hex.Dump(plaintext))
106 | }
107 |
108 | if strings.Compare(string(cryptotext), string(expectedCryptotext)) != 0 {
109 | t.Fatalf("Encrypted text differs from expected!\nExpected: %s\nGot: %s\n",
110 | hex.Dump(expectedCryptotext), hex.Dump(cryptotext))
111 | }
112 |
113 | }
114 |
115 | func TestReverseEncrypt(t *testing.T) {
116 | tag := []byte{192, 165, 165, 138, 112, 105, 67, 167, 10, 78, 204, 32, 77, 236, 146, 173, 91, 175, 146, 53, 43, 15, 69, 55, 133, 158, 89, 221, 140, 12, 117, 34, 155, 231, 154, 103, 195, 18, 139, 225, 245, 92, 240, 135, 121, 95, 51, 38, 110, 231, 27, 218, 38, 127, 128, 35, 170, 52, 162, 219, 27, 24, 249, 191, 194, 251, 188, 93, 85, 211, 229, 150, 151, 189, 34, 252, 105, 173, 227, 169, 97, 191, 137, 37, 110, 235, 72, 170, 99, 143, 98, 201, 2, 80, 226, 224, 2, 143, 7, 116, 26, 29, 199, 232, 112, 105, 209, 37, 55, 108, 161, 205, 10, 43, 172, 78, 169, 94, 44, 130, 201, 232, 192, 37, 1, 127, 33, 89, 183, 114, 83, 210, 122, 132, 135, 242, 96, 115, 61, 147, 41, 179, 237, 34, 72, 153, 81, 47, 11, 117, 95, 224, 60, 198, 211, 181, 221, 185, 117, 3, 172, 6, 189, 90, 237, 81, 147, 118, 8, 31, 165, 59, 143, 60, 120, 39, 228, 156, 199, 166, 140, 165, 241, 150, 242, 198}
117 | keystream := []byte{246, 204, 136, 183, 208, 201, 249, 218, 131, 117, 96, 249, 155, 7, 222, 35, 221, 95, 82, 237, 27, 90, 158, 165, 132, 44, 1, 229, 127, 116, 20, 135, 203, 220, 175, 224, 16, 136, 75, 172, 14, 20, 128, 238, 168, 192, 231, 133, 209, 154, 71, 205, 161, 135, 195, 135, 9, 66, 207, 28, 238, 90, 252, 4, 121, 229, 79, 84, 246, 167, 123, 187, 73, 65, 97, 219, 229, 93, 188, 135, 236, 84, 230, 5, 207, 105, 254, 181, 177, 68, 222, 192, 190, 182, 177, 33, 252, 118, 161, 101, 60, 35, 233, 36, 22, 242, 198, 8, 20, 151, 249, 172, 207, 58, 95, 110, 19, 84, 169, 17, 185, 3, 120, 102, 48, 13, 40, 238, 150, 10, 174, 204, 0, 144, 21, 250, 4, 39, 211, 85, 164, 90, 12, 104, 43, 130, 224, 77, 113, 79, 142, 97, 205, 71, 156, 211, 73, 42, 51, 169, 30, 81, 132, 85, 217, 18, 151, 184, 166, 32, 188, 0, 170, 67, 80, 80, 253, 165, 42, 5, 6, 27, 72, 62, 57, 7, 174, 156, 198, 72, 224, 132, 199, 175, 28, 175, 193, 17, 242, 143, 4, 152, 83, 205, 50, 26, 171, 28, 27, 190, 226, 5, 214, 152, 232, 131, 212, 104, 186, 219, 178, 172, 234, 35, 1, 177, 25, 79, 79, 166, 185, 85, 167, 110, 88, 114, 49, 201, 163, 201, 20, 139, 106, 125, 151, 191, 47, 160, 254, 34, 173, 229}
118 | result := reverseEncrypt(tag, keystream)
119 | expectedResult := "FF^RrnxsSO|sHknCNA`pOpJ`OUN|@@pjEVygP{`SFJuQyNbakMFYXV[uJReyipbbKSO@EbDiCoqhWw\\jeE|`UuN^AkUJHgwYMUGCeOInHci{o]ITTrfyrg^ao\\ddCcNVbRK]Q}guYre~GHMftRlB_fJfCfz^HAklOgUPBRGnuZwvf_BcMxBz}IMv^bujvTfUfy~Ja_zSfSqCweileGpVbXE{}QvfugUCpgjA^EiylGVVE}owA}LrPdf"
120 | if strings.Compare(expectedResult, result) != 0 {
121 | t.Fatalf("Expected encryption differs from result!\nExpected: %s\nGot: %s\n",
122 | expectedResult, result)
123 | }
124 | }
125 |
126 | func TestObfuscationRandomness(t *testing.T) {
127 | testKey := []byte{180, 112, 102, 188, 57, 13, 38, 5, 204, 19, 88, 28, 73, 110, 169, 149, 203,
128 | 140, 250, 223, 0, 166, 73, 5, 37, 9, 239, 74, 200, 165, 26, 7}
129 |
130 | tag := make([]byte, 177)
131 |
132 | rc := randomnessChecker{}
133 | for i := 0; i < 10000; i++ {
134 | _, err := rand.Read(tag)
135 | if err != nil {
136 | t.Fatalf("Error: %v\n", err)
137 | }
138 | obfuscated, _, err := obfuscateTagAndProtobuf(tag, nil, testKey)
139 | if err != nil {
140 | t.Fatalf("Error: %v\n", err)
141 | }
142 | rc.addSample(obfuscated)
143 | }
144 |
145 | err := rc.testInRange(4700, 5300)
146 | if err != nil {
147 | t.Fatal(err)
148 | }
149 | }
150 |
151 | // only supports samples of same size
152 | type randomnessChecker struct {
153 | bitCounts []int // how many bits are 1
154 | sampleCounts []int // how many samples for bit
155 | }
156 |
157 | func (rt *randomnessChecker) addSample(sample []byte) {
158 | for len(rt.bitCounts) < 8*len(sample) {
159 | // allocate bigger arrays (they are all of same size)
160 | rt.bitCounts = append(rt.bitCounts, make([]int, 8*len(sample))...)
161 | rt.sampleCounts = append(rt.sampleCounts, make([]int, 8*len(sample))...)
162 | }
163 |
164 | for sampleIdx := 0; sampleIdx < len(sample); sampleIdx++ {
165 | for bitIdx := 0; bitIdx < 8; bitIdx++ {
166 | mask := byte(1 << uint(bitIdx))
167 | bitCountIdx := sampleIdx*8 + bitIdx
168 | rt.sampleCounts[bitCountIdx] += 1
169 | if sample[sampleIdx]&mask >= 1 {
170 | rt.bitCounts[bitCountIdx] += 1
171 | }
172 | }
173 | }
174 |
175 | }
176 |
177 | func (rt *randomnessChecker) getNumSamples() int {
178 | numSamples := 0
179 | for i := 0; i < len(rt.sampleCounts); i++ {
180 | if rt.sampleCounts[0] != 0 {
181 | numSamples += 1
182 | }
183 | }
184 | return numSamples
185 | }
186 |
187 | // returns error if there are clear issues with randomness
188 | func (rt *randomnessChecker) testSimple() error {
189 | numSamples := rt.getNumSamples()
190 | for i := 0; i < numSamples; i++ {
191 | if rt.bitCounts[i] == 0 {
192 | return errors.New(fmt.Sprintf("Bit #%v is always zero. Sampled %v times.",
193 | i, rt.sampleCounts[i]))
194 | }
195 | if rt.bitCounts[i] == rt.sampleCounts[i] {
196 | return errors.New(fmt.Sprintf("Bit #%v is always one. Sampled %v times.",
197 | i, rt.sampleCounts[i]))
198 | }
199 | }
200 | return nil
201 | }
202 |
203 | // returns error if amount of times a bit is set is not in [min, max]
204 | func (rt *randomnessChecker) testInRange(min, max int) error {
205 | numSamples := rt.getNumSamples()
206 | for i := 0; i < numSamples; i++ {
207 | if rt.bitCounts[i] < min || rt.bitCounts[i] > max {
208 | return errors.New(fmt.Sprintf("Expected: bit #%v is set %v - %v times"+
209 | " out of %v samples. Got: bit is set %v times.",
210 | i, min, max, rt.sampleCounts[i], rt.bitCounts[i]))
211 | }
212 | }
213 | return nil
214 | }
215 |
--------------------------------------------------------------------------------
/tdproxy/README.md:
--------------------------------------------------------------------------------
1 | # tdproxy
2 | `import "github.com/sergeyfrolov/gotapdance/tdproxy"`
3 |
4 | * [Overview](#pkg-overview)
5 | * [Imported Packages](#pkg-imports)
6 | * [Index](#pkg-index)
7 |
8 | ## Overview
9 | Package tdproxy implements TapdanceProxy, which can ListenAndServe() on a given port,
10 | so you can use it as a SOCKS or HTTP proxy elsewhere.
11 |
12 | ## Imported Packages
13 |
14 | - [github.com/sergeyfrolov/gotapdance/tapdance](./../tapdance)
15 |
16 | ## Index
17 | * [Constants](#pkg-constants)
18 | * [Variables](#pkg-variables)
19 | * [type TapDanceProxy](#TapDanceProxy)
20 | * [func NewTapDanceProxy(listenPort int) \*TapDanceProxy](#NewTapDanceProxy)
21 | * [func (proxy \*TapDanceProxy) GetStatistics() (statistics string)](#TapDanceProxy.GetStatistics)
22 | * [func (proxy \*TapDanceProxy) GetStats() (stats string)](#TapDanceProxy.GetStats)
23 | * [func (proxy \*TapDanceProxy) ListenAndServe() error](#TapDanceProxy.ListenAndServe)
24 | * [func (proxy \*TapDanceProxy) Stop() error](#TapDanceProxy.Stop)
25 |
26 | #### Package files
27 | [flow.go](./flow.go) [tapdance.go](./tapdance.go)
28 |
29 | ## Constants
30 | ``` go
31 | const (
32 | ProxyStateInitialized = "Initialized"
33 | ProxyStateListening = "Listening"
34 | ProxyStateStopped = "Stopped"
35 | ProxyStateError = "Error"
36 | )
37 | ```
38 |
39 | ## Variables
40 | ``` go
41 | var Logger = tapdance.Logger()
42 | ```
43 |
44 | ## type [TapDanceProxy](./tapdance.go#L23-L46)
45 | ``` go
46 | type TapDanceProxy struct {
47 | State string
48 | // contains filtered or unexported fields
49 | }
50 | ```
51 | TODO: consider implementing https://golang.org/pkg/net/#Listener or other default interface
52 |
53 | ### func [NewTapDanceProxy](./tapdance.go#L48)
54 | ``` go
55 | func NewTapDanceProxy(listenPort int) *TapDanceProxy
56 | ```
57 |
58 | ### func (\*TapDanceProxy) [GetStatistics](./tapdance.go#L132)
59 | ``` go
60 | func (proxy *TapDanceProxy) GetStatistics() (statistics string)
61 | ```
62 |
63 | ### func (\*TapDanceProxy) [GetStats](./tapdance.go#L146)
64 | ``` go
65 | func (proxy *TapDanceProxy) GetStats() (stats string)
66 | ```
67 |
68 | ### func (\*TapDanceProxy) [ListenAndServe](./tapdance.go#L70)
69 | ``` go
70 | func (proxy *TapDanceProxy) ListenAndServe() error
71 | ```
72 |
73 | ### func (\*TapDanceProxy) [Stop](./tapdance.go#L100)
74 | ``` go
75 | func (proxy *TapDanceProxy) Stop() error
76 | ```
77 |
78 | - - -
79 | Generated by [godoc2ghmd](https://github.com/GandalfUK/godoc2ghmd)
--------------------------------------------------------------------------------
/tdproxy/flow.go:
--------------------------------------------------------------------------------
1 | package tdproxy
2 |
3 | import (
4 | "errors"
5 | "github.com/sergeyfrolov/gotapdance/tapdance"
6 | "io"
7 | "net"
8 | "strconv"
9 | "strings"
10 | "time"
11 | )
12 |
13 | // Connection-oriented state
14 | type tapDanceFlow struct {
15 | // tunnel index and start time
16 | id uint64
17 | startMs time.Time
18 |
19 | // reference to global proxy
20 | proxy *TapDanceProxy
21 |
22 | servConn net.Conn // can cast to tapdance.Conn but don't need to
23 | userConn net.Conn
24 | splitFlows bool
25 | }
26 |
27 | // TODO: use dial() functor
28 | func makeTapDanceFlow(proxy *TapDanceProxy, id uint64, splitFlows bool) *tapDanceFlow {
29 | tdFlow := new(tapDanceFlow)
30 |
31 | tdFlow.proxy = proxy
32 | tdFlow.id = id
33 |
34 | tdFlow.startMs = time.Now()
35 | tdFlow.splitFlows = splitFlows
36 |
37 | Logger.Debugf("Created new TD Flow: %#v\n", tdFlow)
38 | return tdFlow
39 | }
40 |
41 | func (TDstate *tapDanceFlow) redirect() error {
42 | dialer := tapdance.Dialer{SplitFlows: TDstate.splitFlows}
43 | var err error
44 | TDstate.servConn, err = dialer.DialProxy()
45 | if err != nil {
46 | TDstate.userConn.Close()
47 | return err
48 | }
49 | errChan := make(chan error)
50 | defer func() {
51 | TDstate.userConn.Close()
52 | TDstate.servConn.Close()
53 | _ = <-errChan // wait for second goroutine to close
54 | }()
55 |
56 | forwardFromServerToClient := func() {
57 | buf := make([]byte, 65536)
58 | n, _err := io.CopyBuffer(TDstate.userConn, TDstate.servConn, buf)
59 | Logger.Debugf("{tapDanceFlow} forwardFromServerToClient returns, bytes sent: " +
60 | strconv.FormatUint(uint64(n), 10))
61 | if _err == nil {
62 | _err = errors.New("server returned without error")
63 | }
64 | errChan <- _err
65 | return
66 | }
67 |
68 | forwardFromClientToServer := func() {
69 | buf := make([]byte, 65536)
70 | n, _err := io.CopyBuffer(TDstate.servConn, TDstate.userConn, buf)
71 | Logger.Debugf("{tapDanceFlow} forwardFromClientToServer returns, bytes sent: " +
72 | strconv.FormatUint(uint64(n), 10))
73 | if _err == nil {
74 | _err = errors.New("closed by application layer")
75 | }
76 | errChan <- _err
77 | return
78 | }
79 |
80 | go forwardFromServerToClient()
81 | go forwardFromClientToServer()
82 |
83 | if err = <-errChan; err != nil {
84 | if err.Error() == "MSG_CLOSE" || err.Error() == "closed by application layer" {
85 | Logger.Debugln("[Session " + strconv.FormatUint(uint64(TDstate.id), 10) +
86 | " Redirect function returns gracefully: " + err.Error())
87 | TDstate.proxy.closedGracefully.Inc()
88 | err = nil
89 | } else {
90 | str_err := err.Error()
91 |
92 | // statistics
93 | if strings.Contains(str_err, "TapDance station didn't pick up the request") {
94 | TDstate.proxy.notPickedUp.Inc()
95 | } else if strings.Contains(str_err, ": i/o timeout") {
96 | TDstate.proxy.timedOut.Inc()
97 | } else {
98 | TDstate.proxy.unexpectedError.Inc()
99 | }
100 | }
101 | }
102 | return err
103 | }
104 |
--------------------------------------------------------------------------------
/tdproxy/gengodoc.sh:
--------------------------------------------------------------------------------
1 | PACKAGE="github.com/sergeyfrolov/gotapdance/tdproxy"
2 | RFILE="$GOPATH/src/$PACKAGE/README.md"
3 | godoc2ghmd $PACKAGE > $RFILE
4 |
5 |
--------------------------------------------------------------------------------
/tdproxy/tapdance.go:
--------------------------------------------------------------------------------
1 | // Package tdproxy implements TapdanceProxy, which can ListenAndServe() on a given port,
2 | // so you can use it as a SOCKS or HTTP proxy elsewhere.
3 | package tdproxy
4 |
5 | import (
6 | "github.com/sergeyfrolov/gotapdance/tapdance"
7 | "net"
8 | "strconv"
9 | "sync"
10 | "time"
11 | )
12 |
13 | var Logger = tapdance.Logger()
14 |
15 | const (
16 | ProxyStateInitialized = "Initialized"
17 | ProxyStateListening = "Listening"
18 | ProxyStateStopped = "Stopped"
19 | ProxyStateError = "Error"
20 | )
21 |
22 | // TODO: consider implementing https://golang.org/pkg/net/#Listener or other default interface
23 | type TapDanceProxy struct {
24 | State string
25 |
26 | listener net.Listener
27 |
28 | listenPort int
29 |
30 | countTunnels tapdance.CounterUint64
31 |
32 | // statistics
33 | notPickedUp tapdance.CounterUint64
34 | timedOut tapdance.CounterUint64
35 | closedGracefully tapdance.CounterUint64
36 | unexpectedError tapdance.CounterUint64
37 |
38 | connections struct {
39 | sync.RWMutex
40 | m map[uint64]*tapDanceFlow
41 | }
42 |
43 | statsTicker *time.Ticker
44 |
45 | stop bool
46 | }
47 |
48 | func NewTapDanceProxy(listenPort int) *TapDanceProxy {
49 | //Logger.Level = logrus.DebugLevel
50 | proxy := new(TapDanceProxy)
51 | proxy.listenPort = listenPort
52 |
53 | proxy.connections.m = make(map[uint64]*tapDanceFlow)
54 | proxy.State = ProxyStateInitialized
55 |
56 | Logger.Infof("Successfully initialized new Tapdance Proxy")
57 | Logger.Debugf("%#v\n", proxy)
58 |
59 | return proxy
60 | }
61 |
62 | func (proxy *TapDanceProxy) statsHelper() error {
63 | proxy.statsTicker = time.NewTicker(time.Second * time.Duration(60))
64 | for range proxy.statsTicker.C {
65 | Logger.Infof(proxy.GetStatistics())
66 | }
67 | return nil
68 | }
69 |
70 | func (proxy *TapDanceProxy) ListenAndServe() error {
71 | var err error
72 | listenAddress := "127.0.0.1:" + strconv.Itoa(proxy.listenPort)
73 |
74 | proxy.State = ProxyStateListening
75 | proxy.stop = false
76 | if proxy.listener, err = net.Listen("tcp", listenAddress); err != nil {
77 | proxy.State = ProxyStateError
78 | return err
79 | }
80 | Logger.Infof("Accepting connections at port " + strconv.Itoa(proxy.listenPort))
81 | go proxy.statsHelper()
82 |
83 | for !proxy.stop {
84 | if conn, err := proxy.listener.Accept(); err == nil {
85 | go proxy.handleUserConn(conn)
86 | } else {
87 | if proxy.stop {
88 | proxy.State = ProxyStateStopped
89 | err = nil
90 | } else {
91 | proxy.State = ProxyStateError
92 | }
93 | return err
94 | }
95 | }
96 | proxy.State = ProxyStateStopped
97 | return nil
98 | }
99 |
100 | func (proxy *TapDanceProxy) Stop() error {
101 | proxy.stop = true
102 | proxy.listener.Close()
103 | proxy.connections.Lock()
104 | for _, tdState := range proxy.connections.m {
105 | tdState.servConn.Close()
106 | }
107 | proxy.connections.Unlock()
108 | proxy.statsTicker.Stop()
109 | return nil
110 | }
111 |
112 | func (proxy *TapDanceProxy) handleUserConn(userConn net.Conn) {
113 | tdState := proxy.addFlow(&userConn)
114 | defer func() {
115 | proxy.connections.Lock()
116 | delete(proxy.connections.m, tdState.id)
117 | proxy.connections.Unlock()
118 | }()
119 |
120 | // Initial request is not lost, because we still haven't read anything from client socket
121 | // So we just start Redirecting (client socket) <-> (server socket)
122 | if err := tdState.redirect(); err != nil {
123 | Logger.Errorf("[Session " + strconv.FormatUint(uint64(tdState.id), 10) +
124 | "] Shut down with error: " + err.Error())
125 | } else {
126 | Logger.Infof("[Session " + strconv.FormatUint(uint64(tdState.id), 10) +
127 | "] Closed gracefully.")
128 | }
129 | return
130 | }
131 |
132 | func (proxy *TapDanceProxy) GetStatistics() (statistics string) {
133 | statistics = "Sessions total: " +
134 | strconv.FormatUint(uint64(proxy.countTunnels.Get()), 10)
135 | statistics += ". Not picked up: " +
136 | strconv.FormatUint(uint64(proxy.notPickedUp.Get()), 10)
137 | statistics += ". Timed out: " +
138 | strconv.FormatUint(uint64(proxy.timedOut.Get()), 10)
139 | statistics += ". Unexpected error: " +
140 | strconv.FormatUint(uint64(proxy.unexpectedError.Get()), 10)
141 | statistics += ". Graceful close: " +
142 | strconv.FormatUint(uint64(proxy.closedGracefully.Get()), 10)
143 | return
144 | }
145 |
146 | func (proxy *TapDanceProxy) GetStats() (stats string) {
147 | stats = proxy.State + "\nPort: " + strconv.Itoa(proxy.listenPort) +
148 | "\nActive connections: " + strconv.Itoa(len(proxy.connections.m))
149 | return
150 | }
151 |
152 | func (proxy *TapDanceProxy) addFlow(userConn *net.Conn) (pTapdanceState *tapDanceFlow) {
153 | // Init connection state
154 | id := proxy.countTunnels.GetAndInc()
155 |
156 | pTapdanceState = makeTapDanceFlow(proxy, id, false)
157 | pTapdanceState.userConn = *userConn
158 |
159 | proxy.connections.Lock()
160 | proxy.connections.m[id] = pTapdanceState
161 | proxy.connections.Unlock()
162 |
163 | return
164 | }
165 |
--------------------------------------------------------------------------------
/tdproxy/tapdance_test.go:
--------------------------------------------------------------------------------
1 | package tdproxy
2 |
3 | import (
4 | pb "github.com/sergeyfrolov/gotapdance/protobuf"
5 | "github.com/sergeyfrolov/gotapdance/tapdance"
6 | "io/ioutil"
7 | "os"
8 | "testing"
9 |
10 | "crypto/tls"
11 | "fmt"
12 | "golang.org/x/net/websocket"
13 | "math/rand"
14 | "time"
15 |
16 | "io"
17 | )
18 |
19 | func setupTestAssets() error {
20 | tmpDir, err := ioutil.TempDir("/tmp/", "td-test-")
21 | if err != nil {
22 | return err
23 | }
24 | tapdance.AssetsSetDir(tmpDir)
25 | // make sure station won't send new ClientConf
26 | err = tapdance.Assets().SetGeneration(100500)
27 | if err != nil {
28 | return err
29 | }
30 |
31 | // use testing public key
32 | keyType := pb.KeyType_AES_GCM_128
33 | stationTestPubkey, err := ioutil.ReadFile("../assets/station_pubkey_test")
34 | if err != nil {
35 | return err
36 | }
37 |
38 | pubKey := pb.PubKey{
39 | Key: stationTestPubkey,
40 | Type: &keyType,
41 | }
42 | if err != nil {
43 | return err
44 | }
45 | tapdance.Assets().SetPubkey(pubKey)
46 |
47 | // use correct decoy
48 | tapdance1Decoy := pb.InitTLSDecoySpec("192.122.190.104", "tapdance1.freeaeskey.xyz")
49 | err = tapdance.Assets().SetDecoys([]*pb.TLSDecoySpec{tapdance1Decoy})
50 | if err != nil {
51 | return err
52 | }
53 | return nil
54 | }
55 |
56 | func TestMain(m *testing.M) {
57 | err := setupTestAssets()
58 | if err != nil {
59 | panic(err)
60 | }
61 | retCode := m.Run()
62 | os.Exit(retCode)
63 | }
64 |
65 | func TestSendSeq(t *testing.T) {
66 | conn, err := tapdance.Dial("tcp", "sfrolov.io:443")
67 | if err != nil {
68 | t.Error(err)
69 | return
70 | }
71 | conf, err := websocket.NewConfig("wss://sfrolov.io/echo", "http://localhost/")
72 | if err != nil {
73 | t.Error(err)
74 | return
75 | }
76 | wsConn, err := websocket.NewClient(conf,
77 | tls.Client(conn, &tls.Config{ServerName: "sfrolov.io"}))
78 | //err = sendseq.SendSeq(,
79 | // tls.Client(conn, &tls.Config{ServerName: "sfrolov.io"}))
80 | if err != nil {
81 | t.Error(err)
82 | return
83 | }
84 |
85 | rand.Seed(time.Now().UTC().Unix())
86 |
87 | randString := func(n int) []byte {
88 | const alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
89 | b := make([]byte, n)
90 | for i := range b {
91 | b[i] = alphabet[rand.Intn(len(alphabet))]
92 | }
93 | return b
94 | }
95 |
96 | const repetitions = 5
97 | for ii := 0; ii < repetitions; ii++ {
98 | bytesOut := randString(20000 + rand.Intn(40000))
99 | bytesIn := make([]byte, len(bytesOut))
100 | _, err = wsConn.Write(bytesOut)
101 | if err != nil {
102 | t.Error(err)
103 | return
104 | }
105 |
106 | conn.SetDeadline(time.Now().Add(time.Second * time.Duration(10)))
107 | wsConn.SetDeadline(time.Now().Add(time.Second * time.Duration(10)))
108 | _, err = io.ReadFull(wsConn, bytesIn)
109 | if err != nil {
110 | t.Error(err)
111 | }
112 |
113 | for i := range bytesOut {
114 | if bytesIn[i] != bytesOut[i] {
115 | fmt.Println("bytesIn: ", bytesIn)
116 | fmt.Println("bytesOut: ", bytesOut)
117 | t.Errorf("received buffer differs from sent at position %v", i)
118 | }
119 | }
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/test_scripts/README.md:
--------------------------------------------------------------------------------
1 | # Test Scripts
2 |
3 | Collection of quickly written small scripts designed to work with TapDance.
4 |
5 | We move fast and break things, so I don't guarantee your computer
6 | won't catch on fire, if you try to use that.
7 |
8 | * twitter_wget.sh - simply wget's twitter
9 |
10 | * go-1.7.4_wget.sh - downloads Golang 1.7.4 acrhive (~81MB)
11 |
12 | * ip.sh - queries http://ipinfo.io with curl for current ip
13 |
14 | * ssh-td.sh - ssh via TapDance. Usage: `./ssh-td.sh $hostname`
15 |
16 | * nc_send.sh - sends random data to poor innocent server
17 | (specify how much data, e.g. 21k or 42m)
18 |
19 | * seq.py - sends and receives enumerated bytes of data and checks if they are
20 | received successfully and in order. Blatantly stolen from
21 | [ewust's repo](https://github.com/ewust/sendseq).
22 | To use, point TapDance server into seq.py receiver
23 | and proxy seq.py sender through TapDance client.
24 |
25 |
--------------------------------------------------------------------------------
/test_scripts/go-1.7.4_wget.sh:
--------------------------------------------------------------------------------
1 | export https_proxy=127.0.0.1:10500
2 | export http_proxy=127.0.0.1:10500
3 | #wget https://www.twitter.com
4 | rm go1.7.4.linux-amd64.tar.gz
5 | wget storage.googleapis.com/golang/go1.7.4.linux-amd64.tar.gz
6 | sha256sum go1.7.4.linux-amd64.tar.gz
7 | echo "Expected: 47fda42e46b4c3ec93fa5d4d4cc6a748aa3f9411a2a2b7e08e3a6d80d753ec8b"
8 |
--------------------------------------------------------------------------------
/test_scripts/ip.sh:
--------------------------------------------------------------------------------
1 | curl -vi --proxy http://127.0.0.1:10500 "http://ipinfo.io"
2 | echo
3 |
--------------------------------------------------------------------------------
/test_scripts/nc_send.sh:
--------------------------------------------------------------------------------
1 | print_usage() {
2 | echo 'Usage: ./nc_send.sh ${SIZE}'
3 | echo ' ${SIZE} format examples: 67k, 22m '
4 | }
5 |
6 | gen_file() {
7 | mkdir -p random_files
8 | if [ ! -f $rand_filename ]; then
9 | head -c $size $rand_filename
10 | if [ $? -ne 0 ]
11 | then
12 | echo "Generation of file with size ${size} failed!"
13 | print_usage
14 | rm $rand_filename
15 | exit 2
16 | fi
17 | fi
18 | }
19 |
20 | if [[ $# -eq 0 ]] ; then
21 | echo 'Error: send message size is not specified!'
22 | print_usage
23 | exit 1
24 | fi
25 |
26 |
27 | size="$1"
28 | rand_filename="random_files/$size"
29 |
30 | website="twitter.com"
31 | port="443"
32 |
33 | gen_file
34 | nc -X connect -x 127.0.0.1:10500 $website $port -v < $rand_filename
35 |
--------------------------------------------------------------------------------
/test_scripts/seq.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 |
3 | import socket
4 | import time
5 | import struct
6 | import sys
7 | from optparse import OptionParser
8 |
9 |
10 |
11 |
12 | parser = OptionParser()
13 | parser.add_option("-p", "--port", dest="port", default=8888,
14 | help="port to listen on or connect to")
15 | parser.add_option("-c", "--connect", dest="connect",
16 | action="store_true", default=False, help="connect to remote host")
17 | parser.add_option("-H", "--host", dest="host",
18 | default="127.0.0.1", help="remote host to connect to or bind on")
19 | parser.add_option("-s", "--send", dest="send",
20 | action="store_true", default=False, help="send data")
21 |
22 | (options, args) = parser.parse_args()
23 |
24 | connect = options.connect
25 | send = options.send
26 | port = int(options.port)
27 | host = options.host
28 |
29 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
30 |
31 | conn = s
32 |
33 | if connect:
34 | s.connect((host, port))
35 | print 'connected'
36 | else:
37 | s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
38 | s.bind((host, port))
39 | s.listen(1)
40 | conn, addr = s.accept()
41 | print 'Connection from', addr
42 |
43 |
44 |
45 | last = time.time()
46 | last_n = 0
47 | last_buf = ''
48 | extra = ''
49 | #s.send('HELLO')
50 |
51 | last_time = time.time()
52 |
53 | def occasional_print(n):
54 | global last_time
55 | if (n % (256*1024)) == 0:
56 | now = time.time()
57 | bw = (256*1024*4)/(now - last_time)
58 | print '%.06f ---> %08x %.3f MB/s' % (now, n, bw/1000000)
59 | last_time = now
60 |
61 |
62 | BUF_SIZE = 1024
63 |
64 | if send:
65 | ##### SEND
66 | n = 0
67 | for i in xrange(1024*20*1024/(BUF_SIZE/4)):
68 | buf = ''
69 | for j in xrange(BUF_SIZE/4):
70 | n += 1
71 | buf += struct.pack('!I', n)
72 |
73 | occasional_print(n)
74 |
75 | conn.sendall(buf)
76 |
77 | else:
78 | ##### RECEIVE
79 | extra = ''
80 | last_buf = ''
81 | last_n = 0
82 | while True:
83 | buf = conn.recv(BUF_SIZE)
84 | buf = extra + buf
85 | for i in xrange(len(buf)/4):
86 | n, = struct.unpack('!I', buf[4*i:4*i+4])
87 | if n != (last_n + 1) and n != 0:
88 | print '=========ERROR: expected %08x got %08x at offset %d (len %d)' % (last_n+1, n, 4*i, len(buf))
89 | print ''
90 | print '----last buf:'
91 | print last_buf.encode('hex')
92 | print '----this buf:'
93 | print buf.encode('hex')
94 | sys.exit(1)
95 | last_n = n
96 |
97 | occasional_print(n)
98 | last_buf = buf
99 | extra = ''
100 | if (len(buf)%4) != 0:
101 | extra = buf[-(len(buf) % 4):]
102 |
--------------------------------------------------------------------------------
/test_scripts/ssh-td.sh:
--------------------------------------------------------------------------------
1 | print_usage() {
2 | echo 'Usage: ./ip-td.sh ${HOSTNAME}'
3 | }
4 |
5 | if [[ $# -eq 0 ]] ; then
6 | echo 'Error: hostname is not specified!'
7 | print_usage
8 | exit 1
9 | fi
10 |
11 | HOSTNAME="$1"
12 |
13 | ssh ${HOSTNAME} -o "ProxyCommand=nc -X connect -x localhost:10500 %h %p"
14 |
--------------------------------------------------------------------------------
/test_scripts/twitter_wget.sh:
--------------------------------------------------------------------------------
1 | export https_proxy=127.0.0.1:10500
2 | export http_proxy=127.0.0.1:10500
3 | #wget https://www.twitter.com
4 | wget https://twitter.com
5 |
--------------------------------------------------------------------------------
/tools/clientconf.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/binary"
5 | "encoding/hex"
6 | "flag"
7 | "fmt"
8 | "github.com/golang/protobuf/proto"
9 | pb "github.com/sergeyfrolov/gotapdance/protobuf"
10 | "io/ioutil"
11 | "log"
12 | "net"
13 | )
14 |
15 | func printClientConf(clientConf pb.ClientConf) {
16 | fmt.Printf("Generation: %d\n", clientConf.GetGeneration())
17 | if clientConf.GetDefaultPubkey() != nil {
18 | fmt.Printf("Default Pubkey: %s\n", hex.EncodeToString(clientConf.GetDefaultPubkey().Key[:]))
19 | }
20 | if clientConf.DecoyList == nil {
21 | return
22 | }
23 | decoys := clientConf.DecoyList.TlsDecoys
24 | fmt.Printf("Decoy List: %d decoys\n", len(decoys))
25 | for i, decoy := range decoys {
26 | ip := make(net.IP, 4)
27 | binary.BigEndian.PutUint32(ip, decoy.GetIpv4Addr())
28 | fmt.Printf("%d:\n %s (%s)\n", i, decoy.GetHostname(), ip.To4().String())
29 | if decoy.GetPubkey() != nil {
30 | fmt.Printf(" pubkey: %s\n", hex.EncodeToString(decoy.GetPubkey().Key[:]))
31 | }
32 | if decoy.GetTimeout() != 0 {
33 | fmt.Printf(" timeout: %d ms\n", decoy.GetTimeout())
34 | }
35 | if decoy.GetTcpwin() != 0 {
36 | fmt.Printf(" tcpwin: %d bytes\n", decoy.GetTcpwin())
37 | }
38 | }
39 |
40 | }
41 |
42 | func parseClientConf(fname string) pb.ClientConf {
43 |
44 | clientConf := pb.ClientConf{}
45 | buf, err := ioutil.ReadFile(fname)
46 | if err != nil {
47 | log.Fatal("Error reading file:", err)
48 | }
49 | err = proto.Unmarshal(buf, &clientConf)
50 | if err != nil {
51 | log.Fatal("Error parsing ClientConf", err)
52 | }
53 | return clientConf
54 | }
55 |
56 | func parsePubkey(pubkey string) []byte {
57 | pubkey_bin, err := hex.DecodeString(pubkey)
58 | if err != nil {
59 | log.Fatal("Error parsing pubkey:", err)
60 | }
61 | if len(pubkey_bin) != 32 {
62 | log.Fatal("Error: pubkey length: expected 32, got ", len(pubkey_bin))
63 | }
64 | return pubkey_bin
65 |
66 | }
67 |
68 | func updateDecoy(decoy *pb.TLSDecoySpec, host string, ip string, pubkey string, delpubkey bool, timeout int, tcpwin int) {
69 |
70 | if host != "" {
71 | decoy.Hostname = &host
72 | }
73 | if ip != "" {
74 | ip4 := binary.BigEndian.Uint32(net.ParseIP(ip).To4())
75 | decoy.Ipv4Addr = &ip4
76 | }
77 | if pubkey != "" {
78 | decoy.Pubkey.Key = parsePubkey(pubkey)
79 | }
80 | if delpubkey {
81 | decoy.Pubkey = nil
82 | }
83 | if timeout != 0 {
84 | t := uint32(timeout)
85 | decoy.Timeout = &t
86 | }
87 | if tcpwin != 0 {
88 | t := uint32(tcpwin)
89 | decoy.Tcpwin = &t
90 | }
91 | }
92 |
93 | func main() {
94 | var fname = flag.String("f", "", "`ClientConf` file to parse")
95 | var out_fname = flag.String("o", "", "`output` file name to write new/modified config")
96 | var generation = flag.Int("generation", 0, "New/modified generation")
97 | var pubkey = flag.String("pubkey", "", "New/modified (decoy) pubkey. If -add or -update, applies to specific decoy. If -all applies to all decoys. Otherwise, applies to default pubkey.")
98 | var delpubkey = flag.Bool("delpubkey", false, "Delete pubkey from decoy with index specified in -update (or from all decoys if -all)")
99 |
100 | var add = flag.Bool("add", false, "If set, modify fields of all decoys in list with provided pubkey/timeout/tcpwin/host/ip")
101 | var delete = flag.Int("delete", -1, "Specifies `index` of decoy to delete")
102 | var update = flag.Int("update", -1, "Specifies `index` of decoy to update")
103 |
104 | var host = flag.String("host", "", "New/modified decoy host")
105 | var ip = flag.String("ip", "", "New/modified IP address")
106 | var timeout = flag.Int("timeout", 0, "New/modified timeout")
107 | var tcpwin = flag.Int("tcpwin", 0, "New/modified tcpwin")
108 |
109 | var all = flag.Bool("all", false, "If set, replace all pubkeys/timeouts/tcpwins in decoy list with pubkey/timeout/tcpwin if provided")
110 |
111 | var noout = flag.Bool("noout", false, "Don't print ClientConf")
112 | flag.Parse()
113 |
114 | clientConf := pb.ClientConf{}
115 |
116 | // Parse ClientConf
117 | if *fname != "" {
118 | clientConf = parseClientConf(*fname)
119 | }
120 |
121 | // Update generation
122 | if *generation != 0 {
123 | gen := uint32(*generation)
124 | clientConf.Generation = &gen
125 | }
126 |
127 | // Update pubkey
128 | if *pubkey != "" {
129 | if *add || *update != -1 {
130 | // Skip. -add or -delete will use pubkey
131 |
132 | } else {
133 | // Update default public key
134 | if clientConf.DefaultPubkey == nil {
135 | k := pb.PubKey{}
136 | key_type := pb.KeyType_AES_GCM_128
137 | k.Type = &key_type
138 | clientConf.DefaultPubkey = &k
139 | }
140 | clientConf.DefaultPubkey.Key = parsePubkey(*pubkey)
141 | }
142 | }
143 |
144 | // Update all decoys
145 | if *all {
146 | for _, decoy := range clientConf.DecoyList.TlsDecoys {
147 | updateDecoy(decoy, *host, *ip, *pubkey, *delpubkey, *timeout, *tcpwin)
148 | }
149 | }
150 |
151 | // Update a single decoy from the list
152 | if *update != -1 {
153 | decoy := clientConf.DecoyList.TlsDecoys[*update]
154 | updateDecoy(decoy, *host, *ip, *pubkey, *delpubkey, *timeout, *tcpwin)
155 | }
156 |
157 | // Delete a decoy
158 | if *delete != -1 {
159 | idx := *delete
160 | decoys := clientConf.DecoyList.TlsDecoys
161 | clientConf.DecoyList.TlsDecoys = append(decoys[:idx], decoys[idx+1:]...)
162 | }
163 |
164 | // Add a decoy
165 | if *add {
166 | if *host == "" || *ip == "" {
167 | log.Fatal("Error: -add requires -host and -ip")
168 | }
169 | if *update != -1 {
170 | log.Fatal("Error: -add cannot be used with -update")
171 | }
172 |
173 | decoy := pb.TLSDecoySpec{}
174 | updateDecoy(&decoy, *host, *ip, *pubkey, *delpubkey, *timeout, *tcpwin)
175 |
176 | if clientConf.DecoyList == nil {
177 | tls_spec := pb.DecoyList{}
178 | clientConf.DecoyList = &tls_spec
179 | }
180 | clientConf.DecoyList.TlsDecoys = append(clientConf.DecoyList.TlsDecoys, &decoy)
181 | }
182 |
183 | if !*noout {
184 | printClientConf(clientConf)
185 | }
186 |
187 | if *out_fname != "" {
188 | buf, err := proto.Marshal(&clientConf)
189 | if err != nil {
190 | log.Fatal("Error writing output:", err)
191 | }
192 | err = ioutil.WriteFile(*out_fname, buf[:], 0644)
193 | if err != nil {
194 | log.Fatal("Error writing output:", err)
195 | }
196 | }
197 | }
198 |
--------------------------------------------------------------------------------