Skip to content

Commit

Permalink
init 🥳
Browse files Browse the repository at this point in the history
  • Loading branch information
muckelba committed Oct 11, 2024
0 parents commit 39ec72a
Show file tree
Hide file tree
Showing 7 changed files with 293 additions and 0 deletions.
43 changes: 43 additions & 0 deletions .github/workflows/build-and-deploy.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: Build and deploy

on:
push:
branches:
- main

env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}

jobs:
docker:
runs-on: ubuntu-latest
steps:
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
- name: Build and push
uses: docker/build-push-action@v6
with:
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
- name: Deploy to server
uses: darnfish/[email protected]
with:
url: "${{ secrets.WATCHTOWER_URL }}"
api_token: "${{ secrets.WATCHTOWER_API_TOKEN }}"
images: "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}"
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
ipv6_address.txt
.env
four2six
compose.override.yaml
16 changes: 16 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Build
FROM --platform=$BUILDPLATFORM golang:1.23-alpine AS build
ARG TARGETOS
ARG TARGETARCH

WORKDIR /app
COPY go.mod main.go ./
RUN GOOS=${TARGETOS} GOARCH=${TARGETARCH} CGO_ENABLED=0 go build

# Execute
FROM alpine

WORKDIR /app
COPY --from=build /app/four2six four2six

CMD ["./four2six"]
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Four2Six

A tool to forward IPv4 traffic to an IPv6 destination. It can update its destination with a webhook.

I've built this tool to solve a very specific problem. My ISP does not provide me with a dual stack internet connection so the only way to access my home network from the internet is by using IPv6. This tool runs on a cloud server and listens on every IPv4 request to my home network and forwards it via IPv6.
20 changes: 20 additions & 0 deletions compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
services:
four2six:
image: ghcr.io/muckelba/four2six:latest
restart: unless-stopped
networks:
- default
env_file:
- .env
ports:
- 8080:8080
- 8081:8081
volumes:
- ./config:/app/config

networks:
default:
enable_ipv6: true
ipam:
config:
- subnet: 2001:db8::/64
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module github.com/muckelba/four2six

go 1.23.0
202 changes: 202 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
package main

import (
"encoding/json"
"fmt"
"io"
"log"
"net"
"net/http"
"os"
"path/filepath"
"sync"
)

// Config holds the runtime configuration.
type Config struct {
IPv6Address string
IPv6Port string
IPv4Port string
FilePath string
ConfigDir string
WebhookToken string
WebhookListener string
mu sync.RWMutex
}

// forward forwards traffic between the source and destination connections.
func forward(src, dst net.Conn) {
defer src.Close()
defer dst.Close()

// Use io.Copy to forward data in both directions.
go io.Copy(src, dst)
io.Copy(dst, src)
}

// saveIPv6Address saves the current IPv6 address to a file.
func (config *Config) saveIPv6Address() error {
config.mu.RLock()
defer config.mu.RUnlock()

file, err := os.Create(config.FilePath)
if err != nil {
return err
}
defer file.Close()

_, err = file.WriteString(config.IPv6Address)
if err != nil {
return err
}

return nil
}

// loadIPv6Address loads the IPv6 address from a file.
func (config *Config) loadIPv6Address() error {

// Create a config/ dir if it's not existing
err := os.MkdirAll(config.ConfigDir, os.ModePerm)
if err != nil {
return err
}

file, err := os.Open(config.FilePath)
if err != nil {
return err
}
defer file.Close()

var ipv6Addr string
_, err = fmt.Fscanf(file, "%s", &ipv6Addr)
if err != nil {
return err
}

config.mu.Lock()
config.IPv6Address = ipv6Addr
config.mu.Unlock()

return nil
}

// updateIPv6Address handles the webhook to update the IPv6 address.
func updateIPv6Address(config *Config) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Check the token.
token := r.Header.Get("Authorization")
if token != fmt.Sprintf("Bearer %s", config.WebhookToken) {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}

// Parse the request body.
var body struct {
IPv6Address string `json:"ipv6_address"`
}

err := json.NewDecoder(r.Body).Decode(&body)
if err != nil || body.IPv6Address == "" {
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}

// Update the IPv6 address and save to disk.
config.mu.Lock()
config.IPv6Address = body.IPv6Address
config.mu.Unlock()

err = config.saveIPv6Address()
if err != nil {
http.Error(w, "Failed to save IPv6 address", http.StatusInternalServerError)
return
}

logLine := fmt.Sprintf("IPv6 address updated to %s", body.IPv6Address)
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, logLine)
log.Print(logLine)
}
}

func main() {
token := os.Getenv("WEBHOOK_TOKEN")
if token == "" {
log.Fatal("WEBHOOK_TOKEN environment variable not set")
}

ipv6DestinationPort := os.Getenv("DEST_PORT")
if ipv6DestinationPort == "" {
ipv6DestinationPort = "8080" // Default destination port if not set.
}

ipv4SourcePort := os.Getenv("SRC_PORT")
if ipv4SourcePort == "" {
ipv4SourcePort = ":8080" // Default source port if not set.
}

webhookListener := os.Getenv("WEBHOOK_LISTENER")
if webhookListener == "" {
webhookListener = ":8081" // Default webhook listener port.
}

configPath := "config" // Name of the config directory

// Initial configuration.
config := &Config{
IPv6Address: "2001:db8::1", // Default IPv6 address.
IPv6Port: ipv6DestinationPort,
WebhookToken: token,
ConfigDir: filepath.Join(".", configPath),
FilePath: filepath.Join(configPath, "ipv6_address.txt"),
WebhookListener: webhookListener,
IPv4Port: ipv4SourcePort,
}

// Load IPv6 address from the file if it exists.
if err := config.loadIPv6Address(); err != nil {
log.Printf("Failed to load IPv6 address from file: %v. Using default.", err)
}

// Start the HTTP server to listen for webhook updates.
http.HandleFunc("/update", updateIPv6Address(config))
go func() {
log.Printf("Starting webhook server on %s\n", config.WebhookListener)
log.Fatal(http.ListenAndServe(config.WebhookListener, nil))
}()

// Listen for incoming connections on the IPv4 address and port.
listener, err := net.Listen("tcp4", config.IPv4Port) // Listening on IPv4, specified port.
if err != nil {
log.Fatalf("Error listening on IPv4 address: %v", err)
}
defer listener.Close()
log.Printf("Listening on %s for IPv4 connections...\n", config.IPv4Port)

for {
// Accept incoming connections.
srcConn, err := listener.Accept()
if err != nil {
log.Printf("Error accepting connection: %v", err)
continue
}

// Get the current IPv6 address in a thread-safe way.
config.mu.RLock()
ipv6Addr := config.IPv6Address
port := config.IPv6Port
config.mu.RUnlock()

// Dial the destination IPv6 address and port.
dstConn, err := net.Dial("tcp6", fmt.Sprintf("[%s]:%s", ipv6Addr, port))
if err != nil {
log.Printf("Error dialing IPv6 address: %v", err)
srcConn.Close()
continue
}

// Forward traffic between the two connections.
go forward(srcConn, dstConn)
}
}

0 comments on commit 39ec72a

Please sign in to comment.