分类 linux 下的文章

用curl命令通过smtp协议发送邮件

为啥我要用curl来发邮件呢?主要是服务器不是邮件服务器,也没有装sendmail、postfix这类邮件服务,有时写一些脚本会用邮件提醒,这时候用curl就非常方便了。

要说Linux下有什么神级的命令,curl一定是其中之一。curl几乎支持你能想到的所有web相关的协议,来看看 curl官网支持的协议:

Supports...
DICT, FILE, FTP, FTPS, Gopher, HTTP, HTTPS, IMAP, IMAPS, LDAP, LDAPS, MQTT, POP3, POP3S, RTMP, RTMPS, RTSP, SCP, SFTP, SMB, SMBS, SMTP, SMTPS, Telnet and TFTP. curl supports SSL certificates, HTTP POST, HTTP PUT, FTP uploading, HTTP form based upload, proxies, HTTP/2, HTTP/3, cookies, user+password authentication (Basic, Plain, Digest, CRAM-MD5, NTLM, Negotiate and Kerberos), file transfer resume, proxy tunneling and more.

支持邮件相关的协议:IMAP、IMAPS、POP3、POP3S、SMTP、SMTPS,所以用curl来发送邮件是可以的,先看一下help有哪些邮件相关的的参数:

curl --help
--ssl           Try SSL/TLS (FTP, IMAP, POP3, SMTP)
--ssl-reqd      Require SSL/TLS (FTP, IMAP, POP3, SMTP)
--mail-from FROM  Mail from this address
--mail-rcpt TO  Mail to this receiver(s)
--mail-auth AUTH  Originator address of the original email

官网给了一个例子:

curl smtp://mail.example.com --mail-from myself@example.com --mail-rcpt
receiver@example.com --upload-file email.txt

根据 RFC 5322规范,发送邮件需要指定发件人、收件人、主题和内容等信息,官网的例子将这些信息写到email.txt,用--upload-file参数(实际上是PUT请求)发送,email.txt内容:

From: John Smith <john@example.com>
To: Joe Smith <smith@example.com>
Subject: an example.com example email
Date: Mon, 7 Nov 2016 08:45:16

Dear Joe,
Welcome to this example email. What a lovely day.

curl也支持smtps,使用smtps调用gmail的例子:

curl --ssl-reqd \
--url 'smtps://smtp.gmail.com:465' \
--user 'username@gmail.com:password' \
--mail-from 'username@gmail.com' \
--mail-rcpt 'will@nixops.me' \
--upload-file mail.txt

这种方法基本满足需要,但是需要新建一个mail.txt,在脚本中调用最好一行命令能实现,不额外新建文件。这时使用输入重定向,下面以outlook邮箱为例:

curl --ssl-reqd   --url 'smtp://smtp.office365.com:587'   --user 'sender@nixops.me:password'   --mail-from 'sender@nixops.me'   --mail-rcpt 'will@nixops.me'   -T  <(echo -e 'From: sender@nixops.me\nTo: will@nixops.me\nSubject:  备份成功\n\n nixops.me已全部备份完成,请检查');

-T和--upload-file是一样的。通过这种方法,就可以一条命令实现通过smtp发送邮件。

这种方法也有缺点:

  1. 邮箱密码写在脚本里,不够安全
  2. outlook和gmail邮箱,需要指定app专用密码,或者启用低安全性应用访问权限
  3. smtps需curl编译时有ssl,版本不能太低,如果不是古董系统,我相信你不会遇到问题

顺便说一下pop3收邮件:

curl --ssl-reqd   --url 'pop3://outlook.office365.com'   --user 'sender@nixops.me:password'

执行后会返回邮件编号和大小,继续请求指定编号就可以了,如下载第一个,在--url中指定编号:

curl --ssl-reqd   --url 'pop3://outlook.office365.com/1'   --user 'sender@nixops.me:password'

有问题可以用-vvvv看一下curl的调用过程排查,IMAP协议使用也类似,但要复杂一些,就不说了

参考文章:
https://ec.haxx.se/usingcurl/usingcurl-smtp

用asterisk来做语音验证码服务

语音验证码我想多数人都有接触过,和普通短信验证码类似,只是是用电话的方式播报验证码,好处主要有以下几点:

  1. 无内容过滤和审查,不像短信可能会触发关键字检测
  2. 过滤掉非正常用户,如接码平台等
  3. 送达率高,不容易被客户手机防骚扰等安全软件屏蔽

当然也有缺点,主要是通道接通率的问题:

  1. 可能因为使用viop供应商的通道问题,导致主叫显示号码是全球各国家的号码,被供应商和手机安全软件屏蔽或标记风险
  2. 同一主叫显示号码过于频繁会导致接通率低

下面就用FreePBX为例,说一下如何用asterisk做一个语音验证码服务,主要分为以下几步:

  1. 使用voip通道拨打客户电话号码
  2. 接通后播放欢迎语音和验证码,验证码循环播放一次,然后播放再见语音,并挂机

1. 制作音频文件

首先需要制作欢迎语音和再见语音,还有数字0-9的语音,可以自己录制或者使用在线TTS服务,如科大讯飞等,我用的是这个:http://tools.bugscaner.com/tts/
制作好语音之后,格式需要转换为gsm格式,否则asterisk无法播放,可以使用sox命令

sox hello.mp3 -r 8000 -c1 hello.gsm

将转换完成后的欢迎语、数字、再见等gsm文件放到目录 /var/lib/asterisk/sounds/cn/

2. 定制asterisk拨号计划
dianplan是定制呼叫流程,可以自己定制,编辑文件:/etc/asterisk/extensions_custom.conf,加入:

[app-code]
;先把时间、手机号码、验证码打到日志中
exten = _XX.,1,NoOp(app Voice Verify Code ${TRUNK_RING_TIMER},${DIAL_TRUNK_OPTIONS},${EXTEN} ---- ${code} )
exten = _XX.,n,Wait(1)
;设置语言为中文
exten = _XX.,n,Set(CHANNEL(language)=cn)
;播放hello.gsm 语音内容为:尊敬的XX网客户您好
exten = _XX.,n,Playback(hello)
;播放before.gsm,语音内容为:你的验证码是
exten = _XX.,n,Playback(before)
;播放验证码数字
exten = _XX.,n,SayDigits(${code})
;循环播放一次
exten = _XX.,n,Wait(1)
exten = _XX.,n,Playback(before)
exten = _XX.,n,SayDigits(${code})
;播放goodby.gsm 语音内容为:您的验证码播放完毕,再见
exten = _XX.,n,Playback(goodbye)
exten = _XX.,n,Hangup

需要reload asterisk的dianplan :

asterisk -rx "dianplan reload"

3.定制call files脚本测试
call files是最简单的方式,不需要写复杂代码实现,需要根据自己的trunk进行编写,例如我的是这样:

channel: SIP/Skype/+86136xxxxxxxx
;channel: IAX2/to196/+86136xxxxxxxx

Extension: +86136xxxxxxxx
MaxRetries: 0
RetryTime: 30000
Context: app-code
Setvar: code=6789

将以上内容保存为voicecode.call,移动到/var/spool/asterisk/outgoing/,正常情况系统就会自己拨打电话并播放验证码,主要流程就跑通了。当然这是手动的情况,程序调用需要自己写个api服务,按上面的模板生成call file脚本,替换里面的电话号码和验证码即可。

4.随机callerID设置

上面的流程跑通后,实际测试会发现接通率可能会不高,主要原因是国外的viop服务商的主叫号码有限,如果同一主叫号码非常频繁拨打国内号码,且通话时长很短,就会被供应商当成骚扰电话拉黑。这时候可以多买一些国外的主叫号码,做一个随机的主叫显示号码轮询。

FreePBX中具体设置方法,语音验证码使用的trunk选择允许所有cid,并设置Outbound CallerID为一个固定的字符,如:LEVEL3_CID,然后和上面的做法一样,通过自定义dialplan,根据Outbound CallerID的字符进行随机选择号码呼出。

先定制一个有所有主叫号码的呼叫计划并编号,编辑: /etc/asterisk/extensions_custom.conf ,加入以下内容:

[AllLevel3Cid]

exten => PhoneNo1,1,Set(CALLERID(num)=+19366661001)
  same => n,Return
exten => PhoneNo2,1,Set(CALLERID(num)=+19366661002)
  same => n,Return
exten => PhoneNo3,1,Set(CALLERID(num)=+19366661003)
  same => n,Return
exten => PhoneNo4,1,Set(CALLERID(num)=+19366661004)
  same => n,Return
exten => PhoneNo5,1,Set(CALLERID(num)=+19366661005)
  same => n,Return

第二步使用asterisk的随机函数挑选主叫号码,并更改系统自带的拨号方案,使用挑选的号码作为trunk主叫显示号码,这一步需要hook一下trunk-predial呼叫计划,在[AllLevel3Cid]这个下面继续添加:

[macro-dialout-trunk-predial-hook]
;打印一些日志
exten => s,1,NoOp(The caller id name is: ${CALLERID(name)})
exten => s,n,NoOp(The caller id number is: ${CALLERID(num)})
exten => s,n,NoOp(The ampuser is: ${AMPUSER})
exten => s,n,NoOp(The real caller id is: ${REALCALLERIDNUM})
;判断trunk的CID是否是LEVEL3_CID
exten => s,n,GotoIf($[ "${CALLERID(name)}" = "LEVEL3_CID" ]?AllLevel3Cid)
;使用随机主叫号码,共5个号码,随机选一个做为主叫号码
exten => s,n(AllLevel3Cid),Gosub(AllLevel3Cid,PhoneNo${RAND(1,5)},1))
exten => s,n,MacroExit()
exten => s,n(noselectcid),NoOp(not selectcid ))
exten => s,n,MacroExit()

这样就可以使用随机的主叫显示号码了,同样需要reload生效:

asterisk -rx "dianplan reload"

为了提高接通率主叫号码是越多越好,也可以选择那些可以CID透传的VOIP供应商,咳咳,不敢多说 :)

参考文章:

https://www.voip-info.org/asterisk-func-rand/

iptables一些杂七杂八配置整理

这是很久之前的草稿,写的时候大概是2015年,现在(2020年8月),还有三个月centos 6系列就停止维护了,标志着iptables已经快入土了,简单整理了一下格式,还是发出来吧

1. iptables 开启日志

新建一个LOGGIN链:

iptables -N  LOGGING

将需要记录日志的规则跳转到LOGGING链:

iptables -A INPUT -s 192.168.100.0/24 -j LOGGING --log-level 4

也可以指定日志的前缀:

iptables -A INPUT -s 192.168.100.0/24 -j LOGGING --log-level 4 --log-prefix "IPTables-Dropped: "

日志记录在: /var/log/message ,如果想记录在不同的文件,需要配置syslog服务

2. 使用iptables进行单ip限速

创建一个新链:

iptables --new-chain RATE-LIMIT

按源IP进行限速,每秒中最多30个并发,突发20个:

iptables --append RATE-LIMIT \
    --match hashlimit \
    --hashlimit-mode srcip \
    --hashlimit-upto 30/sec \
    --hashlimit-burst 20 \
    --hashlimit-name conn_rate_limit \
    --jump ACCEPT

超过的ip直接DROP掉:

iptables --append RATE-LIMIT --jump DROP

记录日志:

iptables --append RATE-LIMIT --jump LOG --log-prefix "IPTables-Rejected: "
iptables --append RATE-LIMIT --jump REJECT

3. iptables 配合ipset 高效拉黑IP

iptables 拉黑大量ip时性能很差,更效的方式是使用ipset + iptables,先安装 ipset:

yum  install ipset

创建一个集合blackip,通过maxelem指定大小:

ipset create blackip hash:ip hashsize 4096 maxelem 10000000

集合有很多类型,hash:ip/hash:net/hash:ip,port,net 等等,可以满足各种类型的集合需求

使用iptables拒绝blackip集合中所有ip:

iptables -I INPUT -m set --match-set attack src -j DROP

也可以创建带超时时间的集合,ip拉黑后达到超时时间自动解封:

ipset create blackip hash:net hashsize 4096 maxelem 10000000000 timeout 7200

4. iptables 进行端口转发

先要开启服务器的IP转发功能:

echo 1 > /proc/sys/net/ipv4/ip_forward

允许FORWARD:

iptables -A FORWARD -p tcp -d 192.168.1.200 --dport 8080 -m state --state NEW,ESTABLISHED,RELATED -j ACCEPT

到本机eth0 8000端口的流量转发到192.168.1.200:

iptables -t nat -A PREROUTING -p tcp -i eth0 --dport 8000 -j DNAT --to-destination 192.168.1.200:8080

如果服务器上有多网卡,如一块公网网卡eth0(192.168.1.100),一块内网网卡eth1,还需做snat:

iptables -A POSTROUTING -d 192.168.1.100 -o eth1 -p tcp --dport 8000 -j SNAT --to-source 192.168.1.200:8080

或者使用:

iptables -t nat -A POSTROUTING -s 192.168.1.100 -o eth1 -j MASQUERADE

这样就实现了访问本机8000端口的流量转发到192.168.1.200:8080

参考文章:

https://making.pusher.com/per-ip-rate-limiting-with-iptables/
https://tecadmin.net/enable-logging-in-iptables-on-linux/

centos配置双ip和双网关

办公室有台内部的服务器,需要新增一个不同段的IP,很少接触多IP+网关的情况,发现直接配置两个ip上去是不能同时使用的,随手记一下流水账。

操作系统:centos 6.10
正在用的IP1: 10.10.10.98 / Gateway: 11.10.10.1 / NETMASK: 255.255.255.0
需新增的IP2: 10.252.252.98 / Gateway: 10.252.252.1 / NETMASK: 255.255.255.0

一、双网卡配置

配置多IP时非常容易因配置错误导致断网,建议先配置好一个ip,网络正常的情况下,在配置第二块网卡,对于生产上的服务器,怕配置出错可以先备份好网络配置文件,配置好双ip后,用定时任务过几分钟恢复一下网卡配置,如果配置没有问题在去掉定时任务,然后重新配置一次,这样比较保险。

当前在用的IP1.网卡配置如下:

cat /etc/sysconfig/network-scripts/ifcfg-eth0

DEVICE=eth0
TYPE=Ethernet
ONBOOT=yes
NM_CONTROLLED=no
BOOTPROTO=static
IPADDR=10.10.10.98
NETMASK=255.255.255.0
GATEWAY=10.10.10.1
DNS1=8.8.8.8


配置第二块网卡ip:

cat  /etc/sysconfig/network-scripts/ifcfg-eth1

DEVICE=eth1
TYPE=Ethernet
ONBOOT=yes
NM_CONTROLLED=yes
BOOTPROTO=static
IPADDR=10.252.252.98
NETMASK=255.255.255.0
#GATEWAY=10.252.252.1  //网卡2不要配置网关,否则重启网卡后可能会导致断网或网卡1不通
DNS1=8.8.8.8

重启网络生效:

service network restart

正常情况下你的SSH不会掉线,两块网卡都是正常的,但是因为第二块网卡没有配置网关,所以第二块网卡IP还不能用,需要配置双网关才行

二、双网关配置

当前的情况黙认网关是第一块网卡的网关,配置多网关首先需要增加两个路由表:

vim  /etc/iproute2/rt_tables
255     local
254     main
253     default
0       unspec
#新增以下两行
252 e1 
251 e0

保存退出后,接下来手动添加静态路由,需要流量从哪个网卡进来的,就从相应的网关出去:

ip route flush table e0 
ip route add default via 10.10.10.1 dev eth0 src 10.10.10.98 table e0 
ip rule add from 10.10.10.98 table e0

ip route flush table e1 
ip route add default via 10.252.252.1 dev eth1 src 10.252.252.98 table e1 
ip rule add from 10.252.252.98 table e1

双网关配置重启后会生效,还需添加到开机启动,复制到/etc/rc.local中即可,rc.local不会被网络服务覆盖掉

参考文章:
https://mr21.cc/network-technology/centos-two-gateway-configration.html

nginx按条件输出日志

之前写过一篇: nginx两种实用的自定义访问日志格式 里面介绍了两种自定义的日志格式,一种是对程序友好的json格式日志,另外一种是手动分析友好的日志格式。人肉分析日志时,如果不是排查应用相关的日志,关注的访问日志多数是指定条件的,如分析502、404等状态码的日志.error log比较简单,就不说了。下面就介绍一下按条件输出访问日志的一些做法。

nginx 输出日志用到的内置模块是ngx_http_log_module,我们先来看看输出access log的语法:

access_log path [format [buffer=size] [gzip[=level]] [flush=time] [if=condition]];

可以看到access_log是支持if条件的,需要1.7以上版本的nginx,if条件为0或空时不输出日志,有了这个条件判断就可以做很多事了,下面介绍几种按条件输出日志的方式抛砖引玉。

一、按域名输出日志
这种情况主要是针对同一个虚拟主机配置了大量的域名情况,对于某一个域名要输出日志分析的情况。来看配置:

map $host $log_host {

    ~nixops.me  1;
    ~nixops1.me 1;
    default 0;
}

server {

    […]

    access_log /var/log/nginx/access.log main;
    access_log /var/log/nginx/nixops.me.log main if=$log_host;
}

这样 /var/log/nginx/nixops.me.log 里面就只有 nixops.me和nixops1.me这两个域名的日志了。

二、按http状态码输出日志
输出不正常的http状态码到访问日志,如502、503、504、400等状态码,对于分析反向代理或者应用异常会有用。和上面的做法一样,来看配置:

map $status $log_status {

    ~^[50] 1;
    404  1;
    default 0;
}

server {

    […]

    access_log /var/log/nginx/access.log main;
    access_log /var/log/nginx/http_error.log main if=$log_status;
}

这样就输出502、503、504、404日志到/var/log/nginx/http_error.log

三、输出指定uri的日志

输出指定location的日志到访问日志,看配置:

map $uri $log_uri {

    ~*admin 1;
    /api  1;
    default 0;
}

server {

    […]

    access_log /var/log/nginx/access.log main;
    access_log /var/log/nginx/http_uri.log main if=$log_uri;
}

也可以换个方式:

server {

    […]

    set $log_uri 0;
    if ( $uri ~ ^/api ) {
        set $log_uri 1;
    }

    access_log /var/log/nginx/access.log main;
    access_log /var/log/nginx/http_uri.log main if=$log_uri;

}

这种不使用map的方式可以支持正则表达式。

四、输出指定user agent的日志

记录包含指定user agent的请求到访问日志,如搜索引擎的抓取日志:

map $http_user_agent $log_ua {

    ~*Googlebot 1;
    ~*Baiduspider 1;

    default 0;
}
server {

    […]

    access_log /var/log/nginx/access.log main;
    access_log /var/log/nginx/http_ua.log main if=$log_ua;
}

这样就把baidu和google的爬虫抓取日志输出到http_ua.log里了。

五、记录指定cookie

记录指定包含指定cookie的请求日志:

map $http_cookie $log_cookie {

    ~PHPSESSION 1;
    default "";
}
server {

    […]

    access_log /var/log/nginx/access.log main;
    access_log /var/log/nginx/http_cookie.log main if=$log_cookie;
}

聪明的你肯定已经发现,上面的方法都是用map来实现,map一个nginx内置的变量,并设置值0或1,通过if条件判断是否需要记录。 那我们看下通过这种方式,能设置或输出那些条件的日志,看看 nginx内置变量 就知道能输出那些自定义的内容:

$ancient_browser
$arg_
$args
$binary_remote_addr (ngx_http_core_module)
$binary_remote_addr (ngx_stream_core_module)
$body_bytes_sent
$bytes_received
$bytes_sent (ngx_http_core_module)
$bytes_sent (ngx_http_log_module)
$bytes_sent (ngx_stream_core_module)
$connection (ngx_http_core_module)
$connection (ngx_http_log_module)
$connection (ngx_stream_core_module)
$connection_requests (ngx_http_core_module)
$connection_requests (ngx_http_log_module)
$connections_active
$connections_reading
$connections_waiting
$connections_writing
$content_length
$content_type
$cookie_
$date_gmt
$date_local
$document_root
$document_uri
$fastcgi_path_info
$fastcgi_script_name
$geoip_area_code (ngx_http_geoip_module)
$geoip_area_code (ngx_stream_geoip_module)
$geoip_city (ngx_http_geoip_module)
$geoip_city (ngx_stream_geoip_module)
$geoip_city_continent_code (ngx_http_geoip_module)
$geoip_city_continent_code (ngx_stream_geoip_module)
$geoip_city_country_code (ngx_http_geoip_module)
$geoip_city_country_code (ngx_stream_geoip_module)
$geoip_city_country_code3 (ngx_http_geoip_module)
$geoip_city_country_code3 (ngx_stream_geoip_module)
$geoip_city_country_name (ngx_http_geoip_module)
$geoip_city_country_name (ngx_stream_geoip_module)
$geoip_country_code (ngx_http_geoip_module)
$geoip_country_code (ngx_stream_geoip_module)
$geoip_country_code3 (ngx_http_geoip_module)
$geoip_country_code3 (ngx_stream_geoip_module)
$geoip_country_name (ngx_http_geoip_module)
$geoip_country_name (ngx_stream_geoip_module)
$geoip_dma_code (ngx_http_geoip_module)
$geoip_dma_code (ngx_stream_geoip_module)
$geoip_latitude (ngx_http_geoip_module)
$geoip_latitude (ngx_stream_geoip_module)
$geoip_longitude (ngx_http_geoip_module)
$geoip_longitude (ngx_stream_geoip_module)
$geoip_org (ngx_http_geoip_module)
$geoip_org (ngx_stream_geoip_module)
$geoip_postal_code (ngx_http_geoip_module)
$geoip_postal_code (ngx_stream_geoip_module)
$geoip_region (ngx_http_geoip_module)
$geoip_region (ngx_stream_geoip_module)
$geoip_region_name (ngx_http_geoip_module)
$geoip_region_name (ngx_stream_geoip_module)
$gzip_ratio
$host
$hostname (ngx_http_core_module)
$hostname (ngx_stream_core_module)
$http2
$http_
$https
$invalid_referer
$is_args
$jwt_claim_
$jwt_header_
$limit_conn_status (ngx_http_limit_conn_module)
$limit_conn_status (ngx_stream_limit_conn_module)
$limit_rate
$limit_req_status
$memcached_key
$modern_browser
$msec (ngx_http_core_module)
$msec (ngx_http_log_module)
$msec (ngx_stream_core_module)
$msie
$nginx_version (ngx_http_core_module)
$nginx_version (ngx_stream_core_module)
$pid (ngx_http_core_module)
$pid (ngx_stream_core_module)
$pipe (ngx_http_core_module)
$pipe (ngx_http_log_module)
$protocol
$proxy_add_x_forwarded_for
$proxy_host
$proxy_port
$proxy_protocol_addr (ngx_http_core_module)
$proxy_protocol_addr (ngx_stream_core_module)
$proxy_protocol_port (ngx_http_core_module)
$proxy_protocol_port (ngx_stream_core_module)
$proxy_protocol_server_addr (ngx_http_core_module)
$proxy_protocol_server_addr (ngx_stream_core_module)
$proxy_protocol_server_port (ngx_http_core_module)
$proxy_protocol_server_port (ngx_stream_core_module)
$query_string
$realip_remote_addr (ngx_http_realip_module)
$realip_remote_addr (ngx_stream_realip_module)
$realip_remote_port (ngx_http_realip_module)
$realip_remote_port (ngx_stream_realip_module)
$realpath_root
$remote_addr (ngx_http_core_module)
$remote_addr (ngx_stream_core_module)
$remote_port (ngx_http_core_module)
$remote_port (ngx_stream_core_module)
$remote_user
$request
$request_body
$request_body_file
$request_completion
$request_filename
$request_id
$request_length (ngx_http_core_module)
$request_length (ngx_http_log_module)
$request_method
$request_time (ngx_http_core_module)
$request_time (ngx_http_log_module)
$request_uri
$scheme
$secure_link
$secure_link_expires
$sent_http_
$sent_trailer_
$server_addr (ngx_http_core_module)
$server_addr (ngx_stream_core_module)
$server_name
$server_port (ngx_http_core_module)
$server_port (ngx_stream_core_module)
$server_protocol
$session_log_binary_id
$session_log_id
$session_time
$slice_range
$spdy
$spdy_request_priority
$ssl_cipher (ngx_http_ssl_module)
$ssl_cipher (ngx_stream_ssl_module)
$ssl_ciphers (ngx_http_ssl_module)
$ssl_ciphers (ngx_stream_ssl_module)
$ssl_client_cert (ngx_http_ssl_module)
$ssl_client_cert (ngx_stream_ssl_module)
$ssl_client_escaped_cert
$ssl_client_fingerprint (ngx_http_ssl_module)
$ssl_client_fingerprint (ngx_stream_ssl_module)
$ssl_client_i_dn (ngx_http_ssl_module)
$ssl_client_i_dn (ngx_stream_ssl_module)
$ssl_client_i_dn_legacy
$ssl_client_raw_cert (ngx_http_ssl_module)
$ssl_client_raw_cert (ngx_stream_ssl_module)
$ssl_client_s_dn (ngx_http_ssl_module)
$ssl_client_s_dn (ngx_stream_ssl_module)
$ssl_client_s_dn_legacy
$ssl_client_serial (ngx_http_ssl_module)
$ssl_client_serial (ngx_stream_ssl_module)
$ssl_client_v_end (ngx_http_ssl_module)
$ssl_client_v_end (ngx_stream_ssl_module)
$ssl_client_v_remain (ngx_http_ssl_module)
$ssl_client_v_remain (ngx_stream_ssl_module)
$ssl_client_v_start (ngx_http_ssl_module)
$ssl_client_v_start (ngx_stream_ssl_module)
$ssl_client_verify (ngx_http_ssl_module)
$ssl_client_verify (ngx_stream_ssl_module)
$ssl_curves (ngx_http_ssl_module)
$ssl_curves (ngx_stream_ssl_module)
$ssl_early_data
$ssl_preread_alpn_protocols
$ssl_preread_protocol
$ssl_preread_server_name
$ssl_protocol (ngx_http_ssl_module)
$ssl_protocol (ngx_stream_ssl_module)
$ssl_server_name (ngx_http_ssl_module)
$ssl_server_name (ngx_stream_ssl_module)
$ssl_session_id (ngx_http_ssl_module)
$ssl_session_id (ngx_stream_ssl_module)
$ssl_session_reused (ngx_http_ssl_module)
$ssl_session_reused (ngx_stream_ssl_module)
$status (ngx_http_core_module)
$status (ngx_http_log_module)
$status (ngx_stream_core_module)
$tcpinfo_rtt
$tcpinfo_rttvar
$tcpinfo_snd_cwnd
$tcpinfo_rcv_space
$time_iso8601 (ngx_http_core_module)
$time_iso8601 (ngx_http_log_module)
$time_iso8601 (ngx_stream_core_module)
$time_local (ngx_http_core_module)
$time_local (ngx_http_log_module)
$time_local (ngx_stream_core_module)
$uid_got
$uid_reset
$uid_set
$upstream_addr (ngx_http_upstream_module)
$upstream_addr (ngx_stream_upstream_module)
$upstream_bytes_received (ngx_http_upstream_module)
$upstream_bytes_received (ngx_stream_upstream_module)
$upstream_bytes_sent (ngx_http_upstream_module)
$upstream_bytes_sent (ngx_stream_upstream_module)
$upstream_cache_status
$upstream_connect_time (ngx_http_upstream_module)
$upstream_connect_time (ngx_stream_upstream_module)
$upstream_cookie_
$upstream_first_byte_time
$upstream_header_time
$upstream_http_
$upstream_queue_time
$upstream_response_length
$upstream_response_time
$upstream_session_time
$upstream_status
$upstream_trailer_
$uri

这么多变量是不是晕了?不用怕,很多变量其实对于日志分析是没什么用的,常用的变量在nginx两种实用的自定义访问日志格式里面有介绍,过滤掉一下没用的,基本上以下变量就够用:

$host  :访问域名
$uri   :请求uri
$status :http状态码
$request_method :请求方法
$request_completion :请求是否完成
$upstream_addr :反向代理的upstream
$upstream_status  :upstream响应值
$scheme :请求的协议
$http_referer :referer来源
$http_user_agent :浏览器的user agent
$http_cookie : 本地所有cookie,也可以打印指定cookie,如打印session: $http_session
$args 或$arg_ : 请求时的参数

如果有不常见的需求,可以参考官方Nginx里的内置变量进行配置输出。

六、多条件判断
上面例子1-5都是单条件的,也可以使用多条件,nginx没有类似if-else多条件判断语句。涉及到多条件判断的情况,有个通用的写法,先set多个条件的变量,在if判断:

map $http_user_agent $log_ua {

    ~*Googlebot 1;
    ~*Baiduspider 1;
    default 0;
}

map $http_cookie $log_cookie {

    ~PHPSESSION 1;
    default "";
}

server {

    […]

    set $logging 0;
    set $logtmp '';

    if ( $log_ua = 0 ) {
        set $logtest "${logtmp}A";
    }
    if ( $log_cookie = 0 ) {
        set $logtmp "${logtmp}B";
    }
    if ( $logtmp = "AB" ) {
        set $logging 1;
    }

    access_log /var/log/nginx/access.log main if=$logging;

}

nginx if条件句不为0或空时,nginx会认为是true,所以上面的配置可以精简一下写法:

map $http_user_agent $log_ua {

    ~*Googlebot 1;
    ~*Baiduspider 1;
    default 0;
}

map $http_cookie $log_cookie {

    ~PHPSESSION 1;
    default "";
}

server {

    […]

    set $logging 0;
    set $logtmp "$log_ua$log_cookie"
    if ( $logtmp = "11" ) {
        set $logging 1;
    }

    access_log /var/log/nginx/access.log main if=$logging;

}

不用if判断也是可以的,用map的方式:

map $http_user_agent $log_ua {

    ~*Googlebot 1;
    ~*Baiduspider 1;
    default 0;
}

map $http_cookie $log_cookie {

    ~PHPSESSION 1;
    default "";
}

map “$log_ua:$log_cookie” $logging {
“1:1” 1;
default 0;
}

server {

    […]

    access_log /var/log/nginx/access.log main;
    access_log /var/log/nginx/http_multi_conditions.log main if=$logging;
 }

nginx按条件输出日志还是挺简单的,做法就是两种方式:

  1. 上面例子中使用map的方式
  2. 例子三中第二种写法,直接使用if的方式

参考文章:
https://www.bjornjohansen.com/exclude-requests-from-nginx-access-log
https://www.nginx.com/blog/sampling-requests-with-nginx-conditional-logging/

nginx多https证书配置精简

对于nginx多https站点通常的做法是这样:

server 443 ssl;
server_name nixops1.me;
ssl_certificate     /usr/local/nginx/keys/nixops1.me.crt;
ssl_certificate_key /usr/local/nginx/keys/nixops1.me.key;
......

如果有多个https站点,就是每个站点复制一份这个配置。如nixops2.me、nixops3.me等等,在只有几十个https站点的时候还好,如果某个站点的域名特别多,例如有几千个域名甚至上万个域名的时候,这个配置就很恐怖了,需要精简并合并配置。

下面举例说明,假设应用场景为:

  1. 同一站点有大量域名需要配置https证书,其它配置一样
  2. 每个之证书都是泛域名证书,如let's encrypt的wildcard证书
  3. 所有证书命名均为公钥domain.com.crt、私钥为domain.com.key 这样的格式
  4. 所有证书保存在相同目录中,如/usr/local/nginx/conf/keys/

具体做法可以用nginx的map和正则表达式,根据ssl_server_name(即https的访问域名),获取公钥和私钥的完整路径做为变量。配置证书时使用该变量即可。先来看nginx配置文件中HTTP段的map配置:

map $ssl_server_name $NixopsCert {
    default /usr/local/nginx/conf/keys/nixops.me.crt; #指定一个默认的证书
    ~*^(.+\.)*([^\.]+\.[^\.]+)$ /usr/local/nginx/conf/keys/$2.crt;
}

map $ssl_server_name $NixopsKey {
    default /usr/local/nginx/conf/keys/nixops.me.key;
    ~*^(.+\.)*([^\.]+\.[^\.]+)$ /usr/local/nginx/conf/keys/$2.key;
}

这样配置好后,如果通过https访问nixops1.me或*.nixops1.me,获取到的路径分别为:

$NixopsCert  --> /usr/local/nginx/conf/keys/nixops1.me.crt;
$NixopsKey   --> /usr/local/nginx/conf/keys/nixops1.me.key;

然后配置server段证书公钥和私钥时就不需要按传统作法复制配置,只需指定变量即可:

server {
    listen  80;
    listen  443 ssl http2;
    server_name
        nixops.me
        nixops1.me
        nixops2.me
        #......
    ;

    ssl_certificate     $NixopsCert;
    ssl_certificate_key $NixopsKey;

    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
   #其它server段配置
}

这样同一站点有多个证书时,就可以只在server_name里添加,不需要指定https配置了,和http站点添加域名绑定非常类似。

通过nginx反向代理实现内网yum源

自建机房为了节省公网流量都会有内网的yum源,对于自动化程度比较高,软件标准化和准入做的比较好的情况下,常用软件会打成rpm包放进内网源里,很多基础软件和依懒也都会打包进系统模板中。其实多数情况下,需要从yum源里更新的包不多,不需要将整个centos源都同步进内网源里。可以通过nignx反向代理用偷懒的方式实现一个yum源。

我们先来看看yum的工作流程:

  1. 服务器请求根据自己的系统版本、架构、依次请求各启用的源中的repomd.xml文件
  2. repomd.xml中保存了软件仓库中的metadata,也就是“元数据”,这些数据包含了该软件仓库内所有软件包的包名及其所需的依赖环境、软件包里文件列表等信息
  3. yum或dnf下载了metadata后,将这些信息和本地环境对比,进而确认需要安装哪些rpm包,并在用户确认后开始安装或更新

一般Yum源里会有以下五种后缀的文件:

  1. repomd.xml 是repo+md(metadata)的意思,软件源的主配置文件,保存源里面几个主要文件的校验码、时间戳和大小等信息
  2. .xml.gz 后缀的文件,如软件源里包数据(primary.xml)、文件列表数据(filelists.xml.gz)、 其它信息(other.xml)
  3. .bz2后缀的文件,一般是sqlite的压缩档,一般都有primary.sqlite.bz2、filelists.xml.gz、other.sqlite.bz2
  4. .xml其它metadata文件
  5. .rpm具体的软件包

要想节省流量,最好做法是nginx在反向代理时,用proxy_store的方式缓存一份在本地。rpm包比较大肯定是要缓存的,.xml/.bz2/.xml.gz这些文件一般几兆,文件名除了repomd.xml都有校验码。属于更新频繁的文件,但是最好缓存在本地并设置一的较短的缓存时间。

看下yum源服务器的nginx缓存配置:

resolver 8.8.8.8 ipv6=off  valid=10s;

server {

    listen 8001;
    server_name $host;
    # 反带官方源
    location /centos/ {
        proxy_set_header  X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_pass http://mirror.centos.org/centos/;
    }
}

server {
    listen 8002;
    #备用源
    server_name $host;
    location / {
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header  X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_pass       http://mirror.pregi.net;
    }
}

server {
    listen 8003;
    #epel源
    server_name $host;
    location /epel/ {
        proxy_set_header  X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_pass https://dl.fedoraproject.org/pub/epel/;
    }
}

upstream base {
    #官方源
    server 127.0.0.1:8001 fail_timeout=5s max_fails=3;
    #备用源
    server 127.0.0.1:8002  fail_timeout=5s max_fails=3 backup;
}

upstream epel {
    #epel源
    server 127.0.0.1:8003 fail_timeout=5s max_fails=3;
}

server {
    listen       80 ;
    server_name $host;
    root /opt/repos/;
    index index.html ;
    access_log logs/yum.access.log main;

    location /centos/ {
        location ~ (.xml|.gz|.bz2)$ {
                proxy_store on;
                proxy_temp_path "/opt/repos/";
                proxy_set_header Accept-Encoding identity;
                proxy_set_header X-Real-IP $remote_addr;
                proxy_next_upstream error http_502;
                if ( !-e $request_filename ) {
                        proxy_pass http://base;
                }
                if ( -e $request_filename ) {
                                 expires      1d; #保存1天
                }
        }

        location ~ .rpm$ {
                proxy_store on;
                proxy_temp_path "/opt/repos/";
                proxy_set_header Accept-Encoding identity;
                proxy_set_header X-Real-IP $remote_addr;
                proxy_next_upstream error http_502;
                if ( !-e $request_filename ) {
                        proxy_pass http://base;
                }
                if ( -e $request_filename ) {
                                 expires      2000d;
                }
        }
    }
    #EPEL源同/centos/,这里忽略了
    
}

接下来需要配置客户端yum源,如果内网有dns服务器,可以直接改域名的解析,这样就可以不更改客户端配置。否则删除原有所有的源,添加如下内容:

cat /etc/yum.repos.d/all.repo

[Nixops-Base]
name=Nixops-Base
#指到nginx yum缓存服务器
baseurl=http://172.20.1.1/centos/$releasever/os/$basearch/
#关闭gpg检查
gpgcheck=0

[Nixops-Updates]
name=Nixops-Updates
baseurl=http://172.20.1.1/centos/$releasever/updates/$basearch/
gpgcheck=0

[Nixops-Extras]
name=Nixops-Extras
baseurl=http://172.20.1.1/centos/$releasever/extras/$basearch/
gpgcheck=0

#[Nixops-Epel]
#name=Nixops-Epel
#baseurl=http://172.20.1.1/epel/$releasever/$basearch/
#gpgcheck=0

客户端设置好后,就可以用yum安装个软件试试了:

yum clean all
yum install telnet -y 

配置正确就能看到是从Nixops-Updates获取telnet,同时在nginx服务器上能看到请求日志和代向低代理日志

上面主要以centos官方源为例,epel源的配置没有写全,如果有其它软件源也可以按以上配置进行反代缓存,如zabbix、saltstack、remi、percona等源,这是一种比较偷懒又投机的做法,实际使用虽然没有什么问题,但还是建议使用rsync的方式完整的同步yum源。

linux文件描述符和IO重定向

一、文件描述符

在linux和unix系统中,一切都是文件,内核是通过文件描述符来访问文件,文件描述符是非负整数,最大值受系统最大可打开的文件数限制。可以使用命令查看:

ulimit -a

查看open files的值,默认是1024。

二、标准流
先来看看维基百科中标准流的定义: 在linux和unix系统中,一个程序运行时和环境交互(INPUT/OUTPUT)的通道,叫标准流。

对于Linux下的进程,每个进程都有三个标准的文件描述符,对应于三个标准流:

  1. 标准输入:文件描述符为 0,对应标准流 stdin ,对应设备为键盘
  2. 标准输出:文件描述符为 1,对应标准流 stdout,对应设备为显示器
  3. 标准错误:文件描述符为 2,对应标准流 stderr,对应设备为显示器

linux下的用户都是通过终端进行交互,如bash、sh、zsh等,从键盘接收输入,在显示器上打印输出

三、linux I/O重定向分类

Linux重定向操作发生在三个标准流之间,主要来完成复杂的任务,允许你控制程序或命令的输入输出流。
主要分为:

  1. 输入重定向 <
  2. 输入重定向读写模式 <> ,以读写模式重定向到输入,没有则创建文件
  3. 输出重定向 >
  4. 输出重定向追加模式 >>
  5. 非标准重定向 &> ,将标准输出和标准错误重定向
  6. 非标准重定向追加模式 &>>
  7. 管道 | ,将一个命令的输出做为另外一个命令的输入

四、举例

1.正常执行命令,将标准输出和标准错误都显示在终端上,我相信能看到我这篇文章的人都会

./command

2.将标准输出重定向到文件

./command 1> stdout.log 

1在终端中可以省略,就变成了最常见的情况:

./command > stdout.log

此时如果重新执行有错误,标准错误会显示在终端上

3.将标准错误重定向到文件

./command 2> stderr.log

此时标准输出会正常打印到终端上

4.执行命令,将输出日志打印到stdout.log,将错误日志打印到stderr.log

./command 1>stdout.log 2>stderr.log

此时终端什么都不显示

5.执行命令,将标准输出和标准错误都重定向到同一个文件

./command &> all.log

或者用麻烦一点的方法:

./command > all.log 2>&1

6.执行命令,即重定向标准输出又同时在终端打印,需要借助管道符和命令tee

./command |tee  stdout.log

将标准输出和标准错误全部重定向,并输出到终端

./command 2>&1 |tee all.log

还记得上面说的管道的定义吧,这个实际上是执行两个命令,./command 2>&1 将全部日志重定向到标准输出,在作为标准输入传给tee命令

7.执行命令,打印标准输出,重定向标准输出和标准错误

./command 2>stderr.log |tee stdout.log

8.丢弃输出,/dev/null是系统的垃圾箱,可以将标准输出、标准错误全部输出到这个设备

./command >/dev/null
./command 2>/dev/null
./command &> /dev/null

有了上面的说明,我相信这三条命令不用解解释了

9.利用输出重定向创建文件

> newfile.txt

这个命令等同于:

: > newfile.txt

:是一个占位符, 不产生任何输出,这种情况可以用来创建空文件

10.输入重定向

cat 0<  /etc/issue

0也可以省略不写

cat <  /etc/issue

11.输入重定向到交互的shell

cat << EOF
Hi nixops.me
EOF

此时是以EOF为分隔符,可以输入任意内容,直到再次输入EOF完成,输入的内容会做为cat命令的输入显示,此例中输出Hi nixops.me

五、高级用法
上面讲到的例子只是在标准输入(0)、标准输出(1)和标准错误(2)之间进行IO重定向,开头也说了,文件描述符是非负整数,最大值受系统的ulimit里open files限制,除标准文件描述符外,那有没有办法在普通文件描述符之间重定向呢?
我们先来看一个刚刚举过的例子:

./command > all.log 2>&1

这个例子的意思是,将标准错误(2)重定向(>)到标准输出(1),并将所有标准输出重定向到all.log,那如果是普通的文件描述符,是否可以这样重定向呢?

答案是可以的。

例如,我有一个文件描述i和j,两个文件描述符之间进行重定向 i>&j,含义是:

  1. 重定向文件描述符i到j
  2. 指向i文件的所有输出都发送到j

同理,也可以将标准文件描述符,重定向到普通文件描述符,如 >&i,含义是:将标准输出重定向到文件描述符i。

下面我们来举例说明,我们先来创建一个文件描述符,可以用exec命令,以读写模式创建一个文件描述符:

exec 3<>newfile

然后向文件描述符3写入内容:

echo 'nixops.me' >&3

需要关闭文件描述符3,使用命令:

exec 3>&-

关闭后就生成了newfile文件,查看一下内容:

cat newfile
nixops.me

经过上面的例子,下面这些也很好理解,简单说明一下:

  1. \>&- 等同于 1>&- 关闭标准输出
  2. <&- 等同于 0<&- 关闭标准输入
  3. \>&i 等同于 1>&i 将标准输出重定向到文件描述符i
  4. <&i 等同于 0<&i 将文件描述符i重定向到标准输入
  5. i<&- 关闭文件描述符i的输入
  6. i>&- 关闭文件描述符i的输出
  7. i>&j- 将文件描述符j的输出文件移动到i上,并关闭j
  8. i<&j- 将文件描述符j的输入文件移动到i上,并关闭j
  9. 创建文件描述符时名称可以用变量

附:
askubuntu.com里有个答案下,有人做了一个表格,直观的看出在使用重定向的情况下,stdout、stderr在终端是否显示、是否重定向到文件、文件是否会创建。这几种情况一目了然,帮助理解。

终端显示 文件内容 文件状态
语法 StdOut StdErr StdOut StdErr File
command > 创建
command >> 追加
command 2> 创建
command 2>> 追加
command &> 创建
command &>> 追加
command | tee 创建
command | tee -a 追加
command |& tee 创建
command |& tee -a 追加

参考文章:
https://en.wikipedia.org/wiki/Standard_streams
https://askubuntu.com/questions/420981/how-do-i-save-terminal-output-to-a-file/731237#731237
https://www.putorius.net/linux-io-file-descriptors-and-redirection.html
https://www.junmajinlong.com/shell/fd_duplicate/

最新文章

最近回复

分类

归档

统计

  • 文章总数:168篇
  • 分类总数:5个
  • 评论总数:103条
  • 页面总数:172个
  • 本站运行:4870天

其它