├── .editorconfig
├── .github
├── renovate.json
└── workflows
│ └── CI.yaml
├── .gitignore
├── Directory.Build.props
├── Directory.Packages.props
├── LICENSE
├── NatTypeTester.slnx
├── NuGet.Config
├── README.md
├── docs
└── img
│ ├── RFC3489.png
│ ├── RFC5780_4.2.png
│ ├── RFC5780_4.3.png
│ ├── RFC5780_4.4.png
│ └── RFC5780_4.5.png
├── scripts
├── DotNetDllPathPatcher.ps1
└── build.ps1
└── src
├── NatTypeTester.Models
├── Config.cs
├── NatTypeTester.Models.csproj
└── NatTypeTesterModelsModule.cs
├── NatTypeTester.ViewModels
├── MainWindowViewModel.cs
├── NatTypeTester.ViewModels.csproj
├── NatTypeTesterViewModelModule.cs
├── RFC3489ViewModel.cs
├── RFC5780ViewModel.cs
├── SettingViewModel.cs
├── ValueConverters
│ └── StringToIPEndpointTypeConverter.cs
└── ViewModelBase.cs
├── NatTypeTester
├── App.xaml
├── App.xaml.cs
├── Dialogs
│ └── DisposableContentDialog.cs
├── MainWindow.xaml
├── MainWindow.xaml.cs
├── NatTypeTester.csproj
├── NatTypeTesterModule.cs
├── Properties
│ └── DesignTimeResources.xaml
├── Utils
│ └── Extensions.cs
├── Views
│ ├── RFC3489View.xaml
│ ├── RFC3489View.xaml.cs
│ ├── RFC5780View.xaml
│ ├── RFC5780View.xaml.cs
│ ├── SettingView.xaml
│ └── SettingView.xaml.cs
├── app.manifest
└── icon.ico
├── STUN
├── Client
│ ├── IStunClient.cs
│ ├── IStunClient5389.cs
│ ├── IUdpStunClient.cs
│ ├── StunClient3489.cs
│ ├── StunClient5389TCP.cs
│ └── StunClient5389UDP.cs
├── Enums
│ ├── AttributeType.cs
│ ├── BindingTestResult.cs
│ ├── Class.cs
│ ├── FilteringBehavior.cs
│ ├── IpFamily.cs
│ ├── MappingBehavior.cs
│ ├── Method.cs
│ ├── NatType.cs
│ ├── ProxyType.cs
│ ├── StunMessageType.cs
│ └── TransportType.cs
├── HostnameEndpoint.cs
├── Messages
│ ├── StunAttribute.cs
│ ├── StunAttributeValues
│ │ ├── AddressStunAttributeValue.cs
│ │ ├── ChangeRequestStunAttributeValue.cs
│ │ ├── ChangedAddressStunAttributeValue.cs
│ │ ├── ErrorCodeStunAttributeValue.cs
│ │ ├── IStunAttributeValue.cs
│ │ ├── MappedAddressStunAttributeValue.cs
│ │ ├── OtherAddressStunAttributeValue.cs
│ │ ├── ReflectedFromStunAttributeValue.cs
│ │ ├── ResponseAddressStunAttributeValue.cs
│ │ ├── SourceAddressStunAttributeValue.cs
│ │ ├── UnknownStunAttributeValue.cs
│ │ ├── UselessStunAttributeValue.cs
│ │ └── XorMappedAddressStunAttributeValue.cs
│ ├── StunMessage5389.cs
│ └── StunResponse.cs
├── Proxy
│ ├── DirectTcpProxy.cs
│ ├── ITcpProxy.cs
│ ├── IUdpProxy.cs
│ ├── NoneUdpProxy.cs
│ ├── ProxyFactory.cs
│ ├── Socks5TcpProxy.cs
│ ├── Socks5UdpProxy.cs
│ ├── TlsOverSocks5Proxy.cs
│ └── TlsProxy.cs
├── STUN.csproj
├── StunResult
│ ├── ClassicStunResult.cs
│ ├── StunResult.cs
│ └── StunResult5389.cs
├── StunServer.cs
└── Utils
│ └── AttributeExtensions.cs
└── tests
└── UnitTest
├── HostnameEndpointTest.cs
├── StunClien5389UDPTest.cs
├── StunClient3489Test.cs
├── StunClient5389TCPTest.cs
├── UnitTest.csproj
└── XorMappedTest.cs
/.editorconfig:
--------------------------------------------------------------------------------
1 | # 如果要从更高级别的目录继承 .editorconfig 设置,请删除以下行
2 | root = true
3 |
4 | [*]
5 | # 字符集
6 | charset = utf-8
7 |
8 | # 新行首选项
9 | end_of_line = lf
10 | insert_final_newline = true
11 |
12 | # ReSharper properties
13 | resharper_blank_lines_around_single_line_auto_property = 1
14 | resharper_blank_lines_before_block_statements = 1
15 | resharper_braces_for_ifelse = not_required
16 | resharper_braces_redundant = false
17 | resharper_csharp_alignment_tab_fill_style = use_tabs_only
18 | resharper_csharp_indent_style = tab
19 | resharper_csharp_insert_final_newline = true
20 | resharper_csharp_keep_existing_enum_arrangement = false
21 | resharper_csharp_space_before_trailing_comment = false
22 | resharper_csharp_wrap_arguments_style = chop_if_long
23 | resharper_csharp_wrap_lines = false
24 | resharper_for_simple_types = use_explicit_type
25 | resharper_fsharp_insert_final_newline = false
26 | resharper_html_insert_final_newline = false
27 | resharper_instance_members_qualify_declared_in =
28 | resharper_keep_existing_initializer_arrangement = false
29 | resharper_max_initializer_elements_on_line = 1
30 | resharper_place_accessorholder_attribute_on_same_line = false
31 | resharper_place_expr_property_on_single_line = true
32 | resharper_place_field_attribute_on_same_line = false
33 | resharper_resx_insert_final_newline = false
34 | resharper_shaderlab_insert_final_newline = false
35 | resharper_space_within_single_line_array_initializer_braces = true
36 | resharper_t4_insert_final_newline = false
37 | resharper_vb_insert_final_newline = false
38 | resharper_wrap_object_and_collection_initializer_style = wrap_if_long
39 | resharper_xmldoc_indent_text = ZeroIndent
40 | resharper_xmldoc_insert_final_newline = false
41 | resharper_xml_insert_final_newline = false
42 |
43 | [*.csproj]
44 | indent_size = 2
45 |
46 | [*.props]
47 | indent_size = 2
48 |
49 | # c# 文件
50 | [*.cs]
51 |
52 | # 缩进和间距
53 | indent_size = 4
54 | indent_style = tab
55 | tab_width = 4
56 |
57 | #### .NET 编码约定 ####
58 |
59 | # 组织 Using
60 | dotnet_separate_import_directive_groups = false
61 | dotnet_sort_system_directives_first = false
62 | file_header_template = unset
63 |
64 | # this. 和 Me. 首选项
65 | dotnet_style_qualification_for_event = false:suggestion
66 | dotnet_style_qualification_for_field = false
67 | dotnet_style_qualification_for_method = false:suggestion
68 | dotnet_style_qualification_for_property = false:suggestion
69 |
70 | # 语言关键字与 bcl 类型首选项
71 | dotnet_style_predefined_type_for_locals_parameters_members = true:warning
72 | dotnet_style_predefined_type_for_member_access = true:warning
73 |
74 | # 括号首选项
75 | dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary
76 | dotnet_style_parentheses_in_other_binary_operators = never_if_unnecessary
77 | dotnet_style_parentheses_in_other_operators = never_if_unnecessary:warning
78 | dotnet_style_parentheses_in_relational_binary_operators = never_if_unnecessary
79 |
80 | # 修饰符首选项
81 | dotnet_style_require_accessibility_modifiers = for_non_interface_members
82 |
83 | # 表达式级首选项
84 | dotnet_style_coalesce_expression = true:warning
85 | dotnet_style_collection_initializer = true
86 | dotnet_style_explicit_tuple_names = true:warning
87 | dotnet_style_namespace_match_folder = true
88 | dotnet_style_null_propagation = true:warning
89 | dotnet_style_object_initializer = true
90 | dotnet_style_operator_placement_when_wrapping = beginning_of_line
91 | dotnet_style_prefer_auto_properties = true:warning
92 | dotnet_style_prefer_compound_assignment = true:warning
93 | dotnet_style_prefer_conditional_expression_over_assignment = true
94 | dotnet_style_prefer_conditional_expression_over_return = true
95 | dotnet_style_prefer_inferred_anonymous_type_member_names = true
96 | dotnet_style_prefer_inferred_tuple_names = true
97 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true:warning
98 | dotnet_style_prefer_simplified_boolean_expressions = true:warning
99 | dotnet_style_prefer_simplified_interpolation = true
100 |
101 | # 字段首选项
102 | dotnet_style_readonly_field = true
103 |
104 | # 参数首选项
105 | dotnet_code_quality_unused_parameters = all
106 |
107 | # 禁止显示首选项
108 | dotnet_remove_unnecessary_suppression_exclusions = 0
109 |
110 | # 新行首选项
111 | dotnet_style_allow_multiple_blank_lines_experimental = false
112 | dotnet_style_allow_statement_immediately_after_block_experimental = true
113 |
114 | #### c# 编码约定 ####
115 |
116 | # var 首选项
117 | csharp_style_var_elsewhere = false:suggestion
118 | csharp_style_var_for_built_in_types = false:suggestion
119 | csharp_style_var_when_type_is_apparent = false:suggestion
120 |
121 | # Expression-bodied 成员
122 | csharp_style_expression_bodied_accessors = true:suggestion
123 | csharp_style_expression_bodied_constructors = false:warning
124 | csharp_style_expression_bodied_indexers = true:suggestion
125 | csharp_style_expression_bodied_lambdas = true:suggestion
126 | csharp_style_expression_bodied_local_functions = false:warning
127 | csharp_style_expression_bodied_methods = false:warning
128 | csharp_style_expression_bodied_operators = false:warning
129 | csharp_style_expression_bodied_properties = true:suggestion
130 |
131 | # 模式匹配首选项
132 | csharp_style_pattern_matching_over_as_with_null_check = true:warning
133 | csharp_style_pattern_matching_over_is_with_cast_check = true:warning
134 | csharp_style_prefer_not_pattern = true:warning
135 | csharp_style_prefer_pattern_matching = true:warning
136 | csharp_style_prefer_switch_expression = true:warning
137 |
138 | # Null 检查首选项
139 | csharp_style_conditional_delegate_call = true
140 |
141 | # 修饰符首选项
142 | csharp_prefer_static_local_function = true
143 | csharp_preferred_modifier_order = public, private, protected, internal, static, extern, new, virtual, abstract, sealed, override, readonly, unsafe, volatile, async
144 |
145 | # 代码块首选项
146 | csharp_prefer_braces = true:suggestion
147 | csharp_prefer_simple_using_statement = true:warning
148 | csharp_style_namespace_declarations = file_scoped:warning
149 |
150 | # 表达式级首选项
151 | csharp_prefer_simple_default_expression = true:warning
152 | csharp_style_deconstructed_variable_declaration = true
153 | csharp_style_implicit_object_creation_when_type_is_apparent = true:warning
154 | csharp_style_inlined_variable_declaration = true:warning
155 | csharp_style_pattern_local_over_anonymous_function = true
156 | csharp_style_prefer_index_operator = true:warning
157 | csharp_style_prefer_null_check_over_type_check = true:warning
158 | csharp_style_prefer_range_operator = false:none
159 | csharp_style_throw_expression = true
160 | csharp_style_unused_value_assignment_preference = discard_variable
161 | csharp_style_unused_value_expression_statement_preference = discard_variable
162 |
163 | # "using" 指令首选项
164 | csharp_using_directive_placement = outside_namespace:warning
165 |
166 | # 新行首选项
167 | csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true
168 | csharp_style_allow_blank_lines_between_consecutive_braces_experimental = false
169 | csharp_style_allow_embedded_statements_on_same_line_experimental = true
170 |
171 | #### C# 格式规则 ####
172 |
173 | # 新行首选项
174 | csharp_new_line_before_catch = true
175 | csharp_new_line_before_else = true
176 | csharp_new_line_before_finally = true
177 | csharp_new_line_before_members_in_anonymous_types = true
178 | csharp_new_line_before_members_in_object_initializers = true
179 | csharp_new_line_before_open_brace = all
180 | csharp_new_line_between_query_expression_clauses = true
181 |
182 | # 缩进首选项
183 | csharp_indent_block_contents = true
184 | csharp_indent_braces = false
185 | csharp_indent_case_contents = true
186 | csharp_indent_case_contents_when_block = false
187 | csharp_indent_labels = one_less_than_current
188 | csharp_indent_switch_labels = true
189 |
190 | # 空格键首选项
191 | csharp_space_after_cast = false
192 | csharp_space_after_colon_in_inheritance_clause = true
193 | csharp_space_after_comma = true
194 | csharp_space_after_dot = false
195 | csharp_space_after_keywords_in_control_flow_statements = true
196 | csharp_space_after_semicolon_in_for_statement = true
197 | csharp_space_around_binary_operators = before_and_after
198 | csharp_space_around_declaration_statements = false
199 | csharp_space_before_colon_in_inheritance_clause = true
200 | csharp_space_before_comma = false
201 | csharp_space_before_dot = false
202 | csharp_space_before_open_square_brackets = false
203 | csharp_space_before_semicolon_in_for_statement = false
204 | csharp_space_between_empty_square_brackets = false
205 | csharp_space_between_method_call_empty_parameter_list_parentheses = false
206 | csharp_space_between_method_call_name_and_opening_parenthesis = false
207 | csharp_space_between_method_call_parameter_list_parentheses = false
208 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
209 | csharp_space_between_method_declaration_name_and_open_parenthesis = false
210 | csharp_space_between_method_declaration_parameter_list_parentheses = false
211 | csharp_space_between_parentheses = false
212 | csharp_space_between_square_brackets = false
213 |
214 | # 包装首选项
215 | csharp_preserve_single_line_blocks = true
216 | csharp_preserve_single_line_statements = false
217 |
218 | #### 命名样式 ####
219 |
220 | # 命名规则
221 |
222 | dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion
223 | dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
224 | dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
225 |
226 | dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion
227 | dotnet_naming_rule.types_should_be_pascal_case.symbols = types
228 | dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
229 |
230 | dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion
231 | dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
232 | dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
233 |
234 | # 符号规范
235 |
236 | dotnet_naming_symbols.interface.applicable_kinds = interface
237 | dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
238 | dotnet_naming_symbols.interface.required_modifiers =
239 |
240 | dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
241 | dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
242 | dotnet_naming_symbols.types.required_modifiers =
243 |
244 | dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
245 | dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
246 | dotnet_naming_symbols.non_field_members.required_modifiers =
247 |
248 | # 命名样式
249 |
250 | dotnet_naming_style.pascal_case.required_prefix =
251 | dotnet_naming_style.pascal_case.required_suffix =
252 | dotnet_naming_style.pascal_case.word_separator =
253 | dotnet_naming_style.pascal_case.capitalization = pascal_case
254 |
255 | dotnet_naming_style.begins_with_i.required_prefix = I
256 | dotnet_naming_style.begins_with_i.required_suffix =
257 | dotnet_naming_style.begins_with_i.word_separator =
258 | dotnet_naming_style.begins_with_i.capitalization = pascal_case
259 |
--------------------------------------------------------------------------------
/.github/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "assignees": [
4 | "HMBSbige"
5 | ],
6 | "dependencyDashboard": false,
7 | "extends": [
8 | "config:recommended",
9 | ":configMigration",
10 | ":automergeBranch",
11 | ":automergeDigest",
12 | ":automergeMinor",
13 | ":disableRateLimiting"
14 | ],
15 | "packageRules": [
16 | {
17 | "matchSourceUrls": [
18 | "https://github.com/abpframework/abp"
19 | ],
20 | "groupName": "abp"
21 | },
22 | {
23 | "matchSourceUrls": [
24 | "https://github.com/reactiveui/ReactiveUI"
25 | ],
26 | "groupName": "ReactiveUI"
27 | }
28 | ],
29 | "labels": [
30 | "Automatic"
31 | ]
32 | }
--------------------------------------------------------------------------------
/.github/workflows/CI.yaml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on:
3 | - push
4 | - pull_request
5 | env:
6 | ProjectName: ${{ github.event.repository.name }}
7 | NET_TFM: net8.0-windows10.0.22621.0
8 | Configuration: Release
9 |
10 | jobs:
11 | check_format:
12 | name: Check format
13 | runs-on: ubuntu-latest
14 | steps:
15 | - uses: actions/checkout@v4
16 | - uses: actions/setup-dotnet@v4
17 | with:
18 | dotnet-version: 9.0.x
19 | - run: dotnet format -v diag --verify-no-changes
20 |
21 | test:
22 | name: Run tests
23 | runs-on: ${{ matrix.os }}
24 | strategy:
25 | matrix:
26 | os:
27 | - windows-latest
28 | - ubuntu-latest
29 | - macos-latest
30 |
31 | steps:
32 | - uses: actions/checkout@v4
33 | - uses: actions/setup-dotnet@v4
34 | with:
35 | dotnet-version: 9.0.x
36 | - run: dotnet test -c Release
37 |
38 | build:
39 | needs: [test, check_format]
40 | runs-on: windows-latest
41 | steps:
42 | - uses: actions/checkout@v4
43 | - uses: actions/setup-dotnet@v4
44 | with:
45 | dotnet-version: 9.0.x
46 |
47 | - name: Build
48 | shell: pwsh
49 | run: |
50 | .\scripts\build.ps1
51 |
52 | - name: Upload
53 | uses: actions/upload-artifact@v4
54 | with:
55 | name: ${{ env.ProjectName }}
56 | path: src\${{ env.ProjectName }}\bin\${{ env.Configuration }}\${{ env.NET_TFM }}\generic\publish\
57 |
58 | nuget:
59 | needs: [test, check_format]
60 | if: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') }}
61 | runs-on: ubuntu-latest
62 | permissions:
63 | packages: write
64 | strategy:
65 | matrix:
66 | PackageName:
67 | - STUN
68 |
69 | steps:
70 | - uses: actions/checkout@v4
71 | - uses: actions/setup-dotnet@v4
72 | with:
73 | dotnet-version: 9.0.x
74 |
75 | - name: Build
76 | working-directory: src/${{ matrix.PackageName }}
77 | run: dotnet pack
78 |
79 | - name: Push nuget packages
80 | working-directory: src/${{ matrix.PackageName }}/bin/Release
81 | run: |
82 | dotnet nuget push *.nupkg -s https://nuget.pkg.github.com/HMBSbige -k ${{ secrets.GITHUB_TOKEN }} --skip-duplicate
83 | dotnet nuget push *.nupkg -s https://api.nuget.org/v3/index.json -k ${{ secrets.NuGetAPIKey }} --skip-duplicate
84 |
85 | release:
86 | needs: [build, nuget]
87 | runs-on: ubuntu-latest
88 | permissions:
89 | contents: write
90 |
91 | steps:
92 | - uses: actions/download-artifact@v4
93 | with:
94 | name: ${{ env.ProjectName }}
95 | path: ${{ env.ProjectName }}
96 |
97 | - name: Package
98 | shell: pwsh
99 | run: |
100 | New-Item -ItemType Directory -Path builtfiles -Force > $null
101 | $zip_path = "builtfiles/$env:ProjectName-${{ github.ref_name }}.7z"
102 | 7z a -mx9 "$zip_path" ${{ env.ProjectName }}
103 | echo "GENERIC_SHA256=$((Get-FileHash $zip_path -Algorithm SHA256).Hash)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
104 |
105 | - name: Create a new GitHub release
106 | uses: ncipollo/release-action@v1
107 | with:
108 | token: ${{ secrets.GITHUB_TOKEN }}
109 | prerelease: true
110 | draft: false
111 | artifacts: builtfiles/*
112 | body: |
113 | ## Hash
114 | | Filename | SHA-256 |
115 | | :- | :- |
116 | | ${{ env.ProjectName }}-${{ github.ref_name }}.7z | ${{ env.GENERIC_SHA256 }} |
117 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
5 |
6 | # User-specific files
7 | *.rsuser
8 | *.suo
9 | *.user
10 | *.userosscache
11 | *.sln.docstates
12 |
13 | # User-specific files (MonoDevelop/Xamarin Studio)
14 | *.userprefs
15 |
16 | # Mono auto generated files
17 | mono_crash.*
18 |
19 | # Build results
20 | [Dd]ebug/
21 | [Dd]ebugPublic/
22 | [Rr]elease/
23 | [Rr]eleases/
24 | x64/
25 | x86/
26 | [Ww][Ii][Nn]32/
27 | [Aa][Rr][Mm]/
28 | [Aa][Rr][Mm]64/
29 | bld/
30 | [Bb]in/
31 | [Oo]bj/
32 | [Ll]og/
33 | [Ll]ogs/
34 |
35 | # Visual Studio 2015/2017 cache/options directory
36 | .vs/
37 | # Uncomment if you have tasks that create the project's static files in wwwroot
38 | #wwwroot/
39 |
40 | # Visual Studio 2017 auto generated files
41 | Generated\ Files/
42 |
43 | # MSTest test Results
44 | [Tt]est[Rr]esult*/
45 | [Bb]uild[Ll]og.*
46 |
47 | # NUnit
48 | *.VisualState.xml
49 | TestResult.xml
50 | nunit-*.xml
51 |
52 | # Build Results of an ATL Project
53 | [Dd]ebugPS/
54 | [Rr]eleasePS/
55 | dlldata.c
56 |
57 | # Benchmark Results
58 | BenchmarkDotNet.Artifacts/
59 |
60 | # .NET Core
61 | project.lock.json
62 | project.fragment.lock.json
63 | artifacts/
64 |
65 | # ASP.NET Scaffolding
66 | ScaffoldingReadMe.txt
67 |
68 | # StyleCop
69 | StyleCopReport.xml
70 |
71 | # Files built by Visual Studio
72 | *_i.c
73 | *_p.c
74 | *_h.h
75 | *.ilk
76 | *.meta
77 | *.obj
78 | *.iobj
79 | *.pch
80 | *.pdb
81 | *.ipdb
82 | *.pgc
83 | *.pgd
84 | *.rsp
85 | *.sbr
86 | *.tlb
87 | *.tli
88 | *.tlh
89 | *.tmp
90 | *.tmp_proj
91 | *_wpftmp.csproj
92 | *.log
93 | *.vspscc
94 | *.vssscc
95 | .builds
96 | *.pidb
97 | *.svclog
98 | *.scc
99 |
100 | # Chutzpah Test files
101 | _Chutzpah*
102 |
103 | # Visual C++ cache files
104 | ipch/
105 | *.aps
106 | *.ncb
107 | *.opendb
108 | *.opensdf
109 | *.sdf
110 | *.cachefile
111 | *.VC.db
112 | *.VC.VC.opendb
113 |
114 | # Visual Studio profiler
115 | *.psess
116 | *.vsp
117 | *.vspx
118 | *.sap
119 |
120 | # Visual Studio Trace Files
121 | *.e2e
122 |
123 | # TFS 2012 Local Workspace
124 | $tf/
125 |
126 | # Guidance Automation Toolkit
127 | *.gpState
128 |
129 | # ReSharper is a .NET coding add-in
130 | _ReSharper*/
131 | *.[Rr]e[Ss]harper
132 | *.DotSettings.user
133 |
134 | # TeamCity is a build add-in
135 | _TeamCity*
136 |
137 | # DotCover is a Code Coverage Tool
138 | *.dotCover
139 |
140 | # AxoCover is a Code Coverage Tool
141 | .axoCover/*
142 | !.axoCover/settings.json
143 |
144 | # Coverlet is a free, cross platform Code Coverage Tool
145 | coverage*.json
146 | coverage*.xml
147 | coverage*.info
148 |
149 | # Visual Studio code coverage results
150 | *.coverage
151 | *.coveragexml
152 |
153 | # NCrunch
154 | _NCrunch_*
155 | .*crunch*.local.xml
156 | nCrunchTemp_*
157 |
158 | # MightyMoose
159 | *.mm.*
160 | AutoTest.Net/
161 |
162 | # Web workbench (sass)
163 | .sass-cache/
164 |
165 | # Installshield output folder
166 | [Ee]xpress/
167 |
168 | # DocProject is a documentation generator add-in
169 | DocProject/buildhelp/
170 | DocProject/Help/*.HxT
171 | DocProject/Help/*.HxC
172 | DocProject/Help/*.hhc
173 | DocProject/Help/*.hhk
174 | DocProject/Help/*.hhp
175 | DocProject/Help/Html2
176 | DocProject/Help/html
177 |
178 | # Click-Once directory
179 | publish/
180 |
181 | # Publish Web Output
182 | *.[Pp]ublish.xml
183 | *.azurePubxml
184 | # Note: Comment the next line if you want to checkin your web deploy settings,
185 | # but database connection strings (with potential passwords) will be unencrypted
186 | *.pubxml
187 | *.publishproj
188 |
189 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
190 | # checkin your Azure Web App publish settings, but sensitive information contained
191 | # in these scripts will be unencrypted
192 | PublishScripts/
193 |
194 | # NuGet Packages
195 | *.nupkg
196 | # NuGet Symbol Packages
197 | *.snupkg
198 | # The packages folder can be ignored because of Package Restore
199 | **/[Pp]ackages/*
200 | # except build/, which is used as an MSBuild target.
201 | !**/[Pp]ackages/build/
202 | # Uncomment if necessary however generally it will be regenerated when needed
203 | #!**/[Pp]ackages/repositories.config
204 | # NuGet v3's project.json files produces more ignorable files
205 | *.nuget.props
206 | *.nuget.targets
207 |
208 | # Microsoft Azure Build Output
209 | csx/
210 | *.build.csdef
211 |
212 | # Microsoft Azure Emulator
213 | ecf/
214 | rcf/
215 |
216 | # Windows Store app package directories and files
217 | AppPackages/
218 | BundleArtifacts/
219 | Package.StoreAssociation.xml
220 | _pkginfo.txt
221 | *.appx
222 | *.appxbundle
223 | *.appxupload
224 |
225 | # Visual Studio cache files
226 | # files ending in .cache can be ignored
227 | *.[Cc]ache
228 | # but keep track of directories ending in .cache
229 | !?*.[Cc]ache/
230 |
231 | # Others
232 | ClientBin/
233 | ~$*
234 | *~
235 | *.dbmdl
236 | *.dbproj.schemaview
237 | *.jfm
238 | *.pfx
239 | *.publishsettings
240 | orleans.codegen.cs
241 |
242 | # Including strong name files can present a security risk
243 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
244 | #*.snk
245 |
246 | # Since there are multiple workflows, uncomment next line to ignore bower_components
247 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
248 | #bower_components/
249 |
250 | # RIA/Silverlight projects
251 | Generated_Code/
252 |
253 | # Backup & report files from converting an old project file
254 | # to a newer Visual Studio version. Backup files are not needed,
255 | # because we have git ;-)
256 | _UpgradeReport_Files/
257 | Backup*/
258 | UpgradeLog*.XML
259 | UpgradeLog*.htm
260 | ServiceFabricBackup/
261 | *.rptproj.bak
262 |
263 | # SQL Server files
264 | *.mdf
265 | *.ldf
266 | *.ndf
267 |
268 | # Business Intelligence projects
269 | *.rdl.data
270 | *.bim.layout
271 | *.bim_*.settings
272 | *.rptproj.rsuser
273 | *- [Bb]ackup.rdl
274 | *- [Bb]ackup ([0-9]).rdl
275 | *- [Bb]ackup ([0-9][0-9]).rdl
276 |
277 | # Microsoft Fakes
278 | FakesAssemblies/
279 |
280 | # GhostDoc plugin setting file
281 | *.GhostDoc.xml
282 |
283 | # Node.js Tools for Visual Studio
284 | .ntvs_analysis.dat
285 | node_modules/
286 |
287 | # Visual Studio 6 build log
288 | *.plg
289 |
290 | # Visual Studio 6 workspace options file
291 | *.opt
292 |
293 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
294 | *.vbw
295 |
296 | # Visual Studio LightSwitch build output
297 | **/*.HTMLClient/GeneratedArtifacts
298 | **/*.DesktopClient/GeneratedArtifacts
299 | **/*.DesktopClient/ModelManifest.xml
300 | **/*.Server/GeneratedArtifacts
301 | **/*.Server/ModelManifest.xml
302 | _Pvt_Extensions
303 |
304 | # Paket dependency manager
305 | .paket/paket.exe
306 | paket-files/
307 |
308 | # FAKE - F# Make
309 | .fake/
310 |
311 | # CodeRush personal settings
312 | .cr/personal
313 |
314 | # Python Tools for Visual Studio (PTVS)
315 | __pycache__/
316 | *.pyc
317 |
318 | # Cake - Uncomment if you are using it
319 | # tools/**
320 | # !tools/packages.config
321 |
322 | # Tabs Studio
323 | *.tss
324 |
325 | # Telerik's JustMock configuration file
326 | *.jmconfig
327 |
328 | # BizTalk build output
329 | *.btp.cs
330 | *.btm.cs
331 | *.odx.cs
332 | *.xsd.cs
333 |
334 | # OpenCover UI analysis results
335 | OpenCover/
336 |
337 | # Azure Stream Analytics local run output
338 | ASALocalRun/
339 |
340 | # MSBuild Binary and Structured Log
341 | *.binlog
342 |
343 | # NVidia Nsight GPU debugger configuration file
344 | *.nvuser
345 |
346 | # MFractors (Xamarin productivity tool) working folder
347 | .mfractor/
348 |
349 | # Local History for Visual Studio
350 | .localhistory/
351 |
352 | # BeatPulse healthcheck temp database
353 | healthchecksdb
354 |
355 | # Backup folder for Package Reference Convert tool in Visual Studio 2017
356 | MigrationBackup/
357 |
358 | # Ionide (cross platform F# VS Code tools) working folder
359 | .ionide/
360 |
361 | # Fody - auto-generated XML schema
362 | FodyWeavers.xsd
363 |
364 | # JetBrains Rider
365 | .idea/
366 | *.sln.iml
367 |
--------------------------------------------------------------------------------
/Directory.Build.props:
--------------------------------------------------------------------------------
1 |
2 |
3 | net8.0
4 | enable
5 | latest
6 | enable
7 | HMBSbige
8 |
9 |
10 |
--------------------------------------------------------------------------------
/Directory.Packages.props:
--------------------------------------------------------------------------------
1 |
2 |
3 | true
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) Bruce Wayne
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/NatTypeTester.slnx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/NuGet.Config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # NatTypeTester
2 | Channel | Status
3 | -|-
4 | CI | [](https://github.com/HMBSbige/NatTypeTester/actions)
5 | Stun.Net | [](https://www.nuget.org/packages/Stun.Net/)
6 |
7 | ## RFC
8 |
9 | * [RFC 3489](https://datatracker.ietf.org/doc/html/rfc3489)
10 | * [RFC 5780](https://datatracker.ietf.org/doc/html/rfc5780)
11 | * [RFC 8489](https://datatracker.ietf.org/doc/html/rfc8489)
12 |
13 | ## Internet Protocol
14 |
15 | - [x] IPv4
16 | - [x] IPv6
17 |
18 | ## Transmission Protocol
19 |
20 | - [x] UDP
21 | - [x] TCP
22 | - [x] TLS-over-TCP
23 | - [ ] DTLS-over-UDP
24 |
25 | ## RFC3489
26 |
27 |
28 | 
29 |
30 |
31 | ## RFC5389
32 | ### Binding Test
33 |
34 | Checking for UDP Connectivity with the STUN Server
35 |
36 | 
37 |
38 |
39 | ### Mapping Behavior
40 |
41 | Determining NAT Mapping Behavior
42 |
43 | 
44 |
45 |
46 | ### Filtering Behavior
47 |
48 | Determining NAT Filtering Behavior
49 |
50 | 
51 |
52 |
53 | ### Combining Tests
54 |
55 |
56 | 
57 |
58 |
59 |
--------------------------------------------------------------------------------
/docs/img/RFC3489.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HMBSbige/NatTypeTester/b55bf4399984a2c7ee2512fe180d7cc96022b506/docs/img/RFC3489.png
--------------------------------------------------------------------------------
/docs/img/RFC5780_4.2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HMBSbige/NatTypeTester/b55bf4399984a2c7ee2512fe180d7cc96022b506/docs/img/RFC5780_4.2.png
--------------------------------------------------------------------------------
/docs/img/RFC5780_4.3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HMBSbige/NatTypeTester/b55bf4399984a2c7ee2512fe180d7cc96022b506/docs/img/RFC5780_4.3.png
--------------------------------------------------------------------------------
/docs/img/RFC5780_4.4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HMBSbige/NatTypeTester/b55bf4399984a2c7ee2512fe180d7cc96022b506/docs/img/RFC5780_4.4.png
--------------------------------------------------------------------------------
/docs/img/RFC5780_4.5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HMBSbige/NatTypeTester/b55bf4399984a2c7ee2512fe180d7cc96022b506/docs/img/RFC5780_4.5.png
--------------------------------------------------------------------------------
/scripts/DotNetDllPathPatcher.ps1:
--------------------------------------------------------------------------------
1 | using namespace System.IO
2 | using namespace System.Text
3 |
4 | param([string]$exe_path, [string]$target_path = 'bin')
5 | $ErrorActionPreference = 'Stop'
6 | #$DebugPreference = 'Continue'
7 |
8 | $exe_path = (Resolve-Path -Path $exe_path).Path
9 |
10 | Write-Host "Origin path: `"$exe_path`""
11 | Write-Host "Target dll path: $target_path"
12 |
13 | $separator = '\'
14 | $max_path_length = 1024
15 |
16 | $exe_name = [Path]::GetFileName($exe_path)
17 | $dll_name = [Path]::ChangeExtension($exe_name, '.dll')
18 | Write-Debug "exe: $exe_name"
19 | Write-Debug "dll: $dll_name"
20 |
21 | function Update-Exe {
22 | $old_bytes = [Encoding]::UTF8.GetBytes("$dll_name`0")
23 | if ($old_bytes.Count -gt $max_path_length) {
24 | throw [PathTooLongException] 'old dll path is too long'
25 | }
26 |
27 | $new_dll_path = "$target_path$separator$dll_name"
28 | $new_bytes = [Encoding]::UTF8.GetBytes("$new_dll_path`0")
29 | Write-Host "Dll path Change to `"$new_dll_path`""
30 | if ($new_bytes.Count -gt $max_path_length) {
31 | throw [PathTooLongException] 'new dll path is too long'
32 | }
33 |
34 | $bytes = [File]::ReadAllBytes($exe_path)
35 | $index = (Get-Content $exe_path -Raw -Encoding 28591).IndexOf("$dll_name`0")
36 | if ($index -lt 0) {
37 | throw [InvalidDataException] 'Could not find old dll path'
38 | }
39 | Write-Debug "Position: $index"
40 | $end_postion = $index + $($new_bytes.Count)
41 | $end_length = $bytes.Count - $end_postion
42 | if ($end_postion -gt $bytes.Count) {
43 | throw [PathTooLongException] 'new dll path is too long'
44 | }
45 | Write-Debug "End Position: $end_postion"
46 | Write-Debug "End Length: $end_length"
47 |
48 | $fs = [File]::OpenWrite($exe_path)
49 | try {
50 | $fs.Write($bytes, 0, $index)
51 | $fs.Write($new_bytes)
52 | $fs.Write($bytes, $end_postion, $end_length)
53 | }
54 | finally {
55 | $fs.Dispose();
56 | }
57 | }
58 |
59 | function Move-Dll {
60 | $tmpbin = 'tmpbin'
61 | $dir = [Path]::GetDirectoryName($exe_path);
62 | $root = [Path]::GetDirectoryName($dir);
63 | Write-Debug "root path: $root"
64 | Write-Debug "dir path: $dir"
65 |
66 | Rename-Item $dir $tmpbin
67 | New-Item -ItemType Directory $dir > $null
68 | Move-Item $root\$tmpbin $dir
69 | Rename-Item $dir\$tmpbin $target_path
70 | Move-Item $dir\$target_path\$exe_name $dir
71 | }
72 |
73 | Update-Exe
74 | Move-Dll
75 |
--------------------------------------------------------------------------------
/scripts/build.ps1:
--------------------------------------------------------------------------------
1 | $ErrorActionPreference = 'Stop'
2 |
3 | dotnet --info
4 |
5 | $proj = 'NatTypeTester'
6 | $exe = "$proj.exe"
7 | $net_tfm = 'net8.0-windows10.0.22621.0'
8 | $configuration = 'Release'
9 | $output_dir = "src\$proj\bin\$configuration"
10 | $proj_path = "src\$proj\$proj.csproj"
11 | $generic_outdir = "$output_dir\$net_tfm\generic"
12 |
13 | function Build-Generic {
14 | Write-Host 'Building generic'
15 |
16 | $outdir = $generic_outdir
17 | $publishDir = "$outdir\publish"
18 |
19 | Remove-Item $publishDir -Recurse -Force -Confirm:$false -ErrorAction Ignore
20 |
21 | dotnet publish -c $configuration -f $net_tfm $proj_path -o $publishDir
22 | if ($LASTEXITCODE) { exit $LASTEXITCODE }
23 |
24 | & "$PSScriptRoot\DotNetDllPathPatcher.ps1" "$publishDir\$exe" bin
25 | if ($LASTEXITCODE) { exit $LASTEXITCODE }
26 |
27 | Remove-Item "$publishDir\$exe"
28 | }
29 |
30 | function Build {
31 | param([string]$arch)
32 |
33 | $rid = "win-$arch"
34 | Write-Host "Building $rid"
35 |
36 | $outdir = "$output_dir\$net_tfm\$rid"
37 | $publishDir = "$outdir\publish"
38 |
39 | Remove-Item $publishDir -Recurse -Force -Confirm:$false -ErrorAction Ignore
40 |
41 | dotnet publish -c $configuration -f $net_tfm -r $rid --no-self-contained true $proj_path
42 | if ($LASTEXITCODE) { exit $LASTEXITCODE }
43 |
44 | & "$PSScriptRoot\DotNetDllPathPatcher.ps1" "$publishDir\$exe" bin
45 | if ($LASTEXITCODE) { exit $LASTEXITCODE }
46 |
47 | Move-Item "$publishDir\$exe" "$generic_outdir\publish\$proj-$arch.exe"
48 | }
49 |
50 | Build-Generic
51 | Build x64
52 | Build x86
53 | Build arm64
54 |
--------------------------------------------------------------------------------
/src/NatTypeTester.Models/Config.cs:
--------------------------------------------------------------------------------
1 | namespace NatTypeTester.Models;
2 |
3 | [UsedImplicitly]
4 | public sealed partial class Config : ReactiveObject, ISingletonDependency
5 | {
6 | public Config()
7 | {
8 | StunServer = @"";
9 | ProxyType = ProxyType.Plain;
10 | ProxyServer = @"127.0.0.1:1080";
11 | }
12 |
13 | [Reactive]
14 | public partial string StunServer { get; set; }
15 |
16 | [Reactive]
17 | public partial ProxyType ProxyType { get; set; }
18 |
19 | [Reactive]
20 | public partial string ProxyServer { get; set; }
21 |
22 | [Reactive]
23 | public partial string? ProxyUser { get; set; }
24 |
25 | [Reactive]
26 | public partial string? ProxyPassword { get; set; }
27 | }
28 |
--------------------------------------------------------------------------------
/src/NatTypeTester.Models/NatTypeTester.Models.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/NatTypeTester.Models/NatTypeTesterModelsModule.cs:
--------------------------------------------------------------------------------
1 | global using JetBrains.Annotations;
2 | global using ReactiveUI;
3 | global using ReactiveUI.SourceGenerators;
4 | global using STUN.Enums;
5 | global using Volo.Abp.DependencyInjection;
6 | global using Volo.Abp.Modularity;
7 |
8 | namespace NatTypeTester.Models;
9 |
10 | public class NatTypeTesterModelsModule : AbpModule;
11 |
--------------------------------------------------------------------------------
/src/NatTypeTester.ViewModels/MainWindowViewModel.cs:
--------------------------------------------------------------------------------
1 | using DynamicData;
2 | using DynamicData.Binding;
3 | using Microsoft.Extensions.DependencyInjection;
4 | using Microsoft.VisualStudio.Threading;
5 | using NatTypeTester.Models;
6 | using ReactiveUI;
7 | using STUN;
8 | using System.Collections.Frozen;
9 | using System.Reactive.Linq;
10 | using Volo.Abp.DependencyInjection;
11 |
12 | namespace NatTypeTester.ViewModels;
13 |
14 | [ExposeServices(
15 | typeof(MainWindowViewModel),
16 | typeof(IScreen)
17 | )]
18 | public class MainWindowViewModel : ViewModelBase, IScreen
19 | {
20 | public RoutingState Router => TransientCachedServiceProvider.GetRequiredService();
21 |
22 | public Config Config => TransientCachedServiceProvider.GetRequiredService();
23 |
24 | private static readonly FrozenSet DefaultServers =
25 | [
26 | @"stun.hot-chilli.net",
27 | @"stun.fitauto.ru",
28 | @"stun.internetcalls.com",
29 | @"stun.miwifi.com",
30 | @"stun.voip.aebc.com",
31 | @"stun.voipbuster.com",
32 | @"stun.voipstunt.com"
33 | ];
34 |
35 | private SourceList List { get; } = new();
36 |
37 | public readonly IObservableCollection StunServers = new ObservableCollectionExtended();
38 |
39 | public MainWindowViewModel()
40 | {
41 | List.Connect()
42 | .DistinctValues(x => x)
43 | .ObserveOn(RxApp.MainThreadScheduler)
44 | .Bind(StunServers)
45 | .Subscribe();
46 | }
47 |
48 | public void LoadStunServer()
49 | {
50 | foreach (string? server in DefaultServers)
51 | {
52 | List.Add(server);
53 | }
54 |
55 | Config.StunServer = DefaultServers.First();
56 |
57 | Task.Run(() =>
58 | {
59 | const string path = @"stun.txt";
60 |
61 | if (!File.Exists(path))
62 | {
63 | return;
64 | }
65 |
66 | foreach (string line in File.ReadLines(path))
67 | {
68 | if (!string.IsNullOrWhiteSpace(line) && StunServer.TryParse(line, out StunServer? stun))
69 | {
70 | List.Add(stun.ToString());
71 | }
72 | }
73 | }).Forget();
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/NatTypeTester.ViewModels/NatTypeTester.ViewModels.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/NatTypeTester.ViewModels/NatTypeTesterViewModelModule.cs:
--------------------------------------------------------------------------------
1 | using Dns.Net.Abstractions;
2 | using Dns.Net.Clients;
3 | using JetBrains.Annotations;
4 | using Microsoft.Extensions.DependencyInjection.Extensions;
5 | using NatTypeTester.Models;
6 | using Volo.Abp.Modularity;
7 |
8 | namespace NatTypeTester.ViewModels;
9 |
10 | [DependsOn(typeof(NatTypeTesterModelsModule))]
11 | [UsedImplicitly]
12 | public class NatTypeTesterViewModelModule : AbpModule
13 | {
14 | public override void ConfigureServices(ServiceConfigurationContext context)
15 | {
16 | context.Services.TryAddTransient();
17 | context.Services.TryAddTransient();
18 | context.Services.TryAddTransient();
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/NatTypeTester.ViewModels/RFC3489ViewModel.cs:
--------------------------------------------------------------------------------
1 | using Dns.Net.Abstractions;
2 | using Dns.Net.Clients;
3 | using JetBrains.Annotations;
4 | using Microsoft;
5 | using Microsoft.Extensions.DependencyInjection;
6 | using NatTypeTester.Models;
7 | using ReactiveUI;
8 | using Socks5.Models;
9 | using STUN;
10 | using STUN.Client;
11 | using STUN.Proxy;
12 | using STUN.StunResult;
13 | using System.Net;
14 | using System.Net.Sockets;
15 | using System.Reactive;
16 | using System.Reactive.Linq;
17 |
18 | namespace NatTypeTester.ViewModels;
19 |
20 | [UsedImplicitly]
21 | public class RFC3489ViewModel : ViewModelBase, IRoutableViewModel
22 | {
23 | public string UrlPathSegment => @"RFC3489";
24 |
25 | public IScreen HostScreen => TransientCachedServiceProvider.GetRequiredService();
26 |
27 | private Config Config => TransientCachedServiceProvider.GetRequiredService();
28 |
29 | private IDnsClient DnsClient => TransientCachedServiceProvider.GetRequiredService();
30 |
31 | private IDnsClient AAAADnsClient => TransientCachedServiceProvider.GetRequiredService();
32 |
33 | private IDnsClient ADnsClient => TransientCachedServiceProvider.GetRequiredService();
34 |
35 | private ClassicStunResult _result3489;
36 |
37 | public ClassicStunResult Result3489
38 | {
39 | get => _result3489;
40 | set => this.RaiseAndSetIfChanged(ref _result3489, value);
41 | }
42 |
43 | public ReactiveCommand TestClassicNatType { get; }
44 |
45 | public RFC3489ViewModel()
46 | {
47 | _result3489 = new ClassicStunResult();
48 | TestClassicNatType = ReactiveCommand.CreateFromTask(TestClassicNatTypeAsync);
49 | }
50 |
51 | private async Task TestClassicNatTypeAsync(CancellationToken token)
52 | {
53 | Verify.Operation(StunServer.TryParse(Config.StunServer, out StunServer? server), @"Wrong STUN Server!");
54 |
55 | if (!HostnameEndpoint.TryParse(Config.ProxyServer, out HostnameEndpoint? proxyIpe))
56 | {
57 | throw new NotSupportedException(@"Unknown proxy address");
58 | }
59 |
60 | Socks5CreateOption socks5Option = new()
61 | {
62 | Address = await DnsClient.QueryAsync(proxyIpe.Hostname, token),
63 | Port = proxyIpe.Port,
64 | UsernamePassword = new UsernamePassword
65 | {
66 | UserName = Config.ProxyUser,
67 | Password = Config.ProxyPassword
68 | }
69 | };
70 |
71 | IPAddress? serverIp;
72 |
73 | if (Result3489.LocalEndPoint is null)
74 | {
75 | serverIp = await DnsClient.QueryAsync(server.Hostname, token);
76 | Result3489.LocalEndPoint = serverIp.AddressFamily is AddressFamily.InterNetworkV6 ? new IPEndPoint(IPAddress.IPv6Any, IPEndPoint.MinPort) : new IPEndPoint(IPAddress.Any, IPEndPoint.MinPort);
77 | }
78 | else
79 | {
80 | if (Result3489.LocalEndPoint.AddressFamily is AddressFamily.InterNetworkV6)
81 | {
82 | serverIp = await AAAADnsClient.QueryAsync(server.Hostname, token);
83 | }
84 | else
85 | {
86 | serverIp = await ADnsClient.QueryAsync(server.Hostname, token);
87 | }
88 | }
89 |
90 | using IUdpProxy proxy = ProxyFactory.CreateProxy(Config.ProxyType, Result3489.LocalEndPoint, socks5Option);
91 |
92 | using StunClient3489 client = new(new IPEndPoint(serverIp, server.Port), Result3489.LocalEndPoint, proxy);
93 |
94 | try
95 | {
96 | using (Observable.Interval(TimeSpan.FromSeconds(0.1))
97 | .ObserveOn(RxApp.MainThreadScheduler)
98 | // ReSharper disable once AccessToDisposedClosure
99 | .Subscribe(_ => Result3489 = client.State with { }))
100 | {
101 | await client.ConnectProxyAsync(token);
102 |
103 | try
104 | {
105 | await client.QueryAsync(token);
106 | }
107 | finally
108 | {
109 | await client.CloseProxyAsync(token);
110 | }
111 | }
112 | }
113 | finally
114 | {
115 | Result3489 = client.State with { };
116 | }
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/src/NatTypeTester.ViewModels/RFC5780ViewModel.cs:
--------------------------------------------------------------------------------
1 | using Dns.Net.Abstractions;
2 | using Dns.Net.Clients;
3 | using JetBrains.Annotations;
4 | using Microsoft;
5 | using Microsoft.Extensions.DependencyInjection;
6 | using NatTypeTester.Models;
7 | using ReactiveUI;
8 | using Socks5.Models;
9 | using STUN;
10 | using STUN.Client;
11 | using STUN.Enums;
12 | using STUN.Proxy;
13 | using STUN.StunResult;
14 | using System.Net;
15 | using System.Net.Sockets;
16 | using System.Reactive;
17 | using System.Reactive.Linq;
18 |
19 | namespace NatTypeTester.ViewModels;
20 |
21 | [UsedImplicitly]
22 | public class RFC5780ViewModel : ViewModelBase, IRoutableViewModel
23 | {
24 | public string UrlPathSegment => @"RFC5780";
25 |
26 | public IScreen HostScreen => TransientCachedServiceProvider.GetRequiredService();
27 |
28 | private Config Config => TransientCachedServiceProvider.GetRequiredService();
29 |
30 | private IDnsClient DnsClient => TransientCachedServiceProvider.GetRequiredService();
31 |
32 | private IDnsClient AAAADnsClient => TransientCachedServiceProvider.GetRequiredService();
33 |
34 | private IDnsClient ADnsClient => TransientCachedServiceProvider.GetRequiredService();
35 |
36 | private StunResult5389 _result5389;
37 |
38 | public StunResult5389 Result5389
39 | {
40 | get => _result5389;
41 | set => this.RaiseAndSetIfChanged(ref _result5389, value);
42 | }
43 |
44 | private StunResult5389 _udpResult;
45 | private StunResult5389 _tcpResult;
46 | private StunResult5389 _tlsResult;
47 |
48 | private TransportType _transportType;
49 |
50 | public TransportType TransportType
51 | {
52 | get => _transportType;
53 | set => this.RaiseAndSetIfChanged(ref _transportType, value);
54 | }
55 |
56 | public ReactiveCommand DiscoveryNatType { get; }
57 |
58 | public RFC5780ViewModel()
59 | {
60 | _udpResult = new StunResult5389();
61 | _tcpResult = new StunResult5389();
62 | _tlsResult = new StunResult5389();
63 | _result5389 = _udpResult;
64 | DiscoveryNatType = ReactiveCommand.CreateFromTask(DiscoveryNatTypeAsync);
65 | }
66 |
67 | private async Task DiscoveryNatTypeAsync(CancellationToken token)
68 | {
69 | Verify.Operation(StunServer.TryParse(Config.StunServer, out StunServer? server, TransportType is TransportType.Tls ? StunServer.DefaultTlsPort : StunServer.DefaultPort), @"Wrong STUN Server!");
70 |
71 | if (!HostnameEndpoint.TryParse(Config.ProxyServer, out HostnameEndpoint? proxyIpe))
72 | {
73 | throw new NotSupportedException(@"Unknown proxy address");
74 | }
75 |
76 | Socks5CreateOption socks5Option = new()
77 | {
78 | Address = await DnsClient.QueryAsync(proxyIpe.Hostname, token),
79 | Port = proxyIpe.Port,
80 | UsernamePassword = new UsernamePassword
81 | {
82 | UserName = Config.ProxyUser,
83 | Password = Config.ProxyPassword
84 | }
85 | };
86 |
87 | IPAddress? serverIp;
88 |
89 | if (Result5389.LocalEndPoint is null)
90 | {
91 | serverIp = await DnsClient.QueryAsync(server.Hostname, token);
92 | Result5389.LocalEndPoint = serverIp.AddressFamily is AddressFamily.InterNetworkV6 ? new IPEndPoint(IPAddress.IPv6Any, IPEndPoint.MinPort) : new IPEndPoint(IPAddress.Any, IPEndPoint.MinPort);
93 | }
94 | else
95 | {
96 | if (Result5389.LocalEndPoint.AddressFamily is AddressFamily.InterNetworkV6)
97 | {
98 | serverIp = await AAAADnsClient.QueryAsync(server.Hostname, token);
99 | }
100 | else
101 | {
102 | serverIp = await ADnsClient.QueryAsync(server.Hostname, token);
103 | }
104 | }
105 |
106 | TransportType transport = TransportType;
107 |
108 | if (transport is TransportType.Udp)
109 | {
110 | using IUdpProxy proxy = ProxyFactory.CreateProxy(Config.ProxyType, Result5389.LocalEndPoint, socks5Option);
111 | using StunClient5389UDP client = new(new IPEndPoint(serverIp, server.Port), Result5389.LocalEndPoint, proxy);
112 |
113 | try
114 | {
115 | using (Observable.Interval(TimeSpan.FromSeconds(0.1))
116 | .ObserveOn(RxApp.MainThreadScheduler)
117 | // ReSharper disable once AccessToDisposedClosure
118 | .Subscribe(_ => Result5389 = _udpResult = client.State with { }))
119 | {
120 | await client.ConnectProxyAsync(token);
121 |
122 | try
123 | {
124 | await client.QueryAsync(token);
125 | }
126 | finally
127 | {
128 | await client.CloseProxyAsync(token);
129 | }
130 | }
131 | }
132 | finally
133 | {
134 | Result5389 = _udpResult = client.State with { };
135 | }
136 | }
137 | else
138 | {
139 | using ITcpProxy proxy = ProxyFactory.CreateProxy(transport, Config.ProxyType, socks5Option, server.Hostname);
140 | using IStunClient5389 client = new StunClient5389TCP(new IPEndPoint(serverIp, server.Port), Result5389.LocalEndPoint, proxy);
141 |
142 | try
143 | {
144 | using (Observable.Interval(TimeSpan.FromSeconds(0.1))
145 | .ObserveOn(RxApp.MainThreadScheduler)
146 | .Subscribe(_ => UpdateData()))
147 | {
148 | await client.QueryAsync(token);
149 | }
150 | }
151 | finally
152 | {
153 | UpdateData();
154 | }
155 |
156 | void UpdateData()
157 | {
158 | // ReSharper disable once AccessToDisposedClosure
159 | Result5389 = client.State with { };
160 |
161 | if (transport is TransportType.Tcp)
162 | {
163 | _tcpResult = Result5389;
164 | }
165 | else
166 | {
167 | _tlsResult = Result5389;
168 | }
169 | }
170 | }
171 | }
172 |
173 | public void ResetResult()
174 | {
175 | Result5389 = TransportType switch
176 | {
177 | TransportType.Tcp => _tcpResult,
178 | TransportType.Tls => _tlsResult,
179 | _ => _udpResult
180 | };
181 | }
182 | }
183 |
--------------------------------------------------------------------------------
/src/NatTypeTester.ViewModels/SettingViewModel.cs:
--------------------------------------------------------------------------------
1 | using JetBrains.Annotations;
2 | using Microsoft.Extensions.DependencyInjection;
3 | using NatTypeTester.Models;
4 | using ReactiveUI;
5 |
6 | namespace NatTypeTester.ViewModels;
7 |
8 | [UsedImplicitly]
9 | public class SettingViewModel : ViewModelBase, IRoutableViewModel
10 | {
11 | public string UrlPathSegment => @"Settings";
12 |
13 | public IScreen HostScreen => TransientCachedServiceProvider.GetRequiredService();
14 |
15 | public Config Config => TransientCachedServiceProvider.GetRequiredService();
16 | }
17 |
--------------------------------------------------------------------------------
/src/NatTypeTester.ViewModels/ValueConverters/StringToIPEndpointTypeConverter.cs:
--------------------------------------------------------------------------------
1 | using JetBrains.Annotations;
2 | using ReactiveUI;
3 | using System.Net;
4 | using Volo.Abp.DependencyInjection;
5 |
6 | namespace NatTypeTester.ViewModels.ValueConverters;
7 |
8 | [ExposeServices(typeof(IBindingTypeConverter))]
9 | [UsedImplicitly]
10 | public class StringToIPEndpointTypeConverter : IBindingTypeConverter, ISingletonDependency
11 | {
12 | public int GetAffinityForObjects(Type fromType, Type toType)
13 | {
14 | if (fromType == typeof(string) && toType == typeof(IPEndPoint))
15 | {
16 | return 11;
17 | }
18 |
19 | if (fromType == typeof(IPEndPoint) && toType == typeof(string))
20 | {
21 | return 11;
22 | }
23 |
24 | return 0;
25 | }
26 |
27 | public bool TryConvert(object? from, Type toType, object? conversionHint, out object? result)
28 | {
29 | if (toType == typeof(IPEndPoint) && from is string str)
30 | {
31 | if (IPEndPoint.TryParse(str, out IPEndPoint? ipe))
32 | {
33 | result = ipe;
34 | return true;
35 | }
36 |
37 | result = null;
38 | return true;
39 | }
40 |
41 | if (from is IPEndPoint fromIPEndPoint)
42 | {
43 | result = fromIPEndPoint.ToString();
44 | }
45 | else
46 | {
47 | result = string.Empty;
48 | }
49 |
50 | return true;
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/NatTypeTester.ViewModels/ViewModelBase.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.DependencyInjection;
2 | using ReactiveUI;
3 | using Volo.Abp.DependencyInjection;
4 |
5 | namespace NatTypeTester.ViewModels;
6 |
7 | public abstract class ViewModelBase : ReactiveObject, ISingletonDependency
8 | {
9 | public required ITransientCachedServiceProvider TransientCachedServiceProvider { get; init; }
10 |
11 | protected IServiceProvider ServiceProvider => TransientCachedServiceProvider.GetRequiredService();
12 | }
13 |
--------------------------------------------------------------------------------
/src/NatTypeTester/App.xaml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/NatTypeTester/App.xaml.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.DependencyInjection;
2 | using Splat.Microsoft.Extensions.DependencyInjection;
3 | using System.Windows;
4 | using Volo.Abp;
5 |
6 | #pragma warning disable VSTHRD100 // 避免使用 Async Void 方法
7 | namespace NatTypeTester;
8 |
9 | public partial class App
10 | {
11 | private readonly IAbpApplicationWithInternalServiceProvider _application;
12 |
13 | public App()
14 | {
15 | _application = AbpApplicationFactory.Create(options =>
16 | {
17 | options.UseAutofac();
18 | });
19 | }
20 |
21 | protected override async void OnStartup(StartupEventArgs e)
22 | {
23 | try
24 | {
25 | await _application.InitializeAsync();
26 | _application.ServiceProvider.UseMicrosoftDependencyResolver();
27 | _application.Services.GetRequiredService().Show();
28 | }
29 | catch (Exception ex)
30 | {
31 | MessageBox.Show(ex.Message, nameof(NatTypeTester), MessageBoxButton.OK, MessageBoxImage.Error);
32 | }
33 | }
34 |
35 | protected override async void OnExit(ExitEventArgs e)
36 | {
37 | await _application.ShutdownAsync();
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/NatTypeTester/Dialogs/DisposableContentDialog.cs:
--------------------------------------------------------------------------------
1 | using ModernWpf.Controls;
2 |
3 | namespace NatTypeTester.Dialogs;
4 |
5 | public class DisposableContentDialog : ContentDialog, IDisposable
6 | {
7 | public void Dispose()
8 | {
9 | Hide();
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/NatTypeTester/MainWindow.xaml:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 |
17 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
36 |
37 |
38 |
39 |
40 |
47 |
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/src/NatTypeTester/MainWindow.xaml.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.DependencyInjection;
2 | using ModernWpf.Controls;
3 | using NatTypeTester.ViewModels;
4 | using ReactiveMarbles.ObservableEvents;
5 | using ReactiveUI;
6 | using System.Reactive.Disposables;
7 | using Volo.Abp.DependencyInjection;
8 |
9 | namespace NatTypeTester;
10 |
11 | public partial class MainWindow : ISingletonDependency
12 | {
13 | public MainWindow(MainWindowViewModel viewModel, IServiceProvider serviceProvider)
14 | {
15 | InitializeComponent();
16 | ViewModel = viewModel;
17 |
18 | this.WhenActivated(d =>
19 | {
20 | #region Server
21 |
22 | this.Bind(ViewModel,
23 | vm => vm.Config.StunServer,
24 | v => v.ServersComboBox.Text
25 | ).DisposeWith(d);
26 |
27 | this.OneWayBind(ViewModel,
28 | vm => vm.StunServers,
29 | v => v.ServersComboBox.ItemsSource
30 | ).DisposeWith(d);
31 |
32 | #endregion
33 |
34 | this.OneWayBind(ViewModel, vm => vm.Router, v => v.RoutedViewHost.Router).DisposeWith(d);
35 |
36 | NavigationView.Events().SelectionChanged
37 | .Subscribe(parameter =>
38 | {
39 | if (parameter.args.IsSettingsSelected)
40 | {
41 | ViewModel.Router.Navigate.Execute(serviceProvider.GetRequiredService()).Subscribe().Dispose();
42 | return;
43 | }
44 |
45 | if (parameter.args.SelectedItem is not NavigationViewItem { Tag: string tag })
46 | {
47 | return;
48 | }
49 |
50 | switch (tag)
51 | {
52 | case @"1":
53 | {
54 | ViewModel.Router.Navigate.Execute(serviceProvider.GetRequiredService()).Subscribe().Dispose();
55 | break;
56 | }
57 | case @"2":
58 | {
59 | ViewModel.Router.Navigate.Execute(serviceProvider.GetRequiredService()).Subscribe().Dispose();
60 | break;
61 | }
62 | }
63 | }).DisposeWith(d);
64 | NavigationView.SelectedItem = NavigationView.MenuItems.OfType().First();
65 |
66 | ViewModel.LoadStunServer();
67 | });
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/NatTypeTester/NatTypeTester.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0-windows10.0.22621.0
5 | WinExe
6 | true
7 | 8.0.3
8 | icon.ico
9 | app.manifest
10 | true
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | True
33 | Designer
34 | MSBuild:Compile
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/src/NatTypeTester/NatTypeTesterModule.cs:
--------------------------------------------------------------------------------
1 | using JetBrains.Annotations;
2 | using Microsoft.Extensions.DependencyInjection.Extensions;
3 | using NatTypeTester.ViewModels;
4 | using ReactiveUI;
5 | using Splat;
6 | using Splat.Microsoft.Extensions.DependencyInjection;
7 | using Volo.Abp.Autofac;
8 | using Volo.Abp.Modularity;
9 |
10 | namespace NatTypeTester;
11 |
12 | [DependsOn(
13 | typeof(AbpAutofacModule),
14 | typeof(NatTypeTesterViewModelModule)
15 | )]
16 | [UsedImplicitly]
17 | public class NatTypeTesterModule : AbpModule
18 | {
19 | public override void PreConfigureServices(ServiceConfigurationContext context)
20 | {
21 | context.Services.UseMicrosoftDependencyResolver();
22 | Locator.CurrentMutable.InitializeSplat();
23 | Locator.CurrentMutable.InitializeReactiveUI(RegistrationNamespace.Wpf);
24 | }
25 |
26 | public override void ConfigureServices(ServiceConfigurationContext context)
27 | {
28 | context.Services.TryAddTransient();
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/NatTypeTester/Properties/DesignTimeResources.xaml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/NatTypeTester/Utils/Extensions.cs:
--------------------------------------------------------------------------------
1 | using NatTypeTester.Dialogs;
2 |
3 | namespace NatTypeTester.Utils;
4 |
5 | public static class Extensions
6 | {
7 | public static async Task HandleExceptionWithContentDialogAsync(this Exception ex)
8 | {
9 | using DisposableContentDialog dialog = new();
10 | dialog.Title = nameof(NatTypeTester);
11 | dialog.Content = ex.Message;
12 | dialog.PrimaryButtonText = @"OK";
13 | await dialog.ShowAsync();
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/NatTypeTester/Views/RFC3489View.xaml:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
25 |
30 | 0.0.0.0:0
31 | [::]:0
32 |
33 |
37 |
38 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/src/NatTypeTester/Views/RFC3489View.xaml.cs:
--------------------------------------------------------------------------------
1 | using JetBrains.Annotations;
2 | using NatTypeTester.Utils;
3 | using NatTypeTester.ViewModels;
4 | using ReactiveMarbles.ObservableEvents;
5 | using ReactiveUI;
6 | using System.Reactive.Disposables;
7 | using System.Reactive.Linq;
8 | using System.Windows.Input;
9 | using Volo.Abp.DependencyInjection;
10 |
11 | namespace NatTypeTester.Views;
12 |
13 | [ExposeServices(typeof(IViewFor))]
14 | [UsedImplicitly]
15 | public partial class RFC3489View : ITransientDependency
16 | {
17 | public RFC3489View(RFC3489ViewModel viewModel)
18 | {
19 | InitializeComponent();
20 | ViewModel = viewModel;
21 |
22 | this.WhenActivated(d =>
23 | {
24 | this.OneWayBind(ViewModel, vm => vm.Result3489.NatType, v => v.NatTypeTextBox.Text).DisposeWith(d);
25 |
26 | this.Bind(ViewModel, vm => vm.Result3489.LocalEndPoint, v => v.LocalEndComboBox.Text).DisposeWith(d);
27 |
28 | LocalEndComboBox.Events().LostKeyboardFocus.Subscribe(_ => LocalEndComboBox.Text = ViewModel.Result3489.LocalEndPoint?.ToString() ?? string.Empty).DisposeWith(d);
29 |
30 | this.OneWayBind(ViewModel, vm => vm.Result3489.PublicEndPoint, v => v.PublicEndTextBox.Text).DisposeWith(d);
31 |
32 | this.BindCommand(ViewModel, vm => vm.TestClassicNatType, v => v.TestButton).DisposeWith(d);
33 |
34 | this.Events().KeyDown
35 | .Where(x => x.Key == Key.Enter && TestButton.Command.CanExecute(default))
36 | .Subscribe(_ => TestButton.Command.Execute(default))
37 | .DisposeWith(d);
38 |
39 | ViewModel.TestClassicNatType.ThrownExceptions.Subscribe(ex => _ = ex.HandleExceptionWithContentDialogAsync()).DisposeWith(d);
40 | });
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/NatTypeTester/Views/RFC5780View.xaml:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
35 |
40 |
45 |
50 | 0.0.0.0:0
51 | [::]:0
52 |
53 |
58 |
59 |
60 |
61 |
62 |
--------------------------------------------------------------------------------
/src/NatTypeTester/Views/RFC5780View.xaml.cs:
--------------------------------------------------------------------------------
1 | using JetBrains.Annotations;
2 | using NatTypeTester.Utils;
3 | using NatTypeTester.ViewModels;
4 | using ReactiveMarbles.ObservableEvents;
5 | using ReactiveUI;
6 | using STUN.Enums;
7 | using System.Reactive.Disposables;
8 | using System.Reactive.Linq;
9 | using System.Windows;
10 | using System.Windows.Input;
11 | using Volo.Abp.DependencyInjection;
12 |
13 | namespace NatTypeTester.Views;
14 |
15 | [ExposeServices(typeof(IViewFor))]
16 | [UsedImplicitly]
17 | public partial class RFC5780View : ITransientDependency
18 | {
19 | public RFC5780View(RFC5780ViewModel viewModel)
20 | {
21 | InitializeComponent();
22 | ViewModel = viewModel;
23 |
24 | this.WhenActivated(d =>
25 | {
26 | this.Bind(ViewModel, vm => vm.TransportType, v => v.TransportTypeRadioButtons.SelectedIndex, type => (int)type, index => (TransportType)index).DisposeWith(d);
27 | ViewModel.WhenAnyValue(vm => vm.TransportType).Subscribe(_ => ViewModel.ResetResult()).DisposeWith(d);
28 | this.OneWayBind(ViewModel, vm => vm.TransportType, v => v.FilteringBehaviorTextBox.Visibility, type => type is TransportType.Udp ? Visibility.Visible : Visibility.Collapsed).DisposeWith(d);
29 |
30 | this.OneWayBind(ViewModel, vm => vm.Result5389.BindingTestResult, v => v.BindingTestTextBox.Text).DisposeWith(d);
31 |
32 | this.OneWayBind(ViewModel, vm => vm.Result5389.MappingBehavior, v => v.MappingBehaviorTextBox.Text).DisposeWith(d);
33 |
34 | this.OneWayBind(ViewModel, vm => vm.Result5389.FilteringBehavior, v => v.FilteringBehaviorTextBox.Text).DisposeWith(d);
35 |
36 | this.Bind(ViewModel, vm => vm.Result5389.LocalEndPoint, v => v.LocalAddressComboBox.Text).DisposeWith(d);
37 |
38 | LocalAddressComboBox.Events().LostKeyboardFocus.Subscribe(_ => LocalAddressComboBox.Text = ViewModel.Result5389.LocalEndPoint?.ToString() ?? string.Empty).DisposeWith(d);
39 |
40 | this.OneWayBind(ViewModel, vm => vm.Result5389.PublicEndPoint, v => v.MappingAddressTextBox.Text).DisposeWith(d);
41 |
42 | this.BindCommand(ViewModel, vm => vm.DiscoveryNatType, v => v.DiscoveryButton).DisposeWith(d);
43 |
44 | this.Events().KeyDown
45 | .Where(x => x.Key is Key.Enter && DiscoveryButton.Command.CanExecute(default))
46 | .Subscribe(_ => DiscoveryButton.Command.Execute(default))
47 | .DisposeWith(d);
48 |
49 | ViewModel.DiscoveryNatType.ThrownExceptions.Subscribe(ex => _ = ex.HandleExceptionWithContentDialogAsync()).DisposeWith(d);
50 |
51 | ViewModel.DiscoveryNatType.IsExecuting.Subscribe(b => TransportTypeRadioButtons.IsEnabled = !b).DisposeWith(d);
52 | });
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/NatTypeTester/Views/SettingView.xaml:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
35 |
40 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/src/NatTypeTester/Views/SettingView.xaml.cs:
--------------------------------------------------------------------------------
1 | using JetBrains.Annotations;
2 | using NatTypeTester.ViewModels;
3 | using ReactiveUI;
4 | using STUN.Enums;
5 | using System.Reactive.Disposables;
6 | using Volo.Abp.DependencyInjection;
7 |
8 | namespace NatTypeTester.Views;
9 |
10 | [ExposeServices(typeof(IViewFor))]
11 | [UsedImplicitly]
12 | public partial class SettingView : ITransientDependency
13 | {
14 | public SettingView()
15 | {
16 | InitializeComponent();
17 |
18 | this.WhenActivated(d =>
19 | {
20 | this.Bind(ViewModel, vm => vm.Config.ProxyServer, v => v.ProxyServerTextBox.Text).DisposeWith(d);
21 |
22 | this.Bind(ViewModel, vm => vm.Config.ProxyUser, v => v.ProxyUsernameTextBox.Text).DisposeWith(d);
23 |
24 | this.Bind(ViewModel, vm => vm.Config.ProxyPassword, v => v.ProxyPasswordTextBox.Text).DisposeWith(d);
25 |
26 | this.Bind(ViewModel, vm => vm.Config.ProxyType, v => v.ProxyRadioButtons.SelectedIndex, type => (int)type, index => (ProxyType)index).DisposeWith(d);
27 |
28 | this.OneWayBind(ViewModel, vm => vm.Config.ProxyType, v => v.ProxyConfigGrid.IsEnabled, type => type is not ProxyType.Plain).DisposeWith(d);
29 | });
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/NatTypeTester/app.manifest:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
54 |
55 |
56 |
57 | PerMonitorV2
58 | true
59 | true
60 |
61 |
62 |
63 |
64 |
65 |
79 |
80 |
81 |
--------------------------------------------------------------------------------
/src/NatTypeTester/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HMBSbige/NatTypeTester/b55bf4399984a2c7ee2512fe180d7cc96022b506/src/NatTypeTester/icon.ico
--------------------------------------------------------------------------------
/src/STUN/Client/IStunClient.cs:
--------------------------------------------------------------------------------
1 | namespace STUN.Client;
2 |
3 | public interface IStunClient : IDisposable
4 | {
5 | ValueTask QueryAsync(CancellationToken cancellationToken = default);
6 | }
7 |
--------------------------------------------------------------------------------
/src/STUN/Client/IStunClient5389.cs:
--------------------------------------------------------------------------------
1 | using STUN.StunResult;
2 |
3 | namespace STUN.Client;
4 |
5 | public interface IStunClient5389 : IStunClient
6 | {
7 | StunResult5389 State { get; }
8 | ValueTask BindingTestAsync(CancellationToken cancellationToken = default);
9 | ValueTask MappingBehaviorTestAsync(CancellationToken cancellationToken = default);
10 | ValueTask FilteringBehaviorTestAsync(CancellationToken cancellationToken = default);
11 | }
12 |
--------------------------------------------------------------------------------
/src/STUN/Client/IUdpStunClient.cs:
--------------------------------------------------------------------------------
1 | namespace STUN.Client;
2 |
3 | public interface IUdpStunClient : IStunClient
4 | {
5 | TimeSpan ReceiveTimeout { get; set; }
6 | ValueTask ConnectProxyAsync(CancellationToken cancellationToken = default);
7 | ValueTask CloseProxyAsync(CancellationToken cancellationToken = default);
8 | }
9 |
--------------------------------------------------------------------------------
/src/STUN/Client/StunClient3489.cs:
--------------------------------------------------------------------------------
1 | using Microsoft;
2 | using STUN.Enums;
3 | using STUN.Messages;
4 | using STUN.Proxy;
5 | using STUN.StunResult;
6 | using STUN.Utils;
7 | using System.Buffers;
8 | using System.Diagnostics;
9 | using System.Net;
10 | using System.Net.Sockets;
11 |
12 | namespace STUN.Client;
13 |
14 | ///
15 | /// https://tools.ietf.org/html/rfc3489#section-10.1
16 | ///
17 | public class StunClient3489 : IUdpStunClient
18 | {
19 | public virtual IPEndPoint LocalEndPoint => (IPEndPoint)_proxy.Client.LocalEndPoint!;
20 |
21 | public TimeSpan ReceiveTimeout { get; set; } = TimeSpan.FromSeconds(3);
22 |
23 | private readonly IPEndPoint _remoteEndPoint;
24 |
25 | private readonly IUdpProxy _proxy;
26 |
27 | public ClassicStunResult State { get; private set; } = new();
28 |
29 | public StunClient3489(IPEndPoint server, IPEndPoint local, IUdpProxy? proxy = null)
30 | {
31 | Requires.NotNull(server, nameof(server));
32 | Requires.NotNull(local, nameof(local));
33 |
34 | _proxy = proxy ?? new NoneUdpProxy(local);
35 |
36 | _remoteEndPoint = server;
37 |
38 | State.LocalEndPoint = local;
39 | }
40 |
41 | public async ValueTask ConnectProxyAsync(CancellationToken cancellationToken = default)
42 | {
43 | using CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
44 | cts.CancelAfter(ReceiveTimeout);
45 |
46 | await _proxy.ConnectAsync(cts.Token);
47 | }
48 |
49 | public async ValueTask CloseProxyAsync(CancellationToken cancellationToken = default)
50 | {
51 | await _proxy.CloseAsync(cancellationToken);
52 | }
53 |
54 | public async ValueTask QueryAsync(CancellationToken cancellationToken = default)
55 | {
56 | State = new ClassicStunResult();
57 |
58 | // test I
59 | StunResponse? response1 = await Test1Async(cancellationToken);
60 | if (response1 is null)
61 | {
62 | State.NatType = NatType.UdpBlocked;
63 | return;
64 | }
65 |
66 | State.LocalEndPoint = response1.Local;
67 |
68 | IPEndPoint? mappedAddress1 = response1.Message.GetMappedAddressAttribute();
69 | IPEndPoint? changedAddress = response1.Message.GetChangedAddressAttribute();
70 |
71 | State.PublicEndPoint = mappedAddress1; // 显示 test I 得到的映射地址
72 |
73 | // 某些单 IP 服务器的迷惑操作
74 | if (mappedAddress1 is null || changedAddress is null
75 | || Equals(changedAddress.Address, response1.Remote.Address)
76 | || changedAddress.Port == response1.Remote.Port)
77 | {
78 | State.NatType = NatType.UnsupportedServer;
79 | return;
80 | }
81 |
82 | // test II
83 | StunResponse? response2 = await Test2Async(changedAddress, cancellationToken);
84 | IPEndPoint? mappedAddress2 = response2?.Message.GetMappedAddressAttribute();
85 |
86 | if (response2 is not null)
87 | {
88 | // 有些单 IP 服务器并不能测 NAT 类型
89 | if (Equals(response1.Remote.Address, response2.Remote.Address) || response1.Remote.Port == response2.Remote.Port)
90 | {
91 | State.NatType = NatType.UnsupportedServer;
92 | State.PublicEndPoint = mappedAddress2;
93 | return;
94 | }
95 | }
96 |
97 | // is Public IP == link's IP?
98 | if (Equals(mappedAddress1, response1.Local))
99 | {
100 | // No NAT
101 | if (response2 is null)
102 | {
103 | State.NatType = NatType.SymmetricUdpFirewall;
104 | State.PublicEndPoint = mappedAddress1;
105 | }
106 | else
107 | {
108 | State.NatType = NatType.OpenInternet;
109 | State.PublicEndPoint = mappedAddress2;
110 | }
111 | return;
112 | }
113 |
114 | // NAT
115 | if (response2 is not null)
116 | {
117 | State.NatType = NatType.FullCone;
118 | State.PublicEndPoint = mappedAddress2;
119 | return;
120 | }
121 |
122 | // Test I(#2)
123 | StunResponse? response12 = await Test1_2Async(changedAddress, cancellationToken);
124 | IPEndPoint? mappedAddress12 = response12?.Message.GetMappedAddressAttribute();
125 |
126 | if (mappedAddress12 is null)
127 | {
128 | State.NatType = NatType.Unknown;
129 | return;
130 | }
131 |
132 | if (!Equals(mappedAddress12, mappedAddress1))
133 | {
134 | State.NatType = NatType.Symmetric;
135 | State.PublicEndPoint = mappedAddress12;
136 | return;
137 | }
138 |
139 | // Test III
140 | StunResponse? response3 = await Test3Async(cancellationToken);
141 | if (response3 is not null)
142 | {
143 | IPEndPoint? mappedAddress3 = response3.Message.GetMappedAddressAttribute();
144 | if (mappedAddress3 is not null
145 | && Equals(response3.Remote.Address, response1.Remote.Address)
146 | && response3.Remote.Port != response1.Remote.Port)
147 | {
148 | State.NatType = NatType.RestrictedCone;
149 | State.PublicEndPoint = mappedAddress3;
150 | return;
151 | }
152 | }
153 |
154 | State.NatType = NatType.PortRestrictedCone;
155 | State.PublicEndPoint = mappedAddress12;
156 | }
157 |
158 | private async ValueTask RequestAsync(StunMessage5389 sendMessage, IPEndPoint remote, IPEndPoint receive, CancellationToken cancellationToken)
159 | {
160 | try
161 | {
162 | using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(0x10000);
163 | Memory buffer = memoryOwner.Memory;
164 | int length = sendMessage.WriteTo(buffer.Span);
165 |
166 | await _proxy.SendToAsync(buffer[..length], SocketFlags.None, remote, cancellationToken);
167 |
168 | using CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
169 | cts.CancelAfter(ReceiveTimeout);
170 | SocketReceiveMessageFromResult r = await _proxy.ReceiveMessageFromAsync(buffer, SocketFlags.None, receive, cts.Token);
171 |
172 | StunMessage5389 message = new();
173 | if (message.TryParse(buffer[..r.ReceivedBytes]) && message.IsSameTransaction(sendMessage))
174 | {
175 | return new StunResponse(message, (IPEndPoint)r.RemoteEndPoint, new IPEndPoint(r.PacketInformation.Address, ((IPEndPoint)_proxy.Client.LocalEndPoint!).Port));
176 | }
177 | }
178 | catch (OperationCanceledException ex)
179 | {
180 | Debug.WriteLine(ex);
181 | }
182 | return default;
183 | }
184 |
185 | public virtual async ValueTask Test1Async(CancellationToken cancellationToken)
186 | {
187 | StunMessage5389 message = new()
188 | {
189 | StunMessageType = StunMessageType.BindingRequest,
190 | MagicCookie = 0
191 | };
192 | return await RequestAsync(message, _remoteEndPoint, _remoteEndPoint, cancellationToken);
193 | }
194 |
195 | public virtual async ValueTask Test2Async(IPEndPoint other, CancellationToken cancellationToken)
196 | {
197 | StunMessage5389 message = new()
198 | {
199 | StunMessageType = StunMessageType.BindingRequest,
200 | MagicCookie = 0,
201 | Attributes = new[] { AttributeExtensions.BuildChangeRequest(true, true) }
202 | };
203 | return await RequestAsync(message, _remoteEndPoint, other, cancellationToken);
204 | }
205 |
206 | public virtual async ValueTask Test1_2Async(IPEndPoint other, CancellationToken cancellationToken)
207 | {
208 | StunMessage5389 message = new()
209 | {
210 | StunMessageType = StunMessageType.BindingRequest,
211 | MagicCookie = 0
212 | };
213 | return await RequestAsync(message, other, other, cancellationToken);
214 | }
215 |
216 | public virtual async ValueTask Test3Async(CancellationToken cancellationToken)
217 | {
218 | StunMessage5389 message = new()
219 | {
220 | StunMessageType = StunMessageType.BindingRequest,
221 | MagicCookie = 0,
222 | Attributes = new[] { AttributeExtensions.BuildChangeRequest(false, true) }
223 | };
224 | return await RequestAsync(message, _remoteEndPoint, _remoteEndPoint, cancellationToken);
225 | }
226 |
227 | public void Dispose()
228 | {
229 | _proxy.Dispose();
230 |
231 | GC.SuppressFinalize(this);
232 | }
233 | }
234 |
--------------------------------------------------------------------------------
/src/STUN/Client/StunClient5389TCP.cs:
--------------------------------------------------------------------------------
1 | using Microsoft;
2 | using STUN.Enums;
3 | using STUN.Messages;
4 | using STUN.Proxy;
5 | using STUN.StunResult;
6 | using STUN.Utils;
7 | using System.Buffers;
8 | using System.Diagnostics;
9 | using System.Diagnostics.CodeAnalysis;
10 | using System.IO.Pipelines;
11 | using System.Net;
12 |
13 | namespace STUN.Client;
14 |
15 | public class StunClient5389TCP : IStunClient5389
16 | {
17 | public TimeSpan ConnectTimeout { get; set; } = TimeSpan.FromSeconds(3);
18 |
19 | private readonly IPEndPoint _remoteEndPoint;
20 | private IPEndPoint _lastLocalEndPoint;
21 |
22 | private readonly ITcpProxy _proxy;
23 |
24 | public StunResult5389 State { get; private set; } = new();
25 |
26 | public StunClient5389TCP(IPEndPoint server, IPEndPoint local, ITcpProxy? proxy = default)
27 | {
28 | Requires.NotNull(server, nameof(server));
29 | Requires.NotNull(local, nameof(local));
30 |
31 | _proxy = proxy ?? new DirectTcpProxy();
32 |
33 | _remoteEndPoint = server;
34 |
35 | _lastLocalEndPoint = local;
36 | State.LocalEndPoint = local;
37 | }
38 |
39 | public async ValueTask QueryAsync(CancellationToken cancellationToken = default)
40 | {
41 | await MappingBehaviorTestAsync(cancellationToken);
42 | State.FilteringBehavior = FilteringBehavior.None;
43 | }
44 |
45 | public async ValueTask MappingBehaviorTestAsync(CancellationToken cancellationToken = default)
46 | {
47 | State = new StunResult5389();
48 |
49 | // test I
50 | StunResult5389 bindingResult = await BindingTestAsync(cancellationToken);
51 | State = bindingResult with { };
52 | if (State.BindingTestResult is not BindingTestResult.Success)
53 | {
54 | return;
55 | }
56 |
57 | if (!HasValidOtherAddress(State.OtherEndPoint))
58 | {
59 | State.MappingBehavior = MappingBehavior.UnsupportedServer;
60 | return;
61 | }
62 |
63 | if (Equals(State.PublicEndPoint, State.LocalEndPoint))
64 | {
65 | State.MappingBehavior = MappingBehavior.Direct; // or Endpoint-Independent
66 | return;
67 | }
68 |
69 | // test II
70 | StunResult5389 result2 = await MappingBehaviorTestBase2Async();
71 | if (State.MappingBehavior is not MappingBehavior.Unknown)
72 | {
73 | return;
74 | }
75 |
76 | // test III
77 | await MappingBehaviorTestBase3Async();
78 |
79 | return;
80 |
81 | bool HasValidOtherAddress([NotNullWhen(true)] IPEndPoint? other)
82 | {
83 | return other is not null && !Equals(other.Address, _remoteEndPoint.Address) && other.Port != _remoteEndPoint.Port;
84 | }
85 |
86 | async ValueTask MappingBehaviorTestBase2Async()
87 | {
88 | StunResult5389 result = await BindingTestBaseAsync(new IPEndPoint(State.OtherEndPoint.Address, _remoteEndPoint.Port), cancellationToken);
89 |
90 | if (result.BindingTestResult is not BindingTestResult.Success)
91 | {
92 | State.MappingBehavior = MappingBehavior.Fail;
93 | }
94 | else if (Equals(result.PublicEndPoint, State.PublicEndPoint))
95 | {
96 | State.MappingBehavior = MappingBehavior.EndpointIndependent;
97 | }
98 | return result;
99 | }
100 |
101 | async ValueTask MappingBehaviorTestBase3Async()
102 | {
103 | StunResult5389 result3 = await BindingTestBaseAsync(State.OtherEndPoint, cancellationToken);
104 | if (result3.BindingTestResult is not BindingTestResult.Success)
105 | {
106 | State.MappingBehavior = MappingBehavior.Fail;
107 | return;
108 | }
109 |
110 | State.MappingBehavior = Equals(result3.PublicEndPoint, result2.PublicEndPoint) ? MappingBehavior.AddressDependent : MappingBehavior.AddressAndPortDependent;
111 | }
112 | }
113 |
114 | public ValueTask FilteringBehaviorTestAsync(CancellationToken cancellationToken = default)
115 | {
116 | throw new NotSupportedException(@"Filtering test applies only to UDP.");
117 | }
118 |
119 | public async ValueTask BindingTestAsync(CancellationToken cancellationToken = default)
120 | {
121 | return await BindingTestBaseAsync(_remoteEndPoint, cancellationToken);
122 | }
123 |
124 | protected virtual async ValueTask BindingTestBaseAsync(IPEndPoint remote, CancellationToken cancellationToken = default)
125 | {
126 | StunResult5389 result = new();
127 | StunMessage5389 test = new()
128 | {
129 | StunMessageType = StunMessageType.BindingRequest
130 | };
131 | StunResponse? response1 = await RequestAsync(test, remote, cancellationToken);
132 | IPEndPoint? mappedAddress1 = response1?.Message.GetXorMappedAddressAttribute();
133 | IPEndPoint? otherAddress = response1?.Message.GetOtherAddressAttribute();
134 |
135 | if (response1 is null)
136 | {
137 | result.BindingTestResult = BindingTestResult.Fail;
138 | }
139 | else if (mappedAddress1 is null)
140 | {
141 | result.BindingTestResult = BindingTestResult.UnsupportedServer;
142 | }
143 | else
144 | {
145 | result.BindingTestResult = BindingTestResult.Success;
146 | }
147 |
148 | IPEndPoint? local = response1?.Local;
149 |
150 | result.LocalEndPoint = local;
151 | result.PublicEndPoint = mappedAddress1;
152 | result.OtherEndPoint = otherAddress;
153 |
154 | return result;
155 | }
156 |
157 | private async ValueTask RequestAsync(StunMessage5389 sendMessage, IPEndPoint remote, CancellationToken cancellationToken)
158 | {
159 | try
160 | {
161 | using CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
162 | cts.CancelAfter(ConnectTimeout);
163 | IDuplexPipe pipe = await _proxy.ConnectAsync(_lastLocalEndPoint, remote, cts.Token);
164 | try
165 | {
166 | int length = sendMessage.WriteTo(pipe.Output.GetSpan(sendMessage.Length));
167 |
168 | pipe.Output.Advance(length);
169 | await pipe.Output.FlushAsync(cancellationToken);
170 |
171 | StunMessage5389 message = new();
172 | bool success = await ReadPipeAsync(message, pipe.Input);
173 |
174 | if (success && message.IsSameTransaction(sendMessage))
175 | {
176 | IPEndPoint? local = _proxy.CurrentLocalEndPoint;
177 | if (local is not null)
178 | {
179 | _lastLocalEndPoint = local;
180 | return new StunResponse(message, remote, local);
181 | }
182 | }
183 | }
184 | finally
185 | {
186 | await _proxy.CloseAsync(cancellationToken);
187 | }
188 | }
189 | catch (OperationCanceledException ex)
190 | {
191 | Debug.WriteLine(ex);
192 | }
193 |
194 | return default;
195 |
196 | async ValueTask ReadPipeAsync(StunMessage5389 message, PipeReader reader)
197 | {
198 | try
199 | {
200 | while (true)
201 | {
202 | cancellationToken.ThrowIfCancellationRequested();
203 |
204 | ReadResult result = await reader.ReadAsync(cancellationToken);
205 | ReadOnlySequence buffer = result.Buffer;
206 | try
207 | {
208 | if (message.TryParse(ref buffer))
209 | {
210 | return true;
211 | }
212 |
213 | if (result.IsCompleted)
214 | {
215 | break;
216 | }
217 | }
218 | finally
219 | {
220 | reader.AdvanceTo(buffer.Start, buffer.End);
221 | }
222 | }
223 |
224 | return false;
225 | }
226 | finally
227 | {
228 | await reader.CompleteAsync();
229 | }
230 | }
231 | }
232 |
233 | public void Dispose()
234 | {
235 | _proxy.Dispose();
236 |
237 | GC.SuppressFinalize(this);
238 | }
239 | }
240 |
--------------------------------------------------------------------------------
/src/STUN/Client/StunClient5389UDP.cs:
--------------------------------------------------------------------------------
1 | using Microsoft;
2 | using STUN.Enums;
3 | using STUN.Messages;
4 | using STUN.Proxy;
5 | using STUN.StunResult;
6 | using STUN.Utils;
7 | using System.Buffers;
8 | using System.Diagnostics;
9 | using System.Diagnostics.CodeAnalysis;
10 | using System.Net;
11 | using System.Net.Sockets;
12 | using System.Runtime.CompilerServices;
13 |
14 | namespace STUN.Client;
15 |
16 | ///
17 | /// https://tools.ietf.org/html/rfc5389#section-7.2.1
18 | /// https://tools.ietf.org/html/rfc5780#section-4.2
19 | ///
20 | public class StunClient5389UDP : IStunClient5389, IUdpStunClient
21 | {
22 | public TimeSpan ReceiveTimeout { get; set; } = TimeSpan.FromSeconds(3);
23 |
24 | private readonly IPEndPoint _remoteEndPoint;
25 |
26 | private readonly IUdpProxy _proxy;
27 |
28 | public StunResult5389 State { get; private set; } = new();
29 |
30 | public StunClient5389UDP(IPEndPoint server, IPEndPoint local, IUdpProxy? proxy = default)
31 | {
32 | Requires.NotNull(server, nameof(server));
33 | Requires.NotNull(local, nameof(local));
34 |
35 | _proxy = proxy ?? new NoneUdpProxy(local);
36 |
37 | _remoteEndPoint = server;
38 |
39 | State.LocalEndPoint = local;
40 | }
41 |
42 | public async ValueTask ConnectProxyAsync(CancellationToken cancellationToken = default)
43 | {
44 | using CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
45 | cts.CancelAfter(ReceiveTimeout);
46 |
47 | await _proxy.ConnectAsync(cts.Token);
48 | }
49 |
50 | public async ValueTask CloseProxyAsync(CancellationToken cancellationToken = default)
51 | {
52 | await _proxy.CloseAsync(cancellationToken);
53 | }
54 |
55 | public async ValueTask QueryAsync(CancellationToken cancellationToken = default)
56 | {
57 | State = new StunResult5389();
58 |
59 | await FilteringBehaviorTestBaseAsync(cancellationToken);
60 | if (State.BindingTestResult is not BindingTestResult.Success
61 | || State.FilteringBehavior is FilteringBehavior.UnsupportedServer
62 | )
63 | {
64 | return;
65 | }
66 |
67 | if (Equals(State.PublicEndPoint, State.LocalEndPoint))
68 | {
69 | State.MappingBehavior = MappingBehavior.Direct;
70 | return;
71 | }
72 |
73 | // MappingBehaviorTest test II
74 | StunResult5389 result2 = await MappingBehaviorTestBase2Async(cancellationToken);
75 | if (State.MappingBehavior is not MappingBehavior.Unknown)
76 | {
77 | return;
78 | }
79 |
80 | // MappingBehaviorTest test III
81 | await MappingBehaviorTestBase3Async(result2, cancellationToken);
82 | }
83 |
84 | public async ValueTask BindingTestAsync(CancellationToken cancellationToken = default)
85 | {
86 | return await BindingTestBaseAsync(_remoteEndPoint, cancellationToken);
87 | }
88 |
89 | protected virtual async ValueTask BindingTestBaseAsync(IPEndPoint remote, CancellationToken cancellationToken = default)
90 | {
91 | StunResult5389 result = new();
92 | StunMessage5389 test = new()
93 | {
94 | StunMessageType = StunMessageType.BindingRequest
95 | };
96 | StunResponse? response1 = await RequestAsync(test, remote, remote, cancellationToken);
97 | IPEndPoint? mappedAddress1 = response1?.Message.GetXorMappedAddressAttribute();
98 | IPEndPoint? otherAddress = response1?.Message.GetOtherAddressAttribute();
99 |
100 | if (response1 is null)
101 | {
102 | result.BindingTestResult = BindingTestResult.Fail;
103 | }
104 | else if (mappedAddress1 is null)
105 | {
106 | result.BindingTestResult = BindingTestResult.UnsupportedServer;
107 | }
108 | else
109 | {
110 | result.BindingTestResult = BindingTestResult.Success;
111 | }
112 |
113 | IPEndPoint? local = response1?.Local;
114 |
115 | result.LocalEndPoint = local;
116 | result.PublicEndPoint = mappedAddress1;
117 | result.OtherEndPoint = otherAddress;
118 |
119 | return result;
120 | }
121 |
122 | public async ValueTask MappingBehaviorTestAsync(CancellationToken cancellationToken = default)
123 | {
124 | State = new StunResult5389();
125 |
126 | // test I
127 | StunResult5389 bindingResult = await BindingTestAsync(cancellationToken);
128 | State = bindingResult with { };
129 | if (State.BindingTestResult is not BindingTestResult.Success)
130 | {
131 | return;
132 | }
133 |
134 | if (!HasValidOtherAddress(State.OtherEndPoint))
135 | {
136 | State.MappingBehavior = MappingBehavior.UnsupportedServer;
137 | return;
138 | }
139 |
140 | if (Equals(State.PublicEndPoint, State.LocalEndPoint))
141 | {
142 | State.MappingBehavior = MappingBehavior.Direct; // or Endpoint-Independent
143 | return;
144 | }
145 |
146 | // test II
147 | StunResult5389 result2 = await MappingBehaviorTestBase2Async(cancellationToken);
148 | if (State.MappingBehavior is not MappingBehavior.Unknown)
149 | {
150 | return;
151 | }
152 |
153 | // test III
154 | await MappingBehaviorTestBase3Async(result2, cancellationToken);
155 | }
156 |
157 | private async ValueTask MappingBehaviorTestBase2Async(CancellationToken cancellationToken)
158 | {
159 | Verify.Operation(State.OtherEndPoint is not null, @"OTHER-ADDRESS is not returned");
160 |
161 | StunResult5389 result2 = await BindingTestBaseAsync(new IPEndPoint(State.OtherEndPoint.Address, _remoteEndPoint.Port), cancellationToken);
162 |
163 | if (result2.BindingTestResult is not BindingTestResult.Success)
164 | {
165 | State.MappingBehavior = MappingBehavior.Fail;
166 | }
167 | else if (Equals(result2.PublicEndPoint, State.PublicEndPoint))
168 | {
169 | State.MappingBehavior = MappingBehavior.EndpointIndependent;
170 | }
171 |
172 | return result2;
173 | }
174 |
175 | private async ValueTask MappingBehaviorTestBase3Async(StunResult5389 result2, CancellationToken cancellationToken)
176 | {
177 | Verify.Operation(State.OtherEndPoint is not null, @"OTHER-ADDRESS is not returned");
178 |
179 | StunResult5389 result3 = await BindingTestBaseAsync(State.OtherEndPoint, cancellationToken);
180 | if (result3.BindingTestResult is not BindingTestResult.Success)
181 | {
182 | State.MappingBehavior = MappingBehavior.Fail;
183 | return;
184 | }
185 |
186 | State.MappingBehavior = Equals(result3.PublicEndPoint, result2.PublicEndPoint) ? MappingBehavior.AddressDependent : MappingBehavior.AddressAndPortDependent;
187 | }
188 |
189 | public async ValueTask FilteringBehaviorTestAsync(CancellationToken cancellationToken = default)
190 | {
191 | State = new StunResult5389();
192 | await FilteringBehaviorTestBaseAsync(cancellationToken);
193 | }
194 |
195 | private async ValueTask FilteringBehaviorTestBaseAsync(CancellationToken cancellationToken)
196 | {
197 | // test I
198 | StunResult5389 bindingResult = await BindingTestAsync(cancellationToken);
199 | State = bindingResult with { };
200 | if (State.BindingTestResult is not BindingTestResult.Success)
201 | {
202 | return;
203 | }
204 |
205 | if (!HasValidOtherAddress(State.OtherEndPoint))
206 | {
207 | State.FilteringBehavior = FilteringBehavior.UnsupportedServer;
208 | return;
209 | }
210 |
211 | // test II
212 | StunResponse? response2 = await FilteringBehaviorTest2Async(cancellationToken);
213 | if (response2 is not null)
214 | {
215 | State.FilteringBehavior = Equals(response2.Remote, State.OtherEndPoint) ? FilteringBehavior.EndpointIndependent : FilteringBehavior.UnsupportedServer;
216 | return;
217 | }
218 |
219 | // test III
220 | StunResponse? response3 = await FilteringBehaviorTest3Async(cancellationToken);
221 | if (response3 is null)
222 | {
223 | State.FilteringBehavior = FilteringBehavior.AddressAndPortDependent;
224 | return;
225 | }
226 |
227 | if (Equals(response3.Remote.Address, _remoteEndPoint.Address) && response3.Remote.Port != _remoteEndPoint.Port)
228 | {
229 | State.FilteringBehavior = FilteringBehavior.AddressDependent;
230 | }
231 | else
232 | {
233 | State.FilteringBehavior = FilteringBehavior.UnsupportedServer;
234 | }
235 | }
236 |
237 | protected virtual async ValueTask FilteringBehaviorTest2Async(CancellationToken cancellationToken = default)
238 | {
239 | Assumes.NotNull(State.OtherEndPoint);
240 |
241 | StunMessage5389 message = new()
242 | {
243 | StunMessageType = StunMessageType.BindingRequest,
244 | Attributes = new[] { AttributeExtensions.BuildChangeRequest(true, true) }
245 | };
246 | return await RequestAsync(message, _remoteEndPoint, State.OtherEndPoint, cancellationToken);
247 | }
248 |
249 | protected virtual async ValueTask FilteringBehaviorTest3Async(CancellationToken cancellationToken = default)
250 | {
251 | Assumes.NotNull(State.OtherEndPoint);
252 |
253 | StunMessage5389 message = new()
254 | {
255 | StunMessageType = StunMessageType.BindingRequest,
256 | Attributes = new[] { AttributeExtensions.BuildChangeRequest(false, true) }
257 | };
258 | return await RequestAsync(message, _remoteEndPoint, _remoteEndPoint, cancellationToken);
259 | }
260 |
261 | [MethodImpl(MethodImplOptions.AggressiveInlining)]
262 | private bool HasValidOtherAddress([NotNullWhen(true)] IPEndPoint? other)
263 | {
264 | return other is not null && !Equals(other.Address, _remoteEndPoint.Address) && other.Port != _remoteEndPoint.Port;
265 | }
266 |
267 | private async ValueTask RequestAsync(StunMessage5389 sendMessage, IPEndPoint remote, IPEndPoint receive, CancellationToken cancellationToken)
268 | {
269 | try
270 | {
271 | using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(0x10000);
272 | Memory buffer = memoryOwner.Memory;
273 | int length = sendMessage.WriteTo(buffer.Span);
274 |
275 | await _proxy.SendToAsync(buffer[..length], SocketFlags.None, remote, cancellationToken);
276 |
277 | using CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
278 | cts.CancelAfter(ReceiveTimeout);
279 | SocketReceiveMessageFromResult r = await _proxy.ReceiveMessageFromAsync(buffer, SocketFlags.None, receive, cts.Token);
280 |
281 | StunMessage5389 message = new();
282 | if (message.TryParse(buffer[..r.ReceivedBytes]) && message.IsSameTransaction(sendMessage))
283 | {
284 | return new StunResponse(message, (IPEndPoint)r.RemoteEndPoint, new IPEndPoint(r.PacketInformation.Address, ((IPEndPoint)_proxy.Client.LocalEndPoint!).Port));
285 | }
286 | }
287 | catch (OperationCanceledException ex)
288 | {
289 | Debug.WriteLine(ex);
290 | }
291 | return default;
292 | }
293 |
294 | public void Dispose()
295 | {
296 | _proxy.Dispose();
297 |
298 | GC.SuppressFinalize(this);
299 | }
300 | }
301 |
--------------------------------------------------------------------------------
/src/STUN/Enums/AttributeType.cs:
--------------------------------------------------------------------------------
1 | namespace STUN.Enums;
2 |
3 | ///
4 | /// STUN Attribute Registry
5 | ///
6 | ///
7 | /// https://tools.ietf.org/html/rfc3489#section-11.2
8 | /// https://tools.ietf.org/html/rfc5389#section-18.2
9 | /// https://tools.ietf.org/html/rfc5780#section-9.1
10 | /// https://tools.ietf.org/html/rfc8489#section-18.3
11 | ///
12 | public enum AttributeType : ushort
13 | {
14 | Useless = 0x0000,
15 | MappedAddress = 0x0001,
16 | ResponseAddress = 0x0002,
17 | ChangeRequest = 0x0003,
18 | SourceAddress = 0x0004,
19 | ChangedAddress = 0x0005,
20 | Username = 0x0006,
21 | Password = 0x0007,
22 | MessageIntegrity = 0x0008,
23 | ErrorCode = 0x0009,
24 | UnknownAttribute = 0x000A,
25 | ReflectedFrom = 0x000B,
26 | Realm = 0x0014,
27 | Nonce = 0x0015,
28 | MessageIntegritySha256 = 0x001C,
29 | PasswordAlgorithm = 0x001D,
30 | UserHash = 0x001E,
31 | XorMappedAddress = 0x0020,
32 | Padding = 0x0026,
33 | ResponsePort = 0x0027,
34 | PasswordAlgorithms = 0x8002,
35 | AlternateDomain = 0x8003,
36 | Software = 0x8022,
37 | AlternateServer = 0x8023,
38 | CacheTimeout = 0x8027,
39 | Fingerprint = 0x8028,
40 | ResponseOrigin = 0x802B,
41 | OtherAddress = 0x802C,
42 | }
43 |
--------------------------------------------------------------------------------
/src/STUN/Enums/BindingTestResult.cs:
--------------------------------------------------------------------------------
1 | namespace STUN.Enums;
2 |
3 | public enum BindingTestResult
4 | {
5 | Unknown,
6 | UnsupportedServer,
7 | Success,
8 | Fail
9 | }
10 |
--------------------------------------------------------------------------------
/src/STUN/Enums/Class.cs:
--------------------------------------------------------------------------------
1 | namespace STUN.Enums;
2 |
3 | internal enum Class : ushort
4 | {
5 | Request = 0b00000_0_000_0_0000,
6 | Indication = 0b00000_0_000_1_0000,
7 | SuccessResponse = 0b00000_1_000_0_0000,
8 | ErrorResponse = 0b00000_1_000_1_0000,
9 | }
10 |
--------------------------------------------------------------------------------
/src/STUN/Enums/FilteringBehavior.cs:
--------------------------------------------------------------------------------
1 | namespace STUN.Enums;
2 |
3 | public enum FilteringBehavior
4 | {
5 | Unknown,
6 | UnsupportedServer,
7 | EndpointIndependent,
8 | AddressDependent,
9 | AddressAndPortDependent,
10 |
11 | ///
12 | /// Filtering test applies only to UDP.
13 | ///
14 | None
15 | }
16 |
--------------------------------------------------------------------------------
/src/STUN/Enums/IpFamily.cs:
--------------------------------------------------------------------------------
1 | namespace STUN.Enums;
2 |
3 | ///
4 | /// https://tools.ietf.org/html/rfc5389#section-15.1
5 | ///
6 | public enum IpFamily : byte
7 | {
8 | IPv4 = 0x01,
9 | IPv6 = 0x02
10 | }
11 |
--------------------------------------------------------------------------------
/src/STUN/Enums/MappingBehavior.cs:
--------------------------------------------------------------------------------
1 | namespace STUN.Enums;
2 |
3 | public enum MappingBehavior
4 | {
5 | Unknown,
6 | UnsupportedServer,
7 | Direct,
8 | EndpointIndependent,
9 | AddressDependent,
10 | AddressAndPortDependent,
11 | Fail
12 | }
13 |
--------------------------------------------------------------------------------
/src/STUN/Enums/Method.cs:
--------------------------------------------------------------------------------
1 | namespace STUN.Enums;
2 |
3 | internal enum Method : ushort
4 | {
5 | Binding = 0b00000_0_000_0_0001,
6 | SharedSecret = 0b00000_0_000_0_0010,
7 | }
8 |
--------------------------------------------------------------------------------
/src/STUN/Enums/NatType.cs:
--------------------------------------------------------------------------------
1 | namespace STUN.Enums;
2 |
3 | ///
4 | /// https://tools.ietf.org/html/rfc3489#section-5
5 | /// https://tools.ietf.org/html/rfc3489#section-10.1
6 | ///
7 | public enum NatType
8 | {
9 | ///
10 | /// Unknown
11 | ///
12 | Unknown,
13 |
14 | ///
15 | /// Server is not unsupported for testing NAT type
16 | ///
17 | UnsupportedServer,
18 |
19 | ///
20 | /// UDP is always blocked.
21 | ///
22 | UdpBlocked,
23 |
24 | ///
25 | /// No NAT, public IP, no firewall.
26 | ///
27 | OpenInternet,
28 |
29 | ///
30 | /// No NAT, public IP, but symmetric UDP firewall.
31 | ///
32 | SymmetricUdpFirewall,
33 |
34 | ///
35 | /// A full cone NAT is one where all requests from the same internal IP address and port are
36 | /// mapped to the same external IP address and port. Furthermore, any external host can send
37 | /// a packet to the internal host, by sending a packet to the mapped external address.
38 | ///
39 | FullCone,
40 |
41 | ///
42 | /// A restricted cone NAT is one where all requests from the same internal IP address and
43 | /// port are mapped to the same external IP address and port. Unlike a full cone NAT, an external
44 | /// host (with IP address X) can send a packet to the internal host only if the internal host
45 | /// had previously sent a packet to IP address X.
46 | ///
47 | RestrictedCone,
48 |
49 | ///
50 | /// A port restricted cone NAT is like a restricted cone NAT, but the restriction
51 | /// includes port numbers. Specifically, an external host can send a packet, with source IP
52 | /// address X and source port P, to the internal host only if the internal host had previously
53 | /// sent a packet to IP address X and port P.
54 | ///
55 | PortRestrictedCone,
56 |
57 | ///
58 | /// A symmetric NAT is one where all requests from the same internal IP address and port,
59 | /// to a specific destination IP address and port, are mapped to the same external IP address and
60 | /// port. If the same host sends a packet with the same source address and port, but to
61 | /// a different destination, a different mapping is used. Furthermore, only the external host that
62 | /// receives a packet can send a UDP packet back to the internal host.
63 | ///
64 | Symmetric
65 | }
66 |
--------------------------------------------------------------------------------
/src/STUN/Enums/ProxyType.cs:
--------------------------------------------------------------------------------
1 | namespace STUN.Enums;
2 |
3 | public enum ProxyType
4 | {
5 | Plain = 0,
6 | Socks5 = 1
7 | }
8 |
--------------------------------------------------------------------------------
/src/STUN/Enums/StunMessageType.cs:
--------------------------------------------------------------------------------
1 | namespace STUN.Enums;
2 |
3 | ///
4 | /// This enum specifies STUN message type.
5 | ///
6 | ///
7 | /// https://tools.ietf.org/html/rfc5389#section-6
8 | ///
9 | public enum StunMessageType : ushort
10 | {
11 | ///
12 | /// STUN message is binding request.
13 | ///
14 | BindingRequest = Class.Request | Method.Binding,
15 |
16 | ///
17 | /// STUN message is binding request success response.
18 | ///
19 | BindingResponse = Class.SuccessResponse | Method.Binding,
20 |
21 | ///
22 | /// STUN message is binding request error response.
23 | ///
24 | BindingErrorResponse = Class.ErrorResponse | Method.Binding,
25 |
26 | ///
27 | /// STUN message is "shared secret" request.
28 | ///
29 | SharedSecretRequest = Class.Request | Method.SharedSecret,
30 |
31 | ///
32 | /// STUN message is "shared secret" request success response.
33 | ///
34 | SharedSecretResponse = Class.SuccessResponse | Method.SharedSecret,
35 |
36 | ///
37 | /// STUN message is "shared secret" request error response.
38 | ///
39 | SharedSecretErrorResponse = Class.ErrorResponse | Method.SharedSecret,
40 | }
41 |
--------------------------------------------------------------------------------
/src/STUN/Enums/TransportType.cs:
--------------------------------------------------------------------------------
1 | namespace STUN.Enums;
2 |
3 | public enum TransportType
4 | {
5 | Udp,
6 | Tcp,
7 | Tls,
8 | Dtls,
9 | }
10 |
--------------------------------------------------------------------------------
/src/STUN/HostnameEndpoint.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics.CodeAnalysis;
2 | using System.Net;
3 | using System.Net.Sockets;
4 |
5 | namespace STUN;
6 |
7 | public class HostnameEndpoint
8 | {
9 | public string Hostname { get; }
10 | public ushort Port { get; }
11 |
12 | private HostnameEndpoint(string host, ushort port)
13 | {
14 | Hostname = host;
15 | Port = port;
16 | }
17 |
18 | public static bool TryParse(string s, [NotNullWhen(true)] out HostnameEndpoint? result, ushort defaultPort = 0)
19 | {
20 | result = null;
21 | if (string.IsNullOrEmpty(s))
22 | {
23 | return false;
24 | }
25 |
26 | int hostLength = s.Length;
27 | int pos = s.LastIndexOf(':');
28 |
29 | if (pos > 0)
30 | {
31 | if (s[pos - 1] is ']')
32 | {
33 | hostLength = pos;
34 | }
35 | else if (s.AsSpan(0, pos).LastIndexOf(':') is -1)
36 | {
37 | hostLength = pos;
38 | }
39 | }
40 |
41 | string host = s[..hostLength];
42 | UriHostNameType type = Uri.CheckHostName(host);
43 | switch (type)
44 | {
45 | case UriHostNameType.Dns:
46 | case UriHostNameType.IPv4:
47 | case UriHostNameType.IPv6:
48 | {
49 | break;
50 | }
51 | default:
52 | {
53 | return false;
54 | }
55 | }
56 |
57 | if (hostLength == s.Length || ushort.TryParse(s.AsSpan(hostLength + 1), out defaultPort))
58 | {
59 | result = new HostnameEndpoint(host, defaultPort);
60 | return true;
61 | }
62 |
63 | return false;
64 | }
65 |
66 | public override string ToString()
67 | {
68 | if (IPAddress.TryParse(Hostname, out IPAddress? ip) && ip.AddressFamily is AddressFamily.InterNetworkV6)
69 | {
70 | return $@"[{ip}]:{Port}";
71 | }
72 |
73 | return $@"{Hostname}:{Port}";
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/STUN/Messages/StunAttribute.cs:
--------------------------------------------------------------------------------
1 | using Microsoft;
2 | using STUN.Enums;
3 | using STUN.Messages.StunAttributeValues;
4 | using System.Buffers.Binary;
5 | using System.Security.Cryptography;
6 |
7 | namespace STUN.Messages;
8 |
9 | ///
10 | /// https://tools.ietf.org/html/rfc5389#section-15
11 | ///
12 | public class StunAttribute
13 | {
14 | /*
15 | Length 是大端
16 | 必须4字节对齐
17 | 对齐的字节可以是任意值
18 | 0 1 2 3
19 | 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
20 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
21 | | Type | Length |
22 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
23 | | Value (variable) ....
24 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
25 | */
26 |
27 | public AttributeType Type { get; set; } = AttributeType.Useless;
28 |
29 | public ushort Length { get; set; }
30 |
31 | public ushort RealLength => (ushort)(Type == AttributeType.Useless ? 0 : 4 + Length + (4 - Length % 4) % 4);
32 |
33 | public IStunAttributeValue Value { get; set; } = new UselessStunAttributeValue();
34 |
35 | public int WriteTo(Span buffer)
36 | {
37 | int length = 4 + Length;
38 | int n = (4 - length % 4) % 4; // 填充的字节数
39 | int totalLength = length + n;
40 |
41 | Requires.Range(buffer.Length >= totalLength, nameof(buffer));
42 |
43 | BinaryPrimitives.WriteUInt16BigEndian(buffer, (ushort)Type);
44 | BinaryPrimitives.WriteUInt16BigEndian(buffer[2..], Length);
45 | int valueLength = Value.WriteTo(buffer[4..]);
46 |
47 | Assumes.True(valueLength == Length);
48 |
49 | RandomNumberGenerator.Fill(buffer.Slice(length, n));
50 |
51 | return totalLength;
52 | }
53 |
54 | ///
55 | /// Parse 成功字节,0 则表示 Parse 失败
56 | ///
57 | public int TryParse(ReadOnlySpan buffer, ReadOnlySpan magicCookieAndTransactionId)
58 | {
59 | if (buffer.Length < 4)
60 | {
61 | return default;
62 | }
63 |
64 | Type = (AttributeType)BinaryPrimitives.ReadUInt16BigEndian(buffer);
65 |
66 | Length = BinaryPrimitives.ReadUInt16BigEndian(buffer[2..]);
67 |
68 | if (buffer.Length < 4 + Length)
69 | {
70 | return default;
71 | }
72 |
73 | ReadOnlySpan value = buffer.Slice(4, Length);
74 |
75 | IStunAttributeValue t = Type switch
76 | {
77 | AttributeType.MappedAddress => new MappedAddressStunAttributeValue(),
78 | AttributeType.XorMappedAddress => new XorMappedAddressStunAttributeValue(magicCookieAndTransactionId),
79 | AttributeType.ResponseAddress => new ResponseAddressStunAttributeValue(),
80 | AttributeType.ChangeRequest => new ChangeRequestStunAttributeValue(),
81 | AttributeType.SourceAddress => new SourceAddressStunAttributeValue(),
82 | AttributeType.ChangedAddress => new ChangedAddressStunAttributeValue(),
83 | AttributeType.OtherAddress => new OtherAddressStunAttributeValue(),
84 | AttributeType.ReflectedFrom => new ReflectedFromStunAttributeValue(),
85 | AttributeType.ErrorCode => new ErrorCodeStunAttributeValue(),
86 | _ => new UselessStunAttributeValue()
87 | };
88 | if (t.TryParse(value))
89 | {
90 | Value = t;
91 | }
92 |
93 | return 4 + Length + (4 - Length % 4) % 4; // 对齐
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/src/STUN/Messages/StunAttributeValues/AddressStunAttributeValue.cs:
--------------------------------------------------------------------------------
1 | using Microsoft;
2 | using STUN.Enums;
3 | using System.Buffers.Binary;
4 | using System.Net;
5 | using System.Net.Sockets;
6 |
7 | namespace STUN.Messages.StunAttributeValues;
8 |
9 | ///
10 | /// https://tools.ietf.org/html/rfc5389#section-15.1
11 | ///
12 | public abstract class AddressStunAttributeValue : IStunAttributeValue
13 | {
14 | public IpFamily Family { get; set; }
15 |
16 | public ushort Port { get; set; }
17 |
18 | public IPAddress? Address { get; set; }
19 |
20 | public virtual int WriteTo(Span buffer)
21 | {
22 | Verify.Operation(Address is not null, @"You should set Address info!");
23 |
24 | Requires.Range(buffer.Length >= 4 + 4, nameof(buffer));
25 |
26 | buffer[0] = 0;
27 | buffer[1] = (byte)Family;
28 | BinaryPrimitives.WriteUInt16BigEndian(buffer[2..], Port);
29 | Requires.Range(Address.TryWriteBytes(buffer[4..], out int bytesWritten), nameof(buffer));
30 |
31 | return 4 + bytesWritten;
32 | }
33 |
34 | public virtual bool TryParse(ReadOnlySpan buffer)
35 | {
36 | int length = 4;
37 |
38 | if (buffer.Length < length)
39 | {
40 | return false;
41 | }
42 |
43 | Family = (IpFamily)buffer[1];
44 |
45 | switch (Family)
46 | {
47 | case IpFamily.IPv4:
48 | length += 4;
49 | break;
50 | case IpFamily.IPv6:
51 | length += 16;
52 | break;
53 | default:
54 | return false;
55 | }
56 |
57 | if (buffer.Length != length)
58 | {
59 | return false;
60 | }
61 |
62 | Port = BinaryPrimitives.ReadUInt16BigEndian(buffer[2..]);
63 |
64 | Address = new IPAddress(buffer[4..]);
65 |
66 | return true;
67 | }
68 |
69 | public override string? ToString()
70 | {
71 | return Address?.AddressFamily switch
72 | {
73 | AddressFamily.InterNetwork => $@"{Address}:{Port}",
74 | AddressFamily.InterNetworkV6 => $@"[{Address}]:{Port}",
75 | _ => base.ToString()
76 | };
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/STUN/Messages/StunAttributeValues/ChangeRequestStunAttributeValue.cs:
--------------------------------------------------------------------------------
1 | using Microsoft;
2 |
3 | namespace STUN.Messages.StunAttributeValues;
4 |
5 | ///
6 | /// https://tools.ietf.org/html/rfc5780#section-7.2
7 | ///
8 | public class ChangeRequestStunAttributeValue : IStunAttributeValue
9 | {
10 | public bool ChangeIp { get; set; }
11 |
12 | public bool ChangePort { get; set; }
13 |
14 | public int WriteTo(Span buffer)
15 | {
16 | Requires.Range(buffer.Length >= 4, nameof(buffer));
17 |
18 | buffer[0] = buffer[1] = buffer[2] = 0;
19 |
20 | buffer[3] = (byte)(Convert.ToInt32(ChangeIp) << 2 | Convert.ToInt32(ChangePort) << 1);
21 |
22 | return 4;
23 | }
24 |
25 | public bool TryParse(ReadOnlySpan buffer)
26 | {
27 | if (buffer.Length != 4)
28 | {
29 | return false;
30 | }
31 |
32 | ChangeIp = Convert.ToBoolean(buffer[3] >> 2 & 1);
33 | ChangePort = Convert.ToBoolean(buffer[3] >> 1 & 1);
34 |
35 | return true;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/STUN/Messages/StunAttributeValues/ChangedAddressStunAttributeValue.cs:
--------------------------------------------------------------------------------
1 | namespace STUN.Messages.StunAttributeValues;
2 |
3 | ///
4 | /// https://tools.ietf.org/html/rfc3489#section-11.2.3
5 | ///
6 | public class ChangedAddressStunAttributeValue : AddressStunAttributeValue;
7 |
--------------------------------------------------------------------------------
/src/STUN/Messages/StunAttributeValues/ErrorCodeStunAttributeValue.cs:
--------------------------------------------------------------------------------
1 | using Microsoft;
2 | using System.Text;
3 |
4 | namespace STUN.Messages.StunAttributeValues;
5 |
6 | ///
7 | /// https://tools.ietf.org/html/rfc5389#section-15.6
8 | ///
9 | public class ErrorCodeStunAttributeValue : IStunAttributeValue
10 | {
11 | public ushort ErrorCode { get; set; }
12 | public string ReasonPhrase { get; set; } = string.Empty;
13 |
14 | public byte Class => (byte)(ErrorCode % 1000 / 100);
15 | public byte Number => (byte)(ErrorCode % 100);
16 |
17 | public const int MaxReasonPhraseBytesLength = 762;
18 |
19 | public int WriteTo(Span buffer)
20 | {
21 | Requires.Range(buffer.Length >= 4, nameof(buffer));
22 |
23 | buffer[0] = buffer[1] = 0;
24 | buffer[2] = Class;
25 | buffer[3] = Number;
26 |
27 | int length = Encoding.UTF8.GetBytes(ReasonPhrase, buffer[4..]);
28 |
29 | return 4 + Math.Min(length, MaxReasonPhraseBytesLength);
30 | }
31 |
32 | public bool TryParse(ReadOnlySpan buffer)
33 | {
34 | if (buffer.Length is < 4 or > (4 + MaxReasonPhraseBytesLength))
35 | {
36 | return false;
37 | }
38 |
39 | byte @class = (byte)(buffer[2] & 0b111);
40 | ushort number = Math.Min(buffer[3], (ushort)99);
41 |
42 | ErrorCode = (ushort)(@class * 100 + number);
43 |
44 | ReasonPhrase = Encoding.UTF8.GetString(buffer[4..]);
45 |
46 | return true;
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/STUN/Messages/StunAttributeValues/IStunAttributeValue.cs:
--------------------------------------------------------------------------------
1 | namespace STUN.Messages.StunAttributeValues;
2 |
3 | public interface IStunAttributeValue
4 | {
5 | int WriteTo(Span buffer);
6 |
7 | bool TryParse(ReadOnlySpan buffer);
8 | }
9 |
--------------------------------------------------------------------------------
/src/STUN/Messages/StunAttributeValues/MappedAddressStunAttributeValue.cs:
--------------------------------------------------------------------------------
1 | namespace STUN.Messages.StunAttributeValues;
2 |
3 | ///
4 | /// https://tools.ietf.org/html/rfc5389#section-15.1
5 | ///
6 | public class MappedAddressStunAttributeValue : AddressStunAttributeValue;
7 |
--------------------------------------------------------------------------------
/src/STUN/Messages/StunAttributeValues/OtherAddressStunAttributeValue.cs:
--------------------------------------------------------------------------------
1 | namespace STUN.Messages.StunAttributeValues;
2 |
3 | ///
4 | /// https://tools.ietf.org/html/rfc5780#section-7.4
5 | ///
6 | public class OtherAddressStunAttributeValue : AddressStunAttributeValue;
7 |
--------------------------------------------------------------------------------
/src/STUN/Messages/StunAttributeValues/ReflectedFromStunAttributeValue.cs:
--------------------------------------------------------------------------------
1 | namespace STUN.Messages.StunAttributeValues;
2 |
3 | ///
4 | /// https://tools.ietf.org/html/rfc3489#section-11.2.11
5 | ///
6 | public class ReflectedFromStunAttributeValue : AddressStunAttributeValue;
7 |
--------------------------------------------------------------------------------
/src/STUN/Messages/StunAttributeValues/ResponseAddressStunAttributeValue.cs:
--------------------------------------------------------------------------------
1 | namespace STUN.Messages.StunAttributeValues;
2 |
3 | ///
4 | /// https://tools.ietf.org/html/rfc3489#section-11.2.2
5 | ///
6 | public class ResponseAddressStunAttributeValue : AddressStunAttributeValue;
7 |
--------------------------------------------------------------------------------
/src/STUN/Messages/StunAttributeValues/SourceAddressStunAttributeValue.cs:
--------------------------------------------------------------------------------
1 | namespace STUN.Messages.StunAttributeValues;
2 |
3 | ///
4 | /// https://tools.ietf.org/html/rfc3489#section-11.2.5
5 | ///
6 | public class SourceAddressStunAttributeValue : AddressStunAttributeValue;
7 |
--------------------------------------------------------------------------------
/src/STUN/Messages/StunAttributeValues/UnknownStunAttributeValue.cs:
--------------------------------------------------------------------------------
1 | using Microsoft;
2 | using STUN.Enums;
3 | using System.Buffers.Binary;
4 |
5 | namespace STUN.Messages.StunAttributeValues;
6 |
7 | ///
8 | /// https://tools.ietf.org/html/rfc5389#section-15.9
9 | ///
10 | public class UnknownStunAttributeValue : IStunAttributeValue
11 | {
12 | public List Types { get; } = new();
13 |
14 | public int WriteTo(Span buffer)
15 | {
16 | int size = Types.Count << 1;
17 | Requires.Range(buffer.Length >= size, nameof(buffer));
18 |
19 | foreach (AttributeType attributeType in Types)
20 | {
21 | BinaryPrimitives.WriteUInt16BigEndian(buffer, (ushort)attributeType);
22 | buffer = buffer[sizeof(ushort)..];
23 | }
24 |
25 | return size;
26 | }
27 |
28 | public bool TryParse(ReadOnlySpan buffer)
29 | {
30 | if (buffer.Length < 2 || (buffer.Length & 1) == 1)
31 | {
32 | return false;
33 | }
34 |
35 | Types.Clear();
36 | while (!buffer.IsEmpty)
37 | {
38 | ushort type = BinaryPrimitives.ReadUInt16BigEndian(buffer);
39 | Types.Add((AttributeType)type);
40 | buffer = buffer[sizeof(ushort)..];
41 | }
42 |
43 | return true;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/STUN/Messages/StunAttributeValues/UselessStunAttributeValue.cs:
--------------------------------------------------------------------------------
1 | namespace STUN.Messages.StunAttributeValues;
2 |
3 | ///
4 | /// 无法理解的属性
5 | ///
6 | public class UselessStunAttributeValue : IStunAttributeValue
7 | {
8 | public int WriteTo(Span buffer)
9 | {
10 | throw new NotSupportedException();
11 | }
12 |
13 | public bool TryParse(ReadOnlySpan buffer)
14 | {
15 | return true;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/STUN/Messages/StunAttributeValues/XorMappedAddressStunAttributeValue.cs:
--------------------------------------------------------------------------------
1 | using Microsoft;
2 | using System.Buffers.Binary;
3 | using System.Net;
4 |
5 | namespace STUN.Messages.StunAttributeValues;
6 |
7 | ///
8 | /// https://tools.ietf.org/html/rfc5389#section-15.2
9 | ///
10 | public class XorMappedAddressStunAttributeValue : AddressStunAttributeValue
11 | {
12 | private readonly byte[] _magicCookieAndTransactionId;
13 |
14 | public XorMappedAddressStunAttributeValue(ReadOnlySpan magicCookieAndTransactionId)
15 | {
16 | Requires.Argument(magicCookieAndTransactionId.Length == 16, nameof(magicCookieAndTransactionId), @"Wrong Transaction ID length");
17 | _magicCookieAndTransactionId = magicCookieAndTransactionId.ToArray();
18 | }
19 |
20 | public override int WriteTo(Span buffer)
21 | {
22 | Verify.Operation(Address is not null, @"You should set Address info!");
23 |
24 | Requires.Range(buffer.Length >= 4 + 4, nameof(buffer));
25 |
26 | buffer[0] = 0;
27 | buffer[1] = (byte)Family;
28 | BinaryPrimitives.WriteUInt16BigEndian(buffer[2..], Xor(Port));
29 | Requires.Range(Xor(Address).TryWriteBytes(buffer[4..], out int bytesWritten), nameof(buffer));
30 |
31 | return 4 + bytesWritten;
32 | }
33 |
34 | public override bool TryParse(ReadOnlySpan buffer)
35 | {
36 | if (!base.TryParse(buffer))
37 | {
38 | return false;
39 | }
40 |
41 | Assumes.NotNull(Address);
42 |
43 | Port = Xor(Port);
44 |
45 | Address = Xor(Address);
46 |
47 | return true;
48 | }
49 |
50 | private ushort Xor(ushort port)
51 | {
52 | Span span = stackalloc byte[2];
53 | BinaryPrimitives.WriteUInt16BigEndian(span, port);
54 | span[0] ^= _magicCookieAndTransactionId[0];
55 | span[1] ^= _magicCookieAndTransactionId[1];
56 | return BinaryPrimitives.ReadUInt16BigEndian(span);
57 | }
58 |
59 | private IPAddress Xor(IPAddress address)
60 | {
61 | Span b = stackalloc byte[16];
62 | Assumes.True(address.TryWriteBytes(b, out int bytesWritten));
63 |
64 | for (int i = 0; i < bytesWritten; ++i)
65 | {
66 | b[i] ^= _magicCookieAndTransactionId[i];
67 | }
68 |
69 | return new IPAddress(b[..bytesWritten]);
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/STUN/Messages/StunMessage5389.cs:
--------------------------------------------------------------------------------
1 | using Microsoft;
2 | using STUN.Enums;
3 | using System.Buffers;
4 | using System.Buffers.Binary;
5 | using System.Diagnostics;
6 | using System.Security.Cryptography;
7 |
8 | namespace STUN.Messages;
9 |
10 | ///
11 | /// https://tools.ietf.org/html/rfc5389#section-6
12 | ///
13 | public class StunMessage5389
14 | {
15 | #region Header
16 |
17 | private const int SizeOfMessageType = sizeof(StunMessageType);
18 | private const int SizeOfLength = sizeof(ushort);
19 | private const int SizeOfMagicCookie = sizeof(uint);
20 | private const int SizeOfTransactionId = 12;
21 | public const int HeaderLength = SizeOfMessageType + SizeOfLength + SizeOfMagicCookie + SizeOfTransactionId;
22 |
23 | public StunMessageType StunMessageType { get; set; }
24 |
25 | public uint MagicCookie { get; set; }
26 |
27 | public byte[] TransactionId { get; }
28 |
29 | #endregion
30 |
31 | public IEnumerable Attributes { get; set; }
32 |
33 | public ushort MessageLength => (ushort)Attributes.Sum(x => x.RealLength);
34 | public int Length => HeaderLength + MessageLength;
35 |
36 | public StunMessage5389()
37 | {
38 | Attributes = Array.Empty();
39 | StunMessageType = StunMessageType.BindingRequest;
40 | MagicCookie = 0x2112A442;
41 | TransactionId = new byte[SizeOfTransactionId];
42 | RandomNumberGenerator.Fill(TransactionId);
43 | }
44 |
45 | public int WriteTo(Span buffer)
46 | {
47 | ushort messageLength = MessageLength;
48 | int length = Length;
49 | Requires.Range(buffer.Length >= length, nameof(buffer));
50 |
51 | BinaryPrimitives.WriteUInt16BigEndian(buffer, (ushort)StunMessageType);
52 | BinaryPrimitives.WriteUInt16BigEndian(buffer[SizeOfMessageType..], messageLength);
53 | BinaryPrimitives.WriteUInt32BigEndian(buffer[(SizeOfMessageType + SizeOfLength)..], MagicCookie);
54 | TransactionId.CopyTo(buffer[(SizeOfMessageType + SizeOfLength + SizeOfMagicCookie)..]);
55 |
56 | buffer = buffer[HeaderLength..];
57 | foreach (StunAttribute attribute in Attributes)
58 | {
59 | int outLength = attribute.WriteTo(buffer);
60 | buffer = buffer[outLength..];
61 | }
62 |
63 | return length;
64 | }
65 |
66 | public bool TryParse(ReadOnlyMemory buffer)
67 | {
68 | ReadOnlySequence sequence = new(buffer);
69 | return TryParse(ref sequence);
70 | }
71 |
72 | public bool TryParse(ref ReadOnlySequence sequence)
73 | {
74 | if (sequence.Length < HeaderLength)
75 | {
76 | return false; // Check length
77 | }
78 |
79 | SequenceReader reader = new(sequence);
80 |
81 | if (!reader.TryReadBigEndian(out short typeValue))
82 | {
83 | throw Assumes.NotReachable();
84 | }
85 |
86 | StunMessageType type = (StunMessageType)(ushort)(typeValue & 0b0011_1111_1111_1111);
87 |
88 | if (!Enum.IsDefined(type))
89 | {
90 | return false;
91 | }
92 |
93 | StunMessageType = type;
94 |
95 | if (!reader.TryReadBigEndian(out short lengthValue))
96 | {
97 | throw Assumes.NotReachable();
98 | }
99 |
100 | ushort length = (ushort)lengthValue;
101 |
102 | if (sequence.Length - HeaderLength < length)
103 | {
104 | return false; // Check length
105 | }
106 |
107 | if (!reader.TryReadBigEndian(out int magicCookie))
108 | {
109 | throw Assumes.NotReachable();
110 | }
111 |
112 | MagicCookie = (uint)magicCookie;
113 |
114 | reader.UnreadSequence.Slice(0, SizeOfTransactionId).CopyTo(TransactionId);
115 | reader.Advance(SizeOfTransactionId);
116 |
117 | byte[] tempBuffer = ArrayPool.Shared.Rent(length + SizeOfMagicCookie + SizeOfTransactionId);
118 | try
119 | {
120 | reader.UnreadSequence.Slice(0, length).CopyTo(tempBuffer);
121 | reader.Advance(length);
122 | sequence.Slice(SizeOfMessageType + SizeOfLength, SizeOfMagicCookie + SizeOfTransactionId).CopyTo(tempBuffer.AsSpan(length));
123 |
124 | List list = new();
125 |
126 | Span attributeBuffer = tempBuffer.AsSpan(0, length);
127 | ReadOnlySpan magicCookieAndTransactionId = tempBuffer.AsSpan(length, SizeOfMagicCookie + SizeOfTransactionId);
128 |
129 | while (attributeBuffer.Length > default(int))
130 | {
131 | StunAttribute attribute = new();
132 | int offset = attribute.TryParse(attributeBuffer, magicCookieAndTransactionId);
133 | if (offset <= default(int))
134 | {
135 | Debug.WriteLine($@"[Warning] Ignore wrong attribute: {Convert.ToHexString(attributeBuffer)}");
136 | break;
137 | }
138 |
139 | list.Add(attribute);
140 | attributeBuffer = attributeBuffer[offset..];
141 | }
142 |
143 | Attributes = list;
144 | }
145 | finally
146 | {
147 | ArrayPool.Shared.Return(tempBuffer);
148 | }
149 |
150 | sequence = reader.UnreadSequence;
151 | return true;
152 | }
153 |
154 | public bool IsSameTransaction(StunMessage5389 other)
155 | {
156 | return MagicCookie == other.MagicCookie && TransactionId.AsSpan().SequenceEqual(other.TransactionId);
157 | }
158 | }
159 |
--------------------------------------------------------------------------------
/src/STUN/Messages/StunResponse.cs:
--------------------------------------------------------------------------------
1 | using System.Net;
2 |
3 | namespace STUN.Messages;
4 |
5 | public record StunResponse(StunMessage5389 Message, IPEndPoint Remote, IPEndPoint Local)
6 | {
7 | public StunMessage5389 Message { get; set; } = Message;
8 | public IPEndPoint Remote { get; set; } = Remote;
9 | public IPEndPoint Local { get; set; } = Local;
10 | }
11 |
--------------------------------------------------------------------------------
/src/STUN/Proxy/DirectTcpProxy.cs:
--------------------------------------------------------------------------------
1 | using Microsoft;
2 | using Pipelines.Extensions;
3 | using System.IO.Pipelines;
4 | using System.Net;
5 | using System.Net.Sockets;
6 |
7 | namespace STUN.Proxy;
8 |
9 | public class DirectTcpProxy : ITcpProxy, IDisposableObservable
10 | {
11 | public IPEndPoint? CurrentLocalEndPoint
12 | {
13 | get
14 | {
15 | Verify.NotDisposed(this);
16 | return TcpClient?.Client.LocalEndPoint as IPEndPoint;
17 | }
18 | }
19 |
20 | protected TcpClient? TcpClient;
21 |
22 | public virtual async ValueTask ConnectAsync(IPEndPoint local, IPEndPoint dst, CancellationToken cancellationToken = default)
23 | {
24 | Verify.NotDisposed(this);
25 | Requires.NotNull(local, nameof(local));
26 | Requires.NotNull(dst, nameof(dst));
27 |
28 | await CloseAsync(cancellationToken);
29 |
30 | TcpClient = new TcpClient(local) { NoDelay = true };
31 | await TcpClient.ConnectAsync(dst, cancellationToken);
32 |
33 | return TcpClient.Client.AsDuplexPipe();
34 | }
35 |
36 | public ValueTask CloseAsync(CancellationToken cancellationToken = default)
37 | {
38 | Verify.NotDisposed(this);
39 |
40 | CloseClient();
41 |
42 | return default;
43 | }
44 |
45 | protected virtual void CloseClient()
46 | {
47 | if (TcpClient is null)
48 | {
49 | return;
50 | }
51 |
52 | try
53 | {
54 | TcpClient.Client.Close(0);
55 | }
56 | finally
57 | {
58 | TcpClient.Dispose();
59 | TcpClient = default;
60 | }
61 | }
62 |
63 | public bool IsDisposed { get; private set; }
64 |
65 | public void Dispose()
66 | {
67 | IsDisposed = true;
68 |
69 | CloseClient();
70 |
71 | GC.SuppressFinalize(this);
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/STUN/Proxy/ITcpProxy.cs:
--------------------------------------------------------------------------------
1 | using System.IO.Pipelines;
2 | using System.Net;
3 |
4 | namespace STUN.Proxy;
5 |
6 | public interface ITcpProxy : IDisposable
7 | {
8 | IPEndPoint? CurrentLocalEndPoint { get; }
9 |
10 | ValueTask ConnectAsync(IPEndPoint local, IPEndPoint dst, CancellationToken cancellationToken = default);
11 | ValueTask CloseAsync(CancellationToken cancellationToken = default);
12 | }
13 |
--------------------------------------------------------------------------------
/src/STUN/Proxy/IUdpProxy.cs:
--------------------------------------------------------------------------------
1 | using System.Net;
2 | using System.Net.Sockets;
3 |
4 | namespace STUN.Proxy;
5 |
6 | public interface IUdpProxy : IDisposable
7 | {
8 | Socket Client { get; }
9 | ValueTask ConnectAsync(CancellationToken cancellationToken = default);
10 | ValueTask CloseAsync(CancellationToken cancellationToken = default);
11 | ValueTask ReceiveMessageFromAsync(Memory buffer, SocketFlags socketFlags, EndPoint remoteEndPoint, CancellationToken cancellationToken = default);
12 | ValueTask SendToAsync(ReadOnlyMemory buffer, SocketFlags socketFlags, EndPoint remoteEP, CancellationToken cancellationToken = default);
13 | }
14 |
--------------------------------------------------------------------------------
/src/STUN/Proxy/NoneUdpProxy.cs:
--------------------------------------------------------------------------------
1 | using Microsoft;
2 | using System.Net;
3 | using System.Net.Sockets;
4 |
5 | namespace STUN.Proxy;
6 |
7 | public class NoneUdpProxy : IUdpProxy
8 | {
9 | public Socket Client { get; }
10 |
11 | public NoneUdpProxy(IPEndPoint localEndPoint)
12 | {
13 | Requires.NotNull(localEndPoint, nameof(localEndPoint));
14 |
15 | Client = new Socket(localEndPoint.AddressFamily, SocketType.Dgram, ProtocolType.Udp);
16 | Client.Bind(localEndPoint);
17 | }
18 |
19 | public ValueTask ConnectAsync(CancellationToken cancellationToken = default)
20 | {
21 | return default;
22 | }
23 |
24 | public ValueTask CloseAsync(CancellationToken cancellationToken = default)
25 | {
26 | return default;
27 | }
28 |
29 | public ValueTask ReceiveMessageFromAsync(Memory buffer, SocketFlags socketFlags, EndPoint remoteEndPoint, CancellationToken cancellationToken = default)
30 | {
31 | return Client.ReceiveMessageFromAsync(buffer, socketFlags, remoteEndPoint, cancellationToken);
32 | }
33 |
34 | public ValueTask SendToAsync(ReadOnlyMemory buffer, SocketFlags socketFlags, EndPoint remoteEP, CancellationToken cancellationToken = default)
35 | {
36 | return Client.SendToAsync(buffer, socketFlags, remoteEP, cancellationToken);
37 | }
38 |
39 | public void Dispose()
40 | {
41 | Client.Dispose();
42 | GC.SuppressFinalize(this);
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/STUN/Proxy/ProxyFactory.cs:
--------------------------------------------------------------------------------
1 | using Microsoft;
2 | using Socks5.Models;
3 | using STUN.Enums;
4 | using System.Net;
5 |
6 | namespace STUN.Proxy;
7 |
8 | public static class ProxyFactory
9 | {
10 | public static IUdpProxy CreateProxy(ProxyType type, IPEndPoint local, Socks5CreateOption option)
11 | {
12 | switch (type)
13 | {
14 | case ProxyType.Plain:
15 | {
16 | return new NoneUdpProxy(local);
17 | }
18 | case ProxyType.Socks5:
19 | {
20 | Requires.NotNull(option, nameof(option));
21 | Requires.Argument(option.Address is not null, nameof(option), @"Proxy server is null");
22 | return new Socks5UdpProxy(local, option);
23 | }
24 | default:
25 | {
26 | throw Assumes.NotReachable();
27 | }
28 | }
29 | }
30 |
31 | public static ITcpProxy CreateProxy(TransportType transport, ProxyType type, Socks5CreateOption option, string targetHost)
32 | {
33 | switch (transport, type)
34 | {
35 | case (TransportType.Tcp, ProxyType.Plain):
36 | {
37 | return new DirectTcpProxy();
38 | }
39 | case (TransportType.Tcp, ProxyType.Socks5):
40 | {
41 | Requires.NotNull(option, nameof(option));
42 | Requires.Argument(option.Address is not null, nameof(option), @"Proxy server is null");
43 | return new Socks5TcpProxy(option);
44 | }
45 | case (TransportType.Tls, ProxyType.Plain):
46 | {
47 | return new TlsProxy(targetHost);
48 | }
49 | case (TransportType.Tls, ProxyType.Socks5):
50 | {
51 | Requires.NotNull(option, nameof(option));
52 | Requires.Argument(option.Address is not null, nameof(option), @"Proxy server is null");
53 | return new TlsOverSocks5Proxy(option, targetHost);
54 | }
55 | default:
56 | {
57 | throw new NotSupportedException();
58 | }
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/STUN/Proxy/Socks5TcpProxy.cs:
--------------------------------------------------------------------------------
1 | using Microsoft;
2 | using Socks5.Clients;
3 | using Socks5.Models;
4 | using System.IO.Pipelines;
5 | using System.Net;
6 |
7 | namespace STUN.Proxy;
8 |
9 | public class Socks5TcpProxy : ITcpProxy, IDisposableObservable
10 | {
11 | public IPEndPoint? CurrentLocalEndPoint
12 | {
13 | get
14 | {
15 | Verify.NotDisposed(this);
16 | return Socks5Client?.TcpClient.Client.LocalEndPoint as IPEndPoint;
17 | }
18 | }
19 |
20 | protected readonly Socks5CreateOption Socks5Options;
21 |
22 | protected Socks5Client? Socks5Client;
23 |
24 | public Socks5TcpProxy(Socks5CreateOption socks5Options)
25 | {
26 | Requires.NotNull(socks5Options, nameof(socks5Options));
27 | Requires.Argument(socks5Options.Address is not null, nameof(socks5Options), @"SOCKS5 address is null");
28 |
29 | Socks5Options = socks5Options;
30 | }
31 |
32 | public virtual async ValueTask ConnectAsync(IPEndPoint local, IPEndPoint dst, CancellationToken cancellationToken = default)
33 | {
34 | Verify.NotDisposed(this);
35 | Requires.NotNull(local, nameof(local));
36 | Requires.NotNull(dst, nameof(dst));
37 |
38 | await CloseAsync(cancellationToken);
39 |
40 | Socks5Client = new Socks5Client(Socks5Options);
41 |
42 | Socks5Client.TcpClient.Client.Bind(local);
43 |
44 | await Socks5Client.ConnectAsync(dst.Address, (ushort)dst.Port, cancellationToken);
45 |
46 | return Socks5Client.GetPipe();
47 | }
48 |
49 | public ValueTask CloseAsync(CancellationToken cancellationToken = default)
50 | {
51 | Verify.NotDisposed(this);
52 |
53 | CloseClient();
54 |
55 | return default;
56 | }
57 |
58 | protected virtual void CloseClient()
59 | {
60 | if (Socks5Client is null)
61 | {
62 | return;
63 | }
64 |
65 | try
66 | {
67 | Socks5Client.TcpClient.Client.Close(0);
68 | }
69 | finally
70 | {
71 | Socks5Client.Dispose();
72 | Socks5Client = default;
73 | }
74 | }
75 |
76 | public bool IsDisposed { get; private set; }
77 |
78 | public void Dispose()
79 | {
80 | IsDisposed = true;
81 |
82 | CloseClient();
83 |
84 | GC.SuppressFinalize(this);
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/src/STUN/Proxy/Socks5UdpProxy.cs:
--------------------------------------------------------------------------------
1 | using Microsoft;
2 | using Socks5.Clients;
3 | using Socks5.Enums;
4 | using Socks5.Models;
5 | using Socks5.Utils;
6 | using System.Buffers;
7 | using System.Net;
8 | using System.Net.Sockets;
9 | using System.Runtime.CompilerServices;
10 |
11 | namespace STUN.Proxy;
12 |
13 | public class Socks5UdpProxy : IUdpProxy
14 | {
15 | public Socket Client
16 | {
17 | [MethodImpl(MethodImplOptions.AggressiveInlining)]
18 | get
19 | {
20 | Verify.Operation(_socks5Client?.UdpClient is not null, @"Socks5 is not established.");
21 | return _socks5Client.UdpClient;
22 | }
23 | }
24 |
25 | private readonly Socks5CreateOption _socks5Options;
26 | private readonly IPEndPoint _localEndPoint;
27 |
28 | private Socks5Client? _socks5Client;
29 | private ServerBound _udpServerBound;
30 |
31 | public Socks5UdpProxy(IPEndPoint localEndPoint, Socks5CreateOption socks5Options)
32 | {
33 | Requires.NotNull(localEndPoint, nameof(localEndPoint));
34 | Requires.NotNull(socks5Options, nameof(socks5Options));
35 | Requires.Argument(socks5Options.Address is not null, nameof(socks5Options), @"SOCKS5 address is null");
36 |
37 | _localEndPoint = localEndPoint;
38 | _socks5Options = socks5Options;
39 | }
40 |
41 | public async ValueTask ConnectAsync(CancellationToken cancellationToken = default)
42 | {
43 | Verify.Operation(_socks5Client?.Status is not Status.Established, @"SOCKS5 client has been connected");
44 | _socks5Client?.Dispose();
45 |
46 | _socks5Client = new Socks5Client(_socks5Options);
47 | _udpServerBound = await _socks5Client.UdpAssociateAsync(_localEndPoint.Address, (ushort)_localEndPoint.Port, cancellationToken);
48 | }
49 |
50 | public ValueTask CloseAsync(CancellationToken cancellationToken = default)
51 | {
52 | if (_socks5Client is not null)
53 | {
54 | _socks5Client.Dispose();
55 | _socks5Client = null;
56 | }
57 | return default;
58 | }
59 |
60 | public async ValueTask ReceiveMessageFromAsync(Memory buffer, SocketFlags socketFlags, EndPoint remoteEndPoint, CancellationToken cancellationToken = default)
61 | {
62 | Verify.Operation(_socks5Client?.Status is Status.Established && _socks5Client.UdpClient is not null, @"Socks5 is not established.");
63 |
64 | byte[] t = ArrayPool.Shared.Rent(buffer.Length);
65 | try
66 | {
67 | if (_udpServerBound.Type is AddressType.Domain)
68 | {
69 | ThrowErrorAddressType();
70 | }
71 |
72 | IPEndPoint remote = new(_udpServerBound.Address!, _udpServerBound.Port);
73 | SocketReceiveMessageFromResult r = await _socks5Client.UdpClient.ReceiveMessageFromAsync(t, socketFlags, remote, cancellationToken);
74 | Socks5UdpReceivePacket u = Unpack.Udp(t.AsMemory(0, r.ReceivedBytes));
75 |
76 | u.Data.CopyTo(buffer);
77 |
78 | if (u.Type is AddressType.Domain)
79 | {
80 | ThrowErrorAddressType();
81 | }
82 |
83 | return new SocketReceiveMessageFromResult
84 | {
85 | ReceivedBytes = u.Data.Length,
86 | SocketFlags = r.SocketFlags,
87 | RemoteEndPoint = new IPEndPoint(u.Address!, u.Port),
88 | PacketInformation = r.PacketInformation
89 | };
90 | }
91 | finally
92 | {
93 | ArrayPool.Shared.Return(t);
94 | }
95 |
96 | static void ThrowErrorAddressType()
97 | {
98 | throw new InvalidDataException(@"Received error AddressType");
99 | }
100 | }
101 |
102 | public async ValueTask SendToAsync(ReadOnlyMemory buffer, SocketFlags socketFlags, EndPoint remoteEP, CancellationToken cancellationToken = default)
103 | {
104 | Verify.Operation(_socks5Client is not null, @"SOCKS5 client is not connected");
105 |
106 | if (remoteEP is not IPEndPoint remote)
107 | {
108 | ThrowNotSupportedException();
109 | }
110 |
111 | return await _socks5Client.SendUdpAsync(buffer, remote.Address, (ushort)remote.Port, cancellationToken);
112 |
113 | static void ThrowNotSupportedException()
114 | {
115 | throw new NotSupportedException();
116 | }
117 | }
118 |
119 | public void Dispose()
120 | {
121 | _socks5Client?.Dispose();
122 | GC.SuppressFinalize(this);
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/src/STUN/Proxy/TlsOverSocks5Proxy.cs:
--------------------------------------------------------------------------------
1 | using Pipelines.Extensions;
2 | using Socks5.Models;
3 | using System.IO.Pipelines;
4 | using System.Net;
5 | using System.Net.Security;
6 |
7 | namespace STUN.Proxy;
8 |
9 | public class TlsOverSocks5Proxy : Socks5TcpProxy
10 | {
11 | private SslStream? _tlsStream;
12 |
13 | private readonly string _targetHost;
14 |
15 | public TlsOverSocks5Proxy(Socks5CreateOption socks5Options, string targetHost) : base(socks5Options)
16 | {
17 | _targetHost = targetHost;
18 | }
19 |
20 | public override async ValueTask ConnectAsync(IPEndPoint local, IPEndPoint dst, CancellationToken cancellationToken = default)
21 | {
22 | IDuplexPipe pipe = await base.ConnectAsync(local, dst, cancellationToken);
23 |
24 | _tlsStream = new SslStream(pipe.AsStream(true));
25 |
26 | await _tlsStream.AuthenticateAsClientAsync(_targetHost);
27 |
28 | return _tlsStream.AsDuplexPipe();
29 | }
30 |
31 | protected override void CloseClient()
32 | {
33 | _tlsStream?.Dispose();
34 | base.CloseClient();
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/STUN/Proxy/TlsProxy.cs:
--------------------------------------------------------------------------------
1 | using Microsoft;
2 | using Pipelines.Extensions;
3 | using System.IO.Pipelines;
4 | using System.Net;
5 | using System.Net.Security;
6 | using System.Net.Sockets;
7 |
8 | namespace STUN.Proxy;
9 |
10 | public class TlsProxy : DirectTcpProxy
11 | {
12 | private SslStream? _tlsStream;
13 |
14 | private readonly string _targetHost;
15 |
16 | public TlsProxy(string targetHost)
17 | {
18 | _targetHost = targetHost;
19 | }
20 |
21 | public override async ValueTask ConnectAsync(IPEndPoint local, IPEndPoint dst, CancellationToken cancellationToken = default)
22 | {
23 | Verify.NotDisposed(this);
24 | Requires.NotNull(local, nameof(local));
25 | Requires.NotNull(dst, nameof(dst));
26 |
27 | await CloseAsync(cancellationToken);
28 |
29 | TcpClient = new TcpClient(local) { NoDelay = true };
30 | await TcpClient.ConnectAsync(dst, cancellationToken);
31 |
32 | _tlsStream = new SslStream(TcpClient.GetStream(), true);
33 |
34 | await _tlsStream.AuthenticateAsClientAsync(_targetHost);
35 |
36 | return _tlsStream.AsDuplexPipe();
37 | }
38 |
39 | protected override void CloseClient()
40 | {
41 | _tlsStream?.Dispose();
42 | base.CloseClient();
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/STUN/STUN.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | CS1591
5 | true
6 | Copyright © HMBSbige
7 | MIT
8 | https://github.com/HMBSbige/NatTypeTester
9 | https://github.com/HMBSbige/NatTypeTester
10 | stun;nat;rfc3489;rfc5389;rfc5780;rfc8489
11 | 8.0.2
12 | Stun.Net
13 | README.md
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/src/STUN/StunResult/ClassicStunResult.cs:
--------------------------------------------------------------------------------
1 | using STUN.Enums;
2 |
3 | namespace STUN.StunResult;
4 |
5 | public record ClassicStunResult : StunResult
6 | {
7 | public NatType NatType { get; set; } = NatType.Unknown;
8 | }
9 |
--------------------------------------------------------------------------------
/src/STUN/StunResult/StunResult.cs:
--------------------------------------------------------------------------------
1 | using System.Net;
2 |
3 | namespace STUN.StunResult;
4 |
5 | public abstract record StunResult
6 | {
7 | public IPEndPoint? PublicEndPoint { get; set; }
8 | public IPEndPoint? LocalEndPoint { get; set; }
9 | }
10 |
--------------------------------------------------------------------------------
/src/STUN/StunResult/StunResult5389.cs:
--------------------------------------------------------------------------------
1 | using STUN.Enums;
2 | using System.Net;
3 |
4 | namespace STUN.StunResult;
5 |
6 | public record StunResult5389 : StunResult
7 | {
8 | public IPEndPoint? OtherEndPoint { get; set; }
9 |
10 | public BindingTestResult BindingTestResult { get; set; } = BindingTestResult.Unknown;
11 |
12 | public MappingBehavior MappingBehavior { get; set; } = MappingBehavior.Unknown;
13 |
14 | public FilteringBehavior FilteringBehavior { get; set; } = FilteringBehavior.Unknown;
15 | }
16 |
--------------------------------------------------------------------------------
/src/STUN/StunServer.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics.CodeAnalysis;
2 | using System.Net;
3 | using System.Net.Sockets;
4 |
5 | namespace STUN;
6 |
7 | public class StunServer
8 | {
9 | public string Hostname { get; }
10 | public ushort Port { get; }
11 |
12 | public const ushort DefaultPort = 3478;
13 | public const ushort DefaultTlsPort = 5349;
14 |
15 | public StunServer()
16 | {
17 | Hostname = @"stun.syncthing.net";
18 | Port = DefaultPort;
19 | }
20 |
21 | private StunServer(string hostname, ushort port)
22 | {
23 | Hostname = hostname;
24 | Port = port;
25 | }
26 |
27 | public static bool TryParse(string s, [NotNullWhen(true)] out StunServer? result, ushort defaultPort = DefaultPort)
28 | {
29 | if (!HostnameEndpoint.TryParse(s, out HostnameEndpoint? host, defaultPort))
30 | {
31 | result = null;
32 | return false;
33 | }
34 |
35 | result = new StunServer(host.Hostname, host.Port);
36 | return true;
37 | }
38 |
39 | public override string ToString()
40 | {
41 | if (Port is DefaultPort)
42 | {
43 | return Hostname;
44 | }
45 | if (IPAddress.TryParse(Hostname, out IPAddress? ip) && ip.AddressFamily is AddressFamily.InterNetworkV6)
46 | {
47 | return $@"[{ip}]:{Port}";
48 | }
49 | return $@"{Hostname}:{Port}";
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/STUN/Utils/AttributeExtensions.cs:
--------------------------------------------------------------------------------
1 | using STUN.Enums;
2 | using STUN.Messages;
3 | using STUN.Messages.StunAttributeValues;
4 | using System.Net;
5 |
6 | namespace STUN.Utils;
7 |
8 | public static class AttributeExtensions
9 | {
10 | public static StunAttribute BuildChangeRequest(bool changeIp, bool changePort)
11 | {
12 | return new StunAttribute
13 | {
14 | Type = AttributeType.ChangeRequest,
15 | Length = 4,
16 | Value = new ChangeRequestStunAttributeValue { ChangeIp = changeIp, ChangePort = changePort }
17 | };
18 | }
19 |
20 | public static StunAttribute BuildMapping(IpFamily family, IPAddress ip, ushort port)
21 | {
22 | int length = family switch
23 | {
24 | IpFamily.IPv4 => 4,
25 | IpFamily.IPv6 => 16,
26 | _ => throw new ArgumentOutOfRangeException(nameof(family), family, null)
27 | };
28 | return new StunAttribute
29 | {
30 | Type = AttributeType.MappedAddress,
31 | Length = (ushort)(4 + length),
32 | Value = new MappedAddressStunAttributeValue
33 | {
34 | Family = family,
35 | Address = ip,
36 | Port = port
37 | }
38 | };
39 | }
40 |
41 | public static StunAttribute BuildChangeAddress(IpFamily family, IPAddress ip, ushort port)
42 | {
43 | int length = family switch
44 | {
45 | IpFamily.IPv4 => 4,
46 | IpFamily.IPv6 => 16,
47 | _ => throw new ArgumentOutOfRangeException(nameof(family), family, null)
48 | };
49 | return new StunAttribute
50 | {
51 | Type = AttributeType.ChangedAddress,
52 | Length = (ushort)(4 + length),
53 | Value = new ChangedAddressStunAttributeValue
54 | {
55 | Family = family,
56 | Address = ip,
57 | Port = port
58 | }
59 | };
60 | }
61 |
62 | public static IPEndPoint? GetMappedAddressAttribute(this StunMessage5389 response)
63 | {
64 | StunAttribute? mappedAddressAttribute = response.Attributes.FirstOrDefault(t => t.Type == AttributeType.MappedAddress);
65 |
66 | if (mappedAddressAttribute is null)
67 | {
68 | return null;
69 | }
70 |
71 | MappedAddressStunAttributeValue mapped = (MappedAddressStunAttributeValue)mappedAddressAttribute.Value;
72 | return new IPEndPoint(mapped.Address!, mapped.Port);
73 | }
74 |
75 | public static IPEndPoint? GetChangedAddressAttribute(this StunMessage5389 response)
76 | {
77 | StunAttribute? changedAddressAttribute = response.Attributes.FirstOrDefault(t => t.Type == AttributeType.ChangedAddress);
78 |
79 | if (changedAddressAttribute is null)
80 | {
81 | return null;
82 | }
83 |
84 | ChangedAddressStunAttributeValue address = (ChangedAddressStunAttributeValue)changedAddressAttribute.Value;
85 | return new IPEndPoint(address.Address!, address.Port);
86 | }
87 |
88 | public static IPEndPoint? GetXorMappedAddressAttribute(this StunMessage5389 response)
89 | {
90 | StunAttribute? mappedAddressAttribute =
91 | response.Attributes.FirstOrDefault(t => t.Type == AttributeType.XorMappedAddress) ??
92 | response.Attributes.FirstOrDefault(t => t.Type == AttributeType.MappedAddress);
93 |
94 | if (mappedAddressAttribute is null)
95 | {
96 | return null;
97 | }
98 |
99 | AddressStunAttributeValue mapped = (AddressStunAttributeValue)mappedAddressAttribute.Value;
100 | return new IPEndPoint(mapped.Address!, mapped.Port);
101 | }
102 |
103 | public static IPEndPoint? GetOtherAddressAttribute(this StunMessage5389 response)
104 | {
105 | StunAttribute? addressAttribute =
106 | response.Attributes.FirstOrDefault(t => t.Type == AttributeType.OtherAddress) ??
107 | response.Attributes.FirstOrDefault(t => t.Type == AttributeType.ChangedAddress);
108 |
109 | if (addressAttribute is null)
110 | {
111 | return null;
112 | }
113 |
114 | AddressStunAttributeValue address = (AddressStunAttributeValue)addressAttribute.Value;
115 | return new IPEndPoint(address.Address!, address.Port);
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/src/tests/UnitTest/HostnameEndpointTest.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.VisualStudio.TestTools.UnitTesting;
2 | using STUN;
3 |
4 | namespace UnitTest;
5 |
6 | [TestClass]
7 | public class HostnameEndpointTest
8 | {
9 | [TestMethod]
10 | [DataRow(@"www.google.com", ushort.MinValue)]
11 | [DataRow(@"1.1.1.1", (ushort)1)]
12 | [DataRow(@"[2001:db8:1234:5678:11:2233:4455:6677]", (ushort)1919)]
13 | public void IsTrue(string host, ushort port)
14 | {
15 | string str = $@"{host}:{port}";
16 | Assert.IsTrue(StunServer.TryParse(str, out StunServer? server));
17 | Assert.IsNotNull(server);
18 | Assert.AreEqual(host, server.Hostname);
19 | Assert.AreEqual(port, server.Port);
20 | Assert.AreEqual(str, server.ToString());
21 | }
22 |
23 | [TestMethod]
24 | [DataRow(@"")]
25 | [DataRow(@"www.google.com:114514")]
26 | [DataRow(@"/dw.[/[:114")]
27 | [DataRow(@"2001:db8:1234:5678:11:2233:4455:6677:65535")]
28 | public void IsFalse(string str)
29 | {
30 | Assert.IsFalse(StunServer.TryParse(str, out StunServer? server));
31 | Assert.IsNull(server);
32 | }
33 |
34 | [TestMethod]
35 | [DataRow(@"www.google.com")]
36 | [DataRow(@"1.1.1.1")]
37 | [DataRow(@"2001:db8:1234:5678:11:2233:4455:6677")]
38 | [DataRow(@"[2001:db8:1234:5678:11:2233:4455:6677]")]
39 | [DataRow(@"2001:db8:1234:5678:11:2233:4455:db8")]
40 | public void TestDefaultPort(string str)
41 | {
42 | Assert.IsTrue(StunServer.TryParse(str, out StunServer? server));
43 | Assert.IsNotNull(server);
44 | Assert.AreEqual(str, server.Hostname);
45 | Assert.AreEqual(3478, server.Port);
46 | }
47 |
48 | [TestMethod]
49 | [DataRow(@"stun.syncthing.net:114", @"stun.syncthing.net:114")]
50 | [DataRow(@"stun.syncthing.net:3478", @"stun.syncthing.net")]
51 | [DataRow(@"[2001:db8:1234:5678:11:2233:4455:6677]", @"[2001:db8:1234:5678:11:2233:4455:6677]")]
52 | [DataRow(@"[2001:db8:1234:5678:11:2233:4455:6677]:3478", @"[2001:db8:1234:5678:11:2233:4455:6677]")]
53 | [DataRow(@"1.1.1.1:3478", @"1.1.1.1")]
54 | [DataRow(@"1.1.1.1:1919", @"1.1.1.1:1919")]
55 | public void ToString(string str, string expected)
56 | {
57 | Assert.IsTrue(StunServer.TryParse(str, out StunServer? server));
58 | Assert.IsNotNull(server);
59 | Assert.AreEqual(expected, server.ToString());
60 | }
61 |
62 | [TestMethod]
63 | public void DefaultServer()
64 | {
65 | StunServer server = new();
66 | Assert.AreEqual(@"stun.syncthing.net", server.Hostname);
67 | Assert.AreEqual(3478, server.Port);
68 | }
69 |
70 | [TestMethod]
71 | [DataRow(@"stun.syncthing.net:114", @"stun.syncthing.net:114")]
72 | [DataRow(@"stun.syncthing.net:3478", @"stun.syncthing.net:3478")]
73 | [DataRow(@"[2001:db8:1234:5678:11:2233:4455:6677]", @"[2001:db8:1234:5678:11:2233:4455:6677]:0")]
74 | [DataRow(@"[2001:db8:1234:5678:11:2233:4455:6677]:3478", @"[2001:db8:1234:5678:11:2233:4455:6677]:3478")]
75 | [DataRow(@"1.1.1.1:3478", @"1.1.1.1:3478")]
76 | [DataRow(@"1.1.1.1:1919", @"1.1.1.1:1919")]
77 | public void HostnameEndpointToString(string str, string expected)
78 | {
79 | Assert.IsTrue(HostnameEndpoint.TryParse(str, out HostnameEndpoint? server));
80 | Assert.IsNotNull(server);
81 | Assert.AreEqual(expected, server.ToString());
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/src/tests/UnitTest/StunClien5389UDPTest.cs:
--------------------------------------------------------------------------------
1 | using Dns.Net.Clients;
2 | using Microsoft.VisualStudio.TestTools.UnitTesting;
3 | using Moq;
4 | using Moq.Protected;
5 | using STUN;
6 | using STUN.Client;
7 | using STUN.Enums;
8 | using STUN.Messages;
9 | using STUN.StunResult;
10 | using System.Net;
11 |
12 | namespace UnitTest;
13 |
14 | [TestClass]
15 | public class StunClien5389UDPTest
16 | {
17 | private readonly DefaultDnsClient _dnsClient = new();
18 |
19 | private static readonly IPEndPoint Any = new(IPAddress.Any, 0);
20 | private static readonly IPEndPoint LocalAddress1 = IPEndPoint.Parse(@"127.0.0.1:114");
21 | private static readonly IPEndPoint MappedAddress1 = IPEndPoint.Parse(@"1.1.1.1:114");
22 | private static readonly IPEndPoint MappedAddress2 = IPEndPoint.Parse(@"1.1.1.1:514");
23 | private static readonly IPEndPoint ServerAddress = IPEndPoint.Parse(@"2.2.2.2:1919");
24 | private static readonly IPEndPoint ChangedAddress1 = IPEndPoint.Parse(@"3.3.3.3:23333");
25 | private static readonly IPEndPoint ChangedAddress2 = IPEndPoint.Parse(@"2.2.2.2:810");
26 | private static readonly IPEndPoint ChangedAddress3 = IPEndPoint.Parse(@"3.3.3.3:1919");
27 |
28 | private static readonly StunMessage5389 DefaultStunMessage = new();
29 |
30 | [TestMethod]
31 | public async Task BindingTestSuccessAsync()
32 | {
33 | IPAddress ip = await _dnsClient.QueryAsync(@"stun.hot-chilli.net");
34 | using StunClient5389UDP client = new(new IPEndPoint(ip, StunServer.DefaultPort), Any);
35 |
36 | StunResult5389 response = await client.BindingTestAsync();
37 |
38 | Assert.AreEqual(BindingTestResult.Success, response.BindingTestResult);
39 | Assert.AreEqual(MappingBehavior.Unknown, response.MappingBehavior);
40 | Assert.AreEqual(FilteringBehavior.Unknown, response.FilteringBehavior);
41 | Assert.IsNotNull(response.PublicEndPoint);
42 | Assert.IsNotNull(response.LocalEndPoint);
43 | Assert.IsNotNull(response.OtherEndPoint);
44 | }
45 |
46 | [TestMethod]
47 | public async Task BindingTestFailAsync()
48 | {
49 | IPAddress ip = IPAddress.Parse(@"1.1.1.1");
50 | using StunClient5389UDP client = new(new IPEndPoint(ip, StunServer.DefaultPort), Any);
51 |
52 | StunResult5389 response = await client.BindingTestAsync();
53 |
54 | Assert.AreEqual(BindingTestResult.Fail, response.BindingTestResult);
55 | Assert.AreEqual(MappingBehavior.Unknown, response.MappingBehavior);
56 | Assert.AreEqual(FilteringBehavior.Unknown, response.FilteringBehavior);
57 | Assert.IsNull(response.PublicEndPoint);
58 | Assert.IsNull(response.LocalEndPoint);
59 | Assert.IsNull(response.OtherEndPoint);
60 | }
61 |
62 | [TestMethod]
63 | public async Task MappingBehaviorTestFailAsync()
64 | {
65 | Mock mock = new(ServerAddress, Any, default!);
66 | StunClient5389UDP client = mock.Object;
67 |
68 | StunResult5389 fail = new() { BindingTestResult = BindingTestResult.Fail };
69 |
70 | mock.Protected().Setup>(@"BindingTestBaseAsync", ItExpr.IsAny(), ItExpr.IsAny()).ReturnsAsync(fail);
71 |
72 | await client.MappingBehaviorTestAsync();
73 |
74 | Assert.AreEqual(BindingTestResult.Fail, client.State.BindingTestResult);
75 | Assert.AreEqual(MappingBehavior.Unknown, client.State.MappingBehavior);
76 | Assert.AreEqual(FilteringBehavior.Unknown, client.State.FilteringBehavior);
77 | Assert.IsNull(client.State.PublicEndPoint);
78 | Assert.IsNull(client.State.LocalEndPoint);
79 | Assert.IsNull(client.State.OtherEndPoint);
80 | }
81 |
82 | [TestMethod]
83 | public async Task MappingBehaviorTestUnsupportedServerAsync()
84 | {
85 | Mock mock = new(ServerAddress, Any, default!);
86 | StunClient5389UDP client = mock.Object;
87 |
88 | StunResult5389 r1 = new()
89 | {
90 | BindingTestResult = BindingTestResult.Success,
91 | PublicEndPoint = MappedAddress1,
92 | LocalEndPoint = LocalAddress1
93 | };
94 | mock.Protected().Setup>(@"BindingTestBaseAsync", ItExpr.IsAny(), ItExpr.IsAny()).ReturnsAsync(r1);
95 | await TestAsync();
96 |
97 | StunResult5389 r2 = new()
98 | {
99 | BindingTestResult = BindingTestResult.Success,
100 | PublicEndPoint = MappedAddress1,
101 | LocalEndPoint = LocalAddress1,
102 | OtherEndPoint = ChangedAddress2
103 | };
104 | mock.Protected().Setup>(@"BindingTestBaseAsync", ItExpr.IsAny(), ItExpr.IsAny()).ReturnsAsync(r2);
105 | await TestAsync();
106 |
107 | StunResult5389 r3 = new()
108 | {
109 | BindingTestResult = BindingTestResult.Success,
110 | PublicEndPoint = MappedAddress1,
111 | LocalEndPoint = LocalAddress1,
112 | OtherEndPoint = ChangedAddress3
113 | };
114 | mock.Protected().Setup>(@"BindingTestBaseAsync", ItExpr.IsAny(), ItExpr.IsAny()).ReturnsAsync(r3);
115 | await TestAsync();
116 |
117 | return;
118 |
119 | async Task TestAsync()
120 | {
121 | await client.MappingBehaviorTestAsync();
122 |
123 | Assert.AreEqual(BindingTestResult.Success, client.State.BindingTestResult);
124 | Assert.AreEqual(MappingBehavior.UnsupportedServer, client.State.MappingBehavior);
125 | Assert.AreEqual(FilteringBehavior.Unknown, client.State.FilteringBehavior);
126 | Assert.IsNotNull(client.State.PublicEndPoint);
127 | Assert.IsNotNull(client.State.LocalEndPoint);
128 | }
129 | }
130 |
131 | [TestMethod]
132 | public async Task MappingBehaviorTestDirectAsync()
133 | {
134 | Mock mock = new(ServerAddress, Any, default!);
135 | StunClient5389UDP client = mock.Object;
136 |
137 | StunResult5389 response = new()
138 | {
139 | BindingTestResult = BindingTestResult.Success,
140 | PublicEndPoint = MappedAddress1,
141 | LocalEndPoint = MappedAddress1,
142 | OtherEndPoint = ChangedAddress1
143 | };
144 |
145 | mock.Protected().Setup>(@"BindingTestBaseAsync", ItExpr.IsAny(), ItExpr.IsAny()).ReturnsAsync(response);
146 |
147 | await client.MappingBehaviorTestAsync();
148 |
149 | Assert.AreEqual(BindingTestResult.Success, client.State.BindingTestResult);
150 | Assert.AreEqual(MappingBehavior.Direct, client.State.MappingBehavior);
151 | Assert.AreEqual(FilteringBehavior.Unknown, client.State.FilteringBehavior);
152 | Assert.IsNotNull(client.State.PublicEndPoint);
153 | Assert.IsNotNull(client.State.LocalEndPoint);
154 | Assert.IsNotNull(client.State.OtherEndPoint);
155 | }
156 |
157 | [TestMethod]
158 | public async Task MappingBehaviorTestEndpointIndependentAsync()
159 | {
160 | Mock mock = new(ServerAddress, Any, default!);
161 | StunClient5389UDP client = mock.Object;
162 |
163 | StunResult5389 r1 = new()
164 | {
165 | BindingTestResult = BindingTestResult.Success,
166 | PublicEndPoint = MappedAddress1,
167 | LocalEndPoint = LocalAddress1,
168 | OtherEndPoint = ChangedAddress1
169 | };
170 | mock.Protected().Setup>(@"BindingTestBaseAsync", ItExpr.IsAny(), ItExpr.IsAny()).ReturnsAsync(r1);
171 | await client.MappingBehaviorTestAsync();
172 |
173 | Assert.AreEqual(BindingTestResult.Success, client.State.BindingTestResult);
174 | Assert.AreEqual(MappingBehavior.EndpointIndependent, client.State.MappingBehavior);
175 | Assert.AreEqual(FilteringBehavior.Unknown, client.State.FilteringBehavior);
176 | Assert.IsNotNull(client.State.PublicEndPoint);
177 | Assert.IsNotNull(client.State.LocalEndPoint);
178 | Assert.IsNotNull(client.State.OtherEndPoint);
179 | }
180 |
181 | [TestMethod]
182 | public async Task MappingBehaviorTest2FailAsync()
183 | {
184 | Mock mock = new(ServerAddress, Any, default!);
185 | StunClient5389UDP client = mock.Object;
186 |
187 | StunResult5389 r1 = new()
188 | {
189 | BindingTestResult = BindingTestResult.Success,
190 | PublicEndPoint = MappedAddress1,
191 | LocalEndPoint = LocalAddress1,
192 | OtherEndPoint = ChangedAddress1
193 | };
194 | StunResult5389 r2 = new() { BindingTestResult = BindingTestResult.Fail };
195 |
196 | mock.Protected().Setup>(@"BindingTestBaseAsync", ServerAddress, ItExpr.IsAny()).ReturnsAsync(r1);
197 | mock.Protected().Setup>(@"BindingTestBaseAsync", ChangedAddress3, ItExpr.IsAny()).ReturnsAsync(r2);
198 | await client.MappingBehaviorTestAsync();
199 |
200 | Assert.AreEqual(BindingTestResult.Success, client.State.BindingTestResult);
201 | Assert.AreEqual(MappingBehavior.Fail, client.State.MappingBehavior);
202 | Assert.AreEqual(FilteringBehavior.Unknown, client.State.FilteringBehavior);
203 | Assert.IsNotNull(client.State.PublicEndPoint);
204 | Assert.IsNotNull(client.State.LocalEndPoint);
205 | Assert.IsNotNull(client.State.OtherEndPoint);
206 | }
207 |
208 | [TestMethod]
209 | public async Task MappingBehaviorTestAddressDependentAsync()
210 | {
211 | Mock mock = new(ServerAddress, Any, default!);
212 | StunClient5389UDP client = mock.Object;
213 |
214 | StunResult5389 r1 = new()
215 | {
216 | BindingTestResult = BindingTestResult.Success,
217 | PublicEndPoint = MappedAddress1,
218 | LocalEndPoint = LocalAddress1,
219 | OtherEndPoint = ChangedAddress1
220 | };
221 | StunResult5389 r2 = new()
222 | {
223 | BindingTestResult = BindingTestResult.Success,
224 | PublicEndPoint = MappedAddress2,
225 | LocalEndPoint = LocalAddress1,
226 | OtherEndPoint = ChangedAddress1
227 | };
228 | StunResult5389 r3 = new()
229 | {
230 | BindingTestResult = BindingTestResult.Success,
231 | PublicEndPoint = MappedAddress2,
232 | LocalEndPoint = LocalAddress1,
233 | OtherEndPoint = ChangedAddress1
234 | };
235 | mock.Protected().Setup>(@"BindingTestBaseAsync", ServerAddress, ItExpr.IsAny()).ReturnsAsync(r1);
236 | mock.Protected().Setup>(@"BindingTestBaseAsync", ChangedAddress3, ItExpr.IsAny()).ReturnsAsync(r2);
237 | mock.Protected().Setup>(@"BindingTestBaseAsync", ChangedAddress1, ItExpr.IsAny()).ReturnsAsync(r3);
238 |
239 | await client.MappingBehaviorTestAsync();
240 |
241 | Assert.AreEqual(BindingTestResult.Success, client.State.BindingTestResult);
242 | Assert.AreEqual(MappingBehavior.AddressDependent, client.State.MappingBehavior);
243 | Assert.AreEqual(FilteringBehavior.Unknown, client.State.FilteringBehavior);
244 | Assert.IsNotNull(client.State.PublicEndPoint);
245 | Assert.IsNotNull(client.State.LocalEndPoint);
246 | Assert.IsNotNull(client.State.OtherEndPoint);
247 | }
248 |
249 | [TestMethod]
250 | public async Task MappingBehaviorTestAddressAndPortDependentAsync()
251 | {
252 | Mock mock = new(ServerAddress, Any, default!);
253 | StunClient5389UDP client = mock.Object;
254 |
255 | StunResult5389 r1 = new()
256 | {
257 | BindingTestResult = BindingTestResult.Success,
258 | PublicEndPoint = MappedAddress1,
259 | LocalEndPoint = LocalAddress1,
260 | OtherEndPoint = ChangedAddress1
261 | };
262 | StunResult5389 r2 = new()
263 | {
264 | BindingTestResult = BindingTestResult.Success,
265 | PublicEndPoint = MappedAddress2,
266 | LocalEndPoint = LocalAddress1,
267 | OtherEndPoint = ChangedAddress1
268 | };
269 | StunResult5389 r3 = new()
270 | {
271 | BindingTestResult = BindingTestResult.Success,
272 | PublicEndPoint = MappedAddress1,
273 | LocalEndPoint = LocalAddress1,
274 | OtherEndPoint = ChangedAddress1
275 | };
276 | mock.Protected().Setup>(@"BindingTestBaseAsync", ServerAddress, ItExpr.IsAny()).ReturnsAsync(r1);
277 | mock.Protected().Setup>(@"BindingTestBaseAsync", ChangedAddress3, ItExpr.IsAny()).ReturnsAsync(r2);
278 | mock.Protected().Setup>(@"BindingTestBaseAsync", ChangedAddress1, ItExpr.IsAny()).ReturnsAsync(r3);
279 |
280 | await client.MappingBehaviorTestAsync();
281 |
282 | Assert.AreEqual(BindingTestResult.Success, client.State.BindingTestResult);
283 | Assert.AreEqual(MappingBehavior.AddressAndPortDependent, client.State.MappingBehavior);
284 | Assert.AreEqual(FilteringBehavior.Unknown, client.State.FilteringBehavior);
285 | Assert.IsNotNull(client.State.PublicEndPoint);
286 | Assert.IsNotNull(client.State.LocalEndPoint);
287 | Assert.IsNotNull(client.State.OtherEndPoint);
288 | }
289 |
290 | [TestMethod]
291 | public async Task MappingBehaviorTest3FailAsync()
292 | {
293 | Mock mock = new(ServerAddress, Any, default!);
294 | StunClient5389UDP client = mock.Object;
295 |
296 | StunResult5389 r1 = new()
297 | {
298 | BindingTestResult = BindingTestResult.Success,
299 | PublicEndPoint = MappedAddress1,
300 | LocalEndPoint = LocalAddress1,
301 | OtherEndPoint = ChangedAddress1
302 | };
303 | StunResult5389 r2 = new()
304 | {
305 | BindingTestResult = BindingTestResult.Success,
306 | PublicEndPoint = MappedAddress2,
307 | LocalEndPoint = LocalAddress1,
308 | OtherEndPoint = ChangedAddress1
309 | };
310 | StunResult5389 r3 = new() { BindingTestResult = BindingTestResult.Fail };
311 | mock.Protected().Setup>(@"BindingTestBaseAsync", ServerAddress, ItExpr.IsAny()).ReturnsAsync(r1);
312 | mock.Protected().Setup>(@"BindingTestBaseAsync", ChangedAddress3, ItExpr.IsAny()).ReturnsAsync(r2);
313 | mock.Protected().Setup>(@"BindingTestBaseAsync", ChangedAddress1, ItExpr.IsAny()).ReturnsAsync(r3);
314 |
315 | await client.MappingBehaviorTestAsync();
316 |
317 | Assert.AreEqual(BindingTestResult.Success, client.State.BindingTestResult);
318 | Assert.AreEqual(MappingBehavior.Fail, client.State.MappingBehavior);
319 | Assert.AreEqual(FilteringBehavior.Unknown, client.State.FilteringBehavior);
320 | Assert.IsNotNull(client.State.PublicEndPoint);
321 | Assert.IsNotNull(client.State.LocalEndPoint);
322 | Assert.IsNotNull(client.State.OtherEndPoint);
323 | }
324 |
325 | [TestMethod]
326 | public async Task FilteringBehaviorTestFailAsync()
327 | {
328 | Mock mock = new(ServerAddress, Any, default!);
329 | StunClient5389UDP client = mock.Object;
330 |
331 | StunResult5389 fail = new() { BindingTestResult = BindingTestResult.Fail };
332 |
333 | mock.Protected().Setup>(@"BindingTestBaseAsync", ItExpr.IsAny(), ItExpr.IsAny()).ReturnsAsync(fail);
334 |
335 | await client.FilteringBehaviorTestAsync();
336 |
337 | Assert.AreEqual(BindingTestResult.Fail, client.State.BindingTestResult);
338 | Assert.AreEqual(MappingBehavior.Unknown, client.State.MappingBehavior);
339 | Assert.AreEqual(FilteringBehavior.Unknown, client.State.FilteringBehavior);
340 | Assert.IsNull(client.State.PublicEndPoint);
341 | Assert.IsNull(client.State.LocalEndPoint);
342 | Assert.IsNull(client.State.OtherEndPoint);
343 | }
344 |
345 | [TestMethod]
346 | public async Task FilteringBehaviorTestUnsupportedServerAsync()
347 | {
348 | Mock mock = new(ServerAddress, Any, default!);
349 | StunClient5389UDP client = mock.Object;
350 |
351 | StunResult5389 r1 = new()
352 | {
353 | BindingTestResult = BindingTestResult.Success,
354 | PublicEndPoint = MappedAddress1,
355 | LocalEndPoint = LocalAddress1
356 | };
357 | mock.Protected().Setup>(@"BindingTestBaseAsync", ItExpr.IsAny(), ItExpr.IsAny()).ReturnsAsync(r1);
358 | await TestAsync();
359 |
360 | StunResult5389 r2 = new()
361 | {
362 | BindingTestResult = BindingTestResult.Success,
363 | PublicEndPoint = MappedAddress1,
364 | LocalEndPoint = LocalAddress1,
365 | OtherEndPoint = ChangedAddress2
366 | };
367 | mock.Protected().Setup>(@"BindingTestBaseAsync", ItExpr.IsAny(), ItExpr.IsAny()).ReturnsAsync(r2);
368 | await TestAsync();
369 |
370 | StunResult5389 r3 = new()
371 | {
372 | BindingTestResult = BindingTestResult.Success,
373 | PublicEndPoint = MappedAddress1,
374 | LocalEndPoint = LocalAddress1,
375 | OtherEndPoint = ChangedAddress3
376 | };
377 | mock.Protected().Setup>(@"BindingTestBaseAsync", ItExpr.IsAny(), ItExpr.IsAny()).ReturnsAsync(r3);
378 | await TestAsync();
379 |
380 | return;
381 |
382 | async Task TestAsync()
383 | {
384 | await client.FilteringBehaviorTestAsync();
385 |
386 | Assert.AreEqual(BindingTestResult.Success, client.State.BindingTestResult);
387 | Assert.AreEqual(MappingBehavior.Unknown, client.State.MappingBehavior);
388 | Assert.AreEqual(FilteringBehavior.UnsupportedServer, client.State.FilteringBehavior);
389 | Assert.IsNotNull(client.State.PublicEndPoint);
390 | Assert.IsNotNull(client.State.LocalEndPoint);
391 | }
392 | }
393 |
394 | [TestMethod]
395 | public async Task FilteringBehaviorTestEndpointIndependentAsync()
396 | {
397 | Mock mock = new(ServerAddress, Any, default!);
398 | StunClient5389UDP client = mock.Object;
399 |
400 | StunResult5389 r1 = new()
401 | {
402 | BindingTestResult = BindingTestResult.Success,
403 | PublicEndPoint = MappedAddress1,
404 | LocalEndPoint = LocalAddress1,
405 | OtherEndPoint = ChangedAddress1
406 | };
407 | StunResponse r2 = new(DefaultStunMessage, ChangedAddress1, LocalAddress1);
408 | mock.Protected().Setup>(@"BindingTestBaseAsync", ItExpr.IsAny(), ItExpr.IsAny()).ReturnsAsync(r1);
409 | mock.Protected().Setup>(@"FilteringBehaviorTest2Async", ItExpr.IsAny()).ReturnsAsync(r2);
410 |
411 | await client.FilteringBehaviorTestAsync();
412 |
413 | Assert.AreEqual(BindingTestResult.Success, client.State.BindingTestResult);
414 | Assert.AreEqual(MappingBehavior.Unknown, client.State.MappingBehavior);
415 | Assert.AreEqual(FilteringBehavior.EndpointIndependent, client.State.FilteringBehavior);
416 | Assert.IsNotNull(client.State.PublicEndPoint);
417 | Assert.IsNotNull(client.State.LocalEndPoint);
418 | Assert.IsNotNull(client.State.OtherEndPoint);
419 | }
420 |
421 | [TestMethod]
422 | public async Task FilteringBehaviorTest2UnsupportedServerAsync()
423 | {
424 | Mock mock = new(ServerAddress, Any, default!);
425 | StunClient5389UDP client = mock.Object;
426 |
427 | StunResult5389 r1 = new()
428 | {
429 | BindingTestResult = BindingTestResult.Success,
430 | PublicEndPoint = MappedAddress1,
431 | LocalEndPoint = LocalAddress1,
432 | OtherEndPoint = ChangedAddress1
433 | };
434 | StunResponse r2 = new(DefaultStunMessage, ServerAddress, LocalAddress1);
435 | mock.Protected().Setup>(@"BindingTestBaseAsync", ItExpr.IsAny(), ItExpr.IsAny()).ReturnsAsync(r1);
436 | mock.Protected().Setup>(@"FilteringBehaviorTest2Async", ItExpr.IsAny()).ReturnsAsync(r2);
437 |
438 | await client.FilteringBehaviorTestAsync();
439 |
440 | Assert.AreEqual(BindingTestResult.Success, client.State.BindingTestResult);
441 | Assert.AreEqual(MappingBehavior.Unknown, client.State.MappingBehavior);
442 | Assert.AreEqual(FilteringBehavior.UnsupportedServer, client.State.FilteringBehavior);
443 | Assert.IsNotNull(client.State.PublicEndPoint);
444 | Assert.IsNotNull(client.State.LocalEndPoint);
445 | Assert.IsNotNull(client.State.OtherEndPoint);
446 | }
447 |
448 | [TestMethod]
449 | public async Task FilteringBehaviorTestAddressAndPortDependentAsync()
450 | {
451 | Mock mock = new(ServerAddress, Any, default!);
452 | StunClient5389UDP client = mock.Object;
453 |
454 | StunResult5389 r1 = new()
455 | {
456 | BindingTestResult = BindingTestResult.Success,
457 | PublicEndPoint = MappedAddress1,
458 | LocalEndPoint = LocalAddress1,
459 | OtherEndPoint = ChangedAddress1
460 | };
461 | mock.Protected().Setup>(@"BindingTestBaseAsync", ItExpr.IsAny(), ItExpr.IsAny()).ReturnsAsync(r1);
462 | mock.Protected().Setup>(@"FilteringBehaviorTest2Async", ItExpr.IsAny()).ReturnsAsync(default(StunResponse?));
463 | mock.Protected().Setup>(@"FilteringBehaviorTest3Async", ItExpr.IsAny()).ReturnsAsync(default(StunResponse?));
464 |
465 | await client.FilteringBehaviorTestAsync();
466 |
467 | Assert.AreEqual(BindingTestResult.Success, client.State.BindingTestResult);
468 | Assert.AreEqual(MappingBehavior.Unknown, client.State.MappingBehavior);
469 | Assert.AreEqual(FilteringBehavior.AddressAndPortDependent, client.State.FilteringBehavior);
470 | Assert.IsNotNull(client.State.PublicEndPoint);
471 | Assert.IsNotNull(client.State.LocalEndPoint);
472 | Assert.IsNotNull(client.State.OtherEndPoint);
473 | }
474 |
475 | [TestMethod]
476 | public async Task FilteringBehaviorTestAddressDependentAsync()
477 | {
478 | Mock mock = new(ServerAddress, Any, default!);
479 | StunClient5389UDP client = mock.Object;
480 |
481 | StunResult5389 r1 = new()
482 | {
483 | BindingTestResult = BindingTestResult.Success,
484 | PublicEndPoint = MappedAddress1,
485 | LocalEndPoint = LocalAddress1,
486 | OtherEndPoint = ChangedAddress1
487 | };
488 | StunResponse r3 = new(DefaultStunMessage, ChangedAddress2, LocalAddress1);
489 | mock.Protected().Setup>(@"BindingTestBaseAsync", ItExpr.IsAny(), ItExpr.IsAny()).ReturnsAsync(r1);
490 | mock.Protected().Setup>(@"FilteringBehaviorTest2Async", ItExpr.IsAny()).ReturnsAsync(default(StunResponse?));
491 | mock.Protected().Setup>(@"FilteringBehaviorTest3Async", ItExpr.IsAny()).ReturnsAsync(r3);
492 |
493 | await client.FilteringBehaviorTestAsync();
494 |
495 | Assert.AreEqual(BindingTestResult.Success, client.State.BindingTestResult);
496 | Assert.AreEqual(MappingBehavior.Unknown, client.State.MappingBehavior);
497 | Assert.AreEqual(FilteringBehavior.AddressDependent, client.State.FilteringBehavior);
498 | Assert.IsNotNull(client.State.PublicEndPoint);
499 | Assert.IsNotNull(client.State.LocalEndPoint);
500 | Assert.IsNotNull(client.State.OtherEndPoint);
501 | }
502 |
503 | [TestMethod]
504 | public async Task FilteringBehaviorTest3UnsupportedServerAsync()
505 | {
506 | Mock mock = new(ServerAddress, Any, default!);
507 | StunClient5389UDP client = mock.Object;
508 |
509 | StunResult5389 r1 = new()
510 | {
511 | BindingTestResult = BindingTestResult.Success,
512 | PublicEndPoint = MappedAddress1,
513 | LocalEndPoint = LocalAddress1,
514 | OtherEndPoint = ChangedAddress1
515 | };
516 | StunResponse r3 = new(DefaultStunMessage, ServerAddress, LocalAddress1);
517 | mock.Protected().Setup>(@"BindingTestBaseAsync", ItExpr.IsAny(), ItExpr.IsAny()).ReturnsAsync(r1);
518 | mock.Protected().Setup>(@"FilteringBehaviorTest2Async", ItExpr.IsAny()).ReturnsAsync(default(StunResponse?));
519 | mock.Protected().Setup>(@"FilteringBehaviorTest3Async", ItExpr.IsAny()).ReturnsAsync(r3);
520 |
521 | await client.FilteringBehaviorTestAsync();
522 |
523 | Assert.AreEqual(BindingTestResult.Success, client.State.BindingTestResult);
524 | Assert.AreEqual(MappingBehavior.Unknown, client.State.MappingBehavior);
525 | Assert.AreEqual(FilteringBehavior.UnsupportedServer, client.State.FilteringBehavior);
526 | Assert.IsNotNull(client.State.PublicEndPoint);
527 | Assert.IsNotNull(client.State.LocalEndPoint);
528 | Assert.IsNotNull(client.State.OtherEndPoint);
529 | }
530 |
531 | [TestMethod]
532 | public async Task QueryFailTestAsync()
533 | {
534 | Mock mock = new(ServerAddress, Any, default!);
535 | StunClient5389UDP client = mock.Object;
536 |
537 | StunResult5389 fail = new() { BindingTestResult = BindingTestResult.Fail };
538 |
539 | mock.Protected().Setup>(@"BindingTestBaseAsync", ItExpr.IsAny(), ItExpr.IsAny()).ReturnsAsync(fail);
540 |
541 | await client.QueryAsync();
542 |
543 | Assert.AreEqual(BindingTestResult.Fail, client.State.BindingTestResult);
544 | Assert.AreEqual(MappingBehavior.Unknown, client.State.MappingBehavior);
545 | Assert.AreEqual(FilteringBehavior.Unknown, client.State.FilteringBehavior);
546 | Assert.IsNull(client.State.PublicEndPoint);
547 | Assert.IsNull(client.State.LocalEndPoint);
548 | Assert.IsNull(client.State.OtherEndPoint);
549 | }
550 |
551 | [TestMethod]
552 | public async Task QueryUnsupportedServerTestAsync()
553 | {
554 | Mock mock = new(ServerAddress, Any, default!);
555 | StunClient5389UDP client = mock.Object;
556 |
557 | StunResult5389 r1 = new()
558 | {
559 | BindingTestResult = BindingTestResult.Success,
560 | PublicEndPoint = MappedAddress1,
561 | LocalEndPoint = LocalAddress1,
562 | OtherEndPoint = ServerAddress
563 | };
564 | mock.Protected().Setup>(@"BindingTestBaseAsync", ItExpr.IsAny(), ItExpr.IsAny()).ReturnsAsync(r1);
565 |
566 | await client.QueryAsync();
567 |
568 | Assert.AreEqual(BindingTestResult.Success, client.State.BindingTestResult);
569 | Assert.AreEqual(MappingBehavior.Unknown, client.State.MappingBehavior);
570 | Assert.AreEqual(FilteringBehavior.UnsupportedServer, client.State.FilteringBehavior);
571 | Assert.IsNotNull(client.State.PublicEndPoint);
572 | Assert.IsNotNull(client.State.LocalEndPoint);
573 | }
574 |
575 | [TestMethod]
576 | public async Task QueryMappingBehaviorDirectTestAsync()
577 | {
578 | Mock mock = new(ServerAddress, Any, default!);
579 | StunClient5389UDP client = mock.Object;
580 |
581 | StunResult5389 r1 = new()
582 | {
583 | BindingTestResult = BindingTestResult.Success,
584 | PublicEndPoint = MappedAddress1,
585 | LocalEndPoint = MappedAddress1,
586 | OtherEndPoint = ChangedAddress1
587 | };
588 | mock.Protected().Setup>(@"BindingTestBaseAsync", ServerAddress, ItExpr.IsAny()).ReturnsAsync(r1);
589 | mock.Protected().Setup>(@"FilteringBehaviorTest2Async", ItExpr.IsAny()).ReturnsAsync(default(StunResponse?));
590 | mock.Protected().Setup>(@"FilteringBehaviorTest3Async", ItExpr.IsAny()).ReturnsAsync(default(StunResponse?));
591 |
592 | await client.QueryAsync();
593 |
594 | Assert.AreEqual(BindingTestResult.Success, client.State.BindingTestResult);
595 | Assert.AreEqual(MappingBehavior.Direct, client.State.MappingBehavior);
596 | Assert.AreEqual(FilteringBehavior.AddressAndPortDependent, client.State.FilteringBehavior);
597 | Assert.IsNotNull(client.State.PublicEndPoint);
598 | Assert.IsNotNull(client.State.LocalEndPoint);
599 | Assert.IsNotNull(client.State.OtherEndPoint);
600 | }
601 |
602 | [TestMethod]
603 | public async Task QueryMappingBehaviorEndpointIndependentTestAsync()
604 | {
605 | Mock mock = new(ServerAddress, Any, default!);
606 | StunClient5389UDP client = mock.Object;
607 |
608 | StunResult5389 r1 = new()
609 | {
610 | BindingTestResult = BindingTestResult.Success,
611 | PublicEndPoint = MappedAddress1,
612 | LocalEndPoint = LocalAddress1,
613 | OtherEndPoint = ChangedAddress1
614 | };
615 | mock.Protected().Setup>(@"BindingTestBaseAsync", ItExpr.IsAny(), ItExpr.IsAny()).ReturnsAsync(r1);
616 | mock.Protected().Setup>(@"FilteringBehaviorTest2Async", ItExpr.IsAny()).ReturnsAsync(default(StunResponse?));
617 | mock.Protected().Setup>(@"FilteringBehaviorTest3Async", ItExpr.IsAny()).ReturnsAsync(default(StunResponse?));
618 |
619 | await client.QueryAsync();
620 |
621 | Assert.AreEqual(BindingTestResult.Success, client.State.BindingTestResult);
622 | Assert.AreEqual(MappingBehavior.EndpointIndependent, client.State.MappingBehavior);
623 | Assert.AreEqual(FilteringBehavior.AddressAndPortDependent, client.State.FilteringBehavior);
624 | Assert.IsNotNull(client.State.PublicEndPoint);
625 | Assert.IsNotNull(client.State.LocalEndPoint);
626 | Assert.IsNotNull(client.State.OtherEndPoint);
627 | }
628 |
629 | [TestMethod]
630 | public async Task QueryMappingBehaviorAddressAndPortDependentTestAsync()
631 | {
632 | Mock mock = new(ServerAddress, Any, default!);
633 | StunClient5389UDP client = mock.Object;
634 |
635 | StunResult5389 r1 = new()
636 | {
637 | BindingTestResult = BindingTestResult.Success,
638 | PublicEndPoint = MappedAddress1,
639 | LocalEndPoint = LocalAddress1,
640 | OtherEndPoint = ChangedAddress1
641 | };
642 | StunResult5389 r2 = new()
643 | {
644 | BindingTestResult = BindingTestResult.Success,
645 | PublicEndPoint = MappedAddress2,
646 | LocalEndPoint = LocalAddress1,
647 | OtherEndPoint = ChangedAddress1
648 | };
649 | StunResult5389 r3 = new()
650 | {
651 | BindingTestResult = BindingTestResult.Success,
652 | PublicEndPoint = MappedAddress1,
653 | LocalEndPoint = LocalAddress1,
654 | OtherEndPoint = ChangedAddress1
655 | };
656 | mock.Protected().Setup>(@"BindingTestBaseAsync", ServerAddress, ItExpr.IsAny()).ReturnsAsync(r1);
657 | mock.Protected().Setup>(@"BindingTestBaseAsync", ChangedAddress3, ItExpr.IsAny()).ReturnsAsync(r2);
658 | mock.Protected().Setup>(@"BindingTestBaseAsync", ChangedAddress1, ItExpr.IsAny()).ReturnsAsync(r3);
659 | mock.Protected().Setup>(@"FilteringBehaviorTest2Async", ItExpr.IsAny()).ReturnsAsync(default(StunResponse?));
660 | mock.Protected().Setup>(@"FilteringBehaviorTest3Async", ItExpr.IsAny()).ReturnsAsync(default(StunResponse?));
661 |
662 | await client.QueryAsync();
663 |
664 | Assert.AreEqual(BindingTestResult.Success, client.State.BindingTestResult);
665 | Assert.AreEqual(MappingBehavior.AddressAndPortDependent, client.State.MappingBehavior);
666 | Assert.AreEqual(FilteringBehavior.AddressAndPortDependent, client.State.FilteringBehavior);
667 | Assert.IsNotNull(client.State.PublicEndPoint);
668 | Assert.IsNotNull(client.State.LocalEndPoint);
669 | Assert.IsNotNull(client.State.OtherEndPoint);
670 | }
671 | }
672 |
--------------------------------------------------------------------------------
/src/tests/UnitTest/StunClient3489Test.cs:
--------------------------------------------------------------------------------
1 | using Dns.Net.Clients;
2 | using Microsoft.VisualStudio.TestTools.UnitTesting;
3 | using Moq;
4 | using STUN.Client;
5 | using STUN.Enums;
6 | using STUN.Messages;
7 | using System.Net;
8 | using System.Net.Sockets;
9 | using static STUN.Utils.AttributeExtensions;
10 |
11 | namespace UnitTest;
12 |
13 | [TestClass]
14 | public class StunClient3489Test
15 | {
16 | private readonly DefaultDnsClient _dnsClient = new();
17 |
18 | private const string Server = @"stun.hot-chilli.net";
19 | private const ushort Port = 3478;
20 |
21 | private static readonly IPEndPoint Any = new(IPAddress.Any, 0);
22 | private static readonly IPEndPoint IPv6Any = new(IPAddress.IPv6Any, 0);
23 | private static readonly IPEndPoint LocalAddress1 = IPEndPoint.Parse(@"127.0.0.1:114");
24 | private static readonly IPEndPoint MappedAddress1 = IPEndPoint.Parse(@"1.1.1.1:114");
25 | private static readonly IPEndPoint MappedAddress2 = IPEndPoint.Parse(@"1.1.1.1:514");
26 | private static readonly IPEndPoint ServerAddress = IPEndPoint.Parse(@"2.2.2.2:1919");
27 | private static readonly IPEndPoint ChangedAddress1 = IPEndPoint.Parse(@"3.3.3.3:23333");
28 | private static readonly IPEndPoint ChangedAddress2 = IPEndPoint.Parse(@"2.2.2.2:810");
29 |
30 | private static readonly StunMessage5389 DefaultStunMessage = new();
31 |
32 | [TestMethod]
33 | public async Task UdpBlockedTestAsync()
34 | {
35 | Mock mock = new(Any, Any, default!);
36 | StunClient3489 client = mock.Object;
37 |
38 | mock.Setup(x => x.Test1Async(It.IsAny())).ReturnsAsync(default(StunResponse?));
39 |
40 | await client.QueryAsync();
41 | Assert.AreEqual(NatType.UdpBlocked, client.State.NatType);
42 | }
43 |
44 | [TestMethod]
45 | public async Task UnsupportedServerTestAsync()
46 | {
47 | Mock mock = new(Any, Any, default!);
48 | StunClient3489 client = mock.Object;
49 |
50 | mock.Setup(x => x.LocalEndPoint).Returns(LocalAddress1);
51 | StunResponse unknownResponse = new(DefaultStunMessage, Any, LocalAddress1);
52 | mock.Setup(x => x.Test1Async(It.IsAny())).ReturnsAsync(unknownResponse);
53 | await TestAsync();
54 |
55 | StunResponse r1 = new(new StunMessage5389 { Attributes = [BuildMapping(IpFamily.IPv4, MappedAddress1.Address, (ushort)MappedAddress1.Port)] }, ServerAddress, LocalAddress1);
56 | mock.Setup(x => x.Test1Async(It.IsAny())).ReturnsAsync(r1);
57 | await TestAsync();
58 |
59 | StunResponse r2 = new(new StunMessage5389 { Attributes = [BuildChangeAddress(IpFamily.IPv4, ChangedAddress1.Address, (ushort)ChangedAddress1.Port)] }, ServerAddress, LocalAddress1);
60 | mock.Setup(x => x.Test1Async(It.IsAny())).ReturnsAsync(r2);
61 | await TestAsync();
62 |
63 | StunResponse r3 = new(new StunMessage5389 { Attributes = [BuildMapping(IpFamily.IPv4, MappedAddress1.Address, (ushort)MappedAddress1.Port), BuildChangeAddress(IpFamily.IPv4, ServerAddress.Address, (ushort)ChangedAddress1.Port)] }, ServerAddress, LocalAddress1);
64 | mock.Setup(x => x.Test1Async(It.IsAny())).ReturnsAsync(r3);
65 | await TestAsync();
66 |
67 | StunResponse r4 = new(new StunMessage5389 { Attributes = [BuildMapping(IpFamily.IPv4, MappedAddress1.Address, (ushort)MappedAddress1.Port), BuildChangeAddress(IpFamily.IPv4, ChangedAddress1.Address, (ushort)ServerAddress.Port)] }, ServerAddress, LocalAddress1);
68 | mock.Setup(x => x.Test1Async(It.IsAny())).ReturnsAsync(r4);
69 | await TestAsync();
70 |
71 | return;
72 |
73 | async Task TestAsync()
74 | {
75 | await client.QueryAsync();
76 | Assert.AreEqual(NatType.UnsupportedServer, client.State.NatType);
77 | }
78 | }
79 |
80 | [TestMethod]
81 | public async Task NoNatTestAsync()
82 | {
83 | Mock mock = new(Any, Any, default!);
84 | StunClient3489 client = mock.Object;
85 |
86 | StunResponse openInternetTest1Response = new(
87 | new StunMessage5389 { Attributes = [BuildMapping(IpFamily.IPv4, MappedAddress1.Address, (ushort)MappedAddress1.Port), BuildChangeAddress(IpFamily.IPv4, ChangedAddress1.Address, (ushort)ChangedAddress1.Port)] },
88 | ServerAddress,
89 | MappedAddress1
90 | );
91 | StunResponse test2Response = new(
92 | new StunMessage5389 { Attributes = [BuildMapping(IpFamily.IPv4, MappedAddress1.Address, (ushort)MappedAddress1.Port)] },
93 | ChangedAddress1,
94 | MappedAddress1
95 | );
96 |
97 | mock.Setup(x => x.Test1Async(It.IsAny())).ReturnsAsync(openInternetTest1Response);
98 | mock.Setup(x => x.LocalEndPoint).Returns(MappedAddress1);
99 | mock.Setup(x => x.Test2Async(It.IsAny(), It.IsAny