docker默认是通过DOCKER链处理forward等规则,因此,系统的常规(input)链对它限制是无效的,简单说就是docker突破了iptables,docker开放的端口不受iptables的限制。(当然,仅仅是input链,但常规我们也只处理input)

我以前是简单的/etc/docker/daemon.json中禁用docker的iptables自动规则管理来解决问题,不过最近遇到一个情况,因为docker应用需要对访问者ip做dns反向解析,因此,禁用了iptables自动规则后,docker无法访问外网(缺乏相应的转发和nat规则),引起反向解析失败。解析失败没事,但一直要到超时错,就是整整60秒才能响应,这个就无法接受了。 * 以前似乎这么做没问题,就这次不行,而且是多台机器上都一样,不知道是不是docker升级了啥的。

既然不能简单关闭docker的iptables托管,那就只能是在系统的iptables层级上想办法了。
查了资料,docker提供了一个DOCKER-USER链,它是前置于docker的,因此,在它上面打主意就可以了。
下面的例子就是,允许特定ip(1.1.1.1)访问docker上的nginx的80端口,除此之外的所有docker端口的访问均被拒绝。(本机,即input不受影响)

root@xxxxxxxx:~# cat /etc/iptables/rules.v4 
*filter
:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [1039:304535]
:DOCKER-USER DROP [0:0]              #docker-user链,它运行于docker链之前,因此可过滤docker的流量。
#常规的规则(input链),根据情况自己写了。这里的话是开放22的访问。
-A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A INPUT -i lo -j ACCEPT
-A INPUT -p tcp -m tcp --dport 22 -j ACCEPT
#DOCK-USER链规则开始
-A DOCKER-USER -o eth0 -p tcp -j ACCEPT   #允许docker的向外访问。 eth0需要改成外网网卡的实际名称。
-A DOCKER-USER -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT  #允许已建立的连接继续通信
-A DOCKER-USER -i docker0 -o docker0 -j ACCEPT             #允许各个docker间的通信
-A DOCKER-USER -i br-+ -o br-+ -j ACCEPT        #允许各个docker间的通信

-A DOCKER-USER ! -i docker0 -s 1.1.1.1 -p tcp -m tcp --dport 80 -j ACCEPT   #默认规则为禁止,所以这个是白名单,允许1.1.1.1访问本地的80端口。     
....    对外开放的服务规则一直加在这里就好了

-A DOCKER-USER ! -i docker0 -p tcp -m tcp -j DROP  #禁止除白名单外的所有请求

COMMIT

104节点全墙了啊,蛮好的
我先想想就这样凑合还是弄个直连ip呢。。

root@work:~# ping blog.yessure.de
PING blog.yessure.de (104.21.16.1) 56(84) bytes of data.
^C
blog.yessure.de ping statistics
2 packets transmitted, 0 received, 100% packet loss, time 1020ms
root@work:~# curl https://blog.yessure.de -v
Trying 104.21.112.1:443...
^C
root@work:~#

//后续:已改成了ovh直连。

这一个月基本这blog都在说ovh,毕竟le-b ks-a随便拿哪个出来都可以说是今年的机王。
le-b不提了,下单几个全部砍单,还是手工买的。
ks-a,6号下单的,本来还有戏,结果,19号晚ovh出bug,一下放了几千个单,好了,全砍被注定了。
最终,拿了一只le-1结束(9.99o,e3 1245v2,32g,2*960g),这blog也光荣的由这机器支撑了。
没劲,ovh一生黑了。至少明年黑五之前不再鸟它了。
经验教训就是,技术再强也打不过魔法啊。

12月主题是啥?圣诞啊。。下周就回国,然后上海自驾到重庆,一路玩过去,当然,主要也是要看看两边的老人,都是风烛残年, 有一年没一年的了。不过blog不会写这些了,所以,就是断更。

那1月呢?1月预约了个手术,菊花残,满地伤. 你的笑容已泛黄~~ 等养好伤然后就春节啦。。 所以,基本继续断更,毕竟我不是妹子,我的菊花没人想看。

blog做不好是有原因的。

ovh.jpg

.org实在太贵了,可以买好几只鸡腿了,用在1ip博客上实在不值。我不缺10刀,我只是不想浪费,勤俭是个好传统。
趁netcup的促销,换de吧,0.11欧元一个月,希望de不要和xyz一样是在搜索引擎的黑名单。想想应该不会,不然德国人要跳的。
啥?老换域名影响seo? 我要个啥seo啊,被google总共收录了40页累计4次点击,bing收录了135页40次点击。要不是我闲得慌加上手里空闲服务器多,我早关站了。

最后再更新一版,因为原始版有个别地方存在缺陷,我怕谁受伤了来砍我。

这一版,主要是解决了原始代码中的一些问题和不足,以前算毛胚,现在算简装吧。 我希望大家不要直接使用这代码而只是借鉴,所以,相应的yaml我就不发了。具体改了些什么以及原因我也不说明了。算是给开发者的福利。

没有版权,随便使用。但尽量请提供原始出处。我这blog还是需要多点外链。

package main

import (
    "bytes"
    "encoding/json"
    "flag"
    "fmt"
    "log"
    "math/rand"
    "net/http"
    "os"
    "strconv"
    "strings"
    "time"

    "github.com/ovh/go-ovh/ovh"
    "gopkg.in/yaml.v2"
)

/*
获得全部可用产品
curl 'https://ca.api.ovh.com/1.0/order/catalog/public/eco?ovhSubsidiary=ASIA'
搜索返回值,查找 "planCode": "25skleb01",获得options
*/

type Config struct {
    App struct {
        Key         string `yaml:"key"`
        Secret      string `yaml:"secret"`
        ConsumerKey string `yaml:"consumer_key"`
        Region      string `yaml:"region"`
        Interval    int    `yaml:"interval"`
    } `yaml:"app"`

    Telegram struct {
        Token  string `yaml:"token"`
        ChatID string `yaml:"chat_id"`
    } `yaml:"telegram"`

    Server struct {
        IAM                string   `yaml:"iam"`
        Zone               string   `yaml:"zone"`
        RequiredPlanCode   string   `yaml:"required_plan_code"`
        RequiredDisk       string   `yaml:"required_disk"`
        RequiredMemory     string   `yaml:"required_memory"`
        RequiredDatacenter string   `yaml:"required_datacenter"`
        PlanName           string   `yaml:"plan_name"`
        AutoPay            bool     `yaml:"autopay"`
        Options            []string `yaml:"options"`
        Coupon             string   `yaml:"coupon"`
    } `yaml:"server"`
}

var config Config

func loadConfig(configPath string) error {
    configFile, err := os.ReadFile(configPath)
    if err != nil {
        return fmt.Errorf("error reading config file: %v", err)
    }

    err = yaml.Unmarshal(configFile, &config)
    if err != nil {
        return fmt.Errorf("error parsing config file: %v", err)
    }

    return nil
}

func runTask() {
    client, err := ovh.NewClient(
        config.App.Region,
        config.App.Key,
        config.App.Secret,
        config.App.ConsumerKey,
    )
    if err != nil {
        log.Printf("Failed to create OVH client: %v\n", err)
        return
    }

    // 获取数据中心可用性
    var result []map[string]interface{}
    url := "https://eu.api.ovh.com/1.0/dedicated/server/datacenter/availabilities?planCode=" + config.Server.RequiredPlanCode
    if config.Server.RequiredDisk != "" {
        url = url + "&storage=" + config.Server.RequiredDisk
    }
    if config.Server.RequiredMemory != "" {
        url = url + "&memory=" + config.Server.RequiredMemory
    }
    resp, err := http.Get(url)
    if err != nil {
        log.Printf("Failed to get datacenter availabilities: %v\n", err)
        return
    }
    defer resp.Body.Close()
    err = json.NewDecoder(resp.Body).Decode(&result)
    if err != nil {
        log.Printf("Failed to decode datacenter availabilities response: %v\n", err)
        return
    }
    foundAvailable := false
    var fqn, planCode, datacenter string

    for _, item := range result {
        if item["planCode"] == config.Server.RequiredPlanCode {
            fqn = item["fqn"].(string)
            planCode = item["planCode"].(string)
            datacenters := item["datacenters"].([]interface{})

            var availableDCs []map[string]interface{}

            for _, dcInfo := range datacenters {
                dc := dcInfo.(map[string]interface{})
                availability := dc["availability"].(string)
                datacenter = dc["datacenter"].(string)

                if datacenter == "sgp" || datacenter == "syd" {
                    //正常是不会到这里的,但为了防止成大冤种,自保一下
                    continue
                }

                log.Printf("FQN: %s\n", fqn)
                log.Printf("Availability: %s\n", availability)
                log.Printf("Datacenter: %s\n", datacenter)
                log.Println("------------------------")

                if availability != "unavailable" {
                    if config.Server.RequiredDatacenter == "" || contains(strings.Split(config.Server.RequiredDatacenter, ","), datacenter) {
                        availableDCs = append(availableDCs, dc)
                    }
                }
            }

            if len(availableDCs) > 0 {
                popi := rand.Intn(len(availableDCs))
                selectedDC := availableDCs[popi]
                datacenter = selectedDC["datacenter"].(string)
                availability := selectedDC["availability"].(string)
                foundAvailable = true
                log.Printf("RECROD FOUND,SELECT:\n")
                log.Printf("FQN: %s\n", fqn)
                log.Printf("Availability: %s\n", availability)
                log.Printf("Datacenter: %s\n", datacenter)
                                break
            }
        }
    }

    if !foundAvailable {
        log.Printf("all out of stock %s\n", planCode)
        return
    }

    msg := fmt.Sprintf("%s: found %s available at %s", config.Server.IAM, config.Server.PlanName, datacenter)
    sendTelegramMsg(config.Telegram.Token, config.Telegram.ChatID, msg)

    // 创建购物车
    log.Println("Create cart")
    var cartResult map[string]interface{}
    err = client.Post("/order/cart", map[string]interface{}{
        "ovhSubsidiary": config.Server.Zone,
    }, &cartResult)
    if err != nil {
        log.Printf("Failed to create cart: %v\n", err)
        return
    }

    cartID := cartResult["cartId"].(string)
    log.Printf("Cart ID: %s\n", cartID)

    // 分配购物车
    log.Println("Assign cart")
    err = client.Post("/order/cart/"+cartID+"/assign", nil, nil)
    if err != nil {
        log.Printf("Failed to assign cart: %v\n", err)
        return
    }

    // 将商品放入购物车
    log.Println("Put item into cart")
    var itemResult map[string]interface{}
    err = client.Post("/order/cart/"+cartID+"/eco", map[string]interface{}{
        "planCode":    planCode,
        "pricingMode": "default",
        "duration":    "P1M",
        "quantity":    1,
    }, &itemResult)
    if err != nil {
        log.Printf("Failed to add item to cart: %v\n", err)
        return
    }

    var itemID string
    if v, ok := itemResult["itemId"].(json.Number); ok {
        itemID = v.String()
    } else if v, ok := itemResult["itemId"].(string); ok {
        itemID = v
    } else {
        log.Printf("Unexpected type for itemId, expected json.Number or string, got %T\n", itemResult["itemId"])
        return
    }

    log.Printf("Item ID: %s\n", itemID)

    // 检查所需配置
    log.Println("Checking required configuration")
    var requiredConfig []map[string]interface{}
    err = client.Get("/order/cart/"+cartID+"/item/"+itemID+"/requiredConfiguration", &requiredConfig)
    if err != nil {
        log.Printf("Failed to get required configuration: %v\n", err)
        return
    }

    dedicatedOs := "none_64.en"
    var regionValue string
    for _, config := range requiredConfig {
        if config["label"] == "region" {
            if allowedValues, ok := config["allowedValues"].([]interface{}); ok && len(allowedValues) > 0 {
                regionValue = allowedValues[0].(string)
            }
        }
    }

    // 配置数据中心、操作系统和区域
    configurations := []map[string]interface{}{
        {"label": "dedicated_datacenter", "value": datacenter},
        {"label": "dedicated_os", "value": dedicatedOs},
        {"label": "region", "value": regionValue},
    }

    for _, conf := range configurations {
        log.Printf("Configure %s\n", conf["label"])
        err = client.Post("/order/cart/"+cartID+"/item/"+itemID+"/configuration", map[string]interface{}{
            "label": conf["label"],
            "value": conf["value"],
        }, nil)
        if err != nil {
            log.Printf("Failed to configure %s: %v\n", conf["label"], err)
            return
        }
    }

    // 添加选项
    log.Println("Add options")

    itemIDInt, _ := strconv.Atoi(itemID)
    for _, option := range config.Server.Options {
        err = client.Post("/order/cart/"+cartID+"/eco/options", map[string]interface{}{
            "duration":    "P1M",
            "itemId":      itemIDInt,
            "planCode":    option,
            "pricingMode": "default",
            "quantity":    1,
        }, nil)
        if err != nil {
            log.Printf("Failed to add option %s: %v\n", option, err)
            return
        }
    }

    //有优惠码
    if config.Server.Coupon != "" {
        log.Println("Apply coupon")
        client.Post("/order/cart/"+cartID+"/coupon", map[string]interface{}{
            "coupon": config.Server.Coupon,
        }, nil)
        //不处理返回,不管是否成功均需要正常结帐
    }

    // 结账
    log.Println("Checkout")
    var checkoutResult map[string]interface{}
    err = client.Get("/order/cart/"+cartID+"/checkout", &checkoutResult)
    if err != nil {
        log.Printf("Failed to get checkout: %v\n", err)
        return
    }

    err = client.Post("/order/cart/"+cartID+"/checkout", map[string]interface{}{
        "autoPayWithPreferredPaymentMethod": config.Server.AutoPay,
        "waiveRetractationPeriod":           true,
    }, nil)
    if err != nil {
        log.Printf("Failed to checkout: %v\n", err)
        return
    }
    log.Println("Bingo!")
    os.Exit(0)
}

func sendTelegramMsg(botToken, chatID, message string) error {
    url := fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", botToken)
    payload := map[string]string{
        "chat_id": chatID,
        "text":    message,
    }

    jsonData, err := json.Marshal(payload)
    if err != nil {
        return fmt.Errorf("error encoding JSON: %v", err)
    }

    resp, err := http.Post(url, "application/json", bytes.NewBuffer(jsonData))
    if err != nil {
        return fmt.Errorf("error sending request: %v", err)
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {
        return fmt.Errorf("received non-OK response status: %v", resp.Status)
    }

    return nil
}

func contains(slice []string, item string) bool {
    normalizedItem := strings.ToLower(strings.TrimSpace(item))
    for _, v := range slice {
        if strings.ToLower(strings.TrimSpace(v)) == normalizedItem {
            return true
        }
    }
    return false
}

func main() {
    var configPath string
    flag.StringVar(&configPath, "config", "", "path to config file (required)")
    flag.Parse()

    if configPath == "" {
        flag.Usage()
        log.Fatal("config file path is required")
    }

    err := loadConfig(configPath)
    if err != nil {
        log.Fatalf("Failed to load config: %v", err)
    }

    if !config.Server.AutoPay {
        log.Println("WARNNING: NO AUTOPAY")
    }

    if config.App.Interval == 0 {
        config.App.Interval = 10
    }

    for {
        runTask()
        time.Sleep(time.Duration(config.App.Interval) * time.Second)
    }
}