maksim 发布的文章

编码场景

场景:不可信数据输出到前后端页面时,需要考虑可能的代码注入情况。

说明:不可信数据输出到前后端页面时,根据输出场景对其进行相关编码,如HTML实体编码、URL编码。

<html>
    <body>
        <?php
            $str = $_POST['feed'];
            echo htmlspecialchars($str, ENT_QUOTES);
        ?>
    </body>
</html>

净化场景

场景:输出到前后端页面时,如果包含敏感信息需要做脱敏等处理再输出。

说明:针对操作系统命令、SQL和LDAP查询,净化所有输出的敏感信息,如银行卡、手机号、系统信息等。

<?php
    class DesensitizedUtils {
        //【中文姓名】只显示第一个汉字,其他隐藏为2个星号,比如:李**
        function chineseName($str) {
            $resstr = substr_replace($str, '*', 1);
            return $resstr;
        }

        // 【身份证号】显示最后四位,其他隐藏。共计18位或者15位,比如:*************1234    
        function idCardNum($str) {
            $resstr = substr_replace($str, '**************', 0);
            return $resstr;
        }
    
        //【固定电话】 显示后四位,其他隐藏,比如:*******3241
        function fixedPhone($str) {
            $resstr = substr_replace($str, '****', 3, 4);
            return $resstr;
        }
    
        // 【手机号码】前三位,后四位,其他隐藏,比如:135****6810
        function mobilePhone($str) {
            $resstr = substr_replace($str, '****', 3, 4);
            return $resstr;
        }    
    
        // 【地址】只显示到地区,不显示详细地址,比如:上海徐汇区漕河泾开发区***
        function address($str) {
            $resstr = substr_replace($str, '****', -4);
            return $resstr;
        }
    
        // 【电子邮箱】 邮箱前缀仅显示第一个字母,前缀其他隐藏,用星号代替,@及后面的地址显示,比如:d**@126.com
        function email($str) {
            $resstr = substr_replace($str, '****' , 1);
            return $resstr;
        }
    
        // 【银行卡号】前六位,后四位,其他用星号隐藏每位1个星号,比如:622261******1234
        function bankCard($str) {
            $resstr = substr_replace($str, '******', 6);
            return $resstr;
        }
    
        // 【密码】密码的全部字符都用*代替,比如:******
        function password($str) {
            $resstr = substr_replace($str, '*', 0);
            return $resstr;
        }
    }

在我们日常的编码过程中,输入校验可以说是我们业务安全的第一道防线,我们需要对每一次的用户输入做一定的校验,也就是我们常说的,不要信任任何用户的输入。

对于用户的输入,我们需要做到如下的校验:

  • 白名单
  • 黑名单
  • 规范化
  • 合法性校验
  • 防范 SQL 注入文件校验
  • 访问控制

白名单

场景: 当可能输入的集合比较小的,可以选择使用列表选择来确保不能绕过验证。

说明: 不可信数据可设定白名单校验的,应接受所有和白名单匹配的数据,并阻止其他数据。

function isValidateCommandRoutine($command) {
    $WhiteList = array('cmd','ls','sh','env');
    if (false === empty($command)) {  
        foreach ($WhiteList as $key=>$value) {
            if ($value === $command) {
                return true;
            }
        }
        return false;
    } else {
        return false;
    }
}
function validateCommandArray($cmds) {
    if (isValidateCommandRoutine($cmds)) {
        //Process valid input and return here.
        system(EscapeShellCmd($cmds));
    }
    return;
}
       

黑名单

场景:部分可控输入数据集合时,拒绝已知恶意的输入数据。

说明:不可信数据中包含不良输入字符时,如空字节(%00)、换行符(%0d,%0a,\r,\n)、路径字符(../,..\)等,建议直接阻止该数据,若需要接受该数据,则应做不同方式的净化处理。

function isValidateString($s) { 
    $keywords = "[<>]"; 
    if (preg_match("$keywords", $s)) { 
        return false; 
    } else { 
        return true; 
    } 
}
       

规范化

场景:部分可控输入数据集合时,拒绝已知恶意的输入数据。

说明:不可信数据的净化和校验前需进行规范化,如将目录遍历(../或..\)等相对路径转化成绝对路径,URL解码等。

function fileCheck() {
    $basepath = './site/img/';
    $realBase = realpath($basepath);
    $realUserPath = realpath($basepath . $_GET['path']);
    if ($realUserPath === false || strpos($realUserPath, $realBase) !== 0) {
        return false;
    } else {
        return true;
    }
}

合法性校验

场景:当合法值的范围太广泛时,不能明确确定时,根据业务场景进行类似正则匹配。

说明:不可信数据的合法性校验包括:数据类型如字符、数字、日期等特征;数据范围;数据长度等。

function isValidateString() { 
    $patternname = "^[A-Za-z0-9_-]{4,}$"; 
    if ($_POST['name'] && preg_match("$patternname", $_POST['name'])) { 
        return true; 
    } else { 
        return false; 
    } 
}

防范SQL注入

场景:当用户数据进入后端SQL操作时,使用参数化查询来处理避免出现SQL注入。

说明:不可信数据的合法性校验包括:数据类型如字符、数字、日期等特征;数据范围;数据长度等。

<?php
    $dbns = 'mysql:dbname=testdb;host=127.0.0.1';
    $username = 'dbuser';
    $password = 'dbpasswd'
    try {
        $pdo = new PDO($dbns, $username, $password);
        $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        $pdo->exec('set names utf8'); 
    } catch (PDOException $e) {
        echo 'Connection failed: ' . $e->getMessage();
        exit;
    }
    $str = $pdo->prepare('select * from content where id=? and name=?');
    $str->execute(array('id'));
    $str->execute(array('name'));
    print_r($str->fetchAll());
?>

文件校验

场景:用户提交压缩文件,服务器需要进行解压缩存储时。

说明:不可信数据为解压缩的文件时,如果文件位于服务目录外或文件大小超过限制,应拒绝处理。

class Unzip{
    public function __construct() {
       header("content-type:text/html;charset=utf8");
    }
 
    public function unzip($src_file, $dest_dir) {
        $zip = zip_open($src_file)
        if ($zip) {
            $this->create_dirs($dest_dir);
            while ($zip_entry = zip_read($zip)) {
                if (zip_entry_filesize($zip_entry) > 0x640000) {
                    return false;
                }
                $pos_last_slash = strrpos(zip_entry_name($zip_entry), "/");
                if ($pos_last_slash !== false) {
                    $this->create_dirs($dest_dir.substr(zip_entry_name($zip_entry), 0, $pos_last_slash+1));
                }
                if (zip_entry_open($zip, $zip_entry, "r")) {
                    $file_name = $dest_dir.zip_entry_name($zip_entry);              
                    if (!is_file($file_name)) {
                        $fstream = zip_entry_read($zip_entry, zip_entry_filesize($zip_entry));
                        @file_put_contents($file_name, $fstream);
                        chmod($file_name, 0666);
                    }
                    zip_entry_close($zip_entry);
                }
            }
            zip_close($zip);
            return true;
        } else {
            return false;
        }
    }
 
    public function create_dirs($path) {
        if (!is_dir($path)) {
            $directory_path = "";
            $directories = explode("/", $path);
            array_pop($directories);
            foreach($directories as $directory) {
                $directory_path .= $directory . "/";
                if (!is_dir($directory_path)) {
                    mkdir($directory_path);
                    chmod($directory_path, 0755);
                }
            }
        }
    }
}

访问控制

场景:用户提交的数据,在处理前需要进行身份确认。

说明:不可信数据通过上述校验后,还应确认所提交的内容是否与用户的身份匹配,避免越权访问。

<?php
namespace App\Http\Controllers;
use App\User;
use App\Http\Controllers\Controller;

class UserController extends Controller
{
    public function __construct() {
        $this->middleware('auth');
    }

    public function show($id) {
        return view('user.profile', ['user' => User::findOrFail($id)]);
    }
}
?>

你好,我是一笑(Maksim),接下来我们要来聊一聊如何在业务中如何开发出“安全”的代码。

首先来讲一讲为什么会写关于安全的文章,最近真的是被安全团队搞的晕头转向,在老代码中存在大量的安全问题,不断的修修补补,起初感觉有些烦躁,感觉有些接口没有必要这么严格,直到我被“安利”了一款应用,由于涉敏就不在文章中详细说明了。

我来简单说一下那个应用的功能,通过输入手机号或者邮箱,可以将个人信息完整的查询出来,很恐怖,网络之上的确没有秘密,当我自己的信息被查出来之后,我是真的慌了,其中包括了我的身份证号、曾经的住址,用过的邮箱,甚至我怀疑,我开过几次房都能查到。

那这些信息都是怎么被泄露出去的呢?

让我们来看看曾经的新闻:

  • 2013年的时候,腾讯爆出被黑客盗取了大量的用户资料,其中包括7000多万个QQ群,12亿多个部分重复的QQ号码。
  • 2015年10月19日下午消息,乌云今日宣布发现新漏洞,此漏洞将导致 网易 163/126邮箱过亿数据泄漏,泄露信息包括用户名、密码、密码保护信息、登录IP以及用户生日等多个原始信息,影响数量总共近5亿条!
  • 酒店数据库被“黑” 3.27亿人次信息泄露

这些都是我们熟知的互联网公司或者产品,与此同时网上的开源产品,ecshop,discuzz, dedecms 曾经都存在大量的漏洞,如果站长不具备安全意识,没有及时的修复漏洞,那么我们的信息将会更容易被获取。

同时这样的问题还会影响品牌的声誉,会给品牌带来不可估量的损失,我很认同何为舟老师在极客时间《安全攻防技能 30 讲》中的一句话:我们在追求开发效率的同时,一定要把“安全”这俩字放在心上

就在2021 年,北京组织各大公司进行攻防演练,APP 等系统必须进行等保测试,这都意味着未来国家对信息安全将会越来越重视,而变成安全将是每一位程序员的必修课。

在公司内部,基本上所有上规模的公司都会有自己的安全审计部门,而我这段时间也正在被审计部门折磨着,历史遗留的系统有大量的系统,在扫描到我们事业部的时候,让我真的是苦不堪言,一天最多能给出八九个工单。

下面,我通过自己的亲身经历,以及对我司安全规范、《安全攻防技能 30 讲》,《PHP 安全之道》进行整理,剥丝抽茧,将各种安全手段和需要注意的地方,进行逐一讲解,下面是大纲:

接下里,说一下这个系列文章的整体结构其中涉及如下:

  • 输入验证
  • 输出编码
  • 身份验证
  • 短信验证码
  • 图灵验证码
  • 密码管理
  • 回话安全
  • 访问控制
  • 注入攻击
  • 敏感信息
  • CSRF 攻击
  • 文件上传安全
  • 接口安全
  • I/O 操作
  • 运行环境
  • 异常处理
  • 日志规范

做到上面这些,代码就已经算是相对安全了,但是只要代码是在不断产出,就会出现新的漏洞,我也会通过自己的学习和实践,不断去补充这个系列的文章。

参考资料

[1] 何为舟 《安全攻防技能 30 讲》安全攻防技能30讲_安全_漏洞_黑客-极客时间 (geekbang.org)

[2] 王昊天 Web安全攻防实战 https://time.geekbang.org/course/intro/100055001

[3] 栾涛 《PHP 安全之道》人民邮电出版社

更新日志:

  • 2023-3-20 增加文字描述

你好,我是一笑,在前面的章节中,我们实现了基于 select socket 开发,在这里,我们将利用 select 实现 Reactor模型。

Reactor 翻译过来的意思是「反应堆」,可能大家会联想到物理学里的核反应堆,实际上并不是的这个意思。

这里的反应指的是「对事件反应」,也就是来了一个事件,Reactor 就有相对应的反应/响应。

事实上,Reactor 模式也叫 Dispatcher 模式,我觉得这个名字更贴合该模式的含义,即 I/O 多路复用监听事件,收到事件后,根据事件类型分配(Dispatch)给某个进程 / 线程。

具体流程如下:

  1. 监听事件
  2. 捕获事件
  3. 判断事件类型

    • write Handler 写事件
    • read handler 读时间
    • accept handler

EventLoop,它是整个事件驱动的核心,它管理者事件表,不断循环处理就绪的文件事件,在这里我们可以理解为就是一个死循环。


function LoopEventRun() {
    $eventLoop->stop = 0;
    while (!$eventLoop->stop) {
        $eventLoop->processEvents(AE_ALL_EVENTS);
    }
}

AE_ALL_EVENTS 代表着所有事件。在这里我们设计三个常量。

  • const AE_FILE_EVENTS = 1;: 文件事件,其实Socket 也是一种文件事件,socket_create返回的是文件描述符。
  • const AE_TIME_EVENTS = 2;: 时间事件。
  • const AE_ALL_EVENTS = (AE_FILE_EVENTS | AE_TIME_EVENTS); 代表着所有事件

我们首先来实现 tcp server。

<?php

class TcpSocket
{
    protected string $address;
    protected int $port;

    public function __construct(string $address, int $port)
    {
        $this->address = $address;
        $this->port = $port;
    }

    /**
     * @return Socket
     * @author 国柱<[email protected]>
     */
    public function getTcpServer(): Socket

    {
        //创建服务端的socket套接流,net协议为IPv4,protocol协议为TCP
        $socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
        if (!$socket) {
            echo "Failed to create socket";
            exit();
        }

        if (!socket_set_option($socket, SOL_SOCKET, SO_REUSEADDR, 1)) {
            echo "Failed to set socket options!";
            exit();
        }
        socket_set_nonblock($socket);

        // 绑定接收的套接流主机和端口,与客户端相对应
        if (!socket_bind($socket, $this->address, $this->port)) {
            echo 'server bind fail:' . socket_strerror(socket_last_error());
            exit();
        }
        // 监听套接流
        if (!socket_listen($socket, 4)) {
            echo 'server listen fail:' . socket_strerror(socket_last_error());
            exit();
        }
        echo "create socket success: $this->address:$this->port" . PHP_EOL;
        return $socket;
    }

}

这段代码非常简单,就是标准的创建 socket。

<?php

const AE_FILE_EVENTS = 1;
const AE_TIME_EVENTS = 2;
const AE_ALL_EVENTS = (AE_FILE_EVENTS | AE_TIME_EVENTS);

const AE_DONT_WAIT = 4;

const AE_READABLE = 1;
const AE_WRITABLE = 2;
const AE_EXCEPTION = 4;

class EventLoop
{

    public int $stop = 0;

    // 存储所有事件
    public array $eventContainer = [];

    public function addEvent(SocketEvent $event): void
    {
        $this->eventContainer[] = $event;
    }

    /**
     * @param $flags
     * @return int
     * @author 国柱<[email protected]>
     */
    public function processEvents($flags): int
    {
        $read = $write = $except = [];
        $number = 0;
        $processed = 0;
        if ($flags & AE_FILE_EVENTS) {
            // 遍历链表
            foreach ($this->eventContainer as $event) {
                //如果是读事件就将 fd 放入到 rfds 中
                if ($event->mask & AE_READABLE) {
                    $read[] = $event->fd;
                }
                // 如果是写时间爱你就放进 $write 中
                if ($event->mask & AE_WRITABLE) {
                    $write[] = $event->fd;
                }
                // 如果是屏障事件就放进  $except 中
                if ($event->mask & AE_EXCEPTION) {
                    $except[] = $event->fd;
                }
                $number++;
            }
        }

        if ($number || (($flags & AE_TIME_EVENTS) && !($flags & AE_DONT_WAIT))) {
            $retrieval = socket_select($read, $write, $except, null);
            if ($retrieval > 0) {
                foreach ($this->eventContainer as $event) {
                    $socketFd = $event->fd;
                    if (($event->mask & AE_READABLE && in_array($socketFd, $read)) || ($event->mask & AE_WRITABLE && in_array($socketFd, $write)) || ($event->mask & AE_EXCEPTION && in_array($socketFd, $except))) {
                        $mask = 0;

                        if ($event->mask & AE_READABLE && in_array($socketFd, $read)) {
                            $mask |= AE_READABLE;
                        }
                        if ($event->mask & AE_WRITABLE && in_array($socketFd, $write)) {
                            $mask |= AE_WRITABLE;
                        }
                        if ($event->mask & AE_EXCEPTION && in_array($socketFd, $except)) {
                            $mask |= AE_EXCEPTION;
                        }
                        // 执行闭包
                        call_user_func($event->fileProc, $this, $socketFd, $event->clientData, $mask);
                        $processed++;
                        // 清理 read
                        if ($event->mask & AE_READABLE) {
                            $index = array_search($socketFd, $read);
                            unset($read[$index]);
                        }
                        if ($event->mask & AE_WRITABLE) {
                            $index = array_search($socketFd, $write);
                            unset($write[$index]);
                        }
                        if ($event->mask & AE_EXCEPTION) {
                            $index = array_search($socketFd, $except);
                            unset($except[$index]);
                        }
                    }
                }
            }
            return $processed;
        }
        return $processed;
    }

    public function run()
    {
        while (true) {
            if (!$this->stop) {
                $this->processEvents(AE_ALL_EVENTS);
            }
        }
    }

    public
    function createSocketEvent($fd, $mask, $fileProc, $finalizerProc, Client $clientData = null): SocketEvent
    {
        echo "创建 event mask : [$mask]" . PHP_EOL;
        $event = new SocketEvent(
            $fd,
            $mask,
            $fileProc,
            $finalizerProc,
            $clientData);
        $this->addEvent($event);
        return $event;
    }


    public function deleteSocketEventLoop($fd, $mask): void
    {
        foreach ($this->eventContainer as $key => $event) {
            if ($event->fd == $fd && $event->mask == $mask) {
                unset($this->eventContainer[$key]);
            }
        }
    }

}
<?php

class SocketEvent
{
    public Socket $fd;
    public int $mask;
    public $fileProc = null;
    public $finalizerProc = null;
    public ?Client $clientData = null;

    /**
     * @param EventLoop $eventLoop
     * @param Socket $fd
     * @param int $mask
     * @param callable $fileProc
     * @param callable|null $finalizerProc
     * @param Client|null $clientData
     */
    public function __construct(Socket $fd, int $mask, callable $fileProc, callable $finalizerProc = null, Client $clientData = null)
    {
        $this->fd = $fd;
        $this->mask = $mask;
        $this->fileProc = $fileProc;
        $this->finalizerProc = $finalizerProc;
        $this->clientData = $clientData;
    }

}
<?php

class Client
{
    public Socket $fd;
    public string $queryBuf;
}
<?php

ini_set('display_errors', 'On');
error_reporting(E_ALL | E_WARNING);

include "src/TcpSocket.php";
include "src/SocketEvent.php";
include 'src/EventLoop.php';
include "src/Client.php";

$socket = new TcpSocket('127.0.0.1', 8888);
$socketFd = $socket->getTcpServer();

$eventLoop = new EventLoop();
$sendReplyToClient = function (EventLoop $eventLoop, $socketFd, Client $clientData, int $mask) {
    socket_write($socketFd, $clientData->queryBuf, 1024);
    //发送数据完毕后关闭 socket 写事件,要不然就会一直在玄幻
    $eventLoop->deleteSocketEventLoop($socketFd, $mask);
};
$readQueryHandler = function (EventLoop $eventLoop, $socketFd, Client $clientData, int $mask) use ($sendReplyToClient) {
    $data = socket_read($socketFd, 1024);

    if (strlen($data) == 0) {
        echo "Client closed connection" . PHP_EOL;
        $eventLoop->deleteSocketEventLoop($socketFd, AE_READABLE);
        $eventLoop->deleteSocketEventLoop($socketFd, AE_WRITABLE);
        return;
    }

    if ($data) {
        $data = trim($data);
        $clientData->queryBuf = $data;
        $eventLoop->createSocketEvent($socketFd, AE_WRITABLE, $sendReplyToClient, null, $clientData);
    }
    return;
};

$acceptHandler = function (EventLoop $eventLoop, $socketFd) use ($readQueryHandler) {
    $clientFD = socket_accept($socketFd);
    if (!$clientFD) {
        echo "socket_accept_error:" . socket_strerror(socket_last_error()) . PHP_EOL;
        socket_close($socketFd);
        exit();
    }
    socket_set_nonblock($clientFD);
    socket_getpeername($clientFD, $addr, $port);
    echo "Accepted $addr $port" . PHP_EOL;
    $clientData = new Client();
    $clientData->fd = $clientFD;
    $clientData->queryBuf = '';
    $eventLoop->createSocketEvent($clientFD, AE_READABLE, $readQueryHandler, null, $clientData);
};

$eventLoop->createSocketEvent($socketFd, AE_READABLE, $acceptHandler, null, null);
$eventLoop->run();

2023-03-17T13:05:55.png

单 Reactor 单进程的方案因为全部工作都在同一个进程内完成,所以实现起来比较简单,不需要考虑进程间通信,也不用担心多进程竞争。

但是,这种方案存在 2 个缺点:

  • 第一个缺点,因为只有一个进程,无法充分利用 多核 CPU 的性能;
  • 第二个缺点,Handler 对象在业务处理时,整个进程是无法处理其他连接的事件的,如果业务处理耗时比较长,那么就造成响应的延迟;

所以,单 Reactor 单进程的方案不适用计算机密集型的场景,只适用于业务处理非常快速的场景。

Redis 是由 C 语言实现的,在 Redis 6.0 版本之前采用的正是「单 Reactor 单进程」的方案,因为 Redis 业务处理主要是在内存中完成,操作的速度是很快的,性能瓶颈不在 CPU 上,所以 Redis 对于命令的处理是单进程的方案。

由于 PHP 核心包内并没有包含多线程的扩展,所以在下面的一张,我们将实现基于单 Reactor 多进程的方案。

安装 K8S 各节点准备工作

配置要求

  • 一台兼容的 Linux 主机。Kubernetes 项目为基于 Debian 和 Red Hat 的 Linux 发行版以及一些不提供包管理器的发行版提供通用的指令。
  • 每台机器 2 GB 或更多的 RAM(如果少于这个数字将会影响你应用的运行内存)。
  • CPU 2 核心及以上。
  • 集群中的所有机器的网络彼此均能相互连接(公网和内网都可以)。
  • 节点之中不可以有重复的主机名、MAC 地址或 product_uuid。请参见这里了解更多详细信息。
  • 开启机器上的某些端口。请参见这里了解更多详细信息。
  • 禁用交换分区。为了保证 kubelet 正常工作,你必须禁用交换分区。

安装 kubeadm | KubernetesFQDN 设置

vim /etc/hosts

# master 节点
192.168.1.10 k8sm1.maksim.website k8sm1
192.168.1.11 k8sm2.maksim.website k8sm2
192.168.1.12 k8sm3.maksim.website k8sm3

# worker 节点
192.168.1.20 k8sw3.maksim.website k8sw3
192.168.1.21 k8sw3.maksim.website k8sw3
192.168.1.22 k8sw3.maksim.website k8sw3
192.168.1.23 k8sw3.maksim.website k8sw3

# 在各个节点上设置
hostnamectl set-hostname k8sm1

安装依赖软件

# 备份原来的yum源
$ > sudo yum install wget net-tools nfs-utils lrzsz gcc gcc-c++ make cmake libxml2-devel openssl-devel curl curl-devel unzip sudo ntp libaio-devel wget vim ncurses-devel autoconf automake zlib-devel python-devel epel-release openssh-server socat ipvsadm conntrack ntpdate -y

关闭firewalld防火墙

 systemctl stop firewalld && systemctl disable firewalld

安装iptables

# 安装iptables
yum install iptables-services -y
# 禁用iptables
service iptables stop  && systemctl disable iptables

时间同步

$ > ntpdate cn.pool.ntp.org
# 6.2 编辑计划任务,每小时做一次同步

$ > crontab -e

* */1 * * * /usr/sbin/ntpdate  cn.pool.ntp.org

$ > service crond restart

关闭selinux,各个节点操作

设置永久关闭,这样重启机器selinux也处于关闭状态

sed -i 's/SELINUX=enforcing/SELINUX=disabled/' /etc/sysconfig/selinux

sed -i 's/SELINUX=enforcing/SELINUX=disabled/g' /etc/selinux/config

上面文件修改之后,需要重启虚拟机,可以强制重启:

reboot -f

关闭交换分区,各个节点操作

这个当内存不足时,linux会自动使用swap,将部分内存数据存放到磁盘中,这个这样会使性能下降,为了性能考虑推荐关掉,而在安装 k8s 的时候,如果开启了交换分区,k8s 会进行报错。

# 临时关闭
$ > swapoff  -a

# 永久禁用,打开/etc/fstab注释掉swap那一行。
$ > sed -i 's/.*swap.*/#&/' /etc/fstab

设置网桥包经IPTables,core文件生成路径,配置永久生效

echo 1 > /proc/sys/net/bridge/bridge-nf-call-iptables
echo 1 >/proc/sys/net/bridge/bridge-nf-call-ip6tables
echo """
vm.swappiness = 0
net.bridge.bridge-nf-call-iptables = 1
net.ipv4.ip_forward = 1
net.bridge.bridge-nf-call-ip6tables = 1
""" > /etc/sysctl.conf
sysctl -p

开启ipvs

不开启ipvs将会使用iptables,但是效率低,所以官网推荐需要开通ipvs内核

cat > /etc/sysconfig/modules/ipvs.modules <<EOF
#!/bin/bash

ipvs_modules="ip_vs ip_vs_lc ip_vs_wlc ip_vs_rr ip_vs_wrr ip_vs_lblc ip_vs_lblcr ip_vs_dh ip_vs_sh ip_vs_fo ip_vs_nq ip_vs_sed ip_vs_ftp nf_conntrack"

for kernel_module in \${ipvs_modules}; do

 /sbin/modinfo -F filename \${kernel_module} > /dev/null 2>&1

 if [ $? -eq 0 ]; then

 /sbin/modprobe \${kernel_module}

 fi
done

EOF

# 执行
chmod 755 /etc/sysconfig/modules/ipvs.modules && bash /etc/sysconfig/modules/ipvs.modules && lsmod | grep ip_vs

Referer