mirror of
https://github.com/xvzc/SpoofDPI.git
synced 2025-01-04 13:24:46 +00:00
use system-dns when patterns are not matched
This commit is contained in:
parent
24c48c6c9a
commit
641ded49d8
@ -41,7 +41,7 @@ func main() {
|
|||||||
|
|
||||||
if *config.SystemProxy {
|
if *config.SystemProxy {
|
||||||
if err := util.SetOsProxy(*config.Port); err != nil {
|
if err := util.SetOsProxy(*config.Port); err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal("Error while changing proxy settings")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
42
dns/dns.go
42
dns/dns.go
@ -3,6 +3,7 @@ package dns
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"net"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
@ -28,18 +29,30 @@ func NewResolver(config *util.Config) *DnsResolver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DnsResolver) Lookup(domain string) (string, error) {
|
func (d *DnsResolver) Lookup(domain string, useSystemDns bool) (string, error) {
|
||||||
ipRegex := "^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$"
|
ipRegex := "^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$"
|
||||||
|
|
||||||
if r, _ := regexp.MatchString(ipRegex, domain); r {
|
if r, _ := regexp.MatchString(ipRegex, domain); r {
|
||||||
return domain, nil
|
return domain, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if useSystemDns {
|
||||||
|
log.Debug("[DNS] ", domain, " resolving with system dns")
|
||||||
|
return systemLookup(domain)
|
||||||
|
}
|
||||||
|
|
||||||
if d.enableDoh {
|
if d.enableDoh {
|
||||||
|
log.Debug("[DNS] ", domain, " resolving with dns over https")
|
||||||
return dohLookup(domain)
|
return dohLookup(domain)
|
||||||
}
|
}
|
||||||
|
|
||||||
dnsServer := d.host + ":" + d.port
|
log.Debug("[DNS] ", domain, " resolving with custom dns")
|
||||||
|
return customLookup(d.host, d.port, domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
func customLookup(host string, port string, domain string) (string, error) {
|
||||||
|
|
||||||
|
dnsServer := host + ":" + port
|
||||||
|
|
||||||
msg := new(dns.Msg)
|
msg := new(dns.Msg)
|
||||||
msg.SetQuestion(dns.Fqdn(domain), dns.TypeA)
|
msg.SetQuestion(dns.Fqdn(domain), dns.TypeA)
|
||||||
@ -48,17 +61,31 @@ func (d *DnsResolver) Lookup(domain string) (string, error) {
|
|||||||
|
|
||||||
response, _, err := c.Exchange(msg, dnsServer)
|
response, _, err := c.Exchange(msg, dnsServer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", errors.New("couldn not resolve the domain")
|
return "", errors.New("couldn not resolve the domain(custom)")
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, answer := range response.Answer {
|
for _, answer := range response.Answer {
|
||||||
if record, ok := answer.(*dns.A); ok {
|
if record, ok := answer.(*dns.A); ok {
|
||||||
log.Debug("[DNS] resolved ", domain, ": ", record.A.String())
|
|
||||||
return record.A.String(), nil
|
return record.A.String(), nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return "", errors.New("no record found")
|
return "", errors.New("no record found(custom)")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func systemLookup(domain string) (string, error) {
|
||||||
|
systemResolver := net.Resolver{PreferGo: true}
|
||||||
|
ips, err := systemResolver.LookupIPAddr(context.Background(), domain)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.New("couldn not resolve the domain(system)")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ip := range ips {
|
||||||
|
return ip.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", errors.New("no record found(system)")
|
||||||
}
|
}
|
||||||
|
|
||||||
func dohLookup(domain string) (string, error) {
|
func dohLookup(domain string) (string, error) {
|
||||||
@ -68,7 +95,7 @@ func dohLookup(domain string) (string, error) {
|
|||||||
|
|
||||||
rsp, err := c.Query(ctx, dohDns.Domain(domain), dohDns.TypeA)
|
rsp, err := c.Query(ctx, dohDns.Domain(domain), dohDns.TypeA)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", errors.New("could not resolve the domain")
|
return "", errors.New("could not resolve the domain(doh)")
|
||||||
}
|
}
|
||||||
// doh dns answer
|
// doh dns answer
|
||||||
answer := rsp.Answer
|
answer := rsp.Answer
|
||||||
@ -79,12 +106,11 @@ func dohLookup(domain string) (string, error) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug("[DOH] resolved ", domain, ": ", a.Data)
|
|
||||||
return a.Data, nil
|
return a.Data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// close the client
|
// close the client
|
||||||
c.Close()
|
c.Close()
|
||||||
|
|
||||||
return "", errors.New("no record found")
|
return "", errors.New("no record found(doh)")
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
VERSION="v0.10.5"
|
VERSION="v0.10.6"
|
||||||
|
|
||||||
for osarch in 'darwin/amd64' 'darwin/arm64' 'linux/amd64' 'linux/arm' 'linux/arm64' 'linux/mips' 'linux/mipsle'; do
|
for osarch in 'darwin/amd64' 'darwin/arm64' 'linux/amd64' 'linux/arm' 'linux/arm64' 'linux/mips' 'linux/mipsle'; do
|
||||||
GOOS=${osarch%/*} GOARCH=${osarch#*/} go build -ldflags="-w -s -X main.VERSION=${VERSION}" github.com/xvzc/SpoofDPI/cmd/spoof-dpi &&
|
GOOS=${osarch%/*} GOARCH=${osarch#*/} go build -ldflags="-w -s -X main.VERSION=${VERSION}" github.com/xvzc/SpoofDPI/cmd/spoof-dpi &&
|
||||||
|
@ -8,7 +8,7 @@ import (
|
|||||||
"github.com/xvzc/SpoofDPI/packet"
|
"github.com/xvzc/SpoofDPI/packet"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (pxy *Proxy) handleHttps(lConn *net.TCPConn, initPkt *packet.HttpPacket, ip string) {
|
func (pxy *Proxy) handleHttps(lConn *net.TCPConn, exploit bool, initPkt *packet.HttpPacket, ip string) {
|
||||||
// Create a connection to the requested server
|
// Create a connection to the requested server
|
||||||
var port int = 443
|
var port int = 443
|
||||||
var err error
|
var err error
|
||||||
@ -62,17 +62,17 @@ func (pxy *Proxy) handleHttps(lConn *net.TCPConn, initPkt *packet.HttpPacket, ip
|
|||||||
|
|
||||||
go Serve(rConn, lConn, "[HTTPS]", rConn.RemoteAddr().String(), initPkt.Domain(), pxy.timeout)
|
go Serve(rConn, lConn, "[HTTPS]", rConn.RemoteAddr().String(), initPkt.Domain(), pxy.timeout)
|
||||||
|
|
||||||
if pxy.patternExists() && !pxy.patternMatches([]byte(initPkt.Domain())) {
|
if exploit {
|
||||||
log.Debug("[HTTPS] Writing plain client hello to ", initPkt.Domain())
|
log.Debug("[HTTPS] Writing chunked client hello to ", initPkt.Domain())
|
||||||
if _, err := rConn.Write(chPkt.Raw()); err != nil {
|
chunks := splitInChunks(chPkt.Raw(), pxy.windowSize)
|
||||||
log.Debug("[HTTPS] Error writing plain client hello to ", initPkt.Domain(), err)
|
if _, err := WriteChunks(rConn, chunks); err != nil {
|
||||||
|
log.Debug("[HTTPS] Error writing chunked client hello to ", initPkt.Domain(), err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.Debug("[HTTPS] Writing chunked client hello to ", initPkt.Domain())
|
log.Debug("[HTTPS] Writing plain client hello to ", initPkt.Domain())
|
||||||
chunks := pxy.splitInChunks(chPkt.Raw())
|
if _, err := rConn.Write(chPkt.Raw()); err != nil {
|
||||||
if _, err := WriteChunks(rConn, chunks); err != nil {
|
log.Debug("[HTTPS] Error writing plain client hello to ", initPkt.Domain(), err)
|
||||||
log.Debug("[HTTPS] Error writing chunked client hello to ", initPkt.Domain(), err)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -80,17 +80,11 @@ func (pxy *Proxy) handleHttps(lConn *net.TCPConn, initPkt *packet.HttpPacket, ip
|
|||||||
Serve(lConn, rConn, "[HTTPS]", lConn.RemoteAddr().String(), initPkt.Domain(), pxy.timeout)
|
Serve(lConn, rConn, "[HTTPS]", lConn.RemoteAddr().String(), initPkt.Domain(), pxy.timeout)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pxy *Proxy) splitInChunks(bytes []byte) [][]byte {
|
func splitInChunks(bytes []byte, size int) [][]byte {
|
||||||
// If the packet matches the pattern or the URLs, we don't split it
|
|
||||||
if pxy.patternExists() && !pxy.patternMatches(bytes) {
|
|
||||||
return [][]byte{bytes}
|
|
||||||
}
|
|
||||||
|
|
||||||
var chunks [][]byte
|
var chunks [][]byte
|
||||||
var raw []byte = bytes
|
var raw []byte = bytes
|
||||||
var size = pxy.windowSize
|
|
||||||
|
|
||||||
log.Debug("[HTTPS] window-size: ", size)
|
log.Debug("[HTTPS] window-size: ", size)
|
||||||
|
|
||||||
if size > 0 {
|
if size > 0 {
|
||||||
for {
|
for {
|
||||||
@ -111,22 +105,13 @@ func (pxy *Proxy) splitInChunks(bytes []byte) [][]byte {
|
|||||||
return chunks
|
return chunks
|
||||||
}
|
}
|
||||||
|
|
||||||
// When the given window-size <= 0
|
// When the given window-size <= 0
|
||||||
|
|
||||||
if len(raw) < 1 {
|
if len(raw) < 1 {
|
||||||
return [][]byte{raw}
|
return [][]byte{raw}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug("[HTTPS] Using legacy fragmentation.")
|
log.Debug("[HTTPS] Using legacy fragmentation.")
|
||||||
|
|
||||||
return [][]byte{raw[:1], raw[1:]}
|
return [][]byte{raw[:1], raw[1:]}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pxy *Proxy) patternExists() bool {
|
|
||||||
return pxy.allowedPattern != nil || pxy.allowedUrls != nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pxy *Proxy) patternMatches(bytes []byte) bool {
|
|
||||||
return (pxy.allowedPattern != nil && pxy.allowedPattern.Match(bytes)) ||
|
|
||||||
(pxy.allowedUrls != nil && pxy.allowedUrls.Match(bytes))
|
|
||||||
}
|
|
||||||
|
@ -19,8 +19,7 @@ type Proxy struct {
|
|||||||
timeout int
|
timeout int
|
||||||
resolver *dns.DnsResolver
|
resolver *dns.DnsResolver
|
||||||
windowSize int
|
windowSize int
|
||||||
allowedPattern *regexp.Regexp
|
allowedPattern []*regexp.Regexp
|
||||||
allowedUrls *regexp.Regexp
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(config *util.Config) *Proxy {
|
func New(config *util.Config) *Proxy {
|
||||||
@ -30,7 +29,6 @@ func New(config *util.Config) *Proxy {
|
|||||||
timeout: *config.Timeout,
|
timeout: *config.Timeout,
|
||||||
windowSize: *config.WindowSize,
|
windowSize: *config.WindowSize,
|
||||||
allowedPattern: config.AllowedPattern,
|
allowedPattern: config.AllowedPattern,
|
||||||
allowedUrls: config.AllowedUrls,
|
|
||||||
resolver: dns.NewResolver(config),
|
resolver: dns.NewResolver(config),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -42,11 +40,14 @@ func (pxy *Proxy) Start() {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if pxy.timeout > 0 {
|
if pxy.timeout > 0 {
|
||||||
log.Println(fmt.Sprintf("[PROXY] Connection timeout is set to %dms", pxy.timeout))
|
log.Println(fmt.Sprintf("[PROXY] Connection timeout is set to %dms", pxy.timeout))
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Println("[PROXY] Created a listener on port", pxy.port)
|
log.Println("[PROXY] Created a listener on port", pxy.port)
|
||||||
|
if len(pxy.allowedPattern) > 0 {
|
||||||
|
log.Println("[PROXY] Number of white-listed pattern:", len(pxy.allowedPattern))
|
||||||
|
}
|
||||||
|
|
||||||
for {
|
for {
|
||||||
conn, err := l.Accept()
|
conn, err := l.Accept()
|
||||||
@ -76,9 +77,12 @@ func (pxy *Proxy) Start() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ip, err := pxy.resolver.Lookup(pkt.Domain())
|
matched := pxy.patternMatches([]byte(pkt.Domain()))
|
||||||
|
useSystemDns := !matched
|
||||||
|
|
||||||
|
ip, err := pxy.resolver.Lookup(pkt.Domain(), useSystemDns)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debug("[PROXY] Error while dns lookup: ", pkt.Domain(), " ", err)
|
log.Debug("[PROXY] Error while dns lookup: ", pkt.Domain(), " ", err)
|
||||||
conn.Write([]byte(pkt.Version() + " 502 Bad Gateway\r\n\r\n"))
|
conn.Write([]byte(pkt.Version() + " 502 Bad Gateway\r\n\r\n"))
|
||||||
conn.Close()
|
conn.Close()
|
||||||
return
|
return
|
||||||
@ -93,7 +97,7 @@ func (pxy *Proxy) Start() {
|
|||||||
|
|
||||||
if pkt.IsConnectMethod() {
|
if pkt.IsConnectMethod() {
|
||||||
log.Debug("[PROXY] Start HTTPS")
|
log.Debug("[PROXY] Start HTTPS")
|
||||||
pxy.handleHttps(conn.(*net.TCPConn), pkt, ip)
|
pxy.handleHttps(conn.(*net.TCPConn), matched, pkt, ip)
|
||||||
} else {
|
} else {
|
||||||
log.Debug("[PROXY] Start HTTP")
|
log.Debug("[PROXY] Start HTTP")
|
||||||
pxy.handleHttp(conn.(*net.TCPConn), pkt, ip)
|
pxy.handleHttp(conn.(*net.TCPConn), pkt, ip)
|
||||||
@ -102,6 +106,20 @@ func (pxy *Proxy) Start() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (pxy *Proxy) patternMatches(bytes []byte) bool {
|
||||||
|
if pxy.allowedPattern == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pattern := range pxy.allowedPattern {
|
||||||
|
if pattern.Match(bytes) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func isLoopedRequest(ip net.IP) bool {
|
func isLoopedRequest(ip net.IP) bool {
|
||||||
// we don't handle IPv6 at all it seems
|
// we don't handle IPv6 at all it seems
|
||||||
if ip.To4() == nil {
|
if ip.To4() == nil {
|
||||||
|
@ -4,11 +4,9 @@ import (
|
|||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/pterm/pterm"
|
"github.com/pterm/pterm"
|
||||||
"github.com/pterm/pterm/putils"
|
"github.com/pterm/pterm/putils"
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
@ -21,26 +19,23 @@ type Config struct {
|
|||||||
NoBanner *bool
|
NoBanner *bool
|
||||||
SystemProxy *bool
|
SystemProxy *bool
|
||||||
Timeout *int
|
Timeout *int
|
||||||
AllowedPattern *regexp.Regexp
|
AllowedPattern []*regexp.Regexp
|
||||||
AllowedUrls *regexp.Regexp
|
|
||||||
WindowSize *int
|
WindowSize *int
|
||||||
Version *bool
|
Version *bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type ArrayFlags []string
|
type StringArray []string
|
||||||
|
|
||||||
func (i *ArrayFlags) String() string {
|
func (arr *StringArray) String() string {
|
||||||
return "my string representation"
|
return fmt.Sprintf("%s", *arr)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *ArrayFlags) Set(value string) error {
|
func (arr *StringArray) Set(value string) error {
|
||||||
*i = append(*i, value)
|
*arr = append(*arr, value)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var config *Config
|
var config *Config
|
||||||
var allowedHosts ArrayFlags
|
|
||||||
var allowedPattern *string
|
|
||||||
|
|
||||||
func GetConfig() *Config {
|
func GetConfig() *Config {
|
||||||
return config
|
return config
|
||||||
@ -52,38 +47,28 @@ func ParseArgs() {
|
|||||||
config.Port = flag.Int("port", 8080, "port")
|
config.Port = flag.Int("port", 8080, "port")
|
||||||
config.DnsAddr = flag.String("dns-addr", "8.8.8.8", "dns address")
|
config.DnsAddr = flag.String("dns-addr", "8.8.8.8", "dns address")
|
||||||
config.DnsPort = flag.Int("dns-port", 53, "port number for dns")
|
config.DnsPort = flag.Int("dns-port", 53, "port number for dns")
|
||||||
config.EnableDoh = flag.Bool("enable-doh", false, "enable 'dns over https'")
|
config.EnableDoh = flag.Bool("enable-doh", false, "enable 'dns-over-https'")
|
||||||
config.Debug = flag.Bool("debug", false, "enable debug output")
|
config.Debug = flag.Bool("debug", false, "enable debug output")
|
||||||
config.NoBanner = flag.Bool("no-banner", false, "disable banner")
|
config.NoBanner = flag.Bool("no-banner", false, "disable banner")
|
||||||
config.SystemProxy = flag.Bool("system-proxy", true, "enable system-wide proxy")
|
config.SystemProxy = flag.Bool("system-proxy", true, "enable system-wide proxy")
|
||||||
config.Timeout = flag.Int("timeout", 0, "timeout in milliseconds. no timeout when not given")
|
config.Timeout = flag.Int("timeout", 0, "timeout in milliseconds; no timeout when not given")
|
||||||
config.WindowSize = flag.Int("window-size", 0, `chunk size, in number of bytes, for fragmented client hello,
|
config.WindowSize = flag.Int("window-size", 0, `chunk size, in number of bytes, for fragmented client hello,
|
||||||
try lower values if the default value doesn't bypass the DPI;
|
try lower values if the default value doesn't bypass the DPI;
|
||||||
when not given, the client hello packet will be sent in two parts:
|
when not given, the client hello packet will be sent in two parts:
|
||||||
fragmentation for the first data packet and the rest
|
fragmentation for the first data packet and the rest
|
||||||
`)
|
`)
|
||||||
flag.Var(&allowedHosts, "url", "Bypass DPI only on this url, can be passed multiple times")
|
config.Version = flag.Bool("v", false, "print spoof-dpi's version; this may contain some other relevant information")
|
||||||
allowedPattern = flag.String(
|
|
||||||
"pattern",
|
|
||||||
"",
|
|
||||||
"bypass DPI only on packets matching this regex pattern",
|
|
||||||
)
|
|
||||||
config.Version = flag.Bool("v", false, "print spoof-dpi's version. this may contain some other relevant information")
|
|
||||||
|
|
||||||
|
var allowedPattern StringArray
|
||||||
|
flag.Var(
|
||||||
|
&allowedPattern,
|
||||||
|
"pattern",
|
||||||
|
"bypass DPI only on packets matching this regex pattern; can be given multiple times",
|
||||||
|
)
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
if len(allowedHosts) > 0 {
|
for _, pattern := range allowedPattern {
|
||||||
var escapedUrls []string
|
config.AllowedPattern = append(config.AllowedPattern, regexp.MustCompile(pattern))
|
||||||
for _, host := range allowedHosts {
|
|
||||||
escapedUrls = append(escapedUrls, regexp.QuoteMeta(host))
|
|
||||||
}
|
|
||||||
|
|
||||||
allowedHostsRegex := strings.Join(escapedUrls, "|")
|
|
||||||
config.AllowedUrls = regexp.MustCompile(allowedHostsRegex)
|
|
||||||
}
|
|
||||||
|
|
||||||
if *allowedPattern != "" {
|
|
||||||
config.AllowedPattern = regexp.MustCompile(*allowedPattern)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -98,14 +83,6 @@ func PrintColoredBanner() {
|
|||||||
{Level: 0, Text: "DNS : " + fmt.Sprint(*config.DnsAddr)},
|
{Level: 0, Text: "DNS : " + fmt.Sprint(*config.DnsAddr)},
|
||||||
{Level: 0, Text: "DEBUG : " + fmt.Sprint(*config.Debug)},
|
{Level: 0, Text: "DEBUG : " + fmt.Sprint(*config.Debug)},
|
||||||
}).Render()
|
}).Render()
|
||||||
|
|
||||||
if allowedHosts != nil && len(allowedHosts) > 0 {
|
|
||||||
log.Info("White listed urls: ", allowedHosts)
|
|
||||||
}
|
|
||||||
|
|
||||||
if *allowedPattern != "" {
|
|
||||||
log.Info("Regex Pattern: ", *allowedPattern)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func PrintSimpleInfo() {
|
func PrintSimpleInfo() {
|
||||||
|
Loading…
Reference in New Issue
Block a user