fea: support bgp as external.

This commit is contained in:
Daniel Ding
2025-09-01 17:08:47 +08:00
parent 80ea5c26b4
commit 33518bf8e5
22 changed files with 610 additions and 55 deletions

106
cmd/api/v5/bgp.go Normal file
View File

@@ -0,0 +1,106 @@
package v5
import (
"github.com/luscis/openlan/cmd/api"
"github.com/urfave/cli/v2"
)
// openlan bgp enable --route-id 1.1.1.1 --as-path 30
// openlan bgp disable
// openlan bgp add neighbor --address 1.1.1.2 --remote-as 32
type BGP struct {
Cmd
}
func (b BGP) Url(prefix string) string {
return prefix + "/api/bgp"
}
func (b BGP) List(c *cli.Context) error {
return nil
}
func (b BGP) Enable(c *cli.Context) error {
return nil
}
func (b BGP) Disable(c *cli.Context) error {
return nil
}
func (b BGP) Commands(app *api.App) {
app.Command(&cli.Command{
Name: "bgp",
Usage: "External BGP",
Subcommands: []*cli.Command{
{
Name: "ls",
Usage: "Display bgp",
Aliases: []string{"ls"},
Action: b.List,
},
{
Name: "enable",
Usage: "Enable bgp",
Action: b.Enable,
},
{
Name: "disable",
Usage: "Disable bgp",
Action: b.Disable,
},
Neighbor{}.Commands(),
},
})
}
type Neighbor struct {
Cmd
}
func (s Neighbor) Url(prefix string) string {
return prefix + "/api/bgp/neighbor/"
}
func (s Neighbor) Add(c *cli.Context) error {
url := s.Url(c.String("url"))
clt := s.NewHttp(c.String("token"))
if err := clt.PostJSON(url, nil, nil); err != nil {
return err
}
return nil
}
func (s Neighbor) Remove(c *cli.Context) error {
url := s.Url(c.String("url"))
clt := s.NewHttp(c.String("token"))
if err := clt.DeleteJSON(url, nil, nil); err != nil {
return err
}
return nil
}
func (s Neighbor) Commands() *cli.Command {
return &cli.Command{
Name: "neighbor",
Usage: "BGP neighbor",
Subcommands: []*cli.Command{
{
Name: "add",
Usage: "Add BGP neighbor",
Action: s.Add,
},
{
Name: "remove",
Aliases: []string{"rm"},
Usage: "Remove BGP neighbor",
Action: s.Remove,
},
},
}
}

View File

@@ -41,4 +41,5 @@ func Commands(app *api.App) {
ZTrust{}.Commands(app)
Rate{}.Commands(app)
Ceci{}.Commands(app)
BGP{}.Commands(app)
}

View File

@@ -206,12 +206,12 @@ func (s SNAT) Commands() *cli.Command {
Subcommands: []*cli.Command{
{
Name: "enable",
Usage: "enable snat",
Usage: "Enable snat",
Action: s.Enable,
},
{
Name: "disable",
Usage: "disable snat",
Usage: "Disable snat",
Action: s.Disable,
},
},

View File

@@ -1,41 +0,0 @@
bgpd=yes
ospfd=no
ospf6d=no
ripd=no
ripngd=no
isisd=no
pimd=no
pim6d=no
ldpd=no
nhrpd=no
eigrpd=no
babeld=no
sharpd=no
pbrd=no
bfdd=no
fabricd=no
vrrpd=no
pathd=no
vtysh_enable=yes
zebra_options=" -A 127.0.0.1 --limit-fds=100000 -s 90000000 --limit-fds=100000"
mgmtd_options=" -A 127.0.0.1 --limit-fds=100000"
bgpd_options=" -A 127.0.0.1 --limit-fds=100000"
ospfd_options=" -A 127.0.0.1 --limit-fds=100000"
ospf6d_options=" -A ::1 --limit-fds=100000"
ripd_options=" -A 127.0.0.1 --limit-fds=100000"
ripngd_options=" -A ::1 --limit-fds=100000"
isisd_options=" -A 127.0.0.1 --limit-fds=100000"
pimd_options=" -A 127.0.0.1 --limit-fds=100000"
pim6d_options=" -A ::1 --limit-fds=100000"
ldpd_options=" -A 127.0.0.1 --limit-fds=100000"
nhrpd_options=" -A 127.0.0.1 --limit-fds=100000"
eigrpd_options=" -A 127.0.0.1 --limit-fds=100000"
babeld_options=" -A 127.0.0.1 --limit-fds=100000"
sharpd_options=" -A 127.0.0.1 --limit-fds=100000"
pbrd_options=" -A 127.0.0.1 --limit-fds=100000"
staticd_options="-A 127.0.0.1 --limit-fds=100000"
bfdd_options=" -A 127.0.0.1 --limit-fds=100000"
fabricd_options="-A 127.0.0.1 --limit-fds=100000"
vrrpd_options=" -A 127.0.0.1 --limit-fds=100000"
pathd_options=" -A 127.0.0.1 --limit-fds=100000"

View File

@@ -0,0 +1,25 @@
!
ip prefix-list 192.168.11.252-out seq 10 permit 192.168.44.0/24
ip prefix-list 192.168.11.252-out seq 11 permit 192.168.45.0/24
!
router bgp 1
bgp router-id 192.168.11.254
no bgp default ipv4-unicast
neighbor 192.168.11.252 remote-as 2
!
address-family ipv4 unicast
redistribute connected
redistribute kernel
neighbor 192.168.11.252 activate
neighbor 192.168.11.252 route-map 192.168.11.252-in in
neighbor 192.168.11.252 route-map 192.168.11.252-out out
exit-address-family
exit
!
route-map 192.168.11.252-in permit 10
exit
!
route-map 192.168.11.252-out permit 10
match ip address prefix-list 192.168.11.252-out
exit
!

View File

@@ -0,0 +1,25 @@
!
ip prefix-list 192.168.11.254-out.1 seq 5 permit 192.168.33.0/24
!
router bgp 2
bgp router-id 192.168.11.252
no bgp default ipv4-unicast
neighbor 192.168.11.254 remote-as 1
!
address-family ipv4 unicast
redistribute connected
redistribute kernel
neighbor 192.168.11.254 activate
neighbor 192.168.11.254 route-map 192.168.11.254-in in
neighbor 192.168.11.254 route-map 192.168.11.254-out out
exit-address-family
exit
!
route-map 192.168.11.254-out permit 10
match ip address prefix-list 192.168.11.254-out.1
exit
!
route-map 192.168.11.254-in permit 10
exit
!
end

41
dist/rootfs/etc/openlan/frr/daemons vendored Normal file
View File

@@ -0,0 +1,41 @@
bgpd=yes
ospfd=no
ospf6d=no
ripd=no
ripngd=no
isisd=no
pimd=no
pim6d=no
ldpd=no
nhrpd=no
eigrpd=no
babeld=no
sharpd=no
pbrd=no
bfdd=no
fabricd=no
vrrpd=no
pathd=no
vtysh_enable=yes
zebra_options=" -A 127.0.0.1 -s 90000000"
mgmtd_options=" -A 127.0.0.1"
bgpd_options=" -A 127.0.0.1"
ospfd_options=" -A 127.0.0.1"
ospf6d_options=" -A ::1"
ripd_options=" -A 127.0.0.1"
ripngd_options=" -A ::1"
isisd_options=" -A 127.0.0.1"
pimd_options=" -A 127.0.0.1"
pim6d_options=" -A ::1"
ldpd_options=" -A 127.0.0.1"
nhrpd_options=" -A 127.0.0.1"
eigrpd_options=" -A 127.0.0.1"
babeld_options=" -A 127.0.0.1"
sharpd_options=" -A 127.0.0.1"
pbrd_options=" -A 127.0.0.1"
staticd_options="-A 127.0.0.1"
bfdd_options=" -A 127.0.0.1"
fabricd_options="-A 127.0.0.1"
vrrpd_options=" -A 127.0.0.1"
pathd_options=" -A 127.0.0.1"

View File

@@ -0,0 +1,14 @@
{
"name": "bgp",
"provider": "bgp",
"specifies": {
"localas": 30,
"routeid": "192.168.11.1",
"neighbors": [
{
"remoteas": 32,
"address": "192.168.11.11"
}
]
}
}

33
dist/rootfs/var/openlan/script/frr-reload vendored Executable file
View File

@@ -0,0 +1,33 @@
#!/usr/bin/env python3
import socket
import logging
SOCKET_PATH = "/var/openlan/frr/reloader.sock"
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s [%(levelname)s] %(message)s',
datefmt='%Y-%m-%d %H:%M:%S',
)
logger = logging.getLogger(__name__)
def client():
client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
try:
client.connect(SOCKET_PATH)
message = "reload"
logger.debug(f"Sent action: {message}")
client.send(message.encode('utf-8'))
response = client.recv(1024).decode('utf-8')
logger.debug(f"Received response: {response}")
except Exception as e:
logger.error(f"Error: {e}")
finally:
client.close()
if __name__ == "__main__":
client()

65
dist/rootfs/var/openlan/script/frr-server vendored Executable file
View File

@@ -0,0 +1,65 @@
#!/usr/bin/env python3
import socket
import os
import logging
SOCKET_PATH = "/var/openlan/frr/reloader.sock"
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s [%(levelname)s] %(message)s',
datefmt='%Y-%m-%d %H:%M:%S',
)
logger = logging.getLogger(__name__)
def handle_reload():
code = os.system("/usr/lib/frr/frr-reload")
if code == 0:
return "success"
return "fail"
def handle_client(server):
try:
client, _ = server.accept()
with client:
data = client.recv(1024)
message = data.decode('utf-8')
logger.debug(f"Received action: {message}")
response = "unsupport"
if message == "reload":
response = handle_reload()
client.send(response.encode('utf-8'))
logger.debug(f"Sent response: {response}")
client.close()
except Exception as e:
logger.error(f"Error handling client: {e}")
def start_server():
if os.path.exists(SOCKET_PATH):
os.unlink(SOCKET_PATH)
server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
try:
server.bind(SOCKET_PATH)
server.listen(5)
logger.info(f"Start server at {SOCKET_PATH}")
while True:
handle_client(server)
except Exception as e:
logger.error(f"Server error: {e}")
finally:
server.close()
if os.path.exists(SOCKET_PATH):
os.unlink(SOCKET_PATH)
if __name__ == "__main__":
try:
start_server()
except KeyboardInterrupt:
logger.info("\nServer terminated by user")

24
dist/rootfs/var/openlan/script/frr.sh vendored Executable file
View File

@@ -0,0 +1,24 @@
#!/bin/bash
sed -i 's/is\ not\ "/\!=\ "/' /usr/lib/frr/frr-reload.py
if [ ! -e /var/run/frr ]; then
mkdir -p /var/run/frr
chown frr:frr /var/run/frr
fi
for file in daemons frr.conf vtysh.conf; do
if [ -e "/etc/frr/$file" ]; then
continue
fi
if [ -e "/var/openlan/frr/$file" ]; then
cp -rvf /var/openlan/frr/$file /etc/frr/$file
fi
done
# Start reloader server for FRR
exec /var/openlan/script/frr-server &
# Start daemons
source /usr/lib/frr/frrcommon.sh
/usr/lib/frr/watchfrr $(daemon_list)

View File

@@ -33,11 +33,11 @@ function requires() {
yum update -y
yum install -y epel-release
yum install -y openssl net-tools iptables iputils iperf3 tcpdump
yum install -y openvpn dnsmasq bridge-utils ipset procps wget
yum install -y openvpn dnsmasq bridge-utils ipset procps wget frr
elif [ "$sys"x == "debian"x ]; then
apt-get update -y
apt install -y net-tools iptables iproute2 tcpdump ca-certificates iperf3
apt install -y openvpn dnsmasq bridge-utils ipset procps wget iputils-ping
apt install -y openvpn dnsmasq bridge-utils ipset procps wget iputils-ping frr
fi
## Install libreswan from github.
if [ "$sys"x == "redhat"x ]; then

View File

@@ -11,12 +11,13 @@ services:
- /opt/openlan/run/pluto:/run/pluto
frr:
restart: always
image: "quay.io/frrouting/frr:10.2.1"
image: "luscis/openlan:latest.x86_64"
network_mode: host
privileged: true
entrypoint: ["/usr/lib/frr/docker-start"]
entrypoint: ["/var/openlan/script/frr.sh"]
volumes:
- /opt/openlan/etc/frr:/etc/frr
- /opt/openlan/var/openlan/frr:/var/openlan/frr
switch:
restart: always
image: "luscis/openlan:latest.x86_64"
@@ -27,8 +28,11 @@ services:
- /opt/openlan/etc/openlan:/etc/openlan
- /opt/openlan/etc/ipsec.d:/etc/ipsec.d
- /opt/openlan/run/pluto:/run/pluto
- /opt/openlan/etc/frr:/etc/frr
- /opt/openlan/var/openlan/frr:/var/openlan/frr
depends_on:
- ipsec
- frr
proxy:
restart: always
image: "luscis/openlan:latest.x86_64"

View File

@@ -125,9 +125,18 @@ type IPSecer interface {
ListTunnels(call func(obj schema.IPSecTunnel))
}
type Bgper interface {
Enable(data schema.Bgp)
Disable()
AddNeighbor(data schema.BgpNeighbor)
DelNeighbor(data schema.BgpNeighbor)
ListNeighbor(call func(obj schema.BgpNeighbor))
}
type APICall struct {
workers map[string]Networker
secer IPSecer
bgper Bgper
workers map[string]Networker
}
func (i *APICall) AddWorker(name string, obj Networker) {
@@ -152,6 +161,14 @@ func (i *APICall) GetIPSecer() IPSecer {
return i.secer
}
func (i *APICall) SetBgper(value Bgper) {
i.bgper = value
}
func (i *APICall) GetBgper() Bgper {
return i.bgper
}
var Call = &APICall{
workers: make(map[string]Networker),
}

59
pkg/config/bgp.go Normal file
View File

@@ -0,0 +1,59 @@
package config
import "fmt"
type BgpNeighbor struct {
Name string `json:"-" yaml:"-"`
Address string `json:"address" yaml:"address"`
RemoteAs int `json:"remoteas" yaml:"remoteas"`
Secret string `json:"secret,omitempty" yaml:"secret,omitempty"`
State string `json:"state,omitempty" yaml:"state,omitempty"`
}
func (s *BgpNeighbor) Correct() {
}
func (s *BgpNeighbor) Id() string {
return fmt.Sprintf("%s", s.Address)
}
type BgpSpecifies struct {
Name string `json:"-" yaml:"-"`
LocalAs int `json:"localas" yaml:"localas"`
RouteId string `json:"routeid" yaml:"routeid"`
Neighbors []*BgpNeighbor `json:"neighbors" yaml:"neighbors"`
}
func (s *BgpSpecifies) Correct() {
if s.Neighbors == nil {
s.Neighbors = make([]*BgpNeighbor, 0)
}
for _, t := range s.Neighbors {
t.Correct()
}
}
func (s *BgpSpecifies) FindNeighbor(value *BgpNeighbor) (*BgpNeighbor, int) {
for index, obj := range s.Neighbors {
if obj.Id() == value.Id() {
return obj, index
}
}
return nil, -1
}
func (s *BgpSpecifies) AddNeighbor(value *BgpNeighbor) bool {
_, find := s.FindNeighbor(value)
if find == -1 {
s.Neighbors = append(s.Neighbors, value)
}
return find == -1
}
func (s *BgpSpecifies) DelNeighbor(value *BgpNeighbor) (*BgpNeighbor, bool) {
obj, find := s.FindNeighbor(value)
if find != -1 {
s.Neighbors = append(s.Neighbors[:find], s.Neighbors[find+1:]...)
}
return obj, find != -1
}

View File

@@ -30,12 +30,14 @@ type Network struct {
FindHop map[string]*FindHop `json:"findhop,omitempty" yaml:"findhop,omitempty"`
}
func (n *Network) NewSpecifies() interface{} {
func (n *Network) NewSpecifies() any {
switch n.Provider {
case "ipsec":
n.Specifies = &IPSecSpecifies{}
case "router":
n.Specifies = &RouterSpecifies{}
case "bgp":
n.Specifies = &BgpSpecifies{}
default:
n.Specifies = nil
}
@@ -64,6 +66,12 @@ func (n *Network) Correct(sw *Switch) {
obj.Correct()
obj.Name = n.Name
}
case "bgp":
spec := n.Specifies
if obj, ok := spec.(*BgpSpecifies); ok {
obj.Correct()
obj.Name = n.Name
}
default:
br := n.Bridge
br.Network = n.Name

13
pkg/schema/bgp.go Normal file
View File

@@ -0,0 +1,13 @@
package schema
type BgpNeighbor struct {
Address string `json:"address"`
RemoteAs int `json:"remoteas"`
State string `json:"state"`
}
type Bgp struct {
LocalAs int `json:"localas"`
RouteId string `json:"routeid"`
Neighbors []BgpNeighbor `json:"neighbors"`
}

154
pkg/switch/bgp_linux.go Normal file
View File

@@ -0,0 +1,154 @@
package cswitch
import (
"os/exec"
"text/template"
"github.com/luscis/openlan/pkg/api"
co "github.com/luscis/openlan/pkg/config"
"github.com/luscis/openlan/pkg/libol"
"github.com/luscis/openlan/pkg/schema"
)
const (
BgpBin = "/var/openlan/script/frr-reload"
BgpEtc = "/etc/frr/frr.conf"
)
type BgpWorker struct {
*WorkerImpl
spec *co.BgpSpecifies
}
func NewBgpWorker(c *co.Network) *BgpWorker {
w := &BgpWorker{
WorkerImpl: NewWorkerApi(c),
}
w.spec, _ = c.Specifies.(*co.BgpSpecifies)
return w
}
var BgpTmpl = `! GENERATE BY OPENALN
{{- range .Neighbors }}
!
ip prefix-list {{ .Address }}-out seq 10 permit any
ip prefix-list {{ .Address }}-in seq 10 permit any
{{- end }}
!
router bgp {{ .LocalAs }}
bgp router-id {{ .RouteId }}
no bgp default ipv4-unicast
{{- range .Neighbors }}
neighbor {{ .Address }} remote-as {{ .RemoteAs }}
{{- end }}
!
address-family ipv4 unicast
redistribute connected
redistribute kernel
{{- range .Neighbors }}
neighbor {{ .Address }} activate
neighbor {{ .Address }} route-map {{ .Address }}-in in
neighbor {{ .Address }} route-map {{ .Address }}-out out
{{- end }}
exit-address-family
exit
{{- range .Neighbors }}
!
route-map {{ .Address }}-in permit 10
match ip address prefix-list {{ .Address }}-in
exit
{{- end }}
{{- range .Neighbors }}
!
route-map {{ .Address }}-out permit 10
match ip address prefix-list {{ .Address }}-out
exit
{{- end }}
!
`
func (w *BgpWorker) Initialize() {
w.out.Info("BgpWorker.Initialize")
}
func (w *BgpWorker) save() {
file := BgpEtc
out, err := libol.CreateFile(file)
if err != nil || out == nil {
return
}
defer out.Close()
if obj, err := template.New("main").Parse(BgpTmpl); err != nil {
w.out.Warn("BgpWorker.save: %s", err)
} else {
if err := obj.Execute(out, w.spec); err != nil {
w.out.Warn("BgpWorker.save: %s", err)
}
}
}
func (w *BgpWorker) reload() {
w.save()
cmd := exec.Command(BgpBin)
if err := cmd.Run(); err != nil {
w.out.Warn("BgpWorker.reload: %s", err)
return
}
}
func (w *BgpWorker) Start(v api.Switcher) {
w.uuid = v.UUID()
w.out.Info("BgpWorker.Start")
w.reload()
}
func (w *BgpWorker) Stop() {
w.out.Info("BgpWorker.Stop")
}
func (w *BgpWorker) Enable(data schema.Bgp) {
w.spec.LocalAs = data.LocalAs
w.spec.RouteId = data.RouteId
w.reload()
}
func (w *BgpWorker) Disable() {
}
func (w *BgpWorker) Reload(v api.Switcher) {
w.Stop()
w.Initialize()
w.Start(v)
}
func (w *BgpWorker) AddNeighbor(data schema.BgpNeighbor) {
cfg := &co.BgpNeighbor{
Address: data.Address,
RemoteAs: data.RemoteAs,
}
cfg.Correct()
if w.spec.AddNeighbor(cfg) {
w.reload()
}
}
func (w *BgpWorker) DelNeighbor(data schema.BgpNeighbor) {
cfg := &co.BgpNeighbor{
Address: data.Address,
RemoteAs: data.RemoteAs,
}
cfg.Correct()
if _, removed := w.spec.DelNeighbor(cfg); removed {
w.reload()
}
}
func (w *BgpWorker) ListNeighbor(call func(obj schema.BgpNeighbor)) {
for _, nei := range w.spec.Neighbors {
obj := schema.BgpNeighbor{
Address: nei.Address,
RemoteAs: nei.RemoteAs,
}
call(obj)
}
}

View File

@@ -143,10 +143,11 @@ func (w *IPSecWorker) startConn(name string) {
func (w *IPSecWorker) restartTunnel(tun *co.IPSecTunnel) {
name := tun.Name
if tun.Transport == "vxlan" {
switch tun.Transport {
case "vxlan":
w.startConn(name + "-c1")
w.startConn(name + "-c2")
} else if tun.Transport == "gre" {
case "gre":
w.startConn(name + "-c1")
}
}
@@ -156,10 +157,11 @@ func (w *IPSecWorker) addTunnel(tun *co.IPSecTunnel) error {
secTmpl := ""
name := tun.Name
if tun.Transport == "vxlan" {
switch tun.Transport {
case "vxlan":
connTmpl = ipsecTmpl["vxlan"]
secTmpl = ipsecTmpl["secret"]
} else if tun.Transport == "gre" {
case "gre":
connTmpl = ipsecTmpl["gre"]
secTmpl = ipsecTmpl["secret"]
}
@@ -192,10 +194,11 @@ func (w *IPSecWorker) Start(v api.Switcher) {
func (w *IPSecWorker) removeTunnel(tun *co.IPSecTunnel) error {
name := tun.Name
if tun.Transport == "vxlan" {
switch tun.Transport {
case "vxlan":
libol.Exec(IPSecBin, "auto", "--delete", "--asynchronous", name+"-c1")
libol.Exec(IPSecBin, "auto", "--delete", "--asynchronous", name+"-c2")
} else if tun.Transport == "gre" {
case "gre":
libol.Exec(IPSecBin, "auto", "--delete", "--asynchronous", name+"-c1")
}
cfile := fmt.Sprintf("%s/%s.conf", IPSecEtcDir, name)

View File

@@ -24,6 +24,10 @@ func NewNetworker(c *co.Network) api.Networker {
secer := NewIPSecWorker(c)
api.Call.SetIPSecer(secer)
obj = secer
case "bgp":
bgper := NewBgpWorker(c)
api.Call.SetBgper(bgper)
obj = bgper
case "router":
obj = NewRouterWorker(c)
default: