xyxsw
文章35
标签3
分类0
DNS 与 Pdns OpenWrt DNS 递归服务器搭建与 MosDNS 分流

DNS 与 Pdns OpenWrt DNS 递归服务器搭建与 MosDNS 分流

DNS

什么是DNS

域名系统(英语:Domain Name System,缩写:DNS)是互联网的一项服务。它作为将域名和IP地址相互映射的一个分布式数据库,能够使人更方便地访问互联网。

简单来说就是用域名请求服务器换回来ip地址

https://www.nslookup.io/

这个网站可以看dns请求的结果、同理还有linux或者windows的nslookup命令

DNS records for dn11.top

A records

IPv4 address Revalidate in
104.21.27.178 5m
172.67.143.107 5m

DNS 服务器使人们无需存储复杂记不住的ip地址,而是记住有规律的域名来方便的访问互联网

DNS记录

DNS 记录是位于权威 DNS 服务器中的指令,提供一个域的相关信息,包括哪些 IP 地址与该域关联,以及如何处理对该域的请求。

常见的 DNS 记录

SOA 记录 - 存储域的管理信息。

;; ANSWER SECTION:
dn11.top.               1800    IN      SOA     ophelia.ns.cloudflare.com. dns.cloudflare.com. 2322082117 10000 2400 604800 1800

NS 记录 - 存储 DNS 条目的名称服务器。

;; ANSWER SECTION:
dn11.top.               21600   IN      NS      ophelia.ns.cloudflare.com.
dn11.top.               21600   IN      NS      phil.ns.cloudflare.com.

A 记录 - 保存域的 IPv4 地址的记录。

;; ANSWER SECTION:
dn11.top.               300     IN      A       104.21.27.178
dn11.top.               300     IN      A       172.67.143.107

AAAA 记录 - 包含域的 IPv6 地址的记录(与 A 记录相反,A 记录列出的是 IPv4 地址)。

CNAME 记录 - 将一个域或子域转发到另一个域,不提供 IP 地址。

MX 记录 - 将邮件定向到电子邮件服务器。

TXT 记录 - 可让管理员在记录中存储文本注释。这些记录通常用于电子邮件安全。

有几种DNS服务器?有几种工作方式?

有两种DNS服务器:DNS递归服务器和DNS权威服务器

有两种工作方式:递归和迭代

DNS客户端设置使用的DNS服务器一般都是DNS递归服务器,它负责全权处理客户端的DNS查询请求,直到返回最终结果。而DNS服务器之间一般采用迭代查询方式。

如何工作的

DNS查询 涉及 4 个 DNS 服务器:

  1. DNS 递归解析器

DNS 解析器是一种服务器,旨在通过 Web 浏览器等应用程序接收客户端计算机的查询。然后,解析器一般负责发出其他请求,以便满足客户端的 DNS 查询。

常见的 dns 递归解析器有

谷歌 8.8.8.8 8.8.4.4

阿里 223.5.5.5 223.6.6.6

百度 180.76.76.76

腾讯DNSPod 119.29.29.29

CloudFlare 1.1.1.1

更多 https://en.wikipedia.org/wiki/Public_recursive_name_server

  1. 根域名服务器

根域名服务器是将主机名转换(解析)为 IP 地址的第一步。

根域名服务器(英语:root name server,简称“根域名服务器”)是互联网域名解析系统(DNS)中最高级别的域名服务器,负责返回顶级域的权威域名服务器地址。

任意正常 linux 发行版可以运行如下命令来看到这13台根域名服务器

ubuntu@ubuntu_dn11:~$ dig ns . @8.8.8.8

; <<>> DiG 9.18.12-0ubuntu0.22.04.3-Ubuntu <<>> ns . @8.8.8.8
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 9453
;; flags: qr rd ra ad; QUERY: 1, ANSWER: 13, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 512
;; QUESTION SECTION:
;.                              IN      NS

;; ANSWER SECTION:
.                       71928   IN      NS      a.root-servers.net.
.                       71928   IN      NS      b.root-servers.net.
.                       71928   IN      NS      c.root-servers.net.
.                       71928   IN      NS      d.root-servers.net.
.                       71928   IN      NS      e.root-servers.net.
.                       71928   IN      NS      f.root-servers.net.
.                       71928   IN      NS      g.root-servers.net.
.                       71928   IN      NS      h.root-servers.net.
.                       71928   IN      NS      i.root-servers.net.
.                       71928   IN      NS      j.root-servers.net.
.                       71928   IN      NS      k.root-servers.net.
.                       71928   IN      NS      l.root-servers.net.
.                       71928   IN      NS      m.root-servers.net.

;; Query time: 0 msec
;; SERVER: 8.8.8.8#53(8.8.8.8) (UDP)
;; WHEN: Fri Oct 06 13:22:43 UTC 2023
;; MSG SIZE  rcvd: 239

全球13根域名服务器以英文字母A到M依序命名,域名格式为“字母.root-servers.net”。利用任播(anycast)技术在全球多个地点设立镜像站。

截至2023年6月,全球共有1719台根域名服务器在运行。

Q:为什么是13组?1700组不行吗?

A:由于DNS和某些协议(未分片的用户数据报协议(UDP)数据包在IPv4内的最大有效大小为512字节)的共同限制,根域名服务器地址的数量被限制为13个。一次返回的数据放不下啦~

Q:anycast是什么?

A:我们假设有三台服务器α、β、γ和一台客户机,这三台服务器都宣告了同样的一个地址比如说172.16.255.53,客户机访问172.16.255.53时,会因为路由协议的原因,选择客户机距离服务器最近的一条路径。假设α距离客户机近,访问172.16.255.53获得的数据就是α服务器给客户机的。

Q:anycast的好处?

A:防止服务挂掉、流量会自动寻找最佳路径、假设一台服务器挂掉了,整个服务会被另一台服务器承担。

Q:有 . 这个服务器吗

A:存放这13个根域的是一个文件,它被内置在递归服务器内,没有权威服务器会返回 . 这个域的解析给你。
在这里可以下载到这个文件 https://www.iana.org/domains/root/files
运营商通常需要配置一个 “Root Hints 文件” 来管理 DNS 递归解析器。该文件包含了根区域的权威名称服务器的名称和 IP 地址,以便软件可以引导 DNS 解析过程。对于许多软件来说,这个列表已经内置在软件中。https://www.internic.net/domain/named.root

  1. TLD 名称服务器

顶级域名(英语:Top-level Domain, TLD)是互联网域名系统的等级中,位于根域空间的最高级域名。

顶级域名服务器这个服务器是搜索特定 IP 地址的下一步,其上托管了主机名的最后一部分(例如,在 dn11.top 中,TLD 服务器为 “top”)。

https://www.iana.org/domains/root/db 这里是世界上完整的顶级域列表。

  1. 权威性域名服务器

权威性域名服务器是域名服务器查询中的最后一站。

权威名称服务器包含特定于其服务域名的信息(例如,dn11.top)权威性域名服务器包含特定于其所服务的域名的信息(例如 dn11.top),并且它可为递归解析器提供在 DNS A 记录中找到的服务器的 IP 地址(例如找到了上文的ipv4地址 104.21.27.178)

图解工作流程

递归工作流程

我这里画了一张图:其中红色服务器为DNS递归服务器,PC上的DNS客户端只与DNS递归服务器通讯,由递归服务器完成后续查询内容。蓝色云为想要访问的网站的ip地址所在的服务器。

我会逐一讲解每个过程(箭头)都做了什么。

本图的前提是dns递归服务器中不含任何缓存。

①:PC通过浏览器输入了dn11.top这个域名、dns客户端收到了请求dn11.top这个域名的ip的任务、dns客户端去请求DNS递归服务器

②:DNS递归服务器使用内置的Root Hint文件(内有根服务器的NS解析和根服务器的IP地址)去请求根权威服务器、得到了TLD服务器的NS解析并得到了TLD服务器的ip地址

③:TLD服务器的ip地址被返回给DNS递归服务器

④:DNS递归服务器去请求TLD服务器、得到了域名权威服务器的NS解析并得到了域名权威服务器的NS记录和IP地址

⑤:域名权威服务器的ip地址被返回给DNS递归服务器

⑥:DNS递归服务器去请求域名权威服务器、得到了请求域名最终的ip

⑦:请求域名最终的ip被返回给DNS递归服务器

⑧:请求域名最终的ip被返回给PC上的DNS客户端

⑨:PC上的浏览器用请求域名最终的ip访问域名所在的云服务器

⑩:云服务器返回网页数据给PC浏览器、实现页面的访问

迭代工作流程

我这里画了一张图:其中红色服务器为DNS递归服务器,PC上的DNS客户端只与DNS递归服务器通讯,由递归服务器和权威服务器的迭代查询完成后续查询内容。蓝色云为想要访问的网站的ip地址所在的服务器。

我会逐一讲解每个过程(箭头)都做了什么。

本图的前提是dns递归服务器中不含任何缓存。

①:PC通过浏览器输入了dn11.top这个域名、dns客户端收到了请求dn11.top这个域名的ip的任务、dns客户端去请求DNS递归服务器

②:DNS递归服务器使用内置的Root Hint文件(内有根服务器的NS解析和根服务器的IP地址)去请求根权威服务器、根权威服务器收到了请求、找到了TLD权威服务器的NS记录和ip地址

③:根权威服务器请求TLD服务器、TLD服务器收到了请求、找到了域名权威服务器的NS记录和ip地址

④:TLD服务器请求域名权威服务器、权威服务器找到了域名的ip地址

⑤:域名的ip地址从域名权威服务器返回给TLD权威服务器

⑥:域名的ip地址从TLD权威服务器返回给根权威服务器

⑦:域名的ip地址从根权威返回给DNS递归服务器

⑧:请求域名最终的ip被返回给PC上的DNS客户端

⑨:PC上的浏览器用请求域名最终的ip访问域名所在的云服务器

⑩:云服务器返回网页数据给PC浏览器、实现页面的访问

配置pdns

1.openwrt直接启动

装包

opkg update
opkg install pdns pdns-backend-sqlite3 pdns-recursor sqlite3-cli

初始化数据库

创建文件

mkdir -p /usr/share/doc/pdns-backend-sqlite/
touch schema.sqlite3.sql

然后在schema.sqlite3.sql里写入

nano /usr/share/doc/pdns-backend-sqlite/schema.sqlite3.sql
PRAGMA foreign_keys = 1;

CREATE TABLE domains (
  id                    INTEGER PRIMARY KEY,
  name                  VARCHAR(255) NOT NULL COLLATE NOCASE,
  master                VARCHAR(128) DEFAULT NULL,
  last_check            INTEGER DEFAULT NULL,
  type                  VARCHAR(8) NOT NULL,
  notified_serial       INTEGER DEFAULT NULL,
  account               VARCHAR(40) DEFAULT NULL,
  options               VARCHAR(65535) DEFAULT NULL,
  catalog               VARCHAR(255) DEFAULT NULL
);

CREATE UNIQUE INDEX name_index ON domains(name);
CREATE INDEX catalog_idx ON domains(catalog);


CREATE TABLE records (
  id                    INTEGER PRIMARY KEY,
  domain_id             INTEGER DEFAULT NULL,
  name                  VARCHAR(255) DEFAULT NULL,
  type                  VARCHAR(10) DEFAULT NULL,
  content               VARCHAR(65535) DEFAULT NULL,
  ttl                   INTEGER DEFAULT NULL,
  prio                  INTEGER DEFAULT NULL,
  disabled              BOOLEAN DEFAULT 0,
  ordername             VARCHAR(255),
  auth                  BOOL DEFAULT 1,
  FOREIGN KEY(domain_id) REFERENCES domains(id) ON DELETE CASCADE ON UPDATE CASCADE
);

CREATE INDEX records_lookup_idx ON records(name, type);
CREATE INDEX records_lookup_id_idx ON records(domain_id, name, type);
CREATE INDEX records_order_idx ON records(domain_id, ordername);


CREATE TABLE supermasters (
  ip                    VARCHAR(64) NOT NULL,
  nameserver            VARCHAR(255) NOT NULL COLLATE NOCASE,
  account               VARCHAR(40) NOT NULL
);

CREATE UNIQUE INDEX ip_nameserver_pk ON supermasters(ip, nameserver);


CREATE TABLE comments (
  id                    INTEGER PRIMARY KEY,
  domain_id             INTEGER NOT NULL,
  name                  VARCHAR(255) NOT NULL,
  type                  VARCHAR(10) NOT NULL,
  modified_at           INT NOT NULL,
  account               VARCHAR(40) DEFAULT NULL,
  comment               VARCHAR(65535) NOT NULL,
  FOREIGN KEY(domain_id) REFERENCES domains(id) ON DELETE CASCADE ON UPDATE CASCADE
);

CREATE INDEX comments_idx ON comments(domain_id, name, type);
CREATE INDEX comments_order_idx ON comments (domain_id, modified_at);


CREATE TABLE domainmetadata (
 id                     INTEGER PRIMARY KEY,
 domain_id              INT NOT NULL,
 kind                   VARCHAR(32) COLLATE NOCASE,
 content                TEXT,
 FOREIGN KEY(domain_id) REFERENCES domains(id) ON DELETE CASCADE ON UPDATE CASCADE
);

CREATE INDEX domainmetaidindex ON domainmetadata(domain_id);


CREATE TABLE cryptokeys (
 id                     INTEGER PRIMARY KEY,
 domain_id              INT NOT NULL,
 flags                  INT NOT NULL,
 active                 BOOL,
 published              BOOL DEFAULT 1,
 content                TEXT,
 FOREIGN KEY(domain_id) REFERENCES domains(id) ON DELETE CASCADE ON UPDATE CASCADE
);

CREATE INDEX domainidindex ON cryptokeys(domain_id);


CREATE TABLE tsigkeys (
 id                     INTEGER PRIMARY KEY,
 name                   VARCHAR(255) COLLATE NOCASE,
 algorithm              VARCHAR(50) COLLATE NOCASE,
 secret                 VARCHAR(255)
);

CREATE UNIQUE INDEX namealgoindex ON tsigkeys(name, algorithm);

初始化sqlite3

mkdir -p /etc/powerdns/
sqlite3 /etc/powerdns/pdns.sqlite3 < /usr/share/doc/pdns-backend-sqlite/schema.sqlite3.sql
chmod +r /etc/powerdns

配置pdns

nano /etc/powerdns/pdns.conf

写入

launch=gsqlite3
gsqlite3-database=/etc/powerdns/pdns.sqlite3
local-address=<!!你的地址!!>:53
allow-axfr-ips=0.0.0.0/0
slave=yes
allow-notify-from=0.0.0.0/0
allow-dnsupdate-from=0.0.0.0/0
allow-unsigned-notify=yes

解释:由于我们正在配置pdns权威服务器,地址可以选一个你自己子网你喜欢的、比如说我在172.16.3.53

当然监听这种地址需要在网卡上创建一个新ip

对于op来说 可以在web管理界面 选择网络 新建一个接口 区域lan 设备br-lan 地址为172.16.3.53

对于监听53端口、你的本地dnsmasq可能会劫持这个端口

在此我给出的解决方案是关闭dnsmasq

service dnsmasq stop
service dnsmasq disable

然后由于我的dnsmasq会不知道怎么着复活 我写了一个脚本来按死他

nano /root/stop_dnsmasq_if_running.sh
#!/bin/sh
 
# 检查 dnsmasq 是否在运行在端口53上
if netstat -tlunpa | grep ':53 ' | grep -q 'dnsmasq'; then
    service dnsmasq stop
fi
crontab -e
* * * * * /root/stop_dnsmasq_if_running.sh

这个的意思是一个定时脚本 每分钟检测一下 如果他开着 就给他关掉

启动pdns

service pdns restart

配置自己的子域

pdnsutil create-zone ts.dn11 ns1.ts.dn11

其中ts换成你想用的域名

pdnsutil edit-zone ts.dn11

edit-zone会唤醒叫editor的编辑器,如果你没有这个叫editor的编辑器 可以写一下环境变量

nano /etc/profile

添加一行

export EDITOR=/usr/bin/nano

(op的nano默认安装位置在这里、其他你想配的自由发挥)

source /etc/profile

打开edit-zone后 写入

; Warning - every name in this file is ABSOLUTE!
$ORIGIN .
ts.dn11 3600    IN      SOA     ns1.ts.dn11 hostmaster.ts.dn11 2023093018 60 30 604800 60
ts.dn11 3600    IN      NS      ns1.ts.dn11.
ns1.ts.dn11     3600    IN      A     172.16.3.53
ts.dn11 60      IN      A       172.16.3.1

解释:SOA记录里第一个值是你域名权威服务器所在的NS、第二个为邮箱、第三个为流水号(格式YYYYMMDD+2位顺序号)(如果你不改的话pdns会自动帮你顺延流水号)、第四个为刷新间隔、第五个为重试间隔、第六个为过期时间长、第七个为TTL(生存时间值、意为缓存过期时间、设置为60s)

最后一行就是给你的域名加一个A解析

验证

dig a ts.dn11 @172.16.3.53 -p 53

如果返回类似如下内容则成功

root@OP:~# dig a ts.dn11 @172.16.3.53 -p 53

; <<>> DiG 9.18.16 <<>> a ts.dn11 @172.16.3.53 -p 53
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 58804
;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
;; WARNING: recursion requested but not available

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
;; QUESTION SECTION:
;ts.dn11.                       IN      A

;; ANSWER SECTION:
ts.dn11.                60      IN      A       172.16.3.1

;; Query time: 0 msec
;; SERVER: 172.16.3.53#53(172.16.3.53) (UDP)
;; WHEN: Sun Oct 15 18:55:04 CST 2023
;; MSG SIZE  rcvd: 52

配置TLD域、拉取同步

内网用户需要访问 http://172.16.7.102:8083/login (账号密码dn11)来配置自己的域名权威的ns指向

点击dn11域、点击 add record 、添加两条记录

ns1.ts    A      172.16.3.53
ts        NS     ns1.ts.dn11.

之后点击 save 然后点击 apply changes

解释:ns1是你本地自己启的域名权威服务器的地址

然后回到本地 输入

pdnsutil create-zone dn11

创建dn11域

pdnsutil edit-zone dn11

修改里面的soa值为

dn11    300     IN      SOA     a.root.dn11 hostmaster.dn11 1 60 60 604800 60

解释:流水号填小 便于拉取上游的同步

然后写数据库启用主从同步

sqlite3 /etc/powerdns/pdns.sqlite3

进入sqlite命令行

先启用看头

sqlite> .header yes
sqlite> select * from domains;
id|name|master|last_check|type|notified_serial|account|options|catalog
1|ts.dn11|||NATIVE||||
2|dn11|||NATIVE||||

里面大概也许是这样、然后写数据库同步上游

UPDATE domains
SET master = '172.16.7.53', type = 'SLAVE'
WHERE name = 'dn11';

写完按 ctrl + D 退出sqlite命令行

然后使用

pdns_control retrieve dn11

强制拉取一下dn11的记录

最后看下自己的记录有没有被同步

root@OP:~# pdnsutil list-zone dn11

检查一下

root@OP:~# dig ns ts.dn11 @172.16.3.53 -p 53

; <<>> DiG 9.18.16 <<>> ns ts.dn11 @172.16.3.53 -p 53
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 7351
;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
;; WARNING: recursion requested but not available

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
;; QUESTION SECTION:
;ts.dn11.                       IN      NS

;; ANSWER SECTION:
ts.dn11.                60      IN      NS      172.16.3.53.

;; Query time: 0 msec
;; SERVER: 172.16.3.53#53(172.16.3.53) (UDP)
;; WHEN: Sun Oct 15 19:12:09 CST 2023
;; MSG SIZE  rcvd: 61

配置pdns_recursor

nano /etc/powerdns/recursor.conf

写入

# 接受哪些IP的查询请求,0.0.0.0/0表示全部
allow-from=0.0.0.0/0

# 本地监听的接口IP地址
local-address=172.16.255.53:53 172.16.255.53:5300

# 关闭dnssec功能,4.5版本后默认开启
dnssec=off

dont-query=

forward-zones-recurse=dn11=172.16.3.53,.=223.5.5.5

解释:

本地监听的ip地址这里使用anycast、同样的你需要在你的网卡里加一个ip为172.16.255.53(监听5300的原因是、如果路由路径上有人开了dnsmasq、那么这个53请求会被劫持)

并且宣告

root@OP:~# cat /etc/bird.conf

.........

# 宣告 172.16.3.0/24 段
protocol static {
    ipv4 {
        table BGP_table;
        import all;
        export none;
    };

    # 只是为了让BGP的路由表里出现这条路由,不要担心 reject
    # 这个动作其实无所谓,reject 这个动作并不会被发到其他 AS
    # 后续将在导出到 master4 的时候删除这条路由,本地也不会有这一条
    # 请修改为你自己要宣告的段
    route 172.16.3.0/24 reject;
    route 172.16.255.53/32 reject;
}

.........

root@OP:~# service bird restart

dont-query=的意思是允许内网地址进行查询(默认是禁止的、所以这里填空)

forward-zones-recurse=dn11=172.16.3.53,.=223.5.5.5

这个递归配置项的意思是:如果请求到了dn11这个TLD就去请求你自己搭的ns、如果不是这个域就当公网处理转发给公网dns 223.5.5.5

然后重启

service pdns-recursor restart

最后测试一下、请求一下网里存在的域名

root@OP:~# dig op.iraze.dn11 @172.16.255.53 -p 53

; <<>> DiG 9.18.16 <<>> op.iraze.dn11 @172.16.255.53 -p 53
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 4037
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 512
;; QUESTION SECTION:
;op.iraze.dn11.                 IN      A

;; ANSWER SECTION:
op.iraze.dn11.          60      IN      A       172.16.2.2

;; Query time: 39 msec
;; SERVER: 172.16.255.53#53(172.16.255.53) (UDP)
;; WHEN: Sun Oct 15 19:23:32 CST 2023
;; MSG SIZE  rcvd: 58

2.docker拉镜像 装pdns-admin web界面管理

[root@gs-fedora Pdns]# cat docker-compose.yml 
version: "3"

services:
  pdns:
    image: zhonger/pdns:latest
    restart: always
    ports:
      - "172.16.7.53:53:53/tcp"
      - "172.16.7.53:53:53/udp"
    environment:
      - PDNS_launch=gsqlite3
      - PDNS_gsqlite3_database=/var/lib/powerdns/pdns.sqlite3
      - PDNS_webserver_address=0.0.0.0
      - PDNS_webserver_allow_from=0.0.0.0/0
      - PDNS_allow_axfr_ips=0.0.0.0/0,::1
      - PDNS_allow_dnsupdate_from=0.0.0.0/0,::1
      - PDNS_allow_notify_from=0.0.0.0/0,::0
      - PDNS_also_notify=172.16.3.53
      - PDNS_master=yes
      - PDNS_api=yes
      - PDNS_api_key=0F34664B2C9CA2E1B84C5A6B4605C968
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - ./powerdns:/var/lib/powerdns
    command: /bin/bash ./up.sh
    privileged: true
  db:
    image: mysql:latest
    environment:
      - MYSQL_ALLOW_EMPTY_PASSWORD=yes
      - MYSQL_DATABASE=powerdnsadmin
      - MYSQL_USER=pdns 
      - MYSQL_PASSWORD=my_pdns
    restart: always
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - ./pda-mysql:/var/lib/mysql
    privileged: true

  app:
    image: zhonger/powerdns-admin:latest
    restart: always
    depends_on:
      - db
      - pdns
    ports:
      - "8083:80"
    logging:
      driver: json-file
      options:
        max-size: 50m
    volumes:
      - /etc/localtime:/etc/localtime:ro
    environment:
      - SQLALCHEMY_DATABASE_URI=mysql://pdns:my_pdns@db/powerdnsadmin
      - GUNICORN_TIMEOUT=60
      - GUNICORN_WORKERS=2
      - GUNICORN_LOGLEVEL=DEBUG
      - OFFLINE_MODE=False # True for offline, False for external resources 
    privileged: true
[root@gs-fedora Pdns]# cat up.sh 
service system-resolve stop
ip addr add 172.16.7.53 dev eno1

这个key是我随便写的、你可以自己生成一个

PDNS_api_key=0F34664B2C9CA2E1B84C5A6B4605C968

在powerdns admin settings pdns中填写这个key 填好后 在上层的PowerDNS server configuration & statistics里能看到一系列pdns的字段

配置dns分流 MosDNS

装包

opkg update
opkg install mosdns

注意不要装 luci-app-mosdns 我感觉不好用

改配置

nano /etc/mosdns/config.yaml
log:
  level: info
  file: "/tmp/mosdns.log"

api:
  http: "0.0.0.0:9091"

include: []

plugins:
  - tag: main_sequence
    type: sequence
    args:
      # hdu内网dns解析
      - matches: qname hdu.edu.cn
        exec: forward 192.168.0.1:53
      # dn11内网dns解析
      - matches: qname dn11
        exec: forward 172.16.255.53:5300
      # 其他dns解析
      - matches: "!qname hdu.edu.cn dn11"
        exec: forward 223.5.5.5:53
  # 启动端口和ip
  - tag: udp_server_2
    type: udp_server
    args:
      entry: main_sequence
      listen: "172.16.3.13:53"

  - tag: tcp_server_2
    type: tcp_server
    args:
      entry: main_sequence
      listen: "172.16.3.13:53"

同样的 启动需要你br-lan网卡里加一个 172.16.3.13这个ip地址

service mosdns restart
DN11 使用 EBGP 重分发 IBGP OSPF(同as内部peer)

DN11 使用 EBGP 重分发 IBGP OSPF(同as内部peer)

DN11 使用 EBGP 重分发 IBGP OSPF(同as内部peer)

连接 wg peer

1.本地机器

# wg_aws.conf
[Interface]
# 你的私钥
PrivateKey = <PRIVATEKAY>
# 你开的端口号(注意防火墙)
ListenPort = <PORT>
PostUp = /sbin/ip addr add dev %i <your ip> peer <peer ip>
# 不创建路由(由路由协议接管)
Table = off

[Peer]
# 你对端的ip和端口(外网机器)
Endpoint = <ENDPOINT>:<PORT>
# 你对端的公钥(外网机器)
PublicKey = <PUBLICKEY> 
AllowedIPs = 0.0.0.0/0

2.外网机器

# wg_ts_home.conf
[Interface]
# 你的私钥
PrivateKey = <PRIVATEKAY>
# 你开的端口号(注意防火墙)
ListenPort = <PORT>
PostUp = /sbin/ip addr add dev %i <your ip> peer <peer ip>
# 不创建路由(由路由协议接管)
Table = off
# wg 默认的 MTU 是 1420 (这里主要排除外网机器 MTU 异常的问题,洞里打洞不要配这个)
MTU = 1420

[Peer]
# 你对端的ip和端口(本地机器)
Endpoint = <ENDPOINT>:<PORT>
# 你对端的公钥(本地机器)
PublicKey = <PUBLICKEY> 
AllowedIPs = 0.0.0.0/0

bird配置

创建

/etc/bird.conf
/etc/bird/ospf.conf
/etc/bird/ibgp.conf
/etc/bird/bgp.conf

这四个文件

内网 openwrt 和外网机器上几乎需要相同的这四个文件(唯一不同的是 bgp neighbor 那里)

文中的 172.16.3.254、172.16.3.91 改为你本机(你的本地机器或者外网机器)的隧道ip
文中的 4211112243 改为你的 asn (别整出俩 as 号就行)

/etc/bird.conf

log syslog all;
debug protocols all;

router id 172.16.3.254;

ipv4 table BGP_table;
ipv4 table OSPF_table;
ipv4 table IBGP_table;

protocol device{

}

protocol kernel{
    ipv4 {
        export all;
        import none;
    };
}

include "/etc/bird/ospf.conf";
include "/etc/bird/ibgp.conf";
include "/etc/bird/bgp.conf";

protocol pipe PIPE_OSPF_BGP{
    table BGP_table;
    peer table OSPF_table;
    import all;
    export none;
}

protocol pipe PIPE_BGP_MASTER4{
    table master4;
    peer table BGP_table;
    import filter {
        if source = RTS_STATIC then reject;
        krt_prefsrc = 172.16.3.254;
        accept;
    };
    export none;
}

protocol pipe PIPE_IBGP_BGP {
    table BGP_table;
    peer table IBGP_table;
    import filter {
        if source = RTS_OSPF then reject;
        accept;
    };
    export filter {
        if source = RTS_STATIC then reject;
        accept;
    };
}

# 宣告 172.16.3.0/24 段
protocol static {
    ipv4 {
        table BGP_table;
        import all;
        export none;
    };

    # 只是为了让BGP的路由表里出现这条路由,不要担心 reject
    # 这个动作其实无所谓,reject 这个动作并不会被发到其他 AS
    # 后续将在导出到 master4 的时候删除这条路由,本地也不会有这一条
    # 请修改为你自己要宣告的段
    route 172.16.3.0/24 reject;
}

/etc/bird/ibgp.conf

template bgp ibgp_peers {
    local as 4211112243;
    source address 172.16.3.254;
    multihop;
    ipv4 {
        next hop self;
        igp table OSPF_table;
        table IBGP_table;
        import all;
        export all;
    };
}

protocol bgp ibgp_aws from ibgp_peers {
    neighbor 172.16.3.91 internal;
}

bgp.conf

template bgp BGP_peers {
    local 172.16.3.254 as 4211112243;

    ipv4 {
        table BGP_table;
        import all;
        export filter {
            if source ~ [RTS_STATIC, RTS_BGP, RTS_OSPF] then accept;
            reject;
        };
    };
}

# collector 配置
protocol bgp collect_self {
    local as 4211112243;
    multihop;
    neighbor 172.16.255.1 as 4211110101;
    ipv4 {
        add paths tx;
        table BGP_table;
        import none;
        export filter {
            if source ~ [RTS_BGP,RTS_STATIC] then accept;
        };
    };
}

# 如下的 neighbor 配置应按照 wg 实际链接数量为准
# 从模板定义一个BGP邻居
# protocol bgp protocol 名称 from 模板名称
protocol bgp baimeow from BGP_peers {
   neighbor 172.16.4.254%baimeow as 4220084444;
}

# 如下为 如果你有物理链路的 BGP 配置 (没有的别抄)
# protocol bgp Meva_phy from BGP_peers {
#     source address 172.16.3.1;
#     neighbor 172.16.3.97%'br-lan' as 4211111024;
# 
#     ipv4 {
#     import filter {
#             if bgp_path ~ [= 4211111024 =] then bgp_local_pref = 120;
#             bgp_next_hop = 172.16.3.97;
#             accept;
#         };
#     };
# }

ospf.conf

protocol ospf v2 ospf_aws {
        ipv4 {
                table OSPF_table;
                export all;
                import all;
        };
        area 0.0.0.0 {
                interface "wg_aws" {
                        cost 20;
                        type ptp;
                };
                interface "lo" {
                        type bcast;
                };
        };
}
go-zero api文件生成项目框架 | 青训营笔记

go-zero api文件生成项目框架 | 青训营笔记

go-zero api文件生成项目框架

数据结构

.api文件是go-zero自创的文件格式,和protobuf的语法有很大不同,但好在不难理解。具体语法介绍可看官网文档:go-zero.dev/docs/tasks/…

先编写接口定义,然后直接一次性使用 goctl 生成代码。

api文件支持将一个数据结构嵌入另一个结构中,便于编写统一的响应格式。

首先定义空请求结构和基础的响应结构:

syntax = "v1"  
  
type Empty {  
}  
  
type BasicResponse {  
    StatusCode int32 `json:"status_code"`  
    StatusMsg string `json:"status_msg"`  
}

type关键字也支持代码块,只需要用type()包裹,就可以省去关键词。下面定义了注册和登录的请求和响应结构。

type (  
    RegisterRequest struct {  
        Username string `form:"username"`  
        Password string `form:"password"`  
    }  

    RegisterResponse struct {  
        BasicResponse  
        UserId int64 `json:"user_id"`   
    }  
)  
  
type (  
    LoginRequest struct {  
        Username string `form:"username"`  
        Password string `form:"password"`  
    }  

    LoginResponse struct {  
        BasicResponse  
        UserId int64 `json:"user_id"`  
    }  
)

最后还要定义一个获取用户信息的接口。因为本文不涉及创作、社交方面的业务逻辑实现,所以User结构其实没什么可以返回的信息。之后将其嵌入响应类型的user字段中:

type (  
    User {  
        Id int64 `json:"id"`  
        Name string `json:"name"`  
    }  
    
    GetUserInfoRequest struct {  
        UserId int64 `form:"user_id"`   
    }  

    GetUserInfoResponse struct {  
        BasicResponse  
        User User `json:"user"`  
    }  
)

定义接口路由

把所有api路由配置好

@server(  
    group: app  
)  
service app {  
    @handler Ping  
    get /ping (Empty) returns (BasicResponse)  
}  
  
@server(  
    group: user  
    prefix: /douyin/user  
)  
service app {  
    @handler Register  
    post /register (RegisterRequest) returns (RegisterResponse)  
  
    @handler Login  
    post /login (LoginRequest) returns (LoginResponse)  
  
    @handler GetUserInfo  
    get / (GetUserInfoRequest) returns (GetUserInfoResponse)  
}

以下几点需要注意:

  1. 如果service代码块有多个,要求后面的名称(即“app”)相同
  2. 形如装饰器的@server代码块可以做代码分组、路由前缀、鉴权、中间件等多种配置。
    • 这里把路由分成app和user两组,之后生成时会分组放在文件夹里
    • 给user组设置了/douyin/user的路由器前缀,就不用重复写了
    • 鉴权和中间件配置下文会提到
  3. 每个接口需要一个handler,其实就是给函数起名
  4. 接口定义的格式是 <HTTP方法> <子路径> (<请求数据结构>) returns (<响应数据结构>)

创建项目模板

这次不是新建示例项目,而是根据现有的api声明创建

安装 goctl

https://go-zero.dev/docs/tasks/installation/goctl

$ GO111MODULE=on go install github.com/zeromicro/go-zero/tools/goctl@latest

安装 protoc

https://go-zero.dev/docs/tasks/installation/protoc

$ goctl env check --install --verbose --force
goctl api go --api app.api --dir=. --style=goZero 

这会在当前目录生成项目文件。之后进入目录安装依赖:

go mod tidy

测试Ping接口

Ping接口的逻辑在internal/logic/app/pingLogic.go,打开文件并编辑handler函数:

func (l *PingLogic) Ping(req *types.Empty) (resp *types.BasicResponse, err error) {  
    return &types.BasicResponse{
        StatusCode: 0,
        StatusMsg:  "pong",
    }, nil
}

这里的写法和rpc微服务有点区别。注意到传入的resp是一个指针,所以需要手动创建一个响应结构体并设置键值。

这样就算完成Ping接口了,可以运行服务。

gorm 进阶 | 青训营笔记

gorm 进阶 | 青训营笔记

gorm

关于简单的 gorm 总结 可以看上一篇文章 gorm 初体验 | 青训营笔记

软删除

Gorm提供了软删除的能力,需要在结构体中定义一个Deleted字段,此时再调用Delete删除函数,则会生成update语句,并将deleted字段赋值为当前删除时间。

type Product struct {
    ID      uint
    Code    string
    Price   uint
    Deleted gorm.DeletedAt
}

再次执行删除操作:

go
db.Delete(&Product{}, 1)

生成的sql为:

UPDATE `product` SET `deleted`='2023-01-30 22:22:22.202' WHERE `product`.`id` = 1 AND `product`.`deleted` IS NULL

查询被软删除的操作,要使用Unscoped函数:

db.Unscoped().First(&product, 2)

有了DeleteAt字段后,删除操作已经变成了更新操作,那么想要物理删除怎么办?也是使用Unscoped函数:

db.Unscoped().Delete(&Product{}, 1)

Gorm事务

Gorm提供了BeginCommitRollback方法用于使用事务。

// 开启事务
tx := db.Begin()
if err := tx.Create(&Product{Code: "D32", Price: 100}).Error; err != nil {
    // 出现错误回滚
    tx.Rollback()
    return
}
if err := tx.Create(&Product{Code: "D33", Price: 100}).Error; err != nil {
    // 出现错误回滚
    tx.Rollback()
    return
}
// 提交事务
tx.Commit()

在开启事务后,调用增删改操作是应该使用开启事务返回的tx而不是db

Gorm还提供了Transaction函数用于自定提交事务,避免用户漏写CommitRollback

if err = db.Transaction(func(tx *gorm.DB) error {
    if err := db.Create(&Product{Code: "D55", Price: 100}).Error; err != nil {
        return err
    }
    if err := db.Create(&Product{Code: "D56", Price: 100}).Error; err != nil {
        return err
    }
    return nil
}); err != nil {
    return
}

这种写法,当出现错误时会自动进行Rollback,当正常执行时会自动Commit

Hook

当我们想在执行增删改查操作前后做一些额外的操作时,可以使用Gorm提供的Hook能力。

Hook是在创建、查询、更新、删除等操作之前、之后自动调用的函数,如果任何Hook返回错误,Gorm将停止后续的操作并回滚事务。

Hook会开启默认事务,所以会带来了一些性能损失。

性能提高

对于写操作(创建、更新、删除),为了确保数据的完整性,Gorm会将他们封装在事务内运行,但是这样会降低性能,可以使用SkipDefaultTransaction关闭默认事务。

使用PrepareStmt缓存预编译语句可以提高后续调用的速度。

db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{
  SkipDefaultTransaction: true,
  PrepareStmt: true,
})

Gorm生态

Gorm拥有非常丰富的扩展生态,下面列举一些常用的扩展。

GORM 配置

GORM 提供的配置可以在初始化时使用

type Config struct {
  SkipDefaultTransaction   bool
  NamingStrategy           schema.Namer
  Logger                   logger.Interface
  NowFunc                  func() time.Time
  DryRun                   bool
  PrepareStmt              bool
  DisableNestedTransaction bool
  AllowGlobalUpdate        bool
  DisableAutomaticPing     bool
  DisableForeignKeyConstraintWhenMigrating bool
}

跳过默认事务

为了确保数据一致性,GORM 会在事务里执行写入操作(创建、更新、删除)。如果没有这方面的要求,您可以在初始化时禁用它。

db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{
  SkipDefaultTransaction: true,
})

命名策略

GORM 允许用户通过覆盖默认的NamingStrategy来更改命名约定,这需要实现接口 Namer

type Namer interface {
    TableName(table string) string
    SchemaName(table string) string
    ColumnName(table, column string) string
    JoinTableName(table string) string
    RelationshipFKName(Relationship) string
    CheckerName(table, column string) string
    IndexName(table, column string) string
}

默认 NamingStrategy 也提供了几个选项,如:

db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{
  NamingStrategy: schema.NamingStrategy{
    TablePrefix: "t_",   // table name prefix, table for `User` would be `t_users`
    SingularTable: true, // use singular table name, table for `User` would be `user` with this option enabled
    NoLowerCase: true, // skip the snake_casing of names
    NameReplacer: strings.NewReplacer("CID", "Cid"), // use name replacer to change struct/field name before convert it to db name
  },
})

NowFunc

更改创建时间使用的函数

db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{
  NowFunc: func() time.Time {
    return time.Now().Local()
  },
})

DryRun

生成 SQL 但不执行,可以用于准备或测试生成的 SQL

db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{
  DryRun: false,
})

PrepareStmt

PreparedStmt 在执行任何 SQL 时都会创建一个 prepared statement 并将其缓存,以提高后续的效率

db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{
  PrepareStmt: false,
})

禁用嵌套事务

在一个事务中使用 Transaction 方法,GORM 会使用 SavePoint(savedPointName)RollbackTo(savedPointName) 为你提供嵌套事务支持,你可以通过 DisableNestedTransaction 选项关闭它

DisableAutomaticPing

在完成初始化后,GORM 会自动 ping 数据库以检查数据库的可用性,若要禁用该特性,可将其设置为 true

db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{
  DisableAutomaticPing: true,
})

DisableForeignKeyConstraintWhenMigrating

AutoMigrateCreateTable 时,GORM 会自动创建外键约束,若要禁用该特性,可将其设置为 true

db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{
  DisableForeignKeyConstraintWhenMigrating: true,
})
go字节三件套 | 青训营笔记

go字节三件套 | 青训营笔记

Gorm、Kitex、Hertz:三件套介绍与基本用法

Gorm

Gorm是Golang中广受欢迎的ORM(对象关系映射)框架,已经发展数十年,具有强大的功能和出色的性能。

ORM框架用于将面向对象的概念与数据库中的表相对应,简化数据操作过程。在Golang中,自定义的结构体与数据库表一一对应,结构体的实例对应表中的一条记录。

基本用法

定义结构体:在Gorm中,定义结构体来映射数据库表。

type User struct {
  ID    uint   `gorm:"primary_key"`
  Name  string `gorm:"type:varchar(100)"`
  Email string `gorm:"type:varchar(100);uniqueIndex"`
}

增加数据:使用Create方法来新增数据条目。

db.Create(&User{Name: "John", Email: "john@example.com"})

查询数据:使用Find进行数据查找,可以通过Where和Or条件构建查询条件。

var user User
db.Where("name = ?", "John").First(&user)

更新数据:使用Update进行数据更新,可以使用Model结合Update或Updates方法来更新列。

db.Model(&user).Update("Name", "John Updated")

删除数据:使用Delete进行数据删除操作,根据是否包含gorm.deletedat字段执行物理或逻辑删除。

db.Delete(&user)

Kitex

https://www.cloudwego.io/zh/docs/kitex/overview/

Kitex是字节开发的高性能Golang微服务RPC框架,具备出色的可扩展性和高性能。
服务端

func main() {
  srv := kitex.NewServer(new(YourServiceImpl), 
                          kitex.WithServiceAddr(":8888"))
  srv.Run()
}

客户端

func main() {
  client := NewYourServiceClient("127.0.0.1:8888", kitex.WithTransportProtocol(protocol.TRPC))
  // 调用RPC方法
}

高性能:Kitex具有优异的性能表现,适用于高负载的微服务场景。
可扩展:支持多协议,并且有丰富的开源扩展库,可以满足各种需求。

Hertz

https://www.cloudwego.io/zh/docs/hertz/overview/

Hertz是字节开发的HTTP框架,结合了其他开源框架的优点,同时满足字节跳动内部的需求,具有高可用性、高性能和高扩展性。

package main

import (
    "context"

    "github.com/cloudwego/hertz/pkg/app"
    "github.com/cloudwego/hertz/pkg/app/server"
    "github.com/cloudwego/hertz/pkg/common/utils"
    "github.com/cloudwego/hertz/pkg/protocol/consts"
)

func main() {
    h := server.Default()

    h.GET("/ping", func(c context.Context, ctx *app.RequestContext) {
            ctx.JSON(consts.StatusOK, utils.H{"message": "pong"})
    })

    h.Spin()
}

高可用性:Hertz框架被设计为具有高度的稳定性和可用性,适用于大规模的应用场景。
高性能:框架在性能方面表现出色,适合处理高并发请求。
高扩展性:Hertz框架允许根据业务需求进行定制和扩展,以满足复杂应用的要求。

网站安全漏洞 | 青训营笔记

网站安全漏洞 | 青训营笔记

什么是漏洞

漏洞是指在网站或网络应用中存在的安全弱点,它们可能由于各种原因而产生,包括编码错误、配置不当或第三方组件的安全性不足。了解一个网站的基本构成是理解漏洞的第一步。网站通常由以下几个关键组成部分构成,而攻击者通常会尝试从这些部分找出弱点进行攻击:

  • 前端:使用的技术可能包括HTML、CSS、JavaScript、Vue.js、React.js等。
  • 网关:例如nginx或Apache,负责处理进出的网络流量。
  • 后端:可能是用Go、Java、Node.js、Python等语言编写的。
  • 前后端交互:通常通过HTTP或WebSocket进行。

常见的安全事件

由安全漏洞引发的攻击事件不仅可能导致数据泄露、服务瘫痪、成果失窃和系统劫持,还可能带来以下更为严重的后果:

  • 法律责任:泄露用户数据可能导致法律诉讼和巨额罚款。
  • 品牌声誉受损:一次严重的安全事件可能永久性地影响公司的声誉。
  • 业务中断:服务中断可能导致用户流失和收入减少。
  • 内外部信任度下降:员工和合作伙伴也可能因为安全事件而对企业失去信任。

网站攻击者的意图

确切地知道攻击者的意图可以帮助企业更有效地防御威胁。除了政治、经济和竞争等原因外,还有以下可能的攻击动机:

  • 社会工程攻击:通过人为操作或欺骗,使内部人员泄露敏感信息。
  • 黑帽SEO:通过各种手段来操纵搜索引擎排名。
  • 资源占用:攻击者可能只是为了消耗目标网站的资源。
  • 纯粹的破坏欲:有些攻击者可能只是出于破坏的乐趣。

红蓝对抗

红蓝对抗是一种模拟现实世界网络攻防场景的方法,通常用于评估和提升组织的安全防护能力。在这一模式中,红军(Red Team)模拟攻击者,寻找系统的弱点和漏洞;蓝军(Blue Team)则扮演防御方,负责检测和阻止这些攻击。除此之外,还有所谓的“紫军”(Purple Team),负责协调红蓝两方,以确保双方都从对抗中获得最大的学习价值。

攻击方式

网站攻击通常可以分为两大类:针对客户端的攻击和针对服务端的攻击。

  • 针对客户端的攻击:这类攻击直接影响网站的终端用户,例如通过XSS(跨站脚本)或CSRF(跨站请求伪造)等方式。
  • 针对服务端的攻击:这类攻击目标是网站后端的服务器或数据库,例如通过SQL注入或命令执行等方式。 每一种攻击都有其自己的防护机制和修补方案,因此对这些攻击手段有深入的了解是非常重要的。

服务端漏洞

服务端漏洞通常比客户端漏洞更为严重,因为它们直接影响到整个系统的安全性。其中,第三方组件漏洞是一个常见但经常被忽视的问题。例如,一些流行的开源库或框架可能存在已知的安全问题,如果开发人员没有及时更新这些组件,就可能导致整个系统的安全性受到威胁。

SQL注入漏洞

SQL注入是一种常见但危险的攻击手段,它出现在应用程序未能正确地处理用户输入的场合。更准确地说,攻击者通过输入特定的SQL代码片段,使得原SQL查询逻辑被改变,从而达到非法获取数据或执行特定操作的目的。防范SQL注入的基础是编码规范,包括但不限于使用预编译SQL语句、参数化查询,以及进行严格的输入验证。

命令执行

命令执行漏洞通常发生在应用需要与操作系统进行交互时。与SQL注入类似,这一漏洞也常常是由于不正确的输入处理引起的。攻击者可能会尝试插入恶意命令或参数,以此来控制或破坏目标系统。防护措施包括使用安全的API进行命令调用、对用户输入进行严格的验证和过滤,以及使用最小权限原则来限制应用程序对系统资源的访问。

越权漏洞

越权漏洞是授权控制不当导致的一类安全问题,它们通常分为三类:

  • 未授权:用户无需任何授权即可访问受限制的资源。
  • 水平越权:用户访问同一权限级别但不属于自己的资源。
  • 垂直越权:低权限用户访问高权限用户的资源。 越权问题的根本在于不恰当的权限控制和会话管理,因此相应的防护措施应从这两方面入手。

SSRF攻击

服务端请求伪造(SSRF)是一种利用目标服务器作为中间人来发起请求的攻击。这样做可能会暴露原本不可达的内部网络资源。SSRF通常利用的是应用程序或服务器配置中的不当输入验证或URL解析缺陷。防范措施包括限制从服务器发出的请求类型和目标,以及对所有外部输入进行严格的验证。

文件上传漏洞

文件上传漏洞是一种常见的安全风险,尤其是在允许用户上传文件的应用中。如果没有适当的安全措施,攻击者可能会上传含有恶意代码的文件,这些文件一旦被执行,可能会导致严重的安全后果。因此,除了基本的文件类型检测和大小限制外,更高级的防护措施,如内容扫描和安全存储策略,也是非常必要的。

客户端漏洞

客户端漏洞主要影响网站的最终用户,而不是服务器或数据库。这些漏洞通常涉及到如何处理从客户端(通常是Web浏览器)发来的数据和请求。例如,开放重定向是一种常见的客户端漏洞,攻击者通过这种漏洞可以将用户重定向到恶意网站。修复这类问题通常需要在客户端和服务器端都进行适当的输入验证。

跨站脚本(XSS)攻击

跨站脚本(XSS)是一种在目标网站上执行恶意脚本的攻击方式。它利用的是网站没有对用户提交的数据进行适当的过滤和转义。根据攻击的具体形式,XSS攻击可以进一步分为存储型、反射型和DOM型三类。每种类型的XSS攻击都有其独特的防御方法,如输入过滤、输出编码和内容安全策略(CSP)。

跨站请求伪造(CSRF)

跨站请求伪造(CSRF)是一种攻击手法,攻击者通过某种方式诱导用户点击一个链接或按钮,从而在用户不知情的情况下,以该用户的身份执行非预期的操作。防御CSRF攻击的常见方法包括使用CSRF令牌、设置SameSite属性等。

总结

在这篇文章中,我们列举了网站安全漏洞的多个方面,从基础概念到各种类型的安全漏洞,以及如何防范这些漏洞。重要的是,无论是服务端还是客户端,安全漏洞都可能存在,并且都需要得到妥善的处理。

网站安全是一个涉及多个层面的复杂问题,需要开发者、运维人员、以及安全团队共同努力来维护。只有全方位、多层次的安全防护措施,才能有效地减少安全风险,保护网站和用户数据的安全。

go架构 | 青训营笔记

go架构 | 青训营笔记

什么是架构

主要针对互联网服server系统(类似网站)来定义架构:架构是系统的骨架,支撑和链接各个部分,包括组件、连接件、约束规范,以及指导这些内容设计与演化的原理。

  • 组件:类似应用服务,独立模块、数据库、nginx等等、

  • 连接件:分布式调用、进程间调用、调用使用http协议还是tcp协议、组件之间的交互关系、

  • 约束规范: 定规则做限制:例如设计原则、编码规范等等。

组件

组件是架构的构建块,包括独立模块、数据库、Nginx等。

例如,Go语言中可以定义一个组件,如一个HTTP服务器:

package main

import (
    "net/http"
)

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("Hello, World!"))
    })
    http.ListenAndServe(":8080", nil)
}

连接件

连接件处理组件之间的交互关系,例如分布式调用、进程间调用、使用的协议等。

约束规范

约束规范定义了架构中的规则和限制,例如设计原则和编码规范。

几种典型架构

单机架构

单机架构就是把所有功能都实现在一个进程中,并部署在一台机器上。

单机架构的优点是非常简单,缺点是会遇到并发处理的难题,而且运维需要停服。

当时计算机软硬件性能比较基础,这个问题引起人们对网络套接字优化和服务端调度的关注,这个词语也随着时代的发展逐渐变化,如C10M代表的是处理1000万个并发连接。现在的计算机已经有足够的性能和优秀的软件来支撑单机数百万连接了,然而并发问题仍然是后端架构设计中不能略过的关键问题。

单体架构

单体架构是对单机架构的改进。既然单机处理能力有限,就可以采用分布式部署的模式设置多台单机,再设置一个分流环节,将请求转发给各个单体服务器来处理。

单体架构的优点是可以实现水平扩容,处理能力不够时只需要简单添加新的单机即可;运维不需要停服,可以逐个对单机维护不影响其他单机的运行。单体架构下,每台机器的功能是一致的,因此它没有解决这两个缺点:

  1. 职责太多,开发效率低:每台机器都要实现系统的完整功能,不能专注于某个功能的开发。
  2. 爆炸半径大: 爆炸半径是指爆炸可能造成损害的区域范围。在生产环境中指的就是部署的服务故障时可能影响的软件功能范围。单体架构下每一个单机下线,都会造成所有功能的处理承载量下降,因此爆炸半径覆盖了软件多数功能。

垂直应用架构

为了解决单体架构的问题,垂直应用架构提供了一种思路,将系统按照服务拆分任务,分配给不同的服务器,从而实现职责的简单划分,一定程度上提高开发和运维的效率。不过在实践中一个服务往往是大量功能点的集合,仍然存在功能琐碎的问题

更加高级的架构

SOA、微服务架构

SOA(Service-Oriented Architecture)架构具有两个特性:

  1. 将应用的不同功能单元抽象为服务,从而细分职责
  2. 定义服务之间的通信标准,确保服务之间的数据流通

微服务架构则是SOA架构去中心化的演进方向,旨在减少服务之间的沟通消耗,避免SOA服务内部集中沟通导致的过度中心化问题,他实现了水平切分,减少了跨级别调用链,让每个服务直接负责的上下级减少。

这类架构在解决之前的弊端时,也会引入新的问题:

  • 数据一致性问题:不同服务如何确保数据同步
  • 高可用问题:服务之间如何进行可靠合作
  • 治理问题:一个服务出现问题时,要如何容灾
  • 解耦和过微问题:过度细分会导致运维成本提高,要如何权衡?

事件驱动架构

事件驱动架构(EDA)是一种异步架构,其中生产者生成事件,并由一个或多个消费者消费。这允许解耦生产者和消费者,并实现高度可扩展和灵活的系统。

Apache Kafka
Apache Kafka是一种流行的事件流平台。您可以使用Go编写Kafka的生产者和消费者。

如下是示例代码

package main

import (
    "github.com/confluentinc/confluent-kafka-go/kafka"
)

func main() {
    producer, err := kafka.NewProducer(&kafka.ConfigMap{"bootstrap.servers": "localhost"})
    if err != nil {
        panic(err)
    }

    // 生产消息
    producer.Produce(&kafka.Message{
        TopicPartition: kafka.TopicPartition{Topic: &topic, Partition: kafka.PartitionAny},
        Value:          []byte("Message Value"),
    }, nil)

    // 消费消息
    consumer, err := kafka.NewConsumer(&kafka.ConfigMap{
        "bootstrap.servers": "localhost",
        "group.id":          "myGroup",
        "auto.offset.reset": "earliest",
    })
    consumer.SubscribeTopics([]string{"myTopic"}, nil)
    msg, err := consumer.ReadMessage(-1)
    if err == nil {
        fmt.Printf("Message on %s: %s\n", msg.TopicPartition, string(msg.Value))
    }
}

云计算

云计算是指通过软件自动化管理,提供计算资源的服务网络,是现在互联网大规模数据分析和存储的基石。

云计算的基础是虚拟化技术和编排方式,关系到资源的分配和调度。

云计算提供一系列特有的架构:

  • 基础设施即服务IaaS(Infrastructure as a Service):服务商提供互联网基础设施,比如用户只需要获得服务器、云硬盘、宽带网络,而不用关心物理的机房、数据中心、网络怎么建设。但是,用户需要自己进行服务器系统安装、环境部署和软件配置。
  • 平台即服务PaaS(Platform as a Service):服务商提供底层软件设施,比如为用户准备安装了指定操作系统、数据库软件、环境套件的主机。用户需要自己控制上层应用的部署和托管。
  • 软件即服务Saas(Software as a Service):服务商及提供基于软件的解决方案来满足客户的需求,比如用户想要一套OA办公系统,CMS内容管理系统,HRM人事管理系统等,而不用自行搭建和维护这样的服务,能够简化用户的工作。
  • 函数即服务FaaS(Function as a service):服务商提供一个平台,允许客户开发、运行和管理应用程序功能,而不用关心如何启动和部署这样的程序。这个模式是“无服务器”体系架构的方式,具有灵活性,适合构建微服务应用程序。

云原生架构

云原生技术为公司在公有云、私有云、混合云等新型动态环境中,构建和运行可弹性拓展的应用提供了可能。

云原生为公司提供了以下关键能力:

弹性资源调度

  • 服务资源调度:分配微服务、大服务使用的资源。
  • 计算资源调度:分配在线计算、离线计算使用的资源
  • 消息队列:和计算资源调度类似,提供海量的数据吞吐和大数据分析能力。

弹性存储资源

云原生让用户可以将存储资源看作服务,寻找对应的服务来满足自己的存储需求。

  • 经典存储:对象存储、大数据记录存储。
  • 关系型数据库:业务记录存储。
  • 元数据:提供服务发现能力
  • NoSQL:key-value存储,实现缓存和分布式事务。

DevOps

DevOps贯穿整个软件开发周期,使用一系列自动化流程,提高软件开发、交付的效率。DevOps提供了敏捷开发、持续集成/交付(CI/CD)的能力。

微服务架构

微服务架构实现业务功能单元的解耦,同时提供一套统一的通信标准确保服务之间的数据流通。

微服务常见的通信标准是HTTP(RESTful API)和RPC(Thrift, gRPC),需要结合性能、服务治理、协议可解释性来选择。实际场景下,常常使用微服务框架提供的通信能力,而不用自己从头实现一套。

服务网格

服务网格实现业务与治理的结构以及异构系统治理的统一化,从而提供复杂的治理能力。

服务发现

服务注册与发现 Eureka或Consul等服务可以用于服务发现。

服务注册与发现使服务可以动态地找到其他服务的位置。

// 使用Consul进行服务注册
consulConfig := consulapi.DefaultConfig()
consulConfig.Address = "localhost:8500"
client, err := consulapi.NewClient(consulConfig)
if err != nil {
    log.Fatal(err)
}

// 创建新的服务
registration := new(consulapi.AgentServiceRegistration)
registration.ID = "my-service-id"
registration.Name = "my-service"
registration.Port = 8080
registration.Tags = []string{"my-service"}

// 注册服务
agent := client.Agent()
if err := agent.ServiceRegister(registration); err != nil {
    log.Fatal(err)
}

// 服务发现逻辑...

分布式架构

分布式锁

在分布式环境中,协调多个节点的访问资源可能会变得复杂。分布式锁是一种解决方案,可以在多个节点之间同步访问。

可以使用Go语言和Redis创建分布式锁:


package main

import (
    "fmt"
    "github.com/go-redis/redis/v8"
    "golang.org/x/net/context"
)

func main() {
    rdb := redis.NewClient(&redis.Options{
        Addr: "localhost:6379",
    })

    ctx := context.Background()
    lockKey := "lock_key"

    // 尝试获取锁
    locked, err := rdb.SetNX(ctx, lockKey, "lock_value", 0).Result()
    if err != nil || !locked {
        fmt.Println("获取锁失败")
        return
    }

    // 执行业务逻辑...

    // 释放锁
    rdb.Del(ctx, lockKey)
    fmt.Println("业务逻辑执行完毕,锁已释放")
}

优化方式

离在线资源并池

在线业务以IO密集型为主,要求计算的实时性,但是具有潮汐性,不同时间段的压力不同。离线业务以计算密集型为主,对计算实时性不高。

把在线和离线资源放入同一个资源池进行调度,利用在线业务的潮汐性自动扩缩容,能够有效降低物理资源成本,提供更多的弹性资源,增加收益。

负载均衡

简单的http负载均衡 轮询算法

package main

import (
    "net/http"
    "net/http/httputil"
    "net/url"
)

var servers = []string{
    "http://localhost:8081",
    "http://localhost:8082",
}

var currentServer = 0

func nextServer() string {
    server := servers[currentServer]
    currentServer = (currentServer + 1) % len(servers)
    return server
}

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        server := nextServer()
        target, _ := url.Parse(server)
        proxy := httputil.NewSingleHostReverseProxy(target)
        proxy.ServeHTTP(w, r)
    })
    http.ListenAndServe(":8080", nil)
}

亲和性部署

如果两个微服务之间通信密切,则这两个服务的亲和性较高。将满足亲和性条件的容器调度到一台宿主机,让微服务中间件与服务网格通过共享内存通信,能够有效降低通信成本,提高服务可用性。

流量治理

基于微服务中间件和服务网格的流量治理,赋予微服务熔断、重试的能力,提供复杂环境下的流量调度,可以提高微服务调用容错性,增强容灾能力,还能进一步提高开发效率。

CPU水位负载均衡

结合IaaS提供的资源探针和服务网格的负载均衡能力,能够为自动扩缩容提供反馈,打平异构环境下算力的差异。

容器化与服务编排

Docker

可以使用Docker将Go应用程序打包到容器中,并确保它在任何支持Docker的平台上以相同的方式运行。

Kubernetes

Kubernetes是一种强大的容器编排工具,用于自动部署、扩展和管理容器化应用程序。Go语言是Kubernetes的主要实现语言。

总结

现代软件架构的复杂性和灵活性要求了解许多概念和技术。从单机到分布式,从单体到微服务,架构演化为满足不断增长的需求。

通过使用强大和灵活的编程语言,如Go,您可以构建符合这些需求的解决方案。从分布式锁到服务发现,从负载均衡到微服务通信,Go为构建现代、可扩展和高性能的系统提供了强大的工具和库。

不断学习和实践这些概念和技术将使您更好地设计和构建复杂的系统,满足业务目标,并应对未来的挑战。

gorm 初体验 | 青训营笔记

gorm 初体验 | 青训营笔记

gorm

gorm是Golang语言中一款性能极好的ORM库,对开发人员相对是比较友好的。接下来主要学习下gorm库的一些基本使用。

功能概览

  • 全功能 ORM
  • 关联 (Has One,Has Many,Belongs To,Many To Many,多态,单表继承)
  • Create,Save,Update,Delete,Find 中钩子方法
  • 支持 Preload、Joins 的预加载
  • 事务,嵌套事务,Save Point,Rollback To Saved Point
  • Context、预编译模式、DryRun 模式
  • 批量插入,FindInBatches,Find/Create with Map,使用 SQL 表达式、Context Valuer 进行 CRUD
  • SQL 构建器,Upsert,数据库锁,Optimizer/Index/Comment Hint,命名参数,子查询
    复合主键,索引,约束
  • Auto Migration
  • 自定义 Logger
  • 灵活的可扩展插件 API:Database Resolver(多数据库,读写分离)、Prometheus…
  • 每个特性都经过了测试的重重考验
  • 开发者友好

安装

安装

go get -u gorm.io/gorm
go get -u gorm.io/driver/sqlite

模型定义

基本定义

因为orm可以自动创建数据库,创建表,所以我们要用一个约定的形式来描述一个schema,便于他创建表,也便于代码中调用

例如

type User struct {
  Name         string
  Email        string
  Age          uint8
}

这样的一个结构体就可以描述user这个表

gorm.Model

// gorm.Model 的定义
type Model struct {
  ID        uint           `gorm:"primaryKey"`
  CreatedAt time.Time
  UpdatedAt time.Time
  DeletedAt gorm.DeletedAt `gorm:"index"`
}

这个约定了一些gorm的标准配置,相当于implement了这个结构体

例如

type User struct {
  gorm.Model
  Name         string
  Email        string
  Age          uint8
}

这样这个gorm.Model相当于在user表里添加了一些基础的字段

这个user最终会变成 相当于

type User struct {
  ID        uint           `gorm:"primaryKey"`
  CreatedAt time.Time
  UpdatedAt time.Time
  DeletedAt gorm.DeletedAt `gorm:"index"`
  Name         string
  Email        string
  Age          uint8
}

嵌入结构体

如上的gorm.Model就是一种嵌入结构体

官网的解释是:对于匿名字段,GORM 会将其字段包含在父结构体中

其他嵌入方法为:

对于正常的结构体字段,你也可以通过标签 embedded 将其嵌入,例如:

type Author struct {
    Name  string
    Email string
}

type Blog struct {
  ID      int
  Author  Author `gorm:"embedded"`
  Upvotes int32
}
// 它等效于
type Blog struct {
  ID    int64
  Name  string
  Email string
  Upvotes  int32
}

可以使用标签 embeddedPrefix 来为 db 中的字段名添加前缀
Author Author `gorm:”embedded;embeddedPrefix:author_”

字段标签

声明 model 时,tag 是可选的,gorm 支持以下这些 tag: tag 名大小写不敏感,但我们写的时候最好用驼峰命名法。

写法为 在字段后面添加飘号和gorm:

例如

ID        uint           `gorm:"primaryKey"`
标签名 说明
column 指定 database 的列名
type 列数据类型,推荐使用兼容性好的通用类型,例如:所有数据库都支持 bool、int、uint、float、string、time、bytes 并且可以和其他标签一起使用,例如:not nullsize, autoIncrement… 像 varbinary(8) 这样指定数据库数据类型也是支持的。在使用指定数据库数据类型时,它需要是完整的数据库数据类型,如:MEDIUMINT UNSIGNED not NULL AUTO_INCREMENT
serializer 指定将数据序列化或反序列化到数据库中的序列化器, 例如: serializer:json/gob/unixtime
size 定义列数据类型的大小或长度,例如 size: 256
primaryKey 将列定义为主键
unique 将列定义为唯一键
default 定义列的默认值
precision 指定列的精度
scale 指定列大小
not null 指定列为 NOT NULL
autoIncrement 指定列为自动增长
autoIncrementIncrement 自动步长,控制连续记录之间的间隔
embedded 嵌套字段
embeddedPrefix 嵌入字段的列名前缀
autoCreateTime 创建时追踪当前时间,对于 int 字段,它会追踪时间戳秒数,您可以使用 nano/milli 来追踪纳秒、毫秒时间戳,例如:autoCreateTime:nano
autoUpdateTime 创建/更新时追踪当前时间,对于 int 字段,它会追踪时间戳秒数,您可以使用 nano/milli 来追踪纳秒、毫秒时间戳,例如:autoUpdateTime:milli
index 根据参数创建索引,多个字段使用相同的名称则创建复合索引,查看 索引 获取详情
uniqueIndex index 相同,但创建的是唯一索引
check 创建检查约束,例如 check:age > 13,查看 约束 获取详情
<- 设置字段写入的权限, <-:create 只创建、<-:update 只更新、<-:false 无写入权限、<- 创建和更新权限
-> 设置字段读的权限,->:false 无读权限
- 忽略该字段,- 表示无读写,-:migration 表示无迁移权限,-:all 表示无读写迁移权限
comment 迁移时为字段添加注释

连接数据库

官方提供的代码

import (
  "gorm.io/driver/mysql"
  "gorm.io/gorm"
)

func main() {
  // 参考 https://github.com/go-sql-driver/mysql#dsn-data-source-name 获取详情
  dsn := "user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
  db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
}

这里我提供一种封装方案,因为一般数据库配置写在配置文件里,防止泄露与主代码分离‘


import (
    "fmt"
    "gorm.io/driver/mysql"
    "gorm.io/gorm"
    "gorm.io/gorm/schema"
)

type Config struct {
    // 数据库配置
    DBList DBListConf
}

type DBListConf struct {
    Mysql MysqlConf
}

type MysqlConf struct {
    Address     string
    Username    string
    Password    string
    DBName      string
    TablePrefix string
}

type ServiceContext struct {
    Config Config
    DBList *DBList
}

type DBList struct {
    Mysql *gorm.DB
}

func NewServiceContext(c Config) *ServiceContext {
    return &ServiceContext{
        Config: c,
        DBList: initDB(c),
    }
}

func initDB(c Config) *DBList {
    dbList := new(DBList)
    dbList.Mysql = initMysql(c)

    return dbList
}

func initMysql(c Config) *gorm.DB {
    dsn := fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
        c.DBList.Mysql.Username,
        c.DBList.Mysql.Password,
        c.DBList.Mysql.Address,
        c.DBList.Mysql.DBName,
    )

    db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
        NamingStrategy: schema.NamingStrategy{
            TablePrefix:   c.DBList.Mysql.TablePrefix, // 表名前缀
            SingularTable: true,                       // 使用单数表名
        },
        DisableForeignKeyConstraintWhenMigrating: true,
    })
    if err != nil {
        panic(err)
    }

    // 自动建表
    err = db.AutoMigrate(&model.Video{}, &model.Favorite{}, &model.Comment{})
    if err != nil {
        panic(err)
    }

    return db
}

func main() {
    // 创建配置实例
    config := Config{
        DBList: DBListConf{
            Mysql: MysqlConf{
                Address:     "Address",
                Username:    "root",
                Password:    "1111111111111",
                DBName:      "dbname",
                TablePrefix: "Prefix",
            },
        },
    }

    // 创建服务上下文
    serviceContext := NewServiceContext(config)
    
    // 在这里你可以使用 serviceContext 进行操作
    // ...

    fmt.Println("Database initialization and configuration completed.")
}

这里出现了自动建表的操作AutoMigrate 你需要把你想要创建的模型传进来自动建表

CRUD

1. C

接下来开始 crud,

假如你有一个user表,可以使用create创建多个数据

users := []*User{
    User{Name: "Jinzhu", Age: 18, Birthday: time.Now()},
    User{Name: "Jackson", Age: 19, Birthday: time.Now()},
}

result := db.Create(users) // pass a slice to insert multiple row

result.Error        // returns error
result.RowsAffected // returns inserted records count

Error相信你会接

if result.Error != nil {
    return exit
}

或者可以直接在create里

err := db.create().Error
if err != nil {
    return exit
}

2.R

根据主键检索


db.First(&user, 10)
// SELECT * FROM users WHERE id = 10;

db.First(&user, "10")
// SELECT * FROM users WHERE id = 10;

db.Find(&users, []int{1,2,3})
// SELECT * FROM users WHERE id IN (1,2,3);

这里的first为检索第一个 limit为1

检索全部对象

// Get all records
result := db.Find(&users)
// SELECT * FROM users;

result.RowsAffected // returns found records count, equals `len(users)`
result.Error        // returns error

条件检索

这样相当于拼sql语句

// Get first matched record
db.Where("name = ?", "jinzhu").First(&user)
// SELECT * FROM users WHERE name = 'jinzhu' ORDER BY id LIMIT 1;

// Get all matched records
db.Where("name <> ?", "jinzhu").Find(&users)
// SELECT * FROM users WHERE name <> 'jinzhu';

// IN
db.Where("name IN ?", []string{"jinzhu", "jinzhu 2"}).Find(&users)
// SELECT * FROM users WHERE name IN ('jinzhu','jinzhu 2');

// LIKE
db.Where("name LIKE ?", "%jin%").Find(&users)
// SELECT * FROM users WHERE name LIKE '%jin%';

// AND
db.Where("name = ? AND age >= ?", "jinzhu", "22").Find(&users)
// SELECT * FROM users WHERE name = 'jinzhu' AND age >= 22;

// Time
db.Where("updated_at > ?", lastWeek).Find(&users)
// SELECT * FROM users WHERE updated_at > '2000-01-01 00:00:00';

// BETWEEN
db.Where("created_at BETWEEN ? AND ?", lastWeek, today).Find(&users)
// SELECT * FROM users WHERE created_at BETWEEN '2000-01-01 00:00:00' AND '2000-01-08 00:00:00';

高级查询 https://gorm.io/zh_CN/docs/advanced_query.html

3.U

更新 所有字段

db.Save(&User{Name: "jinzhu", Age: 100})
// INSERT INTO `users` (`name`,`age`,`birthday`,`update_at`) VALUES ("jinzhu",100,"0000-00-00 00:00:00","0000-00-00 00:00:00")

db.Save(&User{ID: 1, Name: "jinzhu", Age: 100})
// UPDATE `users` SET `name`="jinzhu",`age`=100,`birthday`="0000-00-00 00:00:00",`update_at`="0000-00-00 00:00:00" WHERE `id` = 1

更多更新方法 https://gorm.io/zh_CN/docs/update.html

4.D

删除一条记录

// Email 的 ID 是 `10`
db.Delete(&email)
// DELETE from emails where id = 10;

// 带额外条件的删除
db.Where("name = ?", "jinzhu").Delete(&email)
// DELETE from emails where id = 10 AND name = "jinzhu";

更多删除方法 https://gorm.io/zh_CN/docs/delete.html

其中的软删除感觉很有用

当调用Delete时,GORM并不会从数据库中删除该记录,而是将该记录的DeleteAt设置为当前时间,而后的一般查询方法将无法查找到此条记录。

总结

gorm是个很好用的go的orm框架,对很多数据库都兼容

性能也很好,操作起来也很简单。

go语言三个小项目 | 青训营笔记

go语言三个小项目 | 青训营笔记

猜数字

  1. 猜数字
    这个项目非常简单,它涉及到随机数的生成和用户输入操作。
    我们使用了bufio库来处理输入数据。
reader := bufio.NewReader(os.Stdin)
input, _ := reader.ReadString('\n')

简单字典

  1. 标准库strconv
    它主要用于字符和其他类型之间的转换。
strconv.Atoi(s string) int 
  1. 标准库strings
strings.TrimSuffix(s string ,  suffix string) string

删除末尾字符,如果没有就正常返回

  1. 网络库
client := &http.Client{}

初始化请求客户端

req,  err := http.NewRequest("POST", "https://api.interpreter.caiyunai.com/v1/dict",  data)

构造头

req.Header.Set("authority",  "api.interpreter.caiyunai.com")

请求

bodyText,  err := io.ReadAll(resp.Body)
err = json.Unmarshal(bodyText,  &dictResponse)
  1. os库
word := os.Args[1]

获取环境参数

socks5代理

package main

import (
    "context"
    "encoding/binary"
    "fmt"
    "io"
    "net"
)

const (
    socks5Ver  = 0x05
    atypeIPV4  = 0x01
    atypeHOST  = 0x03
    cmdConnect = 0x01
)

func main() {
    // 假设在这里建立一个 net.Listener 监听 Socks5 代理请求

    for {
        client, err := acceptConnection()
        if err != nil {
            continue
        }

        // 开启一个 goroutine 处理客户端请求
        go handleSocks5Client(client)
    }
}

func acceptConnection() (net.Conn, error) {
    // 实现接收连接请求的逻辑
}

func handleSocks5Client(conn net.Conn) {
    defer conn.Close()

    // 鉴权阶段
    if err := auth(conn); err != nil {
        fmt.Println("Authentication error:", err)
        return
    }

    // 通讯阶段
    addr, err := connect(conn)
    if err != nil {
        fmt.Println("Connection error:", err)
        return
    }

    // 给客户端回包,表示连接成功
    _, _ = conn.Write([]byte{socks5Ver, 0x00, 0x00, atypeIPV4, 0, 0, 0, 0, 0, 0})

    // 开始进行数据转发
    dest, err := net.Dial("tcp", addr)
    if err != nil {
        fmt.Println("Failed to connect to destination:", err)
        return
    }
    defer dest.Close()

    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()

    // 从客户端到目标地址的数据拷贝
    go func() {
        _, _ = io.Copy(dest, conn)
        cancel()
    }()

    // 从目标地址到客户端的数据拷贝
    go func() {
        _, _ = io.Copy(conn, dest)
        cancel()
    }()

    <-ctx.Done()
}

func auth(conn net.Conn) error {
    buf := make([]byte, 2)
    _, err := io.ReadFull(conn, buf)
    if err != nil {
        return err
    }

    ver, nmethods := buf[0], buf[1]
    methods := make([]byte, nmethods)
    _, err = io.ReadFull(conn, methods)
    if err != nil {
        return err
    }

    // 在这里根据收到的 methods 进行认证处理

    // 假设这里选择不需要认证,回包告知客户端不需要认证
    _, err = conn.Write([]byte{socks5Ver, 0x00})
    if err != nil {
        return err
    }

    return nil
}

func connect(conn net.Conn) (string, error) {
    buf := make([]byte, 4)
    _, err := io.ReadFull(conn, buf)
    if err != nil {
        return "", err
    }

    ver, cmd, _, atyp := buf[0], buf[1], buf[2], buf[3]
    if ver != socks5Ver || cmd != cmdConnect {
        return "", fmt.Errorf("Unsupported SOCKS5 command")
    }

    var addr string
    switch atyp {
    case atypeIPV4:
        buf := make([]byte, 4)
        _, err := io.ReadFull(conn, buf)
        if err != nil {
            return "", err
        }
        addr = fmt.Sprintf("%d.%d.%d.%d", buf[0], buf[1], buf[2], buf[3])

    case atypeHOST:
        hostSizeBuf := make([]byte, 1)
        _, err := io.ReadFull(conn, hostSizeBuf)
        if err != nil {
            return "", err
        }
        hostSize := int(hostSizeBuf[0])
        hostBuf := make([]byte, hostSize)
        _, err = io.ReadFull(conn, hostBuf)
        if err != nil {
            return "", err
        }
        addr = string(hostBuf)
    }

    portBuf := make([]byte, 2)
    _, err = io.ReadFull(conn, portBuf)
    if err != nil {
        return "", err
    }
    port := binary.BigEndian.Uint16(portBuf)

    return fmt.Sprintf("%v:%v", addr, port), nil
}

使用 SOCKS5 代理的实现,涵盖了以下主要步骤:

鉴权阶段:

解析客户端发送的版本号、支持的认证方法数量和认证方法列表。
针对支持的认证方法进行验证,返回认证成功的回包。
通讯阶段:

解析客户端发送的连接请求,包括版本号、命令类型、目标地址类型、目标地址、目标端口等信息。
根据目标地址类型和地址解析方式,获取目标地址和端口号。
向目标地址发起连接请求,并返回连接成功的回包。
进程管理:

使用 goroutine 后台开启两个服务器交互进程。
通过 context 库进行进程管理,确保进程在必要时可以被关闭。
将客户端和目标之间的数据互相拷贝,使得数据能够在两者之间传递。

四、课后个人总结:

在这节课学习的内容中,我收获了很多关于Go语言编程的知识和技能。以下是我的感想:

Go语言的简洁和高效:通过学习这些基础标准库和相关功能,我深刻感受到Go语言的简洁和高效。标准库提供了丰富的功能,让编程变得更加简单和高效。

数据类型转换和字符串处理:学习了strconv和strings包,对于数据类型转换和字符串处理有了更深入的了解。这些工具使得在处理用户输入和数据转换时更加方便。

网络编程和HTTP库:了解了Go语言中的网络编程和net/http包,我现在能够编写简单的HTTP服务器和客户端,这对于开发网络应用和服务端程序非常有用。

文件操作和系统交互:通过os包的学习,我学会了如何在Go语言中进行文件操作和与操作系统进行交互,这对于处理文件和系统配置十分重要。

代理和鉴权:学习了socks5代理和鉴权机制,我对网络代理和安全认证有了更深刻的理解,这对于开发安全性较高的应用非常重要。

上下文处理:context包的学习使我了解了在Go语言中如何优雅地处理请求上下文,更好地控制请求的流程和生命周期。

LINUX 挑战:用户和组管理

LINUX 挑战:用户和组管理

LINUX 挑战:用户和组管理

wget https://labfile.oss.aliyuncs.com/courses/2585/06.tar.gz
tar -xzf 06.tar.gz
chmod 700 ./06.sh
./06.sh
sudo userdel guest2
sudo userdel guest3
sudo userdel guest4
sudo userdel guest5

sudo rm -rf /home/guest2
sudo rm -rf /home/guest3
sudo rm -rf /home/guest4
sudo rm -rf /home/guest5

sudo groupadd guest
mkdir /tmp/tempuser
sudo usermod -u 2000 -g guest -d /tmp/tempuser guest1 
sudo mkdir /home/vsftpduser
sudo useradd vsftpduser -d /home/vsftpduser -s /usr/sbin/nologin 

sudo chown vsftpduser /home/vsftpduser
sudo chgrp vsftpduser /home/vsftpduser
sudo chmod 744 /home/vsftpduser
sudo useradd mag
sudo useradd dev
sudo useradd test

sudo mkdir /srv/projectx/mag
sudo mkdir /srv/projectx/dev
sudo mkdir /srv/projectx/test

sudo usermod -G dev mag
sudo chown dev:dev /srv/projectx
sudo chmod 751 /srv/projectx

sudo chmod o+rx /srv/projectx/dev
sudo chmod o+rx /srv/projectx/test
cat /etc/passwd | grep bash | cut -d ':' -f 1,3 > users.txt
sudo useradd stu
sudo passwd stu
123456
123456

sudo visudo
:21
# 按i往里写
stu ALL=(shiyanlou:shiyanlou) /bin/touch
# 退出按:wq
sudo passwd dev
123456
123456

sudo touch /etc/sudoers.d/dev
sudo chmod 777 /etc/sudoers.d/dev
vim /etc/sudoers.d/dev
# 按i往里写
dev  ALL=(root:root) /usr/sbin/useradd
# 退出按:wq