#530 VPN 与 NAT
开发工具 计算机网络 VPN NAT iptables 2021-05-09我公司办公环境需要用到 OpenVPN,之前允许一个账号在两处登录,现在处于安全考虑只允许一处登录了。
然后,我就不方便了,因为我办公环境有两台电脑(台式机 Windows,笔记本 Ubuntu)都需要接入 VPN。
coding in a complicated world
我公司办公环境需要用到 OpenVPN,之前允许一个账号在两处登录,现在处于安全考虑只允许一处登录了。
然后,我就不方便了,因为我办公环境有两台电脑(台式机 Windows,笔记本 Ubuntu)都需要接入 VPN。
先列一下目前最火的几个 Go 语言框架:
Gin 相比 Beego 非常轻量级,不带 ORM,不支持 Session,正则路由都不支持,需要自行实现。
最新版本 56 个 文件, 算上注释,大概七千行代码。
$ find . -name "*.go" -not -path "*_test.go" | cat | wc -l
56
$ find . -name "*.go" -not -path "*_test.go" | xargs cat | wc -l
6910
Tag | CreatedAt | ReleasedAt |
---|---|---|
v1.9.0 | 2023-02-21 | 2023-02-21 |
v1.8.2 | 2022-12-22 | 2022-12-22 |
v1.8.1 | 2022-06-06 | 2022-06-06 |
v1.8.0 | 2022-05-30 | 2022-05-30 |
v1.7.7 | 2021-11-24 | 2021-11-24 |
v1.7.6 | 2021-08-03 | 2021-11-23 |
v1.7.4 | 2021-08-03 | 2021-08-15 |
v1.7.3 | 2021-08-03 | 2021-08-03 |
v1.7.2 | 2021-05-21 | 2021-05-21 |
v1.7.1 | 2021-04-08 | 2021-04-08 |
v1.7.0 | 2021-04-08 | 2021-04-08 |
v1.6.3 | 2020-05-03 | 2020-05-03 |
v1.6.2 | 2020-03-27 | 2020-03-27 |
v1.6.1 | 2020-03-23 | 2020-03-23 |
v1.6.0 | 2020-03-22 | 2020-03-22 |
v1.5.0 | 2019-11-24 | 2019-11-28 |
v1.4.0 | 2019-05-07 | 2019-05-08 |
v1.3.0 | 2018-08-14 | 2018-08-14 |
v1.2 | 2017-07-02 | 2017-07-02 |
v1.1.4 | 2016-12-04 | 2016-12-05 |
v1.1.3 | 2016-12-03 | 2016-12-04 |
// c: *gin.Context
// func (c *gin.Context) {
// balabala...
// }
c.Request.URL.Query()
c.Param(key string) string // 路径参数
c.Params.ByName(key)
c.Query(key string) string // 查询参数
c.Request.URL.Query().Get(key)
c.GetQuery(key)
c.QueryMap(key string) map[string][]string
c.GetQueryMap(key)
c.QueryArray(key string) []string
c.GetQuery(key string) (string, bool)
c.GetQueryArray(key) 的第 0 个元素
c.GetQueryMap(key string) (map[string]string, bool)
c.GetQueryArray(key string) (values []string, ok bool)
c.PostForm(key string) string
c.PostFormMap(key string) map[string][]string
c.PostFormArray(key string) []string
c.GetPostForm(key string) (string, bool)
c.GetPostFormMap(key string) (map[string]string, bool)
c.GetPostFormArray(key string) (values []string, ok bool)
c.FormFile(key string) (*multipart.FileHeader, error)
c.DefaultQuery(key string, defaultValue string) string
c.DefaultPostForm(key string, defaultValue string) string
package main
import (
"io"
"net/http"
"os"
"path/filepath"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
// 处理 GET 请求中的 URL 参数
r.GET("/hello", func(c *gin.Context) {
name := c.Query("name")
c.String(http.StatusOK, "Hello %s!", name)
})
// 处理带路径参数的 GET 请求
r.GET("/users/:id", func(c *gin.Context) {
id := c.Param("id")
c.String(http.StatusOK, "User ID: %s", id)
})
// 处理 POST 请求中的 JSON 请求体
r.POST("/data", func(c *gin.Context) {
var json struct {
Data string `json:"data"`
}
c.BindJSON(&json)
c.JSON(http.StatusOK, gin.H{"message": "Received data: " + json.Data})
})
// 处理文件上传
r.POST("/upload", func(c *gin.Context) {
file, err := c.FormFile("file")
if err != nil {
c.String(http.StatusBadRequest, "File upload failed: %s", err.Error())
return
}
// 将上传的文件保存到服务器上
dst := filepath.Join("./uploads", file.Filename)
if err := c.SaveUploadedFile(file, dst); err != nil {
c.String(http.StatusInternalServerError, "File save failed: %s", err.Error())
return
}
c.String(http.StatusOK, "File uploaded successfully")
})
// 处理返回文件
r.GET("/download", func(c *gin.Context) {
filename := "example.txt"
filepath := "./downloads/" + filename
// 打开文件
file, err := os.Open(filepath)
if err != nil {
c.String(http.StatusInternalServerError, "Failed to open file: %s", err.Error())
return
}
defer file.Close()
// 设置响应头
c.Header("Content-Disposition", "attachment; filename="+filename)
c.Header("Content-Type", "application/octet-stream")
c.Header("Content-Transfer-Encoding", "binary")
// 将文件内容写入响应主体
_, err = io.Copy(c.Writer, file)
if err != nil {
c.String(http.StatusInternalServerError, "Failed to write file: %s", err.Error())
}
})
r.Run(":8080")
}
设置路由不存在的处理方法:
r.NoRoute(func(c *gin.Context) {
c.JSON(404, gin.H{"message": "page not found"})
})
限制请求大小:
r.MaxMultipartMemory = 10 << 20 // 10MB
// "github.com/gin-contrib/size"
r.Use(size.Check(size.MB * 10))
r.Use(requestSizeLimitMiddleware(10 << 20))
func requestSizeLimitMiddleware(maxSize int64) gin.HandlerFunc {
return func(c *gin.Context) {
if c.Request.ContentLength > maxSize {
c.AbortWithStatusJSON(413, gin.H{"message": "request entity too large"})
return
}
c.Next()
}
}
检查端口可连接性,并支持检测操作系统信息、占用端口的常见应用程序。
关闭或过滤(Closed|Filtered)
默认扫描常用端口
nmap 192.168.31.0/24
# Ping扫描
nmap -sn 192.168.31.0/24
# 快速端口扫描
nmap -F 192.168.31.0/24
# 操作系统
sudo nmap -O -v 192.168.31.42
sudo nmap -A -v 192.168.31.42 # 输出更详细
# 常规检查:端口(-sV)和操作系统(-O)
sudo nmap -sV -O 192.168.31.42
# 检查指定端口
nmap -p 22 192.168.31.42
nmap -p 22,80,443,3306 192.168.31.42
# 所有端口
nmap -p 1-65525 192.168.31.42
nmap -p- 192.168.31.42
# 加上使用这个端口的服务版本信息
nmap -p 22 192.168.31.42 -sV
# 对端口进行探测
nmap -PS 192.168.31.42 # SYN
nmap -PA 192.168.31.42 # ASK
nmap -PU 192.168.31.42 # UDP
sudo nmap -sS 192.168.1.1-10 # SYN
sudo nmap -sA 192.168.31.0/24 # ACK
sudo nmap -sF 192.168.31.0/24 # FIN
sudo nmap -sN 192.168.31.0/24 # TCP Null
sudo nmap -sT 192.168.31.0/24 # TCP Connect
sudo nmap -sU 192.168.31.0/24 # UDP
sudo nmap -sO 192.168.31.0/24 # IP 协议扫描
nmap -O 192.168.0.1/16
nmap -sP 192.168.0.1/16
nmap -sSU 192.168.1.1/24
# Ping Scan
# Parallel DNS resolution
# SYN Stealth Scan
# UDP Scan
type ExpiringMap struct {
m sync.Map
expiry time.Duration
}
func (e *ExpiringMap) set(key, value interface{}) {
e.m.Store(key, value)
go func() {
time.Sleep(e.expiry)
e.m.Delete(key)
}()
}
package main
import (
"sync"
"time"
)
type Item struct {
value interface{}
expiration int64
}
type ExpiringMap struct {
m sync.Mutex
items map[string]Item
timeout time.Duration
}
func NewExpiringMap(timeout time.Duration) *ExpiringMap {
em := &ExpiringMap{
items: make(map[string]Item),
timeout: timeout,
}
go em.cleanup()
return em
}
func (em *ExpiringMap) Set(key string, value interface{}) {
em.m.Lock()
defer em.m.Unlock()
em.items[key] = Item{
value: value,
expiration: time.Now().Add(em.timeout).UnixNano(),
}
}
func (em *ExpiringMap) Get(key string) (interface{}, bool) {
em.m.Lock()
defer em.m.Unlock()
item, found := em.items[key]
if !found {
return nil, false
}
if time.Now().UnixNano() > item.expiration {
return nil, false
}
return item.value, true
}
func (em *ExpiringMap) Delete(key string) {
em.m.Lock()
defer em.m.Unlock()
delete(em.items, key)
}
func (em *ExpiringMap) deleteExpiredKeys() {
em.m.Lock()
defer em.m.Unlock()
now := time.Now().UnixNano()
for key, item := range em.items {
if now < item.expiration {
break
}
delete(em.items, key)
}
}
func (em *ExpiringMap) cleanup() {
for {
time.Sleep(em.timeout) // 阻塞协程
em.deleteExpiredKeys()
}
}
func main() {
em := NewExpiringMap(time.Second * 10)
time.Sleep(time.Second * 15)
_, ok := em.Get("key1")
if !ok {
println("key1 is deleted")
}
_, ok = em.Get("key2")
if !ok {
println("key2 is deleted")
}
}
https://github.com/valyala/fasthttp
package main
import (
"flag"
"fmt"
"log"
"github.com/valyala/fasthttp"
)
var (
addr = flag.String("addr", ":8080", "TCP address to listen to")
compress = flag.Bool("compress", false, "Whether to enable transparent response compression")
)
func main() {
flag.Parse()
h := requestHandler
if *compress {
h = fasthttp.CompressHandler(h)
}
if err := fasthttp.ListenAndServe(*addr, h); err != nil {
log.Fatalf("Error in ListenAndServe: %v", err)
}
}
func requestHandler(ctx *fasthttp.RequestCtx) {
fmt.Fprintf(ctx, "Hello, world!\n\n")
fmt.Fprintf(ctx, "Request method is %q\n", ctx.Method())
fmt.Fprintf(ctx, "RequestURI is %q\n", ctx.RequestURI())
fmt.Fprintf(ctx, "Requested path is %q\n", ctx.Path())
fmt.Fprintf(ctx, "Host is %q\n", ctx.Host())
fmt.Fprintf(ctx, "Query string is %q\n", ctx.QueryArgs())
fmt.Fprintf(ctx, "User-Agent is %q\n", ctx.UserAgent())
fmt.Fprintf(ctx, "Connection has been established at %s\n", ctx.ConnTime())
fmt.Fprintf(ctx, "Request has been started at %s\n", ctx.Time())
fmt.Fprintf(ctx, "Serial request number for the current connection is %d\n", ctx.ConnRequestNum())
fmt.Fprintf(ctx, "Your ip is %q\n\n", ctx.RemoteIP())
fmt.Fprintf(ctx, "Raw request is:\n---CUT---\n%s\n---CUT---", &ctx.Request)
ctx.SetContentType("text/plain; charset=utf8")
// Set arbitrary headers
ctx.Response.Header.Set("X-My-Header", "my-header-value")
// Set cookies
var c fasthttp.Cookie
c.SetKey("cookie-name")
c.SetValue("cookie-value")
ctx.Response.Header.SetCookie(&c)
}
r.Body
-> ctx.PostBody()
r.URL.Path
-> ctx.Path()
r.URL
-> ctx.URI()
r.Method
-> ctx.Method()
r.Header
-> ctx.Request.Header
r.Header.Get()
-> ctx.Request.Header.Peek()
r.Host
-> ctx.Host()
r.Form
-> ctx.QueryArgs()
+ ctx.PostArgs()
r.PostForm
-> ctx.PostArgs()
r.FormValue()
-> ctx.FormValue()
r.FormFile()
-> ctx.FormFile()
r.MultipartForm
-> ctx.MultipartForm()
r.RemoteAddr
-> ctx.RemoteAddr()
r.RequestURI
-> ctx.RequestURI()
r.TLS
-> ctx.IsTLS()
r.Cookie()
-> ctx.Request.Header.Cookie()
r.Referer()
-> ctx.Referer()
r.UserAgent()
-> ctx.UserAgent()
w.Header()
-> ctx.Response.Header
w.Header().Set()
-> ctx.Response.Header.Set()
w.Header().Set("Content-Type")
-> ctx.SetContentType()
w.Header().Set("Set-Cookie")
-> ctx.Response.Header.SetCookie()
w.Write()
-> ctx.Write()
, ctx.SetBody()
, ctx.SetBodyStream()
, ctx.SetBodyStreamWriter()
w.WriteHeader()
-> ctx.SetStatusCode()
w.(http.Hijacker).Hijack()
-> ctx.Hijack()
http.Error()
-> ctx.Error()
http.FileServer()
-> fasthttp.FSHandler()
, fasthttp.FS
http.ServeFile()
-> fasthttp.ServeFile()
http.Redirect()
-> ctx.Redirect()
http.NotFound()
-> ctx.NotFound()
http.StripPrefix()
-> fasthttp.PathRewriteFunc
/user/{user}
named/user/{user}_admin
/user/{user?}
optional/user/{name:[a-zA-Z]{5}}
regex/user/{name?:[a-zA-Z]{5}}
/user/{name:*}
cache allpackage main
import (
"log"
"time"
"github.com/fasthttp/session/v2"
"github.com/fasthttp/session/v2/providers/redis"
)
var serverSession *session.Session
func init() {
var provider session.Provider
var err error
encoder := session.MSGPEncode
decoder := session.MSGPDecode
provider, err = redis.New(redis.Config{
KeyPrefix: "session",
Addr: "127.0.0.1:6379",
PoolSize: 8,
IdleTimeout: 30 * time.Second,
})
if err != nil {
log.Fatal(err)
}
cfg := session.NewDefaultConfig()
cfg.EncodeFunc = encoder
cfg.DecodeFunc = decoder
serverSession = session.New(cfg)
if err = serverSession.SetProvider(provider); err != nil {
log.Fatal(err)
}
}
// 获取 SessionStore
store, err := serverSession.Get(ctx)
if err != nil {
ctx.Error(err.Error(), fasthttp.StatusInternalServerError)
return
}
defer func() {
if err := serverSession.Save(ctx, store); err != nil {
ctx.Error(err.Error(), fasthttp.StatusInternalServerError)
}
}()
// Set
store.Set("foo", "bar")
// Get
val := store.Get("foo")
if val == nil {
// ...
}
store.Delete(key)
store.Set(bytesKey)
val := store.GetBytes(bytesKey)
store.DeleteBytes(bytesKey)
data := store.GetAll() // Dict
data := store.Ptr() // *Dict
store.Flush()
store.Reset()
// Session ID,默认 sessionid
id := store.GetSessionID()
store.SetSessionID(bytesID)
// Session 有效期
changed := store.HasExpirationChanged()
duration := store.GetExpiration()
err := store.SetExpiration(duration)
构建与 fasthttp 和 fasthttp/router 之上的 HTTP 框架,做了一些简单的封装工作。
提供了几个示例。
基础用法:
package main
import (
"github.com/savsgio/atreugo/v11"
)
func main() {
config := atreugo.Config{
Addr: "0.0.0.0:8000",
}
server := atreugo.New(config)
server.GET("/", func(ctx *atreugo.RequestCtx) error {
return ctx.TextResponse("Hello World")
})
server.GET("/echo/{path:*}", func(ctx *atreugo.RequestCtx) error {
return ctx.TextResponse("Echo message: " + ctx.UserValue("path").(string))
})
v1 := server.NewGroupPath("/v1")
v1.GET("/", func(ctx *atreugo.RequestCtx) error {
return ctx.TextResponse("Hello V1 Group")
})
if err := server.ListenAndServe(); err != nil {
panic(err)
}
}
关于 convert 的文章,之前已经写过两篇:
sudo apt install imagemagick
convert -list type
convert -list font # 支持的字体
identify markjour.png
identify -verbose markjour.png
identify -format "Size: %w x %h\n" markjour.png
# Exif 信息
identify -format '%[Exif:*]' ~/Pictures/Photos/2019-09-14-18-48-22.jpg
# sudo apt install exif
exif ~/Pictures/Photos/2019-09-14-18-48-22.jpg
convert -strip input.jpg out.jpg
convert +profile "*" input.jpg out.jpg
<宽> x <高> + <X轴坐标> + <Y轴坐标>
convert -crop 300x400+10+10 src.jpg dest.jpg
# 指定中心位址为基准点:
convert -gravity center -crop 300x400+0+0 src.jpg dest.jpg
convert -crop 25%x100% src.jpg dest.jpg
之前(convert 图片转换的一次示例)合并图片用的就是这个命令。
# 横向
convert +append markjour-l.jpg markjour-c.jpg markjour-r.jpg markjour.jpg
# 纵向
convert -append markjour-t.jpg markjour-c.jpg markjour-b.jpg markjour.jpg
convert -rotate 90 input.jpg output.jpg # 顺时针
convert -rotate -90 input.jpg output.jpg # 逆时针
# 左右反转,镜像效果
convert -flop input.jpg output.jpg
# 上下反转,这个和旋转 270 效果还是不一样的
convert -flip input.jpg output.jpg
# 限宽我很常用,控制页面图片尺寸
convert -resize 108x markjour.png markjour-108.png
convert -sample 50% markjour.png markjour-new.png
convert -sample 200% markjour.png markjour-big.png
PS: 放大时 resize 会自动采样插值,而 sample 不会
convert input.jpg -quality 50 output.jpg
# 灰度,就是常见的黑白照片效果
convert -colorspace gray input.jpg output.jpg
# 分离 RGB 三个通道,输出三张图片,不知道为什么都是灰色
convert -separate input.png output.png
convert -threshold 40% input.png output.png # 也是一种常见效果,不知道叫什么
convert -negate input.png output.png # 反色
# 黑白(非灰度)sRGB -> Gray 2c
convert -monochrome input.png output.png
# 重新转成 sRGB 模式(但是颜色还是留在黑白两色)
convert -define png:color-type=2 input.png output.png
convert -colorspace sRGB -type truecolor input.jpg output.jpg
# 效果很奇特,可以试试:
convert -remap pattern:gray60 input.png output.png
# 替换
convert -fuzz 15% -fill white -opaque "rgb(143,141,250)" -opaque "rgb(216,217,62)" input.png output.png
convert -blur 70 input.png output.png
# 后面的数字对模糊程度有着决定性作用
convert -blur 70x15 input.png output.png
convert -charcoal 2 input.png output.png # 铅笔画风格
convert -noise 3 input.png output.png
convert -paint 4 input.png output.png # 油画风格
convert -spread 50 input.png output.png # 毛玻璃
convert -swirl 60 input.png output.png # 扭曲
convert -paint 4 -raise 5x5 input.png output.png
# 调整透明度
# 先确保图片有 Alpha 通道
convert -define png:format=png32 input.png output.png
convert -channel alpha -fx "0.5" output.png output2.png
# 加边框
convert -mattecolor "#000" -frame 60x60 input.png output.png
convert -mattecolor "#fff" -frame 60x60 input.png output.png
# 相同效果
convert -bordercolor "#fff" -border 60x60 input.png output.png
# 再配合上 raise:
convert -bordercolor "#fff" -border 10x10 input.png output.png
convert -raise 5x5 output.png output2.png
# 去掉边框:
convert -trim -fuzz 10% input.png output.png
convert -fill "#1770CC" \
-font Ubuntu-Mono -pointsize 50 -draw 'text 130,50 "©"' \
-font 楷体_GB2312 -pointsize 40 -draw 'text 50,50 "码厩"' \
-gravity southeast \
input.png output.png
# 改用 RGBA 模式
convert -fill "rgba(23,112,204,0.25)" \
-font Ubuntu-Mono -pointsize 50 -draw 'text 130,50 "©"' \
-font 楷体_GB2312 -pointsize 40 -draw 'text 50,50 "码厩"' \
-gravity southeast \
input.png output.png
# 这个不错,京东那里学来的:
convert -size 100x100 -fill "#1770CC" -gravity center \
-font Ubuntu -pointsize 30 -draw 'rotate -45 text 0,0 "markjour"' \
xc:none miff:- | composite -tile -dissolve 25 - input.png output.png
# 图片水印
convert -size 150x50 -fill "#1770CC" -gravity center \
-font Ubuntu -pointsize 30 -draw 'text 0,0 "markjour"' \
xc:none /tmp/mark.png
convert input.png -gravity southeast -compose over /tmp/mark.png -composite output.png
# 查看图片
# GNOME 桌面好像都是 eog
eog markjour.png
# 或者使用 ImageMagick 自带图片查看工具:
display markjour.png
# 查看颜色信息
convert xc:"#fff" -colorspace sRGB -format "%[pixel:u.p{0,0}]\n" txt:
convert xc:"#fff" -colorspace sRGB -format "%[pixel:u.p{0,0}]\n" info:
convert xc:"#fff" -colorspace sRGB -format "rgb(%[fx:int(255*r)],%[fx:int(255*g)],%[fx:int(255*b)])\n" info:
# 获取指定像素点的颜色(RGB)
convert "input.png[1x1+100+100]" -format "rgb(%[fx:int(255*r)],%[fx:int(255*g)],%[fx:int(255*b)])\n" info:
# 创建一张新图片
convert -size 1000x1000 xc:none /tmp/image.png
convert -size 1000x1000 xc:transparent /tmp/image.png
convert -size 1000x1000 xc:white /tmp/image.png
sudo apt install -y webp
cwebp
转成 WEBP 格式dwebp
转成别的格式cwebp -o xxx.png xxx.webp
dwebp -o xxx.webp xxx.png
想写几个常用的 ffmpeg 命令作个分享,又想起来还有 convert,pwgen 等命令,干脆搞一个 Linux 工具箱系列,总结一下为什么我被锁定在 Linux 平台上无法自拔了。
正则表达式, Regular Expression, 简写: regex, regexp, 正则。
一般用来以指定模式对字符串进行查找,替换,切割。
Linux 下, grep, find 等命令支持几种风格:
find -regextype help
# find: 未知的正则表达式类型 ‘help’;合法的类型是 ‘findutils-default’, ‘ed’, ‘emacs’, ‘gnu-awk’, ‘grep’, ‘posix-awk’, ‘awk’, ‘posix-basic’, ‘posix-egrep’, ‘egrep’, ‘posix-extended’, ‘posix-minimal-basic’, ‘sed’。
man grep | grep -E '\-regexp$'
# -E, --extended-regexp
# -G, --basic-regexp
# -P, --perl-regexp
# -w, --word-regexp
# -x, --line-regexp
因为历史原因,大体上可以分为:
我见过的编程语言中都是采用最为强大的 PCRE 风格:
regex.h
(libc)regex.h
,或者使用第三方库(如 boost), C 11 之后就内置正则支持了(std::regex
, 默认 ECMAScript 风格)| Characters / constructs | Corresponding article |
| :------------------------------------------------------------------------------------------------------------------------------------------------- | :---------------------------- | ----------------- |
| \
, .
, \cX
, \d
, \D
, \f
, \n
, \r
, \s
, \S
, \t
, \v
, \w
, \W
, \0
, \xhh
, \uhhhh
, \uhhhhh
, [\b]
| Character classes |
| ^
, $
, x(?=y)
, x(?!y)
, (?<=y)x
, (?<!y)x
, \b
, \B
| Assertions |
| (x)
, (?:x)
, (?<Name>x)
, x | y
,[xyz]
,[^xyz]
,\Number
| Groups and ranges |
| *
, +
, ?
, x{n}
, x{n,}
, x{n,m}
| Quantifiers |
| \p{UnicodeProperty}
, \P{UnicodeProperty}
| Unicode property escapes |
.
\w
, \d
, \s
, \W
, \D
, \S
[abc]
[^abc]
[a-z]
(abc)
\1
(?:abc)
(?=abc)
(?!abc)
a*
, a+
, a?
a{7,9}
, a{7}
, a{7,}
a+?
, a{2,}?
^
, $
ab|cd
or\b
, \B
wordJavaScript:
g
globali
case insensitivem
multilines
single line (dotall)u
unicodey
stickyGo:
i
m
s
U
Python:
re.A
, re.ASCII
re.I
, re.IGNORECASE
re.M
, re.MULTILINE
re.S
, re.DOTALL
re.DEBUG
re.X
, re.VERBOSE