Compare commits

...

No commits in common. "2.1.4" and "master" have entirely different histories.

157 changed files with 9949 additions and 68 deletions

63
.gitattributes vendored Normal file
View File

@ -0,0 +1,63 @@
###############################################################################
# Set default behavior to automatically normalize line endings.
###############################################################################
* text=auto
###############################################################################
# Set default behavior for command prompt diff.
#
# This is need for earlier builds of msysgit that does not have it on by
# default for csharp files.
# Note: This is only used by command line
###############################################################################
#*.cs diff=csharp
###############################################################################
# Set the merge driver for project and solution files
#
# Merging from the command prompt will add diff markers to the files if there
# are conflicts (Merging from VS is not affected by the settings below, in VS
# the diff markers are never inserted). Diff markers may cause the following
# file extensions to fail to load in VS. An alternative would be to treat
# these files as binary and thus will always conflict and require user
# intervention with every merge. To do so, just uncomment the entries below
###############################################################################
#*.sln merge=binary
#*.csproj merge=binary
#*.vbproj merge=binary
#*.vcxproj merge=binary
#*.vcproj merge=binary
#*.dbproj merge=binary
#*.fsproj merge=binary
#*.lsproj merge=binary
#*.wixproj merge=binary
#*.modelproj merge=binary
#*.sqlproj merge=binary
#*.wwaproj merge=binary
###############################################################################
# behavior for image files
#
# image files are treated as binary by default.
###############################################################################
#*.jpg binary
#*.png binary
#*.gif binary
###############################################################################
# diff behavior for common document formats
#
# Convert binary document formats to text before diffing them. This feature
# is only available from the command line. Turn it on by uncommenting the
# entries below.
###############################################################################
#*.doc diff=astextplain
#*.DOC diff=astextplain
#*.docx diff=astextplain
#*.DOCX diff=astextplain
#*.dot diff=astextplain
#*.DOT diff=astextplain
#*.pdf diff=astextplain
#*.PDF diff=astextplain
#*.rtf diff=astextplain
#*.RTF diff=astextplain

21
.github/ISSUE_TEMPLATE/bug-----.md vendored Normal file
View File

@ -0,0 +1,21 @@
---
name: Bug发现与报告
about: 写一份报告来帮助我们改进
title: ''
labels: ''
assignees: ''
---
**Bug描述**
Bug的详细描述内容
**重现步骤**
1. xxx
2. yyy
3. zzz
**软件信息**
- 操作系统: [e.g. win10-x64]
- FastGithub [e.g. v2.0.0]

363
.gitignore vendored Normal file
View File

@ -0,0 +1,363 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Oo]ut/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd

15
@dnscrypt-proxy/LICENSE Normal file
View File

@ -0,0 +1,15 @@
ISC License
Copyright (c) 2018-2021, Frank Denis <j at pureftpd dot org>
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.

View File

@ -0,0 +1,857 @@
##############################################
# #
# dnscrypt-proxy configuration #
# #
##############################################
## This is an example configuration file.
## You should adjust it to your needs, and save it as "dnscrypt-proxy.toml"
##
## Online documentation is available here: https://dnscrypt.info/doc
##################################
# Global settings #
##################################
## List of servers to use
##
## Servers from the "public-resolvers" source (see down below) can
## be viewed here: https://dnscrypt.info/public-servers
##
## The proxy will automatically pick working servers from this list.
## Note that the require_* filters do NOT apply when using this setting.
##
## By default, this list is empty and all registered servers matching the
## require_* filters will be used instead.
##
## Remove the leading # first to enable this; lines starting with # are ignored.
# server_names = ['scaleway-fr', 'google', 'yandex', 'cloudflare']
## List of local addresses and ports to listen to. Can be IPv4 and/or IPv6.
## Example with both IPv4 and IPv6:
## listen_addresses = ['127.0.0.1:53', '[::1]:53']
##
## To listen to all IPv4 addresses, use `listen_addresses = ['0.0.0.0:53']`
## To listen to all IPv4+IPv6 addresses, use `listen_addresses = ['[::]:53']`
listen_addresses = ['127.0.0.1:53']
## Maximum number of simultaneous client connections to accept
max_clients = 250
## Switch to a different system user after listening sockets have been created.
## Note (1): this feature is currently unsupported on Windows.
## Note (2): this feature is not compatible with systemd socket activation.
## Note (3): when using -pidfile, the PID file directory must be writable by the new user
# user_name = 'nobody'
## Require servers (from remote sources) to satisfy specific properties
# Use servers reachable over IPv4
ipv4_servers = true
# Use servers reachable over IPv6 -- Do not enable if you don't have IPv6 connectivity
ipv6_servers = false
# Use servers implementing the DNSCrypt protocol
dnscrypt_servers = true
# Use servers implementing the DNS-over-HTTPS protocol
doh_servers = true
# Use servers implementing the Oblivious DoH protocol
odoh_servers = false
## Require servers defined by remote sources to satisfy specific properties
# Server must support DNS security extensions (DNSSEC)
require_dnssec = false
# Server must not log user queries (declarative)
require_nolog = true
# Server must not enforce its own blocklist (for parental control, ads blocking...)
require_nofilter = true
# Server names to avoid even if they match all criteria
disabled_server_names = []
## Always use TCP to connect to upstream servers.
## This can be useful if you need to route everything through Tor.
## Otherwise, leave this to `false`, as it doesn't improve security
## (dnscrypt-proxy will always encrypt everything even using UDP), and can
## only increase latency.
force_tcp = false
## SOCKS proxy
## Uncomment the following line to route all TCP connections to a local Tor node
## Tor doesn't support UDP, so set `force_tcp` to `true` as well.
# proxy = 'socks5://127.0.0.1:9050'
## HTTP/HTTPS proxy
## Only for DoH servers
# http_proxy = 'http://127.0.0.1:8888'
## How long a DNS query will wait for a response, in milliseconds.
## If you have a network with *a lot* of latency, you may need to
## increase this. Startup may be slower if you do so.
## Don't increase it too much. 10000 is the highest reasonable value.
timeout = 5000
## Keepalive for HTTP (HTTPS, HTTP/2) queries, in seconds
keepalive = 30
## Add EDNS-client-subnet information to outgoing queries
##
## Multiple networks can be listed; they will be randomly chosen.
## These networks don't have to match your actual networks.
# edns_client_subnet = ["0.0.0.0/0", "2001:db8::/32"]
## Response for blocked queries. Options are `refused`, `hinfo` (default) or
## an IP response. To give an IP response, use the format `a:<IPv4>,aaaa:<IPv6>`.
## Using the `hinfo` option means that some responses will be lies.
## Unfortunately, the `hinfo` option appears to be required for Android 8+
# blocked_query_response = 'refused'
## Load-balancing strategy: 'p2' (default), 'ph', 'p<n>', 'first' or 'random'
## Randomly choose 1 of the fastest 2, half, n, 1 or all live servers by latency.
## The response quality still depends on the server itself.
# lb_strategy = 'p2'
## Set to `true` to constantly try to estimate the latency of all the resolvers
## and adjust the load-balancing parameters accordingly, or to `false` to disable.
## Default is `true` that makes 'p2' `lb_strategy` work well.
# lb_estimator = true
## Log level (0-6, default: 2 - 0 is very verbose, 6 only contains fatal errors)
# log_level = 2
## Log file for the application, as an alternative to sending logs to
## the standard system logging service (syslog/Windows event log).
##
## This file is different from other log files, and will not be
## automatically rotated by the application.
# log_file = 'dnscrypt-proxy.log'
## When using a log file, only keep logs from the most recent launch.
# log_file_latest = true
## Use the system logger (syslog on Unix, Event Log on Windows)
# use_syslog = true
## Delay, in minutes, after which certificates are reloaded
cert_refresh_delay = 240
## DNSCrypt: Create a new, unique key for every single DNS query
## This may improve privacy but can also have a significant impact on CPU usage
## Only enable if you don't have a lot of network load
# dnscrypt_ephemeral_keys = false
## DoH: Disable TLS session tickets - increases privacy but also latency
# tls_disable_session_tickets = false
## DoH: Use a specific cipher suite instead of the server preference
## 49199 = TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
## 49195 = TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
## 52392 = TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305
## 52393 = TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305
## 4865 = TLS_AES_128_GCM_SHA256
## 4867 = TLS_CHACHA20_POLY1305_SHA256
##
## On non-Intel CPUs such as MIPS routers and ARM systems (Android, Raspberry Pi...),
## the following suite improves performance.
## This may also help on Intel CPUs running 32-bit operating systems.
##
## Keep tls_cipher_suite empty if you have issues fetching sources or
## connecting to some DoH servers. Google and Cloudflare are fine with it.
# tls_cipher_suite = [52392, 49199]
## Bootstrap resolvers
##
## These are normal, non-encrypted DNS resolvers, that will be only used
## for one-shot queries when retrieving the initial resolvers list and if
## the system DNS configuration doesn't work.
##
## No user queries will ever be leaked through these resolvers, and they will
## not be used after IP addresses of DoH resolvers have been found (if you are
## using DoH).
##
## They will never be used if lists have already been cached, and if the stamps
## of the configured servers already include IP addresses (which is the case for
## most of DoH servers, and for all DNSCrypt servers and relays).
##
## They will not be used if the configured system DNS works, or after the
## proxy already has at least one usable secure resolver.
##
## Resolvers supporting DNSSEC are recommended, and, if you are using
## DoH, bootstrap resolvers should ideally be operated by a different entity
## than the DoH servers you will be using, especially if you have IPv6 enabled.
##
## People in China may want to use 114.114.114.114:53 here.
## Other popular options include 8.8.8.8, 9.9.9.9 and 1.1.1.1.
##
## If more than one resolver is specified, they will be tried in sequence.
##
## TL;DR: put valid standard resolver addresses here. Your actual queries will
## not be sent there. If you're using DNSCrypt or Anonymized DNS and your
## lists are up to date, these resolvers will not even be used.
bootstrap_resolvers = ['9.9.9.9:53', '8.8.8.8:53']
## Always use the bootstrap resolver before the system DNS settings.
ignore_system_dns = true
## Maximum time (in seconds) to wait for network connectivity before
## initializing the proxy.
## Useful if the proxy is automatically started at boot, and network
## connectivity is not guaranteed to be immediately available.
## Use 0 to not test for connectivity at all (not recommended),
## and -1 to wait as much as possible.
netprobe_timeout = 60
## Address and port to try initializing a connection to, just to check
## if the network is up. It can be any address and any port, even if
## there is nothing answering these on the other side. Just don't use
## a local address, as the goal is to check for Internet connectivity.
## On Windows, a datagram with a single, nul byte will be sent, only
## when the system starts.
## On other operating systems, the connection will be initialized
## but nothing will be sent at all.
netprobe_address = '9.9.9.9:53'
## Offline mode - Do not use any remote encrypted servers.
## The proxy will remain fully functional to respond to queries that
## plugins can handle directly (forwarding, cloaking, ...)
# offline_mode = false
## Additional data to attach to outgoing queries.
## These strings will be added as TXT records to queries.
## Do not use, except on servers explicitly asking for extra data
## to be present.
## encrypted-dns-server can be configured to use this for access control
## in the [access_control] section
# query_meta = ['key1:value1', 'key2:value2', 'token:MySecretToken']
## Automatic log files rotation
# Maximum log files size in MB - Set to 0 for unlimited.
log_files_max_size = 10
# How long to keep backup files, in days
log_files_max_age = 7
# Maximum log files backups to keep (or 0 to keep all backups)
log_files_max_backups = 1
#########################
# Filters #
#########################
## Note: if you are using dnsmasq, disable the `dnssec` option in dnsmasq if you
## configure dnscrypt-proxy to do any kind of filtering (including the filters
## below and blocklists).
## You can still choose resolvers that do DNSSEC validation.
## Immediately respond to IPv6-related queries with an empty response
## This makes things faster when there is no IPv6 connectivity, but can
## also cause reliability issues with some stub resolvers.
block_ipv6 = false
## Immediately respond to A and AAAA queries for host names without a domain name
block_unqualified = true
## Immediately respond to queries for local zones instead of leaking them to
## upstream resolvers (always causing errors or timeouts).
block_undelegated = true
## TTL for synthetic responses sent when a request has been blocked (due to
## IPv6 or blocklists).
reject_ttl = 10
##################################################################################
# Route queries for specific domains to a dedicated set of servers #
##################################################################################
## See the `example-forwarding-rules.txt` file for an example
# forwarding_rules = 'forwarding-rules.txt'
###############################
# Cloaking rules #
###############################
## Cloaking returns a predefined address for a specific name.
## In addition to acting as a HOSTS file, it can also return the IP address
## of a different name. It will also do CNAME flattening.
##
## See the `example-cloaking-rules.txt` file for an example
# cloaking_rules = 'cloaking-rules.txt'
## TTL used when serving entries in cloaking-rules.txt
# cloak_ttl = 600
###########################
# DNS cache #
###########################
## Enable a DNS cache to reduce latency and outgoing traffic
cache = true
## Cache size
cache_size = 4096
## Minimum TTL for cached entries
cache_min_ttl = 60
## Maximum TTL for cached entries
cache_max_ttl = 600
## Minimum TTL for negatively cached entries
cache_neg_min_ttl = 60
## Maximum TTL for negatively cached entries
cache_neg_max_ttl = 600
########################################
# Captive portal handling #
########################################
[captive_portals]
## A file that contains a set of names used by operating systems to
## check for connectivity and captive portals, along with hard-coded
## IP addresses to return.
# map_file = 'example-captive-portals.txt'
##################################
# Local DoH server #
##################################
[local_doh]
## dnscrypt-proxy can act as a local DoH server. By doing so, web browsers
## requiring a direct connection to a DoH server in order to enable some
## features will enable these, without bypassing your DNS proxy.
## Addresses that the local DoH server should listen to
# listen_addresses = ['127.0.0.1:3000']
## Path of the DoH URL. This is not a file, but the part after the hostname
## in the URL. By convention, `/dns-query` is frequently chosen.
## For each `listen_address` the complete URL to access the server will be:
## `https://<listen_address><path>` (ex: `https://127.0.0.1/dns-query`)
# path = '/dns-query'
## Certificate file and key - Note that the certificate has to be trusted.
## See the documentation (wiki) for more information.
# cert_file = 'localhost.pem'
# cert_key_file = 'localhost.pem'
###############################
# Query logging #
###############################
## Log client queries to a file
[query_log]
## Path to the query log file (absolute, or relative to the same directory as the config file)
## Can be set to /dev/stdout in order to log to the standard output.
# file = 'query.log'
## Query log format (currently supported: tsv and ltsv)
format = 'tsv'
## Do not log these query types, to reduce verbosity. Keep empty to log everything.
# ignored_qtypes = ['DNSKEY', 'NS']
############################################
# Suspicious queries logging #
############################################
## Log queries for nonexistent zones
## These queries can reveal the presence of malware, broken/obsolete applications,
## and devices signaling their presence to 3rd parties.
[nx_log]
## Path to the query log file (absolute, or relative to the same directory as the config file)
# file = 'nx.log'
## Query log format (currently supported: tsv and ltsv)
format = 'tsv'
######################################################
# Pattern-based blocking (blocklists) #
######################################################
## Blocklists are made of one pattern per line. Example of valid patterns:
##
## example.com
## =example.com
## *sex*
## ads.*
## ads*.example.*
## ads*.example[0-9]*.com
##
## Example blocklist files can be found at https://download.dnscrypt.info/blocklists/
## A script to build blocklists from public feeds can be found in the
## `utils/generate-domains-blocklists` directory of the dnscrypt-proxy source code.
[blocked_names]
## Path to the file of blocking rules (absolute, or relative to the same directory as the config file)
# blocked_names_file = 'blocked-names.txt'
## Optional path to a file logging blocked queries
# log_file = 'blocked-names.log'
## Optional log format: tsv or ltsv (default: tsv)
# log_format = 'tsv'
###########################################################
# Pattern-based IP blocking (IP blocklists) #
###########################################################
## IP blocklists are made of one pattern per line. Example of valid patterns:
##
## 127.*
## fe80:abcd:*
## 192.168.1.4
[blocked_ips]
## Path to the file of blocking rules (absolute, or relative to the same directory as the config file)
# blocked_ips_file = 'blocked-ips.txt'
## Optional path to a file logging blocked queries
# log_file = 'blocked-ips.log'
## Optional log format: tsv or ltsv (default: tsv)
# log_format = 'tsv'
######################################################
# Pattern-based allow lists (blocklists bypass) #
######################################################
## Allowlists support the same patterns as blocklists
## If a name matches an allowlist entry, the corresponding session
## will bypass names and IP filters.
##
## Time-based rules are also supported to make some websites only accessible at specific times of the day.
[allowed_names]
## Path to the file of allow list rules (absolute, or relative to the same directory as the config file)
# allowed_names_file = 'allowed-names.txt'
## Optional path to a file logging allowed queries
# log_file = 'allowed-names.log'
## Optional log format: tsv or ltsv (default: tsv)
# log_format = 'tsv'
#########################################################
# Pattern-based allowed IPs lists (blocklists bypass) #
#########################################################
## Allowed IP lists support the same patterns as IP blocklists
## If an IP response matches an allow ip entry, the corresponding session
## will bypass IP filters.
##
## Time-based rules are also supported to make some websites only accessible at specific times of the day.
[allowed_ips]
## Path to the file of allowed ip rules (absolute, or relative to the same directory as the config file)
# allowed_ips_file = 'allowed-ips.txt'
## Optional path to a file logging allowed queries
# log_file = 'allowed-ips.log'
## Optional log format: tsv or ltsv (default: tsv)
# log_format = 'tsv'
##########################################
# Time access restrictions #
##########################################
## One or more weekly schedules can be defined here.
## Patterns in the name-based blocked_names file can optionally be followed with @schedule_name
## to apply the pattern 'schedule_name' only when it matches a time range of that schedule.
##
## For example, the following rule in a blocklist file:
## *.youtube.* @time-to-sleep
## would block access to YouTube during the times defined by the 'time-to-sleep' schedule.
##
## {after='21:00', before= '7:00'} matches 0:00-7:00 and 21:00-0:00
## {after= '9:00', before='18:00'} matches 9:00-18:00
[schedules]
# [schedules.'time-to-sleep']
# mon = [{after='21:00', before='7:00'}]
# tue = [{after='21:00', before='7:00'}]
# wed = [{after='21:00', before='7:00'}]
# thu = [{after='21:00', before='7:00'}]
# fri = [{after='23:00', before='7:00'}]
# sat = [{after='23:00', before='7:00'}]
# sun = [{after='21:00', before='7:00'}]
# [schedules.'work']
# mon = [{after='9:00', before='18:00'}]
# tue = [{after='9:00', before='18:00'}]
# wed = [{after='9:00', before='18:00'}]
# thu = [{after='9:00', before='18:00'}]
# fri = [{after='9:00', before='17:00'}]
#########################
# Servers #
#########################
## Remote lists of available servers
## Multiple sources can be used simultaneously, but every source
## requires a dedicated cache file.
##
## Refer to the documentation for URLs of public sources.
##
## A prefix can be prepended to server names in order to
## avoid collisions if different sources share the same for
## different servers. In that case, names listed in `server_names`
## must include the prefixes.
##
## If the `urls` property is missing, cache files and valid signatures
## must already be present. This doesn't prevent these cache files from
## expiring after `refresh_delay` hours.
## Cache freshness is checked every 24 hours, so values for 'refresh_delay'
## of less than 24 hours will have no effect.
## A maximum delay of 168 hours (1 week) is imposed to ensure cache freshness.
[sources]
## An example of a remote source from https://github.com/DNSCrypt/dnscrypt-resolvers
[sources.'public-resolvers']
urls = ['https://raw.githubusercontent.com/DNSCrypt/dnscrypt-resolvers/master/v3/public-resolvers.md', 'https://download.dnscrypt.info/resolvers-list/v3/public-resolvers.md', 'https://ipv6.download.dnscrypt.info/resolvers-list/v3/public-resolvers.md', 'https://download.dnscrypt.net/resolvers-list/v3/public-resolvers.md']
cache_file = 'public-resolvers.md'
minisign_key = 'RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3'
refresh_delay = 72
prefix = ''
## Anonymized DNS relays
[sources.'relays']
urls = ['https://raw.githubusercontent.com/DNSCrypt/dnscrypt-resolvers/master/v3/relays.md', 'https://download.dnscrypt.info/resolvers-list/v3/relays.md', 'https://ipv6.download.dnscrypt.info/resolvers-list/v3/relays.md', 'https://download.dnscrypt.net/resolvers-list/v3/relays.md']
cache_file = 'relays.md'
minisign_key = 'RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3'
refresh_delay = 72
prefix = ''
## ODoH (Oblivious DoH) servers and relays
# [sources.'odoh-servers']
# urls = ['https://raw.githubusercontent.com/DNSCrypt/dnscrypt-resolvers/master/v3/odoh-servers.md', 'https://download.dnscrypt.info/resolvers-list/v3/odoh-servers.md', 'https://ipv6.download.dnscrypt.info/resolvers-list/v3/odoh-servers.md', 'https://download.dnscrypt.net/resolvers-list/v3/odoh-servers.md']
# cache_file = 'odoh-servers.md'
# minisign_key = 'RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3'
# refresh_delay = 24
# prefix = ''
# [sources.'odoh-relays']
# urls = ['https://raw.githubusercontent.com/DNSCrypt/dnscrypt-resolvers/master/v3/odoh-relays.md', 'https://download.dnscrypt.info/resolvers-list/v3/odoh-relays.md', 'https://ipv6.download.dnscrypt.info/resolvers-list/v3/odoh-relays.md', 'https://download.dnscrypt.net/resolvers-list/v3/odoh-relays.md']
# cache_file = 'odoh-relays.md'
# minisign_key = 'RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3'
# refresh_delay = 24
# prefix = ''
## Quad9
# [sources.quad9-resolvers]
# urls = ['https://www.quad9.net/quad9-resolvers.md']
# minisign_key = 'RWQBphd2+f6eiAqBsvDZEBXBGHQBJfeG6G+wJPPKxCZMoEQYpmoysKUN'
# cache_file = 'quad9-resolvers.md'
# prefix = 'quad9-'
## Another example source, with resolvers censoring some websites not appropriate for children
## This is a subset of the `public-resolvers` list, so enabling both is useless
# [sources.'parental-control']
# urls = ['https://raw.githubusercontent.com/DNSCrypt/dnscrypt-resolvers/master/v3/parental-control.md', 'https://download.dnscrypt.info/resolvers-list/v3/parental-control.md', 'https://ipv6.download.dnscrypt.info/resolvers-list/v3/parental-control.md', 'https://download.dnscrypt.net/resolvers-list/v3/parental-control.md']
# cache_file = 'parental-control.md'
# minisign_key = 'RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3'
#########################################
# Servers with known bugs #
#########################################
[broken_implementations]
# Cisco servers currently cannot handle queries larger than 1472 bytes, and don't
# truncate reponses larger than questions as expected by the DNSCrypt protocol.
# This prevents large responses from being received over UDP and over relays.
#
# Older versions of the `dnsdist` server software had a bug with queries larger
# than 1500 bytes. This is fixed since `dnsdist` version 1.5.0, but
# some server may still run an outdated version.
#
# The list below enables workarounds to make non-relayed usage more reliable
# until the servers are fixed.
fragments_blocked = ['cisco', 'cisco-ipv6', 'cisco-familyshield', 'cisco-familyshield-ipv6', 'cleanbrowsing-adult', 'cleanbrowsing-adult-ipv6', 'cleanbrowsing-family', 'cleanbrowsing-family-ipv6', 'cleanbrowsing-security', 'cleanbrowsing-security-ipv6']
#################################################################
# Certificate-based client authentication for DoH #
#################################################################
# Use a X509 certificate to authenticate yourself when connecting to DoH servers.
# This is only useful if you are operating your own, private DoH server(s).
# 'creds' maps servers to certificates, and supports multiple entries.
# If you are not using the standard root CA, an optional "root_ca"
# property set to the path to a root CRT file can be added to a server entry.
[doh_client_x509_auth]
#
# creds = [
# { server_name='*', client_cert='client.crt', client_key='client.key' }
# ]
################################
# Anonymized DNS #
################################
[anonymized_dns]
## Routes are indirect ways to reach DNSCrypt servers.
##
## A route maps a server name ("server_name") to one or more relays that will be
## used to connect to that server.
##
## A relay can be specified as a DNS Stamp (either a relay stamp, or a
## DNSCrypt stamp) or a server name.
##
## The following example routes "example-server-1" via `anon-example-1` or `anon-example-2`,
## and "example-server-2" via the relay whose relay DNS stamp is
## "sdns://gRIxMzcuNzQuMjIzLjIzNDo0NDM".
##
## !!! THESE ARE JUST EXAMPLES !!!
##
## Review the list of available relays from the "relays.md" file, and, for each
## server you want to use, define the relays you want connections to go through.
##
## Carefully choose relays and servers so that they are run by different entities.
##
## "server_name" can also be set to "*" to define a default route, for all servers:
## { server_name='*', via=['anon-example-1', 'anon-example-2'] }
##
## If a route is ["*"], the proxy automatically picks a relay on a distinct network.
## { server_name='*', via=['*'] } is also an option, but is likely to be suboptimal.
##
## Manual selection is always recommended over automatic selection, so that you can
## select (relay,server) pairs that work well and fit your own criteria (close by or
## in different countries, operated by different entities, on distinct ISPs...)
# routes = [
# { server_name='example-server-1', via=['anon-example-1', 'anon-example-2'] },
# { server_name='example-server-2', via=['sdns://gRIxMzcuNzQuMjIzLjIzNDo0NDM'] }
# ]
# Skip resolvers incompatible with anonymization instead of using them directly
skip_incompatible = false
# If public server certificates for a non-conformant server cannot be
# retrieved via a relay, try getting them directly. Actual queries
# will then always go through relays.
# direct_cert_fallback = false
###############################
# DNS64 #
###############################
## DNS64 is a mechanism for synthesizing AAAA records from A records.
## It is used with an IPv6/IPv4 translator to enable client-server
## communication between an IPv6-only client and an IPv4-only server,
## without requiring any changes to either the IPv6 or the IPv4 node,
## for the class of applications that work through NATs.
##
## There are two options to synthesize such records:
## Option 1: Using a set of static IPv6 prefixes;
## Option 2: By discovering the IPv6 prefix from DNS64-enabled resolver.
##
## If both options are configured - only static prefixes are used.
## (Ref. RFC6147, RFC6052, RFC7050)
##
## Do not enable unless you know what DNS64 is and why you need it, or else
## you won't be able to connect to anything at all.
[dns64]
## (Option 1) Static prefix(es) as Pref64::/n CIDRs.
# prefix = ['64:ff9b::/96']
## (Option 2) DNS64-enabled resolver(s) to discover Pref64::/n CIDRs.
## These resolvers are used to query for Well-Known IPv4-only Name (WKN) "ipv4only.arpa." to discover only.
## Set with your ISP's resolvers in case of custom prefixes (other than Well-Known Prefix 64:ff9b::/96).
## IMPORTANT: Default resolvers listed below support Well-Known Prefix 64:ff9b::/96 only.
# resolver = ['[2606:4700:4700::64]:53', '[2001:4860:4860::64]:53']
########################################
# Static entries #
########################################
## Optional, local, static list of additional servers
## Mostly useful for testing your own servers.
[static]
# [static.'myserver']
# stamp = 'sdns://AQcAAAAAAAAAAAAQMi5kbnNjcnlwdC1jZXJ0Lg'

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

16
Directory.Build.props Normal file
View File

@ -0,0 +1,16 @@
<Project>
<PropertyGroup>
<Version>2.1.5</Version>
<Nullable>enable</Nullable>
<TargetFramework>net7.0</TargetFramework>
<IsWebConfigTransformDisabled>true</IsWebConfigTransformDisabled>
<Description>github加速神器</Description>
<Copyright>https://github.com/dotnetcore/FastGithub</Copyright>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Release'">
<DebugType>none</DebugType>
<DebugSymbols>false</DebugSymbols>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,65 @@
using System;
using System.Net;
namespace FastGithub.Configuration
{
/// <summary>
/// 域名配置
/// </summary>
public record DomainConfig
{
/// <summary>
/// 是否发送SNI
/// </summary>
public bool TlsSni { get; init; }
/// <summary>
/// 自定义SNI值的表达式
/// </summary>
public string? TlsSniPattern { get; init; }
/// <summary>
/// 是否忽略服务器证书域名不匹配
/// 当不发送SNI时服务器可能发回域名不匹配的证书
/// </summary>
public bool TlsIgnoreNameMismatch { get; init; }
/// <summary>
/// 使用的ip地址
/// </summary>
public IPAddress? IPAddress { get; init; }
/// <summary>
/// 请求超时时长
/// </summary>
public TimeSpan? Timeout { get; init; }
/// <summary>
/// 目的地
/// 格式为相对或绝对uri
/// </summary>
public Uri? Destination { get; init; }
/// <summary>
/// 自定义响应
/// </summary>
public ResponseConfig? Response { get; init; }
/// <summary>
/// 获取TlsSniPattern
/// </summary>
/// <returns></returns>
public TlsSniPattern GetTlsSniPattern()
{
if (this.TlsSni == false)
{
return Configuration.TlsSniPattern.None;
}
if (string.IsNullOrEmpty(this.TlsSniPattern))
{
return Configuration.TlsSniPattern.Domain;
}
return new TlsSniPattern(this.TlsSniPattern);
}
}
}

View File

@ -0,0 +1,96 @@
using System;
using System.Text.RegularExpressions;
namespace FastGithub.Configuration
{
/// <summary>
/// 表示域名表达式
/// *表示除.之外任意0到多个字符
/// </summary>
public class DomainPattern : IComparable<DomainPattern>
{
private readonly Regex regex;
private readonly string domainPattern;
/// <summary>
/// 域名表达式
/// *表示除.之外任意0到多个字符
/// </summary>
/// <param name="domainPattern">域名表达式</param>
public DomainPattern(string domainPattern)
{
this.domainPattern = domainPattern;
var regexPattern = Regex.Escape(domainPattern).Replace(@"\*", @"[^\.]*");
this.regex = new Regex($"^{regexPattern}$", RegexOptions.IgnoreCase);
}
/// <summary>
/// 与目标比较
/// </summary>
/// <param name="other"></param>
/// <returns></returns>
public int CompareTo(DomainPattern? other)
{
if (other is null)
{
return 1;
}
var segmentsX = this.domainPattern.Split('.');
var segmentsY = other.domainPattern.Split('.');
var value = segmentsX.Length - segmentsY.Length;
if (value != 0)
{
return value;
}
for (var i = segmentsX.Length - 1; i >= 0; i--)
{
var x = segmentsX[i];
var y = segmentsY[i];
value = Compare(x, y);
if (value == 0)
{
continue;
}
return value;
}
return 0;
}
/// <summary>
/// 比较两个分段
/// </summary>
/// <param name="x">abc</param>
/// <param name="y">abc*</param>
/// <returns></returns>
private static int Compare(string x, string y)
{
var valueX = x.Replace('*', char.MaxValue);
var valueY = y.Replace('*', char.MaxValue);
return valueX.CompareTo(valueY);
}
/// <summary>
/// 是否与指定域名匹配
/// </summary>
/// <param name="domain"></param>
/// <returns></returns>
public bool IsMatch(string domain)
{
return this.regex.IsMatch(domain);
}
/// <summary>
/// 转换为文本
/// </summary>
/// <returns></returns>
public override string ToString()
{
return this.domainPattern;
}
}
}

View File

@ -0,0 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.2" />
<PackageReference Include="Microsoft.Extensions.Options" Version="6.0.0" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,109 @@
using Microsoft.Extensions.Options;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Net;
namespace FastGithub.Configuration
{
/// <summary>
/// FastGithub配置
/// </summary>
public class FastGithubConfig
{
private SortedDictionary<DomainPattern, DomainConfig> domainConfigs;
private ConcurrentDictionary<string, DomainConfig?> domainConfigCache;
/// <summary>
/// http代理端口
/// </summary>
public int HttpProxyPort { get; set; }
/// <summary>
/// 回退的dns
/// </summary>
public IPEndPoint[] FallbackDns { get; set; }
/// <summary>
/// FastGithub配置
/// </summary>
/// <param name="options"></param>
/// <param name="logger"></param>
public FastGithubConfig(IOptionsMonitor<FastGithubOptions> options)
{
var opt = options.CurrentValue;
this.HttpProxyPort = opt.HttpProxyPort;
this.FallbackDns = opt.FallbackDns;
this.domainConfigs = ConvertDomainConfigs(opt.DomainConfigs);
this.domainConfigCache = new ConcurrentDictionary<string, DomainConfig?>();
options.OnChange(opt => this.Update(opt));
}
/// <summary>
/// 更新配置
/// </summary>
/// <param name="options"></param>
private void Update(FastGithubOptions options)
{
this.HttpProxyPort = options.HttpProxyPort;
this.FallbackDns = options.FallbackDns;
this.domainConfigs = ConvertDomainConfigs(options.DomainConfigs);
this.domainConfigCache = new ConcurrentDictionary<string, DomainConfig?>();
}
/// <summary>
/// 配置转换
/// </summary>
/// <param name="domainConfigs"></param>
/// <returns></returns>
private static SortedDictionary<DomainPattern, DomainConfig> ConvertDomainConfigs(Dictionary<string, DomainConfig> domainConfigs)
{
var result = new SortedDictionary<DomainPattern, DomainConfig>();
foreach (var kv in domainConfigs)
{
result.Add(new DomainPattern(kv.Key), kv.Value);
}
return result;
}
/// <summary>
/// 是否匹配指定的域名
/// </summary>
/// <param name="domain"></param>
/// <returns></returns>
public bool IsMatch(string domain)
{
return this.TryGetDomainConfig(domain, out _);
}
/// <summary>
/// 尝试获取域名配置
/// </summary>
/// <param name="domain"></param>
/// <param name="value"></param>
/// <returns></returns>
public bool TryGetDomainConfig(string domain, [MaybeNullWhen(false)] out DomainConfig value)
{
value = this.domainConfigCache.GetOrAdd(domain, GetDomainConfig);
return value != null;
DomainConfig? GetDomainConfig(string domain)
{
var key = this.domainConfigs.Keys.FirstOrDefault(item => item.IsMatch(domain));
return key == null ? null : this.domainConfigs[key];
}
}
/// <summary>
/// 获取所有域名表达式
/// </summary>
/// <returns></returns>
public DomainPattern[] GetDomainPatterns()
{
return this.domainConfigs.Keys.ToArray();
}
}
}

View File

@ -0,0 +1,29 @@
using System;
namespace FastGithub.Configuration
{
/// <summary>
/// 表示FastGithub异常
/// </summary>
public class FastGithubException : Exception
{
/// <summary>
/// FastGithub异常
/// </summary>
/// <param name="message"></param>
public FastGithubException(string message)
: base(message)
{
}
/// <summary>
/// FastGithub异常
/// </summary>
/// <param name="message"></param>
/// <param name="innerException"></param>
public FastGithubException(string message, Exception? innerException)
: base(message, innerException)
{
}
}
}

View File

@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.Net;
namespace FastGithub.Configuration
{
/// <summary>
/// FastGithub的配置
/// </summary>
public class FastGithubOptions
{
/// <summary>
/// http代理端口
/// </summary>
public int HttpProxyPort { get; set; } = 38457;
/// <summary>
/// 回退的dns
/// </summary>
public IPEndPoint[] FallbackDns { get; set; } = Array.Empty<IPEndPoint>();
/// <summary>
/// 代理的域名配置
/// </summary>
public Dictionary<string, DomainConfig> DomainConfigs { get; set; } = new();
}
}

View File

@ -0,0 +1,137 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.NetworkInformation;
namespace FastGithub.Configuration
{
/// <summary>
/// 监听器
/// </summary>
public static class GlobalListener
{
private static readonly IPGlobalProperties global = IPGlobalProperties.GetIPGlobalProperties();
private static readonly HashSet<int> tcpListenPorts = GetListenPorts(global.GetActiveTcpListeners);
private static readonly HashSet<int> udpListenPorts = GetListenPorts(global.GetActiveUdpListeners);
/// <summary>
/// ssh端口
/// </summary>
public static int SshPort { get; } = GetAvailableTcpPort(22);
/// <summary>
/// git端口
/// </summary>
public static int GitPort { get; } = GetAvailableTcpPort(9418);
/// <summary>
/// http端口
/// </summary>
public static int HttpPort { get; } = OperatingSystem.IsWindows() ? GetAvailableTcpPort(80) : GetAvailableTcpPort(3880);
/// <summary>
/// https端口
/// </summary>
public static int HttpsPort { get; } = OperatingSystem.IsWindows() ? GetAvailableTcpPort(443) : GetAvailableTcpPort(38443);
/// <summary>
/// 获取已监听的端口
/// </summary>
/// <param name="func"></param>
/// <returns></returns>
private static HashSet<int> GetListenPorts(Func<IPEndPoint[]> func)
{
var hashSet = new HashSet<int>();
try
{
foreach (var endpoint in func())
{
hashSet.Add(endpoint.Port);
}
}
catch (Exception)
{
}
return hashSet;
}
/// <summary>
/// 是可以监听TCP
/// </summary>
/// <param name="port"></param>
/// <returns></returns>
public static bool CanListenTcp(int port)
{
return tcpListenPorts.Contains(port) == false;
}
/// <summary>
/// 是可以监听UDP
/// </summary>
/// <param name="port"></param>
/// <returns></returns>
public static bool CanListenUdp(int port)
{
return udpListenPorts.Contains(port) == false;
}
/// <summary>
/// 是可以监听TCP和Udp
/// </summary>
/// <param name="port"></param>
/// <returns></returns>
public static bool CanListen(int port)
{
return CanListenTcp(port) && CanListenUdp(port);
}
/// <summary>
/// 获取可用的随机Tcp端口
/// </summary>
/// <param name="minPort"></param>
/// <returns></returns>
public static int GetAvailableTcpPort(int minPort)
{
return GetAvailablePort(CanListenTcp, minPort);
}
/// <summary>
/// 获取可用的随机Udp端口
/// </summary>
/// <param name="minPort"></param>
/// <returns></returns>
public static int GetAvailableUdpPort(int minPort)
{
return GetAvailablePort(CanListenUdp, minPort);
}
/// <summary>
/// 获取可用的随机端口
/// </summary>
/// <param name="minPort"></param>
/// <returns></returns>
public static int GetAvailablePort(int minPort)
{
return GetAvailablePort(CanListen, minPort);
}
/// <summary>
/// 获取可用端口
/// </summary>
/// <param name="canFunc"></param>
/// <param name="minPort"></param>
/// <returns></returns>
/// <exception cref="FastGithubException"></exception>
private static int GetAvailablePort(Func<int, bool> canFunc, int minPort)
{
for (var port = minPort; port < IPEndPoint.MaxPort; port++)
{
if (canFunc(port) == true)
{
return port;
}
}
throw new FastGithubException("当前无可用的端口");
}
}
}

View File

@ -0,0 +1,86 @@
using Microsoft.Extensions.Logging;
using System;
namespace FastGithub
{
/// <summary>
/// 日志插值字符串扩展
/// </summary>
public static class LoggerExtensions
{
/// <summary>
/// 输出日志
/// </summary>
/// <param name="logger"></param>
/// <param name="level"></param>
/// <param name="formattableString"></param>
public static void Log(this ILogger logger, LogLevel level, FormattableString formattableString)
=> logger.Log(level, formattableString.Format, formattableString.GetArguments());
/// <summary>
/// 输出日志
/// </summary>
/// <param name="logger"></param>
/// <param name="level"></param>
/// <param name="error"></param>
/// <param name="formattableString"></param>
public static void Log(this ILogger logger, LogLevel level, Exception? error, FormattableString formattableString)
=> logger.Log(level, error, formattableString.Format, formattableString.GetArguments());
/// <summary>
/// 输出Trace日志
/// </summary>
/// <param name="logger"></param>
/// <param name="formattableString"></param>
public static void LogTrace(this ILogger logger, FormattableString formattableString)
=> logger.Log(LogLevel.Trace, formattableString);
/// <summary>
/// 输出Debug日志
/// </summary>
/// <param name="logger"></param>
/// <param name="formattableString"></param>
public static void LogDebug(this ILogger logger, FormattableString formattableString)
=> logger.Log(LogLevel.Debug, formattableString);
/// <summary>
/// 输出Information日志
/// </summary>
/// <param name="logger"></param>
/// <param name="formattableString"></param>
public static void LogInformation(this ILogger logger, FormattableString formattableString)
=> logger.Log(LogLevel.Information, formattableString);
/// <summary>
/// 输出Warning日志
/// </summary>
/// <param name="logger"></param>
/// <param name="formattableString"></param>
public static void LogWarning(this ILogger logger, FormattableString formattableString)
=> logger.Log(LogLevel.Warning, formattableString);
/// <summary>
/// 输出日志
/// </summary>
/// <param name="logger"></param>
/// <param name="formattableString"></param>
public static void LogError(this ILogger logger, FormattableString formattableString)
=> logger.Log(LogLevel.Error, formattableString);
/// <summary>
/// 输出日志
/// </summary>
/// <param name="logger"></param>
/// <param name="formattableString"></param>
public static void LogError(this ILogger logger, Exception error, FormattableString formattableString)
=> logger.Log(LogLevel.Error, error, formattableString);
/// <summary>
/// 输出Critical日志
/// </summary>
/// <param name="logger"></param>
/// <param name="formattableString"></param>
public static void LogCritical(this ILogger logger, FormattableString formattableString)
=> logger.Log(LogLevel.Critical, formattableString);
}
}

View File

@ -0,0 +1,23 @@
namespace FastGithub.Configuration
{
/// <summary>
/// 响应配置
/// </summary>
public record ResponseConfig
{
/// <summary>
/// 状态码
/// </summary>
public int StatusCode { get; init; } = 200;
/// <summary>
/// 内容类型
/// </summary>
public string ContentType { get; init; } = "text/plain;charset=utf-8";
/// <summary>
/// 内容的值
/// </summary>
public string? ContentValue { get; init; }
}
}

View File

@ -0,0 +1,27 @@
using FastGithub.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using System.Net;
namespace FastGithub
{
/// <summary>
/// 服务注册扩展
/// </summary>
public static class ServiceCollectionExtensions
{
/// <summary>
/// 添加配置服务
/// </summary>
/// <param name="services"></param>
/// <returns></returns>
public static IServiceCollection AddConfiguration(this IServiceCollection services)
{
TypeConverterBinder.Bind(val => IPAddress.Parse(val), val => val?.ToString());
TypeConverterBinder.Bind(val => IPEndPoint.Parse(val), val => val?.ToString());
services.TryAddSingleton<FastGithubConfig>();
return services;
}
}
}

View File

@ -0,0 +1,86 @@
using System;
using System.Net;
namespace FastGithub.Configuration
{
/// <summary>
/// Sni自定义值表达式
/// @domain变量表示取域名值
/// @ipadress变量表示取ip
/// @random变量表示取随机值
/// </summary>
public struct TlsSniPattern
{
/// <summary>
/// 获取表示式值
/// </summary>
public string Value { get; }
/// <summary>
/// 无SNI
/// </summary>
public static TlsSniPattern None { get; } = new TlsSniPattern(string.Empty);
/// <summary>
/// 域名SNI
/// </summary>
public static TlsSniPattern Domain { get; } = new TlsSniPattern("@domain");
/// <summary>
/// IP值的SNI
/// </summary>
public static TlsSniPattern IPAddress { get; } = new TlsSniPattern("@ipaddress");
/// <summary>
/// 随机值的SNI
/// </summary>
public static TlsSniPattern Random { get; } = new TlsSniPattern("@random");
/// <summary>
/// Sni自定义值表达式
/// </summary>
/// <param name="value">表示式值</param>
public TlsSniPattern(string? value)
{
this.Value = value ?? string.Empty;
}
/// <summary>
/// 更新域名
/// </summary>
/// <param name="domain"></param>
public TlsSniPattern WithDomain(string domain)
{
var value = this.Value.Replace(Domain.Value, domain, StringComparison.OrdinalIgnoreCase);
return new TlsSniPattern(value);
}
/// <summary>
/// 更新ip地址
/// </summary>
/// <param name="address"></param>
public TlsSniPattern WithIPAddress(IPAddress address)
{
var value = this.Value.Replace(IPAddress.Value, address.ToString(), StringComparison.OrdinalIgnoreCase);
return new TlsSniPattern(value);
}
/// <summary>
/// 更新随机数
/// </summary>
public TlsSniPattern WithRandom()
{
var value = this.Value.Replace(Random.Value, Environment.TickCount64.ToString(), StringComparison.OrdinalIgnoreCase);
return new TlsSniPattern(value);
}
/// <summary>
/// 转换为文本
/// </summary>
/// <returns></returns>
public override string ToString()
{
return this.Value;
}
}
}

View File

@ -0,0 +1,95 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
namespace FastGithub.Configuration
{
/// <summary>
/// TypeConverter类型转换绑定器
/// </summary>
static class TypeConverterBinder
{
private static readonly Dictionary<Type, Binder> binders = new();
/// <summary>
/// 绑定转换器到指定类型
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="reader"></param>
/// <param name="writer"></param>
public static void Bind<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>(Func<string, T?> reader, Func<T?, string?> writer)
{
binders[typeof(T)] = new Binder<T>(reader, writer);
var converterType = typeof(TypeConverter<>).MakeGenericType(typeof(T));
if (TypeDescriptor.GetConverter(typeof(T)).GetType() != converterType)
{
TypeDescriptor.AddAttributes(typeof(T), new TypeConverterAttribute(converterType));
}
}
private abstract class Binder
{
public abstract object? Read(string value);
public abstract string? Write(object? value);
}
private class Binder<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T> : Binder
{
private readonly Func<string, T?> reader;
private readonly Func<T?, string?> writer;
public Binder(Func<string, T?> reader, Func<T?, string?> writer)
{
this.reader = reader;
this.writer = writer;
}
public override object? Read(string value)
{
return this.reader(value);
}
public override string? Write(object? value)
{
return this.writer((T?)value);
}
}
private class TypeConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T> : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
{
return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);
}
public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
{
if (value is string stringVal)
{
if (stringVal.Equals(string.Empty))
{
return default(T);
}
else if (binders.TryGetValue(typeof(T), out var binder))
{
return binder.Read(stringVal);
}
}
return base.ConvertFrom(context, culture, value);
}
public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType)
{
return destinationType == typeof(T) && binders.TryGetValue(destinationType, out var binder)
? binder.Write(value)
: base.ConvertTo(context, culture, value, destinationType);
}
}
}
}

View File

@ -0,0 +1,359 @@
using DNS.Client;
using DNS.Client.RequestResolver;
using DNS.Protocol;
using DNS.Protocol.ResourceRecords;
using FastGithub.Configuration;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
namespace FastGithub.DomainResolve
{
/// <summary>
/// DNS客户端
/// </summary>
sealed class DnsClient
{
private const int DNS_PORT = 53;
private const string LOCALHOST = "localhost";
private readonly DnscryptProxy dnscryptProxy;
private readonly FastGithubConfig fastGithubConfig;
private readonly ILogger<DnsClient> logger;
private readonly ConcurrentDictionary<string, SemaphoreSlim> semaphoreSlims = new();
private readonly IMemoryCache dnsStateCache = new MemoryCache(Options.Create(new MemoryCacheOptions()));
private readonly IMemoryCache dnsLookupCache = new MemoryCache(Options.Create(new MemoryCacheOptions()));
private readonly TimeSpan stateExpiration = TimeSpan.FromMinutes(5d);
private readonly TimeSpan minTimeToLive = TimeSpan.FromSeconds(30d);
private readonly TimeSpan maxTimeToLive = TimeSpan.FromMinutes(10d);
private readonly int resolveTimeout = (int)TimeSpan.FromSeconds(4d).TotalMilliseconds;
private static readonly TimeSpan tcpConnectTimeout = TimeSpan.FromSeconds(2d);
private record LookupResult(IList<IPAddress> Addresses, TimeSpan TimeToLive);
/// <summary>
/// DNS客户端
/// </summary>
/// <param name="dnscryptProxy"></param>
/// <param name="fastGithubConfig"></param>
/// <param name="logger"></param>
public DnsClient(
DnscryptProxy dnscryptProxy,
FastGithubConfig fastGithubConfig,
ILogger<DnsClient> logger)
{
this.dnscryptProxy = dnscryptProxy;
this.fastGithubConfig = fastGithubConfig;
this.logger = logger;
}
/// <summary>
/// 解析域名
/// </summary>
/// <param name="endPoint">远程结节</param>
/// <param name="fastSort">是否使用快速排序</param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async IAsyncEnumerable<IPAddress> ResolveAsync(DnsEndPoint endPoint, bool fastSort, [EnumeratorCancellation] CancellationToken cancellationToken)
{
var hashSet = new HashSet<IPAddress>();
await foreach (var dns in this.GetDnsServersAsync(cancellationToken))
{
var addresses = await this.LookupAsync(dns, endPoint, fastSort, cancellationToken);
foreach (var address in addresses)
{
if (hashSet.Add(address) == true)
{
yield return address;
}
}
}
}
/// <summary>
/// 获取dns服务
/// </summary>
/// <returns></returns>
private async IAsyncEnumerable<IPEndPoint> GetDnsServersAsync([EnumeratorCancellation] CancellationToken cancellationToken)
{
var cryptDns = this.dnscryptProxy.LocalEndPoint;
if (cryptDns != null)
{
yield return cryptDns;
yield return cryptDns;
}
foreach (var dns in this.fastGithubConfig.FallbackDns)
{
if (await this.IsDnsAvailableAsync(dns, cancellationToken))
{
yield return dns;
}
}
}
/// <summary>
/// 获取dns是否可用
/// </summary>
/// <param name="dns"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
private async ValueTask<bool> IsDnsAvailableAsync(IPEndPoint dns, CancellationToken cancellationToken)
{
if (dns.Port != DNS_PORT)
{
return true;
}
if (this.dnsStateCache.TryGetValue<bool>(dns, out var available))
{
return available;
}
var key = dns.ToString();
var semaphore = this.semaphoreSlims.GetOrAdd(key, _ => new SemaphoreSlim(1, 1));
await semaphore.WaitAsync(CancellationToken.None);
try
{
using var timeoutTokenSource = new CancellationTokenSource(tcpConnectTimeout);
using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(timeoutTokenSource.Token, cancellationToken);
using var socket = new Socket(dns.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
await socket.ConnectAsync(dns, linkedTokenSource.Token);
return this.dnsStateCache.Set(dns, true, this.stateExpiration);
}
catch (Exception)
{
cancellationToken.ThrowIfCancellationRequested();
return this.dnsStateCache.Set(dns, false, this.stateExpiration);
}
finally
{
semaphore.Release();
}
}
/// <summary>
/// 解析域名
/// </summary>
/// <param name="dns"></param>
/// <param name="endPoint"></param>
/// <param name="fastSort"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
private async Task<IList<IPAddress>> LookupAsync(IPEndPoint dns, DnsEndPoint endPoint, bool fastSort, CancellationToken cancellationToken = default)
{
var key = $"{dns}/{endPoint}";
var semaphore = this.semaphoreSlims.GetOrAdd(key, _ => new SemaphoreSlim(1, 1));
await semaphore.WaitAsync(CancellationToken.None);
try
{
if (this.dnsLookupCache.TryGetValue<IList<IPAddress>>(key, out var value))
{
return value;
}
var result = await this.LookupCoreAsync(dns, endPoint, fastSort, cancellationToken);
return this.dnsLookupCache.Set(key, result.Addresses, result.TimeToLive);
}
catch (OperationCanceledException)
{
return Array.Empty<IPAddress>();
}
catch (Exception ex)
{
this.logger.LogWarning($"{endPoint.Host}@{dns}->{ex.Message}");
var expiration = IsSocketException(ex) ? this.maxTimeToLive : this.minTimeToLive;
return this.dnsLookupCache.Set(key, Array.Empty<IPAddress>(), expiration);
}
finally
{
semaphore.Release();
}
}
/// <summary>
/// 是否为Socket异常
/// </summary>
/// <param name="ex"></param>
/// <returns></returns>
private static bool IsSocketException(Exception ex)
{
if (ex is SocketException)
{
return true;
}
var inner = ex.InnerException;
return inner != null && IsSocketException(inner);
}
/// <summary>
/// 解析域名
/// </summary>
/// <param name="dns"></param>
/// <param name="endPoint"></param>
/// <param name="fastSort"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
private async Task<LookupResult> LookupCoreAsync(IPEndPoint dns, DnsEndPoint endPoint, bool fastSort, CancellationToken cancellationToken = default)
{
if (endPoint.Host == LOCALHOST)
{
var loopbacks = new List<IPAddress>();
if (Socket.OSSupportsIPv4 == true)
{
loopbacks.Add(IPAddress.Loopback);
}
if (Socket.OSSupportsIPv6 == true)
{
loopbacks.Add(IPAddress.IPv6Loopback);
}
return new LookupResult(loopbacks, TimeSpan.MaxValue);
}
var resolver = dns.Port == DNS_PORT
? (IRequestResolver)new TcpRequestResolver(dns)
: new UdpRequestResolver(dns, new TcpRequestResolver(dns), this.resolveTimeout);
var addressRecords = await GetAddressRecordsAsync(resolver, endPoint.Host, cancellationToken);
var addresses = (IList<IPAddress>)addressRecords
.Where(item => IPAddress.IsLoopback(item.IPAddress) == false)
.Select(item => item.IPAddress)
.ToArray();
if (addresses.Count == 0)
{
return new LookupResult(addresses, this.minTimeToLive);
}
if (fastSort == true)
{
addresses = await OrderByConnectAnyAsync(addresses, endPoint.Port, cancellationToken);
}
var timeToLive = addressRecords.Min(item => item.TimeToLive);
if (timeToLive <= TimeSpan.Zero)
{
timeToLive = this.minTimeToLive;
}
else if (timeToLive > this.maxTimeToLive)
{
timeToLive = this.maxTimeToLive;
}
return new LookupResult(addresses, timeToLive);
}
/// <summary>
/// 获取IP记录
/// </summary>
/// <param name="resolver"></param>
/// <param name="domain"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
private static async Task<IList<IPAddressResourceRecord>> GetAddressRecordsAsync(IRequestResolver resolver, string domain, CancellationToken cancellationToken)
{
var addressRecords = new List<IPAddressResourceRecord>();
if (Socket.OSSupportsIPv4 == true)
{
var records = await GetRecordsAsync(RecordType.A);
addressRecords.AddRange(records);
}
if (Socket.OSSupportsIPv6 == true)
{
var records = await GetRecordsAsync(RecordType.AAAA);
addressRecords.AddRange(records);
}
return addressRecords;
async Task<IEnumerable<IPAddressResourceRecord>> GetRecordsAsync(RecordType recordType)
{
var request = new Request
{
RecursionDesired = true,
OperationCode = OperationCode.Query
};
request.Questions.Add(new Question(new Domain(domain), recordType));
var clientRequest = new ClientRequest(resolver, request);
var response = await clientRequest.Resolve(cancellationToken);
return response.AnswerRecords.OfType<IPAddressResourceRecord>();
}
}
/// <summary>
/// 连接速度排序
/// </summary>
/// <param name="addresses"></param>
/// <param name="port"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
private static async Task<IList<IPAddress>> OrderByConnectAnyAsync(IList<IPAddress> addresses, int port, CancellationToken cancellationToken)
{
if (addresses.Count <= 1)
{
return addresses;
}
using var controlTokenSource = new CancellationTokenSource(tcpConnectTimeout);
using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, controlTokenSource.Token);
var connectTasks = addresses.Select(address => ConnectAsync(address, port, linkedTokenSource.Token));
var fastestAddress = await await Task.WhenAny(connectTasks);
controlTokenSource.Cancel();
if (fastestAddress == null || addresses.First().Equals(fastestAddress))
{
return addresses;
}
var list = new List<IPAddress> { fastestAddress };
foreach (var address in addresses)
{
if (address.Equals(fastestAddress) == false)
{
list.Add(address);
}
}
return list;
}
/// <summary>
/// 连接指定ip和端口
/// </summary>
/// <param name="address"></param>
/// <param name="port"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
private static async Task<IPAddress?> ConnectAsync(IPAddress address, int port, CancellationToken cancellationToken)
{
try
{
using var socket = new Socket(address.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
await socket.ConnectAsync(address, port, cancellationToken);
return address;
}
catch (Exception)
{
return default;
}
}
}
}

View File

@ -0,0 +1,154 @@
using FastGithub.Configuration;
using Microsoft.Extensions.Logging;
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using static PInvoke.AdvApi32;
namespace FastGithub.DomainResolve
{
/// <summary>
/// DnscryptProxy服务
/// </summary>
sealed class DnscryptProxy
{
private readonly ILogger<DnscryptProxy> logger;
private readonly string processName;
private readonly string serviceName;
private readonly string exeFilePath;
private readonly string tomlFilePath;
/// <summary>
/// 相关进程
/// </summary>
private Process? process;
/// <summary>
/// 获取监听的节点
/// </summary>
public IPEndPoint? LocalEndPoint { get; private set; }
/// <summary>
/// DnscryptProxy服务
/// </summary>
/// <param name="logger"></param>
public DnscryptProxy(ILogger<DnscryptProxy> logger)
{
const string PATH = "dnscrypt-proxy";
const string NAME = "dnscrypt-proxy";
this.logger = logger;
this.processName = NAME;
this.serviceName = $"{nameof(FastGithub)}.{NAME}";
this.exeFilePath = Path.Combine(PATH, OperatingSystem.IsWindows() ? $"{NAME}.exe" : NAME);
this.tomlFilePath = Path.Combine(PATH, $"{NAME}.toml");
}
/// <summary>
/// 启动dnscrypt-proxy
/// </summary>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task StartAsync(CancellationToken cancellationToken)
{
try
{
await this.StartCoreAsync(cancellationToken);
}
catch (Exception ex)
{
this.logger.LogWarning($"{this.processName}启动失败:{ex.Message}");
}
}
/// <summary>
/// 启动dnscrypt-proxy
/// </summary>
/// <param name="cancellationToken"></param>
/// <returns></returns>
private async Task StartCoreAsync(CancellationToken cancellationToken)
{
var port = GlobalListener.GetAvailablePort(5533);
var localEndPoint = new IPEndPoint(IPAddress.Loopback, port);
await TomlUtil.SetListensAsync(this.tomlFilePath, localEndPoint, cancellationToken);
await TomlUtil.SetLogLevelAsync(this.tomlFilePath, 6, cancellationToken);
await TomlUtil.SetLBStrategyAsync(this.tomlFilePath, "ph", cancellationToken);
await TomlUtil.SetMinMaxTTLAsync(this.tomlFilePath, TimeSpan.FromMinutes(1d), TimeSpan.FromMinutes(2d), cancellationToken);
if (OperatingSystem.IsWindows() && Environment.UserInteractive == false)
{
ServiceInstallUtil.StopAndDeleteService(this.serviceName);
ServiceInstallUtil.InstallAndStartService(this.serviceName, this.exeFilePath, ServiceStartType.SERVICE_DEMAND_START);
this.process = Process.GetProcessesByName(this.processName).FirstOrDefault(item => item.SessionId == 0);
}
else
{
this.process = StartDnscryptProxy();
}
if (this.process != null)
{
this.LocalEndPoint = localEndPoint;
this.process.EnableRaisingEvents = true;
this.process.Exited += (s, e) => this.LocalEndPoint = null;
}
}
/// <summary>
/// 停止服务
/// </summary>
public void Stop()
{
try
{
if (OperatingSystem.IsWindows() && Environment.UserInteractive == false)
{
ServiceInstallUtil.StopAndDeleteService(this.serviceName);
}
if (this.process != null && this.process.HasExited == false)
{
this.process.Kill();
}
}
catch (Exception ex)
{
this.logger.LogWarning($"{this.processName}停止失败:{ex.Message }");
}
finally
{
this.LocalEndPoint = null;
}
}
/// <summary>
/// 启动DnscryptProxy进程
/// </summary>
/// <returns></returns>
private Process? StartDnscryptProxy()
{
return Process.Start(new ProcessStartInfo
{
FileName = this.exeFilePath,
WorkingDirectory = Path.GetDirectoryName(this.exeFilePath),
UseShellExecute = false,
CreateNoWindow = true,
WindowStyle = ProcessWindowStyle.Hidden
});
}
/// <summary>
/// 转换为字符串
/// </summary>
/// <returns></returns>
public override string ToString()
{
return this.processName;
}
}
}

View File

@ -0,0 +1,73 @@
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace FastGithub.DomainResolve
{
/// <summary>
/// 域名解析后台服务
/// </summary>
sealed class DomainResolveHostedService : BackgroundService
{
private readonly DnscryptProxy dnscryptProxy;
private readonly IDomainResolver domainResolver;
private readonly ILogger<DomainResolveHostedService> logger;
private readonly TimeSpan dnscryptProxyInitDelay = TimeSpan.FromSeconds(5d);
private readonly TimeSpan testPeriodTimeSpan = TimeSpan.FromSeconds(1d);
/// <summary>
/// 域名解析后台服务
/// </summary>
/// <param name="dnscryptProxy"></param>
/// <param name="domainResolver"></param>
public DomainResolveHostedService(
DnscryptProxy dnscryptProxy,
IDomainResolver domainResolver,
ILogger<DomainResolveHostedService> logger)
{
this.dnscryptProxy = dnscryptProxy;
this.domainResolver = domainResolver;
this.logger = logger;
}
/// <summary>
/// 后台任务
/// </summary>
/// <param name="stoppingToken"></param>
/// <returns></returns>
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
try
{
await this.dnscryptProxy.StartAsync(stoppingToken);
await Task.Delay(dnscryptProxyInitDelay, stoppingToken);
while (stoppingToken.IsCancellationRequested == false)
{
await this.domainResolver.TestSpeedAsync(stoppingToken);
await Task.Delay(this.testPeriodTimeSpan, stoppingToken);
}
}
catch (OperationCanceledException)
{
}
catch (Exception ex)
{
this.logger.LogError(ex, "域名解析异常");
}
}
/// <summary>
/// 停止服务
/// </summary>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public override Task StopAsync(CancellationToken cancellationToken)
{
this.dnscryptProxy.Stop();
return base.StopAsync(cancellationToken);
}
}
}

View File

@ -0,0 +1,103 @@
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
namespace FastGithub.DomainResolve
{
/// <summary>
/// 域名解析器
/// </summary>
sealed class DomainResolver : IDomainResolver
{
private const int MAX_IP_COUNT = 3;
private readonly DnsClient dnsClient;
private readonly PersistenceService persistence;
private readonly IPAddressService addressService;
private readonly ILogger<DomainResolver> logger;
private readonly ConcurrentDictionary<DnsEndPoint, IPAddress[]> dnsEndPointAddress = new();
/// <summary>
/// 域名解析器
/// </summary>
/// <param name="dnsClient"></param>
/// <param name="persistence"></param>
/// <param name="addressService"></param>
/// <param name="logger"></param>
public DomainResolver(
DnsClient dnsClient,
PersistenceService persistence,
IPAddressService addressService,
ILogger<DomainResolver> logger)
{
this.dnsClient = dnsClient;
this.persistence = persistence;
this.addressService = addressService;
this.logger = logger;
foreach (var endPoint in persistence.ReadDnsEndPoints())
{
this.dnsEndPointAddress.TryAdd(endPoint, Array.Empty<IPAddress>());
}
}
/// <summary>
/// 解析域名
/// </summary>
/// <param name="endPoint">节点</param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async IAsyncEnumerable<IPAddress> ResolveAsync(DnsEndPoint endPoint, [EnumeratorCancellation] CancellationToken cancellationToken)
{
if (this.dnsEndPointAddress.TryGetValue(endPoint, out var addresses) && addresses.Length > 0)
{
foreach (var address in addresses)
{
yield return address;
}
}
else
{
if (this.dnsEndPointAddress.TryAdd(endPoint, Array.Empty<IPAddress>()))
{
await this.persistence.WriteDnsEndPointsAsync(this.dnsEndPointAddress.Keys, cancellationToken);
}
await foreach (var adddress in this.dnsClient.ResolveAsync(endPoint, fastSort: true, cancellationToken))
{
yield return adddress;
}
}
}
/// <summary>
/// 对所有节点进行测速
/// </summary>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task TestSpeedAsync(CancellationToken cancellationToken)
{
foreach (var keyValue in this.dnsEndPointAddress.OrderBy(item => item.Value.Length))
{
var dnsEndPoint = keyValue.Key;
var oldAddresses = keyValue.Value;
var newAddresses = await this.addressService.GetAddressesAsync(dnsEndPoint, oldAddresses, cancellationToken);
this.dnsEndPointAddress[dnsEndPoint] = newAddresses;
var oldSegmentums = oldAddresses.Take(MAX_IP_COUNT);
var newSegmentums = newAddresses.Take(MAX_IP_COUNT);
if (oldSegmentums.SequenceEqual(newSegmentums) == false)
{
var addressArray = string.Join(", ", newSegmentums.Select(item => item.ToString()));
this.logger.LogInformation($"{dnsEndPoint.Host}:{dnsEndPoint.Port}->[{addressArray}]");
}
}
}
}
}

View File

@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<PackageReference Include="PInvoke.AdvApi32" Version="0.7.124" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="6.0.1" />
<PackageReference Include="DNS" Version="7.0.0" />
<PackageReference Include="Tommy" Version="3.1.2" />
<ProjectReference Include="..\FastGithub.Configuration\FastGithub.Configuration.csproj" />
</ItemGroup>
<ItemGroup>
<None Include="../@dnscrypt-proxy/*" Link="dnscrypt-proxy/%(Filename)%(Extension)">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Include="../@dnscrypt-proxy/$(RuntimeIdentifier)/*" Link="dnscrypt-proxy/%(Filename)%(Extension)">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

@ -0,0 +1,28 @@
using System.Collections.Generic;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
namespace FastGithub.DomainResolve
{
/// <summary>
/// 域名解析器
/// </summary>
public interface IDomainResolver
{
/// <summary>
/// 解析所有ip
/// </summary>
/// <param name="endPoint">节点</param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
IAsyncEnumerable<IPAddress> ResolveAsync(DnsEndPoint endPoint, CancellationToken cancellationToken = default);
/// <summary>
/// 对所有节点进行测速
/// </summary>
/// <param name="cancellationToken"></param>
/// <returns></returns>
Task TestSpeedAsync(CancellationToken cancellationToken = default);
}
}

View File

@ -0,0 +1,142 @@
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
namespace FastGithub.DomainResolve
{
/// <summary>
/// IP服务
/// 域名IP关系缓存10分钟
/// IPEndPoint时延缓存5分钟
/// IPEndPoint连接超时5秒
/// </summary>
sealed class IPAddressService
{
private record DomainAddress(string Domain, IPAddress Address);
private readonly TimeSpan domainAddressExpiration = TimeSpan.FromMinutes(10d);
private readonly IMemoryCache domainAddressCache = new MemoryCache(Options.Create(new MemoryCacheOptions()));
private record AddressElapsed(IPAddress Address, TimeSpan Elapsed);
private readonly TimeSpan problemElapsedExpiration = TimeSpan.FromMinutes(1d);
private readonly TimeSpan normalElapsedExpiration = TimeSpan.FromMinutes(5d);
private readonly TimeSpan connectTimeout = TimeSpan.FromSeconds(5d);
private readonly IMemoryCache addressElapsedCache = new MemoryCache(Options.Create(new MemoryCacheOptions()));
private readonly DnsClient dnsClient;
/// <summary>
/// IP服务
/// </summary>
/// <param name="dnsClient"></param>
public IPAddressService(DnsClient dnsClient)
{
this.dnsClient = dnsClient;
}
/// <summary>
/// 并行获取可连接的IP
/// </summary>
/// <param name="dnsEndPoint"></param>
/// <param name="oldAddresses"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task<IPAddress[]> GetAddressesAsync(DnsEndPoint dnsEndPoint, IEnumerable<IPAddress> oldAddresses, CancellationToken cancellationToken)
{
var ipEndPoints = new HashSet<IPEndPoint>();
// 历史未过期的IP节点
foreach (var address in oldAddresses)
{
var domainAddress = new DomainAddress(dnsEndPoint.Host, address);
if (this.domainAddressCache.TryGetValue(domainAddress, out _))
{
ipEndPoints.Add(new IPEndPoint(address, dnsEndPoint.Port));
}
}
// 新解析出的IP节点
await foreach (var address in this.dnsClient.ResolveAsync(dnsEndPoint, fastSort: false, cancellationToken))
{
ipEndPoints.Add(new IPEndPoint(address, dnsEndPoint.Port));
var domainAddress = new DomainAddress(dnsEndPoint.Host, address);
this.domainAddressCache.Set(domainAddress, default(object), this.domainAddressExpiration);
}
if (ipEndPoints.Count == 0)
{
return Array.Empty<IPAddress>();
}
var addressElapsedTasks = ipEndPoints.Select(item => this.GetAddressElapsedAsync(item, cancellationToken));
var addressElapseds = await Task.WhenAll(addressElapsedTasks);
return addressElapseds
.Where(item => item.Elapsed < TimeSpan.MaxValue)
.OrderBy(item => item.Elapsed)
.Select(item => item.Address)
.ToArray();
}
/// <summary>
/// 获取IP节点的时延
/// </summary>
/// <param name="endPoint"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
private async Task<AddressElapsed> GetAddressElapsedAsync(IPEndPoint endPoint, CancellationToken cancellationToken)
{
if (this.addressElapsedCache.TryGetValue<AddressElapsed>(endPoint, out var addressElapsed))
{
return addressElapsed;
}
var stopWatch = Stopwatch.StartNew();
try
{
using var timeoutTokenSource = new CancellationTokenSource(this.connectTimeout);
using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutTokenSource.Token);
using var socket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
await socket.ConnectAsync(endPoint, linkedTokenSource.Token);
addressElapsed = new AddressElapsed(endPoint.Address, stopWatch.Elapsed);
return this.addressElapsedCache.Set(endPoint, addressElapsed, this.normalElapsedExpiration);
}
catch (Exception ex)
{
cancellationToken.ThrowIfCancellationRequested();
addressElapsed = new AddressElapsed(endPoint.Address, TimeSpan.MaxValue);
var expiration = IsLocalNetworkProblem(ex) ? this.problemElapsedExpiration : this.normalElapsedExpiration;
return this.addressElapsedCache.Set(endPoint, addressElapsed, expiration);
}
finally
{
stopWatch.Stop();
}
}
/// <summary>
/// 是否为本机网络问题
/// </summary>
/// <param name="ex"></param>
/// <returns></returns>
private static bool IsLocalNetworkProblem(Exception ex)
{
if (ex is not SocketException socketException)
{
return false;
}
var code = socketException.SocketErrorCode;
return code == SocketError.NetworkDown || code == SocketError.NetworkUnreachable;
}
}
}

View File

@ -0,0 +1,121 @@
using FastGithub.Configuration;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
namespace FastGithub.DomainResolve
{
/// <summary>
/// 域名持久化
/// </summary>
sealed partial class PersistenceService
{
private static readonly string dataFile = "dnsendpoints.json";
private static readonly SemaphoreSlim dataLocker = new(1, 1);
private readonly FastGithubConfig fastGithubConfig;
private readonly ILogger<PersistenceService> logger;
private record EndPointItem(string Host, int Port);
[JsonSerializable(typeof(EndPointItem[]))]
[JsonSourceGenerationOptions(
WriteIndented = true,
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
private partial class EndPointItemsContext : JsonSerializerContext
{
}
/// <summary>
/// 域名持久化
/// </summary>
/// <param name="fastGithubConfig"></param>
/// <param name="logger"></param>
public PersistenceService(
FastGithubConfig fastGithubConfig,
ILogger<PersistenceService> logger)
{
this.fastGithubConfig = fastGithubConfig;
this.logger = logger;
}
/// <summary>
/// 读取保存的节点
/// </summary>
/// <returns></returns>
public IList<DnsEndPoint> ReadDnsEndPoints()
{
if (File.Exists(dataFile) == false)
{
return Array.Empty<DnsEndPoint>();
}
try
{
dataLocker.Wait();
var utf8Json = File.ReadAllBytes(dataFile);
var endPointItems = JsonSerializer.Deserialize(utf8Json, EndPointItemsContext.Default.EndPointItemArray);
if (endPointItems == null)
{
return Array.Empty<DnsEndPoint>();
}
var dnsEndPoints = new List<DnsEndPoint>();
foreach (var item in endPointItems)
{
if (this.fastGithubConfig.IsMatch(item.Host) == true)
{
dnsEndPoints.Add(new DnsEndPoint(item.Host, item.Port));
}
}
return dnsEndPoints;
}
catch (Exception ex)
{
this.logger.LogWarning(ex.Message, "读取dns记录异常");
return Array.Empty<DnsEndPoint>();
}
finally
{
dataLocker.Release();
}
}
/// <summary>
/// 保存节点到文件
/// </summary>
/// <param name="dnsEndPoints"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task WriteDnsEndPointsAsync(IEnumerable<DnsEndPoint> dnsEndPoints, CancellationToken cancellationToken)
{
try
{
await dataLocker.WaitAsync(CancellationToken.None);
var endPointItems = dnsEndPoints.Select(item => new EndPointItem(item.Host, item.Port)).ToArray();
var utf8Json = JsonSerializer.SerializeToUtf8Bytes(endPointItems, EndPointItemsContext.Default.EndPointItemArray);
await File.WriteAllBytesAsync(dataFile, utf8Json, cancellationToken);
}
catch (Exception ex)
{
this.logger.LogWarning(ex.Message, "保存dns记录异常");
}
finally
{
dataLocker.Release();
}
}
}
}

View File

@ -0,0 +1,28 @@
using FastGithub.DomainResolve;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
namespace FastGithub
{
/// <summary>
/// 服务注册扩展
/// </summary>
public static class ServiceCollectionExtensions
{
/// <summary>
/// 注册域名解析相关服务
/// </summary>
/// <param name="services"></param>
/// <returns></returns>
public static IServiceCollection AddDomainResolve(this IServiceCollection services)
{
services.TryAddSingleton<DnsClient>();
services.TryAddSingleton<DnscryptProxy>();
services.TryAddSingleton<PersistenceService>();
services.TryAddSingleton<IPAddressService>();
services.TryAddSingleton<IDomainResolver, DomainResolver>();
services.AddHostedService<DomainResolveHostedService>();
return services;
}
}
}

View File

@ -0,0 +1,88 @@
using System.IO;
using System.Runtime.Versioning;
using static PInvoke.AdvApi32;
namespace FastGithub.DomainResolve
{
public static class ServiceInstallUtil
{
/// <summary>
/// 安装并启动服务
/// </summary>
/// <param name="serviceName"></param>
/// <param name="binaryPath"></param>
/// <param name="startType"></param>
/// <returns></returns>
[SupportedOSPlatform("windows")]
public static bool InstallAndStartService(string serviceName, string binaryPath, ServiceStartType startType = ServiceStartType.SERVICE_AUTO_START)
{
using var hSCManager = OpenSCManager(null, null, ServiceManagerAccess.SC_MANAGER_ALL_ACCESS);
if (hSCManager.IsInvalid == true)
{
return false;
}
var hService = OpenService(hSCManager, serviceName, ServiceAccess.SERVICE_ALL_ACCESS);
if (hService.IsInvalid == true)
{
hService = CreateService(
hSCManager,
serviceName,
serviceName,
ServiceAccess.SERVICE_ALL_ACCESS,
ServiceType.SERVICE_WIN32_OWN_PROCESS,
startType,
ServiceErrorControl.SERVICE_ERROR_NORMAL,
Path.GetFullPath(binaryPath),
lpLoadOrderGroup: null,
lpdwTagId: 0,
lpDependencies: null,
lpServiceStartName: null,
lpPassword: null);
}
if (hService.IsInvalid == true)
{
return false;
}
using (hService)
{
return StartService(hService, 0, null);
}
}
/// <summary>
/// 停止并删除服务
/// </summary>
/// <param name="serviceName"></param>
/// <returns></returns>
[SupportedOSPlatform("windows")]
public static bool StopAndDeleteService(string serviceName)
{
using var hSCManager = OpenSCManager(null, null, ServiceManagerAccess.SC_MANAGER_ALL_ACCESS);
if (hSCManager.IsInvalid == true)
{
return false;
}
using var hService = OpenService(hSCManager, serviceName, ServiceAccess.SERVICE_ALL_ACCESS);
if (hService.IsInvalid == true)
{
return true;
}
var status = new SERVICE_STATUS();
if (QueryServiceStatus(hService, ref status) == true)
{
if (status.dwCurrentState != ServiceState.SERVICE_STOP_PENDING &&
status.dwCurrentState != ServiceState.SERVICE_STOPPED)
{
ControlService(hService, ServiceControl.SERVICE_CONTROL_STOP, ref status);
}
}
return DeleteService(hService);
}
}
}

View File

@ -0,0 +1,98 @@
using System;
using System.IO;
using System.Net;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Tommy;
namespace FastGithub.DomainResolve
{
/// <summary>
/// doml配置工具
/// </summary>
static class TomlUtil
{
/// <summary>
/// 设置监听地址
/// </summary>
/// <param name="tomlPath"></param>
/// <param name="endpoint"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public static Task SetListensAsync(string tomlPath, IPEndPoint endpoint, CancellationToken cancellationToken)
{
var value = new TomlArray
{
endpoint.ToString()
};
return SetAsync(tomlPath, "listen_addresses", value, cancellationToken);
}
/// <summary>
/// 设置日志等级
/// </summary>
/// <param name="tomlPath"></param>
/// <param name="logLevel"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public static Task SetLogLevelAsync(string tomlPath, int logLevel, CancellationToken cancellationToken)
{
return SetAsync(tomlPath, "log_level", new TomlInteger { Value = logLevel }, cancellationToken);
}
/// <summary>
/// 设置负载均衡模式
/// </summary>
/// <param name="tomlPath"></param>
/// <param name="value"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public static Task SetLBStrategyAsync(string tomlPath, string value, CancellationToken cancellationToken)
{
return SetAsync(tomlPath, "lb_strategy", new TomlString { Value = value }, cancellationToken);
}
/// <summary>
/// 设置TTL
/// </summary>
/// <param name="tomlPath"></param>
/// <param name="minTTL"></param>
/// <param name="maxTTL"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public static async Task SetMinMaxTTLAsync(string tomlPath, TimeSpan minTTL, TimeSpan maxTTL, CancellationToken cancellationToken)
{
var minValue = new TomlInteger { Value = (int)minTTL.TotalSeconds };
var maxValue = new TomlInteger { Value = (int)maxTTL.TotalSeconds };
await SetAsync(tomlPath, "cache_min_ttl", minValue, cancellationToken);
await SetAsync(tomlPath, "cache_neg_min_ttl", minValue, cancellationToken);
await SetAsync(tomlPath, "cache_max_ttl", maxValue, cancellationToken);
await SetAsync(tomlPath, "cache_neg_max_ttl", maxValue, cancellationToken);
}
/// <summary>
/// 设置指定键的值
/// </summary>
/// <param name="tomlPath"></param>
/// <param name="key"></param>
/// <param name="value"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public static async Task SetAsync(string tomlPath, string key, TomlNode value, CancellationToken cancellationToken)
{
var toml = await File.ReadAllTextAsync(tomlPath, cancellationToken);
var reader = new StringReader(toml);
var tomlTable = TOML.Parse(reader);
tomlTable[key] = value;
var builder = new StringBuilder();
var writer = new StringWriter(builder);
tomlTable.WriteTo(writer);
toml = builder.ToString();
await File.WriteAllTextAsync(tomlPath, toml, cancellationToken);
}
}
}

View File

@ -0,0 +1,44 @@
using System;
using System.IO;
using System.IO.Pipelines;
using System.Threading.Tasks;
namespace FastGithub.FlowAnalyze
{
class DelegatingDuplexPipe<TDelegatingStream> : IDuplexPipe, IAsyncDisposable where TDelegatingStream : DelegatingStream
{
private bool disposed;
private readonly object syncRoot = new();
public PipeReader Input { get; }
public PipeWriter Output { get; }
public DelegatingDuplexPipe(IDuplexPipe duplexPipe, Func<Stream, TDelegatingStream> delegatingStreamFactory) :
this(duplexPipe, new StreamPipeReaderOptions(leaveOpen: true), new StreamPipeWriterOptions(leaveOpen: true), delegatingStreamFactory)
{
}
public DelegatingDuplexPipe(IDuplexPipe duplexPipe, StreamPipeReaderOptions readerOptions, StreamPipeWriterOptions writerOptions, Func<Stream, TDelegatingStream> delegatingStreamFactory)
{
var delegatingStream = delegatingStreamFactory(duplexPipe.AsStream());
this.Input = PipeReader.Create(delegatingStream, readerOptions);
this.Output = PipeWriter.Create(delegatingStream, writerOptions);
}
public virtual async ValueTask DisposeAsync()
{
lock (this.syncRoot)
{
if (this.disposed == true)
{
return;
}
this.disposed = true;
}
await this.Input.CompleteAsync();
await this.Output.CompleteAsync();
}
}
}

View File

@ -0,0 +1,142 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace FastGithub.FlowAnalyze
{
abstract class DelegatingStream : Stream
{
protected Stream Inner { get; }
public DelegatingStream(Stream inner)
{
this.Inner = inner;
}
public override bool CanRead
{
get
{
return this.Inner.CanRead;
}
}
public override bool CanSeek
{
get
{
return this.Inner.CanSeek;
}
}
public override bool CanWrite
{
get
{
return this.Inner.CanWrite;
}
}
public override long Length
{
get
{
return this.Inner.Length;
}
}
public override long Position
{
get
{
return this.Inner.Position;
}
set
{
this.Inner.Position = value;
}
}
public override void Flush()
{
this.Inner.Flush();
}
public override Task FlushAsync(CancellationToken cancellationToken)
{
return this.Inner.FlushAsync(cancellationToken);
}
public override int Read(byte[] buffer, int offset, int count)
{
return this.Inner.Read(buffer, offset, count);
}
public override int Read(Span<byte> destination)
{
return this.Inner.Read(destination);
}
public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
return this.Inner.ReadAsync(buffer, offset, count, cancellationToken);
}
public override ValueTask<int> ReadAsync(Memory<byte> destination, CancellationToken cancellationToken = default)
{
return this.Inner.ReadAsync(destination, cancellationToken);
}
public override long Seek(long offset, SeekOrigin origin)
{
return this.Inner.Seek(offset, origin);
}
public override void SetLength(long value)
{
this.Inner.SetLength(value);
}
public override void Write(byte[] buffer, int offset, int count)
{
this.Inner.Write(buffer, offset, count);
}
public override void Write(ReadOnlySpan<byte> source)
{
this.Inner.Write(source);
}
public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
return this.Inner.WriteAsync(buffer, offset, count, cancellationToken);
}
public override ValueTask WriteAsync(ReadOnlyMemory<byte> source, CancellationToken cancellationToken = default)
{
return this.Inner.WriteAsync(source, cancellationToken);
}
public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state)
{
return TaskToApm.Begin(ReadAsync(buffer, offset, count), callback, state);
}
public override int EndRead(IAsyncResult asyncResult)
{
return TaskToApm.End<int>(asyncResult);
}
public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state)
{
return TaskToApm.Begin(WriteAsync(buffer, offset, count), callback, state);
}
public override void EndWrite(IAsyncResult asyncResult)
{
TaskToApm.End(asyncResult);
}
}
}

View File

@ -0,0 +1,175 @@
using System;
using System.Buffers;
using System.IO;
using System.IO.Pipelines;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
namespace FastGithub.FlowAnalyze
{
static class DuplexPipeStreamExtensions
{
public static Stream AsStream(this IDuplexPipe duplexPipe, bool throwOnCancelled = false)
{
return new DuplexPipeStream(duplexPipe, throwOnCancelled);
}
private class DuplexPipeStream : Stream
{
private readonly PipeReader input;
private readonly PipeWriter output;
private readonly bool throwOnCancelled;
private volatile bool cancelCalled;
public DuplexPipeStream(IDuplexPipe duplexPipe, bool throwOnCancelled = false)
{
this.input = duplexPipe.Input;
this.output = duplexPipe.Output;
this.throwOnCancelled = throwOnCancelled;
}
public void CancelPendingRead()
{
this.cancelCalled = true;
this.input.CancelPendingRead();
}
public override bool CanRead => true;
public override bool CanSeek => false;
public override bool CanWrite => true;
public override long Length
{
get
{
throw new NotSupportedException();
}
}
public override long Position
{
get
{
throw new NotSupportedException();
}
set
{
throw new NotSupportedException();
}
}
public override long Seek(long offset, SeekOrigin origin)
{
throw new NotSupportedException();
}
public override void SetLength(long value)
{
throw new NotSupportedException();
}
public override int Read(byte[] buffer, int offset, int count)
{
ValueTask<int> vt = ReadAsyncInternal(new Memory<byte>(buffer, offset, count), default);
return vt.IsCompleted ?
vt.Result :
vt.AsTask().GetAwaiter().GetResult();
}
public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken = default)
{
return ReadAsyncInternal(new Memory<byte>(buffer, offset, count), cancellationToken).AsTask();
}
public override ValueTask<int> ReadAsync(Memory<byte> destination, CancellationToken cancellationToken = default)
{
return ReadAsyncInternal(destination, cancellationToken);
}
public override void Write(byte[] buffer, int offset, int count)
{
WriteAsync(buffer, offset, count).GetAwaiter().GetResult();
}
public override async Task WriteAsync(byte[]? buffer, int offset, int count, CancellationToken cancellationToken)
{
await this.output.WriteAsync(buffer.AsMemory(offset, count), cancellationToken);
}
public override async ValueTask WriteAsync(ReadOnlyMemory<byte> source, CancellationToken cancellationToken = default)
{
await this.output.WriteAsync(source, cancellationToken);
}
public override void Flush()
{
FlushAsync(CancellationToken.None).GetAwaiter().GetResult();
}
public override async Task FlushAsync(CancellationToken cancellationToken)
{
await this.output.FlushAsync(cancellationToken);
}
[AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder<>))]
private async ValueTask<int> ReadAsyncInternal(Memory<byte> destination, CancellationToken cancellationToken)
{
while (true)
{
var result = await this.input.ReadAsync(cancellationToken);
var readableBuffer = result.Buffer;
try
{
if (this.throwOnCancelled && result.IsCanceled && this.cancelCalled)
{
// Reset the bool
this.cancelCalled = false;
throw new OperationCanceledException();
}
if (!readableBuffer.IsEmpty)
{
// buffer.Count is int
var count = (int)Math.Min(readableBuffer.Length, destination.Length);
readableBuffer = readableBuffer.Slice(0, count);
readableBuffer.CopyTo(destination.Span);
return count;
}
if (result.IsCompleted)
{
return 0;
}
}
finally
{
this.input.AdvanceTo(readableBuffer.End, readableBuffer.End);
}
}
}
public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state)
{
return TaskToApm.Begin(ReadAsync(buffer, offset, count), callback, state);
}
public override int EndRead(IAsyncResult asyncResult)
{
return TaskToApm.End<int>(asyncResult);
}
public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state)
{
return TaskToApm.Begin(WriteAsync(buffer, offset, count), callback, state);
}
public override void EndWrite(IAsyncResult asyncResult)
{
TaskToApm.End(asyncResult);
}
}
}
}

View File

@ -0,0 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,12 @@
using System.IO.Pipelines;
namespace FastGithub.FlowAnalyze
{
sealed class FlowAnalyzeDuplexPipe : DelegatingDuplexPipe<FlowAnalyzeStream>
{
public FlowAnalyzeDuplexPipe(IDuplexPipe duplexPipe, IFlowAnalyzer flowAnalyzer) :
base(duplexPipe, stream => new FlowAnalyzeStream(stream, flowAnalyzer))
{
}
}
}

View File

@ -0,0 +1,71 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace FastGithub.FlowAnalyze
{
sealed class FlowAnalyzeStream : DelegatingStream
{
private readonly IFlowAnalyzer flowAnalyzer;
public FlowAnalyzeStream(Stream inner, IFlowAnalyzer flowAnalyzer)
: base(inner)
{
this.flowAnalyzer = flowAnalyzer;
}
public override int Read(byte[] buffer, int offset, int count)
{
int read = base.Read(buffer, offset, count);
this.flowAnalyzer.OnFlow(FlowType.Read, read);
return read;
}
public override int Read(Span<byte> destination)
{
int read = base.Read(destination);
this.flowAnalyzer.OnFlow(FlowType.Read, read);
return read;
}
public override async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
int read = await base.ReadAsync(buffer.AsMemory(offset, count), cancellationToken);
this.flowAnalyzer.OnFlow(FlowType.Read, read);
return read;
}
public override async ValueTask<int> ReadAsync(Memory<byte> destination, CancellationToken cancellationToken = default)
{
int read = await base.ReadAsync(destination, cancellationToken);
this.flowAnalyzer.OnFlow(FlowType.Read, read);
return read;
}
public override void Write(byte[] buffer, int offset, int count)
{
this.flowAnalyzer.OnFlow(FlowType.Wirte, count);
base.Write(buffer, offset, count);
}
public override void Write(ReadOnlySpan<byte> source)
{
this.flowAnalyzer.OnFlow(FlowType.Wirte, source.Length);
base.Write(source);
}
public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
this.flowAnalyzer.OnFlow(FlowType.Wirte, count);
return base.WriteAsync(buffer, offset, count, cancellationToken);
}
public override ValueTask WriteAsync(ReadOnlyMemory<byte> source, CancellationToken cancellationToken = default)
{
this.flowAnalyzer.OnFlow(FlowType.Wirte, source.Length);
return base.WriteAsync(source, cancellationToken);
}
}
}

View File

@ -0,0 +1,104 @@
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Threading;
namespace FastGithub.FlowAnalyze
{
sealed class FlowAnalyzer : IFlowAnalyzer
{
private const int INTERVAL_SECONDS = 5;
private readonly FlowQueues readQueues = new(INTERVAL_SECONDS);
private readonly FlowQueues writeQueues = new(INTERVAL_SECONDS);
/// <summary>
/// 收到数据
/// </summary>
/// <param name="flowType"></param>
/// <param name="length"></param>
public void OnFlow(FlowType flowType, int length)
{
if (flowType == FlowType.Read)
{
this.readQueues.OnFlow(length);
}
else
{
this.writeQueues.OnFlow(length);
}
}
/// <summary>
/// 获取流量分析
/// </summary>
/// <returns></returns>
public FlowStatistics GetFlowStatistics()
{
return new FlowStatistics
{
TotalRead = this.readQueues.TotalBytes,
TotalWrite = this.writeQueues.TotalBytes,
ReadRate = this.readQueues.GetRate(),
WriteRate = this.writeQueues.GetRate()
};
}
private class FlowQueues
{
private int cleaning = 0;
private long totalBytes = 0L;
private record QueueItem(long Ticks, int Length);
private readonly ConcurrentQueue<QueueItem> queues = new();
private readonly int intervalSeconds;
public long TotalBytes => this.totalBytes;
public FlowQueues(int intervalSeconds)
{
this.intervalSeconds = intervalSeconds;
}
public void OnFlow(int length)
{
Interlocked.Add(ref this.totalBytes, length);
this.CleanInvalidRecords();
this.queues.Enqueue(new QueueItem(Environment.TickCount64, length));
}
public double GetRate()
{
this.CleanInvalidRecords();
return (double)this.queues.Sum(item => item.Length) / this.intervalSeconds;
}
/// <summary>
/// 清除无效记录
/// </summary>
/// <returns></returns>
private bool CleanInvalidRecords()
{
if (Interlocked.CompareExchange(ref this.cleaning, 1, 0) != 0)
{
return false;
}
var ticks = Environment.TickCount64;
while (this.queues.TryPeek(out var item))
{
if (ticks - item.Ticks < this.intervalSeconds * 1000)
{
break;
}
else
{
this.queues.TryDequeue(out _);
}
}
Interlocked.Exchange(ref this.cleaning, 0);
return true;
}
}
}
}

View File

@ -0,0 +1,28 @@
namespace FastGithub.FlowAnalyze
{
/// <summary>
/// 流量统计
/// </summary>
public record FlowStatistics
{
/// <summary>
/// 获取总读上行
/// </summary>
public long TotalRead { get; init; }
/// <summary>
/// 获取总下行
/// </summary>
public long TotalWrite { get; init; }
/// <summary>
/// 获取读取速率
/// </summary>
public double ReadRate { get; init; }
/// <summary>
/// 获取写入速率
/// </summary>
public double WriteRate { get; init; }
}
}

View File

@ -0,0 +1,9 @@
using System.Text.Json.Serialization;
namespace FastGithub.FlowAnalyze
{
[JsonSerializable(typeof(FlowStatistics))]
public partial class FlowStatisticsContext : JsonSerializerContext
{
}
}

View File

@ -0,0 +1,18 @@
namespace FastGithub.FlowAnalyze
{
/// <summary>
/// 流量类型
/// </summary>
public enum FlowType
{
/// <summary>
/// 读取
/// </summary>
Read,
/// <summary>
/// 写入
/// </summary>
Wirte
}
}

View File

@ -0,0 +1,21 @@
namespace FastGithub.FlowAnalyze
{
/// <summary>
/// 流量分析器
/// </summary>
public interface IFlowAnalyzer
{
/// <summary>
/// 收到数据
/// </summary>
/// <param name="flowType"></param>
/// <param name="length"></param>
void OnFlow(FlowType flowType, int length);
/// <summary>
/// 获取速率
/// </summary>
/// <returns></returns>
FlowStatistics GetFlowStatistics();
}
}

View File

@ -0,0 +1,37 @@
using FastGithub.FlowAnalyze;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.Extensions.DependencyInjection;
namespace FastGithub
{
/// <summary>
/// ListenOptions扩展
/// </summary>
public static class ListenOptionsExtensions
{
/// <summary>
/// 使用流量分析中间件
/// </summary>
/// <param name="listen"></param>
/// <returns></returns>
public static ListenOptions UseFlowAnalyze(this ListenOptions listen)
{
var flowAnalyzer = listen.ApplicationServices.GetRequiredService<IFlowAnalyzer>();
listen.Use(next => async context =>
{
var oldTransport = context.Transport;
try
{
await using var loggingDuplexPipe = new FlowAnalyzeDuplexPipe(context.Transport, flowAnalyzer);
context.Transport = loggingDuplexPipe;
await next(context);
}
finally
{
context.Transport = oldTransport;
}
});
return listen;
}
}
}

View File

@ -0,0 +1,21 @@
using FastGithub.FlowAnalyze;
using Microsoft.Extensions.DependencyInjection;
namespace FastGithub
{
/// <summary>
/// ServiceCollection扩展
/// </summary>
public static class ServiceCollectionExtensions
{
/// <summary>
/// 添加流量分析
/// </summary>
/// <param name="services"></param>
/// <returns></returns>
public static IServiceCollection AddFlowAnalyze(this IServiceCollection services)
{
return services.AddSingleton<IFlowAnalyzer, FlowAnalyzer>();
}
}
}

View File

@ -0,0 +1,106 @@
using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
namespace FastGithub.FlowAnalyze
{
static class TaskToApm
{
/// <summary>
/// Marshals the Task as an IAsyncResult, using the supplied callback and state
/// to implement the APM pattern.
/// </summary>
/// <param name="task">The Task to be marshaled.</param>
/// <param name="callback">The callback to be invoked upon completion.</param>
/// <param name="state">The state to be stored in the IAsyncResult.</param>
/// <returns>An IAsyncResult to represent the task's asynchronous operation.</returns>
public static IAsyncResult Begin(Task task, AsyncCallback? callback, object? state) =>
new TaskAsyncResult(task, state, callback);
/// <summary>Processes an IAsyncResult returned by Begin.</summary>
/// <param name="asyncResult">The IAsyncResult to unwrap.</param>
public static void End(IAsyncResult asyncResult)
{
if (asyncResult is TaskAsyncResult twar)
{
twar._task.GetAwaiter().GetResult();
return;
}
throw new ArgumentNullException();
}
/// <summary>Processes an IAsyncResult returned by Begin.</summary>
/// <param name="asyncResult">The IAsyncResult to unwrap.</param>
public static TResult End<TResult>(IAsyncResult asyncResult)
{
if (asyncResult is TaskAsyncResult twar && twar._task is Task<TResult> task)
{
return task.GetAwaiter().GetResult();
}
throw new ArgumentNullException();
}
/// <summary>Provides a simple IAsyncResult that wraps a Task.</summary>
/// <remarks>
/// We could use the Task as the IAsyncResult if the Task's AsyncState is the same as the object state,
/// but that's very rare, in particular in a situation where someone cares about allocation, and always
/// using TaskAsyncResult simplifies things and enables additional optimizations.
/// </remarks>
internal sealed class TaskAsyncResult : IAsyncResult
{
/// <summary>The wrapped Task.</summary>
internal readonly Task _task;
/// <summary>Callback to invoke when the wrapped task completes.</summary>
private readonly AsyncCallback? _callback;
/// <summary>Initializes the IAsyncResult with the Task to wrap and the associated object state.</summary>
/// <param name="task">The Task to wrap.</param>
/// <param name="state">The new AsyncState value.</param>
/// <param name="callback">Callback to invoke when the wrapped task completes.</param>
internal TaskAsyncResult(Task task, object? state, AsyncCallback? callback)
{
Debug.Assert(task != null);
_task = task;
AsyncState = state;
if (task.IsCompleted)
{
// Synchronous completion. Invoke the callback. No need to store it.
CompletedSynchronously = true;
callback?.Invoke(this);
}
else if (callback != null)
{
// Asynchronous completion, and we have a callback; schedule it. We use OnCompleted rather than ContinueWith in
// order to avoid running synchronously if the task has already completed by the time we get here but still run
// synchronously as part of the task's completion if the task completes after (the more common case).
_callback = callback;
_task.ConfigureAwait(continueOnCapturedContext: false)
.GetAwaiter()
.OnCompleted(InvokeCallback); // allocates a delegate, but avoids a closure
}
}
/// <summary>Invokes the callback.</summary>
private void InvokeCallback()
{
Debug.Assert(!CompletedSynchronously);
Debug.Assert(_callback != null);
_callback.Invoke(this);
}
/// <summary>Gets a user-defined object that qualifies or contains information about an asynchronous operation.</summary>
public object? AsyncState { get; }
/// <summary>Gets a value that indicates whether the asynchronous operation completed synchronously.</summary>
/// <remarks>This is set lazily based on whether the <see cref="_task"/> has completed by the time this object is created.</remarks>
public bool CompletedSynchronously { get; }
/// <summary>Gets a value that indicates whether the asynchronous operation has completed.</summary>
public bool IsCompleted => _task.IsCompleted;
/// <summary>Gets a <see cref="WaitHandle"/> that is used to wait for an asynchronous operation to complete.</summary>
public WaitHandle AsyncWaitHandle => ((IAsyncResult)_task).AsyncWaitHandle;
}
}
}

View File

@ -0,0 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\FastGithub.DomainResolve\FastGithub.DomainResolve.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,58 @@
using FastGithub.Configuration;
using FastGithub.DomainResolve;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
namespace FastGithub.Http
{
/// <summary>
/// 表示http客户端
/// </summary>
public class HttpClient : HttpMessageInvoker
{
/// <summary>
/// 插入的UserAgent标记
/// </summary>
private readonly static ProductInfoHeaderValue userAgent = new(new ProductHeaderValue(nameof(FastGithub), "1.0"));
/// <summary>
/// http客户端
/// </summary>
/// <param name="domainConfig"></param>
/// <param name="domainResolver"></param>
public HttpClient(DomainConfig domainConfig, IDomainResolver domainResolver)
: this(new HttpClientHandler(domainConfig, domainResolver), disposeHandler: true)
{
}
/// <summary>
/// http客户端
/// </summary>
/// <param name="handler"></param>
/// <param name="disposeHandler"></param>
public HttpClient(HttpMessageHandler handler, bool disposeHandler)
: base(handler, disposeHandler)
{
}
/// <summary>
/// 发送请求
/// </summary>
/// <param name="request"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
if (request.Headers.UserAgent.Contains(userAgent))
{
throw new FastGithubException($"由于{request.RequestUri}实际指向了{nameof(FastGithub)}自身,{nameof(FastGithub)}已中断本次转发");
}
request.Headers.UserAgent.Add(userAgent);
var response = await base.SendAsync(request, cancellationToken);
response.Headers.Server.TryParseAdd(nameof(FastGithub));
return response;
}
}
}

View File

@ -0,0 +1,90 @@
using FastGithub.Configuration;
using FastGithub.DomainResolve;
using System;
using System.Collections.Concurrent;
namespace FastGithub.Http
{
/// <summary>
/// HttpClient工厂
/// </summary>
sealed class HttpClientFactory : IHttpClientFactory
{
private readonly IDomainResolver domainResolver;
/// <summary>
/// 首次生命周期
/// </summary>
private readonly TimeSpan firstLiftTime = TimeSpan.FromSeconds(10d);
/// <summary>
/// 非首次生命周期
/// </summary>
private readonly TimeSpan nextLifeTime = TimeSpan.FromSeconds(100d);
/// <summary>
/// LifetimeHttpHandler清理器
/// </summary>
private readonly LifetimeHttpHandlerCleaner httpHandlerCleaner = new();
/// <summary>
/// LazyOf(LifetimeHttpHandler)缓存
/// </summary>
private readonly ConcurrentDictionary<LifeTimeKey, Lazy<LifetimeHttpHandler>> httpHandlerLazyCache = new();
/// <summary>
/// HttpClient工厂
/// </summary>
/// <param name="domainResolver"></param>
public HttpClientFactory(IDomainResolver domainResolver)
{
this.domainResolver = domainResolver;
}
/// <summary>
/// 创建httpClient
/// </summary>
/// <param name="domain"></param>
/// <param name="domainConfig"></param>
/// <returns></returns>
public HttpClient CreateHttpClient(string domain, DomainConfig domainConfig)
{
var lifeTimeKey = new LifeTimeKey(domain, domainConfig);
var lifetimeHttpHandler = this.httpHandlerLazyCache.GetOrAdd(lifeTimeKey, CreateLifetimeHttpHandlerLazy).Value;
return new HttpClient(lifetimeHttpHandler, disposeHandler: false);
Lazy<LifetimeHttpHandler> CreateLifetimeHttpHandlerLazy(LifeTimeKey lifeTimeKey)
{
return new Lazy<LifetimeHttpHandler>(() => this.CreateLifetimeHttpHandler(lifeTimeKey, this.firstLiftTime), true);
}
}
/// <summary>
/// 创建LifetimeHttpHandler
/// </summary>
/// <param name="lifeTimeKey"></param>
/// <param name="lifeTime"></param>
/// <returns></returns>
private LifetimeHttpHandler CreateLifetimeHttpHandler(LifeTimeKey lifeTimeKey, TimeSpan lifeTime)
{
return new LifetimeHttpHandler(this.domainResolver, lifeTimeKey, lifeTime, this.OnLifetimeHttpHandlerDeactivate);
}
/// <summary>
/// 当有httpHandler失效时
/// </summary>
/// <param name="lifetimeHttpHandler">httpHandler</param>
private void OnLifetimeHttpHandlerDeactivate(LifetimeHttpHandler lifetimeHttpHandler)
{
var lifeTimeKey = lifetimeHttpHandler.LifeTimeKey;
this.httpHandlerLazyCache[lifeTimeKey] = CreateLifetimeHttpHandlerLazy(lifeTimeKey);
this.httpHandlerCleaner.Add(lifetimeHttpHandler);
Lazy<LifetimeHttpHandler> CreateLifetimeHttpHandlerLazy(LifeTimeKey lifeTimeKey)
{
return new Lazy<LifetimeHttpHandler>(() => this.CreateLifetimeHttpHandler(lifeTimeKey, this.nextLifeTime), true);
}
}
}
}

View File

@ -0,0 +1,236 @@
using FastGithub.Configuration;
using FastGithub.DomainResolve;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Security;
using System.Net.Sockets;
using System.Runtime.CompilerServices;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;
namespace FastGithub.Http
{
/// <summary>
/// HttpClientHandler
/// </summary>
class HttpClientHandler : DelegatingHandler
{
private readonly DomainConfig domainConfig;
private readonly IDomainResolver domainResolver;
private readonly TimeSpan connectTimeout = TimeSpan.FromSeconds(10d);
/// <summary>
/// HttpClientHandler
/// </summary>
/// <param name="domainConfig"></param>
/// <param name="domainResolver"></param>
public HttpClientHandler(DomainConfig domainConfig, IDomainResolver domainResolver)
{
this.domainConfig = domainConfig;
this.domainResolver = domainResolver;
this.InnerHandler = this.CreateSocketsHttpHandler();
}
/// <summary>
/// 发送请求
/// </summary>
/// <param name="request"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var uri = request.RequestUri;
if (uri == null)
{
throw new FastGithubException("必须指定请求的URI");
}
// 请求上下文信息
var isHttps = uri.Scheme == Uri.UriSchemeHttps;
var tlsSniValue = this.domainConfig.GetTlsSniPattern().WithDomain(uri.Host).WithRandom();
request.SetRequestContext(new RequestContext(isHttps, tlsSniValue));
// 设置请求头host修改协议为http
request.Headers.Host = uri.Host;
request.RequestUri = new UriBuilder(uri) { Scheme = Uri.UriSchemeHttp }.Uri;
if (this.domainConfig.Timeout != null)
{
using var timeoutTokenSource = new CancellationTokenSource(this.domainConfig.Timeout.Value);
using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutTokenSource.Token);
return await base.SendAsync(request, linkedTokenSource.Token);
}
return await base.SendAsync(request, cancellationToken);
}
/// <summary>
/// 创建转发代理的httpHandler
/// </summary>
/// <returns></returns>
private SocketsHttpHandler CreateSocketsHttpHandler()
{
return new SocketsHttpHandler
{
Proxy = null,
UseProxy = false,
UseCookies = false,
AllowAutoRedirect = false,
AutomaticDecompression = DecompressionMethods.None,
ConnectCallback = this.ConnectCallback
};
}
/// <summary>
/// 连接回调
/// </summary>
/// <param name="context"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
private async ValueTask<Stream> ConnectCallback(SocketsHttpConnectionContext context, CancellationToken cancellationToken)
{
var innerExceptions = new List<Exception>();
var ipEndPoints = this.GetIPEndPointsAsync(context.DnsEndPoint, cancellationToken);
await foreach (var ipEndPoint in ipEndPoints)
{
try
{
using var timeoutTokenSource = new CancellationTokenSource(this.connectTimeout);
using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(timeoutTokenSource.Token, cancellationToken);
return await this.ConnectAsync(context, ipEndPoint, linkedTokenSource.Token);
}
catch (OperationCanceledException)
{
cancellationToken.ThrowIfCancellationRequested();
innerExceptions.Add(new HttpConnectTimeoutException(ipEndPoint.Address));
}
catch (Exception ex)
{
innerExceptions.Add(ex);
}
}
throw new AggregateException("找不到任何可成功连接的IP", innerExceptions);
}
/// <summary>
/// 建立连接
/// </summary>
/// <param name="context"></param>
/// <param name="ipEndPoint"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
private async ValueTask<Stream> ConnectAsync(SocketsHttpConnectionContext context, IPEndPoint ipEndPoint, CancellationToken cancellationToken)
{
var socket = new Socket(ipEndPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
await socket.ConnectAsync(ipEndPoint, cancellationToken);
var stream = new NetworkStream(socket, ownsSocket: true);
var requestContext = context.InitialRequestMessage.GetRequestContext();
if (requestContext.IsHttps == false)
{
return stream;
}
var tlsSniValue = requestContext.TlsSniValue.WithIPAddress(ipEndPoint.Address);
var sslStream = new SslStream(stream, leaveInnerStreamOpen: false);
await sslStream.AuthenticateAsClientAsync(new SslClientAuthenticationOptions
{
TargetHost = tlsSniValue.Value,
RemoteCertificateValidationCallback = ValidateServerCertificate
}, cancellationToken);
return sslStream;
// 验证证书有效性
bool ValidateServerCertificate(object sender, X509Certificate? cert, X509Chain? chain, SslPolicyErrors errors)
{
if (errors.HasFlag(SslPolicyErrors.RemoteCertificateNameMismatch))
{
if (this.domainConfig.TlsIgnoreNameMismatch == true)
{
return true;
}
var domain = context.DnsEndPoint.Host;
var dnsNames = ReadDnsNames(cert);
return dnsNames.Any(dns => IsMatch(dns, domain));
}
return errors == SslPolicyErrors.None;
}
}
/// <summary>
/// 解析为IPEndPoint
/// </summary>
/// <param name="dnsEndPoint"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
private async IAsyncEnumerable<IPEndPoint> GetIPEndPointsAsync(DnsEndPoint dnsEndPoint, [EnumeratorCancellation] CancellationToken cancellationToken)
{
if (IPAddress.TryParse(dnsEndPoint.Host, out var address))
{
yield return new IPEndPoint(address, dnsEndPoint.Port);
}
else
{
if (this.domainConfig.IPAddress != null)
{
yield return new IPEndPoint(this.domainConfig.IPAddress, dnsEndPoint.Port);
}
await foreach (var item in this.domainResolver.ResolveAsync(dnsEndPoint, cancellationToken))
{
yield return new IPEndPoint(item, dnsEndPoint.Port);
}
}
}
/// <summary>
/// 读取使用的DNS名称
/// </summary>
/// <param name="cert"></param>
/// <returns></returns>
private static IEnumerable<string> ReadDnsNames(X509Certificate? cert)
{
if (cert is X509Certificate2 x509)
{
var extension = x509.Extensions.OfType<X509SubjectAlternativeNameExtension>().FirstOrDefault();
if (extension != null)
{
return extension.EnumerateDnsNames();
}
}
return Array.Empty<string>();
}
/// <summary>
/// 比较域名
/// </summary>
/// <param name="dnsName"></param>
/// <param name="domain"></param>
/// <returns></returns>
private static bool IsMatch(string dnsName, string? domain)
{
if (domain == null)
{
return false;
}
if (dnsName == domain)
{
return true;
}
if (dnsName[0] == '*')
{
return domain.EndsWith(dnsName[1..]);
}
return false;
}
}
}

View File

@ -0,0 +1,21 @@
using System;
using System.Net;
namespace FastGithub.Http
{
/// <summary>
/// http连接超时异常
/// </summary>
sealed class HttpConnectTimeoutException : Exception
{
/// <summary>
/// http连接超时异常
/// </summary>
/// <param name="address">连接的ip</param>
public HttpConnectTimeoutException(IPAddress address)
: base(address.ToString())
{
}
}
}

View File

@ -0,0 +1,18 @@
using FastGithub.Configuration;
namespace FastGithub.Http
{
/// <summary>
/// httpClient工厂
/// </summary>
public interface IHttpClientFactory
{
/// <summary>
/// 创建httpClient
/// </summary>
/// <param name="domain"></param>
/// <param name="domainConfig"></param>
/// <returns></returns>
HttpClient CreateHttpClient(string domain, DomainConfig domainConfig);
}
}

View File

@ -0,0 +1,31 @@
using FastGithub.Configuration;
namespace FastGithub.Http
{
/// <summary>
/// 生命周期的Key
/// </summary>
record LifeTimeKey
{
/// <summary>
/// 域名
/// </summary>
public string Domain { get; }
/// <summary>
/// 域名配置
/// </summary>
public DomainConfig DomainConfig { get; }
/// <summary>
/// 生命周期的Key
/// </summary>
/// <param name="domain"></param>
/// <param name="domainConfig"></param>
public LifeTimeKey(string domain, DomainConfig domainConfig)
{
this.Domain = domain;
this.DomainConfig = domainConfig;
}
}
}

View File

@ -0,0 +1,49 @@
using FastGithub.DomainResolve;
using System;
using System.Net.Http;
using System.Threading;
namespace FastGithub.Http
{
/// <summary>
/// 表示自主管理生命周期的的HttpMessageHandler
/// </summary>
sealed class LifetimeHttpHandler : DelegatingHandler
{
private readonly Timer timer;
public LifeTimeKey LifeTimeKey { get; }
/// <summary>
/// 具有生命周期的HttpHandler
/// </summary>
/// <param name="domainResolver"></param>
/// <param name="lifeTimeKey"></param>
/// <param name="lifeTime"></param>
/// <param name="deactivateAction"></param>
public LifetimeHttpHandler(IDomainResolver domainResolver, LifeTimeKey lifeTimeKey, TimeSpan lifeTime, Action<LifetimeHttpHandler> deactivateAction)
{
this.LifeTimeKey = lifeTimeKey;
this.InnerHandler = new HttpClientHandler(lifeTimeKey.DomainConfig, domainResolver);
this.timer = new Timer(this.OnTimerCallback, deactivateAction, lifeTime, Timeout.InfiniteTimeSpan);
}
/// <summary>
/// timer触发时
/// </summary>
/// <param name="state"></param>
private void OnTimerCallback(object? state)
{
this.timer.Dispose();
((Action<LifetimeHttpHandler>)(state!))(this);
}
/// <summary>
/// 这里不释放资源
/// </summary>
/// <param name="disposing"></param>
protected override void Dispose(bool disposing)
{
}
}
}

View File

@ -0,0 +1,133 @@
using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
namespace FastGithub.Http
{
/// <summary>
/// 表示LifetimeHttpHandler清理器
/// </summary>
sealed class LifetimeHttpHandlerCleaner
{
/// <summary>
/// 当前监视生命周期的记录的数量
/// </summary>
private int trackingEntryCount = 0;
/// <summary>
/// 监视生命周期的记录队列
/// </summary>
private readonly ConcurrentQueue<TrackingEntry> trackingEntries = new();
/// <summary>
/// 获取或设置清理的时间间隔
/// 默认10s
/// </summary>
public TimeSpan CleanupInterval { get; set; } = TimeSpan.FromSeconds(10d);
/// <summary>
/// 添加要清除的httpHandler
/// </summary>
/// <param name="handler">httpHandler</param>
public void Add(LifetimeHttpHandler handler)
{
var entry = new TrackingEntry(handler);
this.trackingEntries.Enqueue(entry);
// 从0变为1要启动清理作业
if (Interlocked.Increment(ref this.trackingEntryCount) == 1)
{
this.StartCleanup();
}
}
/// <summary>
/// 启动清理作业
/// </summary>
private async void StartCleanup()
{
await Task.Yield();
while (this.Cleanup() == false)
{
await Task.Delay(this.CleanupInterval);
}
}
/// <summary>
/// 清理失效的拦截器
/// 返回是否完全清理
/// </summary>
/// <returns></returns>
private bool Cleanup()
{
var cleanCount = this.trackingEntries.Count;
for (var i = 0; i < cleanCount; i++)
{
this.trackingEntries.TryDequeue(out var entry);
Debug.Assert(entry != null);
if (entry.CanDispose == false)
{
this.trackingEntries.Enqueue(entry);
continue;
}
entry.Dispose();
if (Interlocked.Decrement(ref this.trackingEntryCount) == 0)
{
return true;
}
}
return false;
}
/// <summary>
/// 表示监视生命周期的记录
/// </summary>
private class TrackingEntry : IDisposable
{
/// <summary>
/// 用于释放资源的对象
/// </summary>
private readonly IDisposable disposable;
/// <summary>
/// 监视对象的弱引用
/// </summary>
private readonly WeakReference weakReference;
/// <summary>
/// 获取是否可以释放资源
/// </summary>
/// <returns></returns>
public bool CanDispose => this.weakReference.IsAlive == false;
/// <summary>
/// 监视生命周期的记录
/// </summary>
/// <param name="handler">激活状态的httpHandler</param>
public TrackingEntry(LifetimeHttpHandler handler)
{
this.disposable = handler.InnerHandler!;
this.weakReference = new WeakReference(handler);
}
/// <summary>
/// 释放资源
/// </summary>
public void Dispose()
{
try
{
this.disposable.Dispose();
}
catch (Exception)
{
}
}
}
}
}

View File

@ -0,0 +1,31 @@
using FastGithub.Configuration;
namespace FastGithub.Http
{
/// <summary>
/// 表示请求上下文
/// </summary>
sealed class RequestContext
{
/// <summary>
/// 获取或设置是否为https请求
/// </summary>
public bool IsHttps { get; }
/// <summary>
/// 获取或设置Sni值
/// </summary>
public TlsSniPattern TlsSniValue { get; }
/// <summary>
/// 请求上下文
/// </summary>
/// <param name="isHttps"></param>
/// <param name="tlsSniValue"></param>
public RequestContext(bool isHttps, TlsSniPattern tlsSniValue)
{
IsHttps = isHttps;
TlsSniValue = tlsSniValue;
}
}
}

View File

@ -0,0 +1,35 @@
using System;
using System.Net.Http;
namespace FastGithub.Http
{
/// <summary>
/// 请求上下文扩展
/// </summary>
static class RequestContextExtensions
{
private static readonly HttpRequestOptionsKey<RequestContext> key = new(nameof(RequestContext));
/// <summary>
/// 设置RequestContext
/// </summary>
/// <param name="httpRequestMessage"></param>
/// <param name="requestContext"></param>
public static void SetRequestContext(this HttpRequestMessage httpRequestMessage, RequestContext requestContext)
{
httpRequestMessage.Options.Set(key, requestContext);
}
/// <summary>
/// 获取RequestContext
/// </summary>
/// <param name="httpRequestMessage"></param>
/// <returns></returns>
public static RequestContext GetRequestContext(this HttpRequestMessage httpRequestMessage)
{
return httpRequestMessage.Options.TryGetValue(key, out var requestContext)
? requestContext
: throw new InvalidOperationException($"请先调用{nameof(SetRequestContext)}");
}
}
}

View File

@ -0,0 +1,23 @@
using FastGithub.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
namespace FastGithub
{
/// <summary>
/// 服务注册扩展
/// </summary>
public static class ServiceCollectionExtensions
{
/// <summary>
/// 添加HttpClient相关服务
/// </summary>
/// <param name="services"></param>
/// <returns></returns>
public static IServiceCollection AddHttpClient(this IServiceCollection services)
{
services.TryAddSingleton<IHttpClientFactory, HttpClientFactory>();
return services;
}
}
}

View File

@ -0,0 +1,63 @@
using FastGithub.HttpServer.HttpMiddlewares;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
namespace FastGithub
{
/// <summary>
/// ApplicationBuilder扩展
/// </summary>
public static class ApplicationBuilderExtensions
{
/// <summary>
/// 使用http代理策略中间件
/// </summary>
/// <param name="app"></param>
/// <returns></returns>
public static IApplicationBuilder UseHttpProxyPac(this IApplicationBuilder app)
{
var middleware = app.ApplicationServices.GetRequiredService<HttpProxyPacMiddleware>();
return app.Use(next => context => middleware.InvokeAsync(context, next));
}
/// <summary>
/// 使用请求日志中间件
/// </summary>
/// <param name="app"></param>
/// <returns></returns>
public static IApplicationBuilder UseRequestLogging(this IApplicationBuilder app)
{
var middleware = app.ApplicationServices.GetRequiredService<RequestLoggingMiddleware>();
return app.Use(next => context => middleware.InvokeAsync(context, next));
}
/// <summary>
/// 禁用请求日志中间件
/// </summary>
/// <param name="app"></param>
/// <returns></returns>
public static IApplicationBuilder DisableRequestLogging(this IApplicationBuilder app)
{
return app.Use(next => context =>
{
var loggingFeature = context.Features.Get<IRequestLoggingFeature>();
if (loggingFeature != null)
{
loggingFeature.Enable = false;
}
return next(context);
});
}
/// <summary>
/// 使用反向代理中间件
/// </summary>
/// <param name="app"></param>
/// <returns></returns>
public static IApplicationBuilder UseHttpReverseProxy(this IApplicationBuilder app)
{
var middleware = app.ApplicationServices.GetRequiredService<HttpReverseProxyMiddleware>();
return app.Use(next => context => middleware.InvokeAsync(context, next));
}
}
}

View File

@ -0,0 +1,78 @@
using FastGithub;
using Microsoft.Extensions.Logging;
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
namespace FastGithub.HttpServer.Certs.CaCertInstallers
{
abstract class CaCertInstallerOfLinux : ICaCertInstaller
{
private readonly ILogger logger;
/// <summary>
/// 更新工具文件名
/// </summary>
protected abstract string CaCertUpdatePath { get; }
/// <summary>
/// 证书根目录
/// </summary>
protected abstract string CaCertStorePath { get; }
[DllImport("libc", SetLastError = true)]
private static extern uint geteuid();
public CaCertInstallerOfLinux(ILogger logger)
{
this.logger = logger;
}
/// <summary>
/// 是否支持
/// </summary>
/// <returns></returns>
public bool IsSupported()
{
return OperatingSystem.IsLinux() && File.Exists(CaCertUpdatePath);
}
/// <summary>
/// 安装ca证书
/// </summary>
/// <param name="caCertFilePath">证书文件路径</param>
public void Install(string caCertFilePath)
{
var destCertFilePath = Path.Combine(CaCertStorePath, Path.GetFileName(caCertFilePath));
if (File.Exists(destCertFilePath) && File.ReadAllBytes(caCertFilePath).SequenceEqual(File.ReadAllBytes(destCertFilePath)))
{
return;
}
if (geteuid() != 0)
{
logger.LogWarning($"无法自动安装CA证书{caCertFilePath}没有root权限");
return;
}
try
{
Directory.CreateDirectory(CaCertStorePath);
foreach (var item in Directory.GetFiles(CaCertStorePath, "fastgithub.*"))
{
File.Delete(item);
}
File.Copy(caCertFilePath, destCertFilePath, overwrite: true);
Process.Start(CaCertUpdatePath).WaitForExit();
logger.LogInformation($"已自动向系统安装CA证书{caCertFilePath}");
}
catch (Exception ex)
{
File.Delete(destCertFilePath);
logger.LogWarning(ex.Message, "自动安装CA证书异常");
}
}
}
}

View File

@ -0,0 +1,16 @@
using Microsoft.Extensions.Logging;
namespace FastGithub.HttpServer.Certs.CaCertInstallers
{
sealed class CaCertInstallerOfLinuxDebian : CaCertInstallerOfLinux
{
protected override string CaCertUpdatePath => "/usr/sbin/update-ca-certificates";
protected override string CaCertStorePath => "/usr/local/share/ca-certificates";
public CaCertInstallerOfLinuxDebian(ILogger<CaCertInstallerOfLinuxDebian> logger)
: base(logger)
{
}
}
}

View File

@ -0,0 +1,16 @@
using Microsoft.Extensions.Logging;
namespace FastGithub.HttpServer.Certs.CaCertInstallers
{
sealed class CaCertInstallerOfLinuxRedHat : CaCertInstallerOfLinux
{
protected override string CaCertUpdatePath => "/usr/bin/update-ca-trust";
protected override string CaCertStorePath => "/etc/pki/ca-trust/source/anchors";
public CaCertInstallerOfLinuxRedHat(ILogger<CaCertInstallerOfLinuxRedHat> logger)
: base(logger)
{
}
}
}

View File

@ -0,0 +1,33 @@
using Microsoft.Extensions.Logging;
using System;
namespace FastGithub.HttpServer.Certs.CaCertInstallers
{
sealed class CaCertInstallerOfMacOS : ICaCertInstaller
{
private readonly ILogger<CaCertInstallerOfMacOS> logger;
public CaCertInstallerOfMacOS(ILogger<CaCertInstallerOfMacOS> logger)
{
this.logger = logger;
}
/// <summary>
/// 是否支持
/// </summary>
/// <returns></returns>
public bool IsSupported()
{
return OperatingSystem.IsMacOS();
}
/// <summary>
/// 安装ca证书
/// </summary>
/// <param name="caCertFilePath">证书文件路径</param>
public void Install(string caCertFilePath)
{
logger.LogWarning($"请手动安装CA证书然后设置信任CA证书{caCertFilePath}");
}
}
}

View File

@ -0,0 +1,57 @@
using Microsoft.Extensions.Logging;
using System;
using System.Security.Cryptography.X509Certificates;
namespace FastGithub.HttpServer.Certs.CaCertInstallers
{
sealed class CaCertInstallerOfWindows : ICaCertInstaller
{
private readonly ILogger<CaCertInstallerOfWindows> logger;
public CaCertInstallerOfWindows(ILogger<CaCertInstallerOfWindows> logger)
{
this.logger = logger;
}
/// <summary>
/// 是否支持
/// </summary>
/// <returns></returns>
public bool IsSupported()
{
return OperatingSystem.IsWindows();
}
/// <summary>
/// 安装ca证书
/// </summary>
/// <param name="caCertFilePath">证书文件路径</param>
public void Install(string caCertFilePath)
{
try
{
using var store = new X509Store(StoreName.Root, StoreLocation.LocalMachine);
store.Open(OpenFlags.ReadWrite);
var caCert = new X509Certificate2(caCertFilePath);
var subjectName = caCert.Subject[3..];
foreach (var item in store.Certificates.Find(X509FindType.FindBySubjectName, subjectName, false))
{
if (item.Thumbprint != caCert.Thumbprint)
{
store.Remove(item);
}
}
if (store.Certificates.Find(X509FindType.FindByThumbprint, caCert.Thumbprint, true).Count == 0)
{
store.Add(caCert);
}
store.Close();
}
catch (Exception)
{
logger.LogWarning($"请手动安装CA证书{caCertFilePath}到“将所有的证书都放入下列存储”\\“受信任的根证书颁发机构”");
}
}
}
}

View File

@ -0,0 +1,155 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
namespace FastGithub.HttpServer.Certs
{
/// <summary>
/// 证书生成器
/// </summary>
static class CertGenerator
{
private static readonly Oid tlsServerOid = new("1.3.6.1.5.5.7.3.1");
private static readonly Oid tlsClientOid = new("1.3.6.1.5.5.7.3.2");
/// <summary>
/// 生成ca证书
/// </summary>
/// <param name="subjectName"></param>
/// <param name="notBefore"></param>
/// <param name="notAfter"></param>
/// <param name="rsaKeySizeInBits"></param>
/// <param name="pathLengthConstraint"></param>
/// <returns></returns>
public static X509Certificate2 CreateCACertificate(
X500DistinguishedName subjectName,
DateTimeOffset notBefore,
DateTimeOffset notAfter,
int rsaKeySizeInBits = 2048,
int pathLengthConstraint = 1)
{
using var rsa = RSA.Create(rsaKeySizeInBits);
var request = new CertificateRequest(subjectName, rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
var basicConstraints = new X509BasicConstraintsExtension(true, pathLengthConstraint > 0, pathLengthConstraint, true);
request.CertificateExtensions.Add(basicConstraints);
var keyUsage = new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature | X509KeyUsageFlags.CrlSign | X509KeyUsageFlags.KeyCertSign, true);
request.CertificateExtensions.Add(keyUsage);
var oids = new OidCollection { tlsServerOid, tlsClientOid };
var enhancedKeyUsage = new X509EnhancedKeyUsageExtension(oids, true);
request.CertificateExtensions.Add(enhancedKeyUsage);
var dnsBuilder = new SubjectAlternativeNameBuilder();
dnsBuilder.Add(subjectName.Name[3..]);
request.CertificateExtensions.Add(dnsBuilder.Build());
var subjectKeyId = new X509SubjectKeyIdentifierExtension(request.PublicKey, false);
request.CertificateExtensions.Add(subjectKeyId);
return request.CreateSelfSigned(notBefore, notAfter);
}
/// <summary>
/// 生成服务器证书
/// </summary>
/// <param name="issuerCertificate"></param>
/// <param name="subjectName"></param>
/// <param name="extraDnsNames"></param>
/// <param name="notBefore"></param>
/// <param name="notAfter"></param>
/// <param name="rsaKeySizeInBits"></param>
/// <returns></returns>
public static X509Certificate2 CreateEndCertificate(
X509Certificate2 issuerCertificate,
X500DistinguishedName subjectName,
IEnumerable<string>? extraDnsNames = default,
DateTimeOffset? notBefore = default,
DateTimeOffset? notAfter = default,
int rsaKeySizeInBits = 2048)
{
using var rsa = RSA.Create(rsaKeySizeInBits);
var request = new CertificateRequest(subjectName, rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
var basicConstraints = new X509BasicConstraintsExtension(false, false, 0, true);
request.CertificateExtensions.Add(basicConstraints);
var keyUsage = new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature | X509KeyUsageFlags.KeyEncipherment, true);
request.CertificateExtensions.Add(keyUsage);
var oids = new OidCollection { tlsServerOid, tlsClientOid };
var enhancedKeyUsage = new X509EnhancedKeyUsageExtension(oids, true);
request.CertificateExtensions.Add(enhancedKeyUsage);
var authorityKeyId = GetAuthorityKeyIdentifierExtension(issuerCertificate);
request.CertificateExtensions.Add(authorityKeyId);
var subjectKeyId = new X509SubjectKeyIdentifierExtension(request.PublicKey, false);
request.CertificateExtensions.Add(subjectKeyId);
var dnsBuilder = new SubjectAlternativeNameBuilder();
dnsBuilder.Add(subjectName.Name[3..]);
if (extraDnsNames != null)
{
foreach (var dnsName in extraDnsNames)
{
dnsBuilder.Add(dnsName);
}
}
var dnsNames = dnsBuilder.Build();
request.CertificateExtensions.Add(dnsNames);
if (notBefore == null || notBefore.Value < issuerCertificate.NotBefore)
{
notBefore = issuerCertificate.NotBefore;
}
if (notAfter == null || notAfter.Value > issuerCertificate.NotAfter)
{
notAfter = issuerCertificate.NotAfter;
}
var serialNumber = BitConverter.GetBytes(Random.Shared.NextInt64());
using var certOnly = request.Create(issuerCertificate, notBefore.Value, notAfter.Value, serialNumber);
return certOnly.CopyWithPrivateKey(rsa);
}
private static void Add(this SubjectAlternativeNameBuilder builder, string name)
{
if (IPAddress.TryParse(name, out var address))
{
builder.AddIpAddress(address);
}
else
{
builder.AddDnsName(name);
}
}
private static X509Extension GetAuthorityKeyIdentifierExtension(X509Certificate2 certificate)
{
var extension = new X509SubjectKeyIdentifierExtension(certificate.PublicKey, false);
#if NET7_0_OR_GREATER
return X509AuthorityKeyIdentifierExtension.CreateFromSubjectKeyIdentifier(extension);
#else
var subjectKeyIdentifier = extension.RawData.AsSpan(2);
var rawData = new byte[subjectKeyIdentifier.Length + 4];
rawData[0] = 0x30;
rawData[1] = 0x16;
rawData[2] = 0x80;
rawData[3] = 0x14;
subjectKeyIdentifier.CopyTo(rawData);
return new X509Extension("2.5.29.35", rawData, false);
#endif
}
}
}

View File

@ -0,0 +1,172 @@
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
namespace FastGithub.HttpServer.Certs
{
/// <summary>
/// 证书服务
/// </summary>
sealed class CertService
{
private const string CACERT_PATH = "cacert";
private readonly IMemoryCache serverCertCache;
private readonly IEnumerable<ICaCertInstaller> certInstallers;
private readonly ILogger<CertService> logger;
private X509Certificate2? caCert;
/// <summary>
/// 获取证书文件路径
/// </summary>
public string CaCerFilePath { get; } = OperatingSystem.IsLinux() ? $"{CACERT_PATH}/fastgithub.crt" : $"{CACERT_PATH}/fastgithub.cer";
/// <summary>
/// 获取私钥文件路径
/// </summary>
public string CaKeyFilePath { get; } = $"{CACERT_PATH}/fastgithub.key";
/// <summary>
/// 证书服务
/// </summary>
/// <param name="serverCertCache"></param>
/// <param name="certInstallers"></param>
/// <param name="logger"></param>
public CertService(
IMemoryCache serverCertCache,
IEnumerable<ICaCertInstaller> certInstallers,
ILogger<CertService> logger)
{
this.serverCertCache = serverCertCache;
this.certInstallers = certInstallers;
this.logger = logger;
Directory.CreateDirectory(CACERT_PATH);
}
/// <summary>
/// 生成CA证书
/// </summary>
public bool CreateCaCertIfNotExists()
{
if (File.Exists(this.CaCerFilePath) && File.Exists(this.CaKeyFilePath))
{
return false;
}
File.Delete(this.CaCerFilePath);
File.Delete(this.CaKeyFilePath);
var notBefore = DateTimeOffset.Now.AddDays(-1);
var notAfter = DateTimeOffset.Now.AddYears(10);
var subjectName = new X500DistinguishedName($"CN={nameof(FastGithub)}");
this.caCert = CertGenerator.CreateCACertificate(subjectName, notBefore, notAfter);
var privateKeyPem = this.caCert.GetRSAPrivateKey()?.ExportRSAPrivateKeyPem();
File.WriteAllText(this.CaKeyFilePath, new string(privateKeyPem), Encoding.ASCII);
var certPem = this.caCert.ExportCertificatePem();
File.WriteAllText(this.CaCerFilePath, new string(certPem), Encoding.ASCII);
return true;
}
/// <summary>
/// 安装和信任CA证书
/// </summary>
public void InstallAndTrustCaCert()
{
var installer = this.certInstallers.FirstOrDefault(item => item.IsSupported());
if (installer != null)
{
installer.Install(this.CaCerFilePath);
}
else
{
this.logger.LogWarning($"请根据你的系统平台手动安装和信任CA证书{this.CaCerFilePath}");
}
GitConfigSslverify(false);
}
/// <summary>
/// 设置ssl验证
/// </summary>
/// <param name="value">是否验证</param>
/// <returns></returns>
public static bool GitConfigSslverify(bool value)
{
try
{
Process.Start(new ProcessStartInfo
{
FileName = "git",
Arguments = $"config --global http.sslverify {value.ToString().ToLower()}",
UseShellExecute = true,
CreateNoWindow = true,
WindowStyle = ProcessWindowStyle.Hidden
});
return true;
}
catch (Exception)
{
return false;
}
}
/// <summary>
/// 获取颁发给指定域名的证书
/// </summary>
/// <param name="domain"></param>
/// <returns></returns>
public X509Certificate2 GetOrCreateServerCert(string? domain)
{
if (this.caCert == null)
{
using var rsa = RSA.Create();
rsa.ImportFromPem(File.ReadAllText(this.CaKeyFilePath));
this.caCert = new X509Certificate2(this.CaCerFilePath).CopyWithPrivateKey(rsa);
}
var key = $"{nameof(CertService)}:{domain}";
var endCert = this.serverCertCache.GetOrCreate(key, GetOrCreateCert);
return endCert!;
// 生成域名的1年证书
X509Certificate2 GetOrCreateCert(ICacheEntry entry)
{
var notBefore = DateTimeOffset.Now.AddDays(-1);
var notAfter = DateTimeOffset.Now.AddYears(1);
entry.SetAbsoluteExpiration(notAfter);
var extraDomains = GetExtraDomains();
var subjectName = new X500DistinguishedName($"CN={domain}");
var endCert = CertGenerator.CreateEndCertificate(this.caCert, subjectName, extraDomains, notBefore, notAfter);
// 重新初始化证书以兼容win平台不能使用内存证书
return new X509Certificate2(endCert.Export(X509ContentType.Pfx));
}
}
/// <summary>
/// 获取域名
/// </summary>
/// <param name="domain"></param>
/// <returns></returns>
private static IEnumerable<string> GetExtraDomains()
{
yield return Environment.MachineName;
yield return IPAddress.Loopback.ToString();
yield return IPAddress.IPv6Loopback.ToString();
}
}
}

View File

@ -0,0 +1,20 @@
namespace FastGithub.HttpServer.Certs
{
/// <summary>
/// CA证书安装器
/// </summary>
interface ICaCertInstaller
{
/// <summary>
/// 是否支持
/// </summary>
/// <returns></returns>
bool IsSupported();
/// <summary>
/// 安装ca证书
/// </summary>
/// <param name="caCertFilePath">证书文件路径</param>
void Install(string caCertFilePath);
}
}

View File

@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Yarp.ReverseProxy" Version="1.1.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\FastGithub.FlowAnalyze\FastGithub.FlowAnalyze.csproj" />
<ProjectReference Include="..\FastGithub.Http\FastGithub.Http.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,68 @@
using FastGithub.Configuration;
using FastGithub.HttpServer.TcpMiddlewares;
using Microsoft.AspNetCore.Http;
using System.IO;
using System.Text;
using System.Threading.Tasks;
namespace FastGithub.HttpServer.HttpMiddlewares
{
/// <summary>
/// http代理策略中间件
/// </summary>
sealed class HttpProxyPacMiddleware
{
private readonly FastGithubConfig fastGithubConfig;
/// <summary>
/// http代理策略中间件
/// </summary>
/// <param name="fastGithubConfig"></param>
public HttpProxyPacMiddleware(FastGithubConfig fastGithubConfig)
{
this.fastGithubConfig = fastGithubConfig;
}
/// <summary>
/// 处理请求
/// </summary>
/// <param name="context"></param>
/// <param name="next"></param>
/// <returns></returns>
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
// http请求经过了httpProxy中间件
var proxyFeature = context.Features.Get<IHttpProxyFeature>();
if (proxyFeature != null && proxyFeature.ProxyProtocol == ProxyProtocol.None)
{
var proxyPac = this.CreateProxyPac(context.Request.Host);
context.Response.ContentType = "application/x-ns-proxy-autoconfig";
context.Response.Headers.Add("Content-Disposition", $"attachment;filename=proxy.pac");
await context.Response.WriteAsync(proxyPac);
}
else
{
await next(context);
}
}
/// <summary>
/// 创建proxypac脚本
/// </summary>
/// <param name="proxyHost"></param>
/// <returns></returns>
private string CreateProxyPac(HostString proxyHost)
{
var buidler = new StringBuilder();
buidler.AppendLine("function FindProxyForURL(url, host){");
buidler.AppendLine($" var fastgithub = 'PROXY {proxyHost}';");
foreach (var domain in fastGithubConfig.GetDomainPatterns())
{
buidler.AppendLine($" if (shExpMatch(host, '{domain}')) return fastgithub;");
}
buidler.AppendLine(" return 'DIRECT';");
buidler.AppendLine("}");
return buidler.ToString();
}
}
}

View File

@ -0,0 +1,140 @@
using FastGithub.Configuration;
using FastGithub.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using System;
using System.Diagnostics.CodeAnalysis;
using System.Net;
using System.Threading.Tasks;
using Yarp.ReverseProxy.Forwarder;
namespace FastGithub.HttpServer.HttpMiddlewares
{
/// <summary>
/// 反向代理中间件
/// </summary>
sealed class HttpReverseProxyMiddleware
{
private static readonly DomainConfig defaultDomainConfig = new() { TlsSni = true };
private readonly IHttpForwarder httpForwarder;
private readonly IHttpClientFactory httpClientFactory;
private readonly FastGithubConfig fastGithubConfig;
private readonly ILogger<HttpReverseProxyMiddleware> logger;
public HttpReverseProxyMiddleware(
IHttpForwarder httpForwarder,
IHttpClientFactory httpClientFactory,
FastGithubConfig fastGithubConfig,
ILogger<HttpReverseProxyMiddleware> logger)
{
this.httpForwarder = httpForwarder;
this.httpClientFactory = httpClientFactory;
this.fastGithubConfig = fastGithubConfig;
this.logger = logger;
}
/// <summary>
/// 处理请求
/// </summary>
/// <param name="context"></param>
/// <param name="next"?
/// <returns></returns>
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
var host = context.Request.Host;
if (TryGetDomainConfig(host, out var domainConfig) == false)
{
await next(context);
}
else if (domainConfig.Response == null)
{
var scheme = context.Request.Scheme;
var destinationPrefix = GetDestinationPrefix(scheme, host, domainConfig.Destination);
var httpClient = httpClientFactory.CreateHttpClient(host.Host, domainConfig);
var error = await httpForwarder.SendAsync(context, destinationPrefix, httpClient, ForwarderRequestConfig.Empty, HttpTransformer.Empty);
await HandleErrorAsync(context, error);
}
else
{
context.Response.StatusCode = domainConfig.Response.StatusCode;
context.Response.ContentType = domainConfig.Response.ContentType;
if (domainConfig.Response.ContentValue != null)
{
await context.Response.WriteAsync(domainConfig.Response.ContentValue);
}
}
}
/// <summary>
/// 获取域名的DomainConfig
/// </summary>
/// <param name="host"></param>
/// <param name="domainConfig"></param>
/// <returns></returns>
private bool TryGetDomainConfig(HostString host, [MaybeNullWhen(false)] out DomainConfig domainConfig)
{
if (fastGithubConfig.TryGetDomainConfig(host.Host, out domainConfig) == true)
{
return true;
}
// 未配置的域名但仍然被解析到本机ip的域名
if (OperatingSystem.IsWindows() && IsDomain(host.Host))
{
logger.LogWarning($"域名{host.Host}可能已经被DNS污染如果域名为本机域名请解析为非回环IP");
domainConfig = defaultDomainConfig;
return true;
}
return false;
// 是否为域名
static bool IsDomain(string host)
{
return IPAddress.TryParse(host, out _) == false && host.Contains('.');
}
}
/// <summary>
/// 获取目标前缀
/// </summary>
/// <param name="scheme"></param>
/// <param name="host"></param>
/// <param name="destination"></param>
/// <returns></returns>
private string GetDestinationPrefix(string scheme, HostString host, Uri? destination)
{
var defaultValue = $"{scheme}://{host}/";
if (destination == null)
{
return defaultValue;
}
var baseUri = new Uri(defaultValue);
var result = new Uri(baseUri, destination).ToString();
logger.LogInformation($"{defaultValue} => {result}");
return result;
}
/// <summary>
/// 处理错误信息
/// </summary>
/// <param name="context"></param>
/// <param name="error"></param>
/// <returns></returns>
private static async Task HandleErrorAsync(HttpContext context, ForwarderError error)
{
if (error == ForwarderError.None || context.Response.HasStarted)
{
return;
}
await context.Response.WriteAsJsonAsync(new
{
error = error.ToString(),
message = context.GetForwarderErrorFeature()?.Exception?.Message
});
}
}
}

View File

@ -0,0 +1,13 @@
namespace FastGithub.HttpServer.HttpMiddlewares
{
/// <summary>
/// 请求日志特性
/// </summary>
public interface IRequestLoggingFeature
{
/// <summary>
/// 是否启用
/// </summary>
bool Enable { get; set; }
}
}

View File

@ -0,0 +1,134 @@
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using System;
using System.Diagnostics;
using System.Text;
using System.Threading.Tasks;
namespace FastGithub.HttpServer.HttpMiddlewares
{
/// <summary>
/// 请求日志中间件
/// </summary>
sealed class RequestLoggingMiddleware
{
private readonly ILogger<RequestLoggingMiddleware> logger;
/// <summary>
/// 请求日志中间件
/// </summary>
/// <param name="logger"></param>
public RequestLoggingMiddleware(ILogger<RequestLoggingMiddleware> logger)
{
this.logger = logger;
}
/// <summary>
/// 执行请求
/// </summary>
/// <param name="context"></param>
/// <param name="next"></param>
/// <returns></returns>
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
var feature = new RequestLoggingFeature();
context.Features.Set<IRequestLoggingFeature>(feature);
var stopwatch = Stopwatch.StartNew();
try
{
await next(context);
}
finally
{
stopwatch.Stop();
}
if (feature.Enable == false)
{
return;
}
var request = context.Request;
var response = context.Response;
var exception = context.GetForwarderErrorFeature()?.Exception;
if (exception == null)
{
logger.LogInformation($"{request.Method} {request.Scheme}://{request.Host}{request.Path} responded {response.StatusCode} in {stopwatch.Elapsed.TotalMilliseconds} ms");
}
else if (IsError(exception))
{
logger.LogError($"{request.Method} {request.Scheme}://{request.Host}{request.Path} responded {response.StatusCode} in {stopwatch.Elapsed.TotalMilliseconds} ms{Environment.NewLine}{exception}");
}
else
{
logger.LogWarning($"{request.Method} {request.Scheme}://{request.Host}{request.Path} responded {response.StatusCode} in {stopwatch.Elapsed.TotalMilliseconds} ms{Environment.NewLine}{GetMessage(exception)}");
}
}
/// <summary>
/// 是否为错误
/// </summary>
/// <param name="exception"></param>
/// <returns></returns>
private static bool IsError(Exception exception)
{
if (exception is OperationCanceledException)
{
return false;
}
if (HasInnerException<ConnectionAbortedException>(exception))
{
return false;
}
return true;
}
/// <summary>
/// 是否有内部异常异常
/// </summary>
/// <typeparam name="TInnerException"></typeparam>
/// <param name="exception"></param>
/// <returns></returns>
private static bool HasInnerException<TInnerException>(Exception exception) where TInnerException : Exception
{
var inner = exception.InnerException;
while (inner != null)
{
if (inner is TInnerException)
{
return true;
}
inner = inner.InnerException;
}
return false;
}
/// <summary>
/// 获取异常信息
/// </summary>
/// <param name="exception"></param>
/// <returns></returns>
private static string GetMessage(Exception exception)
{
var ex = exception;
var builder = new StringBuilder();
while (ex != null)
{
var type = ex.GetType();
builder.Append(type.Namespace).Append('.').Append(type.Name).Append(": ").AppendLine(ex.Message);
ex = ex.InnerException;
}
return builder.ToString();
}
private class RequestLoggingFeature : IRequestLoggingFeature
{
public bool Enable { get; set; } = true;
}
}
}

View File

@ -0,0 +1,185 @@
using FastGithub.Configuration;
using FastGithub.HttpServer.Certs;
using FastGithub.HttpServer.TcpMiddlewares;
using FastGithub.HttpServer.TlsMiddlewares;
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.AspNetCore.Server.Kestrel.Https;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;
namespace FastGithub
{
/// <summary>
/// Kestrel扩展
/// </summary>
public static class KestrelServerExtensions
{
/// <summary>
/// 无限制
/// </summary>
/// <param name="kestrel"></param>
public static void NoLimit(this KestrelServerOptions kestrel)
{
kestrel.Limits.MaxRequestBodySize = null;
kestrel.Limits.MinResponseDataRate = null;
kestrel.Limits.MinRequestBodyDataRate = null;
}
/// <summary>
/// 监听http代理
/// </summary>
/// <param name="kestrel"></param>
public static void ListenHttpProxy(this KestrelServerOptions kestrel)
{
var options = kestrel.ApplicationServices.GetRequiredService<IOptions<FastGithubOptions>>().Value;
var httpProxyPort = options.HttpProxyPort;
if (GlobalListener.CanListenTcp(httpProxyPort) == false)
{
throw new FastGithubException($"tcp端口{httpProxyPort}已经被其它进程占用,请在配置文件更换{nameof(FastGithubOptions.HttpProxyPort)}为其它端口");
}
kestrel.ListenLocalhost(httpProxyPort, listen =>
{
var proxyMiddleware = kestrel.ApplicationServices.GetRequiredService<HttpProxyMiddleware>();
var tunnelMiddleware = kestrel.ApplicationServices.GetRequiredService<TunnelMiddleware>();
listen.Use(next => context => proxyMiddleware.InvokeAsync(next, context));
listen.UseTls();
listen.Use(next => context => tunnelMiddleware.InvokeAsync(next, context));
});
kestrel.GetLogger().LogInformation($"已监听http://localhost:{httpProxyPort}http代理服务启动完成");
}
/// <summary>
/// 监听ssh协议代理
/// </summary>
/// <param name="kestrel"></param>
public static void ListenSshReverseProxy(this KestrelServerOptions kestrel)
{
var sshPort = GlobalListener.SshPort;
kestrel.ListenLocalhost(sshPort, listen =>
{
listen.UseFlowAnalyze();
listen.UseConnectionHandler<GithubSshReverseProxyHandler>();
});
kestrel.GetLogger().LogInformation($"已监听ssh://localhost:{sshPort}github的ssh反向代理服务启动完成");
}
/// <summary>
/// 监听git协议代理代理
/// </summary>
/// <param name="kestrel"></param>
public static void ListenGitReverseProxy(this KestrelServerOptions kestrel)
{
var gitPort = GlobalListener.GitPort;
kestrel.ListenLocalhost(gitPort, listen =>
{
listen.UseFlowAnalyze();
listen.UseConnectionHandler<GithubGitReverseProxyHandler>();
});
kestrel.GetLogger().LogInformation($"已监听git://localhost:{gitPort}github的git反向代理服务启动完成");
}
/// <summary>
/// 监听http反向代理
/// </summary>
/// <param name="kestrel"></param>
public static void ListenHttpReverseProxy(this KestrelServerOptions kestrel)
{
var httpPort = GlobalListener.HttpPort;
kestrel.ListenLocalhost(httpPort);
if (OperatingSystem.IsWindows())
{
kestrel.GetLogger().LogInformation($"已监听http://localhost:{httpPort}http反向代理服务启动完成");
}
}
/// <summary>
/// 监听https反向代理
/// </summary>
/// <param name="kestrel"></param>
/// <exception cref="FastGithubException"></exception>
public static void ListenHttpsReverseProxy(this KestrelServerOptions kestrel)
{
var httpsPort = GlobalListener.HttpsPort;
kestrel.ListenLocalhost(httpsPort, listen =>
{
if (OperatingSystem.IsWindows())
{
listen.UseFlowAnalyze();
}
listen.UseTls();
});
if (OperatingSystem.IsWindows())
{
var logger = kestrel.GetLogger();
logger.LogInformation($"已监听https://localhost:{httpsPort}https反向代理服务启动完成");
}
}
/// <summary>
/// 获取日志
/// </summary>
/// <param name="kestrel"></param>
/// <returns></returns>
private static ILogger GetLogger(this KestrelServerOptions kestrel)
{
var loggerFactory = kestrel.ApplicationServices.GetRequiredService<ILoggerFactory>();
return loggerFactory.CreateLogger($"{nameof(FastGithub)}.{nameof(HttpServer)}");
}
/// <summary>
/// 使用Tls中间件
/// </summary>
/// <param name="listen"></param>
/// <param name="configureOptions">https配置</param>
/// <returns></returns>
public static ListenOptions UseTls(this ListenOptions listen)
{
var certService = listen.ApplicationServices.GetRequiredService<CertService>();
certService.CreateCaCertIfNotExists();
certService.InstallAndTrustCaCert();
return listen.UseTls(domain => certService.GetOrCreateServerCert(domain));
}
/// <summary>
/// 使用Tls中间件
/// </summary>
/// <param name="listen"></param>
/// <param name="configureOptions">https配置</param>
/// <returns></returns>
private static ListenOptions UseTls(this ListenOptions listen, Func<string, X509Certificate2> certFactory)
{
var invadeMiddleware = listen.ApplicationServices.GetRequiredService<TlsInvadeMiddleware>();
var restoreMiddleware = listen.ApplicationServices.GetRequiredService<TlsRestoreMiddleware>();
listen.Use(next => context => invadeMiddleware.InvokeAsync(next, context));
listen.UseHttps(new TlsHandshakeCallbackOptions
{
OnConnection = context =>
{
var options = new SslServerAuthenticationOptions
{
ServerCertificate = certFactory(context.ClientHelloInfo.ServerName)
};
return ValueTask.FromResult(options);
},
});
listen.Use(next => context => restoreMiddleware.InvokeAsync(next, context));
return listen;
}
}
}

View File

@ -0,0 +1,44 @@
using FastGithub.HttpServer.Certs;
using FastGithub.HttpServer.Certs.CaCertInstallers;
using FastGithub.HttpServer.HttpMiddlewares;
using FastGithub.HttpServer.TcpMiddlewares;
using FastGithub.HttpServer.TlsMiddlewares;
using Microsoft.Extensions.DependencyInjection;
namespace FastGithub
{
/// <summary>
/// http反向代理的服务注册扩展
/// </summary>
public static class ServiceCollectionExtensions
{
/// <summary>
/// 添加http反向代理
/// </summary>
/// <param name="services"></param>
/// <returns></returns>
public static IServiceCollection AddReverseProxy(this IServiceCollection services)
{
return services
.AddMemoryCache()
.AddHttpForwarder()
.AddSingleton<CertService>()
.AddSingleton<ICaCertInstaller, CaCertInstallerOfMacOS>()
.AddSingleton<ICaCertInstaller, CaCertInstallerOfWindows>()
.AddSingleton<ICaCertInstaller, CaCertInstallerOfLinuxRedHat>()
.AddSingleton<ICaCertInstaller, CaCertInstallerOfLinuxDebian>()
// tcp
.AddSingleton<HttpProxyMiddleware>()
.AddSingleton<TunnelMiddleware>()
// tls
.AddSingleton<TlsInvadeMiddleware>()
.AddSingleton<TlsRestoreMiddleware>()
// http
.AddSingleton<HttpProxyPacMiddleware>()
.AddSingleton<RequestLoggingMiddleware>()
.AddSingleton<HttpReverseProxyMiddleware>();
}
}
}

View File

@ -0,0 +1,19 @@
using FastGithub.DomainResolve;
namespace FastGithub.HttpServer.TcpMiddlewares
{
/// <summary>
/// github的git代理处理者
/// </summary>
sealed class GithubGitReverseProxyHandler : TcpReverseProxyHandler
{
/// <summary>
/// github的git代理处理者
/// </summary>
/// <param name="domainResolver"></param>
public GithubGitReverseProxyHandler(IDomainResolver domainResolver)
: base(domainResolver, new("github.com", 9418))
{
}
}
}

View File

@ -0,0 +1,19 @@
using FastGithub.DomainResolve;
namespace FastGithub.HttpServer.TcpMiddlewares
{
/// <summary>
/// github的ssh代理处理者
/// </summary>
sealed class GithubSshReverseProxyHandler : TcpReverseProxyHandler
{
/// <summary>
/// github的ssh代理处理者
/// </summary>
/// <param name="domainResolver"></param>
public GithubSshReverseProxyHandler(IDomainResolver domainResolver)
: base(domainResolver, new("github.com", 22))
{
}
}
}

View File

@ -0,0 +1,135 @@
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
using System;
using System.Buffers;
using System.IO.Pipelines;
using System.Text;
using System.Threading.Tasks;
namespace FastGithub.HttpServer.TcpMiddlewares
{
/// <summary>
/// 正向代理中间件
/// </summary>
sealed class HttpProxyMiddleware
{
private readonly HttpParser<HttpRequestHandler> httpParser = new();
private readonly byte[] http200 = Encoding.ASCII.GetBytes("HTTP/1.1 200 Connection Established\r\n\r\n");
private readonly byte[] http400 = Encoding.ASCII.GetBytes("HTTP/1.1 400 Bad Request\r\n\r\n");
/// <summary>
/// 执行中间件
/// </summary>
/// <param name="next"></param>
/// <param name="context"></param>
/// <returns></returns>
public async Task InvokeAsync(ConnectionDelegate next, ConnectionContext context)
{
var result = await context.Transport.Input.ReadAsync();
var httpRequest = this.GetHttpRequestHandler(result, out var consumed);
// 协议错误
if (consumed == 0L)
{
await context.Transport.Output.WriteAsync(this.http400, context.ConnectionClosed);
}
else
{
// 隧道代理连接请求
if (httpRequest.ProxyProtocol == ProxyProtocol.TunnelProxy)
{
var position = result.Buffer.GetPosition(consumed);
context.Transport.Input.AdvanceTo(position);
await context.Transport.Output.WriteAsync(this.http200, context.ConnectionClosed);
}
else
{
var position = result.Buffer.Start;
context.Transport.Input.AdvanceTo(position);
}
context.Features.Set<IHttpProxyFeature>(httpRequest);
await next(context);
}
}
/// <summary>
/// 获取http请求处理者
/// </summary>
/// <param name="result"></param>
/// <param name="consumed"></param>
/// <returns></returns>
private HttpRequestHandler GetHttpRequestHandler(ReadResult result, out long consumed)
{
var handler = new HttpRequestHandler();
var reader = new SequenceReader<byte>(result.Buffer);
if (this.httpParser.ParseRequestLine(handler, ref reader) &&
this.httpParser.ParseHeaders(handler, ref reader))
{
consumed = reader.Consumed;
}
else
{
consumed = 0L;
}
return handler;
}
/// <summary>
/// 代理请求处理器
/// </summary>
private class HttpRequestHandler : IHttpRequestLineHandler, IHttpHeadersHandler, IHttpProxyFeature
{
private HttpMethod method;
public HostString ProxyHost { get; private set; }
public ProxyProtocol ProxyProtocol
{
get
{
if (this.ProxyHost.HasValue == false)
{
return ProxyProtocol.None;
}
if (this.method == HttpMethod.Connect)
{
return ProxyProtocol.TunnelProxy;
}
return ProxyProtocol.HttpProxy;
}
}
void IHttpRequestLineHandler.OnStartLine(HttpVersionAndMethod versionAndMethod, TargetOffsetPathLength targetPath, Span<byte> startLine)
{
this.method = versionAndMethod.Method;
var host = Encoding.ASCII.GetString(startLine.Slice(targetPath.Offset, targetPath.Length));
if (versionAndMethod.Method == HttpMethod.Connect)
{
this.ProxyHost = HostString.FromUriComponent(host);
}
else if (Uri.TryCreate(host, UriKind.Absolute, out var uri))
{
this.ProxyHost = HostString.FromUriComponent(uri);
}
}
void IHttpHeadersHandler.OnHeader(ReadOnlySpan<byte> name, ReadOnlySpan<byte> value)
{
}
void IHttpHeadersHandler.OnHeadersComplete(bool endStream)
{
}
void IHttpHeadersHandler.OnStaticIndexedHeader(int index)
{
}
void IHttpHeadersHandler.OnStaticIndexedHeader(int index, ReadOnlySpan<byte> value)
{
}
}
}
}

View File

@ -0,0 +1,11 @@
using Microsoft.AspNetCore.Http;
namespace FastGithub.HttpServer.TcpMiddlewares
{
interface IHttpProxyFeature
{
HostString ProxyHost { get; }
ProxyProtocol ProxyProtocol { get; }
}
}

View File

@ -0,0 +1,23 @@
namespace FastGithub.HttpServer.TcpMiddlewares
{
/// <summary>
/// 代理协议
/// </summary>
enum ProxyProtocol
{
/// <summary>
/// 无代理
/// </summary>
None,
/// <summary>
/// http代理
/// </summary>
HttpProxy,
/// <summary>
/// 隧道代理
/// </summary>
TunnelProxy
}
}

View File

@ -0,0 +1,77 @@
using FastGithub.DomainResolve;
using Microsoft.AspNetCore.Connections;
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Pipelines;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
namespace FastGithub.HttpServer.TcpMiddlewares
{
/// <summary>
/// tcp协议代理处理者
/// </summary>
abstract class TcpReverseProxyHandler : ConnectionHandler
{
private readonly IDomainResolver domainResolver;
private readonly DnsEndPoint endPoint;
private readonly TimeSpan connectTimeout = TimeSpan.FromSeconds(10d);
/// <summary>
/// tcp协议代理处理者
/// </summary>
/// <param name="domainResolver"></param>
/// <param name="endPoint"></param>
public TcpReverseProxyHandler(IDomainResolver domainResolver, DnsEndPoint endPoint)
{
this.domainResolver = domainResolver;
this.endPoint = endPoint;
}
/// <summary>
/// tcp连接后
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public override async Task OnConnectedAsync(ConnectionContext context)
{
var cancellationToken = context.ConnectionClosed;
using var connection = await CreateConnectionAsync(cancellationToken);
var task1 = connection.CopyToAsync(context.Transport.Output, cancellationToken);
var task2 = context.Transport.Input.CopyToAsync(connection, cancellationToken);
await Task.WhenAny(task1, task2);
}
/// <summary>
/// 创建连接
/// </summary>
/// <param name="cancellationToken"></param>
/// <returns></returns>
/// <exception cref="AggregateException"></exception>
private async Task<Stream> CreateConnectionAsync(CancellationToken cancellationToken)
{
var innerExceptions = new List<Exception>();
await foreach (var address in domainResolver.ResolveAsync(endPoint, cancellationToken))
{
var socket = new Socket(address.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
try
{
using var timeoutTokenSource = new CancellationTokenSource(connectTimeout);
using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutTokenSource.Token);
await socket.ConnectAsync(address, endPoint.Port, linkedTokenSource.Token);
return new NetworkStream(socket, ownsSocket: false);
}
catch (Exception ex)
{
socket.Dispose();
cancellationToken.ThrowIfCancellationRequested();
innerExceptions.Add(ex);
}
}
throw new AggregateException($"无法连接到{endPoint.Host}:{endPoint.Port}", innerExceptions);
}
}
}

View File

@ -0,0 +1,132 @@
using FastGithub.Configuration;
using FastGithub.DomainResolve;
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Connections.Features;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Pipelines;
using System.Net;
using System.Net.Sockets;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
namespace FastGithub.HttpServer.TcpMiddlewares
{
/// <summary>
/// 隧道中间件
/// </summary>
sealed class TunnelMiddleware
{
private readonly FastGithubConfig fastGithubConfig;
private readonly IDomainResolver domainResolver;
private readonly TimeSpan connectTimeout = TimeSpan.FromSeconds(10d);
/// <summary>
/// 隧道中间件
/// </summary>
/// <param name="fastGithubConfig"></param>
/// <param name="domainResolver"></param>
public TunnelMiddleware(
FastGithubConfig fastGithubConfig,
IDomainResolver domainResolver)
{
this.fastGithubConfig = fastGithubConfig;
this.domainResolver = domainResolver;
}
/// <summary>
/// 执行中间件
/// </summary>
/// <param name="next"></param>
/// <param name="context"></param>
/// <returns></returns>
public async Task InvokeAsync(ConnectionDelegate next, ConnectionContext context)
{
var proxyFeature = context.Features.Get<IHttpProxyFeature>();
if (proxyFeature == null || // 非代理
proxyFeature.ProxyProtocol != ProxyProtocol.TunnelProxy || //非隧道代理
context.Features.Get<ITlsConnectionFeature>() != null) // 经过隧道的https
{
await next(context);
}
else
{
var transport = context.Features.Get<IConnectionTransportFeature>()?.Transport;
if (transport != null)
{
var cancellationToken = context.ConnectionClosed;
using var connection = await this.CreateConnectionAsync(proxyFeature.ProxyHost, cancellationToken);
var task1 = connection.CopyToAsync(transport.Output, cancellationToken);
var task2 = transport.Input.CopyToAsync(connection, cancellationToken);
await Task.WhenAny(task1, task2);
}
}
}
/// <summary>
/// 创建连接
/// </summary>
/// <param name="host"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
/// <exception cref="AggregateException"></exception>
private async Task<Stream> CreateConnectionAsync(HostString host, CancellationToken cancellationToken)
{
var innerExceptions = new List<Exception>();
await foreach (var endPoint in this.GetUpstreamEndPointsAsync(host, cancellationToken))
{
var socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
try
{
using var timeoutTokenSource = new CancellationTokenSource(this.connectTimeout);
using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutTokenSource.Token);
await socket.ConnectAsync(endPoint, linkedTokenSource.Token);
return new NetworkStream(socket, ownsSocket: true);
}
catch (Exception ex)
{
socket.Dispose();
cancellationToken.ThrowIfCancellationRequested();
innerExceptions.Add(ex);
}
}
throw new AggregateException($"无法连接到{host}", innerExceptions);
}
/// <summary>
/// 获取目标终节点
/// </summary>
/// <param name="host"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
private async IAsyncEnumerable<EndPoint> GetUpstreamEndPointsAsync(HostString host, [EnumeratorCancellation] CancellationToken cancellationToken)
{
const int HTTPS_PORT = 443;
var targetHost = host.Host;
var targetPort = host.Port ?? HTTPS_PORT;
if (IPAddress.TryParse(targetHost, out var address) == true)
{
yield return new IPEndPoint(address, targetPort);
}
else if (this.fastGithubConfig.IsMatch(targetHost) == false)
{
yield return new DnsEndPoint(targetHost, targetPort);
}
else
{
var dnsEndPoint = new DnsEndPoint(targetHost, targetPort);
await foreach (var item in this.domainResolver.ResolveAsync(dnsEndPoint, cancellationToken))
{
yield return new IPEndPoint(item, targetPort);
}
}
}
}
}

View File

@ -0,0 +1,27 @@
using Microsoft.AspNetCore.Http.Features;
using System;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;
namespace FastGithub.HttpServer.TlsMiddlewares
{
/// <summary>
/// 假冒的TlsConnectionFeature
/// </summary>
sealed class FakeTlsConnectionFeature : ITlsConnectionFeature
{
public static FakeTlsConnectionFeature Instance { get; } = new FakeTlsConnectionFeature();
public X509Certificate2? ClientCertificate
{
get => throw new NotImplementedException();
set => throw new NotImplementedException();
}
public Task<X509Certificate2?> GetClientCertificateAsync(CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
}
}

View File

@ -0,0 +1,64 @@
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Http.Features;
using System.Buffers;
using System.IO.Pipelines;
using System.Threading.Tasks;
namespace FastGithub.HttpServer.TlsMiddlewares
{
/// <summary>
/// https入侵中间件
/// </summary>
sealed class TlsInvadeMiddleware
{
/// <summary>
/// 执行中间件
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public async Task InvokeAsync(ConnectionDelegate next, ConnectionContext context)
{
// 连接不是tls
if (await IsTlsConnectionAsync(context) == false)
{
// 没有任何tls中间件执行过
if (context.Features.Get<ITlsConnectionFeature>() == null)
{
// 设置假的ITlsConnectionFeature迫使https中间件跳过自身的工作
context.Features.Set<ITlsConnectionFeature>(FakeTlsConnectionFeature.Instance);
}
}
await next(context);
}
/// <summary>
/// 是否为tls协议
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
private static async Task<bool> IsTlsConnectionAsync(ConnectionContext context)
{
try
{
var result = await context.Transport.Input.ReadAtLeastAsync(2, context.ConnectionClosed);
var state = IsTlsProtocol(result);
context.Transport.Input.AdvanceTo(result.Buffer.Start);
return state;
}
catch
{
return false;
}
static bool IsTlsProtocol(ReadResult result)
{
var reader = new SequenceReader<byte>(result.Buffer);
return reader.TryRead(out var firstByte) &&
reader.TryRead(out var nextByte) &&
firstByte == 0x16 &&
nextByte == 0x3;
}
}
}
}

View File

@ -0,0 +1,27 @@
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Http.Features;
using System.Threading.Tasks;
namespace FastGithub.HttpServer.TlsMiddlewares
{
/// <summary>
/// https恢复中间件
/// </summary>
sealed class TlsRestoreMiddleware
{
/// <summary>
/// 执行中间件
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public async Task InvokeAsync(ConnectionDelegate next, ConnectionContext context)
{
if (context.Features.Get<ITlsConnectionFeature>() == FakeTlsConnectionFeature.Instance)
{
// 擦除入侵
context.Features.Set<ITlsConnectionFeature>(null);
}
await next(context);
}
}
}

View File

@ -0,0 +1,157 @@
using DNS.Protocol;
using DNS.Protocol.ResourceRecords;
using FastGithub.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Net;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Threading;
using System.Threading.Tasks;
using WindivertDotnet;
namespace FastGithub.PacketIntercept.Dns
{
/// <summary>
/// dns拦截器
/// </summary>
[SupportedOSPlatform("windows")]
sealed class DnsInterceptor : IDnsInterceptor
{
private static readonly Filter filter = Filter.True.And(f => f.Udp.DstPort == 53);
private readonly FastGithubConfig fastGithubConfig;
private readonly ILogger<DnsInterceptor> logger;
private readonly TimeSpan ttl = TimeSpan.FromMinutes(5d);
/// <summary>
/// 刷新DNS缓存
/// </summary>
[DllImport("dnsapi.dll", EntryPoint = "DnsFlushResolverCache", SetLastError = true)]
private static extern void DnsFlushResolverCache();
/// <summary>
/// dns拦截器
/// </summary>
/// <param name="fastGithubConfig"></param>
/// <param name="logger"></param>
/// <param name="options"></param>
public DnsInterceptor(
FastGithubConfig fastGithubConfig,
ILogger<DnsInterceptor> logger,
IOptionsMonitor<FastGithubOptions> options)
{
this.fastGithubConfig = fastGithubConfig;
this.logger = logger;
options.OnChange(_ => DnsFlushResolverCache());
}
/// <summary>
/// DNS拦截
/// </summary>
/// <param name="cancellationToken"></param>
/// <exception cref="Win32Exception"></exception>
/// <returns></returns>
public async Task InterceptAsync(CancellationToken cancellationToken)
{
using var divert = new WinDivert(filter, WinDivertLayer.Network);
using var packet = new WinDivertPacket();
using var addr = new WinDivertAddress();
DnsFlushResolverCache();
cancellationToken.Register(DnsFlushResolverCache);
while (cancellationToken.IsCancellationRequested == false)
{
await divert.RecvAsync(packet, addr, cancellationToken);
try
{
this.ModifyDnsPacket(packet, addr);
}
catch (Exception ex)
{
this.logger.LogWarning(ex.Message);
}
finally
{
await divert.SendAsync(packet, addr, cancellationToken);
}
}
}
/// <summary>
/// 修改DNS数据包
/// </summary>
/// <param name="packet"></param>
/// <param name="addr"></param>
unsafe private void ModifyDnsPacket(WinDivertPacket packet, WinDivertAddress addr)
{
var result = packet.GetParseResult();
var requestPayload = result.DataSpan.ToArray();
if (TryParseRequest(requestPayload, out var request) == false ||
request.OperationCode != OperationCode.Query ||
request.Questions.Count == 0)
{
return;
}
var question = request.Questions.First();
if (question.Type != RecordType.A && question.Type != RecordType.AAAA)
{
return;
}
var domain = question.Name;
if (this.fastGithubConfig.IsMatch(question.Name.ToString()) == false)
{
return;
}
// dns响应数据
var response = Response.FromRequest(request);
var loopback = question.Type == RecordType.A ? IPAddress.Loopback : IPAddress.IPv6Loopback;
var record = new IPAddressResourceRecord(domain, loopback, this.ttl);
response.AnswerRecords.Add(record);
// 修改payload
var writer = packet.GetWriter(packet.Length - result.DataLength);
writer.Write(response.ToArray());
packet.ReverseEndPoint();
packet.ApplyLengthToHeaders();
packet.CalcChecksums(addr);
packet.CalcOutboundFlag(addr);
addr.Flags |= WinDivertAddressFlag.Impostor;
this.logger.LogInformation($"{domain}->{loopback}");
}
/// <summary>
/// 尝试解析请求
/// </summary>
/// <param name="payload"></param>
/// <param name="request"></param>
/// <returns></returns>
static bool TryParseRequest(byte[] payload, [MaybeNullWhen(false)] out Request request)
{
try
{
request = Request.FromArray(payload);
return true;
}
catch (Exception)
{
request = null;
return false;
}
}
}
}

View File

@ -0,0 +1,115 @@
using FastGithub.Configuration;
using Microsoft.Extensions.Logging;
using System;
using System.IO;
using System.Runtime.Versioning;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace FastGithub.PacketIntercept.Dns
{
/// <summary>
/// host文件冲解决者
/// </summary>
[SupportedOSPlatform("windows")]
sealed class HostsConflictSolver : IDnsConflictSolver
{
private readonly FastGithubConfig fastGithubConfig;
private readonly ILogger<HostsConflictSolver> logger;
/// <summary>
/// host文件冲解决者
/// </summary>
/// <param name="fastGithubConfig"></param>
/// <param name="logger"></param>
public HostsConflictSolver(
FastGithubConfig fastGithubConfig,
ILogger<HostsConflictSolver> logger)
{
this.fastGithubConfig = fastGithubConfig;
this.logger = logger;
}
/// <summary>
/// 解决冲突
/// </summary>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task SolveAsync(CancellationToken cancellationToken)
{
var hostsPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System), "drivers/etc/hosts");
if (File.Exists(hostsPath) == false)
{
return;
}
Encoding hostsEncoding;
var hasConflicting = false;
var hostsBuilder = new StringBuilder();
using (var fileStream = new FileStream(hostsPath, FileMode.Open, FileAccess.Read))
{
using var streamReader = new StreamReader(fileStream);
while (streamReader.EndOfStream == false)
{
var line = await streamReader.ReadLineAsync(cancellationToken);
if (this.IsConflictingLine(line))
{
hasConflicting = true;
hostsBuilder.AppendLine($"# {line}");
}
else
{
hostsBuilder.AppendLine(line);
}
}
hostsEncoding = streamReader.CurrentEncoding;
}
if (hasConflicting == true)
{
try
{
await File.WriteAllTextAsync(hostsPath, hostsBuilder.ToString(), hostsEncoding, cancellationToken);
}
catch (Exception ex)
{
this.logger.LogWarning($"无法解决hosts文件冲突{ex.Message}");
}
}
}
/// <summary>
/// 恢复冲突
/// </summary>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public Task RestoreAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
/// <summary>
/// 是否为冲突的行
/// </summary>
/// <param name="line"></param>
/// <returns></returns>
private bool IsConflictingLine(string? line)
{
if (line == null || line.TrimStart().StartsWith("#"))
{
return false;
}
var items = line.Split(new[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries);
if (items.Length < 2)
{
return false;
}
var domain = items[1];
return this.fastGithubConfig.IsMatch(domain);
}
}
}

View File

@ -0,0 +1,161 @@
using FastGithub.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Win32;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Threading;
using System.Threading.Tasks;
namespace FastGithub.PacketIntercept.Dns
{
/// <summary>
/// 代理冲突解决者
/// </summary>
[SupportedOSPlatform("windows")]
sealed class ProxyConflictSolver : IDnsConflictSolver
{
private const int INTERNET_OPTION_REFRESH = 37;
private const int INTERNET_OPTION_PROXY_SETTINGS_CHANGED = 95;
private const char PROXYOVERRIDE_SEPARATOR = ';';
private const string PROXYOVERRIDE_KEY = "ProxyOverride";
private const string INTERNET_SETTINGS = @"Software\Microsoft\Windows\CurrentVersion\Internet Settings";
private readonly IOptions<FastGithubOptions> options;
private readonly ILogger<ProxyConflictSolver> logger;
[DllImport("wininet.dll")]
private static extern bool InternetSetOption(IntPtr hInternet, int dwOption, IntPtr lpBuffer, int dwBufferLength);
/// <summary>
/// 代理冲突解决者
/// </summary>
/// <param name="options"></param>
/// <param name="logger"></param>
public ProxyConflictSolver(
IOptions<FastGithubOptions> options,
ILogger<ProxyConflictSolver> logger)
{
this.options = options;
this.logger = logger;
}
/// <summary>
/// 解决冲突
/// </summary>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public Task SolveAsync(CancellationToken cancellationToken)
{
this.SetToProxyOvride();
this.CheckProxyConflict();
return Task.CompletedTask;
}
/// <summary>
/// 恢复冲突
/// </summary>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public Task RestoreAsync(CancellationToken cancellationToken)
{
this.RemoveFromProxyOvride();
return Task.CompletedTask;
}
/// <summary>
/// 添加到ProxyOvride
/// </summary>
private void SetToProxyOvride()
{
using var settings = Registry.CurrentUser.OpenSubKey(INTERNET_SETTINGS, writable: true);
if (settings == null)
{
return;
}
var items = this.options.Value.DomainConfigs.Keys.ToHashSet();
foreach (var item in GetProxyOvride(settings))
{
items.Add(item);
}
SetProxyOvride(settings, items);
}
/// <summary>
/// 从ProxyOvride移除
/// </summary>
private void RemoveFromProxyOvride()
{
using var settings = Registry.CurrentUser.OpenSubKey(INTERNET_SETTINGS, writable: true);
if (settings == null)
{
return;
}
var proxyOvride = GetProxyOvride(settings);
var items = proxyOvride.Except(this.options.Value.DomainConfigs.Keys);
SetProxyOvride(settings, items);
}
/// <summary>
/// 检测代理冲突
/// </summary>
private void CheckProxyConflict()
{
var systemProxy = HttpClient.DefaultProxy;
if (systemProxy == null)
{
return;
}
foreach (var domain in this.options.Value.DomainConfigs.Keys)
{
var destination = new Uri($"https://{domain.Replace('*', 'a')}");
var proxyServer = systemProxy.GetProxy(destination);
if (proxyServer != null)
{
this.logger.LogError($"由于系统设置了代理{proxyServer}{nameof(FastGithub)}无法加速{domain}");
}
}
}
/// <summary>
/// 获取ProxyOverride
/// </summary>
/// <param name="registryKey"></param>
/// <returns></returns>
private static string[] GetProxyOvride(RegistryKey registryKey)
{
var value = registryKey.GetValue(PROXYOVERRIDE_KEY, null)?.ToString();
if (value == null)
{
return Array.Empty<string>();
}
return value
.Split(PROXYOVERRIDE_SEPARATOR, StringSplitOptions.RemoveEmptyEntries)
.Select(item => item.Trim())
.ToArray();
}
/// <summary>
/// 设置ProxyOverride
/// </summary>
/// <param name="registryKey"></param>
/// <param name="items"></param>
private static void SetProxyOvride(RegistryKey registryKey, IEnumerable<string> items)
{
var value = string.Join(PROXYOVERRIDE_SEPARATOR, items);
registryKey.SetValue(PROXYOVERRIDE_KEY, value, RegistryValueKind.String);
InternetSetOption(IntPtr.Zero, INTERNET_OPTION_PROXY_SETTINGS_CHANGED, IntPtr.Zero, 0);
InternetSetOption(IntPtr.Zero, INTERNET_OPTION_REFRESH, IntPtr.Zero, 0);
}
}
}

View File

@ -0,0 +1,94 @@
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.Versioning;
using System.Threading;
using System.Threading.Tasks;
namespace FastGithub.PacketIntercept
{
/// <summary>
/// dns拦截后台服务
/// </summary>
[SupportedOSPlatform("windows")]
sealed class DnsInterceptHostedService : BackgroundService
{
private readonly IDnsInterceptor dnsInterceptor;
private readonly IEnumerable<IDnsConflictSolver> conflictSolvers;
private readonly ILogger<DnsInterceptHostedService> logger;
private readonly IHost host;
/// <summary>
/// dns拦截后台服务
/// </summary>
/// <param name="dnsInterceptor"></param>
/// <param name="conflictSolvers"></param>
/// <param name="logger"></param>
/// <param name="host"></param>
public DnsInterceptHostedService(
IDnsInterceptor dnsInterceptor,
IEnumerable<IDnsConflictSolver> conflictSolvers,
ILogger<DnsInterceptHostedService> logger,
IHost host)
{
this.dnsInterceptor = dnsInterceptor;
this.conflictSolvers = conflictSolvers;
this.logger = logger;
this.host = host;
}
/// <summary>
/// 启动时处理冲突
/// </summary>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public override async Task StartAsync(CancellationToken cancellationToken)
{
foreach (var solver in this.conflictSolvers)
{
await solver.SolveAsync(cancellationToken);
}
await base.StartAsync(cancellationToken);
}
/// <summary>
/// 停止时恢复冲突
/// </summary>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public override async Task StopAsync(CancellationToken cancellationToken)
{
foreach (var solver in this.conflictSolvers)
{
await solver.RestoreAsync(cancellationToken);
}
await base.StopAsync(cancellationToken);
}
/// <summary>
/// dns后台
/// </summary>
/// <param name="stoppingToken"></param>
/// <returns></returns>
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
try
{
await this.dnsInterceptor.InterceptAsync(stoppingToken);
}
catch (OperationCanceledException)
{
}
catch (Win32Exception ex) when (ex.NativeErrorCode == 995)
{
}
catch (Exception ex)
{
this.logger.LogError(ex, "dns拦截器异常");
await this.host.StopAsync(stoppingToken);
}
}
}
}

View File

@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="DNS" Version="7.0.0" />
<PackageReference Include="WindivertDotnet" Version="1.1.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\FastGithub.Configuration\FastGithub.Configuration.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,25 @@
using System.Threading;
using System.Threading.Tasks;
namespace FastGithub.PacketIntercept
{
/// <summary>
/// Dns冲突解决者
/// </summary>
interface IDnsConflictSolver
{
/// <summary>
/// 解决冲突
/// </summary>
/// <param name="cancellationToken"></param>
/// <returns></returns>
Task SolveAsync(CancellationToken cancellationToken);
/// <summary>
/// 恢复冲突
/// </summary>
/// <param name="cancellationToken"></param>
/// <returns></returns>
Task RestoreAsync(CancellationToken cancellationToken);
}
}

View File

@ -0,0 +1,18 @@
using System.Threading;
using System.Threading.Tasks;
namespace FastGithub.PacketIntercept
{
/// <summary>
/// dns拦截器接口
/// </summary>
interface IDnsInterceptor
{
/// <summary>
/// 拦截数据包
/// </summary>
/// <param name="cancellationToken"></param>
/// <returns></returns>
Task InterceptAsync(CancellationToken cancellationToken);
}
}

View File

@ -0,0 +1,18 @@
using System.Threading;
using System.Threading.Tasks;
namespace FastGithub.PacketIntercept
{
/// <summary>
/// tcp拦截器接口
/// </summary>
interface ITcpInterceptor
{
/// <summary>
/// 拦截数据包
/// </summary>
/// <param name="cancellationToken"></param>
/// <returns></returns>
Task InterceptAsync(CancellationToken cancellationToken);
}
}

View File

@ -0,0 +1,37 @@
using FastGithub.PacketIntercept;
using FastGithub.PacketIntercept.Dns;
using FastGithub.PacketIntercept.Tcp;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using System.Runtime.Versioning;
namespace FastGithub
{
/// <summary>
/// 服务注册扩展
/// </summary>
public static class ServiceCollectionExtensions
{
/// <summary>
/// 注册数据包拦截器
/// </summary>
/// <param name="services"></param>
/// <returns></returns>
[SupportedOSPlatform("windows")]
public static IServiceCollection AddPacketIntercept(this IServiceCollection services)
{
services.AddSingleton<IDnsConflictSolver, HostsConflictSolver>();
services.AddSingleton<IDnsConflictSolver, ProxyConflictSolver>();
services.TryAddSingleton<IDnsInterceptor, DnsInterceptor>();
services.AddHostedService<DnsInterceptHostedService>();
services.AddSingleton<ITcpInterceptor, SshInterceptor>();
services.AddSingleton<ITcpInterceptor, GitInterceptor>();
services.AddSingleton<ITcpInterceptor, HttpInterceptor>();
services.AddSingleton<ITcpInterceptor, HttpsInterceptor>();
services.AddHostedService<TcpInterceptHostedService>();
return services;
}
}
}

View File

@ -0,0 +1,22 @@
using FastGithub.Configuration;
using Microsoft.Extensions.Logging;
using System.Runtime.Versioning;
namespace FastGithub.PacketIntercept.Tcp
{
/// <summary>
/// git拦截器
/// </summary>
[SupportedOSPlatform("windows")]
sealed class GitInterceptor : TcpInterceptor
{
/// <summary>
/// git拦截器
/// </summary>
/// <param name="logger"></param>
public GitInterceptor(ILogger<HttpInterceptor> logger)
: base(9418, GlobalListener.GitPort, logger)
{
}
}
}

View File

@ -0,0 +1,22 @@
using FastGithub.Configuration;
using Microsoft.Extensions.Logging;
using System.Runtime.Versioning;
namespace FastGithub.PacketIntercept.Tcp
{
/// <summary>
/// http拦截器
/// </summary>
[SupportedOSPlatform("windows")]
sealed class HttpInterceptor : TcpInterceptor
{
/// <summary>
/// http拦截器
/// </summary>
/// <param name="logger"></param>
public HttpInterceptor(ILogger<HttpInterceptor> logger)
: base(80, GlobalListener.HttpPort, logger)
{
}
}
}

View File

@ -0,0 +1,22 @@
using FastGithub.Configuration;
using Microsoft.Extensions.Logging;
using System.Runtime.Versioning;
namespace FastGithub.PacketIntercept.Tcp
{
/// <summary>
/// https拦截器
/// </summary>
[SupportedOSPlatform("windows")]
sealed class HttpsInterceptor : TcpInterceptor
{
/// <summary>
/// https拦截器
/// </summary>
/// <param name="logger"></param>
public HttpsInterceptor(ILogger<HttpsInterceptor> logger)
: base(443, GlobalListener.HttpsPort, logger)
{
}
}
}

View File

@ -0,0 +1,22 @@
using FastGithub.Configuration;
using Microsoft.Extensions.Logging;
using System.Runtime.Versioning;
namespace FastGithub.PacketIntercept.Tcp
{
/// <summary>
/// ssh拦截器
/// </summary>
[SupportedOSPlatform("windows")]
sealed class SshInterceptor : TcpInterceptor
{
/// <summary>
/// ssh拦截器
/// </summary>
/// <param name="logger"></param>
public SshInterceptor(ILogger<HttpInterceptor> logger)
: base(22, GlobalListener.SshPort, logger)
{
}
}
}

View File

@ -0,0 +1,104 @@
using Microsoft.Extensions.Logging;
using System;
using System.ComponentModel;
using System.Net;
using System.Net.Sockets;
using System.Runtime.Versioning;
using System.Threading;
using System.Threading.Tasks;
using WindivertDotnet;
namespace FastGithub.PacketIntercept.Tcp
{
/// <summary>
/// tcp拦截器
/// </summary>
[SupportedOSPlatform("windows")]
abstract class TcpInterceptor : ITcpInterceptor
{
private readonly Filter filter;
private readonly ushort oldServerPort;
private readonly ushort newServerPort;
private readonly ILogger logger;
/// <summary>
/// tcp拦截器
/// </summary>
/// <param name="oldServerPort">修改前的服务器端口</param>
/// <param name="newServerPort">修改后的服务器端口</param>
/// <param name="logger"></param>
public TcpInterceptor(int oldServerPort, int newServerPort, ILogger logger)
{
this.filter = Filter.True
.And(f => f.Network.Loopback)
.And(f => f.Tcp.DstPort == oldServerPort || f.Tcp.SrcPort == newServerPort);
this.oldServerPort = (ushort)oldServerPort;
this.newServerPort = (ushort)newServerPort;
this.logger = logger;
}
/// <summary>
/// 拦截指定端口的数据包
/// </summary>
/// <param name="cancellationToken"></param>
/// <exception cref="Win32Exception"></exception>
public async Task InterceptAsync(CancellationToken cancellationToken)
{
if (this.oldServerPort == this.newServerPort)
{
return;
}
using var divert = new WinDivert(this.filter, WinDivertLayer.Network);
using var packet = new WinDivertPacket();
using var addr = new WinDivertAddress();
if (Socket.OSSupportsIPv4)
{
this.logger.LogInformation($"{IPAddress.Loopback}:{this.oldServerPort} <=> {IPAddress.Loopback}:{this.newServerPort}");
}
if (Socket.OSSupportsIPv6)
{
this.logger.LogInformation($"{IPAddress.IPv6Loopback}:{this.oldServerPort} <=> {IPAddress.IPv6Loopback}:{this.newServerPort}");
}
while (cancellationToken.IsCancellationRequested == false)
{
await divert.RecvAsync(packet, addr, cancellationToken);
try
{
this.ModifyTcpPacket(packet, addr);
}
catch (Exception ex)
{
this.logger.LogWarning(ex.Message);
}
finally
{
await divert.SendAsync(packet, addr, cancellationToken);
}
}
}
/// <summary>
/// 修改tcp数据端口的端口
/// </summary>
/// <param name="packet"></param>
/// <param name="addr"></param>
unsafe private void ModifyTcpPacket(WinDivertPacket packet, WinDivertAddress addr)
{
var result = packet.GetParseResult();
if (result.TcpHeader->DstPort == oldServerPort)
{
result.TcpHeader->DstPort = this.newServerPort;
}
else
{
result.TcpHeader->SrcPort = oldServerPort;
}
addr.Flags |= WinDivertAddressFlag.Impostor;
packet.CalcChecksums(addr);
}
}
}

Some files were not shown because too many files have changed in this diff Show More