/*
Copyright © 2022 Alessio Greggi
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
"golang.org/x/sync/errgroup"
c "github.com/alegrey91/fwdctl/internal/constants"
"github.com/alegrey91/fwdctl/internal/rules"
iptables "github.com/alegrey91/fwdctl/pkg/iptables"
)
// applyCmd represents the apply command
var applyCmd = &cobra.Command{
Use: "apply",
Short: "apply rules from file",
Long: `apply rules described in a configuration file`,
Example: c.ProgramName + " apply --file rule.yml",
RunE: func(cmd *cobra.Command, args []string) error {
rulesContent, err := os.Open(c.RulesFile)
if err != nil {
return fmt.Errorf("opening file: %v", err)
}
ruleSet, err := rules.NewRuleSetFromFile(rulesContent)
if err != nil {
return fmt.Errorf("unable to open rules file: %v", err)
}
ipt, err := iptables.NewIPTablesInstance()
if err != nil {
return fmt.Errorf("unable to get iptables instance: %v", err)
}
g := new(errgroup.Group)
rulesFileIsValid := true
g.SetLimit(10)
for _, rule := range ruleSet.Rules {
r := &rule
g.Go(func() error {
err := ipt.ValidateForward(r)
if err != nil {
rulesFileIsValid = false
}
return err
})
}
if err := g.Wait(); err != nil {
return fmt.Errorf("validating rule: %v", err)
}
if rulesFileIsValid {
for ruleId, rule := range ruleSet.Rules {
if err := ipt.CreateForward(&rule); err != nil {
return fmt.Errorf("applying rule (%s): %v", ruleId, err)
}
}
}
return nil
},
}
func init() {
rootCmd.AddCommand(applyCmd)
applyCmd.Flags().StringVarP(&c.RulesFile, "file", "f", "rules.yml", "rules file")
}
/*
Copyright © 2022 Alessio Greggi
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cmd
import (
"fmt"
c "github.com/alegrey91/fwdctl/internal/constants"
iptables "github.com/alegrey91/fwdctl/pkg/iptables"
"github.com/spf13/cobra"
)
var (
iface string
proto string
dport int
saddr string
sport int
)
// createCmd represents the create command
var createCmd = &cobra.Command{
Use: "create",
Aliases: []string{"add"},
SuggestFor: []string{},
Short: "Create forward using IPTables util",
Long: `Create forward rule using IPTables util under the hood.
This is really useful in case you need to forward
the traffic from an internal virtual machine inside
your hypervisor, to external.
+----------------------------+
| +-----------+ |
| | | |
| +-----+:80 VM | |
| | | | |
=:3000<--+ +-----------+ |
| Hypervisor |
+----------------------------+
`,
Example: c.ProgramName + " create -d 3000 -s 192.168.199.105 -p 80",
RunE: func(cmd *cobra.Command, args []string) error {
ipt, err := iptables.NewIPTablesInstance()
if err != nil {
return fmt.Errorf("unable to get iptables instance: %v", err)
}
rule := iptables.NewRule(iface, proto, dport, saddr, sport)
if err := ipt.CreateForward(rule); err != nil{
return fmt.Errorf("creating new rule: %v", err)
}
return nil
},
}
func init() {
rootCmd.AddCommand(createCmd)
createCmd.Flags().StringVarP(&iface, "interface", "i", "lo", "interface name")
createCmd.Flags().StringVarP(&proto, "proto", "P", "tcp", "protocol")
createCmd.Flags().IntVarP(&dport, "destination-port", "d", 0, "destination port")
_ = createCmd.MarkFlagRequired("destination-port")
createCmd.Flags().StringVarP(&saddr, "source-address", "s", "", "source address")
_ = createCmd.MarkFlagRequired("source-address")
createCmd.Flags().IntVarP(&sport, "source-port", "p", 0, "source port")
_ = createCmd.MarkFlagRequired("source-port")
}
/*
Copyright © 2023 Alessio Greggi
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cmd
import (
"github.com/spf13/cobra"
)
// daemonCmd represents the daemon command
var daemonCmd = &cobra.Command{
Use: "daemon",
Short: "Run fwdctl as deamon",
Run: func(cmd *cobra.Command, args []string) {
_ = cmd.Help()
},
}
func init() {
rootCmd.AddCommand(daemonCmd)
}
/*
Copyright © 2022 Alessio Greggi
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cmd
import (
"fmt"
"os"
c "github.com/alegrey91/fwdctl/internal/constants"
"github.com/alegrey91/fwdctl/internal/daemon"
iptables "github.com/alegrey91/fwdctl/pkg/iptables"
"github.com/spf13/cobra"
)
// daemonStartCmd represents the daemon command
var daemonStartCmd = &cobra.Command{
Use: "start",
Short: "Start fwdctl daemon",
Long: ``,
RunE: func(cmd *cobra.Command, args []string) error{
ipt, err := iptables.NewIPTablesInstance()
if err != nil {
return fmt.Errorf("unable to get iptables instance: %v", err)
}
rulesFile, err := cmd.Flags().GetString("file")
if err != nil {
return fmt.Errorf("unable to read from flag: %v", err)
}
if res := daemon.Start(ipt, rulesFile); res != 0 {
os.Exit(1)
}
return nil
},
}
func init() {
daemonCmd.AddCommand(daemonStartCmd)
daemonStartCmd.Flags().StringVarP(&c.RulesFile, "file", "f", "rules.yml", "rules file")
}
/*
Copyright © 2022 Alessio Greggi
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cmd
import (
"fmt"
"github.com/alegrey91/fwdctl/internal/daemon"
"github.com/spf13/cobra"
)
// daemonStopCmd represents the daemon command
var daemonStopCmd = &cobra.Command{
Use: "stop",
Short: "Stop fwdctl daemon",
Long: ``,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("stopping daemon")
daemon.Stop()
},
}
func init() {
daemonCmd.AddCommand(daemonStopCmd)
}
/*
Copyright © 2022 Alessio Greggi
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cmd
import (
"fmt"
"os"
//"os"
c "github.com/alegrey91/fwdctl/internal/constants"
"github.com/alegrey91/fwdctl/internal/rules"
iptables "github.com/alegrey91/fwdctl/pkg/iptables"
"github.com/spf13/cobra"
)
var (
ruleId int
file string
)
// deleteCmd represents the delete command
var deleteCmd = &cobra.Command{
Use: "delete",
Aliases: []string{"rm"},
Short: "Delete forward",
Long: `Delete forward by passing a rule file or rule id.
`,
Example: c.ProgramName + " delete -n 2",
RunE: func(cmd *cobra.Command, args []string) error {
ipt, err := iptables.NewIPTablesInstance()
if err != nil {
return fmt.Errorf("unable to get iptables instance: %v", err)
}
// Delete rule number
if cmd.Flags().Lookup("id").Changed {
if err := ipt.DeleteForwardById(ruleId); err != nil {
return fmt.Errorf("delete forward by ID: %v", err)
}
return nil
}
// Loop over file content and delete rule one-by-one.
if cmd.Flags().Lookup("file").Changed {
if err := deleteFromFile(ipt, file); err != nil {
return fmt.Errorf("delete from file: %v", err)
}
return nil
}
if cmd.Flags().Lookup("all").Changed {
if err := ipt.DeleteAllForwards();err != nil {
return fmt.Errorf("delete all forwards: %v", err)
}
return nil
}
if err = deleteFromFile(ipt, file);err != nil {
return fmt.Errorf("delete from file: %v", err)
}
return nil
},
}
func init() {
rootCmd.AddCommand(deleteCmd)
deleteCmd.Flags().IntVarP(&ruleId, "id", "n", 0, "delete rules through ID")
deleteCmd.Flags().StringVarP(&file, "file", "f", "rules.yml", "delete rules through file")
deleteCmd.Flags().BoolP("all", "a", false, "delete all rules")
deleteCmd.MarkFlagsMutuallyExclusive("id", "file", "all")
}
func deleteFromFile(ipt *iptables.IPTablesInstance, file string) error {
rulesContent, err := os.Open(file)
if err != nil {
return fmt.Errorf("error opening file: %v", err)
}
rulesFile, err := rules.NewRuleSetFromFile(rulesContent)
if err != nil {
return fmt.Errorf("error instantiating ruleset from file: %v", err)
}
for _, rule := range rulesFile.Rules {
err := ipt.DeleteForwardByRule(&rule)
if err != nil {
return fmt.Errorf("error deleting rule [%s %s %d %s %d]: %v", rule.Iface, rule.Proto, rule.Dport, rule.Saddr, rule.Sport, err)
}
}
return nil
}
/*
Copyright © 2022 Alessio Greggi
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cmd
import (
"fmt"
"github.com/spf13/cobra"
)
var outputFile string
// generateCmd represents the generate command
var generateCmd = &cobra.Command{
Use: "generate",
Aliases: []string{"gen"},
Short: "generates templated files",
Long: `generates templated file for fwdtcl
`,
RunE: func(cmd *cobra.Command, args []string) error {
fmt.Println(cmd.Help())
return nil
},
}
func init() {
rootCmd.AddCommand(generateCmd)
}
/*
Copyright © 2022 Alessio Greggi
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cmd
import (
"fmt"
"github.com/spf13/cobra"
"github.com/alegrey91/fwdctl/internal/template"
rt "github.com/alegrey91/fwdctl/internal/template/rules_template"
)
// generateRulesCmd represents the generateRules command
var generateRulesCmd = &cobra.Command{
Use: "rules",
Short: "generates empty rules file",
Long: `generates empty rules file
`,
RunE: func(cmd *cobra.Command, args []string) error {
rules := rt.NewRules()
if err := template.GenerateTemplate(rules, outputFile); err != nil {
return fmt.Errorf("generating template: %w", err)
}
return nil
},
}
func init() {
generateCmd.AddCommand(generateRulesCmd)
generateRulesCmd.PersistentFlags().StringVarP(&outputFile, "output-path", "O", "", "output path")
_ = generateRulesCmd.MarkPersistentFlagRequired("output-path")
}
/*
Copyright © 2022 Alessio Greggi
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cmd
import (
"fmt"
c "github.com/alegrey91/fwdctl/internal/constants"
"github.com/alegrey91/fwdctl/internal/template"
st "github.com/alegrey91/fwdctl/internal/template/systemd_template"
"github.com/spf13/cobra"
)
var installationPath string
var serviceType string
// generateSystemdCmd represents the generateSystemd command
var generateSystemdCmd = &cobra.Command{
Use: "systemd",
Short: "generates systemd service file",
Long: `generates systemd service file to run fwdctl at boot
`,
RunE: func(cmd *cobra.Command, args []string) error {
systemd, err := st.NewSystemdService(serviceType, installationPath, c.RulesFile)
if err != nil {
return fmt.Errorf("cannot create systemd service: %v", err)
}
if err = template.GenerateTemplate(systemd, outputFile); err != nil {
return fmt.Errorf("generating templated file: %v", err)
}
return nil
},
}
func init() {
generateCmd.AddCommand(generateSystemdCmd)
generateSystemdCmd.Flags().StringVarP(&installationPath, "installation-path", "p", "/usr/local/bin", "fwdctl installation path")
generateSystemdCmd.Flags().StringVarP(&c.RulesFile, "file", "f", "rules.yml", "rules file path")
generateSystemdCmd.Flags().StringVarP(&serviceType, "type", "t", "oneshot", "systemd service type [oneshot, fork]")
generateSystemdCmd.PersistentFlags().StringVarP(&outputFile, "output-path", "O", "", "output path")
_ = generateSystemdCmd.MarkPersistentFlagRequired("output-path")
}
/*
Copyright © 2022 Alessio Greggi
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cmd
import (
"fmt"
c "github.com/alegrey91/fwdctl/internal/constants"
"github.com/alegrey91/fwdctl/internal/printer"
iptables "github.com/alegrey91/fwdctl/pkg/iptables"
"github.com/spf13/cobra"
)
var (
format string
)
// listCmd represents the list command
var listCmd = &cobra.Command{
Use: "list",
Aliases: []string{"ls"},
Short: "list forwards",
Long: `list forwards made with iptables`,
Example: c.ProgramName + "list -o table",
RunE: func(cmd *cobra.Command, args []string) error{
ipt, err := iptables.NewIPTablesInstance()
if err != nil {
return fmt.Errorf("getting iptables instance: %v", err)
}
ruleList, err := ipt.ListForward(format)
if err != nil {
return fmt.Errorf("listing rules: %v", err)
}
p := printer.NewPrinter(format)
if err = p.PrintResult(ruleList);err != nil {
return fmt.Errorf("printing result: %v", err)
}
return nil
},
}
func init() {
rootCmd.AddCommand(listCmd)
listCmd.Flags().StringVarP(&format, "output", "o", "table", "output format [table]")
}
/*
Copyright © 2022 Alessio Greggi
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
)
// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
Use: "fwdctl",
Short: "fwdctl is a simple and intuitive CLI to manage IPTables forwards",
Long: ``,
// Uncomment the following line if your bare application
// has an action associated with it:
// Run: func(cmd *cobra.Command, args []string) { },
}
// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
func init() {
cobra.OnInitialize(initConfig)
}
// initConfig reads in config file and ENV variables if set.
func initConfig() {
}
/*
Copyright © 2022 Alessio Greggi
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cmd
import (
"fmt"
c "github.com/alegrey91/fwdctl/internal/constants"
"github.com/spf13/cobra"
)
// versionCmd represents the version command
var versionCmd = &cobra.Command{
Use: "version",
Short: "A brief description of your command",
Long: `A longer description that spans multiple lines and likely contains examples
and usage of using your command. For example:
Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Printf("%s\n", c.Version)
},
}
func init() {
rootCmd.AddCommand(versionCmd)
}
package daemon
import (
"log"
"os"
"os/signal"
"syscall"
"github.com/alegrey91/fwdctl/internal/rules"
"github.com/alegrey91/fwdctl/pkg/iptables"
"github.com/fsnotify/fsnotify"
"github.com/spf13/viper"
)
var (
infoLogger *log.Logger
errorLogger *log.Logger
)
func init() {
// Initialize different loggers
infoLogger = log.New(os.Stdout, "INFO: ", log.Ldate|log.Ltime|log.Lshortfile)
errorLogger = log.New(os.Stderr, "ERROR: ", log.Ldate|log.Ltime|log.Lshortfile)
}
// Print daemon banner
func banner() string {
return `
┌─┐┬ ┬┌┬┐┌─┐┌┬┐┬
├┤ │││ │││ │ │
└ └┴┘─┴┘└─┘ ┴ ┴─┘
┌┬┐┌─┐┌─┐┌┬┐┌─┐┌┐┌
││├─┤├┤ ││││ ││││
─┴┘┴ ┴└─┘┴ ┴└─┘┘└┘`
}
// Start run fwdctl in daemon mode
// The flow has the following steps:
// - Open the rules file
// - Create all the defined forwards
// - Start listening on rules file changes
// - When file changes:
// - Calculate the Diff between old and new ruleset
// - Delete unwanted forwards
// - Create wanted forawrds
//
// - Listen for SIGTERM signals to gracefuly shutdown
// - When SIGTERM signal occurs:
// - Delete all the applied forwards
// - Shutdown the daemon
func Start(ipt *iptables.IPTablesInstance, rulesFile string) int {
infoLogger.Println(banner())
err := createPidFile()
if err != nil {
errorLogger.Println(err)
return 1
}
defer func() {
err = removePidFile()
}()
infoLogger.Println("PID file created")
// preparing rule set from rules file
rulesContent, err := os.Open(rulesFile)
if err != nil {
errorLogger.Printf("error opening file: %v", err)
return 1
}
ruleSet, err := rules.NewRuleSetFromFile(rulesContent)
if err != nil {
errorLogger.Println(err)
return 1
}
// apply all the rules present in rulesFile
for ruleId, rule := range ruleSet.Rules {
err = ipt.CreateForward(&rule)
if err != nil {
infoLogger.Printf("rule %s - %v\n", ruleId, err)
}
}
infoLogger.Println("rules from file have been applied")
// preparing viper module to manage rules file
v := viper.New()
v.SetConfigFile(rulesFile)
v.OnConfigChange(func(e fsnotify.Event) {
infoLogger.Println("configuration has changed")
rulesContent, err := os.Open(rulesFile)
if err != nil {
errorLogger.Printf("error opening file: %v", err)
return
}
newRuleSet, err := rules.NewRuleSetFromFile(rulesContent)
if err != nil {
errorLogger.Println(err)
return
}
rsd := rules.Diff(ruleSet, newRuleSet)
// delete all the rules to be removed
for _, rule := range rsd.ToRemove {
err = ipt.DeleteForwardByRule(rule)
if err != nil {
errorLogger.Println(err)
}
}
// create all the rules to be added
for _, rule := range rsd.ToAdd {
err = ipt.CreateForward(rule)
if err != nil {
errorLogger.Println(err)
}
}
// set the new rule set as the current one
ruleSet = newRuleSet
})
v.WatchConfig()
sigChnl := make(chan os.Signal, 1)
signal.Notify(sigChnl, syscall.SIGTERM)
exitcChnl := make(chan bool, 1)
go func() {
for {
select {
case <-sigChnl:
// flush rules before exit
err := ipt.DeleteAll()
if err != nil {
errorLogger.Println(err)
}
infoLogger.Println("daemon stopped")
exitcChnl <- true
default:
continue
}
}
}()
<-exitcChnl
return 0
}
// Stop send a SIGTERM signal to the daemon process
func Stop() {
infoLogger.Println("stopping daemon")
pid, err := readPidFile()
if err != nil {
errorLogger.Println(err)
}
err = syscall.Kill(pid, syscall.SIGTERM)
if err != nil {
errorLogger.Println(err)
}
}
package daemon
import (
"os"
"strconv"
)
var (
pidFilePath = "/tmp/fwdctl.pid"
)
// Create PID file
func createPidFile() error {
pid := []byte(strconv.Itoa(os.Getpid()))
err := os.WriteFile(pidFilePath, pid, 0644)
if err != nil {
return err
}
return nil
}
// Retrieve PID by reading file content
func readPidFile() (int, error) {
pidB, err := os.ReadFile(pidFilePath)
if err != nil {
return 0, err
}
pid, err := strconv.Atoi(string(pidB))
if err != nil {
return 0, err
}
return pid, nil
}
// Remove PID file
func removePidFile() error {
err := os.Remove(pidFilePath)
if err != nil {
return err
}
return nil
}
package printer
import (
"encoding/json"
"fmt"
"github.com/alegrey91/fwdctl/internal/rules"
"github.com/alegrey91/fwdctl/pkg/iptables"
)
type Json struct {
}
func NewJson() *Json {
return &Json{}
}
func (j *Json) PrintResult(ruleList map[int]string) error {
rules := rules.NewRuleSet()
for _, rule := range ruleList {
jsonRule, err := iptables.ExtractRuleInfo(rule)
if err != nil {
continue
}
rules.Add(*jsonRule)
}
val, err := json.MarshalIndent(rules.Rules, "", " ")
if err != nil {
return err
}
fmt.Println(string(val))
return nil
}
package printer
type Printer interface {
PrintResult(ruleList map[int]string) error
}
func NewPrinter(printFormat string) Printer {
switch printFormat {
case "table":
return NewTable()
case "json":
return NewJson()
case "yaml":
return NewYaml()
default:
return NewTable()
}
}
package printer
import (
"fmt"
"os"
"github.com/alegrey91/fwdctl/pkg/iptables"
"github.com/olekukonko/tablewriter"
)
type Table struct {
}
func NewTable() *Table {
return &Table{}
}
func (t *Table) PrintResult(ruleList map[int]string) error {
table := tablewriter.NewWriter(os.Stdout)
table.SetHeader([]string{"number", "interface", "protocol", "external port", "internal ip", "internal port"})
for ruleId, rule := range ruleList {
tabRule, err := iptables.ExtractRuleInfo(rule)
if err != nil {
continue
}
tabRow := []string{
fmt.Sprintf("%d", ruleId),
tabRule.Iface,
tabRule.Proto,
fmt.Sprintf("%d", tabRule.Dport),
tabRule.Saddr,
fmt.Sprintf("%d", tabRule.Sport),
}
table.Append(tabRow)
}
table.Render()
return nil
}
package printer
import (
"fmt"
yaml "gopkg.in/yaml.v3"
"github.com/alegrey91/fwdctl/internal/rules"
"github.com/alegrey91/fwdctl/pkg/iptables"
)
type Yaml struct {
}
func NewYaml() *Yaml {
return &Yaml{}
}
func (y *Yaml) PrintResult(ruleList map[int]string) error {
rules := rules.NewRuleSet()
for _, rule := range ruleList {
jsonRule, err := iptables.ExtractRuleInfo(rule)
if err != nil {
continue
}
rules.Add(*jsonRule)
}
val, err := yaml.Marshal(rules.Rules)
if err != nil {
return err
}
fmt.Println(string(val))
return nil
}
package rules
import (
"crypto/md5"
"encoding/hex"
"fmt"
"io"
"github.com/alegrey91/fwdctl/pkg/iptables"
"gopkg.in/yaml.v2"
)
func NewRuleSet() *RuleSet {
return &RuleSet{
Rules: make(map[string]iptables.Rule),
}
}
// NewRuleSet return the struct that contains informations about rules
func NewRuleSetFromFile(file io.Reader) (*RuleSet, error) {
// Read rules from file
rulesFile, err := io.ReadAll(file)
if err != nil {
return nil, fmt.Errorf("error reading file: %v", err)
}
// Retrieve rules to fill RuleSet
rules := supportRuleSet{}
err = yaml.Unmarshal(rulesFile, &rules)
if err != nil {
return nil, fmt.Errorf("error unmarshaling content: %v", err)
}
// Fill RuleSet with rules taken from file
rs := NewRuleSet()
for _, rule := range rules.Rules {
ruleHash := hash(rule)
rs.Rules[ruleHash] = rule
}
return rs, nil
}
func hash(rule iptables.Rule) string {
md5.New()
ruleString := fmt.Sprintf("%s%s%d%s%d",
rule.Iface,
rule.Proto,
rule.Dport,
rule.Saddr,
rule.Sport,
)
hash := md5.Sum([]byte(ruleString))
return hex.EncodeToString(hash[:])
}
func (rs *RuleSet) GetHash(rule iptables.Rule) string {
return hash(rule)
}
func (rs *RuleSet) Add(rule iptables.Rule) {
ruleHash := hash(rule)
rs.Rules[ruleHash] = rule
}
func (rs *RuleSet) Remove(ruleHash string) {
delete(rs.Rules, ruleHash)
}
type RuleSetDiff struct {
ToRemove []*iptables.Rule
ToAdd []*iptables.Rule
}
// Diff method returns a *RuleSetDiff struct.
// It contains a list of Rule(s) to be added / remove
// in order to achieve the new RuleSet state.
func Diff(oldRS, newRS *RuleSet) *RuleSetDiff {
ruleSetDiff := &RuleSetDiff{}
// loop over old rules set, to find rules to be removed
for hash := range oldRS.Rules {
// if key in oldRules is not present in rs,
// then the old rule must be removed
if _, ok := newRS.Rules[hash]; !ok {
rule := oldRS.Rules[hash]
ruleSetDiff.ToRemove = append(ruleSetDiff.ToRemove, &rule)
}
}
// loop over new rules set, to find rules to be added
for hash := range newRS.Rules {
// if key in rs in not present in oldRs,
// then the new rule must be added
if _, ok := oldRS.Rules[hash]; !ok {
rule := newRS.Rules[hash]
ruleSetDiff.ToAdd = append(ruleSetDiff.ToAdd, &rule)
}
}
return ruleSetDiff
}
package rules_template
import (
_ "embed"
)
//go:embed rules.yml.tpl
var rulesTemplate string
var rulesTemplateName = "rules"
var rulesFileName = "rules.yml"
type Rule struct {
}
func NewRules() *Rule {
return &Rule{}
}
func (r *Rule) GetTemplateStruct() interface{} {
return r
}
func (r *Rule) GetFileContent() string {
return rulesTemplate
}
func (r *Rule) GetTemplateName() string {
return rulesTemplateName
}
func (r *Rule) GetFileName() string {
return rulesFileName
}
package systemd_template
import (
_ "embed"
"errors"
"fmt"
"os"
"path/filepath"
)
//go:embed fwdctl.service.tpl
var systemdTemplate string
var systemdTemplateName = "systemd"
var systemdFileName = "fwdctl.service"
var allowedServiceTypes = [2]string{"oneshot", "fork"}
type SystemdService struct {
ServiceType string
InstallationPath string
RulesFile string
}
func serviceTypeAllowed(st string) bool {
for _, ast := range allowedServiceTypes {
if ast == st {
return true
}
}
return false
}
func NewSystemdService(serviceType, installationPath, rulesFile string) (*SystemdService, error) {
// checks for systemd service type
if !serviceTypeAllowed(serviceType) {
return nil, fmt.Errorf("service type is not allowed: %s", serviceType)
}
// checks for installation path
if !filepath.IsAbs(installationPath) {
return nil, fmt.Errorf("installation path is not absolute: %s", installationPath)
}
if _, err := os.Stat(installationPath); errors.Is(err, os.ErrNotExist) {
return nil, fmt.Errorf("installation path does not exist: %s", installationPath)
}
// checks for rules file
if !filepath.IsAbs(rulesFile) {
return nil, fmt.Errorf("rules file path is not absolute: %s", rulesFile)
}
if _, err := os.Stat(rulesFile); errors.Is(err, os.ErrNotExist) {
return nil, fmt.Errorf("rules file path does not exist: %s", rulesFile)
}
return &SystemdService{
ServiceType: serviceType,
InstallationPath: installationPath,
RulesFile: rulesFile,
}, nil
}
func (s *SystemdService) GetTemplateStruct() interface{} {
return s
}
func (s *SystemdService) GetFileContent() string {
return systemdTemplate
}
func (s *SystemdService) GetTemplateName() string {
return systemdTemplateName
}
func (s *SystemdService) GetFileName() string {
return systemdFileName
}
package template
import (
"fmt"
"os"
"path/filepath"
"strings"
"text/template"
)
type Generator interface {
GetTemplateStruct() interface{}
GetFileContent() string
GetTemplateName() string
GetFileName() string
}
func GenerateTemplate(g Generator, outputPath string) error {
tpl, err := template.New(g.GetTemplateName()).Parse(g.GetFileContent())
if err != nil {
return fmt.Errorf("error getting template instance: %v", err)
}
if !filepath.IsAbs(outputPath) {
return fmt.Errorf("output path is not absolute: %s", outputPath)
}
// if last char of outputPath is "/" we want to remove,
// so the final output will be cleaned.
// this way: /root/template.file instead of /root//template.file
if outputPath != "/" && outputPath[len(outputPath)-1:] == "/" {
outputPath = strings.TrimSuffix(outputPath, "/")
}
outFile, err := os.Create(filepath.Join(outputPath, g.GetFileName()))
if err != nil {
return fmt.Errorf("error creating output file: %v", err)
}
err = tpl.Execute(outFile, g.GetTemplateStruct())
if err != nil {
return fmt.Errorf("error writing content into file: %v", err)
}
return nil
}
/*
Copyright © 2022 Alessio Greggi
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import "github.com/alegrey91/fwdctl/cmd"
func main() {
cmd.Execute()
}
package iptables
import (
"fmt"
"strconv"
"strings"
"github.com/coreos/go-iptables/iptables"
)
var (
label string = "fwdctl"
)
type IPTablesInstance struct {
*iptables.IPTables
}
func NewIPTablesInstance() (*IPTablesInstance, error) {
ipt := IPTablesInstance{}
iptables, err := getIPTablesInstance()
if err != nil {
return nil, fmt.Errorf("failed: %v", err)
}
ipt.IPTables = iptables
return &ipt, nil
}
func (ipt *IPTablesInstance) ValidateForward(rule *Rule) error {
return validate(rule.Iface, rule.Proto, rule.Dport, rule.Saddr, rule.Sport)
}
func (ipt *IPTablesInstance) CreateForward(rule *Rule) error {
// example rule:
// iptables -t nat -A PREROUTING -i eth0 -p tcp -m tcp --dport 3000 -j DNAT --to-destination 192.168.199.105:80
// check if input interface exists on the system
ifaceExits, err := interfaceExists(rule.Iface)
if err != nil {
return fmt.Errorf("error reading interfaces: %v", err)
}
if !ifaceExits {
return fmt.Errorf("interface %s does not exists", rule.Iface)
}
// check if provided rule already exists
ruleExists, err := ipt.Exists(FwdTable, FwdChain, rule.String()...)
if err != nil {
return fmt.Errorf("%v", err)
}
if ruleExists {
return fmt.Errorf("rule already exists")
}
// apply provided rule
err = ipt.AppendUnique(FwdTable, FwdChain, rule.String()...)
if err != nil {
return fmt.Errorf("rule failed: %v", err)
}
return nil
}
func (ipt *IPTablesInstance) ListForward(outputFormat string) (map[int]string, error) {
ruleList, err := ipt.List(FwdTable, FwdChain)
if err != nil {
return nil, fmt.Errorf("failed listing rules: %v", err)
}
// check listed rules are tagged with custom tag
fwdRules := make(map[int]string)
for ruleId, rule := range ruleList {
if strings.Contains(rule, label) {
fwdRules[ruleId] = rule
}
}
return fwdRules, nil
}
func (ipt *IPTablesInstance) DeleteForwardById(ruleId int) error {
// delete rule
err := ipt.Delete(FwdTable, FwdChain, strconv.Itoa(ruleId))
if err != nil {
return fmt.Errorf("failed deleting rule n. %d\nerr: %v", ruleId, err)
}
return nil
}
func (ipt *IPTablesInstance) DeleteForwardByRule(rule *Rule) error {
// TODO: create function to return []string with packed rule, passing iface, proto, etc as arguments.
err := ipt.Delete(FwdTable, FwdChain, rule.String()...)
if err != nil {
return fmt.Errorf("failed deleting rule: '%s'\n err: %v", rule.String(), err)
}
return nil
}
func (ipt *IPTablesInstance) DeleteAllForwards() error {
ruleList, err := ipt.List(FwdTable, FwdChain)
if err != nil {
return fmt.Errorf("failed listing rules: %v", err)
}
// check listed rules are tagged with custom tag
fwdRules := make(map[int]string)
for ruleId, rule := range ruleList {
if strings.Contains(rule, label) {
fwdRules[ruleId] = rule
}
}
for _, rule := range fwdRules {
r, err := ExtractRuleInfo(rule)
if err != nil {
return fmt.Errorf("error extracting rule info: %v", err)
}
err = ipt.Delete(FwdTable, FwdChain, r.String()...)
if err != nil {
return fmt.Errorf("error deleting rule: %v", err)
}
}
return nil
}
package iptables
import (
"net"
)
func interfaceExists(iface string) (bool, error) {
ifi, err := net.InterfaceByName(iface)
if err != nil {
return false, err
}
if ifi != nil {
return true, nil
}
return false, nil
}
package iptables
import (
"fmt"
"strconv"
"strings"
)
type Rule struct {
Iface string `json:"iface" yaml:"iface" default:"lo"`
Proto string `json:"proto" yaml:"proto" default:"tcp"`
Dport int `json:"dport" yaml:"dport"`
Saddr string `json:"saddr" yaml:"saddr"`
Sport int `json:"sport" yaml:"sport"`
Comment string `json:"comment,omitempty" yaml:"comment,omitempty"`
}
func NewRule(iface string, proto string, dport int, saddr string, sport int) *Rule {
return &Rule{
Iface: iface,
Proto: proto,
Dport: dport,
Saddr: saddr,
Sport: sport,
Comment: label,
}
}
// ExtractRuleInfo extract forward information from rule
// if it matches the requirements.
// Returns the Rule struct and error
func ExtractRuleInfo(rawRule string) (*Rule, error) {
// extract rules info:
// -t nat -A PREROUTING -i eth0 -p tcp -m tcp --dport 3000 -j DNAT --to-destination 192.168.199.105:80
// result:
// Rule{Iface: eth0, Proto: tcp, Dport: 3000, Saddr: 192.168.199.105, Sport: 80}
ruleSplit := strings.Split(rawRule, " ")
rule := &Rule{}
for id, arg := range ruleSplit {
switch arg {
case "-i":
rule.Iface = ruleSplit[id+1]
case "-p":
rule.Proto = ruleSplit[id+1]
case "--dport":
dport, err := strconv.Atoi(ruleSplit[id+1])
if err != nil {
return nil, fmt.Errorf("error converting string '%s' to int: %v", ruleSplit[id+1], err)
}
rule.Dport = dport
case "--to-destination":
rule.Saddr = strings.Split(ruleSplit[id+1], ":")[0]
sport, err := strconv.Atoi(strings.Split(ruleSplit[id+1], ":")[1])
if err != nil {
return nil, fmt.Errorf("error converting string '%s' to int: %v", ruleSplit[id+1], err)
}
rule.Sport = sport
}
}
if rule.Iface == "" {
return nil, fmt.Errorf("missing iface value")
}
if rule.Proto == "" {
return nil, fmt.Errorf("missing proto value")
}
if rule.Dport == 0 {
return nil, fmt.Errorf("missing dport value")
}
if rule.Saddr == "" {
return nil, fmt.Errorf("missing saddr value")
}
if rule.Sport == 0 {
return nil, fmt.Errorf("missing sport value")
}
return rule, nil
}
// String returns a list of string that compose the iptables rule.
// Eg: -t nat -A PREROUTING -i eth0 -p tcp -m tcp --dport 3000 -m comment --comment fwdctl -j DNAT --to-destination 192.168.199.105:80
func (rule *Rule) String() []string {
return []string{
"-i", rule.Iface,
"-p", rule.Proto,
"-m", rule.Proto,
"--dport", fmt.Sprintf("%d", rule.Dport),
"-m", "comment", "--comment", label,
"-j", FwdTarget,
"--to-destination", rule.Saddr + ":" + fmt.Sprintf("%d", rule.Sport),
}
}
package iptables
import (
"sync"
"github.com/coreos/go-iptables/iptables"
)
var once sync.Once
type single *iptables.IPTables
var singleInstance single
// getIPTablesInstance create a singletone instance for iptables.New()
func getIPTablesInstance() (*iptables.IPTables, error) {
var err error
if singleInstance == nil {
once.Do(func() {
singleInstance, err = iptables.New()
})
}
return singleInstance, err
}
package iptables
import (
"fmt"
"net"
)
func validateIface(iface string) error {
if iface == "" {
return fmt.Errorf("name is empty")
}
ifaces, err := net.Interfaces()
if err != nil {
return fmt.Errorf("error: %v", err)
}
found := false
for _, i := range ifaces {
if i.Name == iface {
found = true
}
}
if !found {
return fmt.Errorf("not found")
}
return nil
}
func validateProto(proto string) error {
if proto == "" {
return fmt.Errorf("protocol name is empty")
}
if (proto != "tcp") && (proto != "udp") && (proto != "icmp") {
return fmt.Errorf("protocol name not allowed")
}
return nil
}
func validatePort(port int) error {
if port < 1 || port > 65535 {
return fmt.Errorf("port number not allowed")
}
return nil
}
func validateAddress(address string) error {
// not a valid check for now.
if address == "" {
return fmt.Errorf("address is empty")
}
return nil
}
// validate returns both bool and error.
// The boolean return true in case the rule passes all checks.
// In case it does not, then the error will describe the problem.
func validate(iface string, proto string, dport int, saddr string, sport int) error {
err := validateIface(iface)
if err != nil {
return fmt.Errorf("interface: '%s' %v", iface, err)
}
err = validateProto(proto)
if err != nil {
return fmt.Errorf("protocol: '%s' %v", proto, err)
}
err = validatePort(dport)
if err != nil {
return fmt.Errorf("destination port: '%d' %v", dport, err)
}
err = validateAddress(saddr)
if err != nil {
return fmt.Errorf("source address: '%s' %v", saddr, err)
}
err = validatePort(sport)
if err != nil {
return fmt.Errorf("source port: '%d' %v", sport, err)
}
return nil
}
package main
import (
"crypto/rand"
"encoding/base64"
"fmt"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"github.com/alegrey91/fwdctl/pkg/iptables"
goiptables "github.com/coreos/go-iptables/iptables"
"github.com/rogpeppe/go-internal/testscript"
)
func fwdExists(ts *testscript.TestScript, neg bool, args []string) {
if len(args) < 5 {
ts.Fatalf("syntax: fwd_exists iface proto dest_port src_addr src_port")
}
ipt, err := goiptables.New()
if err != nil {
ts.Fatalf("error creating iptables instance: %q", err)
}
ruleSpec := []string{
"-i", args[0], // interface
"-p", args[1], // protocol
"-m", args[1], // protocol
"--dport", args[2], // destination-port
"-m", "comment", "--comment", "fwdctl",
"-j", iptables.FwdTarget, // target (DNAT)
"--to-destination", args[3] + ":" + args[4], // source-address / source-port
}
exists, err := ipt.Exists(iptables.FwdTable, iptables.FwdChain, ruleSpec...)
if err != nil {
ts.Fatalf("error checking rule: %v", err)
}
if neg && !exists {
ts.Logf("forward doesn't exist")
return
}
if !exists {
ts.Fatalf("forward doesn't exist")
}
}
//nolint:all
func execCmd(ts *testscript.TestScript, neg bool, args []string) {
var backgroundSpecifier = regexp.MustCompile(`^&([a-zA-Z_0-9]+&)?$`)
uuid := getRandomString()
workDir, err := os.Getwd()
if err != nil {
ts.Fatalf("unable to find work dir: %v", err)
}
customCommand := []string{
"/usr/local/bin/harpoon",
"capture",
"-f",
"main.main",
"--save",
"--directory",
fmt.Sprintf("%s/integration-test-syscalls", workDir),
"--include-cmd-stdout",
"--include-cmd-stderr",
"--name",
fmt.Sprintf("main_main_%s", uuid),
"--",
}
// find binary path for primary command
cmdPath, err := exec.LookPath(args[0])
if err != nil {
ts.Fatalf("unable to find binary path for %s: %v", args[0], err)
}
args[0] = cmdPath
customCommand = append(customCommand, args...)
ts.Logf("executing tracing command: %s", strings.Join(customCommand, " "))
// check if command has '&' as last char to be ran in background
if len(args) > 0 && backgroundSpecifier.MatchString(args[len(args)-1]) {
_, err = execBackground(ts, customCommand[0], customCommand[1:len(args)-1]...)
} else {
err = ts.Exec(customCommand[0], customCommand[1:]...)
}
if err != nil {
ts.Logf("[%v]\n", err)
if !neg {
ts.Fatalf("unexpected go command failure")
}
} else {
if neg {
ts.Fatalf("unexpected go command success")
}
}
}
func execBackground(ts *testscript.TestScript, command string, args ...string) (*exec.Cmd, error) {
cmd := exec.Command(command, args...)
path := ts.MkAbs(".")
dir, _ := filepath.Split(path)
var stdoutBuf, stderrBuf strings.Builder
cmd.Dir = dir
cmd.Env = append(cmd.Env, "PWD="+dir)
cmd.Stdout = &stdoutBuf
cmd.Stderr = &stderrBuf
return cmd, cmd.Start()
}
//nolint:all
func getRandomString() string {
b := make([]byte, 4) // 4 bytes will give us 6 base64 characters
_, err := rand.Read(b)
if err != nil {
return ""
}
randomString := base64.URLEncoding.EncodeToString(b)[:6]
return randomString
}
func customCommands() map[string]func(ts *testscript.TestScript, neg bool, args []string) {
return map[string]func(ts *testscript.TestScript, neg bool, args []string){
// fwd_exists check that the given forward exists
// invoke as "fwd_exists iface proto dest_port src_addr src_port"
"fwd_exists": fwdExists,
"exec_cmd": execCmd,
}
}